feat(scale): size the world to a human body -- and it is NOT enough

Jahni: "can't you try and make the world to scale?" Every length parameter
is now derived from the body and annotated with its metre value, so nobody
has to do the arithmetic to notice something is absurd.

  CorridorRadius   4 -> 8     1 m -> 2 m radius (4 m bore)
  CellSize        40 -> 64    keeps a real wall between wider corridors
  TunnelMinRadius  3 -> 6     0.75 m -> 1.5 m
  ShaftMinRadius   5 -> 8     1.25 m -> 2 m
  IslandMinRadius 18 -> 24    4.5 m -> 6 m
  MouthRadius      6 -> 8     passage mouths 4 m
  plus passage reach, spiral, cascade, bridges, ridges, chasms

The reasoning that motivated it still looks right: a 2 m circular bore for
a 1.76 m player clears the head only along the exact centreline, so almost
no cell has a floor with 7.04 voxels of clear air above it. A 4 m bore is
genuinely walkable.

⚠️ BUT IT DID NOT FIX TRAVERSABILITY, and this time it is not the
instrument. The player-fit law is measured at STEP 1 on a mouth-fitted
window -- full resolution, both mouths inside the box -- and across the
eight showcase archetypes it reports:

  6 NotConnectedAtThisResolution   (real negatives at full resolution)
  2 OutOfWindow                    (unmeasurable, not passing)
  1 StartCellNotPlayerFit          (the arrival mouth itself is too small)
  0 CONNECTED

And every archetype reports hard-gate survivors=0, so the showcase is
displaying the least-bad of an entirely failing set.

⛔ DO NOT READ player_fit_fraction / player_clearance FROM THE SHOWCASE
LINE. Those come from the COARSE step-4 measurement in the same log line,
where the 1.36-voxel player is sub-cell and fit is unmeasurable --
player_clearance reads a uniform "1.00 voxels" for all eight archetypes,
which is one cell, not one voxel. Only restricted_law is at step 1 and
only it is trustworthy. Fixing that mixed-resolution reporting is next.

So scale was necessary and insufficient. My hypothesis was that bore
diameter versus body height was the binding constraint; doubling it moved
nothing measurable. Something else is preventing player-sized volume from
existing at all -- worth finding before tuning any further.

Codex applied these edits and then died mid-poll without validating them
(third time a killed agent left a clean-looking unbuilt diff). Build and
suite run by me: build clean, 30 succeeded / 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K8V9my1PL68pmQb2gutZZV
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-09-06 00:43:33 +02:00
1 parent eb47efdec9
commit fc43a86709
17 files changed
+1298 -349

No files matched your search

+46 -1
View File
@@ -11,6 +11,50 @@
A large A-to-Z expansion. The world is a stack of strates the player descends through;
each strate can be a fundamentally different *archetype*, connected at (0,0).
### 8.0 Human-scale generation contract (2026-09-05)
All authored lengths are voxel-space values, converted at the boundary by `0.25 m/voxel`. The body
reference is a **0.34 m radius, 1.76 m tall** capsule (`1.36 × 7.04` voxels). The default strate
height is now **8 chunks = 256 voxels = 64 m**, which is intentionally larger than the 3-4 chunk
boss-arena anchor so an archetype can reserve seals, vertical play, and a high ceiling together.
The default size families are deliberately independent:
| Feature | Default metre derivation | Coupling kept sound |
|---------|--------------------------|---------------------|
| Room network | radius 4-10 m (8-20 m diameters), origin radius 12 m (24 m hub), cell spacing 32 m | `MaxRoomRadius 40 < RoomSpacing/2 64`; a 6 m radial margin remains around the largest room |
| Round passages | 1.5-2 m radius (3-4 m bores); maze corridor radius 2 m (4 m bore) | a 1.76 m body fits vertically with headroom; Maze `8 < CellSize/2 32` |
| Slab | floor at 16 m, ceiling at 38.4 m, 22.4 m open span | floor/ceiling noise and 4-8 m column diameters remain inside the open span |
| SurfaceWorld | 20 m terrain relief, 12 m warp, 60.8 m sky cap | terrain features are bounded below the 64 m strate envelope |
| VerticalShafts | 4-7 m shaft diameters, 3 m connector bore, 8 m ledge interval | shaft spacing is 20 m; connector/ledge values are derived from the roughness envelope |
| FloatingIslands | 12-24 m island diameters, 28 m cell spacing | the largest island leaves a 2 m centre-to-edge gap; thickness is separately derived |
| Inter-strate passages | 3-4 m mouth bore, 3 m mid bore, 8-24 m reach, 16-48 m offset | width, reach, and wander are separate body/placement decisions |
For wall detail, `VOXEL_NOISE_SCALE=1.25` and the proven `sup|fBM|=1.5` give a roughness reach of
`1.875 × SurfaceRoughness` voxels. The default ratios are **0.625** against the minimum tunnel
radius, **0.469** against the Maze corridor radius, **0.469/0.625** against shaft/connector radii,
**0.234** against the minimum island radius (**0.484** including its 6-voxel blend), and
**0.625/0.938** against slab column radius for floor/ceiling roughness. SurfaceWorld is a
heightfield, so its **0.047** ratio is against its 80-voxel relief scale, not a fictitious radial
feature. FlatPlain and CrystalChamber intentionally share `GetSlabDensity` and the same slab
parameter block; they cannot acquire separate scale geometry without a generator change.
This scale contract is informed by [Epic's Unreal level-blockout guidance](https://dev.epicgames.com/documentation/en-us/unreal-engine/designer-01-project-setup-and-level-blockout-in-unreal-engine), which recommends a player-sized reference and gives 2-3 m hall / 3-4 m height starting guidance. The
[2010 ADA Standards](https://www.ada.gov/law-and-regs/design-standards/2010-stds/) are a lower-bound
accessibility reference (915 mm clear walking width and larger passing/turning spaces), while
[NPS Lehman Cave dimensions](https://www.nps.gov/grba/learn/nature/lehman-caves-dimensions.htm) support
keeping natural chambers heterogeneous and allowing 20 m-plus cathedral volumes.
Validation is deliberately split by cost: bounded regression fixtures remain 4 chunks, while the
production definition, season defaults, and owner-facing showcase use 8 chunks. The final focused
showcase (`WorldScaleShowcaseProductionFinal`) exhausted 256 rolls, measured 82 eligible single-region
rolls, skipped 174 multi-region rolls while lateral regions remain gated, and found **0/8** complete
hard-gate survivor set at fine step 1. It still produced diagnostic cards (7 fine renders, 1 refusal,
1 blank). The full `VoxelForge` run (`WorldScaleFullFinal3`) recorded **28 successes, 2 warning-only,
0 failures, 0 not-run**; all box-verdict violation counts were **0**, `WorldRadiusVoxels` stayed **0**,
and operator-stack equivalence paths remained bit-identical. The two authored-corpus relation findings
(2/12) are conservative experiment-envelope findings, not box-verdict violations.
### 8.1 Archetypes (`ECaveGeneratorType`, VoxelStrateTypes.h)
Each archetype has its own param `USTRUCT` (on `UVoxelStrateDefinition`, EditCondition-gated
by `GeneratorType`) and its own density function in `VoxelGenerator.cpp`, dispatched by the
@@ -421,7 +465,8 @@ TunnelNetwork and Underwater keep the nearest hash-room vertical placement at th
`MakeStrateSeed(world-seed, strate-index)` matching the room graph's existing identity; FlatPlain and
CrystalChamber recompute their slab void band and reject column-overlap points. Maze snaps to the
nearest roughness-safe horizontal lattice corridor within one cell, VerticalShafts to the exact
axis of a roughness-safe shaft on the drainage tree within one shaft spacing, and FloatingIslands
axis of a roughness-safe shaft on the drainage tree within two shaft spacings at the default site
density, and FloatingIslands
to a validated blob top within one island spacing.
Those three return false when no footing exists inside that explicit lateral budget. GeneratePassages
interpolates the snapped XY through the control-point chain and recomputes its conservative bound;
+40 -13
View File
@@ -73,6 +73,32 @@ what generator type, and is there a passage/elevator SDF near here?"
Paths relative to `Source/VoxelForge/`. `Public/` = headers, `Private/` = impl.
### 3.0 Human-scale generation contract (2026-09-05)
`VoxelStrateTypes.h` is now authored against the confirmed player capsule, not against naked voxel
counts: one voxel is **0.25 m**, the capsule is **1.36 voxels / 0.34 m radius** and **7.04 voxels /
1.76 m tall**, and the default strate envelope is **8 chunks / 256 voxels / 64 m**. Core defaults are
therefore: room diameters **8-20 m**, origin hub **24 m**, room cells **32 m**, round tunnel bores
**3-4 m**, maze cells **16 m** with a **4 m** bore, slab open span **22.4 m**, shaft diameters
**4-7 m**, island diameters **12-24 m**, and ordinary inter-strate bores **3-4 m**. These are
different derivations, not a global multiplier: passage widths follow the 2.5-4 m walking/fight
target, room/cell spacing preserves a rock web, and the 64 m vertical envelope reserves space for
tall rooms, surface sky, and vertical traversal.
The reachable roughness envelope is `roughness * VOXEL_NOISE_SCALE * 1.5`, or `roughness * 1.875`
voxels. The new defaults keep it below the relevant feature radius: **0.625** of the minimum
TunnelNetwork tunnel radius, **0.469** of Maze corridor radius, **0.469/0.625** of shaft/connector
radii, **0.234** of the minimum island radius (**0.484** including the 6-voxel island blend), and
**0.625/0.938** of slab column radius for floor/ceiling roughness. SurfaceWorld has no radial cave
feature, so its ratio is reported against the 80-voxel terrain-relief scale (**0.047**).
The complete generated `name | voxels | METRES | body/level meaning` audit is emitted by
`VoxelForge.Composer.Showcase` and includes optional transport fields. The official Unreal blockout
guidance ([Unreal level blockout guidance](https://dev.epicgames.com/documentation/en-us/unreal-engine/designer-01-project-setup-and-level-blockout-in-unreal-engine)) uses a player-sized reference and treats 2-3 m halls / 3-4 m heights as useful starting
guidelines; the [2010 ADA Standards](https://www.ada.gov/law-and-regs/design-standards/2010-stds/)
provide a 915 mm clear walking-width floor, not a game-space target; and [NPS Lehman Cave
dimensions](https://www.nps.gov/grba/learn/nature/lehman-caves-dimensions.htm) show why natural
chambers need a wide range rather than one universal volume ratio.
### 3.1 Module & build
| File | Role |
|------|------|
@@ -383,9 +409,9 @@ Header is rich with inline docs. Two namespaces + a per-chunk cache system.
`VF_SuggestLandingPoint` (h:~550, implemented in `VoxelCaveMorphology.cpp`) is the pure landing-site
query: nearest hash-room height at the requested XY for TunnelNetwork/Underwater, slab void midpoint
for FlatPlain/CrystalChamber, and bounded lateral searches for a horizontal lattice corridor (one
Maze cell), the exact axis of a lower-seal/ledge-safe VerticalShafts tree shaft (one ShaftSpacing),
or a validated blob top (one IslandSpacing). The VerticalShafts result is therefore on the drainage
tree, not merely in an open roughness disk.
Maze cell), the exact axis of a lower-seal/ledge-safe VerticalShafts tree shaft (up to two local
shaft spacings at the 0.6 site density), or a validated blob top (one IslandSpacing). The
VerticalShafts result is therefore on the drainage tree, not merely in an open roughness disk.
It returns `false` when those feature-bearing conditions cannot be proven inside the supplied budget;
SurfaceWorld remains refused because its production height can depend on manager-owned biome/per-column
context. It never touches a generator, operator stack, manager, cache, or mutable state.
@@ -537,7 +563,7 @@ renderer and self-contained contact sheet. It consumes `FVoxelStrateSampleGrid`
pass, emits deterministic centre-Y XZ and data-selected XY slices via `IImageWrapper`, and shows filled-cell
and scalar density=0 contour views side by side without blurring the sampled raster. Each image is capped at
512 pixels (including its scale footer). `FVoxelStrateFinePreviewSettings` is a separate caller-selected
fine pass: the composer selects only survivors at step 1, radius 64 (a 128×128 XY ROI), and `MaxCells=2,000,000`;
fine pass: the composer selects only survivors at step 1, radius 64 (a 128×128 XY ROI), and `MaxCells=4,000,000`;
the caller centres it on each coarse survivor's `LargestComponentPoint` and the writer labels it
“largest open space” with its exact ROI and step; cap refusals and blank fine ROIs are recorded explicitly
instead of showing empty evidence. The ordinary 64-card sheet remains sorted by survivor status then
@@ -560,8 +586,9 @@ region manifests remain rejected because their cross-seam law is only 9/16. The
season test currently reports **24 generated / 19 hard-gate survivors / 6 selected / 18 unselected or
rejected**, with **4 vacuous**, **1 primordial-law budget**, **13 policy-not-selected**, and **393,216**
round-trip density samples bit-identical. The specified UE 5.7 editor build succeeded and the full
`VoxelForge` namespace passed **28/28 tests** after the original seam was added; current results must be
re-recorded after the owner build.
`VoxelForge` namespace report `WorldScaleFullFinal3` recorded **28 succeeded, 2 warning-only,
0 failed, 0 not run** in **1,704.010 s**. The later focused production-height showcase was rerun
after the final audit-label correction and is recorded separately below.
**`Public/VoxelSeasonAsset.h` + `Private/VoxelSeasonAsset.cpp`** — `UVoxelSeasonAsset :
UPrimaryDataAsset`, the cookable carrier. Its editor button imports the reviewable JSON, stores the exact
@@ -617,7 +644,7 @@ The plugin's first tests (`OPSTACK-PLAN.md` Phase 0.5). Run them from the editor
| File | Test name | What it proves |
|------|-----------|----------------|
| `VoxelForgeTestFixture.h` | — | `FTestWorld`: a headless world (transient strate definitions → `UVoxelSettings` → a real `UVoxelStrateManager::Initialize`) so tests hit `GetDensityAt`, where the thread_local caches live. One strate per archetype, **pinned via `FixedStrates`** so slot index → archetype is stable across seeds (`SlotSurfaceWorld` etc.). |
| `VoxelForgeTestFixture.h` | — | `FTestWorld`: a headless world (transient strate definitions → `UVoxelSettings` → a real `UVoxelStrateManager::Initialize`) so tests hit `GetDensityAt`, where the thread_local caches live. One strate per archetype, **pinned via `FixedStrates`** so slot index → archetype is stable across seeds (`SlotSurfaceWorld` etc.). Bounded regression fixtures default to 4 chunks; the production/default definition and the owner-facing showcase explicitly use 8 chunks. |
| `VoxelForgeDensityPurityTest.cpp` | `VoxelForge.Determinism.DensityPurity` | 10k points re-sampled in shuffled order, same thread **and** on N workers, asserting BIT equality. `ValidateDeterminism` is game-thread only and cannot see worker-cache divergence. Includes a flat-field canary (AUDIT C1) and a diff-layer pass. |
| ″ | `VoxelForge.Determinism.LiveEditInvalidation` | AUDIT C2 regression: triple the heightfield params, `Initialize` again, require the density to MOVE. The edit does not move the strate, so only `LayoutVersion` changes. |
| `VoxelForgeClassifyTileTest.cpp` | `VoxelForge.Determinism.ClassifyTileSoundness` | Scans for a non-`Mixed` verdict, then brute-forces the exact mesher lattice (`g ∈ [-1, Cells+1]`). **A false verdict is an invisible, collisionless hole** — T1.d v1 was reverted for exactly this. Errors out rather than passing if it found nothing to check. |
@@ -631,16 +658,16 @@ 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. Bootstrap follow-up: **1,422 applications / 168 distinct fields**; **38/64 survival**, feature scale **0→376**, walkable **0.000000→0.286766** (no broadening beyond the prior run); **1,356 Mixed + 608 AllSolid + 596 AllAir = 1,204 proved boxes**, **1,602,524 lattice voxels checked, 0 violations**, 146.250 s total in the final full-namespace run. |
| `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. World-scale run: **1,422 applications / 168 distinct fields**; **36/64 survival**, 60 non-vacuous, 58 largest-component, 40 exact-law passes; **1,411 Mixed + 731 AllSolid + 418 AllAir = 1,149 proved boxes**, **1,529,319 lattice voxels checked, 0 violations**, 148.387 s. |
| ″ | `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. |
| `VoxelForgeComposerPromotionTest.cpp` | `VoxelForge.Composer.Promotion` | Re-measures the 12 project/default members, simulates five deterministic 24-candidate seasons with normalized measured-metric novelty (`<0.20`), cap 6, cumulative JSON promotion, provenance counts, corpus-hash checks, fresh-load gate verification, spread/survival reporting, and deliberate stale-metric corruption. Final run: **9 promoted**, corpus **4/8/9**, survival **20.8/54.2/54.2/45.8/50.0%**, spread **0.869214→0.916302**, **63.556 s**, 0 failures. |
| `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, captures the same grid for the deterministic filled/contour XZ/XY preview, runs a separate step-1 radius-64 ROI pass for the 42 survivors centred on `LargestComponentPoint`, checks exact arrival→departure connectivity, rerolls every recipe/stack for determinism, and brute-forces every uniform box verdict. Final run: **42/64 survival (65.6%)**, **64 distinct recipes**, **0 invalid recipes**, **64 coarse filled + 64 coarse contour pairs**, **42 fine filled + 42 fine contour pairs**, blank plan/card **6/42→3/42**, **243.649 s**, **0 refusals**. |
| `VoxelForgeComposerShowcaseTest.cpp` | `VoxelForge.Composer.Showcase` | Exhausts the bounded parameter-roll set (seeds **0, 7331**, indices **0–63**), excludes multi-region rolls while the lateral gate is off, and measures every missing-family candidate in all six interior target slots at step 4 / radius 256 / `MaxCells=8,000,000`. Hard gates are non-vacuous, largest air share ≥ **0.50**, and exact unsnapped arrival→departure connectivity; selection score is floor-area fraction + clearance tie-break + projected-surface tie-break. It asserts roll/manifest determinism, density sign, zero `WorldRadiusVoxels`, bit-identical metric reruns, and writes one alphabetized card per archetype to `Saved/VoxelForge/Showcase/index.html`, with step-1 radius-64 filled/contour plan + vertical ROI images centred on `LargestComponentPoint`. |
| `VoxelForgeComposerPromotionTest.cpp` | `VoxelForge.Composer.Promotion` | Re-measures the 12 project/default members, simulates five deterministic 24-candidate seasons with normalized measured-metric novelty (`<0.20`), cap 6, cumulative JSON promotion, provenance counts, corpus-hash checks, fresh-load gate verification, spread/survival reporting, and deliberate stale-metric corruption. World-scale run: **16 promoted**, corpus **4/8/16**, survival **45.8/66.7/75.0/66.7/58.3%**, spread **0.677151→0.867010**, **92.390 s**, 0 failures. |
| `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, captures the same grid for the deterministic filled/contour XZ/XY preview, runs a separate step-1 radius-64 ROI pass for the 33 survivors centred on `LargestComponentPoint`, checks exact arrival→departure connectivity, rerolls every recipe/stack for determinism, and brute-forces every uniform box verdict. World-scale run: **33/64 survival (51.6%)**, **64 distinct recipes**, **0 invalid recipes**, **715 proved custom boxes / 951,665 lattice voxels / 0 violations**, blank plan/card **4/33→5/33**, **174.854 s**, **0 refusals**. |
| `VoxelForgeComposerShowcaseTest.cpp` | `VoxelForge.Composer.Showcase` | Exhausts the bounded parameter-roll set (seeds **0, 7331**, indices **0–63**), excludes multi-region rolls while the lateral gate is off, and measures every missing-family candidate in all six interior target slots at step 4 / radius 256 / `MaxCells=8,000,000`. Hard gates are non-vacuous, largest air share ≥ **0.50**, and exact unsnapped arrival→departure connectivity; selection score is floor-area fraction + clearance tie-break + projected-surface tie-break. It asserts roll/manifest determinism, density sign, zero `WorldRadiusVoxels`, bit-identical metric reruns, and writes one alphabetized card per archetype to `Saved/VoxelForge/Showcase/index.html`, with step-1 radius-64 filled/contour plan + vertical ROI images centred on `LargestComponentPoint`. The final production-height run uses **H=8 chunks / 64 m**, emits the full voxel→metre audit, and produced **7 fine renders, 1 refusal, 1 blank**; its hard-gate survivor set was **0/8**. |
| `VoxelForgeComposerSeasonTest.cpp` | `VoxelForge.Composer.Season` | Existing compose/review/393,216-sample round trip plus schema hash tamper rejection, two independent runtime manager/generator instances, season-authoritative seed/spine/radius, unset-season regression, and brute-force verification of every sampled non-Mixed recipe tile verdict (up to 64). |
| `VoxelForgeComposerCorpusFreeTest.cpp` | `VoxelForge.Composer.CorpusFree` | Shares each structure recipe across today's corpus blend, naive independent uniform rolls, and constraint-sampled rolls. The completed equal-arm run uses **16 candidates per arm** (256×3 and 64×3 were stopped before aggregate output for runtime), step 4 / radius 256 / `MaxCells=8,000,000`, fixed passage-law mouths, and 40 box probes per candidate. Result: **13/16, 7/16, 7/16** survival; survivor walkable means **0.066071, 0.012566, 0.012851** and feature-scale means **92.307693, 64.571426, 31.428572**. Box checks: **196/260,876**, **310/412,610**, **262/348,722** proved/voxels, **0 violations** in every arm. |
| `VoxelForgeComposerCorpusFreeTest.cpp` | `VoxelForge.Composer.CorpusFree` | Shares each structure recipe across today's corpus blend, naive independent uniform rolls, and constraint-sampled rolls. The completed equal-arm world-scale run uses **16 candidates per arm**, step 4 / radius 256 / `MaxCells=8,000,000`, fixed passage-law mouths, and 40 box probes per candidate. Result: **10/16, 9/16, 8/16** survival; survivor feature-scale ranges are **8..268**, **8..60**, **8..92** voxels. Box checks: **185/246,235**, **260/346,060**, **299/397,969** proved/voxels, **0 violations** in every arm; constraint parameter and box-stop violations were also 0. |
| `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. |
| `VoxelForgeStrateConnectivityTest.cpp` | `VoxelForge.Generation.StrateConnectivity` / `VoxelForge.Generation.StrateConnectivityRefinement` / `VoxelForge.Generation.VerticalShaftSeamFreedom` | Bounded strate metrics with density-polarity and solid-gap controls, deterministic route rechecks, and refinement sweeps. The refinement test fits the measurement AABB to each arrival/departure mouth pair, reports exact before/after seed-6 cell counts, reruns every negative with doubled margin, measures tree orphan candidates/path reachability and roughness-bubble proxies, asserts axis landings, checks source/mirror physical paths, and requires 16/16 effective arrival→departure results across the 16 VerticalShafts seeds. Margin-binding negatives are reported as measurement limits, never as gap findings. The seam test re-evaluates cell-boundary positions after warming distinct neighbouring chunk contexts in both legacy and operator-stack paths. |
| `VoxelForgeStrateConnectivityTest.cpp` | `VoxelForge.Generation.StrateConnectivity` / `VoxelForge.Generation.StrateConnectivityRefinement` / `VoxelForge.Generation.VerticalShaftSeamFreedom` | Bounded strate metrics with density-polarity and solid-gap controls, deterministic route rechecks, and refinement sweeps. The refinement test uses the bounded 4-chunk density fixture, fits each measurement AABB to the arrival/departure mouth pair plus three shaft spacings (the prescribed local tree fallback), and caps the fitted control at 120,000,000 cells. It reports exact before/after seed-6 cell counts, reruns every negative with the fitted margin doubled, measures tree orphan candidates/path reachability and roughness-bubble proxies, asserts axis landings, checks source/mirror physical paths, and requires 16/16 effective arrival→departure results across the 16 VerticalShafts seeds. Margin-binding negatives are reported as measurement limits, never as gap findings. The seam test re-evaluates cell-boundary positions after warming distinct neighbouring chunk contexts in both legacy and operator-stack paths. |
| `VoxelForgeWorldEdgeSealTest.cpp` | `VoxelForge.Generation.WorldEdgeSeal` | Positive samples outside the radius across every fixture strate, bit-identical interior negative control against radius 0, monotonic smooth-ramp check, radius-0 no-op, radial `ClassifyBox`/T1.d proofs brute-forced for every reported voxel, and synthetic plus generated near-rim passage coverage. Emits one aggregate summary with proved-box and voxel counts. |
## 4. The density pipeline (most-edited hot path)
+172 -8
View File
@@ -381,6 +381,44 @@ small commandlet walking the struct by reflection. Clamp coverage is partial. A
tunables at all (`StrateTopWorldZ` / `StrateBottomWorldZ` are runtime Z bounds) and need an explicit
exclusion list. Bools cannot be blended — roll them by probability or inherit from the dominant parent.
#### Human-scale audit and the killed tunnel-only hypothesis (2026-09-05)
The generation defaults are now derived from the body reference rather than chosen as voxel counts:
**1 voxel = 0.25 m**, the player capsule is **1.36 voxels / 0.34 m radius** and **7.04 voxels /
1.76 m tall**, and the default strate is **8 chunks / 256 voxels / 64 m**. The complete generated
`name | voxels | METRES | body/level meaning` table is emitted by `VoxelForge.Composer.Showcase` and
covers each archetype plus the shared passage/disturbance and optional transport fields.
The requested maze test was run first on the same seed/index and a mouth-fitted step-1 ROI. The
radius is the tunnel radius; the bore is twice the radius:
| Corridor radius | Bore | Player-fit fraction | Traversable share | Minimum clearance | Arrival→departure |
|----------------:|-----:|--------------------:|------------------:|------------------:|-------------------|
| 4 vox / 1.00 m | 2.00 m | 0.009007 | 0.078358 | 1 voxel / 0.25 m | NotConnected |
| 6 vox / 1.50 m | 3.00 m | 0.014545 | 0.165932 | 1 voxel / 0.25 m | NotConnected |
| 8 vox / 2.00 m | 4.00 m | 0.012156 | 0.126354 | 1 voxel / 0.25 m | NotConnected |
| 10 vox / 2.50 m | 5.00 m | 0.009814 | 0.105611 | 1 voxel / 0.25 m | NotConnected |
| 12 vox / 3.00 m | 6.00 m | 0.008697 | 0.185039 | 1 voxel / 0.25 m | NotConnected |
The hypothesis is **killed as a single root cause**. There is no sharp or monotonic fit rise, and
the law stays disconnected at every bore. The unchanged one-voxel minimum also shows that the
failure is not explained by the nominal tunnel diameter alone: the generated field, route mouths,
floor/cap interaction, and/or resolution still need separate diagnosis.
The new defaults use separate derivations: rooms are 8-20 m diameters with a 24 m origin hub and
32 m cells; round passages are 3-4 m bores; Maze is a 16 m cell with a 4 m bore; slabs reserve a
22.4 m floor-to-ceiling span; shaft diameters are 4-7 m with 3 m connectors; islands are 12-24 m
diameters on 28 m cells; ordinary passages are 3-4 m bores and reach 8-24 m. `RoomSpacing` keeps
the largest room 6 m radially clear of the next cell, `CorridorRadius` is 8/32 = 0.25 of Maze half-cell,
and `StrateHeightInChunks` grows from 4 to 8 because 17 m tall rooms, 20 m terrain relief,
vertical shafts, and a high cap do not share a comfortable 32 m envelope. No global multiplier was
used. The roughness reach is `1.875 × roughness` voxels: the new default ratios are
**0.625** (minimum tunnel), **0.469** (Maze), **0.469/0.625** (shaft/connector), **0.234** island
radius (**0.484** including blend), **0.625/0.938** slab column for floor/ceiling, and **0.047**
against SurfaceWorld's 80-voxel relief scale.
The architecture sanity check uses [Epic's Unreal level-blockout guidance](https://dev.epicgames.com/documentation/en-us/unreal-engine/designer-01-project-setup-and-level-blockout-in-unreal-engine) (player-sized reference, 2-3 m halls and 3-4 m heights as starting guidance), the [2010 ADA Standards](https://www.ada.gov/law-and-regs/design-standards/2010-stds/) (915 mm clear walking width and larger passing/turning spaces), and [NPS Lehman Cave dimensions](https://www.nps.gov/grba/learn/nature/lehman-caves-dimensions.htm) (real chambers ranging from passages to 20 m-plus vertical volumes). These are sanity anchors, not claims that one universal ratio makes a good cave.
### ✅ Tier 5 status — promotion is built and measured (2026-09-05)
Promotion is now a real corpus input, not a claim about a future approval UI. A surviving editor
@@ -817,9 +855,9 @@ pass, to:
`Saved/ComposerPreview/structure_seed_0_corpus_9dbcea85_step_4_radius_256_fine_step_1_fine_radius_64/`
The fine pass is explicit and bounded: the current composer selects the 42 coarse survivors, samples
step 1 in a radius-64 (128×128 XY) ROI, and refuses before allocation when `MaxCells=2,000,000` would be
exceeded. The default is radius 64 because it stays around the observed 1.8M-cell budget while exposing
The fine pass is explicit and bounded: the historical composer selected the 42 coarse survivors, sampled
step 1 in a radius-64 (128×128 XY) ROI, and refused before allocation when `MaxCells=2,000,000` would be
exceeded. The default is radius 64 because it stays around the observed multi-million-cell budget while exposing
voxel-scale detail; radius 128 is already roughly 7.1–7.5M cells and radius 256 exceeds the 8M coarse
cap. The public settings let another editor/automation caller choose step, radius, centre, margin, and
cap, or the writer can be called for an explicitly selected list. Fine filenames, captions, and each
@@ -859,8 +897,10 @@ the editor automation test VoxelForge.Composer.Showcase; opening it is offline a
generation hook. It contains one alphabetized card for each native archetype, with the exact four PIE
hand-off values, recipe, coarse step-4 filled/contour XZ+XY views, and a separate step-1 filled/contour
ROI centred on that card's coarse LargestComponentPoint. The fine ROI is bounded at radius 64 and
MaxCells=2,000,000; a refusal or blank ROI is written as text rather than represented by an empty
image. All eight cards in the final run rendered both fine views; refusals **0**, blank cards **0**.
MaxCells=4,000,000; a refusal or blank ROI is written as text rather than represented by an empty
image. The earlier bounded four-chunk run rendered all eight fine views; the final production-height
run is recorded below because its larger window produced **7** fine renders, **1** refusal, and **1**
blank diagnostic card.
The Part A table adds three measurements to the original volumetric walkability ratio:
@@ -881,7 +921,9 @@ those survivors the deterministic score is floor_area + 0.25*clamp(clearance/64,
The fixture and PIE hand-off use the project generator seed **0**, bComposerRollStructure=false,
and leave the existing slot layout, passage endpoints, placement, and WorldRadiusVoxels=0 intact.
Selected-card measurements (each card prints its own exact derived-margin Z window; common XY window
The following is the **pre-scale baseline** captured before the 2026-09-05 human-scale defaults
(the post-scale diagnostic table is below). Each card prints its own exact derived-margin Z window;
common XY window
is [-256,256) × [-256,256) voxels, step **4**, HeadroomCells=2, MaxCells=8,000,000):
| Archetype | walkable fraction | floor area fraction | median clearance | largest surface share |
@@ -921,6 +963,126 @@ cave design
(https://store.steampowered.com/news/posts/?appgroupname=Deep+Rock+Galactic&appids=548430&enddate=1729500950&feed=steam_community_announcements),
and Team Cherry map design (https://www.pcgamer.com/how-to-design-a-great-metroidvania-map/).
#### Final post-scale measurement (2026-09-05)
The new defaults are applied in `VoxelStrateTypes.h`; the production definition, season defaults, and
the owner-facing showcase use `StrateHeightInChunks=8` (64 m). The bounded test fixture remains at
four chunks so the broad regression suite stays finite; this is a test-budget distinction, not a
second production default. The final focused run is
`Saved/AutomationReports/WorldScaleShowcaseProductionFinal/index.json`, with 256 roll evaluations,
82 eligible single-region evaluations, 174 lateral-region evaluations skipped while that gate is off,
and a **0/8** complete hard-gate survivor set. It still wrote fresh diagnostic cards: 7 fine renders,
1 refusal, and 1 blank card.
The before/after values below are deliberately labelled as candidate measurements, not paired
experiments: the before rows are the selected pre-scale candidates from the baseline report, while
the after rows are the best measured post-scale candidates from the production-height search. Floor
area is the coarse 128 m × 128 m projected-footprint proxy (16,384 m² maximum); the ratio is the
candidate's reported roughness-to-feature ratio. A zero or unresolved player law is a result, not a
substituted value.
| Archetype | Before: fit / traversable / law | Before: floor m² / median clearance m / roughness ratio | After: fit / traversable / law | After: floor m² / median clearance m / roughness ratio |
|---|---|---|---|---|
| CrystalChamber | 0.011745 / 0.810378 / NotConnectedAtThisResolution | 16,382 / 17 / 1.813 floor, 1.796 ceiling | 0.002699 / 0.017288 / NotConnectedAtThisResolution | 16,384 / 35 / 0.847 |
| FlatPlain | 0.003938 / 0.053571 / NotConnectedAtThisResolution | 16,382 / 18 / 1.840 floor, 2.308 ceiling | 0.000000 / 0.000000 / unresolved | 16,343 / 43 / 1.092 |
| FloatingIslands | 0.000000 / 0.000000 / StartCellNotPlayerFit | 6,556 / 9 / 0.025 | 0.000000 / 0.000000 / StartCellNotPlayerFit | 4,577 / 29 / 0.283 |
| Maze | 0.010523 / 0.136646 / NotConnectedAtThisResolution | 4,759 / 2 / 0.938 | 0.016851 / 0.058757 / NotConnectedAtThisResolution | 6,115 / 3 / 0.423 |
| SurfaceWorld | 0.001027 / 0.029412 / NotConnectedAtThisResolution | 15,359 / 16 / 0.032 | 0.000627 / 0.008415 / NotConnectedAtThisResolution | 16,384 / 42 / 0.038 |
| TunnelNetwork | 0.004439 / 0.145833 / NotConnectedAtThisResolution | 3,414 / 5 / 1.436 | 0.001197 / 0.612245 / NotConnectedAtThisResolution | 6,153 / 9 / 0.383 |
| Underwater | 0.005251 / 0.160000 / NotConnectedAtThisResolution | 3,376 / 3 / 2.212 | 0.000000 / 0.000000 / OutOfWindow | 21 / 17 / 0.683 |
| VerticalShafts | 0.007458 / 0.233577 / NotConnectedAtThisResolution | 1,864 / 3 / 0.731 shaft, 1.302 connector | 0.009883 / 0.083045 / NotConnectedAtThisResolution | 2,718 / 4 / 0.622 |
The interpreted outcome is mixed. Nominal dimensions and median clearances grew where they should,
and TunnelNetwork's best measured candidate's traversable share rose substantially, but the global law
did not become connected and broad rooms can still have very low player-fit fractions. Maze improved
only modestly and remains disconnected. This is consistent with the radius sweep being a killed
single-cause hypothesis: width alone did not cure the route/mouth/resolution failures.
The direct default roughness arithmetic is stable and independent of which rolled candidate won:
| Archetype / feature | Arithmetic | Ratio |
|---|---|---:|
| TunnelNetwork, Underwater tunnel | reach `= 2 × 1.25 × 1.5 = 3.75 vox`; `3.75 / 6` minimum radius | 0.625 |
| Maze corridor | `3.75 / 8` | 0.469 |
| FlatPlain, CrystalChamber floor / ceiling columns | `7.50 / 12` and `11.25 / 12` using mean 12-voxel column radius | 0.625 / 0.938 |
| VerticalShafts shaft / connector | `3.75 / 8` and `3.75 / 6` | 0.469 / 0.625 |
| FloatingIslands | `5.625 / 24`; including 6-voxel SDF blend, `(5.625+6)/24` | 0.234 / 0.484 |
| SurfaceWorld relief | `3.75 / 80` terrain-relief scale | 0.047 |
The default derivations are not a global multiplier:
* Body and target anchors: `1 voxel = 0.25 m`; the capsule is `1.36 vox` radius and `7.04 vox`
tall. A 2.5 m no-duck corridor is 10 voxels; the 3-4 m fight target is 12-16 voxels. An 8-15 m
room is 32-60 voxels across, and a 20 m cathedral anchor is 80 voxels across. Epic's official
Unreal blockout guidance uses a player-sized reference and gives roughly 2-3 m halls and 3-4 m
heights as starting guidance; the [2010 ADA Standards](https://www.ada.gov/law-and-regs/design-standards/2010-stds/)
provide a 915 mm accessible clear-width lower bound, not a combat-space target.
* TunnelNetwork/Underwater: room radii `16..40 vox = 4..10 m`, so diameters are 8-20 m; the
origin radius `48 vox = 12 m` gives a 24 m, three-chunk hub. `RoomSpacing=128 vox = 32 m` leaves
`64-40=24 vox = 6 m` radial margin around the largest room. `TunnelMin/MaxRadius=6..8` gives
3-4 m bores. `RoomHeightRatio=0.85` makes the largest room's vertical diameter
`2×40×0.85×0.25=17 m`; `H=8×32=256 vox=64 m` leaves enough vertical budget for seals,
passages, and a high room. `MaxTunnelLength=360 vox=90 m` exceeds the worst jittered neighbour
distance with margin.
* FlatPlain/CrystalChamber: floor `0.25×256=64 vox=16 m`, ceiling `0.60×256=153.6 vox=38.4 m`,
leaving `22.4 m` before roughness. Columns are `8..16 vox` radii (4-8 m diameters) on 96-voxel
(24 m) cells. The two names intentionally retain the same slab generator and parameter block.
* Maze: `CellSize=64 vox=16 m`; `CorridorRadius=8 vox=2 m`, hence a 4 m bore; the corridor radius
is `8/32=0.25` of the half-cell, leaving a 6 m centre-to-wall radial margin before other geometry.
* SurfaceWorld: `ElevationRange=80 vox=20 m`, base ground `0.25×256=16 m`, warp `48 vox=12 m`,
sky cap `0.95×256=243.2 vox=60.8 m`; terrace/layer/beach/overhang values are separately
2 m / 2 m / 3 m / 4 m reach + 6 m height optional features.
* VerticalShafts: spacing `80 vox=20 m`, shaft radii `8..14 vox` (4-7 m diameters), connector
radius `6 vox` (3 m bore), ledge spacing `32 vox=8 m`, ledge depth `4 vox=1 m`.
* FloatingIslands: spacing `112 vox=28 m`, radii `24..48 vox` (12-24 m diameters); the largest
leaves `56-48=8 vox=2 m` centre-to-edge radial margin. Thickness is independent:
`48×0.60=28.8 vox=7.2 m` below the centre and `48×0.20=9.6 vox=2.4 m` above it.
* Shared passages and disturbances: mouth/mid radii `8/6 vox` (4/3 m bores), reach `32..96 vox`
(8-24 m), spine distance `64..192 vox` (16-48 m), wander/spiral `24 vox=6 m`, cascade ledge
`8 vox=2 m`; chasm spacing/radius `192/24 vox` (48 m / 6 m), bridge spacing/radius `128/8`
(32 m / 2 m), and ridge spacing/height/thickness `128/32/8` (32 m / 8 m / 2 m). Optional
transport fields are also audited: arch radius `6..12` (1.5-3 m), column `4..8` (1-2 m), pit
`8..16` plus depth `32` (2-4 m plus 8 m), chimney `4..8` plus height `32` (1-2 m plus 8 m),
dome `16..32` (4-8 m), pinch length `24` (6 m), overhang depth `8` (2 m), and the inactive
layer/rib depths `0.3/0.4` (7.5/10 cm). The exact generated row-by-row audit includes nominal
periods for every frequency field.
The 64 m height is deliberate rather than accidental: the GDD boss-space anchor is approximately
3-4 chunks (24-32 m), so the extra vertical envelope is needed to combine that room scale with
seals and vertical traversal. The NPS [Lehman Cave dimensions](https://www.nps.gov/grba/learn/nature/lehman-caves-dimensions.htm)
also support heterogeneous natural volumes rather than one fixed room ratio.
#### Equivalence, box safety, and cost after the default move
The full report `Saved/AutomationReports/WorldScaleFullFinal3/index.json` recorded 28 successes,
2 warning-only tests, 0 failures, and 0 not-run tests in 1,704.010 s. The warnings are known scope
findings: lateral regions remain gated at 9/16 cross-region law, and TunnelNetwork op-stack
sampling has only 240/6,000 samples outside its gate. Neither is a box-soundness violation.
All legacy/stack equivalence tests remained bit-identical to their shared reference paths, including
Maze, both slab archetypes, SurfaceWorld, TunnelNetwork stage A+B, VerticalShafts, and FloatingIslands.
`WorldRadiusVoxels` remained 0, lateral regions stayed gated, density sign remained correct, and every
box brute-force audit reported **0 violations**. Current representative box counts are: ParameterRoll
1,149 proved / 1,529,319 lattice voxels; StructureRoll 715 / 951,665; corpus-free arms 185 / 246,235,
260 / 346,060, and 299 / 397,969; TunnelNetwork production op-stack 7/40 proved (7 AllSolid),
Underwater 34/40, Maze 127/256 plus 250/256 novel lattice boxes, FlatPlain 27/60, CrystalChamber
39/60, tuned CrystalChamber 29/60, VerticalShafts 31/60, and FloatingIslands 15/60 AllAir.
Every listed count has zero brute-force violations.
The pristine pre-scale player-fit showcase did not execute the op-stack classification suite, so a
pure old-default tile-count delta does not exist. The nearest instrumented pre-final checkpoint had
the operator-stack 600-tile scan at **521 Mixed / 23 AllSolid / 56 AllAir**; the final world-scale
scan is **518 / 71 / 11**. Uniform proofs therefore did not collapse (79 → 82), although the
AllAir/AllSolid mix moved. The final non-stack scan is **522 / 22 / 56**. This is a real measured
classification result, but it must not be misrepresented as a pristine causal A/B run. The
production-height showcase itself cost about **1,709.7 s** and produced 7 fine renders, one refusal,
and one blank diagnostic card; its artifact is `Saved/VoxelForge/Showcase/index.html`.
The corrected owner-facing artifact is therefore at `Saved/VoxelForge/Showcase/index.html` (fresh
report: `Saved/AutomationReports/WorldScaleShowcaseProductionFinal/index.json`). It contains the new
metre audit and the rejected-but-useful diagnostic cards; the zero hard-gate survivor set is
intentionally visible rather than hidden by a fallback candidate.
### 3.5 Validation in three layers, and how "good" is ever judged
1. **Construct by design** so the law cannot break — build the connections FIRST and generate around
@@ -1092,8 +1254,10 @@ a passage and arrive in a sealed pocket.**
`SuggestLandingPoint(DesiredX, DesiredY, MaxLateralSnap) -> optional FVector`. Every source knows where
its own air is: `FRoomGraphSource` keeps the nearest room's vertical placement at the requested XY,
`MakeSlabVoidSource` answers in the void band, `FIslandBlobSource` finds a blob top, and the lattice/
shaft sources search their placement grid. Sparse sources may move laterally, but only within one
source spacing/cell; if that bounded search has no footing, falling back is the honest answer.
shaft sources search their placement grid. Sparse sources may move laterally, but only within a
bounded local source neighbourhood (one cell for Maze, two shaft spacings at the default
VerticalShafts density, one spacing for islands); if that search has no footing, falling back is the
honest answer.
Passage placement remains deterministic and cheap, and the slanted control-point chain is bounded too.
Then the measurement pass flood fill is the net: does the upper mouth's air component reach the lower
@@ -656,12 +656,12 @@ bool FVoxelForgeComposerCorpusFreeTest::RunTest(const FString& Parameters)
const double MeasurementSeconds = FPlatformTime::Seconds() - MeasurementStartSeconds;
// Audit the same declared relations against the 12 current authored corpus vectors. Project
// assets carry their own height; the eight C++ defaults use the fixture's 4-chunk height.
// assets carry their own height; the eight C++ defaults use the fixture's 8-chunk height.
int32 AuthoredConstraintViolations = 0;
FString AuthoredViolationList;
for (const FVoxelStrateCorpusEntry& Entry : Corpus.GetEntries())
{
float EntryHeight = 4.0f * (float)CHUNK_SIZE;
float EntryHeight = 8.0f * (float)CHUNK_SIZE;
if (!Entry.SourcePath.Contains(TEXT("/VoxelForge/ComposerDefaults/")))
{
UVoxelStrateDefinition* Definition = LoadObject<UVoxelStrateDefinition>(
@@ -49,7 +49,7 @@ bool FVoxelForgeComposerSeasonTest::RunTest(const FString& Parameters)
Settings.Corpus = &Corpus;
Settings.CandidateCount = 24;
Settings.SelectedStrateCount = 6;
Settings.StrateHeightInChunks = 4;
Settings.StrateHeightInChunks = 8;
Settings.InterStrateGapChunks = 0;
Settings.OriginSpineRadius = 14.0f;
Settings.WorldRadiusVoxels = 0.0f;
@@ -27,7 +27,7 @@ namespace
constexpr float ClearanceNormalizationVoxels = 64.0f;
constexpr int32 FinePreviewSampleStep = 1;
constexpr int32 FinePreviewRadiusVoxels = 64;
constexpr int32 FinePreviewMaxCells = 2000000;
constexpr int32 FinePreviewMaxCells = 4000000;
const ECaveGeneratorType GShowcaseArchetypes[] = {
ECaveGeneratorType::CrystalChamber,
@@ -125,23 +125,48 @@ namespace
int32 EvaluatedCandidates = 0;
TArray<FShowcaseSample> Survivors;
TOptional<FShowcaseSample> Best;
// Best coarse-measured candidate, retained even when a hard gate rejects it. This keeps
// the scale audit useful when the player-fit law correctly finds no survivor.
TOptional<FShowcaseSample> BestMeasured;
int32 MeasuredCandidates = 0;
FString LastFailure;
};
bool VF_IsBetterShowcaseSample(const FShowcaseSample& Sample,
const TOptional<FShowcaseSample>& Current)
{
return !Current.IsSet()
|| Sample.SelectionScore > Current->SelectionScore
|| (Sample.SelectionScore == Current->SelectionScore
&& (Sample.Candidate.Seed < Current->Candidate.Seed
|| (Sample.Candidate.Seed == Current->Candidate.Seed
&& Sample.Candidate.Index < Current->Candidate.Index)));
}
void VF_AddMetricSample(FShowcaseStats& Stats, const FShowcaseSample& Sample)
{
Stats.Survivors.Add(Sample);
if (!Stats.Best.IsSet()
|| Sample.SelectionScore > Stats.Best->SelectionScore
|| (Sample.SelectionScore == Stats.Best->SelectionScore
&& (Sample.Candidate.Seed < Stats.Best->Candidate.Seed
|| (Sample.Candidate.Seed == Stats.Best->Candidate.Seed
&& Sample.Candidate.Index < Stats.Best->Candidate.Index))))
if (VF_IsBetterShowcaseSample(Sample, Stats.Best))
{
Stats.Best = Sample;
}
}
void VF_AddMeasuredSample(FShowcaseStats& Stats, const FShowcaseSample& Sample)
{
// NumSampled is the direct evidence that the coarse grid was built. Keep this diagnostic
// path independent of the legacy gate's bool return and of any future metric-validity
// policy changes: a rejected candidate can still be the most useful scale evidence.
if (Sample.Candidate.bValid && Sample.Metrics.NumSampled > 0)
{
++Stats.MeasuredCandidates;
if (VF_IsBetterShowcaseSample(Sample, Stats.BestMeasured))
{
Stats.BestMeasured = Sample;
}
}
}
bool VF_BuildShowcaseStack(
const VoxelForgeTest::FTestWorld& World,
const FVoxelStrateComposerCandidate& Candidate,
@@ -569,6 +594,396 @@ namespace
return Report;
}
FString VF_FormatWorldScaleAudit()
{
constexpr float VoxelMetres = 0.25f;
constexpr float StrateHeightVoxels = 8.0f * static_cast<float>(CHUNK_SIZE);
FString Report = TEXT(
"WORLD SCALE AUDIT (all distances are authored in voxels; METRES = voxels * 0.25; "
"player radius=1.36 vox/0.34 m, height=7.04 vox/1.76 m; frequency rows show nominal "
"period=1/frequency)\n"
"archetype | name | voxels | METRES | body/level meaning\n");
auto Add = [&Report](const TCHAR* Archetype, const TCHAR* Name,
float Voxels, const TCHAR* Meaning)
{
Report += FString::Printf(
TEXT("%s | %s | %.2f | %.2f | %s\n"), Archetype, Name,
Voxels, Voxels * 0.25f, Meaning);
};
auto AddRange = [&Report](const TCHAR* Archetype, const TCHAR* Name,
float MinVoxels, float MaxVoxels,
const TCHAR* Meaning)
{
Report += FString::Printf(
TEXT("%s | %s | %.2f..%.2f | %.2f..%.2f | %s\n"), Archetype, Name,
MinVoxels, MaxVoxels, MinVoxels * 0.25f, MaxVoxels * 0.25f, Meaning);
};
const TCHAR* TunnelNames[] = { TEXT("TunnelNetwork"), TEXT("Underwater") };
for (const TCHAR* Archetype : TunnelNames)
{
Add(Archetype, TEXT("StrateHeight"), StrateHeightVoxels,
TEXT("8 chunks: a 64 m vertical envelope for tall rooms and passages"));
Add(Archetype, TEXT("BoundarySealThickness"), 4.0f,
TEXT("1 m solid geological boundary; not player space"));
Add(Archetype, TEXT("WormNetworkRange"), 64.0f,
TEXT("16 m side-passage reach around the room/tunnel skeleton"));
Add(Archetype, TEXT("WormFrequency (nominal period)"), 1.0f / 0.015f,
TEXT("16.7 m noise period: about two chunks for broad bends"));
Add(Archetype, TEXT("RoomSpacing"), 128.0f,
TEXT("32 m room-cell spacing; leaves a 6 m radial margin around a 20 m room"));
AddRange(Archetype, TEXT("RoomRadius"), 16.0f, 40.0f,
TEXT("8 m minimum room to 20 m large chamber diameter"));
Add(Archetype, TEXT("OriginRoomRadius"), 48.0f,
TEXT("24 m hub diameter: a three-chunk boss-space anchor"));
AddRange(Archetype, TEXT("TunnelRadius"), 6.0f, 8.0f,
TEXT("3-4 m bores: body height plus generous headroom and fight width"));
Add(Archetype, TEXT("MaxTunnelLength"), 360.0f,
TEXT("90 m: reaches the worst 32 m cell-jitter neighbour with margin"));
Add(Archetype, TEXT("TunnelWarpStrength"), 24.0f,
TEXT("6 m lateral bend, visibly organic but smaller than the 90 m reach"));
Add(Archetype, TEXT("SDFBlendRadius"), 4.0f,
TEXT("1 m junction rounding, below the 1.5 m minimum tunnel radius"));
Add(Archetype, TEXT("CaveWarpStrength"), 16.0f,
TEXT("4 m skeleton distortion"));
Add(Archetype, TEXT("SurfaceRoughness"), 2.0f,
TEXT("0.5 m wall texture; bounded reach is 0.94 m"));
Add(Archetype, TEXT("FloorReliefStrength"), 4.0f,
TEXT("1 m floor undulation, small relative to a 3 m bore"));
Add(Archetype, TEXT("FloorReliefFrequency (nominal period)"), 1.0f / 0.015f,
TEXT("16.7 m floor-undulation period: broad enough to walk as a slope"));
Add(Archetype, TEXT("RoughnessFrequency (nominal period)"), 1.0f / 0.1f,
TEXT("2.5 m wall-detail period: a few body lengths, not voxel-grain tunnel"));
Add(Archetype, TEXT("CaveWarpFrequency (nominal period)"), 1.0f / 0.015f,
TEXT("16.7 m skeleton-warp period: broad landform-scale bend"));
Add(Archetype, TEXT("DomainWarpFrequency (nominal period)"), 1.0f / 0.03f,
TEXT("8.3 m optional roughness-warp period"));
Add(Archetype, TEXT("CliffSampleDist (transport)"), 2.0f,
TEXT("0.5 m optional local slope sample distance"));
Add(Archetype, TEXT("FloorBias"), 4.0f,
TEXT("1 m floor stabilisation applied below room centre"));
Add(Archetype, TEXT("DomainWarpStrength"), 0.0f,
TEXT("0 m optional roughness-coordinate warp; disabled by default"));
Add(Archetype, TEXT("OverhangFrequency (nominal period)"), 1.0f / 0.06f,
TEXT("4.2 m optional shelf-shape period"));
Add(Archetype, TEXT("ScallopFrequency (nominal period)"), 1.0f / 0.1f,
TEXT("2.5 m optional erosion-bowl period"));
Add(Archetype, TEXT("TerraceStepHeight (transport)"), 0.0f,
TEXT("0 m optional step; disabled by default"));
Add(Archetype, TEXT("LayerLineSpacing (transport)"), 0.0f,
TEXT("0 m optional geological band period; disabled by default"));
Add(Archetype, TEXT("LayerLineDepth (transport)"), 0.3f,
TEXT("0.075 m optional groove depth; inactive while spacing is zero"));
Add(Archetype, TEXT("RibbingSpacing (transport)"), 0.0f,
TEXT("0 m optional rib period; disabled by default"));
Add(Archetype, TEXT("RibbingDepth (transport)"), 0.4f,
TEXT("0.1 m optional rib protrusion; inactive while spacing is zero"));
Add(Archetype, TEXT("OverhangDepth (transport)"), 8.0f,
TEXT("2 m optional shelf; terrain-op transport is reset when disabled"));
AddRange(Archetype, TEXT("ArchRadius (transport)"), 6.0f, 12.0f,
TEXT("1.5-3 m optional arch feature"));
AddRange(Archetype, TEXT("ColumnRadius (transport)"), 4.0f, 8.0f,
TEXT("1-2 m optional pillar radius"));
AddRange(Archetype, TEXT("PitRadius (transport)"), 8.0f, 16.0f,
TEXT("2-4 m optional vertical opening radius"));
Add(Archetype, TEXT("PitDepth (transport)"), 32.0f,
TEXT("8 m optional vertical drop"));
AddRange(Archetype, TEXT("ChimneyRadius (transport)"), 4.0f, 8.0f,
TEXT("1-2 m optional upward shaft radius"));
Add(Archetype, TEXT("ChimneyHeight (transport)"), 32.0f,
TEXT("8 m optional upward shaft height"));
AddRange(Archetype, TEXT("DomeRadius (transport)"), 16.0f, 32.0f,
TEXT("4-8 m optional ceiling dome radius"));
Add(Archetype, TEXT("PinchLength (transport)"), 24.0f,
TEXT("6 m optional bottleneck length; default operation is disabled"));
}
for (const TCHAR* Archetype : { TEXT("FlatPlain"), TEXT("CrystalChamber") })
{
Add(Archetype, TEXT("StrateHeight"), StrateHeightVoxels,
TEXT("8 chunks / 64 m: open vertical chamber envelope"));
Add(Archetype, TEXT("Floor height"), StrateHeightVoxels * 0.25f,
TEXT("16 m above the strate bottom"));
Add(Archetype, TEXT("Ceiling height"), StrateHeightVoxels * 0.60f,
TEXT("38.4 m above the bottom; 22.4 m clear span before roughness"));
Add(Archetype, TEXT("FloorRoughness"), 4.0f,
TEXT("1 m rolling floor displacement"));
Add(Archetype, TEXT("CeilingRoughness"), 6.0f,
TEXT("1.5 m downward formation reach"));
Add(Archetype, TEXT("FloorRoughnessFrequency (nominal period)"), 1.0f / 0.04f,
TEXT("6.25 m floor-detail period: broad rolling ground"));
Add(Archetype, TEXT("CeilingRoughnessFrequency (nominal period)"), 1.0f / 0.04f,
TEXT("6.25 m ceiling-formation period"));
AddRange(Archetype, TEXT("ColumnRadius"), 8.0f, 16.0f,
TEXT("4-8 m pillar diameters; large enough to read without erasing a fight lane"));
Add(Archetype, TEXT("ColumnSpacing"), 96.0f,
TEXT("24 m pillar-cell spacing"));
Add(Archetype, TEXT("BoundarySealThickness"), 4.0f,
TEXT("1 m solid boundary"));
}
Add(TEXT("Common layout"), TEXT("OriginSpineRadius"), 14.0f,
TEXT("3.5 m radius / 7 m diameter central descent spine"));
Add(TEXT("Common layout"), TEXT("InterStrateGap"), 0.0f,
TEXT("0 m bedrock gap by default; configured as whole 8 m chunks"));
Add(TEXT("Common layout"), TEXT("WorldRadiusVoxels"), 0.0f,
TEXT("0 m lateral bound: radial regions remain gated off"));
Add(TEXT("Maze"), TEXT("StrateHeight"), StrateHeightVoxels,
TEXT("8 chunks / 64 m: supports several 16 m maze levels"));
Add(TEXT("Maze"), TEXT("CellSize"), 64.0f,
TEXT("16 m lattice cells, matching ordinary fight-space scale"));
Add(TEXT("Maze"), TEXT("CorridorRadius"), 8.0f,
TEXT("2 m radius / 4 m circular bore: upper fight-space target"));
Add(TEXT("Maze"), TEXT("SurfaceRoughness"), 2.0f,
TEXT("0.5 m texture; bounded reach is 0.94 m, below the 2 m radius"));
Add(TEXT("Maze"), TEXT("BoundarySealThickness"), 4.0f,
TEXT("1 m solid boundary"));
Add(TEXT("SurfaceWorld"), TEXT("StrateHeight"), StrateHeightVoxels,
TEXT("8 chunks / 64 m: high sky-cap envelope"));
Add(TEXT("SurfaceWorld"), TEXT("ElevationRange"), 80.0f,
TEXT("20 m terrain relief: human-scale hills, not a miniature room"));
Add(TEXT("SurfaceWorld"), TEXT("Base ground height"), StrateHeightVoxels * 0.25f,
TEXT("16 m mean ground elevation above the strate bottom"));
Add(TEXT("SurfaceWorld"), TEXT("HeightWarpStrength"), 48.0f,
TEXT("12 m horizontal landform distortion"));
Add(TEXT("SurfaceWorld"), TEXT("SurfaceRoughness"), 2.0f,
TEXT("0.5 m ground detail; bounded reach is 0.94 m"));
Add(TEXT("SurfaceWorld"), TEXT("ContinentFrequency (nominal period)"), 1.0f / 0.006f,
TEXT("41.7 m broad landmass period"));
Add(TEXT("SurfaceWorld"), TEXT("MountainFrequency (nominal period)"), 1.0f / 0.012f,
TEXT("20.8 m ridge period"));
Add(TEXT("SurfaceWorld"), TEXT("DetailFrequency (nominal period)"), 1.0f / 0.04f,
TEXT("6.25 m fine terrain period"));
Add(TEXT("SurfaceWorld"), TEXT("TerraceHeight"), 8.0f,
TEXT("2 m optional step; operation remains disabled by strength 0"));
Add(TEXT("SurfaceWorld"), TEXT("LayerLineSpacing"), 8.0f,
TEXT("2 m optional geological band spacing"));
Add(TEXT("SurfaceWorld"), TEXT("LayerLineDepth"), 0.0f,
TEXT("0 m default band displacement: optional operation disabled"));
Add(TEXT("SurfaceWorld"), TEXT("CliffSampleDist"), 2.0f,
TEXT("0.5 m slope sample scale for optional cliff operation"));
Add(TEXT("SurfaceWorld"), TEXT("OverhangReach"), 16.0f,
TEXT("4 m optional shelf reach"));
Add(TEXT("SurfaceWorld"), TEXT("OverhangHeight"), 24.0f,
TEXT("6 m optional shelf zone"));
Add(TEXT("SurfaceWorld"), TEXT("BeachWidth"), 12.0f,
TEXT("3 m shore band"));
Add(TEXT("SurfaceWorld"), TEXT("Sky cap height"), StrateHeightVoxels * 0.95f,
TEXT("60.8 m above the strate bottom; leaves a high open sky"));
Add(TEXT("SurfaceWorld"), TEXT("CeilingRoughness"), 6.0f,
TEXT("1.5 m optional downward cap detail"));
Add(TEXT("SurfaceWorld"), TEXT("CeilingUndulation"), 0.0f,
TEXT("0 m broad cap swell; disabled by default"));
Add(TEXT("SurfaceWorld"), TEXT("CeilingRidgeStrength"), 0.0f,
TEXT("0 m hanging ridge height; disabled by default"));
Add(TEXT("SurfaceWorld"), TEXT("CeilingWarpStrength"), 0.0f,
TEXT("0 m cap-coordinate warp; disabled by default"));
Add(TEXT("SurfaceWorld"), TEXT("BoundarySealThickness"), 4.0f,
TEXT("1 m solid boundary"));
Add(TEXT("SurfaceWorld"), TEXT("HeightWarpFrequency (nominal period)"), 1.0f / 0.008f,
TEXT("31.25 m landform-warp period"));
Add(TEXT("SurfaceWorld"), TEXT("ReliefFrequency (nominal period)"), 1.0f / 0.0015f,
TEXT("166.7 m macro-region period"));
Add(TEXT("SurfaceWorld"), TEXT("OverhangFrequency (nominal period)"), 1.0f / 0.02f,
TEXT("12.5 m optional shelf-shape period"));
Add(TEXT("SurfaceWorld"), TEXT("CeilingRoughnessFrequency (nominal period)"), 1.0f / 0.04f,
TEXT("6.25 m optional cap-detail period"));
Add(TEXT("SurfaceWorld"), TEXT("CeilingUndulationFrequency (nominal period)"), 1.0f / 0.004f,
TEXT("62.5 m optional cap-swell period"));
Add(TEXT("SurfaceWorld"), TEXT("CeilingRidgeFrequency (nominal period)"), 1.0f / 0.02f,
TEXT("12.5 m optional hanging-ridge period"));
Add(TEXT("SurfaceWorld"), TEXT("CeilingWarpFrequency (nominal period)"), 1.0f / 0.01f,
TEXT("25 m optional cap-warp period"));
Add(TEXT("VerticalShafts"), TEXT("StrateHeight"), StrateHeightVoxels,
TEXT("8 chunks / 64 m: a full vertical traversal envelope"));
Add(TEXT("VerticalShafts"), TEXT("ShaftSpacing"), 80.0f,
TEXT("20 m shaft-cell spacing"));
AddRange(TEXT("VerticalShafts"), TEXT("ShaftRadius"), 8.0f, 14.0f,
TEXT("4-7 m shaft diameters"));
Add(TEXT("VerticalShafts"), TEXT("ConnectorRadius"), 6.0f,
TEXT("1.5 m radius / 3 m horizontal connector bore"));
Add(TEXT("VerticalShafts"), TEXT("LedgeSpacing"), 32.0f,
TEXT("8 m vertical landing interval"));
Add(TEXT("VerticalShafts"), TEXT("LedgeDepth"), 4.0f,
TEXT("1 m landing intrusion"));
Add(TEXT("VerticalShafts"), TEXT("SurfaceRoughness"), 2.0f,
TEXT("0.5 m wall texture; bounded reach is 0.94 m"));
Add(TEXT("VerticalShafts"), TEXT("BoundarySealThickness"), 4.0f,
TEXT("1 m solid boundary"));
Add(TEXT("FloatingIslands"), TEXT("StrateHeight"), StrateHeightVoxels,
TEXT("8 chunks / 64 m: vertical sky volume"));
Add(TEXT("FloatingIslands"), TEXT("IslandSpacing"), 112.0f,
TEXT("28 m island-cell spacing; max 24 m diameter leaves a 2 m centre-to-edge gap"));
AddRange(TEXT("FloatingIslands"), TEXT("IslandRadius"), 24.0f, 48.0f,
TEXT("12-24 m landmass diameters: room to cathedral scale"));
Add(TEXT("FloatingIslands"), TEXT("Underside half-depth"), 48.0f * 0.60f,
TEXT("7.2 m below the centre for the largest island; top half is 2.4 m"));
Add(TEXT("FloatingIslands"), TEXT("SurfaceRoughness"), 3.0f,
TEXT("0.75 m island texture; bounded reach is 1.41 m"));
Add(TEXT("FloatingIslands"), TEXT("SDFBlendRadius"), 6.0f,
TEXT("1.5 m island edge blend"));
Add(TEXT("FloatingIslands"), TEXT("BoundarySealThickness"), 4.0f,
TEXT("1 m solid boundary"));
Add(TEXT("Common passage"), TEXT("MouthRadius"), 8.0f,
TEXT("2 m radius / 4 m bore at each strate mouth"));
Add(TEXT("Common passage"), TEXT("MidRadius"), 6.0f,
TEXT("1.5 m radius / 3 m minimum bore between mouths"));
AddRange(TEXT("Common passage"), TEXT("Reach"), 32.0f, 96.0f,
TEXT("8-24 m reach into each strate"));
AddRange(TEXT("Common passage"), TEXT("Distance from spine"), 64.0f, 192.0f,
TEXT("16-48 m placement range"));
Add(TEXT("Common passage"), TEXT("Wander"), 24.0f,
TEXT("6 m sideways meander"));
Add(TEXT("Common passage"), TEXT("VerticalWobble"), 0.0f,
TEXT("0 m vertical wobble by default; descent remains controlled"));
Add(TEXT("Common passage"), TEXT("SpiralRadius"), 24.0f,
TEXT("6 m corkscrew radius"));
Add(TEXT("Common passage"), TEXT("CascadeLedge"), 8.0f,
TEXT("2 m ledge run per cascade step"));
Add(TEXT("Common disturbance"), TEXT("ChasmSpacing"), 192.0f,
TEXT("48 m chasm-cell spacing"));
Add(TEXT("Common disturbance"), TEXT("ChasmRadius"), 24.0f,
TEXT("6 m radius / 12 m opening when enabled"));
Add(TEXT("Common disturbance"), TEXT("BridgeSpacing"), 128.0f,
TEXT("32 m bridge-cell spacing"));
Add(TEXT("Common disturbance"), TEXT("BridgeRadius"), 8.0f,
TEXT("2 m radius / 4 m bridge width"));
Add(TEXT("Common disturbance"), TEXT("RidgeSpacing"), 128.0f,
TEXT("32 m ridge-cell spacing"));
Add(TEXT("Common disturbance"), TEXT("RidgeHeight"), 32.0f,
TEXT("8 m optional blade height"));
Add(TEXT("Common disturbance"), TEXT("RidgeThickness"), 8.0f,
TEXT("2 m optional blade half-width"));
Add(TEXT("Common disturbance"), TEXT("BoundarySealThickness"), 4.0f,
TEXT("1 m solid boundary shared with the active archetype"));
Report += FString::Printf(
TEXT("Audit anchors: 1 voxel=%.2f m; 1 chunk=8.00 m; 2.5 m walk corridor=10 vox; "
"3-4 m fight corridor=12-16 vox; 8-15 m room=32-60 vox; 20 m cathedral=80 vox.\n"),
VoxelMetres);
return Report;
}
FString VF_FormatDefaultRoughnessRatios()
{
const float TunnelReach = VF_RoughnessReach(2.0f);
const float SlabFloorReach = VF_RoughnessReach(4.0f);
const float SlabCeilingReach = VF_RoughnessReach(6.0f);
const float MazeReach = VF_RoughnessReach(2.0f);
const float ShaftReach = VF_RoughnessReach(2.0f);
const float IslandReach = VF_RoughnessReach(3.0f);
return FString::Printf(
TEXT("Default roughness-to-feature ratios (reach = roughness * 1.25 * 1.5; "
"all values are dimensionless): TunnelNetwork=%.3f reach/%.1f-voxel "
"minimum tunnel radius (room mean %.3f); Underwater=same; FlatPlain="
"%.3f floor reach/mean column radius and %.3f ceiling reach/mean column radius; "
"CrystalChamber=same slab engine; Maze=%.3f reach/%.1f-voxel corridor radius; "
"SurfaceWorld=%.3f reach/80-voxel terrain relief; VerticalShafts=%.3f shaft-min "
"and %.3f connector; FloatingIslands=%.3f reach/min island radius "
"(%.3f including 6-voxel blend).\n"),
TunnelReach / 6.0f, 6.0f, TunnelReach / 28.0f,
SlabFloorReach / 12.0f, SlabCeilingReach / 12.0f,
MazeReach / 8.0f, 8.0f, TunnelReach / 80.0f,
ShaftReach / 8.0f, ShaftReach / 6.0f,
IslandReach / 24.0f, (IslandReach + 6.0f) / 24.0f);
}
float VF_ShowcaseRoughnessFeatureRatio(
const FVoxelStrateComposerCandidate& Candidate)
{
switch (Candidate.Archetype)
{
case ECaveGeneratorType::TunnelNetwork:
case ECaveGeneratorType::Underwater:
return VF_RoughnessReach(
Candidate.ArchetypeParams.TunnelNetworkParams.SurfaceRoughness)
/ FMath::Max(
Candidate.ArchetypeParams.TunnelNetworkParams.TunnelMinRadius,
KINDA_SMALL_NUMBER);
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber:
{
const FSlabGenerationParams& P = Candidate.ArchetypeParams.SlabParams;
const float ColumnRadius = 0.5f * (P.ColumnMinRadius + P.ColumnMaxRadius);
return FMath::Max(
VF_RoughnessReach(P.FloorRoughness),
VF_RoughnessReach(P.CeilingRoughness))
/ FMath::Max(ColumnRadius, KINDA_SMALL_NUMBER);
}
case ECaveGeneratorType::Maze:
return VF_RoughnessReach(Candidate.ArchetypeParams.MazeParams.SurfaceRoughness)
/ FMath::Max(Candidate.ArchetypeParams.MazeParams.CorridorRadius,
KINDA_SMALL_NUMBER);
case ECaveGeneratorType::SurfaceWorld:
return VF_RoughnessReach(Candidate.ArchetypeParams.SurfaceParams.SurfaceRoughness)
/ FMath::Max(Candidate.ArchetypeParams.SurfaceParams.ElevationRange,
KINDA_SMALL_NUMBER);
case ECaveGeneratorType::VerticalShafts:
return VF_RoughnessReach(
Candidate.ArchetypeParams.VerticalShaftParams.SurfaceRoughness)
/ FMath::Max(
Candidate.ArchetypeParams.VerticalShaftParams.ShaftMinRadius,
KINDA_SMALL_NUMBER);
case ECaveGeneratorType::FloatingIslands:
{
const FFloatingIslandParams& P = Candidate.ArchetypeParams.FloatingIslandParams;
return (VF_RoughnessReach(P.SurfaceRoughness) + P.SDFBlendRadius)
/ FMath::Max(P.IslandMinRadius, KINDA_SMALL_NUMBER);
}
default:
return 0.0f;
}
}
FString VF_FormatPlayerFitSummary(
const TArray<FShowcaseSample>& Samples,
const TCHAR* Label)
{
FString Report = FString::Printf(
TEXT("%s (walkable floor area is the coarse 128 m x 128 m footprint proxy)\n"
"archetype | PlayerFitFraction | TraversableComponentShare | "
"player-fit arrival->departure | walkable floor area m2 | "
"median vertical clearance m | roughness-to-feature ratio\n"), Label);
for (const ECaveGeneratorType Archetype : GShowcaseArchetypes)
{
const FShowcaseSample* Found = Samples.FindByPredicate(
[Archetype](const FShowcaseSample& Sample)
{
return Sample.Candidate.Archetype == Archetype;
});
if (Found == nullptr)
{
Report += FString::Printf(
TEXT("%s | no measured sample | no measured sample | no measured sample | "
"n/a | n/a | n/a\n"),
VF_GetStrateArchetypeName(Archetype));
continue;
}
const FVoxelStrateMetrics& M = Found->FineMetrics;
const FString Law = M.bPlayerFitResolved
? VF_ShowcaseConnectivityName(Found->PlayerFitLaw.Result)
: TEXT("unresolved");
const float FloorAreaM2 = Found->Metrics.WalkableFloorAreaFraction
* 128.0f * 128.0f;
Report += FString::Printf(
TEXT("%s | %.6f | %.6f | %s | %.1f (%.6f footprint) | %.2f | %.3f\n"),
VF_GetStrateArchetypeName(Archetype),
M.bPlayerFitResolved ? M.PlayerFitFraction : 0.0f,
M.bPlayerFitResolved ? M.TraversableComponentShare : 0.0f,
*Law, FloorAreaM2,
Found->Metrics.WalkableFloorAreaFraction,
Found->Metrics.MedianVerticalClearance * 0.25f,
VF_ShowcaseRoughnessFeatureRatio(Found->Candidate));
}
return Report;
}
bool VF_RunMazeRoughnessExperiment(
const VoxelForgeTest::FTestWorld& World,
const FShowcaseSample& MazeSample,
@@ -636,6 +1051,80 @@ namespace
return true;
}
bool VF_RunMazeCorridorRadiusSweep(
const VoxelForgeTest::FTestWorld& World,
const FShowcaseSample& MazeSample,
const FVoxelStrateMeasureSettings& BaseSettings,
const FVoxelStrateFinePreviewSettings& FinePreviewSettings,
FString& OutReport)
{
if (MazeSample.Candidate.Archetype != ECaveGeneratorType::Maze)
{
OutReport = TEXT("Part A Maze corridor-radius sweep unavailable: previous Maze roll was not found.");
return false;
}
FVector ArrivalPoint = FVector::ZeroVector;
FVector DeparturePoint = FVector::ZeroVector;
if (!VF_GetShowcaseMouthPair(
World, MazeSample.TargetStrateIndex, ArrivalPoint, DeparturePoint))
{
OutReport = TEXT("Part A Maze corridor-radius sweep unavailable: mouth pair was not found.");
return false;
}
int32 TopVoxelZ = 0;
int32 BottomVoxelZ = 0;
if (!World.GetSlotVoxelZRange(MazeSample.TargetStrateIndex, TopVoxelZ, BottomVoxelZ))
{
OutReport = TEXT("Part A Maze corridor-radius sweep unavailable: Z range was not found.");
return false;
}
const FVoxelStrateMeasureSettings FineSettings = VF_MakeFinePlayerFitSettings(
BaseSettings, FinePreviewSettings, ArrivalPoint, DeparturePoint);
const float RadiusSweep[] = { 4.0f, 6.0f, 8.0f, 10.0f, 12.0f };
OutReport = TEXT(
"Part A Maze corridor-radius sweep (same seed/index, fitted step-1 ROI; "
"radius voxels -> bore metres):\n");
for (const float CorridorRadius : RadiusSweep)
{
FVoxelStrateComposerCandidate Candidate = MazeSample.Candidate;
Candidate.ArchetypeParams.MazeParams.CorridorRadius = CorridorRadius;
FVoxelOpStack Stack;
FVoxelOpContext Context;
FString BuildError;
if (!VF_BuildShowcaseStack(
World, Candidate, TopVoxelZ, BottomVoxelZ, Stack, Context, BuildError))
{
OutReport += FString::Printf(
TEXT(" radius=%.0f bore=%.2f m: build failed (%s)\n"),
CorridorRadius, CorridorRadius * 2.0f * VOXEL_SIZE / 100.0f, *BuildError);
continue;
}
Stack.PrepareChunk(Context);
FShowcaseStackSampler Sampler(Stack);
FVoxelStrateMetrics Metrics;
const FVoxelConnectivityDiagnostics Law =
VF_DiagnosePlayerFitConnectivityWithSampler(
Sampler, BottomVoxelZ, TopVoxelZ + 1, Context.EdgeSealThickness,
ArrivalPoint, DeparturePoint, FineSettings, &Metrics);
OutReport += FString::Printf(
TEXT(" radius=%.0f (%.2f m) bore=%.2f m: fit=%.6f traversable=%.6f "
"fit_cells=%lld min_clearance=%.2f voxels (%.2f m) law=%s\n"),
CorridorRadius, CorridorRadius * VOXEL_SIZE / 100.0f,
CorridorRadius * 2.0f * VOXEL_SIZE / 100.0f,
Metrics.PlayerFitFraction, Metrics.TraversableComponentShare,
static_cast<long long>(Metrics.NumPlayerFitCells),
Metrics.MinimumPlayerClearanceVoxels,
Metrics.MinimumPlayerClearanceVoxels * VOXEL_SIZE / 100.0f,
VF_ShowcaseConnectivityName(Law.Result));
}
return true;
}
struct FIslandVerticalAudit
{
int32 NumIslands = 0;
@@ -1073,7 +1562,9 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
}
FTestWorld World;
World.Build(BaseComposerSeed, 2, true);
// The shared fixture defaults to a bounded 4-chunk volume for fast density tests. The
// owner-facing scale audit must exercise the production/default 8-chunk envelope.
World.Build(BaseComposerSeed, 2, true, 8);
if (!World.IsValid())
{
AddError(World.WhyInvalid());
@@ -1188,6 +1679,7 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
{
Stats[ArchetypeIndex].LastFailure = MoveTemp(FailureReason);
}
VF_AddMeasuredSample(Stats[ArchetypeIndex], Sample);
}
}
@@ -1248,6 +1740,7 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
{
Stats[ArchetypeIndex].LastFailure = MoveTemp(FailureReason);
}
VF_AddMeasuredSample(Stats[ArchetypeIndex], Sample);
}
}
@@ -1270,13 +1763,76 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
++ArchetypeIndex)
{
AddInfo(FString::Printf(
TEXT("Showcase search detail %s: eligible=%d hard_gate_survivors=%d last_failure=%s"),
TEXT("Showcase search detail %s: eligible=%d measured=%d hard_gate_survivors=%d last_failure=%s"),
VF_GetStrateArchetypeName(GShowcaseArchetypes[ArchetypeIndex]),
Stats[ArchetypeIndex].EvaluatedCandidates,
Stats[ArchetypeIndex].MeasuredCandidates,
Stats[ArchetypeIndex].Survivors.Num(),
Stats[ArchetypeIndex].LastFailure.IsEmpty()
Stats[ArchetypeIndex].LastFailure.IsEmpty()
? TEXT("none") : *Stats[ArchetypeIndex].LastFailure));
}
TArray<FShowcaseSample> FreshSurvivorSamples;
for (const FShowcaseStats& ArchetypeStats : Stats)
{
if (ArchetypeStats.Best.IsSet())
{
FreshSurvivorSamples.Add(ArchetypeStats.Best.GetValue());
}
}
AddInfo(VF_FormatPlayerFitSummary(
FreshSurvivorSamples, TEXT("AFTER defaults: fresh hard-gate survivors")));
TArray<FShowcaseSample> FreshDiagnosticSamples;
FreshDiagnosticSamples.Reserve(UE_ARRAY_COUNT(GShowcaseArchetypes));
for (const FShowcaseStats& ArchetypeStats : Stats)
{
if (!ArchetypeStats.BestMeasured.IsSet())
{
continue;
}
const FShowcaseSample Measured = ArchetypeStats.BestMeasured.GetValue();
const FVoxelStrateComposerCandidate DiagnosticCandidate = Measured.Candidate;
FShowcaseSample Diagnostic;
FString DiagnosticFailure;
// Re-run the selected coarse measurement with the player-fit pass enabled even when a
// legacy gate fails. This is diagnostic only: it never enters Survivors or changes card
// selection, but it gives every archetype an honest before/after scale row.
VF_EvaluateShowcaseCandidate(
World, DiagnosticCandidate, Measured.TargetStrateIndex, MeasureSettings,
Diagnostic, &DiagnosticFailure, true);
if (Diagnostic.Candidate.bValid && Diagnostic.Metrics.NumSampled > 0)
{
FreshDiagnosticSamples.Add(MoveTemp(Diagnostic));
}
}
AddInfo(VF_FormatPlayerFitSummary(
FreshDiagnosticSamples,
TEXT("AFTER defaults: best measured candidate per archetype (diagnostic; hard gates separate)")));
TArray<FShowcaseSample> PreviewSelections;
PreviewSelections.Reserve(UE_ARRAY_COUNT(GShowcaseArchetypes));
bool bHaveAllPreviewSelections = true;
for (int32 ArchetypeIndex = 0;
ArchetypeIndex < UE_ARRAY_COUNT(GShowcaseArchetypes);
++ArchetypeIndex)
{
if (Stats[ArchetypeIndex].Best.IsSet())
{
PreviewSelections.Add(Stats[ArchetypeIndex].Best.GetValue());
continue;
}
const FShowcaseSample* Diagnostic = FreshDiagnosticSamples.FindByPredicate(
[Archetype = GShowcaseArchetypes[ArchetypeIndex]](const FShowcaseSample& Sample)
{
return Sample.Candidate.Archetype == Archetype;
});
if (Diagnostic == nullptr)
{
bHaveAllPreviewSelections = false;
break;
}
PreviewSelections.Add(*Diagnostic);
}
struct FPreviousShowcaseSelection
{
@@ -1358,6 +1914,10 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
AddInfo(FString::Printf(
TEXT("Previous eight showcase candidates rejected by the new player-fit gate: %d/8."),
NumPreviousShowcaseRejected));
AddInfo(VF_FormatPlayerFitSummary(
PreviousSamples, TEXT("AFTER defaults: historical candidate re-measurement")));
AddInfo(VF_FormatWorldScaleAudit());
AddInfo(VF_FormatDefaultRoughnessRatios());
AddInfo(VF_FormatRoughnessRadiusReport(PreviousSamples));
FString MazeRoughnessReport;
if (const FShowcaseSample* MazeSample = PreviousSamples.FindByPredicate(
@@ -1368,6 +1928,10 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
{
VF_RunMazeRoughnessExperiment(
World, *MazeSample, MeasureSettings, FinePreviewSettings, MazeRoughnessReport);
FString MazeRadiusReport;
VF_RunMazeCorridorRadiusSweep(
World, *MazeSample, MeasureSettings, FinePreviewSettings, MazeRadiusReport);
AddInfo(MazeRadiusReport);
}
else
{
@@ -1392,8 +1956,13 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
}
if (!bHaveAllArchetypes)
{
AddInfo(TEXT("Showcase cards are not written because the new hard gate found no complete "
"eight-archetype survivor set; the diagnostic report above is the result."));
AddInfo(TEXT("No complete eight-archetype hard-gate survivor set was found; the fresh "
"preview cards below are rejected diagnostic candidates for scale review."));
}
if (!bHaveAllPreviewSelections)
{
AddInfo(TEXT("Fresh diagnostic previews are incomplete because at least one archetype had "
"no measurable candidate."));
return true;
}
@@ -1429,8 +1998,9 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
ArchetypeIndex < UE_ARRAY_COUNT(GShowcaseArchetypes);
++ArchetypeIndex)
{
const FShowcaseSample& Selected = Stats[ArchetypeIndex].Best.GetValue();
const FShowcaseSample& Selected = PreviewSelections[ArchetypeIndex];
const FVoxelStrateComposerCandidate& Candidate = Selected.Candidate;
const bool bDiagnosticCard = !bHaveAllArchetypes;
// The output filename namespace is local to this page. Keep it distinct even if a future
// search selects equal candidate indices from two different composer seeds; the PIE fields
@@ -1484,7 +2054,11 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
ShowcaseDirectory, RenderIndex,
VF_FormatNativeParameterRecipe(Candidate),
SampleGrid, Metrics, MeasureSettings.HeadroomCells,
Selected.SelectionScore, Selected.LawText, false, TEXT(""),
Selected.SelectionScore, Selected.LawText, bDiagnosticCard,
bDiagnosticCard
? TEXT("Diagnostic only: no complete hard-gate survivor set; this card is retained "
"to inspect the generated scale and exact player-fit law.")
: TEXT(""),
PreviewCandidate, PreviewError);
TestTrue(FString::Printf(TEXT("selected %s coarse preview writes"),
VF_GetStrateArchetypeName(Candidate.Archetype)),
@@ -1516,6 +2090,11 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
const FVoxelStrateMetrics FineMetrics = VF_MeasureStrateWithSampler(
Sampler, Selected.BottomVoxelZ, Selected.TopVoxelZ + 1, Context.EdgeSealThickness,
FineMeasureSettings, &FineGrid);
FVoxelStrateMetrics FinePlayerMetrics;
VF_DiagnosePlayerFitConnectivityWithSampler(
Sampler, Selected.BottomVoxelZ, Selected.TopVoxelZ + 1,
Context.EdgeSealThickness, FineArrivalPoint, FineDeparturePoint,
FineMeasureSettings, &FinePlayerMetrics);
const auto IsInsideFineXY = [&FineGrid](const FVector& Point)
{
return Point.X >= FineGrid.MinX && Point.X < FineGrid.MaxX
@@ -1531,7 +2110,7 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
!FineMetrics.bValid || FineGrid.CellCount <= FinePreviewMaxCells);
TestTrue(FString::Printf(TEXT("selected %s fine player-fit metrics rerun identically"),
VF_GetStrateArchetypeName(Candidate.Archetype)),
VF_ShowcaseMetricsEqual(Selected.FineMetrics, FineMetrics));
VF_ShowcaseMetricsEqual(Selected.FineMetrics, FinePlayerMetrics));
const bool bFineWritten = VF_WriteStratePreviewFineCandidate(
ShowcaseDirectory, RenderIndex, FineGrid,
FineMeasureSettings.HeadroomCells,
@@ -1546,17 +2125,17 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
VF_GetStrateArchetypeName(Candidate.Archetype), *PreviewError));
return false;
}
PreviewCandidate.bPlayerFitResolved = FineMetrics.bPlayerFitResolved;
PreviewCandidate.PlayerFitRefusalReason = FineMetrics.PlayerFitRefusalReason;
PreviewCandidate.NumPlayerFitCells = FineMetrics.NumPlayerFitCells;
PreviewCandidate.PlayerFitFraction = FineMetrics.PlayerFitFraction;
PreviewCandidate.NumTraversableComponents = FineMetrics.NumTraversableComponents;
PreviewCandidate.bPlayerFitResolved = FinePlayerMetrics.bPlayerFitResolved;
PreviewCandidate.PlayerFitRefusalReason = FinePlayerMetrics.PlayerFitRefusalReason;
PreviewCandidate.NumPlayerFitCells = FinePlayerMetrics.NumPlayerFitCells;
PreviewCandidate.PlayerFitFraction = FinePlayerMetrics.PlayerFitFraction;
PreviewCandidate.NumTraversableComponents = FinePlayerMetrics.NumTraversableComponents;
PreviewCandidate.LargestTraversableComponentCells =
FineMetrics.LargestTraversableComponentCells;
PreviewCandidate.TraversableComponentShare = FineMetrics.TraversableComponentShare;
FinePlayerMetrics.LargestTraversableComponentCells;
PreviewCandidate.TraversableComponentShare = FinePlayerMetrics.TraversableComponentShare;
PreviewCandidate.MinimumPlayerClearanceVoxels =
FineMetrics.MinimumPlayerClearanceVoxels;
if (FineMetrics.bValid)
FinePlayerMetrics.MinimumPlayerClearanceVoxels;
if (FinePlayerMetrics.bValid)
{
TestTrue(FString::Printf(TEXT("selected %s fine preview has filled and contour views"),
VF_GetStrateArchetypeName(Candidate.Archetype)),
@@ -1581,17 +2160,28 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
PreviewCandidate.ComposerSeed = Candidate.Seed;
PreviewCandidate.ComposerTargetStrateIndex = Selected.TargetStrateIndex;
PreviewCandidate.bComposerRollStructure = false;
PreviewCandidate.SelectionReason = FString::Printf(
TEXT("Hard gates passed: non-vacuous, largest air component >= %.2f, and "
"player-fit arrival → departure connectivity (fine step=1; "
"fit cells=%lld). Selected by legacy score "
"floor_area + 0.25*clamp(clearance/%d,0,1) + 0.10*largest_surface = %.6f; "
"floor area is the primary walking signal. Applied to existing target slot %d."),
LargestComponentSurvivalThreshold,
static_cast<long long>(FineMetrics.NumPlayerFitCells),
static_cast<int32>(ClearanceNormalizationVoxels),
Selected.SelectionScore,
Selected.TargetStrateIndex);
PreviewCandidate.SelectionReason = bDiagnosticCard
? FString::Printf(
TEXT("Diagnostic only: the complete hard-gate set was not found. Legacy law=%s; "
"player-fit law=%s; selected by the same score "
"floor_area + 0.25*clamp(clearance/%d,0,1) + 0.10*largest_surface = %.6f; "
"applied to existing target slot %d."),
VF_ShowcaseConnectivityName(Selected.LegacyLaw.Result),
VF_ShowcaseConnectivityName(Selected.PlayerFitLaw.Result),
static_cast<int32>(ClearanceNormalizationVoxels),
Selected.SelectionScore,
Selected.TargetStrateIndex)
: FString::Printf(
TEXT("Hard gates passed: non-vacuous, largest air component >= %.2f, and "
"player-fit arrival → departure connectivity (fine step=1; "
"fit cells=%lld). Selected by legacy score "
"floor_area + 0.25*clamp(clearance/%d,0,1) + 0.10*largest_surface = %.6f; "
"floor area is the primary walking signal. Applied to existing target slot %d."),
LargestComponentSurvivalThreshold,
static_cast<long long>(FineMetrics.NumPlayerFitCells),
static_cast<int32>(ClearanceNormalizationVoxels),
Selected.SelectionScore,
Selected.TargetStrateIndex);
PreviewCandidates.Add(MoveTemp(PreviewCandidate));
TestTrue(FString::Printf(TEXT("selected %s retains a valid exact coarse window"),
@@ -1603,7 +2193,7 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
ArchetypeIndex < UE_ARRAY_COUNT(GShowcaseArchetypes);
++ArchetypeIndex)
{
const FShowcaseSample& Selected = Stats[ArchetypeIndex].Best.GetValue();
const FShowcaseSample& Selected = PreviewSelections[ArchetypeIndex];
Summaries[ArchetypeIndex].Window = PreviewCandidates[ArchetypeIndex].Window;
Summaries[ArchetypeIndex].WindowSummary = VF_FormatShowcaseSummaryWindow(
Stats[ArchetypeIndex]);
@@ -104,7 +104,7 @@ namespace
// retourne ce garde-fou : il EXIGE maintenant des colonnes.
/**
* ⚠️ DENSIFIÉ après le premier run vert. Aux défauts (`RoomSpacing = 80`, `RoomDensity = 0.35`)
* ⚠️ DENSIFIÉ après le premier run vert. Aux anciens défauts (`RoomSpacing = 80`, `RoomDensity = 0.35`)
* le premier passage a rendu **65 échantillons en grotte sur 6000, soit 1,1 %** : bit-identique,
* oui, mais en comparant surtout du roc plein à du roc plein, là où le carve et les vers ne
* s'exécutent même pas. Le compteur avait été écrit exactement pour dire ça, et il l'a dit ;
@@ -125,14 +125,14 @@ namespace
*/
void EnableTunnelFeatures(FStrateGenerationParams& P)
{
P.RoomSpacing = 42.0f; // 80 → 42 : des salles à portée de chaque chunk échantillonné
P.RoomDensity = 0.85f; // 0.35 → 0.85
P.RoomSpacing = 42.0f; // current production scale → 42: rooms reach sampled chunks
P.RoomDensity = 0.85f; // production .35 → .85
P.VerticalScale = 1.35f; // ≠ 1 ⇒ le Z « effectif » diverge du Z monde partout
// ── ÉTAPE B1 : rugosité de paroi (4b) ────────────────────────────────────────────────
// Écrits EXPLICITEMENT, pas laissés au défaut : un test qui dépend d'un défaut se casse en
// silence le jour où le défaut change. `SurfaceRoughness` était le seul de ces champs non nul
// par défaut (5.0), et l'étape A le remettait à zéro — c'est ce zéro qui disparaît ici.
// silence le jour où le défaut change. The production default is now 2.0, but this fixture
// deliberately uses 5.0 so the full roughness branch is exercised.
P.SurfaceRoughness = 5.0f;
P.RoughnessFrequency = 0.1f;
P.RoughnessNoiseType = EVoxelNoiseType::FBM; // les 4 types sont balayés au contrôle 1c
@@ -450,7 +450,9 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
}
const int32 MidChunkZ = ((TopVoxelZ + BottomVoxelZ) / 2) / CHUNK_SIZE;
FStrateGenerationParams P = World.StrateManager->GetGenerationParams(FIntVector(0, 0, MidChunkZ));
const FStrateGenerationParams ProductionP =
World.StrateManager->GetGenerationParams(FIntVector(0, 0, MidChunkZ));
FStrateGenerationParams P = ProductionP;
if (P.StrateTopWorldZ - P.StrateBottomWorldZ <= 0.0f)
{
@@ -1115,8 +1117,8 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
float WorstViolation = 0.0f;
TMap<FString, int32> SolidKillerCounts;
int32 NumRoomKilled = 0, NumTilesAwayFromSpine = 0;
// ±320 voxels = 4 x RoomSpacing. Hors de la boucle : la ligne de rapport en a besoin, et
// une étendue d'échantillonnage qu'on ne peut pas citer est une étendue qu'on ne surveille pas.
// Fixed ±320-voxel probe envelope. The report computes its ratio to the active
// RoomSpacing; keeping the envelope fixed makes before/after tile counts comparable.
const int32 SpanCells = 40;
const int32 SpanVoxelsReported = SpanCells * 8; // Extent = Step * Cells = 1 * 8
int32 TilesHitByRooms = 0, TilesHitByTunnels = 0, TilesHitByPits = 0, TilesHitByChimneys = 0;
@@ -1133,9 +1135,9 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
// ⚠️ L'ÉTENDUE XY ÉTAIT ±32 VOXELS, ET C'EST CE QUI RENDAIT CE BLOC INEXPLOITABLE.
// `RandRange(-4, 4) * 8` échantillonnait 40 tuiles dans un cube de ±32 voxels autour de
// (0,0) — c'est-à-dire l'endroit le PLUS creusé du monde entier, et de loin :
// • `RoomSpacing = 80`, donc ±32 ne couvre même pas la moitié d'UNE cellule de salle ;
// • `OriginRoomRadius = 20` garantit une grosse salle exactement à (0,0), de rayon de
// cull `max(20·1.5, 8) + 3·4 = 42` — qui avale la quasi-totalité de la fenêtre ;
// • the old `RoomSpacing = 80` meant ±32 did not cover half of ONE room cell ;
// • the old `OriginRoomRadius = 20` guaranteed a large room at (0,0), whose cull
// radius `max(20·1.5, 8) + 3·4 = 42` swallowed almost the whole window ;
// • la spine (0,0) descend précisément là.
// La mesure « 4.9 salles sur 7.2 atteignent la boîte » ne décrivait donc pas la densité
// de grottes du monde, elle décrivait le hub de la spine. Aucune conclusion sur la
@@ -1146,9 +1148,8 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
// produirait un verdict FAUX échoue exactement comme avant. On corrige ce que la mesure
// REGARDE, pas ce qu'elle exige.
//
// The XY extent was ±32 voxels around (0,0) -- with RoomSpacing = 80 and a guaranteed
// OriginRoomRadius = 20 room at the origin, that samples the single most cave-dense spot
// in the world and says nothing about deep rock. Widening changes what the measurement
// The old XY extent was ±32 voxels around (0,0) -- inside the guaranteed origin room.
// Widening changes what the measurement
// LOOKS AT, not what it demands: every verdict is still brute-forced below.
const FIntVector Origin(
Rng.RandRange(-SpanCells, SpanCells) * Extent,
@@ -1300,7 +1301,7 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
// A warning that fires every run and always means "this is fine" is noise that
// trains the reader to ignore warnings. Zero proved is the only possible answer on
// the dense fixture (info); on production defaults it would be a real regression
// from 11 (warning).
// from the previous measured count (warning).
const FString ZeroMsg = FString::Printf(
TEXT("[%s] No tile was proved, so the brute force verified nothing -- it has no ")
TEXT("verdict to contradict. Do NOT re-derive the cause: read the two lines above, ")
@@ -1326,15 +1327,14 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
// LES DEUX MONDES, ET POURQUOI IL EN FAUT DEUX
//-------------------------------------------------------------------------
// ⚠️ LA FIXTURE REND CE VERDICT STRUCTURELLEMENT IMPOSSIBLE, ET CE N'EST PAS UN DÉFAUT DE LA
// FIXTURE. `EnableTunnelFeatures` densifie délibérément (`RoomSpacing` 80 → 42,
// `RoomDensity` 0.35 → 0.85) parce qu'aux défauts le premier run n'avait que 1,1 % des
// FIXTURE. `EnableTunnelFeatures` densifie délibérément (production spacing → 42,
// `RoomDensity` 0.35 → 0.85) because the first run had only 1.1% of samples in open cave,
// échantillons en grotte — l'équivalence comparait du roc plein à du roc plein. Cette
// densification est ce qui rend le contrôle 1 SIGNIFIANT.
//
// Mais elle est exactement ANTAGONISTE de la prouvabilité, et l'arithmétique le dit sans
// ambiguïté : le rayon de cull d'une salle vaut `max(R·1.5, R·HeightRatio) + 3·SDFBlendRadius`,
// soit `1.5R + 12` ⇒ entre 27 et 57 pour `R ∈ [10, 30]`, moyenne ≈ 42 — c'est-à-dire
// **exactement le pas du réseau**, à 85 % d'occupation. Des sphères de cull de rayon égal au pas
// soit `1.5R + 12`; at the dense fixture's 85% occupancy those cull spheres overlap the lattice
// du réseau recouvrent l'espace ~3,6 fois. **Aucune boîte de ce monde ne peut être hors de
// toutes les sphères de cull.** Le « 6.3 salles sur 8.3 atteignent la boîte » mesuré est
// exactement ça, et élargir l'échantillonneur n'y a rien changé (39 tuiles sur 40 étaient déjà
@@ -1353,9 +1353,7 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
RunTileScan(Stack, P, Ctx, TEXT("dense fixture"), /*bZeroProvedIsExpected*/ true);
{
FStrateGenerationParams SparseP = P;
SparseP.RoomSpacing = 80.0f; // le défaut d'`UVoxelStrateDefinition`
SparseP.RoomDensity = 0.35f; // idem — voir `EnableTunnelFeatures`
FStrateGenerationParams SparseP = ProductionP;
FVoxelOpStack SparseStack;
VoxelDensityOps::BuildTunnelNetworkStack(SparseStack, SparseP, World.Settings->Seed,
@@ -52,7 +52,10 @@ namespace
case ECaveGeneratorType::Maze:
return FMath::Max(Definition.MazeParams.CellSize, 1.0f);
case ECaveGeneratorType::VerticalShafts:
return FMath::Max(Definition.VerticalShaftParams.ShaftSpacing, 1.0f);
// A 0.6 shaft density does not guarantee an occupied immediate cell. The production
// query therefore permits a bounded two-cell local search and still returns the
// selected site's exact axis.
return FMath::Max(2.0f * Definition.VerticalShaftParams.ShaftSpacing, 1.0f);
case ECaveGeneratorType::FloatingIslands:
return FMath::Max(Definition.FloatingIslandParams.IslandSpacing, 1.0f);
default:
@@ -2107,7 +2107,9 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
FVoxelStrateMeasureSettings BaseSettings;
BaseSettings.CenterXY = FVector2D::ZeroVector;
BaseSettings.MaxCells = 8000000;
// The centered control stays bounded after the 4 m vertical-volume fixture is retained;
// 12 M also leaves room for the scale-aware 320-voxel control below.
BaseSettings.MaxCells = 12000000;
BaseSettings.HeadroomCells = 2;
BaseSettings.InteriorMarginVoxels = -1;
@@ -2121,8 +2123,8 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
{4, 256},
{4, 192},
{2, 192},
{4, 128},
{1, 128},
{4, 160},
{1, 160},
};
bool bAllChecksPassed = true;
@@ -2458,7 +2460,7 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
}
Summary += TEXT(
"Controlled comparisons hold radius constant: (step 4, radius 192) vs (step 2, "
"radius 192), and (step 4, radius 128) vs (step 1, radius 128).\n");
"radius 192), and (step 4, radius 160) vs (step 1, radius 160).\n");
Summary += FString::Printf(
TEXT("VerticalShafts refinement sweep wall-clock: %.3f seconds.\n"),
SweepSeconds);
@@ -3236,10 +3238,17 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
// PART A/B — the production acceptance window follows the actual two mouths. The older
// centered-window diagnostics above remain useful for the origin-spine invariant; they are
// deliberately not reused here because a mouth-fitted box is not expected to contain (0,0).
constexpr int32 FittedMaxCells = 20000000;
// The tree's prescribed neighbour fallback reaches three shaft-grid cells. A mouth window
// must therefore cover three spacings beyond the endpoints before it can be called a fitted
// diagnostic box. This grows with the body-scale shaft spacing rather than preserving the
// old 48-voxel window by habit.
const FVerticalShaftParams& FittedShaftParams =
Layout[VerticalShaftsIndex].Definition->VerticalShaftParams;
const float FittedMargin = FMath::Max(
48.0f, 3.0f * FMath::Max(FittedShaftParams.ShaftSpacing, 1.0f));
const float DoubledFittedMargin = 2.0f * FittedMargin;
constexpr int32 FittedMaxCells = 120000000;
constexpr int32 BeforeChangeMaxCells = 12000000;
constexpr float FittedMargin = 48.0f;
constexpr float DoubledFittedMargin = 96.0f;
auto MakeFittedSettings = [&](int32 SampleStep,
const FVector& A,
@@ -3297,8 +3306,8 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
};
Summary += TEXT(
"PART A — fitted VerticalShafts mouth window (AABB of the two XY mouths + 48 voxels; "
"step 1/2; fitted MaxCells=20000000):\n");
"PART A — fitted VerticalShafts mouth window (AABB of the two XY mouths + three shaft "
"spacings; step 1/2; doubled-margin control; fitted MaxCells=120000000):\n");
Summary += TEXT(
" seed | step | default margin verdict | fitted dimensions/cells | doubled-margin check | "
"interpretation | arrival component share | departure component share\n");
@@ -3341,12 +3350,21 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
{
AddError(FString::Printf(
TEXT("HARD FAILURE: fitted-window seed %d has a VerticalShafts mouth not on a "
"shaft axis (arrival axis=%s, departure axis=%s)."),
"shaft axis (arrival axis=%s, departure axis=%s; arrival=(%.3f,%.3f,%.3f); "
"departure=(%.3f,%.3f,%.3f); spacing=%.3f; density=%.3f)."),
Seed,
IsVerticalShaftAxis(ShaftParams, Seed, ResolutionArrivalPoint)
? TEXT("yes") : TEXT("no"),
IsVerticalShaftAxis(ShaftParams, Seed, ResolutionDeparturePoint)
? TEXT("yes") : TEXT("no")));
? TEXT("yes") : TEXT("no"),
ResolutionArrivalPoint.X,
ResolutionArrivalPoint.Y,
ResolutionArrivalPoint.Z,
ResolutionDeparturePoint.X,
ResolutionDeparturePoint.Y,
ResolutionDeparturePoint.Z,
ShaftParams.ShaftSpacing,
ShaftParams.ShaftDensity));
bPartAFittedChecksPassed = false;
}
@@ -3491,13 +3509,13 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
}
Summary += TEXT(
"PART B — fitted 16-seed audit (step 2, margin 48): strict-local-minimum shafts are the "
"PART B — fitted 16-seed audit (step 2, margin=three shaft spacings): strict-local-minimum shafts are the "
"pre-fix orphan candidates; post-fix parent paths are checked with the same fixed ±2/±3 "
"resolver over a padded [-20,20] cell audit domain. Small-component threshold is one "
"mean-radius shaft volume.\n");
Summary += TEXT(
" seed | fitted verdict | cells | components | small non-largest/total | strict minima/shafts "
"| neighbour fallback | path orphans | axis mouths | doubled margin 96 verdict/cells | "
"| neighbour fallback | path orphans | axis mouths | doubled fitted-margin verdict/cells | "
"tree path XY bounds\n");
static constexpr int32 FittedSeedCases[] = {
@@ -3859,7 +3877,7 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
SmallComponentThresholdMin == INT32_MAX ? 0 : SmallComponentThresholdMin,
SmallComponentThresholdMax);
Summary += FString::Printf(
TEXT(" fitted acceptance arrival->departure: %s (%d/%d effective pass; direct margin48="
TEXT(" fitted acceptance arrival->departure: %s (%d/%d effective pass; direct fitted margin="
"%d/%d, %d fail; binding negatives=%d, trusted negatives=%d, measurement limits=%d); "
"sweep wall-clock=%.3f seconds.\n"),
SweepPasses == UE_ARRAY_COUNT(FittedSeedCases) && SweepFailures == 0
@@ -73,7 +73,8 @@ namespace VoxelForgeTest
* dangerous — and `VoxelForge.Determinism.LargeSeedSurvives` deliberately passes big ones
* (up to 2e9) to prove it stays that way.
*/
void Build(int32 InSeed = 1337, int32 InGapChunks = 2, bool bUseOperatorStack = false)
void Build(int32 InSeed = 1337, int32 InGapChunks = 2, bool bUseOperatorStack = false,
int32 InStrateHeightInChunks = 4)
{
Settings = TStrongObjectPtr<UVoxelSettings>(
NewObject<UVoxelSettings>(GetTransientPackage(), NAME_None, RF_Transient));
@@ -105,7 +106,11 @@ namespace VoxelForgeTest
UVoxelStrateDefinition* Def = NewObject<UVoxelStrateDefinition>(
GetTransientPackage(), NAME_None, RF_Transient);
Def->GeneratorType = Archetypes[i];
Def->StrateHeightInChunks = 4;
// Keep the shared density fixture at the historical 4-chunk test volume unless
// a caller explicitly asks for the production/default 8-chunk envelope. The
// refinement and roll tests use the smaller synthetic volume as a bounded memory
// control; the owner-facing showcase opts into 8 chunks below.
Def->StrateHeightInChunks = FMath::Max(1, InStrateHeightInChunks);
// L'OPT-IN de la pile d'opérateurs. Faux par défaut : les treize tests existants
// doivent continuer à exercer le `switch`, qui reste le comportement de référence.
// Seul le test de solidité de ClassifyTie côté pile le passe à vrai.
@@ -634,6 +634,13 @@ namespace
// than a point on the smoothed ledge edge.
float LandingFromBottom = FMath::Max(
Params.BoundarySealThickness, Params.LedgeDepth) + FloorClearance;
// The measurement API derives its default interior margin as two seal thicknesses.
// Keep a source-level landing strictly inside that same measured window; otherwise a
// lower-roughness default can produce a perfectly valid shaft landing just below the
// diagnostic grid's lower edge and turn a real mouth query into OUT_OF_WINDOW.
LandingFromBottom = FMath::Max(
LandingFromBottom,
2.0f * Params.BoundarySealThickness + 0.01f);
if (Params.LedgeSpacing > 0.0f && Params.LedgeDepth > 0.0f)
{
const float SafeBand = Params.LedgeDepth + FloorClearance;
+166 -101
View File
@@ -1119,34 +1119,34 @@ namespace
if (FieldName == TEXT("WormThreshold")) return Set(0.01f, 0.50f);
if (FieldName == TEXT("WormStrength")) return Set(1.0f, 64.0f);
if (FieldName == TEXT("WormNetworkRange")) return Set(0.0f, H * 1.5f);
if (FieldName == TEXT("RoomSpacing")) return Set(16.0f, H * 2.5f);
if (FieldName == TEXT("RoomSpacing")) return Set(64.0f, FMath::Max(96.0f, H * 0.75f));
if (FieldName == TEXT("RoomDensity")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("MinRoomRadius")) return Set(1.0f, HHalf);
if (FieldName == TEXT("MaxRoomRadius")) return Set(1.0f, H);
if (FieldName == TEXT("MinRoomRadius")) return Set(16.0f, FMath::Max(32.0f, HHalf));
if (FieldName == TEXT("MaxRoomRadius")) return Set(16.0f, FMath::Max(48.0f, H * 0.75f));
if (FieldName == TEXT("RoomHeightRatio")) return Set(0.10f, 1.0f);
if (FieldName == TEXT("RoomShapeVariety")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("RoomFloorCutMin")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("RoomFloorCutMax")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("FloorReliefStrength")) return Set(0.0f, HQuarter);
if (FieldName == TEXT("FloorReliefFrequency")) return Set(0.001f, 0.10f);
if (FieldName == TEXT("OriginRoomRadius")) return Set(0.0f, H * 0.75f);
if (FieldName == TEXT("TunnelMinRadius")) return Set(1.0f, HQuarter);
if (FieldName == TEXT("TunnelMaxRadius")) return Set(1.0f, HHalf);
if (FieldName == TEXT("OriginRoomRadius")) return Set(24.0f, FMath::Max(64.0f, H * 0.75f));
if (FieldName == TEXT("TunnelMinRadius")) return Set(6.0f, FMath::Max(8.0f, HQuarter));
if (FieldName == TEXT("TunnelMaxRadius")) return Set(8.0f, FMath::Max(16.0f, H * 0.25f));
if (FieldName == TEXT("TunnelDensity")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("MaxTunnelLength")) return Set(1.0f, H * 4.0f);
if (FieldName == TEXT("MaxTunnelLength")) return Set(64.0f, H * 4.0f);
if (FieldName == TEXT("TunnelWarpStrength")) return Set(0.0f, H);
if (FieldName == TEXT("TunnelHorizontalBias")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("TunnelEndpointZOffset")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("SDFBlendRadius")) return Set(0.0f, H * 0.15f);
if (FieldName == TEXT("SDFBlendRadius")) return Set(2.0f, FMath::Max(4.0f, H * 0.15f));
if (FieldName == TEXT("WaterLevelRelative")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("CaveWarpStrength")) return Set(0.0f, HHalf);
if (FieldName == TEXT("CaveWarpFrequency")) return Set(0.001f, 0.10f);
if (FieldName == TEXT("SurfaceRoughness")) return Set(0.0f, HQuarter);
if (FieldName == TEXT("SurfaceRoughness")) return Set(0.5f, FMath::Max(4.0f, H * 0.04f));
if (FieldName == TEXT("RoughnessFrequency")) return Set(0.001f, 0.20f);
if (FieldName == TEXT("BoundarySealThickness")) return Set(0.0f, HQuarter);
if (FieldName == TEXT("DomainWarpStrength")) return Set(0.0f, HHalf);
if (FieldName == TEXT("DomainWarpFrequency")) return Set(0.001f, 0.20f);
if (FieldName == TEXT("FloorBias")) return Set(0.0f, HQuarter);
if (FieldName == TEXT("FloorBias")) return Set(0.0f, FMath::Max(4.0f, H * 0.10f));
if (FieldName == TEXT("TerraceStepHeight")) return Set(0.0f, HQuarter);
if (FieldName == TEXT("TerraceHardness")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("TerraceNoiseDisplacement")) return Set(0.0f, 1.0f);
@@ -1161,22 +1161,22 @@ namespace
if (FieldName == TEXT("ScallopStrength")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("ScallopFrequency")) return Set(0.001f, 0.20f);
if (FieldName == TEXT("ArchDensity")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("ArchMinRadius")) return Set(1.0f, H * 0.20f);
if (FieldName == TEXT("ArchMaxRadius")) return Set(1.0f, H * 0.20f);
if (FieldName == TEXT("ArchMinRadius")) return Set(6.0f, FMath::Max(12.0f, H * 0.20f));
if (FieldName == TEXT("ArchMaxRadius")) return Set(12.0f, FMath::Max(24.0f, H * 0.30f));
if (FieldName == TEXT("ColumnDensity")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("ColumnMinRadius")) return Set(1.0f, H * 0.20f);
if (FieldName == TEXT("ColumnMaxRadius")) return Set(1.0f, H * 0.40f);
if (FieldName == TEXT("ColumnMinRadius")) return Set(4.0f, FMath::Max(8.0f, H * 0.20f));
if (FieldName == TEXT("ColumnMaxRadius")) return Set(8.0f, FMath::Max(16.0f, H * 0.40f));
if (FieldName == TEXT("PitDensity")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("PitMinRadius")) return Set(1.0f, H * 0.20f);
if (FieldName == TEXT("PitMaxRadius")) return Set(1.0f, H * 0.40f);
if (FieldName == TEXT("PitMinRadius")) return Set(8.0f, FMath::Max(16.0f, H * 0.20f));
if (FieldName == TEXT("PitMaxRadius")) return Set(16.0f, FMath::Max(32.0f, H * 0.40f));
if (FieldName == TEXT("PitDepth")) return Set(0.0f, HHalf);
if (FieldName == TEXT("ChimneyDensity")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("ChimneyMinRadius")) return Set(1.0f, H * 0.20f);
if (FieldName == TEXT("ChimneyMaxRadius")) return Set(1.0f, H * 0.40f);
if (FieldName == TEXT("ChimneyMinRadius")) return Set(4.0f, FMath::Max(8.0f, H * 0.20f));
if (FieldName == TEXT("ChimneyMaxRadius")) return Set(8.0f, FMath::Max(16.0f, H * 0.40f));
if (FieldName == TEXT("ChimneyHeight")) return Set(0.0f, HHalf);
if (FieldName == TEXT("DomeDensity")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("DomeMinRadius")) return Set(1.0f, H * 0.30f);
if (FieldName == TEXT("DomeMaxRadius")) return Set(1.0f, H * 0.50f);
if (FieldName == TEXT("DomeMinRadius")) return Set(16.0f, FMath::Max(32.0f, H * 0.30f));
if (FieldName == TEXT("DomeMaxRadius")) return Set(32.0f, FMath::Max(48.0f, H * 0.50f));
if (FieldName == TEXT("DomeHeightRatio")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("PinchDensity")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("PinchStrength")) return Set(0.0f, H);
@@ -1190,58 +1190,58 @@ namespace
case ECaveGeneratorType::CrystalChamber:
if (FieldName == TEXT("FloorRelativeHeight")) return Set(0.0f, 0.95f);
if (FieldName == TEXT("CeilingRelativeHeight")) return Set(0.05f, 1.0f);
if (FieldName == TEXT("FloorRoughness")) return Set(0.0f, HQuarter);
if (FieldName == TEXT("FloorRoughness")) return Set(0.5f, FMath::Max(4.0f, H * 0.04f));
if (FieldName == TEXT("FloorRoughnessFrequency")) return Set(0.001f, 0.15f);
if (FieldName == TEXT("CeilingRoughness")) return Set(0.0f, HHalf);
if (FieldName == TEXT("CeilingRoughness")) return Set(1.0f, FMath::Max(16.0f, H * 0.12f));
if (FieldName == TEXT("CeilingRoughnessFrequency")) return Set(0.001f, 0.15f);
if (FieldName == TEXT("ColumnDensity")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("ColumnMinRadius")) return Set(1.0f, H * 0.20f);
if (FieldName == TEXT("ColumnMaxRadius")) return Set(1.0f, H * 0.40f);
if (FieldName == TEXT("ColumnSpacing")) return Set(10.0f, H * 1.5f);
if (FieldName == TEXT("ColumnMinRadius")) return Set(8.0f, FMath::Max(12.0f, H * 0.20f));
if (FieldName == TEXT("ColumnMaxRadius")) return Set(16.0f, FMath::Max(24.0f, H * 0.40f));
if (FieldName == TEXT("ColumnSpacing")) return Set(64.0f, FMath::Max(96.0f, H * 0.75f));
if (FieldName == TEXT("BoundarySealThickness")) return Set(0.0f, HQuarter);
if (FieldName == TEXT("BaseDensity")) return Set(1.0f, 32.0f);
return false;
case ECaveGeneratorType::Maze:
if (FieldName == TEXT("CellSize")) return Set(8.0f, H * 2.0f);
if (FieldName == TEXT("CorridorRadius")) return Set(1.0f, HQuarter);
if (FieldName == TEXT("CellSize")) return Set(48.0f, FMath::Max(64.0f, FMath::Min(96.0f, H * 0.75f)));
if (FieldName == TEXT("CorridorRadius")) return Set(6.0f, 8.0f);
if (FieldName == TEXT("BranchProbability")) return Set(0.05f, 1.0f);
if (FieldName == TEXT("Verticality")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("SurfaceRoughness")) return Set(0.0f, HQuarter);
if (FieldName == TEXT("SurfaceRoughness")) return Set(0.5f, FMath::Max(3.0f, H * 0.02f));
if (FieldName == TEXT("BoundarySealThickness")) return Set(0.0f, HQuarter);
if (FieldName == TEXT("BaseDensity")) return Set(1.0f, 32.0f);
return false;
case ECaveGeneratorType::SurfaceWorld:
if (FieldName == TEXT("BaseGroundRelative")) return Set(0.05f, 0.90f);
if (FieldName == TEXT("ElevationRange")) return Set(0.0f, H * 2.0f);
if (FieldName == TEXT("ElevationRange")) return Set(24.0f, FMath::Max(80.0f, H * 0.50f));
if (FieldName == TEXT("ContinentFrequency")) return Set(0.0005f, 0.10f);
if (FieldName == TEXT("MountainStrength")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("MountainFrequency")) return Set(0.0005f, 0.10f);
if (FieldName == TEXT("DetailFrequency")) return Set(0.0005f, 0.20f);
if (FieldName == TEXT("SurfaceRoughness")) return Set(0.0f, HQuarter);
if (FieldName == TEXT("HeightWarpStrength")) return Set(0.0f, H);
if (FieldName == TEXT("SurfaceRoughness")) return Set(1.0f, FMath::Max(3.0f, H * 0.02f));
if (FieldName == TEXT("HeightWarpStrength")) return Set(0.0f, FMath::Max(48.0f, H * 0.25f));
if (FieldName == TEXT("HeightWarpFrequency")) return Set(0.0005f, 0.10f);
if (FieldName == TEXT("ReliefFrequency")) return Set(0.0002f, 0.02f);
if (FieldName == TEXT("ReliefStrength")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("ReliefContrast")) return Set(0.25f, 4.0f);
if (FieldName == TEXT("TerraceStrength")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("TerraceHeight")) return Set(1.0f, HHalf);
if (FieldName == TEXT("TerraceHeight")) return Set(2.0f, FMath::Max(8.0f, H * 0.08f));
if (FieldName == TEXT("TerraceHardness")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("LayerLineDepth")) return Set(0.0f, HQuarter);
if (FieldName == TEXT("LayerLineSpacing")) return Set(1.0f, H);
if (FieldName == TEXT("LayerLineSpacing")) return Set(2.0f, FMath::Max(8.0f, H * 0.25f));
if (FieldName == TEXT("CliffStrength")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("CliffSlopeThreshold")) return Set(0.05f, 2.0f);
if (FieldName == TEXT("CliffSharpness")) return Set(0.0f, 4.0f);
if (FieldName == TEXT("CliffSampleDist")) return Set(0.5f, HQuarter);
if (FieldName == TEXT("OverhangStrength")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("OverhangReach")) return Set(0.0f, HHalf);
if (FieldName == TEXT("OverhangHeight")) return Set(0.0f, HHalf);
if (FieldName == TEXT("OverhangReach")) return Set(4.0f, FMath::Max(16.0f, H * 0.15f));
if (FieldName == TEXT("OverhangHeight")) return Set(4.0f, FMath::Max(24.0f, H * 0.25f));
if (FieldName == TEXT("OverhangFrequency")) return Set(0.0005f, 0.20f);
if (FieldName == TEXT("OverhangZScale")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("OverhangSlopeThreshold")) return Set(0.05f, 2.0f);
if (FieldName == TEXT("WaterLevelRelative")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("BeachWidth")) return Set(0.0f, HQuarter);
if (FieldName == TEXT("BeachWidth")) return Set(4.0f, FMath::Max(12.0f, H * 0.10f));
if (FieldName == TEXT("CeilingRelative")) return Set(0.30f, 1.0f);
if (FieldName == TEXT("CeilingRoughness")) return Set(0.0f, HHalf);
if (FieldName == TEXT("CeilingRoughnessFrequency")) return Set(0.0005f, 0.20f);
@@ -1256,29 +1256,29 @@ namespace
return false;
case ECaveGeneratorType::VerticalShafts:
if (FieldName == TEXT("ShaftSpacing")) return Set(10.0f, H * 2.0f);
if (FieldName == TEXT("ShaftSpacing")) return Set(64.0f, FMath::Max(80.0f, FMath::Min(112.0f, H * 0.75f)));
if (FieldName == TEXT("ShaftDensity")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("ShaftMinRadius")) return Set(1.0f, H * 0.20f);
if (FieldName == TEXT("ShaftMaxRadius")) return Set(1.0f, H * 0.40f);
if (FieldName == TEXT("ShaftMinRadius")) return Set(8.0f, 16.0f);
if (FieldName == TEXT("ShaftMaxRadius")) return Set(12.0f, 24.0f);
if (FieldName == TEXT("CrossConnectChance")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("ConnectorRadius")) return Set(1.0f, H * 0.20f);
if (FieldName == TEXT("LedgeSpacing")) return Set(0.0f, H);
if (FieldName == TEXT("LedgeDepth")) return Set(0.0f, HQuarter);
if (FieldName == TEXT("SurfaceRoughness")) return Set(0.0f, HQuarter);
if (FieldName == TEXT("ConnectorRadius")) return Set(6.0f, 12.0f);
if (FieldName == TEXT("LedgeSpacing")) return Set(8.0f, FMath::Max(32.0f, H * 0.25f));
if (FieldName == TEXT("LedgeDepth")) return Set(2.0f, FMath::Max(4.0f, H * 0.08f));
if (FieldName == TEXT("SurfaceRoughness")) return Set(0.5f, FMath::Max(3.0f, H * 0.02f));
if (FieldName == TEXT("BoundarySealThickness")) return Set(0.0f, HQuarter);
if (FieldName == TEXT("BaseDensity")) return Set(1.0f, 32.0f);
return false;
case ECaveGeneratorType::FloatingIslands:
if (FieldName == TEXT("IslandSpacing")) return Set(20.0f, H * 3.0f);
if (FieldName == TEXT("IslandSpacing")) return Set(96.0f, FMath::Max(112.0f, FMath::Min(160.0f, H * 0.75f)));
if (FieldName == TEXT("IslandDensity")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("IslandMinRadius")) return Set(2.0f, H * 0.30f);
if (FieldName == TEXT("IslandMaxRadius")) return Set(2.0f, H * 0.75f);
if (FieldName == TEXT("IslandMinRadius")) return Set(24.0f, FMath::Max(32.0f, H * 0.30f));
if (FieldName == TEXT("IslandMaxRadius")) return Set(48.0f, FMath::Max(64.0f, H * 0.50f));
if (FieldName == TEXT("ThicknessRatio")) return Set(0.10f, 1.5f);
if (FieldName == TEXT("VerticalJitter")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("TopFlatten")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("SurfaceRoughness")) return Set(0.0f, HQuarter);
if (FieldName == TEXT("SDFBlendRadius")) return Set(0.0f, HQuarter);
if (FieldName == TEXT("SurfaceRoughness")) return Set(0.5f, FMath::Max(3.0f, H * 0.02f));
if (FieldName == TEXT("SDFBlendRadius")) return Set(2.0f, FMath::Max(6.0f, H * 0.10f));
if (FieldName == TEXT("BoundarySealThickness")) return Set(0.0f, HQuarter);
if (FieldName == TEXT("BaseDensity")) return Set(1.0f, 32.0f);
return false;
@@ -1533,35 +1533,47 @@ namespace
float StrateHeightInVoxels)
{
const float H = FMath::Max(StrateHeightInVoxels, static_cast<float>(CHUNK_SIZE));
const float SpacingMin = FMath::Min(48.0f, H * 0.35f);
const float SpacingMax = FMath::Max(SpacingMin + 8.0f, FMath::Min(160.0f, H * 0.94f));
// Human-scale derivation: an 8 m room is the smallest room-like volume (radius 16),
// a 20 m chamber is the large-room target (radius 40), and a 24 m hub is the three-
// chunk boss-space target. Keep the room-cell margin independent of those radii.
const float SpacingMin = FMath::Max(96.0f, H * 0.35f);
const float SpacingMax = FMath::Max(SpacingMin + 32.0f, FMath::Min(160.0f, H * 0.94f));
const float Coverage = VF_CorpusFreeU(Rng, 0.08f, 0.18f);
P.BoundarySealThickness = VF_CorpusFreeU(
Rng, FMath::Min(3.0f, H * 0.02f), FMath::Min(8.0f, H * 0.08f));
P.RoomSpacing = VF_CorpusFreeU(Rng, SpacingMin, SpacingMax);
P.RoomDensity = VF_CorpusFreeU(Rng, 0.25f, 0.75f);
P.RoomDensity = VF_CorpusFreeU(Rng, 0.20f, 0.45f);
const float RadiusRatio = FMath::Sqrt(
Coverage / (PI * FMath::Max(P.RoomDensity, 0.001f)));
P.MaxRoomRadius = P.RoomSpacing * RadiusRatio;
P.MinRoomRadius = P.MaxRoomRadius * VF_CorpusFreeU(Rng, 0.35f, 0.75f);
P.RoomHeightRatio = VF_CorpusFreeU(Rng, 0.25f, 0.85f);
P.OriginRoomRadius = P.MaxRoomRadius * VF_CorpusFreeU(Rng, 0.60f, 0.95f);
P.MaxRoomRadius = FMath::Min(
FMath::Max(40.0f, P.RoomSpacing * RadiusRatio),
FMath::Min(P.RoomSpacing * 0.45f, H * 0.30f));
P.MinRoomRadius = FMath::Max(
16.0f, P.MaxRoomRadius * VF_CorpusFreeU(Rng, 0.45f, 0.70f));
P.MinRoomRadius = FMath::Min(P.MinRoomRadius, P.MaxRoomRadius);
P.RoomHeightRatio = VF_CorpusFreeU(Rng, 0.70f, 0.95f);
const float MaxRoomVerticalRadius = FMath::Max(
8.0f, H * 0.50f - P.BoundarySealThickness - 1.0f);
P.OriginRoomRadius = FMath::Min(
P.MaxRoomRadius * VF_CorpusFreeU(Rng, 0.90f, 1.15f),
MaxRoomVerticalRadius / FMath::Max(P.RoomHeightRatio, 0.1f));
P.OriginRoomRadius = FMath::Max(P.OriginRoomRadius, P.MinRoomRadius);
P.RoomFloorCutMin = VF_CorpusFreeU(Rng, 0.35f, 0.85f);
P.RoomFloorCutMin = VF_CorpusFreeU(Rng, 0.55f, 0.75f);
P.RoomFloorCutMax = P.RoomFloorCutMin + VF_CorpusFreeU(
Rng, 0.0f, 1.0f - P.RoomFloorCutMin);
Rng, 0.0f, FMath::Min(0.25f, 1.0f - P.RoomFloorCutMin));
P.FloorReliefStrength = VF_CorpusFreeU(
Rng, 0.0f,
FMath::Min(12.0f, P.MaxRoomRadius * P.RoomHeightRatio * 0.25f));
const float TunnelMinCap = FMath::Max(1.5f, P.MinRoomRadius * 0.45f);
const float TunnelMinCap = FMath::Max(6.0f, FMath::Min(8.0f, P.MinRoomRadius * 0.45f));
P.TunnelMinRadius = FMath::Max(
1.5f, P.MinRoomRadius * VF_CorpusFreeU(Rng, 0.25f, 0.45f));
6.0f, P.MinRoomRadius * VF_CorpusFreeU(Rng, 0.25f, 0.40f));
P.TunnelMinRadius = FMath::Min(P.TunnelMinRadius, TunnelMinCap);
P.TunnelMaxRadius = FMath::Max(
P.TunnelMinRadius,
P.MinRoomRadius * VF_CorpusFreeU(Rng, 0.55f, 0.90f));
FMath::Min(8.0f, P.MinRoomRadius * VF_CorpusFreeU(Rng, 0.50f, 0.75f)));
// Adjacent cells can differ in both X and Y. The placement jitter is [0.15, 0.85]
// cell, so the worst local diagonal separation is 1.7 * S * sqrt(2), not the
@@ -1570,17 +1582,17 @@ namespace
const float VerticalReach = 2.0f * P.MaxRoomRadius * P.RoomHeightRatio;
const float NeighborReach = FMath::Sqrt(
HorizontalReach * HorizontalReach + VerticalReach * VerticalReach);
P.MaxTunnelLength = NeighborReach * VF_CorpusFreeU(Rng, 1.0f, 1.40f);
P.TunnelWarpStrength = P.MaxTunnelLength * VF_CorpusFreeU(Rng, 0.02f, 0.12f);
P.WormStrength = P.BaseDensity * VF_CorpusFreeU(Rng, 1.10f, 1.80f);
P.WormNetworkRange = P.RoomSpacing * VF_CorpusFreeU(Rng, 0.20f, 0.60f);
P.CaveWarpStrength = P.RoomSpacing * VF_CorpusFreeU(Rng, 0.02f, 0.12f);
P.MaxTunnelLength = NeighborReach * VF_CorpusFreeU(Rng, 1.05f, 1.30f);
P.TunnelWarpStrength = P.MaxTunnelLength * VF_CorpusFreeU(Rng, 0.03f, 0.08f);
P.WormStrength = P.BaseDensity * VF_CorpusFreeU(Rng, 1.35f, 1.80f);
P.WormNetworkRange = P.RoomSpacing * VF_CorpusFreeU(Rng, 0.25f, 0.50f);
P.CaveWarpStrength = P.RoomSpacing * VF_CorpusFreeU(Rng, 0.03f, 0.10f);
P.SDFBlendRadius = VF_CorpusFreeU(
Rng, 0.5f, FMath::Max(0.5f, FMath::Min(4.0f, P.TunnelMinRadius * 0.35f)));
Rng, 2.0f, FMath::Max(2.0f, FMath::Min(4.0f, P.TunnelMinRadius * 0.50f)));
const float RoughnessMax = FMath::Max(
0.0f, (P.TunnelMinRadius - P.SDFBlendRadius - 0.25f) / VOXEL_NOISE_SCALE);
P.SurfaceRoughness = VF_CorpusFreeU(Rng, 0.0f, RoughnessMax * 0.70f);
P.SurfaceRoughness = VF_CorpusFreeU(Rng, 0.5f, RoughnessMax * 0.60f);
P.FloorBias = VF_CorpusFreeU(
Rng, 0.0f, P.MaxRoomRadius * P.RoomHeightRatio * 0.25f);
@@ -1597,7 +1609,12 @@ namespace
P.ArchMaxRadius = FMath::Max(
P.ArchMinRadius,
P.MinRoomRadius * VF_CorpusFreeU(Rng, 0.20f, 0.45f));
P.DomeMaxRadius = P.MaxRoomRadius * VF_CorpusFreeU(Rng, 0.30f, 0.60f);
// The public envelope has a 32-voxel minimum dome radius. Apply that envelope before
// deriving the height ratio; otherwise a small sampled dome would be clamped larger
// after this equation and could violate the room-height relation.
P.DomeMaxRadius = FMath::Min(
P.MaxRoomRadius,
FMath::Max(32.0f, P.MaxRoomRadius * VF_CorpusFreeU(Rng, 0.30f, 0.60f)));
P.DomeMinRadius = P.DomeMaxRadius * VF_CorpusFreeU(Rng, 0.35f, 0.75f);
P.DomeHeightRatio = FMath::Min(
1.0f,
@@ -1615,23 +1632,27 @@ namespace
const float H = FMath::Max(StrateHeightInVoxels, static_cast<float>(CHUNK_SIZE));
P.BoundarySealThickness = VF_CorpusFreeU(
Rng, FMath::Min(3.0f, H * 0.02f), FMath::Min(8.0f, H * 0.08f));
P.FloorRoughness = VF_CorpusFreeU(Rng, 0.0f, H * 0.04f);
P.CeilingRoughness = VF_CorpusFreeU(Rng, 0.0f, H * 0.08f);
// A slab is an open fight/chamber volume, not a one-voxel shell. At the new
// eight-chunk default this leaves roughly 20-50 m between the two surfaces after
// reserving the 2.5-4 m feature envelopes below.
P.FloorRoughness = VF_CorpusFreeU(Rng, 1.0f, FMath::Max(4.0f, H * 0.04f));
P.CeilingRoughness = VF_CorpusFreeU(Rng, 2.0f, FMath::Max(8.0f, H * 0.06f));
const float FloorNoise = VOXEL_NOISE_SCALE * P.FloorRoughness;
const float CeilingNoise = VOXEL_NOISE_SCALE * P.CeilingRoughness;
const float FloorClearance = VF_CorpusFreeU(Rng, H * 0.08f, H * 0.16f);
const float CeilingClearance = VF_CorpusFreeU(Rng, H * 0.08f, H * 0.16f);
const float FloorClearance = VF_CorpusFreeU(Rng, H * 0.08f, H * 0.12f);
const float CeilingClearance = VF_CorpusFreeU(Rng, H * 0.08f, H * 0.12f);
P.FloorRelativeHeight = (P.BoundarySealThickness + FloorNoise + FloorClearance) / H;
P.CeilingRelativeHeight = 1.0f
- (P.BoundarySealThickness + CeilingNoise + CeilingClearance) / H;
P.ColumnSpacing = VF_CorpusFreeU(
Rng, FMath::Max(24.0f, H * 0.30f), FMath::Max(32.0f, H * 0.75f));
P.ColumnMinRadius = VF_CorpusFreeU(Rng, 1.0f, P.ColumnSpacing * 0.08f);
Rng, FMath::Max(96.0f, H * 0.375f),
FMath::Max(128.0f, FMath::Min(160.0f, H * 0.75f)));
P.ColumnMinRadius = VF_CorpusFreeU(Rng, 8.0f, 12.0f);
P.ColumnMaxRadius = FMath::Max(
P.ColumnMinRadius,
FMath::Min(P.ColumnSpacing * 0.40f, H * 0.38f));
FMath::Min(P.ColumnSpacing * 0.40f, P.ColumnMinRadius + 8.0f));
return true;
}
@@ -1640,12 +1661,16 @@ namespace
float StrateHeightInVoxels)
{
const float H = FMath::Max(StrateHeightInVoxels, static_cast<float>(CHUNK_SIZE));
// Body-derived lattice: the fight corridor is 3-4 m wide (radius 6-8 voxels),
// while the lattice cell is 12-16 m (48-64 voxels). Thus a corridor radius is
// about one eighth of a cell, far below CellSize/2, leaving a real rock wall.
P.CellSize = VF_CorpusFreeU(
Rng, FMath::Max(24.0f, H * 0.25f), FMath::Min(96.0f, H * 0.75f));
P.CorridorRadius = P.CellSize * VF_CorpusFreeU(Rng, 0.12f, 0.24f);
Rng, 48.0f, FMath::Max(64.0f, FMath::Min(96.0f, H * 0.75f)));
P.CorridorRadius = VF_CorpusFreeU(
Rng, 6.0f, FMath::Min(8.0f, P.CellSize * 0.20f));
const float RoughnessCap = FMath::Max(
0.0f, (P.CorridorRadius + 2.0f) / (VOXEL_NOISE_SCALE * 1.25f));
P.SurfaceRoughness = VF_CorpusFreeU(Rng, 0.0f, RoughnessCap * 0.70f);
P.SurfaceRoughness = VF_CorpusFreeU(Rng, 0.5f, RoughnessCap * 0.60f);
P.BoundarySealThickness = VF_CorpusFreeU(
Rng, FMath::Min(3.0f, H * 0.02f), FMath::Min(8.0f, H * 0.08f));
return true;
@@ -1658,9 +1683,11 @@ namespace
const float H = FMath::Max(StrateHeightInVoxels, static_cast<float>(CHUNK_SIZE));
P.BoundarySealThickness = VF_CorpusFreeU(
Rng, FMath::Min(3.0f, H * 0.02f), FMath::Min(8.0f, H * 0.08f));
P.ElevationRange = VF_CorpusFreeU(Rng, H * 0.06f, H * 0.16f);
// Outdoor relief uses 6-20 m as its authored human-scale band; the absolute
// height is still tied to H so the sky cap has room above the terrain.
P.ElevationRange = VF_CorpusFreeU(Rng, H * 0.10f, H * 0.25f);
P.MountainStrength = VF_CorpusFreeU(Rng, 0.20f, 0.80f);
P.SurfaceRoughness = VF_CorpusFreeU(Rng, 0.0f, H * 0.02f);
P.SurfaceRoughness = VF_CorpusFreeU(Rng, 1.0f, FMath::Max(3.0f, H * 0.02f));
const float GroundLowerDeviation = P.ElevationRange * 0.50f
+ P.SurfaceRoughness * VOXEL_NOISE_SCALE;
@@ -1743,18 +1770,21 @@ namespace
const float H = FMath::Max(StrateHeightInVoxels, static_cast<float>(CHUNK_SIZE));
P.BoundarySealThickness = VF_CorpusFreeU(
Rng, FMath::Min(3.0f, H * 0.02f), FMath::Min(8.0f, H * 0.08f));
// A shaft is a 4-7 m diameter vertical room; connectors are 3-6 m bores.
// The 20-28 m site spacing leaves a substantial rock web between shafts.
P.ShaftSpacing = VF_CorpusFreeU(
Rng, FMath::Max(32.0f, H * 0.30f),
FMath::Max(32.0f, FMath::Min(112.0f, H * 0.90f)));
P.SurfaceRoughness = VF_CorpusFreeU(Rng, 0.0f, H * 0.04f);
Rng, 80.0f, FMath::Max(96.0f, FMath::Min(112.0f, H * 0.90f)));
P.SurfaceRoughness = VF_CorpusFreeU(Rng, 1.0f, FMath::Max(3.0f, H * 0.02f));
const float RoughnessReach = P.SurfaceRoughness * VOXEL_NOISE_SCALE * 1.5f;
P.ShaftMinRadius = RoughnessReach + 0.5f + VF_CorpusFreeU(Rng, 1.0f, 4.0f);
P.ShaftMinRadius = FMath::Max(
8.0f, RoughnessReach + 0.5f + VF_CorpusFreeU(Rng, 1.0f, 3.0f));
P.ShaftMaxRadius = FMath::Max(
P.ShaftMinRadius + 0.5f,
FMath::Min(P.ShaftSpacing * 0.40f, P.ShaftMinRadius + 16.0f));
P.ConnectorRadius = RoughnessReach + 1.0f + VF_CorpusFreeU(Rng, 0.0f, 3.0f);
P.ShaftMinRadius + 4.0f,
FMath::Min(P.ShaftSpacing * 0.40f, P.ShaftMinRadius + 8.0f));
P.ConnectorRadius = FMath::Max(
6.0f, RoughnessReach + 1.0f + VF_CorpusFreeU(Rng, 0.0f, 2.0f));
P.LedgeDepth = VF_CorpusFreeU(
Rng, 0.5f, FMath::Max(0.5f, FMath::Min(4.0f, P.ShaftSpacing * 0.08f)));
Rng, 2.0f, FMath::Max(4.0f, FMath::Min(6.0f, P.ShaftSpacing * 0.08f)));
const float FloorClearance = FMath::Max(
1.0f, P.SurfaceRoughness * VOXEL_NOISE_SCALE + 1.0f);
P.LedgeSpacing = 2.0f * (P.LedgeDepth + FloorClearance)
@@ -1770,21 +1800,23 @@ namespace
const float H = FMath::Max(StrateHeightInVoxels, static_cast<float>(CHUNK_SIZE));
P.BoundarySealThickness = VF_CorpusFreeU(
Rng, FMath::Min(3.0f, H * 0.02f), FMath::Min(8.0f, H * 0.08f));
// Island radii are deliberately larger than the roughness envelope: 12-24 m
// diameters read as land masses, while 28-40 m site spacing prevents mush.
P.IslandSpacing = VF_CorpusFreeU(
Rng, FMath::Max(40.0f, H * 0.30f),
FMath::Max(40.0f, FMath::Min(120.0f, H * 0.75f)));
P.ThicknessRatio = VF_CorpusFreeU(Rng, 0.30f, 0.90f);
Rng, 112.0f, FMath::Max(112.0f, FMath::Min(160.0f, H * 0.75f)));
P.ThicknessRatio = VF_CorpusFreeU(Rng, 0.45f, 0.75f);
const float MaxByHeight = (H * 0.50f - P.BoundarySealThickness)
/ FMath::Max(P.ThicknessRatio, 0.25f);
const float MaxRadius = FMath::Max(
2.0f, FMath::Min(P.IslandSpacing * 0.45f, MaxByHeight * 0.85f));
48.0f, FMath::Min(P.IslandSpacing * 0.45f, MaxByHeight * 0.85f));
P.IslandMaxRadius = MaxRadius;
P.IslandMinRadius = P.IslandMaxRadius * VF_CorpusFreeU(Rng, 0.45f, 0.80f);
P.IslandMinRadius = FMath::Max(
24.0f, P.IslandMaxRadius * VF_CorpusFreeU(Rng, 0.50f, 0.80f));
P.SDFBlendRadius = VF_CorpusFreeU(
Rng, 0.5f, FMath::Min(6.0f, P.IslandMinRadius * 0.20f));
Rng, 2.0f, FMath::Min(6.0f, P.IslandMinRadius * 0.20f));
const float RoughnessMax = FMath::Max(
0.0f, (P.IslandMinRadius - P.SDFBlendRadius) / VOXEL_NOISE_SCALE);
P.SurfaceRoughness = VF_CorpusFreeU(Rng, 0.0f, RoughnessMax * 0.70f);
P.SurfaceRoughness = VF_CorpusFreeU(Rng, 0.5f, RoughnessMax * 0.60f);
return true;
}
@@ -1860,8 +1892,39 @@ namespace
}
if (bValid && bConstraintSampled)
{
bValid = VF_ApplyCorpusFreeConstraints(Archetype, Params, ScalarRng,
StrateHeightInVoxels);
// Dependent constraints are authored in terms of the final vector, while the
// reflected envelope clamp below can move an independently sampled field (for
// example SurfaceWorld elevation) after that derivation. Treat the fully clamped
// vector as the candidate and deterministically resample until the declared
// relations hold; never return a vector whose coupling was only true pre-clamp.
constexpr int32 MaxConstraintAttempts = 64;
FString ConstraintViolation;
bool bConstraintValid = false;
for (int32 Attempt = 0; Attempt < MaxConstraintAttempts; ++Attempt)
{
bConstraintValid = VF_ApplyCorpusFreeConstraints(
Archetype, Params, ScalarRng, StrateHeightInVoxels);
if (!bConstraintValid)
{
break;
}
VF_ClampCorpusFreeFields(Params, Archetype, StrateHeightInVoxels);
VF_ResetExcludedFields(Params, Archetype);
if (VF_ValidateStrateCorpusFreeConstraints(
Archetype, Params, StrateHeightInVoxels, ConstraintViolation))
{
break;
}
bConstraintValid = false;
}
if (!bConstraintValid)
{
Result.FailureReason = ConstraintViolation.IsEmpty()
? TEXT("Constraint sampler could not produce a clamped legal vector.")
: FString::Printf(TEXT("Constraint sampler exhausted legal retries: %s"),
*ConstraintViolation);
return Result;
}
}
if (!bValid)
{
@@ -1869,10 +1932,12 @@ namespace
return Result;
}
// Last safety net only: all dependent values above are derived before this envelope
// clamp, and no corpus/default value is consulted.
VF_ClampCorpusFreeFields(Params, Archetype, StrateHeightInVoxels);
VF_ResetExcludedFields(Params, Archetype);
if (!bConstraintSampled)
{
// Last safety net for the independent arm; no corpus/default value is consulted.
VF_ClampCorpusFreeFields(Params, Archetype, StrateHeightInVoxels);
VF_ResetExcludedFields(Params, Archetype);
}
Result.ArchetypeParams = Params;
Result.Params = Params.TunnelNetworkParams;
Result.bValid = true;
@@ -627,9 +627,11 @@ void UVoxelStrateManager::GeneratePassages()
return FMath::Max(Definition.MazeParams.CellSize, 1.0f);
case ECaveGeneratorType::VerticalShafts:
// One shaft grid spacing is the natural nearest-site budget. A sparser layout may
// legitimately decline instead of pulling the passage away from the spine.
return FMath::Max(Definition.VerticalShaftParams.ShaftSpacing, 1.0f);
// A density of 0.6 does not guarantee an occupied cell in the immediate grid
// neighbourhood. Allow the nearest occupied shaft to be two grid cells away while
// remaining a bounded local query; the landing query still returns that shaft's
// exact axis, so this cannot create a non-topological mouth.
return FMath::Max(2.0f * Definition.VerticalShaftParams.ShaftSpacing, 1.0f);
case ECaveGeneratorType::FloatingIslands:
// One island grid spacing protects the intentional radial placement while still
@@ -57,7 +57,7 @@ struct VOXELFORGE_API FVoxelSeasonSelectionPolicy
struct VOXELFORGE_API FVoxelSeasonFixedStrate
{
int32 Seed = 0;
int32 HeightInChunks = 4;
int32 HeightInChunks = 8;
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
FVoxelStrateArchetypeParams Params;
FVoxelOpStackRecipe Recipe;
@@ -85,7 +85,7 @@ struct VOXELFORGE_API FVoxelSeasonCompositionSettings
// Generated candidates use one stable height. Fixed authored slots may provide their own
// height. World layout bounds are written into every selected parameter family below.
int32 StrateHeightInChunks = 4;
int32 StrateHeightInChunks = 8;
int32 InterStrateGapChunks = 0;
float OriginSpineRadius = 14.0f;
float WorldRadiusVoxels = 0.0f;
@@ -125,7 +125,9 @@ struct VOXELFORGE_API FVoxelSeasonCompositionSettings
PlayerFitMeasureSettings.SampleStep = 1;
PlayerFitMeasureSettings.RadiusInVoxels = 64;
PlayerFitMeasureSettings.MaxCells = 2000000;
// A fitted mouth-to-mouth XY window across an 8-chunk (256-voxel) strate can approach
// four million cells. Keep the cap finite and below the old multi-gigabyte retry cost.
PlayerFitMeasureSettings.MaxCells = 4000000;
PlayerFitMeasureSettings.MaxRouteRetries = 16;
PlayerFitMeasureSettings.HeadroomCells = 2;
PlayerFitMeasureSettings.InteriorMarginVoxels = -1;
@@ -66,11 +66,11 @@ public:
// How many chunks tall this strate is.
// Each strate can have a different height!
// Big open caverns = 6-8 chunks, tight tunnels = 3, vertical shaft = 12+
// Surface/sky worlds want a high sky-cap ceiling, so they go much taller.
// 1 chunk = 32 voxels x 25cm = 8 m, so 256 chunks ~= 2 km of vertical strate.
// Human-scale defaults use 8 chunks = 64 m. This gives a 20 m+ vertical budget for
// cathedral rooms, tall shafts, and the surface sky cap without changing the lateral gate.
// 1 chunk = 32 voxels x 25cm = 8 m, so the maximum 256 chunks is ~= 2 km of vertical strate.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Dimensions", meta = (ClampMin = "1", ClampMax = "256"))
int32 StrateHeightInChunks = 4;
int32 StrateHeightInChunks = 8;
//=========================================================================
// BOUNDARY TRANSITION
@@ -38,11 +38,11 @@ struct VOXELFORGE_API FVoxelStrateFinePreviewSettings
{
// The default is deliberately one voxel: the pass exists to expose detail that step 4 cannot.
int32 SampleStep = 1;
// radius=64 gives a 128x128 XY ROI and stays below the default two-million-cell cap for the
// four-chunk fixture. The caller may re-centre it on the deterministic largest open space
// radius=64 gives a 128x128 XY ROI and stays below the default four-million-cell cap for the
// eight-chunk fixture. The caller may re-centre it on the deterministic largest open space
// point found by the coarse air flood fill; it is not inherently the strate midpoint.
int32 RadiusInVoxels = 64;
int32 MaxCells = 2000000;
int32 MaxCells = 4000000;
FVector2D CenterXY = FVector2D::ZeroVector;
int32 InteriorMarginVoxels = -1;
+158 -135
View File
@@ -446,10 +446,10 @@ struct VOXELFORGE_API FStrateGenerationParams
// the cave system instead of spraying disconnected noise pockets through the whole
// strate (the far-field "confetti"). 0 = unlimited (legacy unmasked behaviour).
// 16 → tight braiding right along rooms/tunnels
// 24 → braids + short noodle shortcuts (good default)
// 48+ → loose, wandering side-passages
// 64 → 16 m braids + side passages (human-scale default)
// 96+ → loose, wandering side-passages
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Worm Tunnels", meta = (ClampMin = "0.0"))
float WormNetworkRange = 24.0f;
float WormNetworkRange = 64.0f;
// ===== CAVE MORPHOLOGY (room-and-corridor) =====
//
@@ -465,12 +465,12 @@ struct VOXELFORGE_API FStrateGenerationParams
// Distance between room grid cells (in voxels).
// This controls room SPACING, not size. Larger = rooms further apart.
// 40 → dense room network, rooms nearly touching
// 80 → moderate density, corridors between rooms (good default)
// 120 → sparse rooms, long corridors
// 64 → dense room network
// 128 → 32 m cell spacing (human-scale default)
// 160 → sparse rooms, long corridors
// 200+→ isolated chambers with long tunnel treks
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Rooms")
float RoomSpacing = 80.0f;
float RoomSpacing = 128.0f;
// Probability of a room existing in each grid cell (0-1).
// Not every cell gets a room — this controls how many are filled.
@@ -483,28 +483,29 @@ struct VOXELFORGE_API FStrateGenerationParams
// Smallest possible room radius (in voxels).
// Small rooms feel like alcoves or nooks.
// 5-8 → small alcoves
// 10-15→ natural small chambers (good default)
// 20+ → even "small" rooms are spacious
// 8-12 → alcoves and small rooms
// 16 → 8 m diameter room (human-scale minimum)
// 24+ → spacious small rooms
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Rooms")
float MinRoomRadius = 10.0f;
float MinRoomRadius = 16.0f;
// Largest possible room radius (in voxels).
// Large rooms are cathedral chambers.
// 15-20→ moderate rooms
// 25-35→ large caverns (good default)
// 50+ → massive cathedral spaces
// 24-32→ moderate rooms
// 40 → 20 m diameter chamber (large-room default)
// 80+ → massive cathedral spaces
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Rooms")
float MaxRoomRadius = 30.0f;
float MaxRoomRadius = 40.0f;
// How vertically squished rooms are.
// 1.0 = perfect sphere (tall as wide).
// Lower = flatter, more horizontal chambers.
// 0.2-0.3 → very flat caverns (strate-like, wide and low)
// 0.4-0.5 → natural cave chambers (good default)
// 0.4-0.5 → natural cave chambers
// 0.85 → tall rooms (17 m diameter-height envelope at radius 40)
// 0.7-1.0 → tall, cathedral-like rooms
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Rooms", meta = (ClampMin = "0.1", ClampMax = "1.0"))
float RoomHeightRatio = 0.4f;
float RoomHeightRatio = 0.85f;
// Per-room floor flatness range. Each room hash-rolls a value in [Min, Max].
// FloorCutZ = RoomCenter.Z - RoomRadiusZ * roll
@@ -521,11 +522,11 @@ struct VOXELFORGE_API FStrateGenerationParams
// with navigable flat floors.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Rooms",
meta = (ClampMin = "0.0", ClampMax = "1.0"))
float RoomFloorCutMin = 1.0f;
float RoomFloorCutMin = 0.70f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Rooms",
meta = (ClampMin = "0.0", ClampMax = "1.0"))
float RoomFloorCutMax = 1.0f;
float RoomFloorCutMax = 0.90f;
// Large-scale floor undulation — how many voxels the floor plane rises and falls.
// This is NOT surface roughness (which adds rock texture). This shifts the entire
@@ -535,13 +536,14 @@ struct VOXELFORGE_API FStrateGenerationParams
//
// Only meaningful when RoomFloorCutMin < 1.0 (flat floor mode active).
//
// 0 → perfectly flat floor (default — use surface roughness for texture)
// 0 → perfectly flat floor
// 4 → 1 m variation (human-scale default; subtle beside a room)
// 2-4 → subtle floor variation, barely perceptible gradient
// 5-10 → clear hills and basins — interesting to walk across
// 15+ → dramatic terrain — slopes and drops within the room
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Rooms",
meta = (ClampMin = "0.0", EditCondition = "RoomFloorCutMin < 1.0"))
float FloorReliefStrength = 0.0f;
float FloorReliefStrength = 4.0f;
// How wide the floor hills are. Lower = broader, gentler undulations.
// 0.005 → very broad (one hill spans the whole room)
@@ -567,10 +569,11 @@ struct VOXELFORGE_API FStrateGenerationParams
// making it the natural hub of the strate's cave network.
// 0 → disabled (no guaranteed origin room)
// 15 → moderate hub room
// 20 → large central chamber (good default)
// 30+ → massive starting cavern
// 32 → large central chamber
// 48 → 24 m hub (three-chunk boss-space default)
// 64+ → massive starting cavern
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Rooms")
float OriginRoomRadius = 20.0f;
float OriginRoomRadius = 48.0f;
// Maximum number of backbone tunnels that can force-connect to the origin room.
// Without a limit, every room in the search area that picks origin as its nearest
@@ -588,18 +591,18 @@ struct VOXELFORGE_API FStrateGenerationParams
// ===== TUNNELS (connecting corridors between rooms) =====
// Smallest tunnel radius (in voxels). Creates tight squeeze corridors.
// 2-3 → claustrophobic crawlspaces
// 3-4 → tight but passable (good default)
// 5+ → even "narrow" tunnels are comfortable
// 4-5 → marginal passage
// 6 → 3 m bore (minimum comfortable round passage)
// 8 → 4 m bore (human-scale wide passage)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels")
float TunnelMinRadius = 3.0f;
float TunnelMinRadius = 6.0f;
// Largest tunnel radius (in voxels). Creates wide passages.
// 5-6 → comfortable walking tunnels
// 7-8 → wide passages (good default)
// 6 → 3 m bore (comfortable walking tunnel)
// 8 → 4 m bore (fight-space passage)
// 10+ → practically small rooms
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels")
float TunnelMaxRadius = 7.0f;
float TunnelMaxRadius = 8.0f;
// Probability that two nearby rooms are connected by a tunnel (0-1).
// 0.2 → sparse connections, mostly dead-end rooms
@@ -610,10 +613,11 @@ struct VOXELFORGE_API FStrateGenerationParams
// Maximum tunnel length (in voxels). Rooms further apart won't connect.
// 100 → only close neighbors
// 200 → moderate reach (good default)
// 200 → moderate reach
// 360 → 90 m, reaches the worst jittered neighbour at 32 m cell spacing
// 400+→ long-range connections possible
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels")
float MaxTunnelLength = 200.0f;
float MaxTunnelLength = 360.0f;
// How much tunnel paths curve (in voxels of sideways displacement).
// Without this, tunnels are straight lines between rooms.
@@ -622,10 +626,10 @@ struct VOXELFORGE_API FStrateGenerationParams
// Capped at 25% of tunnel length to prevent kinky short tunnels.
// 0 → perfectly straight tunnels (artificial look)
// 8-12→ gentle natural curves
// 15 → clearly winding passages (good default)
// 24 → 6 m lateral bend (human-scale default)
// 25+ → very curvy, meandering corridors
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels")
float TunnelWarpStrength = 15.0f;
float TunnelWarpStrength = 24.0f;
// Preference for horizontal connections over vertical (0-1).
// Real caves are mostly horizontal — vertical connections are rarer.
@@ -652,17 +656,17 @@ struct VOXELFORGE_API FStrateGenerationParams
// Combined with terrain ops (cliffs, terraces), this creates multi-level rooms
// where one tunnel exits at the upper ledge and another at the lower floor.
// 0.0 → all tunnels connect at room center height
// 0.5 → endpoints range from -50% to +50% of room height (good default)
// 0.25 → endpoints use a restrained quarter-height offset (default)
// 1.0 → full range — tunnels can enter near floor or ceiling
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float TunnelEndpointZOffset = 0.5f;
float TunnelEndpointZOffset = 0.25f;
// ===== SDF BLEND (junction smoothness) =====
// Smooth union blend radius. Controls how rounded the junctions
// are where rooms meet tunnels (or rooms meet rooms).
// 1-2 → sharp, angular junctions
// 3-5 → natural rounded junctions (good default)
// 4 → 1 m rounded junction (default)
// 8+ → very blobby, organic blending
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Blend")
float SDFBlendRadius = 4.0f;
@@ -685,10 +689,10 @@ struct VOXELFORGE_API FStrateGenerationParams
// This controls the overall organic distortion of the cave skeleton.
// 0 → disabled — pristine geometric rooms and straight tunnels
// 4-6 → subtle natural variation (rooms slightly irregular)
// 8-12→ clearly organic caves, tunnels visibly curve (good default)
// 16 → 4 m skeleton distortion (human-scale default)
// 16+ → heavily distorted, surreal cave shapes
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Cave Warp")
float CaveWarpStrength = 8.0f;
float CaveWarpStrength = 16.0f;
// Frequency of the warp noise. Lower = broader, smoother bends.
// This controls the SCALE of the distortion.
@@ -708,10 +712,11 @@ struct VOXELFORGE_API FStrateGenerationParams
// How strong the roughness is (in voxels of displacement).
// 0 → perfectly smooth SDF shapes
// 2-3 → subtle rocky texture
// 4-6 → natural rocky cave surfaces (good default)
// 2 → 0.5 m wall texture (human-scale default; bounded reach 0.94 m)
// 4-6 → stronger natural rocky cave surfaces
// 8+ → very rough, jagged rock
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Roughness")
float SurfaceRoughness = 5.0f;
float SurfaceRoughness = 2.0f;
// Frequency of the roughness noise. Higher = finer details.
// 0.05 → large rocky features (boulders, ledges)
@@ -758,13 +763,14 @@ struct VOXELFORGE_API FStrateGenerationParams
// Only applied below room center Z (fades to 0 at center, strongest at floor).
// Does NOT affect the open space above room center or walls/ceiling.
//
// 0 → disabled — full roughness on all surfaces (default)
// 0 → disabled — full roughness on all surfaces
// 2-3 → subtle flattening, floor still has character
// 4 → 1 m of floor bias (human-scale default)
// 4-6 → noticeably flatter floor while keeping wall/ceiling rough
// 8+ → near-flat floor (good for navigable caves)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Roughness",
meta = (ClampMin = "0.0"))
float FloorBias = 0.0f;
float FloorBias = 4.0f;
// ===== TERRAIN OPERATIONS =====
//
@@ -837,8 +843,9 @@ struct VOXELFORGE_API FStrateGenerationParams
float OverhangStrength = 0.0f;
// How far overhangs protrude from walls (in voxels of density added).
// 2-3 → small, 5-8 → natural, 10+ → dramatic
float OverhangDepth = 5.0f;
// 8 → 2 m optional shelf (human-scale default)
// 16+ → dramatic
float OverhangDepth = 8.0f;
// Frequency of the overhang noise. Z freq is auto 5x lower for horizontal features.
// 0.03 → large rare, 0.06 → moderate, 0.1+ → many small
@@ -919,8 +926,9 @@ struct VOXELFORGE_API FStrateGenerationParams
// 0.06 → moderate (good for cathedral strates)
// 0.1 → frequent bridges
float ArchDensity = 0.0f;
float ArchMinRadius = 3.0f;
float ArchMaxRadius = 6.0f;
// Radius of an optional bridge tube: 1.5-3 m at the human-scale defaults.
float ArchMinRadius = 6.0f;
float ArchMaxRadius = 12.0f;
// ===== COLUMNS / PILLARS =====
//
@@ -939,8 +947,8 @@ struct VOXELFORGE_API FStrateGenerationParams
// 0.15 → moderate (good for cathedral strates)
// 0.3 → dense forest of pillars
float ColumnDensity = 0.0f;
float ColumnMinRadius = 2.0f;
float ColumnMaxRadius = 5.0f;
float ColumnMinRadius = 4.0f;
float ColumnMaxRadius = 8.0f;
// ===== PITS / VERTICAL SHAFTS =====
//
@@ -960,24 +968,25 @@ struct VOXELFORGE_API FStrateGenerationParams
// 0.08 → moderate (good for deep strates with vertical gameplay)
// 0.15 → frequent pits (chaotic, maze-like)
float PitDensity = 0.0f;
float PitMinRadius = 4.0f;
float PitMaxRadius = 10.0f;
float PitDepth = 25.0f;
float PitMinRadius = 8.0f;
float PitMaxRadius = 16.0f;
float PitDepth = 32.0f;
// ----- Chimney / Shaft (upward voids — inverse of pits) -----
// Chimneys: narrow vertical tubes piercing upward from cave ceilings.
float ChimneyDensity = 0.0f;
float ChimneyMinRadius = 2.0f;
float ChimneyMaxRadius = 5.0f;
float ChimneyHeight = 20.0f;
float ChimneyMinRadius = 4.0f;
float ChimneyMaxRadius = 8.0f;
float ChimneyHeight = 32.0f;
// ----- Dome (hemispherical chamber ceilings) -----
// Domes: hemispherical chamber ceilings for cathedral-like feel.
// Domes: hemispherical chamber ceilings for cathedral-like feel. The default 16-32 voxel
// radius is a 4-8 m optional ceiling feature; the surrounding room remains the main volume.
float DomeDensity = 0.0f;
float DomeMinRadius = 8.0f;
float DomeMaxRadius = 15.0f;
float DomeMinRadius = 16.0f;
float DomeMaxRadius = 32.0f;
float DomeHeightRatio = 0.8f;
// ----- Pinch / Bottleneck (passage narrowing) -----
@@ -985,7 +994,7 @@ struct VOXELFORGE_API FStrateGenerationParams
// Pinch: passage narrowing for bottlenecks/chokepoints.
float PinchDensity = 0.0f;
float PinchStrength = 5.0f;
float PinchLength = 12.0f;
float PinchLength = 24.0f;
// ----- Strate boundary sealing -----
@@ -1129,7 +1138,7 @@ struct VOXELFORGE_API FSlabGenerationParams
// 0.0 = at the very bottom of the strate, 1.0 = at the very top.
// Keep this well below CeilingRelativeHeight or you get no open space.
// 0.1-0.2 → floor is near the bottom (lots of headroom)
// 0.25 → floor at 25% height (good default)
// 0.25 → floor at 16 m in a 64 m strate (human-scale default)
// 0.4+ → very low ceiling; claustrophobic plains
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Slab|Shape",
meta = (ClampMin = "0.0", ClampMax = "0.95"))
@@ -1138,12 +1147,12 @@ struct VOXELFORGE_API FSlabGenerationParams
// Where the ceiling surface sits, relative to this strate's height.
// Must be above FloorRelativeHeight. The difference determines how tall
// the open void is.
// 0.6 → moderate ceiling height (tight plains)
// 0.8 → tall open space (good default for FlatPlain)
// 0.6 → ceiling at 38.4 m in a 64 m strate (22.4 m open span)
// 0.8 → tall open space
// 0.9+ → nearly the full strate height is open (cavernous plains)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Slab|Shape",
meta = (ClampMin = "0.05", ClampMax = "1.0"))
float CeilingRelativeHeight = 0.80f;
float CeilingRelativeHeight = 0.60f;
// ===== FLOOR ROUGHNESS =====
// Perlin-based displacement of the floor surface up/down.
@@ -1152,7 +1161,8 @@ struct VOXELFORGE_API FSlabGenerationParams
// How many voxels the floor surface can shift up or down.
// 0 → perfectly flat floor (artificial, but dramatic)
// 3-5 → subtle rolling ground (good for FlatPlain)
// 4 → 1 m rolling ground displacement (human-scale default)
// 3-5 → subtle rolling ground
// 8+ → significant hills, deep valleys
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Slab|Floor")
float FloorRoughness = 4.0f;
@@ -1172,8 +1182,8 @@ struct VOXELFORGE_API FSlabGenerationParams
// How far ceiling formations hang down into the void (in voxels).
// 0 → flat ceiling (no formations)
// 4-6 → subtle bumps (FlatPlain default)
// 10-15 → significant formations (CrystalChamber default)
// 6 → 1.5 m downward formations (FlatPlain/Crystal default)
// 10-15 → significant formations
// 20+ → dramatic columns reaching toward the floor
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Slab|Ceiling")
float CeilingRoughness = 6.0f;
@@ -1205,22 +1215,22 @@ struct VOXELFORGE_API FSlabGenerationParams
// Smallest column radius (in voxels).
// 1-2 → slender needle-like pillars
// 3-4 → narrow columns (good default)
// 8 → 2 m radius / 4 m diameter (human-scale minimum)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Slab|Columns")
float ColumnMinRadius = 2.0f;
float ColumnMinRadius = 8.0f;
// Largest column radius (in voxels).
// 6-8 → natural stone pillars (good default)
// 12-15 → massive trunks
// 8-16 → 2-4 m optional pillars
// 24+ → massive trunks
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Slab|Columns")
float ColumnMaxRadius = 7.0f;
float ColumnMaxRadius = 16.0f;
// Hash grid cell size (higher = columns further apart).
// 30 → dense grid, many candidate slots (most empty due to ColumnDensity)
// 60 → moderate spacing (good default)
// 96 → 24 m spacing (human-scale default)
// 100+ → only occasional columns
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Slab|Columns")
float ColumnSpacing = 60.0f;
float ColumnSpacing = 96.0f;
// ===== BOUNDARY & DENSITY =====
@@ -1265,14 +1275,14 @@ struct VOXELFORGE_API FMazeGenerationParams
GENERATED_BODY()
// Lattice cell size in voxels. Smaller = tighter, more claustrophobic maze.
// 25-35 → very tight warren · 40 → classic maze (default) · 60+ → roomy
// 32 → compact maze cell · 64 → 16 m fight-space cell (default) · 96+ → roomy
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Maze", meta = (ClampMin = "8.0"))
float CellSize = 40.0f;
float CellSize = 64.0f;
// Corridor tube radius in voxels. Keep well below CellSize/2 to leave walls.
// 3 → crawlspace · 4 → walkable (default) · 6 → wide halls
// 4-5 → marginal passage · 8 → 2 m radius / 4 m bore (default) · 10+ → broad hall
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Maze", meta = (ClampMin = "1.0"))
float CorridorRadius = 4.0f;
float CorridorRadius = 8.0f;
// Probability that a horizontal edge between adjacent cells is open (0-1).
// Lower = more dead ends and a more maze-like feel; higher = more open/connected.
@@ -1321,12 +1331,12 @@ struct VOXELFORGE_API FSurfaceGenerationParams
// Mean ground height as a fraction of the strate height (0 = bottom, 1 = top).
// The terrain heightfield varies around this baseline by ElevationRange.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Shape", meta = (ClampMin = "0.05", ClampMax = "0.9"))
float BaseGroundRelative = 0.30f;
float BaseGroundRelative = 0.25f;
// Total vertical amplitude of the terrain heightfield in voxels (peak-to-trough-ish).
// 20 → gentle plains · 60 → hills + valleys (default) · 120+ → tall mountains
// 20 → gentle plains · 80 → 20 m hills + valleys (default) · 120+ → tall mountains
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Shape", meta = (ClampMin = "0.0"))
float ElevationRange = 60.0f;
float ElevationRange = 80.0f;
// Low-frequency "continent" noise — broad landmasses and basins.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Shape")
@@ -1345,9 +1355,10 @@ struct VOXELFORGE_API FSurfaceGenerationParams
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Shape")
float DetailFrequency = 0.04f;
// Small-scale surface roughness in voxels (rocks, bumps).
// Small-scale surface roughness in voxels (rocks, bumps). The default 2 voxels is 0.5 m;
// its proved 0.94 m noise reach remains small beside the 20 m terrain relief.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Shape", meta = (ClampMin = "0.0"))
float SurfaceRoughness = 3.0f;
float SurfaceRoughness = 2.0f;
// ----- Macro relief & landforms -----
@@ -1355,7 +1366,7 @@ struct VOXELFORGE_API FSurfaceGenerationParams
// sampling continents/mountains. Bends straight coastlines and ridgelines into winding,
// organic landforms. 0 = no warp (axis-aligned blobby noise, the old look).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Macro", meta = (ClampMin = "0.0"))
float HeightWarpStrength = 35.0f;
float HeightWarpStrength = 48.0f;
// Frequency of the domain-warp noise. Lower = broader, sweeping bends.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Macro")
@@ -1382,9 +1393,10 @@ struct VOXELFORGE_API FSurfaceGenerationParams
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Macro", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float TerraceStrength = 0.0f;
// Height of each terrace step in voxels (when TerraceStrength > 0).
// Height of each terrace step in voxels (when TerraceStrength > 0). The 8-voxel default is
// a 2 m optional landform step; TerraceStrength remains off by default.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Macro", meta = (ClampMin = "1.0"))
float TerraceHeight = 12.0f;
float TerraceHeight = 8.0f;
// Terrace edge sharpness (0-1). 0 = soft rounded steps; 1 = crisp flat mesas with near-
// vertical risers. Only matters when TerraceStrength > 0. (F20 — the plateau tops flatten
@@ -1405,9 +1417,9 @@ struct VOXELFORGE_API FSurfaceGenerationParams
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Ops", meta = (ClampMin = "0.0"))
float LayerLineDepth = 0.0f;
// Vertical spacing between layer lines in voxels (band period).
// Vertical spacing between layer lines in voxels (band period). The 8-voxel default is 2 m.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Ops", meta = (ClampMin = "1.0"))
float LayerLineSpacing = 4.0f;
float LayerLineSpacing = 8.0f;
// Cliff STEEPENING (0-1 master): where the surface is already STEEP (slope > threshold),
// push the height away from the local mean so gentle slopes become sheer walls / canyon
@@ -1448,13 +1460,13 @@ struct VOXELFORGE_API FSurfaceGenerationParams
// at which the terrain gradient is measured (so a spot over the void can "see" the cliff). Bigger =
// deeper overhangs reaching further out (and a bit more cost). ~8-20.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Ops", meta = (ClampMin = "0.0"))
float OverhangReach = 12.0f;
float OverhangReach = 16.0f;
// Vertical HEIGHT (voxels) of the overhang zone above the local ground — where the shelf sits above
// the ground/void directly under it, AND the band ClassifyTile treats as ambiguous (so it never holes
// a trivially-skipped tile). Larger = taller/higher shelves but more woken air tiles near cliffs.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Ops", meta = (ClampMin = "0.0"))
float OverhangHeight = 20.0f;
float OverhangHeight = 24.0f;
// Horizontal frequency of the shelf-shape noise (breaks the reach up so shelves are ragged, not a
// uniform lip). Lower = broader, smoother shelves.
@@ -1480,9 +1492,9 @@ struct VOXELFORGE_API FSurfaceGenerationParams
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Water", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float WaterLevelRelative = 0.27f;
// Width (voxels) of the flattened beach/shore band around the water line.
// Width (voxels) of the flattened beach/shore band around the water line. The default is 3 m.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Water", meta = (ClampMin = "0.0"))
float BeachWidth = 8.0f;
float BeachWidth = 12.0f;
// ----- Sky cap & boundary -----
@@ -1491,7 +1503,7 @@ struct VOXELFORGE_API FSurfaceGenerationParams
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Sky", meta = (ClampMin = "0.3", ClampMax = "1.0"))
float CeilingRelative = 0.95f;
// Downward bumpiness of the sky-cap ceiling (voxels). 0 = flat ceiling.
// Downward bumpiness of the sky-cap ceiling (voxels). 0 = flat ceiling; 6 = 1.5 m detail.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Surface|Sky", meta = (ClampMin = "0.0"))
float CeilingRoughness = 6.0f;
@@ -1564,39 +1576,41 @@ struct VOXELFORGE_API FVerticalShaftParams
GENERATED_BODY()
// Hash-grid cell size for shaft XY placement (voxels). Larger = shafts further apart.
// 80 voxels = 20 m, leaving room for a 4-7 m diameter shaft and a fight lane.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Shafts", meta = (ClampMin = "10.0"))
float ShaftSpacing = 55.0f;
float ShaftSpacing = 80.0f;
// Probability a grid cell contains a shaft (0-1).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Shafts", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float ShaftDensity = 0.6f;
// Shaft radius range (voxels).
// Shaft radius range (voxels): 4-7 m diameter at the human-scale default.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Shafts", meta = (ClampMin = "1.0"))
float ShaftMinRadius = 5.0f;
float ShaftMinRadius = 8.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Shafts", meta = (ClampMin = "1.0"))
float ShaftMaxRadius = 11.0f;
float ShaftMaxRadius = 14.0f;
// Chance an adjacent pair of shafts is joined by a horizontal connector tunnel (0-1).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Shafts", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float CrossConnectChance = 0.35f;
// Radius of horizontal connector tunnels (voxels).
// Radius of horizontal connector tunnels (voxels): 1.5 m radius / 3 m bore by default.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Shafts", meta = (ClampMin = "1.0"))
float ConnectorRadius = 4.0f;
float ConnectorRadius = 6.0f;
// Vertical spacing of ledges inside shafts (voxels). 0 = no ledges (sheer drops).
// Vertical spacing of ledges inside shafts (voxels). 32 = 8 m landing interval;
// 0 = no ledges (sheer drops).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Shafts", meta = (ClampMin = "0.0"))
float LedgeSpacing = 24.0f;
float LedgeSpacing = 32.0f;
// How far ledges intrude into the shaft (voxels of solid added).
// How far ledges intrude into the shaft (voxels of solid added). 4 = 1 m by default.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Shafts", meta = (ClampMin = "0.0"))
float LedgeDepth = 3.0f;
float LedgeDepth = 4.0f;
// Small-scale wall roughness (voxels).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Shafts", meta = (ClampMin = "0.0"))
float SurfaceRoughness = 3.0f;
float SurfaceRoughness = 2.0f;
// Solid shell thickness at strate top/bottom (voxels).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Shafts", meta = (ClampMin = "0.0"))
@@ -1626,25 +1640,26 @@ struct VOXELFORGE_API FFloatingIslandParams
{
GENERATED_BODY()
// Hash-grid cell size for island placement (voxels). Larger = islands further apart.
// Hash-grid cell size for island placement (voxels). 112 = 28 m, while the largest default
// island is 24 m across, leaving a 2 m centre-to-edge gap.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Islands", meta = (ClampMin = "20.0"))
float IslandSpacing = 95.0f;
float IslandSpacing = 112.0f;
// Probability a grid cell contains an island (0-1).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Islands", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float IslandDensity = 0.5f;
// Island horizontal radius range (voxels).
// Island horizontal radius range (voxels): 12-24 m landmass diameters by default.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Islands", meta = (ClampMin = "2.0"))
float IslandMinRadius = 18.0f;
float IslandMinRadius = 24.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Islands", meta = (ClampMin = "2.0"))
float IslandMaxRadius = 42.0f;
float IslandMaxRadius = 48.0f;
// Island vertical thickness as a fraction of its horizontal radius.
// 0.4 → thin plates · 0.7 → chunky (default) · 1.0 → near-spherical
// 0.4 → thin plates · 0.6 → substantial landmass (default) · 1.0 → near-spherical
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Islands", meta = (ClampMin = "0.1", ClampMax = "1.5"))
float ThicknessRatio = 0.7f;
float ThicknessRatio = 0.6f;
// How much islands scatter vertically within the void (0 = all mid-height, 1 = full spread).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Islands", meta = (ClampMin = "0.0", ClampMax = "1.0"))
@@ -1656,11 +1671,11 @@ struct VOXELFORGE_API FFloatingIslandParams
// Surface roughness on island shells (voxels) — craggy undersides, bumpy tops.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Islands", meta = (ClampMin = "0.0"))
float SurfaceRoughness = 4.0f;
float SurfaceRoughness = 3.0f;
// SmoothMin blend radius for merging overlapping islands (voxels).
// SmoothMin blend radius for merging overlapping islands (voxels): 1.5 m by default.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Islands", meta = (ClampMin = "0.0"))
float SDFBlendRadius = 5.0f;
float SDFBlendRadius = 6.0f;
// Solid shell thickness at strate top/bottom (voxels).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Islands", meta = (ClampMin = "0.0"))
@@ -1696,27 +1711,30 @@ struct VOXELFORGE_API FStrateDisturbanceParams
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Disturbance|Chasms", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float ChasmDensity = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Disturbance|Chasms", meta = (ClampMin = "20.0"))
float ChasmSpacing = 170.0f;
// 192 voxels = 48 m between optional chasm sites.
float ChasmSpacing = 192.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Disturbance|Chasms", meta = (ClampMin = "1.0"))
float ChasmRadius = 14.0f;
float ChasmRadius = 24.0f;
// --- BRIDGES: horizontal solid spans across open space ---
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Disturbance|Bridges", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float BridgeDensity = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Disturbance|Bridges", meta = (ClampMin = "20.0"))
float BridgeSpacing = 120.0f;
// 128 voxels = 32 m between optional bridge sites.
float BridgeSpacing = 128.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Disturbance|Bridges", meta = (ClampMin = "1.0"))
float BridgeRadius = 5.0f;
float BridgeRadius = 8.0f;
// --- RIDGES: thin solid blades rising from the floor ---
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Disturbance|Ridges", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float RidgeDensity = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Disturbance|Ridges", meta = (ClampMin = "20.0"))
float RidgeSpacing = 110.0f;
// 128 voxels = 32 m between optional ridge sites.
float RidgeSpacing = 128.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Disturbance|Ridges", meta = (ClampMin = "0.0"))
float RidgeHeight = 30.0f;
float RidgeHeight = 32.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Disturbance|Ridges", meta = (ClampMin = "1.0"))
float RidgeThickness = 6.0f;
float RidgeThickness = 8.0f;
// Rock solidity used to scale carve/fill strength. Match the strate's BaseDensity.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Disturbance")
@@ -1768,33 +1786,36 @@ struct VOXELFORGE_API FStratePassageConfig
// ----- WIDTH (tapers along the length) -----
// Radius at the two mouths (entry/exit) and at the middle. Equal = uniform tube;
// The defaults are 2 m radius / 4 m bore at mouths and 1.5 m radius / 3 m bore in the middle.
// Mouth > Mid = chambers at the ends with a squeeze between; Mid > Mouth = a bulge.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Width", meta = (ClampMin = "1.0"))
float MouthRadius = 6.0f;
float MouthRadius = 8.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Width", meta = (ClampMin = "1.0"))
float MidRadius = 4.0f;
float MidRadius = 6.0f;
// ----- LENGTH -----
// How far the tunnel reaches INTO each strate (voxels). Auto-capped to the interior.
// How far the tunnel reaches INTO each strate (voxels). Auto-capped to the interior;
// the defaults span 8-24 m.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Length", meta = (ClampMin = "8.0"))
float ReachMin = 40.0f;
float ReachMin = 32.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Length", meta = (ClampMin = "8.0"))
float ReachMax = 90.0f;
float ReachMax = 96.0f;
// ----- PLACEMENT -----
// Horizontal distance range from the (0,0) spine (voxels) where tunnels may appear.
// Horizontal distance range from the (0,0) spine (voxels) where tunnels may appear;
// the defaults are 16-48 m.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Placement", meta = (ClampMin = "0.0"))
float DistanceMin = 60.0f;
float DistanceMin = 64.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Placement", meta = (ClampMin = "0.0"))
float DistanceMax = 200.0f;
float DistanceMax = 192.0f;
// ----- SHAPE detail -----
// Worm: max sideways excursion from the axis (voxels). 0 = straight even in Worm style.
// Worm: max sideways excursion from the axis (voxels). 24 = 6 m; 0 = straight even in Worm style.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Shape", meta = (ClampMin = "0.0"))
float Wander = 15.0f;
float Wander = 24.0f;
// Path resolution (control points). Higher = smoother curves; lower = more faceted /
// cheaper. The worm makes several bends, so keep this reasonably high for smoothness.
@@ -1805,19 +1826,21 @@ struct VOXELFORGE_API FStratePassageConfig
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Shape", meta = (ClampMin = "0.0"))
float VerticalWobble = 0.0f;
// Spiral style: helix radius (voxels) and number of full turns over the descent.
// Spiral style: helix radius (voxels) and number of full turns over the descent. The default
// radius is 6 m.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Spiral", meta = (ClampMin = "1.0"))
float SpiralRadius = 16.0f;
float SpiralRadius = 24.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Spiral", meta = (ClampMin = "0.25"))
float SpiralTurns = 2.0f;
// Cascading style: number of ledge+drop steps, and how far each ledge runs (voxels).
// Cascading style: number of ledge+drop steps, and how far each ledge runs (voxels). The
// default ledge run is 2 m.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Cascade", meta = (ClampMin = "1", ClampMax = "16"))
int32 CascadeSteps = 4;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Cascade", meta = (ClampMin = "0.0"))
float CascadeLedge = 14.0f;
float CascadeLedge = 8.0f;
};
//=============================================================================