Files
Rockgrave/README.md
T
2026-10-25 03:29:04 -03:00

11 KiB

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

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, 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.