fix(shafts): the primordial law holds -- 0 trusted negatives across 16 seeds
Closes the VerticalShafts bug. Two remaining failures were investigated
and neither was a broken world.
PART A -- my measurement window was binding, not the world.
The window was a square centred on (0,0) with a radius, while mouths sit
up to ~200 voxels out in arbitrary directions, so a box holding both was
mostly empty space paid for at full price. Seed 6 at step 1 needed
16,515,072 cells and blew the cap; fitted to the two mouths it needs
6,553,344 and measures fine.
seed 6 step 2 NOT_CONNECTED -> CONNECTED at doubled margin
seed 6 step 1 NOT_CONNECTED -> CONNECTED at doubled margin
seed 14 step 2 CONNECTED
seed 14 step 1 CONNECTED
A fitted box can cut off a route that detours outside it, so every
negative is re-run at doubled margin. 5 of 16 were binding negatives and
all 5 flipped. TRUSTED NEGATIVES: 0. There is no evidence of a single
unescapable strate in 16 seeds.
PART B -- the drainage tree has no orphans, and a third cause existed.
0 / 10,372 unreachable shaft paths (0% orphan rate)
16 strict local minima, all caught by the direct-spine fallback
neighbour fallback fired 0 times; emergency fallback 0
91.8% of non-largest components are smaller than one shaft volume --
they are roughness bubbles, not geometry
So a mouth could land in a noise bubble that is open but unreachable --
worse than useless, exactly the trap the law forbids.
VF_SuggestLandingPoint now returns the selected shaft AXIS, which is on
the drainage tree by construction.
⚠️ Codex found a third cause neither of us listed: tree capsules could
cross ledge bands, so a guaranteed connector could be blocked by a ledge.
Connector Z is now drawn from a deterministic ledge-free interval,
mirrored in the op stack.
⚠️ And an 8th error in my spec: "connect to any neighbour shaft" as a
fallback can create CYCLES -- a loop of shafts that drains nowhere and is
disconnected from the spine. The fallback must preserve strictly
decreasing origin-distance. Implemented that way.
per-archetype law 6/6 · box-verdict violations 0 · stack equivalence
bit-identical over 20,000 samples · sweep 45.3 -> 20.4 s
cache 7.970 us rebuild, 0.073 us hot call (225-cell rolls on rebuild only)
Build clean, 21 succeeded / 0 failed, verified by me in VoxelM.log.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
This commit is contained in:
1 parent
9d2324879d
commit
4f11ce81a4
10 files changed
+1813
-174
No files matched your search
+28
-19
@@ -49,27 +49,33 @@ what the world IS, and both paths compute it.
|
|||||||
`UVoxelGenerator::OriginSpineRadius` ← `VoxelSettings::OriginSpineRadius`. Called before
|
`UVoxelGenerator::OriginSpineRadius` ← `VoxelSettings::OriginSpineRadius`. Called before
|
||||||
every `ApplyBoundarySeal`.
|
every `ApplyBoundarySeal`.
|
||||||
- `VerticalShafts` treats that structural column as a **connector endpoint**, not as a second
|
- `VerticalShafts` treats that structural column as a **connector endpoint**, not as a second
|
||||||
density primitive. A thread-local rebuild collects a 7×7 cell window once, emits tree links only
|
density primitive. A thread-local rebuild collects a direct-indexed geometric halo once (with
|
||||||
for the inner 3×3 cells, and resolves each parent from its complete 5×5 candidate window. Each
|
the stock settings: 9×9 tree-emission cells and a 15×15 roll), emits tree links only for the
|
||||||
|
inner 3×3 cells, and resolves each parent from its complete ±2-cell candidate window. Each
|
||||||
shaft chooses the nearest shaft whose jittered distance to (0,0) is strictly smaller; ties use
|
shaft chooses the nearest shaft whose jittered distance to (0,0) is strictly smaller; ties use
|
||||||
the lowest cell index. A local minimum links directly to the spine. The wider collection is
|
the lowest cell index. If that window has no parent, the deterministic ±3 lower-origin neighbour
|
||||||
therefore fully contained for every emitted shaft and the parent is independent of chunk or
|
fallback fires; a window touching the origin uses the spine directly, with a final direct-spine
|
||||||
evaluation-cell boundaries. The existing probabilistic `VoxelHash::Pair` links and
|
fallback for an unusually empty finite halo. The wider collection is therefore fully contained
|
||||||
|
for every emitted shaft and the parent is independent of chunk or evaluation-cell boundaries.
|
||||||
|
Every parent edge strictly decreases origin distance or terminates at the spine, which proves
|
||||||
|
that no shaft is orphaned or cyclic. The existing probabilistic `VoxelHash::Pair` links and
|
||||||
`Spacing*1.6` cutoff remain as texture and loops; they are additive, not the connectivity law.
|
`Spacing*1.6` cutoff remain as texture and loops; they are additive, not the connectivity law.
|
||||||
Tree links use a radius strictly above the proven `sup|FBM|=1.5` roughness envelope
|
Tree links use a radius strictly above the proven `sup|FBM|=1.5` roughness envelope
|
||||||
(`max(ConnectorRadius, SurfaceRoughness*VOXEL_NOISE_SCALE*1.5 + 1)`).
|
(`max(ConnectorRadius, SurfaceRoughness*VOXEL_NOISE_SCALE*1.5 + 1)`) and choose a shared
|
||||||
`BuildVerticalShaftStack` mirrors the same tree, random links, windows, hashes, and radius;
|
ledge-free Z interval. `BuildVerticalShaftStack` mirrors the same tree, random links, windows,
|
||||||
`VF_ApplyOriginSpine` remains the sole owner of the vertical column itself.
|
hashes, radius, and connector-Z rule; `VF_ApplyOriginSpine` remains the sole owner of the
|
||||||
|
vertical column itself.
|
||||||
- Descent is **player-dug** through the thin seals at (0,0). The single auto-opened
|
- Descent is **player-dug** through the thin seals at (0,0). The single auto-opened
|
||||||
connection is the **surface entry shaft** at (0,0) through the top of strate 0
|
connection is the **surface entry shaft** at (0,0) through the top of strate 0
|
||||||
(`GeneratePassages`, `bOpenSurfaceEntry`).
|
(`GeneratePassages`, `bOpenSurfaceEntry`).
|
||||||
- **Hybrid extras:** auto-carved *shortcut* passages per boundary, placed away from (0,0).
|
- **Hybrid extras:** auto-carved *shortcut* passages per boundary, placed away from (0,0).
|
||||||
Now fully **per-strate** — see §8.8 (the upper strate's `PassageConfig` drives count/style/shape).
|
Now fully **per-strate** — see §8.8 (the upper strate's `PassageConfig` drives count/style/shape).
|
||||||
|
|
||||||
The tree is a construction guarantee for the shaft field: every seeded shaft has one inward
|
The tree is a construction guarantee for the shaft field: every seeded shaft has one inward edge,
|
||||||
edge, and every chain strictly decreases distance to the origin until it reaches a local minimum
|
and every chain strictly decreases distance to the origin until it reaches a local minimum that
|
||||||
that links to the spine. It does not alter passage placement. The acceptance measurement remains
|
links to the spine. The fallback resolver preserves that proof, so no shaft is an orphan or part
|
||||||
the actual arrival→departure flood-fill at the independently placed passage endpoints.
|
of a cycle. It does not alter passage placement. The acceptance measurement remains the actual
|
||||||
|
arrival→departure flood-fill at the independently placed passage endpoints.
|
||||||
|
|
||||||
### 8.2a XY edge seal (bounded-world invariant)
|
### 8.2a XY edge seal (bounded-world invariant)
|
||||||
`UVoxelSettings::WorldRadiusVoxels` and `EdgeSealThickness` define the bounded world in actor-space
|
`UVoxelSettings::WorldRadiusVoxels` and `EdgeSealThickness` define the bounded world in actor-space
|
||||||
@@ -342,8 +348,9 @@ landing point through the pure free function `VF_SuggestLandingPoint` (`VoxelCav
|
|||||||
TunnelNetwork and Underwater keep the nearest hash-room vertical placement at the requested XY, with
|
TunnelNetwork and Underwater keep the nearest hash-room vertical placement at the requested XY, with
|
||||||
`MakeStrateSeed(world-seed, strate-index)` matching the room graph's existing identity; FlatPlain and
|
`MakeStrateSeed(world-seed, strate-index)` matching the room graph's existing identity; FlatPlain and
|
||||||
CrystalChamber recompute their slab void band and reject column-overlap points. Maze snaps to the
|
CrystalChamber recompute their slab void band and reject column-overlap points. Maze snaps to the
|
||||||
nearest roughness-safe horizontal lattice corridor within one cell, VerticalShafts to a roughness-safe
|
nearest roughness-safe horizontal lattice corridor within one cell, VerticalShafts to the exact
|
||||||
shaft within one shaft spacing, and FloatingIslands to a validated blob top within one island spacing.
|
axis of a roughness-safe shaft on the drainage tree within one shaft spacing, and FloatingIslands
|
||||||
|
to a validated blob top within one island spacing.
|
||||||
Those three return false when no footing exists inside that explicit lateral budget. GeneratePassages
|
Those three return false when no footing exists inside that explicit lateral budget. GeneratePassages
|
||||||
interpolates the snapped XY through the control-point chain and recomputes its conservative bound;
|
interpolates the snapped XY through the control-point chain and recomputes its conservative bound;
|
||||||
SurfaceWorld remains deliberately unanswerable because production terrain can be selected through
|
SurfaceWorld remains deliberately unanswerable because production terrain can be selected through
|
||||||
@@ -373,11 +380,13 @@ driven by `EditorBrush*` props.
|
|||||||
sampling must not thrash the (expensive) rebuild.
|
sampling must not thrash the (expensive) rebuild.
|
||||||
- **VerticalShafts field cache**: shaft rolls and connector decisions, including the structural
|
- **VerticalShafts field cache**: shaft rolls and connector decisions, including the structural
|
||||||
tree and structural-spine connector endpoint, are rebuilt only when the thread-local centre cell
|
tree and structural-spine connector endpoint, are rebuilt only when the thread-local centre cell
|
||||||
or a geometry-affecting parameter changes. Each rebuild rolls 49 cells (7×7); it stores/emits
|
or a geometry-affecting parameter changes. Each rebuild rolls a wider direct-indexed geometric
|
||||||
only the inner 3×3 shafts and their capsules for the per-voxel loop. The complete 5×5 parent
|
halo (stock settings: 9×9 tree-emission cells / 15×15 total roll); it stores/emits only the
|
||||||
windows are contained by that 7×7 collection, so no parent decision depends on the evaluation
|
inner 3×3 shafts and their capsules for the per-voxel loop, while spatial culling selects cached
|
||||||
cell. No cell or pair hash may move into the per-voxel loop; the operator-stack source follows
|
connectors. The complete ±2 parent and ±3 fallback windows plus connector reach are contained
|
||||||
the same cache contract.
|
by that halo, so no parent decision depends on the evaluation cell. No cell or pair hash, widened
|
||||||
|
neighbourhood scan, or tree construction may move into the per-voxel loop; the operator-stack
|
||||||
|
source follows the same cache contract.
|
||||||
- **Per-chunk param cache** in `GetDensityAt`: GenType + param struct + disturbance cached
|
- **Per-chunk param cache** in `GetDensityAt`: GenType + param struct + disturbance cached
|
||||||
thread-locally by `(DensityCacheOwnerId, ChunkCoord, LayoutVersion)`; the process-unique owner ID
|
thread-locally by `(DensityCacheOwnerId, ChunkCoord, LayoutVersion)`; the process-unique owner ID
|
||||||
prevents cross-world reuse while adding only one `uint64` compare per voxel. Don't remove the owner
|
prevents cross-world reuse while adding only one `uint64` compare per voxel. Don't remove the owner
|
||||||
|
|||||||
+17
-10
@@ -152,7 +152,7 @@ bit. They are port-correctness oracles, not fidelity checks: the acceptance bar
|
|||||||
| `VoxelDensityOps::MakeSdfFill` | 2 | The same op with `Sign = +1` — FloatingIslands' `Density += Fill·Base·2`. ±1 multiplication is exact in IEEE-754, so the carve path is bit-for-bit unchanged by the generalisation. |
|
| `VoxelDensityOps::MakeSdfFill` | 2 | The same op with `Sign = +1` — FloatingIslands' `Density += Fill·Base·2`. ±1 multiplication is exact in IEEE-754, so the carve path is bit-for-bit unchanged by the generalisation. |
|
||||||
| `VoxelDensityOps::MakeSlabVoidSource` | 1 | Floor surface + ceiling surface → void field. **XY-pure** since §3.1, which is what gives it an **exact `ClassifyBox` with no sampling**: FBM's `[-1,1]` contract bounds both surfaces into known Z bands. Serves FlatPlain **and** CrystalChamber. |
|
| `VoxelDensityOps::MakeSlabVoidSource` | 1 | Floor surface + ceiling surface → void field. **XY-pure** since §3.1, which is what gives it an **exact `ClassifyBox` with no sampling**: FBM's `[-1,1]` contract bounds both surfaces into known Z bands. Serves FlatPlain **and** CrystalChamber. |
|
||||||
| `VoxelDensityOps::MakeGridColumnMod` | 3 | Infinite-height cylinders on a world grid, 3×3 cell memo. Adds solid only ⇒ `FillOnly` when a column reaches the box, `Identity` otherwise — and that `Identity` is what lets the source's `AllAir` verdict survive. |
|
| `VoxelDensityOps::MakeGridColumnMod` | 3 | Infinite-height cylinders on a world grid, 3×3 cell memo. Adds solid only ⇒ `FillOnly` when a column reaches the box, `Identity` otherwise — and that `Identity` is what lets the source's `AllAir` verdict survive. |
|
||||||
| `FShaftFieldSource` (internal) | 1 | VerticalShafts' memoised inner 3×3 shaft/capsule source. Each rebuild collects 7×7 cells so every emitted inner shaft has its complete 5×5 candidate window; the nearest strictly more-central shaft is the deterministic parent, and a local minimum links to the runtime `(0,0)` spine. Existing `VoxelHash::Pair`/`Spacing*1.6` links remain additive texture/loops. Tree and spine capsules use a radius strictly above `sup|FBM|=1.5`; the structural post still owns the column and the ledge modifier ignores the synthetic endpoint. `EffectOverBox` mirrors both tree and random capsules conservatively. |
|
| `FShaftFieldSource` (internal) | 1 | VerticalShafts' memoised inner 3×3 shaft/capsule source. Each rebuild uses a direct-indexed wider halo (default 9×9 tree-emission / 15×15 roll) so every emitted shaft has its complete 5×5 parent window plus capsule reach; the nearest strictly more-central shaft in ±2 cells is the deterministic parent, with a deterministic lower-origin ±3 fallback and a direct `(0,0)` spine fallback. Thus every shaft has a provable path to the spine. Existing `VoxelHash::Pair`/`Spacing*1.6` links remain additive texture/loops. Tree and spine capsules use a radius strictly above `sup|FBM|=1.5`, and tree Z is chosen in a ledge-free band; the structural post still owns the column and the ledge modifier keeps its mathematical axis boundary open. `EffectOverBox` mirrors both tree and random capsules conservatively. |
|
||||||
| `VoxelDensityOps::BuildSlabStack` | — | 6 ops, **no branch on archetype**: FlatPlain and CrystalChamber differ only in defaults, exactly as `GetSlabDensity` already had it. 8 archetypes → 7. |
|
| `VoxelDensityOps::BuildSlabStack` | — | 6 ops, **no branch on archetype**: FlatPlain and CrystalChamber differ only in defaults, exactly as `GetSlabDensity` already had it. 8 archetypes → 7. |
|
||||||
| `FSurfaceColumnSource` (internal) | 1 | The bridge between the two spaces: consumes the ground + sky-cap **height** stacks and produces density. `IsXYPure()` **false** — the heights are XY-pure, a distance to them never is. Owns a **six-box spatial LRU** of direct-indexed per-column cells, keyed by `PrepareChunk` on `(StrateBottomWorldZ, LayoutVersion, Seed, ParamsFingerprint)` so it is **shared down the whole vertical strate stack**, exactly like `GSurfColCache`. Six 81×81 boxes preserve hot columns across interleaved regions at roughly 0.79 MiB TLS before padding (more memory, fewer whole-cache recenter/recompute misses). Fractional XY remains direct-compute. |
|
| `FSurfaceColumnSource` (internal) | 1 | The bridge between the two spaces: consumes the ground + sky-cap **height** stacks and produces density. `IsXYPure()` **false** — the heights are XY-pure, a distance to them never is. Owns a **six-box spatial LRU** of direct-indexed per-column cells, keyed by `PrepareChunk` on `(StrateBottomWorldZ, LayoutVersion, Seed, ParamsFingerprint)` so it is **shared down the whole vertical strate stack**, exactly like `GSurfColCache`. Six 81×81 boxes preserve hot columns across interleaved regions at roughly 0.79 MiB TLS before padding (more memory, fewer whole-cache recenter/recompute misses). Fractional XY remains direct-compute. |
|
||||||
| `VoxelDensityOps::BuildSurfaceStack` | — | SurfaceWorld, complete: column + overhang + 4 structural, plus biome blending when `PerBiomeParams` is non-empty. Takes ownership of an `IVoxelBiomeField`. |
|
| `VoxelDensityOps::BuildSurfaceStack` | — | SurfaceWorld, complete: column + overhang + 4 structural, plus biome blending when `PerBiomeParams` is non-empty. Takes ownership of an `IVoxelBiomeField`. |
|
||||||
@@ -307,11 +307,16 @@ This is **where terrain shape lives.**
|
|||||||
|
|
||||||
### 3.6b Strate measurement — `Public/VoxelStrateMeasure.h` + `Private/VoxelStrateMeasure.cpp`
|
### 3.6b Strate measurement — `Public/VoxelStrateMeasure.h` + `Private/VoxelStrateMeasure.cpp`
|
||||||
Headless, read-only Tier 2 measurement pass. `VF_MeasureStrate` samples one strate between a
|
Headless, read-only Tier 2 measurement pass. `VF_MeasureStrate` samples one strate between a
|
||||||
boundary-seal-derived (or explicitly overridden, including explicit zero) interior margin into a bounded coarse grid,
|
boundary-seal-derived (or explicitly overridden, including explicit zero) interior margin into a bounded coarse grid.
|
||||||
performs one deterministic 6-connected air flood fill, and derives fractions, components,
|
When both `CoverPointA` and `CoverPointB` are set, the XY window is their AABB expanded by
|
||||||
|
`CoverMarginVoxels`; otherwise it is the legacy square centered on `CenterXY` with
|
||||||
|
`RadiusInVoxels`.
|
||||||
|
It performs one deterministic 6-connected air flood fill, and derives fractions, components,
|
||||||
walkability, feature scale, and clearance from that grid. Metrics report the resolved margin and
|
walkability, feature scale, and clearance from that grid. Metrics report the resolved margin and
|
||||||
the inclusive/exclusive voxel Z window used, plus the largest component's deterministic lowest-cell
|
the inclusive/exclusive voxel Z window used, the exact sampled dimensions/bounds, and the largest
|
||||||
representative point, cell count, and count of components holding at least 1% of the air.
|
component's deterministic lowest-cell representative point, cell count, and count of components
|
||||||
|
holding at least 1% of the air. The flood fill also retains component cell counts in discovery order,
|
||||||
|
so diagnostics can classify small non-largest components without a second source sample.
|
||||||
`VF_AreConnected` recovers a deterministic BFS parent path from the same kind of grid and checks
|
`VF_AreConnected` recovers a deterministic BFS parent path from the same kind of grid and checks
|
||||||
candidate routes at full voxel resolution before returning `Connected`. When a candidate is
|
candidate routes at full voxel resolution before returning `Connected`. When a candidate is
|
||||||
refuted, its specific coarse cell edge is added to a deterministic array blocklist and BFS is
|
refuted, its specific coarse cell edge is added to a deterministic array blocklist and BFS is
|
||||||
@@ -326,8 +331,8 @@ has a full-resolution air-verified route, and the query reports the alternate ro
|
|||||||
No UObject state, cache, actor, world, or PIE is required.
|
No UObject state, cache, actor, world, or PIE is required.
|
||||||
| Symbol | Role |
|
| Symbol | Role |
|
||||||
|--------|------|
|
|--------|------|
|
||||||
| `FVoxelStrateMeasureSettings` / `FVoxelStrateMetrics` | Plain settings/result structs for bounded strate sampling and derived measurements; callers may override the interior margin (including zero to sample the seal), and results identify the resolved margin/Z window and deterministic component representatives. |
|
| `FVoxelStrateMeasureSettings` / `FVoxelStrateMetrics` | Plain settings/result structs for bounded strate sampling and derived measurements; callers may override the interior margin (including zero to sample the seal) or fit the XY window to two points with a margin, and results identify the resolved window/dimensions/Z range and deterministic component representatives/sizes. |
|
||||||
| `VF_MeasureStrate` | One-grid/one-flood-fill strate metrics; refuses invalid bounds or a grid over `MaxCells`. |
|
| `VF_MeasureStrate` | One-grid/one-flood-fill strate metrics; resolves either the legacy centered square or the two-point fitted AABB, and refuses invalid bounds or a grid over `MaxCells`. |
|
||||||
| `VF_AreConnected` | Coarse 6-connected BFS plus deterministic blocked-edge retries, with a full-resolution air recheck for every candidate route and explicit endpoint-solid, out-of-window, `NotConnectedAtThisResolution`, `CoarseLiedBudgetExhausted` (unknown), and connected outcomes; diagnostics include endpoint component facts and the retry count from that same grid. |
|
| `VF_AreConnected` | Coarse 6-connected BFS plus deterministic blocked-edge retries, with a full-resolution air recheck for every candidate route and explicit endpoint-solid, out-of-window, `NotConnectedAtThisResolution`, `CoarseLiedBudgetExhausted` (unknown), and connected outcomes; diagnostics include endpoint component facts and the retry count from that same grid. |
|
||||||
| `VF_DiagnoseConnectivity` | The same query plus each mouth's air-component size/share and exact geometric distance to the nearest cell of the other component; proximity is measured in coarse-cell coordinates and does not claim a route through solid. |
|
| `VF_DiagnoseConnectivity` | The same query plus each mouth's air-component size/share and exact geometric distance to the nearest cell of the other component; proximity is measured in coarse-cell coordinates and does not claim a route through solid. |
|
||||||
|
|
||||||
@@ -352,7 +357,9 @@ Header is rich with inline docs. Two namespaces + a per-chunk cache system.
|
|||||||
`VF_SuggestLandingPoint` (h:~550, implemented in `VoxelCaveMorphology.cpp`) is the pure landing-site
|
`VF_SuggestLandingPoint` (h:~550, implemented in `VoxelCaveMorphology.cpp`) is the pure landing-site
|
||||||
query: nearest hash-room height at the requested XY for TunnelNetwork/Underwater, slab void midpoint
|
query: nearest hash-room height at the requested XY for TunnelNetwork/Underwater, slab void midpoint
|
||||||
for FlatPlain/CrystalChamber, and bounded lateral searches for a horizontal lattice corridor (one
|
for FlatPlain/CrystalChamber, and bounded lateral searches for a horizontal lattice corridor (one
|
||||||
Maze cell), lower-seal/ledge-safe shaft (one ShaftSpacing), or validated blob top (one IslandSpacing).
|
Maze cell), the exact axis of a lower-seal/ledge-safe VerticalShafts tree shaft (one ShaftSpacing),
|
||||||
|
or a validated blob top (one IslandSpacing). The VerticalShafts result is therefore on the drainage
|
||||||
|
tree, not merely in an open roughness disk.
|
||||||
It returns `false` when those feature-bearing conditions cannot be proven inside the supplied budget;
|
It returns `false` when those feature-bearing conditions cannot be proven inside the supplied budget;
|
||||||
SurfaceWorld remains refused because its production height can depend on manager-owned biome/per-column
|
SurfaceWorld remains refused because its production height can depend on manager-owned biome/per-column
|
||||||
context. It never touches a generator, operator stack, manager, cache, or mutable state.
|
context. It never touches a generator, operator stack, manager, cache, or mutable state.
|
||||||
@@ -488,13 +495,13 @@ The plugin's first tests (`OPSTACK-PLAN.md` Phase 0.5). Run them from the editor
|
|||||||
| `VoxelForgeCrossPlatformTest.cpp` | `VoxelForge.Determinism.CrossPlatformDigest` | SHAPE digest (sign of density = the world) + FIELD digest (bit-for-bit) over a fixed integer grid, plus `NearIso` bounding how many samples could flip sign. Reports rather than asserts until pinned. Run on Windows and Linux and compare. |
|
| `VoxelForgeCrossPlatformTest.cpp` | `VoxelForge.Determinism.CrossPlatformDigest` | SHAPE digest (sign of density = the world) + FIELD digest (bit-for-bit) over a fixed integer grid, plus `NearIso` bounding how many samples could flip sign. Reports rather than asserts until pinned. Run on Windows and Linux and compare. |
|
||||||
| `VoxelForgeOpStackSlabTest.cpp` | `VoxelForge.OpStack.SlabEquivalence` | **Phase 2's first port.** The same 6-op slab stack vs `GetSlabDensity` over 20k points, run twice — FlatPlain **and** CrystalChamber — which is what demonstrates the two archetypes really are one op. Plus window-invariance and box-verdict brute force. Compares against the reference **as it is now** (post Z-term removal), so green = pure refactor and any visual delta is attributable to §3.1 alone. |
|
| `VoxelForgeOpStackSlabTest.cpp` | `VoxelForge.OpStack.SlabEquivalence` | **Phase 2's first port.** The same 6-op slab stack vs `GetSlabDensity` over 20k points, run twice — FlatPlain **and** CrystalChamber — which is what demonstrates the two archetypes really are one op. Plus window-invariance and box-verdict brute force. Compares against the reference **as it is now** (post Z-term removal), so green = pure refactor and any visual delta is attributable to §3.1 alone. |
|
||||||
| `VoxelForgeOpStackTunnelTest.cpp` | `VoxelForge.OpStack.TunnelNetworkSpineEquivalence` | **Stage A of the last port.** Zeroes the 13 detail-op amplitudes so the *original* takes the path stage A ported — that is what makes an incomplete stack verifiable now. Samples in **clusters** (24 chunks × 250 points), because the SDF cache rebuilds when a query leaves its box and uniform sampling would rebuild per point on both paths. Check 3 (two param sets, A/B interleaved) compares each stack **to itself alone, never to the original** — the original would fail it, see AUDIT §C2. Asserts **zero** box verdicts, which is the honest stage-A result. |
|
| `VoxelForgeOpStackTunnelTest.cpp` | `VoxelForge.OpStack.TunnelNetworkSpineEquivalence` | **Stage A of the last port.** Zeroes the 13 detail-op amplitudes so the *original* takes the path stage A ported — that is what makes an incomplete stack verifiable now. Samples in **clusters** (24 chunks × 250 points), because the SDF cache rebuilds when a query leaves its box and uniform sampling would rebuild per point on both paths. Check 3 (two param sets, A/B interleaved) compares each stack **to itself alone, never to the original** — the original would fail it, see AUDIT §C2. Asserts **zero** box verdicts, which is the honest stage-A result. |
|
||||||
| `VoxelForgeOpStackShaftTest.cpp` | `VoxelForge.OpStack.VerticalShaftEquivalence` | The port that tests **reuse**, not fidelity: three of the five ops are Maze's, unchanged. Forces connectors + ledges on, because both are off or negligible at defaults and a resting param is an untested operator. The source and mirror now share a deterministic inner-3×3-emitted drainage tree whose parents come from rebuild-only 7×7/5×5 windows, plus the old random links. **Fixed 2026-07-29:** its `EffectOverBox` used to return `CarveOnly` because a shaft merely *existed* within a `Spacing*1.6` halo — true almost everywhere at `ShaftSpacing 55 / ShaftDensity 0.6`, hence **0 of 60** tiles. It now tests tree and random capsules (same row-major cell order ⇒ same `VoxelHash::Pair`, so symmetry of `Pair()` is not assumed), with **Z exact** and XY conservative. The sampler was also widened from ±48 voxels to ±440. Every proved tile is brute-forced over its full lattice, and the test reports rebuild-vs-hot cache cost. |
|
| `VoxelForgeOpStackShaftTest.cpp` | `VoxelForge.OpStack.VerticalShaftEquivalence` | The port that tests **reuse**, not fidelity: three of the five ops are Maze's, unchanged. Forces connectors + ledges on, because both are off or negligible at defaults and a resting param is an untested operator. The source and mirror now share a deterministic inner-3×3-emitted drainage tree whose parents come from rebuild-only ±2/±3 windows in a wider direct-indexed halo (default 9×9 emit / 15×15 roll), plus the old random links. Tree connector Z is exact and ledge-free; tree and random capsules are spatially culled from the cached rebuild. **Fixed 2026-07-29:** its `EffectOverBox` used to return `CarveOnly` because a shaft merely *existed* within a `Spacing*1.6` halo — true almost everywhere at `ShaftSpacing 55 / ShaftDensity 0.6`, hence **0 of 60** tiles. It now tests tree and random capsules (same row-major cell order ⇒ same `VoxelHash::Pair`, so symmetry of `Pair()` is not assumed), with **Z exact** and XY conservative. The sampler was also widened from ±48 voxels to ±440. Every proved tile is brute-forced over its full lattice, and the test reports rebuild-vs-hot cache cost. |
|
||||||
| `VoxelForgeOpStackIslandTest.cpp` | `VoxelForge.OpStack.FloatingIslandEquivalence` | The port that runs the stack **backwards** — void + fill vs rock + carve, same classes with the opposite sign. Counts interior-solid and open-void samples separately (on this archetype an aggregate "N solid" is dominated by the seal bands and says nothing about the islands). Counts `AllSolid` and `AllAir` verdicts **separately** too: `AllAir` is the one no cave archetype could ever prove, and it is the entire perf argument here. |
|
| `VoxelForgeOpStackIslandTest.cpp` | `VoxelForge.OpStack.FloatingIslandEquivalence` | The port that runs the stack **backwards** — void + fill vs rock + carve, same classes with the opposite sign. Counts interior-solid and open-void samples separately (on this archetype an aggregate "N solid" is dominated by the seal bands and says nothing about the islands). Counts `AllSolid` and `AllAir` verdicts **separately** too: `AllAir` is the one no cave archetype could ever prove, and it is the entire perf argument here. |
|
||||||
| `VoxelForgeOpStackMazeTest.cpp` | `VoxelForge.OpStack.MazeEquivalence` | **Phase 1's load-bearing test.** The 7-op Maze stack vs `GetMazeDensity` over 20k points (aiming for bit-identity; a side-of-iso disagreement is the hard fail), plus purity across workers and brute force on every box verdict the stack emits. Reports how many tiles the stack can prove uniform — today's `ClassifyTile` proves **zero** for any cave archetype. |
|
| `VoxelForgeOpStackMazeTest.cpp` | `VoxelForge.OpStack.MazeEquivalence` | **Phase 1's load-bearing test.** The 7-op Maze stack vs `GetMazeDensity` over 20k points (aiming for bit-identity; a side-of-iso disagreement is the hard fail), plus purity across workers and brute force on every box verdict the stack emits. Reports how many tiles the stack can prove uniform — today's `ClassifyTile` proves **zero** for any cave archetype. |
|
||||||
| `VoxelForgeStrateParamCoverageTest.cpp` | `VoxelForge.Determinism.StrateParamBlendCoverage` | **The X-macro guard** (added 2026-08-17). `FStrateGenerationParams::Lerp` blends the hand-written `VF_STRATE_PARAM_FIELDS` list, **not** the struct — so a field added to one and not the other compiles, tests green, and silently takes its **default** inside every Gradient/Interleaved transition band. This expands the X-macro a **third** way (after LERP and SNAP), into a name list, and diffs it against the struct's UObject reflection. Pure shape test: no fixture, no world, instant. `GExemptFieldNames` is **empty** — every reflected field is covered today, and any exemption must be written down as a decision. Stakes rise with the world composer, which intends to invent parameter sets through this same `Lerp` (`COMPOSER-NOTES.md`). |
|
| `VoxelForgeStrateParamCoverageTest.cpp` | `VoxelForge.Determinism.StrateParamBlendCoverage` | **The X-macro guard** (added 2026-08-17). `FStrateGenerationParams::Lerp` blends the hand-written `VF_STRATE_PARAM_FIELDS` list, **not** the struct — so a field added to one and not the other compiles, tests green, and silently takes its **default** inside every Gradient/Interleaved transition band. This expands the X-macro a **third** way (after LERP and SNAP), into a name list, and diffs it against the struct's UObject reflection. Pure shape test: no fixture, no world, instant. `GExemptFieldNames` is **empty** — every reflected field is covered today, and any exemption must be written down as a decision. Stakes rise with the world composer, which intends to invent parameter sets through this same `Lerp` (`COMPOSER-NOTES.md`). |
|
||||||
| `VoxelForgeLayoutOrderIndependenceTest.cpp` | `VoxelForge.Determinism.LayoutOrderIndependence` | Builds a known transient soft-pointer pool, then rebuilds it in original, reversed, and swapped orders. Requires a non-empty layout and passage set, and compares every slot's definition/Z/height plus passage endpoints, radius, type, control geometry, and bounds bit-for-bit. |
|
| `VoxelForgeLayoutOrderIndependenceTest.cpp` | `VoxelForge.Determinism.LayoutOrderIndependence` | Builds a known transient soft-pointer pool, then rebuilds it in original, reversed, and swapped orders. Requires a non-empty layout and passage set, and compares every slot's definition/Z/height plus passage endpoints, radius, type, control geometry, and bounds bit-for-bit. |
|
||||||
| `VoxelForgePassageOpenSpaceTest.cpp` | `VoxelForge.Determinism.PassageLandsInOpenSpace` | Uses the real fixture density path to check every generated inter-strate passage whose destination query answers: a 16-point ring outside the mouth's carve/blend band has at least half its samples in destination air, and the endpoint matches the pure open-point result within the mouth's float envelope. This is a connectivity proxy, not a flood-fill proof. Reports checked passages and false/unanswerable archetypes; fails if it inspects zero passages. |
|
| `VoxelForgePassageOpenSpaceTest.cpp` | `VoxelForge.Determinism.PassageLandsInOpenSpace` | Uses the real fixture density path to check every generated inter-strate passage whose destination query answers: a 16-point ring outside the mouth's carve/blend band has at least half its samples in destination air, and the endpoint matches the pure open-point result within the mouth's float envelope. This is a connectivity proxy, not a flood-fill proof. Reports checked passages and false/unanswerable archetypes; fails if it inspects zero passages. |
|
||||||
| `VoxelForgeStrateConnectivityTest.cpp` | `VoxelForge.Generation.StrateConnectivity` / `VoxelForge.Generation.StrateConnectivityRefinement` / `VoxelForge.Generation.VerticalShaftSeamFreedom` | Bounded strate metrics with density-polarity and solid-gap controls, deterministic route rechecks, and refinement sweeps. The refinement test explicitly checks margins 0/2/4/8 at step 2/radius 192, reports components/largest-component share, the `(0,0)` spine's full-window/interior vertical run and component, plus arrival/departure→spine and arrival→departure enums across 16 VerticalShafts seeds. The seam test re-evaluates cell-boundary positions after warming distinct neighbouring chunk contexts in both legacy and operator-stack paths. |
|
| `VoxelForgeStrateConnectivityTest.cpp` | `VoxelForge.Generation.StrateConnectivity` / `VoxelForge.Generation.StrateConnectivityRefinement` / `VoxelForge.Generation.VerticalShaftSeamFreedom` | Bounded strate metrics with density-polarity and solid-gap controls, deterministic route rechecks, and refinement sweeps. The refinement test fits the measurement AABB to each arrival/departure mouth pair, reports exact before/after seed-6 cell counts, reruns every negative with doubled margin, measures tree orphan candidates/path reachability and roughness-bubble proxies, asserts axis landings, checks source/mirror physical paths, and requires 16/16 effective arrival→departure results across the 16 VerticalShafts seeds. Margin-binding negatives are reported as measurement limits, never as gap findings. The seam test re-evaluates cell-boundary positions after warming distinct neighbouring chunk contexts in both legacy and operator-stack paths. |
|
||||||
| `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. |
|
||||||
|
|
||||||
## 4. The density pipeline (most-edited hot path)
|
## 4. The density pipeline (most-edited hot path)
|
||||||
|
|||||||
@@ -174,8 +174,8 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
|
|||||||
//=========================================================================
|
//=========================================================================
|
||||||
// 2. INVARIANCE DE FENÊTRE
|
// 2. INVARIANCE DE FENÊTRE
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// La source garde un cache inner 3×3 `thread_local`; chaque rebuild collecte 7×7 pour les
|
// La source garde un cache inner 3×3 `thread_local`; chaque rebuild collecte un halo 9×9/15×15
|
||||||
// fenêtres d'arbre 5×5. La clé est le jeu de params : c'est exactement le genre d'endroit où
|
// pour les fenêtres d'arbre 5×5 et le fallback ±3. La clé est le jeu de params : c'est exactement le genre d'endroit où
|
||||||
// une clé incomplète produit une couture (AUDIT §C2).
|
// une clé incomplète produit une couture (AUDIT §C2).
|
||||||
{
|
{
|
||||||
std::atomic<int32> Impure{ 0 };
|
std::atomic<int32> Impure{ 0 };
|
||||||
@@ -208,9 +208,9 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
|
|||||||
//==========================================================================
|
//==========================================================================
|
||||||
{
|
{
|
||||||
// Force one distinct centre cell per call, then hold one cell constant. This measures the
|
// Force one distinct centre cell per call, then hold one cell constant. This measures the
|
||||||
// widened collection where it belongs (cache rebuild), separately from the hot per-voxel
|
// widened collection where it belongs (cache rebuild), separately from the hot per-voxel
|
||||||
// path. The implementation deliberately performs 49 cell rolls for the former and zero
|
// path. The implementation deliberately performs 225 cell rolls for the former and zero
|
||||||
// cell rolls for the latter; only the cached inner 3×3/capsule SDFs remain hot.
|
// cell rolls for the latter; only the cached inner 3×3/capsule SDFs remain hot.
|
||||||
constexpr int32 NumRebuildSamples = 256;
|
constexpr int32 NumRebuildSamples = 256;
|
||||||
constexpr int32 NumHotSamples = 20000;
|
constexpr int32 NumHotSamples = 20000;
|
||||||
const float PerfSpacing = FMath::Max(P.ShaftSpacing, 1.0f);
|
const float PerfSpacing = FMath::Max(P.ShaftSpacing, 1.0f);
|
||||||
@@ -239,7 +239,7 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
|
|||||||
|
|
||||||
AddInfo(FString::Printf(
|
AddInfo(FString::Printf(
|
||||||
TEXT("VerticalShafts cache perf: %d forced rebuilds at %.3f us/call and %d hot calls "
|
TEXT("VerticalShafts cache perf: %d forced rebuilds at %.3f us/call and %d hot calls "
|
||||||
"at %.3f us/call (7x7=49 cell rolls only on rebuild; inner 3x3/capsule SDF "
|
"at %.3f us/call (15x15=225 cell rolls only on rebuild; inner 3x3/capsule SDF "
|
||||||
"only on hot calls; sink=%.9g)."),
|
"only on hot calls; sink=%.9g)."),
|
||||||
NumRebuildSamples,
|
NumRebuildSamples,
|
||||||
RebuildSeconds * 1.0e6 / static_cast<double>(NumRebuildSamples),
|
RebuildSeconds * 1.0e6 / static_cast<double>(NumRebuildSamples),
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -672,8 +672,8 @@ namespace
|
|||||||
(MaxLateralSnap + FMath::Max(Params.ShaftMaxRadius, 1.0f)) / Spacing) + 2;
|
(MaxLateralSnap + FMath::Max(Params.ShaftMaxRadius, 1.0f)) / Spacing) + 2;
|
||||||
|
|
||||||
float BestDistSq = FLT_MAX;
|
float BestDistSq = FLT_MAX;
|
||||||
float BestX = 0.0f;
|
float BestAxisX = 0.0f;
|
||||||
float BestY = 0.0f;
|
float BestAxisY = 0.0f;
|
||||||
bool bFound = false;
|
bool bFound = false;
|
||||||
int32 BestCellX = INT32_MAX;
|
int32 BestCellX = INT32_MAX;
|
||||||
int32 BestCellY = INT32_MAX;
|
int32 BestCellY = INT32_MAX;
|
||||||
@@ -711,17 +711,12 @@ namespace
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Distance <= SafeRadius || Distance <= KINDA_SMALL_NUMBER)
|
// A VerticalShafts landing must identify the topology, not merely an open point
|
||||||
{
|
// inside its radius. The safe-radius distance still chooses the nearest confident
|
||||||
BestX = WorldX;
|
// site under the existing snap budget, but the returned XY is the selected shaft's
|
||||||
BestY = WorldY;
|
// exact axis so it is on the deterministic drainage tree by construction.
|
||||||
}
|
BestAxisX = ShaftX;
|
||||||
else
|
BestAxisY = ShaftY;
|
||||||
{
|
|
||||||
const float Scale = SafeRadius / Distance;
|
|
||||||
BestX = ShaftX + DXWorld * Scale;
|
|
||||||
BestY = ShaftY + DYWorld * Scale;
|
|
||||||
}
|
|
||||||
BestDistSq = DistSq;
|
BestDistSq = DistSq;
|
||||||
BestCellX = CellX;
|
BestCellX = CellX;
|
||||||
BestCellY = CellY;
|
BestCellY = CellY;
|
||||||
@@ -729,13 +724,13 @@ namespace
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!bFound || BestDistSq > MaxSnapSq || !FMath::IsFinite(BestX)
|
if (!bFound || BestDistSq > MaxSnapSq || !FMath::IsFinite(BestAxisX)
|
||||||
|| !FMath::IsFinite(BestY))
|
|| !FMath::IsFinite(BestAxisY))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
OutPoint = FVector(BestX, BestY, LandingZ);
|
OutPoint = FVector(BestAxisX, BestAxisY, LandingZ);
|
||||||
return !OutPoint.ContainsNaN() && FMath::IsFinite(OutPoint.X)
|
return !OutPoint.ContainsNaN() && FMath::IsFinite(OutPoint.X)
|
||||||
&& FMath::IsFinite(OutPoint.Y) && FMath::IsFinite(OutPoint.Z);
|
&& FMath::IsFinite(OutPoint.Y) && FMath::IsFinite(OutPoint.Z);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,6 +169,42 @@ namespace
|
|||||||
return (Sdf < DetailThreshold) && (Sdf < FLT_MAX);
|
return (Sdf < DetailThreshold) && (Sdf < FLT_MAX);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
@@ -1520,7 +1556,8 @@ namespace
|
|||||||
// séparée aurait dû faire aussi. À revoir si le profil montre que ça compte.
|
// séparée aurait dû faire aussi. À revoir si le profil montre que ça compte.
|
||||||
//
|
//
|
||||||
// Kept as ONE op against §6's suggestion: the per-voxel connectors derive from the same inner
|
// Kept as ONE op against §6's suggestion: the per-voxel connectors derive from the same inner
|
||||||
// 3×3 roll as the shafts, while a rebuild-only 7×7 collection resolves each 5×5 tree window;
|
// 3×3 roll as the shafts, while a rebuild-only 9×9 collection resolves each 5×5 tree window
|
||||||
|
// and fixed ±3 fallback;
|
||||||
// the downstream ledge mod needs the inner shaft list anyway. What is forfeited is an exact XY
|
// the downstream ledge mod needs the inner shaft list anyway. What is forfeited is an exact XY
|
||||||
// box verdict on the cylinder half alone.
|
// box verdict on the cylinder half alone.
|
||||||
class FShaftFieldSource final : public IVoxelDensityOp
|
class FShaftFieldSource final : public IVoxelDensityOp
|
||||||
@@ -1546,7 +1583,7 @@ namespace
|
|||||||
struct FCells
|
struct FCells
|
||||||
{
|
{
|
||||||
TArray<FShaft, TInlineAllocator<10>> Shafts;
|
TArray<FShaft, TInlineAllocator<10>> Shafts;
|
||||||
TArray<FConn, TInlineAllocator<24>> Conns;
|
TArray<FConn, TInlineAllocator<32>> Conns;
|
||||||
};
|
};
|
||||||
|
|
||||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||||
@@ -1612,21 +1649,37 @@ namespace
|
|||||||
// it may over-enumerate, but it must never miss a cached tree capsule.
|
// it may over-enumerate, but it must never miss a cached tree capsule.
|
||||||
//-----------------------------------------------------------------
|
//-----------------------------------------------------------------
|
||||||
constexpr int32 CandidateRadius = 2;
|
constexpr int32 CandidateRadius = 2;
|
||||||
|
constexpr int32 FallbackRadius = 3;
|
||||||
constexpr uint32 TreeSalt = 0x7A11u;
|
constexpr uint32 TreeSalt = 0x7A11u;
|
||||||
const int32 EmitX0 = FMath::FloorToInt((float)VoxelBox.Min.X / Spacing) - 1;
|
const float TreeRadius = SpineConnectorRadius();
|
||||||
const int32 EmitX1 = FMath::FloorToInt((float)VoxelBox.Max.X / Spacing) + 1;
|
const int32 ConnectorCellPad = FMath::Max(
|
||||||
const int32 EmitY0 = FMath::FloorToInt((float)VoxelBox.Min.Y / Spacing) - 1;
|
1, FMath::CeilToInt(TreeRadius / Spacing));
|
||||||
const int32 EmitY1 = FMath::FloorToInt((float)VoxelBox.Max.Y / Spacing) + 1;
|
const int32 TreeEmitRadius = FallbackRadius + ConnectorCellPad;
|
||||||
|
const int32 QueryX0 = FMath::FloorToInt((float)VoxelBox.Min.X / Spacing) - 1;
|
||||||
|
const int32 QueryX1 = FMath::FloorToInt((float)VoxelBox.Max.X / Spacing) + 1;
|
||||||
|
const int32 QueryY0 = FMath::FloorToInt((float)VoxelBox.Min.Y / Spacing) - 1;
|
||||||
|
const int32 QueryY1 = FMath::FloorToInt((float)VoxelBox.Max.Y / Spacing) + 1;
|
||||||
|
const int32 TreeEmitX0 = FMath::FloorToInt((float)VoxelBox.Min.X / Spacing)
|
||||||
|
- TreeEmitRadius;
|
||||||
|
const int32 TreeEmitX1 = FMath::FloorToInt((float)VoxelBox.Max.X / Spacing)
|
||||||
|
+ TreeEmitRadius;
|
||||||
|
const int32 TreeEmitY0 = FMath::FloorToInt((float)VoxelBox.Min.Y / Spacing)
|
||||||
|
- TreeEmitRadius;
|
||||||
|
const int32 TreeEmitY1 = FMath::FloorToInt((float)VoxelBox.Max.Y / Spacing)
|
||||||
|
+ TreeEmitRadius;
|
||||||
|
|
||||||
TArray<FShaft, TInlineAllocator<64>> TreeCandidates;
|
TArray<FShaft, TInlineAllocator<225>> TreeCandidates;
|
||||||
TArray<FShaft, TInlineAllocator<32>> TreeEmit;
|
TArray<FShaft, TInlineAllocator<81>> TreeEmit;
|
||||||
for (int32 cy = EmitY0 - CandidateRadius; cy <= EmitY1 + CandidateRadius; ++cy)
|
for (int32 cy = TreeEmitY0 - FallbackRadius;
|
||||||
for (int32 cx = EmitX0 - CandidateRadius; cx <= EmitX1 + CandidateRadius; ++cx)
|
cy <= TreeEmitY1 + FallbackRadius; ++cy)
|
||||||
|
for (int32 cx = TreeEmitX0 - FallbackRadius;
|
||||||
|
cx <= TreeEmitX1 + FallbackRadius; ++cx)
|
||||||
{
|
{
|
||||||
FShaft Sh;
|
FShaft Sh;
|
||||||
if (!RollShaft(cx, cy, Sh)) { continue; }
|
if (!RollShaft(cx, cy, Sh)) { continue; }
|
||||||
TreeCandidates.Add(Sh);
|
TreeCandidates.Add(Sh);
|
||||||
if (cx >= EmitX0 && cx <= EmitX1 && cy >= EmitY0 && cy <= EmitY1)
|
if (cx >= TreeEmitX0 && cx <= TreeEmitX1
|
||||||
|
&& cy >= TreeEmitY0 && cy <= TreeEmitY1)
|
||||||
{
|
{
|
||||||
TreeEmit.Add(Sh);
|
TreeEmit.Add(Sh);
|
||||||
}
|
}
|
||||||
@@ -1634,7 +1687,6 @@ namespace
|
|||||||
|
|
||||||
const float BottomZ = P.StrateBottomWorldZ + P.BoundarySealThickness;
|
const float BottomZ = P.StrateBottomWorldZ + P.BoundarySealThickness;
|
||||||
const float TopZ = P.StrateTopWorldZ - P.BoundarySealThickness;
|
const float TopZ = P.StrateTopWorldZ - P.BoundarySealThickness;
|
||||||
const float TreeRadius = SpineConnectorRadius();
|
|
||||||
const float RMinZ = (float)VoxelBox.Min.Z, RMaxZ = (float)VoxelBox.Max.Z;
|
const float RMinZ = (float)VoxelBox.Min.Z, RMaxZ = (float)VoxelBox.Max.Z;
|
||||||
const FVector2D CtrXY(0.5f * (float)(VoxelBox.Min.X + VoxelBox.Max.X),
|
const FVector2D CtrXY(0.5f * (float)(VoxelBox.Min.X + VoxelBox.Max.X),
|
||||||
0.5f * (float)(VoxelBox.Min.Y + VoxelBox.Max.Y));
|
0.5f * (float)(VoxelBox.Min.Y + VoxelBox.Max.Y));
|
||||||
@@ -1689,14 +1741,58 @@ namespace
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Parent == nullptr
|
||||||
|
&& !(FMath::Abs(Child.CellX) <= CandidateRadius
|
||||||
|
&& FMath::Abs(Child.CellY) <= CandidateRadius))
|
||||||
|
{
|
||||||
|
// Keep EffectOverBox's parent resolver identical to GetCells. A local
|
||||||
|
// minimum chooses the nearest lower-origin shaft in the collected halo; the
|
||||||
|
// monotone origin-distance key proves that the resulting tree reaches the
|
||||||
|
// spine and cannot cycle. If the finite halo is empty, the direct spine is the
|
||||||
|
// conservative final fallback.
|
||||||
|
for (const FShaft& Candidate : TreeCandidates)
|
||||||
|
{
|
||||||
|
if (Candidate.CellX == Child.CellX && Candidate.CellY == Child.CellY)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (FMath::Abs(Candidate.CellX - Child.CellX) > FallbackRadius
|
||||||
|
|| FMath::Abs(Candidate.CellY - Child.CellY) > FallbackRadius)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float CandidateOriginSq = FMath::Square(Candidate.X)
|
||||||
|
+ FMath::Square(Candidate.Y);
|
||||||
|
if (!(CandidateOriginSq < ChildOriginSq))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float DistanceSq = FMath::Square(Child.X - Candidate.X)
|
||||||
|
+ FMath::Square(Child.Y - Candidate.Y);
|
||||||
|
const bool bLowerCell = Parent == nullptr
|
||||||
|
|| Candidate.CellY < Parent->CellY
|
||||||
|
|| (Candidate.CellY == Parent->CellY
|
||||||
|
&& Candidate.CellX < Parent->CellX);
|
||||||
|
if (DistanceSq < BestDistanceSq
|
||||||
|
|| (DistanceSq == BestDistanceSq && bLowerCell))
|
||||||
|
{
|
||||||
|
BestDistanceSq = DistanceSq;
|
||||||
|
Parent = &Candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const int32 ParentCellX = Parent != nullptr ? Parent->CellX : 0;
|
const int32 ParentCellX = Parent != nullptr ? Parent->CellX : 0;
|
||||||
const int32 ParentCellY = Parent != nullptr ? Parent->CellY : 0;
|
const int32 ParentCellY = Parent != nullptr ? Parent->CellY : 0;
|
||||||
const uint32 LinkHash = VoxelHash::Pair(
|
const uint32 LinkHash = VoxelHash::Pair(
|
||||||
Child.CellX, Child.CellY, ParentCellX, ParentCellY, Salt ^ TreeSalt);
|
Child.CellX, Child.CellY, ParentCellX, ParentCellY, Salt ^ TreeSalt);
|
||||||
const float Zc = FMath::Lerp(
|
const float Zc = VF_SelectVerticalTreeConnectorZ(
|
||||||
|
P,
|
||||||
P.StrateBottomWorldZ + P.BoundarySealThickness,
|
P.StrateBottomWorldZ + P.BoundarySealThickness,
|
||||||
P.StrateTopWorldZ - P.BoundarySealThickness,
|
P.StrateTopWorldZ - P.BoundarySealThickness,
|
||||||
VoxelHash::ToFloat01(VoxelHash::Mix(LinkHash)));
|
LinkHash);
|
||||||
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);
|
||||||
@@ -1721,7 +1817,7 @@ namespace
|
|||||||
// du voisinage inner 3×3 de la cellule DE SA REQUÊTE. Une paire random visible depuis
|
// du voisinage inner 3×3 de la cellule DE SA REQUÊTE. Une paire random visible depuis
|
||||||
// un point de la boîte a donc ses deux puits dans [cellules de la boîte] ± 1, qui est
|
// un point de la boîte a donc ses deux puits dans [cellules de la boîte] ± 1, qui est
|
||||||
// exactement la plage balayée ici. Les connecteurs de l'arbre sont testés séparément
|
// exactement la plage balayée ici. Les connecteurs de l'arbre sont testés séparément
|
||||||
// au-dessus avec leur collecte 7×7/émission inner 3×3. On peut produire des paires que
|
// au-dessus avec leur collecte élargie/émission du halo géométrique. On peut produire des paires que
|
||||||
// personne ne voit jamais : c'est du `CarveOnly` en trop, pas un trou.
|
// personne ne voit jamais : c'est du `CarveOnly` en trop, pas un trou.
|
||||||
//
|
//
|
||||||
// ⚠️ ET POURQUOI L'ORDRE (A,B) EST LE MÊME QUE CELUI DE `GetCells`. Le hash de paire est
|
// ⚠️ ET POURQUOI L'ORDRE (A,B) EST LE MÊME QUE CELUI DE `GetCells`. Le hash de paire est
|
||||||
@@ -1738,10 +1834,10 @@ namespace
|
|||||||
// insertion order, so each pair gets the same hash without assuming Pair() is symmetric.
|
// insertion order, so each pair gets the same hash without assuming Pair() is symmetric.
|
||||||
if (P.CrossConnectChance > 0.0f)
|
if (P.CrossConnectChance > 0.0f)
|
||||||
{
|
{
|
||||||
const int32 QX0 = EmitX0;
|
const int32 QX0 = QueryX0;
|
||||||
const int32 QX1 = EmitX1;
|
const int32 QX1 = QueryX1;
|
||||||
const int32 QY0 = EmitY0;
|
const int32 QY0 = QueryY0;
|
||||||
const int32 QY1 = EmitY1;
|
const int32 QY1 = QueryY1;
|
||||||
|
|
||||||
TArray<FShaft, TInlineAllocator<32>> Near;
|
TArray<FShaft, TInlineAllocator<32>> Near;
|
||||||
for (int32 cy = QY0; cy <= QY1; ++cy)
|
for (int32 cy = QY0; cy <= QY1; ++cy)
|
||||||
@@ -1825,7 +1921,7 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Le voisinage inner 3×3 + ses connecteurs, mémoïsés par worker. Le rebuild collecte un
|
/** Le voisinage inner 3×3 + ses connecteurs, mémoïsés par worker. Le rebuild collecte un
|
||||||
* 7×7 temporaire pour résoudre les fenêtres parent 5×5. Clé = cellule + tous les params
|
* halo 9×9 émetteur / 15×15 total pour résoudre les fenêtres parent 5×5 et le fallback fixe ±3. Clé = cellule + tous les params
|
||||||
* qui influent (comme l'original) : stable à travers les reconstructions de pile, ce qui
|
* qui influent (comme l'original) : stable à travers les reconstructions de pile, ce qui
|
||||||
* est la leçon retenue du mémo de colonne de SurfaceWorld. */
|
* est la leçon retenue du mémo de colonne de SurfaceWorld. */
|
||||||
const FCells& GetCells(float WorldX, float WorldY) const
|
const FCells& GetCells(float WorldX, float WorldY) const
|
||||||
@@ -1858,35 +1954,80 @@ namespace
|
|||||||
Cache.Shafts.Reset();
|
Cache.Shafts.Reset();
|
||||||
Cache.Conns.Reset();
|
Cache.Conns.Reset();
|
||||||
|
|
||||||
constexpr int32 CollectRadius = 3; // 7×7: inner 3×3 + one 5×5 candidate halo
|
constexpr int32 EmitRadius = 1; // 3×3: shaft cylinders and random links
|
||||||
constexpr int32 EmitRadius = 1; // 3×3: the only cells allowed to emit tree links
|
|
||||||
constexpr int32 CandidateRadius = 2; // 5×5 parent window around an emitting shaft
|
constexpr int32 CandidateRadius = 2; // 5×5 parent window around an emitting shaft
|
||||||
|
constexpr int32 FallbackRadius = 3; // fixed fallback window around an emitting shaft
|
||||||
constexpr uint32 TreeSalt = 0x7A11u;
|
constexpr uint32 TreeSalt = 0x7A11u;
|
||||||
|
|
||||||
TArray<FShaft, TInlineAllocator<49>> CollectedShafts;
|
const float BottomZ = P.StrateBottomWorldZ + P.BoundarySealThickness;
|
||||||
|
const float TopZ = P.StrateTopWorldZ - P.BoundarySealThickness;
|
||||||
|
const float TreeRadius = SpineConnectorRadius();
|
||||||
|
// A tree capsule can affect a cell even when neither endpoint is in that cell.
|
||||||
|
// Keep a rebuild-only child halo for that geometric reach, then add the
|
||||||
|
// parent-search halo. With the defaults this is 4 + 3 cells = 15×15 rolls.
|
||||||
|
const int32 ConnectorCellPad = FMath::Max(
|
||||||
|
1, FMath::CeilToInt(TreeRadius / Spacing));
|
||||||
|
const int32 TreeEmitRadius = FallbackRadius + ConnectorCellPad;
|
||||||
|
const int32 CollectRadius = TreeEmitRadius + FallbackRadius;
|
||||||
|
|
||||||
|
const int32 CollectSide = CollectRadius * 2 + 1;
|
||||||
|
TArray<FShaft, TInlineAllocator<225>> ShaftGrid;
|
||||||
|
TArray<uint8, TInlineAllocator<225>> ShaftPresent;
|
||||||
|
TArray<FShaft, TInlineAllocator<81>> TreeEmitShafts;
|
||||||
|
ShaftGrid.SetNum(CollectSide * CollectSide);
|
||||||
|
ShaftPresent.Init(0, CollectSide * CollectSide);
|
||||||
// Rebuild-only wide collection. `Cache.Shafts` remains the inner 3×3 working set
|
// Rebuild-only wide collection. `Cache.Shafts` remains the inner 3×3 working set
|
||||||
// used by Eval and the ledge modifier, so the 7×7 cost is not paid per voxel.
|
// used by Eval and the ledge modifier; the wider tree work is not paid per voxel.
|
||||||
for (int32 dy = -CollectRadius; dy <= CollectRadius; dy++)
|
for (int32 dy = -CollectRadius; dy <= CollectRadius; dy++)
|
||||||
for (int32 dx = -CollectRadius; dx <= CollectRadius; dx++)
|
for (int32 dx = -CollectRadius; dx <= CollectRadius; dx++)
|
||||||
{
|
{
|
||||||
FShaft Sh;
|
FShaft Sh;
|
||||||
if (!RollShaft(CX + dx, CY + dy, Sh)) { continue; }
|
if (!RollShaft(CX + dx, CY + dy, Sh)) { continue; }
|
||||||
CollectedShafts.Add(Sh);
|
const int32 GridIndex = (dy + CollectRadius) * CollectSide + (dx + CollectRadius);
|
||||||
|
ShaftGrid[GridIndex] = Sh;
|
||||||
|
ShaftPresent[GridIndex] = 1;
|
||||||
if (FMath::Abs(dx) <= EmitRadius && FMath::Abs(dy) <= EmitRadius)
|
if (FMath::Abs(dx) <= EmitRadius && FMath::Abs(dy) <= EmitRadius)
|
||||||
{
|
{
|
||||||
Cache.Shafts.Add(Sh);
|
Cache.Shafts.Add(Sh);
|
||||||
}
|
}
|
||||||
|
if (FMath::Abs(dx) <= TreeEmitRadius && FMath::Abs(dy) <= TreeEmitRadius)
|
||||||
|
{
|
||||||
|
TreeEmitShafts.Add(Sh);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto FindCollectedShaft = [&](int32 CellX, int32 CellY) -> const FShaft*
|
||||||
|
{
|
||||||
|
const int32 dx = CellX - CX;
|
||||||
|
const int32 dy = CellY - CY;
|
||||||
|
if (FMath::Abs(dx) > CollectRadius || FMath::Abs(dy) > CollectRadius)
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
const int32 GridIndex = (dy + CollectRadius) * CollectSide + (dx + CollectRadius);
|
||||||
|
return ShaftPresent[GridIndex] ? &ShaftGrid[GridIndex] : nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
const bool bOriginNearby = FMath::Abs(CX) <= 1 && FMath::Abs(CY) <= 1;
|
const bool bOriginNearby = FMath::Abs(CX) <= 1 && FMath::Abs(CY) <= 1;
|
||||||
if (bOriginNearby && SpineRadius > 0.0f)
|
if (bOriginNearby && SpineRadius > 0.0f)
|
||||||
{
|
{
|
||||||
Cache.Shafts.Add({0.0f, 0.0f, SpineRadius, 0, 0, true});
|
Cache.Shafts.Add({0.0f, 0.0f, SpineRadius, 0, 0, true});
|
||||||
}
|
}
|
||||||
|
|
||||||
const float BottomZ = P.StrateBottomWorldZ + P.BoundarySealThickness;
|
const float CellMinX = static_cast<float>(CX) * Spacing;
|
||||||
const float TopZ = P.StrateTopWorldZ - P.BoundarySealThickness;
|
const float CellMaxX = static_cast<float>(CX + 1) * Spacing;
|
||||||
const float TreeRadius = SpineConnectorRadius();
|
const float CellMinY = static_cast<float>(CY) * Spacing;
|
||||||
|
const float CellMaxY = static_cast<float>(CY + 1) * Spacing;
|
||||||
|
auto ConnectorMayReachCell = [&](const FConn& Conn)
|
||||||
|
{
|
||||||
|
const float MinX = FMath::Min(Conn.A.X, Conn.B.X) - Conn.Radius;
|
||||||
|
const float MaxX = FMath::Max(Conn.A.X, Conn.B.X) + Conn.Radius;
|
||||||
|
const float MinY = FMath::Min(Conn.A.Y, Conn.B.Y) - Conn.Radius;
|
||||||
|
const float MaxY = FMath::Max(Conn.A.Y, Conn.B.Y) + Conn.Radius;
|
||||||
|
const float GapX = FMath::Max3(CellMinX - MaxX, MinX - CellMaxX, 0.0f);
|
||||||
|
const float GapY = FMath::Max3(CellMinY - MaxY, MinY - CellMaxY, 0.0f);
|
||||||
|
return GapX == 0.0f && GapY == 0.0f;
|
||||||
|
};
|
||||||
|
|
||||||
auto EmitTreeConnector = [&](const FShaft& Child, const FShaft* Parent)
|
auto EmitTreeConnector = [&](const FShaft& Child, const FShaft* Parent)
|
||||||
{
|
{
|
||||||
@@ -1894,55 +2035,103 @@ namespace
|
|||||||
const int32 ParentCellY = Parent != nullptr ? Parent->CellY : 0;
|
const int32 ParentCellY = Parent != nullptr ? Parent->CellY : 0;
|
||||||
const uint32 LinkHash = VoxelHash::Pair(
|
const uint32 LinkHash = VoxelHash::Pair(
|
||||||
Child.CellX, Child.CellY, ParentCellX, ParentCellY, Salt ^ TreeSalt);
|
Child.CellX, Child.CellY, ParentCellX, ParentCellY, Salt ^ TreeSalt);
|
||||||
const float Zc = FMath::Lerp(
|
const float Zc = VF_SelectVerticalTreeConnectorZ(
|
||||||
BottomZ, TopZ, VoxelHash::ToFloat01(VoxelHash::Mix(LinkHash)));
|
P, BottomZ, TopZ, LinkHash);
|
||||||
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);
|
||||||
Cache.Conns.Add({ FVector(Child.X, Child.Y, Zc), ParentPoint, TreeRadius });
|
const FConn Conn{
|
||||||
|
FVector(Child.X, Child.Y, Zc), ParentPoint, TreeRadius };
|
||||||
|
if (ConnectorMayReachCell(Conn))
|
||||||
|
{
|
||||||
|
Cache.Conns.Add(Conn);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Structural drainage tree. Every real shaft emitted by this cache chooses the
|
// Structural drainage tree. Every real shaft in the rebuild-only child window
|
||||||
// nearest strictly more-central shaft in its complete 5×5 window; a local minimum
|
// first chooses the nearest strictly more-central shaft in its complete 5×5
|
||||||
// drains to the spine. The explicit row-major cell tie-break is independent of
|
// window. A local minimum falls back to the nearest lower-origin shaft in the
|
||||||
// array iteration order, and the synthetic spine is not a candidate shaft.
|
// deterministic halo; a window touching the origin uses the spine directly, with
|
||||||
for (const FShaft& Child : Cache.Shafts)
|
// a final direct-spine fallback for an unusually empty finite halo. Every parent
|
||||||
|
// edge therefore either strictly decreases origin distance or terminates at the
|
||||||
|
// spine: no cycles and no orphans. Spatial culling makes the same edge visible
|
||||||
|
// throughout its capsule, independent of which cache cell evaluates it.
|
||||||
|
for (const FShaft& Child : TreeEmitShafts)
|
||||||
{
|
{
|
||||||
if (Child.bOriginSpine) continue;
|
if (Child.bOriginSpine) continue;
|
||||||
|
|
||||||
const float ChildOriginSq = FMath::Square(Child.X) + FMath::Square(Child.Y);
|
const float ChildOriginSq = FMath::Square(Child.X) + FMath::Square(Child.Y);
|
||||||
const FShaft* Parent = nullptr;
|
const FShaft* Parent = nullptr;
|
||||||
float BestDistanceSq = FLT_MAX;
|
float BestDistanceSq = FLT_MAX;
|
||||||
for (const FShaft& Candidate : CollectedShafts)
|
for (int32 dy = -CandidateRadius; dy <= CandidateRadius; ++dy)
|
||||||
|
for (int32 dx = -CandidateRadius; dx <= CandidateRadius; ++dx)
|
||||||
{
|
{
|
||||||
if (Candidate.CellX == Child.CellX && Candidate.CellY == Child.CellY)
|
const FShaft* Candidate = FindCollectedShaft(
|
||||||
{
|
Child.CellX + dx, Child.CellY + dy);
|
||||||
continue;
|
if (Candidate == nullptr
|
||||||
}
|
|| (Candidate->CellX == Child.CellX && Candidate->CellY == Child.CellY))
|
||||||
if (FMath::Abs(Candidate.CellX - Child.CellX) > CandidateRadius
|
|
||||||
|| FMath::Abs(Candidate.CellY - Child.CellY) > CandidateRadius)
|
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const float CandidateOriginSq = FMath::Square(Candidate.X)
|
const float CandidateOriginSq = FMath::Square(Candidate->X)
|
||||||
+ FMath::Square(Candidate.Y);
|
+ FMath::Square(Candidate->Y);
|
||||||
if (!(CandidateOriginSq < ChildOriginSq))
|
if (!(CandidateOriginSq < ChildOriginSq))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const float DistanceSq = FMath::Square(Child.X - Candidate.X)
|
const float DistanceSq = FMath::Square(Child.X - Candidate->X)
|
||||||
+ FMath::Square(Child.Y - Candidate.Y);
|
+ FMath::Square(Child.Y - Candidate->Y);
|
||||||
const bool bLowerCell = Parent == nullptr
|
const bool bLowerCell = Parent == nullptr
|
||||||
|| Candidate.CellY < Parent->CellY
|
|| Candidate->CellY < Parent->CellY
|
||||||
|| (Candidate.CellY == Parent->CellY
|
|| (Candidate->CellY == Parent->CellY
|
||||||
&& Candidate.CellX < Parent->CellX);
|
&& Candidate->CellX < Parent->CellX);
|
||||||
if (DistanceSq < BestDistanceSq
|
if (DistanceSq < BestDistanceSq
|
||||||
|| (DistanceSq == BestDistanceSq && bLowerCell))
|
|| (DistanceSq == BestDistanceSq && bLowerCell))
|
||||||
{
|
{
|
||||||
BestDistanceSq = DistanceSq;
|
BestDistanceSq = DistanceSq;
|
||||||
Parent = &Candidate;
|
Parent = Candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Parent == nullptr
|
||||||
|
&& !(FMath::Abs(Child.CellX) <= CandidateRadius
|
||||||
|
&& FMath::Abs(Child.CellY) <= CandidateRadius))
|
||||||
|
{
|
||||||
|
// Specified neighbour-shaft fallback. The lower-origin restriction is the
|
||||||
|
// proof that this fallback cannot create a cycle.
|
||||||
|
for (int32 dy = -FallbackRadius; dy <= FallbackRadius; ++dy)
|
||||||
|
for (int32 dx = -FallbackRadius; dx <= FallbackRadius; ++dx)
|
||||||
|
{
|
||||||
|
const FShaft* Candidate = FindCollectedShaft(
|
||||||
|
Child.CellX + dx, Child.CellY + dy);
|
||||||
|
if (Candidate == nullptr
|
||||||
|
|| (Candidate->CellX == Child.CellX
|
||||||
|
&& Candidate->CellY == Child.CellY))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float CandidateOriginSq = FMath::Square(Candidate->X)
|
||||||
|
+ FMath::Square(Candidate->Y);
|
||||||
|
if (!(CandidateOriginSq < ChildOriginSq))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float DistanceSq = FMath::Square(Child.X - Candidate->X)
|
||||||
|
+ FMath::Square(Child.Y - Candidate->Y);
|
||||||
|
const bool bLowerCell = Parent == nullptr
|
||||||
|
|| Candidate->CellY < Parent->CellY
|
||||||
|
|| (Candidate->CellY == Parent->CellY
|
||||||
|
&& Candidate->CellX < Parent->CellX);
|
||||||
|
if (DistanceSq < BestDistanceSq
|
||||||
|
|| (DistanceSq == BestDistanceSq && bLowerCell))
|
||||||
|
{
|
||||||
|
BestDistanceSq = DistanceSq;
|
||||||
|
Parent = Candidate;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1950,7 +2139,7 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Existing random links remain the texture/loop layer and stay on the inner 3×3
|
// Existing random links remain the texture/loop layer and stay on the inner 3×3
|
||||||
// set. Only the deterministic parent lookup uses the 7×7 collection.
|
// set. Only the deterministic tree build uses the wider 9×9/15×15 collection.
|
||||||
if (P.CrossConnectChance > 0.0f && Cache.Shafts.Num() >= 2)
|
if (P.CrossConnectChance > 0.0f && Cache.Shafts.Num() >= 2)
|
||||||
{
|
{
|
||||||
for (int32 i = 0; i < Cache.Shafts.Num(); i++)
|
for (int32 i = 0; i < Cache.Shafts.Num(); i++)
|
||||||
@@ -1973,7 +2162,12 @@ namespace
|
|||||||
const float Zc = FMath::Lerp(BottomZ, TopZ, VoxelHash::ToFloat01(VoxelHash::Mix(PH)));
|
const float Zc = FMath::Lerp(BottomZ, TopZ, VoxelHash::ToFloat01(VoxelHash::Mix(PH)));
|
||||||
const float ConnectorR = bSpineConnector
|
const float ConnectorR = bSpineConnector
|
||||||
? SpineConnectorRadius() : P.ConnectorRadius;
|
? SpineConnectorRadius() : P.ConnectorRadius;
|
||||||
Cache.Conns.Add({ FVector(A.X, A.Y, Zc), FVector(B.X, B.Y, Zc), ConnectorR });
|
const FConn Conn{
|
||||||
|
FVector(A.X, A.Y, Zc), FVector(B.X, B.Y, Zc), ConnectorR };
|
||||||
|
if (ConnectorMayReachCell(Conn))
|
||||||
|
{
|
||||||
|
Cache.Conns.Add(Conn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2024,7 +2218,10 @@ namespace
|
|||||||
const float D2 = FMath::Square(WorldX - Sh.X) + FMath::Square(WorldY - Sh.Y);
|
const float D2 = FMath::Square(WorldX - Sh.X) + FMath::Square(WorldY - Sh.Y);
|
||||||
if (D2 < BestSq) { BestSq = D2; Near = &Sh; }
|
if (D2 < BestSq) { BestSq = D2; Near = &Sh; }
|
||||||
}
|
}
|
||||||
if (Near && (WorldX - Near->X) + (WorldY - Near->Y) > 0.0f)
|
// The shaft axis is the structural tree's terminal point. Keep the mathematical
|
||||||
|
// half-plane boundary open: tiny cancellation error must not turn an exact axis
|
||||||
|
// landing into a solid ledge cap and sever the parent capsule.
|
||||||
|
if (Near && (WorldX - Near->X) + (WorldY - Near->Y) > 1.0e-3f)
|
||||||
{
|
{
|
||||||
const float Shelf = 1.0f - SmoothStep01(BandT / P.LedgeDepth);
|
const float Shelf = 1.0f - SmoothStep01(BandT / P.LedgeDepth);
|
||||||
InOut.Density = FMath::Max(InOut.Density, Shelf * P.BaseDensity);
|
InOut.Density = FMath::Max(InOut.Density, Shelf * P.BaseDensity);
|
||||||
|
|||||||
@@ -226,6 +226,44 @@ 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)
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
@@ -3739,16 +3777,17 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
|
|||||||
const int32 CY = FMath::FloorToInt(WorldY / Spacing);
|
const int32 CY = FMath::FloorToInt(WorldY / Spacing);
|
||||||
|
|
||||||
// The shaft topology is pure, but it is deliberately split into two windows:
|
// The shaft topology is pure, but it is deliberately split into two windows:
|
||||||
// * COLLECT 7×7 cells once during this thread-local cache rebuild. This is the source data
|
// * COLLECT a deterministic halo once during this thread-local cache rebuild. This is the
|
||||||
// needed to resolve every 5×5 parent window for the emitted cells below.
|
// source data needed to resolve every 5×5 parent window and the fixed ±3 fallback window.
|
||||||
// * EMIT/EVALUATE only the INNER 3×3 cells. A shaft in that inner window has its whole
|
// * EVALUATE only the INNER 3×3 shaft cylinders. Tree links are collected from a wider
|
||||||
// 5×5 candidate window inside the collected 7×7, so two neighbouring evaluation cells
|
// rebuild-only child window and culled to the current cell before the voxel loop sees
|
||||||
// compute the same parent for the same shaft. The 7×7 work is therefore rebuild-only;
|
// them. That matters: a capsule from a child to a parent two or three cells away must be
|
||||||
// the per-voxel loop still sees at most the inner 3×3 shafts plus their cached capsules.
|
// 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; };
|
||||||
thread_local TArray<FLocalShaft, TInlineAllocator<10>> Shafts;
|
thread_local TArray<FLocalShaft, TInlineAllocator<10>> Shafts;
|
||||||
thread_local TArray<FLocalConn, TInlineAllocator<24>> Conns;
|
thread_local TArray<FLocalConn, TInlineAllocator<32>> Conns;
|
||||||
|
thread_local TArray<FLocalShaft, TInlineAllocator<81>> TreeEmitShafts;
|
||||||
thread_local int32 VS_CX = INT32_MAX, VS_CY = INT32_MAX;
|
thread_local int32 VS_CX = INT32_MAX, VS_CY = INT32_MAX;
|
||||||
thread_local uint32 VS_Seed = 0xFFFFFFFFu;
|
thread_local uint32 VS_Seed = 0xFFFFFFFFu;
|
||||||
thread_local float VS_Spacing = -1.0f, VS_Dens = -1.0f, VS_MinR = -1.0f, VS_MaxR = -1.0f,
|
thread_local float VS_Spacing = -1.0f, VS_Dens = -1.0f, VS_MinR = -1.0f, VS_MaxR = -1.0f,
|
||||||
@@ -3771,15 +3810,37 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
|
|||||||
VS_Seal = Params.BoundarySealThickness;
|
VS_Seal = Params.BoundarySealThickness;
|
||||||
Shafts.Reset();
|
Shafts.Reset();
|
||||||
Conns.Reset();
|
Conns.Reset();
|
||||||
|
TreeEmitShafts.Reset();
|
||||||
|
|
||||||
constexpr int32 CollectRadius = 3; // 7×7: inner 3×3 + one 5×5 candidate halo
|
constexpr int32 EmitRadius = 1; // 3×3: shaft cylinders and random links
|
||||||
constexpr int32 EmitRadius = 1; // 3×3: the only cells allowed to emit tree links
|
|
||||||
constexpr int32 CandidateRadius = 2; // 5×5 parent window around an emitting shaft
|
constexpr int32 CandidateRadius = 2; // 5×5 parent window around an emitting shaft
|
||||||
|
constexpr int32 FallbackRadius = 3; // fixed fallback window around an emitting shaft
|
||||||
constexpr uint32 TreeSalt = 0x7A11u;
|
constexpr uint32 TreeSalt = 0x7A11u;
|
||||||
|
|
||||||
TArray<FLocalShaft, TInlineAllocator<49>> CollectedShafts;
|
const float BottomZ = Params.StrateBottomWorldZ + Params.BoundarySealThickness;
|
||||||
|
const float TopZ = Params.StrateTopWorldZ - Params.BoundarySealThickness;
|
||||||
|
const float RoughnessReach = FMath::Max(Params.SurfaceRoughness, 0.0f)
|
||||||
|
* VOXEL_NOISE_SCALE * 1.5f;
|
||||||
|
// A tree link must remain open at the centreline after the SDF roughness pass. The +1
|
||||||
|
// margin makes the radius strictly greater than the proven roughness supremum.
|
||||||
|
const float TreeConnectorRadius = FMath::Max(Params.ConnectorRadius, RoughnessReach + 1.0f);
|
||||||
|
// A tree capsule can affect a cell even when neither endpoint is in that cell. Keep a
|
||||||
|
// rebuild-only child halo for that geometric reach, then add the parent-search halo. The
|
||||||
|
// default is a 4-cell tree-emission radius + 3-cell collection pad = 7-cell radius
|
||||||
|
// (15×15 rolls; the emitted tree-child box itself is 9×9).
|
||||||
|
const int32 ConnectorCellPad = FMath::Max(
|
||||||
|
1, FMath::CeilToInt(TreeConnectorRadius / Spacing));
|
||||||
|
const int32 TreeEmitRadius = FallbackRadius + ConnectorCellPad;
|
||||||
|
const int32 CollectRadius = TreeEmitRadius + FallbackRadius;
|
||||||
|
|
||||||
|
const int32 CollectSide = CollectRadius * 2 + 1;
|
||||||
|
TArray<FLocalShaft, TInlineAllocator<225>> ShaftGrid;
|
||||||
|
TArray<uint8, TInlineAllocator<225>> ShaftPresent;
|
||||||
|
ShaftGrid.SetNum(CollectSide * CollectSide);
|
||||||
|
ShaftPresent.Init(0, CollectSide * CollectSide);
|
||||||
// Collect the wider deterministic neighbourhood once. Only the inner cells are retained
|
// Collect the wider deterministic neighbourhood once. Only the inner cells are retained
|
||||||
// in `Shafts`, because that is the per-voxel working set and the seam-free emission set.
|
// in `Shafts` for the per-voxel cylinder/ledge work; `TreeEmitShafts` is still rebuilt
|
||||||
|
// before evaluation and its connectors are spatially culled below.
|
||||||
for (int32 dy = -CollectRadius; dy <= CollectRadius; dy++)
|
for (int32 dy = -CollectRadius; dy <= CollectRadius; dy++)
|
||||||
for (int32 dx = -CollectRadius; dx <= CollectRadius; dx++)
|
for (int32 dx = -CollectRadius; dx <= CollectRadius; dx++)
|
||||||
{
|
{
|
||||||
@@ -3797,13 +3858,31 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
|
|||||||
Sh.CellX = nx;
|
Sh.CellX = nx;
|
||||||
Sh.CellY = ny;
|
Sh.CellY = ny;
|
||||||
Sh.bOriginSpine = false;
|
Sh.bOriginSpine = false;
|
||||||
CollectedShafts.Add(Sh);
|
const int32 GridIndex = (dy + CollectRadius) * CollectSide + (dx + CollectRadius);
|
||||||
|
ShaftGrid[GridIndex] = Sh;
|
||||||
|
ShaftPresent[GridIndex] = 1;
|
||||||
if (FMath::Abs(dx) <= EmitRadius && FMath::Abs(dy) <= EmitRadius)
|
if (FMath::Abs(dx) <= EmitRadius && FMath::Abs(dy) <= EmitRadius)
|
||||||
{
|
{
|
||||||
Shafts.Add(Sh);
|
Shafts.Add(Sh);
|
||||||
}
|
}
|
||||||
|
if (FMath::Abs(dx) <= TreeEmitRadius && FMath::Abs(dy) <= TreeEmitRadius)
|
||||||
|
{
|
||||||
|
TreeEmitShafts.Add(Sh);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto FindCollectedShaft = [&](int32 CellX, int32 CellY) -> const FLocalShaft*
|
||||||
|
{
|
||||||
|
const int32 dx = CellX - CX;
|
||||||
|
const int32 dy = CellY - CY;
|
||||||
|
if (FMath::Abs(dx) > CollectRadius || FMath::Abs(dy) > CollectRadius)
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
const int32 GridIndex = (dy + CollectRadius) * CollectSide + (dx + CollectRadius);
|
||||||
|
return ShaftPresent[GridIndex] ? &ShaftGrid[GridIndex] : nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
// The structural (0,0) spine is not re-carved here. It participates only as a
|
// The structural (0,0) spine is not re-carved here. It participates only as a
|
||||||
// connector endpoint. The local 3x3 set is origin-adjacent when its centre cell is in
|
// connector endpoint. The local 3x3 set is origin-adjacent when its centre cell is in
|
||||||
// the central 3x3; the structural post remains the sole owner of the vertical column.
|
// the central 3x3; the structural post remains the sole owner of the vertical column.
|
||||||
@@ -3813,13 +3892,20 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
|
|||||||
Shafts.Add({0.0f, 0.0f, OriginSpineRadius, 0, 0, true});
|
Shafts.Add({0.0f, 0.0f, OriginSpineRadius, 0, 0, true});
|
||||||
}
|
}
|
||||||
|
|
||||||
const float BottomZ = Params.StrateBottomWorldZ + Params.BoundarySealThickness;
|
const float CellMinX = static_cast<float>(CX) * Spacing;
|
||||||
const float TopZ = Params.StrateTopWorldZ - Params.BoundarySealThickness;
|
const float CellMaxX = static_cast<float>(CX + 1) * Spacing;
|
||||||
const float RoughnessReach = FMath::Max(Params.SurfaceRoughness, 0.0f)
|
const float CellMinY = static_cast<float>(CY) * Spacing;
|
||||||
* VOXEL_NOISE_SCALE * 1.5f;
|
const float CellMaxY = static_cast<float>(CY + 1) * Spacing;
|
||||||
// A tree link must remain open at the centreline after the SDF roughness pass. The +1
|
auto ConnectorMayReachCell = [&](const FLocalConn& Conn)
|
||||||
// margin makes the radius strictly greater than the proven roughness supremum.
|
{
|
||||||
const float TreeConnectorRadius = FMath::Max(Params.ConnectorRadius, RoughnessReach + 1.0f);
|
const float MinX = FMath::Min(Conn.A.X, Conn.B.X) - Conn.Radius;
|
||||||
|
const float MaxX = FMath::Max(Conn.A.X, Conn.B.X) + Conn.Radius;
|
||||||
|
const float MinY = FMath::Min(Conn.A.Y, Conn.B.Y) - Conn.Radius;
|
||||||
|
const float MaxY = FMath::Max(Conn.A.Y, Conn.B.Y) + Conn.Radius;
|
||||||
|
const float GapX = FMath::Max3(CellMinX - MaxX, MinX - CellMaxX, 0.0f);
|
||||||
|
const float GapY = FMath::Max3(CellMinY - MaxY, MinY - CellMaxY, 0.0f);
|
||||||
|
return GapX == 0.0f && GapY == 0.0f;
|
||||||
|
};
|
||||||
|
|
||||||
auto EmitTreeConnector = [&](const FLocalShaft& Child, const FLocalShaft* Parent)
|
auto EmitTreeConnector = [&](const FLocalShaft& Child, const FLocalShaft* Parent)
|
||||||
{
|
{
|
||||||
@@ -3827,54 +3913,105 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
|
|||||||
const int32 ParentCellY = Parent != nullptr ? Parent->CellY : 0;
|
const int32 ParentCellY = Parent != nullptr ? Parent->CellY : 0;
|
||||||
const uint32 LinkHash = VoxelHash::Pair(
|
const uint32 LinkHash = VoxelHash::Pair(
|
||||||
Child.CellX, Child.CellY, ParentCellX, ParentCellY, S ^ TreeSalt);
|
Child.CellX, Child.CellY, ParentCellX, ParentCellY, S ^ TreeSalt);
|
||||||
const float Zc = FMath::Lerp(BottomZ, TopZ,
|
const float Zc = VF_SelectVerticalTreeConnectorZ(
|
||||||
VoxelHash::ToFloat01(VoxelHash::Mix(LinkHash)));
|
Params, BottomZ, TopZ, LinkHash);
|
||||||
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);
|
||||||
Conns.Add({ FVector(Child.X, Child.Y, Zc), ParentPoint, TreeConnectorRadius });
|
const FLocalConn Conn{
|
||||||
|
FVector(Child.X, Child.Y, Zc), ParentPoint, TreeConnectorRadius };
|
||||||
|
if (ConnectorMayReachCell(Conn))
|
||||||
|
{
|
||||||
|
Conns.Add(Conn);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Structural drainage tree: every real shaft in the inner 3×3 selects one strictly
|
// Structural drainage tree: every real shaft in the rebuild-only child window first
|
||||||
// more-central shaft from its complete 5×5 window. A local minimum has no such parent and
|
// selects one strictly more-central shaft from its complete 5×5 window. If that window
|
||||||
// drains directly to the (0,0) spine. The explicit cell-coordinate tie-break makes this
|
// has a local minimum, select the nearest shaft in the collected deterministic halo whose
|
||||||
// independent of array/iteration order; the synthetic spine is not a candidate shaft.
|
// origin distance is lower; that shaft is already on a strictly descending path by the
|
||||||
for (const FLocalShaft& Child : Shafts)
|
// same rule. A shaft whose window reaches the origin uses the spine directly, and the
|
||||||
|
// final direct-spine fallback covers an unusually empty finite halo. Thus every emitted
|
||||||
|
// parent edge either strictly decreases origin distance or terminates at the spine: no
|
||||||
|
// cycles and no orphans. Spatial culling makes the same edge visible throughout its
|
||||||
|
// capsule, independent of which cache cell is evaluating it.
|
||||||
|
// The explicit cell-coordinate tie-break makes this independent of array iteration order;
|
||||||
|
// the synthetic spine is not a candidate shaft.
|
||||||
|
for (const FLocalShaft& Child : TreeEmitShafts)
|
||||||
{
|
{
|
||||||
if (Child.bOriginSpine) continue;
|
if (Child.bOriginSpine) continue;
|
||||||
|
|
||||||
const float ChildOriginSq = FMath::Square(Child.X) + FMath::Square(Child.Y);
|
const float ChildOriginSq = FMath::Square(Child.X) + FMath::Square(Child.Y);
|
||||||
const FLocalShaft* Parent = nullptr;
|
const FLocalShaft* Parent = nullptr;
|
||||||
float BestDistanceSq = FLT_MAX;
|
float BestDistanceSq = FLT_MAX;
|
||||||
for (const FLocalShaft& Candidate : CollectedShafts)
|
for (int32 dy = -CandidateRadius; dy <= CandidateRadius; ++dy)
|
||||||
|
for (int32 dx = -CandidateRadius; dx <= CandidateRadius; ++dx)
|
||||||
{
|
{
|
||||||
if (Candidate.CellX == Child.CellX && Candidate.CellY == Child.CellY)
|
const FLocalShaft* Candidate = FindCollectedShaft(
|
||||||
{
|
Child.CellX + dx, Child.CellY + dy);
|
||||||
continue;
|
if (Candidate == nullptr
|
||||||
}
|
|| (Candidate->CellX == Child.CellX && Candidate->CellY == Child.CellY))
|
||||||
if (FMath::Abs(Candidate.CellX - Child.CellX) > CandidateRadius
|
|
||||||
|| FMath::Abs(Candidate.CellY - Child.CellY) > CandidateRadius)
|
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const float CandidateOriginSq = FMath::Square(Candidate.X)
|
const float CandidateOriginSq = FMath::Square(Candidate->X)
|
||||||
+ FMath::Square(Candidate.Y);
|
+ FMath::Square(Candidate->Y);
|
||||||
if (!(CandidateOriginSq < ChildOriginSq))
|
if (!(CandidateOriginSq < ChildOriginSq))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const float DistanceSq = FMath::Square(Child.X - Candidate.X)
|
const float DistanceSq = FMath::Square(Child.X - Candidate->X)
|
||||||
+ FMath::Square(Child.Y - Candidate.Y);
|
+ FMath::Square(Child.Y - Candidate->Y);
|
||||||
const bool bLowerCell = Parent == nullptr
|
const bool bLowerCell = Parent == nullptr
|
||||||
|| Candidate.CellY < Parent->CellY
|
|| Candidate->CellY < Parent->CellY
|
||||||
|| (Candidate.CellY == Parent->CellY && Candidate.CellX < Parent->CellX);
|
|| (Candidate->CellY == Parent->CellY && Candidate->CellX < Parent->CellX);
|
||||||
if (DistanceSq < BestDistanceSq
|
if (DistanceSq < BestDistanceSq
|
||||||
|| (DistanceSq == BestDistanceSq && bLowerCell))
|
|| (DistanceSq == BestDistanceSq && bLowerCell))
|
||||||
{
|
{
|
||||||
BestDistanceSq = DistanceSq;
|
BestDistanceSq = DistanceSq;
|
||||||
Parent = &Candidate;
|
Parent = Candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Parent == nullptr
|
||||||
|
&& !(FMath::Abs(Child.CellX) <= CandidateRadius
|
||||||
|
&& FMath::Abs(Child.CellY) <= CandidateRadius))
|
||||||
|
{
|
||||||
|
// This is the specified neighbour-shaft fallback. Restricting it to a lower
|
||||||
|
// origin distance makes the parent graph well-founded even though the fallback
|
||||||
|
// candidate need not be in the child's 5×5 window.
|
||||||
|
for (int32 dy = -FallbackRadius; dy <= FallbackRadius; ++dy)
|
||||||
|
for (int32 dx = -FallbackRadius; dx <= FallbackRadius; ++dx)
|
||||||
|
{
|
||||||
|
const FLocalShaft* Candidate = FindCollectedShaft(
|
||||||
|
Child.CellX + dx, Child.CellY + dy);
|
||||||
|
if (Candidate == nullptr
|
||||||
|
|| (Candidate->CellX == Child.CellX && Candidate->CellY == Child.CellY))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float CandidateOriginSq = FMath::Square(Candidate->X)
|
||||||
|
+ FMath::Square(Candidate->Y);
|
||||||
|
if (!(CandidateOriginSq < ChildOriginSq))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float DistanceSq = FMath::Square(Child.X - Candidate->X)
|
||||||
|
+ FMath::Square(Child.Y - Candidate->Y);
|
||||||
|
const bool bLowerCell = Parent == nullptr
|
||||||
|
|| Candidate->CellY < Parent->CellY
|
||||||
|
|| (Candidate->CellY == Parent->CellY
|
||||||
|
&& Candidate->CellX < Parent->CellX);
|
||||||
|
if (DistanceSq < BestDistanceSq
|
||||||
|
|| (DistanceSq == BestDistanceSq && bLowerCell))
|
||||||
|
{
|
||||||
|
BestDistanceSq = DistanceSq;
|
||||||
|
Parent = Candidate;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3882,7 +4019,7 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Existing probabilistic links remain as texture and loops. They intentionally stay on
|
// Existing probabilistic links remain as texture and loops. They intentionally stay on
|
||||||
// the cached inner 3×3 working set; only the structural parent lookup needs the 7×7 halo.
|
// the cached inner 3×3 working set; only the structural tree build needs the 9×9/15×15 halo.
|
||||||
if (Params.CrossConnectChance > 0.0f && Shafts.Num() >= 2)
|
if (Params.CrossConnectChance > 0.0f && Shafts.Num() >= 2)
|
||||||
{
|
{
|
||||||
for (int32 i = 0; i < Shafts.Num(); i++)
|
for (int32 i = 0; i < Shafts.Num(); i++)
|
||||||
@@ -3906,7 +4043,12 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
|
|||||||
Params.ConnectorRadius,
|
Params.ConnectorRadius,
|
||||||
RoughnessReach + 1.0f)
|
RoughnessReach + 1.0f)
|
||||||
: Params.ConnectorRadius;
|
: Params.ConnectorRadius;
|
||||||
Conns.Add({ FVector(A.X, A.Y, Zc), FVector(B.X, B.Y, Zc), ConnectorR });
|
const FLocalConn Conn{
|
||||||
|
FVector(A.X, A.Y, Zc), FVector(B.X, B.Y, Zc), ConnectorR };
|
||||||
|
if (ConnectorMayReachCell(Conn))
|
||||||
|
{
|
||||||
|
Conns.Add(Conn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3958,7 +4100,10 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
|
|||||||
const float D2 = FMath::Square(WorldX - Sh.X) + FMath::Square(WorldY - Sh.Y);
|
const float D2 = FMath::Square(WorldX - Sh.X) + FMath::Square(WorldY - Sh.Y);
|
||||||
if (D2 < BestSq) { BestSq = D2; Near = &Sh; }
|
if (D2 < BestSq) { BestSq = D2; Near = &Sh; }
|
||||||
}
|
}
|
||||||
if (Near && (WorldX - Near->X) + (WorldY - Near->Y) > 0.0f)
|
// The shaft axis is the structural tree's terminal point. Keep the mathematical
|
||||||
|
// half-plane boundary open: tiny cancellation error must not turn an exact axis
|
||||||
|
// landing into a solid ledge cap and sever the parent capsule.
|
||||||
|
if (Near && (WorldX - Near->X) + (WorldY - Near->Y) > 1.0e-3f)
|
||||||
{
|
{
|
||||||
float Shelf = 1.0f - SmoothStep01(BandT / Params.LedgeDepth);
|
float Shelf = 1.0f - SmoothStep01(BandT / Params.LedgeDepth);
|
||||||
Density = FMath::Max(Density, Shelf * Params.BaseDensity);
|
Density = FMath::Max(Density, Shelf * Params.BaseDensity);
|
||||||
|
|||||||
@@ -97,7 +97,14 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
{
|
{
|
||||||
return Refuse(OutReason, TEXT("SampleStep must be greater than zero."));
|
return Refuse(OutReason, TEXT("SampleStep must be greater than zero."));
|
||||||
}
|
}
|
||||||
if (Settings.RadiusInVoxels <= 0)
|
const bool bHasCoverPointA = Settings.CoverPointA.IsSet();
|
||||||
|
const bool bHasCoverPointB = Settings.CoverPointB.IsSet();
|
||||||
|
if (bHasCoverPointA != bHasCoverPointB)
|
||||||
|
{
|
||||||
|
return Refuse(OutReason, TEXT("CoverPointA and CoverPointB must be set together."));
|
||||||
|
}
|
||||||
|
const bool bUseFittedWindow = bHasCoverPointA && bHasCoverPointB;
|
||||||
|
if (!bUseFittedWindow && Settings.RadiusInVoxels <= 0)
|
||||||
{
|
{
|
||||||
return Refuse(OutReason, TEXT("RadiusInVoxels must be greater than zero."));
|
return Refuse(OutReason, TEXT("RadiusInVoxels must be greater than zero."));
|
||||||
}
|
}
|
||||||
@@ -109,10 +116,26 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
{
|
{
|
||||||
return Refuse(OutReason, TEXT("HeadroomCells cannot be negative."));
|
return Refuse(OutReason, TEXT("HeadroomCells cannot be negative."));
|
||||||
}
|
}
|
||||||
if (!FMath::IsFinite(Settings.CenterXY.X) || !FMath::IsFinite(Settings.CenterXY.Y))
|
if (!bUseFittedWindow
|
||||||
|
&& (!FMath::IsFinite(Settings.CenterXY.X) || !FMath::IsFinite(Settings.CenterXY.Y)))
|
||||||
{
|
{
|
||||||
return Refuse(OutReason, TEXT("CenterXY must contain finite voxel coordinates."));
|
return Refuse(OutReason, TEXT("CenterXY must contain finite voxel coordinates."));
|
||||||
}
|
}
|
||||||
|
if (bUseFittedWindow)
|
||||||
|
{
|
||||||
|
const FVector2D& PointA = Settings.CoverPointA.GetValue();
|
||||||
|
const FVector2D& PointB = Settings.CoverPointB.GetValue();
|
||||||
|
if (!FMath::IsFinite(PointA.X) || !FMath::IsFinite(PointA.Y)
|
||||||
|
|| !FMath::IsFinite(PointB.X) || !FMath::IsFinite(PointB.Y))
|
||||||
|
{
|
||||||
|
return Refuse(OutReason, TEXT("Cover points must contain finite voxel coordinates."));
|
||||||
|
}
|
||||||
|
if (!FMath::IsFinite(Settings.CoverMarginVoxels)
|
||||||
|
|| Settings.CoverMarginVoxels < 0.0f)
|
||||||
|
{
|
||||||
|
return Refuse(OutReason, TEXT("CoverMarginVoxels must be finite and non-negative."));
|
||||||
|
}
|
||||||
|
}
|
||||||
if (Slot.TopChunkZ <= Slot.BottomChunkZ)
|
if (Slot.TopChunkZ <= Slot.BottomChunkZ)
|
||||||
{
|
{
|
||||||
return Refuse(OutReason, TEXT("The requested strate has no positive vertical extent."));
|
return Refuse(OutReason, TEXT("The requested strate has no positive vertical extent."));
|
||||||
@@ -128,13 +151,52 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
|
|
||||||
// Resolve the margin from the same blended parameter query used by generation. The
|
// Resolve the margin from the same blended parameter query used by generation. The
|
||||||
// representative chunk is deliberately in the middle of this slot and uses the sample
|
// representative chunk is deliberately in the middle of this slot and uses the sample
|
||||||
// centre's XY chunk, so a future non-Hard transition resolves through the manager exactly
|
// window centre's XY chunk, so a future non-Hard transition resolves through the manager
|
||||||
// as generation does at that representative location.
|
// exactly as generation does at that representative location.
|
||||||
|
FVector2D WindowCenter = Settings.CenterXY;
|
||||||
|
double MinX64 = 0.0;
|
||||||
|
double MaxX64 = 0.0;
|
||||||
|
double MinY64 = 0.0;
|
||||||
|
double MaxY64 = 0.0;
|
||||||
|
if (bUseFittedWindow)
|
||||||
|
{
|
||||||
|
const FVector2D& PointA = Settings.CoverPointA.GetValue();
|
||||||
|
const FVector2D& PointB = Settings.CoverPointB.GetValue();
|
||||||
|
const double Margin = static_cast<double>(Settings.CoverMarginVoxels);
|
||||||
|
MinX64 = FMath::Min(static_cast<double>(PointA.X), static_cast<double>(PointB.X)) - Margin;
|
||||||
|
MaxX64 = FMath::Max(static_cast<double>(PointA.X), static_cast<double>(PointB.X)) + Margin;
|
||||||
|
MinY64 = FMath::Min(static_cast<double>(PointA.Y), static_cast<double>(PointB.Y)) - Margin;
|
||||||
|
MaxY64 = FMath::Max(static_cast<double>(PointA.Y), static_cast<double>(PointB.Y)) + Margin;
|
||||||
|
WindowCenter = FVector2D(
|
||||||
|
static_cast<float>(0.5 * (MinX64 + MaxX64)),
|
||||||
|
static_cast<float>(0.5 * (MinY64 + MaxY64)));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
MinX64 = static_cast<double>(Settings.CenterXY.X)
|
||||||
|
- static_cast<double>(Settings.RadiusInVoxels);
|
||||||
|
MaxX64 = static_cast<double>(Settings.CenterXY.X)
|
||||||
|
+ static_cast<double>(Settings.RadiusInVoxels);
|
||||||
|
MinY64 = static_cast<double>(Settings.CenterXY.Y)
|
||||||
|
- static_cast<double>(Settings.RadiusInVoxels);
|
||||||
|
MaxY64 = static_cast<double>(Settings.CenterXY.Y)
|
||||||
|
+ static_cast<double>(Settings.RadiusInVoxels);
|
||||||
|
}
|
||||||
|
const double XYExtentX = MaxX64 - MinX64;
|
||||||
|
const double XYExtentY = MaxY64 - MinY64;
|
||||||
|
if (!FMath::IsFinite(MinX64) || !FMath::IsFinite(MaxX64)
|
||||||
|
|| !FMath::IsFinite(MinY64) || !FMath::IsFinite(MaxY64)
|
||||||
|
|| !FMath::IsFinite(XYExtentX) || !FMath::IsFinite(XYExtentY)
|
||||||
|
|| XYExtentX < 1.0 || XYExtentY < 1.0)
|
||||||
|
{
|
||||||
|
return Refuse(OutReason, TEXT("The requested XY window is not a finite positive voxel box."));
|
||||||
|
}
|
||||||
|
|
||||||
const int32 MidChunkZ = Slot.BottomChunkZ
|
const int32 MidChunkZ = Slot.BottomChunkZ
|
||||||
+ (Slot.TopChunkZ - Slot.BottomChunkZ) / 2;
|
+ (Slot.TopChunkZ - Slot.BottomChunkZ) / 2;
|
||||||
const FIntVector RepresentativeChunk(
|
const FIntVector RepresentativeChunk(
|
||||||
FMath::FloorToInt(Settings.CenterXY.X / static_cast<float>(CHUNK_SIZE)),
|
FMath::FloorToInt(WindowCenter.X / static_cast<float>(CHUNK_SIZE)),
|
||||||
FMath::FloorToInt(Settings.CenterXY.Y / static_cast<float>(CHUNK_SIZE)),
|
FMath::FloorToInt(WindowCenter.Y / static_cast<float>(CHUNK_SIZE)),
|
||||||
MidChunkZ);
|
MidChunkZ);
|
||||||
const FStrateGenerationParams RepresentativeParams =
|
const FStrateGenerationParams RepresentativeParams =
|
||||||
Manager.GetGenerationParams(RepresentativeChunk);
|
Manager.GetGenerationParams(RepresentativeChunk);
|
||||||
@@ -188,10 +250,18 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
return Refuse(OutReason, TEXT("The sampled Z window is outside the metrics struct's int32 range."));
|
return Refuse(OutReason, TEXT("The sampled Z window is outside the metrics struct's int32 range."));
|
||||||
}
|
}
|
||||||
|
|
||||||
const int64 XYExtent = static_cast<int64>(Settings.RadiusInVoxels) * 2;
|
|
||||||
const int64 Step = static_cast<int64>(Settings.SampleStep);
|
const int64 Step = static_cast<int64>(Settings.SampleStep);
|
||||||
const int64 NumX64 = CeilDivPositive(XYExtent, Step);
|
const double NumXReal = XYExtentX / static_cast<double>(Step);
|
||||||
const int64 NumY64 = NumX64;
|
const double NumYReal = XYExtentY / static_cast<double>(Step);
|
||||||
|
if (!FMath::IsFinite(NumXReal) || !FMath::IsFinite(NumYReal)
|
||||||
|
|| NumXReal <= 0.0 || NumYReal <= 0.0
|
||||||
|
|| NumXReal > static_cast<double>(INT32_MAX)
|
||||||
|
|| NumYReal > static_cast<double>(INT32_MAX))
|
||||||
|
{
|
||||||
|
return Refuse(OutReason, TEXT("A sample-grid dimension exceeds the supported array size."));
|
||||||
|
}
|
||||||
|
const int64 NumX64 = FMath::CeilToInt64(NumXReal);
|
||||||
|
const int64 NumY64 = FMath::CeilToInt64(NumYReal);
|
||||||
const int64 NumZ64 = CeilDivPositive(InteriorHeight, Step);
|
const int64 NumZ64 = CeilDivPositive(InteriorHeight, Step);
|
||||||
const int64 MaxCells = static_cast<int64>(Settings.MaxCells);
|
const int64 MaxCells = static_cast<int64>(Settings.MaxCells);
|
||||||
|
|
||||||
@@ -216,14 +286,6 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
return Refuse(OutReason, TEXT("The requested sample grid exceeds the bounded cell count."));
|
return Refuse(OutReason, TEXT("The requested sample grid exceeds the bounded cell count."));
|
||||||
}
|
}
|
||||||
|
|
||||||
const double MinX64 = static_cast<double>(Settings.CenterXY.X)
|
|
||||||
- static_cast<double>(Settings.RadiusInVoxels);
|
|
||||||
const double MaxX64 = static_cast<double>(Settings.CenterXY.X)
|
|
||||||
+ static_cast<double>(Settings.RadiusInVoxels);
|
|
||||||
const double MinY64 = static_cast<double>(Settings.CenterXY.Y)
|
|
||||||
- static_cast<double>(Settings.RadiusInVoxels);
|
|
||||||
const double MaxY64 = static_cast<double>(Settings.CenterXY.Y)
|
|
||||||
+ static_cast<double>(Settings.RadiusInVoxels);
|
|
||||||
const float MinX = static_cast<float>(MinX64);
|
const float MinX = static_cast<float>(MinX64);
|
||||||
const float MaxX = static_cast<float>(MaxX64);
|
const float MaxX = static_cast<float>(MaxX64);
|
||||||
const float MinY = static_cast<float>(MinY64);
|
const float MinY = static_cast<float>(MinY64);
|
||||||
@@ -301,7 +363,8 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
int64 NumAir,
|
int64 NumAir,
|
||||||
int64& OutLargestComponentCells,
|
int64& OutLargestComponentCells,
|
||||||
int32& OutLargestComponentLowestCell,
|
int32& OutLargestComponentLowestCell,
|
||||||
int32& OutNumComponentsAtLeast1Pct)
|
int32& OutNumComponentsAtLeast1Pct,
|
||||||
|
TArray<int64>* OutComponentCells)
|
||||||
{
|
{
|
||||||
OutComponents.SetNumUninitialized(Grid.CellCount);
|
OutComponents.SetNumUninitialized(Grid.CellCount);
|
||||||
for (int32 Index = 0; Index < Grid.CellCount; ++Index)
|
for (int32 Index = 0; Index < Grid.CellCount; ++Index)
|
||||||
@@ -315,6 +378,10 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
OutLargestComponentCells = 0;
|
OutLargestComponentCells = 0;
|
||||||
OutLargestComponentLowestCell = INDEX_NONE;
|
OutLargestComponentLowestCell = INDEX_NONE;
|
||||||
OutNumComponentsAtLeast1Pct = 0;
|
OutNumComponentsAtLeast1Pct = 0;
|
||||||
|
if (OutComponentCells != nullptr)
|
||||||
|
{
|
||||||
|
OutComponentCells->Reset();
|
||||||
|
}
|
||||||
|
|
||||||
for (int32 Start = 0; Start < Grid.CellCount; ++Start)
|
for (int32 Start = 0; Start < Grid.CellCount; ++Start)
|
||||||
{
|
{
|
||||||
@@ -359,6 +426,10 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
}
|
}
|
||||||
|
|
||||||
const int64 ComponentCells64 = static_cast<int64>(ComponentCells);
|
const int64 ComponentCells64 = static_cast<int64>(ComponentCells);
|
||||||
|
if (OutComponentCells != nullptr)
|
||||||
|
{
|
||||||
|
OutComponentCells->Add(ComponentCells64);
|
||||||
|
}
|
||||||
if (NumAir > 0 && ComponentCells64 * 100 >= NumAir)
|
if (NumAir > 0 && ComponentCells64 * 100 >= NumAir)
|
||||||
{
|
{
|
||||||
++OutNumComponentsAtLeast1Pct;
|
++OutNumComponentsAtLeast1Pct;
|
||||||
@@ -457,6 +528,7 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
int64 LargestComponentCells = 0;
|
int64 LargestComponentCells = 0;
|
||||||
int32 LargestComponentLowestCell = INDEX_NONE;
|
int32 LargestComponentLowestCell = INDEX_NONE;
|
||||||
int32 NumComponentsAtLeast1Pct = 0;
|
int32 NumComponentsAtLeast1Pct = 0;
|
||||||
|
TArray<int64> ComponentCells;
|
||||||
FloodFillAir(
|
FloodFillAir(
|
||||||
Grid,
|
Grid,
|
||||||
Components,
|
Components,
|
||||||
@@ -464,7 +536,8 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
NumAir,
|
NumAir,
|
||||||
LargestComponentCells,
|
LargestComponentCells,
|
||||||
LargestComponentLowestCell,
|
LargestComponentLowestCell,
|
||||||
NumComponentsAtLeast1Pct);
|
NumComponentsAtLeast1Pct,
|
||||||
|
&ComponentCells);
|
||||||
|
|
||||||
InOutMetrics.NumSampled = Grid.CellCount;
|
InOutMetrics.NumSampled = Grid.CellCount;
|
||||||
InOutMetrics.NumAir = NumAir;
|
InOutMetrics.NumAir = NumAir;
|
||||||
@@ -472,6 +545,7 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
InOutMetrics.AirFraction = static_cast<float>(
|
InOutMetrics.AirFraction = static_cast<float>(
|
||||||
static_cast<double>(NumAir) / static_cast<double>(Grid.CellCount));
|
static_cast<double>(NumAir) / static_cast<double>(Grid.CellCount));
|
||||||
InOutMetrics.NumAirComponents = NumComponents;
|
InOutMetrics.NumAirComponents = NumComponents;
|
||||||
|
InOutMetrics.AirComponentCells = MoveTemp(ComponentCells);
|
||||||
InOutMetrics.LargestComponentCells = LargestComponentCells;
|
InOutMetrics.LargestComponentCells = LargestComponentCells;
|
||||||
InOutMetrics.NumComponentsAtLeast1Pct = NumComponentsAtLeast1Pct;
|
InOutMetrics.NumComponentsAtLeast1Pct = NumComponentsAtLeast1Pct;
|
||||||
InOutMetrics.LargestComponentShare = NumAir > 0
|
InOutMetrics.LargestComponentShare = NumAir > 0
|
||||||
@@ -1005,7 +1079,8 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
NumAir,
|
NumAir,
|
||||||
LargestComponentCells,
|
LargestComponentCells,
|
||||||
LargestComponentLowestCell,
|
LargestComponentLowestCell,
|
||||||
NumComponentsAtLeast1Pct);
|
NumComponentsAtLeast1Pct,
|
||||||
|
nullptr);
|
||||||
|
|
||||||
const int32 StartComponent = Components[Start];
|
const int32 StartComponent = Components[Start];
|
||||||
const int32 GoalComponent = Components[Goal];
|
const int32 GoalComponent = Components[Goal];
|
||||||
@@ -1141,6 +1216,13 @@ FVoxelStrateMetrics VF_MeasureStrate(
|
|||||||
Result.ResolvedMarginVoxels = Grid.ResolvedMarginVoxels;
|
Result.ResolvedMarginVoxels = Grid.ResolvedMarginVoxels;
|
||||||
Result.SampledMinZ = Grid.SampledMinZ;
|
Result.SampledMinZ = Grid.SampledMinZ;
|
||||||
Result.SampledMaxZ = Grid.SampledMaxZ;
|
Result.SampledMaxZ = Grid.SampledMaxZ;
|
||||||
|
Result.SampledNumX = Grid.NumX;
|
||||||
|
Result.SampledNumY = Grid.NumY;
|
||||||
|
Result.SampledNumZ = Grid.NumZ;
|
||||||
|
Result.SampledMinX = Grid.MinX;
|
||||||
|
Result.SampledMaxX = Grid.MaxX;
|
||||||
|
Result.SampledMinY = Grid.MinY;
|
||||||
|
Result.SampledMaxY = Grid.MaxY;
|
||||||
VoxelStrateMeasurePrivate::BuildMetricsFromGrid(Grid, Settings, Result);
|
VoxelStrateMeasurePrivate::BuildMetricsFromGrid(Grid, Settings, Result);
|
||||||
Result.bValid = Result.NumSampled > 0;
|
Result.bValid = Result.NumSampled > 0;
|
||||||
if (!Result.bValid)
|
if (!Result.bValid)
|
||||||
|
|||||||
@@ -553,6 +553,8 @@ namespace VoxelCaveMorphology
|
|||||||
*
|
*
|
||||||
* Propose un point d'atterrissage déterministe complet pour la bouche d'un passage inter-strates.
|
* Propose un point d'atterrissage déterministe complet pour la bouche d'un passage inter-strates.
|
||||||
* Cette requête reste pure et le déplacement latéral est strictement borné par MaxLateralSnap.
|
* Cette requête reste pure et le déplacement latéral est strictement borné par MaxLateralSnap.
|
||||||
|
* Pour VerticalShafts, la réponse XY est exactement l'axe d'un puits sélectionné; elle n'est pas
|
||||||
|
* un point ouvert arbitraire à l'intérieur de son disque.
|
||||||
*
|
*
|
||||||
* @return true when the archetype can provide a confident point; false when it cannot.
|
* @return true when the archetype can provide a confident point; false when it cannot.
|
||||||
*/
|
*/
|
||||||
@@ -573,7 +575,8 @@ VOXELFORGE_API bool VF_SuggestLandingPoint(
|
|||||||
* Complete source-query overload for archetypes whose placement parameters are not part of
|
* Complete source-query overload for archetypes whose placement parameters are not part of
|
||||||
* FStrateGenerationParams/FSlabGenerationParams. The extra structs are read-only inputs only;
|
* FStrateGenerationParams/FSlabGenerationParams. The extra structs are read-only inputs only;
|
||||||
* the function remains pure and SurfaceWorld still returns false for the reason above. The
|
* the function remains pure and SurfaceWorld still returns false for the reason above. The
|
||||||
* returned point is either at the requested XY or within MaxLateralSnap of it.
|
* returned point is either at the requested XY or within MaxLateralSnap of it. VerticalShafts
|
||||||
|
* returns the selected shaft axis so the answer belongs to the connected shaft tree.
|
||||||
*/
|
*/
|
||||||
VOXELFORGE_API bool VF_SuggestLandingPoint(
|
VOXELFORGE_API bool VF_SuggestLandingPoint(
|
||||||
ECaveGeneratorType Archetype,
|
ECaveGeneratorType Archetype,
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ struct VOXELFORGE_API FVoxelStrateMeasureSettings
|
|||||||
int32 SampleStep = 4; // Voxels between samples.
|
int32 SampleStep = 4; // Voxels between samples.
|
||||||
int32 RadiusInVoxels = 256; // Half-extent in X and Y around CenterXY.
|
int32 RadiusInVoxels = 256; // Half-extent in X and Y around CenterXY.
|
||||||
FVector2D CenterXY = FVector2D::ZeroVector; // Actor-space voxel coordinates.
|
FVector2D CenterXY = FVector2D::ZeroVector; // Actor-space voxel coordinates.
|
||||||
|
// When both points are set, replace the square centered on CenterXY with the XY AABB of the
|
||||||
|
// two points expanded by CoverMarginVoxels. This keeps a mouth-to-mouth measurement sized by
|
||||||
|
// the mouths' separation instead of by their distance from the world origin.
|
||||||
|
TOptional<FVector2D> CoverPointA;
|
||||||
|
TOptional<FVector2D> CoverPointB;
|
||||||
|
float CoverMarginVoxels = 48.0f;
|
||||||
int32 MaxCells = 8000000; // Refuse a grid larger than this many cells.
|
int32 MaxCells = 8000000; // Refuse a grid larger than this many cells.
|
||||||
int32 MaxRouteRetries = 16; // Alternate coarse routes to full-resolution-check after the first route.
|
int32 MaxRouteRetries = 16; // Alternate coarse routes to full-resolution-check after the first route.
|
||||||
int32 HeadroomCells = 2; // Air cells above a floor cell required for walkable.
|
int32 HeadroomCells = 2; // Air cells above a floor cell required for walkable.
|
||||||
@@ -54,6 +60,21 @@ struct VOXELFORGE_API FVoxelStrateMetrics
|
|||||||
int32 ResolvedMarginVoxels = 0;
|
int32 ResolvedMarginVoxels = 0;
|
||||||
int32 SampledMinZ = 0;
|
int32 SampledMinZ = 0;
|
||||||
int32 SampledMaxZ = 0;
|
int32 SampledMaxZ = 0;
|
||||||
|
|
||||||
|
// The exact sampled XY box and dimensions. For a fitted window these are the AABB-derived
|
||||||
|
// dimensions; for the legacy window they describe CenterXY +/- RadiusInVoxels.
|
||||||
|
int32 SampledNumX = 0;
|
||||||
|
int32 SampledNumY = 0;
|
||||||
|
int32 SampledNumZ = 0;
|
||||||
|
float SampledMinX = 0.0f;
|
||||||
|
float SampledMaxX = 0.0f;
|
||||||
|
float SampledMinY = 0.0f;
|
||||||
|
float SampledMaxY = 0.0f;
|
||||||
|
|
||||||
|
// Air-component sizes in deterministic flood-fill discovery order. Keeping these facts from
|
||||||
|
// the one required flood fill lets diagnostics classify small non-largest components without
|
||||||
|
// sampling the source a second time.
|
||||||
|
TArray<int64> AirComponentCells;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in new issue
Block a user