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.fbxfirst.- Hammer parity is a goal, not a fact: see the checklist at the bottom for what exists and what does not.
Build
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:
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):
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:
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 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,
UnitScaleFactorderived 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 = Flatto 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.inior View > Reset window layout.