| … | … | @@ -11,9 +11,9 @@ either on the GPU across ten compute stages or on a pool of CPU worker threads. |
| 11 | 11 | - **Sharp features.** Dual Contouring solves a QEF per cell, so a plane stays flat and a |
| 12 | 12 | crease stays a crease. Marching Cubes cannot do this. |
| 13 | 13 | - **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. |
| 17 | 17 | - **Runtime editing.** Spheres, boxes, convex brushes (Quake-style half-space sets), and |
| 18 | 18 | arbitrary meshes, added or subtracted, re-meshing only the chunks they touch. |
| 19 | 19 | - **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 |
| 93 | 93 | `use_gpu` defaults to **false**. The demo scene sets no properties at all, so it runs the CPU |
| 94 | 94 | mesher; every hyper-tps map sets `use_gpu = true` explicitly. |
| 95 | 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 | |
| 96 | 174 | ## DCTerrain |
| 97 | 175 | |
| 98 | 176 | `Node3D`. The streaming, destructible world. |