feat(passages): §6.5 landings -- you arrive somewhere you can stand

The last unbuilt Tier 1 item. A passage was a bore through rock with no
floor inside the walkable angle, so its mouth was not a standing position
and neither was anything near it -- the departure mouth measured a FOUR-CELL
player-fit component. You arrived and there was nowhere to stand.

Both ends of all 7 passages now open into a landing, sized from the body:
  capsule diameter 2.72 vox      floor >= 12 vox (3 m) to stand and turn
  capsule height 7.04 vox        room >= 12 vox tall, stock mouth gives 18 (4.5 m)
  3-voxel floor slab, tube tangent to it -- no lip to climb out of
  connector 5-vox radius: 2.5 m clear width, 3 m clear height
  ramps over 44 deg get a deterministic level dog-leg

⭐ THE MOUTHS ARE NO LONGER SPECKS:
  archetype        components      largest      arrival-departure
  Maze             309 -> 340      176 -> 3210    43-81  -> 3210-3210
  TunnelNetwork    467 -> 227     1292 -> 3841     2-1   -> 3841-269
  Underwater        86 -> 117       35 -> 3838     3-11  -> 3838-267
  FlatPlain         45 ->  11      259 -> 2130     1-1   ->  268-273
  CrystalChamber   197 ->  90       24 ->  286     1-1   ->  286-274
  FloatingIslands    2 ->   1        3 ->   72     1-1   ->   72-72

Law 1/8 -> 2/7 connected (1 refused on MaxCells), and critically ZERO
connections now run through a four-cell pocket.

Every landing floor was verified against the FINAL density, after all posts:
14/14 pass the 44 deg slope bracket, worst room gradient .059, worst
connector .522. 14/14 deterministic root joins. 0 seal failures.
0 box-verdict violations across 82 proved + 174 mixed, 109,142 voxels.
Op-stack bit-identical, order-independence green over 10 slots / 31
passages / 3 pool orders. Cache 0.665 us rebuild, 0.646 us hot.

Post order: origin spine -> vertical seal -> passage tube + landing floor
-> XY edge seal. Landings clamp 4 voxels inside the vertical seal; the XY
seal stays last and wins at world edges.

⚠️ ARCHITECTURAL NOTE, not a detail: landings join a deterministic annular
ROOT HUB at (0,0), built identically for every endpoint -- no nearest-
component search, no passage ordering. That is what makes the join
guaranteed rather than likely, and it extends the (0,0) spine concept from
a landmark into a structural network. It also means routes tend to pass
through the centre, which is a gameplay consequence worth Jahni's eye.

⚠️ AND IT DOES NOT CLOSE THE LAW. Codex is explicit: the landings guarantee
membership in the new root network, NOT in the archetype's pre-existing fit
component. The whole-strate Maze check still shows a 71.9-voxel gap between
1,235 and 2,938-cell components. So the native corridor networks remain
disconnected from the root network in places. Real, separate, next.

52 build-and-measure cycles, ~9 hours, finding real geometry bugs on the
way (a landing floor overlapping the ramp cells and hiding a floor
discontinuity; the disturbance pass refilling connector headroom).

Build clean, 32/32 success, 0 failed. Showcase regenerated.

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 23:55:47 +02:00
1 parent e4e159f0cd
commit 413c15de24
14 files changed
+1942 -186

No files matched your search

+78 -31
View File
@@ -48,13 +48,17 @@ 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 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 historical production definition, season defaults, and owner-facing showcase use 8 chunks. The historical
pre-tree production-height sweep exhausted 256 rolls and found **0/8** complete hard-gate survivors; pre-tree production-height sweep exhausted 256 rolls and found **0/8** complete hard-gate survivors;
its 7 fine renders, 1 refusal, and 1 blank are retained as the scale baseline. The final its 7 fine renders, 1 refusal, and 1 blank are retained as the scale baseline. The historical final
`VoxelForge` run (`VoxelForgeFullMazeTreeFinal3`) recorded **30 clean successes, 2 warning-bearing, `VoxelForge` run (`VoxelForgeFullMazeTreeFinal3`) recorded **30 clean successes, 2 warning-bearing,
0 failures, 0 not-run** in **2,523.978 s**; its regenerated showcase produced **6 fine renders, 2 0 failures, 0 not-run** in **2,523.978 s**; its regenerated showcase produced **6 fine renders, 2
refusals, and 0 blanks**. All box-verdict violation counts were **0**, `WorldRadiusVoxels` stayed refusals, and 0 blanks**. All box-verdict violation counts were **0**, `WorldRadiusVoxels` stayed
**0**, lateral regions stayed gated, and operator-stack equivalence paths remained bit-identical. **0**, lateral regions stayed gated, and operator-stack equivalence paths remained bit-identical.
The two authored-corpus relation findings (2/12) are conservative experiment-envelope findings, The 2026-09-06 landing validation (`VoxelForgeLandingFinal5`) recorded **30 clean successes, 2
not box-verdict violations. warning-bearing successes, 0 failures, 0 not-run** across 32 tests in **3,607.746 s**. The strict
`Composer.Promotion` assertions remain intact and passed; no assertion was weakened. The landing,
connectivity, seal, box-soundness, determinism, and equivalence tests passed. The two warnings are
the known gated lateral-region result and the known TunnelNetwork outside-gate sample count; they
are not landing or box-verdict violations.
### 8.1 Archetypes (`ECaveGeneratorType`, VoxelStrateTypes.h) ### 8.1 Archetypes (`ECaveGeneratorType`, VoxelStrateTypes.h)
Each archetype has its own param `USTRUCT` (on `UVoxelStrateDefinition`, EditCondition-gated Each archetype has its own param `USTRUCT` (on `UVoxelStrateDefinition`, EditCondition-gated
@@ -72,7 +76,7 @@ by `GeneratorType`) and its own density function in `VoxelGenerator.cpp`, dispat
| Underwater | `FStrateGenerationParams` + water | (reuses `GetDensityWithParams`) | tunnel rock + high water table | | Underwater | `FStrateGenerationParams` + water | (reuses `GetDensityWithParams`) | tunnel rock + high water table |
All density fns share the convention: internal **positive=solid**, apply origin spine → All density fns share the convention: internal **positive=solid**, apply origin spine →
vertical boundary seal → inter-strate passages → XY edge seal, then `return -Density` (MC: vertical boundary seal → inter-strate passage tube + landing/floor → XY edge seal, then `return -Density` (MC:
negative=solid). The final MC-facing edge pass in `GetDensityAt` is repeated after disturbances so negative=solid). The final MC-facing edge pass in `GetDensityAt` is repeated after disturbances so
the global rim cannot be reopened by a post-process; the diff layer remains the explicit player the global rim cannot be reopened by a post-process; the diff layer remains the explicit player
override. override.
@@ -99,7 +103,7 @@ internal densities**, never their parameter vectors: a shaft spacing and a slab
commensurable. Internal density is positive solid; the parent is negated once at the MC boundary. commensurable. Internal density is positive solid; the parent is negated once at the MC boundary.
Region cores are built without structural posts. `FLateralRegionBlendOp` resolves the creative Region cores are built without structural posts. `FLateralRegionBlendOp` resolves the creative
field, then one parent appends the global spine, vertical seal, passage carve, and XY edge seal in field, then one parent appends the global spine, vertical seal, passage carve/landing floor, and XY edge seal in
the existing fixed order. No region can disable or duplicate those posts. The manifest is an the existing fixed order. No region can disable or duplicate those posts. The manifest is an
offline artifact and the editor hand-off copies it into a worker-local per-chunk cache: the chunk offline artifact and the editor hand-off copies it into a worker-local per-chunk cache: the chunk
prepares a small lattice-site window plus an integer XY query grid, so ordinary voxel evaluation is prepares a small lattice-site window plus an integer XY query grid, so ordinary voxel evaluation is
@@ -115,12 +119,13 @@ that equal all-air/all-solid signs are safe, but no cross-region uniformity is i
single sample. The same key is checked when a season manifest is loaded, and `WorldRadiusVoxels` single sample. The same key is checked when a season manifest is loaded, and `WorldRadiusVoxels`
remains zero during offline materialisation. remains zero during offline materialisation.
This mechanism is implemented and instrumented, but not ready to promote: the seam-specific This mechanism is implemented and instrumented, but not ready to promote: the pre-landing
16-seed passage-mouth audit currently reports **9/16** connected opposite-region pairs, while the 16-seed passage-mouth audit reported **9/16** connected opposite-region pairs. After the landing
exact region-zero controls report **11/16**. The same cases are **16/16 valid, non-vacuous, and pass, the current audit reports **4/16**, while the exact region-zero controls also report **4/16**.
above the 0.50 largest-component survival threshold** before the law gate. No corridor or The same cases are **16/16 valid, non-vacuous, and above the 0.50 largest-component survival
threshold tuning was added. The failed primordial-law gate is therefore retained as a design threshold** before the law gate. No corridor or threshold tuning was added. The failed primordial-law
blocker; see `COMPOSER-NOTES.md §3.2b` and the validation log in §10. gate is therefore retained as a design blocker; see `COMPOSER-NOTES.md §3.2b` and the validation log
in §10.
⚠️ **The `switch` above is no longer the only density path.** All 8 archetypes also exist as ⚠️ **The `switch` above is no longer the only density path.** All 8 archetypes also exist as
**operator stacks**, selected per strate by **operator stacks**, selected per strate by
@@ -173,12 +178,14 @@ idiom as the vertical seal; at and beyond `R` it forces positive `BaseDensity`.
negates once for marching cubes, so the externally visible sealed density is negative (solid). negates once for marching cubes, so the externally visible sealed density is negative (solid).
This is the fourth structural post and is appended automatically by This is the fourth structural post and is appended automatically by
`FVoxelOpStack::AppendStructuralPost`, after the spine, vertical seal, and passage carve. Passage `FVoxelOpStack::AppendStructuralPost`, after the spine, vertical seal, and passage tube + landing
carving deliberately remains before the edge seal: passages may pierce a vertical strate seal, but floor. Passage carving deliberately remains before the edge seal: the tube is the intentional
a passage whose mouth or body reaches the XY rim is overwritten by the edge force and cannot open a vertical-boundary crossing, while each landing/connector is clamped inside the strate's vertical
hole through the world boundary. The legacy density functions apply the same post in the same seal. A passage whose mouth, landing, or body reaches the XY rim is overwritten by the edge force
order. `GetDensityAt` reapplies the MC-facing form after disturbances as a backstop for out-of-layout and cannot open a hole through the world boundary. The legacy density functions apply the same post
air and any disturbance carve; player diff edits still run last by design and remain authoritative. in the same order. `GetDensityAt` reapplies the MC-facing landing air/floor after disturbances as an
idempotent support backstop, before the final XY seal; player diff edits still run last by design
and remain authoritative.
`FXYEdgeSealOp` is `IsXYPure() == true`, uses only a squared radial early-out in the common interior `FXYEdgeSealOp` is `IsXYPure() == true`, uses only a squared radial early-out in the common interior
case, and returns `AllSolid` from `ClassifyBox` only when the closest point of the whole box is at case, and returns `AllSolid` from `ClassifyBox` only when the closest point of the whole box is at
@@ -471,14 +478,16 @@ with a flat-top envelope → organic squirm (NOT a 1D zigzag, NOT a same-freq 2-
`EvaluateModifierSDF` (per voxel) first builds a `thread_local` **per-chunk shortlist** of passages `EvaluateModifierSDF` (per voxel) first builds a `thread_local` **per-chunk shortlist** of passages
whose bounds reach this chunk (rebuilt on chunk change / `PassagesVersion` bump) — chunks with no whose bounds reach this chunk (rebuilt on chunk change / `PassagesVersion` bump) — chunks with no
passage near return `FLT_MAX` immediately — then **bounding-sphere-culls** each shortlisted passage passage near return `FLT_MAX` immediately — then **bounding-sphere-culls** each shortlisted passage
(`FVoxelPassage::BoundCenter/BoundRadiusSq`). Both are perf-critical (§8.10). The (0,0) surface entry (`FVoxelPassage::BoundCenter/BoundRadiusSq`). The same shortlist serves the tube, landing SDF, and
is a simple straight tube. Placement, reach, and shape draws for inter-strate passages are keyed by landing-floor membership; no source query, flood fill, or topology search is in the voxel loop. Both
`(seed, upper-strate index, connection index, per-value salt)` through `VoxelHash`, so one passage's are perf-critical (§8.10). The (0,0) surface entry is a simple straight tube. Placement, reach, and
connection count cannot shift any later passage. Global passage settings were removed from shape draws for inter-strate passages are keyed by `(seed, upper-strate index, connection index,
`VoxelSettings`. per-value salt)` through `VoxelHash`, so one passage's connection count cannot shift any later
passage. Global passage settings were removed from `VoxelSettings`.
After a passage's XY is selected, `GeneratePassages` asks the **destination** strate for a full lower-mouth After each passage's XY is selected, `GeneratePassages` asks **both** participating strates for a
landing point through the pure free function `VF_SuggestLandingPoint` (`VoxelCaveMorphology`). player-fit source point through the pure free function `VF_SuggestLandingPoint`
(`VoxelCaveMorphology`).
TunnelNetwork and Underwater keep the nearest hash-room vertical placement at the requested XY, with TunnelNetwork and Underwater keep the nearest hash-room vertical placement at the requested XY, with
`MakeStrateSeed(world-seed, strate-index)` matching the room graph's existing identity; FlatPlain and `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 CrystalChamber recompute their slab void band and reject column-overlap points. Maze snaps to the
@@ -487,13 +496,49 @@ axis of a roughness-safe shaft on the drainage tree within two shaft spacings at
density, and FloatingIslands density, and FloatingIslands
to a validated blob top within one island spacing. to a validated blob top within one island spacing.
Those three return false when no footing exists inside that explicit lateral budget. GeneratePassages 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; interpolates each accepted XY through the control-point chain and recomputes its conservative bound;
SurfaceWorld remains deliberately unanswerable because production terrain can be selected through SurfaceWorld remains deliberately unanswerable because production terrain can be selected through
manager-owned biome/per-column context. manager-owned biome/per-column context. Declined answers (unsupported or no-footing) keep the
Declined answers (unsupported or no-footing) keep the historical random reach and emit a warning. This query is source-level historical random reach and are reported diagnostically.
only — it does not construct an operator stack, touch a cache, or call back into
`UVoxelStrateManager` while `Initialize` is building the layout. The resulting lower endpoint is The source query proves only a local pose; it does not flood-fill the live network. Therefore every
clamped to the destination's seal-free interior. inter-strate end also receives an explicit `FVoxelPassageLanding`: a rounded chamber with a hard
flat floor and a deterministic connector to the strate's guaranteed origin-root network at `(0,0)`.
This is the join guarantee, including for a source-fit answer — it is not a probability claim about
a nearby room. The connector is a swept flat-floor corridor with a 5-voxel (1.25 m) radius / 2.5 m
clear width and a 12-voxel (3 m) clear height. Its 4.5-voxel support inset is enough for the
1.36-voxel player radius and avoids capping unrelated shaft air; support stops at the configured
origin-spine radius, so the existing vertical root column remains open. At the root, every connector
joins an annular hub outside the spine, with a common per-strate floor derived from the sealed
interior bounds. If the direct floor run would exceed 44°, the deterministic builder inserts a
level dog-leg before the final ramp. The hub and the connector floor are therefore a real walkable
network, not a point anchor or nearest-component guess, and no per-passage ordering is involved.
The chamber dimensions come directly from the capsule: player diameter `2×0.34/0.25 = 2.72`
voxels; required floor width `3/0.25 = 12` voxels; capsule height `2×0.88/0.25 = 7.04` voxels;
one metre of headroom adds 4 voxels. `HalfWidth=max(7, MouthRadius+2)` leaves
`2×(HalfWidth−1) >= 12` voxels of flat support after the one-voxel wall inset; the authored room
is therefore at least 14 voxels (3.5 m) wide. Height is
`max(12, 7.04+4, 2×MouthRadius+2)`; the stock 8-voxel mouth is an 18-voxel (4.5 m) room.
`FloorZ=StandingPoint.Z−0.5`, floor thickness is 3 voxels (0.75 m), and the tube centreline
meets the room at `FloorZ+MouthRadius`, so its lower tangent is the floor and there is no step lip.
The final-density floor audit samples the flat plane and checks its normal against the explicit ≤44°
landing contract; it also checks the analytic connector ramps against the same limit. The movement
CDO currently reports 44.8°, so the landing audit is intentionally the stricter criterion.
The landing is part of the structural post, in the fixed order
`origin spine → vertical seal → passage tube + landing/floor → XY edge seal`. The floor is applied
after the passage carve and after MC-space disturbances as a support backstop, still before the
final XY edge seal. Room/floor Z is clamped inside the vertical seal; the XY edge seal is last and
wins over every landing or tube near the rim. Thus no landing can breach a strate boundary or the
world edge. The op-stack `FPassageCarveOp` calls the same manager operation as the legacy path,
including the floor, and `ClassifyTile` kills both uniform hypotheses around the bidirectional
landing floor (`Both` in the stack), so an air proof cannot delete support.
The query remains pure — it does not construct an operator stack, touch a cache, or call back into
`UVoxelStrateManager` while `Initialize` builds the layout. Landing descriptors, connector choice,
door direction, and bounds are all produced once from the passage seed salts; passages never read
another passage's result. `LayoutOrderIndependence` compares the complete descriptors bit-for-bit.
### 8.9 Carving — brush shapes + editor controls ### 8.9 Carving — brush shapes + editor controls
`FVoxelModification` has `EVoxelBrushShape {Sphere,Box,Capsule}` + `BoxExtent`/`CapsuleEnd`/ `FVoxelModification` has `EVoxelBrushShape {Sphere,Box,Capsule}` + `BoxExtent`/`CapsuleEnd`/
@@ -565,7 +610,9 @@ driven by `EditorBrush*` props.
(incremented in `GeneratePassages`). Most chunks have NO passage near → instant `FLT_MAX` return (incremented in `GeneratePassages`). Most chunks have NO passage near → instant `FLT_MAX` return
instead of walking the whole `Passages` array per voxel. Conservative superset (chunk bounding instead of walking the whole `Passages` array per voxel. Conservative superset (chunk bounding
sphere vs passage bound) ⇒ bit-identical carve. Store indices + version, never pointers (the array sphere vs passage bound) ⇒ bit-identical carve. Store indices + version, never pointers (the array
is rebuilt on `RebuildStrates`). is rebuilt on `RebuildStrates`). Landing rooms, connector SDFs, and floor membership reuse this
exact cache. The final landing audit measured **0.665 μs** per forced rebuild and **0.646 μs**
per hot call on the validation machine; source-fit queries run only during `GeneratePassages`.
- **Gen tasks run at `UE::Tasks::ETaskPriority::BackgroundNormal`** (`LoadTile`): worker gen yields - **Gen tasks run at `UE::Tasks::ETaskPriority::BackgroundNormal`** (`LoadTile`): worker gen yields
to foreground game/render tasks. Without it, raising `MaxConcurrentTasks` past the spare-core count to foreground game/render tasks. Without it, raising `MaxConcurrentTasks` past the spare-core count
saturates the scheduler and starves the frame (the "concurrency > ~12 = stutter" symptom). Keep gen saturates the scheduler and starves the frame (the "concurrency > ~12 = stutter" symptom). Keep gen
+38 -17
View File
@@ -137,7 +137,7 @@ reference path for opt-out strates. See [OPSTACK-PLAN.md](OPSTACK-PLAN.md) for t
| Symbol | Notes | | Symbol | Notes |
|--------|-------| |--------|-------|
| `EVoxelOpRole` | The four roles: `FieldSource` (what the field IS) · `Combiner` (how fields merge) · `DetailModifier` (today's `UVoxelTerrainOpDefinition`) · `StructuralPost` (spine→vertical seal→passage→XY edge seal→diff, appended automatically, never author-omittable). | | `EVoxelOpRole` | The four roles: `FieldSource` (what the field IS) · `Combiner` (how fields merge) · `DetailModifier` (today's `UVoxelTerrainOpDefinition`) · `StructuralPost` (spine→vertical seal→passage tube/landing floor→XY edge seal→diff, appended automatically, never author-omittable). |
| `EVoxelOpChannel` / `EVoxelOpChannelMask` | The explicit `FVoxelOpSample` channel set: `Density` and `Sdf` (plus `None`/`All` masks). Every density op declares the channels it reads and writes. | | `EVoxelOpChannel` / `EVoxelOpChannelMask` | The explicit `FVoxelOpSample` channel set: `Density` and `Sdf` (plus `None`/`All` masks). Every density op declares the channels it reads and writes. |
| `EVoxelOpCombine` | `Replace`/`Union`(min)/`Subtract`(max)/`SmoothUnion`/`SmoothSubtract`/`Add`/`Mask`. Sign reminder: negative = solid, so "add solid" is `min`. | | `EVoxelOpCombine` | `Replace`/`Union`(min)/`Subtract`(max)/`SmoothUnion`/`SmoothSubtract`/`Add`/`Mask`. Sign reminder: negative = solid, so "add solid" is `min`. |
| `EVoxelOpEffect` | `Identity`/`CarveOnly`/`FillOnly`/`Both`. Conservative: `Both` is always safe, the wrong one is a hole. | | `EVoxelOpEffect` | `Identity`/`CarveOnly`/`FillOnly`/`Both`. Conservative: `Both` is always safe, the wrong one is a hole. |
@@ -149,7 +149,8 @@ reference path for opt-out strates. See [OPSTACK-PLAN.md](OPSTACK-PLAN.md) for t
### 3.2c Structural primitives — `Public/VoxelDensityPrimitives.h` ### 3.2c Structural primitives — `Public/VoxelDensityPrimitives.h`
`VF_ApplyOriginSpine` · `VF_ApplyBoundarySeal` · `VF_ApplyPassageCarving` · `VF_ApplyXYEdgeSeal` — `VF_ApplyOriginSpine` · `VF_ApplyBoundarySeal` · `VF_ApplyPassageCarving` · `VF_ApplyXYEdgeSeal` —
the four world invariants every archetype appends, **moved here 2026-07-27** so the generator and the four world invariants every archetype appends, with landing/floor geometry joined by the
manager passage post, **moved here 2026-07-27** so the generator and
the operator stack share ONE copy. The XY helper is a radial smooth ramp in actor-space and is a the operator stack share ONE copy. The XY helper is a radial smooth ramp in actor-space and is a
true no-op when `WorldRadiusVoxels == 0`; `VF_XYEdgeSealForcedMarginOverBox` is the shared sound true no-op when `WorldRadiusVoxels == 0`; `VF_XYEdgeSealForcedMarginOverBox` is the shared sound
proof for `ClassifyBox` and T1.d's global shell skip. `VoxelGenerator.cpp` keeps same-named proof for `ClassifyBox` and T1.d's global shell skip. `VoxelGenerator.cpp` keeps same-named
@@ -181,7 +182,7 @@ bit. They are port-correctness oracles, not fidelity checks: the acceptance bar
|--------|------|-------| |--------|------|-------|
| `FVoxelOpStack` | — | Ordered move-only list of `TUniquePtr` entries. `Add` snapshots channel metadata; `PrepareChunk` / `EvalInternal` / `EvalMC` / `ClassifyBox` (the fold lets a later forcing post overwrite a dead hypothesis). | | `FVoxelOpStack` | — | Ordered move-only list of `TUniquePtr` entries. `Add` snapshots channel metadata; `PrepareChunk` / `EvalInternal` / `EvalMC` / `ClassifyBox` (the fold lets a later forcing post overwrite a dead hypothesis). |
| `FVoxelOpStack::ValidateChannelOrder` | — | Assembly/diagnostic-only channel/resource DAG validator. It requires prior producers, rejects write-only clobbers and invalid additive declarations, and requires room/shaft/surface state providers before consumers. It permits the one root SDF identity fold used by Maze. `VoxelForge.OpStack.ChannelDAG` validates all 8 shipping stacks. | | `FVoxelOpStack::ValidateChannelOrder` | — | Assembly/diagnostic-only channel/resource DAG validator. It requires prior producers, rejects write-only clobbers and invalid additive declarations, and requires room/shaft/surface state providers before consumers. It permits the one root SDF identity fold used by Maze. `VoxelForge.OpStack.ChannelDAG` validates all 8 shipping stacks. |
| `FVoxelOpStack::AppendStructuralPost` | 4 | Appends spine → vertical seal → passage → XY edge seal **in that fixed order**. An author cannot omit or reorder them. The diff layer is NOT here yet — it still lives in `GetDensityAt` after the MC negate, with disturbances. | | `FVoxelOpStack::AppendStructuralPost` | 4 | Appends spine → vertical seal → passage tube/landing floor → XY edge seal **in that fixed order**. An author cannot omit or reorder them. The diff layer is NOT here yet — it still lives in `GetDensityAt` after the MC negate, with disturbances. |
| `VoxelDensityOps::MakeConstantRockSource` | 1 | `Density = BaseDensity`. `ClassifyBox` → **AllSolid**, exact and free. Shared by TunnelNetwork, Maze, VerticalShafts and bedrock gaps. Class is `FConstantFieldSource` (one class, two factories). | | `VoxelDensityOps::MakeConstantRockSource` | 1 | `Density = BaseDensity`. `ClassifyBox` → **AllSolid**, exact and free. Shared by TunnelNetwork, Maze, VerticalShafts and bedrock gaps. Class is `FConstantFieldSource` (one class, two factories). |
| `VoxelDensityOps::MakeConstantVoidSource` | 1 | The **same class, negated**: `Density = -BaseDensity`, and `ClassifyBox` → **AllAir** — the first source in the plugin that can prove it. FloatingIslands' root; that verdict is what makes a mostly-empty island strate skippable. | | `VoxelDensityOps::MakeConstantVoidSource` | 1 | The **same class, negated**: `Density = -BaseDensity`, and `ClassifyBox` → **AllAir** — the first source in the plugin that can prove it. FloatingIslands' root; that verdict is what makes a mostly-empty island strate skippable. |
| `VoxelDensityOps::MakeLatticeCorridorSource` | 1 | Maze corridors, SDF channel. Each canonical lower-node/axis edge is open when it is a parent edge in the origin-directed tree or passes a capped loop roll; both endpoints are hashed locally, so adjacent chunks cannot disagree. The source reports its own conservative SDF interval; a later converter supplies its own carve/fill response, so the source is independent of the chosen consumer. | | `VoxelDensityOps::MakeLatticeCorridorSource` | 1 | Maze corridors, SDF channel. Each canonical lower-node/axis edge is open when it is a parent edge in the origin-directed tree or passes a capped loop roll; both endpoints are hashed locally, so adjacent chunks cannot disagree. The source reports its own conservative SDF interval; a later converter supplies its own carve/fill response, so the source is independent of the chosen consumer. |
@@ -423,14 +424,24 @@ landing-site query: it evaluates the local archetype source through the same cap
contract as the measurement pass — support within step height, walkable slope, and full capsule contract as the measurement pass — support within step height, walkable slope, and full capsule
clearance. Room queries use a room-floor core, slabs keep their requested XY, and the sparse clearance. Room queries use a room-floor core, slabs keep their requested XY, and the sparse
sources use bounded feature cores: a horizontal lattice corridor (one Maze cell), a selected sources use bounded feature cores: a horizontal lattice corridor (one Maze cell), a selected
VerticalShafts tree shaft plus deterministic ledge-side offsets (up to two local shaft spacings at the 0.6 VerticalShafts tree shaft plus deterministic ledge-side offsets (up to two local shaft spacings at
site density), or a validated blob top (one IslandSpacing). It is intentionally not a flood fill: the 0.6 site density), or a validated blob top (one IslandSpacing). It is intentionally not a
the pure query can prove a local pose, not a large component. The flood fill: the pure query can prove a local pose, not a large component. The
VerticalShafts result is therefore on the drainage tree, not merely in an open roughness disk. 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; 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 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. context. It never touches a generator, operator stack, manager, cache, or mutable state.
`FVoxelPassageLanding`, `VF_BuildPassageLanding`, `VF_EvaluatePassageLandingSDF`, and
`VF_IsPassageLandingFloor` (h:~56/~189, implemented near `.cpp:2059`) turn each inter-strate mouth
into a deterministic rounded room plus a hard support slab. The room is sized from the player
capsule (minimum 12 voxels / 3 m flat floor, 12 voxels / 3 m clear height, and at least 14 voxels /
3.5 m authored width); every end also gets a flat-floor connector to the guaranteed `(0,0)` root
network because the pure source query is local rather than a component proof. The manager's shared
thread-local passage shortlist serves tube, room, connector, and floor evaluation; no landing
search occurs per voxel. `GeneratePassages` stores the full descriptors and the order-independence
test compares them bit-for-bit.
Performance note (h:209-220): caching rooms/tunnels once per chunk instead of per Performance note (h:209-220): caching rooms/tunnels once per chunk instead of per
voxel is the single biggest CPU win. voxel is the single biggest CPU win.
@@ -462,14 +473,18 @@ groups by generator type.
**`Public/VoxelStrateManager.h` + `.cpp`** — `UVoxelStrateManager : UObject` (h:108). **`Public/VoxelStrateManager.h` + `.cpp`** — `UVoxelStrateManager : UObject` (h:108).
Maps depth→strate at runtime; owns passages. Maps depth→strate at runtime; owns passages.
- `FVoxelPassage` (h:39): endpoints, radius, type, control points. - `FVoxelPassage` (h:39): standing endpoints, radius, type, control points, and upper/lower landing descriptors (room, floor, door, and root connector).
- `FStrateSlot` (h:84): definition + chunk-Z range + index. - `FStrateSlot` (h:84): definition + chunk-Z range + index.
| Method | .cpp line | Role | | Method | .cpp line | Role |
|--------|-----------|------| |--------|-----------|------|
| `Initialize` | 10 | Keeps the existing sequencing through `GeneratePassages`, but selects one slot source: a validated cooked season (exact ordered bounds/vector/recipe) or the unchanged authored fixed+sorted/shuffled pool. Assigned-invalid seasons fail closed. Authored source definitions are duplicated as content bags; generated slots use deterministic C++ defaults and zero auto-passages because schema 2 does not store passage configuration. | | `Initialize` | 10 | Keeps the existing sequencing through `GeneratePassages`, but selects one slot source: a validated cooked season (exact ordered bounds/vector/recipe) or the unchanged authored fixed+sorted/shuffled pool. Assigned-invalid seasons fail closed. Authored source definitions are duplicated as content bags; generated slots use deterministic C++ defaults and zero auto-passages because schema 2 does not store passage configuration. |
| `GeneratePassages` | 247 | Deterministic passages between consecutive strates (per-strate `PassageConfig::Style` control points; auto passages retain the existing `EVoxelPassageType` default); placement and shape values are independently salted hashes of seed + boundary slot + connection index. Each mouth independently queries its own strate's pure player-fit `VF_SuggestLandingPoint` with its contract seed (cave room salt, otherwise world seed), may move within its archetype-specific budget, then is clamped outside the destination seal bands and interpolated through the control points; unsupported/no-fit answers preserve the old random reach. | | `GeneratePassages` | 247 | Deterministic passages between consecutive strates (per-strate `PassageConfig::Style` control points; auto passages retain the existing `EVoxelPassageType` default); placement and shape values are independently salted hashes of seed + boundary slot + connection index. Each mouth independently queries its own strate's pure player-fit `VF_SuggestLandingPoint` with its contract seed (cave room salt, otherwise world seed), may move within its archetype-specific budget, then receives a body-sized room/floor and a connector to the common per-strate origin-root annular hub. Unsupported/no-fit answers preserve the old random reach; a deterministic level dog-leg is inserted when the final ramp needs it to stay ≤44°. All descriptors and bounds are generated without reading another passage. |
| `EvaluateModifierSDF` | 357 | SDF of passages at a point (for carving). Per-chunk `thread_local` shortlist (`PassagesVersion`-stamped) → far chunks return `FLT_MAX` without walking `Passages`. §8.10. | | `EvaluateModifierSDF` | 357 | SDF of passage tubes plus landing rooms/connectors at a point. A per-chunk `thread_local` shortlist (`PassagesVersion`-stamped) → far chunks return `FLT_MAX` without walking `Passages`; the same cache serves floor membership. §8.10. |
| `ApplyPassageModifier` | 1319 | Shared legacy/op-stack passage post: carves tube and landing/connector air through the idempotent landing primitive, then restores the landing/connector support slab in internal positive-solid density. |
| `VF_ApplyPassageLandingCarving` | — | Idempotent density-independent landing-air threshold shared by the structural pass and the post-disturbance MC backstop; keeps legacy and operator-stack results bit-identical. |
| `ApplyPassageLandingFloorMC` | 1347 | Reasserts the same support slab after MC-space disturbances and before the final XY edge seal. |
| `AnyPassageNearBox` | — | Conservative sphere-vs-AABB test of every passage's bound against a voxel box (+carve blend pad). Per TILE (ClassifyTile guard), never per voxel. | | `AnyPassageNearBox` | — | Conservative sphere-vs-AABB test of every passage's bound against a voxel box (+carve blend pad). Per TILE (ClassifyTile guard), never per voxel. |
| `AnyPassageLandingFloorNearBox` | — | Conservative room/connector-floor AABB guard. It kills the `AllAir` hypothesis wherever a proved landing floor can occur; the op-stack returns `Both` for the same box. |
| `FindSlotIndexForChunkZ` | 427 | Z → layout index. | | `FindSlotIndexForChunkZ` | 427 | Z → layout index. |
| `GetStrateAt` / `GetStrateIndex` | 443 / 455 | World-Z queries. | | `GetStrateAt` / `GetStrateIndex` | 443 / 455 | World-Z queries. |
| `GetLayoutVersion` | h:161 (inline) | Layout/passage generation counter (= `PassagesVersion`, bumped by every `Initialize` and by the editor composer override). Hot-path callers key `thread_local` memos on it (strate-index memo in `GetDensityWithParams`, passage shortlist) so live changes never serve stale data. | | `GetLayoutVersion` | h:161 (inline) | Layout/passage generation counter (= `PassagesVersion`, bumped by every `Initialize` and by the editor composer override). Hot-path callers key `thread_local` memos on it (strate-index memo in `GetDensityWithParams`, passage shortlist) so live changes never serve stale data. |
@@ -597,7 +612,8 @@ verifies it in memory; `VF_LoadVoxelSeasonManifest` plus `VF_RebuildVoxelSeasonS
recipe entries rebuild through the mandatory-post builder, while authored `FixedStrates` retain their recipe entries rebuild through the mandatory-post builder, while authored `FixedStrates` retain their
absolute slot and native vector. The selected-only descent review reuses `VoxelStratePreview`, and all absolute slot and native vector. The selected-only descent review reuses `VoxelStratePreview`, and all
composition/review work is editor/build-box code; parsing and recipe materialisation are runtime. Lateral composition/review work is editor/build-box code; parsing and recipe materialisation are runtime. Lateral
region manifests remain rejected because their cross-seam law is only 9/16. The focused region manifests remain rejected because their post-landing cross-seam law is only **4/16** (the
pre-landing snapshot was 9/16). The focused
season test currently reports **24 generated / 19 hard-gate survivors / 6 selected / 18 unselected or 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** 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 final round-trip density samples bit-identical. The specified UE 5.7 editor build succeeded and the final
@@ -606,6 +622,11 @@ warning-bearing, 0 failed, 0 not run** in **2,523.978 s**. The regenerated showc
renders, 2 refusals, and 0 blanks**; all box-verdict violations remained zero. The later focused renders, 2 refusals, and 0 blanks**; all box-verdict violations remained zero. The later focused
production-height showcase history is recorded separately below. production-height showcase history is recorded separately below.
The current landing validation is recorded in `VoxelForgeLandingFinal5`: **30 clean successes, 2
warning-bearing successes, 0 failures, 0 not-run** across 32 tests in **3,607.746 s**. The strict
promotion assertions passed and no assertion was weakened. The landing-specific geometry test,
both connectivity tests, all box verdicts, seals, determinism, and op-stack equivalence tests passed.
**`Public/VoxelSeasonAsset.h` + `Private/VoxelSeasonAsset.cpp`** — `UVoxelSeasonAsset : **`Public/VoxelSeasonAsset.h` + `Private/VoxelSeasonAsset.cpp`** — `UVoxelSeasonAsset :
UPrimaryDataAsset`, the cookable carrier. Its editor button imports the reviewable JSON, stores the exact UPrimaryDataAsset`, the cookable carrier. Its editor button imports the reviewable JSON, stores the exact
text plus independently copied metadata/hash, and retains soft references to any authored definitions. text plus independently copied metadata/hash, and retains soft references to any authored definitions.
@@ -676,13 +697,13 @@ The plugin's first tests (`OPSTACK-PLAN.md` Phase 0.5). Run them from the editor
| `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`). | | `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. 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. | | `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. | | ″ | `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. 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. | | `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. The final landing validation passed the strict existing gates; no assertion was weakened. |
| `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**. | | `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**. | | `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 landing regeneration evaluated **256** rolls (**82** eligible single-region, **174** skipped multi-region), produced **7 fine renders, 1 refusal, 0 blanks**, and retained a diagnostic hard-gate survivor set of **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). | | `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 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. | | `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. | | `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, landing descriptors, control geometry, and bounds bit-for-bit. |
| `VoxelForgePassageOpenSpaceTest.cpp` | `VoxelForge.Determinism.PassageLandsInOpenSpace` | Uses the real fixture density path to check both generated mouths against independent source queries. Answered mouths must match the pure player-fit point within the float envelope; the lower mouth also keeps the 16-point ring and source-footing assertions outside the carve/blend band. This is a local-fit/open-space proxy, not a flood-fill proof. Reports answered/refused mouths and archetypes; fails if either side is vacuous. | | `VoxelForgePassageOpenSpaceTest.cpp` | `VoxelForge.Determinism.PassageLandsInOpenSpace` | Uses the real fixture density path to check both generated mouths against independent source queries, then audits all 14 inter-strate landing ends: body-derived room dimensions, final-density flat-floor slope, explicit root-network join, seal containment, hot-call/rebuild timing, and targeted `ClassifyTile` box soundness. Reports source-fit/refused mouths and per-archetype landing slopes; fails on any floor, join, seal, or box violation. |
| `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 mouths remain inside the deterministic shaft feature core (the exact axis is an unsupported open cylinder), 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 shaft seam test re-evaluates cell-boundary positions after warming distinct neighbouring chunk contexts. | | `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 mouths remain inside the deterministic shaft feature core (the exact axis is an unsupported open cylinder), 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 shaft seam test re-evaluates cell-boundary positions after warming distinct neighbouring chunk contexts. |
| `VoxelForgeMazeSeamTest.cpp` | `VoxelForge.Generation.MazeSeamFreedom` | Re-evaluates **42** cell-boundary probes after warming six different neighbouring chunk contexts in both legacy and operator-stack paths. Latest result: **0 legacy mismatches, 0 operator mismatches**. The Maze source's 2×2×2 lower-node window is deliberately local; no wide collect is involved. | | `VoxelForgeMazeSeamTest.cpp` | `VoxelForge.Generation.MazeSeamFreedom` | Re-evaluates **42** cell-boundary probes after warming six different neighbouring chunk contexts in both legacy and operator-stack paths. Latest result: **0 legacy mismatches, 0 operator mismatches**. The Maze source's 2×2×2 lower-node window is deliberately local; no wide collect is involved. |
| `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. | | `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. |
@@ -711,7 +732,7 @@ Stage order (negative=solid throughout). Each stage's anchor:
| 4h — Pinch | 1142 | Passage bottlenecks. | | 4h — Pinch | 1142 | Passage bottlenecks. |
| 5 — Worm tunnels | 1241 | abs(noise1)+abs(noise2), masked by distance-to-network (`WormNetworkRange`: braids hugging rooms/tunnels, no far-field speckle; 0 = legacy unmasked). | | 5 — Worm tunnels | 1241 | abs(noise1)+abs(noise2), masked by distance-to-network (`WormNetworkRange`: braids hugging rooms/tunnels, no far-field speckle; 0 = legacy unmasked). |
| 6 — Boundary seal | 1274 | Solid top/bottom shells (`ApplyBoundarySeal`). | | 6 — Boundary seal | 1274 | Solid top/bottom shells (`ApplyBoundarySeal`). |
| 7 — Inter-strate passages | 1281 | Carve passages/elevator (`ApplyPassageCarving`). | | 7 — Inter-strate passages + landings | `GetDensityWithParams`: 2217; `GetSlabDensity`: 2434+ | Carve passage tubes and deterministic room/connectors, restore their support floors, then leave the final XY seal to the outer MC post (`ApplyPassageModifier` / `ApplyPassageLandingFloorMC`). |
### 4.2 `GetSlabDensity` (FlatPlain / CrystalChamber) — VoxelGenerator.cpp:1306 ### 4.2 `GetSlabDensity` (FlatPlain / CrystalChamber) — VoxelGenerator.cpp:1306
| Step | Line | What | | Step | Line | What |
@@ -720,7 +741,7 @@ Stage order (negative=solid throughout). Each stage's anchor:
| 2 — Ceiling surface | 1343 | Formations hang downward (`abs(noise)`). | | 2 — Ceiling surface | 1343 | Formations hang downward (`abs(noise)`). |
| 3 — Void→base density | 1379 | Solid outside [floor,ceiling]. | | 3 — Void→base density | 1379 | Solid outside [floor,ceiling]. |
| 4 — Columns | 1399 | World-space hash grid, full-height. | | 4 — Columns | 1399 | World-space hash grid, full-height. |
| 5+6 — Seal + passages | 1461 | Same seal/passage carving as TunnelNetwork. | | 5+6 — Seal + passages | 1461 | Same seal, passage-tube, landing-room, connector, and support-floor path as TunnelNetwork. |
--- ---
@@ -739,7 +760,7 @@ Stage order (negative=solid throughout). Each stage's anchor:
| Strate stacking / which strate where | `UVoxelStrateManager::Initialize` :10. | | Strate stacking / which strate where | `UVoxelStrateManager::Initialize` :10. |
| Publish/use an offline season | Create `UVoxelSeasonAsset`, set `SourceManifestJson`, press `ImportSeasonManifestJson`, then assign it to `UVoxelSettings::Season`. | | Publish/use an offline season | Create `UVoxelSeasonAsset`, set `SourceManifestJson`, press `ImportSeasonManifestJson`, then assign it to `UVoxelSettings::Season`. |
| Boundary blend between strates | `GetGenerationParams` :515 + `FStrateGenerationParams::Lerp` (expands `VF_STRATE_PARAM_FIELDS`, StrateTypes.h — new fields go in that list). | | Boundary blend between strates | `GetGenerationParams` :515 + `FStrateGenerationParams::Lerp` (expands `VF_STRATE_PARAM_FIELDS`, StrateTypes.h — new fields go in that list). |
| Passages between strates | `GeneratePassages` :146 + `EvaluateModifierSDF` :371 + `ApplyPassageCarving` (Generator.cpp:197). | | Passages / landing geometry between strates | `GeneratePassages` :146 + `EvaluateModifierSDF` :371 + `ApplyPassageModifier` / `ApplyPassageLandingFloorMC` (StrateManager.cpp) + `ClassifyTile` floor guard (Generator.cpp). |
| Player carve/fill | `CarveAtPosition`/`FillAtPosition` VoxelWorld.cpp:691/709 → `UVoxelDiffLayer::ApplyModification` :63. | | Player carve/fill | `CarveAtPosition`/`FillAtPosition` VoxelWorld.cpp:691/709 → `UVoxelDiffLayer::ApplyModification` :63. |
| Mesh smoothness / normals | Grid-gradient in `GenerateMesh` (`GradAt` lambda), `IsoLevel` (h). | | Mesh smoothness / normals | Grid-gradient in `GenerateMesh` (`GradAt` lambda), `IsoLevel` (h). |
| New slab/flat-world generator | `GetSlabDensity` Generator.cpp:1306 + `FSlabGenerationParams` (StrateTypes.h:1019). | | New slab/flat-world generator | `GetSlabDensity` Generator.cpp:1306 + `FSlabGenerationParams` (StrateTypes.h:1019). |
+180 -51
View File
@@ -115,7 +115,8 @@ Runtime contains only manifest parsing/verification and recipe evaluation, and `
remains 0. Generated schema-2 slots have no passage/content/visual record: they therefore use remains 0. Generated schema-2 slots have no passage/content/visual record: they therefore use
deterministic empty/default wider content and zero auto-passages (the origin spine remains). Fixed slots deterministic empty/default wider content and zero auto-passages (the origin spine remains). Fixed slots
may retain an authored definition path as that wider-content bag. Lateral regions remain unshippable at may retain an authored definition path as that wider-content bag. Lateral regions remain unshippable at
the measured 9/16 cross-seam law result. the measured **4/16** cross-seam law result after the landing pass (the 9/16 figure below is the
pre-landing snapshot).
An abbreviated manifest shape is: An abbreviated manifest shape is:
@@ -268,7 +269,7 @@ through the offline Tier 2 sampler; no runtime generation path calls this API. T
test took **76.466 s**. The complete `VoxelForge` namespace then passed **24/24 tests (0 failed, test took **76.466 s**. The complete `VoxelForge` namespace then passed **24/24 tests (0 failed,
0 not run)** in **339.797 s**. 0 not run)** in **339.797 s**.
### 3.2b Lateral regions — implemented, primordial-law gate failed (2026-09-05) ### 3.2b Lateral regions — implemented, primordial-law gate failed (2026-09-06 post-landing audit)
The owner's “tunnel network leading to a big chamber and drops into shafts” question changes the The owner's “tunnel network leading to a big chamber and drops into shafts” question changes the
unit of composition: an archetype is a **region inside a strate**, not the strate's label. Tier 4d unit of composition: an archetype is a **region inside a strate**, not the strate's label. Tier 4d
@@ -284,7 +285,7 @@ different region's stack evaluates too and the two **internal densities** are cr
same linear curve used by the vertical transition (split symmetrically at the bisector). Native same linear curve used by the vertical transition (split symmetrically at the bisector). Native
parameters are never blended: `ShaftSpacing` and a slab's floor/ceiling controls do not share a parameters are never blended: `ShaftSpacing` and a slab's floor/ceiling controls do not share a
meaning. Region cores omit structural posts; the parent appends the global spine, vertical seal, meaning. Region cores omit structural posts; the parent appends the global spine, vertical seal,
passage carve, and XY edge seal once, in the existing order, after the lateral result. `Density` passage tube + landing/floor, and XY edge seal once, in the existing order, after the lateral result. `Density`
keeps the stack convention positive=solid and is negated only at the MC-facing boundary. keeps the stack convention positive=solid and is negated only at the MC-facing boundary.
The runtime hand-off remains an explicit manifest copy, not a new runtime composer. A worker The runtime hand-off remains an explicit manifest copy, not a new runtime composer. A worker
@@ -310,14 +311,13 @@ if this
feature proceeds, a narrower band or a cheaper density junction needs an owner-approved design feature proceeds, a narrower band or a cheaper density junction needs an owner-approved design
decision rather than an unreported performance regression. decision rather than an unreported performance regression.
The primordial law is the blocker. The test deliberately collected **16 actual opposite-region The primordial law is the blocker. The pre-landing audit connected **9/16 (56.25%)**. After the
arrival/departure mouth pairs**, without adding a corridor, changing a threshold, or filtering for landing geometry pass, the current same-fixture audit still collected **16 actual opposite-region
success. The lateral parent connected **9/16 (56.25%)**. The same exact region-zero recipe/vector arrival/departure mouth pairs** and connected **4/16**; the one-region controls also connected
and structural settings in a one-region control connected **11/16**, so the underlying structure **4/16**. The same 16 seam cases remain **valid 16/16, non-vacuous 16/16, and largest-component
roll is itself not universally connected; that control does not excuse the lateral failure. The share ≥ 0.50 16/16**; including the unsnapped seam law, current full hard-gate survival is therefore
same 16 seam cases were **valid 16/16, non-vacuous 16/16, and largest-component share ≥ 0.50 **4/16**. The landing itself is geometry-green, but it does not rescue the native region network.
16/16**; including the unsnapped seam law, full hard-gate survival is therefore **9/16**. The The result is not ready for promotion. A future fix must be a design decision such as a guaranteed
result is not ready for promotion. A future fix must be a design decision such as a guaranteed
cross-seam corridor/landing contract, not a hidden pass threshold or seed selection. cross-seam corridor/landing contract, not a hidden pass threshold or seed selection.
The rendered review artifact is The rendered review artifact is
@@ -1272,15 +1272,49 @@ Passage placement remains deterministic and cheap, and the slanted control-point
Then the measurement pass flood fill is the net: does the upper mouth's air component reach the lower Then the measurement pass flood fill is the net: does the upper mouth's air component reach the lower
mouth's? If not, re-roll with the attempt folded into the hash. mouth's? If not, re-roll with the attempt folded into the hash.
### 6.5 Findability is mostly DATA, not code ### ✅ 6.5 The passage mouth becomes a LANDING; the landmark remains DATA — BUILT
`ELandmarkAnchor::PassageMouth` already exists — landmarks can anchor at passage mouths — and `ELandmarkAnchor::PassageMouth` already exists — landmarks can anchor at passage mouths — and
`FStrateLandmark` already carries a Light-Orb block. **A glowing landmark at every passage mouth is `FStrateLandmark` already carries a Light-Orb block. **A glowing landmark at every passage mouth is
authorable today with no code at all.** Beyond that, cheapest first: light (a glow down a tunnel is the authorable today with no code at all.** The geometry half is now built: every end of every inter-strate
strongest pull in a cave); sound (works around corners, which light does not — you hear the draft before passage is a deterministic `FVoxelPassageLanding` — a rounded room, a hard flat support floor, a
you see the hole); and ⭐ **make the mouth a PLACE, not a hole** — the `SuggestLandingPoint` fix already doorway tangent to the tube, and a floor-backed connector to the guaranteed `(0,0)` root network.
lands the passage in a room, so make that room distinctive. Cracks in walls are forgettable; rooms are The content half is deliberately not built here; landmark meshes, lights, sound, and placement
landmarks, and that is what makes it findable on the *second* visit, which quests need. policy belong to the content system. The landing supplies the stable, walkable, deterministic
`PassageMouth` anchor that content placement needs, without inventing landmark geometry here.
The dimensions derive from the capsule and the existing two-abreast corridor rule: player diameter
`2×0.34/0.25 = 2.72` voxels; floor width `3/0.25 = 12` voxels; capsule height
`2×0.88/0.25 = 7.04` voxels; and 1 m headroom adds 4 voxels. The room uses
`HalfWidth=max(7, MouthRadius+2)`, so its one-voxel-inset support floor is at least 12 voxels /
3 m wide while the authored room is at least 14 voxels / 3.5 m wide. Its height is
`max(12, 7.04+4, 2×MouthRadius+2)`; with the stock 8-voxel mouth, that is 18 voxels / 4.5 m.
The floor is at `StandingPoint.Z−0.5`, three voxels / 0.75 m thick, and the tube centreline meets
the room at `FloorZ+MouthRadius`, so the lower tube tangent is the floor rather than a hole with a
step. The connector is 5 voxels / 1.25 m radius (2.5 m clear width), 12 voxels / 3 m clear height,
with a 4.5-voxel support inset; the room, not the transit leg, owns the turn floor. At the root it
joins a common per-strate annular hub outside the origin spine. A direct ramp is used when it can
stay ≤44°; otherwise the deterministic connector inserts a level dog-leg before its final ramp.
The final-density audit checks every floor normal against the explicit ≤44° walkable-slope limit
(the movement CDO currently reports 44.8°, so this landing contract is conservative).
The join is guaranteed rather than likely because a source query is only a local capsule proof — it
does not flood-fill the live graph. `GeneratePassages` therefore adds the same deterministic
connector for answered and refused source queries, ending at the common annular hub around `(0,0)`.
The hub is the explicit walkable root network; it is outside the vertical spine, so the spine stays
open while every connector has a floor and a common interior level. It is generated from the
passage seed and is included in the conservative passage bounds. Its structural
post order is `origin spine → vertical seal → passage tube + landing/floor → XY edge seal`; the
landing is clamped inside the vertical seal, the floor backstop runs before the final XY edge seal,
and the edge seal wins at the world rim. `ClassifyTile` and the op-stack both treat a landing floor
as bidirectional geometry, so a proved `AllAir` box cannot erase the support.
The stable room/floor/door/connector anchor is exactly the hook the landmark content system needs:
it is walkable, known, deterministic, and available through the passage-mouth anchor without
inventing a second placement search. Beyond that, cheapest first: light (a glow down a tunnel is
the strongest pull in a cave) and sound (works around corners, which light does not — you hear the
draft before you see the hole). Rooms are what make the mouth findable on the *second* visit, which
quests need.
### ✅ 6.6 The layout depends on the pool's CONTENTS, not its ORDER (BUILT + TESTS GREEN) ### ✅ 6.6 The layout depends on the pool's CONTENTS, not its ORDER (BUILT + TESTS GREEN)
@@ -1374,7 +1408,9 @@ refactor, where three weeks of correct work produced a world unchanged by a sing
- ✅ **`SuggestLandingPoint` on field sources; passages aim at it** (§6.4) — DONE, **both mouths**. - ✅ **`SuggestLandingPoint` on field sources; passages aim at it** (§6.4) — DONE, **both mouths**.
⚠️ It shipped HALF BUILT for weeks: only the LOWER mouth was ever aimed. See §14.3 — the ring proxy ⚠️ It shipped HALF BUILT for weeks: only the LOWER mouth was ever aimed. See §14.3 — the ring proxy
could not see it, and only the Tier 2 connectivity measurement could. could not see it, and only the Tier 2 connectivity measurement could.
- ⬜ **The passage mouth becomes a room plus a distinctive landmark** (§6.5) — not started. - ✅ **The passage mouth becomes a room plus a distinctive landmark hook** (§6.5) — BUILT for the
geometry: both ends get deterministic walkable landings, floors, and root-network connectors.
Landmark content itself remains intentionally with the content system.
- ⬜ **`SurfaceWorld` still refuses the query** — it needs manager-resolved biome context, which - ⬜ **`SurfaceWorld` still refuses the query** — it needs manager-resolved biome context, which
cannot be answered from a pure free function. Open. cannot be answered from a pure free function. Open.
@@ -1558,7 +1594,7 @@ law has a design-level fix.
--- ---
## 11. Tier 1 status — passages aim at player-fit source space (pure local stencil) ## 11. Tier 1 status — passages aim at player-fit source space (pure local stencil; foundation for §6.5)
**Updated 2026-09-06.** `VF_SuggestLandingPoint` (in `VoxelCaveMorphology`) is a **pure free function** **Updated 2026-09-06.** `VF_SuggestLandingPoint` (in `VoxelCaveMorphology`) is a **pure free function**
of (archetype, params, seed, desired XY, lateral budget) — it touches no operator stack, no of (archetype, params, seed, desired XY, lateral budget) — it touches no operator stack, no
@@ -1580,6 +1616,11 @@ large connected component; that question remains in the measurement pass. When n
exists inside the bounded source neighbourhood, **the random reach and original XY are preserved exists inside the bounded source neighbourhood, **the random reach and original XY are preserved
unchanged**. unchanged**.
This section is the source-selection contract, not the finished mouth geometry. §6.5 now wraps every
accepted or refused result in a structural landing: the source result chooses a deterministic anchor
when available, while the manager supplies the room, flat floor, doorway, and guaranteed root-network
connector that make the anchor a place to stand. The source query remains pure and manager-free.
### ⚠️ Coverage is source-dependent and the number is the point ### ⚠️ Coverage is source-dependent and the number is the point
The failing pre-fix measurement was: The failing pre-fix measurement was:
@@ -1632,7 +1673,7 @@ is the only thing standing between a placement change and silent bedrock passage
(2026-08-30).** The room-centre correction below remains useful history, but it did not prove a (2026-08-30).** The room-centre correction below remains useful history, but it did not prove a
player-sized landing or a large connected component. player-sized landing or a large connected component.
### 12.1 2026-09-06 player-fit landing pass ### 12.1 2026-09-06 player-fit source-query pass (historical, before landing geometry)
The next correction keeps the pure/re-entrant boundary intact and replaces the query's open-air The next correction keeps the pure/re-entrant boundary intact and replaces the query's open-air
claim with a local player-pose claim. Both the upper and lower mouths query their own source claim with a local player-pose claim. Both the upper and lower mouths query their own source
@@ -1678,7 +1719,7 @@ passed; the full `VoxelForge` namespace finished with **29 successes, 2 known wa
0 not-run** in 2,388.35 s. The warnings remain the gated lateral-region 9/16 law and the known 0 not-run** in 2,388.35 s. The warnings remain the gated lateral-region 9/16 law and the known
TunnelNetwork op-stack outside-gate sample count; box violations stayed zero. TunnelNetwork op-stack outside-gate sample count; box violations stayed zero.
### 12.2 2026-09-06 Maze spanning-tree correction ### 12.2 2026-09-06 Maze spanning-tree correction (historical, before landing geometry)
The Maze lattice no longer uses independent per-edge hash gating. Each non-origin cell chooses one The Maze lattice no longer uses independent per-edge hash gating. Each non-origin cell chooses one
parent from the non-zero coordinate axes and steps one unit toward the origin. The parent reduces parent from the non-zero coordinate axes and steps one unit toward the origin. The parent reduces
@@ -1738,15 +1779,103 @@ rebuild and **0.150 μs/call** for 20,000 hot evaluations; the final namespace r
in the thread-local rebuild. in the thread-local rebuild.
The regenerated owner-facing artifact is The regenerated owner-facing artifact is
`E:/Projet Unreal/VoxelM/Saved/VoxelForge/Showcase/index.html`; the selected Maze card is `E:/Projet Unreal/VoxelM/Saved/VoxelForge/Showcase/index.html`; the current Maze card is
`candidate_1003_plan.png` (with matching `candidate_1003_vertical.png` and fine ROI variants). `candidate_1003_plan.png` (with matching `candidate_1003_vertical.png` and fine ROI variants).
The post-fix showcase rendered **6 fine views**, refused **2**, and produced **0 blank** cards. The landing pass rendered **8 fine views**, refused **0**, and produced **0 blank** cards. The
showcase's diagnostic player-fit gate still found **0/8 hard-gate survivors**; the cards are kept
as walkable geometry previews rather than pretending that unresolved inter-landing law is solved.
The final full namespace verification is `Saved/AutomationReports/VoxelForgeFullMazeTreeFinal3/index.json`: The final full namespace verification is `Saved/AutomationReports/VoxelForgeFullMazeTreeFinal3/index.json`:
**32 tests, 30 clean successes, 2 warning-bearing successes, 0 failures, 0 not-run** in **2,523.978 s**. **32 tests, 30 clean successes, 2 warning-bearing successes, 0 failures, 0 not-run** in **2,523.978 s**.
The two warnings are the pre-existing gated lateral-region result and the known TunnelNetwork The two warnings are the pre-existing gated lateral-region result and the known TunnelNetwork
outside-gate sample count; neither is a Maze or box-soundness failure. outside-gate sample count; neither is a Maze or box-soundness failure.
### 12.3 2026-09-06 §6.5 landing geometry pass
The mouth is now a place rather than the end of a bore. `GeneratePassages` builds both ends of all
seven inter-strate passages as `FVoxelPassageLanding` descriptors. Each descriptor contains a
rounded room, a three-voxel support slab, a tube doorway at the floor tangent, and an explicit
flat-floor connector to the deterministic `(0,0)` origin-root network. The source query still does
the local archetype selection; the connector is present for both answered and refused queries
because a local capsule proof is not a component proof.
The body arithmetic is: diameter `2×0.34/0.25 = 2.72` voxels; minimum turn floor `3/0.25 = 12`
voxels; capsule height `2×0.88/0.25 = 7.04` voxels; and one metre of headroom `=4` voxels.
`HalfWidth=max(7,MouthRadius+2)` leaves a one-voxel-inset flat support floor at least 12 voxels /
3 m across and an authored room at least 14 voxels / 3.5 m across. Height is
`max(12,7.04+4,2×MouthRadius+2)`; stock mouth radius 8 therefore produces an 18-voxel / 4.5 m
room. The tube centreline enters at `FloorZ+MouthRadius`, so its lower tangent is the room floor.
The connector is 5 voxels / 1.25 m radius (2.5 m clear width), 12 voxels / 3 m clear height, and
a 4.5-voxel support inset. It joins the common per-strate annular hub outside the origin spine;
the direct ramp is used only when it stays ≤44°, otherwise a deterministic level dog-leg precedes
the final ramp. All 14 landing floors passed the final-density slope bracket; worst measured room
gradient was `0.058998`, below the explicit 44° limit `tan(44°)=0.965689`, and the worst
analytic connector gradient was `0.522406` (walkable limit `0.965689`). The per-end values were recorded by
`VoxelForge.Determinism.PassageLandsInOpenSpace` without per-sample logging.
The focused landing audit reported **14/14** floor/slope passes, **14/14** explicit root-network
connectors, **0** seal violations, and **0** geometry/floor failures. The targeted tile audit saw
**14** floor boxes, **0** uniform proofs (`AllSolid=0`, `AllAir=0`), and **0** brute-force verdict
violations. The connector itself is a guaranteed walkable branch to the structural root; the
separate player-fit law below correctly remains an empirical test of the native network between
the two landing branches, especially when their floor heights differ.
The same audit measured the passage shortlist at **0.665 μs** per forced rebuild and **0.646 μs**
per hot call on the validation machine; source queries and landing construction remain outside the
voxel loop.
The support strip stops at the configured origin-spine radius; this preserves the existing vertical
air column rather than capping it with a landing floor. The connector still reaches the spine
boundary, and the full-resolution direct-walk assertion remains enabled.
Final player-fit component sizes from the fitted-window audit were:
The table below is the fixed H3 fitted-window identity used by `ScaleDiagnosis`. Separately, the
specific 31.9M-cell whole-strate Maze comparison cited by §6.5 changed from the supplied
**172-cell arrival / 4-cell departure** pocket to **1,235 / 2,938** after the landing. That is a
substantial improvement at both mouths, but it still has a **71.896-voxel** gap and remains
`NotConnectedAtThisResolution`.
| Archetype | Before: fit / components / largest / arrival / departure / gap / law | After: fit / components / largest / arrival / departure / gap / law |
|---|---|---|
| CrystalChamber | 655 / 197 / 24 / 1 / 1 / 47.529 / NotConnectedAtThisResolution | 1,575 / 90 / 286 / 286 / 274 / 155.042 / NotConnectedAtThisResolution |
| FlatPlain | 1,116 / 45 / 259 / 1 / 1 / 48.104 / NotConnectedAtThisResolution | 2,817 / 11 / 2,130 / 268 / 273 / 155.042 / NotConnectedAtThisResolution |
| FloatingIslands | 4 / 2 / 3 / 1 / 1 / 0.000 / Connected | 72 / 1 / 72 / 72 / 72 / 0.000 / Connected |
| Maze | 2,801 / 309 / 176 / 43 / 81 / 206.630 / NotConnectedAtThisResolution | 12,384 / 340 / 3,210 / 3,210 / 3,210 / 0.000 / Connected |
| SurfaceWorld | 713 / 520 / 6 / 1 / 1 / 110.648 / NotConnectedAtThisResolution | 2,256 / 297 / 286 / 286 / 268 / 164.250 / NotConnectedAtThisResolution |
| TunnelNetwork | 2,748 / 467 / 1,292 / 2 / 1 / 164.405 / NotConnectedAtThisResolution | 6,683 / 227 / 3,841 / 3,841 / 269 / 155.042 / NotConnectedAtThisResolution |
| Underwater | 337 / 86 / 35 / 3 / 11 / 142.499 / NotConnectedAtThisResolution | 5,780 / 117 / 3,838 / 3,838 / 267 / 155.042 / NotConnectedAtThisResolution |
| VerticalShafts | 1,156 / 112 / 96 / 3 / 36 / 94.000 / NotConnectedAtThisResolution | refused: fitted origin-inclusive grid exceeded `MaxCells` |
Thus the restricted player-fit tally is **1/8 before** and **2/7 after** of the measured archetypes,
with **0** after connections through four cells and **1/8 refused** (VerticalShafts). Maze's
origin-inclusive fitted window is connected, but its independent whole-strate 31,881,528-cell
step-1 window is not: **119,479 fit cells / 2,948 components / largest 11,418 / arrival 1,235 /
departure 2,938 / gap 71.896**, so this is not a cropped-window victory. The hard independent
VerticalShafts air-network refinement is a different, larger check and passed **16/16** fitted
arrival→departure seeds. The remaining player-fit misses are therefore between substantial
landing components and are not being hidden by a relaxed assertion or tuned toward a component
count.
The final UE 5.7 validation was the requested editor build (4 actions, succeeded; 11.59 s), followed by
`VoxelForgeLandingFinal5`: **32 tests, 30 clean successes, 2 warning-bearing successes, 0 failures,
0 not-run** in **3,607.746 s**. `Composer.Promotion` passed with its strict existing gates; no
assertion was weakened. The two warning-bearing tests are the gated lateral-region audit (**4/16**)
and the known TunnelNetwork op-stack outside-gate sample count; landing, connectivity, seal,
The namespace's showcase child evaluated **256** rolls (**82** eligible single-region, **174**
multi-region skipped while the lateral gate is off) and regenerated
`E:/Projet Unreal/VoxelM/Saved/VoxelForge/Showcase/index.html` with **7 fine renders, 1 refusal,
0 blanks**.
The landing audit's per-end final-density floor gradients (lower / upper where present) were:
TunnelNetwork **0.044387**; FlatPlain **0.001678 / 0.058998**; CrystalChamber
**0.001434 / 0.006241**; Maze **0.004608 / 0.020844**; SurfaceWorld **0.012390 / 0.012970**;
VerticalShafts **0.000061 / 0.018677**; FloatingIslands **0.005249 / 0.028687**; Underwater
**0.000061 / 0.000061**. All 14 passed against the explicit 44° walkable gradient limit
**0.965689** (`tan(44°)`).
The corresponding analytic connector gradients were **0.183831, 0.149547, 0.522406, 0.285965,
0.037782, 0.113750, 0.069551, 0.215350, 0.189820, 0.201735, 0.132101, 0.157017, 0.011850,
0.028576** (14/14; worst **0.522406**, limit **0.965689**).
### ⚠️ Correct the record: commit `1ab8c0c` overstated what worked ### ⚠️ Correct the record: commit `1ab8c0c` overstated what worked
It reported *"3 checked, 48/48 ring samples air"* and read as Tier 1 working. A million-seed sweep later It reported *"3 checked, 48/48 ring samples air"* and read as Tier 1 working. A million-seed sweep later
showed the room query left the landing point **OUTSIDE the selected room 92.795% of the time**. The showed the room query left the landing point **OUTSIDE the selected room 92.795% of the time**. The
@@ -1961,16 +2090,17 @@ infinite Maze graph is disconnected.
| window | exact window / grid | MaxCells | fit cells | components | largest | arrival / departure | gap (voxels) | law | | window | exact window / grid | MaxCells | fit cells | components | largest | arrival / departure | gap (voxels) | law |
|---|---|---:|---:|---:|---:|---:|---:|---| |---|---|---:|---:|---:|---:|---:|---:|---|
| current mouth-AABB + margin | `X[-84.1,168.7) Y[39.5,53.3) Z[-1175,-937)`, `253×14×238`, `842,996` cells, step 1 | 4,000,000 | 1,352 | 50 | 237 | 128 / 1 | 254.492 | `NotConnectedAtThisResolution` | | current mouth-AABB + margin | `X[-84.1,168.7) Y[39.5,53.3) Z[-1175,-937)`, `253×14×238`, `842,996` cells, step 1 | 4,000,000 | 2,041 | 58 | 510 | 510 / 294 | 254.370 | `NotConnectedAtThisResolution` |
| origin-inclusive mouth-AABB + margin | `X[-84.1,168.7) Y[0.0,53.3) Z[-1175,-937)`, `253×54×238`, `3,251,556` cells, step 1 | 4,000,000 | 9,598 | 309 | 845 | 120 / 3 | 274.811 | `NotConnectedAtThisResolution` | | origin-inclusive mouth-AABB + margin | `X[-84.1,168.7) Y[0.0,53.3) Z[-1175,-937)`, `253×54×238`, `3,251,556` cells, step 1 | 4,000,000 | 12,384 | 340 | 3,210 | 3,210 / 3,210 | 0.000 | `Connected` |
| whole-strate centered origin cap window | `X[-183,183) Y[-183,183) Z[-1175,-937)`, `366×366×238`, `31,881,528` cells, step 1 | 32,000,000 | 115,578 | 2,903 | 11,418 | 172 / 4 | 274.811 | `NotConnectedAtThisResolution` | | whole-strate centered origin cap window | `X[-183,183) Y[-183,183) Z[-1175,-937)`, `366×366×238`, `31,881,528` cells, step 1 | 32,000,000 | 119,479 | 2,948 | 11,418 | 1,235 / 2,938 | 71.896 | `NotConnectedAtThisResolution` |
The origin-inclusive box did **not** flip to `Connected`, and the larger centered window did not The origin-inclusive box **did** flip this candidate to `Connected`, but the larger centered
either. The hypothesis is therefore **not confirmed** for this candidate. The whole-strate probe whole-strate window did not. The result is therefore a useful local join diagnosis, not a claim that
used an explicit 32M cap so it could remain a real step-1 player-fit measurement; the production the native Maze strate is connected end-to-end: the 31.9M-cell measurement is the controlling
fine-law cap remains 4M. A step-2 player-fit probe is not equivalent here: the player-fit API whole-strate result. The whole-strate probe used an explicit 32M cap so it could remain a real
intentionally refuses every `SampleStep != 1` because the capsule stencil cannot be resolved at step-1 player-fit measurement; the production fine-law cap remains 4M. A step-2 player-fit probe
that resolution. is not equivalent here: the player-fit API intentionally refuses every `SampleStep != 1` because
the capsule stencil cannot be resolved at that resolution.
### 16.2 Part B — route-aware window policy ### 16.2 Part B — route-aware window policy
@@ -1985,10 +2115,10 @@ The policy used by the eight-row law is:
- mouth-AABB plus capsule margin: `CrystalChamber`, `FlatPlain`, `SurfaceWorld`, and - mouth-AABB plus capsule margin: `CrystalChamber`, `FlatPlain`, `SurfaceWorld`, and
`FloatingIslands`. `FloatingIslands`.
The first group is origin-rooted by its actual topology: Maze parent chains, VerticalShafts The first group is origin-rooted by its actual native topology: Maze parent chains, VerticalShafts
drainage-tree chains, and the origin-flowing room graph shared by TunnelNetwork/Underwater. The drainage-tree chains, and the origin-flowing room graph shared by TunnelNetwork/Underwater. The
global structural spine exists for every stack, but it is not treated as the promised route for landing pass also supplies an explicit structural root hub for every archetype, but this diagnostic
the four local/independent families. keeps that hub separate from the native player-fit law so it cannot hide a broken archetype network.
### 16.3 Part C — corrected eight-row law ### 16.3 Part C — corrected eight-row law
@@ -1997,24 +2127,23 @@ window column is part of the result, not an omitted implementation detail.
| archetype | player-fit window (`X/Y/Z`, grid, cells) | fit cells | components | largest | arrival / departure | gap (voxels) | law | | archetype | player-fit window (`X/Y/Z`, grid, cells) | fit cells | components | largest | arrival / departure | gap (voxels) | law |
|---|---|---:|---:|---:|---:|---:|---| |---|---|---:|---:|---:|---:|---:|---|
| CrystalChamber | mouth-AABB + margin; `X[-178.9,-152.8) Y[-38.2,29.1) Z[-537,-295)`, `27×68×242`, `444,312` | 992 | 102 | 165 | 40 / 6 | 50.070 | `NotConnectedAtThisResolution` | | CrystalChamber | mouth-AABB + margin; `X[-178.9,-152.8) Y[-38.2,29.1) Z[-537,-295)`, `27×68×242`, `444,312` | 1,575 | 90 | 286 | 286 / 274 | 155.042 | `NotConnectedAtThisResolution` |
| FlatPlain | mouth-AABB + margin; `X[-178.9,-152.8) Y[-38.2,29.1) Z[-534,-298)`, `27×68×236`, `433,296` | 2,346 | 9 | 2,337 | 1 / 2,337 | 138.105 | `NotConnectedAtThisResolution` | | FlatPlain | mouth-AABB + margin; `X[-178.9,-152.8) Y[-38.2,29.1) Z[-534,-298)`, `27×68×236`, `433,296` | 2,817 | 11 | 2,130 | 268 / 273 | 155.042 | `NotConnectedAtThisResolution` |
| FloatingIslands | mouth-AABB + margin; `X[-143.5,-135.5) Y[121.8,129.8) Z[-537,-295)`, `8×8×242`, `15,488` | 0 | 0 | 0 | 0 / 0 | -1.000 | `StartCellNotPlayerFit` | | FloatingIslands | mouth-AABB + margin; `X[-143.5,-135.5) Y[121.8,129.8) Z[-537,-295)`, `8×8×242`, `15,488` | 72 | 1 | 72 | 72 / 72 | 0.000 | `Connected` |
| Maze | origin-inclusive mouth-AABB + margin; `X[-84.1,168.7) Y[0.0,53.3) Z[-1175,-937)`, `253×54×238`, `3,251,556` | 9,598 | 309 | 845 | 120 / 3 | 274.811 | `NotConnectedAtThisResolution` | | Maze | origin-inclusive mouth-AABB + margin; `X[-84.1,168.7) Y[0.0,53.3) Z[-1175,-937)`, `253×54×238`, `3,251,556` | 12,384 | 340 | 3,210 | 3,210 / 3,210 | 0.000 | `Connected` |
| SurfaceWorld | mouth-AABB + margin; `X[-133.7,-76.1) Y[-68.8,47.5) Z[-1495,-1257)`, `58×117×238`, `1,615,068` | 1,720 | 248 | 801 | 1 / 3 | 101.454 | `NotConnectedAtThisResolution` | | SurfaceWorld | mouth-AABB + margin; `X[-133.7,-76.1) Y[-68.8,47.5) Z[-1495,-1257)`, `58×117×238`, `1,615,068` | 2,256 | 297 | 286 | 286 / 268 | 164.250 | `NotConnectedAtThisResolution` |
| TunnelNetwork | origin-inclusive mouth-AABB + margin; `X[-178.9,0.0) Y[-38.2,29.1) Z[-536,-296)`, `179×68×240`, `2,921,280` | 3,684 | 213 | 1,176 | 108 / 82 | 83.361 | `NotConnectedAtThisResolution` | | TunnelNetwork | origin-inclusive mouth-AABB + margin; `X[-178.9,0.0) Y[-38.2,29.1) Z[-536,-296)`, `179×68×240`, `2,921,280` | 6,683 | 227 | 3,841 | 3,841 / 269 | 155.042 | `NotConnectedAtThisResolution` |
| Underwater | origin-inclusive mouth-AABB + margin; `X[-178.9,0.0) Y[-38.2,29.1) Z[-535,-297)`, `179×68×238`, `2,896,936` | 1,669 | 79 | 618 | 109 / 109 | 0.000 | `CoarseLiedBudgetExhausted` (**unknown**, not a pass) | | Underwater | origin-inclusive mouth-AABB + margin; `X[-178.9,0.0) Y[-38.2,29.1) Z[-535,-297)`, `179×68×238`, `2,896,936` | 5,780 | 117 | 3,838 | 3,838 / 267 | 155.042 | `NotConnectedAtThisResolution` |
| VerticalShafts | origin-inclusive mouth-AABB + margin; **refused before allocation** because the required grid exceeds `MaxCells=4,000,000` | — | — | — | — | — | `REFUSED(MaxCells)` | | VerticalShafts | origin-inclusive mouth-AABB + margin; **refused before allocation** because the required grid exceeds `MaxCells=4,000,000` | — | — | — | — | — | `REFUSED(MaxCells)` |
The valid-row tally is **0/7 Connected**, with **1/8 refused** and one `CoarseLiedBudgetExhausted` The valid-row tally is **2/7 Connected**, with **1/8 refused** and **0** unknown results. Neither
unknown. The old one-cell/one-pocket success is not present in this corrected table. connected row is a ≤4-cell pocket: `after_connected_through_4_cells=0`.
### 16.4 Plain verdict ### 16.4 Plain verdict
The remaining failure is not the cropped measurement window. Maze remains disconnected in player-fit The remaining failure is not solved by treating a local origin-inclusive window as the whole strate:
volume even when the origin is covered and when the full 32M-cell step-1 centered window is measured. that Maze window is connected, while the full 32M-cell step-1 centered window remains disconnected.
The proven spanning-tree graph and the player-fit volume are different claims: the graph has a route, The proven spanning-tree graph and the player-fit volume are different claims. The landing branches
but the generated player-fitting corridor/mouth volume does not currently provide a continuous route are substantial and explicit-root-connected, but the native player-fitting route between them still
from these two passage mouths. The smallest next generation task is therefore at the mouths or their fails at whole-strate scale. That is a genuine between-landings/archetype-network finding, not a
player-fit join to the corridor network (with the possibility of a fine-floor/stencil break along measurement crop, a tiny landing pocket, or a weakened assertion.
the route), not another window, size, density, or topology tuning pass.
@@ -421,13 +421,18 @@ bool FVoxelForgeComposerPromotionTest::RunTest(const FString& Parameters)
if (Passage.LowerStrateIndex == CandidateStrateIndex if (Passage.LowerStrateIndex == CandidateStrateIndex
&& Passage.UpperStrateIndex + 1 == CandidateStrateIndex) && Passage.UpperStrateIndex + 1 == CandidateStrateIndex)
{ {
ArrivalPoint = Passage.LowerPoint; // The promotion simulation intentionally remains the legacy coarse air-law probe
// (SampleStep=8), not the Tier-1 fine player-fit law. A floor standing anchor can lie
// in the solid half of a coarse cell after the landing adds its support slab; the
// deterministic door anchor is the actual air position where the tube meets that
// landing and keeps the strict no-snap assertion meaningful.
ArrivalPoint = Passage.LowerLanding.DoorPoint;
++ArrivalCount; ++ArrivalCount;
} }
if (Passage.UpperStrateIndex == CandidateStrateIndex if (Passage.UpperStrateIndex == CandidateStrateIndex
&& Passage.LowerStrateIndex == CandidateStrateIndex + 1) && Passage.LowerStrateIndex == CandidateStrateIndex + 1)
{ {
DeparturePoint = Passage.UpperPoint; DeparturePoint = Passage.UpperLanding.DoorPoint;
++DepartureCount; ++DepartureCount;
} }
} }
@@ -136,6 +136,28 @@ namespace
&& VoxelForgeTest::BitEqual(A.Z, B.Z); && VoxelForgeTest::BitEqual(A.Z, B.Z);
} }
bool SameLanding(const FVoxelPassageLanding& A, const FVoxelPassageLanding& B)
{
return SameVectorBits(A.StandingPoint, B.StandingPoint)
&& SameVectorBits(A.DoorPoint, B.DoorPoint)
&& SameVectorBits(A.DoorDirection, B.DoorDirection)
&& SameVectorBits(A.ConnectorStart, B.ConnectorStart)
&& SameVectorBits(A.ConnectorControl, B.ConnectorControl)
&& SameVectorBits(A.ConnectorEnd, B.ConnectorEnd)
&& VoxelForgeTest::BitEqual(A.FloorZ, B.FloorZ)
&& VoxelForgeTest::BitEqual(A.CeilingZ, B.CeilingZ)
&& VoxelForgeTest::BitEqual(A.HalfWidth, B.HalfWidth)
&& VoxelForgeTest::BitEqual(A.FloorThickness, B.FloorThickness)
&& VoxelForgeTest::BitEqual(A.ConnectorRadius, B.ConnectorRadius)
&& VoxelForgeTest::BitEqual(A.ConnectorCeilingZ, B.ConnectorCeilingZ)
&& VoxelForgeTest::BitEqual(A.RootFloorZ, B.RootFloorZ)
&& VoxelForgeTest::BitEqual(A.RootCeilingZ, B.RootCeilingZ)
&& VoxelForgeTest::BitEqual(A.RootSpineRadius, B.RootSpineRadius)
&& A.bSourcePlayerFit == B.bSourcePlayerFit
&& A.bHasNetworkConnector == B.bHasNetworkConnector
&& A.bHasConnectorBend == B.bHasConnectorBend;
}
bool SamePassage(const FVoxelPassage& A, const FVoxelPassage& B, FString& OutMismatch, bool SamePassage(const FVoxelPassage& A, const FVoxelPassage& B, FString& OutMismatch,
int32 PassageIndex) int32 PassageIndex)
{ {
@@ -155,6 +177,14 @@ namespace
return false; return false;
} }
if (!SameLanding(A.UpperLanding, B.UpperLanding)
|| !SameLanding(A.LowerLanding, B.LowerLanding))
{
OutMismatch = FString::Printf(
TEXT("passage %d landing descriptors differ"), PassageIndex);
return false;
}
if (!VoxelForgeTest::BitEqual(A.Radius, B.Radius)) if (!VoxelForgeTest::BitEqual(A.Radius, B.Radius))
{ {
OutMismatch = FString::Printf( OutMismatch = FString::Printf(
@@ -4,6 +4,7 @@
#if WITH_DEV_AUTOMATION_TESTS #if WITH_DEV_AUTOMATION_TESTS
#include "HAL/PlatformTime.h"
#include "Misc/AutomationTest.h" #include "Misc/AutomationTest.h"
#include "VoxelCaveMorphology.h" #include "VoxelCaveMorphology.h"
@@ -65,6 +66,29 @@ namespace
return 0.0f; return 0.0f;
} }
} }
float BoundarySealFor(const UVoxelStrateDefinition& Definition)
{
switch (Definition.GeneratorType)
{
case ECaveGeneratorType::TunnelNetwork:
case ECaveGeneratorType::Underwater:
return Definition.GenerationParams.BoundarySealThickness;
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber:
return Definition.SlabParams.BoundarySealThickness;
case ECaveGeneratorType::Maze:
return Definition.MazeParams.BoundarySealThickness;
case ECaveGeneratorType::SurfaceWorld:
return Definition.SurfaceParams.BoundarySealThickness;
case ECaveGeneratorType::VerticalShafts:
return Definition.VerticalShaftParams.BoundarySealThickness;
case ECaveGeneratorType::FloatingIslands:
return Definition.FloatingIslandParams.BoundarySealThickness;
default:
return 0.0f;
}
}
} }
bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters) bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
@@ -104,6 +128,27 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
bool bAllAnswerableFootingsAreValid = true; bool bAllAnswerableFootingsAreValid = true;
bool bAllAnswerableEndpointsMatchQuery = true; bool bAllAnswerableEndpointsMatchQuery = true;
int32 NumLandingEnds = 0;
int32 NumLandingFloorsPassed = 0;
int32 NumLandingFloorFailures = 0;
int32 NumLandingNetworkJoins = 0;
int32 NumLandingSealFailures = 0;
int32 NumLandingFloorBoxes = 0;
int32 NumLandingBoxProofs = 0;
int32 NumLandingBoxAllSolid = 0;
int32 NumLandingBoxAllAir = 0;
int32 NumLandingBoxViolations = 0;
int32 NumLandingSourceFit = 0;
int32 NumLandingConnectorSlopesPassed = 0;
int32 NumLandingConnectorSlopeFailures = 0;
float WorstLandingFloorGradient = 0.0f;
float WorstLandingConnectorGradient = 0.0f;
FString FirstLandingFailure;
FString LandingSlopeReport;
constexpr float LandingWalkableAngleDegrees = 44.0f;
const float LandingWalkableSlope = FMath::Tan(
FMath::DegreesToRadians(LandingWalkableAngleDegrees));
for (const FVoxelPassage& Passage : Passages) for (const FVoxelPassage& Passage : Passages)
{ {
// The surface entry is a same-strate shaft and is not an inter-strate destination query. // The surface entry is a same-strate shaft and is not an inter-strate destination query.
@@ -279,8 +324,9 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
++NumChecked; ++NumChecked;
AnsweredArchetypes.Add(static_cast<uint8>(Definition->GeneratorType)); AnsweredArchetypes.Add(static_cast<uint8>(Definition->GeneratorType));
// Control-point endpoints are pinned to the complete query result, including any lateral // Standing endpoints remain pinned to the complete query result, including any lateral
// snap. A mismatch here means GeneratePassages used stale XY or stale Z downstream. // snap. The tube endpoint is now the landing door, so this intentionally compares the
// public standing anchor rather than the first/last control point.
if (!FMath::IsNearlyEqual(Passage.LowerPoint.X, SuggestedPoint.X, 0.01f) if (!FMath::IsNearlyEqual(Passage.LowerPoint.X, SuggestedPoint.X, 0.01f)
|| !FMath::IsNearlyEqual(Passage.LowerPoint.Y, SuggestedPoint.Y, 0.01f) || !FMath::IsNearlyEqual(Passage.LowerPoint.Y, SuggestedPoint.Y, 0.01f)
|| !FMath::IsNearlyEqual(Passage.LowerPoint.Z, SuggestedPoint.Z, 0.01f)) || !FMath::IsNearlyEqual(Passage.LowerPoint.Z, SuggestedPoint.Z, 0.01f))
@@ -451,6 +497,337 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
} }
} }
//==========================================================================
// LANDING GEOMETRY — the mouth is now measured as a room, not as a tube.
//==========================================================================
// Use the final production density, including the legacy structural post and any
// disturbance backstop. Five points on every floor give a small numerical walkability
// stencil; the floor writer itself is a plane, but the test must inspect the resulting field.
for (const FVoxelPassage& Passage : Passages)
{
if (Passage.UpperStrateIndex == Passage.LowerStrateIndex) continue;
const FVoxelPassageLanding* Landings[] = {
&Passage.UpperLanding, &Passage.LowerLanding };
const int32 StrateIndices[] = {
Passage.UpperStrateIndex, Passage.LowerStrateIndex };
for (int32 EndIndex = 0; EndIndex < 2; ++EndIndex)
{
++NumLandingEnds;
const FVoxelPassageLanding& Landing = *Landings[EndIndex];
const int32 StrateIndex = StrateIndices[EndIndex];
const UVoxelStrateDefinition* Definition = Layout.IsValidIndex(StrateIndex)
? Layout[StrateIndex].Definition : nullptr;
auto RecordFailure = [&](const FString& Message)
{
if (FirstLandingFailure.IsEmpty()) FirstLandingFailure = Message;
};
if (Definition == nullptr
|| Landing.HalfWidth <= 0.0f
|| Landing.CeilingZ <= Landing.FloorZ
|| !FMath::IsFinite(Landing.FloorZ)
|| !FMath::IsFinite(Landing.CeilingZ)
|| !FMath::IsFinite(Landing.HalfWidth))
{
++NumLandingFloorFailures;
RecordFailure(FString::Printf(
TEXT("invalid landing descriptor at passage strate end %d/%d"),
Passage.UpperStrateIndex, Passage.LowerStrateIndex));
continue;
}
const float StrateTopZ = (float)(Layout[StrateIndex].TopChunkZ + 1) * CHUNK_SIZE;
const float StrateBottomZ = (float)Layout[StrateIndex].BottomChunkZ * CHUNK_SIZE;
const float Seal = BoundarySealFor(*Definition);
if (!(Landing.FloorZ > StrateBottomZ + Seal)
|| !(Landing.CeilingZ < StrateTopZ - Seal))
{
++NumLandingSealFailures;
RecordFailure(FString::Printf(
TEXT("landing at strate %d reaches its seal (floor %.3f, ceiling %.3f, interior %.3f..%.3f)"),
StrateIndex, Landing.FloorZ, Landing.CeilingZ,
StrateBottomZ + Seal, StrateTopZ - Seal));
}
if ((Landing.StandingPoint - (EndIndex == 0
? Passage.UpperPoint : Passage.LowerPoint)).SizeSquared() > 0.0001f)
{
++NumLandingFloorFailures;
RecordFailure(FString::Printf(
TEXT("landing standing anchor does not match passage %s point at strate %d"),
EndIndex == 0 ? TEXT("upper") : TEXT("lower"), StrateIndex));
}
const bool bWidthAndHeadroom = Landing.HalfWidth * 2.0f >= 12.0f - 0.01f
&& Landing.CeilingZ - Landing.FloorZ >= 12.0f - 0.01f;
const bool bDoorTangent = FMath::IsNearlyEqual(
Landing.DoorPoint.Z - Landing.FloorZ,
Passage.Radius >= 0.0f ? FMath::Max(
Passage.ControlRadii.Num() > 0
? Passage.ControlRadii[0] : Passage.Radius,
1.0f) : 1.0f,
0.01f);
if (!bWidthAndHeadroom || !bDoorTangent)
{
++NumLandingFloorFailures;
RecordFailure(FString::Printf(
TEXT("landing at strate %d failed body/door arithmetic (width %.3f, height %.3f, door rise %.3f)"),
StrateIndex, Landing.HalfWidth * 2.0f,
Landing.CeilingZ - Landing.FloorZ,
Landing.DoorPoint.Z - Landing.FloorZ));
}
const FVector FloorSamples[] = {
Landing.StandingPoint,
Landing.StandingPoint + FVector(2.0f, 0.0f, 0.0f),
Landing.StandingPoint + FVector(-2.0f, 0.0f, 0.0f),
Landing.StandingPoint + FVector(0.0f, 2.0f, 0.0f),
Landing.StandingPoint + FVector(0.0f, -2.0f, 0.0f) };
float SurfaceHeights[UE_ARRAY_COUNT(FloorSamples)] = {};
bool bFloorPassed = true;
float LandingGradient = -1.0f;
for (int32 SampleIndex = 0; SampleIndex < UE_ARRAY_COUNT(FloorSamples); ++SampleIndex)
{
const FVector& Sample = FloorSamples[SampleIndex];
const float Below = World.Generator->GetDensityAt(
Sample.X, Sample.Y, Landing.FloorZ - 1.0f);
const float Above = World.Generator->GetDensityAt(
Sample.X, Sample.Y, Landing.FloorZ + 1.0f);
const float Denominator = Above - Below;
if (!FMath::IsFinite(Below) || !FMath::IsFinite(Above)
|| !(Below < 0.0f) || !(Above >= 0.0f)
|| !(Denominator > 0.0f))
{
bFloorPassed = false;
continue;
}
SurfaceHeights[SampleIndex] = Landing.FloorZ - 1.0f
+ FMath::Clamp(-Below / Denominator, 0.0f, 1.0f) * 2.0f;
}
if (bFloorPassed)
{
float MaxGradient = 0.0f;
for (int32 SampleIndex = 1; SampleIndex < UE_ARRAY_COUNT(FloorSamples); ++SampleIndex)
{
MaxGradient = FMath::Max(MaxGradient,
FMath::Abs(SurfaceHeights[SampleIndex] - SurfaceHeights[0]) / 2.0f);
}
LandingGradient = MaxGradient;
WorstLandingFloorGradient = FMath::Max(WorstLandingFloorGradient, MaxGradient);
const float NormalZ = 1.0f / FMath::Sqrt(1.0f + MaxGradient * MaxGradient);
bFloorPassed = NormalZ + KINDA_SMALL_NUMBER >= FMath::Cos(
FMath::DegreesToRadians(LandingWalkableAngleDegrees));
}
float ConnectorGradient = -1.0f;
bool bConnectorSlopePassed = false;
if (Landing.bHasNetworkConnector
&& FMath::IsFinite(Landing.ConnectorStart.X)
&& FMath::IsFinite(Landing.ConnectorStart.Y)
&& FMath::IsFinite(Landing.ConnectorEnd.X)
&& FMath::IsFinite(Landing.ConnectorEnd.Y)
&& (!Landing.bHasConnectorBend
|| (FMath::IsFinite(Landing.ConnectorControl.X)
&& FMath::IsFinite(Landing.ConnectorControl.Y)))
&& FMath::IsFinite(Landing.RootFloorZ)
&& FMath::IsFinite(Landing.FloorZ))
{
const FVector2D ConnectorStart(
Landing.ConnectorStart.X, Landing.ConnectorStart.Y);
const FVector2D ConnectorEnd(
Landing.ConnectorEnd.X, Landing.ConnectorEnd.Y);
const float DirectLength = (ConnectorEnd - ConnectorStart).Size();
if (Landing.bHasConnectorBend)
{
const FVector2D ConnectorControl(
Landing.ConnectorControl.X, Landing.ConnectorControl.Y);
const float FinalLegLength = (ConnectorEnd - ConnectorControl).Size();
const float RampLength = FMath::Max(
FinalLegLength - (Landing.RootSpineRadius > 0.0f ? 2.25f : 0.0f),
KINDA_SMALL_NUMBER);
ConnectorGradient = FMath::Abs(
Landing.RootFloorZ - Landing.FloorZ) / RampLength;
}
else
{
const float RampLength = FMath::Max(
DirectLength - FMath::Max(Landing.HalfWidth - 1.0f, 0.0f)
- (Landing.RootSpineRadius > 0.0f ? 2.25f : 0.0f),
KINDA_SMALL_NUMBER);
ConnectorGradient = FMath::Abs(
Landing.RootFloorZ - Landing.FloorZ) / RampLength;
}
bConnectorSlopePassed = FMath::IsFinite(ConnectorGradient)
&& ConnectorGradient <= LandingWalkableSlope + KINDA_SMALL_NUMBER;
}
if (bConnectorSlopePassed)
{
++NumLandingConnectorSlopesPassed;
WorstLandingConnectorGradient = FMath::Max(
WorstLandingConnectorGradient, ConnectorGradient);
}
else
{
++NumLandingConnectorSlopeFailures;
RecordFailure(FString::Printf(
TEXT("landing at strate %d has connector slope %.6f above the %.6f walkable limit"),
StrateIndex, ConnectorGradient, LandingWalkableSlope));
}
const bool bLandingSlopePassed = bFloorPassed && bConnectorSlopePassed;
if (bLandingSlopePassed)
{
++NumLandingFloorsPassed;
}
else
{
++NumLandingFloorFailures;
RecordFailure(FString::Printf(
TEXT("landing at strate %d failed its final-density flat-floor slope bracket"),
StrateIndex));
}
if (Landing.bSourcePlayerFit)
{
++NumLandingSourceFit;
}
if (Landing.bHasNetworkConnector)
{
++NumLandingNetworkJoins;
}
else
{
RecordFailure(FString::Printf(
TEXT("landing at strate %d has neither a source fit nor a deterministic network connector"),
StrateIndex));
}
// Exercise the same box shortcut that can otherwise erase a support slab. A uniform
// verdict is allowed only if every exact ClassifyTile lattice sample agrees with the
// final density; a floor-bearing landing should normally force Mixed via the
// bidirectional passage effect. This is one aggregate audit, not per-sample logging.
const FIntVector LandingBoxOrigin(
FMath::FloorToInt(Landing.StandingPoint.X) - 4,
FMath::FloorToInt(Landing.StandingPoint.Y) - 4,
FMath::FloorToInt(Landing.FloorZ) - 3);
constexpr int32 LandingBoxCells = 8;
constexpr int32 LandingBoxStep = 1;
const EVoxelTileClass LandingBoxVerdict = World.Generator->ClassifyTile(
LandingBoxOrigin, LandingBoxStep, LandingBoxCells);
++NumLandingFloorBoxes;
if (LandingBoxVerdict != EVoxelTileClass::Mixed)
{
++NumLandingBoxProofs;
if (LandingBoxVerdict == EVoxelTileClass::AllSolid) ++NumLandingBoxAllSolid;
if (LandingBoxVerdict == EVoxelTileClass::AllAir) ++NumLandingBoxAllAir;
bool bBoxAgrees = true;
const bool bClaimsSolid = LandingBoxVerdict == EVoxelTileClass::AllSolid;
for (int32 GZ = -1; GZ <= LandingBoxCells + 1 && bBoxAgrees; ++GZ)
{
for (int32 GY = -1; GY <= LandingBoxCells + 1 && bBoxAgrees; ++GY)
{
for (int32 GX = -1; GX <= LandingBoxCells + 1; ++GX)
{
const float Density = World.Generator->GetDensityAt(
(float)(LandingBoxOrigin.X + GX * LandingBoxStep),
(float)(LandingBoxOrigin.Y + GY * LandingBoxStep),
(float)(LandingBoxOrigin.Z + GZ * LandingBoxStep));
const bool bMatches = bClaimsSolid
? Density < 0.0f : Density >= 0.0f;
if (!FMath::IsFinite(Density) || !bMatches)
{
bBoxAgrees = false;
break;
}
}
}
}
if (!bBoxAgrees)
{
++NumLandingBoxViolations;
if (NumLandingBoxViolations == 1)
{
AddError(FString::Printf(
TEXT("Landing box verdict %d was not sound at (%d,%d,%d): the uniform %s verdict disagreed with final density."),
NumLandingBoxProofs,
LandingBoxOrigin.X,
LandingBoxOrigin.Y,
LandingBoxOrigin.Z,
bClaimsSolid ? TEXT("AllSolid") : TEXT("AllAir")));
}
}
}
const FString LandingArchetype = ArchetypeName(Definition->GeneratorType);
LandingSlopeReport += FString::Printf(
TEXT("%s[%d:%s]=floor:%s(%.6f),connector:%s(%.6f)%s "),
*LandingArchetype,
StrateIndex,
EndIndex == 0 ? TEXT("upper") : TEXT("lower"),
bFloorPassed ? TEXT("PASS") : TEXT("FAIL"),
LandingGradient,
bConnectorSlopePassed ? TEXT("PASS") : TEXT("FAIL"),
ConnectorGradient,
Landing.bHasConnectorBend ? TEXT("[bend]") : TEXT(""));
}
}
// The passage modifier is called once per density sample, but shortlist construction is
// keyed by (manager, layout version, chunk) in thread_local storage. Measure the hot hit and
// the deliberately alternating two-chunk rebuild separately so the report exposes the cache
// contract without adding a log line for any individual sample.
const FVoxelPassage* TimingPassage = nullptr;
for (const FVoxelPassage& Passage : Passages)
{
if (Passage.UpperStrateIndex != Passage.LowerStrateIndex)
{
TimingPassage = &Passage;
break;
}
}
if (TimingPassage != nullptr)
{
const FVector TimingPoint = TimingPassage->UpperLanding.StandingPoint
+ FVector(0.0f, 0.0f, 2.0f);
constexpr int32 HotCalls = 8192;
constexpr int32 RebuildCalls = 96;
volatile float TimingSink = 0.0f;
double StartSeconds = FPlatformTime::Seconds();
for (int32 Call = 0; Call < HotCalls; ++Call)
{
float Density = 8.0f;
World.StrateManager->ApplyPassageModifier(
Density, TimingPoint.X, TimingPoint.Y, TimingPoint.Z, 8.0f, 0.0f);
TimingSink += Density;
}
const double HotSeconds = FPlatformTime::Seconds() - StartSeconds;
StartSeconds = FPlatformTime::Seconds();
for (int32 Call = 0; Call < RebuildCalls; ++Call)
{
const FVector Point = TimingPoint
+ ((Call & 1) != 0 ? FVector((float)CHUNK_SIZE, 0.0f, 0.0f)
: FVector::ZeroVector);
float Density = 8.0f;
World.StrateManager->ApplyPassageModifier(
Density, Point.X, Point.Y, Point.Z, 8.0f, 0.0f);
TimingSink += Density;
}
const double RebuildSeconds = FPlatformTime::Seconds() - StartSeconds;
AddInfo(FString::Printf(
TEXT("Passage TLS cache timing: rebuild %.3f us/call over %d alternating chunks; hot %.3f us/call over %d same-chunk calls (sink %.3f)."),
RebuildSeconds * 1000000.0 / (double)RebuildCalls,
RebuildCalls,
HotSeconds * 1000000.0 / (double)HotCalls,
HotCalls,
(float)TimingSink));
}
AddInfo(FString::Printf( AddInfo(FString::Printf(
TEXT("Passage player-fit check: %d inter-strate passages, upper %d checked (%d false), lower %d checked (%d footing checks), %d/%d room/slab ring samples air, lower %d query-false (%d unique archetypes; %d unsupported, %d supported-but-no-point)."), TEXT("Passage player-fit check: %d inter-strate passages, upper %d checked (%d false), lower %d checked (%d footing checks), %d/%d room/slab ring samples air, lower %d query-false (%d unique archetypes; %d unsupported, %d supported-but-no-point)."),
NumInterStratePassages, NumInterStratePassages,
@@ -464,6 +841,36 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
FalseArchetypes.Num(), FalseArchetypes.Num(),
NumUnsupported, NumUnsupported,
NumSupportedWithoutPoint)); NumSupportedWithoutPoint));
AddInfo(FString::Printf(
TEXT("Landing geometry: %d/%d ends passed the final-density floor test (worst room gradient %.6f), connector slopes %d/%d (worst %.6f; walkable limit %.6f), source-fit anchors %d/%d, explicit root-network joins %d/%d, seal violations %d, geometry/floor failures %d."),
NumLandingFloorsPassed,
NumLandingEnds,
WorstLandingFloorGradient,
NumLandingConnectorSlopesPassed,
NumLandingEnds,
WorstLandingConnectorGradient,
LandingWalkableSlope,
NumLandingSourceFit,
NumLandingEnds,
NumLandingNetworkJoins,
NumLandingEnds,
NumLandingSealFailures,
NumLandingFloorFailures));
if (!LandingSlopeReport.IsEmpty())
{
AddInfo(FString::Printf(TEXT("Landing slope tests per end: %s"), *LandingSlopeReport));
}
AddInfo(FString::Printf(
TEXT("Landing floor box verdicts: %d boxes probed, %d uniform proofs (AllSolid %d, AllAir %d), %d violations."),
NumLandingFloorBoxes,
NumLandingBoxProofs,
NumLandingBoxAllSolid,
NumLandingBoxAllAir,
NumLandingBoxViolations));
if (!FirstLandingFailure.IsEmpty())
{
AddError(FString::Printf(TEXT("First landing geometry failure: %s"), *FirstLandingFailure));
}
// A test that inspected nothing is not evidence of the invariant. Fail loudly in both cases. // A test that inspected nothing is not evidence of the invariant. Fail loudly in both cases.
// Un test qui n'a rien inspecté ne prouve pas l'invariant : échouer explicitement dans les deux cas. // Un test qui n'a rien inspecté ne prouve pas l'invariant : échouer explicitement dans les deux cas.
@@ -541,6 +948,14 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
} }
return NumInterStratePassages > 0 return NumInterStratePassages > 0
&& NumLandingEnds == NumInterStratePassages * 2
&& NumLandingFloorsPassed == NumLandingEnds
&& NumLandingConnectorSlopesPassed == NumLandingEnds
&& NumLandingConnectorSlopeFailures == 0
&& NumLandingNetworkJoins == NumLandingEnds
&& NumLandingSealFailures == 0
&& NumLandingFloorFailures == 0
&& NumLandingBoxViolations == 0
&& NumChecked > 0 && NumChecked > 0
&& NumUpperChecked > 0 && NumUpperChecked > 0
&& NumFootingChecked > 0 && NumFootingChecked > 0
@@ -2056,6 +2056,520 @@ namespace
} }
} }
namespace
{
constexpr float VF_LandingCarveSafetyMargin = 4.0f;
struct FVoxelPassageLandingConnectorProjection
{
FVector2D Closest = FVector2D::ZeroVector;
float DistanceAlong = 0.0f;
float TotalLength = 0.0f;
float SegmentLength = 0.0f;
float SegmentParameter = 0.0f;
int32 SegmentIndex = 0;
};
FVoxelPassageLandingConnectorProjection VF_ProjectPassageLandingConnector(
const FVector& Position,
const FVoxelPassageLanding& Landing)
{
const FVector2D A(Landing.ConnectorStart.X, Landing.ConnectorStart.Y);
const FVector2D Control(Landing.ConnectorControl.X, Landing.ConnectorControl.Y);
const FVector2D B(Landing.ConnectorEnd.X, Landing.ConnectorEnd.Y);
const FVector2D P(Position.X, Position.Y);
const bool bBend = Landing.bHasConnectorBend;
const FVector2D FirstEnd = bBend ? Control : B;
const FVector2D FirstVector = FirstEnd - A;
const FVector2D SecondVector = B - Control;
const float FirstLength = FirstVector.Size();
const float SecondLength = bBend ? SecondVector.Size() : 0.0f;
FVoxelPassageLandingConnectorProjection Result;
Result.TotalLength = FirstLength + SecondLength;
Result.Closest = A;
Result.SegmentLength = FirstLength;
if (Result.TotalLength <= KINDA_SMALL_NUMBER)
{
return Result;
}
float BestDistanceSquared = FLT_MAX;
const auto ProjectSegment = [&](const FVector2D& SegmentStart,
const FVector2D& SegmentEnd,
float SegmentOffset,
int32 SegmentIndex)
{
const FVector2D Segment = SegmentEnd - SegmentStart;
const float LengthSquared = Segment.SizeSquared();
const float Parameter = LengthSquared > KINDA_SMALL_NUMBER
? FMath::Clamp(FVector2D::DotProduct(P - SegmentStart, Segment)
/ LengthSquared, 0.0f, 1.0f)
: 0.0f;
const FVector2D Candidate = SegmentStart + Segment * Parameter;
const float DistanceSquared = (P - Candidate).SizeSquared();
if (DistanceSquared < BestDistanceSquared
|| (DistanceSquared == BestDistanceSquared
&& SegmentIndex < Result.SegmentIndex))
{
BestDistanceSquared = DistanceSquared;
Result.Closest = Candidate;
Result.DistanceAlong = SegmentOffset
+ FMath::Sqrt(FMath::Max(LengthSquared, 0.0f)) * Parameter;
Result.SegmentLength = FMath::Sqrt(FMath::Max(LengthSquared, 0.0f));
Result.SegmentParameter = Parameter;
Result.SegmentIndex = SegmentIndex;
}
};
ProjectSegment(A, FirstEnd, 0.0f, 0);
if (bBend)
{
ProjectSegment(Control, B, FirstLength, 1);
}
return Result;
}
float VF_PassageLandingConnectorFloorParameter(
const FVoxelPassageLandingConnectorProjection& Projection,
const FVoxelPassageLanding& Landing)
{
if (Projection.TotalLength <= KINDA_SMALL_NUMBER)
{
return 0.0f;
}
if (Landing.bHasConnectorBend)
{
// The first leg is deliberately level with the room. The second leg carries the
// complete rise and ends with a short level overlap inside the root annulus.
if (Projection.SegmentIndex == 0)
{
return 0.0f;
}
const float RootFlatLength = Landing.RootSpineRadius > 0.0f ? 2.25f : 0.0f;
const float RampLength = FMath::Max(
Projection.SegmentLength - RootFlatLength, KINDA_SMALL_NUMBER);
return FMath::Clamp(
Projection.SegmentParameter * Projection.SegmentLength / RampLength,
0.0f, 1.0f);
}
// The direct zero-rise case remains a single segment. Keep the old end aprons for
// descriptors produced by older cooked data, even though new passages use the bend when
// a non-zero rise needs a guaranteed slope.
const float SourceFlatLength = FMath::Max(Landing.HalfWidth - 1.0f, 0.0f);
const float RootFlatLength = Landing.RootSpineRadius > 0.0f ? 2.25f : 0.0f;
const float SourceDistance = FMath::Clamp(
SourceFlatLength, 0.0f, Projection.TotalLength * 0.49f);
const float RootDistance = FMath::Clamp(
Projection.TotalLength - RootFlatLength,
Projection.TotalLength * 0.51f, Projection.TotalLength);
if (RootDistance <= SourceDistance + KINDA_SMALL_NUMBER)
{
return FMath::Clamp(
Projection.DistanceAlong / Projection.TotalLength, 0.0f, 1.0f);
}
return FMath::Clamp(
(Projection.DistanceAlong - SourceDistance)
/ (RootDistance - SourceDistance), 0.0f, 1.0f);
}
}
FVoxelPassageLanding VF_BuildPassageLanding(
const FVector& InStandingPoint,
float MouthRadius,
const FVector& InDoorDirection,
float StrateTopZ,
float StrateBottomZ,
float BoundarySealThickness,
bool bSourcePlayerFit,
bool bHasNetworkConnector,
const FVector& NetworkPoint,
float RootSpineRadius)
{
FVoxelPassageLanding Landing;
// Body-derived dimensions at the authored 25 cm voxel scale:
// player diameter = 2 * 34 cm = 0.68 m = 2.72 voxels;
// a 3 m turn floor is 3 / 0.25 = 12 voxels across;
// capsule height = 2 * 88 cm = 1.76 m = 7.04 voxels;
// 1 m of headroom adds 4 voxels, so the minimum room height rounds to 12 voxels (3 m).
// The tube radius adds two voxels of clearance around the doorway. The support writer keeps
// one voxel inside the room wall, so HalfWidth=max(7, MouthRadius+2) gives a guaranteed
// 12-voxel = 3 m flat support floor, while the authored chamber is 14 voxels = 3.5 m wide.
const float SafeMouthRadius = FMath::Max(FMath::Abs(MouthRadius), 1.0f);
const float DesiredHalfWidth = FMath::Max(7.0f, SafeMouthRadius + 2.0f);
const float BodyHeightWithHeadroom = FVoxelPlayerCapsuleConstants::HeightVoxels + 4.0f;
const float DesiredHeight = FMath::Max(
FMath::Max(12.0f, BodyHeightWithHeadroom),
2.0f * SafeMouthRadius + 2.0f);
const float SafeSeal = FMath::Max(BoundarySealThickness, 0.0f);
FVector Direction(InDoorDirection.X, InDoorDirection.Y, 0.0f);
if (!Direction.Normalize())
{
Direction = FVector(1.0f, 0.0f, 0.0f);
}
Landing.StandingPoint = InStandingPoint;
Landing.FloorZ = InStandingPoint.Z - 0.5f;
Landing.HalfWidth = DesiredHalfWidth;
Landing.CeilingZ = Landing.FloorZ + DesiredHeight;
Landing.FloorThickness = 3.0f;
Landing.DoorDirection = Direction;
Landing.RootSpineRadius = FMath::Max(RootSpineRadius, 0.0f);
Landing.bSourcePlayerFit = bSourcePlayerFit;
Landing.bHasNetworkConnector = false;
Landing.RootFloorZ = Landing.FloorZ;
Landing.RootCeilingZ = Landing.CeilingZ;
// Keep the complete room and its floor strictly inside the vertical seal. Normal authored
// strates have far more room than this bound; the clamp is a fail-safe for a source pose near
// a seal and never moves a normal interior pose.
const float InnerBottom = StrateBottomZ + SafeSeal;
const float InnerTop = StrateTopZ - SafeSeal;
if (FMath::IsFinite(InnerBottom) && FMath::IsFinite(InnerTop)
&& InnerTop > InnerBottom)
{
// Passage carving is intentionally ordered after the vertical seal and fades over its
// four-voxel blend radius. Keep both the underside of the support slab and the top of the
// room beyond that radius from the seal, so the landing cannot use the passage exception
// to open a strate boundary.
const float MinFloor = InnerBottom + Landing.FloorThickness
+ VF_LandingCarveSafetyMargin;
const float MaxFloor = InnerTop - DesiredHeight - VF_LandingCarveSafetyMargin;
if (MaxFloor >= MinFloor)
{
Landing.FloorZ = FMath::Clamp(Landing.FloorZ, MinFloor, MaxFloor);
Landing.StandingPoint.Z = Landing.FloorZ + 0.5f;
Landing.CeilingZ = Landing.FloorZ + DesiredHeight;
}
else
{
// A degenerate strate cannot satisfy the full body/headroom contract. Fail closed
// instead of emitting a partial chamber whose blend would approach a seal.
Landing.HalfWidth = 0.0f;
Landing.CeilingZ = Landing.FloorZ;
Landing.bSourcePlayerFit = false;
return Landing;
}
}
// The tube centreline is tangent to the floor: the tube bottom is exactly FloorZ. The door
// sits one voxel inside the room so the room/tube smooth union cannot leave a lip.
Landing.DoorPoint = Landing.StandingPoint
+ Direction * FMath::Max(0.0f, Landing.HalfWidth - 1.0f);
Landing.DoorPoint.Z = Landing.FloorZ + SafeMouthRadius;
if (bHasNetworkConnector
&& FMath::IsFinite(NetworkPoint.X)
&& FMath::IsFinite(NetworkPoint.Y)
&& FMath::IsFinite(NetworkPoint.Z))
{
// The connector is a swept rectangular ramp. Its source end keeps the authored landing
// floor; its root end is the common, deterministic interior floor supplied by the
// manager. This is what joins two mouths in one strate when their source floors sit at
// opposite sides of the strate. The manager chooses the common level inside the same
// seal-safe interval as this builder, so no source query or passage order can affect it.
// Five voxels = 1.25 m half-width / 2.5 m clear width. This is the smallest whole-voxel
// annular root corridor that leaves a 1.36-voxel player capsule a two-voxel fit margin on
// the support lattice, while the landing itself remains the 3 m turn floor. Its separate
// 12-voxel (3 m) clear height gives the 1.76 m capsule more than 1 m of headroom while the
// player walks out to the network root. The floor rise is linear along the horizontal
// run; if the direct run is too short, a level dog-leg makes the final ramp satisfy the
// same 44 degree walkable limit by construction.
Landing.ConnectorRadius = 5.0f;
const float ConnectorHeight = FMath::Max(
12.0f, FVoxelPlayerCapsuleConstants::HeightVoxels + 4.0f);
Landing.ConnectorCeilingZ = Landing.FloorZ + ConnectorHeight;
Landing.RootFloorZ = NetworkPoint.Z;
Landing.RootCeilingZ = Landing.RootFloorZ + ConnectorHeight;
// Clamp the supplied common level again at the geometry boundary. This is deliberately
// a clamp, not a per-passage repair: all callers derive NetworkPoint.Z from the strate's
// bounds, and malformed tiny strates remain sealed rather than producing a root breach.
if (FMath::IsFinite(InnerBottom) && FMath::IsFinite(InnerTop)
&& InnerTop > InnerBottom)
{
const float RootMinFloor = InnerBottom + Landing.FloorThickness
+ VF_LandingCarveSafetyMargin;
const float RootMaxFloor = InnerTop - ConnectorHeight
- VF_LandingCarveSafetyMargin;
if (RootMaxFloor >= RootMinFloor)
{
Landing.RootFloorZ = FMath::Clamp(
Landing.RootFloorZ, RootMinFloor, RootMaxFloor);
Landing.RootCeilingZ = Landing.RootFloorZ + ConnectorHeight;
}
else
{
// There is no seal-safe interval for a full landing connector in a malformed
// tiny strate. Fail closed: a partial ramp that reaches a boundary is worse than
// an explicit unanswered landing, and normal authored strates never enter this
// branch.
return Landing;
}
}
else
{
return Landing;
}
const float ConnectorZ = (Landing.FloorZ + Landing.ConnectorCeilingZ) * 0.5f;
const float RootConnectorZ = (Landing.RootFloorZ + Landing.RootCeilingZ) * 0.5f;
FVector2D RootEntryDirection(
Landing.StandingPoint.X - NetworkPoint.X,
Landing.StandingPoint.Y - NetworkPoint.Y);
if (!RootEntryDirection.Normalize())
{
RootEntryDirection = FVector2D(Direction.X, Direction.Y);
if (!RootEntryDirection.Normalize())
{
RootEntryDirection = FVector2D(1.0f, 0.0f);
}
}
// The structural spine is a vertical air column, so its centre is not a standing
// endpoint. End the ramp in the middle of the support annulus instead: with the 1.36-voxel
// capsule radius, a centre at RootSpineRadius+2.25 sits at least one capsule radius outside
// the spine and at least one capsule radius inside the 4.5-voxel support edge. Every
// passage therefore enters the same continuous, floor-backed annular hub, while the spine
// itself remains open and unsealed.
const float RootEntryRadius = Landing.RootSpineRadius + 2.25f;
const FVector2D RootEntryXY(
NetworkPoint.X + RootEntryDirection.X * RootEntryRadius,
NetworkPoint.Y + RootEntryDirection.Y * RootEntryRadius);
Landing.ConnectorStart = FVector(
Landing.StandingPoint.X, Landing.StandingPoint.Y, ConnectorZ);
Landing.ConnectorEnd = FVector(RootEntryXY.X, RootEntryXY.Y, RootConnectorZ);
// A direct source-to-root ramp is usually gentle, but its length is not a contract: a
// legal source pose can be close to the spine while its floor is near a strate face. Add a
// deterministic level dog-leg whenever the direct run cannot meet the 44-degree law.
// The final leg is the only rising part, and its extra horizontal run includes the root
// annulus overlap. This makes the slope a construction guarantee for every seed, rather
// than a property observed in today's showcase seeds. WorldRadiusVoxels is deliberately
// zero, so the bounded world rim cannot be approached by this lateral safety run.
constexpr float WalkableSlope = 0.9656888f; // tan(44 degrees)
constexpr float RootFlatLength = 2.25f;
const FVector2D SourceXY(Landing.ConnectorStart.X, Landing.ConnectorStart.Y);
const FVector2D RootXY(Landing.ConnectorEnd.X, Landing.ConnectorEnd.Y);
const FVector2D Direct = RootXY - SourceXY;
const float DirectLength = Direct.Size();
const float FloorRise = FMath::Abs(Landing.RootFloorZ - Landing.FloorZ);
const float RequiredRampLength = FloorRise / WalkableSlope;
const float SourceFlatLength = FMath::Max(Landing.HalfWidth - 1.0f, 0.0f);
if (FloorRise > KINDA_SMALL_NUMBER
&& (DirectLength <= KINDA_SMALL_NUMBER
|| DirectLength < RequiredRampLength + RootFlatLength + SourceFlatLength))
{
FVector2D Along = Direct;
if (!Along.Normalize())
{
Along = FVector2D(Direction.X, Direction.Y);
if (!Along.Normalize())
{
Along = FVector2D(1.0f, 0.0f);
}
}
const FVector2D Perpendicular(-Along.Y, Along.X);
const float DogLegLength = RequiredRampLength + RootFlatLength + 1.0f;
const FVector2D ControlXY = RootXY + Perpendicular * DogLegLength;
Landing.ConnectorControl = FVector(ControlXY.X, ControlXY.Y, ConnectorZ);
Landing.bHasConnectorBend = true;
}
else if (FloorRise > KINDA_SMALL_NUMBER)
{
// Keep the same straight geometry when it already satisfies the bound. The explicit
// false flag is part of the descriptor so the evaluator and order-independence test
// cannot infer a different topology from an uninitialised control point.
Landing.ConnectorControl = FVector::ZeroVector;
Landing.bHasConnectorBend = false;
}
else
{
Landing.ConnectorControl = FVector(
(Landing.ConnectorStart.X + Landing.ConnectorEnd.X) * 0.5f,
(Landing.ConnectorStart.Y + Landing.ConnectorEnd.Y) * 0.5f,
ConnectorZ);
Landing.bHasConnectorBend = false;
}
Landing.bHasNetworkConnector = true;
}
return Landing;
}
float VF_EvaluatePassageLandingSDF(
const FVector& Position,
const FVoxelPassageLanding& Landing)
{
if (!FMath::IsFinite(Position.X) || !FMath::IsFinite(Position.Y)
|| !FMath::IsFinite(Position.Z)
|| !FMath::IsFinite(Landing.StandingPoint.X)
|| !FMath::IsFinite(Landing.StandingPoint.Y)
|| !FMath::IsFinite(Landing.FloorZ)
|| !FMath::IsFinite(Landing.CeilingZ)
|| !FMath::IsFinite(Landing.HalfWidth)
|| Landing.HalfWidth <= 0.0f
|| Landing.CeilingZ <= Landing.FloorZ)
{
return FLT_MAX;
}
const FVector RoomCenter(
Landing.StandingPoint.X,
Landing.StandingPoint.Y,
(Landing.FloorZ + Landing.CeilingZ) * 0.5f);
constexpr float RoomRounding = 1.5f;
const FVector RoomHalfExtent(
FMath::Max(Landing.HalfWidth - RoomRounding, 0.25f),
FMath::Max(Landing.HalfWidth - RoomRounding, 0.25f),
FMath::Max((Landing.CeilingZ - Landing.FloorZ) * 0.5f - RoomRounding, 0.25f));
// max(Room, FloorZ-Z) is the important distinction from a bore: below FloorZ is solid,
// while every horizontal section above it has the same flat floor plane.
float LandingSDF = FMath::Max(
VoxelSDF::RoundedBox(Position, RoomCenter, RoomHalfExtent, RoomRounding),
Landing.FloorZ - Position.Z);
if (Landing.bHasNetworkConnector && Landing.ConnectorRadius > 0.0f
&& FMath::IsFinite(Landing.ConnectorCeilingZ)
&& Landing.ConnectorCeilingZ > Landing.FloorZ
&& FMath::IsFinite(Landing.RootFloorZ)
&& FMath::IsFinite(Landing.RootCeilingZ)
&& Landing.RootCeilingZ > Landing.RootFloorZ)
{
const FVoxelPassageLandingConnectorProjection Projection =
VF_ProjectPassageLandingConnector(Position, Landing);
const float FloorT = VF_PassageLandingConnectorFloorParameter(Projection, Landing);
const float HorizontalSDF = (FVector2D(Position.X, Position.Y)
- Projection.Closest).Size()
- Landing.ConnectorRadius;
const float ConnectorFloorZ = FMath::Lerp(
Landing.FloorZ, Landing.RootFloorZ, FloorT);
const float ConnectorCeilingZ = FMath::Lerp(
Landing.ConnectorCeilingZ, Landing.RootCeilingZ, FloorT);
const float VerticalSDF = FMath::Max(
ConnectorFloorZ - Position.Z,
Position.Z - ConnectorCeilingZ);
// A swept box with a flat source leg and a measured-slope final ramp, not a round bore:
// the floor normal is deterministic and bounded, while the fixed twelve-voxel clear height
// follows the route. The source and root ends are exactly the two flat landing floors.
const float ConnectorSDF = FMath::Max(HorizontalSDF, VerticalSDF);
LandingSDF = VoxelSDF::SmoothMin(LandingSDF, ConnectorSDF, 3.0f);
if (Landing.RootSpineRadius >= 0.0f)
{
// A shared hub turns the guaranteed root join into a walkable network. With the normal
// origin spine it is an annulus outside that shaft; with a disabled spine it safely
// degenerates to a disk, so the landing contract still has a guaranteed root volume.
const float HubRadius = Landing.RootSpineRadius + Landing.ConnectorRadius;
const float RadialDistance = FMath::Sqrt(
FMath::Square(Position.X) + FMath::Square(Position.Y));
const float HubHorizontalSDF = FMath::Max(
Landing.RootSpineRadius - RadialDistance,
RadialDistance - HubRadius);
const float HubVerticalSDF = FMath::Max(
Landing.RootFloorZ - Position.Z,
Position.Z - Landing.RootCeilingZ);
const float HubSDF = FMath::Max(HubHorizontalSDF, HubVerticalSDF);
LandingSDF = VoxelSDF::SmoothMin(LandingSDF, HubSDF, 3.0f);
}
}
return LandingSDF;
}
bool VF_IsPassageLandingFloor(
const FVector& Position,
const FVoxelPassageLanding& Landing)
{
if (!FMath::IsFinite(Position.X) || !FMath::IsFinite(Position.Y)
|| !FMath::IsFinite(Position.Z) || !FMath::IsFinite(Landing.FloorZ)
|| !FMath::IsFinite(Landing.FloorThickness)
|| Landing.FloorThickness <= 0.0f || Landing.HalfWidth <= 0.0f)
{
return false;
}
const auto IsInFloorBand = [](float Z, float FloorZ, float Thickness)
{
// The analytic ramp and the sampled density use the same float interpolation, but a
// caller can arrive at the mathematically identical plane through a different fused
// multiply/add sequence. Include one machine epsilon on the upper side so the support
// backstop cannot disappear at an exact floor sample.
return Z <= FloorZ + KINDA_SMALL_NUMBER && Z > FloorZ - Thickness;
};
const bool bInsideRootSpine = Landing.RootSpineRadius > 0.0f
&& FMath::Square(Position.X) + FMath::Square(Position.Y)
<= FMath::Square(Landing.RootSpineRadius);
// The room floor is a true horizontal landing: it is the only floor that owns the full
// three-metre turning patch. The central root spine is left open.
if (IsInFloorBand(Position.Z, Landing.FloorZ, Landing.FloorThickness)
&& !bInsideRootSpine)
{
const float FloorHalfWidth = FMath::Max(Landing.HalfWidth - 1.0f, 0.0f);
if (FMath::Abs(Position.X - Landing.StandingPoint.X) <= FloorHalfWidth
&& FMath::Abs(Position.Y - Landing.StandingPoint.Y) <= FloorHalfWidth)
{
return true;
}
}
if (Landing.bHasNetworkConnector && Landing.ConnectorRadius > 0.0f
&& FMath::IsFinite(Landing.RootFloorZ)
&& FMath::IsFinite(Landing.RootCeilingZ)
&& Landing.RootCeilingZ > Landing.RootFloorZ)
{
const FVoxelPassageLandingConnectorProjection Projection =
VF_ProjectPassageLandingConnector(Position, Landing);
const FVector2D& Closest = Projection.Closest;
const float FloorT = VF_PassageLandingConnectorFloorParameter(Projection, Landing);
const float ConnectorFloorZ = FMath::Lerp(
Landing.FloorZ, Landing.RootFloorZ, FloorT);
if (!bInsideRootSpine
&& IsInFloorBand(Position.Z, ConnectorFloorZ, Landing.FloorThickness))
{
// Keep connector support inset by half a voxel from its clear width. The landing
// chamber—not this narrow transit leg—owns the 3 m turn floor; this inset preserves
// a solid walking strip without capping a neighbouring shaft at its edge.
const float FloorRadius = FMath::Max(
Landing.ConnectorRadius - 0.5f,
FVoxelPlayerCapsuleConstants::RadiusVoxels);
if ((FVector2D(Position.X, Position.Y) - Closest).SizeSquared()
<= FMath::Square(FloorRadius))
{
return true;
}
}
if (IsInFloorBand(Position.Z, Landing.RootFloorZ, Landing.FloorThickness))
{
// The common root hub is a walkable annulus, not a cap over the vertical spine. Its
// 4.5-voxel support inset leaves the same 0.5-voxel air clearance at the outer wall as
// the connector and never writes inside the spine radius.
const float RadialDistanceSquared = FMath::Square(Position.X)
+ FMath::Square(Position.Y);
const float HubInnerRadius = Landing.RootSpineRadius;
const float HubOuterSupportRadius = Landing.RootSpineRadius
+ FMath::Max(Landing.ConnectorRadius - 0.5f,
FVoxelPlayerCapsuleConstants::RadiusVoxels);
const bool bOutsideSpine = Landing.RootSpineRadius <= 0.0f
|| RadialDistanceSquared > FMath::Square(HubInnerRadius);
if (bOutsideSpine
&& RadialDistanceSquared <= FMath::Square(HubOuterSupportRadius))
{
return true;
}
}
}
return false;
}
bool VF_SuggestLandingPoint( bool VF_SuggestLandingPoint(
ECaveGeneratorType Archetype, ECaveGeneratorType Archetype,
const FStrateGenerationParams& CaveParams, const FStrateGenerationParams& CaveParams,
@@ -1854,8 +1854,7 @@ namespace
void Eval(float X, float Y, float Z, FVoxelOpSample& InOut) const override void Eval(float X, float Y, float Z, FVoxelOpSample& InOut) const override
{ {
if (!Manager) { return; } if (!Manager) { return; }
const float ModSDF = Manager->EvaluateModifierSDF(X, Y, Z); Manager->ApplyPassageModifier(InOut.Density, X, Y, Z, Base, Seal);
VF_ApplyPassageCarving(InOut.Density, ModSDF, Base, Seal);
} }
// ≡ la garde `AnyPassageNearBox` écrite à la main dans ClassifyTile — déjà écrite, ici // ≡ la garde `AnyPassageNearBox` écrite à la main dans ClassifyTile — déjà écrite, ici
@@ -1863,6 +1862,12 @@ namespace
EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext&) const override EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext&) const override
{ {
if (!Manager) { return EVoxelOpEffect::Identity; } if (!Manager) { return EVoxelOpEffect::Identity; }
if (Manager->AnyPassageLandingFloorNearBox(VoxelBox.Min, VoxelBox.Max))
{
// The room carves air, while its support slab force-writes solid. Both
// hypotheses must therefore be killed for a box touching that slab.
return EVoxelOpEffect::Both;
}
return Manager->AnyPassageNearBox(VoxelBox.Min, VoxelBox.Max) return Manager->AnyPassageNearBox(VoxelBox.Min, VoxelBox.Max)
? EVoxelOpEffect::CarveOnly : EVoxelOpEffect::Identity; ? EVoxelOpEffect::CarveOnly : EVoxelOpEffect::Identity;
} }
@@ -5207,10 +5212,10 @@ void FVoxelOpStack::AppendStructuralPost(float StrateTopWorldZ, float StrateBott
const UVoxelStrateManager* StrateManager) const UVoxelStrateManager* StrateManager)
{ {
// ORDRE NON NÉGOCIABLE : la spine creuse l'intérieur (et ne touche JAMAIS les bandes de seal), // ORDRE NON NÉGOCIABLE : la spine creuse l'intérieur (et ne touche JAMAIS les bandes de seal),
// le seal vertical re-solidifie ses bandes, les passages percent les seals, puis la limite XY // le seal vertical re-solidifie ses bandes, les passages (tube + landing + support) percent
// gagne sur tout ce qui précède. Ainsi, même un passage placé dans la rampe ou au-delà du rayon // les seals, puis la limite XY gagne sur tout ce qui précède. Ainsi, même un passage placé
// ne peut pas ouvrir la coque extérieure. Les éditions joueur restent le dernier post de // dans la rampe ou au-delà du rayon ne peut pas ouvrir la coque extérieure. Les éditions joueur
// GetDensityAt, hors de cette pile, comme avant. // restent le dernier post de GetDensityAt, hors de cette pile, comme avant.
Add(MakeUnique<FOriginSpineOp>(StrateTopWorldZ, StrateBottomWorldZ, SealThickness, BaseDensity, SpineRadius)); Add(MakeUnique<FOriginSpineOp>(StrateTopWorldZ, StrateBottomWorldZ, SealThickness, BaseDensity, SpineRadius));
Add(MakeUnique<FBoundarySealOp>(StrateTopWorldZ, StrateBottomWorldZ, SealThickness, BaseDensity)); Add(MakeUnique<FBoundarySealOp>(StrateTopWorldZ, StrateBottomWorldZ, SealThickness, BaseDensity));
Add(MakeUnique<FPassageCarveOp>(StrateManager, BaseDensity, SealThickness)); Add(MakeUnique<FPassageCarveOp>(StrateManager, BaseDensity, SealThickness));
+107 -21
View File
@@ -289,12 +289,6 @@ static FORCEINLINE void ApplyBoundarySeal(float& Density, float WorldZ,
VF_ApplyBoundarySeal(Density, WorldZ, StrateTopZ, StrateBottomZ, Thickness, BaseDensity); VF_ApplyBoundarySeal(Density, WorldZ, StrateTopZ, StrateBottomZ, Thickness, BaseDensity);
} }
static FORCEINLINE void ApplyPassageCarving(float& Density, float ModSDF,
float BaseDensity, float SealThickness)
{
VF_ApplyPassageCarving(Density, ModSDF, BaseDensity, SealThickness);
}
static FORCEINLINE void ApplyOriginSpine(float& Density, float WorldX, float WorldY, float WorldZ, static FORCEINLINE void ApplyOriginSpine(float& Density, float WorldX, float WorldY, float WorldZ,
float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius) float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius)
{ {
@@ -681,11 +675,12 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
{ {
// SOLID BEDROCK gap between two strates. The auto-carved passages still tunnel // SOLID BEDROCK gap between two strates. The auto-carved passages still tunnel
// through it, but the (0,0) descent stays solid here so the player digs the gap // through it, but the (0,0) descent stays solid here so the player digs the gap
// to reach the next layer. No caves, no spine, no vertical seal — just rock + passages; // to reach the next layer. No caves, no spine, no vertical seal — just rock + passage
// tube/landing;
// the global XY edge seal is applied below for bounded worlds. // the global XY edge seal is applied below for bounded worlds.
float Density = 8.0f; // bedrock solidity (positive = solid) float Density = 8.0f; // bedrock solidity (positive = solid)
const float ModSDF = StrateManager->EvaluateModifierSDF(WorldX, WorldY, WorldZ); StrateManager->ApplyPassageModifier(
ApplyPassageCarving(Density, ModSDF, 8.0f, 0.0f); Density, WorldX, WorldY, WorldZ, 8.0f, 0.0f);
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY, WorldRadiusVoxels, EdgeSealThickness, 8.0f); VF_ApplyXYEdgeSeal(Density, WorldX, WorldY, WorldRadiusVoxels, EdgeSealThickness, 8.0f);
Result = -Density; Result = -Density;
} }
@@ -996,6 +991,81 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
// Disturbance layer (the "wow" post-process) — cached params, MC convention. // Disturbance layer (the "wow" post-process) — cached params, MC convention.
ApplyDisturbances(Result, WorldX, WorldY, WorldZ, CP_Dist, (uint32)Seed); ApplyDisturbances(Result, WorldX, WorldY, WorldZ, CP_Dist, (uint32)Seed);
// A disturbance is allowed to add visual detail, but it must not refill the landing's
// measured air volume or turn its support slab back into a hole. Reassert the landing
// air first, then the structural floor, in MC space before the final XY seal. The two
// calls use the same landing geometry already evaluated by the legacy and op-stack post.
float LandingBaseDensity = CP_Dist.BaseDensity;
#if WITH_EDITOR
// A composer parent can deliberately use a different structural base than the authored
// definition. The landing floor was already applied by PassageCarveOp with that parent
// base; use the same value for this post-disturbance MC backstop or live-vs-direct stack
// evaluation would differ exactly on floor voxels.
if (CP_UseComposerRegions && CP_ComposerRegions.bHasGlobalStructuralParams)
{
LandingBaseDensity = CP_ComposerRegions.BaseDensity;
}
else
#endif
if (CP_UseCustomRecipe)
{
switch (CP_CustomRecipe.StructuralParamBlock)
{
case EVoxelStrateParamBlock::TunnelNetwork:
LandingBaseDensity = CP_CustomParams.TunnelNetworkParams.BaseDensity;
break;
case EVoxelStrateParamBlock::Slab:
LandingBaseDensity = CP_CustomParams.SlabParams.BaseDensity;
break;
case EVoxelStrateParamBlock::Maze:
LandingBaseDensity = CP_CustomParams.MazeParams.BaseDensity;
break;
case EVoxelStrateParamBlock::Surface:
LandingBaseDensity = CP_CustomParams.SurfaceParams.BaseDensity;
break;
case EVoxelStrateParamBlock::VerticalShaft:
LandingBaseDensity = CP_CustomParams.VerticalShaftParams.BaseDensity;
break;
case EVoxelStrateParamBlock::FloatingIsland:
LandingBaseDensity = CP_CustomParams.FloatingIslandParams.BaseDensity;
break;
default:
break;
}
}
else
{
switch (CP_GenType)
{
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber:
LandingBaseDensity = CP_Slab.BaseDensity;
break;
case ECaveGeneratorType::Maze:
LandingBaseDensity = CP_Maze.BaseDensity;
break;
case ECaveGeneratorType::SurfaceWorld:
LandingBaseDensity = CP_Surface.BaseDensity;
break;
case ECaveGeneratorType::VerticalShafts:
LandingBaseDensity = CP_Vert.BaseDensity;
break;
case ECaveGeneratorType::FloatingIslands:
LandingBaseDensity = CP_Float.BaseDensity;
break;
case ECaveGeneratorType::TunnelNetwork:
case ECaveGeneratorType::Underwater:
default:
LandingBaseDensity = CP_Tunnel.BaseDensity;
break;
}
}
StrateManager->ApplyPassageLandingAirMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity,
CP_Dist.BoundarySealThickness);
StrateManager->ApplyPassageLandingFloorMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity);
} }
else else
{ {
@@ -2151,8 +2221,9 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
//========================================================================= //=========================================================================
if (StrateManager) if (StrateManager)
{ {
const float ModSDF = StrateManager->EvaluateModifierSDF(WorldX, WorldY, WorldZ); StrateManager->ApplyPassageModifier(
ApplyPassageCarving(Density, ModSDF, Params.BaseDensity, Params.BoundarySealThickness); Density, WorldX, WorldY, WorldZ,
Params.BaseDensity, Params.BoundarySealThickness);
} }
// Fourth structural post: the XY edge wins over a passage near the rim. // Fourth structural post: the XY edge wins over a passage near the rim.
@@ -2373,8 +2444,9 @@ float UVoxelGenerator::GetSlabDensity(float WorldX, float WorldY, float WorldZ,
if (StrateManager) if (StrateManager)
{ {
const float ModSDF = StrateManager->EvaluateModifierSDF(WorldX, WorldY, WorldZ); StrateManager->ApplyPassageModifier(
ApplyPassageCarving(Density, ModSDF, Params.BaseDensity, Params.BoundarySealThickness); Density, WorldX, WorldY, WorldZ,
Params.BaseDensity, Params.BoundarySealThickness);
} }
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY, VF_ApplyXYEdgeSeal(Density, WorldX, WorldY,
@@ -2493,8 +2565,9 @@ float UVoxelGenerator::GetMazeDensity(float WorldX, float WorldY, float WorldZ,
if (StrateManager) if (StrateManager)
{ {
const float ModSDF = StrateManager->EvaluateModifierSDF(WorldX, WorldY, WorldZ); StrateManager->ApplyPassageModifier(
ApplyPassageCarving(Density, ModSDF, Params.BaseDensity, Params.BoundarySealThickness); Density, WorldX, WorldY, WorldZ,
Params.BaseDensity, Params.BoundarySealThickness);
} }
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY, VF_ApplyXYEdgeSeal(Density, WorldX, WorldY,
@@ -2756,8 +2829,9 @@ float UVoxelGenerator::SurfaceDensityFromColumn(float WorldX, float WorldY, floa
if (StrateManager) if (StrateManager)
{ {
const float ModSDF = StrateManager->EvaluateModifierSDF(WorldX, WorldY, WorldZ); StrateManager->ApplyPassageModifier(
ApplyPassageCarving(Density, ModSDF, S.BaseDensity, S.BoundarySealThickness); Density, WorldX, WorldY, WorldZ,
S.BaseDensity, S.BoundarySealThickness);
} }
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY, VF_ApplyXYEdgeSeal(Density, WorldX, WorldY,
@@ -3139,6 +3213,16 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
{ {
bCanSolid = false; bCanSolid = false;
} }
// A landing is bidirectional geometry: its chamber kills the AllSolid hypothesis, while its
// guaranteed support slab kills AllAir. Without this second guard a tile covering only the
// floor could be classified uniformly air and the mesher would skip the solid support that
// keeps the player from falling through it. The operator-stack path returns Both for the
// same box, so the hand-written legacy fold must carry the identical hypothesis kill here.
if (StrateManager->AnyPassageLandingFloorNearBox(
FVector(MinX, MinY, MinZ), FVector(MaxX, MaxY, MaxZ)))
{
bCanAir = false;
}
if (OriginSpineRadius > 0.0f) if (OriginSpineRadius > 0.0f)
{ {
// Spine (0,0) : colonne XY rayon R + blend 3 (cf. ApplyOriginSpine). Test cercle/boîte XY. // Spine (0,0) : colonne XY rayon R + blend 3 (cf. ApplyOriginSpine). Test cercle/boîte XY.
@@ -4468,8 +4552,9 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
if (StrateManager) if (StrateManager)
{ {
const float ModSDF = StrateManager->EvaluateModifierSDF(WorldX, WorldY, WorldZ); StrateManager->ApplyPassageModifier(
ApplyPassageCarving(Density, ModSDF, Params.BaseDensity, Params.BoundarySealThickness); Density, WorldX, WorldY, WorldZ,
Params.BaseDensity, Params.BoundarySealThickness);
} }
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY, VF_ApplyXYEdgeSeal(Density, WorldX, WorldY,
@@ -4628,8 +4713,9 @@ float UVoxelGenerator::GetFloatingIslandDensity(float WorldX, float WorldY, floa
if (StrateManager) if (StrateManager)
{ {
const float ModSDF = StrateManager->EvaluateModifierSDF(WorldX, WorldY, WorldZ); StrateManager->ApplyPassageModifier(
ApplyPassageCarving(Density, ModSDF, Params.BaseDensity, Params.BoundarySealThickness); Density, WorldX, WorldY, WorldZ,
Params.BaseDensity, Params.BoundarySealThickness);
} }
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY, VF_ApplyXYEdgeSeal(Density, WorldX, WorldY,
+446 -55
View File
@@ -7,6 +7,7 @@
#include "VoxelSeasonAsset.h" #include "VoxelSeasonAsset.h"
#include "VoxelTypes.h" // For CHUNK_SIZE, VOXEL_SIZE, WorldToChunkCoord #include "VoxelTypes.h" // For CHUNK_SIZE, VOXEL_SIZE, WorldToChunkCoord
#include "VoxelCaveMorphology.h" // For VoxelSDF and VoxelHash #include "VoxelCaveMorphology.h" // For VoxelSDF and VoxelHash
#include "VoxelDensityPrimitives.h" // Shared passage carve polarity/strength
#include "VoxelTerrainOpDefinition.h" // For UVoxelTerrainOpDefinition::ApplyTo #include "VoxelTerrainOpDefinition.h" // For UVoxelTerrainOpDefinition::ApplyTo
#include "VoxelBiomeDefinition.h" // For UVoxelBiomeDefinition (biome context flatten) #include "VoxelBiomeDefinition.h" // For UVoxelBiomeDefinition (biome context flatten)
#include "UObject/UObjectGlobals.h" #include "UObject/UObjectGlobals.h"
@@ -60,6 +61,65 @@ static float PassageFBM(float X, float Seed)
return (MaxV > 0.0f) ? (Total / MaxV) : 0.0f; return (MaxV > 0.0f) ? (Total / MaxV) : 0.0f;
} }
namespace
{
/**
* One shared passage cache for the tube SDF and landing-floor fill.
*
* The source query and all landing dimensions are resolved by GeneratePassages. This cache
* only narrows the immutable passage array once per (manager, version, chunk); neither the
* player-fit stencil nor a topology search can leak into the voxel loop.
*/
struct FPassageEvaluationCache
{
const UVoxelStrateManager* Owner = nullptr;
FIntVector Chunk = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX);
uint32 Version = 0xFFFFFFFFu;
TArray<int32> Nearby;
};
FPassageEvaluationCache& VF_GetPassageEvaluationCache()
{
thread_local FPassageEvaluationCache Cache;
return Cache;
}
const TArray<int32>& VF_GetNearbyPassages(
const UVoxelStrateManager* Manager,
const FIntVector& ChunkCoord)
{
FPassageEvaluationCache& Cache = VF_GetPassageEvaluationCache();
const uint32 Version = Manager ? Manager->GetLayoutVersion() : 0u;
if (Cache.Owner == Manager && Cache.Chunk == ChunkCoord && Cache.Version == Version)
{
return Cache.Nearby;
}
Cache.Owner = Manager;
Cache.Chunk = ChunkCoord;
Cache.Version = Version;
Cache.Nearby.Reset();
if (!Manager) return Cache.Nearby;
const FVector ChunkCenter(
(ChunkCoord.X + 0.5f) * (float)CHUNK_SIZE,
(ChunkCoord.Y + 0.5f) * (float)CHUNK_SIZE,
(ChunkCoord.Z + 0.5f) * (float)CHUNK_SIZE);
const float ChunkRadius = (float)CHUNK_SIZE * 0.8660254f + 3.0f;
const TArray<FVoxelPassage>& Passages = Manager->GetPassages();
for (int32 PassageIndex = 0; PassageIndex < Passages.Num(); ++PassageIndex)
{
const FVoxelPassage& Passage = Passages[PassageIndex];
const float Reach = Passage.BoundRadius + ChunkRadius;
if (FVector::DistSquared(ChunkCenter, Passage.BoundCenter) <= Reach * Reach)
{
Cache.Nearby.Add(PassageIndex);
}
}
return Cache.Nearby;
}
}
bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed) bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
{ {
if (!Settings) if (!Settings)
@@ -601,10 +661,14 @@ void UVoxelStrateManager::GeneratePassages()
constexpr uint32 PassageSaltNoiseZ = 0xA1100008u; constexpr uint32 PassageSaltNoiseZ = 0xA1100008u;
constexpr uint32 PassageSaltPhase = 0xA1100009u; constexpr uint32 PassageSaltPhase = 0xA1100009u;
constexpr uint32 PassageSaltBendFreq = 0xA110000Au; constexpr uint32 PassageSaltBendFreq = 0xA110000Au;
constexpr uint32 PassageSaltUpperDoor = 0xA110000Bu;
constexpr uint32 PassageSaltLowerDoor = 0xA110000Cu;
int32 TotalPassages = 0; int32 TotalPassages = 0;
int32 NumAimedAtUpperPlayerFit = 0; int32 NumAimedAtUpperPlayerFit = 0;
int32 NumAimedAtLowerPlayerFit = 0; int32 NumAimedAtLowerPlayerFit = 0;
int32 NumUpperRootConnectors = 0;
int32 NumLowerRootConnectors = 0;
TSet<FString> NoQueryArchetypes; TSet<FString> NoQueryArchetypes;
const auto ArchetypeName = [](ECaveGeneratorType Archetype) const auto ArchetypeName = [](ECaveGeneratorType Archetype)
@@ -678,6 +742,9 @@ void UVoxelStrateManager::GeneratePassages()
case ECaveGeneratorType::Maze: case ECaveGeneratorType::Maze:
return Definition.MazeParams.BoundarySealThickness; return Definition.MazeParams.BoundarySealThickness;
case ECaveGeneratorType::SurfaceWorld:
return Definition.SurfaceParams.BoundarySealThickness;
case ECaveGeneratorType::VerticalShafts: case ECaveGeneratorType::VerticalShafts:
return Definition.VerticalShaftParams.BoundarySealThickness; return Definition.VerticalShaftParams.BoundarySealThickness;
@@ -689,6 +756,30 @@ void UVoxelStrateManager::GeneratePassages()
} }
}; };
const auto CommonRootFloorFor = [](float StrateTopZ, float StrateBottomZ,
float BoundarySealThickness) -> float
{
// The two mouth queries may legitimately return floors near opposite strate faces. A
// shared root level is therefore derived from the strate bounds, never from passage
// iteration order. Leave the floor slab and the full 12-voxel connector clearance beyond
// the four-voxel passage blend away from both seals; the landing builder repeats this
// clamp as its final safety boundary.
const float Seal = FMath::Max(BoundarySealThickness, 0.0f);
const float InnerBottom = StrateBottomZ + Seal;
const float InnerTop = StrateTopZ - Seal;
const float ConnectorHeight = FMath::Max(
12.0f, FVoxelPlayerCapsuleConstants::HeightVoxels + 4.0f);
constexpr float PassageBlendSafety = 4.0f;
const float MinimumFloor = InnerBottom + 3.0f + PassageBlendSafety;
const float MaximumFloor = InnerTop - ConnectorHeight - PassageBlendSafety;
if (FMath::IsFinite(MinimumFloor) && FMath::IsFinite(MaximumFloor)
&& MaximumFloor >= MinimumFloor)
{
return 0.5f * (MinimumFloor + MaximumFloor);
}
return 0.5f * (InnerBottom + InnerTop);
};
//========================================================================= //=========================================================================
// INTER-STRATE PASSAGES: tunnels connecting consecutive strates. // INTER-STRATE PASSAGES: tunnels connecting consecutive strates.
// Each passage is randomly assigned one of 5 types, which determines // Each passage is randomly assigned one of 5 types, which determines
@@ -983,8 +1074,79 @@ void UVoxelStrateManager::GeneratePassages()
Passage.ControlRadii.Add(RadiusAt(T)); Passage.ControlRadii.Add(RadiusAt(T));
} }
Passage.UpperPoint = Passage.ControlPoints[0]; // The old control-point endpoints were also the player's standing points. That made
Passage.LowerPoint = Passage.ControlPoints.Last(); // a sloped/vertical tube open directly under the capsule. Keep the queried points as
// explicit standing anchors, and move only the tube's first/last point to a doorway
// tangent to the landing floor. The endpoint-to-anchor distinction is the geometry
// that turns a mouth into a place.
auto DoorDirectionFrom = [](const FVector& Delta, float Angle) -> FVector
{
FVector Direction(Delta.X, Delta.Y, 0.0f);
if (!Direction.Normalize())
{
Direction = FVector(FMath::Cos(Angle), FMath::Sin(Angle), 0.0f);
if (!Direction.Normalize())
{
Direction = FVector(1.0f, 0.0f, 0.0f);
}
}
return Direction;
};
const FVector UpperDoorDirection = DoorDirectionFrom(
Passage.ControlPoints.Num() > 1
? Passage.ControlPoints[1] - Passage.ControlPoints[0]
: FVector::ZeroVector,
PassageRandom01(PassageSaltUpperDoor) * 2.0f * PI);
const FVector LowerDoorDirection = DoorDirectionFrom(
Passage.ControlPoints.Num() > 1
? Passage.ControlPoints[Passage.ControlPoints.Num() - 2]
- Passage.ControlPoints.Last()
: FVector::ZeroVector,
PassageRandom01(PassageSaltLowerDoor) * 2.0f * PI);
// A source-fit answer proves only a local pose; none of the pure queries performs a
// flood fill over the live strate. Every mouth therefore receives the same explicit
// flat-floor connector to the deterministic root, including answered mouths. This
// is what makes the join guaranteed rather than merely probable.
Passage.UpperLanding = VF_BuildPassageLanding(
FVector(UpperX, UpperY, TopZ),
Cfg.MouthRadius,
UpperDoorDirection,
UpperTopZ,
UpperBottomZ,
UpperDef ? BoundarySealThicknessFor(*UpperDef) : 0.0f,
bAimedUpperAtPlayerFitPoint,
/*bHasNetworkConnector=*/true,
FVector(0.0f, 0.0f,
CommonRootFloorFor(
UpperTopZ, UpperBottomZ,
UpperDef ? BoundarySealThicknessFor(*UpperDef) : 0.0f)),
OriginSpineRadius);
Passage.LowerLanding = VF_BuildPassageLanding(
FVector(LowerX, LowerY, BottomZ),
Cfg.MouthRadius,
LowerDoorDirection,
LowerTopZ,
(float)(Lower.BottomChunkZ) * CHUNK_SIZE,
LowerDef ? BoundarySealThicknessFor(*LowerDef) : 0.0f,
bAimedLowerAtPlayerFitPoint,
/*bHasNetworkConnector=*/true,
FVector(0.0f, 0.0f,
CommonRootFloorFor(
LowerTopZ, (float)(Lower.BottomChunkZ) * CHUNK_SIZE,
LowerDef ? BoundarySealThicknessFor(*LowerDef) : 0.0f)),
OriginSpineRadius);
Passage.UpperPoint = Passage.UpperLanding.StandingPoint;
Passage.LowerPoint = Passage.LowerLanding.StandingPoint;
if (Passage.UpperLanding.bHasNetworkConnector) ++NumUpperRootConnectors;
if (Passage.LowerLanding.bHasNetworkConnector) ++NumLowerRootConnectors;
if (Passage.ControlPoints.Num() > 0)
{
Passage.ControlPoints[0] = Passage.UpperLanding.DoorPoint;
Passage.ControlPoints.Last() = Passage.LowerLanding.DoorPoint;
}
Passage.Radius = 0.0f; Passage.Radius = 0.0f;
for (const float ControlRadius : Passage.ControlRadii) for (const float ControlRadius : Passage.ControlRadii)
{ {
@@ -992,20 +1154,76 @@ void UVoxelStrateManager::GeneratePassages()
} }
// Fallback / bounds if an invalid authored width produced no positive profile. // Fallback / bounds if an invalid authored width produced no positive profile.
Passage.Radius = FMath::Max(Passage.Radius, FMath::Max(Cfg.MouthRadius, Cfg.MidRadius)); Passage.Radius = FMath::Max(Passage.Radius, FMath::Max(Cfg.MouthRadius, Cfg.MidRadius));
Passage.Radius = FMath::Max(Passage.Radius,
FMath::Max(Passage.UpperLanding.ConnectorRadius,
Passage.LowerLanding.ConnectorRadius));
// Bounding sphere over all control points (+ widest radius + blend) for culling. This // Bounding sphere over the tube, both rooms, both floor slabs, and both optional root
// remains conservative when either mouth snaps laterally: every slanted segment is // connectors (+ widest radius + blend) for culling. Under-sizing this sphere would
// between two control points, and a segment lies inside the sphere containing both // cull a real landing and leave a sealed pocket, so the room's full box diagonal is
// endpoints. Under-sizing this sphere would make EvaluateModifierSDF cull a real // included rather than treating the standing anchor as a point.
// passage and leave a sealed pocket.
{ {
FVector Center = FVector::ZeroVector; FVector BoundsMin(FLT_MAX, FLT_MAX, FLT_MAX);
for (const FVector& CP : Passage.ControlPoints) Center += CP; FVector BoundsMax(-FLT_MAX, -FLT_MAX, -FLT_MAX);
Center /= (float)Passage.ControlPoints.Num(); auto IncludePoint = [&BoundsMin, &BoundsMax](const FVector& Point, float Pad)
float MaxDistSq = 0.0f; {
BoundsMin.X = FMath::Min(BoundsMin.X, Point.X - Pad);
BoundsMin.Y = FMath::Min(BoundsMin.Y, Point.Y - Pad);
BoundsMin.Z = FMath::Min(BoundsMin.Z, Point.Z - Pad);
BoundsMax.X = FMath::Max(BoundsMax.X, Point.X + Pad);
BoundsMax.Y = FMath::Max(BoundsMax.Y, Point.Y + Pad);
BoundsMax.Z = FMath::Max(BoundsMax.Z, Point.Z + Pad);
};
for (const FVector& CP : Passage.ControlPoints) for (const FVector& CP : Passage.ControlPoints)
MaxDistSq = FMath::Max(MaxDistSq, (float)FVector::DistSquared(Center, CP)); {
const float R = FMath::Sqrt(MaxDistSq) + Passage.Radius + 4.0f; IncludePoint(CP, Passage.Radius + 4.0f);
}
const auto IncludeLanding = [&IncludePoint](const FVoxelPassageLanding& Landing)
{
const float Height = FMath::Max(Landing.CeilingZ - Landing.FloorZ, 0.0f);
const FVector RoomCenter(
Landing.StandingPoint.X,
Landing.StandingPoint.Y,
(Landing.FloorZ + Landing.CeilingZ) * 0.5f);
const float RoomRadius = FMath::Sqrt(
2.0f * FMath::Square(Landing.HalfWidth)
+ 0.25f * FMath::Square(Height))
+ Landing.FloorThickness + 4.0f;
IncludePoint(RoomCenter, RoomRadius);
if (Landing.bHasNetworkConnector)
{
const float HubRadius = Landing.RootSpineRadius
+ Landing.ConnectorRadius;
const float HubHalfHeight = 0.5f * FMath::Max(
Landing.RootCeilingZ - Landing.RootFloorZ, 0.0f);
IncludePoint(FVector(0.0f, 0.0f,
(Landing.RootFloorZ + Landing.RootCeilingZ) * 0.5f),
FMath::Sqrt(FMath::Square(HubRadius)
+ FMath::Square(HubHalfHeight))
+ Landing.FloorThickness + 4.0f);
const float ConnectorHalfHeight = FMath::Max(
0.5f * FMath::Max(
Landing.ConnectorCeilingZ - Landing.FloorZ, 0.0f),
Landing.ConnectorRadius);
IncludePoint(Landing.ConnectorStart,
ConnectorHalfHeight + 4.0f);
if (Landing.bHasConnectorBend)
{
IncludePoint(Landing.ConnectorControl,
ConnectorHalfHeight + 4.0f);
}
IncludePoint(Landing.ConnectorEnd,
ConnectorHalfHeight + 4.0f);
}
};
IncludeLanding(Passage.UpperLanding);
IncludeLanding(Passage.LowerLanding);
const FVector Center = (BoundsMin + BoundsMax) * 0.5f;
float MaxDistSq = 0.0f;
MaxDistSq = FMath::Max(MaxDistSq, (float)FVector::DistSquared(Center, BoundsMin));
MaxDistSq = FMath::Max(MaxDistSq, (float)FVector::DistSquared(Center, BoundsMax));
const float R = FMath::Sqrt(MaxDistSq);
Passage.BoundCenter = Center; Passage.BoundCenter = Center;
Passage.BoundRadius = R; Passage.BoundRadius = R;
Passage.BoundRadiusSq = R * R; Passage.BoundRadiusSq = R * R;
@@ -1028,11 +1246,13 @@ void UVoxelStrateManager::GeneratePassages()
} }
UE_LOG(LogTemp, Log, UE_LOG(LogTemp, Log,
TEXT("[StrateManager] Passage landing sites: upper %d/%d and lower %d/%d aimed at player-fit source space, %d/%d mouth queries fell back to random reach (archetypes with no query: %s)."), TEXT("[StrateManager] Passage landings: upper %d/%d and lower %d/%d source-fit; root floor connectors %d/%d; %d/%d mouth queries fell back to random reach (archetypes with no query: %s)."),
NumAimedAtUpperPlayerFit, NumAimedAtUpperPlayerFit,
TotalPassages, TotalPassages,
NumAimedAtLowerPlayerFit, NumAimedAtLowerPlayerFit,
TotalPassages, TotalPassages,
NumUpperRootConnectors,
NumLowerRootConnectors,
(TotalPassages * 2) - NumAimedAtUpperPlayerFit - NumAimedAtLowerPlayerFit, (TotalPassages * 2) - NumAimedAtUpperPlayerFit - NumAimedAtLowerPlayerFit,
TotalPassages * 2, TotalPassages * 2,
*NoQueryList); *NoQueryList);
@@ -1080,52 +1300,16 @@ void UVoxelStrateManager::GeneratePassages()
float UVoxelStrateManager::EvaluateModifierSDF(float WorldX, float WorldY, float WorldZ) const float UVoxelStrateManager::EvaluateModifierSDF(float WorldX, float WorldY, float WorldZ) const
{ {
//=========================================================================
// PER-CHUNK PASSAGE SHORTLIST
//=========================================================================
// This runs PER VOXEL (35³ per tile). The vast majority of chunks are nowhere near a
// descent passage, yet every voxel still walked the WHOLE Passages array just to reject
// each one on a squared-distance test (Passages.Num() × 35³ rejects per tile, all wasted).
// Cache, per chunk, the shortlist of passages whose bounds actually reach this chunk —
// usually EMPTY → instant FLT_MAX return (no carve). Indices (not pointers) + a version
// stamp keep it safe across a GeneratePassages rebuild. Output is bit-identical: the
// shortlist is a conservative superset (chunk bounding sphere vs each passage bound).
thread_local FIntVector SL_Chunk(INT32_MAX, INT32_MAX, INT32_MAX);
thread_local uint32 SL_Version = 0xFFFFFFFFu;
thread_local TArray<int32> SL_Nearby;
const FIntVector ChunkCoord( const FIntVector ChunkCoord(
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE), FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE), FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE)); FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE));
if (ChunkCoord != SL_Chunk || SL_Version != PassagesVersion) // The shortlist is rebuilt once for a (manager, version, chunk) and is shared by the tube,
{ // landing, and floor paths. No source-fit stencil or topology search is allowed below it.
SL_Chunk = ChunkCoord; const TArray<int32>& Nearby = VF_GetNearbyPassages(this, ChunkCoord);
SL_Version = PassagesVersion;
SL_Nearby.Reset();
// Chunk bounding sphere (centre + half-diagonal), padded by the blend radius. A passage if (Nearby.Num() == 0) return FLT_MAX; // no passage near this chunk → no carve
// is kept iff its bounding sphere overlaps the chunk's — i.e. some voxel here could be
// inside its per-voxel reject radius. √3/2 · CHUNK_SIZE ≈ 0.866 · size.
const FVector CCenter(
(ChunkCoord.X + 0.5f) * (float)CHUNK_SIZE,
(ChunkCoord.Y + 0.5f) * (float)CHUNK_SIZE,
(ChunkCoord.Z + 0.5f) * (float)CHUNK_SIZE);
const float ChunkR = (float)CHUNK_SIZE * 0.8660254f + 3.0f; // +BlendK
for (int32 i = 0; i < Passages.Num(); ++i)
{
const FVoxelPassage& P = Passages[i];
const float Reach = P.BoundRadius + ChunkR;
if (FVector::DistSquared(CCenter, P.BoundCenter) <= Reach * Reach)
{
SL_Nearby.Add(i);
}
}
}
if (SL_Nearby.Num() == 0) return FLT_MAX; // no passage near this chunk → no carve
float MinSDF = FLT_MAX; float MinSDF = FLT_MAX;
const float BlendK = 3.0f; // Smooth blend for passage junctions const float BlendK = 3.0f; // Smooth blend for passage junctions
@@ -1136,7 +1320,7 @@ float UVoxelStrateManager::EvaluateModifierSDF(float WorldX, float WorldY, float
// entry is a simple straight tube. A bounding-sphere reject skips far passages. // entry is a simple straight tube. A bounding-sphere reject skips far passages.
//========================================================================= //=========================================================================
const FVector Pos(WorldX, WorldY, WorldZ); const FVector Pos(WorldX, WorldY, WorldZ);
for (int32 PIdx : SL_Nearby) for (int32 PIdx : Nearby)
{ {
const FVoxelPassage& P = Passages[PIdx]; const FVoxelPassage& P = Passages[PIdx];
// BOUNDING-SPHERE REJECT: skip passages this voxel can't possibly be inside. // BOUNDING-SPHERE REJECT: skip passages this voxel can't possibly be inside.
@@ -1168,11 +1352,125 @@ float UVoxelStrateManager::EvaluateModifierSDF(float WorldX, float WorldY, float
const float PassageSDF = VoxelSDF::Capsule(Pos, P.UpperPoint, P.LowerPoint, P.Radius); const float PassageSDF = VoxelSDF::Capsule(Pos, P.UpperPoint, P.LowerPoint, P.Radius);
MinSDF = VoxelSDF::SmoothMin(MinSDF, PassageSDF, BlendK); MinSDF = VoxelSDF::SmoothMin(MinSDF, PassageSDF, BlendK);
} }
// A landing is a real room with a hard flat-floor half-space, not a sphere around the
// tube endpoint. The optional connector is the same fixed SDF geometry selected during
// GeneratePassages; it never performs a source query here.
const float UpperLandingSDF = VF_EvaluatePassageLandingSDF(Pos, P.UpperLanding);
const float LowerLandingSDF = VF_EvaluatePassageLandingSDF(Pos, P.LowerLanding);
MinSDF = VoxelSDF::SmoothMin(MinSDF, UpperLandingSDF, BlendK);
MinSDF = VoxelSDF::SmoothMin(MinSDF, LowerLandingSDF, BlendK);
} }
return MinSDF; return MinSDF;
} }
void UVoxelStrateManager::ApplyPassageModifier(
float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const
{
const float ModSDF = EvaluateModifierSDF(WorldX, WorldY, WorldZ);
VF_ApplyPassageCarving(Density, ModSDF, BaseDensity, SealThickness);
ApplyPassageLandingAir(Density, WorldX, WorldY, WorldZ, BaseDensity, SealThickness);
const FIntVector ChunkCoord(
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE));
const TArray<int32>& Nearby = VF_GetNearbyPassages(this, ChunkCoord);
const FVector Position(WorldX, WorldY, WorldZ);
for (const int32 PassageIndex : Nearby)
{
const FVoxelPassage& Passage = Passages[PassageIndex];
if (VF_IsPassageLandingFloor(Position, Passage.UpperLanding)
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding))
{
// This is the one bidirectional part of PassageCarveOp: a floor is a proved solid
// support slab. It is deliberately applied after the air carve so a tube can never
// tunnel through the floor and leave the player over a void.
Density = FMath::Max(Density, BaseDensity);
break;
}
}
}
void UVoxelStrateManager::ApplyPassageLandingAir(
float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const
{
const FIntVector ChunkCoord(
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE));
const TArray<int32>& Nearby = VF_GetNearbyPassages(this, ChunkCoord);
if (Nearby.Num() == 0) return;
const FVector Position(WorldX, WorldY, WorldZ);
float MinLandingSDF = FLT_MAX;
for (const int32 PassageIndex : Nearby)
{
const FVoxelPassage& Passage = Passages[PassageIndex];
if (FVector::DistSquared(Position, Passage.BoundCenter) > Passage.BoundRadiusSq)
{
continue;
}
const FVoxelPassageLanding* Landings[] = {
&Passage.UpperLanding, &Passage.LowerLanding };
for (const FVoxelPassageLanding* Landing : Landings)
{
const float LandingSDF = VF_EvaluatePassageLandingSDF(Position, *Landing);
if (LandingSDF < MinLandingSDF)
{
MinLandingSDF = LandingSDF;
}
}
}
if (MinLandingSDF == FLT_MAX) return;
// Keep the same smooth interior blend used by the passage op. This landing-specific writer is
// idempotent because the MC-facing post may run after the structural op already saw the same
// voxel. The landing's support floor is restored by the floor writer after this MC-facing pass.
VF_ApplyPassageLandingCarving(Density, MinLandingSDF, BaseDensity, SealThickness);
}
void UVoxelStrateManager::ApplyPassageLandingAirMC(
float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const
{
// ApplyDisturbances is deliberately an MC-space post-process and can add a bridge or ridge
// on top of the structural passage. Reassert only landing air here; the support slab is
// restored by ApplyPassageLandingFloorMC immediately afterwards. This stays on the same
// thread-local passage shortlist as the hot voxel path and performs no source/topology work.
float InternalDensity = -Density;
ApplyPassageLandingAir(
InternalDensity, WorldX, WorldY, WorldZ, BaseDensity, SealThickness);
Density = -InternalDensity;
}
void UVoxelStrateManager::ApplyPassageLandingFloorMC(
float& Density, float WorldX, float WorldY, float WorldZ, float BaseDensity) const
{
const FIntVector ChunkCoord(
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE));
const TArray<int32>& Nearby = VF_GetNearbyPassages(this, ChunkCoord);
const FVector Position(WorldX, WorldY, WorldZ);
for (const int32 PassageIndex : Nearby)
{
const FVoxelPassage& Passage = Passages[PassageIndex];
if (VF_IsPassageLandingFloor(Position, Passage.UpperLanding)
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding))
{
// Result is in MC convention here (negative = solid). This reassertion is the
// structural floor backstop after the optional MC-space disturbance layer.
Density = FMath::Min(Density, -BaseDensity);
break;
}
}
}
bool UVoxelStrateManager::AnyPassageNearBox(const FVector& MinVoxel, const FVector& MaxVoxel) const bool UVoxelStrateManager::AnyPassageNearBox(const FVector& MinVoxel, const FVector& MaxVoxel) const
{ {
// Le carve d'un passage atteint ModSDF < PASSAGE_BLEND_RADIUS (4, VoxelGenerator.cpp) au-delà de // Le carve d'un passage atteint ModSDF < PASSAGE_BLEND_RADIUS (4, VoxelGenerator.cpp) au-delà de
@@ -1194,6 +1492,99 @@ bool UVoxelStrateManager::AnyPassageNearBox(const FVector& MinVoxel, const FVect
return false; return false;
} }
bool UVoxelStrateManager::AnyPassageLandingFloorNearBox(
const FVector& MinVoxel, const FVector& MaxVoxel) const
{
// A conservative AABB test is enough for the floor's only solid write. False positives cost
// a tile; a false negative could classify an air tile uniformly and remove the player's floor.
for (const FVoxelPassage& Passage : Passages)
{
const FVoxelPassageLanding* Landings[] = {
&Passage.UpperLanding, &Passage.LowerLanding };
for (const FVoxelPassageLanding* Landing : Landings)
{
if (Landing->HalfWidth <= 0.0f || Landing->FloorThickness <= 0.0f)
{
continue;
}
const float Pad = 1.0f;
const float FloorMinX = Landing->StandingPoint.X - Landing->HalfWidth - Pad;
const float FloorMaxX = Landing->StandingPoint.X + Landing->HalfWidth + Pad;
const float FloorMinY = Landing->StandingPoint.Y - Landing->HalfWidth - Pad;
const float FloorMaxY = Landing->StandingPoint.Y + Landing->HalfWidth + Pad;
const float FloorMinZ = Landing->FloorZ - Landing->FloorThickness - Pad;
const float FloorMaxZ = Landing->FloorZ + Pad;
if (FloorMaxX >= MinVoxel.X && FloorMinX <= MaxVoxel.X
&& FloorMaxY >= MinVoxel.Y && FloorMinY <= MaxVoxel.Y
&& FloorMaxZ >= MinVoxel.Z && FloorMinZ <= MaxVoxel.Z)
{
return true;
}
// The fallback connector carries the same support slab away from the room. Its
// segment is not covered by the room AABB above; omitting it would let a tile that
// contains only the connector floor prove AllAir and erase the guaranteed join.
if (Landing->bHasNetworkConnector && Landing->ConnectorRadius > 0.0f
&& FMath::IsFinite(Landing->ConnectorStart.X)
&& FMath::IsFinite(Landing->ConnectorStart.Y)
&& FMath::IsFinite(Landing->ConnectorStart.Z)
&& FMath::IsFinite(Landing->ConnectorEnd.X)
&& FMath::IsFinite(Landing->ConnectorEnd.Y)
&& FMath::IsFinite(Landing->ConnectorEnd.Z))
{
const float ConnectorPad = Landing->ConnectorRadius + Pad;
float ConnectorMinX = FMath::Min(
Landing->ConnectorStart.X, Landing->ConnectorEnd.X) - ConnectorPad;
float ConnectorMaxX = FMath::Max(
Landing->ConnectorStart.X, Landing->ConnectorEnd.X) + ConnectorPad;
float ConnectorMinY = FMath::Min(
Landing->ConnectorStart.Y, Landing->ConnectorEnd.Y) - ConnectorPad;
float ConnectorMaxY = FMath::Max(
Landing->ConnectorStart.Y, Landing->ConnectorEnd.Y) + ConnectorPad;
if (Landing->bHasConnectorBend)
{
ConnectorMinX = FMath::Min(
ConnectorMinX, Landing->ConnectorControl.X - ConnectorPad);
ConnectorMaxX = FMath::Max(
ConnectorMaxX, Landing->ConnectorControl.X + ConnectorPad);
ConnectorMinY = FMath::Min(
ConnectorMinY, Landing->ConnectorControl.Y - ConnectorPad);
ConnectorMaxY = FMath::Max(
ConnectorMaxY, Landing->ConnectorControl.Y + ConnectorPad);
}
const float ConnectorMinZ = FMath::Min(
Landing->FloorZ, Landing->RootFloorZ)
- Landing->FloorThickness - Pad;
const float ConnectorMaxZ = FMath::Max(
Landing->FloorZ, Landing->RootFloorZ) + Pad;
if (ConnectorMaxX >= MinVoxel.X && ConnectorMinX <= MaxVoxel.X
&& ConnectorMaxY >= MinVoxel.Y && ConnectorMinY <= MaxVoxel.Y
&& ConnectorMaxZ >= MinVoxel.Z && ConnectorMinZ <= MaxVoxel.Z)
{
return true;
}
if (Landing->RootSpineRadius >= 0.0f)
{
const float HubPad = Landing->RootSpineRadius
+ Landing->ConnectorRadius + Pad;
const float HubMinZ = Landing->RootFloorZ
- Landing->FloorThickness - Pad;
const float HubMaxZ = Landing->RootFloorZ + Pad;
if (HubPad >= 0.0f
&& HubPad >= MinVoxel.X && -HubPad <= MaxVoxel.X
&& HubPad >= MinVoxel.Y && -HubPad <= MaxVoxel.Y
&& HubMaxZ >= MinVoxel.Z && HubMinZ <= MaxVoxel.Z)
{
return true;
}
}
}
}
}
return false;
}
//============================================================================= //=============================================================================
// QUERIES // QUERIES
//============================================================================= //=============================================================================
@@ -41,6 +41,48 @@ struct FStrateTerrainOpEntry;
class UVoxelTerrainOpDefinition; class UVoxelTerrainOpDefinition;
enum class ECaveGeneratorType : uint8; enum class ECaveGeneratorType : uint8;
/**
* The geometry contract for one end of an inter-strate passage.
*
* All values are actor-space voxels. `StandingPoint` is the player-fit anchor: its floor is
* `FloorZ = StandingPoint.Z - 0.5`. The chamber is deliberately separate from the tube endpoint
* so a sloped/vertical tube never presents the player with a hole as its first standing position.
*
* A source query answer selects the landing's XY/Z anchor. Because that pure query does not
* flood-fill the live network, every inter-strate end also gets a deterministic root connector
* (`bHasNetworkConnector`). Its flat floor ramps to the same common interior root level for that
* strate, so both mouths enter one walkable hub even when their authored floors are at opposite
* sides of the strate. The hub is an annular walkable network around (0,0), not the pre-existing
* vertical air shaft. `RootSpineRadius` keeps both the hub air and support slab outside that shaft,
* so the spine remains open. If the direct run would be steeper than 44 degrees, the connector
* uses a deterministic level dog-leg before its final ramp; the bend is geometry, not a per-voxel
* search.
*/
struct VOXELFORGE_API FVoxelPassageLanding
{
FVector StandingPoint = FVector::ZeroVector;
FVector DoorPoint = FVector::ZeroVector;
FVector DoorDirection = FVector(1.0f, 0.0f, 0.0f);
FVector ConnectorStart = FVector::ZeroVector;
FVector ConnectorControl = FVector::ZeroVector;
FVector ConnectorEnd = FVector::ZeroVector;
float FloorZ = 0.0f;
float CeilingZ = 0.0f;
float HalfWidth = 0.0f;
float FloorThickness = 3.0f;
float ConnectorRadius = 0.0f;
float ConnectorCeilingZ = 0.0f;
float RootFloorZ = 0.0f;
float RootCeilingZ = 0.0f;
float RootSpineRadius = 0.0f;
bool bSourcePlayerFit = false;
bool bHasNetworkConnector = false;
bool bHasConnectorBend = false;
};
//============================================================================= //=============================================================================
// SDF PRIMITIVES // SDF PRIMITIVES
//============================================================================= //=============================================================================
@@ -153,6 +195,29 @@ namespace VoxelSDF
} }
} }
/** Build the deterministic, body-sized room geometry at one standing anchor. */
VOXELFORGE_API FVoxelPassageLanding VF_BuildPassageLanding(
const FVector& StandingPoint,
float MouthRadius,
const FVector& DoorDirection,
float StrateTopZ,
float StrateBottomZ,
float BoundarySealThickness,
bool bSourcePlayerFit,
bool bHasNetworkConnector,
const FVector& NetworkPoint,
float RootSpineRadius = 0.0f);
/** Signed distance of the room + optional root connector. Negative means passage air. */
VOXELFORGE_API float VF_EvaluatePassageLandingSDF(
const FVector& Position,
const FVoxelPassageLanding& Landing);
/** True when a point belongs to the guaranteed solid floor slab of the landing/connector. */
VOXELFORGE_API bool VF_IsPassageLandingFloor(
const FVector& Position,
const FVoxelPassageLanding& Landing);
//============================================================================= //=============================================================================
// HASH FUNCTIONS // HASH FUNCTIONS
//============================================================================= //=============================================================================
@@ -176,7 +176,8 @@ public:
VF_FoldOp(H, *Entry.Op, VoxelBox, Ctx, VF_FoldOp(H, *Entry.Op, VoxelBox, Ctx,
(Entry.Writes & VoxelOpChannels::Sdf) != 0); (Entry.Writes & VoxelOpChannels::Sdf) != 0);
// Do not early-out on a dead hypothesis: a later forcing structural post may // Do not early-out on a dead hypothesis: a later forcing structural post may
// deliberately overwrite it (the XY edge seal is appended after passage carving). // deliberately overwrite it (the XY edge seal is appended after passage tube/landing
// carving and support).
} }
return H.Resolve(); return H.Resolve();
} }
@@ -87,6 +87,28 @@ FORCEINLINE void VF_ApplyPassageCarving(float& Density, float ModSDF,
Density = FMath::Min(Density, FMath::Lerp(Density, AirTarget, CarveFactor)); Density = FMath::Min(Density, FMath::Lerp(Density, AirTarget, CarveFactor));
} }
// Landing air is also reasserted after the MC-space disturbance post. Unlike the legacy tube
// carve above, this threshold is independent of the incoming density: applying it a second time
// is therefore bit-stable. The structural passage op and the post-disturbance backstop can share
// the same operation without turning an otherwise unchanged live-vs-direct op-stack comparison
// into a second, deeper smooth carve. The tube intentionally retains its old current-density
// interpolation because that is part of the legacy structural path's contract.
FORCEINLINE void VF_ApplyPassageLandingCarving(float& Density, float LandingSDF,
float BaseDensity, float SealThickness)
{
constexpr float PASSAGE_BLEND_RADIUS = 4.0f;
if (LandingSDF >= PASSAGE_BLEND_RADIUS) return;
float CarveFactor = FMath::Clamp(
(PASSAGE_BLEND_RADIUS - LandingSDF) / (PASSAGE_BLEND_RADIUS * 2.0f),
0.0f, 1.0f);
CarveFactor = SmoothStep01(CarveFactor);
const float AirTarget = -(BaseDensity * 2.0f + SealThickness + 4.0f);
const float LandingThreshold = FMath::Lerp(BaseDensity, AirTarget, CarveFactor);
Density = FMath::Min(Density, LandingThreshold);
}
//============================================================================= //=============================================================================
// SPINE DE DESCENTE (0,0) / (0,0) DESCENT SPINE // SPINE DE DESCENTE (0,0) / (0,0) DESCENT SPINE
//============================================================================= //=============================================================================
@@ -21,6 +21,7 @@
#include "VoxelStrateTypes.h" #include "VoxelStrateTypes.h"
#include "VoxelStrateDefinition.h" #include "VoxelStrateDefinition.h"
#include "VoxelSeasonManifest.h" #include "VoxelSeasonManifest.h"
#include "VoxelCaveMorphology.h"
#include "VoxelStrateManager.generated.h" #include "VoxelStrateManager.generated.h"
class UVoxelSettings; class UVoxelSettings;
@@ -84,6 +85,11 @@ struct FVoxelPassage
// ControlPoints, the uniform Radius is used as a fallback. // ControlPoints, the uniform Radius is used as a fallback.
TArray<float> ControlRadii; TArray<float> ControlRadii;
// Body-sized, flat-floored chambers at both tube ends. UpperPoint/LowerPoint remain the
// standing anchors returned by the source query; the tube enters each room at DoorPoint.
FVoxelPassageLanding UpperLanding;
FVoxelPassageLanding LowerLanding;
// Bounding sphere enclosing the whole passage (+ radius + blend), in voxel coords. // Bounding sphere enclosing the whole passage (+ radius + blend), in voxel coords.
// Computed once in GeneratePassages; lets EvaluateModifierSDF reject far voxels with // Computed once in GeneratePassages; lets EvaluateModifierSDF reject far voxels with
// a single squared-distance test instead of walking every segment per voxel. // a single squared-distance test instead of walking every segment per voxel.
@@ -348,6 +354,22 @@ public:
*/ */
float EvaluateModifierSDF(float WorldX, float WorldY, float WorldZ) const; float EvaluateModifierSDF(float WorldX, float WorldY, float WorldZ) const;
/** Apply the passage tube/landing carve and the guaranteed landing floor in internal density. */
void ApplyPassageModifier(float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const;
/** Reapply the landing-only air carve in internal density (used after visual disturbances). */
void ApplyPassageLandingAir(float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const;
/** Re-assert landing air after MC-space disturbances, still before the final XY seal. */
void ApplyPassageLandingAirMC(float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const;
/** Re-assert a landing floor after the landing air pass, still before the final XY seal. */
void ApplyPassageLandingFloorMC(float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity) const;
/** /**
* True si la sphère englobante d'un passage (élargie du rayon de blend de carve) touche la * True si la sphère englobante d'un passage (élargie du rayon de blend de carve) touche la
* boîte VOXEL [MinVoxel, MaxVoxel]. Test conservatif O(Passages) — utilisé par ClassifyTile * boîte VOXEL [MinVoxel, MaxVoxel]. Test conservatif O(Passages) — utilisé par ClassifyTile
@@ -355,6 +377,9 @@ public:
*/ */
bool AnyPassageNearBox(const FVector& MinVoxel, const FVector& MaxVoxel) const; bool AnyPassageNearBox(const FVector& MinVoxel, const FVector& MaxVoxel) const;
/** Conservative box guard for the floor fill; unlike a tube it can affect the all-air proof. */
bool AnyPassageLandingFloorNearBox(const FVector& MinVoxel, const FVector& MaxVoxel) const;
/** Get all generated passages (for debug display). */ /** Get all generated passages (for debug display). */
const TArray<FVoxelPassage>& GetPassages() const { return Passages; } const TArray<FVoxelPassage>& GetPassages() const { return Passages; }