Files
2026-10-25 03:29:04 -03:00

360 lines
15 KiB
C++

// 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