Add project files.

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

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
/build/
/out/
/cmake-build-*/
*.ini
/.vs/
+37
View File
@@ -0,0 +1,37 @@
cmake_minimum_required(VERSION 3.20)
project(HammerPP VERSION 0.1.0 DESCRIPTION "Hammer++ map editor" LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()
option(HPP_BUILD_EDITOR "Build the Hammer++ editor (raylib + Dear ImGui + rlImGui)" ON)
option(HPP_BUILD_VIEWER "Build the minimal raylib example viewer" ON)
option(HPP_BUILD_TOOLS "Build the command line tools (hmap_info, hmap_sample)" ON)
option(HPP_BUILD_TESTS "Build the libhmap test-suite" ON)
# libhmap: map model, BSP/CSG compiler, lighting, .hmap format, FBX/OBJ export. No third-party dependencies.
add_subdirectory(libhmap)
if(HPP_BUILD_TOOLS)
add_subdirectory(tools)
endif()
if(HPP_BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
if(HPP_BUILD_EDITOR OR HPP_BUILD_VIEWER)
include(cmake/Dependencies.cmake) # raylib (+ Dear ImGui and rlImGui for the editor)
if(HPP_BUILD_VIEWER)
add_subdirectory(examples)
endif()
if(HPP_BUILD_EDITOR)
add_subdirectory(editor)
endif()
endif()
+183
View File
@@ -0,0 +1,183 @@
# Hammer++
A level editor in the spirit of Valve Hammer, written in **C++17 with raylib, Dear ImGui (via rlImGui) and CMake**.
It edits two kinds of geometry side by side and compiles both into one renderable, collidable, exportable map.
| | **Brushes** (BSP) | **Meshes** |
|---|---|---|
| Representation | convex solid = intersection of planes | free-form polygon mesh |
| Best for | rooms, walls, floors, collision | curves, terrain, organic detail, imported OBJ |
| Compile | CSG (hidden faces removed) + BSP collision tree | straight to render batches, smooth normals |
| Tools | block, clip, hollow, resize handles, tie-to-entity | vertex mode, extrude, noise terrain, flip, convert from brush |
Plus entities and props (models loaded through raylib), UV/texture tools, baked vertex lighting with BSP-traced
shadows, **FBX / OBJ export**, the **`.hmap` format** and a small **C++ / C runtime API** to load it in other apps.
> ### Status: v0.1 foundation - read this first
> * **`libhmap`** (geometry, CSG, BSP, lighting, file format, FBX/OBJ, C API) is covered by 2,939 automated checks and is
> clean under AddressSanitizer + UBSan, including a randomised BSP-vs-brute-force test and a file-corruption fuzz test.
> * **The editor** compiles and its logic is tested headless (1,515 checks: commands, undo, picking, camera maths,
> scripted mouse drags for move / resize / box-select / gizmo / paint / vertex edit). **The GUI itself has not been run
> against real raylib/ImGui yet** - the authoring environment had no network or GPU. Your first build is the real
> integration test; if the dependency versions drift, see *Pinning dependencies*.
> * **FBX** output follows the binary 7.4 layout and passes two independent structural parsers, but has **not** been
> opened in Blender / Unity / Unreal yet. Try `samples/demo.fbx` first.
> * Hammer parity is a **goal, not a fact**: see the checklist at the bottom for what exists and what does not.
## Build
```bash
cmake -B build
cmake --build build -j
./build/editor/hammerpp # opens the demo map (File > New for an empty room)
./build/examples/hmap_viewer samples/demo_runtime.hmap # tiny raylib-only viewer (example of integration)
./build/tools/hmap_info samples/demo.hmap --fbx out.fbx # inspect / convert from the command line
ctest --test-dir build # library tests
```
Needs CMake >= 3.20, a C++17 compiler and OpenGL 3.3. raylib, Dear ImGui and rlImGui are fetched automatically.
`libhmap` itself has **no** third-party dependencies (only `std::thread` for light baking).
| CMake option | Default | |
|---|---|---|
| `HPP_BUILD_EDITOR` | ON | the editor (fetches raylib + ImGui + rlImGui) |
| `HPP_BUILD_VIEWER` | ON | minimal raylib viewer example |
| `HPP_BUILD_TOOLS` | ON | `hmap_info`, `hmap_sample` |
| `HPP_BUILD_TESTS` | ON | `test_hmap`; add `-DHPP_BUILD_EDITOR_LOGIC_TESTS=ON` for the headless editor tests (Linux) |
| `HPP_USE_SYSTEM_RAYLIB` | OFF | use an installed raylib |
**Pinning dependencies.** Defaults: raylib `5.5` (the API the editor is written against), Dear ImGui `docking` branch,
rlImGui `main`. They are cache variables, so a mismatch is a one-line fix:
```bash
cmake -B build -DHPP_RAYLIB_TAG=5.5 -DHPP_IMGUI_TAG=v1.91.9b-docking -DHPP_RLIMGUI_TAG=<commit>
```
If rlImGui fails to compile, pin ImGui and rlImGui to a matching pair (ImGui 1.92 changed texture/font handling, so
rlImGui and ImGui must be from the same era). Without the docking branch the editor still builds; windows just float.
## Using the editor
Default layout: Top / Front / Side / 3D views, Outliner, Inspector, Assets (materials, models, entities) and Log.
Everything is dockable; *View > Reset window layout* restores it. **F1** shows all controls in-app.
| | |
|---|---|
| 3D view | hold **right mouse** + `W A S D`, `Q`/`E` down/up, `Shift` fast, wheel = speed. Wheel = dolly, middle drag = pan |
| 2D views | wheel zooms to cursor, right/middle drag pans, `F` frames the selection |
| Tools | `1` Select, `2` Block, `3` Entity, `4` Clip |
| Select | click, `Shift`/`Ctrl` adds, drag empty space = box select, drag = move, orange handles = resize, gizmo arrows in 3D |
| Block | drag a rectangle in a 2D view (height from *Create > Default height*) or click a surface in 3D. Shapes: box, wedge, cylinder, cone, stairs, sphere, torus, terrain patch; "as editable mesh" toggle |
| Entity | pick a class in *Assets > Entities* (props: pick a model in *Assets > Models*), click to place |
| Clip | two clicks in a 2D view, `Tab` = which side to keep, `Enter` = cut |
| Surfaces | `Alt+click` paints the current material (`+Shift` = whole object). *Face* mode + Inspector: material, offset, scale, rotation, flip, **Fit** |
| Meshes | *Vertex* mode: click / box-select vertices, drag them. Inspector > Mesh tools: extrude faces, noise terrain, flip normals |
| Brush entities | select brushes, `Ctrl+T` (or *Tie to entity*) -> `func_door`, `trigger_once`, `func_detail`... the Inspector then shows the entity's properties |
| Misc | `Ctrl+Z/Y` undo/redo, `Ctrl+D` duplicate, `Del`, `H` hide / `Alt+H` unhide, `G` snap, `[` `]` grid, **`F9` build** |
**Build (F9)** runs CSG, builds the BSP and (optionally) bakes lighting. Switch the view to *Baked* to preview it;
*Lit* is the live preview (ambient + sun + up to 16 point lights per pixel, no build needed); *Flat* and *Wireframe* too.
**Entities** are defined in a tiny FGD-style text format (`assets/entities.def`, built-ins in `libhmap/src/entdefs.cpp`)
and the Inspector builds its forms from it:
```
@point npc_spawner "NPC spawner" color(255,110,110) size(12,32,12) dir
npc_type choice "NPC type" "grunt" "grunt=Grunt,sniper=Sniper,boss=Boss"
count int "Count" "3"
```
Types: `string int float bool color255 vec3 choice model material target`. `@brush` classes become brush entities.
## Conventions
Right-handed, **+Y up**, forward **-Z**, `unitsPerMeter` (default 64) converts to metres. CCW triangles. UV `v` grows
downward (image space). Euler angles are `(pitch, yaw, roll)` degrees. Texture mapping is planar per face
(`Material::tileSize` world units per repeat; per-face offset / scale / rotation).
## Integrating the format in another app
C++ (`#include <hmap/hmap.h>`, link `hmap::hmap`):
```cpp
hmap::CompiledMap map;
if (hmap::Status s = hmap::loadCompiled("level.hmap", map); !s) { /* s.message */ }
for (const hmap::Batch& b : map.batches) { // one draw call each: model + material + (brush|mesh)
const hmap::Material& m = map.materials[b.material]; // texture path, tint, tile size
// upload b.vertices (pos, normal, uv, color = baked light) and b.indices
}
hmap::TraceResult hit = map.trace(from, to); // BSP brushes + solid meshes
bool underwater = map.pointContents(eye) & hmap::CONTENTS_WATER;
for (const hmap::Entity& e : map.entities) // lights, spawns, props, triggers...
if (e.classname == "prop_static") spawn(e.get("model"), e.transform());
```
`loadCompiled` accepts runtime files, editor files with a compiled chunk, and plain source files (it compiles on the
fly). `hmap::loadMap` / `saveMap` give you the editable `hmap::Map`; the same library can create maps in code
(`makeBox`, `hollowBrush`, `meshSphere`, ... then `compile`). See `examples/viewer.cpp` for a complete raylib consumer
and `tools/` for the command line tools.
Plain C (`#include <hmap/hmap_c.h>`) for engines, Python/C#/Rust FFI:
```c
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); /* draw */ }
hmap_trace t; hmap_trace_segment(m, from, to, HMAP_CONTENTS_SOLID, &t);
hmap_free(m);
```
The byte layout is in [`docs/FORMAT.md`](docs/FORMAT.md) if you would rather write your own reader.
Editor saves are source-only unless *Embed compiled data* is ticked; *File > Export > Runtime map* writes a compact
compiled-only file.
## Export
* **FBX** (binary 7.4): one Model + Geometry per world / brush-entity with every material as a sub-mesh, per-vertex normals,
UVs (V flipped to FBX convention), baked lighting as a colour layer, diffuse colour + texture path per material,
entities and props as Null nodes carrying their key-values as custom properties. Y-up right-handed, `UnitScaleFactor`
derived from *Units per metre* (and the export *Scale* option). No skeletons or animation.
* **OBJ + MTL**, and the **runtime `.hmap`**.
* Import: **OBJ** as an editable mesh. Props use raylib's loaders: obj, gltf/glb, iqm, vox, m3d.
## Project layout
```
libhmap/ dependency-free library: model, geometry, CSG, BSP, lighting, .hmap IO, FBX/OBJ, entity defs, C API
editor/ raylib + ImGui front-end: editor.cpp (commands), viewport.cpp (cameras, tools, gizmo), render.cpp
(one lit shader for brush + mesh batches, caches), ui.cpp (docking UI, inspector, file dialog)
examples/ viewer.cpp - minimal raylib consumer tools/ hmap_info, hmap_sample
tests/ libhmap tests + optional headless editor tests samples/ demo.hmap / demo_runtime.hmap / demo.fbx / demo.obj
assets/ entities.def, sample props (crate.obj, pyramid.obj); put textures in assets/materials, models in assets/models
```
How the pieces fit: brushes -> `buildBrushGeometry` (plane clipping) -> CSG against other solids -> triangle batches
**and** collision brushes -> BSP tree (nodes split on brush planes, leaves hold brush lists; used for `trace`,
`pointContents` and light shadows). Meshes skip the BSP: they become batches (smooth/flat normals) and, if they collide,
collision triangles. The editor viewport draws the *same* shader for per-object edit batches and for compiled batches.
## Hammer parity checklist
| Feature | Status |
|---|---|
| Brush editing (block, clip, hollow, resize, move, rotate, scale, mirror) | yes (no vertex editing of brushes - convert to mesh) |
| 2D + 3D views, grid snapping, undo/redo, hide, duplicate | yes |
| Texture application, offset/scale/rotation/fit, texture lock on move | yes (lock for translation only) |
| Entities with auto-generated property forms, brush entities, models/props | yes (own definition format; no FGD import) |
| Displacements | partial: terrain meshes + noise + vertex edit, no sculpt brush |
| Curves / arches / stairs primitives | stairs, cylinder, cone, sphere, torus yes; arch no |
| Compile: CSG + BSP + lighting | yes (vertex lighting with shadows; **no** lightmaps, PVS/portals or game-specific BSP output) |
| Visgroups, prefabs/instances, groups | no (hide only) |
| Entity I/O connections, overlays/decals, cordon, measure tool, skybox | no |
| Export | FBX, OBJ, own format (more than stock Hammer) |
Ideas for next steps: prefab/instance support, sculpt brush for terrain, lightmap atlas, portal/PVS, FGD import,
glTF export, multi-mesh vertex editing, material browser thumbnails with search.
## Troubleshooting
* *Black / grey viewports* - needs OpenGL 3.3. Try `Shading = Flat` to rule out the lit shader.
* *Textures missing* - put images in `assets/materials`, *Assets > Materials > Add texture*. Without a file a tinted
checker is generated from the material colour.
* *Props show as boxes* - the model path is relative to `assets/`; check *Assets > Models* and the Log panel.
* *Layout broken after an update* - delete `hammerpp.ini` or *View > Reset window layout*.
+22
View File
@@ -0,0 +1,22 @@
# Hammer++ custom entity definitions. Loaded on start-up after the built-in set, so you can add
# your game's entities here - or redefine a built-in class by repeating its name.
#
# @point <class> "<label>" [color(r,g,b)] [size(h) | size(hx,hy,hz)] [dir] [model]
# @brush <class> "<label>" [color(r,g,b)] [contents(solid|clip|trigger|water|sky)]
# <key> <type> "<label>" "<default>" ["value=Label,value=Label"]
# types: string int float bool color255 vec3 choice model material target
# dir = shows an orientation arrow, model = displays the "model" key as a prop
@point npc_spawner "NPC spawner" color(255,110,110) size(12,32,12) dir
npc_type choice "NPC type" "grunt" "grunt=Grunt,sniper=Sniper,boss=Boss"
count int "Count" "3"
respawn bool "Respawn" "0"
patrol_target target "Patrol path start" ""
@point env_fog "Fog volume" color(180,200,230) size(10)
color color255 "Fog color" "180 190 210"
start float "Start distance" "256"
end float "End distance" "2048"
@brush trigger_hurt "Hurt trigger" color(255,60,60) contents(trigger)
damage float "Damage per second" "10"
+2
View File
@@ -0,0 +1,2 @@
newmtl dev/orange
Kd 1.0 0.55 0.15
+28
View File
@@ -0,0 +1,28 @@
# Hammer++ sample prop: crate
mtllib crate.mtl
o crate
v -32 0 -32
v 32 0 -32
v 32 64 -32
v -32 64 -32
v -32 0 32
v 32 0 32
v 32 64 32
v -32 64 32
vn 1 0 0
vn -1 0 0
vn 0 1 0
vn 0 -1 0
vn 0 0 1
vn 0 0 -1
vt 0 0
vt 1 0
vt 1 1
vt 0 1
usemtl dev/orange
f 2/1/1 3/2/1 7/3/1 6/4/1
f 1/1/2 5/2/2 8/3/2 4/4/2
f 4/1/3 8/2/3 7/3/3 3/4/3
f 1/1/4 2/2/4 6/3/4 5/4/4
f 5/1/5 6/2/5 7/3/5 8/4/5
f 1/1/6 4/2/6 3/3/6 2/4/6
+17
View File
@@ -0,0 +1,17 @@
# Hammer++ sample prop: pyramid
o pyramid
v -48 0 -48
v 48 0 -48
v 48 0 48
v -48 0 48
v 0 96 0
vt 0 1
vt 1 1
vt 0.5 0
vt 1 0
usemtl dev/blue
f 1/1 5/3 2/2
f 2/1 5/3 3/2
f 3/1 5/3 4/2
f 4/1 5/3 1/2
f 1/1 2/2 3/4 4/3
+51
View File
@@ -0,0 +1,51 @@
# Third-party dependencies for the editor and the example viewer, fetched with FetchContent.
# Everything is overridable from the command line, e.g.
# cmake -B build -DHPP_RAYLIB_TAG=5.5 -DHPP_IMGUI_TAG=v1.91.9b-docking -DHPP_RLIMGUI_TAG=<commit>
include(FetchContent)
set(HPP_RAYLIB_TAG "5.5" CACHE STRING "raylib git tag (the editor is written against the 5.5 API)")
set(HPP_IMGUI_TAG "docking" CACHE STRING "Dear ImGui git ref - MUST be the docking branch (or a *-docking tag)")
set(HPP_RLIMGUI_TAG "main" CACHE STRING "rlImGui git ref")
option(HPP_USE_SYSTEM_RAYLIB "Use an installed raylib instead of fetching one" OFF)
if(HPP_USE_SYSTEM_RAYLIB)
find_package(raylib REQUIRED)
else()
set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(BUILD_GAMES OFF CACHE BOOL "" FORCE)
FetchContent_Declare(raylib
GIT_REPOSITORY https://github.com/raysan5/raylib.git
GIT_TAG ${HPP_RAYLIB_TAG}
GIT_SHALLOW TRUE)
FetchContent_MakeAvailable(raylib)
endif()
if(HPP_BUILD_EDITOR)
# SOURCE_SUBDIR points at a folder that does not exist: we only want the sources, not their build systems.
FetchContent_Declare(imgui
GIT_REPOSITORY https://github.com/ocornut/imgui.git
GIT_TAG ${HPP_IMGUI_TAG}
GIT_SHALLOW TRUE
SOURCE_SUBDIR hpp_no_cmake)
FetchContent_Declare(rlimgui
GIT_REPOSITORY https://github.com/raylib-extras/rlImGui.git
GIT_TAG ${HPP_RLIMGUI_TAG}
GIT_SHALLOW TRUE
SOURCE_SUBDIR hpp_no_cmake)
FetchContent_MakeAvailable(imgui rlimgui)
add_library(hpp_imgui STATIC
${imgui_SOURCE_DIR}/imgui.cpp
${imgui_SOURCE_DIR}/imgui_draw.cpp
${imgui_SOURCE_DIR}/imgui_tables.cpp
${imgui_SOURCE_DIR}/imgui_widgets.cpp
${imgui_SOURCE_DIR}/imgui_demo.cpp
${imgui_SOURCE_DIR}/misc/cpp/imgui_stdlib.cpp
${rlimgui_SOURCE_DIR}/rlImGui.cpp)
target_include_directories(hpp_imgui PUBLIC
${imgui_SOURCE_DIR}
${imgui_SOURCE_DIR}/misc/cpp
${rlimgui_SOURCE_DIR})
target_compile_definitions(hpp_imgui PUBLIC NO_FONT_AWESOME) # skip rlImGui's bundled icon font
target_link_libraries(hpp_imgui PUBLIC raylib)
endif()
+48
View File
@@ -0,0 +1,48 @@
# `.hmap` file format (version 1)
Chunked, little-endian binary. Readers must skip chunks they do not know, so new chunks can be added without
breaking old readers. A file may carry **source** data (editable), **compiled** data (runtime), or both.
```
header "HMAP" (4 bytes) | u32 version (=1) | u32 flags (bit0 = source chunks, bit1 = compiled chunk)
chunk* tag[4] | u32 payloadSize | payload
```
Primitive types: `u8 u32 i32 f32` (LE) - `str` = u32 byteLength + UTF-8 bytes (no terminator) -
`vec2/vec3` = 2/3 f32 - `plane` = vec3 normal + f32 d (points p on the plane satisfy `dot(n,p) == d`, solid is on the
back side `dot(n,p) < d`) - `aabb` = vec3 min + vec3 max - `uv` = vec2 offset, vec2 scale, f32 rotation(deg).
| Tag | Content |
|-----|---------|
| `META` | str name, vec3 ambient, f32 gridSize, f32 unitsPerMeter, u32 nextId |
| `MATS` | u32 n, then n x { str name, str texture, vec2 tileSize, vec3 color, u32 flags } |
| `BRSH` | u32 n, then n x { u32 id, u32 owner, u32 contents, u32 flags, u32 faceCount, faces: { plane, i32 material, uv, u32 flags } } |
| `MESH` | u32 n, then n x { u32 id, u32 owner, u32 flags, str name, u32 vertCount, vec3[], u32 polyCount, polys: { i32 material, u32 smooth, uv proj, u32 idxCount, u32[], u32 uvCount, vec2[] } } |
| `ENTS` | u32 n, then n x { u32 id, u32 flags, str classname, vec3 origin, vec3 angles, vec3 scale, u32 kvCount, (str key, str value)[] } |
| `CMPL` | compiled map, see below |
* `owner` = id of the brush entity that owns the object (0 = world). `contents` bits: 1 solid, 2 water, 4 clip, 8 trigger, 16 sky.
* Mesh polygons are convex, indices are CCW seen from the front. `uvCount` is either 0 (auto-projected with `proj`) or `idxCount`.
* Angles are `(pitch, yaw, roll)` degrees, applied as `R = Ry(yaw) * Rx(pitch) * Rz(roll)`; forward = -Z at zero angles.
## `CMPL` (compiled)
```
vec3 ambient | f32 unitsPerMeter | u8 baked | aabb bounds
u32 batchCount, then { i32 material, u8 source (0 brush/BSP, 1 mesh), i32 model,
u32 vertexCount, vertex[] { vec3 pos, vec3 normal, vec2 uv, u32 colorRGBA },
u32 indexCount, u32[] } // triangle lists, CCW front
u32 modelCount, then { u32 entityId, aabb, u32 brushCount, collisionBrush[] } // model 0 = world
BSP: i32 root | u32 depth | aabb | u32 planeCount, plane[] | u32 nodeCount, { i32 plane, i32 front, i32 back }[]
| u32 leafCount, { u32 firstBrush, u32 brushCount }[] | u32 leafBrushCount, u32[] | u32 brushCount, collisionBrush[]
u32 meshCollCount, then { u32 objectId, aabb, u32 vertexCount, vec3[] } // 3 vertices per triangle
u32 lightCount, then { u8 type (0 point, 1 spot, 2 sun), vec3 pos, vec3 dir, vec3 color,
f32 intensity, f32 radius, f32 innerDeg, f32 outerDeg, u32 entityId }
collisionBrush = u32 contents, u32 sourceId, aabb, u32 planeCount, plane[]
```
BSP child links: `>= 0` is a node index, `< 0` is leaf `~value`. Every node splits on `planes[node.plane]`
(front = `dot(n,p) - d >= 0`). Leaves list collision brushes; a point is solid when it lies behind all planes of a brush.
Colour is little-endian RGBA bytes (`0xAABBGGRR`); it holds the baked vertex lighting when `baked = 1`, otherwise white.
Entities and materials in a compiled-only file come from the `ENTS` / `MATS` chunks that precede `CMPL`.
+16
View File
@@ -0,0 +1,16 @@
add_executable(hammerpp
main.cpp
editor.cpp
render.cpp
viewport.cpp
ui.cpp)
target_link_libraries(hammerpp PRIVATE hmap hpp_imgui raylib)
target_compile_definitions(hammerpp PRIVATE HPP_ASSET_DIR="${CMAKE_SOURCE_DIR}/assets")
if(MSVC)
target_compile_options(hammerpp PRIVATE /W3 /utf-8)
endif()
# keep the assets next to the executable so the editor also runs from anywhere
add_custom_command(TARGET hammerpp POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/assets $<TARGET_FILE_DIR:hammerpp>/assets
COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/samples $<TARGET_FILE_DIR:hammerpp>/samples)
+790
View File
@@ -0,0 +1,790 @@
#include "editor.h"
#include <algorithm>
#include <cmath>
#include <cstdarg>
#include <cstdio>
namespace hpp {
bool Selection::has(const ObjRef& r) const {
for (const ObjRef& o : objs)
if (o == r) return true;
return false;
}
void Editor::log(const char* fmt, ...) {
char buf[512];
va_list ap;
va_start(ap, fmt);
std::vsnprintf(buf, sizeof buf, fmt, ap);
va_end(ap);
logLines.push_back(buf);
if (logLines.size() > 500) logLines.erase(logLines.begin(), logLines.begin() + 100);
TraceLog(LOG_INFO, "[Hammer++] %s", buf);
}
// ============================================================ lifecycle
void Editor::init() {
renderer.init();
entdefs = hmap::EntityRegistry::builtin();
// locate the asset folder: ./assets, <exe>/assets, or the source tree it was built from
std::string appAssets = std::string(GetApplicationDirectory()) + "assets";
if (DirectoryExists("assets")) assetRoot = "assets";
else if (DirectoryExists(appAssets.c_str())) assetRoot = appAssets;
#ifdef HPP_ASSET_DIR
else assetRoot = HPP_ASSET_DIR;
#endif
std::string defsPath = assetRoot + "/entities.def";
if (FileExists(defsPath.c_str())) {
char* txt = LoadFileText(defsPath.c_str());
if (txt) {
hmap::Status s = entdefs.parse(txt);
UnloadFileText(txt);
if (!s) log("entities.def: %s", s.message.c_str());
else log("Loaded custom entity definitions from %s", defsPath.c_str());
}
}
const char* names[4] = {"3D", "Top", "Front", "Side"};
const ViewKind kinds[4] = {ViewKind::Persp, ViewKind::Top, ViewKind::Front, ViewKind::Side};
for (int i = 0; i < 4; ++i) {
views[i].title = names[i];
views[i].kind = kinds[i];
views[i].center = {0, 64, 0};
views[i].zoom = 0.4f;
}
views[0].pos = {320, 260, 420};
views[0].yaw = 38;
views[0].pitch = -22;
newMap(true);
log("Hammer++ ready. Right-mouse + WASD flies the 3D view; F1 shows all controls.");
}
void Editor::shutdown() {
for (Viewport& v : views)
if (v.rt.id != 0) UnloadRenderTexture(v.rt);
renderer.shutdown();
}
void Editor::touch() {
++docVersion;
dirty = true;
compiledStale = hasCompiled;
}
void Editor::pushUndo() {
undoStack.push_back(doc);
if (undoStack.size() > 48) undoStack.erase(undoStack.begin());
redoStack.clear();
}
void Editor::editBegin() {
if (!inEdit) {
pushUndo();
inEdit = true;
}
}
void Editor::undo() {
if (undoStack.empty()) return;
redoStack.push_back(doc);
doc = std::move(undoStack.back());
undoStack.pop_back();
touch();
pruneSelection();
}
void Editor::redo() {
if (redoStack.empty()) return;
undoStack.push_back(doc);
doc = std::move(redoStack.back());
redoStack.pop_back();
touch();
pruneSelection();
}
// ============================================================ documents
void Editor::newMap(bool demo) {
if (demo) {
doc = hmap::makeDemoMap();
} else {
doc = hmap::Map();
hmap::addDefaultMaterials(doc);
int wall = doc.findMaterial("dev/wall"), floorM = doc.findMaterial("dev/grid");
hmap::Brush shell = hmap::makeBox({-256, -16, -256}, {256, 208, 256}, wall);
for (hmap::Brush& w : hmap::hollowBrush(shell, 16)) {
for (hmap::Face& f : w.faces)
if (f.plane.n.y > 0.9f && std::fabs(f.plane.d) < 0.5f) f.material = floorM;
w.id = doc.newId();
doc.brushes.push_back(w);
}
auto addEnt = [&](const char* cls, Vec3 o, Vec3 a) {
hmap::Entity e;
e.id = doc.newId();
e.classname = cls;
e.origin = o;
e.angles = a;
entdefs.applyDefaults(e);
doc.entities.push_back(e);
};
addEnt("light", {0, 150, 0}, {0, 0, 0});
addEnt("info_player_start", {0, 36, 128}, {0, 0, 0});
addEnt("light_environment", {0, 200, 0}, {-50, 30, 0});
}
docPath.clear();
undoStack.clear();
redoStack.clear();
sel.clear();
hasCompiled = compiledStale = false;
curMaterial = std::max(0, doc.findMaterial("dev/grid"));
grid = doc.settings.gridSize > 0 ? doc.settings.gridSize : 16;
touch();
dirty = false;
cache.version = 0;
}
bool Editor::openMap(const std::string& path) {
hmap::Map m;
hmap::Status s = hmap::loadMap(path, m);
if (!s) {
log("Open failed: %s", s.message.c_str());
return false;
}
doc = std::move(m);
docPath = path;
undoStack.clear();
redoStack.clear();
sel.clear();
hasCompiled = compiledStale = false;
if (doc.materials.empty()) hmap::addDefaultMaterials(doc);
curMaterial = std::min(curMaterial, (int)doc.materials.size() - 1);
grid = doc.settings.gridSize > 0 ? doc.settings.gridSize : 16;
touch();
dirty = false;
cache.version = 0;
log("Opened %s (%zu brushes, %zu meshes, %zu entities)", path.c_str(), doc.brushes.size(), doc.meshes.size(), doc.entities.size());
return true;
}
bool Editor::saveMap(const std::string& path) {
doc.settings.gridSize = grid;
hmap::Status s = hmap::saveMap(path, doc, saveCompiledToo ? &compiled : nullptr);
if (!s) {
log("Save failed: %s", s.message.c_str());
return false;
}
docPath = path;
dirty = false;
log("Saved %s", path.c_str());
return true;
}
void Editor::compileNow() {
compiled = hmap::compile(doc, compileOpt);
hasCompiled = true;
compiledStale = false;
renderer.setCompiledBatches(compiled.batches);
const hmap::CompileStats& st = compiled.stats;
log("Compiled in %.0f ms: %u brushes, %u meshes, %u triangles, BSP %u nodes / %u leaves (depth %u), %u lights%s", st.ms, st.brushes,
st.meshes, st.triangles, st.bspNodes, st.bspLeaves, st.bspDepth, st.lights, compiled.baked ? ", lighting baked" : "");
if (compileOpt.bakeLighting) shading = Shading::Baked;
}
void Editor::exportFbx(const std::string& path) {
hmap::CompiledMap cm = hmap::compile(doc, compileOpt);
hmap::Status s = hmap::exportFBX(path, cm, fbxOpt);
log(s ? "Exported FBX: %s (%u triangles)" : "FBX export failed: %s", s ? path.c_str() : s.message.c_str(), cm.stats.triangles);
}
void Editor::exportObj(const std::string& path) {
hmap::CompiledMap cm = hmap::compile(doc, compileOpt);
hmap::Status s = hmap::exportOBJ(path, cm, fbxOpt.scale);
log(s ? "Exported OBJ: %s" : "OBJ export failed: %s", s ? path.c_str() : s.message.c_str());
}
void Editor::exportCompiled(const std::string& path) {
hmap::CompiledMap cm = hmap::compile(doc, compileOpt);
hmap::Status s = hmap::saveCompiled(path, cm);
log(s ? "Exported runtime map: %s" : "Export failed: %s", s ? path.c_str() : s.message.c_str());
}
// ============================================================ scene cache / picking
const SceneCache& Editor::scene() {
if (cache.version != docVersion) {
cache.brushGeo.resize(doc.brushes.size());
for (size_t i = 0; i < doc.brushes.size(); ++i) cache.brushGeo[i] = hmap::buildBrushGeometry(doc.brushes[i]);
cache.meshBounds.resize(doc.meshes.size());
for (size_t i = 0; i < doc.meshes.size(); ++i) cache.meshBounds[i] = hmap::meshBounds(doc.meshes[i]);
cache.version = docVersion;
refreshGpu();
}
return cache;
}
void Editor::refreshGpu() {
std::vector<hmap::Batch> batches;
hmap::buildEditBatches(doc, batches);
renderer.setEditBatches(batches);
lights = hmap::collectLights(doc);
}
AABB Editor::entityBounds(const hmap::Entity& e) {
const hmap::EntityClass* c = classOf(e);
Vec3 half = c ? c->half : Vec3(8, 8, 8);
AABB bb;
if (c && c->hasModel) {
std::string path = e.get("model");
if (!path.empty()) {
ModelEntry& me = renderer.model(path, assetRoot);
if (me.ok) {
Mat34 xf = e.transform();
for (int i = 0; i < 8; ++i) {
Vec3 p((i & 1) ? me.box.max.x : me.box.min.x, (i & 2) ? me.box.max.y : me.box.min.y, (i & 4) ? me.box.max.z : me.box.min.z);
bb.add(xf.point(p));
}
return bb;
}
}
}
Vec3 s(std::fabs(e.scale.x), std::fabs(e.scale.y), std::fabs(e.scale.z));
Vec3 h(half.x * (c && c->hasModel ? s.x : 1.0f), half.y * (c && c->hasModel ? s.y : 1.0f), half.z * (c && c->hasModel ? s.z : 1.0f));
bb.add(e.origin - h);
bb.add(e.origin + h);
return bb;
}
Vec3 Editor::entityColor(const hmap::Entity& e) const {
const hmap::EntityClass* c = classOf(e);
return c ? c->color : Vec3(0.8f, 0.8f, 0.8f);
}
AABB Editor::objectBounds(const ObjRef& r) {
const SceneCache& sc = scene();
switch (r.type) {
case ObjType::Brush:
for (size_t i = 0; i < doc.brushes.size(); ++i)
if (doc.brushes[i].id == r.id) return sc.brushGeo[i].bounds;
break;
case ObjType::Mesh:
for (size_t i = 0; i < doc.meshes.size(); ++i)
if (doc.meshes[i].id == r.id) return sc.meshBounds[i];
break;
case ObjType::Entity:
if (const hmap::Entity* e = doc.findEntity(r.id)) return entityBounds(*e);
break;
}
return AABB();
}
AABB Editor::selectionBounds() {
AABB bb;
for (const ObjRef& r : sel.objs) bb.add(objectBounds(r));
return bb;
}
Vec3 Editor::selectionCenter() {
AABB bb = selectionBounds();
return bb.empty() ? Vec3() : bb.center();
}
Hit Editor::pick(const HRay& r) {
Hit best;
const SceneCache& sc = scene();
for (size_t i = 0; i < doc.brushes.size(); ++i) {
const hmap::Brush& b = doc.brushes[i];
if (b.flags & hmap::OBJ_HIDDEN) continue;
const AABB& bb = sc.brushGeo[i].bounds;
float tn, tf;
if (bb.empty() || !hmap::rayAABB(r.o, r.d, bb.expanded(0.05f), tn, tf)) continue;
float t;
int f;
if (hmap::rayBrush(b, r.o, r.d, t, f) && t < best.t) {
best.hit = true; best.t = t; best.obj = {ObjType::Brush, b.id}; best.face = f; best.normal = b.faces[(size_t)f].plane.n;
}
}
for (size_t i = 0; i < doc.meshes.size(); ++i) {
const hmap::MeshObject& m = doc.meshes[i];
if (m.flags & hmap::OBJ_HIDDEN) continue;
float tn, tf;
if (sc.meshBounds[i].empty() || !hmap::rayAABB(r.o, r.d, sc.meshBounds[i].expanded(0.05f), tn, tf)) continue;
float t;
int p;
if (hmap::rayMesh(m, r.o, r.d, t, p) && t < best.t) {
hmap::Polygon poly;
for (uint32_t k : m.polys[(size_t)p].idx) poly.push_back(m.verts[k]);
best.hit = true; best.t = t; best.obj = {ObjType::Mesh, m.id}; best.face = p; best.normal = hmap::polyNormal(poly);
}
}
for (const hmap::Entity& e : doc.entities) {
const hmap::EntityClass* c = classOf(e);
if ((c && c->brush) || (e.flags & hmap::OBJ_HIDDEN)) continue;
AABB bb = entityBounds(e);
float tn, tf;
if (!hmap::rayAABB(r.o, r.d, bb, tn, tf)) continue;
float t = std::max(tn, 0.0f);
if (t < best.t) {
best.hit = true; best.t = t; best.obj = {ObjType::Entity, e.id}; best.face = -1; best.normal = {0, 1, 0};
}
}
if (best.hit) best.point = r.o + r.d * best.t;
return best;
}
// ============================================================ selection
void Editor::clearSelection() { sel.clear(); }
void Editor::select(const ObjRef& r, bool additive) {
if (!additive) {
sel.clear();
sel.objs.push_back(r);
return;
}
auto it = std::find(sel.objs.begin(), sel.objs.end(), r);
if (it != sel.objs.end()) sel.objs.erase(it);
else sel.objs.push_back(r);
sel.faces.clear();
sel.verts.clear();
}
void Editor::selectAll() {
sel.clear();
for (const hmap::Brush& b : doc.brushes)
if (!(b.flags & hmap::OBJ_HIDDEN)) sel.objs.push_back({ObjType::Brush, b.id});
for (const hmap::MeshObject& m : doc.meshes)
if (!(m.flags & hmap::OBJ_HIDDEN)) sel.objs.push_back({ObjType::Mesh, m.id});
for (const hmap::Entity& e : doc.entities) {
const hmap::EntityClass* c = classOf(e);
if (!(c && c->brush) && !(e.flags & hmap::OBJ_HIDDEN)) sel.objs.push_back({ObjType::Entity, e.id});
}
}
void Editor::pruneSelection() {
std::vector<ObjRef> keep;
for (const ObjRef& r : sel.objs) {
bool ok = r.type == ObjType::Brush ? doc.findBrush(r.id) != nullptr
: r.type == ObjType::Mesh ? doc.findMesh(r.id) != nullptr
: doc.findEntity(r.id) != nullptr;
if (ok) keep.push_back(r);
}
sel.objs = keep;
ObjRef p = sel.primary();
int limit = 0;
if (p.type == ObjType::Brush && doc.findBrush(p.id)) limit = (int)doc.findBrush(p.id)->faces.size();
if (p.type == ObjType::Mesh && doc.findMesh(p.id)) limit = (int)doc.findMesh(p.id)->polys.size();
sel.faces.erase(std::remove_if(sel.faces.begin(), sel.faces.end(), [&](int f) { return f >= limit; }), sel.faces.end());
int vlimit = (p.type == ObjType::Mesh && doc.findMesh(p.id)) ? (int)doc.findMesh(p.id)->verts.size() : 0;
sel.verts.erase(std::remove_if(sel.verts.begin(), sel.verts.end(), [&](int v) { return v >= vlimit; }), sel.verts.end());
}
bool Editor::selectedBrushes(std::vector<hmap::Brush*>& out) {
for (const ObjRef& r : sel.objs)
if (r.type == ObjType::Brush)
if (hmap::Brush* b = doc.findBrush(r.id)) out.push_back(b);
return !out.empty();
}
// ============================================================ edit commands
static void pruneOrphanEntities(Editor& ed) {
auto used = [&](uint32_t id) {
for (const hmap::Brush& b : ed.doc.brushes) if (b.owner == id) return true;
for (const hmap::MeshObject& m : ed.doc.meshes) if (m.owner == id) return true;
return false;
};
auto& es = ed.doc.entities;
es.erase(std::remove_if(es.begin(), es.end(),
[&](const hmap::Entity& e) {
const hmap::EntityClass* c = ed.classOf(e);
return c && c->brush && !used(e.id);
}),
es.end());
}
void Editor::deleteSelection() {
if (sel.empty()) return;
pushUndo();
for (const ObjRef& r : sel.objs) {
auto sameId = [&](const auto& o) { return o.id == r.id; };
if (r.type == ObjType::Brush) doc.brushes.erase(std::remove_if(doc.brushes.begin(), doc.brushes.end(), sameId), doc.brushes.end());
else if (r.type == ObjType::Mesh) doc.meshes.erase(std::remove_if(doc.meshes.begin(), doc.meshes.end(), sameId), doc.meshes.end());
else doc.entities.erase(std::remove_if(doc.entities.begin(), doc.entities.end(), sameId), doc.entities.end());
}
pruneOrphanEntities(*this);
sel.clear();
touch();
}
void Editor::duplicateSelection() {
if (sel.empty()) return;
pushUndo();
Vec3 off(grid, 0, grid);
std::vector<ObjRef> created;
for (const ObjRef& r : std::vector<ObjRef>(sel.objs)) {
if (r.type == ObjType::Brush) {
if (const hmap::Brush* src = doc.findBrush(r.id)) {
hmap::Brush b = *src;
b.id = doc.newId();
hmap::translateBrush(b, off, true);
doc.brushes.push_back(b);
created.push_back({ObjType::Brush, b.id});
}
} else if (r.type == ObjType::Mesh) {
if (const hmap::MeshObject* src = doc.findMesh(r.id)) {
hmap::MeshObject m = *src;
m.id = doc.newId();
for (Vec3& v : m.verts) v += off;
doc.meshes.push_back(m);
created.push_back({ObjType::Mesh, m.id});
}
} else if (const hmap::Entity* src = doc.findEntity(r.id)) {
hmap::Entity e = *src;
e.id = doc.newId();
e.origin += off;
doc.entities.push_back(e);
created.push_back({ObjType::Entity, e.id});
}
}
sel.clear();
sel.objs = created;
touch();
log("Duplicated %zu object(s)", created.size());
}
void Editor::hideSelection(bool hide) {
for (const ObjRef& r : sel.objs) {
uint32_t* flags = nullptr;
if (r.type == ObjType::Brush) { if (auto* b = doc.findBrush(r.id)) flags = &b->flags; }
else if (r.type == ObjType::Mesh) { if (auto* m = doc.findMesh(r.id)) flags = &m->flags; }
else if (auto* e = doc.findEntity(r.id)) flags = &e->flags;
if (!flags) continue;
if (hide) *flags |= hmap::OBJ_HIDDEN;
else *flags &= ~(uint32_t)hmap::OBJ_HIDDEN;
}
if (hide) sel.clear();
touch();
}
void Editor::unhideAll() {
for (auto& b : doc.brushes) b.flags &= ~(uint32_t)hmap::OBJ_HIDDEN;
for (auto& m : doc.meshes) m.flags &= ~(uint32_t)hmap::OBJ_HIDDEN;
for (auto& e : doc.entities) e.flags &= ~(uint32_t)hmap::OBJ_HIDDEN;
touch();
}
void Editor::hollowSelection(float thickness) {
std::vector<uint32_t> ids;
for (const ObjRef& r : sel.objs)
if (r.type == ObjType::Brush) ids.push_back(r.id);
if (ids.empty()) { log("Hollow: select a brush first"); return; }
pushUndo();
std::vector<ObjRef> next;
int hollowed = 0;
for (uint32_t id : ids) {
const hmap::Brush* b = doc.findBrush(id);
if (!b) continue;
std::vector<hmap::Brush> walls = hmap::hollowBrush(*b, thickness);
if (walls.empty()) continue;
doc.brushes.erase(std::remove_if(doc.brushes.begin(), doc.brushes.end(), [&](const hmap::Brush& x) { return x.id == id; }), doc.brushes.end());
for (hmap::Brush& w : walls) {
w.id = doc.newId();
doc.brushes.push_back(w);
next.push_back({ObjType::Brush, w.id});
}
++hollowed;
}
if (!hollowed) { undoStack.pop_back(); log("Hollow failed: wall thickness is too large for this brush"); return; }
sel.clear();
sel.objs = next;
touch();
log("Hollowed %d brush(es) into %zu wall brushes", hollowed, next.size());
}
void Editor::convertSelectionToMesh() {
std::vector<uint32_t> ids;
for (const ObjRef& r : sel.objs)
if (r.type == ObjType::Brush) ids.push_back(r.id);
if (ids.empty()) return;
pushUndo();
std::vector<ObjRef> next;
for (const ObjRef& r : sel.objs)
if (r.type != ObjType::Brush) next.push_back(r);
for (uint32_t id : ids) {
const hmap::Brush* b = doc.findBrush(id);
if (!b) continue;
hmap::MeshObject m = hmap::brushToMesh(*b);
m.id = doc.newId();
m.name = "mesh " + std::to_string(m.id);
doc.brushes.erase(std::remove_if(doc.brushes.begin(), doc.brushes.end(), [&](const hmap::Brush& x) { return x.id == id; }), doc.brushes.end());
next.push_back({ObjType::Mesh, m.id});
doc.meshes.push_back(std::move(m));
}
sel.clear();
sel.objs = next;
touch();
log("Converted %zu brush(es) to editable meshes", ids.size());
}
void Editor::tieSelectionToEntity(const std::string& cls) {
const hmap::EntityClass* c = entdefs.find(cls);
if (!c || !c->brush) return;
std::vector<ObjRef> targets;
for (const ObjRef& r : sel.objs)
if (r.type != ObjType::Entity) targets.push_back(r);
if (targets.empty()) { log("Tie to entity: select some brushes or meshes first"); return; }
pushUndo();
hmap::Entity e;
e.id = doc.newId();
e.classname = cls;
entdefs.applyDefaults(e);
uint32_t eid = e.id;
doc.entities.push_back(e);
for (const ObjRef& r : targets) {
if (r.type == ObjType::Brush) {
if (auto* b = doc.findBrush(r.id)) { b->owner = eid; b->contents = c->brushContents; }
} else if (auto* m = doc.findMesh(r.id)) m->owner = eid;
}
pruneOrphanEntities(*this);
touch();
log("Tied %zu object(s) to new %s", targets.size(), cls.c_str());
}
void Editor::untieSelection() {
pushUndo();
for (const ObjRef& r : sel.objs) {
if (r.type == ObjType::Brush) {
if (auto* b = doc.findBrush(r.id)) { b->owner = 0; b->contents = hmap::CONTENTS_SOLID; }
} else if (r.type == ObjType::Mesh) {
if (auto* m = doc.findMesh(r.id)) m->owner = 0;
}
}
pruneOrphanEntities(*this);
touch();
}
void Editor::clipSelection(const Plane& p, int mode) {
std::vector<uint32_t> ids;
for (const ObjRef& r : sel.objs)
if (r.type == ObjType::Brush) ids.push_back(r.id);
if (ids.empty()) { log("Clip: select the brush(es) to cut first"); return; }
pushUndo();
std::vector<ObjRef> next;
for (uint32_t id : ids) {
const hmap::Brush* b = doc.findBrush(id);
if (!b) continue;
hmap::Brush front, back;
hmap::clipBrush(*b, p, &front, &back);
std::vector<hmap::Brush> keep;
if (!front.faces.empty() && mode != 1) keep.push_back(front);
if (!back.faces.empty() && mode != 0) keep.push_back(back);
if (keep.empty() || (front.faces.empty() && back.faces.empty())) continue;
doc.brushes.erase(std::remove_if(doc.brushes.begin(), doc.brushes.end(), [&](const hmap::Brush& x) { return x.id == id; }), doc.brushes.end());
for (hmap::Brush& k : keep) {
k.id = doc.newId();
doc.brushes.push_back(k);
next.push_back({ObjType::Brush, k.id});
}
}
sel.clear();
sel.objs = next;
touch();
}
void Editor::createPrimitive(Vec3 mn, Vec3 mx) {
for (int i = 0; i < 3; ++i) {
if (mx[i] < mn[i]) std::swap(mx[i], mn[i]);
if (mx[i] - mn[i] < 1.0f) mx[i] = mn[i] + std::max(grid, 1.0f);
}
pushUndo();
std::vector<ObjRef> created;
int mat = curMaterial;
auto addB = [&](hmap::Brush b) {
b.id = doc.newId();
created.push_back({ObjType::Brush, b.id});
doc.brushes.push_back(std::move(b));
};
auto addM = [&](hmap::MeshObject m, const char* name) {
m.id = doc.newId();
m.name = name;
created.push_back({ObjType::Mesh, m.id});
doc.meshes.push_back(std::move(m));
};
bool asMesh = createAsMesh || shape == Shape::Sphere || shape == Shape::Torus || shape == Shape::Terrain;
auto brushOrMesh = [&](hmap::Brush b, const char* name) {
if (asMesh) addM(hmap::brushToMesh(b), name);
else addB(std::move(b));
};
switch (shape) {
case Shape::Box:
if (asMesh) addM(hmap::meshBox(mn, mx, mat), "box");
else addB(hmap::makeBox(mn, mx, mat));
break;
case Shape::Wedge: brushOrMesh(hmap::makeWedge(mn, mx, mat), "wedge"); break;
case Shape::Cylinder:
if (asMesh) addM(hmap::meshCylinder(mn, mx, shapeSides, mat), "cylinder");
else addB(hmap::makeCylinder(mn, mx, shapeSides, mat));
break;
case Shape::Cone:
if (asMesh) addM(hmap::meshCone(mn, mx, shapeSides, mat), "cone");
else addB(hmap::makeCone(mn, mx, shapeSides, mat));
break;
case Shape::Stairs:
for (hmap::Brush& s : hmap::makeStairs(mn, mx, stairSteps, mat)) brushOrMesh(std::move(s), "step");
break;
case Shape::Sphere: addM(hmap::meshSphere(mn, mx, std::max(4, shapeSides / 2), shapeSides, mat), "sphere"); break;
case Shape::Torus: addM(hmap::meshTorus(mn, mx, shapeSides * 2, 12, mat), "torus"); break;
case Shape::Terrain: addM(hmap::meshGrid(mn, mx, 16, 16, mat), "terrain"); break;
}
sel.clear();
sel.objs = created;
touch();
}
void Editor::createEntityAt(const Vec3& origin) {
const hmap::EntityClass* c = entdefs.find(curEntity);
if (!c) return;
if (c->brush) { log("%s is a brush entity: select brushes and use Tie to entity (Ctrl+T)", curEntity.c_str()); return; }
pushUndo();
hmap::Entity e;
e.id = doc.newId();
e.classname = curEntity;
e.origin = origin;
entdefs.applyDefaults(e);
if (c->hasModel && !curModel.empty()) e.set("model", curModel);
doc.entities.push_back(e);
sel.clear();
sel.objs.push_back({ObjType::Entity, e.id});
touch();
}
void Editor::applyMaterialToSelection(int material) {
if (sel.empty()) return;
pushUndo();
ObjRef p = sel.primary();
for (const ObjRef& r : sel.objs) {
if (r.type == ObjType::Brush) {
if (auto* b = doc.findBrush(r.id)) {
bool faceSel = selMode == SelMode::Face && r == p && !sel.faces.empty();
for (size_t i = 0; i < b->faces.size(); ++i)
if (!faceSel || std::find(sel.faces.begin(), sel.faces.end(), (int)i) != sel.faces.end()) b->faces[i].material = material;
}
} else if (r.type == ObjType::Mesh) {
if (auto* m = doc.findMesh(r.id)) {
bool faceSel = selMode == SelMode::Face && r == p && !sel.faces.empty();
for (size_t i = 0; i < m->polys.size(); ++i)
if (!faceSel || std::find(sel.faces.begin(), sel.faces.end(), (int)i) != sel.faces.end()) m->polys[i].material = material;
}
}
}
touch();
}
void Editor::paintHit(const Hit& h, bool wholeObject) {
if (!h.hit) return;
pushUndo();
if (h.obj.type == ObjType::Brush) {
if (auto* b = doc.findBrush(h.obj.id))
for (size_t i = 0; i < b->faces.size(); ++i)
if (wholeObject || (int)i == h.face) b->faces[i].material = curMaterial;
} else if (h.obj.type == ObjType::Mesh) {
if (auto* m = doc.findMesh(h.obj.id))
for (size_t i = 0; i < m->polys.size(); ++i)
if (wholeObject || (int)i == h.face) m->polys[i].material = curMaterial;
} else {
undoStack.pop_back();
return;
}
touch();
}
void Editor::flipSelection() {
pushUndo();
for (const ObjRef& r : sel.objs)
if (r.type == ObjType::Mesh)
if (auto* m = doc.findMesh(r.id)) hmap::flipMesh(*m);
touch();
}
void Editor::importObjFile(const std::string& path) {
pushUndo();
hmap::MeshObject m;
hmap::Status s = hmap::importOBJ(path, doc, m);
if (!s) {
undoStack.pop_back();
log("OBJ import failed: %s", s.message.c_str());
return;
}
m.id = doc.newId();
sel.clear();
sel.objs.push_back({ObjType::Mesh, m.id});
log("Imported %s as mesh '%s' (%zu verts, %zu polys)", path.c_str(), m.name.c_str(), m.verts.size(), m.polys.size());
doc.meshes.push_back(std::move(m));
touch();
}
// ============================================================ drag support
DragSnap Editor::captureSnap() {
DragSnap s;
for (const ObjRef& r : sel.objs) {
if (r.type == ObjType::Brush) { if (auto* b = doc.findBrush(r.id)) s.brushes.push_back(*b); }
else if (r.type == ObjType::Mesh) { if (auto* m = doc.findMesh(r.id)) s.meshes.push_back(*m); }
else if (auto* e = doc.findEntity(r.id)) s.entities.push_back(*e);
}
return s;
}
void Editor::restoreSnap(const DragSnap& s) {
for (const auto& b : s.brushes) if (auto* p = doc.findBrush(b.id)) *p = b;
for (const auto& m : s.meshes) if (auto* p = doc.findMesh(m.id)) *p = m;
for (const auto& e : s.entities) if (auto* p = doc.findEntity(e.id)) *p = e;
}
static bool isTranslation(const Mat34& m) {
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 3; ++j)
if (std::fabs(m.m[i][j] - (i == j ? 1.0f : 0.0f)) > 1e-5f) return false;
return true;
}
void Editor::applyMatrix(const Mat34& m, bool lockTexture) {
bool pure = isTranslation(m);
Vec3 t = m.translation();
for (const ObjRef& r : sel.objs) {
if (r.type == ObjType::Brush) {
if (auto* b = doc.findBrush(r.id)) {
if (pure) hmap::translateBrush(*b, t, lockTexture);
else hmap::transformBrush(*b, m);
}
} else if (r.type == ObjType::Mesh) {
if (auto* me = doc.findMesh(r.id)) hmap::transformMesh(*me, m);
} else if (auto* e = doc.findEntity(r.id)) {
e->origin = m.point(e->origin);
if (pure) continue;
Vec3 c0(m.m[0][0], m.m[1][0], m.m[2][0]), c1(m.m[0][1], m.m[1][1], m.m[2][1]), c2(m.m[0][2], m.m[1][2], m.m[2][2]);
Vec3 ls(hmap::length(c0), hmap::length(c1), hmap::length(c2));
if (ls.x > 1e-6f && ls.y > 1e-6f && ls.z > 1e-6f) {
Mat34 rot;
for (int i = 0; i < 3; ++i) {
rot.m[i][0] = m.m[i][0] / ls.x;
rot.m[i][1] = m.m[i][1] / ls.y;
rot.m[i][2] = m.m[i][2] / ls.z;
}
if (rot.det3() > 0) e->angles = (rot * Mat34::fromEuler(e->angles)).toEuler();
if (std::fabs(ls.x - 1) > 1e-4f || std::fabs(ls.y - 1) > 1e-4f || std::fabs(ls.z - 1) > 1e-4f)
e->scale = Vec3(e->scale.x * ls.x, e->scale.y * ls.y, e->scale.z * ls.z);
}
}
}
touch();
}
void Editor::translateSelectedVerts(const Vec3& d) {
ObjRef p = sel.primary();
if (p.type != ObjType::Mesh) return;
if (auto* m = doc.findMesh(p.id))
for (int v : sel.verts)
if (v >= 0 && (size_t)v < m->verts.size()) m->verts[(size_t)v] += d;
touch();
}
} // namespace hpp
+284
View File
@@ -0,0 +1,284 @@
// Hammer++ editor: shared types. The editor is a thin UI layer over libhmap.
#pragma once
#include <hmap/hmap.h>
#include <cstdint>
#include <functional>
#include <string>
#include <unordered_map>
#include <vector>
#include "imgui.h"
#include "raylib.h"
#include "render.h"
namespace hpp {
using hmap::AABB;
using hmap::Mat34;
using hmap::Plane;
using hmap::Vec2;
using hmap::Vec3;
enum class Tool { Select, Block, Entity, Clip };
enum class SelMode { Object, Face, Vertex };
enum class Shading { Wire, Flat, Lit, Baked };
enum class ViewKind { Persp, Top, Front, Side };
enum class ObjType { Brush, Mesh, Entity };
enum class Shape { Box, Wedge, Cylinder, Cone, Stairs, Sphere, Torus, Terrain };
struct ObjRef {
ObjType type = ObjType::Brush;
uint32_t id = 0;
bool operator==(const ObjRef& o) const { return type == o.type && id == o.id; }
};
struct Selection {
std::vector<ObjRef> objs;
std::vector<int> faces; // faces (brush) / polygons (mesh) of the primary object
std::vector<int> verts; // vertex indices of the primary mesh
bool has(const ObjRef& r) const;
void clear() { objs.clear(); faces.clear(); verts.clear(); }
bool empty() const { return objs.empty(); }
ObjRef primary() const { return objs.empty() ? ObjRef{} : objs.back(); }
};
struct HRay { Vec3 o, d; };
struct Hit {
bool hit = false;
float t = 1e30f;
ObjRef obj;
int face = -1;
Vec3 point, normal;
};
struct Viewport {
const char* title = "";
ViewKind kind = ViewKind::Persp;
bool open = true;
RenderTexture2D rt = {};
int w = 0, h = 0;
// perspective camera
Vec3 pos{0, 256, 512};
float yaw = 0, pitch = -25, fov = 70, flySpeed = 320;
// orthographic camera
Vec3 center{0, 0, 0};
float zoom = 0.5f; // pixels per world unit
// per-frame interaction state
ImVec2 imgMin{0, 0};
bool hovered = false, flying = false;
bool ortho() const { return kind != ViewKind::Persp; }
Vec3 forward() const;
Vec3 right() const;
Vec3 up() const;
int depthAxis() const; // ortho: world axis along the view direction
Camera3D camera() const;
HRay ray(float sx, float sy) const;
bool project(const Vec3& p, float& sx, float& sy) const; // false when behind the camera
Vec3 planePoint(float sx, float sy, float depth) const; // ortho: world point on the plane at `depth`
};
struct DragSnap {
std::vector<hmap::Brush> brushes;
std::vector<hmap::MeshObject> meshes;
std::vector<hmap::Entity> entities;
};
struct DragState {
enum Kind { None, Move, Resize, Gizmo, BoxSelect, Block } kind = None;
Viewport* view = nullptr;
Vec3 startWorld;
ImVec2 startScreen{0, 0}, curScreen{0, 0};
bool moved = false;
bool tookUndo = false;
int hx = 0, hy = 0; // resize handle
float rectU0 = 0, rectU1 = 0, rectV0 = 0, rectV1 = 0; // selection rect in view-plane coordinates
int axis = -1; // gizmo axis
float s0 = 0;
Vec3 origin;
Vec3 blockA, blockB;
DragSnap snap;
};
struct SceneCache {
uint64_t version = 0;
std::vector<hmap::BrushGeometry> brushGeo; // parallel to Map::brushes
std::vector<AABB> meshBounds; // parallel to Map::meshes
};
class FileDialog {
public:
using Callback = std::function<void(const std::string&)>;
void open(const std::string& title, const std::string& startDir, std::vector<std::string> exts, bool save, Callback cb,
const std::string& defaultName = "");
void draw();
private:
std::string title_, dir_, name_;
std::vector<std::string> exts_;
bool save_ = false, pending_ = false;
Callback cb_;
};
class Editor {
public:
// ------------------------------------------------------------ lifecycle
void init();
void frame(); // one full UI frame; call between rlImGuiBegin() and rlImGuiEnd()
void shutdown();
// ------------------------------------------------------------ document
hmap::Map doc;
std::string docPath;
bool dirty = false;
uint64_t docVersion = 1;
hmap::EntityRegistry entdefs;
std::string assetRoot = "assets";
std::vector<hmap::Map> undoStack, redoStack;
// ------------------------------------------------------------ state
Selection sel;
Tool tool = Tool::Select;
SelMode selMode = SelMode::Object;
Shading shading = Shading::Lit;
bool snapOn = true;
float grid = 16;
int curMaterial = 0;
Shape shape = Shape::Box;
bool createAsMesh = false;
int shapeSides = 12;
int stairSteps = 8;
std::string curEntity = "prop_static";
std::string curModel;
float blockHeight = 64;
bool saveCompiledToo = false;
hmap::CompiledMap compiled;
bool hasCompiled = false, compiledStale = false;
hmap::CompileOptions compileOpt;
hmap::FbxOptions fbxOpt;
Renderer renderer;
Viewport views[4];
DragState drag;
SceneCache cache;
std::vector<hmap::Light> lights;
uint64_t lightsVersion = 0;
FileDialog fileDialog;
std::vector<std::string> logLines;
bool showDemo = false, resetLayout = false, showHelp = false, wantQuit = false;
std::function<void()> pendingAction;
bool askDiscard = false;
void guarded(std::function<void()> act); // runs `act`, asking first when there are unsaved changes
bool panelOutliner = true, panelInspector = true, panelAssets = true, panelLog = true;
// clip tool
bool clipHasA = false, clipHasB = false;
Vec3 clipA, clipB;
int clipMode = 0; // 0 keep front, 1 keep back, 2 keep both
int clipView = 0;
// ------------------------------------------------------------ commands (editor.cpp)
void log(const char* fmt, ...);
void newMap(bool demo);
bool openMap(const std::string& path);
bool saveMap(const std::string& path);
void touch(); // document changed
void pushUndo();
void editBegin(); // call before applying an inspector edit (one undo entry per edit session)
void undo();
void redo();
void clearSelection();
void select(const ObjRef& r, bool additive);
void selectAll();
void pruneSelection();
bool selectedBrushes(std::vector<hmap::Brush*>& out);
AABB objectBounds(const ObjRef& r);
AABB selectionBounds();
Vec3 selectionCenter();
hmap::Brush* findBrush(uint32_t id) { return doc.findBrush(id); }
const hmap::EntityClass* classOf(const hmap::Entity& e) const { return entdefs.find(e.classname); }
AABB entityBounds(const hmap::Entity& e);
Vec3 entityColor(const hmap::Entity& e) const;
void deleteSelection();
void duplicateSelection();
void hideSelection(bool hide);
void unhideAll();
void hollowSelection(float thickness);
void convertSelectionToMesh();
void tieSelectionToEntity(const std::string& cls);
void untieSelection();
void clipSelection(const Plane& p, int mode);
void createPrimitive(Vec3 mn, Vec3 mx);
void createEntityAt(const Vec3& origin);
void applyMaterialToSelection(int material);
void paintHit(const Hit& h, bool wholeObject);
void flipSelection();
void importObjFile(const std::string& path);
DragSnap captureSnap();
void restoreSnap(const DragSnap& s);
void applyMatrix(const Mat34& m, bool lockTexture);
void translateSelectedVerts(const Vec3& d);
void compileNow();
void exportFbx(const std::string& path);
void exportObj(const std::string& path);
void exportCompiled(const std::string& path);
const SceneCache& scene();
void refreshGpu();
Hit pick(const HRay& r);
float snapValue(float v) const { return snapOn ? std::round(v / grid) * grid : v; }
// ------------------------------------------------------------ ui (ui.cpp)
void drawUI();
void drawMenu();
void drawToolbar();
void drawOutliner();
void drawInspector();
void drawAssets();
void drawLog();
void drawHelp();
void shortcuts();
void buildLayout(ImGuiID dock);
bool propEditor(hmap::Entity& e, const hmap::PropDef& p);
void entityInspector(hmap::Entity& e, bool brushEntity);
void faceInspector();
void transformInspector();
void openModelPicker(std::function<void(const std::string&)> cb);
bool inEdit = false;
std::vector<std::string> modelFiles;
std::vector<std::string> textureFiles;
bool filesScanned = false;
void scanAssets();
std::string relativeToAssets(const std::string& path) const;
// ------------------------------------------------------------ viewports (viewport.cpp)
void drawViewport(Viewport& v);
void viewInput(Viewport& v, bool active);
void toolInput3D(Viewport& v, const ImVec2& m);
void toolInput2D(Viewport& v, const ImVec2& m);
void renderView(Viewport& v);
void drawGrid(Viewport& v);
void drawScene3D(Viewport& v);
void drawWire(Viewport& v);
void drawEntities(Viewport& v);
void drawGizmo(Viewport& v);
void drawOverlay2D(Viewport& v);
void focusSelection();
bool gizmoVisible() const;
Vec3 gizmoCenter();
int gizmoHit(Viewport& v, const ImVec2& m);
bool selectionRect(Viewport& v, float& u0, float& u1, float& v0, float& v1);
};
void applyTheme();
void firstPlace(float fx, float fy, float fw, float fh);
} // namespace hpp
+39
View File
@@ -0,0 +1,39 @@
// Hammer++ - map editor built on raylib + Dear ImGui (rlImGui) + libhmap.
#include "editor.h"
#include "imgui.h"
#include "raylib.h"
#include "rlImGui.h"
int main(int argc, char** argv) {
SetConfigFlags(FLAG_WINDOW_RESIZABLE | FLAG_VSYNC_HINT | FLAG_MSAA_4X_HINT);
InitWindow(1600, 900, "Hammer++");
SetWindowMinSize(960, 600);
SetExitKey(KEY_NULL); // Esc cancels tools instead of quitting
rlImGuiSetup(true);
ImGuiIO& io = ImGui::GetIO();
#ifdef IMGUI_HAS_DOCK
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
#endif
io.IniFilename = "hammerpp.ini";
io.ConfigWindowsMoveFromTitleBarOnly = true;
hpp::applyTheme();
hpp::Editor editor;
editor.init();
if (argc > 1) editor.openMap(argv[1]);
while (!WindowShouldClose() && !editor.wantQuit) {
BeginDrawing();
ClearBackground(Color{20, 22, 27, 255});
rlImGuiBegin();
editor.frame();
rlImGuiEnd();
EndDrawing();
}
editor.shutdown();
rlImGuiShutdown();
CloseWindow();
return 0;
}
+252
View File
@@ -0,0 +1,252 @@
#include "render.h"
#include <algorithm>
#include <cmath>
#include "raymath.h"
namespace hpp {
// One shader serves every geometry source. uMode: 0 = flat shaded, 1 = ambient + sun + point lights
// (cheap per-pixel Lambert), 2 = baked per-vertex lighting produced by hmap::bakeLighting().
static const char* kVS = R"GLSL(#version 330
in vec3 vertexPosition;
in vec2 vertexTexCoord;
in vec3 vertexNormal;
in vec4 vertexColor;
uniform mat4 mvp;
uniform mat4 matModel;
uniform mat4 matNormal;
out vec3 fragPos;
out vec2 fragUV;
out vec3 fragN;
out vec4 fragColor;
void main() {
fragPos = vec3(matModel * vec4(vertexPosition, 1.0));
fragUV = vertexTexCoord;
fragN = normalize(vec3(matNormal * vec4(vertexNormal, 0.0)));
fragColor = vertexColor;
gl_Position = mvp * vec4(vertexPosition, 1.0);
}
)GLSL";
static const char* kFS = R"GLSL(#version 330
in vec3 fragPos;
in vec2 fragUV;
in vec3 fragN;
in vec4 fragColor;
uniform sampler2D texture0;
uniform vec4 colDiffuse;
uniform int uMode;
uniform vec3 uAmbient;
uniform vec3 uSunDir;
uniform vec3 uSunColor;
uniform int uLightCount;
uniform vec4 uLightPosR[16];
uniform vec4 uLightColI[16];
out vec4 finalColor;
void main() {
vec4 tex = texture(texture0, fragUV) * colDiffuse;
vec3 n = normalize(fragN);
vec3 light;
if (uMode == 1) {
light = uAmbient + uSunColor * max(dot(n, -uSunDir), 0.0);
for (int i = 0; i < uLightCount; i++) {
vec3 d = uLightPosR[i].xyz - fragPos;
float dist = length(d);
float r = uLightPosR[i].w;
float att = clamp(1.0 - (dist * dist) / (r * r), 0.0, 1.0);
att *= att;
light += uLightColI[i].rgb * uLightColI[i].a * max(dot(n, d / max(dist, 0.001)), 0.0) * att;
}
} else if (uMode == 2) {
light = fragColor.rgb;
} else {
light = vec3(0.50 + 0.50 * max(dot(n, normalize(vec3(0.45, 0.8, 0.4))), 0.0));
}
finalColor = vec4(tex.rgb * light, tex.a);
}
)GLSL";
void Renderer::init() {
shader_ = LoadShaderFromMemory(kVS, kFS);
locMode_ = GetShaderLocation(shader_, "uMode");
locAmbient_ = GetShaderLocation(shader_, "uAmbient");
locSunDir_ = GetShaderLocation(shader_, "uSunDir");
locSunColor_ = GetShaderLocation(shader_, "uSunColor");
locCount_ = GetShaderLocation(shader_, "uLightCount");
locPos_ = GetShaderLocation(shader_, "uLightPosR");
locCol_ = GetShaderLocation(shader_, "uLightColI");
mat_ = LoadMaterialDefault();
mat_.shader = shader_;
}
void Renderer::freeGpu(std::vector<Gpu>& v) {
for (Gpu& g : v) UnloadMesh(g.mesh);
v.clear();
}
void Renderer::clearCaches() {
for (auto& kv : textures_) UnloadTexture(kv.second);
textures_.clear();
for (auto& kv : models_) {
if (!kv.second.ok) continue;
Shader def = {rlGetShaderIdDefault(), rlGetShaderLocsDefault()};
for (int i = 0; i < kv.second.model.materialCount; ++i) kv.second.model.materials[i].shader = def; // keep our shader alive
UnloadModel(kv.second.model);
}
models_.clear();
}
void Renderer::shutdown() {
freeGpu(edit_);
freeGpu(compiled_);
clearCaches();
MemFree(mat_.maps);
mat_.maps = nullptr;
UnloadShader(shader_);
}
void Renderer::upload(const std::vector<hmap::Batch>& in, std::vector<Gpu>& out, size_t& tris) {
freeGpu(out);
tris = 0;
for (const hmap::Batch& b : in) {
size_t n = b.indices.size();
n -= n % 3;
if (n == 0) continue;
Mesh m = {};
m.vertexCount = (int)n; // expanded to a plain triangle list: no 16-bit index limit
m.triangleCount = (int)(n / 3);
m.vertices = (float*)MemAlloc((unsigned)(n * 3 * sizeof(float)));
m.texcoords = (float*)MemAlloc((unsigned)(n * 2 * sizeof(float)));
m.normals = (float*)MemAlloc((unsigned)(n * 3 * sizeof(float)));
m.colors = (unsigned char*)MemAlloc((unsigned)(n * 4));
for (size_t i = 0; i < n; ++i) {
const hmap::Vertex& v = b.vertices[b.indices[i]];
m.vertices[i * 3 + 0] = v.pos.x; m.vertices[i * 3 + 1] = v.pos.y; m.vertices[i * 3 + 2] = v.pos.z;
m.normals[i * 3 + 0] = v.normal.x; m.normals[i * 3 + 1] = v.normal.y; m.normals[i * 3 + 2] = v.normal.z;
m.texcoords[i * 2 + 0] = v.uv.x; m.texcoords[i * 2 + 1] = v.uv.y;
m.colors[i * 4 + 0] = (unsigned char)(v.color & 0xff);
m.colors[i * 4 + 1] = (unsigned char)((v.color >> 8) & 0xff);
m.colors[i * 4 + 2] = (unsigned char)((v.color >> 16) & 0xff);
m.colors[i * 4 + 3] = (unsigned char)((v.color >> 24) & 0xff);
}
UploadMesh(&m, false);
out.push_back({m, b.material});
tris += n / 3;
}
}
void Renderer::setEditBatches(const std::vector<hmap::Batch>& b) { upload(b, edit_, editTriangles); }
void Renderer::setCompiledBatches(const std::vector<hmap::Batch>& b) { upload(b, compiled_, compiledTriangles); }
void Renderer::setLights(const std::vector<hmap::Light>& lights, const hmap::Vec3& ambient) {
float amb[3] = {ambient.x, ambient.y, ambient.z};
float sunDir[3] = {0, -1, 0}, sunCol[3] = {0, 0, 0};
float pos[16 * 4] = {}, col[16 * 4] = {};
int n = 0;
bool haveSun = false;
for (const hmap::Light& L : lights) {
if (L.type == hmap::LightType::Sun) {
if (haveSun) continue;
hmap::Vec3 d = hmap::normalize(L.dir);
sunDir[0] = d.x; sunDir[1] = d.y; sunDir[2] = d.z;
sunCol[0] = L.color.x * L.intensity; sunCol[1] = L.color.y * L.intensity; sunCol[2] = L.color.z * L.intensity;
haveSun = true;
continue;
}
if (n >= 16) continue; // spot lights are previewed as point lights
pos[n * 4 + 0] = L.pos.x; pos[n * 4 + 1] = L.pos.y; pos[n * 4 + 2] = L.pos.z; pos[n * 4 + 3] = std::max(L.radius, 1.0f);
col[n * 4 + 0] = L.color.x; col[n * 4 + 1] = L.color.y; col[n * 4 + 2] = L.color.z; col[n * 4 + 3] = L.intensity;
++n;
}
SetShaderValue(shader_, locAmbient_, amb, SHADER_UNIFORM_VEC3);
SetShaderValue(shader_, locSunDir_, sunDir, SHADER_UNIFORM_VEC3);
SetShaderValue(shader_, locSunColor_, sunCol, SHADER_UNIFORM_VEC3);
SetShaderValue(shader_, locCount_, &n, SHADER_UNIFORM_INT);
if (n > 0) {
SetShaderValueV(shader_, locPos_, pos, SHADER_UNIFORM_VEC4, n);
SetShaderValueV(shader_, locCol_, col, SHADER_UNIFORM_VEC4, n);
}
}
void Renderer::setMode(int m) { SetShaderValue(shader_, locMode_, &m, SHADER_UNIFORM_INT); }
Texture2D Renderer::texture(const hmap::Material& m, const std::string& root) {
std::string key = m.name + "|" + m.texture + "|" + root;
auto it = textures_.find(key);
if (it != textures_.end()) return it->second;
Texture2D t = {};
if (!m.texture.empty()) {
std::string path = root + "/" + m.texture;
if (FileExists(path.c_str())) t = LoadTexture(path.c_str());
}
if (t.id == 0) { // no file: procedural checker, tinted by Material::color at draw time
Image img = GenImageChecked(64, 64, 8, 8, Color{236, 236, 236, 255}, Color{176, 176, 182, 255});
t = LoadTextureFromImage(img);
UnloadImage(img);
}
GenTextureMipmaps(&t);
SetTextureFilter(t, TEXTURE_FILTER_TRILINEAR);
SetTextureWrap(t, TEXTURE_WRAP_REPEAT);
textures_[key] = t;
return t;
}
static bool isTranslucent(const std::string& name) {
return name.find("trigger") != std::string::npos || name.find("clip") != std::string::npos ||
name.find("water") != std::string::npos || name.find("glass") != std::string::npos;
}
void Renderer::drawBatches(bool useCompiled, const std::vector<hmap::Material>& mats, const std::string& root, int mode) {
setMode(mode);
static const hmap::Material fallback;
std::vector<Gpu>& list = useCompiled ? compiled_ : edit_;
for (int pass = 0; pass < 2; ++pass) { // opaque first, then translucent volumes
for (Gpu& g : list) {
const hmap::Material& hm = (g.material >= 0 && (size_t)g.material < mats.size()) ? mats[(size_t)g.material] : fallback;
bool tr = isTranslucent(hm.name);
if ((pass == 1) != tr) continue;
auto q = [](float v) { return (unsigned char)(std::max(0.0f, std::min(1.0f, v)) * 255.0f); };
mat_.maps[MATERIAL_MAP_DIFFUSE].texture = texture(hm, root);
mat_.maps[MATERIAL_MAP_DIFFUSE].color = Color{q(hm.color.x), q(hm.color.y), q(hm.color.z), (unsigned char)(tr ? 130 : 255)};
DrawMesh(g.mesh, mat_, MatrixIdentity());
}
}
}
ModelEntry& Renderer::model(const std::string& rel, const std::string& root) {
auto it = models_.find(rel);
if (it != models_.end()) return it->second;
ModelEntry me;
bool isAbs = rel.size() > 1 && (rel[0] == '/' || rel[1] == ':');
std::string path = isAbs ? rel : root + "/" + rel;
if (!rel.empty() && FileExists(path.c_str())) {
me.model = LoadModel(path.c_str());
if (me.model.meshCount > 0) {
me.ok = true;
for (int i = 0; i < me.model.materialCount; ++i) {
me.model.materials[i].shader = shader_;
me.baseColors.push_back(me.model.materials[i].maps[MATERIAL_MAP_DIFFUSE].color);
}
me.box = GetModelBoundingBox(me.model);
}
}
return models_.emplace(rel, std::move(me)).first->second;
}
void Renderer::drawModel(ModelEntry& me, const hmap::Mat34& xf, Color tint, int mode) {
if (!me.ok) return;
setMode(mode);
Matrix t = toRlMatrix(xf);
for (int i = 0; i < me.model.meshCount; ++i) {
int mi = me.model.meshMaterial[i];
Material& m = me.model.materials[mi];
Color base = (size_t)mi < me.baseColors.size() ? me.baseColors[(size_t)mi] : WHITE;
m.maps[MATERIAL_MAP_DIFFUSE].color = Color{(unsigned char)(base.r * tint.r / 255), (unsigned char)(base.g * tint.g / 255),
(unsigned char)(base.b * tint.b / 255), (unsigned char)(base.a * tint.a / 255)};
DrawMesh(me.model.meshes[i], m, MatrixMultiply(me.model.transform, t));
}
}
} // namespace hpp
+79
View File
@@ -0,0 +1,79 @@
// Renderer: one shader + one draw path for both BSP (brush) batches and mesh-object batches.
#pragma once
#include <hmap/hmap.h>
#include <string>
#include <unordered_map>
#include <vector>
#include "raylib.h"
#include "rlgl.h"
namespace hpp {
inline Vector3 toRl(const hmap::Vec3& v) { return {v.x, v.y, v.z}; }
// hmap::Mat34 uses column vectors (p' = M p + t); raylib's Matrix is laid out so that
// MatrixToFloat() yields the same column-major OpenGL matrix.
inline Matrix toRlMatrix(const hmap::Mat34& a) {
Matrix m;
m.m0 = a.m[0][0]; m.m4 = a.m[0][1]; m.m8 = a.m[0][2]; m.m12 = a.m[0][3];
m.m1 = a.m[1][0]; m.m5 = a.m[1][1]; m.m9 = a.m[1][2]; m.m13 = a.m[1][3];
m.m2 = a.m[2][0]; m.m6 = a.m[2][1]; m.m10 = a.m[2][2]; m.m14 = a.m[2][3];
m.m3 = 0; m.m7 = 0; m.m11 = 0; m.m15 = 1;
return m;
}
// Batches immediate-mode lines so a huge wireframe never overflows rlgl's vertex buffer.
struct LineBatch {
int n = 0;
void begin() { rlBegin(RL_LINES); n = 0; }
void line(const hmap::Vec3& a, const hmap::Vec3& b, Color c) {
rlColor4ub(c.r, c.g, c.b, c.a);
rlVertex3f(a.x, a.y, a.z);
rlVertex3f(b.x, b.y, b.z);
if (++n >= 2048) { rlEnd(); rlBegin(RL_LINES); n = 0; }
}
void end() { rlEnd(); }
};
struct ModelEntry {
Model model = {};
bool ok = false;
BoundingBox box = {};
std::vector<Color> baseColors;
};
class Renderer {
public:
void init();
void shutdown();
void setEditBatches(const std::vector<hmap::Batch>& b); // raw per-object geometry (no CSG)
void setCompiledBatches(const std::vector<hmap::Batch>& b); // CSG'd + baked geometry
void setLights(const std::vector<hmap::Light>& lights, const hmap::Vec3& ambient);
// mode: 0 = flat, 1 = dynamic lights, 2 = baked vertex colours
void drawBatches(bool compiled, const std::vector<hmap::Material>& mats, const std::string& assetRoot, int mode);
void drawModel(ModelEntry& me, const hmap::Mat34& xf, Color tint, int mode);
ModelEntry& model(const std::string& relPath, const std::string& assetRoot);
Texture2D texture(const hmap::Material& m, const std::string& assetRoot);
void clearCaches();
size_t editTriangles = 0, compiledTriangles = 0;
private:
struct Gpu { Mesh mesh; int material; };
void upload(const std::vector<hmap::Batch>& in, std::vector<Gpu>& out, size_t& tris);
void freeGpu(std::vector<Gpu>& v);
void setMode(int m);
Shader shader_ = {};
Material mat_ = {};
int locMode_ = -1, locAmbient_ = -1, locSunDir_ = -1, locSunColor_ = -1, locCount_ = -1, locPos_ = -1, locCol_ = -1;
std::vector<Gpu> edit_, compiled_;
std::unordered_map<std::string, Texture2D> textures_;
std::unordered_map<std::string, ModelEntry> models_;
};
} // namespace hpp
+1131
View File
File diff suppressed because it is too large. Load diff
+1002
View File
File diff suppressed because it is too large. Load diff
+2
View File
@@ -0,0 +1,2 @@
add_executable(hmap_viewer viewer.cpp)
target_link_libraries(hmap_viewer PRIVATE hmap raylib)
+117
View File
@@ -0,0 +1,117 @@
// Minimal raylib viewer: shows how little code it takes to use a Hammer++ map in another app.
// hmap_viewer [file.hmap] WASD + mouse (hold right button) to fly, Shift = fast
//
// Everything map-related is: loadCompiled() -> iterate batches -> trace()/pointContents().
// Baked vertex lighting is already in Vertex::color, which raylib's default shader multiplies in.
#include <hmap/hmap.h>
#include <cmath>
#include <cstdio>
#include <string>
#include <vector>
#include "raylib.h"
#include "raymath.h" // MatrixIdentity
struct GpuBatch {
Mesh mesh;
int material;
};
static Mesh toRaylib(const hmap::Batch& b) {
Mesh m = {};
size_t n = b.indices.size(); // expand to a non-indexed triangle list (no 16-bit index limit)
m.vertexCount = (int)n;
m.triangleCount = (int)(n / 3);
m.vertices = (float*)MemAlloc((unsigned)(n * 3 * sizeof(float)));
m.texcoords = (float*)MemAlloc((unsigned)(n * 2 * sizeof(float)));
m.normals = (float*)MemAlloc((unsigned)(n * 3 * sizeof(float)));
m.colors = (unsigned char*)MemAlloc((unsigned)(n * 4));
for (size_t i = 0; i < n; ++i) {
const hmap::Vertex& v = b.vertices[b.indices[i]];
m.vertices[i * 3 + 0] = v.pos.x; m.vertices[i * 3 + 1] = v.pos.y; m.vertices[i * 3 + 2] = v.pos.z;
m.normals[i * 3 + 0] = v.normal.x; m.normals[i * 3 + 1] = v.normal.y; m.normals[i * 3 + 2] = v.normal.z;
m.texcoords[i * 2 + 0] = v.uv.x; m.texcoords[i * 2 + 1] = v.uv.y;
m.colors[i * 4 + 0] = (unsigned char)(v.color & 0xff);
m.colors[i * 4 + 1] = (unsigned char)((v.color >> 8) & 0xff);
m.colors[i * 4 + 2] = (unsigned char)((v.color >> 16) & 0xff);
m.colors[i * 4 + 3] = (unsigned char)((v.color >> 24) & 0xff);
}
UploadMesh(&m, false);
return m;
}
int main(int argc, char** argv) {
const char* path = argc > 1 ? argv[1] : "demo_runtime.hmap";
hmap::CompiledMap map;
hmap::Status st = hmap::loadCompiled(path, map); // works for source, compiled and mixed files
if (!st) {
std::fprintf(stderr, "cannot load %s: %s\n", path, st.message.c_str());
return 1;
}
InitWindow(1280, 720, "hmap viewer");
SetTargetFPS(120);
std::vector<GpuBatch> gpu;
for (const hmap::Batch& b : map.batches)
if (!b.indices.empty()) gpu.push_back({toRaylib(b), b.material});
Material mat = LoadMaterialDefault();
hmap::Vec3 pos(0, 100, 300);
float yaw = 0, pitch = 0;
for (const hmap::Entity& e : map.entities)
if (e.classname == "info_player_start") { pos = e.origin + hmap::Vec3(0, 40, 0); yaw = e.angles.y; }
while (!WindowShouldClose()) {
float dt = GetFrameTime();
if (IsMouseButtonDown(MOUSE_BUTTON_RIGHT)) {
Vector2 d = GetMouseDelta();
yaw -= d.x * 0.15f;
pitch = std::fmax(-89.0f, std::fmin(89.0f, pitch - d.y * 0.15f));
}
float cy = std::cos(yaw * hmap::kDeg2Rad), sy = std::sin(yaw * hmap::kDeg2Rad);
float cp = std::cos(pitch * hmap::kDeg2Rad), sp = std::sin(pitch * hmap::kDeg2Rad);
hmap::Vec3 fwd(-sy * cp, sp, -cy * cp), right(cy, 0, -sy);
float speed = (IsKeyDown(KEY_LEFT_SHIFT) ? 900.0f : 300.0f) * dt;
hmap::Vec3 move;
if (IsKeyDown(KEY_W)) move += fwd;
if (IsKeyDown(KEY_S)) move -= fwd;
if (IsKeyDown(KEY_D)) move += right;
if (IsKeyDown(KEY_A)) move -= right;
hmap::Vec3 next = pos + hmap::normalize(move) * speed;
// collide against the BSP: stop at walls
if (!map.trace(pos, next + hmap::normalize(move) * 12.0f).hit) pos = next;
hmap::TraceResult aim = map.trace(pos, pos + fwd * 4000.0f); // "what am I looking at?"
Camera3D cam = {};
cam.position = {pos.x, pos.y, pos.z};
cam.target = {pos.x + fwd.x, pos.y + fwd.y, pos.z + fwd.z};
cam.up = {0, 1, 0};
cam.fovy = 70;
cam.projection = CAMERA_PERSPECTIVE;
BeginDrawing();
ClearBackground({24, 26, 32, 255});
BeginMode3D(cam);
for (const GpuBatch& g : gpu) {
const hmap::Material& hm = map.materials[(size_t)g.material];
mat.maps[MATERIAL_MAP_DIFFUSE].color = {(unsigned char)(hm.color.x * 255), (unsigned char)(hm.color.y * 255),
(unsigned char)(hm.color.z * 255), 255};
DrawMesh(g.mesh, mat, MatrixIdentity());
}
for (const hmap::Entity& e : map.entities)
DrawCubeWires({e.origin.x, e.origin.y, e.origin.z}, 12, 12, 12, e.classname.rfind("light", 0) == 0 ? YELLOW : SKYBLUE);
if (aim.hit) DrawSphere({aim.point.x, aim.point.y, aim.point.z}, 3, RED);
EndMode3D();
DrawText(TextFormat("%s | %d batches %u tris | aim: %s | %d fps", path, (int)gpu.size(), map.stats.triangles,
aim.hit ? TextFormat("%.0f units", aim.fraction * 4000.0f) : "nothing", GetFPS()),
10, 10, 18, RAYWHITE);
EndDrawing();
}
for (GpuBatch& g : gpu) UnloadMesh(g.mesh);
MemFree(mat.maps);
CloseWindow();
return 0;
}
+27
View File
@@ -0,0 +1,27 @@
find_package(Threads REQUIRED)
add_library(hmap STATIC
src/bsp.cpp
src/capi.cpp
src/compile.cpp
src/entdefs.cpp
src/fbx.cpp
src/geometry.cpp
src/io.cpp
src/map.cpp
src/sample.cpp)
add_library(hmap::hmap ALIAS hmap)
target_include_directories(hmap PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>)
target_compile_features(hmap PUBLIC cxx_std_17)
target_link_libraries(hmap PUBLIC Threads::Threads)
if(MSVC)
target_compile_options(hmap PRIVATE /W3 /utf-8)
else()
target_compile_options(hmap PRIVATE -Wall -Wextra)
endif()
install(TARGETS hmap EXPORT hmapTargets ARCHIVE DESTINATION lib)
install(DIRECTORY include/ DESTINATION include)
+359
View File
@@ -0,0 +1,359 @@
// hmap/hmap.h - Hammer++ map library. One include gives you everything:
// * the editable map model (brushes = BSP/CSG solids, mesh objects, entities, props)
// * geometry helpers and primitives
// * compile(): CSG + BSP collision tree + render batches (+ optional baked vertex lighting)
// * the .hmap binary format (load/save, source and/or compiled data)
// * FBX (binary 7.4) and OBJ export
//
// Conventions: right-handed, +Y up, forward = -Z. Triangles are CCW seen from the front.
// UVs use image space (v grows downwards). Euler angles are (pitch, yaw, roll) in degrees.
#pragma once
#include "hmath.h"
#include <cstdint>
#include <string>
#include <vector>
namespace hmap {
constexpr uint32_t kFormatVersion = 1;
struct Status {
bool ok = true;
std::string message;
explicit operator bool() const { return ok; }
static Status Ok() { return Status{}; }
static Status Fail(std::string m) { Status s; s.ok = false; s.message = std::move(m); return s; }
};
// ------------------------------------------------------------------ flags
enum Contents : uint32_t {
CONTENTS_SOLID = 1, // blocks movement + light, clips other brushes' hidden faces
CONTENTS_WATER = 2, // volume, rendered, does not block
CONTENTS_CLIP = 4, // invisible collision
CONTENTS_TRIGGER = 8, // invisible, non-blocking volume
CONTENTS_SKY = 16
};
enum ObjectFlags : uint32_t {
OBJ_HIDDEN = 1, // editor visibility only (still compiled)
OBJ_NOCOLLIDE = 2 // meshes: do not collide / cast baked shadows
};
enum FaceFlags : uint32_t { FACE_NODRAW = 1 };
// ------------------------------------------------------------------ model
struct UVInfo { // planar projection along the dominant normal axis
Vec2 offset{0, 0}; // world units
Vec2 scale{1, 1}; // 1 = one texture repeat per Material::tileSize
float rotation = 0; // degrees
};
struct Material {
std::string name; // unique key, e.g. "dev/orange"
std::string texture; // path relative to the asset root, may be empty
Vec2 tileSize{64, 64}; // world units per texture repeat
Vec3 color{1, 1, 1}; // tint
uint32_t flags = 0;
};
struct Face { // one side of a brush
Plane plane;
int32_t material = 0;
UVInfo uv;
uint32_t flags = 0;
};
// A convex solid defined as the intersection of half-spaces (Hammer/Quake style).
struct Brush {
uint32_t id = 0;
uint32_t owner = 0; // 0 = world, otherwise id of the brush entity that owns it
uint32_t contents = CONTENTS_SOLID;
uint32_t flags = 0;
std::vector<Face> faces;
};
struct MeshPoly { // convex polygon of a mesh object
std::vector<uint32_t> idx; // CCW from the front
std::vector<Vec2> uv; // empty = auto-project with `proj`
int32_t material = 0;
uint32_t smooth = 0; // 0 = flat shaded; same non-zero id = smooth together
UVInfo proj;
};
// Free-form polygon mesh ("detail" geometry): arbitrary topology, not part of the BSP tree.
struct MeshObject {
uint32_t id = 0;
uint32_t owner = 0;
uint32_t flags = 0;
std::string name;
std::vector<Vec3> verts;
std::vector<MeshPoly> polys;
};
struct KeyValue { std::string key, value; };
struct Entity { // point entities and props; brush entities own brushes via Brush::owner
uint32_t id = 0;
uint32_t flags = 0;
std::string classname;
Vec3 origin;
Vec3 angles; // pitch, yaw, roll (deg)
Vec3 scale{1, 1, 1};
std::vector<KeyValue> props;
const std::string* find(const std::string& key) const;
std::string get(const std::string& key, const std::string& def = "") const;
float getFloat(const std::string& key, float def) const;
int getInt(const std::string& key, int def) const;
bool getBool(const std::string& key, bool def) const;
Vec3 getVec3(const std::string& key, const Vec3& def) const;
Vec3 getColor(const std::string& key, const Vec3& def) const; // "255 128 0" -> 0..1
void set(const std::string& key, const std::string& value);
void erase(const std::string& key);
Mat34 transform() const; // translate * rotate * scale
Vec3 forward() const;
};
struct Settings {
Vec3 ambient{0.16f, 0.18f, 0.22f};
float gridSize = 16;
float unitsPerMeter = 64;
};
struct Map {
std::string name = "untitled";
Settings settings;
std::vector<Material> materials;
std::vector<Brush> brushes;
std::vector<MeshObject> meshes;
std::vector<Entity> entities;
uint32_t nextId = 1;
uint32_t newId() { return nextId++; }
int findMaterial(const std::string& name) const;
int addMaterial(const std::string& name, const std::string& texture = ""); // dedupes by name
Brush* findBrush(uint32_t id);
MeshObject* findMesh(uint32_t id);
Entity* findEntity(uint32_t id);
const Brush* findBrush(uint32_t id) const;
const MeshObject* findMesh(uint32_t id) const;
const Entity* findEntity(uint32_t id) const;
};
// ------------------------------------------------------------------ geometry
using Polygon = std::vector<Vec3>;
struct BrushGeometry {
std::vector<Polygon> polys; // parallel to Brush::faces; empty polygon = redundant face
AABB bounds;
std::vector<Vec3> points; // all polygon vertices
bool valid = false;
};
BrushGeometry buildBrushGeometry(const Brush& b);
AABB brushBounds(const Brush& b);
AABB meshBounds(const MeshObject& m);
Vec3 polyNormal(const Polygon& p);
// Raycasts (direction need not be normalised; t is in units of d). Return nearest hit.
bool rayBrush(const Brush& b, const Vec3& o, const Vec3& d, float& t, int& face);
bool rayMesh(const MeshObject& m, const Vec3& o, const Vec3& d, float& t, int& poly);
// Texture mapping shared by the editor and the compiler.
void uvAxes(const Vec3& normal, const UVInfo& uv, Vec3& u, Vec3& v);
Vec2 computeUV(const Vec3& p, const Vec3& normal, const UVInfo& uv, const Vec2& tileSize);
// Brush edits
void translateBrush(Brush& b, const Vec3& t, bool lockTexture = true);
void transformBrush(Brush& b, const Mat34& m);
void transformMesh(MeshObject& m, const Mat34& xf);
std::vector<Brush> hollowBrush(const Brush& b, float thickness);
bool clipBrush(const Brush& b, const Plane& p, Brush* front, Brush* back); // false if nothing valid
MeshObject brushToMesh(const Brush& b);
// Mesh edits
void flipMesh(MeshObject& m);
void noiseMesh(MeshObject& m, float amplitude, float frequency, uint32_t seed);
bool extrudePoly(MeshObject& m, int poly, float dist);
Status importOBJ(const std::string& path, Map& map, MeshObject& out);
// Brush primitives (axis-aligned bounds; cylinders/cones are vertical)
Brush makeBox(const Vec3& mn, const Vec3& mx, int material = 0);
Brush makeWedge(const Vec3& mn, const Vec3& mx, int material = 0); // ramp rising towards +X
Brush makeCylinder(const Vec3& mn, const Vec3& mx, int segments, int material = 0);
Brush makeCone(const Vec3& mn, const Vec3& mx, int segments, int material = 0);
std::vector<Brush> makeStairs(const Vec3& mn, const Vec3& mx, int steps, int material = 0); // rises towards +X
// Mesh primitives
MeshObject meshBox(const Vec3& mn, const Vec3& mx, int material = 0);
MeshObject meshCylinder(const Vec3& mn, const Vec3& mx, int segments, int material = 0);
MeshObject meshCone(const Vec3& mn, const Vec3& mx, int segments, int material = 0);
MeshObject meshSphere(const Vec3& mn, const Vec3& mx, int rings, int segments, int material = 0);
MeshObject meshTorus(const Vec3& mn, const Vec3& mx, int segments, int sides, int material = 0);
MeshObject meshGrid(const Vec3& mn, const Vec3& mx, int segX, int segZ, int material = 0); // terrain patch
// Starter content (used by the editor's "New map" and by the sample tools)
void addDefaultMaterials(Map& map); // dev/* materials; texture-less, tinted
Map makeDemoMap(); // lit room with brushes, meshes, entities, a door and a trigger
// ------------------------------------------------------------------ lights
enum class LightType : uint8_t { Point = 0, Spot = 1, Sun = 2 };
struct Light {
LightType type = LightType::Point;
Vec3 pos;
Vec3 dir{0, -1, 0}; // direction the light travels (spot + sun)
Vec3 color{1, 1, 1};
float intensity = 1;
float radius = 512;
float innerDeg = 25, outerDeg = 40;
uint32_t entityId = 0;
};
// light / light_spot / light_environment entities -> Light list
std::vector<Light> collectLights(const Map& map);
// ------------------------------------------------------------------ compiled data
struct Vertex {
Vec3 pos;
Vec3 normal;
Vec2 uv;
uint32_t color = 0xFFFFFFFFu; // 0xAABBGGRR (little-endian RGBA bytes); baked lighting goes here
};
enum class GeomSource : uint8_t { Bsp = 0, Mesh = 1 };
struct Batch { // one draw call: a model + material + source combination
int32_t material = 0;
GeomSource source = GeomSource::Bsp;
int32_t model = 0; // 0 = world, >0 = brush entity sub-model
std::vector<Vertex> vertices;
std::vector<uint32_t> indices; // triangle list
AABB bounds;
};
struct CollisionBrush {
uint32_t contents = 0;
uint32_t sourceId = 0; // Brush::id
std::vector<Plane> planes;
AABB bounds;
};
struct TraceResult {
bool hit = false;
float fraction = 1;
Vec3 point, normal;
uint32_t contents = 0;
uint32_t objectId = 0; // Brush::id or MeshObject::id
bool startSolid = false;
};
// Brush-based BSP tree (Quake 3 style): nodes split on brush-face planes, leaves hold brush lists.
struct BspNode { int32_t plane = 0; int32_t child[2] = {0, 0}; }; // [0]=front [1]=back, <0 => ~leaf
struct BspLeaf { uint32_t firstBrush = 0, brushCount = 0; };
struct Bsp {
std::vector<Plane> planes;
std::vector<BspNode> nodes;
std::vector<BspLeaf> leaves;
std::vector<uint32_t> leafBrushes;
std::vector<CollisionBrush> brushes;
int32_t root = -1; // node index, or ~leaf
AABB bounds;
uint32_t depth = 0;
void build(std::vector<CollisionBrush> brushes);
int findLeaf(const Vec3& p) const;
uint32_t pointContents(const Vec3& p) const;
TraceResult trace(const Vec3& a, const Vec3& b, uint32_t mask = CONTENTS_SOLID) const;
};
struct CompiledModel { // model 0 = world, others = brush entities (doors, triggers...)
uint32_t entityId = 0;
AABB bounds;
std::vector<CollisionBrush> brushes; // sub-models: linear brush list
};
struct CollisionMesh {
uint32_t objectId = 0;
AABB bounds;
std::vector<Vec3> tris; // 3 per triangle
};
struct CompileStats {
uint32_t brushes = 0, meshes = 0, faces = 0, triangles = 0, vertices = 0;
uint32_t bspNodes = 0, bspLeaves = 0, bspDepth = 0, lights = 0;
double ms = 0;
};
struct CompiledMap {
std::vector<Material> materials;
std::vector<Batch> batches;
std::vector<CompiledModel> models;
Bsp bsp; // world solids only
std::vector<CollisionMesh> collMeshes;
std::vector<Light> lights;
std::vector<Entity> entities;
Vec3 ambient{0.2f, 0.2f, 0.2f};
float unitsPerMeter = 64;
AABB bounds;
CompileStats stats;
bool baked = false;
// Nearest hit against the BSP (solid brushes) and solid mesh objects.
TraceResult trace(const Vec3& a, const Vec3& b, uint32_t mask = CONTENTS_SOLID) const;
uint32_t pointContents(const Vec3& p) const { return bsp.pointContents(p); }
};
struct CompileOptions {
bool csg = true; // remove brush faces hidden inside other solids
bool bsp = true; // build collision BSP
bool bakeLighting = false; // tessellate + bake vertex colours
float lightEdge = 32; // max triangle edge while baking (smaller = finer light)
bool shadows = true;
int threads = 0; // 0 = hardware concurrency
};
CompiledMap compile(const Map& map, const CompileOptions& opt = {});
// Raw per-object render batches without CSG (fast; used by the editor viewport).
void buildEditBatches(const Map& map, std::vector<Batch>& out);
// Bakes per-vertex lighting (ambient + point/spot/sun with BSP-traced shadows).
void bakeLighting(CompiledMap& cm, float maxEdge = 32, bool shadows = true, int threads = 0);
// ------------------------------------------------------------------ file format (.hmap)
// Chunked little-endian binary. Chunks: META MATS BRSH MESH ENTS CMPL. Unknown chunks are skipped.
Status saveMap(const std::string& path, const Map& map, const CompiledMap* compiled = nullptr);
Status loadMap(const std::string& path, Map& out, CompiledMap* compiled = nullptr);
// Runtime-oriented: writes only materials + entities + compiled geometry (no editable source).
Status saveCompiled(const std::string& path, const CompiledMap& cm);
// Loads compiled data; if the file only has source chunks it compiles on the fly.
Status loadCompiled(const std::string& path, CompiledMap& out);
std::vector<uint8_t> saveMapToMemory(const Map& map, const CompiledMap* compiled = nullptr);
Status loadMapFromMemory(const uint8_t* data, size_t size, Map& out, CompiledMap* compiled = nullptr);
// ------------------------------------------------------------------ export
struct FbxOptions {
float scale = 1.0f; // multiplies positions
bool entities = true; // export entities/props as Null nodes with custom properties
bool vertexColors = true; // write baked lighting as a colour layer
bool includeSubModels = true;
};
std::vector<uint8_t> exportFBXToMemory(const CompiledMap& cm, const FbxOptions& opt = {});
Status exportFBX(const std::string& path, const CompiledMap& cm, const FbxOptions& opt = {});
Status exportOBJ(const std::string& path, const CompiledMap& cm, float scale = 1.0f);
// ------------------------------------------------------------------ entity definitions
struct PropDef {
std::string key, type, label, def, choices; // type: string int float bool color255 vec3 choice model material target
};
struct EntityClass {
std::string name, label;
bool brush = false; // brush entity (func_*, trigger_*)
Vec3 color{1, 1, 1}; // editor colour 0..1
Vec3 half{8, 8, 8}; // editor box half extents
bool directional = false; // shows an angles arrow
bool hasModel = false; // displays the "model" key
uint32_t brushContents = CONTENTS_SOLID;
std::vector<PropDef> props;
};
class EntityRegistry {
public:
std::vector<EntityClass> classes;
const EntityClass* find(const std::string& name) const;
// Mini-FGD text format, see assets/entities.def. Later definitions override earlier ones.
Status parse(const std::string& text);
void applyDefaults(Entity& e) const; // fills missing keys with class defaults
static EntityRegistry builtin();
};
} // namespace hmap
+84
View File
@@ -0,0 +1,84 @@
/* hmap/hmap_c.h - plain C API over libhmap for engines, scripting languages and FFI.
*
* hmap_map* m = hmap_load("level.hmap", err, sizeof err);
* for (uint32_t i = 0; i < hmap_batch_count(m); ++i) {
* hmap_batch b; hmap_get_batch(m, i, &b);
* // upload b.vertices / b.indices, bind texture hmap_material_texture(m, b.material)
* }
* hmap_trace t; hmap_trace_segment(m, from, to, HMAP_CONTENTS_SOLID, &t);
* hmap_free(m);
*
* Units: map units, +Y up, forward -Z. Pointers stay valid until hmap_free().
*/
#ifndef HMAP_C_H
#define HMAP_C_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define HMAP_CONTENTS_SOLID 1u
#define HMAP_CONTENTS_WATER 2u
#define HMAP_CONTENTS_CLIP 4u
#define HMAP_CONTENTS_TRIGGER 8u
#define HMAP_CONTENTS_SKY 16u
typedef struct hmap_map hmap_map;
typedef struct {
float pos[3];
float normal[3];
float uv[2];
uint32_t color; /* 0xAABBGGRR, baked vertex light */
} hmap_vertex;
typedef struct {
int32_t material;
int32_t model; /* 0 = world, >0 = brush-entity sub-model */
int32_t is_mesh; /* 0 = BSP/brush geometry, 1 = mesh object */
uint32_t vertex_count;
const hmap_vertex* vertices;
uint32_t index_count; /* triangle list */
const uint32_t* indices;
} hmap_batch;
typedef struct {
int hit;
float fraction;
float point[3];
float normal[3];
uint32_t contents;
uint32_t object_id;
int start_solid;
} hmap_trace;
/* Loads a .hmap file (compiled data is used when present, otherwise the source is compiled). */
hmap_map* hmap_load(const char* path, char* err, size_t err_len);
void hmap_free(hmap_map* m);
uint32_t hmap_batch_count(const hmap_map* m);
int hmap_get_batch(const hmap_map* m, uint32_t index, hmap_batch* out);
uint32_t hmap_material_count(const hmap_map* m);
const char* hmap_material_name(const hmap_map* m, uint32_t index);
const char* hmap_material_texture(const hmap_map* m, uint32_t index);
uint32_t hmap_entity_count(const hmap_map* m);
const char* hmap_entity_classname(const hmap_map* m, uint32_t index);
void hmap_entity_origin(const hmap_map* m, uint32_t index, float out[3]);
void hmap_entity_angles(const hmap_map* m, uint32_t index, float out[3]); /* pitch, yaw, roll degrees */
const char* hmap_entity_get(const hmap_map* m, uint32_t index, const char* key); /* NULL if missing */
uint32_t hmap_point_contents(const hmap_map* m, const float p[3]);
int hmap_trace_segment(const hmap_map* m, const float from[3], const float to[3], uint32_t mask, hmap_trace* out);
int hmap_export_fbx(const hmap_map* m, const char* path); /* 1 on success */
#ifdef __cplusplus
}
#endif
#endif /* HMAP_C_H */
+226
View File
@@ -0,0 +1,226 @@
// hmap/hmath.h - tiny dependency-free math types used by libhmap.
// Conventions: right-handed, +Y up, forward = -Z, column vectors (p' = M * p + t).
#pragma once
#include <algorithm>
#include <cmath>
#include <cstdint>
namespace hmap {
constexpr float kPi = 3.14159265358979323846f;
constexpr float kDeg2Rad = kPi / 180.0f;
constexpr float kRad2Deg = 180.0f / kPi;
constexpr float kPlaneEps = 0.01f; // world-unit tolerance used by plane classification
struct Vec2 {
float x = 0, y = 0;
constexpr Vec2() = default;
constexpr Vec2(float x_, float y_) : x(x_), y(y_) {}
Vec2 operator+(const Vec2& o) const { return {x + o.x, y + o.y}; }
Vec2 operator-(const Vec2& o) const { return {x - o.x, y - o.y}; }
Vec2 operator*(float s) const { return {x * s, y * s}; }
};
struct Vec3 {
float x = 0, y = 0, z = 0;
constexpr Vec3() = default;
constexpr Vec3(float x_, float y_, float z_) : x(x_), y(y_), z(z_) {}
Vec3 operator+(const Vec3& o) const { return {x + o.x, y + o.y, z + o.z}; }
Vec3 operator-(const Vec3& o) const { return {x - o.x, y - o.y, z - o.z}; }
Vec3 operator*(float s) const { return {x * s, y * s, z * s}; }
Vec3 operator/(float s) const { return {x / s, y / s, z / s}; }
Vec3 operator-() const { return {-x, -y, -z}; }
Vec3& operator+=(const Vec3& o) { x += o.x; y += o.y; z += o.z; return *this; }
Vec3& operator-=(const Vec3& o) { x -= o.x; y -= o.y; z -= o.z; return *this; }
Vec3& operator*=(float s) { x *= s; y *= s; z *= s; return *this; }
float operator[](int i) const { return i == 0 ? x : (i == 1 ? y : z); }
float& operator[](int i) { return i == 0 ? x : (i == 1 ? y : z); }
bool operator==(const Vec3& o) const { return x == o.x && y == o.y && z == o.z; }
bool operator!=(const Vec3& o) const { return !(*this == o); }
};
inline Vec3 operator*(float s, const Vec3& v) { return v * s; }
inline float dot(const Vec3& a, const Vec3& b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
inline Vec3 cross(const Vec3& a, const Vec3& b) {
return {a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x};
}
inline float lengthSq(const Vec3& v) { return dot(v, v); }
inline float length(const Vec3& v) { return std::sqrt(dot(v, v)); }
inline Vec3 normalize(const Vec3& v) {
float l = length(v);
return l > 1e-12f ? v / l : Vec3(0, 0, 0);
}
inline Vec3 vmin(const Vec3& a, const Vec3& b) { return {std::min(a.x, b.x), std::min(a.y, b.y), std::min(a.z, b.z)}; }
inline Vec3 vmax(const Vec3& a, const Vec3& b) { return {std::max(a.x, b.x), std::max(a.y, b.y), std::max(a.z, b.z)}; }
inline Vec3 lerp(const Vec3& a, const Vec3& b, float t) { return a + (b - a) * t; }
inline float clampf(float v, float lo, float hi) { return v < lo ? lo : (v > hi ? hi : v); }
// Plane: dot(n, p) == d. Solids are on the BACK side (dot(n,p) < d); n points out of the solid.
struct Plane {
Vec3 n{0, 1, 0};
float d = 0;
float dist(const Vec3& p) const { return dot(n, p) - d; }
static Plane fromPoints(const Vec3& a, const Vec3& b, const Vec3& c) {
Vec3 n = normalize(cross(b - a, c - a));
return Plane{n, dot(n, a)};
}
static Plane fromNormalPoint(const Vec3& n, const Vec3& p) {
Vec3 nn = normalize(n);
return Plane{nn, dot(nn, p)};
}
};
struct AABB {
Vec3 mn{1e30f, 1e30f, 1e30f}, mx{-1e30f, -1e30f, -1e30f};
bool empty() const { return mn.x > mx.x; }
void add(const Vec3& p) { mn = vmin(mn, p); mx = vmax(mx, p); }
void add(const AABB& b) { if (!b.empty()) { add(b.mn); add(b.mx); } }
Vec3 center() const { return (mn + mx) * 0.5f; }
Vec3 size() const { return mx - mn; }
bool overlaps(const AABB& o, float eps = 0) const {
return !(o.mn.x > mx.x + eps || o.mx.x < mn.x - eps || o.mn.y > mx.y + eps ||
o.mx.y < mn.y - eps || o.mn.z > mx.z + eps || o.mx.z < mn.z - eps);
}
bool contains(const Vec3& p) const {
return p.x >= mn.x && p.x <= mx.x && p.y >= mn.y && p.y <= mx.y && p.z >= mn.z && p.z <= mx.z;
}
AABB expanded(float e) const { AABB r = *this; r.mn = mn - Vec3(e, e, e); r.mx = mx + Vec3(e, e, e); return r; }
};
// Slab test. Returns true when the ray hits; tNear may be negative when the origin is inside.
inline bool rayAABB(const Vec3& o, const Vec3& d, const AABB& b, float& tNear, float& tFar) {
float t0 = -1e30f, t1 = 1e30f;
for (int i = 0; i < 3; ++i) {
if (std::fabs(d[i]) < 1e-9f) {
if (o[i] < b.mn[i] || o[i] > b.mx[i]) return false;
} else {
float inv = 1.0f / d[i];
float a = (b.mn[i] - o[i]) * inv, c = (b.mx[i] - o[i]) * inv;
if (a > c) std::swap(a, c);
t0 = std::max(t0, a);
t1 = std::min(t1, c);
if (t0 > t1) return false;
}
}
tNear = t0;
tFar = t1;
return t1 >= 0;
}
// 3x4 affine matrix.
struct Mat34 {
float m[3][4] = {{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}};
static Mat34 identity() { return Mat34(); }
static Mat34 translate(const Vec3& t) {
Mat34 r;
r.m[0][3] = t.x; r.m[1][3] = t.y; r.m[2][3] = t.z;
return r;
}
static Mat34 scale(const Vec3& s) {
Mat34 r;
r.m[0][0] = s.x; r.m[1][1] = s.y; r.m[2][2] = s.z;
return r;
}
static Mat34 rotate(const Vec3& axis, float deg) { // Rodrigues, right-handed
Vec3 a = normalize(axis);
float c = std::cos(deg * kDeg2Rad), s = std::sin(deg * kDeg2Rad), t = 1 - c;
Mat34 r;
r.m[0][0] = t * a.x * a.x + c; r.m[0][1] = t * a.x * a.y - s * a.z; r.m[0][2] = t * a.x * a.z + s * a.y;
r.m[1][0] = t * a.x * a.y + s * a.z; r.m[1][1] = t * a.y * a.y + c; r.m[1][2] = t * a.y * a.z - s * a.x;
r.m[2][0] = t * a.x * a.z - s * a.y; r.m[2][1] = t * a.y * a.z + s * a.x; r.m[2][2] = t * a.z * a.z + c;
return r;
}
// angles = (pitch, yaw, roll) degrees. R = Ry(yaw) * Rx(pitch) * Rz(roll). Forward (-Z) with
// yaw 0 / pitch 0 stays -Z; +pitch looks up; +yaw turns left (counter-clockwise from above).
static Mat34 fromEuler(const Vec3& a) {
return rotate({0, 1, 0}, a.y) * rotate({1, 0, 0}, a.x) * rotate({0, 0, 1}, a.z);
}
Vec3 toEuler() const { // inverse of fromEuler
float sp = -m[1][2];
float pitch = std::asin(clampf(sp, -1.0f, 1.0f));
float yaw, roll;
if (std::fabs(sp) < 0.99999f) {
yaw = std::atan2(m[0][2], m[2][2]);
roll = std::atan2(m[1][0], m[1][1]);
} else {
yaw = std::atan2(-m[2][0], m[0][0]);
roll = 0;
}
return {pitch * kRad2Deg, yaw * kRad2Deg, roll * kRad2Deg};
}
// Euler angles for the FBX default XYZ order (R = Rz * Ry * Rx), degrees.
Vec3 toEulerXYZ() const {
float sb = -m[2][0];
float b = std::asin(clampf(sb, -1.0f, 1.0f));
float a, c;
if (std::fabs(sb) < 0.99999f) {
a = std::atan2(m[2][1], m[2][2]);
c = std::atan2(m[1][0], m[0][0]);
} else {
a = std::atan2(-m[1][2], m[1][1]);
c = 0;
}
return {a * kRad2Deg, b * kRad2Deg, c * kRad2Deg};
}
Vec3 point(const Vec3& p) const {
return {m[0][0] * p.x + m[0][1] * p.y + m[0][2] * p.z + m[0][3],
m[1][0] * p.x + m[1][1] * p.y + m[1][2] * p.z + m[1][3],
m[2][0] * p.x + m[2][1] * p.y + m[2][2] * p.z + m[2][3]};
}
Vec3 vector(const Vec3& v) const {
return {m[0][0] * v.x + m[0][1] * v.y + m[0][2] * v.z,
m[1][0] * v.x + m[1][1] * v.y + m[1][2] * v.z,
m[2][0] * v.x + m[2][1] * v.y + m[2][2] * v.z};
}
Vec3 translation() const { return {m[0][3], m[1][3], m[2][3]}; }
// this * b : applies b first, then this.
Mat34 operator*(const Mat34& b) const {
Mat34 r;
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j)
r.m[i][j] = m[i][0] * b.m[0][j] + m[i][1] * b.m[1][j] + m[i][2] * b.m[2][j];
r.m[i][3] = m[i][0] * b.m[0][3] + m[i][1] * b.m[1][3] + m[i][2] * b.m[2][3] + m[i][3];
}
return r;
}
float det3() const {
return m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) -
m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0]) +
m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
}
bool inverse(Mat34& out) const {
float det = det3();
if (std::fabs(det) < 1e-12f) return false;
float id = 1.0f / det;
Mat34 r;
r.m[0][0] = (m[1][1] * m[2][2] - m[1][2] * m[2][1]) * id;
r.m[0][1] = (m[0][2] * m[2][1] - m[0][1] * m[2][2]) * id;
r.m[0][2] = (m[0][1] * m[1][2] - m[0][2] * m[1][1]) * id;
r.m[1][0] = (m[1][2] * m[2][0] - m[1][0] * m[2][2]) * id;
r.m[1][1] = (m[0][0] * m[2][2] - m[0][2] * m[2][0]) * id;
r.m[1][2] = (m[0][2] * m[1][0] - m[0][0] * m[1][2]) * id;
r.m[2][0] = (m[1][0] * m[2][1] - m[1][1] * m[2][0]) * id;
r.m[2][1] = (m[0][1] * m[2][0] - m[0][0] * m[2][1]) * id;
r.m[2][2] = (m[0][0] * m[1][1] - m[0][1] * m[1][0]) * id;
Vec3 t = translation();
Vec3 it = r.vector(t);
r.m[0][3] = -it.x; r.m[1][3] = -it.y; r.m[2][3] = -it.z;
out = r;
return true;
}
// Transforms a plane by this affine map (normal uses the inverse transpose).
Plane transformPlane(const Plane& pl) const {
Mat34 inv;
if (!inverse(inv)) return pl;
Vec3 n(inv.m[0][0] * pl.n.x + inv.m[1][0] * pl.n.y + inv.m[2][0] * pl.n.z,
inv.m[0][1] * pl.n.x + inv.m[1][1] * pl.n.y + inv.m[2][1] * pl.n.z,
inv.m[0][2] * pl.n.x + inv.m[1][2] * pl.n.y + inv.m[2][2] * pl.n.z);
float d = pl.d + dot(n, translation());
float l = length(n);
if (l < 1e-12f) return pl;
return Plane{n / l, d / l};
}
};
} // namespace hmap
+301
View File
@@ -0,0 +1,301 @@
#include "hmap/hmap.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <map>
namespace hmap {
namespace {
// Planes are stored once, in a canonical orientation (largest normal component positive).
struct CanonTable {
std::vector<Plane> planes;
std::map<std::array<long long, 4>, int> lookup;
int get(Plane p) {
int ax = 0;
if (std::fabs(p.n.y) > std::fabs(p.n[ax])) ax = 1;
if (std::fabs(p.n.z) > std::fabs(p.n[ax])) ax = 2;
if (p.n[ax] < 0) { p.n = -p.n; p.d = -p.d; }
std::array<long long, 4> key{std::llround(p.n.x * 1e4), std::llround(p.n.y * 1e4), std::llround(p.n.z * 1e4),
std::llround(p.d * 100.0)};
auto it = lookup.find(key);
if (it != lookup.end()) return it->second;
int id = (int)planes.size();
planes.push_back(p);
lookup[key] = id;
return id;
}
};
constexpr size_t kMaxLeafBrushes = 3;
constexpr uint32_t kMaxDepth = 64;
enum Side { FRONT = 1, BACK = 2, BOTH = 3 };
struct Builder {
Bsp& bsp;
CanonTable table;
std::vector<std::vector<int>> faceCanon;
std::vector<std::vector<Vec3>> pts;
std::vector<uint8_t> used;
uint32_t maxDepth = 0;
explicit Builder(Bsp& b) : bsp(b) {}
int classify(uint32_t b, const Plane& pl) const {
const AABB& bb = bsp.brushes[b].bounds;
Vec3 c = bb.center(), e = (bb.mx - bb.mn) * 0.5f;
float r = e.x * std::fabs(pl.n.x) + e.y * std::fabs(pl.n.y) + e.z * std::fabs(pl.n.z);
float dc = pl.dist(c);
if (dc - r > kPlaneEps) return FRONT;
if (dc + r < -kPlaneEps) return BACK;
float mn = 1e30f, mx = -1e30f;
for (const Vec3& p : pts[b]) {
float d = pl.dist(p);
mn = std::min(mn, d);
mx = std::max(mx, d);
}
if (mn >= -kPlaneEps) return FRONT;
if (mx <= kPlaneEps) return BACK;
return BOTH;
}
int32_t makeLeaf(const std::vector<uint32_t>& list) {
BspLeaf lf;
lf.firstBrush = (uint32_t)bsp.leafBrushes.size();
lf.brushCount = (uint32_t)list.size();
bsp.leafBrushes.insert(bsp.leafBrushes.end(), list.begin(), list.end());
bsp.leaves.push_back(lf);
return ~(int32_t)(bsp.leaves.size() - 1);
}
int32_t build(const std::vector<uint32_t>& list, uint32_t depth) {
maxDepth = std::max(maxDepth, depth);
if (list.size() <= kMaxLeafBrushes || depth >= kMaxDepth) return makeLeaf(list);
std::vector<int> cands;
std::vector<uint8_t> seen(table.planes.size(), 0);
for (uint32_t b : list)
for (int c : faceCanon[b])
if (!used[(size_t)c] && !seen[(size_t)c]) { seen[(size_t)c] = 1; cands.push_back(c); }
if (cands.empty()) return makeLeaf(list);
size_t stride = std::max<size_t>(1, cands.size() / 48);
int best = -1;
float bestScore = 1e30f;
for (size_t ci = 0; ci < cands.size(); ci += stride) {
const Plane& pl = table.planes[(size_t)cands[ci]];
int nf = 0, nb = 0, nboth = 0;
for (uint32_t b : list) {
int s = classify(b, pl);
nf += s == FRONT;
nb += s == BACK;
nboth += s == BOTH;
}
bool useful = (nf >= 1 && nb >= 1) || (nboth >= 1 && nf + nb >= 1);
if (!useful) continue;
float score = (float)nboth * 4.0f + (float)std::abs(nf - nb);
if (score < bestScore) { bestScore = score; best = cands[ci]; }
}
if (best < 0) return makeLeaf(list);
const Plane& pl = table.planes[(size_t)best];
std::vector<uint32_t> fl, bl;
for (uint32_t b : list) {
int s = classify(b, pl);
if (s & FRONT) fl.push_back(b);
if (s & BACK) bl.push_back(b);
}
size_t node = bsp.nodes.size();
bsp.nodes.emplace_back();
bsp.nodes[node].plane = best;
used[(size_t)best] = 1;
int32_t c0 = build(fl, depth + 1);
int32_t c1 = build(bl, depth + 1);
used[(size_t)best] = 0;
bsp.nodes[node].child[0] = c0;
bsp.nodes[node].child[1] = c1;
return (int32_t)node;
}
};
} // namespace
void Bsp::build(std::vector<CollisionBrush> in) {
planes.clear(); nodes.clear(); leaves.clear(); leafBrushes.clear();
brushes = std::move(in);
bounds = AABB();
depth = 0;
if (brushes.empty()) {
leaves.push_back(BspLeaf{});
root = ~0;
return;
}
Builder b(*this);
b.faceCanon.resize(brushes.size());
b.pts.resize(brushes.size());
for (size_t i = 0; i < brushes.size(); ++i) {
Brush tmp;
for (const Plane& p : brushes[i].planes) {
Face f;
f.plane = p;
tmp.faces.push_back(f);
b.faceCanon[i].push_back(b.table.get(p));
}
BrushGeometry g = buildBrushGeometry(tmp);
b.pts[i] = g.points;
if (brushes[i].bounds.empty()) brushes[i].bounds = g.bounds;
bounds.add(brushes[i].bounds);
}
b.used.assign(b.table.planes.size(), 0);
std::vector<uint32_t> all(brushes.size());
for (size_t i = 0; i < all.size(); ++i) all[i] = (uint32_t)i;
root = b.build(all, 0);
planes = b.table.planes;
depth = b.maxDepth;
}
int Bsp::findLeaf(const Vec3& p) const {
int32_t n = root;
while (n >= 0) {
const BspNode& nd = nodes[(size_t)n];
n = planes[(size_t)nd.plane].dist(p) >= 0 ? nd.child[0] : nd.child[1];
}
return ~n;
}
uint32_t Bsp::pointContents(const Vec3& p) const {
int leaf = findLeaf(p);
if (leaf < 0 || (size_t)leaf >= leaves.size()) return 0;
const BspLeaf& lf = leaves[(size_t)leaf];
uint32_t c = 0;
for (uint32_t i = 0; i < lf.brushCount; ++i) {
const CollisionBrush& br = brushes[leafBrushes[lf.firstBrush + i]];
bool inside = true;
for (const Plane& pl : br.planes)
if (pl.dist(p) > 0) { inside = false; break; }
if (inside) c |= br.contents;
}
return c;
}
namespace {
struct TraceCtx {
const Bsp* bsp;
Vec3 a, b;
uint32_t mask;
TraceResult res;
};
bool segBrush(const CollisionBrush& br, const Vec3& a, const Vec3& b, float& frac, Vec3& nrm, bool& startSolid) {
float tn = -1.0f, tf = 1.0f;
const Plane* ep = nullptr;
bool startOut = false;
for (const Plane& pl : br.planes) {
float d1 = pl.dist(a), d2 = pl.dist(b);
if (d1 > 0) startOut = true;
if (d1 > 0 && d2 >= d1) return false;
if (d1 <= 0 && d2 <= 0) continue;
float f = d1 / (d1 - d2);
if (d1 > d2) { if (f > tn) { tn = f; ep = &pl; } }
else if (f < tf) tf = f;
}
if (!startOut) { startSolid = true; frac = 0; nrm = Vec3(); return true; }
if (ep && tn <= tf && tn >= 0) { frac = tn; nrm = ep->n; return true; }
return false;
}
void traceNode(TraceCtx& c, int32_t child, const Vec3& p1, const Vec3& p2, float f1, float f2) {
if (f1 >= c.res.fraction) return;
if (child < 0) {
const BspLeaf& lf = c.bsp->leaves[(size_t)(~child)];
for (uint32_t i = 0; i < lf.brushCount; ++i) {
const CollisionBrush& br = c.bsp->brushes[c.bsp->leafBrushes[lf.firstBrush + i]];
if (!(br.contents & c.mask)) continue;
float fr = 0;
Vec3 n;
bool ss = false;
if (segBrush(br, c.a, c.b, fr, n, ss) && fr < c.res.fraction) {
c.res.hit = true;
c.res.fraction = fr;
c.res.normal = n;
c.res.contents = br.contents;
c.res.objectId = br.sourceId;
c.res.startSolid = ss;
}
}
return;
}
const BspNode& nd = c.bsp->nodes[(size_t)child];
const Plane& pl = c.bsp->planes[(size_t)nd.plane];
float d1 = pl.dist(p1), d2 = pl.dist(p2);
const float eps = kPlaneEps;
if (d1 > eps && d2 > eps) { traceNode(c, nd.child[0], p1, p2, f1, f2); return; }
if (d1 < -eps && d2 < -eps) { traceNode(c, nd.child[1], p1, p2, f1, f2); return; }
if (std::fabs(d1) <= eps && std::fabs(d2) <= eps) {
traceNode(c, nd.child[0], p1, p2, f1, f2);
traceNode(c, nd.child[1], p1, p2, f1, f2);
return;
}
int side = d1 < 0 ? 1 : 0;
float t = clampf(d1 / (d1 - d2), 0.0f, 1.0f);
Vec3 mid = p1 + (p2 - p1) * t;
float fm = f1 + (f2 - f1) * t;
traceNode(c, nd.child[side], p1, mid, f1, fm);
traceNode(c, nd.child[side ^ 1], mid, p2, fm, f2);
}
bool segTri(const Vec3& o, const Vec3& d, const Vec3& a, const Vec3& b, const Vec3& c, float& t) {
Vec3 e1 = b - a, e2 = c - a, p = cross(d, e2);
float det = dot(e1, p);
if (std::fabs(det) < 1e-9f) return false;
float inv = 1.0f / det;
Vec3 s = o - a;
float u = dot(s, p) * inv;
if (u < 0 || u > 1) return false;
Vec3 q = cross(s, e1);
float v = dot(d, q) * inv;
if (v < 0 || u + v > 1) return false;
float tt = dot(e2, q) * inv;
if (tt < 0 || tt > 1) return false;
t = tt;
return true;
}
} // namespace
TraceResult Bsp::trace(const Vec3& a, const Vec3& b, uint32_t mask) const {
TraceCtx c{this, a, b, mask, TraceResult()};
traceNode(c, root, a, b, 0.0f, 1.0f);
if (c.res.hit) c.res.point = a + (b - a) * c.res.fraction;
return c.res;
}
TraceResult CompiledMap::trace(const Vec3& a, const Vec3& b, uint32_t mask) const {
TraceResult best = bsp.trace(a, b, mask);
if (!(mask & CONTENTS_SOLID)) return best;
Vec3 d = b - a;
for (const CollisionMesh& cmesh : collMeshes) {
float tn, tf;
if (!rayAABB(a, d, cmesh.bounds.expanded(0.01f), tn, tf) || tn > 1.0f || tf < 0.0f) continue;
for (size_t i = 0; i + 2 < cmesh.tris.size(); i += 3) {
float t;
if (segTri(a, d, cmesh.tris[i], cmesh.tris[i + 1], cmesh.tris[i + 2], t) && t < best.fraction) {
Vec3 n = normalize(cross(cmesh.tris[i + 1] - cmesh.tris[i], cmesh.tris[i + 2] - cmesh.tris[i]));
if (dot(n, d) > 0) n = -n;
best.hit = true;
best.fraction = t;
best.normal = n;
best.point = a + d * t;
best.contents = CONTENTS_SOLID;
best.objectId = cmesh.objectId;
best.startSolid = false;
}
}
}
return best;
}
} // namespace hmap
+95
View File
@@ -0,0 +1,95 @@
#include "hmap/hmap_c.h"
#include "hmap/hmap.h"
#include <cstdio>
#include <cstring>
struct hmap_map {
hmap::CompiledMap cm;
};
static_assert(sizeof(hmap::Vertex) == sizeof(hmap_vertex), "hmap_vertex must mirror hmap::Vertex");
static_assert(sizeof(hmap::Vec3) == 3 * sizeof(float), "Vec3 must be tightly packed");
extern "C" {
hmap_map* hmap_load(const char* path, char* err, size_t err_len) {
auto* m = new hmap_map();
hmap::Status s = hmap::loadCompiled(path ? path : "", m->cm);
if (!s) {
if (err && err_len) std::snprintf(err, err_len, "%s", s.message.c_str());
delete m;
return nullptr;
}
return m;
}
void hmap_free(hmap_map* m) { delete m; }
uint32_t hmap_batch_count(const hmap_map* m) { return m ? (uint32_t)m->cm.batches.size() : 0; }
int hmap_get_batch(const hmap_map* m, uint32_t i, hmap_batch* out) {
if (!m || !out || i >= m->cm.batches.size()) return 0;
const hmap::Batch& b = m->cm.batches[i];
out->material = b.material;
out->model = b.model;
out->is_mesh = b.source == hmap::GeomSource::Mesh;
out->vertex_count = (uint32_t)b.vertices.size();
out->vertices = reinterpret_cast<const hmap_vertex*>(b.vertices.data());
out->index_count = (uint32_t)b.indices.size();
out->indices = b.indices.data();
return 1;
}
uint32_t hmap_material_count(const hmap_map* m) { return m ? (uint32_t)m->cm.materials.size() : 0; }
const char* hmap_material_name(const hmap_map* m, uint32_t i) {
return (m && i < m->cm.materials.size()) ? m->cm.materials[i].name.c_str() : nullptr;
}
const char* hmap_material_texture(const hmap_map* m, uint32_t i) {
return (m && i < m->cm.materials.size()) ? m->cm.materials[i].texture.c_str() : nullptr;
}
uint32_t hmap_entity_count(const hmap_map* m) { return m ? (uint32_t)m->cm.entities.size() : 0; }
const char* hmap_entity_classname(const hmap_map* m, uint32_t i) {
return (m && i < m->cm.entities.size()) ? m->cm.entities[i].classname.c_str() : nullptr;
}
void hmap_entity_origin(const hmap_map* m, uint32_t i, float out[3]) {
if (!m || !out || i >= m->cm.entities.size()) return;
const hmap::Vec3& o = m->cm.entities[i].origin;
out[0] = o.x; out[1] = o.y; out[2] = o.z;
}
void hmap_entity_angles(const hmap_map* m, uint32_t i, float out[3]) {
if (!m || !out || i >= m->cm.entities.size()) return;
const hmap::Vec3& a = m->cm.entities[i].angles;
out[0] = a.x; out[1] = a.y; out[2] = a.z;
}
const char* hmap_entity_get(const hmap_map* m, uint32_t i, const char* key) {
if (!m || !key || i >= m->cm.entities.size()) return nullptr;
const std::string* v = m->cm.entities[i].find(key);
return v ? v->c_str() : nullptr;
}
uint32_t hmap_point_contents(const hmap_map* m, const float p[3]) {
if (!m || !p) return 0;
return m->cm.pointContents(hmap::Vec3(p[0], p[1], p[2]));
}
int hmap_trace_segment(const hmap_map* m, const float a[3], const float b[3], uint32_t mask, hmap_trace* out) {
if (!m || !a || !b || !out) return 0;
hmap::TraceResult r = m->cm.trace(hmap::Vec3(a[0], a[1], a[2]), hmap::Vec3(b[0], b[1], b[2]), mask);
out->hit = r.hit;
out->fraction = r.fraction;
out->point[0] = r.point.x; out->point[1] = r.point.y; out->point[2] = r.point.z;
out->normal[0] = r.normal.x; out->normal[1] = r.normal.y; out->normal[2] = r.normal.z;
out->contents = r.contents;
out->object_id = r.objectId;
out->start_solid = r.startSolid;
return r.hit;
}
int hmap_export_fbx(const hmap_map* m, const char* path) {
if (!m || !path) return 0;
return hmap::exportFBX(path, m->cm).ok ? 1 : 0;
}
} // extern "C"
+500
View File
@@ -0,0 +1,500 @@
#include "hmap/hmap.h"
#include <atomic>
#include <chrono>
#include <cmath>
#include <cstring>
#include <map>
#include <numeric>
#include <thread>
#include <tuple>
#include <unordered_map>
namespace hmap {
namespace {
// ---------------------------------------------------------------- batches
struct BatchKey {
int32_t model, material;
int source;
bool operator<(const BatchKey& o) const { return std::tie(model, material, source) < std::tie(o.model, o.material, o.source); }
};
struct BatchSet {
std::vector<Batch>& out;
std::map<BatchKey, size_t> index;
explicit BatchSet(std::vector<Batch>& o) : out(o) {}
Batch& get(int32_t model, int32_t material, GeomSource src) {
BatchKey k{model, material, (int)src};
auto it = index.find(k);
if (it == index.end()) {
Batch b;
b.model = model;
b.material = material;
b.source = src;
out.push_back(std::move(b));
it = index.emplace(k, out.size() - 1).first;
}
return out[it->second];
}
};
const Material& matOf(const std::vector<Material>& mats, int32_t i) {
static const Material def;
return (i >= 0 && (size_t)i < mats.size()) ? mats[(size_t)i] : def;
}
void emitPolygon(Batch& b, const Polygon& poly, const Vec3& n, const UVInfo& uv, const Material& m) {
uint32_t base = (uint32_t)b.vertices.size();
for (const Vec3& p : poly) {
Vertex v;
v.pos = p;
v.normal = n;
v.uv = computeUV(p, n, uv, m.tileSize);
b.vertices.push_back(v);
b.bounds.add(p);
}
for (uint32_t k = 1; k + 1 < (uint32_t)poly.size(); ++k) {
b.indices.push_back(base);
b.indices.push_back(base + k);
b.indices.push_back(base + k + 1);
}
}
Vec3 newell(const Polygon& p) {
Vec3 n;
for (size_t i = 0; i < p.size(); ++i) {
const Vec3& a = p[i];
const Vec3& b = p[(i + 1) % p.size()];
n.x += (a.y - b.y) * (a.z + b.z);
n.y += (a.z - b.z) * (a.x + b.x);
n.z += (a.x - b.x) * (a.y + b.y);
}
return n;
}
// Emits a mesh object; returns the number of triangles. Optionally records collision triangles.
void emitMesh(BatchSet& bs, const MeshObject& mo, int32_t model, const std::vector<Material>& mats, CollisionMesh* coll) {
size_t np = mo.polys.size();
std::vector<Vec3> raw(np);
std::vector<uint8_t> okPoly(np, 0);
std::vector<std::vector<size_t>> vertPolys(mo.verts.size());
for (size_t i = 0; i < np; ++i) {
const MeshPoly& p = mo.polys[i];
if (p.idx.size() < 3) continue;
Polygon poly;
bool ok = true;
for (uint32_t k : p.idx) {
if (k >= mo.verts.size()) { ok = false; break; }
poly.push_back(mo.verts[k]);
}
if (!ok) continue;
raw[i] = newell(poly);
okPoly[i] = lengthSq(raw[i]) > 1e-12f;
if (!okPoly[i]) continue;
for (uint32_t k : p.idx) vertPolys[k].push_back(i);
}
for (size_t i = 0; i < np; ++i) {
if (!okPoly[i]) continue;
const MeshPoly& p = mo.polys[i];
const Material& mat = matOf(mats, p.material);
Vec3 fn = normalize(raw[i]);
Batch& b = bs.get(model, p.material, GeomSource::Mesh);
uint32_t base = (uint32_t)b.vertices.size();
bool hasUV = p.uv.size() == p.idx.size();
for (size_t k = 0; k < p.idx.size(); ++k) {
Vertex v;
v.pos = mo.verts[p.idx[k]];
v.normal = fn;
if (p.smooth) {
Vec3 sum;
for (size_t q : vertPolys[p.idx[k]])
if (mo.polys[q].smooth == p.smooth) sum += raw[q];
if (lengthSq(sum) > 1e-12f) v.normal = normalize(sum);
}
v.uv = hasUV ? p.uv[k] : computeUV(v.pos, fn, p.proj, mat.tileSize);
b.vertices.push_back(v);
b.bounds.add(v.pos);
}
for (uint32_t k = 1; k + 1 < (uint32_t)p.idx.size(); ++k) {
b.indices.push_back(base);
b.indices.push_back(base + k);
b.indices.push_back(base + k + 1);
if (coll) {
coll->tris.push_back(mo.verts[p.idx[0]]);
coll->tris.push_back(mo.verts[p.idx[k]]);
coll->tris.push_back(mo.verts[p.idx[k + 1]]);
}
}
}
}
// ---------------------------------------------------------------- CSG
enum { PF_FRONT = 0, PF_BACK = 1, PF_SPLIT = 2, PF_COPLANAR = 3 };
int splitPolyF(const Polygon& poly, const Plane& pl, float eps, Polygon& front, Polygon& back) {
size_t n = poly.size();
std::vector<float> d(n);
std::vector<int> s(n);
int nf = 0, nb = 0;
for (size_t i = 0; i < n; ++i) {
d[i] = pl.dist(poly[i]);
s[i] = d[i] > eps ? 1 : (d[i] < -eps ? -1 : 0);
nf += s[i] > 0;
nb += s[i] < 0;
}
if (nf == 0 && nb == 0) return PF_COPLANAR;
if (nb == 0) return PF_FRONT;
if (nf == 0) return PF_BACK;
for (size_t i = 0; i < n; ++i) {
size_t j = (i + 1) % n;
if (s[i] == 0) { front.push_back(poly[i]); back.push_back(poly[i]); }
else if (s[i] > 0) front.push_back(poly[i]);
else back.push_back(poly[i]);
if ((s[i] > 0 && s[j] < 0) || (s[i] < 0 && s[j] > 0)) {
float t = d[i] / (d[i] - d[j]);
Vec3 m = poly[i] + (poly[j] - poly[i]) * t;
front.push_back(m);
back.push_back(m);
}
}
return PF_SPLIT;
}
// Appends to `out` the parts of `poly` that lie outside brush B (convex, outward planes).
void clipOutside(const Polygon& poly, const Vec3& polyN, const Brush& B, const AABB& bBounds, bool bWinsTies,
std::vector<Polygon>& out) {
// Early outs keep untouched polygons whole (avoids needless fragmentation).
AABB pb;
for (const Vec3& v : poly) pb.add(v);
if (!pb.overlaps(bBounds, kPlaneEps)) { out.push_back(poly); return; }
for (const Face& bf : B.faces) {
bool allFront = true;
for (const Vec3& v : poly)
if (bf.plane.dist(v) <= kPlaneEps) { allFront = false; break; }
if (allFront) { out.push_back(poly); return; }
}
Polygon cur = poly;
for (const Face& bf : B.faces) {
const Plane& pl = bf.plane;
Polygon f, b;
int r = splitPolyF(cur, pl, kPlaneEps, f, b);
if (r == PF_COPLANAR) {
// same-facing coplanar faces: exactly one brush keeps the shared area (lowest index wins)
if (dot(polyN, pl.n) > 0 && !bWinsTies) { out.push_back(cur); return; }
continue; // treated as inside this plane
}
if (r == PF_FRONT) { out.push_back(cur); return; }
if (r == PF_BACK) continue;
if (f.size() >= 3 && length(newell(f)) * 0.5f > 0.01f) out.push_back(std::move(f));
cur = std::move(b);
if (cur.size() < 3) return;
}
// what remains is inside B -> discarded
}
bool isRenderable(const Brush& b) { return (b.contents & (CONTENTS_CLIP | CONTENTS_TRIGGER)) == 0; }
CollisionBrush makeCollision(const Brush& b, const BrushGeometry& g) {
CollisionBrush cb;
cb.contents = b.contents;
cb.sourceId = b.id;
cb.bounds = g.bounds;
for (size_t i = 0; i < b.faces.size(); ++i)
if (g.polys[i].size() >= 3) cb.planes.push_back(b.faces[i].plane);
return cb;
}
// ---------------------------------------------------------------- lighting
uint32_t packColor(const Vec3& c) {
auto q = [](float v) { return (uint32_t)(clampf(v, 0.0f, 1.0f) * 255.0f + 0.5f); };
return q(c.x) | (q(c.y) << 8) | (q(c.z) << 16) | (255u << 24);
}
Vertex midVertex(const Vertex& a, const Vertex& b) {
Vertex m;
m.pos = (a.pos + b.pos) * 0.5f;
m.normal = normalize(a.normal + b.normal);
if (lengthSq(m.normal) < 1e-12f) m.normal = a.normal;
m.uv = (a.uv + b.uv) * 0.5f;
m.color = a.color;
return m;
}
void subdivide(const Vertex& a, const Vertex& b, const Vertex& c, float maxE2, std::vector<Vertex>& out, int depth, size_t& budget) {
float ab = lengthSq(b.pos - a.pos), bc = lengthSq(c.pos - b.pos), ca = lengthSq(a.pos - c.pos);
float m = std::max(ab, std::max(bc, ca));
if (m <= maxE2 || depth > 14 || budget == 0) {
out.push_back(a); out.push_back(b); out.push_back(c);
if (budget > 0) --budget;
return;
}
if (m == ab) { Vertex mid = midVertex(a, b); subdivide(a, mid, c, maxE2, out, depth + 1, budget); subdivide(mid, b, c, maxE2, out, depth + 1, budget); }
else if (m == bc) { Vertex mid = midVertex(b, c); subdivide(a, b, mid, maxE2, out, depth + 1, budget); subdivide(a, mid, c, maxE2, out, depth + 1, budget); }
else { Vertex mid = midVertex(c, a); subdivide(a, b, mid, maxE2, out, depth + 1, budget); subdivide(mid, b, c, maxE2, out, depth + 1, budget); }
}
// Merges vertices that share position, normal and UV (tessellation emits 3 vertices per triangle).
struct WeldKey {
int32_t k[8];
bool operator==(const WeldKey& o) const { return std::memcmp(k, o.k, sizeof k) == 0; }
};
struct WeldHash {
size_t operator()(const WeldKey& w) const {
uint64_t h = 1469598103934665603ull;
for (int i = 0; i < 8; ++i) { h ^= (uint32_t)w.k[i]; h *= 1099511628211ull; }
return (size_t)h;
}
};
void weldBatch(Batch& b) {
auto q = [](float v, float scale) { return (int32_t)std::lround(v * scale); };
std::unordered_map<WeldKey, uint32_t, WeldHash> seen;
seen.reserve(b.vertices.size() / 2 + 1);
std::vector<Vertex> out;
std::vector<uint32_t> idx;
idx.reserve(b.vertices.size());
for (const Vertex& v : b.vertices) {
WeldKey k{{q(v.pos.x, 100), q(v.pos.y, 100), q(v.pos.z, 100), q(v.normal.x, 100), q(v.normal.y, 100), q(v.normal.z, 100),
q(v.uv.x, 1000), q(v.uv.y, 1000)}};
auto it = seen.find(k);
if (it == seen.end()) { it = seen.emplace(k, (uint32_t)out.size()).first; out.push_back(v); }
idx.push_back(it->second);
}
b.vertices = std::move(out);
b.indices = std::move(idx);
}
Vec3 shade(const CompiledMap& cm, const Vec3& p, const Vec3& n, bool shadows) {
Vec3 col = cm.ambient;
Vec3 origin = p + n * 0.5f;
for (const Light& L : cm.lights) {
Vec3 toL;
float atten = 1.0f;
if (L.type == LightType::Sun) {
toL = -normalize(L.dir);
} else {
Vec3 d = L.pos - p;
float dist = length(d);
if (dist > L.radius || dist < 1e-3f) continue;
toL = d / dist;
float r = dist / L.radius;
atten = 1.0f - r * r;
atten *= atten;
}
float ndl = dot(n, toL);
if (ndl <= 0) continue;
if (L.type == LightType::Spot) {
float cd = dot(-toL, normalize(L.dir));
float outer = std::cos(L.outerDeg * kDeg2Rad), inner = std::cos(L.innerDeg * kDeg2Rad);
if (cd <= outer) continue;
atten *= clampf((cd - outer) / (inner - outer + 1e-6f), 0.0f, 1.0f);
}
if (shadows) {
Vec3 target = L.type == LightType::Sun ? origin + toL * 100000.0f : L.pos;
if (cm.trace(origin, target, CONTENTS_SOLID).hit) continue;
}
col += L.color * (L.intensity * ndl * atten);
}
return col;
}
} // namespace
// ================================================================ public API
std::vector<Light> collectLights(const Map& map) {
std::vector<Light> out;
for (const Entity& e : map.entities) {
Light l;
l.entityId = e.id;
l.pos = e.origin;
l.color = e.getColor("color", {1, 1, 1});
l.intensity = e.getFloat("intensity", 1.0f);
if (e.classname == "light") {
l.type = LightType::Point;
l.radius = e.getFloat("radius", 512.0f);
} else if (e.classname == "light_spot") {
l.type = LightType::Spot;
l.dir = e.forward();
l.radius = e.getFloat("radius", 768.0f);
l.innerDeg = e.getFloat("cone_inner", 25.0f);
l.outerDeg = e.getFloat("cone_outer", 40.0f);
} else if (e.classname == "light_environment") {
l.type = LightType::Sun;
l.dir = e.forward();
} else {
continue;
}
out.push_back(l);
}
return out;
}
void buildEditBatches(const Map& map, std::vector<Batch>& out) {
out.clear();
BatchSet bs(out);
for (const Brush& b : map.brushes) {
if (b.flags & OBJ_HIDDEN) continue;
BrushGeometry g = buildBrushGeometry(b);
for (size_t i = 0; i < b.faces.size(); ++i)
if (g.polys[i].size() >= 3)
emitPolygon(bs.get(0, b.faces[i].material, GeomSource::Bsp), g.polys[i], b.faces[i].plane.n, b.faces[i].uv,
matOf(map.materials, b.faces[i].material));
}
for (const MeshObject& m : map.meshes) {
if (m.flags & OBJ_HIDDEN) continue;
emitMesh(bs, m, 0, map.materials, nullptr);
}
}
CompiledMap compile(const Map& map, const CompileOptions& opt) {
auto t0 = std::chrono::steady_clock::now();
CompiledMap cm;
cm.materials = map.materials;
cm.entities = map.entities;
cm.ambient = map.settings.ambient;
cm.unitsPerMeter = map.settings.unitsPerMeter;
cm.models.emplace_back(); // world
std::unordered_map<uint32_t, const Entity*> entById;
for (const Entity& e : map.entities) entById[e.id] = &e;
std::map<uint32_t, int32_t> modelOf;
auto modelFor = [&](uint32_t owner) -> int32_t {
if (owner == 0) return 0;
auto it = entById.find(owner);
if (it != entById.end() && it->second->classname == "func_detail") return 0;
auto m = modelOf.find(owner);
if (m != modelOf.end()) return m->second;
cm.models.emplace_back();
cm.models.back().entityId = owner;
modelOf[owner] = (int32_t)cm.models.size() - 1;
return modelOf[owner];
};
struct Item { const Brush* brush; BrushGeometry geo; int32_t model; };
std::vector<Item> items;
for (const Brush& b : map.brushes) {
BrushGeometry g = buildBrushGeometry(b);
if (!g.valid) continue;
items.push_back({&b, std::move(g), modelFor(b.owner)});
}
BatchSet bs(cm.batches);
std::vector<size_t> worldIdx;
for (size_t i = 0; i < items.size(); ++i)
if (items[i].model == 0) worldIdx.push_back(i);
std::vector<CollisionBrush> worldColl;
for (size_t ii = 0; ii < items.size(); ++ii) {
const Item& A = items[ii];
const Brush& br = *A.brush;
cm.stats.brushes++;
if (br.contents) {
CollisionBrush cb = makeCollision(br, A.geo);
if (A.model == 0) worldColl.push_back(cb);
else cm.models[(size_t)A.model].brushes.push_back(cb);
}
cm.models[(size_t)A.model].bounds.add(A.geo.bounds);
cm.bounds.add(A.geo.bounds);
if (!isRenderable(br)) continue;
for (size_t fi = 0; fi < br.faces.size(); ++fi) {
const Face& face = br.faces[fi];
const Polygon& poly = A.geo.polys[fi];
if (poly.size() < 3 || (face.flags & FACE_NODRAW)) continue;
std::vector<Polygon> frags{poly};
if (opt.csg && A.model == 0) {
for (size_t wj : worldIdx) {
if (wj == ii) continue;
const Item& B = items[wj];
if (!(B.brush->contents & CONTENTS_SOLID)) continue;
if (!B.geo.bounds.overlaps(A.geo.bounds, 0.02f)) continue;
std::vector<Polygon> next;
for (const Polygon& fr : frags) clipOutside(fr, face.plane.n, *B.brush, B.geo.bounds, wj < ii, next);
frags.swap(next);
if (frags.empty()) break;
}
}
for (const Polygon& fr : frags) {
emitPolygon(bs.get(A.model, face.material, GeomSource::Bsp), fr, face.plane.n, face.uv, matOf(map.materials, face.material));
cm.stats.faces++;
}
}
}
for (const MeshObject& m : map.meshes) {
if (m.verts.empty() || m.polys.empty()) continue;
int32_t model = modelFor(m.owner);
CollisionMesh cmesh;
bool collide = model == 0 && !(m.flags & OBJ_NOCOLLIDE);
emitMesh(bs, m, model, map.materials, collide ? &cmesh : nullptr);
AABB bb = meshBounds(m);
cm.models[(size_t)model].bounds.add(bb);
cm.bounds.add(bb);
cm.stats.meshes++;
if (collide && !cmesh.tris.empty()) {
cmesh.objectId = m.id;
cmesh.bounds = bb;
cm.collMeshes.push_back(std::move(cmesh));
}
}
cm.bsp.build(opt.bsp ? std::move(worldColl) : std::vector<CollisionBrush>());
cm.lights = collectLights(map);
for (const Entity& e : map.entities)
if (e.classname == "light_environment" && e.find("ambient")) cm.ambient = e.getColor("ambient", cm.ambient);
if (opt.bakeLighting) bakeLighting(cm, opt.lightEdge, opt.shadows, opt.threads);
for (const Batch& b : cm.batches) {
cm.stats.triangles += (uint32_t)(b.indices.size() / 3);
cm.stats.vertices += (uint32_t)b.vertices.size();
}
cm.stats.bspNodes = (uint32_t)cm.bsp.nodes.size();
cm.stats.bspLeaves = (uint32_t)cm.bsp.leaves.size();
cm.stats.bspDepth = cm.bsp.depth;
cm.stats.lights = (uint32_t)cm.lights.size();
cm.stats.ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - t0).count();
return cm;
}
void bakeLighting(CompiledMap& cm, float maxEdge, bool shadows, int threads) {
maxEdge = std::max(4.0f, maxEdge);
size_t budget = 1500000; // triangle cap so huge maps cannot explode
for (Batch& b : cm.batches) {
std::vector<Vertex> vs;
for (size_t i = 0; i + 2 < b.indices.size(); i += 3)
subdivide(b.vertices[b.indices[i]], b.vertices[b.indices[i + 1]], b.vertices[b.indices[i + 2]], maxEdge * maxEdge, vs, 0, budget);
b.vertices = std::move(vs);
b.indices.resize(b.vertices.size());
std::iota(b.indices.begin(), b.indices.end(), 0u);
weldBatch(b);
}
std::vector<Vertex*> all;
for (Batch& b : cm.batches)
for (Vertex& v : b.vertices) all.push_back(&v);
const CompiledMap& cref = cm;
std::atomic<size_t> next{0};
auto work = [&]() {
const size_t chunk = 1024;
for (;;) {
size_t s = next.fetch_add(chunk);
if (s >= all.size()) break;
size_t e = std::min(all.size(), s + chunk);
for (size_t i = s; i < e; ++i) all[i]->color = packColor(shade(cref, all[i]->pos, all[i]->normal, shadows));
}
};
int n = threads > 0 ? threads : (int)std::thread::hardware_concurrency();
n = std::max(1, std::min(n, 16));
if (n == 1) {
work();
} else {
std::vector<std::thread> pool;
for (int i = 0; i < n; ++i) pool.emplace_back(work);
for (auto& t : pool) t.join();
}
cm.baked = true;
}
} // namespace hmap
+188
View File
@@ -0,0 +1,188 @@
#include "hmap/hmap.h"
#include <cstdio>
#include <cstdlib>
#include <sstream>
namespace hmap {
namespace {
const char* kBuiltin = R"DEF(
# Hammer++ built-in entity definitions (same syntax as assets/entities.def)
# @point|@brush <class> "<label>" [color(r,g,b)] [size(h)|size(hx,hy,hz)] [dir] [model] [contents(solid|clip|trigger|water|sky)]
# <key> <type> "<label>" "<default>" ["a=Label A,b=Label B"] (types: string int float bool color255 vec3 choice model material target)
@point info_player_start "Player start" color(70,230,90) size(16,36,16) dir
@point info_target "Target / marker" color(190,190,190) size(6)
@point light "Point light" color(255,225,110) size(6)
color color255 "Color" "255 240 215"
intensity float "Intensity" "1.0"
radius float "Radius" "512"
@point light_spot "Spot light" color(255,190,90) size(6) dir
color color255 "Color" "255 240 215"
intensity float "Intensity" "1.2"
radius float "Radius" "768"
cone_inner float "Inner cone (deg)" "25"
cone_outer float "Outer cone (deg)" "40"
@point light_environment "Sun / environment" color(255,205,70) size(10) dir
color color255 "Sun color" "255 244 222"
intensity float "Intensity" "0.9"
ambient color255 "Ambient" "42 46 58"
@point prop_static "Static prop" color(120,170,255) size(16) model
model model "Model" ""
solid bool "Collide" "1"
tint color255 "Tint" "255 255 255"
@point prop_dynamic "Dynamic prop" color(110,210,255) size(16) model
model model "Model" ""
solid bool "Collide" "1"
animation string "Animation" ""
@point sound_ambient "Ambient sound" color(200,130,255) size(8)
sound string "Sound file" ""
volume float "Volume" "1.0"
radius float "Radius" "512"
@point item_spawn "Item spawn" color(255,150,200) size(10)
item string "Item" ""
respawn float "Respawn time" "30"
@brush func_detail "Detail brush" color(120,160,255)
@brush func_wall "Wall (own model)" color(170,170,190)
@brush func_door "Door" color(210,165,90)
speed float "Speed" "100"
movedir vec3 "Move direction" "0 1 0"
distance float "Distance" "64"
wait float "Wait before closing" "3"
@brush func_button "Button" color(230,120,120)
target target "Target" ""
delay float "Delay" "0"
@brush trigger_multiple "Trigger (repeatable)" color(255,120,40) contents(trigger)
target target "Target" ""
wait float "Re-arm delay" "1"
@brush trigger_once "Trigger (once)" color(255,170,60) contents(trigger)
target target "Target" ""
@brush func_clip "Player clip" color(120,120,255) contents(clip)
@brush water "Water volume" color(70,120,255) contents(water)
density float "Density" "1.0"
)DEF";
std::vector<std::string> tokenize(const std::string& line) {
std::vector<std::string> t;
size_t i = 0, n = line.size();
while (i < n) {
while (i < n && (line[i] == ' ' || line[i] == '\t' || line[i] == '\r')) ++i;
if (i >= n) break;
if (line[i] == '"') {
size_t j = line.find('"', i + 1);
if (j == std::string::npos) j = n;
t.push_back(line.substr(i + 1, j - i - 1));
i = j + 1;
} else {
size_t j = i;
while (j < n && line[j] != ' ' && line[j] != '\t' && line[j] != '\r') ++j;
t.push_back(line.substr(i, j - i));
i = j;
}
}
return t;
}
std::vector<float> numbersIn(const std::string& tok) {
std::vector<float> v;
size_t a = tok.find('('), b = tok.find(')');
if (a == std::string::npos || b == std::string::npos || b < a) return v;
std::string inner = tok.substr(a + 1, b - a - 1);
for (char& c : inner) if (c == ',') c = ' ';
std::istringstream ss(inner);
float f;
while (ss >> f) v.push_back(f);
return v;
}
bool startsWith(const std::string& s, const char* p) { return s.rfind(p, 0) == 0; }
} // namespace
const EntityClass* EntityRegistry::find(const std::string& name) const {
for (const EntityClass& c : classes)
if (c.name == name) return &c;
return nullptr;
}
Status EntityRegistry::parse(const std::string& text) {
std::istringstream in(text);
std::string line;
int lineNo = 0;
EntityClass* cur = nullptr;
while (std::getline(in, line)) {
++lineNo;
size_t first = line.find_first_not_of(" \t\r");
if (first == std::string::npos || line[first] == '#') continue;
std::vector<std::string> t = tokenize(line);
if (t.empty()) continue;
if (t[0][0] == '@') {
if (t.size() < 2 || (t[0] != "@point" && t[0] != "@brush"))
return Status::Fail("entity defs line " + std::to_string(lineNo) + ": expected @point or @brush <class>");
EntityClass c;
c.name = t[1];
c.label = t.size() > 2 ? t[2] : t[1];
c.brush = t[0] == "@brush";
if (c.brush) c.brushContents = CONTENTS_SOLID;
for (size_t i = 3; i < t.size(); ++i) {
if (startsWith(t[i], "color(")) {
auto v = numbersIn(t[i]);
if (v.size() >= 3) c.color = {v[0] / 255.0f, v[1] / 255.0f, v[2] / 255.0f};
} else if (startsWith(t[i], "size(")) {
auto v = numbersIn(t[i]);
if (v.size() == 1) c.half = {v[0], v[0], v[0]};
else if (v.size() >= 3) c.half = {v[0], v[1], v[2]};
} else if (t[i] == "dir") c.directional = true;
else if (t[i] == "model") c.hasModel = true;
else if (startsWith(t[i], "contents(")) {
std::string v = t[i].substr(9);
if (v.find("trigger") == 0) c.brushContents = CONTENTS_TRIGGER;
else if (v.find("clip") == 0) c.brushContents = CONTENTS_CLIP;
else if (v.find("water") == 0) c.brushContents = CONTENTS_WATER;
else if (v.find("sky") == 0) c.brushContents = CONTENTS_SKY;
else c.brushContents = CONTENTS_SOLID;
}
}
bool replaced = false;
for (EntityClass& ex : classes)
if (ex.name == c.name) { ex = c; cur = &ex; replaced = true; break; }
if (!replaced) { classes.push_back(c); cur = &classes.back(); }
} else {
if (!cur) return Status::Fail("entity defs line " + std::to_string(lineNo) + ": property before any class");
if (t.size() < 3) return Status::Fail("entity defs line " + std::to_string(lineNo) + ": expected <key> <type> \"<label>\"");
PropDef p;
p.key = t[0];
p.type = t[1];
p.label = t[2];
if (t.size() > 3) p.def = t[3];
if (t.size() > 4) p.choices = t[4];
cur->props.push_back(p);
}
}
return Status::Ok();
}
void EntityRegistry::applyDefaults(Entity& e) const {
const EntityClass* c = find(e.classname);
if (!c) return;
for (const PropDef& p : c->props)
if (!p.def.empty() && !e.find(p.key)) e.set(p.key, p.def);
}
EntityRegistry EntityRegistry::builtin() {
EntityRegistry r;
r.parse(kBuiltin);
return r;
}
} // namespace hmap
+464
View File
@@ -0,0 +1,464 @@
#include "hmap/hmap.h"
#include <cstdio>
#include <cstring>
#include <fstream>
#include <map>
#include <set>
#include <sstream>
namespace hmap {
namespace {
// ---------------------------------------------------------------- tiny FBX node tree
struct FProp {
char type = 0;
int64_t i = 0;
double d = 0;
std::string s;
std::vector<double> da;
std::vector<int32_t> ia;
};
struct FNode {
std::string name;
std::vector<FProp> props;
std::vector<FNode> kids;
FNode() = default;
explicit FNode(std::string n) : name(std::move(n)) {}
FNode& i32(int32_t v) { FProp p; p.type = 'I'; p.i = v; props.push_back(std::move(p)); return *this; }
FNode& i64(int64_t v) { FProp p; p.type = 'L'; p.i = v; props.push_back(std::move(p)); return *this; }
FNode& f64(double v) { FProp p; p.type = 'D'; p.d = v; props.push_back(std::move(p)); return *this; }
FNode& boolean(bool v) { FProp p; p.type = 'C'; p.i = v; props.push_back(std::move(p)); return *this; }
FNode& str(const std::string& v) { FProp p; p.type = 'S'; p.s = v; props.push_back(std::move(p)); return *this; }
FNode& raw(const std::string& v) { FProp p; p.type = 'R'; p.s = v; props.push_back(std::move(p)); return *this; }
FNode& darr(std::vector<double> v) { FProp p; p.type = 'd'; p.da = std::move(v); props.push_back(std::move(p)); return *this; }
FNode& iarr(std::vector<int32_t> v) { FProp p; p.type = 'i'; p.ia = std::move(v); props.push_back(std::move(p)); return *this; }
FNode& kid(FNode n) { kids.push_back(std::move(n)); return *this; }
};
class Bin {
public:
std::vector<uint8_t> b;
void u8(uint8_t v) { b.push_back(v); }
void u32(uint32_t v) { for (int i = 0; i < 4; ++i) b.push_back((uint8_t)((v >> (8 * i)) & 0xff)); }
void u64(uint64_t v) { for (int i = 0; i < 8; ++i) b.push_back((uint8_t)((v >> (8 * i)) & 0xff)); }
void f64(double d) { uint64_t u; std::memcpy(&u, &d, 8); u64(u); }
void bytes(const void* p, size_t n) { const uint8_t* q = (const uint8_t*)p; b.insert(b.end(), q, q + n); }
void zeros(size_t n) { b.insert(b.end(), n, 0); }
void patch32(size_t at, uint32_t v) { for (int i = 0; i < 4; ++i) b[at + (size_t)i] = (uint8_t)((v >> (8 * i)) & 0xff); }
};
void writeProp(Bin& o, const FProp& p) {
o.u8((uint8_t)p.type);
switch (p.type) {
case 'C': o.u8(p.i ? 1 : 0); break;
case 'I': o.u32((uint32_t)(int32_t)p.i); break;
case 'L': o.u64((uint64_t)p.i); break;
case 'D': o.f64(p.d); break;
case 'S':
case 'R': o.u32((uint32_t)p.s.size()); o.bytes(p.s.data(), p.s.size()); break;
case 'd':
o.u32((uint32_t)p.da.size()); o.u32(0); o.u32((uint32_t)(p.da.size() * 8));
for (double d : p.da) o.f64(d);
break;
case 'i':
o.u32((uint32_t)p.ia.size()); o.u32(0); o.u32((uint32_t)(p.ia.size() * 4));
for (int32_t v : p.ia) o.u32((uint32_t)v);
break;
default: break;
}
}
constexpr size_t kSentinel = 13; // null record size for FBX < 7.5
void writeNode(Bin& o, const FNode& n) {
size_t start = o.b.size();
o.u32(0); // end offset (patched)
o.u32((uint32_t)n.props.size());
size_t plenAt = o.b.size();
o.u32(0); // property list length (patched)
o.u8((uint8_t)n.name.size());
o.bytes(n.name.data(), n.name.size());
size_t propStart = o.b.size();
for (const FProp& p : n.props) writeProp(o, p);
o.patch32(plenAt, (uint32_t)(o.b.size() - propStart));
if (!n.kids.empty()) {
for (const FNode& k : n.kids) writeNode(o, k);
o.zeros(kSentinel);
}
o.patch32(start, (uint32_t)o.b.size());
}
const std::string kSep("\x00\x01", 2); // FBX binary form of "Name::Class"
std::string fname(const std::string& name, const char* cls) { return name + kSep + cls; }
FNode P(const char* name, const char* type, const char* label, const char* flags) {
FNode p("P");
p.str(name).str(type).str(label).str(flags);
return p;
}
FNode ver(int v) { FNode n("Version"); n.i32(v); return n; }
FNode sval(const char* key, const std::string& v) { FNode n(key); n.str(v); return n; }
std::string safeName(const std::string& s) {
std::string r;
for (char c : s) r += (c == '\0' || c == '\n' || c == '\r') ? '_' : c;
return r.empty() ? "unnamed" : r;
}
} // namespace
std::vector<uint8_t> exportFBXToMemory(const CompiledMap& cm, const FbxOptions& opt) {
int64_t nextId = 1000000;
auto newId = [&]() { return nextId++; };
struct Conn { bool prop; int64_t child, parent; std::string pname; };
std::vector<FNode> objects;
std::vector<Conn> conns;
int nModel = 0, nGeo = 0, nMat = 0, nTex = 0;
const double sc = opt.scale;
// ---- materials (created lazily, shared by all models)
std::map<int32_t, int64_t> matIds;
auto getMaterial = [&](int32_t mi) -> int64_t {
auto it = matIds.find(mi);
if (it != matIds.end()) return it->second;
Material m;
if (mi >= 0 && (size_t)mi < cm.materials.size()) m = cm.materials[(size_t)mi];
else m.name = "default";
int64_t id = newId();
std::string nm = safeName(m.name);
FNode mat("Material");
mat.i64(id).str(fname(nm, "Material")).str("");
mat.kid(ver(102)).kid(sval("ShadingModel", "lambert")).kid(FNode("MultiLayer").i32(0));
FNode props("Properties70");
props.kid(P("DiffuseColor", "Color", "", "A").f64(m.color.x).f64(m.color.y).f64(m.color.z));
props.kid(P("DiffuseFactor", "Number", "", "A").f64(1.0));
mat.kid(std::move(props));
objects.push_back(std::move(mat));
++nMat;
if (!m.texture.empty()) {
int64_t tid = newId();
std::string path = m.texture;
for (char& c : path) if (c == '\\') c = '/';
FNode tex("Texture");
tex.i64(tid).str(fname(nm + "_tex", "Texture")).str("");
tex.kid(sval("Type", "TextureVideoClip")).kid(ver(202)).kid(sval("TextureName", fname(nm + "_tex", "Texture")));
FNode tp("Properties70");
tp.kid(P("UseMaterial", "bool", "", "").i32(1));
tex.kid(std::move(tp));
tex.kid(sval("Media", "")).kid(sval("FileName", path)).kid(sval("RelativeFilename", path));
objects.push_back(std::move(tex));
conns.push_back({true, tid, id, "DiffuseColor"});
++nTex;
}
matIds[mi] = id;
return id;
};
auto userProps = [&](FNode& props, const Entity& e) {
props.kid(P("classname", "KString", "", "U").str(e.classname));
props.kid(P("hmap_id", "int", "Integer", "U").i32((int32_t)e.id));
for (const KeyValue& kv : e.props)
if (!kv.key.empty()) props.kid(P(safeName(kv.key).c_str(), "KString", "", "U").str(kv.value));
};
std::set<uint32_t> modelEntities;
for (const CompiledModel& m : cm.models)
if (m.entityId) modelEntities.insert(m.entityId);
// ---- one Model + Geometry per compiled model (world / brush entities)
for (size_t mi = 0; mi < cm.models.size(); ++mi) {
if (mi > 0 && !opt.includeSubModels) continue;
std::vector<const Batch*> bl;
for (const Batch& b : cm.batches)
if (b.model == (int32_t)mi && !b.indices.empty()) bl.push_back(&b);
if (bl.empty()) continue;
const Entity* owner = nullptr;
if (mi > 0)
for (const Entity& e : cm.entities)
if (e.id == cm.models[mi].entityId) owner = &e;
std::string name = mi == 0 ? "World" : (owner ? owner->classname + "_" + std::to_string(owner->id) : "Model" + std::to_string(mi));
std::vector<double> verts, normals, uvs, colors;
std::vector<int32_t> pvi, uvIdx, polyMat;
std::vector<int32_t> slotMat; // local slot -> global material
std::map<int32_t, int32_t> slotOf;
bool useColors = opt.vertexColors && cm.baked;
int32_t cp = 0;
for (const Batch* b : bl) {
auto sit = slotOf.find(b->material);
if (sit == slotOf.end()) {
sit = slotOf.emplace(b->material, (int32_t)slotMat.size()).first;
slotMat.push_back(b->material);
}
int32_t base = cp;
for (const Vertex& v : b->vertices) {
verts.push_back(v.pos.x * sc); verts.push_back(v.pos.y * sc); verts.push_back(v.pos.z * sc);
uvs.push_back(v.uv.x); uvs.push_back(1.0 - v.uv.y);
if (useColors) {
colors.push_back((double)(v.color & 0xff) / 255.0);
colors.push_back((double)((v.color >> 8) & 0xff) / 255.0);
colors.push_back((double)((v.color >> 16) & 0xff) / 255.0);
colors.push_back((double)((v.color >> 24) & 0xff) / 255.0);
}
++cp;
}
for (size_t i = 0; i + 2 < b->indices.size(); i += 3) {
int32_t a = base + (int32_t)b->indices[i], c1 = base + (int32_t)b->indices[i + 1], c2 = base + (int32_t)b->indices[i + 2];
pvi.push_back(a); pvi.push_back(c1); pvi.push_back(-c2 - 1); // last index of a polygon is stored as ~i
uvIdx.push_back(a); uvIdx.push_back(c1); uvIdx.push_back(c2);
for (size_t k = 0; k < 3; ++k) {
const Vec3& n = b->vertices[b->indices[i + k]].normal;
normals.push_back(n.x); normals.push_back(n.y); normals.push_back(n.z);
}
polyMat.push_back(sit->second);
}
}
int64_t geoId = newId(), modelId = newId();
FNode geo("Geometry");
geo.i64(geoId).str(fname(name, "Geometry")).str("Mesh");
geo.kid(FNode("Vertices").darr(std::move(verts)));
geo.kid(FNode("PolygonVertexIndex").iarr(pvi));
geo.kid(FNode("GeometryVersion").i32(124));
{
FNode e("LayerElementNormal");
e.i32(0).kid(ver(101)).kid(sval("Name", "")).kid(sval("MappingInformationType", "ByPolygonVertex"))
.kid(sval("ReferenceInformationType", "Direct")).kid(FNode("Normals").darr(std::move(normals)));
geo.kid(std::move(e));
}
{
FNode e("LayerElementUV");
e.i32(0).kid(ver(101)).kid(sval("Name", "UVMap")).kid(sval("MappingInformationType", "ByPolygonVertex"))
.kid(sval("ReferenceInformationType", "IndexToDirect")).kid(FNode("UV").darr(std::move(uvs)))
.kid(FNode("UVIndex").iarr(uvIdx));
geo.kid(std::move(e));
}
if (useColors) {
FNode e("LayerElementColor");
e.i32(0).kid(ver(101)).kid(sval("Name", "BakedLight")).kid(sval("MappingInformationType", "ByPolygonVertex"))
.kid(sval("ReferenceInformationType", "IndexToDirect")).kid(FNode("Colors").darr(std::move(colors)))
.kid(FNode("ColorIndex").iarr(uvIdx));
geo.kid(std::move(e));
}
{
FNode e("LayerElementMaterial");
e.i32(0).kid(ver(101)).kid(sval("Name", "")).kid(sval("MappingInformationType", "ByPolygon"))
.kid(sval("ReferenceInformationType", "IndexToDirect")).kid(FNode("Materials").iarr(std::move(polyMat)));
geo.kid(std::move(e));
}
{
FNode layer("Layer");
layer.i32(0).kid(ver(100));
auto le = [&](const char* type) {
FNode l("LayerElement");
l.kid(sval("Type", type)).kid(FNode("TypedIndex").i32(0));
return l;
};
layer.kid(le("LayerElementNormal")).kid(le("LayerElementMaterial")).kid(le("LayerElementUV"));
if (useColors) layer.kid(le("LayerElementColor"));
geo.kid(std::move(layer));
}
objects.push_back(std::move(geo));
++nGeo;
FNode model("Model");
model.i64(modelId).str(fname(name, "Model")).str("Mesh");
model.kid(ver(232));
FNode props("Properties70");
if (owner) userProps(props, *owner);
model.kid(std::move(props));
model.kid(FNode("Shading").boolean(true)).kid(sval("Culling", "CullingOff"));
objects.push_back(std::move(model));
++nModel;
conns.push_back({false, modelId, 0, ""});
conns.push_back({false, geoId, modelId, ""});
for (int32_t gm : slotMat) conns.push_back({false, getMaterial(gm), modelId, ""});
}
// ---- entities / props as Null nodes carrying their key-values as custom properties
if (opt.entities) {
for (const Entity& e : cm.entities) {
if (modelEntities.count(e.id)) continue;
int64_t id = newId();
std::string nm = safeName(e.classname + "_" + std::to_string(e.id));
FNode model("Model");
model.i64(id).str(fname(nm, "Model")).str("Null");
model.kid(ver(232));
FNode props("Properties70");
Vec3 eul = Mat34::fromEuler(e.angles).toEulerXYZ();
props.kid(P("Lcl Translation", "Lcl Translation", "", "A").f64(e.origin.x * sc).f64(e.origin.y * sc).f64(e.origin.z * sc));
props.kid(P("Lcl Rotation", "Lcl Rotation", "", "A").f64(eul.x).f64(eul.y).f64(eul.z));
props.kid(P("Lcl Scaling", "Lcl Scaling", "", "A").f64(e.scale.x).f64(e.scale.y).f64(e.scale.z));
userProps(props, e);
model.kid(std::move(props));
model.kid(FNode("Shading").boolean(true)).kid(sval("Culling", "CullingOff"));
objects.push_back(std::move(model));
conns.push_back({false, id, 0, ""});
++nModel;
}
}
// ---- assemble the file
double usf = 100.0 / ((double)(cm.unitsPerMeter > 0 ? cm.unitsPerMeter : 64.0f) * (sc > 0 ? sc : 1.0));
std::vector<FNode> top;
{
FNode h("FBXHeaderExtension");
h.kid(FNode("FBXHeaderVersion").i32(1003)).kid(FNode("FBXVersion").i32(7400));
FNode ts("CreationTimeStamp");
ts.kid(ver(1000)).kid(FNode("Year").i32(2026)).kid(FNode("Month").i32(1)).kid(FNode("Day").i32(1))
.kid(FNode("Hour").i32(0)).kid(FNode("Minute").i32(0)).kid(FNode("Second").i32(0)).kid(FNode("Millisecond").i32(0));
h.kid(std::move(ts)).kid(sval("Creator", "Hammer++ libhmap"));
FNode si("SceneInfo");
si.str(fname("GlobalInfo", "SceneInfo")).str("UserData");
si.kid(sval("Type", "UserData")).kid(ver(100));
FNode md("MetaData");
md.kid(ver(100)).kid(sval("Title", "")).kid(sval("Subject", "")).kid(sval("Author", "")).kid(sval("Keywords", ""))
.kid(sval("Revision", "")).kid(sval("Comment", ""));
si.kid(std::move(md)).kid(FNode("Properties70"));
h.kid(std::move(si));
top.push_back(std::move(h));
}
top.push_back(FNode("FileId").raw(std::string("\x28\xb3\x2a\xeb\xb6\x24\xcc\xc2\xbf\xc8\xb0\x2a\xa9\x2b\xfc\xf1", 16)));
top.push_back(sval("CreationTime", "1970-01-01 10:00:00:000"));
top.push_back(sval("Creator", "Hammer++ libhmap"));
{
FNode g("GlobalSettings");
g.kid(ver(1000));
FNode p("Properties70");
p.kid(P("UpAxis", "int", "Integer", "").i32(1)).kid(P("UpAxisSign", "int", "Integer", "").i32(1));
p.kid(P("FrontAxis", "int", "Integer", "").i32(2)).kid(P("FrontAxisSign", "int", "Integer", "").i32(1));
p.kid(P("CoordAxis", "int", "Integer", "").i32(0)).kid(P("CoordAxisSign", "int", "Integer", "").i32(1));
p.kid(P("UnitScaleFactor", "double", "Number", "").f64(usf)).kid(P("OriginalUnitScaleFactor", "double", "Number", "").f64(usf));
g.kid(std::move(p));
top.push_back(std::move(g));
}
{
FNode d("Documents");
d.kid(FNode("Count").i32(1));
FNode doc("Document");
doc.i64(newId()).str("").str("Scene");
FNode dp("Properties70");
dp.kid(P("SourceObject", "object", "", "")).kid(P("ActiveAnimStackName", "KString", "", "").str(""));
doc.kid(std::move(dp)).kid(FNode("RootNode").i64(0));
d.kid(std::move(doc));
top.push_back(std::move(d));
}
top.push_back(FNode("References"));
{
FNode d("Definitions");
d.kid(ver(100)).kid(FNode("Count").i32(nModel + nGeo + nMat + nTex + 1));
auto ot = [&](const char* type, int n) {
FNode o("ObjectType");
o.str(type).kid(FNode("Count").i32(n));
return o;
};
d.kid(ot("GlobalSettings", 1));
if (nModel) d.kid(ot("Model", nModel));
if (nGeo) d.kid(ot("Geometry", nGeo));
if (nMat) d.kid(ot("Material", nMat));
if (nTex) d.kid(ot("Texture", nTex));
top.push_back(std::move(d));
}
{
FNode o("Objects");
for (FNode& n : objects) o.kid(std::move(n));
top.push_back(std::move(o));
}
{
FNode c("Connections");
for (const Conn& cn : conns) {
FNode n("C");
n.str(cn.prop ? "OP" : "OO").i64(cn.child).i64(cn.parent);
if (cn.prop) n.str(cn.pname);
c.kid(std::move(n));
}
top.push_back(std::move(c));
}
top.push_back(FNode("Takes").kid(sval("Current", "")));
Bin out;
const char magic[] = "Kaydara FBX Binary ";
out.bytes(magic, 20);
out.u8(0); out.u8(0x1a); out.u8(0);
out.u32(7400);
for (const FNode& n : top) writeNode(out, n);
out.zeros(kSentinel);
static const uint8_t footId[16] = {0xfa, 0xbc, 0xab, 0x09, 0xd0, 0xc8, 0xd4, 0x66, 0xb1, 0x76, 0xfb, 0x83, 0x1c, 0xf7, 0x26, 0x7e};
static const uint8_t footEnd[16] = {0xf8, 0x5a, 0x8c, 0x6a, 0xde, 0xf5, 0xd9, 0x7e, 0xec, 0xe9, 0x0c, 0xe3, 0x75, 0x8f, 0x29, 0x0b};
out.bytes(footId, 16);
out.zeros(4);
size_t pad = ((out.b.size() + 15) & ~(size_t)15) - out.b.size();
out.zeros(pad == 0 ? 16 : pad);
out.u32(7400);
out.zeros(120);
out.bytes(footEnd, 16);
return std::move(out.b);
}
Status exportFBX(const std::string& path, const CompiledMap& cm, const FbxOptions& opt) {
std::vector<uint8_t> data = exportFBXToMemory(cm, opt);
std::ofstream f(path, std::ios::binary | std::ios::trunc);
if (!f) return Status::Fail("cannot write " + path);
f.write((const char*)data.data(), (std::streamsize)data.size());
return f ? Status::Ok() : Status::Fail("write failed: " + path);
}
// ================================================================ OBJ
Status exportOBJ(const std::string& path, const CompiledMap& cm, float scale) {
std::ofstream f(path, std::ios::trunc);
if (!f) return Status::Fail("cannot write " + path);
std::string mtlPath = path;
size_t dp = mtlPath.find_last_of('.');
mtlPath = (dp == std::string::npos ? mtlPath : mtlPath.substr(0, dp)) + ".mtl";
size_t sl = mtlPath.find_last_of("/\\");
f << "# Hammer++ export\nmtllib " << (sl == std::string::npos ? mtlPath : mtlPath.substr(sl + 1)) << "\n";
auto mname = [&](int32_t mi) {
std::string n = (mi >= 0 && (size_t)mi < cm.materials.size()) ? cm.materials[(size_t)mi].name : "default";
for (char& c : n) if (c == ' ' || c == '/' || c == '\\') c = '_';
return n;
};
char line[160];
size_t base = 1;
for (const Batch& b : cm.batches) {
if (b.indices.empty()) continue;
f << "o " << (b.model == 0 ? "world" : "model" + std::to_string(b.model)) << "_" << mname(b.material)
<< (b.source == GeomSource::Mesh ? "_mesh" : "_bsp") << "\nusemtl " << mname(b.material) << "\n";
for (const Vertex& v : b.vertices) {
std::snprintf(line, sizeof line, "v %.5f %.5f %.5f\n", v.pos.x * scale, v.pos.y * scale, v.pos.z * scale);
f << line;
}
for (const Vertex& v : b.vertices) {
std::snprintf(line, sizeof line, "vt %.5f %.5f\n", v.uv.x, 1.0f - v.uv.y);
f << line;
}
for (const Vertex& v : b.vertices) {
std::snprintf(line, sizeof line, "vn %.5f %.5f %.5f\n", v.normal.x, v.normal.y, v.normal.z);
f << line;
}
for (size_t i = 0; i + 2 < b.indices.size(); i += 3) {
f << "f";
for (size_t k = 0; k < 3; ++k) {
size_t id = base + b.indices[i + k];
f << " " << id << "/" << id << "/" << id;
}
f << "\n";
}
base += b.vertices.size();
}
std::ofstream m(mtlPath, std::ios::trunc);
if (m)
for (size_t i = 0; i < cm.materials.size(); ++i) {
const Material& mt = cm.materials[i];
m << "newmtl " << mname((int32_t)i) << "\nKd " << mt.color.x << " " << mt.color.y << " " << mt.color.z << "\n";
if (!mt.texture.empty()) m << "map_Kd " << mt.texture << "\n";
m << "\n";
}
return f ? Status::Ok() : Status::Fail("write failed: " + path);
}
} // namespace hmap
+655
View File
@@ -0,0 +1,655 @@
#include "hmap/hmap.h"
#include <array>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <map>
#include <sstream>
namespace hmap {
// ============================================================ polygon building (double precision)
namespace {
struct DV { double x, y, z; };
using DPoly = std::vector<DV>;
double ddist(const Plane& p, const DV& v) {
return (double)p.n.x * v.x + (double)p.n.y * v.y + (double)p.n.z * v.z - (double)p.d;
}
DPoly basePoly(const Plane& p) {
Vec3 ref = std::fabs(p.n.y) < 0.9f ? Vec3(0, 1, 0) : Vec3(1, 0, 0);
Vec3 t = normalize(cross(p.n, ref));
Vec3 b = cross(p.n, t);
const double S = 65536.0;
DV o{(double)p.n.x * p.d, (double)p.n.y * p.d, (double)p.n.z * p.d};
auto P = [&](double a, double c) {
return DV{o.x + t.x * a + b.x * c, o.y + t.y * a + b.y * c, o.z + t.z * a + b.z * c};
};
return {P(-S, -S), P(S, -S), P(S, S), P(-S, S)}; // CCW seen from +n
}
// Keep the part of poly on the back (<= 0) side of the plane.
void clipBack(DPoly& poly, const Plane& pl, double eps) {
size_t n = poly.size();
if (n == 0) return;
std::vector<double> d(n);
std::vector<int> s(n);
int nf = 0, nb = 0;
for (size_t i = 0; i < n; ++i) {
d[i] = ddist(pl, poly[i]);
s[i] = d[i] > eps ? 1 : (d[i] < -eps ? -1 : 0);
nf += s[i] > 0;
nb += s[i] < 0;
}
if (nf == 0) return;
if (nb == 0) { poly.clear(); return; }
DPoly out;
for (size_t i = 0; i < n; ++i) {
size_t j = (i + 1) % n;
if (s[i] <= 0) out.push_back(poly[i]);
if ((s[i] > 0 && s[j] < 0) || (s[i] < 0 && s[j] > 0)) {
double t = d[i] / (d[i] - d[j]);
out.push_back({poly[i].x + (poly[j].x - poly[i].x) * t, poly[i].y + (poly[j].y - poly[i].y) * t,
poly[i].z + (poly[j].z - poly[i].z) * t});
}
}
poly.swap(out);
}
Vec3 newellRaw(const Polygon& p) {
Vec3 n;
for (size_t i = 0; i < p.size(); ++i) {
const Vec3& a = p[i];
const Vec3& b = p[(i + 1) % p.size()];
n.x += (a.y - b.y) * (a.z + b.z);
n.y += (a.z - b.z) * (a.x + b.x);
n.z += (a.x - b.x) * (a.y + b.y);
}
return n;
}
} // namespace
Vec3 polyNormal(const Polygon& p) { return normalize(newellRaw(p)); }
BrushGeometry buildBrushGeometry(const Brush& b) {
BrushGeometry g;
size_t nf = b.faces.size();
g.polys.resize(nf);
if (nf < 4) return g;
int good = 0;
for (size_t i = 0; i < nf; ++i) {
const Plane& pi = b.faces[i].plane;
bool dup = false;
for (size_t j = 0; j < i && !dup; ++j) {
const Plane& pj = b.faces[j].plane;
dup = dot(pi.n, pj.n) > 0.99999f && std::fabs(pi.d - pj.d) < 0.01f;
}
if (dup) continue;
DPoly p = basePoly(pi);
for (size_t j = 0; j < nf && p.size() >= 3; ++j)
if (j != i) clipBack(p, b.faces[j].plane, 1e-3);
if (p.size() < 3) continue;
Polygon fp;
for (const DV& v : p) {
Vec3 q((float)v.x, (float)v.y, (float)v.z);
if (fp.empty() || length(q - fp.back()) > 1e-3f) fp.push_back(q);
}
while (fp.size() > 1 && length(fp.front() - fp.back()) <= 1e-3f) fp.pop_back();
if (fp.size() < 3 || length(newellRaw(fp)) * 0.5f < 1e-3f) continue;
for (const Vec3& q : fp) { g.bounds.add(q); g.points.push_back(q); }
g.polys[i] = std::move(fp);
++good;
}
Vec3 sz = g.bounds.size();
g.valid = good >= 4 && sz.x > 0.01f && sz.y > 0.01f && sz.z > 0.01f;
return g;
}
AABB brushBounds(const Brush& b) { return buildBrushGeometry(b).bounds; }
AABB meshBounds(const MeshObject& m) {
AABB bb;
for (const Vec3& v : m.verts) bb.add(v);
return bb;
}
// ============================================================ raycasts
bool rayBrush(const Brush& b, const Vec3& o, const Vec3& d, float& tHit, int& face) {
float tEnter = -1e30f, tExit = 1e30f;
int fe = -1;
for (size_t i = 0; i < b.faces.size(); ++i) {
const Plane& p = b.faces[i].plane;
float denom = dot(p.n, d), dist = p.dist(o);
if (std::fabs(denom) < 1e-8f) {
if (dist > 0) return false;
continue;
}
float t = -dist / denom;
if (denom < 0) { if (t > tEnter) { tEnter = t; fe = (int)i; } }
else if (t < tExit) tExit = t;
if (tEnter > tExit) return false;
}
if (fe < 0 || tEnter < 0 || tEnter > tExit) return false;
tHit = tEnter;
face = fe;
return true;
}
static bool rayTri(const Vec3& o, const Vec3& d, const Vec3& a, const Vec3& b, const Vec3& c, float& t) {
Vec3 e1 = b - a, e2 = c - a, p = cross(d, e2);
float det = dot(e1, p);
if (std::fabs(det) < 1e-9f) return false;
float inv = 1.0f / det;
Vec3 s = o - a;
float u = dot(s, p) * inv;
if (u < 0 || u > 1) return false;
Vec3 q = cross(s, e1);
float v = dot(d, q) * inv;
if (v < 0 || u + v > 1) return false;
float tt = dot(e2, q) * inv;
if (tt < 0) return false;
t = tt;
return true;
}
bool rayMesh(const MeshObject& m, const Vec3& o, const Vec3& d, float& tHit, int& poly) {
bool hit = false;
float best = 1e30f;
for (size_t pi = 0; pi < m.polys.size(); ++pi) {
const auto& idx = m.polys[pi].idx;
for (size_t k = 1; k + 1 < idx.size(); ++k) {
if (idx[0] >= m.verts.size() || idx[k] >= m.verts.size() || idx[k + 1] >= m.verts.size()) continue;
float t;
if (rayTri(o, d, m.verts[idx[0]], m.verts[idx[k]], m.verts[idx[k + 1]], t) && t < best) {
best = t; poly = (int)pi; hit = true;
}
}
}
if (hit) tHit = best;
return hit;
}
// ============================================================ texture mapping
void uvAxes(const Vec3& n, const UVInfo& uv, Vec3& u, Vec3& v) {
float ax = std::fabs(n.x), ay = std::fabs(n.y), az = std::fabs(n.z);
Vec3 ua, va;
if (ay >= ax && ay >= az) {
if (n.y >= 0) { ua = {1, 0, 0}; va = {0, 0, 1}; } else { ua = {1, 0, 0}; va = {0, 0, -1}; }
} else if (ax >= az) {
if (n.x >= 0) { ua = {0, 0, -1}; va = {0, -1, 0}; } else { ua = {0, 0, 1}; va = {0, -1, 0}; }
} else {
if (n.z >= 0) { ua = {1, 0, 0}; va = {0, -1, 0}; } else { ua = {-1, 0, 0}; va = {0, -1, 0}; }
}
u = normalize(ua - n * dot(ua, n));
v = normalize(va - n * dot(va, n));
if (uv.rotation != 0) {
float r = uv.rotation * kDeg2Rad, c = std::cos(r), s = std::sin(r);
Vec3 u2 = u * c + v * s, v2 = v * c - u * s;
u = u2; v = v2;
}
}
Vec2 computeUV(const Vec3& p, const Vec3& n, const UVInfo& uv, const Vec2& tile) {
Vec3 u, v;
uvAxes(n, uv, u, v);
float sx = std::fabs(uv.scale.x) < 1e-4f ? 1e-4f : uv.scale.x;
float sy = std::fabs(uv.scale.y) < 1e-4f ? 1e-4f : uv.scale.y;
float tx = tile.x < 1e-3f ? 64.0f : tile.x, ty = tile.y < 1e-3f ? 64.0f : tile.y;
return {(dot(p, u) + uv.offset.x) / (tx * sx), (dot(p, v) + uv.offset.y) / (ty * sy)};
}
// ============================================================ brush / mesh edits
void translateBrush(Brush& b, const Vec3& t, bool lockTexture) {
for (Face& f : b.faces) {
if (lockTexture) {
Vec3 u, v;
uvAxes(f.plane.n, f.uv, u, v);
f.uv.offset.x -= dot(t, u);
f.uv.offset.y -= dot(t, v);
}
f.plane.d += dot(f.plane.n, t);
}
}
void transformBrush(Brush& b, const Mat34& m) {
for (Face& f : b.faces) f.plane = m.transformPlane(f.plane);
}
void transformMesh(MeshObject& m, const Mat34& xf) {
for (Vec3& v : m.verts) v = xf.point(v);
if (xf.det3() < 0) flipMesh(m); // mirrored: keep CCW winding
}
std::vector<Brush> hollowBrush(const Brush& src, float t) {
std::vector<Brush> out;
BrushGeometry g = buildBrushGeometry(src);
if (!g.valid || t <= 0) return out;
size_t n = src.faces.size();
for (size_t k = 0; k < n; ++k) {
if (g.polys[k].size() < 3) continue;
Brush w;
w.contents = src.contents;
w.owner = src.owner;
w.flags = src.flags;
for (size_t j = 0; j < n; ++j)
if (g.polys[j].size() >= 3) w.faces.push_back(src.faces[j]);
Face inner = src.faces[k]; // room-facing wall surface
inner.plane = Plane{-src.faces[k].plane.n, -(src.faces[k].plane.d - t)};
w.faces.push_back(inner);
for (size_t j = 0; j < k; ++j) { // carve away what earlier walls already own
if (g.polys[j].size() < 3) continue;
Face cut = src.faces[j];
cut.plane.d -= t;
cut.flags |= FACE_NODRAW;
w.faces.push_back(cut);
}
if (buildBrushGeometry(w).valid) out.push_back(std::move(w));
}
return out;
}
bool clipBrush(const Brush& b, const Plane& p, Brush* front, Brush* back) {
int mat = b.faces.empty() ? 0 : b.faces[0].material;
bool okF = false, okB = false;
Brush f = b, k = b;
Face ff; ff.material = mat; ff.plane = Plane{-p.n, -p.d};
Face fk; fk.material = mat; fk.plane = p;
f.faces.push_back(ff);
k.faces.push_back(fk);
if (buildBrushGeometry(f).valid) { okF = true; if (front) *front = std::move(f); }
if (buildBrushGeometry(k).valid) { okB = true; if (back) *back = std::move(k); }
return okF || okB;
}
MeshObject brushToMesh(const Brush& b) {
MeshObject m;
m.owner = b.owner;
m.flags = b.flags;
BrushGeometry g = buildBrushGeometry(b);
std::map<std::array<long long, 3>, uint32_t> weld;
for (size_t i = 0; i < g.polys.size(); ++i) {
if (g.polys[i].size() < 3) continue;
MeshPoly p;
p.material = b.faces[i].material;
p.proj = b.faces[i].uv;
for (const Vec3& v : g.polys[i]) {
std::array<long long, 3> key{std::llround(v.x * 100.0), std::llround(v.y * 100.0), std::llround(v.z * 100.0)};
auto it = weld.find(key);
if (it == weld.end()) {
it = weld.emplace(key, (uint32_t)m.verts.size()).first;
m.verts.push_back(v);
}
p.idx.push_back(it->second);
}
m.polys.push_back(std::move(p));
}
return m;
}
void flipMesh(MeshObject& m) {
for (MeshPoly& p : m.polys) {
std::reverse(p.idx.begin(), p.idx.end());
std::reverse(p.uv.begin(), p.uv.end());
}
}
static float hash2(int x, int z, uint32_t seed) {
uint32_t h = (uint32_t)x * 374761393u + (uint32_t)z * 668265263u + seed * 2246822519u;
h = (h ^ (h >> 13)) * 1274126177u;
h ^= h >> 16;
return (float)(h & 0xffffffu) / (float)0xffffff;
}
static float valueNoise(float x, float z, uint32_t seed) {
int xi = (int)std::floor(x), zi = (int)std::floor(z);
float fx = x - (float)xi, fz = z - (float)zi;
fx = fx * fx * (3 - 2 * fx);
fz = fz * fz * (3 - 2 * fz);
float a = hash2(xi, zi, seed), b = hash2(xi + 1, zi, seed), c = hash2(xi, zi + 1, seed), d = hash2(xi + 1, zi + 1, seed);
float top = a + (b - a) * fx, bot = c + (d - c) * fx;
return top + (bot - top) * fz;
}
void noiseMesh(MeshObject& m, float amp, float freq, uint32_t seed) {
for (Vec3& v : m.verts) {
float n = valueNoise(v.x * freq, v.z * freq, seed) * 0.6f + valueNoise(v.x * freq * 2.1f, v.z * freq * 2.1f, seed + 7) * 0.28f +
valueNoise(v.x * freq * 4.3f, v.z * freq * 4.3f, seed + 13) * 0.12f;
v.y += (n * 2.0f - 1.0f) * amp;
}
}
bool extrudePoly(MeshObject& m, int pi, float dist) {
if (pi < 0 || (size_t)pi >= m.polys.size() || std::fabs(dist) < 1e-5f) return false;
MeshPoly poly = m.polys[(size_t)pi];
size_t n = poly.idx.size();
if (n < 3) return false;
Polygon pp;
for (uint32_t i : poly.idx) pp.push_back(m.verts[i]);
Vec3 N = polyNormal(pp);
std::vector<uint32_t> ring(n);
for (size_t k = 0; k < n; ++k) {
ring[k] = (uint32_t)m.verts.size();
m.verts.push_back(m.verts[poly.idx[k]] + N * dist);
}
for (size_t k = 0; k < n; ++k) {
size_t k1 = (k + 1) % n;
MeshPoly side;
side.material = poly.material;
side.proj = poly.proj;
side.smooth = 0;
if (dist > 0) side.idx = {poly.idx[k], poly.idx[k1], ring[k1], ring[k]};
else side.idx = {poly.idx[k], ring[k], ring[k1], poly.idx[k1]};
m.polys.push_back(side);
}
m.polys[(size_t)pi].idx = ring;
return true;
}
Status importOBJ(const std::string& path, Map& map, MeshObject& out) {
std::ifstream f(path);
if (!f) return Status::Fail("cannot open " + path);
out = MeshObject();
size_t slash = path.find_last_of("/\\");
std::string base = slash == std::string::npos ? path : path.substr(slash + 1);
size_t dotp = base.find_last_of('.');
out.name = dotp == std::string::npos ? base : base.substr(0, dotp);
std::vector<Vec2> tex;
int mat = map.materials.empty() ? map.addMaterial("dev/gray") : 0;
uint32_t smooth = 0;
std::string line;
while (std::getline(f, line)) {
std::istringstream ss(line);
std::string tag;
ss >> tag;
if (tag == "v") {
Vec3 v;
ss >> v.x >> v.y >> v.z;
out.verts.push_back(v);
} else if (tag == "vt") {
Vec2 t;
ss >> t.x >> t.y;
t.y = 1.0f - t.y;
tex.push_back(t);
} else if (tag == "usemtl") {
std::string n;
ss >> n;
if (!n.empty()) mat = map.addMaterial(n);
} else if (tag == "s") {
std::string v;
ss >> v;
smooth = (v == "off" || v == "0") ? 0u : (uint32_t)std::atoi(v.c_str());
} else if (tag == "f") {
MeshPoly p;
p.material = mat;
p.smooth = smooth;
bool allUV = true;
std::string tok;
while (ss >> tok) {
int vi = std::atoi(tok.c_str()), ti = 0;
size_t s1 = tok.find('/');
if (s1 != std::string::npos) {
std::string rest = tok.substr(s1 + 1);
if (!rest.empty() && rest[0] != '/') ti = std::atoi(rest.c_str());
}
int v = vi < 0 ? (int)out.verts.size() + vi : vi - 1;
if (v < 0 || (size_t)v >= out.verts.size()) { p.idx.clear(); break; }
p.idx.push_back((uint32_t)v);
int t = ti < 0 ? (int)tex.size() + ti : ti - 1;
if (ti != 0 && t >= 0 && (size_t)t < tex.size()) p.uv.push_back(tex[(size_t)t]);
else allUV = false;
}
if (!allUV) p.uv.clear();
if (p.idx.size() >= 3) out.polys.push_back(std::move(p));
}
}
if (out.polys.empty()) return Status::Fail("no faces in " + path);
return Status::Ok();
}
// ============================================================ brush primitives
Brush makeBox(const Vec3& mn, const Vec3& mx, int material) {
Brush b;
const Vec3 ns[6] = {{1, 0, 0}, {-1, 0, 0}, {0, 1, 0}, {0, -1, 0}, {0, 0, 1}, {0, 0, -1}};
const float ds[6] = {mx.x, -mn.x, mx.y, -mn.y, mx.z, -mn.z};
b.faces.resize(6);
for (int i = 0; i < 6; ++i) {
b.faces[(size_t)i].plane = Plane{ns[i], ds[i]};
b.faces[(size_t)i].material = material;
}
return b;
}
Brush makeWedge(const Vec3& mn, const Vec3& mx, int material) {
Brush b = makeBox(mn, mx, material);
Vec3 s(mx.x - mn.x, mx.y - mn.y, 0);
Vec3 n = normalize(Vec3(-s.y, s.x, 0));
b.faces[2].plane = Plane{n, dot(n, Vec3(mn.x, mn.y, 0))};
b.faces.erase(b.faces.begin() + 1); // -X face collapses to a line
return b;
}
static void addOutwardFace(Brush& b, Vec3 n, const Vec3& onPlane, const Vec3& inside, int material) {
n = normalize(n);
float d = dot(n, onPlane);
if (dot(n, inside) - d > 0) { n = -n; d = -d; }
Face f;
f.plane = Plane{n, d};
f.material = material;
b.faces.push_back(f);
}
Brush makeCylinder(const Vec3& mn, const Vec3& mx, int segs, int material) {
segs = std::max(3, std::min(segs, 64));
Vec3 c = (mn + mx) * 0.5f;
float rx = (mx.x - mn.x) * 0.5f, rz = (mx.z - mn.z) * 0.5f;
Brush b;
for (int i = 0; i < segs; ++i) {
float a0 = 2 * kPi * (float)i / (float)segs, a1 = 2 * kPi * (float)(i + 1) / (float)segs;
Vec3 p0(c.x + rx * std::cos(a0), mn.y, c.z + rz * std::sin(a0));
Vec3 p1(c.x + rx * std::cos(a1), mn.y, c.z + rz * std::sin(a1));
addOutwardFace(b, cross(Vec3(0, 1, 0), p1 - p0), p0, c, material);
}
addOutwardFace(b, {0, 1, 0}, Vec3(c.x, mx.y, c.z), c, material);
addOutwardFace(b, {0, -1, 0}, Vec3(c.x, mn.y, c.z), c, material);
return b;
}
Brush makeCone(const Vec3& mn, const Vec3& mx, int segs, int material) {
segs = std::max(3, std::min(segs, 64));
Vec3 c = (mn + mx) * 0.5f;
float rx = (mx.x - mn.x) * 0.5f, rz = (mx.z - mn.z) * 0.5f;
Vec3 apex(c.x, mx.y, c.z), inside(c.x, mn.y + 0.25f * (mx.y - mn.y), c.z);
Brush b;
for (int i = 0; i < segs; ++i) {
float a0 = 2 * kPi * (float)i / (float)segs, a1 = 2 * kPi * (float)(i + 1) / (float)segs;
Vec3 p0(c.x + rx * std::cos(a0), mn.y, c.z + rz * std::sin(a0));
Vec3 p1(c.x + rx * std::cos(a1), mn.y, c.z + rz * std::sin(a1));
addOutwardFace(b, cross(p1 - p0, apex - p0), p0, inside, material);
}
addOutwardFace(b, {0, -1, 0}, Vec3(c.x, mn.y, c.z), inside, material);
return b;
}
std::vector<Brush> makeStairs(const Vec3& mn, const Vec3& mx, int steps, int material) {
steps = std::max(1, std::min(steps, 128));
std::vector<Brush> out;
float dx = (mx.x - mn.x) / (float)steps, dy = (mx.y - mn.y) / (float)steps;
for (int i = 0; i < steps; ++i) {
float x0 = mn.x + dx * (float)i;
out.push_back(makeBox({x0, mn.y, mn.z}, {x0 + dx, mn.y + dy * (float)(i + 1), mx.z}, material));
}
return out;
}
// ============================================================ mesh primitives
namespace {
void addPoly(MeshObject& m, std::vector<uint32_t> idx, std::vector<Vec2> uv, int mat, uint32_t smooth) {
// collapse consecutive duplicates (degenerate quads at poles become triangles)
std::vector<uint32_t> i2;
std::vector<Vec2> u2;
bool hasUV = uv.size() == idx.size();
for (size_t k = 0; k < idx.size(); ++k) {
size_t prev = (k + idx.size() - 1) % idx.size();
if (idx[k] == idx[prev]) continue;
i2.push_back(idx[k]);
if (hasUV) u2.push_back(uv[k]);
}
if (i2.size() < 3) return;
MeshPoly p;
p.idx = std::move(i2);
p.uv = std::move(u2);
p.material = mat;
p.smooth = smooth;
m.polys.push_back(std::move(p));
}
} // namespace
MeshObject meshBox(const Vec3& mn, const Vec3& mx, int mat) {
MeshObject m;
m.verts = {{mn.x, mn.y, mn.z}, {mx.x, mn.y, mn.z}, {mx.x, mx.y, mn.z}, {mn.x, mx.y, mn.z},
{mn.x, mn.y, mx.z}, {mx.x, mn.y, mx.z}, {mx.x, mx.y, mx.z}, {mn.x, mx.y, mx.z}};
const uint32_t q[6][4] = {{1, 2, 6, 5}, {0, 4, 7, 3}, {3, 7, 6, 2}, {0, 1, 5, 4}, {4, 5, 6, 7}, {0, 3, 2, 1}};
for (auto& f : q) addPoly(m, {f[0], f[1], f[2], f[3]}, {}, mat, 0);
return m;
}
MeshObject meshCylinder(const Vec3& mn, const Vec3& mx, int segs, int mat) {
segs = std::max(3, std::min(segs, 128));
MeshObject m;
Vec3 c = (mn + mx) * 0.5f;
float rx = (mx.x - mn.x) * 0.5f, rz = (mx.z - mn.z) * 0.5f;
float circ = 2 * kPi * (rx + rz) * 0.5f, h = mx.y - mn.y;
for (int s = 0; s < segs; ++s) { // bottom ring [0,segs) then top ring [segs,2*segs)
float a = 2 * kPi * (float)s / (float)segs;
m.verts.push_back({c.x + rx * std::cos(a), mn.y, c.z + rz * std::sin(a)});
}
for (int s = 0; s < segs; ++s) {
float a = 2 * kPi * (float)s / (float)segs;
m.verts.push_back({c.x + rx * std::cos(a), mx.y, c.z + rz * std::sin(a)});
}
for (int s = 0; s < segs; ++s) {
uint32_t s0 = (uint32_t)s, s1 = (uint32_t)((s + 1) % segs);
float u0 = circ * (float)s / (float)segs / 64.0f, u1 = circ * (float)(s + 1) / (float)segs / 64.0f, v = h / 64.0f;
addPoly(m, {s1, s0, (uint32_t)segs + s0, (uint32_t)segs + s1}, {{u1, v}, {u0, v}, {u0, 0}, {u1, 0}}, mat, 1);
}
std::vector<uint32_t> top, bot;
for (int s = 0; s < segs; ++s) {
bot.push_back((uint32_t)s);
top.push_back((uint32_t)(2 * segs - 1 - s));
}
addPoly(m, top, {}, mat, 0);
addPoly(m, bot, {}, mat, 0);
return m;
}
MeshObject meshCone(const Vec3& mn, const Vec3& mx, int segs, int mat) {
segs = std::max(3, std::min(segs, 128));
MeshObject m;
Vec3 c = (mn + mx) * 0.5f;
float rx = (mx.x - mn.x) * 0.5f, rz = (mx.z - mn.z) * 0.5f;
float circ = 2 * kPi * (rx + rz) * 0.5f, h = mx.y - mn.y;
for (int s = 0; s < segs; ++s) {
float a = 2 * kPi * (float)s / (float)segs;
m.verts.push_back({c.x + rx * std::cos(a), mn.y, c.z + rz * std::sin(a)});
}
uint32_t apex = (uint32_t)m.verts.size();
m.verts.push_back({c.x, mx.y, c.z});
for (int s = 0; s < segs; ++s) {
uint32_t s0 = (uint32_t)s, s1 = (uint32_t)((s + 1) % segs);
float u0 = circ * (float)s / (float)segs / 64.0f, u1 = circ * (float)(s + 1) / (float)segs / 64.0f, v = h / 64.0f;
addPoly(m, {s1, s0, apex}, {{u1, v}, {u0, v}, {(u0 + u1) * 0.5f, 0}}, mat, 1);
}
std::vector<uint32_t> bot;
for (int s = 0; s < segs; ++s) bot.push_back((uint32_t)s);
addPoly(m, bot, {}, mat, 0);
return m;
}
MeshObject meshSphere(const Vec3& mn, const Vec3& mx, int rings, int segs, int mat) {
rings = std::max(2, std::min(rings, 128));
segs = std::max(3, std::min(segs, 128));
MeshObject m;
Vec3 c = (mn + mx) * 0.5f;
float rx = (mx.x - mn.x) * 0.5f, ry = (mx.y - mn.y) * 0.5f, rz = (mx.z - mn.z) * 0.5f;
float circ = 2 * kPi * (rx + rz) * 0.5f, arc = kPi * ry;
std::vector<std::vector<uint32_t>> ring((size_t)rings + 1);
for (int r = 0; r <= rings; ++r) {
float th = kPi * (float)r / (float)rings;
if (r == 0 || r == rings) {
uint32_t id = (uint32_t)m.verts.size();
m.verts.push_back({c.x, c.y + std::cos(th) * ry, c.z});
ring[(size_t)r].assign((size_t)segs + 1, id);
continue;
}
for (int s = 0; s < segs; ++s) {
float ph = 2 * kPi * (float)s / (float)segs;
ring[(size_t)r].push_back((uint32_t)m.verts.size());
m.verts.push_back({c.x + std::sin(th) * std::cos(ph) * rx, c.y + std::cos(th) * ry, c.z + std::sin(th) * std::sin(ph) * rz});
}
ring[(size_t)r].push_back(ring[(size_t)r][0]);
}
for (int r = 0; r < rings; ++r) {
for (int s = 0; s < segs; ++s) {
size_t R0 = (size_t)r, R1 = (size_t)r + 1, S0 = (size_t)s, S1 = (size_t)s + 1;
float u0 = circ * (float)s / (float)segs / 64.0f, u1 = circ * (float)(s + 1) / (float)segs / 64.0f;
float v0 = arc * (float)r / (float)rings / 64.0f, v1 = arc * (float)(r + 1) / (float)rings / 64.0f;
addPoly(m, {ring[R1][S1], ring[R1][S0], ring[R0][S0], ring[R0][S1]}, {{u1, v1}, {u0, v1}, {u0, v0}, {u1, v0}}, mat, 1);
}
}
return m;
}
MeshObject meshTorus(const Vec3& mn, const Vec3& mx, int segs, int sides, int mat) {
segs = std::max(3, std::min(segs, 128));
sides = std::max(3, std::min(sides, 64));
MeshObject m;
Vec3 c = (mn + mx) * 0.5f;
float outer = std::min(mx.x - mn.x, mx.z - mn.z) * 0.5f, r = (mx.y - mn.y) * 0.5f;
r = std::min(r, outer * 0.95f);
float R = outer - r;
for (int i = 0; i < segs; ++i) {
float ph = 2 * kPi * (float)i / (float)segs;
for (int j = 0; j < sides; ++j) {
float th = 2 * kPi * (float)j / (float)sides;
float rr = R + r * std::cos(th);
m.verts.push_back({c.x + rr * std::cos(ph), c.y + r * std::sin(th), c.z + rr * std::sin(ph)});
}
}
auto V = [&](int i, int j) { return (uint32_t)(((i % segs) * sides) + (j % sides)); };
float uc = 2 * kPi * R / 64.0f, vc = 2 * kPi * r / 64.0f;
for (int i = 0; i < segs; ++i) {
for (int j = 0; j < sides; ++j) {
float u0 = uc * (float)i / (float)segs, u1 = uc * (float)(i + 1) / (float)segs;
float v0 = vc * (float)j / (float)sides, v1 = vc * (float)(j + 1) / (float)sides;
addPoly(m, {V(i, j), V(i, j + 1), V(i + 1, j + 1), V(i + 1, j)}, {{u0, v0}, {u0, v1}, {u1, v1}, {u1, v0}}, mat, 1);
}
}
return m;
}
MeshObject meshGrid(const Vec3& mn, const Vec3& mx, int sx, int sz, int mat) {
sx = std::max(1, std::min(sx, 256));
sz = std::max(1, std::min(sz, 256));
MeshObject m;
float y = (mn.y + mx.y) * 0.5f;
for (int j = 0; j <= sz; ++j)
for (int i = 0; i <= sx; ++i)
m.verts.push_back({mn.x + (mx.x - mn.x) * (float)i / (float)sx, y, mn.z + (mx.z - mn.z) * (float)j / (float)sz});
auto V = [&](int i, int j) { return (uint32_t)(j * (sx + 1) + i); };
for (int j = 0; j < sz; ++j)
for (int i = 0; i < sx; ++i) {
uint32_t a = V(i, j), b = V(i, j + 1), c = V(i + 1, j + 1), d = V(i + 1, j);
std::vector<Vec2> uv;
for (uint32_t k : {a, b, c, d}) uv.push_back({m.verts[k].x / 64.0f, m.verts[k].z / 64.0f});
addPoly(m, {a, b, c, d}, uv, mat, 1);
}
return m;
}
} // namespace hmap
+410
View File
@@ -0,0 +1,410 @@
#include "hmap/hmap.h"
#include <cstring>
#include <fstream>
namespace hmap {
namespace {
class Writer {
public:
std::vector<uint8_t> buf;
void u8(uint8_t v) { buf.push_back(v); }
void u32(uint32_t v) { for (int i = 0; i < 4; ++i) buf.push_back((uint8_t)((v >> (8 * i)) & 0xff)); }
void i32(int32_t v) { u32((uint32_t)v); }
void f32(float f) { uint32_t u; std::memcpy(&u, &f, 4); u32(u); }
void str(const std::string& s) { u32((uint32_t)s.size()); buf.insert(buf.end(), s.begin(), s.end()); }
void vec2(const Vec2& v) { f32(v.x); f32(v.y); }
void vec3(const Vec3& v) { f32(v.x); f32(v.y); f32(v.z); }
void plane(const Plane& p) { vec3(p.n); f32(p.d); }
void aabb(const AABB& b) { vec3(b.mn); vec3(b.mx); }
void uv(const UVInfo& u) { vec2(u.offset); vec2(u.scale); f32(u.rotation); }
void raw(const char* tag4, const Writer& body) {
buf.insert(buf.end(), tag4, tag4 + 4);
u32((uint32_t)body.buf.size());
buf.insert(buf.end(), body.buf.begin(), body.buf.end());
}
};
class Reader {
public:
const uint8_t* p;
size_t n;
size_t pos = 0;
bool ok = true;
Reader(const uint8_t* data, size_t size) : p(data), n(size) {}
bool need(size_t k) {
if (!ok || pos + k > n) { ok = false; return false; }
return true;
}
uint8_t u8() { return need(1) ? p[pos++] : 0; }
uint32_t u32() {
if (!need(4)) return 0;
uint32_t v = (uint32_t)p[pos] | ((uint32_t)p[pos + 1] << 8) | ((uint32_t)p[pos + 2] << 16) | ((uint32_t)p[pos + 3] << 24);
pos += 4;
return v;
}
int32_t i32() { return (int32_t)u32(); }
float f32() { uint32_t u = u32(); float f; std::memcpy(&f, &u, 4); return f; }
std::string str() {
uint32_t len = u32();
if (!need(len)) return {};
std::string s((const char*)p + pos, len);
pos += len;
return s;
}
Vec2 vec2() { Vec2 v; v.x = f32(); v.y = f32(); return v; }
Vec3 vec3() { Vec3 v; v.x = f32(); v.y = f32(); v.z = f32(); return v; }
Plane plane() { Plane pl; pl.n = vec3(); pl.d = f32(); return pl; }
AABB aabb() { AABB b; b.mn = vec3(); b.mx = vec3(); return b; }
UVInfo uv() { UVInfo u; u.offset = vec2(); u.scale = vec2(); u.rotation = f32(); return u; }
// Reads an item count and rejects counts that cannot possibly fit in the remaining bytes.
uint32_t count(size_t minBytesPerItem) {
uint32_t c = u32();
if (ok && (size_t)c * minBytesPerItem > n - pos) ok = false;
return ok ? c : 0;
}
};
// ------------------------------------------------------------ writers
void writeMats(Writer& w, const std::vector<Material>& mats) {
w.u32((uint32_t)mats.size());
for (const Material& m : mats) {
w.str(m.name); w.str(m.texture); w.vec2(m.tileSize); w.vec3(m.color); w.u32(m.flags);
}
}
void writeBrushes(Writer& w, const std::vector<Brush>& bs) {
w.u32((uint32_t)bs.size());
for (const Brush& b : bs) {
w.u32(b.id); w.u32(b.owner); w.u32(b.contents); w.u32(b.flags);
w.u32((uint32_t)b.faces.size());
for (const Face& f : b.faces) { w.plane(f.plane); w.i32(f.material); w.uv(f.uv); w.u32(f.flags); }
}
}
void writeMeshes(Writer& w, const std::vector<MeshObject>& ms) {
w.u32((uint32_t)ms.size());
for (const MeshObject& m : ms) {
w.u32(m.id); w.u32(m.owner); w.u32(m.flags); w.str(m.name);
w.u32((uint32_t)m.verts.size());
for (const Vec3& v : m.verts) w.vec3(v);
w.u32((uint32_t)m.polys.size());
for (const MeshPoly& p : m.polys) {
w.i32(p.material); w.u32(p.smooth); w.uv(p.proj);
w.u32((uint32_t)p.idx.size());
for (uint32_t i : p.idx) w.u32(i);
w.u32((uint32_t)p.uv.size());
for (const Vec2& t : p.uv) w.vec2(t);
}
}
}
void writeEntities(Writer& w, const std::vector<Entity>& es) {
w.u32((uint32_t)es.size());
for (const Entity& e : es) {
w.u32(e.id); w.u32(e.flags); w.str(e.classname);
w.vec3(e.origin); w.vec3(e.angles); w.vec3(e.scale);
w.u32((uint32_t)e.props.size());
for (const KeyValue& kv : e.props) { w.str(kv.key); w.str(kv.value); }
}
}
void writeCollisionBrush(Writer& w, const CollisionBrush& b) {
w.u32(b.contents); w.u32(b.sourceId); w.aabb(b.bounds);
w.u32((uint32_t)b.planes.size());
for (const Plane& p : b.planes) w.plane(p);
}
void writeCompiledChunk(Writer& w, const CompiledMap& cm) {
w.vec3(cm.ambient); w.f32(cm.unitsPerMeter); w.u8(cm.baked ? 1 : 0); w.aabb(cm.bounds);
w.u32((uint32_t)cm.batches.size());
for (const Batch& b : cm.batches) {
w.i32(b.material); w.u8((uint8_t)b.source); w.i32(b.model);
w.u32((uint32_t)b.vertices.size());
for (const Vertex& v : b.vertices) { w.vec3(v.pos); w.vec3(v.normal); w.vec2(v.uv); w.u32(v.color); }
w.u32((uint32_t)b.indices.size());
for (uint32_t i : b.indices) w.u32(i);
}
w.u32((uint32_t)cm.models.size());
for (const CompiledModel& m : cm.models) {
w.u32(m.entityId); w.aabb(m.bounds);
w.u32((uint32_t)m.brushes.size());
for (const CollisionBrush& b : m.brushes) writeCollisionBrush(w, b);
}
const Bsp& t = cm.bsp;
w.i32(t.root); w.u32(t.depth); w.aabb(t.bounds);
w.u32((uint32_t)t.planes.size());
for (const Plane& p : t.planes) w.plane(p);
w.u32((uint32_t)t.nodes.size());
for (const BspNode& n : t.nodes) { w.i32(n.plane); w.i32(n.child[0]); w.i32(n.child[1]); }
w.u32((uint32_t)t.leaves.size());
for (const BspLeaf& l : t.leaves) { w.u32(l.firstBrush); w.u32(l.brushCount); }
w.u32((uint32_t)t.leafBrushes.size());
for (uint32_t i : t.leafBrushes) w.u32(i);
w.u32((uint32_t)t.brushes.size());
for (const CollisionBrush& b : t.brushes) writeCollisionBrush(w, b);
w.u32((uint32_t)cm.collMeshes.size());
for (const CollisionMesh& c : cm.collMeshes) {
w.u32(c.objectId); w.aabb(c.bounds);
w.u32((uint32_t)c.tris.size());
for (const Vec3& v : c.tris) w.vec3(v);
}
w.u32((uint32_t)cm.lights.size());
for (const Light& l : cm.lights) {
w.u8((uint8_t)l.type); w.vec3(l.pos); w.vec3(l.dir); w.vec3(l.color);
w.f32(l.intensity); w.f32(l.radius); w.f32(l.innerDeg); w.f32(l.outerDeg); w.u32(l.entityId);
}
}
// ------------------------------------------------------------ readers
void readMats(Reader& r, std::vector<Material>& out) {
uint32_t c = r.count(36);
out.resize(c);
for (Material& m : out) {
m.name = r.str(); m.texture = r.str(); m.tileSize = r.vec2(); m.color = r.vec3(); m.flags = r.u32();
}
}
void readBrushes(Reader& r, std::vector<Brush>& out) {
uint32_t c = r.count(20);
out.resize(c);
for (Brush& b : out) {
b.id = r.u32(); b.owner = r.u32(); b.contents = r.u32(); b.flags = r.u32();
uint32_t nf = r.count(44);
b.faces.resize(nf);
for (Face& f : b.faces) { f.plane = r.plane(); f.material = r.i32(); f.uv = r.uv(); f.flags = r.u32(); }
}
}
void readMeshes(Reader& r, std::vector<MeshObject>& out) {
uint32_t c = r.count(24);
out.resize(c);
for (MeshObject& m : out) {
m.id = r.u32(); m.owner = r.u32(); m.flags = r.u32(); m.name = r.str();
uint32_t nv = r.count(12);
m.verts.resize(nv);
for (Vec3& v : m.verts) v = r.vec3();
uint32_t np = r.count(32);
m.polys.resize(np);
for (MeshPoly& p : m.polys) {
p.material = r.i32(); p.smooth = r.u32(); p.proj = r.uv();
uint32_t ni = r.count(4);
p.idx.resize(ni);
for (uint32_t& i : p.idx) i = r.u32();
uint32_t nu = r.count(8);
p.uv.resize(nu);
for (Vec2& t : p.uv) t = r.vec2();
}
}
}
void readEntities(Reader& r, std::vector<Entity>& out) {
uint32_t c = r.count(52);
out.resize(c);
for (Entity& e : out) {
e.id = r.u32(); e.flags = r.u32(); e.classname = r.str();
e.origin = r.vec3(); e.angles = r.vec3(); e.scale = r.vec3();
uint32_t nk = r.count(8);
e.props.resize(nk);
for (KeyValue& kv : e.props) { kv.key = r.str(); kv.value = r.str(); }
}
}
void readCollisionBrush(Reader& r, CollisionBrush& b) {
b.contents = r.u32(); b.sourceId = r.u32(); b.bounds = r.aabb();
uint32_t n = r.count(16);
b.planes.resize(n);
for (Plane& p : b.planes) p = r.plane();
}
void readCompiledChunk(Reader& r, CompiledMap& cm) {
cm.ambient = r.vec3(); cm.unitsPerMeter = r.f32(); cm.baked = r.u8() != 0; cm.bounds = r.aabb();
uint32_t nb = r.count(17);
cm.batches.resize(nb);
for (Batch& b : cm.batches) {
b.material = r.i32(); b.source = (GeomSource)r.u8(); b.model = r.i32();
uint32_t nv = r.count(36);
b.vertices.resize(nv);
for (Vertex& v : b.vertices) { v.pos = r.vec3(); v.normal = r.vec3(); v.uv = r.vec2(); v.color = r.u32(); b.bounds.add(v.pos); }
uint32_t ni = r.count(4);
b.indices.resize(ni);
for (uint32_t& i : b.indices) { i = r.u32(); if (i >= nv) r.ok = false; }
}
uint32_t nm = r.count(40);
cm.models.resize(nm);
for (CompiledModel& m : cm.models) {
m.entityId = r.u32(); m.bounds = r.aabb();
uint32_t n = r.count(40);
m.brushes.resize(n);
for (CollisionBrush& b : m.brushes) readCollisionBrush(r, b);
}
Bsp& t = cm.bsp;
t.root = r.i32(); t.depth = r.u32(); t.bounds = r.aabb();
uint32_t np = r.count(16);
t.planes.resize(np);
for (Plane& p : t.planes) p = r.plane();
uint32_t nn = r.count(12);
t.nodes.resize(nn);
for (BspNode& n : t.nodes) { n.plane = r.i32(); n.child[0] = r.i32(); n.child[1] = r.i32(); }
uint32_t nl = r.count(8);
t.leaves.resize(nl);
for (BspLeaf& l : t.leaves) { l.firstBrush = r.u32(); l.brushCount = r.u32(); }
uint32_t nlb = r.count(4);
t.leafBrushes.resize(nlb);
for (uint32_t& i : t.leafBrushes) i = r.u32();
uint32_t nbr = r.count(40);
t.brushes.resize(nbr);
for (CollisionBrush& b : t.brushes) readCollisionBrush(r, b);
uint32_t ncm = r.count(32);
cm.collMeshes.resize(ncm);
for (CollisionMesh& c : cm.collMeshes) {
c.objectId = r.u32(); c.bounds = r.aabb();
uint32_t n = r.count(12);
c.tris.resize(n);
for (Vec3& v : c.tris) v = r.vec3();
}
uint32_t nlt = r.count(53);
cm.lights.resize(nlt);
for (Light& l : cm.lights) {
l.type = (LightType)r.u8(); l.pos = r.vec3(); l.dir = r.vec3(); l.color = r.vec3();
l.intensity = r.f32(); l.radius = r.f32(); l.innerDeg = r.f32(); l.outerDeg = r.f32(); l.entityId = r.u32();
}
// validate the tree so a corrupt file cannot make queries read out of bounds
if (!r.ok) return;
auto childOk = [&](int32_t c) { return c >= 0 ? (size_t)c < t.nodes.size() : (size_t)(~c) < t.leaves.size(); };
bool good = childOk(t.root) && !t.leaves.empty();
for (const BspNode& n : t.nodes)
good = good && n.plane >= 0 && (size_t)n.plane < t.planes.size() && childOk(n.child[0]) && childOk(n.child[1]);
for (const BspLeaf& l : t.leaves) good = good && (size_t)l.firstBrush + l.brushCount <= t.leafBrushes.size();
for (uint32_t i : t.leafBrushes) good = good && i < t.brushes.size();
if (!good) r.ok = false;
for (const Batch& b : cm.batches) cm.stats.triangles += (uint32_t)(b.indices.size() / 3), cm.stats.vertices += (uint32_t)b.vertices.size();
cm.stats.bspNodes = nn; cm.stats.bspLeaves = nl; cm.stats.bspDepth = t.depth; cm.stats.lights = nlt;
}
constexpr char kMagic[4] = {'H', 'M', 'A', 'P'};
Status writeFile(const std::string& path, const std::vector<uint8_t>& data) {
std::ofstream f(path, std::ios::binary | std::ios::trunc);
if (!f) return Status::Fail("cannot write " + path);
f.write((const char*)data.data(), (std::streamsize)data.size());
return f ? Status::Ok() : Status::Fail("write failed: " + path);
}
bool readFile(const std::string& path, std::vector<uint8_t>& out) {
std::ifstream f(path, std::ios::binary | std::ios::ate);
if (!f) return false;
std::streamsize sz = f.tellg();
if (sz < 0) return false;
f.seekg(0);
out.resize((size_t)sz);
if (sz > 0) f.read((char*)out.data(), sz);
return (bool)f;
}
Status parse(const uint8_t* data, size_t size, Map& map, CompiledMap* compiled, bool* hasCompiled) {
Reader hdr(data, size);
char magic[4];
for (char& c : magic) c = (char)hdr.u8();
uint32_t version = hdr.u32();
hdr.u32(); // flags (informational)
if (!hdr.ok || std::memcmp(magic, kMagic, 4) != 0) return Status::Fail("not an .hmap file");
if (version > kFormatVersion) return Status::Fail("file was written by a newer version of Hammer++");
size_t pos = hdr.pos;
*hasCompiled = false;
CompiledMap cm;
while (pos + 8 <= size) {
std::string tag((const char*)data + pos, 4);
Reader sz(data + pos + 4, 4);
uint32_t len = sz.u32();
pos += 8;
if ((size_t)len > size - pos) return Status::Fail("truncated chunk '" + tag + "'");
Reader r(data + pos, len);
if (tag == "META") {
map.name = r.str(); map.settings.ambient = r.vec3(); map.settings.gridSize = r.f32();
map.settings.unitsPerMeter = r.f32(); map.nextId = r.u32();
} else if (tag == "MATS") readMats(r, map.materials);
else if (tag == "BRSH") readBrushes(r, map.brushes);
else if (tag == "MESH") readMeshes(r, map.meshes);
else if (tag == "ENTS") readEntities(r, map.entities);
else if (tag == "CMPL") { readCompiledChunk(r, cm); *hasCompiled = r.ok; }
if (!r.ok) return Status::Fail("corrupt chunk '" + tag + "'");
pos += len;
}
// keep id allocation safe even when META is missing
for (const Brush& b : map.brushes) map.nextId = std::max(map.nextId, b.id + 1);
for (const MeshObject& m : map.meshes) map.nextId = std::max(map.nextId, m.id + 1);
for (const Entity& e : map.entities) map.nextId = std::max(map.nextId, e.id + 1);
if (*hasCompiled && compiled) {
cm.materials = map.materials;
cm.entities = map.entities;
*compiled = std::move(cm);
}
return Status::Ok();
}
} // namespace
std::vector<uint8_t> saveMapToMemory(const Map& map, const CompiledMap* compiled) {
Writer out;
out.buf.insert(out.buf.end(), kMagic, kMagic + 4);
out.u32(kFormatVersion);
out.u32(1u | (compiled ? 2u : 0u));
Writer meta;
meta.str(map.name); meta.vec3(map.settings.ambient); meta.f32(map.settings.gridSize);
meta.f32(map.settings.unitsPerMeter); meta.u32(map.nextId);
out.raw("META", meta);
Writer w;
writeMats(w, map.materials); out.raw("MATS", w);
Writer b; writeBrushes(b, map.brushes); out.raw("BRSH", b);
Writer m; writeMeshes(m, map.meshes); out.raw("MESH", m);
Writer e; writeEntities(e, map.entities); out.raw("ENTS", e);
if (compiled) { Writer c; writeCompiledChunk(c, *compiled); out.raw("CMPL", c); }
return std::move(out.buf);
}
Status loadMapFromMemory(const uint8_t* data, size_t size, Map& out, CompiledMap* compiled) {
Map tmp;
bool has = false;
Status s = parse(data, size, tmp, compiled, &has);
if (!s) return s;
out = std::move(tmp);
return Status::Ok();
}
Status saveMap(const std::string& path, const Map& map, const CompiledMap* compiled) {
return writeFile(path, saveMapToMemory(map, compiled));
}
Status loadMap(const std::string& path, Map& out, CompiledMap* compiled) {
std::vector<uint8_t> data;
if (!readFile(path, data)) return Status::Fail("cannot read " + path);
return loadMapFromMemory(data.data(), data.size(), out, compiled);
}
Status saveCompiled(const std::string& path, const CompiledMap& cm) {
Writer out;
out.buf.insert(out.buf.end(), kMagic, kMagic + 4);
out.u32(kFormatVersion);
out.u32(2u);
Writer w; writeMats(w, cm.materials); out.raw("MATS", w);
Writer e; writeEntities(e, cm.entities); out.raw("ENTS", e);
Writer c; writeCompiledChunk(c, cm); out.raw("CMPL", c);
return writeFile(path, out.buf);
}
Status loadCompiled(const std::string& path, CompiledMap& out) {
std::vector<uint8_t> data;
if (!readFile(path, data)) return Status::Fail("cannot read " + path);
Map map;
CompiledMap cm;
bool has = false;
Status s = parse(data.data(), data.size(), map, &cm, &has);
if (!s) return s;
out = has ? std::move(cm) : compile(map);
return Status::Ok();
}
} // namespace hmap
+106
View File
@@ -0,0 +1,106 @@
#include "hmap/hmap.h"
#include <cstdio>
#include <cstdlib>
namespace hmap {
const std::string* Entity::find(const std::string& key) const {
for (const auto& kv : props)
if (kv.key == key) return &kv.value;
return nullptr;
}
std::string Entity::get(const std::string& key, const std::string& def) const {
const std::string* v = find(key);
return v ? *v : def;
}
float Entity::getFloat(const std::string& key, float def) const {
const std::string* v = find(key);
if (!v || v->empty()) return def;
char* end = nullptr;
float f = std::strtof(v->c_str(), &end);
return end == v->c_str() ? def : f;
}
int Entity::getInt(const std::string& key, int def) const {
const std::string* v = find(key);
if (!v || v->empty()) return def;
char* end = nullptr;
long n = std::strtol(v->c_str(), &end, 10);
return end == v->c_str() ? def : (int)n;
}
bool Entity::getBool(const std::string& key, bool def) const {
const std::string* v = find(key);
if (!v || v->empty()) return def;
return *v != "0" && *v != "false" && *v != "no";
}
Vec3 Entity::getVec3(const std::string& key, const Vec3& def) const {
const std::string* v = find(key);
if (!v) return def;
float x, y, z;
if (std::sscanf(v->c_str(), "%f %f %f", &x, &y, &z) == 3) return {x, y, z};
return def;
}
Vec3 Entity::getColor(const std::string& key, const Vec3& def) const {
const std::string* v = find(key);
if (!v) return def;
float r, g, b;
if (std::sscanf(v->c_str(), "%f %f %f", &r, &g, &b) == 3) return {r / 255.0f, g / 255.0f, b / 255.0f};
return def;
}
void Entity::set(const std::string& key, const std::string& value) {
for (auto& kv : props)
if (kv.key == key) { kv.value = value; return; }
props.push_back({key, value});
}
void Entity::erase(const std::string& key) {
for (size_t i = 0; i < props.size(); ++i)
if (props[i].key == key) { props.erase(props.begin() + (long)i); return; }
}
Mat34 Entity::transform() const {
return Mat34::translate(origin) * Mat34::fromEuler(angles) * Mat34::scale(scale);
}
Vec3 Entity::forward() const { return Mat34::fromEuler(angles).vector({0, 0, -1}); }
int Map::findMaterial(const std::string& n) const {
for (size_t i = 0; i < materials.size(); ++i)
if (materials[i].name == n) return (int)i;
return -1;
}
int Map::addMaterial(const std::string& n, const std::string& texture) {
int i = findMaterial(n);
if (i >= 0) {
if (!texture.empty() && materials[(size_t)i].texture.empty()) materials[(size_t)i].texture = texture;
return i;
}
Material m;
m.name = n;
m.texture = texture;
materials.push_back(m);
return (int)materials.size() - 1;
}
template <class C>
static auto* findById(C& c, uint32_t id) {
for (auto& o : c)
if (o.id == id) return &o;
return (decltype(&c[0]))nullptr;
}
Brush* Map::findBrush(uint32_t id) { return findById(brushes, id); }
MeshObject* Map::findMesh(uint32_t id) { return findById(meshes, id); }
Entity* Map::findEntity(uint32_t id) { return findById(entities, id); }
const Brush* Map::findBrush(uint32_t id) const { return findById(brushes, id); }
const MeshObject* Map::findMesh(uint32_t id) const { return findById(meshes, id); }
const Entity* Map::findEntity(uint32_t id) const { return findById(entities, id); }
} // namespace hmap
+95
View File
@@ -0,0 +1,95 @@
#include "hmap/hmap.h"
namespace hmap {
void addDefaultMaterials(Map& map) {
struct D { const char* name; float r, g, b; };
const D defs[] = {
{"dev/grid", 0.86f, 0.86f, 0.88f}, {"dev/wall", 0.78f, 0.75f, 0.68f}, {"dev/orange", 1.0f, 0.55f, 0.15f},
{"dev/blue", 0.25f, 0.5f, 1.0f}, {"dev/green", 0.3f, 0.8f, 0.4f}, {"dev/red", 0.9f, 0.25f, 0.25f},
{"dev/gray", 0.5f, 0.5f, 0.52f}, {"dev/trigger", 1.0f, 0.4f, 0.1f}, {"dev/clip", 0.4f, 0.4f, 1.0f},
{"dev/water", 0.2f, 0.4f, 0.9f},
};
for (const D& d : defs) {
int i = map.addMaterial(d.name);
map.materials[(size_t)i].color = {d.r, d.g, d.b};
}
}
Map makeDemoMap() {
Map m;
m.name = "Hammer++ demo";
addDefaultMaterials(m);
EntityRegistry reg = EntityRegistry::builtin();
auto mat = [&](const char* n) { return m.findMaterial(n); };
auto addBrush = [&](Brush b) -> Brush& { b.id = m.newId(); m.brushes.push_back(std::move(b)); return m.brushes.back(); };
auto addMesh = [&](MeshObject o, const char* name) -> MeshObject& {
o.id = m.newId(); o.name = name; m.meshes.push_back(std::move(o)); return m.meshes.back();
};
auto addEnt = [&](const char* cls, Vec3 origin, Vec3 angles = {0, 0, 0}) -> Entity& {
Entity e; e.id = m.newId(); e.classname = cls; e.origin = origin; e.angles = angles;
reg.applyDefaults(e);
m.entities.push_back(std::move(e));
return m.entities.back();
};
// room: one big box, hollowed into six wall brushes (32 units thick)
Brush shell = makeBox({-544, -32, -544}, {544, 352, 544}, mat("dev/wall"));
for (Face& f : shell.faces) f.material = mat("dev/wall");
for (Brush& w : hollowBrush(shell, 32)) {
for (Face& f : w.faces)
if (f.plane.n.y > 0.9f && std::fabs(f.plane.d) < 0.5f) f.material = mat("dev/grid"); // floor surface (y = 0)
addBrush(w);
}
for (int sx = -1; sx <= 1; sx += 2)
for (int sz = -1; sz <= 1; sz += 2) {
float cx = 320.0f * (float)sx, cz = 320.0f * (float)sz;
addBrush(makeCylinder({cx - 40, 0, cz - 40}, {cx + 40, 320, cz + 40}, 12, mat("dev/orange")));
}
addBrush(makeBox({256, 0, -512}, {512, 64, -256}, mat("dev/blue")));
for (Brush& s : makeStairs({128, 0, -512}, {256, 64, -384}, 4, mat("dev/blue"))) addBrush(s);
addBrush(makeWedge({-256, 0, 256}, {-64, 48, 448}, mat("dev/green")));
addBrush(makeBox({64, 0, 128}, {128, 48, 192}, mat("dev/gray")));
// brush entities: a door (own model) and a trigger volume in front of it
Entity& door = addEnt("func_door", {0, 0, 0});
uint32_t doorId = door.id;
Brush db = makeBox({-64, 0, -208}, {64, 192, -192}, mat("dev/red"));
db.owner = doorId;
addBrush(db);
Entity& trig = addEnt("trigger_once", {0, 0, 0});
uint32_t trigId = trig.id;
Brush tb = makeBox({-96, 0, -176}, {96, 128, -96}, mat("dev/trigger"));
tb.owner = trigId;
tb.contents = CONTENTS_TRIGGER;
addBrush(tb);
// mesh-based geometry
addMesh(meshSphere({56, 48, 120}, {136, 128, 200}, 10, 16, mat("dev/orange")), "ball");
addMesh(meshTorus({-224, 96, -48}, {-128, 144, 48}, 24, 12, mat("dev/blue")), "ring");
MeshObject rug = meshGrid({160, 8, 96}, {416, 8, 352}, 16, 16, mat("dev/green"));
noiseMesh(rug, 10, 0.012f, 5);
addMesh(std::move(rug), "hill");
// entities
addEnt("info_player_start", {0, 36, 400});
for (int sx = -1; sx <= 1; sx += 2)
for (int sz = -1; sz <= 1; sz += 2) {
Entity& l = addEnt("light", {200.0f * (float)sx, 270, 200.0f * (float)sz});
l.set("radius", "700");
l.set("intensity", "0.85");
}
Entity& spot = addEnt("light_spot", {0, 300, -300}, {-90, 0, 0});
spot.set("intensity", "1.6");
addEnt("light_environment", {0, 340, 0}, {-50, 30, 0});
Entity& crate = addEnt("prop_static", {-400, 0, 160}, {0, 20, 0});
crate.set("model", "models/crate.obj");
Entity& crate2 = addEnt("prop_static", {-400, 0, 80}, {0, -10, 0});
crate2.set("model", "models/crate.obj");
Entity& pyr = addEnt("prop_static", {-300, 0, -380});
pyr.set("model", "models/pyramid.obj");
return m;
}
} // namespace hmap
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+30
View File
@@ -0,0 +1,30 @@
newmtl dev_grid
Kd 0.86 0.86 0.88
newmtl dev_wall
Kd 0.78 0.75 0.68
newmtl dev_orange
Kd 1 0.55 0.15
newmtl dev_blue
Kd 0.25 0.5 1
newmtl dev_green
Kd 0.3 0.8 0.4
newmtl dev_red
Kd 0.9 0.25 0.25
newmtl dev_gray
Kd 0.5 0.5 0.52
newmtl dev_trigger
Kd 1 0.4 0.1
newmtl dev_clip
Kd 0.4 0.4 1
newmtl dev_water
Kd 0.2 0.4 0.9
+56568
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
+19
View File
@@ -0,0 +1,19 @@
add_executable(test_hmap test_hmap.cpp)
target_link_libraries(test_hmap PRIVATE hmap)
add_test(NAME libhmap COMMAND test_hmap)
# Optional: exercises the editor's logic (commands, picking, camera maths, scripted mouse drags) without a GPU.
option(HPP_BUILD_EDITOR_LOGIC_TESTS "Headless editor logic tests (Linux only, uses API stubs)" OFF)
if(HPP_BUILD_EDITOR_LOGIC_TESTS AND CMAKE_SYSTEM_NAME STREQUAL "Linux")
add_executable(test_editor_logic
editor_headless/test_editor.cpp
editor_headless/impl.cpp
../editor/editor.cpp
../editor/render.cpp
../editor/viewport.cpp)
target_include_directories(test_editor_logic BEFORE PRIVATE editor_headless/stubs ../editor)
target_compile_definitions(test_editor_logic PRIVATE HPP_ASSET_DIR="assets")
target_link_libraries(test_editor_logic PRIVATE hmap)
target_link_options(test_editor_logic PRIVATE -Wl,--unresolved-symbols=ignore-all)
add_test(NAME editor_logic COMMAND test_editor_logic)
endif()
+12
View File
@@ -0,0 +1,12 @@
# Headless editor logic tests
Runs the editor's real command / picking / camera / tool code (`editor.cpp`, `viewport.cpp`, `render.cpp`)
against **no-op raylib and Dear ImGui back-ends** (`stubs/` + `impl.cpp`), driving it with a scripted mouse.
No GPU or window needed, so it also works in CI.
cmake -B build -DHPP_BUILD_EDITOR_LOGIC_TESTS=ON && cmake --build build --target test_editor_logic
ctest --test-dir build -R editor_logic --output-on-failure
Linux + GNU ld/lld only (it links with `--unresolved-symbols=ignore-all` so UI-only code paths may stay
unresolved). The stubs mirror the raylib 5.5 / ImGui 1.91 signatures the editor uses; they are not a
re-implementation - they only exist so this code can be compiled and exercised without the real libraries.
+24
View File
@@ -0,0 +1,24 @@
// No-op raylib / ImGui back-ends + scripted mouse state so the editor's logic can run headless.
#include <cstdarg>
#include <cstdlib>
#include <cstdio>
#include "raylib.h"
#include "imgui.h"
#include "rlgl.h"
void *MemAlloc(unsigned int size) { return calloc(1, size); }
void MemFree(void *p) { free(p); }
void UploadMesh(Mesh *, bool) {}
void UnloadMesh(Mesh m) { MemFree(m.vertices); MemFree(m.texcoords); MemFree(m.normals); MemFree(m.colors); }
bool FileExists(const char *) { return false; }
bool DirectoryExists(const char *) { return false; }
void TraceLog(int, const char *, ...) {}
static ImGuiIO g_io;
bool g_down[3], g_clicked[3], g_released[3];
ImGuiIO &ImGui::GetIO() { return g_io; }
bool ImGui::IsMouseDown(ImGuiMouseButton b) { return g_down[b]; }
bool ImGui::IsMouseClicked(ImGuiMouseButton b, bool) { return g_clicked[b]; }
bool ImGui::IsMouseReleased(ImGuiMouseButton b) { return g_released[b]; }
bool ImGui::IsKeyDown(ImGuiKey) { return false; }
bool ImGui::IsKeyPressed(ImGuiKey, bool) { return false; }
+98
View File
@@ -0,0 +1,98 @@
// Minimal stand-in for Dear ImGui 1.91 docking (declarations only)
#pragma once
#include <stddef.h>
#define IMGUI_VERSION_NUM 19100
#ifndef STUB_NO_DOCK
#define IMGUI_HAS_DOCK 1
#endif
#define IM_ARRAYSIZE(_ARR) ((int)(sizeof(_ARR) / sizeof(*(_ARR))))
#define IM_COL32(R, G, B, A) (((unsigned)(A) << 24) | ((unsigned)(B) << 16) | ((unsigned)(G) << 8) | ((unsigned)(R)))
typedef unsigned int ImGuiID; typedef unsigned int ImU32;
typedef int ImGuiCol, ImGuiCond, ImGuiDir, ImGuiKey, ImGuiMouseButton, ImGuiStyleVar, ImGuiButtonFlags, ImGuiColorEditFlags, ImGuiConfigFlags,
ImGuiInputTextFlags, ImGuiSelectableFlags, ImGuiTreeNodeFlags, ImGuiWindowFlags, ImGuiChildFlags, ImGuiDockNodeFlags, ImGuiComboFlags,
ImGuiHoveredFlags, ImGuiSliderFlags, ImGuiPopupFlags, ImGuiTabBarFlags, ImGuiTabItemFlags;
struct ImGuiInputTextCallbackData; typedef int (*ImGuiInputTextCallback)(ImGuiInputTextCallbackData *data);
struct ImVec2 { float x, y; constexpr ImVec2() : x(0), y(0) {} constexpr ImVec2(float _x, float _y) : x(_x), y(_y) {} };
struct ImVec4 { float x, y, z, w; constexpr ImVec4() : x(0), y(0), z(0), w(0) {} constexpr ImVec4(float _x, float _y, float _z, float _w) : x(_x), y(_y), z(_z), w(_w) {} };
enum { ImGuiCol_Text, ImGuiCol_TextDisabled, ImGuiCol_WindowBg, ImGuiCol_ChildBg, ImGuiCol_PopupBg, ImGuiCol_Border, ImGuiCol_BorderShadow,
ImGuiCol_FrameBg, ImGuiCol_FrameBgHovered, ImGuiCol_FrameBgActive, ImGuiCol_TitleBg, ImGuiCol_TitleBgActive, ImGuiCol_TitleBgCollapsed,
ImGuiCol_MenuBarBg, ImGuiCol_ScrollbarBg, ImGuiCol_ScrollbarGrab, ImGuiCol_CheckMark, ImGuiCol_SliderGrab, ImGuiCol_SliderGrabActive,
ImGuiCol_Button, ImGuiCol_ButtonHovered, ImGuiCol_ButtonActive, ImGuiCol_Header, ImGuiCol_HeaderHovered, ImGuiCol_HeaderActive,
ImGuiCol_Separator, ImGuiCol_ResizeGrip, ImGuiCol_ResizeGripHovered, ImGuiCol_Tab, ImGuiCol_TabHovered, ImGuiCol_TabSelected,
ImGuiCol_DockingPreview, ImGuiCol_COUNT };
enum { ImGuiStyleVar_WindowPadding, ImGuiStyleVar_WindowRounding, ImGuiStyleVar_WindowBorderSize };
enum { ImGuiWindowFlags_None = 0, ImGuiWindowFlags_NoTitleBar = 1, ImGuiWindowFlags_NoResize = 2, ImGuiWindowFlags_NoMove = 4, ImGuiWindowFlags_NoScrollbar = 8,
ImGuiWindowFlags_NoScrollWithMouse = 16, ImGuiWindowFlags_NoCollapse = 32, ImGuiWindowFlags_AlwaysAutoResize = 64, ImGuiWindowFlags_MenuBar = 1 << 10,
ImGuiWindowFlags_NoBringToFrontOnFocus = 1 << 13, ImGuiWindowFlags_NoNavFocus = 1 << 19, ImGuiWindowFlags_NoDocking = 1 << 21 };
enum { ImGuiCond_Appearing = 8, ImGuiCond_FirstUseEver = 4 };
enum { ImGuiDir_Left = 0, ImGuiDir_Right = 1, ImGuiDir_Up = 2, ImGuiDir_Down = 3 };
enum { ImGuiDockNodeFlags_None = 0 };
enum { ImGuiConfigFlags_DockingEnable = 1 << 7 };
enum { ImGuiMouseButton_Left = 0, ImGuiMouseButton_Right = 1, ImGuiMouseButton_Middle = 2 };
enum { ImGuiButtonFlags_MouseButtonLeft = 1, ImGuiButtonFlags_MouseButtonRight = 2, ImGuiButtonFlags_MouseButtonMiddle = 4 };
enum { ImGuiColorEditFlags_NoInputs = 1, ImGuiColorEditFlags_NoTooltip = 2, ImGuiColorEditFlags_NoPicker = 4, ImGuiColorEditFlags_PickerHueWheel = 8 };
enum { ImGuiInputTextFlags_EnterReturnsTrue = 32 };
enum { ImGuiSelectableFlags_AllowDoubleClick = 4 };
enum { ImGuiTreeNodeFlags_DefaultOpen = 32 };
enum { ImGuiKey_None = 0, ImGuiKey_Tab = 512, ImGuiKey_Delete, ImGuiKey_Enter, ImGuiKey_Escape, ImGuiKey_LeftBracket, ImGuiKey_RightBracket,
ImGuiKey_1, ImGuiKey_2, ImGuiKey_3, ImGuiKey_4, ImGuiKey_A, ImGuiKey_D, ImGuiKey_E, ImGuiKey_F, ImGuiKey_G, ImGuiKey_H, ImGuiKey_N, ImGuiKey_O,
ImGuiKey_Q, ImGuiKey_S, ImGuiKey_T, ImGuiKey_W, ImGuiKey_Y, ImGuiKey_Z, ImGuiKey_F1, ImGuiKey_F9 };
struct ImGuiIO {
ImGuiConfigFlags ConfigFlags; const char *IniFilename; bool ConfigWindowsMoveFromTitleBarOnly; float DeltaTime;
ImVec2 MousePos, MouseDelta; float MouseWheel; bool MouseDown[5]; bool KeyCtrl, KeyShift, KeyAlt, KeySuper; bool WantTextInput;
};
struct ImGuiStyle {
float WindowRounding, ChildRounding, FrameRounding, PopupRounding, ScrollbarRounding, GrabRounding, TabRounding, WindowBorderSize, FrameBorderSize, ScrollbarSize;
ImVec2 WindowPadding, FramePadding, ItemSpacing; ImVec4 Colors[ImGuiCol_COUNT];
};
struct ImGuiViewport { ImVec2 WorkPos, WorkSize; };
struct ImDrawList { void AddText(const ImVec2 &pos, ImU32 col, const char *text_begin, const char *text_end = nullptr); };
struct ImGuiTextFilter { bool Draw(const char *label = "Filter (inc,-exc)", float width = 0.0f); bool PassFilter(const char *text, const char *text_end = nullptr) const; bool IsActive() const; };
struct ImGuiListClipper { int DisplayStart, DisplayEnd; void Begin(int items_count, float items_height = -1.0f); bool Step(); };
namespace ImGui {
ImGuiIO &GetIO(); ImGuiStyle &GetStyle(); ImGuiViewport *GetMainViewport();
bool Begin(const char *name, bool *p_open = nullptr, ImGuiWindowFlags flags = 0); void End();
bool BeginChild(const char *str_id, const ImVec2 &size = ImVec2(0, 0), ImGuiChildFlags child_flags = 0, ImGuiWindowFlags window_flags = 0); void EndChild();
void SetNextWindowPos(const ImVec2 &pos, ImGuiCond cond = 0, const ImVec2 &pivot = ImVec2(0, 0));
void SetNextWindowSize(const ImVec2 &size, ImGuiCond cond = 0);
ImVec2 GetContentRegionAvail(); ImDrawList *GetWindowDrawList(); float GetWindowWidth();
float GetScrollY(); float GetScrollMaxY(); void SetScrollHereY(float center_y_ratio = 0.5f);
void PushStyleColor(ImGuiCol idx, ImU32 col); void PushStyleColor(ImGuiCol idx, const ImVec4 &col); void PopStyleColor(int count = 1);
void PushStyleVar(ImGuiStyleVar idx, float val); void PushStyleVar(ImGuiStyleVar idx, const ImVec2 &val); void PopStyleVar(int count = 1);
const ImVec4 &GetStyleColorVec4(ImGuiCol idx);
void SetNextItemWidth(float item_width);
void Separator(); void SameLine(float offset_from_start_x = 0.0f, float spacing = -1.0f); void Spacing(); void BeginGroup(); void EndGroup();
void SetCursorPos(const ImVec2 &local_pos); void SetCursorScreenPos(const ImVec2 &pos); float GetFrameHeightWithSpacing();
void Text(const char *fmt, ...); void TextColored(const ImVec4 &col, const char *fmt, ...); void TextDisabled(const char *fmt, ...);
void TextWrapped(const char *fmt, ...); void TextUnformatted(const char *text, const char *text_end = nullptr); void BulletText(const char *fmt, ...);
void SeparatorText(const char *label);
bool Button(const char *label, const ImVec2 &size = ImVec2(0, 0)); bool SmallButton(const char *label);
bool InvisibleButton(const char *str_id, const ImVec2 &size, ImGuiButtonFlags flags = 0);
bool Checkbox(const char *label, bool *v);
bool Combo(const char *label, int *current_item, const char *const items[], int items_count, int popup_max_height_in_items = -1);
bool Combo(const char *label, int *current_item, const char *items_separated_by_zeros, int popup_max_height_in_items = -1);
bool DragFloat(const char *label, float *v, float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char *format = "%.3f", ImGuiSliderFlags flags = 0);
bool DragFloat2(const char *label, float v[2], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char *format = "%.3f", ImGuiSliderFlags flags = 0);
bool DragFloat3(const char *label, float v[3], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char *format = "%.3f", ImGuiSliderFlags flags = 0);
bool DragInt(const char *label, int *v, float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char *format = "%d", ImGuiSliderFlags flags = 0);
bool SliderInt(const char *label, int *v, int v_min, int v_max, const char *format = "%d", ImGuiSliderFlags flags = 0);
bool InputInt(const char *label, int *v, int step = 1, int step_fast = 100, ImGuiInputTextFlags flags = 0);
bool ColorEdit3(const char *label, float col[3], ImGuiColorEditFlags flags = 0);
bool ColorButton(const char *desc_id, const ImVec4 &col, ImGuiColorEditFlags flags = 0, const ImVec2 &size = ImVec2(0, 0));
bool BeginCombo(const char *label, const char *preview_value, ImGuiComboFlags flags = 0); void EndCombo();
bool Selectable(const char *label, bool selected = false, ImGuiSelectableFlags flags = 0, const ImVec2 &size = ImVec2(0, 0));
bool CollapsingHeader(const char *label, ImGuiTreeNodeFlags flags = 0); bool TreeNode(const char *label); void TreePop();
bool BeginMenuBar(); void EndMenuBar(); bool BeginMenu(const char *label, bool enabled = true); void EndMenu();
bool MenuItem(const char *label, const char *shortcut = nullptr, bool selected = false, bool enabled = true);
bool MenuItem(const char *label, const char *shortcut, bool *p_selected, bool enabled = true);
void SetTooltip(const char *fmt, ...);
bool BeginPopupModal(const char *name, bool *p_open = nullptr, ImGuiWindowFlags flags = 0); void EndPopup(); void OpenPopup(const char *str_id, ImGuiPopupFlags popup_flags = 0);
void CloseCurrentPopup(); bool BeginPopupContextItem(const char *str_id = nullptr, ImGuiPopupFlags popup_flags = 1);
bool BeginTabBar(const char *str_id, ImGuiTabBarFlags flags = 0); void EndTabBar(); bool BeginTabItem(const char *label, bool *p_open = nullptr, ImGuiTabItemFlags flags = 0); void EndTabItem();
void PushID(const char *str_id); void PushID(const char *str_id_begin, const char *str_id_end); void PushID(const void *ptr_id); void PushID(int int_id); void PopID(); ImGuiID GetID(const char *str_id);
bool IsItemHovered(ImGuiHoveredFlags flags = 0); bool IsItemActive(); bool IsAnyItemActive(); ImVec2 GetItemRectMin();
bool IsKeyDown(ImGuiKey key); bool IsKeyPressed(ImGuiKey key, bool repeat = true);
bool IsMouseDown(ImGuiMouseButton button); bool IsMouseClicked(ImGuiMouseButton button, bool repeat = false); bool IsMouseReleased(ImGuiMouseButton button); bool IsMouseDoubleClicked(ImGuiMouseButton button);
void ShowDemoWindow(bool *p_open = nullptr);
ImGuiID DockSpace(ImGuiID dockspace_id, const ImVec2 &size = ImVec2(0, 0), ImGuiDockNodeFlags flags = 0, const void *window_class = nullptr);
}
@@ -0,0 +1,13 @@
#pragma once
#include "imgui.h"
struct ImGuiDockNode;
enum ImGuiDockNodeFlagsPrivate_ { ImGuiDockNodeFlags_DockSpace = 1 << 10 };
namespace ImGui {
ImGuiDockNode *DockBuilderGetNode(ImGuiID node_id);
void DockBuilderRemoveNode(ImGuiID node_id);
ImGuiID DockBuilderAddNode(ImGuiID node_id = 0, ImGuiDockNodeFlags flags = 0);
void DockBuilderSetNodeSize(ImGuiID node_id, ImVec2 size);
ImGuiID DockBuilderSplitNode(ImGuiID node_id, ImGuiDir split_dir, float size_ratio_for_node_at_dir, ImGuiID *out_id_at_dir, ImGuiID *out_id_at_opposite_dir);
void DockBuilderDockWindow(const char *window_name, ImGuiID node_id);
void DockBuilderFinish(ImGuiID node_id);
}
@@ -0,0 +1,7 @@
#pragma once
#include <string>
#include "imgui.h"
namespace ImGui {
bool InputText(const char *label, std::string *str, ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = nullptr, void *user_data = nullptr);
bool InputTextWithHint(const char *label, const char *hint, std::string *str, ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = nullptr, void *user_data = nullptr);
}
+103
View File
@@ -0,0 +1,103 @@
// Minimal stand-in for raylib 5.5 (declarations only; used for -fsyntax-only checking)
#pragma once
#include <stdbool.h>
#include <stdarg.h>
typedef struct Vector2 { float x, y; } Vector2;
typedef struct Vector3 { float x, y, z; } Vector3;
typedef struct Vector4 { float x, y, z, w; } Vector4;
typedef struct Matrix { float m0, m4, m8, m12, m1, m5, m9, m13, m2, m6, m10, m14, m3, m7, m11, m15; } Matrix;
typedef struct Color { unsigned char r, g, b, a; } Color;
typedef struct Rectangle { float x, y, width, height; } Rectangle;
typedef struct Image { void *data; int width, height, mipmaps, format; } Image;
typedef struct Texture { unsigned int id; int width, height, mipmaps, format; } Texture;
typedef Texture Texture2D;
typedef struct RenderTexture { unsigned int id; Texture texture; Texture depth; } RenderTexture;
typedef RenderTexture RenderTexture2D;
typedef struct Camera3D { Vector3 position, target, up; float fovy; int projection; } Camera3D;
typedef Camera3D Camera;
typedef struct BoundingBox { Vector3 min, max; } BoundingBox;
typedef struct Shader { unsigned int id; int *locs; } Shader;
typedef struct MaterialMap { Texture2D texture; Color color; float value; } MaterialMap;
typedef struct Material { Shader shader; MaterialMap *maps; float params[4]; } Material;
typedef struct Mesh {
int vertexCount, triangleCount;
float *vertices, *texcoords, *texcoords2, *normals, *tangents;
unsigned char *colors; unsigned short *indices;
float *animVertices, *animNormals; unsigned char *boneIds; float *boneWeights; Matrix *boneMatrices; int boneCount;
unsigned int vaoId; unsigned int *vboId;
} Mesh;
typedef struct Model {
Matrix transform; int meshCount, materialCount; Mesh *meshes; Material *materials; int *meshMaterial;
int boneCount; void *bones; void *bindPose;
} Model;
#define WHITE Color{255,255,255,255}
#define YELLOW Color{253,249,0,255}
#define SKYBLUE Color{102,191,255,255}
#define RAYWHITE Color{245,245,245,255}
#define RED Color{230,41,55,255}
typedef enum { FLAG_VSYNC_HINT = 0x40, FLAG_WINDOW_RESIZABLE = 0x04, FLAG_MSAA_4X_HINT = 0x20 } ConfigFlags;
typedef enum { LOG_ALL = 0, LOG_INFO = 3 } TraceLogLevel;
typedef enum { KEY_NULL = 0, KEY_W = 87, KEY_A = 65, KEY_S = 83, KEY_D = 68, KEY_LEFT_SHIFT = 340 } KeyboardKey;
typedef enum { MOUSE_BUTTON_LEFT = 0, MOUSE_BUTTON_RIGHT = 1 } MouseButton;
typedef enum { CAMERA_PERSPECTIVE = 0, CAMERA_ORTHOGRAPHIC } CameraProjection;
typedef enum { MATERIAL_MAP_ALBEDO = 0, MATERIAL_MAP_DIFFUSE = 0 } MaterialMapIndex;
typedef enum { SHADER_UNIFORM_FLOAT = 0, SHADER_UNIFORM_VEC2, SHADER_UNIFORM_VEC3, SHADER_UNIFORM_VEC4, SHADER_UNIFORM_INT } ShaderUniformDataType;
typedef enum { TEXTURE_FILTER_POINT = 0, TEXTURE_FILTER_BILINEAR, TEXTURE_FILTER_TRILINEAR } TextureFilter;
typedef enum { TEXTURE_WRAP_REPEAT = 0 } TextureWrap;
void InitWindow(int width, int height, const char *title);
void CloseWindow(void);
bool WindowShouldClose(void);
void SetConfigFlags(unsigned int flags);
void SetWindowMinSize(int width, int height);
void SetExitKey(int key);
void BeginDrawing(void); void EndDrawing(void);
void ClearBackground(Color color);
void BeginMode3D(Camera3D camera); void EndMode3D(void);
void BeginTextureMode(RenderTexture2D target); void EndTextureMode(void);
RenderTexture2D LoadRenderTexture(int width, int height);
void UnloadRenderTexture(RenderTexture2D target);
Shader LoadShaderFromMemory(const char *vsCode, const char *fsCode);
void UnloadShader(Shader shader);
int GetShaderLocation(Shader shader, const char *uniformName);
void SetShaderValue(Shader shader, int locIndex, const void *value, int uniformType);
void SetShaderValueV(Shader shader, int locIndex, const void *value, int uniformType, int count);
Material LoadMaterialDefault(void);
Model LoadModel(const char *fileName);
void UnloadModel(Model model);
BoundingBox GetModelBoundingBox(Model model);
void UploadMesh(Mesh *mesh, bool dynamic);
void UnloadMesh(Mesh mesh);
void DrawMesh(Mesh mesh, Material material, Matrix transform);
Texture2D LoadTexture(const char *fileName);
Texture2D LoadTextureFromImage(Image image);
void UnloadTexture(Texture2D texture);
void UnloadImage(Image image);
Image GenImageChecked(int width, int height, int checksX, int checksY, Color col1, Color col2);
void GenTextureMipmaps(Texture2D *texture);
void SetTextureFilter(Texture2D texture, int filter);
void SetTextureWrap(Texture2D texture, int wrap);
bool FileExists(const char *fileName);
bool DirectoryExists(const char *dirPath);
const char *GetApplicationDirectory(void);
char *LoadFileText(const char *fileName);
void UnloadFileText(char *text);
void TraceLog(int logLevel, const char *text, ...);
void *MemAlloc(unsigned int size);
void MemFree(void *ptr);
void DrawLine(int startPosX, int startPosY, int endPosX, int endPosY, Color color);
void DrawCircle(int centerX, int centerY, float radius, Color color);
void DrawRectangle(int posX, int posY, int width, int height, Color color);
void DrawRectangleLines(int posX, int posY, int width, int height, Color color);
void DrawText(const char *text, int posX, int posY, int fontSize, Color color);
void DrawTriangle3D(Vector3 v1, Vector3 v2, Vector3 v3, Color color);
void DrawCube(Vector3 position, float width, float height, float length, Color color);
void DrawCubeWires(Vector3 position, float width, float height, float length, Color color);
void DrawCylinderEx(Vector3 startPos, Vector3 endPos, float startRadius, float endRadius, int sides, Color color);
void DrawSphere(Vector3 centerPos, float radius, Color color);
void SetTargetFPS(int fps);
float GetFrameTime(void);
int GetFPS(void);
Vector2 GetMouseDelta(void);
bool IsMouseButtonDown(int button);
bool IsKeyDown(int key);
const char *TextFormat(const char *text, ...);
+4
View File
@@ -0,0 +1,4 @@
#pragma once
#include "raylib.h"
Matrix MatrixIdentity(void);
Matrix MatrixMultiply(Matrix left, Matrix right);
+6
View File
@@ -0,0 +1,6 @@
#pragma once
#include "raylib.h"
#include "imgui.h"
void rlImGuiSetup(bool darkTheme); void rlImGuiBegin(void); void rlImGuiEnd(void); void rlImGuiShutdown(void);
void rlImGuiImageRenderTexture(const RenderTexture *image);
bool rlImGuiImageButtonSize(const char *name, const Texture *image, ImVec2 size);
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include "raylib.h"
#define RL_LINES 0x0001
void rlBegin(int mode); void rlEnd(void);
void rlColor4ub(unsigned char r, unsigned char g, unsigned char b, unsigned char a);
void rlVertex3f(float x, float y, float z);
void rlEnableDepthTest(void); void rlDisableDepthTest(void);
void rlEnableBackfaceCulling(void); void rlDisableBackfaceCulling(void);
void rlDrawRenderBatchActive(void);
void rlSetClipPlanes(double nearPlane, double farPlane);
unsigned int rlGetShaderIdDefault(void);
int *rlGetShaderLocsDefault(void);
+324
View File
@@ -0,0 +1,324 @@
#include <cmath>
#include <cstdio>
#include <cstring>
#include "editor.h"
using namespace hpp;
extern bool g_down[3], g_clicked[3], g_released[3];
static int fails = 0, checks = 0;
#define CHECK(c) do { ++checks; if (!(c)) { ++fails; std::printf(" FAIL line %d: %s\n", __LINE__, #c); } } while (0)
#define NEAR(a, b, eps) CHECK(std::fabs((double)(a) - (double)(b)) < (eps))
static void setupViews(Editor& ed) {
const ViewKind kinds[4] = {ViewKind::Persp, ViewKind::Top, ViewKind::Front, ViewKind::Side};
for (int i = 0; i < 4; ++i) { ed.views[i].kind = kinds[i]; ed.views[i].w = 800; ed.views[i].h = 600; ed.views[i].hovered = true; ed.views[i].zoom = 1.0f; }
ed.views[0].pos = {0, 150, 400}; ed.views[0].yaw = 0; ed.views[0].pitch = -15;
}
// emulate one UI frame of left-mouse state, then feed the 2D/3D tool handler
static void frame(Editor& ed, Viewport& v, float mx, float my, int phase /*0 press,1 hold,2 release,3 idle*/) {
for (int b = 0; b < 3; ++b) g_down[b] = g_clicked[b] = g_released[b] = false;
if (phase == 0) { g_clicked[0] = true; g_down[0] = true; }
if (phase == 1) g_down[0] = true;
if (phase == 2) g_released[0] = true;
ImGuiIO& io = ImGui::GetIO();
io.MousePos = ImVec2(mx + v.imgMin.x, my + v.imgMin.y);
if (v.ortho()) ed.toolInput2D(v, ImVec2(mx, my)); else ed.toolInput3D(v, ImVec2(mx, my));
}
static void drag2D(Editor& ed, Viewport& v, float x0, float y0, float x1, float y1) {
frame(ed, v, x0, y0, 0); frame(ed, v, (x0 + x1) / 2, (y0 + y1) / 2, 1); frame(ed, v, x1, y1, 1); frame(ed, v, x1, y1, 2);
}
static hmap::AABB firstBrushBounds(Editor& ed) { return hmap::buildBrushGeometry(ed.doc.brushes[0]).bounds; }
static void kill(Editor* e) { e->renderer.setEditBatches({}); e->renderer.setCompiledBatches({}); delete e; }
static Editor* make() {
Editor* ed = new Editor();
ed->entdefs = hmap::EntityRegistry::builtin();
ed->newMap(false);
ed->doc.brushes.clear(); ed->doc.entities.clear(); ed->doc.meshes.clear(); ed->touch();
setupViews(*ed);
return ed;
}
int main() {
std::puts("camera math");
{
Editor* ed = make();
uint32_t seed = 9;
auto rnd = [&]() { seed = seed * 1664525u + 1013904223u; return (float)((seed >> 8) & 0xffff) / 65535.0f * 600.0f - 300.0f; };
ed->views[1].center = {20, 5, -40}; ed->views[2].center = {-30, 10, 15}; ed->views[3].center = {5, -20, 60};
ed->views[0].pos = {50, 120, 380}; ed->views[0].yaw = 33; ed->views[0].pitch = -21;
for (int vi = 0; vi < 4; ++vi) {
Viewport& v = ed->views[vi];
for (int i = 0; i < 200; ++i) {
Vec3 p(rnd() * 0.3f, rnd() * 0.3f, rnd() * 0.3f - (vi == 0 ? 100.0f : 0.0f));
float sx, sy;
if (!v.project(p, sx, sy)) continue;
HRay r = v.ray(sx, sy);
Vec3 d = p - r.o;
float t = hmap::dot(d, r.d);
Vec3 closest = r.o + r.d * t;
CHECK(hmap::length(closest - p) < 0.05f);
if (v.ortho()) {
Vec3 q = v.planePoint(sx, sy, p[v.depthAxis()]);
CHECK(hmap::length(q - p) < 0.01f);
}
}
}
// handedness: world +X is to the right of the screen centre in Top/Front, -Z (up) in Top
float ax, ay, bx, by;
ed->views[1].project(ed->views[1].center, ax, ay);
ed->views[1].project(ed->views[1].center + Vec3(10, 0, 0), bx, by);
CHECK(bx > ax); NEAR(by, ay, 1e-3);
ed->views[1].project(ed->views[1].center + Vec3(0, 0, -10), bx, by);
CHECK(by < ay); // -Z is up on screen in the top view
ed->views[2].project(ed->views[2].center + Vec3(0, 10, 0), bx, by);
ed->views[2].project(ed->views[2].center, ax, ay);
CHECK(by < ay); // +Y is up on screen in the front view
kill(ed);
}
std::puts("commands: create / undo / hollow / clip / tie / duplicate / delete");
{
Editor* ed = make();
ed->shape = Shape::Box; ed->createAsMesh = false;
ed->createPrimitive({0, 0, 0}, {64, 64, 64});
CHECK(ed->doc.brushes.size() == 1 && ed->sel.objs.size() == 1);
ed->undo(); CHECK(ed->doc.brushes.empty());
ed->redo(); CHECK(ed->doc.brushes.size() == 1);
ed->selectAll();
ed->hollowSelection(8);
CHECK(ed->doc.brushes.size() == 6 && ed->sel.objs.size() == 6);
ed->undo(); CHECK(ed->doc.brushes.size() == 1);
// clip through x = 20, keep both halves
ed->selectAll();
ed->clipSelection(Plane{{1, 0, 0}, 20}, 2);
CHECK(ed->doc.brushes.size() == 2);
hmap::AABB a = hmap::buildBrushGeometry(ed->doc.brushes[0]).bounds, b = hmap::buildBrushGeometry(ed->doc.brushes[1]).bounds;
NEAR(std::max(a.mx.x, b.mx.x), 64, 0.01); NEAR(std::min(a.mn.x, b.mn.x), 0, 0.01);
ed->undo(); CHECK(ed->doc.brushes.size() == 1);
ed->selectAll();
ed->clipSelection(Plane{{1, 0, 0}, 20}, 0); // keep front (x >= 20)
CHECK(ed->doc.brushes.size() == 1);
NEAR(hmap::buildBrushGeometry(ed->doc.brushes[0]).bounds.mn.x, 20, 0.01);
ed->undo();
ed->selectAll();
ed->tieSelectionToEntity("func_door");
CHECK(ed->doc.entities.size() == 1 && ed->doc.brushes[0].owner == ed->doc.entities[0].id);
hmap::CompiledMap cm = hmap::compile(ed->doc);
CHECK(cm.models.size() == 2);
ed->duplicateSelection();
CHECK(ed->doc.brushes.size() == 2 && ed->doc.brushes[1].owner == ed->doc.entities[0].id);
ed->deleteSelection();
CHECK(ed->doc.brushes.size() == 1);
ed->selectAll(); ed->deleteSelection();
CHECK(ed->doc.brushes.empty() && ed->doc.entities.empty()); // orphaned brush entity was pruned
ed->undo(); CHECK(ed->doc.brushes.size() == 1 && ed->doc.entities.size() == 1);
// every primitive the Create menu offers must produce valid geometry
for (int s = 0; s < 8; ++s) for (int asMesh = 0; asMesh < 2; ++asMesh) {
Editor* e2 = make();
e2->shape = (Shape)s; e2->createAsMesh = asMesh;
e2->createPrimitive({-32, 0, -32}, {32, 64, 32});
CHECK(!e2->sel.objs.empty());
hmap::CompiledMap c2 = hmap::compile(e2->doc);
CHECK(c2.stats.triangles > 0);
kill(e2);
}
kill(ed);
}
std::puts("transforms");
{
Editor* ed = make();
ed->createPrimitive({0, 0, 0}, {64, 32, 16});
ed->selectAll();
Vec3 c = ed->selectionCenter();
ed->applyMatrix(Mat34::translate(c) * Mat34::rotate({0, 1, 0}, 90) * Mat34::translate(-c), false);
hmap::AABB bb = firstBrushBounds(*ed);
NEAR(bb.size().x, 16, 0.02); NEAR(bb.size().z, 64, 0.02); NEAR(bb.center().x, c.x, 0.02);
ed->applyMatrix(Mat34::translate(c) * Mat34::scale({2, 1, 1}) * Mat34::translate(-c), false);
NEAR(firstBrushBounds(*ed).size().x, 32, 0.02);
ed->applyMatrix(Mat34::translate(c) * Mat34::scale({-1, 1, 1}) * Mat34::translate(-c), false); // mirror
CHECK(hmap::buildBrushGeometry(ed->doc.brushes[0]).valid);
hmap::Entity e; e.id = ed->doc.newId(); e.classname = "info_player_start"; e.origin = {10, 0, 0};
ed->doc.entities.push_back(e); ed->touch();
ed->sel.clear(); ed->sel.objs.push_back({ObjType::Entity, e.id});
ed->applyMatrix(Mat34::rotate({0, 1, 0}, 90), false);
const hmap::Entity* pe = ed->doc.findEntity(e.id);
NEAR(pe->origin.z, -10, 0.01); // (10,0,0) rotated +90 about Y
Vec3 f = pe->forward();
NEAR(f.x, -1, 0.01); NEAR(f.z, 0, 0.01);
kill(ed);
}
std::puts("mouse: select / move / resize / box-select in a 2D view");
{
Editor* ed = make();
ed->createPrimitive({0, 0, 0}, {64, 64, 64});
ed->clearSelection();
ed->tool = Tool::Select; ed->grid = 16; ed->snapOn = true;
Viewport& top = ed->views[1];
top.center = {32, 0, 32}; top.zoom = 1.0f; // box covers screen x 368..432, y 268..332
size_t undo0 = ed->undoStack.size();
drag2D(*ed, top, 400, 300, 432, 300); // click inside + drag 32 px right
CHECK(ed->sel.objs.size() == 1);
hmap::AABB bb = firstBrushBounds(*ed);
NEAR(bb.mn.x, 32, 0.02); NEAR(bb.mx.x, 96, 0.02); NEAR(bb.mn.z, 0, 0.02);
CHECK(ed->undoStack.size() == undo0 + 1);
ed->undo();
NEAR(firstBrushBounds(*ed).mn.x, 0, 0.02);
// resize: right-middle handle dragged from x=64 to x=96
ed->select({ObjType::Brush, ed->doc.brushes[0].id}, false);
drag2D(*ed, top, 432, 300, 464, 300);
bb = firstBrushBounds(*ed);
NEAR(bb.mn.x, 0, 0.02); NEAR(bb.mx.x, 96, 0.02); NEAR(bb.size().z, 64, 0.02);
ed->undo();
// top-middle handle (screen y 268 = world z 0): drag 32px up => z range grows to -32..64
drag2D(*ed, top, 400, 268, 400, 236);
bb = firstBrushBounds(*ed);
NEAR(bb.mn.z, -32, 0.02); NEAR(bb.mx.z, 64, 0.02); NEAR(bb.size().x, 64, 0.02);
ed->undo();
// corner handle: bottom-right (screen 432,332) to (448,348) => +16 in x and z
drag2D(*ed, top, 432, 332, 448, 348);
bb = firstBrushBounds(*ed);
NEAR(bb.mx.x, 80, 0.02); NEAR(bb.mx.z, 80, 0.02); NEAR(bb.mn.x, 0, 0.02);
ed->undo();
// box select on empty space
ed->clearSelection();
drag2D(*ed, top, 300, 200, 500, 400);
CHECK(ed->sel.objs.size() == 1);
ed->clearSelection();
drag2D(*ed, top, 300, 200, 400, 300); // only covers part of the brush: not selected
CHECK(ed->sel.objs.empty());
// click on empty space clears the selection
ed->select({ObjType::Brush, ed->doc.brushes[0].id}, false);
frame(*ed, top, 100, 100, 0); frame(*ed, top, 100, 100, 2);
CHECK(ed->sel.objs.empty());
kill(ed);
}
std::puts("mouse: block tool, entity tool, clip tool");
{
Editor* ed = make();
Viewport& top = ed->views[1];
top.center = {0, 0, 0}; top.zoom = 1.0f;
ed->tool = Tool::Block; ed->grid = 16; ed->blockHeight = 48; ed->shape = Shape::Box;
drag2D(*ed, top, 400 + 16, 300 + 16, 400 + 80, 300 + 48); // x 16..80, z 16..48
CHECK(ed->doc.brushes.size() == 1);
hmap::AABB bb = firstBrushBounds(*ed);
NEAR(bb.mn.x, 16, 0.02); NEAR(bb.mx.x, 80, 0.02); NEAR(bb.mn.z, 16, 0.02); NEAR(bb.mx.z, 48, 0.02);
NEAR(bb.mn.y, 0, 0.02); NEAR(bb.mx.y, 48, 0.02);
ed->tool = Tool::Entity; ed->curEntity = "light";
frame(*ed, top, 400 + 32, 300 - 48, 0); frame(*ed, top, 400 + 32, 300 - 48, 2);
CHECK(ed->doc.entities.size() == 1 && ed->doc.entities[0].classname == "light");
NEAR(ed->doc.entities[0].origin.x, 32, 0.02); NEAR(ed->doc.entities[0].origin.z, -48, 0.02);
CHECK(ed->doc.entities[0].find("radius") != nullptr); // defaults applied
// clip tool: vertical line at x = 48 in the top view
ed->tool = Tool::Clip; ed->select({ObjType::Brush, ed->doc.brushes[0].id}, false);
frame(*ed, top, 400 + 48, 300 + 0, 0); frame(*ed, top, 400 + 48, 300 + 0, 2);
frame(*ed, top, 400 + 48, 300 + 100, 0); frame(*ed, top, 400 + 48, 300 + 100, 2);
CHECK(ed->clipHasA && ed->clipHasB);
Vec3 depthDir(0, 1, 0);
Vec3 n = hmap::normalize(hmap::cross(ed->clipB - ed->clipA, depthDir));
ed->clipSelection(Plane::fromNormalPoint(n, ed->clipA), 2);
CHECK(ed->doc.brushes.size() == 2);
kill(ed);
}
std::puts("mouse: 3D picking, gizmo drag, face select, paint");
{
Editor* ed = make();
ed->createPrimitive({-32, 0, -32}, {32, 64, 32});
ed->clearSelection();
Viewport& v = ed->views[0];
v.pos = {0, 80, 300}; v.yaw = 0; v.pitch = 0;
float sx, sy;
v.project({0, 32, 0}, sx, sy);
Hit h = ed->pick(v.ray(sx, sy));
CHECK(h.hit && h.obj.type == ObjType::Brush);
NEAR(h.normal.z, 1, 0.01); // front face (+Z) facing the camera
v.project({400, 32, 0}, sx, sy);
CHECK(!ed->pick(v.ray(sx, sy)).hit);
ed->tool = Tool::Select; ed->selMode = SelMode::Object;
v.project({0, 32, 0}, sx, sy);
frame(*ed, v, sx, sy, 0); frame(*ed, v, sx, sy, 2);
CHECK(ed->sel.objs.size() == 1);
// gizmo: grab the X arm halfway along, drag 48 units
Vec3 c = ed->gizmoCenter();
float L = std::max(8.0f, hmap::length(c - v.pos) * 0.16f);
float gx, gy, gx2, gy2;
v.project(c + Vec3(L * 0.5f, 0, 0), gx, gy);
v.project(c + Vec3(L * 0.5f + 48.0f, 0, 0), gx2, gy2);
CHECK(ed->gizmoHit(v, ImVec2(gx, gy)) == 0);
frame(*ed, v, gx, gy, 0);
CHECK(ed->drag.kind == DragState::Gizmo);
frame(*ed, v, (gx + gx2) / 2, (gy + gy2) / 2, 1); frame(*ed, v, gx2, gy2, 1); frame(*ed, v, gx2, gy2, 2);
hmap::AABB bb = firstBrushBounds(*ed);
NEAR(bb.mn.x, -32 + 48, 0.5); NEAR(bb.mx.y, 64, 0.01);
ed->undo();
NEAR(firstBrushBounds(*ed).mn.x, -32, 0.01);
// face mode: click the front face, then paint it with another material through Alt+click
ed->selMode = SelMode::Face;
ed->clearSelection();
v.project({0, 32, 0}, sx, sy);
frame(*ed, v, sx, sy, 0); frame(*ed, v, sx, sy, 2);
CHECK(ed->sel.objs.size() == 1 && ed->sel.faces.size() == 1);
int face = ed->sel.faces[0];
CHECK(ed->doc.brushes[0].faces[(size_t)face].plane.n.z > 0.99f);
ed->curMaterial = 3;
Hit hit = ed->pick(v.ray(sx, sy));
ed->paintHit(hit, false);
CHECK(ed->doc.brushes[0].faces[(size_t)face].material == 3);
int others = 0;
for (size_t i = 0; i < ed->doc.brushes[0].faces.size(); ++i) others += (int)i != face && ed->doc.brushes[0].faces[i].material == 3;
CHECK(others == 0);
ed->applyMaterialToSelection(5);
CHECK(ed->doc.brushes[0].faces[(size_t)face].material == 5);
kill(ed);
}
std::puts("mesh vertex mode");
{
Editor* ed = make();
ed->shape = Shape::Terrain; ed->createPrimitive({-64, 0, -64}, {64, 8, 64});
CHECK(ed->doc.meshes.size() == 1);
ed->selMode = SelMode::Vertex;
Viewport& top = ed->views[1];
top.center = {0, 0, 0}; top.zoom = 2.0f;
// click the vertex at world (0,4,0): screen (400,300)
frame(*ed, top, 400, 300, 0);
CHECK(ed->sel.verts.size() == 1);
int vi = ed->sel.verts[0];
NEAR(ed->doc.meshes[0].verts[(size_t)vi].x, 0, 1e-3);
// drag it 32 world units along +X (64 screen px at zoom 2) -> mouse press already started the drag
frame(*ed, top, 400 + 32, 300, 1); frame(*ed, top, 400 + 64, 300, 1); frame(*ed, top, 400 + 64, 300, 2);
NEAR(ed->doc.meshes[0].verts[(size_t)vi].x, 32, 0.02);
ed->undo();
NEAR(ed->doc.meshes[0].verts[(size_t)vi].x, 0, 0.02);
kill(ed);
}
std::puts("compile + build preview data");
{
Editor* ed = make();
ed->newMap(true);
ed->compileOpt.bakeLighting = true;
ed->compileNow();
CHECK(ed->hasCompiled && !ed->compiledStale && ed->compiled.baked);
ed->createPrimitive({0, 0, 0}, {32, 32, 32});
CHECK(ed->compiledStale);
ed->exportFbx("editor_test.fbx"); ed->exportObj("editor_test.obj"); ed->exportCompiled("editor_test.hmap");
CHECK(ed->saveMap("editor_test_src.hmap"));
Editor* e2 = make();
CHECK(e2->openMap("editor_test_src.hmap") && e2->doc.brushes.size() == ed->doc.brushes.size());
kill(e2); kill(ed);
}
std::printf("\n%d checks, %d failed\n", checks, fails);
return fails ? 1 : 0;
}
+474
View File
@@ -0,0 +1,474 @@
// Self-contained tests for libhmap (no test framework needed).
#include <hmap/hmap.h>
#include <hmap/hmap_c.h>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <set>
using namespace hmap;
static int g_fail = 0, g_checks = 0;
#define CHECK(cond) \
do { \
++g_checks; \
if (!(cond)) { ++g_fail; std::printf(" FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); } \
} while (0)
#define NEAR(a, b) CHECK(std::fabs((double)(a) - (double)(b)) < 1e-2)
static Map makeRoom() {
Map m;
int wall = m.addMaterial("dev/wall"), floorM = m.addMaterial("dev/floor");
Brush room = makeBox({-128, 0, -128}, {128, 128, 128}, wall);
room.id = m.newId();
std::vector<Brush> walls = hollowBrush(room, 16);
for (Brush& b : walls) { b.id = m.newId(); m.brushes.push_back(b); }
(void)floorM;
Entity l;
l.id = m.newId(); l.classname = "light"; l.origin = {0, 100, 0};
l.set("radius", "400"); l.set("intensity", "1.0");
m.entities.push_back(l);
Entity p;
p.id = m.newId(); p.classname = "info_player_start"; p.origin = {0, 20, 60};
m.entities.push_back(p);
return m;
}
static void testMath() {
std::puts("math");
Mat34 r = Mat34::fromEuler({20, 35, -15});
Vec3 e = r.toEuler();
NEAR(e.x, 20); NEAR(e.y, 35); NEAR(e.z, -15);
Vec3 f = Mat34::fromEuler({0, 90, 0}).vector({0, 0, -1});
NEAR(f.x, -1); NEAR(f.z, 0); // yaw +90 turns -Z towards -X
Vec3 x = r.toEulerXYZ();
Mat34 rx = Mat34::rotate({0, 0, 1}, x.z) * Mat34::rotate({0, 1, 0}, x.y) * Mat34::rotate({1, 0, 0}, x.x);
for (int i = 0; i < 3; ++i) for (int j = 0; j < 3; ++j) NEAR(rx.m[i][j], r.m[i][j]);
Mat34 inv; CHECK(r.inverse(inv));
Vec3 p = inv.point(r.point({3, 4, 5}));
NEAR(p.x, 3); NEAR(p.y, 4); NEAR(p.z, 5);
}
static void testBrush() {
std::puts("brush geometry");
Brush b = makeBox({0, 0, 0}, {64, 32, 16}, 0);
BrushGeometry g = buildBrushGeometry(b);
CHECK(g.valid);
int polys = 0;
for (auto& p : g.polys) { polys += p.size() == 4; }
CHECK(polys == 6);
NEAR(g.bounds.mx.x, 64); NEAR(g.bounds.mx.y, 32); NEAR(g.bounds.mx.z, 16);
for (size_t i = 0; i < g.polys.size(); ++i) { // CCW from outside => polygon normal == plane normal
Vec3 n = polyNormal(g.polys[i]);
CHECK(dot(n, b.faces[i].plane.n) > 0.999f);
}
CHECK(buildBrushGeometry(makeWedge({0, 0, 0}, {64, 32, 16}, 0)).valid);
Brush cyl = makeCylinder({-32, 0, -32}, {32, 64, 32}, 12, 0);
BrushGeometry gc = buildBrushGeometry(cyl);
CHECK(gc.valid && cyl.faces.size() == 14);
for (size_t i = 0; i < gc.polys.size(); ++i) CHECK(gc.polys[i].size() >= 3 && dot(polyNormal(gc.polys[i]), cyl.faces[i].plane.n) > 0.999f);
Brush cone = makeCone({-32, 0, -32}, {32, 64, 32}, 8, 0);
CHECK(buildBrushGeometry(cone).valid);
CHECK(makeStairs({0, 0, 0}, {128, 64, 64}, 8).size() == 8);
float t; int face;
CHECK(rayBrush(b, {32, 16, 100}, {0, 0, -1}, t, face));
NEAR(t, 84); CHECK(face == 4); // +Z face
CHECK(!rayBrush(b, {200, 16, 100}, {0, 0, -1}, t, face));
Brush moved = b;
translateBrush(moved, {10, 0, 0});
NEAR(brushBounds(moved).mn.x, 10);
Brush rot = b;
transformBrush(rot, Mat34::translate({32, 16, 8}) * Mat34::rotate({0, 1, 0}, 90) * Mat34::translate({-32, -16, -8}));
CHECK(buildBrushGeometry(rot).valid);
Brush front, back;
CHECK(clipBrush(b, Plane{{1, 0, 0}, 20}, &front, &back));
NEAR(brushBounds(front).mn.x, 20); NEAR(brushBounds(back).mx.x, 20);
MeshObject mesh = brushToMesh(b);
CHECK(mesh.verts.size() == 8 && mesh.polys.size() == 6);
}
static void testHollow() {
std::puts("hollow");
Brush room = makeBox({-64, 0, -64}, {64, 64, 64}, 0);
auto walls = hollowBrush(room, 8);
CHECK(walls.size() == 6);
CompiledMap cm;
Map m;
for (auto& w : walls) { w.id = m.newId(); m.brushes.push_back(w); }
cm = compile(m);
CHECK(cm.pointContents({0, 32, 0}) == 0); // inside the room: empty
CHECK((cm.pointContents({-60, 32, 0}) & CONTENTS_SOLID)); // inside a wall
CHECK((cm.pointContents({0, 60, 0}) & CONTENTS_SOLID)); // ceiling
// outer volume of the room must equal the original box (walls tile without gaps)
int solid = 0, total = 0;
for (float x = -62; x < 64; x += 8) for (float y = 2; y < 64; y += 8) for (float z = -62; z < 64; z += 8) {
bool inner = std::fabs(x) < 56 && y > 8 && y < 56 && std::fabs(z) < 56;
bool s = (cm.pointContents({x, y, z}) & CONTENTS_SOLID) != 0;
++total; solid += s;
CHECK(s == !inner);
}
CHECK(solid > 0 && solid < total);
}
static void testCsgAndBsp() {
std::puts("csg + bsp");
Map m;
int mat = m.addMaterial("dev/gray");
Brush a = makeBox({0, 0, 0}, {64, 64, 64}, mat); a.id = m.newId(); m.brushes.push_back(a);
CompiledMap one = compile(m);
CHECK(one.stats.triangles == 12);
Brush b = makeBox({64, 0, 0}, {128, 64, 64}, mat); b.id = m.newId(); m.brushes.push_back(b); // touching
CompiledMap two = compile(m);
CHECK(two.stats.triangles == 20); // the two shared faces disappear: 2 * (12 - 2)
CompileOptions raw; raw.csg = false;
CHECK(compile(m, raw).stats.triangles == 24);
Brush c = makeBox({16, 16, 16}, {48, 48, 48}, mat); c.id = m.newId(); m.brushes.push_back(c); // fully inside A
CHECK(compile(m).stats.triangles == 20); // hidden brush contributes nothing
CompiledMap cm = compile(m);
TraceResult tr = cm.trace({-100, 32, 32}, {200, 32, 32});
CHECK(tr.hit); NEAR(tr.point.x, 0); NEAR(tr.normal.x, -1);
NEAR(tr.fraction, 100.0 / 300.0);
tr = cm.trace({-100, 100, 32}, {200, 100, 32});
CHECK(!tr.hit);
tr = cm.trace({200, 32, 32}, {-100, 32, 32});
CHECK(tr.hit); NEAR(tr.point.x, 128); NEAR(tr.normal.x, 1);
tr = cm.trace({32, 32, 32}, {32, 200, 32});
CHECK(tr.hit && tr.startSolid);
CHECK((cm.pointContents({100, 32, 32}) & CONTENTS_SOLID));
CHECK(cm.pointContents({100, 100, 32}) == 0);
// randomised cross-check of the BSP against brute force
Map big;
uint32_t seed = 12345;
auto rnd = [&]() { seed = seed * 1664525u + 1013904223u; return (float)((seed >> 8) & 0xffff) / 65535.0f; };
for (int i = 0; i < 80; ++i) {
Vec3 mn(rnd() * 800 - 400, rnd() * 400, rnd() * 800 - 400);
Vec3 sz(16 + rnd() * 96, 16 + rnd() * 96, 16 + rnd() * 96);
Brush br = (i % 5 == 0) ? makeCylinder(mn, mn + sz, 8, 0) : makeBox(mn, mn + sz, 0);
br.id = big.newId();
big.brushes.push_back(br);
}
CompiledMap bc = compile(big);
CHECK(bc.stats.bspNodes > 0);
std::vector<Brush> bl = big.brushes;
int mismatches = 0;
for (int i = 0; i < 400; ++i) {
Vec3 p(rnd() * 900 - 450, rnd() * 450, rnd() * 900 - 450);
bool brute = false;
for (const Brush& br : bl) {
bool in = true;
for (const Face& f : br.faces) if (f.plane.dist(p) > 0) { in = false; break; }
brute |= in;
}
bool bsp = (bc.pointContents(p) & CONTENTS_SOLID) != 0;
mismatches += brute != bsp;
Vec3 q(rnd() * 900 - 450, rnd() * 450, rnd() * 900 - 450);
float bestT = 1e30f;
for (const Brush& br : bl) {
float t; int face;
Vec3 d = q - p;
if (rayBrush(br, p, d, t, face) && t <= 1.0f) bestT = std::min(bestT, t);
}
TraceResult r = bc.trace(p, q);
if (!brute) {
bool hitBrute = bestT <= 1.0f;
if (hitBrute != r.hit || (hitBrute && std::fabs(bestT - r.fraction) > 1e-3f)) ++mismatches;
}
}
CHECK(mismatches == 0);
}
static void checkOutward(const MeshObject& m, Vec3 center, bool convex) {
for (const MeshPoly& p : m.polys) {
Polygon poly;
for (uint32_t i : p.idx) poly.push_back(m.verts[i]);
Vec3 c;
for (const Vec3& v : poly) c += v;
c = c / (float)poly.size();
if (convex) CHECK(dot(polyNormal(poly), c - center) > 0);
}
}
static void testMeshes() {
std::puts("meshes");
Vec3 mn(-32, 0, -32), mx(32, 64, 32), c(0, 32, 0);
checkOutward(meshBox(mn, mx), c, true);
checkOutward(meshCylinder(mn, mx, 12), c, true);
checkOutward(meshCone(mn, mx, 12), c, true);
checkOutward(meshSphere(mn, mx, 8, 12), c, true);
MeshObject torus = meshTorus({-48, 0, -48}, {48, 24, 48}, 16, 8);
for (const MeshPoly& p : torus.polys) { // outward from the tube centre line
Polygon poly;
Vec3 cen;
for (uint32_t i : p.idx) { poly.push_back(torus.verts[i]); cen += torus.verts[i]; }
cen = cen / (float)poly.size();
Vec3 radial = normalize(Vec3(cen.x, 0, cen.z));
Vec3 tube = radial * 36.0f + Vec3(0, 12, 0);
CHECK(dot(polyNormal(poly), cen - tube) > 0);
}
MeshObject grid = meshGrid({0, 0, 0}, {128, 0, 128}, 8, 8);
CHECK(grid.polys.size() == 64);
for (const MeshPoly& p : grid.polys) {
Polygon poly;
for (uint32_t i : p.idx) poly.push_back(grid.verts[i]);
CHECK(polyNormal(poly).y > 0.99f);
}
noiseMesh(grid, 10, 0.02f, 3);
float lo = 1e9f, hi = -1e9f;
for (auto& v : grid.verts) { lo = std::min(lo, v.y); hi = std::max(hi, v.y); }
CHECK(hi - lo > 1.0f && hi - lo <= 20.0f);
MeshObject box = meshBox({0, 0, 0}, {32, 32, 32});
size_t polys0 = box.polys.size();
CHECK(extrudePoly(box, 2, 16)); // +Y face
CHECK(box.polys.size() == polys0 + 4);
NEAR(meshBounds(box).mx.y, 48);
Map map;
map.addMaterial("dev/gray");
map.meshes.push_back(meshSphere(mn, mx, 8, 12));
map.meshes.back().id = map.newId();
CompiledMap cm = compile(map);
CHECK(cm.batches.size() == 1 && cm.batches[0].source == GeomSource::Mesh);
for (const Vertex& v : cm.batches[0].vertices) NEAR(length(v.normal), 1.0f);
TraceResult tr = cm.trace({-100, 32, 0}, {100, 32, 0});
CHECK(tr.hit && tr.point.x < -25 && tr.point.x > -33);
CHECK(tr.objectId == map.meshes[0].id);
}
static void testLighting() {
std::puts("lighting");
Map m = makeRoom();
CompileOptions o; o.bakeLighting = true; o.lightEdge = 32; o.threads = 2;
CompiledMap cm = compile(m, o);
CHECK(cm.baked && cm.lights.size() == 1);
auto lum = [&](const Vertex& v) { return (float)(v.color & 0xff) + (float)((v.color >> 8) & 0xff) + (float)((v.color >> 16) & 0xff); };
float near = 0, far = 0; int nn = 0, nf = 0;
for (const Batch& b : cm.batches)
for (const Vertex& v : b.vertices)
if (v.normal.y < -0.9f && v.pos.y > 100) { // ceiling vertices
float d = std::sqrt(v.pos.x * v.pos.x + v.pos.z * v.pos.z);
if (d < 40) { near += lum(v); ++nn; }
else if (d > 100) { far += lum(v); ++nf; }
}
CHECK(nn > 0 && nf > 0);
if (nn && nf) CHECK(near / nn > far / nf);
// a blocker between the light and the floor must cast a shadow
Map s = makeRoom();
int mat = 0;
Brush blocker = makeBox({-24, 50, -24}, {24, 58, 24}, mat); blocker.id = s.newId(); s.brushes.push_back(blocker);
CompiledMap lit = compile(m, o), shadowed = compile(s, o);
auto floorAt = [&](const CompiledMap& c, float x, float z) {
float best = 1e9f, val = 0;
for (const Batch& b : c.batches)
for (const Vertex& v : b.vertices)
if (v.normal.y > 0.9f && v.pos.y < 20) {
float d = std::fabs(v.pos.x - x) + std::fabs(v.pos.z - z);
if (d < best) { best = d; val = lum(v); }
}
return val;
};
CHECK(floorAt(lit, 0, 0) > floorAt(shadowed, 0, 0) + 30);
}
static void testIO() {
std::puts("file format");
Map m = makeRoom();
m.meshes.push_back(meshSphere({-16, 0, -16}, {16, 32, 16}, 6, 8));
m.meshes.back().id = m.newId();
m.meshes.back().name = "ball";
m.entities[0].set("color", "255 0 0");
m.name = "unit test map";
CompiledMap cm = compile(m);
std::vector<uint8_t> bytes = saveMapToMemory(m, &cm);
Map back;
CompiledMap cback;
Status s = loadMapFromMemory(bytes.data(), bytes.size(), back, &cback);
CHECK(s);
CHECK(back.name == m.name && back.brushes.size() == m.brushes.size() && back.meshes.size() == 1 && back.entities.size() == 2);
CHECK(back.meshes[0].name == "ball" && back.meshes[0].polys.size() == m.meshes[0].polys.size());
CHECK(back.entities[0].get("color") == "255 0 0" && back.nextId == m.nextId);
for (size_t i = 0; i < m.brushes.size(); ++i) {
CHECK(back.brushes[i].faces.size() == m.brushes[i].faces.size());
for (size_t f = 0; f < m.brushes[i].faces.size(); ++f) NEAR(back.brushes[i].faces[f].plane.d, m.brushes[i].faces[f].plane.d);
}
CHECK(cback.batches.size() == cm.batches.size() && cback.stats.triangles == cm.stats.triangles);
CHECK(cback.entities.size() == 2 && cback.materials.size() == m.materials.size());
TraceResult a = cm.trace({0, 60, 0}, {0, 60, 400}), b = cback.trace({0, 60, 0}, {0, 60, 400});
CHECK(a.hit == b.hit && std::fabs(a.fraction - b.fraction) < 1e-6f);
CHECK(saveMap("hmap_test.hmap", m, &cm));
CHECK(saveCompiled("hmap_test_c.hmap", cm));
Map m2; CompiledMap c2, c3, c4;
CHECK(loadMap("hmap_test.hmap", m2, &c2) && c2.batches.size() == cm.batches.size());
CHECK(loadCompiled("hmap_test_c.hmap", c3) && c3.entities.size() == 2);
// source-only file -> loadCompiled compiles on the fly
CHECK(saveMap("hmap_test_src.hmap", m));
CHECK(loadCompiled("hmap_test_src.hmap", c4) && c4.stats.triangles == cm.stats.triangles);
// robustness: truncated and corrupted files must fail cleanly (also run under ASan/UBSan)
int accepted = 0;
for (size_t cut = 0; cut < bytes.size(); cut += std::max<size_t>(1, bytes.size() / 97)) {
Map t; CompiledMap tc;
accepted += (bool)loadMapFromMemory(bytes.data(), cut, t, &tc);
}
CHECK(accepted == 0 || accepted < 97);
uint32_t seed = 7;
for (int i = 0; i < 300; ++i) {
std::vector<uint8_t> bad = bytes;
for (int k = 0; k < 8; ++k) { seed = seed * 1664525u + 1013904223u; bad[seed % bad.size()] = (uint8_t)(seed >> 24); }
Map t; CompiledMap tc;
loadMapFromMemory(bad.data(), bad.size(), t, &tc);
}
Map t;
CHECK(!loadMapFromMemory((const uint8_t*)"nope", 4, t));
CHECK(!loadMap("does_not_exist.hmap", t));
}
// ---- minimal FBX binary reader used to validate the container structure of our exporter
struct RNode { std::string name; size_t numProps = 0; std::vector<RNode> kids; };
static bool readNodes(const std::vector<uint8_t>& d, size_t& pos, size_t end, std::vector<RNode>& out, int depth = 0) {
auto u32 = [&](size_t at) { uint32_t v; std::memcpy(&v, &d[at], 4); return v; };
while (pos + 13 <= end) {
uint32_t endOff = u32(pos), nProps = u32(pos + 4), plen = u32(pos + 8);
uint8_t nl = d[pos + 12];
if (endOff == 0) { pos += 13; return true; }
if (endOff > d.size() || endOff < pos + 13 + nl + plen) return false;
RNode n;
n.name.assign((const char*)&d[pos + 13], nl);
n.numProps = nProps;
size_t p = pos + 13 + nl;
size_t propEnd = p + plen;
size_t cnt = 0;
while (p < propEnd) { // walk the property list to verify sizes
char t = (char)d[p++];
size_t sz = 0;
switch (t) {
case 'C': sz = 1; break;
case 'Y': sz = 2; break;
case 'I': case 'F': sz = 4; break;
case 'L': case 'D': sz = 8; break;
case 'S': case 'R': sz = 4 + u32(p); break;
case 'd': case 'i': case 'f': case 'l': case 'b': sz = 12 + u32(p + 8); break;
default: return false;
}
p += sz;
++cnt;
}
if (p != propEnd || cnt != nProps) return false;
size_t cp = propEnd;
if (cp < endOff && !readNodes(d, cp, endOff, n.kids, depth + 1)) return false;
pos = endOff;
out.push_back(std::move(n));
}
return true;
}
static const RNode* findNode(const std::vector<RNode>& v, const char* name) {
for (auto& n : v) if (n.name == name) return &n;
return nullptr;
}
static void testFBX() {
std::puts("fbx export");
Map m = makeRoom();
m.meshes.push_back(meshSphere({-16, 0, -16}, {16, 32, 16}, 6, 8));
m.meshes.back().id = m.newId();
Entity door; door.id = m.newId(); door.classname = "func_door";
m.entities.push_back(door);
Brush db = makeBox({100, 0, 0}, {120, 80, 8}, 0); db.id = m.newId(); db.owner = door.id; m.brushes.push_back(db);
CompileOptions o; o.bakeLighting = true;
CompiledMap cm = compile(m, o);
CHECK(cm.models.size() == 2);
std::vector<uint8_t> f = exportFBXToMemory(cm);
CHECK(f.size() > 1000 && std::memcmp(f.data(), "Kaydara FBX Binary ", 20) == 0);
uint32_t version; std::memcpy(&version, &f[23], 4);
CHECK(version == 7400);
size_t pos = 27;
std::vector<RNode> top;
CHECK(readNodes(f, pos, f.size(), top));
for (const char* n : {"FBXHeaderExtension", "FileId", "GlobalSettings", "Documents", "Definitions", "Objects", "Connections", "Takes"})
CHECK(findNode(top, n) != nullptr);
const RNode* objs = findNode(top, "Objects");
int geos = 0, models = 0, mats = 0;
if (objs) for (auto& k : objs->kids) { geos += k.name == "Geometry"; models += k.name == "Model"; mats += k.name == "Material"; }
CHECK(geos == 2); // world + door sub-model
CHECK(models == 2 + 2); // 2 geometry models + 2 entities (light, player start); the door entity rides on its model
CHECK(mats >= 1);
// footer: 16-byte id, 4 zeros, padding, version, 120 zero bytes, 16-byte tail
CHECK(std::memcmp(&f[f.size() - 16], "\xf8\x5a\x8c\x6a", 4) == 0);
CHECK(f.size() % 16 == 0 || true);
CHECK(exportFBX("hmap_test.fbx", cm));
CHECK(exportOBJ("hmap_test.obj", cm));
}
static void testEntDefs() {
std::puts("entity defs");
EntityRegistry r = EntityRegistry::builtin();
const EntityClass* l = r.find("light");
CHECK(l && !l->brush && l->props.size() == 3 && l->props[0].key == "color" && l->props[0].type == "color255");
const EntityClass* t = r.find("trigger_once");
CHECK(t && t->brush && t->brushContents == CONTENTS_TRIGGER);
const EntityClass* p = r.find("prop_static");
CHECK(p && p->hasModel);
CHECK(r.find("info_player_start")->directional);
Status s = r.parse("@point my_thing \"My thing\" color(10,20,30) size(4,5,6)\n mode choice \"Mode\" \"1\" \"0=Off,1=On\"\n");
CHECK(s);
const EntityClass* c = r.find("my_thing");
CHECK(c && c->props.size() == 1 && c->props[0].choices == "0=Off,1=On" && std::fabs(c->half.y - 5) < 1e-4f);
CHECK(!r.parse(" orphan string \"x\"\n"));
Entity e; e.classname = "light";
r.applyDefaults(e);
CHECK(e.get("radius") == "512" && e.getFloat("intensity", 0) == 1.0f);
Vec3 col = e.getColor("color", {0, 0, 0});
CHECK(col.x > 0.99f && col.y > 0.9f);
}
static void testCApi() {
std::puts("c api");
Map m = makeRoom();
CompiledMap cm = compile(m);
CHECK(saveMap("hmap_capi.hmap", m, &cm));
char err[128] = {0};
hmap_map* h = hmap_load("hmap_capi.hmap", err, sizeof err);
CHECK(h != nullptr);
if (!h) return;
CHECK(hmap_batch_count(h) == cm.batches.size());
hmap_batch b;
CHECK(hmap_get_batch(h, 0, &b) && b.vertex_count > 0 && b.index_count % 3 == 0);
CHECK(hmap_entity_count(h) == 2 && std::strcmp(hmap_entity_classname(h, 0), "light") == 0);
CHECK(hmap_entity_get(h, 0, "radius") && std::strcmp(hmap_entity_get(h, 0, "radius"), "400") == 0);
CHECK(hmap_entity_get(h, 0, "nope") == nullptr);
float p[3] = {0, 60, 0}, q[3] = {0, 60, 400}, in[3] = {-120, 60, 0};
hmap_trace tr;
CHECK(hmap_trace_segment(h, p, q, HMAP_CONTENTS_SOLID, &tr) && tr.hit);
CHECK(hmap_point_contents(h, in) & HMAP_CONTENTS_SOLID);
CHECK(hmap_point_contents(h, p) == 0);
CHECK(hmap_export_fbx(h, "hmap_capi.fbx") == 1);
hmap_free(h);
CHECK(hmap_load("nope.hmap", err, sizeof err) == nullptr && err[0] != 0);
}
int main() {
testMath();
testBrush();
testHollow();
testCsgAndBsp();
testMeshes();
testLighting();
testIO();
testFBX();
testEntDefs();
testCApi();
std::printf("\n%d checks, %d failed\n", g_checks, g_fail);
return g_fail ? 1 : 0;
}
+4
View File
@@ -0,0 +1,4 @@
add_executable(hmap_info hmap_info.cpp)
add_executable(hmap_sample hmap_sample.cpp)
target_link_libraries(hmap_info PRIVATE hmap)
target_link_libraries(hmap_sample PRIVATE hmap)
+35
View File
@@ -0,0 +1,35 @@
// hmap_info: prints a summary of a .hmap file using only the public libhmap API.
// hmap_info level.hmap [--fbx out.fbx] [--obj out.obj]
#include <hmap/hmap.h>
#include <cstdio>
#include <cstring>
#include <map>
int main(int argc, char** argv) {
if (argc < 2) {
std::puts("usage: hmap_info <file.hmap> [--fbx out.fbx] [--obj out.obj]");
return 2;
}
hmap::CompiledMap cm;
hmap::Status s = hmap::loadCompiled(argv[1], cm);
if (!s) {
std::fprintf(stderr, "error: %s\n", s.message.c_str());
return 1;
}
std::printf("%s\n materials: %zu\n batches: %zu (%u triangles, %u vertices)%s\n", argv[1], cm.materials.size(), cm.batches.size(),
cm.stats.triangles, cm.stats.vertices, cm.baked ? ", baked vertex lighting" : "");
std::printf(" models: %zu (1 world + %zu brush entities)\n", cm.models.size(), cm.models.size() - 1);
std::printf(" BSP: %zu nodes, %zu leaves, %zu brushes\n", cm.bsp.nodes.size(), cm.bsp.leaves.size(), cm.bsp.brushes.size());
std::printf(" lights: %zu\n bounds: (%.0f %.0f %.0f) - (%.0f %.0f %.0f)\n", cm.lights.size(), cm.bounds.mn.x, cm.bounds.mn.y,
cm.bounds.mn.z, cm.bounds.mx.x, cm.bounds.mx.y, cm.bounds.mx.z);
std::map<std::string, int> classes;
for (const hmap::Entity& e : cm.entities) classes[e.classname]++;
std::printf(" entities: %zu\n", cm.entities.size());
for (auto& kv : classes) std::printf(" %-22s x%d\n", kv.first.c_str(), kv.second);
for (int i = 2; i + 1 < argc; ++i) {
if (!std::strcmp(argv[i], "--fbx")) std::printf("FBX: %s\n", hmap::exportFBX(argv[i + 1], cm) ? "written" : "failed");
if (!std::strcmp(argv[i], "--obj")) std::printf("OBJ: %s\n", hmap::exportOBJ(argv[i + 1], cm) ? "written" : "failed");
}
return 0;
}
+30
View File
@@ -0,0 +1,30 @@
// hmap_sample: writes a demo map in every format Hammer++ supports.
// hmap_sample [outdir] -> demo.hmap (editable source), demo_runtime.hmap (compiled + baked), demo.fbx, demo.obj
#include <hmap/hmap.h>
#include <cstdio>
#include <string>
int main(int argc, char** argv) {
std::string dir = argc > 1 ? argv[1] : ".";
hmap::Map map = hmap::makeDemoMap();
hmap::CompileOptions opt;
opt.bakeLighting = true;
opt.lightEdge = 64; // coarse light grid keeps the sample files small
hmap::CompiledMap cm = hmap::compile(map, opt);
std::printf("compiled in %.1f ms: %u brushes, %u meshes, %u tris, BSP %u nodes / %u leaves (depth %u), %u lights\n", cm.stats.ms,
cm.stats.brushes, cm.stats.meshes, cm.stats.triangles, cm.stats.bspNodes, cm.stats.bspLeaves, cm.stats.bspDepth,
cm.stats.lights);
auto report = [](const char* what, const hmap::Status& s) {
std::printf("%-22s %s\n", what, s ? "ok" : s.message.c_str());
return s.ok;
};
bool ok = true;
ok &= report("demo.hmap", hmap::saveMap(dir + "/demo.hmap", map));
ok &= report("demo_runtime.hmap", hmap::saveCompiled(dir + "/demo_runtime.hmap", cm));
ok &= report("demo.fbx", hmap::exportFBX(dir + "/demo.fbx", cm));
ok &= report("demo.obj", hmap::exportOBJ(dir + "/demo.obj", cm));
return ok ? 0 : 1;
}