| … | … | @@ -1,28 +1,30 @@ |
| 1 | 1 | # dc-terrain |
| 2 | 2 | |
| 3 | ### [Full documentation in `docs/`](docs/) | |
| 4 | ||
| 3 | 5 | Dual Contouring voxel terrain for Godot 4, as a GDExtension. |
| 4 | 6 | |
| 5 | A signed distance field goes in, a sharp-featured triangle mesh comes out — streamed | |
| 6 | around the camera as an octree clipmap, edited at runtime with CSG brushes, and meshed | |
| 7 | either on the GPU across ten compute stages or on a pool of CPU worker threads. | |
| 7 | A signed distance field goes in, a sharp-featured triangle mesh comes out — streamed around | |
| 8 | the camera as an octree clipmap, edited at runtime with CSG brushes, and meshed on the GPU | |
| 9 | across ten compute stages or on a pool of CPU worker threads. | |
| 8 | 10 | |
| 9 | 11 | ## Features |
| 10 | 12 | |
| 11 | 13 | - **Sharp features.** Dual Contouring solves a QEF per cell, so a plane stays flat and a |
| 12 | 14 | crease stays a crease. Marching Cubes cannot do this. |
| 13 | 15 | - **Octree clipmap LOD.** Detail follows the camera. Cross-LOD seams are stitched, not hidden. |
| 14 | - **Six meshers, not one.** GPU dual contouring, CPU dual contouring, cross-LOD seam | |
| 15 | stitching, exact brush meshing, voxel pockets inside the exact path, and two | |
| 16 | simplifiers. See [Meshing paths](#meshing-paths) — they are not interchangeable. | |
| 17 | - **Runtime editing.** Spheres, boxes, convex brushes (Quake-style half-space sets), and | |
| 16 | - **Six meshers, not one.** GPU dual contouring, CPU dual contouring, seam stitching, exact | |
| 17 | brush meshing, voxel pockets, and two simplifiers — see [Meshing paths](docs/meshing.md). | |
| 18 | They are not interchangeable. | |
| 19 | - **Runtime editing.** Spheres, boxes, convex brushes (Quake-style half-space sets) and | |
| 18 | 20 | arbitrary meshes, added or subtracted, re-meshing only the chunks they touch. |
| 19 | 21 | - **Pluggable generators.** The density field is a `DCGenerator` — built-in noise, an OBJ |
| 20 | mesh SDF, a GPU Gems 3 volume field, a GDScript virtual, or a proc-nodes graph compiled | |
| 21 | to GLSL. | |
| 22 | mesh SDF, a GPU Gems 3 volume field, a GDScript virtual, or a proc-nodes graph compiled to | |
| 23 | GLSL. See [Generators](docs/generators.md). | |
| 22 | 24 | - **Two collision paths.** Concave trimesh bodies through `PhysicsServer3D`, or direct SDF |
| 23 | queries (`get_distance`, `get_capsule_push`) with no physics bodies at all. | |
| 24 | - **A separate exact path.** `DCLevel` meshes authored convex brushes exactly — no voxels, | |
| 25 | no LOD — for the finite, sharp, low-poly parts of a world. | |
| 25 | queries with no physics bodies at all. | |
| 26 | - **A separate exact path.** `DCLevel` meshes authored convex brushes exactly — no voxels, no | |
| 27 | LOD — for the finite, sharp, low-poly parts of a world. See [DCLevel](docs/api-dclevel.md). | |
| 26 | 28 | |
| 27 | 29 | `libdc/` is the meshing core and has no Godot dependency; `src/` is the GDExtension around it. |
| 28 | 30 | |
| … | … | @@ -31,10 +33,10 @@ either on the GPU across ten compute stages or on a pool of CPU worker threads. |
| 31 | 33 | - Godot **4.3+** (`compatibility_minimum`; the demo project targets 4.6) |
| 32 | 34 | - `godot-cpp` at `./godot-cpp` — a submodule or a symlink |
| 33 | 35 | - SCons, a C++17 compiler, OpenMP |
| 34 | - Vulkan headers (`libvulkan-dev` on Linux) — the GPU pipeline talks to `RenderingDevice` | |
| 35 | - [`proc-nodes`](../proc-nodes) checked out **beside** this repo. `SConstruct` adds | |
| 36 | `../proc-nodes/src/shared/` to the include path for `erosion_filter.inc` and friends: one | |
| 37 | copy of that math, compiled here as C++ and there as GLSL. Nothing else couples the two. | |
| 36 | - Vulkan headers (`libvulkan-dev` on Linux) | |
| 37 | - [`proc-nodes`](../proc-nodes) checked out **beside** this repo — `SConstruct` adds | |
| 38 | `../proc-nodes/src/shared/` to the include path for the erosion filter, one copy of that | |
| 39 | math compiled here as C++ and there as GLSL | |
| 38 | 40 | |
| 39 | 41 | ## Build |
| 40 | 42 | |
| … | … | @@ -46,23 +48,16 @@ scons -j$(nproc) target=template_release |
| 46 | 48 | scons -j$(nproc) target=editor |
| 47 | 49 | ``` |
| 48 | 50 | |
| 49 | Build **all three targets**. Godot picks the library by target, and an entry that is missing | |
| 50 | from the `.gdextension` silently falls back to another one — so a stale `template_debug` | |
| 51 | answers for the `editor` you thought you just rebuilt. | |
| 52 | ||
| 53 | Cross-compiling for Windows needs the posix-threads mingw variant (the win32 variant ships | |
| 54 | neither libgomp nor `<thread>`) and an explicit header path: | |
| 55 | ||
| 56 | ```sh | |
| 57 | scons -j$(nproc) platform=windows vulkan_include=/path/to/Vulkan-Headers/include | |
| 58 | ``` | |
| 51 | Build **all three targets**. Godot picks the library by target, and a target missing from the | |
| 52 | `.gdextension` silently falls back to another — so a stale `template_debug` answers for the | |
| 53 | `editor` you thought you just rebuilt. | |
| 59 | 54 | |
| 60 | 55 | ## Install |
| 61 | 56 | |
| 62 | Copy or symlink `dc_terrain.gdextension` and `bin/` into the consuming project, and put the | |
| 63 | `shaders/` directory **beside the library** — the GPU pipeline resolves its shader directory | |
| 64 | from its own module path at runtime (`dladdr` / `GetModuleFileName`), so `bin/linux/shaders` | |
| 65 | is a symlink to `../../shaders`. Override with `DC_TERRAIN_SHADER_DIR`. | |
| 57 | Copy or symlink `dc_terrain.gdextension` and `bin/` into the consuming project, and put | |
| 58 | `shaders/` **beside the library** — the GPU pipeline resolves its shader directory from its | |
| 59 | own module path at runtime, which is why `bin/linux/shaders` is a symlink to `../../shaders`. | |
| 60 | Override with `DC_TERRAIN_SHADER_DIR`. | |
| 66 | 61 | |
| 67 | 62 | If the shaders are not found the extension does not fail: it falls back to CPU meshing, |
| 68 | 63 | quietly, at a fraction of the frame rate. |
| … | … | @@ -77,407 +72,29 @@ extends Node3D |
| 77 | 72 | func _ready() -> void: |
| 78 | 73 | terrain.voxel_size = 1.0 |
| 79 | 74 | terrain.octree_depth = 5 |
| 80 | terrain.use_gpu = true # default is false — see below | |
| 75 | terrain.use_gpu = true # default is false — and the CPU path is texture-blind | |
| 81 | 76 | terrain.collision_mode = 0 # 0 = Trimesh, 1 = SDF, 2 = Both |
| 82 | 77 | terrain.rebuild() |
| 83 | 78 | |
| 84 | 79 | func _unhandled_input(event: InputEvent) -> void: |
| 85 | 80 | if event.is_action_pressed("dig"): |
| 86 | # negative strength removes material, positive adds it | |
| 87 | terrain.edit_terrain(_aim_point(), 3.0, -1.0) | |
| 81 | terrain.edit_terrain(_aim_point(), 3.0, -1.0) # negative strength removes | |
| 88 | 82 | ``` |
| 89 | 83 | |
| 90 | 84 | LOD follows the active `Camera3D` of the viewport — the editor viewport camera in the editor, |
| 91 | 85 | `get_viewport().get_camera_3d()` at runtime. There is nothing to assign. |
| 92 | 86 | |
| 93 | `use_gpu` defaults to **false**. The demo scene sets no properties at all, so it runs the CPU | |
| 94 | mesher; every hyper-tps map sets `use_gpu = true` explicitly. | |
| 95 | ||
| 96 | ## Meshing paths | |
| 97 | ||
| 98 | Six of them. They differ in what they can represent and in what attributes they produce, so | |
| 99 | they are not interchangeable. | |
| 100 | ||
| 101 | #### GPU dual contouring — `src/dc_gpu_mesh_pipeline.cpp`, `shaders/` | |
| 102 | ||
| 103 | Ten compute stages on one `RenderingDevice`: | |
| 104 | ||
| 105 | ``` | |
| 106 | density → edge_classify → scan → stream_compact → edge_intersect | |
| 107 | → voxel_classify → scan → stream_compact → qef_solve → quad_emit → readback | |
| 108 | ``` | |
| 109 | ||
| 110 | The **only** path that produces the full vertex attribute set: position, normal, | |
| 111 | `material_id`, two face-projection candidates (`proj`, `proj2`), and per-vertex curvature in | |
| 112 | metres. Requires `use_gpu = true`. | |
| 113 | ||
| 114 | #### CPU dual contouring — `libdc/src/dc_contour.cpp` | |
| 115 | ||
| 116 | Density grid → octree → QEF → recursive contour (`cellProc`/`faceProc`/`edgeProc`) → clip → | |
| 117 | smooth, on a pool of worker threads auto-scaled to the core count. | |
| 118 | ||
| 119 | **It produces geometry only.** `materials`, `proj`, `proj2` and `curv` come back empty — the | |
| 120 | CPU path preserves them across vertex splits when they already exist but never generates | |
| 121 | them. So `use_textures` and Quake per-face projection do nothing here, and the terrain | |
| 122 | renders in flat debug colours whatever the atlas says. This is the default path, because | |
| 123 | `use_gpu` defaults to false. | |
| 124 | ||
| 125 | #### Seam stitching — `libdc/src/dc_seam.cpp` | |
| 126 | ||
| 127 | Cross-LOD gaps are not faded over, they are filled. Boundary nodes of adjacent cells are | |
| 128 | collected into a **temporary** octree, contoured, and emitted as a separate `ArrayMesh` per | |
| 129 | node, then discarded. Ported from ngildea/leven. `debug_seams` draws them. | |
| 130 | ||
| 131 | #### Exact brush meshing — `libdc/src/dc_level_mesh.cpp` | |
| 132 | ||
| 133 | The `DCLevel` path. **No QEF, no voxels, no smoothing.** Every face is triangulated with its | |
| 134 | own vertices carrying its own plane normal — nothing is welded, so a crease is sharp because | |
| 135 | the geometry says so rather than because an angle threshold noticed it. | |
| 136 | ||
| 137 | Render and collision diverge on purpose: render is the csg4-**clipped** visible skin with | |
| 138 | hidden surface removed; collision is the **unclipped** convex brush hull, which is both exact | |
| 139 | and cheaper than a trimesh. Clipping removes hidden surface, not volume. | |
| 140 | ||
| 141 | #### Voxel pockets — `libdc/include/dc_pocket.h` | |
| 142 | ||
| 143 | A second mesher living inside the exact path, and the one that is easiest to miss. | |
| 144 | ||
| 145 | `DCBrushStore::csg4_view` feeds the exact mesher only **clean** brushes; anything an active | |
| 146 | carve touches is excluded, because a carved solid is non-convex and the winding clip cannot | |
| 147 | represent it. Correct — but until something meshed the excluded brushes, a carve did not | |
| 148 | carve, it *deleted*. A pocket is that missing piece: | |
| 149 | ||
| 150 | ``` | |
| 151 | pocket = DC mesh of ( union(component brushes) − union(active carves) ) | |
| 152 | ``` | |
| 153 | ||
| 154 | The brushes stay authoritative and the pocket is a derived cache that may be thrown away and | |
| 155 | rebuilt. Undo drops a carve and the component returns to the exact mesher unchanged — no | |
| 156 | plane re-fitting, so no error accumulates. Voxels are used here and nowhere else in the exact | |
| 157 | path because a sphere carve is not a half-space: mesh boolean is the wrong *shape* of cost, | |
| 158 | O(global), against voxel DC's O(local). `pocket_voxel_size` tunes it, and `get_stats()` | |
| 159 | reports `pocket_triangles` separately from `triangles` so a pocket that has quietly become | |
| 160 | most of the frame cannot hide inside one number. | |
| 161 | ||
| 162 | #### Simplification — `libdc/src/dc_simplify.cpp` | |
| 163 | ||
| 164 | Two, plus a reorder: | |
| 165 | ||
| 166 | - `dc_simplify_mca` — multiple-choice QEM (Wu & Kobbelt). Pick K random edges, collapse the | |
| 167 | lowest-error one. Fast. | |
| 168 | - `dc_simplify_meshopt` — meshoptimizer with **border vertex locking**, which is what keeps | |
| 169 | T-junctions from opening between LOD levels. Higher quality. | |
| 170 | - a vertex-cache reorder, which changes no geometry. | |
| 171 | ||
| 172 | `use_mesh_simplify` gates the post-process, and it runs on the **GPU path only**. | |
| 173 | ||
| 174 | ## DCTerrain | |
| 175 | ||
| 176 | `Node3D`. The streaming, destructible world. | |
| 177 | ||
| 178 | #### `terrain.rebuild()` | |
| 179 | ||
| 180 | Drop every chunk and re-mesh from scratch. Bumps a generation counter so in-flight worker | |
| 181 | results are discarded. Most property setters call this for you. | |
| 182 | ||
| 183 | #### `terrain.warmup_shaders()` | |
| 184 | ||
| 185 | Draw a one-triangle mesh through each per-LOD terrain material so the render pipelines are | |
| 186 | compiled before real chunks arrive, instead of stuttering as each LOD appears for the first | |
| 187 | time. | |
| 188 | ||
| 189 | #### Shape | |
| 190 | ||
| 191 | ```gdscript | |
| 192 | generator: DCTerrainGenerator # overrides the built-in noise when set | |
| 193 | use_noise: bool = true | |
| 194 | seed: int | |
| 195 | terrain_size: float # 10 .. 100000 | |
| 196 | ground_bias: float # -1 .. 1 | |
| 197 | solid_floor_depth: float | |
| 198 | sky_height: float | |
| 199 | flat_center_radius: float | |
| 200 | spherical: bool # planetoid mode | |
| 201 | planet_radius: float | |
| 202 | floating_islands: bool | |
| 203 | island_radius: float | |
| 204 | island_depth_ratio: float | |
| 205 | island_spacing: float | |
| 206 | ``` | |
| 207 | ||
| 208 | Plus the noise bands — `primary_`, `ridge_`, `tunnel_`, `warp_`, `detail_` each with a | |
| 209 | `_frequency` and `_strength` (and `tunnel_radius`) — and the Voronoi floor set | |
| 210 | (`voronoi_cell_count`, `floor_strength`, `floor_height_min`, `floor_height_max`, | |
| 211 | `room_height`). `max_noise_octaves` and `max_ridge_octaves` cap octave count. | |
| 212 | ||
| 213 | These properties configure the **built-in** noise generator. Assigning `generator` replaces | |
| 214 | it wholesale; the properties then describe nothing on screen. Ask | |
| 215 | `is_using_builtin_generator()` rather than assuming — a generator that fails to load falls | |
| 216 | back to noise, which renders a perfectly plausible landscape that is not the one you asked | |
| 217 | for. | |
| 218 | ||
| 219 | #### Meshing and LOD | |
| 220 | ||
| 221 | ```gdscript | |
| 222 | voxel_size: float = 1.0 # 0.1 .. 10 | |
| 223 | octree_depth: int = 5 # 3 .. 20 | |
| 224 | use_gpu: bool = false | |
| 225 | seamless_lod: bool = false # force every chunk to LOD 0 | |
| 226 | lod_mode: int # 0 = Eager Fade, 1 = Blocking Swap (default) | |
| 227 | lod_factor: float = 3.0 | |
| 228 | hysteresis: float = 1.3 | |
| 229 | fade_speed: float = 3.0 | |
| 230 | use_screen_space_error: bool | |
| 231 | screen_split_threshold: float | |
| 232 | lod_frozen: bool # freeze traversal during a grapple or teleport | |
| 233 | use_adaptive_qef: bool = true | |
| 234 | use_shortest_diagonal: bool = true | |
| 235 | use_mesh_simplify: bool = true # meshoptimizer pass, GPU path only | |
| 236 | force_direct_eval: bool = false | |
| 237 | csg_refinement: int = 2 # 1 .. 4 | |
| 238 | ``` | |
| 239 | ||
| 240 | #### Display | |
| 241 | ||
| 242 | ```gdscript | |
| 243 | terrain_color: Color | |
| 244 | custom_material: Material # overrides the internal per-LOD materials | |
| 245 | use_textures: bool = false # sample the atlas instead of flat debug colors | |
| 246 | normal_perturbation: float = 0.12 | |
| 247 | ``` | |
| 248 | ||
| 249 | `set_texture_atlas(atlas)`, `set_glow_atlas(atlas)`, `set_norm_atlas(atlas)` take a | |
| 250 | `Texture2DArray` whose layer index is the `material_id`. `set_projection_table(table)` takes | |
| 251 | 12 floats per entry, indexed per-face. | |
| 252 | ||
| 253 | #### Collision | |
| 254 | ||
| 255 | ```gdscript | |
| 256 | collision_mode: int = 0 # 0 = Trimesh, 1 = SDF, 2 = Both | |
| 257 | collision_lod_max: int = 5 # collision is built for LOD 0..N only | |
| 258 | collision_distance: float = 100.0 | |
| 259 | ``` | |
| 260 | ||
| 261 | In SDF mode nothing is added to the scene tree — query the field directly: | |
| 262 | ||
| 263 | #### `terrain.get_distance(position) -> float` | |
| 264 | ||
| 265 | Signed distance to the surface. Positive inside solid, negative in air. | |
| 266 | ||
| 267 | #### `terrain.get_surface_normal(position) -> Vector3` | |
| 268 | ||
| 269 | Normalized gradient of the density field. | |
| 270 | ||
| 271 | #### `terrain.get_capsule_push(position, radius, height) -> Vector3` | |
| 272 | ||
| 273 | Push-out vector for a capsule, or zero. Tests bottom, centre and top. | |
| 274 | ||
| 275 | #### `terrain.sample_density(position) -> float` | |
| 87 | ## Documentation | |
| 276 | 88 | |
| 277 | The raw field, for spawn placement and the like. Positive is solid. | |
| 278 | ||
| 279 | #### Editing | |
| 280 | ||
| 281 | #### `terrain.edit_terrain(position, radius, strength, op = 0, interactive = true, material_id = 0)` | |
| 282 | ||
| 283 | A sphere. `strength` positive adds material, negative removes it. `op` is `0` for the smooth | |
| 284 | metaball blend or `1` for hard SDF CSG. | |
| 285 | ||
| 286 | `interactive` picks the worker lane. `true` is a live carve and takes the priority lane for | |
| 287 | immediate feedback; `false` is a replayed or network-synced edit and goes at normal priority, | |
| 288 | because flooding the priority lane starves LOD pre-meshing. | |
| 289 | ||
| 290 | #### `terrain.edit_terrain_box(position, half_extents, strength, interactive = true, material_id = 0, persistent = false)` | |
| 291 | #### `terrain.edit_terrain_convex(planes, center, radius, strength, interactive = true, material_id = 0, face_proj = [])` | |
| 292 | ||
| 293 | A convex brush is an intersection of half-spaces: `planes` is flat `[nx, ny, nz, d, ...]` with | |
| 294 | outward normals, inside being `n·p <= d` — the same layout a Quake `.map` importer already | |
| 295 | produces. `center` and `radius` are the bounding sphere used for chunk culling. | |
| 296 | ||
| 297 | #### `terrain.stamp_mesh_obj(obj_path, position, scale, strength, interactive = true, material_id = 0)` | |
| 298 | #### `terrain.stamp_mesh_arrays(vertices, indices, position, scale, strength, interactive = true, material_id = 0, persistent = false)` | |
| 299 | ||
| 300 | `persistent = true` marks an edit as **map-authored landscape**: it survives `clear_edits()`. | |
| 301 | There are two journals over one edit list — the player's, which the game owns and replays, and | |
| 302 | the map's, which must not be dropped with it. Use `clear_landscape_edits()` to drop only the | |
| 303 | latter. | |
| 304 | ||
| 305 | ```gdscript | |
| 306 | terrain.clear_edits() # player journal | |
| 307 | terrain.clear_landscape_edits() # map-authored landscape; does not rebuild | |
| 308 | terrain.get_edit_count() | |
| 309 | terrain.get_edits_as_array() | |
| 310 | terrain.load_edits_from_array(edits) | |
| 311 | ``` | |
| 312 | ||
| 313 | Brushes already placed can be picked and moved: `pick_brush(point)`, `get_brush_center(idx)`, | |
| 314 | `get_brush_radius(idx)`, `get_brush_hull_triangles(idx)`, `lift_brush(idx)`, | |
| 315 | `move_brush(idx, delta)`. | |
| 316 | ||
| 317 | #### CSG primitives | |
| 318 | ||
| 319 | Live primitives, distinct from the edit list — combined within a `group`, then unioned with | |
| 320 | the terrain. | |
| 321 | ||
| 322 | ```gdscript | |
| 323 | terrain.add_csg_primitive(type, op, group, position, size) | |
| 324 | # type: 0 = sphere, 1 = box. op: 0 = union, 1 = subtract, 2 = intersect. | |
| 325 | # size: sphere = [radius, 0, 0], box = [hx, hy, hz] | |
| 326 | terrain.set_csg_primitive_position(index, position) | |
| 327 | terrain.set_csg_primitive_size(index, size) | |
| 328 | terrain.remove_csg_primitive(index) | |
| 329 | terrain.clear_csg_primitives() | |
| 330 | terrain.get_csg_primitive_count() | |
| 331 | ``` | |
| 332 | ||
| 333 | #### Stats and instruments | |
| 334 | ||
| 335 | ```gdscript | |
| 336 | terrain.get_chunk_count() | |
| 337 | terrain.get_active_chunk_count() | |
| 338 | terrain.get_pending_count() | |
| 339 | terrain.get_collision_queue_size() | |
| 340 | terrain.get_splits_this_frame() | |
| 341 | ``` | |
| 342 | ||
| 343 | The debug flags — `debug_logging`, `debug_lod_colors`, `debug_seams`, `debug_clipmap`, | |
| 344 | `debug_wireframe`, `debug_edits` — draw overlays. The rest answer questions no overlay can: | |
| 345 | ||
| 346 | #### `terrain.build_stamp() -> String` | |
| 347 | ||
| 348 | Which binary this is, baked at compile time. The `.so` under a consuming project's `addons/` | |
| 349 | is typically a symlink into `dc-terrain/bin` and is not rebuilt by anything automatic — so | |
| 350 | "I changed the C++" and "the game runs the change" are independent facts. This settles the | |
| 351 | second one. | |
| 352 | ||
| 353 | #### `terrain.debug_gpu_density(origin, voxel_size, cells) -> PackedFloat32Array` | |
| 354 | ||
| 355 | The real density pipeline — same shader, same uniform set, same buffers as the mesher — over | |
| 356 | a small grid. `sample_density()` is the CPU twin; comparing the two is the parity check. | |
| 357 | Returns empty if the GPU refused, which is itself the answer. A local `RenderingDevice` is | |
| 358 | null under `--headless`, so this only answers in a real render session. | |
| 359 | ||
| 360 | #### `terrain.debug_chunk_info(position) -> Dictionary` | |
| 361 | ||
| 362 | The streaming state of the leaf under a point: `lod`, `voxel_size`, `chunk_size`, `state`, | |
| 363 | `triangles`, `has_collision`, `collision_eligible`, `origin`. Empty when no leaf contains the | |
| 364 | point — distinct from LOD 0. | |
| 365 | ||
| 366 | #### `terrain.debug_collision_faces(center, radius) -> PackedVector3Array` | |
| 367 | #### `terrain.debug_visual_faces(center, radius) -> PackedVector3Array` | |
| 368 | ||
| 369 | Collision triangles and drawn triangles, both in world space, for comparison. Collision is | |
| 370 | built as bare `PhysicsServer3D` RIDs rather than scene-tree nodes, so this is the only way to | |
| 371 | see it. Gaps are the finding: nothing is built past `collision_lod_max`, however solid the | |
| 372 | terrain looks. | |
| 373 | ||
| 374 | #### `terrain.bake_volume_mesh(glsl, origin, voxel_size, cells) -> ArrayMesh` | |
| 375 | ||
| 376 | Takes a `float proc_density(vec3 pos)` GLSL body, fills a density grid with it and meshes it, | |
| 377 | both through the GPU thread the terrain already runs. Returns null if the GPU declined. It | |
| 378 | lives here rather than in proc-nodes because a second `VkDevice` bring-up starves — one | |
| 379 | device, one GPU thread, and this borrows them. | |
| 380 | ||
| 381 | `get_scalar_query_counts()` / `reset_scalar_query_counts()` count scalar-path evaluations, so | |
| 382 | "how much of the frame is the CPU field" is a number rather than a guess. | |
| 383 | ||
| 384 | ## DCLevel | |
| 385 | ||
| 386 | `Node3D`. The exact path: authored convex brushes, meshed exactly, with no voxels and no LOD. | |
| 387 | ||
| 388 | It shares no code with the octree clipmap. The world — procedural, destructible everywhere — | |
| 389 | keeps `DCTerrain`; the level — authored, finite, sharp, low-poly — gets this. | |
| 390 | ||
| 391 | ``` | |
| 392 | BrushStore --csg4--> visible skin --dc_level_mesh--> ArrayMesh | |
| 393 | \--unclipped hulls--------------------> convex colliders | |
| 394 | ``` | |
| 395 | ||
| 396 | ```gdscript | |
| 397 | var id := level.add_brush(planes, material_id, plane_proj, level_id, sky, key, clearance, group) | |
| 398 | level.rebuild() | |
| 399 | ``` | |
| 400 | ||
| 401 | `planes` is the same flat `(nx, ny, nz, d)` layout `edit_terrain_convex` takes, so an importer | |
| 402 | drives either. `key` is the brush's durable name — pass `""` for an import, which is named | |
| 403 | deterministically on every peer. `sky` renders and clips but gets no collider. `clearance` | |
| 404 | marks air the level owns — the headroom over a stair, the bore of an archway — rendering and | |
| 405 | colliding with nothing, existing only so `build_shell` encloses it. | |
| 406 | ||
| 407 | Editing is by durable key and is journalled and reversible: | |
| 408 | ||
| 409 | ```gdscript | |
| 410 | level.translate_brush(id, delta) | |
| 411 | level.scale_brush(id, scale, pivot) | |
| 412 | level.split_brush(id, normal, d, key) | |
| 413 | level.subtract_brush(id, cutter, key) | |
| 414 | level.intersect_brush(id, other, key) | |
| 415 | level.merge_brushes(ids, key) | |
| 416 | level.add_face(id, normal, d, proj, key) | |
| 417 | level.remove_face(id, face) | |
| 418 | level.set_face_plane(id, face, normal, d) | |
| 419 | ||
| 420 | level.apply_carve_sphere(center, radius, key) | |
| 421 | level.apply_carve_box(center, half_extents, key) | |
| 422 | level.undo_carve_by_key(key) | |
| 423 | level.redo_carve_by_key(key) | |
| 424 | ||
| 425 | level.apply_journal(records) # replay | |
| 426 | level.fingerprint() # convergence check | |
| 427 | ``` | |
| 428 | ||
| 429 | `build_shell(cell, clamp_cells)` derives the shell the surrounding `DCTerrain` must be cut to. | |
| 430 | `build_shell_start` / `shell_building` / `shell_ready` / `build_shell_take` are the async form. | |
| 431 | ||
| 432 | `raycast(from, to, include_clearance)` and `pick_face(brush_id, point)` support editor picking; | |
| 433 | `get_stats()` reports brush, face, triangle, vertex, collider, carve and pocket counts plus | |
| 434 | per-stage milliseconds (`ms_split`, `ms_clip`, `ms_mesh`, `ms_upload`, `ms_collide`, | |
| 435 | `ms_pocket`). Clearance and pocket figures are reported *separately* on purpose: a level | |
| 436 | showing 1190 brushes and 12747 triangles has not lost seven of them, it has seven passages — | |
| 437 | and pocket triangles have a different cost curve from the exact mesh, so one combined number | |
| 438 | would hide a pocket that had quietly become most of the frame. | |
| 439 | ||
| 440 | ## Generators | |
| 441 | ||
| 442 | Assign a `DCTerrainGenerator` resource to `DCTerrain.generator` to replace the built-in field. | |
| 443 | ||
| 444 | | Class | Field | | |
| 89 | | | | | |
| 445 | 90 | |---|---| |
| 446 | | `DCTerrainGeneratorNoise` | The built-in noise, as a resource. Same properties as `DCTerrain`. | | |
| 447 | | `DCTerrainGeneratorMeshSDF` | An SDF from an OBJ mesh. `mesh_path`, `reload()`, `get_triangle_count()`. | | |
| 448 | | `DCTerrainGeneratorGeiss` | GPU Gems 3 ch.1. **GPU only**, and produces nothing but air until `volume_dir` points at the eight `16³` volumes — deliberately, so a half-loaded field cannot pass for the real one. Check `is_loaded()`. | | |
| 449 | | `DCTerrainGeneratorProcNodes` | A proc-nodes graph compiled to a GLSL density function, reached by ClassDB reflection so there is no compile-time dependency. Float and Int socket defaults are auto-discovered and exposed as grouped inspector properties. | | |
| 450 | | `DCTerrainGeneratorProcNodesBounded` | The same graph bridge at **asset** scale: no UV-space mapping, no added noise layers, no vertical clip — `evaluate` takes world coordinates and the field is meshed at the size the graph says it is. Bounds come from the graph's own `Bounds Min` / `Bounds Max` group outputs; a graph declaring none is honestly unbounded. **GPU only** — `evaluate` returns air on the CPU path rather than inventing a field. | | |
| 451 | | `DCTerrainGeneratorScript` | Override `_evaluate(x, y, z, lod_voxel_size) -> float` in GDScript. Called from worker threads and serialized through a mutex, so it is slow — for prototyping, not production. | | |
| 452 | ||
| 453 | `DCTerrainUtils.mesh_from_generator(generator, origin, voxel_size, grid_size)` and | |
| 454 | `mesh_from_generator_auto(generator, voxel_size)` mesh a generator straight to an `ArrayMesh` | |
| 455 | without a terrain node. | |
| 456 | ||
| 457 | ## Testing | |
| 458 | ||
| 459 | `libdc` is Godot-free, so its gates are plain executables. Each one carries its own build line | |
| 460 | in its header comment: | |
| 461 | ||
| 462 | ```sh | |
| 463 | # from the repo root | |
| 464 | g++ -O2 -std=c++17 -I libdc/include -o /tmp/csg4_gate \ | |
| 465 | tests/test_csg4_gate.cpp libdc/src/dc_csg4.cpp | |
| 466 | /tmp/csg4_gate | |
| 467 | ``` | |
| 468 | ||
| 469 | `test/CMakeLists.txt` builds `test_fandisk`, the meshing-accuracy check against | |
| 470 | `test/data/fandisk.obj`: | |
| 471 | ||
| 472 | ```sh | |
| 473 | cmake -S test -B test/build && cmake --build test/build && ./test/build/test_fandisk | |
| 474 | ``` | |
| 91 | | [Meshing paths](docs/meshing.md) | The six meshers, what each represents, and which vertex attributes each produces | | |
| 92 | | [DCTerrain API](docs/api-dcterrain.md) | Properties, editing, collision, and the debug instruments | | |
| 93 | | [DCLevel API](docs/api-dclevel.md) | The exact brush path — import, journalled editing, carves, shells | | |
| 94 | | [Generators](docs/generators.md) | The five generator resources and the `DCGenerator` interface | | |
| 95 | | [Testing](docs/testing.md) | How the gates are built and run | | |
| 475 | 96 | |
| 476 | GPU gates compile and dispatch **standalone**, on their own `VkDevice`, in their own process, | |
| 477 | linking `-lvulkan` — because the in-engine oracle submits work on the live device, and letting | |
| 478 | it run long enough to answer is what once interleaved its uniform set with the mesh pipeline's | |
| 479 | and emptied the world. `tests/gate_hblk_compile.sh` and `tests/gate_polish_units.py` are | |
| 480 | scripted gates; `demo/*.gd` are gates that need a running Godot. | |
| 97 | Design notes, postmortems and the ProcWorld archive are also under [`docs/`](docs/). | |
| 481 | 98 | |
| 482 | 99 | ## Layout |
| 483 | 100 | |
| … | … | @@ -488,7 +105,7 @@ shaders/ compute shaders, loaded from disk beside the library at runtime |
| 488 | 105 | tests/ libdc + GPU gates, one file each, build line in the header |
| 489 | 106 | test/ CMake-built fandisk accuracy check |
| 490 | 107 | demo/ a Godot project: fly camera, CSG edits, CPU/GPU benchmark |
| 491 | docs/ designs, postmortems, and a mirror of the ProcWorld archive | |
| 108 | docs/ API reference, designs, postmortems, ProcWorld archive | |
| 492 | 109 | thirdparty/ meshoptimizer (MIT) |
| 493 | 110 | ``` |
| 494 | 111 |
| … | … | @@ -0,0 +1,152 @@ |
| 1 | # DCLevel API | |
| 2 | ||
| 3 | `Node3D`. The exact path: authored convex brushes, meshed exactly, with no voxels and no LOD. | |
| 4 | ||
| 5 | It shares no code with the octree clipmap. The world — procedural, destructible everywhere — | |
| 6 | keeps [`DCTerrain`](api-dcterrain.md); the level — authored, finite, sharp, low-poly — gets | |
| 7 | this. That separation is the architectural pivot, not an optimisation. | |
| 8 | ||
| 9 | ``` | |
| 10 | BrushStore --csg4--> visible skin --dc_level_mesh--> ArrayMesh | |
| 11 | \--unclipped hulls--------------------> convex colliders | |
| 12 | ``` | |
| 13 | ||
| 14 | How the mesh is built, and why carved brushes need a second mesher, is in | |
| 15 | [Meshing paths](meshing.md#exact-brush-meshing). | |
| 16 | ||
| 17 | ## Import | |
| 18 | ||
| 19 | #### `add_brush(planes, material_id, plane_proj, level_id, sky, key, clearance, group) -> int` | |
| 20 | ||
| 21 | `planes` is a flat `(nx, ny, nz, d)` quad list — the same layout | |
| 22 | [`DCTerrain.edit_terrain_convex`](api-dcterrain.md#editing) takes, so one importer drives | |
| 23 | either. `plane_proj` is a parallel per-face projection index (1-based; empty = none). | |
| 24 | ||
| 25 | - `key` — the brush's **durable** name, the dot of the op that created it. Pass `""` for an | |
| 26 | import, which is then named `i:<level_id>:<n>` deterministically on every peer. | |
| 27 | - `sky` — a Quake sky brush. Renders and clips normally but gets no collider, and is the | |
| 28 | authored outside-wall marker. | |
| 29 | - `clearance` — **air the level owns** rather than geometry: the headroom over a flight of | |
| 30 | stairs, the bore of an archway driven into a hillside. It renders nothing, collides with | |
| 31 | nothing and clips nothing. Its only effect is that `build_shell` encloses it, so the | |
| 32 | surrounding `DCTerrain` is cut away to leave the passage. | |
| 33 | - `group` — brushes in one group are one thing, so an arch is a single object rather than | |
| 34 | nine brushes. | |
| 35 | ||
| 36 | Returns `0` when the plane set encloses nothing, **or** when a brush with this key already | |
| 37 | exists — in which case the return is that brush's id. Replay is not once-only, so a create | |
| 38 | must be idempotent. | |
| 39 | ||
| 40 | #### `apply_journal(records)` | |
| 41 | ||
| 42 | Replay a journal of ops. | |
| 43 | ||
| 44 | #### `fingerprint()` | |
| 45 | ||
| 46 | A convergence check — two peers that applied the same journal should agree. | |
| 47 | ||
| 48 | ## Editing | |
| 49 | ||
| 50 | All of these take a durable `key` where an op creates or renames geometry. | |
| 51 | ||
| 52 | ```gdscript | |
| 53 | translate_brush(brush_id, delta) | |
| 54 | scale_brush(brush_id, scale, pivot) | |
| 55 | move_brush_points(brush_id, from, delta, key) | |
| 56 | split_brush(brush_id, normal, d, key) | |
| 57 | subtract_brush(brush_id, cutter, key) | |
| 58 | intersect_brush(brush_id, other, key) | |
| 59 | merge_brushes(brush_ids, key) | |
| 60 | ||
| 61 | set_face_plane(brush_id, face, normal, d) | |
| 62 | add_face(brush_id, normal, d, proj, key) | |
| 63 | remove_face(brush_id, face) | |
| 64 | ||
| 65 | remove_brush(brush_id) / restore_brush(brush_id) | |
| 66 | remove_level(level_id) / restore_level(level_id) | |
| 67 | clear() | |
| 68 | ``` | |
| 69 | ||
| 70 | Hiding is a **view**, filtered at mesh emit and never at csg4's input — so a hidden brush | |
| 71 | still clips its neighbours: | |
| 72 | ||
| 73 | ```gdscript | |
| 74 | set_brush_hidden(brush_id, hidden) | |
| 75 | is_brush_hidden(brush_id) -> bool | |
| 76 | get_hidden_brush_ids() | |
| 77 | clear_hidden() | |
| 78 | ``` | |
| 79 | ||
| 80 | ## Carves | |
| 81 | ||
| 82 | A carve leaves brush-land, so it is meshed as a [voxel pocket](meshing.md#voxel-pockets). | |
| 83 | ||
| 84 | ```gdscript | |
| 85 | apply_carve_sphere(center, radius, key) -> int | |
| 86 | apply_carve_box(center, half_extents, key) -> int | |
| 87 | undo_carve(carve_id) / redo_carve(carve_id) | |
| 88 | undo_carve_by_key(key) / redo_carve_by_key(key) | |
| 89 | ``` | |
| 90 | ||
| 91 | Undo drops the carve and the component returns to the exact mesher unchanged — no plane | |
| 92 | re-fitting, so no error accumulates over repeated undo/redo. | |
| 93 | ||
| 94 | ## Shells | |
| 95 | ||
| 96 | #### `build_shell(cell, clamp_cells)` | |
| 97 | ||
| 98 | The sealed volume the level occupies, as a mesh, for one subtractive edit into the | |
| 99 | surrounding `DCTerrain`. Clearance brushes are enclosed by it. | |
| 100 | ||
| 101 | #### `build_shell_start(cell, clamp_cells)` · `shell_building()` · `shell_ready()` · `build_shell_take()` | |
| 102 | ||
| 103 | The same, off the caller's thread. | |
| 104 | ||
| 105 | ## Queries | |
| 106 | ||
| 107 | ```gdscript | |
| 108 | get_brush(brush_id) -> Dictionary | |
| 109 | brush_by_key(key) -> int / get_brush_key(brush_id) -> String | |
| 110 | get_brush_group(brush_id) / get_group_brush_ids(group) / get_group_keys() | |
| 111 | get_live_brush_ids() / get_sky_brush_ids() / get_clearance_brush_ids() | |
| 112 | ||
| 113 | pick_face(brush_id, point) -> int | |
| 114 | face_by_key(brush_id, key) -> int | |
| 115 | get_face_winding(brush_id, face) | |
| 116 | face_is_live(brush_id, face) -> bool | |
| 117 | raycast(from, to, include_clearance) -> Dictionary | |
| 118 | ||
| 119 | get_brush_edges() / get_brush_edges_of(brush_id) / get_brush_vertices_of(brush_id) | |
| 120 | get_clearance_edges() | |
| 121 | get_cut_preview(brush_id, normal, d) | |
| 122 | get_brush_id_from_shape(shape_index) -> int | |
| 123 | ``` | |
| 124 | ||
| 125 | ## Properties and stats | |
| 126 | ||
| 127 | ```gdscript | |
| 128 | build_collision: bool # the only inspector property on this class | |
| 129 | ``` | |
| 130 | ||
| 131 | `pocket_voxel_size` has a bound setter and getter but **no inspector property**, so it is | |
| 132 | reachable only from script. | |
| 133 | ||
| 134 | ```gdscript | |
| 135 | get_brush_count() get_triangle_count() get_vertex_count() | |
| 136 | get_collider_count() get_carve_count() get_pocket_count() | |
| 137 | rebuild() | |
| 138 | ``` | |
| 139 | ||
| 140 | #### `get_stats() -> Dictionary` | |
| 141 | ||
| 142 | `brushes`, `faces`, `triangles`, `vertices`, `faces_removed`, `faces_fragmented`, | |
| 143 | `surface_area`, `max_normal_deviation`, `colliders`, `sky_brushes`, `clearance_brushes`, | |
| 144 | `hidden_brushes`, `carves`, `pockets`, `pocket_triangles`, `pocket_voxel_size`, | |
| 145 | `pockets_rebuilt`, and per-stage milliseconds `ms_split`, `ms_clip`, `ms_mesh`, `ms_upload`, | |
| 146 | `ms_collide`, `ms_pocket`. | |
| 147 | ||
| 148 | Clearance brushes are counted separately because `brushes` includes them and they are not | |
| 149 | geometry — a level reporting 1190 brushes and 12747 triangles has not lost seven of them, it | |
| 150 | has seven passages. `pocket_triangles` is separate from `triangles` for the same class of | |
| 151 | reason: the two have different cost curves, and one combined number would hide a pocket that | |
| 152 | had quietly become most of the frame. |
| … | … | @@ -0,0 +1,279 @@ |
| 1 | # DCTerrain API | |
| 2 | ||
| 3 | `Node3D`. The streaming, destructible world. For the exact authored path see | |
| 4 | [DCLevel](api-dclevel.md); for what the meshers do and do not produce see | |
| 5 | [Meshing paths](meshing.md). | |
| 6 | ||
| 7 | ## Lifecycle | |
| 8 | ||
| 9 | #### `rebuild()` | |
| 10 | ||
| 11 | Drop every chunk and re-mesh from scratch. Bumps a generation counter so in-flight worker | |
| 12 | results are discarded. Most property setters call this for you. | |
| 13 | ||
| 14 | #### `warmup_shaders()` | |
| 15 | ||
| 16 | Draw a one-triangle mesh through each per-LOD terrain material, so the render pipelines are | |
| 17 | compiled before real chunks arrive instead of stuttering as each LOD appears for the first | |
| 18 | time. This warms the **render** materials, not the compute pipelines. | |
| 19 | ||
| 20 | #### `is_using_builtin_generator() -> bool` | |
| 21 | ||
| 22 | Whether the built-in noise is what is actually meshing. Ask, rather than assume: a `generator` | |
| 23 | that fails to load falls back to noise, which renders a perfectly plausible landscape that is | |
| 24 | not the one you asked for. | |
| 25 | ||
| 26 | ## Shape | |
| 27 | ||
| 28 | These configure the **built-in** noise generator. Assigning `generator` replaces it wholesale, | |
| 29 | after which they describe nothing on screen. | |
| 30 | ||
| 31 | ```gdscript | |
| 32 | generator: DCTerrainGenerator # overrides the built-in noise when set | |
| 33 | use_noise: bool | |
| 34 | seed: int | |
| 35 | terrain_size: float # 10 .. 100000 | |
| 36 | ground_bias: float # -1 .. 1 | |
| 37 | solid_floor_depth: float # 0 .. 100 | |
| 38 | sky_height: float # 0 .. 100 | |
| 39 | flat_center_radius: float # 0 .. 50 | |
| 40 | spherical: bool # planetoid mode | |
| 41 | planet_radius: float # 10 .. 1000000 | |
| 42 | floating_islands: bool | |
| 43 | island_radius: float # 10 .. 500 | |
| 44 | island_depth_ratio: float # 0.5 .. 10 | |
| 45 | island_spacing: float # 50 .. 1000 | |
| 46 | max_noise_octaves: int # 0 .. 20 | |
| 47 | max_ridge_octaves: int # 0 .. 20 | |
| 48 | ``` | |
| 49 | ||
| 50 | Noise bands, each a `_frequency` (0.001 .. 1.0) and a `_strength`: | |
| 51 | ||
| 52 | ```gdscript | |
| 53 | primary_frequency / primary_strength # strength 0 .. 5 | |
| 54 | ridge_frequency / ridge_strength # strength 0 .. 5 | |
| 55 | tunnel_frequency / tunnel_strength # strength 0 .. 5, plus tunnel_radius 0 .. 2 | |
| 56 | warp_frequency / warp_strength # strength 0 .. 30 | |
| 57 | detail_frequency / detail_strength # strength 0 .. 2 | |
| 58 | ``` | |
| 59 | ||
| 60 | Voronoi floors: | |
| 61 | ||
| 62 | ```gdscript | |
| 63 | voronoi_cell_count: int # 0 .. 20 | |
| 64 | floor_strength: float # 0 .. 3 | |
| 65 | floor_height_min: float # -50 .. 50 | |
| 66 | floor_height_max: float # -50 .. 50 | |
| 67 | room_height: float # 1 .. 30 | |
| 68 | ``` | |
| 69 | ||
| 70 | ## Meshing and LOD | |
| 71 | ||
| 72 | ```gdscript | |
| 73 | voxel_size: float # 0.1 .. 10 | |
| 74 | octree_depth: int # 3 .. 20 | |
| 75 | use_gpu: bool # default false — the CPU path is texture-blind | |
| 76 | seamless_lod: bool # force every chunk to LOD 0 | |
| 77 | lod_mode: int # 0 = Eager Fade, 1 = Blocking Swap | |
| 78 | lod_factor: float # 1 .. 8 | |
| 79 | hysteresis: float # 1 .. 2 | |
| 80 | fade_speed: float # 0.5 .. 20 | |
| 81 | use_screen_space_error: bool | |
| 82 | screen_split_threshold: float # 0.1 .. 4 | |
| 83 | lod_frozen: bool # freeze traversal during a grapple or teleport | |
| 84 | use_adaptive_qef: bool | |
| 85 | use_shortest_diagonal: bool | |
| 86 | use_mesh_simplify: bool # meshoptimizer post-process, GPU path only | |
| 87 | adaptive_arch_lod: bool # experimental architecture sub-LOD | |
| 88 | arch_min_lod: int | |
| 89 | force_direct_eval: bool | |
| 90 | csg_refinement: int # 1 .. 4 | |
| 91 | ``` | |
| 92 | ||
| 93 | `lod_mode` **Blocking Swap** holds a split until the children are meshed (the godot-voxel | |
| 94 | pattern); **Eager Fade** splits immediately and crossfades over the gap. | |
| 95 | ||
| 96 | #### `debug_kick_lod()` | |
| 97 | ||
| 98 | Force one LOD re-traversal — a pure `invalidate_settled`, with no freeze and no camera move. | |
| 99 | ||
| 100 | ## Display | |
| 101 | ||
| 102 | ```gdscript | |
| 103 | terrain_color: Color | |
| 104 | custom_material: Material # overrides the internal per-LOD materials | |
| 105 | use_textures: bool # sample the atlas instead of flat debug colours | |
| 106 | normal_perturbation: float # 0 .. 0.3 | |
| 107 | ``` | |
| 108 | ||
| 109 | #### `set_texture_atlas(atlas)` · `set_glow_atlas(atlas)` · `set_norm_atlas(atlas)` | |
| 110 | ||
| 111 | `Texture2DArray` whose **layer index is the `material_id`**. Glow is RGBA — rgb emissive, a | |
| 112 | gloss that drives roughness. Norm is a tangent-space normal map. | |
| 113 | ||
| 114 | #### `set_projection_table(table)` | |
| 115 | ||
| 116 | `PackedFloat32Array`, 12 floats per entry, uploaded as an RGBA32F data texture and sampled by | |
| 117 | the shader through each vertex's projection index. | |
| 118 | ||
| 119 | These only have an effect on the GPU path, which is the only one that emits `material_id` and | |
| 120 | projection indices. | |
| 121 | ||
| 122 | ## Collision | |
| 123 | ||
| 124 | ```gdscript | |
| 125 | collision_mode: int # 0 = Trimesh, 1 = SDF, 2 = Both | |
| 126 | collision_lod_max: int # collision built for LOD 0..N | |
| 127 | collision_distance: float # 10 .. 500 | |
| 128 | ``` | |
| 129 | ||
| 130 | Trimesh collision is built as bare `PhysicsServer3D` RIDs hung off the clipmap nodes, not as | |
| 131 | scene-tree `CollisionShape3D`s — so nothing in GDScript can see it directly. Requests beyond | |
| 132 | `collision_distance` are held in a deferred queue and promoted on approach rather than built. | |
| 133 | ||
| 134 | In SDF mode nothing is added to the scene tree; query the field: | |
| 135 | ||
| 136 | #### `get_distance(position) -> float` | |
| 137 | ||
| 138 | Signed distance to the surface. Positive inside solid, negative in air. | |
| 139 | ||
| 140 | #### `get_surface_normal(position) -> Vector3` | |
| 141 | ||
| 142 | Normalized gradient of the density field. | |
| 143 | ||
| 144 | #### `get_capsule_push(position, radius, height) -> Vector3` | |
| 145 | ||
| 146 | Push-out vector for a capsule, or zero if there is no intersection. Tests bottom, centre and | |
| 147 | top. | |
| 148 | ||
| 149 | #### `sample_density(position) -> float` | |
| 150 | ||
| 151 | The raw field, for spawn placement and similar. Positive is solid. | |
| 152 | ||
| 153 | ## Editing | |
| 154 | ||
| 155 | #### `edit_terrain(position, radius, strength, op = 0, interactive = true, material_id = 0)` | |
| 156 | ||
| 157 | A sphere. `strength` positive adds material, negative removes it. `op` is `0` for the smooth | |
| 158 | metaball blend or `1` for hard SDF CSG — the sharp union/difference a box already does. | |
| 159 | ||
| 160 | `interactive` selects the worker lane. `true` is a live carve and takes the **priority** lane | |
| 161 | for immediate feedback; `false` is a replayed or network-synced edit and goes at normal | |
| 162 | priority, because flooding the priority lane starves LOD pre-meshing. | |
| 163 | ||
| 164 | #### `edit_terrain_box(position, half_extents, strength, interactive = true, material_id = 0, persistent = false)` | |
| 165 | ||
| 166 | #### `edit_terrain_convex(planes, center, radius, strength, interactive = true, material_id = 0, face_proj = [])` | |
| 167 | ||
| 168 | A convex brush is an intersection of half-spaces. `planes` is a flat | |
| 169 | `[nx, ny, nz, d, ...]` with outward normals, inside being `n·p <= d` — the layout a Quake | |
| 170 | `.map` importer already produces, and the same one [`DCLevel.add_brush`](api-dclevel.md) | |
| 171 | takes, so one importer drives either. `center` and `radius` are the bounding sphere used for | |
| 172 | chunk culling. `face_proj` is a parallel per-face projection index. | |
| 173 | ||
| 174 | #### `stamp_mesh_obj(obj_path, position, scale, strength, interactive = true, material_id = 0)` | |
| 175 | ||
| 176 | #### `stamp_mesh_arrays(vertices, indices, position, scale, strength, interactive = true, material_id = 0, persistent = false)` | |
| 177 | ||
| 178 | `persistent = true` marks an edit as **map-authored landscape**: it survives `clear_edits()`. | |
| 179 | There are two journals over one edit list — the player's, which the game owns and replays, and | |
| 180 | the map's, which must not be dropped with it. | |
| 181 | ||
| 182 | ```gdscript | |
| 183 | clear_edits() # the player journal | |
| 184 | clear_landscape_edits() # map-authored landscape only; does NOT rebuild — stamp right after | |
| 185 | get_edit_count() -> int | |
| 186 | get_edits_as_array() -> Array | |
| 187 | load_edits_from_array(edits) | |
| 188 | ``` | |
| 189 | ||
| 190 | Placed brushes can be picked and moved: | |
| 191 | ||
| 192 | ```gdscript | |
| 193 | pick_brush(point) -> int | |
| 194 | get_brush_center(idx) -> Vector3 | |
| 195 | get_brush_radius(idx) -> float | |
| 196 | get_brush_hull_triangles(idx) -> PackedVector3Array | |
| 197 | lift_brush(idx) -> Dictionary | |
| 198 | move_brush(idx, delta) | |
| 199 | ``` | |
| 200 | ||
| 201 | ## CSG primitives | |
| 202 | ||
| 203 | Live primitives, distinct from the edit list. Primitives sharing a `group` are combined, then | |
| 204 | unioned with the terrain. | |
| 205 | ||
| 206 | ```gdscript | |
| 207 | add_csg_primitive(type, op, group, position, size) | |
| 208 | # type: 0 = sphere, 1 = box | |
| 209 | # op: 0 = union, 1 = subtract, 2 = intersect | |
| 210 | # size: sphere = (radius, 0, 0), box = (hx, hy, hz) | |
| 211 | ||
| 212 | set_csg_primitive_position(index, position) | |
| 213 | set_csg_primitive_size(index, size) | |
| 214 | remove_csg_primitive(index) | |
| 215 | clear_csg_primitives() | |
| 216 | get_csg_primitive_count() -> int | |
| 217 | ``` | |
| 218 | ||
| 219 | ## Stats | |
| 220 | ||
| 221 | ```gdscript | |
| 222 | get_chunk_count() -> int | |
| 223 | get_active_chunk_count() -> int | |
| 224 | get_pending_count() -> int | |
| 225 | get_collision_queue_size() -> int | |
| 226 | get_splits_this_frame() -> int | |
| 227 | ``` | |
| 228 | ||
| 229 | ## Debug instruments | |
| 230 | ||
| 231 | The flags — `debug_logging`, `debug_lod_colors`, `debug_seams`, `debug_clipmap`, | |
| 232 | `debug_wireframe`, `debug_edits` — draw overlays. The rest answer questions no overlay can. | |
| 233 | ||
| 234 | #### `build_stamp() -> String` | |
| 235 | ||
| 236 | Which binary this is, baked in at compile time. The `.so` under a consuming project's | |
| 237 | `addons/` is typically a symlink into `dc-terrain/bin` and is not rebuilt by anything | |
| 238 | automatic, so "I changed the C++" and "the game runs the change" are independent facts. This | |
| 239 | settles the second. | |
| 240 | ||
| 241 | #### `debug_gpu_density(origin, voxel_size, cells) -> PackedFloat32Array` | |
| 242 | ||
| 243 | The real density pipeline — same shader, same uniform set, same buffers as the mesher — over a | |
| 244 | small grid. `sample_density(position)` is the CPU twin; comparing the two is the parity check. | |
| 245 | ||
| 246 | Returns **empty** if the GPU refused, which is itself the answer. A local `RenderingDevice` is | |
| 247 | null under `--headless`, so this only answers in a real render session. | |
| 248 | ||
| 249 | #### `debug_chunk_info(position) -> Dictionary` | |
| 250 | ||
| 251 | The streaming state of the leaf under a point: `lod`, `voxel_size`, `chunk_size`, `state`, | |
| 252 | `mesh_state`, `triangles`, `has_collision`, `collision_eligible`, `parent_collision_stale`, | |
| 253 | `origin`. **Empty** when no leaf contains the point — which is distinct from LOD 0. | |
| 254 | ||
| 255 | (The doc comment above this method in `dc_terrain.h` lists only eight of these ten keys; the | |
| 256 | implementation is the authority.) | |
| 257 | ||
| 258 | #### `debug_collision_faces(center, radius) -> PackedVector3Array` | |
| 259 | #### `debug_visual_faces(center, radius) -> PackedVector3Array` | |
| 260 | ||
| 261 | Collision triangles and drawn triangles, both in world space, for comparison. Since collision | |
| 262 | is bare RIDs, this is the only way to see it. Gaps are the finding: nothing is built past | |
| 263 | `collision_lod_max`, however solid the terrain looks. | |
| 264 | ||
| 265 | #### `debug_sdf_at(position)` | |
| 266 | ||
| 267 | Print the SDF contribution of every edit at a point. | |
| 268 | ||
| 269 | #### `get_scalar_query_counts() -> Dictionary` · `reset_scalar_query_counts()` | |
| 270 | ||
| 271 | Returns `sample_density`, `eval_with_edits` and `collision_mode`. Those two counters cover | |
| 272 | every scalar-path evaluation there is — `sample_density` for map scripts, `_eval_with_edits` | |
| 273 | for the SDF collision family, which only runs in SDF mode — so "how much of the frame is the | |
| 274 | CPU field" is a number rather than a guess. `collision_mode` is returned alongside because | |
| 275 | the second counter is meaningless without it. | |
| 276 | ||
| 277 | #### `bake_volume_mesh(glsl, origin, voxel_size, cells) -> ArrayMesh` | |
| 278 | ||
| 279 | See [one-shot meshing](meshing.md#one-shot-meshing). |
| … | … | @@ -0,0 +1,97 @@ |
| 1 | # Generators | |
| 2 | ||
| 3 | The density field is a `DCGenerator` — pure C++, no Godot. `DCTerrainGenerator` is the | |
| 4 | abstract `Resource` that wraps one so it can be assigned in the inspector or from GDScript. | |
| 5 | ||
| 6 | Assign it to [`DCTerrain.generator`](api-dcterrain.md#shape) to replace the built-in noise. | |
| 7 | Ask `is_using_builtin_generator()` afterwards rather than assuming it took: a generator that | |
| 8 | fails to load falls back to noise, which renders a plausible landscape that is not yours. | |
| 9 | ||
| 10 | ## DCTerrainGeneratorNoise | |
| 11 | ||
| 12 | The built-in noise as a resource, with the same 27 properties `DCTerrain` exposes directly — | |
| 13 | `seed`, `use_noise`, the five noise bands, the terrain-shape set, `spherical` / | |
| 14 | `planet_radius`, and the Voronoi floors. | |
| 15 | ||
| 16 | ## DCTerrainGeneratorMeshSDF | |
| 17 | ||
| 18 | An SDF built from an OBJ mesh, over a BVH. Use it to verify meshing accuracy (the fandisk | |
| 19 | benchmark) or to stamp an arbitrary mesh shape as terrain. | |
| 20 | ||
| 21 | ```gdscript | |
| 22 | mesh_path: String # PROPERTY_HINT_GLOBAL_FILE, "*.obj" | |
| 23 | reload() # re-read from disk after changing mesh_path | |
| 24 | get_triangle_count() -> int | |
| 25 | ``` | |
| 26 | ||
| 27 | ## DCTerrainGeneratorGeiss | |
| 28 | ||
| 29 | GPU Gems 3 chapter 1 — the Geiss terrain field. It exists to be **compared**: the same field | |
| 30 | hyper-tps raymarches, meshed here, so marching and meshing can be measured against each other | |
| 31 | on identical terrain. | |
| 32 | ||
| 33 | **GPU only.** The field samples eight 16³ noise volumes, and until `volume_dir` points at them | |
| 34 | `has_gpu_shader()` is false and the generator produces nothing but air — deliberately, so a | |
| 35 | half-loaded field can never be mistaken for the real one. | |
| 36 | ||
| 37 | ```gdscript | |
| 38 | volume_dir: String # PROPERTY_HINT_DIR | |
| 39 | reload() -> bool # loads the eight .vol files. All or nothing. | |
| 40 | is_loaded() -> bool # check this before trusting a mesh built from it | |
| 41 | ``` | |
| 42 | ||
| 43 | ## DCTerrainGeneratorProcNodes | |
| 44 | ||
| 45 | Bridges a proc-nodes `ProcNodeGraph`, reached through `ClassDB` reflection so there is no | |
| 46 | compile-time dependency on proc-nodes' classes. | |
| 47 | ||
| 48 | The graph is compiled to a GLSL density function via `compile_to_glsl_function()` and wrapped | |
| 49 | to match `evaluate_density(x, y, z)`. Float and Int socket defaults are auto-discovered from | |
| 50 | the graph and exposed as grouped inspector properties (e.g. `Gradient Noise/Scale`); changing | |
| 51 | one updates the graph, marks the GLSL dirty and triggers a rebuild. | |
| 52 | ||
| 53 | This is the **world** generator: it maps world metres into a UV-like node space through | |
| 54 | `terrain_height`, adds its own mountain / relief / detail layers, and clips density to | |
| 55 | `[floor_y, ceiling_y]`. | |
| 56 | ||
| 57 | ## DCTerrainGeneratorProcNodesBounded | |
| 58 | ||
| 59 | The same bridge at **asset** scale, and a different class precisely because the world | |
| 60 | generator's assumptions are wrong for an asset. | |
| 61 | ||
| 62 | It scales nothing. `evaluate` takes world coordinates and the field is meshed at the size the | |
| 63 | graph says it is. Bounds are answered from the graph's own `Bounds Min` / `Bounds Max` group | |
| 64 | outputs, read by name; a graph declaring none falls back to "unbounded", which is the honest | |
| 65 | answer rather than a guessed box. | |
| 66 | ||
| 67 | **GPU only, deliberately** — `evaluate` returns air rather than inventing a CPU field, because | |
| 68 | neither the reflection call nor the graph's cached execution plan has been shown to be safe on | |
| 69 | dc-terrain's worker threads. | |
| 70 | ||
| 71 | ## DCTerrainGeneratorScript | |
| 72 | ||
| 73 | Override a virtual in GDScript: | |
| 74 | ||
| 75 | ```gdscript | |
| 76 | extends DCTerrainGeneratorScript | |
| 77 | ||
| 78 | func _evaluate(x: float, y: float, z: float, lod_voxel_size: float) -> float: | |
| 79 | return 8.0 - y # positive = solid, negative = air | |
| 80 | ``` | |
| 81 | ||
| 82 | `_evaluate` is called from **worker threads**, and the GDScript VM is single-threaded, so the | |
| 83 | class serialises every call through a mutex and a deferred call. It is therefore much slower | |
| 84 | than a native generator. For prototyping, not production. | |
| 85 | ||
| 86 | ## Meshing a generator directly | |
| 87 | ||
| 88 | `DCTerrainUtils` meshes one without a terrain node: | |
| 89 | ||
| 90 | ```gdscript | |
| 91 | DCTerrainUtils.mesh_from_generator(generator, origin, voxel_size, grid_size) -> ArrayMesh | |
| 92 | DCTerrainUtils.mesh_from_generator_auto(generator, voxel_size) -> ArrayMesh | |
| 93 | ``` | |
| 94 | ||
| 95 | `origin` is the world-space corner of the meshing volume; `grid_size` is voxels per axis (32, | |
| 96 | 64, 128 are typical). The `_auto` form takes bounds from the generator's `get_bounds()` with | |
| 97 | padding. |
| … | … | @@ -0,0 +1,140 @@ |
| 1 | # Meshing paths | |
| 2 | ||
| 3 | Six of them. They differ in what they can represent and in which vertex attributes they | |
| 4 | produce, so they are not interchangeable. | |
| 5 | ||
| 6 | | path | representation | attributes produced | | |
| 7 | |---|---|---| | |
| 8 | | [GPU dual contouring](#gpu-dual-contouring) | voxel | position, normal, `material_id`, `proj`, `proj2`, curvature | | |
| 9 | | [CPU dual contouring](#cpu-dual-contouring) | voxel | position, normal **only** | | |
| 10 | | [Seam stitching](#seam-stitching) | temporary octree, discarded | one `ArrayMesh` per node | | |
| 11 | | [Exact brush meshing](#exact-brush-meshing) | brush planes, no QEF | per-face vertices, own plane normal | | |
| 12 | | [Voxel pockets](#voxel-pockets) | voxel, inside the exact path | derived cache, rebuildable | | |
| 13 | | [Simplification](#simplification) | post-process | — | | |
| 14 | ||
| 15 | ## GPU dual contouring | |
| 16 | ||
| 17 | `src/dc_gpu_mesh_pipeline.cpp`, `shaders/` | |
| 18 | ||
| 19 | Ten compute stages on one `RenderingDevice`: | |
| 20 | ||
| 21 | ``` | |
| 22 | density → edge_classify → scan → stream_compact → edge_intersect | |
| 23 | → voxel_classify → scan → stream_compact → qef_solve → quad_emit → readback | |
| 24 | ``` | |
| 25 | ||
| 26 | The **only** path that produces the full vertex attribute set. The readback fills | |
| 27 | `materials`, `proj`, `proj2` and `curv` alongside position and normal — 16 floats per vertex: | |
| 28 | slots 0-2 position, 3-5 normal, 8 `material_id`, 10 and 11 the two projection candidates | |
| 29 | (both read as uint bits), and 12 curvature in metres as a float. | |
| 30 | ||
| 31 | Requires `use_gpu = true`, and requires the shaders to be found on disk (see Install). | |
| 32 | ||
| 33 | ## CPU dual contouring | |
| 34 | ||
| 35 | `libdc/src/dc_contour.cpp`, driven by `src/dc_worker_cpu.cpp` | |
| 36 | ||
| 37 | Density grid → octree → QEF → recursive contour (`cellProc` / `faceProc` / `edgeProc`) → clip | |
| 38 | → smooth, on a pool of workers auto-scaled to the CPU core count. In the editor a single | |
| 39 | worker is shared across all `DCTerrain` instances, because multiple `RenderingDevice`s crash | |
| 40 | on some GPUs. | |
| 41 | ||
| 42 | **It produces geometry only.** `materials`, `proj`, `proj2` and `curv` come back empty — | |
| 43 | `DCMeshData` says so in its own field comments, and the contour code only *preserves* those | |
| 44 | arrays across vertex splits when they are already populated. It never generates them. | |
| 45 | ||
| 46 | The consequence is user-visible: `use_textures` and Quake per-face projection do nothing on | |
| 47 | this path, and the terrain renders in flat debug colours whatever the atlas holds. Since | |
| 48 | `use_gpu` defaults to `false`, this is the **default** path. | |
| 49 | ||
| 50 | ## Seam stitching | |
| 51 | ||
| 52 | `libdc/src/dc_seam.cpp` | |
| 53 | ||
| 54 | Cross-LOD gaps are not faded over, they are filled. Boundary nodes of adjacent cells are | |
| 55 | collected into a **temporary** octree (arena-allocated, discarded after use), contoured, and | |
| 56 | emitted as a separate `ArrayMesh` per node. Ported from ngildea/leven. | |
| 57 | ||
| 58 | `debug_seams` draws them. `seamless_lod` sidesteps the problem entirely by forcing every | |
| 59 | chunk to LOD 0, at the obvious cost. | |
| 60 | ||
| 61 | ## Exact brush meshing | |
| 62 | ||
| 63 | `libdc/src/dc_level_mesh.cpp` — the `DCLevel` path | |
| 64 | ||
| 65 | **No QEF, no voxels, no smoothing.** Every face is triangulated with its own vertices | |
| 66 | carrying its own plane normal. Nothing is welded and nothing is smoothed, so a crease is | |
| 67 | sharp because the geometry says so — not because an angle threshold noticed it. Kobbelt's | |
| 68 | non-convergence result for reconstructed normals does not apply to geometry that *is* the | |
| 69 | brush planes. | |
| 70 | ||
| 71 | Render and collision diverge deliberately: | |
| 72 | ||
| 73 | ``` | |
| 74 | render = the csg4-CLIPPED visible skin (hidden surface removed) | |
| 75 | collision = the UNCLIPPED convex brush hull (exact, and cheaper than a trimesh) | |
| 76 | ``` | |
| 77 | ||
| 78 | Clipping removes hidden **surface**, not **volume**. | |
| 79 | ||
| 80 | UVs are not computed here. Each vertex carries its face's `proj` index (1-based into the | |
| 81 | importer's projection table, 0 = none) plus its `material_id`, and the fragment shader | |
| 82 | resolves the projection from that table. On the exact mesh both projection candidates are the | |
| 83 | same face — a fragment's face is known exactly — so the shader's crease-pick becomes a no-op | |
| 84 | rather than a heuristic. The material-quantization problem disappears by construction rather | |
| 85 | than being solved. | |
| 86 | ||
| 87 | ## Voxel pockets | |
| 88 | ||
| 89 | `libdc/include/dc_pocket.h` | |
| 90 | ||
| 91 | A second mesher living inside the exact path, and the one that is easiest to miss. | |
| 92 | ||
| 93 | `DCBrushStore::csg4_view` feeds the exact mesher only **clean** brushes; anything an active | |
| 94 | carve touches is excluded. That exclusion is correct — a carved solid is non-convex, so it is | |
| 95 | no longer a brush and the winding clip cannot represent it. But until something meshed the | |
| 96 | excluded brushes, a carve did not carve, it *deleted*. `tests/test_pocket.cpp` part 1 pins | |
| 97 | that behaviour so the claim stays checkable. | |
| 98 | ||
| 99 | ``` | |
| 100 | pocket = DC mesh of ( union(component brushes) − union(active carves) ) | |
| 101 | ``` | |
| 102 | ||
| 103 | The brushes stay authoritative; the pocket is a derived cache that may be thrown away and | |
| 104 | rebuilt at any time. Undo drops a carve and the component returns to the exact mesher | |
| 105 | unchanged — no plane re-fitting, so no error accumulates. | |
| 106 | ||
| 107 | Voxels are used here and nowhere else in the exact path because a sphere carve is not a | |
| 108 | half-space: it leaves brush-land entirely. Mesh boolean is the wrong *shape* of cost — | |
| 109 | O(global) — against voxel DC's O(local). | |
| 110 | ||
| 111 | `pocket_voxel_size` tunes the resolution. `get_stats()` reports `pocket_triangles` separately | |
| 112 | from `triangles`, on purpose: the exact mesh and the voxel pockets have completely different | |
| 113 | cost curves, and one combined number would hide a pocket that had quietly become most of the | |
| 114 | frame. | |
| 115 | ||
| 116 | ## Simplification | |
| 117 | ||
| 118 | `libdc/src/dc_simplify.cpp` | |
| 119 | ||
| 120 | Two algorithms plus a reorder: | |
| 121 | ||
| 122 | - **`dc_simplify_mca`** — multiple-choice QEM (Wu & Kobbelt). For each collapse, evaluate `k` | |
| 123 | random edges and collapse the lowest-error one. Fast, and good enough for LOD terrain. | |
| 124 | - **`dc_simplify_meshopt`** — meshoptimizer with **border vertex locking**. Locking border | |
| 125 | vertices is what stops T-junctions opening between LOD levels. Higher quality. | |
| 126 | - **a vertex-cache reorder**, which changes no geometry at all. | |
| 127 | ||
| 128 | `use_mesh_simplify` gates the post-process, and it runs on the **GPU path only**. | |
| 129 | ||
| 130 | ## One-shot meshing | |
| 131 | ||
| 132 | Two entry points mesh something without a streaming terrain node at all: | |
| 133 | ||
| 134 | - `DCTerrainUtils.mesh_from_generator(...)` / `mesh_from_generator_auto(...)` — a generator | |
| 135 | straight to an `ArrayMesh`, via `dc_mesher.h`'s convenience pipeline (fill grid → octree → | |
| 136 | QEF → contour → post-process). | |
| 137 | - `DCTerrain.bake_volume_mesh(...)` — a GLSL `float proc_density(vec3)` body, filled and | |
| 138 | meshed through the GPU thread the terrain already runs. It lives on `DCTerrain` rather than | |
| 139 | in proc-nodes because a second `VkDevice` bring-up starves: one device, one GPU thread, and | |
| 140 | this borrows them. |
| … | … | @@ -0,0 +1,62 @@ |
| 1 | # Testing | |
| 2 | ||
| 3 | ## libdc gates — `tests/` | |
| 4 | ||
| 5 | `libdc` is Godot-free, so its gates are plain executables. There is **no build system** here: | |
| 6 | each file carries its own compile line in its header comment. | |
| 7 | ||
| 8 | ```sh | |
| 9 | # from the repo root | |
| 10 | g++ -O2 -std=c++17 -I libdc/include -o /tmp/csg4_gate \ | |
| 11 | tests/test_csg4_gate.cpp libdc/src/dc_csg4.cpp | |
| 12 | /tmp/csg4_gate | |
| 13 | ``` | |
| 14 | ||
| 15 | Some gates read fixtures that are **not in the repo** (`/tmp/startmap_brushes.bin`, for | |
| 16 | instance) and abort without them. Most other `/tmp` paths in these files are outputs — `.obj` | |
| 17 | dumps written for inspection. | |
| 18 | ||
| 19 | Two kinds of file live here and nothing distinguishes them by name or exit code: **regression | |
| 20 | gates**, which assert, and **one-shot research oracles**, which test a hypothesis and whose | |
| 21 | failure is a recorded result rather than a broken build. `test_hermite_qef.cpp` is the second | |
| 22 | kind. Read the header before treating a non-zero exit as a regression. | |
| 23 | ||
| 24 | ## GPU gates | |
| 25 | ||
| 26 | GPU gates compile and dispatch **standalone** — their own `VkDevice`, their own process, | |
| 27 | nothing else on the queue, linking `-lvulkan`. | |
| 28 | ||
| 29 | That pattern exists for a reason. The in-engine oracle *submits* density fills on the live | |
| 30 | device, and letting it run long enough to answer is what once interleaved its uniform set with | |
| 31 | the mesh pipeline's and emptied the world. Its short deadline is a guard, not a defect. | |
| 32 | ||
| 33 | A gate that inspects GLSL text or runs outside Godot cannot tell a correct shader from one | |
| 34 | that never ran — which is why `DCTerrain.debug_gpu_density()` exists as the in-engine | |
| 35 | counterpart, and why it only answers in a real render session. | |
| 36 | ||
| 37 | ## Meshing accuracy — `test/` | |
| 38 | ||
| 39 | `test/CMakeLists.txt` builds `test_fandisk`, which reconstructs the fandisk benchmark model | |
| 40 | from its SDF and checks that sharp features survive: | |
| 41 | ||
| 42 | ```sh | |
| 43 | cmake -S test -B test/build && cmake --build test/build | |
| 44 | ./test/build/test_fandisk # defaults to test/data/fandisk.obj | |
| 45 | ``` | |
| 46 | ||
| 47 | The CMakeLists covers `test_fandisk` only; the other files in `test/` carry their own g++ | |
| 48 | lines the same way `tests/` does. | |
| 49 | ||
| 50 | ## Scripted gates | |
| 51 | ||
| 52 | - `tests/gate_hblk_compile.sh` | |
| 53 | - `tests/gate_polish_units.py` | |
| 54 | - `demo/*.gd` — gates that need a running Godot (`volume_bake_gate.gd`, | |
| 55 | `gate_bounded_procnodes.gd`, `compile_failure_gate.gd`, `procnodes_eval_probe.gd`, | |
| 56 | `pn_diag.gd`) | |
| 57 | ||
| 58 | ## The demo | |
| 59 | ||
| 60 | `demo/` is a Godot project with a fly camera, CSG edit controls and a CPU/GPU benchmark state | |
| 61 | machine. Note that its scene sets **no** properties, so it runs on the defaults — which means | |
| 62 | the CPU mesher. Set `use_gpu` to exercise the GPU pipeline. |