// 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 #include #include 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 faces; }; struct MeshPoly { // convex polygon of a mesh object std::vector idx; // CCW from the front std::vector 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 verts; std::vector 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 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 materials; std::vector brushes; std::vector meshes; std::vector 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; struct BrushGeometry { std::vector polys; // parallel to Brush::faces; empty polygon = redundant face AABB bounds; std::vector 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 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 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 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 vertices; std::vector indices; // triangle list AABB bounds; }; struct CollisionBrush { uint32_t contents = 0; uint32_t sourceId = 0; // Brush::id std::vector 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 planes; std::vector nodes; std::vector leaves; std::vector leafBrushes; std::vector brushes; int32_t root = -1; // node index, or ~leaf AABB bounds; uint32_t depth = 0; void build(std::vector 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 brushes; // sub-models: linear brush list }; struct CollisionMesh { uint32_t objectId = 0; AABB bounds; std::vector 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 materials; std::vector batches; std::vector models; Bsp bsp; // world solids only std::vector collMeshes; std::vector lights; std::vector 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& 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 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 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 props; }; class EntityRegistry { public: std::vector 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