CDLOD terrain quadtree — reasoning (why CDLOD, and the rejected alternatives)
The design doc names only the chosen path (CDLOD); this records why it won and what lost, so the decision isn't re-litigated.
The requirement
Draw planetary terrain draped on a DEM at a bounded triangle cost — crisp under the camera, cheap on the horizon — with no cracks and no popping, and (session constraint) with the selection maths fully correctness-testable without a GPU.
The candidates
- Uniform fixed grid. One resolution everywhere. Rejected: either too coarse near the camera or millions of sub-pixel triangles far away. No distance adaptivity at all.
- Geomipmapping (LOD per tile + skirts). Discrete per-tile LOD with vertical "skirts" hiding the crack. Rejected: skirts are a visible shading/z artifact at grazing flight angles, LOD changes pop (no continuous morph), and the per-tile LOD jump between neighbours can exceed one level (T-junctions the skirt only hides, not fixes).
- ROAM (triangle bintree). Continuous, crack-free, but a per-frame CPU triangle split/merge priority queue — heavy, pointer-chasing, hard to make deterministic and GPU-friendly. Superseded in practice by GPU-grid schemes.
- Clipmaps / geometry clipmaps. Excellent for a single continuous heightfield centred on the camera, but ring-based (not a quadtree), awkward to drape selective OSM/tile regions on, and the toroidal update is stateful (fights the FC-7 "pure function of the camera" property we want for scripted flythroughs).
- CDLOD (chosen). A quadtree whose node grid mesh is constant (say 33×33) and whose node size varies with depth; selection is a cheap pure recursion keyed on camera distance; the ≤ 1-level neighbour invariant + a per-vertex morph give continuous, crack-free, pop-free transitions. The selection maths is pure
f64/f32(no GPU) → fully RED-when-broken testable headless; the morph is onelerpthe GPU and CPU agree on tof32. It reuses our existingWorldPosdistance frame andbuild_terrain_meshdrape — no new subsystem.
Decisions inside CDLOD
range_ratio = 2.0,morph_start_ratio = 0.66— Strugar's canonical values; the morph band is the outer third of each range (long enough to hide the switch, short enough that most of a node is full-detail).- Children the finer range doesn't reach are drawn at the parent level, not recursed — this is precisely what bounds adjacent nodes to ≤ 1 level apart (the anti-crack invariant, asserted in
adjacent_nodes_differ_by_at_most_one_level). - Distance to the node's world AABB (nearest point), not centre — a big node close to the camera must count as close (its near corner), or the near field under-subdivides.
- GPU morph as a compute-parity proof first, render-pass wiring deferred — same staging as the scattering LUT (
sky.wgslsampling deferred), keeping the landed 21-pass pipeline untouched (L2) while still proving the shader maths is the CPU maths.
Deferred / next
- Wire the selected node instances + an in-shader morphing terrain vertex pass into
facett-map3d's renderer (the actual on-screen draw; needs a GPU box to robot-verify). - Frustum culling of selected nodes (currently the whole extent tiles; a finite region is fine, a planetary streamer wants the frustum cull + the §5 tile budget joined in).
- Join CDLOD depth to the §4 HLOD feature budget so buildings/roads thin with the terrain LOD on the same ladder.