OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain commits / a16f152

README: six meshers, and the one claim that was wrong about them
The Features bullet said the CPU path "produces the same mesh" as the GPU path. It does not, and DCMeshData says so four times. dc_gpu_mesh_pipeline.cpp:855-886 fills materials, proj, proj2 and curv from the readback. dc_contour.cpp only PRESERVES those across vertex splits when they are already present (has_mat/has_proj, 882-884) and never generates them. So on the CPU path there is no material_id, no per-face projection and no curvature -- use_textures and Quake face projection do nothing, and the terrain renders in flat debug colours whatever the atlas holds. use_gpu defaults to false, so that is the DEFAULT path. Three meshers were missing from the README entirely: - seam stitching (dc_seam.cpp) -- temporary octrees built from the boundary nodes of adjacent cells, contoured into their own ArrayMesh per node and discarded. Ported from ngildea/leven. - voxel pockets (dc_pocket.h) -- a second mesher inside the exact path. csg4_view excludes any brush an active carve touches, so before pockets a carve did not carve, it deleted. - simplification is TWO algorithms plus a reorder, not one: MCA/QEM and meshoptimizer with border locking (which is what stops T-junctions opening between LOD levels). And the exact path's real claim was under-sold: dc_level_mesh uses no QEF at all. Per-face vertices carry their face's own plane normal, nothing is welded, so a crease is sharp by construction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
a16f1520caa9541fdc58cb4fbfa7af4f98e6beaf
by plankatron <fleeky@gmail.com> · 2026-09-10T14:40:19.000Z

Changed files (1) +81 -3

README.md +81 -3
……@@ -11,9 +11,9 @@ either on the GPU across ten compute stages or on a pool of CPU worker threads.
1111- **Sharp features.** Dual Contouring solves a QEF per cell, so a plane stays flat and a
1212 crease stays a crease. Marching Cubes cannot do this.
1313- **Octree clipmap LOD.** Detail follows the camera. Cross-LOD seams are stitched, not hidden.
14- **GPU meshing.** density → edge classify → scan → compact → edge intersect → voxel classify
15 → scan → compact → QEF solve → quad emit, on one `RenderingDevice`. CPU dual contouring
16 is the fallback and produces the same mesh.
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.
1717- **Runtime editing.** Spheres, boxes, convex brushes (Quake-style half-space sets), and
1818 arbitrary meshes, added or subtracted, re-meshing only the chunks they touch.
1919- **Pluggable generators.** The density field is a `DCGenerator` — built-in noise, an OBJ
……@@ -93,6 +93,84 @@ LOD follows the active `Camera3D` of the viewport — the editor viewport camera
9393`use_gpu` defaults to **false**. The demo scene sets no properties at all, so it runs the CPU
9494mesher; every hyper-tps map sets `use_gpu = true` explicitly.
9595
96## Meshing paths
97
98Six of them. They differ in what they can represent and in what attributes they produce, so
99they are not interchangeable.
100
101#### GPU dual contouring — `src/dc_gpu_mesh_pipeline.cpp`, `shaders/`
102
103Ten compute stages on one `RenderingDevice`:
104
105```
106density → edge_classify → scan → stream_compact → edge_intersect
107 → voxel_classify → scan → stream_compact → qef_solve → quad_emit → readback
108```
109
110The **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
112metres. Requires `use_gpu = true`.
113
114#### CPU dual contouring — `libdc/src/dc_contour.cpp`
115
116Density grid → octree → QEF → recursive contour (`cellProc`/`faceProc`/`edgeProc`) → clip →
117smooth, 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
120CPU path preserves them across vertex splits when they already exist but never generates
121them. So `use_textures` and Quake per-face projection do nothing here, and the terrain
122renders 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
127Cross-LOD gaps are not faded over, they are filled. Boundary nodes of adjacent cells are
128collected into a **temporary** octree, contoured, and emitted as a separate `ArrayMesh` per
129node, then discarded. Ported from ngildea/leven. `debug_seams` draws them.
130
131#### Exact brush meshing — `libdc/src/dc_level_mesh.cpp`
132
133The `DCLevel` path. **No QEF, no voxels, no smoothing.** Every face is triangulated with its
134own vertices carrying its own plane normal — nothing is welded, so a crease is sharp because
135the geometry says so rather than because an angle threshold noticed it.
136
137Render and collision diverge on purpose: render is the csg4-**clipped** visible skin with
138hidden surface removed; collision is the **unclipped** convex brush hull, which is both exact
139and cheaper than a trimesh. Clipping removes hidden surface, not volume.
140
141#### Voxel pockets — `libdc/include/dc_pocket.h`
142
143A 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
146carve touches is excluded, because a carved solid is non-convex and the winding clip cannot
147represent it. Correct — but until something meshed the excluded brushes, a carve did not
148carve, it *deleted*. A pocket is that missing piece:
149
150```
151pocket = DC mesh of ( union(component brushes) − union(active carves) )
152```
153
154The brushes stay authoritative and the pocket is a derived cache that may be thrown away and
155rebuilt. Undo drops a carve and the component returns to the exact mesher unchanged — no
156plane re-fitting, so no error accumulates. Voxels are used here and nowhere else in the exact
157path because a sphere carve is not a half-space: mesh boolean is the wrong *shape* of cost,
158O(global), against voxel DC's O(local). `pocket_voxel_size` tunes it, and `get_stats()`
159reports `pocket_triangles` separately from `triangles` so a pocket that has quietly become
160most of the frame cannot hide inside one number.
161
162#### Simplification — `libdc/src/dc_simplify.cpp`
163
164Two, 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
96174## DCTerrain
97175
98176`Node3D`. The streaming, destructible world.