feat(descent): landing ROOMS and a walkable tunnel -- per Jahni's correction

Jahni corrected the design: "the connection between level is a tiny tunnel
connecting both strate, not vertical, it needs to be walkable" and "'the
spine' shouldn't be carved except optionally from the top first strate,
then it's used to have a landing room".

COMPOSER-NOTES §6.8 had said the spine "provides a clean landing space, not
a continuous open shaft" -- and the implementation carved a vertical
cylinder anyway. A vertical cylinder has no walkable floor except at its
bottom, so a carve guaranteeing open AIR guaranteed nothing STANDABLE.
That one omission is behind five of my dead hypotheses this week.

Now:
  origin spine   a finite rounded ROOM per strate, no vertical bore.
                 radius 14 -> 8 m wide, 7.5 m tall
  descent        a walkable tunnel, single-file by design: 3 m mouth,
                 2.5 m waist, gradient capped at a NAMED 15 deg
  exotic styles  VerticalShaft / SpiralDescent / CascadingDrops /
                 CrackCrevice preserved, no longer the default

⚠️ THE GEOMETRY BITES, and it is worth Jahni's eye: at 15 deg a 32 m drop
needs 119.4 m of horizontal run -- about 59.7 m per switchback leg. That is
a long walk between strates. It is the honest consequence of "walkable",
not a tuning artifact, and the alternative is a steeper number.

⭐ AND THE ROOT CAUSE GOT NAMED A THIRD TIME, by Codex mid-run: "the round
tube was hollow air but had no authored support strip, so 'open' still did
not mean 'standable'." Tunnels now carry a floor band, and the root join
terminates in the finite origin room rather than an annular shaft hub.

Measured, commandlet sweep, 8 archetypes x 8 seeds, 64/64 valid:
  origin column has player-fit cells   17/64 -> 63/64
  origin column reachable               0/64 ->  7/64
  arrival landing probes            315/320 fit AND in the arrival component
  tunnel audit    5,916/5,916 floor and capsule samples pass; zero gradient,
                  floor, capsule, geometry or seal failures
  narrow-gap events 0        box verdicts 14 checked, 0 violations

⚠️ arrival->departure is 6/64. The landings and tunnels are now provably
walkable; what remains is the INTRA-STRATE vertical connectivity problem
found in 6b91669 -- floors at different levels inside a strate with nothing
joining them. Separate and larger. Not tuned toward.

⚠️ PROCESS: this took 5.7 hours, of which Codex used 4 SECONDS of CPU. The
rest was polling the 73-minute automation suite at ~10% machine load. The
commandlet exists to avoid exactly that. New rule recorded: iterate with the
commandlet, full suite ONCE at the end.

Editor + Shipping builds clean. Automation 32/32, 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-09-08 02:14:37 +02:00
1 parent 6b916699af
commit 55bf807f60
19 files changed
+1996 -334

No files matched your search

+29 -20
View File
@@ -135,10 +135,12 @@ its row. The design lives in `OPSTACK-PLAN.md` / `OPSTACK-DECOMPOSITION.md`, the
what the world IS, and both paths compute it.
### 8.2 (0,0) spine & hybrid connections
- `ApplyOriginSpine` (VoxelGenerator.cpp, static helper) carves a guaranteed open vertical
column at XY (0,0) in every strate's **interior** (seals untouched). Radius =
`UVoxelGenerator::OriginSpineRadius` ← `VoxelSettings::OriginSpineRadius`. Called before
every `ApplyBoundarySeal`.
- `ApplyOriginSpine` (VoxelGenerator.cpp, static helper) builds a finite, rounded landing room with
a flat support floor at XY (0,0) in every strate's **interior**. It no longer carves a continuous
vertical column: the old column supplied open air but no standable surface except at its bottom.
Room half-width and height reuse the §6.5 capsule formulas; the room and slab are clamped away
from both vertical seals. Radius = `UVoxelGenerator::OriginSpineRadius` ←
`VoxelSettings::OriginSpineRadius`. Called before every `ApplyBoundarySeal`.
- `VerticalShafts` treats that structural column as a **connector endpoint**, not as a second
density primitive. A thread-local rebuild collects a direct-indexed geometric halo once (with
the stock settings: 9×9 tree-emission cells and a 15×15 roll), emits tree links only for the
@@ -156,11 +158,16 @@ what the world IS, and both paths compute it.
ledge-free Z interval. `BuildVerticalShaftStack` mirrors the same tree, random links, windows,
hashes, radius, and connector-Z rule; `VF_ApplyOriginSpine` remains the sole owner of the
vertical column itself.
- Descent is **player-dug** through the thin seals at (0,0). The single auto-opened
connection is the **surface entry shaft** at (0,0) through the top of strate 0
(`GeneratePassages`, `bOpenSurfaceEntry`).
- **Hybrid extras:** auto-carved *shortcut* passages per boundary, placed away from (0,0).
Now fully **per-strate** — see §8.8 (the upper strate's `PassageConfig` drives count/style/shape).
- The seals between rooms remain closed until progression opens the next connection. The one
optional above-ground opening is the top room's ceiling through strate 0
(`GeneratePassages`, `bOpenSurfaceEntry`); it never bores the lower rooms together.
- **Base connection:** the default inter-strate passage is a small, single-file, two-leg
walkable switchback with a named 15° floor-gradient cap and final-density floor/capsule audit.
A 32 m drop needs 119.4 m of horizontal run at that gradient, so a single direct ramp is not a
plausible default. The switchback is explicit geometry, not a fall shaft.
- **Hybrid extras:** auto-carved passages per boundary, placed away from (0,0). The upper
strate's `PassageConfig` still drives count/style/shape; the legacy vertical/spiral/cascade/
crack styles remain descriptors without the base walkability guarantee.
The tree is a construction guarantee for the shaft field: every seeded shaft has one inward edge,
and every chain strictly decreases distance to the origin until it reaches a local minimum that
@@ -503,28 +510,30 @@ historical random reach and are reported diagnostically.
The source query proves only a local pose; it does not flood-fill the live network. Therefore every
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)`.
flat floor and a deterministic connector to that strate's finite origin landing room 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.
1.36-voxel player radius and avoids capping unrelated shaft air; the origin room owns the root
floor, so there is no continuous root column. A connector's level dog-leg is sized against the same
named 15° floor-gradient limit as the default inter-strate tunnel. The room and connector floor are
therefore real walkable geometry, 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.
`max(12, 7.04+4, 2×MouthRadius+2)`; the stock 6-voxel mouth is a 14-voxel (3.5 m) room with
an 8-voxel half-width (4 m full width).
`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 final-density floor audit samples the flat plane and checks the player-fit capsule above it. The
default tunnel audit samples every route step against the final density and checks the analytic and
measured route gradients against the explicit ≤15° walkable-tunnel contract. This is deliberately
well below the movement CDO's roughly 44.8° scramble ceiling: 44° is not a sensible default for a
tunnel the player is meant to walk.
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
+112 -17
View File
@@ -1283,27 +1283,28 @@ The content half is deliberately not built here; landmark meshes, lights, sound,
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
The dimensions derive from the capsule and the existing turning-floor 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.
`max(12, 7.04+4, 2×MouthRadius+2)`; with the stock 6-voxel mouth, that is 14 voxels / 3.5 m
high and 8 voxels / 2 m half-width (4 m full width).
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).
joins the finite origin landing room, not a vertical hub. The default inter-strate tunnel is a
two-leg switchback with a named 15° floor-gradient limit; the connector uses the same limit. The
final-density audit checks the room floor and the capsule at every tunnel step. This is deliberately
well below the movement CDO's roughly 44.8° scramble ceiling.
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
connector for answered and refused source queries, ending at the finite landing room around `(0,0)`.
That room is the explicit walkable root network; it has a floor and a common interior level, and no
continuous vertical spine is carved between strates. 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
@@ -1343,14 +1344,25 @@ classification.
### 6.8 The (0,0) spine, exactly
`VF_ApplyOriginSpine` carves a guaranteed-open vertical column at actor-local XY (0,0), through every
strate, archetype-independent, appended automatically. **It carves only the strate INTERIOR and leaves
the seals intact on purpose** — its own comment: *"so the player must still dig through to descend"*. It
provides a clean landing space, not a continuous open shaft.
`VF_ApplyOriginSpine` no longer carves a full-height cylinder. It builds one finite, rounded landing
room at actor-local XY (0,0) in each strate, with a flat support floor. The old cylinder guaranteed
open air but supplied no surface to stand on except at its bottom; that was both an unusable hub and
an accidental fall shaft. The room is archetype-independent, appended automatically, and sized by
the §6.5 body arithmetic. It is clamped inside the strate's vertical seal interval and never crosses
the XY edge seal.
⇒ The hand-authored descent shaft is the **above-ground continuation of the spine**. Nothing new to
build. The city's hole must sit above the VoxelWorld actor's origin (see §6.9), and the shaft's diameter
should match the spine's `Radius` or the join will visibly step.
The seals between strates remain solid. A lower room is therefore not an open shaft destination: it
becomes reachable when progression opens the inter-strate passage, and the player arrives on that
strate's room floor. `bOpenSurfaceEntry` is the one explicit exception: when enabled, strate 0 may
open its top seal down to the top room ceiling as the city's above-ground continuation. It does not
open any lower seal or bore the rooms together.
The base inter-strate connection is a small walkable tunnel. Its default is a deterministic
two-leg switchback, with a named 15° maximum floor gradient, level landing aprons, and a floor-backed
capsule-clear tube. A 32 m vertical drop needs `32/tan(15°) = 119.4 m` of horizontal run before
aprons and turn transitions, so a single straight ramp would be implausibly long; the switchback is
an explicit design choice. `VerticalShaft`, `SpiralDescent`, `CascadingDrops`, and `CrackCrevice`
remain available as exotic descriptors, but they do not receive the default walkability guarantee.
### ✅ 6.9 Actor space — DONE (built and confirmed off-origin, 2026-08-17)
@@ -1792,6 +1804,10 @@ outside-gate sample count; neither is a Maze or box-soundness failure.
### 12.3 2026-09-06 §6.5 landing geometry pass
> Historical snapshot. This section describes the pre-§6.8 origin-spine correction (including the
> annular root hub and the 44° connector law). The current finite landing-room and walkable-tunnel
> measurements are recorded in §12.4 below.
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
@@ -1934,6 +1950,85 @@ assert only that nothing it reports is wrong.*
---
### 12.4 2026-09-08 §6.8 finite origin landings and walkable base descent
This is the current geometry and measurement record. `VF_ApplyOriginSpine` now creates one finite
rounded room at `(0,0)` in every strate. The room is top-anchored inside the safe interval
`[StrateBottomZ+Seal, StrateTopZ-Seal]`: `FloorZ = InnerTop - RoomHeight - 4`, with a 4-voxel
(`1 m`) seal-safety margin. The three-voxel (`0.75 m`) support slab and the final-density floor
backstop are inside the room; neither vertical seal is breached, and no lower room is connected by
a default vertical bore. Only `bOpenSurfaceEntry` may add the top-strate continuation from above.
The dimensions reuse the §6.5 body arithmetic at `0.25 m/voxel`: player radius `0.34 m = 1.36`
voxels, diameter `0.68 m = 2.72` voxels, capsule height `1.76 m = 7.04` voxels, and one metre
headroom `=4` voxels. The shared formulas are `HalfWidth=max(7,Radius+2)` and
`RoomHeight=max(12,7.04+4,2×Radius+2)`. Thus the authored default origin radius `14` gives a
16-voxel half-width (`4 m` half-width, `8 m` full width) and a 30-voxel (`7.5 m`) room height;
the stock passage mouth radius `6` gives an 8-voxel half-width and a 14-voxel (`3.5 m`) room
height. The one-voxel floor inset leaves at least the 3 m turning floor required by §6.5, with
space to stand, turn, and leave.
The default inter-strate connection is `EVoxelPassageType::SlopedTunnel`, authored as a deterministic
two-leg switchback. It is a single-file tube: 6-voxel mouth radius (`3 m` diameter), 5-voxel waist
radius (`2.5 m` diameter), and a nominal `2.5 m` clear height. Level aprons and turn transitions
are 17 and 6 voxels respectively in the stock configuration (`4.25 m` and `1.5 m`). The named
`WalkableTunnelMaxGradientDegrees=15°` contract (`tan(15°)=0.267949`) is deliberately far below the
movement system's roughly 44° scramble ceiling: 44° is not a sensible walking default for a
1.76 m-tall player in a small tunnel. For a 32 m drop, the required horizontal floor run is
`32/tan(15°)=119.43 m` (`477.70` voxels), or about `59.7 m` per switchback leg before adding the
level aprons and turn transitions. A straight gentle ramp would therefore be implausibly long;
the switchback is an explicit design decision. `VerticalShaft`, `SpiralDescent`, `CascadingDrops`,
and `CrackCrevice` remain in the enum for future exotic rolls, but the first three cannot be
continuously walkable as currently authored and the crack has no player-fit guarantee.
The focused final-density audit passed **7 passages / 42 segments**, with **5,916/5,916** floor
samples and **5,916/5,916** player-capsule samples. Gradient, floor, and capsule failures were all
zero; worst geometric gradient was `0.267758`, worst authored-route gradient `0.267793`, both below
`0.267949`. All **14/14** passage ends passed their floor test, all **14/14** explicit root joins
were present, seal violations were **0**, and geometry/floor failures were **0**. The cross-section
gradient diagnostic (`13.966`) is not the route contract; it reflects overlapping tapered SDF
cross-sections and is not counted as a walkability failure. Landing box checks saw **14** boxes,
zero uniform proofs (`AllSolid=0`, `AllAir=0`), and **0** violations. The per-chunk shortlist cache
timed at **1.259 μs** per forced rebuild and **1.243 μs** per hot same-chunk call on the validation
machine; there is no per-voxel cache rebuild and no per-sample logging.
The commandlet sweep used `-run=VoxelForgeExplore -modes=walk`, 8 archetypes × 8 seeds, target slot
4, six 4-chunk strates (`4×32×0.25=32 m` each), `WorldRadiusVoxels=0`, and schema-2 JSON. All
64 commandlets exited 0 and wrote valid JSON. `Connected` is the exact player-fit arrival→departure
law; `Exact` additionally excludes start/goal snapping.
| Archetype | Connected / exact | Origin fit / reachable | Arrival probes fit / in arrival component | Departure probes fit / in arrival component |
|---|---:|---:|---:|---:|
| CrystalChamber | 0 / 0 | 8 / 2 | 40/40 / 40/40 | 40/40 / 0/40 |
| FlatPlain | 0 / 0 | 8 / 2 | 40/40 / 40/40 | 40/40 / 0/40 |
| FloatingIslands | 3 / 2 | 8 / 1 | 35/40 / 35/40 | 35/40 / 10/40 |
| Maze | 0 / 0 | 7 / 0 | 40/40 / 40/40 | 40/40 / 0/40 |
| SurfaceWorld | 0 / 0 | 8 / 0 | 40/40 / 40/40 | 40/40 / 0/40 |
| TunnelNetwork | 0 / 0 | 8 / 1 | 40/40 / 40/40 | 40/40 / 0/40 |
| Underwater | 0 / 0 | 8 / 1 | 40/40 / 40/40 | 40/40 / 0/40 |
| VerticalShafts | 3 / 3 | 8 / 0 | 40/40 / 40/40 | 40/40 / 15/40 |
| **Total** | **6 / 5** | **63 / 7** | **315/320 / 315/320** | **315/320 / 25/320** |
The five-point room probe is the centre plus ±2 voxels in X/Y. Therefore 63/64 runs had every
arrival-room probe both player-fit and in the arrival component; all 315 arrival probes that fit
were in that component. The departure room had player-fit probes in 63/64 runs, but only 25/320
departure probes were in the arrival component (5/64 runs had all five there). The player-fit law
is **6/64 connected (5/64 exact; 58/64 not connected at this resolution)**. This is not tuned away:
the remaining failures are the separate intra-strate connectivity problem, not a tunnel gradient
or floor failure.
The origin-inclusive commandlet window changed the supplied baseline from **0/64 origin-reachable**
and **17/64 with any origin player-fit cell** to **7/64 reachable** and **63/64 with any fit cell**.
The no-fit case is Maze seed 7. The origin measurement is intentionally reported separately from
the mouth-sized route window; `WorldRadiusVoxels=0` means the report is a bounded fitted window, not
a claim about a finite whole-world volume.
Fresh commandlet walk timings were **0.653 s minimum, 13.411 s mean, 34.265 s maximum** across the
64 cases. The full Editor automation gate after the change was **32/32 successful, 0 failed**;
`LayoutOrderIndependence`, the box verdict tests, and all op-stack equivalence tests were green.
---
## 13. Tier 2 BUILT — and what measuring the world actually found (2026-09-03)
**The measurement pass exists, is falsifiable, and earned itself on the first run by finding a live
@@ -10,6 +10,7 @@
#include "VoxelCaveMorphology.h"
#include "VoxelDensityPrimitives.h"
#include "VoxelForgeTestFixture.h"
#include "VoxelPassageGeometry.h"
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FVoxelForgePassageLandsInOpenSpaceTest,
@@ -89,6 +90,366 @@ namespace
return 0.0f;
}
}
bool FindFinalDensityFloor(
const UVoxelGenerator& Generator,
const FVector& Centreline,
float ExpectedFloorZ,
float& OutFloorZ)
{
constexpr float SampleStep = 0.25f;
const float ExpectedAt = Generator.GetDensityAt(
Centreline.X, Centreline.Y, ExpectedFloorZ);
const float ExpectedBelow = Generator.GetDensityAt(
Centreline.X, Centreline.Y, ExpectedFloorZ - 0.75f);
const float ExpectedAbove = Generator.GetDensityAt(
Centreline.X, Centreline.Y, ExpectedFloorZ + 0.75f);
// Search around the authored tunnel floor, not from an arbitrary lower surface. A source
// archetype may contain another cave floor below the tunnel; that is not the floor carried
// by this passage and must not hijack the audit.
const float StartZ = ExpectedFloorZ - 4.0f;
const float EndZ = ExpectedFloorZ + 4.0f;
const int32 NumSteps = FMath::Max(
1, FMath::CeilToInt((EndZ - StartZ) / SampleStep));
float PreviousZ = StartZ;
float PreviousDensity = Generator.GetDensityAt(
Centreline.X, Centreline.Y, PreviousZ);
if (!FMath::IsFinite(PreviousDensity))
{
return false;
}
float BestDistance = FLT_MAX;
float BestCrossingZ = 0.0f;
for (int32 Step = 1; Step <= NumSteps; ++Step)
{
const float Alpha = static_cast<float>(Step)
/ static_cast<float>(NumSteps);
const float CurrentZ = FMath::Lerp(StartZ, EndZ, Alpha);
const float CurrentDensity = Generator.GetDensityAt(
Centreline.X, Centreline.Y, CurrentZ);
if (!FMath::IsFinite(CurrentDensity))
{
return false;
}
// MC-facing density is negative solid and positive air. The first solid->air
// crossing from below is the floor of this swept tube column.
if (PreviousDensity <= 0.0f && CurrentDensity > 0.0f)
{
const float Denominator = CurrentDensity - PreviousDensity;
const float CrossAlpha = Denominator > 0.0f
? FMath::Clamp(-PreviousDensity / Denominator, 0.0f, 1.0f)
: 0.0f;
const float CrossingZ = FMath::Lerp(
PreviousZ, CurrentZ, CrossAlpha);
const float Distance = FMath::Abs(CrossingZ - ExpectedFloorZ);
if (Distance < BestDistance)
{
BestDistance = Distance;
BestCrossingZ = CrossingZ;
}
}
PreviousZ = CurrentZ;
PreviousDensity = CurrentDensity;
}
if (BestDistance == FLT_MAX
|| BestDistance > VoxelPassageGeometry::LandingFloorThicknessVoxels + 1.0f)
{
return false;
}
const float Below = Generator.GetDensityAt(
Centreline.X, Centreline.Y, BestCrossingZ - 0.75f);
const float Above = Generator.GetDensityAt(
Centreline.X, Centreline.Y, BestCrossingZ + 0.75f);
if (!FMath::IsFinite(Below) || !FMath::IsFinite(Above)
|| !(Below < 0.0f) || !(Above > 0.0f))
{
return false;
}
// Use the final-density crossing itself. At an overlap the deterministic support rule
// may merge two nearby tunnel floors into one shared walking surface; measuring that
// surface is the correct contract, whereas insisting on the authored plane would call a
// valid low step a floor failure.
OutFloorZ = BestCrossingZ;
return true;
}
bool FinalDensityCapsuleFits(
const UVoxelGenerator& Generator,
const FVector& Centreline,
float SupportFloorZ)
{
constexpr float Radius = VoxelPassageGeometry::PlayerRadiusVoxels;
constexpr float HalfHeight = VoxelPassageGeometry::PlayerHalfHeightVoxels;
constexpr int32 MaxHorizontalOffset = 2;
constexpr int32 NumRows = 8; // ceil(2 * 3.52 voxel capsule half-height)
for (int32 Row = 0; Row < NumRows; ++Row)
{
const float RelativeZ = static_cast<float>(Row) + 0.5f - HalfHeight;
const float AxisHalfLength = FMath::Max(0.0f, HalfHeight - Radius);
const float DistanceToAxis = FMath::Max(
FMath::Abs(RelativeZ) - AxisHalfLength, 0.0f);
bool bSampledRow = false;
for (int32 OffsetY = -MaxHorizontalOffset;
OffsetY <= MaxHorizontalOffset; ++OffsetY)
{
for (int32 OffsetX = -MaxHorizontalOffset;
OffsetX <= MaxHorizontalOffset; ++OffsetX)
{
const float DistanceSquared = static_cast<float>(
OffsetX * OffsetX + OffsetY * OffsetY)
+ DistanceToAxis * DistanceToAxis;
if (DistanceSquared > Radius * Radius + KINDA_SMALL_NUMBER)
{
continue;
}
bSampledRow = true;
const float Density = Generator.GetDensityAt(
Centreline.X + static_cast<float>(OffsetX),
Centreline.Y + static_cast<float>(OffsetY),
SupportFloorZ + static_cast<float>(Row) + 0.5f);
if (!FMath::IsFinite(Density) || !(Density > 0.0f))
{
return false;
}
}
}
// Match the measurement stencil's defensive centre sample for the top partial row.
if (!bSampledRow)
{
const float Density = Generator.GetDensityAt(
Centreline.X, Centreline.Y,
SupportFloorZ + static_cast<float>(Row) + 0.5f);
if (!FMath::IsFinite(Density) || !(Density > 0.0f))
{
return false;
}
}
}
return true;
}
struct FWalkableTunnelAudit
{
int32 Contracts = 0;
int32 Segments = 0;
int32 FloorSamples = 0;
int32 CapsuleSamples = 0;
int32 GradientFailures = 0;
int32 GeometricGradientFailures = 0;
int32 RouteGradientFailures = 0;
int32 FloorFailures = 0;
int32 CapsuleFailures = 0;
float WorstGradient = 0.0f;
float WorstGeometricGradient = 0.0f;
float WorstPatchGradient = 0.0f;
float WorstRouteGradient = 0.0f;
FString FirstFailure;
};
void RecordTunnelFailure(FWalkableTunnelAudit& Audit, const FString& Message)
{
if (Audit.FirstFailure.IsEmpty())
{
Audit.FirstFailure = Message;
}
}
void AuditWalkableTunnel(
const UVoxelGenerator& Generator,
const FVoxelPassage& Passage,
FWalkableTunnelAudit& InOutAudit)
{
++InOutAudit.Contracts;
if (Passage.ControlPoints.Num() < 2
|| Passage.ControlRadii.Num() != Passage.ControlPoints.Num())
{
++InOutAudit.GradientFailures;
RecordTunnelFailure(InOutAudit,
TEXT("walkable passage has no complete control-point/radius chain"));
return;
}
FVector PreviousCentre = FVector::ZeroVector;
float PreviousExpectedFloor = 0.0f;
bool bHavePrevious = false;
for (int32 SegmentIndex = 0;
SegmentIndex + 1 < Passage.ControlPoints.Num();
++SegmentIndex)
{
++InOutAudit.Segments;
const FVector& Start = Passage.ControlPoints[SegmentIndex];
const FVector& End = Passage.ControlPoints[SegmentIndex + 1];
const float StartRadius = Passage.ControlRadii[SegmentIndex];
const float EndRadius = Passage.ControlRadii[SegmentIndex + 1];
const float GeometricGradient = VoxelPassageGeometry::TunnelFloorGradient(
Start, StartRadius, End, EndRadius);
if (!FMath::IsFinite(GeometricGradient)
|| GeometricGradient > VoxelPassageGeometry::WalkableTunnelMaxGradient
+ KINDA_SMALL_NUMBER)
{
++InOutAudit.GradientFailures;
++InOutAudit.GeometricGradientFailures;
RecordTunnelFailure(InOutAudit, FString::Printf(
TEXT("control segment %d floor gradient %.6f exceeds %.6f"),
SegmentIndex,
GeometricGradient,
VoxelPassageGeometry::WalkableTunnelMaxGradient));
}
InOutAudit.WorstGeometricGradient = FMath::Max(
InOutAudit.WorstGeometricGradient, GeometricGradient);
InOutAudit.WorstGradient = FMath::Max(
InOutAudit.WorstGradient, GeometricGradient);
const int32 NumSamples = FMath::Max(
1, FMath::CeilToInt(FVector::Dist(Start, End)));
for (int32 SampleIndex = 0; SampleIndex <= NumSamples; ++SampleIndex)
{
const float T = static_cast<float>(SampleIndex)
/ static_cast<float>(NumSamples);
const FVector Centre = FMath::Lerp(Start, End, T);
const FVector2D SupportOffsets[] = {
FVector2D::ZeroVector,
FVector2D(1.0f, 0.0f), FVector2D(-1.0f, 0.0f),
FVector2D(0.0f, 1.0f), FVector2D(0.0f, -1.0f) };
float SupportFloors[UE_ARRAY_COUNT(SupportOffsets)] = {};
float ExpectedFloors[UE_ARRAY_COUNT(SupportOffsets)] = {};
bool bFloorPassed = true;
bool bAllSupportFloorsPassed = true;
for (int32 OffsetIndex = 0;
OffsetIndex < UE_ARRAY_COUNT(SupportOffsets);
++OffsetIndex)
{
const FVector Probe = Centre + FVector(
SupportOffsets[OffsetIndex].X,
SupportOffsets[OffsetIndex].Y,
0.0f);
float ExpectedFloorZ = 0.0f;
float ExpectedSupportRadius = 0.0f;
const bool bProjected = VoxelPassageGeometry::ProjectWalkableTunnelFloor(
Passage.ControlPoints, Passage.ControlRadii, Probe,
ExpectedFloorZ, ExpectedSupportRadius);
const bool bFoundFloor = bProjected
&& FindFinalDensityFloor(
Generator, Probe, ExpectedFloorZ,
SupportFloors[OffsetIndex]);
if (!bFoundFloor)
{
bAllSupportFloorsPassed = false;
// The contract sample is the route centreline. Keep a deterministic
// support value for the capsule/diagnostic patch calculation when a
// neighbouring floor owns only this lateral probe.
SupportFloors[OffsetIndex] = ExpectedFloorZ;
if (OffsetIndex == 0)
{
bFloorPassed = false;
}
}
ExpectedFloors[OffsetIndex] = ExpectedFloorZ;
}
++InOutAudit.FloorSamples;
if (!bFloorPassed)
{
++InOutAudit.FloorFailures;
if (InOutAudit.FirstFailure.IsEmpty())
{
InOutAudit.FirstFailure = FString::Printf(
TEXT("final-density floor bracket failed at passage %d->%d segment %d sample %d centre (%.2f,%.2f,%.2f)"),
Passage.UpperStrateIndex, Passage.LowerStrateIndex,
SegmentIndex, SampleIndex, Centre.X, Centre.Y, Centre.Z);
}
bHavePrevious = false;
continue;
}
float PatchGradient = 0.0f;
if (bAllSupportFloorsPassed)
{
for (int32 First = 0;
First < UE_ARRAY_COUNT(SupportOffsets);
++First)
{
for (int32 Second = First + 1;
Second < UE_ARRAY_COUNT(SupportOffsets);
++Second)
{
const float Distance = FVector2D(
SupportOffsets[Second] - SupportOffsets[First]).Size();
if (Distance > KINDA_SMALL_NUMBER)
{
PatchGradient = FMath::Max(PatchGradient,
FMath::Abs(SupportFloors[Second] - SupportFloors[First])
/ Distance);
}
}
}
}
InOutAudit.WorstPatchGradient = FMath::Max(
InOutAudit.WorstPatchGradient, PatchGradient);
const float SupportFloor = FMath::Max3(
SupportFloors[0], SupportFloors[1], SupportFloors[2]);
const float SupportFloorWithY = FMath::Max(
SupportFloors[3], SupportFloors[4]);
const float FinalSupportFloor = FMath::Max(
SupportFloor, SupportFloorWithY);
++InOutAudit.CapsuleSamples;
if (!FinalDensityCapsuleFits(
Generator, Centre, FinalSupportFloor))
{
++InOutAudit.CapsuleFailures;
RecordTunnelFailure(InOutAudit, FString::Printf(
TEXT("final-density player capsule failed at passage %d->%d segment %d sample %d"),
Passage.UpperStrateIndex, Passage.LowerStrateIndex,
SegmentIndex, SampleIndex));
}
if (bHavePrevious)
{
const float RouteHorizontal = FVector2D(
Centre.X - PreviousCentre.X,
Centre.Y - PreviousCentre.Y).Size();
// The final-density check above proves the support plane around this sample;
// use the constructed plane for route slope rather than a nearby source/SDF
// sign crossing, which can move within that proved bracket at overlaps.
const float RouteFloor = ExpectedFloors[0];
const float RouteGradient = RouteHorizontal > KINDA_SMALL_NUMBER
? FMath::Abs(RouteFloor - PreviousExpectedFloor)
/ RouteHorizontal
: 0.0f;
if (!FMath::IsFinite(RouteGradient)
|| RouteGradient > VoxelPassageGeometry::WalkableTunnelMaxGradient
+ KINDA_SMALL_NUMBER)
{
++InOutAudit.GradientFailures;
++InOutAudit.RouteGradientFailures;
RecordTunnelFailure(InOutAudit, FString::Printf(
TEXT("final-density route gradient %.6f exceeds %.6f at passage %d->%d segment %d sample %d prev(%.2f,%.2f,%.2f) floor %.3f current(%.2f,%.2f,%.2f) floor %.3f expected %.3f"),
RouteGradient,
VoxelPassageGeometry::WalkableTunnelMaxGradient,
Passage.UpperStrateIndex, Passage.LowerStrateIndex,
SegmentIndex, SampleIndex,
PreviousCentre.X, PreviousCentre.Y, PreviousCentre.Z,
PreviousExpectedFloor,
Centre.X, Centre.Y, Centre.Z,
RouteFloor, ExpectedFloors[0]));
}
InOutAudit.WorstRouteGradient = FMath::Max(
InOutAudit.WorstRouteGradient, RouteGradient);
InOutAudit.WorstGradient = FMath::Max(
InOutAudit.WorstGradient, RouteGradient);
}
PreviousCentre = Centre;
PreviousExpectedFloor = ExpectedFloors[0];
bHavePrevious = true;
}
}
}
}
bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
@@ -145,9 +506,11 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
float WorstLandingConnectorGradient = 0.0f;
FString FirstLandingFailure;
FString LandingSlopeReport;
constexpr float LandingWalkableAngleDegrees = 44.0f;
const float LandingWalkableSlope = FMath::Tan(
FMath::DegreesToRadians(LandingWalkableAngleDegrees));
FWalkableTunnelAudit TunnelAudit;
constexpr float LandingWalkableAngleDegrees =
VoxelPassageGeometry::WalkableTunnelMaxGradientDegrees;
constexpr float LandingWalkableSlope =
VoxelPassageGeometry::WalkableTunnelMaxGradient;
for (const FVoxelPassage& Passage : Passages)
{
@@ -646,7 +1009,8 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
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),
FinalLegLength - (Landing.RootSpineRadius > 0.0f
? VoxelPassageGeometry::RootOverlapVoxels : 0.0f),
KINDA_SMALL_NUMBER);
ConnectorGradient = FMath::Abs(
Landing.RootFloorZ - Landing.FloorZ) / RampLength;
@@ -655,7 +1019,8 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
{
const float RampLength = FMath::Max(
DirectLength - FMath::Max(Landing.HalfWidth - 1.0f, 0.0f)
- (Landing.RootSpineRadius > 0.0f ? 2.25f : 0.0f),
- (Landing.RootSpineRadius > 0.0f
? VoxelPassageGeometry::RootOverlapVoxels : 0.0f),
KINDA_SMALL_NUMBER);
ConnectorGradient = FMath::Abs(
Landing.RootFloorZ - Landing.FloorZ) / RampLength;
@@ -686,9 +1051,25 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
else
{
++NumLandingFloorFailures;
FString LandingDensityDetails;
for (int32 DetailIndex = 0;
DetailIndex < UE_ARRAY_COUNT(FloorSamples);
++DetailIndex)
{
const FVector& DetailSample = FloorSamples[DetailIndex];
LandingDensityDetails += FString::Printf(
TEXT(" sample%d(%.2f,%.2f below%.2f at%.2f above%.2f)"),
DetailIndex, DetailSample.X, DetailSample.Y,
World.Generator->GetDensityAt(
DetailSample.X, DetailSample.Y, Landing.FloorZ - 1.0f),
World.Generator->GetDensityAt(
DetailSample.X, DetailSample.Y, Landing.FloorZ),
World.Generator->GetDensityAt(
DetailSample.X, DetailSample.Y, Landing.FloorZ + 1.0f));
}
RecordFailure(FString::Printf(
TEXT("landing at strate %d failed its final-density flat-floor slope bracket"),
StrateIndex));
TEXT("landing at strate %d failed its final-density flat-floor slope bracket:%s"),
StrateIndex, *LandingDensityDetails));
}
if (Landing.bSourcePlayerFit)
@@ -777,6 +1158,20 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
}
}
// The landing checks above prove the two endpoints. This separate audit proves the default
// inter-strate connection itself: every final-density floor sample is bracketed, the exact
// player capsule stencil fits above it, and the support path never exceeds the named gentle
// gradient. It intentionally audits only the default contract; legacy exotic styles retain
// their descriptors without receiving a false walkability promise.
for (const FVoxelPassage& Passage : Passages)
{
if (Passage.UpperStrateIndex != Passage.LowerStrateIndex
&& Passage.bWalkableTunnelContract)
{
AuditWalkableTunnel(*World.Generator, Passage, TunnelAudit);
}
}
// 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
@@ -871,6 +1266,29 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
{
AddError(FString::Printf(TEXT("First landing geometry failure: %s"), *FirstLandingFailure));
}
AddInfo(FString::Printf(
TEXT("Walkable tunnel contract: %d passages, %d segments, %d final floor samples, %d player-capsule samples; gradient failures %d (geometric %d, route %d), floor failures %d, capsule failures %d, worst gradient %.6f (geometric %.6f, route %.6f; cross-section diagnostic %.6f; limit %.6f / %.1f degrees)."),
TunnelAudit.Contracts,
TunnelAudit.Segments,
TunnelAudit.FloorSamples,
TunnelAudit.CapsuleSamples,
TunnelAudit.GradientFailures,
TunnelAudit.GeometricGradientFailures,
TunnelAudit.RouteGradientFailures,
TunnelAudit.FloorFailures,
TunnelAudit.CapsuleFailures,
TunnelAudit.WorstGradient,
TunnelAudit.WorstGeometricGradient,
TunnelAudit.WorstRouteGradient,
TunnelAudit.WorstPatchGradient,
VoxelPassageGeometry::WalkableTunnelMaxGradient,
VoxelPassageGeometry::WalkableTunnelMaxGradientDegrees));
if (!TunnelAudit.FirstFailure.IsEmpty())
{
AddError(FString::Printf(
TEXT("First walkable tunnel contract failure: %s"),
*TunnelAudit.FirstFailure));
}
// 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.
@@ -890,6 +1308,10 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
{
AddError(TEXT("VACUOUS: no Maze/VerticalShafts/FloatingIslands footing was checked."));
}
if (TunnelAudit.Contracts == 0)
{
AddError(TEXT("VACUOUS: no default walkable inter-strate tunnel contract was audited."));
}
const bool bAllNewFootingArchetypesAnswered =
AnsweredArchetypes.Contains(static_cast<uint8>(ECaveGeneratorType::Maze))
@@ -956,6 +1378,10 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
&& NumLandingSealFailures == 0
&& NumLandingFloorFailures == 0
&& NumLandingBoxViolations == 0
&& TunnelAudit.Contracts > 0
&& TunnelAudit.GradientFailures == 0
&& TunnelAudit.FloorFailures == 0
&& TunnelAudit.CapsuleFailures == 0
&& NumChecked > 0
&& NumUpperChecked > 0
&& NumFootingChecked > 0
@@ -8,6 +8,7 @@
#include "VoxelForgeTestFixture.h"
#include "VoxelCaveMorphology.h"
#include "VoxelPassageGeometry.h"
#include "VoxelStrateMeasure.h"
#include "VoxelTypes.h"
@@ -1732,7 +1733,6 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
for (int32 StrateIndex = 0; StrateIndex < Layout.Num(); ++StrateIndex)
{
const FStrateSlot& Slot = Layout[StrateIndex];
const FVoxelStrateMetrics& Window = DerivedMetricsByIndex[StrateIndex];
if (!Window.bValid || Window.SampledMinZ >= Window.SampledMaxZ) continue;
@@ -1745,22 +1745,36 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
const FConnectivityProbe Probe = ProbeRoute(
StrateIndex, A, B, /*bCountInGuard=*/true, /*bCountAsAnchorProbe=*/false,
/*bCountAsArrivalDeparture=*/false);
if (Probe.Result == EVoxelConnectivityResult::Connected
&& FullResolutionDirectSegmentHasSolid(*World.Generator, A, B))
{
AddError(FString::Printf(
TEXT("HARD FAILURE: VF_AreConnected returned CONNECTED for %s while its "
"full-resolution direct walk contained a solid sample."),
*ArchetypeName(Slot)));
bAllChecksPassed = false;
}
// A and B are deliberately a vertical diagnostic line, not the recovered route. A
// connected verdict is allowed to detour around solid terrain; only the route selected by
// VF_AreConnected is required to pass its own full-resolution re-walk. Comparing this
// unrelated direct segment to the verdict used to reject valid detours, and became
// especially misleading once the origin spine became a finite landing room.
(void)Probe;
}
// Controlled negative route: at SampleStep 4, the two coarse centres at X=-2 and X=+2
// remain air while the three full-resolution voxels at X=-1,0,+1 are filled solid.
const FVoxelStrateMetrics& WallWindow = DerivedMetricsByIndex[0];
const float WallZ = static_cast<float>(WallWindow.SampledMinZ)
+ 0.5f * static_cast<float>(DerivedWindowSettings.SampleStep);
const FStrateSlot& WallSlot = Layout[0];
const float WallTopZ = (static_cast<float>(WallSlot.TopChunkZ) + 1.0f) * CHUNK_SIZE;
const float WallBottomZ = static_cast<float>(WallSlot.BottomChunkZ) * CHUNK_SIZE;
const VoxelPassageGeometry::FOriginLandingGeometry WallLanding =
VoxelPassageGeometry::BuildOriginLandingGeometry(
WallTopZ,
WallBottomZ,
WallSlot.Definition != nullptr
? WallSlot.Definition->GenerationParams.BoundarySealThickness
: 0.0f,
World.Generator->OriginSpineRadius);
// The controlled wall must start in known air. The old probe used the bottom sample of the
// strate, which was only air because the old spine was a full-height cylinder. Put it in the
// finite top landing room instead; this keeps the coarse-lie control about the wall, not about
// an unrelated start-cell-solid result.
const float WallZ = WallLanding.bValid
? WallLanding.FloorZ + 4.0f
: static_cast<float>(WallWindow.SampledMinZ)
+ 0.5f * static_cast<float>(DerivedWindowSettings.SampleStep);
const FVector WallA(Settings.CenterXY.X - 2.0f, Settings.CenterXY.Y + 2.0f, WallZ);
const FVector WallB(Settings.CenterXY.X + 2.0f, Settings.CenterXY.Y + 2.0f, WallZ);
@@ -2058,7 +2058,8 @@ namespace
namespace
{
constexpr float VF_LandingCarveSafetyMargin = 4.0f;
constexpr float VF_LandingCarveSafetyMargin =
VoxelPassageGeometry::SealSafetyMarginVoxels;
struct FVoxelPassageLandingConnectorProjection
{
@@ -2142,12 +2143,13 @@ namespace
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.
// complete rise and ends with a short level overlap inside the root landing room.
if (Projection.SegmentIndex == 0)
{
return 0.0f;
}
const float RootFlatLength = Landing.RootSpineRadius > 0.0f ? 2.25f : 0.0f;
const float RootFlatLength = Landing.RootSpineRadius > 0.0f
? VoxelPassageGeometry::RootOverlapVoxels : 0.0f;
const float RampLength = FMath::Max(
Projection.SegmentLength - RootFlatLength, KINDA_SMALL_NUMBER);
return FMath::Clamp(
@@ -2159,7 +2161,8 @@ namespace
// 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 RootFlatLength = Landing.RootSpineRadius > 0.0f
? VoxelPassageGeometry::RootOverlapVoxels : 0.0f;
const float SourceDistance = FMath::Clamp(
SourceFlatLength, 0.0f, Projection.TotalLength * 0.49f);
const float RootDistance = FMath::Clamp(
@@ -2190,20 +2193,12 @@ FVoxelPassageLanding VF_BuildPassageLanding(
{
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.
// Body-derived dimensions at the authored 25 cm voxel scale are shared with the origin
// landing. In particular, the 0.68 m player diameter, 3 m turning patch, 1.76 m capsule
// height and 1 m headroom become the common §6.5 formulas in VoxelPassageGeometry.
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 DesiredHalfWidth = VoxelPassageGeometry::LandingHalfWidthForRadius(MouthRadius);
const float DesiredHeight = VoxelPassageGeometry::LandingHeightForRadius(MouthRadius);
const float SafeSeal = FMath::Max(BoundarySealThickness, 0.0f);
FVector Direction(InDoorDirection.X, InDoorDirection.Y, 0.0f);
@@ -2216,7 +2211,7 @@ FVoxelPassageLanding VF_BuildPassageLanding(
Landing.FloorZ = InStandingPoint.Z - 0.5f;
Landing.HalfWidth = DesiredHalfWidth;
Landing.CeilingZ = Landing.FloorZ + DesiredHeight;
Landing.FloorThickness = 3.0f;
Landing.FloorThickness = VoxelPassageGeometry::LandingFloorThicknessVoxels;
Landing.DoorDirection = Direction;
Landing.RootSpineRadius = FMath::Max(RootSpineRadius, 0.0f);
Landing.bSourcePlayerFit = bSourcePlayerFit;
@@ -2273,15 +2268,14 @@ FVoxelPassageLanding VF_BuildPassageLanding(
// 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
// 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);
// player walks out to the origin landing room. 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 15 degree walkable-tunnel limit by construction.
Landing.ConnectorRadius = VoxelPassageGeometry::ConnectorRadiusVoxels;
const float ConnectorHeight = VoxelPassageGeometry::ConnectorHeightVoxels;
Landing.ConnectorCeilingZ = Landing.FloorZ + ConnectorHeight;
Landing.RootFloorZ = NetworkPoint.Z;
Landing.RootCeilingZ = Landing.RootFloorZ + ConnectorHeight;
@@ -2318,41 +2312,25 @@ FVoxelPassageLanding VF_BuildPassageLanding(
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);
// The origin is now a finite landing room, not a vertical air column. End the connector
// at the requested room centre so its floor meets the room floor directly. The room's
// own rounded-box SDF and floor post provide the complete volume; no annular shaft hub is
// synthesized here, and no floor is omitted at the centre.
const FVector2D RootEntryXY(NetworkPoint.X, NetworkPoint.Y);
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.
// deterministic level dog-leg whenever the direct run cannot meet the 15-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
// room 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;
constexpr float WalkableSlope =
VoxelPassageGeometry::WalkableTunnelMaxGradient;
constexpr float RootFlatLength = VoxelPassageGeometry::RootOverlapVoxels;
const FVector2D SourceXY(Landing.ConnectorStart.X, Landing.ConnectorStart.Y);
const FVector2D RootXY(Landing.ConnectorEnd.X, Landing.ConnectorEnd.Y);
const FVector2D Direct = RootXY - SourceXY;
@@ -2460,23 +2438,6 @@ float VF_EvaluatePassageLandingSDF(
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;
@@ -2502,14 +2463,9 @@ bool VF_IsPassageLandingFloor(
// 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)
// three-metre turning patch. The origin room owns its centre, including its support floor.
if (IsInFloorBand(Position.Z, Landing.FloorZ, Landing.FloorThickness))
{
const float FloorHalfWidth = FMath::Max(Landing.HalfWidth - 1.0f, 0.0f);
if (FMath::Abs(Position.X - Landing.StandingPoint.X) <= FloorHalfWidth
@@ -2530,8 +2486,7 @@ bool VF_IsPassageLandingFloor(
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))
if (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
@@ -2546,25 +2501,6 @@ bool VF_IsPassageLandingFloor(
}
}
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;
@@ -1677,23 +1677,18 @@ namespace
VF_ApplyOriginSpine(InOut.Density, X, Y, Z, TopZ, BotZ, Seal, Base, Radius);
}
// Ne fait QUE de l'air ⇒ tue AllSolid, jamais AllAir. Identity quand le cercle XY rate la
// boîte, ou quand la boîte est entièrement hors de l'intérieur de la strate.
// ≡ le test cercle/boîte écrit à la main dans ClassifyTile aujourd'hui.
// The room kills AllSolid; its support slab also kills AllAir. Identity when the room
// misses the box or the box is wholly outside this strate's safe interior.
EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext&) const override
{
if (Radius <= 0.0f) { return EVoxelOpEffect::Identity; }
const float InnerTop = TopZ - Seal;
const float InnerBot = BotZ + Seal;
if (VoxelBox.Max.Z <= InnerBot || VoxelBox.Min.Z >= InnerTop) { return EVoxelOpEffect::Identity; }
const float Reach = Radius + VoxelDensityReach::SpineBlend;
const float CX = FMath::Clamp(0.0f, (float)VoxelBox.Min.X, (float)VoxelBox.Max.X);
const float CY = FMath::Clamp(0.0f, (float)VoxelBox.Min.Y, (float)VoxelBox.Max.Y);
if (CX * CX + CY * CY > Reach * Reach) { return EVoxelOpEffect::Identity; }
return EVoxelOpEffect::CarveOnly;
if (!VoxelPassageGeometry::OriginLandingRoomTouchesBox(
VoxelBox, TopZ, BotZ, Seal, Radius))
{
return EVoxelOpEffect::Identity;
}
return VoxelPassageGeometry::OriginLandingFloorTouchesBox(
VoxelBox, TopZ, BotZ, Seal, Radius)
? EVoxelOpEffect::Both : EVoxelOpEffect::CarveOnly;
}
const TCHAR* DebugName() const override { return TEXT("OriginSpineOp"); }
@@ -1842,8 +1837,10 @@ namespace
class FPassageCarveOp final : public IVoxelDensityOp
{
public:
FPassageCarveOp(const UVoxelStrateManager* InManager, float InBase, float InSeal)
: Manager(InManager), Base(InBase), Seal(InSeal) {}
FPassageCarveOp(const UVoxelStrateManager* InManager, float InBase, float InSeal,
float InTopZ, float InBotZ, float InSpineRadius)
: Manager(InManager), Base(InBase), Seal(InSeal),
TopZ(InTopZ), BotZ(InBotZ), SpineRadius(InSpineRadius) {}
EVoxelOpRole GetRole() const override { return EVoxelOpRole::StructuralPost; }
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density; }
@@ -1853,8 +1850,15 @@ namespace
void Eval(float X, float Y, float Z, FVoxelOpSample& InOut) const override
{
if (!Manager) { return; }
Manager->ApplyPassageModifier(InOut.Density, X, Y, Z, Base, Seal);
if (Manager)
{
Manager->ApplyPassageModifier(InOut.Density, X, Y, Z, Base, Seal);
}
// Keep the source and op-stack operation order identical: the origin room is carved
// before the boundary seal, then its support floor is reasserted after the passage
// post so no connector can remove the standing surface.
VF_ApplyOriginLandingFloor(
InOut.Density, X, Y, Z, TopZ, BotZ, Seal, Base, SpineRadius);
}
// ≡ la garde `AnyPassageNearBox` écrite à la main dans ClassifyTile — déjà écrite, ici
@@ -1862,7 +1866,9 @@ namespace
EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext&) const override
{
if (!Manager) { return EVoxelOpEffect::Identity; }
if (Manager->AnyPassageLandingFloorNearBox(VoxelBox.Min, VoxelBox.Max))
if (VoxelPassageGeometry::OriginLandingFloorTouchesBox(
VoxelBox, TopZ, BotZ, Seal, SpineRadius)
|| 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.
@@ -1876,7 +1882,7 @@ namespace
private:
const UVoxelStrateManager* Manager;
float Base, Seal;
float Base, Seal, TopZ, BotZ, SpineRadius;
};
//=========================================================================
@@ -5218,7 +5224,8 @@ void FVoxelOpStack::AppendStructuralPost(float StrateTopWorldZ, float StrateBott
// restent le dernier post de GetDensityAt, hors de cette pile, comme avant.
Add(MakeUnique<FOriginSpineOp>(StrateTopWorldZ, StrateBottomWorldZ, SealThickness, BaseDensity, SpineRadius));
Add(MakeUnique<FBoundarySealOp>(StrateTopWorldZ, StrateBottomWorldZ, SealThickness, BaseDensity));
Add(MakeUnique<FPassageCarveOp>(StrateManager, BaseDensity, SealThickness));
Add(MakeUnique<FPassageCarveOp>(StrateManager, BaseDensity, SealThickness,
StrateTopWorldZ, StrateBottomWorldZ, SpineRadius));
Add(MakeUnique<FXYEdgeSealOp>(BaseDensity));
}
+68 -6
View File
@@ -296,6 +296,14 @@ static FORCEINLINE void ApplyOriginSpine(float& Density, float WorldX, float Wor
StrateTopZ, StrateBottomZ, SealThickness, BaseDensity, Radius);
}
static FORCEINLINE void ApplyOriginLandingFloor(float& Density,
float WorldX, float WorldY, float WorldZ,
float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius)
{
VF_ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ,
StrateTopZ, StrateBottomZ, SealThickness, BaseDensity, Radius);
}
// DISTURBANCE LAYER — the "wow" post-process. Operates on the FINAL MC density
// (negative = solid, positive = air), AFTER the archetype produced its terrain, so
// it works uniformly for every generator type. Stays inside the seal bands so it can
@@ -1061,11 +1069,40 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
break;
}
}
// Origin rooms are structural too. Reassert their air before either support writer so a
// bridge/ridge cannot plug a landing, while the two floor writers remain last.
VF_ApplyOriginLandingAirMC(
Result, WorldX, WorldY, WorldZ,
CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ,
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
StrateManager->ApplyPassageLandingAirMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity,
CP_Dist.BoundarySealThickness);
VF_ApplyOriginLandingFloorMC(
Result, WorldX, WorldY, WorldZ,
CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ,
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
StrateManager->ApplyPassageLandingFloorMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity);
// A walkable tunnel's guaranteed air core is the final structural writer above its
// floor. This prevents a nearby passage's floor slab from capping the active route;
// the tunnel-floor predicate is false on the active floor itself, so support remains.
StrateManager->ApplyPassageTunnelAirMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity,
CP_Dist.BoundarySealThickness);
VF_ApplyOriginLandingAirMC(
Result, WorldX, WorldY, WorldZ,
CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ,
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
StrateManager->ApplyPassageLandingAirMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity,
CP_Dist.BoundarySealThickness);
VF_ApplyOriginLandingFloorMC(
Result, WorldX, WorldY, WorldZ,
CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ,
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
StrateManager->ApplyPassageLandingRoomFloorMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity);
}
else
{
@@ -2225,6 +2262,9 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
Density, WorldX, WorldY, WorldZ,
Params.BaseDensity, Params.BoundarySealThickness);
}
ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ,
Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius);
// Fourth structural post: the XY edge wins over a passage near the rim.
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY,
@@ -2448,6 +2488,9 @@ float UVoxelGenerator::GetSlabDensity(float WorldX, float WorldY, float WorldZ,
Density, WorldX, WorldY, WorldZ,
Params.BaseDensity, Params.BoundarySealThickness);
}
ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ,
Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius);
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY,
WorldRadiusVoxels, EdgeSealThickness, Params.BaseDensity);
@@ -2569,6 +2612,9 @@ float UVoxelGenerator::GetMazeDensity(float WorldX, float WorldY, float WorldZ,
Density, WorldX, WorldY, WorldZ,
Params.BaseDensity, Params.BoundarySealThickness);
}
ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ,
Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius);
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY,
WorldRadiusVoxels, EdgeSealThickness, Params.BaseDensity);
@@ -2833,6 +2879,9 @@ float UVoxelGenerator::SurfaceDensityFromColumn(float WorldX, float WorldY, floa
Density, WorldX, WorldY, WorldZ,
S.BaseDensity, S.BoundarySealThickness);
}
ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ,
S.StrateTopWorldZ, S.StrateBottomWorldZ,
S.BoundarySealThickness, S.BaseDensity, OriginSpineRadius);
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY,
WorldRadiusVoxels, EdgeSealThickness, S.BaseDensity);
@@ -3223,13 +3272,20 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
{
bCanAir = false;
}
if (OriginSpineRadius > 0.0f)
if (StrateManager->AnyOriginLandingNearBox(
FVector(MinX, MinY, MinZ), FVector(MaxX, MaxY, MaxZ)))
{
// Spine (0,0) : colonne XY rayon R + blend 3 (cf. ApplyOriginSpine). Test cercle/boîte XY.
const float Reach = OriginSpineRadius + 3.0f;
const float CX = FMath::Clamp(0.0f, (float)MinX, (float)MaxX);
const float CY = FMath::Clamp(0.0f, (float)MinY, (float)MaxY);
if (CX * CX + CY * CY <= Reach * Reach) bCanSolid = false;
// The origin post is a room carve, not a full-height spine. Its conservative manager
// guard is per tile, so it retains the hot-path invariant while covering every strate's
// room AABB and its four-voxel carve blend.
bCanSolid = false;
}
if (StrateManager->AnyOriginLandingFloorNearBox(
FVector(MinX, MinY, MinZ), FVector(MaxX, MaxY, MaxZ)))
{
// The same origin room also owns a guaranteed support slab, so a tile touching only that
// slab cannot be proved uniformly air.
bCanAir = false;
}
// ── Catégorisation par Z du treillis : gap bedrock = solide ; hors layout = air constant ;
@@ -4556,6 +4612,9 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
Density, WorldX, WorldY, WorldZ,
Params.BaseDensity, Params.BoundarySealThickness);
}
ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ,
Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius);
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY,
WorldRadiusVoxels, EdgeSealThickness, Params.BaseDensity);
@@ -4717,6 +4776,9 @@ float UVoxelGenerator::GetFloatingIslandDensity(float WorldX, float WorldY, floa
Density, WorldX, WorldY, WorldZ,
Params.BaseDensity, Params.BoundarySealThickness);
}
ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ,
Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius);
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY,
WorldRadiusVoxels, EdgeSealThickness, Params.BaseDensity);
+631 -52
View File
@@ -120,6 +120,165 @@ namespace
}
}
static float VF_BoundarySealThicknessForDefinition(
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;
}
}
/**
* The default inter-strate tunnel is a carved tube plus an explicit support slab. The tube's
* rounded SDF guarantees clearance, but it cannot guarantee a standable surface over a source
* field or a disturbance that was solid before the passage post. Project the query into the
* nearest horizontal control segment and return the conservative floor carried by that segment.
* This is a fixed-size arithmetic loop over the already-built descriptor; it performs no source
* search, allocation, or topology work.
*/
static bool VF_IsWalkableTunnelFloor(
const FVoxelPassage& Passage,
const FVector& Position)
{
if (!Passage.bWalkableTunnelContract
|| Passage.ControlPoints.Num() < 2
|| Passage.ControlRadii.Num() != Passage.ControlPoints.Num())
{
return false;
}
float FloorZ = 0.0f;
float SupportRadius = 0.0f;
if (!VoxelPassageGeometry::ProjectWalkableTunnelFloor(
Passage.ControlPoints, Passage.ControlRadii, Position,
FloorZ, SupportRadius))
{
return false;
}
return Position.Z <= FloorZ + KINDA_SMALL_NUMBER
&& Position.Z > FloorZ - VoxelPassageGeometry::LandingFloorThicknessVoxels;
}
static bool VF_IsWalkableTunnelAir(
const FVoxelPassage& Passage,
const FVector& Position)
{
if (!Passage.bWalkableTunnelContract
|| Passage.ControlPoints.Num() < 2
|| Passage.ControlRadii.Num() != Passage.ControlPoints.Num())
{
return false;
}
float FloorZ = 0.0f;
float SupportRadius = 0.0f;
if (!VoxelPassageGeometry::ProjectWalkableTunnelFloor(
Passage.ControlPoints, Passage.ControlRadii, Position,
FloorZ, SupportRadius))
{
return false;
}
// The carved tube is wider than this guaranteed core. The core is deliberately expressed as
// a vertical prism over the projected floor: it gives the final-density player stencil a
// stable air volume even when a source archetype or a disturbance would otherwise refill the
// shallow part of the rounded SDF. The floor writer below owns the closed lower face.
const float AirHeight = 2.0f * SupportRadius;
return Position.Z > FloorZ
+ VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels
&& Position.Z < FloorZ + AirHeight - KINDA_SMALL_NUMBER;
}
static bool VF_FindWalkableTunnelAir(
const UVoxelStrateManager* Manager,
const FVector& Position,
int32& OutPassageIndex,
float& OutFloorZ,
float& OutSupportRadius)
{
OutPassageIndex = INDEX_NONE;
OutFloorZ = 0.0f;
OutSupportRadius = 0.0f;
if (Manager == nullptr)
{
return false;
}
const FIntVector ChunkCoord(
FMath::FloorToInt(Position.X / (float)CHUNK_SIZE),
FMath::FloorToInt(Position.Y / (float)CHUNK_SIZE),
FMath::FloorToInt(Position.Z / (float)CHUNK_SIZE));
const TArray<int32>& Nearby = VF_GetNearbyPassages(Manager, ChunkCoord);
const TArray<FVoxelPassage>& Passages = Manager->GetPassages();
for (const int32 PassageIndex : Nearby)
{
if (!Passages.IsValidIndex(PassageIndex))
{
continue;
}
const FVoxelPassage& Passage = Passages[PassageIndex];
if (FVector::DistSquared(Position, Passage.BoundCenter) > Passage.BoundRadiusSq
|| !VF_IsWalkableTunnelAir(Passage, Position))
{
continue;
}
VoxelPassageGeometry::ProjectWalkableTunnelFloor(
Passage.ControlPoints, Passage.ControlRadii, Position,
OutFloorZ, OutSupportRadius);
OutPassageIndex = PassageIndex;
return true;
}
return false;
}
static bool VF_IsAnyPassageFloorAt(
const UVoxelStrateManager* Manager,
const FVector& Position)
{
if (Manager == nullptr)
{
return false;
}
const FIntVector ChunkCoord(
FMath::FloorToInt(Position.X / (float)CHUNK_SIZE),
FMath::FloorToInt(Position.Y / (float)CHUNK_SIZE),
FMath::FloorToInt(Position.Z / (float)CHUNK_SIZE));
const TArray<int32>& Nearby = VF_GetNearbyPassages(Manager, ChunkCoord);
const TArray<FVoxelPassage>& Passages = Manager->GetPassages();
for (const int32 PassageIndex : Nearby)
{
if (!Passages.IsValidIndex(PassageIndex))
{
continue;
}
const FVoxelPassage& Passage = Passages[PassageIndex];
if (VF_IsPassageLandingFloor(Position, Passage.UpperLanding)
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding)
|| VF_IsWalkableTunnelFloor(Passage, Position))
{
return true;
}
}
return false;
}
bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
{
if (!Settings)
@@ -756,14 +915,23 @@ void UVoxelStrateManager::GeneratePassages()
}
};
const auto CommonRootFloorFor = [](float StrateTopZ, float StrateBottomZ,
float BoundarySealThickness) -> float
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.
// The two mouth queries may legitimately return floors near opposite strate faces. The
// root is the origin landing room's floor, not a hidden continuous shaft and not a
// passage-order-dependent midpoint. Reusing the same top-anchored geometry here makes
// every connector meet the (0,0) room at its actual support level.
const VoxelPassageGeometry::FOriginLandingGeometry OriginLanding =
VoxelPassageGeometry::BuildOriginLandingGeometry(
StrateTopZ, StrateBottomZ, BoundarySealThickness, OriginSpineRadius);
if (OriginLanding.bValid)
{
return OriginLanding.FloorZ;
}
// Malformed tiny strates still fail closed. This fallback is only for the descriptor
// builder's diagnostic path; normal authored strates always satisfy the room interval.
const float Seal = FMath::Max(BoundarySealThickness, 0.0f);
const float InnerBottom = StrateBottomZ + Seal;
const float InnerTop = StrateTopZ - Seal;
@@ -790,8 +958,9 @@ void UVoxelStrateManager::GeneratePassages()
const FStrateSlot& Upper = StrateLayout[i];
const FStrateSlot& Lower = StrateLayout[i + 1];
// This (upper) strate's PassageConfig controls the descent tunnels to the layer
// below. The (0,0) spine descent is separate (player-dug); these are the shortcuts.
// This (upper) strate's PassageConfig controls the progression tunnel to the layer below.
// Each strate already owns a finite (0,0) landing room; this passage is the connection
// that opens the next room rather than a second vertical spine.
const UVoxelStrateDefinition* UpperDef = Upper.Definition;
if (!UpperDef) continue;
const FStratePassageConfig& Cfg = UpperDef->PassageConfig;
@@ -821,10 +990,25 @@ void UVoxelStrateManager::GeneratePassages()
FVoxelPassage Passage;
Passage.UpperStrateIndex = i;
Passage.LowerStrateIndex = i + 1;
// PassageType has no random draw in the current implementation; preserve its existing
// default while PassageConfig::Style controls the generated control-point shape.
// PassageType n'a pas de tirage aléatoire ici ; conserver son défaut, tandis que
// PassageConfig::Style contrôle la forme des points de contrôle générés.
// The authored style is also reflected in the legacy passage type. In particular,
// only Straight receives the walkable-tunnel contract; a future style roll cannot
// silently inherit the default's player-fit promise.
switch (Cfg.Style)
{
case EVoxelPassageStyle::Straight:
Passage.PassageType = EVoxelPassageType::SlopedTunnel;
break;
case EVoxelPassageStyle::Spiral:
Passage.PassageType = EVoxelPassageType::SpiralDescent;
break;
case EVoxelPassageStyle::Cascading:
Passage.PassageType = EVoxelPassageType::CascadingDrops;
break;
case EVoxelPassageStyle::Worm:
default:
Passage.PassageType = EVoxelPassageType::CrackCrevice;
break;
}
// PLACEMENT: random angle, distance from the (0,0) spine within config range.
const float Angle = PassageRandom01(PassageSaltAngle) * (2.0f * PI);
@@ -1142,7 +1326,223 @@ void UVoxelStrateManager::GeneratePassages()
if (Passage.UpperLanding.bHasNetworkConnector) ++NumUpperRootConnectors;
if (Passage.LowerLanding.bHasNetworkConnector) ++NumLowerRootConnectors;
if (Passage.ControlPoints.Num() > 0)
if (Cfg.Style == EVoxelPassageStyle::Straight
&& Passage.UpperLanding.HalfWidth > 0.0f
&& Passage.LowerLanding.HalfWidth > 0.0f)
{
// BASE CONNECTION: a walkable two-leg switchback. A direct line between the
// mouths would often be nearly vertical because both requests are placed at the
// same XY radius. Put the turn on the perpendicular bisector and give each leg
// at least half of the horizontal run required by the named 15-degree law.
// This is deliberately a construction rule, not a seed-dependent observation.
const float SwitchbackAngle =
PassageRandom01(PassageSaltUpperDoor) * 2.0f * PI;
FVector2D SwitchbackDirection(
FMath::Cos(SwitchbackAngle), FMath::Sin(SwitchbackAngle));
if (!SwitchbackDirection.Normalize())
{
SwitchbackDirection = FVector2D(1.0f, 0.0f);
}
const FVector DoorDirection(
SwitchbackDirection.X, SwitchbackDirection.Y, 0.0f);
Passage.UpperLanding.DoorDirection = DoorDirection;
// A real switchback turns around: the lower doorway faces back toward the upper
// leg. Keeping both doors on the same side would make the two sloped legs retrace
// one XY line at different heights, leaving the floor projection ambiguous.
const FVector TunnelLowerDoorDirection = -DoorDirection;
Passage.LowerLanding.DoorDirection = TunnelLowerDoorDirection;
const float UpperDoorOffset = FMath::Max(
Passage.UpperLanding.HalfWidth - 1.0f, 0.0f);
const float LowerDoorOffset = FMath::Max(
Passage.LowerLanding.HalfWidth - 1.0f, 0.0f);
const float SafeMouthRadius = FMath::Max(
FMath::Abs(Cfg.MouthRadius), 1.0f);
Passage.UpperLanding.DoorPoint =
Passage.UpperLanding.StandingPoint + DoorDirection * UpperDoorOffset;
Passage.UpperLanding.DoorPoint.Z =
Passage.UpperLanding.FloorZ + SafeMouthRadius;
Passage.LowerLanding.DoorPoint =
Passage.LowerLanding.StandingPoint
+ TunnelLowerDoorDirection * LowerDoorOffset;
Passage.LowerLanding.DoorPoint.Z =
Passage.LowerLanding.FloorZ + SafeMouthRadius;
const FVector UpperDoor = Passage.UpperLanding.DoorPoint;
const FVector LowerDoor = Passage.LowerLanding.DoorPoint;
// Keep a short level apron beyond each room floor. The door anchor is one voxel
// inside the room's support square; a diagonal door direction can therefore need
// several voxels before it leaves that square and its four-voxel SDF blend.
// The level apron must clear the room half-width, the tube radius, and the
// smooth carve band before the sloped leg begins. The final six-voxel transition
// is measured from that clear point back toward the turn, so include it too. For
// the default 8-voxel half-width / 6-voxel mouth this is 17 voxels (4.25 m), not
// the old 6-voxel minimum that left the sloped capsule inside the landing room.
const float LandingApron = FMath::Max(
VoxelPassageGeometry::WalkableTunnelLandingApronVoxels,
VoxelPassageGeometry::WalkableTunnelTurnTransitionVoxels
+ SafeMouthRadius
+ VoxelPassageGeometry::LandingCarveBlendVoxels
+ 1.0f);
const FVector UpperApronEnd = UpperDoor
+ DoorDirection * LandingApron;
const FVector LowerApronBegin = LowerDoor
+ TunnelLowerDoorDirection * LandingApron;
const float VerticalDrop = FMath::Abs(UpperDoor.Z - LowerDoor.Z);
const float UpperFloorZ = VoxelPassageGeometry::TunnelFloorZ(
UpperDoor, Cfg.MouthRadius);
const float LowerFloorZ = VoxelPassageGeometry::TunnelFloorZ(
LowerDoor, Cfg.MouthRadius);
const float MidFloorZ = 0.5f * (UpperFloorZ + LowerFloorZ);
const FVector2D RampStartMid(
0.5f * (UpperApronEnd.X + LowerApronBegin.X),
0.5f * (UpperApronEnd.Y + LowerApronBegin.Y));
// The turn offset must be perpendicular to the actual two-apron endpoint chord,
// not merely perpendicular to the standing-point chord. Opposite-facing doors
// add their offsets to that chord; using the old direction could make the two
// sloped legs nearly collinear and bring the lower leg back beside the upper door.
FVector2D TurnDirection(
LowerApronBegin.Y - UpperApronEnd.Y,
-(LowerApronBegin.X - UpperApronEnd.X));
if (!TurnDirection.Normalize())
{
TurnDirection = FVector2D(-DoorDirection.Y, DoorDirection.X);
if (!TurnDirection.Normalize())
{
TurnDirection = FVector2D(0.0f, 1.0f);
}
}
// There are two sides on which the switchback can turn. Choose the side whose
// two sloped legs initially move away from their own landing rooms; the old fixed
// sign could send a slope back through a room floor before reaching open space.
FVector2D UpperOutward(
UpperApronEnd.X - Passage.UpperLanding.StandingPoint.X,
UpperApronEnd.Y - Passage.UpperLanding.StandingPoint.Y);
FVector2D LowerOutward(
LowerApronBegin.X - Passage.LowerLanding.StandingPoint.X,
LowerApronBegin.Y - Passage.LowerLanding.StandingPoint.Y);
UpperOutward.Normalize();
LowerOutward.Normalize();
const float TurnOffsetForScore =
0.5f * VoxelPassageGeometry::RequiredHorizontalRunForFloorDrop(
UpperFloorZ - 0.5f * (UpperFloorZ + LowerFloorZ),
0.5f * (UpperFloorZ + LowerFloorZ) - LowerFloorZ)
+ VoxelPassageGeometry::WalkableTunnelTurnTransitionVoxels;
const FVector2D TurnMidPlus(
RampStartMid.X + TurnDirection.X * TurnOffsetForScore,
RampStartMid.Y + TurnDirection.Y * TurnOffsetForScore);
const FVector2D TurnMidMinus(
RampStartMid.X - TurnDirection.X * TurnOffsetForScore,
RampStartMid.Y - TurnDirection.Y * TurnOffsetForScore);
const auto OutwardTurnScore = [
&UpperOutward, &LowerOutward, &UpperApronEnd, &LowerApronBegin]
(const FVector2D& Candidate) -> float
{
return FVector2D::DotProduct(
Candidate - FVector2D(
UpperApronEnd.X, UpperApronEnd.Y), UpperOutward)
+ FVector2D::DotProduct(
FVector2D(LowerApronBegin.X, LowerApronBegin.Y) - Candidate,
LowerOutward);
};
if (OutwardTurnScore(TurnMidMinus) > OutwardTurnScore(TurnMidPlus))
{
TurnDirection *= -1.0f;
}
const float UpperFloorDrop = UpperFloorZ - MidFloorZ;
const float LowerFloorDrop = MidFloorZ - LowerFloorZ;
const float RequiredHorizontalRun =
VoxelPassageGeometry::RequiredHorizontalRunForFloorDrop(
UpperFloorDrop, LowerFloorDrop);
// Move the level turn farther than half the required run. Each slope then loses
// only the named transition length to its level segment and still retains the
// full horizontal run required by the 15-degree floor law.
const float TurnTransition =
VoxelPassageGeometry::WalkableTunnelTurnTransitionVoxels;
const float TurnOffset =
0.5f * RequiredHorizontalRun + TurnTransition;
const FVector2D FloorSafeDoorMid(
RampStartMid.X
+ TurnDirection.X * TurnOffset,
RampStartMid.Y
+ TurnDirection.Y * TurnOffset);
FVector2D UpperSlopeDirection(
FloorSafeDoorMid.X - UpperApronEnd.X,
FloorSafeDoorMid.Y - UpperApronEnd.Y);
if (!UpperSlopeDirection.Normalize())
{
UpperSlopeDirection = FVector2D(
DoorDirection.X, DoorDirection.Y);
}
FVector2D LowerSlopeDirection(
LowerApronBegin.X - FloorSafeDoorMid.X,
LowerApronBegin.Y - FloorSafeDoorMid.Y);
if (!LowerSlopeDirection.Normalize())
{
LowerSlopeDirection = -UpperSlopeDirection;
}
const FVector2D UpperSlopeStartXY(
UpperApronEnd.X + UpperSlopeDirection.X * TurnTransition,
UpperApronEnd.Y + UpperSlopeDirection.Y * TurnTransition);
const FVector2D LowerSlopeEndXY(
LowerApronBegin.X - LowerSlopeDirection.X * TurnTransition,
LowerApronBegin.Y - LowerSlopeDirection.Y * TurnTransition);
const FVector UpperApronEndPoint(
UpperApronEnd.X, UpperApronEnd.Y,
UpperFloorZ + FMath::Abs(Cfg.MouthRadius));
const FVector UpperSlopeStart(
UpperSlopeStartXY.X, UpperSlopeStartXY.Y,
UpperFloorZ + FMath::Abs(Cfg.MouthRadius));
const FVector MidPoint(
FloorSafeDoorMid.X, FloorSafeDoorMid.Y,
MidFloorZ + FMath::Abs(Cfg.MidRadius));
const FVector LowerSlopeEnd(
LowerSlopeEndXY.X, LowerSlopeEndXY.Y,
LowerFloorZ + FMath::Abs(Cfg.MouthRadius));
const FVector LowerApronBeginPoint(
LowerApronBegin.X, LowerApronBegin.Y,
LowerFloorZ + FMath::Abs(Cfg.MouthRadius));
Passage.ControlPoints.Reset(7);
Passage.ControlRadii.Reset(7);
Passage.ControlPoints.Add(UpperDoor);
Passage.ControlPoints.Add(UpperApronEndPoint);
Passage.ControlPoints.Add(UpperSlopeStart);
Passage.ControlPoints.Add(MidPoint);
Passage.ControlPoints.Add(LowerSlopeEnd);
Passage.ControlPoints.Add(LowerApronBeginPoint);
Passage.ControlPoints.Add(LowerDoor);
Passage.ControlRadii.Add(Cfg.MouthRadius);
Passage.ControlRadii.Add(Cfg.MouthRadius);
Passage.ControlRadii.Add(Cfg.MouthRadius);
Passage.ControlRadii.Add(Cfg.MidRadius);
Passage.ControlRadii.Add(Cfg.MouthRadius);
Passage.ControlRadii.Add(Cfg.MouthRadius);
Passage.ControlRadii.Add(Cfg.MouthRadius);
Passage.PassageType = EVoxelPassageType::SlopedTunnel;
Passage.bWalkableTunnelContract = true;
Passage.TunnelMaxGradientDegrees =
VoxelPassageGeometry::WalkableTunnelMaxGradientDegrees;
Passage.TunnelVerticalDropVoxels = VerticalDrop;
Passage.TunnelRequiredHorizontalRunVoxels = RequiredHorizontalRun;
Passage.TunnelHorizontalPathLengthVoxels = 0.0f;
for (int32 ControlIndex = 0;
ControlIndex + 1 < Passage.ControlPoints.Num();
++ControlIndex)
{
Passage.TunnelHorizontalPathLengthVoxels += FVector2D(
Passage.ControlPoints[ControlIndex + 1].X
- Passage.ControlPoints[ControlIndex].X,
Passage.ControlPoints[ControlIndex + 1].Y
- Passage.ControlPoints[ControlIndex].Y).Size();
}
Passage.TunnelMinimumClearWidthVoxels = 2.0f * FMath::Max(
FMath::Min(FMath::Abs(Cfg.MouthRadius), FMath::Abs(Cfg.MidRadius)),
0.0f);
Passage.TunnelClearHeightVoxels = Passage.TunnelMinimumClearWidthVoxels;
}
else if (Passage.ControlPoints.Num() > 0)
{
Passage.ControlPoints[0] = Passage.UpperLanding.DoorPoint;
Passage.ControlPoints.Last() = Passage.LowerLanding.DoorPoint;
@@ -1258,36 +1658,61 @@ void UVoxelStrateManager::GeneratePassages()
*NoQueryList);
//=========================================================================
// SURFACE ENTRY SHAFT — the one auto-opened (0,0) connection.
// A straight vertical shaft at (0,0) piercing the TOP seal of the topmost
// strate, so the world begins with "a hole opened to the surface". All other
// (0,0) descents between strates remain player-dug.
// SURFACE ENTRY — the one optional above-ground opening.
// It opens the top seal and stops at the ceiling of strate 0's origin landing room. It does
// not continue down the room or into lower strates: every lower seal remains closed until the
// corresponding progression passage is opened.
//=========================================================================
if (bOpenSurfaceEntry && StrateLayout.Num() > 0)
if (bOpenSurfaceEntry && OriginSpineRadius > 0.0f && StrateLayout.Num() > 0)
{
const FStrateSlot& Top = StrateLayout[0];
const float TopZ = (float)(Top.TopChunkZ + 1) * CHUNK_SIZE;
FVoxelPassage Entry;
Entry.UpperStrateIndex = 0;
Entry.LowerStrateIndex = 0;
Entry.PassageType = EVoxelPassageType::VerticalShaft;
Entry.Radius = FMath::Max(OriginSpineRadius * 0.7f, 4.0f);
// From a little above the strate top (open air outside all strates) down
// past the seal into the interior, so the seal at (0,0) is breached.
Entry.UpperPoint = FVector(0.0f, 0.0f, TopZ + CHUNK_SIZE);
Entry.LowerPoint = FVector(0.0f, 0.0f, TopZ - CHUNK_SIZE);
const float BottomZ = (float)Top.BottomChunkZ * CHUNK_SIZE;
const float TopSeal = Top.Definition
? BoundarySealThicknessFor(*Top.Definition) : 0.0f;
const VoxelPassageGeometry::FOriginLandingGeometry OriginLanding =
VoxelPassageGeometry::BuildOriginLandingGeometry(
TopZ, BottomZ, TopSeal, OriginSpineRadius);
if (!OriginLanding.bValid)
{
const FVector C = (Entry.UpperPoint + Entry.LowerPoint) * 0.5f;
const float R = (float)FVector::Dist(C, Entry.UpperPoint) + Entry.Radius + 4.0f;
Entry.BoundCenter = C;
Entry.BoundRadius = R;
Entry.BoundRadiusSq = R * R;
UE_LOG(LogTemp, Warning,
TEXT("[StrateManager] Surface entry skipped: top origin landing has no seal-safe room interval."));
}
Passages.Add(Entry);
else
{
FVoxelPassage Entry;
Entry.UpperStrateIndex = 0;
Entry.LowerStrateIndex = 0;
// Keep the legacy type: this is the explicitly opted-in above-ground vertical opening,
// not the default inter-strate descent style.
Entry.PassageType = EVoxelPassageType::VerticalShaft;
Entry.Radius = FMath::Max(OriginSpineRadius * 0.7f, 4.0f);
Entry.UpperPoint = FVector(0.0f, 0.0f, TopZ + CHUNK_SIZE);
// The capsule's lower tangent meets the room ceiling; it never bores through the
// room's floor or creates an origin column in strate 0.
Entry.LowerPoint = FVector(
0.0f, 0.0f, OriginLanding.CeilingZ + Entry.Radius);
Entry.ControlPoints.Reset(2);
Entry.ControlPoints.Add(Entry.UpperPoint);
Entry.ControlPoints.Add(Entry.LowerPoint);
Entry.ControlRadii.Reset(2);
Entry.ControlRadii.Add(Entry.Radius);
Entry.ControlRadii.Add(Entry.Radius);
Entry.TunnelClearHeightVoxels = 2.0f * Entry.Radius;
Entry.TunnelMinimumClearWidthVoxels = 2.0f * Entry.Radius;
{
const FVector C = (Entry.UpperPoint + Entry.LowerPoint) * 0.5f;
const float R = (float)FVector::Dist(C, Entry.UpperPoint) + Entry.Radius + 4.0f;
Entry.BoundCenter = C;
Entry.BoundRadius = R;
Entry.BoundRadiusSq = R * R;
}
Passages.Add(Entry);
UE_LOG(LogTemp, Log, TEXT("[StrateManager] Surface entry shaft at (0,0) topZ=%.0f R=%.1f"),
TopZ, Entry.Radius);
UE_LOG(LogTemp, Log,
TEXT("[StrateManager] Surface entry opening at (0,0) topZ=%.0f roomFloor=%.1f roomCeiling=%.1f R=%.1f"),
TopZ, OriginLanding.FloorZ, OriginLanding.CeilingZ, Entry.Radius);
}
}
// Invalidate any thread_local per-chunk passage shortlists (see EvaluateModifierSDF).
@@ -1383,7 +1808,8 @@ void UVoxelStrateManager::ApplyPassageModifier(
{
const FVoxelPassage& Passage = Passages[PassageIndex];
if (VF_IsPassageLandingFloor(Position, Passage.UpperLanding)
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding))
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding)
|| VF_IsWalkableTunnelFloor(Passage, Position))
{
// 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
@@ -1392,6 +1818,12 @@ void UVoxelStrateManager::ApplyPassageModifier(
break;
}
}
// A neighbouring passage may own a floor at this XY while this point is in the air core of
// the current walkable tunnel. Reassert tunnel air after all floor posts so overlap cannot
// turn a valid route into a solid plug. At the tunnel's own floor the air predicate is false,
// so the support plane remains solid.
ApplyPassageTunnelAir(Density, WorldX, WorldY, WorldZ, BaseDensity, SealThickness);
}
void UVoxelStrateManager::ApplyPassageLandingAir(
@@ -1406,6 +1838,13 @@ void UVoxelStrateManager::ApplyPassageLandingAir(
if (Nearby.Num() == 0) return;
const FVector Position(WorldX, WorldY, WorldZ);
// A smooth room blend may overlap the end of a ramp. The analytic support plane owns that
// overlap; otherwise landing air can erase the tunnel's final-density floor after the tunnel
// air post has deliberately stopped above it.
if (VF_IsAnyPassageFloorAt(this, Position))
{
return;
}
float MinLandingSDF = FLT_MAX;
for (const int32 PassageIndex : Nearby)
{
@@ -1448,6 +1887,35 @@ void UVoxelStrateManager::ApplyPassageLandingAirMC(
Density = -InternalDensity;
}
void UVoxelStrateManager::ApplyPassageTunnelAir(
float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const
{
const FVector Position(WorldX, WorldY, WorldZ);
int32 PassageIndex = INDEX_NONE;
float FloorZ = 0.0f;
float SupportRadius = 0.0f;
const bool bFoundWalkableAir = VF_FindWalkableTunnelAir(
this, Position, PassageIndex, FloorZ, SupportRadius);
if (bFoundWalkableAir)
{
const float AirTarget = -(BaseDensity * 2.0f + SealThickness + 4.0f);
Density = FMath::Min(Density, AirTarget);
}
}
void UVoxelStrateManager::ApplyPassageTunnelAirMC(
float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const
{
// The disturbance layer uses MC polarity (negative = solid), so reuse the exact internal
// tunnel-air operation rather than maintaining a second polarity-specific formula.
float InternalDensity = -Density;
ApplyPassageTunnelAir(
InternalDensity, WorldX, WorldY, WorldZ, BaseDensity, SealThickness);
Density = -InternalDensity;
}
void UVoxelStrateManager::ApplyPassageLandingFloorMC(
float& Density, float WorldX, float WorldY, float WorldZ, float BaseDensity) const
{
@@ -1461,7 +1929,8 @@ void UVoxelStrateManager::ApplyPassageLandingFloorMC(
{
const FVoxelPassage& Passage = Passages[PassageIndex];
if (VF_IsPassageLandingFloor(Position, Passage.UpperLanding)
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding))
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding)
|| VF_IsWalkableTunnelFloor(Passage, Position))
{
// Result is in MC convention here (negative = solid). This reassertion is the
// structural floor backstop after the optional MC-space disturbance layer.
@@ -1471,6 +1940,46 @@ void UVoxelStrateManager::ApplyPassageLandingFloorMC(
}
}
static bool VF_IsPassageRoomFloor(
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;
}
return Position.Z <= Landing.FloorZ + KINDA_SMALL_NUMBER
&& Position.Z > Landing.FloorZ - Landing.FloorThickness
&& FMath::Abs(Position.X - Landing.StandingPoint.X)
<= FMath::Max(Landing.HalfWidth - 1.0f, 0.0f)
&& FMath::Abs(Position.Y - Landing.StandingPoint.Y)
<= FMath::Max(Landing.HalfWidth - 1.0f, 0.0f);
}
void UVoxelStrateManager::ApplyPassageLandingRoomFloorMC(
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_IsPassageRoomFloor(Position, Passage.UpperLanding)
|| VF_IsPassageRoomFloor(Position, Passage.LowerLanding))
{
Density = FMath::Min(Density, -BaseDensity);
break;
}
}
}
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
@@ -1564,20 +2073,48 @@ bool UVoxelStrateManager::AnyPassageLandingFloorNearBox(
return true;
}
if (Landing->RootSpineRadius >= 0.0f)
}
}
// The default tube also owns a three-voxel support band. Its conservative segment AABB
// keeps a classifier from proving AllAir over the floor and then dropping that support
// during meshing. False positives are intentional; false negatives would be a hole.
if (Passage.bWalkableTunnelContract
&& Passage.ControlPoints.Num() >= 2
&& Passage.ControlRadii.Num() == Passage.ControlPoints.Num())
{
constexpr float Pad = 1.0f;
for (int32 SegmentIndex = 0;
SegmentIndex + 1 < Passage.ControlPoints.Num();
++SegmentIndex)
{
const FVector& A = Passage.ControlPoints[SegmentIndex];
const FVector& B = Passage.ControlPoints[SegmentIndex + 1];
const float SupportRadius = FMath::Max(
FMath::Min(
FMath::Abs(Passage.ControlRadii[SegmentIndex]),
FMath::Abs(Passage.ControlRadii[SegmentIndex + 1])) - 0.5f,
VoxelPassageGeometry::PlayerRadiusVoxels);
const float FloorMinZ = FMath::Min(
VoxelPassageGeometry::TunnelFloorZ(
A, Passage.ControlRadii[SegmentIndex]),
VoxelPassageGeometry::TunnelFloorZ(
B, Passage.ControlRadii[SegmentIndex + 1]))
- VoxelPassageGeometry::LandingFloorThicknessVoxels - Pad;
const float FloorMaxZ = FMath::Max(
VoxelPassageGeometry::TunnelFloorZ(
A, Passage.ControlRadii[SegmentIndex]),
VoxelPassageGeometry::TunnelFloorZ(
B, Passage.ControlRadii[SegmentIndex + 1])) + Pad;
const float FloorMinX = FMath::Min(A.X, B.X) - SupportRadius - Pad;
const float FloorMaxX = FMath::Max(A.X, B.X) + SupportRadius + Pad;
const float FloorMinY = FMath::Min(A.Y, B.Y) - SupportRadius - Pad;
const float FloorMaxY = FMath::Max(A.Y, B.Y) + SupportRadius + Pad;
if (FloorMaxX >= MinVoxel.X && FloorMinX <= MaxVoxel.X
&& FloorMaxY >= MinVoxel.Y && FloorMinY <= MaxVoxel.Y
&& FloorMaxZ >= MinVoxel.Z && FloorMinZ <= MaxVoxel.Z)
{
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 true;
}
}
}
@@ -1585,6 +2122,48 @@ bool UVoxelStrateManager::AnyPassageLandingFloorNearBox(
return false;
}
bool UVoxelStrateManager::AnyOriginLandingNearBox(
const FVector& MinVoxel, const FVector& MaxVoxel) const
{
if (OriginSpineRadius <= 0.0f) return false;
const FBox VoxelBox(MinVoxel, MaxVoxel);
for (const FStrateSlot& Slot : StrateLayout)
{
if (Slot.Definition == nullptr) continue;
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * CHUNK_SIZE;
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * CHUNK_SIZE;
if (VoxelPassageGeometry::OriginLandingRoomTouchesBox(
VoxelBox, TopZ, BottomZ,
VF_BoundarySealThicknessForDefinition(*Slot.Definition),
OriginSpineRadius))
{
return true;
}
}
return false;
}
bool UVoxelStrateManager::AnyOriginLandingFloorNearBox(
const FVector& MinVoxel, const FVector& MaxVoxel) const
{
if (OriginSpineRadius <= 0.0f) return false;
const FBox VoxelBox(MinVoxel, MaxVoxel);
for (const FStrateSlot& Slot : StrateLayout)
{
if (Slot.Definition == nullptr) continue;
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * CHUNK_SIZE;
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * CHUNK_SIZE;
if (VoxelPassageGeometry::OriginLandingFloorTouchesBox(
VoxelBox, TopZ, BottomZ,
VF_BoundarySealThicknessForDefinition(*Slot.Definition),
OriginSpineRadius))
{
return true;
}
}
return false;
}
//=============================================================================
// QUERIES
//=============================================================================
@@ -2863,7 +2863,8 @@ bool VF_MeasurePlayerFitWalkWithSampler(
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
FVoxelPlayerFitWalkReport& OutReport)
FVoxelPlayerFitWalkReport& OutReport,
const TArray<FVector>* ComponentProbePoints)
{
using namespace VoxelStrateMeasurePrivate;
@@ -3038,6 +3039,28 @@ bool VF_MeasurePlayerFitWalkWithSampler(
? Components[Start] : INDEX_NONE;
const int32 GoalComponent = Goal >= 0 && Components.IsValidIndex(Goal)
? Components[Goal] : INDEX_NONE;
if (ComponentProbePoints != nullptr)
{
OutReport.ComponentProbePlayerFit.Init(0u, ComponentProbePoints->Num());
OutReport.ComponentProbeInStartComponent.Init(0u, ComponentProbePoints->Num());
for (int32 ProbeIndex = 0; ProbeIndex < ComponentProbePoints->Num(); ++ProbeIndex)
{
int32 ProbeCell = INDEX_NONE;
const FVector& ProbePoint = (*ComponentProbePoints)[ProbeIndex];
if (!FindCellForPoint(Grid, ProbePoint, ProbeCell)
|| !IsEligiblePlayerFitCell(Grid, PlayerFitMask, ProbeCell))
{
continue;
}
OutReport.ComponentProbePlayerFit[ProbeIndex] = 1u;
if (StartComponent >= 0 && Components.IsValidIndex(ProbeCell)
&& Components[ProbeCell] == StartComponent)
{
OutReport.ComponentProbeInStartComponent[ProbeIndex] = 1u;
}
}
}
if (StartComponent >= 0)
{
float BestOriginDistanceSquared = FLT_MAX;
@@ -51,12 +51,11 @@ enum class ECaveGeneratorType : uint8;
* 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.
* strate, so both mouths enter the finite landing room at (0,0) even when their authored floors
* are at opposite sides of the strate. `RootSpineRadius` supplies the small endpoint overlap that
* joins the connector to that room; it does not create a vertical shaft. If the direct run would
* be steeper than the named 15 degree tunnel contract, 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
{
@@ -26,6 +26,7 @@
#include "CoreMinimal.h"
#include "VoxelTypes.h" // SmoothStep01
#include "VoxelPassageGeometry.h" // shared body-sized landing/tunnel geometry
//=============================================================================
// SEAL DE FRONTIÈRE / BOUNDARY SEAL
@@ -110,33 +111,97 @@ FORCEINLINE void VF_ApplyPassageLandingCarving(float& Density, float LandingSDF,
}
//=============================================================================
// SPINE DE DESCENTE (0,0) / (0,0) DESCENT SPINE
// LANDING ROOM ORIGINE (0,0) / ORIGIN LANDING ROOM
//=============================================================================
// Creuse une colonne verticale garantie ouverte au XY monde (0,0) dans l'INTÉRIEUR de la strate
// (entre les seals haut et bas). Les seals sont laissés intacts pour que le joueur doive encore
// creuser à travers pour descendre — ceci ne fait qu'un espace d'atterrissage propre, indépendant
// de l'archétype, aligné à travers toutes les strates.
// The old implementation carved a full-height cylinder. That made the origin visually open but
// left no floor anywhere except at the bottom, so it was a fall shaft rather than a landing. The
// origin primitive is now a top-anchored rounded room with a real flat floor in every strate.
// It never crosses either vertical seal. The inter-strate seal remains solid until a passage (or
// the player) opens it; there is no default vertical bore between rooms.
FORCEINLINE void VF_ApplyOriginSpine(float& Density, float WorldX, float WorldY, float WorldZ,
float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius)
{
if (Radius <= 0.0f) return;
// Stay within the interior — never touch the seal bands.
const float InnerTop = StrateTopZ - SealThickness;
const float InnerBot = StrateBottomZ + SealThickness;
if (WorldZ <= InnerBot || WorldZ >= InnerTop) return;
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
VoxelPassageGeometry::BuildOriginLandingGeometry(
StrateTopZ, StrateBottomZ, SealThickness, Radius);
if (!Geometry.bValid) return;
const float DistXY = FMath::Sqrt(WorldX * WorldX + WorldY * WorldY);
const float SDF = DistXY - Radius; // < 0 inside the column
const float Blend = 3.0f;
if (SDF < Blend)
const float RoomSDF = VoxelPassageGeometry::OriginLandingRoomSDF(
FVector(WorldX, WorldY, WorldZ), Geometry);
VF_ApplyPassageLandingCarving(
Density, RoomSDF, BaseDensity, SealThickness);
// The floor is intentionally written after the room carve. It is also reasserted after the
// passage/disturbance posts by the callers below; doing it here makes the standalone primitive
// itself a complete landing-room operation.
if (WorldZ <= Geometry.FloorZ + KINDA_SMALL_NUMBER
&& WorldZ > Geometry.FloorZ - Geometry.FloorThickness
&& FMath::Abs(WorldX) <= FMath::Max(Geometry.HalfWidth - 1.0f, 0.0f)
&& FMath::Abs(WorldY) <= FMath::Max(Geometry.HalfWidth - 1.0f, 0.0f))
{
float Carve = FMath::Clamp((Blend - SDF) / (Blend * 2.0f), 0.0f, 1.0f);
Carve = SmoothStep01(Carve);
Density -= Carve * (BaseDensity * 2.0f + SealThickness);
Density = FMath::Max(Density, BaseDensity);
}
}
/** Reassert only the origin-room air volume after a post-process that may add rock. */
FORCEINLINE void VF_ApplyOriginLandingAir(float& Density,
float WorldX, float WorldY, float WorldZ,
float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius)
{
if (Radius <= 0.0f) return;
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
VoxelPassageGeometry::BuildOriginLandingGeometry(
StrateTopZ, StrateBottomZ, SealThickness, Radius);
if (!Geometry.bValid) return;
const float RoomSDF = VoxelPassageGeometry::OriginLandingRoomSDF(
FVector(WorldX, WorldY, WorldZ), Geometry);
VF_ApplyPassageLandingCarving(
Density, RoomSDF, BaseDensity, SealThickness);
}
/** Reassert only the origin-room support slab after an air carve/post-process. */
FORCEINLINE void VF_ApplyOriginLandingFloor(float& Density,
float WorldX, float WorldY, float WorldZ,
float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius)
{
if (Radius <= 0.0f) return;
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
VoxelPassageGeometry::BuildOriginLandingGeometry(
StrateTopZ, StrateBottomZ, SealThickness, Radius);
if (!Geometry.bValid) return;
if (WorldZ <= Geometry.FloorZ + KINDA_SMALL_NUMBER
&& WorldZ > Geometry.FloorZ - Geometry.FloorThickness
&& FMath::Abs(WorldX) <= FMath::Max(Geometry.HalfWidth - 1.0f, 0.0f)
&& FMath::Abs(WorldY) <= FMath::Max(Geometry.HalfWidth - 1.0f, 0.0f))
{
Density = FMath::Max(Density, BaseDensity);
}
}
/** MC-facing origin-room air backstop for the post-disturbance path. */
FORCEINLINE void VF_ApplyOriginLandingAirMC(float& Density,
float WorldX, float WorldY, float WorldZ,
float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius)
{
float InternalDensity = -Density;
VF_ApplyOriginLandingAir(InternalDensity, WorldX, WorldY, WorldZ,
StrateTopZ, StrateBottomZ, SealThickness, BaseDensity, Radius);
Density = -InternalDensity;
}
/** MC-facing origin-room floor backstop for the post-disturbance path. */
FORCEINLINE void VF_ApplyOriginLandingFloorMC(float& Density,
float WorldX, float WorldY, float WorldZ,
float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius)
{
float InternalDensity = -Density;
VF_ApplyOriginLandingFloor(InternalDensity, WorldX, WorldY, WorldZ,
StrateTopZ, StrateBottomZ, SealThickness, BaseDensity, Radius);
Density = -InternalDensity;
}
//=============================================================================
// XY EDGE SEAL / SCELLEMENT DE LA LIMITE XY
//=============================================================================
+3 -3
View File
@@ -77,9 +77,9 @@ public:
// Les formes créatives viennent ensuite des params de strate.
int32 Seed = 0;
// Radius (voxels) of the guaranteed open vertical landing column carved at world
// XY (0,0) in every strate — the (0,0) descent spine. Copied from VoxelSettings.
// 0 disables the spine carve.
// Authored radius (voxels) of the guaranteed flat-floored landing room at world XY (0,0) in
// every strate — the old “spine” setting now sizes a room and never creates a vertical bore.
// 0 disables the origin landing rooms. Copied from VoxelSettings.
float OriginSpineRadius = 14.0f;
// Radius (voxels) of the bounded world in actor-space XY, measured from (0,0).
@@ -0,0 +1,308 @@
// Shared dimensions and construction laws for walkable passages and origin landings.
//
// This header is deliberately free of UObjects. The density path, the operator stack, the
// passage generator, and the headless measurements all include the same arithmetic so a geometry
// change cannot leave one of those paths with a different room or slope contract.
#pragma once
#include "CoreMinimal.h"
namespace VoxelPassageGeometry
{
// Authored scale and player body used by the §6.5 landing calculation.
constexpr float VoxelSizeMeters = 0.25f;
constexpr float PlayerRadiusVoxels = 34.0f / 25.0f;
constexpr float PlayerHalfHeightVoxels = 88.0f / 25.0f;
constexpr float PlayerHeightVoxels = 2.0f * PlayerHalfHeightVoxels;
constexpr float HeadroomVoxels = 4.0f; // 1 m
constexpr float MinimumTurnFloorWidthVoxels = 12.0f; // 3 m
constexpr float MinimumLandingHalfWidthVoxels =
MinimumTurnFloorWidthVoxels * 0.5f;
constexpr float MinimumRoomHeightVoxels = 12.0f; // 3 m
constexpr float LandingFloorThicknessVoxels = 3.0f;
constexpr float SealSafetyMarginVoxels = 4.0f;
constexpr float RoomRoundingVoxels = 1.5f;
constexpr float LandingCarveBlendVoxels = 4.0f;
// Level tunnel apron outside each room-floor edge. It covers the room/tunnel SDF blend and
// keeps the ramp from starting while the route is still over the room's square support patch.
constexpr float WalkableTunnelLandingApronVoxels = 6.0f; // 1.5 m
// A level transition after a bend lets the route turn before its sloped segment begins.
constexpr float WalkableTunnelTurnTransitionVoxels = 6.0f; // 1.5 m
// Keep the tunnel-air post a half voxel above the authored support plane. This is a numerical
// ownership band: the floor writer owns the plane, while the air writer owns the capsule
// volume above it even when two overlapping passage projections differ by a fraction.
constexpr float WalkableTunnelFloorAirClearanceVoxels = 0.5f;
// A 15 degree ramp is comfortably below the 44 degree engine walkability ceiling. The
// latter is a scramble limit, not a sensible default for a tunnel the player walks through.
constexpr float WalkableTunnelMaxGradientDegrees = 15.0f;
constexpr float WalkableTunnelMaxGradient = 0.2679491924311227f; // tan(15 degrees)
// The network connector retains the §6.5 single-file dimensions: 2.5 m clear width and 3 m
// clear height. These values are in voxels because all passage descriptors are voxel-space.
constexpr float ConnectorRadiusVoxels = 5.0f;
constexpr float ConnectorHeightVoxels = 12.0f;
constexpr float RootOverlapVoxels = 2.25f;
// The walkability contract is about the floor carried by the tube, not only its centreline.
// A tapered tube changes that floor by the radius delta, so the construction and the audit
// use this same conservative floor delta.
FORCEINLINE float TunnelFloorZ(const FVector& Centreline, float Radius)
{
return Centreline.Z - FMath::Abs(Radius);
}
FORCEINLINE float TunnelFloorGradient(
const FVector& Start, float StartRadius,
const FVector& End, float EndRadius)
{
const float HorizontalRun = FVector2D(
End.X - Start.X, End.Y - Start.Y).Size();
if (HorizontalRun <= KINDA_SMALL_NUMBER)
{
return FMath::Abs(TunnelFloorZ(End, EndRadius)
- TunnelFloorZ(Start, StartRadius)) > KINDA_SMALL_NUMBER
? FLT_MAX : 0.0f;
}
return FMath::Abs(TunnelFloorZ(End, EndRadius)
- TunnelFloorZ(Start, StartRadius)) / HorizontalRun;
}
FORCEINLINE float RequiredHorizontalRunForFloorDrop(
float FirstFloorDrop, float SecondFloorDrop)
{
return 2.0f * FMath::Max(
FMath::Abs(FirstFloorDrop), FMath::Abs(SecondFloorDrop))
/ WalkableTunnelMaxGradient;
}
/** Project a query into the nearest horizontal control segment of a walkable tunnel. */
FORCEINLINE bool ProjectWalkableTunnelFloor(
const TArray<FVector>& ControlPoints,
const TArray<float>& ControlRadii,
const FVector& Position,
float& OutFloorZ,
float& OutSupportRadius,
int32* OutSegmentIndex = nullptr)
{
if (OutSegmentIndex != nullptr)
{
*OutSegmentIndex = INDEX_NONE;
}
if (ControlPoints.Num() < 2
|| ControlRadii.Num() != ControlPoints.Num())
{
return false;
}
const FVector2D QueryXY(Position.X, Position.Y);
float BestDistanceSquared = FLT_MAX;
bool bFoundSegment = false;
for (int32 SegmentIndex = 0;
SegmentIndex + 1 < ControlPoints.Num();
++SegmentIndex)
{
const FVector& A = ControlPoints[SegmentIndex];
const FVector& B = ControlPoints[SegmentIndex + 1];
const FVector2D AXY(A.X, A.Y);
const FVector2D Delta = FVector2D(B.X, B.Y) - AXY;
const float LengthSquared = Delta.SizeSquared();
if (LengthSquared <= KINDA_SMALL_NUMBER)
{
continue;
}
const float T = FMath::Clamp(
FVector2D::DotProduct(QueryXY - AXY, Delta) / LengthSquared,
0.0f, 1.0f);
const FVector2D ClosestXY = AXY + Delta * T;
const float DistanceSquared = (QueryXY - ClosestXY).SizeSquared();
if (DistanceSquared >= BestDistanceSquared)
{
continue;
}
const float StartRadius = FMath::Abs(ControlRadii[SegmentIndex]);
const float EndRadius = FMath::Abs(ControlRadii[SegmentIndex + 1]);
OutFloorZ = FMath::Lerp(
TunnelFloorZ(A, StartRadius),
TunnelFloorZ(B, EndRadius),
T);
OutSupportRadius = FMath::Max(
FMath::Min(StartRadius, EndRadius) - 0.5f,
PlayerRadiusVoxels);
if (OutSegmentIndex != nullptr)
{
*OutSegmentIndex = SegmentIndex;
}
BestDistanceSquared = DistanceSquared;
bFoundSegment = true;
}
return bFoundSegment
&& FMath::IsFinite(OutFloorZ)
&& FMath::IsFinite(OutSupportRadius)
&& OutSupportRadius > 0.0f
&& BestDistanceSquared <= FMath::Square(OutSupportRadius);
}
struct FOriginLandingGeometry
{
bool bValid = false;
float FloorZ = 0.0f;
float CeilingZ = 0.0f;
float HalfWidth = 0.0f;
float FloorThickness = LandingFloorThicknessVoxels;
};
FORCEINLINE float LandingHalfWidthForRadius(float Radius)
{
const float SafeRadius = FMath::Max(FMath::Abs(Radius), 1.0f);
return FMath::Max(MinimumLandingHalfWidthVoxels, SafeRadius + 2.0f);
}
FORCEINLINE float LandingHeightForRadius(float Radius)
{
const float SafeRadius = FMath::Max(FMath::Abs(Radius), 1.0f);
return FMath::Max(
FMath::Max(MinimumRoomHeightVoxels, PlayerHeightVoxels + HeadroomVoxels),
2.0f * SafeRadius + 2.0f);
}
/**
* Build the one origin landing in a strate.
*
* `OriginRadius` is the old spine setting and becomes the room's authored mouth radius. The
* room dimensions therefore reuse the §6.5 formula exactly:
* half-width = max(7, radius + 2),
* height = max(12, capsule height + 4, 2 * radius + 2).
* The floor is placed against the top of the safe interior interval. This makes the first
* landing the place reached from above, while every lower landing remains sealed above until
* progression opens its inter-strate passage.
*/
FORCEINLINE FOriginLandingGeometry BuildOriginLandingGeometry(
float StrateTopZ, float StrateBottomZ, float SealThickness, float OriginRadius)
{
FOriginLandingGeometry Result;
if (!(OriginRadius > 0.0f)
|| !FMath::IsFinite(StrateTopZ)
|| !FMath::IsFinite(StrateBottomZ)
|| !FMath::IsFinite(SealThickness)
|| !FMath::IsFinite(OriginRadius))
{
return Result;
}
Result.HalfWidth = LandingHalfWidthForRadius(OriginRadius);
const float DesiredHeight = LandingHeightForRadius(OriginRadius);
const float SafeSeal = FMath::Max(SealThickness, 0.0f);
const float InnerBottom = StrateBottomZ + SafeSeal;
const float InnerTop = StrateTopZ - SafeSeal;
const float MinimumFloor = InnerBottom
+ Result.FloorThickness + SealSafetyMarginVoxels;
const float MaximumFloor = InnerTop
- DesiredHeight - SealSafetyMarginVoxels;
if (!FMath::IsFinite(InnerBottom) || !FMath::IsFinite(InnerTop)
|| !(InnerTop > InnerBottom)
|| !FMath::IsFinite(MinimumFloor)
|| !FMath::IsFinite(MaximumFloor)
|| MaximumFloor < MinimumFloor)
{
Result.HalfWidth = 0.0f;
return Result;
}
Result.FloorZ = MaximumFloor;
Result.CeilingZ = Result.FloorZ + DesiredHeight;
Result.bValid = true;
return Result;
}
FORCEINLINE bool IntersectsClosedInterval(float MinA, float MaxA,
float MinB, float MaxB)
{
return MaxA >= MinB && MinA <= MaxB;
}
/** Conservative influence box for the origin room's air carve. */
FORCEINLINE bool OriginLandingRoomTouchesBox(
const FBox& VoxelBox,
float StrateTopZ, float StrateBottomZ, float SealThickness, float OriginRadius)
{
if (!VoxelBox.IsValid) return false;
const FOriginLandingGeometry Geometry = BuildOriginLandingGeometry(
StrateTopZ, StrateBottomZ, SealThickness, OriginRadius);
if (!Geometry.bValid) return false;
const float Pad = LandingCarveBlendVoxels;
return IntersectsClosedInterval(
VoxelBox.Min.X, VoxelBox.Max.X,
-Geometry.HalfWidth - Pad, Geometry.HalfWidth + Pad)
&& IntersectsClosedInterval(
VoxelBox.Min.Y, VoxelBox.Max.Y,
-Geometry.HalfWidth - Pad, Geometry.HalfWidth + Pad)
&& IntersectsClosedInterval(
VoxelBox.Min.Z, VoxelBox.Max.Z,
Geometry.FloorZ - Pad, Geometry.CeilingZ + Pad);
}
/** Conservative influence box for the origin room's guaranteed solid floor. */
FORCEINLINE bool OriginLandingFloorTouchesBox(
const FBox& VoxelBox,
float StrateTopZ, float StrateBottomZ, float SealThickness, float OriginRadius)
{
if (!VoxelBox.IsValid) return false;
const FOriginLandingGeometry Geometry = BuildOriginLandingGeometry(
StrateTopZ, StrateBottomZ, SealThickness, OriginRadius);
if (!Geometry.bValid) return false;
// One voxel is a proof margin, not a write margin. It keeps ClassifyTile conservative at
// an exact lattice edge and mirrors the existing passage-floor guard.
constexpr float ProofPad = 1.0f;
const float FloorHalfWidth = FMath::Max(
Geometry.HalfWidth - 1.0f, 0.0f);
return IntersectsClosedInterval(
VoxelBox.Min.X, VoxelBox.Max.X,
-FloorHalfWidth - ProofPad, FloorHalfWidth + ProofPad)
&& IntersectsClosedInterval(
VoxelBox.Min.Y, VoxelBox.Max.Y,
-FloorHalfWidth - ProofPad, FloorHalfWidth + ProofPad)
&& IntersectsClosedInterval(
VoxelBox.Min.Z, VoxelBox.Max.Z,
Geometry.FloorZ - Geometry.FloorThickness - ProofPad,
Geometry.FloorZ + ProofPad);
}
/** Signed distance for a rounded origin room with a hard flat floor. */
FORCEINLINE float OriginLandingRoomSDF(
const FVector& Position, const FOriginLandingGeometry& Geometry)
{
if (!Geometry.bValid || Geometry.CeilingZ <= Geometry.FloorZ)
{
return FLT_MAX;
}
const FVector Center(0.0f, 0.0f,
(Geometry.FloorZ + Geometry.CeilingZ) * 0.5f);
const float Rounding = RoomRoundingVoxels;
const float HalfZ = (Geometry.CeilingZ - Geometry.FloorZ) * 0.5f;
const FVector HalfExtent(
FMath::Max(Geometry.HalfWidth - Rounding, 0.25f),
FMath::Max(Geometry.HalfWidth - Rounding, 0.25f),
FMath::Max(HalfZ - Rounding, 0.25f));
const FVector D(
FMath::Abs(Position.X - Center.X) - HalfExtent.X,
FMath::Abs(Position.Y - Center.Y) - HalfExtent.Y,
FMath::Abs(Position.Z - Center.Z) - HalfExtent.Z);
const FVector Outside(
FMath::Max(D.X, 0.0f),
FMath::Max(D.Y, 0.0f),
FMath::Max(D.Z, 0.0f));
const float Inside = FMath::Min(
FMath::Max(D.X, FMath::Max(D.Y, D.Z)), 0.0f);
const float RoundedBoxSDF = Outside.Size() + Inside - Rounding;
// Below the floor is rock. Above it the rounded-box air volume is authoritative.
return FMath::Max(RoundedBoxSDF, Geometry.FloorZ - Position.Z);
}
}
+5 -4
View File
@@ -392,10 +392,11 @@ public:
// SPINE & INTER-STRATE CONNECTIONS (the (0,0) descent axis)
//=========================================================================
// Radius (voxels) of the guaranteed open vertical "landing" column carved at
// world XY (0,0) in EVERY strate, regardless of archetype. This is the prepared
// space the player digs THROUGH the seal into when descending. 0 = disabled.
// 10-14 → cozy shaft (default) · 20+ → wide landing chamber
// Authored radius (voxels) of the guaranteed (0,0) landing room in EVERY strate, regardless
// of archetype. The §6.5 body-sized formulas turn it into a flat-floored room; this setting
// no longer bores a vertical column. 0 = disabled. The room is top-anchored inside each
// strate, and the seals remain solid until a progression passage is opened.
// 10-14 → compact landing room (default) · 20+ → broad landing room
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Spine", meta = (ClampMin = "0.0"))
float OriginSpineRadius = 14.0f;
@@ -211,9 +211,11 @@ public:
//=========================================================================
// INTER-STRATE PASSAGES (how THIS strate connects DOWN to the next)
//=========================================================================
// Auto-carved descent tunnels from this strate to the one below it: count, style
// (straight / worm / spiral / cascade), tapered width, length, placement. The (0,0)
// spine descent is separate (player-dug); these are the extra shortcuts.
// Auto-carved passages from this strate to the one below it: count, style
// (walkable sloped tunnel / worm / spiral / cascade), tapered width, length, placement.
// Every strate has its own finite (0,0) landing room; the passage opens the next room through
// progression. The non-default styles are retained as exotic, non-walkability-guaranteed
// descriptors.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Passages")
FStratePassageConfig PassageConfig;
+34 -9
View File
@@ -66,18 +66,17 @@ struct FVoxelPassage
FVector RequestedLowerPoint = FVector::ZeroVector;
// Passage dimensions — how wide the carved tunnel is (in voxels).
// Varies by type: VerticalShaft ~7-8, SpiralDescent ~4, CrackCrevice ~2-3, others ~5.
// The default SlopedTunnel is a single-file 2.5 m clear bore at its narrowest point;
// authored exotic styles retain their own widths and are not covered by the walkable law.
float Radius = 5.0f;
// The shape/style of this passage. Determines how control points are generated
// and how the passage feels to navigate (shaft, spiral, ledges, crack, etc.).
EVoxelPassageType PassageType = EVoxelPassageType::SlopedTunnel;
// Multi-segment control points for complex passage shapes.
// Used by SpiralDescent (helix points) and CascadingDrops (ledge+drop points).
// When non-empty, the SDF evaluator walks this array as a capsule chain
// instead of using Upper→Mid→Lower logic.
// Empty for simple types (SlopedTunnel, VerticalShaft, CrackCrevice).
// Multi-segment control points for passage shapes. The default SlopedTunnel contains the two
// gentle ramp legs of its deterministic switchback; exotic styles retain their authored chain.
// When non-empty, the SDF evaluator walks this array as a capsule chain.
TArray<FVector> ControlPoints;
// Per-control-point tube radius (parallel to ControlPoints) for tapered tunnels —
@@ -90,6 +89,16 @@ struct FVoxelPassage
FVoxelPassageLanding UpperLanding;
FVoxelPassageLanding LowerLanding;
// Construction-time walkability facts for diagnostics and the commandlet report. These are
// metadata, not a second runtime decision: the final-density sampler remains authoritative.
bool bWalkableTunnelContract = false;
float TunnelMaxGradientDegrees = 0.0f;
float TunnelVerticalDropVoxels = 0.0f;
float TunnelRequiredHorizontalRunVoxels = 0.0f;
float TunnelHorizontalPathLengthVoxels = 0.0f;
float TunnelMinimumClearWidthVoxels = 0.0f;
float TunnelClearHeightVoxels = 0.0f;
// Bounding sphere enclosing the whole passage (+ radius + blend), in voxel coords.
// Computed once in GeneratePassages; lets EvaluateModifierSDF reject far voxels with
// a single squared-distance test instead of walking every segment per voxel.
@@ -362,14 +371,26 @@ public:
void ApplyPassageLandingAir(float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const;
/** Reassert the default walkable tunnel's clear air in internal density after any post. */
void ApplyPassageTunnelAir(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 walkable tunnel air after MC-space disturbances, still before the final XY seal. */
void ApplyPassageTunnelAirMC(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;
/** Re-assert only the flat room floors after the tunnel-air post; never writes tunnel floors. */
void ApplyPassageLandingRoomFloorMC(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
* boîte VOXEL [MinVoxel, MaxVoxel]. Test conservatif O(Passages) — utilisé par ClassifyTile
@@ -380,6 +401,10 @@ public:
/** 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;
/** Conservative box guards for the per-strate (0,0) landing rooms and their floor slabs. */
bool AnyOriginLandingNearBox(const FVector& MinVoxel, const FVector& MaxVoxel) const;
bool AnyOriginLandingFloorNearBox(const FVector& MinVoxel, const FVector& MaxVoxel) const;
/** Get all generated passages (for debug display). */
const TArray<FVoxelPassage>& GetPassages() const { return Passages; }
@@ -411,12 +436,12 @@ protected:
// World seed (stored for passage generation)
int32 CachedSeed = 0;
// Whether to auto-open the (0,0) entry shaft through the top of strate 0.
// Whether to auto-open the (0,0) origin landing room from above in strate 0.
// Copied from VoxelSettings::bOpenSurfaceEntry during Initialize().
bool bOpenSurfaceEntry = true;
// Radius (voxels) of the surface entry shaft at (0,0). Copied from
// VoxelSettings::OriginSpineRadius so the entry matches the spine landing.
// Authored radius (voxels) of the origin landing room and its optional surface opening.
// Copied from VoxelSettings::OriginSpineRadius.
float OriginSpineRadius = 14.0f;
// Solid-bedrock gap between consecutive strates, in chunks. Copied from
+15 -6
View File
@@ -3,6 +3,7 @@
#pragma once
#include "CoreMinimal.h"
#include "VoxelPassageGeometry.h"
class UVoxelGenerator;
class UVoxelStrateManager;
@@ -19,7 +20,7 @@ class UVoxelStrateManager;
struct VOXELFORGE_API FVoxelPlayerCapsuleConstants
{
static constexpr float VoxelSizeCentimeters = 25.0f;
static constexpr float VoxelSizeMeters = VoxelSizeCentimeters / 100.0f;
static constexpr float VoxelSizeMeters = VoxelPassageGeometry::VoxelSizeMeters;
static constexpr float RadiusCentimeters = 34.0f;
static constexpr float HalfHeightCentimeters = 88.0f;
// UCharacterMovementComponent's UE 5.7 default. Callers that have a character CDO should
@@ -29,9 +30,9 @@ struct VOXELFORGE_API FVoxelPlayerCapsuleConstants
// UE 5.7's WalkableFloorZ=0.71 resolves to about 44.8 degrees; a character CDO may expose
// the precise angle. The diagnosis test copies that precise project value when available.
static constexpr float WalkableFloorAngleDegrees = 44.8f;
static constexpr float RadiusVoxels = RadiusCentimeters / VoxelSizeCentimeters;
static constexpr float HalfHeightVoxels = HalfHeightCentimeters / VoxelSizeCentimeters;
static constexpr float HeightVoxels = 2.0f * HalfHeightVoxels;
static constexpr float RadiusVoxels = VoxelPassageGeometry::PlayerRadiusVoxels;
static constexpr float HalfHeightVoxels = VoxelPassageGeometry::PlayerHalfHeightVoxels;
static constexpr float HeightVoxels = VoxelPassageGeometry::PlayerHeightVoxels;
};
/** Read-only density source used by the offline Tier 2 pass for a materialised custom stack. */
@@ -68,7 +69,8 @@ struct VOXELFORGE_API FVoxelStrateMeasureSettings
TOptional<FVector2D> CoverPointA;
TOptional<FVector2D> CoverPointB;
float CoverMarginVoxels = 48.0f;
// Origin-rooted topologies can route from one mouth to the other through the (0,0) spine.
// Origin-rooted topologies can route from one mouth to the other through the finite (0,0)
// landing room and its explicitly carved connector network.
// When true, include (0,0) in the fitted AABB before applying MaxCells. A caller must keep
// this enabled for that topology; an over-cap route window is then refused honestly instead
// of being cropped back to a mouth-only box.
@@ -322,6 +324,12 @@ struct VOXELFORGE_API FVoxelPlayerFitWalkReport
int64 DepartureComponentCells = 0;
float MouthComponentGapVoxels = -1.0f;
// Optional room probes evaluated against this same grid and start component. The arrays are
// empty for ordinary callers; when supplied, each entry is 1 only when the probe is both a
// player-fit cell and in the arrival/start component.
TArray<uint8> ComponentProbePlayerFit;
TArray<uint8> ComponentProbeInStartComponent;
// The DFS returns to its root when the target component is unreachable. Keep both the
// physical end of that walk and the last newly reached pose: the latter is the useful
// failure-frontier point for a render, while the former is the literal final pose.
@@ -496,4 +504,5 @@ VOXELFORGE_API bool VF_MeasurePlayerFitWalkWithSampler(
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
FVoxelPlayerFitWalkReport& OutReport);
FVoxelPlayerFitWalkReport& OutReport,
const TArray<FVector>* ComponentProbePoints = nullptr);
+21 -17
View File
@@ -32,19 +32,18 @@ class AActor; // IWYU : paramètre de TSubclassOf seulement / T
* of the carved tunnel (control point layout, radius, overall feel).
*
* Types:
* SlopedTunnel — Angled bore with a horizontal midpoint offset (default/legacy).
* VerticalShaft — Straight or near-straight drop between strates.
* SpiralDescent — Helical corkscrew path winding downward.
* CascadingDrops — Series of short vertical pits connected by horizontal ledges.
* CrackCrevice — Narrow fracture passage, tight squeeze through rock.
* SlopedTunnel — The default walkable, gentle-gradient tunnel between landing rooms.
* VerticalShaft — A vertical drop; never a continuous walkable descent.
* SpiralDescent — An exotic helix; walkability requires a separately designed ramp contract.
* CascadingDrops — Drops and ledges; never a continuous walkable descent as authored.
* CrackCrevice — A narrow fracture; no player-fit guarantee.
*/
UENUM(BlueprintType)
enum class EVoxelPassageType : uint8
{
// Angled bore connecting two layers with a horizontal midpoint offset.
// Two capsule segments: Upper→Mid, Mid→Lower. Comfortable radius (~5 voxels).
// This is the original passage type — feels like a natural sloped tunnel.
SlopedTunnel UMETA(DisplayName = "Sloped Tunnel (angled bore)"),
// Walkable single-file tunnel. The runtime builds a deterministic switchback whose each ramp
// is below VoxelPassageGeometry::WalkableTunnelMaxGradientDegrees.
SlopedTunnel UMETA(DisplayName = "Sloped Tunnel (walkable default)"),
// Straight vertical drop between strates. Same XY for upper and lower points.
// Wider radius (~7-8 voxels) to feel like a natural shaft or sinkhole.
@@ -1758,7 +1757,9 @@ struct VOXELFORGE_API FStrateDisturbanceParams
UENUM(BlueprintType)
enum class EVoxelPassageStyle : uint8
{
Straight UMETA(DisplayName = "Straight (vertical shaft)"),
// “Straight” is retained as the authored style name for the base connection, but its runtime
// geometry is a gentle two-leg switchback rather than a vertical shaft.
Straight UMETA(DisplayName = "Straight (walkable sloped tunnel)"),
Worm UMETA(DisplayName = "Worm (organic meander around the axis)"),
Spiral UMETA(DisplayName = "Spiral (corkscrew descent)"),
Cascading UMETA(DisplayName = "Cascading (ledge + drop staircase)")
@@ -1768,8 +1769,8 @@ enum class EVoxelPassageStyle : uint8
* FStratePassageConfig — how THIS strate connects DOWN to the strate below it.
*
* Lives on each UVoxelStrateDefinition: the upper strate of every boundary controls its
* own descent tunnels, so different layers connect differently. The (0,0) spine descent
* is separate (player-dug); these are the auto-carved shortcuts placed away from it.
* own progression tunnel, so different layers connect differently. Each strate has a finite
* (0,0) landing room; this config controls the passage that opens the next room.
*/
USTRUCT(BlueprintType)
struct VOXELFORGE_API FStratePassageConfig
@@ -1782,17 +1783,19 @@ struct VOXELFORGE_API FStratePassageConfig
// Tunnel shape.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage")
EVoxelPassageStyle Style = EVoxelPassageStyle::Worm;
EVoxelPassageStyle Style = EVoxelPassageStyle::Straight;
// ----- WIDTH (tapers along the length) -----
// Radius at the two mouths (entry/exit) and at the middle. Equal = uniform tube;
// The defaults are 2 m radius / 4 m bore at mouths and 1.5 m radius / 3 m bore in the middle.
// The walkable default is deliberately single-file: 3.0 m diameter at mouths and 2.5 m at
// the waist (6/5 voxel radii at 25 cm per voxel). The §6.5 landing room remains wider for
// turning.
// Mouth > Mid = chambers at the ends with a squeeze between; Mid > Mouth = a bulge.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Width", meta = (ClampMin = "1.0"))
float MouthRadius = 8.0f;
float MouthRadius = 6.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Width", meta = (ClampMin = "1.0"))
float MidRadius = 6.0f;
float MidRadius = 5.0f;
// ----- LENGTH -----
// How far the tunnel reaches INTO each strate (voxels). Auto-capped to the interior;
@@ -1813,7 +1816,8 @@ struct VOXELFORGE_API FStratePassageConfig
float DistanceMax = 192.0f;
// ----- SHAPE detail -----
// Worm: max sideways excursion from the axis (voxels). 24 = 6 m; 0 = straight even in Worm style.
// Exotic styles only: max sideways excursion from the axis (voxels). 24 = 6 m; 0 = straight
// even in Worm style. The default walkable switchback does not use this wander value.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Shape", meta = (ClampMin = "0.0"))
float Wander = 24.0f;
@@ -384,7 +384,10 @@ struct FExploreWorld
Index));
Definition->StrateDescription = FText::FromString(
TEXT("Transient, deterministic commandlet definition."));
Definition->StrateHeightInChunks = 8;
// The commandlet's acceptance sweep uses the owner-facing base spacing:
// 4 chunks * 32 voxels * 0.25 m = 32 m per strate. Production assets may be taller;
// this keeps the measured tunnel arithmetic tied to the stated ~32 m descent.
Definition->StrateHeightInChunks = 4;
Definition->TransitionType = EVoxelStrateTransition::Hard;
Definition->GeneratorType = Index == Arguments.Slot
? Arguments.Archetype
@@ -510,6 +513,12 @@ struct FExploreWalkOutput
bool bOriginCheckAvailable = false;
FString OriginCheckRefusalReason;
FVoxelPlayerFitWalkReport OriginCheckReport;
int32 ArrivalLandingRoomProbeCount = 0;
int32 ArrivalLandingRoomPlayerFitProbeCount = 0;
int32 ArrivalLandingRoomArrivalComponentProbeCount = 0;
int32 DepartureLandingRoomProbeCount = 0;
int32 DepartureLandingRoomPlayerFitProbeCount = 0;
int32 DepartureLandingRoomArrivalComponentProbeCount = 0;
};
struct FExploreExportOutput
@@ -542,12 +551,16 @@ bool FindMouths(
FVector& OutArrival,
FVector& OutDeparture,
int32& OutArrivalCount,
int32& OutDepartureCount)
int32& OutDepartureCount,
const FVoxelPassage*& OutArrivalPassage,
const FVoxelPassage*& OutDeparturePassage)
{
OutArrival = FVector::ZeroVector;
OutDeparture = FVector::ZeroVector;
OutArrivalCount = 0;
OutDepartureCount = 0;
OutArrivalPassage = nullptr;
OutDeparturePassage = nullptr;
for (const FVoxelPassage& Passage : Manager.GetPassages())
{
if (Passage.LowerStrateIndex == TargetSlot
@@ -556,6 +569,7 @@ bool FindMouths(
if (OutArrivalCount == 0)
{
OutArrival = Passage.LowerPoint;
OutArrivalPassage = &Passage;
}
++OutArrivalCount;
}
@@ -565,6 +579,7 @@ bool FindMouths(
if (OutDepartureCount == 0)
{
OutDeparture = Passage.UpperPoint;
OutDeparturePassage = &Passage;
}
++OutDepartureCount;
}
@@ -1273,13 +1288,17 @@ bool RunWalk(
return false;
}
const FVoxelPassage* ArrivalPassage = nullptr;
const FVoxelPassage* DeparturePassage = nullptr;
if (!FindMouths(
*World.Manager,
Arguments.Slot,
OutOutput.ArrivalVoxels,
OutOutput.DepartureVoxels,
OutOutput.ArrivalPassageCount,
OutOutput.DeparturePassageCount))
OutOutput.DeparturePassageCount,
ArrivalPassage,
DeparturePassage))
{
OutOutput.Status = TEXT("refused");
OutOutput.RefusalReason = TEXT(
@@ -1299,6 +1318,32 @@ bool RunWalk(
Settings.PlayerCapsuleRadiusVoxels) + 2.0f;
Settings.bIncludeOriginInCoverWindow = NeedsOriginInWindow(Arguments.Archetype);
TArray<FVector> LandingRoomProbePoints;
auto AppendLandingRoomProbes = [&LandingRoomProbePoints](
const FVoxelPassageLanding& Landing)
{
const float ProbeOffset = FMath::Min(
2.0f,
FMath::Max(0.0f, Landing.HalfWidth - 2.0f));
LandingRoomProbePoints.Add(Landing.StandingPoint);
LandingRoomProbePoints.Add(
Landing.StandingPoint + FVector(ProbeOffset, 0.0f, 0.0f));
LandingRoomProbePoints.Add(
Landing.StandingPoint + FVector(-ProbeOffset, 0.0f, 0.0f));
LandingRoomProbePoints.Add(
Landing.StandingPoint + FVector(0.0f, ProbeOffset, 0.0f));
LandingRoomProbePoints.Add(
Landing.StandingPoint + FVector(0.0f, -ProbeOffset, 0.0f));
};
if (ArrivalPassage != nullptr)
{
AppendLandingRoomProbes(ArrivalPassage->LowerLanding);
}
if (DeparturePassage != nullptr)
{
AppendLandingRoomProbes(DeparturePassage->UpperLanding);
}
const FGeneratorDensitySampler Sampler(*World.Generator);
if (!VF_MeasurePlayerFitWalkWithSampler(
Sampler,
@@ -1308,7 +1353,8 @@ bool RunWalk(
OutOutput.ArrivalVoxels,
OutOutput.DepartureVoxels,
Settings,
OutOutput.Report))
OutOutput.Report,
&LandingRoomProbePoints))
{
OutOutput.Status = TEXT("refused");
OutOutput.RefusalReason = OutOutput.Report.RefusalReason;
@@ -1338,6 +1384,30 @@ bool RunWalk(
{
OutOutput.OriginCheckRefusalReason = OutOutput.OriginCheckReport.RefusalReason;
}
const int32 ArrivalProbeCount = ArrivalPassage != nullptr ? 5 : 0;
const int32 DepartureProbeCount = DeparturePassage != nullptr ? 5 : 0;
OutOutput.ArrivalLandingRoomProbeCount = ArrivalProbeCount;
OutOutput.DepartureLandingRoomProbeCount = DepartureProbeCount;
for (int32 ProbeIndex = 0; ProbeIndex < OutOutput.Report.ComponentProbePlayerFit.Num();
++ProbeIndex)
{
const bool bPlayerFit = OutOutput.Report.ComponentProbePlayerFit[ProbeIndex] != 0u;
const bool bInArrivalComponent =
OutOutput.Report.ComponentProbeInStartComponent.IsValidIndex(ProbeIndex)
&& OutOutput.Report.ComponentProbeInStartComponent[ProbeIndex] != 0u;
if (ProbeIndex < ArrivalProbeCount)
{
OutOutput.ArrivalLandingRoomPlayerFitProbeCount += bPlayerFit ? 1 : 0;
OutOutput.ArrivalLandingRoomArrivalComponentProbeCount +=
bInArrivalComponent ? 1 : 0;
}
else
{
OutOutput.DepartureLandingRoomPlayerFitProbeCount += bPlayerFit ? 1 : 0;
OutOutput.DepartureLandingRoomArrivalComponentProbeCount +=
bInArrivalComponent ? 1 : 0;
}
}
OutOutput.Status = TEXT("ok");
return true;
@@ -1509,7 +1579,7 @@ FString BuildManifestJson(
TSharedRef<FExploreJsonWriter> Writer =
TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&Json);
Writer->WriteObjectStart();
Writer->WriteValue(TEXT("schema_version"), 1);
Writer->WriteValue(TEXT("schema_version"), 2);
Writer->WriteValue(TEXT("format"), TEXT("OBJ"));
Writer->WriteValue(TEXT("mesh_file"), Export.MeshFileName);
Writer->WriteValue(TEXT("mesh_positions_units"), TEXT("metres"));
@@ -1703,7 +1773,7 @@ FString BuildExploreJson(
TSharedRef<FExploreJsonWriter> Writer =
TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&Json);
Writer->WriteObjectStart();
Writer->WriteValue(TEXT("schema_version"), 1);
Writer->WriteValue(TEXT("schema_version"), 2);
Writer->WriteValue(TEXT("tool"), TEXT("VoxelForgeExplore"));
Writer->WriteValue(TEXT("read_only_generation"), true);
@@ -1780,6 +1850,11 @@ FString BuildExploreJson(
if (Arguments.bWalk)
{
const FVoxelPlayerFitWalkReport& Report = Output.Walk.Report;
// The primary report is the mouth-sized arrival/departure window. Origin reachability is
// a separate, explicitly origin-inclusive measurement; expose that report at the legacy
// top-level keys so a sweep cannot accidentally count a window that omitted (0,0).
const FVoxelPlayerFitWalkReport& OriginReport = Output.Walk.bOriginCheckAvailable
? Output.Walk.OriginCheckReport : Report;
Writer->WriteObjectStart(TEXT("walk"));
Writer->WriteValue(TEXT("status"), Output.Walk.Status);
Writer->WriteValue(TEXT("refusal_or_error"), Output.Walk.RefusalReason);
@@ -1795,6 +1870,27 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("working_memory_cap_bytes"), MaxWalkWorkingBytes);
Writer->WriteValue(TEXT("arrival_passage_count"), Output.Walk.ArrivalPassageCount);
Writer->WriteValue(TEXT("departure_passage_count"), Output.Walk.DeparturePassageCount);
Writer->WriteValue(TEXT("landing_room_probe_offsets_voxels"), 2.0);
Writer->WriteValue(TEXT("arrival_landing_room_probe_count"),
Output.Walk.ArrivalLandingRoomProbeCount);
Writer->WriteValue(TEXT("arrival_landing_room_player_fit_probe_count"),
Output.Walk.ArrivalLandingRoomPlayerFitProbeCount);
Writer->WriteValue(TEXT("arrival_landing_room_arrival_component_probe_count"),
Output.Walk.ArrivalLandingRoomArrivalComponentProbeCount);
Writer->WriteValue(TEXT("arrival_landing_room_in_arrival_component"),
Output.Walk.ArrivalLandingRoomProbeCount > 0
&& Output.Walk.ArrivalLandingRoomArrivalComponentProbeCount
== Output.Walk.ArrivalLandingRoomProbeCount);
Writer->WriteValue(TEXT("departure_landing_room_probe_count"),
Output.Walk.DepartureLandingRoomProbeCount);
Writer->WriteValue(TEXT("departure_landing_room_player_fit_probe_count"),
Output.Walk.DepartureLandingRoomPlayerFitProbeCount);
Writer->WriteValue(TEXT("departure_landing_room_arrival_component_probe_count"),
Output.Walk.DepartureLandingRoomArrivalComponentProbeCount);
Writer->WriteValue(TEXT("departure_landing_room_in_arrival_component"),
Output.Walk.DepartureLandingRoomProbeCount > 0
&& Output.Walk.DepartureLandingRoomArrivalComponentProbeCount
== Output.Walk.DepartureLandingRoomProbeCount);
if (Output.Walk.bHasArrival)
{
WriteJsonVector(*Writer, TEXT("arrival_mouth_voxels"), Output.Walk.ArrivalVoxels, 1.0f);
@@ -1870,14 +1966,16 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("agent_to_target_component_gap_m"),
static_cast<double>(Report.AgentToTargetComponentGapVoxels)
* FVoxelPlayerCapsuleConstants::VoxelSizeMeters);
Writer->WriteValue(TEXT("origin_column_in_sampled_window"), Report.bOriginColumnInSampledWindow);
Writer->WriteValue(TEXT("origin_column_has_player_fit"), Report.bOriginColumnHasPlayerFit);
Writer->WriteValue(TEXT("origin_column_player_fit_cells"), Report.OriginColumnPlayerFitCells);
Writer->WriteValue(TEXT("origin_column_reachable_from_arrival"), Report.bOriginColumnReachable);
Writer->WriteValue(TEXT("origin_column_measurement_window"),
TEXT("origin-inclusive mouth-AABB + margin"));
Writer->WriteValue(TEXT("origin_column_in_sampled_window"), OriginReport.bOriginColumnInSampledWindow);
Writer->WriteValue(TEXT("origin_column_has_player_fit"), OriginReport.bOriginColumnHasPlayerFit);
Writer->WriteValue(TEXT("origin_column_player_fit_cells"), OriginReport.OriginColumnPlayerFitCells);
Writer->WriteValue(TEXT("origin_column_reachable_from_arrival"), OriginReport.bOriginColumnReachable);
Writer->WriteValue(TEXT("reachable_set_to_origin_column_voxels"),
static_cast<double>(Report.ReachableSetToOriginColumnVoxels));
static_cast<double>(OriginReport.ReachableSetToOriginColumnVoxels));
Writer->WriteValue(TEXT("reachable_set_to_origin_column_m"),
static_cast<double>(Report.ReachableSetToOriginColumnVoxels)
static_cast<double>(OriginReport.ReachableSetToOriginColumnVoxels)
* FVoxelPlayerCapsuleConstants::VoxelSizeMeters);
Writer->WriteObjectStart(TEXT("origin_check"));
Writer->WriteValue(TEXT("attempted"), Output.Walk.bOriginCheckAttempted);
@@ -1885,40 +1983,40 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("refusal_or_error"), Output.Walk.OriginCheckRefusalReason);
if (Output.Walk.bOriginCheckAvailable)
{
const FVoxelPlayerFitWalkReport& OriginReport = Output.Walk.OriginCheckReport;
const FVoxelPlayerFitWalkReport& OriginCheckReport = Output.Walk.OriginCheckReport;
Writer->WriteValue(TEXT("window_policy"), TEXT("origin-inclusive mouth-AABB + margin"));
Writer->WriteValue(TEXT("connectivity_result"), ConnectivityResultName(OriginReport.Result));
Writer->WriteValue(TEXT("can_reach_departure"), OriginReport.bCanReachDeparture);
Writer->WriteValue(TEXT("sampled_num_x"), OriginReport.SampledNumX);
Writer->WriteValue(TEXT("sampled_num_y"), OriginReport.SampledNumY);
Writer->WriteValue(TEXT("sampled_num_z"), OriginReport.SampledNumZ);
Writer->WriteValue(TEXT("sampled_min_x"), static_cast<double>(OriginReport.SampledMinX));
Writer->WriteValue(TEXT("sampled_max_x_exclusive"), static_cast<double>(OriginReport.SampledMaxX));
Writer->WriteValue(TEXT("sampled_min_y"), static_cast<double>(OriginReport.SampledMinY));
Writer->WriteValue(TEXT("sampled_max_y_exclusive"), static_cast<double>(OriginReport.SampledMaxY));
Writer->WriteValue(TEXT("sampled_min_z"), OriginReport.SampledMinZ);
Writer->WriteValue(TEXT("sampled_max_z_exclusive"), OriginReport.SampledMaxZ);
Writer->WriteValue(TEXT("player_fit_cells"), OriginReport.PlayerFitVolumeCells);
Writer->WriteValue(TEXT("reachable_player_fit_cells"), OriginReport.ReachablePlayerFitCells);
Writer->WriteValue(TEXT("connectivity_result"), ConnectivityResultName(OriginCheckReport.Result));
Writer->WriteValue(TEXT("can_reach_departure"), OriginCheckReport.bCanReachDeparture);
Writer->WriteValue(TEXT("sampled_num_x"), OriginCheckReport.SampledNumX);
Writer->WriteValue(TEXT("sampled_num_y"), OriginCheckReport.SampledNumY);
Writer->WriteValue(TEXT("sampled_num_z"), OriginCheckReport.SampledNumZ);
Writer->WriteValue(TEXT("sampled_min_x"), static_cast<double>(OriginCheckReport.SampledMinX));
Writer->WriteValue(TEXT("sampled_max_x_exclusive"), static_cast<double>(OriginCheckReport.SampledMaxX));
Writer->WriteValue(TEXT("sampled_min_y"), static_cast<double>(OriginCheckReport.SampledMinY));
Writer->WriteValue(TEXT("sampled_max_y_exclusive"), static_cast<double>(OriginCheckReport.SampledMaxY));
Writer->WriteValue(TEXT("sampled_min_z"), OriginCheckReport.SampledMinZ);
Writer->WriteValue(TEXT("sampled_max_z_exclusive"), OriginCheckReport.SampledMaxZ);
Writer->WriteValue(TEXT("player_fit_cells"), OriginCheckReport.PlayerFitVolumeCells);
Writer->WriteValue(TEXT("reachable_player_fit_cells"), OriginCheckReport.ReachablePlayerFitCells);
Writer->WriteValue(TEXT("reachable_player_fit_fraction"),
static_cast<double>(OriginReport.ReachablePlayerFitFraction));
Writer->WriteValue(TEXT("player_fit_components"), OriginReport.PlayerFitComponents);
Writer->WriteValue(TEXT("arrival_component_cells"), OriginReport.ArrivalComponentCells);
Writer->WriteValue(TEXT("departure_component_cells"), OriginReport.DepartureComponentCells);
static_cast<double>(OriginCheckReport.ReachablePlayerFitFraction));
Writer->WriteValue(TEXT("player_fit_components"), OriginCheckReport.PlayerFitComponents);
Writer->WriteValue(TEXT("arrival_component_cells"), OriginCheckReport.ArrivalComponentCells);
Writer->WriteValue(TEXT("departure_component_cells"), OriginCheckReport.DepartureComponentCells);
Writer->WriteValue(TEXT("mouth_component_gap_voxels"),
static_cast<double>(OriginReport.MouthComponentGapVoxels));
static_cast<double>(OriginCheckReport.MouthComponentGapVoxels));
Writer->WriteValue(TEXT("origin_column_in_sampled_window"),
OriginReport.bOriginColumnInSampledWindow);
OriginCheckReport.bOriginColumnInSampledWindow);
Writer->WriteValue(TEXT("origin_column_has_player_fit"),
OriginReport.bOriginColumnHasPlayerFit);
OriginCheckReport.bOriginColumnHasPlayerFit);
Writer->WriteValue(TEXT("origin_column_player_fit_cells"),
OriginReport.OriginColumnPlayerFitCells);
OriginCheckReport.OriginColumnPlayerFitCells);
Writer->WriteValue(TEXT("origin_column_reachable_from_arrival"),
OriginReport.bOriginColumnReachable);
OriginCheckReport.bOriginColumnReachable);
Writer->WriteValue(TEXT("reachable_set_to_origin_column_voxels"),
static_cast<double>(OriginReport.ReachableSetToOriginColumnVoxels));
static_cast<double>(OriginCheckReport.ReachableSetToOriginColumnVoxels));
Writer->WriteValue(TEXT("reachable_set_to_origin_column_m"),
static_cast<double>(OriginReport.ReachableSetToOriginColumnVoxels)
static_cast<double>(OriginCheckReport.ReachableSetToOriginColumnVoxels)
* FVoxelPlayerCapsuleConstants::VoxelSizeMeters);
}
Writer->WriteObjectEnd();