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

+27
View File
@@ -0,0 +1,27 @@
find_package(Threads REQUIRED)
add_library(hmap STATIC
src/bsp.cpp
src/capi.cpp
src/compile.cpp
src/entdefs.cpp
src/fbx.cpp
src/geometry.cpp
src/io.cpp
src/map.cpp
src/sample.cpp)
add_library(hmap::hmap ALIAS hmap)
target_include_directories(hmap PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>)
target_compile_features(hmap PUBLIC cxx_std_17)
target_link_libraries(hmap PUBLIC Threads::Threads)
if(MSVC)
target_compile_options(hmap PRIVATE /W3 /utf-8)
else()
target_compile_options(hmap PRIVATE -Wall -Wextra)
endif()
install(TARGETS hmap EXPORT hmapTargets ARCHIVE DESTINATION lib)
install(DIRECTORY include/ DESTINATION include)
+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
+301
View File
@@ -0,0 +1,301 @@
#include "hmap/hmap.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <map>
namespace hmap {
namespace {
// Planes are stored once, in a canonical orientation (largest normal component positive).
struct CanonTable {
std::vector<Plane> planes;
std::map<std::array<long long, 4>, int> lookup;
int get(Plane p) {
int ax = 0;
if (std::fabs(p.n.y) > std::fabs(p.n[ax])) ax = 1;
if (std::fabs(p.n.z) > std::fabs(p.n[ax])) ax = 2;
if (p.n[ax] < 0) { p.n = -p.n; p.d = -p.d; }
std::array<long long, 4> key{std::llround(p.n.x * 1e4), std::llround(p.n.y * 1e4), std::llround(p.n.z * 1e4),
std::llround(p.d * 100.0)};
auto it = lookup.find(key);
if (it != lookup.end()) return it->second;
int id = (int)planes.size();
planes.push_back(p);
lookup[key] = id;
return id;
}
};
constexpr size_t kMaxLeafBrushes = 3;
constexpr uint32_t kMaxDepth = 64;
enum Side { FRONT = 1, BACK = 2, BOTH = 3 };
struct Builder {
Bsp& bsp;
CanonTable table;
std::vector<std::vector<int>> faceCanon;
std::vector<std::vector<Vec3>> pts;
std::vector<uint8_t> used;
uint32_t maxDepth = 0;
explicit Builder(Bsp& b) : bsp(b) {}
int classify(uint32_t b, const Plane& pl) const {
const AABB& bb = bsp.brushes[b].bounds;
Vec3 c = bb.center(), e = (bb.mx - bb.mn) * 0.5f;
float r = e.x * std::fabs(pl.n.x) + e.y * std::fabs(pl.n.y) + e.z * std::fabs(pl.n.z);
float dc = pl.dist(c);
if (dc - r > kPlaneEps) return FRONT;
if (dc + r < -kPlaneEps) return BACK;
float mn = 1e30f, mx = -1e30f;
for (const Vec3& p : pts[b]) {
float d = pl.dist(p);
mn = std::min(mn, d);
mx = std::max(mx, d);
}
if (mn >= -kPlaneEps) return FRONT;
if (mx <= kPlaneEps) return BACK;
return BOTH;
}
int32_t makeLeaf(const std::vector<uint32_t>& list) {
BspLeaf lf;
lf.firstBrush = (uint32_t)bsp.leafBrushes.size();
lf.brushCount = (uint32_t)list.size();
bsp.leafBrushes.insert(bsp.leafBrushes.end(), list.begin(), list.end());
bsp.leaves.push_back(lf);
return ~(int32_t)(bsp.leaves.size() - 1);
}
int32_t build(const std::vector<uint32_t>& list, uint32_t depth) {
maxDepth = std::max(maxDepth, depth);
if (list.size() <= kMaxLeafBrushes || depth >= kMaxDepth) return makeLeaf(list);
std::vector<int> cands;
std::vector<uint8_t> seen(table.planes.size(), 0);
for (uint32_t b : list)
for (int c : faceCanon[b])
if (!used[(size_t)c] && !seen[(size_t)c]) { seen[(size_t)c] = 1; cands.push_back(c); }
if (cands.empty()) return makeLeaf(list);
size_t stride = std::max<size_t>(1, cands.size() / 48);
int best = -1;
float bestScore = 1e30f;
for (size_t ci = 0; ci < cands.size(); ci += stride) {
const Plane& pl = table.planes[(size_t)cands[ci]];
int nf = 0, nb = 0, nboth = 0;
for (uint32_t b : list) {
int s = classify(b, pl);
nf += s == FRONT;
nb += s == BACK;
nboth += s == BOTH;
}
bool useful = (nf >= 1 && nb >= 1) || (nboth >= 1 && nf + nb >= 1);
if (!useful) continue;
float score = (float)nboth * 4.0f + (float)std::abs(nf - nb);
if (score < bestScore) { bestScore = score; best = cands[ci]; }
}
if (best < 0) return makeLeaf(list);
const Plane& pl = table.planes[(size_t)best];
std::vector<uint32_t> fl, bl;
for (uint32_t b : list) {
int s = classify(b, pl);
if (s & FRONT) fl.push_back(b);
if (s & BACK) bl.push_back(b);
}
size_t node = bsp.nodes.size();
bsp.nodes.emplace_back();
bsp.nodes[node].plane = best;
used[(size_t)best] = 1;
int32_t c0 = build(fl, depth + 1);
int32_t c1 = build(bl, depth + 1);
used[(size_t)best] = 0;
bsp.nodes[node].child[0] = c0;
bsp.nodes[node].child[1] = c1;
return (int32_t)node;
}
};
} // namespace
void Bsp::build(std::vector<CollisionBrush> in) {
planes.clear(); nodes.clear(); leaves.clear(); leafBrushes.clear();
brushes = std::move(in);
bounds = AABB();
depth = 0;
if (brushes.empty()) {
leaves.push_back(BspLeaf{});
root = ~0;
return;
}
Builder b(*this);
b.faceCanon.resize(brushes.size());
b.pts.resize(brushes.size());
for (size_t i = 0; i < brushes.size(); ++i) {
Brush tmp;
for (const Plane& p : brushes[i].planes) {
Face f;
f.plane = p;
tmp.faces.push_back(f);
b.faceCanon[i].push_back(b.table.get(p));
}
BrushGeometry g = buildBrushGeometry(tmp);
b.pts[i] = g.points;
if (brushes[i].bounds.empty()) brushes[i].bounds = g.bounds;
bounds.add(brushes[i].bounds);
}
b.used.assign(b.table.planes.size(), 0);
std::vector<uint32_t> all(brushes.size());
for (size_t i = 0; i < all.size(); ++i) all[i] = (uint32_t)i;
root = b.build(all, 0);
planes = b.table.planes;
depth = b.maxDepth;
}
int Bsp::findLeaf(const Vec3& p) const {
int32_t n = root;
while (n >= 0) {
const BspNode& nd = nodes[(size_t)n];
n = planes[(size_t)nd.plane].dist(p) >= 0 ? nd.child[0] : nd.child[1];
}
return ~n;
}
uint32_t Bsp::pointContents(const Vec3& p) const {
int leaf = findLeaf(p);
if (leaf < 0 || (size_t)leaf >= leaves.size()) return 0;
const BspLeaf& lf = leaves[(size_t)leaf];
uint32_t c = 0;
for (uint32_t i = 0; i < lf.brushCount; ++i) {
const CollisionBrush& br = brushes[leafBrushes[lf.firstBrush + i]];
bool inside = true;
for (const Plane& pl : br.planes)
if (pl.dist(p) > 0) { inside = false; break; }
if (inside) c |= br.contents;
}
return c;
}
namespace {
struct TraceCtx {
const Bsp* bsp;
Vec3 a, b;
uint32_t mask;
TraceResult res;
};
bool segBrush(const CollisionBrush& br, const Vec3& a, const Vec3& b, float& frac, Vec3& nrm, bool& startSolid) {
float tn = -1.0f, tf = 1.0f;
const Plane* ep = nullptr;
bool startOut = false;
for (const Plane& pl : br.planes) {
float d1 = pl.dist(a), d2 = pl.dist(b);
if (d1 > 0) startOut = true;
if (d1 > 0 && d2 >= d1) return false;
if (d1 <= 0 && d2 <= 0) continue;
float f = d1 / (d1 - d2);
if (d1 > d2) { if (f > tn) { tn = f; ep = &pl; } }
else if (f < tf) tf = f;
}
if (!startOut) { startSolid = true; frac = 0; nrm = Vec3(); return true; }
if (ep && tn <= tf && tn >= 0) { frac = tn; nrm = ep->n; return true; }
return false;
}
void traceNode(TraceCtx& c, int32_t child, const Vec3& p1, const Vec3& p2, float f1, float f2) {
if (f1 >= c.res.fraction) return;
if (child < 0) {
const BspLeaf& lf = c.bsp->leaves[(size_t)(~child)];
for (uint32_t i = 0; i < lf.brushCount; ++i) {
const CollisionBrush& br = c.bsp->brushes[c.bsp->leafBrushes[lf.firstBrush + i]];
if (!(br.contents & c.mask)) continue;
float fr = 0;
Vec3 n;
bool ss = false;
if (segBrush(br, c.a, c.b, fr, n, ss) && fr < c.res.fraction) {
c.res.hit = true;
c.res.fraction = fr;
c.res.normal = n;
c.res.contents = br.contents;
c.res.objectId = br.sourceId;
c.res.startSolid = ss;
}
}
return;
}
const BspNode& nd = c.bsp->nodes[(size_t)child];
const Plane& pl = c.bsp->planes[(size_t)nd.plane];
float d1 = pl.dist(p1), d2 = pl.dist(p2);
const float eps = kPlaneEps;
if (d1 > eps && d2 > eps) { traceNode(c, nd.child[0], p1, p2, f1, f2); return; }
if (d1 < -eps && d2 < -eps) { traceNode(c, nd.child[1], p1, p2, f1, f2); return; }
if (std::fabs(d1) <= eps && std::fabs(d2) <= eps) {
traceNode(c, nd.child[0], p1, p2, f1, f2);
traceNode(c, nd.child[1], p1, p2, f1, f2);
return;
}
int side = d1 < 0 ? 1 : 0;
float t = clampf(d1 / (d1 - d2), 0.0f, 1.0f);
Vec3 mid = p1 + (p2 - p1) * t;
float fm = f1 + (f2 - f1) * t;
traceNode(c, nd.child[side], p1, mid, f1, fm);
traceNode(c, nd.child[side ^ 1], mid, p2, fm, f2);
}
bool segTri(const Vec3& o, const Vec3& d, const Vec3& a, const Vec3& b, const Vec3& c, float& t) {
Vec3 e1 = b - a, e2 = c - a, p = cross(d, e2);
float det = dot(e1, p);
if (std::fabs(det) < 1e-9f) return false;
float inv = 1.0f / det;
Vec3 s = o - a;
float u = dot(s, p) * inv;
if (u < 0 || u > 1) return false;
Vec3 q = cross(s, e1);
float v = dot(d, q) * inv;
if (v < 0 || u + v > 1) return false;
float tt = dot(e2, q) * inv;
if (tt < 0 || tt > 1) return false;
t = tt;
return true;
}
} // namespace
TraceResult Bsp::trace(const Vec3& a, const Vec3& b, uint32_t mask) const {
TraceCtx c{this, a, b, mask, TraceResult()};
traceNode(c, root, a, b, 0.0f, 1.0f);
if (c.res.hit) c.res.point = a + (b - a) * c.res.fraction;
return c.res;
}
TraceResult CompiledMap::trace(const Vec3& a, const Vec3& b, uint32_t mask) const {
TraceResult best = bsp.trace(a, b, mask);
if (!(mask & CONTENTS_SOLID)) return best;
Vec3 d = b - a;
for (const CollisionMesh& cmesh : collMeshes) {
float tn, tf;
if (!rayAABB(a, d, cmesh.bounds.expanded(0.01f), tn, tf) || tn > 1.0f || tf < 0.0f) continue;
for (size_t i = 0; i + 2 < cmesh.tris.size(); i += 3) {
float t;
if (segTri(a, d, cmesh.tris[i], cmesh.tris[i + 1], cmesh.tris[i + 2], t) && t < best.fraction) {
Vec3 n = normalize(cross(cmesh.tris[i + 1] - cmesh.tris[i], cmesh.tris[i + 2] - cmesh.tris[i]));
if (dot(n, d) > 0) n = -n;
best.hit = true;
best.fraction = t;
best.normal = n;
best.point = a + d * t;
best.contents = CONTENTS_SOLID;
best.objectId = cmesh.objectId;
best.startSolid = false;
}
}
}
return best;
}
} // namespace hmap
+95
View File
@@ -0,0 +1,95 @@
#include "hmap/hmap_c.h"
#include "hmap/hmap.h"
#include <cstdio>
#include <cstring>
struct hmap_map {
hmap::CompiledMap cm;
};
static_assert(sizeof(hmap::Vertex) == sizeof(hmap_vertex), "hmap_vertex must mirror hmap::Vertex");
static_assert(sizeof(hmap::Vec3) == 3 * sizeof(float), "Vec3 must be tightly packed");
extern "C" {
hmap_map* hmap_load(const char* path, char* err, size_t err_len) {
auto* m = new hmap_map();
hmap::Status s = hmap::loadCompiled(path ? path : "", m->cm);
if (!s) {
if (err && err_len) std::snprintf(err, err_len, "%s", s.message.c_str());
delete m;
return nullptr;
}
return m;
}
void hmap_free(hmap_map* m) { delete m; }
uint32_t hmap_batch_count(const hmap_map* m) { return m ? (uint32_t)m->cm.batches.size() : 0; }
int hmap_get_batch(const hmap_map* m, uint32_t i, hmap_batch* out) {
if (!m || !out || i >= m->cm.batches.size()) return 0;
const hmap::Batch& b = m->cm.batches[i];
out->material = b.material;
out->model = b.model;
out->is_mesh = b.source == hmap::GeomSource::Mesh;
out->vertex_count = (uint32_t)b.vertices.size();
out->vertices = reinterpret_cast<const hmap_vertex*>(b.vertices.data());
out->index_count = (uint32_t)b.indices.size();
out->indices = b.indices.data();
return 1;
}
uint32_t hmap_material_count(const hmap_map* m) { return m ? (uint32_t)m->cm.materials.size() : 0; }
const char* hmap_material_name(const hmap_map* m, uint32_t i) {
return (m && i < m->cm.materials.size()) ? m->cm.materials[i].name.c_str() : nullptr;
}
const char* hmap_material_texture(const hmap_map* m, uint32_t i) {
return (m && i < m->cm.materials.size()) ? m->cm.materials[i].texture.c_str() : nullptr;
}
uint32_t hmap_entity_count(const hmap_map* m) { return m ? (uint32_t)m->cm.entities.size() : 0; }
const char* hmap_entity_classname(const hmap_map* m, uint32_t i) {
return (m && i < m->cm.entities.size()) ? m->cm.entities[i].classname.c_str() : nullptr;
}
void hmap_entity_origin(const hmap_map* m, uint32_t i, float out[3]) {
if (!m || !out || i >= m->cm.entities.size()) return;
const hmap::Vec3& o = m->cm.entities[i].origin;
out[0] = o.x; out[1] = o.y; out[2] = o.z;
}
void hmap_entity_angles(const hmap_map* m, uint32_t i, float out[3]) {
if (!m || !out || i >= m->cm.entities.size()) return;
const hmap::Vec3& a = m->cm.entities[i].angles;
out[0] = a.x; out[1] = a.y; out[2] = a.z;
}
const char* hmap_entity_get(const hmap_map* m, uint32_t i, const char* key) {
if (!m || !key || i >= m->cm.entities.size()) return nullptr;
const std::string* v = m->cm.entities[i].find(key);
return v ? v->c_str() : nullptr;
}
uint32_t hmap_point_contents(const hmap_map* m, const float p[3]) {
if (!m || !p) return 0;
return m->cm.pointContents(hmap::Vec3(p[0], p[1], p[2]));
}
int hmap_trace_segment(const hmap_map* m, const float a[3], const float b[3], uint32_t mask, hmap_trace* out) {
if (!m || !a || !b || !out) return 0;
hmap::TraceResult r = m->cm.trace(hmap::Vec3(a[0], a[1], a[2]), hmap::Vec3(b[0], b[1], b[2]), mask);
out->hit = r.hit;
out->fraction = r.fraction;
out->point[0] = r.point.x; out->point[1] = r.point.y; out->point[2] = r.point.z;
out->normal[0] = r.normal.x; out->normal[1] = r.normal.y; out->normal[2] = r.normal.z;
out->contents = r.contents;
out->object_id = r.objectId;
out->start_solid = r.startSolid;
return r.hit;
}
int hmap_export_fbx(const hmap_map* m, const char* path) {
if (!m || !path) return 0;
return hmap::exportFBX(path, m->cm).ok ? 1 : 0;
}
} // extern "C"
+500
View File
@@ -0,0 +1,500 @@
#include "hmap/hmap.h"
#include <atomic>
#include <chrono>
#include <cmath>
#include <cstring>
#include <map>
#include <numeric>
#include <thread>
#include <tuple>
#include <unordered_map>
namespace hmap {
namespace {
// ---------------------------------------------------------------- batches
struct BatchKey {
int32_t model, material;
int source;
bool operator<(const BatchKey& o) const { return std::tie(model, material, source) < std::tie(o.model, o.material, o.source); }
};
struct BatchSet {
std::vector<Batch>& out;
std::map<BatchKey, size_t> index;
explicit BatchSet(std::vector<Batch>& o) : out(o) {}
Batch& get(int32_t model, int32_t material, GeomSource src) {
BatchKey k{model, material, (int)src};
auto it = index.find(k);
if (it == index.end()) {
Batch b;
b.model = model;
b.material = material;
b.source = src;
out.push_back(std::move(b));
it = index.emplace(k, out.size() - 1).first;
}
return out[it->second];
}
};
const Material& matOf(const std::vector<Material>& mats, int32_t i) {
static const Material def;
return (i >= 0 && (size_t)i < mats.size()) ? mats[(size_t)i] : def;
}
void emitPolygon(Batch& b, const Polygon& poly, const Vec3& n, const UVInfo& uv, const Material& m) {
uint32_t base = (uint32_t)b.vertices.size();
for (const Vec3& p : poly) {
Vertex v;
v.pos = p;
v.normal = n;
v.uv = computeUV(p, n, uv, m.tileSize);
b.vertices.push_back(v);
b.bounds.add(p);
}
for (uint32_t k = 1; k + 1 < (uint32_t)poly.size(); ++k) {
b.indices.push_back(base);
b.indices.push_back(base + k);
b.indices.push_back(base + k + 1);
}
}
Vec3 newell(const Polygon& p) {
Vec3 n;
for (size_t i = 0; i < p.size(); ++i) {
const Vec3& a = p[i];
const Vec3& b = p[(i + 1) % p.size()];
n.x += (a.y - b.y) * (a.z + b.z);
n.y += (a.z - b.z) * (a.x + b.x);
n.z += (a.x - b.x) * (a.y + b.y);
}
return n;
}
// Emits a mesh object; returns the number of triangles. Optionally records collision triangles.
void emitMesh(BatchSet& bs, const MeshObject& mo, int32_t model, const std::vector<Material>& mats, CollisionMesh* coll) {
size_t np = mo.polys.size();
std::vector<Vec3> raw(np);
std::vector<uint8_t> okPoly(np, 0);
std::vector<std::vector<size_t>> vertPolys(mo.verts.size());
for (size_t i = 0; i < np; ++i) {
const MeshPoly& p = mo.polys[i];
if (p.idx.size() < 3) continue;
Polygon poly;
bool ok = true;
for (uint32_t k : p.idx) {
if (k >= mo.verts.size()) { ok = false; break; }
poly.push_back(mo.verts[k]);
}
if (!ok) continue;
raw[i] = newell(poly);
okPoly[i] = lengthSq(raw[i]) > 1e-12f;
if (!okPoly[i]) continue;
for (uint32_t k : p.idx) vertPolys[k].push_back(i);
}
for (size_t i = 0; i < np; ++i) {
if (!okPoly[i]) continue;
const MeshPoly& p = mo.polys[i];
const Material& mat = matOf(mats, p.material);
Vec3 fn = normalize(raw[i]);
Batch& b = bs.get(model, p.material, GeomSource::Mesh);
uint32_t base = (uint32_t)b.vertices.size();
bool hasUV = p.uv.size() == p.idx.size();
for (size_t k = 0; k < p.idx.size(); ++k) {
Vertex v;
v.pos = mo.verts[p.idx[k]];
v.normal = fn;
if (p.smooth) {
Vec3 sum;
for (size_t q : vertPolys[p.idx[k]])
if (mo.polys[q].smooth == p.smooth) sum += raw[q];
if (lengthSq(sum) > 1e-12f) v.normal = normalize(sum);
}
v.uv = hasUV ? p.uv[k] : computeUV(v.pos, fn, p.proj, mat.tileSize);
b.vertices.push_back(v);
b.bounds.add(v.pos);
}
for (uint32_t k = 1; k + 1 < (uint32_t)p.idx.size(); ++k) {
b.indices.push_back(base);
b.indices.push_back(base + k);
b.indices.push_back(base + k + 1);
if (coll) {
coll->tris.push_back(mo.verts[p.idx[0]]);
coll->tris.push_back(mo.verts[p.idx[k]]);
coll->tris.push_back(mo.verts[p.idx[k + 1]]);
}
}
}
}
// ---------------------------------------------------------------- CSG
enum { PF_FRONT = 0, PF_BACK = 1, PF_SPLIT = 2, PF_COPLANAR = 3 };
int splitPolyF(const Polygon& poly, const Plane& pl, float eps, Polygon& front, Polygon& back) {
size_t n = poly.size();
std::vector<float> d(n);
std::vector<int> s(n);
int nf = 0, nb = 0;
for (size_t i = 0; i < n; ++i) {
d[i] = pl.dist(poly[i]);
s[i] = d[i] > eps ? 1 : (d[i] < -eps ? -1 : 0);
nf += s[i] > 0;
nb += s[i] < 0;
}
if (nf == 0 && nb == 0) return PF_COPLANAR;
if (nb == 0) return PF_FRONT;
if (nf == 0) return PF_BACK;
for (size_t i = 0; i < n; ++i) {
size_t j = (i + 1) % n;
if (s[i] == 0) { front.push_back(poly[i]); back.push_back(poly[i]); }
else if (s[i] > 0) front.push_back(poly[i]);
else back.push_back(poly[i]);
if ((s[i] > 0 && s[j] < 0) || (s[i] < 0 && s[j] > 0)) {
float t = d[i] / (d[i] - d[j]);
Vec3 m = poly[i] + (poly[j] - poly[i]) * t;
front.push_back(m);
back.push_back(m);
}
}
return PF_SPLIT;
}
// Appends to `out` the parts of `poly` that lie outside brush B (convex, outward planes).
void clipOutside(const Polygon& poly, const Vec3& polyN, const Brush& B, const AABB& bBounds, bool bWinsTies,
std::vector<Polygon>& out) {
// Early outs keep untouched polygons whole (avoids needless fragmentation).
AABB pb;
for (const Vec3& v : poly) pb.add(v);
if (!pb.overlaps(bBounds, kPlaneEps)) { out.push_back(poly); return; }
for (const Face& bf : B.faces) {
bool allFront = true;
for (const Vec3& v : poly)
if (bf.plane.dist(v) <= kPlaneEps) { allFront = false; break; }
if (allFront) { out.push_back(poly); return; }
}
Polygon cur = poly;
for (const Face& bf : B.faces) {
const Plane& pl = bf.plane;
Polygon f, b;
int r = splitPolyF(cur, pl, kPlaneEps, f, b);
if (r == PF_COPLANAR) {
// same-facing coplanar faces: exactly one brush keeps the shared area (lowest index wins)
if (dot(polyN, pl.n) > 0 && !bWinsTies) { out.push_back(cur); return; }
continue; // treated as inside this plane
}
if (r == PF_FRONT) { out.push_back(cur); return; }
if (r == PF_BACK) continue;
if (f.size() >= 3 && length(newell(f)) * 0.5f > 0.01f) out.push_back(std::move(f));
cur = std::move(b);
if (cur.size() < 3) return;
}
// what remains is inside B -> discarded
}
bool isRenderable(const Brush& b) { return (b.contents & (CONTENTS_CLIP | CONTENTS_TRIGGER)) == 0; }
CollisionBrush makeCollision(const Brush& b, const BrushGeometry& g) {
CollisionBrush cb;
cb.contents = b.contents;
cb.sourceId = b.id;
cb.bounds = g.bounds;
for (size_t i = 0; i < b.faces.size(); ++i)
if (g.polys[i].size() >= 3) cb.planes.push_back(b.faces[i].plane);
return cb;
}
// ---------------------------------------------------------------- lighting
uint32_t packColor(const Vec3& c) {
auto q = [](float v) { return (uint32_t)(clampf(v, 0.0f, 1.0f) * 255.0f + 0.5f); };
return q(c.x) | (q(c.y) << 8) | (q(c.z) << 16) | (255u << 24);
}
Vertex midVertex(const Vertex& a, const Vertex& b) {
Vertex m;
m.pos = (a.pos + b.pos) * 0.5f;
m.normal = normalize(a.normal + b.normal);
if (lengthSq(m.normal) < 1e-12f) m.normal = a.normal;
m.uv = (a.uv + b.uv) * 0.5f;
m.color = a.color;
return m;
}
void subdivide(const Vertex& a, const Vertex& b, const Vertex& c, float maxE2, std::vector<Vertex>& out, int depth, size_t& budget) {
float ab = lengthSq(b.pos - a.pos), bc = lengthSq(c.pos - b.pos), ca = lengthSq(a.pos - c.pos);
float m = std::max(ab, std::max(bc, ca));
if (m <= maxE2 || depth > 14 || budget == 0) {
out.push_back(a); out.push_back(b); out.push_back(c);
if (budget > 0) --budget;
return;
}
if (m == ab) { Vertex mid = midVertex(a, b); subdivide(a, mid, c, maxE2, out, depth + 1, budget); subdivide(mid, b, c, maxE2, out, depth + 1, budget); }
else if (m == bc) { Vertex mid = midVertex(b, c); subdivide(a, b, mid, maxE2, out, depth + 1, budget); subdivide(a, mid, c, maxE2, out, depth + 1, budget); }
else { Vertex mid = midVertex(c, a); subdivide(a, b, mid, maxE2, out, depth + 1, budget); subdivide(mid, b, c, maxE2, out, depth + 1, budget); }
}
// Merges vertices that share position, normal and UV (tessellation emits 3 vertices per triangle).
struct WeldKey {
int32_t k[8];
bool operator==(const WeldKey& o) const { return std::memcmp(k, o.k, sizeof k) == 0; }
};
struct WeldHash {
size_t operator()(const WeldKey& w) const {
uint64_t h = 1469598103934665603ull;
for (int i = 0; i < 8; ++i) { h ^= (uint32_t)w.k[i]; h *= 1099511628211ull; }
return (size_t)h;
}
};
void weldBatch(Batch& b) {
auto q = [](float v, float scale) { return (int32_t)std::lround(v * scale); };
std::unordered_map<WeldKey, uint32_t, WeldHash> seen;
seen.reserve(b.vertices.size() / 2 + 1);
std::vector<Vertex> out;
std::vector<uint32_t> idx;
idx.reserve(b.vertices.size());
for (const Vertex& v : b.vertices) {
WeldKey k{{q(v.pos.x, 100), q(v.pos.y, 100), q(v.pos.z, 100), q(v.normal.x, 100), q(v.normal.y, 100), q(v.normal.z, 100),
q(v.uv.x, 1000), q(v.uv.y, 1000)}};
auto it = seen.find(k);
if (it == seen.end()) { it = seen.emplace(k, (uint32_t)out.size()).first; out.push_back(v); }
idx.push_back(it->second);
}
b.vertices = std::move(out);
b.indices = std::move(idx);
}
Vec3 shade(const CompiledMap& cm, const Vec3& p, const Vec3& n, bool shadows) {
Vec3 col = cm.ambient;
Vec3 origin = p + n * 0.5f;
for (const Light& L : cm.lights) {
Vec3 toL;
float atten = 1.0f;
if (L.type == LightType::Sun) {
toL = -normalize(L.dir);
} else {
Vec3 d = L.pos - p;
float dist = length(d);
if (dist > L.radius || dist < 1e-3f) continue;
toL = d / dist;
float r = dist / L.radius;
atten = 1.0f - r * r;
atten *= atten;
}
float ndl = dot(n, toL);
if (ndl <= 0) continue;
if (L.type == LightType::Spot) {
float cd = dot(-toL, normalize(L.dir));
float outer = std::cos(L.outerDeg * kDeg2Rad), inner = std::cos(L.innerDeg * kDeg2Rad);
if (cd <= outer) continue;
atten *= clampf((cd - outer) / (inner - outer + 1e-6f), 0.0f, 1.0f);
}
if (shadows) {
Vec3 target = L.type == LightType::Sun ? origin + toL * 100000.0f : L.pos;
if (cm.trace(origin, target, CONTENTS_SOLID).hit) continue;
}
col += L.color * (L.intensity * ndl * atten);
}
return col;
}
} // namespace
// ================================================================ public API
std::vector<Light> collectLights(const Map& map) {
std::vector<Light> out;
for (const Entity& e : map.entities) {
Light l;
l.entityId = e.id;
l.pos = e.origin;
l.color = e.getColor("color", {1, 1, 1});
l.intensity = e.getFloat("intensity", 1.0f);
if (e.classname == "light") {
l.type = LightType::Point;
l.radius = e.getFloat("radius", 512.0f);
} else if (e.classname == "light_spot") {
l.type = LightType::Spot;
l.dir = e.forward();
l.radius = e.getFloat("radius", 768.0f);
l.innerDeg = e.getFloat("cone_inner", 25.0f);
l.outerDeg = e.getFloat("cone_outer", 40.0f);
} else if (e.classname == "light_environment") {
l.type = LightType::Sun;
l.dir = e.forward();
} else {
continue;
}
out.push_back(l);
}
return out;
}
void buildEditBatches(const Map& map, std::vector<Batch>& out) {
out.clear();
BatchSet bs(out);
for (const Brush& b : map.brushes) {
if (b.flags & OBJ_HIDDEN) continue;
BrushGeometry g = buildBrushGeometry(b);
for (size_t i = 0; i < b.faces.size(); ++i)
if (g.polys[i].size() >= 3)
emitPolygon(bs.get(0, b.faces[i].material, GeomSource::Bsp), g.polys[i], b.faces[i].plane.n, b.faces[i].uv,
matOf(map.materials, b.faces[i].material));
}
for (const MeshObject& m : map.meshes) {
if (m.flags & OBJ_HIDDEN) continue;
emitMesh(bs, m, 0, map.materials, nullptr);
}
}
CompiledMap compile(const Map& map, const CompileOptions& opt) {
auto t0 = std::chrono::steady_clock::now();
CompiledMap cm;
cm.materials = map.materials;
cm.entities = map.entities;
cm.ambient = map.settings.ambient;
cm.unitsPerMeter = map.settings.unitsPerMeter;
cm.models.emplace_back(); // world
std::unordered_map<uint32_t, const Entity*> entById;
for (const Entity& e : map.entities) entById[e.id] = &e;
std::map<uint32_t, int32_t> modelOf;
auto modelFor = [&](uint32_t owner) -> int32_t {
if (owner == 0) return 0;
auto it = entById.find(owner);
if (it != entById.end() && it->second->classname == "func_detail") return 0;
auto m = modelOf.find(owner);
if (m != modelOf.end()) return m->second;
cm.models.emplace_back();
cm.models.back().entityId = owner;
modelOf[owner] = (int32_t)cm.models.size() - 1;
return modelOf[owner];
};
struct Item { const Brush* brush; BrushGeometry geo; int32_t model; };
std::vector<Item> items;
for (const Brush& b : map.brushes) {
BrushGeometry g = buildBrushGeometry(b);
if (!g.valid) continue;
items.push_back({&b, std::move(g), modelFor(b.owner)});
}
BatchSet bs(cm.batches);
std::vector<size_t> worldIdx;
for (size_t i = 0; i < items.size(); ++i)
if (items[i].model == 0) worldIdx.push_back(i);
std::vector<CollisionBrush> worldColl;
for (size_t ii = 0; ii < items.size(); ++ii) {
const Item& A = items[ii];
const Brush& br = *A.brush;
cm.stats.brushes++;
if (br.contents) {
CollisionBrush cb = makeCollision(br, A.geo);
if (A.model == 0) worldColl.push_back(cb);
else cm.models[(size_t)A.model].brushes.push_back(cb);
}
cm.models[(size_t)A.model].bounds.add(A.geo.bounds);
cm.bounds.add(A.geo.bounds);
if (!isRenderable(br)) continue;
for (size_t fi = 0; fi < br.faces.size(); ++fi) {
const Face& face = br.faces[fi];
const Polygon& poly = A.geo.polys[fi];
if (poly.size() < 3 || (face.flags & FACE_NODRAW)) continue;
std::vector<Polygon> frags{poly};
if (opt.csg && A.model == 0) {
for (size_t wj : worldIdx) {
if (wj == ii) continue;
const Item& B = items[wj];
if (!(B.brush->contents & CONTENTS_SOLID)) continue;
if (!B.geo.bounds.overlaps(A.geo.bounds, 0.02f)) continue;
std::vector<Polygon> next;
for (const Polygon& fr : frags) clipOutside(fr, face.plane.n, *B.brush, B.geo.bounds, wj < ii, next);
frags.swap(next);
if (frags.empty()) break;
}
}
for (const Polygon& fr : frags) {
emitPolygon(bs.get(A.model, face.material, GeomSource::Bsp), fr, face.plane.n, face.uv, matOf(map.materials, face.material));
cm.stats.faces++;
}
}
}
for (const MeshObject& m : map.meshes) {
if (m.verts.empty() || m.polys.empty()) continue;
int32_t model = modelFor(m.owner);
CollisionMesh cmesh;
bool collide = model == 0 && !(m.flags & OBJ_NOCOLLIDE);
emitMesh(bs, m, model, map.materials, collide ? &cmesh : nullptr);
AABB bb = meshBounds(m);
cm.models[(size_t)model].bounds.add(bb);
cm.bounds.add(bb);
cm.stats.meshes++;
if (collide && !cmesh.tris.empty()) {
cmesh.objectId = m.id;
cmesh.bounds = bb;
cm.collMeshes.push_back(std::move(cmesh));
}
}
cm.bsp.build(opt.bsp ? std::move(worldColl) : std::vector<CollisionBrush>());
cm.lights = collectLights(map);
for (const Entity& e : map.entities)
if (e.classname == "light_environment" && e.find("ambient")) cm.ambient = e.getColor("ambient", cm.ambient);
if (opt.bakeLighting) bakeLighting(cm, opt.lightEdge, opt.shadows, opt.threads);
for (const Batch& b : cm.batches) {
cm.stats.triangles += (uint32_t)(b.indices.size() / 3);
cm.stats.vertices += (uint32_t)b.vertices.size();
}
cm.stats.bspNodes = (uint32_t)cm.bsp.nodes.size();
cm.stats.bspLeaves = (uint32_t)cm.bsp.leaves.size();
cm.stats.bspDepth = cm.bsp.depth;
cm.stats.lights = (uint32_t)cm.lights.size();
cm.stats.ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - t0).count();
return cm;
}
void bakeLighting(CompiledMap& cm, float maxEdge, bool shadows, int threads) {
maxEdge = std::max(4.0f, maxEdge);
size_t budget = 1500000; // triangle cap so huge maps cannot explode
for (Batch& b : cm.batches) {
std::vector<Vertex> vs;
for (size_t i = 0; i + 2 < b.indices.size(); i += 3)
subdivide(b.vertices[b.indices[i]], b.vertices[b.indices[i + 1]], b.vertices[b.indices[i + 2]], maxEdge * maxEdge, vs, 0, budget);
b.vertices = std::move(vs);
b.indices.resize(b.vertices.size());
std::iota(b.indices.begin(), b.indices.end(), 0u);
weldBatch(b);
}
std::vector<Vertex*> all;
for (Batch& b : cm.batches)
for (Vertex& v : b.vertices) all.push_back(&v);
const CompiledMap& cref = cm;
std::atomic<size_t> next{0};
auto work = [&]() {
const size_t chunk = 1024;
for (;;) {
size_t s = next.fetch_add(chunk);
if (s >= all.size()) break;
size_t e = std::min(all.size(), s + chunk);
for (size_t i = s; i < e; ++i) all[i]->color = packColor(shade(cref, all[i]->pos, all[i]->normal, shadows));
}
};
int n = threads > 0 ? threads : (int)std::thread::hardware_concurrency();
n = std::max(1, std::min(n, 16));
if (n == 1) {
work();
} else {
std::vector<std::thread> pool;
for (int i = 0; i < n; ++i) pool.emplace_back(work);
for (auto& t : pool) t.join();
}
cm.baked = true;
}
} // namespace hmap
+188
View File
@@ -0,0 +1,188 @@
#include "hmap/hmap.h"
#include <cstdio>
#include <cstdlib>
#include <sstream>
namespace hmap {
namespace {
const char* kBuiltin = R"DEF(
# Hammer++ built-in entity definitions (same syntax as assets/entities.def)
# @point|@brush <class> "<label>" [color(r,g,b)] [size(h)|size(hx,hy,hz)] [dir] [model] [contents(solid|clip|trigger|water|sky)]
# <key> <type> "<label>" "<default>" ["a=Label A,b=Label B"] (types: string int float bool color255 vec3 choice model material target)
@point info_player_start "Player start" color(70,230,90) size(16,36,16) dir
@point info_target "Target / marker" color(190,190,190) size(6)
@point light "Point light" color(255,225,110) size(6)
color color255 "Color" "255 240 215"
intensity float "Intensity" "1.0"
radius float "Radius" "512"
@point light_spot "Spot light" color(255,190,90) size(6) dir
color color255 "Color" "255 240 215"
intensity float "Intensity" "1.2"
radius float "Radius" "768"
cone_inner float "Inner cone (deg)" "25"
cone_outer float "Outer cone (deg)" "40"
@point light_environment "Sun / environment" color(255,205,70) size(10) dir
color color255 "Sun color" "255 244 222"
intensity float "Intensity" "0.9"
ambient color255 "Ambient" "42 46 58"
@point prop_static "Static prop" color(120,170,255) size(16) model
model model "Model" ""
solid bool "Collide" "1"
tint color255 "Tint" "255 255 255"
@point prop_dynamic "Dynamic prop" color(110,210,255) size(16) model
model model "Model" ""
solid bool "Collide" "1"
animation string "Animation" ""
@point sound_ambient "Ambient sound" color(200,130,255) size(8)
sound string "Sound file" ""
volume float "Volume" "1.0"
radius float "Radius" "512"
@point item_spawn "Item spawn" color(255,150,200) size(10)
item string "Item" ""
respawn float "Respawn time" "30"
@brush func_detail "Detail brush" color(120,160,255)
@brush func_wall "Wall (own model)" color(170,170,190)
@brush func_door "Door" color(210,165,90)
speed float "Speed" "100"
movedir vec3 "Move direction" "0 1 0"
distance float "Distance" "64"
wait float "Wait before closing" "3"
@brush func_button "Button" color(230,120,120)
target target "Target" ""
delay float "Delay" "0"
@brush trigger_multiple "Trigger (repeatable)" color(255,120,40) contents(trigger)
target target "Target" ""
wait float "Re-arm delay" "1"
@brush trigger_once "Trigger (once)" color(255,170,60) contents(trigger)
target target "Target" ""
@brush func_clip "Player clip" color(120,120,255) contents(clip)
@brush water "Water volume" color(70,120,255) contents(water)
density float "Density" "1.0"
)DEF";
std::vector<std::string> tokenize(const std::string& line) {
std::vector<std::string> t;
size_t i = 0, n = line.size();
while (i < n) {
while (i < n && (line[i] == ' ' || line[i] == '\t' || line[i] == '\r')) ++i;
if (i >= n) break;
if (line[i] == '"') {
size_t j = line.find('"', i + 1);
if (j == std::string::npos) j = n;
t.push_back(line.substr(i + 1, j - i - 1));
i = j + 1;
} else {
size_t j = i;
while (j < n && line[j] != ' ' && line[j] != '\t' && line[j] != '\r') ++j;
t.push_back(line.substr(i, j - i));
i = j;
}
}
return t;
}
std::vector<float> numbersIn(const std::string& tok) {
std::vector<float> v;
size_t a = tok.find('('), b = tok.find(')');
if (a == std::string::npos || b == std::string::npos || b < a) return v;
std::string inner = tok.substr(a + 1, b - a - 1);
for (char& c : inner) if (c == ',') c = ' ';
std::istringstream ss(inner);
float f;
while (ss >> f) v.push_back(f);
return v;
}
bool startsWith(const std::string& s, const char* p) { return s.rfind(p, 0) == 0; }
} // namespace
const EntityClass* EntityRegistry::find(const std::string& name) const {
for (const EntityClass& c : classes)
if (c.name == name) return &c;
return nullptr;
}
Status EntityRegistry::parse(const std::string& text) {
std::istringstream in(text);
std::string line;
int lineNo = 0;
EntityClass* cur = nullptr;
while (std::getline(in, line)) {
++lineNo;
size_t first = line.find_first_not_of(" \t\r");
if (first == std::string::npos || line[first] == '#') continue;
std::vector<std::string> t = tokenize(line);
if (t.empty()) continue;
if (t[0][0] == '@') {
if (t.size() < 2 || (t[0] != "@point" && t[0] != "@brush"))
return Status::Fail("entity defs line " + std::to_string(lineNo) + ": expected @point or @brush <class>");
EntityClass c;
c.name = t[1];
c.label = t.size() > 2 ? t[2] : t[1];
c.brush = t[0] == "@brush";
if (c.brush) c.brushContents = CONTENTS_SOLID;
for (size_t i = 3; i < t.size(); ++i) {
if (startsWith(t[i], "color(")) {
auto v = numbersIn(t[i]);
if (v.size() >= 3) c.color = {v[0] / 255.0f, v[1] / 255.0f, v[2] / 255.0f};
} else if (startsWith(t[i], "size(")) {
auto v = numbersIn(t[i]);
if (v.size() == 1) c.half = {v[0], v[0], v[0]};
else if (v.size() >= 3) c.half = {v[0], v[1], v[2]};
} else if (t[i] == "dir") c.directional = true;
else if (t[i] == "model") c.hasModel = true;
else if (startsWith(t[i], "contents(")) {
std::string v = t[i].substr(9);
if (v.find("trigger") == 0) c.brushContents = CONTENTS_TRIGGER;
else if (v.find("clip") == 0) c.brushContents = CONTENTS_CLIP;
else if (v.find("water") == 0) c.brushContents = CONTENTS_WATER;
else if (v.find("sky") == 0) c.brushContents = CONTENTS_SKY;
else c.brushContents = CONTENTS_SOLID;
}
}
bool replaced = false;
for (EntityClass& ex : classes)
if (ex.name == c.name) { ex = c; cur = &ex; replaced = true; break; }
if (!replaced) { classes.push_back(c); cur = &classes.back(); }
} else {
if (!cur) return Status::Fail("entity defs line " + std::to_string(lineNo) + ": property before any class");
if (t.size() < 3) return Status::Fail("entity defs line " + std::to_string(lineNo) + ": expected <key> <type> \"<label>\"");
PropDef p;
p.key = t[0];
p.type = t[1];
p.label = t[2];
if (t.size() > 3) p.def = t[3];
if (t.size() > 4) p.choices = t[4];
cur->props.push_back(p);
}
}
return Status::Ok();
}
void EntityRegistry::applyDefaults(Entity& e) const {
const EntityClass* c = find(e.classname);
if (!c) return;
for (const PropDef& p : c->props)
if (!p.def.empty() && !e.find(p.key)) e.set(p.key, p.def);
}
EntityRegistry EntityRegistry::builtin() {
EntityRegistry r;
r.parse(kBuiltin);
return r;
}
} // namespace hmap
+464
View File
@@ -0,0 +1,464 @@
#include "hmap/hmap.h"
#include <cstdio>
#include <cstring>
#include <fstream>
#include <map>
#include <set>
#include <sstream>
namespace hmap {
namespace {
// ---------------------------------------------------------------- tiny FBX node tree
struct FProp {
char type = 0;
int64_t i = 0;
double d = 0;
std::string s;
std::vector<double> da;
std::vector<int32_t> ia;
};
struct FNode {
std::string name;
std::vector<FProp> props;
std::vector<FNode> kids;
FNode() = default;
explicit FNode(std::string n) : name(std::move(n)) {}
FNode& i32(int32_t v) { FProp p; p.type = 'I'; p.i = v; props.push_back(std::move(p)); return *this; }
FNode& i64(int64_t v) { FProp p; p.type = 'L'; p.i = v; props.push_back(std::move(p)); return *this; }
FNode& f64(double v) { FProp p; p.type = 'D'; p.d = v; props.push_back(std::move(p)); return *this; }
FNode& boolean(bool v) { FProp p; p.type = 'C'; p.i = v; props.push_back(std::move(p)); return *this; }
FNode& str(const std::string& v) { FProp p; p.type = 'S'; p.s = v; props.push_back(std::move(p)); return *this; }
FNode& raw(const std::string& v) { FProp p; p.type = 'R'; p.s = v; props.push_back(std::move(p)); return *this; }
FNode& darr(std::vector<double> v) { FProp p; p.type = 'd'; p.da = std::move(v); props.push_back(std::move(p)); return *this; }
FNode& iarr(std::vector<int32_t> v) { FProp p; p.type = 'i'; p.ia = std::move(v); props.push_back(std::move(p)); return *this; }
FNode& kid(FNode n) { kids.push_back(std::move(n)); return *this; }
};
class Bin {
public:
std::vector<uint8_t> b;
void u8(uint8_t v) { b.push_back(v); }
void u32(uint32_t v) { for (int i = 0; i < 4; ++i) b.push_back((uint8_t)((v >> (8 * i)) & 0xff)); }
void u64(uint64_t v) { for (int i = 0; i < 8; ++i) b.push_back((uint8_t)((v >> (8 * i)) & 0xff)); }
void f64(double d) { uint64_t u; std::memcpy(&u, &d, 8); u64(u); }
void bytes(const void* p, size_t n) { const uint8_t* q = (const uint8_t*)p; b.insert(b.end(), q, q + n); }
void zeros(size_t n) { b.insert(b.end(), n, 0); }
void patch32(size_t at, uint32_t v) { for (int i = 0; i < 4; ++i) b[at + (size_t)i] = (uint8_t)((v >> (8 * i)) & 0xff); }
};
void writeProp(Bin& o, const FProp& p) {
o.u8((uint8_t)p.type);
switch (p.type) {
case 'C': o.u8(p.i ? 1 : 0); break;
case 'I': o.u32((uint32_t)(int32_t)p.i); break;
case 'L': o.u64((uint64_t)p.i); break;
case 'D': o.f64(p.d); break;
case 'S':
case 'R': o.u32((uint32_t)p.s.size()); o.bytes(p.s.data(), p.s.size()); break;
case 'd':
o.u32((uint32_t)p.da.size()); o.u32(0); o.u32((uint32_t)(p.da.size() * 8));
for (double d : p.da) o.f64(d);
break;
case 'i':
o.u32((uint32_t)p.ia.size()); o.u32(0); o.u32((uint32_t)(p.ia.size() * 4));
for (int32_t v : p.ia) o.u32((uint32_t)v);
break;
default: break;
}
}
constexpr size_t kSentinel = 13; // null record size for FBX < 7.5
void writeNode(Bin& o, const FNode& n) {
size_t start = o.b.size();
o.u32(0); // end offset (patched)
o.u32((uint32_t)n.props.size());
size_t plenAt = o.b.size();
o.u32(0); // property list length (patched)
o.u8((uint8_t)n.name.size());
o.bytes(n.name.data(), n.name.size());
size_t propStart = o.b.size();
for (const FProp& p : n.props) writeProp(o, p);
o.patch32(plenAt, (uint32_t)(o.b.size() - propStart));
if (!n.kids.empty()) {
for (const FNode& k : n.kids) writeNode(o, k);
o.zeros(kSentinel);
}
o.patch32(start, (uint32_t)o.b.size());
}
const std::string kSep("\x00\x01", 2); // FBX binary form of "Name::Class"
std::string fname(const std::string& name, const char* cls) { return name + kSep + cls; }
FNode P(const char* name, const char* type, const char* label, const char* flags) {
FNode p("P");
p.str(name).str(type).str(label).str(flags);
return p;
}
FNode ver(int v) { FNode n("Version"); n.i32(v); return n; }
FNode sval(const char* key, const std::string& v) { FNode n(key); n.str(v); return n; }
std::string safeName(const std::string& s) {
std::string r;
for (char c : s) r += (c == '\0' || c == '\n' || c == '\r') ? '_' : c;
return r.empty() ? "unnamed" : r;
}
} // namespace
std::vector<uint8_t> exportFBXToMemory(const CompiledMap& cm, const FbxOptions& opt) {
int64_t nextId = 1000000;
auto newId = [&]() { return nextId++; };
struct Conn { bool prop; int64_t child, parent; std::string pname; };
std::vector<FNode> objects;
std::vector<Conn> conns;
int nModel = 0, nGeo = 0, nMat = 0, nTex = 0;
const double sc = opt.scale;
// ---- materials (created lazily, shared by all models)
std::map<int32_t, int64_t> matIds;
auto getMaterial = [&](int32_t mi) -> int64_t {
auto it = matIds.find(mi);
if (it != matIds.end()) return it->second;
Material m;
if (mi >= 0 && (size_t)mi < cm.materials.size()) m = cm.materials[(size_t)mi];
else m.name = "default";
int64_t id = newId();
std::string nm = safeName(m.name);
FNode mat("Material");
mat.i64(id).str(fname(nm, "Material")).str("");
mat.kid(ver(102)).kid(sval("ShadingModel", "lambert")).kid(FNode("MultiLayer").i32(0));
FNode props("Properties70");
props.kid(P("DiffuseColor", "Color", "", "A").f64(m.color.x).f64(m.color.y).f64(m.color.z));
props.kid(P("DiffuseFactor", "Number", "", "A").f64(1.0));
mat.kid(std::move(props));
objects.push_back(std::move(mat));
++nMat;
if (!m.texture.empty()) {
int64_t tid = newId();
std::string path = m.texture;
for (char& c : path) if (c == '\\') c = '/';
FNode tex("Texture");
tex.i64(tid).str(fname(nm + "_tex", "Texture")).str("");
tex.kid(sval("Type", "TextureVideoClip")).kid(ver(202)).kid(sval("TextureName", fname(nm + "_tex", "Texture")));
FNode tp("Properties70");
tp.kid(P("UseMaterial", "bool", "", "").i32(1));
tex.kid(std::move(tp));
tex.kid(sval("Media", "")).kid(sval("FileName", path)).kid(sval("RelativeFilename", path));
objects.push_back(std::move(tex));
conns.push_back({true, tid, id, "DiffuseColor"});
++nTex;
}
matIds[mi] = id;
return id;
};
auto userProps = [&](FNode& props, const Entity& e) {
props.kid(P("classname", "KString", "", "U").str(e.classname));
props.kid(P("hmap_id", "int", "Integer", "U").i32((int32_t)e.id));
for (const KeyValue& kv : e.props)
if (!kv.key.empty()) props.kid(P(safeName(kv.key).c_str(), "KString", "", "U").str(kv.value));
};
std::set<uint32_t> modelEntities;
for (const CompiledModel& m : cm.models)
if (m.entityId) modelEntities.insert(m.entityId);
// ---- one Model + Geometry per compiled model (world / brush entities)
for (size_t mi = 0; mi < cm.models.size(); ++mi) {
if (mi > 0 && !opt.includeSubModels) continue;
std::vector<const Batch*> bl;
for (const Batch& b : cm.batches)
if (b.model == (int32_t)mi && !b.indices.empty()) bl.push_back(&b);
if (bl.empty()) continue;
const Entity* owner = nullptr;
if (mi > 0)
for (const Entity& e : cm.entities)
if (e.id == cm.models[mi].entityId) owner = &e;
std::string name = mi == 0 ? "World" : (owner ? owner->classname + "_" + std::to_string(owner->id) : "Model" + std::to_string(mi));
std::vector<double> verts, normals, uvs, colors;
std::vector<int32_t> pvi, uvIdx, polyMat;
std::vector<int32_t> slotMat; // local slot -> global material
std::map<int32_t, int32_t> slotOf;
bool useColors = opt.vertexColors && cm.baked;
int32_t cp = 0;
for (const Batch* b : bl) {
auto sit = slotOf.find(b->material);
if (sit == slotOf.end()) {
sit = slotOf.emplace(b->material, (int32_t)slotMat.size()).first;
slotMat.push_back(b->material);
}
int32_t base = cp;
for (const Vertex& v : b->vertices) {
verts.push_back(v.pos.x * sc); verts.push_back(v.pos.y * sc); verts.push_back(v.pos.z * sc);
uvs.push_back(v.uv.x); uvs.push_back(1.0 - v.uv.y);
if (useColors) {
colors.push_back((double)(v.color & 0xff) / 255.0);
colors.push_back((double)((v.color >> 8) & 0xff) / 255.0);
colors.push_back((double)((v.color >> 16) & 0xff) / 255.0);
colors.push_back((double)((v.color >> 24) & 0xff) / 255.0);
}
++cp;
}
for (size_t i = 0; i + 2 < b->indices.size(); i += 3) {
int32_t a = base + (int32_t)b->indices[i], c1 = base + (int32_t)b->indices[i + 1], c2 = base + (int32_t)b->indices[i + 2];
pvi.push_back(a); pvi.push_back(c1); pvi.push_back(-c2 - 1); // last index of a polygon is stored as ~i
uvIdx.push_back(a); uvIdx.push_back(c1); uvIdx.push_back(c2);
for (size_t k = 0; k < 3; ++k) {
const Vec3& n = b->vertices[b->indices[i + k]].normal;
normals.push_back(n.x); normals.push_back(n.y); normals.push_back(n.z);
}
polyMat.push_back(sit->second);
}
}
int64_t geoId = newId(), modelId = newId();
FNode geo("Geometry");
geo.i64(geoId).str(fname(name, "Geometry")).str("Mesh");
geo.kid(FNode("Vertices").darr(std::move(verts)));
geo.kid(FNode("PolygonVertexIndex").iarr(pvi));
geo.kid(FNode("GeometryVersion").i32(124));
{
FNode e("LayerElementNormal");
e.i32(0).kid(ver(101)).kid(sval("Name", "")).kid(sval("MappingInformationType", "ByPolygonVertex"))
.kid(sval("ReferenceInformationType", "Direct")).kid(FNode("Normals").darr(std::move(normals)));
geo.kid(std::move(e));
}
{
FNode e("LayerElementUV");
e.i32(0).kid(ver(101)).kid(sval("Name", "UVMap")).kid(sval("MappingInformationType", "ByPolygonVertex"))
.kid(sval("ReferenceInformationType", "IndexToDirect")).kid(FNode("UV").darr(std::move(uvs)))
.kid(FNode("UVIndex").iarr(uvIdx));
geo.kid(std::move(e));
}
if (useColors) {
FNode e("LayerElementColor");
e.i32(0).kid(ver(101)).kid(sval("Name", "BakedLight")).kid(sval("MappingInformationType", "ByPolygonVertex"))
.kid(sval("ReferenceInformationType", "IndexToDirect")).kid(FNode("Colors").darr(std::move(colors)))
.kid(FNode("ColorIndex").iarr(uvIdx));
geo.kid(std::move(e));
}
{
FNode e("LayerElementMaterial");
e.i32(0).kid(ver(101)).kid(sval("Name", "")).kid(sval("MappingInformationType", "ByPolygon"))
.kid(sval("ReferenceInformationType", "IndexToDirect")).kid(FNode("Materials").iarr(std::move(polyMat)));
geo.kid(std::move(e));
}
{
FNode layer("Layer");
layer.i32(0).kid(ver(100));
auto le = [&](const char* type) {
FNode l("LayerElement");
l.kid(sval("Type", type)).kid(FNode("TypedIndex").i32(0));
return l;
};
layer.kid(le("LayerElementNormal")).kid(le("LayerElementMaterial")).kid(le("LayerElementUV"));
if (useColors) layer.kid(le("LayerElementColor"));
geo.kid(std::move(layer));
}
objects.push_back(std::move(geo));
++nGeo;
FNode model("Model");
model.i64(modelId).str(fname(name, "Model")).str("Mesh");
model.kid(ver(232));
FNode props("Properties70");
if (owner) userProps(props, *owner);
model.kid(std::move(props));
model.kid(FNode("Shading").boolean(true)).kid(sval("Culling", "CullingOff"));
objects.push_back(std::move(model));
++nModel;
conns.push_back({false, modelId, 0, ""});
conns.push_back({false, geoId, modelId, ""});
for (int32_t gm : slotMat) conns.push_back({false, getMaterial(gm), modelId, ""});
}
// ---- entities / props as Null nodes carrying their key-values as custom properties
if (opt.entities) {
for (const Entity& e : cm.entities) {
if (modelEntities.count(e.id)) continue;
int64_t id = newId();
std::string nm = safeName(e.classname + "_" + std::to_string(e.id));
FNode model("Model");
model.i64(id).str(fname(nm, "Model")).str("Null");
model.kid(ver(232));
FNode props("Properties70");
Vec3 eul = Mat34::fromEuler(e.angles).toEulerXYZ();
props.kid(P("Lcl Translation", "Lcl Translation", "", "A").f64(e.origin.x * sc).f64(e.origin.y * sc).f64(e.origin.z * sc));
props.kid(P("Lcl Rotation", "Lcl Rotation", "", "A").f64(eul.x).f64(eul.y).f64(eul.z));
props.kid(P("Lcl Scaling", "Lcl Scaling", "", "A").f64(e.scale.x).f64(e.scale.y).f64(e.scale.z));
userProps(props, e);
model.kid(std::move(props));
model.kid(FNode("Shading").boolean(true)).kid(sval("Culling", "CullingOff"));
objects.push_back(std::move(model));
conns.push_back({false, id, 0, ""});
++nModel;
}
}
// ---- assemble the file
double usf = 100.0 / ((double)(cm.unitsPerMeter > 0 ? cm.unitsPerMeter : 64.0f) * (sc > 0 ? sc : 1.0));
std::vector<FNode> top;
{
FNode h("FBXHeaderExtension");
h.kid(FNode("FBXHeaderVersion").i32(1003)).kid(FNode("FBXVersion").i32(7400));
FNode ts("CreationTimeStamp");
ts.kid(ver(1000)).kid(FNode("Year").i32(2026)).kid(FNode("Month").i32(1)).kid(FNode("Day").i32(1))
.kid(FNode("Hour").i32(0)).kid(FNode("Minute").i32(0)).kid(FNode("Second").i32(0)).kid(FNode("Millisecond").i32(0));
h.kid(std::move(ts)).kid(sval("Creator", "Hammer++ libhmap"));
FNode si("SceneInfo");
si.str(fname("GlobalInfo", "SceneInfo")).str("UserData");
si.kid(sval("Type", "UserData")).kid(ver(100));
FNode md("MetaData");
md.kid(ver(100)).kid(sval("Title", "")).kid(sval("Subject", "")).kid(sval("Author", "")).kid(sval("Keywords", ""))
.kid(sval("Revision", "")).kid(sval("Comment", ""));
si.kid(std::move(md)).kid(FNode("Properties70"));
h.kid(std::move(si));
top.push_back(std::move(h));
}
top.push_back(FNode("FileId").raw(std::string("\x28\xb3\x2a\xeb\xb6\x24\xcc\xc2\xbf\xc8\xb0\x2a\xa9\x2b\xfc\xf1", 16)));
top.push_back(sval("CreationTime", "1970-01-01 10:00:00:000"));
top.push_back(sval("Creator", "Hammer++ libhmap"));
{
FNode g("GlobalSettings");
g.kid(ver(1000));
FNode p("Properties70");
p.kid(P("UpAxis", "int", "Integer", "").i32(1)).kid(P("UpAxisSign", "int", "Integer", "").i32(1));
p.kid(P("FrontAxis", "int", "Integer", "").i32(2)).kid(P("FrontAxisSign", "int", "Integer", "").i32(1));
p.kid(P("CoordAxis", "int", "Integer", "").i32(0)).kid(P("CoordAxisSign", "int", "Integer", "").i32(1));
p.kid(P("UnitScaleFactor", "double", "Number", "").f64(usf)).kid(P("OriginalUnitScaleFactor", "double", "Number", "").f64(usf));
g.kid(std::move(p));
top.push_back(std::move(g));
}
{
FNode d("Documents");
d.kid(FNode("Count").i32(1));
FNode doc("Document");
doc.i64(newId()).str("").str("Scene");
FNode dp("Properties70");
dp.kid(P("SourceObject", "object", "", "")).kid(P("ActiveAnimStackName", "KString", "", "").str(""));
doc.kid(std::move(dp)).kid(FNode("RootNode").i64(0));
d.kid(std::move(doc));
top.push_back(std::move(d));
}
top.push_back(FNode("References"));
{
FNode d("Definitions");
d.kid(ver(100)).kid(FNode("Count").i32(nModel + nGeo + nMat + nTex + 1));
auto ot = [&](const char* type, int n) {
FNode o("ObjectType");
o.str(type).kid(FNode("Count").i32(n));
return o;
};
d.kid(ot("GlobalSettings", 1));
if (nModel) d.kid(ot("Model", nModel));
if (nGeo) d.kid(ot("Geometry", nGeo));
if (nMat) d.kid(ot("Material", nMat));
if (nTex) d.kid(ot("Texture", nTex));
top.push_back(std::move(d));
}
{
FNode o("Objects");
for (FNode& n : objects) o.kid(std::move(n));
top.push_back(std::move(o));
}
{
FNode c("Connections");
for (const Conn& cn : conns) {
FNode n("C");
n.str(cn.prop ? "OP" : "OO").i64(cn.child).i64(cn.parent);
if (cn.prop) n.str(cn.pname);
c.kid(std::move(n));
}
top.push_back(std::move(c));
}
top.push_back(FNode("Takes").kid(sval("Current", "")));
Bin out;
const char magic[] = "Kaydara FBX Binary ";
out.bytes(magic, 20);
out.u8(0); out.u8(0x1a); out.u8(0);
out.u32(7400);
for (const FNode& n : top) writeNode(out, n);
out.zeros(kSentinel);
static const uint8_t footId[16] = {0xfa, 0xbc, 0xab, 0x09, 0xd0, 0xc8, 0xd4, 0x66, 0xb1, 0x76, 0xfb, 0x83, 0x1c, 0xf7, 0x26, 0x7e};
static const uint8_t footEnd[16] = {0xf8, 0x5a, 0x8c, 0x6a, 0xde, 0xf5, 0xd9, 0x7e, 0xec, 0xe9, 0x0c, 0xe3, 0x75, 0x8f, 0x29, 0x0b};
out.bytes(footId, 16);
out.zeros(4);
size_t pad = ((out.b.size() + 15) & ~(size_t)15) - out.b.size();
out.zeros(pad == 0 ? 16 : pad);
out.u32(7400);
out.zeros(120);
out.bytes(footEnd, 16);
return std::move(out.b);
}
Status exportFBX(const std::string& path, const CompiledMap& cm, const FbxOptions& opt) {
std::vector<uint8_t> data = exportFBXToMemory(cm, opt);
std::ofstream f(path, std::ios::binary | std::ios::trunc);
if (!f) return Status::Fail("cannot write " + path);
f.write((const char*)data.data(), (std::streamsize)data.size());
return f ? Status::Ok() : Status::Fail("write failed: " + path);
}
// ================================================================ OBJ
Status exportOBJ(const std::string& path, const CompiledMap& cm, float scale) {
std::ofstream f(path, std::ios::trunc);
if (!f) return Status::Fail("cannot write " + path);
std::string mtlPath = path;
size_t dp = mtlPath.find_last_of('.');
mtlPath = (dp == std::string::npos ? mtlPath : mtlPath.substr(0, dp)) + ".mtl";
size_t sl = mtlPath.find_last_of("/\\");
f << "# Hammer++ export\nmtllib " << (sl == std::string::npos ? mtlPath : mtlPath.substr(sl + 1)) << "\n";
auto mname = [&](int32_t mi) {
std::string n = (mi >= 0 && (size_t)mi < cm.materials.size()) ? cm.materials[(size_t)mi].name : "default";
for (char& c : n) if (c == ' ' || c == '/' || c == '\\') c = '_';
return n;
};
char line[160];
size_t base = 1;
for (const Batch& b : cm.batches) {
if (b.indices.empty()) continue;
f << "o " << (b.model == 0 ? "world" : "model" + std::to_string(b.model)) << "_" << mname(b.material)
<< (b.source == GeomSource::Mesh ? "_mesh" : "_bsp") << "\nusemtl " << mname(b.material) << "\n";
for (const Vertex& v : b.vertices) {
std::snprintf(line, sizeof line, "v %.5f %.5f %.5f\n", v.pos.x * scale, v.pos.y * scale, v.pos.z * scale);
f << line;
}
for (const Vertex& v : b.vertices) {
std::snprintf(line, sizeof line, "vt %.5f %.5f\n", v.uv.x, 1.0f - v.uv.y);
f << line;
}
for (const Vertex& v : b.vertices) {
std::snprintf(line, sizeof line, "vn %.5f %.5f %.5f\n", v.normal.x, v.normal.y, v.normal.z);
f << line;
}
for (size_t i = 0; i + 2 < b.indices.size(); i += 3) {
f << "f";
for (size_t k = 0; k < 3; ++k) {
size_t id = base + b.indices[i + k];
f << " " << id << "/" << id << "/" << id;
}
f << "\n";
}
base += b.vertices.size();
}
std::ofstream m(mtlPath, std::ios::trunc);
if (m)
for (size_t i = 0; i < cm.materials.size(); ++i) {
const Material& mt = cm.materials[i];
m << "newmtl " << mname((int32_t)i) << "\nKd " << mt.color.x << " " << mt.color.y << " " << mt.color.z << "\n";
if (!mt.texture.empty()) m << "map_Kd " << mt.texture << "\n";
m << "\n";
}
return f ? Status::Ok() : Status::Fail("write failed: " + path);
}
} // namespace hmap
+655
View File
@@ -0,0 +1,655 @@
#include "hmap/hmap.h"
#include <array>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <map>
#include <sstream>
namespace hmap {
// ============================================================ polygon building (double precision)
namespace {
struct DV { double x, y, z; };
using DPoly = std::vector<DV>;
double ddist(const Plane& p, const DV& v) {
return (double)p.n.x * v.x + (double)p.n.y * v.y + (double)p.n.z * v.z - (double)p.d;
}
DPoly basePoly(const Plane& p) {
Vec3 ref = std::fabs(p.n.y) < 0.9f ? Vec3(0, 1, 0) : Vec3(1, 0, 0);
Vec3 t = normalize(cross(p.n, ref));
Vec3 b = cross(p.n, t);
const double S = 65536.0;
DV o{(double)p.n.x * p.d, (double)p.n.y * p.d, (double)p.n.z * p.d};
auto P = [&](double a, double c) {
return DV{o.x + t.x * a + b.x * c, o.y + t.y * a + b.y * c, o.z + t.z * a + b.z * c};
};
return {P(-S, -S), P(S, -S), P(S, S), P(-S, S)}; // CCW seen from +n
}
// Keep the part of poly on the back (<= 0) side of the plane.
void clipBack(DPoly& poly, const Plane& pl, double eps) {
size_t n = poly.size();
if (n == 0) return;
std::vector<double> d(n);
std::vector<int> s(n);
int nf = 0, nb = 0;
for (size_t i = 0; i < n; ++i) {
d[i] = ddist(pl, poly[i]);
s[i] = d[i] > eps ? 1 : (d[i] < -eps ? -1 : 0);
nf += s[i] > 0;
nb += s[i] < 0;
}
if (nf == 0) return;
if (nb == 0) { poly.clear(); return; }
DPoly out;
for (size_t i = 0; i < n; ++i) {
size_t j = (i + 1) % n;
if (s[i] <= 0) out.push_back(poly[i]);
if ((s[i] > 0 && s[j] < 0) || (s[i] < 0 && s[j] > 0)) {
double t = d[i] / (d[i] - d[j]);
out.push_back({poly[i].x + (poly[j].x - poly[i].x) * t, poly[i].y + (poly[j].y - poly[i].y) * t,
poly[i].z + (poly[j].z - poly[i].z) * t});
}
}
poly.swap(out);
}
Vec3 newellRaw(const Polygon& p) {
Vec3 n;
for (size_t i = 0; i < p.size(); ++i) {
const Vec3& a = p[i];
const Vec3& b = p[(i + 1) % p.size()];
n.x += (a.y - b.y) * (a.z + b.z);
n.y += (a.z - b.z) * (a.x + b.x);
n.z += (a.x - b.x) * (a.y + b.y);
}
return n;
}
} // namespace
Vec3 polyNormal(const Polygon& p) { return normalize(newellRaw(p)); }
BrushGeometry buildBrushGeometry(const Brush& b) {
BrushGeometry g;
size_t nf = b.faces.size();
g.polys.resize(nf);
if (nf < 4) return g;
int good = 0;
for (size_t i = 0; i < nf; ++i) {
const Plane& pi = b.faces[i].plane;
bool dup = false;
for (size_t j = 0; j < i && !dup; ++j) {
const Plane& pj = b.faces[j].plane;
dup = dot(pi.n, pj.n) > 0.99999f && std::fabs(pi.d - pj.d) < 0.01f;
}
if (dup) continue;
DPoly p = basePoly(pi);
for (size_t j = 0; j < nf && p.size() >= 3; ++j)
if (j != i) clipBack(p, b.faces[j].plane, 1e-3);
if (p.size() < 3) continue;
Polygon fp;
for (const DV& v : p) {
Vec3 q((float)v.x, (float)v.y, (float)v.z);
if (fp.empty() || length(q - fp.back()) > 1e-3f) fp.push_back(q);
}
while (fp.size() > 1 && length(fp.front() - fp.back()) <= 1e-3f) fp.pop_back();
if (fp.size() < 3 || length(newellRaw(fp)) * 0.5f < 1e-3f) continue;
for (const Vec3& q : fp) { g.bounds.add(q); g.points.push_back(q); }
g.polys[i] = std::move(fp);
++good;
}
Vec3 sz = g.bounds.size();
g.valid = good >= 4 && sz.x > 0.01f && sz.y > 0.01f && sz.z > 0.01f;
return g;
}
AABB brushBounds(const Brush& b) { return buildBrushGeometry(b).bounds; }
AABB meshBounds(const MeshObject& m) {
AABB bb;
for (const Vec3& v : m.verts) bb.add(v);
return bb;
}
// ============================================================ raycasts
bool rayBrush(const Brush& b, const Vec3& o, const Vec3& d, float& tHit, int& face) {
float tEnter = -1e30f, tExit = 1e30f;
int fe = -1;
for (size_t i = 0; i < b.faces.size(); ++i) {
const Plane& p = b.faces[i].plane;
float denom = dot(p.n, d), dist = p.dist(o);
if (std::fabs(denom) < 1e-8f) {
if (dist > 0) return false;
continue;
}
float t = -dist / denom;
if (denom < 0) { if (t > tEnter) { tEnter = t; fe = (int)i; } }
else if (t < tExit) tExit = t;
if (tEnter > tExit) return false;
}
if (fe < 0 || tEnter < 0 || tEnter > tExit) return false;
tHit = tEnter;
face = fe;
return true;
}
static bool rayTri(const Vec3& o, const Vec3& d, const Vec3& a, const Vec3& b, const Vec3& c, float& t) {
Vec3 e1 = b - a, e2 = c - a, p = cross(d, e2);
float det = dot(e1, p);
if (std::fabs(det) < 1e-9f) return false;
float inv = 1.0f / det;
Vec3 s = o - a;
float u = dot(s, p) * inv;
if (u < 0 || u > 1) return false;
Vec3 q = cross(s, e1);
float v = dot(d, q) * inv;
if (v < 0 || u + v > 1) return false;
float tt = dot(e2, q) * inv;
if (tt < 0) return false;
t = tt;
return true;
}
bool rayMesh(const MeshObject& m, const Vec3& o, const Vec3& d, float& tHit, int& poly) {
bool hit = false;
float best = 1e30f;
for (size_t pi = 0; pi < m.polys.size(); ++pi) {
const auto& idx = m.polys[pi].idx;
for (size_t k = 1; k + 1 < idx.size(); ++k) {
if (idx[0] >= m.verts.size() || idx[k] >= m.verts.size() || idx[k + 1] >= m.verts.size()) continue;
float t;
if (rayTri(o, d, m.verts[idx[0]], m.verts[idx[k]], m.verts[idx[k + 1]], t) && t < best) {
best = t; poly = (int)pi; hit = true;
}
}
}
if (hit) tHit = best;
return hit;
}
// ============================================================ texture mapping
void uvAxes(const Vec3& n, const UVInfo& uv, Vec3& u, Vec3& v) {
float ax = std::fabs(n.x), ay = std::fabs(n.y), az = std::fabs(n.z);
Vec3 ua, va;
if (ay >= ax && ay >= az) {
if (n.y >= 0) { ua = {1, 0, 0}; va = {0, 0, 1}; } else { ua = {1, 0, 0}; va = {0, 0, -1}; }
} else if (ax >= az) {
if (n.x >= 0) { ua = {0, 0, -1}; va = {0, -1, 0}; } else { ua = {0, 0, 1}; va = {0, -1, 0}; }
} else {
if (n.z >= 0) { ua = {1, 0, 0}; va = {0, -1, 0}; } else { ua = {-1, 0, 0}; va = {0, -1, 0}; }
}
u = normalize(ua - n * dot(ua, n));
v = normalize(va - n * dot(va, n));
if (uv.rotation != 0) {
float r = uv.rotation * kDeg2Rad, c = std::cos(r), s = std::sin(r);
Vec3 u2 = u * c + v * s, v2 = v * c - u * s;
u = u2; v = v2;
}
}
Vec2 computeUV(const Vec3& p, const Vec3& n, const UVInfo& uv, const Vec2& tile) {
Vec3 u, v;
uvAxes(n, uv, u, v);
float sx = std::fabs(uv.scale.x) < 1e-4f ? 1e-4f : uv.scale.x;
float sy = std::fabs(uv.scale.y) < 1e-4f ? 1e-4f : uv.scale.y;
float tx = tile.x < 1e-3f ? 64.0f : tile.x, ty = tile.y < 1e-3f ? 64.0f : tile.y;
return {(dot(p, u) + uv.offset.x) / (tx * sx), (dot(p, v) + uv.offset.y) / (ty * sy)};
}
// ============================================================ brush / mesh edits
void translateBrush(Brush& b, const Vec3& t, bool lockTexture) {
for (Face& f : b.faces) {
if (lockTexture) {
Vec3 u, v;
uvAxes(f.plane.n, f.uv, u, v);
f.uv.offset.x -= dot(t, u);
f.uv.offset.y -= dot(t, v);
}
f.plane.d += dot(f.plane.n, t);
}
}
void transformBrush(Brush& b, const Mat34& m) {
for (Face& f : b.faces) f.plane = m.transformPlane(f.plane);
}
void transformMesh(MeshObject& m, const Mat34& xf) {
for (Vec3& v : m.verts) v = xf.point(v);
if (xf.det3() < 0) flipMesh(m); // mirrored: keep CCW winding
}
std::vector<Brush> hollowBrush(const Brush& src, float t) {
std::vector<Brush> out;
BrushGeometry g = buildBrushGeometry(src);
if (!g.valid || t <= 0) return out;
size_t n = src.faces.size();
for (size_t k = 0; k < n; ++k) {
if (g.polys[k].size() < 3) continue;
Brush w;
w.contents = src.contents;
w.owner = src.owner;
w.flags = src.flags;
for (size_t j = 0; j < n; ++j)
if (g.polys[j].size() >= 3) w.faces.push_back(src.faces[j]);
Face inner = src.faces[k]; // room-facing wall surface
inner.plane = Plane{-src.faces[k].plane.n, -(src.faces[k].plane.d - t)};
w.faces.push_back(inner);
for (size_t j = 0; j < k; ++j) { // carve away what earlier walls already own
if (g.polys[j].size() < 3) continue;
Face cut = src.faces[j];
cut.plane.d -= t;
cut.flags |= FACE_NODRAW;
w.faces.push_back(cut);
}
if (buildBrushGeometry(w).valid) out.push_back(std::move(w));
}
return out;
}
bool clipBrush(const Brush& b, const Plane& p, Brush* front, Brush* back) {
int mat = b.faces.empty() ? 0 : b.faces[0].material;
bool okF = false, okB = false;
Brush f = b, k = b;
Face ff; ff.material = mat; ff.plane = Plane{-p.n, -p.d};
Face fk; fk.material = mat; fk.plane = p;
f.faces.push_back(ff);
k.faces.push_back(fk);
if (buildBrushGeometry(f).valid) { okF = true; if (front) *front = std::move(f); }
if (buildBrushGeometry(k).valid) { okB = true; if (back) *back = std::move(k); }
return okF || okB;
}
MeshObject brushToMesh(const Brush& b) {
MeshObject m;
m.owner = b.owner;
m.flags = b.flags;
BrushGeometry g = buildBrushGeometry(b);
std::map<std::array<long long, 3>, uint32_t> weld;
for (size_t i = 0; i < g.polys.size(); ++i) {
if (g.polys[i].size() < 3) continue;
MeshPoly p;
p.material = b.faces[i].material;
p.proj = b.faces[i].uv;
for (const Vec3& v : g.polys[i]) {
std::array<long long, 3> key{std::llround(v.x * 100.0), std::llround(v.y * 100.0), std::llround(v.z * 100.0)};
auto it = weld.find(key);
if (it == weld.end()) {
it = weld.emplace(key, (uint32_t)m.verts.size()).first;
m.verts.push_back(v);
}
p.idx.push_back(it->second);
}
m.polys.push_back(std::move(p));
}
return m;
}
void flipMesh(MeshObject& m) {
for (MeshPoly& p : m.polys) {
std::reverse(p.idx.begin(), p.idx.end());
std::reverse(p.uv.begin(), p.uv.end());
}
}
static float hash2(int x, int z, uint32_t seed) {
uint32_t h = (uint32_t)x * 374761393u + (uint32_t)z * 668265263u + seed * 2246822519u;
h = (h ^ (h >> 13)) * 1274126177u;
h ^= h >> 16;
return (float)(h & 0xffffffu) / (float)0xffffff;
}
static float valueNoise(float x, float z, uint32_t seed) {
int xi = (int)std::floor(x), zi = (int)std::floor(z);
float fx = x - (float)xi, fz = z - (float)zi;
fx = fx * fx * (3 - 2 * fx);
fz = fz * fz * (3 - 2 * fz);
float a = hash2(xi, zi, seed), b = hash2(xi + 1, zi, seed), c = hash2(xi, zi + 1, seed), d = hash2(xi + 1, zi + 1, seed);
float top = a + (b - a) * fx, bot = c + (d - c) * fx;
return top + (bot - top) * fz;
}
void noiseMesh(MeshObject& m, float amp, float freq, uint32_t seed) {
for (Vec3& v : m.verts) {
float n = valueNoise(v.x * freq, v.z * freq, seed) * 0.6f + valueNoise(v.x * freq * 2.1f, v.z * freq * 2.1f, seed + 7) * 0.28f +
valueNoise(v.x * freq * 4.3f, v.z * freq * 4.3f, seed + 13) * 0.12f;
v.y += (n * 2.0f - 1.0f) * amp;
}
}
bool extrudePoly(MeshObject& m, int pi, float dist) {
if (pi < 0 || (size_t)pi >= m.polys.size() || std::fabs(dist) < 1e-5f) return false;
MeshPoly poly = m.polys[(size_t)pi];
size_t n = poly.idx.size();
if (n < 3) return false;
Polygon pp;
for (uint32_t i : poly.idx) pp.push_back(m.verts[i]);
Vec3 N = polyNormal(pp);
std::vector<uint32_t> ring(n);
for (size_t k = 0; k < n; ++k) {
ring[k] = (uint32_t)m.verts.size();
m.verts.push_back(m.verts[poly.idx[k]] + N * dist);
}
for (size_t k = 0; k < n; ++k) {
size_t k1 = (k + 1) % n;
MeshPoly side;
side.material = poly.material;
side.proj = poly.proj;
side.smooth = 0;
if (dist > 0) side.idx = {poly.idx[k], poly.idx[k1], ring[k1], ring[k]};
else side.idx = {poly.idx[k], ring[k], ring[k1], poly.idx[k1]};
m.polys.push_back(side);
}
m.polys[(size_t)pi].idx = ring;
return true;
}
Status importOBJ(const std::string& path, Map& map, MeshObject& out) {
std::ifstream f(path);
if (!f) return Status::Fail("cannot open " + path);
out = MeshObject();
size_t slash = path.find_last_of("/\\");
std::string base = slash == std::string::npos ? path : path.substr(slash + 1);
size_t dotp = base.find_last_of('.');
out.name = dotp == std::string::npos ? base : base.substr(0, dotp);
std::vector<Vec2> tex;
int mat = map.materials.empty() ? map.addMaterial("dev/gray") : 0;
uint32_t smooth = 0;
std::string line;
while (std::getline(f, line)) {
std::istringstream ss(line);
std::string tag;
ss >> tag;
if (tag == "v") {
Vec3 v;
ss >> v.x >> v.y >> v.z;
out.verts.push_back(v);
} else if (tag == "vt") {
Vec2 t;
ss >> t.x >> t.y;
t.y = 1.0f - t.y;
tex.push_back(t);
} else if (tag == "usemtl") {
std::string n;
ss >> n;
if (!n.empty()) mat = map.addMaterial(n);
} else if (tag == "s") {
std::string v;
ss >> v;
smooth = (v == "off" || v == "0") ? 0u : (uint32_t)std::atoi(v.c_str());
} else if (tag == "f") {
MeshPoly p;
p.material = mat;
p.smooth = smooth;
bool allUV = true;
std::string tok;
while (ss >> tok) {
int vi = std::atoi(tok.c_str()), ti = 0;
size_t s1 = tok.find('/');
if (s1 != std::string::npos) {
std::string rest = tok.substr(s1 + 1);
if (!rest.empty() && rest[0] != '/') ti = std::atoi(rest.c_str());
}
int v = vi < 0 ? (int)out.verts.size() + vi : vi - 1;
if (v < 0 || (size_t)v >= out.verts.size()) { p.idx.clear(); break; }
p.idx.push_back((uint32_t)v);
int t = ti < 0 ? (int)tex.size() + ti : ti - 1;
if (ti != 0 && t >= 0 && (size_t)t < tex.size()) p.uv.push_back(tex[(size_t)t]);
else allUV = false;
}
if (!allUV) p.uv.clear();
if (p.idx.size() >= 3) out.polys.push_back(std::move(p));
}
}
if (out.polys.empty()) return Status::Fail("no faces in " + path);
return Status::Ok();
}
// ============================================================ brush primitives
Brush makeBox(const Vec3& mn, const Vec3& mx, int material) {
Brush b;
const Vec3 ns[6] = {{1, 0, 0}, {-1, 0, 0}, {0, 1, 0}, {0, -1, 0}, {0, 0, 1}, {0, 0, -1}};
const float ds[6] = {mx.x, -mn.x, mx.y, -mn.y, mx.z, -mn.z};
b.faces.resize(6);
for (int i = 0; i < 6; ++i) {
b.faces[(size_t)i].plane = Plane{ns[i], ds[i]};
b.faces[(size_t)i].material = material;
}
return b;
}
Brush makeWedge(const Vec3& mn, const Vec3& mx, int material) {
Brush b = makeBox(mn, mx, material);
Vec3 s(mx.x - mn.x, mx.y - mn.y, 0);
Vec3 n = normalize(Vec3(-s.y, s.x, 0));
b.faces[2].plane = Plane{n, dot(n, Vec3(mn.x, mn.y, 0))};
b.faces.erase(b.faces.begin() + 1); // -X face collapses to a line
return b;
}
static void addOutwardFace(Brush& b, Vec3 n, const Vec3& onPlane, const Vec3& inside, int material) {
n = normalize(n);
float d = dot(n, onPlane);
if (dot(n, inside) - d > 0) { n = -n; d = -d; }
Face f;
f.plane = Plane{n, d};
f.material = material;
b.faces.push_back(f);
}
Brush makeCylinder(const Vec3& mn, const Vec3& mx, int segs, int material) {
segs = std::max(3, std::min(segs, 64));
Vec3 c = (mn + mx) * 0.5f;
float rx = (mx.x - mn.x) * 0.5f, rz = (mx.z - mn.z) * 0.5f;
Brush b;
for (int i = 0; i < segs; ++i) {
float a0 = 2 * kPi * (float)i / (float)segs, a1 = 2 * kPi * (float)(i + 1) / (float)segs;
Vec3 p0(c.x + rx * std::cos(a0), mn.y, c.z + rz * std::sin(a0));
Vec3 p1(c.x + rx * std::cos(a1), mn.y, c.z + rz * std::sin(a1));
addOutwardFace(b, cross(Vec3(0, 1, 0), p1 - p0), p0, c, material);
}
addOutwardFace(b, {0, 1, 0}, Vec3(c.x, mx.y, c.z), c, material);
addOutwardFace(b, {0, -1, 0}, Vec3(c.x, mn.y, c.z), c, material);
return b;
}
Brush makeCone(const Vec3& mn, const Vec3& mx, int segs, int material) {
segs = std::max(3, std::min(segs, 64));
Vec3 c = (mn + mx) * 0.5f;
float rx = (mx.x - mn.x) * 0.5f, rz = (mx.z - mn.z) * 0.5f;
Vec3 apex(c.x, mx.y, c.z), inside(c.x, mn.y + 0.25f * (mx.y - mn.y), c.z);
Brush b;
for (int i = 0; i < segs; ++i) {
float a0 = 2 * kPi * (float)i / (float)segs, a1 = 2 * kPi * (float)(i + 1) / (float)segs;
Vec3 p0(c.x + rx * std::cos(a0), mn.y, c.z + rz * std::sin(a0));
Vec3 p1(c.x + rx * std::cos(a1), mn.y, c.z + rz * std::sin(a1));
addOutwardFace(b, cross(p1 - p0, apex - p0), p0, inside, material);
}
addOutwardFace(b, {0, -1, 0}, Vec3(c.x, mn.y, c.z), inside, material);
return b;
}
std::vector<Brush> makeStairs(const Vec3& mn, const Vec3& mx, int steps, int material) {
steps = std::max(1, std::min(steps, 128));
std::vector<Brush> out;
float dx = (mx.x - mn.x) / (float)steps, dy = (mx.y - mn.y) / (float)steps;
for (int i = 0; i < steps; ++i) {
float x0 = mn.x + dx * (float)i;
out.push_back(makeBox({x0, mn.y, mn.z}, {x0 + dx, mn.y + dy * (float)(i + 1), mx.z}, material));
}
return out;
}
// ============================================================ mesh primitives
namespace {
void addPoly(MeshObject& m, std::vector<uint32_t> idx, std::vector<Vec2> uv, int mat, uint32_t smooth) {
// collapse consecutive duplicates (degenerate quads at poles become triangles)
std::vector<uint32_t> i2;
std::vector<Vec2> u2;
bool hasUV = uv.size() == idx.size();
for (size_t k = 0; k < idx.size(); ++k) {
size_t prev = (k + idx.size() - 1) % idx.size();
if (idx[k] == idx[prev]) continue;
i2.push_back(idx[k]);
if (hasUV) u2.push_back(uv[k]);
}
if (i2.size() < 3) return;
MeshPoly p;
p.idx = std::move(i2);
p.uv = std::move(u2);
p.material = mat;
p.smooth = smooth;
m.polys.push_back(std::move(p));
}
} // namespace
MeshObject meshBox(const Vec3& mn, const Vec3& mx, int mat) {
MeshObject m;
m.verts = {{mn.x, mn.y, mn.z}, {mx.x, mn.y, mn.z}, {mx.x, mx.y, mn.z}, {mn.x, mx.y, mn.z},
{mn.x, mn.y, mx.z}, {mx.x, mn.y, mx.z}, {mx.x, mx.y, mx.z}, {mn.x, mx.y, mx.z}};
const uint32_t q[6][4] = {{1, 2, 6, 5}, {0, 4, 7, 3}, {3, 7, 6, 2}, {0, 1, 5, 4}, {4, 5, 6, 7}, {0, 3, 2, 1}};
for (auto& f : q) addPoly(m, {f[0], f[1], f[2], f[3]}, {}, mat, 0);
return m;
}
MeshObject meshCylinder(const Vec3& mn, const Vec3& mx, int segs, int mat) {
segs = std::max(3, std::min(segs, 128));
MeshObject m;
Vec3 c = (mn + mx) * 0.5f;
float rx = (mx.x - mn.x) * 0.5f, rz = (mx.z - mn.z) * 0.5f;
float circ = 2 * kPi * (rx + rz) * 0.5f, h = mx.y - mn.y;
for (int s = 0; s < segs; ++s) { // bottom ring [0,segs) then top ring [segs,2*segs)
float a = 2 * kPi * (float)s / (float)segs;
m.verts.push_back({c.x + rx * std::cos(a), mn.y, c.z + rz * std::sin(a)});
}
for (int s = 0; s < segs; ++s) {
float a = 2 * kPi * (float)s / (float)segs;
m.verts.push_back({c.x + rx * std::cos(a), mx.y, c.z + rz * std::sin(a)});
}
for (int s = 0; s < segs; ++s) {
uint32_t s0 = (uint32_t)s, s1 = (uint32_t)((s + 1) % segs);
float u0 = circ * (float)s / (float)segs / 64.0f, u1 = circ * (float)(s + 1) / (float)segs / 64.0f, v = h / 64.0f;
addPoly(m, {s1, s0, (uint32_t)segs + s0, (uint32_t)segs + s1}, {{u1, v}, {u0, v}, {u0, 0}, {u1, 0}}, mat, 1);
}
std::vector<uint32_t> top, bot;
for (int s = 0; s < segs; ++s) {
bot.push_back((uint32_t)s);
top.push_back((uint32_t)(2 * segs - 1 - s));
}
addPoly(m, top, {}, mat, 0);
addPoly(m, bot, {}, mat, 0);
return m;
}
MeshObject meshCone(const Vec3& mn, const Vec3& mx, int segs, int mat) {
segs = std::max(3, std::min(segs, 128));
MeshObject m;
Vec3 c = (mn + mx) * 0.5f;
float rx = (mx.x - mn.x) * 0.5f, rz = (mx.z - mn.z) * 0.5f;
float circ = 2 * kPi * (rx + rz) * 0.5f, h = mx.y - mn.y;
for (int s = 0; s < segs; ++s) {
float a = 2 * kPi * (float)s / (float)segs;
m.verts.push_back({c.x + rx * std::cos(a), mn.y, c.z + rz * std::sin(a)});
}
uint32_t apex = (uint32_t)m.verts.size();
m.verts.push_back({c.x, mx.y, c.z});
for (int s = 0; s < segs; ++s) {
uint32_t s0 = (uint32_t)s, s1 = (uint32_t)((s + 1) % segs);
float u0 = circ * (float)s / (float)segs / 64.0f, u1 = circ * (float)(s + 1) / (float)segs / 64.0f, v = h / 64.0f;
addPoly(m, {s1, s0, apex}, {{u1, v}, {u0, v}, {(u0 + u1) * 0.5f, 0}}, mat, 1);
}
std::vector<uint32_t> bot;
for (int s = 0; s < segs; ++s) bot.push_back((uint32_t)s);
addPoly(m, bot, {}, mat, 0);
return m;
}
MeshObject meshSphere(const Vec3& mn, const Vec3& mx, int rings, int segs, int mat) {
rings = std::max(2, std::min(rings, 128));
segs = std::max(3, std::min(segs, 128));
MeshObject m;
Vec3 c = (mn + mx) * 0.5f;
float rx = (mx.x - mn.x) * 0.5f, ry = (mx.y - mn.y) * 0.5f, rz = (mx.z - mn.z) * 0.5f;
float circ = 2 * kPi * (rx + rz) * 0.5f, arc = kPi * ry;
std::vector<std::vector<uint32_t>> ring((size_t)rings + 1);
for (int r = 0; r <= rings; ++r) {
float th = kPi * (float)r / (float)rings;
if (r == 0 || r == rings) {
uint32_t id = (uint32_t)m.verts.size();
m.verts.push_back({c.x, c.y + std::cos(th) * ry, c.z});
ring[(size_t)r].assign((size_t)segs + 1, id);
continue;
}
for (int s = 0; s < segs; ++s) {
float ph = 2 * kPi * (float)s / (float)segs;
ring[(size_t)r].push_back((uint32_t)m.verts.size());
m.verts.push_back({c.x + std::sin(th) * std::cos(ph) * rx, c.y + std::cos(th) * ry, c.z + std::sin(th) * std::sin(ph) * rz});
}
ring[(size_t)r].push_back(ring[(size_t)r][0]);
}
for (int r = 0; r < rings; ++r) {
for (int s = 0; s < segs; ++s) {
size_t R0 = (size_t)r, R1 = (size_t)r + 1, S0 = (size_t)s, S1 = (size_t)s + 1;
float u0 = circ * (float)s / (float)segs / 64.0f, u1 = circ * (float)(s + 1) / (float)segs / 64.0f;
float v0 = arc * (float)r / (float)rings / 64.0f, v1 = arc * (float)(r + 1) / (float)rings / 64.0f;
addPoly(m, {ring[R1][S1], ring[R1][S0], ring[R0][S0], ring[R0][S1]}, {{u1, v1}, {u0, v1}, {u0, v0}, {u1, v0}}, mat, 1);
}
}
return m;
}
MeshObject meshTorus(const Vec3& mn, const Vec3& mx, int segs, int sides, int mat) {
segs = std::max(3, std::min(segs, 128));
sides = std::max(3, std::min(sides, 64));
MeshObject m;
Vec3 c = (mn + mx) * 0.5f;
float outer = std::min(mx.x - mn.x, mx.z - mn.z) * 0.5f, r = (mx.y - mn.y) * 0.5f;
r = std::min(r, outer * 0.95f);
float R = outer - r;
for (int i = 0; i < segs; ++i) {
float ph = 2 * kPi * (float)i / (float)segs;
for (int j = 0; j < sides; ++j) {
float th = 2 * kPi * (float)j / (float)sides;
float rr = R + r * std::cos(th);
m.verts.push_back({c.x + rr * std::cos(ph), c.y + r * std::sin(th), c.z + rr * std::sin(ph)});
}
}
auto V = [&](int i, int j) { return (uint32_t)(((i % segs) * sides) + (j % sides)); };
float uc = 2 * kPi * R / 64.0f, vc = 2 * kPi * r / 64.0f;
for (int i = 0; i < segs; ++i) {
for (int j = 0; j < sides; ++j) {
float u0 = uc * (float)i / (float)segs, u1 = uc * (float)(i + 1) / (float)segs;
float v0 = vc * (float)j / (float)sides, v1 = vc * (float)(j + 1) / (float)sides;
addPoly(m, {V(i, j), V(i, j + 1), V(i + 1, j + 1), V(i + 1, j)}, {{u0, v0}, {u0, v1}, {u1, v1}, {u1, v0}}, mat, 1);
}
}
return m;
}
MeshObject meshGrid(const Vec3& mn, const Vec3& mx, int sx, int sz, int mat) {
sx = std::max(1, std::min(sx, 256));
sz = std::max(1, std::min(sz, 256));
MeshObject m;
float y = (mn.y + mx.y) * 0.5f;
for (int j = 0; j <= sz; ++j)
for (int i = 0; i <= sx; ++i)
m.verts.push_back({mn.x + (mx.x - mn.x) * (float)i / (float)sx, y, mn.z + (mx.z - mn.z) * (float)j / (float)sz});
auto V = [&](int i, int j) { return (uint32_t)(j * (sx + 1) + i); };
for (int j = 0; j < sz; ++j)
for (int i = 0; i < sx; ++i) {
uint32_t a = V(i, j), b = V(i, j + 1), c = V(i + 1, j + 1), d = V(i + 1, j);
std::vector<Vec2> uv;
for (uint32_t k : {a, b, c, d}) uv.push_back({m.verts[k].x / 64.0f, m.verts[k].z / 64.0f});
addPoly(m, {a, b, c, d}, uv, mat, 1);
}
return m;
}
} // namespace hmap
+410
View File
@@ -0,0 +1,410 @@
#include "hmap/hmap.h"
#include <cstring>
#include <fstream>
namespace hmap {
namespace {
class Writer {
public:
std::vector<uint8_t> buf;
void u8(uint8_t v) { buf.push_back(v); }
void u32(uint32_t v) { for (int i = 0; i < 4; ++i) buf.push_back((uint8_t)((v >> (8 * i)) & 0xff)); }
void i32(int32_t v) { u32((uint32_t)v); }
void f32(float f) { uint32_t u; std::memcpy(&u, &f, 4); u32(u); }
void str(const std::string& s) { u32((uint32_t)s.size()); buf.insert(buf.end(), s.begin(), s.end()); }
void vec2(const Vec2& v) { f32(v.x); f32(v.y); }
void vec3(const Vec3& v) { f32(v.x); f32(v.y); f32(v.z); }
void plane(const Plane& p) { vec3(p.n); f32(p.d); }
void aabb(const AABB& b) { vec3(b.mn); vec3(b.mx); }
void uv(const UVInfo& u) { vec2(u.offset); vec2(u.scale); f32(u.rotation); }
void raw(const char* tag4, const Writer& body) {
buf.insert(buf.end(), tag4, tag4 + 4);
u32((uint32_t)body.buf.size());
buf.insert(buf.end(), body.buf.begin(), body.buf.end());
}
};
class Reader {
public:
const uint8_t* p;
size_t n;
size_t pos = 0;
bool ok = true;
Reader(const uint8_t* data, size_t size) : p(data), n(size) {}
bool need(size_t k) {
if (!ok || pos + k > n) { ok = false; return false; }
return true;
}
uint8_t u8() { return need(1) ? p[pos++] : 0; }
uint32_t u32() {
if (!need(4)) return 0;
uint32_t v = (uint32_t)p[pos] | ((uint32_t)p[pos + 1] << 8) | ((uint32_t)p[pos + 2] << 16) | ((uint32_t)p[pos + 3] << 24);
pos += 4;
return v;
}
int32_t i32() { return (int32_t)u32(); }
float f32() { uint32_t u = u32(); float f; std::memcpy(&f, &u, 4); return f; }
std::string str() {
uint32_t len = u32();
if (!need(len)) return {};
std::string s((const char*)p + pos, len);
pos += len;
return s;
}
Vec2 vec2() { Vec2 v; v.x = f32(); v.y = f32(); return v; }
Vec3 vec3() { Vec3 v; v.x = f32(); v.y = f32(); v.z = f32(); return v; }
Plane plane() { Plane pl; pl.n = vec3(); pl.d = f32(); return pl; }
AABB aabb() { AABB b; b.mn = vec3(); b.mx = vec3(); return b; }
UVInfo uv() { UVInfo u; u.offset = vec2(); u.scale = vec2(); u.rotation = f32(); return u; }
// Reads an item count and rejects counts that cannot possibly fit in the remaining bytes.
uint32_t count(size_t minBytesPerItem) {
uint32_t c = u32();
if (ok && (size_t)c * minBytesPerItem > n - pos) ok = false;
return ok ? c : 0;
}
};
// ------------------------------------------------------------ writers
void writeMats(Writer& w, const std::vector<Material>& mats) {
w.u32((uint32_t)mats.size());
for (const Material& m : mats) {
w.str(m.name); w.str(m.texture); w.vec2(m.tileSize); w.vec3(m.color); w.u32(m.flags);
}
}
void writeBrushes(Writer& w, const std::vector<Brush>& bs) {
w.u32((uint32_t)bs.size());
for (const Brush& b : bs) {
w.u32(b.id); w.u32(b.owner); w.u32(b.contents); w.u32(b.flags);
w.u32((uint32_t)b.faces.size());
for (const Face& f : b.faces) { w.plane(f.plane); w.i32(f.material); w.uv(f.uv); w.u32(f.flags); }
}
}
void writeMeshes(Writer& w, const std::vector<MeshObject>& ms) {
w.u32((uint32_t)ms.size());
for (const MeshObject& m : ms) {
w.u32(m.id); w.u32(m.owner); w.u32(m.flags); w.str(m.name);
w.u32((uint32_t)m.verts.size());
for (const Vec3& v : m.verts) w.vec3(v);
w.u32((uint32_t)m.polys.size());
for (const MeshPoly& p : m.polys) {
w.i32(p.material); w.u32(p.smooth); w.uv(p.proj);
w.u32((uint32_t)p.idx.size());
for (uint32_t i : p.idx) w.u32(i);
w.u32((uint32_t)p.uv.size());
for (const Vec2& t : p.uv) w.vec2(t);
}
}
}
void writeEntities(Writer& w, const std::vector<Entity>& es) {
w.u32((uint32_t)es.size());
for (const Entity& e : es) {
w.u32(e.id); w.u32(e.flags); w.str(e.classname);
w.vec3(e.origin); w.vec3(e.angles); w.vec3(e.scale);
w.u32((uint32_t)e.props.size());
for (const KeyValue& kv : e.props) { w.str(kv.key); w.str(kv.value); }
}
}
void writeCollisionBrush(Writer& w, const CollisionBrush& b) {
w.u32(b.contents); w.u32(b.sourceId); w.aabb(b.bounds);
w.u32((uint32_t)b.planes.size());
for (const Plane& p : b.planes) w.plane(p);
}
void writeCompiledChunk(Writer& w, const CompiledMap& cm) {
w.vec3(cm.ambient); w.f32(cm.unitsPerMeter); w.u8(cm.baked ? 1 : 0); w.aabb(cm.bounds);
w.u32((uint32_t)cm.batches.size());
for (const Batch& b : cm.batches) {
w.i32(b.material); w.u8((uint8_t)b.source); w.i32(b.model);
w.u32((uint32_t)b.vertices.size());
for (const Vertex& v : b.vertices) { w.vec3(v.pos); w.vec3(v.normal); w.vec2(v.uv); w.u32(v.color); }
w.u32((uint32_t)b.indices.size());
for (uint32_t i : b.indices) w.u32(i);
}
w.u32((uint32_t)cm.models.size());
for (const CompiledModel& m : cm.models) {
w.u32(m.entityId); w.aabb(m.bounds);
w.u32((uint32_t)m.brushes.size());
for (const CollisionBrush& b : m.brushes) writeCollisionBrush(w, b);
}
const Bsp& t = cm.bsp;
w.i32(t.root); w.u32(t.depth); w.aabb(t.bounds);
w.u32((uint32_t)t.planes.size());
for (const Plane& p : t.planes) w.plane(p);
w.u32((uint32_t)t.nodes.size());
for (const BspNode& n : t.nodes) { w.i32(n.plane); w.i32(n.child[0]); w.i32(n.child[1]); }
w.u32((uint32_t)t.leaves.size());
for (const BspLeaf& l : t.leaves) { w.u32(l.firstBrush); w.u32(l.brushCount); }
w.u32((uint32_t)t.leafBrushes.size());
for (uint32_t i : t.leafBrushes) w.u32(i);
w.u32((uint32_t)t.brushes.size());
for (const CollisionBrush& b : t.brushes) writeCollisionBrush(w, b);
w.u32((uint32_t)cm.collMeshes.size());
for (const CollisionMesh& c : cm.collMeshes) {
w.u32(c.objectId); w.aabb(c.bounds);
w.u32((uint32_t)c.tris.size());
for (const Vec3& v : c.tris) w.vec3(v);
}
w.u32((uint32_t)cm.lights.size());
for (const Light& l : cm.lights) {
w.u8((uint8_t)l.type); w.vec3(l.pos); w.vec3(l.dir); w.vec3(l.color);
w.f32(l.intensity); w.f32(l.radius); w.f32(l.innerDeg); w.f32(l.outerDeg); w.u32(l.entityId);
}
}
// ------------------------------------------------------------ readers
void readMats(Reader& r, std::vector<Material>& out) {
uint32_t c = r.count(36);
out.resize(c);
for (Material& m : out) {
m.name = r.str(); m.texture = r.str(); m.tileSize = r.vec2(); m.color = r.vec3(); m.flags = r.u32();
}
}
void readBrushes(Reader& r, std::vector<Brush>& out) {
uint32_t c = r.count(20);
out.resize(c);
for (Brush& b : out) {
b.id = r.u32(); b.owner = r.u32(); b.contents = r.u32(); b.flags = r.u32();
uint32_t nf = r.count(44);
b.faces.resize(nf);
for (Face& f : b.faces) { f.plane = r.plane(); f.material = r.i32(); f.uv = r.uv(); f.flags = r.u32(); }
}
}
void readMeshes(Reader& r, std::vector<MeshObject>& out) {
uint32_t c = r.count(24);
out.resize(c);
for (MeshObject& m : out) {
m.id = r.u32(); m.owner = r.u32(); m.flags = r.u32(); m.name = r.str();
uint32_t nv = r.count(12);
m.verts.resize(nv);
for (Vec3& v : m.verts) v = r.vec3();
uint32_t np = r.count(32);
m.polys.resize(np);
for (MeshPoly& p : m.polys) {
p.material = r.i32(); p.smooth = r.u32(); p.proj = r.uv();
uint32_t ni = r.count(4);
p.idx.resize(ni);
for (uint32_t& i : p.idx) i = r.u32();
uint32_t nu = r.count(8);
p.uv.resize(nu);
for (Vec2& t : p.uv) t = r.vec2();
}
}
}
void readEntities(Reader& r, std::vector<Entity>& out) {
uint32_t c = r.count(52);
out.resize(c);
for (Entity& e : out) {
e.id = r.u32(); e.flags = r.u32(); e.classname = r.str();
e.origin = r.vec3(); e.angles = r.vec3(); e.scale = r.vec3();
uint32_t nk = r.count(8);
e.props.resize(nk);
for (KeyValue& kv : e.props) { kv.key = r.str(); kv.value = r.str(); }
}
}
void readCollisionBrush(Reader& r, CollisionBrush& b) {
b.contents = r.u32(); b.sourceId = r.u32(); b.bounds = r.aabb();
uint32_t n = r.count(16);
b.planes.resize(n);
for (Plane& p : b.planes) p = r.plane();
}
void readCompiledChunk(Reader& r, CompiledMap& cm) {
cm.ambient = r.vec3(); cm.unitsPerMeter = r.f32(); cm.baked = r.u8() != 0; cm.bounds = r.aabb();
uint32_t nb = r.count(17);
cm.batches.resize(nb);
for (Batch& b : cm.batches) {
b.material = r.i32(); b.source = (GeomSource)r.u8(); b.model = r.i32();
uint32_t nv = r.count(36);
b.vertices.resize(nv);
for (Vertex& v : b.vertices) { v.pos = r.vec3(); v.normal = r.vec3(); v.uv = r.vec2(); v.color = r.u32(); b.bounds.add(v.pos); }
uint32_t ni = r.count(4);
b.indices.resize(ni);
for (uint32_t& i : b.indices) { i = r.u32(); if (i >= nv) r.ok = false; }
}
uint32_t nm = r.count(40);
cm.models.resize(nm);
for (CompiledModel& m : cm.models) {
m.entityId = r.u32(); m.bounds = r.aabb();
uint32_t n = r.count(40);
m.brushes.resize(n);
for (CollisionBrush& b : m.brushes) readCollisionBrush(r, b);
}
Bsp& t = cm.bsp;
t.root = r.i32(); t.depth = r.u32(); t.bounds = r.aabb();
uint32_t np = r.count(16);
t.planes.resize(np);
for (Plane& p : t.planes) p = r.plane();
uint32_t nn = r.count(12);
t.nodes.resize(nn);
for (BspNode& n : t.nodes) { n.plane = r.i32(); n.child[0] = r.i32(); n.child[1] = r.i32(); }
uint32_t nl = r.count(8);
t.leaves.resize(nl);
for (BspLeaf& l : t.leaves) { l.firstBrush = r.u32(); l.brushCount = r.u32(); }
uint32_t nlb = r.count(4);
t.leafBrushes.resize(nlb);
for (uint32_t& i : t.leafBrushes) i = r.u32();
uint32_t nbr = r.count(40);
t.brushes.resize(nbr);
for (CollisionBrush& b : t.brushes) readCollisionBrush(r, b);
uint32_t ncm = r.count(32);
cm.collMeshes.resize(ncm);
for (CollisionMesh& c : cm.collMeshes) {
c.objectId = r.u32(); c.bounds = r.aabb();
uint32_t n = r.count(12);
c.tris.resize(n);
for (Vec3& v : c.tris) v = r.vec3();
}
uint32_t nlt = r.count(53);
cm.lights.resize(nlt);
for (Light& l : cm.lights) {
l.type = (LightType)r.u8(); l.pos = r.vec3(); l.dir = r.vec3(); l.color = r.vec3();
l.intensity = r.f32(); l.radius = r.f32(); l.innerDeg = r.f32(); l.outerDeg = r.f32(); l.entityId = r.u32();
}
// validate the tree so a corrupt file cannot make queries read out of bounds
if (!r.ok) return;
auto childOk = [&](int32_t c) { return c >= 0 ? (size_t)c < t.nodes.size() : (size_t)(~c) < t.leaves.size(); };
bool good = childOk(t.root) && !t.leaves.empty();
for (const BspNode& n : t.nodes)
good = good && n.plane >= 0 && (size_t)n.plane < t.planes.size() && childOk(n.child[0]) && childOk(n.child[1]);
for (const BspLeaf& l : t.leaves) good = good && (size_t)l.firstBrush + l.brushCount <= t.leafBrushes.size();
for (uint32_t i : t.leafBrushes) good = good && i < t.brushes.size();
if (!good) r.ok = false;
for (const Batch& b : cm.batches) cm.stats.triangles += (uint32_t)(b.indices.size() / 3), cm.stats.vertices += (uint32_t)b.vertices.size();
cm.stats.bspNodes = nn; cm.stats.bspLeaves = nl; cm.stats.bspDepth = t.depth; cm.stats.lights = nlt;
}
constexpr char kMagic[4] = {'H', 'M', 'A', 'P'};
Status writeFile(const std::string& path, const std::vector<uint8_t>& data) {
std::ofstream f(path, std::ios::binary | std::ios::trunc);
if (!f) return Status::Fail("cannot write " + path);
f.write((const char*)data.data(), (std::streamsize)data.size());
return f ? Status::Ok() : Status::Fail("write failed: " + path);
}
bool readFile(const std::string& path, std::vector<uint8_t>& out) {
std::ifstream f(path, std::ios::binary | std::ios::ate);
if (!f) return false;
std::streamsize sz = f.tellg();
if (sz < 0) return false;
f.seekg(0);
out.resize((size_t)sz);
if (sz > 0) f.read((char*)out.data(), sz);
return (bool)f;
}
Status parse(const uint8_t* data, size_t size, Map& map, CompiledMap* compiled, bool* hasCompiled) {
Reader hdr(data, size);
char magic[4];
for (char& c : magic) c = (char)hdr.u8();
uint32_t version = hdr.u32();
hdr.u32(); // flags (informational)
if (!hdr.ok || std::memcmp(magic, kMagic, 4) != 0) return Status::Fail("not an .hmap file");
if (version > kFormatVersion) return Status::Fail("file was written by a newer version of Hammer++");
size_t pos = hdr.pos;
*hasCompiled = false;
CompiledMap cm;
while (pos + 8 <= size) {
std::string tag((const char*)data + pos, 4);
Reader sz(data + pos + 4, 4);
uint32_t len = sz.u32();
pos += 8;
if ((size_t)len > size - pos) return Status::Fail("truncated chunk '" + tag + "'");
Reader r(data + pos, len);
if (tag == "META") {
map.name = r.str(); map.settings.ambient = r.vec3(); map.settings.gridSize = r.f32();
map.settings.unitsPerMeter = r.f32(); map.nextId = r.u32();
} else if (tag == "MATS") readMats(r, map.materials);
else if (tag == "BRSH") readBrushes(r, map.brushes);
else if (tag == "MESH") readMeshes(r, map.meshes);
else if (tag == "ENTS") readEntities(r, map.entities);
else if (tag == "CMPL") { readCompiledChunk(r, cm); *hasCompiled = r.ok; }
if (!r.ok) return Status::Fail("corrupt chunk '" + tag + "'");
pos += len;
}
// keep id allocation safe even when META is missing
for (const Brush& b : map.brushes) map.nextId = std::max(map.nextId, b.id + 1);
for (const MeshObject& m : map.meshes) map.nextId = std::max(map.nextId, m.id + 1);
for (const Entity& e : map.entities) map.nextId = std::max(map.nextId, e.id + 1);
if (*hasCompiled && compiled) {
cm.materials = map.materials;
cm.entities = map.entities;
*compiled = std::move(cm);
}
return Status::Ok();
}
} // namespace
std::vector<uint8_t> saveMapToMemory(const Map& map, const CompiledMap* compiled) {
Writer out;
out.buf.insert(out.buf.end(), kMagic, kMagic + 4);
out.u32(kFormatVersion);
out.u32(1u | (compiled ? 2u : 0u));
Writer meta;
meta.str(map.name); meta.vec3(map.settings.ambient); meta.f32(map.settings.gridSize);
meta.f32(map.settings.unitsPerMeter); meta.u32(map.nextId);
out.raw("META", meta);
Writer w;
writeMats(w, map.materials); out.raw("MATS", w);
Writer b; writeBrushes(b, map.brushes); out.raw("BRSH", b);
Writer m; writeMeshes(m, map.meshes); out.raw("MESH", m);
Writer e; writeEntities(e, map.entities); out.raw("ENTS", e);
if (compiled) { Writer c; writeCompiledChunk(c, *compiled); out.raw("CMPL", c); }
return std::move(out.buf);
}
Status loadMapFromMemory(const uint8_t* data, size_t size, Map& out, CompiledMap* compiled) {
Map tmp;
bool has = false;
Status s = parse(data, size, tmp, compiled, &has);
if (!s) return s;
out = std::move(tmp);
return Status::Ok();
}
Status saveMap(const std::string& path, const Map& map, const CompiledMap* compiled) {
return writeFile(path, saveMapToMemory(map, compiled));
}
Status loadMap(const std::string& path, Map& out, CompiledMap* compiled) {
std::vector<uint8_t> data;
if (!readFile(path, data)) return Status::Fail("cannot read " + path);
return loadMapFromMemory(data.data(), data.size(), out, compiled);
}
Status saveCompiled(const std::string& path, const CompiledMap& cm) {
Writer out;
out.buf.insert(out.buf.end(), kMagic, kMagic + 4);
out.u32(kFormatVersion);
out.u32(2u);
Writer w; writeMats(w, cm.materials); out.raw("MATS", w);
Writer e; writeEntities(e, cm.entities); out.raw("ENTS", e);
Writer c; writeCompiledChunk(c, cm); out.raw("CMPL", c);
return writeFile(path, out.buf);
}
Status loadCompiled(const std::string& path, CompiledMap& out) {
std::vector<uint8_t> data;
if (!readFile(path, data)) return Status::Fail("cannot read " + path);
Map map;
CompiledMap cm;
bool has = false;
Status s = parse(data.data(), data.size(), map, &cm, &has);
if (!s) return s;
out = has ? std::move(cm) : compile(map);
return Status::Ok();
}
} // namespace hmap
+106
View File
@@ -0,0 +1,106 @@
#include "hmap/hmap.h"
#include <cstdio>
#include <cstdlib>
namespace hmap {
const std::string* Entity::find(const std::string& key) const {
for (const auto& kv : props)
if (kv.key == key) return &kv.value;
return nullptr;
}
std::string Entity::get(const std::string& key, const std::string& def) const {
const std::string* v = find(key);
return v ? *v : def;
}
float Entity::getFloat(const std::string& key, float def) const {
const std::string* v = find(key);
if (!v || v->empty()) return def;
char* end = nullptr;
float f = std::strtof(v->c_str(), &end);
return end == v->c_str() ? def : f;
}
int Entity::getInt(const std::string& key, int def) const {
const std::string* v = find(key);
if (!v || v->empty()) return def;
char* end = nullptr;
long n = std::strtol(v->c_str(), &end, 10);
return end == v->c_str() ? def : (int)n;
}
bool Entity::getBool(const std::string& key, bool def) const {
const std::string* v = find(key);
if (!v || v->empty()) return def;
return *v != "0" && *v != "false" && *v != "no";
}
Vec3 Entity::getVec3(const std::string& key, const Vec3& def) const {
const std::string* v = find(key);
if (!v) return def;
float x, y, z;
if (std::sscanf(v->c_str(), "%f %f %f", &x, &y, &z) == 3) return {x, y, z};
return def;
}
Vec3 Entity::getColor(const std::string& key, const Vec3& def) const {
const std::string* v = find(key);
if (!v) return def;
float r, g, b;
if (std::sscanf(v->c_str(), "%f %f %f", &r, &g, &b) == 3) return {r / 255.0f, g / 255.0f, b / 255.0f};
return def;
}
void Entity::set(const std::string& key, const std::string& value) {
for (auto& kv : props)
if (kv.key == key) { kv.value = value; return; }
props.push_back({key, value});
}
void Entity::erase(const std::string& key) {
for (size_t i = 0; i < props.size(); ++i)
if (props[i].key == key) { props.erase(props.begin() + (long)i); return; }
}
Mat34 Entity::transform() const {
return Mat34::translate(origin) * Mat34::fromEuler(angles) * Mat34::scale(scale);
}
Vec3 Entity::forward() const { return Mat34::fromEuler(angles).vector({0, 0, -1}); }
int Map::findMaterial(const std::string& n) const {
for (size_t i = 0; i < materials.size(); ++i)
if (materials[i].name == n) return (int)i;
return -1;
}
int Map::addMaterial(const std::string& n, const std::string& texture) {
int i = findMaterial(n);
if (i >= 0) {
if (!texture.empty() && materials[(size_t)i].texture.empty()) materials[(size_t)i].texture = texture;
return i;
}
Material m;
m.name = n;
m.texture = texture;
materials.push_back(m);
return (int)materials.size() - 1;
}
template <class C>
static auto* findById(C& c, uint32_t id) {
for (auto& o : c)
if (o.id == id) return &o;
return (decltype(&c[0]))nullptr;
}
Brush* Map::findBrush(uint32_t id) { return findById(brushes, id); }
MeshObject* Map::findMesh(uint32_t id) { return findById(meshes, id); }
Entity* Map::findEntity(uint32_t id) { return findById(entities, id); }
const Brush* Map::findBrush(uint32_t id) const { return findById(brushes, id); }
const MeshObject* Map::findMesh(uint32_t id) const { return findById(meshes, id); }
const Entity* Map::findEntity(uint32_t id) const { return findById(entities, id); }
} // namespace hmap
+95
View File
@@ -0,0 +1,95 @@
#include "hmap/hmap.h"
namespace hmap {
void addDefaultMaterials(Map& map) {
struct D { const char* name; float r, g, b; };
const D defs[] = {
{"dev/grid", 0.86f, 0.86f, 0.88f}, {"dev/wall", 0.78f, 0.75f, 0.68f}, {"dev/orange", 1.0f, 0.55f, 0.15f},
{"dev/blue", 0.25f, 0.5f, 1.0f}, {"dev/green", 0.3f, 0.8f, 0.4f}, {"dev/red", 0.9f, 0.25f, 0.25f},
{"dev/gray", 0.5f, 0.5f, 0.52f}, {"dev/trigger", 1.0f, 0.4f, 0.1f}, {"dev/clip", 0.4f, 0.4f, 1.0f},
{"dev/water", 0.2f, 0.4f, 0.9f},
};
for (const D& d : defs) {
int i = map.addMaterial(d.name);
map.materials[(size_t)i].color = {d.r, d.g, d.b};
}
}
Map makeDemoMap() {
Map m;
m.name = "Hammer++ demo";
addDefaultMaterials(m);
EntityRegistry reg = EntityRegistry::builtin();
auto mat = [&](const char* n) { return m.findMaterial(n); };
auto addBrush = [&](Brush b) -> Brush& { b.id = m.newId(); m.brushes.push_back(std::move(b)); return m.brushes.back(); };
auto addMesh = [&](MeshObject o, const char* name) -> MeshObject& {
o.id = m.newId(); o.name = name; m.meshes.push_back(std::move(o)); return m.meshes.back();
};
auto addEnt = [&](const char* cls, Vec3 origin, Vec3 angles = {0, 0, 0}) -> Entity& {
Entity e; e.id = m.newId(); e.classname = cls; e.origin = origin; e.angles = angles;
reg.applyDefaults(e);
m.entities.push_back(std::move(e));
return m.entities.back();
};
// room: one big box, hollowed into six wall brushes (32 units thick)
Brush shell = makeBox({-544, -32, -544}, {544, 352, 544}, mat("dev/wall"));
for (Face& f : shell.faces) f.material = mat("dev/wall");
for (Brush& w : hollowBrush(shell, 32)) {
for (Face& f : w.faces)
if (f.plane.n.y > 0.9f && std::fabs(f.plane.d) < 0.5f) f.material = mat("dev/grid"); // floor surface (y = 0)
addBrush(w);
}
for (int sx = -1; sx <= 1; sx += 2)
for (int sz = -1; sz <= 1; sz += 2) {
float cx = 320.0f * (float)sx, cz = 320.0f * (float)sz;
addBrush(makeCylinder({cx - 40, 0, cz - 40}, {cx + 40, 320, cz + 40}, 12, mat("dev/orange")));
}
addBrush(makeBox({256, 0, -512}, {512, 64, -256}, mat("dev/blue")));
for (Brush& s : makeStairs({128, 0, -512}, {256, 64, -384}, 4, mat("dev/blue"))) addBrush(s);
addBrush(makeWedge({-256, 0, 256}, {-64, 48, 448}, mat("dev/green")));
addBrush(makeBox({64, 0, 128}, {128, 48, 192}, mat("dev/gray")));
// brush entities: a door (own model) and a trigger volume in front of it
Entity& door = addEnt("func_door", {0, 0, 0});
uint32_t doorId = door.id;
Brush db = makeBox({-64, 0, -208}, {64, 192, -192}, mat("dev/red"));
db.owner = doorId;
addBrush(db);
Entity& trig = addEnt("trigger_once", {0, 0, 0});
uint32_t trigId = trig.id;
Brush tb = makeBox({-96, 0, -176}, {96, 128, -96}, mat("dev/trigger"));
tb.owner = trigId;
tb.contents = CONTENTS_TRIGGER;
addBrush(tb);
// mesh-based geometry
addMesh(meshSphere({56, 48, 120}, {136, 128, 200}, 10, 16, mat("dev/orange")), "ball");
addMesh(meshTorus({-224, 96, -48}, {-128, 144, 48}, 24, 12, mat("dev/blue")), "ring");
MeshObject rug = meshGrid({160, 8, 96}, {416, 8, 352}, 16, 16, mat("dev/green"));
noiseMesh(rug, 10, 0.012f, 5);
addMesh(std::move(rug), "hill");
// entities
addEnt("info_player_start", {0, 36, 400});
for (int sx = -1; sx <= 1; sx += 2)
for (int sz = -1; sz <= 1; sz += 2) {
Entity& l = addEnt("light", {200.0f * (float)sx, 270, 200.0f * (float)sz});
l.set("radius", "700");
l.set("intensity", "0.85");
}
Entity& spot = addEnt("light_spot", {0, 300, -300}, {-90, 0, 0});
spot.set("intensity", "1.6");
addEnt("light_environment", {0, 340, 0}, {-50, 30, 0});
Entity& crate = addEnt("prop_static", {-400, 0, 160}, {0, 20, 0});
crate.set("model", "models/crate.obj");
Entity& crate2 = addEnt("prop_static", {-400, 0, 80}, {0, -10, 0});
crate2.set("model", "models/crate.obj");
Entity& pyr = addEnt("prop_static", {-300, 0, -380});
pyr.set("model", "models/pyramid.obj");
return m;
}
} // namespace hmap