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:
1 parent
e4e159f0cd
commit
413c15de24
14 files changed
+1943
-187
No files matched your search
+78
-31
@@ -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
|
||||||
|
|||||||
+39
-18
@@ -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. |
|
||||||
@@ -422,15 +423,25 @@ Header is rich with inline docs. Two namespaces + a per-chunk cache system.
|
|||||||
landing-site query: it evaluates the local archetype source through the same capsule/support
|
landing-site query: it evaluates the local archetype source through the same capsule/support
|
||||||
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
@@ -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));
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user