Add project files.
This commit is contained in:
commit
a9ae435321
52 files changed
+65378
No files matched your search
@@ -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)
|
||||
@@ -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
|
||||
@@ -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 */
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user