feat(gen): Jahni's playtest corrections -- rooms flattened, roads deleted
From walking a TunnelNetwork preset at (0,0). Four corrections, and two of
them were the same mistake.
2a. The (0,0) landing room is sized to host a FUTURE straight vertical
shaft from the surface, with flooring around the origin itself. The
room is the landing for a shaft the player digs later.
2b. The inter-strate descent no longer starts at the spine -- placed by
deterministic hash within a radius around (0,0). The spine room is for
the surface shaft; the descent is elsewhere.
2c. ⛔ DELETED the landing->hub connector roads from 413c15d. Jahni:
"its straight up making other tunnels straightly through everything...
they have nothing to do here." Zero references remain.
2d. ⭐ FLATTEN THE ROOM JOINS instead. Where two spherical rooms intersect
and leave a lip above head height, the join is brought down to a
walkable level.
⭐ 2c AND 2d ARE ONE CORRECTION. I solved "rooms do not connect" by
TUNNELLING BETWEEN THEM when the answer was MAKING THEIR FLOORS MEET. The
81-run sweep had already said so -- zero narrow-gap events, every failure a
vertical severance, renders showing "a sharp diagonal ledge descending into
black open space". A radial corridor to the origin could never have fixed
that; flattening the joins is aimed at exactly it.
2e. Descent tunnel 1.5x wider, per "1.5x bigger wouldn't be too much".
2f. Corridors WANDER. Jahni, from the renders: "its not a maze, its still a
latice made straight paths." The spanning tree changed WHICH edges
exist, not what an edge looks like -- every corridor was a straight
axis-aligned capsule, every junction 90 degrees. Now hash-jittered
chains, seam-safe like the tree.
RENDERER: 192x128 -> 512x288, and cameras are now seeded from the
player-fit set -- "a density-checked pose from the exact player-fit walk
mask". Previously they sat INSIDE ROCK looking at back-faces, which Jahni
spotted instantly and no metric could. First genuinely legible pictures of
this world.
ParallelFor landed at 6 sites in VoxelStrateMeasure before I stopped the
run; the rest of that work continues next.
⚠️ AND THE RENDERER IS ARCHITECTURALLY WRONG, per Jahni: "why, in engine,
does it take a minute to have clear view of thousands of angles, but in our
renderer it takes ages?" Because the engine MESHES ONCE and rasterises --
~32k density calls per chunk, then every angle is free. I RAYMARCH: tens of
millions of density calls PER IMAGE, nothing reused between frames. We
already have the mesher and an export mode. Next task renders from the
mesh.
Also from him: runs must stay under 30 minutes. This one hit 33 and
produced 2 images.
Build clean. Committed as a checkpoint -- the task was stopped mid-Part-3,
not finished.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
This commit is contained in:
1 parent
55bf807f60
commit
d97373c5a8
18 files changed
+2396
-984
No files matched your search
+13
-12
@@ -447,11 +447,11 @@ context. It never touches a generator, operator stack, manager, cache, or mutabl
|
|||||||
`VF_IsPassageLandingFloor` (h:~56/~189, implemented near `.cpp:2059`) turn each inter-strate mouth
|
`VF_IsPassageLandingFloor` (h:~56/~189, implemented near `.cpp:2059`) turn each inter-strate mouth
|
||||||
into a deterministic rounded room plus a hard support slab. The room is sized from the player
|
into a deterministic rounded room plus a hard support slab. The room is sized from the player
|
||||||
capsule (minimum 12 voxels / 3 m flat floor, 12 voxels / 3 m clear height, and at least 14 voxels /
|
capsule (minimum 12 voxels / 3 m flat floor, 12 voxels / 3 m clear height, and at least 14 voxels /
|
||||||
3.5 m authored width); every end also gets a flat-floor connector to the guaranteed `(0,0)` root
|
3.5 m authored width); the `(0,0)` origin landing is enlarged for the reserved future shaft, while
|
||||||
network because the pure source query is local rather than a component proof. The manager's shared
|
inter-strate mouths remain local rooms and deliberately have no landing-to-root connector road.
|
||||||
thread-local passage shortlist serves tube, room, connector, and floor evaluation; no landing
|
The manager's shared thread-local passage shortlist serves tube, room, and floor evaluation; no
|
||||||
search occurs per voxel. `GeneratePassages` stores the full descriptors and the order-independence
|
landing search occurs per voxel. `GeneratePassages` stores the full descriptors and the
|
||||||
test compares them bit-for-bit.
|
order-independence test compares them bit-for-bit.
|
||||||
|
|
||||||
Performance note (h:209-220): caching rooms/tunnels once per chunk instead of per
|
Performance note (h:209-220): caching rooms/tunnels once per chunk instead of per
|
||||||
voxel is the single biggest CPU win.
|
voxel is the single biggest CPU win.
|
||||||
@@ -484,18 +484,19 @@ groups by generator type.
|
|||||||
|
|
||||||
**`Public/VoxelStrateManager.h` + `.cpp`** — `UVoxelStrateManager : UObject` (h:108).
|
**`Public/VoxelStrateManager.h` + `.cpp`** — `UVoxelStrateManager : UObject` (h:108).
|
||||||
Maps depth→strate at runtime; owns passages.
|
Maps depth→strate at runtime; owns passages.
|
||||||
- `FVoxelPassage` (h:39): standing endpoints, radius, type, control points, and upper/lower landing descriptors (room, floor, door, and root connector).
|
- `FVoxelPassage` (h:39): standing endpoints, radius, type, wandering control points, and
|
||||||
|
upper/lower local landing descriptors (room, floor, and door; no root connector).
|
||||||
- `FStrateSlot` (h:84): definition + chunk-Z range + index.
|
- `FStrateSlot` (h:84): definition + chunk-Z range + index.
|
||||||
| Method | .cpp line | Role |
|
| Method | .cpp line | Role |
|
||||||
|--------|-----------|------|
|
|--------|-----------|------|
|
||||||
| `Initialize` | 10 | Keeps the existing sequencing through `GeneratePassages`, but selects one slot source: a validated cooked season (exact ordered bounds/vector/recipe) or the unchanged authored fixed+sorted/shuffled pool. Assigned-invalid seasons fail closed. Authored source definitions are duplicated as content bags; generated slots use deterministic C++ defaults and zero auto-passages because schema 2 does not store passage configuration. |
|
| `Initialize` | 10 | Keeps the existing sequencing through `GeneratePassages`, but selects one slot source: a validated cooked season (exact ordered bounds/vector/recipe) or the unchanged authored fixed+sorted/shuffled pool. Assigned-invalid seasons fail closed. Authored source definitions are duplicated as content bags; generated slots use deterministic C++ defaults and zero auto-passages because schema 2 does not store passage configuration. |
|
||||||
| `GeneratePassages` | 247 | Deterministic passages between consecutive strates (per-strate `PassageConfig::Style` control points; auto passages retain the existing `EVoxelPassageType` default); placement and shape values are independently salted hashes of seed + boundary slot + connection index. Each mouth independently queries its own strate's pure player-fit `VF_SuggestLandingPoint` with its contract seed (cave room salt, otherwise world seed), may move within its archetype-specific budget, then receives a body-sized room/floor and a connector to the common per-strate origin-root annular hub. Unsupported/no-fit answers preserve the old random reach; a deterministic level dog-leg is inserted when the final ramp needs it to stay ≤44°. All descriptors and bounds are generated without reading another passage. |
|
| `GeneratePassages` | 247 | Deterministic passages between consecutive strates (per-strate `PassageConfig::Style` control points; auto passages retain the existing `EVoxelPassageType` default); placement and shape values are independently salted hashes of seed + boundary slot + connection index. Each mouth independently queries its own strate's pure player-fit `VF_SuggestLandingPoint` with its contract seed (cave room salt, otherwise world seed), may move within its archetype-specific budget, then receives a body-sized local room/floor. The origin landing is reserved for the future shaft; no landing-to-root road is generated. Unsupported/no-fit answers preserve the old random reach; a deterministic level dog-leg is inserted when the final ramp needs it to stay ≤44°. All descriptors and bounds are generated without reading another passage. |
|
||||||
| `EvaluateModifierSDF` | 357 | SDF of passage tubes plus landing rooms/connectors at a point. A per-chunk `thread_local` shortlist (`PassagesVersion`-stamped) → far chunks return `FLT_MAX` without walking `Passages`; the same cache serves floor membership. §8.10. |
|
| `EvaluateModifierSDF` | 357 | SDF of passage tubes plus local landing rooms at a point. A per-chunk `thread_local` shortlist (`PassagesVersion`-stamped) → far chunks return `FLT_MAX` without walking `Passages`; the same cache serves floor membership. §8.10. |
|
||||||
| `ApplyPassageModifier` | 1319 | Shared legacy/op-stack passage post: carves tube and landing/connector air through the idempotent landing primitive, then restores the landing/connector support slab in internal positive-solid density. |
|
| `ApplyPassageModifier` | 1319 | Shared legacy/op-stack passage post: carves tube and local landing air through the idempotent landing primitive, then restores the local landing support slab in internal positive-solid density. |
|
||||||
| `VF_ApplyPassageLandingCarving` | — | Idempotent density-independent landing-air threshold shared by the structural pass and the post-disturbance MC backstop; keeps legacy and operator-stack results bit-identical. |
|
| `VF_ApplyPassageLandingCarving` | — | Idempotent density-independent landing-air threshold shared by the structural pass and the post-disturbance MC backstop; keeps legacy and operator-stack results bit-identical. |
|
||||||
| `ApplyPassageLandingFloorMC` | 1347 | Reasserts the same support slab after MC-space disturbances and before the final XY edge seal. |
|
| `ApplyPassageLandingFloorMC` | 1347 | Reasserts the same support slab after MC-space disturbances and before the final XY edge seal. |
|
||||||
| `AnyPassageNearBox` | — | Conservative sphere-vs-AABB test of every passage's bound against a voxel box (+carve blend pad). Per TILE (ClassifyTile guard), never per voxel. |
|
| `AnyPassageNearBox` | — | Conservative sphere-vs-AABB test of every passage's bound against a voxel box (+carve blend pad). Per TILE (ClassifyTile guard), never per voxel. |
|
||||||
| `AnyPassageLandingFloorNearBox` | — | Conservative room/connector-floor AABB guard. It kills the `AllAir` hypothesis wherever a proved landing floor can occur; the op-stack returns `Both` for the same box. |
|
| `AnyPassageLandingFloorNearBox` | — | Conservative local-landing-floor AABB guard. It kills the `AllAir` hypothesis wherever a proved landing floor can occur; the op-stack returns `Both` for the same box. |
|
||||||
| `FindSlotIndexForChunkZ` | 427 | Z → layout index. |
|
| `FindSlotIndexForChunkZ` | 427 | Z → layout index. |
|
||||||
| `GetStrateAt` / `GetStrateIndex` | 443 / 455 | World-Z queries. |
|
| `GetStrateAt` / `GetStrateIndex` | 443 / 455 | World-Z queries. |
|
||||||
| `GetLayoutVersion` | h:161 (inline) | Layout/passage generation counter (= `PassagesVersion`, bumped by every `Initialize` and by the editor composer override). Hot-path callers key `thread_local` memos on it (strate-index memo in `GetDensityWithParams`, passage shortlist) so live changes never serve stale data. |
|
| `GetLayoutVersion` | h:161 (inline) | Layout/passage generation counter (= `PassagesVersion`, bumped by every `Initialize` and by the editor composer override). Hot-path callers key `thread_local` memos on it (strate-index memo in `GetDensityWithParams`, passage shortlist) so live changes never serve stale data. |
|
||||||
@@ -714,7 +715,7 @@ The plugin's first tests (`OPSTACK-PLAN.md` Phase 0.5). Run them from the editor
|
|||||||
| `VoxelForgeComposerSeasonTest.cpp` | `VoxelForge.Composer.Season` | Existing compose/review/393,216-sample round trip plus schema hash tamper rejection, two independent runtime manager/generator instances, season-authoritative seed/spine/radius, unset-season regression, and brute-force verification of every sampled non-Mixed recipe tile verdict (up to 64). |
|
| `VoxelForgeComposerSeasonTest.cpp` | `VoxelForge.Composer.Season` | Existing compose/review/393,216-sample round trip plus schema hash tamper rejection, two independent runtime manager/generator instances, season-authoritative seed/spine/radius, unset-season regression, and brute-force verification of every sampled non-Mixed recipe tile verdict (up to 64). |
|
||||||
| `VoxelForgeComposerCorpusFreeTest.cpp` | `VoxelForge.Composer.CorpusFree` | Shares each structure recipe across today's corpus blend, naive independent uniform rolls, and constraint-sampled rolls. The completed equal-arm world-scale run uses **16 candidates per arm**, step 4 / radius 256 / `MaxCells=8,000,000`, fixed passage-law mouths, and 40 box probes per candidate. Result: **10/16, 9/16, 8/16** survival; survivor feature-scale ranges are **8..268**, **8..60**, **8..92** voxels. Box checks: **185/246,235**, **260/346,060**, **299/397,969** proved/voxels, **0 violations** in every arm; constraint parameter and box-stop violations were also 0. |
|
| `VoxelForgeComposerCorpusFreeTest.cpp` | `VoxelForge.Composer.CorpusFree` | Shares each structure recipe across today's corpus blend, naive independent uniform rolls, and constraint-sampled rolls. The completed equal-arm world-scale run uses **16 candidates per arm**, step 4 / radius 256 / `MaxCells=8,000,000`, fixed passage-law mouths, and 40 box probes per candidate. Result: **10/16, 9/16, 8/16** survival; survivor feature-scale ranges are **8..268**, **8..60**, **8..92** voxels. Box checks: **185/246,235**, **260/346,060**, **299/397,969** proved/voxels, **0 violations** in every arm; constraint parameter and box-stop violations were also 0. |
|
||||||
| `VoxelForgeLayoutOrderIndependenceTest.cpp` | `VoxelForge.Determinism.LayoutOrderIndependence` | Builds a known transient soft-pointer pool, then rebuilds it in original, reversed, and swapped orders. Requires a non-empty layout and passage set, and compares every slot's definition/Z/height plus passage endpoints, landing descriptors, control geometry, and bounds bit-for-bit. |
|
| `VoxelForgeLayoutOrderIndependenceTest.cpp` | `VoxelForge.Determinism.LayoutOrderIndependence` | Builds a known transient soft-pointer pool, then rebuilds it in original, reversed, and swapped orders. Requires a non-empty layout and passage set, and compares every slot's definition/Z/height plus passage endpoints, landing descriptors, control geometry, and bounds bit-for-bit. |
|
||||||
| `VoxelForgePassageOpenSpaceTest.cpp` | `VoxelForge.Determinism.PassageLandsInOpenSpace` | Uses the real fixture density path to check both generated mouths against independent source queries, then audits all 14 inter-strate landing ends: body-derived room dimensions, final-density flat-floor slope, explicit root-network join, seal containment, hot-call/rebuild timing, and targeted `ClassifyTile` box soundness. Reports source-fit/refused mouths and per-archetype landing slopes; fails on any floor, join, seal, or box violation. |
|
| `VoxelForgePassageOpenSpaceTest.cpp` | `VoxelForge.Determinism.PassageLandsInOpenSpace` | Uses the real fixture density path to check both generated mouths against independent source queries, then audits all inter-strate landing ends: body-derived room dimensions, final-density flat-floor slope, local floor/join geometry, seal containment, hot-call/rebuild timing, and targeted `ClassifyTile` box soundness. Reports source-fit/refused mouths and per-archetype landing slopes; fails on any floor, join, seal, or box violation. |
|
||||||
| `VoxelForgeStrateConnectivityTest.cpp` | `VoxelForge.Generation.StrateConnectivity` / `VoxelForge.Generation.StrateConnectivityRefinement` / `VoxelForge.Generation.VerticalShaftSeamFreedom` | Bounded strate metrics with density-polarity and solid-gap controls, deterministic route rechecks, and refinement sweeps. The refinement test uses the bounded 4-chunk density fixture, fits each measurement AABB to the arrival/departure mouth pair plus three shaft spacings (the prescribed local tree fallback), and caps the fitted control at 120,000,000 cells. It reports exact before/after seed-6 cell counts, reruns every negative with the fitted margin doubled, measures tree orphan candidates/path reachability and roughness-bubble proxies, asserts mouths remain inside the deterministic shaft feature core (the exact axis is an unsupported open cylinder), checks source/mirror physical paths, and requires 16/16 effective arrival→departure results across the 16 VerticalShafts seeds. Margin-binding negatives are reported as measurement limits, never as gap findings. The shaft seam test re-evaluates cell-boundary positions after warming distinct neighbouring chunk contexts. |
|
| `VoxelForgeStrateConnectivityTest.cpp` | `VoxelForge.Generation.StrateConnectivity` / `VoxelForge.Generation.StrateConnectivityRefinement` / `VoxelForge.Generation.VerticalShaftSeamFreedom` | Bounded strate metrics with density-polarity and solid-gap controls, deterministic route rechecks, and refinement sweeps. The refinement test uses the bounded 4-chunk density fixture, fits each measurement AABB to the arrival/departure mouth pair plus three shaft spacings (the prescribed local tree fallback), and caps the fitted control at 120,000,000 cells. It reports exact before/after seed-6 cell counts, reruns every negative with the fitted margin doubled, measures tree orphan candidates/path reachability and roughness-bubble proxies, asserts mouths remain inside the deterministic shaft feature core (the exact axis is an unsupported open cylinder), checks source/mirror physical paths, and requires 16/16 effective arrival→departure results across the 16 VerticalShafts seeds. Margin-binding negatives are reported as measurement limits, never as gap findings. The shaft seam test re-evaluates cell-boundary positions after warming distinct neighbouring chunk contexts. |
|
||||||
| `VoxelForgeMazeSeamTest.cpp` | `VoxelForge.Generation.MazeSeamFreedom` | Re-evaluates **42** cell-boundary probes after warming six different neighbouring chunk contexts in both legacy and operator-stack paths. Latest result: **0 legacy mismatches, 0 operator mismatches**. The Maze source's 2×2×2 lower-node window is deliberately local; no wide collect is involved. |
|
| `VoxelForgeMazeSeamTest.cpp` | `VoxelForge.Generation.MazeSeamFreedom` | Re-evaluates **42** cell-boundary probes after warming six different neighbouring chunk contexts in both legacy and operator-stack paths. Latest result: **0 legacy mismatches, 0 operator mismatches**. The Maze source's 2×2×2 lower-node window is deliberately local; no wide collect is involved. |
|
||||||
| `VoxelForgeWorldEdgeSealTest.cpp` | `VoxelForge.Generation.WorldEdgeSeal` | Positive samples outside the radius across every fixture strate, bit-identical interior negative control against radius 0, monotonic smooth-ramp check, radius-0 no-op, radial `ClassifyBox`/T1.d proofs brute-forced for every reported voxel, and synthetic plus generated near-rim passage coverage. Emits one aggregate summary with proved-box and voxel counts. |
|
| `VoxelForgeWorldEdgeSealTest.cpp` | `VoxelForge.Generation.WorldEdgeSeal` | Positive samples outside the radius across every fixture strate, bit-identical interior negative control against radius 0, monotonic smooth-ramp check, radius-0 no-op, radial `ClassifyBox`/T1.d proofs brute-forced for every reported voxel, and synthetic plus generated near-rim passage coverage. Emits one aggregate summary with proved-box and voxel counts. |
|
||||||
@@ -743,7 +744,7 @@ Stage order (negative=solid throughout). Each stage's anchor:
|
|||||||
| 4h — Pinch | 1142 | Passage bottlenecks. |
|
| 4h — Pinch | 1142 | Passage bottlenecks. |
|
||||||
| 5 — Worm tunnels | 1241 | abs(noise1)+abs(noise2), masked by distance-to-network (`WormNetworkRange`: braids hugging rooms/tunnels, no far-field speckle; 0 = legacy unmasked). |
|
| 5 — Worm tunnels | 1241 | abs(noise1)+abs(noise2), masked by distance-to-network (`WormNetworkRange`: braids hugging rooms/tunnels, no far-field speckle; 0 = legacy unmasked). |
|
||||||
| 6 — Boundary seal | 1274 | Solid top/bottom shells (`ApplyBoundarySeal`). |
|
| 6 — Boundary seal | 1274 | Solid top/bottom shells (`ApplyBoundarySeal`). |
|
||||||
| 7 — Inter-strate passages + landings | `GetDensityWithParams`: 2217; `GetSlabDensity`: 2434+ | Carve passage tubes and deterministic room/connectors, restore their support floors, then leave the final XY seal to the outer MC post (`ApplyPassageModifier` / `ApplyPassageLandingFloorMC`). |
|
| 7 — Inter-strate passages + landings | `GetDensityWithParams`: 2217; `GetSlabDensity`: 2434+ | Carve wandering passage tubes and deterministic local landing rooms, restore their support floors, then leave the final XY seal to the outer MC post (`ApplyPassageModifier` / `ApplyPassageLandingFloorMC`). |
|
||||||
|
|
||||||
### 4.2 `GetSlabDensity` (FlatPlain / CrystalChamber) — VoxelGenerator.cpp:1306
|
### 4.2 `GetSlabDensity` (FlatPlain / CrystalChamber) — VoxelGenerator.cpp:1306
|
||||||
| Step | Line | What |
|
| Step | Line | What |
|
||||||
|
|||||||
@@ -141,21 +141,11 @@ namespace
|
|||||||
return SameVectorBits(A.StandingPoint, B.StandingPoint)
|
return SameVectorBits(A.StandingPoint, B.StandingPoint)
|
||||||
&& SameVectorBits(A.DoorPoint, B.DoorPoint)
|
&& SameVectorBits(A.DoorPoint, B.DoorPoint)
|
||||||
&& SameVectorBits(A.DoorDirection, B.DoorDirection)
|
&& SameVectorBits(A.DoorDirection, B.DoorDirection)
|
||||||
&& SameVectorBits(A.ConnectorStart, B.ConnectorStart)
|
|
||||||
&& SameVectorBits(A.ConnectorControl, B.ConnectorControl)
|
|
||||||
&& SameVectorBits(A.ConnectorEnd, B.ConnectorEnd)
|
|
||||||
&& VoxelForgeTest::BitEqual(A.FloorZ, B.FloorZ)
|
&& VoxelForgeTest::BitEqual(A.FloorZ, B.FloorZ)
|
||||||
&& VoxelForgeTest::BitEqual(A.CeilingZ, B.CeilingZ)
|
&& VoxelForgeTest::BitEqual(A.CeilingZ, B.CeilingZ)
|
||||||
&& VoxelForgeTest::BitEqual(A.HalfWidth, B.HalfWidth)
|
&& VoxelForgeTest::BitEqual(A.HalfWidth, B.HalfWidth)
|
||||||
&& VoxelForgeTest::BitEqual(A.FloorThickness, B.FloorThickness)
|
&& VoxelForgeTest::BitEqual(A.FloorThickness, B.FloorThickness)
|
||||||
&& VoxelForgeTest::BitEqual(A.ConnectorRadius, B.ConnectorRadius)
|
&& A.bSourcePlayerFit == B.bSourcePlayerFit;
|
||||||
&& VoxelForgeTest::BitEqual(A.ConnectorCeilingZ, B.ConnectorCeilingZ)
|
|
||||||
&& VoxelForgeTest::BitEqual(A.RootFloorZ, B.RootFloorZ)
|
|
||||||
&& VoxelForgeTest::BitEqual(A.RootCeilingZ, B.RootCeilingZ)
|
|
||||||
&& VoxelForgeTest::BitEqual(A.RootSpineRadius, B.RootSpineRadius)
|
|
||||||
&& A.bSourcePlayerFit == B.bSourcePlayerFit
|
|
||||||
&& A.bHasNetworkConnector == B.bHasNetworkConnector
|
|
||||||
&& A.bHasConnectorBend == B.bHasConnectorBend;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SamePassage(const FVoxelPassage& A, const FVoxelPassage& B, FString& OutMismatch,
|
bool SamePassage(const FVoxelPassage& A, const FVoxelPassage& B, FString& OutMismatch,
|
||||||
|
|||||||
@@ -492,7 +492,6 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
|
|||||||
int32 NumLandingEnds = 0;
|
int32 NumLandingEnds = 0;
|
||||||
int32 NumLandingFloorsPassed = 0;
|
int32 NumLandingFloorsPassed = 0;
|
||||||
int32 NumLandingFloorFailures = 0;
|
int32 NumLandingFloorFailures = 0;
|
||||||
int32 NumLandingNetworkJoins = 0;
|
|
||||||
int32 NumLandingSealFailures = 0;
|
int32 NumLandingSealFailures = 0;
|
||||||
int32 NumLandingFloorBoxes = 0;
|
int32 NumLandingFloorBoxes = 0;
|
||||||
int32 NumLandingBoxProofs = 0;
|
int32 NumLandingBoxProofs = 0;
|
||||||
@@ -500,17 +499,12 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
|
|||||||
int32 NumLandingBoxAllAir = 0;
|
int32 NumLandingBoxAllAir = 0;
|
||||||
int32 NumLandingBoxViolations = 0;
|
int32 NumLandingBoxViolations = 0;
|
||||||
int32 NumLandingSourceFit = 0;
|
int32 NumLandingSourceFit = 0;
|
||||||
int32 NumLandingConnectorSlopesPassed = 0;
|
|
||||||
int32 NumLandingConnectorSlopeFailures = 0;
|
|
||||||
float WorstLandingFloorGradient = 0.0f;
|
float WorstLandingFloorGradient = 0.0f;
|
||||||
float WorstLandingConnectorGradient = 0.0f;
|
|
||||||
FString FirstLandingFailure;
|
FString FirstLandingFailure;
|
||||||
FString LandingSlopeReport;
|
FString LandingSlopeReport;
|
||||||
FWalkableTunnelAudit TunnelAudit;
|
FWalkableTunnelAudit TunnelAudit;
|
||||||
constexpr float LandingWalkableAngleDegrees =
|
constexpr float LandingWalkableAngleDegrees =
|
||||||
VoxelPassageGeometry::WalkableTunnelMaxGradientDegrees;
|
VoxelPassageGeometry::WalkableTunnelMaxGradientDegrees;
|
||||||
constexpr float LandingWalkableSlope =
|
|
||||||
VoxelPassageGeometry::WalkableTunnelMaxGradient;
|
|
||||||
|
|
||||||
for (const FVoxelPassage& Passage : Passages)
|
for (const FVoxelPassage& Passage : Passages)
|
||||||
{
|
{
|
||||||
@@ -985,65 +979,10 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
|
|||||||
FMath::DegreesToRadians(LandingWalkableAngleDegrees));
|
FMath::DegreesToRadians(LandingWalkableAngleDegrees));
|
||||||
}
|
}
|
||||||
|
|
||||||
float ConnectorGradient = -1.0f;
|
// Landing rooms are intentionally local now. Their flat floors are the only landing
|
||||||
bool bConnectorSlopePassed = false;
|
// contract; the removed radial root connectors were the source of the straight roads
|
||||||
if (Landing.bHasNetworkConnector
|
// through unrelated rooms.
|
||||||
&& FMath::IsFinite(Landing.ConnectorStart.X)
|
const bool bLandingSlopePassed = bFloorPassed;
|
||||||
&& FMath::IsFinite(Landing.ConnectorStart.Y)
|
|
||||||
&& FMath::IsFinite(Landing.ConnectorEnd.X)
|
|
||||||
&& FMath::IsFinite(Landing.ConnectorEnd.Y)
|
|
||||||
&& (!Landing.bHasConnectorBend
|
|
||||||
|| (FMath::IsFinite(Landing.ConnectorControl.X)
|
|
||||||
&& FMath::IsFinite(Landing.ConnectorControl.Y)))
|
|
||||||
&& FMath::IsFinite(Landing.RootFloorZ)
|
|
||||||
&& FMath::IsFinite(Landing.FloorZ))
|
|
||||||
{
|
|
||||||
const FVector2D ConnectorStart(
|
|
||||||
Landing.ConnectorStart.X, Landing.ConnectorStart.Y);
|
|
||||||
const FVector2D ConnectorEnd(
|
|
||||||
Landing.ConnectorEnd.X, Landing.ConnectorEnd.Y);
|
|
||||||
const float DirectLength = (ConnectorEnd - ConnectorStart).Size();
|
|
||||||
if (Landing.bHasConnectorBend)
|
|
||||||
{
|
|
||||||
const FVector2D ConnectorControl(
|
|
||||||
Landing.ConnectorControl.X, Landing.ConnectorControl.Y);
|
|
||||||
const float FinalLegLength = (ConnectorEnd - ConnectorControl).Size();
|
|
||||||
const float RampLength = FMath::Max(
|
|
||||||
FinalLegLength - (Landing.RootSpineRadius > 0.0f
|
|
||||||
? VoxelPassageGeometry::RootOverlapVoxels : 0.0f),
|
|
||||||
KINDA_SMALL_NUMBER);
|
|
||||||
ConnectorGradient = FMath::Abs(
|
|
||||||
Landing.RootFloorZ - Landing.FloorZ) / RampLength;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
const float RampLength = FMath::Max(
|
|
||||||
DirectLength - FMath::Max(Landing.HalfWidth - 1.0f, 0.0f)
|
|
||||||
- (Landing.RootSpineRadius > 0.0f
|
|
||||||
? VoxelPassageGeometry::RootOverlapVoxels : 0.0f),
|
|
||||||
KINDA_SMALL_NUMBER);
|
|
||||||
ConnectorGradient = FMath::Abs(
|
|
||||||
Landing.RootFloorZ - Landing.FloorZ) / RampLength;
|
|
||||||
}
|
|
||||||
bConnectorSlopePassed = FMath::IsFinite(ConnectorGradient)
|
|
||||||
&& ConnectorGradient <= LandingWalkableSlope + KINDA_SMALL_NUMBER;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bConnectorSlopePassed)
|
|
||||||
{
|
|
||||||
++NumLandingConnectorSlopesPassed;
|
|
||||||
WorstLandingConnectorGradient = FMath::Max(
|
|
||||||
WorstLandingConnectorGradient, ConnectorGradient);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
++NumLandingConnectorSlopeFailures;
|
|
||||||
RecordFailure(FString::Printf(
|
|
||||||
TEXT("landing at strate %d has connector slope %.6f above the %.6f walkable limit"),
|
|
||||||
StrateIndex, ConnectorGradient, LandingWalkableSlope));
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool bLandingSlopePassed = bFloorPassed && bConnectorSlopePassed;
|
|
||||||
if (bLandingSlopePassed)
|
if (bLandingSlopePassed)
|
||||||
{
|
{
|
||||||
++NumLandingFloorsPassed;
|
++NumLandingFloorsPassed;
|
||||||
@@ -1076,16 +1015,6 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
|
|||||||
{
|
{
|
||||||
++NumLandingSourceFit;
|
++NumLandingSourceFit;
|
||||||
}
|
}
|
||||||
if (Landing.bHasNetworkConnector)
|
|
||||||
{
|
|
||||||
++NumLandingNetworkJoins;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
RecordFailure(FString::Printf(
|
|
||||||
TEXT("landing at strate %d has neither a source fit nor a deterministic network connector"),
|
|
||||||
StrateIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exercise the same box shortcut that can otherwise erase a support slab. A uniform
|
// Exercise the same box shortcut that can otherwise erase a support slab. A uniform
|
||||||
// verdict is allowed only if every exact ClassifyTile lattice sample agrees with the
|
// verdict is allowed only if every exact ClassifyTile lattice sample agrees with the
|
||||||
@@ -1146,15 +1075,12 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
|
|||||||
|
|
||||||
const FString LandingArchetype = ArchetypeName(Definition->GeneratorType);
|
const FString LandingArchetype = ArchetypeName(Definition->GeneratorType);
|
||||||
LandingSlopeReport += FString::Printf(
|
LandingSlopeReport += FString::Printf(
|
||||||
TEXT("%s[%d:%s]=floor:%s(%.6f),connector:%s(%.6f)%s "),
|
TEXT("%s[%d:%s]=flat-floor:%s(%.6f) "),
|
||||||
*LandingArchetype,
|
*LandingArchetype,
|
||||||
StrateIndex,
|
StrateIndex,
|
||||||
EndIndex == 0 ? TEXT("upper") : TEXT("lower"),
|
EndIndex == 0 ? TEXT("upper") : TEXT("lower"),
|
||||||
bFloorPassed ? TEXT("PASS") : TEXT("FAIL"),
|
bFloorPassed ? TEXT("PASS") : TEXT("FAIL"),
|
||||||
LandingGradient,
|
LandingGradient);
|
||||||
bConnectorSlopePassed ? TEXT("PASS") : TEXT("FAIL"),
|
|
||||||
ConnectorGradient,
|
|
||||||
Landing.bHasConnectorBend ? TEXT("[bend]") : TEXT(""));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1237,18 +1163,12 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
|
|||||||
NumUnsupported,
|
NumUnsupported,
|
||||||
NumSupportedWithoutPoint));
|
NumSupportedWithoutPoint));
|
||||||
AddInfo(FString::Printf(
|
AddInfo(FString::Printf(
|
||||||
TEXT("Landing geometry: %d/%d ends passed the final-density floor test (worst room gradient %.6f), connector slopes %d/%d (worst %.6f; walkable limit %.6f), source-fit anchors %d/%d, explicit root-network joins %d/%d, seal violations %d, geometry/floor failures %d."),
|
TEXT("Landing geometry: %d/%d ends passed the final-density flat-floor test (worst gradient %.6f), source-fit anchors %d/%d, seal violations %d, geometry/floor failures %d."),
|
||||||
NumLandingFloorsPassed,
|
NumLandingFloorsPassed,
|
||||||
NumLandingEnds,
|
NumLandingEnds,
|
||||||
WorstLandingFloorGradient,
|
WorstLandingFloorGradient,
|
||||||
NumLandingConnectorSlopesPassed,
|
|
||||||
NumLandingEnds,
|
|
||||||
WorstLandingConnectorGradient,
|
|
||||||
LandingWalkableSlope,
|
|
||||||
NumLandingSourceFit,
|
NumLandingSourceFit,
|
||||||
NumLandingEnds,
|
NumLandingEnds,
|
||||||
NumLandingNetworkJoins,
|
|
||||||
NumLandingEnds,
|
|
||||||
NumLandingSealFailures,
|
NumLandingSealFailures,
|
||||||
NumLandingFloorFailures));
|
NumLandingFloorFailures));
|
||||||
if (!LandingSlopeReport.IsEmpty())
|
if (!LandingSlopeReport.IsEmpty())
|
||||||
@@ -1372,9 +1292,6 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
|
|||||||
return NumInterStratePassages > 0
|
return NumInterStratePassages > 0
|
||||||
&& NumLandingEnds == NumInterStratePassages * 2
|
&& NumLandingEnds == NumInterStratePassages * 2
|
||||||
&& NumLandingFloorsPassed == NumLandingEnds
|
&& NumLandingFloorsPassed == NumLandingEnds
|
||||||
&& NumLandingConnectorSlopesPassed == NumLandingEnds
|
|
||||||
&& NumLandingConnectorSlopeFailures == 0
|
|
||||||
&& NumLandingNetworkJoins == NumLandingEnds
|
|
||||||
&& NumLandingSealFailures == 0
|
&& NumLandingSealFailures == 0
|
||||||
&& NumLandingFloorFailures == 0
|
&& NumLandingFloorFailures == 0
|
||||||
&& NumLandingBoxViolations == 0
|
&& NumLandingBoxViolations == 0
|
||||||
|
|||||||
@@ -420,16 +420,23 @@ namespace
|
|||||||
for (int32 TunnelIndex = 0; TunnelIndex < Cache.Tunnels.Num(); ++TunnelIndex)
|
for (int32 TunnelIndex = 0; TunnelIndex < Cache.Tunnels.Num(); ++TunnelIndex)
|
||||||
{
|
{
|
||||||
const FCachedTunnel& Tunnel = Cache.Tunnels[TunnelIndex];
|
const FCachedTunnel& Tunnel = Cache.Tunnels[TunnelIndex];
|
||||||
if (Tunnel.bHasMidpoint)
|
if (Tunnel.ControlPoints.Num() >= 2
|
||||||
|
&& Tunnel.ControlRadii.Num() == Tunnel.ControlPoints.Num())
|
||||||
|
{
|
||||||
|
for (int32 SegmentIndex = 0;
|
||||||
|
SegmentIndex + 1 < Tunnel.ControlPoints.Num();
|
||||||
|
++SegmentIndex)
|
||||||
{
|
{
|
||||||
VF_ConsiderTunnelSegment(
|
VF_ConsiderTunnelSegment(
|
||||||
World, Params, Core, Tunnel.EndpointA, Tunnel.Midpoint,
|
World, Params, Core,
|
||||||
Tunnel.RadiusA, Tunnel.RadiusMid,
|
Tunnel.ControlPoints[SegmentIndex],
|
||||||
FString::Printf(TEXT("tunnel[%d].A-mid"), TunnelIndex), OutBest);
|
Tunnel.ControlPoints[SegmentIndex + 1],
|
||||||
VF_ConsiderTunnelSegment(
|
Tunnel.ControlRadii[SegmentIndex],
|
||||||
World, Params, Core, Tunnel.Midpoint, Tunnel.EndpointB,
|
Tunnel.ControlRadii[SegmentIndex + 1],
|
||||||
Tunnel.RadiusMid, Tunnel.RadiusB,
|
FString::Printf(TEXT("tunnel[%d].segment%d"),
|
||||||
FString::Printf(TEXT("tunnel[%d].mid-B"), TunnelIndex), OutBest);
|
TunnelIndex, SegmentIndex),
|
||||||
|
OutBest);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -196,42 +196,6 @@ namespace
|
|||||||
return A + B;
|
return A + B;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Structural tree capsules must meet shaft axes without landing inside a ledge band. This is
|
|
||||||
// the same pure helper as the generator path: one deterministic safe interval is shared by the
|
|
||||||
// whole strate, so a link cannot be blocked when it crosses an unrelated shaft's ledge.
|
|
||||||
FORCEINLINE float VF_SelectVerticalTreeConnectorZ(
|
|
||||||
const FVerticalShaftParams& Params,
|
|
||||||
float BottomZ,
|
|
||||||
float TopZ,
|
|
||||||
uint32 LinkHash)
|
|
||||||
{
|
|
||||||
if (Params.LedgeSpacing > 0.0f && Params.LedgeDepth > 0.0f)
|
|
||||||
{
|
|
||||||
const float Period = Params.LedgeSpacing;
|
|
||||||
const float RelativeBottom = BottomZ - Params.StrateBottomWorldZ;
|
|
||||||
const float RelativeTop = TopZ - Params.StrateBottomWorldZ;
|
|
||||||
const int32 FirstPeriod = FMath::FloorToInt(RelativeBottom / Period);
|
|
||||||
const float Random01 = VoxelHash::ToFloat01(VoxelHash::Mix(LinkHash));
|
|
||||||
|
|
||||||
for (int32 PeriodOffset = 0; PeriodOffset <= 1; ++PeriodOffset)
|
|
||||||
{
|
|
||||||
const float PeriodStart = static_cast<float>(FirstPeriod + PeriodOffset) * Period;
|
|
||||||
const float SafeStart = FMath::Max(
|
|
||||||
RelativeBottom, PeriodStart + Params.LedgeDepth + 0.01f);
|
|
||||||
const float SafeEnd = FMath::Min(
|
|
||||||
RelativeTop, PeriodStart + Period - Params.LedgeDepth - 0.01f);
|
|
||||||
if (SafeEnd > SafeStart)
|
|
||||||
{
|
|
||||||
return Params.StrateBottomWorldZ
|
|
||||||
+ FMath::Lerp(SafeStart, SafeEnd, Random01);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return FMath::Lerp(BottomZ, TopZ,
|
|
||||||
VoxelHash::ToFloat01(VoxelHash::Mix(LinkHash)));
|
|
||||||
}
|
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// RÔLE 1 — SOURCE : CHAMP CONSTANT / CONSTANT FIELD (roc ET vide)
|
// RÔLE 1 — SOURCE : CHAMP CONSTANT / CONSTANT FIELD (roc ET vide)
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
@@ -1856,7 +1820,7 @@ namespace
|
|||||||
}
|
}
|
||||||
// Keep the source and op-stack operation order identical: the origin room is carved
|
// 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
|
// before the boundary seal, then its support floor is reasserted after the passage
|
||||||
// post so no connector can remove the standing surface.
|
// post so no passage-air post can remove the standing surface.
|
||||||
VF_ApplyOriginLandingFloor(
|
VF_ApplyOriginLandingFloor(
|
||||||
InOut.Density, X, Y, Z, TopZ, BotZ, Seal, Base, SpineRadius);
|
InOut.Density, X, Y, Z, TopZ, BotZ, Seal, Base, SpineRadius);
|
||||||
}
|
}
|
||||||
@@ -1925,7 +1889,13 @@ namespace
|
|||||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||||
|
|
||||||
struct FShaft { float X, Y, R; int32 CellX, CellY; bool bOriginSpine; };
|
struct FShaft { float X, Y, R; int32 CellX, CellY; bool bOriginSpine; };
|
||||||
struct FConn { FVector A, B; float Radius; };
|
struct FConn
|
||||||
|
{
|
||||||
|
FVector A, B;
|
||||||
|
float Radius = 0.0f;
|
||||||
|
float FloorZ = 0.0f;
|
||||||
|
bool bWalkableFloor = false;
|
||||||
|
};
|
||||||
|
|
||||||
// ⚠️ DÉCLARÉE ICI, avant toute fonction qui la renvoie. Un type imbriqué doit exister au
|
// ⚠️ DÉCLARÉE ICI, avant toute fonction qui la renvoie. Un type imbriqué doit exister au
|
||||||
// moment où le COMPILATEUR lit la SIGNATURE — les corps de méthodes sont différés, pas les
|
// moment où le COMPILATEUR lit la SIGNATURE — les corps de méthodes sont différés, pas les
|
||||||
@@ -2178,17 +2148,26 @@ namespace
|
|||||||
|
|
||||||
auto EmitTreeConnector = [&](const FShaft& Child, const FShaft* Parent)
|
auto EmitTreeConnector = [&](const FShaft& Child, const FShaft* Parent)
|
||||||
{
|
{
|
||||||
const int32 ParentCellX = Parent != nullptr ? Parent->CellX : 0;
|
const float Zc = VoxelPassageGeometry::VerticalShaftConnectorCenterZ(
|
||||||
const int32 ParentCellY = Parent != nullptr ? Parent->CellY : 0;
|
P.StrateBottomWorldZ,
|
||||||
const uint32 LinkHash = VoxelHash::Pair(
|
BottomZ,
|
||||||
Child.CellX, Child.CellY, ParentCellX, ParentCellY, Salt ^ TreeSalt);
|
TopZ,
|
||||||
const float Zc = VF_SelectVerticalTreeConnectorZ(
|
P.LedgeSpacing,
|
||||||
P, BottomZ, TopZ, LinkHash);
|
P.LedgeDepth,
|
||||||
|
TreeRadius);
|
||||||
|
const float FloorZ = VoxelPassageGeometry::VerticalShaftConnectorFloorZ(
|
||||||
|
P.StrateBottomWorldZ,
|
||||||
|
BottomZ,
|
||||||
|
TopZ,
|
||||||
|
P.LedgeSpacing,
|
||||||
|
P.LedgeDepth,
|
||||||
|
TreeRadius);
|
||||||
const FVector ParentPoint = Parent != nullptr
|
const FVector ParentPoint = Parent != nullptr
|
||||||
? FVector(Parent->X, Parent->Y, Zc)
|
? FVector(Parent->X, Parent->Y, Zc)
|
||||||
: FVector(0.0f, 0.0f, Zc);
|
: FVector(0.0f, 0.0f, Zc);
|
||||||
const FConn Conn{
|
const FConn Conn{
|
||||||
FVector(Child.X, Child.Y, Zc), ParentPoint, TreeRadius };
|
FVector(Child.X, Child.Y, Zc), ParentPoint, TreeRadius,
|
||||||
|
FloorZ, true };
|
||||||
if (ConnectorMayReachCell(Conn))
|
if (ConnectorMayReachCell(Conn))
|
||||||
{
|
{
|
||||||
Cache.Conns.Add(Conn);
|
Cache.Conns.Add(Conn);
|
||||||
@@ -2310,7 +2289,8 @@ namespace
|
|||||||
const float ConnectorR = bSpineConnector
|
const float ConnectorR = bSpineConnector
|
||||||
? SpineConnectorRadius() : P.ConnectorRadius;
|
? SpineConnectorRadius() : P.ConnectorRadius;
|
||||||
const FConn Conn{
|
const FConn Conn{
|
||||||
FVector(A.X, A.Y, Zc), FVector(B.X, B.Y, Zc), ConnectorR };
|
FVector(A.X, A.Y, Zc), FVector(B.X, B.Y, Zc), ConnectorR,
|
||||||
|
0.0f, false };
|
||||||
if (ConnectorMayReachCell(Conn))
|
if (ConnectorMayReachCell(Conn))
|
||||||
{
|
{
|
||||||
Cache.Conns.Add(Conn);
|
Cache.Conns.Add(Conn);
|
||||||
@@ -2404,6 +2384,109 @@ namespace
|
|||||||
const FShaftFieldSource* Field; // NON possédant : la pile possède la source
|
const FShaftFieldSource* Field; // NON possédant : la pile possède la source
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The deterministic shaft tree is a walkable network as well as an air topology. Reassert
|
||||||
|
// its air core after partial shaft ledges, then add the same inset floor band as the native
|
||||||
|
// path at the common first ledge level. This lets a player-fit route cross between shafts
|
||||||
|
// without restoring the removed landing-to-origin roads.
|
||||||
|
class FShaftConnectorAirMod final : public IVoxelDensityOp
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit FShaftConnectorAirMod(const FShaftFieldSource* InField, float InBaseDensity)
|
||||||
|
: Field(InField), BaseDensity(InBaseDensity) {}
|
||||||
|
|
||||||
|
EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; }
|
||||||
|
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density; }
|
||||||
|
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||||
|
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::ShaftGeometry; }
|
||||||
|
bool IsAdditive() const override { return false; }
|
||||||
|
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||||
|
|
||||||
|
void Eval(float WorldX, float WorldY, float WorldZ,
|
||||||
|
FVoxelOpSample& InOut) const override
|
||||||
|
{
|
||||||
|
if (Field == nullptr) return;
|
||||||
|
const FShaftFieldSource::FCells& Cells = Field->GetCellsAt(WorldX, WorldY);
|
||||||
|
static thread_local bool bLoggedTemporaryShaftConnectors = false;
|
||||||
|
if (!bLoggedTemporaryShaftConnectors
|
||||||
|
&& FMath::IsNearlyEqual(WorldZ, -700.0f)
|
||||||
|
&& WorldX > -180.0f && WorldX < -170.0f
|
||||||
|
&& WorldY > -48.0f && WorldY < -38.0f)
|
||||||
|
{
|
||||||
|
bLoggedTemporaryShaftConnectors = true;
|
||||||
|
UE_LOG(LogTemp, Display,
|
||||||
|
TEXT("[VoxelForgeExplore][TemporaryShaftConnectors] query=(%.3f,%.3f,%.3f) count=%d"),
|
||||||
|
WorldX, WorldY, WorldZ, Cells.Conns.Num());
|
||||||
|
for (const FShaftFieldSource::FConn& DebugConn : Cells.Conns)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Display,
|
||||||
|
TEXT("[VoxelForgeExplore][TemporaryShaftConnectors] A=(%.3f,%.3f,%.3f) "
|
||||||
|
"B=(%.3f,%.3f,%.3f) radius=%.3f floor=%.3f walk=%d"),
|
||||||
|
DebugConn.A.X, DebugConn.A.Y, DebugConn.A.Z,
|
||||||
|
DebugConn.B.X, DebugConn.B.Y, DebugConn.B.Z,
|
||||||
|
DebugConn.Radius, DebugConn.FloorZ,
|
||||||
|
DebugConn.bWalkableFloor ? 1 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const FShaftFieldSource::FConn& Conn : Cells.Conns)
|
||||||
|
{
|
||||||
|
if (!Conn.bWalkableFloor) continue;
|
||||||
|
VoxelPassageGeometry::VF_ApplyVerticalShaftConnectorAir(
|
||||||
|
InOut.Density, WorldX, WorldY, WorldZ,
|
||||||
|
Conn.A, Conn.B, Conn.Radius, Conn.FloorZ, BaseDensity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override
|
||||||
|
{
|
||||||
|
return Field != nullptr ? EVoxelOpEffect::Both : EVoxelOpEffect::Identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TCHAR* DebugName() const override { return TEXT("ShaftConnectorAirOp"); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
const FShaftFieldSource* Field = nullptr; // NON possédant : la pile possède la source
|
||||||
|
float BaseDensity = 8.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
class FShaftConnectorFloorMod final : public IVoxelDensityOp
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
FShaftConnectorFloorMod(const FShaftFieldSource* InField, float InBaseDensity)
|
||||||
|
: Field(InField), BaseDensity(InBaseDensity) {}
|
||||||
|
|
||||||
|
EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; }
|
||||||
|
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density; }
|
||||||
|
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||||
|
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::ShaftGeometry; }
|
||||||
|
bool IsAdditive() const override { return false; }
|
||||||
|
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||||
|
|
||||||
|
void Eval(float WorldX, float WorldY, float WorldZ,
|
||||||
|
FVoxelOpSample& InOut) const override
|
||||||
|
{
|
||||||
|
if (Field == nullptr) return;
|
||||||
|
const FShaftFieldSource::FCells& Cells = Field->GetCellsAt(WorldX, WorldY);
|
||||||
|
for (const FShaftFieldSource::FConn& Conn : Cells.Conns)
|
||||||
|
{
|
||||||
|
if (!Conn.bWalkableFloor) continue;
|
||||||
|
VoxelPassageGeometry::VF_ApplyVerticalShaftConnectorFloor(
|
||||||
|
InOut.Density, WorldX, WorldY, WorldZ,
|
||||||
|
Conn.A, Conn.B, Conn.Radius, Conn.FloorZ, BaseDensity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override
|
||||||
|
{
|
||||||
|
return Field != nullptr ? EVoxelOpEffect::FillOnly : EVoxelOpEffect::Identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TCHAR* DebugName() const override { return TEXT("ShaftConnectorFloorOp"); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
const FShaftFieldSource* Field = nullptr; // NON possédant : la pile possède la source
|
||||||
|
float BaseDensity = 8.0f;
|
||||||
|
};
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// RÔLE 1 — SOURCE : ÎLES FLOTTANTES / FLOATING ISLAND BLOBS
|
// RÔLE 1 — SOURCE : ÎLES FLOTTANTES / FLOATING ISLAND BLOBS
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
@@ -2774,6 +2857,12 @@ namespace
|
|||||||
/** La salle de SDF minimal pour le dernier voxel évalué. -1 = aucune. */
|
/** La salle de SDF minimal pour le dernier voxel évalué. -1 = aucune. */
|
||||||
int32 NearestRoom = -1;
|
int32 NearestRoom = -1;
|
||||||
|
|
||||||
|
/** The warped SDF-space position belonging to the current voxel. */
|
||||||
|
FVector LastWarpedPosition = FVector::ZeroVector;
|
||||||
|
|
||||||
|
/** The unwarped position belonging to the current voxel. */
|
||||||
|
FVector LastWorldPosition = FVector::ZeroVector;
|
||||||
|
|
||||||
/** ÉTAPE C1 — les params de la strate avec l'op de CETTE salle appliqué par-dessus.
|
/** ÉTAPE C1 — les params de la strate avec l'op de CETTE salle appliqué par-dessus.
|
||||||
* Mémo par voxel : invalidé au début de chaque `Eval`, calculé au PREMIER modificateur
|
* Mémo par voxel : invalidé au début de chaque `Eval`, calculé au PREMIER modificateur
|
||||||
* qui le demande. C'est ce qui reproduit le coût de l'original (une copie de struct par
|
* qui le demande. C'est ce qui reproduit le coût de l'original (une copie de struct par
|
||||||
@@ -2795,6 +2884,18 @@ namespace
|
|||||||
* Lu par les arches, les dômes, le pincement et le biais de sol. */
|
* Lu par les arches, les dômes, le pincement et le biais de sol. */
|
||||||
int32 GetNearestRoomIdx() const { return State().NearestRoom; }
|
int32 GetNearestRoomIdx() const { return State().NearestRoom; }
|
||||||
|
|
||||||
|
bool HasTunnelAirGeometry() const
|
||||||
|
{
|
||||||
|
return P.RoomDensity > 0.0f && P.RoomSpacing > 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
float GetTunnelCoreSDF() const
|
||||||
|
{
|
||||||
|
const FVector& Position = State().LastWorldPosition;
|
||||||
|
return VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF(
|
||||||
|
Position.X, Position.Y, Position.Z, State().Cache);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ÉTAPE C1 — L'OVERRIDE D'OP PAR SALLE. Les params de la strate avec l'op de terrain tiré
|
* ÉTAPE C1 — L'OVERRIDE D'OP PAR SALLE. Les params de la strate avec l'op de terrain tiré
|
||||||
* pour la salle la plus proche appliqué par-dessus. **ONZE des douze modificateurs de détail
|
* pour la salle la plus proche appliqué par-dessus. **ONZE des douze modificateurs de détail
|
||||||
@@ -2875,6 +2976,7 @@ namespace
|
|||||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||||
{
|
{
|
||||||
FState& S = State();
|
FState& S = State();
|
||||||
|
S.LastWorldPosition = FVector(WorldX, WorldY, WorldZ);
|
||||||
|
|
||||||
// ⚠️ REMIS À -1 INCONDITIONNELLEMENT, ce que l'original ne fait pas : chez lui
|
// ⚠️ REMIS À -1 INCONDITIONNELLEMENT, ce que l'original ne fait pas : chez lui
|
||||||
// `NearestRoomIdx` est un `thread_local` qui, quand `RoomDensity <= 0`, garde la valeur
|
// `NearestRoomIdx` est un `thread_local` qui, quand `RoomDensity <= 0`, garde la valeur
|
||||||
@@ -2919,6 +3021,7 @@ namespace
|
|||||||
WorldY * WF + 9.7f,
|
WorldY * WF + 9.7f,
|
||||||
EffectiveZ * WF + VoxelHash::SeedOffset(SeedU, 0.41f))) * VOXEL_NOISE_SCALE * WS;
|
EffectiveZ * WF + VoxelHash::SeedOffset(SeedU, 0.41f))) * VOXEL_NOISE_SCALE * WS;
|
||||||
}
|
}
|
||||||
|
S.LastWarpedPosition = FVector(WarpedX, WarpedY, WarpedZ);
|
||||||
|
|
||||||
//---------------------------------------------------------------
|
//---------------------------------------------------------------
|
||||||
// LE CACHE PAR BOÎTE DE RECHERCHE
|
// LE CACHE PAR BOÎTE DE RECHERCHE
|
||||||
@@ -3388,37 +3491,61 @@ namespace
|
|||||||
|
|
||||||
for (const FCachedTunnel& Tunnel : B.Cache.Tunnels)
|
for (const FCachedTunnel& Tunnel : B.Cache.Tunnels)
|
||||||
{
|
{
|
||||||
const float MaxRadius = FMath::Max3(
|
float MaxRadius = FMath::Max(
|
||||||
FMath::Abs(Tunnel.RadiusA), FMath::Abs(Tunnel.RadiusB),
|
FMath::Abs(Tunnel.RadiusA), FMath::Abs(Tunnel.RadiusB));
|
||||||
Tunnel.bHasMidpoint ? FMath::Abs(Tunnel.RadiusMid) : 0.0f);
|
float TunnelLower = FLT_MAX;
|
||||||
const FVector SegmentAMin(
|
if (Tunnel.ControlPoints.Num() >= 2
|
||||||
FMath::Min(Tunnel.EndpointA.X, Tunnel.bHasMidpoint ? Tunnel.Midpoint.X : Tunnel.EndpointB.X),
|
&& Tunnel.ControlRadii.Num() == Tunnel.ControlPoints.Num())
|
||||||
FMath::Min(Tunnel.EndpointA.Y, Tunnel.bHasMidpoint ? Tunnel.Midpoint.Y : Tunnel.EndpointB.Y),
|
|
||||||
FMath::Min(Tunnel.EndpointA.Z, Tunnel.bHasMidpoint ? Tunnel.Midpoint.Z : Tunnel.EndpointB.Z));
|
|
||||||
const FVector SegmentAMax(
|
|
||||||
FMath::Max(Tunnel.EndpointA.X, Tunnel.bHasMidpoint ? Tunnel.Midpoint.X : Tunnel.EndpointB.X),
|
|
||||||
FMath::Max(Tunnel.EndpointA.Y, Tunnel.bHasMidpoint ? Tunnel.Midpoint.Y : Tunnel.EndpointB.Y),
|
|
||||||
FMath::Max(Tunnel.EndpointA.Z, Tunnel.bHasMidpoint ? Tunnel.Midpoint.Z : Tunnel.EndpointB.Z));
|
|
||||||
|
|
||||||
float TunnelLower = VF_DistanceBetweenBoxes(
|
|
||||||
QMin, QMax, SegmentAMin, SegmentAMax) - MaxRadius;
|
|
||||||
if (Tunnel.bHasMidpoint)
|
|
||||||
{
|
{
|
||||||
const FVector SegmentBMin(
|
for (int32 SegmentIndex = 0;
|
||||||
FMath::Min(Tunnel.Midpoint.X, Tunnel.EndpointB.X),
|
SegmentIndex + 1 < Tunnel.ControlPoints.Num();
|
||||||
FMath::Min(Tunnel.Midpoint.Y, Tunnel.EndpointB.Y),
|
++SegmentIndex)
|
||||||
FMath::Min(Tunnel.Midpoint.Z, Tunnel.EndpointB.Z));
|
{
|
||||||
const FVector SegmentBMax(
|
const FVector& A = Tunnel.ControlPoints[SegmentIndex];
|
||||||
FMath::Max(Tunnel.Midpoint.X, Tunnel.EndpointB.X),
|
const FVector& BPoint = Tunnel.ControlPoints[SegmentIndex + 1];
|
||||||
FMath::Max(Tunnel.Midpoint.Y, Tunnel.EndpointB.Y),
|
MaxRadius = FMath::Max(
|
||||||
FMath::Max(Tunnel.Midpoint.Z, Tunnel.EndpointB.Z));
|
MaxRadius,
|
||||||
|
FMath::Max(FMath::Abs(Tunnel.ControlRadii[SegmentIndex]),
|
||||||
|
FMath::Abs(Tunnel.ControlRadii[SegmentIndex + 1])));
|
||||||
|
const FVector SegmentMin(
|
||||||
|
FMath::Min(A.X, BPoint.X),
|
||||||
|
FMath::Min(A.Y, BPoint.Y),
|
||||||
|
FMath::Min(A.Z, BPoint.Z));
|
||||||
|
const FVector SegmentMax(
|
||||||
|
FMath::Max(A.X, BPoint.X),
|
||||||
|
FMath::Max(A.Y, BPoint.Y),
|
||||||
|
FMath::Max(A.Z, BPoint.Z));
|
||||||
TunnelLower = FMath::Min(
|
TunnelLower = FMath::Min(
|
||||||
TunnelLower,
|
TunnelLower,
|
||||||
VF_DistanceBetweenBoxes(QMin, QMax, SegmentBMin, SegmentBMax) - MaxRadius);
|
VF_DistanceBetweenBoxes(QMin, QMax, SegmentMin, SegmentMax));
|
||||||
|
}
|
||||||
|
TunnelLower -= MaxRadius;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const FVector SegmentMin(
|
||||||
|
FMath::Min(Tunnel.EndpointA.X, Tunnel.EndpointB.X),
|
||||||
|
FMath::Min(Tunnel.EndpointA.Y, Tunnel.EndpointB.Y),
|
||||||
|
FMath::Min(Tunnel.EndpointA.Z, Tunnel.EndpointB.Z));
|
||||||
|
const FVector SegmentMax(
|
||||||
|
FMath::Max(Tunnel.EndpointA.X, Tunnel.EndpointB.X),
|
||||||
|
FMath::Max(Tunnel.EndpointA.Y, Tunnel.EndpointB.Y),
|
||||||
|
FMath::Max(Tunnel.EndpointA.Z, Tunnel.EndpointB.Z));
|
||||||
|
TunnelLower = VF_DistanceBetweenBoxes(
|
||||||
|
QMin, QMax, SegmentMin, SegmentMax) - MaxRadius;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FVector NoWarpSegmentMin(
|
||||||
|
FMath::Min(Tunnel.EndpointA.X, Tunnel.EndpointB.X),
|
||||||
|
FMath::Min(Tunnel.EndpointA.Y, Tunnel.EndpointB.Y),
|
||||||
|
FMath::Min(Tunnel.EndpointA.Z, Tunnel.EndpointB.Z));
|
||||||
|
const FVector NoWarpSegmentMax(
|
||||||
|
FMath::Max(Tunnel.EndpointA.X, Tunnel.EndpointB.X),
|
||||||
|
FMath::Max(Tunnel.EndpointA.Y, Tunnel.EndpointB.Y),
|
||||||
|
FMath::Max(Tunnel.EndpointA.Z, Tunnel.EndpointB.Z));
|
||||||
const float TunnelLowerNoWarp = VF_DistanceBetweenBoxes(
|
const float TunnelLowerNoWarp = VF_DistanceBetweenBoxes(
|
||||||
NoWarpMin, NoWarpMax, SegmentAMin, SegmentAMax) - MaxRadius;
|
NoWarpMin, NoWarpMax, NoWarpSegmentMin, NoWarpSegmentMax)
|
||||||
|
- MaxRadius;
|
||||||
Consider(TunnelLower);
|
Consider(TunnelLower);
|
||||||
CountThreshold(TunnelLower, B.HitTunnels);
|
CountThreshold(TunnelLower, B.HitTunnels);
|
||||||
CountThreshold(TunnelLowerNoWarp, B.HitTunnelsNoWarp);
|
CountThreshold(TunnelLowerNoWarp, B.HitTunnelsNoWarp);
|
||||||
@@ -3482,6 +3609,13 @@ namespace
|
|||||||
|
|
||||||
const TCHAR* DebugName() const override { return TEXT("RoomGraphSource"); }
|
const TCHAR* DebugName() const override { return TEXT("RoomGraphSource"); }
|
||||||
|
|
||||||
|
bool IsTunnelSupportFloor() const
|
||||||
|
{
|
||||||
|
const FVector& Position = State().LastWorldPosition;
|
||||||
|
return VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
|
||||||
|
Position.X, Position.Y, Position.Z, State().Cache);
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
FStrateGenerationParams P;
|
FStrateGenerationParams P;
|
||||||
int32 Seed;
|
int32 Seed;
|
||||||
@@ -3491,6 +3625,99 @@ namespace
|
|||||||
uint32 LayoutVersion = 0;
|
uint32 LayoutVersion = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The graph tunnel floor closes the rounded bottom of the core into a finite, walkable support
|
||||||
|
// slab. It is separate from the inter-strate passage floor: this owns only the wandering
|
||||||
|
// intra-strate graph edges and does not create any new corridor.
|
||||||
|
class FCaveTunnelFloorOp final : public IVoxelDensityOp
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
FCaveTunnelFloorOp(const FRoomGraphSource* InRooms, float InBase)
|
||||||
|
: Rooms(InRooms), Base(InBase)
|
||||||
|
{}
|
||||||
|
|
||||||
|
EVoxelOpRole GetRole() const override { return EVoxelOpRole::StructuralPost; }
|
||||||
|
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::None; }
|
||||||
|
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||||
|
EVoxelOpResourceMask RequiredResources() const override
|
||||||
|
{
|
||||||
|
return VoxelOpResources::RoomGeometry;
|
||||||
|
}
|
||||||
|
bool IsAdditive() const override { return false; }
|
||||||
|
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||||
|
|
||||||
|
void Eval(float, float, float, FVoxelOpSample& InOut) const override
|
||||||
|
{
|
||||||
|
if (Rooms != nullptr && Rooms->IsTunnelSupportFloor())
|
||||||
|
{
|
||||||
|
InOut.Density = FMath::Max(
|
||||||
|
InOut.Density,
|
||||||
|
FMath::Max(Base * 2.0f, 1.0f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override
|
||||||
|
{
|
||||||
|
return Rooms != nullptr && Rooms->HasTunnelAirGeometry()
|
||||||
|
? EVoxelOpEffect::Both
|
||||||
|
: EVoxelOpEffect::Identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TCHAR* DebugName() const override { return TEXT("CaveTunnelFloorOp"); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
const FRoomGraphSource* Rooms = nullptr;
|
||||||
|
float Base = 8.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A graph tunnel's raw SDF is the structural air contract for the wandering chain. It runs
|
||||||
|
// after the passage post because a different passage can project a solid support floor onto
|
||||||
|
// the same voxel; the core keeps its own half-voxel support band and restores the route above
|
||||||
|
// it. FRoomGraphSource evaluates this post against the cached world-space chain, so the cave
|
||||||
|
// warp cannot turn a walkable floor ramp into a vertical break.
|
||||||
|
class FCaveTunnelAirOp final : public IVoxelDensityOp
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
FCaveTunnelAirOp(const FRoomGraphSource* InRooms, float InBase, float InInset)
|
||||||
|
: Rooms(InRooms), Base(InBase), Inset(InInset)
|
||||||
|
{}
|
||||||
|
|
||||||
|
EVoxelOpRole GetRole() const override { return EVoxelOpRole::StructuralPost; }
|
||||||
|
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density; }
|
||||||
|
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||||
|
EVoxelOpResourceMask RequiredResources() const override
|
||||||
|
{
|
||||||
|
return VoxelOpResources::RoomGeometry;
|
||||||
|
}
|
||||||
|
bool IsAdditive() const override { return false; }
|
||||||
|
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||||
|
|
||||||
|
void Eval(float, float, float, FVoxelOpSample& InOut) const override
|
||||||
|
{
|
||||||
|
if (Rooms != nullptr
|
||||||
|
&& !Rooms->IsTunnelSupportFloor()
|
||||||
|
&& Rooms->GetTunnelCoreSDF() < -Inset)
|
||||||
|
{
|
||||||
|
InOut.Density = FMath::Min(
|
||||||
|
InOut.Density,
|
||||||
|
-FMath::Max(Base * 2.0f, 1.0f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override
|
||||||
|
{
|
||||||
|
return Rooms != nullptr && Rooms->HasTunnelAirGeometry()
|
||||||
|
? EVoxelOpEffect::CarveOnly
|
||||||
|
: EVoxelOpEffect::Identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TCHAR* DebugName() const override { return TEXT("CaveTunnelAirOp"); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
const FRoomGraphSource* Rooms = nullptr;
|
||||||
|
float Base = 8.0f;
|
||||||
|
float Inset = 2.0f;
|
||||||
|
};
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// LE GATE DE BOÎTE DES MODIFICATEURS DE CAVE
|
// LE GATE DE BOÎTE DES MODIFICATEURS DE CAVE
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
@@ -5215,7 +5442,8 @@ bool FVoxelOpStack::ValidateChannelOrder(FString* OutError) const
|
|||||||
|
|
||||||
void FVoxelOpStack::AppendStructuralPost(float StrateTopWorldZ, float StrateBottomWorldZ,
|
void FVoxelOpStack::AppendStructuralPost(float StrateTopWorldZ, float StrateBottomWorldZ,
|
||||||
float SealThickness, float BaseDensity, float SpineRadius,
|
float SealThickness, float BaseDensity, float SpineRadius,
|
||||||
const UVoxelStrateManager* StrateManager)
|
const UVoxelStrateManager* StrateManager,
|
||||||
|
bool bAppendEdgeSeal)
|
||||||
{
|
{
|
||||||
// ORDRE NON NÉGOCIABLE : la spine creuse l'intérieur (et ne touche JAMAIS les bandes de seal),
|
// ORDRE NON NÉGOCIABLE : la spine creuse l'intérieur (et ne touche JAMAIS les bandes de seal),
|
||||||
// le seal vertical re-solidifie ses bandes, les passages (tube + landing + support) percent
|
// le seal vertical re-solidifie ses bandes, les passages (tube + landing + support) percent
|
||||||
@@ -5226,7 +5454,10 @@ void FVoxelOpStack::AppendStructuralPost(float StrateTopWorldZ, float StrateBott
|
|||||||
Add(MakeUnique<FBoundarySealOp>(StrateTopWorldZ, StrateBottomWorldZ, SealThickness, BaseDensity));
|
Add(MakeUnique<FBoundarySealOp>(StrateTopWorldZ, StrateBottomWorldZ, SealThickness, BaseDensity));
|
||||||
Add(MakeUnique<FPassageCarveOp>(StrateManager, BaseDensity, SealThickness,
|
Add(MakeUnique<FPassageCarveOp>(StrateManager, BaseDensity, SealThickness,
|
||||||
StrateTopWorldZ, StrateBottomWorldZ, SpineRadius));
|
StrateTopWorldZ, StrateBottomWorldZ, SpineRadius));
|
||||||
|
if (bAppendEdgeSeal)
|
||||||
|
{
|
||||||
Add(MakeUnique<FXYEdgeSealOp>(BaseDensity));
|
Add(MakeUnique<FXYEdgeSealOp>(BaseDensity));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
@@ -5466,6 +5697,8 @@ namespace VoxelDensityOps
|
|||||||
OutStack.Add(MakeSdfRoughnessMod(P.SurfaceRoughness, 0.1f, 3, P.SurfaceRoughness + 4.0f));
|
OutStack.Add(MakeSdfRoughnessMod(P.SurfaceRoughness, 0.1f, 3, P.SurfaceRoughness + 4.0f));
|
||||||
OutStack.Add(MakeSdfCarve(CarveBlend, P.BaseDensity));
|
OutStack.Add(MakeSdfCarve(CarveBlend, P.BaseDensity));
|
||||||
OutStack.Add(MakeUnique<FShaftLedgeMod>(P, ShaftPtr));
|
OutStack.Add(MakeUnique<FShaftLedgeMod>(P, ShaftPtr));
|
||||||
|
OutStack.Add(MakeUnique<FShaftConnectorAirMod>(ShaftPtr, P.BaseDensity));
|
||||||
|
OutStack.Add(MakeUnique<FShaftConnectorFloorMod>(ShaftPtr, P.BaseDensity));
|
||||||
|
|
||||||
if (bAppendStructuralPosts)
|
if (bAppendStructuralPosts)
|
||||||
{
|
{
|
||||||
@@ -5552,7 +5785,12 @@ namespace VoxelDensityOps
|
|||||||
{
|
{
|
||||||
OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ,
|
OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ,
|
||||||
P.BoundarySealThickness, P.BaseDensity,
|
P.BoundarySealThickness, P.BaseDensity,
|
||||||
SpineRadius, StrateManager);
|
SpineRadius, StrateManager, false);
|
||||||
|
OutStack.Add(MakeUnique<FCaveTunnelFloorOp>(RoomPtr, P.BaseDensity));
|
||||||
|
OutStack.Add(MakeUnique<FCaveTunnelAirOp>(
|
||||||
|
RoomPtr, P.BaseDensity,
|
||||||
|
VoxelPassageGeometry::CaveTunnelAirCoreInsetVoxels));
|
||||||
|
OutStack.Add(MakeUnique<FXYEdgeSealOp>(P.BaseDensity));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -230,44 +230,6 @@ static float CellularNoise3D(const FVector& Position)
|
|||||||
return VoxelNoise::Cellular3D(Position);
|
return VoxelNoise::Cellular3D(Position);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Structural tree capsules must meet shaft axes without landing inside a ledge band. A single
|
|
||||||
// deterministic safe interval is enough for the whole tree: every shaft in one strate shares the
|
|
||||||
// same ledge phase, and keeping every tree link in that interval also keeps crossings of unrelated
|
|
||||||
// shafts out of their additive ledges. The fallback is only for malformed settings with no safe
|
|
||||||
// interval; the stock params always take the proven branch.
|
|
||||||
static float VF_SelectVerticalTreeConnectorZ(
|
|
||||||
const FVerticalShaftParams& Params,
|
|
||||||
float BottomZ,
|
|
||||||
float TopZ,
|
|
||||||
uint32 LinkHash)
|
|
||||||
{
|
|
||||||
if (Params.LedgeSpacing > 0.0f && Params.LedgeDepth > 0.0f)
|
|
||||||
{
|
|
||||||
const float Period = Params.LedgeSpacing;
|
|
||||||
const float RelativeBottom = BottomZ - Params.StrateBottomWorldZ;
|
|
||||||
const float RelativeTop = TopZ - Params.StrateBottomWorldZ;
|
|
||||||
const int32 FirstPeriod = FMath::FloorToInt(RelativeBottom / Period);
|
|
||||||
const float Random01 = VoxelHash::ToFloat01(VoxelHash::Mix(LinkHash));
|
|
||||||
|
|
||||||
for (int32 PeriodOffset = 0; PeriodOffset <= 1; ++PeriodOffset)
|
|
||||||
{
|
|
||||||
const float PeriodStart = static_cast<float>(FirstPeriod + PeriodOffset) * Period;
|
|
||||||
const float SafeStart = FMath::Max(
|
|
||||||
RelativeBottom, PeriodStart + Params.LedgeDepth + 0.01f);
|
|
||||||
const float SafeEnd = FMath::Min(
|
|
||||||
RelativeTop, PeriodStart + Period - Params.LedgeDepth - 0.01f);
|
|
||||||
if (SafeEnd > SafeStart)
|
|
||||||
{
|
|
||||||
return Params.StrateBottomWorldZ
|
|
||||||
+ FMath::Lerp(SafeStart, SafeEnd, Random01);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return FMath::Lerp(BottomZ, TopZ,
|
|
||||||
VoxelHash::ToFloat01(VoxelHash::Mix(LinkHash)));
|
|
||||||
}
|
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// DENSITY PIPELINE HELPERS (partagés entre TunnelNetwork et Slab)
|
// DENSITY PIPELINE HELPERS (partagés entre TunnelNetwork et Slab)
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
@@ -309,7 +271,7 @@ static FORCEINLINE void ApplyOriginLandingFloor(float& Density,
|
|||||||
// it works uniformly for every generator type. Stays inside the seal bands so it can
|
// it works uniformly for every generator type. Stays inside the seal bands so it can
|
||||||
// never breach a strate boundary. All features are hash-placed and deterministic.
|
// never breach a strate boundary. All features are hash-placed and deterministic.
|
||||||
static void ApplyDisturbances(float& MC, float X, float Y, float Z,
|
static void ApplyDisturbances(float& MC, float X, float Y, float Z,
|
||||||
const FStrateDisturbanceParams& D, uint32 Seed)
|
const FStrateDisturbanceParams& D, uint32 Seed, bool bProtectVerticalShaftAir)
|
||||||
{
|
{
|
||||||
const float InnerTop = D.StrateTopWorldZ - D.BoundarySealThickness;
|
const float InnerTop = D.StrateTopWorldZ - D.BoundarySealThickness;
|
||||||
const float InnerBot = D.StrateBottomWorldZ + D.BoundarySealThickness;
|
const float InnerBot = D.StrateBottomWorldZ + D.BoundarySealThickness;
|
||||||
@@ -418,9 +380,12 @@ static void ApplyDisturbances(float& MC, float X, float Y, float Z,
|
|||||||
{
|
{
|
||||||
float f = FMath::Clamp((Blend - sdf) / (Blend * 2.0f), 0.0f, 1.0f);
|
float f = FMath::Clamp((Blend - sdf) / (Blend * 2.0f), 0.0f, 1.0f);
|
||||||
f = SmoothStep01(f);
|
f = SmoothStep01(f);
|
||||||
|
if (!bProtectVerticalShaftAir)
|
||||||
|
{
|
||||||
MC = FMath::Min(MC, -f * Solid); // force solid
|
MC = FMath::Min(MC, -f * Solid); // force solid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- RIDGES: thin solid blades rising from the floor ---
|
// --- RIDGES: thin solid blades rising from the floor ---
|
||||||
if (D.RidgeDensity > 0.0f && D.RidgeSpacing > 0.0f && D.RidgeHeight > 0.0f)
|
if (D.RidgeDensity > 0.0f && D.RidgeSpacing > 0.0f && D.RidgeHeight > 0.0f)
|
||||||
@@ -465,7 +430,10 @@ static void ApplyDisturbances(float& MC, float X, float Y, float Z,
|
|||||||
f = SmoothStep01(f) * zFade;
|
f = SmoothStep01(f) * zFade;
|
||||||
best = FMath::Max(best, f);
|
best = FMath::Max(best, f);
|
||||||
}
|
}
|
||||||
if (best > 0.0f) MC = FMath::Min(MC, -best * Solid); // force solid
|
if (best > 0.0f && !bProtectVerticalShaftAir)
|
||||||
|
{
|
||||||
|
MC = FMath::Min(MC, -best * Solid); // force solid
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -498,6 +466,57 @@ namespace
|
|||||||
// address. Relaxed ordering is sufficient: this allocates uniqueness, it publishes no data.
|
// address. Relaxed ordering is sufficient: this allocates uniqueness, it publishes no data.
|
||||||
std::atomic<uint64> GNextDensityCacheOwnerId { 0 };
|
std::atomic<uint64> GNextDensityCacheOwnerId { 0 };
|
||||||
|
|
||||||
|
struct FTunnelCoreCacheState
|
||||||
|
{
|
||||||
|
FChunkSDFCache Cache;
|
||||||
|
uint64 OwnerId = 0;
|
||||||
|
FIntVector Chunk = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX);
|
||||||
|
uint32 ParamsFingerprint = 0xFFFFFFFFu;
|
||||||
|
uint32 LayoutVersion = 0xFFFFFFFFu;
|
||||||
|
bool bValid = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
static thread_local FTunnelCoreCacheState GTunnelCoreCache;
|
||||||
|
void PrepareTunnelCoreCache(
|
||||||
|
const UVoxelStrateManager& Manager,
|
||||||
|
const FIntVector& ChunkCoord,
|
||||||
|
const FStrateGenerationParams& Params,
|
||||||
|
uint32 ParamsFingerprint,
|
||||||
|
uint32 LayoutVersion,
|
||||||
|
uint64 OwnerId,
|
||||||
|
FTunnelCoreCacheState& OutState)
|
||||||
|
{
|
||||||
|
if (OutState.bValid
|
||||||
|
&& OutState.OwnerId == OwnerId
|
||||||
|
&& OutState.Chunk == ChunkCoord
|
||||||
|
&& OutState.ParamsFingerprint == ParamsFingerprint
|
||||||
|
&& OutState.LayoutVersion == LayoutVersion)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float ChunkMinX = static_cast<float>(ChunkCoord.X * CHUNK_SIZE);
|
||||||
|
const float ChunkMinY = static_cast<float>(ChunkCoord.Y * CHUNK_SIZE);
|
||||||
|
const float ChunkMaxX = ChunkMinX + static_cast<float>(CHUNK_SIZE);
|
||||||
|
const float ChunkMaxY = ChunkMinY + static_cast<float>(CHUNK_SIZE);
|
||||||
|
const float Expansion = FMath::Max(Params.CaveWarpStrength, 0.0f) + 2.0f;
|
||||||
|
const int32 StrateIndex = Manager.GetStrateIndex(
|
||||||
|
(static_cast<float>(ChunkCoord.Z) + 0.5f)
|
||||||
|
* static_cast<float>(CHUNK_SIZE) * VOXEL_SIZE);
|
||||||
|
|
||||||
|
VoxelCaveMorphology::BuildChunkCache(
|
||||||
|
OutState.Cache,
|
||||||
|
ChunkMinX - Expansion, ChunkMinY - Expansion,
|
||||||
|
ChunkMaxX + Expansion, ChunkMaxY + Expansion,
|
||||||
|
Params, static_cast<uint32>(Manager.GetWorldSeed()),
|
||||||
|
StrateIndex, nullptr);
|
||||||
|
OutState.OwnerId = OwnerId;
|
||||||
|
OutState.Chunk = ChunkCoord;
|
||||||
|
OutState.ParamsFingerprint = ParamsFingerprint;
|
||||||
|
OutState.LayoutVersion = LayoutVersion;
|
||||||
|
OutState.bValid = true;
|
||||||
|
}
|
||||||
|
|
||||||
struct FVoxelStackParamRefs
|
struct FVoxelStackParamRefs
|
||||||
{
|
{
|
||||||
const FSlabGenerationParams* Slab = nullptr;
|
const FSlabGenerationParams* Slab = nullptr;
|
||||||
@@ -673,6 +692,10 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
|||||||
// Query per-chunk params from the manager so each strate has different caves.
|
// Query per-chunk params from the manager so each strate has different caves.
|
||||||
float Result;
|
float Result;
|
||||||
|
|
||||||
|
// A vertical tree connector may need to stay open through the shared disturbance and
|
||||||
|
// structural-post tail below. This is one bit for this density call, not cached voxel data.
|
||||||
|
VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker();
|
||||||
|
|
||||||
FIntVector ChunkCoord(
|
FIntVector ChunkCoord(
|
||||||
FMath::FloorToInt(WorldX / CHUNK_SIZE),
|
FMath::FloorToInt(WorldX / CHUNK_SIZE),
|
||||||
FMath::FloorToInt(WorldY / CHUNK_SIZE),
|
FMath::FloorToInt(WorldY / CHUNK_SIZE),
|
||||||
@@ -831,6 +854,23 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
|||||||
// silently fall back to the old one-archetype switch.
|
// silently fall back to the old one-archetype switch.
|
||||||
CP_UseOpStack = CP_UseOpStack || CP_UseComposerRegions;
|
CP_UseOpStack = CP_UseOpStack || CP_UseComposerRegions;
|
||||||
#endif
|
#endif
|
||||||
|
bool bUseNativeTunnelCore = !CP_UseCustomRecipe
|
||||||
|
&& (CP_GenType == ECaveGeneratorType::TunnelNetwork
|
||||||
|
|| CP_GenType == ECaveGeneratorType::Underwater);
|
||||||
|
#if WITH_EDITOR
|
||||||
|
bUseNativeTunnelCore = bUseNativeTunnelCore && !CP_UseComposerRegions;
|
||||||
|
#endif
|
||||||
|
if (bUseNativeTunnelCore)
|
||||||
|
{
|
||||||
|
PrepareTunnelCoreCache(
|
||||||
|
*StrateManager, ChunkCoord, CP_Tunnel, CP_TunnelFP,
|
||||||
|
LayoutVersion, DensityCacheOwnerId, GTunnelCoreCache);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GTunnelCoreCache.bValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (CP_UseOpStack)
|
if (CP_UseOpStack)
|
||||||
{
|
{
|
||||||
CP_OpStack = FVoxelOpStack(); // move-assign : libère l'ancienne pile
|
CP_OpStack = FVoxelOpStack(); // move-assign : libère l'ancienne pile
|
||||||
@@ -997,8 +1037,13 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
|||||||
CP_TunnelFP, LayoutVersion); break;
|
CP_TunnelFP, LayoutVersion); break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disturbance layer (the "wow" post-process) — cached params, MC convention.
|
// Disturbance layer (the "wow" post-process) — cached params, MC convention. The
|
||||||
ApplyDisturbances(Result, WorldX, WorldY, WorldZ, CP_Dist, (uint32)Seed);
|
// vertical shaft tree opens a walkable route before this shared pass, so a bridge/ridge
|
||||||
|
// must not refill it. Both native and operator-stack paths use the same marker helper.
|
||||||
|
const bool bProtectVerticalShaftAir =
|
||||||
|
VoxelPassageGeometry::VerticalShaftConnectorAirMarker();
|
||||||
|
ApplyDisturbances(Result, WorldX, WorldY, WorldZ, CP_Dist, (uint32)Seed,
|
||||||
|
bProtectVerticalShaftAir);
|
||||||
|
|
||||||
// A disturbance is allowed to add visual detail, but it must not refill the landing's
|
// A disturbance is allowed to add visual detail, but it must not refill the landing's
|
||||||
// measured air volume or turn its support slab back into a hole. Reassert the landing
|
// measured air volume or turn its support slab back into a hole. Reassert the landing
|
||||||
@@ -1103,6 +1148,45 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
|||||||
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
|
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
|
||||||
StrateManager->ApplyPassageLandingRoomFloorMC(
|
StrateManager->ApplyPassageLandingRoomFloorMC(
|
||||||
Result, WorldX, WorldY, WorldZ, LandingBaseDensity);
|
Result, WorldX, WorldY, WorldZ, LandingBaseDensity);
|
||||||
|
|
||||||
|
// Disturbance features are authored as a generic MC-space post and may add a ridge or
|
||||||
|
// bridge over a graph tunnel. Reassert the native cached tunnel core here, after every
|
||||||
|
// solid floor writer but before the global XY seal. This cache is built once per chunk,
|
||||||
|
// never once per voxel.
|
||||||
|
if (GTunnelCoreCache.bValid)
|
||||||
|
{
|
||||||
|
const bool bTunnelSupportFloor =
|
||||||
|
VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
|
||||||
|
WorldX, WorldY, WorldZ, GTunnelCoreCache.Cache);
|
||||||
|
if (bTunnelSupportFloor)
|
||||||
|
{
|
||||||
|
Result = FMath::Min(
|
||||||
|
Result,
|
||||||
|
-FMath::Max(LandingBaseDensity * 2.0f, 1.0f));
|
||||||
|
}
|
||||||
|
const float CoreSDF = VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF(
|
||||||
|
WorldX, WorldY, WorldZ, GTunnelCoreCache.Cache);
|
||||||
|
if (!bTunnelSupportFloor
|
||||||
|
&& CoreSDF < -VoxelPassageGeometry::CaveTunnelAirCoreInsetVoxels)
|
||||||
|
{
|
||||||
|
Result = FMath::Max(
|
||||||
|
Result,
|
||||||
|
FMath::Max(LandingBaseDensity * 2.0f, 1.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The graph tunnel can overlap an inter-strate landing at a room mouth. Its air
|
||||||
|
// backstop is allowed to reopen the tunnel, but the landing's proved support floor
|
||||||
|
// must own the final floor band; otherwise the graph post can erase the only support
|
||||||
|
// surface at the mouth and leave the player-fit graph with a disconnected pocket.
|
||||||
|
VF_ApplyOriginLandingFloorMC(
|
||||||
|
Result, WorldX, WorldY, WorldZ,
|
||||||
|
CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ,
|
||||||
|
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
|
||||||
|
StrateManager->ApplyPassageLandingFloorMC(
|
||||||
|
Result, WorldX, WorldY, WorldZ, LandingBaseDensity);
|
||||||
|
StrateManager->ApplyPassageLandingRoomFloorMC(
|
||||||
|
Result, WorldX, WorldY, WorldZ, LandingBaseDensity);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -1164,6 +1248,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker();
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2262,6 +2347,28 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
|
|||||||
Density, WorldX, WorldY, WorldZ,
|
Density, WorldX, WorldY, WorldZ,
|
||||||
Params.BaseDensity, Params.BoundarySealThickness);
|
Params.BaseDensity, Params.BoundarySealThickness);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Terrain operations and passage support floors are allowed to write solid density, but the
|
||||||
|
// graph's own tunnel floor/core are structural route geometry. Close the rounded bottom first,
|
||||||
|
// then reopen only the core above its half-voxel air clearance. Both predicates are shared
|
||||||
|
// with the operator stack and are evaluated from the per-chunk cache, never per-voxel rebuilt.
|
||||||
|
const bool bTunnelSupportFloor =
|
||||||
|
VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
|
||||||
|
WorldX, WorldY, WorldZ, SDFCache);
|
||||||
|
if (bTunnelSupportFloor)
|
||||||
|
{
|
||||||
|
Density = FMath::Max(Density, FMath::Max(Params.BaseDensity * 2.0f, 1.0f));
|
||||||
|
}
|
||||||
|
const float TunnelCoreSDF = (Params.RoomDensity > 0.0f && Params.RoomSpacing > 0.0f)
|
||||||
|
? VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF(WorldX, WorldY, WorldZ, SDFCache)
|
||||||
|
: FLT_MAX;
|
||||||
|
if (!bTunnelSupportFloor
|
||||||
|
&& TunnelCoreSDF < -VoxelPassageGeometry::CaveTunnelAirCoreInsetVoxels)
|
||||||
|
{
|
||||||
|
Density = FMath::Min(
|
||||||
|
Density,
|
||||||
|
-FMath::Max(Params.BaseDensity * 2.0f, 1.0f));
|
||||||
|
}
|
||||||
ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ,
|
ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ,
|
||||||
Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
|
Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
|
||||||
Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius);
|
Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius);
|
||||||
@@ -4271,7 +4378,13 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
|
|||||||
// them. That matters: a capsule from a child to a parent two or three cells away must be
|
// them. That matters: a capsule from a child to a parent two or three cells away must be
|
||||||
// present in the cells along its segment, not only in the cache centred on the child.
|
// present in the cells along its segment, not only in the cache centred on the child.
|
||||||
struct FLocalShaft { float X, Y, R; int32 CellX, CellY; bool bOriginSpine; };
|
struct FLocalShaft { float X, Y, R; int32 CellX, CellY; bool bOriginSpine; };
|
||||||
struct FLocalConn { FVector A, B; float Radius; };
|
struct FLocalConn
|
||||||
|
{
|
||||||
|
FVector A, B;
|
||||||
|
float Radius = 0.0f;
|
||||||
|
float FloorZ = 0.0f;
|
||||||
|
bool bWalkableFloor = false;
|
||||||
|
};
|
||||||
thread_local TArray<FLocalShaft, TInlineAllocator<10>> Shafts;
|
thread_local TArray<FLocalShaft, TInlineAllocator<10>> Shafts;
|
||||||
thread_local TArray<FLocalConn, TInlineAllocator<32>> Conns;
|
thread_local TArray<FLocalConn, TInlineAllocator<32>> Conns;
|
||||||
thread_local TArray<FLocalShaft, TInlineAllocator<81>> TreeEmitShafts;
|
thread_local TArray<FLocalShaft, TInlineAllocator<81>> TreeEmitShafts;
|
||||||
@@ -4396,17 +4509,26 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
|
|||||||
|
|
||||||
auto EmitTreeConnector = [&](const FLocalShaft& Child, const FLocalShaft* Parent)
|
auto EmitTreeConnector = [&](const FLocalShaft& Child, const FLocalShaft* Parent)
|
||||||
{
|
{
|
||||||
const int32 ParentCellX = Parent != nullptr ? Parent->CellX : 0;
|
const float Zc = VoxelPassageGeometry::VerticalShaftConnectorCenterZ(
|
||||||
const int32 ParentCellY = Parent != nullptr ? Parent->CellY : 0;
|
Params.StrateBottomWorldZ,
|
||||||
const uint32 LinkHash = VoxelHash::Pair(
|
BottomZ,
|
||||||
Child.CellX, Child.CellY, ParentCellX, ParentCellY, S ^ TreeSalt);
|
TopZ,
|
||||||
const float Zc = VF_SelectVerticalTreeConnectorZ(
|
Params.LedgeSpacing,
|
||||||
Params, BottomZ, TopZ, LinkHash);
|
Params.LedgeDepth,
|
||||||
|
TreeConnectorRadius);
|
||||||
|
const float FloorZ = VoxelPassageGeometry::VerticalShaftConnectorFloorZ(
|
||||||
|
Params.StrateBottomWorldZ,
|
||||||
|
BottomZ,
|
||||||
|
TopZ,
|
||||||
|
Params.LedgeSpacing,
|
||||||
|
Params.LedgeDepth,
|
||||||
|
TreeConnectorRadius);
|
||||||
const FVector ParentPoint = Parent != nullptr
|
const FVector ParentPoint = Parent != nullptr
|
||||||
? FVector(Parent->X, Parent->Y, Zc)
|
? FVector(Parent->X, Parent->Y, Zc)
|
||||||
: FVector(0.0f, 0.0f, Zc);
|
: FVector(0.0f, 0.0f, Zc);
|
||||||
const FLocalConn Conn{
|
const FLocalConn Conn{
|
||||||
FVector(Child.X, Child.Y, Zc), ParentPoint, TreeConnectorRadius };
|
FVector(Child.X, Child.Y, Zc), ParentPoint, TreeConnectorRadius,
|
||||||
|
FloorZ, true };
|
||||||
if (ConnectorMayReachCell(Conn))
|
if (ConnectorMayReachCell(Conn))
|
||||||
{
|
{
|
||||||
Conns.Add(Conn);
|
Conns.Add(Conn);
|
||||||
@@ -4531,7 +4653,8 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
|
|||||||
RoughnessReach + 1.0f)
|
RoughnessReach + 1.0f)
|
||||||
: Params.ConnectorRadius;
|
: Params.ConnectorRadius;
|
||||||
const FLocalConn Conn{
|
const FLocalConn Conn{
|
||||||
FVector(A.X, A.Y, Zc), FVector(B.X, B.Y, Zc), ConnectorR };
|
FVector(A.X, A.Y, Zc), FVector(B.X, B.Y, Zc), ConnectorR,
|
||||||
|
0.0f, false };
|
||||||
if (ConnectorMayReachCell(Conn))
|
if (ConnectorMayReachCell(Conn))
|
||||||
{
|
{
|
||||||
Conns.Add(Conn);
|
Conns.Add(Conn);
|
||||||
@@ -4598,6 +4721,29 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The drainage tree is a walkable shaft network, not only a connected air SDF. Reassert its
|
||||||
|
// air core after partial shaft ledges, then emit support bands at the common first ledge level
|
||||||
|
// so every parent edge can be traversed by the player-fit stencil. This remains local to
|
||||||
|
// shaft edges; no landing-to-origin road is added.
|
||||||
|
for (const FLocalConn& C : Conns)
|
||||||
|
{
|
||||||
|
if (C.bWalkableFloor)
|
||||||
|
{
|
||||||
|
VoxelPassageGeometry::VF_ApplyVerticalShaftConnectorAir(
|
||||||
|
Density, WorldX, WorldY, WorldZ,
|
||||||
|
C.A, C.B, C.Radius, C.FloorZ, Params.BaseDensity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const FLocalConn& C : Conns)
|
||||||
|
{
|
||||||
|
if (C.bWalkableFloor)
|
||||||
|
{
|
||||||
|
VoxelPassageGeometry::VF_ApplyVerticalShaftConnectorFloor(
|
||||||
|
Density, WorldX, WorldY, WorldZ,
|
||||||
|
C.A, C.B, C.Radius, C.FloorZ, Params.BaseDensity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ApplyOriginSpine(Density, WorldX, WorldY, WorldZ,
|
ApplyOriginSpine(Density, WorldX, WorldY, WorldZ,
|
||||||
Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
|
Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
|
||||||
Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius);
|
Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius);
|
||||||
|
|||||||
@@ -269,9 +269,10 @@ static bool VF_IsAnyPassageFloorAt(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const FVoxelPassage& Passage = Passages[PassageIndex];
|
const FVoxelPassage& Passage = Passages[PassageIndex];
|
||||||
if (VF_IsPassageLandingFloor(Position, Passage.UpperLanding)
|
if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker()
|
||||||
|
&& (VF_IsPassageLandingFloor(Position, Passage.UpperLanding)
|
||||||
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding)
|
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding)
|
||||||
|| VF_IsWalkableTunnelFloor(Passage, Position))
|
|| VF_IsWalkableTunnelFloor(Passage, Position)))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -826,8 +827,6 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
int32 TotalPassages = 0;
|
int32 TotalPassages = 0;
|
||||||
int32 NumAimedAtUpperPlayerFit = 0;
|
int32 NumAimedAtUpperPlayerFit = 0;
|
||||||
int32 NumAimedAtLowerPlayerFit = 0;
|
int32 NumAimedAtLowerPlayerFit = 0;
|
||||||
int32 NumUpperRootConnectors = 0;
|
|
||||||
int32 NumLowerRootConnectors = 0;
|
|
||||||
TSet<FString> NoQueryArchetypes;
|
TSet<FString> NoQueryArchetypes;
|
||||||
|
|
||||||
const auto ArchetypeName = [](ECaveGeneratorType Archetype)
|
const auto ArchetypeName = [](ECaveGeneratorType Archetype)
|
||||||
@@ -915,39 +914,6 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const auto CommonRootFloorFor = [&](float StrateTopZ, float StrateBottomZ,
|
|
||||||
float BoundarySealThickness) -> float
|
|
||||||
{
|
|
||||||
// The two mouth queries may legitimately return floors near opposite strate faces. 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;
|
|
||||||
const float ConnectorHeight = FMath::Max(
|
|
||||||
12.0f, FVoxelPlayerCapsuleConstants::HeightVoxels + 4.0f);
|
|
||||||
constexpr float PassageBlendSafety = 4.0f;
|
|
||||||
const float MinimumFloor = InnerBottom + 3.0f + PassageBlendSafety;
|
|
||||||
const float MaximumFloor = InnerTop - ConnectorHeight - PassageBlendSafety;
|
|
||||||
if (FMath::IsFinite(MinimumFloor) && FMath::IsFinite(MaximumFloor)
|
|
||||||
&& MaximumFloor >= MinimumFloor)
|
|
||||||
{
|
|
||||||
return 0.5f * (MinimumFloor + MaximumFloor);
|
|
||||||
}
|
|
||||||
return 0.5f * (InnerBottom + InnerTop);
|
|
||||||
};
|
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// INTER-STRATE PASSAGES: tunnels connecting consecutive strates.
|
// INTER-STRATE PASSAGES: tunnels connecting consecutive strates.
|
||||||
// Each passage is randomly assigned one of 5 types, which determines
|
// Each passage is randomly assigned one of 5 types, which determines
|
||||||
@@ -974,6 +940,17 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
|
|
||||||
const float DistLo = FMath::Min(Cfg.DistanceMin, Cfg.DistanceMax);
|
const float DistLo = FMath::Min(Cfg.DistanceMin, Cfg.DistanceMax);
|
||||||
const float DistHi = FMath::Max(Cfg.DistanceMin, Cfg.DistanceMax);
|
const float DistHi = FMath::Max(Cfg.DistanceMin, Cfg.DistanceMax);
|
||||||
|
// The origin room owns the future straight shaft. Keep the progression mouth outside its
|
||||||
|
// carve/blend envelope, then choose a deterministic point in the authored annulus. If an
|
||||||
|
// asset has an invalid inner radius, repair only that radius; the outer authored radius is
|
||||||
|
// preserved unless it is too small to contain the required inner edge.
|
||||||
|
const float OriginLandingHalfWidth =
|
||||||
|
VoxelPassageGeometry::LandingHalfWidthForRadius(OriginSpineRadius);
|
||||||
|
const float MinimumDescentDistance = FMath::Max(
|
||||||
|
OriginLandingHalfWidth + VoxelPassageGeometry::LandingCarveBlendVoxels,
|
||||||
|
VoxelPassageGeometry::MinimumTurnFloorWidthVoxels);
|
||||||
|
const float PlacementLo = FMath::Max(DistLo, MinimumDescentDistance);
|
||||||
|
const float PlacementHi = FMath::Max(DistHi, PlacementLo);
|
||||||
|
|
||||||
const int32 Conns = FMath::Max(0, Cfg.Connections);
|
const int32 Conns = FMath::Max(0, Cfg.Connections);
|
||||||
for (int32 c = 0; c < Conns; c++)
|
for (int32 c = 0; c < Conns; c++)
|
||||||
@@ -1010,9 +987,11 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// PLACEMENT: random angle, distance from the (0,0) spine within config range.
|
// PLACEMENT: random angle, distance from the (0,0) spine within a deterministic
|
||||||
|
// annulus. It is deliberately not the spine: that room is reserved for the future
|
||||||
|
// surface shaft, while this passage opens a separate room in the strate network.
|
||||||
const float Angle = PassageRandom01(PassageSaltAngle) * (2.0f * PI);
|
const float Angle = PassageRandom01(PassageSaltAngle) * (2.0f * PI);
|
||||||
const float Distance = PassageRandomRange(DistLo, DistHi, PassageSaltDistance);
|
const float Distance = PassageRandomRange(PlacementLo, PlacementHi, PassageSaltDistance);
|
||||||
const float PX = FMath::Cos(Angle) * Distance;
|
const float PX = FMath::Cos(Angle) * Distance;
|
||||||
const float PY = FMath::Sin(Angle) * Distance;
|
const float PY = FMath::Sin(Angle) * Distance;
|
||||||
|
|
||||||
@@ -1049,7 +1028,8 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
PX,
|
PX,
|
||||||
PY,
|
PY,
|
||||||
MaxLateralSnapFor(*UpperDef),
|
MaxLateralSnapFor(*UpperDef),
|
||||||
SuggestedUpperPoint);
|
SuggestedUpperPoint,
|
||||||
|
CachedSeed);
|
||||||
|
|
||||||
if (bAimedUpperAtPlayerFitPoint)
|
if (bAimedUpperAtPlayerFitPoint)
|
||||||
{
|
{
|
||||||
@@ -1088,7 +1068,8 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
PX,
|
PX,
|
||||||
PY,
|
PY,
|
||||||
MaxLateralSnap,
|
MaxLateralSnap,
|
||||||
SuggestedLowerPoint);
|
SuggestedLowerPoint,
|
||||||
|
CachedSeed);
|
||||||
|
|
||||||
if (bAimedLowerAtPlayerFitPoint)
|
if (bAimedLowerAtPlayerFitPoint)
|
||||||
{
|
{
|
||||||
@@ -1289,10 +1270,9 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
: FVector::ZeroVector,
|
: FVector::ZeroVector,
|
||||||
PassageRandom01(PassageSaltLowerDoor) * 2.0f * PI);
|
PassageRandom01(PassageSaltLowerDoor) * 2.0f * PI);
|
||||||
|
|
||||||
// A source-fit answer proves only a local pose; none of the pure queries performs a
|
// A source-fit answer is the landing itself. Keep it local to the requested room: the
|
||||||
// flood fill over the live strate. Every mouth therefore receives the same explicit
|
// (0,0) landing is reserved for the future straight shaft, so no radial connector is
|
||||||
// flat-floor connector to the deterministic root, including answered mouths. This
|
// synthesized here.
|
||||||
// is what makes the join guaranteed rather than merely probable.
|
|
||||||
Passage.UpperLanding = VF_BuildPassageLanding(
|
Passage.UpperLanding = VF_BuildPassageLanding(
|
||||||
FVector(UpperX, UpperY, TopZ),
|
FVector(UpperX, UpperY, TopZ),
|
||||||
Cfg.MouthRadius,
|
Cfg.MouthRadius,
|
||||||
@@ -1300,13 +1280,7 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
UpperTopZ,
|
UpperTopZ,
|
||||||
UpperBottomZ,
|
UpperBottomZ,
|
||||||
UpperDef ? BoundarySealThicknessFor(*UpperDef) : 0.0f,
|
UpperDef ? BoundarySealThicknessFor(*UpperDef) : 0.0f,
|
||||||
bAimedUpperAtPlayerFitPoint,
|
bAimedUpperAtPlayerFitPoint);
|
||||||
/*bHasNetworkConnector=*/true,
|
|
||||||
FVector(0.0f, 0.0f,
|
|
||||||
CommonRootFloorFor(
|
|
||||||
UpperTopZ, UpperBottomZ,
|
|
||||||
UpperDef ? BoundarySealThicknessFor(*UpperDef) : 0.0f)),
|
|
||||||
OriginSpineRadius);
|
|
||||||
Passage.LowerLanding = VF_BuildPassageLanding(
|
Passage.LowerLanding = VF_BuildPassageLanding(
|
||||||
FVector(LowerX, LowerY, BottomZ),
|
FVector(LowerX, LowerY, BottomZ),
|
||||||
Cfg.MouthRadius,
|
Cfg.MouthRadius,
|
||||||
@@ -1314,17 +1288,9 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
LowerTopZ,
|
LowerTopZ,
|
||||||
(float)(Lower.BottomChunkZ) * CHUNK_SIZE,
|
(float)(Lower.BottomChunkZ) * CHUNK_SIZE,
|
||||||
LowerDef ? BoundarySealThicknessFor(*LowerDef) : 0.0f,
|
LowerDef ? BoundarySealThicknessFor(*LowerDef) : 0.0f,
|
||||||
bAimedLowerAtPlayerFitPoint,
|
bAimedLowerAtPlayerFitPoint);
|
||||||
/*bHasNetworkConnector=*/true,
|
|
||||||
FVector(0.0f, 0.0f,
|
|
||||||
CommonRootFloorFor(
|
|
||||||
LowerTopZ, (float)(Lower.BottomChunkZ) * CHUNK_SIZE,
|
|
||||||
LowerDef ? BoundarySealThicknessFor(*LowerDef) : 0.0f)),
|
|
||||||
OriginSpineRadius);
|
|
||||||
Passage.UpperPoint = Passage.UpperLanding.StandingPoint;
|
Passage.UpperPoint = Passage.UpperLanding.StandingPoint;
|
||||||
Passage.LowerPoint = Passage.LowerLanding.StandingPoint;
|
Passage.LowerPoint = Passage.LowerLanding.StandingPoint;
|
||||||
if (Passage.UpperLanding.bHasNetworkConnector) ++NumUpperRootConnectors;
|
|
||||||
if (Passage.LowerLanding.bHasNetworkConnector) ++NumLowerRootConnectors;
|
|
||||||
|
|
||||||
if (Cfg.Style == EVoxelPassageStyle::Straight
|
if (Cfg.Style == EVoxelPassageStyle::Straight
|
||||||
&& Passage.UpperLanding.HalfWidth > 0.0f
|
&& Passage.UpperLanding.HalfWidth > 0.0f
|
||||||
@@ -1554,14 +1520,11 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
}
|
}
|
||||||
// Fallback / bounds if an invalid authored width produced no positive profile.
|
// Fallback / bounds if an invalid authored width produced no positive profile.
|
||||||
Passage.Radius = FMath::Max(Passage.Radius, FMath::Max(Cfg.MouthRadius, Cfg.MidRadius));
|
Passage.Radius = FMath::Max(Passage.Radius, FMath::Max(Cfg.MouthRadius, Cfg.MidRadius));
|
||||||
Passage.Radius = FMath::Max(Passage.Radius,
|
|
||||||
FMath::Max(Passage.UpperLanding.ConnectorRadius,
|
|
||||||
Passage.LowerLanding.ConnectorRadius));
|
|
||||||
|
|
||||||
// Bounding sphere over the tube, both rooms, both floor slabs, and both optional root
|
// Bounding sphere over the tube, both rooms, and both floor slabs (+ widest radius +
|
||||||
// connectors (+ widest radius + blend) for culling. Under-sizing this sphere would
|
// blend) for culling. Under-sizing this sphere would cull a real landing and leave a
|
||||||
// cull a real landing and leave a sealed pocket, so the room's full box diagonal is
|
// sealed pocket, so the room's full box diagonal is included rather than treating the
|
||||||
// included rather than treating the standing anchor as a point.
|
// standing anchor as a point.
|
||||||
{
|
{
|
||||||
FVector BoundsMin(FLT_MAX, FLT_MAX, FLT_MAX);
|
FVector BoundsMin(FLT_MAX, FLT_MAX, FLT_MAX);
|
||||||
FVector BoundsMax(-FLT_MAX, -FLT_MAX, -FLT_MAX);
|
FVector BoundsMax(-FLT_MAX, -FLT_MAX, -FLT_MAX);
|
||||||
@@ -1590,31 +1553,6 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
+ 0.25f * FMath::Square(Height))
|
+ 0.25f * FMath::Square(Height))
|
||||||
+ Landing.FloorThickness + 4.0f;
|
+ Landing.FloorThickness + 4.0f;
|
||||||
IncludePoint(RoomCenter, RoomRadius);
|
IncludePoint(RoomCenter, RoomRadius);
|
||||||
if (Landing.bHasNetworkConnector)
|
|
||||||
{
|
|
||||||
const float HubRadius = Landing.RootSpineRadius
|
|
||||||
+ Landing.ConnectorRadius;
|
|
||||||
const float HubHalfHeight = 0.5f * FMath::Max(
|
|
||||||
Landing.RootCeilingZ - Landing.RootFloorZ, 0.0f);
|
|
||||||
IncludePoint(FVector(0.0f, 0.0f,
|
|
||||||
(Landing.RootFloorZ + Landing.RootCeilingZ) * 0.5f),
|
|
||||||
FMath::Sqrt(FMath::Square(HubRadius)
|
|
||||||
+ FMath::Square(HubHalfHeight))
|
|
||||||
+ Landing.FloorThickness + 4.0f);
|
|
||||||
const float ConnectorHalfHeight = FMath::Max(
|
|
||||||
0.5f * FMath::Max(
|
|
||||||
Landing.ConnectorCeilingZ - Landing.FloorZ, 0.0f),
|
|
||||||
Landing.ConnectorRadius);
|
|
||||||
IncludePoint(Landing.ConnectorStart,
|
|
||||||
ConnectorHalfHeight + 4.0f);
|
|
||||||
if (Landing.bHasConnectorBend)
|
|
||||||
{
|
|
||||||
IncludePoint(Landing.ConnectorControl,
|
|
||||||
ConnectorHalfHeight + 4.0f);
|
|
||||||
}
|
|
||||||
IncludePoint(Landing.ConnectorEnd,
|
|
||||||
ConnectorHalfHeight + 4.0f);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
IncludeLanding(Passage.UpperLanding);
|
IncludeLanding(Passage.UpperLanding);
|
||||||
IncludeLanding(Passage.LowerLanding);
|
IncludeLanding(Passage.LowerLanding);
|
||||||
@@ -1646,13 +1584,11 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log,
|
UE_LOG(LogTemp, Log,
|
||||||
TEXT("[StrateManager] Passage landings: upper %d/%d and lower %d/%d source-fit; root floor connectors %d/%d; %d/%d mouth queries fell back to random reach (archetypes with no query: %s)."),
|
TEXT("[StrateManager] Passage landings: upper %d/%d and lower %d/%d source-fit; %d/%d mouth queries fell back to random reach (archetypes with no query: %s)."),
|
||||||
NumAimedAtUpperPlayerFit,
|
NumAimedAtUpperPlayerFit,
|
||||||
TotalPassages,
|
TotalPassages,
|
||||||
NumAimedAtLowerPlayerFit,
|
NumAimedAtLowerPlayerFit,
|
||||||
TotalPassages,
|
TotalPassages,
|
||||||
NumUpperRootConnectors,
|
|
||||||
NumLowerRootConnectors,
|
|
||||||
(TotalPassages * 2) - NumAimedAtUpperPlayerFit - NumAimedAtLowerPlayerFit,
|
(TotalPassages * 2) - NumAimedAtUpperPlayerFit - NumAimedAtLowerPlayerFit,
|
||||||
TotalPassages * 2,
|
TotalPassages * 2,
|
||||||
*NoQueryList);
|
*NoQueryList);
|
||||||
@@ -1686,7 +1622,9 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
// Keep the legacy type: this is the explicitly opted-in above-ground vertical opening,
|
// Keep the legacy type: this is the explicitly opted-in above-ground vertical opening,
|
||||||
// not the default inter-strate descent style.
|
// not the default inter-strate descent style.
|
||||||
Entry.PassageType = EVoxelPassageType::VerticalShaft;
|
Entry.PassageType = EVoxelPassageType::VerticalShaft;
|
||||||
Entry.Radius = FMath::Max(OriginSpineRadius * 0.7f, 4.0f);
|
// Match the reserved future shaft envelope exactly. The entry is only opened through
|
||||||
|
// the top seal into the finite landing room; lower strate seals remain untouched.
|
||||||
|
Entry.Radius = FMath::Max(OriginSpineRadius, 1.0f);
|
||||||
Entry.UpperPoint = FVector(0.0f, 0.0f, TopZ + CHUNK_SIZE);
|
Entry.UpperPoint = FVector(0.0f, 0.0f, TopZ + CHUNK_SIZE);
|
||||||
// The capsule's lower tangent meets the room ceiling; it never bores through the
|
// 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.
|
// room's floor or creates an origin column in strate 0.
|
||||||
@@ -1779,8 +1717,8 @@ float UVoxelStrateManager::EvaluateModifierSDF(float WorldX, float WorldY, float
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A landing is a real room with a hard flat-floor half-space, not a sphere around the
|
// A landing is a real room with a hard flat-floor half-space, not a sphere around the
|
||||||
// tube endpoint. The optional connector is the same fixed SDF geometry selected during
|
// tube endpoint. It is the same fixed SDF geometry selected during GeneratePassages; it
|
||||||
// GeneratePassages; it never performs a source query here.
|
// never performs a source query here and does not synthesize a radial root connector.
|
||||||
const float UpperLandingSDF = VF_EvaluatePassageLandingSDF(Pos, P.UpperLanding);
|
const float UpperLandingSDF = VF_EvaluatePassageLandingSDF(Pos, P.UpperLanding);
|
||||||
const float LowerLandingSDF = VF_EvaluatePassageLandingSDF(Pos, P.LowerLanding);
|
const float LowerLandingSDF = VF_EvaluatePassageLandingSDF(Pos, P.LowerLanding);
|
||||||
MinSDF = VoxelSDF::SmoothMin(MinSDF, UpperLandingSDF, BlendK);
|
MinSDF = VoxelSDF::SmoothMin(MinSDF, UpperLandingSDF, BlendK);
|
||||||
@@ -1807,9 +1745,10 @@ void UVoxelStrateManager::ApplyPassageModifier(
|
|||||||
for (const int32 PassageIndex : Nearby)
|
for (const int32 PassageIndex : Nearby)
|
||||||
{
|
{
|
||||||
const FVoxelPassage& Passage = Passages[PassageIndex];
|
const FVoxelPassage& Passage = Passages[PassageIndex];
|
||||||
if (VF_IsPassageLandingFloor(Position, Passage.UpperLanding)
|
if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker()
|
||||||
|
&& (VF_IsPassageLandingFloor(Position, Passage.UpperLanding)
|
||||||
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding)
|
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding)
|
||||||
|| VF_IsWalkableTunnelFloor(Passage, Position))
|
|| VF_IsWalkableTunnelFloor(Passage, Position)))
|
||||||
{
|
{
|
||||||
// This is the one bidirectional part of PassageCarveOp: a floor is a proved solid
|
// 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
|
// support slab. It is deliberately applied after the air carve so a tube can never
|
||||||
@@ -1928,9 +1867,10 @@ void UVoxelStrateManager::ApplyPassageLandingFloorMC(
|
|||||||
for (const int32 PassageIndex : Nearby)
|
for (const int32 PassageIndex : Nearby)
|
||||||
{
|
{
|
||||||
const FVoxelPassage& Passage = Passages[PassageIndex];
|
const FVoxelPassage& Passage = Passages[PassageIndex];
|
||||||
if (VF_IsPassageLandingFloor(Position, Passage.UpperLanding)
|
if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker()
|
||||||
|
&& (VF_IsPassageLandingFloor(Position, Passage.UpperLanding)
|
||||||
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding)
|
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding)
|
||||||
|| VF_IsWalkableTunnelFloor(Passage, Position))
|
|| VF_IsWalkableTunnelFloor(Passage, Position)))
|
||||||
{
|
{
|
||||||
// Result is in MC convention here (negative = solid). This reassertion is the
|
// Result is in MC convention here (negative = solid). This reassertion is the
|
||||||
// structural floor backstop after the optional MC-space disturbance layer.
|
// structural floor backstop after the optional MC-space disturbance layer.
|
||||||
@@ -1971,8 +1911,9 @@ void UVoxelStrateManager::ApplyPassageLandingRoomFloorMC(
|
|||||||
for (const int32 PassageIndex : Nearby)
|
for (const int32 PassageIndex : Nearby)
|
||||||
{
|
{
|
||||||
const FVoxelPassage& Passage = Passages[PassageIndex];
|
const FVoxelPassage& Passage = Passages[PassageIndex];
|
||||||
if (VF_IsPassageRoomFloor(Position, Passage.UpperLanding)
|
if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker()
|
||||||
|| VF_IsPassageRoomFloor(Position, Passage.LowerLanding))
|
&& (VF_IsPassageRoomFloor(Position, Passage.UpperLanding)
|
||||||
|
|| VF_IsPassageRoomFloor(Position, Passage.LowerLanding)))
|
||||||
{
|
{
|
||||||
Density = FMath::Min(Density, -BaseDensity);
|
Density = FMath::Min(Density, -BaseDensity);
|
||||||
break;
|
break;
|
||||||
@@ -2030,50 +1971,6 @@ bool UVoxelStrateManager::AnyPassageLandingFloorNearBox(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The fallback connector carries the same support slab away from the room. Its
|
|
||||||
// segment is not covered by the room AABB above; omitting it would let a tile that
|
|
||||||
// contains only the connector floor prove AllAir and erase the guaranteed join.
|
|
||||||
if (Landing->bHasNetworkConnector && Landing->ConnectorRadius > 0.0f
|
|
||||||
&& FMath::IsFinite(Landing->ConnectorStart.X)
|
|
||||||
&& FMath::IsFinite(Landing->ConnectorStart.Y)
|
|
||||||
&& FMath::IsFinite(Landing->ConnectorStart.Z)
|
|
||||||
&& FMath::IsFinite(Landing->ConnectorEnd.X)
|
|
||||||
&& FMath::IsFinite(Landing->ConnectorEnd.Y)
|
|
||||||
&& FMath::IsFinite(Landing->ConnectorEnd.Z))
|
|
||||||
{
|
|
||||||
const float ConnectorPad = Landing->ConnectorRadius + Pad;
|
|
||||||
float ConnectorMinX = FMath::Min(
|
|
||||||
Landing->ConnectorStart.X, Landing->ConnectorEnd.X) - ConnectorPad;
|
|
||||||
float ConnectorMaxX = FMath::Max(
|
|
||||||
Landing->ConnectorStart.X, Landing->ConnectorEnd.X) + ConnectorPad;
|
|
||||||
float ConnectorMinY = FMath::Min(
|
|
||||||
Landing->ConnectorStart.Y, Landing->ConnectorEnd.Y) - ConnectorPad;
|
|
||||||
float ConnectorMaxY = FMath::Max(
|
|
||||||
Landing->ConnectorStart.Y, Landing->ConnectorEnd.Y) + ConnectorPad;
|
|
||||||
if (Landing->bHasConnectorBend)
|
|
||||||
{
|
|
||||||
ConnectorMinX = FMath::Min(
|
|
||||||
ConnectorMinX, Landing->ConnectorControl.X - ConnectorPad);
|
|
||||||
ConnectorMaxX = FMath::Max(
|
|
||||||
ConnectorMaxX, Landing->ConnectorControl.X + ConnectorPad);
|
|
||||||
ConnectorMinY = FMath::Min(
|
|
||||||
ConnectorMinY, Landing->ConnectorControl.Y - ConnectorPad);
|
|
||||||
ConnectorMaxY = FMath::Max(
|
|
||||||
ConnectorMaxY, Landing->ConnectorControl.Y + ConnectorPad);
|
|
||||||
}
|
|
||||||
const float ConnectorMinZ = FMath::Min(
|
|
||||||
Landing->FloorZ, Landing->RootFloorZ)
|
|
||||||
- Landing->FloorThickness - Pad;
|
|
||||||
const float ConnectorMaxZ = FMath::Max(
|
|
||||||
Landing->FloorZ, Landing->RootFloorZ) + Pad;
|
|
||||||
if (ConnectorMaxX >= MinVoxel.X && ConnectorMinX <= MaxVoxel.X
|
|
||||||
&& ConnectorMaxY >= MinVoxel.Y && ConnectorMinY <= MaxVoxel.Y
|
|
||||||
&& ConnectorMaxZ >= MinVoxel.Z && ConnectorMinZ <= MaxVoxel.Z)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The default tube also owns a three-voxel support band. Its conservative segment AABB
|
// The default tube also owns a three-voxel support band. Its conservative segment AABB
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "VoxelGenerator.h"
|
#include "VoxelGenerator.h"
|
||||||
#include "VoxelStrateManager.h"
|
#include "VoxelStrateManager.h"
|
||||||
#include "VoxelTypes.h"
|
#include "VoxelTypes.h"
|
||||||
|
#include "Async/ParallelFor.h"
|
||||||
|
|
||||||
namespace VoxelStrateMeasurePrivate
|
namespace VoxelStrateMeasurePrivate
|
||||||
{
|
{
|
||||||
@@ -388,21 +389,26 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
OutGrid.Density.SetNumUninitialized(OutGrid.CellCount);
|
OutGrid.Density.SetNumUninitialized(OutGrid.CellCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool bSawNonFiniteDensity = false;
|
// Each Z slab writes a disjoint contiguous region. GetDensityAt is a const, read-only
|
||||||
int32 CellIndex = 0;
|
// query used concurrently by the mesher, and custom samplers are required to provide
|
||||||
for (int32 Z = 0; Z < OutGrid.NumZ; ++Z)
|
// the same const/thread-safe contract. The non-finite flag is also per-cell so the
|
||||||
|
// diagnostic reduction below never depends on task completion order.
|
||||||
|
TArray<uint8> NonFiniteFlags;
|
||||||
|
NonFiniteFlags.Init(0u, OutGrid.CellCount);
|
||||||
|
ParallelFor(OutGrid.NumZ, [&](int32 Z)
|
||||||
{
|
{
|
||||||
for (int32 Y = 0; Y < OutGrid.NumY; ++Y)
|
for (int32 Y = 0; Y < OutGrid.NumY; ++Y)
|
||||||
{
|
{
|
||||||
for (int32 X = 0; X < OutGrid.NumX; ++X)
|
for (int32 X = 0; X < OutGrid.NumX; ++X)
|
||||||
{
|
{
|
||||||
|
const int32 CellIndex = OutGrid.Index(X, Y, Z);
|
||||||
const FVector Sample = OutGrid.CellCenter(X, Y, Z);
|
const FVector Sample = OutGrid.CellCenter(X, Y, Z);
|
||||||
const float Density = Sampler != nullptr
|
const float Density = Sampler != nullptr
|
||||||
? Sampler->SampleDensity(Sample.X, Sample.Y, Sample.Z)
|
? Sampler->SampleDensity(Sample.X, Sample.Y, Sample.Z)
|
||||||
: Generator->GetDensityAt(Sample.X, Sample.Y, Sample.Z);
|
: Generator->GetDensityAt(Sample.X, Sample.Y, Sample.Z);
|
||||||
if (!FMath::IsFinite(Density))
|
if (!FMath::IsFinite(Density))
|
||||||
{
|
{
|
||||||
bSawNonFiniteDensity = true;
|
NonFiniteFlags[CellIndex] = 1u;
|
||||||
}
|
}
|
||||||
|
|
||||||
// MC convention in this codebase: negative is solid, positive is air.
|
// MC convention in this codebase: negative is solid, positive is air.
|
||||||
@@ -410,11 +416,16 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
{
|
{
|
||||||
OutGrid.Density[CellIndex] = Density;
|
OutGrid.Density[CellIndex] = Density;
|
||||||
}
|
}
|
||||||
OutGrid.Air[CellIndex++] = Density > 0.0f ? 1u : 0u;
|
OutGrid.Air[CellIndex] = Density > 0.0f ? 1u : 0u;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bool bSawNonFiniteDensity = false;
|
||||||
|
for (int32 CellIndex = 0; CellIndex < OutGrid.CellCount; ++CellIndex)
|
||||||
|
{
|
||||||
|
bSawNonFiniteDensity |= NonFiniteFlags[CellIndex] != 0u;
|
||||||
|
}
|
||||||
if (bSawNonFiniteDensity)
|
if (bSawNonFiniteDensity)
|
||||||
{
|
{
|
||||||
return Refuse(OutReason, TEXT("At least one density sample was non-finite."));
|
return Refuse(OutReason, TEXT("At least one density sample was non-finite."));
|
||||||
@@ -552,16 +563,52 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
|
|
||||||
void BuildManhattanDistanceToSolid(const FSampleGrid& Grid, TArray<int32>& OutDistances)
|
void BuildManhattanDistanceToSolid(const FSampleGrid& Grid, TArray<int32>& OutDistances)
|
||||||
{
|
{
|
||||||
// Two raster passes with 6-neighbour Manhattan relaxation. The result is an approximation
|
// Two wavefront passes with 6-neighbour Manhattan relaxation. A cell depends only on the
|
||||||
// of distance-to-solid in the original field: each coarse grid unit is SampleStep VOXELS.
|
// immediately preceding diagonal in the forward pass (or following diagonal in the
|
||||||
|
// reverse pass), so each diagonal is safe to process in parallel while the diagonal
|
||||||
|
// barriers preserve the exact raster result. Each coarse grid unit is SampleStep VOXELS.
|
||||||
constexpr int32 Infinity = INT32_MAX;
|
constexpr int32 Infinity = INT32_MAX;
|
||||||
OutDistances.SetNumUninitialized(Grid.CellCount);
|
OutDistances.SetNumUninitialized(Grid.CellCount);
|
||||||
for (int32 Index = 0; Index < Grid.CellCount; ++Index)
|
ParallelFor(Grid.CellCount, [&](int32 Index)
|
||||||
{
|
{
|
||||||
OutDistances[Index] = Grid.Air[Index] == 0u ? 0 : Infinity;
|
OutDistances[Index] = Grid.Air[Index] == 0u ? 0 : Infinity;
|
||||||
|
});
|
||||||
|
|
||||||
|
const int32 LastDiagonal = Grid.NumX + Grid.NumY + Grid.NumZ - 3;
|
||||||
|
const auto ProcessDiagonal = [&](int32 Diagonal, bool bForward)
|
||||||
|
{
|
||||||
|
const int32 MinZ = FMath::Max(
|
||||||
|
0, Diagonal - (Grid.NumX - 1) - (Grid.NumY - 1));
|
||||||
|
const int32 MaxZ = FMath::Min(Grid.NumZ - 1, Diagonal);
|
||||||
|
if (MinZ > MaxZ)
|
||||||
|
{
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Relax = [&](int32 Current, int32 Neighbor)
|
int32 DiagonalCellCount = 0;
|
||||||
|
for (int32 Z = MinZ; Z <= MaxZ; ++Z)
|
||||||
|
{
|
||||||
|
const int32 MinY = FMath::Max(
|
||||||
|
0, Diagonal - Z - (Grid.NumX - 1));
|
||||||
|
const int32 MaxY = FMath::Min(Grid.NumY - 1, Diagonal - Z);
|
||||||
|
DiagonalCellCount += FMath::Max(0, MaxY - MinY + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto ProcessZ = [&](int32 Z)
|
||||||
|
{
|
||||||
|
const int32 MinY = FMath::Max(
|
||||||
|
0, Diagonal - Z - (Grid.NumX - 1));
|
||||||
|
const int32 MaxY = FMath::Min(Grid.NumY - 1, Diagonal - Z);
|
||||||
|
for (int32 Y = MinY; Y <= MaxY; ++Y)
|
||||||
|
{
|
||||||
|
const int32 X = Diagonal - Z - Y;
|
||||||
|
const int32 Current = Grid.Index(X, Y, Z);
|
||||||
|
if (Grid.Air[Current] == 0u)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto Relax = [&](int32 Neighbor)
|
||||||
{
|
{
|
||||||
if (OutDistances[Neighbor] == Infinity)
|
if (OutDistances[Neighbor] == Infinity)
|
||||||
{
|
{
|
||||||
@@ -569,37 +616,51 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
}
|
}
|
||||||
const int32 Candidate = OutDistances[Neighbor] == INT32_MAX - 1
|
const int32 Candidate = OutDistances[Neighbor] == INT32_MAX - 1
|
||||||
? Infinity : OutDistances[Neighbor] + 1;
|
? Infinity : OutDistances[Neighbor] + 1;
|
||||||
OutDistances[Current] = FMath::Min(OutDistances[Current], Candidate);
|
OutDistances[Current] = FMath::Min(
|
||||||
|
OutDistances[Current], Candidate);
|
||||||
};
|
};
|
||||||
|
|
||||||
for (int32 Z = 0; Z < Grid.NumZ; ++Z)
|
if (bForward)
|
||||||
{
|
{
|
||||||
for (int32 Y = 0; Y < Grid.NumY; ++Y)
|
if (X > 0) Relax(Grid.Index(X - 1, Y, Z));
|
||||||
|
if (Y > 0) Relax(Grid.Index(X, Y - 1, Z));
|
||||||
|
if (Z > 0) Relax(Grid.Index(X, Y, Z - 1));
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
for (int32 X = 0; X < Grid.NumX; ++X)
|
if (X + 1 < Grid.NumX) Relax(Grid.Index(X + 1, Y, Z));
|
||||||
{
|
if (Y + 1 < Grid.NumY) Relax(Grid.Index(X, Y + 1, Z));
|
||||||
const int32 Current = Grid.Index(X, Y, Z);
|
if (Z + 1 < Grid.NumZ) Relax(Grid.Index(X, Y, Z + 1));
|
||||||
if (Grid.Air[Current] == 0u) continue;
|
|
||||||
if (X > 0) Relax(Current, Grid.Index(X - 1, Y, Z));
|
|
||||||
if (Y > 0) Relax(Current, Grid.Index(X, Y - 1, Z));
|
|
||||||
if (Z > 0) Relax(Current, Grid.Index(X, Y, Z - 1));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
for (int32 Z = Grid.NumZ - 1; Z >= 0; --Z)
|
// Tiny edge diagonals are cheaper to execute directly; substantial diagonals get
|
||||||
|
// one task per Z row, with every row writing a disjoint set of cells.
|
||||||
|
const int32 ZCount = MaxZ - MinZ + 1;
|
||||||
|
if (ZCount > 1 && DiagonalCellCount >= 256)
|
||||||
{
|
{
|
||||||
for (int32 Y = Grid.NumY - 1; Y >= 0; --Y)
|
ParallelFor(ZCount, [&](int32 ZOffset)
|
||||||
{
|
{
|
||||||
for (int32 X = Grid.NumX - 1; X >= 0; --X)
|
ProcessZ(MinZ + ZOffset);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
const int32 Current = Grid.Index(X, Y, Z);
|
for (int32 Z = MinZ; Z <= MaxZ; ++Z)
|
||||||
if (Grid.Air[Current] == 0u) continue;
|
{
|
||||||
if (X + 1 < Grid.NumX) Relax(Current, Grid.Index(X + 1, Y, Z));
|
ProcessZ(Z);
|
||||||
if (Y + 1 < Grid.NumY) Relax(Current, Grid.Index(X, Y + 1, Z));
|
|
||||||
if (Z + 1 < Grid.NumZ) Relax(Current, Grid.Index(X, Y, Z + 1));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int32 Diagonal = 0; Diagonal <= LastDiagonal; ++Diagonal)
|
||||||
|
{
|
||||||
|
ProcessDiagonal(Diagonal, true);
|
||||||
|
}
|
||||||
|
for (int32 Diagonal = LastDiagonal; Diagonal >= 0; --Diagonal)
|
||||||
|
{
|
||||||
|
ProcessDiagonal(Diagonal, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1092,14 +1153,23 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
}
|
}
|
||||||
|
|
||||||
OutPlayerFit.Init(0u, Grid.CellCount);
|
OutPlayerFit.Init(0u, Grid.CellCount);
|
||||||
|
TArray<int64> PlayerFitCounts;
|
||||||
|
PlayerFitCounts.Init(0, Grid.NumZ);
|
||||||
|
|
||||||
|
// A candidate pose only reads the immutable sampled grid and writes its own mask cell.
|
||||||
|
// Give each Z slab private stencil scratch, then reduce the per-slab counts in Z order
|
||||||
|
// below. This removes the old shared scratch/count race without changing any predicate.
|
||||||
|
ParallelFor(FMath::Max(Grid.NumZ - 1, 0), [&](int32 SliceIndex)
|
||||||
|
{
|
||||||
|
const int32 Z = SliceIndex + 1;
|
||||||
TArray<float> SupportHeights;
|
TArray<float> SupportHeights;
|
||||||
SupportHeights.SetNumUninitialized(SupportOffsets.Num());
|
SupportHeights.SetNumUninitialized(SupportOffsets.Num());
|
||||||
TArray<int32> SupportAnchorZs;
|
TArray<int32> SupportAnchorZs;
|
||||||
SupportAnchorZs.SetNumUninitialized(SupportOffsets.Num());
|
SupportAnchorZs.SetNumUninitialized(SupportOffsets.Num());
|
||||||
TArray<uint8> bSupported;
|
TArray<uint8> bSupported;
|
||||||
bSupported.Init(0u, SupportOffsets.Num());
|
bSupported.Init(0u, SupportOffsets.Num());
|
||||||
for (int32 Z = 1; Z < Grid.NumZ; ++Z)
|
int64 NumPlayerFitInSlice = 0;
|
||||||
{
|
|
||||||
for (int32 Y = 0; Y < Grid.NumY; ++Y)
|
for (int32 Y = 0; Y < Grid.NumY; ++Y)
|
||||||
{
|
{
|
||||||
for (int32 X = 0; X < Grid.NumX; ++X)
|
for (int32 X = 0; X < Grid.NumX; ++X)
|
||||||
@@ -1188,14 +1258,18 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (OutPlayerFit[Current] != 0u)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
OutPlayerFit[Current] = 1u;
|
OutPlayerFit[Current] = 1u;
|
||||||
++OutNumPlayerFitCells;
|
++NumPlayerFitInSlice;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
PlayerFitCounts[Z] = NumPlayerFitInSlice;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fixed-index reduction is intentional: even though the terms are integral, keeping the
|
||||||
|
// reduction serial documents the determinism rule shared by the floating metrics below.
|
||||||
|
for (int32 Z = 1; Z < Grid.NumZ; ++Z)
|
||||||
|
{
|
||||||
|
OutNumPlayerFitCells += PlayerFitCounts[Z];
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1211,11 +1285,16 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
|
|
||||||
const int32 PlaneCells = Grid.NumX * Grid.NumY;
|
const int32 PlaneCells = Grid.NumX * Grid.NumY;
|
||||||
constexpr int32 Infinity = INT32_MAX / 4;
|
constexpr int32 Infinity = INT32_MAX / 4;
|
||||||
|
TArray<int32> PerSliceMinimum;
|
||||||
|
PerSliceMinimum.Init(Infinity, Grid.NumZ);
|
||||||
|
|
||||||
|
// Each horizontal distance transform is independent. Keep its mutable raster in the
|
||||||
|
// worker and publish one integer result per Z slice; the final minimum is reduced in
|
||||||
|
// ascending Z order for an explicit deterministic reduction.
|
||||||
|
ParallelFor(Grid.NumZ, [&](int32 Z)
|
||||||
|
{
|
||||||
TArray<int32> Distances;
|
TArray<int32> Distances;
|
||||||
Distances.SetNumUninitialized(PlaneCells);
|
Distances.SetNumUninitialized(PlaneCells);
|
||||||
int32 MinimumDistance = Infinity;
|
|
||||||
for (int32 Z = 0; Z < Grid.NumZ; ++Z)
|
|
||||||
{
|
|
||||||
for (int32 Y = 0; Y < Grid.NumY; ++Y)
|
for (int32 Y = 0; Y < Grid.NumY; ++Y)
|
||||||
{
|
{
|
||||||
for (int32 X = 0; X < Grid.NumX; ++X)
|
for (int32 X = 0; X < Grid.NumX; ++X)
|
||||||
@@ -1269,6 +1348,7 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int32 SliceMinimum = Infinity;
|
||||||
for (int32 Y = 0; Y < Grid.NumY; ++Y)
|
for (int32 Y = 0; Y < Grid.NumY; ++Y)
|
||||||
{
|
{
|
||||||
for (int32 X = 0; X < Grid.NumX; ++X)
|
for (int32 X = 0; X < Grid.NumX; ++X)
|
||||||
@@ -1276,11 +1356,18 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
const int32 Cell = Grid.Index(X, Y, Z);
|
const int32 Cell = Grid.Index(X, Y, Z);
|
||||||
if (PlayerFit[Cell] != 0u)
|
if (PlayerFit[Cell] != 0u)
|
||||||
{
|
{
|
||||||
MinimumDistance = FMath::Min(
|
SliceMinimum = FMath::Min(
|
||||||
MinimumDistance, Distances[X + Grid.NumX * Y]);
|
SliceMinimum, Distances[X + Grid.NumX * Y]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
PerSliceMinimum[Z] = SliceMinimum;
|
||||||
|
});
|
||||||
|
|
||||||
|
int32 MinimumDistance = Infinity;
|
||||||
|
for (int32 Z = 0; Z < Grid.NumZ; ++Z)
|
||||||
|
{
|
||||||
|
MinimumDistance = FMath::Min(MinimumDistance, PerSliceMinimum[Z]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return MinimumDistance == Infinity
|
return MinimumDistance == Infinity
|
||||||
|
|||||||
@@ -10,17 +10,17 @@
|
|||||||
// infinite worlds without storing anything.
|
// infinite worlds without storing anything.
|
||||||
//
|
//
|
||||||
// ROOMS:
|
// ROOMS:
|
||||||
// - Origin room: guaranteed large room at (0,0) per strate — the hub
|
// - Origin room: guaranteed large room at (0,0) per strate — the future shaft landing
|
||||||
// - Hash rooms: ellipsoids, rounded boxes, or elongated capsules
|
// - Hash rooms: ellipsoids, rounded boxes, or elongated capsules
|
||||||
// - All blended with smooth-min for organic junctions
|
// - All blended with smooth-min for organic junctions
|
||||||
//
|
//
|
||||||
// TUNNELS:
|
// TUNNELS:
|
||||||
// - Connect pairs of rooms based on distance and probability
|
// - Connect pairs of rooms based on distance and probability
|
||||||
// - Tapered (variable min/max radius at each endpoint)
|
// - Tapered (variable min/max radius at each endpoint)
|
||||||
// - Curved (midpoint displaced perpendicular to tunnel direction)
|
// - Curved wandering chains with deterministic perpendicular control-point jitter
|
||||||
// - Horizontal bias (vertical connections penalized)
|
// - Horizontal bias (vertical connections penalized)
|
||||||
// - Endpoint Z offset (tunnels enter rooms at different heights)
|
// - Floor-aligned endpoints and variable radii preserve walkable room joins
|
||||||
// - Origin room forces connections to all nearby rooms
|
// - The origin room is not joined to landings by radial connector roads
|
||||||
//
|
//
|
||||||
// PIPELINE POSITION:
|
// PIPELINE POSITION:
|
||||||
// Step 3 (cave warp) bends the SDF query coordinates before we evaluate.
|
// Step 3 (cave warp) bends the SDF query coordinates before we evaluate.
|
||||||
@@ -48,14 +48,10 @@ enum class ECaveGeneratorType : uint8;
|
|||||||
* `FloorZ = StandingPoint.Z - 0.5`. The chamber is deliberately separate from the tube endpoint
|
* `FloorZ = StandingPoint.Z - 0.5`. The chamber is deliberately separate from the tube endpoint
|
||||||
* so a sloped/vertical tube never presents the player with a hole as its first standing position.
|
* so a sloped/vertical tube never presents the player with a hole as its first standing position.
|
||||||
*
|
*
|
||||||
* A source query answer selects the landing's XY/Z anchor. Because that pure query does not
|
* A source query answer selects the landing's XY/Z anchor. The landing is deliberately local to
|
||||||
* flood-fill the live network, every inter-strate end also gets a deterministic root connector
|
* that answer: it is a flat-floored room where the inter-strate tube meets a legal player-fit
|
||||||
* (`bHasNetworkConnector`). Its flat floor ramps to the same common interior root level for that
|
* pose. The (0,0) landing is the only place reserved for the future straight surface shaft;
|
||||||
* strate, so both mouths enter the finite landing room at (0,0) even when their authored floors
|
* progression mouths do not receive hidden radial roads to that spine room.
|
||||||
* 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
|
struct VOXELFORGE_API FVoxelPassageLanding
|
||||||
{
|
{
|
||||||
@@ -63,23 +59,12 @@ struct VOXELFORGE_API FVoxelPassageLanding
|
|||||||
FVector DoorPoint = FVector::ZeroVector;
|
FVector DoorPoint = FVector::ZeroVector;
|
||||||
FVector DoorDirection = FVector(1.0f, 0.0f, 0.0f);
|
FVector DoorDirection = FVector(1.0f, 0.0f, 0.0f);
|
||||||
|
|
||||||
FVector ConnectorStart = FVector::ZeroVector;
|
|
||||||
FVector ConnectorControl = FVector::ZeroVector;
|
|
||||||
FVector ConnectorEnd = FVector::ZeroVector;
|
|
||||||
|
|
||||||
float FloorZ = 0.0f;
|
float FloorZ = 0.0f;
|
||||||
float CeilingZ = 0.0f;
|
float CeilingZ = 0.0f;
|
||||||
float HalfWidth = 0.0f;
|
float HalfWidth = 0.0f;
|
||||||
float FloorThickness = 3.0f;
|
float FloorThickness = 3.0f;
|
||||||
float ConnectorRadius = 0.0f;
|
|
||||||
float ConnectorCeilingZ = 0.0f;
|
|
||||||
float RootFloorZ = 0.0f;
|
|
||||||
float RootCeilingZ = 0.0f;
|
|
||||||
float RootSpineRadius = 0.0f;
|
|
||||||
|
|
||||||
bool bSourcePlayerFit = false;
|
bool bSourcePlayerFit = false;
|
||||||
bool bHasNetworkConnector = false;
|
|
||||||
bool bHasConnectorBend = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
@@ -202,17 +187,14 @@ VOXELFORGE_API FVoxelPassageLanding VF_BuildPassageLanding(
|
|||||||
float StrateTopZ,
|
float StrateTopZ,
|
||||||
float StrateBottomZ,
|
float StrateBottomZ,
|
||||||
float BoundarySealThickness,
|
float BoundarySealThickness,
|
||||||
bool bSourcePlayerFit,
|
bool bSourcePlayerFit);
|
||||||
bool bHasNetworkConnector,
|
|
||||||
const FVector& NetworkPoint,
|
|
||||||
float RootSpineRadius = 0.0f);
|
|
||||||
|
|
||||||
/** Signed distance of the room + optional root connector. Negative means passage air. */
|
/** Signed distance of the local landing room. Negative means passage air. */
|
||||||
VOXELFORGE_API float VF_EvaluatePassageLandingSDF(
|
VOXELFORGE_API float VF_EvaluatePassageLandingSDF(
|
||||||
const FVector& Position,
|
const FVector& Position,
|
||||||
const FVoxelPassageLanding& Landing);
|
const FVoxelPassageLanding& Landing);
|
||||||
|
|
||||||
/** True when a point belongs to the guaranteed solid floor slab of the landing/connector. */
|
/** True when a point belongs to the guaranteed solid floor slab of the landing room. */
|
||||||
VOXELFORGE_API bool VF_IsPassageLandingFloor(
|
VOXELFORGE_API bool VF_IsPassageLandingFloor(
|
||||||
const FVector& Position,
|
const FVector& Position,
|
||||||
const FVoxelPassageLanding& Landing);
|
const FVoxelPassageLanding& Landing);
|
||||||
@@ -537,7 +519,7 @@ namespace VoxelNoise
|
|||||||
// - Determine room existence, position, size
|
// - Determine room existence, position, size
|
||||||
// - Run O(N²) nearest-neighbor backbone
|
// - Run O(N²) nearest-neighbor backbone
|
||||||
// - Decide O(N²) tunnel connections with hash lookups
|
// - Decide O(N²) tunnel connections with hash lookups
|
||||||
// - Compute tunnel radii, Z offsets, midpoint warping
|
// - Compute tunnel radii, floor-aligned endpoints, and wandering control chains
|
||||||
//
|
//
|
||||||
// With caching: build once, evaluate 32K times with just SDF math.
|
// With caching: build once, evaluate 32K times with just SDF math.
|
||||||
// This is the single biggest CPU performance win for cave generation.
|
// This is the single biggest CPU performance win for cave generation.
|
||||||
@@ -588,19 +570,41 @@ struct FCachedRoom
|
|||||||
float ShapeR = 0.0f;
|
float ShapeR = 0.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
// A pre-computed tunnel segment — all connection decisions and hash-derived
|
// A local flat-floor bridge over the overlap of two room bodies. It is not a new graph edge: it
|
||||||
// properties (radius, Z offset, midpoint warp) are resolved during cache build.
|
// only gives an existing spherical-room intersection one shared walkable floor level, removing
|
||||||
|
// the high lip that otherwise appears when the two room floor cuts disagree.
|
||||||
|
struct FCachedRoomFloorJoin
|
||||||
|
{
|
||||||
|
FVector Start;
|
||||||
|
FVector End;
|
||||||
|
float Radius = 0.0f;
|
||||||
|
float FloorZ = 0.0f;
|
||||||
|
float CeilingZ = 0.0f;
|
||||||
|
FVector BoundCenter;
|
||||||
|
float BoundRadiusSq = 0.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A pre-computed wandering tunnel chain — all connection decisions and hash-derived properties
|
||||||
|
// (radii, Z offsets, control points) are resolved during cache build.
|
||||||
struct FCachedTunnel
|
struct FCachedTunnel
|
||||||
{
|
{
|
||||||
FVector EndpointA; // First room's connection point (with Z offset)
|
FVector EndpointA; // First room's floor-tangent connection point in SDF coordinates
|
||||||
FVector EndpointB; // Second room's connection point (with Z offset)
|
FVector EndpointB; // Second room's floor-tangent connection point in SDF coordinates
|
||||||
float RadiusA; // Tube radius at endpoint A
|
float RadiusA; // Tube radius at endpoint A
|
||||||
float RadiusB; // Tube radius at endpoint B
|
float RadiusB; // Tube radius at endpoint B
|
||||||
// Warped midpoint — only used if bHasMidpoint is true.
|
// Compatibility summary of the chain's first/middle/last points. These fields are retained
|
||||||
// Creates a two-segment curved path instead of a straight tube.
|
// for diagnostics and old cache consumers; EvaluateSDFCached uses ControlPoints when present.
|
||||||
FVector Midpoint;
|
FVector Midpoint;
|
||||||
float RadiusMid; // Radius at the midpoint (average of A and B)
|
float RadiusMid;
|
||||||
bool bHasMidpoint; // True when TunnelWarpStrength > 0 and tunnel is long enough
|
bool bHasMidpoint;
|
||||||
|
TArray<FVector> ControlPoints;
|
||||||
|
TArray<float> ControlRadii;
|
||||||
|
// World-space copy used by the final structural air backstop. Evaluating this copy avoids
|
||||||
|
// turning a gentle authored floor ramp into a vertical break when the cave warp is nonlinear.
|
||||||
|
TArray<FVector> WorldControlPoints;
|
||||||
|
TArray<float> WorldControlRadii;
|
||||||
|
FVector WorldBoundCenter = FVector::ZeroVector;
|
||||||
|
float WorldBoundRadiusSq = 0.0f;
|
||||||
// Bounding sphere for quick per-voxel rejection
|
// Bounding sphere for quick per-voxel rejection
|
||||||
FVector BoundCenter; // Center of the bounding sphere
|
FVector BoundCenter; // Center of the bounding sphere
|
||||||
float BoundRadiusSq; // Squared radius — if voxel is further, skip this tunnel
|
float BoundRadiusSq; // Squared radius — if voxel is further, skip this tunnel
|
||||||
@@ -658,6 +662,7 @@ struct FCachedColumn
|
|||||||
struct FChunkSDFCache
|
struct FChunkSDFCache
|
||||||
{
|
{
|
||||||
TArray<FCachedRoom> Rooms;
|
TArray<FCachedRoom> Rooms;
|
||||||
|
TArray<FCachedRoomFloorJoin> RoomFloorJoins;
|
||||||
TArray<FCachedTunnel> Tunnels;
|
TArray<FCachedTunnel> Tunnels;
|
||||||
TArray<FCachedPit> Pits;
|
TArray<FCachedPit> Pits;
|
||||||
TArray<FCachedChimney> Chimneys;
|
TArray<FCachedChimney> Chimneys;
|
||||||
@@ -681,6 +686,15 @@ namespace VoxelCaveMorphology
|
|||||||
return VoxelHash::Mix(WorldSeed ^ (uint32)(StrateIndex * 7919 + 104729));
|
return VoxelHash::Mix(WorldSeed ^ (uint32)(StrateIndex * 7919 + 104729));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Map a world-space query to the SDF coordinate space used by the room/tunnel cache. Keeping
|
||||||
|
// this transform in the morphology module lets the generator's post-disturbance structural
|
||||||
|
// check share the exact same warp as the source and the landing query.
|
||||||
|
VOXELFORGE_API FVector ApplyCaveWarp(
|
||||||
|
const FVector& WorldPoint,
|
||||||
|
const FStrateGenerationParams& Params,
|
||||||
|
uint32 Seed
|
||||||
|
);
|
||||||
|
|
||||||
// PHASE 1: Build the SDF cache for a rectangular region.
|
// PHASE 1: Build the SDF cache for a rectangular region.
|
||||||
// Collects all rooms, computes nearest-neighbor backbone, decides tunnel
|
// Collects all rooms, computes nearest-neighbor backbone, decides tunnel
|
||||||
// connections, and pre-computes all tunnel geometry (radii, offsets, midpoints).
|
// connections, and pre-computes all tunnel geometry (radii, offsets, midpoints).
|
||||||
@@ -697,7 +711,7 @@ namespace VoxelCaveMorphology
|
|||||||
// @param Seed — world seed
|
// @param Seed — world seed
|
||||||
// @param StrateIndex — which strate (offsets hash for variety)
|
// @param StrateIndex — which strate (offsets hash for variety)
|
||||||
// @param TerrainOps — optional probability pool; null = no per-room ops
|
// @param TerrainOps — optional probability pool; null = no per-room ops
|
||||||
void BuildChunkCache(
|
VOXELFORGE_API void BuildChunkCache(
|
||||||
FChunkSDFCache& OutCache,
|
FChunkSDFCache& OutCache,
|
||||||
float SearchMinX, float SearchMinY,
|
float SearchMinX, float SearchMinY,
|
||||||
float SearchMaxX, float SearchMaxY,
|
float SearchMaxX, float SearchMaxY,
|
||||||
@@ -720,13 +734,37 @@ namespace VoxelCaveMorphology
|
|||||||
// per-room terrain op assigned to this voxel's room.
|
// per-room terrain op assigned to this voxel's room.
|
||||||
// (Room shape variety is baked into FCachedRoom by BuildChunkCache — no per-voxel roll.)
|
// (Room shape variety is baked into FCachedRoom by BuildChunkCache — no per-voxel roll.)
|
||||||
// @return negative = inside cave, positive = solid rock
|
// @return negative = inside cave, positive = solid rock
|
||||||
float EvaluateSDFCached(
|
VOXELFORGE_API float EvaluateSDFCached(
|
||||||
float WorldX, float WorldY, float WorldZ,
|
float WorldX, float WorldY, float WorldZ,
|
||||||
const FChunkSDFCache& Cache,
|
const FChunkSDFCache& Cache,
|
||||||
float SDFBlendRadius,
|
float SDFBlendRadius,
|
||||||
int32* OutNearestRoomIdx = nullptr
|
int32* OutNearestRoomIdx = nullptr
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Evaluate the raw union of cached graph-tunnel capsules. This is intentionally separate from
|
||||||
|
// the SmoothMin morphology: the generator uses it after terrain/passage posts to reassert the
|
||||||
|
// tunnel's walkable air core without reintroducing a connector or changing room ownership.
|
||||||
|
VOXELFORGE_API float EvaluateTunnelCoreSDF(
|
||||||
|
float WorldX, float WorldY, float WorldZ,
|
||||||
|
const FChunkSDFCache& Cache
|
||||||
|
);
|
||||||
|
|
||||||
|
// Evaluate the final structural air contract against the world-space wandering chain. Room
|
||||||
|
// SDFs remain evaluated in warped coordinates; only this post uses world coordinates so its
|
||||||
|
// walkable floor follows the authored chain exactly.
|
||||||
|
VOXELFORGE_API float EvaluateTunnelCoreWorldSDF(
|
||||||
|
float WorldX, float WorldY, float WorldZ,
|
||||||
|
const FChunkSDFCache& Cache
|
||||||
|
);
|
||||||
|
|
||||||
|
// True for the finite support slab beneath a world-space graph tunnel. The native density
|
||||||
|
// path and the operator stack use this same predicate before reopening the air core, keeping
|
||||||
|
// the rounded capsule's bottom from becoming an unsupported point.
|
||||||
|
VOXELFORGE_API bool IsTunnelSupportFloorWorldPoint(
|
||||||
|
float WorldX, float WorldY, float WorldZ,
|
||||||
|
const FChunkSDFCache& Cache
|
||||||
|
);
|
||||||
|
|
||||||
// CONVENIENCE WRAPPER: builds a temporary cache and evaluates in one call.
|
// CONVENIENCE WRAPPER: builds a temporary cache and evaluates in one call.
|
||||||
// Use this for one-off queries (debug visualization, single-point sampling).
|
// Use this for one-off queries (debug visualization, single-point sampling).
|
||||||
// For chunk generation, use BuildChunkCache + EvaluateSDFCached instead.
|
// For chunk generation, use BuildChunkCache + EvaluateSDFCached instead.
|
||||||
@@ -748,7 +786,9 @@ namespace VoxelCaveMorphology
|
|||||||
* clear capsule volume. The local source stencil deliberately does not claim flood-fill
|
* clear capsule volume. The local source stencil deliberately does not claim flood-fill
|
||||||
* connectivity or a measured component size; it selects a topology-backed feature core instead.
|
* connectivity or a measured component size; it selects a topology-backed feature core instead.
|
||||||
* For TunnelNetwork/Underwater, Seed must be the strate-specific room seed returned by
|
* For TunnelNetwork/Underwater, Seed must be the strate-specific room seed returned by
|
||||||
* VoxelCaveMorphology::MakeStrateSeed; for slab, maze, shaft, and island archetypes, Seed is the
|
* VoxelCaveMorphology::MakeStrateSeed; the optional WorldSeed selects the production cave-warp
|
||||||
|
* field and should be the generator world seed. If omitted, the room seed is used for both for
|
||||||
|
* backward-compatible standalone queries. For slab, maze, shaft, and island archetypes, Seed is the
|
||||||
* generator world seed consumed by their source. SurfaceWorld deliberately has no answer here:
|
* generator world seed consumed by their source. SurfaceWorld deliberately has no answer here:
|
||||||
* its production height can be selected by manager-owned biome context and per-chunk state,
|
* its production height can be selected by manager-owned biome context and per-chunk state,
|
||||||
* which this re-entrant query cannot inspect.
|
* which this re-entrant query cannot inspect.
|
||||||
@@ -772,7 +812,8 @@ VOXELFORGE_API bool VF_SuggestLandingPoint(
|
|||||||
float DesiredX,
|
float DesiredX,
|
||||||
float DesiredY,
|
float DesiredY,
|
||||||
float MaxLateralSnap,
|
float MaxLateralSnap,
|
||||||
FVector& OutPoint
|
FVector& OutPoint,
|
||||||
|
int32 WorldSeed = MIN_int32
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -797,5 +838,6 @@ VOXELFORGE_API bool VF_SuggestLandingPoint(
|
|||||||
float DesiredX,
|
float DesiredX,
|
||||||
float DesiredY,
|
float DesiredY,
|
||||||
float MaxLateralSnap,
|
float MaxLateralSnap,
|
||||||
FVector& OutPoint
|
FVector& OutPoint,
|
||||||
|
int32 WorldSeed = MIN_int32
|
||||||
);
|
);
|
||||||
@@ -250,7 +250,7 @@ public:
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* RÔLE 4 — ajoute les invariants de monde, dans l'ordre fixe, à la fin de la pile.
|
* RÔLE 4 — ajoute les invariants de monde, dans l'ordre fixe, à la fin de la pile.
|
||||||
* spine (0,0) → seal de frontière → carve de passage → seal de limite XY.
|
* spine (0,0) → seal de frontière → carve de passage → [posts spécifiques] → seal de limite XY.
|
||||||
*
|
*
|
||||||
* ⚠️ La couche de diff (édits joueur) n'est PAS ici : elle vit dans `GetDensityAt`, APRÈS la
|
* ⚠️ La couche de diff (édits joueur) n'est PAS ici : elle vit dans `GetDensityAt`, APRÈS la
|
||||||
* négation MC, avec les disturbances. Elle rejoindra la pile quand les disturbances seront
|
* négation MC, avec les disturbances. Elle rejoindra la pile quand les disturbances seront
|
||||||
@@ -266,7 +266,8 @@ public:
|
|||||||
*/
|
*/
|
||||||
VOXELFORGE_API void AppendStructuralPost(float StrateTopWorldZ, float StrateBottomWorldZ,
|
VOXELFORGE_API void AppendStructuralPost(float StrateTopWorldZ, float StrateBottomWorldZ,
|
||||||
float SealThickness, float BaseDensity, float SpineRadius,
|
float SealThickness, float BaseDensity, float SpineRadius,
|
||||||
const UVoxelStrateManager* StrateManager);
|
const UVoxelStrateManager* StrateManager,
|
||||||
|
bool bAppendEdgeSeal = true);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
TArray<FOpEntry> Ops;
|
TArray<FOpEntry> Ops;
|
||||||
@@ -387,10 +388,10 @@ namespace VoxelDensityOps
|
|||||||
bool bAppendStructuralPosts = true);
|
bool bAppendStructuralPosts = true);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TunnelNetwork — **COMPLET, 20 ops** :
|
* TunnelNetwork — **COMPLET** :
|
||||||
* ConstantRock → RoomGraph(warp + pits + cheminées) → SdfCarve → CaveRoughness(4b)
|
* ConstantRock → RoomGraph(warp + pits + cheminées) → SdfCarve → CaveRoughness(4b)
|
||||||
* → Terrace → LayerLines → Ribbing → Overhang → Cliff → Scallop → Arch → RoomColumn(4d)
|
* → Terrace → LayerLines → Ribbing → Overhang → Cliff → Scallop → Arch → RoomColumn(4d)
|
||||||
* → Dome(4g) → Pinch(4h) → FloorBias → Worms → [structural ×4]
|
* → Dome(4g) → Pinch(4h) → FloorBias → Worms → [spine, boundary, passage, graph-air, XY seal]
|
||||||
*
|
*
|
||||||
* L'override d'op PAR SALLE (étape C1) n'ajoute aucun opérateur : `FRoomGraphSource` publie
|
* L'override d'op PAR SALLE (étape C1) n'ajoute aucun opérateur : `FRoomGraphSource` publie
|
||||||
* `LocalParams()` — les params de la strate avec l'op de la salle la plus proche appliqué — et
|
* `LocalParams()` — les params de la strate avec l'op de la salle la plus proche appliqué — et
|
||||||
|
|||||||
@@ -140,9 +140,12 @@ FORCEINLINE void VF_ApplyOriginSpine(float& Density, float WorldX, float WorldY,
|
|||||||
&& WorldZ > Geometry.FloorZ - Geometry.FloorThickness
|
&& WorldZ > Geometry.FloorZ - Geometry.FloorThickness
|
||||||
&& FMath::Abs(WorldX) <= FMath::Max(Geometry.HalfWidth - 1.0f, 0.0f)
|
&& FMath::Abs(WorldX) <= FMath::Max(Geometry.HalfWidth - 1.0f, 0.0f)
|
||||||
&& FMath::Abs(WorldY) <= FMath::Max(Geometry.HalfWidth - 1.0f, 0.0f))
|
&& FMath::Abs(WorldY) <= FMath::Max(Geometry.HalfWidth - 1.0f, 0.0f))
|
||||||
|
{
|
||||||
|
if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker())
|
||||||
{
|
{
|
||||||
Density = FMath::Max(Density, BaseDensity);
|
Density = FMath::Max(Density, BaseDensity);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Reassert only the origin-room air volume after a post-process that may add rock. */
|
/** Reassert only the origin-room air volume after a post-process that may add rock. */
|
||||||
@@ -175,9 +178,12 @@ FORCEINLINE void VF_ApplyOriginLandingFloor(float& Density,
|
|||||||
&& WorldZ > Geometry.FloorZ - Geometry.FloorThickness
|
&& WorldZ > Geometry.FloorZ - Geometry.FloorThickness
|
||||||
&& FMath::Abs(WorldX) <= FMath::Max(Geometry.HalfWidth - 1.0f, 0.0f)
|
&& FMath::Abs(WorldX) <= FMath::Max(Geometry.HalfWidth - 1.0f, 0.0f)
|
||||||
&& FMath::Abs(WorldY) <= FMath::Max(Geometry.HalfWidth - 1.0f, 0.0f))
|
&& FMath::Abs(WorldY) <= FMath::Max(Geometry.HalfWidth - 1.0f, 0.0f))
|
||||||
|
{
|
||||||
|
if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker())
|
||||||
{
|
{
|
||||||
Density = FMath::Max(Density, BaseDensity);
|
Density = FMath::Max(Density, BaseDensity);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** MC-facing origin-room air backstop for the post-disturbance path. */
|
/** MC-facing origin-room air backstop for the post-disturbance path. */
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ namespace VoxelPassageGeometry
|
|||||||
constexpr float SealSafetyMarginVoxels = 4.0f;
|
constexpr float SealSafetyMarginVoxels = 4.0f;
|
||||||
constexpr float RoomRoundingVoxels = 1.5f;
|
constexpr float RoomRoundingVoxels = 1.5f;
|
||||||
constexpr float LandingCarveBlendVoxels = 4.0f;
|
constexpr float LandingCarveBlendVoxels = 4.0f;
|
||||||
|
// The origin landing is reserved for the same straight spine that the vertical-shaft
|
||||||
|
// generator uses: OriginSpineRadius on each side, with a three-voxel / 0.75 m floor ledge
|
||||||
|
// around it after the support patch's one-voxel inset. At the default radius (14 voxels) this
|
||||||
|
// is a 28-voxel / 7 m future shaft diameter.
|
||||||
|
constexpr float OriginShaftFloorClearanceVoxels = 4.0f;
|
||||||
// Level tunnel apron outside each room-floor edge. It covers the room/tunnel SDF blend and
|
// 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.
|
// 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
|
constexpr float WalkableTunnelLandingApronVoxels = 6.0f; // 1.5 m
|
||||||
@@ -33,18 +38,16 @@ namespace VoxelPassageGeometry
|
|||||||
// ownership band: the floor writer owns the plane, while the air writer owns the capsule
|
// 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.
|
// volume above it even when two overlapping passage projections differ by a fraction.
|
||||||
constexpr float WalkableTunnelFloorAirClearanceVoxels = 0.5f;
|
constexpr float WalkableTunnelFloorAirClearanceVoxels = 0.5f;
|
||||||
|
// The graph-tunnel post owns the interior above its floor after terrain and passage-floor
|
||||||
|
// writers have run. The support-floor predicate is also an exclusion mask for the air post,
|
||||||
|
// so overlapping graph tubes cannot erase one another's walkable support band.
|
||||||
|
constexpr float CaveTunnelAirCoreInsetVoxels = 0.5f;
|
||||||
|
|
||||||
// A 15 degree ramp is comfortably below the 44 degree engine walkability ceiling. The
|
// 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.
|
// latter is a scramble limit, not a sensible default for a tunnel the player walks through.
|
||||||
constexpr float WalkableTunnelMaxGradientDegrees = 15.0f;
|
constexpr float WalkableTunnelMaxGradientDegrees = 15.0f;
|
||||||
constexpr float WalkableTunnelMaxGradient = 0.2679491924311227f; // tan(15 degrees)
|
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.
|
// 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
|
// A tapered tube changes that floor by the radius delta, so the construction and the audit
|
||||||
// use this same conservative floor delta.
|
// use this same conservative floor delta.
|
||||||
@@ -53,6 +56,204 @@ namespace VoxelPassageGeometry
|
|||||||
return Centreline.Z - FMath::Abs(Radius);
|
return Centreline.Z - FMath::Abs(Radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vertical-shaft tree links are round air capsules, but the player-fit graph needs a
|
||||||
|
// deterministic support plane to walk between shafts. Keep that plane on the first
|
||||||
|
// seal-safe ledge shared by the whole strate, so every tree edge has the same walk level.
|
||||||
|
// This is shaft-network support, not an origin landing connector.
|
||||||
|
FORCEINLINE float VerticalShaftConnectorFloorThickness(float Radius)
|
||||||
|
{
|
||||||
|
return FMath::Min(
|
||||||
|
LandingFloorThicknessVoxels,
|
||||||
|
FMath::Max(0.5f, FMath::Abs(Radius) * 0.5f));
|
||||||
|
}
|
||||||
|
|
||||||
|
FORCEINLINE float VerticalShaftConnectorFloorZ(
|
||||||
|
float StrateBottomZ,
|
||||||
|
float InteriorBottomZ,
|
||||||
|
float InteriorTopZ,
|
||||||
|
float LedgeSpacing,
|
||||||
|
float LedgeDepth,
|
||||||
|
float ConnectorRadius)
|
||||||
|
{
|
||||||
|
const float FloorThickness = VerticalShaftConnectorFloorThickness(ConnectorRadius);
|
||||||
|
const float MinimumFloor = InteriorBottomZ
|
||||||
|
+ FloorThickness + SealSafetyMarginVoxels;
|
||||||
|
const float MaximumFloor = InteriorTopZ
|
||||||
|
- PlayerHeightVoxels - SealSafetyMarginVoxels;
|
||||||
|
if (!FMath::IsFinite(StrateBottomZ)
|
||||||
|
|| !FMath::IsFinite(InteriorBottomZ)
|
||||||
|
|| !FMath::IsFinite(InteriorTopZ)
|
||||||
|
|| !FMath::IsFinite(LedgeSpacing)
|
||||||
|
|| !FMath::IsFinite(LedgeDepth)
|
||||||
|
|| !FMath::IsFinite(MinimumFloor)
|
||||||
|
|| !FMath::IsFinite(MaximumFloor)
|
||||||
|
|| !(MaximumFloor > MinimumFloor))
|
||||||
|
{
|
||||||
|
return 0.5f * (InteriorBottomZ + InteriorTopZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LedgeSpacing > 0.0f && LedgeDepth > 0.0f)
|
||||||
|
{
|
||||||
|
const float RelativeBottom = InteriorBottomZ - StrateBottomZ;
|
||||||
|
const int32 FirstPeriod = FMath::FloorToInt(RelativeBottom / LedgeSpacing);
|
||||||
|
// The small fixed bound keeps malformed authored values from turning generation into
|
||||||
|
// an unbounded search. Normal shaft strates find the first or second period.
|
||||||
|
constexpr int32 MaxPeriods = 64;
|
||||||
|
for (int32 PeriodOffset = 0; PeriodOffset < MaxPeriods; ++PeriodOffset)
|
||||||
|
{
|
||||||
|
const float PeriodStart = static_cast<float>(FirstPeriod + PeriodOffset)
|
||||||
|
* LedgeSpacing;
|
||||||
|
const float Candidate = StrateBottomZ + PeriodStart + LedgeDepth;
|
||||||
|
if (Candidate > MinimumFloor && Candidate < MaximumFloor)
|
||||||
|
{
|
||||||
|
return Candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMath::Clamp(
|
||||||
|
0.5f * (InteriorBottomZ + InteriorTopZ), MinimumFloor, MaximumFloor);
|
||||||
|
}
|
||||||
|
|
||||||
|
FORCEINLINE float VerticalShaftConnectorCenterZ(
|
||||||
|
float StrateBottomZ,
|
||||||
|
float InteriorBottomZ,
|
||||||
|
float InteriorTopZ,
|
||||||
|
float LedgeSpacing,
|
||||||
|
float LedgeDepth,
|
||||||
|
float ConnectorRadius)
|
||||||
|
{
|
||||||
|
const float SafeRadius = FMath::Abs(ConnectorRadius);
|
||||||
|
const float FloorThickness = VerticalShaftConnectorFloorThickness(SafeRadius);
|
||||||
|
return VerticalShaftConnectorFloorZ(
|
||||||
|
StrateBottomZ, InteriorBottomZ, InteriorTopZ,
|
||||||
|
LedgeSpacing, LedgeDepth, SafeRadius)
|
||||||
|
+ SafeRadius - FloorThickness;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Add a flat, inset support band below one horizontal shaft-tree capsule. */
|
||||||
|
FORCEINLINE void VF_ApplyVerticalShaftConnectorFloor(
|
||||||
|
float& Density,
|
||||||
|
float WorldX,
|
||||||
|
float WorldY,
|
||||||
|
float WorldZ,
|
||||||
|
const FVector& Start,
|
||||||
|
const FVector& End,
|
||||||
|
float Radius,
|
||||||
|
float FloorZ,
|
||||||
|
float BaseDensity)
|
||||||
|
{
|
||||||
|
const float SafeRadius = FMath::Abs(Radius);
|
||||||
|
const float FloorThickness = VerticalShaftConnectorFloorThickness(SafeRadius);
|
||||||
|
const float FloorRadius = FMath::Max(0.5f, SafeRadius - 0.5f);
|
||||||
|
if (!(SafeRadius > 0.0f)
|
||||||
|
|| !(BaseDensity > 0.0f)
|
||||||
|
|| !FMath::IsFinite(WorldX)
|
||||||
|
|| !FMath::IsFinite(WorldY)
|
||||||
|
|| !FMath::IsFinite(WorldZ)
|
||||||
|
|| !FMath::IsFinite(FloorZ)
|
||||||
|
|| !FMath::IsFinite(Start.X) || !FMath::IsFinite(Start.Y)
|
||||||
|
|| !FMath::IsFinite(End.X) || !FMath::IsFinite(End.Y))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FVector2D A(Start.X, Start.Y);
|
||||||
|
const FVector2D Delta(End.X - Start.X, End.Y - Start.Y);
|
||||||
|
const float LengthSquared = Delta.SizeSquared();
|
||||||
|
const float T = LengthSquared > KINDA_SMALL_NUMBER
|
||||||
|
? FMath::Clamp(
|
||||||
|
FVector2D::DotProduct(FVector2D(WorldX, WorldY) - A, Delta)
|
||||||
|
/ LengthSquared,
|
||||||
|
0.0f, 1.0f)
|
||||||
|
: 0.0f;
|
||||||
|
const FVector2D Closest = A + Delta * T;
|
||||||
|
if ((FVector2D(WorldX, WorldY) - Closest).SizeSquared()
|
||||||
|
> FMath::Square(FloorRadius))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WorldZ <= FloorZ + KINDA_SMALL_NUMBER
|
||||||
|
&& WorldZ > FloorZ - FloorThickness)
|
||||||
|
{
|
||||||
|
Density = FMath::Max(Density, BaseDensity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyVerticalShaftConnectorAir runs before the generator's shared disturbance pass. Keep
|
||||||
|
// this one-bit, call-local hand-off so a bridge/ridge cannot refill the core it just opened.
|
||||||
|
// It is thread-local state, never a per-voxel cache, and is reset around every density query.
|
||||||
|
FORCEINLINE bool& VerticalShaftConnectorAirMarker()
|
||||||
|
{
|
||||||
|
static thread_local bool bMarked = false;
|
||||||
|
return bMarked;
|
||||||
|
}
|
||||||
|
|
||||||
|
FORCEINLINE void ResetVerticalShaftConnectorAirMarker()
|
||||||
|
{
|
||||||
|
VerticalShaftConnectorAirMarker() = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reassert the walkable air core above one horizontal shaft-tree floor band. */
|
||||||
|
FORCEINLINE void VF_ApplyVerticalShaftConnectorAir(
|
||||||
|
float& Density,
|
||||||
|
float WorldX,
|
||||||
|
float WorldY,
|
||||||
|
float WorldZ,
|
||||||
|
const FVector& Start,
|
||||||
|
const FVector& End,
|
||||||
|
float Radius,
|
||||||
|
float FloorZ,
|
||||||
|
float BaseDensity)
|
||||||
|
{
|
||||||
|
const float SafeRadius = FMath::Abs(Radius);
|
||||||
|
// Leave a one-voxel wall band for the roughness/solid shell, while retaining more than
|
||||||
|
// enough width for the 1.36-voxel player capsule and its support stencil.
|
||||||
|
const float CoreRadius = FMath::Max(0.5f, SafeRadius - 1.0f);
|
||||||
|
if (!(SafeRadius > 0.0f)
|
||||||
|
|| !(BaseDensity > 0.0f)
|
||||||
|
|| !FMath::IsFinite(WorldX)
|
||||||
|
|| !FMath::IsFinite(WorldY)
|
||||||
|
|| !FMath::IsFinite(WorldZ)
|
||||||
|
|| !FMath::IsFinite(FloorZ)
|
||||||
|
|| !FMath::IsFinite(Start.X) || !FMath::IsFinite(Start.Y)
|
||||||
|
|| !FMath::IsFinite(Start.Z)
|
||||||
|
|| !FMath::IsFinite(End.X) || !FMath::IsFinite(End.Y)
|
||||||
|
|| !FMath::IsFinite(End.Z))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The exact floor sample belongs to the air core's lower ownership edge. The floor
|
||||||
|
// writer only owns the three voxels below FloorZ, so clearing from the half-voxel band
|
||||||
|
// above it cannot remove support.
|
||||||
|
if (WorldZ + KINDA_SMALL_NUMBER
|
||||||
|
< FloorZ + WalkableTunnelFloorAirClearanceVoxels)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FVector Delta = End - Start;
|
||||||
|
const float LengthSquared = Delta.SizeSquared();
|
||||||
|
const float T = LengthSquared > KINDA_SMALL_NUMBER
|
||||||
|
? FMath::Clamp(FVector::DotProduct(FVector(WorldX, WorldY, WorldZ) - Start, Delta)
|
||||||
|
/ LengthSquared, 0.0f, 1.0f)
|
||||||
|
: 0.0f;
|
||||||
|
const FVector Closest = Start + Delta * T;
|
||||||
|
if ((FVector(WorldX, WorldY, WorldZ) - Closest).SizeSquared()
|
||||||
|
> FMath::Square(CoreRadius))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal convention: negative is air, positive is solid. This is intentionally a
|
||||||
|
// post-ledge write; a partial shaft shelf must not cap the tree route's player-height
|
||||||
|
// envelope after the capsule has already carved it.
|
||||||
|
VerticalShaftConnectorAirMarker() = true;
|
||||||
|
Density = FMath::Min(Density, -BaseDensity);
|
||||||
|
}
|
||||||
|
|
||||||
FORCEINLINE float TunnelFloorGradient(
|
FORCEINLINE float TunnelFloorGradient(
|
||||||
const FVector& Start, float StartRadius,
|
const FVector& Start, float StartRadius,
|
||||||
const FVector& End, float EndRadius)
|
const FVector& End, float EndRadius)
|
||||||
@@ -194,7 +395,14 @@ namespace VoxelPassageGeometry
|
|||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Result.HalfWidth = LandingHalfWidthForRadius(OriginRadius);
|
// Keep a solid walking ledge around the future shaft, not merely a room whose wall is
|
||||||
|
// tangent to it. The shaft diameter is 2 * OriginRadius; the extra four voxels leave a
|
||||||
|
// three-voxel / 0.75 m floor margin after the support patch's one-voxel inset at the
|
||||||
|
// authored 25 cm scale.
|
||||||
|
Result.HalfWidth = FMath::Max(
|
||||||
|
LandingHalfWidthForRadius(OriginRadius),
|
||||||
|
FMath::Max(FMath::Abs(OriginRadius), 1.0f)
|
||||||
|
+ OriginShaftFloorClearanceVoxels);
|
||||||
const float DesiredHeight = LandingHeightForRadius(OriginRadius);
|
const float DesiredHeight = LandingHeightForRadius(OriginRadius);
|
||||||
const float SafeSeal = FMath::Max(SealThickness, 0.0f);
|
const float SafeSeal = FMath::Max(SealThickness, 0.0f);
|
||||||
const float InnerBottom = StrateBottomZ + SafeSeal;
|
const float InnerBottom = StrateBottomZ + SafeSeal;
|
||||||
|
|||||||
@@ -393,9 +393,10 @@ public:
|
|||||||
//=========================================================================
|
//=========================================================================
|
||||||
|
|
||||||
// Authored radius (voxels) of the guaranteed (0,0) landing room in EVERY strate, regardless
|
// 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
|
// of archetype. The §6.5 body-sized formulas turn it into a flat-floored room reserved for a
|
||||||
// no longer bores a vertical column. 0 = disabled. The room is top-anchored inside each
|
// future straight shaft of diameter 2 * this radius. At the default 14-voxel radius that is
|
||||||
// strate, and the seals remain solid until a progression passage is opened.
|
// 28 voxels / 7 m, with a three-voxel / 0.75 m floor ledge around it. This setting does not bore the
|
||||||
|
// shaft today: lower seals remain solid until a progression passage is opened.
|
||||||
// 10-14 → compact landing room (default) · 20+ → broad landing room
|
// 10-14 → compact landing room (default) · 20+ → broad landing room
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Spine", meta = (ClampMin = "0.0"))
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Spine", meta = (ClampMin = "0.0"))
|
||||||
float OriginSpineRadius = 14.0f;
|
float OriginSpineRadius = 14.0f;
|
||||||
|
|||||||
@@ -35,7 +35,12 @@ struct VOXELFORGE_API FVoxelPlayerCapsuleConstants
|
|||||||
static constexpr float HeightVoxels = VoxelPassageGeometry::PlayerHeightVoxels;
|
static constexpr float HeightVoxels = VoxelPassageGeometry::PlayerHeightVoxels;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Read-only density source used by the offline Tier 2 pass for a materialised custom stack. */
|
/**
|
||||||
|
* Read-only density source used by the offline Tier 2 pass for a materialised custom stack.
|
||||||
|
*
|
||||||
|
* SampleDensity is called concurrently during grid sampling; implementations must be const,
|
||||||
|
* re-entrant, and safe for concurrent read-only calls, just like UVoxelGenerator::GetDensityAt.
|
||||||
|
*/
|
||||||
class VOXELFORGE_API IVoxelStrateDensitySampler
|
class VOXELFORGE_API IVoxelStrateDensitySampler
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -70,7 +75,7 @@ struct VOXELFORGE_API FVoxelStrateMeasureSettings
|
|||||||
TOptional<FVector2D> CoverPointB;
|
TOptional<FVector2D> CoverPointB;
|
||||||
float CoverMarginVoxels = 48.0f;
|
float CoverMarginVoxels = 48.0f;
|
||||||
// Origin-rooted topologies can route from one mouth to the other through the finite (0,0)
|
// Origin-rooted topologies can route from one mouth to the other through the finite (0,0)
|
||||||
// landing room and its explicitly carved connector network.
|
// landing room and its local room/join geometry. There is no hidden radial connector network.
|
||||||
// When true, include (0,0) in the fitted AABB before applying MaxCells. A caller must keep
|
// 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
|
// this enabled for that topology; an over-cap route window is then refused honestly instead
|
||||||
// of being cropped back to a mouth-only box.
|
// of being cropped back to a mouth-only box.
|
||||||
|
|||||||
@@ -620,8 +620,8 @@ struct VOXELFORGE_API FStrateGenerationParams
|
|||||||
|
|
||||||
// How much tunnel paths curve (in voxels of sideways displacement).
|
// How much tunnel paths curve (in voxels of sideways displacement).
|
||||||
// Without this, tunnels are straight lines between rooms.
|
// Without this, tunnels are straight lines between rooms.
|
||||||
// This adds a hash-derived midpoint offset to each tunnel, creating
|
// This adds deterministic hash-jittered control points along each tunnel, creating
|
||||||
// unique curves — some bend left, some right, some are nearly straight.
|
// unique wandering chains — some bend left, some right, some are nearly straight.
|
||||||
// Capped at 25% of tunnel length to prevent kinky short tunnels.
|
// Capped at 25% of tunnel length to prevent kinky short tunnels.
|
||||||
// 0 → perfectly straight tunnels (artificial look)
|
// 0 → perfectly straight tunnels (artificial look)
|
||||||
// 8-12→ gentle natural curves
|
// 8-12→ gentle natural curves
|
||||||
@@ -642,21 +642,20 @@ struct VOXELFORGE_API FStrateGenerationParams
|
|||||||
// Topology of the guaranteed tunnel network.
|
// Topology of the guaranteed tunnel network.
|
||||||
// true → each room links to its best candidate among rooms CLOSER to (0,0): the whole
|
// true → each room links to its best candidate among rooms CLOSER to (0,0): the whole
|
||||||
// network becomes a tree rooted at the origin room — every cave is reachable
|
// network becomes a tree rooted at the origin room — every cave is reachable
|
||||||
// from the spine hub, tunnels flow inward like tributaries (intentional descent
|
// from the graph root, tunnels flow inward like tributaries (intentional descent
|
||||||
// structure). TunnelDensity still adds loops on top.
|
// structure). This graph root is not a radial landing-to-hub road. TunnelDensity
|
||||||
|
// still adds loops on top.
|
||||||
// false → legacy nearest-neighbor pairing: organic scattered clusters, but connectivity
|
// false → legacy nearest-neighbor pairing: organic scattered clusters, but connectivity
|
||||||
// between clusters is NOT guaranteed (isolated pockets are common).
|
// between clusters is NOT guaranteed (isolated pockets are common).
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels")
|
||||||
bool bTunnelsFlowTowardOrigin = true;
|
bool bTunnelsFlowTowardOrigin = true;
|
||||||
|
|
||||||
// How much tunnel endpoints shift up/down within rooms (0-1).
|
// Vertical wander amount for tunnel control points (0-1). Endpoints are always tangent to
|
||||||
// Fraction of the room's vertical radius. Each tunnel endpoint gets
|
// their room's deterministic floor so a room join cannot be vertically severed; this value
|
||||||
// a hash-derived Z offset, so tunnels enter rooms at different heights.
|
// controls only the interior chain's bounded up/down variation.
|
||||||
// Combined with terrain ops (cliffs, terraces), this creates multi-level rooms
|
// 0.0 → level floor-to-floor chain
|
||||||
// where one tunnel exits at the upper ledge and another at the lower floor.
|
// 0.25 → restrained vertical wander (default)
|
||||||
// 0.0 → all tunnels connect at room center height
|
// 1.0 → strongest interior vertical variation
|
||||||
// 0.25 → endpoints use a restrained quarter-height offset (default)
|
|
||||||
// 1.0 → full range — tunnels can enter near floor or ceiling
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||||
float TunnelEndpointZOffset = 0.25f;
|
float TunnelEndpointZOffset = 0.25f;
|
||||||
|
|
||||||
@@ -1787,15 +1786,15 @@ struct VOXELFORGE_API FStratePassageConfig
|
|||||||
|
|
||||||
// ----- WIDTH (tapers along the length) -----
|
// ----- WIDTH (tapers along the length) -----
|
||||||
// Radius at the two mouths (entry/exit) and at the middle. Equal = uniform tube;
|
// Radius at the two mouths (entry/exit) and at the middle. Equal = uniform tube;
|
||||||
// The walkable default is deliberately single-file: 3.0 m diameter at mouths and 2.5 m at
|
// The walkable default is 1.5x the original comfortable profile: 4.5 m diameter at mouths
|
||||||
// the waist (6/5 voxel radii at 25 cm per voxel). The §6.5 landing room remains wider for
|
// and 3.75 m at the waist (9/7.5 voxel radii at 25 cm per voxel). The §6.5 landing room
|
||||||
// turning.
|
// remains wider for turning.
|
||||||
// Mouth > Mid = chambers at the ends with a squeeze between; Mid > Mouth = a bulge.
|
// Mouth > Mid = chambers at the ends with a squeeze between; Mid > Mouth = a bulge.
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Width", meta = (ClampMin = "1.0"))
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Width", meta = (ClampMin = "1.0"))
|
||||||
float MouthRadius = 6.0f;
|
float MouthRadius = 9.0f;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Width", meta = (ClampMin = "1.0"))
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Width", meta = (ClampMin = "1.0"))
|
||||||
float MidRadius = 5.0f;
|
float MidRadius = 7.5f;
|
||||||
|
|
||||||
// ----- LENGTH -----
|
// ----- LENGTH -----
|
||||||
// How far the tunnel reaches INTO each strate (voxels). Auto-capped to the interior;
|
// How far the tunnel reaches INTO each strate (voxels). Auto-capped to the interior;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#include "VoxelDiffLayer.h"
|
#include "VoxelDiffLayer.h"
|
||||||
#include "VoxelGenerator.h"
|
#include "VoxelGenerator.h"
|
||||||
#include "VoxelMarchingCubesMesher.h"
|
#include "VoxelMarchingCubesMesher.h"
|
||||||
|
#include "VoxelNoise.h"
|
||||||
#include "VoxelSettings.h"
|
#include "VoxelSettings.h"
|
||||||
#include "VoxelStrateDefinition.h"
|
#include "VoxelStrateDefinition.h"
|
||||||
#include "VoxelStrateManager.h"
|
#include "VoxelStrateManager.h"
|
||||||
@@ -32,21 +33,21 @@ namespace
|
|||||||
{
|
{
|
||||||
using FExploreJsonWriter = TJsonWriter<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>;
|
using FExploreJsonWriter = TJsonWriter<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>;
|
||||||
|
|
||||||
constexpr int32 DefaultRenderWidth = 192;
|
constexpr int32 DefaultRenderWidth = 512;
|
||||||
constexpr int32 DefaultRenderHeight = 128;
|
constexpr int32 DefaultRenderHeight = 288;
|
||||||
constexpr float DefaultRenderStepVoxels = 0.25f;
|
constexpr float DefaultRenderStepVoxels = 0.25f;
|
||||||
constexpr float DefaultRenderMaxDistanceVoxels = 256.0f;
|
constexpr float DefaultRenderMaxDistanceVoxels = 160.0f;
|
||||||
constexpr int32 DefaultExportSize = 128;
|
constexpr int32 DefaultExportSize = 128;
|
||||||
constexpr int32 DefaultMaxWalkCells = 32000000;
|
constexpr int32 DefaultMaxWalkCells = 32000000;
|
||||||
constexpr int32 MaxSyntheticStrates = 64;
|
constexpr int32 MaxSyntheticStrates = 64;
|
||||||
constexpr int32 MaxRenderPixels = 1048576;
|
constexpr int32 MaxRenderPixels = 1048576;
|
||||||
constexpr int64 MaxRenderDensitySamples = 200000000ll;
|
constexpr int64 MaxRenderDensitySamples = 400000000ll;
|
||||||
constexpr int32 MaxExportSize = 128;
|
constexpr int32 MaxExportSize = 128;
|
||||||
constexpr int64 MaxWalkWorkingBytes = 768ll * 1024ll * 1024ll;
|
constexpr int64 MaxWalkWorkingBytes = 768ll * 1024ll * 1024ll;
|
||||||
// The exact fine-grid walk retains Air + Density + player-fit bits, and can transiently hold
|
// The exact fine-grid walk retains Air + Density + player-fit bits + non-finite flags, and can
|
||||||
// Parent + Queue + Path or Visited + NarrowCell + DFS stacks. 20 bytes/cell is a conservative
|
// transiently hold Parent + Queue + Path or Visited + NarrowCell + DFS stacks. 21 bytes/cell is a
|
||||||
// preflight figure for those arrays, including allocator slack/headroom.
|
// conservative preflight figure for those arrays, including allocator slack/headroom.
|
||||||
constexpr int64 EstimatedWalkBytesPerCell = 20ll;
|
constexpr int64 EstimatedWalkBytesPerCell = 21ll;
|
||||||
constexpr int64 MaxExportWorkingBytes = 1024ll * 1024ll * 1024ll;
|
constexpr int64 MaxExportWorkingBytes = 1024ll * 1024ll * 1024ll;
|
||||||
|
|
||||||
const TCHAR* ArchetypeName(ECaveGeneratorType Archetype)
|
const TCHAR* ArchetypeName(ECaveGeneratorType Archetype)
|
||||||
@@ -475,6 +476,8 @@ struct FExploreRenderFrame
|
|||||||
{
|
{
|
||||||
FString FileName;
|
FString FileName;
|
||||||
FString Purpose = TEXT("overview");
|
FString Purpose = TEXT("overview");
|
||||||
|
FString CameraSeedSource = TEXT("player_fit");
|
||||||
|
FVector CameraSeedPoseVoxels = FVector::ZeroVector;
|
||||||
FVector CameraVoxels = FVector::ZeroVector;
|
FVector CameraVoxels = FVector::ZeroVector;
|
||||||
FVector TargetVoxels = FVector::ZeroVector;
|
FVector TargetVoxels = FVector::ZeroVector;
|
||||||
int32 HitPixels = 0;
|
int32 HitPixels = 0;
|
||||||
@@ -494,6 +497,9 @@ struct FExploreRenderOutput
|
|||||||
float MaxDistanceVoxels = 0.0f;
|
float MaxDistanceVoxels = 0.0f;
|
||||||
int64 EstimatedFixedDensitySamples = 0;
|
int64 EstimatedFixedDensitySamples = 0;
|
||||||
FString StepRationale;
|
FString StepRationale;
|
||||||
|
FString CameraSeedPolicy;
|
||||||
|
int32 CameraSeedCount = 0;
|
||||||
|
bool bAllCamerasPlayerFit = false;
|
||||||
TArray<FExploreRenderFrame> Frames;
|
TArray<FExploreRenderFrame> Frames;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -888,6 +894,7 @@ bool SavePng(
|
|||||||
bool RunRender(
|
bool RunRender(
|
||||||
const FExploreArguments& Arguments,
|
const FExploreArguments& Arguments,
|
||||||
const FExploreWorld& World,
|
const FExploreWorld& World,
|
||||||
|
const FExploreWalkOutput& CameraSeedWalk,
|
||||||
FExploreRenderOutput& OutOutput)
|
FExploreRenderOutput& OutOutput)
|
||||||
{
|
{
|
||||||
OutOutput = FExploreRenderOutput();
|
OutOutput = FExploreRenderOutput();
|
||||||
@@ -910,17 +917,120 @@ bool RunRender(
|
|||||||
"fixed 0.25-voxel default step (override with -renderstep), detects density sign crossings, "
|
"fixed 0.25-voxel default step (override with -renderstep), detects density sign crossings, "
|
||||||
"and refines each crossing with 10 bisection iterations; it never sphere-traces.");
|
"and refines each crossing with 10 bisection iterations; it never sphere-traces.");
|
||||||
|
|
||||||
const float MiddleZ = 0.5f * static_cast<float>(World.TargetBottomWorldZ + World.TargetTopWorldZ);
|
OutOutput.CameraSeedPolicy = TEXT(
|
||||||
const FVector Target(0.0f, 0.0f, MiddleZ);
|
"Every camera origin is a density-checked pose from the exact player-fit walk mask: "
|
||||||
const FVector CameraPositions[] = {
|
"arrival, departure, last reached, and agent-final poses are considered in fixed order; "
|
||||||
FVector(0.0f, -192.0f, MiddleZ + 32.0f),
|
"duplicate poses are removed. The target is offset toward the fitted route so the view is "
|
||||||
FVector(192.0f, -160.0f, MiddleZ + 64.0f),
|
"from inside the walkable space.");
|
||||||
FVector(-160.0f, 128.0f, MiddleZ + 96.0f),
|
|
||||||
|
TArray<TPair<FVector, FString>, TInlineAllocator<4>> CameraSeeds;
|
||||||
|
const auto AddCameraSeed = [&](bool bAvailable, const FVector& Position, const TCHAR* Source)
|
||||||
|
{
|
||||||
|
if (!bAvailable
|
||||||
|
|| !FMath::IsFinite(Position.X)
|
||||||
|
|| !FMath::IsFinite(Position.Y)
|
||||||
|
|| !FMath::IsFinite(Position.Z))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The walk mask is authoritative, but this guard catches stale/corrupt report data before
|
||||||
|
// a render can silently start inside rock.
|
||||||
|
const float Density = World.Generator->GetDensityAt(Position.X, Position.Y, Position.Z);
|
||||||
|
if (!FMath::IsFinite(Density) || !(Density > 0.0f))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const TPair<FVector, FString>& Existing : CameraSeeds)
|
||||||
|
{
|
||||||
|
if (Existing.Key.Equals(Position, 0.001f))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CameraSeeds.Emplace(Position, FString(Source));
|
||||||
};
|
};
|
||||||
|
|
||||||
for (int32 ViewIndex = 0; ViewIndex < UE_ARRAY_COUNT(CameraPositions); ++ViewIndex)
|
AddCameraSeed(
|
||||||
|
CameraSeedWalk.bHasArrival,
|
||||||
|
CameraSeedWalk.ArrivalVoxels,
|
||||||
|
TEXT("arrival_player_fit"));
|
||||||
|
AddCameraSeed(
|
||||||
|
CameraSeedWalk.bHasDeparture,
|
||||||
|
CameraSeedWalk.DepartureVoxels,
|
||||||
|
TEXT("departure_player_fit"));
|
||||||
|
AddCameraSeed(
|
||||||
|
CameraSeedWalk.Report.bHasLastReachedPosition,
|
||||||
|
CameraSeedWalk.Report.LastReachedVoxels,
|
||||||
|
TEXT("last_reached_player_fit"));
|
||||||
|
AddCameraSeed(
|
||||||
|
CameraSeedWalk.Report.bHasAgentFinalPosition,
|
||||||
|
CameraSeedWalk.Report.AgentFinalVoxels,
|
||||||
|
TEXT("agent_final_player_fit"));
|
||||||
|
|
||||||
|
if (CameraSeeds.Num() == 0)
|
||||||
{
|
{
|
||||||
const FVector CameraPosition = CameraPositions[ViewIndex];
|
OutOutput.Status = TEXT("refused");
|
||||||
|
OutOutput.RefusalReason = TEXT(
|
||||||
|
"The player-fit seed pass produced no density-positive pose; refusing an inside-rock camera.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
OutOutput.CameraSeedCount = CameraSeeds.Num();
|
||||||
|
OutOutput.bAllCamerasPlayerFit = true;
|
||||||
|
FVector RouteFocus = FVector::ZeroVector;
|
||||||
|
int32 RouteFocusCount = 0;
|
||||||
|
if (CameraSeedWalk.bHasArrival)
|
||||||
|
{
|
||||||
|
RouteFocus += CameraSeedWalk.ArrivalVoxels;
|
||||||
|
++RouteFocusCount;
|
||||||
|
}
|
||||||
|
if (CameraSeedWalk.bHasDeparture)
|
||||||
|
{
|
||||||
|
RouteFocus += CameraSeedWalk.DepartureVoxels;
|
||||||
|
++RouteFocusCount;
|
||||||
|
}
|
||||||
|
if (RouteFocusCount > 0)
|
||||||
|
{
|
||||||
|
RouteFocus /= static_cast<float>(RouteFocusCount);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RouteFocus = CameraSeeds[0].Key;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FVector KeyLightDirection = FVector(-0.35f, -0.45f, 0.82f).GetSafeNormal();
|
||||||
|
const FVector FillLightDirection = FVector(0.65f, 0.30f, 0.70f).GetSafeNormal();
|
||||||
|
constexpr float AmbientFill = 0.62f;
|
||||||
|
constexpr float KeyLightStrength = 0.50f;
|
||||||
|
constexpr float FillLightStrength = 0.24f;
|
||||||
|
|
||||||
|
for (int32 ViewIndex = 0; ViewIndex < CameraSeeds.Num(); ++ViewIndex)
|
||||||
|
{
|
||||||
|
const FVector CameraSeedPose = CameraSeeds[ViewIndex].Key;
|
||||||
|
const FVector CameraPosition = CameraSeedPose
|
||||||
|
+ FVector::UpVector * (FVoxelPlayerCapsuleConstants::HalfHeightVoxels - 0.5f);
|
||||||
|
const float CameraDensity = World.Generator->GetDensityAt(
|
||||||
|
CameraPosition.X, CameraPosition.Y, CameraPosition.Z);
|
||||||
|
if (!FMath::IsFinite(CameraDensity) || !(CameraDensity > 0.0f))
|
||||||
|
{
|
||||||
|
OutOutput.Status = TEXT("refused");
|
||||||
|
OutOutput.RefusalReason = TEXT(
|
||||||
|
"The eye-height offset of a player-fit camera entered solid density.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
FVector LookDirection = RouteFocus - CameraPosition;
|
||||||
|
LookDirection.Z = 0.0f;
|
||||||
|
LookDirection.Normalize();
|
||||||
|
if (LookDirection.IsNearlyZero())
|
||||||
|
{
|
||||||
|
LookDirection = FVector(
|
||||||
|
ViewIndex == 1 ? -0.6f : 0.8f,
|
||||||
|
ViewIndex == 2 ? 0.7f : -0.4f,
|
||||||
|
0.18f).GetSafeNormal();
|
||||||
|
}
|
||||||
|
// Keep the camera at the fitted pose, while aiming at roughly eye level in the route.
|
||||||
|
const FVector Target = CameraPosition + LookDirection * 96.0f + FVector::UpVector * 8.0f;
|
||||||
FExploreCameraBasis Basis;
|
FExploreCameraBasis Basis;
|
||||||
if (!BuildCameraBasis(
|
if (!BuildCameraBasis(
|
||||||
CameraPosition,
|
CameraPosition,
|
||||||
@@ -936,9 +1046,8 @@ bool RunRender(
|
|||||||
}
|
}
|
||||||
|
|
||||||
TArray<FColor> Pixels;
|
TArray<FColor> Pixels;
|
||||||
Pixels.Init(FColor(7, 11, 18, 255), Arguments.RenderWidth * Arguments.RenderHeight);
|
Pixels.Init(FColor(12, 18, 28, 255), Arguments.RenderWidth * Arguments.RenderHeight);
|
||||||
int32 HitPixels = 0;
|
int32 HitPixels = 0;
|
||||||
const FVector LightDirection = FVector(-0.35f, -0.45f, 0.82f).GetSafeNormal();
|
|
||||||
for (int32 PixelY = 0; PixelY < Arguments.RenderHeight; ++PixelY)
|
for (int32 PixelY = 0; PixelY < Arguments.RenderHeight; ++PixelY)
|
||||||
{
|
{
|
||||||
for (int32 PixelX = 0; PixelX < Arguments.RenderWidth; ++PixelX)
|
for (int32 PixelX = 0; PixelX < Arguments.RenderWidth; ++PixelX)
|
||||||
@@ -965,11 +1074,16 @@ bool RunRender(
|
|||||||
}
|
}
|
||||||
|
|
||||||
++HitPixels;
|
++HitPixels;
|
||||||
const float Diffuse = FMath::Max(0.0f, FVector::DotProduct(Normal, LightDirection));
|
const float Key = FMath::Max(
|
||||||
const float Brightness = 0.18f + 0.82f * Diffuse;
|
0.0f, FVector::DotProduct(Normal, KeyLightDirection));
|
||||||
const uint8 RockR = static_cast<uint8>(FMath::Clamp(54.0f * Brightness, 0.0f, 255.0f));
|
const float Fill = FMath::Max(
|
||||||
const uint8 RockG = static_cast<uint8>(FMath::Clamp(72.0f * Brightness, 0.0f, 255.0f));
|
0.0f, FVector::DotProduct(Normal, FillLightDirection));
|
||||||
const uint8 RockB = static_cast<uint8>(FMath::Clamp(92.0f * Brightness, 0.0f, 255.0f));
|
const float Brightness = FMath::Clamp(
|
||||||
|
AmbientFill + KeyLightStrength * Key + FillLightStrength * Fill,
|
||||||
|
0.0f, 1.25f);
|
||||||
|
const uint8 RockR = static_cast<uint8>(FMath::Clamp(100.0f * Brightness, 0.0f, 255.0f));
|
||||||
|
const uint8 RockG = static_cast<uint8>(FMath::Clamp(128.0f * Brightness, 0.0f, 255.0f));
|
||||||
|
const uint8 RockB = static_cast<uint8>(FMath::Clamp(160.0f * Brightness, 0.0f, 255.0f));
|
||||||
PutPixel(
|
PutPixel(
|
||||||
Pixels,
|
Pixels,
|
||||||
Arguments.RenderWidth,
|
Arguments.RenderWidth,
|
||||||
@@ -992,6 +1106,9 @@ bool RunRender(
|
|||||||
|
|
||||||
FExploreRenderFrame& Frame = OutOutput.Frames.AddDefaulted_GetRef();
|
FExploreRenderFrame& Frame = OutOutput.Frames.AddDefaulted_GetRef();
|
||||||
Frame.FileName = FString::Printf(TEXT("render_%02d.png"), ViewIndex);
|
Frame.FileName = FString::Printf(TEXT("render_%02d.png"), ViewIndex);
|
||||||
|
Frame.Purpose = CameraSeeds[ViewIndex].Value;
|
||||||
|
Frame.CameraSeedSource = CameraSeeds[ViewIndex].Value;
|
||||||
|
Frame.CameraSeedPoseVoxels = CameraSeedPose;
|
||||||
Frame.CameraVoxels = CameraPosition;
|
Frame.CameraVoxels = CameraPosition;
|
||||||
Frame.TargetVoxels = Target;
|
Frame.TargetVoxels = Target;
|
||||||
Frame.HitPixels = HitPixels;
|
Frame.HitPixels = HitPixels;
|
||||||
@@ -1086,17 +1203,12 @@ bool RunFailureBoundaryRender(
|
|||||||
}
|
}
|
||||||
const float SegmentLength = Segment.Size();
|
const float SegmentLength = Segment.Size();
|
||||||
const float ViewSideOffset = FMath::Min(64.0f, FMath::Max(16.0f, 0.25f * SegmentLength));
|
const float ViewSideOffset = FMath::Min(64.0f, FMath::Max(16.0f, 0.25f * SegmentLength));
|
||||||
const FVector CameraCandidates[][3] = {
|
// Failure-focus views are still required to originate from the fitted graph. The old
|
||||||
{
|
// implementation nudged the camera four voxels toward/away from the boundary and could put
|
||||||
AgentFinal - SegmentDirection * 4.0f,
|
// the viewpoint in the very rock that this diagnostic is meant to explain.
|
||||||
bHaveLastReached ? LastReached : AgentFinal,
|
const FVector CameraCandidates[] = {
|
||||||
AgentFinal,
|
AgentFinal,
|
||||||
},
|
bHaveLastReached ? LastReached : AgentFinal,
|
||||||
{
|
|
||||||
TargetComponentCell + SegmentDirection * 4.0f,
|
|
||||||
TargetComponentCell,
|
|
||||||
TargetComponentCell,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
const FVector ViewTargets[] = {
|
const FVector ViewTargets[] = {
|
||||||
Focus + ViewSide * ViewSideOffset,
|
Focus + ViewSide * ViewSideOffset,
|
||||||
@@ -1105,16 +1217,17 @@ bool RunFailureBoundaryRender(
|
|||||||
|
|
||||||
for (int32 ViewIndex = 0; ViewIndex < UE_ARRAY_COUNT(ViewTargets); ++ViewIndex)
|
for (int32 ViewIndex = 0; ViewIndex < UE_ARRAY_COUNT(ViewTargets); ++ViewIndex)
|
||||||
{
|
{
|
||||||
FVector CameraPosition = CameraCandidates[ViewIndex][2];
|
const FVector CameraSeedPose = CameraCandidates[ViewIndex];
|
||||||
for (int32 CandidateIndex = 0; CandidateIndex < 3; ++CandidateIndex)
|
const FVector CameraPosition = CameraSeedPose
|
||||||
|
+ FVector::UpVector * (FVoxelPlayerCapsuleConstants::HalfHeightVoxels - 0.5f);
|
||||||
|
const float CameraDensity = World.Generator->GetDensityAt(
|
||||||
|
CameraPosition.X, CameraPosition.Y, CameraPosition.Z);
|
||||||
|
if (!FMath::IsFinite(CameraDensity) || !(CameraDensity > 0.0f))
|
||||||
{
|
{
|
||||||
const FVector Candidate = CameraCandidates[ViewIndex][CandidateIndex];
|
UE_LOG(LogTemp, Warning,
|
||||||
const float Density = World.Generator->GetDensityAt(Candidate.X, Candidate.Y, Candidate.Z);
|
TEXT("[VoxelForgeExplore] refusing failure-focus camera %d: fitted pose is not air."),
|
||||||
if (FMath::IsFinite(Density) && Density > 0.0f)
|
ViewIndex);
|
||||||
{
|
return false;
|
||||||
CameraPosition = Candidate;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const float FailureMaxDistanceVoxels = FMath::Max(
|
const float FailureMaxDistanceVoxels = FMath::Max(
|
||||||
Arguments.RenderMaxDistanceVoxels,
|
Arguments.RenderMaxDistanceVoxels,
|
||||||
@@ -1134,9 +1247,13 @@ bool RunFailureBoundaryRender(
|
|||||||
}
|
}
|
||||||
|
|
||||||
TArray<FColor> Pixels;
|
TArray<FColor> Pixels;
|
||||||
Pixels.Init(FColor(7, 11, 18, 255), Arguments.RenderWidth * Arguments.RenderHeight);
|
Pixels.Init(FColor(12, 18, 28, 255), Arguments.RenderWidth * Arguments.RenderHeight);
|
||||||
int32 HitPixels = 0;
|
int32 HitPixels = 0;
|
||||||
const FVector LightDirection = FVector(-0.35f, -0.45f, 0.82f).GetSafeNormal();
|
const FVector KeyLightDirection = FVector(-0.35f, -0.45f, 0.82f).GetSafeNormal();
|
||||||
|
const FVector FillLightDirection = FVector(0.65f, 0.30f, 0.70f).GetSafeNormal();
|
||||||
|
constexpr float AmbientFill = 0.62f;
|
||||||
|
constexpr float KeyLightStrength = 0.50f;
|
||||||
|
constexpr float FillLightStrength = 0.24f;
|
||||||
for (int32 PixelY = 0; PixelY < Arguments.RenderHeight; ++PixelY)
|
for (int32 PixelY = 0; PixelY < Arguments.RenderHeight; ++PixelY)
|
||||||
{
|
{
|
||||||
for (int32 PixelX = 0; PixelX < Arguments.RenderWidth; ++PixelX)
|
for (int32 PixelX = 0; PixelX < Arguments.RenderWidth; ++PixelX)
|
||||||
@@ -1163,11 +1280,16 @@ bool RunFailureBoundaryRender(
|
|||||||
}
|
}
|
||||||
|
|
||||||
++HitPixels;
|
++HitPixels;
|
||||||
const float Diffuse = FMath::Max(0.0f, FVector::DotProduct(Normal, LightDirection));
|
const float Key = FMath::Max(
|
||||||
const float Brightness = 0.18f + 0.82f * Diffuse;
|
0.0f, FVector::DotProduct(Normal, KeyLightDirection));
|
||||||
const uint8 RockR = static_cast<uint8>(FMath::Clamp(54.0f * Brightness, 0.0f, 255.0f));
|
const float Fill = FMath::Max(
|
||||||
const uint8 RockG = static_cast<uint8>(FMath::Clamp(72.0f * Brightness, 0.0f, 255.0f));
|
0.0f, FVector::DotProduct(Normal, FillLightDirection));
|
||||||
const uint8 RockB = static_cast<uint8>(FMath::Clamp(92.0f * Brightness, 0.0f, 255.0f));
|
const float Brightness = FMath::Clamp(
|
||||||
|
AmbientFill + KeyLightStrength * Key + FillLightStrength * Fill,
|
||||||
|
0.0f, 1.25f);
|
||||||
|
const uint8 RockR = static_cast<uint8>(FMath::Clamp(100.0f * Brightness, 0.0f, 255.0f));
|
||||||
|
const uint8 RockG = static_cast<uint8>(FMath::Clamp(128.0f * Brightness, 0.0f, 255.0f));
|
||||||
|
const uint8 RockB = static_cast<uint8>(FMath::Clamp(160.0f * Brightness, 0.0f, 255.0f));
|
||||||
PutPixel(
|
PutPixel(
|
||||||
Pixels,
|
Pixels,
|
||||||
Arguments.RenderWidth,
|
Arguments.RenderWidth,
|
||||||
@@ -1243,6 +1365,10 @@ bool RunFailureBoundaryRender(
|
|||||||
FExploreRenderFrame& Frame = InOutOutput.Frames.AddDefaulted_GetRef();
|
FExploreRenderFrame& Frame = InOutOutput.Frames.AddDefaulted_GetRef();
|
||||||
Frame.FileName = FString::Printf(TEXT("failure_boundary_%02d.png"), ViewIndex);
|
Frame.FileName = FString::Printf(TEXT("failure_boundary_%02d.png"), ViewIndex);
|
||||||
Frame.Purpose = TEXT("agent_final_red_target_component_nearest_green_last_reached_orange");
|
Frame.Purpose = TEXT("agent_final_red_target_component_nearest_green_last_reached_orange");
|
||||||
|
Frame.CameraSeedSource = bHaveLastReached && ViewIndex == 1
|
||||||
|
? TEXT("last_reached_player_fit")
|
||||||
|
: TEXT("agent_final_player_fit");
|
||||||
|
Frame.CameraSeedPoseVoxels = CameraSeedPose;
|
||||||
Frame.CameraVoxels = CameraPosition;
|
Frame.CameraVoxels = CameraPosition;
|
||||||
Frame.TargetVoxels = ViewTargets[ViewIndex];
|
Frame.TargetVoxels = ViewTargets[ViewIndex];
|
||||||
Frame.HitPixels = HitPixels;
|
Frame.HitPixels = HitPixels;
|
||||||
@@ -1273,8 +1399,8 @@ bool RunWalk(
|
|||||||
{
|
{
|
||||||
OutOutput = FExploreWalkOutput();
|
OutOutput = FExploreWalkOutput();
|
||||||
OutOutput.WindowPolicy = NeedsOriginInWindow(Arguments.Archetype)
|
OutOutput.WindowPolicy = NeedsOriginInWindow(Arguments.Archetype)
|
||||||
? TEXT("origin-inclusive mouth-AABB + margin")
|
? TEXT("origin-inclusive target-strate passage-envelope AABB + margin")
|
||||||
: TEXT("mouth-AABB + margin");
|
: TEXT("target-strate passage-envelope AABB + margin");
|
||||||
|
|
||||||
const int64 EstimatedWorkingBytes = static_cast<int64>(Arguments.MaxWalkCells)
|
const int64 EstimatedWorkingBytes = static_cast<int64>(Arguments.MaxWalkCells)
|
||||||
* EstimatedWalkBytesPerCell;
|
* EstimatedWalkBytesPerCell;
|
||||||
@@ -1308,6 +1434,57 @@ bool RunWalk(
|
|||||||
OutOutput.bHasArrival = true;
|
OutOutput.bHasArrival = true;
|
||||||
OutOutput.bHasDeparture = true;
|
OutOutput.bHasDeparture = true;
|
||||||
|
|
||||||
|
// Temporary seed-14 probe while diagnosing the fitted VerticalShafts junction. This is
|
||||||
|
// intentionally narrow and will be removed after the floor/tree seam is corrected.
|
||||||
|
if (Arguments.Archetype == ECaveGeneratorType::VerticalShafts && Arguments.Seed == 14)
|
||||||
|
{
|
||||||
|
const FVector ProbeA = OutOutput.ArrivalVoxels;
|
||||||
|
const FVector ProbeB(-100.8f, -26.9f, ProbeA.Z);
|
||||||
|
for (int32 Index = 0; Index <= 10; ++Index)
|
||||||
|
{
|
||||||
|
const float T = static_cast<float>(Index) / 10.0f;
|
||||||
|
const FVector P = FMath::Lerp(ProbeA, ProbeB, T);
|
||||||
|
const float Dm704 = World.Generator->GetDensityAt(P.X, P.Y, -704.0f);
|
||||||
|
const float Dm702 = World.Generator->GetDensityAt(P.X, P.Y, -702.0f);
|
||||||
|
const float Dm7005 = World.Generator->GetDensityAt(P.X, P.Y, -700.5f);
|
||||||
|
const float Dm700 = World.Generator->GetDensityAt(P.X, P.Y, -700.0f);
|
||||||
|
const float Dm6995 = World.Generator->GetDensityAt(P.X, P.Y, -699.5f);
|
||||||
|
const float Dm697 = World.Generator->GetDensityAt(P.X, P.Y, -697.0f);
|
||||||
|
float MinBody = FLT_MAX;
|
||||||
|
int32 NumSolidBody = 0;
|
||||||
|
int32 SolidOffsetX = 0;
|
||||||
|
int32 SolidOffsetY = 0;
|
||||||
|
int32 SolidBodyRow = -1;
|
||||||
|
for (int32 OffsetY = -1; OffsetY <= 1; ++OffsetY)
|
||||||
|
{
|
||||||
|
for (int32 OffsetX = -1; OffsetX <= 1; ++OffsetX)
|
||||||
|
{
|
||||||
|
for (int32 BodyRow = 0; BodyRow < 8; ++BodyRow)
|
||||||
|
{
|
||||||
|
const float BodyDensity = World.Generator->GetDensityAt(
|
||||||
|
P.X + static_cast<float>(OffsetX),
|
||||||
|
P.Y + static_cast<float>(OffsetY),
|
||||||
|
-699.5f + static_cast<float>(BodyRow));
|
||||||
|
if (BodyDensity < MinBody)
|
||||||
|
{
|
||||||
|
MinBody = BodyDensity;
|
||||||
|
SolidOffsetX = OffsetX;
|
||||||
|
SolidOffsetY = OffsetY;
|
||||||
|
SolidBodyRow = BodyRow;
|
||||||
|
}
|
||||||
|
NumSolidBody += BodyDensity <= 0.0f ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UE_LOG(LogTemp, Display,
|
||||||
|
TEXT("[VoxelForgeExplore][TemporaryShaftProbe] t=%.2f xy=(%.2f,%.2f) "
|
||||||
|
"d[-704,-702,-700.5,-700,-699.5,-697]=[%.3f,%.3f,%.3f,%.3f,%.3f,%.3f] "
|
||||||
|
"bodyMin=%.3f bodySolid=%d/72 firstMinOffset=(%d,%d) row=%d"),
|
||||||
|
T, P.X, P.Y, Dm704, Dm702, Dm7005, Dm700, Dm6995, Dm697,
|
||||||
|
MinBody, NumSolidBody, SolidOffsetX, SolidOffsetY, SolidBodyRow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
FVoxelStrateMeasureSettings Settings;
|
FVoxelStrateMeasureSettings Settings;
|
||||||
Settings.SampleStep = 1;
|
Settings.SampleStep = 1;
|
||||||
Settings.MaxCells = Arguments.MaxWalkCells;
|
Settings.MaxCells = Arguments.MaxWalkCells;
|
||||||
@@ -1316,9 +1493,86 @@ bool RunWalk(
|
|||||||
Settings.CoverPointB = FVector2D(OutOutput.DepartureVoxels.X, OutOutput.DepartureVoxels.Y);
|
Settings.CoverPointB = FVector2D(OutOutput.DepartureVoxels.X, OutOutput.DepartureVoxels.Y);
|
||||||
Settings.CoverMarginVoxels = FMath::CeilToFloat(
|
Settings.CoverMarginVoxels = FMath::CeilToFloat(
|
||||||
Settings.PlayerCapsuleRadiusVoxels) + 2.0f;
|
Settings.PlayerCapsuleRadiusVoxels) + 2.0f;
|
||||||
|
|
||||||
|
// A mouth-AABB is not a landing window: the landing door and its local apron can legitimately
|
||||||
|
// leave that box before the player reaches the next room. Expand the same two-point box just
|
||||||
|
// enough to contain the selected production mouth envelope. This is still a bounded,
|
||||||
|
// deterministic measurement window; the central inter-strate descent belongs to the adjacent
|
||||||
|
// strata and is not folded into this intra-strate walk question.
|
||||||
|
const float WindowMinX = FMath::Min(OutOutput.ArrivalVoxels.X, OutOutput.DepartureVoxels.X);
|
||||||
|
const float WindowMaxX = FMath::Max(OutOutput.ArrivalVoxels.X, OutOutput.DepartureVoxels.X);
|
||||||
|
const float WindowMinY = FMath::Min(OutOutput.ArrivalVoxels.Y, OutOutput.DepartureVoxels.Y);
|
||||||
|
const float WindowMaxY = FMath::Max(OutOutput.ArrivalVoxels.Y, OutOutput.DepartureVoxels.Y);
|
||||||
|
const auto IncludeTargetPassageEnvelope = [&](
|
||||||
|
const FVoxelPassage* Passage,
|
||||||
|
const FVoxelPassageLanding& Landing)
|
||||||
|
{
|
||||||
|
if (Passage == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto IncludeXY = [&](const FVector& Point, float Radius)
|
||||||
|
{
|
||||||
|
if (!FMath::IsFinite(Point.X) || !FMath::IsFinite(Point.Y)
|
||||||
|
|| !FMath::IsFinite(Point.Z) || !FMath::IsFinite(Radius))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const float AbsRadius = FMath::Abs(Radius);
|
||||||
|
// Include control points whose centreline belongs to the measured strate. A control
|
||||||
|
// point just outside the boundary belongs to the adjacent strate's descent; pulling
|
||||||
|
// that whole switchback into this bounded walk box can multiply the sampled volume
|
||||||
|
// without improving the intra-strate mouth-to-mouth question.
|
||||||
|
if (Point.Z < static_cast<float>(World.TargetBottomWorldZ)
|
||||||
|
|| Point.Z > static_cast<float>(World.TargetTopWorldZ))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const float ExcessX = FMath::Max(
|
||||||
|
0.0f,
|
||||||
|
FMath::Max(
|
||||||
|
WindowMinX - (Point.X + AbsRadius),
|
||||||
|
(Point.X - AbsRadius) - WindowMaxX));
|
||||||
|
const float ExcessY = FMath::Max(
|
||||||
|
0.0f,
|
||||||
|
FMath::Max(
|
||||||
|
WindowMinY - (Point.Y + AbsRadius),
|
||||||
|
(Point.Y - AbsRadius) - WindowMaxY));
|
||||||
|
Settings.CoverMarginVoxels = FMath::Max(
|
||||||
|
Settings.CoverMarginVoxels,
|
||||||
|
FMath::Max(ExcessX, ExcessY));
|
||||||
|
};
|
||||||
|
|
||||||
|
IncludeXY(
|
||||||
|
Landing.StandingPoint,
|
||||||
|
Landing.HalfWidth + VoxelPassageGeometry::LandingCarveBlendVoxels);
|
||||||
|
// The first/last three points are the production mouth, apron, and first slope section.
|
||||||
|
// Do not pull the whole inter-strate switchback into the bounded target-strate test.
|
||||||
|
const int32 NumLocalPoints = FMath::Min(3, Passage->ControlPoints.Num());
|
||||||
|
const bool bUseUpperEnd = &Landing == &Passage->UpperLanding;
|
||||||
|
const int32 FirstPoint = bUseUpperEnd
|
||||||
|
? 0 : FMath::Max(0, Passage->ControlPoints.Num() - NumLocalPoints);
|
||||||
|
const int32 LastPoint = bUseUpperEnd
|
||||||
|
? NumLocalPoints : Passage->ControlPoints.Num();
|
||||||
|
for (int32 PointIndex = FirstPoint; PointIndex < LastPoint; ++PointIndex)
|
||||||
|
{
|
||||||
|
const float Radius = Passage->ControlRadii.IsValidIndex(PointIndex)
|
||||||
|
? Passage->ControlRadii[PointIndex] : Passage->Radius;
|
||||||
|
IncludeXY(Passage->ControlPoints[PointIndex], Radius
|
||||||
|
+ VoxelPassageGeometry::LandingCarveBlendVoxels);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
IncludeTargetPassageEnvelope(
|
||||||
|
ArrivalPassage,
|
||||||
|
ArrivalPassage != nullptr ? ArrivalPassage->LowerLanding : FVoxelPassageLanding());
|
||||||
|
IncludeTargetPassageEnvelope(
|
||||||
|
DeparturePassage,
|
||||||
|
DeparturePassage != nullptr ? DeparturePassage->UpperLanding : FVoxelPassageLanding());
|
||||||
Settings.bIncludeOriginInCoverWindow = NeedsOriginInWindow(Arguments.Archetype);
|
Settings.bIncludeOriginInCoverWindow = NeedsOriginInWindow(Arguments.Archetype);
|
||||||
|
|
||||||
TArray<FVector> LandingRoomProbePoints;
|
TArray<FVector> LandingRoomProbePoints;
|
||||||
|
|
||||||
auto AppendLandingRoomProbes = [&LandingRoomProbePoints](
|
auto AppendLandingRoomProbes = [&LandingRoomProbePoints](
|
||||||
const FVoxelPassageLanding& Landing)
|
const FVoxelPassageLanding& Landing)
|
||||||
{
|
{
|
||||||
@@ -1386,9 +1640,12 @@ bool RunWalk(
|
|||||||
}
|
}
|
||||||
const int32 ArrivalProbeCount = ArrivalPassage != nullptr ? 5 : 0;
|
const int32 ArrivalProbeCount = ArrivalPassage != nullptr ? 5 : 0;
|
||||||
const int32 DepartureProbeCount = DeparturePassage != nullptr ? 5 : 0;
|
const int32 DepartureProbeCount = DeparturePassage != nullptr ? 5 : 0;
|
||||||
|
const int32 LandingProbeCount = ArrivalProbeCount + DepartureProbeCount;
|
||||||
OutOutput.ArrivalLandingRoomProbeCount = ArrivalProbeCount;
|
OutOutput.ArrivalLandingRoomProbeCount = ArrivalProbeCount;
|
||||||
OutOutput.DepartureLandingRoomProbeCount = DepartureProbeCount;
|
OutOutput.DepartureLandingRoomProbeCount = DepartureProbeCount;
|
||||||
for (int32 ProbeIndex = 0; ProbeIndex < OutOutput.Report.ComponentProbePlayerFit.Num();
|
for (int32 ProbeIndex = 0;
|
||||||
|
ProbeIndex < FMath::Min(LandingProbeCount,
|
||||||
|
OutOutput.Report.ComponentProbePlayerFit.Num());
|
||||||
++ProbeIndex)
|
++ProbeIndex)
|
||||||
{
|
{
|
||||||
const bool bPlayerFit = OutOutput.Report.ComponentProbePlayerFit[ProbeIndex] != 0u;
|
const bool bPlayerFit = OutOutput.Report.ComponentProbePlayerFit[ProbeIndex] != 0u;
|
||||||
@@ -1408,7 +1665,6 @@ bool RunWalk(
|
|||||||
bInArrivalComponent ? 1 : 0;
|
bInArrivalComponent ? 1 : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
OutOutput.Status = TEXT("ok");
|
OutOutput.Status = TEXT("ok");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1829,11 +2085,15 @@ FString BuildExploreJson(
|
|||||||
Writer->WriteValue(TEXT("bisection_iterations"), Output.Render.BisectionIterations);
|
Writer->WriteValue(TEXT("bisection_iterations"), Output.Render.BisectionIterations);
|
||||||
Writer->WriteValue(TEXT("human_marker_height_m"), 1.76);
|
Writer->WriteValue(TEXT("human_marker_height_m"), 1.76);
|
||||||
Writer->WriteValue(TEXT("step_rationale"), Output.Render.StepRationale);
|
Writer->WriteValue(TEXT("step_rationale"), Output.Render.StepRationale);
|
||||||
|
Writer->WriteValue(TEXT("camera_seed_policy"), Output.Render.CameraSeedPolicy);
|
||||||
|
Writer->WriteValue(TEXT("camera_seed_count"), Output.Render.CameraSeedCount);
|
||||||
|
Writer->WriteValue(TEXT("all_cameras_player_fit"), Output.Render.bAllCamerasPlayerFit);
|
||||||
Writer->WriteArrayStart(TEXT("images"));
|
Writer->WriteArrayStart(TEXT("images"));
|
||||||
for (const FExploreRenderFrame& Frame : Output.Render.Frames)
|
for (const FExploreRenderFrame& Frame : Output.Render.Frames)
|
||||||
{
|
{
|
||||||
Writer->WriteObjectStart();
|
Writer->WriteObjectStart();
|
||||||
Writer->WriteValue(TEXT("purpose"), Frame.Purpose);
|
Writer->WriteValue(TEXT("purpose"), Frame.Purpose);
|
||||||
|
Writer->WriteValue(TEXT("camera_seed_source"), Frame.CameraSeedSource);
|
||||||
Writer->WriteValue(TEXT("file"), Frame.FileName);
|
Writer->WriteValue(TEXT("file"), Frame.FileName);
|
||||||
WriteJsonVector(*Writer, TEXT("camera_voxels"), Frame.CameraVoxels, 1.0f);
|
WriteJsonVector(*Writer, TEXT("camera_voxels"), Frame.CameraVoxels, 1.0f);
|
||||||
WriteJsonVector(*Writer, TEXT("target_voxels"), Frame.TargetVoxels, 1.0f);
|
WriteJsonVector(*Writer, TEXT("target_voxels"), Frame.TargetVoxels, 1.0f);
|
||||||
@@ -2102,16 +2362,8 @@ int32 UVoxelForgeExploreCommandlet::Main(const FString& Params)
|
|||||||
|
|
||||||
FExploreRunOutput Output;
|
FExploreRunOutput Output;
|
||||||
bool bRequestedModeFailed = false;
|
bool bRequestedModeFailed = false;
|
||||||
if (Arguments.bRender)
|
FExploreWalkOutput RenderSeedWalk;
|
||||||
{
|
const FExploreWalkOutput* CameraSeedWalk = nullptr;
|
||||||
const double Start = FPlatformTime::Seconds();
|
|
||||||
if (!RunRender(Arguments, World, Output.Render))
|
|
||||||
{
|
|
||||||
bRequestedModeFailed = true;
|
|
||||||
}
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("[VoxelForgeExplore] render %.3fs (%s)"),
|
|
||||||
FPlatformTime::Seconds() - Start, *Output.Render.Status);
|
|
||||||
}
|
|
||||||
if (Arguments.bWalk)
|
if (Arguments.bWalk)
|
||||||
{
|
{
|
||||||
const double Start = FPlatformTime::Seconds();
|
const double Start = FPlatformTime::Seconds();
|
||||||
@@ -2121,6 +2373,31 @@ int32 UVoxelForgeExploreCommandlet::Main(const FString& Params)
|
|||||||
}
|
}
|
||||||
UE_LOG(LogTemp, Display, TEXT("[VoxelForgeExplore] walk %.3fs (%s)"),
|
UE_LOG(LogTemp, Display, TEXT("[VoxelForgeExplore] walk %.3fs (%s)"),
|
||||||
FPlatformTime::Seconds() - Start, *Output.Walk.Status);
|
FPlatformTime::Seconds() - Start, *Output.Walk.Status);
|
||||||
|
CameraSeedWalk = &Output.Walk;
|
||||||
|
}
|
||||||
|
else if (Arguments.bRender)
|
||||||
|
{
|
||||||
|
// A render-only invocation still has to obey the inside-walkable-space contract. Run the
|
||||||
|
// same focused walk as a private seed pass; it is intentionally omitted from the render-only
|
||||||
|
// JSON so the selected mode remains render.
|
||||||
|
const double Start = FPlatformTime::Seconds();
|
||||||
|
if (!RunWalk(Arguments, World, RenderSeedWalk))
|
||||||
|
{
|
||||||
|
bRequestedModeFailed = true;
|
||||||
|
}
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("[VoxelForgeExplore] render camera seed walk %.3fs (%s)"),
|
||||||
|
FPlatformTime::Seconds() - Start, *RenderSeedWalk.Status);
|
||||||
|
CameraSeedWalk = &RenderSeedWalk;
|
||||||
|
}
|
||||||
|
if (Arguments.bRender)
|
||||||
|
{
|
||||||
|
const double Start = FPlatformTime::Seconds();
|
||||||
|
if (CameraSeedWalk == nullptr || !RunRender(Arguments, World, *CameraSeedWalk, Output.Render))
|
||||||
|
{
|
||||||
|
bRequestedModeFailed = true;
|
||||||
|
}
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("[VoxelForgeExplore] render %.3fs (%s)"),
|
||||||
|
FPlatformTime::Seconds() - Start, *Output.Render.Status);
|
||||||
}
|
}
|
||||||
if (Arguments.bFailureFocusRender && Arguments.bRender && Arguments.bWalk)
|
if (Arguments.bFailureFocusRender && Arguments.bRender && Arguments.bWalk)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,6 +14,12 @@
|
|||||||
"IsBetaVersion": true,
|
"IsBetaVersion": true,
|
||||||
"IsExperimentalVersion": false,
|
"IsExperimentalVersion": false,
|
||||||
"Installed": false,
|
"Installed": false,
|
||||||
|
"Plugins": [
|
||||||
|
{
|
||||||
|
"Name": "RealtimeMeshComponent",
|
||||||
|
"Enabled": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"Modules": [
|
"Modules": [
|
||||||
{
|
{
|
||||||
"Name": "VoxelForge",
|
"Name": "VoxelForge",
|
||||||
|
|||||||
Reference in new issue
Block a user