feat(shafts): a drainage tree -- the field goes from 22% to 97% connected

The plateau was the tell. Roughness changed the component COUNT (it
manufactures noise pockets) but the giant component sat at ~0.22 in every
variant, and widening connectors past the proven roughness bound barely
moved it. That is a random graph below its percolation threshold: 0.6
sites, 0.35 bonds, and a 1.6-cell pair cutoff that drops many neighbour
pairs once jitter spreads them 1.7 (orthogonal) to 2.4 (diagonal) cells
apart.

So the lever was topology. Every shaft now emits a deterministic connector
to the nearest shaft STRICTLY closer to the origin, so chains drain inward
and terminate at the spine. The probabilistic connectors stay as texture
and loops -- the tree guarantees you can get out, the noise keeps it from
reading as a diagram.

Deliberately NOT tuned: raising CrossConnectChance/ShaftDensity/the cutoff
toward the percolation threshold buys a world connected at one seed and
shattered at the next, which is exactly the 1-in-16 behaviour we had. A
phase transition is not a knob.

  seed 1337   components 30 -> 17,  largest share .220 -> .975
  16 seeds    spine is the largest component in 16/16 (was 8/16)
              largest share .58 - .9999

Seam safety, because a parent computed from a 3x3 window would differ
between chunks and the connector would exist in one and not the other:
collect over 7x7, emit only for the inner 3x3, whose 5x5 candidate windows
are then fully contained. 154 seam probes per path, 0 mismatches.

Box verdicts re-checked against the new air: 27 proved, 33 mixed,
0 violations. Stack mirror bit-identical over 20,000 samples.
Perf: 1.634 us forced rebuild, 0.100 us hot call -- the 49 rolls happen
only on cache rebuild, never per voxel.

⚠️ ACCEPTANCE STILL FAILS: arrival->departure 4/16 (was 1/16). Reported,
not tuned. With 97% of the air in one component, two mouths that both land
IN the field would essentially always connect -- so the remaining failure
is placement, not topology. Next.

Build clean, 20 succeeded / 0 warnings / 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bp7FxoQcSoiuhxjuLr6aBM
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-08-30 17:26:20 +02:00
1 parent 58e61cdefa
commit 7a20eeed0d
6 files changed
+608 -91

No files matched your search

+22 -15
View File
@@ -46,23 +46,27 @@ what the world IS, and both paths compute it.
`UVoxelGenerator::OriginSpineRadius` ← `VoxelSettings::OriginSpineRadius`. Called before
every `ApplyBoundarySeal`.
- `VerticalShafts` treats that structural column as a **connector endpoint**, not as a second
density primitive: while rebuilding its memoised 3×3 shaft neighbourhood, an origin-adjacent
query adds `(0,0,OriginSpineRadius)` to the local shaft set for pair generation only. The
existing `VoxelHash::Pair` gate and `Spacing*1.6` neighbour cutoff remain in force, so the link
is a pure function of the shaft cell and seed and is independent of evaluation order. Spine
links use a radius above the proven `sup|FBM|=1.5` roughness envelope (`max(ConnectorRadius,
SurfaceRoughness*VOXEL_NOISE_SCALE*1.5 + 1)`); the ordinary connector radius and ordinary shaft
roughness are unchanged. `BuildVerticalShaftStack` receives the same runtime spine radius and
mirrors this source rule; `VF_ApplyOriginSpine` remains the sole owner of the column itself.
density primitive. A thread-local rebuild collects a 7×7 cell window once, emits tree links only
for the inner 3×3 cells, and resolves each parent from its complete 5×5 candidate window. Each
shaft chooses the nearest shaft whose jittered distance to (0,0) is strictly smaller; ties use
the lowest cell index. A local minimum links directly to the spine. The wider collection is
therefore fully contained for every emitted shaft and the parent is independent of chunk or
evaluation-cell boundaries. The existing probabilistic `VoxelHash::Pair` links and
`Spacing*1.6` cutoff remain as texture and loops; they are additive, not the connectivity law.
Tree links use a radius strictly above the proven `sup|FBM|=1.5` roughness envelope
(`max(ConnectorRadius, SurfaceRoughness*VOXEL_NOISE_SCALE*1.5 + 1)`).
`BuildVerticalShaftStack` mirrors the same tree, random links, windows, hashes, and radius;
`VF_ApplyOriginSpine` remains the sole owner of the vertical column itself.
- Descent is **player-dug** through the thin seals at (0,0). The single auto-opened
connection is the **surface entry shaft** at (0,0) through the top of strate 0
(`GeneratePassages`, `bOpenSurfaceEntry`).
- **Hybrid extras:** auto-carved *shortcut* passages per boundary, placed away from (0,0).
Now fully **per-strate** — see §8.8 (the upper strate's `PassageConfig` drives count/style/shape).
This connector is local topology, not a promise that independently placed shortcut mouths are
globally connected. The acceptance measurement for `VerticalShafts` remains the actual
arrival→departure flood-fill at the passage endpoints.
The tree is a construction guarantee for the shaft field: every seeded shaft has one inward
edge, and every chain strictly decreases distance to the origin until it reaches a local minimum
that links to the spine. It does not alter passage placement. The acceptance measurement remains
the actual arrival→departure flood-fill at the independently placed passage endpoints.
### 8.3 Disturbance layer (the "wow" post-process)
`FStrateDisturbanceParams` (on the definition, all archetypes). `ApplyDisturbances`
@@ -337,10 +341,13 @@ driven by `EditorBrush*` props.
no loads AND no LOD mismatches outstanding.
- **SDF cache** (`GetDensityWithParams`): search-BOX validity, not chunk-key — gradient ±1
sampling must not thrash the (expensive) rebuild.
- **VerticalShafts field cache**: shaft rolls and pair decisions, including the structural-spine
connector endpoint, are rebuilt only when the thread-local centre cell or a geometry-affecting
parameter changes. They must not be re-hashed in the per-voxel loop; the operator-stack source
follows the same cache contract.
- **VerticalShafts field cache**: shaft rolls and connector decisions, including the structural
tree and structural-spine connector endpoint, are rebuilt only when the thread-local centre cell
or a geometry-affecting parameter changes. Each rebuild rolls 49 cells (7×7); it stores/emits
only the inner 3×3 shafts and their capsules for the per-voxel loop. The complete 5×5 parent
windows are contained by that 7×7 collection, so no parent decision depends on the evaluation
cell. No cell or pair hash may move into the per-voxel loop; the operator-stack source follows
the same cache contract.
- **Per-chunk param cache** in `GetDensityAt`: GenType + param struct + disturbance cached
thread-locally by `(DensityCacheOwnerId, ChunkCoord, LayoutVersion)`; the process-unique owner ID
prevents cross-world reuse while adding only one `uint64` compare per voxel. Don't remove the owner