Add project files.

This commit is contained in:
alejo committed 2026-10-25 03:29:04 -03:00
commit a9ae435321
52 files changed
+65378

No files matched your search

+359
View File
@@ -0,0 +1,359 @@
// hmap/hmap.h - Hammer++ map library. One include gives you everything:
// * the editable map model (brushes = BSP/CSG solids, mesh objects, entities, props)
// * geometry helpers and primitives
// * compile(): CSG + BSP collision tree + render batches (+ optional baked vertex lighting)
// * the .hmap binary format (load/save, source and/or compiled data)
// * FBX (binary 7.4) and OBJ export
//
// Conventions: right-handed, +Y up, forward = -Z. Triangles are CCW seen from the front.
// UVs use image space (v grows downwards). Euler angles are (pitch, yaw, roll) in degrees.
#pragma once
#include "hmath.h"
#include <cstdint>
#include <string>
#include <vector>
namespace hmap {
constexpr uint32_t kFormatVersion = 1;
struct Status {
bool ok = true;
std::string message;
explicit operator bool() const { return ok; }
static Status Ok() { return Status{}; }
static Status Fail(std::string m) { Status s; s.ok = false; s.message = std::move(m); return s; }
};
// ------------------------------------------------------------------ flags
enum Contents : uint32_t {
CONTENTS_SOLID = 1, // blocks movement + light, clips other brushes' hidden faces
CONTENTS_WATER = 2, // volume, rendered, does not block
CONTENTS_CLIP = 4, // invisible collision
CONTENTS_TRIGGER = 8, // invisible, non-blocking volume
CONTENTS_SKY = 16
};
enum ObjectFlags : uint32_t {
OBJ_HIDDEN = 1, // editor visibility only (still compiled)
OBJ_NOCOLLIDE = 2 // meshes: do not collide / cast baked shadows
};
enum FaceFlags : uint32_t { FACE_NODRAW = 1 };
// ------------------------------------------------------------------ model
struct UVInfo { // planar projection along the dominant normal axis
Vec2 offset{0, 0}; // world units
Vec2 scale{1, 1}; // 1 = one texture repeat per Material::tileSize
float rotation = 0; // degrees
};
struct Material {
std::string name; // unique key, e.g. "dev/orange"
std::string texture; // path relative to the asset root, may be empty
Vec2 tileSize{64, 64}; // world units per texture repeat
Vec3 color{1, 1, 1}; // tint
uint32_t flags = 0;
};
struct Face { // one side of a brush
Plane plane;
int32_t material = 0;
UVInfo uv;
uint32_t flags = 0;
};
// A convex solid defined as the intersection of half-spaces (Hammer/Quake style).
struct Brush {
uint32_t id = 0;
uint32_t owner = 0; // 0 = world, otherwise id of the brush entity that owns it
uint32_t contents = CONTENTS_SOLID;
uint32_t flags = 0;
std::vector<Face> faces;
};
struct MeshPoly { // convex polygon of a mesh object
std::vector<uint32_t> idx; // CCW from the front
std::vector<Vec2> uv; // empty = auto-project with `proj`
int32_t material = 0;
uint32_t smooth = 0; // 0 = flat shaded; same non-zero id = smooth together
UVInfo proj;
};
// Free-form polygon mesh ("detail" geometry): arbitrary topology, not part of the BSP tree.
struct MeshObject {
uint32_t id = 0;
uint32_t owner = 0;
uint32_t flags = 0;
std::string name;
std::vector<Vec3> verts;
std::vector<MeshPoly> polys;
};
struct KeyValue { std::string key, value; };
struct Entity { // point entities and props; brush entities own brushes via Brush::owner
uint32_t id = 0;
uint32_t flags = 0;
std::string classname;
Vec3 origin;
Vec3 angles; // pitch, yaw, roll (deg)
Vec3 scale{1, 1, 1};
std::vector<KeyValue> props;
const std::string* find(const std::string& key) const;
std::string get(const std::string& key, const std::string& def = "") const;
float getFloat(const std::string& key, float def) const;
int getInt(const std::string& key, int def) const;
bool getBool(const std::string& key, bool def) const;
Vec3 getVec3(const std::string& key, const Vec3& def) const;
Vec3 getColor(const std::string& key, const Vec3& def) const; // "255 128 0" -> 0..1
void set(const std::string& key, const std::string& value);
void erase(const std::string& key);
Mat34 transform() const; // translate * rotate * scale
Vec3 forward() const;
};
struct Settings {
Vec3 ambient{0.16f, 0.18f, 0.22f};
float gridSize = 16;
float unitsPerMeter = 64;
};
struct Map {
std::string name = "untitled";
Settings settings;
std::vector<Material> materials;
std::vector<Brush> brushes;
std::vector<MeshObject> meshes;
std::vector<Entity> entities;
uint32_t nextId = 1;
uint32_t newId() { return nextId++; }
int findMaterial(const std::string& name) const;
int addMaterial(const std::string& name, const std::string& texture = ""); // dedupes by name
Brush* findBrush(uint32_t id);
MeshObject* findMesh(uint32_t id);
Entity* findEntity(uint32_t id);
const Brush* findBrush(uint32_t id) const;
const MeshObject* findMesh(uint32_t id) const;
const Entity* findEntity(uint32_t id) const;
};
// ------------------------------------------------------------------ geometry
using Polygon = std::vector<Vec3>;
struct BrushGeometry {
std::vector<Polygon> polys; // parallel to Brush::faces; empty polygon = redundant face
AABB bounds;
std::vector<Vec3> points; // all polygon vertices
bool valid = false;
};
BrushGeometry buildBrushGeometry(const Brush& b);
AABB brushBounds(const Brush& b);
AABB meshBounds(const MeshObject& m);
Vec3 polyNormal(const Polygon& p);
// Raycasts (direction need not be normalised; t is in units of d). Return nearest hit.
bool rayBrush(const Brush& b, const Vec3& o, const Vec3& d, float& t, int& face);
bool rayMesh(const MeshObject& m, const Vec3& o, const Vec3& d, float& t, int& poly);
// Texture mapping shared by the editor and the compiler.
void uvAxes(const Vec3& normal, const UVInfo& uv, Vec3& u, Vec3& v);
Vec2 computeUV(const Vec3& p, const Vec3& normal, const UVInfo& uv, const Vec2& tileSize);
// Brush edits
void translateBrush(Brush& b, const Vec3& t, bool lockTexture = true);
void transformBrush(Brush& b, const Mat34& m);
void transformMesh(MeshObject& m, const Mat34& xf);
std::vector<Brush> hollowBrush(const Brush& b, float thickness);
bool clipBrush(const Brush& b, const Plane& p, Brush* front, Brush* back); // false if nothing valid
MeshObject brushToMesh(const Brush& b);
// Mesh edits
void flipMesh(MeshObject& m);
void noiseMesh(MeshObject& m, float amplitude, float frequency, uint32_t seed);
bool extrudePoly(MeshObject& m, int poly, float dist);
Status importOBJ(const std::string& path, Map& map, MeshObject& out);
// Brush primitives (axis-aligned bounds; cylinders/cones are vertical)
Brush makeBox(const Vec3& mn, const Vec3& mx, int material = 0);
Brush makeWedge(const Vec3& mn, const Vec3& mx, int material = 0); // ramp rising towards +X
Brush makeCylinder(const Vec3& mn, const Vec3& mx, int segments, int material = 0);
Brush makeCone(const Vec3& mn, const Vec3& mx, int segments, int material = 0);
std::vector<Brush> makeStairs(const Vec3& mn, const Vec3& mx, int steps, int material = 0); // rises towards +X
// Mesh primitives
MeshObject meshBox(const Vec3& mn, const Vec3& mx, int material = 0);
MeshObject meshCylinder(const Vec3& mn, const Vec3& mx, int segments, int material = 0);
MeshObject meshCone(const Vec3& mn, const Vec3& mx, int segments, int material = 0);
MeshObject meshSphere(const Vec3& mn, const Vec3& mx, int rings, int segments, int material = 0);
MeshObject meshTorus(const Vec3& mn, const Vec3& mx, int segments, int sides, int material = 0);
MeshObject meshGrid(const Vec3& mn, const Vec3& mx, int segX, int segZ, int material = 0); // terrain patch
// Starter content (used by the editor's "New map" and by the sample tools)
void addDefaultMaterials(Map& map); // dev/* materials; texture-less, tinted
Map makeDemoMap(); // lit room with brushes, meshes, entities, a door and a trigger
// ------------------------------------------------------------------ lights
enum class LightType : uint8_t { Point = 0, Spot = 1, Sun = 2 };
struct Light {
LightType type = LightType::Point;
Vec3 pos;
Vec3 dir{0, -1, 0}; // direction the light travels (spot + sun)
Vec3 color{1, 1, 1};
float intensity = 1;
float radius = 512;
float innerDeg = 25, outerDeg = 40;
uint32_t entityId = 0;
};
// light / light_spot / light_environment entities -> Light list
std::vector<Light> collectLights(const Map& map);
// ------------------------------------------------------------------ compiled data
struct Vertex {
Vec3 pos;
Vec3 normal;
Vec2 uv;
uint32_t color = 0xFFFFFFFFu; // 0xAABBGGRR (little-endian RGBA bytes); baked lighting goes here
};
enum class GeomSource : uint8_t { Bsp = 0, Mesh = 1 };
struct Batch { // one draw call: a model + material + source combination
int32_t material = 0;
GeomSource source = GeomSource::Bsp;
int32_t model = 0; // 0 = world, >0 = brush entity sub-model
std::vector<Vertex> vertices;
std::vector<uint32_t> indices; // triangle list
AABB bounds;
};
struct CollisionBrush {
uint32_t contents = 0;
uint32_t sourceId = 0; // Brush::id
std::vector<Plane> planes;
AABB bounds;
};
struct TraceResult {
bool hit = false;
float fraction = 1;
Vec3 point, normal;
uint32_t contents = 0;
uint32_t objectId = 0; // Brush::id or MeshObject::id
bool startSolid = false;
};
// Brush-based BSP tree (Quake 3 style): nodes split on brush-face planes, leaves hold brush lists.
struct BspNode { int32_t plane = 0; int32_t child[2] = {0, 0}; }; // [0]=front [1]=back, <0 => ~leaf
struct BspLeaf { uint32_t firstBrush = 0, brushCount = 0; };
struct Bsp {
std::vector<Plane> planes;
std::vector<BspNode> nodes;
std::vector<BspLeaf> leaves;
std::vector<uint32_t> leafBrushes;
std::vector<CollisionBrush> brushes;
int32_t root = -1; // node index, or ~leaf
AABB bounds;
uint32_t depth = 0;
void build(std::vector<CollisionBrush> brushes);
int findLeaf(const Vec3& p) const;
uint32_t pointContents(const Vec3& p) const;
TraceResult trace(const Vec3& a, const Vec3& b, uint32_t mask = CONTENTS_SOLID) const;
};
struct CompiledModel { // model 0 = world, others = brush entities (doors, triggers...)
uint32_t entityId = 0;
AABB bounds;
std::vector<CollisionBrush> brushes; // sub-models: linear brush list
};
struct CollisionMesh {
uint32_t objectId = 0;
AABB bounds;
std::vector<Vec3> tris; // 3 per triangle
};
struct CompileStats {
uint32_t brushes = 0, meshes = 0, faces = 0, triangles = 0, vertices = 0;
uint32_t bspNodes = 0, bspLeaves = 0, bspDepth = 0, lights = 0;
double ms = 0;
};
struct CompiledMap {
std::vector<Material> materials;
std::vector<Batch> batches;
std::vector<CompiledModel> models;
Bsp bsp; // world solids only
std::vector<CollisionMesh> collMeshes;
std::vector<Light> lights;
std::vector<Entity> entities;
Vec3 ambient{0.2f, 0.2f, 0.2f};
float unitsPerMeter = 64;
AABB bounds;
CompileStats stats;
bool baked = false;
// Nearest hit against the BSP (solid brushes) and solid mesh objects.
TraceResult trace(const Vec3& a, const Vec3& b, uint32_t mask = CONTENTS_SOLID) const;
uint32_t pointContents(const Vec3& p) const { return bsp.pointContents(p); }
};
struct CompileOptions {
bool csg = true; // remove brush faces hidden inside other solids
bool bsp = true; // build collision BSP
bool bakeLighting = false; // tessellate + bake vertex colours
float lightEdge = 32; // max triangle edge while baking (smaller = finer light)
bool shadows = true;
int threads = 0; // 0 = hardware concurrency
};
CompiledMap compile(const Map& map, const CompileOptions& opt = {});
// Raw per-object render batches without CSG (fast; used by the editor viewport).
void buildEditBatches(const Map& map, std::vector<Batch>& out);
// Bakes per-vertex lighting (ambient + point/spot/sun with BSP-traced shadows).
void bakeLighting(CompiledMap& cm, float maxEdge = 32, bool shadows = true, int threads = 0);
// ------------------------------------------------------------------ file format (.hmap)
// Chunked little-endian binary. Chunks: META MATS BRSH MESH ENTS CMPL. Unknown chunks are skipped.
Status saveMap(const std::string& path, const Map& map, const CompiledMap* compiled = nullptr);
Status loadMap(const std::string& path, Map& out, CompiledMap* compiled = nullptr);
// Runtime-oriented: writes only materials + entities + compiled geometry (no editable source).
Status saveCompiled(const std::string& path, const CompiledMap& cm);
// Loads compiled data; if the file only has source chunks it compiles on the fly.
Status loadCompiled(const std::string& path, CompiledMap& out);
std::vector<uint8_t> saveMapToMemory(const Map& map, const CompiledMap* compiled = nullptr);
Status loadMapFromMemory(const uint8_t* data, size_t size, Map& out, CompiledMap* compiled = nullptr);
// ------------------------------------------------------------------ export
struct FbxOptions {
float scale = 1.0f; // multiplies positions
bool entities = true; // export entities/props as Null nodes with custom properties
bool vertexColors = true; // write baked lighting as a colour layer
bool includeSubModels = true;
};
std::vector<uint8_t> exportFBXToMemory(const CompiledMap& cm, const FbxOptions& opt = {});
Status exportFBX(const std::string& path, const CompiledMap& cm, const FbxOptions& opt = {});
Status exportOBJ(const std::string& path, const CompiledMap& cm, float scale = 1.0f);
// ------------------------------------------------------------------ entity definitions
struct PropDef {
std::string key, type, label, def, choices; // type: string int float bool color255 vec3 choice model material target
};
struct EntityClass {
std::string name, label;
bool brush = false; // brush entity (func_*, trigger_*)
Vec3 color{1, 1, 1}; // editor colour 0..1
Vec3 half{8, 8, 8}; // editor box half extents
bool directional = false; // shows an angles arrow
bool hasModel = false; // displays the "model" key
uint32_t brushContents = CONTENTS_SOLID;
std::vector<PropDef> props;
};
class EntityRegistry {
public:
std::vector<EntityClass> classes;
const EntityClass* find(const std::string& name) const;
// Mini-FGD text format, see assets/entities.def. Later definitions override earlier ones.
Status parse(const std::string& text);
void applyDefaults(Entity& e) const; // fills missing keys with class defaults
static EntityRegistry builtin();
};
} // namespace hmap
+84
View File
@@ -0,0 +1,84 @@
/* hmap/hmap_c.h - plain C API over libhmap for engines, scripting languages and FFI.
*
* hmap_map* m = hmap_load("level.hmap", err, sizeof err);
* for (uint32_t i = 0; i < hmap_batch_count(m); ++i) {
* hmap_batch b; hmap_get_batch(m, i, &b);
* // upload b.vertices / b.indices, bind texture hmap_material_texture(m, b.material)
* }
* hmap_trace t; hmap_trace_segment(m, from, to, HMAP_CONTENTS_SOLID, &t);
* hmap_free(m);
*
* Units: map units, +Y up, forward -Z. Pointers stay valid until hmap_free().
*/
#ifndef HMAP_C_H
#define HMAP_C_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define HMAP_CONTENTS_SOLID 1u
#define HMAP_CONTENTS_WATER 2u
#define HMAP_CONTENTS_CLIP 4u
#define HMAP_CONTENTS_TRIGGER 8u
#define HMAP_CONTENTS_SKY 16u
typedef struct hmap_map hmap_map;
typedef struct {
float pos[3];
float normal[3];
float uv[2];
uint32_t color; /* 0xAABBGGRR, baked vertex light */
} hmap_vertex;
typedef struct {
int32_t material;
int32_t model; /* 0 = world, >0 = brush-entity sub-model */
int32_t is_mesh; /* 0 = BSP/brush geometry, 1 = mesh object */
uint32_t vertex_count;
const hmap_vertex* vertices;
uint32_t index_count; /* triangle list */
const uint32_t* indices;
} hmap_batch;
typedef struct {
int hit;
float fraction;
float point[3];
float normal[3];
uint32_t contents;
uint32_t object_id;
int start_solid;
} hmap_trace;
/* Loads a .hmap file (compiled data is used when present, otherwise the source is compiled). */
hmap_map* hmap_load(const char* path, char* err, size_t err_len);
void hmap_free(hmap_map* m);
uint32_t hmap_batch_count(const hmap_map* m);
int hmap_get_batch(const hmap_map* m, uint32_t index, hmap_batch* out);
uint32_t hmap_material_count(const hmap_map* m);
const char* hmap_material_name(const hmap_map* m, uint32_t index);
const char* hmap_material_texture(const hmap_map* m, uint32_t index);
uint32_t hmap_entity_count(const hmap_map* m);
const char* hmap_entity_classname(const hmap_map* m, uint32_t index);
void hmap_entity_origin(const hmap_map* m, uint32_t index, float out[3]);
void hmap_entity_angles(const hmap_map* m, uint32_t index, float out[3]); /* pitch, yaw, roll degrees */
const char* hmap_entity_get(const hmap_map* m, uint32_t index, const char* key); /* NULL if missing */
uint32_t hmap_point_contents(const hmap_map* m, const float p[3]);
int hmap_trace_segment(const hmap_map* m, const float from[3], const float to[3], uint32_t mask, hmap_trace* out);
int hmap_export_fbx(const hmap_map* m, const char* path); /* 1 on success */
#ifdef __cplusplus
}
#endif
#endif /* HMAP_C_H */
+226
View File
@@ -0,0 +1,226 @@
// hmap/hmath.h - tiny dependency-free math types used by libhmap.
// Conventions: right-handed, +Y up, forward = -Z, column vectors (p' = M * p + t).
#pragma once
#include <algorithm>
#include <cmath>
#include <cstdint>
namespace hmap {
constexpr float kPi = 3.14159265358979323846f;
constexpr float kDeg2Rad = kPi / 180.0f;
constexpr float kRad2Deg = 180.0f / kPi;
constexpr float kPlaneEps = 0.01f; // world-unit tolerance used by plane classification
struct Vec2 {
float x = 0, y = 0;
constexpr Vec2() = default;
constexpr Vec2(float x_, float y_) : x(x_), y(y_) {}
Vec2 operator+(const Vec2& o) const { return {x + o.x, y + o.y}; }
Vec2 operator-(const Vec2& o) const { return {x - o.x, y - o.y}; }
Vec2 operator*(float s) const { return {x * s, y * s}; }
};
struct Vec3 {
float x = 0, y = 0, z = 0;
constexpr Vec3() = default;
constexpr Vec3(float x_, float y_, float z_) : x(x_), y(y_), z(z_) {}
Vec3 operator+(const Vec3& o) const { return {x + o.x, y + o.y, z + o.z}; }
Vec3 operator-(const Vec3& o) const { return {x - o.x, y - o.y, z - o.z}; }
Vec3 operator*(float s) const { return {x * s, y * s, z * s}; }
Vec3 operator/(float s) const { return {x / s, y / s, z / s}; }
Vec3 operator-() const { return {-x, -y, -z}; }
Vec3& operator+=(const Vec3& o) { x += o.x; y += o.y; z += o.z; return *this; }
Vec3& operator-=(const Vec3& o) { x -= o.x; y -= o.y; z -= o.z; return *this; }
Vec3& operator*=(float s) { x *= s; y *= s; z *= s; return *this; }
float operator[](int i) const { return i == 0 ? x : (i == 1 ? y : z); }
float& operator[](int i) { return i == 0 ? x : (i == 1 ? y : z); }
bool operator==(const Vec3& o) const { return x == o.x && y == o.y && z == o.z; }
bool operator!=(const Vec3& o) const { return !(*this == o); }
};
inline Vec3 operator*(float s, const Vec3& v) { return v * s; }
inline float dot(const Vec3& a, const Vec3& b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
inline Vec3 cross(const Vec3& a, const Vec3& b) {
return {a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x};
}
inline float lengthSq(const Vec3& v) { return dot(v, v); }
inline float length(const Vec3& v) { return std::sqrt(dot(v, v)); }
inline Vec3 normalize(const Vec3& v) {
float l = length(v);
return l > 1e-12f ? v / l : Vec3(0, 0, 0);
}
inline Vec3 vmin(const Vec3& a, const Vec3& b) { return {std::min(a.x, b.x), std::min(a.y, b.y), std::min(a.z, b.z)}; }
inline Vec3 vmax(const Vec3& a, const Vec3& b) { return {std::max(a.x, b.x), std::max(a.y, b.y), std::max(a.z, b.z)}; }
inline Vec3 lerp(const Vec3& a, const Vec3& b, float t) { return a + (b - a) * t; }
inline float clampf(float v, float lo, float hi) { return v < lo ? lo : (v > hi ? hi : v); }
// Plane: dot(n, p) == d. Solids are on the BACK side (dot(n,p) < d); n points out of the solid.
struct Plane {
Vec3 n{0, 1, 0};
float d = 0;
float dist(const Vec3& p) const { return dot(n, p) - d; }
static Plane fromPoints(const Vec3& a, const Vec3& b, const Vec3& c) {
Vec3 n = normalize(cross(b - a, c - a));
return Plane{n, dot(n, a)};
}
static Plane fromNormalPoint(const Vec3& n, const Vec3& p) {
Vec3 nn = normalize(n);
return Plane{nn, dot(nn, p)};
}
};
struct AABB {
Vec3 mn{1e30f, 1e30f, 1e30f}, mx{-1e30f, -1e30f, -1e30f};
bool empty() const { return mn.x > mx.x; }
void add(const Vec3& p) { mn = vmin(mn, p); mx = vmax(mx, p); }
void add(const AABB& b) { if (!b.empty()) { add(b.mn); add(b.mx); } }
Vec3 center() const { return (mn + mx) * 0.5f; }
Vec3 size() const { return mx - mn; }
bool overlaps(const AABB& o, float eps = 0) const {
return !(o.mn.x > mx.x + eps || o.mx.x < mn.x - eps || o.mn.y > mx.y + eps ||
o.mx.y < mn.y - eps || o.mn.z > mx.z + eps || o.mx.z < mn.z - eps);
}
bool contains(const Vec3& p) const {
return p.x >= mn.x && p.x <= mx.x && p.y >= mn.y && p.y <= mx.y && p.z >= mn.z && p.z <= mx.z;
}
AABB expanded(float e) const { AABB r = *this; r.mn = mn - Vec3(e, e, e); r.mx = mx + Vec3(e, e, e); return r; }
};
// Slab test. Returns true when the ray hits; tNear may be negative when the origin is inside.
inline bool rayAABB(const Vec3& o, const Vec3& d, const AABB& b, float& tNear, float& tFar) {
float t0 = -1e30f, t1 = 1e30f;
for (int i = 0; i < 3; ++i) {
if (std::fabs(d[i]) < 1e-9f) {
if (o[i] < b.mn[i] || o[i] > b.mx[i]) return false;
} else {
float inv = 1.0f / d[i];
float a = (b.mn[i] - o[i]) * inv, c = (b.mx[i] - o[i]) * inv;
if (a > c) std::swap(a, c);
t0 = std::max(t0, a);
t1 = std::min(t1, c);
if (t0 > t1) return false;
}
}
tNear = t0;
tFar = t1;
return t1 >= 0;
}
// 3x4 affine matrix.
struct Mat34 {
float m[3][4] = {{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}};
static Mat34 identity() { return Mat34(); }
static Mat34 translate(const Vec3& t) {
Mat34 r;
r.m[0][3] = t.x; r.m[1][3] = t.y; r.m[2][3] = t.z;
return r;
}
static Mat34 scale(const Vec3& s) {
Mat34 r;
r.m[0][0] = s.x; r.m[1][1] = s.y; r.m[2][2] = s.z;
return r;
}
static Mat34 rotate(const Vec3& axis, float deg) { // Rodrigues, right-handed
Vec3 a = normalize(axis);
float c = std::cos(deg * kDeg2Rad), s = std::sin(deg * kDeg2Rad), t = 1 - c;
Mat34 r;
r.m[0][0] = t * a.x * a.x + c; r.m[0][1] = t * a.x * a.y - s * a.z; r.m[0][2] = t * a.x * a.z + s * a.y;
r.m[1][0] = t * a.x * a.y + s * a.z; r.m[1][1] = t * a.y * a.y + c; r.m[1][2] = t * a.y * a.z - s * a.x;
r.m[2][0] = t * a.x * a.z - s * a.y; r.m[2][1] = t * a.y * a.z + s * a.x; r.m[2][2] = t * a.z * a.z + c;
return r;
}
// angles = (pitch, yaw, roll) degrees. R = Ry(yaw) * Rx(pitch) * Rz(roll). Forward (-Z) with
// yaw 0 / pitch 0 stays -Z; +pitch looks up; +yaw turns left (counter-clockwise from above).
static Mat34 fromEuler(const Vec3& a) {
return rotate({0, 1, 0}, a.y) * rotate({1, 0, 0}, a.x) * rotate({0, 0, 1}, a.z);
}
Vec3 toEuler() const { // inverse of fromEuler
float sp = -m[1][2];
float pitch = std::asin(clampf(sp, -1.0f, 1.0f));
float yaw, roll;
if (std::fabs(sp) < 0.99999f) {
yaw = std::atan2(m[0][2], m[2][2]);
roll = std::atan2(m[1][0], m[1][1]);
} else {
yaw = std::atan2(-m[2][0], m[0][0]);
roll = 0;
}
return {pitch * kRad2Deg, yaw * kRad2Deg, roll * kRad2Deg};
}
// Euler angles for the FBX default XYZ order (R = Rz * Ry * Rx), degrees.
Vec3 toEulerXYZ() const {
float sb = -m[2][0];
float b = std::asin(clampf(sb, -1.0f, 1.0f));
float a, c;
if (std::fabs(sb) < 0.99999f) {
a = std::atan2(m[2][1], m[2][2]);
c = std::atan2(m[1][0], m[0][0]);
} else {
a = std::atan2(-m[1][2], m[1][1]);
c = 0;
}
return {a * kRad2Deg, b * kRad2Deg, c * kRad2Deg};
}
Vec3 point(const Vec3& p) const {
return {m[0][0] * p.x + m[0][1] * p.y + m[0][2] * p.z + m[0][3],
m[1][0] * p.x + m[1][1] * p.y + m[1][2] * p.z + m[1][3],
m[2][0] * p.x + m[2][1] * p.y + m[2][2] * p.z + m[2][3]};
}
Vec3 vector(const Vec3& v) const {
return {m[0][0] * v.x + m[0][1] * v.y + m[0][2] * v.z,
m[1][0] * v.x + m[1][1] * v.y + m[1][2] * v.z,
m[2][0] * v.x + m[2][1] * v.y + m[2][2] * v.z};
}
Vec3 translation() const { return {m[0][3], m[1][3], m[2][3]}; }
// this * b : applies b first, then this.
Mat34 operator*(const Mat34& b) const {
Mat34 r;
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j)
r.m[i][j] = m[i][0] * b.m[0][j] + m[i][1] * b.m[1][j] + m[i][2] * b.m[2][j];
r.m[i][3] = m[i][0] * b.m[0][3] + m[i][1] * b.m[1][3] + m[i][2] * b.m[2][3] + m[i][3];
}
return r;
}
float det3() const {
return m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) -
m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0]) +
m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
}
bool inverse(Mat34& out) const {
float det = det3();
if (std::fabs(det) < 1e-12f) return false;
float id = 1.0f / det;
Mat34 r;
r.m[0][0] = (m[1][1] * m[2][2] - m[1][2] * m[2][1]) * id;
r.m[0][1] = (m[0][2] * m[2][1] - m[0][1] * m[2][2]) * id;
r.m[0][2] = (m[0][1] * m[1][2] - m[0][2] * m[1][1]) * id;
r.m[1][0] = (m[1][2] * m[2][0] - m[1][0] * m[2][2]) * id;
r.m[1][1] = (m[0][0] * m[2][2] - m[0][2] * m[2][0]) * id;
r.m[1][2] = (m[0][2] * m[1][0] - m[0][0] * m[1][2]) * id;
r.m[2][0] = (m[1][0] * m[2][1] - m[1][1] * m[2][0]) * id;
r.m[2][1] = (m[0][1] * m[2][0] - m[0][0] * m[2][1]) * id;
r.m[2][2] = (m[0][0] * m[1][1] - m[0][1] * m[1][0]) * id;
Vec3 t = translation();
Vec3 it = r.vector(t);
r.m[0][3] = -it.x; r.m[1][3] = -it.y; r.m[2][3] = -it.z;
out = r;
return true;
}
// Transforms a plane by this affine map (normal uses the inverse transpose).
Plane transformPlane(const Plane& pl) const {
Mat34 inv;
if (!inverse(inv)) return pl;
Vec3 n(inv.m[0][0] * pl.n.x + inv.m[1][0] * pl.n.y + inv.m[2][0] * pl.n.z,
inv.m[0][1] * pl.n.x + inv.m[1][1] * pl.n.y + inv.m[2][1] * pl.n.z,
inv.m[0][2] * pl.n.x + inv.m[1][2] * pl.n.y + inv.m[2][2] * pl.n.z);
float d = pl.d + dot(n, translation());
float l = length(n);
if (l < 1e-12f) return pl;
return Plane{n / l, d / l};
}
};
} // namespace hmap