105 files changed
+1271
-4882
No files matched your search
+6
-1
@@ -12,8 +12,13 @@
|
|||||||
# The guarded single-entry test harness is source, not generated output.
|
# The guarded single-entry test harness is source, not generated output.
|
||||||
!/Tools/
|
!/Tools/
|
||||||
!/Tools/**
|
!/Tools/**
|
||||||
|
/Tools/**/__pycache__/
|
||||||
|
|
||||||
|
# Closed / historical documents.
|
||||||
|
!/Docs/
|
||||||
|
!/Docs/**
|
||||||
|
|
||||||
!VoxelForge.uplugin
|
!VoxelForge.uplugin
|
||||||
|
|
||||||
# Keep every design / doc markdown at any depth (AUDIT P1 — these were untracked)
|
# Keep every design / doc markdown at any depth
|
||||||
!*.md
|
!*.md
|
||||||
+72
-85
@@ -36,7 +36,7 @@ radius, **0.300** against the Maze corridor radius, **0.469/0.625** against shaf
|
|||||||
**0.234** against the minimum island radius (**0.484** including its 6-voxel blend), and
|
**0.234** against the minimum island radius (**0.484** including its 6-voxel blend), and
|
||||||
**0.625/0.938** against slab column radius for floor/ceiling roughness. SurfaceWorld is a
|
**0.625/0.938** against slab column radius for floor/ceiling roughness. SurfaceWorld is a
|
||||||
heightfield, so its **0.047** ratio is against its 80-voxel relief scale, not a fictitious radial
|
heightfield, so its **0.047** ratio is against its 80-voxel relief scale, not a fictitious radial
|
||||||
feature. FlatPlain and CrystalChamber intentionally share `GetSlabDensity` and the same slab
|
feature. FlatPlain and CrystalChamber intentionally share `BuildSlabStack` and the same slab
|
||||||
parameter block; they cannot acquire separate scale geometry without a generator change.
|
parameter block; they cannot acquire separate scale geometry without a generator change.
|
||||||
|
|
||||||
This scale contract is informed by [Epic's Unreal level-blockout guidance](https://dev.epicgames.com/documentation/en-us/unreal-engine/designer-01-project-setup-and-level-blockout-in-unreal-engine), which recommends a player-sized reference and gives 2-3 m hall / 3-4 m height starting guidance. The
|
This scale contract is informed by [Epic's Unreal level-blockout guidance](https://dev.epicgames.com/documentation/en-us/unreal-engine/designer-01-project-setup-and-level-blockout-in-unreal-engine), which recommends a player-sized reference and gives 2-3 m hall / 3-4 m height starting guidance. The
|
||||||
@@ -62,22 +62,23 @@ are not landing or box-verdict violations.
|
|||||||
|
|
||||||
### 8.1 Archetypes (`ECaveGeneratorType`, VoxelStrateTypes.h)
|
### 8.1 Archetypes (`ECaveGeneratorType`, VoxelStrateTypes.h)
|
||||||
Each archetype has its own param `USTRUCT` (on `UVoxelStrateDefinition`, EditCondition-gated
|
Each archetype has its own param `USTRUCT` (on `UVoxelStrateDefinition`, EditCondition-gated
|
||||||
by `GeneratorType`) and its own density function in `VoxelGenerator.cpp`, dispatched by the
|
by `GeneratorType`) and its own **operator stack** builder in `VoxelDensityOpStack.cpp`, selected per
|
||||||
`switch` in `GetDensityAt`.
|
chunk by `VF_BuildOpStackForChunk` in `GetDensityAt` (the same factory `ClassifyTile` uses). The stack
|
||||||
|
is the only density path; a degenerate (zero-height) strate has none and is open air.
|
||||||
|
|
||||||
| Archetype | Params struct | Density fn | Idea |
|
| Archetype | Params struct | Stack builder | Idea |
|
||||||
|-----------|---------------|------------|------|
|
|-----------|---------------|------------|------|
|
||||||
| TunnelNetwork | `FStrateGenerationParams` | `GetDensityWithParams` | rooms+tunnels (original) |
|
| TunnelNetwork | `FStrateGenerationParams` | `BuildTunnelNetworkStack` (fused evaluator: `GetDensityWithParams`) | rooms+tunnels (original) |
|
||||||
| FlatPlain / CrystalChamber | `FSlabGenerationParams` | `GetSlabDensity` | floor/ceiling void (original) |
|
| FlatPlain / CrystalChamber | `FSlabGenerationParams` | `BuildSlabStack` | floor/ceiling void (original) |
|
||||||
| Maze | `FMazeGenerationParams` | `GetMazeDensity` | origin-directed spanning-tree corridors on a 3D lattice, with capped loop edges and a thread-local per-cell capsule cache |
|
| Maze | `FMazeGenerationParams` | `BuildMazeStack` | origin-directed spanning-tree corridors on a 3D lattice, with capped loop edges and a thread-local per-cell capsule cache |
|
||||||
| SurfaceWorld | `FSurfaceGenerationParams` | `GetSurfaceDensity` | heightfield terrain: domain-warped continents+ridged mtns+detail, a low-freq **relief map** (`M`) that scales mountains/elevation for plains↔highland variety, **F20 heightfield terrain ops** (`Surface|Ops` — all default off ⇒ byte-identical): **Cliff** (slope-gated steepening — where the analytic structural slope > `CliffSlopeThreshold`, push the height away from the local mean by `CliffSharpness` ⇒ gentle slopes become sheer walls/canyon faces that hug real steep ground, gentle areas untouched; 4 structural resamples only when enabled), **Terrace** (relief-gated plateau quantize + `TerraceHardness` soft-round↔crisp-mesa), **LayerLines** (sedimentary sine shelves, slope-expressed) — pure per-column height remaps in the single oracle `ComputeSurfaceTerrainZ` (`SampleSurfaceStructuralZ` = pre-op raw height, re-sampled at an XY offset for Cliff's slope), biome-selected + border-blended for free via each biome's `SurfaceParams` + the height output-lerp; plus **phase-2 Overhang** (the first VOLUMETRIC op — real jutting shelves like a cliff lip): in `SurfaceDensityFromColumn`, for AIR voxels in the window `(TerrainZ, TerrainZ+OverhangHeight]` above a steep slope, the heightfield is re-sampled UPHILL (toward the cliff) by a reach that GROWS with height and unioned in. Low in the window the shift is ~0 (borrows nearby low rock ⇒ stays air over the void); high up it reaches the far cliff (solid) ⇒ a shelf attached to the cliff, tapering out over the void with air underneath. Per-column `OverhangAmp`(=strength·slope-gate) + unit uphill dir `(DirX,DirY)` are resolved once in `ComputeSurfaceColumn` — the gradient sampled at the REACH scale (`OverhangReach`) so a point out over the void can "see" the cliff to know which way is uphill — and cached on `FSurfaceColumn`. It's genuine 3D (per-voxel structural re-eval), so it's gated hard to steep overhang columns; the union only ADDS rock (never removes), capped at `TerrainZ+OverhangHeight`, so `ClassifyTile` forces Mixed only in `(TerrainZ, TerrainZ+OverhangMargin]` (upward-only; margin = max `OverhangHeight`) — a shelf never holes a trivially-skipped tile, and only the thin cliff-edge band of air tiles is woken (NOT far-field like phase-3 spikes/holes). Overhang undersides classify as ground rock by the F17 rule (down-facing but below `TerrainZ`). Known v1 limit: applies at all LODs (Step-agnostic) — may alias far; gate to fine tiles later. Beaches at water line, high sky-cap ceiling. The cap is shapeable terrain in its own right (`ComputeSurfaceCeiling`, `Surface|Sky` params: `CeilingUndulation` broad inverted hills/valleys, `CeilingRidgeStrength` hanging ridgelines, `CeilingRoughness`+freq fine bumps, `CeilingWarp*`) — defaults (strengths 0, freq 0.04) = old flat-ish cap. (`Surface|Macro` params = the cheap precursor to biomes; `ReliefStrength=0` ⇒ old uniform terrain.) **Sky-cap vs ground is a PER-TRIANGLE surface class (F17), not a per-tile verdict**: the mesher classifies each unique vertex on the **worker** — only down-facing verts (`N.Z<-0.1`) pay a memoized `GetSurfaceHeightAt` column query; nearer `CeilSurf` ⇒ sky-cap, nearer `TerrainZ` ⇒ terrain overhang stays ground (a future SurfaceWorld cave roof — down-facing but below `TerrainZ` — also lands ground by the same rule; non-surface strates always ground). Triangles take the majority class of their 3 verts and are packed as **two contiguous index runs** (ground then cap, `FVoxelMeshData::NumCeilingTriangles`; skirts inherit their source triangle's run) → RMC **polygroups 0/1** → one **section per non-empty group** (`ApplyMeshToTile`, material slot = group index): slot 0 = strate `OverrideMaterial`/default (min-corner chunk), slot 1 = `CeilingMaterial` resolved at the tile's **TOP chunk** (mid as gap fallback, then min; fallback: ground material) — a coarse tile is 2^level chunks tall, so its min corner can sit in a lower strate/gap while the cap belongs to the strate above (this was the "far cap = ground material" residue), so the shadowless overhead rock is tinted separately instead of reading flat/bright. Shadow is **per section** via `FRealtimeMeshSectionConfig::bCastsShadow` (NOT the component `SetCastShadow` — RMC's proxy ignores the component flag; this is also why level≥2 far tiles only stopped casting once the section flag was wired): ground casts at level≤1, the cap section never casts, so the rock ceiling never shadows the terrain below it. History: v1 was a game-thread centre height-oracle (misclassified coarse far tiles → terrain material on the cap underside); v2 a whole-tile worker normal VOTE — which painted **mixed coarse tiles** (one far tile spanning terrain AND cap) entirely with the winner's material, and put sky material under terrain overhangs. The per-triangle class fixes both and is the identity channel caves/F8 will reuse. |
|
| SurfaceWorld | `FSurfaceGenerationParams` | `BuildSurfaceStack` | heightfield terrain: domain-warped continents+ridged mtns+detail, a low-freq **relief map** (`M`) that scales mountains/elevation for plains↔highland variety, **F20 heightfield terrain ops** (`Surface|Ops` — all default off ⇒ byte-identical): **Cliff** (slope-gated steepening — where the analytic structural slope > `CliffSlopeThreshold`, push the height away from the local mean by `CliffSharpness` ⇒ gentle slopes become sheer walls/canyon faces that hug real steep ground, gentle areas untouched; 4 structural resamples only when enabled), **Terrace** (relief-gated plateau quantize + `TerraceHardness` soft-round↔crisp-mesa), **LayerLines** (sedimentary sine shelves, slope-expressed) — pure per-column height remaps in the single oracle `ComputeSurfaceTerrainZ` (`SampleSurfaceStructuralZ` = pre-op raw height, re-sampled at an XY offset for Cliff's slope), biome-selected + border-blended for free via each biome's `SurfaceParams` + the height output-lerp; plus **phase-2 Overhang** (the first VOLUMETRIC op — real jutting shelves like a cliff lip): in `FOverhangShelfMod`, for AIR voxels in the window `(TerrainZ, TerrainZ+OverhangHeight]` above a steep slope, the heightfield is re-sampled UPHILL (toward the cliff) by a reach that GROWS with height and unioned in. Low in the window the shift is ~0 (borrows nearby low rock ⇒ stays air over the void); high up it reaches the far cliff (solid) ⇒ a shelf attached to the cliff, tapering out over the void with air underneath. Per-column `OverhangAmp`(=strength·slope-gate) + unit uphill dir `(DirX,DirY)` are resolved once in `ComputeSurfaceColumn` — the gradient sampled at the REACH scale (`OverhangReach`) so a point out over the void can "see" the cliff to know which way is uphill — and cached on `FSurfaceColumn`. It's genuine 3D (per-voxel structural re-eval), so it's gated hard to steep overhang columns; the union only ADDS rock (never removes), capped at `TerrainZ+OverhangHeight`, so `ClassifyTile` forces Mixed only in `(TerrainZ, TerrainZ+OverhangMargin]` (upward-only; margin = max `OverhangHeight`) — a shelf never holes a trivially-skipped tile, and only the thin cliff-edge band of air tiles is woken (NOT far-field like phase-3 spikes/holes). Overhang undersides classify as ground rock by the F17 rule (down-facing but below `TerrainZ`). Known v1 limit: applies at all LODs (Step-agnostic) — may alias far; gate to fine tiles later. Beaches at water line, high sky-cap ceiling. The cap is shapeable terrain in its own right (`ComputeSurfaceCeiling`, `Surface|Sky` params: `CeilingUndulation` broad inverted hills/valleys, `CeilingRidgeStrength` hanging ridgelines, `CeilingRoughness`+freq fine bumps, `CeilingWarp*`) — defaults (strengths 0, freq 0.04) = old flat-ish cap. (`Surface|Macro` params = the cheap precursor to biomes; `ReliefStrength=0` ⇒ old uniform terrain.) **Sky-cap vs ground is a PER-TRIANGLE surface class (F17), not a per-tile verdict**: the mesher classifies each unique vertex on the **worker** — only down-facing verts (`N.Z<-0.1`) pay a memoized `GetSurfaceHeightAt` column query; nearer `CeilSurf` ⇒ sky-cap, nearer `TerrainZ` ⇒ terrain overhang stays ground (a future SurfaceWorld cave roof — down-facing but below `TerrainZ` — also lands ground by the same rule; non-surface strates always ground). Triangles take the majority class of their 3 verts and are packed as **two contiguous index runs** (ground then cap, `FVoxelMeshData::NumCeilingTriangles`; skirts inherit their source triangle's run) → RMC **polygroups 0/1** → one **section per non-empty group** (`ApplyMeshToTile`, material slot = group index): slot 0 = strate `OverrideMaterial`/default (min-corner chunk), slot 1 = `CeilingMaterial` resolved at the tile's **TOP chunk** (mid as gap fallback, then min; fallback: ground material) — a coarse tile is 2^level chunks tall, so its min corner can sit in a lower strate/gap while the cap belongs to the strate above (this was the "far cap = ground material" residue), so the shadowless overhead rock is tinted separately instead of reading flat/bright. Shadow is **per section** via `FRealtimeMeshSectionConfig::bCastsShadow` (NOT the component `SetCastShadow` — RMC's proxy ignores the component flag; this is also why level≥2 far tiles only stopped casting once the section flag was wired): ground casts at level≤1, the cap section never casts, so the rock ceiling never shadows the terrain below it. History: v1 was a game-thread centre height-oracle (misclassified coarse far tiles → terrain material on the cap underside); v2 a whole-tile worker normal VOTE — which painted **mixed coarse tiles** (one far tile spanning terrain AND cap) entirely with the winner's material, and put sky material under terrain overhangs. The per-triangle class fixes both and is the identity channel caves/F8 will reuse. |
|
||||||
| VerticalShafts | `FVerticalShaftParams` | `GetVerticalShaftDensity` | full-height shafts + horizontal connectors + partial ledges |
|
| VerticalShafts | `FVerticalShaftParams` | `BuildVerticalShaftStack` | full-height shafts + horizontal connectors + partial ledges |
|
||||||
| FloatingIslands | `FFloatingIslandParams` | `GetFloatingIslandDensity` | asymmetric islands: flat land top + underside tapering to a point, lobed (domain-warped) outline, in an open void |
|
| FloatingIslands | `FFloatingIslandParams` | `BuildFloatingIslandStack` | asymmetric islands: flat land top + underside tapering to a point, lobed (domain-warped) outline, in an open void |
|
||||||
| Underwater | `FStrateGenerationParams` + water | (reuses `GetDensityWithParams`) | tunnel rock + high water table |
|
| Underwater | `FStrateGenerationParams` + water | (reuses `BuildTunnelNetworkStack`) | tunnel rock + high water table |
|
||||||
|
|
||||||
All density fns share the convention: internal **positive=solid**, apply origin spine →
|
All stacks share the convention: internal **positive=solid**, and `AppendStructuralPost` ends every
|
||||||
vertical boundary seal → inter-strate passage tube + landing/floor → XY edge seal, then `return -Density` (MC:
|
stack with origin spine → vertical boundary seal → inter-strate passage tube + landing/floor → XY
|
||||||
negative=solid). The final MC-facing edge pass in `GetDensityAt` is repeated after disturbances so
|
edge seal; `EvalMC` negates once (MC: negative=solid). The final MC-facing edge pass in `GetDensityAt` is repeated after disturbances so
|
||||||
the global rim cannot be reopened by a post-process; the diff layer remains the explicit player
|
the global rim cannot be reopened by a post-process; the diff layer remains the explicit player
|
||||||
override.
|
override.
|
||||||
StrateManager provides params per chunk via `GetMaze/Surface/VerticalShaft/FloatingIslandParamsForChunk`
|
StrateManager provides params per chunk via `GetMaze/Surface/VerticalShaft/FloatingIslandParamsForChunk`
|
||||||
@@ -127,12 +128,9 @@ threshold** before the law gate. No corridor or threshold tuning was added. The
|
|||||||
gate is therefore retained as a design blocker; see `COMPOSER-NOTES.md §3.2b` and the validation log
|
gate is therefore retained as a design blocker; see `COMPOSER-NOTES.md §3.2b` and the validation log
|
||||||
in §10.
|
in §10.
|
||||||
|
|
||||||
⚠️ **The `switch` above is no longer the only density path.** All 8 archetypes also exist as
|
The operator-stack design lives in `Docs/archive/OPSTACK-PLAN.md` /
|
||||||
**operator stacks**, selected per strate by
|
`Docs/archive/OPSTACK-DECOMPOSITION.md`, the symbol index in `CODEMAP §3.2d` — not repeated here.
|
||||||
`bUseOperatorStack` and evaluated instead of the `switch`; each is bit-identical to the function in
|
What matters for *this* document: the archetype table describes what the world IS.
|
||||||
its row. The design lives in `OPSTACK-PLAN.md` / `OPSTACK-DECOMPOSITION.md`, the symbol index in
|
|
||||||
`CODEMAP §3.2d` — not repeated here. What matters for *this* document: the archetype table describes
|
|
||||||
what the world IS, and both paths compute it.
|
|
||||||
|
|
||||||
### 8.2 (0,0) spine & hybrid connections
|
### 8.2 (0,0) spine & hybrid connections
|
||||||
- `ApplyOriginSpine` (VoxelGenerator.cpp, static helper) builds a finite, rounded landing room with
|
- `ApplyOriginSpine` (VoxelGenerator.cpp, static helper) builds a finite, rounded landing room with
|
||||||
@@ -230,8 +228,8 @@ they are never needed for the connectivity proof.
|
|||||||
|
|
||||||
The evaluation window is exactly the lower nodes in `{-1,0}³`, an 8-node / 2×2×2 local halo. The
|
The evaluation window is exactly the lower nodes in `{-1,0}³`, an 8-node / 2×2×2 local halo. The
|
||||||
edge predicate checks the adjacent `+1` endpoint's parent locally, so it does not need a wider
|
edge predicate checks the adjacent `+1` endpoint's parent locally, so it does not need a wider
|
||||||
collect region and cannot disagree at a chunk boundary. Both `GetMazeDensity` and
|
collect region and cannot disagree at a chunk boundary. `MakeLatticeCorridorSource` rebuilds those
|
||||||
`MakeLatticeCorridorSource` rebuild those decisions in their thread-local per-cell cache; the
|
decisions in its thread-local per-cell cache; the
|
||||||
voxel loop evaluates only cached capsule SDFs. This is deliberately independent of
|
voxel loop evaluates only cached capsule SDFs. This is deliberately independent of
|
||||||
`WorldRadiusVoxels` (which remains `0`) and the still-gated lateral-region system.
|
`WorldRadiusVoxels` (which remains `0`) and the still-gated lateral-region system.
|
||||||
|
|
||||||
@@ -291,9 +289,11 @@ when close (separate HISMs → crossing the near boundary never touches the far
|
|||||||
(per grid, sharing ONE `MaxConcurrentDecorationTasks` budget — each throttles against the other's in-flight count):
|
(per grid, sharing ONE `MaxConcurrentDecorationTasks` budget — each throttles against the other's in-flight count):
|
||||||
snapshot the update's decoration palette (built once on the GAME thread — biome, see below) then fire an async `UE::Tasks` march
|
snapshot the update's decoration palette (built once on the GAME thread — biome, see below) then fire an async `UE::Tasks` march
|
||||||
(`BuildCellSpawns`, `BackgroundNormal`, capped at `MaxConcurrentDecorationTasks` in flight via `InFlightCells`).
|
(`BuildCellSpawns`, `BackgroundNormal`, capped at `MaxConcurrentDecorationTasks` in flight via `InFlightCells`).
|
||||||
**(3)** `ProcessDecoResults`: drain finished tasks' results (`Mpsc` queue → `ReadyResults`), epoch-guarded
|
**(3)** `ProcessDecoResults`: drain finished cell marches (`DecoResults`, `Mpsc`) and fold each into its
|
||||||
(`DecoEpoch`, bumped on clear/strate-change so stale in-flight results are discarded) + range-checked, and
|
grid's region build (`MergeCellResult`; a result whose build is gone or whose `BuildId` no longer matches —
|
||||||
**apply (spawn) budgeted** (`MaxDecorationCellsPerFrame` — the only game-thread cost, SpawnActor/AddInstance).
|
region cleared + re-marched — is discarded), then **apply completed regions budgeted**
|
||||||
|
(`MaxDecorationCellsPerFrame` regions/frame via `ApplyRegion` — the only game-thread cost; a region that left
|
||||||
|
range while it marched is dropped unbuilt).
|
||||||
`BuildCellSpawns` (worker) finds each column's surface point(s) and rolls the entries there (shared
|
`BuildCellSpawns` (worker) finds each column's surface point(s) and rolls the entries there (shared
|
||||||
`PlaceAtCrossing`). Candidate columns are **snapped to INTEGER voxel XY** (integer jitter) so the generator's
|
`PlaceAtCrossing`). Candidate columns are **snapped to INTEGER voxel XY** (integer jitter) so the generator's
|
||||||
surface-column cache (T1.a, §8.10) applies — FRACTIONAL XY bypasses it and recomputes the noise-heavy
|
surface-column cache (T1.a, §8.10) applies — FRACTIONAL XY bypasses it and recomputes the noise-heavy
|
||||||
@@ -321,7 +321,7 @@ result's `Entries` snapshot. `DecorationMaxCrossingsPerColumn` caps cave columns
|
|||||||
task count before UObject teardown (tasks read the Generator); `BeginDestroy` is the backstop. **Determinism:**
|
task count before UObject teardown (tasks read the Generator); `BeginDestroy` is the backstop. **Determinism:**
|
||||||
pure hash of (cell, column, crossing, entry, seed) + the density surface snap. **Decorations exist ONLY in
|
pure hash of (cell, column, crossing, entry, seed) + the density surface snap. **Decorations exist ONLY in
|
||||||
the player's current strate** (march is strate-bounded) → a strate change wipes + rebuilds them, and there
|
the player's current strate** (march is strate-bounded) → a strate change wipes + rebuilds them, and there
|
||||||
is **no cross-strate light bleed to cull** (the old `SetActiveStrate` light-culling pass is SUBSUMED — gone).
|
is **no cross-strate light bleed to cull**.
|
||||||
**Render paths:** `ActorClass` → real actors (lights/logic, pricey game-thread spawn); `InstancedMesh` → HISM
|
**Render paths:** `ActorClass` → real actors (lights/logic, pricey game-thread spawn); `InstancedMesh` → HISM
|
||||||
(no tick/actor/collision, emissive glows far), per-cell-per-entry. **Per-entry HISM tuning for dense groundcover**
|
(no tick/actor/collision, emissive glows far), per-cell-per-entry. **Per-entry HISM tuning for dense groundcover**
|
||||||
(`FStrateDecoration`, only the InstancedMesh path): `CullDistance` (cm; 0 = no cull — the lever that makes dense
|
(`FStrateDecoration`, only the InstancedMesh path): `CullDistance` (cm; 0 = no cull — the lever that makes dense
|
||||||
@@ -332,8 +332,8 @@ grass affordable: placed thickly, drawn only near → GPU cost bounded by area-w
|
|||||||
`MinSlopeAngle` (lower companion to Max — band a prop onto a tilt range, e.g. 30..70 = slopes only),
|
`MinSlopeAngle` (lower companion to Max — band a prop onto a tilt range, e.g. 30..70 = slopes only),
|
||||||
`bWallExcludeOverhangs` (wall-only-upright: drop normals with N.Z < 0 so downward overhangs don't take wall props).
|
`bWallExcludeOverhangs` (wall-only-upright: drop normals with N.Z < 0 so downward overhangs don't take wall props).
|
||||||
**Shared vocabulary (2026-07-06):** these gates + spawn/transform/render fields now live on `FPlacementProfile`
|
**Shared vocabulary (2026-07-06):** these gates + spawn/transform/render fields now live on `FPlacementProfile`
|
||||||
(embedded as `Profile` on `FStrateDecoration`, `FStrateLandmark`, and the coming `FStrateSetPiece`), so all three
|
(embedded as `Profile` on `FStrateDecoration` and `FStrateLandmark`), so both scatter primitives are
|
||||||
scatter primitives are authored identically. Rotation unified to `RotationOffset` (fixed) + `RandomRotation`
|
authored identically. Rotation unified to `RotationOffset` (fixed) + `RandomRotation`
|
||||||
(per-axis hash-random) — decoration's ctor defaults `RandomRotation.Yaw = 360` (full random heading, replacing the
|
(per-axis hash-random) — decoration's ctor defaults `RandomRotation.Yaw = 360` (full random heading, replacing the
|
||||||
old `bRandomYaw`/`MinYaw`/`MaxYaw`; banded yaw = offset + a smaller random range). Distribution is unchanged; the
|
old `bRandomYaw`/`MinYaw`/`MaxYaw`; banded yaw = offset + a smaller random range). Distribution is unchanged; the
|
||||||
exact per-instance yaw values reshuffle once (different hash mix).
|
exact per-instance yaw values reshuffle once (different hash mix).
|
||||||
@@ -345,7 +345,7 @@ SampleMoisture / SampleBiomeAt) on demand, so it's deterministic + worker-safe (
|
|||||||
strate's already-resolved `FBiomeContext`, so no re-resolve). Opt-in per entry (empty list = zero cost);
|
strate's already-resolved `FBiomeContext`, so no re-resolve). Opt-in per entry (empty list = zero cost);
|
||||||
wired into both the deco worker (`BuildCellSpawns`) and landmark placement (`SpawnLandmarkInstance`). This
|
wired into both the deco worker (`BuildCellSpawns`) and landmark placement (`SpawnLandmarkInstance`). This
|
||||||
is the shared core of the coming quest `FindFeature` locator (same predicate, run as an outward search) and
|
is the shared core of the coming quest `FindFeature` locator (same predicate, run as an outward search) and
|
||||||
the anchor gate for `FStrateSetPiece`. The BP bridge `AVoxelWorld::GetVoxelSurfaceHeightAt` exposes the
|
the landmark anchor gate. The BP bridge `AVoxelWorld::GetVoxelSurfaceHeightAt` exposes the
|
||||||
trace-free deterministic ground/ceiling height so authored ruin/set-piece Blueprints self-arrange on the
|
trace-free deterministic ground/ceiling height so authored ruin/set-piece Blueprints self-arrange on the
|
||||||
real surface before it meshes. Next types (water-edge band, relief-peak local-max, slope) are additive.
|
real surface before it meshes. Next types (water-edge band, relief-peak local-max, slope) are additive.
|
||||||
**F7 COMPANIONS (relational decoration, `FDecoCompanion`):** each `FStrateDecoration` may list `Companions`
|
**F7 COMPANIONS (relational decoration, `FDecoCompanion`):** each `FStrateDecoration` may list `Companions`
|
||||||
@@ -397,12 +397,12 @@ Static-mobility HISM re-caches its proxy — fine for player-paced digging.) Not
|
|||||||
grass on a freshly-exposed ledge / regrowth after fill-back — both self-correct on the next natural re-stream. **Freeze note:** a huge set-piece mesh hitches on register (game-thread proxy/distance-field build —
|
grass on a freshly-exposed ledge / regrowth after fill-back — both self-correct on the next natural re-stream. **Freeze note:** a huge set-piece mesh hitches on register (game-thread proxy/distance-field build —
|
||||||
NOT async-fixable; the spawn is game-thread by engine rule; the asset is a hard ref so already resident) →
|
NOT async-fixable; the spawn is game-thread by engine rule; the asset is a hard ref so already resident) →
|
||||||
mitigate ASSET-side (Nanite on the mesh, bake distance fields), optionally budget spawns across frames.
|
mitigate ASSET-side (Nanite on the mesh, bake distance fields), optionally budget spawns across frames.
|
||||||
`ApplyDecoResult` buckets spawns per entry and builds each HISM with ONE batched `AddInstances`
|
`ApplyRegion` buckets a region's spawns per mesh (all its cells merged) and builds each HISM with ONE batched
|
||||||
(single cluster-tree build, set cull/shadow BEFORE `RegisterComponent`) — the game-thread hitch-killer for dense cells.
|
`AddInstances` (single cluster-tree build, set cull/shadow BEFORE `RegisterComponent`) — the game-thread
|
||||||
|
hitch-killer for dense cells.
|
||||||
**Per-entry tier (`StreamTier`, default Far)** picks NearGrid or FarGrid; radius + column spacing are PER-GRID
|
**Per-entry tier (`StreamTier`, default Far)** picks NearGrid or FarGrid; radius + column spacing are PER-GRID
|
||||||
settings, never per-entry (a per-entry radius would re-introduce the in-place re-stream flicker — see the two-grid
|
settings, never per-entry (a per-entry radius would re-introduce the in-place re-stream flicker — see the two-grid
|
||||||
rationale above). This REPLACES the vestigial `MaxLODLevel`; the dead `DecorationActorRadiusChunks` setting is
|
rationale above). `CullDistance` still bounds GPU draw on top (orthogonal to which grid
|
||||||
repurposed as `DecorationNearRadiusChunks`. `CullDistance` still bounds GPU draw on top (orthogonal to which grid
|
|
||||||
streams the entry). **No LOD area-density compensation** (placement is per real
|
streams the entry). **No LOD area-density compensation** (placement is per real
|
||||||
surface point, density-stable with distance). **SpawnDensity semantics CHANGED** vs the old vertex scatter: it
|
surface point, density-stable with distance). **SpawnDensity semantics CHANGED** vs the old vertex scatter: it
|
||||||
rolls per column surface-point (not per mesh vertex) → expect a one-time density re-tune. **Settings
|
rolls per column surface-point (not per mesh vertex) → expect a one-time density re-tune. **Settings
|
||||||
@@ -436,22 +436,23 @@ null entry = "evaluated, nothing placed" so it isn't retried). Deterministic (ha
|
|||||||
Strate-bounded (wiped on strate change, like decorations). This is the real home for "rare prop at all
|
Strate-bounded (wiped on strate change, like decorations). This is the real home for "rare prop at all
|
||||||
distances" — the job the decoration FarGrid could approximate at moderate range but not at extreme radius.
|
distances" — the job the decoration FarGrid could approximate at moderate range but not at extreme radius.
|
||||||
|
|
||||||
**(B) WATER — tile-driven, level-0 only (continuous plane, never pops).** `PopulateTileWater(tile)` in
|
**(B) WATER — ONE strate-global ocean plane that follows the player (`UpdateWater`, from `Tick`).** A single
|
||||||
`ApplyMeshToTile` (level-0 tiles), `ClearTileWater(tile)` in `UnloadTile`. One scaled engine plane
|
scaled engine plane (`/Engine/BasicShapes/Plane`, no collision, no shadow) snapped to a coarse cell around the
|
||||||
(`/Engine/BasicShapes/Plane`) per water-surface chunk (per-chunk-Z plane logic assumes a single chunk's
|
player, so it only repositions when the player crosses a cell; terrain pokes through it, so it reads as water at
|
||||||
vertical span — hence level-0 only), keyed `TMap<FIntVector, UStaticMeshComponent*>` (reflected UPROPERTY).
|
every LOD and to the horizon with no per-tile gaps (one draw). Water Z: `bHasWater` + `WaterLevelRelative` →
|
||||||
Water Z: `bHasWater` + `WaterLevelRelative` → `StrateManager::GetWaterLevelWorldZForChunk`. Biome
|
`StrateManager::GetWaterLevelWorldZForChunk` (no water in the current strate ⇒ plane hidden). Material:
|
||||||
`WaterMaterial` overrides `UVoxelStrateDefinition::WaterMaterial` (level stays strate-global).
|
`UVoxelStrateDefinition::WaterMaterial`. ⚠️ OPEN: `UVoxelBiomeDefinition::WaterMaterial` (the biome
|
||||||
|
content profile's water override, §8.14) is not read by the single-plane path — owner to decide whether
|
||||||
|
the biome override should come back (REVIEW_FINDINGS, *Owner decisions*).
|
||||||
|
|
||||||
`ClearAll`/`SetSeed` on `ChangeSeed`/regenerate clears both subsystems (decorations re-stream on the next
|
`ClearAll`/`SetSeed` on `ChangeSeed`/regenerate clears both subsystems (decorations re-stream on the next
|
||||||
Tick via the INT_MIN sentinels). **Per-biome content (§8.14):** decorations resolve the dominant biome **PER COLUMN** on the worker
|
Tick via the INT_MIN sentinels). **Per-biome content (§8.14):** decorations resolve the dominant biome **PER COLUMN** on the worker
|
||||||
(`ResolveBiomeSampleAt` via the strate's `FBiomeContext`, box-cached → one rebuild per chunk footprint). The
|
(`ResolveBiomeSampleAt` via the strate's `FBiomeContext`, box-cached → one rebuild per chunk footprint). The
|
||||||
update builds ONE flat decoration palette (every biome's list concatenated; `CurrentEntryBiome[i]` tags entry
|
update builds ONE flat decoration palette per grid (every biome's list concatenated; `EntryBiome[i]` tags entry
|
||||||
`i` with its context-biome index, -1 = strate fallback), and `PlaceAtCrossing` rolls only the entries the
|
`i` with its context-biome index, -1 = strate fallback), and `PlaceAtCrossing` rolls only the entries the
|
||||||
column's biome owns. This replaced the old per-CELL `GetDominantBiomeAt` (one biome for a whole 8 m cell →
|
column's biome owns. Borders follow the warped-Voronoi field at column resolution, no straight lines (a
|
||||||
axis-aligned border snapping); borders now follow the warped-Voronoi field at column resolution, no straight
|
per-CELL biome would snap borders to the 8 m cell grid). `Initialize` also takes `UVoxelSettings*` (for the
|
||||||
lines. Water still uses the chunk-centre `GetDominantBiomeAt` for its material. `Initialize` now also takes
|
grid tunables).
|
||||||
`UVoxelSettings*` (for the grid tunables). `ContentMaxLevel` is now legacy/dead for decorations.
|
|
||||||
|
|
||||||
### 8.6 Atmosphere — `VoxelAtmosphereManager.h/.cpp` (NEW)
|
### 8.6 Atmosphere — `VoxelAtmosphereManager.h/.cpp` (NEW)
|
||||||
`UVoxelAtmosphereManager` (owned by `AVoxelWorld`, gated by `bManageAtmosphere`).
|
`UVoxelAtmosphereManager` (owned by `AVoxelWorld`, gated by `bManageAtmosphere`).
|
||||||
@@ -521,7 +522,7 @@ flat floor. **There is no connector to the origin landing room at `(0,0)`.** The
|
|||||||
those radial roads after a playtest (`d97373c`, 2026-09-08: they cut straight through everything);
|
those radial roads after a playtest (`d97373c`, 2026-09-08: they cut straight through everything);
|
||||||
room joins are flattened to walkable height instead. A source-fit landing therefore stays local to
|
room joins are flattened to walkable height instead. A source-fit landing therefore stays local to
|
||||||
the room it was fitted into (`GeneratePassages`, the `VF_BuildPassageLanding` calls).
|
the room it was fitted into (`GeneratePassages`, the `VF_BuildPassageLanding` calls).
|
||||||
⚠️ Open gap (Sol's code review, `DESIGN-SOL-2026-09-15-CODE.md` §1.3/§2.4): when the footing query
|
⚠️ Open gap (Sol's code review, `Docs/archive/DESIGN-SOL-2026-09-15-CODE.md` §1.3/§2.4): when the footing query
|
||||||
declines, the landing falls back to the historical random reach, and nothing proves that pose joins
|
declines, the landing falls back to the historical random reach, and nothing proves that pose joins
|
||||||
the walk network. The canonical capability gate covers one scenario, not every seed or mix.
|
the walk network. The canonical capability gate covers one scenario, not every seed or mix.
|
||||||
Proposed fix, not built: retry deterministically at other candidate spots instead of falling back.
|
Proposed fix, not built: retry deterministically at other candidate spots instead of falling back.
|
||||||
@@ -554,8 +555,8 @@ The landing is part of the structural post, in the fixed order
|
|||||||
after the passage carve and after MC-space disturbances as a support backstop, still before the
|
after the passage carve and after MC-space disturbances as a support backstop, still before the
|
||||||
final XY edge seal. Room/floor Z is clamped inside the vertical seal; the XY edge seal is last and
|
final XY edge seal. Room/floor Z is clamped inside the vertical seal; the XY edge seal is last and
|
||||||
wins over every landing or tube near the rim. Thus no landing can breach a strate boundary or the
|
wins over every landing or tube near the rim. Thus no landing can breach a strate boundary or the
|
||||||
world edge. The op-stack `FPassageCarveOp` calls the same manager operation as the legacy path,
|
world edge. The op-stack `FPassageCarveOp` calls the manager's `ApplyPassageModifier`, including
|
||||||
including the floor, and `ClassifyTile` kills both uniform hypotheses around the bidirectional
|
the floor, and `ClassifyTile` kills both uniform hypotheses around the bidirectional
|
||||||
landing floor (`Both` in the stack), so an air proof cannot delete support.
|
landing floor (`Both` in the stack), so an air proof cannot delete support.
|
||||||
|
|
||||||
The query remains pure — it does not construct an operator stack, touch a cache, or call back into
|
The query remains pure — it does not construct an operator stack, touch a cache, or call back into
|
||||||
@@ -621,8 +622,7 @@ driven by `EditorBrush*` props.
|
|||||||
inner 3×3 shafts and their capsules for the per-voxel loop, while spatial culling selects cached
|
inner 3×3 shafts and their capsules for the per-voxel loop, while spatial culling selects cached
|
||||||
connectors. The complete ±2 parent and ±3 fallback windows plus connector reach are contained
|
connectors. The complete ±2 parent and ±3 fallback windows plus connector reach are contained
|
||||||
by that halo, so no parent decision depends on the evaluation cell. No cell or pair hash, widened
|
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
|
neighbourhood scan, or tree construction may move into the per-voxel loop (`FShaftFieldSource`).
|
||||||
source follows the same cache contract.
|
|
||||||
- **Maze field cache**: parent/loop decisions are rebuilt only when the thread-local cell, seed, or
|
- **Maze field cache**: parent/loop decisions are rebuilt only when the thread-local cell, seed, or
|
||||||
loop parameters change. The rebuild evaluates the 24 canonical lower-node/axis edges in the
|
loop parameters change. The rebuild evaluates the 24 canonical lower-node/axis edges in the
|
||||||
local `{-1,0}³` window and emits at most 24 capsules; the per-voxel loop performs no hash,
|
local `{-1,0}³` window and emits at most 24 capsules; the per-voxel loop performs no hash,
|
||||||
@@ -684,13 +684,13 @@ driven by `EditorBrush*` props.
|
|||||||
position → seamless across chunk borders (both sides use identical pure samples). NO per-vertex
|
position → seamless across chunk borders (both sides use identical pure samples). NO per-vertex
|
||||||
`GetDensityAt` (was ~6/vertex, often as costly as the whole grid). `ComputeGradientNormal` is now
|
`GetDensityAt` (was ~6/vertex, often as costly as the whole grid). `ComputeGradientNormal` is now
|
||||||
unused. Only NORMALS changed vs the old path; geometry is identical.
|
unused. Only NORMALS changed vs the old path; geometry is identical.
|
||||||
- **Surface column cache (T1.a)** (`FSurfaceColumnCache` = LRU of `FSurfaceColumnBox`, `GetDensityAt`
|
- **Surface column cache (T1.a)** (density path: the surface stack's `FSurfaceColumnSource` memo;
|
||||||
SurfaceWorld branch): the heightfield + sky-cap + biome blend are a PURE function of (XY, seed,
|
`ClassifyTile`: `FSurfaceColumnCache` = LRU of `FSurfaceColumnBox`): the heightfield + sky-cap + biome blend are a PURE function of (XY, seed,
|
||||||
strate) — **ZERO Z dependence** (climate/Voronoi are pure-XY; surface params are per-strate constant
|
strate) — **ZERO Z dependence** (climate/Voronoi are pure-XY; surface params are per-strate constant
|
||||||
under Hard transitions) — yet sampled ~33× per column (once per Z grid-point). Cached per integer XY
|
under Hard transitions) — yet sampled ~33× per column (once per Z grid-point). Cached per integer XY
|
||||||
(box-valid, like the SDF cache) and reused down the column. **Keyed by (XY box, StrateKey, Seed), NOT
|
(box-valid, like the SDF cache) and reused down the column. **Keyed by XY box + strate identity + seed
|
||||||
ChunkZ** (`StrateKey = round(StrateBottomWorldZ)`, taken from the params so it can't disagree with
|
(plus layout version / params fingerprint), NOT ChunkZ** (the strate identity is derived from
|
||||||
them) and held as a small **LRU of 6 boxes** so the WHOLE vertical view-distance stack — and XY
|
`StrateBottomWorldZ`, taken from the params so it can't disagree with them) and held as a small **LRU of 6 boxes** so the WHOLE vertical view-distance stack — and XY
|
||||||
neighbours the scheduler interleaves — share one another's heavy column noise instead of each
|
neighbours the scheduler interleaves — share one another's heavy column noise instead of each
|
||||||
recomputing it ~once per vertical chunk (this was the dominant `GenerateMesh` cost: the same 2D
|
recomputing it ~once per vertical chunk (this was the dominant `GenerateMesh` cost: the same 2D
|
||||||
heightfield recomputed per altitude). It also makes pure-air / pure-solid chunks cheap (they hit the
|
heightfield recomputed per altitude). It also makes pure-air / pure-solid chunks cheap (they hit the
|
||||||
@@ -764,28 +764,14 @@ driven by `EditorBrush*` props.
|
|||||||
inside a strate band), then the OUTERMOST shell keeps generating tiles outward until it covers
|
inside a strate band), then the OUTERMOST shell keeps generating tiles outward until it covers
|
||||||
the requested distance (`VF_OuterShell`, shared by `BuildDesiredTiles` and `IsTileInClipRange`
|
the requested distance (`VF_OuterShell`, shared by `BuildDesiredTiles` and `IsTileInClipRange`
|
||||||
so the cull sees the same horizon; the ring's dz sweep is pre-clamped to the vertical strate
|
so the cull sees the same horizon; the ring's dz sweep is pre-clamped to the vertical strate
|
||||||
band). As MC tiles the ring cost grows with (distance/2^MaxClipLevel)² — which is why the ring
|
band). The ring is made of level-MaxClipLevel MC tiles, so its cost grows with
|
||||||
defaults to **F18 SHEETS** (`bFarSheetRing`): in an open strate the far field is exactly two
|
(distance/2^MaxClipLevel)². The far "sheet" ring (heightfield tiles past `MaxClipLevel`) was
|
||||||
heightfields (TerrainZ + CeilSurf, per-column oracle), so the ring streams tiles at level
|
dropped by the owner's decision; it no longer exists in the selector or the settings.
|
||||||
`MaxClipLevel + FarSheetSpanLevels` (one sheet = 2^span MC footprints per axis → 4-16× fewer
|
(A "step cap" variant — raising CoarseTileCells per level so far levels keep a fine Step — was
|
||||||
components) meshed by `GenerateSheetMesh` as two displaced grids — ground polygroup 0 / cap
|
tried and rejected 2026-07-06.)
|
||||||
polygroup 1, tags true by construction, same materials/UVs/F6 masks, ~3-6× cheaper gen per
|
|
||||||
area than band-cut MC. **XY hole**: a partially-covered sheet renders its whole footprint, so
|
|
||||||
the MC-covered box around the player (level-MaxClipLevel box, shrunk 1 tile for a seam-overlap
|
|
||||||
ring) is CUT out of the sheet at cell granularity (`SheetHole*Vox`, passed to
|
|
||||||
`GenerateSheetMesh`); when the player crosses a MaxClipLevel tile the hole moves and the
|
|
||||||
overlapping sheets re-queue via `BandRemeshQueue` — the shrink means the newly-cut area's MC
|
|
||||||
tiles were already desired one crossing earlier (loaded before uncovered). Sheet tiles take
|
|
||||||
the strate from the BAND (mid chunk): band unarmed
|
|
||||||
(inter-strate gap) ⇒ empty ring until landing; non-open strates ⇒ empty sheets (their far
|
|
||||||
ring was enclosed rock anyway); carved features (passages/spine/chasms/diff) don't show at
|
|
||||||
sheet distance — the MC shells keep them near. (A "step cap" variant — raising CoarseTileCells
|
|
||||||
per level so far levels keep a fine Step — was tried and rejected 2026-07-06.)
|
|
||||||
Trade-offs accepted: other strates simply don't render at far LOD (they're sealed/enclosed —
|
Trade-offs accepted: other strates simply don't render at far LOD (they're sealed/enclosed —
|
||||||
invisible except through passage mouths, which read as dark holes); passage tubes crossing the gap
|
invisible except through passage mouths, which read as dark holes); passage tubes crossing the gap
|
||||||
are cut at coarse levels only (near levels mesh full).
|
are cut at coarse levels only (near levels mesh full).
|
||||||
`GetLODForChunk` / `LODToStep` / `IsChunkInRange` / `GetStrateChunkZBounds` and the
|
|
||||||
ViewDistance/LOD/strate-Z/ceiling settings are now DEAD/unused (left in place).
|
|
||||||
- **SKIRTS — LOD-seam crack filler** (`GenerateMesh`, after the cell loop; `VoxelSettings::bGenerateSkirts`
|
- **SKIRTS — LOD-seam crack filler** (`GenerateMesh`, after the cell loop; `VoxelSettings::bGenerateSkirts`
|
||||||
+ `SkirtCells`, wired onto the mesher at setup). Neighbouring shells mesh at different resolutions so
|
+ `SkirtCells`, wired onto the mesher at setup). Neighbouring shells mesh at different resolutions so
|
||||||
their iso-surfaces don't meet along the shared face → a thin see-through crack. After meshing, every
|
their iso-surfaces don't meet along the shared face → a thin see-through crack. After meshing, every
|
||||||
@@ -818,9 +804,8 @@ driven by `EditorBrush*` props.
|
|||||||
drain; `VoxelSettings::MaxUnloadsPerFrame`): the cull APPROVES removals (strict load-before-unload) but
|
drain; `VoxelSettings::MaxUnloadsPerFrame`): the cull APPROVES removals (strict load-before-unload) but
|
||||||
doesn't destroy in place — it queues them. `ProcessUnloadQueue` runs at most `MaxUnloadsPerFrame`
|
doesn't destroy in place — it queues them. `ProcessUnloadQueue` runs at most `MaxUnloadsPerFrame`
|
||||||
`UnloadTile`s/frame (scaled up to 4× with backlog, capped so a huge backlog can't re-spike). WHY: a
|
`UnloadTile`s/frame (scaled up to 4× with backlog, capped so a huge backlog can't re-spike). WHY: a
|
||||||
fast traversal culls a whole shell's worth of tiles in ONE frame, and each `UnloadTile` does
|
fast traversal culls a whole shell's worth of tiles in ONE frame, and each `UnloadTile` strips and
|
||||||
`DestroyComponent` + (level 0) `ContentManager::ClearChunk` → `Destroy()` of every decoration actor —
|
parks its component (T2.c pool) — an unbudgeted burst = a game-thread spike ("stuff torn down behind you" at speed). Mesh APPLIES were
|
||||||
an unbudgeted burst = a game-thread spike ("stuff torn down behind you" at speed). Mesh APPLIES were
|
|
||||||
already budgeted; this matches it for DESTROYS. Re-desired tiles are cancelled out of the queue (still
|
already budgeted; this matches it for DESTROYS. Re-desired tiles are cancelled out of the queue (still
|
||||||
loaded → no reload). `PendingUnload` is cleared in `RegenerateAllChunks`/`EndPlay` (tiles already gone).
|
loaded → no reload). `PendingUnload` is cleared in `RegenerateAllChunks`/`EndPlay` (tiles already gone).
|
||||||
- **Collision only at LEVEL 0** (`ApplyMeshToTile`): `UpdateSectionConfig(..., Tile.Level==0)`. Far tiles
|
- **Collision only at LEVEL 0** (`ApplyMeshToTile`): `UpdateSectionConfig(..., Tile.Level==0)`. Far tiles
|
||||||
@@ -833,7 +818,7 @@ driven by `EditorBrush*` props.
|
|||||||
= 0; `=1` restores it as an A/B instrument). Measured on the headless game path with nested
|
= 0; `=1` restores it as an A/B instrument). Measured on the headless game path with nested
|
||||||
refinement off, the classifier plus its exact validation cost more than they saved. Mode 0 had
|
refinement off, the classifier plus its exact validation cost more than they saved. Mode 0 had
|
||||||
ready p95 182 vs 190-195 ms, generation p95 125-127 vs 139-141 ms, and 18% fewer density calls,
|
ready p95 182 vs 190-195 ms, generation p95 125-127 vs 139-141 ms, and 18% fewer density calls,
|
||||||
with 841/841 tiles triangle-identical (WORK-NEXT.md). Proving a 33³ core uniform costs almost
|
with 841/841 tiles triangle-identical (Docs/archive/WORK-NEXT.md). Proving a 33³ core uniform costs almost
|
||||||
as much as meshing 35³. The history below explains why it existed. Its soundness tests still
|
as much as meshing 35³. The history below explains why it existed. Its soundness tests still
|
||||||
guard it.
|
guard it.
|
||||||
- **Trivial-empty tile reject (T1.d) — v2 SHIPPED (2026-07-05); v1 was reverted 2026-06-26.**
|
- **Trivial-empty tile reject (T1.d) — v2 SHIPPED (2026-07-05); v1 was reverted 2026-06-26.**
|
||||||
@@ -844,9 +829,11 @@ driven by `EditorBrush*` props.
|
|||||||
wasted) and gen throughput had become the felt gameplay limit ("standing waiting for generation").
|
wasted) and gen throughput had become the felt gameplay limit ("standing waiting for generation").
|
||||||
**Why v1 failed & how v2 avoids it:** v1 used a GLOBAL analytic ceiling bound — not conservative when
|
**Why v1 failed & how v2 avoids it:** v1 used a GLOBAL analytic ceiling bound — not conservative when
|
||||||
the cap hangs low (`CeilingRoughness`/`RidgeStrength`) → holes in the roof. v2 makes NO amplitude
|
the cap hangs low (`CeilingRoughness`/`RidgeStrength`) → holes in the roof. v2 makes NO amplitude
|
||||||
guesses: it evaluates `ComputeSurfaceColumn` (the SAME function as the density path, via the SHARED
|
guesses: it evaluates `ComputeSurfaceColumn` (cached per thread in `GSurfColCache`) — the column
|
||||||
`GSurfColCache`) **on the exact lattice the mesher would sample** (margin ring included) — same
|
the surface stack's height operators reproduce (ground height pinned bit for bit by
|
||||||
functions + same inputs ⇒ the same floats ⇒ the verdict is exact at the lattice, not an estimate.
|
`VoxelForge.OpStack.SurfaceHeightEquivalence`) — **on the exact lattice the mesher would sample**
|
||||||
|
(margin ring included) — same inputs ⇒ the same floats ⇒ the verdict is exact at the lattice, not
|
||||||
|
an estimate.
|
||||||
Per-z rules: gap chunk = solid; surface seal band (ApplyBoundarySeal inequalities, `BaseDensity>0`)
|
Per-z rules: gap chunk = solid; surface seal band (ApplyBoundarySeal inequalities, `BaseDensity>0`)
|
||||||
= solid; surface interior z: air ⇔ `TerrainZ ≤ z ≤ CeilSurf` (MC `D ≥ 0`); ANY other archetype /
|
= solid; surface interior z: air ⇔ `TerrainZ ≤ z ≤ CeilSurf` (MC `D ≥ 0`); ANY other archetype /
|
||||||
out-of-layout chunkZ ⇒ `Mixed` (cave interiors are not provable in v1 of this classifier — a future
|
out-of-layout chunkZ ⇒ `Mixed` (cave interiors are not provable in v1 of this classifier — a future
|
||||||
@@ -860,7 +847,7 @@ driven by `EditorBrush*` props.
|
|||||||
A false `Mixed` only costs CPU; the code must NEVER emit a false AllSolid/AllAir (that's a hole).
|
A false `Mixed` only costs CPU; the code must NEVER emit a false AllSolid/AllAir (that's a hole).
|
||||||
Capture tiles (`bWantCapture`, density-volume shadow window) always generate — the volume wants the
|
Capture tiles (`bWantCapture`, density-volume shadow window) always generate — the volume wants the
|
||||||
grid even for uniform cells. A sparse ~5×5 column pre-pass exits Mixed fast on surface-crossing
|
grid even for uniform cells. A sparse ~5×5 column pre-pass exits Mixed fast on surface-crossing
|
||||||
tiles; a Mixed verdict leaves its columns warm in `GSurfColCache` for the GenerateMesh that follows.
|
tiles; the columns stay warm in `GSurfColCache` for the full pass and for neighbouring tiles.
|
||||||
- **Worker-built StreamSet (T1.f)** (`BuildTileStreamSet`, `LoadTile` task → `FChunkResult::Streams`):
|
- **Worker-built StreamSet (T1.f)** (`BuildTileStreamSet`, `LoadTile` task → `FChunkResult::Streams`):
|
||||||
the RMC vertex/index buffers (`FRealtimeMeshStreamSet`) are built ON THE GEN WORKER, not on the game
|
the RMC vertex/index buffers (`FRealtimeMeshStreamSet`) are built ON THE GEN WORKER, not on the game
|
||||||
thread. The per-vertex builder loop was the dominant game-thread streaming cost (measured: game
|
thread. The per-vertex builder loop was the dominant game-thread streaming cost (measured: game
|
||||||
@@ -981,9 +968,9 @@ change *anything*, e.g. frequencies, which scalar multipliers couldn't.)
|
|||||||
- **Consumption — SURFACE (output-blend).** Per chunk, `CP_SurfaceBiomeParams[]` holds each biome's
|
- **Consumption — SURFACE (output-blend).** Per chunk, `CP_SurfaceBiomeParams[]` holds each biome's
|
||||||
resolved surface params (its override when `bOverrideTerrain` + GeneratorType matches, else the
|
resolved surface params (its override when `bOverrideTerrain` + GeneratorType matches, else the
|
||||||
strate's) with **structural fields forced from the strate** (Z bounds, seal, base density, water
|
strate's) with **structural fields forced from the strate** (Z bounds, seal, base density, water
|
||||||
level). Per voxel: `ResolveBiomeSampleAt` → dominant `PD` (+ neighbour `PN`); `GetSurfaceDensity`
|
level). Per voxel: `ResolveBiomeSampleAt` → dominant `PD` (+ neighbour `PN`); the surface stack's
|
||||||
computes `ComputeSurfaceTerrainZ` for `PD` and, in the border band, for `PN`, and **lerps the
|
biome-blend height source (and `ComputeSurfaceColumn`) computes the terrain height for `PD` and, in
|
||||||
resulting HEIGHTS**. Blending heights (not params) is seamless across *any* difference (frequencies
|
the border band, for `PN`, and **lerps the resulting HEIGHTS**. Blending heights (not params) is seamless across *any* difference (frequencies
|
||||||
included) — what per-param blend never could. `PD==PN`, weight 0 ⇒ bit-identical, no biomes.
|
included) — what per-param blend never could. `PD==PN`, weight 0 ⇒ bit-identical, no biomes.
|
||||||
- **Consumption — CAVES: structural overrides are NOT applied (determinism).** Rooms/tunnels are
|
- **Consumption — CAVES: structural overrides are NOT applied (determinism).** Rooms/tunnels are
|
||||||
decided over a wide COLLECT region spanning chunks (§8.4); making room params vary by region would
|
decided over a wide COLLECT region spanning chunks (§8.4); making room params vary by region would
|
||||||
@@ -1117,7 +1104,7 @@ pending).** `AVoxelWorld::RegisterStreamingAnchor(Actor, Policy, RadiusChunks)`
|
|||||||
(BlueprintCallable) add an actor to `StreamingAnchors`. `UpdateChunksAroundPosition` prunes dead anchors +
|
(BlueprintCallable) add an actor to `StreamingAnchors`. `UpdateChunksAroundPosition` prunes dead anchors +
|
||||||
detects chunk crossings (rebuilds the desired set when any anchor crosses a level-0 boundary — same cadence
|
detects chunk crossings (rebuilds the desired set when any anchor crosses a level-0 boundary — same cadence
|
||||||
as player movement, coalesced into one rebuild). `AddAnchorDesiredTiles` (inside `BuildDesiredTiles`, after
|
as player movement, coalesced into one rebuild). `AddAnchorDesiredTiles` (inside `BuildDesiredTiles`, after
|
||||||
the player clipmap + sheet ring) folds each anchor's Chebyshev box of **level-0** tiles into the SAME
|
the player clipmap) folds each anchor's Chebyshev box of **level-0** tiles into the SAME
|
||||||
`DesiredStamped`/`DesiredSorted` set (deduped vs the clipmap by stamp) → the existing delta cull releases an
|
`DesiredStamped`/`DesiredSorted` set (deduped vs the clipmap by stamp) → the existing delta cull releases an
|
||||||
anchor's tiles automatically when it moves away / unregisters. Zero cost when no anchors (empty loop). The box
|
anchor's tiles automatically when it moves away / unregisters. Zero cost when no anchors (empty loop). The box
|
||||||
defaults to a THIN shape (its chunk + 1 horizontal ring + 1 chunk below for ground safety, nothing above —
|
defaults to a THIN shape (its chunk + 1 horizontal ring + 1 chunk below for ground safety, nothing above —
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ try to run anything.
|
|||||||
- **Determinism:** all randomness = hash of (coord, seed, strateIndex). No RNG state — same seed ⇒
|
- **Determinism:** all randomness = hash of (coord, seed, strateIndex). No RNG state — same seed ⇒
|
||||||
same world. Player edits (diff layer) are the only non-deterministic overlay.
|
same world. Player edits (diff layer) are the only non-deterministic overlay.
|
||||||
- **Async safety:** worker tasks only READ Generator/Mesher, must check `bShuttingDown`, and return via
|
- **Async safety:** worker tasks only READ Generator/Mesher, must check `bShuttingDown`, and return via
|
||||||
`ProcessQueue` (must stay `EQueueMode::Mpsc`). `EndPlay` blocks on `ActiveTaskCount → 0`.
|
the result queues — `ProcessQueue` and its `CriticalProcessQueue` / `EditedProcessQueue` /
|
||||||
|
`NearProcessQueue` siblings (all must stay `EQueueMode::Mpsc`). `EndPlay` blocks on `ActiveTaskCount → 0`.
|
||||||
- **Carry the `Epoch`** through any new async path (stale results are dropped on mismatch).
|
- **Carry the `Epoch`** through any new async path (stale results are dropped on mismatch).
|
||||||
- **Don't "optimize" the perf invariants** in ARCHITECTURE `§8.10` — the `thread_local` box-valid
|
- **Don't "optimize" the perf invariants** in ARCHITECTURE `§8.10` — the `thread_local` box-valid
|
||||||
caches, two-pass MC loop, SSE noise, and clipmap streaming are intentional.
|
caches, two-pass MC loop, SSE noise, and clipmap streaming are intentional.
|
||||||
|
|||||||
+202
-195
@@ -54,9 +54,12 @@ A **density-field voxel terrain** plugin for Unreal Engine, built around undergr
|
|||||||
▼
|
▼
|
||||||
UVoxelGenerator::GetDensityAt(x,y,z) ← THE density entry point
|
UVoxelGenerator::GetDensityAt(x,y,z) ← THE density entry point
|
||||||
│ asks StrateManager which strate/params/generator-type applies
|
│ asks StrateManager which strate/params/generator-type applies
|
||||||
├─ TunnelNetwork → GetDensityWithParams() (rooms+tunnels+ops+worms+seal+passages)
|
├─ builds the chunk's operator stack once per chunk (VF_BuildOpStackForChunk;
|
||||||
|
│ editor composer / cooked-season recipe stacks first) → every archetype
|
||||||
|
│ TunnelNetwork/Underwater → fused evaluator = GetDensityWithParams()
|
||||||
│ └─ VoxelCaveMorphology::BuildChunkCache + EvaluateSDFCached (room/tunnel SDF)
|
│ └─ VoxelCaveMorphology::BuildChunkCache + EvaluateSDFCached (room/tunnel SDF)
|
||||||
├─ FlatPlain / CrystalChamber → GetSlabDensity() (floor+ceiling+columns+seal+passages)
|
│ others → FVoxelOpStack::EvalMC (source → modifiers → structural posts)
|
||||||
|
├─ + disturbances + structural tail (landing floor, tunnel core, XY seal)
|
||||||
└─ + UVoxelDiffLayer::GetDensityOffset() (player carve/fill)
|
└─ + UVoxelDiffLayer::GetDensityOffset() (player carve/fill)
|
||||||
│
|
│
|
||||||
▼ FVoxelMeshData (verts/tris/uvs/normals)
|
▼ FVoxelMeshData (verts/tris/uvs/normals)
|
||||||
@@ -118,27 +121,25 @@ remains `0`, and lateral regions remain gated off.
|
|||||||
| `Public/VoxelForgeModule.h` / `Private/VoxelForgeModule.cpp` | `FVoxelForgeModule` boilerplate (Startup/Shutdown just log). |
|
| `Public/VoxelForgeModule.h` / `Private/VoxelForgeModule.cpp` | `FVoxelForgeModule` boilerplate (Startup/Shutdown just log). |
|
||||||
| `Source/VoxelForgeEditor/VoxelForgeEditor.Build.cs` / `Private/VoxelForgeExploreCommandlet.*` | Editor/commandlet-only `-run=VoxelForgeExplore`; owns bounded render/`VF_` walk/OBJ export orchestration and is not linked by Game/Shipping. |
|
| `Source/VoxelForgeEditor/VoxelForgeEditor.Build.cs` / `Private/VoxelForgeExploreCommandlet.*` | Editor/commandlet-only `-run=VoxelForgeExplore`; owns bounded render/`VF_` walk/OBJ export orchestration and is not linked by Game/Shipping. |
|
||||||
| `Public/VoxelGeometryHash.h` | Header-only geometry digest shared by commandlet export and runtime tile-hash dump; hashes mesh counts and the vertex/normal/UV/color/triangle arrays with the historical CRC order. |
|
| `Public/VoxelGeometryHash.h` | Header-only geometry digest shared by commandlet export and runtime tile-hash dump; hashes mesh counts and the vertex/normal/UV/color/triangle arrays with the historical CRC order. |
|
||||||
| `Public/VoxelStats.h` / `Private/VoxelStats.cpp` | `stat VoxelForge` DWORD counters for tile classification, skipping, meshing, operator-stack verdicts, and cave-bail diagnosis. The former ambiguous `Cave Bail Not Op Stack` is split into `Sole Slot`, `Boundary Tile`, `No Layout`, and late `Recheck` counters, so each increment names one guard/context. |
|
| `Public/VoxelStats.h` / `Private/VoxelStats.cpp` | `stat VoxelForge` DWORD counters for tile classification, skipping, meshing, operator-stack verdicts, and cave-bail diagnosis. Each `Cave Bail *` counter names one guard; `Cave Bail No Stack` counts slots with no buildable stack (no definition, or a degenerate strate). |
|
||||||
|
|
||||||
### 3.2 Foundational types — `Public/VoxelTypes.h` (no UClass, everyone includes it)
|
### 3.2 Foundational types — `Public/VoxelTypes.h` (no UClass, everyone includes it)
|
||||||
| Symbol | Line | Notes |
|
| Symbol | Line | Notes |
|
||||||
|--------|------|-------|
|
|--------|------|-------|
|
||||||
| `CHUNK_SIZE` (32), `CHUNK_SIZE_SQUARED`, `CHUNK_VOLUME` | 19-21 | Chunk dimensions. (64³ tried for fewer draws → reverted: streaming too bursty. fps is fixed render-side instead.) |
|
| `CHUNK_SIZE` (32) | 211 | Chunk dimensions. 64³ would cut draw calls but makes streaming too bursty; fps is fixed render-side instead. |
|
||||||
| `VOXEL_SIZE` (25.0f cm) | 23 | World scale. |
|
| `VOXEL_SIZE` (25.0f cm) | 213 | World scale. |
|
||||||
| `EVoxelFace` enum + `GetFaceDirection` / `GetFaceNormal` | 33-61 | 6 cube faces. |
|
| `EVoxelFace` enum | 266 | 6 cube faces. |
|
||||||
| `WorldToChunkCoord` / `WorldToLocalCoord` / `ChunkToWorldPos` | 74-104 | Coord-space conversions (handle negatives via floor/positive-modulo). |
|
| `WorldToChunkCoord` | 287 | World cm → chunk coord (handles negatives via floor). |
|
||||||
| `LocalToIndex` / `IndexToLocal` / `IsValidLocalCoord` | 107-131 | Flat-array 3D↔1D indexing. |
|
| `VoxelMath::IsFiniteFast` / `IsFinite` | 24 / 45 | Exact IEEE bit test (`voxel.FastIsFinite` A/B switch). |
|
||||||
| `VoxelMath::IsFiniteFast` / `IsFinite` | ~24-53 | Exact IEEE bit test (`voxel.FastIsFinite` A/B switch). |
|
| `VoxelMath::DetSinCos` / `DetSin` / `DetCos` | 63 / 181 / 189 | **The only sin/cos allowed where the world is decided** (2026-09-16, `10fd0b6`). Fixed double period reduction and float polynomial, no CRT: `FMath::Sin/Cos` are CRT `sinf/cosf`, whose FMA3 variant is picked by CPU. Pinned by `VoxelForge.Determinism.DetSinCos`; `-voxel.CrtFma3=0` flips the CRT path to prove it. |
|
||||||
| `VoxelMath::DetSinCos` / `DetSin` / `DetCos` | ~63-195 | **The only sin/cos allowed where the world is decided** (2026-09-16, `10fd0b6`). Fixed double period reduction and float polynomial, no CRT: `FMath::Sin/Cos` are CRT `sinf/cosf`, whose FMA3 variant is picked by CPU. Pinned by `VoxelForge.Determinism.DetSinCos`; `-voxel.CrtFma3=0` flips the CRT path to prove it. |
|
| `SmoothStep01` | 306 | 3x²-2x³ — used everywhere for blends. |
|
||||||
| `SmoothStep01` | 140 | 3x²-2x³ — used everywhere for blends. |
|
| `VOXEL_NOISE_SCALE` (1.25f) | 313 | Rescales UE PerlinNoise3D to ~[-1,1]. |
|
||||||
| `VOXEL_NOISE_SCALE` (1.25f) | 147 | Rescales UE PerlinNoise3D to ~[-1,1]. |
|
| `EVoxelTileClass` enum (`Mixed`/`AllSolid`/`AllAir`) | 231 | T1.d verdict. Lives here so `VoxelDensityOp.h` can share it without a UCLASS dependency. A false `AllSolid`/`AllAir` is a HOLE; a false `Mixed` only costs CPU. |
|
||||||
| `EVoxelTileClass` enum (`Mixed`/`AllSolid`/`AllAir`) | — | T1.d verdict. **MOVED here from `VoxelGenerator.h` 2026-07-27** so `VoxelDensityOp.h` can share it without a UCLASS dependency. A false `AllSolid`/`AllAir` is a HOLE; a false `Mixed` only costs CPU. |
|
| `FVoxelMeshData` struct | 365-391 | Mesher output (Vertices/Triangles/UVs/Normals/**Colors**). Plain C++, not USTRUCT. `Colors` = F6 material masks (R=dominant biome palette, G=slope, B=border blend weight, A=neighbour biome palette). §8.15. |
|
||||||
| `FVoxelMeshData` struct | 157-173 | Mesher output (Vertices/Triangles/UVs/Normals/**Colors**). Plain C++, not USTRUCT. `Colors` = F6 material masks (R=dominant biome palette, G=slope, B=border blend weight, A=neighbour biome palette). §8.15. |
|
|
||||||
|
|
||||||
### 3.2b Density operator stack contract — `Public/VoxelDensityOp.h` (plain C++, no UHT)
|
### 3.2b Density operator stack contract — `Public/VoxelDensityOp.h` (plain C++, no UHT)
|
||||||
**Production path behind a per-strate opt-in** — the stack is built/evaluated by `GetDensityAt` for
|
**The production density path** — `GetDensityAt` builds and evaluates a stack for every strate of
|
||||||
all eight archetypes when `bUseOperatorStack` is enabled, while the legacy `switch` remains the
|
all eight archetypes; there is no other path. See [Docs/archive/OPSTACK-PLAN.md](Docs/archive/OPSTACK-PLAN.md) for the decomposition.
|
||||||
reference path for opt-out strates. See [OPSTACK-PLAN.md](OPSTACK-PLAN.md) for the decomposition.
|
|
||||||
|
|
||||||
| Symbol | Notes |
|
| Symbol | Notes |
|
||||||
|--------|-------|
|
|--------|-------|
|
||||||
@@ -159,29 +160,24 @@ manager passage post, **moved here 2026-07-27** so the generator and
|
|||||||
the operator stack share ONE copy. The XY helper is a radial smooth ramp in actor-space and is a
|
the operator stack share ONE copy. The XY helper is a radial smooth ramp in actor-space and is a
|
||||||
true no-op when `WorldRadiusVoxels == 0`; `VF_XYEdgeSealForcedMarginOverBox` is the shared sound
|
true no-op when `WorldRadiusVoxels == 0`; `VF_XYEdgeSealForcedMarginOverBox` is the shared sound
|
||||||
proof for `ClassifyBox` and T1.d's global shell skip. `VoxelGenerator.cpp` keeps same-named
|
proof for `ClassifyBox` and T1.d's global shell skip. `VoxelGenerator.cpp` keeps same-named
|
||||||
`static FORCEINLINE` forwarders for the original three helpers; the new edge helper is called
|
`static FORCEINLINE` forwarders for `ApplyBoundarySeal` and `ApplyOriginSpine`; the passage and
|
||||||
directly. **Convention: INTERNAL (positive = solid); `VF_ApplyXYEdgeSealMC` is the output wrapper.**
|
edge helpers are called directly. **Convention: INTERNAL (positive = solid); `VF_ApplyXYEdgeSealMC` is the output wrapper.**
|
||||||
|
|
||||||
### 3.2d Operator stack — `Public/VoxelDensityOpStack.h` + `Private/VoxelDensityOpStack.cpp`
|
### 3.2d Operator stack — `Public/VoxelDensityOpStack.h` + `Private/VoxelDensityOpStack.cpp`
|
||||||
⚠️ **Feeds the game, behind a per-strate opt-in** (Phase 1 step 3). `GetDensityAt` builds the stack
|
⚠️ **This IS the game's generation path.** `GetDensityAt` builds the stack in its per-chunk refetch
|
||||||
in its per-chunk refetch block and evaluates it *instead of* the `switch` only when
|
block for every strate — **all 8 archetypes**: Maze, FlatPlain, CrystalChamber, SurfaceWorld,
|
||||||
`UVoxelStrateManager::UsesOperatorStackForChunk` says so — strate ticked `bUseOperatorStack` **and**
|
VerticalShafts, FloatingIslands, TunnelNetwork, Underwater. An editor composer candidate or a
|
||||||
archetype in the ported list, which is now **all 8 of 8**: Maze, FlatPlain, CrystalChamber,
|
cooked-season recipe supplies its own stack; otherwise (or if that stack cannot be built) the
|
||||||
SurfaceWorld, VerticalShafts, FloatingIslands, TunnelNetwork, Underwater. A strate that has not
|
archetype's native stack is used. A degenerate (zero-height) strate has no stack and is open air.
|
||||||
ticked the box still takes the `switch`, unchanged — the flag is the only thing that switches paths.
|
`UVoxelStrateManager::UsesOperatorStackForChunk` only says whether a slot has a known archetype.
|
||||||
**`ClassifyTile` IS wired now**, for CAVE archetypes only and behind the same per-strate opt-in:
|
**`ClassifyTile`**, for CAVE archetypes, builds the strate's stack through the *same* factory
|
||||||
where it used to `return Mixed` without a call, it builds the strate's stack through the *same*
|
`GetDensityAt` uses (`VF_BuildOpStackForChunk`) and folds `ClassifyBox`. SurfaceWorld and bedrock
|
||||||
factory `GetDensityAt` uses (`VF_BuildOpStackForChunk`) and folds `ClassifyBox`. SurfaceWorld and
|
gaps keep their hand-written proofs — the exact-lattice column test is better than any box bound.
|
||||||
bedrock gaps keep their hand-written proofs — the exact-lattice column test is better than any box
|
Guards, all failing to `Mixed`: one cave slot per tile, no mixed cave/surface/gap tile, a stack on
|
||||||
bound. Guards, all failing to `Mixed`: one cave slot per tile, no mixed cave/surface/gap tile, the
|
*every* chunk the box touches, the params **bit-identical** across every chunk coord the box touches
|
||||||
opt-in true on *every* chunk the box touches, the params **bit-identical** across every chunk coord
|
(blended transition bands make one stack unable to represent the tile — `AUDIT §C2`), a
|
||||||
the box touches (blended transition bands make one stack unable to represent the tile — `AUDIT §C2`),
|
27-chunk-coord cap, and the disturbances folded in by hand since they are applied after the stack.
|
||||||
a 27-chunk-coord cap, and the disturbances folded in by hand since they are applied after the stack.
|
|
||||||
Brute-forced end to end by `VoxelForge.OpStack.ClassifyTileSoundness`.
|
Brute-forced end to end by `VoxelForge.OpStack.ClassifyTileSoundness`.
|
||||||
⛔ Never run both paths in one world. **Comparing them IS legitimate now** — the ~1 ULP residue of
|
|
||||||
AUDIT §C10 is gone since `FPSemantics = Precise`, and all eight equivalence tests compare bit for
|
|
||||||
bit. They are port-correctness oracles, not fidelity checks: the acceptance bar is §2.6.1 (same seed
|
|
||||||
⇒ same world on every peer), which does not require resembling the pre-refactor world.
|
|
||||||
|
|
||||||
| Symbol | Role | Notes |
|
| Symbol | Role | Notes |
|
||||||
|--------|------|-------|
|
|--------|------|-------|
|
||||||
@@ -197,7 +193,7 @@ bit. They are port-correctness oracles, not fidelity checks: the acceptance bar
|
|||||||
| `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**: the proved FBM supremum `1.5` 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**: the proved FBM supremum `1.5` 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 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. The source publishes a conservative interval covering both shafts and connector capsules; it does not answer for the ledge or converter. |
|
| `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. The source publishes a conservative interval covering both shafts and connector capsules; it does not answer for the ledge or converter. |
|
||||||
| `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. |
|
||||||
| `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. `FSurfaceColumn` is deliberately trivial: all five outputs are written before the cache's `Computed` publish, so removing its default initialization removes a TLS constructor leaf without changing a value. |
|
| `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. `FSurfaceColumn` is deliberately trivial: all five outputs are written before the cache's `Computed` publish, so removing its default initialization removes a TLS constructor leaf without changing a value. |
|
||||||
| `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`. |
|
||||||
| `VoxelDensityOps::BuildVerticalShaftStack` | — | 9 ops, and **three are Maze's reused unchanged** (`ConstantRock`, `SdfRoughness`, `SdfCarve`) with different tuning (freq 0.1 vs 0.12, window `rough+4` vs `R+rough+2`). Passes the runtime spine radius into `FShaftFieldSource`, which mirrors the generator's connector-only origin endpoint and roughness-safe spine capsule. The measured proof of `OPSTACK-PLAN §2.5`'s reuse claim. |
|
| `VoxelDensityOps::BuildVerticalShaftStack` | — | 9 ops, and **three are Maze's reused unchanged** (`ConstantRock`, `SdfRoughness`, `SdfCarve`) with different tuning (freq 0.1 vs 0.12, window `rough+4` vs `R+rough+2`). Passes the runtime spine radius into `FShaftFieldSource`, which mirrors the generator's connector-only origin endpoint and roughness-safe spine capsule. The measured proof of `OPSTACK-PLAN §2.5`'s reuse claim. |
|
||||||
@@ -213,7 +209,7 @@ bit. They are port-correctness oracles, not fidelity checks: the acceptance bar
|
|||||||
| `FRoomGraphSource::LocalParams()` | — | **The per-room op override** (DECOMPOSITION §2's "no clean home"). Strate params + the nearest room's `UVoxelTerrainOpDefinition::ApplyTo`, memoised once per voxel and read by eleven modifiers. One op owns the shared state, the rest read it — the same pattern as `FOverhangShelfMod` ← `FSurfaceColumnSource`. The detail consumers use the propagated interval for their shared near/far gate; unknown remains conservative, and the per-room parameter override is never used to claim an unproved box identity. |
|
| `FRoomGraphSource::LocalParams()` | — | **The per-room op override** (DECOMPOSITION §2's "no clean home"). Strate params + the nearest room's `UVoxelTerrainOpDefinition::ApplyTo`, memoised once per voxel and read by eleven modifiers. One op owns the shared state, the rest read it — the same pattern as `FOverhangShelfMod` ← `FSurfaceColumnSource`. The detail consumers use the propagated interval for their shared near/far gate; unknown remains conservative, and the per-room parameter override is never used to claim an unproved box identity. |
|
||||||
| `FIslandBlobSource` (internal) | 1 | Hash-placed tapered flat-top blobs, `SmoothMin`'d, in a **domain-warped XY frame** (the warp stays inside the op — see the deviation note vs DECOMPOSITION §7). SDF channel only. The source publishes an interval covering the full authored radius envelope (`max(MinRadius, MaxRadius)`), the two-axis warp·**√2** envelope, top surface and SmoothMin dip; the fill converter makes its own decision. **No lower Z bound exists** — a hairline thread of matter hangs below each island down its axis, so only the TOP may reject. |
|
| `FIslandBlobSource` (internal) | 1 | Hash-placed tapered flat-top blobs, `SmoothMin`'d, in a **domain-warped XY frame** (the warp stays inside the op — see the deviation note vs DECOMPOSITION §7). SDF channel only. The source publishes an interval covering the full authored radius envelope (`max(MinRadius, MaxRadius)`), the two-axis warp·**√2** envelope, top surface and SmoothMin dip; the fill converter makes its own decision. **No lower Z bound exists** — a hairline thread of matter hangs below each island down its axis, so only the TOP may reject. |
|
||||||
| `VoxelDensityOps::BuildFloatingIslandStack` | — | 8 ops, and **the stack runs backwards**: void source + fill instead of rock source + carve, using the *same* classes with the opposite sign. Only the blob source is new. Reuse by **inversion** — a stronger result than reuse by identity, since it says the abstract axis (the density sign) is the right one. |
|
| `VoxelDensityOps::BuildFloatingIslandStack` | — | 8 ops, and **the stack runs backwards**: void source + fill instead of rock source + carve, using the *same* classes with the opposite sign. Only the blob source is new. Reuse by **inversion** — a stronger result than reuse by identity, since it says the abstract axis (the density sign) is the right one. |
|
||||||
| `VoxelDensityOps::BuildMazeStack` | — | The 8-op Maze stack (4 structural posts included). Its lattice source mirrors the origin-directed tree + capped loops and rebuilds the 2×2×2 lower-node window once per cell; the per-voxel loop only evaluates cached capsules. If this ever becomes one op, the refactor failed its own test (§2.5). Callers must skip it on a **degenerate strate** (top−bottom ≤ 0): `GetMazeDensity` early-outs to air there and the stack has no such early-out by design — `GetDensityAt` falls back to the `switch`. |
|
| `VoxelDensityOps::BuildMazeStack` | — | The 8-op Maze stack (4 structural posts included). Its lattice source mirrors the origin-directed tree + capped loops and rebuilds the 2×2×2 lower-node window once per cell; the per-voxel loop only evaluates cached capsules. If this ever becomes one op, the refactor failed its own test (§2.5). Callers must skip it on a **degenerate strate** (top−bottom ≤ 0): the stack has no such early-out by design — `VF_BuildOpStackForChunk` refuses it and `GetDensityAt` returns air. |
|
||||||
|
|
||||||
#### Lateral region parent (Tier 4d)
|
#### Lateral region parent (Tier 4d)
|
||||||
|
|
||||||
@@ -252,29 +248,30 @@ is a **column** property, or one opaque op (`OPSTACK-PLAN §2.5`'s failure mode)
|
|||||||
| `VoxelHeightOps::BuildSurfaceHeightStack` | 5 ops in `ComputeSurfaceTerrainZ`'s order — structural → cliff → terrace → layer lines → beach. **Order is not negotiable.** |
|
| `VoxelHeightOps::BuildSurfaceHeightStack` | 5 ops in `ComputeSurfaceTerrainZ`'s order — structural → cliff → terrace → layer lines → beach. **Order is not negotiable.** |
|
||||||
|
|
||||||
### 3.3 Chunk identity
|
### 3.3 Chunk identity
|
||||||
`VoxelChunk.h` (the old `FVoxelChunk` coord wrapper) was DELETED — dead since the tile
|
Tile identity lives in `FVoxelTileKey` (VoxelTypes.h:326).
|
||||||
redesign; tile identity lives in `FVoxelTileKey` (VoxelWorld.h).
|
|
||||||
|
|
||||||
### 3.4 Settings — `Public/VoxelSettings.h`
|
### 3.4 Settings — `Public/VoxelSettings.h`
|
||||||
`UVoxelSettings : UPrimaryDataAsset` — the single tuning asset assigned on `AVoxelWorld`.
|
`UVoxelSettings : UPrimaryDataAsset` — the single tuning asset assigned on `AVoxelWorld`.
|
||||||
| Group | Fields (line) |
|
| Group | Fields (line) |
|
||||||
|-------|---------------|
|
|-------|---------------|
|
||||||
| Streaming | `ViewDistanceXY=16`, `ViewDistanceUp/Down=5`, `MaxConcurrentTasks=16`, `MaxMeshAppliesPerFrame=4` (defaults — actual values live on the data asset) |
|
| Streaming | `ViewDistanceXY=16`, `ViewDistanceUp/Down=5`, `MaxConcurrentTasks=16`, `MaxMeshAppliesPerFrame=4` (defaults — actual values live on the data asset) |
|
||||||
| Clipmap | `ClipRadius`, `MaxClipLevel=5`, `FullResClipLevels`, `CoarseTileCells`, `RenderDistanceChunks=768` (measured ship defaults; custom horizontal reach: the outermost shell keeps generating until it covers this many chunks; 0 = off), `bFarSheetRing=false` by default, plus `FarSheetSpanLevels` (F18 — the optional render-distance ring streams per-surface SHEETS: level MaxClipLevel+span heightfield tiles instead of MC, see `GenerateSheetMesh`), skirts, `LODOctaveDrop` (T2.b octave drop on coarse tiles — 0 = off/byte-identical). Existing serialized data assets retain their authored values until deliberately migrated. |
|
| Clipmap | `ClipRadius`, `MaxClipLevel=5`, `FullResClipLevels`, `CoarseTileCells`, `RenderDistanceChunks=768` (measured ship defaults; custom horizontal reach: the outermost shell keeps generating until it covers this many chunks; 0 = off), skirts, `LODOctaveDrop` (T2.b octave drop on coarse tiles — 0 = off/byte-identical). Existing serialized data assets retain their authored values until deliberately migrated. |
|
||||||
| World bounds | `WorldRadiusVoxels=8192` and `EdgeSealThickness=64` (actor-space XY radial shell; radius 0 = true legacy no-op) |
|
| World bounds | `WorldRadiusVoxels=8192` and `EdgeSealThickness=64` (actor-space XY radial shell; radius 0 = true legacy no-op) |
|
||||||
| Lighting | `bEnableDensityVolume` + DensityVolume* tunables (§3.11 density clipmap / mini-sun shadows) |
|
| Lighting | `bEnableDensityVolume` + DensityVolume* tunables (§3.11 density clipmap / mini-sun shadows) |
|
||||||
| Rendering | `VoxelMaterial` (61) |
|
| Rendering | `VoxelMaterial` (377) |
|
||||||
| Strates | Optional cooked `Season`; effective seed/spine/radius/season accessors read it when assigned. Otherwise `Seed`, `CurrentSeason=1`, `StratePool`, `FixedStrates`, and `TotalStrates=10` retain the authored path. |
|
| Strates | Optional cooked `Season`; effective seed/spine/radius/season accessors read it when assigned. Otherwise `Seed`, `CurrentSeason=1`, `StratePool`, `FixedStrates`, and `TotalStrates=10` retain the authored path. |
|
||||||
| Carving budget | `MaxModifications=0` (97), `MaxBrushRadius=15` (102), `MaxTotalVolume=0` (107). 0 = unlimited. |
|
| Carving budget | `MaxModifications=0` (449), `MaxBrushRadius=15` (454), `MaxTotalVolume=0` (459). 0 = unlimited. |
|
||||||
|
|
||||||
### 3.5 World orchestrator — `Public/VoxelWorld.h` + `Private/VoxelWorld.cpp`
|
### 3.5 World orchestrator — `Public/VoxelWorld.h` + `Private/VoxelWorld.cpp`
|
||||||
`AVoxelWorld : AActor` — owns everything, drives streaming. Also `FChunkResult` struct
|
`AVoxelWorld : AActor` — owns everything, drives streaming. Also `FChunkResult` struct
|
||||||
(VoxelWorld.h:35) = async task payload (coord, chunk, meshdata, LOD, **Epoch**, request timing,
|
(VoxelWorld.h:99) = async task payload (coord, chunk, meshdata, LOD, **Epoch**, request timing,
|
||||||
worker-built stream geometry counts/bytes, desired-set epoch and obsolete/cancellation state).
|
worker-built stream geometry counts/bytes, desired-set epoch and obsolete/cancellation state).
|
||||||
|
|
||||||
**Owned objects (UPROPERTY):** `Settings`, `Generator`, `Mesher`, `StrateManager`,
|
**Owned objects (UPROPERTY):** `Settings`, `Generator`, `Mesher`, `StrateManager`,
|
||||||
`DiffLayer` (VoxelWorld.h:55-78). **Storage:** `Chunks` map, `ChunkMeshes` map,
|
`DiffLayer`, `ContentManager`, `AtmosphereManager`, `DensityVolume` (VoxelWorld.h:188-228).
|
||||||
`ChunkLODs`, `ProcessQueue` (TQueue), `PendingChunkCoord` (TSet) (VoxelWorld.h:85-312).
|
**Storage:** `LoadedTiles` (TSet), `TileComponents` map + `TileComponentPool` (VoxelWorld.h:260-308),
|
||||||
|
the four MPSC result queues `CriticalProcessQueue` / `EditedProcessQueue` / `NearProcessQueue` /
|
||||||
|
`ProcessQueue`, and `PendingTiles` (TSet) (VoxelWorld.h:908-912).
|
||||||
**Async state:** `bShuttingDown`, `ActiveTaskCount` (atomics), `GenerationEpoch`, and the
|
**Async state:** `bShuttingDown`, `ActiveTaskCount` (atomics), `GenerationEpoch`, and the
|
||||||
game-thread `PendingTileCancellation` map whose per-request atomic flags are read-only on workers.
|
game-thread `PendingTileCancellation` map whose per-request atomic flags are read-only on workers.
|
||||||
|
|
||||||
@@ -293,45 +290,45 @@ casse cette arithmetique *structurellement* — `BeginPlay` loggue une **Error**
|
|||||||
|
|
||||||
| Method | .cpp line | Role |
|
| Method | .cpp line | Role |
|
||||||
|--------|-----------|------|
|
|--------|-----------|------|
|
||||||
| `AVoxelWorld()` ctor | 12 | Enables Tick. |
|
| `AVoxelWorld()` ctor | 176 | Enables Tick. |
|
||||||
| `RegenerateAllChunks()` | 21 | Bumps epoch, unloads all → Tick reloads. CallInEditor button. |
|
| `RegenerateAllChunks()` | 530 | Bumps epoch, unloads all → Tick reloads. CallInEditor button. |
|
||||||
| `ValidateDeterminism()` | — | **F2 CallInEditor button (PIE)**: re-samples boundary points under left- vs right-chunk cache warm-ups + a same-alignment repeat; any non-zero delta = window-invariance regression (§8.4). Run after every "bit-identical" hot-path refactor. |
|
| `ValidateDeterminism()` | — | **F2 CallInEditor button (PIE)**: re-samples boundary points under left- vs right-chunk cache warm-ups + a same-alignment repeat; any non-zero delta = window-invariance regression (§8.4). Run after every "bit-identical" hot-path refactor. |
|
||||||
| `GetMaxConcurrentTasks()` | — | T2.d — asset `MaxConcurrentTasks` capped to logical cores − 2 (all three budget checks use it). |
|
| `GetMaxConcurrentTasks()` | — | T2.d — asset `MaxConcurrentTasks` capped to logical cores − 2 (all three budget checks use it). |
|
||||||
| `PostEditChangeProperty` | 45 | Editor live-edit hook. |
|
| `PostEditChangeProperty` | 1000 | Editor live-edit hook. |
|
||||||
| `OnObjectModifiedInEditor` | 58 | Regenerates when a strate asset is edited (if `bLiveEditStrates`). |
|
| `OnObjectModifiedInEditor` | 1022 | Regenerates when a strate asset is edited (if `bLiveEditStrates`). |
|
||||||
| `EndPlay` | 140 | Sets `bShuttingDown`, **waits for `ActiveTaskCount`→0**, unbinds delegate. |
|
| `EndPlay` | 1118 | Sets `bShuttingDown`, **waits for `ActiveTaskCount`→0**, unbinds delegate. |
|
||||||
| `BeginPlay` | 177 | Constructs Generator/Mesher/StrateManager/DiffLayer, wires services, seeds. |
|
| `BeginPlay` | 1429 | Constructs Generator/Mesher/StrateManager/DiffLayer, wires services, seeds. |
|
||||||
| `Tick` | 220 | `UpdateChunksAroundPosition(player, heading)` + `ProcessPendingChunks()`; the clean LOD0 latency summary is flushed at `EndPlay`. |
|
| `Tick` | 4395 | `UpdateChunksAroundPosition(player, heading)` + `ProcessPendingChunks()`; the clean LOD0 latency summary is flushed at `EndPlay`. |
|
||||||
| `GetPlayerPosition` | 231 | Pawn position or zero. (`GetLODForChunk`/`LODToStep`/`IsChunkInRange` removed — dead since the clipmap.) |
|
| `GetPlayerPosition` | 4831 | Pawn position or zero. |
|
||||||
| `ProcessPendingChunks` | 301 | Drains ProcessQueue under per-frame budget; reads the per-request cancellation token before removing its map entry, drops obsolete/aborted results, then applies each live result via `ApplyTileResult`. |
|
| `ProcessPendingChunks` | 5856 | Drains ProcessQueue under per-frame budget; reads the per-request cancellation token before removing its map entry, drops obsolete/aborted results, then applies each live result via `ApplyTileResult`. |
|
||||||
| `ApplyTileResult` | — | **Shared game-thread apply** for one `FChunkResult` (async drain + sync carve): discards stale generation epochs and keys no longer desired, marks loaded, ingests capture, EMPTY releases the tile's existing component (a re-gen can flip content→empty on band change — old geometry must not linger), else `ApplyMeshToTile`. Returns true iff a visible mesh uploaded (counts the budget). Doesn't touch `PendingTiles` (caller's). |
|
| `ApplyTileResult` | — | **Shared game-thread apply** for one `FChunkResult` (async drain + sync carve): discards stale generation epochs and keys no longer desired, marks loaded, ingests capture, EMPTY releases the tile's existing component (a re-gen can flip content→empty on band change — old geometry must not linger), else `ApplyMeshToTile`. Returns true iff a visible mesh uploaded (counts the budget). Doesn't touch `PendingTiles` (caller's). |
|
||||||
| `FVoxelStackSampler` / `FScopedVoxelStackRegistration` (`VoxelStackSampler.h/.cpp`) | — | **Diagnostic raw stack sampler, off by default.** A sampler thread captures registered generation workers' stacks (`CaptureThreadStackBackTrace`) every N µs and tags each with the tile's LOD. It writes bounded raw PCs to CSV plus a symbolization-free summary under `Saved/`; symbol resolution is deferred until after process exit so the measured process never initializes/calls DbgHelp for this diagnostic. Lifecycle teardown is serialized and registration TLS is session-tagged. Registration is per tile TASK in `LoadTile` (and the export's tasks), never per sample; disabled costs one relaxed atomic load. Game `-voxel.SampleStacks=<us>`, commandlet `-samplestacks[=us]`. |
|
| `FVoxelStackSampler` / `FScopedVoxelStackRegistration` (`VoxelStackSampler.h/.cpp`) | — | **Diagnostic raw stack sampler, off by default.** A sampler thread captures registered generation workers' stacks (`CaptureThreadStackBackTrace`) every N µs and tags each with the tile's LOD. It writes bounded raw PCs to CSV plus a symbolization-free summary under `Saved/`; symbol resolution is deferred until after process exit so the measured process never initializes/calls DbgHelp for this diagnostic. Lifecycle teardown is serialized and registration TLS is session-tagged. Registration is per tile TASK in `LoadTile` (and the export's tasks), never per sample; disabled costs one relaxed atomic load. Game `-voxel.SampleStacks=<us>`, commandlet `-samplestacks[=us]`. |
|
||||||
| Room-cache performance (2026-09-13): `VF_FindPlayerFitPointForRoomMemoized` / `GPlayerFitMemo` (`VoxelCaveMorphology.cpp`), `voxel.TileCacheWindow` + `VoxelGenLOD::TileOriginVoxels/TileCellsPerAxis` (`VoxelGenerator.h`), `FChunkSDFSpatialIndex` | — | **Player-fit memo:** exact 97-word key, process-wide, `voxel.PlayerFitMemo`. **Tile cache window:** the room cache covers the requesting tile's footprint plus a fixed collect margin. It is set via `TGuardValue` in `GenerateMesh`/classify, and is window-invariant by construction (the margin does not grow with the tile). **LOD-gated:** fused path at LOD ≥ `voxel.TileCacheWindowMinLOD` (3), op-stack at LOD ≥ `voxel.TileCacheWindowOpStackMinLOD` (4); lower LODs keep the 4-chunk worker window, because the tile window slowed LOD0 evaluation ~6%. Strates whose authored terrain ops exceed the proven 256-voxel extent (or are malformed/unloaded) fall back to the legacy window (`VF_ComputeTerrainOpEnvelope`). `voxel.SpatialIndex` is -1 auto (off at fused LOD0, on elsewhere), 0 or 1. **Spatial index:** ordered XY buckets over cache primitives, exact rejection. The `static_assert` in `VoxelStrateTypes.h` ties `VF_STRATE_PARAM_FIELDS` to `FStrateGenerationParams` (83 fields + 8 B padding). |
|
| Room-cache performance (2026-09-13): `VF_FindPlayerFitPointForRoomMemoized` / `GPlayerFitMemo` (`VoxelCaveMorphology.cpp`), `voxel.TileCacheWindow` + `VoxelGenLOD::TileOriginVoxels/TileCellsPerAxis` (`VoxelGenerator.h`), `FChunkSDFSpatialIndex` | — | **Player-fit memo:** exact 97-word key, process-wide, `voxel.PlayerFitMemo`. **Tile cache window:** the room cache covers the requesting tile's footprint plus a fixed collect margin. It is set via `TGuardValue` in `GenerateMesh`/classify, and is window-invariant by construction (the margin does not grow with the tile). **LOD-gated:** fused path at LOD ≥ `voxel.TileCacheWindowMinLOD` (3), op-stack at LOD ≥ `voxel.TileCacheWindowOpStackMinLOD` (4); lower LODs keep the 4-chunk worker window, because the tile window slowed LOD0 evaluation ~6%. Strates whose authored terrain ops exceed the proven 256-voxel extent (or are malformed/unloaded) fall back to the legacy window (`VF_ComputeTerrainOpEnvelope`). `voxel.SpatialIndex` is -1 auto (off at fused LOD0, on elsewhere), 0 or 1. **Spatial index:** ordered XY buckets over cache primitives, exact rejection. The `static_assert` in `VoxelStrateTypes.h` ties `VF_STRATE_PARAM_FIELDS` to `FStrateGenerationParams` (83 fields + 8 B padding). |
|
||||||
| `GenerateTileResult` | — | **Shared worker-side gen** for one tile (async `LoadTile` task + sync `SyncRemeshTile`): [ClassifyTile (T1.d) + exact validation, only when `voxel.OuterClassifierMode=1`; **default 0 since 2026-09-13**] → `GenerateMesh`/`GenerateSheetMesh` → `BuildTileStreamSet`; records vertex count and resident stream allocation bytes for the horizon trace. Reads Generator/Mesher only → safe on a worker or the game thread; fills `FChunkResult`, no enqueue. |
|
| `GenerateTileResult` | — | **Shared worker-side gen** for one tile (async `LoadTile` task + sync `SyncRemeshTile`): [ClassifyTile (T1.d) + exact validation, only when `voxel.OuterClassifierMode=1`; **default 0 since 2026-09-13**] → `GenerateMesh` → `BuildTileStreamSet`; records vertex count and resident stream allocation bytes for the horizon trace. Reads Generator/Mesher only → safe on a worker or the game thread; fills `FChunkResult`, no enqueue. |
|
||||||
| `RecordTileHash` / `WriteTileHashDump` | — | Headless `-voxel.TileHashDump=<absolute path>` parity instrument. Records the exact `FVoxelMeshData` used by the streaming result, including empty/classified tiles, then writes sorted absolute tile keys, LOD step/cells, content-band limits, settings digest context, and the shared `VoxelGeometryHash` at `EndPlay`. |
|
| `RecordTileHash` / `WriteTileHashDump` | — | Headless `-voxel.TileHashDump=<absolute path>` parity instrument. Records the exact `FVoxelMeshData` used by the streaming result, including empty/classified tiles, then writes sorted absolute tile keys, LOD step/cells, content-band limits, settings digest context, and the shared `VoxelGeometryHash` at `EndPlay`. |
|
||||||
| `SyncRemeshTile` | — | **INSTANT DIG**: level-0 same-frame re-mesh on the game thread (`GenerateTileResult` + `ApplyTileResult` inline, Cells=CHUNK_SIZE/Step=1 + strate band, no capture). Used for the tile under the brush centre so a carve is visible THIS frame; one full-res gen on the game thread. |
|
| `SyncRemeshTile` | — | **INSTANT DIG**: level-0 same-frame re-mesh on the game thread (`GenerateTileResult` + `ApplyTileResult` inline, Cells=CHUNK_SIZE/Step=1 + strate band, no capture). Used for the tile under the brush centre so a carve is visible THIS frame; one full-res gen on the game thread. |
|
||||||
| `UpdateChunksAroundPosition` | 362 | Builds desired set, cancels pending keys that left it, sorts from the **true pawn position**, submits the occupied/support LOD0 tile and the next tile along the pawn heading as an absolute `BackgroundHigh` prefix, then loads/unloads and handles LOD changes. **Delta cull**: `BuildDesiredTiles` returns the LEAVERS (stamped `DesiredStamped` map, one sweep) — only those + `TransitionHold` are considered per crossing, not every loaded tile. `BuildDesiredTiles` also applies `RenderDistanceChunks`: the outermost shell widens to cover the distance — as level-MaxClipLevel MC tiles, or (F18 `bFarSheetRing`) as a SHEET ring at level MaxClipLevel+span (covered-check vs the MaxClipLevel box; `VF_OuterShell` shared with `IsTileInClipRange` so the cull sees the same horizon), dz pre-clamped to the vertical band. **§9.3 anchors:** also prunes dead `StreamingAnchors` + detects their chunk crossings → rebuilds the desired set when an anchor moves/(un)registers (`bAnchorsMoved`/`bForceDesiredRebuild`), same cadence as player movement. §8.10. |
|
| `UpdateChunksAroundPosition` | 6870 | Builds desired set, cancels pending keys that left it, sorts from the **true pawn position**, submits the occupied/support LOD0 tile and the next tile along the pawn heading as an absolute `BackgroundHigh` prefix, then loads/unloads and handles LOD changes. **Delta cull**: `BuildDesiredTiles` returns the LEAVERS (stamped `DesiredStamped` map, one sweep) — only those + `TransitionHold` are considered per crossing, not every loaded tile. `BuildDesiredTiles` also applies `RenderDistanceChunks`: the outermost shell widens to cover the distance as level-MaxClipLevel MC tiles (`VF_OuterShell` shared with `IsTileInClipRange` so the cull sees the same horizon), dz pre-clamped to the vertical band. **§9.3 anchors:** also prunes dead `StreamingAnchors` + detects their chunk crossings → rebuilds the desired set when an anchor moves/(un)registers (`bAnchorsMoved`/`bForceDesiredRebuild`), same cadence as player movement. §8.10. |
|
||||||
| `AdvanceHeadlessCollisionGateStressTest` / `ObserveHeadlessCollisionGateStressTest` / `MaybeFinishHeadlessCollisionGateStressTest` | — | Explicit `-voxel.TestCollisionGateStress=1` harness only. The `GetDensityAt` column at capsule feet defines the local air/solid surface; each frame records actor position, velocity, direct foot tile and gate support readiness, gate state, signed feet clearance, and a ≤0.5-voxel sweep between positions. PASS requires no measured clearance below −5 cm; support submission/readiness, travel, and holds are diagnostics. Result logs include the terminal density column and the first crossing below spawn height. |
|
| `AdvanceHeadlessCollisionGateStressTest` / `ObserveHeadlessCollisionGateStressTest` / `MaybeFinishHeadlessCollisionGateStressTest` | — | Explicit `-voxel.TestCollisionGateStress=1` harness only. The `GetDensityAt` column at capsule feet defines the local air/solid surface; each frame records actor position, velocity, direct foot tile and gate support readiness, gate state, signed feet clearance, and a ≤0.5-voxel sweep between positions. PASS requires no measured clearance below −5 cm; support submission/readiness, travel, and holds are diagnostics. Result logs include the terminal density column and the first crossing below spawn height. |
|
||||||
| `BuildDesiredTiles` | ~722 | Builds `DesiredSorted`+`DesiredStamped` (player clipmap shells + F18 sheet ring) then `AddAnchorDesiredTiles()` before the leaver sweep; it orders from the true local pawn position and builds `CriticalDesiredTiles` for the occupied/support and heading tiles. Also rebuilds `DesiredTransitionDescendants` (ancestor → desired descendants) for the reconcile pass. |
|
| `BuildDesiredTiles` | 6482 | Builds `DesiredSorted`+`DesiredStamped` (player clipmap shells) then `AddAnchorDesiredTiles()` before the leaver sweep; it orders from the true local pawn position and builds `CriticalDesiredTiles` for the occupied/support and heading tiles. Also rebuilds `DesiredTransitionDescendants` (ancestor → desired descendants) for the reconcile pass. |
|
||||||
| `VoxelClipmapDesiredTiles::Build` / `AlignedRingBounds` (`Private/VoxelClipmapDesiredTiles.h`) | — | The pure clipmap selector. **Every finer ring snaps outward to a child-pair boundary** (`AlignedRingBounds`, alignment 2; the last MC ring aligns to the sheet grouping width), so a coarser tile is wholly inside or wholly outside the finer ring: **no two desired tiles of different levels overlap** (a 7-wide odd ring used to leave one half-covered coarse tile per axis side, drawn over the fine one: 635 pairs at the owner config). Cost: LOD0 ring 343 → 512 tiles. Guarded by `VoxelForge.Streaming.ClipmapDesiredTilesNoOverlapCoverage`. Anchor tiles appended later are outside this guarantee. |
|
| `VoxelClipmapDesiredTiles::Build` / `AlignedRingBounds` (`Private/VoxelClipmapDesiredTiles.h`) | — | The pure clipmap selector. **Every finer ring snaps outward to a child-pair boundary** (`AlignedRingBounds`, alignment 2; the outermost ring needs none), so a coarser tile is wholly inside or wholly outside the finer ring: **no two desired tiles of different levels overlap** (an unaligned 7-wide odd ring leaves one half-covered coarse tile per axis side, drawn over the fine one: 635 pairs at the owner config). Cost: LOD0 ring 343 → 512 tiles. Guarded by `VoxelForge.Streaming.ClipmapDesiredTilesNoOverlapCoverage`. Anchor tiles appended later are outside this guarantee. |
|
||||||
| `ReconcileReadyTransitionVisibility` | — | Visibility-only pass while streaming is unsettled: hides a retired tile once all desired tiles over its footprint are loaded, reveals new tiles once no visible retired tile overlaps them. **Indexed** (dyadic ancestors + `DesiredTransitionDescendants`, visible retired tiles collected once per pass): moving p95 32 ms → 0.31 ms. `voxel.MeasureReadyTransitionVisibility=1` logs per-frame samples. |
|
| `ReconcileReadyTransitionVisibility` | — | Visibility-only pass while streaming is unsettled: hides a retired tile once all desired tiles over its footprint are loaded, reveals new tiles once no visible retired tile overlaps them. **Indexed** (dyadic ancestors + `DesiredTransitionDescendants`, visible retired tiles collected once per pass): moving p95 32 ms → 0.31 ms. `voxel.MeasureReadyTransitionVisibility=1` logs per-frame samples. |
|
||||||
| `RequestCollisionGateSupportTile` / `UpdatePawnCollisionGate` | — | The gate submits the pawn's level-0 support tile **immediately** as `CollisionCritical`, even before the first desired set exists (the desired rebuild then adopts the same pending key). The gate holds downward Z only, with **no timeout** (the old 2 s release fired at every startup, while the game thread applied nothing for ~2.2 s). |
|
| `RequestCollisionGateSupportTile` / `UpdatePawnCollisionGate` | — | The gate submits the pawn's level-0 support tile **immediately** as `CollisionCritical`, even before the first desired set exists (the desired rebuild then adopts the same pending key). The gate holds downward Z only, with **no timeout** (a 2 s release would fire at every startup, while the game thread applies nothing for ~2.2 s). |
|
||||||
| `AddAnchorDesiredTiles` | — | **§9.3 multi-anchor:** folds each `FVoxelStreamingAnchor`'s thin level-0 box (`XYRadiusChunks`/`ZBelowChunks`/`ZAboveChunks`) into the SAME desired set (deduped by stamp) so AI/remote players keep collision loaded around them; delta cull releases them on move/unregister. **§9.4:** a tile only a CollisionOnly anchor wants (clipmap didn't stamp it) → `CollisionOnlyTiles` → hidden at apply. Zero cost when no anchors. |
|
| `AddAnchorDesiredTiles` | — | **§9.3 multi-anchor:** folds each `FVoxelStreamingAnchor`'s thin level-0 box (`XYRadiusChunks`/`ZBelowChunks`/`ZAboveChunks`) into the SAME desired set (deduped by stamp) so AI/remote players keep collision loaded around them; delta cull releases them on move/unregister. **§9.4:** a tile only a CollisionOnly anchor wants (clipmap didn't stamp it) → `CollisionOnlyTiles` → hidden at apply. Zero cost when no anchors. |
|
||||||
| `ReconcileAnchorTileVisibility` | — | **§9.4:** after each rebuild, toggle `SetVisibility` on ALREADY-LOADED tiles that flipped render↔collision-only (diff `CollisionOnlyTiles` vs prev — bounded, no O(loaded) scan; unhide only if still desired). No-op without CollisionOnly anchors. |
|
| `ReconcileAnchorTileVisibility` | — | **§9.4:** after each rebuild, toggle `SetVisibility` on ALREADY-LOADED tiles that flipped render↔collision-only (diff `CollisionOnlyTiles` vs prev — bounded, no O(loaded) scan; unhide only if still desired). No-op without CollisionOnly anchors. |
|
||||||
| `RegisterStreamingAnchor` / `UnregisterStreamingAnchor` | — | **BlueprintCallable §9.3:** add/remove an actor as a streaming anchor (`EVoxelAnchorPolicy` CollisionOnly/FullVisual + XY/ZBelow/ZAbove box). Idempotent; forces a rebuild next Tick. §9.4: CollisionOnly tiles cook collision but are hidden (no draw/VSM) unless the player clipmap wants them too. |
|
| `RegisterStreamingAnchor` / `UnregisterStreamingAnchor` | — | **BlueprintCallable §9.3:** add/remove an actor as a streaming anchor (`EVoxelAnchorPolicy` CollisionOnly/FullVisual + XY/ZBelow/ZAbove box). Idempotent; forces a rebuild next Tick. §9.4: CollisionOnly tiles cook collision but are hidden (no draw/VSM) unless the player clipmap wants them too. |
|
||||||
| `LoadTile` | 445 | Budget check → `UE::Tasks::Launch` background gen+mesh; RAII task guard. Worker runs `Generator->ClassifyTile` first (T1.d): AllSolid/AllAir ⇒ skip `GenerateMesh`, tile stays empty (capture tiles always generate). It carries `GenerationEpoch`/desired epoch and checks `bShuttingDown`, generation pause, and its read-only cancellation token between expensive stages; obsolete work returns through `ProcessQueue`. STRATE CONTENT CUT: tiles ≥ `StrateContentCutMinLevel` pass the player-strate band (`MeshBandChunkLo/Hi` → voxels) to `GenerateMesh` + stamp it on `FChunkResult::BandChunkLo/Hi`; band change re-queues via `BandRemeshQueue` (see `UpdateChunksAroundPosition`). TOO-COARSE SKIP: if one cell is taller than the band (`Step > band height` — level ≥7 territory) the tile is enqueued EMPTY without launching a task (cell-granular cut could only render garbage). F18: `Tile.Level > MaxClipLevel` = SHEET tile → routed to `GenerateSheetMesh` (band mid-chunk = strate ref; band unarmed ⇒ empty; MC-ring sampling density, cells capped 128/axis; carries the XY hole `SheetHole*Vox` — hole moves ⇒ overlapping sheets re-queue via `BandRemeshQueue`, see `UpdateChunksAroundPosition`). §8.10. |
|
| `LoadTile` | 7291 | Budget check → `UE::Tasks::Launch` background gen+mesh; RAII task guard. Worker runs `Generator->ClassifyTile` first (T1.d): AllSolid/AllAir ⇒ skip `GenerateMesh`, tile stays empty (capture tiles always generate). It carries `GenerationEpoch`/desired epoch and checks `bShuttingDown`, generation pause, and its read-only cancellation token between expensive stages; obsolete work returns through `ProcessQueue`. STRATE CONTENT CUT: tiles ≥ `StrateContentCutMinLevel` pass the player-strate band (`MeshBandChunkLo/Hi` → voxels) to `GenerateMesh` + stamp it on `FChunkResult::BandChunkLo/Hi`; band change re-queues via `BandRemeshQueue` (see `UpdateChunksAroundPosition`). TOO-COARSE SKIP: if one cell is taller than the band (`Step > band height` — level ≥7 territory) the tile is enqueued EMPTY without launching a task (cell-granular cut could only render garbage). §8.10. |
|
||||||
| `UnloadTile` | — | Clears tile state; marks any matching generation request obsolete without removing its pending slot, so a late result cannot clear a replacement request; the component is PARKED in the pool (T2.c), not destroyed. |
|
| `UnloadTile` | — | Clears tile state; marks any matching generation request obsolete without removing its pending slot, so a late result cannot clear a replacement request; the component is PARKED in the pool (T2.c), not destroyed. |
|
||||||
| `ApplyMeshToTile` | — | Upload geometry. One component per tile (clipmap keeps count low; supersedes the old region batching); worker-built streams (T1.f) → `CreateSectionGroup(MoveTemp)`. Reuses the component's existing `URealtimeMesh` (no per-apply mesh alloc). **F17: two polygroups** (0 ground / 1 sky-cap, per-triangle class from the mesher) → RMC auto-section per non-empty group; slot 0 = override/default material, slot 1 = `CeilingMaterial` (fallback ground); config gated by `FChunkResult::bHasGroundTris/bHasCeilingTris`. Takes `FChunkResult&`; strate lookups clamp Z into `Result.BandChunkLo/Hi` (strate content cut) — ground at clamped bottom chunk, cap at clamped top (mid = gap fallback). Collision level-0 only (T1.c) both groups; shadow per SECTION: ground casts at level≤1, cap never. **§9.4:** `SetVisibility(false)` when the tile is in `CollisionOnlyTiles` (anchor-only, hidden — collision still cooks). §8.10 + ARCHITECTURE SurfaceWorld row. |
|
| `ApplyMeshToTile` | — | Upload geometry. One component per tile (clipmap keeps count low; supersedes the old region batching); worker-built streams (T1.f) → `CreateSectionGroup(MoveTemp)`. Reuses the component's existing `URealtimeMesh` (no per-apply mesh alloc). **F17: two polygroups** (0 ground / 1 sky-cap, per-triangle class from the mesher) → RMC auto-section per non-empty group; slot 0 = override/default material, slot 1 = `CeilingMaterial` (fallback ground); config gated by `FChunkResult::bHasGroundTris/bHasCeilingTris`. Takes `FChunkResult&`; strate lookups clamp Z into `Result.BandChunkLo/Hi` (strate content cut) — ground at clamped bottom chunk, cap at clamped top (mid = gap fallback). Collision level-0 only (T1.c) both groups; shadow per SECTION: ground casts at level≤1, cap never. **§9.4:** `SetVisibility(false)` when the tile is in `CollisionOnlyTiles` (anchor-only, hidden — collision still cooks). §8.10 + ARCHITECTURE SurfaceWorld row. |
|
||||||
| `AcquireTileComponent` / `ReleaseTileComponent` | — | **T2.c component pool** (`TileComponentPool`, bounded): park on unload (geometry+collision stripped, hidden, stays registered), pop on apply — no `NewObject`/`RegisterComponent`/GC churn during travel & regen. §8.10. |
|
| `AcquireTileComponent` / `ReleaseTileComponent` | — | **T2.c component pool** (`TileComponentPool`, bounded): park on unload (geometry+collision stripped, hidden, stays registered), pop on apply — no `NewObject`/`RegisterComponent`/GC churn during travel & regen. §8.10. |
|
||||||
| `GetStrateAtPosition` | 965 | Gameplay query → strate index. |
|
| `GetStrateAtPosition` | 8351 | Gameplay query → strate index. |
|
||||||
| `GetBiomeAtWorldLocation` | — | **BlueprintCallable** biome probe at a world point (undoes actor xf → voxel → `Generator::QueryBiomeAt`). Returns `FVoxelBiomeQuery` for BP debug ("what biome / how many decos under the cursor?"). |
|
| `GetBiomeAtWorldLocation` | — | **BlueprintCallable** biome probe at a world point (undoes actor xf → voxel → `Generator::QueryBiomeAt`). Returns `FVoxelBiomeQuery` for BP debug ("what biome / how many decos under the cursor?"). |
|
||||||
| `GetVoxelSurfaceHeightAt` | ~1534 | **BlueprintCallable** ground finder (F7 bridge): world XY → terrain + sky-cap world-Z via `Generator::GetSurfaceHeightAt`, NO trace/collision, deterministic, available before the area meshes → self-arranging prefab/ruin BPs snap their parts to the real ground. False (outs=input Z) on non-SurfaceWorld strates; ignores passage/spine carving. |
|
| `GetVoxelSurfaceHeightAt` | 8385 | **BlueprintCallable** ground finder (F7 bridge): world XY → terrain + sky-cap world-Z via `Generator::GetSurfaceHeightAt`, NO trace/collision, deterministic, available before the area meshes → self-arranging prefab/ruin BPs snap their parts to the real ground. False (outs=input Z) on non-SurfaceWorld strates; ignores passage/spine carving. |
|
||||||
| `CarveAtPosition` / `FillAtPosition` | 691 / 709 | Build `FVoxelModification` → `ApplyModification`. |
|
| `CarveAtPosition` / `FillAtPosition` | 8413 / 8422 | Build `FVoxelModification` → `ApplyModification`. |
|
||||||
| `ApplyModification` | ~1581 | Single funnel for all brushes: DiffLayer → **sync-remesh the brush-centre level-0 tile** (`SyncRemeshTile`, instant hole; skipped if that tile is mid-gen — would race a stale in-flight result) → `RemeshDirtyChunks(..., excludeCenter)` for the neighbours → `RemoveDecorationsInSphere`. |
|
| `ApplyModification` | 8431 | Single funnel for all brushes: DiffLayer → **sync-remesh the brush-centre level-0 tile** (`SyncRemeshTile`, instant hole; skipped if that tile is mid-gen — would race a stale in-flight result) → `RemeshDirtyChunks(..., ExcludeTile)` for the neighbours → `RemoveDecorationsInSphere`. |
|
||||||
| `ClearAllModifications` | 726 | Clears diff layer, regenerates. |
|
| `ClearAllModifications` | 8696 | Clears diff layer, regenerates. |
|
||||||
| `ChangeSeed` | 740 | **Legacy season reset**: new seed everywhere, clear diffs, bump season, reload. Rejected while a cooked season owns the seed. |
|
| `ChangeSeed` | 8710 | **Legacy season reset**: new seed everywhere, clear diffs, bump season, reload. Rejected while a cooked season owns the seed. |
|
||||||
| `GetCurrentSeed` / `GetCurrentSeason` / `GetCurrentSeasonContentHash` | — | Effective cooked-or-legacy identity; the hash is the join-time stale-season comparison value. |
|
| `GetCurrentSeed` / `GetCurrentSeason` / `GetCurrentSeasonContentHash` | — | Effective cooked-or-legacy identity; the hash is the join-time stale-season comparison value. |
|
||||||
| `RemeshDirtyChunks` | 798 | Queue loaded level-0 dirty tiles onto `DirtyRemeshQueue` (async re-mesh, no pop) + `MarkDirtyVoxelBox` the volume. Optional `ExcludeTile` = the sync'd centre. Drained FIRST in the submit loop at **BackgroundHigh** (ahead of streaming/band) so a dig never waits behind streaming; in-flight tiles stay QUEUED (not dropped) so a stale pre-carve result is corrected once it lands — fixes "hole shows up a beat late / not until I move". |
|
| `RemeshDirtyChunks` | 8801 | Queue loaded level-0 dirty tiles onto `DirtyRemeshQueue` (async re-mesh, no pop) + `MarkDirtyVoxelBox` the volume. Optional `ExcludeTile` = the sync'd centre. Drained FIRST in the submit loop at **BackgroundHigh** (ahead of streaming/band) so a dig never waits behind streaming; in-flight tiles stay QUEUED (not dropped) so a stale pre-carve result is corrected once it lands — fixes "hole shows up a beat late / not until I move". |
|
||||||
|
|
||||||
> **Game-thread profiling (Perf):** `AVoxelWorld::Tick` and its sub-steps are wrapped in `TRACE_CPUPROFILER_EVENT_SCOPE` — `VoxelForge_Tick / UpdateChunks / BuildDesiredTiles / CullTiles / SubmitTiles / ProcessPending / ProcessUnload / UpdateDecorations / UpdateWater`. Capture a `Count/Incl/Excl` Insights timer export and read the `Excl` column to see which step owns the per-frame cost (the actor tick shows as `BP_VoxelWorld_C` if subclassed in BP). `VoxelForge_ClassifyTile` (T1.d) / `VoxelForge_GenerateMesh` + `VoxelForge_BuildStreams` are worker-side (off the frame): the RMC `FRealtimeMeshStreamSet` is now built on the gen worker (`BuildTileStreamSet`) and carried on `FChunkResult::Streams` (TSharedPtr), so `ApplyMeshToTile` is game-thread-cheap — just material/ceiling resolve + `CreateSectionGroup(MoveTemp)`. See ARCHITECTURE §8.10 "Worker-built StreamSet (T1.f)".
|
> **Game-thread profiling (Perf):** `AVoxelWorld::Tick` and its sub-steps are wrapped in `TRACE_CPUPROFILER_EVENT_SCOPE` — `VoxelForge_Tick / UpdateChunks / BuildDesiredTiles / CullTiles / SubmitTiles / ProcessPending / ProcessUnload / UpdateDecorations / UpdateWater`. Capture a `Count/Incl/Excl` Insights timer export and read the `Excl` column to see which step owns the per-frame cost (the actor tick shows as `BP_VoxelWorld_C` if subclassed in BP). `VoxelForge_ClassifyTile` (T1.d) / `VoxelForge_GenerateMesh` + `VoxelForge_BuildStreams` are worker-side (off the frame): the RMC `FRealtimeMeshStreamSet` is now built on the gen worker (`BuildTileStreamSet`) and carried on `FChunkResult::Streams` (TSharedPtr), so `ApplyMeshToTile` is game-thread-cheap — just material/ceiling resolve + `CreateSectionGroup(MoveTemp)`. See ARCHITECTURE §8.10 "Worker-built StreamSet (T1.f)".
|
||||||
|
|
||||||
@@ -343,31 +340,30 @@ This is **where terrain shape lives.**
|
|||||||
| Symbol | .cpp line | Role |
|
| Symbol | .cpp line | Role |
|
||||||
|--------|-----------|------|
|
|--------|-----------|------|
|
||||||
| `UVoxelGenerator` / `DensityCacheOwnerId` | — | Constructor allocates a process-unique integer identity (relaxed atomic, once per object). `GetDensityAt` includes it in the `CP_*` thread-local key, preventing a worker from serving another generator/world's params, biome context, `CP_UseOpStack`, or stack when `(ChunkCoord, LayoutVersion)` happens to match. Hot-path cost: one `uint64` compare per voxel. Scope is deliberately only the proved `CP_*` path. |
|
| `UVoxelGenerator` / `DensityCacheOwnerId` | — | Constructor allocates a process-unique integer identity (relaxed atomic, once per object). `GetDensityAt` includes it in the `CP_*` thread-local key, preventing a worker from serving another generator/world's params, biome context, `CP_UseOpStack`, or stack when `(ChunkCoord, LayoutVersion)` happens to match. Hot-path cost: one `uint64` compare per voxel. Scope is deliberately only the proved `CP_*` path. |
|
||||||
| `FractalNoise3D` (static) | 25 | fBM (layered Perlin). |
|
| `FractalNoise3D` (static) | 931 | fBM (layered Perlin). |
|
||||||
| `RidgedNoise3D` (static) | 55 | Ridged multifractal — craggy. |
|
| `RidgedNoise3D` (static) | 949 | Ridged multifractal — craggy. |
|
||||||
| `CellularNoise3D` (static) | 101 | Worley/cellular — grotto/scallop. |
|
| `CellularNoise3D` (static) | 976 | Worley/cellular — grotto/scallop. |
|
||||||
| `ApplyBoundarySeal` (static) | 170 | Solidifies strate top/bottom shells. |
|
| `ApplyBoundarySeal` (static) | 996 | Solidifies strate top/bottom shells. |
|
||||||
| `ApplyPassageCarving` (static) | 197 | Punches passages/elevator through the seal. |
|
| `InitializeSettings` | 2691 | Copies the effective seed, spine radius, and global XY world-bound settings from `UVoxelSettings` (the season owns all three when assigned); radius 0 preserves the unbounded field. |
|
||||||
| `InitializeSettings` | 211 | Copies the effective seed, spine radius, and global XY world-bound settings from `UVoxelSettings` (the season owns all three when assigned); radius 0 preserves the unbounded field. |
|
| **`GetDensityAt`** | 2712 | **Entry point.** Picks strate + generator type and, on the per-chunk refetch, builds the chunk's operator stack (editor composer → cooked-season recipe → native archetype stack); evaluates it (TunnelNetwork/Underwater through the fused `GetDensityWithParams`), then adds disturbances, the structural tail and the diff layer. A degenerate strate has no stack and is air. Its `CP_*` state is keyed by `(DensityCacheOwnerId, ChunkCoord, LayoutVersion)`; workers copy immutable recipe/vector data and never read editor composer state. |
|
||||||
| **`GetDensityAt`** | 218 | **Entry point.** Picks strate + generator type, materialises a cooked-season recipe on the existing per-chunk refetch when present, dispatches, then adds disturbances/diff. Its `CP_*` state is keyed by `(DensityCacheOwnerId, ChunkCoord, LayoutVersion)`; workers copy immutable recipe/vector data and never read editor composer state. |
|
| **`GetDensityWithParams`** | 3749 | TunnelNetwork pipeline (~1000 lines) — the fused evaluator of the TunnelNetwork/Underwater stack in `GetDensityAt`. See §4. ⚠️ Takes **required** `ParamsFingerprint` + `LayoutVersion` since the AUDIT §C2 fix (2026-07-28) — they go into the SDF cache key so a chunk can no longer be evaluated against a neighbour's rooms. Callers compute the CRC **once per chunk** (`CP_TunnelFP`), never per voxel. The optional `voxel.WormBlockSkip` path (default 0) uses `VF_TryGetWormBlockSkip` + a worker-local 4³ lattice-block cache to prove `|N1| >= WormThreshold` and remove both worm noise calls only; it never changes `NetworkMask` or the fallback field path. |
|
||||||
| **`GetDensityWithParams`** | 277 | TunnelNetwork pipeline (~1000 lines). See §4. ⚠️ Takes **required** `ParamsFingerprint` + `LayoutVersion` since the AUDIT §C2 fix (2026-07-28) — they go into the SDF cache key so a chunk can no longer be evaluated against a neighbour's rooms. Callers compute the CRC **once per chunk** (`CP_TunnelFP`), never per voxel. The optional `voxel.WormBlockSkip` path (default 0) uses `VF_TryGetWormBlockSkip` + a worker-local 4³ lattice-block cache to prove `|N1| >= WormThreshold` and remove both worm noise calls only; it never changes `NetworkMask` or the fallback field path. |
|
|
||||||
| **`GetSlabDensity`** | 1306 | FlatPlain/CrystalChamber pipeline. See §4.2. |
|
|
||||||
| `SampleSurfaceStructuralZ` | — | **F20:** the RAW SurfaceWorld heightfield (continents+mountains+detail), BEFORE any terrain op; returns terrain Z + relief M. Cliff re-samples it at an XY offset for a cheap analytic slope. |
|
| `SampleSurfaceStructuralZ` | — | **F20:** the RAW SurfaceWorld heightfield (continents+mountains+detail), BEFORE any terrain op; returns terrain Z + relief M. Cliff re-samples it at an XY offset for a cheap analytic slope. |
|
||||||
| `ComputeSurfaceTerrainZ` / `GetSurfaceDensity` | — | SurfaceWorld heightfield → terrain Z, then density; biome **output-blend** lerps dominant/neighbour heights (`ParamsD`/`ParamsN`/weight). **F20 surface ops** (`FSurfaceGenerationParams`, biome-selected + slope/relief-conditioned, all default off): Cliff (slope-gated STEEPENING — push height from local mean where steep ⇒ sheer walls; 4 structural resamples only when on), Terrace (relief-gated + `TerraceHardness`), LayerLines (sedimentary shelves) — pure per-column height REMAPS applied here so the single height oracle stays consistent (MC/sheets/ClassifyTile/deco/BP bridge). **Phase 2 OVERHANG** (volumetric — real jutting shelves): in `SurfaceDensityFromColumn`, for AIR voxels in a window `(TerrainZ, TerrainZ+OverhangHeight]` above a steep slope, the heightfield is re-sampled UPHILL (toward the cliff) by a reach that GROWS with height (tiny low ⇒ air over the void, full high ⇒ borrows the far cliff rock) and unioned in ⇒ a shelf attached to the cliff, tapering out over the void with air beneath (the sketch). Per-column `OverhangAmp`(=strength·slope-gate) + unit uphill `(DirX,DirY)` resolved once in `ComputeSurfaceColumn` (gradient sampled at the REACH scale so a spot over the void can see the cliff), cached on `FSurfaceColumn`. Genuine 3D (per-voxel structural re-eval, gated to steep overhang columns). Off ⇒ byte-identical. The field-preserving LOD0 pass removes only needless temporaries/default construction; every output remains bit-identical. §8.14. |
|
| `ComputeSurfaceTerrainZ` / `ComputeSurfaceColumn` | — | SurfaceWorld heightfield → terrain Z (+ sky-cap ceiling and overhang gate per column); biome **output-blend** lerps dominant/neighbour heights (`ParamsD`/`ParamsN`/weight). **F20 surface ops** (`FSurfaceGenerationParams`, biome-selected + slope/relief-conditioned, all default off): Cliff (slope-gated STEEPENING — push height from local mean where steep ⇒ sheer walls; 4 structural resamples only when on), Terrace (relief-gated + `TerraceHardness`), LayerLines (sedimentary shelves) — pure per-column height REMAPS applied here so the single height oracle stays consistent (MC/ClassifyTile/deco/BP bridge). **Phase 2 OVERHANG** (volumetric — real jutting shelves): in the stack's `FOverhangShelfMod`, for AIR voxels in a window `(TerrainZ, TerrainZ+OverhangHeight]` above a steep slope, the heightfield is re-sampled UPHILL (toward the cliff) by a reach that GROWS with height (tiny low ⇒ air over the void, full high ⇒ borrows the far cliff rock) and unioned in ⇒ a shelf attached to the cliff, tapering out over the void with air beneath (the sketch). Per-column `OverhangAmp`(=strength·slope-gate) + unit uphill `(DirX,DirY)` resolved once in `ComputeSurfaceColumn` (gradient sampled at the REACH scale so a spot over the void can see the cliff), cached on `FSurfaceColumn`. Genuine 3D (per-voxel structural re-eval, gated to steep overhang columns). Off ⇒ byte-identical. The field-preserving LOD0 pass removes only needless temporaries/default construction; every output remains bit-identical. §8.14. |
|
||||||
| `VF_BuildOpStackForChunk` (file-static) | — | **The archetype → stack mapping, written down once.** `GetDensityAt` and `ClassifyTile` both call it; params are passed in, never fetched here. A second copy would be the worst bug available in this file — a tile skipped on the verdict of a stack that is not the one producing its density is a hole. Returns false (⇒ caller falls back to the `switch`) for an unported archetype, missing params, or a **degenerate strate**, since five archetype functions early-out to air there and the stack deliberately has no such early-out. `Refs.Surface == nullptr` makes it refuse SurfaceWorld, which is how `ClassifyTile` keeps its own exact-lattice proof. |
|
| `VF_BuildOpStackForChunk` (file-static) | — | **The archetype → stack mapping, written down once.** `GetDensityAt` and `ClassifyTile` both call it; params are passed in, never fetched here. A second copy would be the worst bug available in this file — a tile skipped on the verdict of a stack that is not the one producing its density is a hole. Returns false for an unknown archetype, missing params, or a **degenerate strate** (the stack deliberately has no degenerate early-out): `GetDensityAt` then returns air and `ClassifyTile` returns `Mixed`. `Refs.Surface == nullptr` makes it refuse SurfaceWorld, which is how `ClassifyTile` keeps its own exact-lattice proof. |
|
||||||
| `ClassifyTile` | — | **T1.d trivial-tile reject. ⛔ Not called by streaming by default since 2026-09-13** (`voxel.OuterClassifierMode=0`; net-negative in game, see ARCHITECTURE §8.10). Cooked/editor recipes now build the exact recipe stack and call its proved `ClassifyBox`; a tile crossing a slot/gap, any diff, or any disturbance returns `Mixed`. This replaces the old global `HasComposerRecipeOverride()` force-to-Mixed. Native cave/surface/gap proofs and all §8.10 caches remain unchanged. |
|
| `ClassifyTile` | — | **T1.d trivial-tile reject. ⛔ Not called by streaming by default since 2026-09-13** (`voxel.OuterClassifierMode=0`; net-negative in game, see ARCHITECTURE §8.10). Cooked/editor recipes now build the exact recipe stack and call its proved `ClassifyBox`; a tile crossing a slot/gap, any diff, or any disturbance returns `Mixed`. This replaces the old global `HasComposerRecipeOverride()` force-to-Mixed. Native cave/surface/gap proofs and all §8.10 caches remain unchanged. |
|
||||||
| `SampleRelief` / `SampleMoisture` | — | Climate fields (pure XY, [0,1]). Relief = shared source of truth for the relief map M. §8.14. |
|
| `SampleRelief` / `SampleMoisture` | — | Climate fields (pure XY, [0,1]). Relief = shared source of truth for the relief map M. §8.14. |
|
||||||
| `SampleBiomeAt` | — | Warped-Voronoi + climate biome query (dominant + neighbour + weight). Reference used by the preview bake + `GetDominantBiomeAt`. §8.14. |
|
| `SampleBiomeAt` | — | Warped-Voronoi + climate biome query (dominant + neighbour + weight). Reference used by the preview bake + `GetDominantBiomeAt`. §8.14. |
|
||||||
| `ResolveBiomeSampleAt` / `RebuildBiomeGrid` | — | Hot-path biome resolve (FBiomeSample) via a box-validated per-chunk cell-grid cache. Bit-identical to `SampleBiomeAt`. §8.14, §8.10. |
|
| `ResolveBiomeSampleAt` / `RebuildBiomeGrid` | — | Hot-path biome resolve (FBiomeSample) via a box-validated per-chunk cell-grid cache. Bit-identical to `SampleBiomeAt`. §8.14, §8.10. |
|
||||||
| `GetDominantBiomeAt` | — | Game-thread query → dominant biome ASSET (content/atmosphere). §8.14. |
|
| `GetDominantBiomeAt` | — | Game-thread query → dominant biome ASSET (content/atmosphere). §8.14. |
|
||||||
| `QueryBiomeAt` | — | Rich game-thread biome probe → `FVoxelBiomeQuery` (dominant/neighbour asset, relief/moisture, blend weight, dominant deco count). Diagnostic behind `AVoxelWorld::GetBiomeAtWorldLocation`. §8.14. |
|
| `QueryBiomeAt` | — | Rich game-thread biome probe → `FVoxelBiomeQuery` (dominant/neighbour asset, relief/moisture, blend weight, dominant deco count). Diagnostic behind `AVoxelWorld::GetBiomeAtWorldLocation`. §8.14. |
|
||||||
| `EvaluateTerrainConditions` | ~2548 | **F7 aware placement:** AND-evaluate an entry's `FTerrainCondition[]` (relief/moisture/biome-border) at a candidate voxel XY. Empty = true (zero cost). Pure query (SampleRelief/SampleMoisture/SampleBiomeAt) → worker-safe + game-thread; caller passes the strate's `FBiomeContext` (freq/contrast + Voronoi map). Consumed by deco `BuildCellSpawns` + `SpawnLandmarkInstance`; shared core of the future quest FindFeature locator. |
|
| `EvaluateTerrainConditions` | 6973 | **F7 aware placement:** AND-evaluate an entry's `FTerrainCondition[]` (relief/moisture/biome-border) at a candidate voxel XY. Empty = true (zero cost). Pure query (SampleRelief/SampleMoisture/SampleBiomeAt) → worker-safe + game-thread; caller passes the strate's `FBiomeContext` (freq/contrast + Voronoi map). Consumed by deco `BuildCellSpawns` + `SpawnLandmarkInstance`; shared core of the future quest FindFeature locator. |
|
||||||
|
|
||||||
> **Per-voxel hot-path memos (perf pass 2, all bit-identical — same hashes/math, hoisted per
|
> **Per-voxel hot-path memos (perf pass 2, all bit-identical — same hashes/math, hoisted per
|
||||||
> chunk/cell):** `GetDensityAt` uses the DiffLayer snapshot cache (§3.9); `GetDensityWithParams`
|
> chunk/cell):** `GetDensityAt` uses the DiffLayer snapshot cache (§3.9); `GetDensityWithParams`
|
||||||
> memoizes the strate index per (chunkZ, `GetLayoutVersion()`); `thread_local` per-cell lattice
|
> memoizes the strate index per (chunkZ, `GetLayoutVersion()`); `thread_local` per-cell lattice
|
||||||
> bakes cover slab columns (`GetSlabDensity` step 4), maze open edges, vertical shafts +
|
> bakes cover slab columns (`FGridColumnMod`), maze open edges (`FLatticeCorridorSource`), vertical
|
||||||
> cross-connectors, floating-island constants, and the disturbance chasms/bridges/ridges; worm
|
> shafts + cross-connectors (`FShaftFieldSource`), floating-island constants (`FIslandBlobSource`),
|
||||||
|
> and the disturbance chasms/bridges/ridges; worm
|
||||||
> tunnels short-circuit the 2nd Perlin when N1 ≥ WormThreshold (N2 ≥ 0 ⇒ can't carve). With
|
> tunnels short-circuit the 2nd Perlin when N1 ≥ WormThreshold (N2 ≥ 0 ⇒ can't carve). With
|
||||||
> `voxel.WormBlockSkip=1`, a worker-local cache proves whole 4³ lattice blocks only when the
|
> `voxel.WormBlockSkip=1`, a worker-local cache proves whole 4³ lattice blocks only when the
|
||||||
> actual 3D Perlin bound (`L = 8.118988160`, including `VOXEL_NOISE_SCALE`) plus a `1e-3`
|
> actual 3D Perlin bound (`L = 8.118988160`, including `VOXEL_NOISE_SCALE`) plus a `1e-3`
|
||||||
@@ -438,22 +434,21 @@ is the shared digest used by `Tools/VoxelForgeTest.ps1 -Scenario parity`.
|
|||||||
### 3.7 Cave morphology (SDF rooms/tunnels) — `Public/VoxelCaveMorphology.h` + `.cpp`
|
### 3.7 Cave morphology (SDF rooms/tunnels) — `Public/VoxelCaveMorphology.h` + `.cpp`
|
||||||
Header is rich with inline docs. Two namespaces + a per-chunk cache system.
|
Header is rich with inline docs. Two namespaces + a per-chunk cache system.
|
||||||
|
|
||||||
- `namespace VoxelSDF` (h:46): `Sphere`, `Ellipsoid`, `Capsule`, `RoundedBox`,
|
- `namespace VoxelSDF` (h:78): `Sphere`, `Ellipsoid`, `Capsule`, `RoundedBox`,
|
||||||
`TaperedCapsule`, `SmoothMin`, `SmoothMax` — all FORCEINLINE SDF primitives.
|
`TaperedCapsule`, `SmoothMin`, `SmoothMax` — all FORCEINLINE SDF primitives.
|
||||||
- `namespace VoxelHash` (h:158): `Mix`, `Cell`, `Pair`, `ToFloat01`, `ToFloatSigned`
|
- `namespace VoxelHash` (h:210): `Mix`, `Cell`, `Pair`, `ToFloat01`, `ToFloatSigned`
|
||||||
— deterministic hashing for room/tunnel placement (no storage, infinite worlds).
|
— deterministic hashing for room/tunnel placement (no storage, infinite worlds).
|
||||||
- Cache structs (h:224-330): `FCachedRoom`, `FCachedTunnel`, `FCachedPit`,
|
- Cache structs (h:520-950): `FCachedRoom`, `FCachedTunnel`, `FCachedPit`,
|
||||||
`FCachedChimney`, `FCachedColumn`, `FChunkSDFCache`.
|
`FCachedChimney`, `FCachedColumn`, `FChunkSDFCache`.
|
||||||
- `namespace VoxelCaveMorphology`:
|
- `namespace VoxelCaveMorphology`:
|
||||||
| Function | .cpp line | Role |
|
| Function | .cpp line | Role |
|
||||||
|----------|-----------|------|
|
|----------|-----------|------|
|
||||||
| `BuildChunkCache` | 47 | **Phase 1** (once/chunk): collect rooms, guaranteed backbone (`bTunnelsFlowTowardOrigin`: tree rooted at the (0,0) hub — every room reachable, links flow inward; false = legacy NN forest), slope-aware link metric (`TunnelHorizontalBias` now applies to backbone too), decide tunnels, **cull zero-connection rooms** (no sealed bubbles), store rooms by their OWN reach (fixes origin-room clipping at `MaxInfluence`), pre-bake pits/chimneys/columns via the shared `BakeRoomFeature` hash-placement skeleton (one gate/XY/radius pattern + per-type Emit lambda), hash-roll per-room terrain op. |
|
| `BuildChunkCache` | 6782 | **Phase 1** (once/chunk): collect rooms, guaranteed backbone (`bTunnelsFlowTowardOrigin`: tree rooted at the (0,0) hub — every room reachable, links flow inward; false = legacy NN forest), slope-aware link metric (`TunnelHorizontalBias` now applies to backbone too), decide tunnels, **cull zero-connection rooms** (no sealed bubbles), store rooms by their OWN reach (fixes origin-room clipping at `MaxInfluence`), pre-bake pits/chimneys/columns via the shared `BakeRoomFeature` hash-placement skeleton (one gate/XY/radius pattern + per-type Emit lambda), hash-roll per-room terrain op. |
|
||||||
| `EvaluateSDFCached` | 757 | **Phase 2** (per voxel): SmoothMin over cached rooms/tunnels; returns nearest room idx for terrain-op lookup. **Signature changed (perf pass 2): `RoomShapeVariety` param REMOVED** — the shape roll + capsule trig are pre-baked into `FCachedRoom` (`ShapeType/ShapeA/ShapeB/ShapeR`) by `BuildChunkCache`, bit-identical. |
|
| `EvaluateSDFCached` | 8569 | **Phase 2** (per voxel): SmoothMin over cached rooms/tunnels; returns nearest room idx for terrain-op lookup. **Signature changed (perf pass 2): `RoomShapeVariety` param REMOVED** — the shape roll + capsule trig are pre-baked into `FCachedRoom` (`ShapeType/ShapeA/ShapeB/ShapeR`) by `BuildChunkCache`, bit-identical. |
|
||||||
| `EvaluateSDF` | 738 | Convenience wrapper (builds temp cache) for one-off queries. |
|
|
||||||
|
|
||||||
`MakeStrateSeed` (h:~477) is the shared pure world-seed/strate-index salt used by both
|
`MakeStrateSeed` (h:965) is the shared pure world-seed/strate-index salt used by both
|
||||||
`BuildChunkCache` and the TunnelNetwork/Underwater destination landing queries.
|
`BuildChunkCache` and the TunnelNetwork/Underwater destination landing queries.
|
||||||
`VF_SuggestLandingPoint` (h:~550, implemented in `VoxelCaveMorphology.cpp`) is the pure player-fit
|
`VF_SuggestLandingPoint` (h:1121, implemented in `VoxelCaveMorphology.cpp`) is the pure player-fit
|
||||||
landing-site query: it evaluates the local archetype source through the same capsule/support
|
landing-site query: it evaluates the local archetype source through the same capsule/support
|
||||||
contract as the measurement pass — support within step height, walkable slope, and full capsule
|
contract as the measurement pass — support within step height, walkable slope, and full capsule
|
||||||
clearance. Room queries use a room-floor core, slabs keep their requested XY, and the sparse
|
clearance. Room queries use a room-floor core, slabs keep their requested XY, and the sparse
|
||||||
@@ -467,7 +462,7 @@ SurfaceWorld remains refused because its production height can depend on manager
|
|||||||
context. It never touches a generator, operator stack, manager, cache, or mutable state.
|
context. It never touches a generator, operator stack, manager, cache, or mutable state.
|
||||||
|
|
||||||
`FVoxelPassageLanding`, `VF_BuildPassageLanding`, `VF_EvaluatePassageLandingSDF`, and
|
`FVoxelPassageLanding`, `VF_BuildPassageLanding`, `VF_EvaluatePassageLandingSDF`, and
|
||||||
`VF_IsPassageLandingFloor` (h:~56/~189, implemented near `.cpp:2059`) turn each inter-strate mouth
|
`VF_IsPassageLandingFloor` (h:57/184-199, implemented near `.cpp:6468`) turn each inter-strate mouth
|
||||||
into a deterministic rounded room plus a hard support slab. The room is sized from the player
|
into a deterministic rounded room plus a hard support slab. The room is sized from the player
|
||||||
capsule (minimum 12 voxels / 3 m flat floor, 12 voxels / 3 m clear height, and at least 14 voxels /
|
capsule (minimum 12 voxels / 3 m flat floor, 12 voxels / 3 m clear height, and at least 14 voxels /
|
||||||
3.5 m authored width); the `(0,0)` origin landing is enlarged for the reserved future shaft, while
|
3.5 m authored width); the `(0,0)` origin landing is enlarged for the reserved future shaft, while
|
||||||
@@ -476,62 +471,61 @@ The manager's shared thread-local passage shortlist serves tube, room, and floor
|
|||||||
landing search occurs per voxel. `GeneratePassages` stores the full descriptors and the
|
landing search occurs per voxel. `GeneratePassages` stores the full descriptors and the
|
||||||
order-independence test compares them bit-for-bit.
|
order-independence test compares them bit-for-bit.
|
||||||
|
|
||||||
Performance note (h:209-220): caching rooms/tunnels once per chunk instead of per
|
Performance note (h:520-531): caching rooms/tunnels once per chunk instead of per
|
||||||
voxel is the single biggest CPU win.
|
voxel is the single biggest CPU win.
|
||||||
|
|
||||||
### 3.8 Strate system
|
### 3.8 Strate system
|
||||||
**`Public/VoxelStrateTypes.h`** — shared structs/enums (1228 lines, the data vocabulary):
|
**`Public/VoxelStrateTypes.h`** — shared structs/enums (1228 lines, the data vocabulary):
|
||||||
| Symbol | Line | Role |
|
| Symbol | Line | Role |
|
||||||
|--------|------|------|
|
|--------|------|------|
|
||||||
| `EVoxelPassageType` | 38 | Sloped/Vertical/Spiral/Cascading/Crack passage shapes. |
|
| `EVoxelPassageType` | 42 | Sloped/Vertical/Spiral/Cascading/Crack passage shapes. |
|
||||||
| `ESurfaceType` | 78 | Floor/Wall/Ceiling/Any (decoration placement). |
|
| `ESurfaceType` | 81 | Floor/Wall/Ceiling/Any (decoration placement). |
|
||||||
| `EVoxelNoiseType` | 99 | FBM/Ridged/Mixed/Cellular. |
|
| `EVoxelNoiseType` | 128 | FBM/Ridged/Mixed/Cellular. |
|
||||||
| `ECaveGeneratorType` | 146 | TunnelNetwork / FlatPlain / CrystalChamber. |
|
| `ECaveGeneratorType` | 175 | TunnelNetwork / FlatPlain / CrystalChamber. |
|
||||||
| `EVoxelStrateTransition` | 183 | Gradient / Hard / Interleaved boundary blends. |
|
| `EVoxelStrateTransition` | 228 | Gradient / Hard / Interleaved boundary blends. |
|
||||||
| **`FStrateGenerationParams`** | ~350 | The giant TunnelNetwork param bag (rock, worms, rooms, tunnels, warp, roughness, live terrain-detail fields, dead terrain-op transport slots, boundary seal). `Lerp()` static blends two sets at boundaries — it expands the **`VF_STRATE_PARAM_FIELDS` X-macro** (defined just above the struct): **adding a field to the struct? add it to that list** or blends silently reset it to default. |
|
| **`FStrateGenerationParams`** | 373 | The giant TunnelNetwork param bag (rock, worms, rooms, tunnels, warp, roughness, live terrain-detail fields, dead terrain-op transport slots, boundary seal). `Lerp()` static blends two sets at boundaries — it expands the **`VF_STRATE_PARAM_FIELDS` X-macro** (defined just above the struct): **adding a field to the struct? add it to that list** or blends silently reset it to default. |
|
||||||
| `FStrateTerrainOpEntry` | 965 | Soft-ptr to a terrain op + Weight + Probability. |
|
| `FStrateTerrainOpEntry` | 1169 | Soft-ptr to a terrain op + Weight + Probability. |
|
||||||
| **`FSlabGenerationParams`** | 1019 | Floor/ceiling heights, roughness, columns, seal — for slab generators. |
|
| **`FSlabGenerationParams`** | 1223 | Floor/ceiling heights, roughness, columns, seal — for slab generators. |
|
||||||
| **`FPlacementProfile`** | ~1747 | **Shared placement vocabulary** for every scatter primitive (`FStrateDecoration`, `FStrateLandmark`, coming `FStrateSetPiece`): spawn (ActorClass/InstancedMesh), Filter gates (surface/slope/overhang/water/RequiredBiome + **F7 awareness `Conditions[]`** — `FTerrainCondition` relief/moisture/biome-border predicates, AND-ed, evaluated by `Generator::EvaluateTerrainConditions`), Transform (align/offsets/RotationOffset+RandomRotation/scale), Render (cull/shadow). Each primitive embeds it as `Profile` + keeps only its own DISTRIBUTION fields. Per-primitive defaults set in each struct's ctor (deco: scale 0.8-1.2 + RandomRotation.Yaw=360; landmark: Ceiling + no align). |
|
| **`FPlacementProfile`** | 2000 | **Shared placement vocabulary** for every scatter primitive (`FStrateDecoration`, `FStrateLandmark`; set-pieces are landmarks): spawn (ActorClass/InstancedMesh), Filter gates (surface/slope/overhang/water/RequiredBiome + **F7 awareness `Conditions[]`** — `FTerrainCondition` relief/moisture/biome-border predicates, AND-ed, evaluated by `Generator::EvaluateTerrainConditions`), Transform (align/offsets/RotationOffset+RandomRotation/scale), Render (cull/shadow). Each primitive embeds it as `Profile` + keeps only its own DISTRIBUTION fields. Per-primitive defaults set in each struct's ctor (deco: scale 0.8-1.2 + RandomRotation.Yaw=360; landmark: Ceiling + no align). |
|
||||||
| `FStrateDecoration` / `FStrateLandmark` | ~1830 / ~1900 | `Profile` + distribution: deco = StreamTier/SpawnDensity/MaxPerChunk; landmark = SpacingChunks/JitterFraction/SpawnProbability/StreamRadiusChunks + Light-Orb block. |
|
| `FStrateDecoration` / `FStrateLandmark` | 2207 / 2271 | `Profile` + distribution: deco = StreamTier/SpawnDensity/MaxPerChunk; landmark = SpacingChunks/JitterFraction/SpawnProbability/StreamRadiusChunks + Light-Orb block. |
|
||||||
| `ELandmarkAnchor` (on `FStrateLandmark`) | ~1940 | F7: `AnchorMode` = HashLattice / PassageMouth + passage toggles + exclusion (`ExclusionRadiusChunks`/`Priority`) folded into `FStrateLandmark` (set-pieces merged in — one primitive, one `Landmarks` list, `UpdateLandmarks`). |
|
| `ELandmarkAnchor` (on `FStrateLandmark`) | 2251 | F7: `AnchorMode` = HashLattice / PassageMouth + passage toggles + exclusion (`ExclusionRadiusChunks`/`Priority`) folded into `FStrateLandmark` (set-pieces merged in — one primitive, one `Landmarks` list, `UpdateLandmarks`). |
|
||||||
| `FStrateAmbientActor` / `FStrateCreature` | ~2090 / ~2110 | Content spawn entries (consumed by future systems). |
|
| `FStrateAmbientActor` / `FStrateCreature` | 2405 / 2425 | Content spawn entries (consumed by future systems). |
|
||||||
|
|
||||||
**`Public/VoxelStrateDefinition.h`** — `UVoxelStrateDefinition : UPrimaryDataAsset`
|
**`Public/VoxelStrateDefinition.h`** — `UVoxelStrateDefinition : UPrimaryDataAsset`
|
||||||
(line 36). One asset = one strate *type*. Fields: identity, `StrateHeightInChunks`(60),
|
(line 46). One asset = one strate *type*. Fields: identity, `StrateHeightInChunks`(75),
|
||||||
`TransitionType`(79)/`TransitionBlendChunks`(89), `GeneratorType`(102),
|
`TransitionType`(93)/`TransitionBlendChunks`(103), `GeneratorType`(116),
|
||||||
`GenerationParams`(113), `SlabParams`(124), `Biomes[]`+`BiomeMapParams` (the biome list +
|
`GenerationParams`(137), `SlabParams`(148), `Biomes[]`+`BiomeMapParams` (the biome list +
|
||||||
field tuning — empty ⇒ unchanged world, §8.14), `TerrainOperations`(147), visuals/fog/light,
|
field tuning — empty ⇒ unchanged world, §8.14), `TerrainOperations`(252), visuals/fog/light,
|
||||||
content lists, audio, `GameplayTags`(223), `bUseOperatorStack` (the OPSTACK A/B opt-in — only bites
|
content lists, audio, `GameplayTags`(393). EditConditions show/hide param groups by generator
|
||||||
if the archetype is in `UsesOperatorStackForChunk`'s ported list). EditConditions show/hide param
|
type.
|
||||||
groups by generator type.
|
|
||||||
|
|
||||||
**`Public/VoxelStrateManager.h` + `.cpp`** — `UVoxelStrateManager : UObject` (h:108).
|
**`Public/VoxelStrateManager.h` + `.cpp`** — `UVoxelStrateManager : UObject` (h:152).
|
||||||
Maps depth→strate at runtime; owns passages.
|
Maps depth→strate at runtime; owns passages.
|
||||||
- `FVoxelPassage` (h:39): standing endpoints, radius, type, wandering control points, and
|
- `FVoxelPassage` (h:46): standing endpoints, radius, type, wandering control points, and
|
||||||
upper/lower local landing descriptors (room, floor, and door; no root connector).
|
upper/lower local landing descriptors (room, floor, and door; no root connector).
|
||||||
- `FStrateSlot` (h:84): definition + chunk-Z range + index.
|
- `FStrateSlot` (h:128): definition + chunk-Z range + index.
|
||||||
| Method | .cpp line | Role |
|
| Method | .cpp line | Role |
|
||||||
|--------|-----------|------|
|
|--------|-----------|------|
|
||||||
| `Initialize` | 10 | Keeps the existing sequencing through `GeneratePassages`, but selects one slot source: a validated cooked season (exact ordered bounds/vector/recipe) or the unchanged authored fixed+sorted/shuffled pool. Assigned-invalid seasons fail closed. Authored source definitions are duplicated as content bags; generated slots use deterministic C++ defaults and zero auto-passages because schema 2 does not store passage configuration. |
|
| `Initialize` | 1164 | Keeps the existing sequencing through `GeneratePassages`, but selects one slot source: a validated cooked season (exact ordered bounds/vector/recipe) or the unchanged authored fixed+sorted/shuffled pool. Assigned-invalid seasons fail closed. Authored source definitions are duplicated as content bags; generated slots use deterministic C++ defaults and zero auto-passages because schema 2 does not store passage configuration. |
|
||||||
| `GeneratePassages` | 247 | Deterministic passages between consecutive strates (per-strate `PassageConfig::Style` control points; auto passages retain the existing `EVoxelPassageType` default); placement and shape values are independently salted hashes of seed + boundary slot + connection index. Each mouth independently queries its own strate's pure player-fit `VF_SuggestLandingPoint` with its contract seed (cave room salt, otherwise world seed), may move within its archetype-specific budget, then receives a body-sized local room/floor. The origin landing is reserved for the future shaft; no landing-to-root road is generated. Unsupported/no-fit answers preserve the old random reach; a deterministic level dog-leg is inserted when the final ramp needs it to stay ≤44°. All descriptors and bounds are generated without reading another passage. |
|
| `GeneratePassages` | 1659 | Deterministic passages between consecutive strates (per-strate `PassageConfig::Style` control points; auto passages retain the existing `EVoxelPassageType` default); placement and shape values are independently salted hashes of seed + boundary slot + connection index. Each mouth independently queries its own strate's pure player-fit `VF_SuggestLandingPoint` with its contract seed (cave room salt, otherwise world seed), may move within its archetype-specific budget, then receives a body-sized local room/floor. The origin landing is reserved for the future shaft; no landing-to-root road is generated. Unsupported/no-fit answers preserve the old random reach; a deterministic level dog-leg is inserted when the final ramp needs it to stay ≤44°. All descriptors and bounds are generated without reading another passage. |
|
||||||
| `EvaluateModifierSDF` | 357 | SDF of passage tubes plus local landing rooms at a point. A per-chunk `thread_local` shortlist (`PassagesVersion`-stamped) → far chunks return `FLT_MAX` without walking `Passages`; the same cache serves floor membership. `FPassageEvaluationCache::FloorProjections` reuses native/generic floor projections and walkable-air results once per `(sample, passage)` using exact double XYZ equality and unchanged float outputs; `voxel.FloorRound1PassageProjectionCache=0` disables only this A/B cache. §8.10. |
|
| `EvaluateModifierSDF` | 2643 | SDF of passage tubes plus local landing rooms at a point. A per-chunk `thread_local` shortlist (`PassagesVersion`-stamped) → far chunks return `FLT_MAX` without walking `Passages`; the same cache serves floor membership. `FPassageEvaluationCache::FloorProjections` reuses native/generic floor projections and walkable-air results once per `(sample, passage)` using exact double XYZ equality and unchanged float outputs; `voxel.FloorRound1PassageProjectionCache=0` disables only this A/B cache. §8.10. |
|
||||||
| `ApplyPassageModifier` | 1319 | Shared legacy/op-stack passage post: carves tube and local landing air through the idempotent landing primitive, then restores the local landing support slab in internal positive-solid density. |
|
| `ApplyPassageModifier` | 2772 | Shared legacy/op-stack passage post: carves tube and local landing air through the idempotent landing primitive, then restores the local landing support slab in internal positive-solid density. |
|
||||||
| `VF_ApplyPassageLandingCarving` | — | Idempotent density-independent landing-air threshold shared by the structural pass and the post-disturbance MC backstop; keeps legacy and operator-stack results bit-identical. |
|
| `VF_ApplyPassageLandingCarving` | — | Idempotent density-independent landing-air threshold shared by the structural pass and the post-disturbance MC backstop, so the two agree bit for bit. |
|
||||||
| `ApplyPassageLandingFloorMC` | 1347 | Reasserts the same support slab after MC-space disturbances and before the final XY edge seal. |
|
| `ApplyPassageLandingFloorMC` | 2983 | Reasserts the same support slab after MC-space disturbances and before the final XY edge seal. |
|
||||||
| `AnyPassageNearBox` | — | Conservative sphere-vs-AABB test of every passage's bound against a voxel box (+carve blend pad). Per TILE (ClassifyTile guard), never per voxel. |
|
| `AnyPassageNearBox` | — | Conservative sphere-vs-AABB test of every passage's bound against a voxel box (+carve blend pad). Per TILE (ClassifyTile guard), never per voxel. |
|
||||||
| `AnyPassageLandingFloorNearBox` | — | Conservative local-landing-floor AABB guard. It kills the `AllAir` hypothesis wherever a proved landing floor can occur; the op-stack returns `Both` for the same box. |
|
| `AnyPassageLandingFloorNearBox` | — | Conservative local-landing-floor AABB guard. It kills the `AllAir` hypothesis wherever a proved landing floor can occur; the op-stack returns `Both` for the same box. |
|
||||||
| `FindSlotIndexForChunkZ` | 427 | Z → layout index. |
|
| `FindSlotIndexForChunkZ` | 6111 | Z → layout index. |
|
||||||
| `GetStrateAt` / `GetStrateIndex` | 443 / 455 | World-Z queries. |
|
| `GetStrateAt` / `GetStrateIndex` | 6127 / 6139 | World-Z queries. |
|
||||||
| `GetLayoutVersion` | h:161 (inline) | Layout/passage generation counter (= `PassagesVersion`, bumped by every `Initialize` and by the editor composer override). Hot-path callers key `thread_local` memos on it (strate-index memo in `GetDensityWithParams`, passage shortlist) so live changes never serve stale data. |
|
| `GetLayoutVersion` | h:250 (inline) | Layout/passage generation counter (= `PassagesVersion`, bumped by every `Initialize` and by the editor composer override). Hot-path callers key `thread_local` memos on it (strate-index memo in `GetDensityWithParams`, passage shortlist) so live changes never serve stale data. |
|
||||||
| `GetStrateForChunk` | 466 | Chunk → definition. |
|
| `GetStrateForChunk` | 6150 | Chunk → definition. |
|
||||||
| `GetGeneratorTypeForChunk` | 476 | Chunk → generator type. |
|
| `GetGeneratorTypeForChunk` | 6176 | Chunk → generator type. |
|
||||||
| `UsesOperatorStackForChunk` | 559 | Chunk → should `GetDensityAt` take the operator stack? `bUseOperatorStack` on the definition **AND** archetype in the ported list — **now all 8 of 8** (Maze, FlatPlain, CrystalChamber, SurfaceWorld, VerticalShafts, FloatingIslands, TunnelNetwork, Underwater). **That list is written down here and nowhere else.** With every archetype ported the flag is now the *only* thing that decides the path, so ticking the box is no longer a no-op anywhere — it is a real switch onto the operator stack for that strate. |
|
| `UsesOperatorStackForChunk` | 6203 | Chunk → does its slot have an operator stack? True for any in-layout slot with a definition whose archetype is one of the 8 (Maze, FlatPlain, CrystalChamber, SurfaceWorld, VerticalShafts, FloatingIslands, TunnelNetwork, Underwater); false outside the layout. **That list is written down here and nowhere else.** `ClassifyTile` guards on it; `GetDensityAt` uses it to share TunnelNetwork stacks by chunk Z. |
|
||||||
| `GetRecipeForChunk` | — | Runtime immutable recipe/vector/strate-seed copy for cooked seasons; editor slot overrides use the same worker hand-off. |
|
| `GetRecipeForChunk` | — | Runtime immutable recipe/vector/strate-seed copy for cooked seasons; editor slot overrides use the same worker hand-off. |
|
||||||
| `GetSlabParamsForChunk` | 490 | Slab params with runtime Z bounds (no blend — slabs use Hard). |
|
| `GetSlabParamsForChunk` | 6263 | Slab params with runtime Z bounds (no blend — slabs use Hard). |
|
||||||
| `GetBiomeContextForChunk` | — | Flatten the strate's `Biomes[]` + `BiomeMapParams` into a POD `FBiomeContext` for the biome field. Empty ⇒ biomes disabled. §8.14. |
|
| `GetBiomeContextForChunk` | — | Flatten the strate's `Biomes[]` + `BiomeMapParams` into a POD `FBiomeContext` for the biome field. Empty ⇒ biomes disabled. §8.14. |
|
||||||
| `GetGenerationParams` | 515 | **Blended** TunnelNetwork params (handles Gradient/Hard/Interleaved transitions). |
|
| `GetGenerationParams` | 6509 | **Blended** TunnelNetwork params (handles Gradient/Hard/Interleaved transitions). |
|
||||||
| `SetComposerOverrideForStrate` / `GetComposerOverrideForChunk` | editor-only | Temporary density-only candidate overlay for one existing slot. Copies immutable params/recipe into the worker refetch path, leaves layout/content/passages intact, and bumps `PassagesVersion` to invalidate generator memos. |
|
| `SetComposerOverrideForStrate` / `GetComposerOverrideForChunk` | editor-only | Temporary density-only candidate overlay for one existing slot. Copies immutable params/recipe into the worker refetch path, leaves layout/content/passages intact, and bumps `PassagesVersion` to invalidate generator memos. |
|
||||||
| `BuildParamsFromDefinition` (static) | 1399 | Returns the definition's base `GenerationParams` only. Terrain-op assets are passed separately to `BuildChunkCache`, which applies one selected op per room and pre-bakes Column/Pit/Chimney features. |
|
| `BuildParamsFromDefinition` (static) | 6815 | Returns the definition's base `GenerationParams` only. Terrain-op assets are passed separately to `BuildChunkCache`, which applies one selected op per room and pre-bakes Column/Pit/Chimney features. |
|
||||||
|
|
||||||
**`Public/VoxelTerrainOpDefinition.h` + `.cpp`** — `UVoxelTerrainOpDefinition : UPrimaryDataAsset`
|
**`Public/VoxelTerrainOpDefinition.h` + `.cpp`** — `UVoxelTerrainOpDefinition : UPrimaryDataAsset`
|
||||||
(h:67). One asset = one terrain op. `EVoxelTerrainOpType` (h:36): Terrace, LayerLines,
|
(h:67). One asset = one terrain op. `EVoxelTerrainOpType` (h:36): Terrace, LayerLines,
|
||||||
@@ -556,7 +550,7 @@ atmosphere override, `WaterMaterial`, `MaterialPaletteIndex` (F6 — baked to ve
|
|||||||
`UVoxelStrateDefinition::Biomes[]`. Generator-agnostic (surface biomes now, cave biomes later). §8.14.
|
`UVoxelStrateDefinition::Biomes[]`. Generator-agnostic (surface biomes now, cave biomes later). §8.14.
|
||||||
|
|
||||||
**`Public/VoxelStrateComposer.h` + `Private/VoxelStrateComposer.cpp`** (Tier 4a/4b + Tier 5) — offline
|
**`Public/VoxelStrateComposer.h` + `Private/VoxelStrateComposer.cpp`** (Tier 4a/4b + Tier 5) — offline
|
||||||
`FVoxelStrateCorpus`, `VF_RollStrateParams`, and `VF_RollStrateStructure`. `LoadFromAssetRegistry` enumerates every project
|
`FVoxelStrateCorpus`, `VF_RollStrateParamsDetailed`, and `VF_RollStrateStructure`. `LoadFromAssetRegistry` enumerates every project
|
||||||
`UVoxelStrateDefinition` through the Asset Registry, excludes `Saved/Autosaves` and `Saved/Cooked`
|
`UVoxelStrateDefinition` through the Asset Registry, excludes `Saved/Autosaves` and `Saved/Cooked`
|
||||||
copies, and adds one `VoxelStrateTypes.h` default vector for each of the eight exact archetypes.
|
copies, and adds one `VoxelStrateTypes.h` default vector for each of the eight exact archetypes.
|
||||||
The corpus is grouped by exact `ECaveGeneratorType`: the 2026-09-04 run loaded **4 project
|
The corpus is grouped by exact `ECaveGeneratorType`: the 2026-09-04 run loaded **4 project
|
||||||
@@ -668,31 +662,29 @@ text plus independently copied metadata/hash, and retains soft references to any
|
|||||||
`UVoxelSettings::Season` is the only activation switch.
|
`UVoxelSettings::Season` is the only activation switch.
|
||||||
|
|
||||||
### 3.9 Player edits — `Public/VoxelDiffLayer.h` + `.cpp`
|
### 3.9 Player edits — `Public/VoxelDiffLayer.h` + `.cpp`
|
||||||
`UVoxelDiffLayer : UObject` (h:77). Stores `FVoxelModification` (h:43: Center/Radius/Strength;
|
`UVoxelDiffLayer : UObject` (h:137). Stores `FVoxelModification` (h:60: Center/Radius/Strength;
|
||||||
**negative Strength = carve, positive = fill**) grouped by chunk in `TMap ChunkMods`.
|
**negative Strength = carve, positive = fill**) grouped by chunk in `TMap ChunkMods`.
|
||||||
| Method | .cpp line | Role |
|
| Method | .cpp line | Role |
|
||||||
|--------|-----------|------|
|
|--------|-----------|------|
|
||||||
| `SetBudget` | 10 | From VoxelSettings carving caps. |
|
| `SetBudget` | 102 | From VoxelSettings carving caps. |
|
||||||
| `CanModify` | 20 | Budget check (no consume) — for UI. |
|
| `CanModify` | 112 | Budget check (no consume) — for UI. |
|
||||||
| `GetRemainingModifications` / `GetRemainingVolume` | 47 / 53 | -1 = unlimited. |
|
| `GetRemainingModifications` / `GetRemainingVolume` | 169 / 175 | -1 = unlimited. |
|
||||||
| `ApplyModification` | 63 | Enforces budget, stores in all overlapped chunks, returns dirty coords. |
|
| `ApplyModification` | 185 | Enforces budget, stores in all overlapped chunks, returns dirty coords. |
|
||||||
| `GetDensityOffset` | 131 | Per-voxel combined diff (smoothstep falloff, additive). |
|
| `GetDensityOffset` | 299 | Per-voxel combined diff (smoothstep falloff, additive). |
|
||||||
| `HasModifications` | 160 | Fast reject for hot path. |
|
| `HasModifications` | 421 | Fast reject for hot path. |
|
||||||
| `HasAnyMods` / `GetModsVersion` | h:238 / h:241 (inline) | Lock-free atomics: any-mod-exists flag + monotonic mod-state version (bumped by `ApplyModification`/`Clear`). |
|
| `HasAnyMods` / `GetModsVersion` | h:246 / h:249 (inline) | Lock-free atomics: any-mod-exists flag + monotonic mod-state version (bumped by `ApplyModification`/`Clear`). |
|
||||||
| `GetChunkModsSnapshot` | — | Copy one chunk's mod list under ONE read lock. Workers snapshot per (chunk, version) instead of locking per voxel — `GetDensityAt` keys a `thread_local` 64-slot direct-mapped cache on it (~27 lock ops per tile task instead of ~86k once any carve exists). |
|
| `GetChunkModsSnapshot` | — | Copy one chunk's mod list under ONE read lock. Workers snapshot per (chunk, version) instead of locking per voxel — `GetDensityAt` keys a `thread_local` 64-slot direct-mapped cache on it (~27 lock ops per tile task instead of ~86k once any carve exists). |
|
||||||
| `HasAnyModInChunkRange` | — | Any modified chunk key in an inclusive chunk box? One key walk under a read lock — ClassifyTile's diff guard (per tile, conservative by construction: mods are stored in every chunk their radius overlaps). |
|
| `HasAnyModInChunkRange` | — | Any modified chunk key in an inclusive chunk box? One key walk under a read lock — ClassifyTile's diff guard (per tile, conservative by construction: mods are stored in every chunk their radius overlaps). |
|
||||||
| `EvaluateMods` (static) | — | Lock-free pure evaluation of a mod list at a voxel — shared core of `GetDensityOffset` and the generator's snapshot path. |
|
| `EvaluateMods` (static) | — | Lock-free pure evaluation of a mod list at a voxel — shared core of `GetDensityOffset` and the generator's snapshot path. |
|
||||||
| `Clear` | 170 | Wipe all (season reset). |
|
| `Clear` | 435 | Wipe all (season reset). |
|
||||||
| `GetTotalModificationCount` / `GetModifiedChunkCount` | 182 / 192 | Stats. |
|
| `GetTotalModificationCount` / `GetModifiedChunkCount` | 454 / 465 | Stats. |
|
||||||
|
|
||||||
### 3.10 Mesher — `Public/VoxelMarchingCubesMesher.h` + `.cpp`
|
### 3.10 Mesher — `Public/VoxelMarchingCubesMesher.h` + `.cpp`
|
||||||
`UVoxelMarchingCubesMesher : UObject` (h:21). Holds `Generator` ptr, `IsoLevel=0`, skirt params.
|
`UVoxelMarchingCubesMesher : UObject` (h:42). Holds `Generator` ptr, `IsoLevel=0`, skirt params.
|
||||||
(The dead trio `GetDensity`/`InterpolateEdge`/`ComputeGradientNormal` + `GradientOffset` was
|
The pre-sampled grid supplies positions AND gradients inline (T1.b).
|
||||||
removed — since T1.b the pre-sampled grid supplies positions AND gradients inline.)
|
|
||||||
| Method | .cpp line | Role |
|
| Method | .cpp line | Role |
|
||||||
|--------|-----------|------|
|
|--------|-----------|------|
|
||||||
| **`GenerateSheetMesh`** | ~455 | **F18 far-field SHEET** (render-distance ring, `Tile.Level > MaxClipLevel`): two displaced heightfield grids per tile — ground (polygroup 0) + sky-cap (polygroup 1) from `GetSurfaceHeightAt` columns (StrateChunkZ = band mid identifies the strate; non-SurfaceWorld ⇒ empty). Classes true BY CONSTRUCTION (no vote/probes). Same conventions as `GenerateMesh` (world-cm positions, planar UVs, F6 colour masks, −N double-faced perimeter skirts per bucket, ground‖cap + `NumCeilingTriangles`). Margin ring keeps normals continuous between sheets. XY HOLE params: cells fully inside the MC-covered box around the player (`AVoxelWorld::SheetHole*Vox`, shrunk 1 tile for seam overlap) are skipped — a partially-covered sheet must not overlay near terrain; hole-edge cells get no skirt. Carved features (passages/spine/chasms) + diff layer NOT represented — accepted at sheet distance. |
|
| **`GenerateMesh`** | 96 | The MC loop over cells; `Step` controls LOD sampling. Sets the T2.b octave bias for the tile (`TGuardValue` on `VoxelGenLOD::OctaveBias`, from `LODOctaveDrop` × log2(Step); 0 at LOD0/off). Edge `t` + grid-gradient normals computed inline (`SampleG`/`GradAt`). Optional `OutCaptureGrid` (4th arg) = CAPTURE-DURING-MESHING: when non-null + full-res (`CellsPerAxis==CHUNK_SIZE`), copies the already-sampled `CHUNK_SIZE³` density grid (quantized via `VF_QuantizeDensity`, VoxelTypes.h) so the density clipmap reuses it instead of re-sampling `GetDensityAt`. Pure read of the grid — §8.10 untouched. **F17 surface class**: each unique vertex is classified sol/sky-cap in `GetOrCreateVertex` (down-facing only → memoized `GetSurfaceHeightAt`, nearer `CeilSurf` = cap); triangles bucket by majority into `GroundTris`/`CapTris` (thread_local), skirts emit per bucket, then `Triangles = ground‖cap` + `FVoxelMeshData::NumCeilingTriangles` (→ polygroups in `BuildTileStreamSet`). Same tris, only index ORDER changes. STRATE CONTENT CUT: optional `BandZMin/MaxVox` params restrict the cz cell loop (+ gz sampling rows) to the player-strate band — coarse straddling tiles mesh ONE strate (kills far-LOD inter-strate aliasing holes); meshed cells bit-identical. |
|
||||||
| **`GenerateMesh`** | ~15 | The MC loop over cells; `Step` controls LOD sampling. Sets the T2.b octave bias for the tile (`TGuardValue` on `VoxelGenLOD::OctaveBias`, from `LODOctaveDrop` × log2(Step); 0 at LOD0/off). Edge `t` + grid-gradient normals computed inline (`SampleG`/`GradAt`). Optional `OutCaptureGrid` (4th arg) = CAPTURE-DURING-MESHING: when non-null + full-res (`CellsPerAxis==CHUNK_SIZE`), copies the already-sampled `CHUNK_SIZE³` density grid (quantized via `VF_QuantizeDensity`, VoxelTypes.h) so the density clipmap reuses it instead of re-sampling `GetDensityAt`. Pure read of the grid — §8.10 untouched. **F17 surface class**: each unique vertex is classified sol/sky-cap in `GetOrCreateVertex` (down-facing only → memoized `GetSurfaceHeightAt`, nearer `CeilSurf` = cap); triangles bucket by majority into `GroundTris`/`CapTris` (thread_local), skirts emit per bucket, then `Triangles = ground‖cap` + `FVoxelMeshData::NumCeilingTriangles` (→ polygroups in `BuildTileStreamSet`). Same tris, only index ORDER changes. STRATE CONTENT CUT: optional `BandZMin/MaxVox` params restrict the cz cell loop (+ gz sampling rows) to the player-strate band — coarse straddling tiles mesh ONE strate (kills far-LOD inter-strate aliasing holes); meshed cells bit-identical. |
|
|
||||||
|
|
||||||
**`Public/MarchingCubesTables.h`** — `EdgeTable` + `TriTable` reference data (Paul
|
**`Public/MarchingCubesTables.h`** — `EdgeTable` + `TriTable` reference data (Paul
|
||||||
Bourke). Cube corner/edge layout documented at top (lines 7-37). Rarely needs editing.
|
Bourke). Cube corner/edge layout documented at top (lines 7-37). Rarely needs editing.
|
||||||
@@ -711,8 +703,10 @@ Bourke). Cube corner/edge layout documented at top (lines 7-37). Rarely needs ed
|
|||||||
---
|
---
|
||||||
|
|
||||||
### 3.12 Automation tests — `Private/Tests/` (added 2026-07-27, `#if WITH_DEV_AUTOMATION_TESTS`)
|
### 3.12 Automation tests — `Private/Tests/` (added 2026-07-27, `#if WITH_DEV_AUTOMATION_TESTS`)
|
||||||
The plugin's first tests (`OPSTACK-PLAN.md` Phase 0.5). Run them from the editor's
|
The plugin's first tests (`Docs/archive/OPSTACK-PLAN.md` Phase 0.5). Run them from the editor's
|
||||||
**Session Frontend → Automation**, filter `VoxelForge`.
|
**Session Frontend → Automation**, filter `VoxelForge`. The `OpStack.*Equivalence` names are
|
||||||
|
historical: those tests check the shipped stacks themselves and no longer compare against a
|
||||||
|
second density path.
|
||||||
|
|
||||||
| File | Test name | What it proves |
|
| File | Test name | What it proves |
|
||||||
|------|-----------|----------------|
|
|------|-----------|----------------|
|
||||||
@@ -724,61 +718,74 @@ The plugin's first tests (`OPSTACK-PLAN.md` Phase 0.5). Run them from the editor
|
|||||||
| `VoxelForgeClassifyTileTest.cpp` | `VoxelForge.OpStack.BoxVerdictFold` | Pure-logic walk of the fold in `VoxelDensityOp.h`, case by case — including the seal-forces-AllSolid case that justifies `ClassifyBox` existing. Also the only `.cpp` that includes the op header, so the build actually sees it. |
|
| `VoxelForgeClassifyTileTest.cpp` | `VoxelForge.OpStack.BoxVerdictFold` | Pure-logic walk of the fold in `VoxelDensityOp.h`, case by case — including the seal-forces-AllSolid case that justifies `ClassifyBox` existing. Also the only `.cpp` that includes the op header, so the build actually sees it. |
|
||||||
| `VoxelForgeHeightStackTest.cpp` | `VoxelForge.OpStack.SurfaceHeightEquivalence` | The height-space stack vs `ComputeSurfaceTerrainZ`, in **altitudes**. Runs twice: defaults, then **all F20 terrain ops ON** — the load-bearing pass, since the ops are off by default and the defaults pass exercises only the structural source. Also brute-forces `MaxDisplacement` (a false bound would be a hole). Bar is bit-identity; a height delta is a visibly different world, not rounding. |
|
| `VoxelForgeHeightStackTest.cpp` | `VoxelForge.OpStack.SurfaceHeightEquivalence` | The height-space stack vs `ComputeSurfaceTerrainZ`, in **altitudes**. Runs twice: defaults, then **all F20 terrain ops ON** — the load-bearing pass, since the ops are off by default and the defaults pass exercises only the structural source. Also brute-forces `MaxDisplacement` (a false bound would be a hole). Bar is bit-identity; a height delta is a visibly different world, not rounding. |
|
||||||
| `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` | The same 6-op slab stack run three times — FlatPlain, CrystalChamber and a tuned CrystalChamber (the worst case for the `ClassifyBox` amplitude bounds): finite density over 20k points, window-invariance across order and threads, and box-verdict brute force. |
|
||||||
| `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 ±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 box query 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 publishes an isolated interval for 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 ledge and converter decide their own effects. 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 box query 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 publishes an isolated interval for 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 ledge and converter decide their own effects. 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 8-op Maze stack vs `GetMazeDensity` over 20k points (bit-identity; a side-of-iso disagreement is the hard fail), plus purity across workers, a 64-seed spanning-tree/connectivity audit, corridor morphology, cache rebuild-vs-hot timing, and brute force on every box verdict the stack emits. Latest audit: **0 tree edge-count violations, 0 tree-disconnected, 0 Maze-disconnected**; interior degrees `d1=15.91%`, `d2=36.60%`, `d3=31.73%`, `d4=12.95%`, `d5=2.56%`, `d6=0.25%`, mean **2.504** versus a cubic grid's degree-6/mean-6.000; runs `min=1 / median=1 / p90=3 / max=18`. Latest focused cache timing: **1.436 μs rebuild**, **0.150 μs hot call**; final namespace timing: **1.130 μs rebuild**, **0.145 μs hot call**. Latest box audit: **104 proved, 152 Mixed, 138,424 voxels, 0 violations**. |
|
| `VoxelForgeOpStackMazeTest.cpp` | `VoxelForge.OpStack.MazeEquivalence` | The 8-op Maze stack: decomposition, purity across workers, a 64-seed spanning-tree/connectivity audit, corridor morphology, cache rebuild-vs-hot timing, and brute force on every box verdict the stack emits. Latest audit: **0 tree edge-count violations, 0 tree-disconnected, 0 Maze-disconnected**; interior degrees `d1=15.91%`, `d2=36.60%`, `d3=31.73%`, `d4=12.95%`, `d5=2.56%`, `d6=0.25%`, mean **2.504** versus a cubic grid's degree-6/mean-6.000; runs `min=1 / median=1 / p90=3 / max=18`. Latest focused cache timing: **1.436 μs rebuild**, **0.150 μs hot call**; final namespace timing: **1.130 μs rebuild**, **0.145 μs hot call**. Latest box audit: **104 proved, 152 Mixed, 138,424 voxels, 0 violations**. |
|
||||||
| `VoxelForgeStrateParamCoverageTest.cpp` | `VoxelForge.Determinism.StrateParamBlendCoverage` | **The X-macro guard** (added 2026-08-17). `FStrateGenerationParams::Lerp` blends the hand-written `VF_STRATE_PARAM_FIELDS` list, **not** the struct — so a field added to one and not the other compiles, tests green, and silently takes its **default** inside every Gradient/Interleaved transition band. This expands the X-macro a **third** way (after LERP and SNAP), into a name list, and diffs it against the struct's UObject reflection. Pure shape test: no fixture, no world, instant. `GExemptFieldNames` is **empty** — every reflected field is covered today, and any exemption must be written down as a decision. Stakes rise with the world composer, which intends to invent parameter sets through this same `Lerp` (`COMPOSER-NOTES.md`). |
|
| `VoxelForgeStrateParamCoverageTest.cpp` | `VoxelForge.Determinism.StrateParamBlendCoverage` | **The X-macro guard** (added 2026-08-17). `FStrateGenerationParams::Lerp` blends the hand-written `VF_STRATE_PARAM_FIELDS` list, **not** the struct — so a field added to one and not the other compiles, tests green, and silently takes its **default** inside every Gradient/Interleaved transition band. This expands the X-macro a **third** way (after LERP and SNAP), into a name list, and diffs it against the struct's UObject reflection. Pure shape test: no fixture, no world, instant. `GExemptFieldNames` is **empty** — every reflected field is covered today, and any exemption must be written down as a decision. Stakes rise with the world composer, which intends to invent parameter sets through this same `Lerp` (`COMPOSER-NOTES.md`). |
|
||||||
| `VoxelForgeComposerParameterRollTest.cpp` | `VoxelForge.Composer.ParameterRoll` | Asset-Registry corpus audit + complete per-archetype spread/exclusion/clamp table; asserts bit-identical deterministic rerolls and same-archetype parents; measures 64 transient candidate strates with `VF_MeasureStrate` plus the exact unsnapped arrival→departure law; brute-forces every rolled production box verdict. World-scale run: **1,422 applications / 168 distinct fields**; **36/64 survival**, 60 non-vacuous, 58 largest-component, 40 exact-law passes; **1,411 Mixed + 731 AllSolid + 418 AllAir = 1,149 proved boxes**, **1,529,319 lattice voxels checked, 0 violations**, 148.387 s. |
|
| `VoxelForgeComposerParameterRollTest.cpp` | `VoxelForge.Composer.ParameterRoll` | Asset-Registry corpus audit + complete per-archetype spread/exclusion/clamp table; asserts bit-identical deterministic rerolls and same-archetype parents; measures 64 transient candidate strates with `VF_MeasureStrate` plus the exact unsnapped arrival→departure law; brute-forces every rolled production box verdict. World-scale run: **1,422 applications / 168 distinct fields**; **36/64 survival**, 60 non-vacuous, 58 largest-component, 40 exact-law passes; **1,411 Mixed + 731 AllSolid + 418 AllAir = 1,149 proved boxes**, **1,529,319 lattice voxels checked, 0 violations**, 148.387 s. |
|
||||||
| ″ | `VoxelForge.Composer.TerrainDetailLiveness` | Fixed 4,096-point `GetDensityAt` lattice, legacy and operator-stack paths; changes one terrain-detail group at a time with an empty terrain-op pool. Proves 9 live groups / 23 fields and 3 dead groups / 11 fields; all 24 rows match. |
|
| ″ | `VoxelForge.Composer.TerrainDetailLiveness` | Fixed 4,096-point `GetDensityAt` lattice on the TunnelNetwork slot; changes one terrain-detail group at a time with an empty terrain-op pool. Proves 9 live groups / 23 fields and 3 dead groups / 11 fields; all 12 rows must match. |
|
||||||
| `VoxelForgeComposerPromotionTest.cpp` | `VoxelForge.Composer.Promotion` | Re-measures the 12 project/default members, simulates five deterministic 24-candidate seasons with normalized measured-metric novelty (`<0.20`), cap 6, cumulative JSON promotion, provenance counts, corpus-hash checks, fresh-load gate verification, spread/survival reporting, and deliberate stale-metric corruption. The final landing validation passed the strict existing gates; no assertion was weakened. |
|
| `VoxelForgeComposerPromotionTest.cpp` | `VoxelForge.Composer.Promotion` | Re-measures the 12 project/default members, simulates five deterministic 24-candidate seasons with normalized measured-metric novelty (`<0.20`), cap 6, cumulative JSON promotion, provenance counts, corpus-hash checks, fresh-load gate verification, spread/survival reporting, and deliberate stale-metric corruption. The final landing validation passed the strict existing gates; no assertion was weakened. |
|
||||||
| `VoxelForgeComposerStructureRollTest.cpp` | `VoxelForge.Composer.StructureRoll` | Rolls root polarity → legal shape source → polarity-derived conversion → 4–8 declaration-legal modifiers → mandatory structural posts; blends the six native parameter families independently, measures 64 novel stacks, captures the same grid for the deterministic filled/contour XZ/XY preview, runs a separate step-1 radius-64 ROI pass for the 33 survivors centred on `LargestComponentPoint`, checks exact arrival→departure connectivity, rerolls every recipe/stack for determinism, and brute-forces every uniform box verdict. World-scale run: **33/64 survival (51.6%)**, **64 distinct recipes**, **0 invalid recipes**, **715 proved custom boxes / 951,665 lattice voxels / 0 violations**, blank plan/card **4/33→5/33**, **174.854 s**, **0 refusals**. |
|
| `VoxelForgeComposerStructureRollTest.cpp` | `VoxelForge.Composer.StructureRoll` | Rolls root polarity → legal shape source → polarity-derived conversion → 4–8 declaration-legal modifiers → mandatory structural posts; blends the six native parameter families independently, measures 64 novel stacks, captures the same grid for the deterministic filled/contour XZ/XY preview, runs a separate step-1 radius-64 ROI pass for the 33 survivors centred on `LargestComponentPoint`, checks exact arrival→departure connectivity, rerolls every recipe/stack for determinism, and brute-forces every uniform box verdict. World-scale run: **33/64 survival (51.6%)**, **64 distinct recipes**, **0 invalid recipes**, **715 proved custom boxes / 951,665 lattice voxels / 0 violations**, blank plan/card **4/33→5/33**, **174.854 s**, **0 refusals**. |
|
||||||
| `VoxelForgeComposerShowcaseTest.cpp` | `VoxelForge.Composer.Showcase` | Exhausts the bounded parameter-roll set (seeds **0, 7331**, indices **0–63**), excludes multi-region rolls while the lateral gate is off, and measures every missing-family candidate in all six interior target slots at step 4 / radius 256 / `MaxCells=8,000,000`. Hard gates are non-vacuous, largest air share ≥ **0.50**, and exact unsnapped arrival→departure connectivity; selection score is floor-area fraction + clearance tie-break + projected-surface tie-break. It asserts roll/manifest determinism, density sign, zero `WorldRadiusVoxels`, bit-identical metric reruns, and writes one alphabetized card per archetype to `Saved/VoxelForge/Showcase/index.html`, with step-1 radius-64 filled/contour plan + vertical ROI images centred on `LargestComponentPoint`. The final landing regeneration evaluated **256** rolls (**82** eligible single-region, **174** skipped multi-region), produced **7 fine renders, 1 refusal, 0 blanks**, and retained a diagnostic hard-gate survivor set of **0/8**. |
|
| `VoxelForgeComposerShowcaseTest.cpp` | `VoxelForge.Composer.Showcase` | Exhausts the bounded parameter-roll set (seeds **0, 7331**, indices **0–63**), excludes multi-region rolls while the lateral gate is off, and measures every missing-family candidate in all six interior target slots at step 4 / radius 256 / `MaxCells=8,000,000`. Hard gates are non-vacuous, largest air share ≥ **0.50**, and exact unsnapped arrival→departure connectivity; selection score is floor-area fraction + clearance tie-break + projected-surface tie-break. It asserts roll/manifest determinism, density sign, zero `WorldRadiusVoxels`, bit-identical metric reruns, and writes one alphabetized card per archetype to `Saved/VoxelForge/Showcase/index.html`, with step-1 radius-64 filled/contour plan + vertical ROI images centred on `LargestComponentPoint`. The final landing regeneration evaluated **256** rolls (**82** eligible single-region, **174** skipped multi-region), produced **7 fine renders, 1 refusal, 0 blanks**, and retained a diagnostic hard-gate survivor set of **0/8**. |
|
||||||
| `VoxelForgeComposerSeasonTest.cpp` | `VoxelForge.Composer.Season` | Existing compose/review/393,216-sample round trip plus schema hash tamper rejection, two independent runtime manager/generator instances, season-authoritative seed/spine/radius, unset-season regression, and brute-force verification of every sampled non-Mixed recipe tile verdict (up to 64). |
|
| `VoxelForgeComposerSeasonTest.cpp` | `VoxelForge.Composer.Season` | Existing compose/review/393,216-sample round trip plus schema hash tamper rejection, two independent runtime manager/generator instances, season-authoritative seed/spine/radius, unset-season regression, and brute-force verification of every sampled non-Mixed recipe tile verdict (up to 64). |
|
||||||
| `VoxelForgeComposerCorpusFreeTest.cpp` | `VoxelForge.Composer.CorpusFree` | Shares each structure recipe across today's corpus blend, naive independent uniform rolls, and constraint-sampled rolls. The completed equal-arm world-scale run uses **16 candidates per arm**, step 4 / radius 256 / `MaxCells=8,000,000`, fixed passage-law mouths, and 40 box probes per candidate. Result: **10/16, 9/16, 8/16** survival; survivor feature-scale ranges are **8..268**, **8..60**, **8..92** voxels. Box checks: **185/246,235**, **260/346,060**, **299/397,969** proved/voxels, **0 violations** in every arm; constraint parameter and box-stop violations were also 0. |
|
| `VoxelForgeComposerCorpusFreeTest.cpp` | `VoxelForge.Composer.CorpusFree` | Shares each structure recipe across today's corpus blend, naive independent uniform rolls, and constraint-sampled rolls. The completed equal-arm world-scale run uses **16 candidates per arm**, step 4 / radius 256 / `MaxCells=8,000,000`, fixed passage-law mouths, and 40 box probes per candidate. Result: **10/16, 9/16, 8/16** survival; survivor feature-scale ranges are **8..268**, **8..60**, **8..92** voxels. Box checks: **185/246,235**, **260/346,060**, **299/397,969** proved/voxels, **0 violations** in every arm; constraint parameter and box-stop violations were also 0. |
|
||||||
| `VoxelForgeLayoutOrderIndependenceTest.cpp` | `VoxelForge.Determinism.LayoutOrderIndependence` | Builds a known transient soft-pointer pool, then rebuilds it in original, reversed, and swapped orders. Requires a non-empty layout and passage set, and compares every slot's definition/Z/height plus passage endpoints, landing descriptors, control geometry, and bounds bit-for-bit. |
|
| `VoxelForgeLayoutOrderIndependenceTest.cpp` | `VoxelForge.Determinism.LayoutOrderIndependence` | Builds a known transient soft-pointer pool, then rebuilds it in original, reversed, and swapped orders. Requires a non-empty layout and passage set, and compares every slot's definition/Z/height plus passage endpoints, landing descriptors, control geometry, and bounds bit-for-bit. |
|
||||||
| `VoxelForgePassageOpenSpaceTest.cpp` | `VoxelForge.Determinism.PassageLandsInOpenSpace` | Uses the real fixture density path to check both generated mouths against independent source queries, then audits all inter-strate landing ends: body-derived room dimensions, final-density flat-floor slope, local floor/join geometry, seal containment, hot-call/rebuild timing, and targeted `ClassifyTile` box soundness. Reports source-fit/refused mouths and per-archetype landing slopes; fails on any floor, join, seal, or box violation. |
|
| `VoxelForgePassageOpenSpaceTest.cpp` | `VoxelForge.Determinism.PassageLandsInOpenSpace` | Uses the real fixture density path to check both generated mouths against independent source queries (Maze/VerticalShafts/FloatingIslands footings are probed on the archetype stack built without a strate manager, i.e. without passage carving), then audits all inter-strate landing ends: body-derived room dimensions, final-density flat-floor slope, local floor/join geometry, seal containment, hot-call/rebuild timing, and targeted `ClassifyTile` box soundness. Reports source-fit/refused mouths and per-archetype landing slopes; fails on any floor, join, seal, or box violation. |
|
||||||
| `VoxelForgeStrateConnectivityTest.cpp` | `VoxelForge.Generation.StrateConnectivity` / `VoxelForge.Generation.StrateConnectivityRefinement` / `VoxelForge.Generation.VerticalShaftSeamFreedom` | Bounded strate metrics with density-polarity and solid-gap controls, deterministic route rechecks, and refinement sweeps. The refinement test uses the bounded 4-chunk density fixture, fits each measurement AABB to the arrival/departure mouth pair plus three shaft spacings (the prescribed local tree fallback), and caps the fitted control at 120,000,000 cells. It reports exact before/after seed-6 cell counts, reruns every negative with the fitted margin doubled, measures tree orphan candidates/path reachability and roughness-bubble proxies, asserts mouths remain inside the deterministic shaft feature core (the exact axis is an unsupported open cylinder), checks source/mirror physical paths, and requires 16/16 effective arrival→departure results across the 16 VerticalShafts seeds. Margin-binding negatives are reported as measurement limits, never as gap findings. The shaft seam test re-evaluates cell-boundary positions after warming distinct neighbouring chunk contexts. |
|
| `VoxelForgeStrateConnectivityTest.cpp` | `VoxelForge.Generation.StrateConnectivity` / `VoxelForge.Generation.StrateConnectivityRefinement` / `VoxelForge.Generation.VerticalShaftSeamFreedom` | Bounded strate metrics with density-polarity and solid-gap controls, deterministic route rechecks, and refinement sweeps. The refinement test uses the bounded 4-chunk density fixture, fits each measurement AABB to the arrival/departure mouth pair plus three shaft spacings (the prescribed local tree fallback), and caps the fitted control at 120,000,000 cells. It reports exact before/after seed-6 cell counts, reruns every negative with the fitted margin doubled, measures tree orphan candidates/path reachability and roughness-bubble proxies, asserts mouths remain inside the deterministic shaft feature core (the exact axis is an unsupported open cylinder), checks source/mirror physical paths, and requires 16/16 effective arrival→departure results across the 16 VerticalShafts seeds. Margin-binding negatives are reported as measurement limits, never as gap findings. The shaft seam test re-evaluates cell-boundary positions after warming distinct neighbouring chunk contexts. |
|
||||||
| `VoxelForgeMazeSeamTest.cpp` | `VoxelForge.Generation.MazeSeamFreedom` | Re-evaluates **42** cell-boundary probes after warming six different neighbouring chunk contexts in both legacy and operator-stack paths. Latest result: **0 legacy mismatches, 0 operator mismatches**. The Maze source's 2×2×2 lower-node window is deliberately local; no wide collect is involved. |
|
| `VoxelForgeMazeSeamTest.cpp` | `VoxelForge.Generation.MazeSeamFreedom` | Re-evaluates **42** cell-boundary probes through `GetDensityAt` after warming six different neighbouring chunk contexts; must report **0 mismatches**. The Maze source's 2×2×2 lower-node window is deliberately local; no wide collect is involved. |
|
||||||
| `VoxelForgeClipmapNoOverlapTest.cpp` | `VoxelForge.Streaming.ClipmapDesiredTilesNoOverlapCoverage` | Sweeps 6,174 centres (negative coords, every parity) × radius/level/render-distance/vertical-clamp/sheet combinations: **zero overlapping desired pairs AND coverage ⊇ the frozen `7486c29` selector**. Failed on `7486c29` (1,022,679 overlapping pairs) as intended. |
|
| `VoxelForgeClipmapNoOverlapTest.cpp` | `VoxelForge.Streaming.ClipmapDesiredTilesNoOverlapCoverage` | Sweeps 5,145 cases (343 centres with negative coords and every parity × 5 radius/level/render-distance settings × 3 vertical clamps): **zero overlapping desired pairs AND coverage ⊇ the frozen `7486c29` selector**. Failed on `7486c29` (1,022,679 overlapping pairs) as intended. |
|
||||||
| `VoxelForgeDensityBlockEquivalenceTest.cpp` | `VoxelForge.Density.OperatorBlockMatchesScalarGrid` | Block evaluation (`EvalBlock` after `BuildActiveOpList` pruning) vs scalar, **bit-identical**: 5.1M mesher-grid samples (6 seeds × 8 archetypes × levels 0–5, with edits) + 300k direct `EvalBlock` samples. Scopes `voxel.UseFusedEvaluator=0` so the real block path runs. An injected optimistic `FPassageCarveOp::EffectOverBox` produced 5,612 mismatches. |
|
| `VoxelForgeDensityBlockEquivalenceTest.cpp` | `VoxelForge.Density.OperatorBlockMatchesScalarGrid` | Block evaluation (`EvalBlock` after `BuildActiveOpList` pruning) vs scalar, **bit-identical**: 5.1M mesher-grid samples (6 seeds × 8 archetypes × levels 0–5, with edits) + 300k direct `EvalBlock` samples. Scopes `voxel.UseFusedEvaluator=0` so the real block path runs. An injected optimistic `FPassageCarveOp::EffectOverBox` produced 5,612 mismatches. |
|
||||||
| `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. |
|
||||||
|
| `VoxelForgeCorrectnessRoundTest.cpp` | `VoxelForge.Determinism.DiffApiBoundary` / `DiffLayerIdentity` / `EmptyPassageVersion` / `SurfaceBiomeCacheIdentity` / `TileCacheContextSwitch` / `ZeroTransitionBlend` | Regression coverage for the cold-audit correctness fixes (diff-layer API edges, cache identities, empty-passage layout version, zero transition blend). |
|
||||||
|
| `VoxelForgeDescentCorridorTest.cpp` | `VoxelForge.Descent.OwnerPassageCorridor` / `VoxelForge.Descent.OwnerSeedSweep` | Samples the authored A-to-B passage as a body-sized walking corridor in the final owner field. |
|
||||||
|
| `VoxelForgeDetSinCosTest.cpp` | `VoxelForge.Determinism.DetSinCos` | Pinned contract for `VoxelMath::DetSin` / `DetCos` / `DetSinCos`. |
|
||||||
|
| `VoxelForgeIsFiniteTest.cpp` | `VoxelForge.Math.IsFiniteExact` | Bit-exact equivalence of the generation-path finite predicate (`VoxelMath::IsFinite`). |
|
||||||
|
| `VoxelForgeLargeSeedTest.cpp` | `VoxelForge.Determinism.LargeSeedSurvives` | AUDIT C1: the world must still be a world at a large seed (bounded `VoxelHash::SeedOffset`). |
|
||||||
|
| `VoxelForgeLateralRegionsTest.cpp` | `VoxelForge.Composer.LateralRegions` | Tier 4d lateral region partition, density blending, conservative box proofs, and the arrival→departure gate (asserts `VF_LateralRegionsAreShippable` stays false while the seam rate is imperfect). |
|
||||||
|
| `VoxelForgeOpStackChannelTest.cpp` | `VoxelForge.OpStack.ChannelDAG` | Channel/resource declarations are executable stack-assembly metadata (`FVoxelOpStack::ValidateChannelOrder`). |
|
||||||
|
| `VoxelForgeScaleDiagnosisTest.cpp` | `VoxelForge.Composer.ScaleDiagnosis` | Measurement-only scale diagnosis for the player-fit showcase. |
|
||||||
|
| `VoxelForgeSealedSolidProofTest.cpp` | `VoxelForge.Determinism.SealedSolidProofSoundness` | Sealed-solid tile proof soundness and invalidation (one-sided: may say false too often, never a wrong true). |
|
||||||
|
| `VoxelForgeTilePostReachTest.cpp` | `VoxelForge.Correctness.TilePostReachProof` | The tile-reach proof must fail loudly when its envelope is deliberately made too small. |
|
||||||
|
| `VoxelForgeVerticalStrateReachTest.cpp` | `VoxelForge.Streaming.VerticalStrateReach` | Surface-strate streaming keeps the ground visible when the pawn is high in the same strate. |
|
||||||
|
| `VoxelForgeWormBlockSkipTest.cpp` | `VoxelForge.Determinism.WormBlockSkipSoundness` | Soundness and coverage of the worker-local worm N1 block proof (`voxel.WormBlockSkip`). |
|
||||||
|
| `VoxelForgePlayerFitWindow.h` | — | Shared measurement-window policy for player-fit diagnostics (test/editor side only). |
|
||||||
|
|
||||||
## 4. The density pipeline (most-edited hot path)
|
## 4. The density pipeline (most-edited hot path)
|
||||||
|
|
||||||
### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:277
|
### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749
|
||||||
Stage order (negative=solid throughout). Each stage's anchor:
|
Stage order (negative=solid throughout). Each stage's anchor:
|
||||||
| Step | Line | What |
|
| Step | Line | What |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 1 — Vertical scale | 307 | Stretch Z before noise (`VerticalScale`). |
|
| 1 — Vertical scale | 3799 | Stretch Z before noise (`VerticalScale`). |
|
||||||
| 2 — Base density | 316 | Everything starts solid at `BaseDensity`. |
|
| 2 — Base density | 3808 | Everything starts solid at `BaseDensity`. |
|
||||||
| 3 — Cave warp | 321 | Domain-warp the SDF query coords (organic shapes). |
|
| 3 — Cave warp | 3813 | Domain-warp the SDF query coords (organic shapes). |
|
||||||
| 4 — SDF morphology | 368 | Rooms+tunnels via `BuildChunkCache`/`EvaluateSDFCached`. |
|
| 4 — SDF morphology | 3863 | Rooms+tunnels via `BuildChunkCache`/`EvaluateSDFCached`. |
|
||||||
| 4b — Surface roughness | 564 | Volumetric noise near surfaces (fBM/Ridged/Mixed). |
|
| 4b — Surface roughness | 4267 | Volumetric noise near surfaces (fBM/Ridged/Mixed). |
|
||||||
| 4c–4h — Terrain ops | 688 | Per-room op applied near surfaces. Sub-anchors below. |
|
| 4c–4h — Terrain ops | 4395 | Per-room op applied near surfaces. Sub-anchors below. |
|
||||||
| · Terracing | 727 | Step-like ledges. |
|
| · Terracing | 4437 | Step-like ledges. |
|
||||||
| · Layer lines | 823 | Horizontal grooves (sin of Z). |
|
| · Layer lines | 4534 | Horizontal grooves (sin of Z). |
|
||||||
| · Ribbing | 853 | Parallel ridges (sin of Z). |
|
| · Ribbing | 4564 | Parallel ridges (sin of Z). |
|
||||||
| · Overhangs | 882 | Low-Z-freq noise shelves. |
|
| · Overhangs | 4593 | Low-Z-freq noise shelves. |
|
||||||
| · Cliff sharpening | 918 | Amplify vertical gradient. |
|
| · Cliff sharpening | 4629 | Amplify vertical gradient. |
|
||||||
| · Scallop | 962 | Cellular erosion bowls. |
|
| · Scallop | 4673 | Cellular erosion bowls. |
|
||||||
| · Arch/Bridge | 998 | Hash-placed capsules across voids. |
|
| · Arch/Bridge | 4709 | Hash-placed capsules across voids. |
|
||||||
| 4d — Columns | 1053 | Pre-baked vertical cylinders. |
|
| 4d — Columns | 4738 | Pre-baked vertical cylinders. |
|
||||||
| 4g — Domes | 1077 | Room-relative hemispherical ceilings. |
|
| 4g — Domes | 4766 | Room-relative hemispherical ceilings. |
|
||||||
| 4h — Pinch | 1142 | Passage bottlenecks. |
|
| 4h — Pinch | 4831 | Passage bottlenecks. |
|
||||||
| 5 — Worm tunnels | 1241 | abs(noise1)+abs(noise2), masked by distance-to-network (`WormNetworkRange`: braids hugging rooms/tunnels, no far-field speckle; 0 = legacy unmasked). |
|
| 5 — Worm tunnels | 4905 | abs(noise1)+abs(noise2), masked by distance-to-network (`WormNetworkRange`: braids hugging rooms/tunnels, no far-field speckle; 0 = legacy unmasked). |
|
||||||
| 6 — Boundary seal | 1274 | Solid top/bottom shells (`ApplyBoundarySeal`). |
|
| 6 — Boundary seal | 4987 | Solid top/bottom shells (`ApplyBoundarySeal`). |
|
||||||
| 7 — Inter-strate passages + landings | `GetDensityWithParams`: 2217; `GetSlabDensity`: 2434+ | Carve wandering passage tubes and deterministic local landing rooms, restore their support floors, then leave the final XY seal to the outer MC post (`ApplyPassageModifier` / `ApplyPassageLandingFloorMC`). |
|
| 7 — Inter-strate passages + landings | 5008 | Carve wandering passage tubes and deterministic local landing rooms, restore their support floors, then leave the final XY seal to the outer MC post (`ApplyPassageModifier` / `ApplyPassageLandingFloorMC`). |
|
||||||
|
|
||||||
### 4.2 `GetSlabDensity` (FlatPlain / CrystalChamber) — VoxelGenerator.cpp:1306
|
### 4.2 `BuildSlabStack` (FlatPlain / CrystalChamber) — VoxelDensityOpStack.cpp
|
||||||
| Step | Line | What |
|
| Step | Op | What |
|
||||||
|------|------|------|
|
|------|----|------|
|
||||||
| 1 — Floor surface | 1317 | Noisy floor height. |
|
| 1 — Floor surface | `FSlabVoidSource` | Noisy floor height (XY-pure). |
|
||||||
| 2 — Ceiling surface | 1343 | Formations hang downward (`abs(noise)`). |
|
| 2 — Ceiling surface | `FSlabVoidSource` | Formations hang downward (`abs(noise)`). |
|
||||||
| 3 — Void→base density | 1379 | Solid outside [floor,ceiling]. |
|
| 3 — Void→base density | `FSlabVoidSource` | Solid outside [floor,ceiling]. |
|
||||||
| 4 — Columns | 1399 | World-space hash grid, full-height. |
|
| 4 — Columns | `FGridColumnMod` | World-space hash grid, full-height. |
|
||||||
| 5+6 — Seal + passages | 1461 | Same seal, passage-tube, landing-room, connector, and support-floor path as TunnelNetwork. |
|
| 5+6 — Seal + passages | `AppendStructuralPost` | Spine, vertical seal, passage tube/landing floor, XY seal — the same structural post as every stack. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -786,25 +793,25 @@ Stage order (negative=solid throughout). Each stage's anchor:
|
|||||||
|
|
||||||
| Goal | Location |
|
| Goal | Location |
|
||||||
|------|----------|
|
|------|----------|
|
||||||
| Chunk size / voxel scale | `VoxelTypes.h:19-23` (rebuild everything). |
|
| Chunk size / voxel scale | `VoxelTypes.h:211-213` (rebuild everything). |
|
||||||
| View distance / task budget / LOD distances | `VoxelSettings.h` (no recompile of logic — data asset). |
|
| View distance / task budget / LOD distances | `VoxelSettings.h` (no recompile of logic — data asset). |
|
||||||
| LOD step mapping | `AVoxelWorld::LODToStep` VoxelWorld.cpp:268; `GetLODForChunk` :242. |
|
| LOD step mapping | `LoadTile` VoxelWorld.cpp:7329-7335: extent `CHUNK_SIZE << Level`, cells from `FullResClipLevels`/`CoarseTileCells`, `Step = Extent / Cells`; tile selection in `VoxelClipmapDesiredTiles::Build` (`Private/VoxelClipmapDesiredTiles.h`). |
|
||||||
| How chunks stream in/out | `UpdateChunksAroundPosition` VoxelWorld.cpp:362. |
|
| How chunks stream in/out | `UpdateChunksAroundPosition` VoxelWorld.cpp:6870. |
|
||||||
| Async threading / stale-result handling | `LoadTile` :445, `ProcessPendingChunks` :301, generation Epoch + per-request cancellation token. |
|
| Async threading / stale-result handling | `LoadTile` :7291, `ProcessPendingChunks` :5856, generation Epoch + per-request cancellation token. |
|
||||||
| Add a new cave feature / terrain op | Add enum in `VoxelTerrainOpDefinition.h:36`, params there, `ApplyTo` (.cpp:6), transport fields in `FStrateGenerationParams`, consume it in a new Step inside `GetDensityWithParams`. |
|
| Add a new cave feature / terrain op | Add enum in `VoxelTerrainOpDefinition.h:36`, params there, `ApplyTo` (.cpp:6), transport fields in `FStrateGenerationParams`, consume it in a new Step inside `GetDensityWithParams`. |
|
||||||
| Tweak room/tunnel shapes | `VoxelCaveMorphology.cpp` `BuildChunkCache` :47 / `EvaluateSDFCached` :757. |
|
| Tweak room/tunnel shapes | `VoxelCaveMorphology.cpp` `BuildChunkCache` :6782 / `EvaluateSDFCached` :8569. |
|
||||||
| Worm tunnel behavior | `GetDensityWithParams` Step 5, VoxelGenerator.cpp:1241. |
|
| Worm tunnel behavior | `GetDensityWithParams` Step 5, VoxelGenerator.cpp:4905. |
|
||||||
| Strate stacking / which strate where | `UVoxelStrateManager::Initialize` :10. |
|
| Strate stacking / which strate where | `UVoxelStrateManager::Initialize` :1164. |
|
||||||
| Publish/use an offline season | Create `UVoxelSeasonAsset`, set `SourceManifestJson`, press `ImportSeasonManifestJson`, then assign it to `UVoxelSettings::Season`. |
|
| Publish/use an offline season | Create `UVoxelSeasonAsset`, set `SourceManifestJson`, press `ImportSeasonManifestJson`, then assign it to `UVoxelSettings::Season`. |
|
||||||
| Boundary blend between strates | `GetGenerationParams` :515 + `FStrateGenerationParams::Lerp` (expands `VF_STRATE_PARAM_FIELDS`, StrateTypes.h — new fields go in that list). |
|
| Boundary blend between strates | `GetGenerationParams` :6509 + `FStrateGenerationParams::Lerp` (expands `VF_STRATE_PARAM_FIELDS`, StrateTypes.h — new fields go in that list). |
|
||||||
| Passages / landing geometry between strates | `GeneratePassages` :146 + `EvaluateModifierSDF` :371 + `ApplyPassageModifier` / `ApplyPassageLandingFloorMC` (StrateManager.cpp) + `ClassifyTile` floor guard (Generator.cpp). |
|
| Passages / landing geometry between strates | `GeneratePassages` :1659 + `EvaluateModifierSDF` :2643 + `ApplyPassageModifier` / `ApplyPassageLandingFloorMC` (StrateManager.cpp) + `ClassifyTile` floor guard (Generator.cpp). |
|
||||||
| Player carve/fill | `CarveAtPosition`/`FillAtPosition` VoxelWorld.cpp:691/709 → `UVoxelDiffLayer::ApplyModification` :63. |
|
| Player carve/fill | `CarveAtPosition`/`FillAtPosition` VoxelWorld.cpp:8413/8422 → `UVoxelDiffLayer::ApplyModification` :185. |
|
||||||
| Mesh smoothness / normals | Grid-gradient in `GenerateMesh` (`GradAt` lambda), `IsoLevel` (h). |
|
| Mesh smoothness / normals | Grid-gradient in `GenerateMesh` (`GradAt` lambda), `IsoLevel` (h). |
|
||||||
| New slab/flat-world generator | `GetSlabDensity` Generator.cpp:1306 + `FSlabGenerationParams` (StrateTypes.h:1019). |
|
| New slab/flat-world generator | `VoxelDensityOps::BuildSlabStack` + `FSlabVoidSource` / `FGridColumnMod` (VoxelDensityOpStack.cpp) + `FSlabGenerationParams` (StrateTypes.h:1223). |
|
||||||
| Biome placement / layout | `BiomeMapParams` on the strate (cell size, warp, climate freqs) + each biome's climate box. Bake `AVoxelWorld::BakeBiomePreview` to tune. §8.14. |
|
| Biome placement / layout | `BiomeMapParams` on the strate (cell size, warp, climate freqs) + each biome's climate box. Bake `AVoxelWorld::BakeBiomePreview` to tune. §8.14. |
|
||||||
| What a biome does to terrain | A full archetype param override on the biome (`bOverrideTerrain` + `SurfaceParams`); surface output-blends dominant/neighbour heights in `GetSurfaceDensity`. Caves = content/atmosphere only (determinism, §8.14). |
|
| What a biome does to terrain | A full archetype param override on the biome (`bOverrideTerrain` + `SurfaceParams`); surface output-blends dominant/neighbour heights in the surface stack (`BuildSurfaceStack` with per-biome params) and in `ComputeSurfaceColumn`. Caves = content/atmosphere only (determinism, §8.14). |
|
||||||
| Add a biome / biome content | New `UVoxelBiomeDefinition` asset → add to the strate's `Biomes[]`. §8.14 / §8.12. |
|
| Add a biome / biome content | New `UVoxelBiomeDefinition` asset → add to the strate's `Biomes[]`. §8.14 / §8.12. |
|
||||||
| Season reset | `AVoxelWorld::ChangeSeed` :740. |
|
| Season reset | `AVoxelWorld::ChangeSeed` :8710. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -36,7 +36,7 @@ composer fills in; nobody opens it in the editor any more.
|
|||||||
|
|
||||||
### This is op-stack "Phase 3", and it is REOPENED
|
### This is op-stack "Phase 3", and it is REOPENED
|
||||||
|
|
||||||
The 2026-08-16 `OPSTACK-HANDOFF.md` says in bold *"Do not start Phase 3."* **That is superseded.**
|
The 2026-08-16 `Docs/archive/OPSTACK-HANDOFF.md` says in bold *"Do not start Phase 3."* **That is superseded.**
|
||||||
Asked what the world was missing, Jahni described exactly Phase 3 plus a composer above it.
|
Asked what the world was missing, Jahni described exactly Phase 3 plus a composer above it.
|
||||||
|
|
||||||
It is **not** a restart of the refactor. The eight operators are live, composable, and production. What
|
It is **not** a restart of the refactor. The eight operators are live, composable, and production. What
|
||||||
@@ -1490,7 +1490,7 @@ roughness and should stay late in the stack.
|
|||||||
|
|
||||||
Kept because each cost real time or would otherwise be repeated.
|
Kept because each cost real time or would otherwise be repeated.
|
||||||
|
|
||||||
- ⛔ **"Do not start Phase 3" is SUPERSEDED.** `OPSTACK-HANDOFF.md` still says it; believe this file.
|
- ⛔ **"Do not start Phase 3" is SUPERSEDED.** `Docs/archive/OPSTACK-HANDOFF.md` still says it; believe this file.
|
||||||
- ⛔ **Jahni does not want to author strates either.** The composer must invent them. An earlier reading
|
- ⛔ **Jahni does not want to author strates either.** The composer must invent them. An earlier reading
|
||||||
had him authoring strates while the system arranged them — one level too shallow, and it shaped a
|
had him authoring strates while the system arranged them — one level too shallow, and it shaped a
|
||||||
whole afternoon of wrong design.
|
whole afternoon of wrong design.
|
||||||
@@ -1553,7 +1553,7 @@ build succeeded (14 actions). The full headless suite reported **22 succeeded, 0
|
|||||||
determinism, box-verdict, and connectivity checks stayed green. The 4b→4h fuse was deliberately
|
determinism, box-verdict, and connectivity checks stayed green. The 4b→4h fuse was deliberately
|
||||||
not applied after the intervening-channel audit above.
|
not applied after the intervening-channel audit above.
|
||||||
|
|
||||||
**⭐ This closes the pending-verification thread opened by `OPSTACK-HANDOFF.md`.** That file flagged
|
**⭐ This closes the pending-verification thread opened by `Docs/archive/OPSTACK-HANDOFF.md`.** That file flagged
|
||||||
commits `4d33321` (Sol's boundary fold) and `91585ea` as *built but NOT re-verified*, with two
|
commits `4d33321` (Sol's boundary fold) and `91585ea` as *built but NOT re-verified*, with two
|
||||||
acceptance conditions. Both now met:
|
acceptance conditions. Both now met:
|
||||||
- **`violations` = 0** everywhere. Measured: TunnelNetwork at production defaults **12 of 40 tiles
|
- **`violations` = 0** everywhere. Measured: TunnelNetwork at production defaults **12 of 40 tiles
|
||||||
|
|||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+39
-13
@@ -49,19 +49,8 @@ Legend: ✅ verified against code · ◻️ checklist box.
|
|||||||
> ("nothing is set in stone"). ✅ BUILT & WORKING together with pass 2 (ticked 2026-07-27):
|
> ("nothing is set in stone"). ✅ BUILT & WORKING together with pass 2 (ticked 2026-07-27):
|
||||||
> • **Terracing gradient Z-only** (6→2 SDF samples — see the ticked item above).
|
> • **Terracing gradient Z-only** (6→2 SDF samples — see the ticked item above).
|
||||||
> • **Lerp X-macro** + **BakeRoomFeature dedupe** (both ticked above, bit-identical).
|
> • **Lerp X-macro** + **BakeRoomFeature dedupe** (both ticked above, bit-identical).
|
||||||
> • **T2.b LOD octave drop** — opt-in `UVoxelSettings::LODOctaveDrop` (default 0 = byte-identical);
|
> • **T2.b / T2.c / T2.d and F2** shipped in the same batch — tracked in `fable-idea.md` (Tier 2
|
||||||
> `VoxelGenLOD::OctaveBias` thread_local set per tile in `GenerateMesh`, per-voxel fractal sites
|
> status, F2) and ARCHITECTURE §8.10, not repeated here.
|
||||||
> wrapped in `VoxelGenLOD::Eff(N)`, XY-field noise deliberately excluded. ARCHITECTURE §8.10.
|
|
||||||
> • **T2.c tile component pool** — `TileComponentPool` + `Acquire/ReleaseTileComponent`; unload
|
|
||||||
> parks (RemoveSectionGroup strips geometry+collision, hidden, stays registered), apply pops.
|
|
||||||
> Bonus: `ApplyMeshToTile` reuses the existing `URealtimeMesh` (`GetRealtimeMeshAs`) — the old
|
|
||||||
> unconditional `InitializeRealtimeMesh` allocated + orphaned one mesh UObject PER APPLY.
|
|
||||||
> • **T2.d worker clamp** — `GetMaxConcurrentTasks()` caps the asset budget to logical cores − 2
|
|
||||||
> (the BackgroundNormal priority half had already shipped). **Tier 2 is now COMPLETE** —
|
|
||||||
> T2.a was already done (float SSE `VoxelNoise` core, §8.10).
|
|
||||||
> • **F2 determinism validator** — `AVoxelWorld::ValidateDeterminism` CallInEditor button:
|
|
||||||
> boundary points sampled under two cache-window alignments + a repeat pass; any non-zero
|
|
||||||
> delta = §8.4 regression. The tool that VERIFIES all the "bit-identical" claims above.
|
|
||||||
|
|
||||||
## Performance
|
## Performance
|
||||||
- [x] ~~**`VoxelGenerator.cpp` (terrain-op gradient)** — compute once per voxel and reuse.~~
|
- [x] ~~**`VoxelGenerator.cpp` (terrain-op gradient)** — compute once per voxel and reuse.~~
|
||||||
@@ -116,6 +105,43 @@ Legend: ✅ verified against code · ◻️ checklist box.
|
|||||||
two-grid deco redesign (`StreamTier` / `DecorationNearRadiusChunks`).
|
two-grid deco redesign (`StreamTier` / `DecorationNearRadiusChunks`).
|
||||||
**CORRECTION: `GetStrateChunkZBounds` is NOT dead** — `BuildDesiredTiles` uses it for the
|
**CORRECTION: `GetStrateChunkZBounds` is NOT dead** — `BuildDesiredTiles` uses it for the
|
||||||
strate-aware vertical clamp; struck from the dead list.
|
strate-aware vertical clamp; struck from the dead list.
|
||||||
|
- [x] **2026-09 cleanup (`cleanup/2026-09`)** — the far sheet ring runtime (`GenerateSheetMesh` + its
|
||||||
|
streaming path) and ~40 unreferenced functions/members/types removed (each proven by a search of
|
||||||
|
`Source/`, `Tools/`, `Config/` and build files; see that branch's `refactor(dead-code)` commit).
|
||||||
|
Reflected (`UPROPERTY`/`UFUNCTION`/`UENUM`…) and enum candidates were NOT removed — see
|
||||||
|
*Owner decisions* below.
|
||||||
|
|
||||||
|
## Owner decisions (2026-09 cleanup)
|
||||||
|
Items left for the owner: unused data that assets, Blueprints, tools or tests may reference by name,
|
||||||
|
and changes that would alter generated terrain.
|
||||||
|
- [x] `UVoxelSettings::bFarSheetRing` / `FarSheetSpanLevels`, `voxel.TestFarSheetRing` and the
|
||||||
|
selector's sheet branch — removed (owner go-ahead); the harness no longer passes the switch and the
|
||||||
|
`sheet_tile` dump field stays for the parity compare.
|
||||||
|
- [x] `UVoxelSettings::CeilingViewMultiplier` / `CeilingBandChunks` / `DensityVolumeMaxTasks` — removed
|
||||||
|
(never read).
|
||||||
|
- [x] **Operator stack as the only density path** (cleanup/2026-09, owner-approved). Every strate —
|
||||||
|
authored, cooked-season (recipe or native) and editor composer candidate — now generates through its
|
||||||
|
operator stack; a degenerate strate is air. Removed: `UVoxelStrateDefinition::bUseOperatorStack`, the
|
||||||
|
archetype `switch` in `GetDensityAt`, `GetSlabDensity` / `GetMazeDensity` / `GetSurfaceDensity` /
|
||||||
|
`SurfaceDensityFromColumn` / `GetVerticalShaftDensity` / `GetFloatingIslandDensity`, the four
|
||||||
|
`Cave Bail Not Op Stack *` stats, the startup opt-in warning, commandlet `-opstack`, and the
|
||||||
|
legacy-vs-stack comparisons in the tests. **Terrain changes for any strate asset that did not
|
||||||
|
already have the opt-in ticked.**
|
||||||
|
- [ ] **No-strate-manager fallback world.** A world whose settings have no season, pool or fixed
|
||||||
|
strates runs `GetDensityAt`'s fallback: default `FStrateGenerationParams` through
|
||||||
|
`GetDensityWithParams` with its direct-caller ("legacy") structural posts. It is the only caller of
|
||||||
|
that post path outside tests/commandlet diagnostics. Decide whether an empty configuration should
|
||||||
|
keep generating generic caves or refuse to generate.
|
||||||
|
- [ ] `UVoxelStrateDefinition::WaterColor`, `Creatures`, `AmbientSound`, `Music`, `MusicVolume` and the
|
||||||
|
`FStrateCreature` / `FStrateAmbientActor` spawn fields (`SpawnChancePerChunk`, `MaxPerStrate`,
|
||||||
|
`SpawnWeight`) — authored data with no consuming system yet (F9 audio, creatures).
|
||||||
|
- [ ] `UVoxelBiomeDefinition::WaterMaterial` — never read; the water plane uses the strate's
|
||||||
|
`WaterMaterial` only.
|
||||||
|
- [ ] Plain C++ enums with no user: `EVoxelFace` (`VoxelTypes.h`), `EVoxelSeasonSelectionReason`
|
||||||
|
(`VoxelSeasonManifest.h`), `EVoxelOpCombine` (`VoxelDensityOp.h` — its comment carries the
|
||||||
|
two-channel sign convention; keep that text if the enum goes).
|
||||||
|
- [ ] `IVoxelHeightOp::MaxDisplacement` / `FVoxelHeightStack::MaxTotalDisplacement` — no caller; kept as
|
||||||
|
the documented hook for a future heightfield `ClassifyBox`.
|
||||||
|
|
||||||
## Over-complexity (behavior-preserving splits, optional)
|
## Over-complexity (behavior-preserving splits, optional)
|
||||||
|
|
||||||
|
|||||||
@@ -39,12 +39,8 @@
|
|||||||
#include "Misc/AutomationTest.h"
|
#include "Misc/AutomationTest.h"
|
||||||
|
|
||||||
#include "VoxelForgeTestFixture.h"
|
#include "VoxelForgeTestFixture.h"
|
||||||
// Inclus ici DÉLIBÉRÉMENT : VoxelDensityOp.h n'est encore inclus par aucun .cpp, donc le
|
// Le fold défini par VoxelDensityOp.h prétend reproduire ClassifyTile : ce fichier le teste.
|
||||||
// compilateur ne le verrait jamais. Le fold qu'il définit prétend reproduire ClassifyTile — ce
|
// The fold defined in VoxelDensityOp.h claims to reproduce ClassifyTile; this file tests it.
|
||||||
// fichier est l'endroit naturel pour que cette prétention soit à la fois COMPILÉE et TESTÉE.
|
|
||||||
// Deliberately included here: VoxelDensityOp.h is not yet included by any .cpp, so the compiler
|
|
||||||
// would never see it. Its fold claims to reproduce ClassifyTile, so this is the natural place for
|
|
||||||
// that claim to be both compiled and tested.
|
|
||||||
#include "VoxelDensityOp.h"
|
#include "VoxelDensityOp.h"
|
||||||
|
|
||||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||||
@@ -153,7 +149,7 @@ bool FVoxelForgeClassifyTileTest::RunTest(const FString& Parameters)
|
|||||||
|
|
||||||
AddInfo(FString::Printf(
|
AddInfo(FString::Printf(
|
||||||
TEXT("ClassifyTile verdicts over %d scanned tiles: Mixed %d, AllSolid %d, AllAir %d ")
|
TEXT("ClassifyTile verdicts over %d scanned tiles: Mixed %d, AllSolid %d, AllAir %d ")
|
||||||
TEXT("(brute-forcing %d of them). Cave archetypes now use the opt-in operator-stack ")
|
TEXT("(brute-forcing %d of them). Cave archetypes use the operator-stack ")
|
||||||
TEXT("ClassifyBox fold; the global XY edge proof also skips proven outer-shell tiles. A low ")
|
TEXT("ClassifyBox fold; the global XY edge proof also skips proven outer-shell tiles. A low ")
|
||||||
TEXT("non-Mixed count is expected: every uncertain case remains Mixed for safety."),
|
TEXT("non-Mixed count is expected: every uncertain case remains Mixed for safety."),
|
||||||
NumTilesScanned, NumMixed, NumAllSolid, NumAllAir, ToVerify.Num()));
|
NumTilesScanned, NumMixed, NumAllSolid, NumAllAir, ToVerify.Num()));
|
||||||
@@ -215,10 +211,10 @@ bool FVoxelForgeClassifyTileTest::RunTest(const FString& Parameters)
|
|||||||
TestEqual(TEXT("no tile was classified uniform while containing a surface (a false verdict is a hole)"),
|
TestEqual(TEXT("no tile was classified uniform while containing a surface (a false verdict is a hole)"),
|
||||||
NumHoles, 0);
|
NumHoles, 0);
|
||||||
|
|
||||||
// ── Stabilité du verdict : ClassifyTile partage GSurfColCache avec GetDensityAt, donc le
|
// ── Stabilité du verdict : le premier ClassifyTile a réchauffé son cache de colonnes et le
|
||||||
// brute-force ci-dessus a réchauffé les caches. Re-classifier doit rendre le MÊME verdict.
|
// brute-force ci-dessus les caches de GetDensityAt. Re-classifier doit rendre le MÊME verdict.
|
||||||
// Verdict stability: ClassifyTile shares GSurfColCache with GetDensityAt, so the brute force
|
// Verdict stability: the first ClassifyTile warmed its column cache and the brute force
|
||||||
// above warmed the caches. Re-classifying must yield the SAME verdict.
|
// above warmed GetDensityAt's caches. Re-classifying must yield the SAME verdict.
|
||||||
for (const FTileSpec& Spec : ToVerify)
|
for (const FTileSpec& Spec : ToVerify)
|
||||||
{
|
{
|
||||||
const EVoxelTileClass A = Gen->ClassifyTile(Spec.Origin, Spec.Step, Spec.Cells);
|
const EVoxelTileClass A = Gen->ClassifyTile(Spec.Origin, Spec.Step, Spec.Cells);
|
||||||
@@ -237,25 +233,24 @@ bool FVoxelForgeClassifyTileTest::RunTest(const FString& Parameters)
|
|||||||
}
|
}
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// LE CHEMIN PILE D'OPÉRATEURS DE ClassifyTile — MÊME FORCE BRUTE, MONDE OPT-IN
|
// LE CHEMIN PILE D'OPÉRATEURS DE ClassifyTile — MÊME FORCE BRUTE, STRATES DE CAVE
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// `ClassifyTile` rendait `Mixed` sans appel pour tout archétype de CAVE. Il consulte désormais
|
// Pour un slot de CAVE, `ClassifyTile` consulte `FVoxelOpStack::ClassifyBox` — donc **la pile peut
|
||||||
// `FVoxelOpStack::ClassifyBox` quand la strate a coché `bUseOperatorStack` — donc **un tout nouveau
|
// faire sauter le maillage d'une tuile**, et son erreur est un TROU : pas de triangles, pas de
|
||||||
// chemin peut faire sauter le maillage d'une tuile**, et son erreur est un TROU : pas de triangles,
|
// collision, invisible jusqu'à ce qu'un joueur tombe au travers.
|
||||||
// pas de collision, invisible jusqu'à ce qu'un joueur tombe au travers.
|
|
||||||
//
|
//
|
||||||
// Ce test est le même oracle par force brute que `ClassifyTileSoundness`, sur un monde dont TOUTES
|
// Ce test est le même oracle par force brute que `ClassifyTileSoundness`, avec un tirage uniforme
|
||||||
// les strates ont coché la case. Il ne vérifie pas le pliage (c'est `BoxVerdictFold`) ni les
|
// sur tout le layout. Il ne vérifie pas le pliage (c'est `BoxVerdictFold`) ni les opérateurs : il
|
||||||
// opérateurs (ce sont les huit tests d'équivalence) : il vérifie le **câblage** — que la pile
|
// vérifie le **câblage** — que la pile interrogée par le classifieur est bien celle qui produit la
|
||||||
// interrogée par le classifieur est bien celle qui produit la densité, params, drapeau et
|
// densité, params et disturbances compris.
|
||||||
// disturbances compris.
|
|
||||||
//
|
//
|
||||||
// ⚠️ LE COMPTEUR À LIRE EN PREMIER est le nombre de tuiles réellement brute-forcées. Un run vert
|
// ⚠️ LE COMPTEUR À LIRE EN PREMIER est le nombre de tuiles réellement brute-forcées. Un run vert
|
||||||
// avec zéro verdict non-Mixed ne prouverait RIEN — exactement le piège que ce fichier documente
|
// avec zéro verdict non-Mixed ne prouverait RIEN — exactement le piège que ce fichier documente
|
||||||
// depuis sa première version, et la raison pour laquelle l'absence de verdict est une ERREUR ici.
|
// depuis sa première version, et la raison pour laquelle l'absence de verdict est une ERREUR ici.
|
||||||
//
|
//
|
||||||
// Same brute-force oracle as ClassifyTileSoundness, on a world where every strate has opted in.
|
// Same brute-force oracle as ClassifyTileSoundness, drawn uniformly over the whole layout so cave
|
||||||
// It checks the WIRING, not the fold and not the operators.
|
// slots (which classify through their stack) dominate. It checks the WIRING, not the fold and not
|
||||||
|
// the operators.
|
||||||
|
|
||||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||||
FVoxelForgeOpStackClassifyTileTest,
|
FVoxelForgeOpStackClassifyTileTest,
|
||||||
@@ -266,11 +261,10 @@ bool FVoxelForgeOpStackClassifyTileTest::RunTest(const FString& Parameters)
|
|||||||
{
|
{
|
||||||
using namespace VoxelForgeTest;
|
using namespace VoxelForgeTest;
|
||||||
|
|
||||||
// ⚠️ `bUseOperatorStack = true` sur toutes les strates : c'est LE point du test. La fixture
|
// La fixture donne à ce monde une `LayoutVersion` unique dans le processus, sans quoi les caches
|
||||||
// donne à ce monde une `LayoutVersion` unique dans le processus, sans quoi les caches par chunk
|
// par chunk de `GetDensityAt` — dont `CP_UseOpStack` — pourraient encore porter ceux d'un autre test.
|
||||||
// de `GetDensityAt` — dont `CP_UseOpStack` — pourraient encore porter ceux d'un autre test.
|
|
||||||
FTestWorld World;
|
FTestWorld World;
|
||||||
World.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/true);
|
World.Build(/*Seed*/1337, /*GapChunks*/2);
|
||||||
if (!World.IsValid())
|
if (!World.IsValid())
|
||||||
{
|
{
|
||||||
AddError(World.WhyInvalid());
|
AddError(World.WhyInvalid());
|
||||||
@@ -287,8 +281,8 @@ bool FVoxelForgeOpStackClassifyTileTest::RunTest(const FString& Parameters)
|
|||||||
const int32 BottomVoxelZ = World.BottomChunkZ * CHUNK_SIZE;
|
const int32 BottomVoxelZ = World.BottomChunkZ * CHUNK_SIZE;
|
||||||
|
|
||||||
// Tirage uniforme sur tout le layout, PAS biaisé vers SurfaceWorld comme l'autre test : ici ce
|
// Tirage uniforme sur tout le layout, PAS biaisé vers SurfaceWorld comme l'autre test : ici ce
|
||||||
// sont précisément les strates de cave qui intéressent, puisque ce sont elles qui passent par le
|
// sont précisément les strates de cave qui intéressent, puisque ce sont elles que ClassifyTile
|
||||||
// nouveau chemin. SurfaceWorld continue d'être prouvé par le code écrit à la main.
|
// juge par leur pile. SurfaceWorld est prouvé par le test colonne écrit à la main.
|
||||||
for (int32 t = 0; t < NumTilesScanned; ++t)
|
for (int32 t = 0; t < NumTilesScanned; ++t)
|
||||||
{
|
{
|
||||||
FTileSpec Spec;
|
FTileSpec Spec;
|
||||||
@@ -316,20 +310,17 @@ bool FVoxelForgeOpStackClassifyTileTest::RunTest(const FString& Parameters)
|
|||||||
|
|
||||||
AddInfo(FString::Printf(
|
AddInfo(FString::Printf(
|
||||||
TEXT("ClassifyTile ON THE OPERATOR-STACK PATH, %d scanned tiles: Mixed %d, AllSolid %d, ")
|
TEXT("ClassifyTile ON THE OPERATOR-STACK PATH, %d scanned tiles: Mixed %d, AllSolid %d, ")
|
||||||
TEXT("AllAir %d (brute-forcing %d). Compare with VoxelForge.Determinism.ClassifyTileSoundness, ")
|
TEXT("AllAir %d (brute-forcing %d). Every cave-slot verdict is a tile the mesher skips on ")
|
||||||
TEXT("which runs the SAME scan on a world that has NOT opted in: every verdict beyond what ")
|
TEXT("the strength of the stack's ClassifyBox fold -- and a hole if the wiring is wrong, ")
|
||||||
TEXT("that test reports is a tile the mesher now skips and did not before. That difference ")
|
TEXT("which is what the brute force below is for."),
|
||||||
TEXT("IS the T1.d prize OPSTACK-PLAN has been aiming at -- and every one of those tiles is a ")
|
|
||||||
TEXT("hole if the wiring is wrong, which is what the brute force below is for."),
|
|
||||||
NumTilesScanned, NumMixed, NumAllSolid, NumAllAir, ToVerify.Num()));
|
NumTilesScanned, NumMixed, NumAllSolid, NumAllAir, ToVerify.Num()));
|
||||||
|
|
||||||
if (ToVerify.Num() == 0)
|
if (ToVerify.Num() == 0)
|
||||||
{
|
{
|
||||||
AddError(TEXT("VACUOUS: not one tile got a non-Mixed verdict on the operator-stack path, so ")
|
AddError(TEXT("VACUOUS: not one tile got a non-Mixed verdict on the operator-stack path, so ")
|
||||||
TEXT("this test verified NOTHING about the new wiring. Either no strate actually ")
|
TEXT("this test verified NOTHING about the wiring. Every guard in the cave branch ")
|
||||||
TEXT("opted in (check FTestWorld::Build's bUseOperatorStack), or every guard in the ")
|
TEXT("of ClassifyTile bailed to Mixed -- the params-identical check and the ")
|
||||||
TEXT("cave branch of ClassifyTile bailed to Mixed -- the params-identical check and ")
|
TEXT("27-chunk-coord cap are the likeliest. Do NOT read a green run as proof."));
|
||||||
TEXT("the 27-chunk-coord cap are the likeliest. Do NOT read a green run as proof."));
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -122,22 +122,15 @@ namespace
|
|||||||
const int32 MaxLevel = FMath::Clamp(Parameters.MaxClipLevel, 0, 8);
|
const int32 MaxLevel = FMath::Clamp(Parameters.MaxClipLevel, 0, 8);
|
||||||
const int32 OuterRadius = VoxelClipmapDesiredTiles::OuterShellRadius(
|
const int32 OuterRadius = VoxelClipmapDesiredTiles::OuterShellRadius(
|
||||||
Parameters, Radius, MaxLevel);
|
Parameters, Radius, MaxLevel);
|
||||||
int32 SheetLevel = MaxLevel;
|
|
||||||
int32 SheetRadius = OuterRadius;
|
|
||||||
VoxelClipmapDesiredTiles::OuterShell(
|
|
||||||
Parameters, Radius, MaxLevel, SheetLevel, SheetRadius);
|
|
||||||
const bool bSheetRing = SheetLevel > MaxLevel;
|
|
||||||
const int32 MaxMCRingLevel = bSheetRing ? SheetLevel - 1 : MaxLevel;
|
|
||||||
|
|
||||||
for (int32 Level = 0; Level <= MaxMCRingLevel; ++Level)
|
for (int32 Level = 0; Level <= MaxLevel; ++Level)
|
||||||
{
|
{
|
||||||
const int32 Pow = 1 << Level;
|
const int32 Pow = 1 << Level;
|
||||||
const FIntVector CenterLevel = VoxelClipmapDesiredTiles::FloorDiv(Center, Pow);
|
const FIntVector CenterLevel = VoxelClipmapDesiredTiles::FloorDiv(Center, Pow);
|
||||||
const FIntVector CenterFiner = Level > 0
|
const FIntVector CenterFiner = Level > 0
|
||||||
? VoxelClipmapDesiredTiles::FloorDiv(Center, Pow >> 1)
|
? VoxelClipmapDesiredTiles::FloorDiv(Center, Pow >> 1)
|
||||||
: FIntVector::ZeroValue;
|
: FIntVector::ZeroValue;
|
||||||
const int32 LevelRadius = (Level == MaxLevel && !bSheetRing)
|
const int32 LevelRadius = (Level == MaxLevel) ? OuterRadius : Radius;
|
||||||
? OuterRadius : Radius;
|
|
||||||
|
|
||||||
int32 DeltaZMin = -LevelRadius;
|
int32 DeltaZMin = -LevelRadius;
|
||||||
int32 DeltaZMax = LevelRadius;
|
int32 DeltaZMax = LevelRadius;
|
||||||
@@ -178,50 +171,6 @@ namespace
|
|||||||
OutTiles.Emplace(Tile, Level);
|
OutTiles.Emplace(Tile, Level);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bSheetRing)
|
|
||||||
{
|
|
||||||
const int32 SheetPow = 1 << SheetLevel;
|
|
||||||
const FIntVector CenterSheet = VoxelClipmapDesiredTiles::FloorDiv(Center, SheetPow);
|
|
||||||
const FIntVector CenterMC = VoxelClipmapDesiredTiles::FloorDiv(
|
|
||||||
Center, 1 << MaxMCRingLevel);
|
|
||||||
const int32 LevelDelta = SheetLevel - MaxMCRingLevel;
|
|
||||||
|
|
||||||
int32 DeltaZMin = -SheetRadius;
|
|
||||||
int32 DeltaZMax = SheetRadius;
|
|
||||||
if (MinChunkZ != MIN_int32)
|
|
||||||
{
|
|
||||||
DeltaZMin = FMath::Max(DeltaZMin,
|
|
||||||
VoxelClipmapDesiredTiles::FloorDiv(MinChunkZ, SheetPow) - CenterSheet.Z);
|
|
||||||
DeltaZMax = FMath::Min(DeltaZMax,
|
|
||||||
VoxelClipmapDesiredTiles::FloorDiv(MaxChunkZ, SheetPow) - CenterSheet.Z);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int32 DeltaZ = DeltaZMin; DeltaZ <= DeltaZMax; ++DeltaZ)
|
|
||||||
for (int32 DeltaY = -SheetRadius; DeltaY <= SheetRadius; ++DeltaY)
|
|
||||||
for (int32 DeltaX = -SheetRadius; DeltaX <= SheetRadius; ++DeltaX)
|
|
||||||
{
|
|
||||||
const FIntVector Tile = CenterSheet + FIntVector(DeltaX, DeltaY, DeltaZ);
|
|
||||||
const int32 TileMinZ = Tile.Z << SheetLevel;
|
|
||||||
const int32 TileMaxZ = ((Tile.Z + 1) << SheetLevel) - 1;
|
|
||||||
if (TileMaxZ < MinChunkZ || TileMinZ > MaxChunkZ)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool bCovered =
|
|
||||||
((Tile.X << LevelDelta) >= CenterMC.X - Radius)
|
|
||||||
&& ((((Tile.X + 1) << LevelDelta) - 1) <= CenterMC.X + Radius)
|
|
||||||
&& ((Tile.Y << LevelDelta) >= CenterMC.Y - Radius)
|
|
||||||
&& ((((Tile.Y + 1) << LevelDelta) - 1) <= CenterMC.Y + Radius)
|
|
||||||
&& ((Tile.Z << LevelDelta) >= CenterMC.Z - Radius)
|
|
||||||
&& ((((Tile.Z + 1) << LevelDelta) - 1) <= CenterMC.Z + Radius);
|
|
||||||
if (!bCovered)
|
|
||||||
{
|
|
||||||
OutTiles.Emplace(Tile, SheetLevel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static FString DescribeMeasure(const FOverlapMeasure& Measure)
|
static FString DescribeMeasure(const FOverlapMeasure& Measure)
|
||||||
@@ -245,7 +194,6 @@ bool FVoxelForgeClipmapNoOverlapTest::RunTest(const FString& Parameters)
|
|||||||
Owner.ClipRadius = 3;
|
Owner.ClipRadius = 3;
|
||||||
Owner.MaxClipLevel = 5;
|
Owner.MaxClipLevel = 5;
|
||||||
Owner.RenderDistanceChunks = 768;
|
Owner.RenderDistanceChunks = 768;
|
||||||
Owner.bFarSheetRing = false;
|
|
||||||
|
|
||||||
FParameters Defaults;
|
FParameters Defaults;
|
||||||
|
|
||||||
@@ -291,16 +239,13 @@ bool FVoxelForgeClipmapNoOverlapTest::RunTest(const FString& Parameters)
|
|||||||
int32 Radius;
|
int32 Radius;
|
||||||
int32 MaxLevel;
|
int32 MaxLevel;
|
||||||
int32 RenderDistance;
|
int32 RenderDistance;
|
||||||
bool bSheet;
|
|
||||||
int32 SheetSpan;
|
|
||||||
};
|
};
|
||||||
static const FSweepSettings SweepSettings[] = {
|
static const FSweepSettings SweepSettings[] = {
|
||||||
{1, 0, 0, false, 2},
|
{1, 0, 0},
|
||||||
{2, 2, 0, false, 2},
|
{2, 2, 0},
|
||||||
{3, 4, 0, false, 2},
|
{3, 4, 0},
|
||||||
{2, 3, 49, false, 2},
|
{2, 3, 49},
|
||||||
{2, 3, 49, true, 2},
|
{2, 4, 87},
|
||||||
{2, 4, 87, true, 3},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
int32 SweptCases = 0;
|
int32 SweptCases = 0;
|
||||||
@@ -319,8 +264,6 @@ bool FVoxelForgeClipmapNoOverlapTest::RunTest(const FString& Parameters)
|
|||||||
P.ClipRadius = Sweep.Radius;
|
P.ClipRadius = Sweep.Radius;
|
||||||
P.MaxClipLevel = Sweep.MaxLevel;
|
P.MaxClipLevel = Sweep.MaxLevel;
|
||||||
P.RenderDistanceChunks = Sweep.RenderDistance;
|
P.RenderDistanceChunks = Sweep.RenderDistance;
|
||||||
P.bFarSheetRing = Sweep.bSheet;
|
|
||||||
P.FarSheetSpanLevels = Sweep.SheetSpan;
|
|
||||||
|
|
||||||
// Exercise the unbounded view, a tight local clamp, and a clamp crossing a tile boundary.
|
// Exercise the unbounded view, a tight local clamp, and a clamp crossing a tile boundary.
|
||||||
for (int32 ClampCase = 0; ClampCase < 3; ++ClampCase)
|
for (int32 ClampCase = 0; ClampCase < 3; ++ClampCase)
|
||||||
@@ -353,9 +296,9 @@ bool FVoxelForgeClipmapNoOverlapTest::RunTest(const FString& Parameters)
|
|||||||
if (FirstFailure.IsEmpty())
|
if (FirstFailure.IsEmpty())
|
||||||
{
|
{
|
||||||
FirstFailure = FString::Printf(
|
FirstFailure = FString::Printf(
|
||||||
TEXT("overlap center=(%d,%d,%d) radius=%d level=%d distance=%d sheet=%d clamp=%d: %s"),
|
TEXT("overlap center=(%d,%d,%d) radius=%d level=%d distance=%d clamp=%d: %s"),
|
||||||
X, Y, Z, Sweep.Radius, Sweep.MaxLevel, Sweep.RenderDistance,
|
X, Y, Z, Sweep.Radius, Sweep.MaxLevel, Sweep.RenderDistance,
|
||||||
Sweep.bSheet ? 1 : 0, ClampCase, *DescribeMeasure(CurrentOverlap));
|
ClampCase, *DescribeMeasure(CurrentOverlap));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!CoversEveryLegacyTile(Legacy, Current, MinZ, MaxZ))
|
if (!CoversEveryLegacyTile(Legacy, Current, MinZ, MaxZ))
|
||||||
@@ -364,9 +307,9 @@ bool FVoxelForgeClipmapNoOverlapTest::RunTest(const FString& Parameters)
|
|||||||
if (FirstFailure.IsEmpty())
|
if (FirstFailure.IsEmpty())
|
||||||
{
|
{
|
||||||
FirstFailure = FString::Printf(
|
FirstFailure = FString::Printf(
|
||||||
TEXT("coverage center=(%d,%d,%d) radius=%d level=%d distance=%d sheet=%d clamp=%d"),
|
TEXT("coverage center=(%d,%d,%d) radius=%d level=%d distance=%d clamp=%d"),
|
||||||
X, Y, Z, Sweep.Radius, Sweep.MaxLevel, Sweep.RenderDistance,
|
X, Y, Z, Sweep.Radius, Sweep.MaxLevel, Sweep.RenderDistance,
|
||||||
Sweep.bSheet ? 1 : 0, ClampCase);
|
ClampCase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -356,7 +356,7 @@ bool FVoxelForgeComposerCorpusFreeTest::RunTest(const FString& Parameters)
|
|||||||
Corpus.GetEntries().Num(), 12);
|
Corpus.GetEntries().Num(), 12);
|
||||||
|
|
||||||
FTestWorld World;
|
FTestWorld World;
|
||||||
World.Build(1337, 2, true);
|
World.Build(1337, 2);
|
||||||
if (!World.IsValid())
|
if (!World.IsValid())
|
||||||
{
|
{
|
||||||
AddError(World.WhyInvalid());
|
AddError(World.WhyInvalid());
|
||||||
|
|||||||
@@ -955,7 +955,7 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
|
|||||||
}
|
}
|
||||||
|
|
||||||
FTestWorld World;
|
FTestWorld World;
|
||||||
World.Build(1337, 2, true);
|
World.Build(1337, 2);
|
||||||
if (!World.IsValid())
|
if (!World.IsValid())
|
||||||
{
|
{
|
||||||
AddError(World.WhyInvalid());
|
AddError(World.WhyInvalid());
|
||||||
@@ -1117,7 +1117,6 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
|
|||||||
CandidateDefinition->SurfaceParams = Roll.ArchetypeParams.SurfaceParams;
|
CandidateDefinition->SurfaceParams = Roll.ArchetypeParams.SurfaceParams;
|
||||||
CandidateDefinition->VerticalShaftParams = Roll.ArchetypeParams.VerticalShaftParams;
|
CandidateDefinition->VerticalShaftParams = Roll.ArchetypeParams.VerticalShaftParams;
|
||||||
CandidateDefinition->FloatingIslandParams = Roll.ArchetypeParams.FloatingIslandParams;
|
CandidateDefinition->FloatingIslandParams = Roll.ArchetypeParams.FloatingIslandParams;
|
||||||
CandidateDefinition->bUseOperatorStack = true;
|
|
||||||
CandidateDefinition->TransitionType = EVoxelStrateTransition::Hard;
|
CandidateDefinition->TransitionType = EVoxelStrateTransition::Hard;
|
||||||
World.Reinitialize();
|
World.Reinitialize();
|
||||||
|
|
||||||
@@ -1342,123 +1341,109 @@ bool FVoxelForgeTerrainDetailLivenessTest::RunTest(const FString& Parameters)
|
|||||||
constexpr int32 ExpectedLatticeSamples = 16 * 16 * 16;
|
constexpr int32 ExpectedLatticeSamples = 16 * 16 * 16;
|
||||||
FString Table = TEXT(
|
FString Table = TEXT(
|
||||||
"TERRAIN DETAIL LIVENESS (same 4096-point lattice; only one group changed per row)\n"
|
"TERRAIN DETAIL LIVENESS (same 4096-point lattice; only one group changed per row)\n"
|
||||||
"path | group | samples | baseline digest | variant digest | baseline mean | variant mean | "
|
"group | samples | baseline digest | variant digest | baseline mean | variant mean | "
|
||||||
"changed | sign changes | sum abs delta | max abs delta | verdict\n");
|
"changed | sign changes | sum abs delta | max abs delta | verdict\n");
|
||||||
int32 TotalRows = 0;
|
int32 TotalRows = 0;
|
||||||
int32 FailedRows = 0;
|
int32 FailedRows = 0;
|
||||||
|
|
||||||
for (const bool bUseOperatorStack : { false, true })
|
FTestWorld World;
|
||||||
|
World.Build(1337, 2);
|
||||||
|
if (!World.IsValid())
|
||||||
{
|
{
|
||||||
FTestWorld World;
|
AddError(World.WhyInvalid());
|
||||||
World.Build(1337, 2, bUseOperatorStack);
|
return false;
|
||||||
if (!World.IsValid())
|
}
|
||||||
|
|
||||||
|
UVoxelStrateDefinition* Definition =
|
||||||
|
World.Definitions[FTestWorld::SlotTunnelNetwork].Get();
|
||||||
|
if (Definition == nullptr)
|
||||||
|
{
|
||||||
|
AddError(TEXT("The fixture has no TunnelNetwork definition for the liveness experiment."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Definition->TerrainOperations.Reset();
|
||||||
|
TestEqual(TEXT("fixture has no terrain-op asset pool"), Definition->TerrainOperations.Num(), 0);
|
||||||
|
|
||||||
|
TArray<FVector> Points;
|
||||||
|
VF_BuildTerrainDetailLattice(World, Points);
|
||||||
|
TestEqual(TEXT("experiment uses the fixed lattice"), Points.Num(), ExpectedLatticeSamples);
|
||||||
|
|
||||||
|
FStrateGenerationParams BaselineParams = Definition->GenerationParams;
|
||||||
|
VF_ClearTerrainDetailFields(BaselineParams);
|
||||||
|
TArray<float> FirstBaseline;
|
||||||
|
bool bHaveFirstBaseline = false;
|
||||||
|
|
||||||
|
for (const FTerrainDetailProbeSpec& Spec : GTerrainDetailProbeSpecs)
|
||||||
|
{
|
||||||
|
Definition->GenerationParams = BaselineParams;
|
||||||
|
World.Reinitialize();
|
||||||
|
|
||||||
|
TArray<float> BaselineSamples;
|
||||||
|
const FTerrainDetailDensitySummary BaselineSummary =
|
||||||
|
VF_SampleTerrainDetailLattice(World, Points, BaselineSamples);
|
||||||
|
if (!bHaveFirstBaseline)
|
||||||
{
|
{
|
||||||
AddError(World.WhyInvalid());
|
FirstBaseline = BaselineSamples;
|
||||||
return false;
|
bHaveFirstBaseline = true;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
UVoxelStrateDefinition* Definition =
|
|
||||||
World.Definitions[FTestWorld::SlotTunnelNetwork].Get();
|
|
||||||
if (Definition == nullptr)
|
|
||||||
{
|
{
|
||||||
AddError(TEXT("The fixture has no TunnelNetwork definition for the liveness experiment."));
|
bool bSameBaseline = FirstBaseline.Num() == BaselineSamples.Num();
|
||||||
return false;
|
for (int32 SampleIndex = 0; bSameBaseline && SampleIndex < FirstBaseline.Num(); ++SampleIndex)
|
||||||
}
|
|
||||||
Definition->TerrainOperations.Reset();
|
|
||||||
TestEqual(
|
|
||||||
FString::Printf(TEXT("%s fixture has no terrain-op asset pool"),
|
|
||||||
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy")),
|
|
||||||
Definition->TerrainOperations.Num(), 0);
|
|
||||||
|
|
||||||
TArray<FVector> Points;
|
|
||||||
VF_BuildTerrainDetailLattice(World, Points);
|
|
||||||
TestEqual(
|
|
||||||
FString::Printf(TEXT("%s experiment uses the fixed lattice"),
|
|
||||||
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy")),
|
|
||||||
Points.Num(), ExpectedLatticeSamples);
|
|
||||||
|
|
||||||
FStrateGenerationParams BaselineParams = Definition->GenerationParams;
|
|
||||||
VF_ClearTerrainDetailFields(BaselineParams);
|
|
||||||
TArray<float> FirstBaseline;
|
|
||||||
bool bHaveFirstBaseline = false;
|
|
||||||
|
|
||||||
for (const FTerrainDetailProbeSpec& Spec : GTerrainDetailProbeSpecs)
|
|
||||||
{
|
|
||||||
Definition->GenerationParams = BaselineParams;
|
|
||||||
World.Reinitialize();
|
|
||||||
|
|
||||||
TArray<float> BaselineSamples;
|
|
||||||
const FTerrainDetailDensitySummary BaselineSummary =
|
|
||||||
VF_SampleTerrainDetailLattice(World, Points, BaselineSamples);
|
|
||||||
if (!bHaveFirstBaseline)
|
|
||||||
{
|
{
|
||||||
FirstBaseline = BaselineSamples;
|
bSameBaseline = BitEqual(FirstBaseline[SampleIndex], BaselineSamples[SampleIndex]);
|
||||||
bHaveFirstBaseline = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
bool bSameBaseline = FirstBaseline.Num() == BaselineSamples.Num();
|
|
||||||
for (int32 SampleIndex = 0; bSameBaseline && SampleIndex < FirstBaseline.Num(); ++SampleIndex)
|
|
||||||
{
|
|
||||||
bSameBaseline = BitEqual(FirstBaseline[SampleIndex], BaselineSamples[SampleIndex]);
|
|
||||||
}
|
|
||||||
TestTrue(
|
|
||||||
FString::Printf(TEXT("%s baseline is stable before %s probe"),
|
|
||||||
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy"),
|
|
||||||
Spec.Name),
|
|
||||||
bSameBaseline);
|
|
||||||
}
|
|
||||||
|
|
||||||
FStrateGenerationParams VariantParams = BaselineParams;
|
|
||||||
VF_SetTerrainDetailProbe(VariantParams, Spec.Group);
|
|
||||||
Definition->GenerationParams = VariantParams;
|
|
||||||
World.Reinitialize();
|
|
||||||
|
|
||||||
TArray<float> VariantSamples;
|
|
||||||
const FTerrainDetailDensitySummary VariantSummary =
|
|
||||||
VF_SampleTerrainDetailLattice(World, Points, VariantSamples);
|
|
||||||
const FTerrainDetailDensityDelta Delta =
|
|
||||||
VF_CompareTerrainDetailSamples(BaselineSamples, VariantSamples);
|
|
||||||
|
|
||||||
const bool bObservedLive = Delta.Changed > 0;
|
|
||||||
const bool bVerdictMatches = bObservedLive == Spec.bExpectedLive;
|
|
||||||
++TotalRows;
|
|
||||||
if (!bVerdictMatches)
|
|
||||||
{
|
|
||||||
++FailedRows;
|
|
||||||
}
|
}
|
||||||
TestTrue(
|
TestTrue(
|
||||||
FString::Printf(TEXT("%s %s field group has the measured liveness verdict"),
|
FString::Printf(TEXT("baseline is stable before %s probe"), Spec.Name),
|
||||||
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy"),
|
bSameBaseline);
|
||||||
Spec.Name),
|
|
||||||
bVerdictMatches);
|
|
||||||
|
|
||||||
Table += FString::Printf(
|
|
||||||
TEXT("%s | %s | %d | 0x%08x | 0x%08x | %.9g | %.9g | %d | %d | %.9g | %.9g | %s\n"),
|
|
||||||
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy"),
|
|
||||||
Spec.Name,
|
|
||||||
Points.Num(),
|
|
||||||
BaselineSummary.Digest,
|
|
||||||
VariantSummary.Digest,
|
|
||||||
BaselineSummary.Mean,
|
|
||||||
VariantSummary.Mean,
|
|
||||||
Delta.Changed,
|
|
||||||
Delta.SignChanged,
|
|
||||||
Delta.SumAbs,
|
|
||||||
Delta.MaxAbs,
|
|
||||||
bObservedLive ? TEXT("LIVE") : TEXT("DEAD"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FStrateGenerationParams VariantParams = BaselineParams;
|
||||||
|
VF_SetTerrainDetailProbe(VariantParams, Spec.Group);
|
||||||
|
Definition->GenerationParams = VariantParams;
|
||||||
|
World.Reinitialize();
|
||||||
|
|
||||||
|
TArray<float> VariantSamples;
|
||||||
|
const FTerrainDetailDensitySummary VariantSummary =
|
||||||
|
VF_SampleTerrainDetailLattice(World, Points, VariantSamples);
|
||||||
|
const FTerrainDetailDensityDelta Delta =
|
||||||
|
VF_CompareTerrainDetailSamples(BaselineSamples, VariantSamples);
|
||||||
|
|
||||||
|
const bool bObservedLive = Delta.Changed > 0;
|
||||||
|
const bool bVerdictMatches = bObservedLive == Spec.bExpectedLive;
|
||||||
|
++TotalRows;
|
||||||
|
if (!bVerdictMatches)
|
||||||
|
{
|
||||||
|
++FailedRows;
|
||||||
|
}
|
||||||
|
TestTrue(
|
||||||
|
FString::Printf(TEXT("%s field group has the measured liveness verdict"), Spec.Name),
|
||||||
|
bVerdictMatches);
|
||||||
|
|
||||||
|
Table += FString::Printf(
|
||||||
|
TEXT("%s | %d | 0x%08x | 0x%08x | %.9g | %.9g | %d | %d | %.9g | %.9g | %s\n"),
|
||||||
|
Spec.Name,
|
||||||
|
Points.Num(),
|
||||||
|
BaselineSummary.Digest,
|
||||||
|
VariantSummary.Digest,
|
||||||
|
BaselineSummary.Mean,
|
||||||
|
VariantSummary.Mean,
|
||||||
|
Delta.Changed,
|
||||||
|
Delta.SignChanged,
|
||||||
|
Delta.SumAbs,
|
||||||
|
Delta.MaxAbs,
|
||||||
|
bObservedLive ? TEXT("LIVE") : TEXT("DEAD"));
|
||||||
}
|
}
|
||||||
|
|
||||||
AddInfo(Table);
|
AddInfo(Table);
|
||||||
AddInfo(FString::Printf(
|
AddInfo(FString::Printf(
|
||||||
TEXT("Terrain detail liveness: %d group/path rows measured, %d mismatches; LIVE groups "
|
TEXT("Terrain detail liveness: %d group rows measured, %d mismatches; LIVE groups "
|
||||||
"must have at least one bit-changed density and DEAD groups must have none. No "
|
"must have at least one bit-changed density and DEAD groups must have none. No "
|
||||||
"terrain-operation assets were present in the fixture pool."),
|
"terrain-operation assets were present in the fixture pool."),
|
||||||
TotalRows, FailedRows));
|
TotalRows, FailedRows));
|
||||||
TestEqual(TEXT("all terrain detail liveness rows matched their measured verdict"), FailedRows, 0);
|
TestEqual(TEXT("all terrain detail liveness rows matched their measured verdict"), FailedRows, 0);
|
||||||
TestEqual(TEXT("both density paths measured every terrain detail group"), TotalRows,
|
TestEqual(TEXT("every terrain detail group was measured"), TotalRows,
|
||||||
static_cast<int32>(2 * UE_ARRAY_COUNT(GTerrainDetailProbeSpecs)));
|
static_cast<int32>(UE_ARRAY_COUNT(GTerrainDetailProbeSpecs)));
|
||||||
return FailedRows == 0;
|
return FailedRows == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -394,7 +394,7 @@ bool FVoxelForgeComposerPromotionTest::RunTest(const FString& Parameters)
|
|||||||
}
|
}
|
||||||
|
|
||||||
FTestWorld World;
|
FTestWorld World;
|
||||||
World.Build(1337, 2, true);
|
World.Build(1337, 2);
|
||||||
if (!World.IsValid())
|
if (!World.IsValid())
|
||||||
{
|
{
|
||||||
AddError(World.WhyInvalid());
|
AddError(World.WhyInvalid());
|
||||||
|
|||||||
@@ -1590,7 +1590,7 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
|
|||||||
FTestWorld World;
|
FTestWorld World;
|
||||||
// The shared fixture defaults to a bounded 4-chunk volume for fast density tests. The
|
// The shared fixture defaults to a bounded 4-chunk volume for fast density tests. The
|
||||||
// owner-facing scale audit must exercise the production/default 8-chunk envelope.
|
// owner-facing scale audit must exercise the production/default 8-chunk envelope.
|
||||||
World.Build(BaseComposerSeed, 2, true, 8);
|
World.Build(BaseComposerSeed, 2, 8);
|
||||||
if (!World.IsValid())
|
if (!World.IsValid())
|
||||||
{
|
{
|
||||||
AddError(World.WhyInvalid());
|
AddError(World.WhyInvalid());
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
|
|||||||
}
|
}
|
||||||
|
|
||||||
FTestWorld World;
|
FTestWorld World;
|
||||||
World.Build(1337, 2, true);
|
World.Build(1337, 2);
|
||||||
if (!World.IsValid())
|
if (!World.IsValid())
|
||||||
{
|
{
|
||||||
AddError(World.WhyInvalid());
|
AddError(World.WhyInvalid());
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
|||||||
bool FVoxelForgeTileCacheContextDeterminismTest::RunTest(const FString& Parameters)
|
bool FVoxelForgeTileCacheContextDeterminismTest::RunTest(const FString& Parameters)
|
||||||
{
|
{
|
||||||
FTestWorld World;
|
FTestWorld World;
|
||||||
World.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/true);
|
World.Build(/*Seed*/1337, /*GapChunks*/2);
|
||||||
if (!World.IsValid())
|
if (!World.IsValid())
|
||||||
{
|
{
|
||||||
AddError(World.WhyInvalid());
|
AddError(World.WhyInvalid());
|
||||||
@@ -208,8 +208,8 @@ bool FVoxelForgeTileCacheContextDeterminismTest::RunTest(const FString& Paramete
|
|||||||
// level-0 tile at the same origin is compared with a fresh world's level-0 output.
|
// level-0 tile at the same origin is compared with a fresh world's level-0 output.
|
||||||
FTestWorld WarmWorld;
|
FTestWorld WarmWorld;
|
||||||
FTestWorld FreshWorld;
|
FTestWorld FreshWorld;
|
||||||
WarmWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/true);
|
WarmWorld.Build(/*Seed*/1337, /*GapChunks*/2);
|
||||||
FreshWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/true);
|
FreshWorld.Build(/*Seed*/1337, /*GapChunks*/2);
|
||||||
if (!WarmWorld.IsValid() || !FreshWorld.IsValid())
|
if (!WarmWorld.IsValid() || !FreshWorld.IsValid())
|
||||||
{
|
{
|
||||||
AddError(WarmWorld.IsValid() && FreshWorld.IsValid()
|
AddError(WarmWorld.IsValid() && FreshWorld.IsValid()
|
||||||
|
|||||||
@@ -348,7 +348,7 @@ bool FVoxelForgeDensityBlockEquivalenceTest::RunTest(const FString& Parameters)
|
|||||||
for (int32 SeedIndex = 0; SeedIndex < UE_ARRAY_COUNT(Seeds); ++SeedIndex)
|
for (int32 SeedIndex = 0; SeedIndex < UE_ARRAY_COUNT(Seeds); ++SeedIndex)
|
||||||
{
|
{
|
||||||
VoxelForgeTest::FTestWorld World;
|
VoxelForgeTest::FTestWorld World;
|
||||||
World.Build(Seeds[SeedIndex], /*gap chunks=*/2, /*operator stack=*/true);
|
World.Build(Seeds[SeedIndex], /*gap chunks=*/2);
|
||||||
if (!World.IsValid())
|
if (!World.IsValid())
|
||||||
{
|
{
|
||||||
AddError(World.WhyInvalid());
|
AddError(World.WhyInvalid());
|
||||||
@@ -470,8 +470,8 @@ bool FVoxelForgeDensityBlockEquivalenceTest::RunTest(const FString& Parameters)
|
|||||||
|
|
||||||
// No-edit grids. In the two canonical network archetypes this enters the actual worker
|
// No-edit grids. In the two canonical network archetypes this enters the actual worker
|
||||||
// block session, which partitions each mesher grid by resolved chunk before running the
|
// block session, which partitions each mesher grid by resolved chunk before running the
|
||||||
// active operator list. The six legacy density routes still run their production scalar
|
// active operator list. The six other archetypes run the scalar stack evaluator, as they
|
||||||
// evaluator, as they do outside the block-safe network branch.
|
// do in production outside the block-safe network branch.
|
||||||
for (int32 GridIndex = 0; GridIndex < GridSetCount; ++GridIndex)
|
for (int32 GridIndex = 0; GridIndex < GridSetCount; ++GridIndex)
|
||||||
{
|
{
|
||||||
BaseResults[GridIndex] = CompareGeneratorBlockWithScalar(
|
BaseResults[GridIndex] = CompareGeneratorBlockWithScalar(
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
// seam, and in multiplayer a world divergence.
|
// seam, and in multiplayer a world divergence.
|
||||||
//
|
//
|
||||||
// POURQUOI CE TEST EXISTE / WHY THIS TEST EXISTS:
|
// POURQUOI CE TEST EXISTE / WHY THIS TEST EXISTS:
|
||||||
// ~30 caches thread_local à clé manuelle vivent sous GetDensityAt (CP_*, GSurfColCache, les
|
// ~30 caches thread_local à clé manuelle vivent sous GetDensityAt (CP_*, les mémos par cellule
|
||||||
// slots de diff, le cache SDF). Chacun est correct exactement tant que sa CLÉ contient toutes
|
// et par colonne des piles, les slots de diff, le cache SDF). Chacun est correct exactement tant que sa CLÉ contient toutes
|
||||||
// les entrées dont dépend la valeur cachée. Une entrée oubliée ne casse rien tout de suite :
|
// les entrées dont dépend la valeur cachée. Une entrée oubliée ne casse rien tout de suite :
|
||||||
// elle produit une mauvaise valeur seulement quand le cache est chaud pour une AUTRE entrée —
|
// elle produit une mauvaise valeur seulement quand le cache est chaud pour une AUTRE entrée —
|
||||||
// c'est-à-dire de façon intermittente, dépendante de l'ordre, et invisible en jeu jusqu'à ce
|
// c'est-à-dire de façon intermittente, dépendante de l'ordre, et invisible en jeu jusqu'à ce
|
||||||
@@ -351,7 +351,7 @@ bool FVoxelForgeLiveEditInvalidationTest::RunTest(const FString& Parameters)
|
|||||||
TEXT("STALE PARAMS (AUDIT C2): the SurfaceWorld heightfield params were tripled and the ")
|
TEXT("STALE PARAMS (AUDIT C2): the SurfaceWorld heightfield params were tripled and the ")
|
||||||
TEXT("layout rebuilt, yet all %d probe densities are bit-identical. A per-chunk cache is ")
|
TEXT("layout rebuilt, yet all %d probe densities are bit-identical. A per-chunk cache is ")
|
||||||
TEXT("still keyed on ChunkCoord alone and skipped its refetch. Suspects, in order: ")
|
TEXT("still keyed on ChunkCoord alone and skipped its refetch. Suspects, in order: ")
|
||||||
TEXT("CP_Chunk/CP_Version in GetDensityAt, the GSurfColCache box key, and the ")
|
TEXT("CP_Chunk/CP_Version in GetDensityAt, the FSurfaceColumnSource column memo key, and the ")
|
||||||
TEXT("FChunkBiomeCache validity box (which says nothing about the FBiomeContext its ")
|
TEXT("FChunkBiomeCache validity box (which says nothing about the FBiomeContext its ")
|
||||||
TEXT("cells were classified against)."), Probes.Num()));
|
TEXT("cells were classified against)."), Probes.Num()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -761,14 +761,13 @@ namespace
|
|||||||
const float SimulatedCarvedFinalDensity = -SimulatedCarvedInternalDensity;
|
const float SimulatedCarvedFinalDensity = -SimulatedCarvedInternalDensity;
|
||||||
UE_LOG(LogTemp, Display,
|
UE_LOG(LogTemp, Display,
|
||||||
TEXT("[VoxelForgeDescentWriterContext] seed=0 sample=(%.3f,%.3f,%.3f) "
|
TEXT("[VoxelForgeDescentWriterContext] seed=0 sample=(%.3f,%.3f,%.3f) "
|
||||||
"chunk=(%d,%d,%d) archetype=%s asset_stack_flag=%d effective_op_stack=%d "
|
"chunk=(%d,%d,%d) archetype=%s effective_op_stack=%d "
|
||||||
"passage_sdf=%.6f simulated_passage_carve_density=%.6f "
|
"passage_sdf=%.6f simulated_passage_carve_density=%.6f "
|
||||||
"terrain_ops='%s' terrain_op_paths='%s' "
|
"terrain_ops='%s' terrain_op_paths='%s' "
|
||||||
"base=%.3f tunnel_columns=(%.3f,%.3f,%.3f) slab_columns=(%.3f,%.3f,%.3f)"),
|
"base=%.3f tunnel_columns=(%.3f,%.3f,%.3f) slab_columns=(%.3f,%.3f,%.3f)"),
|
||||||
OutSamples.FirstSolidPosition.X, OutSamples.FirstSolidPosition.Y,
|
OutSamples.FirstSolidPosition.X, OutSamples.FirstSolidPosition.Y,
|
||||||
OutSamples.FirstSolidPosition.Z, ChunkCoord.X, ChunkCoord.Y, ChunkCoord.Z,
|
OutSamples.FirstSolidPosition.Z, ChunkCoord.X, ChunkCoord.Y, ChunkCoord.Z,
|
||||||
*GeneratorName, Definition != nullptr && Definition->bUseOperatorStack ? 1 : 0,
|
*GeneratorName, bUsesOperatorStack ? 1 : 0, PassageSDF, SimulatedCarvedFinalDensity,
|
||||||
bUsesOperatorStack ? 1 : 0, PassageSDF, SimulatedCarvedFinalDensity,
|
|
||||||
*TerrainOps, *TerrainOpPaths, Params.BaseDensity, Params.ColumnDensity,
|
*TerrainOps, *TerrainOpPaths, Params.BaseDensity, Params.ColumnDensity,
|
||||||
Params.ColumnMinRadius, Params.ColumnMaxRadius,
|
Params.ColumnMinRadius, Params.ColumnMaxRadius,
|
||||||
Definition != nullptr ? Definition->SlabParams.ColumnDensity : -1.0f,
|
Definition != nullptr ? Definition->SlabParams.ColumnDensity : -1.0f,
|
||||||
|
|||||||
@@ -8,23 +8,15 @@
|
|||||||
// voudrait dire soit un canal par-voxel pour une propriété de COLONNE, soit un seul opérateur
|
// voudrait dire soit un canal par-voxel pour une propriété de COLONNE, soit un seul opérateur
|
||||||
// opaque — ce que `OPSTACK-PLAN §2.5` appelle exactement l'échec du refactor.
|
// opaque — ce que `OPSTACK-PLAN §2.5` appelle exactement l'échec du refactor.
|
||||||
//
|
//
|
||||||
// D'où une seconde famille, `VoxelHeightOp.h`. **Ce test est ce qui dit si elle était une bonne
|
// D'où une seconde famille, `VoxelHeightOp.h`, mesurée ici contre `ComputeSurfaceTerrainZ`.
|
||||||
// idée** — la même méthode que la Phase 1 a appliquée à la densité : décomposer, puis MESURER
|
|
||||||
// contre l'original, avant de construire par-dessus.
|
|
||||||
//
|
//
|
||||||
// ⚠️ CE QUE CE TEST COUVRE
|
// ⚠️ CE QUE CE TEST COUVRE
|
||||||
// ✅ la pile de HAUTEUR du sol, contre `ComputeSurfaceTerrainZ` (défauts puis tous les ops F20) ;
|
// ✅ la pile de HAUTEUR du sol, contre `ComputeSurfaceTerrainZ` (défauts puis tous les ops F20) ;
|
||||||
// ✅ les canaux de densité courants : ordre, fenêtre et overhang ;
|
// ✅ la pile de densité SurfaceWorld : décomposition, ordre des canaux, densité finie, overhang ;
|
||||||
// ✅ le mélange synthétique à poids zéro.
|
// ✅ le mélange synthétique à poids zéro.
|
||||||
//
|
//
|
||||||
// The old GetSurfaceDensity helper remains a differential diagnostic only: it does not carry the
|
// LA BARRE : **bit à bit** pour les opérations en espace hauteur.
|
||||||
// current common structural post. The assertions target the height stack and the density stack
|
// The bar is bit-identity for height-space operations.
|
||||||
// used by the owner path, so room ownership and the authored-floor removal are not mistaken for
|
|
||||||
// a failed height port.
|
|
||||||
//
|
|
||||||
// LA BARRE : **bit à bit** remains the rule for height-space operations. Density comparisons to
|
|
||||||
// the retired direct helper are reported, not asserted, because room ownership and the common
|
|
||||||
// MC post intentionally make those paths different.
|
|
||||||
|
|
||||||
#if WITH_DEV_AUTOMATION_TESTS
|
#if WITH_DEV_AUTOMATION_TESTS
|
||||||
|
|
||||||
@@ -285,18 +277,9 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters)
|
|||||||
// ÉTAPE 2a — LE PONT VERS L'ESPACE DENSITÉ
|
// ÉTAPE 2a — LE PONT VERS L'ESPACE DENSITÉ
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// `FSurfaceColumnSource` consomme les DEUX piles de hauteur (sol + voûte) et rend une densité.
|
// `FSurfaceColumnSource` consomme les DEUX piles de hauteur (sol + voûte) et rend une densité.
|
||||||
// La référence est `GetSurfaceDensity`, qui est exactement la variante **sans overhang**
|
// Cette passe est la variante **sans overhang** et **sans biomes** ; l'overhang a sa propre
|
||||||
// (il passe `OverhangAmp = 0`) et **sans biomes** (ParamsD == ParamsN, poids 0) — donc la
|
// passe juste en dessous. / No-overhang, no-biome variant; the overhang has its own pass below.
|
||||||
// comparaison est nette plutôt qu'approximative.
|
|
||||||
//
|
|
||||||
// ⚠️ Ce que ce bloc NE teste PAS, et qu'il ne faut pas croire testé : l'overhang et le mélange
|
|
||||||
// de biomes. Tous deux arrivent à l'étape 2b, avec le chemin CACHÉ pour référence — c'est le
|
|
||||||
// seul qui les calcule.
|
|
||||||
{
|
{
|
||||||
// ⚠️ `OverhangStrength = 0` EXPLICITEMENT : `GetSurfaceDensity` passe `OverhangAmp = 0`,
|
|
||||||
// donc il n'en calcule aucun. Comparer une pile qui en produit à une référence qui n'en
|
|
||||||
// produit pas ferait échouer le test pour la seule raison que la référence est incomplète.
|
|
||||||
// L'overhang a sa propre passe juste en dessous, avec le bon oracle.
|
|
||||||
FSurfaceGenerationParams P = AllOps;
|
FSurfaceGenerationParams P = AllOps;
|
||||||
P.OverhangStrength = 0.0f;
|
P.OverhangStrength = 0.0f;
|
||||||
|
|
||||||
@@ -318,8 +301,7 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters)
|
|||||||
Ctx.StrateBottomWorldZ = P.StrateBottomWorldZ;
|
Ctx.StrateBottomWorldZ = P.StrateBottomWorldZ;
|
||||||
Stack.PrepareChunk(Ctx);
|
Stack.PrepareChunk(Ctx);
|
||||||
|
|
||||||
int32 NumDiff = 0, NumSideDisagree = 0, WorstIdx = -1, NumNonFinite = 0;
|
int32 NumNonFinite = 0;
|
||||||
float WorstDelta = 0.0f;
|
|
||||||
|
|
||||||
FRandomStream Rng(5150);
|
FRandomStream Rng(5150);
|
||||||
for (int32 i = 0; i < NumHeightSamples; ++i)
|
for (int32 i = 0; i < NumHeightSamples; ++i)
|
||||||
@@ -327,44 +309,23 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters)
|
|||||||
const float X = (float)Rng.RandRange(-4 * CHUNK_SIZE, 4 * CHUNK_SIZE);
|
const float X = (float)Rng.RandRange(-4 * CHUNK_SIZE, 4 * CHUNK_SIZE);
|
||||||
const float Y = (float)Rng.RandRange(-4 * CHUNK_SIZE, 4 * CHUNK_SIZE);
|
const float Y = (float)Rng.RandRange(-4 * CHUNK_SIZE, 4 * CHUNK_SIZE);
|
||||||
const float Z = (float)Rng.RandRange(BottomVoxelZ, TopVoxelZ);
|
const float Z = (float)Rng.RandRange(BottomVoxelZ, TopVoxelZ);
|
||||||
|
if (!FMath::IsFinite(Stack.EvalMC(X, Y, Z)))
|
||||||
// ParamsD == ParamsN, poids 0 ⇒ une seule évaluation, pas de biomes.
|
|
||||||
const float Old = Gen->GetSurfaceDensity(X, Y, Z, P, P, 0.0f);
|
|
||||||
const float New = Stack.EvalMC(X, Y, Z);
|
|
||||||
if (!FMath::IsFinite(New))
|
|
||||||
{
|
{
|
||||||
++NumNonFinite;
|
++NumNonFinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!BitEqual(Old, New))
|
|
||||||
{
|
|
||||||
++NumDiff;
|
|
||||||
const float Delta = FMath::Abs(Old - New);
|
|
||||||
if (Delta > WorstDelta) { WorstDelta = Delta; WorstIdx = i; }
|
|
||||||
}
|
|
||||||
if ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSideDisagree; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AddInfo(FString::Printf(
|
|
||||||
TEXT("SurfaceWorld density legacy diagnostic: %d of %d samples differ from "
|
|
||||||
"GetSurfaceDensity (largest |delta| %.9g), %d side disagreements; "
|
|
||||||
"current stack non-finite samples=%d."),
|
|
||||||
NumDiff, NumHeightSamples, WorstDelta, NumSideDisagree, NumNonFinite));
|
|
||||||
TestEqual(TEXT("surface: current density stack has no non-finite samples"),
|
TestEqual(TEXT("surface: current density stack has no non-finite samples"),
|
||||||
NumNonFinite, 0);
|
NumNonFinite, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// ÉTAPE 2b — L'OVERHANG, contre le SEUL oracle qui le calcule
|
// ÉTAPE 2b — L'OVERHANG
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// `GetSurfaceDensity` passe `OverhangAmp = 0`. La seule référence est donc le chemin caché :
|
// `ComputeSurfaceColumn` résout le gate et la direction amont par colonne ; la moitié des
|
||||||
// `ComputeSurfaceColumn` (qui résout le gate et la direction amont par colonne) suivi de
|
// échantillons est placée DANS la fenêtre d'overhang qu'il rapporte, pour que l'op overhang de
|
||||||
// `SurfaceDensityFromColumn` (qui applique l'union par voxel). Les deux viennent d'être
|
// la pile soit réellement exercé. / Half the samples are placed inside the overhang window that
|
||||||
// exposées pour ça.
|
// ComputeSurfaceColumn reports, so the stack's overhang op is actually exercised.
|
||||||
//
|
|
||||||
// C'est aussi la passe qui vérifie le MÉMO DE COLONNE de `FSurfaceColumnSource` : l'op overhang
|
|
||||||
// lit la colonne produite par la source, et s'ils divergeaient d'un XY, l'union se ferait au
|
|
||||||
// mauvais endroit. Un mémo mal clé se verrait ici.
|
|
||||||
{
|
{
|
||||||
FSurfaceGenerationParams P = AllOps;
|
FSurfaceGenerationParams P = AllOps;
|
||||||
P.OverhangStrength = 0.8f;
|
P.OverhangStrength = 0.8f;
|
||||||
@@ -391,8 +352,7 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters)
|
|||||||
TArray<FSurfaceGenerationParams> NoBiomeParams;
|
TArray<FSurfaceGenerationParams> NoBiomeParams;
|
||||||
FChunkBiomeCache BiomeCache;
|
FChunkBiomeCache BiomeCache;
|
||||||
|
|
||||||
int32 NumDiff = 0, NumSideDisagree = 0, NumInWindow = 0, NumNonFinite = 0;
|
int32 NumInWindow = 0, NumNonFinite = 0;
|
||||||
float WorstDelta = 0.0f;
|
|
||||||
|
|
||||||
FRandomStream Rng(1337);
|
FRandomStream Rng(1337);
|
||||||
for (int32 i = 0; i < NumHeightSamples; ++i)
|
for (int32 i = 0; i < NumHeightSamples; ++i)
|
||||||
@@ -418,27 +378,16 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters)
|
|||||||
Z = (float)Rng.RandRange(BottomVoxelZ, TopVoxelZ);
|
Z = (float)Rng.RandRange(BottomVoxelZ, TopVoxelZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
const float Old = Gen->SurfaceDensityFromColumn(X, Y, Z, TerrainZ, CeilSurf,
|
if (!FMath::IsFinite(Stack.EvalMC(X, Y, Z)))
|
||||||
Amp, DirX, DirY, P);
|
|
||||||
const float New = Stack.EvalMC(X, Y, Z);
|
|
||||||
if (!FMath::IsFinite(New))
|
|
||||||
{
|
{
|
||||||
++NumNonFinite;
|
++NumNonFinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!BitEqual(Old, New))
|
|
||||||
{
|
|
||||||
++NumDiff;
|
|
||||||
WorstDelta = FMath::Max(WorstDelta, FMath::Abs(Old - New));
|
|
||||||
}
|
|
||||||
if ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSideDisagree; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AddInfo(FString::Printf(
|
AddInfo(FString::Printf(
|
||||||
TEXT("Overhang legacy diagnostic: %d of %d samples differ from "
|
TEXT("Overhang: %d of %d samples deliberately inside the overhang window; current "
|
||||||
"SurfaceDensityFromColumn (largest |delta| %.9g), %d side disagreements, "
|
"stack non-finite samples=%d."),
|
||||||
"%d deliberately inside the overhang window; current stack non-finite samples=%d."),
|
NumInWindow, NumHeightSamples, NumNonFinite));
|
||||||
NumDiff, NumHeightSamples, WorstDelta, NumSideDisagree, NumInWindow, NumNonFinite));
|
|
||||||
TestEqual(TEXT("overhang: current density stack has no non-finite samples"),
|
TestEqual(TEXT("overhang: current density stack has no non-finite samples"),
|
||||||
NumNonFinite, 0);
|
NumNonFinite, 0);
|
||||||
|
|
||||||
|
|||||||
@@ -549,7 +549,7 @@ bool FVoxelForgeLateralRegionsTest::RunTest(const FString& Parameters)
|
|||||||
}
|
}
|
||||||
|
|
||||||
FTestWorld World;
|
FTestWorld World;
|
||||||
World.Build(Seed, 2, false);
|
World.Build(Seed, 2);
|
||||||
int32 TopWorldZ = 0;
|
int32 TopWorldZ = 0;
|
||||||
int32 BottomWorldZ = 0;
|
int32 BottomWorldZ = 0;
|
||||||
FVoxelStrateRegionManifest Manifest;
|
FVoxelStrateRegionManifest Manifest;
|
||||||
@@ -612,7 +612,7 @@ bool FVoxelForgeLateralRegionsTest::RunTest(const FString& Parameters)
|
|||||||
}
|
}
|
||||||
|
|
||||||
FTestWorld World;
|
FTestWorld World;
|
||||||
World.Build(Seed, 2, false);
|
World.Build(Seed, 2);
|
||||||
int32 TopWorldZ = 0;
|
int32 TopWorldZ = 0;
|
||||||
int32 BottomWorldZ = 0;
|
int32 BottomWorldZ = 0;
|
||||||
FVoxelStrateRegionManifest Manifest;
|
FVoxelStrateRegionManifest Manifest;
|
||||||
@@ -661,7 +661,7 @@ bool FVoxelForgeLateralRegionsTest::RunTest(const FString& Parameters)
|
|||||||
TestTrue(TEXT("found a single-region recipe identity case"), bRecipeIdentityChecked);
|
TestTrue(TEXT("found a single-region recipe identity case"), bRecipeIdentityChecked);
|
||||||
|
|
||||||
FTestWorld World;
|
FTestWorld World;
|
||||||
World.Build(MultiSeed, 2, false);
|
World.Build(MultiSeed, 2);
|
||||||
if (!World.IsValid())
|
if (!World.IsValid())
|
||||||
{
|
{
|
||||||
AddError(World.WhyInvalid());
|
AddError(World.WhyInvalid());
|
||||||
@@ -955,7 +955,7 @@ bool FVoxelForgeLateralRegionsTest::RunTest(const FString& Parameters)
|
|||||||
}
|
}
|
||||||
|
|
||||||
FTestWorld LawWorld;
|
FTestWorld LawWorld;
|
||||||
LawWorld.Build(Seed, 2, false);
|
LawWorld.Build(Seed, 2);
|
||||||
FVoxelStrateRegionManifest LawManifest;
|
FVoxelStrateRegionManifest LawManifest;
|
||||||
FVoxelOpStack LawStack;
|
FVoxelOpStack LawStack;
|
||||||
FVoxelOpContext LawContext;
|
FVoxelOpContext LawContext;
|
||||||
|
|||||||
@@ -100,65 +100,45 @@ bool FVoxelForgeMazeSeamTest::RunTest(const FString& Parameters)
|
|||||||
{
|
{
|
||||||
using namespace VoxelForgeTest;
|
using namespace VoxelForgeTest;
|
||||||
|
|
||||||
FTestWorld LegacyWorld;
|
FTestWorld World;
|
||||||
LegacyWorld.Build(7331, 2, false, 4);
|
World.Build(7331, 2, 4);
|
||||||
FTestWorld StackWorld;
|
if (!World.IsValid())
|
||||||
StackWorld.Build(7331, 2, true, 4);
|
|
||||||
if (!LegacyWorld.IsValid() || !StackWorld.IsValid())
|
|
||||||
{
|
{
|
||||||
AddError(LegacyWorld.IsValid() ? StackWorld.WhyInvalid() : LegacyWorld.WhyInvalid());
|
AddError(World.WhyInvalid());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int32 LegacyTop = 0, LegacyBottom = 0;
|
int32 MazeTopZ = 0, MazeBottomZ = 0;
|
||||||
int32 StackTop = 0, StackBottom = 0;
|
if (!World.GetSlotVoxelZRange(FTestWorld::SlotMaze, MazeTopZ, MazeBottomZ))
|
||||||
if (!LegacyWorld.GetSlotVoxelZRange(FTestWorld::SlotMaze, LegacyTop, LegacyBottom)
|
|
||||||
|| !StackWorld.GetSlotVoxelZRange(FTestWorld::SlotMaze, StackTop, StackBottom))
|
|
||||||
{
|
{
|
||||||
AddError(TEXT("The seam fixture has no Maze slot."));
|
AddError(TEXT("The seam fixture has no Maze slot."));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const int32 LegacyMidChunkZ = ((LegacyTop + LegacyBottom) / 2) / CHUNK_SIZE;
|
const int32 MazeMidChunkZ = ((MazeTopZ + MazeBottomZ) / 2) / CHUNK_SIZE;
|
||||||
const int32 StackMidChunkZ = ((StackTop + StackBottom) / 2) / CHUNK_SIZE;
|
const FMazeGenerationParams MazeParams = World.StrateManager->GetMazeParamsForChunk(
|
||||||
const FMazeGenerationParams LegacyParams = LegacyWorld.StrateManager->GetMazeParamsForChunk(
|
FIntVector(0, 0, MazeMidChunkZ));
|
||||||
FIntVector(0, 0, LegacyMidChunkZ));
|
|
||||||
const FMazeGenerationParams StackParams = StackWorld.StrateManager->GetMazeParamsForChunk(
|
|
||||||
FIntVector(0, 0, StackMidChunkZ));
|
|
||||||
|
|
||||||
TArray<FVector> LegacyProbes;
|
TArray<FVector> SeamProbes;
|
||||||
TArray<FVector> StackProbes;
|
AddMazeBoundaryProbes(MazeParams, MazeBottomZ, MazeTopZ, SeamProbes);
|
||||||
AddMazeBoundaryProbes(LegacyParams, LegacyBottom, LegacyTop, LegacyProbes);
|
|
||||||
AddMazeBoundaryProbes(StackParams, StackBottom, StackTop, StackProbes);
|
|
||||||
TestEqual(TEXT("legacy and operator seam tests have the same probe count"),
|
|
||||||
LegacyProbes.Num(), StackProbes.Num());
|
|
||||||
|
|
||||||
int32 LegacyFirst = INDEX_NONE, StackFirst = INDEX_NONE;
|
int32 FirstProbe = INDEX_NONE;
|
||||||
float LegacyBefore = 0.0f, LegacyAfter = 0.0f;
|
float FirstBefore = 0.0f, FirstAfter = 0.0f;
|
||||||
float StackBefore = 0.0f, StackAfter = 0.0f;
|
const int32 NumMismatches = CountContextMismatches(
|
||||||
const int32 LegacyMismatches = CountContextMismatches(
|
World, SeamProbes, FirstProbe, FirstBefore, FirstAfter);
|
||||||
LegacyWorld, LegacyProbes, LegacyFirst, LegacyBefore, LegacyAfter);
|
|
||||||
const int32 StackMismatches = CountContextMismatches(
|
|
||||||
StackWorld, StackProbes, StackFirst, StackBefore, StackAfter);
|
|
||||||
|
|
||||||
TestEqual(TEXT("legacy Maze has no chunk-context seam mismatches"), LegacyMismatches, 0);
|
TestEqual(TEXT("Maze has no chunk-context seam mismatches"), NumMismatches, 0);
|
||||||
TestEqual(TEXT("operator-stack Maze has no chunk-context seam mismatches"), StackMismatches, 0);
|
TestTrue(TEXT("Maze seam test sampled cell-boundary probes"), SeamProbes.Num() > 0);
|
||||||
TestTrue(TEXT("Maze seam test sampled cell-boundary probes"), LegacyProbes.Num() > 0);
|
|
||||||
|
|
||||||
if (LegacyFirst != INDEX_NONE)
|
if (FirstProbe != INDEX_NONE)
|
||||||
{
|
{
|
||||||
AddError(FString::Printf(TEXT("Legacy Maze seam mismatch at probe %d: %.9g -> %.9g"),
|
AddError(FString::Printf(TEXT("Maze seam mismatch at probe %d: %.9g -> %.9g"),
|
||||||
LegacyFirst, LegacyBefore, LegacyAfter));
|
FirstProbe, FirstBefore, FirstAfter));
|
||||||
}
|
|
||||||
if (StackFirst != INDEX_NONE)
|
|
||||||
{
|
|
||||||
AddError(FString::Printf(TEXT("Operator Maze seam mismatch at probe %d: %.9g -> %.9g"),
|
|
||||||
StackFirst, StackBefore, StackAfter));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AddInfo(FString::Printf(
|
AddInfo(FString::Printf(
|
||||||
TEXT("Maze seam freedom: %d boundary probes, legacy mismatches=%d, operator mismatches=%d; "
|
TEXT("Maze seam freedom: %d boundary probes, mismatches=%d; "
|
||||||
"local window=2x2x2 child nodes ({-1,0}^3), no wide collect."),
|
"local window=2x2x2 child nodes ({-1,0}^3), no wide collect."),
|
||||||
LegacyProbes.Num(), LegacyMismatches, StackMismatches));
|
SeamProbes.Num(), NumMismatches));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +155,7 @@ bool FVoxelForgeCaveWarpTileLodSeamTest::RunTest(const FString& Parameters)
|
|||||||
using namespace VoxelForgeTest;
|
using namespace VoxelForgeTest;
|
||||||
|
|
||||||
FTestWorld World;
|
FTestWorld World;
|
||||||
World.Build(1337, 2, true, 4);
|
World.Build(1337, 2, 4);
|
||||||
if (!World.IsValid())
|
if (!World.IsValid())
|
||||||
{
|
{
|
||||||
AddError(World.WhyInvalid());
|
AddError(World.WhyInvalid());
|
||||||
|
|||||||
@@ -90,9 +90,8 @@ bool FVoxelForgeOpStackChannelTest::RunTest(const FString& Parameters)
|
|||||||
TestTrue(*FString::Printf(TEXT("%s shipping stack has a legal channel DAG"), Name), bValid);
|
TestTrue(*FString::Printf(TEXT("%s shipping stack has a legal channel DAG"), Name), bValid);
|
||||||
};
|
};
|
||||||
|
|
||||||
// These are the eight builders currently shipped behind bUseOperatorStack. Keep the
|
// These are the eight shipped archetype builders. Keep the archetype list explicit so adding
|
||||||
// archetype list explicit so adding a builder without adding its validator assertion is
|
// a builder without adding its validator assertion is visible in this test.
|
||||||
// visible in this test.
|
|
||||||
int32 MidChunkZ = 0;
|
int32 MidChunkZ = 0;
|
||||||
|
|
||||||
if (MidChunkForSlot(FTestWorld::SlotTunnelNetwork, TEXT("TunnelNetwork"), MidChunkZ))
|
if (MidChunkForSlot(FTestWorld::SlotTunnelNetwork, TEXT("TunnelNetwork"), MidChunkZ))
|
||||||
|
|||||||
@@ -1,30 +1,18 @@
|
|||||||
// VoxelForgeOpStackIslandTest.cpp
|
// VoxelForgeOpStackIslandTest.cpp
|
||||||
// FloatingIslands — le portage qui fait tourner la pile À L'ENVERS.
|
// FloatingIslands — la pile qui tourne À L'ENVERS / the stack that runs BACKWARDS.
|
||||||
// FloatingIslands — the port that runs the stack BACKWARDS.
|
|
||||||
//
|
//
|
||||||
// CE QUE CELUI-CI PROUVE EN PLUS DES AUTRES
|
// Les autres archétypes partent de ROC et CREUSENT ; FloatingIslands part du VIDE et REMPLIT, avec
|
||||||
// `VerticalShaftEquivalence` a mesuré la réutilisation À L'IDENTIQUE : trois opérateurs de Maze
|
// les MÊMES opérateurs au signe près (convention interne positif = solide) :
|
||||||
// repris sans une ligne de changement. Celui-ci mesure quelque chose de plus fort, et de plus
|
|
||||||
// risqué pour l'abstraction : **la réutilisation PAR INVERSION**.
|
|
||||||
//
|
|
||||||
// Les quatre archétypes déjà portés partent tous de ROC et CREUSENT. FloatingIslands part du VIDE
|
|
||||||
// et REMPLIT. Si l'axe abstrait choisi (le SIGNE de la densité, convention interne positif = solide)
|
|
||||||
// est le bon, alors les deux extrémités de la pile doivent être les MÊMES opérateurs au signe près :
|
|
||||||
//
|
//
|
||||||
// FConstantFieldSource(+Base) ←→ FConstantFieldSource(-Base)
|
// FConstantFieldSource(+Base) ←→ FConstantFieldSource(-Base)
|
||||||
// FSdfConvertOp(Sign = -1) ←→ FSdfConvertOp(Sign = +1)
|
// FSdfConvertOp(Sign = -1) ←→ FSdfConvertOp(Sign = +1)
|
||||||
//
|
//
|
||||||
// Et c'est le cas : le seul opérateur neuf de ce portage est le blob d'île. Un archétype qui se
|
// Le seul opérateur propre à l'archétype est le blob d'île. / The only archetype-specific op is the
|
||||||
// réutilise en s'INVERSANT est une preuve plus forte qu'un archétype qui se réutilise à l'identique
|
// island blob; the rest is Maze's rock + carve with the opposite sign.
|
||||||
// — le premier dit que l'abstraction a trouvé le bon axe, le second seulement que deux archétypes
|
|
||||||
// se ressemblaient.
|
|
||||||
//
|
//
|
||||||
// ET LE VERDICT DE BOÎTE : c'est ici que `ClassifyBox` peut rendre **AllAir** pour la première fois
|
// ET LE VERDICT DE BOÎTE : une strate d'îles flottantes est surtout vide, donc `ClassifyBox` doit
|
||||||
// de tout le plugin. Une strate d'îles flottantes est, par construction, surtout vide ; aucun
|
// savoir rendre **AllAir** (`OPSTACK-DECOMPOSITION §7`). Le test compte AllSolid et AllAir
|
||||||
// archétype de grotte n'a jamais su prouver « tout air » (`OPSTACK-DECOMPOSITION §7`). Le test
|
// SÉPARÉMENT, parce qu'un total agrégé masquerait exactement ce verdict-là.
|
||||||
// compte les deux verdicts SÉPARÉMENT, parce qu'un total agrégé masquerait exactement ce gain-là.
|
|
||||||
//
|
|
||||||
// LA BARRE : bit à bit, comme les autres depuis `FPSemantics = Precise` (AUDIT §C9/§C10).
|
|
||||||
|
|
||||||
#if WITH_DEV_AUTOMATION_TESTS
|
#if WITH_DEV_AUTOMATION_TESTS
|
||||||
|
|
||||||
@@ -92,8 +80,8 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters)
|
|||||||
|
|
||||||
if (P.StrateTopWorldZ - P.StrateBottomWorldZ <= 0.0f)
|
if (P.StrateTopWorldZ - P.StrateBottomWorldZ <= 0.0f)
|
||||||
{
|
{
|
||||||
AddError(TEXT("The FloatingIslands strate has degenerate Z bounds, which sends ")
|
AddError(TEXT("The FloatingIslands strate has degenerate Z bounds; GetDensityAt builds no ")
|
||||||
TEXT("GetFloatingIslandDensity down its early-out. The op stack has none by design."));
|
TEXT("stack for it. The op stack has no degenerate-strate early-out by design."));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +115,7 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters)
|
|||||||
}
|
}
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// 1. ÉQUIVALENCE
|
// 1. LIVENESS — roche d'île et vide ouvert / island rock and open void
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// On compte SÉPARÉMENT le solide d'intérieur et le solide de seal : sur cet archétype la
|
// On compte SÉPARÉMENT le solide d'intérieur et le solide de seal : sur cet archétype la
|
||||||
// quasi-totalité du volume est de l'air, donc un « N solides » agrégé serait dominé par les
|
// quasi-totalité du volume est de l'air, donc un « N solides » agrégé serait dominé par les
|
||||||
@@ -135,74 +123,34 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters)
|
|||||||
const float InnerBot = P.StrateBottomWorldZ + P.BoundarySealThickness;
|
const float InnerBot = P.StrateBottomWorldZ + P.BoundarySealThickness;
|
||||||
const float InnerTop = P.StrateTopWorldZ - P.BoundarySealThickness;
|
const float InnerTop = P.StrateTopWorldZ - P.BoundarySealThickness;
|
||||||
|
|
||||||
int32 NumDiff = 0, NumSideDisagree = 0, WorstIdx = -1;
|
|
||||||
int32 NumInsideIsland = 0, NumOpenVoid = 0;
|
int32 NumInsideIsland = 0, NumOpenVoid = 0;
|
||||||
float WorstDelta = 0.0f;
|
|
||||||
|
|
||||||
for (int32 i = 0; i < NumIslandSamples; ++i)
|
for (int32 i = 0; i < NumIslandSamples; ++i)
|
||||||
{
|
{
|
||||||
const float X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (float)Points[i].Z;
|
const float X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (float)Points[i].Z;
|
||||||
|
|
||||||
const float Old = Gen->GetFloatingIslandDensity(X, Y, Z, P);
|
|
||||||
const float New = Stack.EvalMC(X, Y, Z);
|
const float New = Stack.EvalMC(X, Y, Z);
|
||||||
|
|
||||||
const bool bInterior = (Z > InnerBot && Z < InnerTop);
|
const bool bInterior = (Z > InnerBot && Z < InnerTop);
|
||||||
if (bInterior && Old < 0.0f) { ++NumInsideIsland; } // solide loin des seals ⇒ une île
|
if (bInterior && New < 0.0f) { ++NumInsideIsland; } // solide loin des seals ⇒ une île
|
||||||
if (bInterior && Old >= 0.0f) { ++NumOpenVoid; }
|
if (bInterior && New >= 0.0f) { ++NumOpenVoid; }
|
||||||
|
|
||||||
if (!BitEqual(Old, New))
|
|
||||||
{
|
|
||||||
++NumDiff;
|
|
||||||
const float D = FMath::Abs(Old - New);
|
|
||||||
if (D > WorstDelta) { WorstDelta = D; WorstIdx = i; }
|
|
||||||
}
|
|
||||||
if ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSideDisagree; }
|
|
||||||
}
|
}
|
||||||
|
AddInfo(FString::Printf(
|
||||||
if (NumDiff == 0)
|
TEXT("FloatingIslands: %d of %d samples inside island rock away from the seal bands, %d in ")
|
||||||
{
|
TEXT("open void."),
|
||||||
AddInfo(FString::Printf(
|
NumInsideIsland, NumIslandSamples, NumOpenVoid));
|
||||||
TEXT("FloatingIslands: bit-identical across %d samples (%d inside island rock away from ")
|
|
||||||
TEXT("the seal bands, %d in open void, so the void source, the blobs, the roughness and ")
|
|
||||||
TEXT("the fill were all exercised). The stack runs BACKWARDS -- void source + fill ")
|
|
||||||
TEXT("instead of rock source + carve -- using the SAME operators with the opposite ")
|
|
||||||
TEXT("sign. Only the blob source is new (OPSTACK-PLAN 2.5)."),
|
|
||||||
NumIslandSamples, NumInsideIsland, NumOpenVoid));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
AddError(FString::Printf(
|
|
||||||
TEXT("FloatingIslands: %d of %d samples differ (largest |delta| %.9g at (%.0f, %.0f, ")
|
|
||||||
TEXT("%.0f)); %d cross the isosurface. Since /fp:precise the bar is bit-identity, so ")
|
|
||||||
TEXT("this is a real port error. Check, in order: the C1 warp fix (BOTH paths must now ")
|
|
||||||
TEXT("use VoxelHash::SeedOffset(S, 0.0007f) -- if only one was changed, EVERY warped ")
|
|
||||||
TEXT("sample differs), then the SdfConvert SIGN (+1 fills, -1 carves), then the 'Isld' ")
|
|
||||||
TEXT("salt (0x49736C64), the roughness frequency (0.08 / 4 octaves here, NOT Maze's ")
|
|
||||||
TEXT("0.12 / 3), the per-island TaperEnd and TopFlatten dome branch, and the ")
|
|
||||||
TEXT("SmoothMin blend K = max(SDFBlendRadius, 0.01)."),
|
|
||||||
NumDiff, NumIslandSamples, WorstDelta,
|
|
||||||
WorstIdx >= 0 ? Points[WorstIdx].X : 0.0f,
|
|
||||||
WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f,
|
|
||||||
WorstIdx >= 0 ? Points[WorstIdx].Z : 0.0f,
|
|
||||||
NumSideDisagree));
|
|
||||||
}
|
|
||||||
|
|
||||||
TestEqual(TEXT("no sample lands on the opposite side of the isosurface"), NumSideDisagree, 0);
|
|
||||||
|
|
||||||
if (NumInsideIsland == 0)
|
if (NumInsideIsland == 0)
|
||||||
{
|
{
|
||||||
AddWarning(TEXT("No sample landed inside island rock away from the seal bands, so the blob ")
|
AddWarning(TEXT("No sample landed inside island rock away from the seal bands, so the blob ")
|
||||||
TEXT("source and the fill were never meaningfully exercised -- the equivalence ")
|
TEXT("source and the fill were never meaningfully exercised. Raise IslandDensity ")
|
||||||
TEXT("above then only proves that two empty voids agree. Raise IslandDensity or ")
|
TEXT("or IslandMaxRadius."));
|
||||||
TEXT("IslandMaxRadius."));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// 2. INVARIANCE DE FENÊTRE
|
// 2. INVARIANCE DE FENÊTRE
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// La source garde un cache 3×3 `thread_local` dont la clé est le jeu de params — et cette clé
|
// La source garde un cache 3×3 `thread_local` dont la clé est le jeu de params — et cette clé
|
||||||
// inclut délibérément `BoundarySealThickness`, que l'original omet alors que `SpreadZ` le lit
|
// inclut délibérément `BoundarySealThickness`, que `SpreadZ` lit (voir la note dans
|
||||||
// (voir la note dans FIslandBlobSource::GetCells).
|
// FIslandBlobSource::GetCells).
|
||||||
{
|
{
|
||||||
std::atomic<int32> Impure{ 0 };
|
std::atomic<int32> Impure{ 0 };
|
||||||
const int32 NumBlocks = FMath::Max(4, FMath::Min(16, FPlatformMisc::NumberOfCores()));
|
const int32 NumBlocks = FMath::Max(4, FMath::Min(16, FPlatformMisc::NumberOfCores()));
|
||||||
@@ -296,9 +244,8 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters)
|
|||||||
AddInfo(FString::Printf(
|
AddInfo(FString::Printf(
|
||||||
TEXT("Box verdicts over 60 FloatingIslands tiles (XY sampled from +/- %d voxels = %.1f x ")
|
TEXT("Box verdicts over 60 FloatingIslands tiles (XY sampled from +/- %d voxels = %.1f x ")
|
||||||
TEXT("IslandSpacing %.0f): %d proved AllSolid, %d proved AllAir, ")
|
TEXT("IslandSpacing %.0f): %d proved AllSolid, %d proved AllAir, ")
|
||||||
TEXT("%d Mixed, %d voxels checked, %d violations. Today's ClassifyTile proves ZERO of these. The AllAir count is the new ")
|
TEXT("%d Mixed, %d voxels checked, %d violations. The AllAir count matters most: a ")
|
||||||
TEXT("thing: no cave archetype has ever been able to prove 'all air', and a floating-")
|
TEXT("floating-island strate is mostly open air (OPSTACK-DECOMPOSITION 7)."),
|
||||||
TEXT("island strate is mostly exactly that (OPSTACK-DECOMPOSITION 7)."),
|
|
||||||
SpanVoxels, (float)SpanVoxels / FMath::Max(P.IslandSpacing, 1.0f), P.IslandSpacing,
|
SpanVoxels, (float)SpanVoxels / FMath::Max(P.IslandSpacing, 1.0f), P.IslandSpacing,
|
||||||
NumProvedSolid, NumProvedAir, NumMixed, NumBruteSamples, NumUnsound));
|
NumProvedSolid, NumProvedAir, NumMixed, NumBruteSamples, NumUnsound));
|
||||||
|
|
||||||
|
|||||||
@@ -1,73 +1,17 @@
|
|||||||
// VoxelForgeOpStackMazeTest.cpp
|
// VoxelForgeOpStackMazeTest.cpp
|
||||||
// PHASE 1, LE TEST QUI COMPTE — la pile d'opérateurs Maze contre GetMazeDensity.
|
// La pile d'opérateurs Maze / The Maze operator stack.
|
||||||
// PHASE 1'S LOAD-BEARING TEST — the Maze operator stack against GetMazeDensity.
|
|
||||||
//
|
//
|
||||||
// CE QUE LA PHASE 1 DEVAIT PROUVER / WHAT PHASE 1 HAD TO PROVE
|
// Ce que ce test vérifie / What this test checks:
|
||||||
// Le déclencheur d'arrêt de `OPSTACK-PLAN §4` : **« est-ce que la séparation source / modifier tombe
|
// • TOPOLOGIE — le graphe parent orienté vers l'origine est un arbre couvrant (N-1 arêtes, zéro
|
||||||
// naturellement du code existant ? »** Réponse mesurée : oui. Maze se décompose en huit opérateurs
|
// cellule déconnectée), les boucles optionnelles restent bornées, et le graphe est déterministe
|
||||||
// sans contorsion, le SDF est reproduit BIT POUR BIT, et aucun échantillon ne change de côté de
|
// sur 64 seeds. / The origin-directed parent graph is a spanning tree, deterministic per seed.
|
||||||
// l'isosurface.
|
// • DÉCOMPOSITION — huit opérateurs (roche + couloirs + rugosité + carve + 4 structurels), pas un
|
||||||
//
|
// `FMazeOp` monolithique. / Eight ops, not one wrapped monolith.
|
||||||
// ═════════════════════════════════════════════════════════════════════════════════════════
|
// • INVARIANCE DE FENÊTRE — le cache par cellule est `thread_local` : bit-identique quel que soit
|
||||||
// ✅ MISE À JOUR 2026-07-27 : LE PLANCHER ULP N'EXISTE PLUS. C'ÉTAIT `/fp:fast`.
|
// l'ordre des requêtes et le thread. / Bit-identical across query order and worker threads.
|
||||||
// ═════════════════════════════════════════════════════════════════════════════════════════
|
// • VERDICTS DE BOÎTE — chaque verdict uniforme survit à la force brute (un faux verdict est un
|
||||||
// `FPSemantics = Precise` sur le module (AUDIT §C9, posé pour le cross-play Linux/Windows) fait
|
// trou), y compris pour une pile source/convertisseur jamais écrite à la main.
|
||||||
// passer ce test à **BIT-IDENTIQUE**. La section ci-dessous décrit un état RÉVOLU ; elle est gardée
|
// Every uniform box verdict survives brute force, including for a novel source/converter stack.
|
||||||
// parce qu'elle explique pourquoi les cinq expériences d'isolation avaient toutes échoué (sous
|
|
||||||
// `/fp:fast` le compilateur transforme selon le CONTEXTE — il n'y avait aucune variable à isoler)
|
|
||||||
// et parce qu'elle dit quoi regarder si la bit-identité régresse un jour.
|
|
||||||
//
|
|
||||||
// **Conséquence pratique : ce test est maintenant un instrument BEAUCOUP plus fin.** Le moindre
|
|
||||||
// écart est désormais une vraie trouvaille, pas du bruit à noter. La machinerie de gradation ULP
|
|
||||||
// est conservée exprès — c'est elle qui signalerait une régression du modèle flottant.
|
|
||||||
//
|
|
||||||
// UPDATE: the ULP floor is GONE — FPSemantics = Precise makes this test bit-identical. The section
|
|
||||||
// below describes a past state, kept because it explains why five isolation experiments all failed
|
|
||||||
// (under /fp:fast the compiler transforms by CONTEXT — there was no variable to isolate) and what to
|
|
||||||
// look at if bit-identity ever regresses.
|
|
||||||
//
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────────────────
|
|
||||||
// ⚠️ LE PLANCHER ULP (HISTORIQUE) — lire ceci avant de « corriger » un écart résiduel
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────────────────
|
|
||||||
// (HISTORIQUE, avant `FPSemantics = Precise`) la pile reproduisait `GetMazeDensity` à ~1-2 ULP près
|
|
||||||
// sur ~2 % des échantillons (ceux qui tombent
|
|
||||||
// dans la coquille de blend du SDF, où `Blend - Sdf` annule catastrophiquement et amplifie le
|
|
||||||
// dernier arrondi). **Zéro échantillon ne traverse l'isosurface**, donc pas un triangle ne bouge.
|
|
||||||
//
|
|
||||||
// L'origine exacte de ce dernier arrondi n'a PAS été identifiée, après six cycles de build et cinq
|
|
||||||
// hypothèses toutes réfutées par la mesure (aller-retour FVector · fenêtre de rugosité · `/fp:fast`
|
|
||||||
// entre unités de compilation · contexte d'inlining · constante de compilation vs donnée
|
|
||||||
// d'exécution). Ce qui EST établi par la mesure :
|
|
||||||
//
|
|
||||||
// • le SDF est bit-identique sur 126/126 des écarts — le treillis, les hashs, l'ensemble d'arêtes
|
|
||||||
// et `VoxelSDF::Capsule` sont donc exacts ;
|
|
||||||
// • l'écart naît entièrement dans la conversion SDF→densité, au dernier arrondi ;
|
|
||||||
// • il est DÉTERMINISTE (mêmes échantillons, même delta, même coordonnée à chaque run) ;
|
|
||||||
// • il ne dépend ni de l'unité de compilation, ni de l'inlining, ni du modèle flottant.
|
|
||||||
//
|
|
||||||
// **Décision (Jahni, 2026-07-27) : on l'accepte et on avance.** Aucune décision du projet ne dépend
|
|
||||||
// de la réponse, et la chasse coûtait plus que l'information. Consigné comme point ouvert dans
|
|
||||||
// `AUDIT-2026-07.md §C10`.
|
|
||||||
//
|
|
||||||
// ⚠️ LA RÈGLE QUI EN DÉCOULE, ELLE, EST IMPORTANTE :
|
|
||||||
// **ne jamais faire tourner les deux chemins (switch d'archétype et pile d'opérateurs) dans le même
|
|
||||||
// monde, et ne jamais comparer leurs sorties pour égalité.** Ce n'est PAS un risque de désync entre
|
|
||||||
// clients — dans un même binaire le champ est prouvé pur (`VoxelForge.Determinism.DensityPurity`,
|
|
||||||
// bit-identique entre threads et ordres de requête) et tous les pairs exécutent le même chemin. Mais
|
|
||||||
// une strate à moitié migrée produirait une couture. Le vrai sujet multijoueur est ailleurs :
|
|
||||||
// `AUDIT §C9` (le défaut FP d'UBT diffère selon la toolchain).
|
|
||||||
//
|
|
||||||
// Never run both paths in one world and never compare their outputs for equality. This is NOT a
|
|
||||||
// client-desync risk — within one binary the field is proven pure and every peer runs the same path —
|
|
||||||
// but a half-migrated strate would produce a seam.
|
|
||||||
//
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────────────────
|
|
||||||
// LA BARRE D'ACCEPTATION, ENCODÉE CI-DESSOUS / THE ACCEPTANCE BAR, ENCODED BELOW
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────────────────
|
|
||||||
// • ÉCHEC DUR : un seul échantillon qui change de côté de l'isosurface (la géométrie bouge).
|
|
||||||
// • INFO : des écarts à l'échelle de l'ULP (le plancher, attendu).
|
|
||||||
// • WARN : un écart plus grand — ÇA, c'est une vraie dérive de portage, et il faut chercher.
|
|
||||||
// Un test qui avertit à chaque portage serait ignoré par le portage qui compte.
|
|
||||||
|
|
||||||
#if WITH_DEV_AUTOMATION_TESTS
|
#if WITH_DEV_AUTOMATION_TESTS
|
||||||
|
|
||||||
@@ -355,13 +299,13 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMazeDensity court-circuite sur une strate dégénérée (`return 1.0f`). Cette garde appartient
|
// La pile suppose une strate valide ; `GetDensityAt` rend de l'air sans pile sur une strate
|
||||||
// à la fonction d'archétype, pas à un opérateur ; la pile suppose une strate valide.
|
// dégénérée. / The stack assumes a valid strate; GetDensityAt returns air for a degenerate one.
|
||||||
if (MazeParams.StrateTopWorldZ - MazeParams.StrateBottomWorldZ <= 0.0f)
|
if (MazeParams.StrateTopWorldZ - MazeParams.StrateBottomWorldZ <= 0.0f)
|
||||||
{
|
{
|
||||||
AddError(FString::Printf(
|
AddError(FString::Printf(
|
||||||
TEXT("The Maze strate has degenerate Z bounds (top %.1f, bottom %.1f), which sends ")
|
TEXT("The Maze strate has degenerate Z bounds (top %.1f, bottom %.1f); GetDensityAt ")
|
||||||
TEXT("GetMazeDensity down its early-out. The op stack has no such early-out by design."),
|
TEXT("builds no stack for it. The op stack has no degenerate-strate early-out by design."),
|
||||||
MazeParams.StrateTopWorldZ, MazeParams.StrateBottomWorldZ));
|
MazeParams.StrateTopWorldZ, MazeParams.StrateBottomWorldZ));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -485,70 +429,6 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//=========================================================================
|
|
||||||
// ÉQUIVALENCE — géométrie d'abord, bits ensuite.
|
|
||||||
//=========================================================================
|
|
||||||
int32 NumDiff = 0, WorstIdx = -1, NumBeyondUlpNoise = 0, NumSolidDisagreements = 0;
|
|
||||||
float WorstDelta = 0.0f;
|
|
||||||
for (int32 i = 0; i < NumMazeSamples; ++i)
|
|
||||||
{
|
|
||||||
const float X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (float)Points[i].Z;
|
|
||||||
|
|
||||||
const float Old = Gen->GetMazeDensity(X, Y, Z, MazeParams); // MC : négatif = solide
|
|
||||||
const float New = Stack.EvalMC(X, Y, Z);
|
|
||||||
|
|
||||||
if (!BitEqual(Old, New))
|
|
||||||
{
|
|
||||||
++NumDiff;
|
|
||||||
const float Delta = FMath::Abs(Old - New);
|
|
||||||
if (Delta > WorstDelta) { WorstDelta = Delta; WorstIdx = i; }
|
|
||||||
|
|
||||||
// `Blend - Sdf` annule catastrophiquement au bord de la coquille de blend, donc un
|
|
||||||
// écart d'ULP sur le SDF ressort amplifié sur la densité : marge généreuse, mais bornée.
|
|
||||||
const float UlpNoise = 16.0f * FMath::Max(FMath::Abs(Old), 1.0f) * FLT_EPSILON;
|
|
||||||
if (Delta > UlpNoise) { ++NumBeyondUlpNoise; }
|
|
||||||
}
|
|
||||||
// Le mesher ne lit que le SIGNE (D >= IsoLevel ⇒ air). Un désaccord de CÔTÉ bouge la géométrie.
|
|
||||||
if ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSolidDisagreements; }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (NumDiff == 0)
|
|
||||||
{
|
|
||||||
AddInfo(FString::Printf(TEXT("Bit-identical across %d samples."), NumMazeSamples));
|
|
||||||
}
|
|
||||||
else if (NumBeyondUlpNoise == 0)
|
|
||||||
{
|
|
||||||
AddInfo(FString::Printf(
|
|
||||||
TEXT("%d of %d samples differ, ALL at ULP scale (largest |delta| %.9g at (%.0f, %.0f, ")
|
|
||||||
TEXT("%.0f)), and 0 cross the isosurface -- not one triangle would move. This is the ")
|
|
||||||
TEXT("accepted floor; see the header comment and AUDIT-2026-07.md C10. The SDF itself is ")
|
|
||||||
TEXT("reproduced BIT FOR BIT, so the lattice, the hashes and VoxelSDF::Capsule are exact; ")
|
|
||||||
TEXT("only the final SDF->density rounding differs. Do not go hunting this again without ")
|
|
||||||
TEXT("reading C10 first -- five hypotheses have already been measured and refuted."),
|
|
||||||
NumDiff, NumMazeSamples, WorstDelta,
|
|
||||||
WorstIdx >= 0 ? Points[WorstIdx].X : 0.0f,
|
|
||||||
WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f,
|
|
||||||
WorstIdx >= 0 ? Points[WorstIdx].Z : 0.0f));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
AddWarning(FString::Printf(
|
|
||||||
TEXT("%d of %d samples differ and %d are TOO LARGE to be the accepted ULP floor (largest ")
|
|
||||||
TEXT("|delta| %.9g at (%.0f, %.0f, %.0f)); %d cross the isosurface. THIS one is real port ")
|
|
||||||
TEXT("drift, not the known floor. Check, in order: the roughness apply-window ")
|
|
||||||
TEXT("(R + SurfaceRoughness + 2), the carve blend (2.0), the noise frequency (0.12) and ")
|
|
||||||
TEXT("octave count (3), and the order of the structural post ops."),
|
|
||||||
NumDiff, NumMazeSamples, NumBeyondUlpNoise, WorstDelta,
|
|
||||||
WorstIdx >= 0 ? Points[WorstIdx].X : 0.0f,
|
|
||||||
WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f,
|
|
||||||
WorstIdx >= 0 ? Points[WorstIdx].Z : 0.0f,
|
|
||||||
NumSolidDisagreements));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Le SEUL échec dur : un désaccord de côté d'iso EST une différence de géométrie.
|
|
||||||
TestEqual(TEXT("no sample lands on the opposite side of the isosurface from the original"),
|
|
||||||
NumSolidDisagreements, 0);
|
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// INVARIANCE DE FENÊTRE — la pile doit tenir les mêmes règles que le générateur.
|
// INVARIANCE DE FENÊTRE — la pile doit tenir les mêmes règles que le générateur.
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
@@ -624,10 +504,9 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
|
|||||||
//=========================================================================
|
//=========================================================================
|
||||||
// LE VERDICT DE BOÎTE — le prix perf de l'intervalle SDF conservatif.
|
// LE VERDICT DE BOÎTE — le prix perf de l'intervalle SDF conservatif.
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// ClassifyTile renvoie Mixed pour tout archétype de grotte, donc TunnelNetwork, Maze,
|
// Pour un slot de cave, ClassifyTile plie `ClassifyBox` de cette pile : tout nombre > 0 ici est
|
||||||
// VerticalShafts, FloatingIslands, FlatPlain, CrystalChamber et Underwater ne captent RIEN du
|
// une tuile que le mesher saute. / For a cave slot ClassifyTile folds this stack's ClassifyBox:
|
||||||
// gain T1.d. Tout nombre > 0 ici est le saut de tuile propre à la pile, pas une décision du
|
// every proved box here is a tile the mesher skips.
|
||||||
// classifieur historique.
|
|
||||||
{
|
{
|
||||||
int32 NumProved = 0, NumMixed = 0, NumUnsound = 0, NumBruteSamples = 0;
|
int32 NumProved = 0, NumMixed = 0, NumUnsound = 0, NumBruteSamples = 0;
|
||||||
FRandomStream Rng(24680);
|
FRandomStream Rng(24680);
|
||||||
@@ -689,17 +568,14 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
|
|||||||
|
|
||||||
AddInfo(FString::Printf(
|
AddInfo(FString::Printf(
|
||||||
TEXT("Box verdicts over %d Maze tiles (XY sampled from +/- %d voxels = %.1f x ")
|
TEXT("Box verdicts over %d Maze tiles (XY sampled from +/- %d voxels = %.1f x ")
|
||||||
TEXT("CellSize %.0f): %d proved uniform, %d Mixed, %d voxels checked, %d violations. Today's ClassifyTile ")
|
TEXT("CellSize %.0f): %d proved uniform, %d Mixed, %d voxels checked, %d violations."),
|
||||||
TEXT("proves ZERO of these -- every cave archetype falls through to \"pas prouvable en ")
|
|
||||||
TEXT("v1\". Any number above zero here is tile-skipping Maze has never had."),
|
|
||||||
NumMazeBoxTests, SpanVoxels, (float)SpanVoxels / FMath::Max(MazeParams.CellSize, 1.0f), MazeParams.CellSize,
|
NumMazeBoxTests, SpanVoxels, (float)SpanVoxels / FMath::Max(MazeParams.CellSize, 1.0f), MazeParams.CellSize,
|
||||||
NumProved, NumMixed, NumBruteSamples, NumUnsound));
|
NumProved, NumMixed, NumBruteSamples, NumUnsound));
|
||||||
|
|
||||||
if (NumProved == 0)
|
if (NumProved == 0)
|
||||||
{
|
{
|
||||||
AddWarning(TEXT("The stack proved no tile uniform, so it is not yet better than today's ")
|
AddWarning(TEXT("The stack proved no tile uniform, so ClassifyTile skips no Maze tile. ")
|
||||||
TEXT("classifier for Maze. Not a correctness problem, but the perf case for ")
|
TEXT("Not a correctness problem, but a perf regression."));
|
||||||
TEXT("the port rests on this number."));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
// VoxelForgeOpStackShaftTest.cpp
|
// VoxelForgeOpStackShaftTest.cpp
|
||||||
// VerticalShafts — le portage qui teste la RÉUTILISATION et le contrat de l'opérateur courant.
|
// VerticalShafts — la RÉUTILISATION d'opérateurs et le contrat de la pile.
|
||||||
// VerticalShafts — the port that tests REUSE and the current operator contract.
|
// VerticalShafts — operator REUSE and the stack's contract.
|
||||||
//
|
//
|
||||||
// CE QUE CELUI-CI PROUVE EN PLUS DES AUTRES
|
// Trois des cinq opérateurs de VerticalShafts sont ceux de Maze, repris tels quels :
|
||||||
// Les portages précédents demandaient « la décomposition reproduit-elle l'original ? ». Celui-ci
|
// `ConstantRock`, `SdfRoughness`, `SdfCarve` — mêmes ops, autre source et autres réglages
|
||||||
// demande **« les opérateurs se RÉUTILISENT-ils vraiment entre archétypes ? »**, qui est la thèse
|
// (fréquence 0.1 au lieu de 0.12, fenêtre `rough + 4` au lieu de `R + rough + 2`). C'est la thèse
|
||||||
// de `OPSTACK-PLAN §2.5` et la seule raison de faire ce refactor plutôt que de nettoyer le `switch`.
|
// de `OPSTACK-PLAN §2.5` : les opérateurs se réutilisent entre archétypes.
|
||||||
//
|
//
|
||||||
// Trois des cinq opérateurs de VerticalShafts sont ceux de Maze, **repris sans une ligne de
|
// Three of the five VerticalShafts ops are Maze's, reused unchanged. The assertions below target
|
||||||
// changement** : `ConstantRock`, `SdfRoughness`, `SdfCarve`. Dans le `switch`, `GetMazeDensity` et
|
// the stack used in-game: channel count/order, feature liveness, window invariance, cache cost and
|
||||||
// `GetVerticalShaftDensity` sont deux fonctions de ~100 lignes qui n'ont rien en commun à l'œil.
|
// box-proof soundness.
|
||||||
// En opérateurs, ce sont les mêmes trois ops avec une source différente et d'autres réglages
|
|
||||||
// (fréquence 0.1 au lieu de 0.12, fenêtre `rough + 4` au lieu de `R + rough + 2`).
|
|
||||||
//
|
|
||||||
// The direct helper remains useful migration telemetry, but it is not the owner path after the
|
|
||||||
// structural-post split. The assertions below target the operator stack actually used in-game:
|
|
||||||
// current channel count/order, feature liveness, cache isolation and box-proof soundness.
|
|
||||||
|
|
||||||
#if WITH_DEV_AUTOMATION_TESTS
|
#if WITH_DEV_AUTOMATION_TESTS
|
||||||
|
|
||||||
@@ -93,8 +87,8 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
|
|||||||
|
|
||||||
if (P.StrateTopWorldZ - P.StrateBottomWorldZ <= 0.0f)
|
if (P.StrateTopWorldZ - P.StrateBottomWorldZ <= 0.0f)
|
||||||
{
|
{
|
||||||
AddError(TEXT("The VerticalShafts strate has degenerate Z bounds, which sends ")
|
AddError(TEXT("The VerticalShafts strate has degenerate Z bounds; GetDensityAt builds no ")
|
||||||
TEXT("GetVerticalShaftDensity down its early-out. The op stack has none by design."));
|
TEXT("stack for it. The op stack has no degenerate-strate early-out by design."));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,49 +122,17 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
|
|||||||
}
|
}
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// 1. CURRENT-OWNER DIAGNOSTIC
|
// 1. LIVENESS — des échantillons tombent dans l'air des puits / samples land in shaft air
|
||||||
// GetVerticalShaftDensity is the retired direct path. Its delta is reported so a future
|
|
||||||
// port can be audited, but it is not an owner-path failure: the game evaluates Stack plus
|
|
||||||
// the common MC tail.
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
int32 NumDiff = 0, NumSideDisagree = 0, WorstIdx = -1, NumInsideShaft = 0;
|
int32 NumInsideShaft = 0;
|
||||||
float WorstDelta = 0.0f;
|
|
||||||
|
|
||||||
for (int32 i = 0; i < NumShaftSamples; ++i)
|
for (int32 i = 0; i < NumShaftSamples; ++i)
|
||||||
{
|
{
|
||||||
const float X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (float)Points[i].Z;
|
const float X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (float)Points[i].Z;
|
||||||
|
// air ⇒ dans un puits/connecteur/étagère / air ⇒ inside a shaft, connector or ledge
|
||||||
const float Old = Gen->GetVerticalShaftDensity(X, Y, Z, P);
|
if (EvalStackMCForTest(Stack, X, Y, Z) >= 0.0f) { ++NumInsideShaft; }
|
||||||
const float New = EvalStackMCForTest(Stack, X, Y, Z);
|
|
||||||
|
|
||||||
if (Old >= 0.0f) { ++NumInsideShaft; } // air ⇒ dans un puits/connecteur/étagère
|
|
||||||
|
|
||||||
if (!BitEqual(Old, New))
|
|
||||||
{
|
|
||||||
++NumDiff;
|
|
||||||
const float D = FMath::Abs(Old - New);
|
|
||||||
if (D > WorstDelta) { WorstDelta = D; WorstIdx = i; }
|
|
||||||
}
|
|
||||||
if ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSideDisagree; }
|
|
||||||
}
|
}
|
||||||
|
AddInfo(FString::Printf(TEXT("VerticalShafts: %d of %d samples are air in the stack."),
|
||||||
if (NumDiff == 0)
|
NumInsideShaft, NumShaftSamples));
|
||||||
{
|
|
||||||
AddInfo(FString::Printf(
|
|
||||||
TEXT("VerticalShafts: bit-identical across %d samples (%d of them inside a shaft, so ")
|
|
||||||
TEXT("the cylinders, connectors, roughness, carve and ledges were all exercised). ")
|
|
||||||
TEXT("THREE of the five ops here are Maze's, reused unchanged -- operator reuse across ")
|
|
||||||
TEXT("archetypes is now measured rather than intended (OPSTACK-PLAN 2.5)."),
|
|
||||||
NumShaftSamples, NumInsideShaft));
|
|
||||||
}
|
|
||||||
AddInfo(FString::Printf(
|
|
||||||
TEXT("VerticalShafts legacy diagnostic: %d of %d samples differ (largest |delta| %.9g at "
|
|
||||||
"(%.0f, %.0f, %.0f)); %d cross the isosurface; %d samples are air in the stack."),
|
|
||||||
NumDiff, NumShaftSamples, WorstDelta,
|
|
||||||
WorstIdx >= 0 ? Points[WorstIdx].X : 0.0f,
|
|
||||||
WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f,
|
|
||||||
WorstIdx >= 0 ? Points[WorstIdx].Z : 0.0f,
|
|
||||||
NumSideDisagree, NumInsideShaft));
|
|
||||||
TestTrue(TEXT("shaft feature sampling exercised the operator stack"), NumInsideShaft > 0);
|
TestTrue(TEXT("shaft feature sampling exercised the operator stack"), NumInsideShaft > 0);
|
||||||
|
|
||||||
if (NumInsideShaft == 0)
|
if (NumInsideShaft == 0)
|
||||||
@@ -268,14 +230,14 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
|
|||||||
const int32 CellY = (i / 16) - 8;
|
const int32 CellY = (i / 16) - 8;
|
||||||
LastX = (static_cast<float>(CellX) + 0.37f) * PerfSpacing;
|
LastX = (static_cast<float>(CellX) + 0.37f) * PerfSpacing;
|
||||||
LastY = (static_cast<float>(CellY) + 0.61f) * PerfSpacing;
|
LastY = (static_cast<float>(CellY) + 0.61f) * PerfSpacing;
|
||||||
Sink += Gen->GetVerticalShaftDensity(LastX, LastY, PerfZ, P);
|
Sink += EvalStackMCForTest(Stack, LastX, LastY, PerfZ);
|
||||||
}
|
}
|
||||||
const double RebuildSeconds = FPlatformTime::Seconds() - RebuildStart;
|
const double RebuildSeconds = FPlatformTime::Seconds() - RebuildStart;
|
||||||
|
|
||||||
const double HotStart = FPlatformTime::Seconds();
|
const double HotStart = FPlatformTime::Seconds();
|
||||||
for (int32 i = 0; i < NumHotSamples; ++i)
|
for (int32 i = 0; i < NumHotSamples; ++i)
|
||||||
{
|
{
|
||||||
Sink += Gen->GetVerticalShaftDensity(LastX, LastY, PerfZ, P);
|
Sink += EvalStackMCForTest(Stack, LastX, LastY, PerfZ);
|
||||||
}
|
}
|
||||||
const double HotSeconds = FPlatformTime::Seconds() - HotStart;
|
const double HotSeconds = FPlatformTime::Seconds() - HotStart;
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,20 @@
|
|||||||
// VoxelForgeOpStackSlabTest.cpp
|
// VoxelForgeOpStackSlabTest.cpp
|
||||||
// PHASE 2, PREMIER PORTAGE — la pile Slab courante, sur LES DEUX archétypes.
|
// La pile Slab, sur LES DEUX archétypes / The Slab operator stack, on BOTH archetypes.
|
||||||
// PHASE 2'S FIRST PORT — the current Slab operator stack, on BOTH archetypes.
|
|
||||||
//
|
//
|
||||||
// CE QUE CE TEST DOIT PROUVER / WHAT THIS TEST HAS TO PROVE
|
// CE QUE CE TEST DOIT PROUVER / WHAT THIS TEST HAS TO PROVE
|
||||||
// Trois choses, et la troisième est la raison d'être du portage :
|
|
||||||
//
|
//
|
||||||
// 1. CURRENT OWNER — the stack's channels, window invariance and conservative box verdicts
|
// 1. LE CHEMIN DU JEU — la pile rend une densité finie, invariante à la fenêtre (ordre et
|
||||||
// guard the path used by the game. `GetSlabDensity` is retained as a legacy diagnostic only:
|
// threads), et ses verdicts de boîte sont conservatifs.
|
||||||
// it still owns the former full passage/support tail, while the operator stack is followed by
|
// The stack's density is finite and window-invariant, and its box verdicts are conservative.
|
||||||
// the current common MC post.
|
// 2. UN OPÉRATEUR, DEUX ARCHÉTYPES — la MÊME pile est vérifiée pour FlatPlain ET CrystalChamber.
|
||||||
// 2. UN OPÉRATEUR, DEUX ARCHÉTYPES — la MÊME pile est vérifiée pour FlatPlain ET
|
|
||||||
// CrystalChamber. The legacy helper is sampled for migration telemetry, not used as the
|
|
||||||
// owner oracle; if the current stack becomes non-finite or loses its channel contract, this
|
|
||||||
// test still fails.
|
|
||||||
// ⚠️ La fixture ne règle que `GeneratorType`, donc les deux slots portent des params PAR
|
// ⚠️ La fixture ne règle que `GeneratorType`, donc les deux slots portent des params PAR
|
||||||
// DÉFAUT : à eux seuls ils exécutent la même configuration à deux profondeurs. C'est la
|
// DÉFAUT : à eux seuls ils exécutent la même configuration à deux profondeurs. C'est la
|
||||||
// TROISIÈME passe (`CrystalChamber(tuned)`, `CeilingRoughness` 6 → 20) qui fait réellement
|
// TROISIÈME passe (`CrystalChamber(tuned)`, `CeilingRoughness` 6 → 20) qui fait réellement
|
||||||
// varier ce qui distingue les deux archétypes — et qui sert en même temps de pire cas aux
|
// varier ce qui distingue les deux archétypes — et qui sert en même temps de pire cas aux
|
||||||
// bornes d'amplitude de `ClassifyBox`. Voir le bloc en bas de fichier.
|
// bornes d'amplitude de `ClassifyBox`. Voir le bloc en bas de fichier.
|
||||||
// 3. LE VERDICT DE BOÎTE — et c'est ici que §3.1 se paie. `ClassifyTile` prouve ZÉRO tuile pour
|
// 3. LE VERDICT DE BOÎTE — les deux surfaces sont XY-PURES, donc leurs bornes en Z sont connues
|
||||||
// FlatPlain et CrystalChamber aujourd'hui. Depuis que les deux surfaces sont XY-PURES, leurs
|
// exactement (contrat [-1,1] de FBM) : toute tuile entièrement sous le sol ou entre les deux
|
||||||
// bornes en Z sont connues exactement (contrat [-1,1] de FBM), donc toute tuile entièrement
|
// bandes se prouve SANS échantillonner.
|
||||||
// sous le sol ou entre les deux bandes se prouve SANS échantillonner.
|
|
||||||
//
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────────────────
|
|
||||||
// The old direct helper contains the removed support-floor/room tail. Room ownership and the
|
|
||||||
// authored-floor removal deliberately split that structural post from the operator stack, so its
|
|
||||||
// differences remain visible as diagnostics while the owner assertions below cover the live path.
|
|
||||||
|
|
||||||
#if WITH_DEV_AUTOMATION_TESTS
|
#if WITH_DEV_AUTOMATION_TESTS
|
||||||
|
|
||||||
@@ -73,14 +61,13 @@ bool FVoxelForgeOpStackSlabTest::RunTest(const FString& Parameters)
|
|||||||
int32 TopVoxelZ, int32 BottomVoxelZ,
|
int32 TopVoxelZ, int32 BottomVoxelZ,
|
||||||
int32 SlotIndex, const TCHAR* SlotName)
|
int32 SlotIndex, const TCHAR* SlotName)
|
||||||
{
|
{
|
||||||
// `GetSlabDensity` court-circuite sur une strate dégénérée (`return 1.0f`). Cette garde
|
// La pile suppose une strate valide ; `GetDensityAt` rend de l'air sans pile sur une strate
|
||||||
// appartient à la fonction d'archétype, pas à un opérateur ; la pile suppose une strate
|
// dégénérée. / The stack assumes a valid strate; GetDensityAt returns air for a degenerate one.
|
||||||
// valide, et `GetDensityAt` retombe sur le `switch` dans ce cas.
|
|
||||||
if (SlabParams.StrateTopWorldZ - SlabParams.StrateBottomWorldZ <= 0.0f)
|
if (SlabParams.StrateTopWorldZ - SlabParams.StrateBottomWorldZ <= 0.0f)
|
||||||
{
|
{
|
||||||
AddError(FString::Printf(
|
AddError(FString::Printf(
|
||||||
TEXT("%s has degenerate Z bounds (top %.1f, bottom %.1f), which sends GetSlabDensity ")
|
TEXT("%s has degenerate Z bounds (top %.1f, bottom %.1f); GetDensityAt builds no ")
|
||||||
TEXT("down its early-out. The op stack has no such early-out by design."),
|
TEXT("stack for it. The op stack has no degenerate-strate early-out by design."),
|
||||||
SlotName, SlabParams.StrateTopWorldZ, SlabParams.StrateBottomWorldZ));
|
SlotName, SlabParams.StrateTopWorldZ, SlabParams.StrateBottomWorldZ));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -114,90 +101,16 @@ bool FVoxelForgeOpStackSlabTest::RunTest(const FString& Parameters)
|
|||||||
}
|
}
|
||||||
|
|
||||||
//=====================================================================
|
//=====================================================================
|
||||||
// 1. ÉQUIVALENCE — géométrie d'abord, bits ensuite.
|
// 1. DENSITÉ FINIE / FINITE DENSITY
|
||||||
//=====================================================================
|
//=====================================================================
|
||||||
// ─────────────────────────────────────────────────────────────────────
|
|
||||||
// LE BON MÈTRE — corrigé 2026-07-27 après que la passe `tuned` a crié au loup
|
|
||||||
// ─────────────────────────────────────────────────────────────────────
|
|
||||||
// Première version : `16 · max(|Old|, 1) · FLT_EPSILON`, c.-à-d. l'ULP mesuré sur la
|
|
||||||
// DENSITÉ DE SORTIE. C'est le mauvais mètre, et il se trompe exactement là où le test
|
|
||||||
// regarde le plus : la densité vaut `min(Z - Sol, Plafond - Z)`, donc PRÈS DE L'ISOSURFACE
|
|
||||||
// la sortie tend vers 0 pendant que les intermédiaires (surfaces, Z monde, amplitudes de
|
|
||||||
// bruit) valent des CENTAINES. Un arrondi né à l'échelle 400 était jugé contre un mètre
|
|
||||||
// à l'échelle 1 — 400× trop serré.
|
|
||||||
//
|
|
||||||
// Mesuré : la passe `tuned` (rugosités ×2.25 et ×3.33) a vu ses écarts croître ×4.5, et
|
|
||||||
// son pire écart valait **0.345 ULP de |Z|**. Sous-ULP à l'échelle où l'erreur naît.
|
|
||||||
// L'erreur est donc proportionnelle à l'AMPLITUDE, ce qui est la signature d'un arrondi
|
|
||||||
// ordinaire, pas d'une transcription fausse.
|
|
||||||
//
|
|
||||||
// Le mètre correct est la magnitude des quantités D'OÙ VIENT l'erreur. Le test reste
|
|
||||||
// discriminant : une vraie dérive de portage (offset de bruit faux, `abs()` manquant,
|
|
||||||
// clamp oublié) déplace la surface de plusieurs VOXELS — 4 ordres de grandeur au-dessus
|
|
||||||
// de ce seuil, pas 4 fois.
|
|
||||||
//
|
|
||||||
// The first yardstick measured ULPs on the OUTPUT density, which tends to 0 near the
|
|
||||||
// isosurface while the intermediates are in the hundreds. Rounding born at scale ~400 was
|
|
||||||
// judged against a yardstick of scale 1. Real port drift moves the surface by voxels —
|
|
||||||
// four orders of magnitude above this bound, so the test stays discriminating.
|
|
||||||
const float SurfaceScale = FMath::Max(FMath::Abs(SlabParams.StrateTopWorldZ),
|
|
||||||
FMath::Abs(SlabParams.StrateBottomWorldZ));
|
|
||||||
|
|
||||||
int32 NumDiff = 0, WorstIdx = -1, NumBeyondUlpNoise = 0, NumSolidDisagreements = 0;
|
|
||||||
int32 NumNonFinite = 0;
|
int32 NumNonFinite = 0;
|
||||||
float WorstDelta = 0.0f, WorstOld = 0.0f, WorstUlpsOfScale = 0.0f;
|
|
||||||
// Le pire cas PARMI LES DÉPASSEMENTS — c'est lui qui dit si un WARN est du bruit ou une dérive.
|
|
||||||
float WorstOutlierDelta = 0.0f, WorstOutlierOld = 0.0f, WorstOutlierUlps = 0.0f;
|
|
||||||
|
|
||||||
for (int32 i = 0; i < NumSlabSamples; ++i)
|
for (int32 i = 0; i < NumSlabSamples; ++i)
|
||||||
{
|
{
|
||||||
const float X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (float)Points[i].Z;
|
if (!FMath::IsFinite(Stack.EvalMC((float)Points[i].X, (float)Points[i].Y, (float)Points[i].Z)))
|
||||||
|
|
||||||
const float Old = Gen->GetSlabDensity(X, Y, Z, SlabParams); // MC : négatif = solide
|
|
||||||
const float New = Stack.EvalMC(X, Y, Z);
|
|
||||||
if (!FMath::IsFinite(New))
|
|
||||||
{
|
{
|
||||||
++NumNonFinite;
|
++NumNonFinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!BitEqual(Old, New))
|
|
||||||
{
|
|
||||||
++NumDiff;
|
|
||||||
const float Delta = FMath::Abs(Old - New);
|
|
||||||
|
|
||||||
// L'échelle à laquelle CET échantillon calcule : la sortie, sa propre altitude, et
|
|
||||||
// les bornes de la strate. C'est le plus grand des trois qui porte l'arrondi.
|
|
||||||
const float Scale = FMath::Max3(FMath::Abs(Old), FMath::Abs(Z),
|
|
||||||
FMath::Max(SurfaceScale, 1.0f));
|
|
||||||
const float Ulps = Delta / (Scale * FLT_EPSILON);
|
|
||||||
|
|
||||||
if (Delta > WorstDelta)
|
|
||||||
{
|
|
||||||
WorstDelta = Delta; WorstIdx = i; WorstOld = Old; WorstUlpsOfScale = Ulps;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Delta > 16.0f * Scale * FLT_EPSILON)
|
|
||||||
{
|
|
||||||
++NumBeyondUlpNoise;
|
|
||||||
if (Delta > WorstOutlierDelta)
|
|
||||||
{
|
|
||||||
WorstOutlierDelta = Delta; WorstOutlierOld = Old; WorstOutlierUlps = Ulps;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Le mesher ne lit que le SIGNE. Un désaccord de CÔTÉ bouge la géométrie.
|
|
||||||
if ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSolidDisagreements; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AddInfo(FString::Printf(
|
|
||||||
TEXT("%s legacy slab diagnostic: %d of %d samples differ, %d exceed the old ULP "
|
|
||||||
"bound, largest |delta| %.9g at (%.0f,%.0f,%.0f), %d side disagreements; "
|
|
||||||
"current stack non-finite samples=%d."),
|
|
||||||
SlotName, NumDiff, NumSlabSamples, NumBeyondUlpNoise, WorstDelta,
|
|
||||||
WorstIdx >= 0 ? Points[WorstIdx].X : 0.0f,
|
|
||||||
WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f,
|
|
||||||
WorstIdx >= 0 ? Points[WorstIdx].Z : 0.0f,
|
|
||||||
NumSolidDisagreements, NumNonFinite));
|
|
||||||
TestEqual(*FString::Printf(TEXT("%s: current stack has no non-finite density"), SlotName),
|
TestEqual(*FString::Printf(TEXT("%s: current stack has no non-finite density"), SlotName),
|
||||||
NumNonFinite, 0);
|
NumNonFinite, 0);
|
||||||
|
|
||||||
@@ -300,9 +213,9 @@ bool FVoxelForgeOpStackSlabTest::RunTest(const FString& Parameters)
|
|||||||
|
|
||||||
AddInfo(FString::Printf(
|
AddInfo(FString::Printf(
|
||||||
TEXT("%s box verdicts over %d tiles (XY sampled from +/- %d voxels = %.1f x ")
|
TEXT("%s box verdicts over %d tiles (XY sampled from +/- %d voxels = %.1f x ")
|
||||||
TEXT("ColumnSpacing %.0f): %d proved uniform, %d Mixed, %d voxels checked, %d violations. Today's ")
|
TEXT("ColumnSpacing %.0f): %d proved uniform, %d Mixed, %d voxels checked, %d violations. ")
|
||||||
TEXT("ClassifyTile proves ZERO of these. This number is the whole point of making ")
|
TEXT("This number is the whole point of making the slab surfaces XY-pure ")
|
||||||
TEXT("the slab surfaces XY-pure (OPSTACK-DECOMPOSITION 3.1)."),
|
TEXT("(OPSTACK-DECOMPOSITION 3.1)."),
|
||||||
SlotName, NumSlabTiles, SpanVoxels,
|
SlotName, NumSlabTiles, SpanVoxels,
|
||||||
(float)SpanVoxels / FMath::Max(SlabParams.ColumnSpacing, 1.0f), SlabParams.ColumnSpacing,
|
(float)SpanVoxels / FMath::Max(SlabParams.ColumnSpacing, 1.0f), SlabParams.ColumnSpacing,
|
||||||
NumProved, NumMixed, NumBruteSamples, NumUnsound));
|
NumProved, NumMixed, NumBruteSamples, NumUnsound));
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "Misc/AutomationTest.h"
|
#include "Misc/AutomationTest.h"
|
||||||
|
|
||||||
#include "VoxelCaveMorphology.h"
|
#include "VoxelCaveMorphology.h"
|
||||||
|
#include "VoxelDensityOpStack.h"
|
||||||
#include "VoxelDensityPrimitives.h"
|
#include "VoxelDensityPrimitives.h"
|
||||||
#include "VoxelForgeTestFixture.h"
|
#include "VoxelForgeTestFixture.h"
|
||||||
#include "VoxelPassageGeometry.h"
|
#include "VoxelPassageGeometry.h"
|
||||||
@@ -900,11 +901,27 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
|
|||||||
++NumFootingChecked;
|
++NumFootingChecked;
|
||||||
|
|
||||||
// The live world's passage modifier is intentionally absent here. The endpoint is
|
// The live world's passage modifier is intentionally absent here. The endpoint is
|
||||||
// already known to be passage-carved, so this second generator checks the archetype
|
// already known to be passage-carved, so the archetype's own stack, built without a
|
||||||
// source itself: air at the landing and solid immediately below it.
|
// strate manager (hence without passage carving), checks the source itself: air at
|
||||||
TStrongObjectPtr<UVoxelGenerator> SourceOnlyGenerator(
|
// the landing and solid immediately below it.
|
||||||
NewObject<UVoxelGenerator>(GetTransientPackage(), NAME_None, RF_Transient));
|
const UVoxelGenerator& LiveGenerator = *World.Generator;
|
||||||
SourceOnlyGenerator->InitializeSettings(World.Settings.Get());
|
FVoxelOpStack SourceStack;
|
||||||
|
FVoxelOpContext SourceContext;
|
||||||
|
SourceContext.Seed = (uint32)LiveGenerator.Seed;
|
||||||
|
SourceContext.LayoutVersion = World.StrateManager->GetLayoutVersion();
|
||||||
|
SourceContext.StrateTopWorldZ = StrateTopZ;
|
||||||
|
SourceContext.StrateBottomWorldZ = StrateBottomZ;
|
||||||
|
SourceContext.WorldRadiusVoxels = LiveGenerator.WorldRadiusVoxels;
|
||||||
|
SourceContext.EdgeSealThickness = LiveGenerator.EdgeSealThickness;
|
||||||
|
// The generator resets the shaft-connector hand-off around every density query; a
|
||||||
|
// direct stack evaluation must model that boundary too.
|
||||||
|
auto EvalSource = [&SourceStack](float QueryX, float QueryY, float QueryZ)
|
||||||
|
{
|
||||||
|
VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker();
|
||||||
|
const float SourceDensity = SourceStack.EvalMC(QueryX, QueryY, QueryZ);
|
||||||
|
VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker();
|
||||||
|
return SourceDensity;
|
||||||
|
};
|
||||||
|
|
||||||
float LandingDensity = 0.0f;
|
float LandingDensity = 0.0f;
|
||||||
float FootingDensity = 0.0f;
|
float FootingDensity = 0.0f;
|
||||||
@@ -915,13 +932,16 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
|
|||||||
FMazeGenerationParams P = Definition->MazeParams;
|
FMazeGenerationParams P = Definition->MazeParams;
|
||||||
P.StrateTopWorldZ = StrateTopZ;
|
P.StrateTopWorldZ = StrateTopZ;
|
||||||
P.StrateBottomWorldZ = StrateBottomZ;
|
P.StrateBottomWorldZ = StrateBottomZ;
|
||||||
LandingDensity = SourceOnlyGenerator->GetMazeDensity(
|
VoxelDensityOps::BuildMazeStack(SourceStack, P, LiveGenerator.Seed,
|
||||||
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z, P);
|
LiveGenerator.OriginSpineRadius, /*StrateManager*/nullptr);
|
||||||
|
SourceStack.PrepareChunk(SourceContext);
|
||||||
|
LandingDensity = EvalSource(
|
||||||
|
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z);
|
||||||
const float FloorProbeZ = SuggestedPoint.Z
|
const float FloorProbeZ = SuggestedPoint.Z
|
||||||
- FMath::Max(P.CorridorRadius, 0.5f)
|
- FMath::Max(P.CorridorRadius, 0.5f)
|
||||||
- P.SurfaceRoughness * VOXEL_NOISE_SCALE * 1.5f - 3.0f;
|
- P.SurfaceRoughness * VOXEL_NOISE_SCALE * 1.5f - 3.0f;
|
||||||
FootingDensity = SourceOnlyGenerator->GetMazeDensity(
|
FootingDensity = EvalSource(
|
||||||
SuggestedPoint.X, SuggestedPoint.Y, FloorProbeZ, P);
|
SuggestedPoint.X, SuggestedPoint.Y, FloorProbeZ);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -930,11 +950,14 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
|
|||||||
FVerticalShaftParams P = Definition->VerticalShaftParams;
|
FVerticalShaftParams P = Definition->VerticalShaftParams;
|
||||||
P.StrateTopWorldZ = StrateTopZ;
|
P.StrateTopWorldZ = StrateTopZ;
|
||||||
P.StrateBottomWorldZ = StrateBottomZ;
|
P.StrateBottomWorldZ = StrateBottomZ;
|
||||||
LandingDensity = SourceOnlyGenerator->GetVerticalShaftDensity(
|
VoxelDensityOps::BuildVerticalShaftStack(SourceStack, P, LiveGenerator.Seed,
|
||||||
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z, P);
|
LiveGenerator.OriginSpineRadius, /*StrateManager*/nullptr);
|
||||||
|
SourceStack.PrepareChunk(SourceContext);
|
||||||
|
LandingDensity = EvalSource(
|
||||||
|
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z);
|
||||||
const float FloorProbeZ = StrateBottomZ + P.BoundarySealThickness * 0.5f;
|
const float FloorProbeZ = StrateBottomZ + P.BoundarySealThickness * 0.5f;
|
||||||
FootingDensity = SourceOnlyGenerator->GetVerticalShaftDensity(
|
FootingDensity = EvalSource(
|
||||||
SuggestedPoint.X, SuggestedPoint.Y, FloorProbeZ, P);
|
SuggestedPoint.X, SuggestedPoint.Y, FloorProbeZ);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -943,10 +966,13 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
|
|||||||
FFloatingIslandParams P = Definition->FloatingIslandParams;
|
FFloatingIslandParams P = Definition->FloatingIslandParams;
|
||||||
P.StrateTopWorldZ = StrateTopZ;
|
P.StrateTopWorldZ = StrateTopZ;
|
||||||
P.StrateBottomWorldZ = StrateBottomZ;
|
P.StrateBottomWorldZ = StrateBottomZ;
|
||||||
LandingDensity = SourceOnlyGenerator->GetFloatingIslandDensity(
|
VoxelDensityOps::BuildFloatingIslandStack(SourceStack, P, LiveGenerator.Seed,
|
||||||
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z, P);
|
LiveGenerator.OriginSpineRadius, /*StrateManager*/nullptr);
|
||||||
FootingDensity = SourceOnlyGenerator->GetFloatingIslandDensity(
|
SourceStack.PrepareChunk(SourceContext);
|
||||||
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z - 1.0f, P);
|
LandingDensity = EvalSource(
|
||||||
|
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z);
|
||||||
|
FootingDensity = EvalSource(
|
||||||
|
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z - 1.0f);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -140,7 +140,6 @@ namespace
|
|||||||
Definition->SurfaceParams = Candidate.ArchetypeParams.SurfaceParams;
|
Definition->SurfaceParams = Candidate.ArchetypeParams.SurfaceParams;
|
||||||
Definition->VerticalShaftParams = Candidate.ArchetypeParams.VerticalShaftParams;
|
Definition->VerticalShaftParams = Candidate.ArchetypeParams.VerticalShaftParams;
|
||||||
Definition->FloatingIslandParams = Candidate.ArchetypeParams.FloatingIslandParams;
|
Definition->FloatingIslandParams = Candidate.ArchetypeParams.FloatingIslandParams;
|
||||||
Definition->bUseOperatorStack = true;
|
|
||||||
Definition->TransitionType = EVoxelStrateTransition::Hard;
|
Definition->TransitionType = EVoxelStrateTransition::Hard;
|
||||||
World.Reinitialize();
|
World.Reinitialize();
|
||||||
}
|
}
|
||||||
@@ -869,7 +868,7 @@ bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters)
|
|||||||
// Resolve every probe from the current corpus. This keeps the diagnosis attached to real
|
// Resolve every probe from the current corpus. This keeps the diagnosis attached to real
|
||||||
// single-region candidates without making a corpus asset edit look like a geometry failure.
|
// single-region candidates without making a corpus asset edit look like a geometry failure.
|
||||||
FTestWorld World;
|
FTestWorld World;
|
||||||
World.Build(DiagnosisSeed, 2, true, 8);
|
World.Build(DiagnosisSeed, 2, 8);
|
||||||
if (!World.IsValid())
|
if (!World.IsValid())
|
||||||
{
|
{
|
||||||
AddError(World.WhyInvalid());
|
AddError(World.WhyInvalid());
|
||||||
|
|||||||
@@ -64,8 +64,7 @@ bool FVoxelForgeSealedSolidProofTest::RunTest(const FString& Parameters)
|
|||||||
// Forty voxels makes a complete 32-cell level-0 tile fit inside a bottom boundary seal. The
|
// Forty voxels makes a complete 32-cell level-0 tile fit inside a bottom boundary seal. The
|
||||||
// production asset remains untouched; this is a transient proof-only fixture configuration.
|
// production asset remains untouched; this is a transient proof-only fixture configuration.
|
||||||
FTestWorld World;
|
FTestWorld World;
|
||||||
World.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/false,
|
World.Build(/*Seed*/1337, /*GapChunks*/2, /*InStrateHeightInChunks*/8);
|
||||||
/*InStrateHeightInChunks*/8);
|
|
||||||
if (!World.IsValid())
|
if (!World.IsValid())
|
||||||
{
|
{
|
||||||
AddError(World.WhyInvalid());
|
AddError(World.WhyInvalid());
|
||||||
|
|||||||
@@ -1325,54 +1325,38 @@ bool FVoxelForgeVerticalShaftSeamTest::RunTest(const FString& Parameters)
|
|||||||
{
|
{
|
||||||
using namespace VoxelForgeTest;
|
using namespace VoxelForgeTest;
|
||||||
|
|
||||||
bool bAllChecksPassed = true;
|
FTestWorld World;
|
||||||
FString Summary = TEXT("VerticalShafts seam-freedom check (cell-boundary probes with two "
|
World.Build(/*InSeed=*/1337, /*InGapChunks=*/2);
|
||||||
"neighbouring chunk-context orders):\n");
|
if (!World.IsValid())
|
||||||
for (const bool bUseOperatorStack : { false, true })
|
|
||||||
{
|
{
|
||||||
FTestWorld World;
|
AddError(TEXT("HARD FAILURE: could not build the seam-test fixture."));
|
||||||
World.Build(/*InSeed=*/1337, /*InGapChunks=*/2, bUseOperatorStack);
|
return false;
|
||||||
if (!World.IsValid())
|
|
||||||
{
|
|
||||||
AddError(FString::Printf(
|
|
||||||
TEXT("HARD FAILURE: could not build the %s seam-test fixture."),
|
|
||||||
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy")));
|
|
||||||
bAllChecksPassed = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
int32 Checked = 0;
|
|
||||||
int32 Mismatches = 0;
|
|
||||||
FString FirstMismatch;
|
|
||||||
if (!CheckVerticalShaftSeams(World, Checked, Mismatches, FirstMismatch))
|
|
||||||
{
|
|
||||||
AddError(FString::Printf(
|
|
||||||
TEXT("HARD FAILURE: the %s seam test sampled no VerticalShafts positions."),
|
|
||||||
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy")));
|
|
||||||
bAllChecksPassed = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Summary += FString::Printf(
|
|
||||||
TEXT(" %s path: %d same-position re-evaluations, %d bit mismatches%s%s.\n"),
|
|
||||||
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy"),
|
|
||||||
Checked,
|
|
||||||
Mismatches,
|
|
||||||
Mismatches > 0 ? TEXT("; first ") : TEXT(""),
|
|
||||||
Mismatches > 0 ? *FirstMismatch : TEXT(""));
|
|
||||||
if (Mismatches != 0)
|
|
||||||
{
|
|
||||||
AddError(FString::Printf(
|
|
||||||
TEXT("SEAM: %s VerticalShafts density changed after different chunk-context "
|
|
||||||
"orders: %s"),
|
|
||||||
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy"),
|
|
||||||
*FirstMismatch));
|
|
||||||
bAllChecksPassed = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AddInfo(Summary);
|
int32 Checked = 0;
|
||||||
return bAllChecksPassed;
|
int32 Mismatches = 0;
|
||||||
|
FString FirstMismatch;
|
||||||
|
if (!CheckVerticalShaftSeams(World, Checked, Mismatches, FirstMismatch))
|
||||||
|
{
|
||||||
|
AddError(TEXT("HARD FAILURE: the seam test sampled no VerticalShafts positions."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AddInfo(FString::Printf(
|
||||||
|
TEXT("VerticalShafts seam-freedom check (cell-boundary probes with two neighbouring "
|
||||||
|
"chunk-context orders): %d same-position re-evaluations, %d bit mismatches%s%s."),
|
||||||
|
Checked,
|
||||||
|
Mismatches,
|
||||||
|
Mismatches > 0 ? TEXT("; first ") : TEXT(""),
|
||||||
|
Mismatches > 0 ? *FirstMismatch : TEXT("")));
|
||||||
|
if (Mismatches != 0)
|
||||||
|
{
|
||||||
|
AddError(FString::Printf(
|
||||||
|
TEXT("SEAM: VerticalShafts density changed after different chunk-context orders: %s"),
|
||||||
|
*FirstMismatch));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
|
bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
// ---------------
|
// ---------------
|
||||||
// The interesting invariants (density purity across worker threads, ClassifyTile soundness)
|
// The interesting invariants (density purity across worker threads, ClassifyTile soundness)
|
||||||
// only fire on the REAL path — UVoxelGenerator::GetDensityAt — because that is where the
|
// only fire on the REAL path — UVoxelGenerator::GetDensityAt — because that is where the
|
||||||
// thread_local per-chunk caches live (CP_*, GSurfColCache, the diff slots, the SDF cache).
|
// thread_local per-chunk caches live (CP_*, the prepared stacks, the diff slots, the SDF cache).
|
||||||
// Calling GetSurfaceDensity / GetMazeDensity directly bypasses every one of them and would
|
// Evaluating a stack directly bypasses every one of them and would test almost nothing.
|
||||||
// test almost nothing. GetDensityAt in turn needs a live UVoxelStrateManager, whose only
|
// GetDensityAt in turn needs a live UVoxelStrateManager, whose only
|
||||||
// entry point is Initialize(UVoxelSettings*, int32) reading TSoftObjectPtr pools.
|
// entry point is Initialize(UVoxelSettings*, int32) reading TSoftObjectPtr pools.
|
||||||
//
|
//
|
||||||
// So the fixture builds a whole synthetic world in memory: transient strate definitions →
|
// So the fixture builds a whole synthetic world in memory: transient strate definitions →
|
||||||
@@ -46,7 +46,7 @@ namespace VoxelForgeTest
|
|||||||
* generator + diff layer. No AActor, no UWorld, no PIE.
|
* generator + diff layer. No AActor, no UWorld, no PIE.
|
||||||
*
|
*
|
||||||
* The default layout stacks one strate of EVERY archetype (in ECaveGeneratorType order),
|
* The default layout stacks one strate of EVERY archetype (in ECaveGeneratorType order),
|
||||||
* so a single fixture exercises all eight density functions and their per-chunk caches,
|
* so a single fixture exercises all eight archetype stacks and their per-chunk caches,
|
||||||
* plus the gap-bedrock path when InterStrateGapChunks > 0.
|
* plus the gap-bedrock path when InterStrateGapChunks > 0.
|
||||||
*/
|
*/
|
||||||
struct FTestWorld
|
struct FTestWorld
|
||||||
@@ -73,8 +73,7 @@ namespace VoxelForgeTest
|
|||||||
* dangerous — and `VoxelForge.Determinism.LargeSeedSurvives` deliberately passes big ones
|
* dangerous — and `VoxelForge.Determinism.LargeSeedSurvives` deliberately passes big ones
|
||||||
* (up to 2e9) to prove it stays that way.
|
* (up to 2e9) to prove it stays that way.
|
||||||
*/
|
*/
|
||||||
void Build(int32 InSeed = 1337, int32 InGapChunks = 2, bool bUseOperatorStack = false,
|
void Build(int32 InSeed = 1337, int32 InGapChunks = 2, int32 InStrateHeightInChunks = 4)
|
||||||
int32 InStrateHeightInChunks = 4)
|
|
||||||
{
|
{
|
||||||
Settings = TStrongObjectPtr<UVoxelSettings>(
|
Settings = TStrongObjectPtr<UVoxelSettings>(
|
||||||
NewObject<UVoxelSettings>(GetTransientPackage(), NAME_None, RF_Transient));
|
NewObject<UVoxelSettings>(GetTransientPackage(), NAME_None, RF_Transient));
|
||||||
@@ -111,10 +110,6 @@ namespace VoxelForgeTest
|
|||||||
// refinement and roll tests use the smaller synthetic volume as a bounded memory
|
// refinement and roll tests use the smaller synthetic volume as a bounded memory
|
||||||
// control; the owner-facing showcase opts into 8 chunks below.
|
// control; the owner-facing showcase opts into 8 chunks below.
|
||||||
Def->StrateHeightInChunks = FMath::Max(1, InStrateHeightInChunks);
|
Def->StrateHeightInChunks = FMath::Max(1, InStrateHeightInChunks);
|
||||||
// L'OPT-IN de la pile d'opérateurs. Faux par défaut : les treize tests existants
|
|
||||||
// doivent continuer à exercer le `switch`, qui reste le comportement de référence.
|
|
||||||
// Seul le test de solidité de ClassifyTie côté pile le passe à vrai.
|
|
||||||
Def->bUseOperatorStack = bUseOperatorStack;
|
|
||||||
// Hard transitions: param blending across a boundary would make "which archetype
|
// Hard transitions: param blending across a boundary would make "which archetype
|
||||||
// owns this chunk" ambiguous, and these tests want an unambiguous mapping.
|
// owns this chunk" ambiguous, and these tests want an unambiguous mapping.
|
||||||
Def->TransitionType = EVoxelStrateTransition::Hard;
|
Def->TransitionType = EVoxelStrateTransition::Hard;
|
||||||
@@ -235,8 +230,8 @@ namespace VoxelForgeTest
|
|||||||
/**
|
/**
|
||||||
* A spread of world sample points that deliberately crosses chunk boundaries, strate
|
* A spread of world sample points that deliberately crosses chunk boundaries, strate
|
||||||
* boundaries and bedrock gaps — the exact conditions under which a per-chunk cache with a
|
* boundaries and bedrock gaps — the exact conditions under which a per-chunk cache with a
|
||||||
* missing key input produces a wrong answer. Integer XY on purpose: that is the branch
|
* missing key input produces a wrong answer. Integer XY on purpose: that is the lattice the
|
||||||
* GetDensityAt's T1.a column cache actually takes (fractional XY bypasses the cache).
|
* mesher samples, and the one the per-column memos are keyed on.
|
||||||
*/
|
*/
|
||||||
inline void BuildSamplePoints(const FTestWorld& World, int32 Count, int32 Seed,
|
inline void BuildSamplePoints(const FTestWorld& World, int32 Count, int32 Seed,
|
||||||
TArray<FVector>& OutPoints)
|
TArray<FVector>& OutPoints)
|
||||||
|
|||||||
@@ -154,72 +154,50 @@ bool FVoxelForgeTilePostReachProofTest::RunTest(const FString& Parameters)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
FTestWorld NativeWorld;
|
FTestWorld World;
|
||||||
NativeWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/false);
|
World.Build(/*Seed*/1337, /*GapChunks*/2);
|
||||||
FTestWorld FusedWorld;
|
if (!World.IsValid())
|
||||||
FusedWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/true);
|
|
||||||
if (!NativeWorld.IsValid() || !FusedWorld.IsValid())
|
|
||||||
{
|
{
|
||||||
AddError(NativeWorld.IsValid() && FusedWorld.IsValid()
|
AddError(World.WhyInvalid());
|
||||||
? TEXT("Unexpected invalid reach-proof fixture")
|
|
||||||
: (!NativeWorld.IsValid() ? NativeWorld.WhyInvalid() : FusedWorld.WhyInvalid()));
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
TArray<FIntVector> NativeOrigins;
|
TArray<FIntVector> Origins;
|
||||||
TArray<FIntVector> FusedOrigins;
|
AppendWorldReachTiles(World, Origins);
|
||||||
AppendWorldReachTiles(NativeWorld, NativeOrigins);
|
TestTrue(TEXT("the representative tile set is non-empty"), Origins.Num() > 0);
|
||||||
AppendWorldReachTiles(FusedWorld, FusedOrigins);
|
|
||||||
TestTrue(TEXT("the representative native tile set is non-empty"), NativeOrigins.Num() > 0);
|
|
||||||
TestTrue(TEXT("the representative fused tile set is non-empty"), FusedOrigins.Num() > 0);
|
|
||||||
|
|
||||||
TStrongObjectPtr<UVoxelMarchingCubesMesher> NativeMesher(
|
TStrongObjectPtr<UVoxelMarchingCubesMesher> Mesher(
|
||||||
NewObject<UVoxelMarchingCubesMesher>(GetTransientPackage(), NAME_None, RF_Transient));
|
NewObject<UVoxelMarchingCubesMesher>(GetTransientPackage(), NAME_None, RF_Transient));
|
||||||
TStrongObjectPtr<UVoxelMarchingCubesMesher> FusedMesher(
|
Mesher->SetGenerator(World.Generator.Get());
|
||||||
NewObject<UVoxelMarchingCubesMesher>(GetTransientPackage(), NAME_None, RF_Transient));
|
|
||||||
NativeMesher->SetGenerator(NativeWorld.Generator.Get());
|
|
||||||
FusedMesher->SetGenerator(FusedWorld.Generator.Get());
|
|
||||||
|
|
||||||
VoxelGenLOD::ResetTilePostReachDiagnostics();
|
VoxelGenLOD::ResetTilePostReachDiagnostics();
|
||||||
CVarGuard.SetScale(1.0f);
|
CVarGuard.SetScale(1.0f);
|
||||||
const FReachSnapshot NativeFull = RunReachTiles(*NativeMesher, NativeOrigins);
|
const FReachSnapshot Full = RunReachTiles(*Mesher, Origins);
|
||||||
const FReachSnapshot FusedFull = RunReachTiles(*FusedMesher, FusedOrigins);
|
|
||||||
const uint64 FullFinalComparisons = NativeFull.FinalComparisons + FusedFull.FinalComparisons;
|
|
||||||
const uint64 FullFinalDifferences = NativeFull.FinalDifferences + FusedFull.FinalDifferences;
|
|
||||||
TestTrue(TEXT("normal reach compares final fields on the representative tiles"),
|
TestTrue(TEXT("normal reach compares final fields on the representative tiles"),
|
||||||
FullFinalComparisons > 0);
|
Full.FinalComparisons > 0);
|
||||||
TestEqual(TEXT("normal reach has no final-field differences"), FullFinalDifferences, 0ull);
|
TestEqual(TEXT("normal reach has no final-field differences"), Full.FinalDifferences, 0ull);
|
||||||
TestEqual(TEXT("normal reach has no skipped-post differences"),
|
TestEqual(TEXT("normal reach has no skipped-post differences"), Full.AllDifferences, 0ull);
|
||||||
NativeFull.AllDifferences + FusedFull.AllDifferences, 0ull);
|
|
||||||
|
|
||||||
VoxelGenLOD::ResetTilePostReachDiagnostics();
|
VoxelGenLOD::ResetTilePostReachDiagnostics();
|
||||||
// Use a deliberately tiny but valid scale so at least one envelope is rejected while the same
|
// Use a deliberately tiny but valid scale so at least one envelope is rejected while the same
|
||||||
// final-field samples remain exercised; this is the negative control, not a supported setting.
|
// final-field samples remain exercised; this is the negative control, not a supported setting.
|
||||||
CVarGuard.SetScale(0.01f);
|
CVarGuard.SetScale(0.01f);
|
||||||
const FReachSnapshot NativeShrunk = RunReachTiles(*NativeMesher, NativeOrigins);
|
const FReachSnapshot Shrunk = RunReachTiles(*Mesher, Origins);
|
||||||
const FReachSnapshot FusedShrunk = RunReachTiles(*FusedMesher, FusedOrigins);
|
|
||||||
const uint64 ShrunkFinalComparisons =
|
|
||||||
NativeShrunk.FinalComparisons + FusedShrunk.FinalComparisons;
|
|
||||||
const uint64 ShrunkFinalDifferences =
|
|
||||||
NativeShrunk.FinalDifferences + FusedShrunk.FinalDifferences;
|
|
||||||
TestTrue(TEXT("shrunk reach still executes final-field comparisons"),
|
TestTrue(TEXT("shrunk reach still executes final-field comparisons"),
|
||||||
ShrunkFinalComparisons > 0);
|
Shrunk.FinalComparisons > 0);
|
||||||
TestTrue(TEXT("shrunk reach demonstrably fails the final-field proof"),
|
TestTrue(TEXT("shrunk reach demonstrably fails the final-field proof"),
|
||||||
ShrunkFinalDifferences > 0);
|
Shrunk.FinalDifferences > 0);
|
||||||
|
|
||||||
AddInfo(FString::Printf(
|
AddInfo(FString::Printf(
|
||||||
TEXT("tile_post_reach_proof: native_tiles=%llu fused_tiles=%llu full_final=%llu/%llu "
|
TEXT("tile_post_reach_proof: tiles=%llu full_final=%llu/%llu shrunk_final=%llu/%llu "
|
||||||
"shrunk_final=%llu/%llu native_samples=%lld/%lld fused_samples=%lld/%lld."),
|
"samples=%lld/%lld."),
|
||||||
static_cast<unsigned long long>(NativeFull.Tiles),
|
static_cast<unsigned long long>(Full.Tiles),
|
||||||
static_cast<unsigned long long>(FusedFull.Tiles),
|
static_cast<unsigned long long>(Full.FinalComparisons),
|
||||||
static_cast<unsigned long long>(FullFinalComparisons),
|
static_cast<unsigned long long>(Full.FinalDifferences),
|
||||||
static_cast<unsigned long long>(FullFinalDifferences),
|
static_cast<unsigned long long>(Shrunk.FinalComparisons),
|
||||||
static_cast<unsigned long long>(ShrunkFinalComparisons),
|
static_cast<unsigned long long>(Shrunk.FinalDifferences),
|
||||||
static_cast<unsigned long long>(ShrunkFinalDifferences),
|
static_cast<long long>(Full.DensitySamples),
|
||||||
static_cast<long long>(NativeFull.DensitySamples),
|
static_cast<long long>(Shrunk.DensitySamples)));
|
||||||
static_cast<long long>(NativeShrunk.DensitySamples),
|
|
||||||
static_cast<long long>(FusedFull.DensitySamples),
|
|
||||||
static_cast<long long>(FusedShrunk.DensitySamples)));
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,15 +29,13 @@ bool FVoxelForgeVerticalStrateReachTest::RunTest(const FString& Parameters)
|
|||||||
Clipmap.ClipRadius = 3;
|
Clipmap.ClipRadius = 3;
|
||||||
Clipmap.MaxClipLevel = 3;
|
Clipmap.MaxClipLevel = 3;
|
||||||
Clipmap.RenderDistanceChunks = 768;
|
Clipmap.RenderDistanceChunks = 768;
|
||||||
Clipmap.bFarSheetRing = true;
|
|
||||||
Clipmap.FarSheetSpanLevels = 2;
|
|
||||||
|
|
||||||
TArray<FVoxelTileKey> Desired;
|
TArray<FVoxelTileKey> Desired;
|
||||||
VoxelClipmapDesiredTiles::Build(
|
VoxelClipmapDesiredTiles::Build(
|
||||||
FIntVector(0, 0, PlayerChunkZ), MinChunkZ, MaxChunkZ, Clipmap, Desired);
|
FIntVector(0, 0, PlayerChunkZ), MinChunkZ, MaxChunkZ, Clipmap, Desired);
|
||||||
|
|
||||||
TArray<int32> CountsByLevel;
|
TArray<int32> CountsByLevel;
|
||||||
CountsByLevel.Init(0, 6);
|
CountsByLevel.Init(0, 4);
|
||||||
for (const FVoxelTileKey& Tile : Desired)
|
for (const FVoxelTileKey& Tile : Desired)
|
||||||
{
|
{
|
||||||
if (CountsByLevel.IsValidIndex(Tile.Level))
|
if (CountsByLevel.IsValidIndex(Tile.Level))
|
||||||
@@ -46,7 +44,9 @@ bool FVoxelForgeVerticalStrateReachTest::RunTest(const FString& Parameters)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const int32 ExpectedCounts[] = {343, 218, 178, 178, 178, 9586};
|
// Aligned rings (child-pair boundaries) below MaxClipLevel; the MaxClipLevel ring is stretched
|
||||||
|
// to the 768-chunk render distance (radius 96 at level 3) and clamped to the strate's Z band.
|
||||||
|
const int32 ExpectedCounts[] = {512, 320, 272, 372442};
|
||||||
for (int32 Level = 0; Level < static_cast<int32>(UE_ARRAY_COUNT(ExpectedCounts)); ++Level)
|
for (int32 Level = 0; Level < static_cast<int32>(UE_ARRAY_COUNT(ExpectedCounts)); ++Level)
|
||||||
{
|
{
|
||||||
TestEqual(FString::Printf(TEXT("desired tile count at level %d"), Level),
|
TestEqual(FString::Printf(TEXT("desired tile count at level %d"), Level),
|
||||||
@@ -63,26 +63,28 @@ bool FVoxelForgeVerticalStrateReachTest::RunTest(const FString& Parameters)
|
|||||||
|
|
||||||
TestTrue(TEXT("level 0 tile around the high pawn is present"),
|
TestTrue(TEXT("level 0 tile around the high pawn is present"),
|
||||||
HasTile(0, FIntVector(0, 0, 0)));
|
HasTile(0, FIntVector(0, 0, 0)));
|
||||||
TestTrue(TEXT("level 4 bridge tile covering the ground below the pawn is present"),
|
TestTrue(TEXT("level 3 tile covering the ground below the pawn is present"),
|
||||||
HasTile(4, FIntVector(0, 0, -3)));
|
HasTile(3, FIntVector(0, 0, -8)));
|
||||||
TestTrue(TEXT("adjacent level 4 ground tile is present"),
|
TestTrue(TEXT("adjacent level 3 ground tile is present"),
|
||||||
HasTile(4, FIntVector(1, 0, -3)));
|
HasTile(3, FIntVector(1, 0, -8)));
|
||||||
TestTrue(TEXT("outer level 5 sheet reaches the 768-chunk horizon"),
|
TestTrue(TEXT("the outer level 3 ring reaches the 768-chunk horizon"),
|
||||||
HasTile(5, FIntVector(24, 0, 0)));
|
HasTile(3, FIntVector(96, 0, 0)));
|
||||||
|
TestFalse(TEXT("the outer ring stops at the 768-chunk horizon"),
|
||||||
|
HasTile(3, FIntVector(97, 0, 0)));
|
||||||
TestFalse(TEXT("clamping does not request a full hidden LOD0 column"),
|
TestFalse(TEXT("clamping does not request a full hidden LOD0 column"),
|
||||||
HasTile(0, FIntVector(0, 0, -47)));
|
HasTile(0, FIntVector(0, 0, -47)));
|
||||||
|
|
||||||
for (int32 Level = 0; Level <= 5; ++Level)
|
for (int32 Level = 0; Level <= 3; ++Level)
|
||||||
{
|
{
|
||||||
TestTrue(FString::Printf(TEXT("clip level %d is non-empty"), Level),
|
TestTrue(FString::Printf(TEXT("clip level %d is non-empty"), Level),
|
||||||
CountsByLevel[Level] > 0);
|
CountsByLevel[Level] > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
AddInfo(FString::Printf(
|
AddInfo(FString::Printf(
|
||||||
TEXT("center_z=%d strate=[%d..%d] view=[%d..%d] levels=[%d,%d,%d,%d,%d,%d] total=%d"),
|
TEXT("center_z=%d strate=[%d..%d] view=[%d..%d] levels=[%d,%d,%d,%d] total=%d"),
|
||||||
PlayerChunkZ, StrateBottomZ, StrateTopZ, MinChunkZ, MaxChunkZ,
|
PlayerChunkZ, StrateBottomZ, StrateTopZ, MinChunkZ, MaxChunkZ,
|
||||||
CountsByLevel[0], CountsByLevel[1], CountsByLevel[2], CountsByLevel[3],
|
CountsByLevel[0], CountsByLevel[1], CountsByLevel[2], CountsByLevel[3],
|
||||||
CountsByLevel[4], CountsByLevel[5], Desired.Num()));
|
Desired.Num()));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,26 +67,20 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
|
|||||||
FailureKinds += Kind;
|
FailureKinds += Kind;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The ordinary fixture deliberately uses the requested default, so existing op-stack and
|
// The ordinary fixture deliberately uses the requested default, so the other tests exercise
|
||||||
// legacy tests exercise the setting without having to know about the new post.
|
// the setting without having to know about the new post.
|
||||||
FTestWorld Legacy;
|
FTestWorld Bounded;
|
||||||
Legacy.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/false);
|
Bounded.Build(/*Seed*/1337, /*GapChunks*/2);
|
||||||
FTestWorld StackWorld;
|
|
||||||
StackWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/true);
|
|
||||||
FTestWorld Unbounded;
|
FTestWorld Unbounded;
|
||||||
Unbounded.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/false);
|
Unbounded.Build(/*Seed*/1337, /*GapChunks*/2);
|
||||||
|
|
||||||
if (!Legacy.IsValid() || !StackWorld.IsValid() || !Unbounded.IsValid())
|
if (!Bounded.IsValid() || !Unbounded.IsValid())
|
||||||
{
|
{
|
||||||
AddError(Legacy.IsValid() && StackWorld.IsValid() && Unbounded.IsValid()
|
AddError(!Bounded.IsValid() ? Bounded.WhyInvalid() : Unbounded.WhyInvalid());
|
||||||
? TEXT("Unexpected edge-seal fixture state")
|
|
||||||
: (!Legacy.IsValid() ? Legacy.WhyInvalid()
|
|
||||||
: (!StackWorld.IsValid() ? StackWorld.WhyInvalid() : Unbounded.WhyInvalid())));
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
SetEdgeSettings(Legacy, TestWorldRadius);
|
SetEdgeSettings(Bounded, TestWorldRadius);
|
||||||
SetEdgeSettings(StackWorld, TestWorldRadius);
|
|
||||||
SetEdgeSettings(Unbounded, 0.0f);
|
SetEdgeSettings(Unbounded, 0.0f);
|
||||||
|
|
||||||
//==========================================================================
|
//==========================================================================
|
||||||
@@ -103,12 +97,12 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
|
|||||||
for (int32 Slot = 0; Slot < 8; ++Slot)
|
for (int32 Slot = 0; Slot < 8; ++Slot)
|
||||||
{
|
{
|
||||||
int32 TopZ = 0, BottomZ = 0;
|
int32 TopZ = 0, BottomZ = 0;
|
||||||
if (Legacy.GetSlotVoxelZRange(Slot, TopZ, BottomZ))
|
if (Bounded.GetSlotVoxelZRange(Slot, TopZ, BottomZ))
|
||||||
{
|
{
|
||||||
StrateZ.Add((float)((TopZ + BottomZ) / 2));
|
StrateZ.Add((float)((TopZ + BottomZ) / 2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
StrateZ.Add(Legacy.MidVoxelZ());
|
StrateZ.Add(Bounded.MidVoxelZ());
|
||||||
|
|
||||||
int32 NumPositiveSamples = 0;
|
int32 NumPositiveSamples = 0;
|
||||||
for (const FVector& XY : FarXY)
|
for (const FVector& XY : FarXY)
|
||||||
@@ -117,8 +111,7 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
|
|||||||
{
|
{
|
||||||
const FVector P(XY.X, XY.Y, Z);
|
const FVector P(XY.X, XY.Y, Z);
|
||||||
++NumPositiveSamples;
|
++NumPositiveSamples;
|
||||||
if (!IsSolidMC(*Legacy.Generator, P)) Fail(TEXT("positive-legacy"));
|
if (!IsSolidMC(*Bounded.Generator, P)) Fail(TEXT("positive"));
|
||||||
if (!IsSolidMC(*StackWorld.Generator, P)) Fail(TEXT("positive-opstack"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +131,7 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
|
|||||||
for (const float Z : StrateZ)
|
for (const float Z : StrateZ)
|
||||||
{
|
{
|
||||||
const FVector P(XY.X, XY.Y, Z);
|
const FVector P(XY.X, XY.Y, Z);
|
||||||
const float BoundedD = Legacy.Generator->GetDensityAt(P.X, P.Y, P.Z);
|
const float BoundedD = Bounded.Generator->GetDensityAt(P.X, P.Y, P.Z);
|
||||||
const float UnboundedD = Unbounded.Generator->GetDensityAt(P.X, P.Y, P.Z);
|
const float UnboundedD = Unbounded.Generator->GetDensityAt(P.X, P.Y, P.Z);
|
||||||
++NumInteriorComparisons;
|
++NumInteriorComparisons;
|
||||||
if (!BitEqual(BoundedD, UnboundedD)) Fail(TEXT("interior-not-no-op"));
|
if (!BitEqual(BoundedD, UnboundedD)) Fail(TEXT("interior-not-no-op"));
|
||||||
@@ -193,7 +186,7 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
|
|||||||
// CLASSIFYBOX — force boxes in the ramp and beyond the radius, then brute-force every voxel.
|
// CLASSIFYBOX — force boxes in the ramp and beyond the radius, then brute-force every voxel.
|
||||||
//==========================================================================
|
//==========================================================================
|
||||||
int32 TopShaftZ = 0, BottomShaftZ = 0;
|
int32 TopShaftZ = 0, BottomShaftZ = 0;
|
||||||
if (!StackWorld.GetSlotVoxelZRange(FTestWorld::SlotVerticalShafts, TopShaftZ, BottomShaftZ))
|
if (!Bounded.GetSlotVoxelZRange(FTestWorld::SlotVerticalShafts, TopShaftZ, BottomShaftZ))
|
||||||
{
|
{
|
||||||
Fail(TEXT("missing-shaft-slot"));
|
Fail(TEXT("missing-shaft-slot"));
|
||||||
}
|
}
|
||||||
@@ -218,17 +211,17 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
|
|||||||
};
|
};
|
||||||
|
|
||||||
const int32 MidChunkZ = FMath::FloorToInt((BoxZ0 + BoxZ1) * 0.5f / CHUNK_SIZE);
|
const int32 MidChunkZ = FMath::FloorToInt((BoxZ0 + BoxZ1) * 0.5f / CHUNK_SIZE);
|
||||||
const FVerticalShaftParams ShaftParams = StackWorld.StrateManager->GetVerticalShaftParamsForChunk(
|
const FVerticalShaftParams ShaftParams = Bounded.StrateManager->GetVerticalShaftParamsForChunk(
|
||||||
FIntVector(0, 0, MidChunkZ));
|
FIntVector(0, 0, MidChunkZ));
|
||||||
FVoxelOpStack Stack;
|
FVoxelOpStack Stack;
|
||||||
VoxelDensityOps::BuildVerticalShaftStack(
|
VoxelDensityOps::BuildVerticalShaftStack(
|
||||||
Stack, ShaftParams, StackWorld.Settings->Seed,
|
Stack, ShaftParams, Bounded.Settings->Seed,
|
||||||
StackWorld.Generator->OriginSpineRadius, StackWorld.StrateManager.Get());
|
Bounded.Generator->OriginSpineRadius, Bounded.StrateManager.Get());
|
||||||
|
|
||||||
FVoxelOpContext Ctx;
|
FVoxelOpContext Ctx;
|
||||||
Ctx.ChunkCoord = FIntVector(0, 0, MidChunkZ);
|
Ctx.ChunkCoord = FIntVector(0, 0, MidChunkZ);
|
||||||
Ctx.Seed = (uint32)StackWorld.Settings->Seed;
|
Ctx.Seed = (uint32)Bounded.Settings->Seed;
|
||||||
Ctx.LayoutVersion = StackWorld.StrateManager->GetLayoutVersion();
|
Ctx.LayoutVersion = Bounded.StrateManager->GetLayoutVersion();
|
||||||
Ctx.StrateTopWorldZ = ShaftParams.StrateTopWorldZ;
|
Ctx.StrateTopWorldZ = ShaftParams.StrateTopWorldZ;
|
||||||
Ctx.StrateBottomWorldZ = ShaftParams.StrateBottomWorldZ;
|
Ctx.StrateBottomWorldZ = ShaftParams.StrateBottomWorldZ;
|
||||||
Ctx.WorldRadiusVoxels = TestWorldRadius;
|
Ctx.WorldRadiusVoxels = TestWorldRadius;
|
||||||
@@ -255,14 +248,14 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
|
|||||||
for (int32 X = X0; X <= X1; ++X)
|
for (int32 X = X0; X <= X1; ++X)
|
||||||
{
|
{
|
||||||
++NumVoxelsChecked;
|
++NumVoxelsChecked;
|
||||||
if (!IsSolidMC(*StackWorld.Generator, FVector((float)X, (float)Y, (float)Z)))
|
if (!IsSolidMC(*Bounded.Generator, FVector((float)X, (float)Y, (float)Z)))
|
||||||
Fail(TEXT("classifybox-false-allsolid"));
|
Fail(TEXT("classifybox-false-allsolid"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The T1.d path must be able to skip an edge tile without knowing which Z category it hits.
|
// The T1.d path must be able to skip an edge tile without knowing which Z category it hits.
|
||||||
const EVoxelTileClass EdgeTile = StackWorld.Generator->ClassifyTile(
|
const EVoxelTileClass EdgeTile = Bounded.Generator->ClassifyTile(
|
||||||
FIntVector(320, 0, BottomShaftZ + 8), 1, 8);
|
FIntVector(320, 0, BottomShaftZ + 8), 1, 8);
|
||||||
int32 NumTilesProved = (EdgeTile == EVoxelTileClass::AllSolid) ? 1 : 0;
|
int32 NumTilesProved = (EdgeTile == EVoxelTileClass::AllSolid) ? 1 : 0;
|
||||||
if (EdgeTile != EVoxelTileClass::AllSolid) Fail(TEXT("classifytile-missed-edge"));
|
if (EdgeTile != EVoxelTileClass::AllSolid) Fail(TEXT("classifytile-missed-edge"));
|
||||||
@@ -283,9 +276,7 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
|
|||||||
// coverage only: the synthetic helper case above is the hard assertion even if landing-site
|
// coverage only: the synthetic helper case above is the hard assertion even if landing-site
|
||||||
// adjustment moves every generated mouth out of this small test rim.
|
// adjustment moves every generated mouth out of this small test rim.
|
||||||
FTestWorld GeneratedPassageWorld;
|
FTestWorld GeneratedPassageWorld;
|
||||||
GeneratedPassageWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/false);
|
GeneratedPassageWorld.Build(/*Seed*/1337, /*GapChunks*/2);
|
||||||
FTestWorld GeneratedPassageStackWorld;
|
|
||||||
GeneratedPassageStackWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/true);
|
|
||||||
auto CheckGeneratedPassages = [&](FTestWorld& World, const TCHAR* FailureKind)
|
auto CheckGeneratedPassages = [&](FTestWorld& World, const TCHAR* FailureKind)
|
||||||
{
|
{
|
||||||
if (!World.IsValid())
|
if (!World.IsValid())
|
||||||
@@ -310,7 +301,6 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
CheckGeneratedPassages(GeneratedPassageWorld, TEXT("generated-passage-breaches-rim"));
|
CheckGeneratedPassages(GeneratedPassageWorld, TEXT("generated-passage-breaches-rim"));
|
||||||
CheckGeneratedPassages(GeneratedPassageStackWorld, TEXT("generated-opstack-passage-breaches-rim"));
|
|
||||||
|
|
||||||
// One sorted summary block; individual samples intentionally never log.
|
// One sorted summary block; individual samples intentionally never log.
|
||||||
AddInfo(FString::Printf(
|
AddInfo(FString::Printf(
|
||||||
@@ -319,7 +309,7 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
|
|||||||
TEXT(" classify_tile_proved=%d\n")
|
TEXT(" classify_tile_proved=%d\n")
|
||||||
TEXT(" interior_comparisons=%d\n")
|
TEXT(" interior_comparisons=%d\n")
|
||||||
TEXT(" near_rim_passage_checks=%d\n")
|
TEXT(" near_rim_passage_checks=%d\n")
|
||||||
TEXT(" positive_sample_points=%d (checked in both paths)\n")
|
TEXT(" positive_sample_points=%d\n")
|
||||||
TEXT(" rim_directions=%d\n")
|
TEXT(" rim_directions=%d\n")
|
||||||
TEXT(" rim_max_adjacent_solidness_jump=%.6f (threshold=%.6f)\n")
|
TEXT(" rim_max_adjacent_solidness_jump=%.6f (threshold=%.6f)\n")
|
||||||
TEXT(" voxels_checked=%lld\n")
|
TEXT(" voxels_checked=%lld\n")
|
||||||
|
|||||||
@@ -43,11 +43,11 @@ void UVoxelAtmosphereManager::UpdateForPlayer(const FVector& PlayerWorldPos)
|
|||||||
if (!O) return;
|
if (!O) return;
|
||||||
|
|
||||||
// Le champ de densite est en espace ACTEUR : les requetes de strate et de biome veulent des
|
// Le champ de densite est en espace ACTEUR : les requetes de strate et de biome veulent des
|
||||||
// coordonnees LOCALES. Le placement d acteurs, lui, veut du MONDE. Les deux cohabitent ici, ce
|
// coordonnees LOCALES. Le placement d acteurs, lui, veut du MONDE. Les deux cohabitent ici : ne
|
||||||
// qui est exactement pourquoi ce fichier supposait "acteur a l origine" avant le 2026-08-17.
|
// pas supposer "acteur a l origine".
|
||||||
// The density field is in ACTOR space: strate and biome queries want LOCAL coordinates, while
|
// The density field is in ACTOR space: strate and biome queries want LOCAL coordinates, while
|
||||||
// actor placement wants WORLD. Both live in this function - which is exactly why it used to
|
// actor placement wants WORLD. Both live in this function - do not assume "actor at origin /
|
||||||
// assume "actor at origin / identity".
|
// identity".
|
||||||
const FTransform Xf = O->GetActorTransform();
|
const FTransform Xf = O->GetActorTransform();
|
||||||
const FVector LocalPos = Xf.InverseTransformPosition(PlayerWorldPos);
|
const FVector LocalPos = Xf.InverseTransformPosition(PlayerWorldPos);
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ namespace
|
|||||||
// Density coordinates and cached cull radii are float-valued. FVector is double-valued in
|
// Density coordinates and cached cull radii are float-valued. FVector is double-valued in
|
||||||
// UE5, so using FVector::DistSquared here needlessly widens every candidate reject. Keep the
|
// UE5, so using FVector::DistSquared here needlessly widens every candidate reject. Keep the
|
||||||
// cull in the same build-fixed float domain as the density query; the exact field output is
|
// cull in the same build-fixed float domain as the density query; the exact field output is
|
||||||
// intentionally no longer required to match the pre-optimization double round-trip.
|
// intentionally not required to match a double-precision round-trip.
|
||||||
FORCEINLINE float VF_FloatDistSquared(
|
FORCEINLINE float VF_FloatDistSquared(
|
||||||
float X, float Y, float Z, const FVector& Center)
|
float X, float Y, float Z, const FVector& Center)
|
||||||
{
|
{
|
||||||
@@ -81,7 +81,6 @@ namespace
|
|||||||
struct FVFTunnelShapeEvaluation
|
struct FVFTunnelShapeEvaluation
|
||||||
{
|
{
|
||||||
float SDF = FLT_MAX;
|
float SDF = FLT_MAX;
|
||||||
bool bSupportFloor = false;
|
|
||||||
bool bHasSweptFloor = false;
|
bool bHasSweptFloor = false;
|
||||||
float SweptFloorZ = -FLT_MAX;
|
float SweptFloorZ = -FLT_MAX;
|
||||||
float SweptFloorRadius = 0.0f;
|
float SweptFloorRadius = 0.0f;
|
||||||
@@ -548,7 +547,6 @@ namespace
|
|||||||
Profile.NaturalStartFloorZ = FloorA;
|
Profile.NaturalStartFloorZ = FloorA;
|
||||||
Profile.NaturalEndFloorZ = FloorB;
|
Profile.NaturalEndFloorZ = FloorB;
|
||||||
Profile.bLedgeTransition = false;
|
Profile.bLedgeTransition = false;
|
||||||
Profile.NumSteps = 0;
|
|
||||||
|
|
||||||
if (bHasLedgeLevels)
|
if (bHasLedgeLevels)
|
||||||
{
|
{
|
||||||
@@ -934,27 +932,6 @@ namespace
|
|||||||
return FloorZ;
|
return FloorZ;
|
||||||
}
|
}
|
||||||
|
|
||||||
FORCEINLINE float VF_TunnelFloorAtSegment(
|
|
||||||
const FVector& A, float RadiusA,
|
|
||||||
const FVector& B, float RadiusB,
|
|
||||||
float T, int32 SegmentIndex, int32 NumSegments,
|
|
||||||
const FVector& Position,
|
|
||||||
const FCachedTunnel& Tunnel,
|
|
||||||
const void* CacheIdentity, int32 TunnelIndex,
|
|
||||||
bool bEvaluateRelief,
|
|
||||||
const FVFFloorReliefColumnKey& Column,
|
|
||||||
const FTunnelFloorSegmentProfile* FloorProfile,
|
|
||||||
const FVFRoomFloorOwnership* RoomFloorOwnership)
|
|
||||||
{
|
|
||||||
const FVFTunnelFloorTerms Terms = VF_TunnelFloorTerms(
|
|
||||||
A, RadiusA, B, RadiusB,
|
|
||||||
T, SegmentIndex, NumSegments, Tunnel, FloorProfile,
|
|
||||||
RoomFloorOwnership);
|
|
||||||
return VF_TunnelFloorFromTerms(
|
|
||||||
Position, Tunnel, Terms,
|
|
||||||
CacheIdentity, TunnelIndex, bEvaluateRelief, Column);
|
|
||||||
}
|
|
||||||
|
|
||||||
FORCEINLINE bool VF_ProjectTunnelSegmentXY(
|
FORCEINLINE bool VF_ProjectTunnelSegmentXY(
|
||||||
const FVector& Position, const FVector& A, const FVector& B,
|
const FVector& Position, const FVector& A, const FVector& B,
|
||||||
float& OutT, float& OutDistanceSquared)
|
float& OutT, float& OutDistanceSquared)
|
||||||
@@ -1229,76 +1206,6 @@ namespace
|
|||||||
RoomFloorOwnership);
|
RoomFloorOwnership);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool VF_ProjectSweptTunnelFloor(
|
|
||||||
const FVector& Position,
|
|
||||||
const FCachedTunnel& Tunnel,
|
|
||||||
bool bWorldChain,
|
|
||||||
float& OutFloorZ,
|
|
||||||
float& OutSupportRadius,
|
|
||||||
const void* CacheIdentity, int32 TunnelIndex,
|
|
||||||
const FVFRoomFloorOwnership* RoomFloorOwnership = nullptr)
|
|
||||||
{
|
|
||||||
const FVFFloorReliefColumnKey ReliefColumn =
|
|
||||||
VF_MakeFloorReliefColumnKey(
|
|
||||||
static_cast<float>(Position.X), static_cast<float>(Position.Y));
|
|
||||||
const TArray<FVector>& ControlPoints = bWorldChain
|
|
||||||
? Tunnel.WorldControlPoints : Tunnel.ControlPoints;
|
|
||||||
const TArray<float>& ControlRadii = bWorldChain
|
|
||||||
? Tunnel.WorldControlRadii : Tunnel.ControlRadii;
|
|
||||||
if (ControlPoints.Num() < 2
|
|
||||||
|| ControlRadii.Num() != ControlPoints.Num())
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int32 ControlPointCount = ControlPoints.Num();
|
|
||||||
const int32 NumSegments = ControlPointCount - 1;
|
|
||||||
const FVector* ControlPointData = ControlPoints.GetData();
|
|
||||||
const float* ControlRadiusData = ControlRadii.GetData();
|
|
||||||
const TArray<FTunnelFloorSegmentProfile>* FloorProfiles = bWorldChain
|
|
||||||
? &Tunnel.WorldFloorProfiles : &Tunnel.FloorProfiles;
|
|
||||||
const FTunnelFloorSegmentProfile* FloorProfileData =
|
|
||||||
FloorProfiles->Num() == NumSegments ? FloorProfiles->GetData() : nullptr;
|
|
||||||
float BestDistanceSquared = FLT_MAX;
|
|
||||||
bool bFound = false;
|
|
||||||
for (int32 SegmentIndex = 0;
|
|
||||||
SegmentIndex < NumSegments;
|
|
||||||
++SegmentIndex)
|
|
||||||
{
|
|
||||||
float T = 0.0f;
|
|
||||||
float DistanceSquared = FLT_MAX;
|
|
||||||
if (!VF_ProjectTunnelSegmentXY(
|
|
||||||
Position,
|
|
||||||
ControlPointData[SegmentIndex], ControlPointData[SegmentIndex + 1],
|
|
||||||
T, DistanceSquared)
|
|
||||||
|| DistanceSquared >= BestDistanceSquared)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const float RadiusA = ControlRadiusData[SegmentIndex];
|
|
||||||
const float RadiusB = ControlRadiusData[SegmentIndex + 1];
|
|
||||||
OutFloorZ = VF_TunnelFloorAtSegment(
|
|
||||||
ControlPointData[SegmentIndex], RadiusA,
|
|
||||||
ControlPointData[SegmentIndex + 1], RadiusB,
|
|
||||||
T, SegmentIndex, NumSegments, Position, Tunnel,
|
|
||||||
CacheIdentity, TunnelIndex, /*bEvaluateRelief=*/true,
|
|
||||||
ReliefColumn,
|
|
||||||
FloorProfileData != nullptr ? FloorProfileData + SegmentIndex : nullptr,
|
|
||||||
RoomFloorOwnership);
|
|
||||||
OutSupportRadius = FMath::Max(
|
|
||||||
FMath::Min(FMath::Abs(RadiusA), FMath::Abs(RadiusB)) - 0.5f,
|
|
||||||
VoxelPassageGeometry::PlayerRadiusVoxels);
|
|
||||||
BestDistanceSquared = DistanceSquared;
|
|
||||||
bFound = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return bFound
|
|
||||||
&& VoxelMath::IsFinite(OutFloorZ)
|
|
||||||
&& VoxelMath::IsFinite(OutSupportRadius)
|
|
||||||
&& BestDistanceSquared <= FMath::Square(OutSupportRadius);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void FChunkSDFSpatialIndex::Reset()
|
void FChunkSDFSpatialIndex::Reset()
|
||||||
@@ -1449,7 +1356,7 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Never turn a malformed authored bound into a giant allocation. A failed build is
|
// Never turn a malformed authored bound into a giant allocation. A failed build is
|
||||||
// deliberately indistinguishable from the old path to the density evaluator.
|
// deliberately indistinguishable from the unindexed linear scan to the density evaluator.
|
||||||
constexpr int64 MaxBuckets = 1024 * 1024;
|
constexpr int64 MaxBuckets = 1024 * 1024;
|
||||||
constexpr int64 MaxReferences = 4 * 1024 * 1024;
|
constexpr int64 MaxReferences = 4 * 1024 * 1024;
|
||||||
TArray<int32> ItemMinX;
|
TArray<int32> ItemMinX;
|
||||||
@@ -3719,11 +3626,11 @@ namespace
|
|||||||
// Once the lower and upper ends bracket the room, the authored room/floor field is
|
// Once the lower and upper ends bracket the room, the authored room/floor field is
|
||||||
// monotone through this narrow vertical landing band for the finite parameter envelope
|
// monotone through this narrow vertical landing band for the finite parameter envelope
|
||||||
// used by the generator. Find the one-voxel bucket with a logarithmic search, then run
|
// used by the generator. Find the one-voxel bucket with a logarithmic search, then run
|
||||||
// the exact legacy bisection in that bucket. The latter is kept byte-for-byte in its
|
// the exact bisection in that bucket. The latter is kept byte-for-byte in its arithmetic
|
||||||
// arithmetic and iteration count so the endpoint remains identical to the old scan.
|
// and iteration count so the endpoint is identical to the one-voxel walk below.
|
||||||
// If the bracket is not monotone (or the local bucket check cannot reproduce the crossing),
|
// If the bracket is not monotone (or the local bucket check cannot reproduce the crossing),
|
||||||
// fall back to the original one-voxel walk below; malformed/unsupported inputs therefore
|
// fall back to that one-voxel walk; malformed/unsupported inputs therefore keep its
|
||||||
// retain the old behavior.
|
// result.
|
||||||
auto BisectBracket = [&](float BracketLow, float BracketHigh,
|
auto BisectBracket = [&](float BracketLow, float BracketHigh,
|
||||||
float BracketLowValue) -> bool
|
float BracketLowValue) -> bool
|
||||||
{
|
{
|
||||||
@@ -5396,9 +5303,9 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The slab source is an XY height band. Evaluate the same two pure height fields as
|
// The slab source is an XY height band. Evaluate the same two pure height fields as
|
||||||
// GetSlabDensity, then choose their midpoint after intersecting the seal-free interior.
|
// FSlabVoidSource, then choose their midpoint after intersecting the seal-free interior.
|
||||||
// La source slab est une bande de hauteurs XY : on recalcule les deux champs purs comme
|
// La source slab est une bande de hauteurs XY : on recalcule les deux champs purs comme
|
||||||
// GetSlabDensity, puis on prend leur milieu après intersection avec l'intérieur sans seal.
|
// FSlabVoidSource, puis on prend leur milieu après intersection avec l'intérieur sans seal.
|
||||||
bool VF_SuggestSlabLandingPoint(
|
bool VF_SuggestSlabLandingPoint(
|
||||||
const FSlabGenerationParams& Params,
|
const FSlabGenerationParams& Params,
|
||||||
int32 Seed,
|
int32 Seed,
|
||||||
@@ -5537,7 +5444,8 @@ namespace
|
|||||||
|
|
||||||
// Maze corridors are lattice edges. Only a horizontal edge is a useful landing source: a
|
// Maze corridors are lattice edges. Only a horizontal edge is a useful landing source: a
|
||||||
// vertical edge can prove air, but cannot prove a place to stand. The edge set and Z levels
|
// vertical edge can prove air, but cannot prove a place to stand. The edge set and Z levels
|
||||||
// below use the same origin-directed tree + loop contract as GetMazeDensity, with no cache.
|
// below use the same origin-directed tree + loop contract as FLatticeCorridorSource, with no
|
||||||
|
// cache.
|
||||||
bool VF_SuggestMazeLandingPoint(
|
bool VF_SuggestMazeLandingPoint(
|
||||||
const FMazeGenerationParams& Params,
|
const FMazeGenerationParams& Params,
|
||||||
int32 Seed,
|
int32 Seed,
|
||||||
@@ -5958,14 +5866,6 @@ namespace
|
|||||||
SampleShaft,
|
SampleShaft,
|
||||||
OutPoint))
|
OutPoint))
|
||||||
{
|
{
|
||||||
if (Seed == 14 && FMath::IsNearlyEqual(StrateBottomZ, -736.0f))
|
|
||||||
{
|
|
||||||
UE_LOG(LogTemp, Display,
|
|
||||||
TEXT("[VoxelForgeExplore][TemporaryVerticalLanding] axis=(%.3f,%.3f) "
|
|
||||||
"safeRadius=%.3f candidate=(%.3f,%.3f,%.3f)"),
|
|
||||||
BestAxisX, BestAxisY, BestSafeRadius,
|
|
||||||
CandidateFeet.X, CandidateFeet.Y, CandidateFeet.Z);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5980,7 +5880,7 @@ namespace
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Evaluate only the island source, in the same internal-density convention as
|
// Evaluate only the island source, in the same internal-density convention as
|
||||||
// GetFloatingIslandDensity (positive = solid, negative = void). This is intentionally a
|
// FIslandBlobSource (positive = solid, negative = void). This is intentionally a
|
||||||
// local pure evaluator: it lets the query bracket the actual blob top instead of treating a
|
// local pure evaluator: it lets the query bracket the actual blob top instead of treating a
|
||||||
// nominal island centre as footing. Full-octave noise is used because this query has no LOD
|
// nominal island centre as footing. Full-octave noise is used because this query has no LOD
|
||||||
// state by design and GeneratePassages runs before a tile's LOD is selected.
|
// state by design and GeneratePassages runs before a tile's LOD is selected.
|
||||||
@@ -6017,7 +5917,7 @@ namespace
|
|||||||
for (int32 IslandIndex = 0; IslandIndex < NumIslands; ++IslandIndex)
|
for (int32 IslandIndex = 0; IslandIndex < NumIslands; ++IslandIndex)
|
||||||
{
|
{
|
||||||
const FVFIslandSite& Island = Islands[IslandIndex];
|
const FVFIslandSite& Island = Islands[IslandIndex];
|
||||||
// Production GetFloatingIslandDensity evaluates exactly this 3x3 cell neighbourhood
|
// Production FIslandBlobSource evaluates exactly this 3x3 cell neighbourhood
|
||||||
// for the queried XY. The outer search may collect more candidates so a lateral snap
|
// for the queried XY. The outer search may collect more candidates so a lateral snap
|
||||||
// can be found, but those distant blobs must not influence the returned footing.
|
// can be found, but those distant blobs must not influence the returned footing.
|
||||||
if (FMath::Abs(Island.CellX - BaseCellX) > 1
|
if (FMath::Abs(Island.CellX - BaseCellX) > 1
|
||||||
@@ -7221,14 +7121,14 @@ void VoxelCaveMorphology::BuildChunkCache(
|
|||||||
// (Frontier rooms with no closer candidate in reach fall back to plain NN — a far
|
// (Frontier rooms with no closer candidate in reach fall back to plain NN — a far
|
||||||
// cluster stays internally chained even when it can't bridge to the origin side.)
|
// cluster stays internally chained even when it can't bridge to the origin side.)
|
||||||
//
|
//
|
||||||
// bTunnelsFlowTowardOrigin = false (legacy): plain nearest-neighbor pairing. NOTE:
|
// bTunnelsFlowTowardOrigin = false (legacy): plain nearest-neighbor pairing. NOTE: an
|
||||||
// despite what this comment used to claim, an NN-graph is a FOREST of small clusters,
|
// NN-graph is a FOREST of small clusters, not a connected tree — isolated cave pockets
|
||||||
// not a connected tree — isolated cave pockets are expected in this mode.
|
// are expected in this mode.
|
||||||
//
|
//
|
||||||
// Selection metric (not the reach filter) penalizes vertical separation via
|
// Selection metric (not the reach filter) penalizes vertical separation via
|
||||||
// TunnelHorizontalBias, so the GUARANTEED links also prefer walkable slopes —
|
// TunnelHorizontalBias, so the GUARANTEED links also prefer walkable slopes —
|
||||||
// previously only the random TunnelDensity extras were biased, which is why
|
// biasing only the random TunnelDensity extras lets backbone tunnels come out
|
||||||
// backbone tunnels could come out absurdly steep.
|
// absurdly steep.
|
||||||
TArray<int32, TInlineAllocator<64>> NearestNeighbor;
|
TArray<int32, TInlineAllocator<64>> NearestNeighbor;
|
||||||
NearestNeighbor.SetNumUninitialized(NumRooms);
|
NearestNeighbor.SetNumUninitialized(NumRooms);
|
||||||
|
|
||||||
@@ -8367,7 +8267,7 @@ void VoxelCaveMorphology::BuildChunkCache(
|
|||||||
MaxExtent += FloorReliefEnvelope + BlendEnvelope * 3.0f;
|
MaxExtent += FloorReliefEnvelope + BlendEnvelope * 3.0f;
|
||||||
|
|
||||||
// --- PRE-BAKED SHAPE ---
|
// --- PRE-BAKED SHAPE ---
|
||||||
// Same hash roll + thresholds + capsule trig the evaluator used to redo PER VOXEL;
|
// Same hash roll + thresholds + capsule trig the evaluator would otherwise redo PER VOXEL;
|
||||||
// done once here → EvaluateSDFCached just switches on ShapeType. Bit-identical output.
|
// done once here → EvaluateSDFCached just switches on ShapeType. Bit-identical output.
|
||||||
{
|
{
|
||||||
const uint32 ShapeHash = VoxelHash::Mix(CR.Hash ^ 0xDEADBEEFu);
|
const uint32 ShapeHash = VoxelHash::Mix(CR.Hash ^ 0xDEADBEEFu);
|
||||||
@@ -8859,7 +8759,7 @@ float VoxelCaveMorphology::EvaluateSDFCached(
|
|||||||
// A room owns every point inside its authored body. The short mouth ownership fade below
|
// A room owns every point inside its authored body. The short mouth ownership fade below
|
||||||
// handles the fillet outside the wall; this broader test is the hard A > B rule for the room
|
// handles the fillet outside the wall; this broader test is the hard A > B rule for the room
|
||||||
// interior. The exact raw shape sign was captured during the room SDF pass above, so this
|
// interior. The exact raw shape sign was captured during the room SDF pass above, so this
|
||||||
// point no longer performs a second spatial-index traversal.
|
// point does not perform a second spatial-index traversal.
|
||||||
auto EvaluateTunnel = [&](int32 TunnelIdx)
|
auto EvaluateTunnel = [&](int32 TunnelIdx)
|
||||||
{
|
{
|
||||||
const FCachedTunnel& Tunnel = TunnelData[TunnelIdx];
|
const FCachedTunnel& Tunnel = TunnelData[TunnelIdx];
|
||||||
@@ -8960,50 +8860,6 @@ float VoxelCaveMorphology::EvaluateSDFCached(
|
|||||||
return MinSDF;
|
return MinSDF;
|
||||||
}
|
}
|
||||||
|
|
||||||
float VoxelCaveMorphology::EvaluateTunnelCoreSDF(
|
|
||||||
float WorldX, float WorldY, float WorldZ,
|
|
||||||
const FChunkSDFCache& Cache,
|
|
||||||
bool bUseSpatialIndex)
|
|
||||||
{
|
|
||||||
if (VoxelDensityAblation::IsTunnelCoreOff())
|
|
||||||
{
|
|
||||||
return FLT_MAX;
|
|
||||||
}
|
|
||||||
const FVector Pos(WorldX, WorldY, WorldZ);
|
|
||||||
float MinSDF = FLT_MAX;
|
|
||||||
const FCachedTunnel* TunnelData = Cache.Tunnels.GetData();
|
|
||||||
|
|
||||||
auto EvaluateTunnel = [&](int32 TunnelIdx)
|
|
||||||
{
|
|
||||||
const FCachedTunnel& Tunnel = TunnelData[TunnelIdx];
|
|
||||||
if (Tunnel.SDFInfluenceRadius > 0.0f
|
|
||||||
&& VF_DistanceSquaredToAabb(
|
|
||||||
WorldX, WorldY, WorldZ,
|
|
||||||
Tunnel.SDFCenterlineMin, Tunnel.SDFCenterlineMax)
|
|
||||||
> FMath::Square(Tunnel.SDFInfluenceRadius))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (VF_FloatDistSquared(WorldX, WorldY, WorldZ, Tunnel.BoundCenter)
|
|
||||||
> Tunnel.BoundRadiusSq)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MinSDF = FMath::Min(
|
|
||||||
MinSDF,
|
|
||||||
VF_EvaluateSweptTunnel(
|
|
||||||
Pos, Tunnel, /*bWorldChain=*/false, Cache.SDFBlendRadius,
|
|
||||||
/*bApplyFloorCut=*/false, &Cache, TunnelIdx).SDF);
|
|
||||||
};
|
|
||||||
|
|
||||||
VF_ForEachSpatialCandidate(
|
|
||||||
Cache.TunnelSpatialIndex, Cache.Tunnels.Num(), WorldX, WorldY, EvaluateTunnel,
|
|
||||||
bUseSpatialIndex);
|
|
||||||
|
|
||||||
return MinSDF;
|
|
||||||
}
|
|
||||||
|
|
||||||
FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
|
FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
|
||||||
float WorldX, float WorldY, float WorldZ,
|
float WorldX, float WorldY, float WorldZ,
|
||||||
const FChunkSDFCache& Cache,
|
const FChunkSDFCache& Cache,
|
||||||
@@ -9454,7 +9310,7 @@ bool VoxelCaveMorphology::AnyTunnelCoreWorldNearLattice(
|
|||||||
// The broad sphere already encloses the swept radius and finite floor band. Add the
|
// The broad sphere already encloses the swept radius and finite floor band. Add the
|
||||||
// vertical relief and room-mouth ownership envelopes before asking whether the lattice
|
// vertical relief and room-mouth ownership envelopes before asking whether the lattice
|
||||||
// box can touch it. A miss is therefore an identity proof for the core evaluator and
|
// box can touch it. A miss is therefore an identity proof for the core evaluator and
|
||||||
// its support/room-floor tail; its only field writes are bSupportFloor, bRoomFloor, or
|
// its support/room-floor tail; its only field writes are bRoomFloor or
|
||||||
// CoreSDF < -inset, all of which are inside this envelope.
|
// CoreSDF < -inset, all of which are inside this envelope.
|
||||||
const float Reach = (
|
const float Reach = (
|
||||||
FMath::Sqrt(FMath::Max(BoundRadiusSq, 0.0f))
|
FMath::Sqrt(FMath::Max(BoundRadiusSq, 0.0f))
|
||||||
@@ -9477,254 +9333,6 @@ float VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF(
|
|||||||
WorldX, WorldY, WorldZ, Cache, nullptr, bUseSpatialIndex).SDF;
|
WorldX, WorldY, WorldZ, Cache, nullptr, bUseSpatialIndex).SDF;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
|
|
||||||
float WorldX, float WorldY, float WorldZ,
|
|
||||||
const FChunkSDFCache& Cache,
|
|
||||||
bool bUseSpatialIndex)
|
|
||||||
{
|
|
||||||
if (VoxelDensityAblation::IsTunnelCoreOff())
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const FVector Pos(WorldX, WorldY, WorldZ);
|
|
||||||
const FCachedTunnel* TunnelData = Cache.Tunnels.GetData();
|
|
||||||
|
|
||||||
auto TestTunnel = [&](int32 TunnelIdx) -> bool
|
|
||||||
{
|
|
||||||
const FCachedTunnel& Tunnel = TunnelData[TunnelIdx];
|
|
||||||
const bool bHasWorldChain = Tunnel.WorldControlPoints.Num() >= 2
|
|
||||||
&& Tunnel.WorldControlRadii.Num() == Tunnel.WorldControlPoints.Num();
|
|
||||||
const FVector& CenterlineMin = bHasWorldChain
|
|
||||||
? Tunnel.WorldCenterlineMin : Tunnel.SDFCenterlineMin;
|
|
||||||
const FVector& CenterlineMax = bHasWorldChain
|
|
||||||
? Tunnel.WorldCenterlineMax : Tunnel.SDFCenterlineMax;
|
|
||||||
const float InfluenceRadius = bHasWorldChain
|
|
||||||
? Tunnel.WorldInfluenceRadius : Tunnel.SDFInfluenceRadius;
|
|
||||||
if (InfluenceRadius > 0.0f
|
|
||||||
&& VF_DistanceSquaredToAabb(
|
|
||||||
WorldX, WorldY, WorldZ, CenterlineMin, CenterlineMax)
|
|
||||||
> FMath::Square(InfluenceRadius))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FVector& BoundCenter = bHasWorldChain
|
|
||||||
? Tunnel.WorldBoundCenter : Tunnel.BoundCenter;
|
|
||||||
const float BoundRadiusSq = bHasWorldChain
|
|
||||||
? Tunnel.WorldBoundRadiusSq : Tunnel.BoundRadiusSq;
|
|
||||||
if (BoundRadiusSq > 0.0f
|
|
||||||
&& VF_FloatDistSquared(WorldX, WorldY, WorldZ, BoundCenter) > BoundRadiusSq)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FVFRoomFloorOwnership RoomMouthOwnership =
|
|
||||||
VF_FindWorldTunnelMouthOwnership(Pos, Tunnel, Cache.SDFBlendRadius);
|
|
||||||
if (RoomMouthOwnership.bValid)
|
|
||||||
{
|
|
||||||
float ProjectedFloorZ = 0.0f;
|
|
||||||
float SupportRadius = 0.0f;
|
|
||||||
if (VF_ProjectSweptTunnelFloor(
|
|
||||||
Pos, Tunnel, bHasWorldChain, ProjectedFloorZ, SupportRadius,
|
|
||||||
&Cache, TunnelIdx, &RoomMouthOwnership)
|
|
||||||
&& ProjectedFloorZ > RoomMouthOwnership.FloorZ + KINDA_SMALL_NUMBER)
|
|
||||||
{
|
|
||||||
// Only a raised tunnel apron is clipped. If the tunnel is lower, the room's
|
|
||||||
// SmoothMax floor already wins naturally and retaining this support band is
|
|
||||||
// capability-safe for legacy callers.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return VF_EvaluateSweptTunnel(
|
|
||||||
Pos, Tunnel, bHasWorldChain, Cache.SDFBlendRadius,
|
|
||||||
/*bApplyFloorCut=*/true, &Cache, TunnelIdx,
|
|
||||||
&RoomMouthOwnership).bSupportFloor;
|
|
||||||
};
|
|
||||||
|
|
||||||
bool bSupport = false;
|
|
||||||
int32 CandidateCount = 0;
|
|
||||||
auto TestRange = [&](int32 Begin, int32 End)
|
|
||||||
{
|
|
||||||
const int32* ItemIndexData = Cache.TunnelSpatialIndex.ItemIndices.GetData();
|
|
||||||
for (int32 Cursor = Begin; Cursor < End; ++Cursor)
|
|
||||||
{
|
|
||||||
const int32 TunnelIdx = ItemIndexData[Cursor];
|
|
||||||
if (!Cache.Tunnels.IsValidIndex(TunnelIdx))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
++CandidateCount;
|
|
||||||
if (TestTunnel(TunnelIdx))
|
|
||||||
{
|
|
||||||
bSupport = true;
|
|
||||||
if (VoxelDensityProfile::AreCountersEnabled())
|
|
||||||
{
|
|
||||||
VoxelDensityProfile::AddCounter(
|
|
||||||
VoxelDensityProfile::ECounter::TunnelCoreEvaluated);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
int32 Begin = 0;
|
|
||||||
int32 End = 0;
|
|
||||||
if (bUseSpatialIndex && Cache.TunnelSpatialIndex.GetRange(WorldX, WorldY, Begin, End))
|
|
||||||
{
|
|
||||||
TestRange(Begin, End);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
for (int32 TunnelIdx = 0; TunnelIdx < Cache.Tunnels.Num() && !bSupport; ++TunnelIdx)
|
|
||||||
{
|
|
||||||
++CandidateCount;
|
|
||||||
if (TestTunnel(TunnelIdx))
|
|
||||||
{
|
|
||||||
bSupport = true;
|
|
||||||
if (VoxelDensityProfile::AreCountersEnabled())
|
|
||||||
{
|
|
||||||
VoxelDensityProfile::AddCounter(
|
|
||||||
VoxelDensityProfile::ECounter::TunnelCoreEvaluated);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (VoxelDensityProfile::AreCountersEnabled())
|
|
||||||
{
|
|
||||||
VoxelDensityProfile::AddCounter(
|
|
||||||
VoxelDensityProfile::ECounter::TunnelCoreCandidates,
|
|
||||||
static_cast<uint64>(CandidateCount));
|
|
||||||
}
|
|
||||||
return bSupport;
|
|
||||||
}
|
|
||||||
|
|
||||||
void VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
|
|
||||||
float WorldX, float WorldY,
|
|
||||||
const FChunkSDFCache& Cache,
|
|
||||||
FTunnelSupportFloorColumn& OutColumn,
|
|
||||||
bool bUseSpatialIndex)
|
|
||||||
{
|
|
||||||
if (VoxelDensityAblation::IsTunnelCoreOff())
|
|
||||||
{
|
|
||||||
OutColumn.Reset();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
VoxelDensityProfile::FScopedTimer ProfileTimer(
|
|
||||||
VoxelDensityProfile::EBucket::TunnelCoreSupport);
|
|
||||||
OutColumn.Reset();
|
|
||||||
|
|
||||||
if (VoxelDensityProfile::AreCountersEnabled())
|
|
||||||
{
|
|
||||||
VoxelDensityProfile::AddCounter(
|
|
||||||
VoxelDensityProfile::ECounter::TunnelSupportColumnBuilds);
|
|
||||||
}
|
|
||||||
|
|
||||||
const FVector ColumnPosition(WorldX, WorldY, 0.0f);
|
|
||||||
const FCachedTunnel* TunnelData = Cache.Tunnels.GetData();
|
|
||||||
|
|
||||||
auto EvaluateTunnel = [&](int32 TunnelIdx)
|
|
||||||
{
|
|
||||||
const FCachedTunnel& Tunnel = TunnelData[TunnelIdx];
|
|
||||||
const FVFRoomFloorOwnership RoomMouthOwnership =
|
|
||||||
VF_FindWorldTunnelMouthOwnership(ColumnPosition, Tunnel, Cache.SDFBlendRadius);
|
|
||||||
float FloorZ = 0.0f;
|
|
||||||
float SupportRadius = 0.0f;
|
|
||||||
if (!VF_ProjectSweptTunnelFloor(
|
|
||||||
ColumnPosition, Tunnel, /*bWorldChain=*/true,
|
|
||||||
FloorZ, SupportRadius, &Cache, TunnelIdx, &RoomMouthOwnership))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
float MinZ = FloorZ - VoxelPassageGeometry::LandingFloorThicknessVoxels;
|
|
||||||
float MaxZ = FloorZ + VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels;
|
|
||||||
|
|
||||||
// Preserve the public point predicate's world bounding sphere exactly, while resolving
|
|
||||||
// its Z interval once. The XY projection already proves the support-radius condition.
|
|
||||||
if (Tunnel.WorldBoundRadiusSq > 0.0f)
|
|
||||||
{
|
|
||||||
const float DX = WorldX - static_cast<float>(Tunnel.WorldBoundCenter.X);
|
|
||||||
const float DY = WorldY - static_cast<float>(Tunnel.WorldBoundCenter.Y);
|
|
||||||
const float Remaining = Tunnel.WorldBoundRadiusSq - (DX * DX + DY * DY);
|
|
||||||
if (Remaining < 0.0f)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const float HalfZ = FMath::Sqrt(Remaining);
|
|
||||||
MinZ = FMath::Max(MinZ, static_cast<float>(Tunnel.WorldBoundCenter.Z) - HalfZ);
|
|
||||||
MaxZ = FMath::Min(MaxZ, static_cast<float>(Tunnel.WorldBoundCenter.Z) + HalfZ);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (MinZ <= MaxZ)
|
|
||||||
{
|
|
||||||
FTunnelSupportFloorInterval& Interval =
|
|
||||||
OutColumn.Intervals.Emplace_GetRef();
|
|
||||||
Interval.TunnelIndex = TunnelIdx;
|
|
||||||
Interval.FloorZ = FloorZ;
|
|
||||||
Interval.MinZ = MinZ;
|
|
||||||
Interval.MaxZ = MaxZ;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const int32 CandidateCount = VF_ForEachSpatialCandidate(
|
|
||||||
Cache.TunnelSpatialIndex, Cache.Tunnels.Num(), WorldX, WorldY, EvaluateTunnel,
|
|
||||||
bUseSpatialIndex);
|
|
||||||
if (VoxelDensityProfile::AreCountersEnabled())
|
|
||||||
{
|
|
||||||
VoxelDensityProfile::AddCounter(
|
|
||||||
VoxelDensityProfile::ECounter::TunnelSupportColumnCandidates,
|
|
||||||
static_cast<uint64>(CandidateCount));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Candidate IDs are deterministic and allow the world evaluator to find a
|
|
||||||
// tunnel's own band without re-running the XY projection. The public union
|
|
||||||
// query below simply tests every band; graph columns normally contain only a
|
|
||||||
// handful of candidates.
|
|
||||||
OutColumn.Intervals.Sort(
|
|
||||||
[](const FTunnelSupportFloorInterval& A,
|
|
||||||
const FTunnelSupportFloorInterval& B)
|
|
||||||
{
|
|
||||||
return A.TunnelIndex < B.TunnelIndex;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VoxelCaveMorphology::IsTunnelSupportFloorColumnZ(
|
|
||||||
float WorldZ,
|
|
||||||
const FTunnelSupportFloorColumn& Column)
|
|
||||||
{
|
|
||||||
for (const FTunnelSupportFloorInterval& Interval : Column.Intervals)
|
|
||||||
{
|
|
||||||
if (WorldZ <= Interval.MaxZ)
|
|
||||||
{
|
|
||||||
if (WorldZ >= Interval.MinZ)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VoxelCaveMorphology::GetTunnelSupportFloorColumnBand(
|
|
||||||
int32 TunnelIndex,
|
|
||||||
const FTunnelSupportFloorColumn& Column,
|
|
||||||
float& OutFloorZ,
|
|
||||||
float& OutMinZ,
|
|
||||||
float& OutMaxZ)
|
|
||||||
{
|
|
||||||
for (const FTunnelSupportFloorInterval& Interval : Column.Intervals)
|
|
||||||
{
|
|
||||||
if (Interval.TunnelIndex == TunnelIndex)
|
|
||||||
{
|
|
||||||
OutFloorZ = Interval.FloorZ;
|
|
||||||
OutMinZ = Interval.MinZ;
|
|
||||||
OutMaxZ = Interval.MaxZ;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// CONVENIENCE WRAPPER (backward compatible)
|
// CONVENIENCE WRAPPER (backward compatible)
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
@@ -9734,35 +9342,3 @@ bool VoxelCaveMorphology::GetTunnelSupportFloorColumnBand(
|
|||||||
// internally widens the COLLECT region to (2*MaxTunnelLength + MaxInfluence) so the
|
// internally widens the COLLECT region to (2*MaxTunnelLength + MaxInfluence) so the
|
||||||
// graph it builds is the same one the chunk path would build at this point.
|
// graph it builds is the same one the chunk path would build at this point.
|
||||||
|
|
||||||
float VoxelCaveMorphology::EvaluateSDF(
|
|
||||||
float WorldX, float WorldY, float WorldZ,
|
|
||||||
const FStrateGenerationParams& Params,
|
|
||||||
uint32 Seed, int32 StrateIndex)
|
|
||||||
{
|
|
||||||
const float RoomRadiusEnvelope = FMath::Max(
|
|
||||||
FMath::Abs(Params.MinRoomRadius), FMath::Abs(Params.MaxRoomRadius));
|
|
||||||
const float TunnelRadiusEnvelope = FMath::Max(
|
|
||||||
0.5f,
|
|
||||||
FMath::Max(FMath::Abs(Params.TunnelMinRadius), FMath::Abs(Params.TunnelMaxRadius))
|
|
||||||
* 1.18f);
|
|
||||||
const float FloorReliefEnvelope = FMath::Abs(Params.FloorReliefStrength)
|
|
||||||
* VOXEL_NOISE_SCALE * 1.5f;
|
|
||||||
const float BlendEnvelope = FMath::Max(Params.SDFBlendRadius, 0.0f);
|
|
||||||
const float Margin = FMath::Max(
|
|
||||||
RoomRadiusEnvelope + FloorReliefEnvelope,
|
|
||||||
FMath::Abs(Params.TunnelWarpStrength) + TunnelRadiusEnvelope
|
|
||||||
) + BlendEnvelope * 3.0f;
|
|
||||||
|
|
||||||
FChunkSDFCache TempCache;
|
|
||||||
BuildChunkCache(
|
|
||||||
TempCache,
|
|
||||||
WorldX - Margin, WorldY - Margin,
|
|
||||||
WorldX + Margin, WorldY + Margin,
|
|
||||||
Params, Seed, StrateIndex
|
|
||||||
);
|
|
||||||
|
|
||||||
return EvaluateSDFCached(
|
|
||||||
WorldX, WorldY, WorldZ,
|
|
||||||
TempCache, Params.SDFBlendRadius
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -9,8 +9,6 @@ namespace VoxelClipmapDesiredTiles
|
|||||||
int32 ClipRadius = 3;
|
int32 ClipRadius = 3;
|
||||||
int32 MaxClipLevel = 4;
|
int32 MaxClipLevel = 4;
|
||||||
int32 RenderDistanceChunks = 0;
|
int32 RenderDistanceChunks = 0;
|
||||||
bool bFarSheetRing = true;
|
|
||||||
int32 FarSheetSpanLevels = 2;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
FORCEINLINE int32 FloorDiv(int32 Value, int32 Divisor)
|
FORCEINLINE int32 FloorDiv(int32 Value, int32 Divisor)
|
||||||
@@ -60,12 +58,6 @@ namespace VoxelClipmapDesiredTiles
|
|||||||
{
|
{
|
||||||
OutLevel = MaxLevel;
|
OutLevel = MaxLevel;
|
||||||
OutRadius = OuterShellRadius(Parameters, Radius, MaxLevel);
|
OutRadius = OuterShellRadius(Parameters, Radius, MaxLevel);
|
||||||
if (Parameters.bFarSheetRing && OutRadius > Radius)
|
|
||||||
{
|
|
||||||
OutLevel = MaxLevel + FMath::Clamp(Parameters.FarSheetSpanLevels, 1, 4);
|
|
||||||
OutRadius = FMath::Max(1,
|
|
||||||
(Parameters.RenderDistanceChunks + (1 << OutLevel) - 1) >> OutLevel);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FORCEINLINE void ResolveVerticalBounds(
|
FORCEINLINE void ResolveVerticalBounds(
|
||||||
@@ -97,37 +89,21 @@ namespace VoxelClipmapDesiredTiles
|
|||||||
const int32 Radius = FMath::Max(1, Parameters.ClipRadius);
|
const int32 Radius = FMath::Max(1, Parameters.ClipRadius);
|
||||||
const int32 MaxLevel = FMath::Clamp(Parameters.MaxClipLevel, 0, 8);
|
const int32 MaxLevel = FMath::Clamp(Parameters.MaxClipLevel, 0, 8);
|
||||||
const int32 OuterRadius = OuterShellRadius(Parameters, Radius, MaxLevel);
|
const int32 OuterRadius = OuterShellRadius(Parameters, Radius, MaxLevel);
|
||||||
int32 SheetLevel = MaxLevel;
|
|
||||||
int32 SheetRadius = OuterRadius;
|
|
||||||
OuterShell(Parameters, Radius, MaxLevel, SheetLevel, SheetRadius);
|
|
||||||
const bool bSheetRing = SheetLevel > MaxLevel;
|
|
||||||
const int32 MaxMCRingLevel = bSheetRing ? SheetLevel - 1 : MaxLevel;
|
|
||||||
FIntVector PreviousRingMin = FIntVector::ZeroValue;
|
FIntVector PreviousRingMin = FIntVector::ZeroValue;
|
||||||
FIntVector PreviousRingMax = FIntVector::ZeroValue;
|
FIntVector PreviousRingMax = FIntVector::ZeroValue;
|
||||||
FIntVector MCRingMin = FIntVector::ZeroValue;
|
|
||||||
FIntVector MCRingMax = FIntVector::ZeroValue;
|
|
||||||
|
|
||||||
for (int32 Level = 0; Level <= MaxMCRingLevel; ++Level)
|
for (int32 Level = 0; Level <= MaxLevel; ++Level)
|
||||||
{
|
{
|
||||||
const int32 Pow = 1 << Level;
|
const int32 Pow = 1 << Level;
|
||||||
const FIntVector CenterLevel = FloorDiv(Center, Pow);
|
const FIntVector CenterLevel = FloorDiv(Center, Pow);
|
||||||
const int32 LevelRadius = (Level == MaxLevel && !bSheetRing)
|
const int32 LevelRadius = (Level == MaxLevel) ? OuterRadius : Radius;
|
||||||
? OuterRadius : Radius;
|
|
||||||
|
|
||||||
// Every finer shell must end on a complete child-pair boundary so a coarser tile is
|
// Every finer shell must end on a complete child-pair boundary so a coarser tile is
|
||||||
// either wholly inside it or wholly outside it. The final MC ring uses the sheet's
|
// either wholly inside it or wholly outside it. The outermost ring has no coarser
|
||||||
// grouping width when a sheet ring follows it; otherwise the ordinary LOD boundary
|
// neighbour and needs no alignment.
|
||||||
// only needs pair alignment.
|
const int32 RingAlignment = Level < MaxLevel ? 2 : 1;
|
||||||
const int32 RingAlignment = Level < MaxMCRingLevel
|
|
||||||
? 2
|
|
||||||
: (bSheetRing ? (1 << (SheetLevel - MaxMCRingLevel)) : 1);
|
|
||||||
FIntVector RingMin, RingMax;
|
FIntVector RingMin, RingMax;
|
||||||
AlignedRingBounds(CenterLevel, LevelRadius, RingAlignment, RingMin, RingMax);
|
AlignedRingBounds(CenterLevel, LevelRadius, RingAlignment, RingMin, RingMax);
|
||||||
if (Level == MaxMCRingLevel)
|
|
||||||
{
|
|
||||||
MCRingMin = RingMin;
|
|
||||||
MCRingMax = RingMax;
|
|
||||||
}
|
|
||||||
|
|
||||||
int32 DeltaZMin = RingMin.Z - CenterLevel.Z;
|
int32 DeltaZMin = RingMin.Z - CenterLevel.Z;
|
||||||
int32 DeltaZMax = RingMax.Z - CenterLevel.Z;
|
int32 DeltaZMax = RingMax.Z - CenterLevel.Z;
|
||||||
@@ -172,48 +148,5 @@ namespace VoxelClipmapDesiredTiles
|
|||||||
PreviousRingMin = RingMin;
|
PreviousRingMin = RingMin;
|
||||||
PreviousRingMax = RingMax;
|
PreviousRingMax = RingMax;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bSheetRing)
|
|
||||||
{
|
|
||||||
const int32 SheetPow = 1 << SheetLevel;
|
|
||||||
const FIntVector CenterSheet = FloorDiv(Center, SheetPow);
|
|
||||||
const int32 LevelDelta = SheetLevel - MaxMCRingLevel;
|
|
||||||
const int32 MCPerSheet = 1 << LevelDelta;
|
|
||||||
|
|
||||||
int32 DeltaZMin = -SheetRadius;
|
|
||||||
int32 DeltaZMax = SheetRadius;
|
|
||||||
if (MinChunkZ != MIN_int32)
|
|
||||||
{
|
|
||||||
DeltaZMin = FMath::Max(DeltaZMin,
|
|
||||||
FloorDiv(MinChunkZ, SheetPow) - CenterSheet.Z);
|
|
||||||
DeltaZMax = FMath::Min(DeltaZMax,
|
|
||||||
FloorDiv(MaxChunkZ, SheetPow) - CenterSheet.Z);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int32 DeltaZ = DeltaZMin; DeltaZ <= DeltaZMax; ++DeltaZ)
|
|
||||||
for (int32 DeltaY = -SheetRadius; DeltaY <= SheetRadius; ++DeltaY)
|
|
||||||
for (int32 DeltaX = -SheetRadius; DeltaX <= SheetRadius; ++DeltaX)
|
|
||||||
{
|
|
||||||
const FIntVector Tile = CenterSheet + FIntVector(DeltaX, DeltaY, DeltaZ);
|
|
||||||
const int32 TileMinZ = Tile.Z << SheetLevel;
|
|
||||||
const int32 TileMaxZ = ((Tile.Z + 1) << SheetLevel) - 1;
|
|
||||||
if (TileMaxZ < MinChunkZ || TileMinZ > MaxChunkZ)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool bCovered =
|
|
||||||
(Tile.X * MCPerSheet >= MCRingMin.X)
|
|
||||||
&& ((Tile.X + 1) * MCPerSheet - 1 <= MCRingMax.X)
|
|
||||||
&& (Tile.Y * MCPerSheet >= MCRingMin.Y)
|
|
||||||
&& ((Tile.Y + 1) * MCPerSheet - 1 <= MCRingMax.Y)
|
|
||||||
&& (Tile.Z * MCPerSheet >= MCRingMin.Z)
|
|
||||||
&& ((Tile.Z + 1) * MCPerSheet - 1 <= MCRingMax.Z);
|
|
||||||
if (!bCovered)
|
|
||||||
{
|
|
||||||
OutTiles.Emplace(Tile, SheetLevel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,22 +1,12 @@
|
|||||||
// VoxelDensityOpStack.cpp
|
// VoxelDensityOpStack.cpp
|
||||||
// Les opérateurs concrets de la Phase 1 : la décomposition de Maze + le post-traitement structurel.
|
// Les opérateurs concrets : les décompositions d'archétypes + le post-traitement structurel.
|
||||||
// The concrete Phase 1 operators: the Maze decomposition + the structural post-process.
|
// The concrete operators: the archetype decompositions + the structural post-process.
|
||||||
//
|
//
|
||||||
// Ces opérateurs alimentent le jeu derrière l'opt-in décrit dans l'en-tête de VoxelDensityOpStack.h.
|
// Ces opérateurs génèrent chaque strate du jeu (voir l'en-tête de VoxelDensityOpStack.h).
|
||||||
|
// These operators generate every strate in the game (see the VoxelDensityOpStack.h header).
|
||||||
//
|
//
|
||||||
// FIDÉLITÉ / FIDELITY
|
// Convention de signe INTERNE : positif = solide ; la pile négative une fois pour le MC.
|
||||||
// Chaque corps ci-dessous est une transcription LITTÉRALE du bloc correspondant de
|
// Internal sign convention: positive = solid; the stack negates once for MC.
|
||||||
// `UVoxelGenerator::GetMazeDensity` — mêmes hashes, mêmes constantes, même ordre d'opérations
|
|
||||||
// flottantes, même convention de signe (INTERNE : positif = solide). L'objectif est
|
|
||||||
// l'égalité BIT à BIT, vérifiée par `VoxelForge.OpStack.MazeEquivalence`.
|
|
||||||
//
|
|
||||||
// `OPSTACK-PLAN §2.6` n'EXIGE pas l'identité binaire avec l'ancien système — mais Maze se
|
|
||||||
// décompose si proprement qu'on peut l'obtenir, et quand on peut l'obtenir il faut la prendre :
|
|
||||||
// une égalité binaire transforme « je crois que la décomposition est correcte » en preuve.
|
|
||||||
//
|
|
||||||
// §2.6 does not REQUIRE bit-identity with the old system — but Maze decomposes cleanly enough that
|
|
||||||
// it is achievable, and when it is achievable it should be taken: bit-equality turns "I believe the
|
|
||||||
// decomposition is right" into a proof.
|
|
||||||
|
|
||||||
#include "VoxelDensityOpStack.h"
|
#include "VoxelDensityOpStack.h"
|
||||||
#include "VoxelStrateComposer.h"
|
#include "VoxelStrateComposer.h"
|
||||||
@@ -92,23 +82,20 @@ namespace
|
|||||||
* gradients.) Se tromper ici coûte une boîte de recherche un peu plus large, jamais un
|
* gradients.) Se tromper ici coûte une boîte de recherche un peu plus large, jamais un
|
||||||
* verdict faux : plus large ⇒ SUR-ensemble de primitives ⇒ `Identity` plus rare.
|
* verdict faux : plus large ⇒ SUR-ensemble de primitives ⇒ `Identity` plus rare.
|
||||||
*
|
*
|
||||||
* ⚠️⚠️ **CORRIGÉ DE 2.0 À 1.5 LE 2026-07-28, ET CETTE CONSTANTE ÉTAIT LE TERME DOMINANT DE
|
* ⚠️⚠️ **CETTE CONSTANTE EST LE TERME DOMINANT DE TOUTE LA FONCTION.** À lire avant d'y
|
||||||
* TOUTE LA FONCTION PENDANT TROIS BUILDS.** À lire avant d'y retoucher.
|
* retoucher.
|
||||||
*
|
*
|
||||||
* La dilatation vaut `CaveWarpStrength · VOXEL_NOISE_SCALE · CETTE BORNE`. Avec les défauts
|
* La dilatation vaut `CaveWarpStrength · VOXEL_NOISE_SCALE · CETTE BORNE`. Avec les défauts
|
||||||
* (`CaveWarpStrength = 8`, `SCALE = 1.25`) elle valait **20 voxels** — appliquée des deux
|
* (`CaveWarpStrength = 8`, `SCALE = 1.25`), une borne de 2.0 vaut **20 voxels** — appliquée
|
||||||
* côtés de chaque axe d'une tuile de **10 voxels**, soit une boîte de requête de 50 voxels,
|
* des deux côtés de chaque axe d'une tuile de **10 voxels**, soit une boîte de requête de
|
||||||
* **125× le volume de la tuile**. Trois passes de resserrement (le ver, les colonnes,
|
* 50 voxels, **125× le volume de la tuile** : `BoxHalfDiag` est alors dominé par cette
|
||||||
* l'échantillonneur, la disjonction des tunnels) ont été faites AUTOUR de ce terme sans que
|
* dilatation et non par la géométrie, et tout resserrement ailleurs ne gagne presque rien.
|
||||||
* personne ne le mesure. Le test des tunnels, annoncé « un ordre de grandeur plus serré », ne
|
|
||||||
* gagnait en pratique que 25 % — exactement parce que `BoxHalfDiag` était dominé par cette
|
|
||||||
* dilatation et non par la géométrie.
|
|
||||||
*
|
*
|
||||||
* ⚠️ ET LE RESTE DU PLUGIN N'A JAMAIS ÉTÉ AUSSI PRUDENT : `BuildChunkCache` est appelée avec
|
* ⚠️ LE RESTE DU PLUGIN EST MOINS PRUDENT : `BuildChunkCache` est appelée avec
|
||||||
* `Expansion = CaveWarpStrength + 2` (ici comme dans `GetDensityWithParams`), ce qui suppose
|
* `Expansion = CaveWarpStrength + 2` (ici comme dans `GetDensityWithParams`), ce qui suppose
|
||||||
* `|Perlin3D| · SCALE ≤ CaveWarpStrength`, donc `|Perlin3D| ≤ 0.8`. Le code qui tourne en
|
* `|Perlin3D| · SCALE ≤ CaveWarpStrength`, donc `|Perlin3D| ≤ 0.8`. Le code de production
|
||||||
* production depuis toujours parie déjà là-dessus. Prendre 2.0 était 2,5× plus conservateur
|
* parie déjà là-dessus. 2.0 serait 2,5× plus conservateur que l'hypothèse dont dépend déjà la
|
||||||
* que l'hypothèse dont dépend déjà la correction du cache.
|
* correction du cache.
|
||||||
*
|
*
|
||||||
* LA BORNE 1.5, DÉMONTRÉE (et non observée) :
|
* LA BORNE 1.5, DÉMONTRÉE (et non observée) :
|
||||||
* 1. `GradDot` rend `±u ± v` où `u` et `v` sont deux composantes **distinctes** de l'offset
|
* 1. `GradDot` rend `±u ± v` où `u` et `v` sont deux composantes **distinctes** de l'offset
|
||||||
@@ -122,11 +109,10 @@ namespace
|
|||||||
* est la borne qui se démontre sans analyse de cas sur les hash. `√3/2 ≈ 0.87`, la borne
|
* est la borne qui se démontre sans analyse de cas sur les hash. `√3/2 ≈ 0.87`, la borne
|
||||||
* classique de Perlin 3D, dépend du jeu de gradients : on ne s'appuie pas dessus.)
|
* classique de Perlin 3D, dépend du jeu de gradients : on ne s'appuie pas dessus.)
|
||||||
*
|
*
|
||||||
* Was 2.0, and that constant was the dominant term of this whole function for three builds:
|
* This bound is the dominant term of the whole function: 2.0 would inflate a 10-voxel tile
|
||||||
* it inflated a 10-voxel tile into a 50-voxel query box (125x the volume), which is why the
|
* into a 50-voxel query box (125x the volume). The rest of the plugin already assumes
|
||||||
* "order of magnitude tighter" tunnel test only won 25%. The rest of the plugin has always
|
* |Perlin3D| <= 0.8 (BuildChunkCache's Expansion = CaveWarpStrength + 2). 1.5 is PROVED
|
||||||
* assumed |Perlin3D| <= 0.8 (BuildChunkCache's Expansion = CaveWarpStrength + 2). 1.5 is
|
* above from GradDot's two-distinct-axes form and the per-axis weighted bound of 0.5.
|
||||||
* PROVED above from GradDot's two-distinct-axes form and the per-axis weighted bound of 0.5.
|
|
||||||
*/
|
*/
|
||||||
static constexpr float VF_PerlinAbsBound = 1.5f;
|
static constexpr float VF_PerlinAbsBound = 1.5f;
|
||||||
|
|
||||||
@@ -356,38 +342,11 @@ namespace
|
|||||||
// ⚠️ CE QUE ÇA COÛTE, dit franchement : l'original teste UNE fois et saute les douze ; la pile
|
// ⚠️ CE QUE ÇA COÛTE, dit franchement : l'original teste UNE fois et saute les douze ; la pile
|
||||||
// teste douze fois. Douze comparaisons flottantes parfaitement prédites par voxel de roc profond
|
// teste douze fois. Douze comparaisons flottantes parfaitement prédites par voxel de roc profond
|
||||||
// — mesurable, mais c'est exactement le genre de chose que `AUDIT §C10` dit de MESURER avant
|
// — mesurable, mais c'est exactement le genre de chose que `AUDIT §C10` dit de MESURER avant
|
||||||
// d'optimiser. Noté dans OPSTACK-PROGRESS comme poste de perf, pas « corrigé » à l'aveugle.
|
// d'optimiser. C'est un poste de perf connu, pas à « corriger » à l'aveugle.
|
||||||
//
|
//
|
||||||
// STAGE B5 DECISION: repeated early-out in each op, NOT a scoping container — the stack is a flat
|
// STAGE B5 DECISION: repeated early-out in each op, NOT a scoping container — the stack is a flat
|
||||||
// list that ClassifyBox folds op by op, and an op that only exists inside a container is not
|
// list that ClassifyBox folds op by op, and an op that only exists inside a container is not
|
||||||
// composable. Cost stated honestly: twelve predictable compares instead of one branch.
|
// composable. Cost stated honestly: twelve predictable compares instead of one branch.
|
||||||
/** Distance d'un point à un SEGMENT (pas à une droite). Écrite ici plutôt que prise dans
|
|
||||||
* `FMath` : cinq lignes, aucune ambiguïté d'API, et elle sert une borne de correction — le
|
|
||||||
* genre d'endroit où « je crois que cette fonction fait ça » n'est pas suffisant. */
|
|
||||||
FORCEINLINE float VF_DistPointSegment(const FVector& P, const FVector& A, const FVector& B)
|
|
||||||
{
|
|
||||||
const FVector AB = B - A;
|
|
||||||
const double LenSq = FVector::DotProduct(AB, AB);
|
|
||||||
const double T = (LenSq > KINDA_SMALL_NUMBER)
|
|
||||||
? FMath::Clamp(FVector::DotProduct(P - A, AB) / LenSq, 0.0, 1.0)
|
|
||||||
: 0.0;
|
|
||||||
return (float)FVector::Dist(P, A + AB * T);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** La même chose en 2D, pour les connecteurs de puits : ce sont des capsules HORIZONTALES, donc
|
|
||||||
* Z se teste exactement et seul XY demande une distance point-segment. */
|
|
||||||
FORCEINLINE float VF_DistPointSegment2D(const FVector2D& P, const FVector2D& A, const FVector2D& B)
|
|
||||||
{
|
|
||||||
const FVector2D AB = B - A;
|
|
||||||
const double LenSq = (double)AB.X * AB.X + (double)AB.Y * AB.Y;
|
|
||||||
const double T = (LenSq > KINDA_SMALL_NUMBER)
|
|
||||||
? FMath::Clamp(((double)(P.X - A.X) * AB.X + (double)(P.Y - A.Y) * AB.Y) / LenSq, 0.0, 1.0)
|
|
||||||
: 0.0;
|
|
||||||
const double DX = (double)P.X - ((double)A.X + AB.X * T);
|
|
||||||
const double DY = (double)P.Y - ((double)A.Y + AB.Y * T);
|
|
||||||
return (float)FMath::Sqrt(DX * DX + DY * DY);
|
|
||||||
}
|
|
||||||
|
|
||||||
FORCEINLINE bool VF_NearCaveSurface(float Sdf, float SDFBlendRadius)
|
FORCEINLINE bool VF_NearCaveSurface(float Sdf, float SDFBlendRadius)
|
||||||
{
|
{
|
||||||
// Transcrit tel quel, ordre des comparaisons compris :
|
// Transcrit tel quel, ordre des comparaisons compris :
|
||||||
@@ -705,7 +664,7 @@ namespace
|
|||||||
, CorridorRadius(FMath::Max(P.CorridorRadius, 0.5f))
|
, CorridorRadius(FMath::Max(P.CorridorRadius, 0.5f))
|
||||||
, BranchProbability(P.BranchProbability)
|
, BranchProbability(P.BranchProbability)
|
||||||
, Verticality(P.Verticality)
|
, Verticality(P.Verticality)
|
||||||
, Salt((uint32)Seed ^ 0x4D617A65u) // 'Maze' — identique à GetMazeDensity
|
, Salt((uint32)Seed ^ 0x4D617A65u) // 'Maze'
|
||||||
{}
|
{}
|
||||||
|
|
||||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
|
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
|
||||||
@@ -795,18 +754,8 @@ namespace
|
|||||||
return FVector((X + 0.5f) * CellSize, (Y + 0.5f) * CellSize, (Z + 0.5f) * CellSize);
|
return FVector((X + 0.5f) * CellSize, (Y + 0.5f) * CellSize, (Z + 0.5f) * CellSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sur-approximation volontaire : boîte englobante du segment contre la boîte élargie.
|
|
||||||
// Un test capsule/AABB exact serait plus serré ; il coûterait plus cher pour un gain nul
|
|
||||||
// ici, car la réponse ne sert qu'à un rejet grossier par tuile.
|
|
||||||
static bool SegmentHitsBox(const FVector& A, const FVector& B, const FVector& Min, const FVector& Max)
|
|
||||||
{
|
|
||||||
return FMath::Min(A.X, B.X) <= Max.X && FMath::Max(A.X, B.X) >= Min.X
|
|
||||||
&& FMath::Min(A.Y, B.Y) <= Max.Y && FMath::Max(A.Y, B.Y) >= Min.Y
|
|
||||||
&& FMath::Min(A.Z, B.Z) <= Max.Z && FMath::Max(A.Z, B.Z) >= Min.Z;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Le cache par CELLULE, partagé avec le même contrat que GetMazeDensity. Il est `thread_local` et non
|
* Le cache par CELLULE. Il est `thread_local` et non
|
||||||
* membre parce que la pile est PARTAGÉE entre workers en lecture — un membre mutable serait
|
* membre parce que la pile est PARTAGÉE entre workers en lecture — un membre mutable serait
|
||||||
* une course. La reconstruction seule inspecte le petit voisinage ; Eval ne refait aucune
|
* une course. La reconstruction seule inspecte le petit voisinage ; Eval ne refait aucune
|
||||||
* décision de parent ou de boucle.
|
* décision de parent ou de boucle.
|
||||||
@@ -870,16 +819,13 @@ namespace
|
|||||||
//=========================================================================
|
//=========================================================================
|
||||||
// RÔLE 1 — SOURCE : DALLE / SLAB VOID (FlatPlain ET CrystalChamber)
|
// RÔLE 1 — SOURCE : DALLE / SLAB VOID (FlatPlain ET CrystalChamber)
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// Transcription littérale des ÉTAPES 1-3 de `GetSlabDensity` : surface de sol, surface de
|
// Surface de sol, surface de plafond, puis `Density = -min(distAuSol, distAuPlafond)`.
|
||||||
// plafond, puis `Density = -min(distAuSol, distAuPlafond)`.
|
|
||||||
//
|
//
|
||||||
// DEUX archétypes, UN opérateur. `GetSlabDensity` est appelé pour FlatPlain et
|
// DEUX archétypes, UN opérateur, sans le moindre branchement sur le type — CrystalChamber
|
||||||
// CrystalChamber sans le moindre branchement sur le type — CrystalChamber n'est rien d'autre
|
// n'est rien d'autre que FlatPlain avec un `CeilingRoughness` plus grand (OPSTACK-PLAN §4) :
|
||||||
// que FlatPlain avec un `CeilingRoughness` plus grand. C'est le premier vrai gain du refactor
|
// la différence entre eux est un jeu de valeurs par défaut.
|
||||||
// (OPSTACK-PLAN §4) : deux des huit archétypes disparaissent dans un seul opérateur, et la
|
|
||||||
// différence entre eux redevient ce qu'elle a toujours été — un jeu de valeurs par défaut.
|
|
||||||
//
|
//
|
||||||
// Two archetypes, ONE op: GetSlabDensity is called for both with no branch on the type.
|
// Two archetypes, ONE op, with no branch on the type.
|
||||||
// CrystalChamber IS FlatPlain with a bigger CeilingRoughness.
|
// CrystalChamber IS FlatPlain with a bigger CeilingRoughness.
|
||||||
//
|
//
|
||||||
// XY-PUR depuis §3.1 (le terme en Z des deux bruits est parti). C'est ce qui rend
|
// XY-PUR depuis §3.1 (le terme en Z des deux bruits est parti). C'est ce qui rend
|
||||||
@@ -911,25 +857,22 @@ namespace
|
|||||||
bool IsAdditive() const override { return false; }
|
bool IsAdditive() const override { return false; }
|
||||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||||
|
|
||||||
// ⚠️⚠️ CORRIGÉ 2026-07-27 : c'était `true`, ET C'ÉTAIT FAUX.
|
// ⚠️⚠️ `false`, ET NE PAS LE PASSER À `true`.
|
||||||
//
|
//
|
||||||
// Le contrat de `IsXYPure` est « **`Eval`** ne dépend pas de Z » — pas « les surfaces ne
|
// Le contrat de `IsXYPure` est « **`Eval`** ne dépend pas de Z » — pas « les surfaces ne
|
||||||
// dépendent pas de Z ». Or `Eval` calcule `min(Z - sol, plafond - Z)` : il dépend de Z de
|
// dépendent pas de Z ». Or `Eval` calcule `min(Z - sol, plafond - Z)` : il dépend de Z de
|
||||||
// la façon la plus directe qui soit. §3.1 a rendu les SURFACES pures en XY ; la DENSITÉ,
|
// la façon la plus directe qui soit. §3.1 a rendu les SURFACES pures en XY ; la DENSITÉ,
|
||||||
// elle, ne l'a jamais été et ne peut pas l'être — c'est une distance à une surface.
|
// elle, ne l'a jamais été et ne peut pas l'être — c'est une distance à une surface.
|
||||||
//
|
//
|
||||||
// Latent seulement parce que personne ne lit encore ce drapeau. Le jour où le cache de
|
// Le jour où un cache de colonnes générique lit ce drapeau, un `true` ici ferait partager
|
||||||
// colonnes T1.a devient générique (l'étape suivante), un `true` ici ferait partager UNE
|
// UNE valeur de densité sur TOUTE la pile verticale de chunks — un monde silencieusement
|
||||||
// valeur de densité sur TOUTE la pile verticale de chunks — un monde silencieusement faux,
|
// faux, que `ValidateDeterminism` ne verrait pas parce qu'il échantillonne le long d'un
|
||||||
// que `ValidateDeterminism` ne verrait pas parce qu'il échantillonne le long d'un bord X.
|
// bord X. C'est exactement le piège que l'avertissement de `VoxelDensityOp.h` décrit.
|
||||||
// C'est exactement le piège que l'avertissement de `VoxelDensityOp.h` décrit, et je suis
|
|
||||||
// tombé dedans en écrivant l'opérateur qui le cite.
|
|
||||||
//
|
//
|
||||||
// ⚠️ FIXED: this said `true` and was WRONG. The contract is "**Eval** does not depend on Z",
|
// ⚠️ Must stay `false`. The contract is "**Eval** does not depend on Z", and Eval computes
|
||||||
// and Eval computes min(Z - floor, ceil - Z). §3.1 made the SURFACES XY-pure; the DENSITY
|
// min(Z - floor, ceil - Z). §3.1 made the SURFACES XY-pure; the DENSITY never was and
|
||||||
// never was and cannot be — it is a distance to a surface. Latent only because nothing reads
|
// cannot be — it is a distance to a surface. A generic column cache reading `true` here
|
||||||
// the flag yet; a generic T1.a column cache would have shared one density down the whole
|
// would share one density down the whole vertical chunk stack.
|
||||||
// vertical chunk stack.
|
|
||||||
//
|
//
|
||||||
// C'est précisément cette distinction qui justifie l'espace-hauteur (`VoxelHeightOp.h`) :
|
// C'est précisément cette distinction qui justifie l'espace-hauteur (`VoxelHeightOp.h`) :
|
||||||
// ce qui est pur en XY, ce sont les HAUTEURS, et elles y sont dans un type qui n'a pas de Z.
|
// ce qui est pur en XY, ce sont les HAUTEURS, et elles y sont dans un type qui n'a pas de Z.
|
||||||
@@ -993,8 +936,8 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// The surface coordinates stay in float; the old FVector round-trip only preserved
|
// The surface coordinates stay in float; an FVector (double) round-trip is not part of
|
||||||
// legacy rounding and is no longer part of the output contract.
|
// the output contract.
|
||||||
float SurfaceFloor(float WorldX, float WorldY) const
|
float SurfaceFloor(float WorldX, float WorldY) const
|
||||||
{
|
{
|
||||||
if (FloorRoughness <= 0.0f) { return FloorZ; }
|
if (FloorRoughness <= 0.0f) { return FloorZ; }
|
||||||
@@ -1037,7 +980,7 @@ namespace
|
|||||||
// RÔLE 1 — SOURCE : COLONNE DE SURFACE / SURFACE COLUMN (SurfaceWorld)
|
// RÔLE 1 — SOURCE : COLONNE DE SURFACE / SURFACE COLUMN (SurfaceWorld)
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// Le pont entre les deux espaces : consomme DEUX piles de hauteur (sol et voûte) et en fait une
|
// Le pont entre les deux espaces : consomme DEUX piles de hauteur (sol et voûte) et en fait une
|
||||||
// densité. C'est tout le combine de `SurfaceDensityFromColumn` :
|
// densité :
|
||||||
//
|
//
|
||||||
// Density = TerrainZ - Z ← solide sous le sol
|
// Density = TerrainZ - Z ← solide sous le sol
|
||||||
// Density = max(Density, Z - CeilSurf) ← solide au-dessus de la voûte
|
// Density = max(Density, Z - CeilSurf) ← solide au-dessus de la voûte
|
||||||
@@ -1120,12 +1063,6 @@ namespace
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sans biomes — délègue, pour qu'il n'existe qu'UN corps de construction et UN compteur
|
|
||||||
* d'identité. Deux constructeurs qui s'initialisent chacun de leur côté, c'est deux
|
|
||||||
* endroits où oublier un membre. */
|
|
||||||
FSurfaceColumnSource(const FSurfaceGenerationParams& InP, int32 Seed)
|
|
||||||
: FSurfaceColumnSource(InP, Seed, TArray<FSurfaceGenerationParams>(), nullptr) {}
|
|
||||||
|
|
||||||
void BuildSingleBiome(const FSurfaceGenerationParams& InP, int32 Seed)
|
void BuildSingleBiome(const FSurfaceGenerationParams& InP, int32 Seed)
|
||||||
{
|
{
|
||||||
// Construite à la main (pas via BuildSurfaceHeightStack) pour GARDER le pointeur vers la
|
// Construite à la main (pas via BuildSurfaceHeightStack) pour GARDER le pointeur vers la
|
||||||
@@ -1342,8 +1279,6 @@ namespace
|
|||||||
return S.Height;
|
return S.Height;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FSurfaceGenerationParams& GetParams() const { return P; }
|
|
||||||
|
|
||||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
|
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
|
||||||
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::None; }
|
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::None; }
|
||||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||||
@@ -1352,29 +1287,29 @@ namespace
|
|||||||
bool IsXYPure() const override { return false; } // voir le bloc ci-dessus
|
bool IsXYPure() const override { return false; } // voir le bloc ci-dessus
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ⚠️ C'EST ICI QUE SE JOUE LA PERF DE CET ARCHÉTYPE — corrigé 2026-07-27 après mesure.
|
* ⚠️ C'EST ICI QUE SE JOUE LA PERF DE CET ARCHÉTYPE.
|
||||||
*
|
*
|
||||||
* Le mémo de colonne était clé sur `InstanceId`, qui change à CHAQUE reconstruction de pile,
|
* Ne PAS clé ce mémo sur `InstanceId` : il change à CHAQUE reconstruction de pile,
|
||||||
* c'est-à-dire à chaque chunk. Résultat : une strate haute de 4 chunks recalculait ses
|
* c'est-à-dire à chaque chunk, et une strate haute de 4 chunks recalculerait ses colonnes
|
||||||
* colonnes **4 fois**, resamples du cliff compris. Le chemin d'origine ne fait pas ça —
|
* **4 fois**, resamples du cliff compris. `GSurfColCache` est clé sur
|
||||||
* `GSurfColCache` est clé sur `(boîte XY, StrateKey, Seed, LayoutVersion)` **SANS ChunkZ**,
|
* `(boîte XY, StrateKey, Seed, LayoutVersion)` **SANS ChunkZ**, délibérément, « shared down
|
||||||
* délibérément, « shared down the whole vertical strate stack ». Cette pile reprend la
|
* the whole vertical strate stack ». Cette pile reprend la même identité de
|
||||||
* même identité de strate/layout/seed, en ajoutant l'empreinte obligatoire des params pour
|
* strate/layout/seed, en ajoutant l'empreinte obligatoire des params pour protéger ses
|
||||||
* protéger ses sorties propres ; son mémo est maintenant un LRU spatial de six boîtes.
|
* sorties propres ; son mémo est un LRU spatial de six boîtes.
|
||||||
*
|
*
|
||||||
* Donc la clé garde l'identité partagée : ce qui rend deux colonnes interchangeables, c'est
|
* Donc la clé garde l'identité partagée : ce qui rend deux colonnes interchangeables, c'est
|
||||||
* la STRATE, le seed, la version de layout et les params, pas le chunk. Le mémo étant
|
* la STRATE, le seed, la version de layout et les params, pas le chunk. Le mémo étant
|
||||||
* `thread_local`, il SURVIT à la reconstruction de la pile — seule la clé l'invalidait.
|
* `thread_local`, il SURVIT à la reconstruction de la pile — seule la clé l'invalide.
|
||||||
*
|
*
|
||||||
* POURQUOI C'EST SÛR : les hauteurs sont XY-pures par construction (c'est tout l'objet de
|
* POURQUOI C'EST SÛR : les hauteurs sont XY-pures par construction (c'est tout l'objet de
|
||||||
* `VoxelHeightOp.h`, où le type n'a pas de Z), et le champ de biomes est documenté
|
* `VoxelHeightOp.h`, où le type n'a pas de Z), et le champ de biomes est documenté
|
||||||
* XY-pur — « ZERO Z dependence: the climate/Voronoi fields are pure-XY ». C'est exactement
|
* XY-pur — « ZERO Z dependence: the climate/Voronoi fields are pure-XY ». C'est exactement
|
||||||
* la justification sur laquelle `GSurfColCache` repose déjà.
|
* la justification sur laquelle `GSurfColCache` repose déjà.
|
||||||
*
|
*
|
||||||
* The memo was keyed on InstanceId, which changes every chunk, so a 4-chunk strate recomputed
|
* Do not key the memo on InstanceId: it changes every chunk, so a 4-chunk strate would
|
||||||
* every column 4x. GSurfColCache deliberately omits ChunkZ and shares down the whole vertical
|
* recompute every column 4x. GSurfColCache deliberately omits ChunkZ and shares down the whole
|
||||||
* stack; this now shares the same strate/layout/seed identity and adds the required params
|
* vertical stack; this shares the same strate/layout/seed identity and adds the required
|
||||||
* fingerprint for its own outputs. The six-box LRU keeps independent XY regions alive. Safe
|
* params fingerprint for its own outputs. The six-box LRU keeps independent XY regions alive. Safe
|
||||||
* because heights are XY-pure by type and the biome field is documented Z-independent.
|
* because heights are XY-pure by type and the biome field is documented Z-independent.
|
||||||
*/
|
*/
|
||||||
void PrepareChunk(const FVoxelOpContext& Ctx) override
|
void PrepareChunk(const FVoxelOpContext& Ctx) override
|
||||||
@@ -1616,13 +1551,13 @@ namespace
|
|||||||
//=========================================================================
|
//=========================================================================
|
||||||
// RÔLE 3 — MODIFIER : COLONNES SUR GRILLE MONDE / WORLD-GRID COLUMNS
|
// RÔLE 3 — MODIFIER : COLONNES SUR GRILLE MONDE / WORLD-GRID COLUMNS
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// ÉTAPE 4 de `GetSlabDensity`. Des cylindres de hauteur infinie posés sur une grille de
|
// Des cylindres de hauteur infinie posés sur une grille de
|
||||||
// `ColumnSpacing`, un tirage d'existence et un jitter par cellule. Le champ de vide décide déjà
|
// `ColumnSpacing`, un tirage d'existence et un jitter par cellule. Le champ de vide décide déjà
|
||||||
// où est le solide, donc la colonne n'a qu'à AJOUTER de la densité le long de son XY — elle
|
// où est le solide, donc la colonne n'a qu'à AJOUTER de la densité le long de son XY — elle
|
||||||
// n'est visible que là où le vide avait creusé autour d'elle.
|
// n'est visible que là où le vide avait creusé autour d'elle.
|
||||||
//
|
//
|
||||||
// Le cache 3×3 par cellule est repris tel quel (il était déjà `thread_local` dans l'original,
|
// Le cache 3×3 par cellule est `thread_local`, ce que la note de threading de VoxelDensityOp.h
|
||||||
// et c'est exactement ce que la note de threading de VoxelDensityOp.h autorise). Sa clé
|
// autorise. Sa clé
|
||||||
// contient tous les paramètres qui influent sur le résultat + le seed, donc un changement de
|
// contient tous les paramètres qui influent sur le résultat + le seed, donc un changement de
|
||||||
// layout qui change un param invalide bien ; un changement qui n'en touche aucun produirait
|
// layout qui change un param invalide bien ; un changement qui n'en touche aucun produirait
|
||||||
// des colonnes identiques (cf. AUDIT C2 — la clé est complète, pas seulement le coord).
|
// des colonnes identiques (cf. AUDIT C2 — la clé est complète, pas seulement le coord).
|
||||||
@@ -1709,7 +1644,7 @@ namespace
|
|||||||
private:
|
private:
|
||||||
struct FSlabColumn { float X, Y, R; };
|
struct FSlabColumn { float X, Y, R; };
|
||||||
|
|
||||||
static constexpr float ColBlend = 2.0f; // identique à GetSlabDensity
|
static constexpr float ColBlend = 2.0f;
|
||||||
|
|
||||||
/** Le tirage d'une cellule : existence, jitter, rayon. Fonction PURE de (cellule, seed,
|
/** Le tirage d'une cellule : existence, jitter, rayon. Fonction PURE de (cellule, seed,
|
||||||
* params) — donc `Eval` et `EffectOverBox` voient forcément la même colonne. */
|
* params) — donc `Eval` et `EffectOverBox` voient forcément la même colonne. */
|
||||||
@@ -1766,8 +1701,7 @@ namespace
|
|||||||
// SEUL changement est `Density += Fill·Base·2` au lieu de `Density -= Carve·Base·2`.
|
// SEUL changement est `Density += Fill·Base·2` au lieu de `Density -= Carve·Base·2`.
|
||||||
//
|
//
|
||||||
// Un archétype qui CREUSE dans du roc et un archétype qui REMPLIT du vide sont donc le même
|
// Un archétype qui CREUSE dans du roc et un archétype qui REMPLIT du vide sont donc le même
|
||||||
// opérateur au signe près, exactement comme la source constante au-dessus. C'est la symétrie
|
// opérateur au signe près, exactement comme la source constante au-dessus.
|
||||||
// que le `switch` ne pouvait pas montrer : les deux blocs y sont à 900 lignes l'un de l'autre.
|
|
||||||
//
|
//
|
||||||
// ⚠️ `Sign` vaut ±1.0f et rien d'autre. La multiplication par ±1 est EXACTE en IEEE-754, donc
|
// ⚠️ `Sign` vaut ±1.0f et rien d'autre. La multiplication par ±1 est EXACTE en IEEE-754, donc
|
||||||
// `D += (-1·F)·B·2` rend bit pour bit ce que `D -= F·B·2` rendait — l'égalité binaire des trois
|
// `D += (-1·F)·B·2` rend bit pour bit ce que `D -= F·B·2` rendait — l'égalité binaire des trois
|
||||||
@@ -2018,8 +1952,7 @@ namespace
|
|||||||
*
|
*
|
||||||
* (Le `ClassifyTile` actuel n'a pas cette marge — il exclut simplement ces z du test de
|
* (Le `ClassifyTile` actuel n'a pas cette marge — il exclut simplement ces z du test de
|
||||||
* colonne. La fenêtre est infime et demande que l'archétype produise de l'air pile à ce z,
|
* colonne. La fenêtre est infime et demande que l'archétype produise de l'air pile à ce z,
|
||||||
* mais elle est réelle ; notée plutôt que corrigée en douce, puisque le chemin d'aujourd'hui
|
* mais elle est réelle.)
|
||||||
* n'est pas touché par cette Phase 1.)
|
|
||||||
*/
|
*/
|
||||||
EVoxelTileClass ClassifyBox(const FBox& VoxelBox, const FVoxelOpContext&) const override
|
EVoxelTileClass ClassifyBox(const FBox& VoxelBox, const FVoxelOpContext&) const override
|
||||||
{
|
{
|
||||||
@@ -2320,7 +2253,7 @@ namespace
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
FShaftFieldSource(const FVerticalShaftParams& InP, int32 Seed, float InSpineRadius)
|
FShaftFieldSource(const FVerticalShaftParams& InP, int32 Seed, float InSpineRadius)
|
||||||
: P(InP), Salt((uint32)Seed ^ 0x53686674u) // 'Shft' — identique à GetVerticalShaftDensity
|
: P(InP), Salt((uint32)Seed ^ 0x53686674u) // 'Shft'
|
||||||
, SpineRadius(InSpineRadius) {}
|
, SpineRadius(InSpineRadius) {}
|
||||||
|
|
||||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
|
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
|
||||||
@@ -2480,9 +2413,9 @@ 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
|
||||||
* 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
|
* halo 9×9 émetteur / 15×15 total pour résoudre les fenêtres parent 5×5 et le fallback
|
||||||
* qui influent (comme l'original) : stable à travers les reconstructions de pile, ce qui
|
* fixe ±3. Clé = cellule + tous les params qui influent : stable à travers les
|
||||||
* est la leçon retenue du mémo de colonne de SurfaceWorld. */
|
* reconstructions de pile, ce qui 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
|
||||||
{
|
{
|
||||||
const float Spacing = FMath::Max(P.ShaftSpacing, 1.0f);
|
const float Spacing = FMath::Max(P.ShaftSpacing, 1.0f);
|
||||||
@@ -2776,9 +2709,9 @@ namespace
|
|||||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||||
{
|
{
|
||||||
if (P.LedgeSpacing <= 0.0f || P.LedgeDepth <= 0.0f || Field == nullptr) { return; }
|
if (P.LedgeSpacing <= 0.0f || P.LedgeDepth <= 0.0f || Field == nullptr) { return; }
|
||||||
// ⚠️ La garde de l'original est `CaveSDF < 0` — le SDF APRÈS rugosité, tel que la pile
|
// ⚠️ La garde est `Sdf < 0` — le SDF APRÈS rugosité, tel que la pile l'a laissé. C'est
|
||||||
// l'a laissé. C'est bien `InOut.Sdf` ici, pas une re-évaluation : re-calculer donnerait
|
// bien `InOut.Sdf` ici, pas une re-évaluation : re-calculer donnerait le SDF SANS
|
||||||
// le SDF SANS rugosité et déplacerait les étagères.
|
// rugosité et déplacerait les étagères.
|
||||||
if (InOut.Sdf >= 0.0f) { return; }
|
if (InOut.Sdf >= 0.0f) { return; }
|
||||||
|
|
||||||
const float Phase = FMath::Frac((WorldZ - P.StrateBottomWorldZ) / P.LedgeSpacing);
|
const float Phase = FMath::Frac((WorldZ - P.StrateBottomWorldZ) / P.LedgeSpacing);
|
||||||
@@ -2851,27 +2784,6 @@ namespace
|
|||||||
{
|
{
|
||||||
if (Field == nullptr) return;
|
if (Field == nullptr) return;
|
||||||
const FShaftFieldSource::FCells& Cells = Field->GetCellsAt(WorldX, WorldY);
|
const FShaftFieldSource::FCells& Cells = Field->GetCellsAt(WorldX, WorldY);
|
||||||
static thread_local bool bLoggedTemporaryShaftConnectors = false;
|
|
||||||
if (!bLoggedTemporaryShaftConnectors
|
|
||||||
&& FMath::IsNearlyEqual(WorldZ, -700.0f)
|
|
||||||
&& WorldX > -180.0f && WorldX < -170.0f
|
|
||||||
&& WorldY > -48.0f && WorldY < -38.0f)
|
|
||||||
{
|
|
||||||
bLoggedTemporaryShaftConnectors = true;
|
|
||||||
UE_LOG(LogTemp, Display,
|
|
||||||
TEXT("[VoxelForgeExplore][TemporaryShaftConnectors] query=(%.3f,%.3f,%.3f) count=%d"),
|
|
||||||
WorldX, WorldY, WorldZ, Cells.Conns.Num());
|
|
||||||
for (const FShaftFieldSource::FConn& DebugConn : Cells.Conns)
|
|
||||||
{
|
|
||||||
UE_LOG(LogTemp, Display,
|
|
||||||
TEXT("[VoxelForgeExplore][TemporaryShaftConnectors] A=(%.3f,%.3f,%.3f) "
|
|
||||||
"B=(%.3f,%.3f,%.3f) radius=%.3f floor=%.3f walk=%d"),
|
|
||||||
DebugConn.A.X, DebugConn.A.Y, DebugConn.A.Z,
|
|
||||||
DebugConn.B.X, DebugConn.B.Y, DebugConn.B.Z,
|
|
||||||
DebugConn.Radius, DebugConn.FloorZ,
|
|
||||||
DebugConn.bWalkableFloor ? 1 : 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const FShaftFieldSource::FConn& Conn : Cells.Conns)
|
for (const FShaftFieldSource::FConn& Conn : Cells.Conns)
|
||||||
{
|
{
|
||||||
if (!Conn.bWalkableFloor) continue;
|
if (!Conn.bWalkableFloor) continue;
|
||||||
@@ -2945,26 +2857,22 @@ namespace
|
|||||||
//
|
//
|
||||||
// ⚠️ ÉCART ASSUMÉ AVEC `OPSTACK-DECOMPOSITION §7`, qui décrivait un `FRAME IslandWarp` enveloppant
|
// ⚠️ ÉCART ASSUMÉ AVEC `OPSTACK-DECOMPOSITION §7`, qui décrivait un `FRAME IslandWarp` enveloppant
|
||||||
// la source. Le warp reste À L'INTÉRIEUR de l'opérateur, et c'est délibéré : `§7` compte trois
|
// la source. Le warp reste À L'INTÉRIEUR de l'opérateur, et c'est délibéré : `§7` compte trois
|
||||||
// usages de frames (îles, caves de TunnelNetwork, tunnels), mais **deux d'entre eux ne sont pas
|
// usages de frames (îles, caves de TunnelNetwork, tunnels), et aucun n'en a besoin. Voir
|
||||||
// encore portés**. Inventer l'infrastructure de frame pour son unique utilisateur actuel, c'est
|
// `BuildTunnelNetworkStack` : `CaveWarp` n'enveloppe qu'UN opérateur (donc c'est une variable
|
||||||
// la concevoir contre un seul exemple — précisément ce que ce refactor a évité jusqu'ici en
|
// locale, pas un frame) et `VerticalScale` est une fonction pure d'un scalaire. **Zéro frame
|
||||||
// n'abstrayant qu'à la deuxième occurrence (cf. `IVoxelBiomeField`, né d'un besoin réel).
|
// sur trois candidats.** Ne pas rouvrir : le warp reste local ICI pour la même raison qu'il
|
||||||
// ✅ RÉPONDU (2026-08-16) : TunnelNetwork EST porté, et le deuxième usage réel a dissous la
|
// est resté local là-bas. On n'abstrait qu'à la deuxième occurrence réelle (cf.
|
||||||
// question au lieu de la trancher. Voir `BuildTunnelNetworkStack` : `CaveWarp` n'enveloppe
|
// `IVoxelBiomeField`, né d'un besoin réel).
|
||||||
// qu'UN opérateur (donc c'est une variable locale, pas un frame) et `VerticalScale` est une
|
|
||||||
// fonction pure d'un scalaire. **Zéro frame sur trois candidats.** Ne pas rouvrir : le warp
|
|
||||||
// reste local ICI pour la même raison qu'il est resté local là-bas.
|
|
||||||
//
|
//
|
||||||
// The warp stays INSIDE the op against §7's FRAME suggestion. ✅ ANSWERED 2026-08-16: this said
|
// The warp stays INSIDE the op against §7's FRAME suggestion. TunnelNetwork is ported, and the
|
||||||
// "revisit when TunnelNetwork brings the second real use" — TunnelNetwork is ported, and the
|
// second real use dissolved the question rather than settling it. See BuildTunnelNetworkStack:
|
||||||
// second use dissolved the question rather than settling it. See BuildTunnelNetworkStack:
|
|
||||||
// CaveWarp wraps exactly ONE operator (a local variable, not a frame) and VerticalScale is a
|
// CaveWarp wraps exactly ONE operator (a local variable, not a frame) and VerticalScale is a
|
||||||
// pure function of a scalar. ZERO frames out of three candidates. Do not reopen.
|
// pure function of a scalar. ZERO frames out of three candidates. Do not reopen.
|
||||||
class FIslandBlobSource final : public IVoxelDensityOp
|
class FIslandBlobSource final : public IVoxelDensityOp
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
FIslandBlobSource(const FFloatingIslandParams& InP, int32 Seed)
|
FIslandBlobSource(const FFloatingIslandParams& InP, int32 Seed)
|
||||||
: P(InP), Salt((uint32)Seed ^ 0x49736C64u) // 'Isld' — identique à GetFloatingIslandDensity
|
: P(InP), Salt((uint32)Seed ^ 0x49736C64u) // 'Isld'
|
||||||
{}
|
{}
|
||||||
|
|
||||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
|
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
|
||||||
@@ -3040,8 +2948,8 @@ namespace
|
|||||||
* • en Z, `Sdf ≥ WorldZ − TopSurf ≥ WorldZ − TopZ`, donc au-dessus du sommet + marge il
|
* • en Z, `Sdf ≥ WorldZ − TopSurf ≥ WorldZ − TopZ`, donc au-dessus du sommet + marge il
|
||||||
* n'y a plus rien à faire. **En dessous, il n'y a PAS de borne** : sous une île, le SDF
|
* n'y a plus rien à faire. **En dessous, il n'y a PAS de borne** : sous une île, le SDF
|
||||||
* vaut ≈ `DistXY` à toute profondeur, donc un mince fil de matière descend le long de
|
* vaut ≈ `DistXY` à toute profondeur, donc un mince fil de matière descend le long de
|
||||||
* l'axe. C'est le comportement de l'original ; le confondre avec « rien en dessous »
|
* l'axe. Le confondre avec « rien en dessous » serait un TROU, et c'est pourquoi seule
|
||||||
* serait un TROU, et c'est pourquoi seule la borne HAUTE est testée.
|
* la borne HAUTE est testée.
|
||||||
* • cette source ne couvre pas l'aval : elle publie seulement son propre intervalle SDF.
|
* • cette source ne couvre pas l'aval : elle publie seulement son propre intervalle SDF.
|
||||||
* La rugosité, le blend du fill et le creux du SmoothMin sont bornés par leurs propres
|
* La rugosité, le blend du fill et le creux du SmoothMin sont bornés par leurs propres
|
||||||
* opérateurs dans le pliage, chacun avec son contrat isolé.
|
* opérateurs dans le pliage, chacun avec son contrat isolé.
|
||||||
@@ -3137,8 +3045,7 @@ namespace
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
/** Tirage d'une cellule. PURE en (cellule, seed, params) ⇒ `Eval` et `EffectOverBox` ne
|
/** Tirage d'une cellule. PURE en (cellule, seed, params) ⇒ `Eval` et `EffectOverBox` ne
|
||||||
* peuvent pas voir des îles différentes. Transcription littérale du bloc de cuisson de
|
* peuvent pas voir des îles différentes. */
|
||||||
* `GetFloatingIslandDensity`. */
|
|
||||||
bool RollIsland(int32 nx, int32 ny, FIsland& Out) const
|
bool RollIsland(int32 nx, int32 ny, FIsland& Out) const
|
||||||
{
|
{
|
||||||
const float H = P.StrateTopWorldZ - P.StrateBottomWorldZ;
|
const float H = P.StrateTopWorldZ - P.StrateBottomWorldZ;
|
||||||
@@ -3172,17 +3079,15 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Le voisinage 3×3, mémoïsé par worker — la même cuisson `thread_local` que l'original.
|
* Le voisinage 3×3, mémoïsé par worker (`thread_local`).
|
||||||
*
|
*
|
||||||
* ⚠️ LA CLÉ INCLUT `BoundarySealThickness`, QUE L'ORIGINAL OMET. `SpreadZ` s'en sert
|
* ⚠️ LA CLÉ INCLUT `BoundarySealThickness` : `SpreadZ` s'en sert
|
||||||
* (`H·0.5 − max(TopHalf, UnderDepth) − Seal`), donc dans `GetFloatingIslandDensity` une
|
* (`H·0.5 − max(TopHalf, UnderDepth) − Seal`), donc sans lui une édition à chaud qui ne
|
||||||
* édition à chaud qui ne change QUE l'épaisseur de seal sert des îles périmées. Même famille
|
* change QUE l'épaisseur de seal servirait des îles périmées. Même famille que `AUDIT §C2` :
|
||||||
* que `AUDIT §C2` et que la régression d'overhang du 2026-07-27 : une clé de cache
|
* une clé de cache incomplète ne se voit pas, elle produit du terrain plausible.
|
||||||
* incomplète ne se voit pas, elle produit du terrain plausible. Ajouter le champ ne coûte
|
|
||||||
* qu'un recalcul, jamais une valeur différente — donc l'égalité binaire tient.
|
|
||||||
*
|
*
|
||||||
* The key includes BoundarySealThickness, which the original omits although SpreadZ reads it.
|
* The key includes BoundarySealThickness because SpreadZ reads it; without it a live edit
|
||||||
* Adding it can only cost a recompute, never change a value — bit-equality is unaffected.
|
* of the seal thickness alone would serve stale islands.
|
||||||
*/
|
*/
|
||||||
const FCells& GetCells(float WorldX, float WorldY) const
|
const FCells& GetCells(float WorldX, float WorldY) const
|
||||||
{
|
{
|
||||||
@@ -4019,10 +3924,9 @@ namespace
|
|||||||
// clé « égalité de chunk » se retournait à chaque cellule de bord et reconstruisait le
|
// clé « égalité de chunk » se retournait à chaque cellule de bord et reconstruisait le
|
||||||
// cache (coûteux) en boucle. Comme le cache couvre la boîte + MaxInfluence, toute requête
|
// cache (coûteux) en boucle. Comme le cache couvre la boîte + MaxInfluence, toute requête
|
||||||
// DANS la boîte est correcte. Ne pas « simplifier » en clé de chunk.
|
// DANS la boîte est correcte. Ne pas « simplifier » en clé de chunk.
|
||||||
// (Les champs de cache vivent maintenant dans `FState`, au-dessus, pour que les
|
// (Les champs de cache vivent dans `FState`, au-dessus, pour que les modificateurs de
|
||||||
// modificateurs de détail de l'étape B puissent les lire. Même durée de vie, même
|
// détail puissent les lire ; ce sont des `thread_local` partagés entre instances.)
|
||||||
// partage entre instances qu'avant : ce sont les mêmes `thread_local`, déménagés.)
|
// ⚠️ LA CLÉ INCLUT LES PARAMS, CONTRAIREMENT À L'ORIGINAL.
|
||||||
// ⚠️ AJOUTÉ PAR RAPPORT À L'ORIGINAL — la leçon du 2026-07-27 (régression d'overhang).
|
|
||||||
// L'original ne clé QUE sur (boîte, strate, seed) : deux jeux de params différents dans
|
// L'original ne clé QUE sur (boîte, strate, seed) : deux jeux de params différents dans
|
||||||
// la MÊME strate au MÊME seed se servent mutuellement leur cache. En production
|
// la MÊME strate au MÊME seed se servent mutuellement leur cache. En production
|
||||||
// `RebuildStrates` masque le trou en bougeant la strate ; en test, deux piles construites
|
// `RebuildStrates` masque le trou en bougeant la strate ; en test, deux piles construites
|
||||||
@@ -4313,7 +4217,6 @@ namespace
|
|||||||
// room's own volume.
|
// room's own volume.
|
||||||
InOut.bHasTunnelCoreWorldEvaluation = true;
|
InOut.bHasTunnelCoreWorldEvaluation = true;
|
||||||
InOut.TunnelCoreWorldSDF = S.LastTunnelCoreWorldEvaluation.SDF;
|
InOut.TunnelCoreWorldSDF = S.LastTunnelCoreWorldEvaluation.SDF;
|
||||||
InOut.bTunnelCoreSupportFloor = false;
|
|
||||||
InOut.bTunnelCoreRoomFloor =
|
InOut.bTunnelCoreRoomFloor =
|
||||||
S.LastTunnelCoreWorldEvaluation.bRoomFloor;
|
S.LastTunnelCoreWorldEvaluation.bRoomFloor;
|
||||||
InOut.bHasTunnelCoreSweptFloor =
|
InOut.bHasTunnelCoreSweptFloor =
|
||||||
@@ -4322,8 +4225,6 @@ namespace
|
|||||||
S.LastTunnelCoreWorldEvaluation.SweptFloorZ;
|
S.LastTunnelCoreWorldEvaluation.SweptFloorZ;
|
||||||
InOut.TunnelCoreSweptFloorRadius =
|
InOut.TunnelCoreSweptFloorRadius =
|
||||||
S.LastTunnelCoreWorldEvaluation.SweptFloorRadius;
|
S.LastTunnelCoreWorldEvaluation.SweptFloorRadius;
|
||||||
InOut.bProtectedStructuralFloor = false;
|
|
||||||
InOut.StructuralFloorMinimumDensity = 0.0f;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// These cache hand-offs remain valid when tunnel core is off; detail operators still
|
// These cache hand-offs remain valid when tunnel core is off; detail operators still
|
||||||
@@ -4445,7 +4346,6 @@ namespace
|
|||||||
bool bMayHaveTunnelCoreTail = false;
|
bool bMayHaveTunnelCoreTail = false;
|
||||||
uint32 ExactTailQueries = 0;
|
uint32 ExactTailQueries = 0;
|
||||||
uint32 ExactTailEvaluated = 0;
|
uint32 ExactTailEvaluated = 0;
|
||||||
uint32 ExactTailCacheHits = 0;
|
|
||||||
uint64 ExactTailCycles = 0;
|
uint64 ExactTailCycles = 0;
|
||||||
uint64 PropagateCycles = 0;
|
uint64 PropagateCycles = 0;
|
||||||
uint64 ExactPrimitiveCycles = 0;
|
uint64 ExactPrimitiveCycles = 0;
|
||||||
@@ -4534,12 +4434,6 @@ namespace
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool HasRoomColumnsForLastBox() const
|
|
||||||
{
|
|
||||||
const FBoxState& B = BoxState();
|
|
||||||
return !B.bValid || GetBoxCache().Columns.Num() > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
float RoomColumnFillSupremumForLastBox() const
|
float RoomColumnFillSupremumForLastBox() const
|
||||||
{
|
{
|
||||||
const FBoxState& B = BoxState();
|
const FBoxState& B = BoxState();
|
||||||
@@ -5623,18 +5517,6 @@ namespace
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
auto EffectiveZRangeToWorld = [&](float MinEffectiveZ,
|
|
||||||
float MaxEffectiveZ) -> FVector2D
|
|
||||||
{
|
|
||||||
if (P.VerticalScale > 0.0f && P.VerticalScale != 1.0f)
|
|
||||||
{
|
|
||||||
const float WorldA = MinEffectiveZ * P.VerticalScale;
|
|
||||||
const float WorldB = MaxEffectiveZ * P.VerticalScale;
|
|
||||||
return FVector2D(FMath::Min(WorldA, WorldB), FMath::Max(WorldA, WorldB));
|
|
||||||
}
|
|
||||||
return FVector2D(MinEffectiveZ, MaxEffectiveZ);
|
|
||||||
};
|
|
||||||
|
|
||||||
auto DistanceBlockToInfiniteLineLower = [&](const FExactLatticeBlock& Block,
|
auto DistanceBlockToInfiniteLineLower = [&](const FExactLatticeBlock& Block,
|
||||||
const FVector& A,
|
const FVector& A,
|
||||||
const FVector& BPoint) -> float
|
const FVector& BPoint) -> float
|
||||||
@@ -8331,7 +8213,7 @@ namespace
|
|||||||
// sortie, ce qui est la raison pour laquelle l'ordre de `BuildTunnelNetworkStack` reste celui
|
// sortie, ce qui est la raison pour laquelle l'ordre de `BuildTunnelNetworkStack` reste celui
|
||||||
// de l'original, ligne pour ligne.
|
// de l'original, ligne pour ligne.
|
||||||
//
|
//
|
||||||
// TIER 3b AUDIT (2026-09-04): this op is intentionally still separate. Ten operators lie
|
// This op is intentionally separate from 4b. Ten operators lie
|
||||||
// between 4b and this phase (4c through 4h), and every one reads+writes Density. Moving this
|
// between 4b and this phase (4c through 4h), and every one reads+writes Density. Moving this
|
||||||
// phase next to 4b would reorder those density updates, so a bit-identical fuse is not legal.
|
// phase next to 4b would reorder those density updates, so a bit-identical fuse is not legal.
|
||||||
class FFloorBiasMod final : public IVoxelDensityOp
|
class FFloorBiasMod final : public IVoxelDensityOp
|
||||||
@@ -9427,8 +9309,8 @@ namespace VoxelDensityOps
|
|||||||
|
|
||||||
TUniquePtr<IVoxelDensityOp> MakeConstantVoidSource(float BaseDensity)
|
TUniquePtr<IVoxelDensityOp> MakeConstantVoidSource(float BaseDensity)
|
||||||
{
|
{
|
||||||
// `float Density = -Params.BaseDensity; // start as open air (void)` — la négation unaire
|
// `Density = -BaseDensity` : on part du vide. La négation unaire est exacte.
|
||||||
// est exacte, donc c'est littéralement la première ligne de GetFloatingIslandDensity.
|
// Start as open air; unary negation is exact.
|
||||||
return MakeUnique<FConstantFieldSource>(-BaseDensity);
|
return MakeUnique<FConstantFieldSource>(-BaseDensity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9463,18 +9345,13 @@ namespace VoxelDensityOps
|
|||||||
return MakeUnique<FGridColumnMod>(P, Seed);
|
return MakeUnique<FGridColumnMod>(P, Seed);
|
||||||
}
|
}
|
||||||
|
|
||||||
TUniquePtr<IVoxelDensityOp> MakeSurfaceColumnSource(const FSurfaceGenerationParams& P, int32 Seed)
|
|
||||||
{
|
|
||||||
return MakeUnique<FSurfaceColumnSource>(P, Seed);
|
|
||||||
}
|
|
||||||
|
|
||||||
void BuildSurfaceStack(FVoxelOpStack& OutStack, const FSurfaceGenerationParams& P,
|
void BuildSurfaceStack(FVoxelOpStack& OutStack, const FSurfaceGenerationParams& P,
|
||||||
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
|
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
|
||||||
const TArray<FSurfaceGenerationParams>& PerBiomeParams,
|
const TArray<FSurfaceGenerationParams>& PerBiomeParams,
|
||||||
TUniquePtr<IVoxelBiomeField> BiomeField,
|
TUniquePtr<IVoxelBiomeField> BiomeField,
|
||||||
bool bAppendStructuralPosts)
|
bool bAppendStructuralPosts)
|
||||||
{
|
{
|
||||||
// ARCHÉTYPE COMPLET depuis l'étape 2c : vide + overhang + mélange de biomes.
|
// ARCHÉTYPE COMPLET : vide + overhang + mélange de biomes.
|
||||||
// `PerBiomeParams` vide ⇒ chemin sans biomes, strictement inchangé.
|
// `PerBiomeParams` vide ⇒ chemin sans biomes, strictement inchangé.
|
||||||
TUniquePtr<FSurfaceColumnSource> ColumnSource =
|
TUniquePtr<FSurfaceColumnSource> ColumnSource =
|
||||||
MakeUnique<FSurfaceColumnSource>(P, Seed, PerBiomeParams, MoveTemp(BiomeField));
|
MakeUnique<FSurfaceColumnSource>(P, Seed, PerBiomeParams, MoveTemp(BiomeField));
|
||||||
@@ -9497,10 +9374,8 @@ namespace VoxelDensityOps
|
|||||||
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
|
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
|
||||||
bool bAppendStructuralPosts)
|
bool bAppendStructuralPosts)
|
||||||
{
|
{
|
||||||
// DEUX archétypes entrent ici, aucun branchement ne les distingue — parce que
|
// DEUX archétypes entrent ici, aucun branchement ne les distingue : FlatPlain et
|
||||||
// `GetSlabDensity` n'en fait aucun non plus. FlatPlain et CrystalChamber ne diffèrent que
|
// CrystalChamber ne diffèrent que par leurs valeurs par défaut.
|
||||||
// par leurs valeurs par défaut, et c'est maintenant visible dans le code plutôt que dans
|
|
||||||
// un commentaire. 8 archétypes → 7.
|
|
||||||
OutStack.Add(MakeSlabVoidSource(P, Seed));
|
OutStack.Add(MakeSlabVoidSource(P, Seed));
|
||||||
OutStack.Add(MakeGridColumnMod(P, Seed));
|
OutStack.Add(MakeGridColumnMod(P, Seed));
|
||||||
|
|
||||||
@@ -9516,16 +9391,10 @@ namespace VoxelDensityOps
|
|||||||
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
|
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
|
||||||
bool bAppendStructuralPosts)
|
bool bAppendStructuralPosts)
|
||||||
{
|
{
|
||||||
// ⚠️ LA PREUVE QUE L'ABSTRACTION EST RÉELLE, et elle vaut d'être dite : TROIS des cinq
|
// TROIS des cinq opérateurs ci-dessous sont ceux de Maze, repris tels quels —
|
||||||
// opérateurs ci-dessous sont ceux de Maze, **repris sans une ligne de changement** —
|
// `ConstantRock`, `SdfRoughness`, `SdfCarve` — avec une source différente (`§2.5`).
|
||||||
// `ConstantRock`, `SdfRoughness`, `SdfCarve`. Dans le `switch`, Maze et VerticalShafts sont
|
|
||||||
// deux fonctions de ~100 lignes qui n'ont rien en commun à l'œil ; en opérateurs, ce sont
|
|
||||||
// les MÊMES trois ops avec une source différente. C'est exactement ce que `§2.5` prédisait
|
|
||||||
// et ce que la Phase 1 avait parié.
|
|
||||||
//
|
//
|
||||||
// THREE of the five ops below are Maze's, reused without a line changed. In the switch,
|
// THREE of the five ops below are Maze's, reused unchanged, with a different source.
|
||||||
// Maze and VerticalShafts are two unrelated ~100-line functions; as operators they are the
|
|
||||||
// same three ops with a different source.
|
|
||||||
constexpr float CarveBlend = 2.0f;
|
constexpr float CarveBlend = 2.0f;
|
||||||
|
|
||||||
TUniquePtr<FShaftFieldSource> ShaftSource =
|
TUniquePtr<FShaftFieldSource> ShaftSource =
|
||||||
@@ -9534,9 +9403,8 @@ namespace VoxelDensityOps
|
|||||||
|
|
||||||
OutStack.Add(MakeConstantRockSource(P.BaseDensity));
|
OutStack.Add(MakeConstantRockSource(P.BaseDensity));
|
||||||
OutStack.Add(MoveTemp(ShaftSource));
|
OutStack.Add(MoveTemp(ShaftSource));
|
||||||
// Fréquence 0.1 et fenêtre `SurfaceRoughness + 4` — les constantes de
|
// Fréquence 0.1 et fenêtre `SurfaceRoughness + 4` — PAS celles de Maze
|
||||||
// `GetVerticalShaftDensity`, PAS celles de Maze (0.12 / `R + rough + 2`). Même opérateur,
|
// (0.12 / `R + rough + 2`). Même opérateur, réglages différents.
|
||||||
// réglages différents : c'est le point.
|
|
||||||
OutStack.Add(MakeSdfRoughnessMod(P.SurfaceRoughness, 0.1f, 3, P.SurfaceRoughness + 4.0f));
|
OutStack.Add(MakeSdfRoughnessMod(P.SurfaceRoughness, 0.1f, 3, P.SurfaceRoughness + 4.0f));
|
||||||
OutStack.Add(MakeSdfCarve(CarveBlend, P.BaseDensity));
|
OutStack.Add(MakeSdfCarve(CarveBlend, P.BaseDensity));
|
||||||
OutStack.Add(MakeUnique<FShaftLedgeMod>(P, ShaftPtr));
|
OutStack.Add(MakeUnique<FShaftLedgeMod>(P, ShaftPtr));
|
||||||
@@ -9565,20 +9433,6 @@ namespace VoxelDensityOps
|
|||||||
// plus proche appliqué ; les onze modificateurs concernés y lisent leurs champs au lieu des
|
// plus proche appliqué ; les onze modificateurs concernés y lisent leurs champs au lieu des
|
||||||
// leurs. La rugosité (4b) NON — dans l'original elle précède la déclaration du shadow.
|
// leurs. La rugosité (4b) NON — dans l'original elle précède la déclaration du shadow.
|
||||||
//
|
//
|
||||||
// ⚠️ CE PARAGRAPHE ÉTAIT PÉRIMÉ ET DISAIT LE CONTRAIRE DU CODE (corrigé 2026-08-16).
|
|
||||||
// Il annonçait « étape A sur trois, les douze modificateurs et l'override par salle ne sont
|
|
||||||
// pas encore portés, c'est pour ça que `UsesOperatorStackForChunk` rend **false** pour
|
|
||||||
// TunnelNetwork ». Les trois étapes sont terminées : les douze modificateurs sont ajoutés
|
|
||||||
// douze lignes plus bas, l'override C1 est en place, et `UsesOperatorStackForChunk` rend
|
|
||||||
// **true** pour TunnelNetwork. Un commentaire qui contredit le code sous lui est
|
|
||||||
// exactement le piège « lire le code, pas le commentaire » à l'envers.
|
|
||||||
//
|
|
||||||
// STALE PARAGRAPH REMOVED 2026-08-16. It claimed "stage A of three, the detail modifiers and
|
|
||||||
// the per-room override are not ported yet, which is why the archetype is still off in
|
|
||||||
// UsesOperatorStackForChunk". All three stages are done, the twelve modifiers are added a
|
|
||||||
// dozen lines below, and that function returns TRUE for TunnelNetwork. A comment that
|
|
||||||
// contradicts the code beneath it is the "read the code, not the comment" trap in reverse.
|
|
||||||
//
|
|
||||||
//---------------------------------------------------------------------
|
//---------------------------------------------------------------------
|
||||||
// ⚠️ CE PORTAGE RETIRE L'IDÉE DE « FRAME OPS » (OPSTACK-DECOMPOSITION §1)
|
// ⚠️ CE PORTAGE RETIRE L'IDÉE DE « FRAME OPS » (OPSTACK-DECOMPOSITION §1)
|
||||||
//---------------------------------------------------------------------
|
//---------------------------------------------------------------------
|
||||||
@@ -9592,7 +9446,7 @@ namespace VoxelDensityOps
|
|||||||
// qu'ajouter un canal pour éviter une division.
|
// qu'ajouter un canal pour éviter une division.
|
||||||
// Il restait le warp d'îles (§7), déjà gardé local pour la même raison. **Zéro frame sur
|
// Il restait le warp d'îles (§7), déjà gardé local pour la même raison. **Zéro frame sur
|
||||||
// trois candidats** : ce n'était pas une infrastructure manquante, c'était trois fois la
|
// trois candidats** : ce n'était pas une infrastructure manquante, c'était trois fois la
|
||||||
// même chose vue de loin. Noté ici plutôt que laissé en TODO permanent.
|
// même chose vue de loin.
|
||||||
constexpr float CarveMinDivisor = 1.0f; // TunnelNetwork plancher son diviseur, cf. FSdfConvertOp
|
constexpr float CarveMinDivisor = 1.0f; // TunnelNetwork plancher son diviseur, cf. FSdfConvertOp
|
||||||
|
|
||||||
// La source de salles est retenue par pointeur non possédant : les terrasses re-interrogent
|
// La source de salles est retenue par pointeur non possédant : les terrasses re-interrogent
|
||||||
@@ -9665,8 +9519,8 @@ namespace VoxelDensityOps
|
|||||||
|
|
||||||
OutStack.Add(MakeConstantVoidSource(P.BaseDensity));
|
OutStack.Add(MakeConstantVoidSource(P.BaseDensity));
|
||||||
OutStack.Add(MakeUnique<FIslandBlobSource>(P, Seed));
|
OutStack.Add(MakeUnique<FIslandBlobSource>(P, Seed));
|
||||||
// Fréquence 0.08 et 4 octaves — les constantes de `GetFloatingIslandDensity`. Quatrième
|
// Fréquence 0.08 et 4 octaves — les constantes de FloatingIslands (Maze 0.12/3,
|
||||||
// archétype à réutiliser cet opérateur (Maze 0.12/3, VerticalShafts 0.1/3).
|
// VerticalShafts 0.1/3).
|
||||||
OutStack.Add(MakeSdfRoughnessMod(P.SurfaceRoughness, 0.08f, 4,
|
OutStack.Add(MakeSdfRoughnessMod(P.SurfaceRoughness, 0.08f, 4,
|
||||||
P.SurfaceRoughness + BlendK + 2.0f));
|
P.SurfaceRoughness + BlendK + 2.0f));
|
||||||
OutStack.Add(MakeSdfFill(BlendK, P.BaseDensity));
|
OutStack.Add(MakeSdfFill(BlendK, P.BaseDensity));
|
||||||
@@ -9683,15 +9537,14 @@ namespace VoxelDensityOps
|
|||||||
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
|
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
|
||||||
bool bAppendStructuralPosts)
|
bool bAppendStructuralPosts)
|
||||||
{
|
{
|
||||||
// Les constantes viennent telles quelles de GetMazeDensity — elles y étaient codées en dur.
|
// Constantes Maze, codées en dur. / Maze's hard-coded constants.
|
||||||
constexpr float CarveBlend = 2.0f;
|
constexpr float CarveBlend = 2.0f;
|
||||||
constexpr float RoughFrequency = 0.12f;
|
constexpr float RoughFrequency = 0.12f;
|
||||||
constexpr int32 RoughOctaves = 3;
|
constexpr int32 RoughOctaves = 3;
|
||||||
|
|
||||||
const float R = FMath::Max(P.CorridorRadius, 0.5f);
|
const float R = FMath::Max(P.CorridorRadius, 0.5f);
|
||||||
|
|
||||||
// Fenêtre d'application de la rugosité : `MazeSDF < R + SurfaceRoughness + 2.0f` dans
|
// Fenêtre d'application de la rugosité : `MazeSDF < R + SurfaceRoughness + 2.0f`.
|
||||||
// l'original. Reproduite à l'identique pour que l'égalité binaire tienne.
|
|
||||||
const float RoughApplyWithin = R + P.SurfaceRoughness + 2.0f;
|
const float RoughApplyWithin = R + P.SurfaceRoughness + 2.0f;
|
||||||
|
|
||||||
OutStack.Add(MakeConstantRockSource(P.BaseDensity));
|
OutStack.Add(MakeConstantRockSource(P.BaseDensity));
|
||||||
|
|||||||
@@ -218,11 +218,6 @@ namespace VoxelDensityProfile
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void SetEnabled(bool bEnabled)
|
|
||||||
{
|
|
||||||
SetMode(bEnabled ? EMode::Sampled : EMode::Disabled, DefaultSampleInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SetMode(EMode Mode, uint32 SampleInterval)
|
void SetMode(EMode Mode, uint32 SampleInterval)
|
||||||
{
|
{
|
||||||
if (SampleInterval == 0)
|
if (SampleInterval == 0)
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ void FVoxelForgeModule::StartupModule()
|
|||||||
RequestedFma3,
|
RequestedFma3,
|
||||||
Fma3After);
|
Fma3After);
|
||||||
#endif
|
#endif
|
||||||
// These parsers used to be entered lazily by worker-side density/mesher calls. Resolve the
|
// Resolve the command line once while the module is starting, before any generation task is
|
||||||
// command line once while the module is starting, before any generation task is submitted.
|
// submitted, instead of lazily from worker-side density/mesher calls.
|
||||||
VoxelGenLOD::InitializeConsoleSwitches();
|
VoxelGenLOD::InitializeConsoleSwitches();
|
||||||
UVoxelMarchingCubesMesher::InitializeConsoleSwitches();
|
UVoxelMarchingCubesMesher::InitializeConsoleSwitches();
|
||||||
UE_LOG(LogTemp, Log, TEXT("VoxelForge module started!"));
|
UE_LOG(LogTemp, Log, TEXT("VoxelForge module started!"));
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -91,8 +91,7 @@ void UVoxelMarchingCubesMesher::InitializeConsoleSwitches()
|
|||||||
//=============================================================================
|
//=============================================================================
|
||||||
// MAIN ALGORITHM
|
// MAIN ALGORITHM
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// (L'ancien trio GetDensity / InterpolateEdge / ComputeGradientNormal a été retiré :
|
// La grille pré-échantillonnée fournit positions ET gradients.
|
||||||
// mort depuis T1.b — la grille pré-échantillonnée fournit positions ET gradients.)
|
|
||||||
|
|
||||||
FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, int32 Step, int32 InCellsPerAxis,
|
FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, int32 Step, int32 InCellsPerAxis,
|
||||||
TArray<uint8>* OutCaptureGrid,
|
TArray<uint8>* OutCaptureGrid,
|
||||||
@@ -141,8 +140,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
//=========================================================================
|
//=========================================================================
|
||||||
// Le discriminant est SÉMANTIQUE, pas géométrique (fable-idea F17) : un vertex orienté vers
|
// Le discriminant est SÉMANTIQUE, pas géométrique (fable-idea F17) : un vertex orienté vers
|
||||||
// le bas est un sky-cap seulement s'il est proche de CeilSurf ; proche de TerrainZ c'est un
|
// le bas est un sky-cap seulement s'il est proche de CeilSurf ; proche de TerrainZ c'est un
|
||||||
// surplomb de terrain (reste "sol" — l'ancien vote par tuile mettait le matériau ciel sous
|
// surplomb de terrain (reste "sol" — un vote par tuile mettrait le matériau ciel sous les
|
||||||
// les surplombs des tuiles majoritairement plafond). Un futur toit de grotte (aussi down-
|
// surplombs des tuiles majoritairement plafond). Un futur toit de grotte (aussi down-
|
||||||
// facing, mais SOUS TerrainZ) tombera correctement côté "sol/roche" par la même règle.
|
// facing, mais SOUS TerrainZ) tombera correctement côté "sol/roche" par la même règle.
|
||||||
// Coût : GetSurfaceHeightAt (pile XY complète) UNIQUEMENT pour les vertex down-facing,
|
// Coût : GetSurfaceHeightAt (pile XY complète) UNIQUEMENT pour les vertex down-facing,
|
||||||
// mémoïsé par colonne quantifiée au pas de la grille → ≤ (colonnes touchées) appels ;
|
// mémoïsé par colonne quantifiée au pas de la grille → ≤ (colonnes touchées) appels ;
|
||||||
@@ -1126,218 +1125,3 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
|
|
||||||
return MeshData;
|
return MeshData;
|
||||||
}
|
}
|
||||||
|
|
||||||
//=============================================================================
|
|
||||||
// F18 — FEUILLE DE CHAMP LOINTAIN (voir le doc de la déclaration dans le .h)
|
|
||||||
//=============================================================================
|
|
||||||
// Deux grilles déplacées (sol + cap) au lieu d'un marching cubes 3D. Conventions IDENTIQUES à
|
|
||||||
// GenerateMesh : positions monde cm, UVs planaires (voxels), masques F6 par GetBiomeMaterialAt,
|
|
||||||
// jupes double-face extrudées le long de −N par seau, indices sol‖cap + NumCeilingTriangles.
|
|
||||||
// Winding : le chemin MC émet (0,2,1) des tables Bourke ⇒ une surface orientée +Z s'écrit
|
|
||||||
// (x,y) → (x,y+1) → (x+1,y) ; le cap (orienté −Z) inverse.
|
|
||||||
FVoxelMeshData UVoxelMarchingCubesMesher::GenerateSheetMesh(FIntVector OriginVoxels, int32 StepXY,
|
|
||||||
int32 CellsXY, int32 StrateChunkZ,
|
|
||||||
int32 HoleMinXVox, int32 HoleMinYVox,
|
|
||||||
int32 HoleMaxXVox, int32 HoleMaxYVox)
|
|
||||||
{
|
|
||||||
FVoxelMeshData MeshData;
|
|
||||||
if (!Generator) return MeshData;
|
|
||||||
StepXY = FMath::Max(1, StepXY);
|
|
||||||
CellsXY = FMath::Clamp(CellsXY, 2, 512);
|
|
||||||
|
|
||||||
// TROU XY — cellule [x0, x0+Step]×[y0, y0+Step] entièrement dans le rectangle (Max exclusif)
|
|
||||||
// ⇒ sautée : la zone est couverte par les coquilles MC (voir VoxelWorld.h SheetHole*).
|
|
||||||
auto CellInHole = [&](int32 cx, int32 cy) -> bool
|
|
||||||
{
|
|
||||||
const int32 X0 = OriginVoxels.X + cx * StepXY;
|
|
||||||
const int32 Y0 = OriginVoxels.Y + cy * StepXY;
|
|
||||||
return X0 >= HoleMinXVox && X0 + StepXY <= HoleMaxXVox
|
|
||||||
&& Y0 >= HoleMinYVox && Y0 + StepXY <= HoleMaxYVox;
|
|
||||||
};
|
|
||||||
|
|
||||||
const int32 GridDim = CellsXY + 1; // points de grille par axe
|
|
||||||
const int32 MDim = GridDim + 2; // + anneau de marge ±1 (normales continues entre feuilles)
|
|
||||||
|
|
||||||
// Early-out : la validité SurfaceWorld ne dépend que de ChunkZ (les strates sont des couches
|
|
||||||
// horizontales) — une sonde suffit. La sentinelle par colonne plus bas reste par prudence.
|
|
||||||
{
|
|
||||||
float T0 = 0.0f, C0 = 0.0f;
|
|
||||||
if (!Generator->GetSurfaceHeightAt((float)OriginVoxels.X, (float)OriginVoxels.Y,
|
|
||||||
StrateChunkZ, T0, C0))
|
|
||||||
{
|
|
||||||
return MeshData; // pas une strate SurfaceWorld → feuille vide
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Colonnes (TerrainZ, CeilSurf) en Z-voxel monde absolu, marge incluse. X==FLT_MAX ⇒ invalide.
|
|
||||||
static thread_local TArray<FVector2f> SheetCols;
|
|
||||||
SheetCols.SetNumUninitialized(MDim * MDim);
|
|
||||||
for (int32 gy = -1; gy <= GridDim; ++gy)
|
|
||||||
{
|
|
||||||
if (ShouldAbortWork()) return FVoxelMeshData();
|
|
||||||
for (int32 gx = -1; gx <= GridDim; ++gx)
|
|
||||||
{
|
|
||||||
float Tz = 0.0f, Cz = 0.0f;
|
|
||||||
const bool bOk = Generator->GetSurfaceHeightAt(
|
|
||||||
(float)(OriginVoxels.X + gx * StepXY), (float)(OriginVoxels.Y + gy * StepXY),
|
|
||||||
StrateChunkZ, Tz, Cz);
|
|
||||||
SheetCols[(gy + 1) * MDim + (gx + 1)] = bOk ? FVector2f(Tz, Cz)
|
|
||||||
: FVector2f(FLT_MAX, -FLT_MAX);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
auto Col = [&](int32 gx, int32 gy) -> const FVector2f&
|
|
||||||
{
|
|
||||||
return SheetCols[(gy + 1) * MDim + (gx + 1)];
|
|
||||||
};
|
|
||||||
|
|
||||||
// Index de vertex par point de grille (−1 = pas créé), sol et cap séparés — le partage de
|
|
||||||
// vertex est structurel (grille régulière), pas besoin de map de déduplication.
|
|
||||||
static thread_local TArray<int32> GroundIdx;
|
|
||||||
static thread_local TArray<int32> CapIdx;
|
|
||||||
GroundIdx.Init(-1, GridDim * GridDim);
|
|
||||||
CapIdx.Init(-1, GridDim * GridDim);
|
|
||||||
|
|
||||||
auto MakeVert = [&](int32 gx, int32 gy, bool bCap) -> int32
|
|
||||||
{
|
|
||||||
int32& Slot = (bCap ? CapIdx : GroundIdx)[gy * GridDim + gx];
|
|
||||||
if (Slot >= 0) return Slot;
|
|
||||||
|
|
||||||
const FVector2f C = Col(gx, gy);
|
|
||||||
const float H = bCap ? C.Y : C.X;
|
|
||||||
// Pente par différences centrales sur la grille de hauteurs (voxels/voxels — l'échelle cm
|
|
||||||
// se simplifie). Voisin invalide ⇒ composante plate (ne devrait pas arriver : cf. early-out).
|
|
||||||
auto HAt = [&](int32 x, int32 y) -> float
|
|
||||||
{
|
|
||||||
const FVector2f& N = Col(x, y);
|
|
||||||
return (N.X == FLT_MAX) ? H : (bCap ? N.Y : N.X);
|
|
||||||
};
|
|
||||||
const float Sx = (HAt(gx + 1, gy) - HAt(gx - 1, gy)) / (2.0f * (float)StepXY);
|
|
||||||
const float Sy = (HAt(gx, gy + 1) - HAt(gx, gy - 1)) / (2.0f * (float)StepXY);
|
|
||||||
// Sol : air au-dessus ⇒ normale vers le haut. Cap : l'air (l'intérieur de la strate) est
|
|
||||||
// EN-DESSOUS ⇒ normale vers le bas (−∇(z − C) = (Cx, Cy, −1)).
|
|
||||||
FVector Normal = bCap ? FVector(Sx, Sy, -1.0f) : FVector(-Sx, -Sy, 1.0f);
|
|
||||||
if (!Normal.Normalize())
|
|
||||||
{
|
|
||||||
Normal = FVector(0.0f, 0.0f, bCap ? -1.0f : 1.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
const float Xv = (float)(OriginVoxels.X + gx * StepXY);
|
|
||||||
const float Yv = (float)(OriginVoxels.Y + gy * StepXY);
|
|
||||||
const FVector WorldPos(Xv * VOXEL_SIZE, Yv * VOXEL_SIZE, H * VOXEL_SIZE);
|
|
||||||
|
|
||||||
Slot = MeshData.Vertices.Num();
|
|
||||||
MeshData.Vertices.Add(WorldPos);
|
|
||||||
MeshData.Normals.Add(Normal);
|
|
||||||
MeshData.UVs.Add(FVector2D(Xv, Yv)); // == WorldPos.XY / VOXEL_SIZE (convention MC)
|
|
||||||
|
|
||||||
// Mêmes masques F6 que GenerateMesh (palette biome dominant/voisin, pente, fondu).
|
|
||||||
int32 PalD = 0, PalN = 0; float BlendW = 0.0f;
|
|
||||||
Generator->GetBiomeMaterialAt(Xv, Yv, H, PalD, PalN, BlendW);
|
|
||||||
const uint8 Rr = (uint8)FMath::Clamp(PalD, 0, 255);
|
|
||||||
const uint8 Aa = (uint8)FMath::Clamp(PalN, 0, 255);
|
|
||||||
const uint8 Gg = (uint8)FMath::Clamp(FMath::RoundToInt((1.0f - FMath::Abs((float)Normal.Z)) * 255.0f), 0, 255);
|
|
||||||
const uint8 Bb = (uint8)FMath::Clamp(FMath::RoundToInt(BlendW * 255.0f), 0, 255);
|
|
||||||
MeshData.Colors.Add(FColor(Rr, Gg, Bb, Aa));
|
|
||||||
|
|
||||||
return Slot;
|
|
||||||
};
|
|
||||||
|
|
||||||
static thread_local TArray<int32> SheetGroundTris;
|
|
||||||
static thread_local TArray<int32> SheetCapTris;
|
|
||||||
SheetGroundTris.Reset();
|
|
||||||
SheetCapTris.Reset();
|
|
||||||
|
|
||||||
for (int32 cy = 0; cy < CellsXY; ++cy)
|
|
||||||
{
|
|
||||||
if (ShouldAbortWork()) return FVoxelMeshData();
|
|
||||||
for (int32 cx = 0; cx < CellsXY; ++cx)
|
|
||||||
{
|
|
||||||
if (CellInHole(cx, cy)) continue; // couverte par les coquilles MC proches
|
|
||||||
if (Col(cx, cy).X == FLT_MAX || Col(cx + 1, cy).X == FLT_MAX ||
|
|
||||||
Col(cx, cy + 1).X == FLT_MAX || Col(cx + 1, cy + 1).X == FLT_MAX)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// SOL (polygroup 0) — orienté +Z.
|
|
||||||
const int32 g00 = MakeVert(cx, cy, false);
|
|
||||||
const int32 g10 = MakeVert(cx + 1, cy, false);
|
|
||||||
const int32 g01 = MakeVert(cx, cy + 1, false);
|
|
||||||
const int32 g11 = MakeVert(cx + 1, cy + 1, false);
|
|
||||||
SheetGroundTris.Add(g00); SheetGroundTris.Add(g01); SheetGroundTris.Add(g10);
|
|
||||||
SheetGroundTris.Add(g10); SheetGroundTris.Add(g01); SheetGroundTris.Add(g11);
|
|
||||||
|
|
||||||
// CAP (polygroup 1) — orienté −Z ⇒ winding inversé.
|
|
||||||
const int32 c00 = MakeVert(cx, cy, true);
|
|
||||||
const int32 c10 = MakeVert(cx + 1, cy, true);
|
|
||||||
const int32 c01 = MakeVert(cx, cy + 1, true);
|
|
||||||
const int32 c11 = MakeVert(cx + 1, cy + 1, true);
|
|
||||||
SheetCapTris.Add(c00); SheetCapTris.Add(c10); SheetCapTris.Add(c01);
|
|
||||||
SheetCapTris.Add(c10); SheetCapTris.Add(c11); SheetCapTris.Add(c01);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// JUPES périmètre — mêmes règles que le chemin MC : extrusion le long de −N (vers le solide :
|
|
||||||
// bas pour le sol, HAUT pour le cap), double face, émises PAR SEAU (héritent le polygroup).
|
|
||||||
// Entre feuilles voisines les coins coïncident (mêmes échantillons monde) → pas de fissure ;
|
|
||||||
// la jupe couvre surtout la couture feuille ↔ anneau MC et les décalages de niveau.
|
|
||||||
if (bGenerateSkirts && (SheetGroundTris.Num() + SheetCapTris.Num()) > 0)
|
|
||||||
{
|
|
||||||
const float SkirtDepth = FMath::Max(1.0f, SkirtCells) * (float)StepXY * VOXEL_SIZE;
|
|
||||||
|
|
||||||
auto EmitEdgeSkirt = [&](int32 ax, int32 ay, int32 bx, int32 by, bool bCap, TArray<int32>& Tris)
|
|
||||||
{
|
|
||||||
if (Col(ax, ay).X == FLT_MAX || Col(bx, by).X == FLT_MAX) return;
|
|
||||||
const int32 iA = MakeVert(ax, ay, bCap);
|
|
||||||
const int32 iB = MakeVert(bx, by, bCap);
|
|
||||||
// COPIES par valeur — les Add ci-dessous réallouent Vertices/Normals/Colors.
|
|
||||||
const FVector PA = MeshData.Vertices[iA], PB = MeshData.Vertices[iB];
|
|
||||||
const FVector NA = MeshData.Normals[iA], NB = MeshData.Normals[iB];
|
|
||||||
const FColor CA = MeshData.Colors[iA], CB = MeshData.Colors[iB];
|
|
||||||
|
|
||||||
const int32 iA2 = MeshData.Vertices.Num();
|
|
||||||
MeshData.Vertices.Add(PA - NA * SkirtDepth);
|
|
||||||
MeshData.Normals.Add(NA);
|
|
||||||
MeshData.UVs.Add(FVector2D(PA.X / VOXEL_SIZE, PA.Y / VOXEL_SIZE));
|
|
||||||
MeshData.Colors.Add(CA);
|
|
||||||
const int32 iB2 = MeshData.Vertices.Num();
|
|
||||||
MeshData.Vertices.Add(PB - NB * SkirtDepth);
|
|
||||||
MeshData.Normals.Add(NB);
|
|
||||||
MeshData.UVs.Add(FVector2D(PB.X / VOXEL_SIZE, PB.Y / VOXEL_SIZE));
|
|
||||||
MeshData.Colors.Add(CB);
|
|
||||||
|
|
||||||
Tris.Add(iA); Tris.Add(iB); Tris.Add(iB2);
|
|
||||||
Tris.Add(iA); Tris.Add(iB2); Tris.Add(iA2);
|
|
||||||
Tris.Add(iA); Tris.Add(iB2); Tris.Add(iB);
|
|
||||||
Tris.Add(iA); Tris.Add(iA2); Tris.Add(iB2);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Une jupe n'est émise que si la cellule de bord adjacente a réellement été maillée
|
|
||||||
// (pas dans le trou XY) — sinon mur flottant sans surface.
|
|
||||||
for (int32 c = 0; c < CellsXY; ++c)
|
|
||||||
{
|
|
||||||
if (ShouldAbortWork()) return FVoxelMeshData();
|
|
||||||
for (int32 Pass = 0; Pass < 2; ++Pass)
|
|
||||||
{
|
|
||||||
const bool bCap = (Pass == 1);
|
|
||||||
TArray<int32>& Tris = bCap ? SheetCapTris : SheetGroundTris;
|
|
||||||
if (!CellInHole(c, 0))
|
|
||||||
EmitEdgeSkirt(c, 0, c + 1, 0, bCap, Tris); // bord Y-min
|
|
||||||
if (!CellInHole(c, CellsXY - 1))
|
|
||||||
EmitEdgeSkirt(c, CellsXY, c + 1, CellsXY, bCap, Tris); // bord Y-max
|
|
||||||
if (!CellInHole(0, c))
|
|
||||||
EmitEdgeSkirt(0, c, 0, c + 1, bCap, Tris); // bord X-min
|
|
||||||
if (!CellInHole(CellsXY - 1, c))
|
|
||||||
EmitEdgeSkirt(CellsXY, c, CellsXY, c + 1, bCap, Tris); // bord X-max
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Concatène sol PUIS cap : un run d'indices contigu par polygroup (contrat F17/BuildTileStreamSet).
|
|
||||||
MeshData.Triangles.Reserve(SheetGroundTris.Num() + SheetCapTris.Num());
|
|
||||||
MeshData.Triangles.Append(SheetGroundTris);
|
|
||||||
MeshData.Triangles.Append(SheetCapTris);
|
|
||||||
MeshData.NumCeilingTriangles = SheetCapTris.Num() / 3;
|
|
||||||
|
|
||||||
return MeshData;
|
|
||||||
}
|
|
||||||
@@ -1410,59 +1410,6 @@ namespace VoxelSeasonManifestPrivate
|
|||||||
return bValid;
|
return bValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SaveJson(const FString& FilePath, const TSharedPtr<FJsonObject>& Root,
|
|
||||||
FString& OutReport)
|
|
||||||
{
|
|
||||||
const FString Directory = FPaths::GetPath(FilePath);
|
|
||||||
if (!Directory.IsEmpty() && !IFileManager::Get().MakeDirectory(*Directory, true))
|
|
||||||
{
|
|
||||||
OutReport = FString::Printf(TEXT("could not create manifest directory %s"), *Directory);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
FString JsonText;
|
|
||||||
TSharedRef<TJsonWriter<TCHAR, TPrettyJsonPrintPolicy<TCHAR>>> Writer =
|
|
||||||
TJsonWriterFactory<TCHAR, TPrettyJsonPrintPolicy<TCHAR>>::Create(&JsonText);
|
|
||||||
if (!FJsonSerializer::Serialize(Root.ToSharedRef(), Writer) || !Writer->Close()
|
|
||||||
|| !FFileHelper::SaveStringToFile(JsonText, *FilePath))
|
|
||||||
{
|
|
||||||
OutReport = FString::Printf(TEXT("could not write season manifest %s"), *FilePath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool LoadJson(const FString& FilePath, TSharedPtr<FJsonObject>& OutRoot,
|
|
||||||
FString& OutReport, FString* OutJsonText = nullptr)
|
|
||||||
{
|
|
||||||
FString JsonText;
|
|
||||||
if (!FFileHelper::LoadFileToString(JsonText, *FilePath))
|
|
||||||
{
|
|
||||||
OutReport = FString::Printf(TEXT("could not read season manifest %s"), *FilePath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonText);
|
|
||||||
if (!FJsonSerializer::Deserialize(Reader, OutRoot) || !OutRoot.IsValid())
|
|
||||||
{
|
|
||||||
OutReport = FString::Printf(TEXT("invalid JSON in season manifest %s"), *FilePath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (OutJsonText != nullptr)
|
|
||||||
{
|
|
||||||
*OutJsonText = MoveTemp(JsonText);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const TCHAR* VF_GetVoxelSeasonSelectionReasonName(EVoxelSeasonSelectionReason Reason)
|
|
||||||
{
|
|
||||||
switch (Reason)
|
|
||||||
{
|
|
||||||
case EVoxelSeasonSelectionReason::Grounded: return TEXT("grounded");
|
|
||||||
case EVoxelSeasonSelectionReason::Outlier: return TEXT("outlier");
|
|
||||||
case EVoxelSeasonSelectionReason::Fixed: return TEXT("fixed");
|
|
||||||
}
|
|
||||||
return TEXT("unknown");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
|
|||||||
@@ -863,12 +863,6 @@ void FVoxelStackSampler::Shutdown()
|
|||||||
StopAndWrite();
|
StopAndWrite();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FVoxelStackSampler::IsRunning() const
|
|
||||||
{
|
|
||||||
FScopeLock LifecycleLock(&LifecycleMutex);
|
|
||||||
return Impl != nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool FVoxelStackSampler::RegisterCurrentThread(int32 LODLevel)
|
bool FVoxelStackSampler::RegisterCurrentThread(int32 LODLevel)
|
||||||
{
|
{
|
||||||
// Keep this as the first and only operation in the disabled fast path. In particular, do not
|
// Keep this as the first and only operation in the disabled fast path. In particular, do not
|
||||||
|
|||||||
@@ -10,10 +10,6 @@ DEFINE_STAT(STAT_VoxelForgeTilesSkippedAllAir);
|
|||||||
DEFINE_STAT(STAT_VoxelForgeTilesMeshed);
|
DEFINE_STAT(STAT_VoxelForgeTilesMeshed);
|
||||||
DEFINE_STAT(STAT_VoxelForgeTilesOpStackSolid);
|
DEFINE_STAT(STAT_VoxelForgeTilesOpStackSolid);
|
||||||
DEFINE_STAT(STAT_VoxelForgeTilesOpStackAir);
|
DEFINE_STAT(STAT_VoxelForgeTilesOpStackAir);
|
||||||
DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackSoleSlot);
|
|
||||||
DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackBoundaryTile);
|
|
||||||
DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackNoLayout);
|
|
||||||
DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackRecheck);
|
|
||||||
DEFINE_STAT(STAT_VoxelForgeCaveMixOutOfLayout);
|
DEFINE_STAT(STAT_VoxelForgeCaveMixOutOfLayout);
|
||||||
DEFINE_STAT(STAT_VoxelForgeCaveMixGap);
|
DEFINE_STAT(STAT_VoxelForgeCaveMixGap);
|
||||||
DEFINE_STAT(STAT_VoxelForgeCaveMixSurfaceWorld);
|
DEFINE_STAT(STAT_VoxelForgeCaveMixSurfaceWorld);
|
||||||
|
|||||||
@@ -3800,55 +3800,6 @@ bool FVoxelStrateCorpus::LoadFromAssetRegistry(
|
|||||||
return bLoaded && NumUnresolved == 0 && bPromotedLoaded;
|
return bLoaded && NumUnresolved == 0 && bPromotedLoaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FVoxelStrateCorpus::LoadFromSettings(const UVoxelSettings* Settings, FString& OutReport)
|
|
||||||
{
|
|
||||||
// Compatibility/audit entry point. It intentionally does not load any soft reference: the
|
|
||||||
// Asset Registry is the corpus source, while the settings pool is reported as the reason the
|
|
||||||
// first Tier 4a run saw only one vector.
|
|
||||||
TArray<FString> SettingsPaths;
|
|
||||||
if (Settings != nullptr)
|
|
||||||
{
|
|
||||||
TSet<FString> UniquePaths;
|
|
||||||
for (const TPair<int32, TSoftObjectPtr<UVoxelStrateDefinition>>& Pair : Settings->FixedStrates)
|
|
||||||
{
|
|
||||||
const FString Path = Pair.Value.ToSoftObjectPath().ToString();
|
|
||||||
if (!Path.IsEmpty())
|
|
||||||
{
|
|
||||||
UniquePaths.Add(Path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const TSoftObjectPtr<UVoxelStrateDefinition>& Reference : Settings->StratePool)
|
|
||||||
{
|
|
||||||
const FString Path = Reference.ToSoftObjectPath().ToString();
|
|
||||||
if (!Path.IsEmpty())
|
|
||||||
{
|
|
||||||
UniquePaths.Add(Path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const FString& Path : UniquePaths)
|
|
||||||
{
|
|
||||||
SettingsPaths.Add(Path);
|
|
||||||
}
|
|
||||||
SettingsPaths.Sort();
|
|
||||||
}
|
|
||||||
|
|
||||||
FString RegistryReport;
|
|
||||||
const bool bLoaded = LoadFromAssetRegistry(RegistryReport);
|
|
||||||
OutReport = FString::Printf(
|
|
||||||
TEXT("Settings audit: %d unique fixed/pool soft paths"), SettingsPaths.Num());
|
|
||||||
for (const FString& Path : SettingsPaths)
|
|
||||||
{
|
|
||||||
OutReport += FString::Printf(TEXT(" [%s]"), *Path);
|
|
||||||
}
|
|
||||||
if (Settings == nullptr)
|
|
||||||
{
|
|
||||||
OutReport += TEXT(" [settings object was null]");
|
|
||||||
}
|
|
||||||
OutReport += TEXT("; settings references are diagnostic only. ");
|
|
||||||
OutReport += RegistryReport;
|
|
||||||
return bLoaded;
|
|
||||||
}
|
|
||||||
|
|
||||||
int32 FVoxelStrateCorpus::NumForArchetype(ECaveGeneratorType Archetype) const
|
int32 FVoxelStrateCorpus::NumForArchetype(ECaveGeneratorType Archetype) const
|
||||||
{
|
{
|
||||||
int32 Count = 0;
|
int32 Count = 0;
|
||||||
@@ -4921,7 +4872,7 @@ FVoxelStrateRollInfo VF_RollStrateParamsDetailed(const FVoxelStrateCorpus& Corpu
|
|||||||
Repeat.ArchetypeParams, Result.ArchetypeParams, Result.Archetype)
|
Repeat.ArchetypeParams, Result.ArchetypeParams, Result.Archetype)
|
||||||
&& Repeat.ParentEntryIndices == Result.ParentEntryIndices
|
&& Repeat.ParentEntryIndices == Result.ParentEntryIndices
|
||||||
&& Repeat.ParentWeights == Result.ParentWeights,
|
&& Repeat.ParentWeights == Result.ParentWeights,
|
||||||
TEXT("VF_RollStrateParams lost determinism for the same corpus/seed/index."));
|
TEXT("VF_RollStrateParamsDetailed lost determinism for the same corpus/seed/index."));
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
return Result;
|
return Result;
|
||||||
@@ -5301,12 +5252,6 @@ bool VF_ValidateStrateCorpusFreeConstraints(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
FStrateGenerationParams VF_RollStrateParams(const FVoxelStrateCorpus& Corpus,
|
|
||||||
int32 Seed, int32 Index)
|
|
||||||
{
|
|
||||||
return VF_RollStrateParamsDetailed(Corpus, Seed, Index).Params;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
const TCHAR* VF_RecipeOpCode(EVoxelStrateOpClass OpClass)
|
const TCHAR* VF_RecipeOpCode(EVoxelStrateOpClass OpClass)
|
||||||
@@ -5532,15 +5477,6 @@ FVoxelStrateRegionManifest VF_RollStrateRegionManifest(
|
|||||||
ECaveGeneratorType::VerticalShafts,
|
ECaveGeneratorType::VerticalShafts,
|
||||||
ECaveGeneratorType::FloatingIslands,
|
ECaveGeneratorType::FloatingIslands,
|
||||||
};
|
};
|
||||||
static const EVoxelStrateParamBlock BlockIds[] =
|
|
||||||
{
|
|
||||||
EVoxelStrateParamBlock::TunnelNetwork,
|
|
||||||
EVoxelStrateParamBlock::Slab,
|
|
||||||
EVoxelStrateParamBlock::Maze,
|
|
||||||
EVoxelStrateParamBlock::Surface,
|
|
||||||
EVoxelStrateParamBlock::VerticalShaft,
|
|
||||||
EVoxelStrateParamBlock::FloatingIsland,
|
|
||||||
};
|
|
||||||
static const uint32 BlockSalts[] = { 0x1001u, 0x1003u, 0x1005u,
|
static const uint32 BlockSalts[] = { 0x1001u, 0x1003u, 0x1005u,
|
||||||
0x1007u, 0x1009u, 0x100Bu };
|
0x1007u, 0x1009u, 0x100Bu };
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
// Runtime strate layout generation and queries.
|
// Runtime strate layout generation and queries.
|
||||||
|
|
||||||
#include "VoxelStrateManager.h"
|
#include "VoxelStrateManager.h"
|
||||||
#include "CoreGlobals.h" // GIsAutomationTesting — the opt-in diagnostic stays quiet under tests
|
|
||||||
#include "VoxelSettings.h"
|
#include "VoxelSettings.h"
|
||||||
#include "VoxelSeasonAsset.h"
|
#include "VoxelSeasonAsset.h"
|
||||||
#include "VoxelTypes.h" // For CHUNK_SIZE, VOXEL_SIZE, WorldToChunkCoord
|
#include "VoxelTypes.h" // For CHUNK_SIZE, VOXEL_SIZE, WorldToChunkCoord
|
||||||
@@ -234,8 +233,8 @@ namespace
|
|||||||
{
|
{
|
||||||
// One projection result per passage is enough for the current voxel sample. The surrounding
|
// One projection result per passage is enough for the current voxel sample. The surrounding
|
||||||
// passage cache already moves with (manager, layout, chunk); a generation invalidates old flags
|
// passage cache already moves with (manager, layout, chunk); a generation invalidates old flags
|
||||||
// before any caller can observe them. Keeping the entries indexed by passage avoids the TMap
|
// before any caller can observe them. Keeping the entries indexed by passage avoids a TMap
|
||||||
// rehash path that this hot loop used to carry, while a collision-free hit remains exact.
|
// rehash path in this hot loop, while a collision-free hit remains exact.
|
||||||
struct FPassageFloorProjectionCacheEntry
|
struct FPassageFloorProjectionCacheEntry
|
||||||
{
|
{
|
||||||
// The sample key lives once in FPassageEvaluationCache. A monotonically increasing
|
// The sample key lives once in FPassageEvaluationCache. A monotonically increasing
|
||||||
@@ -1334,7 +1333,6 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
|||||||
}
|
}
|
||||||
|
|
||||||
Slot.Definition->GeneratorType = SeasonStrate.Archetype;
|
Slot.Definition->GeneratorType = SeasonStrate.Archetype;
|
||||||
Slot.Definition->bUseOperatorStack = SeasonStrate.bUsesRecipe;
|
|
||||||
Slot.Definition->StrateHeightInChunks = SeasonStrate.HeightInChunks;
|
Slot.Definition->StrateHeightInChunks = SeasonStrate.HeightInChunks;
|
||||||
Slot.Definition->GenerationParams = SeasonStrate.Params.TunnelNetworkParams;
|
Slot.Definition->GenerationParams = SeasonStrate.Params.TunnelNetworkParams;
|
||||||
Slot.Definition->SlabParams = SeasonStrate.Params.SlabParams;
|
Slot.Definition->SlabParams = SeasonStrate.Params.SlabParams;
|
||||||
@@ -1401,10 +1399,9 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
|||||||
: FString::Printf(TEXT("Value_%d"), static_cast<int32>(Slot.Definition->GeneratorType));
|
: FString::Printf(TEXT("Value_%d"), static_cast<int32>(Slot.Definition->GeneratorType));
|
||||||
VoxelForgeStartupTrace::RecordEvent(TEXT("strate_slot"), FString::Printf(
|
VoxelForgeStartupTrace::RecordEvent(TEXT("strate_slot"), FString::Printf(
|
||||||
TEXT("\"index\":%d,\"definition\":\"%s\",\"archetype\":\"%s\",\"top_chunk_z\":%d,"
|
TEXT("\"index\":%d,\"definition\":\"%s\",\"archetype\":\"%s\",\"top_chunk_z\":%d,"
|
||||||
"\"bottom_chunk_z\":%d,\"height_chunks\":%d,\"operator_stack\":%d"),
|
"\"bottom_chunk_z\":%d,\"height_chunks\":%d"),
|
||||||
Slot.StrateIndex, *DefinitionPath, *ArchetypeName,
|
Slot.StrateIndex, *DefinitionPath, *ArchetypeName,
|
||||||
Slot.TopChunkZ, Slot.BottomChunkZ, Slot.HeightInChunks,
|
Slot.TopChunkZ, Slot.BottomChunkZ, Slot.HeightInChunks));
|
||||||
Slot.Definition->bUseOperatorStack ? 1 : 0));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("[StrateManager] Strate %d: '%s' | Z chunks [%d to %d] | %d chunks tall"),
|
UE_LOG(LogTemp, Log, TEXT("[StrateManager] Strate %d: '%s' | Z chunks [%d to %d] | %d chunks tall"),
|
||||||
@@ -1415,79 +1412,6 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
|||||||
Slot.HeightInChunks);
|
Slot.HeightInChunks);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Diagnostic de configuration, une seule fois par construction de layout. SurfaceWorld est
|
|
||||||
// volontairement exclu : son chemin T1.d exact-lattice ne dépend pas de ce drapeau.
|
|
||||||
// Configuration diagnostic once per layout build. SurfaceWorld is deliberately excluded:
|
|
||||||
// its exact-lattice T1.d path does not depend on this flag.
|
|
||||||
int32 NumCaveSlots = 0;
|
|
||||||
int32 NumOperatorStackDisabledCaves = 0;
|
|
||||||
for (const FStrateSlot& Slot : StrateLayout)
|
|
||||||
{
|
|
||||||
const ECaveGeneratorType SlotArchetype = bUseSeason
|
|
||||||
? SeasonStrates[Slot.StrateIndex].Archetype : Slot.Definition->GeneratorType;
|
|
||||||
if (!Slot.Definition || SlotArchetype == ECaveGeneratorType::SurfaceWorld)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
++NumCaveSlots;
|
|
||||||
const bool bUsesStack = bUseSeason
|
|
||||||
? SeasonStrates[Slot.StrateIndex].bUsesRecipe : Slot.Definition->bUseOperatorStack;
|
|
||||||
if (!bUsesStack)
|
|
||||||
{
|
|
||||||
++NumOperatorStackDisabledCaves;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ⚠️ WARNING EN ÉDITEUR/JEU, JAMAIS EN TEST. Les tests `Determinism.*` construisent
|
|
||||||
// DÉLIBÉRÉMENT un monde non opt-in — c'est leur oracle de comparaison — et le framework
|
|
||||||
// d'automatisation compte un Warning comme un échec. Un diagnostic ne doit pas casser la suite
|
|
||||||
// qu'il est censé éclairer. Le message reste écrit UNE fois : seule la verbosité change.
|
|
||||||
// Warning in editor/game where it is actionable, never in tests: the Determinism.* tests build
|
|
||||||
// a non-opted-in world ON PURPOSE as their comparison oracle, and the automation framework
|
|
||||||
// treats a Warning as a failure. One message, two verbosities.
|
|
||||||
const bool bQuietDiagnostic = GIsAutomationTesting;
|
|
||||||
|
|
||||||
if (NumOperatorStackDisabledCaves > 0)
|
|
||||||
{
|
|
||||||
const FString Summary = FString::Printf(
|
|
||||||
TEXT("[StrateManager] Operator-stack opt-in: %d/%d cave layout slots have Use Operator Stack disabled. These slots cannot use operator-stack ClassifyBox/T1.d; enable the asset setting on the listed definitions if that is intended."),
|
|
||||||
NumOperatorStackDisabledCaves, NumCaveSlots);
|
|
||||||
|
|
||||||
if (bQuietDiagnostic) { UE_LOG(LogTemp, Verbose, TEXT("%s"), *Summary); }
|
|
||||||
else { UE_LOG(LogTemp, Warning, TEXT("%s"), *Summary); }
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
UE_LOG(LogTemp, Log,
|
|
||||||
TEXT("[StrateManager] Operator-stack opt-in: all %d cave layout slots have Use Operator Stack enabled."),
|
|
||||||
NumCaveSlots);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const FStrateSlot& Slot : StrateLayout)
|
|
||||||
{
|
|
||||||
const bool bSeasonStack = bUseSeason
|
|
||||||
&& SeasonStrates[Slot.StrateIndex].bUsesRecipe;
|
|
||||||
const ECaveGeneratorType SlotArchetype = bUseSeason
|
|
||||||
? SeasonStrates[Slot.StrateIndex].Archetype : Slot.Definition->GeneratorType;
|
|
||||||
if (!Slot.Definition
|
|
||||||
|| SlotArchetype == ECaveGeneratorType::SurfaceWorld
|
|
||||||
|| (bUseSeason ? bSeasonStack : Slot.Definition->bUseOperatorStack))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FString Line = FString::Printf(
|
|
||||||
TEXT("[StrateManager] cave slot=%d name='%s' Z chunks=[%d to %d] bUseOperatorStack=false"),
|
|
||||||
Slot.StrateIndex,
|
|
||||||
*Slot.Definition->StrateName.ToString(),
|
|
||||||
Slot.TopChunkZ,
|
|
||||||
Slot.BottomChunkZ);
|
|
||||||
|
|
||||||
if (bQuietDiagnostic) { UE_LOG(LogTemp, Verbose, TEXT("%s"), *Line); }
|
|
||||||
else { UE_LOG(LogTemp, Warning, TEXT("%s"), *Line); }
|
|
||||||
}
|
|
||||||
|
|
||||||
CachedSeed = WorldSeed;
|
CachedSeed = WorldSeed;
|
||||||
bOpenSurfaceEntry = Settings->bOpenSurfaceEntry;
|
bOpenSurfaceEntry = Settings->bOpenSurfaceEntry;
|
||||||
OriginSpineRadius = bUseSeason
|
OriginSpineRadius = bUseSeason
|
||||||
@@ -1804,13 +1728,13 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
case ECaveGeneratorType::TunnelNetwork:
|
case ECaveGeneratorType::TunnelNetwork:
|
||||||
case ECaveGeneratorType::Underwater:
|
case ECaveGeneratorType::Underwater:
|
||||||
// Une salle est une CIBLE ÉPARSE en XY, exactement comme un couloir de labyrinthe : le
|
// Une salle est une CIBLE ÉPARSE en XY, exactement comme un couloir de labyrinthe : le
|
||||||
// budget doit donc être réel. Zéro ici était un bug — la requête trouvait la salle la
|
// budget doit donc être réel. Zéro ici est un bug — la requête trouve la salle la plus
|
||||||
// plus proche puis atterrissait à côté d'elle dans 92,8 % des cas (balayage d'un million
|
// proche puis atterrit à côté d'elle dans 92,8 % des cas (balayage d'un million de
|
||||||
// de seeds). Un espacement de salles est le voisinage local naturel.
|
// seeds). Un espacement de salles est le voisinage local naturel.
|
||||||
//
|
//
|
||||||
// A room is an XY-SPARSE target, exactly like a maze corridor, so the budget must be
|
// A room is an XY-SPARSE target, exactly like a maze corridor, so the budget must be
|
||||||
// real. Zero here was the bug: the query found the nearest room and then landed beside
|
// real. Zero here is a bug: the query finds the nearest room and then lands beside it
|
||||||
// it 92.8% of the time (million-seed sweep). One room spacing is the natural local
|
// 92.8% of the time (million-seed sweep). One room spacing is the natural local
|
||||||
// neighbourhood, and it protects the configured spine-distance distribution.
|
// neighbourhood, and it protects the configured spine-distance distribution.
|
||||||
return FMath::Max(Definition.GenerationParams.RoomSpacing, 1.0f);
|
return FMath::Max(Definition.GenerationParams.RoomSpacing, 1.0f);
|
||||||
|
|
||||||
@@ -2771,9 +2695,9 @@ float UVoxelStrateManager::EvaluateModifierSDF(float WorldX, float WorldY, float
|
|||||||
{
|
{
|
||||||
const FVoxelPassage& P = PassageData[PIdx];
|
const FVoxelPassage& P = PassageData[PIdx];
|
||||||
// BOUNDING-SPHERE REJECT: skip passages this voxel can't possibly be inside.
|
// BOUNDING-SPHERE REJECT: skip passages this voxel can't possibly be inside.
|
||||||
// EvaluateModifierSDF runs PER VOXEL and used to evaluate every passage's full
|
// EvaluateModifierSDF runs PER VOXEL; evaluating every passage's full capsule chain
|
||||||
// capsule chain unconditionally — the dominant lag source once passages became
|
// unconditionally is the dominant lag source with 12-segment worm passages. Far
|
||||||
// 12-segment worms. Now far passages cost a single squared-distance compare.
|
// passages cost a single squared-distance compare.
|
||||||
if (FVector::DistSquared(Pos, P.BoundCenter) > P.BoundRadiusSq) continue;
|
if (FVector::DistSquared(Pos, P.BoundCenter) > P.BoundRadiusSq) continue;
|
||||||
if (VoxelDensityProfile::AreCountersEnabled())
|
if (VoxelDensityProfile::AreCountersEnabled())
|
||||||
{
|
{
|
||||||
@@ -3023,20 +2947,6 @@ void UVoxelStrateManager::ApplyPassageLandingAir(
|
|||||||
VF_ApplyPassageLandingCarving(Density, MinLandingSDF, BaseDensity, SealThickness);
|
VF_ApplyPassageLandingCarving(Density, MinLandingSDF, BaseDensity, SealThickness);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UVoxelStrateManager::ApplyPassageLandingAirMC(
|
|
||||||
float& Density, float WorldX, float WorldY, float WorldZ,
|
|
||||||
float BaseDensity, float SealThickness) const
|
|
||||||
{
|
|
||||||
// ApplyDisturbances is deliberately an MC-space post-process and can add a bridge or ridge
|
|
||||||
// on top of the structural passage. Reassert only landing air here; the support slab is
|
|
||||||
// restored by ApplyPassageLandingFloorMC immediately afterwards. This stays on the same
|
|
||||||
// thread-local passage shortlist as the hot voxel path and performs no source/topology work.
|
|
||||||
float InternalDensity = -Density;
|
|
||||||
ApplyPassageLandingAir(
|
|
||||||
InternalDensity, WorldX, WorldY, WorldZ, BaseDensity, SealThickness);
|
|
||||||
Density = -InternalDensity;
|
|
||||||
}
|
|
||||||
|
|
||||||
void UVoxelStrateManager::ApplyPassageTunnelAir(
|
void UVoxelStrateManager::ApplyPassageTunnelAir(
|
||||||
float& Density, float WorldX, float WorldY, float WorldZ,
|
float& Density, float WorldX, float WorldY, float WorldZ,
|
||||||
float BaseDensity, float SealThickness) const
|
float BaseDensity, float SealThickness) const
|
||||||
@@ -3070,18 +2980,6 @@ void UVoxelStrateManager::ApplyPassageTunnelAir(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void UVoxelStrateManager::ApplyPassageTunnelAirMC(
|
|
||||||
float& Density, float WorldX, float WorldY, float WorldZ,
|
|
||||||
float BaseDensity, float SealThickness) const
|
|
||||||
{
|
|
||||||
// The disturbance layer uses MC polarity (negative = solid), so reuse the exact internal
|
|
||||||
// tunnel-air operation rather than maintaining a second polarity-specific formula.
|
|
||||||
float InternalDensity = -Density;
|
|
||||||
ApplyPassageTunnelAir(
|
|
||||||
InternalDensity, WorldX, WorldY, WorldZ, BaseDensity, SealThickness);
|
|
||||||
Density = -InternalDensity;
|
|
||||||
}
|
|
||||||
|
|
||||||
void UVoxelStrateManager::ApplyPassageLandingFloorMC(
|
void UVoxelStrateManager::ApplyPassageLandingFloorMC(
|
||||||
float& Density, float WorldX, float WorldY, float WorldZ, float BaseDensity) const
|
float& Density, float WorldX, float WorldY, float WorldZ, float BaseDensity) const
|
||||||
{
|
{
|
||||||
@@ -3280,8 +3178,8 @@ void UVoxelStrateManager::ApplyPassageStructuralPostsMC(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The graph tunnel no longer exposes a separate support slab. Inter-strate landing carving
|
// The graph tunnel does not expose a separate support slab. Inter-strate landing carving
|
||||||
// remains independent and is still composed here when its own reach proof allows it.
|
// is independent and is composed here when its own reach proof allows it.
|
||||||
if (!bDisableLandingPosts
|
if (!bDisableLandingPosts
|
||||||
&& !bAnyLandingFloor && !bLegacyTunnelSupportFloor && MinLandingSDF < FLT_MAX)
|
&& !bAnyLandingFloor && !bLegacyTunnelSupportFloor && MinLandingSDF < FLT_MAX)
|
||||||
{
|
{
|
||||||
@@ -4799,7 +4697,7 @@ bool UVoxelStrateManager::AnyPassageAirPostNearLattice(
|
|||||||
|
|
||||||
// The union above is conservative but can be much smaller than the queried box. Restrict the
|
// The union above is conservative but can be much smaller than the queried box. Restrict the
|
||||||
// exact walk to its lattice intersection; a false positive still pays the exact predicates,
|
// exact walk to its lattice intersection; a false positive still pays the exact predicates,
|
||||||
// while a far-away long-passage sphere no longer does.
|
// while a far-away long-passage sphere does not.
|
||||||
if (!VF_LatticeAxisRange(
|
if (!VF_LatticeAxisRange(
|
||||||
FMath::Max((float)VoxelBox.Min.X, PotentialMinX),
|
FMath::Max((float)VoxelBox.Min.X, PotentialMinX),
|
||||||
FMath::Min((float)VoxelBox.Max.X, PotentialMaxX),
|
FMath::Min((float)VoxelBox.Max.X, PotentialMaxX),
|
||||||
@@ -6307,65 +6205,42 @@ bool UVoxelStrateManager::UsesOperatorStackForChunk(const FIntVector& ChunkCoord
|
|||||||
const int32 SlotIdx = FindSlotIndexForChunkZ(ChunkCoord.Z);
|
const int32 SlotIdx = FindSlotIndexForChunkZ(ChunkCoord.Z);
|
||||||
if (SlotIdx < 0 || !StrateLayout[SlotIdx].Definition) { return false; }
|
if (SlotIdx < 0 || !StrateLayout[SlotIdx].Definition) { return false; }
|
||||||
|
|
||||||
if (SeasonStrates.IsValidIndex(SlotIdx))
|
// Every strate is generated by the operator stack: authored definitions, cooked-season entries
|
||||||
{
|
// (recipe or native) and editor composer candidates alike. The slot definition carries the
|
||||||
// A recipe is an explicit stack opt-in. Native fixed entries retain the authored/switch
|
// archetype in every case.
|
||||||
// route while still reading their season parameter vector.
|
|
||||||
return SeasonStrates[SlotIdx].bUsesRecipe;
|
|
||||||
}
|
|
||||||
|
|
||||||
#if WITH_EDITOR
|
|
||||||
if (FindComposerOverride(StrateLayout[SlotIdx].StrateIndex) != nullptr)
|
|
||||||
{
|
|
||||||
// Temporary composer candidates are measured through the stack path. Do not inherit an
|
|
||||||
// unrelated asset flag from the slot being replaced.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
const UVoxelStrateDefinition* Def = StrateLayout[SlotIdx].Definition;
|
const UVoxelStrateDefinition* Def = StrateLayout[SlotIdx].Definition;
|
||||||
if (!Def->bUseOperatorStack) { return false; }
|
|
||||||
|
|
||||||
// LA LISTE DES ARCHÉTYPES PORTÉS — le seul endroit où elle est écrite. Un archétype non porté
|
// LA LISTE DES ARCHÉTYPES — le seul endroit où elle est écrite.
|
||||||
// ignore le drapeau et retombe sur le `switch`, pour qu'on puisse cocher la case sur n'importe
|
// THE ARCHETYPE LIST, written down exactly once.
|
||||||
// quelle strate sans rien casser en attendant son portage.
|
|
||||||
// THE PORTED-ARCHETYPE LIST, written down exactly once. An unported archetype ignores the flag
|
|
||||||
// and falls back to the switch, so the box can be ticked anywhere without breaking anything.
|
|
||||||
switch (Def->GeneratorType)
|
switch (Def->GeneratorType)
|
||||||
{
|
{
|
||||||
case ECaveGeneratorType::Maze: return true; // Phase 1
|
case ECaveGeneratorType::Maze: return true;
|
||||||
case ECaveGeneratorType::FlatPlain: // Phase 2 — les deux partagent
|
case ECaveGeneratorType::FlatPlain: // les deux partagent
|
||||||
case ECaveGeneratorType::CrystalChamber: return true; // UNE seule pile (BuildSlabStack)
|
case ECaveGeneratorType::CrystalChamber: return true; // UNE seule pile (BuildSlabStack)
|
||||||
|
|
||||||
case ECaveGeneratorType::SurfaceWorld:
|
case ECaveGeneratorType::SurfaceWorld:
|
||||||
// ✅ La garde « pas de biomes » est TOMBÉE (étape 2c) : le combiner `Mask` existe, donc une
|
// Pas de garde « pas de biomes » : le combiner `Mask` existe, donc une strate à biomes
|
||||||
// strate à biomes mélange bien ses hauteurs comme le chemin d'origine. Les trois archétypes
|
// mélange bien ses hauteurs comme le chemin d'origine.
|
||||||
// du dessus plus celui-ci font 5 des 8 portés.
|
// No no-biome guard: the Mask combiner exists, so a biome strate blends its heights
|
||||||
// The no-biome guard is GONE: the Mask combiner exists, so a biome strate blends its heights
|
|
||||||
// exactly as the original path does.
|
// exactly as the original path does.
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case ECaveGeneratorType::VerticalShafts: return true; // Phase 2 — 3 ops repris de Maze tels quels
|
case ECaveGeneratorType::VerticalShafts: return true; // 3 ops repris de Maze tels quels
|
||||||
|
|
||||||
case ECaveGeneratorType::FloatingIslands:
|
case ECaveGeneratorType::FloatingIslands:
|
||||||
// Phase 2 — la pile qui tourne à l'ENVERS : source de VIDE + fill, au lieu de source de ROC
|
// La pile qui tourne à l'ENVERS : source de VIDE + fill, au lieu de source de ROC
|
||||||
// + carve, avec les MÊMES opérateurs au signe près.
|
// + carve, avec les MÊMES opérateurs au signe près.
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case ECaveGeneratorType::Underwater:
|
case ECaveGeneratorType::Underwater:
|
||||||
// ⚠️ AUCUNE PILE À ELLE : `Underwater` EST `TunnelNetwork` plus un drapeau d'eau consommé
|
// ⚠️ AUCUNE PILE À ELLE : `Underwater` EST `TunnelNetwork` plus un drapeau d'eau consommé
|
||||||
// côté rendu. `GetDensityAt` les met dans le même `case`, et `WaterLevelRelative` n'est lu
|
// côté rendu. `VF_BuildOpStackForChunk` les met dans le même `case`, et `WaterLevelRelative` n'est lu
|
||||||
// que par `GetWaterLevel*` de ce manager — jamais par la densité (vérifié, pas supposé).
|
// que par `GetWaterLevel*` de ce manager — jamais par la densité (vérifié, pas supposé).
|
||||||
case ECaveGeneratorType::TunnelNetwork:
|
case ECaveGeneratorType::TunnelNetwork:
|
||||||
// Phase 2, LE DERNIER, et le plus gros : ~1080 lignes portées en trois étapes (squelette
|
// Le plus gros : squelette SDF → douze modificateurs de détail → override d'op par salle,
|
||||||
// SDF → douze modificateurs de détail → override d'op par salle), 19 opérateurs, dont
|
// 19 opérateurs, dont `FRoomGraphSource` qui **APPELLE** `BuildChunkCache`/`EvaluateSDFCached`
|
||||||
// `FRoomGraphSource` qui **APPELLE** `BuildChunkCache`/`EvaluateSDFCached` au lieu de les
|
// au lieu de les transcrire — c'est là que vit la discipline d'invariance de fenêtre
|
||||||
// transcrire — c'est là que vit la discipline d'invariance de fenêtre d'ARCHITECTURE §8.4,
|
// d'ARCHITECTURE §8.4 ; en forker une copie la casserait.
|
||||||
// et en forker une copie aurait été le pire résultat possible de ce refactor.
|
|
||||||
//
|
|
||||||
// **8 SUR 8.** Le `switch` d'archétypes a désormais un jumeau en pile d'opérateurs, opt-in
|
|
||||||
// par strate, chacun vérifié par un test d'équivalence bit à bit contre sa fonction
|
|
||||||
// d'origine. Ce qui n'est PAS fait : `ClassifyTile` n'utilise toujours pas `ClassifyBox`.
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
default: return false;
|
default: return false;
|
||||||
@@ -6712,7 +6587,7 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
|||||||
// Three transition styles:
|
// Three transition styles:
|
||||||
//
|
//
|
||||||
// GRADIENT (default):
|
// GRADIENT (default):
|
||||||
// Classic linear lerp of all params across BlendChunks. Smooth,
|
// Classic linear lerp of all params across TransitionBlendChunks. Smooth,
|
||||||
// invisible boundary. Cave shape morphs gradually from one strate
|
// invisible boundary. Cave shape morphs gradually from one strate
|
||||||
// to the next over several chunks.
|
// to the next over several chunks.
|
||||||
//
|
//
|
||||||
@@ -6720,7 +6595,7 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
|||||||
// No blending at all — params switch instantly at the boundary.
|
// No blending at all — params switch instantly at the boundary.
|
||||||
// The abrupt change in density, room size, roughness, etc. creates
|
// The abrupt change in density, room size, roughness, etc. creates
|
||||||
// a natural cliff, ledge, or visible material discontinuity.
|
// a natural cliff, ledge, or visible material discontinuity.
|
||||||
// BlendChunks is ignored (effectively 0).
|
// TransitionBlendChunks is ignored (effectively 0).
|
||||||
//
|
//
|
||||||
// INTERLEAVED:
|
// INTERLEAVED:
|
||||||
// 3D Perlin noise warps the effective boundary Z position per XY column.
|
// 3D Perlin noise warps the effective boundary Z position per XY column.
|
||||||
@@ -6740,7 +6615,7 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
|||||||
// CHECK BOTTOM BOUNDARY (transitioning to strate below)
|
// CHECK BOTTOM BOUNDARY (transitioning to strate below)
|
||||||
//---------------------------------------------------------------------
|
//---------------------------------------------------------------------
|
||||||
// DistFromBottom = how many chunks above the bottom edge of this strate.
|
// DistFromBottom = how many chunks above the bottom edge of this strate.
|
||||||
// When 0, we're right at the boundary. When == BlendChunks, we're at
|
// When 0, we're right at the boundary. When == TransitionBlendChunks, we're at
|
||||||
// the outer edge of the transition zone.
|
// the outer edge of the transition zone.
|
||||||
int32 DistFromBottom = ChunkCoord.Z - Slot.BottomChunkZ;
|
int32 DistFromBottom = ChunkCoord.Z - Slot.BottomChunkZ;
|
||||||
|
|
||||||
@@ -6749,7 +6624,7 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
|||||||
// The upper strate (this one) controls the transition type at its lower edge
|
// The upper strate (this one) controls the transition type at its lower edge
|
||||||
const EVoxelStrateTransition TransType = Slot.Definition->TransitionType;
|
const EVoxelStrateTransition TransType = Slot.Definition->TransitionType;
|
||||||
|
|
||||||
// Per-definition blend distance (overrides the manager's default BlendChunks)
|
// Per-definition blend distance.
|
||||||
const int32 EffectiveBlend = Slot.Definition->TransitionBlendChunks;
|
const int32 EffectiveBlend = Slot.Definition->TransitionBlendChunks;
|
||||||
|
|
||||||
// Prepare the neighbor's params (only used for Gradient and Interleaved)
|
// Prepare the neighbor's params (only used for Gradient and Interleaved)
|
||||||
@@ -6929,7 +6804,7 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
|||||||
//=============================================================================
|
//=============================================================================
|
||||||
// Returns the definition's base GenerationParams (cave shape, SDF, roughness, etc.).
|
// Returns the definition's base GenerationParams (cave shape, SDF, roughness, etc.).
|
||||||
//
|
//
|
||||||
// NOTE: Terrain op fields (TerraceStepHeight, ColumnDensity, etc.) are no longer
|
// NOTE: Terrain op fields (TerraceStepHeight, ColumnDensity, etc.) are not
|
||||||
// merged here. They default to 0 (disabled) in the base params, and are applied
|
// merged here. They default to 0 (disabled) in the base params, and are applied
|
||||||
// per-room during BuildChunkCache() via FCachedRoom::RoomOp — each room hash-rolls
|
// per-room during BuildChunkCache() via FCachedRoom::RoomOp — each room hash-rolls
|
||||||
// one op from the strate's probability pool (FStrateTerrainOpEntry::Probability).
|
// one op from the strate's probability pool (FStrateTerrainOpEntry::Probability).
|
||||||
@@ -6952,93 +6827,3 @@ FStrateGenerationParams UVoxelStrateManager::BuildParamsFromDefinition(const UVo
|
|||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64 UVoxelStrateManager::GetGenerationParamsFingerprint() const
|
|
||||||
{
|
|
||||||
// The diagnostic command-line override changes density inputs without changing an asset. Do
|
|
||||||
// not let a verdict from the unoverridden layout survive into an overridden session (or vice
|
|
||||||
// versa); a zero fingerprint conservatively disables persistence for that session.
|
|
||||||
const FRuntimeRoughnessOverrides& RoughnessOverrides = VF_GetRuntimeRoughnessOverrides();
|
|
||||||
if (RoughnessOverrides.bSurfaceRoughness
|
|
||||||
|| RoughnessOverrides.bFrequency
|
|
||||||
|| RoughnessOverrides.bNoiseType)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The session cache is deliberately conservative. These inputs are valid density inputs but
|
|
||||||
// are not represented by the fixed-size hash below, so returning zero disables verdict reuse
|
|
||||||
// instead of pretending that a partial key is complete.
|
|
||||||
if (IsUsingSeason())
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
#if WITH_EDITOR
|
|
||||||
if (ComposerOverrides.Num() > 0)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
uint32 A = 0x9E3779B9u;
|
|
||||||
uint32 B = 0x85EBCA6Bu;
|
|
||||||
auto HashBytes = [&A, &B](const void* Data, int32 Size)
|
|
||||||
{
|
|
||||||
A = FCrc::MemCrc32(Data, Size, A);
|
|
||||||
B = FCrc::MemCrc32(Data, Size, B ^ 0xA511E9B3u);
|
|
||||||
};
|
|
||||||
auto HashValue = [&HashBytes](const auto& Value)
|
|
||||||
{
|
|
||||||
HashBytes(&Value, sizeof(Value));
|
|
||||||
};
|
|
||||||
|
|
||||||
HashValue(CachedSeed);
|
|
||||||
HashValue(bOpenSurfaceEntry);
|
|
||||||
HashValue(OriginSpineRadius);
|
|
||||||
HashValue(InterStrateGapChunks);
|
|
||||||
const int32 LayoutCount = StrateLayout.Num();
|
|
||||||
HashValue(LayoutCount);
|
|
||||||
|
|
||||||
for (const FStrateSlot& Slot : StrateLayout)
|
|
||||||
{
|
|
||||||
if (Slot.Definition == nullptr)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildChunkCache resolves these arrays into the room cache. Hashing UObject pointers or
|
|
||||||
// asset names would not prove the asset contents, so dynamic operation/biome layouts
|
|
||||||
// conservatively opt out. The normal synthetic/runtime profile has neither array.
|
|
||||||
if (Slot.Definition->TerrainOperations.Num() > 0
|
|
||||||
|| Slot.Definition->Biomes.Num() > 0)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
HashValue(Slot.StrateIndex);
|
|
||||||
HashValue(Slot.TopChunkZ);
|
|
||||||
HashValue(Slot.BottomChunkZ);
|
|
||||||
HashValue(Slot.HeightInChunks);
|
|
||||||
HashValue(Slot.Definition->GeneratorType);
|
|
||||||
HashValue(Slot.Definition->bUseOperatorStack);
|
|
||||||
HashValue(Slot.Definition->bEnableWorms);
|
|
||||||
HashValue(VF_WormsForceOff());
|
|
||||||
// Development-only stage ablations change the field and therefore belong in every
|
|
||||||
// persistent/verdict key even though they are not authored generation parameters.
|
|
||||||
HashValue(VoxelDensityAblation::GetResolvedMask());
|
|
||||||
HashValue(Slot.Definition->StrateHeightInChunks);
|
|
||||||
HashValue(Slot.Definition->TransitionType);
|
|
||||||
HashValue(Slot.Definition->TransitionBlendChunks);
|
|
||||||
HashValue(Slot.Definition->GenerationParams);
|
|
||||||
HashValue(Slot.Definition->SlabParams);
|
|
||||||
HashValue(Slot.Definition->MazeParams);
|
|
||||||
HashValue(Slot.Definition->SurfaceParams);
|
|
||||||
HashValue(Slot.Definition->VerticalShaftParams);
|
|
||||||
HashValue(Slot.Definition->FloatingIslandParams);
|
|
||||||
HashValue(Slot.Definition->PassageConfig);
|
|
||||||
HashValue(Slot.Definition->bHasWater);
|
|
||||||
HashValue(Slot.Definition->BiomeMapParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
const uint64 Result = (static_cast<uint64>(B) << 32) | static_cast<uint64>(A);
|
|
||||||
return Result != 0 ? Result : 1;
|
|
||||||
}
|
|
||||||
@@ -2973,24 +2973,6 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
int32 GetSixNeighbour(const FSampleGrid& Grid, int32 Cell, int32 Direction)
|
|
||||||
{
|
|
||||||
int32 X = 0;
|
|
||||||
int32 Y = 0;
|
|
||||||
int32 Z = 0;
|
|
||||||
DecodeIndex(Grid, Cell, X, Y, Z);
|
|
||||||
switch (Direction)
|
|
||||||
{
|
|
||||||
case 0: return X + 1 < Grid.NumX ? Grid.Index(X + 1, Y, Z) : INDEX_NONE;
|
|
||||||
case 1: return X > 0 ? Grid.Index(X - 1, Y, Z) : INDEX_NONE;
|
|
||||||
case 2: return Y + 1 < Grid.NumY ? Grid.Index(X, Y + 1, Z) : INDEX_NONE;
|
|
||||||
case 3: return Y > 0 ? Grid.Index(X, Y - 1, Z) : INDEX_NONE;
|
|
||||||
case 4: return Z + 1 < Grid.NumZ ? Grid.Index(X, Y, Z + 1) : INDEX_NONE;
|
|
||||||
case 5: return Z > 0 ? Grid.Index(X, Y, Z - 1) : INDEX_NONE;
|
|
||||||
default: return INDEX_NONE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int32 CountPlayerFitNeighbours(
|
int32 CountPlayerFitNeighbours(
|
||||||
const FSampleGrid& Grid,
|
const FSampleGrid& Grid,
|
||||||
const TArray<uint8>& PlayerFitMask,
|
const TArray<uint8>& PlayerFitMask,
|
||||||
@@ -3458,26 +3440,6 @@ FVoxelConnectivityDiagnostics VF_DiagnoseConnectivityWithSampler(
|
|||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
EVoxelConnectivityResult VF_ArePlayerFitConnectedWithSampler(
|
|
||||||
const IVoxelStrateDensitySampler& Sampler,
|
|
||||||
int32 StrateBottomWorldZ,
|
|
||||||
int32 StrateTopWorldZ,
|
|
||||||
float BoundarySealThickness,
|
|
||||||
const FVector& AVoxel,
|
|
||||||
const FVector& BVoxel,
|
|
||||||
const FVoxelStrateMeasureSettings& Settings,
|
|
||||||
bool& bOutStartSnapped,
|
|
||||||
bool& bOutGoalSnapped,
|
|
||||||
FVoxelConnectivityDiagnostics* OutDiagnostics)
|
|
||||||
{
|
|
||||||
int32 NumRouteRetries = 0;
|
|
||||||
return VoxelStrateMeasurePrivate::QueryConnectivity(
|
|
||||||
nullptr, nullptr, &Sampler, INDEX_NONE,
|
|
||||||
StrateBottomWorldZ, StrateTopWorldZ, BoundarySealThickness, true,
|
|
||||||
AVoxel, BVoxel, Settings, bOutStartSnapped, bOutGoalSnapped,
|
|
||||||
NumRouteRetries, OutDiagnostics, true, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
FVoxelConnectivityDiagnostics VF_DiagnosePlayerFitConnectivityWithSampler(
|
FVoxelConnectivityDiagnostics VF_DiagnosePlayerFitConnectivityWithSampler(
|
||||||
const IVoxelStrateDensitySampler& Sampler,
|
const IVoxelStrateDensitySampler& Sampler,
|
||||||
int32 StrateBottomWorldZ,
|
int32 StrateBottomWorldZ,
|
||||||
|
|||||||
@@ -23,11 +23,10 @@
|
|||||||
#include "VoxelTilePostReach.h"
|
#include "VoxelTilePostReach.h"
|
||||||
#include "VoxelClipmapDesiredTiles.h"
|
#include "VoxelClipmapDesiredTiles.h"
|
||||||
// IWYU (FPSemantics = Precise ⇒ plus de PCH partagé) : GetPlayerPosition déréférence le pawn, donc
|
// IWYU (FPSemantics = Precise ⇒ plus de PCH partagé) : GetPlayerPosition déréférence le pawn, donc
|
||||||
// APawn doit être COMPLET — `Casts.h` n'en donne qu'une déclaration avant. APlayerController était
|
// APawn doit être COMPLET — `Casts.h` n'en donne qu'une déclaration avant. APlayerController n'est
|
||||||
// complet par transitivité seulement : on l'inclut explicitement, c'est exactement la fragilité
|
// complet que par transitivité : on l'inclut explicitement.
|
||||||
// qu'on est en train de retirer.
|
|
||||||
// GetPlayerPosition dereferences the pawn, so APawn must be COMPLETE — Casts.h only forward-declares
|
// GetPlayerPosition dereferences the pawn, so APawn must be COMPLETE — Casts.h only forward-declares
|
||||||
// it. APlayerController was complete transitively only; include it explicitly.
|
// it. APlayerController is complete only transitively; include it explicitly.
|
||||||
#include "GameFramework/Pawn.h"
|
#include "GameFramework/Pawn.h"
|
||||||
#include "GameFramework/PlayerController.h"
|
#include "GameFramework/PlayerController.h"
|
||||||
#include "GameFramework/PawnMovementComponent.h"
|
#include "GameFramework/PawnMovementComponent.h"
|
||||||
@@ -59,7 +58,6 @@
|
|||||||
#include "Serialization/JsonWriter.h"
|
#include "Serialization/JsonWriter.h"
|
||||||
#include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes (Perf 0)
|
#include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes (Perf 0)
|
||||||
|
|
||||||
static int32 VF_MaxMarchingCubesLevel(const UVoxelSettings* Settings);
|
|
||||||
static bool VF_TileOverlapsVoxelBounds(const FVoxelTileKey& Tile,
|
static bool VF_TileOverlapsVoxelBounds(const FVoxelTileKey& Tile,
|
||||||
const FVector& BoundsMin, const FVector& BoundsMax);
|
const FVector& BoundsMin, const FVector& BoundsMax);
|
||||||
static bool VF_TileFootprintsOverlap(const FVoxelTileKey& A, const FVoxelTileKey& B);
|
static bool VF_TileFootprintsOverlap(const FVoxelTileKey& A, const FVoxelTileKey& B);
|
||||||
@@ -186,11 +184,11 @@ AVoxelWorld::AVoxelWorld()
|
|||||||
//=============================================================================
|
//=============================================================================
|
||||||
// T1.f — build the RMC geometry buffers OFF the game thread.
|
// T1.f — build the RMC geometry buffers OFF the game thread.
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// FRealtimeMeshStreamSet is plain CPU data; the per-vertex/per-triangle builder loop used to run
|
// FRealtimeMeshStreamSet is plain CPU data; on the game thread (in ApplyMeshToTile) the
|
||||||
// in ApplyMeshToTile ON THE GAME THREAD, where it was the dominant streaming cost (game thread
|
// per-vertex/per-triangle builder loop is the dominant streaming cost (game thread >6 ms while
|
||||||
// >6 ms while moving, GPU/draw idle). It touches ONLY the POD MeshData arrays — no UObject, no
|
// moving, GPU/draw idle). It touches ONLY the POD MeshData arrays — no UObject, no generator — so
|
||||||
// generator — so it's safe on the gen worker. The game thread then just uploads the finished
|
// it's safe on the gen worker. The game thread then just uploads the finished streams
|
||||||
// streams (CreateSectionGroup). Byte-identical geometry; the only thing that moved is WHERE it runs.
|
// (CreateSectionGroup). Byte-identical geometry; only WHERE it runs differs.
|
||||||
static void BuildTileStreamSet(RealtimeMesh::FRealtimeMeshStreamSet& Streams, const FVoxelMeshData& MeshData)
|
static void BuildTileStreamSet(RealtimeMesh::FRealtimeMeshStreamSet& Streams, const FVoxelMeshData& MeshData)
|
||||||
{
|
{
|
||||||
RealtimeMesh::TRealtimeMeshBuilderLocal<uint32, FPackedNormal, FVector2DHalf, 1> Builder(Streams);
|
RealtimeMesh::TRealtimeMeshBuilderLocal<uint32, FPackedNormal, FVector2DHalf, 1> Builder(Streams);
|
||||||
@@ -1586,16 +1584,12 @@ void AVoxelWorld::BeginPlay()
|
|||||||
int32 TestMaxClipLevel = -1;
|
int32 TestMaxClipLevel = -1;
|
||||||
const bool bTestMaxClipLevel =
|
const bool bTestMaxClipLevel =
|
||||||
FParse::Value(FCommandLine::Get(), TEXT("voxel.TestMaxClipLevel="), TestMaxClipLevel);
|
FParse::Value(FCommandLine::Get(), TEXT("voxel.TestMaxClipLevel="), TestMaxClipLevel);
|
||||||
int32 TestFarSheetRing = -1;
|
if (bTestRenderDistance || bTestMaxClipLevel)
|
||||||
const bool bTestFarSheetRing =
|
|
||||||
FParse::Value(FCommandLine::Get(), TEXT("voxel.TestFarSheetRing="), TestFarSheetRing);
|
|
||||||
if (bTestRenderDistance || bTestMaxClipLevel || bTestFarSheetRing)
|
|
||||||
{
|
{
|
||||||
if (UVoxelSettings* TestSettings = DuplicateObject<UVoxelSettings>(Settings, this))
|
if (UVoxelSettings* TestSettings = DuplicateObject<UVoxelSettings>(Settings, this))
|
||||||
{
|
{
|
||||||
const int32 AuthoredRenderDistance = Settings->RenderDistanceChunks;
|
const int32 AuthoredRenderDistance = Settings->RenderDistanceChunks;
|
||||||
const int32 AuthoredMaxClipLevel = Settings->MaxClipLevel;
|
const int32 AuthoredMaxClipLevel = Settings->MaxClipLevel;
|
||||||
const bool bAuthoredFarSheetRing = Settings->bFarSheetRing;
|
|
||||||
if (bTestRenderDistance)
|
if (bTestRenderDistance)
|
||||||
{
|
{
|
||||||
TestSettings->RenderDistanceChunks = TestRenderDistanceChunks;
|
TestSettings->RenderDistanceChunks = TestRenderDistanceChunks;
|
||||||
@@ -1604,18 +1598,12 @@ void AVoxelWorld::BeginPlay()
|
|||||||
{
|
{
|
||||||
TestSettings->MaxClipLevel = FMath::Clamp(TestMaxClipLevel, 0, 8);
|
TestSettings->MaxClipLevel = FMath::Clamp(TestMaxClipLevel, 0, 8);
|
||||||
}
|
}
|
||||||
if (bTestFarSheetRing)
|
|
||||||
{
|
|
||||||
TestSettings->bFarSheetRing = TestFarSheetRing != 0;
|
|
||||||
}
|
|
||||||
Settings = TestSettings;
|
Settings = TestSettings;
|
||||||
UE_LOG(LogTemp, Display,
|
UE_LOG(LogTemp, Display,
|
||||||
TEXT("[VoxelForgeHorizonMeasure] transient_settings=1 "
|
TEXT("[VoxelForgeHorizonMeasure] transient_settings=1 "
|
||||||
"render_distance_chunks=%d authored=%d max_clip_level=%d authored=%d "
|
"render_distance_chunks=%d authored=%d max_clip_level=%d authored=%d"),
|
||||||
"far_sheet_ring=%d authored=%d"),
|
|
||||||
Settings->RenderDistanceChunks, AuthoredRenderDistance,
|
Settings->RenderDistanceChunks, AuthoredRenderDistance,
|
||||||
Settings->MaxClipLevel, AuthoredMaxClipLevel,
|
Settings->MaxClipLevel, AuthoredMaxClipLevel);
|
||||||
Settings->bFarSheetRing ? 1 : 0, bAuthoredFarSheetRing ? 1 : 0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1707,18 +1695,17 @@ void AVoxelWorld::BeginPlay()
|
|||||||
TEXT("\"asset\":\"%s\",\"seed\":%d,\"total_strates\":%d,\"inter_strate_gap_chunks\":%d,"
|
TEXT("\"asset\":\"%s\",\"seed\":%d,\"total_strates\":%d,\"inter_strate_gap_chunks\":%d,"
|
||||||
"\"strate_pool\":%d,\"strate_content_cut_min_level\":%d,"
|
"\"strate_pool\":%d,\"strate_content_cut_min_level\":%d,"
|
||||||
"\"effective_strate_content_cut_min_level\":%d,\"max_clip_level\":%d,\"clip_radius\":%d,"
|
"\"effective_strate_content_cut_min_level\":%d,\"max_clip_level\":%d,\"clip_radius\":%d,"
|
||||||
"\"render_distance_chunks\":%d,\"far_sheet_ring\":%d,\"far_sheet_span_levels\":%d,"
|
"\"render_distance_chunks\":%d,"
|
||||||
"\"enable_density_volume\":%d,\"density_volume_resolution\":%d,\"density_volume_levels\":%d,"
|
"\"enable_density_volume\":%d,\"density_volume_resolution\":%d,\"density_volume_levels\":%d,"
|
||||||
"\"density_volume_max_tasks\":%d,\"density_volume_gpu_upload\":%d"),
|
"\"density_volume_gpu_upload\":%d"),
|
||||||
*SettingsPath,
|
*SettingsPath,
|
||||||
Settings->GetEffectiveWorldSeed(), Settings->TotalStrates,
|
Settings->GetEffectiveWorldSeed(), Settings->TotalStrates,
|
||||||
Settings->InterStrateGapChunks, Settings->StratePool.Num(),
|
Settings->InterStrateGapChunks, Settings->StratePool.Num(),
|
||||||
Settings->StrateContentCutMinLevel,
|
Settings->StrateContentCutMinLevel,
|
||||||
Settings->GetEffectiveStrateContentCutMinLevel(), Settings->MaxClipLevel,
|
Settings->GetEffectiveStrateContentCutMinLevel(), Settings->MaxClipLevel,
|
||||||
Settings->ClipRadius, Settings->RenderDistanceChunks,
|
Settings->ClipRadius, Settings->RenderDistanceChunks,
|
||||||
Settings->bFarSheetRing ? 1 : 0, Settings->FarSheetSpanLevels,
|
|
||||||
Settings->bEnableDensityVolume ? 1 : 0, Settings->DensityVolumeResolution,
|
Settings->bEnableDensityVolume ? 1 : 0, Settings->DensityVolumeResolution,
|
||||||
Settings->DensityVolumeLevels, Settings->DensityVolumeMaxTasks,
|
Settings->DensityVolumeLevels,
|
||||||
Settings->bDensityVolumeGPUUpload ? 1 : 0));
|
Settings->bDensityVolumeGPUUpload ? 1 : 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2232,7 +2219,7 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
|
|||||||
|
|
||||||
void AVoxelWorld::RecordTileHash(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels,
|
void AVoxelWorld::RecordTileHash(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels,
|
||||||
int32 Step, int32 Cells, int32 BandChunkLo, int32 BandChunkHi,
|
int32 Step, int32 Cells, int32 BandChunkLo, int32 BandChunkHi,
|
||||||
bool bSheetTile, const FVoxelMeshData& MeshData)
|
const FVoxelMeshData& MeshData)
|
||||||
{
|
{
|
||||||
if (!bTileHashDumpEnabled)
|
if (!bTileHashDumpEnabled)
|
||||||
{
|
{
|
||||||
@@ -2246,7 +2233,6 @@ void AVoxelWorld::RecordTileHash(const FVoxelTileKey& Tile, const FIntVector& Or
|
|||||||
Record.Cells = Cells;
|
Record.Cells = Cells;
|
||||||
Record.BandChunkLo = BandChunkLo;
|
Record.BandChunkLo = BandChunkLo;
|
||||||
Record.BandChunkHi = BandChunkHi;
|
Record.BandChunkHi = BandChunkHi;
|
||||||
Record.bSheetTile = bSheetTile;
|
|
||||||
Record.bEmpty = MeshData.IsEmpty();
|
Record.bEmpty = MeshData.IsEmpty();
|
||||||
Record.NumTriangles = MeshData.Triangles.Num() / 3;
|
Record.NumTriangles = MeshData.Triangles.Num() / 3;
|
||||||
Record.GeometryHash = VoxelForgeGeometryHash::Compute(MeshData);
|
Record.GeometryHash = VoxelForgeGeometryHash::Compute(MeshData);
|
||||||
@@ -2294,7 +2280,6 @@ void AVoxelWorld::WriteTileHashDump()
|
|||||||
Writer->WriteValue(TEXT("coarse_tile_cells"), Settings->CoarseTileCells);
|
Writer->WriteValue(TEXT("coarse_tile_cells"), Settings->CoarseTileCells);
|
||||||
Writer->WriteValue(TEXT("max_clip_level"), Settings->MaxClipLevel);
|
Writer->WriteValue(TEXT("max_clip_level"), Settings->MaxClipLevel);
|
||||||
Writer->WriteValue(TEXT("render_distance_chunks"), Settings->RenderDistanceChunks);
|
Writer->WriteValue(TEXT("render_distance_chunks"), Settings->RenderDistanceChunks);
|
||||||
Writer->WriteValue(TEXT("far_sheet_ring"), Settings->bFarSheetRing);
|
|
||||||
Writer->WriteValue(TEXT("strate_content_cut_min_level"),
|
Writer->WriteValue(TEXT("strate_content_cut_min_level"),
|
||||||
Settings->GetEffectiveStrateContentCutMinLevel());
|
Settings->GetEffectiveStrateContentCutMinLevel());
|
||||||
Writer->WriteObjectEnd();
|
Writer->WriteObjectEnd();
|
||||||
@@ -2315,7 +2300,8 @@ void AVoxelWorld::WriteTileHashDump()
|
|||||||
Writer->WriteValue(TEXT("cells"), Record.Cells);
|
Writer->WriteValue(TEXT("cells"), Record.Cells);
|
||||||
Writer->WriteValue(TEXT("band_chunk_lo"), Record.BandChunkLo);
|
Writer->WriteValue(TEXT("band_chunk_lo"), Record.BandChunkLo);
|
||||||
Writer->WriteValue(TEXT("band_chunk_hi"), Record.BandChunkHi);
|
Writer->WriteValue(TEXT("band_chunk_hi"), Record.BandChunkHi);
|
||||||
Writer->WriteValue(TEXT("sheet_tile"), Record.bSheetTile);
|
// The parity harness still compares this field; the runtime has no sheet tiles.
|
||||||
|
Writer->WriteValue(TEXT("sheet_tile"), false);
|
||||||
Writer->WriteValue(TEXT("empty"), Record.bEmpty);
|
Writer->WriteValue(TEXT("empty"), Record.bEmpty);
|
||||||
Writer->WriteValue(TEXT("triangle_count"), Record.NumTriangles);
|
Writer->WriteValue(TEXT("triangle_count"), Record.NumTriangles);
|
||||||
Writer->WriteValue(TEXT("geometry_hash"), Record.GeometryHash);
|
Writer->WriteValue(TEXT("geometry_hash"), Record.GeometryHash);
|
||||||
@@ -3018,11 +3004,10 @@ void AVoxelWorld::AdvanceHeadlessCollisionGateStressTest(
|
|||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Display,
|
UE_LOG(LogTemp, Display,
|
||||||
TEXT("[VoxelForgeCollisionGateStressTest] owner_settings clip_radius=%d "
|
TEXT("[VoxelForgeCollisionGateStressTest] owner_settings clip_radius=%d "
|
||||||
"max_clip_level=%d render_distance_chunks=%d far_sheet_ring=%d"),
|
"max_clip_level=%d render_distance_chunks=%d"),
|
||||||
Settings != nullptr ? Settings->ClipRadius : -1,
|
Settings != nullptr ? Settings->ClipRadius : -1,
|
||||||
Settings != nullptr ? Settings->MaxClipLevel : -1,
|
Settings != nullptr ? Settings->MaxClipLevel : -1,
|
||||||
Settings != nullptr ? Settings->RenderDistanceChunks : -1,
|
Settings != nullptr ? Settings->RenderDistanceChunks : -1);
|
||||||
Settings != nullptr && Settings->bFarSheetRing ? 1 : 0);
|
|
||||||
float CapsuleHalfHeightCm = 0.0f;
|
float CapsuleHalfHeightCm = 0.0f;
|
||||||
if (const ACharacter* Character = Cast<ACharacter>(PlayerPawn))
|
if (const ACharacter* Character = Cast<ACharacter>(PlayerPawn))
|
||||||
{
|
{
|
||||||
@@ -4577,12 +4562,11 @@ void AVoxelWorld::Tick(float DeltaTime)
|
|||||||
const double SpanZ = static_cast<double>(Max.Z - Min.Z + 1) * TileExtentMetres;
|
const double SpanZ = static_cast<double>(Max.Z - Min.Z + 1) * TileExtentMetres;
|
||||||
VoxelForgeStartupTrace::RecordEvent(TEXT("steady_desired_level"), FString::Printf(
|
VoxelForgeStartupTrace::RecordEvent(TEXT("steady_desired_level"), FString::Printf(
|
||||||
TEXT("\"level\":%d,\"tiles\":%d,\"min_coord\":[%d,%d,%d],\"max_coord\":[%d,%d,%d],"
|
TEXT("\"level\":%d,\"tiles\":%d,\"min_coord\":[%d,%d,%d],\"max_coord\":[%d,%d,%d],"
|
||||||
"\"tile_extent_m\":%s,\"coverage_extent_m\":[%s,%s,%s],\"sheet\":%d"),
|
"\"tile_extent_m\":%s,\"coverage_extent_m\":[%s,%s,%s]"),
|
||||||
Level, SteadyLevelCounts.FindChecked(Level),
|
Level, SteadyLevelCounts.FindChecked(Level),
|
||||||
Min.X, Min.Y, Min.Z, Max.X, Max.Y, Max.Z,
|
Min.X, Min.Y, Min.Z, Max.X, Max.Y, Max.Z,
|
||||||
*FString::SanitizeFloat(TileExtentMetres),
|
*FString::SanitizeFloat(TileExtentMetres),
|
||||||
*FString::SanitizeFloat(SpanX), *FString::SanitizeFloat(SpanY), *FString::SanitizeFloat(SpanZ),
|
*FString::SanitizeFloat(SpanX), *FString::SanitizeFloat(SpanY), *FString::SanitizeFloat(SpanZ)));
|
||||||
Settings && Level > VF_MaxMarchingCubesLevel(Settings) ? 1 : 0));
|
|
||||||
VoxelForgeStartupTrace::RecordEvent(TEXT("steady_loaded_level"), FString::Printf(
|
VoxelForgeStartupTrace::RecordEvent(TEXT("steady_loaded_level"), FString::Printf(
|
||||||
TEXT("\"level\":%d,\"tiles\":%d,\"vertices\":%llu,\"triangles\":%llu,\"geometry_bytes\":%llu"),
|
TEXT("\"level\":%d,\"tiles\":%d,\"vertices\":%llu,\"triangles\":%llu,\"geometry_bytes\":%llu"),
|
||||||
Level, LoadedLevelCounts.FindRef(Level),
|
Level, LoadedLevelCounts.FindRef(Level),
|
||||||
@@ -6217,8 +6201,7 @@ void AVoxelWorld::SyncRemeshTile(const FVoxelTileKey& Tile)
|
|||||||
Result.GenerationStartCycles = GenerateStartCycles;
|
Result.GenerationStartCycles = GenerateStartCycles;
|
||||||
GenerateTileResult(Tile, OriginVoxels, Step, Cells, GenerationEpoch, /*bWantCapture*/ false,
|
GenerateTileResult(Tile, OriginVoxels, Step, Cells, GenerationEpoch, /*bWantCapture*/ false,
|
||||||
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
|
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
|
||||||
/*bSheetTile*/ false, /*SheetChunkZ*/ 0,
|
Result, nullptr);
|
||||||
/*Hole*/ 0, 0, 0, 0, Result, nullptr); // hole unused (not a sheet tile)
|
|
||||||
Result.GenerationEndCycles = GenerateStartCycles != 0
|
Result.GenerationEndCycles = GenerateStartCycles != 0
|
||||||
? FPlatformTime::Cycles64() : 0;
|
? FPlatformTime::Cycles64() : 0;
|
||||||
|
|
||||||
@@ -6471,30 +6454,16 @@ static FORCEINLINE FIntVector VF_FloorDiv(const FIntVector& V, int32 D)
|
|||||||
return FIntVector(VF_FloorDiv(V.X, D), VF_FloorDiv(V.Y, D), VF_FloorDiv(V.Z, D));
|
return FIntVector(VF_FloorDiv(V.X, D), VF_FloorDiv(V.Y, D), VF_FloorDiv(V.Z, D));
|
||||||
}
|
}
|
||||||
|
|
||||||
// F18 — the outer shell is either the MC render-distance level or the larger sheet ring. When
|
// The outer shell is the MaxClipLevel ring stretched to RenderDistanceChunks. Shared by desired
|
||||||
// the sheet span skips levels, VF_MaxMarchingCubesLevel fills them with MC bridge shells.
|
// selection and range culling so both use the same outer horizon.
|
||||||
// Shared by desired selection and range culling so both use the same outer horizon.
|
|
||||||
static FORCEINLINE void VF_OuterShell(const UVoxelSettings* Settings, int32 R, int32 MaxLevel,
|
static FORCEINLINE void VF_OuterShell(const UVoxelSettings* Settings, int32 R, int32 MaxLevel,
|
||||||
int32& OutLevel, int32& OutRadius)
|
int32& OutLevel, int32& OutRadius)
|
||||||
{
|
{
|
||||||
VoxelClipmapDesiredTiles::FParameters Parameters;
|
VoxelClipmapDesiredTiles::FParameters Parameters;
|
||||||
Parameters.RenderDistanceChunks = Settings ? Settings->RenderDistanceChunks : 0;
|
Parameters.RenderDistanceChunks = Settings ? Settings->RenderDistanceChunks : 0;
|
||||||
Parameters.bFarSheetRing = Settings && Settings->bFarSheetRing;
|
|
||||||
Parameters.FarSheetSpanLevels = Settings ? Settings->FarSheetSpanLevels : 2;
|
|
||||||
VoxelClipmapDesiredTiles::OuterShell(Parameters, R, MaxLevel, OutLevel, OutRadius);
|
VoxelClipmapDesiredTiles::OuterShell(Parameters, R, MaxLevel, OutLevel, OutRadius);
|
||||||
}
|
}
|
||||||
|
|
||||||
// When an expanded sheet ring skips LOD levels, keep an MC bridge at every intervening level.
|
|
||||||
static int32 VF_MaxMarchingCubesLevel(const UVoxelSettings* Settings)
|
|
||||||
{
|
|
||||||
const int32 Radius = Settings ? FMath::Max(1, Settings->ClipRadius) : 3;
|
|
||||||
const int32 MaxLevel = Settings ? FMath::Clamp(Settings->MaxClipLevel, 0, 8) : 4;
|
|
||||||
int32 OuterLevel = MaxLevel, OuterRadius = Radius;
|
|
||||||
VF_OuterShell(Settings, Radius, MaxLevel, OuterLevel, OuterRadius);
|
|
||||||
(void)OuterRadius;
|
|
||||||
return OuterLevel > MaxLevel ? OuterLevel - 1 : MaxLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& PlayerPosition,
|
void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& PlayerPosition,
|
||||||
APawn* PlayerPawn, const FVector& PlayerHeading,
|
APawn* PlayerPawn, const FVector& PlayerHeading,
|
||||||
TArray<FVoxelTileKey>& OutLeavers)
|
TArray<FVoxelTileKey>& OutLeavers)
|
||||||
@@ -6512,8 +6481,7 @@ void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& Pla
|
|||||||
const int32 R = Settings ? FMath::Max(1, Settings->ClipRadius) : 3;
|
const int32 R = Settings ? FMath::Max(1, Settings->ClipRadius) : 3;
|
||||||
const int32 MaxLevel = Settings ? FMath::Clamp(Settings->MaxClipLevel, 0, 8) : 4;
|
const int32 MaxLevel = Settings ? FMath::Clamp(Settings->MaxClipLevel, 0, 8) : 4;
|
||||||
|
|
||||||
// Render-distance tiles expand horizontally. The sheet ring retains its configured span;
|
// Render-distance tiles expand horizontally.
|
||||||
// intermediate levels are emitted as marching-cubes bridge shells below it.
|
|
||||||
// IsTileInClipRange shares the outer-shell calculation so culling sees this same horizon.
|
// IsTileInClipRange shares the outer-shell calculation so culling sees this same horizon.
|
||||||
int32 StrateTopZ = 0, StrateBottomZ = 0;
|
int32 StrateTopZ = 0, StrateBottomZ = 0;
|
||||||
bool bHasStrate = false;
|
bool bHasStrate = false;
|
||||||
@@ -6537,8 +6505,6 @@ void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& Pla
|
|||||||
TileParameters.ClipRadius = R;
|
TileParameters.ClipRadius = R;
|
||||||
TileParameters.MaxClipLevel = MaxLevel;
|
TileParameters.MaxClipLevel = MaxLevel;
|
||||||
TileParameters.RenderDistanceChunks = Settings ? Settings->RenderDistanceChunks : 0;
|
TileParameters.RenderDistanceChunks = Settings ? Settings->RenderDistanceChunks : 0;
|
||||||
TileParameters.bFarSheetRing = Settings && Settings->bFarSheetRing;
|
|
||||||
TileParameters.FarSheetSpanLevels = Settings ? Settings->FarSheetSpanLevels : 2;
|
|
||||||
VoxelClipmapDesiredTiles::Build(Center, ZLo, ZHi, TileParameters, DesiredSorted);
|
VoxelClipmapDesiredTiles::Build(Center, ZLo, ZHi, TileParameters, DesiredSorted);
|
||||||
for (const FVoxelTileKey& Key : DesiredSorted)
|
for (const FVoxelTileKey& Key : DesiredSorted)
|
||||||
{
|
{
|
||||||
@@ -6681,10 +6647,10 @@ void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& Pla
|
|||||||
DesiredTransitionMaxLevel = FMath::Max(DesiredTransitionMaxLevel, Pair.Key.Level);
|
DesiredTransitionMaxLevel = FMath::Max(DesiredTransitionMaxLevel, Pair.Key.Level);
|
||||||
}
|
}
|
||||||
// A worker from the previous desired epoch can finish late with a component at a level no
|
// A worker from the previous desired epoch can finish late with a component at a level no
|
||||||
// longer present in TileComponents when this index is built. Index through the supported
|
// longer present in TileComponents when this index is built. Index through the highest
|
||||||
// maximum sheet level (MaxClipLevel 8 + FarSheetSpanLevels 4) so that such a retired tile still
|
// possible tile level (MaxClipLevel is clamped to 8) so that such a retired tile still
|
||||||
// finds all of its desired descendants when the result is reconciled.
|
// finds all of its desired descendants when the result is reconciled.
|
||||||
constexpr int32 MaxSupportedTileLevel = 12;
|
constexpr int32 MaxSupportedTileLevel = 8;
|
||||||
const int32 DescendantIndexMaxLevel = FMath::Max(
|
const int32 DescendantIndexMaxLevel = FMath::Max(
|
||||||
DesiredTransitionMaxLevel, MaxSupportedTileLevel);
|
DesiredTransitionMaxLevel, MaxSupportedTileLevel);
|
||||||
for (const FVoxelTileKey& Key : DesiredSorted)
|
for (const FVoxelTileKey& Key : DesiredSorted)
|
||||||
@@ -6727,7 +6693,7 @@ void AVoxelWorld::CancelObsoleteTileWork()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fold every registered anchor's small level-0 box into the current desired set (§9.3). Runs inside
|
// Fold every registered anchor's small level-0 box into the current desired set (§9.3). Runs inside
|
||||||
// BuildDesiredTiles after the player clipmap + sheet ring, keyed on the same DesiredStamp so the leaver
|
// BuildDesiredTiles after the player clipmap, keyed on the same DesiredStamp so the leaver
|
||||||
// sweep + delta cull handle anchor tiles leaving. Level-0 only (collision lives on level-0 tiles); empty
|
// sweep + delta cull handle anchor tiles leaving. Level-0 only (collision lives on level-0 tiles); empty
|
||||||
// tiles in the box are ~free (the trivial-tile reject skips gen). Dedup vs the player clipmap by stamp.
|
// tiles in the box are ~free (the trivial-tile reject skips gen). Dedup vs the player clipmap by stamp.
|
||||||
// §9.4: a tile the player clipmap did NOT stamp (bNew) that only a CollisionOnly anchor wants goes into
|
// §9.4: a tile the player clipmap did NOT stamp (bNew) that only a CollisionOnly anchor wants goes into
|
||||||
@@ -6839,7 +6805,7 @@ void AVoxelWorld::UnregisterStreamingAnchor(AActor* Actor)
|
|||||||
bool AVoxelWorld::IsTileInClipRange(const FVoxelTileKey& Tile, const FIntVector& Center) const
|
bool AVoxelWorld::IsTileInClipRange(const FVoxelTileKey& Tile, const FIntVector& Center) const
|
||||||
{
|
{
|
||||||
// In range = the tile's centre falls within the OUTERMOST shell (VF_OuterShell — the
|
// In range = the tile's centre falls within the OUTERMOST shell (VF_OuterShell — the
|
||||||
// render-distance/sheet-ring level+radius, same as BuildDesiredTiles). A loaded-but-not-
|
// render-distance level+radius, same as BuildDesiredTiles). A loaded-but-not-
|
||||||
// desired tile in range is mid-LOD-transition (wait for its replacement); one out of range
|
// desired tile in range is mid-LOD-transition (wait for its replacement); one out of range
|
||||||
// has left the view entirely (cull immediately).
|
// has left the view entirely (cull immediately).
|
||||||
const int32 R = Settings ? FMath::Max(1, Settings->ClipRadius) : 3;
|
const int32 R = Settings ? FMath::Max(1, Settings->ClipRadius) : 3;
|
||||||
@@ -7021,13 +6987,13 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
const double SpanZ = static_cast<double>(Summary.Max.Z - Summary.Min.Z + 1) * TileExtentMetres;
|
const double SpanZ = static_cast<double>(Summary.Max.Z - Summary.Min.Z + 1) * TileExtentMetres;
|
||||||
VoxelForgeStartupTrace::RecordEvent(TEXT("desired_level"), FString::Printf(
|
VoxelForgeStartupTrace::RecordEvent(TEXT("desired_level"), FString::Printf(
|
||||||
TEXT("\"level\":%d,\"tiles\":%d,\"min_coord\":[%d,%d,%d],\"max_coord\":[%d,%d,%d],"
|
TEXT("\"level\":%d,\"tiles\":%d,\"min_coord\":[%d,%d,%d],\"max_coord\":[%d,%d,%d],"
|
||||||
"\"tile_extent_m\":%s,\"coverage_extent_m\":[%s,%s,%s],\"sheet\":%d"),
|
"\"tile_extent_m\":%s,\"coverage_extent_m\":[%s,%s,%s]"),
|
||||||
Summary.Level, Summary.Count,
|
Summary.Level, Summary.Count,
|
||||||
Summary.Min.X, Summary.Min.Y, Summary.Min.Z,
|
Summary.Min.X, Summary.Min.Y, Summary.Min.Z,
|
||||||
Summary.Max.X, Summary.Max.Y, Summary.Max.Z,
|
Summary.Max.X, Summary.Max.Y, Summary.Max.Z,
|
||||||
*FString::SanitizeFloat(TileExtentMetres),
|
*FString::SanitizeFloat(TileExtentMetres),
|
||||||
*FString::SanitizeFloat(SpanX), *FString::SanitizeFloat(SpanY), *FString::SanitizeFloat(SpanZ),
|
*FString::SanitizeFloat(SpanX), *FString::SanitizeFloat(SpanY),
|
||||||
Settings && Summary.Level > VF_MaxMarchingCubesLevel(Settings) ? 1 : 0));
|
*FString::SanitizeFloat(SpanZ)));
|
||||||
}
|
}
|
||||||
bStartupTraceDesiredRecorded = true;
|
bStartupTraceDesiredRecorded = true;
|
||||||
}
|
}
|
||||||
@@ -7044,13 +7010,8 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
// either way; everything else re-gens in place via BandRemeshQueue (no visual pop).
|
// either way; everything else re-gens in place via BandRemeshQueue (no visual pop).
|
||||||
{
|
{
|
||||||
const int32 CutMin = Settings ? Settings->GetEffectiveStrateContentCutMinLevel() : 9;
|
const int32 CutMin = Settings ? Settings->GetEffectiveStrateContentCutMinLevel() : 9;
|
||||||
// F18 — l'anneau feuille dépend aussi de la bande (sa strate de référence) : on l'arme
|
|
||||||
// dès que les feuilles sont actives, même si la coupe de contenu MC est désactivée.
|
|
||||||
const bool bSheetsWantBand = Settings && Settings->bFarSheetRing
|
|
||||||
&& Settings->RenderDistanceChunks > 0;
|
|
||||||
const int32 TopMC = VF_MaxMarchingCubesLevel(Settings);
|
|
||||||
int32 NewLo = MIN_int32, NewHi = MAX_int32;
|
int32 NewLo = MIN_int32, NewHi = MAX_int32;
|
||||||
if ((CutMin <= 8 || bSheetsWantBand) && StrateManager)
|
if (CutMin <= 8 && StrateManager)
|
||||||
{
|
{
|
||||||
int32 StrTopZ = 0, StrBotZ = 0;
|
int32 StrTopZ = 0, StrBotZ = 0;
|
||||||
if (StrateManager->GetStrateChunkZBounds(CenterChunk.Z, StrTopZ, StrBotZ))
|
if (StrateManager->GetStrateChunkZBounds(CenterChunk.Z, StrTopZ, StrBotZ))
|
||||||
@@ -7069,8 +7030,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
NewLo, NewHi, MeshBandChunkLo, MeshBandChunkHi, CutMin, CenterChunk.Z);
|
NewLo, NewHi, MeshBandChunkLo, MeshBandChunkHi, CutMin, CenterChunk.Z);
|
||||||
for (const FVoxelTileKey& T : LoadedTiles)
|
for (const FVoxelTileKey& T : LoadedTiles)
|
||||||
{
|
{
|
||||||
// Sheets (above the last MC bridge level) always depend on the strate band.
|
if (T.Level < CutMin) continue;
|
||||||
if (T.Level < CutMin && T.Level <= TopMC) continue;
|
|
||||||
const int32 CLo = T.Coord.Z << T.Level;
|
const int32 CLo = T.Coord.Z << T.Level;
|
||||||
const int32 CHi = ((T.Coord.Z + 1) << T.Level) - 1;
|
const int32 CHi = ((T.Coord.Z + 1) << T.Level) - 1;
|
||||||
const bool bInOld = CLo >= MeshBandChunkLo && CHi <= MeshBandChunkHi;
|
const bool bInOld = CLo >= MeshBandChunkLo && CHi <= MeshBandChunkHi;
|
||||||
@@ -7087,44 +7047,6 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The sheet XY hole follows the outermost MC bridge level and shrinks by one tile, leaving
|
|
||||||
// an overlap at the mesh transition. Requeue sheets overlapping either the old or new hole
|
|
||||||
// when the player crosses a bridge-level tile.
|
|
||||||
{
|
|
||||||
int32 NewMinX = MAX_int32, NewMinY = MAX_int32;
|
|
||||||
int32 NewMaxX = MIN_int32, NewMaxY = MIN_int32;
|
|
||||||
const int32 TopMCLvl = VF_MaxMarchingCubesLevel(Settings);
|
|
||||||
if (Settings && Settings->bFarSheetRing && Settings->RenderDistanceChunks > 0)
|
|
||||||
{
|
|
||||||
const int32 RClip = FMath::Max(1, Settings->ClipRadius);
|
|
||||||
const int32 Shrink = FMath::Max(0, RClip - 1);
|
|
||||||
const int32 ExtM = CHUNK_SIZE << TopMCLvl; // tuile MaxLevel en voxels
|
|
||||||
const FIntVector CM = VF_FloorDiv(CenterChunk, 1 << TopMCLvl); // tuile MaxLevel du joueur
|
|
||||||
NewMinX = (CM.X - Shrink) * ExtM;
|
|
||||||
NewMinY = (CM.Y - Shrink) * ExtM;
|
|
||||||
NewMaxX = (CM.X + Shrink + 1) * ExtM; // EXCLUSIF
|
|
||||||
NewMaxY = (CM.Y + Shrink + 1) * ExtM;
|
|
||||||
}
|
|
||||||
if (NewMinX != SheetHoleMinXVox || NewMinY != SheetHoleMinYVox
|
|
||||||
|| NewMaxX != SheetHoleMaxXVox || NewMaxY != SheetHoleMaxYVox)
|
|
||||||
{
|
|
||||||
for (const FVoxelTileKey& T : LoadedTiles)
|
|
||||||
{
|
|
||||||
if (T.Level <= TopMCLvl) continue; // seules les feuilles portent le trou
|
|
||||||
const int32 Ext = CHUNK_SIZE << T.Level;
|
|
||||||
const int32 TMinX = T.Coord.X * Ext, TMinY = T.Coord.Y * Ext;
|
|
||||||
const bool bOldOv = TMinX < SheetHoleMaxXVox && TMinX + Ext > SheetHoleMinXVox
|
|
||||||
&& TMinY < SheetHoleMaxYVox && TMinY + Ext > SheetHoleMinYVox;
|
|
||||||
const bool bNewOv = TMinX < NewMaxX && TMinX + Ext > NewMinX
|
|
||||||
&& TMinY < NewMaxY && TMinY + Ext > NewMinY;
|
|
||||||
if ((bOldOv || bNewOv) && IsDesired(T)) BandRemeshQueue.Add(T);
|
|
||||||
}
|
|
||||||
SheetHoleMinXVox = NewMinX; SheetHoleMinYVox = NewMinY;
|
|
||||||
SheetHoleMaxXVox = NewMaxX; SheetHoleMaxYVox = NewMaxY;
|
|
||||||
bAllChunksLoaded = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cull rules — STRICT LOAD-BEFORE-UNLOAD so crossing a shell boundary NEVER leaves a hole:
|
// Cull rules — STRICT LOAD-BEFORE-UNLOAD so crossing a shell boundary NEVER leaves a hole:
|
||||||
// - out of clip range → left the view entirely, no replacement coming → cull now.
|
// - out of clip range → left the view entirely, no replacement coming → cull now.
|
||||||
// - in range (mid-LOD-transition) → cull ONLY once EVERY desired tile that overlaps its
|
// - in range (mid-LOD-transition) → cull ONLY once EVERY desired tile that overlaps its
|
||||||
@@ -7391,20 +7313,9 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
|
|||||||
const int32 FullRes = Settings ? FMath::Max(1, Settings->FullResClipLevels) : 2;
|
const int32 FullRes = Settings ? FMath::Max(1, Settings->FullResClipLevels) : 2;
|
||||||
const int32 Extent = CHUNK_SIZE << Tile.Level;
|
const int32 Extent = CHUNK_SIZE << Tile.Level;
|
||||||
|
|
||||||
// F18 — sheet-ring keys follow the last MC bridge level and mesh as two heightfields
|
const int32 Cells = (Tile.Level < FullRes)
|
||||||
// (GenerateSheetMesh). Their sampling step matches that MC level; cells grow with sheet extent
|
|
||||||
// up to 128 per axis, after which the step increases.
|
|
||||||
const int32 TopMC = VF_MaxMarchingCubesLevel(Settings);
|
|
||||||
const bool bSheetTile = Tile.Level > TopMC;
|
|
||||||
|
|
||||||
int32 Cells = (Tile.Level < FullRes)
|
|
||||||
? CHUNK_SIZE
|
? CHUNK_SIZE
|
||||||
: (Settings ? FMath::Clamp(Settings->CoarseTileCells, 4, CHUNK_SIZE) : 16);
|
: (Settings ? FMath::Clamp(Settings->CoarseTileCells, 4, CHUNK_SIZE) : 16);
|
||||||
if (bSheetTile)
|
|
||||||
{
|
|
||||||
const int32 StepMC = FMath::Max(1, (CHUNK_SIZE << TopMC) / Cells);
|
|
||||||
Cells = FMath::Clamp(Extent / StepMC, 4, 128);
|
|
||||||
}
|
|
||||||
const int32 Step = FMath::Max(1, Extent / Cells);
|
const int32 Step = FMath::Max(1, Extent / Cells);
|
||||||
const uint32 TaskEpoch = GenerationEpoch;
|
const uint32 TaskEpoch = GenerationEpoch;
|
||||||
const uint32 TaskDesiredEpoch = DesiredEpoch;
|
const uint32 TaskDesiredEpoch = DesiredEpoch;
|
||||||
@@ -7425,40 +7336,8 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
|
|||||||
// below the configured minimum and all LODs in the gap (no band) mesh full.
|
// below the configured minimum and all LODs in the gap (no band) mesh full.
|
||||||
int32 BandVoxLo = INT32_MIN, BandVoxHi = INT32_MAX;
|
int32 BandVoxLo = INT32_MIN, BandVoxHi = INT32_MAX;
|
||||||
int32 BandChunkLo = MIN_int32, BandChunkHi = MAX_int32;
|
int32 BandChunkLo = MIN_int32, BandChunkHi = MAX_int32;
|
||||||
int32 SheetChunkZ = 0;
|
|
||||||
if (bSheetTile)
|
|
||||||
{
|
|
||||||
// F18 — la feuille a besoin de la STRATE de référence (les deux heightfields sont ceux de
|
|
||||||
// la strate du joueur) : pas de bande armée (gap inter-strates, ou bounds introuvables)
|
|
||||||
// ⇒ rien à mailler, tuile vide (re-queue automatique via BandRemeshQueue en atterrissant).
|
|
||||||
if (MeshBandChunkLo == MIN_int32)
|
|
||||||
{
|
|
||||||
FChunkResult Empty;
|
|
||||||
Empty.Tile = Tile;
|
|
||||||
Empty.Epoch = TaskEpoch;
|
|
||||||
Empty.DesiredEpoch = TaskDesiredEpoch;
|
|
||||||
Empty.ModificationEpoch = TaskModificationEpoch;
|
|
||||||
Empty.RequestStartCycles = RequestStartCycles;
|
|
||||||
Empty.WorkPriority = WorkPriority;
|
|
||||||
if (WorkPriority == EVoxelTileWorkPriority::CollisionCritical)
|
|
||||||
CriticalProcessQueue.Enqueue(MoveTemp(Empty));
|
|
||||||
else if (WorkPriority == EVoxelTileWorkPriority::Edited)
|
|
||||||
EditedProcessQueue.Enqueue(MoveTemp(Empty));
|
|
||||||
else if (WorkPriority == EVoxelTileWorkPriority::LOD0)
|
|
||||||
NearProcessQueue.Enqueue(MoveTemp(Empty));
|
|
||||||
else
|
|
||||||
ProcessQueue.Enqueue(MoveTemp(Empty));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
BandChunkLo = MeshBandChunkLo; // pour la résolution matériaux sol/cap dans ApplyMeshToTile
|
|
||||||
BandChunkHi = MeshBandChunkHi;
|
|
||||||
SheetChunkZ = MeshBandChunkLo + (MeshBandChunkHi - MeshBandChunkLo) / 2; // chunk au cœur de la strate
|
|
||||||
}
|
|
||||||
// F18 — trou XY courant (zone couverte par les coquilles MC, découpée des feuilles).
|
|
||||||
const int32 HoleMinX = SheetHoleMinXVox, HoleMinY = SheetHoleMinYVox;
|
|
||||||
const int32 HoleMaxX = SheetHoleMaxXVox, HoleMaxY = SheetHoleMaxYVox;
|
|
||||||
const int32 CutMin = Settings ? Settings->GetEffectiveStrateContentCutMinLevel() : 9;
|
const int32 CutMin = Settings ? Settings->GetEffectiveStrateContentCutMinLevel() : 9;
|
||||||
if (!bSheetTile && Tile.Level >= CutMin && MeshBandChunkLo != MIN_int32)
|
if (Tile.Level >= CutMin && MeshBandChunkLo != MIN_int32)
|
||||||
{
|
{
|
||||||
BandChunkLo = MeshBandChunkLo;
|
BandChunkLo = MeshBandChunkLo;
|
||||||
BandChunkHi = MeshBandChunkHi;
|
BandChunkHi = MeshBandChunkHi;
|
||||||
@@ -7512,7 +7391,6 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
|
|||||||
: UE::Tasks::ETaskPriority::BackgroundNormal;
|
: UE::Tasks::ETaskPriority::BackgroundNormal;
|
||||||
UE::Tasks::Launch(TEXT("ChunkGen"), [this, Tile, OriginVoxels, Step, Cells, TaskEpoch, bWantCapture,
|
UE::Tasks::Launch(TEXT("ChunkGen"), [this, Tile, OriginVoxels, Step, Cells, TaskEpoch, bWantCapture,
|
||||||
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
|
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
|
||||||
bSheetTile, SheetChunkZ, HoleMinX, HoleMinY, HoleMaxX, HoleMaxY,
|
|
||||||
RequestStartCycles, TaskDesiredEpoch, TaskModificationEpoch, Cancellation,
|
RequestStartCycles, TaskDesiredEpoch, TaskModificationEpoch, Cancellation,
|
||||||
WorkPriority]()
|
WorkPriority]()
|
||||||
{
|
{
|
||||||
@@ -7565,7 +7443,6 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
|
|||||||
Result.GenerationStartCycles = GenerationStartCycles;
|
Result.GenerationStartCycles = GenerationStartCycles;
|
||||||
GenerateTileResult(Tile, OriginVoxels, Step, Cells, TaskEpoch, bWantCapture,
|
GenerateTileResult(Tile, OriginVoxels, Step, Cells, TaskEpoch, bWantCapture,
|
||||||
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
|
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
|
||||||
bSheetTile, SheetChunkZ, HoleMinX, HoleMinY, HoleMaxX, HoleMaxY,
|
|
||||||
Result, &Cancellation->bObsolete);
|
Result, &Cancellation->bObsolete);
|
||||||
Result.GenerationEndCycles = GenerationStartCycles != 0
|
Result.GenerationEndCycles = GenerationStartCycles != 0
|
||||||
? FPlatformTime::Cycles64() : 0;
|
? FPlatformTime::Cycles64() : 0;
|
||||||
@@ -7611,8 +7488,6 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
|
|||||||
void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels,
|
void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels,
|
||||||
int32 Step, int32 Cells, uint32 Epoch, bool bWantCapture,
|
int32 Step, int32 Cells, uint32 Epoch, bool bWantCapture,
|
||||||
int32 BandVoxLo, int32 BandVoxHi, int32 BandChunkLo, int32 BandChunkHi,
|
int32 BandVoxLo, int32 BandVoxHi, int32 BandChunkLo, int32 BandChunkHi,
|
||||||
bool bSheetTile, int32 SheetChunkZ,
|
|
||||||
int32 HoleMinX, int32 HoleMinY, int32 HoleMaxX, int32 HoleMaxY,
|
|
||||||
FChunkResult& Result,
|
FChunkResult& Result,
|
||||||
const std::atomic<bool>* ObsoleteFlag)
|
const std::atomic<bool>* ObsoleteFlag)
|
||||||
{
|
{
|
||||||
@@ -7666,7 +7541,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
|||||||
/ static_cast<double>(WormEligibleSamples)
|
/ static_cast<double>(WormEligibleSamples)
|
||||||
: 0.0;
|
: 0.0;
|
||||||
UE_LOG(LogTemp, Display,
|
UE_LOG(LogTemp, Display,
|
||||||
TEXT("[VoxelForgeTileProfile] tile=(%d,%d,%d) level=%d step=%d cells=%d sheet=%d aborted=%d empty=%d triangles=%d ceiling_triangles=%d "
|
TEXT("[VoxelForgeTileProfile] tile=(%d,%d,%d) level=%d step=%d cells=%d aborted=%d empty=%d triangles=%d ceiling_triangles=%d "
|
||||||
"band=(%d,%d) "
|
"band=(%d,%d) "
|
||||||
"verdict=%d proof=%d air_proof=%d classify=%.6f mesh=%.6f streams=%.6f seconds=%.6f "
|
"verdict=%d proof=%d air_proof=%d classify=%.6f mesh=%.6f streams=%.6f seconds=%.6f "
|
||||||
"cache_build=%.6f evaluation=%.6f "
|
"cache_build=%.6f evaluation=%.6f "
|
||||||
@@ -7686,7 +7561,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
|||||||
"worm_eligible=%llu worm_block_proofs=%llu worm_block_skipped=%llu "
|
"worm_eligible=%llu worm_block_proofs=%llu worm_block_skipped=%llu "
|
||||||
"worm_skip_rate=%.6f"),
|
"worm_skip_rate=%.6f"),
|
||||||
Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, Tile.Level, Step, Cells,
|
Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, Tile.Level, Step, Cells,
|
||||||
bSheetTile ? 1 : 0, Result.bAborted ? 1 : 0,
|
Result.bAborted ? 1 : 0,
|
||||||
Result.bEmpty ? 1 : 0, Result.NumTriangles, Result.NumCeilingTriangles,
|
Result.bEmpty ? 1 : 0, Result.NumTriangles, Result.NumCeilingTriangles,
|
||||||
BandChunkLo, BandChunkHi,
|
BandChunkLo, BandChunkHi,
|
||||||
ClassifyVerdict, Result.bSealedSolidProof ? 1 : 0,
|
ClassifyVerdict, Result.bSealedSolidProof ? 1 : 0,
|
||||||
@@ -7825,7 +7700,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
|||||||
// (Gate IsoLevel == 0 : les verdicts du classifieur supposent l'iso MC à zéro exactement.)
|
// (Gate IsoLevel == 0 : les verdicts du classifieur supposent l'iso MC à zéro exactement.)
|
||||||
bool bTrivialEmpty = false;
|
bool bTrivialEmpty = false;
|
||||||
if (GVoxelForgeSealedSolidProof != 0
|
if (GVoxelForgeSealedSolidProof != 0
|
||||||
&& !bSheetTile && !bWantCapture && Generator && Mesher
|
&& !bWantCapture && Generator && Mesher
|
||||||
&& Mesher->IsoLevel == 0.0f)
|
&& Mesher->IsoLevel == 0.0f)
|
||||||
{
|
{
|
||||||
TotalSealedSolidProofCandidates.fetch_add(1, std::memory_order_relaxed);
|
TotalSealedSolidProofCandidates.fetch_add(1, std::memory_order_relaxed);
|
||||||
@@ -7867,7 +7742,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
|||||||
}
|
}
|
||||||
const int32 OuterClassifierMode = FMath::Clamp(GVoxelForgeOuterClassifierMode, 0, 1);
|
const int32 OuterClassifierMode = FMath::Clamp(GVoxelForgeOuterClassifierMode, 0, 1);
|
||||||
if (!bTrivialEmpty && OuterClassifierMode != 0
|
if (!bTrivialEmpty && OuterClassifierMode != 0
|
||||||
&& !bSheetTile && !bWantCapture && Generator && Mesher && Mesher->IsoLevel == 0.0f)
|
&& !bWantCapture && Generator && Mesher && Mesher->IsoLevel == 0.0f)
|
||||||
{
|
{
|
||||||
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_ClassifyTile);
|
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_ClassifyTile);
|
||||||
INC_DWORD_STAT(STAT_VoxelForgeTilesClassified);
|
INC_DWORD_STAT(STAT_VoxelForgeTilesClassified);
|
||||||
@@ -7972,8 +7847,6 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
|||||||
bTrivialEmpty = (ValidatedVerdict != EVoxelTileClass::Mixed);
|
bTrivialEmpty = (ValidatedVerdict != EVoxelTileClass::Mixed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// F18 — feuille : deux heightfields sol/cap échantillonnés par colonne (pas de marching
|
|
||||||
// cubes, pas de classifieur — la classe de surface est vraie par construction).
|
|
||||||
// `TilesMeshed` peut dépasser `TilesClassified` : les tuiles qui ratent cette porte sont
|
// `TilesMeshed` peut dépasser `TilesClassified` : les tuiles qui ratent cette porte sont
|
||||||
// maillées sans classification. / `TilesMeshed` may exceed `TilesClassified`: tiles that
|
// maillées sans classification. / `TilesMeshed` may exceed `TilesClassified`: tiles that
|
||||||
// fail this gate are meshed without classification.
|
// fail this gate are meshed without classification.
|
||||||
@@ -7982,13 +7855,10 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
|||||||
{
|
{
|
||||||
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_GenerateMesh);
|
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_GenerateMesh);
|
||||||
const double MeshStartSeconds = bMeasureTile ? FPlatformTime::Seconds() : 0.0;
|
const double MeshStartSeconds = bMeasureTile ? FPlatformTime::Seconds() : 0.0;
|
||||||
MeshData = bSheetTile
|
MeshData = Mesher->GenerateMesh(OriginVoxels, Step, Cells,
|
||||||
? Mesher->GenerateSheetMesh(OriginVoxels, Step, Cells, SheetChunkZ,
|
bWantCapture ? &Result.CaptureGrid : nullptr,
|
||||||
HoleMinX, HoleMinY, HoleMaxX, HoleMaxY)
|
BandVoxLo, BandVoxHi,
|
||||||
: Mesher->GenerateMesh(OriginVoxels, Step, Cells,
|
&Result.MesherDensityCalls);
|
||||||
bWantCapture ? &Result.CaptureGrid : nullptr,
|
|
||||||
BandVoxLo, BandVoxHi,
|
|
||||||
&Result.MesherDensityCalls);
|
|
||||||
TotalMesherDensityCalls.fetch_add(
|
TotalMesherDensityCalls.fetch_add(
|
||||||
static_cast<uint64>(Result.MesherDensityCalls), std::memory_order_relaxed);
|
static_cast<uint64>(Result.MesherDensityCalls), std::memory_order_relaxed);
|
||||||
MeshSeconds = bMeasureTile ? FPlatformTime::Seconds() - MeshStartSeconds : 0.0;
|
MeshSeconds = bMeasureTile ? FPlatformTime::Seconds() - MeshStartSeconds : 0.0;
|
||||||
@@ -8041,7 +7911,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
|||||||
// Parity instrumentation observes the same FVoxelMeshData that the runtime stream builder
|
// Parity instrumentation observes the same FVoxelMeshData that the runtime stream builder
|
||||||
// consumes. It is intentionally after the abort fence, so a canceled worker cannot publish a
|
// consumes. It is intentionally after the abort fence, so a canceled worker cannot publish a
|
||||||
// partial identity, and it records empty/proof tiles as well as visible meshes.
|
// partial identity, and it records empty/proof tiles as well as visible meshes.
|
||||||
RecordTileHash(Tile, OriginVoxels, Step, Cells, BandChunkLo, BandChunkHi, bSheetTile, MeshData);
|
RecordTileHash(Tile, OriginVoxels, Step, Cells, BandChunkLo, BandChunkHi, MeshData);
|
||||||
|
|
||||||
Result.ClassifySeconds = ClassifySeconds;
|
Result.ClassifySeconds = ClassifySeconds;
|
||||||
Result.MeshSeconds = MeshSeconds;
|
Result.MeshSeconds = MeshSeconds;
|
||||||
@@ -8157,7 +8027,7 @@ void AVoxelWorld::UnloadTile(const FVoxelTileKey& Tile)
|
|||||||
CollisionNotRequiredTiles.Remove(Tile);
|
CollisionNotRequiredTiles.Remove(Tile);
|
||||||
CollisionSolidTiles.Remove(Tile);
|
CollisionSolidTiles.Remove(Tile);
|
||||||
PendingCollisionCooks.Remove(Tile);
|
PendingCollisionCooks.Remove(Tile);
|
||||||
// Water + decorations are no longer tile-bound (water is one player-following ocean plane via
|
// Water + decorations are not tile-bound (water is one player-following ocean plane via
|
||||||
// UpdateWater; decorations stream by distance via UpdateDecorations) — nothing to clear per tile.
|
// UpdateWater; decorations stream by distance via UpdateDecorations) — nothing to clear per tile.
|
||||||
if (URealtimeMeshComponent** Comp = TileComponents.Find(Tile))
|
if (URealtimeMeshComponent** Comp = TileComponents.Find(Tile))
|
||||||
{
|
{
|
||||||
@@ -8333,7 +8203,7 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
|
|||||||
}
|
}
|
||||||
BindRealtimeMeshCollisionEvent(RTMesh);
|
BindRealtimeMeshCollisionEvent(RTMesh);
|
||||||
// Shadow casting: far (level >= 2) tiles never cast; the sky-cap SECTION never casts either
|
// Shadow casting: far (level >= 2) tiles never cast; the sky-cap SECTION never casts either
|
||||||
// — otherwise the high rock ceiling shadows the entire terrain below it. F17: shadow is now
|
// — otherwise the high rock ceiling shadows the entire terrain below it. F17: shadow is
|
||||||
// PER SECTION, so a mixed tile keeps its ground shadow while its cap stays shadowless.
|
// PER SECTION, so a mixed tile keeps its ground shadow while its cap stays shadowless.
|
||||||
const bool bCastShadow = (Tile.Level <= 1);
|
const bool bCastShadow = (Tile.Level <= 1);
|
||||||
MeshComp->SetCastShadow(bCastShadow);
|
MeshComp->SetCastShadow(bCastShadow);
|
||||||
@@ -8453,7 +8323,7 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
|
|||||||
TotalSeconds);
|
TotalSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Water is no longer spawned per tile — it's a single player-following ocean plane (UpdateWater,
|
// Water is not spawned per tile — it's a single player-following ocean plane (UpdateWater,
|
||||||
// driven from Tick), so it renders at every LOD and to the horizon with no per-tile gaps.
|
// driven from Tick), so it renders at every LOD and to the horizon with no per-tile gaps.
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -9135,9 +9005,9 @@ void AVoxelWorld::UpdateOrbLightMPC()
|
|||||||
const FVoxelActiveOrb& O = Orbs[i];
|
const FVoxelActiveOrb& O = Orbs[i];
|
||||||
V = FLinearColor((float)O.WorldPos.X, (float)O.WorldPos.Y, (float)O.WorldPos.Z, O.FalloffWorld);
|
V = FLinearColor((float)O.WorldPos.X, (float)O.WorldPos.Y, (float)O.WorldPos.Z, O.FalloffWorld);
|
||||||
}
|
}
|
||||||
// ALWAYS write, even when unchanged. A skip-if-identical cache was tried here and BROKE the
|
// ALWAYS write, even when unchanged. A skip-if-identical cache BREAKS the lighting: the
|
||||||
// lighting: the MPC's world INSTANCE can be reset/recreated behind our back (PIE init order,
|
// MPC's world INSTANCE can be reset/recreated behind our back (PIE init order, asset
|
||||||
// asset recompile), and a cached skip then leaves it holding defaults forever. The per-frame
|
// recompile), and a cached skip then leaves it holding defaults forever. The per-frame
|
||||||
// rewrite is what makes the collection self-healing — and 4 vector writes cost nothing.
|
// rewrite is what makes the collection self-healing — and 4 vector writes cost nothing.
|
||||||
UKismetMaterialLibrary::SetVectorParameterValue(this, OrbLightMPC, OrbNames[i], V);
|
UKismetMaterialLibrary::SetVectorParameterValue(this, OrbLightMPC, OrbNames[i], V);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,16 +76,6 @@ namespace
|
|||||||
|
|
||||||
namespace VoxelWormField
|
namespace VoxelWormField
|
||||||
{
|
{
|
||||||
float EvaluateExact(
|
|
||||||
float WorldX,
|
|
||||||
float WorldY,
|
|
||||||
float WorldZ,
|
|
||||||
float Threshold,
|
|
||||||
const FParameters& Parameters)
|
|
||||||
{
|
|
||||||
return EvaluateExactField(WorldX, WorldY, WorldZ, Threshold, Parameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
float Evaluate(
|
float Evaluate(
|
||||||
float WorldX,
|
float WorldX,
|
||||||
float WorldY,
|
float WorldY,
|
||||||
|
|||||||
@@ -22,9 +22,9 @@
|
|||||||
#include "VoxelStrateTypes.h" // FStrateDecoration / FStrateAmbientActor
|
#include "VoxelStrateTypes.h" // FStrateDecoration / FStrateAmbientActor
|
||||||
#include "VoxelBiomeDefinition.generated.h"
|
#include "VoxelBiomeDefinition.generated.h"
|
||||||
|
|
||||||
// IWYU : utilisé en pointeur seulement ⇒ déclaration avant. Fournie gratuitement par le PCH
|
// IWYU : utilisé en pointeur seulement ⇒ déclaration avant. Le PCH partagé ne la fournit pas :
|
||||||
// partagé jusqu'ici ; `FPSemantics = Precise` nous en sort. / Pointer-only use, so a forward
|
// `FPSemantics = Precise` nous en sort. / Pointer-only use, so a forward declaration is enough.
|
||||||
// declaration is enough. The shared PCH used to provide this for free.
|
// The shared PCH does not provide it (FPSemantics = Precise opts this module out of it).
|
||||||
class UMaterialInterface;
|
class UMaterialInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ namespace VoxelHash
|
|||||||
* collident donneraient un bruit identique PARTOUT. Salé par site, il faudrait qu'ils
|
* collident donneraient un bruit identique PARTOUT. Salé par site, il faudrait qu'ils
|
||||||
* collident sur les ~50 sites à la fois — c'est-à-dire jamais.
|
* collident sur les ~50 sites à la fois — c'est-à-dire jamais.
|
||||||
*
|
*
|
||||||
* The multiplier no longer decorrelates by amplifying — it IDENTIFIES the site, and the hash
|
* The multiplier does not decorrelate by amplifying — it IDENTIFIES the site, and the hash
|
||||||
* decorrelates. Output is already in final units and bounded, so the ULP is 10% of a voxel step.
|
* decorrelates. Output is already in final units and bounded, so the ULP is 10% of a voxel step.
|
||||||
*
|
*
|
||||||
* @param SiteKey la constante littérale d'origine (`7.3f`, `97.7f`, …). Gardée VISIBLE au site
|
* @param SiteKey la constante littérale d'origine (`7.3f`, `97.7f`, …). Gardée VISIBLE au site
|
||||||
@@ -439,10 +439,10 @@ namespace VoxelMazeTopology
|
|||||||
// le motif qui a produit `AUDIT §C1` (un correctif appliqué à une copie sur deux). Il monte donc au
|
// le motif qui a produit `AUDIT §C1` (un correctif appliqué à une copie sur deux). Il monte donc au
|
||||||
// point le plus bas qui voit déjà le hash, et le générateur comme la pile d'opérateurs l'appellent.
|
// point le plus bas qui voit déjà le hash, et le générateur comme la pile d'opérateurs l'appellent.
|
||||||
//
|
//
|
||||||
// Ce corps était `static float CellularNoise3D(const FVector&)` dans VoxelGenerator.cpp, invisible
|
// Il vit ici pour que le générateur et la pile d'opérateurs partagent la même implémentation.
|
||||||
// à la pile d'opérateurs. Il vit ici pour que les deux chemins partagent la même implémentation.
|
|
||||||
// The hot overload uses FVector3f: noise coordinates are floats, so no double round-trip is paid
|
// The hot overload uses FVector3f: noise coordinates are floats, so no double round-trip is paid
|
||||||
// before the hash/lattice arithmetic. Output compatibility with the old field is not a contract.
|
// before the hash/lattice arithmetic. Output compatibility with a double-precision evaluation is
|
||||||
|
// not a contract.
|
||||||
//
|
//
|
||||||
// Algorithme : distance au point-feature le plus proche dans une grille hachée.
|
// Algorithme : distance au point-feature le plus proche dans une grille hachée.
|
||||||
// 1. cellule entière du point 2. voisinage 3×3×3 3. rendre (F2 − F1), normalisé ~[-1, 1]
|
// 1. cellule entière du point 2. voisinage 3×3×3 3. rendre (F2 − F1), normalisé ~[-1, 1]
|
||||||
@@ -520,7 +520,7 @@ namespace VoxelNoise
|
|||||||
// PER-CHUNK SDF CACHE
|
// PER-CHUNK SDF CACHE
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// The room list and tunnel connections are IDENTICAL for all voxels in a chunk.
|
// The room list and tunnel connections are IDENTICAL for all voxels in a chunk.
|
||||||
// Without caching, EvaluateSDF rebuilds these 32,768 times per 32³ chunk:
|
// Without caching, a per-voxel evaluation would rebuild these 32,768 times per 32³ chunk:
|
||||||
// - Hash every grid cell in the search area
|
// - Hash every grid cell in the search area
|
||||||
// - Determine room existence, position, size
|
// - Determine room existence, position, size
|
||||||
// - Run O(N²) nearest-neighbor backbone
|
// - Run O(N²) nearest-neighbor backbone
|
||||||
@@ -604,9 +604,9 @@ struct FCachedRoom
|
|||||||
float RoomOpWeight = 1.0f;
|
float RoomOpWeight = 1.0f;
|
||||||
|
|
||||||
// PRE-BAKED SHAPE (BuildChunkCache). The shape roll, variety thresholds and the capsule's
|
// PRE-BAKED SHAPE (BuildChunkCache). The shape roll, variety thresholds and the capsule's
|
||||||
// Cos/Sin direction used to be re-derived PER VOXEL per room inside EvaluateSDFCached — hash
|
// Cos/Sin direction are constants of the room: re-deriving them PER VOXEL inside
|
||||||
// mixes + trig in the hottest loop of the plugin for values that are constants of the room.
|
// EvaluateSDFCached would put hash mixes + trig in the hottest loop of the plugin.
|
||||||
// Bit-identical to the old per-voxel roll (same hashes, same math, done once per chunk).
|
// Bit-identical to a per-voxel roll (same hashes, same math, done once per chunk).
|
||||||
// 0 = ellipsoid → ShapeA = radii (x=y=RadiusXY, z=RadiusZ)
|
// 0 = ellipsoid → ShapeA = radii (x=y=RadiusXY, z=RadiusZ)
|
||||||
// 1 = rounded box → ShapeA = half-extents, ShapeR = corner rounding
|
// 1 = rounded box → ShapeA = half-extents, ShapeR = corner rounding
|
||||||
// 2 = capsule → ShapeA/ShapeB = world endpoints, ShapeR = tube radius
|
// 2 = capsule → ShapeA/ShapeB = world endpoints, ShapeR = tube radius
|
||||||
@@ -637,9 +637,8 @@ struct FCachedRoomFloorJoin
|
|||||||
|
|
||||||
// One immutable floor segment authored while the tunnel chain is built. The floor is continuous
|
// One immutable floor segment authored while the tunnel chain is built. The floor is continuous
|
||||||
// unless bLedgeTransition is set; that marker means the segment terminates at a deliberate
|
// unless bLedgeTransition is set; that marker means the segment terminates at a deliberate
|
||||||
// dramatic ledge and the vertical riser is placed at the next control-point boundary. NumSteps is
|
// dramatic ledge and the vertical riser is placed at the next control-point boundary. A steep
|
||||||
// retained as a compatibility/diagnostic field and is always zero for newly authored tunnels: a
|
// floor is never quantised into a staircase. ReliefScale is baked with a derivative bound,
|
||||||
// steep floor is never quantised into a staircase. ReliefScale is baked with a derivative bound,
|
|
||||||
// so evaluation only reads this profile and never decides a floor shape per voxel.
|
// so evaluation only reads this profile and never decides a floor shape per voxel.
|
||||||
struct FTunnelFloorSegmentProfile
|
struct FTunnelFloorSegmentProfile
|
||||||
{
|
{
|
||||||
@@ -652,9 +651,6 @@ struct FTunnelFloorSegmentProfile
|
|||||||
float NaturalEndFloorZ = 0.0f;
|
float NaturalEndFloorZ = 0.0f;
|
||||||
float ReliefScale = 0.0f;
|
float ReliefScale = 0.0f;
|
||||||
bool bLedgeTransition = false;
|
bool bLedgeTransition = false;
|
||||||
// Deprecated staircase count. New profiles leave this at zero; keep the member so old
|
|
||||||
// diagnostic consumers can still compile while the authored representation has one meaning.
|
|
||||||
int32 NumSteps = 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// A pre-computed wandering tunnel chain — all connection decisions and hash-derived properties
|
// A pre-computed wandering tunnel chain — all connection decisions and hash-derived properties
|
||||||
@@ -933,13 +929,10 @@ struct FChunkSDFCache
|
|||||||
VoxelDensityProfile::FCacheMemoryBreakdown GetAllocatedSizeBreakdown() const;
|
VoxelDensityProfile::FCacheMemoryBreakdown GetAllocatedSizeBreakdown() const;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** World-space tunnel shape query. Legacy support-floor fields remain source-compatible but are
|
/** World-space tunnel shape query. */
|
||||||
* no longer produced or consumed by the graph-tunnel generation path. */
|
|
||||||
struct FTunnelCoreWorldEvaluation
|
struct FTunnelCoreWorldEvaluation
|
||||||
{
|
{
|
||||||
float SDF = FLT_MAX;
|
float SDF = FLT_MAX;
|
||||||
// Deprecated compatibility output. The separate graph-tunnel support slab was removed.
|
|
||||||
bool bSupportFloor = false;
|
|
||||||
// Room geometry owns the lower side while the world-space tunnel is inside a room. The
|
// Room geometry owns the lower side while the world-space tunnel is inside a room. The
|
||||||
// tunnel SDF may still reopen air there, but its swept bottom must not turn that room air back
|
// tunnel SDF may still reopen air there, but its swept bottom must not turn that room air back
|
||||||
// into a shelf or wall.
|
// into a shelf or wall.
|
||||||
@@ -955,7 +948,6 @@ struct FTunnelCoreWorldEvaluation
|
|||||||
// CAVE MORPHOLOGY EVALUATOR
|
// CAVE MORPHOLOGY EVALUATOR
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// Two-phase evaluation: BuildChunkCache (once per chunk) + EvaluateSDFCached (per voxel).
|
// Two-phase evaluation: BuildChunkCache (once per chunk) + EvaluateSDFCached (per voxel).
|
||||||
// The original EvaluateSDF is kept as a convenience wrapper for backward compatibility.
|
|
||||||
|
|
||||||
namespace VoxelCaveMorphology
|
namespace VoxelCaveMorphology
|
||||||
{
|
{
|
||||||
@@ -1065,15 +1057,6 @@ namespace VoxelCaveMorphology
|
|||||||
bool* OutRoomOwnsBottom = nullptr
|
bool* OutRoomOwnsBottom = nullptr
|
||||||
);
|
);
|
||||||
|
|
||||||
// Evaluate the raw union of cached graph-tunnel capsules. This is intentionally separate from
|
|
||||||
// the SmoothMin morphology: the generator uses it after terrain/passage posts to reassert the
|
|
||||||
// tunnel's walkable air core without reintroducing a connector or changing room ownership.
|
|
||||||
VOXELFORGE_API float EvaluateTunnelCoreSDF(
|
|
||||||
float WorldX, float WorldY, float WorldZ,
|
|
||||||
const FChunkSDFCache& Cache,
|
|
||||||
bool bUseSpatialIndex = true
|
|
||||||
);
|
|
||||||
|
|
||||||
// Evaluate the final structural air contract against the world-space wandering chain. Room
|
// Evaluate the final structural air contract against the world-space wandering chain. Room
|
||||||
// SDFs remain evaluated in warped coordinates; only this post uses world coordinates so its
|
// SDFs remain evaluated in warped coordinates; only this post uses world coordinates so its
|
||||||
// walkable floor follows the authored chain exactly.
|
// walkable floor follows the authored chain exactly.
|
||||||
@@ -1106,46 +1089,6 @@ namespace VoxelCaveMorphology
|
|||||||
float ReachScale = 1.0f
|
float ReachScale = 1.0f
|
||||||
);
|
);
|
||||||
|
|
||||||
// Deprecated compatibility query for the removed graph-tunnel support slab. Production
|
|
||||||
// generation does not call this predicate; the tunnel's own swept shape owns its floor.
|
|
||||||
VOXELFORGE_API bool IsTunnelSupportFloorWorldPoint(
|
|
||||||
float WorldX, float WorldY, float WorldZ,
|
|
||||||
const FChunkSDFCache& Cache,
|
|
||||||
bool bUseSpatialIndex = true
|
|
||||||
);
|
|
||||||
|
|
||||||
// Deprecated compatibility helper for callers that still inspect the old support-band
|
|
||||||
// diagnostic. It is detached from production density generation.
|
|
||||||
VOXELFORGE_API void BuildTunnelSupportFloorColumn(
|
|
||||||
float WorldX, float WorldY,
|
|
||||||
const FChunkSDFCache& Cache,
|
|
||||||
FTunnelSupportFloorColumn& OutColumn,
|
|
||||||
bool bUseSpatialIndex = true
|
|
||||||
);
|
|
||||||
|
|
||||||
VOXELFORGE_API bool IsTunnelSupportFloorColumnZ(
|
|
||||||
float WorldZ,
|
|
||||||
const FTunnelSupportFloorColumn& Column
|
|
||||||
);
|
|
||||||
|
|
||||||
// Returns the legacy projected band for one tunnel at this XY column. The world evaluator no
|
|
||||||
// longer uses it to write a support slab.
|
|
||||||
VOXELFORGE_API bool GetTunnelSupportFloorColumnBand(
|
|
||||||
int32 TunnelIndex,
|
|
||||||
const FTunnelSupportFloorColumn& Column,
|
|
||||||
float& OutFloorZ,
|
|
||||||
float& OutMinZ,
|
|
||||||
float& OutMaxZ
|
|
||||||
);
|
|
||||||
|
|
||||||
// CONVENIENCE WRAPPER: builds a temporary cache and evaluates in one call.
|
|
||||||
// Use this for one-off queries (debug visualization, single-point sampling).
|
|
||||||
// For chunk generation, use BuildChunkCache + EvaluateSDFCached instead.
|
|
||||||
float EvaluateSDF(
|
|
||||||
float WorldX, float WorldY, float WorldZ,
|
|
||||||
const FStrateGenerationParams& Params,
|
|
||||||
uint32 Seed, int32 StrateIndex
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -17,7 +17,8 @@
|
|||||||
// budgeted. The game thread never does decoration density math. Flow (UpdateDecorations each Tick):
|
// budgeted. The game thread never does decoration density math. Flow (UpdateDecorations each Tick):
|
||||||
// - recompute desired cell set on player cell-boundary / strate change
|
// - recompute desired cell set on player cell-boundary / strate change
|
||||||
// - LaunchDecoTasks: fire async march tasks (capped by MaxConcurrentDecorationTasks)
|
// - LaunchDecoTasks: fire async march tasks (capped by MaxConcurrentDecorationTasks)
|
||||||
// - ProcessDecoResults: drain finished tasks' results, apply (spawn) budgeted, epoch-guarded
|
// - ProcessDecoResults: drain finished cell results into their region builds (BuildId-guarded),
|
||||||
|
// then apply completed regions (spawn) budgeted
|
||||||
// Decorations exist ONLY in the player's current strate (march is strate-bounded) → a strate change
|
// Decorations exist ONLY in the player's current strate (march is strate-bounded) → a strate change
|
||||||
// wipes + rebuilds them, and there is no cross-strate light bleed to cull.
|
// wipes + rebuilds them, and there is no cross-strate light bleed to cull.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -1,34 +1,32 @@
|
|||||||
// VoxelDensityOp.h
|
// VoxelDensityOp.h
|
||||||
// LE CONTRAT de la pile d'opérateurs de densité / THE density operator stack CONTRACT.
|
// LE CONTRAT de la pile d'opérateurs de densité / THE density operator stack CONTRACT.
|
||||||
// Phase 1 de OPSTACK-PLAN.md. The contract is implemented by the opt-in stack path in
|
// Design : Docs/archive/OPSTACK-PLAN.md. The contract is implemented by the stack path in
|
||||||
// UVoxelGenerator::GetDensityAt; the legacy archetype switch remains for non-opt-in strates.
|
// UVoxelGenerator::GetDensityAt, which generates every strate.
|
||||||
//
|
//
|
||||||
// ─────────────────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────────────────
|
||||||
// POURQUOI / WHY
|
// POURQUOI / WHY
|
||||||
// ─────────────────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────────────────
|
||||||
// UVoxelGenerator::GetDensityAt est aujourd'hui un `switch` sur 8 ECaveGeneratorType, chacun
|
// Chaque archétype (ECaveGeneratorType) est une composition d'opérateurs, pas une fonction de
|
||||||
// possédant sa fonction de densité et son struct de params. Conséquences : une nouvelle idée de
|
// densité monolithique qui posséderait le voxel entier. Une idée de monde s'exprime en empilant
|
||||||
// monde coûte ~6 sites d'édition, et surtout LES IDÉES NE PEUVENT PAS SE COMBINER — un archétype
|
// sources, combinateurs et modificateurs — et les idées peuvent donc SE COMBINER : « une strate de
|
||||||
// possède le voxel entier. On ne peut pas écrire « une strate de surface dont les montagnes
|
// surface dont les montagnes contiennent un réseau de salles, avec des îles flottantes au-dessus ».
|
||||||
// contiennent un réseau de salles, avec des îles flottantes dans le vide au-dessus », à aucun prix.
|
|
||||||
//
|
//
|
||||||
// GetDensityAt is today a `switch` over 8 ECaveGeneratorType, each owning a bespoke density
|
// Each archetype (ECaveGeneratorType) is a composition of operators, not a monolithic density
|
||||||
// function and param struct. A new world idea costs ~6 edit sites, and — the real problem —
|
// function that owns the whole voxel. A world idea is expressed by stacking sources, combiners and
|
||||||
// IDEAS CANNOT COMBINE: one archetype owns the whole voxel.
|
// modifiers — so ideas can COMBINE.
|
||||||
//
|
//
|
||||||
// ─────────────────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────────────────
|
||||||
// ⚠️ CE N'EST PAS L'ANCIEN SYSTÈME DE « ROOM OPERATIONS » / THIS IS NOT THE OLD ROOM-OPS SYSTEM
|
// ⚠️ CE N'EST PAS L'ANCIEN SYSTÈME DE « ROOM OPERATIONS » / THIS IS NOT THE OLD ROOM-OPS SYSTEM
|
||||||
// ─────────────────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────────────────
|
||||||
// UVoxelTerrainOpDefinition (Terrace, Ribbing, Cliff, Scallop, Overhang, Arch, Column, Pit…) ne
|
// UVoxelTerrainOpDefinition (Terrace, Ribbing, Cliff, Scallop, Overhang, Arch, Column, Pit…) ne
|
||||||
// sait que PERTURBER une densité près d'une surface qui existe déjà. Il ne décide jamais ce que le
|
// sait que PERTURBER une densité près d'une surface qui existe déjà. Il ne décide jamais ce que le
|
||||||
// champ EST — cette décision vit dans le `switch`. Une pile d'opérateurs qui se contenterait de
|
// champ EST — c'est le rôle des sources de champ ci-dessous.
|
||||||
// cela aurait reconstruit le switch avec des étapes en plus.
|
|
||||||
//
|
//
|
||||||
// D'où QUATRE RÔLES, dont l'ancien système n'occupait que le troisième :
|
// D'où QUATRE RÔLES, dont l'ancien système n'occupait que le troisième :
|
||||||
//
|
//
|
||||||
// 1. FIELD SOURCE — fabrique un champ À PARTIR DE RIEN. C'est ce qui fait qu'une grotte est
|
// 1. FIELD SOURCE — fabrique un champ À PARTIR DE RIEN. C'est ce qui fait qu'une grotte est
|
||||||
// une grotte et qu'un monde ouvert est un monde ouvert. Chaque archétype
|
// une grotte et qu'un monde ouvert est un monde ouvert. Chaque archétype
|
||||||
// d'aujourd'hui est fondamentalement l'une de ces sources.
|
// est fondamentalement l'une de ces sources.
|
||||||
// 2. COMBINER — comment deux champs fusionnent (min/max/smooth/mask). C'EST le rôle qui
|
// 2. COMBINER — comment deux champs fusionnent (min/max/smooth/mask). C'EST le rôle qui
|
||||||
// achète la composition ; sans lui il n'y a pas de refactor.
|
// achète la composition ; sans lui il n'y a pas de refactor.
|
||||||
// 3. DETAIL MODIFIER — l'ancien système, rétrogradé à un rôle sur quatre. Inchangé.
|
// 3. DETAIL MODIFIER — l'ancien système, rétrogradé à un rôle sur quatre. Inchangé.
|
||||||
@@ -56,9 +54,8 @@
|
|||||||
// An SDF-only writer reports an SDF interval; a later converter folds that interval through its own
|
// An SDF-only writer reports an SDF interval; a later converter folds that interval through its own
|
||||||
// response. Each operator answers for itself, so an unverified composition remains sound.
|
// response. Each operator answers for itself, so an unverified composition remains sound.
|
||||||
//
|
//
|
||||||
// La première version de cette interface n'avait qu'une direction numérique. Presque tout opérateur
|
// Presque tout opérateur est UNIDIRECTIONNEL : il ne fait que creuser, ou que remplir. La direction
|
||||||
// existant est UNIDIRECTIONNEL : il ne fait que creuser, ou que remplir. Cela suffit à reproduire
|
// suffit donc à exprimer GÉNÉRIQUEMENT les gardes de ClassifyTile :
|
||||||
// GÉNÉRIQUEMENT chaque garde écrite à la main dans ClassifyTile :
|
|
||||||
//
|
//
|
||||||
// « passages ⇒ bCanSolid = false » EST CarveOnly
|
// « passages ⇒ bCanSolid = false » EST CarveOnly
|
||||||
// « ponts/arêtes ⇒ bCanAir = false » EST FillOnly
|
// « ponts/arêtes ⇒ bCanAir = false » EST FillOnly
|
||||||
@@ -71,26 +68,16 @@
|
|||||||
// it never lets an SDF writer impersonate a downstream neighbour.
|
// it never lets an SDF writer impersonate a downstream neighbour.
|
||||||
//
|
//
|
||||||
// ─────────────────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────────────────
|
||||||
// LE GROS LOT PERF : les strates de grotte ne sautent AUCUNE tuile aujourd'hui
|
// CLASSIFYTILE
|
||||||
// THE PERF PRIZE: cave strates skip ZERO tiles today
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────────────────
|
||||||
// ClassifyTile conserve ses preuves exactes pour les gaps de bedrock et SurfaceWorld ; les
|
// ClassifyTile garde ses preuves exactes pour les gaps de bedrock et SurfaceWorld ; les slots de
|
||||||
// archétypes de cave opt-in passent maintenant par le même pliage `ClassifyBox`. Le post XY fournit
|
// cave passent par le pliage `ClassifyBox` de la pile même qui produit leur densité. Le post XY
|
||||||
// en plus la première preuve globale de coque : une tuile entièrement dans la bande forcée peut être
|
// fournit en plus une preuve globale de coque : une tuile entièrement dans la bande forcée peut être
|
||||||
// sautée sans connaître l'archétype ou le Z.
|
// sautée sans connaître l'archétype ou le Z.
|
||||||
//
|
//
|
||||||
// **Traiter cela comme un livrable explicite de chaque portage, pas comme un effet de bord.**
|
// ClassifyTile keeps its exact proofs for bedrock gaps and SurfaceWorld; cave slots go through the
|
||||||
//
|
// `ClassifyBox` fold of the same stack that produces their density. The XY post adds a global
|
||||||
// ─────────────────────────────────────────────────────────────────────────────────────────
|
// shell proof: a tile entirely inside the forced band is skipped without knowing archetype or Z.
|
||||||
// ÉTAT / STATUS
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────────────────
|
|
||||||
// Les huit archétypes ont maintenant une pile derrière l'opt-in `bUseOperatorStack`, et ClassifyTile
|
|
||||||
// utilise cette pile pour les slots de cave opt-in. Le chemin legacy reste disponible pour les
|
|
||||||
// strates non opt-in; les tests d'équivalence servent de garde de portage.
|
|
||||||
//
|
|
||||||
// All eight archetypes now have an operator-stack path behind `bUseOperatorStack`, and ClassifyTile
|
|
||||||
// uses that stack for opt-in cave slots. The legacy switch remains available for opt-out strates;
|
|
||||||
// equivalence tests are port-correctness guards.
|
|
||||||
//
|
//
|
||||||
// NOTE sur les UENUM : ces types sont volontairement du C++ nu (pas d'UHT, pas de .generated.h).
|
// NOTE sur les UENUM : ces types sont volontairement du C++ nu (pas d'UHT, pas de .generated.h).
|
||||||
// Ils deviendront UENUM/USTRUCT en Phase 3, quand les opérateurs deviendront des data assets et
|
// Ils deviendront UENUM/USTRUCT en Phase 3, quand les opérateurs deviendront des data assets et
|
||||||
@@ -337,11 +324,6 @@ struct FVoxelOpSample
|
|||||||
// FLT_MAX = "no surface nearby" — the initial state, and the early-out placed sources use.
|
// FLT_MAX = "no surface nearby" — the initial state, and the early-out placed sources use.
|
||||||
float Sdf = FLT_MAX;
|
float Sdf = FLT_MAX;
|
||||||
|
|
||||||
// Deprecated structural-floor compatibility fields. Graph tunnels now carry their floor in
|
|
||||||
// the swept shape itself; the production stack does not use these fields to write a slab.
|
|
||||||
bool bProtectedStructuralFloor = false;
|
|
||||||
float StructuralFloorMinimumDensity = 0.0f;
|
|
||||||
|
|
||||||
// Block evaluation keeps the room source's per-sample selection explicit. The scalar path
|
// Block evaluation keeps the room source's per-sample selection explicit. The scalar path
|
||||||
// still uses the source's worker-local state, but a later block-capable modifier must never
|
// still uses the source's worker-local state, but a later block-capable modifier must never
|
||||||
// observe the state belonging to the last sample of the preceding operator.
|
// observe the state belonging to the last sample of the preceding operator.
|
||||||
@@ -353,7 +335,6 @@ struct FVoxelOpSample
|
|||||||
// consulting a last-sample TLS value after an op-major evaluation.
|
// consulting a last-sample TLS value after an op-major evaluation.
|
||||||
bool bHasTunnelCoreWorldEvaluation = false;
|
bool bHasTunnelCoreWorldEvaluation = false;
|
||||||
float TunnelCoreWorldSDF = FLT_MAX;
|
float TunnelCoreWorldSDF = FLT_MAX;
|
||||||
bool bTunnelCoreSupportFloor = false;
|
|
||||||
bool bTunnelCoreRoomFloor = false;
|
bool bTunnelCoreRoomFloor = false;
|
||||||
bool bHasTunnelCoreSweptFloor = false;
|
bool bHasTunnelCoreSweptFloor = false;
|
||||||
float TunnelCoreSweptFloorZ = -FLT_MAX;
|
float TunnelCoreSweptFloorZ = -FLT_MAX;
|
||||||
@@ -720,13 +701,12 @@ public:
|
|||||||
/**
|
/**
|
||||||
* DIAGNOSTIC UNIQUEMENT — le nom que les rapports de test impriment pour cet opérateur.
|
* DIAGNOSTIC UNIQUEMENT — le nom que les rapports de test impriment pour cet opérateur.
|
||||||
*
|
*
|
||||||
* ⚠️ POURQUOI CETTE MÉTHODE EXISTE, ET CE QU'ELLE A COÛTÉ DE NE PAS AVOIR. Le premier build de
|
* ⚠️ POURQUOI CETTE MÉTHODE EXISTE. Sans nom par opérateur, un rapport « 0 tuile prouvée sur
|
||||||
* l'`EffectOverBox` spatial est revenu **vert avec 0 tuile prouvée sur 40**, et la seule chose
|
* 40 » ne peut dire que « ou bien les tuiles traversent toutes une grotte, ou bien la source
|
||||||
* que le rapport pouvait dire était « ou bien les tuiles traversent toutes une grotte, ou bien
|
* n'atteint pas sa branche `Identity` » — deux causes, zéro nombre pour les départager, alors
|
||||||
* la source n'atteint pas sa branche `Identity` ». Deux causes, zéro nombre pour les
|
* que la vraie cause peut être un TROISIÈME opérateur (p. ex. un ver qui rend `CarveOnly`
|
||||||
* départager — exactement le piège que ce projet a déjà payé plusieurs fois. La vraie cause
|
* partout). Avec un nom par opérateur, `ClassifyBoxAttributed` répond « c'est celui-là » au
|
||||||
* était un TROISIÈME opérateur (le ver, qui rendait `CarveOnly` partout). Avec un nom par
|
* lieu de laisser deviner.
|
||||||
* opérateur, `ClassifyBoxAttributed` répond « c'est celui-là » au lieu de laisser deviner.
|
|
||||||
*
|
*
|
||||||
* N'entre dans AUCUNE clé de cache, dans aucun hash, dans aucune décision de génération : le
|
* N'entre dans AUCUNE clé de cache, dans aucun hash, dans aucune décision de génération : le
|
||||||
* changer ne peut pas changer le monde. Le défaut est volontairement laconique — un opérateur
|
* changer ne peut pas changer le monde. Le défaut est volontairement laconique — un opérateur
|
||||||
|
|||||||
@@ -2,34 +2,21 @@
|
|||||||
// La PILE : un conteneur ordonné d'opérateurs, plus les fabriques d'opérateurs concrets.
|
// La PILE : un conteneur ordonné d'opérateurs, plus les fabriques d'opérateurs concrets.
|
||||||
// The STACK: an ordered container of operators, plus the concrete-operator factories.
|
// The STACK: an ordered container of operators, plus the concrete-operator factories.
|
||||||
//
|
//
|
||||||
// ⚠️ CECI ALIMENTE LE JEU, MAIS SEULEMENT SUR OPT-IN (depuis OPSTACK-PLAN §4, Phase 1, point 3).
|
// ⚠️ CECI EST LE CHEMIN DE GÉNÉRATION DU JEU.
|
||||||
// `UVoxelGenerator::GetDensityAt` construit la pile par chunk et l'évalue à la place du `switch`
|
// `UVoxelGenerator::GetDensityAt` construit la pile par chunk pour chaque strate :
|
||||||
// UNIQUEMENT quand `UVoxelStrateManager::UsesOperatorStackForChunk` rend true — c.-à-d. quand la
|
|
||||||
// strate a coché `bUseOperatorStack` ET que son archétype figure dans la liste des portés :
|
|
||||||
// **Maze, FlatPlain, CrystalChamber, SurfaceWorld, VerticalShafts, FloatingIslands, TunnelNetwork,
|
// **Maze, FlatPlain, CrystalChamber, SurfaceWorld, VerticalShafts, FloatingIslands, TunnelNetwork,
|
||||||
// Underwater (8 sur 8)**.
|
// Underwater (8 sur 8)**. `ClassifyTile` construit la même pile pour les archétypes de cave et plie
|
||||||
// Toute autre strate passe encore par le `switch`, inchangé.
|
// `ClassifyBox`; les gaps et SurfaceWorld gardent leurs preuves exactes dédiées.
|
||||||
// `ClassifyTile` est branché pour les archétypes de cave opt-in : il construit la même pile et
|
|
||||||
// plie `ClassifyBox`; les gaps et SurfaceWorld gardent leurs preuves exactes dédiées.
|
|
||||||
//
|
//
|
||||||
// THIS FEEDS THE GAME, BUT ONLY BEHIND AN OPT-IN. GetDensityAt builds the stack per chunk and
|
// THIS IS THE GAME'S GENERATION PATH. GetDensityAt builds the stack per chunk for every strate.
|
||||||
// evaluates it instead of the switch only when UsesOperatorStackForChunk returns true (strate ticked
|
// ClassifyTile uses the same stack for cave archetypes; gaps and SurfaceWorld retain their exact
|
||||||
// bUseOperatorStack AND its archetype is ported — all 8). ClassifyTile uses the same stack for cave
|
// hand-written proofs.
|
||||||
// archetypes; gaps and SurfaceWorld retain their exact hand-written proofs.
|
|
||||||
//
|
|
||||||
// ⛔ NE JAMAIS faire tourner les deux chemins dans le même monde.
|
|
||||||
// ⚠️ EN REVANCHE, LES COMPARER EST DEVENU LÉGITIME — cette ligne disait l'inverse et elle est
|
|
||||||
// périmée. `AUDIT §C10` (le résidu ~1 ULP) est CLOS depuis `FPSemantics = Precise` : les cinq tests
|
|
||||||
// d'équivalence comparent bit à bit et sont verts. Ils ne sont plus des contrôles de FIDÉLITÉ (la
|
|
||||||
// barre `§2.6.1` n'exige aucune ressemblance avec l'ancien monde) mais des oracles de
|
|
||||||
// CORRECTION DE PORTAGE — une faute de transcription reste un vrai bug, et l'ancienne fonction est
|
|
||||||
// le moyen le moins cher de l'attraper.
|
|
||||||
//
|
//
|
||||||
// POURQUOI CETTE FORME / WHY THIS SHAPE
|
// POURQUOI CETTE FORME / WHY THIS SHAPE
|
||||||
// La question à laquelle la Phase 1 doit répondre n'est pas « est-ce que ça marche ? » mais
|
// La question à laquelle cette forme répond n'est pas « est-ce que ça marche ? » mais
|
||||||
// **« est-ce que la séparation source / modifier tombe naturellement du code existant ? »**
|
// **« est-ce que la séparation source / modifier tombe naturellement du code existant ? »**
|
||||||
// (OPSTACK-PLAN §4, le déclencheur d'arrêt). En portant Maze hors du chemin chaud et en le
|
// (OPSTACK-PLAN §4). Chaque archétype a été porté puis comparé bit à bit à sa fonction
|
||||||
// comparant à l'original, cette question reçoit une réponse MESURÉE plutôt qu'une opinion.
|
// d'origine avant qu'elle ne soit retirée : la réponse a été MESURÉE plutôt qu'une opinion.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
@@ -203,10 +190,10 @@ public:
|
|||||||
return EvalSample(WorldX, WorldY, WorldZ).Density;
|
return EvalSample(WorldX, WorldY, WorldZ).Density;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** L'état COMPLET (densité + SDF) après toute la pile. Diagnostic : quand une comparaison
|
/** L'état COMPLET (densité + SDF) après toute la pile. Diagnostic : quand deux évaluations
|
||||||
* avec l'ancien chemin diverge, c'est le canal SDF qui dit si l'écart naît avant ou après
|
* divergent, c'est le canal SDF qui dit si l'écart naît avant ou après la conversion.
|
||||||
* la conversion. / The full state after the stack — the SDF channel is what says whether a
|
* / The full state after the stack — the SDF channel is what says whether a divergence is
|
||||||
* divergence is born before or after the carve. */
|
* born before or after the carve. */
|
||||||
FVoxelOpSample EvalSample(float WorldX, float WorldY, float WorldZ) const
|
FVoxelOpSample EvalSample(float WorldX, float WorldY, float WorldZ) const
|
||||||
{
|
{
|
||||||
FVoxelOpSample S;
|
FVoxelOpSample S;
|
||||||
@@ -229,11 +216,11 @@ public:
|
|||||||
|
|
||||||
// The room-graph source publishes the authored floor ownership while evaluating the
|
// The room-graph source publishes the authored floor ownership while evaluating the
|
||||||
// shape, but it must not be reasserted here. This stack is followed by the common
|
// shape, but it must not be reasserted here. This stack is followed by the common
|
||||||
// disturbance/passage writers; composing the floor at this point made every authored
|
// disturbance/passage writers; composing the floor at this point would make every authored
|
||||||
// floor sample look like a post-hoc clamp (and counted 100% of them). The generator now
|
// floor sample look like a post-hoc clamp (and counted 100% of them). The generator
|
||||||
// consumes the same immutable core result after those writers and composes the floor once,
|
// consumes the same immutable core result after those writers and composes the floor once,
|
||||||
// at the final MC boundary. The metadata remains on the sample for that hand-off and for
|
// at the final MC boundary. The metadata remains on the sample for that hand-off and for
|
||||||
// diagnostics, but it is no longer allowed to modify density inside the operator stack.
|
// diagnostics, but it is not allowed to modify density inside the operator stack.
|
||||||
return S;
|
return S;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -568,12 +555,6 @@ namespace VoxelDensityOps
|
|||||||
* `FillOnly` quand une colonne atteint la boîte, `Identity` (le cas courant) sinon. */
|
* `FillOnly` quand une colonne atteint la boîte, `Identity` (le cas courant) sinon. */
|
||||||
VOXELFORGE_API TUniquePtr<IVoxelDensityOp> MakeGridColumnMod(const FSlabGenerationParams& P, int32 Seed);
|
VOXELFORGE_API TUniquePtr<IVoxelDensityOp> MakeGridColumnMod(const FSlabGenerationParams& P, int32 Seed);
|
||||||
|
|
||||||
/** Rôle 1 — le pont entre les deux espaces : consomme les piles de HAUTEUR (sol + voûte,
|
|
||||||
* `VoxelHeightOp.h`) et en fait une densité. `IsXYPure()` est **false** — les hauteurs sont
|
|
||||||
* pures en XY, la densité est une distance à celles-ci et ne peut pas l'être. */
|
|
||||||
VOXELFORGE_API TUniquePtr<IVoxelDensityOp> MakeSurfaceColumnSource(const FSurfaceGenerationParams& P,
|
|
||||||
int32 Seed);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SurfaceWorld, COMPLET : colonne (sol + voûte) → densité, overhang 3D, post structurel, et le
|
* SurfaceWorld, COMPLET : colonne (sol + voûte) → densité, overhang 3D, post structurel, et le
|
||||||
* mélange de biomes quand `PerBiomeParams` est non vide.
|
* mélange de biomes quand `PerBiomeParams` est non vide.
|
||||||
@@ -597,9 +578,8 @@ namespace VoxelDensityOps
|
|||||||
* FlatPlain ET CrystalChamber — la même pile, **sans branchement sur le type** :
|
* FlatPlain ET CrystalChamber — la même pile, **sans branchement sur le type** :
|
||||||
* SlabVoidSource → GridColumnMod → [structural post ×4]
|
* SlabVoidSource → GridColumnMod → [structural post ×4]
|
||||||
*
|
*
|
||||||
* C'est le premier vrai gain du refactor (OPSTACK-PLAN §4) : deux des huit archétypes
|
* Deux des huit archétypes tiennent dans un opérateur (OPSTACK-PLAN §4) : leur différence est
|
||||||
* disparaissent dans un opérateur, et leur différence redevient ce qu'elle était déjà dans
|
* un jeu de valeurs par défaut, pas du code.
|
||||||
* `GetSlabDensity` — un jeu de valeurs par défaut, pas du code.
|
|
||||||
*/
|
*/
|
||||||
VOXELFORGE_API void BuildSlabStack(FVoxelOpStack& OutStack, const FSlabGenerationParams& P,
|
VOXELFORGE_API void BuildSlabStack(FVoxelOpStack& OutStack, const FSlabGenerationParams& P,
|
||||||
int32 Seed, float SpineRadius,
|
int32 Seed, float SpineRadius,
|
||||||
@@ -611,10 +591,8 @@ namespace VoxelDensityOps
|
|||||||
* ConstantRock → ShaftField → SdfRoughness → SdfCarve → ShaftLedge
|
* ConstantRock → ShaftField → SdfRoughness → SdfCarve → ShaftLedge
|
||||||
* → ShaftConnectorAir → ShaftConnectorFloor → [structural post ×4]
|
* → ShaftConnectorAir → ShaftConnectorFloor → [structural post ×4]
|
||||||
*
|
*
|
||||||
* C'est la démonstration que `§2.5` promettait : dans le `switch`, Maze et VerticalShafts sont
|
* Trois de ces ops sont ceux de Maze (`§2.5`), avec une source différente et d'autres réglages
|
||||||
* deux fonctions de ~100 lignes sans rien de commun à l'œil ; en opérateurs, ce sont les mêmes
|
* (fréquence 0.1 au lieu de 0.12, fenêtre `rough + 4` au lieu de `R + rough + 2`).
|
||||||
* trois ops avec une source différente et d'autres réglages (fréquence 0.1 au lieu de 0.12,
|
|
||||||
* fenêtre `rough + 4` au lieu de `R + rough + 2`).
|
|
||||||
*/
|
*/
|
||||||
VOXELFORGE_API void BuildVerticalShaftStack(FVoxelOpStack& OutStack, const FVerticalShaftParams& P,
|
VOXELFORGE_API void BuildVerticalShaftStack(FVoxelOpStack& OutStack, const FVerticalShaftParams& P,
|
||||||
int32 Seed, float SpineRadius,
|
int32 Seed, float SpineRadius,
|
||||||
|
|||||||
@@ -68,8 +68,8 @@ FORCEINLINE void VF_ApplyBoundarySeal(float& Density, float WorldZ,
|
|||||||
// CARVE DE PASSAGE / PASSAGE CARVING
|
// CARVE DE PASSAGE / PASSAGE CARVING
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// Creuse un passage inter-strates. Évalué APRÈS le seal pour que les passages puissent percer à
|
// Creuse un passage inter-strates. Évalué APRÈS le seal pour que les passages puissent percer à
|
||||||
// travers le bouchon solide. Le rayon de blend hard-codé à 4.0f correspond à l'ancienne valeur —
|
// travers le bouchon solide. Le rayon de blend est hard-codé à 4.0f — à exposer via
|
||||||
// à exposer via UVoxelSettings si on veut pouvoir le tweaker.
|
// UVoxelSettings si on veut pouvoir le tweaker.
|
||||||
FORCEINLINE void VF_ApplyPassageCarving(float& Density, float ModSDF,
|
FORCEINLINE void VF_ApplyPassageCarving(float& Density, float ModSDF,
|
||||||
float BaseDensity, float SealThickness)
|
float BaseDensity, float SealThickness)
|
||||||
{
|
{
|
||||||
@@ -300,7 +300,6 @@ FORCEINLINE void VF_ApplyXYEdgeSealMC(float& Density, float WorldX, float WorldY
|
|||||||
// mathematical inner edge the helper is a true no-op, so claiming AllSolid there would be false.
|
// mathematical inner edge the helper is a true no-op, so claiming AllSolid there would be false.
|
||||||
namespace VoxelDensityReach
|
namespace VoxelDensityReach
|
||||||
{
|
{
|
||||||
constexpr float SpineBlend = 3.0f;
|
|
||||||
constexpr float PassageBlend = 4.0f;
|
constexpr float PassageBlend = 4.0f;
|
||||||
constexpr float EdgeProofMargin = 1.0f;
|
constexpr float EdgeProofMargin = 1.0f;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -312,7 +312,6 @@ namespace VoxelDensityProfile
|
|||||||
FCacheMemoryBreakdown RoomGraphCacheBreakdown;
|
FCacheMemoryBreakdown RoomGraphCacheBreakdown;
|
||||||
};
|
};
|
||||||
|
|
||||||
VOXELFORGE_API void SetEnabled(bool bEnabled);
|
|
||||||
VOXELFORGE_API void SetMode(EMode Mode, uint32 SampleInterval = DefaultSampleInterval);
|
VOXELFORGE_API void SetMode(EMode Mode, uint32 SampleInterval = DefaultSampleInterval);
|
||||||
// Counters are collected only while a diagnostic mode is enabled. In the ordinary clean
|
// Counters are collected only while a diagnostic mode is enabled. In the ordinary clean
|
||||||
// path this is a single relaxed load, so the measurement hooks stay dormant.
|
// path this is a single relaxed load, so the measurement hooks stay dormant.
|
||||||
|
|||||||
@@ -177,9 +177,9 @@ private:
|
|||||||
void LaunchPendingFills(); // flush PendingFills onto the dedicated fill thread
|
void LaunchPendingFills(); // flush PendingFills onto the dedicated fill thread
|
||||||
void DrainResults(); // apply finished fills into the toroidal arrays (+ GPU dirty)
|
void DrainResults(); // apply finished fills into the toroidal arrays (+ GPU dirty)
|
||||||
|
|
||||||
// DEDICATED FILL THREAD. The volume fill used to run on the shared UE::Tasks pool (BackgroundLow),
|
// DEDICATED FILL THREAD. On the shared UE::Tasks pool (BackgroundLow) the volume fill STARVES
|
||||||
// where it STARVED behind mesh-gen (10 s to resolve shadows at a fresh spot). It now runs on its own
|
// behind mesh-gen (10 s to resolve shadows at a fresh spot). It runs on its own thread (off the
|
||||||
// thread (off the pool) so it's fast AND never steals a core from mesh-gen. Game thread enqueues
|
// pool) so it's fast AND never steals a core from mesh-gen. Game thread enqueues
|
||||||
// FPendingFill (Spsc), the thread re-evaluates GetDensityAt and pushes FFillResult into Results
|
// FPendingFill (Spsc), the thread re-evaluates GetDensityAt and pushes FFillResult into Results
|
||||||
// (existing Mpsc, drained on the game thread by DrainResults). Capture still short-circuits most of
|
// (existing Mpsc, drained on the game thread by DrainResults). Capture still short-circuits most of
|
||||||
// this (cache hits blit on the game thread, no fill); the thread is the backstop for misses.
|
// this (cache hits blit on the game thread, no fill); the thread is the backstop for misses.
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
//
|
//
|
||||||
// Pipeline density-only: plus de grille de blocs, plus de surface heightfield.
|
// Pipeline density-only: plus de grille de blocs, plus de surface heightfield.
|
||||||
// Le mesher appelle GetDensityAt() par voxel pour reconstruire la géométrie.
|
// Le mesher appelle GetDensityAt() par voxel pour reconstruire la géométrie.
|
||||||
// Toute la logique de caves vit dans GetDensityWithParams / GetSlabDensity,
|
// Chaque strate est générée par sa pile d'opérateurs (VoxelDensityOpStack.h), pilotée par les
|
||||||
// pilotée par les params de strate récupérés auprès du StrateManager.
|
// params de strate récupérés auprès du StrateManager.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
@@ -202,12 +202,11 @@ public:
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Densité pour une strate TunnelNetwork (rooms + tunnels + worm noise).
|
* Densité pour une strate TunnelNetwork (rooms + tunnels + worm noise).
|
||||||
* Utilisée en interne par GetDensityAt quand la strate est de ce type.
|
* C'est l'évaluateur fusionné de la pile TunnelNetwork/Underwater dans GetDensityAt.
|
||||||
* Exposée pour permettre des tests isolés avec des params custom.
|
* Exposée pour permettre des tests isolés avec des params custom.
|
||||||
*
|
*
|
||||||
* ⚠️ `ParamsFingerprint` ET `LayoutVersion` SONT OBLIGATOIRES, ET C'EST LE CORRECTIF
|
* ⚠️ `ParamsFingerprint` ET `LayoutVersion` SONT OBLIGATOIRES (`AUDIT §C2`). Le cache SDF
|
||||||
* D'`AUDIT §C2` (2026-07-28). Le cache SDF interne est clé sur (boîte XY, strate, seed) et
|
* interne est clé sur (boîte XY, strate, seed) et PAS sur les params. Or `GetGenerationParams` BLENDE les params à l'intérieur d'une même
|
||||||
* PAS sur les params. Or `GetGenerationParams` BLENDE les params à l'intérieur d'une même
|
|
||||||
* strate — `Alpha` dépend du chunk Z en mode `Gradient` (le DÉFAUT, avec
|
* strate — `Alpha` dépend du chunk Z en mode `Gradient` (le DÉFAUT, avec
|
||||||
* `TransitionBlendChunks = 2`) et du chunk XY en plus en mode `Interleaved`. Deux chunks de la
|
* `TransitionBlendChunks = 2`) et du chunk XY en plus en mode `Interleaved`. Deux chunks de la
|
||||||
* même strate, même seed, donc même clé, mais des params DIFFÉRENTS : le worker évalue le
|
* même strate, même seed, donc même clé, mais des params DIFFÉRENTS : le worker évalue le
|
||||||
@@ -243,56 +242,8 @@ public:
|
|||||||
bool bCollectFusedDiagnostics = false,
|
bool bCollectFusedDiagnostics = false,
|
||||||
// Optional same-sample hand-off from EvaluateSDFCached. This is
|
// Optional same-sample hand-off from EvaluateSDFCached. This is
|
||||||
// only a room-shape ownership bit; it does not alter the scalar
|
// only a room-shape ownership bit; it does not alter the scalar
|
||||||
// result and avoids a duplicate room-index query in native paths.
|
// result and avoids a duplicate room-index query.
|
||||||
bool* OutRoomOwnsBottom = nullptr,
|
bool* OutRoomOwnsBottom = nullptr) const;
|
||||||
// Optional same-sample warped position. Native GetDensityAt uses
|
|
||||||
// this to feed the core post without applying the cave warp twice.
|
|
||||||
FVector* OutWarpedPosition = nullptr) const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Densité pour une strate Slab (FlatPlain / CrystalChamber).
|
|
||||||
* Vide horizontal entre un sol bruité et un plafond bruité.
|
|
||||||
*/
|
|
||||||
float GetSlabDensity(float WorldX, float WorldY, float WorldZ,
|
|
||||||
const FSlabGenerationParams& Params) const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Densité pour une strate Maze — couloirs sur un treillis 3D déterministe, avec les décisions
|
|
||||||
* parent/boucle reconstruites par cellule dans un cache thread-local; le hot path ne parcourt
|
|
||||||
* que les capsules déjà émises.
|
|
||||||
*/
|
|
||||||
float GetMazeDensity(float WorldX, float WorldY, float WorldZ,
|
|
||||||
const FMazeGenerationParams& Params) const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Densité pour une strate SurfaceWorld — terrain à ciel ouvert (collines,
|
|
||||||
* montagnes, plages) sous un plafond solide, avec nappe d'eau optionnelle.
|
|
||||||
*
|
|
||||||
* Biome output-blend: the heightfield is evaluated with ParamsD (the voxel's dominant
|
|
||||||
* biome) and, when NeighborWeight > 0, also with ParamsN (its nearest neighbour); the
|
|
||||||
* two surface HEIGHTS are lerped. This stays seamless across ANY param difference
|
|
||||||
* (frequencies included). With no biomes, pass ParamsD == ParamsN and weight 0 →
|
|
||||||
* bit-identical to the single-param terrain. Structural fields (Z bounds, seal, base,
|
|
||||||
* water level) must be equal in both (forced from the strate).
|
|
||||||
*/
|
|
||||||
float GetSurfaceDensity(float WorldX, float WorldY, float WorldZ,
|
|
||||||
const FSurfaceGenerationParams& ParamsD,
|
|
||||||
const FSurfaceGenerationParams& ParamsN,
|
|
||||||
float NeighborWeight) const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Densité pour une strate VerticalShafts — puits verticaux pleine hauteur,
|
|
||||||
* vires horizontales, et connecteurs horizontaux occasionnels.
|
|
||||||
*/
|
|
||||||
float GetVerticalShaftDensity(float WorldX, float WorldY, float WorldZ,
|
|
||||||
const FVerticalShaftParams& Params) const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Densité pour une strate FloatingIslands — masses de terre suspendues dans
|
|
||||||
* un grand vide ouvert (îles flottantes), sommets aplatis, dessous rugueux.
|
|
||||||
*/
|
|
||||||
float GetFloatingIslandDensity(float WorldX, float WorldY, float WorldZ,
|
|
||||||
const FFloatingIslandParams& Params) const;
|
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// CLIMATE & BIOME FIELDS (pure XY, deterministic, window-invariant)
|
// CLIMATE & BIOME FIELDS (pure XY, deterministic, window-invariant)
|
||||||
@@ -309,20 +260,19 @@ public:
|
|||||||
* La chaîne de hauteur complète de SurfaceWorld : structural → cliff → terrace → layer lines →
|
* La chaîne de hauteur complète de SurfaceWorld : structural → cliff → terrace → layer lines →
|
||||||
* plage. Rend une ALTITUDE monde en voxels, pas une densité.
|
* plage. Rend une ALTITUDE monde en voxels, pas une densité.
|
||||||
*
|
*
|
||||||
* PUBLIQUE pour la même raison que `GetSlabDensity` / `GetMazeDensity` : permettre un test
|
* PUBLIQUE pour permettre un test isolé : c'est la référence de
|
||||||
* isolé. C'est la référence de `VoxelForge.OpStack.SurfaceHeightEquivalence`, qui compare la
|
* `VoxelForge.OpStack.SurfaceHeightEquivalence`, qui compare la pile d'opérateurs de hauteur
|
||||||
* pile d'opérateurs de hauteur (`VoxelHeightOp.h`) à cette fonction point par point.
|
* (`VoxelHeightOp.h`) à cette fonction point par point.
|
||||||
* Public so the height-op stack can be measured against it — same reason as GetSlabDensity.
|
* Public so the height-op stack can be measured against it.
|
||||||
*/
|
*/
|
||||||
float ComputeSurfaceTerrainZ(float WorldX, float WorldY, const FSurfaceGenerationParams& Params) const;
|
float ComputeSurfaceTerrainZ(float WorldX, float WorldY, const FSurfaceGenerationParams& Params) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* La colonne de surface : terrain Z, plafond, et le gate d'OVERHANG résolu par colonne
|
* La colonne de surface : terrain Z, plafond, et le gate d'OVERHANG résolu par colonne
|
||||||
* (amplitude + direction amont). PUBLIQUES toutes deux pour la même raison que ci-dessus :
|
* (amplitude + direction amont). C'est la colonne que `ClassifyTile` teste pour SurfaceWorld ;
|
||||||
* c'est le seul chemin qui calcule l'overhang — `GetSurfaceDensity` passe `OverhangAmp = 0` —
|
* publique pour que les tests puissent placer des échantillons dans la fenêtre d'overhang.
|
||||||
* donc c'est la seule référence possible pour `FOverhangShelfMod`.
|
* The surface column ClassifyTile tests for SurfaceWorld; public so tests can aim samples at
|
||||||
* Public because this is the ONLY path that computes the overhang (GetSurfaceDensity passes 0),
|
* the overhang window.
|
||||||
* so it is the only possible reference for the ported op.
|
|
||||||
*/
|
*/
|
||||||
void ComputeSurfaceColumn(float WorldX, float WorldY, int32 ChunkZ,
|
void ComputeSurfaceColumn(float WorldX, float WorldY, int32 ChunkZ,
|
||||||
const FSurfaceGenerationParams& BaseSurface, const FBiomeContext& BiomeCtx,
|
const FSurfaceGenerationParams& BaseSurface, const FBiomeContext& BiomeCtx,
|
||||||
@@ -330,12 +280,6 @@ public:
|
|||||||
float& OutTerrainZ, float& OutCeilSurf,
|
float& OutTerrainZ, float& OutCeilSurf,
|
||||||
float& OutOverhangAmp, float& OutDirX, float& OutDirY) const;
|
float& OutOverhangAmp, float& OutDirX, float& OutDirY) const;
|
||||||
|
|
||||||
/** Le combine par voxel : colonne → densité, overhang compris, puis le post structurel. */
|
|
||||||
float SurfaceDensityFromColumn(float WorldX, float WorldY, float WorldZ,
|
|
||||||
float TerrainZ, float CeilSurf,
|
|
||||||
float OverhangAmp, float DirX, float DirY,
|
|
||||||
const FSurfaceGenerationParams& S) const;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Moisture field at a world XY → [0,1]. The second climate axis for biome placement.
|
* Moisture field at a world XY → [0,1]. The second climate axis for biome placement.
|
||||||
*/
|
*/
|
||||||
@@ -415,7 +359,7 @@ public:
|
|||||||
*
|
*
|
||||||
* GAP (bedrock), hors-layout (air constant) et SurfaceWorld plient leurs hypothèses par Z ;
|
* GAP (bedrock), hors-layout (air constant) et SurfaceWorld plient leurs hypothèses par Z ;
|
||||||
* SurfaceWorld évalue ses colonnes terrain/plafond avec le MÊME ComputeSurfaceColumn que le
|
* SurfaceWorld évalue ses colonnes terrain/plafond avec le MÊME ComputeSurfaceColumn que le
|
||||||
* chemin densité. Un unique slot cave opt-in peut ajouter le verdict conservatif de sa pile,
|
* chemin densité. Un unique slot cave peut ajouter le verdict conservatif de sa pile,
|
||||||
* sous gardes d'archétype et de params bit-identiques. Spine/passages/disturbances/diff restent
|
* sous gardes d'archétype et de params bit-identiques. Spine/passages/disturbances/diff restent
|
||||||
* des gardes conservatrices. Worker-safe (lecture seule + caches thread_local partagés avec
|
* des gardes conservatrices. Worker-safe (lecture seule + caches thread_local partagés avec
|
||||||
* GetDensityAt — un verdict Mixed laisse les colonnes chaudes pour la génération qui suit).
|
* GetDensityAt — un verdict Mixed laisse les colonnes chaudes pour la génération qui suit).
|
||||||
@@ -468,11 +412,6 @@ private:
|
|||||||
FSurfaceGenerationParams& OutSurface, FBiomeContext& OutBiomeCtx,
|
FSurfaceGenerationParams& OutSurface, FBiomeContext& OutBiomeCtx,
|
||||||
TArray<FSurfaceGenerationParams>& OutBiomeParams) const;
|
TArray<FSurfaceGenerationParams>& OutBiomeParams) const;
|
||||||
|
|
||||||
// ComputeSurfaceColumn et SurfaceDensityFromColumn ont été DÉPLACÉES en `public` (voir plus
|
|
||||||
// haut) : c'est le seul chemin qui calcule l'overhang, donc la seule référence possible pour
|
|
||||||
// VoxelForge.OpStack.SurfaceHeightEquivalence. Une seule déclaration chacune.
|
|
||||||
// Moved to public above — the only path that computes the overhang, hence the only oracle.
|
|
||||||
|
|
||||||
/** (Re)build the per-chunk biome cell grid covering chunk (X,Y) footprint + margin. */
|
/** (Re)build the per-chunk biome cell grid covering chunk (X,Y) footprint + margin. */
|
||||||
void RebuildBiomeGrid(int32 ChunkX, int32 ChunkY, int32 ChunkZ,
|
void RebuildBiomeGrid(int32 ChunkX, int32 ChunkY, int32 ChunkZ,
|
||||||
const FBiomeContext& Ctx, FChunkBiomeCache& Cache,
|
const FBiomeContext& Ctx, FChunkBiomeCache& Cache,
|
||||||
|
|||||||
@@ -78,35 +78,6 @@ public:
|
|||||||
int32 BandZMinVox = INT32_MIN, int32 BandZMaxVox = INT32_MAX,
|
int32 BandZMinVox = INT32_MIN, int32 BandZMaxVox = INT32_MAX,
|
||||||
int64* OutDensitySampleCount = nullptr);
|
int64* OutDensitySampleCount = nullptr);
|
||||||
|
|
||||||
/**
|
|
||||||
* F18 — FEUILLE de champ lointain (anneau render-distance, cf. UVoxelSettings::bFarSheetRing).
|
|
||||||
* Dans une strate ouverte (SurfaceWorld) le champ lointain est exactement DEUX heightfields —
|
|
||||||
* TerrainZ (sol) et CeilSurf (plafond sky-cap), déjà calculés par colonne par l'oracle
|
|
||||||
* GetSurfaceHeightAt. On construit donc deux grilles déplacées régulières au lieu d'un marching
|
|
||||||
* cubes 3D : sol = polygroup 0, cap = polygroup 1 (classes vraies PAR CONSTRUCTION — pas de vote,
|
|
||||||
* pas de sonde de classification), mêmes conventions que GenerateMesh (positions monde cm, UVs
|
|
||||||
* planaires, masques couleur F6 biome/pente/fondu, normales du gradient de hauteur, jupes
|
|
||||||
* périmètre par seau, run d'indices sol‖cap + NumCeilingTriangles).
|
|
||||||
*
|
|
||||||
* @param OriginVoxels - Coin min de la tuile (voxels). Seul XY est utilisé (les hauteurs sont absolues).
|
|
||||||
* @param StepXY - Pas d'échantillonnage XY en voxels (aligné sur l'anneau MC pour la continuité).
|
|
||||||
* @param CellsXY - Cellules par axe XY (extent = CellsXY × StepXY).
|
|
||||||
* @param StrateChunkZ - Chunk Z DANS la strate de référence (le cœur de la bande) — identifie la
|
|
||||||
* strate dont on maille sol+cap. Hors SurfaceWorld ⇒ mesh vide.
|
|
||||||
* @param HoleMin/MaxX/YVox - TROU XY (voxels, Max EXCLUSIF ; sentinelles MAX/MIN = pas de trou) :
|
|
||||||
* les cellules ENTIÈREMENT dans ce rectangle (la zone couverte par les
|
|
||||||
* coquilles MC autour du joueur) sont sautées — sinon la feuille recouvre
|
|
||||||
* le terrain proche avec son échantillonnage grossier. Les cellules à
|
|
||||||
* cheval restent (anneau de recouvrement au raccord) ; pas de jupe sur
|
|
||||||
* les bords du trou (le terrain MC remplit derrière).
|
|
||||||
* Non couvert (accepté, cf. fable-idea F18) : passages/spine/chasms creusés (le heightfield pur ne
|
|
||||||
* les contient pas), diff layer — invisibles à distance de feuille, l'anneau MC proche les garde.
|
|
||||||
*/
|
|
||||||
FVoxelMeshData GenerateSheetMesh(FIntVector OriginVoxels, int32 StepXY, int32 CellsXY,
|
|
||||||
int32 StrateChunkZ,
|
|
||||||
int32 HoleMinXVox = INT32_MAX, int32 HoleMinYVox = INT32_MAX,
|
|
||||||
int32 HoleMaxXVox = INT32_MIN, int32 HoleMaxYVox = INT32_MIN);
|
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// SERVICES (injectés par AVoxelWorld)
|
// SERVICES (injectés par AVoxelWorld)
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
|
|||||||
@@ -45,10 +45,6 @@ namespace VoxelPassageGeometry
|
|||||||
// ownership band: the floor writer owns the plane, while the air writer owns the capsule
|
// ownership band: the floor writer owns the plane, while the air writer owns the capsule
|
||||||
// volume above it even when two overlapping passage projections differ by a fraction.
|
// volume above it even when two overlapping passage projections differ by a fraction.
|
||||||
constexpr float WalkableTunnelFloorAirClearanceVoxels = 0.5f;
|
constexpr float WalkableTunnelFloorAirClearanceVoxels = 0.5f;
|
||||||
// The graph-tunnel post owns the interior above its floor after terrain and passage-floor
|
|
||||||
// writers have run. The support-floor predicate is also an exclusion mask for the air post,
|
|
||||||
// so overlapping graph tubes cannot erase one another's walkable support band.
|
|
||||||
constexpr float CaveTunnelAirCoreInsetVoxels = 0.5f;
|
|
||||||
|
|
||||||
// A 15 degree ramp is comfortably below the 44 degree engine walkability ceiling. The
|
// A 15 degree ramp is comfortably below the 44 degree engine walkability ceiling. The
|
||||||
// latter is a scramble limit, not a sensible default for a tunnel the player walks through.
|
// latter is a scramble limit, not a sensible default for a tunnel the player walks through.
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ public:
|
|||||||
int32 GetSeasonSeed() const { return SeasonSeed; }
|
int32 GetSeasonSeed() const { return SeasonSeed; }
|
||||||
float GetOriginSpineRadius() const { return OriginSpineRadius; }
|
float GetOriginSpineRadius() const { return OriginSpineRadius; }
|
||||||
float GetWorldRadiusVoxels() const { return WorldRadiusVoxels; }
|
float GetWorldRadiusVoxels() const { return WorldRadiusVoxels; }
|
||||||
const FString& GetManifestContentHash() const { return ManifestContentHash; }
|
|
||||||
|
|
||||||
/** Create the asset in Content Browser, choose a JSON file, then press this button. */
|
/** Create the asset in Content Browser, choose a JSON file, then press this button. */
|
||||||
UFUNCTION(CallInEditor, Category = "Season")
|
UFUNCTION(CallInEditor, Category = "Season")
|
||||||
|
|||||||
@@ -134,10 +134,6 @@ struct VOXELFORGE_API FVoxelSeasonCompositionSettings
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Friendly aliases for callers that use the task's shorter naming.
|
|
||||||
using FVoxelSeasonComposerSettings = FVoxelSeasonCompositionSettings;
|
|
||||||
using FVoxelSeasonComposeSettings = FVoxelSeasonCompositionSettings;
|
|
||||||
|
|
||||||
/** Compact audit row; the full vectors live only for selected strates. */
|
/** Compact audit row; the full vectors live only for selected strates. */
|
||||||
struct VOXELFORGE_API FVoxelSeasonCandidateAudit
|
struct VOXELFORGE_API FVoxelSeasonCandidateAudit
|
||||||
{
|
{
|
||||||
@@ -260,10 +256,6 @@ struct VOXELFORGE_API FVoxelSeasonManifest
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Human-readable enum names used by JSON and the review page. */
|
|
||||||
VOXELFORGE_API const TCHAR* VF_GetVoxelSeasonSelectionReasonName(
|
|
||||||
EVoxelSeasonSelectionReason Reason);
|
|
||||||
|
|
||||||
/** Compose, validate, select, serialize, and (by default) write one season. Editor/build-box only. */
|
/** Compose, validate, select, serialize, and (by default) write one season. Editor/build-box only. */
|
||||||
#if WITH_EDITOR
|
#if WITH_EDITOR
|
||||||
VOXELFORGE_API FVoxelSeasonManifest VF_ComposeSeason(
|
VOXELFORGE_API FVoxelSeasonManifest VF_ComposeSeason(
|
||||||
|
|||||||
@@ -123,21 +123,6 @@ public:
|
|||||||
return FMath::Clamp(FMath::Max(StrateContentCutMinLevel, 1), 1, 9);
|
return FMath::Clamp(FMath::Max(StrateContentCutMinLevel, 1), 1, 9);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open-world SKY reach: the sky-cap ceiling of an open strate (SurfaceWorld / FloatingIslands)
|
|
||||||
// is FAR, so the ceiling BAND is streamed across a wider horizontal radius = ViewDistanceXY ×
|
|
||||||
// this, so the sky reaches toward the horizon instead of being a patch over the player's head.
|
|
||||||
// Only the ceiling band (top CeilingBandChunks of the strate) gets the wide radius — the empty
|
|
||||||
// air below it stays at base XY. Cost grows ~ multiplier² (more far chunks/draws; gen is cheap
|
|
||||||
// and batching keeps the game-thread cost low). 1 = off. Live-tunable on the asset — push to 4
|
|
||||||
// if the draw budget allows; a proper to-the-horizon view (terrain too) is the chunk-LOD clipmap.
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "1"))
|
|
||||||
int32 CeilingViewMultiplier = 2;
|
|
||||||
|
|
||||||
// How many chunks down from the strate top get the wide CeilingViewMultiplier radius (covers
|
|
||||||
// the sky-cap slab). Bigger = no gaps if the ceiling surface dips, but more far chunks.
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "1"))
|
|
||||||
int32 CeilingBandChunks = 4;
|
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// CLIPMAP (chunked-LOD streaming — supersedes the ViewDistance box above)
|
// CLIPMAP (chunked-LOD streaming — supersedes the ViewDistance box above)
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
@@ -183,25 +168,6 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "0"))
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "0"))
|
||||||
int32 RenderDistanceChunks = 768;
|
int32 RenderDistanceChunks = 768;
|
||||||
|
|
||||||
// F18 — the render-distance ring streams per-surface SHEETS instead of MC tiles: in an open
|
|
||||||
// strate the far field is exactly two heightfields (TerrainZ + sky-cap CeilSurf, both already
|
|
||||||
// computed per column), so each far tile becomes two displaced grids (ground polygroup 0 /
|
|
||||||
// cap polygroup 1 — same materials), ~3-6× cheaper to generate and far fewer components (one
|
|
||||||
// sheet spans 2^FarSheetSpanLevels MC-tile footprints per axis). Non-open strates produce
|
|
||||||
// empty sheets (their far ring was enclosed rock anyway). Carved features (passages, chasms,
|
|
||||||
// spine) don't show at sheet distance. Needs the strate band armed (StrateContentCutMinLevel
|
|
||||||
// active); in the inter-strate gap the sheet ring blanks until you land. The ship default is OFF:
|
|
||||||
// the ring stays real MC tiles at level MaxClipLevel (pre-F18 behaviour). Only matters when
|
|
||||||
// RenderDistanceChunks > 0.
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap")
|
|
||||||
bool bFarSheetRing = false;
|
|
||||||
|
|
||||||
// Sheet tile size = MaxClipLevel + this many levels (2 → one sheet covers 4×4 MC-tile
|
|
||||||
// footprints → ~16× fewer far components). Sampling density stays that of the MaxClipLevel
|
|
||||||
// MC ring (cell count grows instead), capped at 128 cells/axis (beyond, cells coarsen).
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "1", ClampMax = "4", EditCondition = "bFarSheetRing"))
|
|
||||||
int32 FarSheetSpanLevels = 2;
|
|
||||||
|
|
||||||
// SKIRTS — seal the thin cracks where neighbouring clipmap shells (different resolutions) meet.
|
// SKIRTS — seal the thin cracks where neighbouring clipmap shells (different resolutions) meet.
|
||||||
// A short wall is extruded into the solid from each surface edge on the tile's outer faces.
|
// A short wall is extruded into the solid from each surface edge on the tile's outer faces.
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap")
|
||||||
@@ -345,12 +311,6 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting", meta = (ClampMin = "1", ClampMax = "5"))
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting", meta = (ClampMin = "1", ClampMax = "5"))
|
||||||
int32 DensityVolumeLevels = 3;
|
int32 DensityVolumeLevels = 3;
|
||||||
|
|
||||||
// DEPRECATED / unused: the volume fill no longer runs on the shared UE::Tasks pool (where it
|
|
||||||
// starved behind mesh-gen). It now runs on ONE dedicated thread off the pool, so there's no task
|
|
||||||
// budget to cap. Kept only so existing saved assets don't error; safe to ignore.
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting", meta = (ClampMin = "1", ClampMax = "16"))
|
|
||||||
int32 DensityVolumeMaxTasks = 4;
|
|
||||||
|
|
||||||
// A fill box is split into Z-slabs of at most this many cells per task, so no single task is
|
// A fill box is split into Z-slabs of at most this many cells per task, so no single task is
|
||||||
// huge (a full level refill on startup/teleport fans out across workers). Lower = more, smaller
|
// huge (a full level refill on startup/teleport fans out across workers). Lower = more, smaller
|
||||||
// tasks (better parallelism / latency); higher = fewer, fatter tasks.
|
// tasks (better parallelism / latency); higher = fewer, fatter tasks.
|
||||||
|
|||||||
@@ -48,15 +48,6 @@ public:
|
|||||||
uint64 RetainedSamples = 0;
|
uint64 RetainedSamples = 0;
|
||||||
uint64 DroppedSamples = 0;
|
uint64 DroppedSamples = 0;
|
||||||
uint64 TotalFrames = 0;
|
uint64 TotalFrames = 0;
|
||||||
// Kept for source compatibility with the earlier in-process summary. Raw sessions leave
|
|
||||||
// these zero; the offline summary owns all resolved-frame counts and tables.
|
|
||||||
uint64 ResolvedFrames = 0;
|
|
||||||
uint64 UnknownFrames = 0;
|
|
||||||
uint64 WaitLikeFrames = 0;
|
|
||||||
uint64 SamplesWithResolvedLeaf = 0;
|
|
||||||
uint64 SamplesWithWaitLikeFrame = 0;
|
|
||||||
uint64 UniqueProgramCounters = 0;
|
|
||||||
uint64 InlineSymbols = 0;
|
|
||||||
uint64 MaxActiveRegisteredThreads = 0;
|
uint64 MaxActiveRegisteredThreads = 0;
|
||||||
uint64 ModuleCount = 0;
|
uint64 ModuleCount = 0;
|
||||||
double RunSeconds = 0.0;
|
double RunSeconds = 0.0;
|
||||||
@@ -78,8 +69,6 @@ public:
|
|||||||
/** Used by the module shutdown path as a final lifecycle backstop. */
|
/** Used by the module shutdown path as a final lifecycle backstop. */
|
||||||
void Shutdown();
|
void Shutdown();
|
||||||
|
|
||||||
bool IsRunning() const;
|
|
||||||
|
|
||||||
/** Called only by FScopedVoxelStackRegistration; cheap when disabled. */
|
/** Called only by FScopedVoxelStackRegistration; cheap when disabled. */
|
||||||
static bool RegisterCurrentThread(int32 LODLevel = INDEX_NONE);
|
static bool RegisterCurrentThread(int32 LODLevel = INDEX_NONE);
|
||||||
static void DeregisterCurrentThread();
|
static void DeregisterCurrentThread();
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ namespace VoxelForgeStartupTrace
|
|||||||
int32 TileY = 0;
|
int32 TileY = 0;
|
||||||
int32 TileZ = 0;
|
int32 TileZ = 0;
|
||||||
int32 Level = -1;
|
int32 Level = -1;
|
||||||
int32 Verdict = -1; // Mixed=0, AllSolid=1, AllAir=2, -1=not classified (sheet/early path)
|
int32 Verdict = -1; // Mixed=0, AllSolid=1, AllAir=2, -1=not classified (early path)
|
||||||
bool bSealedSolidProof = false;
|
bool bSealedSolidProof = false;
|
||||||
bool bEmpty = true;
|
bool bEmpty = true;
|
||||||
int32 Vertices = 0;
|
int32 Vertices = 0;
|
||||||
|
|||||||
@@ -14,10 +14,6 @@ DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Skipped All Air"), STAT_VoxelForge
|
|||||||
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Meshed"), STAT_VoxelForgeTilesMeshed, STATGROUP_VoxelForge, VOXELFORGE_API);
|
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Meshed"), STAT_VoxelForgeTilesMeshed, STATGROUP_VoxelForge, VOXELFORGE_API);
|
||||||
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Operator Stack Solid"), STAT_VoxelForgeTilesOpStackSolid, STATGROUP_VoxelForge, VOXELFORGE_API);
|
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Operator Stack Solid"), STAT_VoxelForgeTilesOpStackSolid, STATGROUP_VoxelForge, VOXELFORGE_API);
|
||||||
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Operator Stack Air"), STAT_VoxelForgeTilesOpStackAir, STATGROUP_VoxelForge, VOXELFORGE_API);
|
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Operator Stack Air"), STAT_VoxelForgeTilesOpStackAir, STATGROUP_VoxelForge, VOXELFORGE_API);
|
||||||
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Not Op Stack Sole Slot"), STAT_VoxelForgeCaveBailNotOpStackSoleSlot, STATGROUP_VoxelForge, VOXELFORGE_API);
|
|
||||||
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Not Op Stack Boundary Tile"), STAT_VoxelForgeCaveBailNotOpStackBoundaryTile, STATGROUP_VoxelForge, VOXELFORGE_API);
|
|
||||||
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Not Op Stack No Layout"), STAT_VoxelForgeCaveBailNotOpStackNoLayout, STATGROUP_VoxelForge, VOXELFORGE_API);
|
|
||||||
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Not Op Stack Recheck"), STAT_VoxelForgeCaveBailNotOpStackRecheck, STATGROUP_VoxelForge, VOXELFORGE_API);
|
|
||||||
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Mix Out Of Layout"), STAT_VoxelForgeCaveMixOutOfLayout, STATGROUP_VoxelForge, VOXELFORGE_API);
|
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Mix Out Of Layout"), STAT_VoxelForgeCaveMixOutOfLayout, STATGROUP_VoxelForge, VOXELFORGE_API);
|
||||||
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Mix Gap"), STAT_VoxelForgeCaveMixGap, STATGROUP_VoxelForge, VOXELFORGE_API);
|
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Mix Gap"), STAT_VoxelForgeCaveMixGap, STATGROUP_VoxelForge, VOXELFORGE_API);
|
||||||
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Mix Surface World"), STAT_VoxelForgeCaveMixSurfaceWorld, STATGROUP_VoxelForge, VOXELFORGE_API);
|
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Mix Surface World"), STAT_VoxelForgeCaveMixSurfaceWorld, STATGROUP_VoxelForge, VOXELFORGE_API);
|
||||||
|
|||||||
@@ -594,11 +594,6 @@ public:
|
|||||||
const FString& PromotedStorePath = FString(),
|
const FString& PromotedStorePath = FString(),
|
||||||
const IVoxelStratePromotionVerifier* PromotionVerifier = nullptr);
|
const IVoxelStratePromotionVerifier* PromotionVerifier = nullptr);
|
||||||
|
|
||||||
/**
|
|
||||||
* Compatibility alias retained for callers compiled against the first Tier 4a pass. The
|
|
||||||
* settings argument is audited only for diagnostics; it is not a corpus input.
|
|
||||||
*/
|
|
||||||
bool LoadFromSettings(const UVoxelSettings* Settings, FString& OutReport);
|
|
||||||
bool LoadFromDefinitions(const TArray<UVoxelStrateDefinition*>& Definitions, FString& OutReport);
|
bool LoadFromDefinitions(const TArray<UVoxelStrateDefinition*>& Definitions, FString& OutReport);
|
||||||
|
|
||||||
/** Add an already-resolved vector, primarily for offline tools and focused tests. */
|
/** Add an already-resolved vector, primarily for offline tools and focused tests. */
|
||||||
@@ -625,7 +620,6 @@ public:
|
|||||||
int32 Num() const { return Entries.Num(); }
|
int32 Num() const { return Entries.Num(); }
|
||||||
const TArray<FVoxelStrateCorpusEntry>& GetEntries() const { return Entries; }
|
const TArray<FVoxelStrateCorpusEntry>& GetEntries() const { return Entries; }
|
||||||
const TArray<FVoxelStrateFieldSpread>& GetFieldSpreads() const { return FieldSpreads; }
|
const TArray<FVoxelStrateFieldSpread>& GetFieldSpreads() const { return FieldSpreads; }
|
||||||
const TArray<FString>& GetSkippedDefinitions() const { return SkippedDefinitions; }
|
|
||||||
|
|
||||||
int32 NumForArchetype(ECaveGeneratorType Archetype) const;
|
int32 NumForArchetype(ECaveGeneratorType Archetype) const;
|
||||||
int32 NumForProvenance(EVoxelStrateCorpusProvenance Provenance) const;
|
int32 NumForProvenance(EVoxelStrateCorpusProvenance Provenance) const;
|
||||||
@@ -804,10 +798,6 @@ VOXELFORGE_API uint32 VF_HashStrateStructureRecipe(const FVoxelOpStackRecipe& Re
|
|||||||
VOXELFORGE_API bool VF_AreStrateStructureRecipesIdentical(
|
VOXELFORGE_API bool VF_AreStrateStructureRecipesIdentical(
|
||||||
const FVoxelOpStackRecipe& A, const FVoxelOpStackRecipe& B);
|
const FVoxelOpStackRecipe& A, const FVoxelOpStackRecipe& B);
|
||||||
|
|
||||||
/** The compact API requested by the composer design. */
|
|
||||||
VOXELFORGE_API FStrateGenerationParams VF_RollStrateParams(
|
|
||||||
const FVoxelStrateCorpus& Corpus, int32 Seed, int32 Index);
|
|
||||||
|
|
||||||
/** Bitwise comparison over exactly the fields in FStrateGenerationParams::Lerp. */
|
/** Bitwise comparison over exactly the fields in FStrateGenerationParams::Lerp. */
|
||||||
VOXELFORGE_API bool VF_AreStrateParamsBitIdentical(
|
VOXELFORGE_API bool VF_AreStrateParamsBitIdentical(
|
||||||
const FStrateGenerationParams& A, const FStrateGenerationParams& B);
|
const FStrateGenerationParams& A, const FStrateGenerationParams& B);
|
||||||
|
|||||||
@@ -115,29 +115,6 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation")
|
||||||
ECaveGeneratorType GeneratorType = ECaveGeneratorType::TunnelNetwork;
|
ECaveGeneratorType GeneratorType = ECaveGeneratorType::TunnelNetwork;
|
||||||
|
|
||||||
/**
|
|
||||||
* OPERATOR STACK (experimental) — generate this strate through the composable density operator
|
|
||||||
* stack instead of the hardcoded archetype switch. Same world, different machinery.
|
|
||||||
*
|
|
||||||
* This is the A/B switch for `OPSTACK-PLAN §2.6`'s acceptance bar: flip it, regenerate, and
|
|
||||||
* judge on a screenshot that it is recognisably the same place. Both systems coexist
|
|
||||||
* indefinitely — the switch is not going away until every archetype is ported.
|
|
||||||
*
|
|
||||||
* ⚠️ ONLY `Maze` IS PORTED SO FAR. On any other GeneratorType this flag is ignored and the
|
|
||||||
* switch runs as before, so setting it is harmless but does nothing yet.
|
|
||||||
*
|
|
||||||
* ⚠️ Do NOT flip this on a strate mid-session and expect the old and new geometry to agree to
|
|
||||||
* the bit — they differ by ~1-2 ULP with ZERO isosurface crossings, so the shape is identical
|
|
||||||
* but the floats are not (`AUDIT-2026-07.md §C10`). Regenerate the world after changing it
|
|
||||||
* rather than letting old and new tiles sit side by side.
|
|
||||||
*
|
|
||||||
* Pile d'opérateurs (expérimental) : génère cette strate via la pile composable au lieu du
|
|
||||||
* `switch` d'archétype. Seul `Maze` est porté ; ailleurs le drapeau est ignoré.
|
|
||||||
*/
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation",
|
|
||||||
meta = (DisplayName = "Use Operator Stack (experimental)"))
|
|
||||||
bool bUseOperatorStack = false;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authoritative per-strate worm switch. Existing assets default to enabled. The manager
|
* Authoritative per-strate worm switch. Existing assets default to enabled. The manager
|
||||||
* resolves a disabled strate to WormStrength == 0 before any fused, stack, composer, or
|
* resolves a disabled strate to WormStrength == 0 before any fused, stack, composer, or
|
||||||
|
|||||||
@@ -249,15 +249,6 @@ public:
|
|||||||
* live edit) can never serve a stale strate index or passage shortlist. */
|
* live edit) can never serve a stale strate index or passage shortlist. */
|
||||||
uint32 GetLayoutVersion() const { return PassagesVersion; }
|
uint32 GetLayoutVersion() const { return PassagesVersion; }
|
||||||
|
|
||||||
/**
|
|
||||||
* Fingerprint of the immutable density inputs used by the generator.
|
|
||||||
*
|
|
||||||
* Returns zero when the manager has dynamic inputs that this compact cache key intentionally
|
|
||||||
* does not serialize (seasons, composer recipes, biome maps, or terrain-op assets). Zero is a
|
|
||||||
* fail-closed answer: callers must skip verdict reuse rather than risk an old all-air proof.
|
|
||||||
*/
|
|
||||||
uint64 GetGenerationParamsFingerprint() const;
|
|
||||||
|
|
||||||
/** Diagnostic-only switch for isolating the inter-strate support writer. */
|
/** Diagnostic-only switch for isolating the inter-strate support writer. */
|
||||||
void SetPassageSupportFloorWritesEnabledForDiagnostics(bool bEnabled)
|
void SetPassageSupportFloorWritesEnabledForDiagnostics(bool bEnabled)
|
||||||
{
|
{
|
||||||
@@ -289,7 +280,7 @@ public:
|
|||||||
* Get generation params for a chunk, with boundary blending.
|
* Get generation params for a chunk, with boundary blending.
|
||||||
*
|
*
|
||||||
* BLENDING CONCEPT:
|
* BLENDING CONCEPT:
|
||||||
* When a chunk is near a strate boundary (within BlendChunks of the edge),
|
* When a chunk is near a strate boundary (within the definition's TransitionBlendChunks of the edge),
|
||||||
* the params are lerped between the two adjacent strates. This prevents
|
* the params are lerped between the two adjacent strates. This prevents
|
||||||
* hard visual seams where one strate ends and another begins.
|
* hard visual seams where one strate ends and another begins.
|
||||||
*
|
*
|
||||||
@@ -303,7 +294,7 @@ public:
|
|||||||
*
|
*
|
||||||
* Returns TunnelNetwork if the chunk is outside all strates or if the
|
* Returns TunnelNetwork if the chunk is outside all strates or if the
|
||||||
* strate definition is null. The generator uses this to pick which
|
* strate definition is null. The generator uses this to pick which
|
||||||
* density function to call (GetDensityWithParams vs GetSlabDensity).
|
* operator stack to build for the chunk.
|
||||||
*
|
*
|
||||||
* @param ChunkCoord - The chunk position
|
* @param ChunkCoord - The chunk position
|
||||||
* @return The ECaveGeneratorType for the strate containing this chunk
|
* @return The ECaveGeneratorType for the strate containing this chunk
|
||||||
@@ -311,12 +302,9 @@ public:
|
|||||||
ECaveGeneratorType GetGeneratorTypeForChunk(const FIntVector& ChunkCoord) const;
|
ECaveGeneratorType GetGeneratorTypeForChunk(const FIntVector& ChunkCoord) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True when this chunk's strate opts into the density OPERATOR STACK instead of the hardcoded
|
* True when this chunk lies in a strate slot whose archetype has an operator stack (all eight
|
||||||
* archetype switch (`UVoxelStrateDefinition::bUseOperatorStack`).
|
* do). False outside the layout or for a slot without a definition. This predicate is the ONLY
|
||||||
*
|
* place the archetype list is written down.
|
||||||
* Returns false for archetypes that have no port yet, so the flag can be set on any strate
|
|
||||||
* without changing its output until that archetype lands. Only `Maze` is ported today — this
|
|
||||||
* predicate is where that list grows, and it is deliberately the ONLY place it is written down.
|
|
||||||
*/
|
*/
|
||||||
bool UsesOperatorStackForChunk(const FIntVector& ChunkCoord) const;
|
bool UsesOperatorStackForChunk(const FIntVector& ChunkCoord) const;
|
||||||
|
|
||||||
@@ -419,14 +407,6 @@ public:
|
|||||||
void ApplyPassageTunnelAir(float& Density, float WorldX, float WorldY, float WorldZ,
|
void ApplyPassageTunnelAir(float& Density, float WorldX, float WorldY, float WorldZ,
|
||||||
float BaseDensity, float SealThickness) const;
|
float BaseDensity, float SealThickness) const;
|
||||||
|
|
||||||
/** Re-assert landing air after MC-space disturbances, still before the final XY seal. */
|
|
||||||
void ApplyPassageLandingAirMC(float& Density, float WorldX, float WorldY, float WorldZ,
|
|
||||||
float BaseDensity, float SealThickness) const;
|
|
||||||
|
|
||||||
/** Re-assert walkable tunnel air after MC-space disturbances, still before the final XY seal. */
|
|
||||||
void ApplyPassageTunnelAirMC(float& Density, float WorldX, float WorldY, float WorldZ,
|
|
||||||
float BaseDensity, float SealThickness) const;
|
|
||||||
|
|
||||||
/** Apply the landing/tunnel MC post in one cached passage scan after disturbances. */
|
/** Apply the landing/tunnel MC post in one cached passage scan after disturbances. */
|
||||||
void ApplyPassageStructuralPostsMC(float& Density, float WorldX, float WorldY, float WorldZ,
|
void ApplyPassageStructuralPostsMC(float& Density, float WorldX, float WorldY, float WorldZ,
|
||||||
float BaseDensity, float SealThickness) const;
|
float BaseDensity, float SealThickness) const;
|
||||||
@@ -530,9 +510,6 @@ protected:
|
|||||||
// allocator address reuse must not make a stale worker-local stack look current.
|
// allocator address reuse must not make a stale worker-local stack look current.
|
||||||
uint64 CacheLifetimeId = 0;
|
uint64 CacheLifetimeId = 0;
|
||||||
|
|
||||||
// How many chunks at strate boundaries are blended (transition zone)
|
|
||||||
int32 BlendChunks = 2;
|
|
||||||
|
|
||||||
// World seed (stored for passage generation)
|
// World seed (stored for passage generation)
|
||||||
int32 CachedSeed = 0;
|
int32 CachedSeed = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -510,23 +510,6 @@ VOXELFORGE_API FVoxelConnectivityDiagnostics VF_DiagnoseConnectivityWithSampler(
|
|||||||
const FVector& BVoxel,
|
const FVector& BVoxel,
|
||||||
const FVoxelStrateMeasureSettings& Settings);
|
const FVoxelStrateMeasureSettings& Settings);
|
||||||
|
|
||||||
/**
|
|
||||||
* Player-fit connectivity over a custom stack. This is deliberately a separate API from the
|
|
||||||
* legacy air route: it refuses unless Settings.SampleStep == 1, resolves endpoints against the
|
|
||||||
* direct capsule-occupancy/floor mask, and flood-fills/routes only through player-fit cells.
|
|
||||||
*/
|
|
||||||
VOXELFORGE_API EVoxelConnectivityResult VF_ArePlayerFitConnectedWithSampler(
|
|
||||||
const IVoxelStrateDensitySampler& Sampler,
|
|
||||||
int32 StrateBottomWorldZ,
|
|
||||||
int32 StrateTopWorldZ,
|
|
||||||
float BoundarySealThickness,
|
|
||||||
const FVector& AVoxel,
|
|
||||||
const FVector& BVoxel,
|
|
||||||
const FVoxelStrateMeasureSettings& Settings,
|
|
||||||
bool& bOutStartSnapped,
|
|
||||||
bool& bOutGoalSnapped,
|
|
||||||
FVoxelConnectivityDiagnostics* OutDiagnostics = nullptr);
|
|
||||||
|
|
||||||
/** Full diagnostics variant for the player-fit custom-stack route. */
|
/** Full diagnostics variant for the player-fit custom-stack route. */
|
||||||
VOXELFORGE_API FVoxelConnectivityDiagnostics VF_DiagnosePlayerFitConnectivityWithSampler(
|
VOXELFORGE_API FVoxelConnectivityDiagnostics VF_DiagnosePlayerFitConnectivityWithSampler(
|
||||||
const IVoxelStrateDensitySampler& Sampler,
|
const IVoxelStrateDensitySampler& Sampler,
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ enum class ECaveGeneratorType : uint8
|
|||||||
* is defined on the UPPER strate's definition and controls the boundary between
|
* is defined on the UPPER strate's definition and controls the boundary between
|
||||||
* itself and the strate below it.
|
* itself and the strate below it.
|
||||||
*
|
*
|
||||||
* Gradient: Smooth linear interpolation of generation params across BlendChunks.
|
* Gradient: Smooth linear interpolation of generation params across TransitionBlendChunks.
|
||||||
* This is the classic approach — no seam visible, but also no dramatic
|
* This is the classic approach — no seam visible, but also no dramatic
|
||||||
* geological feature at the boundary. Best for similar-looking strates.
|
* geological feature at the boundary. Best for similar-looking strates.
|
||||||
*
|
*
|
||||||
@@ -228,7 +228,7 @@ UENUM(BlueprintType)
|
|||||||
enum class EVoxelStrateTransition : uint8
|
enum class EVoxelStrateTransition : uint8
|
||||||
{
|
{
|
||||||
// Smooth linear interpolation of all generation params across the blend zone.
|
// Smooth linear interpolation of all generation params across the blend zone.
|
||||||
// No visible boundary — params change gradually over BlendChunks distance.
|
// No visible boundary — params change gradually over TransitionBlendChunks distance.
|
||||||
Gradient UMETA(DisplayName = "Gradient (smooth blend)"),
|
Gradient UMETA(DisplayName = "Gradient (smooth blend)"),
|
||||||
|
|
||||||
// No blending — params switch instantly at the boundary.
|
// No blending — params switch instantly at the boundary.
|
||||||
@@ -2256,7 +2256,7 @@ enum class ELandmarkAnchor : uint8
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* FStrateLandmark — a RARE, deliberately-placed object: mini-suns, ruins, shrines, monuments (§8.5, F7).
|
* FStrateLandmark — a RARE, deliberately-placed object: mini-suns, ruins, shrines, monuments (§8.5, F7).
|
||||||
* The one placement primitive for "notable things you navigate by" (set-pieces folded in here 2026-07-06).
|
* The one placement primitive for "notable things you navigate by" (set-pieces included).
|
||||||
*
|
*
|
||||||
* This is the right primitive for things like the underground "mini-suns" (in-lore light sources): one
|
* This is the right primitive for things like the underground "mini-suns" (in-lore light sources): one
|
||||||
* object per ~`SpacingChunks` lattice cell, so the work scales with how MANY landmarks are in range
|
* object per ~`SpacingChunks` lattice cell, so the work scales with how MANY landmarks are in range
|
||||||
|
|||||||
@@ -411,14 +411,4 @@ namespace VoxelGenLOD
|
|||||||
Density = Before;
|
Density = Before;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Compare a scalar proof input such as EvaluateModifierSDF without changing the field. */
|
|
||||||
FORCEINLINE void CompareSkippedPostValue(
|
|
||||||
float Candidate, float Canonical,
|
|
||||||
ETilePostComparisonKind Kind = ETilePostComparisonKind::Other)
|
|
||||||
{
|
|
||||||
if (bTilePostReachDebug && !bTilePostReachBypass)
|
|
||||||
{
|
|
||||||
RecordSkippedPostComparison(Kind, !SameFloatBits(Candidate, Canonical));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -203,15 +203,12 @@ namespace VoxelMath
|
|||||||
// Pourquoi 32 ? Puissance de 2 → astuces bit à bit + bon alignement GPU.
|
// Pourquoi 32 ? Puissance de 2 → astuces bit à bit + bon alignement GPU.
|
||||||
// VOXEL_SIZE = 25 cm/voxel (Unreal travaille en centimètres). 1 chunk = 8 m.
|
// VOXEL_SIZE = 25 cm/voxel (Unreal travaille en centimètres). 1 chunk = 8 m.
|
||||||
//
|
//
|
||||||
// NOTE: 64³ a été essayé ("B", 2026-06-16) pour couper les draw calls (8× moins de
|
// NOTE: 64³ coupe les draw calls (8× moins de chunks) mais rend le streaming trop
|
||||||
// chunks) mais le streaming devenait trop saccadé (briques 8× plus lourdes → applies
|
// saccadé (briques 8× plus lourdes → applies + spawns de contenu en gros à-coups sur
|
||||||
// + spawns de contenu en gros à-coups sur le game thread). Reverté à 32³ : le fps se
|
// le game thread). Le fps se règle côté RENDU (ombres off sur LOD lointain, voir
|
||||||
// règle côté RENDU (ombres off sur LOD lointain, voir ApplyMeshToChunk), pas via la
|
// ApplyMeshToChunk), pas via la taille de chunk. La taille de chunk reste le levier streaming-vs-draws si besoin.
|
||||||
// taille de chunk. La taille de chunk reste le levier streaming-vs-draws si besoin.
|
|
||||||
|
|
||||||
constexpr int32 CHUNK_SIZE = 32;
|
constexpr int32 CHUNK_SIZE = 32;
|
||||||
constexpr int32 CHUNK_SIZE_SQUARED = CHUNK_SIZE * CHUNK_SIZE; // 1024
|
|
||||||
constexpr int32 CHUNK_VOLUME = CHUNK_SIZE * CHUNK_SIZE * CHUNK_SIZE; // 32768
|
|
||||||
|
|
||||||
constexpr float VOXEL_SIZE = 25.0f;
|
constexpr float VOXEL_SIZE = 25.0f;
|
||||||
|
|
||||||
@@ -276,26 +273,6 @@ enum class EVoxelFace : uint8
|
|||||||
NegativeZ, // Down (-Z)
|
NegativeZ, // Down (-Z)
|
||||||
};
|
};
|
||||||
|
|
||||||
inline FIntVector GetFaceDirection(EVoxelFace Face)
|
|
||||||
{
|
|
||||||
switch (Face)
|
|
||||||
{
|
|
||||||
case EVoxelFace::PositiveX: return FIntVector( 1, 0, 0);
|
|
||||||
case EVoxelFace::NegativeX: return FIntVector(-1, 0, 0);
|
|
||||||
case EVoxelFace::PositiveY: return FIntVector( 0, 1, 0);
|
|
||||||
case EVoxelFace::NegativeY: return FIntVector( 0, -1, 0);
|
|
||||||
case EVoxelFace::PositiveZ: return FIntVector( 0, 0, 1);
|
|
||||||
case EVoxelFace::NegativeZ: return FIntVector( 0, 0, -1);
|
|
||||||
default: return FIntVector( 0, 0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline FVector GetFaceNormal(EVoxelFace Face)
|
|
||||||
{
|
|
||||||
const FIntVector Dir = GetFaceDirection(Face);
|
|
||||||
return FVector(Dir.X, Dir.Y, Dir.Z);
|
|
||||||
}
|
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// COORDINATE CONVERSION
|
// COORDINATE CONVERSION
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
@@ -319,53 +296,6 @@ inline FIntVector WorldToChunkCoord(const FVector& WorldPos)
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline FIntVector WorldToLocalCoord(const FVector& WorldPos)
|
|
||||||
{
|
|
||||||
// ((x % n) + n) % n → modulo positif même pour x négatif.
|
|
||||||
// (-5 % 32) = -5 en C++, mais on veut 27.
|
|
||||||
return FIntVector(
|
|
||||||
((FMath::FloorToInt(WorldPos.X / VOXEL_SIZE) % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE,
|
|
||||||
((FMath::FloorToInt(WorldPos.Y / VOXEL_SIZE) % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE,
|
|
||||||
((FMath::FloorToInt(WorldPos.Z / VOXEL_SIZE) % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline FVector ChunkToWorldPos(const FIntVector& ChunkCoord)
|
|
||||||
{
|
|
||||||
return FVector(
|
|
||||||
ChunkCoord.X * CHUNK_SIZE * VOXEL_SIZE,
|
|
||||||
ChunkCoord.Y * CHUNK_SIZE * VOXEL_SIZE,
|
|
||||||
ChunkCoord.Z * CHUNK_SIZE * VOXEL_SIZE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Index 3D → 1D pour un tableau plat (voir doc CLAUDE.md: "x + y*SizeX + z*SizeX*SizeY").
|
|
||||||
inline int32 LocalToIndex(int32 X, int32 Y, int32 Z)
|
|
||||||
{
|
|
||||||
return X + (Y * CHUNK_SIZE) + (Z * CHUNK_SIZE_SQUARED);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline FIntVector IndexToLocal(int32 Index)
|
|
||||||
{
|
|
||||||
return FIntVector(
|
|
||||||
Index % CHUNK_SIZE,
|
|
||||||
(Index / CHUNK_SIZE) % CHUNK_SIZE,
|
|
||||||
Index / CHUNK_SIZE_SQUARED
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool IsValidLocalCoord(int32 X, int32 Y, int32 Z)
|
|
||||||
{
|
|
||||||
return X >= 0 && X < CHUNK_SIZE
|
|
||||||
&& Y >= 0 && Y < CHUNK_SIZE
|
|
||||||
&& Z >= 0 && Z < CHUNK_SIZE;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool IsValidLocalCoord(const FIntVector& Coord)
|
|
||||||
{
|
|
||||||
return IsValidLocalCoord(Coord.X, Coord.Y, Coord.Z);
|
|
||||||
}
|
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// MATH HELPERS (partagés entre générateur, morphologie, manager)
|
// MATH HELPERS (partagés entre générateur, morphologie, manager)
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
|
|||||||
@@ -160,7 +160,6 @@ struct FVoxelTileHashDumpRecord
|
|||||||
int32 Cells = 0;
|
int32 Cells = 0;
|
||||||
int32 BandChunkLo = MIN_int32;
|
int32 BandChunkLo = MIN_int32;
|
||||||
int32 BandChunkHi = MAX_int32;
|
int32 BandChunkHi = MAX_int32;
|
||||||
bool bSheetTile = false;
|
|
||||||
bool bEmpty = true;
|
bool bEmpty = true;
|
||||||
int32 NumTriangles = 0;
|
int32 NumTriangles = 0;
|
||||||
FString GeometryHash;
|
FString GeometryHash;
|
||||||
@@ -567,7 +566,7 @@ private:
|
|||||||
void LogReadyTransitionVisibilityMeasure();
|
void LogReadyTransitionVisibilityMeasure();
|
||||||
void RecordTileHash(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels,
|
void RecordTileHash(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels,
|
||||||
int32 Step, int32 Cells, int32 BandChunkLo, int32 BandChunkHi,
|
int32 Step, int32 Cells, int32 BandChunkLo, int32 BandChunkHi,
|
||||||
bool bSheetTile, const FVoxelMeshData& MeshData);
|
const FVoxelMeshData& MeshData);
|
||||||
void WriteTileHashDump();
|
void WriteTileHashDump();
|
||||||
|
|
||||||
// Packed shader params, recomputed each Tick. ALL meaningful data is in .xyz — a material Vector
|
// Packed shader params, recomputed each Tick. ALL meaningful data is in .xyz — a material Vector
|
||||||
@@ -761,7 +760,7 @@ public:
|
|||||||
EVoxelTileWorkPriority WorkPriority = EVoxelTileWorkPriority::Horizon);
|
EVoxelTileWorkPriority WorkPriority = EVoxelTileWorkPriority::Horizon);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Worker-side gen for one tile: classify → GenerateMesh/GenerateSheetMesh → BuildTileStreamSet.
|
* Worker-side gen for one tile: classify → GenerateMesh → BuildTileStreamSet.
|
||||||
* Fills Result (no enqueue, no bookkeeping). Called from the async ChunkGen task AND from the
|
* Fills Result (no enqueue, no bookkeeping). Called from the async ChunkGen task AND from the
|
||||||
* synchronous carve path (SyncRemeshTile) — reads Generator/Mesher only, so it's safe on either
|
* synchronous carve path (SyncRemeshTile) — reads Generator/Mesher only, so it's safe on either
|
||||||
* thread. See LoadTile for how the parameters are derived.
|
* thread. See LoadTile for how the parameters are derived.
|
||||||
@@ -769,8 +768,6 @@ public:
|
|||||||
void GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels,
|
void GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels,
|
||||||
int32 Step, int32 Cells, uint32 Epoch, bool bWantCapture,
|
int32 Step, int32 Cells, uint32 Epoch, bool bWantCapture,
|
||||||
int32 BandVoxLo, int32 BandVoxHi, int32 BandChunkLo, int32 BandChunkHi,
|
int32 BandVoxLo, int32 BandVoxHi, int32 BandChunkLo, int32 BandChunkHi,
|
||||||
bool bSheetTile, int32 SheetChunkZ,
|
|
||||||
int32 HoleMinX, int32 HoleMinY, int32 HoleMaxX, int32 HoleMaxY,
|
|
||||||
FChunkResult& Result,
|
FChunkResult& Result,
|
||||||
const std::atomic<bool>* ObsoleteFlag = nullptr);
|
const std::atomic<bool>* ObsoleteFlag = nullptr);
|
||||||
|
|
||||||
@@ -933,15 +930,6 @@ public:
|
|||||||
// result cannot publish and the post-carve result is admitted immediately.
|
// result cannot publish and the post-carve result is admitted immediately.
|
||||||
TSet<FVoxelTileKey> DirtyRemeshQueue;
|
TSet<FVoxelTileKey> DirtyRemeshQueue;
|
||||||
|
|
||||||
// F18 — TROU XY de l'anneau feuille (voxels ; Max EXCLUSIF ; sentinelles MAX/MIN = pas de
|
|
||||||
// trou) : la zone couverte par les coquilles MC (boîte niveau-MaxClipLevel autour du joueur,
|
|
||||||
// rétrécie d'une tuile pour garder un anneau de recouvrement au raccord) est DÉCOUPÉE des
|
|
||||||
// feuilles — sinon une feuille partiellement couverte recouvre le terrain proche avec son
|
|
||||||
// échantillonnage grossier. Mis à jour par crossing ; changement ⇒ re-queue des feuilles
|
|
||||||
// chevauchantes via BandRemeshQueue (re-gen en place).
|
|
||||||
int32 SheetHoleMinXVox = MAX_int32, SheetHoleMinYVox = MAX_int32;
|
|
||||||
int32 SheetHoleMaxXVox = MIN_int32, SheetHoleMaxYVox = MIN_int32;
|
|
||||||
|
|
||||||
// Set to true during EndPlay — async tasks check this before accessing UObjects
|
// Set to true during EndPlay — async tasks check this before accessing UObjects
|
||||||
std::atomic<bool> bShuttingDown{false};
|
std::atomic<bool> bShuttingDown{false};
|
||||||
|
|
||||||
@@ -1018,8 +1006,7 @@ public:
|
|||||||
TArray<FReadyTransitionVisibilitySample> ReadyTransitionVisibilitySamples;
|
TArray<FReadyTransitionVisibilitySample> ReadyTransitionVisibilitySamples;
|
||||||
TArray<FFarFieldSubmitSample> FarFieldSubmitSamples;
|
TArray<FFarFieldSubmitSample> FarFieldSubmitSamples;
|
||||||
|
|
||||||
// Clean performance accounting. LOD 0..8 covers every marching-cubes clip level; sheet tiles
|
// Clean performance accounting. LOD 0..8 covers every marching-cubes clip level.
|
||||||
// above the configured MC range never enter the outer classifier.
|
|
||||||
static constexpr int32 TrackedClassifierLODCount = 9;
|
static constexpr int32 TrackedClassifierLODCount = 9;
|
||||||
std::atomic<uint64> TotalWorkerGenerationCycles{0};
|
std::atomic<uint64> TotalWorkerGenerationCycles{0};
|
||||||
std::atomic<uint64> TotalWorkerGenerationTasks{0};
|
std::atomic<uint64> TotalWorkerGenerationTasks{0};
|
||||||
@@ -1259,8 +1246,8 @@ public:
|
|||||||
// Desired-set membership STAMPÉE : clé → numéro du dernier crossing où la tuile était désirée.
|
// Desired-set membership STAMPÉE : clé → numéro du dernier crossing où la tuile était désirée.
|
||||||
// BuildDesiredTiles upserte le stamp courant puis balaie la map UNE fois : les entrées à stamp
|
// BuildDesiredTiles upserte le stamp courant puis balaie la map UNE fois : les entrées à stamp
|
||||||
// périmé sont les "leavers" (retirées + renvoyées). Le cull ne considère que ces leavers + la
|
// périmé sont les "leavers" (retirées + renvoyées). Le cull ne considère que ces leavers + la
|
||||||
// TransitionHold — fini le scan O(toutes-les-tuiles-chargées) à chaque crossing (le spike
|
// TransitionHold — pas de scan O(toutes-les-tuiles-chargées) à chaque crossing (un tel scan
|
||||||
// CullTiles ~1.6 ms/crossing de la trace 2026-07-05).
|
// coûtait ~1.6 ms/crossing dans CullTiles).
|
||||||
TMap<FVoxelTileKey, uint32> DesiredStamped;
|
TMap<FVoxelTileKey, uint32> DesiredStamped;
|
||||||
uint32 DesiredStamp = 0;
|
uint32 DesiredStamp = 0;
|
||||||
bool IsDesired(const FVoxelTileKey& T) const
|
bool IsDesired(const FVoxelTileKey& T) const
|
||||||
|
|||||||
@@ -17,14 +17,6 @@ namespace VoxelWormField
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** Evaluate the exact two-noise field, retaining the N1 threshold short-circuit. */
|
/** Evaluate the exact two-noise field, retaining the N1 threshold short-circuit. */
|
||||||
VOXELFORGE_API float EvaluateExact(
|
|
||||||
float WorldX,
|
|
||||||
float WorldY,
|
|
||||||
float WorldZ,
|
|
||||||
float Threshold,
|
|
||||||
const FParameters& Parameters);
|
|
||||||
|
|
||||||
/** Evaluate the exact two-noise field. */
|
|
||||||
VOXELFORGE_API float Evaluate(
|
VOXELFORGE_API float Evaluate(
|
||||||
float WorldX,
|
float WorldX,
|
||||||
float WorldY,
|
float WorldY,
|
||||||
|
|||||||
Loaded 100 of 105 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user