feat(composer): cave FEEL is rollable -- 23 of 34 fields were live all along

Jahni: "we need to be able to have everything tuned, so feeling can be
quite different." So the 34 excluded terrain-detail fields had to be
settled -- they are the difference between varying how BIG a cave is and
varying how it FEELS.

Settled by EXPERIMENT, not by reading, because the exclusion rested on the
same kind of structural claim that produced the withdrawn fuse assertion
and the "99.95% void" reading. Sample GetDensityAt over a lattice, change
ONLY those fields, re-sample, per operation group:

  LIVE (23 fields)  terrace, layer-line, ribbing, overhang, cliff,
                    scallop, arch, dome, pinch  -> now rolled
  DEAD (11 fields)  column, pit, chimney -> pre-baked into the chunk cache
                    from Definition->TerrainOperations, so the values in
                    FStrateGenerationParams are transport, not tunables

The call chain for the dead ones: BuildParamsFromDefinition returns only
the base params (VoxelStrateManager.cpp:1399); VoxelGenerator separately
reads Definition->TerrainOperations (VoxelGenerator.cpp:1099);
BuildChunkCache calls RoomOp->ApplyTo and pre-bakes pits/chimneys/columns
(VoxelCaveMorphology.cpp:1741).

⚠️ AND THE TRAP FIRED, exactly as specced against: these are native
unreflected fields, so REMOVING THEM FROM THE EXCLUSION LIST WOULD HAVE
ROLLED NOTHING. A knob that looks like it works and does not is worse than
an honest exclusion. An explicit jitter path was needed. This is why the
task was specced as an experiment with a liveness assertion (24 group/path
rows, 0 mismatches) rather than as a list edit.

⚠️ HONEST NEGATIVE: rolling detail did NOT materially broaden the measured
distributions. Feature scale 4-376 -> 0-376; walkable unchanged. All 17/17
TunnelNetwork/Underwater candidates activated live detail and every one of
the nine live groups appeared, so it is not that the fields are inert --
it is that most of them have ZERO CORPUS SPREAD, because the four authored
assets leave them at header defaults. Jitter is a fraction of measured
spread, and a fraction of zero is zero.

⇒ Real limitation, recorded not patched: "ranges come from corpus spread"
(§3.3) is right in principle and DEGENERATES on a homogeneous corpus. The
composer needs a bootstrap for zero-spread fields. Next.

Box verdicts with detail rolled: 1,228 proved, 1,634,468 voxels
brute-forced, 0 violations. Survival 59/64.

Recommendation recorded for later: roll terrain-operation parameter
STRUCTS directly into a transient candidate pool rather than corpus-ing
over assets -- there is exactly one such asset in the project, so an asset
corpus would starve like the strate corpus did.

Build clean, 25 succeeded / 0 failed, verified by me.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-09-04 16:30:57 +02:00
1 parent 859a9b25c1
commit f088c19f88
5 files changed
+781 -128

No files matched your search

+124 -44
View File
@@ -264,10 +264,11 @@ The hand-authored C++ defaults in `VoxelStrateTypes.h` are also corpus members:
`CrystalChamber=1`, `Maze=1`, `SurfaceWorld=2`, `VerticalShafts=1`, `FloatingIslands=2`, and
`Underwater=1`.
The complete 253-descriptor spread table is emitted by the automation test, including sample count,
min/max/mean/standard deviation, excluded/reflected status, and clamp metadata. The 230 zero-range
rows are intentionally omitted below; these are the **23 rows with real spread** (float formatting
is the test's measured output):
The complete 253-descriptor spread table is emitted by the automation test, including authored and
synthetic operation-default sample counts, min/max/mean/standard deviation, excluded/reflected
status, and clamp metadata. The current run has **212 zero-range rows** and **41 rows with real
spread**. The shape-spread rows below are retained from the prior report; the newly measured
terrain-detail spread is listed explicitly after the liveness experiment.
| Archetype | Field | N | Min | Max | Mean | StdDev |
|---|---|---:|---:|---:|---:|---:|
@@ -295,54 +296,133 @@ is the test's measured output):
| FloatingIslands | TopFlatten | 2 | 0.600000024 | 0.800000012 | 0.700000018 | 0.099999994 |
| FloatingIslands | SurfaceRoughness | 2 | 0 | 4 | 2 | 2 |
The reflection/use audit found **36 excluded fields**: the 34 terrain-op transport fields
`TerraceStepHeight`, `TerraceHardness`, `TerraceNoiseDisplacement`, `LayerLineSpacing`,
`LayerLineDepth`, `OverhangStrength`, `OverhangDepth`, `OverhangFrequency`, `RibbingSpacing`,
`RibbingDepth`, `CliffStrength`, `ScallopStrength`, `ScallopFrequency`, `ArchDensity`,
`ArchMinRadius`, `ArchMaxRadius`, `ColumnDensity`, `ColumnMinRadius`, `ColumnMaxRadius`,
`PitDensity`, `PitMinRadius`, `PitMaxRadius`, `PitDepth`, `ChimneyDensity`, `ChimneyMinRadius`,
`ChimneyMaxRadius`, `ChimneyHeight`, `DomeDensity`, `DomeMinRadius`, `DomeMaxRadius`,
`DomeHeightRatio`, `PinchDensity`, `PinchStrength`, `PinchLength` — populated per room by
`UVoxelTerrainOpDefinition` — plus the manager-owned runtime bounds `StrateTopWorldZ` and
`StrateBottomWorldZ`. The tunnel transport fields are measured directly from the native
`FStrateGenerationParams` X-macro even though they are not reflected `UPROPERTY`s; their values are
reported but deliberately not jittered. Same-named fields in sibling structs are not automatically
excluded: the exclusion applies to the tunnel transport slots whose generation source is the
terrain-op pool. The test prints the complete field-by-field table, including excluded fields,
reflection status, and clamp metadata.
The 34 terrain-detail fields now have the following effective spread. `N` is shown as
`authored+op-defaults=effective`; the TunnelNetwork group has 3 authored vectors and Underwater
has 1. The default seed is present only for the 23 live fields, one matching operation type per
field. The values are the test's population statistics (the operation shape defaults happen to be
identical to the FStrate defaults, so only activation fields have non-zero spread here).
The terrain-field source is now traced in code. `VoxelStrateManager.cpp` loads the active strate's
`TerrainOperations` soft-object pool, and `VoxelGenerator.cpp` passes that pool to
`VoxelCaveMorphology::BuildChunkCache`. For each nearest room, `FVoxelTerrainOpDefinition::ApplyTo`
overlays the selected operation onto a local copy (`FStrateGenerationParams LocalTerrainParams =
Params`) before the room operator runs. Thus the 34 values are transported through the strate
params, but their authored cave-detail values come from `UVoxelTerrainOpDefinition` assets selected
by the active strate's terrain-operation pool. Rolling those slots in the strate composer is not a
useful way to roll cave detail: selected room operations overwrite them. It is not a literal
absolute no-op, because the local base copy is still used where no operation overwrites a slot.
The correct future design is a **second corpus** over terrain-op assets plus the active pool's
membership/weights; this task does not build it. The project currently has **1**
`UVoxelTerrainOpDefinition` asset (the Asset Registry count used by the test).
| Field | Tunnel N | Tunnel min..max | Tunnel mean | Tunnel SD | Underwater N | Underwater min..max | Underwater mean | Underwater SD | Result |
|---|---:|---:|---:|---:|---:|---:|---:|---:|---|
| TerraceStepHeight | 3+1=4 | 0..5 | 1.25 | 2.16506351 | 1+1=2 | 0..5 | 2.5 | 2.5 | LIVE |
| TerraceHardness | 3+1=4 | 0.5..0.5 | 0.5 | 0 | 1+1=2 | 0.5..0.5 | 0.5 | 0 | LIVE |
| TerraceNoiseDisplacement | 3+1=4 | 0.5..0.5 | 0.5 | 0 | 1+1=2 | 0.5..0.5 | 0.5 | 0 | LIVE |
| LayerLineSpacing | 3+1=4 | 0..4 | 1 | 1.73205081 | 1+1=2 | 0..4 | 2 | 2 | LIVE |
| LayerLineDepth | 3+1=4 | 0.300000012..0.300000012 | 0.300000012 | 0 | 1+1=2 | 0.300000012..0.300000012 | 0.300000012 | 0 | LIVE |
| OverhangStrength | 3+1=4 | 0..0.5 | 0.125 | 0.216506351 | 1+1=2 | 0..0.5 | 0.25 | 0.25 | LIVE |
| OverhangDepth | 3+1=4 | 5..5 | 5 | 0 | 1+1=2 | 5..5 | 5 | 0 | LIVE |
| OverhangFrequency | 3+1=4 | 0.0599999987..0.0599999987 | 0.0599999987 | 0 | 1+1=2 | 0.0599999987..0.0599999987 | 0.0599999987 | 0 | LIVE |
| RibbingSpacing | 3+1=4 | 0..3 | 0.75 | 1.29903811 | 1+1=2 | 0..3 | 1.5 | 1.5 | LIVE |
| RibbingDepth | 3+1=4 | 0.400000006..0.400000006 | 0.400000006 | 0 | 1+1=2 | 0.400000006..0.400000006 | 0.400000006 | 0 | LIVE |
| CliffStrength | 3+1=4 | 0..0.5 | 0.125 | 0.216506351 | 1+1=2 | 0..0.5 | 0.25 | 0.25 | LIVE |
| ScallopStrength | 3+1=4 | 0..0.5 | 0.125 | 0.216506351 | 1+1=2 | 0..0.5 | 0.25 | 0.25 | LIVE |
| ScallopFrequency | 3+1=4 | 0.100000001..0.100000001 | 0.100000001 | 0 | 1+1=2 | 0.100000001..0.100000001 | 0.100000001 | 0 | LIVE |
| ArchDensity | 3+1=4 | 0..0.100000001 | 0.0250000004 | 0.0433012708 | 1+1=2 | 0..0.100000001 | 0.0500000007 | 0.0500000007 | LIVE |
| ArchMinRadius | 3+1=4 | 3..3 | 3 | 0 | 1+1=2 | 3..3 | 3 | 0 | LIVE |
| ArchMaxRadius | 3+1=4 | 6..6 | 6 | 0 | 1+1=2 | 6..6 | 6 | 0 | LIVE |
| ColumnDensity | 3+0=3 | 0..0 | 0 | 0 | 1+0=1 | 0..0 | 0 | 0 | DEAD |
| ColumnMinRadius | 3+0=3 | 2..2 | 2 | 0 | 1+0=1 | 2..2 | 2 | 0 | DEAD |
| ColumnMaxRadius | 3+0=3 | 5..5 | 5 | 0 | 1+0=1 | 5..5 | 5 | 0 | DEAD |
| PitDensity | 3+0=3 | 0..0 | 0 | 0 | 1+0=1 | 0..0 | 0 | 0 | DEAD |
| PitMinRadius | 3+0=3 | 4..4 | 4 | 0 | 1+0=1 | 4..4 | 4 | 0 | DEAD |
| PitMaxRadius | 3+0=3 | 10..10 | 10 | 0 | 1+0=1 | 10..10 | 10 | 0 | DEAD |
| PitDepth | 3+0=3 | 25..25 | 25 | 0 | 1+0=1 | 25..25 | 25 | 0 | DEAD |
| ChimneyDensity | 3+0=3 | 0..0 | 0 | 0 | 1+0=1 | 0..0 | 0 | 0 | DEAD |
| ChimneyMinRadius | 3+0=3 | 2..2 | 2 | 0 | 1+0=1 | 2..2 | 2 | 0 | DEAD |
| ChimneyMaxRadius | 3+0=3 | 5..5 | 5 | 0 | 1+0=1 | 5..5 | 5 | 0 | DEAD |
| ChimneyHeight | 3+0=3 | 20..20 | 20 | 0 | 1+0=1 | 20..20 | 20 | 0 | DEAD |
| DomeDensity | 3+1=4 | 0..0.150000006 | 0.0375000015 | 0.0649519079 | 1+1=2 | 0..0.150000006 | 0.075000003 | 0.075000003 | LIVE |
| DomeMinRadius | 3+1=4 | 8..8 | 8 | 0 | 1+1=2 | 8..8 | 8 | 0 | LIVE |
| DomeMaxRadius | 3+1=4 | 15..15 | 15 | 0 | 1+1=2 | 15..15 | 15 | 0 | LIVE |
| DomeHeightRatio | 3+1=4 | 0.800000012..0.800000012 | 0.800000012 | 0 | 1+1=2 | 0.800000012..0.800000012 | 0.800000012 | 0 | LIVE |
| PinchDensity | 3+1=4 | 0..0.150000006 | 0.0375000015 | 0.0649519079 | 1+1=2 | 0..0.150000006 | 0.075000003 | 0.075000003 | LIVE |
| PinchStrength | 3+1=4 | 5..5 | 5 | 0 | 1+1=2 | 5..5 | 5 | 0 | LIVE |
| PinchLength | 3+1=4 | 12..12 | 12 | 0 | 1+1=2 | 12..12 | 12 | 0 | LIVE |
The first pass excluded all **34** terrain-detail fields. That was an unmeasured structural claim,
so `VoxelForge.Composer.TerrainDetailLiveness` now tests it directly. For both the legacy and
operator-stack `GetDensityAt` paths, it uses a fixed **4,096-point** lattice (16×16×16, 4-voxel
spacing, the TunnelNetwork fixture), clears the terrain-operation pool, records a baseline, then
changes only one operation group to substantial values. The two paths produced bit-identical
results. The aggregate verdict is **9 live groups / 23 live fields** and **3 dead groups / 11 dead
fields**:
| Operation group | Changed samples / 4,096 | Sign changes | Sum abs delta | Max abs delta | Verdict |
|---|---:|---:|---:|---:|---|
| Terrace | 1,314 | 40 | 2,177.12156 | 5.09695816 | LIVE |
| LayerLines | 532 | 8 | 163.082606 | 0.64941144 | LIVE |
| Overhang | 569 | 12 | 598.018486 | 4.41974068 | LIVE |
| Ribbing | 933 | 6 | 213.929947 | 1.49233627 | LIVE |
| Cliff | 391 | 3 | 372.185603 | 2.80882835 | LIVE |
| Scallop | 38 | 0 | 5.78068841 | 1.01755953 | LIVE |
| Arch | 418 | 190 | 7,327.12817 | 36.0000019 | LIVE |
| Column | 0 | 0 | 0 | 0 | DEAD |
| Pit | 0 | 0 | 0 | 0 | DEAD |
| Chimney | 0 | 0 | 0 | 0 | DEAD |
| Dome | 227 | 44 | 3,262.50304 | 24.0000019 | LIVE |
| Pinch | 226 | 22 | 1,217.39489 | 16.3533421 | LIVE |
The result is identical for both paths, so each row above represents two measured rows. The
baseline mean was `-3.86253725` for every group. Variant means were, respectively, `-3.8647801`,
`-3.82272216`, `-4.00853786`, `-3.91476624`, `-3.86411419`, `-3.86112595`, `-5.6513869`,
`-3.86253725`, `-3.86253725`, `-3.86253725`, `-3.06602772`, and `-4.1597528` in the table's
order. The live fields are therefore removed from the exclusion list and are now eligible for the
roll. Only the 11 direct Column/Pit/Chimney transport fields plus the two runtime Z bounds remain
excluded.
The dead-field call chain is concrete. `UVoxelStrateManager::BuildParamsFromDefinition`
(`Private/VoxelStrateManager.cpp:1399-1404`) returns only `Definition->GenerationParams`; it does
not merge terrain-op assets. `VoxelGenerator::GetDensityAt` (`Private/VoxelGenerator.cpp:1099-1109`)
gets the active definition's `TerrainOperations` pool and passes it to
`VoxelCaveMorphology::BuildChunkCache`. The cache applies the selected
`UVoxelTerrainOpDefinition` at `Private/VoxelCaveMorphology.cpp:1741`, then consumes
`PitDensity`/shape at `:1744-1764`, `ChimneyDensity`/shape at `:1767-1787`, and
`ColumnDensity`/shape at `:1790-1803`. Sampling uses the resulting `SDFCache.Pits` and
`SDFCache.Chimneys` at `Private/VoxelGenerator.cpp:1147-1206` and `SDFCache.Columns` at
`:1730-1752`; it does not read the direct strata Column/Pit/Chimney slots. The project contains
exactly **1** `UVoxelTerrainOpDefinition` asset, `Content/VoxelForge/Blocks/NewDataAsset.uasset`,
so a second asset corpus would have one member and no useful spread.
The composer now seeds each of the 23 live fields with **one matching per-operation default** from
the `UVoxelTerrainOpDefinition` header (applied through `ApplyTo`, with no duplicated constants),
then adds any authored strate values. The current TunnelNetwork group has 3 authored samples + 1
operation-default sample; Underwater has 1 + 1. The dead fields receive no synthetic seed and stay
excluded. The complete test table exposes `authored`, `op-defaults`, and effective `samples`.
Recommendation for the dead trio: do **not** build an asset corpus for the one current asset. The
right future composer abstraction is direct terrain-op parameter structs/configurations, seeded by
the per-operation defaults and any authored op values, with the rolled op payload carried alongside
`FVoxelStrateRollInfo` and injected into the transient candidate's terrain-op pool. That requires a
small candidate/materialisation API and a populated-pool liveness/box test; simply jittering the
current direct FStrate slots would remain a silent no-op. This task rolls every field that the
existing direct strata path can actually move and leaves the dead trio neutral until that API exists.
The bool policy is explicit: `bTunnelsFlowTowardOrigin` is inherited from the dominant parent.
`OriginRoomMaxConnections` and `RoughnessNoiseType` retain `Lerp`'s existing SNAP behavior and
are not jittered. In this editor build clamp metadata was available while the table was built; it
is **not promised in a cooked runtime**, so the commandlet/cook-time bake remains owed.
The fed 64-candidate run (step 4 sweep, radius 256, max 8,000,000 cells, fixture seed 1337)
produced **59/64 survivors (92.2%)**. There were 60 non-vacuous candidates, 60 with largest
component share ≥ 0.50, 63 exact unsnapped arrival→departure law passes, and no roll failures.
The complete 64-row table is emitted by `VoxelForge.Composer.ParameterRoll`; each row reports
archetype, weighted parents, air fraction, largest share, walkable fraction, feature scale, and
arrival→departure verdict. The current run took **114.845 s** total (**0.014 s** corpus load,
**114.827 s** roll/measurement).
The controlled 64-candidate run (step 4 sweep, radius 256, max 8,000,000 cells, fixture seed 1337)
kept the existing corpus hash/RNG stream while enabling the live detail fields. It produced
**59/64 survivors (92.2%)**: 60 non-vacuous candidates, 60 with largest component share ≥ 0.50,
63 exact unsnapped arrival→departure law passes, and no roll failures. The complete 64-row table
is emitted by `VoxelForge.Composer.ParameterRoll`; each row reports archetype, weighted parents,
air fraction, largest share, walkable fraction, feature scale, and arrival→departure verdict.
Feature scale was **0→376** (the prior `190ff38` run was **4→376**), while walkable fraction was
**0.000000→0.388506** (prior **0.000→0.389**). Thus detail variation widened the feature-scale
range only at the low end by four voxels; it did not increase the upper scale or the walkable
spread. The 17 TunnelNetwork/Underwater candidates all carried at least one non-zero live detail
activation, and each of Terrace/LayerLines/Overhang/Ribbing/Cliff/Scallop/Arch/Dome/Pinch appeared
non-zero in all 17. The measurement is therefore not broadly sensitive to these detail fields in
this 64-candidate corpus, which is an empirical result, not a reason to remove the live knobs. The
controlled run took **113.317 s** total (**0.015 s** corpus load, **113.300 s** roll/measurement).
The §6.2 check is no longer vacuous: **1,224 boxes proved**, **1,629,144 voxels checked**,
**0 violations**. Its verdict mix was 1,336 Mixed, 549 AllSolid, and 675 AllAir; the law check
reported no violating candidate. The box law used unit step and brute-forced every voxel of each
uniform box, while the candidate sweep retained step 4. The test asserts zero violations and keeps
the law open to future changed candidates rather than treating this clean run as a license to tune
the survival rate.
The §6.2 check remains non-vacuous: **1,228 boxes proved**, **1,634,468 voxels checked**, and
**0 violations**. Its verdict mix was 1,332 Mixed, 553 AllSolid, and 675 AllAir; no candidate
violated the box law. The box law used unit step and brute-forced every voxel of each uniform box,
while the candidate sweep retained step 4. The test asserts zero violations and keeps the law open
to future changed candidates rather than treating this clean run as a license to tune the survival
rate.
### 3.4 The measurement pass — one grid, one flood fill, three jobs