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:
1 parent
859a9b25c1
commit
f088c19f88
5 files changed
+781
-128
No files matched your search
+16
-10
@@ -378,7 +378,7 @@ context. It never touches a generator, operator stack, manager, cache, or mutabl
|
||||
| `EVoxelNoiseType` | 99 | FBM/Ridged/Mixed/Cellular. |
|
||||
| `ECaveGeneratorType` | 146 | TunnelNetwork / FlatPlain / CrystalChamber. |
|
||||
| `EVoxelStrateTransition` | 183 | Gradient / Hard / Interleaved boundary blends. |
|
||||
| **`FStrateGenerationParams`** | ~350 | The giant TunnelNetwork param bag (rock, worms, rooms, tunnels, warp, roughness, all terrain-op transport fields, boundary seal). `Lerp()` static blends two sets at boundaries — it expands the **`VF_STRATE_PARAM_FIELDS` X-macro** (defined just above the struct): **adding a field to the struct? add it to that list** or blends silently reset it to default. |
|
||||
| **`FStrateGenerationParams`** | ~350 | The giant TunnelNetwork param bag (rock, worms, rooms, tunnels, warp, roughness, live terrain-detail fields, dead terrain-op transport slots, boundary seal). `Lerp()` static blends two sets at boundaries — it expands the **`VF_STRATE_PARAM_FIELDS` X-macro** (defined just above the struct): **adding a field to the struct? add it to that list** or blends silently reset it to default. |
|
||||
| `FStrateTerrainOpEntry` | 965 | Soft-ptr to a terrain op + Weight + Probability. |
|
||||
| **`FSlabGenerationParams`** | 1019 | Floor/ceiling heights, roughness, columns, seal — for slab generators. |
|
||||
| **`FPlacementProfile`** | ~1747 | **Shared placement vocabulary** for every scatter primitive (`FStrateDecoration`, `FStrateLandmark`, coming `FStrateSetPiece`): spawn (ActorClass/InstancedMesh), Filter gates (surface/slope/overhang/water/RequiredBiome + **F7 awareness `Conditions[]`** — `FTerrainCondition` relief/moisture/biome-border predicates, AND-ed, evaluated by `Generator::EvaluateTerrainConditions`), Transform (align/offsets/RotationOffset+RandomRotation/scale), Render (cull/shadow). Each primitive embeds it as `Profile` + keeps only its own DISTRIBUTION fields. Per-primitive defaults set in each struct's ctor (deco: scale 0.8-1.2 + RandomRotation.Yaw=360; landmark: Ceiling + no align). |
|
||||
@@ -414,7 +414,7 @@ Maps depth→strate at runtime; owns passages.
|
||||
| `GetSlabParamsForChunk` | 490 | Slab params with runtime Z bounds (no blend — slabs use Hard). |
|
||||
| `GetBiomeContextForChunk` | — | Flatten the strate's `Biomes[]` + `BiomeMapParams` into a POD `FBiomeContext` for the biome field. Empty ⇒ biomes disabled. §8.14. |
|
||||
| `GetGenerationParams` | 515 | **Blended** TunnelNetwork params (handles Gradient/Hard/Interleaved transitions). |
|
||||
| `BuildParamsFromDefinition` (static) | 771 | Base params + merge all referenced terrain op assets. The one place ops fold into params. |
|
||||
| `BuildParamsFromDefinition` (static) | 1399 | Returns the definition's base `GenerationParams` only. Terrain-op assets are passed separately to `BuildChunkCache`, which applies one selected op per room and pre-bakes Column/Pit/Chimney features. |
|
||||
|
||||
**`Public/VoxelTerrainOpDefinition.h` + `.cpp`** — `UVoxelTerrainOpDefinition : UPrimaryDataAsset`
|
||||
(h:67). One asset = one terrain op. `EVoxelTerrainOpType` (h:36): Terrace, LayerLines,
|
||||
@@ -446,13 +446,18 @@ The corpus is grouped by exact `ECaveGeneratorType`: the 2026-09-04 run loaded *
|
||||
vectors + 8 defaults = 12 members** in **8 groups**. Sibling families use their native structs
|
||||
(`FSlabGenerationParams`, `FMazeGenerationParams`, `FSurfaceGenerationParams`,
|
||||
`FVerticalShaftParams`, `FFloatingIslandParams`); no cross-archetype blend is attempted.
|
||||
Spreads are measured per group, including the plain-native tunnel transport fields for reporting.
|
||||
Rolls are pure in `(corpus contents, seed, index)`: weighted 2–3-parent same-archetype selection →
|
||||
`FStrateGenerationParams::Lerp` or reflected native-family blend → ±15% of measured field range
|
||||
jitter → editor-reflection clamps → ordered-pair repair. Bool `bTunnelsFlowTowardOrigin` inherits
|
||||
from the dominant parent; integer/enum SNAP fields are not jittered. This API is not called by
|
||||
runtime generation. The settings audit found **one unique path**, `/Game/VoxelForge/DA_Strate3`,
|
||||
which explains the old one-vector corpus; the project assets were never in that pool.
|
||||
Spreads are measured per group, including the plain-native tunnel fields. The fixed-lattice audit
|
||||
found **23 live terrain-detail fields** (Terrace, LayerLines, Overhang, Ribbing, Cliff, Scallop,
|
||||
Arch, Dome, Pinch) and **11 dead direct transport fields** (Column, Pit, Chimney); only the latter
|
||||
remain excluded. Live fields receive one matching `UVoxelTerrainOpDefinition` header-default sample
|
||||
in addition to authored strate values, so an all-default strate corpus still has useful activation
|
||||
spread. Rolls are pure in `(corpus contents, seed, index)`: weighted 2–3-parent same-archetype
|
||||
selection → `FStrateGenerationParams::Lerp` or reflected native-family blend → ±15% of measured
|
||||
field range jitter → editor-reflection clamps → ordered-pair repair. Bool
|
||||
`bTunnelsFlowTowardOrigin` inherits from the dominant parent; integer/enum SNAP fields are not
|
||||
jittered. This API is not called by runtime generation. The settings audit found **one unique path**,
|
||||
`/Game/VoxelForge/DA_Strate3`, which explains the old one-vector corpus; the project assets were
|
||||
never in that pool.
|
||||
|
||||
`FVoxelOpStackRecipe` is the serialisable Tier 4b manifest: root polarity, root/source/conversion
|
||||
class IDs, an ordered modifier ID list, and one native parameter-family ID per entry. Posts are not
|
||||
@@ -526,7 +531,8 @@ The plugin's first tests (`OPSTACK-PLAN.md` Phase 0.5). Run them from the editor
|
||||
| `VoxelForgeOpStackIslandTest.cpp` | `VoxelForge.OpStack.FloatingIslandEquivalence` | The port that runs the stack **backwards** — void + fill vs rock + carve, same classes with the opposite sign. Counts interior-solid and open-void samples separately (on this archetype an aggregate "N solid" is dominated by the seal bands and says nothing about the islands). Counts `AllSolid` and `AllAir` verdicts **separately** too: `AllAir` is the one no cave archetype could ever prove, and it is the entire perf argument here. |
|
||||
| `VoxelForgeOpStackMazeTest.cpp` | `VoxelForge.OpStack.MazeEquivalence` | **Phase 1's load-bearing test.** The 7-op Maze stack vs `GetMazeDensity` over 20k points (aiming for bit-identity; a side-of-iso disagreement is the hard fail), plus purity across workers and brute force on every box verdict the stack emits. Reports how many tiles the stack can prove uniform — today's `ClassifyTile` proves **zero** for any cave archetype. |
|
||||
| `VoxelForgeStrateParamCoverageTest.cpp` | `VoxelForge.Determinism.StrateParamBlendCoverage` | **The X-macro guard** (added 2026-08-17). `FStrateGenerationParams::Lerp` blends the hand-written `VF_STRATE_PARAM_FIELDS` list, **not** the struct — so a field added to one and not the other compiles, tests green, and silently takes its **default** inside every Gradient/Interleaved transition band. This expands the X-macro a **third** way (after LERP and SNAP), into a name list, and diffs it against the struct's UObject reflection. Pure shape test: no fixture, no world, instant. `GExemptFieldNames` is **empty** — every reflected field is covered today, and any exemption must be written down as a decision. Stakes rise with the world composer, which intends to invent parameter sets through this same `Lerp` (`COMPOSER-NOTES.md`). |
|
||||
| `VoxelForgeComposerParameterRollTest.cpp` | `VoxelForge.Composer.ParameterRoll` | Asset-Registry corpus audit + complete per-archetype spread/exclusion/clamp table; asserts bit-identical deterministic rerolls and same-archetype parents; measures 64 transient candidate strates with `VF_MeasureStrate` plus the exact unsnapped arrival→departure law; brute-forces every rolled production box verdict. Current run: **59/64 survival (92.2%)**, **1,336 Mixed + 549 AllSolid + 675 AllAir = 1,224 proved boxes**, **1,629,144 lattice voxels checked, 0 violations**, 114.845 s total. |
|
||||
| `VoxelForgeComposerParameterRollTest.cpp` | `VoxelForge.Composer.ParameterRoll` | Asset-Registry corpus audit + complete per-archetype spread/exclusion/clamp table; asserts bit-identical deterministic rerolls and same-archetype parents; measures 64 transient candidate strates with `VF_MeasureStrate` plus the exact unsnapped arrival→departure law; brute-forces every rolled production box verdict. Current detail-enabled run: **59/64 survival (92.2%)**, **1,332 Mixed + 553 AllSolid + 675 AllAir = 1,228 proved boxes**, **1,634,468 lattice voxels checked, 0 violations**, 113.317 s total. Feature scale **0→376**; walkable **0.000000→0.388506**; all 17 TunnelNetwork/Underwater candidates carried live detail activation. |
|
||||
| ″ | `VoxelForge.Composer.TerrainDetailLiveness` | Fixed 4,096-point `GetDensityAt` lattice, legacy and operator-stack paths; changes one terrain-detail group at a time with an empty terrain-op pool. Proves 9 live groups / 23 fields and 3 dead groups / 11 fields; all 24 rows match. |
|
||||
| `VoxelForgeComposerStructureRollTest.cpp` | `VoxelForge.Composer.StructureRoll` | Rolls root polarity → legal shape source → polarity-derived conversion → 4–8 declaration-legal modifiers → mandatory structural posts; blends the six native parameter families independently, measures 64 novel stacks, checks exact arrival→departure connectivity, rerolls every recipe/stack for determinism, and brute-forces every uniform box verdict. Focused run: **46/64 survival (71.9%)**, **64 distinct recipes**, **RoomGraph 15 / Lattice 19 / Shaft 8 / Island 14 / Noise 8**, **k=4:11 / 5:13 / 6:11 / 7:10 / 8:19**, **489 proved boxes / 650,859 voxels / 0 violations**. |
|
||||
| `VoxelForgeLayoutOrderIndependenceTest.cpp` | `VoxelForge.Determinism.LayoutOrderIndependence` | Builds a known transient soft-pointer pool, then rebuilds it in original, reversed, and swapped orders. Requires a non-empty layout and passage set, and compares every slot's definition/Z/height plus passage endpoints, radius, type, control geometry, and bounds bit-for-bit. |
|
||||
| `VoxelForgePassageOpenSpaceTest.cpp` | `VoxelForge.Determinism.PassageLandsInOpenSpace` | Uses the real fixture density path to check every generated inter-strate passage whose destination query answers: a 16-point ring outside the mouth's carve/blend band has at least half its samples in destination air, and the endpoint matches the pure open-point result within the mouth's float envelope. This is a connectivity proxy, not a flood-fill proof. Reports checked passages and false/unanswerable archetypes; fails if it inspects zero passages. |
|
||||
|
||||
+124
-44
@@ -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
|
||||
|
||||
|
||||
@@ -330,6 +330,251 @@ namespace
|
||||
}
|
||||
return Report;
|
||||
}
|
||||
|
||||
enum class ETerrainDetailProbeGroup : uint8
|
||||
{
|
||||
Terrace,
|
||||
LayerLines,
|
||||
Overhang,
|
||||
Ribbing,
|
||||
Cliff,
|
||||
Scallop,
|
||||
Arch,
|
||||
Column,
|
||||
Pit,
|
||||
Chimney,
|
||||
Dome,
|
||||
Pinch,
|
||||
};
|
||||
|
||||
struct FTerrainDetailProbeSpec
|
||||
{
|
||||
ETerrainDetailProbeGroup Group;
|
||||
const TCHAR* Name;
|
||||
bool bExpectedLive;
|
||||
};
|
||||
|
||||
static const FTerrainDetailProbeSpec GTerrainDetailProbeSpecs[] =
|
||||
{
|
||||
{ ETerrainDetailProbeGroup::Terrace, TEXT("Terrace"), true },
|
||||
{ ETerrainDetailProbeGroup::LayerLines,TEXT("LayerLines"),true },
|
||||
{ ETerrainDetailProbeGroup::Overhang, TEXT("Overhang"), true },
|
||||
{ ETerrainDetailProbeGroup::Ribbing, TEXT("Ribbing"), true },
|
||||
{ ETerrainDetailProbeGroup::Cliff, TEXT("Cliff"), true },
|
||||
{ ETerrainDetailProbeGroup::Scallop, TEXT("Scallop"), true },
|
||||
{ ETerrainDetailProbeGroup::Arch, TEXT("Arch"), true },
|
||||
{ ETerrainDetailProbeGroup::Column, TEXT("Column"), false },
|
||||
{ ETerrainDetailProbeGroup::Pit, TEXT("Pit"), false },
|
||||
{ ETerrainDetailProbeGroup::Chimney, TEXT("Chimney"), false },
|
||||
{ ETerrainDetailProbeGroup::Dome, TEXT("Dome"), true },
|
||||
{ ETerrainDetailProbeGroup::Pinch, TEXT("Pinch"), true },
|
||||
};
|
||||
|
||||
void VF_ClearTerrainDetailFields(FStrateGenerationParams& Params)
|
||||
{
|
||||
Params.TerraceStepHeight = 0.0f;
|
||||
Params.TerraceHardness = 0.0f;
|
||||
Params.TerraceNoiseDisplacement = 0.0f;
|
||||
Params.LayerLineSpacing = 0.0f;
|
||||
Params.LayerLineDepth = 0.0f;
|
||||
Params.OverhangStrength = 0.0f;
|
||||
Params.OverhangDepth = 0.0f;
|
||||
Params.OverhangFrequency = 0.0f;
|
||||
Params.RibbingSpacing = 0.0f;
|
||||
Params.RibbingDepth = 0.0f;
|
||||
Params.CliffStrength = 0.0f;
|
||||
Params.ScallopStrength = 0.0f;
|
||||
Params.ScallopFrequency = 0.0f;
|
||||
Params.ArchDensity = 0.0f;
|
||||
Params.ArchMinRadius = 0.0f;
|
||||
Params.ArchMaxRadius = 0.0f;
|
||||
Params.ColumnDensity = 0.0f;
|
||||
Params.ColumnMinRadius = 0.0f;
|
||||
Params.ColumnMaxRadius = 0.0f;
|
||||
Params.PitDensity = 0.0f;
|
||||
Params.PitMinRadius = 0.0f;
|
||||
Params.PitMaxRadius = 0.0f;
|
||||
Params.PitDepth = 0.0f;
|
||||
Params.ChimneyDensity = 0.0f;
|
||||
Params.ChimneyMinRadius = 0.0f;
|
||||
Params.ChimneyMaxRadius = 0.0f;
|
||||
Params.ChimneyHeight = 0.0f;
|
||||
Params.DomeDensity = 0.0f;
|
||||
Params.DomeMinRadius = 0.0f;
|
||||
Params.DomeMaxRadius = 0.0f;
|
||||
Params.DomeHeightRatio = 0.0f;
|
||||
Params.PinchDensity = 0.0f;
|
||||
Params.PinchStrength = 0.0f;
|
||||
Params.PinchLength = 0.0f;
|
||||
}
|
||||
|
||||
void VF_SetTerrainDetailProbe(FStrateGenerationParams& Params,
|
||||
ETerrainDetailProbeGroup Group)
|
||||
{
|
||||
switch (Group)
|
||||
{
|
||||
case ETerrainDetailProbeGroup::Terrace:
|
||||
Params.TerraceStepHeight = 12.0f;
|
||||
Params.TerraceHardness = 0.9f;
|
||||
Params.TerraceNoiseDisplacement = 1.0f;
|
||||
break;
|
||||
case ETerrainDetailProbeGroup::LayerLines:
|
||||
Params.LayerLineSpacing = 12.0f;
|
||||
Params.LayerLineDepth = 1.0f;
|
||||
break;
|
||||
case ETerrainDetailProbeGroup::Overhang:
|
||||
Params.OverhangStrength = 1.0f;
|
||||
Params.OverhangDepth = 12.0f;
|
||||
Params.OverhangFrequency = 0.03f;
|
||||
break;
|
||||
case ETerrainDetailProbeGroup::Ribbing:
|
||||
Params.RibbingSpacing = 10.0f;
|
||||
Params.RibbingDepth = 1.5f;
|
||||
break;
|
||||
case ETerrainDetailProbeGroup::Cliff:
|
||||
Params.CliffStrength = 1.0f;
|
||||
break;
|
||||
case ETerrainDetailProbeGroup::Scallop:
|
||||
Params.ScallopStrength = 2.0f;
|
||||
Params.ScallopFrequency = 0.04f;
|
||||
break;
|
||||
case ETerrainDetailProbeGroup::Arch:
|
||||
Params.ArchDensity = 1.0f;
|
||||
Params.ArchMinRadius = 8.0f;
|
||||
Params.ArchMaxRadius = 14.0f;
|
||||
break;
|
||||
case ETerrainDetailProbeGroup::Column:
|
||||
Params.ColumnDensity = 1.0f;
|
||||
Params.ColumnMinRadius = 8.0f;
|
||||
Params.ColumnMaxRadius = 14.0f;
|
||||
break;
|
||||
case ETerrainDetailProbeGroup::Pit:
|
||||
Params.PitDensity = 1.0f;
|
||||
Params.PitMinRadius = 8.0f;
|
||||
Params.PitMaxRadius = 14.0f;
|
||||
Params.PitDepth = 64.0f;
|
||||
break;
|
||||
case ETerrainDetailProbeGroup::Chimney:
|
||||
Params.ChimneyDensity = 1.0f;
|
||||
Params.ChimneyMinRadius = 8.0f;
|
||||
Params.ChimneyMaxRadius = 14.0f;
|
||||
Params.ChimneyHeight = 64.0f;
|
||||
break;
|
||||
case ETerrainDetailProbeGroup::Dome:
|
||||
Params.DomeDensity = 1.0f;
|
||||
Params.DomeMinRadius = 14.0f;
|
||||
Params.DomeMaxRadius = 24.0f;
|
||||
Params.DomeHeightRatio = 1.25f;
|
||||
break;
|
||||
case ETerrainDetailProbeGroup::Pinch:
|
||||
Params.PinchDensity = 1.0f;
|
||||
Params.PinchStrength = 12.0f;
|
||||
Params.PinchLength = 32.0f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void VF_BuildTerrainDetailLattice(const VoxelForgeTest::FTestWorld& World,
|
||||
TArray<FVector>& OutPoints)
|
||||
{
|
||||
int32 TopVoxelZ = 0;
|
||||
int32 BottomVoxelZ = 0;
|
||||
OutPoints.Reset();
|
||||
if (!World.GetSlotVoxelZRange(VoxelForgeTest::FTestWorld::SlotTunnelNetwork,
|
||||
TopVoxelZ, BottomVoxelZ))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr int32 Side = 16;
|
||||
constexpr int32 Step = 4;
|
||||
const int32 CentreZ = (TopVoxelZ + BottomVoxelZ) / 2;
|
||||
OutPoints.Reserve(Side * Side * Side);
|
||||
for (int32 IZ = 0; IZ < Side; ++IZ)
|
||||
for (int32 IY = 0; IY < Side; ++IY)
|
||||
for (int32 IX = 0; IX < Side; ++IX)
|
||||
{
|
||||
OutPoints.Add(FVector(
|
||||
(float)(-32 + IX * Step),
|
||||
(float)(-32 + IY * Step),
|
||||
(float)(CentreZ - 32 + IZ * Step)));
|
||||
}
|
||||
}
|
||||
|
||||
struct FTerrainDetailDensitySummary
|
||||
{
|
||||
uint32 Digest = 2166136261u;
|
||||
double Mean = 0.0;
|
||||
double Min = 0.0;
|
||||
double Max = 0.0;
|
||||
};
|
||||
|
||||
FTerrainDetailDensitySummary VF_SampleTerrainDetailLattice(
|
||||
const VoxelForgeTest::FTestWorld& World,
|
||||
const TArray<FVector>& Points,
|
||||
TArray<float>& OutDensities)
|
||||
{
|
||||
FTerrainDetailDensitySummary Summary;
|
||||
OutDensities.Reset(Points.Num());
|
||||
if (Points.Num() == 0 || !World.Generator.IsValid())
|
||||
{
|
||||
return Summary;
|
||||
}
|
||||
|
||||
Summary.Min = FLT_MAX;
|
||||
Summary.Max = -FLT_MAX;
|
||||
double Sum = 0.0;
|
||||
for (const FVector& Point : Points)
|
||||
{
|
||||
const float Density = World.Generator->GetDensityAt(Point.X, Point.Y, Point.Z);
|
||||
OutDensities.Add(Density);
|
||||
uint32 Bits = 0;
|
||||
FMemory::Memcpy(&Bits, &Density, sizeof(Bits));
|
||||
Summary.Digest ^= Bits;
|
||||
Summary.Digest *= 16777619u;
|
||||
Summary.Min = FMath::Min(Summary.Min, (double)Density);
|
||||
Summary.Max = FMath::Max(Summary.Max, (double)Density);
|
||||
Sum += Density;
|
||||
}
|
||||
Summary.Mean = Sum / (double)Points.Num();
|
||||
return Summary;
|
||||
}
|
||||
|
||||
struct FTerrainDetailDensityDelta
|
||||
{
|
||||
int32 Changed = 0;
|
||||
int32 SignChanged = 0;
|
||||
double SumAbs = 0.0;
|
||||
double MaxAbs = 0.0;
|
||||
};
|
||||
|
||||
FTerrainDetailDensityDelta VF_CompareTerrainDetailSamples(
|
||||
const TArray<float>& Baseline,
|
||||
const TArray<float>& Variant)
|
||||
{
|
||||
FTerrainDetailDensityDelta Delta;
|
||||
if (Baseline.Num() != Variant.Num())
|
||||
{
|
||||
return Delta;
|
||||
}
|
||||
for (int32 Index = 0; Index < Baseline.Num(); ++Index)
|
||||
{
|
||||
const float A = Baseline[Index];
|
||||
const float B = Variant[Index];
|
||||
if (!VoxelForgeTest::BitEqual(A, B))
|
||||
{
|
||||
++Delta.Changed;
|
||||
}
|
||||
if ((A < 0.0f) != (B < 0.0f))
|
||||
{
|
||||
++Delta.SignChanged;
|
||||
}
|
||||
const double AbsDelta = FMath::Abs((double)B - (double)A);
|
||||
Delta.SumAbs += AbsDelta;
|
||||
Delta.MaxAbs = FMath::Max(Delta.MaxAbs, AbsDelta);
|
||||
}
|
||||
return Delta;
|
||||
}
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
@@ -390,7 +635,7 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Corpus membership: %d project strate assets discovered, %d project vectors loaded, "
|
||||
"%d defaults, %d total members. Terrain-op Asset Registry count (not rolled in this "
|
||||
"task): %d."),
|
||||
"task; live fields use header defaults instead): %d."),
|
||||
ProjectStrateAssets.Num(), NumProjectCorpusEntries, NumDefaultCorpusEntries, Corpus.Num(),
|
||||
ProjectTerrainOpAssets.Num()));
|
||||
AddInfo(CorpusMembership);
|
||||
@@ -422,19 +667,57 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
|
||||
TestEqual(TEXT("all project strate assets plus eight defaults are corpus members"),
|
||||
Corpus.Num(), ProjectStrateAssets.Num() + 8);
|
||||
|
||||
static const TCHAR* LiveTerrainDetailFields[] =
|
||||
{
|
||||
TEXT("TerraceStepHeight"), TEXT("TerraceHardness"), TEXT("TerraceNoiseDisplacement"),
|
||||
TEXT("LayerLineSpacing"), TEXT("LayerLineDepth"),
|
||||
TEXT("OverhangStrength"), TEXT("OverhangDepth"), TEXT("OverhangFrequency"),
|
||||
TEXT("RibbingSpacing"), TEXT("RibbingDepth"), TEXT("CliffStrength"),
|
||||
TEXT("ScallopStrength"), TEXT("ScallopFrequency"), TEXT("ArchDensity"),
|
||||
TEXT("ArchMinRadius"), TEXT("ArchMaxRadius"),
|
||||
TEXT("DomeDensity"), TEXT("DomeMinRadius"), TEXT("DomeMaxRadius"),
|
||||
TEXT("DomeHeightRatio"), TEXT("PinchDensity"), TEXT("PinchStrength"),
|
||||
TEXT("PinchLength"),
|
||||
};
|
||||
static const TCHAR* DeadTerrainDetailFields[] =
|
||||
{
|
||||
TEXT("ColumnDensity"), TEXT("ColumnMinRadius"), TEXT("ColumnMaxRadius"),
|
||||
TEXT("PitDensity"), TEXT("PitMinRadius"), TEXT("PitMaxRadius"), TEXT("PitDepth"),
|
||||
TEXT("ChimneyDensity"), TEXT("ChimneyMinRadius"), TEXT("ChimneyMaxRadius"),
|
||||
TEXT("ChimneyHeight"),
|
||||
};
|
||||
for (const TCHAR* FieldName : LiveTerrainDetailFields)
|
||||
{
|
||||
const FVoxelStrateFieldSpread* Spread = Corpus.FindSpread(
|
||||
ECaveGeneratorType::TunnelNetwork, FString(FieldName));
|
||||
TestTrue(FString::Printf(TEXT("live terrain field %s is no longer excluded"), FieldName),
|
||||
Spread != nullptr && !Spread->bExcluded);
|
||||
}
|
||||
for (const TCHAR* FieldName : DeadTerrainDetailFields)
|
||||
{
|
||||
const FVoxelStrateFieldSpread* Spread = Corpus.FindSpread(
|
||||
ECaveGeneratorType::TunnelNetwork, FString(FieldName));
|
||||
TestTrue(FString::Printf(TEXT("dead terrain field %s remains excluded"), FieldName),
|
||||
Spread != nullptr && Spread->bExcluded);
|
||||
}
|
||||
TestEqual(TEXT("exactly eleven direct terrain transport fields remain excluded"),
|
||||
static_cast<int32>(UE_ARRAY_COUNT(DeadTerrainDetailFields)), 11);
|
||||
|
||||
FString SpreadTable = TEXT(
|
||||
"CORPUS SPREAD (population stddev; jitter range = ±15% of max-min; clamps are safety only)\n"
|
||||
"archetype | struct | field | kind | excluded | reflected | samples | min | max | mean | stddev | clamp\n");
|
||||
"archetype | struct | field | kind | excluded | reflected | authored | op-defaults | samples | min | max | mean | stddev | clamp\n");
|
||||
for (const FVoxelStrateFieldSpread& Spread : Corpus.GetFieldSpreads())
|
||||
{
|
||||
SpreadTable += FString::Printf(
|
||||
TEXT("%s | %s | %s | %s | %s | %s | %d | %s | %s | %s | %s | %s\n"),
|
||||
TEXT("%s | %s | %s | %s | %s | %s | %d | %d | %d | %s | %s | %s | %s | %s\n"),
|
||||
VF_GetStrateArchetypeName(Spread.Archetype),
|
||||
*Spread.ParamStructName,
|
||||
*Spread.FieldName,
|
||||
VF_FieldKindName(Spread.Kind),
|
||||
Spread.bExcluded ? TEXT("yes") : TEXT("no"),
|
||||
Spread.bReflected ? TEXT("yes") : TEXT("no"),
|
||||
Spread.AuthoredSampleCount,
|
||||
Spread.DefaultSeedCount,
|
||||
Spread.SampleCount,
|
||||
*VF_FormatSpreadValue(Spread.Min),
|
||||
*VF_FormatSpreadValue(Spread.Max),
|
||||
@@ -455,8 +738,10 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
|
||||
"Bool policy: bTunnelsFlowTowardOrigin is inherited from the dominant weighted parent; "
|
||||
"int/enum SNAP fields use the native family blend and are not jittered. Parent selection "
|
||||
"is weight-1.0 (equal) because UVoxelStrateDefinition has no corpus-weight field; parents "
|
||||
"are selected only within one exact archetype group. FStrateGenerationParams terrain-op "
|
||||
"transport fields remain excluded; the sibling families' authored fields are rolled."));
|
||||
"are selected only within one exact archetype group. The 23 empirically live terrain-detail "
|
||||
"fields are rolled, with one per-operation header-default sample added to their spread; "
|
||||
"only the 11 empirically dead Column/Pit/Chimney transport fields remain excluded. The "
|
||||
"sibling families' authored fields are rolled."));
|
||||
|
||||
TestTrue(TEXT("the reflected parameter schema is covered or explicitly excluded"),
|
||||
Corpus.IsSchemaValid());
|
||||
@@ -509,6 +794,9 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
|
||||
int32 TotalBoxProved = 0;
|
||||
int64 TotalBoxCheckedVoxels = 0;
|
||||
int32 TotalBoxViolations = 0;
|
||||
int32 NumTunnelLikeCandidates = 0;
|
||||
int32 NumCandidatesWithLiveDetail = 0;
|
||||
int32 LiveActivationVariationCounts[9] = {};
|
||||
TArray<int32> BoxViolationCandidates;
|
||||
FString FirstBoxViolation;
|
||||
|
||||
@@ -534,6 +822,41 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Roll.Archetype == ECaveGeneratorType::TunnelNetwork
|
||||
|| Roll.Archetype == ECaveGeneratorType::Underwater)
|
||||
{
|
||||
const FStrateGenerationParams& DetailParams =
|
||||
Roll.ArchetypeParams.TunnelNetworkParams;
|
||||
const float LiveActivationValues[] =
|
||||
{
|
||||
DetailParams.TerraceStepHeight,
|
||||
DetailParams.LayerLineSpacing,
|
||||
DetailParams.OverhangStrength,
|
||||
DetailParams.RibbingSpacing,
|
||||
DetailParams.CliffStrength,
|
||||
DetailParams.ScallopStrength,
|
||||
DetailParams.ArchDensity,
|
||||
DetailParams.DomeDensity,
|
||||
DetailParams.PinchDensity,
|
||||
};
|
||||
++NumTunnelLikeCandidates;
|
||||
bool bHasLiveDetail = false;
|
||||
for (int32 DetailIndex = 0;
|
||||
DetailIndex < static_cast<int32>(UE_ARRAY_COUNT(LiveActivationValues));
|
||||
++DetailIndex)
|
||||
{
|
||||
if (LiveActivationValues[DetailIndex] != 0.0f)
|
||||
{
|
||||
bHasLiveDetail = true;
|
||||
++LiveActivationVariationCounts[DetailIndex];
|
||||
}
|
||||
}
|
||||
if (bHasLiveDetail)
|
||||
{
|
||||
++NumCandidatesWithLiveDetail;
|
||||
}
|
||||
}
|
||||
|
||||
bool bParentsMatchArchetype = true;
|
||||
for (const int32 ParentIndex : Roll.ParentEntryIndices)
|
||||
{
|
||||
@@ -682,6 +1005,31 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
|
||||
LargestComponentSurvivalThreshold, NumNonVacuous, NumLargestEnough, NumLawPasses,
|
||||
NumRollFailures, RollAndMeasureSeconds));
|
||||
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Live detail activation coverage: %d/%d TunnelNetwork/Underwater candidates had at "
|
||||
"least one non-zero live activation; Terrace/Layer/Overhang/Ribbing/Cliff/Scallop/"
|
||||
"Arch/Dome/Pinch non-zero candidate counts = %d/%d/%d/%d/%d/%d/%d/%d/%d."),
|
||||
NumCandidatesWithLiveDetail, NumTunnelLikeCandidates,
|
||||
LiveActivationVariationCounts[0], LiveActivationVariationCounts[1],
|
||||
LiveActivationVariationCounts[2], LiveActivationVariationCounts[3],
|
||||
LiveActivationVariationCounts[4], LiveActivationVariationCounts[5],
|
||||
LiveActivationVariationCounts[6], LiveActivationVariationCounts[7],
|
||||
LiveActivationVariationCounts[8]));
|
||||
TestTrue(TEXT("at least one TunnelNetwork/Underwater candidate carries live detail variation"),
|
||||
NumCandidatesWithLiveDetail > 0);
|
||||
static const TCHAR* LiveActivationNames[] =
|
||||
{
|
||||
TEXT("Terrace"), TEXT("LayerLines"), TEXT("Overhang"), TEXT("Ribbing"), TEXT("Cliff"),
|
||||
TEXT("Scallop"), TEXT("Arch"), TEXT("Dome"), TEXT("Pinch"),
|
||||
};
|
||||
for (int32 DetailIndex = 0;
|
||||
DetailIndex < static_cast<int32>(UE_ARRAY_COUNT(LiveActivationNames)); ++DetailIndex)
|
||||
{
|
||||
TestTrue(FString::Printf(TEXT("live %s activation appears in a rolled candidate"),
|
||||
LiveActivationNames[DetailIndex]),
|
||||
LiveActivationVariationCounts[DetailIndex] > 0);
|
||||
}
|
||||
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Rolled §6.2 box verdict check: mixed=%d, AllSolid=%d, AllAir=%d, proved=%d, "
|
||||
"brute-force lattice samples=%lld, violations=%d; violating candidates=%s. "
|
||||
@@ -716,4 +1064,137 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FVoxelForgeTerrainDetailLivenessTest,
|
||||
"VoxelForge.Composer.TerrainDetailLiveness",
|
||||
EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FVoxelForgeTerrainDetailLivenessTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
using namespace VoxelForgeTest;
|
||||
(void)Parameters;
|
||||
|
||||
constexpr int32 ExpectedLatticeSamples = 16 * 16 * 16;
|
||||
FString Table = TEXT(
|
||||
"TERRAIN DETAIL LIVENESS (same 4096-point lattice; only one group changed per row)\n"
|
||||
"path | group | samples | baseline digest | variant digest | baseline mean | variant mean | "
|
||||
"changed | sign changes | sum abs delta | max abs delta | verdict\n");
|
||||
int32 TotalRows = 0;
|
||||
int32 FailedRows = 0;
|
||||
|
||||
for (const bool bUseOperatorStack : { false, true })
|
||||
{
|
||||
FTestWorld World;
|
||||
World.Build(1337, 2, bUseOperatorStack);
|
||||
if (!World.IsValid())
|
||||
{
|
||||
AddError(World.WhyInvalid());
|
||||
return false;
|
||||
}
|
||||
|
||||
UVoxelStrateDefinition* Definition =
|
||||
World.Definitions[FTestWorld::SlotTunnelNetwork].Get();
|
||||
if (Definition == nullptr)
|
||||
{
|
||||
AddError(TEXT("The fixture has no TunnelNetwork definition for the liveness experiment."));
|
||||
return false;
|
||||
}
|
||||
Definition->TerrainOperations.Reset();
|
||||
TestEqual(
|
||||
FString::Printf(TEXT("%s fixture has no terrain-op asset pool"),
|
||||
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy")),
|
||||
Definition->TerrainOperations.Num(), 0);
|
||||
|
||||
TArray<FVector> Points;
|
||||
VF_BuildTerrainDetailLattice(World, Points);
|
||||
TestEqual(
|
||||
FString::Printf(TEXT("%s experiment uses the fixed lattice"),
|
||||
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy")),
|
||||
Points.Num(), ExpectedLatticeSamples);
|
||||
|
||||
FStrateGenerationParams BaselineParams = Definition->GenerationParams;
|
||||
VF_ClearTerrainDetailFields(BaselineParams);
|
||||
TArray<float> FirstBaseline;
|
||||
bool bHaveFirstBaseline = false;
|
||||
|
||||
for (const FTerrainDetailProbeSpec& Spec : GTerrainDetailProbeSpecs)
|
||||
{
|
||||
Definition->GenerationParams = BaselineParams;
|
||||
World.Reinitialize();
|
||||
|
||||
TArray<float> BaselineSamples;
|
||||
const FTerrainDetailDensitySummary BaselineSummary =
|
||||
VF_SampleTerrainDetailLattice(World, Points, BaselineSamples);
|
||||
if (!bHaveFirstBaseline)
|
||||
{
|
||||
FirstBaseline = BaselineSamples;
|
||||
bHaveFirstBaseline = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
bool bSameBaseline = FirstBaseline.Num() == BaselineSamples.Num();
|
||||
for (int32 SampleIndex = 0; bSameBaseline && SampleIndex < FirstBaseline.Num(); ++SampleIndex)
|
||||
{
|
||||
bSameBaseline = BitEqual(FirstBaseline[SampleIndex], BaselineSamples[SampleIndex]);
|
||||
}
|
||||
TestTrue(
|
||||
FString::Printf(TEXT("%s baseline is stable before %s probe"),
|
||||
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy"),
|
||||
Spec.Name),
|
||||
bSameBaseline);
|
||||
}
|
||||
|
||||
FStrateGenerationParams VariantParams = BaselineParams;
|
||||
VF_SetTerrainDetailProbe(VariantParams, Spec.Group);
|
||||
Definition->GenerationParams = VariantParams;
|
||||
World.Reinitialize();
|
||||
|
||||
TArray<float> VariantSamples;
|
||||
const FTerrainDetailDensitySummary VariantSummary =
|
||||
VF_SampleTerrainDetailLattice(World, Points, VariantSamples);
|
||||
const FTerrainDetailDensityDelta Delta =
|
||||
VF_CompareTerrainDetailSamples(BaselineSamples, VariantSamples);
|
||||
|
||||
const bool bObservedLive = Delta.Changed > 0;
|
||||
const bool bVerdictMatches = bObservedLive == Spec.bExpectedLive;
|
||||
++TotalRows;
|
||||
if (!bVerdictMatches)
|
||||
{
|
||||
++FailedRows;
|
||||
}
|
||||
TestTrue(
|
||||
FString::Printf(TEXT("%s %s field group has the measured liveness verdict"),
|
||||
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy"),
|
||||
Spec.Name),
|
||||
bVerdictMatches);
|
||||
|
||||
Table += FString::Printf(
|
||||
TEXT("%s | %s | %d | 0x%08x | 0x%08x | %.9g | %.9g | %d | %d | %.9g | %.9g | %s\n"),
|
||||
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy"),
|
||||
Spec.Name,
|
||||
Points.Num(),
|
||||
BaselineSummary.Digest,
|
||||
VariantSummary.Digest,
|
||||
BaselineSummary.Mean,
|
||||
VariantSummary.Mean,
|
||||
Delta.Changed,
|
||||
Delta.SignChanged,
|
||||
Delta.SumAbs,
|
||||
Delta.MaxAbs,
|
||||
bObservedLive ? TEXT("LIVE") : TEXT("DEAD"));
|
||||
}
|
||||
}
|
||||
|
||||
AddInfo(Table);
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Terrain detail liveness: %d group/path rows measured, %d mismatches; LIVE groups "
|
||||
"must have at least one bit-changed density and DEAD groups must have none. No "
|
||||
"terrain-operation assets were present in the fixture pool."),
|
||||
TotalRows, FailedRows));
|
||||
TestEqual(TEXT("all terrain detail liveness rows matched their measured verdict"), FailedRows, 0);
|
||||
TestEqual(TEXT("both density paths measured every terrain detail group"), TotalRows,
|
||||
static_cast<int32>(2 * UE_ARRAY_COUNT(GTerrainDetailProbeSpecs)));
|
||||
return FailedRows == 0;
|
||||
}
|
||||
|
||||
#endif // WITH_DEV_AUTOMATION_TESTS
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "VoxelSettings.h"
|
||||
#include "VoxelStrateDefinition.h"
|
||||
#include "VoxelTerrainOpDefinition.h"
|
||||
#include "VoxelDensityOpStack.h"
|
||||
|
||||
#include <type_traits>
|
||||
@@ -547,6 +548,88 @@ namespace
|
||||
return VF_WriteRuntimeField(Memory, Archetype, FieldName, Value);
|
||||
}
|
||||
|
||||
const TCHAR* VF_GetTerrainOperationFieldPrefix(EVoxelTerrainOpType Type)
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
case EVoxelTerrainOpType::Terrace: return TEXT("Terrace");
|
||||
case EVoxelTerrainOpType::LayerLines: return TEXT("LayerLine");
|
||||
case EVoxelTerrainOpType::Ribbing: return TEXT("Ribbing");
|
||||
case EVoxelTerrainOpType::Cliff: return TEXT("Cliff");
|
||||
case EVoxelTerrainOpType::Scallop: return TEXT("Scallop");
|
||||
case EVoxelTerrainOpType::Overhang: return TEXT("Overhang");
|
||||
case EVoxelTerrainOpType::Arch: return TEXT("Arch");
|
||||
case EVoxelTerrainOpType::Column: return TEXT("Column");
|
||||
case EVoxelTerrainOpType::Pit: return TEXT("Pit");
|
||||
case EVoxelTerrainOpType::Chimney: return TEXT("Chimney");
|
||||
case EVoxelTerrainOpType::Dome: return TEXT("Dome");
|
||||
case EVoxelTerrainOpType::Pinch: return TEXT("Pinch");
|
||||
default: return TEXT("");
|
||||
}
|
||||
}
|
||||
|
||||
void VF_AddTerrainOperationDefaultSamples(
|
||||
TArray<TArray<double>>& Values,
|
||||
const TArray<FVoxelStrateFieldSpread>& Spreads)
|
||||
{
|
||||
if (Values.Num() != Spreads.Num())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UVoxelTerrainOpDefinition* Operation = NewObject<UVoxelTerrainOpDefinition>(
|
||||
GetTransientPackage(), NAME_None, RF_Transient);
|
||||
if (Operation == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
static const EVoxelTerrainOpType OperationTypes[] =
|
||||
{
|
||||
EVoxelTerrainOpType::Terrace,
|
||||
EVoxelTerrainOpType::LayerLines,
|
||||
EVoxelTerrainOpType::Ribbing,
|
||||
EVoxelTerrainOpType::Cliff,
|
||||
EVoxelTerrainOpType::Scallop,
|
||||
EVoxelTerrainOpType::Overhang,
|
||||
EVoxelTerrainOpType::Arch,
|
||||
EVoxelTerrainOpType::Column,
|
||||
EVoxelTerrainOpType::Pit,
|
||||
EVoxelTerrainOpType::Chimney,
|
||||
EVoxelTerrainOpType::Dome,
|
||||
EVoxelTerrainOpType::Pinch,
|
||||
};
|
||||
|
||||
for (const EVoxelTerrainOpType Type : OperationTypes)
|
||||
{
|
||||
const TCHAR* Prefix = VF_GetTerrainOperationFieldPrefix(Type);
|
||||
Operation->Type = Type;
|
||||
FStrateGenerationParams Defaults;
|
||||
Operation->ApplyTo(Defaults);
|
||||
|
||||
// Seed only the fields owned by this operation. The values come from the actual
|
||||
// UVoxelTerrainOpDefinition UPROPERTY initializers through ApplyTo; no second asset
|
||||
// corpus and no duplicated default constants are introduced here.
|
||||
for (int32 FieldIndex = 0; FieldIndex < Spreads.Num(); ++FieldIndex)
|
||||
{
|
||||
const FVoxelStrateFieldSpread& Spread = Spreads[FieldIndex];
|
||||
if (!VF_IsTunnelArchetype(Spread.Archetype)
|
||||
|| Spread.bExcluded
|
||||
|| !Spread.FieldName.StartsWith(Prefix))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
double Value = 0.0;
|
||||
if (VF_ReadNamedField(&Defaults, Spread.Archetype, Spread.FieldName, Value)
|
||||
&& FMath::IsFinite(Value))
|
||||
{
|
||||
Values[FieldIndex].Add(Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VF_AddPropertyMetadata(FVoxelStrateFieldSpread& Spread, const FProperty* Property)
|
||||
{
|
||||
#if WITH_EDITOR
|
||||
@@ -607,26 +690,11 @@ namespace
|
||||
OutIndices.Add(VF_SpreadKey(Archetype, Name), OutSpreads.Num() - 1);
|
||||
}
|
||||
|
||||
void VF_ResetNonTunableFields(FStrateGenerationParams& Params)
|
||||
void VF_ResetDeadTerrainTransportFields(FStrateGenerationParams& Params)
|
||||
{
|
||||
// These are terrain-op transport values (written per room by UVoxelTerrainOpDefinition)
|
||||
// plus manager-owned runtime Z bounds. They are not rolled in this composer pass.
|
||||
Params.TerraceStepHeight = 0.0f;
|
||||
Params.TerraceHardness = 0.0f;
|
||||
Params.TerraceNoiseDisplacement = 0.0f;
|
||||
Params.LayerLineSpacing = 0.0f;
|
||||
Params.LayerLineDepth = 0.0f;
|
||||
Params.OverhangStrength = 0.0f;
|
||||
Params.OverhangDepth = 0.0f;
|
||||
Params.OverhangFrequency = 0.0f;
|
||||
Params.RibbingSpacing = 0.0f;
|
||||
Params.RibbingDepth = 0.0f;
|
||||
Params.CliffStrength = 0.0f;
|
||||
Params.ScallopStrength = 0.0f;
|
||||
Params.ScallopFrequency = 0.0f;
|
||||
Params.ArchDensity = 0.0f;
|
||||
Params.ArchMinRadius = 0.0f;
|
||||
Params.ArchMaxRadius = 0.0f;
|
||||
// Empirical liveness proved these direct FStrate slots are dead: the runtime bakes these
|
||||
// three operation families from UVoxelTerrainOpDefinition values into room caches before
|
||||
// sampling. Keep them at their neutral values until terrain-op structs are rolled too.
|
||||
Params.ColumnDensity = 0.0f;
|
||||
Params.ColumnMinRadius = 0.0f;
|
||||
Params.ColumnMaxRadius = 0.0f;
|
||||
@@ -638,13 +706,6 @@ namespace
|
||||
Params.ChimneyMinRadius = 0.0f;
|
||||
Params.ChimneyMaxRadius = 0.0f;
|
||||
Params.ChimneyHeight = 0.0f;
|
||||
Params.DomeDensity = 0.0f;
|
||||
Params.DomeMinRadius = 0.0f;
|
||||
Params.DomeMaxRadius = 0.0f;
|
||||
Params.DomeHeightRatio = 0.0f;
|
||||
Params.PinchDensity = 0.0f;
|
||||
Params.PinchStrength = 0.0f;
|
||||
Params.PinchLength = 0.0f;
|
||||
Params.StrateTopWorldZ = 0.0f;
|
||||
Params.StrateBottomWorldZ = 0.0f;
|
||||
}
|
||||
@@ -662,7 +723,7 @@ namespace
|
||||
{
|
||||
if (VF_IsTunnelArchetype(Archetype))
|
||||
{
|
||||
VF_ResetNonTunableFields(Params.TunnelNetworkParams);
|
||||
VF_ResetDeadTerrainTransportFields(Params.TunnelNetworkParams);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -690,7 +751,12 @@ namespace
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (Spread != nullptr && !Spread->bReflected && !Spread->bExcluded)
|
||||
double NativeValue = 0.0;
|
||||
FStrateGenerationParams NativeDefaults;
|
||||
const bool bNativeFStrateField = VF_IsTunnelArchetype(Archetype)
|
||||
&& VF_ReadFStrateField(NativeDefaults, FieldName, NativeValue);
|
||||
if (Spread != nullptr && !Spread->bReflected && !Spread->bExcluded
|
||||
&& !bNativeFStrateField)
|
||||
{
|
||||
bSchemaValid = false;
|
||||
VF_AppendSchemaError(SchemaError,
|
||||
@@ -807,6 +873,38 @@ namespace
|
||||
return;
|
||||
}
|
||||
|
||||
// FStrateGenerationParams deliberately keeps the terrain-detail transport fields as
|
||||
// native, non-UPROPERTY members. Handle those fields through the same measured spread
|
||||
// policy as reflected fields; otherwise removing an exclusion would only change the
|
||||
// report while the candidate value stayed at its blended parent value.
|
||||
if (VF_IsTunnelArchetype(Archetype))
|
||||
{
|
||||
for (const FVoxelStrateFieldSpread& Spread : Corpus.GetFieldSpreads())
|
||||
{
|
||||
if (Spread.Archetype != Archetype
|
||||
|| Spread.bReflected
|
||||
|| Spread.bExcluded
|
||||
|| Spread.Kind != EVoxelStrateFieldKind::Continuous)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const double CorpusRange = FMath::Max(0.0, Spread.Max - Spread.Min);
|
||||
if (!(CorpusRange > 0.0))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
double Value = 0.0;
|
||||
if (VF_ReadNamedField(Memory, Archetype, Spread.FieldName, Value))
|
||||
{
|
||||
Value += static_cast<double>((Rng.FRand() * 2.0f - 1.0f) * GJitterFraction)
|
||||
* CorpusRange;
|
||||
VF_WriteNamedField(Memory, Archetype, Spread.FieldName, Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TMap<FString, FProperty*> Properties;
|
||||
VF_CollectProperties(Struct, Properties);
|
||||
for (const TPair<FString, FProperty*>& Pair : Properties)
|
||||
@@ -1327,44 +1425,21 @@ const TArray<FVoxelStrateFieldExclusion>& FVoxelStrateCorpus::GetNonTunableField
|
||||
{
|
||||
TArray<FVoxelStrateFieldExclusion> Result;
|
||||
|
||||
const TCHAR* TerrainOpReason = TEXT(
|
||||
"Transport slot only: generation overlays it per room from a UVoxelTerrainOpDefinition "
|
||||
"selected through the active strate's TerrainOperations pool; it is not the authored "
|
||||
"strate-generation source.");
|
||||
VF_AddExclusion(Result, TEXT("TerraceStepHeight"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("TerraceHardness"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("TerraceNoiseDisplacement"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("LayerLineSpacing"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("LayerLineDepth"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("OverhangStrength"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("OverhangDepth"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("OverhangFrequency"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("RibbingSpacing"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("RibbingDepth"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("CliffStrength"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("ScallopStrength"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("ScallopFrequency"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("ArchDensity"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("ArchMinRadius"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("ArchMaxRadius"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("ColumnDensity"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("ColumnMinRadius"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("ColumnMaxRadius"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("PitDensity"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("PitMinRadius"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("PitMaxRadius"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("PitDepth"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("ChimneyDensity"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("ChimneyMinRadius"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("ChimneyMaxRadius"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("ChimneyHeight"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("DomeDensity"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("DomeMinRadius"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("DomeMaxRadius"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("DomeHeightRatio"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("PinchDensity"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("PinchStrength"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("PinchLength"), TerrainOpReason);
|
||||
const TCHAR* DeadTerrainOpReason = TEXT(
|
||||
"Empirically dead when only this FStrateGenerationParams slot changes: the runtime "
|
||||
"bakes Column/Pit/Chimney geometry from UVoxelTerrainOpDefinition values into the "
|
||||
"room cache, so this direct strata slot does not feed GetDensityAt.");
|
||||
VF_AddExclusion(Result, TEXT("ColumnDensity"), DeadTerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("ColumnMinRadius"), DeadTerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("ColumnMaxRadius"), DeadTerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("PitDensity"), DeadTerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("PitMinRadius"), DeadTerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("PitMaxRadius"), DeadTerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("PitDepth"), DeadTerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("ChimneyDensity"), DeadTerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("ChimneyMinRadius"), DeadTerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("ChimneyMaxRadius"), DeadTerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("ChimneyHeight"), DeadTerrainOpReason);
|
||||
|
||||
VF_AddExclusion(Result, TEXT("StrateTopWorldZ"), TEXT(
|
||||
"Runtime Z bound supplied by UVoxelStrateManager for the active layout slot; not a tunable."));
|
||||
@@ -1500,10 +1575,17 @@ void FVoxelStrateCorpus::RebuildSpreads()
|
||||
}
|
||||
}
|
||||
|
||||
for (int32 FieldIndex = 0; FieldIndex < FieldSpreads.Num(); ++FieldIndex)
|
||||
{
|
||||
FieldSpreads[FieldIndex].AuthoredSampleCount = Values[FieldIndex].Num();
|
||||
}
|
||||
VF_AddTerrainOperationDefaultSamples(Values, FieldSpreads);
|
||||
|
||||
for (int32 FieldIndex = 0; FieldIndex < FieldSpreads.Num(); ++FieldIndex)
|
||||
{
|
||||
FVoxelStrateFieldSpread& Spread = FieldSpreads[FieldIndex];
|
||||
const TArray<double>& Samples = Values[FieldIndex];
|
||||
Spread.DefaultSeedCount = Samples.Num() - Spread.AuthoredSampleCount;
|
||||
Spread.SampleCount = Samples.Num();
|
||||
if (Samples.Num() == 0)
|
||||
{
|
||||
|
||||
@@ -120,7 +120,7 @@ enum class EVoxelStrateFieldKind : uint8
|
||||
Enum,
|
||||
};
|
||||
|
||||
/** A field deliberately kept out of the parameter roll. */
|
||||
/** A field deliberately kept out of the parameter roll after the measured liveness audit. */
|
||||
struct VOXELFORGE_API FVoxelStrateFieldExclusion
|
||||
{
|
||||
FString FieldName;
|
||||
@@ -137,6 +137,10 @@ struct VOXELFORGE_API FVoxelStrateFieldSpread
|
||||
bool bExcluded = false;
|
||||
bool bReflected = false;
|
||||
|
||||
// Actual authored corpus samples and synthetic per-operation default samples are kept
|
||||
// separate in the report. SampleCount is their effective total used for the statistics.
|
||||
int32 AuthoredSampleCount = 0;
|
||||
int32 DefaultSeedCount = 0;
|
||||
int32 SampleCount = 0;
|
||||
double Min = 0.0;
|
||||
double Max = 0.0;
|
||||
@@ -246,10 +250,10 @@ public:
|
||||
ECaveGeneratorType Archetype, const FString& FieldName) const;
|
||||
const FVoxelStrateFieldSpread* FindSpread(const FString& FieldName) const;
|
||||
|
||||
/** Stable hash of source paths, archetypes, weights, and every listed scalar field. */
|
||||
/** Stable hash of source paths, archetypes, weights, and every listed scalar corpus field. */
|
||||
uint32 GetContentsHash() const;
|
||||
|
||||
/** Explicit exclusion list discovered from the struct reflection/use audit. */
|
||||
/** Explicit exclusion list retained after the fixed-lattice terrain-detail liveness audit. */
|
||||
static const TArray<FVoxelStrateFieldExclusion>& GetNonTunableFields();
|
||||
|
||||
/** True in an editor build where UPROPERTY metadata was available while spreads were built. */
|
||||
|
||||
Reference in new issue
Block a user