Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5ed321b9e |
No files matched your search
-12
@@ -9,20 +9,12 @@
|
|||||||
!/Source/
|
!/Source/
|
||||||
!/Source/**
|
!/Source/**
|
||||||
|
|
||||||
# Plugin runtime shader source.
|
|
||||||
!/Shaders/
|
|
||||||
!/Shaders/**
|
|
||||||
|
|
||||||
# Portable CPU generator and D3D11 viewer.
|
# Portable CPU generator and D3D11 viewer.
|
||||||
!/Standalone/
|
!/Standalone/
|
||||||
!/Standalone/**
|
!/Standalone/**
|
||||||
/Standalone/Build/
|
/Standalone/Build/
|
||||||
/Standalone/bin/
|
/Standalone/bin/
|
||||||
|
|
||||||
# Research and prototype experiments.
|
|
||||||
!/Experiments/
|
|
||||||
!/Experiments/**
|
|
||||||
|
|
||||||
# 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/**
|
||||||
@@ -40,7 +32,3 @@
|
|||||||
/Standalone/data/*.err
|
/Standalone/data/*.err
|
||||||
/Standalone/data/*.vfmesh
|
/Standalone/data/*.vfmesh
|
||||||
/Standalone/Research/**/*.log
|
/Standalone/Research/**/*.log
|
||||||
|
|
||||||
# Web viewer world exports (regenerate with Standalone/Tools/webviewer/export_world.py)
|
|
||||||
/Standalone/Tools/webviewer/meshes/*-arrival.glb
|
|
||||||
/Standalone/Tools/webviewer/meshes/*-passage.glb
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
# AGENTS.md — VoxelForge (read automatically by Codex)
|
|
||||||
|
|
||||||
You are the implementation agent. The orchestrator (Claude) writes each round's brief, reviews your diff against the
|
|
||||||
code, and commits. The owner decides direction. Be warm and direct; credit what worked; say plainly what didn't.
|
|
||||||
|
|
||||||
Read first, as the task needs: `CURRENT-STATE.md` (what ships, reference numbers, **closed experiments: don't reopen
|
|
||||||
them without new evidence**), then `CODEMAP.md` and `ARCHITECTURE.md`. Round reports live in
|
|
||||||
`Standalone/Research/WorldAssemblerM3/Linux*/REPORT.md`.
|
|
||||||
|
|
||||||
## Standing rules (every round, unless the brief says otherwise)
|
|
||||||
|
|
||||||
- Run as user `jahni`. Write only inside this plugin, plus `/home/jahni/VFHost` and `/home/jahni/UnrealEngine` when
|
|
||||||
the brief asks for a UE build. No installs, no system changes.
|
|
||||||
- **No `git commit`, `push` or `stash`.** Leave changes in the working tree; the orchestrator reviews and commits.
|
|
||||||
- Headless processes and builds: `taskset -c 0-11 prlimit --as=6442450944` (12 P-core threads, 6 GiB). Never use all
|
|
||||||
cores.
|
|
||||||
- Unreal (UBT, the editor, commandlets): `taskset -c 0-11` only, **never `prlimit`** (CoreCLR fails). UE builds use
|
|
||||||
`-NoUBA -MaxParallelActions=4`, never while timing.
|
|
||||||
- Stop any single run that passes 30 minutes. Logs go in `Saved/Linux/l<round>/`.
|
|
||||||
- **Timing:** quiet machine only (check `uptime` first; load < ~1.5, nothing else building). L80 protocol: one warm-up
|
|
||||||
per binary, ≥5 interleaved runs, W8 and W12, all four owner layers. Every number states the window that produced it.
|
|
||||||
- **Same world for everyone:** a seed must give the same world on every machine and worker count (Clang and GCC,
|
|
||||||
W1/W8/W12). Byte-identity with older versions is NOT required. If the output changes: bump
|
|
||||||
`WorldAssemblyEvaluatorVersion`, add baselines, and show captures.
|
|
||||||
- Density sign: negative = solid, positive = air. Core voxels are 25 cm. Never mix metres and voxels without an
|
|
||||||
explicit conversion (v225 shipped a reach bug from exactly that).
|
|
||||||
- The fast tier (`Standalone/Tools/verify/verify.py fast`) must stay green. Don't loosen an audit to make a change pass.
|
|
||||||
|
|
||||||
## How to work
|
|
||||||
|
|
||||||
1. **Count before you build.** Measure where the time or the failure actually is (stack sampler, census, counters).
|
|
||||||
State which parts are measured and which are inferred.
|
|
||||||
2. **Suspect the measurement first.** An alarming or contradictory number is the instrument until a control rules it
|
|
||||||
out (A/A runs, an ablation that verifiably removes the work).
|
|
||||||
3. **No fix without a root cause.** Reproduce it, trace it to its source, fix the cause. A guard that hides the symptom
|
|
||||||
is not a fix.
|
|
||||||
4. **One change at a time,** each with its own check.
|
|
||||||
5. **Subtract before you add.** Prefer removing work to reorganising it. The smallest edit that solves the problem.
|
|
||||||
6. **Attack the premise.** When two fixes sharing one premise fail the same gate, write that premise in one sentence
|
|
||||||
and question it before trying a third. **After three failed fixes, stop:** it is probably the design, not the
|
|
||||||
hypothesis. Report it instead of trying fix #4.
|
|
||||||
7. **Tests must be able to fail.** Name what would make each check fail. Add a negative control when you add an audit.
|
|
||||||
8. **Refactors:** pin the behaviour (hashes), change the structure, prove nothing changed.
|
|
||||||
9. **Verify before you claim.** Don't report a check as passed unless it ran on the final source. Say which checks
|
|
||||||
didn't run and why.
|
|
||||||
10. Changes that speed things up go in the shared layer, so every generator benefits.
|
|
||||||
|
|
||||||
## Report
|
|
||||||
|
|
||||||
Write the round's `REPORT.md`, short and plain:
|
|
||||||
- the result first;
|
|
||||||
- what changed and why;
|
|
||||||
- the measurements, with their windows;
|
|
||||||
- the checks run, and those not run;
|
|
||||||
- remaining risks.
|
|
||||||
|
|
||||||
List any changed Unreal `Source/` files, because the owner compiles them on Windows.
|
|
||||||
+12
-489
@@ -73,7 +73,7 @@ is the only density path; a degenerate (zero-height) strate has none and is open
|
|||||||
| Maze | `FMazeGenerationParams` | `BuildMazeStack` | 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` | `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. |
|
| 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` | `BuildVerticalShaftStack` | full-height shafts + horizontal connectors + partial ledges |
|
| VerticalShafts | `FVerticalShaftParams` | `BuildVerticalShaftStack` | full-height shafts + horizontal connectors + partial ledges |
|
||||||
| FloatingIslands | `FFloatingIslandParams` | `BuildFloatingIslandStack` | asymmetric islands: flat land top + underside tapering to a point, lobed (domain-warped) outline, beneath the shared ceiling at its configured strate upper boundary |
|
| 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 `BuildTunnelNetworkStack`) | tunnel rock + high water table |
|
| Underwater | `FStrateGenerationParams` + water | (reuses `BuildTunnelNetworkStack`) | tunnel rock + high water table |
|
||||||
|
|
||||||
All stacks share the convention: internal **positive=solid**, and `AppendStructuralPost` ends every
|
All stacks share the convention: internal **positive=solid**, and `AppendStructuralPost` ends every
|
||||||
@@ -1431,9 +1431,8 @@ density roots, including a 100 cm datum-offset negative control. See
|
|||||||
evaluators keep their recorded cap behavior. The clip joins the exact near density root at the owning
|
evaluators keep their recorded cap behavior. The clip joins the exact near density root at the owning
|
||||||
boundary without changing LOD0–3 density hashes. Future families with planar roofs must declare their
|
boundary without changing LOD0–3 density hashes. Future families with planar roofs must declare their
|
||||||
cap height and bounds through the family plan/provider contract; the shared tile generator consumes
|
cap height and bounds through the family plan/provider contract; the shared tile generator consumes
|
||||||
that declaration. FloatingIslands uses the same resolved strate-boundary data flow: its upper seal
|
that declaration. FloatingIslands declares no planar roof, so open sky above a surface island is the
|
||||||
follows its plan's chunk range and the shared boundary-seal primitive, while SurfaceWorld keeps its
|
expected view.
|
||||||
existing heightfield cap.
|
|
||||||
|
|
||||||
For each requested far SurfaceWorld XY column at LOD4–8, desired-tile selection adds cap-height
|
For each requested far SurfaceWorld XY column at LOD4–8, desired-tile selection adds cap-height
|
||||||
bands only when they do not overlap an existing or already accepted 3D clipmap tile. The original
|
bands only when they do not overlap an existing or already accepted 3D clipmap tile. The original
|
||||||
@@ -1472,9 +1471,8 @@ neighborhood have installed collision (or are explicitly collision-not-required)
|
|||||||
now** — the active view has ground, horizon and the family-appropriate ceiling, with no exposed
|
now** — the active view has ground, horizon and the family-appropriate ceiling, with no exposed
|
||||||
coverage hole or unexpected sky; (3) **refined soon** — each target LOD resolves within its stated
|
coverage hole or unexpected sky; (3) **refined soon** — each target LOD resolves within its stated
|
||||||
time or travel distance after entering range. While moving, report the largest frame/time gap in
|
time or travel distance after entering range. While moving, report the largest frame/time gap in
|
||||||
enclosure and maximum distance that unresolved work trails the pawn. FloatingIslands is enclosed by a
|
enclosure and maximum distance that unresolved work trails the pawn. FloatingIslands has open sky by
|
||||||
planar roof at the upper boundary of its configured strate range; a taller strate places that roof
|
design, so its enclosure check excludes a planar roof.
|
||||||
farther above its islands.
|
|
||||||
|
|
||||||
The shared `AVoxelWorld::UpdateChunksAroundPosition` scheduler serves both the Core snapshot path and
|
The shared `AVoxelWorld::UpdateChunksAroundPosition` scheduler serves both the Core snapshot path and
|
||||||
Core-off legacy path. It admits the collision-critical 3×3 ring first. The measured S5 order remains
|
Core-off legacy path. It admits the collision-critical 3×3 ring first. The measured S5 order remains
|
||||||
@@ -1522,12 +1520,11 @@ collision ring became walkable at 0.632 s. `voxel.StreamingCoarseFirst=1` with
|
|||||||
residency by about one third on the staged plan, but null-RHI residency does not prove rendered
|
residency by about one third on the staged plan, but null-RHI residency does not prove rendered
|
||||||
enclosure. Keep coarse-first opt-in until that view is checked in a rendered owner-plan run.
|
enclosure. Keep coarse-first opt-in until that view is checked in a rendered owner-plan run.
|
||||||
|
|
||||||
The separate `voxel.StreamingStartupApplyBoost` switch is default-on in the current code. At `1`, it
|
The separate `voxel.StreamingStartupApplyBoost` switch stays default-off. At `1`, it temporarily allows
|
||||||
temporarily allows up to 24 visible mesh submissions / 14 ms while the initial desired set and
|
12 visible mesh submissions / 14 ms while the initial desired set and walkable ring are incomplete,
|
||||||
walkable ring are incomplete, with a 30 second ceiling; at `0`, the authored budget is unchanged.
|
with a 30 second ceiling; at `0`, the authored budget is unchanged. On the matched legacy-order test,
|
||||||
The historical matched S6 run changed full-set readiness from 17.412 s to 17.346 s and removed
|
the boost changed full-set readiness from 17.412 s to 17.346 s and removed count-cap frames, but did
|
||||||
count-cap frames, but did not improve end-to-end residency. These current defaults are defined in
|
not improve end-to-end residency. Keep the normal 4 / 2 ms guard for play.
|
||||||
`Source/VoxelForge/Private/VoxelWorld.cpp`; the S6 measurement remains historical context.
|
|
||||||
|
|
||||||
The S6 trace found the shared pool was not saturated: its 10-slot idle estimate was about 132
|
The S6 trace found the shared pool was not saturated: its 10-slot idle estimate was about 132
|
||||||
slot-seconds on legacy order and 73 on gated coarse-first, while game-thread tile applies totaled only
|
slot-seconds on legacy order and 73 on gated coarse-first, while game-thread tile applies totaled only
|
||||||
@@ -1557,8 +1554,8 @@ by its RAII guard when the worker lambda exits. This still counts queued and run
|
|||||||
results do not consume an admission slot. `voxel.StreamingLegacyPendingResultCap=1` restores the old
|
results do not consume an admission slot. `voxel.StreamingLegacyPendingResultCap=1` restores the old
|
||||||
`PendingTiles` accounting; the test harness's `NearReserveMode=Legacy` also sets
|
`PendingTiles` accounting; the test harness's `NearReserveMode=Legacy` also sets
|
||||||
`voxel.StreamingDynamicNearReserve=0`, reproducing the S6 path. `StreamingDynamicNearReserve` now
|
`voxel.StreamingDynamicNearReserve=0`, reproducing the S6 path. `StreamingDynamicNearReserve` now
|
||||||
defaults to `1`; `StreamingCoarseFirst` remains `0`. That historical S6 matched measurement disabled
|
defaults to `1`; `StreamingCoarseFirst` remains `0`, and the apply boost remains `0` in the matched
|
||||||
the startup apply boost (`voxel.StreamingStartupApplyBoost=0`); the current default is on (§8.27).
|
measurement.
|
||||||
|
|
||||||
On the 2,578-key owner SurfaceWorld trace, the old-path control reached the full set in 17.527 s,
|
On the 2,578-key owner SurfaceWorld trace, the old-path control reached the full set in 17.527 s,
|
||||||
walkable ring in 0.650 s, and full LOD0 in 2.281 s. Dynamic admission reached them in 7.780 s,
|
walkable ring in 0.650 s, and full LOD0 in 2.281 s. Dynamic admission reached them in 7.780 s,
|
||||||
@@ -1843,480 +1840,6 @@ fill and check page tiles against the evaluator they actually sample. This is th
|
|||||||
the Unreal adapter and its asynchronous MPSC/Epoch flow are unchanged. See
|
the Unreal adapter and its asynchronous MPSC/Epoch flow are unchanged. See
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md).
|
[`Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md).
|
||||||
|
|
||||||
### 8.37 Shared strate boundaries and FloatingIslands lower seal — World Assembler L18
|
|
||||||
|
|
||||||
`FStandaloneShapeWorld::GetResolvedStratumBounds` supplies one physical lower/upper-bound descriptor
|
|
||||||
to layer ranges, far-field sample protection, and far-shell continuity for every strate. SurfaceWorld
|
|
||||||
retains its exact `topChunkZ * 32` Core-voxel mapping and native heightfield cap. Native FloatingIslands
|
|
||||||
takes its resolved bounds from its plan, whose chunk range is converted through the serialized
|
|
||||||
owner/Core voxel ratio. Its version-3 field removes only the lower all-XY seal and calls the same
|
|
||||||
`VF_ApplyUpperBoundarySeal` primitive used by the shared two-sided `VF_ApplyBoundarySeal` for its upper
|
|
||||||
boundary. The ceiling therefore sits at that strate's own upper boundary, with the common
|
|
||||||
`boundarySealSubvoxels` thickness; there is no FloatingIslands-specific height. The recipe range is
|
|
||||||
un-inset to match the Unreal adapter. Versions 1 and 2 retain their serialized two-sided seals.
|
|
||||||
SurfaceWorld's field and output hashes remain unchanged. See
|
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux18/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux18/REPORT.md).
|
|
||||||
|
|
||||||
### 8.38 Closed shared boundary ceilings and LOD sampling audit — World Assembler L19
|
|
||||||
|
|
||||||
`AppendStratumBoundaryCeilings` fills a candidate cell when the authoritative field is solid just
|
|
||||||
inside that strate's configured upper boundary. It does not require the raw field to cross back to
|
|
||||||
air above the boundary: the ceiling itself closes the strate even when the field continues solid
|
|
||||||
through that plane. This avoids isolated holes where a coarse lattice node has no zero crossing.
|
|
||||||
`FFarMeshColumnIndex::HasDownwardCellCoverage` still suppresses a fallback quad only after its
|
|
||||||
interior and perimeter probes all hit the existing downward face. The L19 `farHorizonVersion` is 4
|
|
||||||
for this shared boundary-cap change; hashes remain direct at LOD0–3 and salted at LOD4–8. No density
|
|
||||||
query, stratum position, scale, or asynchronous route changes. The strict LOD0–8 roof and paired-edge
|
|
||||||
audit and the matched before/after captures are in
|
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux19/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux19/REPORT.md).
|
|
||||||
|
|
||||||
### 8.39 Resolved layout snapshots and nominal strate bands — World Assembler L21 (historical)
|
|
||||||
|
|
||||||
`ResolvedLayoutSnapshotV1` is a separate Core-owned record built from the resolved manager slots and
|
|
||||||
the native SurfaceWorld/FloatingIslands plans. It has a canonical JSON representation, schema version,
|
|
||||||
and stable FNV-1a identity. Its physical intervals use half-open micrometre coordinates; recipe anchors
|
|
||||||
retain their serialized fixed-point units. The Unreal adapter exports it beside `world_assembly`, so the
|
|
||||||
layout digest never salts a recipe, manifest, density, or mesh hash. The standalone exporter follows
|
|
||||||
the same builder after plan resolution. It runs once per plan request and is not read from density or
|
|
||||||
tile-worker loops.
|
|
||||||
|
|
||||||
Each record separates the nominal ceiling, content, and bedrock bands. A strict layout requires that
|
|
||||||
sequence for every strate and requires bedrock(N) to touch the next strate's ceiling(N+1). The two
|
|
||||||
bands remain separately owned. Gradient and Interleaved interfaces may declare interpenetration only
|
|
||||||
inside their explicit transition zone; Hard has zero effective width. The manager's `TransitionBlendChunks`
|
|
||||||
is expressed in owner-chunk metres. L21 recorded the older Interleaved allowance as blend width plus
|
|
||||||
warp amplitude; L29 now treats the declared blend width as the complete physical crossing allowance.
|
|
||||||
Domain warp changes finger placement, but cannot extend overlap beyond that width. Compatibility validation reports current
|
|
||||||
gaps, overlaps, ambiguities, inactive/missing bands, and unhonoured transitions while accepting them;
|
|
||||||
strict validation rejects them. Core dispatches per-strate recipe fields without applying that
|
|
||||||
parameter blend, even though the legacy manager helper implements it. Both band types default diggable,
|
|
||||||
with protection represented explicitly.
|
|
||||||
|
|
||||||
Compatibility facts from L21: native SurfaceWorld plan/query bounds use 32 Core voxels (8 m) per
|
|
||||||
dispatch chunk, while FloatingIslands converts the 32-voxel owner chunk through its 12.5/25 cm scale
|
|
||||||
(16 Core voxels, 4 m). In the prototype where the top slot is FloatingIslands, this leaves a nominal
|
|
||||||
60 m interval between the FI bottom (−60 m) and SurfaceWorld top (−120 m); the Core selectors fall
|
|
||||||
through to the compiled analytic FloatingIslands fallback there. Core does not apply the manager's
|
|
||||||
parameter blend at either recipe boundary. The FI v3 upper seal remains a ceiling inside its current
|
|
||||||
content range, and its missing lower bedrock is recorded without changing evaluation. See
|
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux21/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux21/REPORT.md).
|
|
||||||
|
|
||||||
### 8.40 Fixed-metre strate layout units and shared boundary volumes — World Assembler L22
|
|
||||||
|
|
||||||
`StrateLayoutChunkSizeMeters` is one world-level setting, defaulting to **8 m**, independent of the
|
|
||||||
legacy 32-voxel chunk and Core's 32³ storage tile. The director resolves the setting once to physical
|
|
||||||
micrometres and Core subvoxels; every generator plan and dispatch reads that snapshot. At a 25 cm Core
|
|
||||||
voxel, an 8 m layout chunk is 32 Core voxels (8192 1/256-voxel units); at 20 cm it is 40 Core voxels
|
|
||||||
(10240 units). The Core tile remains 32³ voxels and is not a layout chunk.
|
|
||||||
|
|
||||||
The owner's FI/SW intervals are contiguous: FloatingIslands occupies [−120,+8) m and SurfaceWorld
|
|
||||||
occupies [−632,−120) m. Each strate is ceiling → content → bedrock. Both solid, diggable boundary
|
|
||||||
bands use its generator's existing `BoundarySealThickness` and `BaseDensity`; `InterStrateGapChunks`
|
|
||||||
controls only spacing, and `EdgeSealThickness` remains horizontal. No new thickness parameter or hidden
|
|
||||||
band height was added. The shared composition layer emits both volumes for every generator and the
|
|
||||||
shared mesh path closes their visible ceiling and floor faces.
|
|
||||||
|
|
||||||
The snapshot is schema V2 and is consumed by bounds planning, native and legacy dispatch, solid-band
|
|
||||||
composition, and validation. Strict validation accepts the real owner layouts, and the boundary probe
|
|
||||||
audit agrees at all 27 sampled endpoints in both recipe modes. The owner authored Gradient transition
|
|
||||||
is recorded as an explicit Hard fallback with zero interpenetration; Gradient and Interleaved mixing
|
|
||||||
remain unimplemented and must not be inferred from the declared blend width. Seven validator negative
|
|
||||||
controls continue to reject malformed bands.
|
|
||||||
|
|
||||||
Evidence and owner caveats are in
|
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux22/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux22/REPORT.md).
|
|
||||||
|
|
||||||
### 8.41 Director-resolved inter-strate passages and walkable landings — World Assembler L23
|
|
||||||
|
|
||||||
Core passage geometry is derived after the director has resolved physical strate bands and native
|
|
||||||
route anchors. `PlanResolvedLayoutPassagesV1` joins every adjacent resolved pair. Its placement hash
|
|
||||||
uses `(seed, upper strate, lower strate, connection, salt)`; `PassageConfig.DistanceMax` is the
|
|
||||||
outer radius around world `(0,0)` (24 m for the owner's first interface), while `DistanceMin` is
|
|
||||||
the inner radius used when a mouth has no route anchor. A route-bound mouth reuses the upper
|
|
||||||
strate's departure or lower strate's arrival anchor, after checking it lies within `DistanceMax`.
|
|
||||||
The legacy `UVoxelStrateManager::GeneratePassages` and its owner-voxel control points remain the
|
|
||||||
legacy generator path; Core does not import the exported legacy `passages` array. The season
|
|
||||||
manifest carries recipes and plans, and the adapter rebuilds the resolved layout and its passages
|
|
||||||
from the current manager slots and route anchors each time it attaches the Core world.
|
|
||||||
|
|
||||||
`ApplyResolvedStrateComposition` composes the current generator field, applies the resolved ceiling
|
|
||||||
and bedrock bands, then unions the passage carve and floor support. Only the hashed capsule path
|
|
||||||
opens a band; the support strip stays below the player capsule. The lower mouth index splits the
|
|
||||||
monotone descent from the short route-bound landing tail, whose height may vary with the route.
|
|
||||||
Both route and passage audits sample the 0.34 m by 1.76 m capsule, support probes, and grade; the
|
|
||||||
negative control compiles the same world without passage carving and must detect a blocked capsule.
|
|
||||||
|
|
||||||
The L23 shared-field change is `WorldAssemblyEvaluatorVersion=9`. It does not change the L22 bands
|
|
||||||
or any generator's field away from a passage. The L23 report includes exact-query/hash comparisons,
|
|
||||||
the blocked control, captures, and the Windows adapter checklist. §8.10's cache, proof, streaming,
|
|
||||||
and async rules remain unchanged.
|
|
||||||
|
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux23/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux23/REPORT.md)
|
|
||||||
records the audit and captures.
|
|
||||||
|
|
||||||
### 8.42 Per-strate natural passages and the FloatingIslands anchor helix — World Assembler L24
|
|
||||||
|
|
||||||
`FStratePassageConfig.Style` now drives the shared Core planner for every generator. The default is
|
|
||||||
`Worm`; Straight, Spiral and Cascading remain selectable per upper strate. Passage radius, reach,
|
|
||||||
distance and style stay in the resolved-layout input, so equal seeds and route anchors produce equal
|
|
||||||
paths on every peer. The passage carver adds bounded, deterministic wall relief (`0.24 m` before the
|
|
||||||
gravity-facing attenuation) while its support tread follows the same route profile below the capsule.
|
|
||||||
|
|
||||||
FloatingIslands' owner config selects Spiral. A seed-derived anchor island near `(0,0)` is joined to
|
|
||||||
bedrock by a 6 m radius rock root. The 3 m helix descends from the island mouth to the `-119.5 m`
|
|
||||||
bedrock top inside that root, leaving at least the authored clearance around its 1.5 m bore, then
|
|
||||||
widens into the Worm route toward the SurfaceWorld arrival. The selected anchor is ordinal 2; the
|
|
||||||
other island ordinals and their placement hashes are unchanged. FloatingIslands recipe geometry is
|
|
||||||
version 5 and the shared passage evaluator is version 10; the resolved-layout snapshot stays V3.
|
|
||||||
|
|
||||||
The passage naturalness audit rejects a horizontal run above 8 m (segments flatter than 0.5 degrees)
|
|
||||||
or wall relief below 0.20 m peak-to-peak across at least 8 valid samples. The smooth-wall control
|
|
||||||
has zero relief and fails; the anchored Spiral has 0 m flat run and 0.793 m measured relief. Strict
|
|
||||||
layout controls, route binding, capsule clearance, support, grade, and the no-carve negative control
|
|
||||||
remain required. Passage-centered interval proofs remain unknown/fallback rather than falsely
|
|
||||||
certified. At the lower mouth, the shared layer now relies on native terrain plus the passage tread;
|
|
||||||
it does not stamp additional flat route-anchor shelves over the cave opening. The L24 report records
|
|
||||||
compiler/worker parity, unchanged SurfaceWorld hashes away from the route, captures and the Windows
|
|
||||||
adapter checklist.
|
|
||||||
|
|
||||||
The Linux offscreen capture tool accepts `--headlamp=on` to aim brighter readable shading from the
|
|
||||||
camera for interior views and `--clip-plane=NX,NY,NZ,D` to discard the plane's negative half-space for
|
|
||||||
cutaway views. Both affect captures only, not generated geometry.
|
|
||||||
|
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux24/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux24/REPORT.md)
|
|
||||||
records the evidence and owner-facing caveats.
|
|
||||||
|
|
||||||
### 8.43 Enclosed FloatingIslands helix and native-solid passage support — World Assembler L25
|
|
||||||
|
|
||||||
L24's support tread was composited into the passage field without requiring native solid terrain at
|
|
||||||
the support sample. A 6 m root could not contain the resolved helix either: the centreline reaches
|
|
||||||
5.562 m from the anchor axis and the mouth bore is 4.5 m. With the old root radius, the tread could
|
|
||||||
therefore read as a free-floating spiral outside the rock. This was not a unit conversion error.
|
|
||||||
|
|
||||||
The shared passage composer now emits route-mouth support, tread, and fallback support only where
|
|
||||||
the pre-passage native density is solid. The open-air audit samples each support footprint at 0.25 m
|
|
||||||
spacing and two depths, including route-mouth shelves; its L24 ungated-support control must report a
|
|
||||||
violation. The FI anchor root is now a relief-bearing 13.25 m nominal rock column. Across five
|
|
||||||
heights its measured radius is 12.0–13.375 m. Taking the maximum 5.562 m centreline radius and the
|
|
||||||
full 4.5 m mouth bore at every height gives a conservative minimum wall of 1.938 m, exceeding the
|
|
||||||
previous 1.5 m reserve. The authored mid-bore remains 3 m. The helix is selected from its route-bound
|
|
||||||
root point and joins the lower route before the tunnel hands off through the shared SW ceiling.
|
|
||||||
|
|
||||||
The configured FI Spiral passes the capsule/support/grade audit with zero blocked, unsupported, or
|
|
||||||
grade-failure samples; the L23 smooth-wall control fails naturalness and the L24 ungated-support
|
|
||||||
control fails the open-air audit. The other style overrides also pass the open-air audit, but their
|
|
||||||
capsule results are recorded individually in the L25 report because some overrides are not
|
|
||||||
walkable after floating support is suppressed. Evaluator version is 12, FI geometry version is 7,
|
|
||||||
and resolved-layout schema V3 remains unchanged. The L25 report records measurements, hashes,
|
|
||||||
captures, style results, and the single Unreal adapter version edit.
|
|
||||||
|
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux25/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux25/REPORT.md)
|
|
||||||
records the evidence and caveats.
|
|
||||||
|
|
||||||
### 8.44 Walkable passages as tunnels in rock — World Assembler L26
|
|
||||||
|
|
||||||
L25 correctly stopped support from appearing in native air, but its SurfaceWorld-recipe passages
|
|
||||||
still crossed a long open volume in the SurfaceWorld arrival strata. The segment map places that gap
|
|
||||||
inside SurfaceWorld (about `-146` to `-497 m`), after the TunnelNetwork transition; it is not TunnelNetwork
|
|
||||||
cave air. Both ends meet native rock. The shared Core planner now finds contiguous unsupported ranges
|
|
||||||
from the native field sign and caches them by resolved-layout identity. It adds a deterministic,
|
|
||||||
relief-bearing rock span only across those ranges. The span is at least 5 m wide, several metres thick,
|
|
||||||
and joins native rock at both ends. The passage carve then opens the bore through the added density,
|
|
||||||
leaving a grounded floor and rock walls/ceiling. SurfaceWorld away from the passage remains unchanged.
|
|
||||||
|
|
||||||
Native route support remains gated on pre-passage solid density. Before adding any support density, the
|
|
||||||
composer also checks the resolved player capsule, so neighboring floor segments cannot restore rock
|
|
||||||
inside the avatar path. This keeps all four style overrides walkable in both recipes. The bridge audit
|
|
||||||
measures its authored density field at 256 route sections, checks the 5 m width and connected ends, and
|
|
||||||
records a relief-bearing thickness range; the open-air audit still runs the L24 negative control.
|
|
||||||
|
|
||||||
The shared evaluator is version 13; FloatingIslands geometry remains version 7 and resolved-layout
|
|
||||||
schema V3 is unchanged. These changes are in Core and the standalone audit/capture path; no Unreal
|
|
||||||
adapter edit is required. The L26 report contains the eight-style table, hashes, captures and run history.
|
|
||||||
|
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux26/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux26/REPORT.md)
|
|
||||||
records the final evidence.
|
|
||||||
|
|
||||||
### 8.45 Tile-bounded passage evaluation — World Assembler M3 Linux L28
|
|
||||||
|
|
||||||
Each mesh tile scopes passage composition to the inclusive density-sample AABB, including the
|
|
||||||
mesher's normal halo. A conservative feature AABB BVH selects route segments, columns and mouth
|
|
||||||
support for that tile. A per-passage nearest-segment BVH bounds the long helix search; a separate
|
|
||||||
passage-bound BVH rejects far L9 proof boxes. An unindexed native-only proof object uses a conservative
|
|
||||||
linear fallback. No field, union order, serialized layout or recipe version changes.
|
|
||||||
|
|
||||||
The column's exact complex SDF is evaluated only in XY/Z feature candidates. A cheap density-aware
|
|
||||||
radial guard remains for samples sharing the column's Z slab: a positive column distance can still
|
|
||||||
lower positive base density, so skipping that guard needs an exact bound on the base field. This is
|
|
||||||
the remaining SurfaceWorld cost noted in the Linux L28 report. The passage candidate lists and
|
|
||||||
proof bounds preserve the L27 output.
|
|
||||||
|
|
||||||
[Linux28 report](Standalone/Research/WorldAssemblerM3/Linux28/REPORT.md)
|
|
||||||
records counts, parity, costs and audit caveats.
|
|
||||||
|
|
||||||
### 8.46 Honored bedrock/ceiling transitions — World Assembler L29
|
|
||||||
|
|
||||||
The upper strate's `EVoxelStrateTransition` controls the shared interface between its bedrock band
|
|
||||||
and the next strate's ceiling band in the Core composition path. `TransitionBlendChunks` is converted
|
|
||||||
using the resolved layout chunk width (8 m by default). Hard preserves the flat interface. Gradient
|
|
||||||
moves the shared surface with a smooth 2D hash field whose peak-to-peak range is at most one quarter
|
|
||||||
of the declared width. Interleaved uses a seed/interface/coordinate-keyed, domain-warped 3D hash field
|
|
||||||
to set actual overlap between the facing bands from zero to the declared width. The two faces move
|
|
||||||
inward by half that overlap; the warp changes finger placement without increasing the crossing
|
|
||||||
allowance. Both bands keep authored thickness, and their solid intervals touch or overlap in every
|
|
||||||
XY column. Core does not blend unrelated generator fields.
|
|
||||||
|
|
||||||
Each boundary band keeps at least **two Core voxels** of solid thickness (0.5 m at the default 25 cm
|
|
||||||
voxel size). Strict validation rejects thinner bands and declared interpenetration beyond the
|
|
||||||
transition width. The world audit samples a 512 m square XY area, measures actual crossing depth and
|
|
||||||
minimum band thickness, and sweeps every non-passage column through both bands at half-voxel spacing.
|
|
||||||
Passage columns are excluded from the hole sweep. Near an interface mouth, the shared face returns to
|
|
||||||
its nominal height inside the mouth radius plus a 1 m support margin, then fades back to the selected
|
|
||||||
transition over 2 m so the authored landing floor remains supported. Negative controls reject a
|
|
||||||
transition one micrometre wider than declared and a band one micrometre below the minimum. See
|
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux29/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux29/REPORT.md)
|
|
||||||
for audit measurements, hashes, timings, captures and run history.
|
|
||||||
|
|
||||||
### 8.47 Ground-bound SurfaceWorld departure routes — World Assembler M3 Linux L33
|
|
||||||
|
|
||||||
An intra-strate SurfaceWorld route follows the resolved terrain ground profile. Its route-point Z
|
|
||||||
is the ground height plus the player's half-height; the route does not descend to the strate's
|
|
||||||
bedrock boundary. Bedrock belongs to the separate inter-strate passage, which begins at the
|
|
||||||
SurfaceWorld departure mouth on the ground and tunnels outward and down through native rock and
|
|
||||||
the boundary into the next strate.
|
|
||||||
|
|
||||||
The old fixed candidate envelope used a 64 m chord and 16 m centre spacing. On the L33 upper-SW
|
|
||||||
fixture, replaying that envelope sampled ground from −507.767 to −464.857 m; 366/648 candidate paths
|
|
||||||
exceeded the 15° grade limit. The worst failures clustered near the outer west/northwest terrain
|
|
||||||
shoulder (up to 79.971°) and the steep approach slopes around the origin. Candidate diagnostics
|
|
||||||
separate grade failures from endpoints outside the bounded arrival/departure placement disks.
|
|
||||||
The bounded search now limits its longest chord to the two mouth radii minus a 3 m clearance
|
|
||||||
margin, checks centres every 8 m, and ranks endpoints near the route anchors. It finds a 21.5 m
|
|
||||||
route from (−2.75, 0) to (18.75, 0), sampled on ground and accepted by the full capsule/support/
|
|
||||||
grade audit.
|
|
||||||
|
|
||||||
The SurfaceWorld departure passage starts at the ground mouth with a 1.5 m portal, then smoothsteps
|
|
||||||
to a 3.5 m rock tunnel over 9 m. Its ring is sized and oriented outward from the mouth, so the
|
|
||||||
approach does not cut back under the walk route. The `FloatingIslands → SurfaceWorld → TunnelNetwork`
|
|
||||||
fixture passes all four passage styles, including both interfaces, capsule clearance, support,
|
|
||||||
grade, walkability, naturalness, open-air, column ends, long-bridge and route-binding audits.
|
|
||||||
|
|
||||||
`FloorProfileBlend` previously compared each curved floor profile with its straight chord and
|
|
||||||
applied a 2-voxel limit. That is a curvature measurement, not a seam measurement. It now checks
|
|
||||||
endpoint height and tangent continuity at 0.01 voxel and 0.001 voxel/m; the 3-voxel step and
|
|
||||||
0.25 voxel/m tangent-step controls are both detected. SurfaceWorld owner hashes remain identical
|
|
||||||
to L32 because the route-mouth, expanded column sizing and short-span support changes apply only
|
|
||||||
when SurfaceWorld is the upper strate. No evaluator or recipe version changed.
|
|
||||||
|
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux33/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux33/REPORT.md)
|
|
||||||
records the four-style audits, captures, determinism, owner hashes, costs and retries.
|
|
||||||
|
|
||||||
### 8.48 Irregular passage-wall relief and grounded tread shoulders — World Assembler M3 L35
|
|
||||||
|
|
||||||
L34's owner-SW profile had a 7.438 m radius repeat with 0.0361 m (3.61 cm) amplitude; an independent
|
|
||||||
mesh-normal scan also found a 4.750 m period at 0.02682 (1.537°). The geometry came from piecewise
|
|
||||||
capsule joins, while broad value-noise bands added a second regular pattern. The shared carve now
|
|
||||||
uses Catmull–Rom centerline interpolation on eligible interior segments, excluding short spans and
|
|
||||||
terminal controls. Passage walls use deterministic, rotated 43/29/19 m relief with reduced terrain
|
|
||||||
band contribution; other `VF_FieldSurfaceRelief` users, including grounded bridge columns, are
|
|
||||||
unchanged. The shared evaluator is version 21.
|
|
||||||
|
|
||||||
The residual radius scan on owner-SW style 0 measures a 7.062 m period at 0.0109 m (1.09 cm),
|
|
||||||
with 0.0044 m (0.44 cm) coherent amplitude. The independent L35 mesh-normal scan passes at 8 m
|
|
||||||
and 0.02496 axial amplitude, below its 0.0255 threshold. The ripple audit samples both side-wall
|
|
||||||
radii every 0.125 m in 32 m windows, scans 0.5–8 m periods, and rejects amplitudes of 0.035 m or
|
|
||||||
more. Its synthetic control uses 2 m radius spheres spaced every 1.5 m; it measures 0.0599 m and
|
|
||||||
must fail.
|
|
||||||
|
|
||||||
At the FI mouth, the flat apron is the shared passage support tread, not `LandingExtensionQ`, a tile
|
|
||||||
edge or a separate support layer. The tread is required by capsule and support probes. L35 gives its
|
|
||||||
outer entry shoulder a shallow taper and seeded grain while preserving the supported walk core and
|
|
||||||
lower-mouth overlap width. The shallow quarter-weight support-height/radius spline keeps the tread
|
|
||||||
continuous without moving its XY footprint.
|
|
||||||
|
|
||||||
Cascade routes also replace identical full-depth cosine lobes with a seeded shallow profile on each
|
|
||||||
turn. Passage, capsule, support, open-air, bridge, binding, strict layout, capture, determinism, ring,
|
|
||||||
and cost evidence are recorded in
|
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md).
|
|
||||||
|
|
||||||
### 8.49 Walk-floor washboard and FI mouth naturalness — World Assembler M3 L36
|
|
||||||
|
|
||||||
L35's exported owner-SW mid-tunnel floor measured a 7.250 m repeat at 0.02252 m amplitude; its
|
|
||||||
upper-SW fixture arrival measured a 1.438 m repeat at 0.02482 m. Both came from segment-built,
|
|
||||||
one-sided support treads meeting the native terrain zero crossing at per-segment joins. The floor
|
|
||||||
probe samples the exported walk mesh along the resolved route, separately from the wall-radius scan.
|
|
||||||
Eligible tread sections overlap by up to 2 m and all sparse route segments now use a bounded
|
|
||||||
signed-distance slab, including SurfaceWorld-to-TunnelNetwork routes. This keeps the tread top stable
|
|
||||||
across segment joins while capsule-clearance and native-solid gates retain walkability and open air.
|
|
||||||
Regular tread tops remain seated 5 cm lower and ease back to 0.20 m within 20 m of a lower landing;
|
|
||||||
seeded shoulder grain is removed, and gravity-facing relief is reduced while side-wall and ceiling
|
|
||||||
relief remain irregular.
|
|
||||||
|
|
||||||
On v23, the same mesh probes measure 7.188 m / 0.00067 m (0.067 cm) on owner-SW and 6.500 m /
|
|
||||||
0.00057 m (0.057 cm) on the upper fixture. The density audit checks five 32 m walk-floor windows at
|
|
||||||
0.125 m spacing, rejects amplitudes of 0.015 m or more, and detects its beaded control. The FI mouth
|
|
||||||
uses a local 2 m symmetric signed-field filter with the route protected and joins route-mouth support
|
|
||||||
to native terrain with a 1.5 m smooth signed-distance union. Its mesh audit reduces the L35
|
|
||||||
42.27° fade-ring jump to 27.13° and finds no detached component above the 0.0001 m² area floor; one
|
|
||||||
8-triangle subthreshold component at 0.0000266 m² is recorded. The evaluator is version 23. Full
|
|
||||||
recipe/style audits, route support, open-air, strict layout, legacy validators, determinism, capture and
|
|
||||||
paired-cost results are in
|
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md).
|
|
||||||
|
|
||||||
### 8.50 Immutable field binding — World Assembler M3 L40
|
|
||||||
|
|
||||||
`FStandaloneShapeWorld` now keeps a sorted `FBoundStratumField` table built from the resolved layout
|
|
||||||
at world construction. Each entry owns a concrete SurfaceWorld, FloatingIslands, or legacy adapter
|
|
||||||
in `FGeneratorFieldAdapterV1`, plus the resolved bounds and native plan pointer. The adapters share
|
|
||||||
the immutable `densityAt(x,y,z,out)` shape, while `std::variant` keeps dispatch concrete instead of
|
|
||||||
adding a virtual call to every density sample. FloatingIslands keeps its original fixed-point entry
|
|
||||||
point for sample evaluation and its boundary parameter table is captured when the adapter is bound.
|
|
||||||
|
|
||||||
`DensityAt`, native SurfaceWorld/FloatingIslands evaluation, legacy SurfaceWorld bridging and
|
|
||||||
`ClassifyBoxDetailed` select from the bound table. The native adapters no longer ask the manager
|
|
||||||
which generator or plan to use per sample; box proof reuses the bound field objects. The existing
|
|
||||||
float-to-fixed conversion order, density arithmetic and shared boundary composition remain intact.
|
|
||||||
The manager's layout-width value is captured once for the legacy-compatible chunk-index projection.
|
|
||||||
|
|
||||||
The legacy adapter is still a compatibility bridge: `UVoxelGenerator::GetDensityAt` retains its
|
|
||||||
own manager routing when invoked for legacy SurfaceWorld fields or out-of-plan fallback queries.
|
|
||||||
Other shared-composition policy branches and passage/boundary planning remain outside this slice.
|
|
||||||
No Unreal adapter or `VoxelWorld.cpp` change was needed, and the evaluator version is unchanged.
|
|
||||||
L37/L39 owner hashes are the parity baseline; the L40 report records fast, medium, full and paired
|
|
||||||
cost evidence.
|
|
||||||
|
|
||||||
### 8.51 Resolved metre layout authority — World Assembler M3 L41
|
|
||||||
|
|
||||||
`UVoxelStrateManager::Initialize` now resolves the layout in integer micrometres before passages or
|
|
||||||
Core work are scheduled. The configured layout width is rounded once to micrometres; legacy height
|
|
||||||
and inter-strate gap counts are multiplied by that width. Each slot stores its half-open physical
|
|
||||||
interval `[bottom, top)`, height, transition mode/width, copied passage settings and their physical
|
|
||||||
dimensions. The effective world seed is captured in the manager before passage generation.
|
|
||||||
|
|
||||||
The historical 8 m layout keeps its old placement: the first upper edge is +8 m, and each authored
|
|
||||||
chunk of height or gap is exactly 8 m. `TopChunkZ` and `BottomChunkZ` are floor-based projections of
|
|
||||||
the resolved interval, including mathematical floor for negative Z. Season schema v2 still validates
|
|
||||||
its aligned top/bottom chunk values and stacked order as before, then maps those accepted projections
|
|
||||||
to the same physical intervals. L29's facing ceiling/bedrock bands are derived from these physical
|
|
||||||
intervals and the copied transition width, so their metre bounds retain the prior chunk-based values.
|
|
||||||
|
|
||||||
The Core adapter builds SurfaceWorld and FloatingIslands bounds, shared-layout bands, and resolved
|
|
||||||
passage dimensions from the micrometre fields. FI and SW both receive the same
|
|
||||||
`LayoutChunkWidthSubvoxels = round(layoutWidthMicrometres * 256 / coreVoxelMicrometres)`; layout
|
|
||||||
conversion does not use the owner/Core scale. Owner/Core ratios remain in the legacy FloatingIslands
|
|
||||||
parameter adapter for generator dimensions that are still authored in owner voxels. The manager's
|
|
||||||
legacy passage builder converts resolved passage micrometres back to owner voxels only at its legacy
|
|
||||||
geometry API boundary.
|
|
||||||
|
|
||||||
Chunk coordinates remain in `GetStrateForChunk`, `GetGeneratorTypeForChunk`, chunk-parameter queries,
|
|
||||||
season-v2 validation, and legacy generator dispatch because those interfaces accept layout-chunk
|
|
||||||
indices. Their values are cached projections from the physical slot bounds. World-Z queries and the
|
|
||||||
Core snapshot use the half-open metre intervals. L40's bound Core field table remains the native
|
|
||||||
per-sample dispatch path; this migration adds no manager lookup to it. The Unreal manager and adapter
|
|
||||||
cannot be compiled in this Linux workspace, so their small source changes still need the Windows
|
|
||||||
Unreal build before integration.
|
|
||||||
|
|
||||||
### 8.52 Resolved boundary and landing capabilities — World Assembler M3 L42
|
|
||||||
|
|
||||||
`ResolvedLayoutSnapshotV3::finalize` materializes runtime capability records from the immutable
|
|
||||||
strate/interface fields before sampling begins. The upper interface records whether its Gradient FI
|
|
||||||
bedrock air gap remains open and whether a SurfaceWorld-to-TunnelNetwork passage gets the special
|
|
||||||
landing support profile. Per-strate passage records carry the existing SurfaceWorld landing profile,
|
|
||||||
FloatingIslands route-support behavior, and passage-anchor column center policy. Shared boundary and
|
|
||||||
passage composition reads these records; it no longer compares generator-kind strings.
|
|
||||||
|
|
||||||
The records are deterministic derivatives of `generator_kind`, `transition_mode`, and the resolved
|
|
||||||
adjacent strate, and are omitted from canonical JSON. Those source fields remain in the hashed layout,
|
|
||||||
so owner layout IDs and density hashes retain their L41 identity. The upper strate still controls
|
|
||||||
Hard/Gradient/Interleaved evaluation; each strate still composes ceiling, content, then breakable
|
|
||||||
bedrock. L29 transition samples and the passage-mouth fade arithmetic are unchanged.
|
|
||||||
|
|
||||||
`BindResolvedStratumFields` still tests the resolved generator kind once to bind the concrete
|
|
||||||
SurfaceWorld, FloatingIslands, or legacy evaluator. Passage planning in `ResolvedLayoutInternal.h`
|
|
||||||
also retains generator-specific anchor and default-mouth placement rules; those are outside the
|
|
||||||
shared composition boundary policy. No Unreal source changed. See the L42 report for hashes, tier
|
|
||||||
results and paired cost measurements.
|
|
||||||
|
|
||||||
### 8.53 Shared Core landmark SDF stamps — World Assembler M3 L43
|
|
||||||
|
|
||||||
`ResolvedLayoutStampV1` adds a small, version-1 SDF vocabulary to the shared resolved layout:
|
|
||||||
carve ellipsoid, carve arch, fill pillar and fill dais. Requests and resolved records use integer
|
|
||||||
micrometres and carry a stable ID, strate, position, conservative bounds, half extents, smooth-CSG
|
|
||||||
width, bounded irregularity amplitude/wavelength and quarter-turn orientation. A stamp's seeded
|
|
||||||
noise and bounded placement offsets derive from `(world seed, strate, stable ID)`; records are
|
|
||||||
sorted by stable ID before the layout identity is finalized. If no request is made, the stamp fields
|
|
||||||
are omitted from canonical JSON, preserving the no-stamp layout identity and density hashes.
|
|
||||||
|
|
||||||
Resolution retries a finite, deterministic set of candidates and rejects a record if its complete
|
|
||||||
bound intersects a passage mouth or walk corridor, reaches a strate content face, or overlaps the
|
|
||||||
ceiling or bedrock band. The director reports failure if no safe candidate exists. The strict-layout
|
|
||||||
negative control forces a candidate onto a route and into a ceiling band and expects both rejections.
|
|
||||||
|
|
||||||
`FStandaloneShapeWorld` applies stamps after the shared resolved-strate boundary and passage
|
|
||||||
composition. Carves use smooth maximum with the negated stamp SDF; fills use smooth minimum. Seeded
|
|
||||||
trilinear value noise adds low-frequency relief without changing the conservative bound. The tile
|
|
||||||
query scope builds a BVH candidate list from each tile's sample bounds. Samples in a tile outside
|
|
||||||
every stamp box do no stamp SDF evaluation. Box classification marks stamp-intersecting boxes mixed,
|
|
||||||
and native-only readiness refuses to skip them.
|
|
||||||
|
|
||||||
The `--landmark-chamber-demo` recipe flag adds an irregular ellipsoid chamber, a separate arched
|
|
||||||
entrance and a low fill dais in the owner SurfaceWorld strate, about 31 m from the passage arrival
|
|
||||||
mouth and off the walk route. It stays disabled by default. Active records carry vocabulary version
|
|
||||||
1 in the layout identity, making the demo an explicit world change; no Unreal adapter was added.
|
|
||||||
L43's report records the off/on hashes, walk and interface audits, outside-stamp timing, tier
|
|
||||||
results, captures and any failed runs.
|
|
||||||
|
|
||||||
### 8.54 Opt-in native lattice field — World Assembler M3 R26 slice 1 (Linux L58)
|
|
||||||
|
|
||||||
`Core.Lattice.PrimitiveField` is a version-1 recipe selected only for one Maze or VerticalShafts
|
|
||||||
strate by `--recipe=lattice --lattice-strate=N`. The legacy generator assets and their default
|
|
||||||
dispatch stay unchanged. Planning first binds the actual incoming lower mouth and outgoing upper
|
|
||||||
mouth from the resolved passage records, then creates arrival/exit overlap corridors, a mandatory
|
|
||||||
four-turn helix and the maze spine; two deterministic dead ends are added after that route. The
|
|
||||||
resolved room and edge seeds use `(world seed, strate, recipe version, stable ID)`. An audit control
|
|
||||||
translates the helix 20 m away from both route junctions to make the shaft network orphaned.
|
|
||||||
|
|
||||||
The prototype helix uses a 6 m centreline radius, 3 m clear width, 5 m pitch, four turns and 2.6 m
|
|
||||||
finished floor-to-soffit height. Its centreline descends 20 m over roughly 151 m; the inner-edge
|
|
||||||
grade is about 10.0 degrees. The 2.4 m floor-wall smooth-max gives an isolated ideal fillet radius
|
|
||||||
of 0.8485 m. A separate 0.4 m feature-union blend prevents adjoining ramps from erasing the tread.
|
|
||||||
The swept-clear diameter is 15 m; the conservative padded feature envelope is about 20.6 m (the
|
|
||||||
approved approximately 21 m site envelope).
|
|
||||||
|
|
||||||
The layout reuses the resolved cavern graph vocabulary. A per-strate opt-in selects the native
|
|
||||||
negative-solid/positive-air field; canonical feature order is stable-ID order. Room/corridor bounds
|
|
||||||
are indexed by the graph BVH and polyline segments have a nested deterministic BVH for density and
|
|
||||||
interval queries. Box classification stays conservative, and `isTileShapeFree` refuses any tile
|
|
||||||
that intersects the graph. The L58 pipeline audits the composed field from actual mouth to mouth
|
|
||||||
with a 0.34 m by 1.76 m capsule, per-station support, 2.2 m clearance and a 15 degree grade limit;
|
|
||||||
it also checks the orphan control, brute-force interval samples and the composed wall-foot fillet.
|
|
||||||
The Linux58 report contains the measured reachability, surface, peer-determinism, timing and capture
|
|
||||||
results; the code path is not promoted to legacy Maze/VerticalShafts generation.
|
|
||||||
|
|
||||||
### 8.55 Unreal CoreGeometry asset bridge and U1 asset version
|
|
||||||
|
|
||||||
`UVoxelStrateDefinition::CoreGeometry` is optional and defaults to recipe `None` with no stamps,
|
|
||||||
rooms, or tunnels. The Unreal adapter preserves the established passage planner, then resolves
|
|
||||||
authored stamps and cavern graph records, then finalizes the layout. Stable names are required and
|
|
||||||
are scoped by strate in Core; landmarks may target a stamp, room, or tunnel by that stable name.
|
|
||||||
Core profile heightfields are restricted to SurfaceWorld. Native caves require the TunnelNetwork or
|
|
||||||
Underwater archetype; native lattice requires Maze or VerticalShafts. Core recipe/version fields
|
|
||||||
enter the resolved layout identity when selected. An imported manifest cannot be mixed with a
|
|
||||||
per-strate profiled heightfield.
|
|
||||||
|
|
||||||
The checked-out Core API contains stamp vocabularies V1, V2, and V3. Unreal uses V1 for the original
|
|
||||||
stamp kinds and V3 for monolith/platform motifs, then combines and strictly validates the result.
|
|
||||||
Research17 names V2, but the source API and motif vocabulary require V3; source was followed.
|
|
||||||
Research27 water remains out of scope for U1.
|
|
||||||
|
|
||||||
The shared VoxelForge custom asset version is 2: version 1 records the tunnel-floor slope migration;
|
|
||||||
version 2 adds load-time conversion for inter-strate gap, strate height, and transition width. The
|
|
||||||
saved strate-layout width is used, currently 8 m. Old packages are converted in memory and marked
|
|
||||||
dirty, but U1 does not save them. This does not yet migrate the rest of Research8's reflected
|
|
||||||
spatial-property inventory: legacy chunk distances, owner/Core voxel lengths and frequencies,
|
|
||||||
placement vectors, and other centimetre fields remain in their prior units. The U1 report records
|
|
||||||
the five BuildHost asset values, hashes, and remaining work.
|
|
||||||
|
|
||||||
## 9. Multiplayer model (listen-server first, dedicated-friendly)
|
## 9. Multiplayer model (listen-server first, dedicated-friendly)
|
||||||
|
|
||||||
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
|
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ then wait for his results / pasted errors. Same for in-editor checks — ask for
|
|||||||
try to run anything.
|
try to run anything.
|
||||||
|
|
||||||
## Read first (in order, load only what the task needs)
|
## Read first (in order, load only what the task needs)
|
||||||
- **[CURRENT-STATE.md](CURRENT-STATE.md) — current shipping snapshot and verification entry point.**
|
|
||||||
- **[CODEMAP.md](CODEMAP.md) — ALWAYS.** Navigation: what it is, data-flow (§2), symbol→`file:line`
|
- **[CODEMAP.md](CODEMAP.md) — ALWAYS.** Navigation: what it is, data-flow (§2), symbol→`file:line`
|
||||||
index (§3), "I want to change X → go here" (§5), conventions & gotchas (§6). Trust **symbol names
|
index (§3), "I want to change X → go here" (§5), conventions & gotchas (§6). Trust **symbol names
|
||||||
over line numbers** (lines drift).
|
over line numbers** (lines drift).
|
||||||
@@ -27,12 +26,9 @@ try to run anything.
|
|||||||
- **Density sign: negative = solid, positive = air** (MC convention at the mesher).
|
- **Density sign: negative = solid, positive = air** (MC convention at the mesher).
|
||||||
`FVoxelModification::Strength` negative = carve. *The #1 source of confusion.*
|
`FVoxelModification::Strength` negative = carve. *The #1 source of confusion.*
|
||||||
- **Density functions take VOXEL coords, not cm.** The legacy plugin field uses 12.5 cm voxels;
|
- **Density functions take VOXEL coords, not cm.** The legacy plugin field uses 12.5 cm voxels;
|
||||||
the opt-in Core field uses 25 cm voxels and its own 32³-voxel storage tile. The legacy 32³ chunk
|
the opt-in Core field uses 25 cm voxels and its own tile keys. One legacy 32³ chunk is 4 m.
|
||||||
spans 4 m at 12.5 cm. Strate placement uses the separate world-level `StrateLayoutChunkSizeMeters`
|
Conversions live in `VoxelTypes.h` and the Core bridge; do not mix scales in queries, anchors, edits,
|
||||||
setting (8 m by default), resolved once and shared by every generator. At 25 cm that layout unit is
|
or surface lookups.
|
||||||
32 Core voxels; at 20 cm it is 40. Never infer the layout unit from `CHUNK_SIZE` or the Core tile.
|
|
||||||
Conversions live in `VoxelTypes.h`, the director, and the Core bridge; do not mix scales in queries,
|
|
||||||
anchors, edits, or surface lookups.
|
|
||||||
- **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.
|
||||||
- **Core world invariants:** Core density keeps the same sign (negative solid, positive air), but Core
|
- **Core world invariants:** Core density keeps the same sign (negative solid, positive air), but Core
|
||||||
|
|||||||
+33
-248
@@ -29,9 +29,7 @@ A **density-field voxel terrain** plugin for Unreal Engine, built around undergr
|
|||||||
### Core terminology
|
### Core terminology
|
||||||
| Term | Meaning |
|
| Term | Meaning |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| Legacy chunk | 32×32×32 voxels (`CHUNK_SIZE`). At the legacy 12.5 cm voxel size it spans 4 m; it is not the strate-layout unit or the Core tile. |
|
| Chunk | 32×32×32 voxels (`CHUNK_SIZE`). Voxel = 12.5 cm (`VOXEL_SIZE`), so a chunk is 4 m. |
|
||||||
| Strate-layout chunk | World-level `StrateLayoutChunkSizeMeters`, default 8 m, shared by all generators and independent of voxel size. `UVoxelStrateManager::Initialize` resolves it to integer micrometres; slot bounds, heights, gap and transition widths use those physical values. Core derives the common `LayoutChunkWidthSubvoxels` from that same width. At 25 cm it is 32 Core voxels; at 20 cm it is 40 Core voxels. |
|
|
||||||
| Core tile | Internal 32×32×32 Core voxel tile. Its metre size follows the Core voxel size and stays separate from the configurable layout chunk. |
|
|
||||||
| Density | Scalar field. `< 0` solid, `> 0` air, `0` = surface (`IsoLevel`). |
|
| Density | Scalar field. `< 0` solid, `> 0` air, `0` = surface (`IsoLevel`). |
|
||||||
| Strate | Vertical layer of the world, stacked downward. Has its own generator + params. |
|
| Strate | Vertical layer of the world, stacked downward. Has its own generator + params. |
|
||||||
| Generator type | Archetype per strate: `TunnelNetwork`, `FlatPlain`, `CrystalChamber`, `Maze`, `SurfaceWorld`, `VerticalShafts`, `FloatingIslands`, `Underwater`. See §8. |
|
| Generator type | Archetype per strate: `TunnelNetwork`, `FlatPlain`, `CrystalChamber`, `Maze`, `SurfaceWorld`, `VerticalShafts`, `FloatingIslands`, `Underwater`. See §8. |
|
||||||
@@ -70,10 +68,8 @@ A **density-field voxel terrain** plugin for Unreal Engine, built around undergr
|
|||||||
(RealtimeMeshComponent)
|
(RealtimeMeshComponent)
|
||||||
```
|
```
|
||||||
|
|
||||||
`UVoxelStrateManager` owns resolved half-open metre intervals (`[bottom, top)`) and is the side
|
`UVoxelStrateManager` is the side oracle: "what strate is at this Z, what params,
|
||||||
oracle: "what strate is at this Z, what params, what generator type, and is there a passage/elevator
|
what generator type, and is there a passage/elevator SDF near here?"
|
||||||
SDF near here?" Integer chunk bounds on each slot are compatibility projections for existing chunk
|
|
||||||
dispatch APIs.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -82,28 +78,23 @@ dispatch APIs.
|
|||||||
Paths relative to `Source/VoxelForge/`. `Public/` = headers, `Private/` = impl.
|
Paths relative to `Source/VoxelForge/`. `Public/` = headers, `Private/` = impl.
|
||||||
|
|
||||||
### 3.0 Human-scale generation contract (2026-10-01)
|
### 3.0 Human-scale generation contract (2026-10-01)
|
||||||
`VOXEL_SIZE` is **12.5 cm** for the legacy generator. Its 32³-voxel chunks span **4 m**. The
|
`VOXEL_SIZE` is **12.5 cm**; 32³ voxels make each chunk **4 m**. The gameplay capsule remains
|
||||||
world-level strate-layout unit is a separate setting with an **8 m default**; a 16-chunk strate is
|
**0.34 m radius / 1.76 m tall** (**2.72 / 14.08 voxels**). The default strate envelope is
|
||||||
therefore **128 m**, and a 64-chunk strate is **512 m**. Core still stores work in 32³-voxel tiles;
|
**16 chunks / 512 voxels / 64 m**; serialized owner strate assets that still say 8 chunks must be
|
||||||
at 25 cm those tiles happen to span 8 m, but they are not the layout unit. The gameplay capsule remains
|
changed to 16. Research-field dimensions are authored in metres and converted at runtime. The live
|
||||||
**0.34 m radius / 1.76 m tall** (**2.72 / 14.08 legacy voxels**). Research-field dimensions are authored
|
strate generator continues to read voxel-valued parameters from its assets, so those authored
|
||||||
in metres and converted at runtime. The live
|
dimensions become half as large in metres unless their asset values are doubled.
|
||||||
strate generator continues to read voxel-valued shape parameters from its assets, so those authored
|
|
||||||
dimensions become half as large in metres unless their asset values are doubled. U1 adds a
|
|
||||||
versioned in-memory conversion for strate height, transition width, and inter-strate gap only; the
|
|
||||||
broader Research8 voxel/chunk/centimetre inventory is not yet migrated.
|
|
||||||
|
|
||||||
The intended metre-scale families remain: room diameters **8-20 m**, origin hub **24 m**, room cells
|
The intended metre-scale families remain: room diameters **8-20 m**, origin hub **24 m**, room cells
|
||||||
**32 m**, round tunnel bores **3-4 m**, maze cells **16 m** with a **6.25 m nominal bore** (**3.125 m**
|
**32 m**, round tunnel bores **3-4 m**, maze cells **16 m** with a **6.25 m nominal bore** (**3.125 m**
|
||||||
radius; **3.0625 m** worst-case usable floor after roughness), slab open span **22.4 m**, shaft
|
radius; **3.0625 m** worst-case usable floor after roughness), slab open span **22.4 m**, shaft
|
||||||
diameters **4-7 m**, island diameters **12-24 m**, and ordinary inter-strate bores **3-4 m**. Source
|
diameters **4-7 m**, island diameters **12-24 m**, and ordinary inter-strate bores **3-4 m**. Source
|
||||||
new fixed physical dimensions are authored in metres and converted to voxel counts at the boundary.
|
new fixed physical dimensions are authored in metres and converted to voxel counts at the boundary.
|
||||||
The owner's voxel-valued live strate shape settings are not migrated by U1: unchanged saved values
|
The owner's voxel-valued live strate assets are not migrated by C++: unchanged saved values therefore
|
||||||
therefore produce half-size rooms, tunnels, features and noise periods in metres. See the U1 report
|
produce half-size rooms, tunnels, features and noise periods in metres. See the step-3 migration list
|
||||||
for the conversion coverage and remaining Research8 fields.
|
in the handoff report for the exact `DA_Settings` fields and values.
|
||||||
|
|
||||||
**Prior reauthoring recommendation (not applied in U1; no packages were edited):** preserve
|
**Owner asset migration (no packages were edited):** preserve `DA_Settings` distances by changing
|
||||||
`DA_Settings` distances by changing
|
|
||||||
`ViewDistanceXY 32→64`, `ViewDistanceUp/Down 10→20`, `PawnCollisionRadiusChunks 2→4`,
|
`ViewDistanceXY 32→64`, `ViewDistanceUp/Down 10→20`, `PawnCollisionRadiusChunks 2→4`,
|
||||||
`StrateViewMarginChunks 6→12`, `ClipRadius 6→12`, `RenderDistanceChunks 1536→3072`,
|
`StrateViewMarginChunks 6→12`, `ClipRadius 6→12`, `RenderDistanceChunks 1536→3072`,
|
||||||
`EdgeSealThickness 128→256 voxels`, decoration radius/near radius `12/6→24/12 chunks`, region
|
`EdgeSealThickness 128→256 voxels`, decoration radius/near radius `12/6→24/12 chunks`, region
|
||||||
@@ -113,9 +104,8 @@ size `8→16 cells`, spacing/far spacing `8→16 voxels`, march step `4→8 voxe
|
|||||||
`12→24 cells`; it is currently disabled. Keep `MaxClipLevel=6` and zero-valued disabled/unbounded
|
`12→24 cells`; it is currently disabled. Keep `MaxClipLevel=6` and zero-valued disabled/unbounded
|
||||||
limits unchanged; double finite world radii and gaps, and multiply finite voxel-volume caps by 8.
|
limits unchanged; double finite world radii and gaps, and multiply finite voxel-volume caps by 8.
|
||||||
The loaded `Crystal Cave` slot in `DA_Strate3` is 64 chunks tall; use 128 chunks to preserve its
|
The loaded `Crystal Cave` slot in `DA_Strate3` is 64 chunks tall; use 128 chunks to preserve its
|
||||||
former 512 m span. U1 automatically converts only strate height and transition widths and the
|
former 512 m span. Its voxel-authored live-generator lengths remain half-size until that asset is
|
||||||
settings gap at load; the remaining recommendations above are pending. Its voxel-authored
|
re-authored; the editor-only research field is the step-3 target.
|
||||||
live-generator lengths remain half-size until their asset fields are migrated.
|
|
||||||
|
|
||||||
Step-3 six-worker scale benchmark (idle machine, `Saved/Research/step3/scale/step3_scale_cacheperf_v1`):
|
Step-3 six-worker scale benchmark (idle machine, `Saved/Research/step3/scale/step3_scale_cacheperf_v1`):
|
||||||
the 519³ field took **7.133834–7.825292 s** across three runs (median **7.347496 s**); the 259³
|
the 519³ field took **7.133834–7.825292 s** across three runs (median **7.347496 s**); the 259³
|
||||||
@@ -173,17 +163,13 @@ remains `0`, and lateral regions remain gated off.
|
|||||||
| `../../Standalone/Core/src/CoreNamespacePreamble.h` | First include of every Core TU. Forces FP contraction off per compiler (Clang/MSVC/GCC pragmas; unknown compiler = `#error`) and defines `vfcore::fp_contract::selfTest`, which `World::compile` runs before building: a fused multiply-add (e.g. Clang `-ffp-contract=fast`) refuses world generation. The world depends on contraction (Linux2/Linux3 reports), so never remove either. |
|
| `../../Standalone/Core/src/CoreNamespacePreamble.h` | First include of every Core TU. Forces FP contraction off per compiler (Clang/MSVC/GCC pragmas; unknown compiler = `#error`) and defines `vfcore::fp_contract::selfTest`, which `World::compile` runs before building: a fused multiply-add (e.g. Clang `-ffp-contract=fast`) refuses world generation. The world depends on contraction (Linux2/Linux3 reports), so never remove either. |
|
||||||
| `../../VoxelForgeCore/Private/ModuleEntry/` | Unreal module entry and an isolated `CoreMinimal.h` shim for the actual engine header. |
|
| `../../VoxelForgeCore/Private/ModuleEntry/` | Unreal module entry and an isolated `CoreMinimal.h` shim for the actual engine header. |
|
||||||
| `../../Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` | Standard-library-only public Core API. `VOXELFORGECORE_API` exports the API from the Unreal module and is empty for standalone builds. |
|
| `../../Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` | Standard-library-only public Core API. `VOXELFORGECORE_API` exports the API from the Unreal module and is empty for standalone builds. |
|
||||||
| `../../Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` / `../../Standalone/Core/src/ResolvedLayoutInternal.h` | `ResolvedLayoutSnapshotV3`: physical layout-chunk width, versioned micrometre intervals, owned ceiling/content/bedrock bands, honored transition settings, deterministic per-column transition bands, recipe anchors, director-planned passages, canonical JSON/FNV identity, compatibility/strict validators, and negative-control audit. Built once from the director-resolved plan and consumed by bounds planning, Core dispatch, composition, and validation. §8.40–8.42, §8.46. |
|
|
||||||
| `ResolvedLayoutStratePassageCapabilitiesV1::{hasNativeCaveField,hasNativeLatticeField}`, `IsNativeLatticeFieldStratum`, `HasNativeGraphField` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` / `Standalone/Core/src/ResolvedLayoutInternal.h` | Explicit per-strate native graph capability; `Core.Lattice.PrimitiveField` is restricted to Maze/VerticalShafts recipe version 1. Lattice seeds include world seed, strate, recipe version and stable feature ID; explicit polyline records keep their endpoints and receive conservative bounds. §8.54. |
|
|
||||||
| `FCavernRuntimeFeature::FSegmentNode`, `ApplyResolvedNativeCavesField`, `ClassifyNativeCaveFieldBox` | `Standalone/Core/src/ShapeWorldInternal.h` | A deterministic nested segment BVH accelerates native graph distance and conservative interval evaluation; the outer graph BVH still supplies tile candidates and protects `isTileShapeFree`. The 0.4 m feature-union width is separate from the floor fillet width. §8.54. |
|
|
||||||
| `../../Standalone/Core/src/FloatingIslandsInternal.h` | Canonical standalone/Core floating-island planner, geometry-versioned field, manifest serializer, influence bounds, capsule-route checks, and thickness audit. |
|
| `../../Standalone/Core/src/FloatingIslandsInternal.h` | Canonical standalone/Core floating-island planner, geometry-versioned field, manifest serializer, influence bounds, capsule-route checks, and thickness audit. |
|
||||||
| `ApplyOwnerFourStratePlan` | `Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp` | BuildHost-only transient fixed-slot plan from the four staged owner strate assets; leaves the authored settings and strate packages untouched. |
|
| `Tools/VoxelForgeTest.ps1` | The single guarded test entry point. `-Scenario canonical|owner|probe|perf|tests|surface-fall|crossing|gate-stress|visual-edit|visual-lod|visual-gap-walk|visual-gap-inject|dig-motion|parity|horizon|horizon-render|determinism|region-eval|step2d|step2e|step2f|step2f-capture|step3-scale|step3-game`, optional `-Build`, `-Assets owner|default`, `-Cvars`, absolute `-Out`, `-Label`, `-HorizonLevel`, `-HorizonRenderDistanceChunks`, `-Step2fProfile=0..7`, `-Step2fCaptureView=inside|wall-closeup`, `-ParityExportSize=32|64|128`, `-ParityExportTile=x,y,z`, `-SharedDDC`, `-SampleDensityStacks`, `-StackSampleIntervalUs`, and `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH`; stages the host/assets, isolates user/DDC/config state, serializes one Unreal launch at a time, enforces the 8 GiB/30-minute guards, waits for 6 GiB free RAM before launches, and emits compact `result.json`/`summary.txt` plus full logs. Parity can render its game leg with `-EnableRender` and compares the selected export region against the exact streamed tile hashes. `visual-gap-walk` records camera-frustum surface-point coverage at 6000 cm/s; `visual-gap-inject` proves an early component hide is detected; `dig-motion` logs per-frame vertical velocity, gate, support readiness, and movement mode. `-Cvars @{ 'voxel.TestGapAuditLegacyHandoff'=1 }` replays the old visibility handoff for A/B audit baselines. Horizon traces report desired/loaded tiles, resident stream geometry, worker seconds, steady wall time, and peak private memory; `horizon-render` captures the owner view; `step2f-capture` selects one of eight parameter sets, finds a positive-density camera point and aims at a sampled rock wall for an inside or wall-closeup view, then saves it under `Saved/Research/step2f/captures` (pass the persistent render DDC with `-SharedDDC`); `determinism` compares per-tile hashes across the default CRT path and `-voxel.CrtFma3=0`; `region-eval` compares the live path with the default-off editor chunk program on the 259³ lattice and five-seed probe set; `step2d` retains the earlier independent field; `step2e` retains its earlier benchmark; `step2f` compares frozen step 2e and the coherent field for all eight profiles on the same 259³ ring, applies geometry gates, and checks sound bounds, seams, and 1-vs-6-worker hashes. `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH` checks a full-ring hash at another partition count. `-SampleDensityStacks` captures profiles, and `-StackSampleIntervalUs` sets the sampling interval.
|
||||||
| `Tools/VoxelForgeTest.ps1` | The single guarded test entry point. `-Scenario canonical|owner|probe|perf|tests|surface-fall|crossing|gate-stress|visual-edit|visual-lod|visual-gap-walk|visual-gap-inject|dig-motion|parity|horizon|horizon-render|determinism|region-eval|standalone-export|compile-only|step2d|step2e|step2f|step2f-capture|step3-scale|step3-game`, optional `-Build`, `-Assets owner|default`, `-Cvars`, absolute `-Out`, `-Label`, `-HorizonLevel`, `-HorizonRenderDistanceChunks`, `-Step2fProfile=0..7`, `-Step2fCaptureView=inside|wall-closeup`, `-ParityExportSize=32|64|128`, `-ParityExportTile=x,y,z`, `-SharedDDC`, `-SampleDensityStacks`, `-StackSampleIntervalUs`, and `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH`; stages the host/assets, isolates user/DDC/config state, allows the owner's interactive editor while refusing overlapping `UnrealEditor-Cmd` launches, runs owned processes at Normal priority with a 12-logical-CPU cap, enforces the 8 GiB/30-minute guards, waits for 6 GiB free RAM before launches, and emits compact `result.json`/`summary.txt` plus full logs. Parity can render its game leg with `-EnableRender` and compares the selected export region against the exact streamed tile hashes. `visual-gap-walk` records camera-frustum surface-point coverage at 6000 cm/s; `visual-gap-inject` proves an early component hide is detected; `dig-motion` logs per-frame vertical velocity, gate, support readiness, and movement mode. `-Cvars @{ 'voxel.TestGapAuditLegacyHandoff'=1 }` replays the old visibility handoff for A/B audit baselines. Horizon traces report desired/loaded tiles, resident stream geometry, worker seconds, steady wall time, and peak private memory; `horizon-render` captures the owner view; `step2f-capture` selects one of eight parameter sets, finds a positive-density camera point and aims at a sampled rock wall for an inside or wall-closeup view, then saves it under `Saved/Research/step2f/captures` (pass the persistent render DDC with `-SharedDDC`); `determinism` compares per-tile hashes across the default CRT path and `-voxel.CrtFma3=0`; `region-eval` compares the live path with the default-off editor chunk program on the 259³ lattice and five-seed probe set; `step2d` retains the earlier independent field; `step2e` retains its earlier benchmark; `step2f` compares frozen step 2e and the coherent field for all eight profiles on the same 259³ ring, applies geometry gates, and checks sound bounds, seams, and 1-vs-6-worker hashes. `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH` checks a full-ring hash at another partition count. `-SampleDensityStacks` captures profiles, and `-StackSampleIntervalUs` sets the sampling interval.
|
|
||||||
| Step-3 scale and game validation | `step3-scale` measures the same 519³ and 259³ six-worker lattices at 12.5 cm. The cached medians are 7.347496 s and 1.169567 s respectively; determinism and brick sound-proof audits pass. `step3-game` routes in-game generation through the brick evaluator and records tile timings, spawn fill, moving frames, and profile-switch rebuilds. The visible D3D12 run measured tile generation p50/p95/max 3.326/31.045/169.867 ms, full-detail L0 fill 12.369 s for 2,765 tiles, and moving-frame p50/p95/p99/max 4.856/8.922/13.090/80.575 ms. Switching profiles rebuilt 5,815 loaded tiles. Both high-resolution screenshot requests were logged, but the harness found only one fresh PNG and it is a blank pale viewport; no lit comparison capture is accepted. Raw output: `Saved/TestHarness/step3_game_visible_final`; scale output: `Saved/Research/step3/scale/step3_scale_cacheperf_v1`. |
|
| Step-3 scale and game validation | `step3-scale` measures the same 519³ and 259³ six-worker lattices at 12.5 cm. The cached medians are 7.347496 s and 1.169567 s respectively; determinism and brick sound-proof audits pass. `step3-game` routes in-game generation through the brick evaluator and records tile timings, spawn fill, moving frames, and profile-switch rebuilds. The visible D3D12 run measured tile generation p50/p95/max 3.326/31.045/169.867 ms, full-detail L0 fill 12.369 s for 2,765 tiles, and moving-frame p50/p95/p99/max 4.856/8.922/13.090/80.575 ms. Switching profiles rebuilt 5,815 loaded tiles. Both high-resolution screenshot requests were logged, but the harness found only one fresh PNG and it is a blank pale viewport; no lit comparison capture is accepted. Raw output: `Saved/TestHarness/step3_game_visible_final`; scale output: `Saved/Research/step3/scale/step3_scale_cacheperf_v1`. |
|
||||||
| `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`; schema-v1 owner-asset export check and BuildHost Core actor tests. `-coreworldassembly=PATH` supplies a reviewed manifest transiently to `-coreworldtest=1`; it does not save a data asset. `FStandaloneCoreAdapterScheduler` runs Core work at 1/8/16 workers; `VerifyStandaloneCoreAdapter` checks hashes and walkability. Existing bounded render/`VF_` walk/OBJ orchestration and research options remain editor/commandlet-only and are not linked by Game/Shipping. |
|
| `Source/VoxelForgeEditor/VoxelForgeEditor.Build.cs` / `Private/VoxelForgeExploreCommandlet.*` | Editor/commandlet-only `-run=VoxelForgeExplore`; schema-v1 owner-asset export check and BuildHost Core actor tests. `-coreworldassembly=PATH` supplies a reviewed manifest transiently to `-coreworldtest=1`; it does not save a data asset. `FStandaloneCoreAdapterScheduler` runs Core work at 1/8/16 workers; `VerifyStandaloneCoreAdapter` checks hashes and walkability. Existing bounded render/`VF_` walk/OBJ orchestration and research options remain editor/commandlet-only and are not linked by Game/Shipping. |
|
||||||
| `Source/VoxelForge/Public/VoxelSettings.h` / `Private/VoxelSettings.cpp` | `UVoxelSettings` owns optional canonical world-assembly JSON and its Core digest. The editor file picker plus `ImportWorldAssemblyManifestJson` imports the reviewed manifest into the cooked settings asset and validates the recipe seed against the effective settings seed. |
|
| `Source/VoxelForge/Public/VoxelSettings.h` / `Private/VoxelSettings.cpp` | `UVoxelSettings` owns optional canonical world-assembly JSON and its Core digest. The editor file picker plus `ImportWorldAssemblyManifestJson` imports the reviewed manifest into the cooked settings asset and validates the recipe seed against the effective settings seed. |
|
||||||
| `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp` | Resolves Core's common `LayoutChunkWidthSubvoxels` from the manager's physical layout width; builds SurfaceWorld/FloatingIslands bounds, L29 bands and passage settings from resolved micrometre slot fields; keeps owner/Core ratios in the legacy FloatingIslands parameter adapter. Chunk fields validate imported plans as compatibility projections. Exports canonical `resolved_layout` separately from `world_assembly` and its hash. Invalid plans fail Core initialization. |
|
| `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp` | Validates the optional manifest against each resolved `SurfaceWorld` slot (unique strate index and exact top/bottom chunks), then places canonical `world_assembly` JSON and its hash in the Core snapshot. Invalid plans fail Core initialization. |
|
||||||
| `Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.*` | Six-worker lattice drivers for step 2c through step 2f. The original path still compares live `GetDensityAt` with the editor chunk program. Research modes assign contiguous batches dynamically so surface-heavy work balances across workers; steps 2e and 2f use 4³ proof bricks and stage each evaluated brick before copying contiguous X rows, while step 2d retains 8³. Each skip requires a macro Lipschitz bound plus the complete final-detail supremum; uncertain bricks retain every 12.5 cm detail sample. Timings split classification, point evaluation, and uniform fill; step 2f compares all profiles and hashes workers 1 and 6. |
|
| `Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.*` | Six-worker lattice drivers for step 2c through step 2f. The original path still compares live `GetDensityAt` with the editor chunk program. Research modes assign contiguous batches dynamically so surface-heavy work balances across workers; steps 2e and 2f use 4³ proof bricks and stage each evaluated brick before copying contiguous X rows, while step 2d retains 8³. Each skip requires a macro Lipschitz bound plus the complete final-detail supremum; uncertain bricks retain every 12.5 cm detail sample. Timings split classification, point evaluation, and uniform fill; step 2f compares all profiles and hashes workers 1 and 6. |
|
||||||
| `Public/VoxelRedefinedDensityField.h` / `Private/VoxelRedefinedDensityField.cpp` | Two default-off `WITH_EDITOR` research fields, selected by `voxel.ResearchRedefinedDensityField` (step 2d) and `voxel.ResearchParameterizedCave` (steps 2e/2f). Step 2f retains a metre-scaled 2e baseline for its rejection audit. New profiles use strongly jittered, individually rotated elliptical rooms capped against nearest neighbors and curved cubic-Bezier passages with deterministic centerline and width variation along each edge. Room heights and feature rolls vary by cell; ledges, occasional pillars, stalactites/stalagmites, basin floors, and bounded hanging masses remain features. Point and brick detail sampling share independently rotated 96/48/24-sample bands and a smooth ridge response, preserving 12/6/3 m wavelengths at 12.5 cm per sample. The parameterized mesher evaluates the exact field through prepared 4³ bricks and the same sound skip proof in commandlet and editor-game meshing. In-editor and in-game profile changes invalidate and rebuild all loaded tiles. In an editor game with this field enabled, sealed-solid, out-of-layout, and outer-classifier verdicts are declined without changing their console switches. |
|
| `Public/VoxelRedefinedDensityField.h` / `Private/VoxelRedefinedDensityField.cpp` | Two default-off `WITH_EDITOR` research fields, selected by `voxel.ResearchRedefinedDensityField` (step 2d) and `voxel.ResearchParameterizedCave` (steps 2e/2f). Step 2f retains a metre-scaled 2e baseline for its rejection audit. New profiles use strongly jittered, individually rotated elliptical rooms capped against nearest neighbors and curved cubic-Bezier passages with deterministic centerline and width variation along each edge. Room heights and feature rolls vary by cell; ledges, occasional pillars, stalactites/stalagmites, basin floors, and bounded hanging masses remain features. Point and brick detail sampling share independently rotated 96/48/24-sample bands and a smooth ridge response, preserving 12/6/3 m wavelengths at 12.5 cm per sample. The parameterized mesher evaluates the exact field through prepared 4³ bricks and the same sound skip proof in commandlet and editor-game meshing. In-editor and in-game profile changes invalidate and rebuild all loaded tiles. In an editor game with this field enabled, sealed-solid, out-of-layout, and outer-classifier verdicts are declined without changing their console switches. |
|
||||||
| `Private/Tests/VoxelForgeParameterizedGenerationTest.cpp` | Separate three-seed functional sweep for the step 2f parameterized field; passage landing air/floor brackets and arrival-to-exit walking routes are checked without changing the live-world tests. |
|
| `Private/Tests/VoxelForgeParameterizedGenerationTest.cpp` | Separate three-seed functional sweep for the step 2f parameterized field; passage landing air/floor brackets and arrival-to-exit walking routes are checked without changing the live-world tests. |
|
||||||
@@ -195,7 +181,7 @@ remains `0`, and lateral regions remain gated off.
|
|||||||
| Symbol | Line | Notes |
|
| Symbol | Line | Notes |
|
||||||
|--------|------|-------|
|
|--------|------|-------|
|
||||||
| `CHUNK_SIZE` (32) | 211 | Chunk dimensions. 64³ would cut draw calls but makes 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` (12.5f cm) | 213 | Legacy generator scale; `CHUNK_SIZE` 32 spans 4 m here. Keep separate from the 8 m default layout chunk and 32³ Core tile. |
|
| `VOXEL_SIZE` (12.5f cm) | 213 | World scale; one 32³ chunk is 4 m. |
|
||||||
| `EVoxelFace` enum | 266 | 6 cube faces. |
|
| `EVoxelFace` enum | 266 | 6 cube faces. |
|
||||||
| `WorldToChunkCoord` | 287 | World cm → chunk coord (handles negatives via floor). |
|
| `WorldToChunkCoord` | 287 | World cm → chunk coord (handles negatives via floor). |
|
||||||
| `VoxelMath::IsFiniteFast` / `IsFinite` | 24 / 45 | Exact IEEE bit test (`voxel.FastIsFinite` A/B switch). |
|
| `VoxelMath::IsFiniteFast` / `IsFinite` | 24 / 45 | Exact IEEE bit test (`voxel.FastIsFinite` A/B switch). |
|
||||||
@@ -222,16 +208,14 @@ all eight archetypes; there is no other path. See [Docs/archive/OPSTACK-PLAN.md]
|
|||||||
| `FVoxelBoxHypotheses` + `FVoxelBoxSdfInterval` + `VF_ForceHypotheses` / `VF_FoldEffect` / `VF_FoldOp` | The fold that turns a stack into an `EVoxelTileClass`, carrying the SDF interval between independent writers and consumers. Unknown is biased toward `Mixed`; the mapping and soundness rule are written out in the header. |
|
| `FVoxelBoxHypotheses` + `FVoxelBoxSdfInterval` + `VF_ForceHypotheses` / `VF_FoldEffect` / `VF_FoldOp` | The fold that turns a stack into an `EVoxelTileClass`, carrying the SDF interval between independent writers and consumers. Unknown is biased toward `Mixed`; the mapping and soundness rule are written out in the header. |
|
||||||
|
|
||||||
### 3.2c Structural primitives — `Public/VoxelDensityPrimitives.h`
|
### 3.2c Structural primitives — `Public/VoxelDensityPrimitives.h`
|
||||||
`VF_ApplyOriginSpine` · `VF_ApplyBoundarySeal` · `VF_ApplyUpperBoundarySeal` · `VF_ApplyPassageCarving` · `VF_ApplyXYEdgeSeal` —
|
`VF_ApplyOriginSpine` · `VF_ApplyBoundarySeal` · `VF_ApplyPassageCarving` · `VF_ApplyXYEdgeSeal` —
|
||||||
the four world invariants every archetype appends, with landing/floor geometry joined by the
|
the four world invariants every archetype appends, with landing/floor geometry joined by the
|
||||||
manager passage post, **moved here 2026-07-27** so the generator and
|
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 `ApplyBoundarySeal` and `ApplyOriginSpine`; the passage and
|
`static FORCEINLINE` forwarders for `ApplyBoundarySeal` and `ApplyOriginSpine`; the passage and
|
||||||
edge helpers are called directly. `VF_ApplyUpperBoundarySealValue` contains the common upper-band
|
edge helpers are called directly. **Convention: INTERNAL (positive = solid); `VF_ApplyXYEdgeSealMC` is the output wrapper.**
|
||||||
math used by both one-sided and two-sided seals. **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`
|
||||||
⚠️ **This IS the game's generation path.** `GetDensityAt` builds the stack in its per-chunk refetch
|
⚠️ **This IS the game's generation path.** `GetDensityAt` builds the stack in its per-chunk refetch
|
||||||
@@ -388,7 +372,7 @@ casse cette arithmetique *structurellement* — `BeginPlay` loggue une **Error**
|
|||||||
| `AdvanceHeadlessDigMotionTest` / `RecordHeadlessDigMotionFrame` | 3060 / 3217 | Explicit `-voxel.TestDigMotion=1` repro: optional near digs, then repeats underfoot digs and samples `Generator->GetDensityAt` immediately after each edit, on its first subsequent simulated frame, and on every following observation frame. `-voxel.TestDigMotionFieldOverride=1` is a measurement-only prototype that clears `CurrentFloor` and switches CMC to falling when the sampled post-edit density is air; it is inactive by default. The test counts false support, false air and downward velocity beyond one gravity step. `-voxel.TestDigMotionUnderfootDigCount` is clamped to 1–100; `-voxel.TestDigMotionUnderfootIntervalSeconds=0` performs per-frame rapid edits. The accompanying gap audit is scoped to the dig session. |
|
| `AdvanceHeadlessDigMotionTest` / `RecordHeadlessDigMotionFrame` | 3060 / 3217 | Explicit `-voxel.TestDigMotion=1` repro: optional near digs, then repeats underfoot digs and samples `Generator->GetDensityAt` immediately after each edit, on its first subsequent simulated frame, and on every following observation frame. `-voxel.TestDigMotionFieldOverride=1` is a measurement-only prototype that clears `CurrentFloor` and switches CMC to falling when the sampled post-edit density is air; it is inactive by default. The test counts false support, false air and downward velocity beyond one gravity step. `-voxel.TestDigMotionUnderfootDigCount` is clamped to 1–100; `-voxel.TestDigMotionUnderfootIntervalSeconds=0` performs per-frame rapid edits. The accompanying gap audit is scoped to the dig session. |
|
||||||
| `MaybeFinishHeadlessStreamingTest` | — | The existing `voxel.TestExitSeconds` diagnostic emits `VoxelForgeStreamingExitDiag` at most every 30 elapsed seconds, including threshold and early-return test flags; no output when the timer is unconfigured. |
|
| `MaybeFinishHeadlessStreamingTest` | — | The existing `voxel.TestExitSeconds` diagnostic emits `VoxelForgeStreamingExitDiag` at most every 30 elapsed seconds, including threshold and early-return test flags; no output when the timer is unconfigured. |
|
||||||
| `UpdateVisualStalenessAudit` | 5034 | Test-only camera-frustum coverage audit: tracks up to 24 real pre-skirt isosurface points per visible component, then counts frames where no visible tile component covers a previously visible, still-desired point (excluding edited bounds). `voxel.TestVisualGapInjectEarlyHide=1` hides a valid component for three frames and must be caught. `voxel.TestGapAuditLegacyHandoff=1` restores old handoff culling for A/B baselines. |
|
| `UpdateVisualStalenessAudit` | 5034 | Test-only camera-frustum coverage audit: tracks up to 24 real pre-skirt isosurface points per visible component, then counts frames where no visible tile component covers a previously visible, still-desired point (excluding edited bounds). `voxel.TestVisualGapInjectEarlyHide=1` hides a valid component for three frames and must be caught. `voxel.TestGapAuditLegacyHandoff=1` restores old handoff culling for A/B baselines. |
|
||||||
| `ProcessPendingChunks` | 7435 | Drains `CriticalProcessQueue`, `EditedProcessQueue`, `NearProcessQueue`, then `ProcessQueue`; applies under the authored 4-mesh/2 ms budget during normal play and the default-on bounded startup budget (24 meshes/14 ms, at most 30 s) until the initial desired set and walkable ring are ready. `voxel.StreamingStartupApplyBoost=0` restores the authored budget. `startup_apply_config` traces the actor-read CVar and both budgets; `startup_apply_budget` records activation/end. Reads per-request cancellation before removing its map entry, traces stale/obsolete/aborted discards with epoch and ordinal, then applies via `ApplyTileResult`. Step 1 prequeue still releases here and emits `VFStep1UploadCorpusReleased`. |
|
| `ProcessPendingChunks` | 7435 | Drains `CriticalProcessQueue`, `EditedProcessQueue`, `NearProcessQueue`, then `ProcessQueue`; applies under the authored 4-mesh/2 ms budget during normal play and the bounded shared startup budget (12 meshes/14 ms) until the initial desired set and walkable ring are ready. `voxel.StreamingStartupApplyBoost=0` restores the authored budget. `startup_apply_config` traces the actor-read CVar and both budgets; `startup_apply_budget` records activation/end. Reads per-request cancellation before removing its map entry, traces stale/obsolete/aborted discards with epoch and ordinal, then applies via `ApplyTileResult`. Step 1 prequeue still releases here and emits `VFStep1UploadCorpusReleased`. |
|
||||||
| `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). |
|
||||||
@@ -396,7 +380,7 @@ casse cette arithmetique *structurellement* — `BeginPlay` loggue une **Error**
|
|||||||
| `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. Uses `PreEditDensityCache` when the tile is resident in the bounded local cache, replays the current DiffLayer over that exact procedural lattice, then meshes/applies inline so a carve is visible THIS frame. A cache miss uses normal generation; it captures the unedited lattice if the tile needs meshing, while uniform-tile proofs remain enabled. |
|
| `SyncRemeshTile` | — | **INSTANT DIG**: level-0 same-frame re-mesh on the game thread. Uses `PreEditDensityCache` when the tile is resident in the bounded local cache, replays the current DiffLayer over that exact procedural lattice, then meshes/applies inline so a carve is visible THIS frame. A cache miss uses normal generation; it captures the unedited lattice if the tile needs meshing, while uniform-tile proofs remain enabled. |
|
||||||
| `PreEditDensityCache` / `VoxelForgePreEditDensityCache::BuildEditedGrid` | — | Per-world cache of the pawn's current L0 tile: exact 35³ pre-edit samples, max **171,500 bytes**. It is pruned when the pawn crosses chunk boundaries and cleared on `RegenerateAllChunks`. `BuildEditedGrid` snapshots touched diff chunks once and reproduces `GetDensityAt`'s final `pre-edit - EvaluateMods` operation before the per-call shared-grid mesher path. |
|
| `PreEditDensityCache` / `VoxelForgePreEditDensityCache::BuildEditedGrid` | — | Per-world cache of the pawn's current L0 tile: exact 35³ pre-edit samples, max **171,500 bytes**. It is pruned when the pawn crosses chunk boundaries and cleared on `RegenerateAllChunks`. `BuildEditedGrid` snapshots touched diff chunks once and reproduces `GetDensityAt`'s final `pre-edit - EvaluateMods` operation before the per-call shared-grid mesher path. |
|
||||||
| `UpdateChunksAroundPosition` / `GetGenerationAdmissionCount` | 10990 / 10971 | Shared Core/legacy scheduler. Builds the desired set and delta-culls leavers; admits the occupied/support tile plus collision-required LOD0 tiles in the pawn's 3×3 XY ring as `CollisionCritical`. S5 coarse-first admission stays opt-in (`voxel.StreamingCoarseFirst=1`). `voxel.StreamingDynamicNearReserve=1` is the default: it keeps the authored near reserve while moving or until the stationary walkable ring is ready, then makes those slots available to coarse work. Admission counts submitted queued/running tasks, not completed results waiting in the MPSC result queues; `voxel.StreamingLegacyPendingResultCap=1` restores the old `PendingTiles`-based cap for the negative control. `voxel.StreamingFarApplySupplement=1` is the default L121 lane: after the visible-apply cap, at most one far result is admitted when under half the apply-time budget and startup boost is inactive. `PendingTiles` still lasts through game-thread apply for duplicate suppression and result bookkeeping. The clipmap stays non-overlapping and load-before-unload / `TransitionHold` reconciliation remains authoritative. Standalone has no mirror of this actor scheduler. §8.10 + §8.26–8.28. |
|
| `UpdateChunksAroundPosition` / `GetGenerationAdmissionCount` | 10990 / 10971 | Shared Core/legacy scheduler. Builds the desired set and delta-culls leavers; admits the occupied/support tile plus collision-required LOD0 tiles in the pawn's 3×3 XY ring as `CollisionCritical`. S5 coarse-first admission stays opt-in (`voxel.StreamingCoarseFirst=1`). `voxel.StreamingDynamicNearReserve=1` is now the default: it keeps the authored near reserve while moving or until the stationary walkable ring is ready, then makes those slots available to coarse work. Admission counts submitted queued/running tasks, not completed results waiting in the MPSC result queues; `voxel.StreamingLegacyPendingResultCap=1` restores the old `PendingTiles`-based cap for the negative control. `PendingTiles` still lasts through game-thread apply for duplicate suppression and result bookkeeping. The clipmap stays non-overlapping and load-before-unload / `TransitionHold` reconciliation remains authoritative. Standalone has no mirror of this actor scheduler. §8.10 + §8.26–8.28. |
|
||||||
| `AdvanceHeadlessCollisionGateStressTest` / `ObserveHeadlessCollisionGateStressTest` / `MaybeFinishHeadlessCollisionGateStressTest` / `MeasureHeadlessCollisionGateStressTerrain` | — | Explicit `-voxel.TestCollisionGateStress=1` harness only. The normal spawn-height cvar offsets from the measured density surface; `voxel.TestCollisionGateStressDeepDrop=1` carves a process-local box shaft from above the top to just above the floor tile, extending along the post-landing route so the pawn does not run into the shaft sidewall, temporarily raises only the transient test DiffLayer brush-radius allowance for that fixture, removes only the local pawn's redundant streaming anchor for this isolated repro, proves every sample in two complete outside-ring level-0 tiles is air before seeding their known-empty status, prewarms the known-empty in-ring column, and asserts that the support walk reaches the in-layout floor outside the aligned level-0 render ring before the pawn drops. The log reports whether the fixed gate retains that otherwise out-of-ring target. Other streaming anchors remain active. The fixture rejects a target or floor outside the layout, a non-air empty-tile proof, or a floor inside the aligned level-0 ring. The `GetDensityAt` column at capsule feet defines the local air/solid surface; the deep-drop column crossing is cached after its first exact scan. 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; deep-drop gate-on also requires horizontal travel during a hold. Result logs include the terminal density column, first crossing below spawn height, hold count/time, and gated horizontal progress. |
|
| `AdvanceHeadlessCollisionGateStressTest` / `ObserveHeadlessCollisionGateStressTest` / `MaybeFinishHeadlessCollisionGateStressTest` / `MeasureHeadlessCollisionGateStressTerrain` | — | Explicit `-voxel.TestCollisionGateStress=1` harness only. The normal spawn-height cvar offsets from the measured density surface; `voxel.TestCollisionGateStressDeepDrop=1` carves a process-local box shaft from above the top to just above the floor tile, extending along the post-landing route so the pawn does not run into the shaft sidewall, temporarily raises only the transient test DiffLayer brush-radius allowance for that fixture, removes only the local pawn's redundant streaming anchor for this isolated repro, proves every sample in two complete outside-ring level-0 tiles is air before seeding their known-empty status, prewarms the known-empty in-ring column, and asserts that the support walk reaches the in-layout floor outside the aligned level-0 render ring before the pawn drops. The log reports whether the fixed gate retains that otherwise out-of-ring target. Other streaming anchors remain active. The fixture rejects a target or floor outside the layout, a non-air empty-tile proof, or a floor inside the aligned level-0 ring. The `GetDensityAt` column at capsule feet defines the local air/solid surface; the deep-drop column crossing is cached after its first exact scan. 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; deep-drop gate-on also requires horizontal travel during a hold. Result logs include the terminal density column, first crossing below spawn height, hold count/time, and gated horizontal progress. |
|
||||||
| `BuildDesiredTiles` | 6482 | Builds `DesiredSorted`+`DesiredStamped` (player clipmap shells), then adds streaming anchors and the pending player's collision-only support column before the leaver sweep. It orders from the true local pawn position and builds `CriticalDesiredTiles` for the unresolved support target, occupied/support and heading tiles. Also rebuilds `DesiredTransitionDescendants` (ancestor → desired descendants, including face/edge-touching replacements) for the reconcile pass. |
|
| `BuildDesiredTiles` | 6482 | Builds `DesiredSorted`+`DesiredStamped` (player clipmap shells), then adds streaming anchors and the pending player's collision-only support column before the leaver sweep. It orders from the true local pawn position and builds `CriticalDesiredTiles` for the unresolved support target, occupied/support and heading tiles. Also rebuilds `DesiredTransitionDescendants` (ancestor → desired descendants, including face/edge-touching replacements) for the reconcile pass. |
|
||||||
| `CollisionGateDesiredTiles` / `RequestCollisionGateSupportTile` | 5062 | Retains an unresolved support target and up to three level-0 tiles below it across desired-set rebuilds. The additional keys are hidden collision-only tiles, queued ahead of ordinary streaming, and pruned once resolved or outside the active column. |
|
| `CollisionGateDesiredTiles` / `RequestCollisionGateSupportTile` | 5062 | Retains an unresolved support target and up to three level-0 tiles below it across desired-set rebuilds. The additional keys are hidden collision-only tiles, queued ahead of ordinary streaming, and pruned once resolved or outside the active column. |
|
||||||
@@ -607,16 +591,13 @@ content lists, audio, `GameplayTags`(393). EditConditions show/hide param groups
|
|||||||
type.
|
type.
|
||||||
|
|
||||||
**`Public/VoxelStrateManager.h` + `.cpp`** — `UVoxelStrateManager : UObject` (h:152).
|
**`Public/VoxelStrateManager.h` + `.cpp`** — `UVoxelStrateManager : UObject` (h:152).
|
||||||
Resolves each slot's physical metre interval, layout gap, transition width, world seed and passage
|
Maps depth→strate at runtime; owns passages.
|
||||||
settings during `Initialize`, then owns passages. World-Z APIs query the physical intervals; legacy
|
|
||||||
chunk APIs use the cached integer projection.
|
|
||||||
- `FVoxelPassage` (h:46): 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:128): content definition, resolved micrometre bounds/height, resolved transition
|
- `FStrateSlot` (h:128): definition + chunk-Z range + index.
|
||||||
and passage inputs, and compatibility chunk-Z projection + strate index.
|
|
||||||
| Method | .cpp line | Role |
|
| Method | .cpp line | Role |
|
||||||
|--------|-----------|------|
|
|--------|-----------|------|
|
||||||
| `Initialize` | 1186 | 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` | 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. |
|
| `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` | 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. |
|
| `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` | 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. |
|
| `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. |
|
||||||
@@ -624,18 +605,18 @@ chunk APIs use the cached integer projection.
|
|||||||
| `ApplyPassageLandingFloorMC` | 2983 | 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` / `FindSlotIndexForWorldZCentimetres` | 6239 / 6251 | Chunk callers use the cached compatibility projection; world-Z callers search physical half-open micrometre intervals. |
|
| `FindSlotIndexForChunkZ` | 6111 | Z → layout index. |
|
||||||
| `GetStrateAt` / `GetStrateIndex` | 6276 / 6287 | World-Z queries against half-open micrometre intervals. |
|
| `GetStrateAt` / `GetStrateIndex` | 6127 / 6139 | World-Z queries. |
|
||||||
| `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. |
|
| `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` | 6298 | Chunk → definition via the compatibility projection. |
|
| `GetStrateForChunk` | 6150 | Chunk → definition. |
|
||||||
| `GetGeneratorTypeForChunk` | 6324 | Chunk → generator type via the compatibility projection. |
|
| `GetGeneratorTypeForChunk` | 6176 | Chunk → generator type. |
|
||||||
| `UsesOperatorStackForChunk` | 6351 | 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. |
|
| `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` | 6415 | 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` | 6661 | **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) | 6985 | 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,
|
||||||
@@ -1156,202 +1137,6 @@ Implementation and evidence: [`Standalone/Research/WorldAssemblerM3/Linux9/REPOR
|
|||||||
The base manifest field is authoritative for tile generation and exact queries. See ARCHITECTURE
|
The base manifest field is authoritative for tile generation and exact queries. See ARCHITECTURE
|
||||||
§8.36 and [`Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md).
|
§8.36 and [`Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md).
|
||||||
|
|
||||||
### 3.23 Shared strate boundaries and FloatingIslands seal — World Assembler M3 Linux18
|
|
||||||
|
|
||||||
| Symbol | Canonical file | Role |
|
|
||||||
|---|---|---|
|
|
||||||
| `FStandaloneShapeWorld::FResolvedStratumBounds`, `MakeResolvedStratumBounds`, `GetResolvedStratumBounds` | `Standalone/Core/src/ShapeWorldInternal.h` | Resolves each strate's physical lower/upper range and seal width for layer ranges, far-field sample protection, and far-shell continuity. Native FloatingIslands uses its plan's owner/Core ratio; SurfaceWorld retains its `32` Core voxels per slot chunk. |
|
|
||||||
| `VF_ApplyUpperBoundarySeal` / `VF_ApplyUpperBoundarySealValue` | `Standalone/Source/VoxelForge/Public/VoxelDensityPrimitives.h` and `Source/VoxelForge/Public/VoxelDensityPrimitives.h` | Shared one-sided top seal used by version-3 native FloatingIslands; the two-sided seal reuses the same upper-band math. |
|
|
||||||
|
|
||||||
Geometry version 3 removes the FI lower all-XY seal while retaining its ceiling through the shared
|
|
||||||
upper-bound path. Versions 1 and 2 retain their serialized two-sided behavior. See ARCHITECTURE §8.37
|
|
||||||
and [`Standalone/Research/WorldAssemblerM3/Linux18/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux18/REPORT.md).
|
|
||||||
|
|
||||||
### 3.24 Resolved inter-strate passages — World Assembler M3 L23
|
|
||||||
|
|
||||||
| Symbol | Canonical file | Role |
|
|
||||||
|---|---|---|
|
|
||||||
| `PlanResolvedLayoutPassagesV1`, `ResolvedLayoutPassageV1::lowerMouthControlIndex` | `Standalone/Core/src/ResolvedLayoutInternal.h`, public record in `VoxelForgeCore.h` | Plans deterministic fixed-metre passages from adjacent director-resolved strata, binds route departure/arrival mouths, and records the derived split between the monotone descent and route-bound landing tail. |
|
|
||||||
| `FStandaloneShapeWorld::ApplyResolvedStrateComposition`, `ApplyResolvedStratePassageCarves` | `Standalone/Core/src/ShapeWorldInternal.h` | Applies resolved boundary bands first, then opens the passage capsule and supported floor along the passage for every native generator. |
|
|
||||||
| `--audit-resolved-layout-passages` | `Standalone/Source/StreamBenchmarkPipeline.h` | Samples passage capsules, floor support, mouth ownership/radius, in-strate route bindings, and a blocked-passage negative control. |
|
|
||||||
|
|
||||||
The evaluator version is 9. See ARCHITECTURE §8.41 and
|
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux23/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux23/REPORT.md).
|
|
||||||
|
|
||||||
### 3.25 Per-strate natural passages and FloatingIslands anchor helix — World Assembler L24
|
|
||||||
|
|
||||||
| Symbol | Canonical file | Role |
|
|
||||||
|---|---|---|
|
|
||||||
| `FStratePassageConfig::Style`, `EVoxelPassageStyle` | `Source/VoxelForge/Public/VoxelStrateTypes.h` | Selects Straight, Worm, Spiral or Cascading per upper strate; Worm is the default. |
|
|
||||||
| `PlanResolvedLayoutPassagesV1` | `Standalone/Core/src/ResolvedLayoutInternal.h` | Applies the selected style in the shared planner; a configured FI Spiral follows the anchor root into the bedrock and smoothly transitions into the inter-strate route. |
|
|
||||||
| Anchor root recipe fields and `EvaluateIslandSDF` | `Standalone/Core/src/FloatingIslandsInternal.h` | Add the seed-stable ordinal-2 island/root without changing other island placements; geometry version 5. |
|
|
||||||
| `ApplyResolvedStratePassageCarves` | `Standalone/Core/src/ShapeWorldInternal.h` | Applies deterministic rough wall relief and the supported route profile to all native generator fields. |
|
|
||||||
| `--audit-resolved-layout-passages` naturalness checks | `Standalone/Source/StreamBenchmarkPipeline.h` | Checks 8 m maximum flat run, 0.20 m minimum wall relief, capsule/support/grade, mouths, routes and the blocked-passage control. |
|
|
||||||
| `--headlamp=on`, `--clip-plane=NX,NY,NZ,D` | `Standalone/Source/LinuxOffscreenCapture.cpp` | Adds camera-directed light or discards the plane's negative half-space for offscreen cutaways; neither affects Core output. |
|
|
||||||
|
|
||||||
The shared evaluator is version 10, the FI recipe geometry is version 5, and resolved-layout schema
|
|
||||||
V3 is unchanged. See ARCHITECTURE §8.42 and
|
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux24/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux24/REPORT.md).
|
|
||||||
|
|
||||||
### 3.26 Enclosed FloatingIslands helix and open-air support gate — World Assembler L25
|
|
||||||
|
|
||||||
| Symbol | Canonical file | Role |
|
|
||||||
|---|---|---|
|
|
||||||
| `ApplyResolvedStratePassageCarves` | `Standalone/Core/src/ShapeWorldInternal.h` | Emits route-mouth support, tread and fallback support only over pre-passage native solid density, shared by every passage style. |
|
|
||||||
| `auditPassageOpenAir` / `--audit-resolved-layout-passages` | `Standalone/Source/StreamBenchmarkPipeline.h` | Samples the support footprint at 0.25 m spacing and two depths; compares against L24 ungated support as a required negative control. |
|
|
||||||
| Anchor root dimensions and relief | `Standalone/Core/src/FloatingIslandsInternal.h` | Raises the FI anchor root from a 6 m cylinder to a nominal 13.25 m relief-bearing rock column sized around the 3 m helix and 4.5 m mouth. |
|
|
||||||
| Passage route and root measures | `Standalone/Core/src/ResolvedLayoutInternal.h`, `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` | Binds the Spiral to the root and reports measured centreline/root clearance. |
|
|
||||||
|
|
||||||
The shared evaluator is version 12 and FI geometry version is 7; resolved-layout schema V3 is
|
|
||||||
unchanged. See ARCHITECTURE §8.43 and
|
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux25/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux25/REPORT.md).
|
|
||||||
|
|
||||||
### 3.27 Tunnels in native rock with grounded passage bridges — World Assembler L26
|
|
||||||
|
|
||||||
| Symbol | Canonical file | Role |
|
|
||||||
|---|---|---|
|
|
||||||
| `BuildPassageBridgePlan` / `PassageBridgeSignedDistance` | `Standalone/Core/src/ShapeWorldInternal.h` | Uses native density sign to plan cached, connected rock spans only across unsupported route ranges; shared by both recipes and all passage styles. |
|
|
||||||
| `PassageBridgeCache` | `Standalone/Source/OwnerWorldLoader.h` | Shares deterministic ranges by resolved-layout identity across shape instances. |
|
|
||||||
| `ApplyResolvedStratePassageCarves` | `Standalone/Core/src/ShapeWorldInternal.h` | Adds bridge density before carving the bore; gates native floor support on solid input and player-capsule clearance. |
|
|
||||||
| `auditPassageBridges` / `auditPassageOpenAir` / `--audit-resolved-layout-passages` | `Standalone/Source/StreamBenchmarkPipeline.h` | Measures bridge width, thickness, relief and connected ends; checks capsule support, open air, naturalness, route binding and the L24 negative control. |
|
|
||||||
| `PassageBridgeRangeAuditV1` / `passageBridgeRanges()` / `isPassageBridgeSample()` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`, `Standalone/Core/src/VoxelForgeCore.cpp` | Exposes audit-only bridge ranges and density queries to the standalone validator. |
|
|
||||||
|
|
||||||
The shared evaluator is version 13; FloatingIslands geometry remains version 7 and resolved-layout
|
|
||||||
schema V3 is unchanged. See ARCHITECTURE §8.44 and
|
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux26/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux26/REPORT.md).
|
|
||||||
|
|
||||||
### 3.28 Tile-bounded passage evaluation — World Assembler M3 Linux L28
|
|
||||||
|
|
||||||
| Symbol | Canonical file | Role |
|
|
||||||
|---|---|---|
|
|
||||||
| FScopedPassageTileQueries / BuildPassageFeatureTileList / ResolvedPassageIntersectsBox | Standalone/Core/src/ShapeWorldInternal.h; scope entered by GenerateMesh in Standalone/Core/src/VoxelForgeCore.cpp | Builds conservative per-tile route, column and mouth-support candidates from an AABB BVH; long paths use a nearest-segment BVH and L9 box checks use a separate passage-bound BVH. Empty native-only proof indexes fall back conservatively. The density arithmetic and serialized versions stay unchanged. |
|
|
||||||
|
|
||||||
See ARCHITECTURE §8.45 and
|
|
||||||
[Linux28 report](Standalone/Research/WorldAssemblerM3/Linux28/REPORT.md).
|
|
||||||
|
|
||||||
### 3.29 Honored bedrock/ceiling transitions — World Assembler L29
|
|
||||||
|
|
||||||
| Symbol | Canonical file | Role |
|
|
||||||
|---|---|---|
|
|
||||||
| `EvaluateResolvedLayoutTransitionBandsV1`, `AuditResolvedStrateTransitions` | Public Core header / `Standalone/Core/src/ResolvedLayoutInternal.h`; `Standalone/Source/StreamBenchmarkPipeline.h` | Resolve the deterministic mode field from seed, interface and coordinates; audit actual crossing depth, minimum band thickness, passage-excluded holes, and strict negative controls. |
|
|
||||||
| `FStandaloneShapeWorld::ApplyResolvedStrateBoundaryBand` | `Standalone/Core/src/ShapeWorldInternal.h` | Applies Hard, Gradient or Interleaved to both facing bands in the shared Core composition path; tapers locally at passage mouths to preserve landing support. |
|
|
||||||
|
|
||||||
The shared evaluator is version 15. Recipe and resolved-layout schema versions are unchanged.
|
|
||||||
The Unreal adapter now forwards the authored transition type and chunk width; those Unreal edits
|
|
||||||
were not compiled here. See ARCHITECTURE §8.46 and
|
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux29/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux29/REPORT.md).
|
|
||||||
|
|
||||||
### 3.30 SurfaceWorld as an upper strate and floor-profile joins — World Assembler L33
|
|
||||||
|
|
||||||
| Symbol | Canonical file | Role |
|
|
||||||
|---|---|---|
|
|
||||||
| `BuildFiniteStrateRoute` | `Standalone/Source/WorldAssemblyPrototype.h` | Samples route heights from the composed terrain ground; bounds candidate chords by the arrival/departure placement radii and records per-segment grade failures. |
|
|
||||||
| `PlanResolvedLayoutPassagesV1` | `Standalone/Core/src/ResolvedLayoutInternal.h` | Starts an upper-SurfaceWorld departure at its terrain mouth, opens a small portal and smoothsteps to the rock tunnel while orienting the ring outward. |
|
|
||||||
| `BuildPassageVoidCrossingPlan`, `PassageBridgeSignedDistance` | `Standalone/Core/src/ShapeWorldInternal.h` | Sizes unsupported ranges from the full control-segment window only for upper SurfaceWorld; other upper generators keep the previous bracketing-sample bounds. |
|
|
||||||
| `--audit-resolved-layout-passages` | `Standalone/Source/StreamBenchmarkPipeline.h` | Uses floor height rather than bore-centre height for naturalness and records failed-only wall probes for column audits. |
|
|
||||||
| `FloorProfileBlend` | `Standalone/Core/src/ShapeWorldInternal.h` | Measures endpoint height/tangent continuity; chord deviation is informational, with step and tangent-step negative controls. |
|
|
||||||
|
|
||||||
The owner SurfaceWorld hashes match L32 and evaluator/recipe versions are unchanged. See
|
|
||||||
ARCHITECTURE §8.47 and
|
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux33/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux33/REPORT.md).
|
|
||||||
|
|
||||||
### 3.31 Passage-wall ripple and FI tread-shoulder audit — World Assembler M3 L35
|
|
||||||
|
|
||||||
| Symbol | Canonical file | Role |
|
|
||||||
|---|---|---|
|
|
||||||
| `VF_FieldPassageWallRelief` | `Standalone/Core/src/VoxelForgeFieldShared.h` | Supplies passage-only irregular wall relief at rotated 43/29/19 m scales. Other `VF_FieldSurfaceRelief` users stay unchanged. |
|
|
||||||
| `ApplyResolvedStratePassageCarves` | `Standalone/Core/src/ShapeWorldInternal.h` | Catmull–Rom smooths eligible interior sweep joins, applies passage-only wall relief, and tapers/grains the entry support shoulder below the capsule/support probe footprint. |
|
|
||||||
| `PlanResolvedLayoutPassagesV1` | `Standalone/Core/src/ResolvedLayoutInternal.h` | Gives each Cascade turn a seeded shallow radial profile instead of identical repeated full-depth cosine lobes. |
|
|
||||||
| `auditPassageRipples` / `PassageRippleAudit` | `Standalone/Source/StreamBenchmarkPipeline.h` | Samples wall radii and walk-floor zero crossings at 0.125 m spacing, scans periods 0.5–8 m in 32 m windows, and rejects wall/floor amplitudes ≥0.035/0.015 m. Sphere and beaded-floor controls must be detected. |
|
|
||||||
|
|
||||||
`WorldAssemblyEvaluatorVersion=21`. See ARCHITECTURE §8.48 and
|
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md).
|
|
||||||
|
|
||||||
### 3.32 Walk-floor washboard and FI mouth blend — World Assembler M3 L36
|
|
||||||
|
|
||||||
| Symbol | Canonical file | Role |
|
|
||||||
|---|---|---|
|
|
||||||
| `ApplyResolvedStratePassageCarves` | `Standalone/Core/src/ShapeWorldInternal.h` | Extends eligible tread sections by up to 2 m and uses bounded signed-distance support on every sparse route, including SurfaceWorld-to-TunnelNetwork, removes seeded shoulder grain, and seats regular tread tops 5 cm lower with a smooth return at the lower landing. Capsule/open-air gates remain. Gravity-facing wall relief is reduced; side and ceiling relief remain. |
|
|
||||||
| `ApplyResolvedStratePassageCarves` mouth support | `Standalone/Core/src/ShapeWorldInternal.h` | Builds exact signed-distance route-mouth tread boxes and smooth-unions them with native terrain over 1.5 m. |
|
|
||||||
| `NativeFloatingIslandDensityAt` | `Standalone/Core/src/ShapeWorldInternal.h` | Applies a local 2 m symmetric signed-field filter around FI passage mouths, exempts resolved route support, and removes isolated negative samples thinner than 2 m on all axes. |
|
|
||||||
| `auditPassageRipples` / `PassageRippleAudit` | `Standalone/Source/StreamBenchmarkPipeline.h` | Keeps the wall-radius audit and adds walk-floor zero-crossing profiles at 0.125 m, with five 32 m windows and a 0.015 m limit plus a beaded-floor negative control. |
|
|
||||||
| `RunMouthFragmentAuditL36.py`, `ProbeWalkFloorProfileL36.py` | `Standalone/Research/WorldAssemblerM3/Linux36/` | Compare L35 floor and FI-mouth controls with L36; the mouth check fails on detached components at or above 0.0001 m² and checks normal jumps across the fade ring. |
|
|
||||||
|
|
||||||
`WorldAssemblyEvaluatorVersion=23`. See ARCHITECTURE §8.49 and
|
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md).
|
|
||||||
|
|
||||||
### 3.33 Immutable field binding — World Assembler M3 L40
|
|
||||||
|
|
||||||
| Symbol | Canonical file | Role |
|
|
||||||
|---|---|---|
|
|
||||||
| `FSurfaceWorldFieldAdapter`, `FFloatingIslandFieldAdapter`, `FLegacyGeneratorFieldAdapter`, `FGeneratorFieldAdapterV1` | `Standalone/Core/src/ShapeWorldInternal.h` | Concrete, immutable density adapters with one shared `densityAt(x,y,z,out)` call shape; the variant avoids per-sample virtual dispatch. FI retains its fixed-point entry point and prebound legacy boundary parameters. |
|
|
||||||
| `FBoundStratumField`, `BindResolvedStratumFields`, `FindBoundStratumForChunk` | `Standalone/Core/src/ShapeWorldInternal.h` | Bind a resolved strate's plan, evaluator and chunk interval once during world initialization; native field and classification queries select this table. |
|
|
||||||
| `NativeSurfaceDensityAt`, `NativeFloatingIslandDensityAt`, `ClassifyBoxDetailed` | `Standalone/Core/src/ShapeWorldInternal.h` | Consume the prebound adapters and plan pointers instead of looking up strate type/parameters through the manager per query. |
|
|
||||||
|
|
||||||
Hash parity is unchanged from L37/L39; see ARCHITECTURE §8.50 and
|
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux40/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux40/REPORT.md).
|
|
||||||
|
|
||||||
### 3.34 Resolved boundary and landing capabilities — World Assembler M3 L42
|
|
||||||
|
|
||||||
| Symbol | Canonical file | Role |
|
|
||||||
|---|---|---|
|
|
||||||
| `ResolvedLayoutStratePassageCapabilitiesV1`, `ResolvedLayoutInterfaceCapabilitiesV1` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` | Runtime-only resolved records for surface/island landing, passage-column anchoring, the FI Gradient air gap, and SW-to-TunnelNetwork support. They are derived during layout finalization from fields already included in the canonical layout identity. |
|
|
||||||
| `ResolvedLayoutSnapshotV3::finalize` | `Standalone/Core/src/ResolvedLayoutInternal.h` | Resolves those capabilities once before sampling; canonical JSON and stable layout IDs stay unchanged. |
|
|
||||||
| `FStandaloneShapeWorld::ApplyResolvedStrateBoundaryBand`, `ApplyResolvedStratePassageCarves`, `BuildPassageVoidCrossingPlan`, `PassageBridgeSignedDistance` | `Standalone/Core/src/ShapeWorldInternal.h` | Consume resolved capability records. Generator-kind selection remains only in `BindResolvedStratumFields`, where it binds the concrete evaluator once. |
|
|
||||||
|
|
||||||
Owner hashes match L41; see ARCHITECTURE §8.52 and
|
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux42/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux42/REPORT.md).
|
|
||||||
|
|
||||||
### 3.35 Core landmark SDF stamps — World Assembler M3 L43
|
|
||||||
|
|
||||||
| Symbol | Canonical file | Role |
|
|
||||||
|---|---|---|
|
|
||||||
| `ResolvedLayoutStampRequestV1`, `ResolvedLayoutStampV1`, `ResolvedSdfStampKindV1` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` | Version-1 bounded stamp vocabulary and integer-micrometre request/resolved records shared by all Core generators. |
|
|
||||||
| `ResolveResolvedLayoutStampsV1`, `AuditResolvedLayoutStampPlacementV1` | `Standalone/Core/src/ResolvedLayoutInternal.h` | Seed/strate/stable-ID placement with bounded retries; rejects passage-mouth, walk-corridor, content-face, ceiling and bedrock conflicts. Empty requests preserve the old canonical layout payload. |
|
|
||||||
| `BuildStampSpatialIndex`, `ApplyResolvedSdfStamps`, `StampSdf` | `Standalone/Core/src/ShapeWorldInternal.h` | BVH-built conservative bounds select stamps once per tile; shared density composition applies smooth carve/fill SDFs with seeded value-noise relief. Box classification and native-only readiness decline boxes touched by a stamp. |
|
|
||||||
| `--landmark-chamber-demo` | `Standalone/Source/StreamBenchmarkHeadless.cpp` | Opt-in owner-world recipe: one SW ellipsoid chamber, arched entrance and low fill dais, placed beside the passage arrival. Off by default. |
|
|
||||||
|
|
||||||
See ARCHITECTURE §8.53 and
|
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux43/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux43/REPORT.md)
|
|
||||||
for layout identities, owner hashes, outside-box timing, audits and captures.
|
|
||||||
|
|
||||||
### 3.36 Native lattice field — World Assembler M3 R26 slice 1 (Linux L58)
|
|
||||||
|
|
||||||
| Symbol | Canonical file | Role |
|
|
||||||
|---|---|---|
|
|
||||||
| `IsNativeLatticeFieldStratum`, `LayoutLatticeFeatureSeed`, `ResolvedCavernRoomV1::unionWidthMicrometres`, `ResolvedCavernEdgeV1::unionWidthMicrometres` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`, `Standalone/Core/src/ResolvedLayoutInternal.h` | Versioned Maze/VerticalShafts opt-in and deterministic seed from `(seed, strate, recipe version, stable ID)`; optional union width preserves prior cave JSON when unset. |
|
|
||||||
| `MakeNativeLatticeSpine`, `--recipe=lattice`, `--lattice-strate=N` | `Standalone/Source/StreamBenchmarkHeadless.cpp` | Resolves actual adjacent mouths, builds a mandatory four-turn helix and maze spine, then adds two seeded dead ends; `--lattice-orphan-control` moves the helix away from both junctions. |
|
|
||||||
| `AuditNativeLatticeField` | `Standalone/Source/StreamBenchmarkPipeline.h` | Samples the final field along the mouth-to-mouth capsule route, checking support, headroom, grade, orphan rejection, composed fillet, interval signs and feature-tile exclusion. |
|
|
||||||
| `BuildLatticeFixtureL58.py`, `CaptureNativeLatticeL58.py`, `AuditLatticeSurfaceL58.py` | `Standalone/Research/WorldAssemblerM3/Linux58/` | Isolated Maze strate fixture, four reviewed captures and the L54 whole-surface triangle audit adapted to explicit native polylines. |
|
|
||||||
|
|
||||||
See ARCHITECTURE §8.54 and
|
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux58/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux58/REPORT.md)
|
|
||||||
for resolved records, final-field reachability, controls, timing, hashes and captures.
|
|
||||||
|
|
||||||
### 3.37 Unreal CoreGeometry assets and partial metre upgrade — World Assembler U1
|
|
||||||
|
|
||||||
`Public/VoxelStrateTypes.h` declares optional `FVoxelForgeCoreGeometry`. Its default recipe is Off
|
|
||||||
and its stamp, room, and tunnel arrays are empty. `Private/VoxelForgeCoreAdapter.cpp` maps authored
|
|
||||||
metres to Core layout requests after passage planning and before finalizing the layout. Stamps use
|
|
||||||
the V1 resolver for original stamp kinds and V3 for monolith/platform motifs because the checked-out
|
|
||||||
Core API exposes V3; authored cavern records use `ResolveResolvedCavernGraphV1`. Stable feature
|
|
||||||
names are namespaced by strate in the resolved layout. `FStrateLandmark::CoreGeometryStableName`
|
|
||||||
binds one landmark to such a resolved record, and `VoxelContentManager.cpp` builds its lookup from
|
|
||||||
the finalized snapshot. `VoxelWorld.cpp` retains resolved layout identity in content hashing.
|
|
||||||
|
|
||||||
`Private/VoxelForgeAssetVersion.h` sets the shared custom version to 2. `UVoxelSettings::Serialize`
|
|
||||||
and `UVoxelStrateDefinition::Serialize`/`PostLoad` upgrade only inter-strate gap, strate height,
|
|
||||||
and transition width using the saved 8 m layout chunk. These conversions are version-guarded and
|
|
||||||
idempotent. No package save is performed by the audit. Other Research8 chunks, owner-voxel lengths,
|
|
||||||
Core-voxel lengths/frequencies, and centimetre fields remain on their existing units and need a
|
|
||||||
separate migration.
|
|
||||||
|
|
||||||
Likely compile/UHT touchpoints are the reflected nested fields in `VoxelStrateTypes.h`, Core snapshot
|
|
||||||
and resolver signatures in `VoxelForgeCoreAdapter.cpp`, `FCustomVersionRegistration` and the old
|
|
||||||
tunnel-slope version in `VoxelStrateDefinition.cpp`, plus the standalone `VoxelForgeExplore` audit
|
|
||||||
in `VoxelForgeExploreCommandlet.cpp`. `GetStrateForChunk` still receives layout-chunk Z, not a Core
|
|
||||||
tile index. See `Standalone/Research/WorldAssemblerM3/Unreal1/REPORT.md` for BuildHost evidence.
|
|
||||||
|
|
||||||
## 4. The density pipeline (most-edited hot path)
|
## 4. The density pipeline (most-edited hot path)
|
||||||
|
|
||||||
### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749
|
### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
# VoxelForge current state
|
|
||||||
|
|
||||||
As of 2026-10-31. L131 is on `assembler/world` as `8107196`; this L132 worktree is on `wip/l132-dispatch-truth`, starting at `ebae71b`. Current-state claims below point to their code or measured report.
|
|
||||||
|
|
||||||
## What ships today
|
|
||||||
|
|
||||||
- **Core:** evaluator version **227**. Unreal's `AVoxelWorld::bUseVoxelForgeCore` is opt-in and defaults to `false`; the legacy generator remains the default runtime path. [Core version](Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h), [runtime switch](Source/VoxelForge/Public/VoxelWorld.h), [L130 report](Standalone/Research/WorldAssemblerM3/Linux130/REPORT.md).
|
|
||||||
- **Verified owner4 generator dispatch (L132):** SW binds `SurfaceWorld-native` because the owner export contains a matching `SurfaceWorld.Heightfield2D` v2 plan. FI binds `compiled-analytic/FloatingIslands` because its `FloatingIslands.ClusterField` v0 layout has no matching native island plan. TN and VS bind `compiled-analytic/TunnelNetwork` and `compiled-analytic/VerticalShafts`; their legacy recipe IDs provide neither a `NativeCaves`/`NativeLattice` graph recipe nor native field capability. The L132 fast-tier report prints the binding and generated tile-center share at each owner strate. [Dispatch](Standalone/Core/src/ShapeWorldInternal.h), [adapter recipe checks](Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h), [L132 report](Standalone/Research/WorldAssemblerM3/Linux132/REPORT.md).
|
|
||||||
- **Passages:** continuous smooth swept tubes with a continuous floor profile. L125 stamps conservative route slabs and outside sentinels into the tile cache; exact route geometry is still evaluated. All 21,425 route samples passed the open-air walk check; the nine-station floor probe's maximum root offset was 7.884 mm. L130 sets the shared influence reach to **1.0533 m** (0.75 m shell + 0.21 m relief + 0.50 m smooth-max support / 6 + 0.01 m margin), converted once from metres to Core voxels for the proof, feature, fallback, and off-grid bounds. [L125](Standalone/Research/WorldAssemblerM3/Linux125/REPORT.md), [L130](Standalone/Research/WorldAssemblerM3/Linux130/REPORT.md).
|
|
||||||
- **Streaming:** arrival uses a bounded startup apply burst: `voxel.StreamingStartupApplyBoost=1` by default, up to 24 visible meshes or 14 ms per frame for at most 30 seconds, until the initial desired set and walkable ring are ready. `voxel.StreamingFarApplySupplement=1` is also default-on: after the normal visible apply cap, at most one far result applies when under half the apply-time budget; it is inactive during the startup burst. Dynamic near reserve defaults on; coarse-first remains off; the legacy pending-result cap defaults off. The L165/L166 incremental desired-set path now defaults on (`voxel.StreamingIncrementalDesiredSet=1`): Core clipmap keys, overlay membership, and priority order update incrementally, with same-center Core keys cached. No separate overlay or priority incremental switches are required. Audits stay off (`StreamingIncrementalDesiredAuditEvery=0`, `StreamingIncrementalDesiredMaterializationAuditEvery=0`, mismatch injection `=0`). L157 early admission and apply-cap controls stay off (`StreamingFastTravelWaitForAllCriticalTiles=1`, `StreamingFastTravelApplyBudgetMilliseconds=0`). L160 ahead-first and same-center rebuild suppression stay off (`StreamingFastTravelForwardFirst=0`, `StreamingRebuildOnCoreRegionCompletion=1`). Its separate Core desired-cache switch stays off (`StreamingCoreDesiredCacheByCenter=0`); the new incremental path enables its own same-center Core cache. The far-apply lane is Unreal-only. [CVar source](Source/VoxelForge/Private/VoxelWorld.cpp), [L121 report](Standalone/Research/WorldAssemblerM3/Linux121/REPORT.md), [architecture](ARCHITECTURE.md).
|
|
||||||
- **Determinism:** proven on Linux with Clang and GCC at W1/W8/W12. L131's unchanged-output comparison covers owner4's SW/FI/TN/VS views in 24 compiler/worker cases; the canonical owner4/owner2 recipe matrix also passes. Windows determinism is unverified. [L131 checks](Standalone/Research/WorldAssemblerM3/Linux131/REPORT.md), [v227 canonical hashes](Standalone/Tools/verify/baselines/owner-hash/canonical/).
|
|
||||||
|
|
||||||
## Current performance references
|
|
||||||
|
|
||||||
Headless medians in seconds, five interleaved L80-style measurements per version in each cell. Near-ready means the complete LOD0–2 set; whole-view is the full requested view. Values below are the edited L131 binary; the paired pre-cleanup comparison is in the [L131 report](Standalone/Research/WorldAssemblerM3/Linux131/REPORT.md) and [timing summary](Saved/Linux/l131/timing/summary.json).
|
|
||||||
|
|
||||||
| Stratum | Near-ready W8 / W12 | Whole-view W8 / W12 |
|
|
||||||
|---|---:|---:|
|
|
||||||
| S0 | 0.804 / 0.848 | 1.811 / 1.832 |
|
|
||||||
| S1 | 1.014 / 1.002 | 1.993 / 1.886 |
|
|
||||||
| S2 | 1.060 / 1.012 | 1.960 / 1.865 |
|
|
||||||
| S3 | 0.917 / 0.884 | 1.887 / 1.782 |
|
|
||||||
|
|
||||||
Across 40 paired runs per metric, the mean relative change from pre-cleanup L130 was −0.22% near-ready (95% CI −1.02% to +0.57%) and −0.29% whole-view (−1.04% to +0.46%); neither indicates a measurable slowdown. The machine gate was load 0.81 and 99.2% idle. [L80 protocol](Standalone/Research/WorldAssemblerM3/Linux80/REPORT.md), [runner and raw runs](Standalone/Research/WorldAssemblerM3/Linux131/RunL131Timing.py).
|
|
||||||
|
|
||||||
The latest recorded Linux Unreal start-to-LOD0–2 values are L127's one run per stratum: S0 2.456 s, S1 2.231 s, S2 2.514 s, S3 2.221 s. L130 did not rerun UE startup or movement. Five L127 runs at 30 m/s, 12 s moving plus 5 s dwell, had median moving near coverage **40.22%**, far holes **3.46%**, tick p99 **10.80 ms**, and worst tick **13.27 ms**. After stopping, near coverage reached 100% and far holes reached 0%. These are the latest UE references, not v227 UE measurements. [L127 report](Standalone/Research/WorldAssemblerM3/Linux127/REPORT.md), [L127 movement summary](Saved/Linux/l127/ue/movement-pairs-summary.json), [L127 start summaries](Saved/Linux/l127/ue/merge/start-summary.json).
|
|
||||||
|
|
||||||
## Open gaps
|
|
||||||
|
|
||||||
- Multiplayer world lifecycle is unfinished: seed/layout replication, authoritative modification RPCs, and serialized diff replay for late join are still open. See [ARCHITECTURE §9.5 and §9.7](ARCHITECTURE.md).
|
|
||||||
- Determinism has not been verified on Windows. Linux Clang/GCC agreement does not establish MSVC agreement.
|
|
||||||
- NullRHI measures residency and apply timing, not the first presented pixel or rendered enclosure. L127 also found four low-contrast views among 23 otherwise verified captures. [L121](Standalone/Research/WorldAssemblerM3/Linux121/REPORT.md), [L127](Standalone/Research/WorldAssemblerM3/Linux127/REPORT.md).
|
|
||||||
- UE startup/movement numbers above predate L130's v227 reach change; there is no L130/L131 UE movement comparison.
|
|
||||||
|
|
||||||
## How to verify
|
|
||||||
|
|
||||||
- Build standalone Clang or GCC with [`Standalone/BuildLinux.sh`](Standalone/BuildLinux.sh), for example `bash Standalone/BuildLinux.sh --compiler=clang++-19 --variant=l132-clang --fp-contract=off --jobs=4`. The script pins CPUs 0–11 and applies `prlimit --as=6442450944` itself.
|
|
||||||
- Fast tier: `python3 Standalone/Tools/verify/verify.py fast --out Saved/Linux/l132 --overall-timeout 1740`. It includes the L132 four-pose owner density-dispatch report; per-run logs and the summary stay under `Saved/Linux/l132`.
|
|
||||||
- Canonical output baselines are the v227 files in `Standalone/Tools/verify/baselines/owner-hash/canonical/`. L131 also provides reproducible pre-cleanup identity matrices for owner4 tile hashes and owner4/owner2 canonical hashes under `Standalone/Research/WorldAssemblerM3/Linux131/`.
|
|
||||||
- Timing follows L80: quiet machine, no overlapping build, one warm-up per version, five interleaved runs per stratum and worker count, W8/W12. Each headless run uses `taskset -c 0-11`, `prlimit --as=6442450944`, and a 1,740 s timeout. See [`RunL131Timing.py`](Standalone/Research/WorldAssemblerM3/Linux131/RunL131Timing.py).
|
|
||||||
- Linux host build: `/home/jahni/UnrealEngine/Engine/Build/BatchFiles/Linux/Build.sh VFHostEditor Linux Development -Project=/home/jahni/VFHost/VFHost.uproject -NoUBA -MaxParallelActions=4 -Log=/home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/l131/ubt.log`. Run Unreal without `prlimit`.
|
|
||||||
|
|
||||||
## Closed experiments
|
|
||||||
|
|
||||||
These rejected speed/streaming proposals stay closed unless new evidence changes their measured failure. Reports and surviving patches are linked in the [L131 inventory](Standalone/Research/WorldAssemblerM3/Linux131/REPORT.md).
|
|
||||||
|
|
||||||
| Line | Result |
|
|
||||||
|---|---|
|
|
||||||
| L116 tunnel chord bound and z cache | Pair count fell 20.8%, but stayed 1.96× L109 and readiness was 15–30% slower. Patch: [`chord-bound-zcolumn-not-merged.patch`](Standalone/Research/WorldAssemblerM3/Linux116/patches/chord-bound-zcolumn-not-merged.patch), commit `89504ec`; see [report](Standalone/Research/WorldAssemblerM3/Linux116/REPORT.md). |
|
|
||||||
| L118 pair pruning / deferred floor work | Pruned 17–18% of candidate pairs but remained 17.2–25.7% behind L109 near-ready. [Report](Standalone/Research/WorldAssemblerM3/Linux118/REPORT.md); patch: [`l118-designs.patch`](Standalone/Research/WorldAssemblerM3/Linux118/l118-designs.patch). |
|
|
||||||
| L126 floor-call filters | Cut floor calls 20–23% without reducing pair counts; all eight cells missed L109. Candidate source was restored and no standalone patch was kept; see [report](Standalone/Research/WorldAssemblerM3/Linux126/REPORT.md) and commit `01a5094`. |
|
|
||||||
| L128 half-open slab ownership | Ownership audit passed, but readiness was 14.8–28.0% behind L109 and moving coverage dropped. Report and patch `l128-slab-ownership.patch` are in archived commit `7022d02`. |
|
|
||||||
| L129 2 m physical reach | v225's metre/Core-voxel bounds mismatch made the correctly bounded candidate do more work; it was slower and failed UE movement gates. Report and patch `l129-reach-2m.patch` are in archived commit `0d1140c`. |
|
|
||||||
| L120 priority split / extra worker slots | Neither met coverage and tick gates; 10 slots reached a 16.67 ms worst tick against 15.43 ms baseline. [Report](Standalone/Research/WorldAssemblerM3/Linux120/REPORT.md); no production patch retained. |
|
|
||||||
| L122 admission relaxation | Early admission missed p99 or far-hole gates; no behavior retained. [Report](Standalone/Research/WorldAssemblerM3/Linux122/REPORT.md); patch: [`l122-trace.patch`](Standalone/Research/WorldAssemblerM3/Linux122/l122-trace.patch). |
|
|
||||||
| L123 lookahead / completion gate | Lookahead failed p99 and worsened near/far coverage; queue-only also failed p99. [Report](Standalone/Research/WorldAssemblerM3/Linux123/REPORT.md); patch: [`l123-queue-lookahead.patch`](Standalone/Research/WorldAssemblerM3/Linux123/l123-queue-lookahead.patch). |
|
|
||||||
| L124 early LOD0 pages | Optimistic gain stayed below 100 ms except negative cases; no publication/scheduling implementation was made. [Report](Standalone/Research/WorldAssemblerM3/Linux124/REPORT.md). |
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1,201 +0,0 @@
|
|||||||
# L162 — Living Plane
|
|
||||||
|
|
||||||
The EGL/OpenGL 4.3 runner builds and passes its llvmpipe correctness and frame checks. The sandbox has no accessible DRM render node, so it did not run or report Intel GPU performance. The runner refuses `--device=gpu` here instead of falling back to software.
|
|
||||||
|
|
||||||
## Implementation
|
|
||||||
|
|
||||||
`LivingPlane.cpp` runs a ping-pong `RG32F` height/velocity field through a runtime-compiled GLSL compute shader. The damped wave update includes a weak foundation spring, a Gaussian sphere push, and an optional slow breathing force. The spring is needed because a persistent local push otherwise accelerates the zero-frequency height mode and sinks the whole plane. The sphere moves across the plane, stops, and bobs. A vertex-displaced mesh and lit sphere render to an EGL pbuffer-backed framebuffer and save as PNGs.
|
|
||||||
|
|
||||||
The timestep is selected from the largest 2D grid eigenfrequency:
|
|
||||||
|
|
||||||
`dt = 1.30 / sqrt(8*c*c/(dx*dx) + foundationStiffness)`
|
|
||||||
|
|
||||||
The resulting spectral step ratio is 1.30, below the explicit update's 2.0 stability limit. The runner warms the compute kernel for eight unmeasured dispatches, then records one `GL_TIME_ELAPSED` query per requested simulation step in `timing.json`.
|
|
||||||
|
|
||||||
The first-step equation audit also caught a GL state error during bring-up: frame rendering left the graphics program active before the next compute dispatch. `Dispatch` now binds the compute program before every step, so the audit fails if a frame silently stops simulation.
|
|
||||||
|
|
||||||
Device selection enumerates EGL devices and uses `EGL_EXT_platform_device` for an accessible Intel render node first. On this host, the known integrated device is Intel on `renderD128`. Automatic mode falls back to a surfaceless Mesa context with `LIBGL_ALWAYS_SOFTWARE=1`; explicit GPU mode fails when no accessible hardware node is present. EGL does not expose Vulkan's integrated/discrete enum, so Intel vendor/render-node identity is the selection heuristic.
|
|
||||||
|
|
||||||
The Unreal handoff is in [UNREAL-MAPPING.md](UNREAL-MAPPING.md). It keeps the prototype render-only and describes the density mapping if the surface later needs to affect voxel queries. No Unreal `Source/` files changed.
|
|
||||||
|
|
||||||
## Measurements and checks
|
|
||||||
|
|
||||||
- Built with `Experiments/LivingPlane/build.sh` using the installed EGL, desktop GL, and libpng development files; no shader compiler or package install was needed.
|
|
||||||
- Final llvmpipe run: 128² field, 360 steps, `dt=9.56814 ms`, spectral ratio 1.30. The first-step equation audit and finite-field/dynamics audit passed. Final field range was −0.139533 m to +0.0366704 m. It wrote 16 128×128 PNGs and a JSON file containing 360 per-step query samples. The frames were inspected; the moving sphere and wake are visible.
|
|
||||||
- Auto-selection smoke check: 32², 12 steps; selected llvmpipe and passed both audits. Breathing smoke check: 64², 180 steps with `--breathing=on`; passed both audits.
|
|
||||||
- Negative device-selection control: `--device=gpu` returned an error because the sandbox has no accessible EGL DRM render node.
|
|
||||||
- Existing fast verification tier passed: 278.9 seconds, ending in `SUMMARY PASS`. Its build and logs are under `Saved/Linux/l162/verify/` and `Saved/Linux/l162/`.
|
|
||||||
- The llvmpipe `GL_TIME_ELAPSED` samples are preserved in JSON but are software-driver query values, not GPU timings. The requested hardware timing pass remains to be run outside the sandbox.
|
|
||||||
- No Unreal build was run; no Unreal `Source/` files changed.
|
|
||||||
|
|
||||||
## Intel GPU pass
|
|
||||||
|
|
||||||
Run these from the plugin root as `jahni`. The frame run writes 512×512 PNGs to `Saved/Linux/l162/gpu-frames-512/`; the two timing runs write `timing.json` with one GPU query per step and skip PNG capture. They still read the field for the first-step and final-state correctness audits.
|
|
||||||
Before the timing runs, check `uptime` and run them on a quiet machine (load below about 1.5, with no other build active).
|
|
||||||
|
|
||||||
```sh
|
|
||||||
uptime
|
|
||||||
taskset -c 0-11 prlimit --as=6442450944 -- timeout 1740s ./Saved/Linux/l162/living-plane --device=gpu --size=512 --steps=1800 --capture-every=12 --out=Saved/Linux/l162/gpu-frames-512
|
|
||||||
taskset -c 0-11 prlimit --as=6442450944 -- timeout 1740s ./Saved/Linux/l162/living-plane --device=gpu --size=512 --steps=1800 --capture-every=0 --out=Saved/Linux/l162/gpu-timing-512
|
|
||||||
taskset -c 0-11 prlimit --as=6442450944 -- timeout 1740s ./Saved/Linux/l162/living-plane --device=gpu --size=1024 --steps=1800 --capture-every=0 --out=Saved/Linux/l162/gpu-timing-1024
|
|
||||||
```
|
|
||||||
|
|
||||||
Encode the captured sequence as MP4 with:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
ffmpeg -y -framerate 30 -i Saved/Linux/l162/gpu-frames-512/frame_%06d.png -c:v libx264 -pix_fmt yuv420p Saved/Linux/l162/living-plane-512.mp4
|
|
||||||
```
|
|
||||||
|
|
||||||
## Files and remaining risk
|
|
||||||
|
|
||||||
The only changed file outside `Experiments/LivingPlane/` is `.gitignore`, which now allows `Experiments/` and its contents. GPU timings and Intel render-node selection remain unverified until the commands above run on the host outside this sandbox.
|
|
||||||
|
|
||||||
## Intel GPU pass (run by the orchestrator outside the sandbox, 2026-11-02)
|
|
||||||
|
|
||||||
- The device was selected correctly: `Mesa Intel(R) Graphics (ADL GT2)`, OpenGL 4.6, `/dev/dri/renderD128`. 512²,
|
|
||||||
1,800 steps, 151 frames; the equation audit passes. MP4: `Saved/Linux/l162/living-plane-512.mp4` (local).
|
|
||||||
- **The `GL_TIME_ELAPSED` per-dispatch values (~0.0001 ms) are not credible** for 262k cells, so they aren't
|
|
||||||
reported as timings.
|
|
||||||
- Wall-time difference between 1,800 and 200 steps, divided by the 1,600 steps (start-up cancels out; this includes
|
|
||||||
submission, so it's an upper bound): **0.080 ms/step at 512²** and **0.292 ms/step at 1024²**.
|
|
||||||
- Visual read: the effect is very subtle in the current render (a slight dip under the sphere, faint rings). It needs
|
|
||||||
a larger push amplitude, grazing light or height colouring, and a closer camera to read.
|
|
||||||
|
|
||||||
## L163 — readable, living plane
|
|
||||||
|
|
||||||
The plane now reads as rolling terrain, and the objects leave visible troughs and wakes. `alive` adds travelling
|
|
||||||
breathing and a viscoelastic imprint that remains after lift-off. The simulation still runs in one GPU dispatch per
|
|
||||||
step; CPU work only supplies the two object states.
|
|
||||||
|
|
||||||
`LivingPlane.cpp` now keeps the dynamic displacement, velocity, and imprint in an `RGBA32F` ping-pong field. The
|
|
||||||
surface shader adds broad static terrain relief, calculates normals from relief plus displacement, and uses grazing
|
|
||||||
light, elevation colours, slope tint, and 7.5 cm contour bands. The camera follows the first sphere at a closer angle.
|
|
||||||
The wake scene rolls the first sphere, pauses and wobbles it, lifts it, then sends a second sphere through the wake.
|
|
||||||
The alive scene runs that same sequence with low-frequency travelling swells and a decaying contact imprint. Its
|
|
||||||
defaults are breathing strength 4.8 m/s², imprint capture 0.15 m/s, recovery time 6 s, and maximum imprint depth
|
|
||||||
0.34 m. These values have command-line overrides.
|
|
||||||
|
|
||||||
The timer query check found no early read or reuse in L162: it allocated a unique query per step, called `glFinish`,
|
|
||||||
then read every result. The saved Intel/Mesa 25.0.7 samples are 0 or 52 ns for most dispatches (512² median 0,
|
|
||||||
p95 52 ns; largest sample 17.552 µs), which cannot be a credible per-dispatch cost. The old usage did not reuse
|
|
||||||
query objects, so lifetime is not the cause; the saved data point to Intel/Mesa's timer-query reporting or timing
|
|
||||||
scope, but the sandbox cannot isolate which. L163 uses a 64-query ring, reads each query before reusing its slot,
|
|
||||||
checks availability after `glFinish`, and records `GL_QUERY_COUNTER_BITS`. On llvmpipe the availability check passed
|
|
||||||
with 64 counter bits and zero waits. The Intel query result still needs the GPU diagnostic command below.
|
|
||||||
|
|
||||||
### Measurements and checks
|
|
||||||
|
|
||||||
- llvmpipe wake: 128², 600 steps (`dt=9.56814 ms`, 5.741 s simulation window), 151 frames at 30 fps. First-step
|
|
||||||
equation audit and timer-query lifetime check passed; final displacement range was −0.177488 m to +0.104854 m.
|
|
||||||
- llvmpipe alive: same grid, steps, and window; equation audit and timer-query lifetime check passed. Final
|
|
||||||
displacement range was −0.200437 m to +0.115904 m, with a −0.111667 m stored imprint.
|
|
||||||
- llvmpipe cost: 512², 1,800 steps, `--queries=off`, no frame capture: **0.767473 ms/step** in one timed
|
|
||||||
simulation-loop window. `timing.json` includes dispatch submission, the step-0 equation readback, and final
|
|
||||||
`glFinish`, but excludes setup and frame capture. This is a software-renderer measurement, not the Intel GPU cost.
|
|
||||||
- Both five-second llvmpipe previews are in `Saved/Linux/l163/llvmpipe-wake.mp4` and
|
|
||||||
`Saved/Linux/l163/llvmpipe-alive.mp4`. They are 128² review previews; use the GPU commands below for the owner
|
|
||||||
videos.
|
|
||||||
- Equation-audit negative control: a temporary copy with its expected first-step height forced to zero exited FAIL
|
|
||||||
at actual −0.0488777 m. The final source passes that same check.
|
|
||||||
- The fast verification tier passed in 278.5 s. Build and logs are under `Saved/Linux/l163/verify/`.
|
|
||||||
- No Unreal `Source/` files changed and no Unreal build ran.
|
|
||||||
|
|
||||||
### Intel GPU run
|
|
||||||
|
|
||||||
Run from the plugin root as `jahni`. First build the runner. For timed runs, check `uptime` and keep the machine
|
|
||||||
quiet. The wall-time field is valid only when `--capture-every=0`; use `--queries=off` for the step-cost measurement.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
bash Experiments/LivingPlane/build.sh
|
|
||||||
uptime
|
|
||||||
taskset -c 0-11 prlimit --as=6442450944 -- timeout 1740s ./Saved/Linux/l163/living-plane --device=gpu --scene=wake --size=512 --steps=2400 --capture-every=16 --queries=off --out=Saved/Linux/l163/gpu-wake-512
|
|
||||||
taskset -c 0-11 prlimit --as=6442450944 -- timeout 1740s ./Saved/Linux/l163/living-plane --device=gpu --scene=alive --size=512 --steps=2400 --capture-every=16 --queries=off --out=Saved/Linux/l163/gpu-alive-512
|
|
||||||
ffmpeg -y -framerate 30 -i Saved/Linux/l163/gpu-wake-512/frame_%06d.png -c:v libx264 -pix_fmt yuv420p Saved/Linux/l163/living-plane-wake-512.mp4
|
|
||||||
ffmpeg -y -framerate 30 -i Saved/Linux/l163/gpu-alive-512/frame_%06d.png -c:v libx264 -pix_fmt yuv420p Saved/Linux/l163/living-plane-alive-512.mp4
|
|
||||||
taskset -c 0-11 prlimit --as=6442450944 -- timeout 1740s ./Saved/Linux/l163/living-plane --device=gpu --scene=alive --size=512 --steps=1800 --capture-every=0 --queries=off --out=Saved/Linux/l163/gpu-timing-512
|
|
||||||
taskset -c 0-11 prlimit --as=6442450944 -- timeout 1740s ./Saved/Linux/l163/living-plane --device=gpu --scene=alive --size=512 --steps=1800 --capture-every=0 --queries=on --out=Saved/Linux/l163/gpu-query-check-512
|
|
||||||
```
|
|
||||||
|
|
||||||
`gpu-timing-512/timing.json` reports the 512² batch wall time per step, including dispatch submission and completion.
|
|
||||||
`gpu-query-check-512/timing.json` contains query counter bits, waits before slot reuse, availability status, and raw
|
|
||||||
per-step query samples. The sandbox has no accessible Intel DRM node, so the current Intel step cost and new query
|
|
||||||
diagnostic remain unmeasured here.
|
|
||||||
|
|
||||||
### L163 Intel GPU pass (run by the orchestrator, 2026-11-02)
|
|
||||||
|
|
||||||
- Intel ADL GT2 (Mesa 25.0.7, `renderD128`): wake and alive scenes, 512², 2,400 steps, 151 frames each; equation
|
|
||||||
audits PASS. Videos (local): `Saved/Linux/l163/living-plane-wake-512.mp4`, `living-plane-alive-512.mp4`.
|
|
||||||
- Step cost, alive scene, 512², 1,800 steps, no capture: **0.172 ms/step** with queries off, 0.152 ms/step with
|
|
||||||
queries on (batch wall time per step).
|
|
||||||
- The Intel `GL_TIME_ELAPSED` median is still 0 with 64 counter bits and the ring/availability checks passing. That
|
|
||||||
looks like a Mesa/Intel reporting issue; use wall time.
|
|
||||||
- **Open issue:** the same scene with the same steps gave different final fields (min −0.637 m with queries off vs
|
|
||||||
−0.327 m with queries on). Something depends on more than the step index (e.g. wall-clock time); investigate.
|
|
||||||
|
|
||||||
## L164 — repeatable field readback and UE runtime actor
|
|
||||||
|
|
||||||
**Result:** the repeatability check passes on llvmpipe, and the new `VoxelForgeLivingPlane` runtime module compiles
|
|
||||||
in the Linux UE 5.7 host. The surface actor is independent of VoxelForge generation and sends no field readback to
|
|
||||||
the CPU.
|
|
||||||
|
|
||||||
### Repeatability finding
|
|
||||||
|
|
||||||
`uTime` is `Step * dt`, and `GetBodiesAt` receives that same value. Neither the object schedule nor the breathing
|
|
||||||
force uses wall time or a rendered-frame counter. Every compute step already issued an image-access barrier before
|
|
||||||
the next ping-pong image read. The defect was on the diagnostic side: the step-zero and final `glGetTexImage`
|
|
||||||
readbacks followed shader image writes without `GL_TEXTURE_UPDATE_BARRIER_BIT`. The Khronos OpenGL specification
|
|
||||||
requires that barrier for `GetTexImage` to observe earlier shader writes ([§7.12](https://registry.khronos.org/OpenGL/specs/gl/glspec44.compatibility.pdf)). Query/capture pacing could therefore affect the reported field values without changing any simulation input. The barrier is now issued before both readbacks.
|
|
||||||
|
|
||||||
`check_repeatability.py` runs the same 512² alive scene for 1,800 steps twice: queries off with no captures, then
|
|
||||||
queries on with captures every 900 steps. The checksum covers every final RGBA32F field value using canonical
|
|
||||||
little-endian float bytes.
|
|
||||||
|
|
||||||
- llvmpipe, queries off/capture off: field range −0.637382 m to +0.109308 m; checksum
|
|
||||||
`1ba08f5dafc01f82`.
|
|
||||||
- llvmpipe, queries on/capture every 900: the same range and checksum; equation and timer-query audits pass; three
|
|
||||||
frames written.
|
|
||||||
- Negative control: changing one saved checksum made the comparison exit 1 with `FAIL field checksum mismatch`.
|
|
||||||
- The llvmpipe runs were each one 1,800-step simulation-loop window. Logs and JSON are under
|
|
||||||
`Saved/Linux/l164/repeatability/`.
|
|
||||||
|
|
||||||
The Intel run is not available in this sandbox. The owner should repeat the checksum comparison on the GPU to
|
|
||||||
confirm that the missing transfer-readback barrier accounts for the earlier Intel min mismatch:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
python3 Experiments/LivingPlane/check_repeatability.py --device=gpu --out=Saved/Linux/l164/gpu-repeatability
|
|
||||||
```
|
|
||||||
|
|
||||||
### Unreal implementation and checks
|
|
||||||
|
|
||||||
The plugin descriptor adds a separate `VoxelForgeLivingPlane` runtime module. Its RDG global shader is in
|
|
||||||
`Shaders/LivingPlane.usf`; module startup maps `/Plugin/VoxelForgeLivingPlane` to the plugin shader directory. The
|
|
||||||
actor owns two UAV-capable RGBA32F render targets, advances them at the stable fixed step, and supplies the current
|
|
||||||
texture to a movable plane mesh material. Its Blueprint/C++ object input includes world position, radius, velocity,
|
|
||||||
base push, and active state. The CPU converts those values to plane-local metres and uploads uniforms; it does not
|
|
||||||
read the field back. The ignore rules now include `Shaders/` so that the shader ships in the worktree diff.
|
|
||||||
`Experiments/LivingPlane/UE-SETUP.md` gives level setup, the full material graph, and object driving steps.
|
|
||||||
|
|
||||||
- Linux UE 5.7 `VFHostEditor Linux Development`: **PASS**, final incremental build compiled
|
|
||||||
`VoxelForgeLivingPlaneActor.cpp` and linked `libUnrealEditor-VoxelForgeLivingPlane.so`. Command used
|
|
||||||
`-NoUBA -MaxParallelActions=4`, `taskset -c 0-11`, and no `prlimit`. Log:
|
|
||||||
`Saved/Linux/l164/ue-build-final.log`. The initial attempt exposed an incorrect RDG texture registration overload;
|
|
||||||
it had already compiled `VoxelForgeLivingPlaneModule.cpp` and the generated module code. The corrected final
|
|
||||||
build passed.
|
|
||||||
- The host `PluginDeps/VoxelForge/Source` and `.uplugin` links were temporarily pointed at this worktree for the
|
|
||||||
build and restored to `/home/jahni/VoxelM/Plugins/VoxelForge` afterward. The temporary shader link was removed.
|
|
||||||
- Standalone build, final repeatability comparison, checksum-mismatch negative control, plugin JSON parse, and
|
|
||||||
`git diff --check` passed.
|
|
||||||
- Not run: fast generation verification (generation sources are untouched), Intel GPU checksum comparison, or a UE
|
|
||||||
editor render/shader-runtime check. The Linux UBT build verifies C++ and module integration, not first-use shader
|
|
||||||
compilation or material appearance.
|
|
||||||
|
|
||||||
New Unreal source and shader files for the owner's Windows build:
|
|
||||||
|
|
||||||
- `Source/VoxelForgeLivingPlane/VoxelForgeLivingPlane.Build.cs`
|
|
||||||
- `Source/VoxelForgeLivingPlane/Public/VoxelForgeLivingPlaneActor.h`
|
|
||||||
- `Source/VoxelForgeLivingPlane/Private/VoxelForgeLivingPlaneActor.cpp`
|
|
||||||
- `Source/VoxelForgeLivingPlane/Private/VoxelForgeLivingPlaneModule.cpp`
|
|
||||||
- `Shaders/LivingPlane.usf`
|
|
||||||
|
|
||||||
No VoxelForge generation module files changed. Remaining risks are the Intel checksum confirmation and first-use UE
|
|
||||||
shader/material rendering, which require the owner's GPU/editor run.
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
# Living Plane in Unreal Engine 5.7
|
|
||||||
|
|
||||||
`VoxelForgeLivingPlane` is a separate runtime module. It does not call the VoxelForge terrain generators and it does not read the simulated texture back to the CPU. The actor owns two transient `RGBA32f` render targets and advances them with an RDG compute pass. The CPU sends object state and fixed-step uniforms only.
|
|
||||||
|
|
||||||
## Add it to a level
|
|
||||||
|
|
||||||
1. Enable the VoxelForge plugin and restart the editor so the `VoxelForgeLivingPlane` runtime module and its shader mapping load.
|
|
||||||
2. Create a material named `M_LivingPlane` using the graph below.
|
|
||||||
3. Place `VoxelForgeLivingPlaneActor` in the level. Assign `M_LivingPlane` to **Plane Material**.
|
|
||||||
4. Keep the actor on the world XY plane with unit actor scale. `Plane Length Meters` controls the mesh width and simulation width; the built-in plane mesh is scaled to that size.
|
|
||||||
5. The actor creates and clears its two GPU state targets on `BeginPlay`. The mesh has collision disabled; this surface is render-only and does not change VoxelForge density or collision queries.
|
|
||||||
|
|
||||||
## Material graph
|
|
||||||
|
|
||||||
Use a Surface, Opaque material. The actor binds these parameters on its dynamic material instance:
|
|
||||||
|
|
||||||
- `LivingField`: Texture Object Parameter. The texture is the current RGBA32f simulation state; **R** is displacement in metres.
|
|
||||||
- `PlaneLengthMeters`: Scalar Parameter.
|
|
||||||
- `FieldSize`: Scalar Parameter, equal to the render target width and height.
|
|
||||||
|
|
||||||
For World Position Offset:
|
|
||||||
|
|
||||||
1. Read `LivingField` at `TextureCoordinate[0]` with a Texture Sample.
|
|
||||||
2. Multiply its **R** output by `100` to convert metres to Unreal centimetres.
|
|
||||||
3. Append `(0, 0)` to that value, making `(0, 0, heightCm)`, and connect it to **World Position Offset**.
|
|
||||||
|
|
||||||
For the displaced normal, sample the height at four UV offsets using the same texture object:
|
|
||||||
|
|
||||||
- Left/right UV: `TexCoord + (-1/FieldSize, 0)` and `TexCoord + (1/FieldSize, 0)`.
|
|
||||||
- Down/up UV: `TexCoord + (0, -1/FieldSize)` and `TexCoord + (0, 1/FieldSize)`.
|
|
||||||
- `dHdx = (Right.R - Left.R) * FieldSize / (2 * PlaneLengthMeters)`.
|
|
||||||
- `dHdy = (Up.R - Down.R) * FieldSize / (2 * PlaneLengthMeters)`.
|
|
||||||
- Make and normalize the tangent-space vector `(-dHdx, -dHdy, 1)`, then connect it to **Normal**.
|
|
||||||
|
|
||||||
The built-in plane's U/V tangent directions align with local X/Y. Use a neutral base colour and roughness around `0.7` to start. The graph assumes a horizontal plane; add a local-to-world transform if the plane needs arbitrary rotation.
|
|
||||||
|
|
||||||
## Drive objects
|
|
||||||
|
|
||||||
`ObjectInputs` has two slots. Each `FVFLivingPlaneObjectInput` contains world position in centimetres, world velocity in centimetres per second, radius in centimetres, base push acceleration in metres per second squared, and an active flag. Blueprint can use **Set Object State** or set the array entry; C++ can call the same method. Set the object's center about one radius above the plane while it contacts it, and mark the slot inactive after lift-off.
|
|
||||||
|
|
||||||
The compute shader uses the radius to scale the Gaussian contact footprint. The original wave equation keeps the L163 wave speed, damping, foundation spring, travelling breathing force, and recovering imprint. The base push acceleration supplies the object force; lateral speed adds `0.25 m/s²` per `1 m/s`, and downward speed adds `0.5 m/s²` per `1 m/s`. Upward velocity does not add force. Object positions and velocities are transformed into the plane's local frame and converted from centimetres to metres before upload.
|
|
||||||
|
|
||||||
Each step uses `dt = 1.30 / sqrt(8*c*c/(dx*dx) + foundationStiffness)`, with `dx = PlaneLengthMeters / FieldSize`. Simulation time passed to breathing is the fixed step index times `dt`. Game-thread delta time only determines how many fixed steps are queued; no GPU field data is read back in the normal path.
|
|
||||||
|
|
||||||
## Build files
|
|
||||||
|
|
||||||
The owner's Windows build needs every added source and shader file:
|
|
||||||
|
|
||||||
- `Source/VoxelForgeLivingPlane/VoxelForgeLivingPlane.Build.cs`
|
|
||||||
- `Source/VoxelForgeLivingPlane/Public/VoxelForgeLivingPlaneActor.h`
|
|
||||||
- `Source/VoxelForgeLivingPlane/Private/VoxelForgeLivingPlaneActor.cpp`
|
|
||||||
- `Source/VoxelForgeLivingPlane/Private/VoxelForgeLivingPlaneModule.cpp`
|
|
||||||
- `Shaders/LivingPlane.usf`
|
|
||||||
|
|
||||||
The `VoxelForge.uplugin` runtime-module entry must ship with them.
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# Unreal mapping note
|
|
||||||
|
|
||||||
The prototype stores height, vertical velocity, a recovering imprint, and a spare
|
|
||||||
channel in a pair of `RGBA32F` textures. A world-owned Unreal component can keep
|
|
||||||
equivalent transient render graph textures and advance them with a compute pass. It
|
|
||||||
should own simulation time and clear or restore its buffers when the world is recreated.
|
|
||||||
|
|
||||||
The sphere path becomes a world-space interaction event: position, contact radius,
|
|
||||||
and impulse strength are supplied to the compute pass. For multiplayer or deterministic
|
|
||||||
replay, replicate the compact events and simulation tick, or record the resulting field;
|
|
||||||
do not assume that independent GPU floating-point runs will produce byte-identical
|
|
||||||
textures.
|
|
||||||
|
|
||||||
The rendered top surface can remain a render-only displaced mesh. If it must participate
|
|
||||||
in voxel collision or queries, its height must also reach the density evaluator. For a
|
|
||||||
height `h(x,y)`, the plugin's density convention is negative inside solid and positive
|
|
||||||
in air, so the plane's signed density is `z - h(x,y)`. Keep this calculation in metres
|
|
||||||
until the existing voxel/Core boundary converts with the active voxel size (25 cm by
|
|
||||||
default); do not mix metre distances and voxel counts in the wave solver.
|
|
||||||
|
|
||||||
The current sample is an isolated visual field. Wiring this into `AVoxelWorld` would
|
|
||||||
need an explicit owner for simulation lifetime, collision/update cadence, save/load,
|
|
||||||
and the replication policy before it could affect shipped terrain.
|
|
||||||
|
|
||||||
## L164 render-only Unreal implementation
|
|
||||||
|
|
||||||
`Source/VoxelForgeLivingPlane/` now implements this render-only path as a separate
|
|
||||||
runtime module. `AVoxelForgeLivingPlaneActor` owns two UAV-capable RGBA32F render
|
|
||||||
targets, advances the wave, breathing, and imprint equations with an RDG global shader,
|
|
||||||
and binds the current texture to its plane material. Blueprint/C++ supplies object
|
|
||||||
position, radius, velocity, push, and active state. The actor does not call the
|
|
||||||
generation modules and does not read the field back. Level setup and the material graph
|
|
||||||
are in [UE-SETUP.md](UE-SETUP.md).
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
PLUGIN_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)"
|
|
||||||
OUTPUT_DIR="${PLUGIN_ROOT}/Saved/Linux/l164"
|
|
||||||
CXX="${CXX:-g++}"
|
|
||||||
|
|
||||||
command -v "${CXX}" >/dev/null || { echo "C++ compiler not found: ${CXX}" >&2; exit 2; }
|
|
||||||
pkg-config --exists egl gl libpng || {
|
|
||||||
echo "EGL, desktop OpenGL, and libpng development files are required" >&2
|
|
||||||
exit 2
|
|
||||||
}
|
|
||||||
mkdir -p "${OUTPUT_DIR}"
|
|
||||||
read -r -a GRAPHICS_FLAGS <<<"$(pkg-config --cflags --libs egl gl libpng)"
|
|
||||||
|
|
||||||
taskset -c 0-11 prlimit --as=6442450944 -- \
|
|
||||||
"${CXX}" -std=c++20 -O2 -g -Wall -Wextra -Wpedantic \
|
|
||||||
"${SCRIPT_DIR}/LivingPlane.cpp" -o "${OUTPUT_DIR}/living-plane" \
|
|
||||||
"${GRAPHICS_FLAGS[@]}"
|
|
||||||
|
|
||||||
echo "built ${OUTPUT_DIR}/living-plane"
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Compare final field checksums across different query and capture settings."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import pathlib
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
PLUGIN_ROOT = pathlib.Path(__file__).resolve().parents[2]
|
|
||||||
|
|
||||||
|
|
||||||
def read_checksum(path: pathlib.Path) -> str:
|
|
||||||
try:
|
|
||||||
with path.open(encoding="utf-8") as source:
|
|
||||||
result = json.load(source)
|
|
||||||
except (OSError, json.JSONDecodeError) as error:
|
|
||||||
raise RuntimeError(f"could not read {path}: {error}") from error
|
|
||||||
checksum = result.get("field_checksum_fnv1a64")
|
|
||||||
if not isinstance(checksum, str) or len(checksum) != 16:
|
|
||||||
raise RuntimeError(f"{path} has no 16-digit field checksum")
|
|
||||||
return checksum
|
|
||||||
|
|
||||||
|
|
||||||
def compare(left: pathlib.Path, right: pathlib.Path) -> bool:
|
|
||||||
left_checksum = read_checksum(left)
|
|
||||||
right_checksum = read_checksum(right)
|
|
||||||
if left_checksum != right_checksum:
|
|
||||||
print(
|
|
||||||
"FAIL field checksum mismatch: "
|
|
||||||
f"{left}={left_checksum}, {right}={right_checksum}",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
print(f"PASS field checksum matches: {left_checksum}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
|
||||||
parser.add_argument(
|
|
||||||
"--binary",
|
|
||||||
type=pathlib.Path,
|
|
||||||
default=PLUGIN_ROOT / "Saved/Linux/l164/living-plane",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--out",
|
|
||||||
type=pathlib.Path,
|
|
||||||
default=PLUGIN_ROOT / "Saved/Linux/l164/repeatability",
|
|
||||||
)
|
|
||||||
parser.add_argument("--device", choices=("auto", "cpu", "gpu"), default="cpu")
|
|
||||||
parser.add_argument("--size", type=int, default=512)
|
|
||||||
parser.add_argument("--steps", type=int, default=1800)
|
|
||||||
parser.add_argument(
|
|
||||||
"--compare-only",
|
|
||||||
nargs=2,
|
|
||||||
type=pathlib.Path,
|
|
||||||
metavar=("LEFT_JSON", "RIGHT_JSON"),
|
|
||||||
help="compare existing timing.json files; used to exercise the failure path",
|
|
||||||
)
|
|
||||||
options = parser.parse_args()
|
|
||||||
|
|
||||||
if options.compare_only:
|
|
||||||
return 0 if compare(*options.compare_only) else 1
|
|
||||||
|
|
||||||
if not options.binary.is_file():
|
|
||||||
parser.error(f"binary not found: {options.binary}; run Experiments/LivingPlane/build.sh")
|
|
||||||
|
|
||||||
options.out.mkdir(parents=True, exist_ok=True)
|
|
||||||
off_dir = options.out / "queries-off_capture-off"
|
|
||||||
on_dir = options.out / "queries-on_capture-every-900"
|
|
||||||
for directory in (off_dir, on_dir):
|
|
||||||
if directory.exists():
|
|
||||||
shutil.rmtree(directory)
|
|
||||||
|
|
||||||
common = [
|
|
||||||
f"--device={options.device}",
|
|
||||||
"--scene=alive",
|
|
||||||
f"--size={options.size}",
|
|
||||||
f"--steps={options.steps}",
|
|
||||||
]
|
|
||||||
runs = (
|
|
||||||
("--capture-every=0", "--queries=off", off_dir),
|
|
||||||
("--capture-every=900", "--queries=on", on_dir),
|
|
||||||
)
|
|
||||||
for capture, queries, output in runs:
|
|
||||||
command = [
|
|
||||||
"taskset",
|
|
||||||
"-c",
|
|
||||||
"0-11",
|
|
||||||
"prlimit",
|
|
||||||
"--as=6442450944",
|
|
||||||
"--",
|
|
||||||
"timeout",
|
|
||||||
"1740s",
|
|
||||||
str(options.binary.resolve()),
|
|
||||||
*common,
|
|
||||||
capture,
|
|
||||||
queries,
|
|
||||||
f"--out={output}",
|
|
||||||
]
|
|
||||||
print("RUN " + " ".join(command), flush=True)
|
|
||||||
log_path = options.out / f"{output.name}.log"
|
|
||||||
with log_path.open("w", encoding="utf-8") as log_file:
|
|
||||||
try:
|
|
||||||
subprocess.run(
|
|
||||||
command,
|
|
||||||
cwd=PLUGIN_ROOT,
|
|
||||||
check=True,
|
|
||||||
stdout=log_file,
|
|
||||||
stderr=subprocess.STDOUT,
|
|
||||||
)
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
print(log_path.read_text(encoding="utf-8"), file=sys.stderr)
|
|
||||||
raise
|
|
||||||
print(log_path.read_text(encoding="utf-8").rstrip())
|
|
||||||
|
|
||||||
left = off_dir / "timing.json"
|
|
||||||
right = on_dir / "timing.json"
|
|
||||||
return 0 if compare(left, right) else 1
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
try:
|
|
||||||
raise SystemExit(main())
|
|
||||||
except (OSError, RuntimeError, subprocess.CalledProcessError) as error:
|
|
||||||
print(f"FAIL repeatability check: {error}", file=sys.stderr)
|
|
||||||
raise SystemExit(1)
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
Texture2D<float4> SourceState;
|
|
||||||
RWTexture2D<float4> TargetState;
|
|
||||||
|
|
||||||
int2 GridSize;
|
|
||||||
float Dt;
|
|
||||||
float Dx;
|
|
||||||
float PlaneLength;
|
|
||||||
float WaveSpeed;
|
|
||||||
float Damping;
|
|
||||||
float FoundationStiffness;
|
|
||||||
float TimeSeconds;
|
|
||||||
float2 BreathingSettings;
|
|
||||||
float4 ImprintSettings;
|
|
||||||
float4 ObjectPositionRadius[2];
|
|
||||||
float4 ObjectVelocityPush[2];
|
|
||||||
|
|
||||||
[numthreads(8, 8, 1)]
|
|
||||||
void MainCS(uint3 DispatchThreadId : SV_DispatchThreadID)
|
|
||||||
{
|
|
||||||
int2 Pixel = int2(DispatchThreadId.xy);
|
|
||||||
if (Pixel.x >= GridSize.x || Pixel.y >= GridSize.y)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int2 Lo = int2(0, 0);
|
|
||||||
int2 Hi = GridSize - 1;
|
|
||||||
float4 State = SourceState.Load(int3(Pixel, 0));
|
|
||||||
float Height = State.r;
|
|
||||||
float Velocity = State.g;
|
|
||||||
float Imprint = State.b;
|
|
||||||
float Left = SourceState.Load(int3(max(Pixel + int2(-1, 0), Lo), 0)).r;
|
|
||||||
float Right = SourceState.Load(int3(min(Pixel + int2(1, 0), Hi), 0)).r;
|
|
||||||
float Down = SourceState.Load(int3(max(Pixel + int2(0, -1), Lo), 0)).r;
|
|
||||||
float Up = SourceState.Load(int3(min(Pixel + int2(0, 1), Hi), 0)).r;
|
|
||||||
float Laplacian = (Left + Right + Down + Up - 4.0 * Height) / max(Dx * Dx, 1.0e-8);
|
|
||||||
float2 World = (float2(Pixel) + 0.5) * Dx - PlaneLength * 0.5;
|
|
||||||
|
|
||||||
float Interaction = 0.0;
|
|
||||||
float Contact = 0.0;
|
|
||||||
for (int ObjectIndex = 0; ObjectIndex < 2; ++ObjectIndex)
|
|
||||||
{
|
|
||||||
float4 PositionRadius = ObjectPositionRadius[ObjectIndex];
|
|
||||||
if (PositionRadius.w <= 0.0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
float4 VelocityPush = ObjectVelocityPush[ObjectIndex];
|
|
||||||
float Radius = PositionRadius.w;
|
|
||||||
float Sigma = max(Radius * (0.58 / 0.43), Dx);
|
|
||||||
float2 Offset = World - PositionRadius.xy;
|
|
||||||
float Weight = exp(-dot(Offset, Offset) / max(2.0 * Sigma * Sigma, 1.0e-8));
|
|
||||||
float HeightAboveContact = max(PositionRadius.z - Radius, 0.0);
|
|
||||||
float ContactHeight = 1.0 - smoothstep(0.0, max(Radius, Dx), HeightAboveContact);
|
|
||||||
float ImpactSpeed = length(VelocityPush.xy) * 0.25 + max(-VelocityPush.z, 0.0) * 0.5;
|
|
||||||
float Push = max(VelocityPush.w, 0.0) + ImpactSpeed;
|
|
||||||
Interaction -= Push * Weight * ContactHeight;
|
|
||||||
if (Push > 0.0)
|
|
||||||
{
|
|
||||||
Contact = max(Contact, Weight * ContactHeight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
float Breathing = 0.0;
|
|
||||||
if (BreathingSettings.x != 0.0)
|
|
||||||
{
|
|
||||||
float Swell = sin(World.x * 0.48 - TimeSeconds * 0.22)
|
|
||||||
* cos(World.y * 0.42 + TimeSeconds * 0.16);
|
|
||||||
Breathing = BreathingSettings.y * Swell * sin(TimeSeconds * 0.48);
|
|
||||||
}
|
|
||||||
|
|
||||||
float NextImprint = Imprint;
|
|
||||||
if (ImprintSettings.x != 0.0)
|
|
||||||
{
|
|
||||||
NextImprint = clamp(Imprint * exp(-Dt / max(ImprintSettings.z, 1.0e-4))
|
|
||||||
- Contact * ImprintSettings.y * Dt, -ImprintSettings.w, 0.0);
|
|
||||||
}
|
|
||||||
float FoundationTarget = ImprintSettings.x != 0.0 ? Imprint : 0.0;
|
|
||||||
float Acceleration = WaveSpeed * WaveSpeed * Laplacian
|
|
||||||
- FoundationStiffness * (Height - FoundationTarget) + Interaction + Breathing;
|
|
||||||
float NextVelocity = (Velocity + Acceleration * Dt) * exp(-Damping * Dt);
|
|
||||||
float NextHeight = Height + NextVelocity * Dt;
|
|
||||||
TargetState[Pixel] = float4(NextHeight, NextVelocity, NextImprint, 0.0);
|
|
||||||
}
|
|
||||||
@@ -322,108 +322,6 @@ bool FVoxelForgeClipmapNoOverlapTest::RunTest(const FString& Parameters)
|
|||||||
FirstFailure.IsEmpty() ? TEXT("none") : *FirstFailure));
|
FirstFailure.IsEmpty() ? TEXT("none") : *FirstFailure));
|
||||||
TestEqual(TEXT("all swept desired sets have zero overlapping pairs"), OverlapCases, 0);
|
TestEqual(TEXT("all swept desired sets have zero overlapping pairs"), OverlapCases, 0);
|
||||||
TestEqual(TEXT("all swept desired sets cover the legacy footprint"), CoverageFailures, 0);
|
TestEqual(TEXT("all swept desired sets cover the legacy footprint"), CoverageFailures, 0);
|
||||||
|
|
||||||
static const FIntVector DeltaCenters[] = {
|
|
||||||
FIntVector(-5, -2, -1), FIntVector(-3, 1, 2), FIntVector(-1, -3, 0),
|
|
||||||
FIntVector(0, 0, 0), FIntVector(1, 3, -2), FIntVector(3, -1, 1),
|
|
||||||
FIntVector(5, 2, -3),
|
|
||||||
};
|
|
||||||
static const FIntVector DeltaSteps[] = {
|
|
||||||
FIntVector(-1, 0, 0), FIntVector(1, 0, 0),
|
|
||||||
FIntVector(0, -1, 0), FIntVector(0, 1, 0),
|
|
||||||
FIntVector(0, 0, -1), FIntVector(0, 0, 1),
|
|
||||||
};
|
|
||||||
int32 DeltaCases = 0;
|
|
||||||
int32 DeltaFailures = 0;
|
|
||||||
FString FirstDeltaFailure;
|
|
||||||
for (const FIntVector& Center : DeltaCenters)
|
|
||||||
for (const FSweepSettings& Sweep : SweepSettings)
|
|
||||||
for (int32 ClampCase = 0; ClampCase < 3; ++ClampCase)
|
|
||||||
{
|
|
||||||
FParameters P;
|
|
||||||
P.ClipRadius = Sweep.Radius;
|
|
||||||
P.MaxClipLevel = Sweep.MaxLevel;
|
|
||||||
P.RenderDistanceChunks = Sweep.RenderDistance;
|
|
||||||
|
|
||||||
const auto GetVerticalBounds = [&](int32 CenterZ, int32& OutMinZ, int32& OutMaxZ)
|
|
||||||
{
|
|
||||||
OutMinZ = MIN_int32;
|
|
||||||
OutMaxZ = MAX_int32;
|
|
||||||
if (ClampCase == 1)
|
|
||||||
{
|
|
||||||
OutMinZ = CenterZ - 2;
|
|
||||||
OutMaxZ = CenterZ + 3;
|
|
||||||
}
|
|
||||||
else if (ClampCase == 2)
|
|
||||||
{
|
|
||||||
const int32 Band = VoxelClipmapDesiredTiles::FloorDiv(
|
|
||||||
CenterZ, 1 << Sweep.MaxLevel);
|
|
||||||
const int64 BandStart = static_cast<int64>(Band)
|
|
||||||
* (int64{1} << static_cast<uint32>(Sweep.MaxLevel));
|
|
||||||
OutMinZ = static_cast<int32>(BandStart - 1);
|
|
||||||
OutMaxZ = OutMinZ + 6;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
int32 PreviousMinZ = 0;
|
|
||||||
int32 PreviousMaxZ = 0;
|
|
||||||
GetVerticalBounds(Center.Z, PreviousMinZ, PreviousMaxZ);
|
|
||||||
TArray<FVoxelTileKey> PreviousTiles;
|
|
||||||
VoxelClipmapDesiredTiles::Build(Center, PreviousMinZ, PreviousMaxZ, P, PreviousTiles);
|
|
||||||
|
|
||||||
for (const FIntVector& Step : DeltaSteps)
|
|
||||||
{
|
|
||||||
const FIntVector NextCenter = Center + Step;
|
|
||||||
int32 NextMinZ = 0;
|
|
||||||
int32 NextMaxZ = 0;
|
|
||||||
GetVerticalBounds(NextCenter.Z, NextMinZ, NextMaxZ);
|
|
||||||
TArray<FVoxelTileKey> Entering;
|
|
||||||
TArray<FVoxelTileKey> Leaving;
|
|
||||||
VoxelClipmapDesiredTiles::BuildDelta(
|
|
||||||
Center, PreviousMinZ, PreviousMaxZ,
|
|
||||||
NextCenter, NextMinZ, NextMaxZ, P, Entering, Leaving);
|
|
||||||
|
|
||||||
TSet<FVoxelTileKey> Updated;
|
|
||||||
for (const FVoxelTileKey& Tile : PreviousTiles) Updated.Add(Tile);
|
|
||||||
for (const FVoxelTileKey& Tile : Leaving) Updated.Remove(Tile);
|
|
||||||
for (const FVoxelTileKey& Tile : Entering) Updated.Add(Tile);
|
|
||||||
|
|
||||||
TArray<FVoxelTileKey> FullNextTiles;
|
|
||||||
VoxelClipmapDesiredTiles::Build(
|
|
||||||
NextCenter, NextMinZ, NextMaxZ, P, FullNextTiles);
|
|
||||||
TSet<FVoxelTileKey> FullNextSet;
|
|
||||||
for (const FVoxelTileKey& Tile : FullNextTiles) FullNextSet.Add(Tile);
|
|
||||||
bool bMatches = Updated.Num() == FullNextSet.Num();
|
|
||||||
if (bMatches)
|
|
||||||
{
|
|
||||||
for (const FVoxelTileKey& Tile : Updated)
|
|
||||||
{
|
|
||||||
if (!FullNextSet.Contains(Tile))
|
|
||||||
{
|
|
||||||
bMatches = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!bMatches)
|
|
||||||
{
|
|
||||||
++DeltaFailures;
|
|
||||||
if (FirstDeltaFailure.IsEmpty())
|
|
||||||
{
|
|
||||||
FirstDeltaFailure = FString::Printf(
|
|
||||||
TEXT("center=(%d,%d,%d) step=(%d,%d,%d) radius=%d level=%d clamp=%d entering=%d leaving=%d full=%d updated=%d"),
|
|
||||||
Center.X, Center.Y, Center.Z, Step.X, Step.Y, Step.Z,
|
|
||||||
Sweep.Radius, Sweep.MaxLevel, ClampCase,
|
|
||||||
Entering.Num(), Leaving.Num(), FullNextSet.Num(), Updated.Num());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
++DeltaCases;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AddInfo(FString::Printf(
|
|
||||||
TEXT("clipmap delta cases=%d failures=%d first=%s"), DeltaCases, DeltaFailures,
|
|
||||||
FirstDeltaFailure.IsEmpty() ? TEXT("none") : *FirstDeltaFailure));
|
|
||||||
TestEqual(TEXT("clipmap delta update equals a full rebuild on every tested crossing"),
|
|
||||||
DeltaFailures, 0);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -399,7 +399,6 @@ bool FVoxelForgeComposerSeasonTest::RunTest(const FString& Parameters)
|
|||||||
LegacySettings->WorldRadiusVoxels = 987.0f;
|
LegacySettings->WorldRadiusVoxels = 987.0f;
|
||||||
LegacySettings->TotalStrates = 1;
|
LegacySettings->TotalStrates = 1;
|
||||||
LegacyDefinition->StrateHeightInChunks = 7;
|
LegacyDefinition->StrateHeightInChunks = 7;
|
||||||
LegacyDefinition->StrateHeightMeters = 7.0 * 8.0;
|
|
||||||
LegacySettings->StratePool.Add(
|
LegacySettings->StratePool.Add(
|
||||||
TSoftObjectPtr<UVoxelStrateDefinition>(LegacyDefinition.Get()));
|
TSoftObjectPtr<UVoxelStrateDefinition>(LegacyDefinition.Get()));
|
||||||
TStrongObjectPtr<UVoxelStrateManager> LegacyManager(
|
TStrongObjectPtr<UVoxelStrateManager> LegacyManager(
|
||||||
|
|||||||
@@ -525,7 +525,6 @@ bool FVoxelForgeZeroTransitionBlendTest::RunTest(const FString& Parameters)
|
|||||||
{
|
{
|
||||||
UVoxelStrateDefinition* Definition = World.Definitions[Index].Get();
|
UVoxelStrateDefinition* Definition = World.Definitions[Index].Get();
|
||||||
Definition->TransitionBlendChunks = 0;
|
Definition->TransitionBlendChunks = 0;
|
||||||
Definition->TransitionWidthMeters = 0.0;
|
|
||||||
Definition->TransitionType = (Index & 1) == 0
|
Definition->TransitionType = (Index & 1) == 0
|
||||||
? EVoxelStrateTransition::Gradient : EVoxelStrateTransition::Interleaved;
|
? EVoxelStrateTransition::Gradient : EVoxelStrateTransition::Interleaved;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ namespace
|
|||||||
Settings->Seed = LayoutSeed;
|
Settings->Seed = LayoutSeed;
|
||||||
Settings->TotalStrates = NumPoolDefinitions;
|
Settings->TotalStrates = NumPoolDefinitions;
|
||||||
Settings->InterStrateGapChunks = 1;
|
Settings->InterStrateGapChunks = 1;
|
||||||
Settings->InterStrateGapMeters = Settings->StrateLayoutChunkSizeMeters;
|
|
||||||
Settings->bOpenSurfaceEntry = true;
|
Settings->bOpenSurfaceEntry = true;
|
||||||
|
|
||||||
static const ECaveGeneratorType GeneratorTypes[] = {
|
static const ECaveGeneratorType GeneratorTypes[] = {
|
||||||
@@ -70,8 +69,6 @@ namespace
|
|||||||
FString::Printf(TEXT("Layout order test strate %d"), Index));
|
FString::Printf(TEXT("Layout order test strate %d"), Index));
|
||||||
Def->GeneratorType = GeneratorTypes[Index];
|
Def->GeneratorType = GeneratorTypes[Index];
|
||||||
Def->StrateHeightInChunks = 3 + Index;
|
Def->StrateHeightInChunks = 3 + Index;
|
||||||
Def->StrateHeightMeters = static_cast<double>(Def->StrateHeightInChunks)
|
|
||||||
* Settings->StrateLayoutChunkSizeMeters;
|
|
||||||
Def->TransitionType = EVoxelStrateTransition::Hard;
|
Def->TransitionType = EVoxelStrateTransition::Hard;
|
||||||
|
|
||||||
// Non-trivial, distinct passage configs exercise the connection index and the
|
// Non-trivial, distinct passage configs exercise the connection index and the
|
||||||
|
|||||||
@@ -79,8 +79,6 @@ namespace VoxelForgeTest
|
|||||||
NewObject<UVoxelSettings>(GetTransientPackage(), NAME_None, RF_Transient));
|
NewObject<UVoxelSettings>(GetTransientPackage(), NAME_None, RF_Transient));
|
||||||
Settings->Seed = InSeed;
|
Settings->Seed = InSeed;
|
||||||
Settings->InterStrateGapChunks = InGapChunks;
|
Settings->InterStrateGapChunks = InGapChunks;
|
||||||
Settings->InterStrateGapMeters = static_cast<double>(InGapChunks)
|
|
||||||
* Settings->StrateLayoutChunkSizeMeters;
|
|
||||||
// WorldRadiusVoxels/EdgeSealThickness intentionally inherit the UVoxelSettings defaults;
|
// WorldRadiusVoxels/EdgeSealThickness intentionally inherit the UVoxelSettings defaults;
|
||||||
// WorldEdgeSeal overrides them explicitly when it needs a small test world or radius 0.
|
// WorldEdgeSeal overrides them explicitly when it needs a small test world or radius 0.
|
||||||
|
|
||||||
@@ -112,8 +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);
|
||||||
Def->StrateHeightMeters = static_cast<double>(Def->StrateHeightInChunks)
|
|
||||||
* Settings->StrateLayoutChunkSizeMeters;
|
|
||||||
// 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;
|
||||||
|
|||||||
@@ -4,27 +4,6 @@
|
|||||||
|
|
||||||
namespace VoxelClipmapDesiredTiles
|
namespace VoxelClipmapDesiredTiles
|
||||||
{
|
{
|
||||||
FORCEINLINE int64 FloorDivWide(int64 Value, int64 Divisor)
|
|
||||||
{
|
|
||||||
if (Divisor <= 0) { return 0; }
|
|
||||||
const int64 Quotient = Value / Divisor;
|
|
||||||
const int64 Remainder = Value % Divisor;
|
|
||||||
return Remainder < 0 ? Quotient - 1 : Quotient;
|
|
||||||
}
|
|
||||||
|
|
||||||
FORCEINLINE int64 CeilDivWide(int64 Value, int64 Divisor)
|
|
||||||
{
|
|
||||||
if (Divisor <= 0) { return 0; }
|
|
||||||
const int64 Quotient = Value / Divisor;
|
|
||||||
const int64 Remainder = Value % Divisor;
|
|
||||||
return Remainder > 0 ? Quotient + 1 : Quotient;
|
|
||||||
}
|
|
||||||
|
|
||||||
FORCEINLINE int32 ClampInt32(int64 Value)
|
|
||||||
{
|
|
||||||
return static_cast<int32>(FMath::Clamp<int64>(Value, MIN_int32, MAX_int32));
|
|
||||||
}
|
|
||||||
|
|
||||||
struct FParameters
|
struct FParameters
|
||||||
{
|
{
|
||||||
int32 ClipRadius = 3;
|
int32 ClipRadius = 3;
|
||||||
@@ -34,10 +13,7 @@ namespace VoxelClipmapDesiredTiles
|
|||||||
|
|
||||||
FORCEINLINE int32 FloorDiv(int32 Value, int32 Divisor)
|
FORCEINLINE int32 FloorDiv(int32 Value, int32 Divisor)
|
||||||
{
|
{
|
||||||
if (Divisor <= 0) { return 0; }
|
return Value >= 0 ? (Value / Divisor) : -(((-Value) + Divisor - 1) / Divisor);
|
||||||
const int32 Quotient = Value / Divisor;
|
|
||||||
const int32 Remainder = Value % Divisor;
|
|
||||||
return Remainder < 0 ? Quotient - 1 : Quotient;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FORCEINLINE FIntVector FloorDiv(const FIntVector& Value, int32 Divisor)
|
FORCEINLINE FIntVector FloorDiv(const FIntVector& Value, int32 Divisor)
|
||||||
@@ -48,10 +24,7 @@ namespace VoxelClipmapDesiredTiles
|
|||||||
|
|
||||||
FORCEINLINE int32 CeilDiv(int32 Value, int32 Divisor)
|
FORCEINLINE int32 CeilDiv(int32 Value, int32 Divisor)
|
||||||
{
|
{
|
||||||
if (Divisor <= 0) { return 0; }
|
return -FloorDiv(-Value, Divisor);
|
||||||
const int32 Quotient = Value / Divisor;
|
|
||||||
const int32 Remainder = Value % Divisor;
|
|
||||||
return Remainder > 0 ? Quotient + 1 : Quotient;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FORCEINLINE void AlignedRingBounds(const FIntVector& Center, int32 Radius, int32 Alignment,
|
FORCEINLINE void AlignedRingBounds(const FIntVector& Center, int32 Radius, int32 Alignment,
|
||||||
@@ -60,12 +33,10 @@ namespace VoxelClipmapDesiredTiles
|
|||||||
const auto AlignAxis = [Radius, Alignment](int32 CenterAxis,
|
const auto AlignAxis = [Radius, Alignment](int32 CenterAxis,
|
||||||
int32& OutAxisMin, int32& OutAxisMax)
|
int32& OutAxisMin, int32& OutAxisMax)
|
||||||
{
|
{
|
||||||
const int64 SafeRadius = FMath::Max<int64>(0, Radius);
|
const int32 BaseMin = CenterAxis - Radius;
|
||||||
const int64 SafeAlignment = FMath::Max<int64>(1, Alignment);
|
const int32 BaseEnd = CenterAxis + Radius + 1;
|
||||||
const int64 BaseMin = static_cast<int64>(CenterAxis) - SafeRadius;
|
OutAxisMin = FloorDiv(BaseMin, Alignment) * Alignment;
|
||||||
const int64 BaseEnd = static_cast<int64>(CenterAxis) + SafeRadius + 1;
|
OutAxisMax = CeilDiv(BaseEnd, Alignment) * Alignment - 1;
|
||||||
OutAxisMin = ClampInt32(FloorDivWide(BaseMin, SafeAlignment) * SafeAlignment);
|
|
||||||
OutAxisMax = ClampInt32(CeilDivWide(BaseEnd, SafeAlignment) * SafeAlignment - 1);
|
|
||||||
};
|
};
|
||||||
AlignAxis(Center.X, OutMin.X, OutMax.X);
|
AlignAxis(Center.X, OutMin.X, OutMax.X);
|
||||||
AlignAxis(Center.Y, OutMin.Y, OutMax.Y);
|
AlignAxis(Center.Y, OutMin.Y, OutMax.Y);
|
||||||
@@ -78,13 +49,8 @@ namespace VoxelClipmapDesiredTiles
|
|||||||
{
|
{
|
||||||
return Radius;
|
return Radius;
|
||||||
}
|
}
|
||||||
const int32 SafeLevel = FMath::Clamp(MaxLevel, 0, 8);
|
return FMath::Max(Radius,
|
||||||
const int64 Scale = int64{1} << static_cast<uint32>(SafeLevel);
|
(Parameters.RenderDistanceChunks + (1 << MaxLevel) - 1) >> MaxLevel);
|
||||||
const int64 Numerator = static_cast<int64>(Parameters.RenderDistanceChunks)
|
|
||||||
+ Scale - 1;
|
|
||||||
const int64 DesiredRadius = Numerator / Scale;
|
|
||||||
return static_cast<int32>(FMath::Clamp<int64>(
|
|
||||||
FMath::Max<int64>(Radius, DesiredRadius), 0, MAX_int32));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FORCEINLINE void OuterShell(const FParameters& Parameters, int32 Radius, int32 MaxLevel,
|
FORCEINLINE void OuterShell(const FParameters& Parameters, int32 Radius, int32 MaxLevel,
|
||||||
@@ -107,12 +73,12 @@ namespace VoxelClipmapDesiredTiles
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
OutMinChunkZ = ClampInt32(static_cast<int64>(CenterChunkZ) - ViewDistanceDown);
|
OutMinChunkZ = CenterChunkZ - ViewDistanceDown;
|
||||||
OutMaxChunkZ = ClampInt32(static_cast<int64>(CenterChunkZ) + ViewDistanceUp);
|
OutMaxChunkZ = CenterChunkZ + ViewDistanceUp;
|
||||||
if (bHasStrate)
|
if (bHasStrate)
|
||||||
{
|
{
|
||||||
OutMinChunkZ = ClampInt32(static_cast<int64>(StrateBottomChunkZ) - StrateViewMarginChunks);
|
OutMinChunkZ = StrateBottomChunkZ - StrateViewMarginChunks;
|
||||||
OutMaxChunkZ = ClampInt32(static_cast<int64>(StrateTopChunkZ) + StrateViewMarginChunks);
|
OutMaxChunkZ = StrateTopChunkZ + StrateViewMarginChunks;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +94,7 @@ namespace VoxelClipmapDesiredTiles
|
|||||||
|
|
||||||
for (int32 Level = 0; Level <= MaxLevel; ++Level)
|
for (int32 Level = 0; Level <= MaxLevel; ++Level)
|
||||||
{
|
{
|
||||||
const int32 Pow = static_cast<int32>(uint32{1} << static_cast<uint32>(Level));
|
const int32 Pow = 1 << Level;
|
||||||
const FIntVector CenterLevel = FloorDiv(Center, Pow);
|
const FIntVector CenterLevel = FloorDiv(Center, Pow);
|
||||||
const int32 LevelRadius = (Level == MaxLevel) ? OuterRadius : Radius;
|
const int32 LevelRadius = (Level == MaxLevel) ? OuterRadius : Radius;
|
||||||
|
|
||||||
@@ -139,23 +105,24 @@ namespace VoxelClipmapDesiredTiles
|
|||||||
FIntVector RingMin, RingMax;
|
FIntVector RingMin, RingMax;
|
||||||
AlignedRingBounds(CenterLevel, LevelRadius, RingAlignment, RingMin, RingMax);
|
AlignedRingBounds(CenterLevel, LevelRadius, RingAlignment, RingMin, RingMax);
|
||||||
|
|
||||||
int64 TileMinZ = RingMin.Z;
|
int32 DeltaZMin = RingMin.Z - CenterLevel.Z;
|
||||||
int64 TileMaxZ = RingMax.Z;
|
int32 DeltaZMax = RingMax.Z - CenterLevel.Z;
|
||||||
if (LevelRadius > Radius && MinChunkZ != MIN_int32)
|
if (LevelRadius > Radius && MinChunkZ != MIN_int32)
|
||||||
{
|
{
|
||||||
TileMinZ = FMath::Max<int64>(TileMinZ, FloorDiv(MinChunkZ, Pow));
|
DeltaZMin = FMath::Max(DeltaZMin, FloorDiv(MinChunkZ, Pow) - CenterLevel.Z);
|
||||||
TileMaxZ = FMath::Min<int64>(TileMaxZ, FloorDiv(MaxChunkZ, Pow));
|
DeltaZMax = FMath::Min(DeltaZMax, FloorDiv(MaxChunkZ, Pow) - CenterLevel.Z);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int64 Z = TileMinZ; Z <= TileMaxZ; ++Z)
|
for (int32 DeltaZ = DeltaZMin; DeltaZ <= DeltaZMax; ++DeltaZ)
|
||||||
for (int64 Y = RingMin.Y; Y <= RingMax.Y; ++Y)
|
for (int32 DeltaY = RingMin.Y - CenterLevel.Y;
|
||||||
for (int64 X = RingMin.X; X <= RingMax.X; ++X)
|
DeltaY <= RingMax.Y - CenterLevel.Y; ++DeltaY)
|
||||||
|
for (int32 DeltaX = RingMin.X - CenterLevel.X;
|
||||||
|
DeltaX <= RingMax.X - CenterLevel.X; ++DeltaX)
|
||||||
{
|
{
|
||||||
const FIntVector Tile(static_cast<int32>(X), static_cast<int32>(Y),
|
const FIntVector Tile = CenterLevel + FIntVector(DeltaX, DeltaY, DeltaZ);
|
||||||
static_cast<int32>(Z));
|
const int32 TileMinZ = Tile.Z << Level;
|
||||||
const int64 TileBottomChunkZ = static_cast<int64>(Tile.Z) * Pow;
|
const int32 TileMaxZ = ((Tile.Z + 1) << Level) - 1;
|
||||||
const int64 TileTopChunkZ = (static_cast<int64>(Tile.Z) + 1) * Pow - 1;
|
if (TileMaxZ < MinChunkZ || TileMinZ > MaxChunkZ)
|
||||||
if (TileTopChunkZ < MinChunkZ || TileBottomChunkZ > MaxChunkZ)
|
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -163,12 +130,12 @@ namespace VoxelClipmapDesiredTiles
|
|||||||
if (Level > 0)
|
if (Level > 0)
|
||||||
{
|
{
|
||||||
const bool bCovered =
|
const bool bCovered =
|
||||||
(2 * static_cast<int64>(Tile.X) >= PreviousRingMin.X)
|
(2 * Tile.X >= PreviousRingMin.X)
|
||||||
&& (2 * static_cast<int64>(Tile.X) + 1 <= PreviousRingMax.X)
|
&& (2 * Tile.X + 1 <= PreviousRingMax.X)
|
||||||
&& (2 * static_cast<int64>(Tile.Y) >= PreviousRingMin.Y)
|
&& (2 * Tile.Y >= PreviousRingMin.Y)
|
||||||
&& (2 * static_cast<int64>(Tile.Y) + 1 <= PreviousRingMax.Y)
|
&& (2 * Tile.Y + 1 <= PreviousRingMax.Y)
|
||||||
&& (2 * static_cast<int64>(Tile.Z) >= PreviousRingMin.Z)
|
&& (2 * Tile.Z >= PreviousRingMin.Z)
|
||||||
&& (2 * static_cast<int64>(Tile.Z) + 1 <= PreviousRingMax.Z);
|
&& (2 * Tile.Z + 1 <= PreviousRingMax.Z);
|
||||||
if (bCovered)
|
if (bCovered)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@@ -182,218 +149,4 @@ namespace VoxelClipmapDesiredTiles
|
|||||||
PreviousRingMax = RingMax;
|
PreviousRingMax = RingMax;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute the exact key difference between two adjacent clipmap centers without visiting
|
|
||||||
// unchanged ring cells. A coarser ring can change membership when its finer child ring moves,
|
|
||||||
// even when the coarser ring bounds themselves stay fixed; the cover-box delta below includes
|
|
||||||
// those boundary keys as well.
|
|
||||||
FORCEINLINE void BuildDelta(
|
|
||||||
const FIntVector& PreviousCenter, int32 PreviousMinChunkZ, int32 PreviousMaxChunkZ,
|
|
||||||
const FIntVector& Center, int32 MinChunkZ, int32 MaxChunkZ,
|
|
||||||
const FParameters& Parameters,
|
|
||||||
TArray<FVoxelTileKey>& OutEntering, TArray<FVoxelTileKey>& OutLeaving)
|
|
||||||
{
|
|
||||||
struct FBox
|
|
||||||
{
|
|
||||||
FIntVector Min = FIntVector::ZeroValue;
|
|
||||||
FIntVector Max = FIntVector::ZeroValue;
|
|
||||||
bool bValid = false;
|
|
||||||
};
|
|
||||||
OutEntering.Reset();
|
|
||||||
OutLeaving.Reset();
|
|
||||||
|
|
||||||
const int32 Radius = FMath::Max(1, Parameters.ClipRadius);
|
|
||||||
const int32 MaxLevel = FMath::Clamp(Parameters.MaxClipLevel, 0, 8);
|
|
||||||
const int32 OuterRadius = OuterShellRadius(Parameters, Radius, MaxLevel);
|
|
||||||
FBox PreviousValid[9];
|
|
||||||
FBox CurrentValid[9];
|
|
||||||
FBox PreviousCovered[9];
|
|
||||||
FBox CurrentCovered[9];
|
|
||||||
const auto BuildRings = [&](const FIntVector& RingCenter, int32 RingMinChunkZ,
|
|
||||||
int32 RingMaxChunkZ, FBox* OutValid, FBox* OutCovered)
|
|
||||||
{
|
|
||||||
FIntVector ChildRingMin = FIntVector::ZeroValue;
|
|
||||||
FIntVector ChildRingMax = FIntVector::ZeroValue;
|
|
||||||
for (int32 Level = 0; Level <= MaxLevel; ++Level)
|
|
||||||
{
|
|
||||||
const int32 Pow = static_cast<int32>(uint32{1} << static_cast<uint32>(Level));
|
|
||||||
const FIntVector CenterLevel = FloorDiv(RingCenter, Pow);
|
|
||||||
const int32 LevelRadius = (Level == MaxLevel) ? OuterRadius : Radius;
|
|
||||||
const int32 RingAlignment = Level < MaxLevel ? 2 : 1;
|
|
||||||
FIntVector RingMin, RingMax;
|
|
||||||
AlignedRingBounds(CenterLevel, LevelRadius, RingAlignment, RingMin, RingMax);
|
|
||||||
FBox Valid;
|
|
||||||
Valid.Min = RingMin;
|
|
||||||
Valid.Max = RingMax;
|
|
||||||
Valid.Min.Z = FMath::Max(Valid.Min.Z, FloorDiv(RingMinChunkZ, Pow));
|
|
||||||
Valid.Max.Z = FMath::Min(Valid.Max.Z, FloorDiv(RingMaxChunkZ, Pow));
|
|
||||||
Valid.bValid = Valid.Min.X <= Valid.Max.X
|
|
||||||
&& Valid.Min.Y <= Valid.Max.Y && Valid.Min.Z <= Valid.Max.Z;
|
|
||||||
OutValid[Level] = Valid;
|
|
||||||
|
|
||||||
FBox Covered;
|
|
||||||
if (Level > 0)
|
|
||||||
{
|
|
||||||
Covered.Min = FIntVector(
|
|
||||||
CeilDiv(ChildRingMin.X, 2), CeilDiv(ChildRingMin.Y, 2),
|
|
||||||
CeilDiv(ChildRingMin.Z, 2));
|
|
||||||
Covered.Max = FIntVector(
|
|
||||||
ClampInt32(FloorDivWide(static_cast<int64>(ChildRingMax.X) - 1, 2)),
|
|
||||||
ClampInt32(FloorDivWide(static_cast<int64>(ChildRingMax.Y) - 1, 2)),
|
|
||||||
ClampInt32(FloorDivWide(static_cast<int64>(ChildRingMax.Z) - 1, 2)));
|
|
||||||
Covered.bValid = Covered.Min.X <= Covered.Max.X
|
|
||||||
&& Covered.Min.Y <= Covered.Max.Y && Covered.Min.Z <= Covered.Max.Z;
|
|
||||||
}
|
|
||||||
OutCovered[Level] = Covered;
|
|
||||||
ChildRingMin = RingMin;
|
|
||||||
ChildRingMax = RingMax;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
BuildRings(PreviousCenter, PreviousMinChunkZ, PreviousMaxChunkZ,
|
|
||||||
PreviousValid, PreviousCovered);
|
|
||||||
BuildRings(Center, MinChunkZ, MaxChunkZ, CurrentValid, CurrentCovered);
|
|
||||||
|
|
||||||
const auto Intersect = [](const FBox& A, const FBox& B)
|
|
||||||
{
|
|
||||||
FBox Result;
|
|
||||||
if (!A.bValid || !B.bValid) return Result;
|
|
||||||
Result.Min = FIntVector(
|
|
||||||
FMath::Max(A.Min.X, B.Min.X), FMath::Max(A.Min.Y, B.Min.Y),
|
|
||||||
FMath::Max(A.Min.Z, B.Min.Z));
|
|
||||||
Result.Max = FIntVector(
|
|
||||||
FMath::Min(A.Max.X, B.Max.X), FMath::Min(A.Max.Y, B.Max.Y),
|
|
||||||
FMath::Min(A.Max.Z, B.Max.Z));
|
|
||||||
Result.bValid = Result.Min.X <= Result.Max.X
|
|
||||||
&& Result.Min.Y <= Result.Max.Y && Result.Min.Z <= Result.Max.Z;
|
|
||||||
return Result;
|
|
||||||
};
|
|
||||||
const auto AppendDifference = [&Intersect](const FBox& A, const FBox& B, auto& OutBoxes)
|
|
||||||
{
|
|
||||||
if (!A.bValid) return;
|
|
||||||
const FBox Common = Intersect(A, B);
|
|
||||||
if (!Common.bValid)
|
|
||||||
{
|
|
||||||
OutBoxes.Add(A);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Six disjoint slabs cover A minus Common. The later slabs are narrowed to the
|
|
||||||
// common extent on each already-partitioned axis, so no key is emitted twice.
|
|
||||||
FBox Slab = A;
|
|
||||||
if (A.Min.X < Common.Min.X)
|
|
||||||
{
|
|
||||||
Slab.Max.X = Common.Min.X - 1;
|
|
||||||
Slab.bValid = Slab.Min.X <= Slab.Max.X;
|
|
||||||
if (Slab.bValid) OutBoxes.Add(Slab);
|
|
||||||
}
|
|
||||||
Slab = A;
|
|
||||||
if (Common.Max.X < A.Max.X)
|
|
||||||
{
|
|
||||||
Slab.Min.X = Common.Max.X + 1;
|
|
||||||
Slab.bValid = Slab.Min.X <= Slab.Max.X;
|
|
||||||
if (Slab.bValid) OutBoxes.Add(Slab);
|
|
||||||
}
|
|
||||||
|
|
||||||
Slab = A;
|
|
||||||
Slab.Min.X = Common.Min.X;
|
|
||||||
Slab.Max.X = Common.Max.X;
|
|
||||||
if (A.Min.Y < Common.Min.Y)
|
|
||||||
{
|
|
||||||
Slab.Max.Y = Common.Min.Y - 1;
|
|
||||||
Slab.bValid = Slab.Min.Y <= Slab.Max.Y;
|
|
||||||
if (Slab.bValid) OutBoxes.Add(Slab);
|
|
||||||
}
|
|
||||||
Slab = A;
|
|
||||||
Slab.Min.X = Common.Min.X;
|
|
||||||
Slab.Max.X = Common.Max.X;
|
|
||||||
if (Common.Max.Y < A.Max.Y)
|
|
||||||
{
|
|
||||||
Slab.Min.Y = Common.Max.Y + 1;
|
|
||||||
Slab.bValid = Slab.Min.Y <= Slab.Max.Y;
|
|
||||||
if (Slab.bValid) OutBoxes.Add(Slab);
|
|
||||||
}
|
|
||||||
|
|
||||||
Slab = A;
|
|
||||||
Slab.Min.X = Common.Min.X;
|
|
||||||
Slab.Max.X = Common.Max.X;
|
|
||||||
Slab.Min.Y = Common.Min.Y;
|
|
||||||
Slab.Max.Y = Common.Max.Y;
|
|
||||||
if (A.Min.Z < Common.Min.Z)
|
|
||||||
{
|
|
||||||
Slab.Max.Z = Common.Min.Z - 1;
|
|
||||||
Slab.bValid = Slab.Min.Z <= Slab.Max.Z;
|
|
||||||
if (Slab.bValid) OutBoxes.Add(Slab);
|
|
||||||
}
|
|
||||||
Slab = A;
|
|
||||||
Slab.Min.X = Common.Min.X;
|
|
||||||
Slab.Max.X = Common.Max.X;
|
|
||||||
Slab.Min.Y = Common.Min.Y;
|
|
||||||
Slab.Max.Y = Common.Max.Y;
|
|
||||||
if (Common.Max.Z < A.Max.Z)
|
|
||||||
{
|
|
||||||
Slab.Min.Z = Common.Max.Z + 1;
|
|
||||||
Slab.bValid = Slab.Min.Z <= Slab.Max.Z;
|
|
||||||
if (Slab.bValid) OutBoxes.Add(Slab);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const auto Contains = [](const FBox& Box, const FIntVector& Point)
|
|
||||||
{
|
|
||||||
return Box.bValid && Point.X >= Box.Min.X && Point.X <= Box.Max.X
|
|
||||||
&& Point.Y >= Box.Min.Y && Point.Y <= Box.Max.Y
|
|
||||||
&& Point.Z >= Box.Min.Z && Point.Z <= Box.Max.Z;
|
|
||||||
};
|
|
||||||
const auto ForEachBox = [](const FBox& Box, const auto& Callback)
|
|
||||||
{
|
|
||||||
if (!Box.bValid) return;
|
|
||||||
for (int64 Z = Box.Min.Z; Z <= Box.Max.Z; ++Z)
|
|
||||||
for (int64 Y = Box.Min.Y; Y <= Box.Max.Y; ++Y)
|
|
||||||
for (int64 X = Box.Min.X; X <= Box.Max.X; ++X)
|
|
||||||
{
|
|
||||||
Callback(FIntVector(static_cast<int32>(X), static_cast<int32>(Y),
|
|
||||||
static_cast<int32>(Z)));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (int32 Level = 0; Level <= MaxLevel; ++Level)
|
|
||||||
{
|
|
||||||
TArray<FBox, TInlineAllocator<16>> ChangedBoxes;
|
|
||||||
AppendDifference(CurrentValid[Level], PreviousValid[Level], ChangedBoxes);
|
|
||||||
AppendDifference(PreviousValid[Level], CurrentValid[Level], ChangedBoxes);
|
|
||||||
|
|
||||||
if (Level > 0)
|
|
||||||
{
|
|
||||||
const FBox CommonValid = Intersect(CurrentValid[Level], PreviousValid[Level]);
|
|
||||||
if (CommonValid.bValid)
|
|
||||||
{
|
|
||||||
TArray<FBox, TInlineAllocator<16>> CoverDifferences;
|
|
||||||
AppendDifference(CurrentCovered[Level], PreviousCovered[Level], CoverDifferences);
|
|
||||||
AppendDifference(PreviousCovered[Level], CurrentCovered[Level], CoverDifferences);
|
|
||||||
for (const FBox& CoverDifference : CoverDifferences)
|
|
||||||
{
|
|
||||||
const FBox ChangedCover = Intersect(CoverDifference, CommonValid);
|
|
||||||
if (ChangedCover.bValid) ChangedBoxes.Add(ChangedCover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const FBox& ChangedBox : ChangedBoxes)
|
|
||||||
{
|
|
||||||
ForEachBox(ChangedBox, [&](const FIntVector& Coord)
|
|
||||||
{
|
|
||||||
const bool bCurrentMember = Contains(CurrentValid[Level], Coord)
|
|
||||||
&& !Contains(CurrentCovered[Level], Coord);
|
|
||||||
const bool bPreviousMember = Contains(PreviousValid[Level], Coord)
|
|
||||||
&& !Contains(PreviousCovered[Level], Coord);
|
|
||||||
if (bCurrentMember && !bPreviousMember)
|
|
||||||
{
|
|
||||||
OutEntering.Emplace(Coord, Level);
|
|
||||||
}
|
|
||||||
else if (bPreviousMember && !bCurrentMember)
|
|
||||||
{
|
|
||||||
OutLeaving.Emplace(Coord, Level);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,6 @@
|
|||||||
#include "Materials/MaterialInterface.h"
|
#include "Materials/MaterialInterface.h"
|
||||||
#include "Tasks/Task.h"
|
#include "Tasks/Task.h"
|
||||||
#include "HAL/PlatformProcess.h"
|
#include "HAL/PlatformProcess.h"
|
||||||
#include "Misc/Crc.h"
|
|
||||||
#include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes
|
#include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes
|
||||||
#include <VoxelForgeCore/VoxelForgeCore.h>
|
#include <VoxelForgeCore/VoxelForgeCore.h>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
@@ -241,10 +240,10 @@ static std::vector<VoxelForgeCore::WorldAssemblyFeatureQueryV1> VF_ContentFeatur
|
|||||||
{
|
{
|
||||||
using namespace VoxelForgeCore;
|
using namespace VoxelForgeCore;
|
||||||
constexpr double Q = WorldAssemblySubvoxelsPerVoxel;
|
constexpr double Q = WorldAssemblySubvoxelsPerVoxel;
|
||||||
const std::array<std::int64_t,3> Min{static_cast<std::int64_t>(std::floor(MinX*Q)),
|
const std::array<int64,3> Min{static_cast<int64>(std::floor(MinX*Q)),
|
||||||
static_cast<std::int64_t>(std::floor(MinY*Q)),static_cast<std::int64_t>(std::floor(MinZ*Q))};
|
static_cast<int64>(std::floor(MinY*Q)),static_cast<int64>(std::floor(MinZ*Q))};
|
||||||
const std::array<std::int64_t,3> Max{static_cast<std::int64_t>(std::ceil(MaxX*Q)),
|
const std::array<int64,3> Max{static_cast<int64>(std::ceil(MaxX*Q)),
|
||||||
static_cast<std::int64_t>(std::ceil(MaxY*Q)),static_cast<std::int64_t>(std::ceil(MaxZ*Q))};
|
static_cast<int64>(std::ceil(MaxY*Q)),static_cast<int64>(std::ceil(MaxZ*Q))};
|
||||||
std::vector<WorldAssemblyFeatureQueryV1> Queries;
|
std::vector<WorldAssemblyFeatureQueryV1> Queries;
|
||||||
if (Surface)
|
if (Surface)
|
||||||
if (const auto* Plan = Surface->FindPlan(StrateIndex))
|
if (const auto* Plan = Surface->FindPlan(StrateIndex))
|
||||||
@@ -323,54 +322,11 @@ void UVoxelContentManager::Initialize(AActor* InOwner, UVoxelStrateManager* InSt
|
|||||||
Seed = InSeed;
|
Seed = InSeed;
|
||||||
NativeSurfaceSnapshot.Reset();
|
NativeSurfaceSnapshot.Reset();
|
||||||
NativeFloatingIslandSnapshot.Reset();
|
NativeFloatingIslandSnapshot.Reset();
|
||||||
CoreGeometryAnchors.Reset();
|
|
||||||
|
|
||||||
if (Settings)
|
if (Settings)
|
||||||
{
|
{
|
||||||
FString ManifestJson, ManifestHash, ManifestError;
|
FString ManifestJson, ManifestHash, ManifestError;
|
||||||
const AVoxelWorld* VoxelWorld = Cast<AVoxelWorld>(Owner.Get());
|
const AVoxelWorld* VoxelWorld = Cast<AVoxelWorld>(Owner.Get());
|
||||||
FString ResolvedLayoutJson, ResolvedLayoutIdentity;
|
|
||||||
if (VoxelWorld != nullptr
|
|
||||||
&& VoxelWorld->GetVoxelForgeCoreResolvedLayout(ResolvedLayoutJson, ResolvedLayoutIdentity))
|
|
||||||
{
|
|
||||||
FTCHARToUTF8 Utf8Layout(*ResolvedLayoutJson);
|
|
||||||
std::string LayoutError;
|
|
||||||
const std::optional<VoxelForgeCore::ResolvedLayoutSnapshotV3> ResolvedLayout =
|
|
||||||
VoxelForgeCore::ResolvedLayoutSnapshotV3::FromJson(
|
|
||||||
std::string_view(Utf8Layout.Get(), static_cast<std::size_t>(Utf8Layout.Length())), LayoutError);
|
|
||||||
if (ResolvedLayout)
|
|
||||||
{
|
|
||||||
const auto AddAnchor = [this](const std::string& StableId, int32 StrateIndex,
|
|
||||||
const std::array<std::int64_t, 3>& PositionMicrometres)
|
|
||||||
{
|
|
||||||
const FVector PositionCm(
|
|
||||||
static_cast<double>(PositionMicrometres[0]) / 10000.0,
|
|
||||||
static_cast<double>(PositionMicrometres[1]) / 10000.0,
|
|
||||||
static_cast<double>(PositionMicrometres[2]) / 10000.0);
|
|
||||||
const FString FullName = UTF8_TO_TCHAR(StableId.c_str());
|
|
||||||
FResolvedCoreGeometryAnchor Anchor{ StrateIndex, PositionCm };
|
|
||||||
CoreGeometryAnchors.Add(FullName, Anchor);
|
|
||||||
const int32 Marker = FullName.Find(TEXT("/core/"), ESearchCase::CaseSensitive, ESearchDir::FromEnd);
|
|
||||||
if (Marker != INDEX_NONE)
|
|
||||||
{
|
|
||||||
const FString ShortName = FullName.Mid(Marker + 6);
|
|
||||||
if (!ShortName.IsEmpty()) CoreGeometryAnchors.Add(ShortName, Anchor);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
for (const VoxelForgeCore::ResolvedLayoutStampV1& Stamp : ResolvedLayout->stamps)
|
|
||||||
AddAnchor(Stamp.stableId, Stamp.strateIndex, Stamp.positionMicrometres);
|
|
||||||
for (const VoxelForgeCore::ResolvedCavernRoomV1& Room : ResolvedLayout->cavernRooms)
|
|
||||||
AddAnchor(Room.stableId, Room.strateIndex, Room.centerMicrometres);
|
|
||||||
for (const VoxelForgeCore::ResolvedCavernEdgeV1& Edge : ResolvedLayout->cavernEdges)
|
|
||||||
if (!Edge.polylineMicrometres.empty())
|
|
||||||
AddAnchor(Edge.stableId, Edge.strateIndex, Edge.polylineMicrometres.front());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
UE_LOG(LogTemp, Warning, TEXT("[VoxelContentManager] Could not read resolved Core geometry anchors: %s"),
|
|
||||||
UTF8_TO_TCHAR(LayoutError.c_str()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const bool bHasRuntimeAssembly = VoxelWorld != nullptr
|
const bool bHasRuntimeAssembly = VoxelWorld != nullptr
|
||||||
&& VoxelWorld->GetVoxelForgeCoreWorldAssembly(ManifestJson, ManifestHash);
|
&& VoxelWorld->GetVoxelForgeCoreWorldAssembly(ManifestJson, ManifestHash);
|
||||||
if (!bHasRuntimeAssembly)
|
if (!bHasRuntimeAssembly)
|
||||||
@@ -603,13 +559,9 @@ int32 UVoxelContentManager::RegionSize() const
|
|||||||
// truncation, a negative-coord cell is queued under one region but routed back to another → its march
|
// truncation, a negative-coord cell is queued under one region but routed back to another → its march
|
||||||
// result is dropped → blank chunk. Truncation == floor for A>=0 and for R==1, which is exactly why only
|
// result is dropped → blank chunk. Truncation == floor for A>=0 and for R==1, which is exactly why only
|
||||||
// NEGATIVE coordinates at R>=2 were affected. R (region size) is always > 0 here.
|
// NEGATIVE coordinates at R>=2 were affected. R (region size) is always > 0 here.
|
||||||
static FORCEINLINE int32 FloorDivPos(int64 A, int32 R)
|
static FORCEINLINE int32 FloorDivPos(int32 A, int32 R)
|
||||||
{
|
{
|
||||||
if (R <= 0) { return 0; }
|
return (A >= 0) ? (A / R) : -(((-A) + R - 1) / R);
|
||||||
const int64 Quotient = A / R;
|
|
||||||
const int64 Remainder = A % R;
|
|
||||||
return static_cast<int32>(FMath::Clamp<int64>(
|
|
||||||
Remainder < 0 ? Quotient - 1 : Quotient, MIN_int32, MAX_int32));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static FORCEINLINE FIntPoint CellToRegion(const FIntPoint& Cell, int32 R)
|
static FORCEINLINE FIntPoint CellToRegion(const FIntPoint& Cell, int32 R)
|
||||||
@@ -621,10 +573,10 @@ static FORCEINLINE FIntPoint CellToRegion(const FIntPoint& Cell, int32 R)
|
|||||||
static FORCEINLINE bool IsRegionDesired(const FIntPoint& Region, const FIntPoint& PlayerCell,
|
static FORCEINLINE bool IsRegionDesired(const FIntPoint& Region, const FIntPoint& PlayerCell,
|
||||||
int32 FarR, int32 R)
|
int32 FarR, int32 R)
|
||||||
{
|
{
|
||||||
const int32 MinX = FloorDivPos(static_cast<int64>(PlayerCell.X) - FarR, R);
|
const int32 MinX = FloorDivPos(PlayerCell.X - FarR, R);
|
||||||
const int32 MaxX = FloorDivPos(static_cast<int64>(PlayerCell.X) + FarR, R);
|
const int32 MaxX = FloorDivPos(PlayerCell.X + FarR, R);
|
||||||
const int32 MinY = FloorDivPos(static_cast<int64>(PlayerCell.Y) - FarR, R);
|
const int32 MinY = FloorDivPos(PlayerCell.Y - FarR, R);
|
||||||
const int32 MaxY = FloorDivPos(static_cast<int64>(PlayerCell.Y) + FarR, R);
|
const int32 MaxY = FloorDivPos(PlayerCell.Y + FarR, R);
|
||||||
return Region.X >= MinX && Region.X <= MaxX && Region.Y >= MinY && Region.Y <= MaxY;
|
return Region.X >= MinX && Region.X <= MaxX && Region.Y >= MinY && Region.Y <= MaxY;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1086,19 +1038,8 @@ bool UVoxelContentManager::MeasurePlacementCostForValidation(
|
|||||||
OutAudit.MaximumDensityResidual=FMath::Max(OutAudit.MaximumDensityResidual,
|
OutAudit.MaximumDensityResidual=FMath::Max(OutAudit.MaximumDensityResidual,
|
||||||
FMath::Abs(static_cast<double>(Density)));
|
FMath::Abs(static_cast<double>(Density)));
|
||||||
++OutAudit.AnchorChecks;
|
++OutAudit.AnchorChecks;
|
||||||
// Core content crossings use the same finite-step column refinement as runtime
|
if (FMath::Abs(Density)>0.25f)
|
||||||
// placement. Allow at most half a Core voxel of field residual in this audit.
|
{ OutError=TEXT("Populated fixture is not anchored on the canonical field."); return false; }
|
||||||
// Les croisements de contenu Core gardent le raffinement discret du runtime;
|
|
||||||
// l'audit tolere au plus un demi-voxel Core de residu de champ.
|
|
||||||
if (FMath::Abs(Density)>0.5f)
|
|
||||||
{
|
|
||||||
OutError=FString::Printf(
|
|
||||||
TEXT("Populated fixture is not anchored on the canonical field: residual=%.4f core_vox=(%.3f,%.3f,%.3f) local_cm=(%.2f,%.2f,%.2f)."),
|
|
||||||
Density, Local.X/CurrentCtx.VoxelSizeCm,
|
|
||||||
Local.Y/CurrentCtx.VoxelSizeCm, Local.Z/CurrentCtx.VoxelSizeCm,
|
|
||||||
Local.X, Local.Y, Local.Z);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!Spawns.IsEmpty())
|
if (!Spawns.IsEmpty())
|
||||||
@@ -1161,22 +1102,17 @@ void UVoxelContentManager::RebuildDesiredCells(FDecoGrid& G, const FIntPoint& Pl
|
|||||||
const int32 R = RegionSize();
|
const int32 R = RegionSize();
|
||||||
|
|
||||||
// Desired regions = every region whose footprint touches the radius-FarR cell box around the player.
|
// Desired regions = every region whose footprint touches the radius-FarR cell box around the player.
|
||||||
const FIntPoint RMin(FloorDivPos(static_cast<int64>(PlayerCell.X) - FarR, R),
|
const FIntPoint RMin(FloorDivPos(PlayerCell.X - FarR, R),
|
||||||
FloorDivPos(static_cast<int64>(PlayerCell.Y) - FarR, R));
|
FloorDivPos(PlayerCell.Y - FarR, R));
|
||||||
const FIntPoint RMax(FloorDivPos(static_cast<int64>(PlayerCell.X) + FarR, R),
|
const FIntPoint RMax(FloorDivPos(PlayerCell.X + FarR, R),
|
||||||
FloorDivPos(static_cast<int64>(PlayerCell.Y) + FarR, R));
|
FloorDivPos(PlayerCell.Y + FarR, R));
|
||||||
|
|
||||||
TSet<FIntPoint> DesiredRegions;
|
TSet<FIntPoint> DesiredRegions;
|
||||||
constexpr int64 MaxRegionReserve = 4096;
|
DesiredRegions.Reserve((RMax.X - RMin.X + 1) * (RMax.Y - RMin.Y + 1));
|
||||||
const int64 WidthX = static_cast<int64>(RMax.X) - RMin.X + 1;
|
for (int32 ry = RMin.Y; ry <= RMax.Y; ++ry)
|
||||||
const int64 WidthY = static_cast<int64>(RMax.Y) - RMin.Y + 1;
|
for (int32 rx = RMin.X; rx <= RMax.X; ++rx)
|
||||||
const int64 ReserveCount = WidthY > 0 && WidthX > MaxRegionReserve / WidthY
|
|
||||||
? MaxRegionReserve : FMath::Max<int64>(0, WidthX * WidthY);
|
|
||||||
DesiredRegions.Reserve(static_cast<int32>(ReserveCount));
|
|
||||||
for (int64 ry = RMin.Y; ry <= RMax.Y; ++ry)
|
|
||||||
for (int64 rx = RMin.X; rx <= RMax.X; ++rx)
|
|
||||||
{
|
{
|
||||||
DesiredRegions.Add(FIntPoint(static_cast<int32>(rx), static_cast<int32>(ry)));
|
DesiredRegions.Add(FIntPoint(rx, ry));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unload loaded regions no longer desired (plain DestroyComponent — no per-instance removal).
|
// Unload loaded regions no longer desired (plain DestroyComponent — no per-instance removal).
|
||||||
@@ -1528,15 +1464,8 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
|
|||||||
const uint32 CH = VoxelHash::Mix(H ^ (0x00C0FFEEu + (uint32)ci * 0x9E3779B1u));
|
const uint32 CH = VoxelHash::Mix(H ^ (0x00C0FFEEu + (uint32)ci * 0x9E3779B1u));
|
||||||
if (VoxelHash::ToFloat01(CH) > Comp.Probability) continue;
|
if (VoxelHash::ToFloat01(CH) > Comp.Probability) continue;
|
||||||
|
|
||||||
const int64 Span = FMath::Max<int64>(0,
|
const int32 Span = FMath::Max(0, Comp.CountMax - Comp.CountMin);
|
||||||
static_cast<int64>(Comp.CountMax) - Comp.CountMin);
|
const int32 Count = Comp.CountMin + (int32)(VoxelHash::ToFloat01(VoxelHash::Mix(CH ^ 0x1234u)) * (float)(Span + 1));
|
||||||
const uint64 CountRange = static_cast<uint64>(Span) + 1u;
|
|
||||||
const float CountOffset = VoxelHash::ToFloat01(
|
|
||||||
VoxelHash::Mix(CH ^ 0x1234u)) * static_cast<float>(CountRange);
|
|
||||||
const int64 CountWide = static_cast<int64>(Comp.CountMin)
|
|
||||||
+ static_cast<int64>(CountOffset);
|
|
||||||
const int32 Count = static_cast<int32>(FMath::Clamp<int64>(
|
|
||||||
CountWide, MIN_int32, MAX_int32));
|
|
||||||
for (int32 ii = 0; ii < Count; ++ii)
|
for (int32 ii = 0; ii < Count; ++ii)
|
||||||
{
|
{
|
||||||
if (CompBudget <= 0) break;
|
if (CompBudget <= 0) break;
|
||||||
@@ -1585,15 +1514,8 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
|
|||||||
const uint32 SH = VoxelHash::Mix(IH ^ (0x0000544Bu + (uint32)si * 0x27D4EB2Fu));
|
const uint32 SH = VoxelHash::Mix(IH ^ (0x0000544Bu + (uint32)si * 0x27D4EB2Fu));
|
||||||
if (VoxelHash::ToFloat01(SH) > Sub.Probability) continue;
|
if (VoxelHash::ToFloat01(SH) > Sub.Probability) continue;
|
||||||
|
|
||||||
const int64 SubSpan = FMath::Max<int64>(0,
|
const int32 SubSpan = FMath::Max(0, Sub.CountMax - Sub.CountMin);
|
||||||
static_cast<int64>(Sub.CountMax) - Sub.CountMin);
|
const int32 SubCount = Sub.CountMin + (int32)(VoxelHash::ToFloat01(VoxelHash::Mix(SH ^ 0x1234u)) * (float)(SubSpan + 1));
|
||||||
const uint64 SubCountRange = static_cast<uint64>(SubSpan) + 1u;
|
|
||||||
const float SubCountOffset = VoxelHash::ToFloat01(
|
|
||||||
VoxelHash::Mix(SH ^ 0x1234u)) * static_cast<float>(SubCountRange);
|
|
||||||
const int64 SubCountWide = static_cast<int64>(Sub.CountMin)
|
|
||||||
+ static_cast<int64>(SubCountOffset);
|
|
||||||
const int32 SubCount = static_cast<int32>(FMath::Clamp<int64>(
|
|
||||||
SubCountWide, MIN_int32, MAX_int32));
|
|
||||||
for (int32 sj = 0; sj < SubCount; ++sj)
|
for (int32 sj = 0; sj < SubCount; ++sj)
|
||||||
{
|
{
|
||||||
if (CompBudget <= 0) break;
|
if (CompBudget <= 0) break;
|
||||||
@@ -2452,28 +2374,6 @@ void UVoxelContentManager::UpdateLandmarks(const FVector& PlayerWorldPos)
|
|||||||
const float ExclWorld = FMath::Max(0.0f, L.ExclusionRadiusChunks) * ChunkWorld;
|
const float ExclWorld = FMath::Max(0.0f, L.ExclusionRadiusChunks) * ChunkWorld;
|
||||||
if (ExclWorld > 0.0f) bAnyExclusion = true;
|
if (ExclWorld > 0.0f) bAnyExclusion = true;
|
||||||
|
|
||||||
if (!L.CoreGeometryStableName.IsEmpty())
|
|
||||||
{
|
|
||||||
const FResolvedCoreGeometryAnchor* Anchor = CoreGeometryAnchors.Find(L.CoreGeometryStableName);
|
|
||||||
if (Anchor == nullptr || Anchor->StrateIndex != StrateIndex) continue;
|
|
||||||
const uint32 StableNameHash = FCrc::StrCrc32(*L.CoreGeometryStableName);
|
|
||||||
const uint32 H = DecoHash(static_cast<int32>(StableNameHash), StrateIndex,
|
|
||||||
0, 0, 0, EntryIdx, LocalSeed, 0xC04E6E0u);
|
|
||||||
if (VoxelHash::ToFloat01(H) > L.SpawnProbability) continue;
|
|
||||||
const float LocalX = static_cast<float>(Anchor->LocalPositionCm.X);
|
|
||||||
const float LocalY = static_cast<float>(Anchor->LocalPositionCm.Y);
|
|
||||||
const float dx = LocalX - LocalPlayer.X, dy = LocalY - LocalPlayer.Y;
|
|
||||||
const float DistSq = dx * dx + dy * dy;
|
|
||||||
if (DistSq <= GatherWorld * GatherWorld)
|
|
||||||
{
|
|
||||||
// A Core-bound landmark is one stable candidate, independent of the hash lattice.
|
|
||||||
// Un landmark lie a Core reste un candidat stable, independant de la grille de hachage.
|
|
||||||
Cands.Add({ FIntVector(EntryIdx, 0, EntryIdx), EntryIdx, H, LocalX, LocalY,
|
|
||||||
ExclWorld, L.Priority, DistSq <= RadiusWorld * RadiusWorld });
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (L.AnchorMode == ELandmarkAnchor::HashLattice)
|
if (L.AnchorMode == ELandmarkAnchor::HashLattice)
|
||||||
{
|
{
|
||||||
const float SpacingChunks = FMath::Max(1.0f, L.SpacingChunks);
|
const float SpacingChunks = FMath::Max(1.0f, L.SpacingChunks);
|
||||||
|
|||||||
@@ -155,11 +155,9 @@ int32 UVoxelDensityVolume::NumLevels() const
|
|||||||
|
|
||||||
FORCEINLINE int32 UVoxelDensityVolume::FloorDiv(int32 A, int32 B)
|
FORCEINLINE int32 UVoxelDensityVolume::FloorDiv(int32 A, int32 B)
|
||||||
{
|
{
|
||||||
// True floor division (B > 0). Remainder adjustment handles INT_MIN without negating it.
|
// True floor division (B > 0). FMath::DivideAndRoundDown truncates toward zero for negatives —
|
||||||
if (B <= 0) { return 0; }
|
// a footgun the content manager hit too (see FloorDivPos there). Cells span the origin, so floor.
|
||||||
const int32 Quotient = A / B;
|
return (A >= 0) ? (A / B) : -(((-A) + B - 1) / B);
|
||||||
const int32 Remainder = A % B;
|
|
||||||
return Remainder < 0 ? Quotient - 1 : Quotient;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FORCEINLINE uint8 UVoxelDensityVolume::Quantize(float MCDensity)
|
FORCEINLINE uint8 UVoxelDensityVolume::Quantize(float MCDensity)
|
||||||
@@ -182,8 +180,7 @@ void UVoxelDensityVolume::EnsureAllocated()
|
|||||||
for (int32 L = 0; L < N; ++L)
|
for (int32 L = 0; L < N; ++L)
|
||||||
{
|
{
|
||||||
FClipLevel& Lv = Levels[L];
|
FClipLevel& Lv = Levels[L];
|
||||||
Lv.Step = static_cast<int32>(uint32{1}
|
Lv.Step = 1 << L;
|
||||||
<< static_cast<uint32>(FMath::Clamp(L, 0, 8)));
|
|
||||||
Lv.OriginCells = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX);
|
Lv.OriginCells = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX);
|
||||||
Lv.bHasData = false;
|
Lv.bHasData = false;
|
||||||
Lv.Density.SetNumZeroed(Count); // start all-air (0)
|
Lv.Density.SetNumZeroed(Count); // start all-air (0)
|
||||||
@@ -635,8 +632,7 @@ void UVoxelDensityVolume::ProcessOneFill(const FPendingFill& F)
|
|||||||
if (!Gen) return;
|
if (!Gen) return;
|
||||||
if (bShuttingDown.load(std::memory_order_relaxed) || bFillThreadStop.load(std::memory_order_relaxed)) return;
|
if (bShuttingDown.load(std::memory_order_relaxed) || bFillThreadStop.load(std::memory_order_relaxed)) return;
|
||||||
|
|
||||||
const int32 Step = static_cast<int32>(uint32{1}
|
const int32 Step = 1 << F.Level;
|
||||||
<< static_cast<uint32>(FMath::Clamp(F.Level, 0, 8)));
|
|
||||||
FFillResult R;
|
FFillResult R;
|
||||||
R.Level = F.Level;
|
R.Level = F.Level;
|
||||||
R.Epoch = F.Epoch;
|
R.Epoch = F.Epoch;
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
|
||||||
|
|
||||||
// Shared custom package version for idempotent VoxelForge asset migrations.
|
|
||||||
// Version de package partagee pour les migrations VoxelForge idempotentes.
|
|
||||||
namespace VoxelForgeAssetVersion
|
|
||||||
{
|
|
||||||
inline const FGuid GUID(0xA76D5E31, 0x4B2C4F9A, 0xB8C1D204, 0x6E9F7738);
|
|
||||||
inline constexpr int32 TunnelFloorSlopeDegrees = 1;
|
|
||||||
inline constexpr int32 MetreAuthoringV1 = 2;
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -15,73 +15,9 @@
|
|||||||
#include "HAL/IConsoleManager.h"
|
#include "HAL/IConsoleManager.h"
|
||||||
#include "Misc/CommandLine.h"
|
#include "Misc/CommandLine.h"
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <array>
|
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
struct FVoxelMesherThreadState
|
|
||||||
{
|
|
||||||
int32 OctaveBias=0,SampleStep=1,TileCells=0,TileReachBlocks=0,TileReachBlockIndex=INDEX_NONE;
|
|
||||||
FIntVector TileOrigin=FIntVector::ZeroValue;
|
|
||||||
uint8 TilePostReachFlags=VoxelGenLOD::AllTileReach;
|
|
||||||
bool bTilePostReachDebug=false,bTilePostReachBypass=false;
|
|
||||||
bool bTileReachDiagnosticTile=false,bTileCoreReachProofEnabled=false;
|
|
||||||
bool bTileCoreReachDecisionRecorded=false,bTileBlockReachValid=false;
|
|
||||||
float TileReachScale=1.0f;
|
|
||||||
std::array<uint8,VoxelGenLOD::TileReachMaxBlockCount> TileBlockReachFlags{};
|
|
||||||
|
|
||||||
static FVoxelMesherThreadState Capture()
|
|
||||||
{
|
|
||||||
FVoxelMesherThreadState State;
|
|
||||||
State.OctaveBias=VoxelGenLOD::OctaveBias;
|
|
||||||
State.SampleStep=VoxelGenLOD::SampleStep;
|
|
||||||
State.TileOrigin=VoxelGenLOD::TileOriginVoxels;
|
|
||||||
State.TileCells=VoxelGenLOD::TileCellsPerAxis;
|
|
||||||
State.TilePostReachFlags=VoxelGenLOD::TilePostReachFlags;
|
|
||||||
State.bTilePostReachDebug=VoxelGenLOD::bTilePostReachDebug;
|
|
||||||
State.bTilePostReachBypass=VoxelGenLOD::bTilePostReachBypass;
|
|
||||||
State.bTileReachDiagnosticTile=VoxelGenLOD::bTileReachDiagnosticTile;
|
|
||||||
State.bTileCoreReachProofEnabled=VoxelGenLOD::bTileCoreReachProofEnabled;
|
|
||||||
State.bTileCoreReachDecisionRecorded=VoxelGenLOD::bTileCoreReachDecisionRecorded;
|
|
||||||
State.bTileBlockReachValid=VoxelGenLOD::bTileBlockReachValid;
|
|
||||||
State.TileReachScale=VoxelGenLOD::TileReachScale;
|
|
||||||
State.TileReachBlocks=VoxelGenLOD::TileReachBlocksPerAxis;
|
|
||||||
State.TileReachBlockIndex=VoxelGenLOD::TileReachBlockIndex;
|
|
||||||
std::copy(std::begin(VoxelGenLOD::TileBlockReachFlags),
|
|
||||||
std::end(VoxelGenLOD::TileBlockReachFlags),State.TileBlockReachFlags.begin());
|
|
||||||
return State;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Apply() const
|
|
||||||
{
|
|
||||||
VoxelGenLOD::OctaveBias=OctaveBias;
|
|
||||||
VoxelGenLOD::SampleStep=SampleStep;
|
|
||||||
VoxelGenLOD::TileOriginVoxels=TileOrigin;
|
|
||||||
VoxelGenLOD::TileCellsPerAxis=TileCells;
|
|
||||||
VoxelGenLOD::TilePostReachFlags=TilePostReachFlags;
|
|
||||||
VoxelGenLOD::bTilePostReachDebug=bTilePostReachDebug;
|
|
||||||
VoxelGenLOD::bTilePostReachBypass=bTilePostReachBypass;
|
|
||||||
VoxelGenLOD::bTileReachDiagnosticTile=bTileReachDiagnosticTile;
|
|
||||||
VoxelGenLOD::bTileCoreReachProofEnabled=bTileCoreReachProofEnabled;
|
|
||||||
VoxelGenLOD::bTileCoreReachDecisionRecorded=bTileCoreReachDecisionRecorded;
|
|
||||||
VoxelGenLOD::bTileBlockReachValid=bTileBlockReachValid;
|
|
||||||
VoxelGenLOD::TileReachScale=TileReachScale;
|
|
||||||
VoxelGenLOD::TileReachBlocksPerAxis=TileReachBlocks;
|
|
||||||
VoxelGenLOD::TileReachBlockIndex=TileReachBlockIndex;
|
|
||||||
std::copy(TileBlockReachFlags.begin(),TileBlockReachFlags.end(),
|
|
||||||
std::begin(VoxelGenLOD::TileBlockReachFlags));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
struct FVoxelMesherThreadStateScope
|
|
||||||
{
|
|
||||||
explicit FVoxelMesherThreadStateScope(const FVoxelMesherThreadState& InTarget)
|
|
||||||
:Previous(FVoxelMesherThreadState::Capture())
|
|
||||||
{ InTarget.Apply(); }
|
|
||||||
~FVoxelMesherThreadStateScope(){Previous.Apply();}
|
|
||||||
FVoxelMesherThreadState Previous;
|
|
||||||
};
|
|
||||||
|
|
||||||
int32 GVoxelForgeUseOperatorBlock = 1;
|
int32 GVoxelForgeUseOperatorBlock = 1;
|
||||||
FAutoConsoleVariableRef CVarVoxelForgeUseOperatorBlock(
|
FAutoConsoleVariableRef CVarVoxelForgeUseOperatorBlock(
|
||||||
TEXT("voxel.UseOperatorBlock"),
|
TEXT("voxel.UseOperatorBlock"),
|
||||||
@@ -756,7 +692,6 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
// capacité, donc plus de malloc/free de ~170 Ko (35³ floats) par tuile.
|
// capacité, donc plus de malloc/free de ~170 Ko (35³ floats) par tuile.
|
||||||
static thread_local TArray<float> DensityGrid;
|
static thread_local TArray<float> DensityGrid;
|
||||||
DensityGrid.SetNumUninitialized(MDim * MDim * MDim);
|
DensityGrid.SetNumUninitialized(MDim * MDim * MDim);
|
||||||
float* const DensityGridData=DensityGrid.GetData();
|
|
||||||
const FVoxelSharedDensityGrid* ReuseGrid = InSharedDensityGrid != nullptr
|
const FVoxelSharedDensityGrid* ReuseGrid = InSharedDensityGrid != nullptr
|
||||||
? InSharedDensityGrid : SharedDensityGrid;
|
? InSharedDensityGrid : SharedDensityGrid;
|
||||||
const int32 SharedDeltaX = ReuseGrid
|
const int32 SharedDeltaX = ReuseGrid
|
||||||
@@ -804,15 +739,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
const bool bUseOperatorBlock = !bUseSharedDensityGrid
|
const bool bUseOperatorBlock = !bUseSharedDensityGrid
|
||||||
&& !bUseParameterizedBrickEvaluator
|
&& !bUseParameterizedBrickEvaluator
|
||||||
&& GVoxelForgeUseOperatorBlock != 0;
|
&& GVoxelForgeUseOperatorBlock != 0;
|
||||||
const int32 GridZSampleCount=GzHi-GzLo+1;
|
if (bUseOperatorBlock)
|
||||||
const std::size_t GridFillLaneCount=std::min<std::size_t>({
|
|
||||||
GridFillExecution.WorkerLimit,16,
|
|
||||||
static_cast<std::size_t>(FMath::Max(0,GridZSampleCount))});
|
|
||||||
const bool bUseParallelDensityGrid=GridFillExecution.ParallelFor!=nullptr
|
|
||||||
&&GridFillLaneCount>1&&GridFillExecution.BeginWorkerContext!=nullptr
|
|
||||||
&&GridFillExecution.EndWorkerContext!=nullptr&&!bUseSharedDensityGrid
|
|
||||||
&&!bUseParameterizedBrickEvaluator&&OutCaptureGrid==nullptr;
|
|
||||||
if (bUseOperatorBlock&&!bUseParallelDensityGrid)
|
|
||||||
{
|
{
|
||||||
// The grid includes one sample of halo on every side. The generator groups these exact
|
// The grid includes one sample of halo on every side. The generator groups these exact
|
||||||
// lattice points by chunk key and lets the prepared op stack fill each group once.
|
// lattice points by chunk key and lets the prepared op stack fill each group once.
|
||||||
@@ -962,80 +889,6 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
else if(bUseParallelDensityGrid)
|
|
||||||
{
|
|
||||||
const FVoxelMesherThreadState TileThreadState=FVoxelMesherThreadState::Capture();
|
|
||||||
std::array<int64,16> LaneSampleCounts{};
|
|
||||||
std::atomic<bool> bAborted{false};
|
|
||||||
auto RunLane=[&](std::size_t Lane,std::size_t Worker)
|
|
||||||
{
|
|
||||||
void* WorkerState=GridFillExecution.BeginWorkerContext(
|
|
||||||
GridFillExecution.WorkerContext,Worker);
|
|
||||||
int64 LaneSamples=0;
|
|
||||||
{
|
|
||||||
FVoxelMesherThreadStateScope ThreadStateScope(TileThreadState);
|
|
||||||
const int32 FirstGz=GzLo+static_cast<int32>(
|
|
||||||
(static_cast<std::size_t>(GridZSampleCount)*Lane)/GridFillLaneCount);
|
|
||||||
const int32 EndGz=GzLo+static_cast<int32>(
|
|
||||||
(static_cast<std::size_t>(GridZSampleCount)*(Lane+1))/GridFillLaneCount);
|
|
||||||
if(bUseOperatorBlock)
|
|
||||||
{
|
|
||||||
Generator->BeginDensityBlock(
|
|
||||||
OriginVoxels+FIntVector(-Step,-Step,FirstGz*Step),Step,
|
|
||||||
MDim,MDim,EndGz-FirstGz);
|
|
||||||
}
|
|
||||||
for(int32 gz=FirstGz;gz<EndGz;++gz)
|
|
||||||
{
|
|
||||||
if(ShouldAbortWork())
|
|
||||||
{ bAborted.store(true,std::memory_order_relaxed);break; }
|
|
||||||
for(int32 gy=-1;gy<=GridDim;++gy)
|
|
||||||
{
|
|
||||||
if(ShouldAbortWork())
|
|
||||||
{ bAborted.store(true,std::memory_order_relaxed);break; }
|
|
||||||
for(int32 gx=-1;gx<=GridDim;++gx)
|
|
||||||
{
|
|
||||||
const int32 LocalIndex=((gz+1)*MDim+(gy+1))*MDim+(gx+1);
|
|
||||||
const int32 ReachBlockX=FMath::Clamp(gx,0,CellsPerAxis-1)
|
|
||||||
/VoxelGenLOD::TileReachBlockCells;
|
|
||||||
const int32 ReachBlockY=FMath::Clamp(gy,0,CellsPerAxis-1)
|
|
||||||
/VoxelGenLOD::TileReachBlockCells;
|
|
||||||
const int32 ReachBlockZ=FMath::Clamp(gz,0,CellsPerAxis-1)
|
|
||||||
/VoxelGenLOD::TileReachBlockCells;
|
|
||||||
VoxelGenLOD::TileReachBlockIndex=
|
|
||||||
((ReachBlockZ*ReachBlocksPerAxis)+ReachBlockY)
|
|
||||||
*ReachBlocksPerAxis+ReachBlockX;
|
|
||||||
float PreEditDensity=0.0f;
|
|
||||||
DensityGridData[LocalIndex]=Generator->GetDensityAt(
|
|
||||||
OriginVoxels.X+gx*Step,OriginVoxels.Y+gy*Step,
|
|
||||||
OriginVoxels.Z+gz*Step,
|
|
||||||
bCapturePreEditGrid?&PreEditDensity:nullptr);
|
|
||||||
if(bCapturePreEditGrid)
|
|
||||||
OutPreEditDensityGrid->Samples[LocalIndex]=PreEditDensity;
|
|
||||||
++LaneSamples;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(bAborted.load(std::memory_order_relaxed))break;
|
|
||||||
}
|
|
||||||
if(bUseOperatorBlock)Generator->EndDensityBlock();
|
|
||||||
}
|
|
||||||
LaneSampleCounts[Lane]=LaneSamples;
|
|
||||||
GridFillExecution.EndWorkerContext(
|
|
||||||
GridFillExecution.WorkerContext,WorkerState,Worker);
|
|
||||||
};
|
|
||||||
struct FDispatchContext{decltype(RunLane)* Run=nullptr;};
|
|
||||||
FDispatchContext DispatchContext{&RunLane};
|
|
||||||
const auto DispatchLane=[](void* RawContext,std::size_t Lane,std::size_t Worker)
|
|
||||||
{
|
|
||||||
auto& Context=*static_cast<FDispatchContext*>(RawContext);
|
|
||||||
(*Context.Run)(Lane,Worker);
|
|
||||||
};
|
|
||||||
GridFillExecution.ParallelFor(GridFillExecution.SchedulerContext,
|
|
||||||
GridFillLaneCount,GridFillLaneCount,&DispatchContext,DispatchLane);
|
|
||||||
if(OutDensitySampleCount)
|
|
||||||
for(std::size_t Lane=0;Lane<GridFillLaneCount;++Lane)
|
|
||||||
*OutDensitySampleCount+=LaneSampleCounts[Lane];
|
|
||||||
if(bAborted.load(std::memory_order_relaxed))return FVoxelMeshData();
|
|
||||||
}
|
|
||||||
else for (int32 gz = GzLo; gz <= GzHi; gz++)
|
else for (int32 gz = GzLo; gz <= GzHi; gz++)
|
||||||
{
|
{
|
||||||
if (ShouldAbortWork())
|
if (ShouldAbortWork())
|
||||||
@@ -1106,7 +959,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
GridSampleCount * sizeof(float));
|
GridSampleCount * sizeof(float));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (bUseOperatorBlock&&!bUseParallelDensityGrid)
|
if (bUseOperatorBlock)
|
||||||
{
|
{
|
||||||
Generator->EndDensityBlock();
|
Generator->EndDensityBlock();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2590,8 +2590,8 @@ static float EvaluateInternal(const UVoxelGenerator& Generator,
|
|||||||
{
|
{
|
||||||
EnsurePlan(Generator);
|
EnsurePlan(Generator);
|
||||||
}
|
}
|
||||||
const bool bChunkResolved = (Context != nullptr && Context->bChunkResolved)
|
const bool bChunkResolved = Context != nullptr && Context->bChunkResolved
|
||||||
|| (ParameterizedContext != nullptr && ParameterizedContext->bChunkResolved);
|
|| ParameterizedContext != nullptr && ParameterizedContext->bChunkResolved;
|
||||||
const FIntVector Chunk = Context != nullptr && Context->bChunkResolved
|
const FIntVector Chunk = Context != nullptr && Context->bChunkResolved
|
||||||
? Context->Chunk
|
? Context->Chunk
|
||||||
: ParameterizedContext != nullptr && ParameterizedContext->bChunkResolved
|
: ParameterizedContext != nullptr && ParameterizedContext->bChunkResolved
|
||||||
|
|||||||
@@ -1958,11 +1958,10 @@ namespace VoxelSeasonCompositionPrivate
|
|||||||
}
|
}
|
||||||
|
|
||||||
void CopyDefinitionParams(const UVoxelStrateDefinition& Definition,
|
void CopyDefinitionParams(const UVoxelStrateDefinition& Definition,
|
||||||
double LayoutChunkMeters, FVoxelSeasonFixedStrate& OutFixed)
|
FVoxelSeasonFixedStrate& OutFixed)
|
||||||
{
|
{
|
||||||
OutFixed = FVoxelSeasonFixedStrate();
|
OutFixed = FVoxelSeasonFixedStrate();
|
||||||
OutFixed.HeightInChunks = FMath::Max(1, FMath::RoundToInt(
|
OutFixed.HeightInChunks = Definition.StrateHeightInChunks;
|
||||||
Definition.StrateHeightMeters / FMath::Max(0.001, LayoutChunkMeters)));
|
|
||||||
OutFixed.Archetype = Definition.GeneratorType;
|
OutFixed.Archetype = Definition.GeneratorType;
|
||||||
OutFixed.SourceDefinitionPath = Definition.GetPathName();
|
OutFixed.SourceDefinitionPath = Definition.GetPathName();
|
||||||
OutFixed.bUsesRecipe = false;
|
OutFixed.bUsesRecipe = false;
|
||||||
@@ -2000,9 +1999,7 @@ namespace VoxelSeasonCompositionPrivate
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
FVoxelSeasonFixedStrate Fixed;
|
FVoxelSeasonFixedStrate Fixed;
|
||||||
const double LayoutChunkMeters = Settings.WorldSettings != nullptr
|
CopyDefinitionParams(*Definition, Fixed);
|
||||||
? Settings.WorldSettings->StrateLayoutChunkSizeMeters : 8.0;
|
|
||||||
CopyDefinitionParams(*Definition, LayoutChunkMeters, Fixed);
|
|
||||||
Fixed.Seed = SeasonSeed;
|
Fixed.Seed = SeasonSeed;
|
||||||
if (Fixed.SourceDefinitionPath.IsEmpty())
|
if (Fixed.SourceDefinitionPath.IsEmpty())
|
||||||
{
|
{
|
||||||
@@ -2977,9 +2974,7 @@ FVoxelSeasonManifest VF_ComposeSeason(int32 SeasonSeed, const UVoxelSettings* Wo
|
|||||||
if (WorldSettings != nullptr)
|
if (WorldSettings != nullptr)
|
||||||
{
|
{
|
||||||
Settings.SelectedStrateCount = WorldSettings->TotalStrates;
|
Settings.SelectedStrateCount = WorldSettings->TotalStrates;
|
||||||
Settings.InterStrateGapChunks = FMath::Max(0, FMath::RoundToInt(
|
Settings.InterStrateGapChunks = WorldSettings->InterStrateGapChunks;
|
||||||
WorldSettings->InterStrateGapMeters * 1000000.0
|
|
||||||
/ FMath::Max(1.0, static_cast<double>(WorldSettings->StrateLayoutChunkSizeMeters) * 1000000.0)));
|
|
||||||
Settings.OriginSpineRadius = WorldSettings->OriginSpineRadius;
|
Settings.OriginSpineRadius = WorldSettings->OriginSpineRadius;
|
||||||
Settings.WorldRadiusVoxels = WorldSettings->WorldRadiusVoxels;
|
Settings.WorldRadiusVoxels = WorldSettings->WorldRadiusVoxels;
|
||||||
Settings.SeasonNumber = WorldSettings->CurrentSeason;
|
Settings.SeasonNumber = WorldSettings->CurrentSeason;
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
#include "VoxelSettings.h"
|
#include "VoxelSettings.h"
|
||||||
|
|
||||||
#include "VoxelForgeAssetVersion.h"
|
|
||||||
#include "VoxelStrateDefinition.h"
|
|
||||||
#include "VoxelSeasonAsset.h"
|
#include "VoxelSeasonAsset.h"
|
||||||
|
|
||||||
#if WITH_EDITOR
|
#if WITH_EDITOR
|
||||||
@@ -11,74 +9,6 @@
|
|||||||
#include <VoxelForgeCore/VoxelForgeCore.h>
|
#include <VoxelForgeCore/VoxelForgeCore.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
void UVoxelSettings::Serialize(FArchive& Ar)
|
|
||||||
{
|
|
||||||
Ar.UsingCustomVersion(VoxelForgeAssetVersion::GUID);
|
|
||||||
Super::Serialize(Ar);
|
|
||||||
if (Ar.IsLoading() && Ar.CustomVer(VoxelForgeAssetVersion::GUID)
|
|
||||||
< VoxelForgeAssetVersion::MetreAuthoringV1)
|
|
||||||
{
|
|
||||||
const double LayoutChunkMeters = FMath::IsFinite(StrateLayoutChunkSizeMeters)
|
|
||||||
? FMath::Max(0.001, static_cast<double>(StrateLayoutChunkSizeMeters)) : 8.0;
|
|
||||||
InterStrateGapMeters = static_cast<double>(FMath::Max(0, InterStrateGapChunks))
|
|
||||||
* LayoutChunkMeters;
|
|
||||||
UE_LOG(LogTemp, Display,
|
|
||||||
TEXT("[VoxelForgeAssetUpgrade] type=Settings asset=%s gap_chunks=%d gap_m=%.6f layout_chunk_m=%.6f"),
|
|
||||||
*GetPathName(), InterStrateGapChunks, InterStrateGapMeters, LayoutChunkMeters);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void UVoxelSettings::PostLoad()
|
|
||||||
{
|
|
||||||
Super::PostLoad();
|
|
||||||
if (GetLinkerCustomVersion(VoxelForgeAssetVersion::GUID)
|
|
||||||
>= VoxelForgeAssetVersion::MetreAuthoringV1)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve the legacy count against this settings asset's saved physical layout width.
|
|
||||||
// Convertit les anciens comptes avec la largeur physique sauvegardee dans ces reglages.
|
|
||||||
const double LayoutChunkMeters = FMath::IsFinite(StrateLayoutChunkSizeMeters)
|
|
||||||
? FMath::Max(0.001, static_cast<double>(StrateLayoutChunkSizeMeters)) : 8.0;
|
|
||||||
InterStrateGapMeters = static_cast<double>(FMath::Max(0, InterStrateGapChunks))
|
|
||||||
* LayoutChunkMeters;
|
|
||||||
|
|
||||||
TSet<UVoxelStrateDefinition*> Migrated;
|
|
||||||
const auto MigrateStrate = [this, LayoutChunkMeters, &Migrated](
|
|
||||||
const TSoftObjectPtr<UVoxelStrateDefinition>& SoftDefinition)
|
|
||||||
{
|
|
||||||
UVoxelStrateDefinition* Definition = SoftDefinition.LoadSynchronous();
|
|
||||||
if (Definition == nullptr || Migrated.Contains(Definition)
|
|
||||||
|| Definition->GetLinkerCustomVersion(VoxelForgeAssetVersion::GUID)
|
|
||||||
>= VoxelForgeAssetVersion::MetreAuthoringV1)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Migrated.Add(Definition);
|
|
||||||
const int32 OldHeightChunks = Definition->StrateHeightInChunks;
|
|
||||||
const int32 OldTransitionChunks = Definition->TransitionBlendChunks;
|
|
||||||
Definition->StrateHeightMeters = static_cast<double>(FMath::Max(1, OldHeightChunks))
|
|
||||||
* LayoutChunkMeters;
|
|
||||||
Definition->TransitionWidthMeters = static_cast<double>(FMath::Max(0, OldTransitionChunks))
|
|
||||||
* LayoutChunkMeters;
|
|
||||||
Definition->MarkPackageDirty();
|
|
||||||
UE_LOG(LogTemp, Display,
|
|
||||||
TEXT("[VoxelForgeAssetUpgrade] type=Strate asset=%s height_chunks=%d height_m=%.6f transition_chunks=%d transition_m=%.6f effective_layout_chunk_m=%.6f"),
|
|
||||||
*Definition->GetPathName(), OldHeightChunks, Definition->StrateHeightMeters,
|
|
||||||
OldTransitionChunks, Definition->TransitionWidthMeters, LayoutChunkMeters);
|
|
||||||
};
|
|
||||||
for (const TSoftObjectPtr<UVoxelStrateDefinition>& Definition : StratePool)
|
|
||||||
{
|
|
||||||
MigrateStrate(Definition);
|
|
||||||
}
|
|
||||||
for (const TPair<int32, TSoftObjectPtr<UVoxelStrateDefinition>>& Definition : FixedStrates)
|
|
||||||
{
|
|
||||||
MigrateStrate(Definition.Value);
|
|
||||||
}
|
|
||||||
MarkPackageDirty();
|
|
||||||
}
|
|
||||||
|
|
||||||
int32 UVoxelSettings::GetEffectiveWorldSeed() const
|
int32 UVoxelSettings::GetEffectiveWorldSeed() const
|
||||||
{
|
{
|
||||||
if (!Season.IsNull())
|
if (!Season.IsNull())
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
#include "VoxelStartupTrace.h"
|
#include "VoxelStartupTrace.h"
|
||||||
|
|
||||||
#include "HAL/FileManager.h"
|
#include "HAL/FileManager.h"
|
||||||
#include "HAL/PlatformTLS.h"
|
|
||||||
#include "HAL/PlatformTime.h"
|
#include "HAL/PlatformTime.h"
|
||||||
#include "CoreGlobals.h"
|
|
||||||
#include "Misc/CommandLine.h"
|
#include "Misc/CommandLine.h"
|
||||||
#include "Misc/Parse.h"
|
#include "Misc/Parse.h"
|
||||||
#include "UObject/UObjectGlobals.h"
|
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
|
||||||
@@ -18,13 +15,7 @@ namespace
|
|||||||
{
|
{
|
||||||
FArchive* Archive = nullptr;
|
FArchive* Archive = nullptr;
|
||||||
FString Path;
|
FString Path;
|
||||||
uint32 GameThreadId = 0;
|
|
||||||
double StartSeconds = 0.0;
|
double StartSeconds = 0.0;
|
||||||
double GameThreadJsonWriteSeconds = 0.0;
|
|
||||||
double GameThreadArchiveSerializeSeconds = 0.0;
|
|
||||||
double GarbageCollectionStartSeconds = 0.0;
|
|
||||||
FDelegateHandle PreGarbageCollectHandle;
|
|
||||||
FDelegateHandle PostGarbageCollectHandle;
|
|
||||||
int32 TileRecordCap = 4096;
|
int32 TileRecordCap = 4096;
|
||||||
int32 TileRecordsSeen = 0;
|
int32 TileRecordsSeen = 0;
|
||||||
int32 TileRecordsWritten = 0;
|
int32 TileRecordsWritten = 0;
|
||||||
@@ -59,22 +50,15 @@ namespace
|
|||||||
void WriteLineLocked(FState& State, const FString& Line)
|
void WriteLineLocked(FState& State, const FString& Line)
|
||||||
{
|
{
|
||||||
if (State.Archive == nullptr) return;
|
if (State.Archive == nullptr) return;
|
||||||
const uint64 StartCycles = FPlatformTime::Cycles64();
|
|
||||||
const FString WithNewline = Line + TEXT("\n");
|
const FString WithNewline = Line + TEXT("\n");
|
||||||
FTCHARToUTF8 Utf8(*WithNewline);
|
FTCHARToUTF8 Utf8(*WithNewline);
|
||||||
State.Archive->Serialize(const_cast<ANSICHAR*>(Utf8.Get()), Utf8.Length());
|
State.Archive->Serialize(const_cast<ANSICHAR*>(Utf8.Get()), Utf8.Length());
|
||||||
if (FPlatformTLS::GetCurrentThreadId() == State.GameThreadId)
|
|
||||||
{
|
|
||||||
State.GameThreadArchiveSerializeSeconds += FPlatformTime::ToSeconds64(
|
|
||||||
FPlatformTime::Cycles64() - StartCycles);
|
|
||||||
}
|
|
||||||
// The file archive buffers records; flushing every tile/request can stall the game thread.
|
// The file archive buffers records; flushing every tile/request can stall the game thread.
|
||||||
// L'archive tamponne les lignes ; vider apres chaque tuile/requete peut bloquer le thread de jeu.
|
// L'archive tamponne les lignes ; vider apres chaque tuile/requete peut bloquer le thread de jeu.
|
||||||
}
|
}
|
||||||
|
|
||||||
void WriteEventLocked(FState& State, const TCHAR* Type, const FString& FieldsJson)
|
void WriteEventLocked(FState& State, const TCHAR* Type, const FString& FieldsJson)
|
||||||
{
|
{
|
||||||
const uint64 StartCycles = FPlatformTime::Cycles64();
|
|
||||||
FString Line = FString::Printf(
|
FString Line = FString::Printf(
|
||||||
TEXT("{\"type\":\"%s\",\"t_s\":%s"),
|
TEXT("{\"type\":\"%s\",\"t_s\":%s"),
|
||||||
Type,
|
Type,
|
||||||
@@ -86,11 +70,6 @@ namespace
|
|||||||
}
|
}
|
||||||
Line += TEXT("}");
|
Line += TEXT("}");
|
||||||
WriteLineLocked(State, Line);
|
WriteLineLocked(State, Line);
|
||||||
if (FPlatformTLS::GetCurrentThreadId() == State.GameThreadId)
|
|
||||||
{
|
|
||||||
State.GameThreadJsonWriteSeconds += FPlatformTime::ToSeconds64(
|
|
||||||
FPlatformTime::Cycles64() - StartCycles);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
double Percentile(TArray<double> Values, double Fraction)
|
double Percentile(TArray<double> Values, double Fraction)
|
||||||
@@ -219,7 +198,6 @@ void BeginFromCommandLine()
|
|||||||
|
|
||||||
TUniquePtr<FState> NewState = MakeUnique<FState>();
|
TUniquePtr<FState> NewState = MakeUnique<FState>();
|
||||||
NewState->Path = Path;
|
NewState->Path = Path;
|
||||||
NewState->GameThreadId = FPlatformTLS::GetCurrentThreadId();
|
|
||||||
FParse::Value(FCommandLine::Get(), TEXT("voxel.StartupTraceMaxTiles="), NewState->TileRecordCap);
|
FParse::Value(FCommandLine::Get(), TEXT("voxel.StartupTraceMaxTiles="), NewState->TileRecordCap);
|
||||||
NewState->TileRecordCap = FMath::Clamp(NewState->TileRecordCap, 64, 16384);
|
NewState->TileRecordCap = FMath::Clamp(NewState->TileRecordCap, 64, 16384);
|
||||||
NewState->Samples.Reserve(NewState->TileRecordCap);
|
NewState->Samples.Reserve(NewState->TileRecordCap);
|
||||||
@@ -233,32 +211,6 @@ void BeginFromCommandLine()
|
|||||||
NewState->bActive = true;
|
NewState->bActive = true;
|
||||||
GState = MoveTemp(NewState);
|
GState = MoveTemp(NewState);
|
||||||
GTraceActive.store(true, std::memory_order_release);
|
GTraceActive.store(true, std::memory_order_release);
|
||||||
GState->PreGarbageCollectHandle = FCoreUObjectDelegates::GetPreGarbageCollectDelegate()
|
|
||||||
.AddLambda([]()
|
|
||||||
{
|
|
||||||
FScopeLock CallbackLock(>raceLock);
|
|
||||||
if (GState.IsValid() && GState->bActive)
|
|
||||||
{
|
|
||||||
GState->GarbageCollectionStartSeconds = FPlatformTime::Seconds();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
GState->PostGarbageCollectHandle = FCoreUObjectDelegates::GetPostGarbageCollect()
|
|
||||||
.AddLambda([]()
|
|
||||||
{
|
|
||||||
FScopeLock CallbackLock(>raceLock);
|
|
||||||
if (!GState.IsValid() || !GState->bActive
|
|
||||||
|| GState->GarbageCollectionStartSeconds <= 0.0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const double DurationMilliseconds = FMath::Max(
|
|
||||||
0.0, FPlatformTime::Seconds() - GState->GarbageCollectionStartSeconds) * 1000.0;
|
|
||||||
GState->GarbageCollectionStartSeconds = 0.0;
|
|
||||||
WriteEventLocked(*GState, TEXT("garbage_collect"), FString::Printf(
|
|
||||||
TEXT("\"frame\":%llu,\"duration_ms\":%s"),
|
|
||||||
static_cast<unsigned long long>(GFrameCounter),
|
|
||||||
*Number(DurationMilliseconds)));
|
|
||||||
});
|
|
||||||
WriteEventLocked(*GState, TEXT("header"), FString::Printf(
|
WriteEventLocked(*GState, TEXT("header"), FString::Printf(
|
||||||
TEXT("\"schema\":1,\"path\":\"%s\",\"tile_record_cap\":%d"),
|
TEXT("\"schema\":1,\"path\":\"%s\",\"tile_record_cap\":%d"),
|
||||||
*JsonEscape(GState->Path), GState->TileRecordCap));
|
*JsonEscape(GState->Path), GState->TileRecordCap));
|
||||||
@@ -269,21 +221,6 @@ bool IsActive()
|
|||||||
return GTraceActive.load(std::memory_order_acquire);
|
return GTraceActive.load(std::memory_order_acquire);
|
||||||
}
|
}
|
||||||
|
|
||||||
FWriteMetrics ConsumeCurrentThreadWriteMetrics()
|
|
||||||
{
|
|
||||||
FWriteMetrics Metrics;
|
|
||||||
FScopeLock Lock(>raceLock);
|
|
||||||
if (!GState.IsValid() || FPlatformTLS::GetCurrentThreadId() != GState->GameThreadId)
|
|
||||||
{
|
|
||||||
return Metrics;
|
|
||||||
}
|
|
||||||
Metrics.JsonWriteMilliseconds = GState->GameThreadJsonWriteSeconds * 1000.0;
|
|
||||||
Metrics.ArchiveSerializeMilliseconds = GState->GameThreadArchiveSerializeSeconds * 1000.0;
|
|
||||||
GState->GameThreadJsonWriteSeconds = 0.0;
|
|
||||||
GState->GameThreadArchiveSerializeSeconds = 0.0;
|
|
||||||
return Metrics;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RecordEvent(const TCHAR* Type, const FString& FieldsJson)
|
void RecordEvent(const TCHAR* Type, const FString& FieldsJson)
|
||||||
{
|
{
|
||||||
FScopeLock Lock(>raceLock);
|
FScopeLock Lock(>raceLock);
|
||||||
@@ -291,16 +228,15 @@ void RecordEvent(const TCHAR* Type, const FString& FieldsJson)
|
|||||||
WriteEventLocked(*GState, Type, FieldsJson);
|
WriteEventLocked(*GState, Type, FieldsJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RecordRequest(const FTileSample& Sample, uint32 Epoch, uint32 DesiredEpoch)
|
void RecordRequest(const FTileSample& Sample, uint32 Epoch)
|
||||||
{
|
{
|
||||||
FScopeLock Lock(>raceLock);
|
FScopeLock Lock(>raceLock);
|
||||||
if (!GState.IsValid() || !GState->bActive) return;
|
if (!GState.IsValid() || !GState->bActive) return;
|
||||||
WriteEventLocked(*GState, TEXT("request"), FString::Printf(
|
WriteEventLocked(*GState, TEXT("request"), FString::Printf(
|
||||||
TEXT("\"tile\":[%d,%d,%d],\"level\":%d,\"priority\":%d,"
|
TEXT("\"tile\":[%d,%d,%d],\"level\":%d,\"priority\":%d,"
|
||||||
"\"request_ordinal\":%llu,\"epoch\":%u,\"desired_epoch\":%u,"
|
"\"request_ordinal\":%llu,\"epoch\":%u,\"resident_at_request\":%d"),
|
||||||
"\"resident_at_request\":%d"),
|
|
||||||
Sample.TileX, Sample.TileY, Sample.TileZ, Sample.Level, Sample.WorkPriority,
|
Sample.TileX, Sample.TileY, Sample.TileZ, Sample.Level, Sample.WorkPriority,
|
||||||
static_cast<unsigned long long>(Sample.RequestOrdinal), Epoch, DesiredEpoch,
|
static_cast<unsigned long long>(Sample.RequestOrdinal), Epoch,
|
||||||
Sample.bResidentAtRequest ? 1 : 0));
|
Sample.bResidentAtRequest ? 1 : 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,11 +297,7 @@ void RecordTile(const FTileSample& Sample)
|
|||||||
"\"submitted_t_s\":%s,\"worker_start_t_s\":%s,\"worker_end_t_s\":%s,\"worker_thread_id\":%u,"
|
"\"submitted_t_s\":%s,\"worker_start_t_s\":%s,\"worker_end_t_s\":%s,\"worker_thread_id\":%u,"
|
||||||
"\"verdict\":%d,\"proof\":%d,\"empty\":%d,\"vertices\":%d,\"triangles\":%d,\"geometry_bytes\":%llu,"
|
"\"verdict\":%d,\"proof\":%d,\"empty\":%d,\"vertices\":%d,\"triangles\":%d,\"geometry_bytes\":%llu,"
|
||||||
"\"request_to_apply_s\":%s,\"queue_wait_s\":%s,\"worker_queue_s\":%s,\"result_queue_s\":%s,\"worker_task_s\":%s,"
|
"\"request_to_apply_s\":%s,\"queue_wait_s\":%s,\"worker_queue_s\":%s,\"result_queue_s\":%s,\"worker_task_s\":%s,"
|
||||||
"\"generation_s\":%s,"
|
"\"generation_s\":%s,\"classify_s\":%s,\"mesh_s\":%s,\"stream_s\":%s,\"apply_s\":%s"),
|
||||||
#if PLATFORM_LINUX
|
|
||||||
"\"core_generate_s\":%s,\"mesh_copy_s\":%s,\"rmc_stream_build_s\":%s,"
|
|
||||||
#endif
|
|
||||||
"\"classify_s\":%s,\"mesh_s\":%s,\"stream_s\":%s,\"apply_s\":%s"),
|
|
||||||
Sample.TileX, Sample.TileY, Sample.TileZ,
|
Sample.TileX, Sample.TileY, Sample.TileZ,
|
||||||
Sample.Level, Sample.WorkPriority,
|
Sample.Level, Sample.WorkPriority,
|
||||||
static_cast<unsigned long long>(Sample.RequestOrdinal),
|
static_cast<unsigned long long>(Sample.RequestOrdinal),
|
||||||
@@ -380,12 +312,8 @@ void RecordTile(const FTileSample& Sample)
|
|||||||
static_cast<unsigned long long>(Sample.GeometryBytes),
|
static_cast<unsigned long long>(Sample.GeometryBytes),
|
||||||
*Number(Sample.RequestToApplySeconds), *Number(Sample.QueueWaitSeconds),
|
*Number(Sample.RequestToApplySeconds), *Number(Sample.QueueWaitSeconds),
|
||||||
*Number(Sample.WorkerQueueSeconds), *Number(Sample.ResultQueueSeconds),
|
*Number(Sample.WorkerQueueSeconds), *Number(Sample.ResultQueueSeconds),
|
||||||
*Number(Sample.WorkerTaskSeconds), *Number(Sample.GenerationSeconds),
|
*Number(Sample.WorkerTaskSeconds),
|
||||||
#if PLATFORM_LINUX
|
*Number(Sample.GenerationSeconds), *Number(Sample.ClassifySeconds),
|
||||||
*Number(Sample.CoreGenerateSeconds), *Number(Sample.MeshCopySeconds),
|
|
||||||
*Number(Sample.StreamBuildSeconds),
|
|
||||||
#endif
|
|
||||||
*Number(Sample.ClassifySeconds),
|
|
||||||
*Number(Sample.MeshSeconds), *Number(Sample.StreamSeconds), *Number(Sample.ApplySeconds)));
|
*Number(Sample.MeshSeconds), *Number(Sample.StreamSeconds), *Number(Sample.ApplySeconds)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,7 +351,6 @@ void RecordApplyFrame(const FApplyFrameSample& Sample)
|
|||||||
"\"active_tasks\":%d,\"running_workers\":%d,\"task_cap\":%d,"
|
"\"active_tasks\":%d,\"running_workers\":%d,\"task_cap\":%d,"
|
||||||
"\"pending_tiles\":%d,\"pending_results_estimate\":%d,"
|
"\"pending_tiles\":%d,\"pending_results_estimate\":%d,"
|
||||||
"\"results_drained\":%d,\"apply_results\":%d,\"meshes_applied\":%d,\"max_applies\":%d,"
|
"\"results_drained\":%d,\"apply_results\":%d,\"meshes_applied\":%d,\"max_applies\":%d,"
|
||||||
"\"supplemental_far_applies\":%d,"
|
|
||||||
"\"apply_budget_ms\":%s,\"count_limit_reached\":%d,\"time_limit_reached\":%d,"
|
"\"apply_budget_ms\":%s,\"count_limit_reached\":%d,\"time_limit_reached\":%d,"
|
||||||
"\"pending_by_lod\":%s,\"apply_results_by_lod\":%s,"
|
"\"pending_by_lod\":%s,\"apply_results_by_lod\":%s,"
|
||||||
"\"meshes_by_lod\":%s,\"apply_ms_by_lod\":%s"),
|
"\"meshes_by_lod\":%s,\"apply_ms_by_lod\":%s"),
|
||||||
@@ -433,8 +360,7 @@ void RecordApplyFrame(const FApplyFrameSample& Sample)
|
|||||||
Sample.RunningWorkerTasks, Sample.TaskCap, Sample.PendingTiles,
|
Sample.RunningWorkerTasks, Sample.TaskCap, Sample.PendingTiles,
|
||||||
Sample.PendingResultsEstimate, Sample.ResultsDrained, Sample.ApplyResultsByLevelTotal,
|
Sample.PendingResultsEstimate, Sample.ResultsDrained, Sample.ApplyResultsByLevelTotal,
|
||||||
Sample.MeshesApplied,
|
Sample.MeshesApplied,
|
||||||
Sample.MaxApplies, Sample.SupplementalFarApplies,
|
Sample.MaxApplies, *Number(Sample.ApplyBudgetMilliseconds),
|
||||||
*Number(Sample.ApplyBudgetMilliseconds),
|
|
||||||
Sample.bCountLimitReached ? 1 : 0, Sample.bTimeLimitReached ? 1 : 0,
|
Sample.bCountLimitReached ? 1 : 0, Sample.bTimeLimitReached ? 1 : 0,
|
||||||
*PendingByLevel, *ApplyResultsByLevel, *MeshesByLevel, *ApplyMillisecondsByLevel));
|
*PendingByLevel, *ApplyResultsByLevel, *MeshesByLevel, *ApplyMillisecondsByLevel));
|
||||||
}
|
}
|
||||||
@@ -444,18 +370,6 @@ void Finish(const TCHAR* Reason)
|
|||||||
FScopeLock Lock(>raceLock);
|
FScopeLock Lock(>raceLock);
|
||||||
if (!GState.IsValid() || !GState->bActive) return;
|
if (!GState.IsValid() || !GState->bActive) return;
|
||||||
GTraceActive.store(false, std::memory_order_release);
|
GTraceActive.store(false, std::memory_order_release);
|
||||||
if (GState->PreGarbageCollectHandle.IsValid())
|
|
||||||
{
|
|
||||||
FCoreUObjectDelegates::GetPreGarbageCollectDelegate().Remove(
|
|
||||||
GState->PreGarbageCollectHandle);
|
|
||||||
GState->PreGarbageCollectHandle.Reset();
|
|
||||||
}
|
|
||||||
if (GState->PostGarbageCollectHandle.IsValid())
|
|
||||||
{
|
|
||||||
FCoreUObjectDelegates::GetPostGarbageCollect().Remove(
|
|
||||||
GState->PostGarbageCollectHandle);
|
|
||||||
GState->PostGarbageCollectHandle.Reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close any stage left open by an abnormal early exit so the summary remains useful.
|
// Close any stage left open by an abnormal early exit so the summary remains useful.
|
||||||
const double Now = FPlatformTime::Seconds();
|
const double Now = FPlatformTime::Seconds();
|
||||||
@@ -465,19 +379,7 @@ void Finish(const TCHAR* Reason)
|
|||||||
}
|
}
|
||||||
GState->StageStarts.Reset();
|
GState->StageStarts.Reset();
|
||||||
WriteEventLocked(*GState, TEXT("summary"), BuildSummaryFieldsLocked(*GState, Reason));
|
WriteEventLocked(*GState, TEXT("summary"), BuildSummaryFieldsLocked(*GState, Reason));
|
||||||
const uint64 FlushStartCycles = FPlatformTime::Cycles64();
|
|
||||||
GState->Archive->Flush();
|
GState->Archive->Flush();
|
||||||
const double FlushMilliseconds = FPlatformTime::ToSeconds64(
|
|
||||||
FPlatformTime::Cycles64() - FlushStartCycles) * 1000.0;
|
|
||||||
WriteEventLocked(*GState, TEXT("trace_finish_flush"), FString::Printf(
|
|
||||||
TEXT("\"first_flush_ms\":%s"), *Number(FlushMilliseconds)));
|
|
||||||
const uint64 FinalFlushStartCycles = FPlatformTime::Cycles64();
|
|
||||||
GState->Archive->Flush();
|
|
||||||
const double FinalFlushMilliseconds = FPlatformTime::ToSeconds64(
|
|
||||||
FPlatformTime::Cycles64() - FinalFlushStartCycles) * 1000.0;
|
|
||||||
UE_LOG(LogTemp, Display,
|
|
||||||
TEXT("[VoxelForgeStartupTrace] finish_flush_ms=%.3f final_flush_ms=%.3f"),
|
|
||||||
FlushMilliseconds, FinalFlushMilliseconds);
|
|
||||||
delete GState->Archive;
|
delete GState->Archive;
|
||||||
GState->Archive = nullptr;
|
GState->Archive = nullptr;
|
||||||
GState->bActive = false;
|
GState->bActive = false;
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
// Serialized strate-definition migrations.
|
// Serialized strate-definition migrations.
|
||||||
|
|
||||||
#include "VoxelStrateDefinition.h"
|
#include "VoxelStrateDefinition.h"
|
||||||
#include "VoxelForgeAssetVersion.h"
|
|
||||||
|
|
||||||
#include "Serialization/CustomVersion.h"
|
#include "Serialization/CustomVersion.h"
|
||||||
#include "VoxelPassageGeometry.h"
|
#include "VoxelPassageGeometry.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
constexpr int32 GVoxelForgeStrateGentleSlopeDegreesVersion = VoxelForgeAssetVersion::TunnelFloorSlopeDegrees;
|
// The field name is retained for package compatibility, but version 1 changes its meaning
|
||||||
|
// from a raw tangent/gradient to authored degrees. This GUID is local to the plugin.
|
||||||
|
const FGuid GVoxelForgeStrateSerializationVersion(
|
||||||
|
0xA76D5E31, 0x4B2C4F9A, 0xB8C1D204, 0x6E9F7738);
|
||||||
|
constexpr int32 GVoxelForgeStrateGentleSlopeDegreesVersion = 1;
|
||||||
FCustomVersionRegistration GVoxelForgeStrateSerializationRegistration(
|
FCustomVersionRegistration GVoxelForgeStrateSerializationRegistration(
|
||||||
VoxelForgeAssetVersion::GUID,
|
GVoxelForgeStrateSerializationVersion,
|
||||||
VoxelForgeAssetVersion::MetreAuthoringV1,
|
GVoxelForgeStrateGentleSlopeDegreesVersion,
|
||||||
TEXT("VoxelForgeStrateSerialization"));
|
TEXT("VoxelForgeStrateSerialization"));
|
||||||
|
|
||||||
float LegacyGradientToDegrees(float LegacyGradient)
|
float LegacyGradientToDegrees(float LegacyGradient)
|
||||||
@@ -55,31 +58,6 @@ namespace
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void UVoxelStrateDefinition::Serialize(FArchive& Ar)
|
|
||||||
{
|
|
||||||
Ar.UsingCustomVersion(VoxelForgeAssetVersion::GUID);
|
|
||||||
Super::Serialize(Ar);
|
|
||||||
|
|
||||||
if (Ar.IsLoading() && Ar.CustomVer(VoxelForgeAssetVersion::GUID)
|
|
||||||
< VoxelForgeAssetVersion::MetreAuthoringV1)
|
|
||||||
{
|
|
||||||
// A stand-alone strate has no owning settings object during Serialize. Convert using the
|
|
||||||
// current 8 m layout default here; UVoxelSettings corrects referenced old strates using
|
|
||||||
// their saved effective width during its own load upgrade.
|
|
||||||
// Une strate seule n'a pas ses reglages proprietaires ici; UVoxelSettings corrige ensuite
|
|
||||||
// les references avec leur largeur physique sauvegardee.
|
|
||||||
constexpr double LegacyDefaultLayoutChunkMeters = 8.0;
|
|
||||||
StrateHeightMeters = static_cast<double>(FMath::Max(1, StrateHeightInChunks))
|
|
||||||
* LegacyDefaultLayoutChunkMeters;
|
|
||||||
TransitionWidthMeters = static_cast<double>(FMath::Max(0, TransitionBlendChunks))
|
|
||||||
* LegacyDefaultLayoutChunkMeters;
|
|
||||||
UE_LOG(LogTemp, Display,
|
|
||||||
TEXT("[VoxelForgeAssetUpgrade] type=Strate asset=%s height_chunks=%d height_m=%.6f transition_chunks=%d transition_m=%.6f fallback_layout_chunk_m=%.6f"),
|
|
||||||
*GetPathName(), StrateHeightInChunks, StrateHeightMeters,
|
|
||||||
TransitionBlendChunks, TransitionWidthMeters, LegacyDefaultLayoutChunkMeters);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void UVoxelStrateDefinition::PostLoad()
|
void UVoxelStrateDefinition::PostLoad()
|
||||||
{
|
{
|
||||||
Super::PostLoad();
|
Super::PostLoad();
|
||||||
@@ -89,7 +67,7 @@ void UVoxelStrateDefinition::PostLoad()
|
|||||||
// is normalized to the ignored compatibility default before any runtime copy is made.
|
// is normalized to the ignored compatibility default before any runtime copy is made.
|
||||||
GenerationParams.bTunnelFloorEnabled = true;
|
GenerationParams.bTunnelFloorEnabled = true;
|
||||||
|
|
||||||
if (GetLinkerCustomVersion(VoxelForgeAssetVersion::GUID)
|
if (GetLinkerCustomVersion(GVoxelForgeStrateSerializationVersion)
|
||||||
>= GVoxelForgeStrateGentleSlopeDegreesVersion)
|
>= GVoxelForgeStrateGentleSlopeDegreesVersion)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
#include "VoxelTerrainOpDefinition.h" // For UVoxelTerrainOpDefinition::ApplyTo
|
#include "VoxelTerrainOpDefinition.h" // For UVoxelTerrainOpDefinition::ApplyTo
|
||||||
#include "VoxelBiomeDefinition.h" // For UVoxelBiomeDefinition (biome context flatten)
|
#include "VoxelBiomeDefinition.h" // For UVoxelBiomeDefinition (biome context flatten)
|
||||||
#include "HAL/IConsoleManager.h"
|
#include "HAL/IConsoleManager.h"
|
||||||
#include <cmath> // std::llround
|
|
||||||
#include "Misc/CommandLine.h"
|
#include "Misc/CommandLine.h"
|
||||||
#include "Misc/Crc.h"
|
#include "Misc/Crc.h"
|
||||||
#include "Misc/Parse.h"
|
#include "Misc/Parse.h"
|
||||||
@@ -25,27 +24,6 @@ namespace
|
|||||||
{
|
{
|
||||||
std::atomic<uint64> GNextStrateManagerLifetimeId { 0 };
|
std::atomic<uint64> GNextStrateManagerLifetimeId { 0 };
|
||||||
|
|
||||||
int64 VF_OwnerVoxelsToMicrometres(double OwnerVoxels)
|
|
||||||
{
|
|
||||||
return static_cast<int64>(std::llround(
|
|
||||||
OwnerVoxels * static_cast<double>(VOXEL_SIZE) * 10000.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
float VF_ResolvedWorldMicrometresToOwnerVoxels(int64 WorldMicrometres)
|
|
||||||
{
|
|
||||||
return static_cast<float>(static_cast<double>(WorldMicrometres)
|
|
||||||
/ (static_cast<double>(VOXEL_SIZE) * 10000.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
int64 VF_FloorDivide64(int64 Numerator, int64 Denominator)
|
|
||||||
{
|
|
||||||
if (Denominator <= 0) return 0;
|
|
||||||
int64 Quotient = Numerator / Denominator;
|
|
||||||
const int64 Remainder = Numerator % Denominator;
|
|
||||||
if (Remainder < 0) --Quotient;
|
|
||||||
return Quotient;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exact A/B switch for the repeated passage-floor projection item. The other field-
|
// Exact A/B switch for the repeated passage-floor projection item. The other field-
|
||||||
// preserving rewrites in this round are deliberately unconditional; this switch isolates
|
// preserving rewrites in this round are deliberately unconditional; this switch isolates
|
||||||
// the projection reuse without changing any density arithmetic.
|
// the projection reuse without changing any density arithmetic.
|
||||||
@@ -343,7 +321,7 @@ namespace
|
|||||||
const FVector ChunkCenter(
|
const FVector ChunkCenter(
|
||||||
(ChunkCoord.X + 0.5f) * (float)CHUNK_SIZE,
|
(ChunkCoord.X + 0.5f) * (float)CHUNK_SIZE,
|
||||||
(ChunkCoord.Y + 0.5f) * (float)CHUNK_SIZE,
|
(ChunkCoord.Y + 0.5f) * (float)CHUNK_SIZE,
|
||||||
(ChunkCoord.Z + 0.5f) * Manager->GetStrateLayoutChunkWidthVoxels());
|
(ChunkCoord.Z + 0.5f) * (float)CHUNK_SIZE);
|
||||||
const float ChunkRadius = (float)CHUNK_SIZE * 0.8660254f + 3.0f;
|
const float ChunkRadius = (float)CHUNK_SIZE * 0.8660254f + 3.0f;
|
||||||
const TArray<FVoxelPassage>& Passages = Manager->GetPassages();
|
const TArray<FVoxelPassage>& Passages = Manager->GetPassages();
|
||||||
Cache.FloorProjections.SetNum(Passages.Num());
|
Cache.FloorProjections.SetNum(Passages.Num());
|
||||||
@@ -1125,7 +1103,7 @@ static bool VF_FindWalkableTunnelAir(
|
|||||||
const FIntVector ChunkCoord(
|
const FIntVector ChunkCoord(
|
||||||
FMath::FloorToInt(Position.X / (float)CHUNK_SIZE),
|
FMath::FloorToInt(Position.X / (float)CHUNK_SIZE),
|
||||||
FMath::FloorToInt(Position.Y / (float)CHUNK_SIZE),
|
FMath::FloorToInt(Position.Y / (float)CHUNK_SIZE),
|
||||||
FMath::FloorToInt(Position.Z / Manager->GetStrateLayoutChunkWidthVoxels()));
|
FMath::FloorToInt(Position.Z / (float)CHUNK_SIZE));
|
||||||
const TArray<int32>& Nearby = VF_GetNearbyPassages(Manager, ChunkCoord);
|
const TArray<int32>& Nearby = VF_GetNearbyPassages(Manager, ChunkCoord);
|
||||||
const TArray<FVoxelPassage>& Passages = Manager->GetPassages();
|
const TArray<FVoxelPassage>& Passages = Manager->GetPassages();
|
||||||
const FVoxelPassage* PassageData = Passages.GetData();
|
const FVoxelPassage* PassageData = Passages.GetData();
|
||||||
@@ -1160,7 +1138,7 @@ static bool VF_IsAnyPassageFloorAt(
|
|||||||
const FIntVector ChunkCoord(
|
const FIntVector ChunkCoord(
|
||||||
FMath::FloorToInt(Position.X / (float)CHUNK_SIZE),
|
FMath::FloorToInt(Position.X / (float)CHUNK_SIZE),
|
||||||
FMath::FloorToInt(Position.Y / (float)CHUNK_SIZE),
|
FMath::FloorToInt(Position.Y / (float)CHUNK_SIZE),
|
||||||
FMath::FloorToInt(Position.Z / Manager->GetStrateLayoutChunkWidthVoxels()));
|
FMath::FloorToInt(Position.Z / (float)CHUNK_SIZE));
|
||||||
const TArray<int32>& Nearby = VF_GetNearbyPassages(Manager, ChunkCoord);
|
const TArray<int32>& Nearby = VF_GetNearbyPassages(Manager, ChunkCoord);
|
||||||
const TArray<FVoxelPassage>& Passages = Manager->GetPassages();
|
const TArray<FVoxelPassage>& Passages = Manager->GetPassages();
|
||||||
const FVoxelPassage* PassageData = Passages.GetData();
|
const FVoxelPassage* PassageData = Passages.GetData();
|
||||||
@@ -1191,17 +1169,6 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
|||||||
UE_LOG(LogTemp, Error, TEXT("[StrateManager] No settings provided!"));
|
UE_LOG(LogTemp, Error, TEXT("[StrateManager] No settings provided!"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!FMath::IsFinite(Settings->StrateLayoutChunkSizeMeters)
|
|
||||||
|| Settings->StrateLayoutChunkSizeMeters < 0.25f
|
|
||||||
|| Settings->StrateLayoutChunkSizeMeters > 64.0f)
|
|
||||||
{
|
|
||||||
UE_LOG(LogTemp, Error, TEXT("[StrateManager] StrateLayoutChunkSizeMeters must be in [0.25, 64] m."));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
StrateLayoutChunkSizeMicrometres = static_cast<uint32>(std::llround(
|
|
||||||
static_cast<double>(Settings->StrateLayoutChunkSizeMeters) * 1000000.0));
|
|
||||||
StrateLayoutChunkWidthVoxels = static_cast<float>(
|
|
||||||
static_cast<double>(StrateLayoutChunkSizeMicrometres) / (10000.0 * VOXEL_SIZE));
|
|
||||||
|
|
||||||
FVoxelSeasonManifest SeasonManifest;
|
FVoxelSeasonManifest SeasonManifest;
|
||||||
bool bUseSeason = false;
|
bool bUseSeason = false;
|
||||||
@@ -1255,8 +1222,7 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
|||||||
? SeasonManifest.Strates.Num() : Settings->TotalStrates;
|
? SeasonManifest.Strates.Num() : Settings->TotalStrates;
|
||||||
const int32 LayoutGapChunks = bUseSeason
|
const int32 LayoutGapChunks = bUseSeason
|
||||||
? SeasonManifest.InterStrateGapChunks
|
? SeasonManifest.InterStrateGapChunks
|
||||||
: FMath::Max(0, FMath::RoundToInt(static_cast<double>(Settings->InterStrateGapMeters)
|
: FMath::Max(0, Settings->InterStrateGapChunks);
|
||||||
* 1000000.0 / std::max<uint32>(1, StrateLayoutChunkSizeMicrometres)));
|
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// STEP 1: Build shuffled pool (seed-based randomization)
|
// STEP 1: Build shuffled pool (seed-based randomization)
|
||||||
@@ -1323,11 +1289,8 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve the stack in physical integer micrometres first. The historical chunk indices are
|
// Current Z position (in chunks). Starts at 0 and goes downward (negative).
|
||||||
// projected from these half-open bounds only after each slot is complete.
|
int32 CurrentTopZ = 0;
|
||||||
int64 CurrentTopWorldMicrometres = StrateLayoutChunkSizeMicrometres;
|
|
||||||
const int64 ResolvedGapMicrometres = static_cast<int64>(LayoutGapChunks)
|
|
||||||
* StrateLayoutChunkSizeMicrometres;
|
|
||||||
|
|
||||||
for (int32 i = 0; i < TotalStrates; i++)
|
for (int32 i = 0; i < TotalStrates; i++)
|
||||||
{
|
{
|
||||||
@@ -1370,8 +1333,7 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
|||||||
}
|
}
|
||||||
|
|
||||||
Slot.Definition->GeneratorType = SeasonStrate.Archetype;
|
Slot.Definition->GeneratorType = SeasonStrate.Archetype;
|
||||||
Slot.Definition->StrateHeightMeters = static_cast<double>(SeasonStrate.HeightInChunks)
|
Slot.Definition->StrateHeightInChunks = SeasonStrate.HeightInChunks;
|
||||||
* StrateLayoutChunkSizeMicrometres / 1000000.0;
|
|
||||||
Slot.Definition->GenerationParams = SeasonStrate.Params.TunnelNetworkParams;
|
Slot.Definition->GenerationParams = SeasonStrate.Params.TunnelNetworkParams;
|
||||||
Slot.Definition->SlabParams = SeasonStrate.Params.SlabParams;
|
Slot.Definition->SlabParams = SeasonStrate.Params.SlabParams;
|
||||||
Slot.Definition->MazeParams = SeasonStrate.Params.MazeParams;
|
Slot.Definition->MazeParams = SeasonStrate.Params.MazeParams;
|
||||||
@@ -1380,31 +1342,18 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
|||||||
Slot.Definition->FloatingIslandParams = SeasonStrate.Params.FloatingIslandParams;
|
Slot.Definition->FloatingIslandParams = SeasonStrate.Params.FloatingIslandParams;
|
||||||
|
|
||||||
Slot.HeightInChunks = SeasonStrate.HeightInChunks;
|
Slot.HeightInChunks = SeasonStrate.HeightInChunks;
|
||||||
const int32 LayoutChunkVoxels=FMath::RoundToInt(StrateLayoutChunkWidthVoxels);
|
Slot.TopChunkZ = SeasonStrate.TopWorldZ / CHUNK_SIZE - 1;
|
||||||
Slot.TopChunkZ = SeasonStrate.TopWorldZ / LayoutChunkVoxels - 1;
|
Slot.BottomChunkZ = SeasonStrate.BottomWorldZ / CHUNK_SIZE;
|
||||||
Slot.BottomChunkZ = SeasonStrate.BottomWorldZ / LayoutChunkVoxels;
|
if (SeasonStrate.TopWorldZ % CHUNK_SIZE != 0
|
||||||
Slot.TopWorldMicrometres = (static_cast<int64>(Slot.TopChunkZ) + 1)
|
|| SeasonStrate.BottomWorldZ % CHUNK_SIZE != 0
|
||||||
* StrateLayoutChunkSizeMicrometres;
|
|| Slot.TopChunkZ != CurrentTopZ
|
||||||
Slot.BottomWorldMicrometres = static_cast<int64>(Slot.BottomChunkZ)
|
|| Slot.BottomChunkZ != CurrentTopZ - (Slot.HeightInChunks - 1))
|
||||||
* StrateLayoutChunkSizeMicrometres;
|
|
||||||
Slot.HeightMicrometres = Slot.TopWorldMicrometres - Slot.BottomWorldMicrometres;
|
|
||||||
const int64 ExpectedTopChunk = VF_FloorDivide64(
|
|
||||||
CurrentTopWorldMicrometres, StrateLayoutChunkSizeMicrometres) - 1;
|
|
||||||
const int64 ExpectedBottomChunk = ExpectedTopChunk - (Slot.HeightInChunks - 1);
|
|
||||||
if (SeasonStrate.TopWorldZ % LayoutChunkVoxels != 0
|
|
||||||
|| SeasonStrate.BottomWorldZ % LayoutChunkVoxels != 0
|
|
||||||
|| Slot.TopChunkZ != ExpectedTopChunk
|
|
||||||
|| Slot.BottomChunkZ != ExpectedBottomChunk
|
|
||||||
|| Slot.TopWorldMicrometres != CurrentTopWorldMicrometres
|
|
||||||
|| Slot.HeightMicrometres != static_cast<int64>(Slot.HeightInChunks)
|
|
||||||
* StrateLayoutChunkSizeMicrometres)
|
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Error,
|
UE_LOG(LogTemp, Error,
|
||||||
TEXT("[StrateManager] Season slot %d bounds do not describe the declared stacked layout."), i);
|
TEXT("[StrateManager] Season slot %d bounds do not describe the declared stacked layout."), i);
|
||||||
RestorePreviousLayout();
|
RestorePreviousLayout();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
CurrentTopWorldMicrometres = Slot.BottomWorldMicrometres - ResolvedGapMicrometres;
|
|
||||||
SeasonStrates.Add(SeasonStrate);
|
SeasonStrates.Add(SeasonStrate);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -1427,54 +1376,15 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve the authored chunk height into an exact physical interval.
|
// Compute Z range from definition's height
|
||||||
Slot.HeightInChunks = FMath::Max(1, FMath::RoundToInt(
|
Slot.HeightInChunks = Slot.Definition->StrateHeightInChunks;
|
||||||
Slot.Definition->StrateHeightMeters * 1000000.0
|
Slot.TopChunkZ = CurrentTopZ;
|
||||||
/ std::max<uint32>(1, StrateLayoutChunkSizeMicrometres)));
|
Slot.BottomChunkZ = CurrentTopZ - (Slot.HeightInChunks - 1);
|
||||||
Slot.TopWorldMicrometres = CurrentTopWorldMicrometres;
|
|
||||||
Slot.HeightMicrometres = static_cast<int64>(Slot.HeightInChunks)
|
|
||||||
* StrateLayoutChunkSizeMicrometres;
|
|
||||||
Slot.BottomWorldMicrometres = Slot.TopWorldMicrometres - Slot.HeightMicrometres;
|
|
||||||
Slot.TopChunkZ = static_cast<int32>(VF_FloorDivide64(
|
|
||||||
Slot.TopWorldMicrometres, StrateLayoutChunkSizeMicrometres) - 1);
|
|
||||||
Slot.BottomChunkZ = static_cast<int32>(VF_FloorDivide64(
|
|
||||||
Slot.BottomWorldMicrometres, StrateLayoutChunkSizeMicrometres));
|
|
||||||
CurrentTopWorldMicrometres = Slot.BottomWorldMicrometres - ResolvedGapMicrometres;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Slot.ResolvedTransitionType = Slot.Definition->TransitionType;
|
// Move the cursor down for the next strate, leaving a solid-bedrock gap of
|
||||||
Slot.ResolvedTransitionBlendChunks = FMath::Max(0, FMath::RoundToInt(
|
// InterStrateGapChunks chunks between this strate and the next.
|
||||||
Slot.Definition->TransitionWidthMeters * 1000000.0
|
CurrentTopZ = Slot.BottomChunkZ - 1 - LayoutGapChunks;
|
||||||
/ std::max<uint32>(1, StrateLayoutChunkSizeMicrometres)));
|
|
||||||
Slot.TransitionWidthMicrometres = static_cast<int64>(Slot.ResolvedTransitionBlendChunks)
|
|
||||||
* StrateLayoutChunkSizeMicrometres;
|
|
||||||
Slot.ResolvedPassageConfig = Slot.Definition->PassageConfig;
|
|
||||||
Slot.PassageMouthRadiusMicrometres = VF_OwnerVoxelsToMicrometres(
|
|
||||||
Slot.ResolvedPassageConfig.MouthRadius);
|
|
||||||
Slot.PassageMidRadiusMicrometres = VF_OwnerVoxelsToMicrometres(
|
|
||||||
Slot.ResolvedPassageConfig.MidRadius);
|
|
||||||
Slot.PassageReachMinimumMicrometres = VF_OwnerVoxelsToMicrometres(
|
|
||||||
Slot.ResolvedPassageConfig.ReachMin);
|
|
||||||
Slot.PassageReachMaximumMicrometres = VF_OwnerVoxelsToMicrometres(
|
|
||||||
Slot.ResolvedPassageConfig.ReachMax);
|
|
||||||
Slot.PassageDistanceMinimumMicrometres = VF_OwnerVoxelsToMicrometres(
|
|
||||||
Slot.ResolvedPassageConfig.DistanceMin);
|
|
||||||
Slot.PassageDistanceMaximumMicrometres = VF_OwnerVoxelsToMicrometres(
|
|
||||||
Slot.ResolvedPassageConfig.DistanceMax);
|
|
||||||
Slot.PassageWanderMicrometres = VF_OwnerVoxelsToMicrometres(
|
|
||||||
Slot.ResolvedPassageConfig.Wander);
|
|
||||||
Slot.PassageVerticalWobbleMicrometres = VF_OwnerVoxelsToMicrometres(
|
|
||||||
Slot.ResolvedPassageConfig.VerticalWobble);
|
|
||||||
Slot.PassageSpiralRadiusMicrometres = VF_OwnerVoxelsToMicrometres(
|
|
||||||
Slot.ResolvedPassageConfig.SpiralRadius);
|
|
||||||
Slot.PassageCascadeLedgeMicrometres = VF_OwnerVoxelsToMicrometres(
|
|
||||||
Slot.ResolvedPassageConfig.CascadeLedge);
|
|
||||||
Slot.LegacyTopWorldZVoxels = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
|
||||||
Slot.TopWorldMicrometres);
|
|
||||||
Slot.LegacyBottomWorldZVoxels = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
|
||||||
Slot.BottomWorldMicrometres);
|
|
||||||
Slot.LegacyHeightVoxels = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
|
||||||
Slot.HeightMicrometres);
|
|
||||||
|
|
||||||
StrateLayout.Add(Slot);
|
StrateLayout.Add(Slot);
|
||||||
|
|
||||||
@@ -1507,7 +1417,6 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
|||||||
OriginSpineRadius = bUseSeason
|
OriginSpineRadius = bUseSeason
|
||||||
? SeasonManifest.OriginSpineRadius : Settings->OriginSpineRadius;
|
? SeasonManifest.OriginSpineRadius : Settings->OriginSpineRadius;
|
||||||
InterStrateGapChunks = LayoutGapChunks;
|
InterStrateGapChunks = LayoutGapChunks;
|
||||||
ResolvedInterStrateGapMicrometres = ResolvedGapMicrometres;
|
|
||||||
if (bUseSeason)
|
if (bUseSeason)
|
||||||
{
|
{
|
||||||
ActiveSeasonContentHash = SeasonManifest.ContentHash;
|
ActiveSeasonContentHash = SeasonManifest.ContentHash;
|
||||||
@@ -1616,8 +1525,8 @@ bool UVoxelStrateManager::SetComposerOverrideForStrate(
|
|||||||
}
|
}
|
||||||
VF_SetComposerRuntimeBounds(
|
VF_SetComposerRuntimeBounds(
|
||||||
Override->Params,
|
Override->Params,
|
||||||
TargetSlot->LegacyTopWorldZVoxels,
|
(float)(TargetSlot->TopChunkZ + 1) * CHUNK_SIZE,
|
||||||
TargetSlot->LegacyBottomWorldZVoxels);
|
(float)TargetSlot->BottomChunkZ * CHUNK_SIZE);
|
||||||
Override->bUseRecipe = bUseRecipe;
|
Override->bUseRecipe = bUseRecipe;
|
||||||
if (Recipe != nullptr)
|
if (Recipe != nullptr)
|
||||||
{
|
{
|
||||||
@@ -1636,8 +1545,8 @@ bool UVoxelStrateManager::SetComposerOverrideForStrate(
|
|||||||
Override->bUseRegions = true;
|
Override->bUseRegions = true;
|
||||||
Override->Regions = *InRegions;
|
Override->Regions = *InRegions;
|
||||||
VF_RekeyStrateRegionManifest(Override->Regions, CandidateSeed, StrateIndex);
|
VF_RekeyStrateRegionManifest(Override->Regions, CandidateSeed, StrateIndex);
|
||||||
Override->Regions.StrateTopWorldZ = TargetSlot->LegacyTopWorldZVoxels;
|
Override->Regions.StrateTopWorldZ = (float)(TargetSlot->TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
Override->Regions.StrateBottomWorldZ = TargetSlot->LegacyBottomWorldZVoxels;
|
Override->Regions.StrateBottomWorldZ = (float)TargetSlot->BottomChunkZ * CHUNK_SIZE;
|
||||||
VF_SetStrateArchetypeRuntimeBounds(Override->Params,
|
VF_SetStrateArchetypeRuntimeBounds(Override->Params,
|
||||||
Override->Regions.StrateTopWorldZ,
|
Override->Regions.StrateTopWorldZ,
|
||||||
Override->Regions.StrateBottomWorldZ);
|
Override->Regions.StrateBottomWorldZ);
|
||||||
@@ -1884,36 +1793,14 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
// that opens the next room rather than a second vertical spine.
|
// that opens the next room rather than a second vertical spine.
|
||||||
const UVoxelStrateDefinition* UpperDef = Upper.Definition;
|
const UVoxelStrateDefinition* UpperDef = Upper.Definition;
|
||||||
if (!UpperDef) continue;
|
if (!UpperDef) continue;
|
||||||
// GeneratePassages is the legacy owner-voxel route adapter. Its immutable source is the
|
const FStratePassageConfig& Cfg = UpperDef->PassageConfig;
|
||||||
// physical passage record resolved during Initialize, converted back to owner voxels here.
|
|
||||||
FStratePassageConfig Cfg = Upper.ResolvedPassageConfig;
|
|
||||||
Cfg.MouthRadius = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
|
||||||
Upper.PassageMouthRadiusMicrometres);
|
|
||||||
Cfg.MidRadius = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
|
||||||
Upper.PassageMidRadiusMicrometres);
|
|
||||||
Cfg.ReachMin = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
|
||||||
Upper.PassageReachMinimumMicrometres);
|
|
||||||
Cfg.ReachMax = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
|
||||||
Upper.PassageReachMaximumMicrometres);
|
|
||||||
Cfg.DistanceMin = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
|
||||||
Upper.PassageDistanceMinimumMicrometres);
|
|
||||||
Cfg.DistanceMax = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
|
||||||
Upper.PassageDistanceMaximumMicrometres);
|
|
||||||
Cfg.Wander = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
|
||||||
Upper.PassageWanderMicrometres);
|
|
||||||
Cfg.VerticalWobble = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
|
||||||
Upper.PassageVerticalWobbleMicrometres);
|
|
||||||
Cfg.SpiralRadius = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
|
||||||
Upper.PassageSpiralRadiusMicrometres);
|
|
||||||
Cfg.CascadeLedge = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
|
||||||
Upper.PassageCascadeLedgeMicrometres);
|
|
||||||
|
|
||||||
// Upper strate floor and lower strate ceiling (differ when there's a bedrock gap).
|
// Upper strate floor and lower strate ceiling (differ when there's a bedrock gap).
|
||||||
const float UpperTopZ = Upper.LegacyTopWorldZVoxels;
|
const float UpperTopZ = (float)(Upper.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
const float UpperBottomZ = Upper.LegacyBottomWorldZVoxels;
|
const float UpperBottomZ = (float)(Upper.BottomChunkZ) * CHUNK_SIZE;
|
||||||
const float LowerTopZ = Lower.LegacyTopWorldZVoxels;
|
const float LowerTopZ = (float)(Lower.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
const float UpperMax = Upper.LegacyHeightVoxels * 0.9f;
|
const float UpperMax = (float)Upper.HeightInChunks * CHUNK_SIZE * 0.9f;
|
||||||
const float LowerMax = Lower.LegacyHeightVoxels * 0.9f;
|
const float LowerMax = (float)Lower.HeightInChunks * CHUNK_SIZE * 0.9f;
|
||||||
|
|
||||||
const float DistLo = FMath::Min(Cfg.DistanceMin, Cfg.DistanceMax);
|
const float DistLo = FMath::Min(Cfg.DistanceMin, Cfg.DistanceMax);
|
||||||
const float DistHi = FMath::Max(Cfg.DistanceMin, Cfg.DistanceMax);
|
const float DistHi = FMath::Max(Cfg.DistanceMin, Cfg.DistanceMax);
|
||||||
@@ -2043,7 +1930,7 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
LowerDef->FloatingIslandParams,
|
LowerDef->FloatingIslandParams,
|
||||||
LowerQuerySeed,
|
LowerQuerySeed,
|
||||||
LowerTopZ,
|
LowerTopZ,
|
||||||
Lower.LegacyBottomWorldZVoxels,
|
(float)(Lower.BottomChunkZ) * CHUNK_SIZE,
|
||||||
PX,
|
PX,
|
||||||
PY,
|
PY,
|
||||||
MaxLateralSnap,
|
MaxLateralSnap,
|
||||||
@@ -2103,7 +1990,7 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
if (bAimedLowerAtPlayerFitPoint)
|
if (bAimedLowerAtPlayerFitPoint)
|
||||||
{
|
{
|
||||||
const float LowerSealThickness = BoundarySealThicknessFor(*LowerDef);
|
const float LowerSealThickness = BoundarySealThicknessFor(*LowerDef);
|
||||||
const float LowerBottomZ = Lower.LegacyBottomWorldZVoxels;
|
const float LowerBottomZ = (float)(Lower.BottomChunkZ) * CHUNK_SIZE;
|
||||||
const float InnerBottomZ = LowerBottomZ + LowerSealThickness;
|
const float InnerBottomZ = LowerBottomZ + LowerSealThickness;
|
||||||
const float InnerTopZ = LowerTopZ - LowerSealThickness;
|
const float InnerTopZ = LowerTopZ - LowerSealThickness;
|
||||||
if (VoxelMath::IsFinite(SuggestedLowerPoint.X)
|
if (VoxelMath::IsFinite(SuggestedLowerPoint.X)
|
||||||
@@ -2271,7 +2158,7 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
Cfg.MouthRadius,
|
Cfg.MouthRadius,
|
||||||
LowerDoorDirection,
|
LowerDoorDirection,
|
||||||
LowerTopZ,
|
LowerTopZ,
|
||||||
Lower.LegacyBottomWorldZVoxels,
|
(float)(Lower.BottomChunkZ) * CHUNK_SIZE,
|
||||||
LowerDef ? BoundarySealThicknessFor(*LowerDef) : 0.0f,
|
LowerDef ? BoundarySealThicknessFor(*LowerDef) : 0.0f,
|
||||||
bAimedLowerAtPlayerFitPoint);
|
bAimedLowerAtPlayerFitPoint);
|
||||||
Passage.UpperPoint = Passage.UpperLanding.StandingPoint;
|
Passage.UpperPoint = Passage.UpperLanding.StandingPoint;
|
||||||
@@ -2696,8 +2583,8 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
if (bOpenSurfaceEntry && OriginSpineRadius > 0.0f && StrateLayout.Num() > 0)
|
if (bOpenSurfaceEntry && OriginSpineRadius > 0.0f && StrateLayout.Num() > 0)
|
||||||
{
|
{
|
||||||
const FStrateSlot& Top = StrateLayout[0];
|
const FStrateSlot& Top = StrateLayout[0];
|
||||||
const float TopZ = Top.LegacyTopWorldZVoxels;
|
const float TopZ = (float)(Top.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
const float BottomZ = Top.LegacyBottomWorldZVoxels;
|
const float BottomZ = (float)Top.BottomChunkZ * CHUNK_SIZE;
|
||||||
const float TopSeal = Top.Definition
|
const float TopSeal = Top.Definition
|
||||||
? BoundarySealThicknessFor(*Top.Definition) : 0.0f;
|
? BoundarySealThicknessFor(*Top.Definition) : 0.0f;
|
||||||
const VoxelPassageGeometry::FOriginLandingGeometry OriginLanding =
|
const VoxelPassageGeometry::FOriginLandingGeometry OriginLanding =
|
||||||
@@ -2720,7 +2607,7 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
// the top seal into the finite landing room; lower strate seals remain untouched.
|
// the top seal into the finite landing room; lower strate seals remain untouched.
|
||||||
Entry.Radius = FMath::Max(OriginSpineRadius,
|
Entry.Radius = FMath::Max(OriginSpineRadius,
|
||||||
VoxelPassageGeometry::MetersToVoxels(0.25f));
|
VoxelPassageGeometry::MetersToVoxels(0.25f));
|
||||||
Entry.UpperPoint = FVector(0.0f, 0.0f, TopZ + StrateLayoutChunkWidthVoxels);
|
Entry.UpperPoint = FVector(0.0f, 0.0f, TopZ + CHUNK_SIZE);
|
||||||
// The capsule's lower tangent meets the room ceiling; it never bores through the
|
// The capsule's lower tangent meets the room ceiling; it never bores through the
|
||||||
// room's floor or creates an origin column in strate 0.
|
// room's floor or creates an origin column in strate 0.
|
||||||
Entry.LowerPoint = FVector(
|
Entry.LowerPoint = FVector(
|
||||||
@@ -2784,7 +2671,7 @@ float UVoxelStrateManager::EvaluateModifierSDF(float WorldX, float WorldY, float
|
|||||||
const FIntVector ChunkCoord(
|
const FIntVector ChunkCoord(
|
||||||
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
|
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
|
||||||
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
|
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
|
||||||
FMath::FloorToInt(WorldZ / StrateLayoutChunkWidthVoxels));
|
FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE));
|
||||||
|
|
||||||
// The shortlist is rebuilt once for a (manager, version, chunk) and is shared by the tube,
|
// The shortlist is rebuilt once for a (manager, version, chunk) and is shared by the tube,
|
||||||
// landing, and floor paths. No source-fit stencil or topology search is allowed below it.
|
// landing, and floor paths. No source-fit stencil or topology search is allowed below it.
|
||||||
@@ -2930,7 +2817,7 @@ void UVoxelStrateManager::ApplyPassageModifier(
|
|||||||
const FIntVector ChunkCoord(
|
const FIntVector ChunkCoord(
|
||||||
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
|
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
|
||||||
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
|
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
|
||||||
FMath::FloorToInt(WorldZ / StrateLayoutChunkWidthVoxels));
|
FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE));
|
||||||
const TArray<int32>& Nearby = VF_GetNearbyPassages(this, ChunkCoord);
|
const TArray<int32>& Nearby = VF_GetNearbyPassages(this, ChunkCoord);
|
||||||
const FVoxelPassage* PassageData = Passages.GetData();
|
const FVoxelPassage* PassageData = Passages.GetData();
|
||||||
const FVector Position(WorldX, WorldY, WorldZ);
|
const FVector Position(WorldX, WorldY, WorldZ);
|
||||||
@@ -3023,7 +2910,7 @@ void UVoxelStrateManager::ApplyPassageLandingAir(
|
|||||||
const FIntVector ChunkCoord(
|
const FIntVector ChunkCoord(
|
||||||
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
|
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
|
||||||
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
|
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
|
||||||
FMath::FloorToInt(WorldZ / StrateLayoutChunkWidthVoxels));
|
FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE));
|
||||||
const TArray<int32>& Nearby = VF_GetNearbyPassages(this, ChunkCoord);
|
const TArray<int32>& Nearby = VF_GetNearbyPassages(this, ChunkCoord);
|
||||||
if (Nearby.Num() == 0) return;
|
if (Nearby.Num() == 0) return;
|
||||||
|
|
||||||
@@ -3134,7 +3021,7 @@ void UVoxelStrateManager::ApplyPassageLandingFloorMC(
|
|||||||
const FIntVector ChunkCoord(
|
const FIntVector ChunkCoord(
|
||||||
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
|
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
|
||||||
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
|
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
|
||||||
FMath::FloorToInt(WorldZ / StrateLayoutChunkWidthVoxels));
|
FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE));
|
||||||
const TArray<int32>& Nearby = VF_GetNearbyPassages(this, ChunkCoord);
|
const TArray<int32>& Nearby = VF_GetNearbyPassages(this, ChunkCoord);
|
||||||
const FVoxelPassage* PassageData = Passages.GetData();
|
const FVoxelPassage* PassageData = Passages.GetData();
|
||||||
const FVector Position(WorldX, WorldY, WorldZ);
|
const FVector Position(WorldX, WorldY, WorldZ);
|
||||||
@@ -3230,7 +3117,7 @@ void UVoxelStrateManager::ApplyPassageStructuralPostsMC(
|
|||||||
const FIntVector ChunkCoord(
|
const FIntVector ChunkCoord(
|
||||||
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
|
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
|
||||||
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
|
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
|
||||||
FMath::FloorToInt(WorldZ / StrateLayoutChunkWidthVoxels));
|
FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE));
|
||||||
const TArray<int32>& Nearby = VF_GetNearbyPassages(this, ChunkCoord);
|
const TArray<int32>& Nearby = VF_GetNearbyPassages(this, ChunkCoord);
|
||||||
if (Nearby.Num() == 0) return;
|
if (Nearby.Num() == 0) return;
|
||||||
|
|
||||||
@@ -3357,7 +3244,7 @@ void UVoxelStrateManager::ApplyPassageNativeFloorMC(
|
|||||||
const FIntVector ChunkCoord(
|
const FIntVector ChunkCoord(
|
||||||
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
|
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
|
||||||
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
|
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
|
||||||
FMath::FloorToInt(WorldZ / StrateLayoutChunkWidthVoxels));
|
FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE));
|
||||||
const TArray<int32>& Nearby = VF_GetNearbyPassages(this, ChunkCoord);
|
const TArray<int32>& Nearby = VF_GetNearbyPassages(this, ChunkCoord);
|
||||||
if (Nearby.Num() == 0
|
if (Nearby.Num() == 0
|
||||||
|| VoxelPassageGeometry::VerticalShaftConnectorAirMarker())
|
|| VoxelPassageGeometry::VerticalShaftConnectorAirMarker())
|
||||||
@@ -3548,7 +3435,7 @@ void UVoxelStrateManager::ApplyPassageLandingRoomFloorMC(
|
|||||||
const FIntVector ChunkCoord(
|
const FIntVector ChunkCoord(
|
||||||
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
|
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
|
||||||
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
|
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
|
||||||
FMath::FloorToInt(WorldZ / StrateLayoutChunkWidthVoxels));
|
FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE));
|
||||||
const TArray<int32>& Nearby = VF_GetNearbyPassages(this, ChunkCoord);
|
const TArray<int32>& Nearby = VF_GetNearbyPassages(this, ChunkCoord);
|
||||||
const FVoxelPassage* PassageData = Passages.GetData();
|
const FVoxelPassage* PassageData = Passages.GetData();
|
||||||
const FVector Position(WorldX, WorldY, WorldZ);
|
const FVector Position(WorldX, WorldY, WorldZ);
|
||||||
@@ -6022,8 +5909,8 @@ bool UVoxelStrateManager::AnyLandingFloorAtLattice(
|
|||||||
for (const FStrateSlot& Slot : StrateLayout)
|
for (const FStrateSlot& Slot : StrateLayout)
|
||||||
{
|
{
|
||||||
if (Slot.Definition == nullptr) continue;
|
if (Slot.Definition == nullptr) continue;
|
||||||
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * CHUNK_SIZE;
|
||||||
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * CHUNK_SIZE;
|
||||||
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
||||||
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
|
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
|
||||||
VoxelPassageGeometry::BuildOriginLandingGeometry(
|
VoxelPassageGeometry::BuildOriginLandingGeometry(
|
||||||
@@ -6065,8 +5952,8 @@ bool UVoxelStrateManager::AnyOriginLandingNearBox(
|
|||||||
for (const FStrateSlot& Slot : StrateLayout)
|
for (const FStrateSlot& Slot : StrateLayout)
|
||||||
{
|
{
|
||||||
if (Slot.Definition == nullptr) continue;
|
if (Slot.Definition == nullptr) continue;
|
||||||
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * CHUNK_SIZE;
|
||||||
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * CHUNK_SIZE;
|
||||||
if (VoxelPassageGeometry::OriginLandingRoomTouchesBox(
|
if (VoxelPassageGeometry::OriginLandingRoomTouchesBox(
|
||||||
VoxelBox, TopZ, BottomZ,
|
VoxelBox, TopZ, BottomZ,
|
||||||
VF_BoundarySealThicknessForDefinition(*Slot.Definition),
|
VF_BoundarySealThicknessForDefinition(*Slot.Definition),
|
||||||
@@ -6086,8 +5973,8 @@ bool UVoxelStrateManager::AnyOriginLandingFloorNearBox(
|
|||||||
for (const FStrateSlot& Slot : StrateLayout)
|
for (const FStrateSlot& Slot : StrateLayout)
|
||||||
{
|
{
|
||||||
if (Slot.Definition == nullptr) continue;
|
if (Slot.Definition == nullptr) continue;
|
||||||
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * CHUNK_SIZE;
|
||||||
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * CHUNK_SIZE;
|
||||||
if (VoxelPassageGeometry::OriginLandingFloorTouchesBox(
|
if (VoxelPassageGeometry::OriginLandingFloorTouchesBox(
|
||||||
VoxelBox, TopZ, BottomZ,
|
VoxelBox, TopZ, BottomZ,
|
||||||
VF_BoundarySealThicknessForDefinition(*Slot.Definition),
|
VF_BoundarySealThicknessForDefinition(*Slot.Definition),
|
||||||
@@ -6111,8 +5998,8 @@ bool UVoxelStrateManager::AnyOriginLandingNearLattice(
|
|||||||
for (const FStrateSlot& Slot : StrateLayout)
|
for (const FStrateSlot& Slot : StrateLayout)
|
||||||
{
|
{
|
||||||
if (Slot.Definition == nullptr) continue;
|
if (Slot.Definition == nullptr) continue;
|
||||||
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * CHUNK_SIZE;
|
||||||
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * CHUNK_SIZE;
|
||||||
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
||||||
if (VoxelPassageGeometry::OriginLandingRoomTouchesLattice(
|
if (VoxelPassageGeometry::OriginLandingRoomTouchesLattice(
|
||||||
VoxelBox, LatticeOrigin, Step, TopZ, BottomZ, Seal, OriginSpineRadius,
|
VoxelBox, LatticeOrigin, Step, TopZ, BottomZ, Seal, OriginSpineRadius,
|
||||||
@@ -6149,8 +6036,8 @@ bool UVoxelStrateManager::AnyOriginLandingAirNearLattice(
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * CHUNK_SIZE;
|
||||||
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * CHUNK_SIZE;
|
||||||
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
||||||
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
|
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
|
||||||
VoxelPassageGeometry::BuildOriginLandingGeometry(
|
VoxelPassageGeometry::BuildOriginLandingGeometry(
|
||||||
@@ -6226,8 +6113,8 @@ bool UVoxelStrateManager::AnyOriginLandingFloorNearLattice(
|
|||||||
for (const FStrateSlot& Slot : StrateLayout)
|
for (const FStrateSlot& Slot : StrateLayout)
|
||||||
{
|
{
|
||||||
if (Slot.Definition == nullptr) continue;
|
if (Slot.Definition == nullptr) continue;
|
||||||
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * CHUNK_SIZE;
|
||||||
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * CHUNK_SIZE;
|
||||||
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
||||||
if (VoxelPassageGeometry::OriginLandingFloorTouchesLattice(
|
if (VoxelPassageGeometry::OriginLandingFloorTouchesLattice(
|
||||||
VoxelBox, LatticeOrigin, Step, TopZ, BottomZ, Seal, OriginSpineRadius))
|
VoxelBox, LatticeOrigin, Step, TopZ, BottomZ, Seal, OriginSpineRadius))
|
||||||
@@ -6244,36 +6131,15 @@ bool UVoxelStrateManager::AnyOriginLandingFloorNearLattice(
|
|||||||
|
|
||||||
int32 UVoxelStrateManager::FindSlotIndexForChunkZ(int32 ChunkZ) const
|
int32 UVoxelStrateManager::FindSlotIndexForChunkZ(int32 ChunkZ) const
|
||||||
{
|
{
|
||||||
// Legacy chunk callers use the projected interval cached during Initialize. The source bounds
|
// Linear search through strate layout.
|
||||||
// are the slot's physical micrometre interval; these indices do not define or stack strates.
|
// With ~10-20 strates this is fine. If we ever have hundreds,
|
||||||
for (int32 i = 0; i < StrateLayout.Num(); ++i)
|
// switch to binary search (layout is sorted by Z).
|
||||||
|
for (int32 i = 0; i < StrateLayout.Num(); i++)
|
||||||
{
|
{
|
||||||
const FStrateSlot& Slot = StrateLayout[i];
|
const FStrateSlot& Slot = StrateLayout[i];
|
||||||
if (ChunkZ <= Slot.TopChunkZ && ChunkZ >= Slot.BottomChunkZ) return i;
|
if (ChunkZ <= Slot.TopChunkZ && ChunkZ >= Slot.BottomChunkZ)
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int32 UVoxelStrateManager::FindSlotIndexForWorldZCentimetres(double WorldZCentimetres) const
|
|
||||||
{
|
{
|
||||||
const double WorldZMicrometres = WorldZCentimetres * 10000.0;
|
return i;
|
||||||
int32 Low = 0;
|
|
||||||
int32 High = StrateLayout.Num() - 1;
|
|
||||||
while (Low <= High)
|
|
||||||
{
|
|
||||||
const int32 Mid = Low + (High - Low) / 2;
|
|
||||||
const FStrateSlot& Slot = StrateLayout[Mid];
|
|
||||||
if (WorldZMicrometres >= static_cast<double>(Slot.TopWorldMicrometres))
|
|
||||||
{
|
|
||||||
High = Mid - 1;
|
|
||||||
}
|
|
||||||
else if (WorldZMicrometres < static_cast<double>(Slot.BottomWorldMicrometres))
|
|
||||||
{
|
|
||||||
Low = Mid + 1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return Mid;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
@@ -6281,8 +6147,9 @@ int32 UVoxelStrateManager::FindSlotIndexForWorldZCentimetres(double WorldZCentim
|
|||||||
|
|
||||||
UVoxelStrateDefinition* UVoxelStrateManager::GetStrateAt(float WorldZ) const
|
UVoxelStrateDefinition* UVoxelStrateManager::GetStrateAt(float WorldZ) const
|
||||||
{
|
{
|
||||||
if (!FMath::IsFinite(WorldZ)) return nullptr;
|
// Convert world Z to chunk Z coordinate
|
||||||
const int32 SlotIdx = FindSlotIndexForWorldZCentimetres(WorldZ);
|
int32 ChunkZ = FMath::FloorToInt((WorldZ / VOXEL_SIZE) / CHUNK_SIZE);
|
||||||
|
int32 SlotIdx = FindSlotIndexForChunkZ(ChunkZ);
|
||||||
if (SlotIdx >= 0)
|
if (SlotIdx >= 0)
|
||||||
{
|
{
|
||||||
return StrateLayout[SlotIdx].Definition;
|
return StrateLayout[SlotIdx].Definition;
|
||||||
@@ -6292,8 +6159,8 @@ UVoxelStrateDefinition* UVoxelStrateManager::GetStrateAt(float WorldZ) const
|
|||||||
|
|
||||||
int32 UVoxelStrateManager::GetStrateIndex(float WorldZ) const
|
int32 UVoxelStrateManager::GetStrateIndex(float WorldZ) const
|
||||||
{
|
{
|
||||||
if (!FMath::IsFinite(WorldZ)) return -1;
|
int32 ChunkZ = FMath::FloorToInt((WorldZ / VOXEL_SIZE) / CHUNK_SIZE);
|
||||||
const int32 SlotIdx = FindSlotIndexForWorldZCentimetres(WorldZ);
|
int32 SlotIdx = FindSlotIndexForChunkZ(ChunkZ);
|
||||||
if (SlotIdx >= 0)
|
if (SlotIdx >= 0)
|
||||||
{
|
{
|
||||||
return StrateLayout[SlotIdx].StrateIndex;
|
return StrateLayout[SlotIdx].StrateIndex;
|
||||||
@@ -6406,13 +6273,9 @@ bool UVoxelStrateManager::IsGapChunk(const FIntVector& ChunkCoord) const
|
|||||||
if (StrateLayout.Num() == 0) return false;
|
if (StrateLayout.Num() == 0) return false;
|
||||||
|
|
||||||
// Above the top strate or below the bottom strate = open air, NOT a gap.
|
// Above the top strate or below the bottom strate = open air, NOT a gap.
|
||||||
const int64 ChunkCenterMicrometres = static_cast<int64>(ChunkCoord.Z)
|
const int32 StackTop = StrateLayout[0].TopChunkZ;
|
||||||
* StrateLayoutChunkSizeMicrometres + StrateLayoutChunkSizeMicrometres / 2;
|
const int32 StackBottom = StrateLayout.Last().BottomChunkZ;
|
||||||
if (ChunkCenterMicrometres >= StrateLayout[0].TopWorldMicrometres
|
if (ChunkCoord.Z > StackTop || ChunkCoord.Z < StackBottom) return false;
|
||||||
|| ChunkCenterMicrometres < StrateLayout.Last().BottomWorldMicrometres)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inside the stack's Z span but not in any strate slot → it's a bedrock gap.
|
// Inside the stack's Z span but not in any strate slot → it's a bedrock gap.
|
||||||
return FindSlotIndexForChunkZ(ChunkCoord.Z) < 0;
|
return FindSlotIndexForChunkZ(ChunkCoord.Z) < 0;
|
||||||
@@ -6455,9 +6318,9 @@ FSlabGenerationParams UVoxelStrateManager::GetSlabParamsForChunk(const FIntVecto
|
|||||||
|
|
||||||
// Fill in the runtime Z bounds (voxel coordinates, same convention as
|
// Fill in the runtime Z bounds (voxel coordinates, same convention as
|
||||||
// FStrateGenerationParams::StrateTopWorldZ / StrateBottomWorldZ).
|
// FStrateGenerationParams::StrateTopWorldZ / StrateBottomWorldZ).
|
||||||
// TopChunkZ+1 because the top chunk's CEILING is at (TopChunkZ+1)*StrateLayoutChunkWidthVoxels.
|
// TopChunkZ+1 because the top chunk's CEILING is at (TopChunkZ+1)*CHUNK_SIZE.
|
||||||
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
Result.StrateBottomWorldZ = (float)(Slot.BottomChunkZ) * CHUNK_SIZE;
|
||||||
|
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
@@ -6490,8 +6353,8 @@ FMazeGenerationParams UVoxelStrateManager::GetMazeParamsForChunk(const FIntVecto
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * CHUNK_SIZE;
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6517,8 +6380,8 @@ FSurfaceGenerationParams UVoxelStrateManager::GetSurfaceParamsForChunk(const FIn
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * CHUNK_SIZE;
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6544,8 +6407,8 @@ FVerticalShaftParams UVoxelStrateManager::GetVerticalShaftParamsForChunk(const F
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * CHUNK_SIZE;
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6571,8 +6434,8 @@ FFloatingIslandParams UVoxelStrateManager::GetFloatingIslandParamsForChunk(const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * CHUNK_SIZE;
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6606,14 +6469,14 @@ FBiomeContext UVoxelStrateManager::GetBiomeContextForChunk(const FIntVector& Chu
|
|||||||
|
|
||||||
bool UVoxelStrateManager::GetStrateUnrealZRange(float WorldZ, float& OutTopZ, float& OutBottomZ) const
|
bool UVoxelStrateManager::GetStrateUnrealZRange(float WorldZ, float& OutTopZ, float& OutBottomZ) const
|
||||||
{
|
{
|
||||||
if (!FMath::IsFinite(WorldZ)) return false;
|
const int32 ChunkZ = FMath::FloorToInt((WorldZ / VOXEL_SIZE) / CHUNK_SIZE);
|
||||||
const int32 SlotIdx = FindSlotIndexForWorldZCentimetres(WorldZ);
|
const int32 SlotIdx = FindSlotIndexForChunkZ(ChunkZ);
|
||||||
if (SlotIdx < 0) return false;
|
if (SlotIdx < 0) return false;
|
||||||
|
|
||||||
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
||||||
// Return the resolved physical bounds in Unreal centimeters.
|
// Voxel-space Z bounds → Unreal units. Ceiling = top chunk's upper edge.
|
||||||
OutTopZ = static_cast<float>(static_cast<double>(Slot.TopWorldMicrometres) / 10000.0);
|
OutTopZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE * VOXEL_SIZE;
|
||||||
OutBottomZ = static_cast<float>(static_cast<double>(Slot.BottomWorldMicrometres) / 10000.0);
|
OutBottomZ = (float)(Slot.BottomChunkZ) * CHUNK_SIZE * VOXEL_SIZE;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6626,8 +6489,8 @@ FStrateDisturbanceParams UVoxelStrateManager::GetDisturbanceParamsForChunk(const
|
|||||||
}
|
}
|
||||||
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
||||||
FStrateDisturbanceParams Result = Slot.Definition->Disturbances;
|
FStrateDisturbanceParams Result = Slot.Definition->Disturbances;
|
||||||
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
Result.StrateBottomWorldZ = (float)(Slot.BottomChunkZ) * CHUNK_SIZE;
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6659,8 +6522,8 @@ float UVoxelStrateManager::GetWaterLevelWorldZForChunk(const FIntVector& ChunkCo
|
|||||||
}
|
}
|
||||||
if (Rel <= 0.0f) return -FLT_MAX;
|
if (Rel <= 0.0f) return -FLT_MAX;
|
||||||
|
|
||||||
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
const float BottomZ = (float)(Slot.BottomChunkZ) * CHUNK_SIZE;
|
||||||
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
const float TopZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
return FMath::Lerp(BottomZ, TopZ, FMath::Clamp(Rel, 0.0f, 1.0f));
|
return FMath::Lerp(BottomZ, TopZ, FMath::Clamp(Rel, 0.0f, 1.0f));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6704,8 +6567,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
|||||||
FindComposerOverride(Slot.StrateIndex))
|
FindComposerOverride(Slot.StrateIndex))
|
||||||
{
|
{
|
||||||
FStrateGenerationParams Result = Override->Params.TunnelNetworkParams;
|
FStrateGenerationParams Result = Override->Params.TunnelNetworkParams;
|
||||||
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * CHUNK_SIZE;
|
||||||
// A candidate is a hard replacement of this slot. Do not blend its rolled vector with an
|
// A candidate is a hard replacement of this slot. Do not blend its rolled vector with an
|
||||||
// authored neighbour at a boundary; this is also how the offline fixture measures it.
|
// authored neighbour at a boundary; this is also how the offline fixture measures it.
|
||||||
return ResolveFinalParams(Result);
|
return ResolveFinalParams(Result);
|
||||||
@@ -6715,8 +6578,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
|||||||
if (SeasonStrates.IsValidIndex(SlotIdx))
|
if (SeasonStrates.IsValidIndex(SlotIdx))
|
||||||
{
|
{
|
||||||
FStrateGenerationParams Result = SeasonStrates[SlotIdx].Params.TunnelNetworkParams;
|
FStrateGenerationParams Result = SeasonStrates[SlotIdx].Params.TunnelNetworkParams;
|
||||||
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * CHUNK_SIZE;
|
||||||
// Season vectors are already final, measured slot records. Vertical structural posts own
|
// Season vectors are already final, measured slot records. Vertical structural posts own
|
||||||
// their boundary; blending them with another selected recipe would describe neither one.
|
// their boundary; blending them with another selected recipe would describe neither one.
|
||||||
return ResolveFinalParams(Result);
|
return ResolveFinalParams(Result);
|
||||||
@@ -6733,17 +6596,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
|||||||
//
|
//
|
||||||
// TopChunkZ=0, CHUNK_SIZE=32: top of the strate = chunk 0's top edge = voxel Z=32
|
// TopChunkZ=0, CHUNK_SIZE=32: top of the strate = chunk 0's top edge = voxel Z=32
|
||||||
// BottomChunkZ=-3: bottom of the strate = chunk -3's bottom edge = voxel Z=-3*32 = -96
|
// BottomChunkZ=-3: bottom of the strate = chunk -3's bottom edge = voxel Z=-3*32 = -96
|
||||||
BaseParams.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
BaseParams.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
BaseParams.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
BaseParams.StrateBottomWorldZ = (float)(Slot.BottomChunkZ) * CHUNK_SIZE;
|
||||||
|
|
||||||
// Core currently dispatches one generator field per resolved strate. Gradient and
|
|
||||||
// interleaved metadata therefore use the explicitly declared Hard fallback for every
|
|
||||||
// legacy parameter lookup as well; mixing parameter vectors here would disagree with the
|
|
||||||
// shared ceiling/content/bedrock composition layer.
|
|
||||||
if (bHardTransitionFallbackForAssembly)
|
|
||||||
{
|
|
||||||
return ResolveFinalParams(BaseParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// BOUNDARY BLENDING (transition-type aware)
|
// BOUNDARY BLENDING (transition-type aware)
|
||||||
@@ -6781,23 +6635,18 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
|||||||
//---------------------------------------------------------------------
|
//---------------------------------------------------------------------
|
||||||
// CHECK BOTTOM BOUNDARY (transitioning to strate below)
|
// CHECK BOTTOM BOUNDARY (transitioning to strate below)
|
||||||
//---------------------------------------------------------------------
|
//---------------------------------------------------------------------
|
||||||
// Legacy chunk callers project their sample to a layout interval; physical distance and
|
// DistFromBottom = how many chunks above the bottom edge of this strate.
|
||||||
// transition width are both resolved in micrometres before projecting the blend fraction.
|
|
||||||
// When 0, we're right at the boundary. When == TransitionBlendChunks, 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.
|
||||||
const int64 DistFromBottomMicrometres = static_cast<int64>(ChunkCoord.Z)
|
int32 DistFromBottom = ChunkCoord.Z - Slot.BottomChunkZ;
|
||||||
* StrateLayoutChunkSizeMicrometres - Slot.BottomWorldMicrometres;
|
|
||||||
const int32 DistFromBottom = static_cast<int32>(VF_FloorDivide64(
|
|
||||||
DistFromBottomMicrometres, StrateLayoutChunkSizeMicrometres));
|
|
||||||
|
|
||||||
if (SlotIdx + 1 < StrateLayout.Num())
|
if (SlotIdx + 1 < StrateLayout.Num())
|
||||||
{
|
{
|
||||||
// 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.ResolvedTransitionType;
|
const EVoxelStrateTransition TransType = Slot.Definition->TransitionType;
|
||||||
|
|
||||||
// Per-definition blend distance.
|
// Per-definition blend distance.
|
||||||
const int32 EffectiveBlend = static_cast<int32>(VF_FloorDivide64(
|
const int32 EffectiveBlend = Slot.Definition->TransitionBlendChunks;
|
||||||
Slot.TransitionWidthMicrometres, StrateLayoutChunkSizeMicrometres));
|
|
||||||
|
|
||||||
// Prepare the neighbor's params (only used for Gradient and Interleaved)
|
// Prepare the neighbor's params (only used for Gradient and Interleaved)
|
||||||
const FStrateSlot& BelowSlot = StrateLayout[SlotIdx + 1];
|
const FStrateSlot& BelowSlot = StrateLayout[SlotIdx + 1];
|
||||||
@@ -6821,8 +6670,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
|||||||
if (EffectiveBlend > 0 && DistFromBottom < EffectiveBlend)
|
if (EffectiveBlend > 0 && DistFromBottom < EffectiveBlend)
|
||||||
{
|
{
|
||||||
FStrateGenerationParams BelowParams = BuildParamsFromDefinition(BelowSlot.Definition);
|
FStrateGenerationParams BelowParams = BuildParamsFromDefinition(BelowSlot.Definition);
|
||||||
BelowParams.StrateTopWorldZ = BelowSlot.LegacyTopWorldZVoxels;
|
BelowParams.StrateTopWorldZ = (float)(BelowSlot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
BelowParams.StrateBottomWorldZ = BelowSlot.LegacyBottomWorldZVoxels;
|
BelowParams.StrateBottomWorldZ = (float)(BelowSlot.BottomChunkZ) * CHUNK_SIZE;
|
||||||
|
|
||||||
// Linear alpha: 0 at EffectiveBlend chunks away, 1 at the boundary
|
// Linear alpha: 0 at EffectiveBlend chunks away, 1 at the boundary
|
||||||
float Alpha = 1.0f - ((float)DistFromBottom / (float)EffectiveBlend);
|
float Alpha = 1.0f - ((float)DistFromBottom / (float)EffectiveBlend);
|
||||||
@@ -6859,8 +6708,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
|||||||
if (DistFromBottom < CheckRange)
|
if (DistFromBottom < CheckRange)
|
||||||
{
|
{
|
||||||
FStrateGenerationParams BelowParams = BuildParamsFromDefinition(BelowSlot.Definition);
|
FStrateGenerationParams BelowParams = BuildParamsFromDefinition(BelowSlot.Definition);
|
||||||
BelowParams.StrateTopWorldZ = BelowSlot.LegacyTopWorldZVoxels;
|
BelowParams.StrateTopWorldZ = (float)(BelowSlot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
BelowParams.StrateBottomWorldZ = BelowSlot.LegacyBottomWorldZVoxels;
|
BelowParams.StrateBottomWorldZ = (float)(BelowSlot.BottomChunkZ) * CHUNK_SIZE;
|
||||||
|
|
||||||
// Sample 3D Perlin noise to warp the boundary position.
|
// Sample 3D Perlin noise to warp the boundary position.
|
||||||
// XY frequency 0.15 gives medium-scale variation (~6-7 chunks per cycle).
|
// XY frequency 0.15 gives medium-scale variation (~6-7 chunks per cycle).
|
||||||
@@ -6898,18 +6747,14 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
|||||||
//---------------------------------------------------------------------
|
//---------------------------------------------------------------------
|
||||||
// Mirror logic: the ABOVE strate's TransitionType controls its lower edge,
|
// Mirror logic: the ABOVE strate's TransitionType controls its lower edge,
|
||||||
// which is this strate's upper edge. So we read from StrateLayout[SlotIdx-1].
|
// which is this strate's upper edge. So we read from StrateLayout[SlotIdx-1].
|
||||||
const int64 DistFromTopMicrometres = Slot.TopWorldMicrometres
|
int32 DistFromTop = Slot.TopChunkZ - ChunkCoord.Z;
|
||||||
- (static_cast<int64>(ChunkCoord.Z) + 1) * StrateLayoutChunkSizeMicrometres;
|
|
||||||
const int32 DistFromTop = static_cast<int32>(VF_FloorDivide64(
|
|
||||||
DistFromTopMicrometres, StrateLayoutChunkSizeMicrometres));
|
|
||||||
|
|
||||||
if (SlotIdx > 0)
|
if (SlotIdx > 0)
|
||||||
{
|
{
|
||||||
// The strate ABOVE controls the transition at its lower edge (= our upper edge)
|
// The strate ABOVE controls the transition at its lower edge (= our upper edge)
|
||||||
const FStrateSlot& AboveSlot = StrateLayout[SlotIdx - 1];
|
const FStrateSlot& AboveSlot = StrateLayout[SlotIdx - 1];
|
||||||
const EVoxelStrateTransition TransType = AboveSlot.ResolvedTransitionType;
|
const EVoxelStrateTransition TransType = AboveSlot.Definition->TransitionType;
|
||||||
const int32 EffectiveBlend = static_cast<int32>(VF_FloorDivide64(
|
const int32 EffectiveBlend = AboveSlot.Definition->TransitionBlendChunks;
|
||||||
AboveSlot.TransitionWidthMicrometres, StrateLayoutChunkSizeMicrometres));
|
|
||||||
|
|
||||||
switch (TransType)
|
switch (TransType)
|
||||||
{
|
{
|
||||||
@@ -6924,8 +6769,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
|||||||
if (EffectiveBlend > 0 && DistFromTop < EffectiveBlend)
|
if (EffectiveBlend > 0 && DistFromTop < EffectiveBlend)
|
||||||
{
|
{
|
||||||
FStrateGenerationParams AboveParams = BuildParamsFromDefinition(AboveSlot.Definition);
|
FStrateGenerationParams AboveParams = BuildParamsFromDefinition(AboveSlot.Definition);
|
||||||
AboveParams.StrateTopWorldZ = AboveSlot.LegacyTopWorldZVoxels;
|
AboveParams.StrateTopWorldZ = (float)(AboveSlot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
AboveParams.StrateBottomWorldZ = AboveSlot.LegacyBottomWorldZVoxels;
|
AboveParams.StrateBottomWorldZ = (float)(AboveSlot.BottomChunkZ) * CHUNK_SIZE;
|
||||||
|
|
||||||
float Alpha = 1.0f - ((float)DistFromTop / (float)EffectiveBlend);
|
float Alpha = 1.0f - ((float)DistFromTop / (float)EffectiveBlend);
|
||||||
Alpha = FMath::Clamp(Alpha, 0.0f, 1.0f);
|
Alpha = FMath::Clamp(Alpha, 0.0f, 1.0f);
|
||||||
@@ -6946,8 +6791,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
|||||||
if (DistFromTop < CheckRange)
|
if (DistFromTop < CheckRange)
|
||||||
{
|
{
|
||||||
FStrateGenerationParams AboveParams = BuildParamsFromDefinition(AboveSlot.Definition);
|
FStrateGenerationParams AboveParams = BuildParamsFromDefinition(AboveSlot.Definition);
|
||||||
AboveParams.StrateTopWorldZ = AboveSlot.LegacyTopWorldZVoxels;
|
AboveParams.StrateTopWorldZ = (float)(AboveSlot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
AboveParams.StrateBottomWorldZ = AboveSlot.LegacyBottomWorldZVoxels;
|
AboveParams.StrateBottomWorldZ = (float)(AboveSlot.BottomChunkZ) * CHUNK_SIZE;
|
||||||
|
|
||||||
// Same noise function but with a different seed offset to avoid
|
// Same noise function but with a different seed offset to avoid
|
||||||
// symmetry between top and bottom boundaries of adjacent strates
|
// symmetry between top and bottom boundaries of adjacent strates
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
namespace VoxelStreamingAdmissionOrder
|
|
||||||
{
|
|
||||||
template <typename Iterator, typename Less>
|
|
||||||
void StableSortCandidates(Iterator First, Iterator Last, Less Compare)
|
|
||||||
{
|
|
||||||
std::stable_sort(First, Last, Compare);
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename Range, typename IsAtCapacity, typename SubmitCandidate,
|
|
||||||
typename OnCapacityReached>
|
|
||||||
void AdmitInOrder(const Range& Candidates, IsAtCapacity AtCapacity,
|
|
||||||
SubmitCandidate Submit, OnCapacityReached CapacityReached)
|
|
||||||
{
|
|
||||||
for (const auto& Candidate : Candidates)
|
|
||||||
{
|
|
||||||
if (AtCapacity())
|
|
||||||
{
|
|
||||||
CapacityReached();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Submit(Candidate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -261,12 +261,8 @@ namespace VoxelHash
|
|||||||
FORCEINLINE uint32 Cell(int32 CellX, int32 CellY, uint32 Seed)
|
FORCEINLINE uint32 Cell(int32 CellX, int32 CellY, uint32 Seed)
|
||||||
{
|
{
|
||||||
uint32 H = Seed;
|
uint32 H = Seed;
|
||||||
// Bias in unsigned arithmetic. Adding the signed cell coordinate first overflows for
|
H ^= Mix((uint32)(CellX + 0x7FFFFFFF));
|
||||||
// positive cells and makes the world hash compiler-dependent.
|
H ^= Mix((uint32)(CellY + 0x7FFFFFFF) * 2654435761u);
|
||||||
const uint32 BiasedX = static_cast<uint32>(CellX) + 0x7FFFFFFFu;
|
|
||||||
const uint32 BiasedY = static_cast<uint32>(CellY) + 0x7FFFFFFFu;
|
|
||||||
H ^= Mix(BiasedX);
|
|
||||||
H ^= Mix(BiasedY * 2654435761u);
|
|
||||||
return Mix(H);
|
return Mix(H);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,14 +274,10 @@ namespace VoxelHash
|
|||||||
int32 MinX = FMath::Min(AX, BX), MinY = FMath::Min(AY, BY);
|
int32 MinX = FMath::Min(AX, BX), MinY = FMath::Min(AY, BY);
|
||||||
int32 MaxX = FMath::Max(AX, BX), MaxY = FMath::Max(AY, BY);
|
int32 MaxX = FMath::Max(AX, BX), MaxY = FMath::Max(AY, BY);
|
||||||
uint32 H = Seed ^ 0xDEADBEEF;
|
uint32 H = Seed ^ 0xDEADBEEF;
|
||||||
const uint32 BiasedMinX = static_cast<uint32>(MinX) + 0x7FFFFFFFu;
|
H ^= Mix((uint32)(MinX + 0x7FFFFFFF));
|
||||||
const uint32 BiasedMinY = static_cast<uint32>(MinY) + 0x7FFFFFFFu;
|
H ^= Mix((uint32)(MinY + 0x7FFFFFFF) * 2654435761u);
|
||||||
const uint32 BiasedMaxX = static_cast<uint32>(MaxX) + 0x7FFFFFFFu;
|
H ^= Mix((uint32)(MaxX + 0x7FFFFFFF) * 374761393u);
|
||||||
const uint32 BiasedMaxY = static_cast<uint32>(MaxY) + 0x7FFFFFFFu;
|
H ^= Mix((uint32)(MaxY + 0x7FFFFFFF) * 668265263u);
|
||||||
H ^= Mix(BiasedMinX);
|
|
||||||
H ^= Mix(BiasedMinY * 2654435761u);
|
|
||||||
H ^= Mix(BiasedMaxX * 374761393u);
|
|
||||||
H ^= Mix(BiasedMaxY * 668265263u);
|
|
||||||
return Mix(H);
|
return Mix(H);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,13 +477,10 @@ namespace VoxelNoise
|
|||||||
|
|
||||||
// Hash the neighbor cell to get a feature point position [0,1)
|
// Hash the neighbor cell to get a feature point position [0,1)
|
||||||
// Using three different hash mixes for X, Y, Z offsets
|
// Using three different hash mixes for X, Y, Z offsets
|
||||||
const uint32 BiasedX = static_cast<uint32>(NX) + 0x7FFFFFFFu;
|
|
||||||
const uint32 BiasedY = static_cast<uint32>(NY) + 0x7FFFFFFFu;
|
|
||||||
const uint32 BiasedZ = static_cast<uint32>(NZ) + 0x7FFFFFFFu;
|
|
||||||
uint32 H = VoxelHash::Mix(
|
uint32 H = VoxelHash::Mix(
|
||||||
BiasedX
|
(uint32)(NX + 0x7FFFFFFF)
|
||||||
^ VoxelHash::Mix(BiasedY * 2654435761u)
|
^ VoxelHash::Mix((uint32)(NY + 0x7FFFFFFF) * 2654435761u)
|
||||||
^ VoxelHash::Mix(BiasedZ * 374761393u)
|
^ VoxelHash::Mix((uint32)(NZ + 0x7FFFFFFF) * 374761393u)
|
||||||
);
|
);
|
||||||
|
|
||||||
float FPX = (float)DX + VoxelHash::ToFloat01(H) - FracX;
|
float FPX = (float)DX + VoxelHash::ToFloat01(H) - FracX;
|
||||||
|
|||||||
@@ -340,14 +340,6 @@ private:
|
|||||||
float SuppressRadiusWorld = 0.0f;
|
float SuppressRadiusWorld = 0.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Game-thread lookup copied from the immutable resolved Core layout.
|
|
||||||
// Recherche game-thread copiee depuis la disposition Core resolue et immuable.
|
|
||||||
struct FResolvedCoreGeometryAnchor
|
|
||||||
{
|
|
||||||
int32 StrateIndex = INDEX_NONE;
|
|
||||||
FVector LocalPositionCm = FVector::ZeroVector;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** WORKER-THREAD surface find → fills OutSpawns for one cell. SurfaceWorld uses the height oracle
|
/** WORKER-THREAD surface find → fills OutSpawns for one cell. SurfaceWorld uses the height oracle
|
||||||
* (cheap, O(1)/column); other archetypes ray-march the density column. No UObject access except
|
* (cheap, O(1)/column); other archetypes ray-march the density column. No UObject access except
|
||||||
* Generator (thread-safe). Determinism-critical. Resolves the dominant biome PER COLUMN
|
* Generator (thread-safe). Determinism-critical. Resolves the dominant biome PER COLUMN
|
||||||
@@ -444,7 +436,6 @@ private:
|
|||||||
// (kept until the cell leaves range so the surface-find isn't repeated). Strate-bounded.
|
// (kept until the cell leaves range so the surface-find isn't repeated). Strate-bounded.
|
||||||
// Keyed by FIntVector(cellX|passageIdx, cellY|side, entryIndex) — lattice cell OR passage endpoint.
|
// Keyed by FIntVector(cellX|passageIdx, cellY|side, entryIndex) — lattice cell OR passage endpoint.
|
||||||
TMap<FIntVector, FLandmarkInstance> LandmarkInstances;
|
TMap<FIntVector, FLandmarkInstance> LandmarkInstances;
|
||||||
TMap<FString, FResolvedCoreGeometryAnchor> CoreGeometryAnchors;
|
|
||||||
int32 LastLandmarkStrate = INT32_MIN; // strate change → wipe + rebuild landmarks
|
int32 LastLandmarkStrate = INT32_MIN; // strate change → wipe + rebuild landmarks
|
||||||
|
|
||||||
// Set in BeginDestroy; worker tasks check it before touching us.
|
// Set in BeginDestroy; worker tasks check it before touching us.
|
||||||
|
|||||||
@@ -41,27 +41,21 @@
|
|||||||
// SealFactor > 0 et BaseDensity > 0, le résultat est solide GARANTI quelle qu'ait été l'entrée.
|
// SealFactor > 0 et BaseDensity > 0, le résultat est solide GARANTI quelle qu'ait été l'entrée.
|
||||||
// C'est exactement ce qu'encode `IVoxelDensityOp::ClassifyBox` (voir VoxelDensityOp.h), et la
|
// C'est exactement ce qu'encode `IVoxelDensityOp::ClassifyBox` (voir VoxelDensityOp.h), et la
|
||||||
// raison pour laquelle cette méthode existe.
|
// raison pour laquelle cette méthode existe.
|
||||||
FORCEINLINE void VF_ApplyUpperBoundarySealValue(float& Density, float WorldZ,
|
|
||||||
float StrateTopZ, float Thickness, float BaseDensity)
|
|
||||||
{
|
|
||||||
const float DistTop = StrateTopZ - WorldZ;
|
|
||||||
if (DistTop >= 0.0f && DistTop < Thickness)
|
|
||||||
{
|
|
||||||
float SealFactor = 1.0f - (DistTop / Thickness);
|
|
||||||
SealFactor = SmoothStep01(SealFactor);
|
|
||||||
Density = FMath::Max(Density, SealFactor * BaseDensity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FORCEINLINE void VF_ApplyBoundarySeal(float& Density, float WorldZ,
|
FORCEINLINE void VF_ApplyBoundarySeal(float& Density, float WorldZ,
|
||||||
float StrateTopZ, float StrateBottomZ,
|
float StrateTopZ, float StrateBottomZ,
|
||||||
float Thickness, float BaseDensity)
|
float Thickness, float BaseDensity)
|
||||||
{
|
{
|
||||||
if (VoxelDensityAblation::IsBoundarySealOff() || Thickness <= 0.0f) return;
|
if (VoxelDensityAblation::IsBoundarySealOff() || Thickness <= 0.0f) return;
|
||||||
|
|
||||||
|
const float DistTop = StrateTopZ - WorldZ; // + si on est sous le plafond
|
||||||
const float DistBot = WorldZ - StrateBottomZ; // + si on est au-dessus du sol
|
const float DistBot = WorldZ - StrateBottomZ; // + si on est au-dessus du sol
|
||||||
|
|
||||||
VF_ApplyUpperBoundarySealValue(Density, WorldZ, StrateTopZ, Thickness, BaseDensity);
|
if (DistTop >= 0.0f && DistTop < Thickness)
|
||||||
|
{
|
||||||
|
float SealFactor = 1.0f - (DistTop / Thickness);
|
||||||
|
SealFactor = SmoothStep01(SealFactor);
|
||||||
|
Density = FMath::Max(Density, SealFactor * BaseDensity);
|
||||||
|
}
|
||||||
if (DistBot >= 0.0f && DistBot < Thickness)
|
if (DistBot >= 0.0f && DistBot < Thickness)
|
||||||
{
|
{
|
||||||
float SealFactor = 1.0f - (DistBot / Thickness);
|
float SealFactor = 1.0f - (DistBot / Thickness);
|
||||||
@@ -70,16 +64,6 @@ FORCEINLINE void VF_ApplyBoundarySeal(float& Density, float WorldZ,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The same structural seal, limited to a strate's upper boundary. Native finite fields use this
|
|
||||||
// when their lower boundary intentionally has no seal; keeping the math here makes that ceiling
|
|
||||||
// use the same shared density primitive as every two-sided strate seal.
|
|
||||||
FORCEINLINE void VF_ApplyUpperBoundarySeal(float& Density, float WorldZ,
|
|
||||||
float StrateTopZ, float Thickness, float BaseDensity)
|
|
||||||
{
|
|
||||||
if (VoxelDensityAblation::IsBoundarySealOff() || Thickness <= 0.0f) return;
|
|
||||||
VF_ApplyUpperBoundarySealValue(Density, WorldZ, StrateTopZ, Thickness, BaseDensity);
|
|
||||||
}
|
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// CARVE DE PASSAGE / PASSAGE CARVING
|
// CARVE DE PASSAGE / PASSAGE CARVING
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
|
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <cstddef>
|
|
||||||
#include "VoxelTypes.h" // Pour FVoxelMeshData, CHUNK_SIZE, VOXEL_SIZE, etc.
|
#include "VoxelTypes.h" // Pour FVoxelMeshData, CHUNK_SIZE, VOXEL_SIZE, etc.
|
||||||
#include "VoxelGenerator.h"
|
#include "VoxelGenerator.h"
|
||||||
#include "VoxelMarchingCubesMesher.generated.h"
|
#include "VoxelMarchingCubesMesher.generated.h"
|
||||||
@@ -39,24 +38,6 @@ struct FVoxelSharedDensityGrid
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Host-owned scheduling hooks for a predicted-heavy density-grid fill. Each task owns one
|
|
||||||
// deterministic Z slab and gets the source tile's worker-local sampling context.
|
|
||||||
struct FVoxelGridFillExecution
|
|
||||||
{
|
|
||||||
using FTask = void(*)(void* taskContext, std::size_t index, std::size_t workerIndex);
|
|
||||||
using FParallelFor = double(*)(void* schedulerContext, std::size_t count,
|
|
||||||
std::size_t workerLimit, void* taskContext, FTask task);
|
|
||||||
using FBeginWorkerContext = void*(*)(void* context, std::size_t workerIndex);
|
|
||||||
using FEndWorkerContext = void(*)(void* context, void* workerState,
|
|
||||||
std::size_t workerIndex);
|
|
||||||
void* SchedulerContext = nullptr;
|
|
||||||
FParallelFor ParallelFor = nullptr;
|
|
||||||
std::size_t WorkerLimit = 1;
|
|
||||||
void* WorkerContext = nullptr;
|
|
||||||
FBeginWorkerContext BeginWorkerContext = nullptr;
|
|
||||||
FEndWorkerContext EndWorkerContext = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Stable plan data copied onto one worker-owned mesher for hybrid LOD0 generation.
|
// Stable plan data copied onto one worker-owned mesher for hybrid LOD0 generation.
|
||||||
// Donnees stables du plan copiees dans un mailleur de worker pour le mode hybride LOD0.
|
// Donnees stables du plan copiees dans un mailleur de worker pour le mode hybride LOD0.
|
||||||
struct FVoxelHybridSharpFeatureBox
|
struct FVoxelHybridSharpFeatureBox
|
||||||
@@ -130,8 +111,6 @@ public:
|
|||||||
// GenerateMesh calls finish. A malformed/incomplete lattice is ignored and falls back to
|
// GenerateMesh calls finish. A malformed/incomplete lattice is ignored and falls back to
|
||||||
// the canonical generator path rather than changing geometry.
|
// the canonical generator path rather than changing geometry.
|
||||||
void SetSharedDensityGrid(const FVoxelSharedDensityGrid* InGrid) { SharedDensityGrid = InGrid; }
|
void SetSharedDensityGrid(const FVoxelSharedDensityGrid* InGrid) { SharedDensityGrid = InGrid; }
|
||||||
void SetGridFillExecution(const FVoxelGridFillExecution& InExecution)
|
|
||||||
{ GridFillExecution = InExecution; }
|
|
||||||
|
|
||||||
// The caller owns this mesher instance for one tile task; no shared mutable feature state.
|
// The caller owns this mesher instance for one tile task; no shared mutable feature state.
|
||||||
// L'appelant possede cette instance pour une tuile; aucune donnee partagee modifiable.
|
// L'appelant possede cette instance pour une tuile; aucune donnee partagee modifiable.
|
||||||
@@ -158,7 +137,6 @@ private:
|
|||||||
|
|
||||||
const std::atomic<bool>* ShutdownFlag = nullptr;
|
const std::atomic<bool>* ShutdownFlag = nullptr;
|
||||||
const FVoxelSharedDensityGrid* SharedDensityGrid = nullptr;
|
const FVoxelSharedDensityGrid* SharedDensityGrid = nullptr;
|
||||||
FVoxelGridFillExecution GridFillExecution;
|
|
||||||
TArray<FVoxelHybridSharpFeatureBox> HybridSharpFeatures;
|
TArray<FVoxelHybridSharpFeatureBox> HybridSharpFeatures;
|
||||||
bool bUseHybridSharpMesher = false;
|
bool bUseHybridSharpMesher = false;
|
||||||
bool bUseHybridSharpFeatureSnap = false;
|
bool bUseHybridSharpFeatureSnap = false;
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ class UVoxelSettings : public UPrimaryDataAsset
|
|||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
public:
|
public:
|
||||||
virtual void Serialize(FArchive& Ar) override;
|
|
||||||
virtual void PostLoad() override;
|
|
||||||
|
|
||||||
/** Season seed/radius replace the mutable authored values only when Season is assigned. */
|
/** Season seed/radius replace the mutable authored values only when Season is assigned. */
|
||||||
int32 GetEffectiveWorldSeed() const;
|
int32 GetEffectiveWorldSeed() const;
|
||||||
@@ -413,23 +411,14 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (ClampMin = "1"))
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (ClampMin = "1"))
|
||||||
int32 TotalStrates = 10;
|
int32 TotalStrates = 10;
|
||||||
|
|
||||||
// Legacy count retained so pre-metre assets still deserialize during the upgrade.
|
// Vertical SEPARATION between consecutive strates, in chunks of solid bedrock.
|
||||||
// Ancien compte conserve pour lire les assets precedant les metres.
|
// 0 = strates touch (just the seals between them). Higher = a thick rock layer
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (DeprecatedProperty, DeprecationMessage = "Use InterStrateGapMeters."))
|
// between each biome that the player must dig through at (0,0), and that the
|
||||||
|
// auto-carved passages tunnel across.
|
||||||
|
// 0 → adjacent layers (default) · 1-2 → a real bedrock band · 4+ → deep separation
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (ClampMin = "0", ClampMax = "16"))
|
||||||
int32 InterStrateGapChunks = 0;
|
int32 InterStrateGapChunks = 0;
|
||||||
|
|
||||||
// Physical separation between strata; the layout remains aligned to whole layout chunks.
|
|
||||||
// Separation physique; la disposition reste alignee sur des chunks entiers.
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (Units = "m", ClampMin = "0", ClampMax = "1024"))
|
|
||||||
double InterStrateGapMeters = 0.0;
|
|
||||||
|
|
||||||
// Physical width of one vertical strate-layout chunk. This is independent of the
|
|
||||||
// Core tile width (32^3 voxels) and of the source generator's voxel resolution.
|
|
||||||
// 8 m preserves the owner's current SurfaceWorld extent at 25 cm Core voxels.
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates|World Assembly",
|
|
||||||
meta = (ClampMin = "0.25", ClampMax = "64.0", Units = "m"))
|
|
||||||
float StrateLayoutChunkSizeMeters = 8.0f;
|
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// SPINE & INTER-STRATE CONNECTIONS (the (0,0) descent axis)
|
// SPINE & INTER-STRATE CONNECTIONS (the (0,0) descent axis)
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
|
|||||||
@@ -35,9 +35,6 @@ namespace VoxelForgeStartupTrace
|
|||||||
double ResultQueueSeconds = 0.0;
|
double ResultQueueSeconds = 0.0;
|
||||||
double WorkerTaskSeconds = 0.0;
|
double WorkerTaskSeconds = 0.0;
|
||||||
double GenerationSeconds = 0.0;
|
double GenerationSeconds = 0.0;
|
||||||
double CoreGenerateSeconds = 0.0;
|
|
||||||
double MeshCopySeconds = 0.0;
|
|
||||||
double StreamBuildSeconds = 0.0;
|
|
||||||
double ClassifySeconds = 0.0;
|
double ClassifySeconds = 0.0;
|
||||||
double MeshSeconds = 0.0;
|
double MeshSeconds = 0.0;
|
||||||
double StreamSeconds = 0.0;
|
double StreamSeconds = 0.0;
|
||||||
@@ -59,7 +56,6 @@ namespace VoxelForgeStartupTrace
|
|||||||
int32 ResultsDrained = 0;
|
int32 ResultsDrained = 0;
|
||||||
int32 ApplyResultsByLevelTotal = 0;
|
int32 ApplyResultsByLevelTotal = 0;
|
||||||
int32 MeshesApplied = 0;
|
int32 MeshesApplied = 0;
|
||||||
int32 SupplementalFarApplies = 0;
|
|
||||||
int32 MaxApplies = 0;
|
int32 MaxApplies = 0;
|
||||||
double ApplyBudgetMilliseconds = 0.0;
|
double ApplyBudgetMilliseconds = 0.0;
|
||||||
bool bCountLimitReached = false;
|
bool bCountLimitReached = false;
|
||||||
@@ -70,25 +66,15 @@ namespace VoxelForgeStartupTrace
|
|||||||
TStaticArray<double, 9> ApplySecondsByLevel{};
|
TStaticArray<double, 9> ApplySecondsByLevel{};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct VOXELFORGE_API FWriteMetrics
|
|
||||||
{
|
|
||||||
double JsonWriteMilliseconds = 0.0;
|
|
||||||
double ArchiveSerializeMilliseconds = 0.0;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Starts when -voxel.StartupTraceFile=<absolute path> is present. */
|
/** Starts when -voxel.StartupTraceFile=<absolute path> is present. */
|
||||||
VOXELFORGE_API void BeginFromCommandLine();
|
VOXELFORGE_API void BeginFromCommandLine();
|
||||||
|
|
||||||
VOXELFORGE_API bool IsActive();
|
VOXELFORGE_API bool IsActive();
|
||||||
|
|
||||||
/** Returns and resets startup-trace writes performed by the current game-thread frame. */
|
|
||||||
VOXELFORGE_API FWriteMetrics ConsumeCurrentThreadWriteMetrics();
|
|
||||||
|
|
||||||
/** FieldsJson is the contents after the type/timestamp object prefix, without braces. */
|
/** FieldsJson is the contents after the type/timestamp object prefix, without braces. */
|
||||||
VOXELFORGE_API void RecordEvent(const TCHAR* Type, const FString& FieldsJson = FString());
|
VOXELFORGE_API void RecordEvent(const TCHAR* Type, const FString& FieldsJson = FString());
|
||||||
|
|
||||||
VOXELFORGE_API void RecordRequest(const FTileSample& Sample, uint32 Epoch,
|
VOXELFORGE_API void RecordRequest(const FTileSample& Sample, uint32 Epoch);
|
||||||
uint32 DesiredEpoch = 0);
|
|
||||||
|
|
||||||
VOXELFORGE_API void StageBegin(const TCHAR* Name);
|
VOXELFORGE_API void StageBegin(const TCHAR* Name);
|
||||||
VOXELFORGE_API void StageEnd(const TCHAR* Name);
|
VOXELFORGE_API void StageEnd(const TCHAR* Name);
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ class VOXELFORGE_API UVoxelStrateDefinition : public UPrimaryDataAsset
|
|||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
public:
|
public:
|
||||||
virtual void Serialize(FArchive& Ar) override;
|
|
||||||
virtual void PostLoad() override;
|
virtual void PostLoad() override;
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
@@ -67,14 +66,14 @@ public:
|
|||||||
// DIMENSIONS
|
// DIMENSIONS
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
|
|
||||||
// Legacy serialized chunk count kept for Blueprint/package compatibility during the unit migration.
|
// How many chunks tall this strate is.
|
||||||
// Ancien nombre de chunks conserve pour la compatibilite Blueprint/package pendant la migration.
|
// Each strate can have a different height!
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Dimensions", meta = (DeprecatedProperty, DeprecationMessage = "Use StrateHeightMeters."))
|
// Human-scale defaults use 16 chunks = 64 m. This gives a 20 m+ vertical budget for
|
||||||
|
// cathedral rooms, tall shafts, and the surface sky cap without changing the lateral gate.
|
||||||
|
// 1 chunk = 32 voxels x 12.5cm = 4 m; 16 chunks preserves the 64 m authored default.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Dimensions", meta = (ClampMin = "1", ClampMax = "256"))
|
||||||
int32 StrateHeightInChunks = 16;
|
int32 StrateHeightInChunks = 16;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Dimensions", meta = (Units = "m", ClampMin = "1", ClampMax = "2048"))
|
|
||||||
double StrateHeightMeters = 128.0;
|
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// BOUNDARY TRANSITION
|
// BOUNDARY TRANSITION
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
@@ -100,13 +99,9 @@ public:
|
|||||||
// 2 → moderate (good default)
|
// 2 → moderate (good default)
|
||||||
// 4 → wide, gentle blend across many chunks
|
// 4 → wide, gentle blend across many chunks
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Boundary",
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Boundary",
|
||||||
meta = (DeprecatedProperty, DeprecationMessage = "Use TransitionWidthMeters."))
|
meta = (ClampMin = "1", ClampMax = "8", EditCondition = "TransitionType != EVoxelStrateTransition::Hard"))
|
||||||
int32 TransitionBlendChunks = 2;
|
int32 TransitionBlendChunks = 2;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Boundary",
|
|
||||||
meta = (Units = "m", ClampMin = "0", ClampMax = "512", EditCondition = "TransitionType != EVoxelStrateTransition::Hard"))
|
|
||||||
double TransitionWidthMeters = 16.0;
|
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// GENERATOR TYPE
|
// GENERATOR TYPE
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
@@ -120,11 +115,6 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation")
|
||||||
ECaveGeneratorType GeneratorType = ECaveGeneratorType::TunnelNetwork;
|
ECaveGeneratorType GeneratorType = ECaveGeneratorType::TunnelNetwork;
|
||||||
|
|
||||||
// Optional native Core authoring; leaving it empty keeps the established generator path.
|
|
||||||
// Edition Core native facultative; laissez vide pour garder le generateur existant.
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Core Geometry")
|
|
||||||
FVoxelForgeCoreGeometry CoreGeometry;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
|
|||||||
@@ -121,8 +121,8 @@ struct FVoxelPassage
|
|||||||
* FStrateSlot — Runtime info for one strate in the layout.
|
* FStrateSlot — Runtime info for one strate in the layout.
|
||||||
*
|
*
|
||||||
* Created during Initialize() and stored in the StrateLayout array.
|
* Created during Initialize() and stored in the StrateLayout array.
|
||||||
* Each slot owns its resolved half-open physical Z interval. Chunk coordinates and legacy
|
* Each slot knows its definition, its Z-range in chunk coordinates,
|
||||||
* authoring values below are projections kept for existing callers and assets.
|
* and its index in the sequence.
|
||||||
*/
|
*/
|
||||||
USTRUCT()
|
USTRUCT()
|
||||||
struct FStrateSlot
|
struct FStrateSlot
|
||||||
@@ -136,33 +136,6 @@ struct FStrateSlot
|
|||||||
// Strate index (0 = topmost, increases downward)
|
// Strate index (0 = topmost, increases downward)
|
||||||
int32 StrateIndex = 0;
|
int32 StrateIndex = 0;
|
||||||
|
|
||||||
// Resolved physical bounds, in micrometres, with interval [bottom, top). These are computed
|
|
||||||
// once during Initialize from the configured metre chunk width and legacy asset dimensions.
|
|
||||||
int64 TopWorldMicrometres = 0;
|
|
||||||
int64 BottomWorldMicrometres = 0;
|
|
||||||
int64 HeightMicrometres = 0;
|
|
||||||
float LegacyTopWorldZVoxels = 0.0f;
|
|
||||||
float LegacyBottomWorldZVoxels = 0.0f;
|
|
||||||
float LegacyHeightVoxels = 0.0f;
|
|
||||||
|
|
||||||
// Immutable transition and passage inputs copied when this slot is resolved. The width is
|
|
||||||
// physical; chunk counts and raw owner-voxel values remain compatibility projections for
|
|
||||||
// legacy parameter and route adapters.
|
|
||||||
EVoxelStrateTransition ResolvedTransitionType = EVoxelStrateTransition::Gradient;
|
|
||||||
int32 ResolvedTransitionBlendChunks = 2;
|
|
||||||
int64 TransitionWidthMicrometres = 0;
|
|
||||||
FStratePassageConfig ResolvedPassageConfig;
|
|
||||||
int64 PassageMouthRadiusMicrometres = 0;
|
|
||||||
int64 PassageMidRadiusMicrometres = 0;
|
|
||||||
int64 PassageReachMinimumMicrometres = 0;
|
|
||||||
int64 PassageReachMaximumMicrometres = 0;
|
|
||||||
int64 PassageDistanceMinimumMicrometres = 0;
|
|
||||||
int64 PassageDistanceMaximumMicrometres = 0;
|
|
||||||
int64 PassageWanderMicrometres = 0;
|
|
||||||
int64 PassageVerticalWobbleMicrometres = 0;
|
|
||||||
int64 PassageSpiralRadiusMicrometres = 0;
|
|
||||||
int64 PassageCascadeLedgeMicrometres = 0;
|
|
||||||
|
|
||||||
// Z chunk range (inclusive). TopZ > BottomZ since Z decreases downward.
|
// Z chunk range (inclusive). TopZ > BottomZ since Z decreases downward.
|
||||||
// Example: TopZ = 0, BottomZ = -3 means this strate spans chunks Z=0 to Z=-3
|
// Example: TopZ = 0, BottomZ = -3 means this strate spans chunks Z=0 to Z=-3
|
||||||
int32 TopChunkZ = 0;
|
int32 TopChunkZ = 0;
|
||||||
@@ -271,17 +244,6 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category = "Strate")
|
UFUNCTION(BlueprintCallable, Category = "Strate")
|
||||||
int32 GetStrateIndex(float WorldZ) const;
|
int32 GetStrateIndex(float WorldZ) const;
|
||||||
|
|
||||||
// Director-resolved physical strate chunk width expressed in owner voxel units.
|
|
||||||
// This is independent of Core's 32^3 tile width.
|
|
||||||
float GetStrateLayoutChunkWidthVoxels() const { return StrateLayoutChunkWidthVoxels; }
|
|
||||||
uint32 GetStrateLayoutChunkSizeMicrometres() const { return StrateLayoutChunkSizeMicrometres; }
|
|
||||||
int64 GetResolvedInterStrateGapMicrometres() const
|
|
||||||
{ return ResolvedInterStrateGapMicrometres; }
|
|
||||||
// Called by the Core adapter through a const manager pointer; the flag is runtime routing
|
|
||||||
// state, not layout data, hence const + mutable.
|
|
||||||
void SetHardTransitionFallbackForAssembly(bool bEnabled) const
|
|
||||||
{ bHardTransitionFallbackForAssembly = bEnabled; }
|
|
||||||
|
|
||||||
/** Layout/passage generation counter (bumped by every Initialize → GeneratePassages).
|
/** Layout/passage generation counter (bumped by every Initialize → GeneratePassages).
|
||||||
* Hot-path callers key thread_local memos on this so an editor rebuild (RebuildStrates /
|
* Hot-path callers key thread_local memos on this so an editor rebuild (RebuildStrates /
|
||||||
* live edit) can never serve a stale strate index or passage shortlist. */
|
* live edit) can never serve a stale strate index or passage shortlist. */
|
||||||
@@ -559,15 +521,9 @@ protected:
|
|||||||
// Copied from VoxelSettings::OriginSpineRadius.
|
// Copied from VoxelSettings::OriginSpineRadius.
|
||||||
float OriginSpineRadius = 28.0f;
|
float OriginSpineRadius = 28.0f;
|
||||||
|
|
||||||
// Compatibility gap projection, in layout chunks. Physical authority is the micrometre value
|
// Solid-bedrock gap between consecutive strates, in chunks. Copied from
|
||||||
// resolved during Initialize below.
|
// VoxelSettings::InterStrateGapChunks during Initialize().
|
||||||
int32 InterStrateGapChunks = 0;
|
int32 InterStrateGapChunks = 0;
|
||||||
int64 ResolvedInterStrateGapMicrometres = 0;
|
|
||||||
|
|
||||||
// Resolved once from VoxelSettings::StrateLayoutChunkSizeMeters during Initialize().
|
|
||||||
float StrateLayoutChunkWidthVoxels = 32.0f;
|
|
||||||
uint32 StrateLayoutChunkSizeMicrometres = 8000000;
|
|
||||||
mutable bool bHardTransitionFallbackForAssembly = false;
|
|
||||||
|
|
||||||
// Production keeps the legacy writer available only as a fail-closed fallback for a
|
// Production keeps the legacy writer available only as a fail-closed fallback for a
|
||||||
// non-native/malformed passage; native D-floors bypass it. This switch never changes the
|
// non-native/malformed passage; native D-floors bypass it. This switch never changes the
|
||||||
@@ -584,7 +540,6 @@ protected:
|
|||||||
// Find which slot a chunk Z coordinate falls into.
|
// Find which slot a chunk Z coordinate falls into.
|
||||||
// Returns INDEX into StrateLayout, or -1 if not found.
|
// Returns INDEX into StrateLayout, or -1 if not found.
|
||||||
int32 FindSlotIndexForChunkZ(int32 ChunkZ) const;
|
int32 FindSlotIndexForChunkZ(int32 ChunkZ) const;
|
||||||
int32 FindSlotIndexForWorldZCentimetres(double WorldZCentimetres) const;
|
|
||||||
|
|
||||||
#if WITH_EDITOR
|
#if WITH_EDITOR
|
||||||
const FVoxelStrateComposerSlotOverride* FindComposerOverride(int32 StrateIndex) const;
|
const FVoxelStrateComposerSlotOverride* FindComposerOverride(int32 StrateIndex) const;
|
||||||
|
|||||||
@@ -200,222 +200,16 @@ enum class ECaveGeneratorType : uint8
|
|||||||
Underwater UMETA(DisplayName = "Underwater (flooded cavern system)"),
|
Underwater UMETA(DisplayName = "Underwater (flooded cavern system)"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Optional native-Core recipe selected independently for one strate.
|
|
||||||
// Recette Core native facultative, choisie independamment pour chaque strate.
|
|
||||||
UENUM(BlueprintType)
|
|
||||||
enum class EVoxelForgeCoreGeneratorRecipe : uint8
|
|
||||||
{
|
|
||||||
None UMETA(DisplayName = "Off (legacy recipe)"),
|
|
||||||
NativeCaves UMETA(DisplayName = "Native caves"),
|
|
||||||
NativeLattice UMETA(DisplayName = "Native lattice"),
|
|
||||||
ProfiledHeightfield UMETA(DisplayName = "Profiled heightfield")
|
|
||||||
};
|
|
||||||
|
|
||||||
UENUM(BlueprintType)
|
|
||||||
enum class EVoxelForgeCoreHeightfieldProfile : uint8
|
|
||||||
{
|
|
||||||
PeriodicHills,
|
|
||||||
MirroredRelief,
|
|
||||||
FlatField
|
|
||||||
};
|
|
||||||
|
|
||||||
UENUM(BlueprintType)
|
|
||||||
enum class EVoxelForgeCoreStampKind : uint8
|
|
||||||
{
|
|
||||||
CarveEllipsoid,
|
|
||||||
CarveArch,
|
|
||||||
FillPillar,
|
|
||||||
FillDais,
|
|
||||||
FillMonolith,
|
|
||||||
FillPlatform
|
|
||||||
};
|
|
||||||
|
|
||||||
// Stable authored volume stamp. All distances and positions are metres at the asset boundary.
|
|
||||||
// Volume stable, defini par son auteur. Toutes les positions et distances sont en metres.
|
|
||||||
USTRUCT(BlueprintType)
|
|
||||||
struct VOXELFORGE_API FVoxelForgeCoreStampDefinition
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
|
||||||
FString StableName;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
|
||||||
EVoxelForgeCoreStampKind Kind = EVoxelForgeCoreStampKind::CarveEllipsoid;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
|
||||||
FVector AnchorPositionMeters = FVector::ZeroVector;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
|
||||||
double PlacementRadiusMeters = 0.0;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0.5"))
|
|
||||||
FVector HalfExtentsMeters = FVector(2.0, 2.0, 2.0);
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
|
||||||
double SmoothWidthMeters = 0.0;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
|
||||||
double IrregularityAmplitudeMeters = 0.0;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
|
||||||
double IrregularityWavelengthMeters = 0.0;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
|
||||||
double RoundingRadiusMeters = 0.5;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
|
||||||
double MinimumSpacingMeters = 0.0;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (ClampMin = "0", ClampMax = "3"))
|
|
||||||
uint8 YawQuarterTurns = 0;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
|
||||||
bool bAttachToFloor = false;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
|
||||||
double FloorMeters = 0.0;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Authored cavern chamber, in local world metres.
|
|
||||||
// Salle cavernicole authored, en metres locaux du monde.
|
|
||||||
USTRUCT(BlueprintType)
|
|
||||||
struct VOXELFORGE_API FVoxelForgeCoreRoomDefinition
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
|
||||||
FString StableName;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
|
||||||
FVector CenterMeters = FVector::ZeroVector;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0.5"))
|
|
||||||
FVector HalfExtentsMeters = FVector(8.0, 8.0, 4.0);
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
|
||||||
double FloorMeters = 0.0;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
|
||||||
double SmoothWidthMeters = 0.0;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
|
||||||
double UnionWidthMeters = 0.0;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
|
||||||
double IrregularityAmplitudeMeters = 0.0;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
|
||||||
double IrregularityWavelengthMeters = 0.0;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Authored cubic tunnel between a stable start anchor and a stable room name.
|
|
||||||
// Tunnel cubique authored entre une ancre stable et une salle nommee.
|
|
||||||
USTRUCT(BlueprintType)
|
|
||||||
struct VOXELFORGE_API FVoxelForgeCoreTunnelDefinition
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
|
||||||
FString StableName;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
|
||||||
FString StartAnchorStableName;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
|
||||||
FString EndRoomStableName;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
|
||||||
FVector ControlPoint0Meters = FVector::ZeroVector;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
|
||||||
FVector ControlPoint1Meters = FVector::ZeroVector;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
|
||||||
FVector ControlPoint2Meters = FVector::ZeroVector;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
|
||||||
FVector ControlPoint3Meters = FVector::ZeroVector;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0.5"))
|
|
||||||
double BoreRadiusMeters = 2.0;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "1"))
|
|
||||||
double BoreHeightMeters = 4.0;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
|
||||||
double FloorSupportDepthMeters = 0.5;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
|
||||||
double SmoothWidthMeters = 0.0;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
|
||||||
double UnionWidthMeters = 0.0;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
|
||||||
double IrregularityAmplitudeMeters = 0.0;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
|
||||||
double IrregularityWavelengthMeters = 0.0;
|
|
||||||
|
|
||||||
// Lattice tunnels require an explicit bounded polyline; cave tunnels use the cubic curve.
|
|
||||||
// Les tunnels en treillis exigent une polyligne bornee; les tunnels caves utilisent la courbe cubique.
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
|
||||||
TArray<FVector> PolylineMeters;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Optional Core authored recipe. Empty arrays and None preserve the prior asset output.
|
|
||||||
// Recette Core authored facultative. None et les tableaux vides conservent la sortie historique.
|
|
||||||
USTRUCT(BlueprintType)
|
|
||||||
struct VOXELFORGE_API FVoxelForgeCoreGeometry
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
|
||||||
EVoxelForgeCoreGeneratorRecipe GeneratorRecipe = EVoxelForgeCoreGeneratorRecipe::None;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
|
||||||
EVoxelForgeCoreHeightfieldProfile HeightfieldProfile = EVoxelForgeCoreHeightfieldProfile::PeriodicHills;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry|Heightfield", meta = (Units = "m"))
|
|
||||||
double HeightfieldBaseMeters = 0.0;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry|Heightfield", meta = (Units = "m"))
|
|
||||||
FVector2D HeightfieldAmplitudeMeters = FVector2D(20.0, 20.0);
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry|Heightfield", meta = (Units = "m", ClampMin = "0"))
|
|
||||||
FVector2D HeightfieldWavelengthMeters = FVector2D(256.0, 256.0);
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry|Heightfield", meta = (ClampMin = "0.01", ClampMax = "4.0"))
|
|
||||||
double MirrorLambda = 1.0;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry|Heightfield", meta = (Units = "m", ClampMin = "0"))
|
|
||||||
double MinimumArenaClearanceMeters = 0.0;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
|
||||||
TArray<FVoxelForgeCoreStampDefinition> Stamps;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
|
||||||
TArray<FVoxelForgeCoreRoomDefinition> Rooms;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
|
||||||
TArray<FVoxelForgeCoreTunnelDefinition> Tunnels;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EVoxelStrateTransition — How two adjacent strates meet at their shared boundary.
|
* EVoxelStrateTransition — How two adjacent strates blend at their shared boundary.
|
||||||
*
|
*
|
||||||
* Each strate boundary can use a different transition style. The transition type
|
* Each strate boundary can use a different transition style. The transition type
|
||||||
* 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.
|
||||||
*
|
*
|
||||||
* In the Core composition path, TransitionBlendChunks defines the full permitted
|
* Gradient: Smooth linear interpolation of generation params across TransitionBlendChunks.
|
||||||
* crossing depth between the upper bedrock and lower ceiling bands. Each band keeps
|
* This is the classic approach — no seam visible, but also no dramatic
|
||||||
* its authored thickness and the solid bands always overlap or touch. The legacy manager
|
* geological feature at the boundary. Best for similar-looking strates.
|
||||||
* may also blend generation parameters for Gradient; Core does not blend unrelated fields.
|
|
||||||
*
|
|
||||||
* Gradient: A low-amplitude smooth height field moves the shared interface and both
|
|
||||||
* content-facing band surfaces. Best for gentle geological variation.
|
|
||||||
*
|
*
|
||||||
* Hard: No blending at all. The strate's params apply right up to the boundary,
|
* Hard: No blending at all. The strate's params apply right up to the boundary,
|
||||||
* then instantly switch to the neighbor's params. The abrupt change in
|
* then instantly switch to the neighbor's params. The abrupt change in
|
||||||
@@ -423,21 +217,26 @@ struct VOXELFORGE_API FVoxelForgeCoreGeometry
|
|||||||
* discontinuity at the boundary. Best for strates that should feel like
|
* discontinuity at the boundary. Best for strates that should feel like
|
||||||
* distinct geological layers with a visible dividing line.
|
* distinct geological layers with a visible dividing line.
|
||||||
*
|
*
|
||||||
* Interleaved: A deterministic 3D domain-warped hash field sets the overlap depth per XY
|
* Interleaved: The boundary position is warped by a 3D noise field, so "fingers"
|
||||||
* column from zero to the declared width, creating irregular fingers.
|
* of one strate reach into the other. Some XY columns transition early,
|
||||||
* Best for strates that should feel grown together.
|
* others late, creating an irregular, interlocking boundary. The warp
|
||||||
|
* amplitude is ~2 chunks, and Z frequency is lower than XY to produce
|
||||||
|
* horizontal finger-like intrusions. Best for strates that should feel
|
||||||
|
* like they grew into each other over geological time.
|
||||||
*/
|
*/
|
||||||
UENUM(BlueprintType)
|
UENUM(BlueprintType)
|
||||||
enum class EVoxelStrateTransition : uint8
|
enum class EVoxelStrateTransition : uint8
|
||||||
{
|
{
|
||||||
// Smoothly undulates the shared bedrock/ceiling interface in the Core composition path.
|
// Smooth linear interpolation of all generation params across the blend zone.
|
||||||
|
// 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.
|
||||||
// Creates natural cliffs, ledges, or material discontinuities.
|
// Creates natural cliffs, ledges, or material discontinuities.
|
||||||
Hard UMETA(DisplayName = "Hard (sharp boundary with cliff)"),
|
Hard UMETA(DisplayName = "Hard (sharp boundary with cliff)"),
|
||||||
|
|
||||||
// A 3D domain-warped hash field creates finger-like intrusions within the declared width.
|
// 3D noise warps the boundary position per XY column.
|
||||||
|
// Creates finger-like intrusions of one strate into the other.
|
||||||
Interleaved UMETA(DisplayName = "Interleaved (domain-warped fingers)")
|
Interleaved UMETA(DisplayName = "Interleaved (domain-warped fingers)")
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2053,6 +1852,8 @@ struct VOXELFORGE_API FStrateDisturbanceParams
|
|||||||
UENUM(BlueprintType)
|
UENUM(BlueprintType)
|
||||||
enum class EVoxelPassageStyle : uint8
|
enum class EVoxelPassageStyle : uint8
|
||||||
{
|
{
|
||||||
|
// “Straight” is retained as the authored style name for the base connection, but its runtime
|
||||||
|
// geometry is a gentle two-leg switchback rather than a vertical shaft.
|
||||||
Straight UMETA(DisplayName = "Straight (walkable sloped tunnel)"),
|
Straight UMETA(DisplayName = "Straight (walkable sloped tunnel)"),
|
||||||
Worm UMETA(DisplayName = "Worm (organic meander around the axis)"),
|
Worm UMETA(DisplayName = "Worm (organic meander around the axis)"),
|
||||||
Spiral UMETA(DisplayName = "Spiral (corkscrew descent)"),
|
Spiral UMETA(DisplayName = "Spiral (corkscrew descent)"),
|
||||||
@@ -2077,12 +1878,14 @@ struct VOXELFORGE_API FStratePassageConfig
|
|||||||
|
|
||||||
// Tunnel shape.
|
// Tunnel shape.
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage")
|
||||||
EVoxelPassageStyle Style = EVoxelPassageStyle::Worm;
|
EVoxelPassageStyle Style = EVoxelPassageStyle::Straight;
|
||||||
|
|
||||||
// ----- WIDTH (tapers along the length) -----
|
// ----- WIDTH (tapers along the length) -----
|
||||||
// Radius at the two mouths (entry/exit) and at the middle. Equal = uniform tube;
|
// Radius at the two mouths (entry/exit) and at the middle. Equal = uniform tube;
|
||||||
// Core raises both radii to at least 12 owner voxels (1.5 m) for capsule clearance.
|
// The walkable default is 1.5x the original comfortable profile: 2.25 m diameter at mouths
|
||||||
// The passage landing remains wider for turning.
|
// and 1.875 m at the waist when the stored radii remain 9/7.5 voxels at 12.5 cm per voxel.
|
||||||
|
// Double these asset values to retain the previous 4.5/3.75 m dimensions. The §6.5 landing
|
||||||
|
// room remains wider for turning.
|
||||||
// Mouth > Mid = chambers at the ends with a squeeze between; Mid > Mouth = a bulge.
|
// Mouth > Mid = chambers at the ends with a squeeze between; Mid > Mouth = a bulge.
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Width", meta = (ClampMin = "1.0"))
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Width", meta = (ClampMin = "1.0"))
|
||||||
float MouthRadius = 9.0f;
|
float MouthRadius = 9.0f;
|
||||||
@@ -2109,8 +1912,8 @@ struct VOXELFORGE_API FStratePassageConfig
|
|||||||
float DistanceMax = 192.0f;
|
float DistanceMax = 192.0f;
|
||||||
|
|
||||||
// ----- SHAPE detail -----
|
// ----- SHAPE detail -----
|
||||||
// Max sideways variation from the winding path's axis (voxels). Core keeps the route inside
|
// Exotic styles only: max sideways excursion from the axis (voxels). 24 = 6 m; 0 = straight
|
||||||
// DistanceMax and adds enough distance to meet the walkable grade.
|
// even in Worm style. The default walkable switchback does not use this wander value.
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Shape", meta = (ClampMin = "0.0"))
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Shape", meta = (ClampMin = "0.0"))
|
||||||
float Wander = 24.0f;
|
float Wander = 24.0f;
|
||||||
|
|
||||||
@@ -2123,8 +1926,8 @@ struct VOXELFORGE_API FStratePassageConfig
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Shape", meta = (ClampMin = "0.0"))
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Shape", meta = (ClampMin = "0.0"))
|
||||||
float VerticalWobble = 0.0f;
|
float VerticalWobble = 0.0f;
|
||||||
|
|
||||||
// Spiral style: helix radius (voxels) and minimum number of turns. Core adds turns when
|
// Spiral style: helix radius (voxels) and number of full turns over the descent. The default
|
||||||
// needed to meet the walkable grade. The default stored radius is 3 m at the legacy scale.
|
// radius is 6 m.
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Spiral", meta = (ClampMin = "1.0"))
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Spiral", meta = (ClampMin = "1.0"))
|
||||||
float SpiralRadius = 24.0f;
|
float SpiralRadius = 24.0f;
|
||||||
|
|
||||||
@@ -2491,11 +2294,6 @@ struct VOXELFORGE_API FStrateLandmark
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark")
|
||||||
ELandmarkAnchor AnchorMode = ELandmarkAnchor::HashLattice;
|
ELandmarkAnchor AnchorMode = ELandmarkAnchor::HashLattice;
|
||||||
|
|
||||||
// Optional stable Core feature binding; an empty value keeps lattice/mouth placement unchanged.
|
|
||||||
// Liaison facultative vers une ancre Core stable; vide, le placement existant reste identique.
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Core Geometry")
|
|
||||||
FString CoreGeometryStableName;
|
|
||||||
|
|
||||||
// ----- Hash lattice (AnchorMode == HashLattice — scattered placement; cheap at any radius) -----
|
// ----- Hash lattice (AnchorMode == HashLattice — scattered placement; cheap at any radius) -----
|
||||||
|
|
||||||
// Average spacing between instances, IN CHUNKS = the lattice cell size (one candidate per cell, so cost
|
// Average spacing between instances, IN CHUNKS = the lattice cell size (one candidate per cell, so cost
|
||||||
|
|||||||
@@ -332,33 +332,16 @@ struct FVoxelTileKey
|
|||||||
FVoxelTileKey(const FIntVector& InCoord, int32 InLevel) : Coord(InCoord), Level(InLevel) {}
|
FVoxelTileKey(const FIntVector& InCoord, int32 InLevel) : Coord(InCoord), Level(InLevel) {}
|
||||||
|
|
||||||
// Cell size (sampling step) in voxels, and tile extent in voxels per axis.
|
// Cell size (sampling step) in voxels, and tile extent in voxels per axis.
|
||||||
int32 StepVoxels() const
|
int32 StepVoxels() const { return 1 << Level; }
|
||||||
{
|
int32 ExtentVoxels() const { return CHUNK_SIZE << Level; }
|
||||||
const uint32 SafeLevel = static_cast<uint32>(FMath::Clamp(Level, 0, 8));
|
|
||||||
return static_cast<int32>(uint32{1} << SafeLevel);
|
|
||||||
}
|
|
||||||
int32 ExtentVoxels() const
|
|
||||||
{
|
|
||||||
const uint32 SafeLevel = static_cast<uint32>(FMath::Clamp(Level, 0, 8));
|
|
||||||
return static_cast<int32>(static_cast<uint32>(CHUNK_SIZE) * (uint32{1} << SafeLevel));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Min-corner of the tile in VOXEL coords (what the mesher takes).
|
// Min-corner of the tile in VOXEL coords (what the mesher takes).
|
||||||
FIntVector OriginVoxels() const
|
FIntVector OriginVoxels() const { return Coord * (CHUNK_SIZE << Level); }
|
||||||
{
|
|
||||||
const int64 Extent = ExtentVoxels();
|
|
||||||
const auto ScaleAxis = [Extent](int32 Axis)
|
|
||||||
{
|
|
||||||
return static_cast<int32>(FMath::Clamp<int64>(
|
|
||||||
static_cast<int64>(Axis) * Extent, MIN_int32, MAX_int32));
|
|
||||||
};
|
|
||||||
return FIntVector(ScaleAxis(Coord.X), ScaleAxis(Coord.Y), ScaleAxis(Coord.Z));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tile centre in world cm (for distance sorting / LOD selection).
|
// Tile centre in world cm (for distance sorting / LOD selection).
|
||||||
FVector CenterCm() const
|
FVector CenterCm() const
|
||||||
{
|
{
|
||||||
const double Ext = static_cast<double>(ExtentVoxels()) * static_cast<double>(VOXEL_SIZE);
|
const double Ext = (double)(CHUNK_SIZE << Level) * (double)VOXEL_SIZE;
|
||||||
return FVector((Coord.X + 0.5) * Ext, (Coord.Y + 0.5) * Ext, (Coord.Z + 0.5) * Ext);
|
return FVector((Coord.X + 0.5) * Ext, (Coord.Y + 0.5) * Ext, (Coord.Z + 0.5) * Ext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,9 +52,6 @@ struct FVoxelForgeCoreTileHashRecord
|
|||||||
struct FVoxelTileCancellationState
|
struct FVoxelTileCancellationState
|
||||||
{
|
{
|
||||||
std::atomic<bool> bObsolete{false};
|
std::atomic<bool> bObsolete{false};
|
||||||
uint64 RequestOrdinal = 0;
|
|
||||||
uint32 RequestEpoch = 0;
|
|
||||||
int32 WorkPriority = 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -148,10 +145,6 @@ struct FChunkResult
|
|||||||
// per-tile profiling remains opt-in.
|
// per-tile profiling remains opt-in.
|
||||||
uint64 GenerationStartCycles = 0;
|
uint64 GenerationStartCycles = 0;
|
||||||
uint64 GenerationEndCycles = 0;
|
uint64 GenerationEndCycles = 0;
|
||||||
// Optional phase timings recorded only while the bounded startup trace is active.
|
|
||||||
double CoreGenerateSeconds = 0.0;
|
|
||||||
double MeshCopySeconds = 0.0;
|
|
||||||
double StreamBuildSeconds = 0.0;
|
|
||||||
uint64 ApplyStartCycles = 0;
|
uint64 ApplyStartCycles = 0;
|
||||||
EVoxelTileWorkPriority WorkPriority = EVoxelTileWorkPriority::Horizon;
|
EVoxelTileWorkPriority WorkPriority = EVoxelTileWorkPriority::Horizon;
|
||||||
// Ordinals make admitted order and actual worker start order auditable across MPSC results.
|
// Ordinals make admitted order and actual worker start order auditable across MPSC results.
|
||||||
@@ -359,10 +352,6 @@ public:
|
|||||||
* the first resolved collidable support tile. */
|
* the first resolved collidable support tile. */
|
||||||
TSet<FVoxelTileKey> CollisionNotRequiredTiles;
|
TSet<FVoxelTileKey> CollisionNotRequiredTiles;
|
||||||
|
|
||||||
/** Known-empty collision classifications survive unload for the tile's current edit epoch. */
|
|
||||||
TMap<FVoxelTileKey, uint32> KnownEmptyCollisionTileEpoch;
|
|
||||||
TSet<FVoxelTileKey> KnownEmptyCollisionSolidTiles;
|
|
||||||
|
|
||||||
/** Subset of CollisionNotRequiredTiles proved all-solid. A support probe inside one of these
|
/** Subset of CollisionNotRequiredTiles proved all-solid. A support probe inside one of these
|
||||||
* tiles searches upward for the boundary tile; all-air/empty tiles search downward. */
|
* tiles searches upward for the boundary tile; all-air/empty tiles search downward. */
|
||||||
TSet<FVoxelTileKey> CollisionSolidTiles;
|
TSet<FVoxelTileKey> CollisionSolidTiles;
|
||||||
@@ -625,7 +614,6 @@ public:
|
|||||||
FVector GetVoxelForgeCoreDefaultCenterWorld() const;
|
FVector GetVoxelForgeCoreDefaultCenterWorld() const;
|
||||||
bool QueryVoxelForgeCoreDensityAt(FVector WorldLocation, float& OutDensity) const;
|
bool QueryVoxelForgeCoreDensityAt(FVector WorldLocation, float& OutDensity) const;
|
||||||
bool GetVoxelForgeCoreWorldAssembly(FString& OutCanonicalJson, FString& OutContentHash) const;
|
bool GetVoxelForgeCoreWorldAssembly(FString& OutCanonicalJson, FString& OutContentHash) const;
|
||||||
bool GetVoxelForgeCoreResolvedLayout(FString& OutCanonicalJson, FString& OutStableIdentity) const;
|
|
||||||
|
|
||||||
UFUNCTION(BlueprintPure, Category = "Voxel World|Strate")
|
UFUNCTION(BlueprintPure, Category = "Voxel World|Strate")
|
||||||
int32 GetStrateAtPosition(FVector WorldPosition) const;
|
int32 GetStrateAtPosition(FVector WorldPosition) const;
|
||||||
@@ -790,10 +778,7 @@ private:
|
|||||||
TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> VoxelForgeCoreState;
|
TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> VoxelForgeCoreState;
|
||||||
FString VoxelForgeCoreWorldAssemblyJson;
|
FString VoxelForgeCoreWorldAssemblyJson;
|
||||||
FString VoxelForgeCoreWorldAssemblyHash;
|
FString VoxelForgeCoreWorldAssemblyHash;
|
||||||
FString VoxelForgeCoreResolvedLayoutJson;
|
|
||||||
FString VoxelForgeCoreResolvedLayoutIdentity;
|
|
||||||
FVector CoreFallbackCenterLocalCm = FVector::ZeroVector;
|
FVector CoreFallbackCenterLocalCm = FVector::ZeroVector;
|
||||||
int32 CoreRenderDistanceChunks = 0;
|
|
||||||
bool bCoreRegionCompilePending = false;
|
bool bCoreRegionCompilePending = false;
|
||||||
TArray<FVoxelTileKey> PendingCoreRegionTiles;
|
TArray<FVoxelTileKey> PendingCoreRegionTiles;
|
||||||
TSet<FVoxelTileKey> PendingCoreRegionTileSet;
|
TSet<FVoxelTileKey> PendingCoreRegionTileSet;
|
||||||
@@ -1209,9 +1194,6 @@ public:
|
|||||||
// and records the ones ONLY a CollisionOnly anchor wants in CollisionOnlyTiles.
|
// and records the ones ONLY a CollisionOnly anchor wants in CollisionOnlyTiles.
|
||||||
void AddAnchorDesiredTiles();
|
void AddAnchorDesiredTiles();
|
||||||
void ReconcileCollisionDesiredTiles();
|
void ReconcileCollisionDesiredTiles();
|
||||||
void RestoreKnownEmptyCollisionTiles();
|
|
||||||
void RebaseKnownEmptyCollisionEpoch();
|
|
||||||
void InvalidateKnownEmptyCollisionTile(const FVoxelTileKey& Tile, const TCHAR* Reason);
|
|
||||||
// Forces a desired-set rebuild next Tick even if neither the player nor an anchor crossed a tile
|
// Forces a desired-set rebuild next Tick even if neither the player nor an anchor crossed a tile
|
||||||
// boundary (set by UnregisterStreamingAnchor so a removed anchor's tiles get culled).
|
// boundary (set by UnregisterStreamingAnchor so a removed anchor's tiles get culled).
|
||||||
bool bForceDesiredRebuild = false;
|
bool bForceDesiredRebuild = false;
|
||||||
@@ -1247,30 +1229,10 @@ public:
|
|||||||
bool bSampleStacksStarted = false;
|
bool bSampleStacksStarted = false;
|
||||||
bool bStartupApplyBoostStarted = false;
|
bool bStartupApplyBoostStarted = false;
|
||||||
bool bStartupApplyBoostEnded = false;
|
bool bStartupApplyBoostEnded = false;
|
||||||
// Fast-travel admission stays disabled until the initial near LOD and collision set is ready.
|
|
||||||
bool bInitialArrivalSetSettled = false;
|
|
||||||
uint64 StartupApplyBoostStartCycles = 0;
|
|
||||||
// Trace the resolved startup budget once per world. / Tracer une fois le budget de demarrage resolu par monde.
|
// Trace the resolved startup budget once per world. / Tracer une fois le budget de demarrage resolu par monde.
|
||||||
bool bStartupApplyConfigTraced = false;
|
bool bStartupApplyConfigTraced = false;
|
||||||
uint64 LastApplyFrameCycles = 0;
|
uint64 LastApplyFrameCycles = 0;
|
||||||
float StartupTraceSpeedCmPerSecond = 0.0f;
|
float StartupTraceSpeedCmPerSecond = 0.0f;
|
||||||
bool bSpeedAwareTravelMode = false;
|
|
||||||
bool bSpeedAwareFastMode = false;
|
|
||||||
TArray<FVoxelTileKey> CachedCoreDesiredTiles;
|
|
||||||
TSet<FVoxelTileKey> CachedCoreDesiredTileSet;
|
|
||||||
FIntVector CachedCoreDesiredCenter = FIntVector(MAX_int32, MAX_int32, MAX_int32);
|
|
||||||
bool bHasCachedCoreDesiredTiles = false;
|
|
||||||
TArray<FVoxelTileKey> CachedDesiredCandidateKeys;
|
|
||||||
TSet<FVoxelTileKey> CachedDesiredCandidateSet;
|
|
||||||
TSet<FVoxelTileKey> CachedDesiredBaseVisualTiles;
|
|
||||||
TSet<FVoxelTileKey> CachedDesiredOverlayTiles;
|
|
||||||
FIntVector CachedDesiredMaterializationCenter = FIntVector(MAX_int32, MAX_int32, MAX_int32);
|
|
||||||
bool bHasCachedDesiredMaterialization = false;
|
|
||||||
TArray<FVoxelTileKey> CachedDesiredPriorityOrder;
|
|
||||||
FIntVector CachedDesiredPriorityCenter = FIntVector(MAX_int32, MAX_int32, MAX_int32);
|
|
||||||
bool bHasCachedDesiredPriorityOrder = false;
|
|
||||||
int32 IncrementalDesiredCenterCrossings = 0;
|
|
||||||
int32 IncrementalDesiredMaterializationUpdates = 0;
|
|
||||||
TArray<FVoxelTileKey> DesiredSorted; // critical floor prefix, then true-pawn distance-first
|
TArray<FVoxelTileKey> DesiredSorted; // critical floor prefix, then true-pawn distance-first
|
||||||
TArray<FVoxelTileKey> CriticalDesiredTiles; // occupied/support tile, then heading tile
|
TArray<FVoxelTileKey> CriticalDesiredTiles; // occupied/support tile, then heading tile
|
||||||
// Ancestor -> desired descendants, rebuilt with DesiredSorted. Reconciliation uses this
|
// Ancestor -> desired descendants, rebuilt with DesiredSorted. Reconciliation uses this
|
||||||
@@ -1378,7 +1340,6 @@ public:
|
|||||||
double HeadlessStreamingTestDistanceCm = 0.0;
|
double HeadlessStreamingTestDistanceCm = 0.0;
|
||||||
float HeadlessStreamingTestSpeedCmPerSecond = 0.0f;
|
float HeadlessStreamingTestSpeedCmPerSecond = 0.0f;
|
||||||
float HeadlessStreamingTestStartSeconds = 0.0f;
|
float HeadlessStreamingTestStartSeconds = 0.0f;
|
||||||
float HeadlessStreamingTestMoveDurationSeconds = 0.0f;
|
|
||||||
float HeadlessStreamingTestExitSeconds = 0.0f;
|
float HeadlessStreamingTestExitSeconds = 0.0f;
|
||||||
float HeadlessStreamingTestTeleportAtSeconds = 0.0f;
|
float HeadlessStreamingTestTeleportAtSeconds = 0.0f;
|
||||||
float HeadlessStreamingTestTeleportDistanceCm = 0.0f;
|
float HeadlessStreamingTestTeleportDistanceCm = 0.0f;
|
||||||
@@ -1619,7 +1580,7 @@ public:
|
|||||||
// ONE frame → a game-thread teardown spike. Drained by ProcessUnloadQueue (catch-up scaled).
|
// ONE frame → a game-thread teardown spike. Drained by ProcessUnloadQueue (catch-up scaled).
|
||||||
TSet<FVoxelTileKey> PendingUnload;
|
TSet<FVoxelTileKey> PendingUnload;
|
||||||
|
|
||||||
void ProcessPendingChunks(uint64 TickStartCycles);
|
void ProcessPendingChunks();
|
||||||
void ProcessUnloadQueue(); // budgeted teardown drain (see PendingUnload)
|
void ProcessUnloadQueue(); // budgeted teardown drain (see PendingUnload)
|
||||||
void QueuePendingUnload(const FVoxelTileKey& Tile);
|
void QueuePendingUnload(const FVoxelTileKey& Tile);
|
||||||
void HideOverlappingDesiredVisuals(const FVoxelTileKey& RetiredTile);
|
void HideOverlappingDesiredVisuals(const FVoxelTileKey& RetiredTile);
|
||||||
|
|||||||
@@ -7,13 +7,6 @@ public class VoxelForge : ModuleRules
|
|||||||
{
|
{
|
||||||
public VoxelForge(ReadOnlyTargetRules Target) : base(Target)
|
public VoxelForge(ReadOnlyTargetRules Target) : base(Target)
|
||||||
{
|
{
|
||||||
if (Target.Platform == UnrealTargetPlatform.Linux)
|
|
||||||
{
|
|
||||||
// Test translation units have independent anonymous-namespace helpers that collide in
|
|
||||||
// Linux Unity batches. Keep Linux compilation one source per TU; Windows remains unchanged.
|
|
||||||
bUseUnity = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// PCH = Precompiled Headers - speeds up compilation
|
// PCH = Precompiled Headers - speeds up compilation
|
||||||
// UseExplicitOrSharedPCHs is the modern recommended setting
|
// UseExplicitOrSharedPCHs is the modern recommended setting
|
||||||
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||||
|
|||||||
@@ -39,13 +39,6 @@ public class VoxelForgeCore : ModuleRules
|
|||||||
|
|
||||||
PublicDependencyModuleNames.Add("Core");
|
PublicDependencyModuleNames.Add("Core");
|
||||||
CppStandard = CppStandardVersion.Cpp20;
|
CppStandard = CppStandardVersion.Cpp20;
|
||||||
if (Target.Platform == UnrealTargetPlatform.Linux)
|
|
||||||
{
|
|
||||||
// The compatibility include directory contains Unreal-shaped placeholders. On Linux,
|
|
||||||
// let generated module TUs pass those names through to UE while Core shims opt in.
|
|
||||||
PrivateDefinitions.Add("VFCORE_COMPAT_FORWARD_HEADERS=1");
|
|
||||||
MinCpuArchX64 = MinimumCpuArchitectureX64.AVX2;
|
|
||||||
}
|
|
||||||
// Canonical Standalone code predates UE's stricter warning defaults; keep this narrow to legal local shadowing.
|
// Canonical Standalone code predates UE's stricter warning defaults; keep this narrow to legal local shadowing.
|
||||||
CppCompileWarningSettings.ShadowVariableWarningLevel = WarningLevel.Off;
|
CppCompileWarningSettings.ShadowVariableWarningLevel = WarningLevel.Off;
|
||||||
FPSemantics = FPSemanticsMode.Precise;
|
FPSemantics = FPSemanticsMode.Precise;
|
||||||
|
|||||||
@@ -878,8 +878,6 @@ struct FExploreArguments
|
|||||||
bool bPassageFloorWritesOverride = false;
|
bool bPassageFloorWritesOverride = false;
|
||||||
int32 PassageFloorWrites = 1;
|
int32 PassageFloorWrites = 1;
|
||||||
bool bStandaloneWorldExport = false;
|
bool bStandaloneWorldExport = false;
|
||||||
bool bOwnerFourStratePlan = false;
|
|
||||||
bool bSkipStandaloneCoreAdapterParity = false;
|
|
||||||
bool bCoreWorldTest = false;
|
bool bCoreWorldTest = false;
|
||||||
bool bCoreWorldStreamTraceOnly = false;
|
bool bCoreWorldStreamTraceOnly = false;
|
||||||
bool bRenderCameraOverride = false;
|
bool bRenderCameraOverride = false;
|
||||||
@@ -924,12 +922,6 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
|||||||
int32 StandaloneWorldExport = 0;
|
int32 StandaloneWorldExport = 0;
|
||||||
FParse::Value(*Params, TEXT("standaloneworldexport="), StandaloneWorldExport);
|
FParse::Value(*Params, TEXT("standaloneworldexport="), StandaloneWorldExport);
|
||||||
OutArguments.bStandaloneWorldExport = StandaloneWorldExport != 0;
|
OutArguments.bStandaloneWorldExport = StandaloneWorldExport != 0;
|
||||||
int32 OwnerFourStratePlan = 0;
|
|
||||||
FParse::Value(*Params, TEXT("ownerfourstrateplan="), OwnerFourStratePlan);
|
|
||||||
OutArguments.bOwnerFourStratePlan = OwnerFourStratePlan != 0;
|
|
||||||
int32 SkipStandaloneCoreAdapterParity = 0;
|
|
||||||
FParse::Value(*Params, TEXT("skipstandalonecoreadapterparity="), SkipStandaloneCoreAdapterParity);
|
|
||||||
OutArguments.bSkipStandaloneCoreAdapterParity = SkipStandaloneCoreAdapterParity != 0;
|
|
||||||
int32 CoreWorldTest = 0;
|
int32 CoreWorldTest = 0;
|
||||||
FParse::Value(*Params, TEXT("coreworldtest="), CoreWorldTest);
|
FParse::Value(*Params, TEXT("coreworldtest="), CoreWorldTest);
|
||||||
OutArguments.bCoreWorldTest = CoreWorldTest != 0;
|
OutArguments.bCoreWorldTest = CoreWorldTest != 0;
|
||||||
@@ -1274,11 +1266,12 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Passage walks need both mouths; a render/export-only generation can inspect an end slot.
|
// The historical explorer contract needs two adjacent mouths around an interior slot. A
|
||||||
// Parity exports also accept the game's authored single-strate layout.
|
// parity export can instead target the game's authored single-strate layout, which has no
|
||||||
const bool bRequiresInteriorSlot = OutArguments.bWalk;
|
// such mouths and is valid for export-only mode. Build() still validates the slot against
|
||||||
const int32 MinimumSlot = OutArguments.bGameRuntimeConfig || !bRequiresInteriorSlot ? 0 : 1;
|
// the loaded settings layout.
|
||||||
const int32 MaximumSlot = OutArguments.bGameRuntimeConfig || !bRequiresInteriorSlot
|
const int32 MinimumSlot = OutArguments.bGameRuntimeConfig ? 0 : 1;
|
||||||
|
const int32 MaximumSlot = OutArguments.bGameRuntimeConfig
|
||||||
? MaxSyntheticStrates - 1 : MaxSyntheticStrates - 2;
|
? MaxSyntheticStrates - 1 : MaxSyntheticStrates - 2;
|
||||||
if (OutArguments.Slot < MinimumSlot || OutArguments.Slot > MaximumSlot)
|
if (OutArguments.Slot < MinimumSlot || OutArguments.Slot > MaximumSlot)
|
||||||
{
|
{
|
||||||
@@ -1291,8 +1284,8 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
OutError = FString::Printf(
|
OutError = FString::Printf(
|
||||||
TEXT("slot must be in [0,%d]; walk mode requires an interior slot in [1,%d] for both passage mouths."),
|
TEXT("slot must be an interior slot in [1,%d] so arrival and departure mouths exist."),
|
||||||
MaxSyntheticStrates - 1, MaxSyntheticStrates - 2);
|
MaxSyntheticStrates - 2);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1505,17 +1498,6 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
|||||||
OutError = TEXT("standaloneworldexport requires modes=export and settingsref, and must omit strateref.");
|
OutError = TEXT("standaloneworldexport requires modes=export and settingsref, and must omit strateref.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (OutArguments.bOwnerFourStratePlan
|
|
||||||
&& (OutArguments.SettingsReference.IsEmpty() || !OutArguments.StrateReference.IsEmpty()))
|
|
||||||
{
|
|
||||||
OutError = TEXT("ownerfourstrateplan requires settingsref and must omit strateref.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (OutArguments.bSkipStandaloneCoreAdapterParity && !OutArguments.bStandaloneWorldExport)
|
|
||||||
{
|
|
||||||
OutError = TEXT("skipstandalonecoreadapterparity requires standaloneworldexport.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (OutArguments.bCoreWorldTest
|
if (OutArguments.bCoreWorldTest
|
||||||
&& (OutArguments.SettingsReference.IsEmpty() || !OutArguments.StrateReference.IsEmpty()))
|
&& (OutArguments.SettingsReference.IsEmpty() || !OutArguments.StrateReference.IsEmpty()))
|
||||||
{
|
{
|
||||||
@@ -1720,66 +1702,6 @@ struct FExploreMeshBvhNode
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
bool ApplyOwnerFourStratePlan(UVoxelSettings* Settings,
|
|
||||||
TArray<TStrongObjectPtr<UVoxelStrateDefinition>>& OutDefinitions,
|
|
||||||
bool bDuplicateDefinitions, FString& OutError)
|
|
||||||
{
|
|
||||||
if (Settings == nullptr)
|
|
||||||
{
|
|
||||||
OutError = TEXT("The transient owner plan has no settings object.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!Settings->Season.IsNull())
|
|
||||||
{
|
|
||||||
OutError = TEXT("The transient four-strate owner plan cannot replace an assigned season manifest.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static const TCHAR* AssetPaths[] = {
|
|
||||||
TEXT("/Game/VoxelForge/DA_Strate1.DA_Strate1"),
|
|
||||||
TEXT("/Game/VoxelForge/DA_Strate2.DA_Strate2"),
|
|
||||||
TEXT("/Game/VoxelForge/DA_Strate3.DA_Strate3"),
|
|
||||||
TEXT("/Game/VoxelForge/DA_Strate4.DA_Strate4")
|
|
||||||
};
|
|
||||||
Settings->TotalStrates = UE_ARRAY_COUNT(AssetPaths);
|
|
||||||
Settings->StratePool.Reset();
|
|
||||||
Settings->FixedStrates.Reset();
|
|
||||||
OutDefinitions.Reset();
|
|
||||||
OutDefinitions.Reserve(UE_ARRAY_COUNT(AssetPaths));
|
|
||||||
|
|
||||||
for (int32 Index = 0; Index < UE_ARRAY_COUNT(AssetPaths); ++Index)
|
|
||||||
{
|
|
||||||
UVoxelStrateDefinition* AuthoredDefinition = LoadObject<UVoxelStrateDefinition>(
|
|
||||||
nullptr, AssetPaths[Index]);
|
|
||||||
if (AuthoredDefinition == nullptr)
|
|
||||||
{
|
|
||||||
OutError = FString::Printf(
|
|
||||||
TEXT("Could not load staged owner strate asset '%s'."), AssetPaths[Index]);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
UVoxelStrateDefinition* Definition = AuthoredDefinition;
|
|
||||||
if (bDuplicateDefinitions)
|
|
||||||
{
|
|
||||||
Definition = DuplicateObject<UVoxelStrateDefinition>(
|
|
||||||
AuthoredDefinition, GetTransientPackage(),
|
|
||||||
FName(*FString::Printf(TEXT("VoxelForgeOwnerPlan_Strate_%d"), Index)));
|
|
||||||
if (Definition == nullptr)
|
|
||||||
{
|
|
||||||
OutError = FString::Printf(
|
|
||||||
TEXT("Could not make a transient copy of owner strate asset '%s'."), AssetPaths[Index]);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Settings->FixedStrates.Add(Index,
|
|
||||||
TSoftObjectPtr<UVoxelStrateDefinition>(Definition));
|
|
||||||
OutDefinitions.Add(TStrongObjectPtr<UVoxelStrateDefinition>(Definition));
|
|
||||||
}
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display,
|
|
||||||
TEXT("[VoxelForgeOwnerPlan] transient fixed slots=4 assets=DA_Strate1,DA_Strate2,DA_Strate3,DA_Strate4 settings_asset_untouched=1"));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct FExploreWorld
|
struct FExploreWorld
|
||||||
{
|
{
|
||||||
TStrongObjectPtr<UVoxelSettings> Settings;
|
TStrongObjectPtr<UVoxelSettings> Settings;
|
||||||
@@ -1912,7 +1834,7 @@ struct FExploreWorld
|
|||||||
Settings->InterStrateGapChunks, Settings->SkirtCells);
|
Settings->InterStrateGapChunks, Settings->SkirtCells);
|
||||||
}
|
}
|
||||||
|
|
||||||
const int32 NumStrates = Arguments.bOwnerFourStratePlan ? 4 : bUseAuthoredSettings
|
const int32 NumStrates = bUseAuthoredSettings
|
||||||
? (Arguments.bGameRuntimeConfig
|
? (Arguments.bGameRuntimeConfig
|
||||||
? FMath::Max(Settings->TotalStrates, 1)
|
? FMath::Max(Settings->TotalStrates, 1)
|
||||||
: FMath::Max(Settings->TotalStrates, Arguments.Slot + 2))
|
: FMath::Max(Settings->TotalStrates, Arguments.Slot + 2))
|
||||||
@@ -1942,18 +1864,10 @@ struct FExploreWorld
|
|||||||
// loaded package remains untouched.
|
// loaded package remains untouched.
|
||||||
Settings->TotalStrates = NumStrates;
|
Settings->TotalStrates = NumStrates;
|
||||||
|
|
||||||
if (Arguments.bOwnerFourStratePlan)
|
|
||||||
{
|
|
||||||
if (!ApplyOwnerFourStratePlan(Settings.Get(), Definitions, true, OutError))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (bUseAuthoredStrate)
|
if (bUseAuthoredStrate)
|
||||||
{
|
{
|
||||||
// Keep the historical single-asset interior probe isolated from loaded packages.
|
// The owner’s snapshot states that every slot uses DA_Strate3. Clone it per slot so
|
||||||
|
// commandlet overrides never mutate a loaded package or leak between slots.
|
||||||
Settings->StratePool.Reset();
|
Settings->StratePool.Reset();
|
||||||
Settings->FixedStrates.Reset();
|
Settings->FixedStrates.Reset();
|
||||||
}
|
}
|
||||||
@@ -2009,7 +1923,6 @@ struct FExploreWorld
|
|||||||
}
|
}
|
||||||
Definitions.Add(TStrongObjectPtr<UVoxelStrateDefinition>(Definition));
|
Definitions.Add(TStrongObjectPtr<UVoxelStrateDefinition>(Definition));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
auto ApplyTargetOverrides = [&Arguments](UVoxelStrateDefinition* Definition) -> bool
|
auto ApplyTargetOverrides = [&Arguments](UVoxelStrateDefinition* Definition) -> bool
|
||||||
{
|
{
|
||||||
@@ -13559,16 +13472,14 @@ bool VerifyStandaloneCoreAdapter(const FString& Json, FString& OutError)
|
|||||||
|
|
||||||
const std::array<std::uint64_t, 4> Actual = StandaloneCoreAggregateHashes(Rows);
|
const std::array<std::uint64_t, 4> Actual = StandaloneCoreAggregateHashes(Rows);
|
||||||
const std::array<std::uint64_t, 4> Expected{
|
const std::array<std::uint64_t, 4> Expected{
|
||||||
0xde345030348f2c5dull, 0xcbf0888004618b87ull,
|
0xdb2991c73215af4cull, 0x8236c5edd7fc8e4full,
|
||||||
0x7a11e28e052d69a8ull, 0x52b88607e8dbcbfdull};
|
0x4c48fa75a543d58eull, 0xbea22e76b34a4dfcull};
|
||||||
const VoxelForgeCore::ShapeBuildStats Stats = CoreWorld.buildStats();
|
const VoxelForgeCore::ShapeBuildStats Stats = CoreWorld.buildStats();
|
||||||
constexpr std::uint64_t ExpectedWalkSamples = 21425;
|
const bool bPassageWalkabilityPassed = Stats.walkSamples == 5642 && Stats.walkFailures == 0;
|
||||||
const bool bPassageWalkabilityPassed = Stats.walkSamples == ExpectedWalkSamples
|
if (Rows.size() != 2226 || Actual != Expected || !bPassageWalkabilityPassed)
|
||||||
&& Stats.walkFailures == 0;
|
|
||||||
if (Rows.size() != 2578 || Actual != Expected || !bPassageWalkabilityPassed)
|
|
||||||
{
|
{
|
||||||
OutError = FString::Printf(
|
OutError = FString::Printf(
|
||||||
TEXT("Core adapter parity failed at workers=%llu: tiles=%llu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx walk=%llu/%llu expected_walk=%llu"),
|
TEXT("Core adapter parity failed at workers=%llu: tiles=%llu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx walk=%llu/%llu expected=5642/5642"),
|
||||||
static_cast<unsigned long long>(WorkerLimit),
|
static_cast<unsigned long long>(WorkerLimit),
|
||||||
static_cast<unsigned long long>(Rows.size()),
|
static_cast<unsigned long long>(Rows.size()),
|
||||||
static_cast<unsigned long long>(Actual[0]),
|
static_cast<unsigned long long>(Actual[0]),
|
||||||
@@ -13576,13 +13487,12 @@ bool VerifyStandaloneCoreAdapter(const FString& Json, FString& OutError)
|
|||||||
static_cast<unsigned long long>(Actual[2]),
|
static_cast<unsigned long long>(Actual[2]),
|
||||||
static_cast<unsigned long long>(Actual[3]),
|
static_cast<unsigned long long>(Actual[3]),
|
||||||
static_cast<unsigned long long>(Stats.walkSamples - Stats.walkFailures),
|
static_cast<unsigned long long>(Stats.walkSamples - Stats.walkFailures),
|
||||||
static_cast<unsigned long long>(Stats.walkSamples),
|
static_cast<unsigned long long>(Stats.walkSamples));
|
||||||
static_cast<unsigned long long>(ExpectedWalkSamples));
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display,
|
UE_LOG(LogTemp, Display,
|
||||||
TEXT("[VoxelForgeCoreAdapter] pass schema=2 workers=%llu asset_clip_radius=%d asset_max_level=%d reference_clip_radius=3 reference_max_level=8 tiles=%llu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx walk=%llu/%llu"),
|
TEXT("[VoxelForgeCoreAdapter] pass schema=1 workers=%llu asset_clip_radius=%d asset_max_level=%d reference_clip_radius=3 reference_max_level=8 tiles=%llu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx walk=5642/5642"),
|
||||||
static_cast<unsigned long long>(WorkerLimit),
|
static_cast<unsigned long long>(WorkerLimit),
|
||||||
OwnerClipRadius,
|
OwnerClipRadius,
|
||||||
OwnerMaxLod,
|
OwnerMaxLod,
|
||||||
@@ -13590,9 +13500,7 @@ bool VerifyStandaloneCoreAdapter(const FString& Json, FString& OutError)
|
|||||||
static_cast<unsigned long long>(Actual[0]),
|
static_cast<unsigned long long>(Actual[0]),
|
||||||
static_cast<unsigned long long>(Actual[1]),
|
static_cast<unsigned long long>(Actual[1]),
|
||||||
static_cast<unsigned long long>(Actual[2]),
|
static_cast<unsigned long long>(Actual[2]),
|
||||||
static_cast<unsigned long long>(Actual[3]),
|
static_cast<unsigned long long>(Actual[3]));
|
||||||
static_cast<unsigned long long>(Stats.walkSamples - Stats.walkFailures),
|
|
||||||
static_cast<unsigned long long>(ExpectedWalkSamples));
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -13607,24 +13515,6 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
|
|||||||
*Arguments.SettingsReference);
|
*Arguments.SettingsReference);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
TStrongObjectPtr<UVoxelSettings> OwnerPlanSettings;
|
|
||||||
TArray<TStrongObjectPtr<UVoxelStrateDefinition>> OwnerPlanDefinitions;
|
|
||||||
UVoxelSettings* ExportSettings = AuthoredSettings;
|
|
||||||
if (Arguments.bOwnerFourStratePlan)
|
|
||||||
{
|
|
||||||
OwnerPlanSettings = TStrongObjectPtr<UVoxelSettings>(
|
|
||||||
DuplicateObject<UVoxelSettings>(AuthoredSettings, GetTransientPackage()));
|
|
||||||
if (!OwnerPlanSettings.IsValid())
|
|
||||||
{
|
|
||||||
OutError = TEXT("Could not make a transient copy of owner settings for the four-strate plan.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
ExportSettings = OwnerPlanSettings.Get();
|
|
||||||
if (!ApplyOwnerFourStratePlan(ExportSettings, OwnerPlanDefinitions, false, OutError))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
UVoxelStrateManager* Manager = NewObject<UVoxelStrateManager>(
|
UVoxelStrateManager* Manager = NewObject<UVoxelStrateManager>(
|
||||||
GetTransientPackage(), NAME_None, RF_Transient);
|
GetTransientPackage(), NAME_None, RF_Transient);
|
||||||
int32 Seed = AuthoredSettings->Seed;
|
int32 Seed = AuthoredSettings->Seed;
|
||||||
@@ -13636,7 +13526,7 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
|
|||||||
OriginSpineRadius = Season->GetOriginSpineRadius();
|
OriginSpineRadius = Season->GetOriginSpineRadius();
|
||||||
WorldRadiusVoxels = Season->GetWorldRadiusVoxels();
|
WorldRadiusVoxels = Season->GetWorldRadiusVoxels();
|
||||||
}
|
}
|
||||||
if (!Manager->Initialize(ExportSettings, Seed))
|
if (!Manager->Initialize(AuthoredSettings, Seed))
|
||||||
{
|
{
|
||||||
OutError = TEXT("Owner settings did not produce a valid resolved strate layout.");
|
OutError = TEXT("Owner settings did not produce a valid resolved strate layout.");
|
||||||
return false;
|
return false;
|
||||||
@@ -13644,7 +13534,7 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
|
|||||||
Seed = Manager->GetWorldSeed();
|
Seed = Manager->GetWorldSeed();
|
||||||
UVoxelGenerator* Generator = NewObject<UVoxelGenerator>(
|
UVoxelGenerator* Generator = NewObject<UVoxelGenerator>(
|
||||||
GetTransientPackage(), NAME_None, RF_Transient);
|
GetTransientPackage(), NAME_None, RF_Transient);
|
||||||
Generator->InitializeSettings(ExportSettings);
|
Generator->InitializeSettings(AuthoredSettings);
|
||||||
Generator->SetStrateManager(Manager);
|
Generator->SetStrateManager(Manager);
|
||||||
|
|
||||||
TArray<FStandaloneExportSample> Samples;
|
TArray<FStandaloneExportSample> Samples;
|
||||||
@@ -13778,25 +13668,25 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
|
|||||||
Writer->WriteValue(TEXT("source"), TEXT("VoxelForgeExplore standalone base-world export"));
|
Writer->WriteValue(TEXT("source"), TEXT("VoxelForgeExplore standalone base-world export"));
|
||||||
Writer->WriteValue(TEXT("voxel_size_cm"), StandaloneCoreVoxelSizeCm);
|
Writer->WriteValue(TEXT("voxel_size_cm"), StandaloneCoreVoxelSizeCm);
|
||||||
Writer->WriteValue(TEXT("seed"), Seed);
|
Writer->WriteValue(TEXT("seed"), Seed);
|
||||||
Writer->WriteValue(TEXT("open_surface_entry"), ExportSettings->bOpenSurfaceEntry);
|
Writer->WriteValue(TEXT("open_surface_entry"), AuthoredSettings->bOpenSurfaceEntry);
|
||||||
Writer->WriteValue(TEXT("origin_spine_radius"), OriginSpineRadius);
|
Writer->WriteValue(TEXT("origin_spine_radius"), OriginSpineRadius);
|
||||||
Writer->WriteValue(TEXT("world_radius_voxels"), WorldRadiusVoxels);
|
Writer->WriteValue(TEXT("world_radius_voxels"), WorldRadiusVoxels);
|
||||||
Writer->WriteValue(TEXT("edge_seal_thickness"), ExportSettings->EdgeSealThickness);
|
Writer->WriteValue(TEXT("edge_seal_thickness"), AuthoredSettings->EdgeSealThickness);
|
||||||
Writer->WriteValue(TEXT("total_strates"), Manager->GetNumStrates());
|
Writer->WriteValue(TEXT("total_strates"), Manager->GetNumStrates());
|
||||||
Writer->WriteValue(TEXT("inter_strate_gap_chunks"), ExportSettings->InterStrateGapChunks);
|
Writer->WriteValue(TEXT("inter_strate_gap_chunks"), AuthoredSettings->InterStrateGapChunks);
|
||||||
Writer->WriteValue(TEXT("clip_radius"), ExportSettings->ClipRadius);
|
Writer->WriteValue(TEXT("clip_radius"), AuthoredSettings->ClipRadius);
|
||||||
Writer->WriteValue(TEXT("max_clip_level"), ExportSettings->MaxClipLevel);
|
Writer->WriteValue(TEXT("max_clip_level"), AuthoredSettings->MaxClipLevel);
|
||||||
Writer->WriteValue(TEXT("full_res_clip_levels"), ExportSettings->FullResClipLevels);
|
Writer->WriteValue(TEXT("full_res_clip_levels"), AuthoredSettings->FullResClipLevels);
|
||||||
Writer->WriteValue(TEXT("coarse_tile_cells"), ExportSettings->CoarseTileCells);
|
Writer->WriteValue(TEXT("coarse_tile_cells"), AuthoredSettings->CoarseTileCells);
|
||||||
Writer->WriteValue(TEXT("render_distance_chunks"), ExportSettings->RenderDistanceChunks);
|
Writer->WriteValue(TEXT("render_distance_chunks"), AuthoredSettings->RenderDistanceChunks);
|
||||||
Writer->WriteValue(TEXT("generate_skirts"), ExportSettings->bGenerateSkirts);
|
Writer->WriteValue(TEXT("generate_skirts"), AuthoredSettings->bGenerateSkirts);
|
||||||
Writer->WriteValue(TEXT("skirt_cells"), ExportSettings->SkirtCells);
|
Writer->WriteValue(TEXT("skirt_cells"), AuthoredSettings->SkirtCells);
|
||||||
Writer->WriteValue(TEXT("lod_octave_drop"), ExportSettings->LODOctaveDrop);
|
Writer->WriteValue(TEXT("lod_octave_drop"), AuthoredSettings->LODOctaveDrop);
|
||||||
Writer->WriteValue(TEXT("strate_content_cut_min_level"), ExportSettings->GetEffectiveStrateContentCutMinLevel());
|
Writer->WriteValue(TEXT("strate_content_cut_min_level"), AuthoredSettings->GetEffectiveStrateContentCutMinLevel());
|
||||||
TSet<const UObject*> ActiveObjects;
|
TSet<const UObject*> ActiveObjects;
|
||||||
StandaloneWriteJsonValue(*Writer, TEXT("settings_asset"),
|
StandaloneWriteJsonValue(*Writer, TEXT("settings_asset"),
|
||||||
StandaloneSerializeStruct(ExportSettings->GetClass(), ExportSettings,
|
StandaloneSerializeStruct(AuthoredSettings->GetClass(), AuthoredSettings,
|
||||||
ActiveObjects, ExportSettings));
|
ActiveObjects, AuthoredSettings));
|
||||||
|
|
||||||
Writer->WriteArrayStart(TEXT("strates"));
|
Writer->WriteArrayStart(TEXT("strates"));
|
||||||
for (const FStrateSlot& Slot : Manager->GetLayout())
|
for (const FStrateSlot& Slot : Manager->GetLayout())
|
||||||
@@ -13866,68 +13756,6 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
|
|||||||
Writer->WriteObjectEnd();
|
Writer->WriteObjectEnd();
|
||||||
Writer->Close();
|
Writer->Close();
|
||||||
|
|
||||||
// Export the exact Core manifest and resolved layout that the Unreal adapter just
|
|
||||||
// generated from these live settings and slots. The standalone research tools consume
|
|
||||||
// these snapshots instead of maintaining a second copy of the planning rules.
|
|
||||||
FString RuntimeSnapshotJson;
|
|
||||||
if (!VoxelForgeCoreAdapter::SerializeResolvedWorld(
|
|
||||||
ExportSettings, Manager, 3, 8, StandaloneCoreVoxelSizeCm,
|
|
||||||
RuntimeSnapshotJson, OutError))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
FTCHARToUTF8 RuntimeSnapshotUtf8(*RuntimeSnapshotJson);
|
|
||||||
std::string CoreSnapshotError;
|
|
||||||
const std::optional<VoxelForgeCore::ResolvedWorldSnapshot> CoreSnapshot =
|
|
||||||
VoxelForgeCore::ResolvedWorldSnapshot::FromJson(
|
|
||||||
std::string_view(RuntimeSnapshotUtf8.Get(),
|
|
||||||
static_cast<std::size_t>(RuntimeSnapshotUtf8.Length())), CoreSnapshotError);
|
|
||||||
if (!CoreSnapshot || CoreSnapshot->worldAssembly() == nullptr
|
|
||||||
|| CoreSnapshot->resolvedLayout() == nullptr)
|
|
||||||
{
|
|
||||||
OutError = FString::Printf(
|
|
||||||
TEXT("Core adapter export did not produce a complete manifest and resolved layout: %s"),
|
|
||||||
UTF8_TO_TCHAR(CoreSnapshotError.c_str()));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string CoreAssemblyText = CoreSnapshot->worldAssembly()->ToJson();
|
|
||||||
const std::string CoreLayoutText = CoreSnapshot->resolvedLayout()->ToJson();
|
|
||||||
if (CoreAssemblyText.empty() || CoreLayoutText.empty())
|
|
||||||
{
|
|
||||||
OutError = TEXT("Core adapter export produced an empty canonical manifest or layout.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const FString CanonicalAssemblyJson = UTF8_TO_TCHAR(CoreAssemblyText.c_str());
|
|
||||||
const FString CanonicalLayoutJson = UTF8_TO_TCHAR(CoreLayoutText.c_str());
|
|
||||||
TSharedPtr<FJsonObject> ExportRoot;
|
|
||||||
const TSharedRef<TJsonReader<>> ExportReader = TJsonReaderFactory<>::Create(Json);
|
|
||||||
if (!FJsonSerializer::Deserialize(ExportReader, ExportRoot) || !ExportRoot.IsValid())
|
|
||||||
{
|
|
||||||
OutError = TEXT("Could not reopen the owner export to attach the Core plan snapshots.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
TSharedPtr<FJsonObject> AssemblyObject;
|
|
||||||
const TSharedRef<TJsonReader<>> AssemblyReader =
|
|
||||||
TJsonReaderFactory<>::Create(CanonicalAssemblyJson);
|
|
||||||
if (!FJsonSerializer::Deserialize(AssemblyReader, AssemblyObject) || !AssemblyObject.IsValid())
|
|
||||||
{
|
|
||||||
OutError = TEXT("Could not parse the Core adapter's canonical manifest for export.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
ExportRoot->SetObjectField(TEXT("world_assembly"), AssemblyObject.ToSharedRef());
|
|
||||||
ExportRoot->SetStringField(TEXT("core_world_assembly_json"), CanonicalAssemblyJson);
|
|
||||||
ExportRoot->SetStringField(TEXT("resolved_layout_json"), CanonicalLayoutJson);
|
|
||||||
FString ExportedJson;
|
|
||||||
const TSharedRef<TJsonWriter<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>> ExportWriter =
|
|
||||||
TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&ExportedJson);
|
|
||||||
if (!FJsonSerializer::Serialize(ExportRoot.ToSharedRef(), ExportWriter))
|
|
||||||
{
|
|
||||||
OutError = TEXT("Could not serialize the owner export with its canonical Core plan.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Json = MoveTemp(ExportedJson);
|
|
||||||
|
|
||||||
const FString Path = FPaths::Combine(Arguments.OutDirectory, TEXT("owner_world.json"));
|
const FString Path = FPaths::Combine(Arguments.OutDirectory, TEXT("owner_world.json"));
|
||||||
if (!FFileHelper::SaveStringToFile(
|
if (!FFileHelper::SaveStringToFile(
|
||||||
Json, *Path, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
|
Json, *Path, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
|
||||||
@@ -13936,16 +13764,11 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (OutJson) *OutJson = Json;
|
if (OutJson) *OutJson = Json;
|
||||||
if (!Arguments.bSkipStandaloneCoreAdapterParity && !VerifyStandaloneCoreAdapter(Json, OutError))
|
if (!VerifyStandaloneCoreAdapter(Json, OutError))
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreAdapter] %s"), *OutError);
|
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreAdapter] %s"), *OutError);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (Arguments.bSkipStandaloneCoreAdapterParity)
|
|
||||||
{
|
|
||||||
UE_LOG(LogTemp, Display,
|
|
||||||
TEXT("[VoxelForgeCoreAdapter] skipped legacy W1/W8/W16 fixture parity for this export-only update."));
|
|
||||||
}
|
|
||||||
UE_LOG(LogTemp, Display,
|
UE_LOG(LogTemp, Display,
|
||||||
TEXT("[VoxelForgeStandaloneExport] seed=%d voxel_size_cm=%.3f strates=%d passages=%d samples=%d camera_found=%d file=%s"),
|
TEXT("[VoxelForgeStandaloneExport] seed=%d voxel_size_cm=%.3f strates=%d passages=%d samples=%d camera_found=%d file=%s"),
|
||||||
Seed, StandaloneCoreVoxelSizeCm, Manager->GetNumStrates(), Manager->GetPassages().Num(),
|
Seed, StandaloneCoreVoxelSizeCm, Manager->GetNumStrates(), Manager->GetPassages().Num(),
|
||||||
@@ -14985,7 +14808,7 @@ bool CreateLoggedOwnerSurfaceSettingsCopy(
|
|||||||
UVoxelSettings* Source, int32 Seed,
|
UVoxelSettings* Source, int32 Seed,
|
||||||
TStrongObjectPtr<UVoxelSettings>& OutSettings,
|
TStrongObjectPtr<UVoxelSettings>& OutSettings,
|
||||||
TStrongObjectPtr<UVoxelStrateDefinition>& OutSurfaceDefinition,
|
TStrongObjectPtr<UVoxelStrateDefinition>& OutSurfaceDefinition,
|
||||||
FString& OutError, bool bCompactTracePlan = false)
|
FString& OutError)
|
||||||
{
|
{
|
||||||
if (Source == nullptr)
|
if (Source == nullptr)
|
||||||
{
|
{
|
||||||
@@ -15001,10 +14824,11 @@ bool CreateLoggedOwnerSurfaceSettingsCopy(
|
|||||||
nullptr, TEXT("/Game/VoxelForge/DA_Strate1.DA_Strate1"));
|
nullptr, TEXT("/Game/VoxelForge/DA_Strate1.DA_Strate1"));
|
||||||
if (SurfaceDefinition == nullptr
|
if (SurfaceDefinition == nullptr
|
||||||
|| SurfaceDefinition->GeneratorType != ECaveGeneratorType::SurfaceWorld
|
|| SurfaceDefinition->GeneratorType != ECaveGeneratorType::SurfaceWorld
|
||||||
|| SurfaceDefinition->StrateHeightInChunks <= 0)
|
|| SurfaceDefinition->StrateHeightInChunks <= 0
|
||||||
|
|| Source->TotalStrates != 1)
|
||||||
{
|
{
|
||||||
OutError = FString::Printf(
|
OutError = FString::Printf(
|
||||||
TEXT("The staged owner assets do not contain a usable DA_Strate1 SurfaceWorld (definition_loaded=%d type=%d height=%d settings_strates=%d)."),
|
TEXT("The staged owner assets do not describe the logged single SurfaceWorld (definition_loaded=%d type=%d height=%d settings_strates=%d)."),
|
||||||
SurfaceDefinition != nullptr ? 1 : 0,
|
SurfaceDefinition != nullptr ? 1 : 0,
|
||||||
SurfaceDefinition != nullptr ? static_cast<int32>(SurfaceDefinition->GeneratorType) : INDEX_NONE,
|
SurfaceDefinition != nullptr ? static_cast<int32>(SurfaceDefinition->GeneratorType) : INDEX_NONE,
|
||||||
SurfaceDefinition != nullptr ? SurfaceDefinition->StrateHeightInChunks : INDEX_NONE,
|
SurfaceDefinition != nullptr ? SurfaceDefinition->StrateHeightInChunks : INDEX_NONE,
|
||||||
@@ -15019,24 +14843,9 @@ bool CreateLoggedOwnerSurfaceSettingsCopy(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
OutSettings = TStrongObjectPtr<UVoxelSettings>(SettingsCopy);
|
OutSettings = TStrongObjectPtr<UVoxelSettings>(SettingsCopy);
|
||||||
if (bCompactTracePlan)
|
|
||||||
{
|
|
||||||
SurfaceDefinition = DuplicateObject<UVoxelStrateDefinition>(SurfaceDefinition,
|
|
||||||
GetTransientPackage(), TEXT("VoxelForgeCompactSurfaceTraceDefinition"));
|
|
||||||
if (SurfaceDefinition == nullptr)
|
|
||||||
{
|
|
||||||
OutError = TEXT("Could not make a transient DA_Strate1 copy for the compact common-plan trace.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// L14's historical fixed-length SurfaceWorld route search cannot place the
|
|
||||||
// new trace fixture even at 16 chunks. Use the same simple, walkable FlatPlain
|
|
||||||
// plan in both host revisions so the streaming comparison reaches AVoxelWorld.
|
|
||||||
SurfaceDefinition->GeneratorType = ECaveGeneratorType::FlatPlain;
|
|
||||||
SurfaceDefinition->StrateHeightInChunks = 16;
|
|
||||||
}
|
|
||||||
OutSurfaceDefinition = TStrongObjectPtr<UVoxelStrateDefinition>(SurfaceDefinition);
|
OutSurfaceDefinition = TStrongObjectPtr<UVoxelStrateDefinition>(SurfaceDefinition);
|
||||||
SettingsCopy->Seed = Seed;
|
SettingsCopy->Seed = Seed;
|
||||||
SettingsCopy->TotalStrates = 1;
|
SettingsCopy->TotalStrates = Source->TotalStrates;
|
||||||
SettingsCopy->StratePool.Reset();
|
SettingsCopy->StratePool.Reset();
|
||||||
SettingsCopy->FixedStrates.Reset();
|
SettingsCopy->FixedStrates.Reset();
|
||||||
SettingsCopy->FixedStrates.Add(0,
|
SettingsCopy->FixedStrates.Add(0,
|
||||||
@@ -15046,10 +14855,8 @@ bool CreateLoggedOwnerSurfaceSettingsCopy(
|
|||||||
SettingsCopy->bUseWorldAssemblyManifestOverride = false;
|
SettingsCopy->bUseWorldAssemblyManifestOverride = false;
|
||||||
const int32 BottomChunkZ = 1 - SurfaceDefinition->StrateHeightInChunks;
|
const int32 BottomChunkZ = 1 - SurfaceDefinition->StrateHeightInChunks;
|
||||||
UE_LOG(LogTemp, Display,
|
UE_LOG(LogTemp, Display,
|
||||||
TEXT("[VoxelForgeCoreTestLayout] source=owner_asset_transient_copy definition=DA_Strate1 generator=%d seed=%d strata=1 bounds=[0,%d] height_chunks=%d compact_trace=%d gap=%d imported_override=cleared"),
|
TEXT("[VoxelForgeCoreTestLayout] source=owner_asset_transient_copy definition=DA_Strate1 seed=%d strata=1 bounds=[0,%d] height_chunks=%d gap=%d imported_override=cleared"),
|
||||||
static_cast<int32>(SurfaceDefinition->GeneratorType), Seed, BottomChunkZ,
|
Seed, BottomChunkZ, SurfaceDefinition->StrateHeightInChunks, SettingsCopy->InterStrateGapChunks);
|
||||||
SurfaceDefinition->StrateHeightInChunks,
|
|
||||||
bCompactTracePlan ? 1 : 0, SettingsCopy->InterStrateGapChunks);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16625,15 +16432,7 @@ bool RunCoreFloatingIslandActorIntegrationTest(
|
|||||||
}
|
}
|
||||||
float StagedWalkSpeedCmPerSecond = 0.0f;
|
float StagedWalkSpeedCmPerSecond = 0.0f;
|
||||||
float StagedSprintSpeedCmPerSecond = 0.0f;
|
float StagedSprintSpeedCmPerSecond = 0.0f;
|
||||||
if (Arguments.bCoreWorldStreamTraceOnly)
|
if (!ReadStagedPlayerSpeeds(
|
||||||
{
|
|
||||||
StagedWalkSpeedCmPerSecond = 600.0f;
|
|
||||||
StagedSprintSpeedCmPerSecond = 6000.0f;
|
|
||||||
UE_LOG(LogTemp, Display,
|
|
||||||
TEXT("[VoxelForgePawnSpeedFixture] source=trace_fixture_defaults walk_cm_s=%.3f sprint_cm_s=%.3f"),
|
|
||||||
StagedWalkSpeedCmPerSecond, StagedSprintSpeedCmPerSecond);
|
|
||||||
}
|
|
||||||
else if (!ReadStagedPlayerSpeeds(
|
|
||||||
StagedWalkSpeedCmPerSecond, StagedSprintSpeedCmPerSecond, OutError))
|
StagedWalkSpeedCmPerSecond, StagedSprintSpeedCmPerSecond, OutError))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -17266,8 +17065,7 @@ bool RunCoreFloatingIslandActorIntegrationTest(
|
|||||||
{
|
{
|
||||||
if (World == nullptr || World->DesiredSorted.IsEmpty()) return false;
|
if (World == nullptr || World->DesiredSorted.IsEmpty()) return false;
|
||||||
for (const FVoxelTileKey& Key : World->DesiredSorted)
|
for (const FVoxelTileKey& Key : World->DesiredSorted)
|
||||||
if (!World->LoadedTiles.Contains(Key)
|
if (!World->LoadedTiles.Contains(Key)) return false;
|
||||||
&& !World->CollisionNotRequiredTiles.Contains(Key)) return false;
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
const auto CollisionSetSettled = [](const AVoxelWorld* World)
|
const auto CollisionSetSettled = [](const AVoxelWorld* World)
|
||||||
@@ -17856,15 +17654,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
|
|
||||||
float StagedWalkSpeedCmPerSecond = 0.0f;
|
float StagedWalkSpeedCmPerSecond = 0.0f;
|
||||||
float StagedSprintSpeedCmPerSecond = 0.0f;
|
float StagedSprintSpeedCmPerSecond = 0.0f;
|
||||||
if (Arguments.bCoreWorldStreamTraceOnly)
|
if (!ReadStagedPlayerSpeeds(
|
||||||
{
|
|
||||||
StagedWalkSpeedCmPerSecond = 600.0f;
|
|
||||||
StagedSprintSpeedCmPerSecond = 6000.0f;
|
|
||||||
UE_LOG(LogTemp, Display,
|
|
||||||
TEXT("[VoxelForgePawnSpeedFixture] source=trace_fixture_defaults walk_cm_s=%.3f sprint_cm_s=%.3f"),
|
|
||||||
StagedWalkSpeedCmPerSecond, StagedSprintSpeedCmPerSecond);
|
|
||||||
}
|
|
||||||
else if (!ReadStagedPlayerSpeeds(
|
|
||||||
StagedWalkSpeedCmPerSecond, StagedSprintSpeedCmPerSecond, OutError))
|
StagedWalkSpeedCmPerSecond, StagedSprintSpeedCmPerSecond, OutError))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -17873,26 +17663,8 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
const int32 OwnerSeed = ResolveCoreTestWorldSeed(AuthoredSettings);
|
const int32 OwnerSeed = ResolveCoreTestWorldSeed(AuthoredSettings);
|
||||||
TStrongObjectPtr<UVoxelSettings> OwnerSurfaceSettings;
|
TStrongObjectPtr<UVoxelSettings> OwnerSurfaceSettings;
|
||||||
TStrongObjectPtr<UVoxelStrateDefinition> OwnerSurfaceDefinition;
|
TStrongObjectPtr<UVoxelStrateDefinition> OwnerSurfaceDefinition;
|
||||||
TArray<TStrongObjectPtr<UVoxelStrateDefinition>> OwnerPlanDefinitions;
|
if (!CreateLoggedOwnerSurfaceSettingsCopy(AuthoredSettings, OwnerSeed,
|
||||||
if (Arguments.bOwnerFourStratePlan)
|
OwnerSurfaceSettings, OwnerSurfaceDefinition, OutError))
|
||||||
{
|
|
||||||
UVoxelSettings* PlanSettings = DuplicateObject<UVoxelSettings>(
|
|
||||||
AuthoredSettings, GetTransientPackage(), TEXT("VoxelForgeOwnerFourStrateTraceSettings"));
|
|
||||||
if (PlanSettings == nullptr)
|
|
||||||
{
|
|
||||||
OutError = TEXT("Could not duplicate owner settings for the transient four-strate Core trace.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
PlanSettings->Seed = OwnerSeed;
|
|
||||||
if (!ApplyOwnerFourStratePlan(PlanSettings, OwnerPlanDefinitions, true, OutError))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
OwnerSurfaceSettings = TStrongObjectPtr<UVoxelSettings>(PlanSettings);
|
|
||||||
}
|
|
||||||
else if (!CreateLoggedOwnerSurfaceSettingsCopy(AuthoredSettings, OwnerSeed,
|
|
||||||
OwnerSurfaceSettings, OwnerSurfaceDefinition, OutError,
|
|
||||||
Arguments.bCoreWorldStreamTraceOnly))
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -18050,25 +17822,6 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
OutError = TEXT("Could not create the BuildHost stream-trace pawn.");
|
OutError = TEXT("Could not create the BuildHost stream-trace pawn.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
FVector StartupTraceSpawnCm = FVector::ZeroVector;
|
|
||||||
FString StartupTraceSpawnText;
|
|
||||||
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StartupTraceSpawnCm="),
|
|
||||||
StartupTraceSpawnText, false))
|
|
||||||
{
|
|
||||||
TArray<FString> Components;
|
|
||||||
StartupTraceSpawnText.ParseIntoArray(Components, TEXT(","), true);
|
|
||||||
if (Components.Num() != 3
|
|
||||||
|| !LexTryParseString(StartupTraceSpawnCm.X, *Components[0])
|
|
||||||
|| !LexTryParseString(StartupTraceSpawnCm.Y, *Components[1])
|
|
||||||
|| !LexTryParseString(StartupTraceSpawnCm.Z, *Components[2])
|
|
||||||
|| !FMath::IsFinite(StartupTraceSpawnCm.X)
|
|
||||||
|| !FMath::IsFinite(StartupTraceSpawnCm.Y)
|
|
||||||
|| !FMath::IsFinite(StartupTraceSpawnCm.Z))
|
|
||||||
{
|
|
||||||
OutError = TEXT("voxel.StartupTraceSpawnCm must be finite X,Y,Z centimetres.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
USceneComponent* PawnRoot = NewObject<USceneComponent>(PlayerPawn, NAME_None, RF_Transient);
|
USceneComponent* PawnRoot = NewObject<USceneComponent>(PlayerPawn, NAME_None, RF_Transient);
|
||||||
if (PawnRoot == nullptr)
|
if (PawnRoot == nullptr)
|
||||||
{
|
{
|
||||||
@@ -18077,7 +17830,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
}
|
}
|
||||||
PlayerPawn->SetRootComponent(PawnRoot);
|
PlayerPawn->SetRootComponent(PawnRoot);
|
||||||
PawnRoot->RegisterComponent();
|
PawnRoot->RegisterComponent();
|
||||||
PlayerPawn->SetActorLocation(StartupTraceSpawnCm, false, nullptr, ETeleportType::TeleportPhysics);
|
PlayerPawn->SetActorLocation(FVector::ZeroVector, false, nullptr, ETeleportType::TeleportPhysics);
|
||||||
PlayerController->Possess(PlayerPawn);
|
PlayerController->Possess(PlayerPawn);
|
||||||
if (TestWorld->GetFirstPlayerController() != PlayerController
|
if (TestWorld->GetFirstPlayerController() != PlayerController
|
||||||
|| PlayerController->GetPawn() != PlayerPawn)
|
|| PlayerController->GetPawn() != PlayerPawn)
|
||||||
@@ -18106,10 +17859,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
: EVoxelForgeCoreMesherRecipe::MarchingCubesV1);
|
: EVoxelForgeCoreMesherRecipe::MarchingCubesV1);
|
||||||
Actor->CoreClipRadius = 3;
|
Actor->CoreClipRadius = 3;
|
||||||
Actor->CoreMaxClipLevel = 8;
|
Actor->CoreMaxClipLevel = 8;
|
||||||
// A timing trace must use the production tile path. Core density hashing disables the
|
Actor->bCaptureVoxelForgeCoreHashesForValidation = true;
|
||||||
// native heightfield band shortcut and measures a different workload; hash validation
|
|
||||||
// remains enabled in the dedicated parity tests below.
|
|
||||||
Actor->bCaptureVoxelForgeCoreHashesForValidation = false;
|
|
||||||
Actor->FinishSpawning(ActorTransform);
|
Actor->FinishSpawning(ActorTransform);
|
||||||
TestWorld->InitializeActorsForPlay(FURL());
|
TestWorld->InitializeActorsForPlay(FURL());
|
||||||
TestWorld->BeginPlay();
|
TestWorld->BeginPlay();
|
||||||
@@ -18119,19 +17869,13 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Actor->SetActorTickEnabled(true);
|
Actor->SetActorTickEnabled(true);
|
||||||
VoxelForgeStartupTrace::RecordEvent(TEXT("trace_spawn_location"), FString::Printf(
|
|
||||||
TEXT("\"location_cm\":[%s,%s,%s]"),
|
|
||||||
*FString::SanitizeFloat(StartupTraceSpawnCm.X),
|
|
||||||
*FString::SanitizeFloat(StartupTraceSpawnCm.Y),
|
|
||||||
*FString::SanitizeFloat(StartupTraceSpawnCm.Z)));
|
|
||||||
|
|
||||||
auto DesiredTilesLoaded = [](const AVoxelWorld* World)
|
auto DesiredTilesLoaded = [](const AVoxelWorld* World)
|
||||||
{
|
{
|
||||||
if (World == nullptr || World->DesiredSorted.IsEmpty()) return false;
|
if (World == nullptr || World->DesiredSorted.IsEmpty()) return false;
|
||||||
for (const FVoxelTileKey& Key : World->DesiredSorted)
|
for (const FVoxelTileKey& Key : World->DesiredSorted)
|
||||||
{
|
{
|
||||||
if (!World->LoadedTiles.Contains(Key)
|
if (!World->LoadedTiles.Contains(Key)) return false;
|
||||||
&& !World->CollisionNotRequiredTiles.Contains(Key)) return false;
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
@@ -18172,60 +17916,11 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
TArray<double> SpawnTickMilliseconds;
|
TArray<double> SpawnTickMilliseconds;
|
||||||
TArray<double> WalkTickMilliseconds;
|
TArray<double> WalkTickMilliseconds;
|
||||||
TArray<TPair<double, double>> TickSamples;
|
TArray<TPair<double, double>> TickSamples;
|
||||||
double WorldTickStartedSeconds = 0.0;
|
|
||||||
double WorldActorPhaseStartedSeconds = 0.0;
|
|
||||||
double WorldPostActorStartedSeconds = 0.0;
|
|
||||||
double WorldPreActorMilliseconds = 0.0;
|
|
||||||
double WorldActorPhaseMilliseconds = 0.0;
|
|
||||||
double WorldPostActorMilliseconds = 0.0;
|
|
||||||
const FDelegateHandle WorldTickStartHandle = FWorldDelegates::OnWorldTickStart.AddLambda(
|
|
||||||
[&](UWorld* World, ELevelTick, float)
|
|
||||||
{
|
|
||||||
if (World == TestWorld)
|
|
||||||
{
|
|
||||||
WorldTickStartedSeconds = FPlatformTime::Seconds();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const FDelegateHandle WorldPreActorTickHandle = FWorldDelegates::OnWorldPreActorTick.AddLambda(
|
|
||||||
[&](UWorld* World, ELevelTick, float)
|
|
||||||
{
|
|
||||||
if (World == TestWorld)
|
|
||||||
{
|
|
||||||
const double NowSeconds = FPlatformTime::Seconds();
|
|
||||||
WorldPreActorMilliseconds =
|
|
||||||
(NowSeconds - WorldTickStartedSeconds) * 1000.0;
|
|
||||||
WorldActorPhaseStartedSeconds = NowSeconds;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const FDelegateHandle WorldPostActorTickHandle = FWorldDelegates::OnWorldPostActorTick.AddLambda(
|
|
||||||
[&](UWorld* World, ELevelTick, float)
|
|
||||||
{
|
|
||||||
if (World == TestWorld)
|
|
||||||
{
|
|
||||||
const double NowSeconds = FPlatformTime::Seconds();
|
|
||||||
WorldActorPhaseMilliseconds =
|
|
||||||
(NowSeconds - WorldActorPhaseStartedSeconds) * 1000.0;
|
|
||||||
WorldPostActorStartedSeconds = NowSeconds;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const FDelegateHandle WorldTickEndHandle = FWorldDelegates::OnWorldTickEnd.AddLambda(
|
|
||||||
[&](UWorld* World, ELevelTick, float)
|
|
||||||
{
|
|
||||||
if (World == TestWorld)
|
|
||||||
{
|
|
||||||
WorldPostActorMilliseconds =
|
|
||||||
(FPlatformTime::Seconds() - WorldPostActorStartedSeconds) * 1000.0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
bool bInitialSetReady = false;
|
bool bInitialSetReady = false;
|
||||||
double InitialSetReadySeconds = -1.0;
|
double InitialSetReadySeconds = -1.0;
|
||||||
while (FPlatformTime::Seconds() - ActorBeginSeconds < RunDurationSeconds)
|
while (FPlatformTime::Seconds() - ActorBeginSeconds < RunDurationSeconds)
|
||||||
{
|
{
|
||||||
const double TickStartSeconds = FPlatformTime::Seconds();
|
const double TickStartSeconds = FPlatformTime::Seconds();
|
||||||
const uint64 TickFrame = GFrameCounter;
|
|
||||||
WorldPreActorMilliseconds = 0.0;
|
|
||||||
WorldActorPhaseMilliseconds = 0.0;
|
|
||||||
WorldPostActorMilliseconds = 0.0;
|
|
||||||
TestWorld->Tick(LEVELTICK_All, 0.016f);
|
TestWorld->Tick(LEVELTICK_All, 0.016f);
|
||||||
const double TickMilliseconds =
|
const double TickMilliseconds =
|
||||||
(FPlatformTime::Seconds() - TickStartSeconds) * 1000.0;
|
(FPlatformTime::Seconds() - TickStartSeconds) * 1000.0;
|
||||||
@@ -18233,7 +17928,6 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
const double ElapsedSeconds = FPlatformTime::Seconds() - ActorBeginSeconds;
|
const double ElapsedSeconds = FPlatformTime::Seconds() - ActorBeginSeconds;
|
||||||
TickSamples.Emplace(ElapsedSeconds, TickMilliseconds);
|
TickSamples.Emplace(ElapsedSeconds, TickMilliseconds);
|
||||||
|
|
||||||
const double ReadinessCheckStartSeconds = FPlatformTime::Seconds();
|
|
||||||
if (!bInitialSetReady && Actor->IsVoxelForgeCoreReady()
|
if (!bInitialSetReady && Actor->IsVoxelForgeCoreReady()
|
||||||
&& Actor->bAllChunksLoaded && Actor->PendingTiles.IsEmpty()
|
&& Actor->bAllChunksLoaded && Actor->PendingTiles.IsEmpty()
|
||||||
&& Actor->PendingCollisionCooks.IsEmpty() && Actor->PendingUnload.IsEmpty()
|
&& Actor->PendingCollisionCooks.IsEmpty() && Actor->PendingUnload.IsEmpty()
|
||||||
@@ -18248,30 +17942,8 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
Actor->LoadedTiles.Num(), Actor->CollisionDesiredTiles.Num(),
|
Actor->LoadedTiles.Num(), Actor->CollisionDesiredTiles.Num(),
|
||||||
Actor->CollisionReadyTiles.Num()));
|
Actor->CollisionReadyTiles.Num()));
|
||||||
}
|
}
|
||||||
const double ReadinessCheckMilliseconds =
|
|
||||||
(FPlatformTime::Seconds() - ReadinessCheckStartSeconds) * 1000.0;
|
|
||||||
const double SleepStartSeconds = FPlatformTime::Seconds();
|
|
||||||
FPlatformProcess::Sleep(0.016f);
|
FPlatformProcess::Sleep(0.016f);
|
||||||
const double SleepMilliseconds =
|
|
||||||
(FPlatformTime::Seconds() - SleepStartSeconds) * 1000.0;
|
|
||||||
VoxelForgeStartupTrace::RecordEvent(TEXT("commandlet_world_tick_frame"), FString::Printf(
|
|
||||||
TEXT("\"frame\":%llu,\"world_tick_ms\":%s,"
|
|
||||||
"\"world_pre_actor_ms\":%s,\"world_actor_phase_ms\":%s,"
|
|
||||||
"\"world_post_actor_ms\":%s,"
|
|
||||||
"\"readiness_check_ms\":%s,\"sleep_ms\":%s,\"loop_ms\":%s"),
|
|
||||||
static_cast<unsigned long long>(TickFrame),
|
|
||||||
*FString::SanitizeFloat(TickMilliseconds),
|
|
||||||
*FString::SanitizeFloat(WorldPreActorMilliseconds),
|
|
||||||
*FString::SanitizeFloat(WorldActorPhaseMilliseconds),
|
|
||||||
*FString::SanitizeFloat(WorldPostActorMilliseconds),
|
|
||||||
*FString::SanitizeFloat(ReadinessCheckMilliseconds),
|
|
||||||
*FString::SanitizeFloat(SleepMilliseconds),
|
|
||||||
*FString::SanitizeFloat((FPlatformTime::Seconds() - TickStartSeconds) * 1000.0)));
|
|
||||||
}
|
}
|
||||||
FWorldDelegates::OnWorldTickStart.Remove(WorldTickStartHandle);
|
|
||||||
FWorldDelegates::OnWorldPreActorTick.Remove(WorldPreActorTickHandle);
|
|
||||||
FWorldDelegates::OnWorldPostActorTick.Remove(WorldPostActorTickHandle);
|
|
||||||
FWorldDelegates::OnWorldTickEnd.Remove(WorldTickEndHandle);
|
|
||||||
|
|
||||||
for (const TPair<double, double>& Sample : TickSamples)
|
for (const TPair<double, double>& Sample : TickSamples)
|
||||||
{
|
{
|
||||||
@@ -18351,7 +18023,6 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
const double CarveBrushRadiusCoreVoxels =
|
const double CarveBrushRadiusCoreVoxels =
|
||||||
CarveBrushRadiusLegacyVoxels * static_cast<double>(VOXEL_SIZE) / CoreVoxelSizeCm;
|
CarveBrushRadiusLegacyVoxels * static_cast<double>(VOXEL_SIZE) / CoreVoxelSizeCm;
|
||||||
const int32 CollisionRadius = FMath::Clamp(AuthoredSettings->PawnCollisionRadiusChunks, 1, 4);
|
const int32 CollisionRadius = FMath::Clamp(AuthoredSettings->PawnCollisionRadiusChunks, 1, 4);
|
||||||
FIntVector ColdStartCollisionCenter = FIntVector::ZeroValue;
|
|
||||||
uint64 ExpectedEditDigest = 0;
|
uint64 ExpectedEditDigest = 0;
|
||||||
std::array<std::uint64_t, 4> InitialAggregates{};
|
std::array<std::uint64_t, 4> InitialAggregates{};
|
||||||
std::array<std::uint64_t, 4> TravelAggregates{};
|
std::array<std::uint64_t, 4> TravelAggregates{};
|
||||||
@@ -18537,17 +18208,10 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
BiomeProbeSpanVoxels);
|
BiomeProbeSpanVoxels);
|
||||||
|
|
||||||
VoxelForgeCore::World ReferenceWorld(std::move(*Snapshot));
|
VoxelForgeCore::World ReferenceWorld(std::move(*Snapshot));
|
||||||
// Match the actor's cold-start fallback camera when the transient world has no pawn.
|
const std::vector<VoxelForgeCore::TileKey> Desired = ReferenceWorld.desiredTiles({});
|
||||||
// Aligner la reference sur la camera de repli de l'acteur sans pawn au demarrage.
|
|
||||||
const VoxelForgeCore::WorldConfiguration& ReferenceConfiguration =
|
|
||||||
ReferenceWorld.configuration();
|
|
||||||
const VoxelForgeCore::Vec3d InitialCameraVoxels = ReferenceConfiguration.viewerStartFound
|
|
||||||
? ReferenceConfiguration.viewerStartVoxels : VoxelForgeCore::Vec3d{};
|
|
||||||
const std::vector<VoxelForgeCore::TileKey> Desired =
|
|
||||||
ReferenceWorld.desiredTiles(InitialCameraVoxels);
|
|
||||||
if (Desired.empty())
|
if (Desired.empty())
|
||||||
{
|
{
|
||||||
OutError = TEXT("Standalone Core returned an empty reference clipmap at the cold-start camera.");
|
OutError = TEXT("Standalone Core returned an empty reference clipmap at the origin.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ExpectedInitialTileCount = Desired.size();
|
ExpectedInitialTileCount = Desired.size();
|
||||||
@@ -18594,23 +18258,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
UTF8_TO_TCHAR(CoreError.c_str()));
|
UTF8_TO_TCHAR(CoreError.c_str()));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Match the runtime collision probe around the same cold-start viewer tile.
|
ReferenceWorld.retainTileRegions(Desired);
|
||||||
// Reproduire la sonde de collision autour de la meme tuile de depart a froid.
|
|
||||||
ColdStartCollisionCenter = FIntVector(
|
|
||||||
FMath::FloorToInt(InitialCameraVoxels.x / 32.0),
|
|
||||||
FMath::FloorToInt(InitialCameraVoxels.y / 32.0),
|
|
||||||
FMath::FloorToInt(InitialCameraVoxels.z / 32.0));
|
|
||||||
std::vector<VoxelForgeCore::TileKey> ColdStartCollisionTiles;
|
|
||||||
ColdStartCollisionTiles.reserve(static_cast<std::size_t>(2 * CollisionRadius + 1)
|
|
||||||
* static_cast<std::size_t>(2 * CollisionRadius + 1)
|
|
||||||
* static_cast<std::size_t>(2 * CollisionRadius + 1));
|
|
||||||
for (int32 Z = -CollisionRadius; Z <= CollisionRadius; ++Z)
|
|
||||||
for (int32 Y = -CollisionRadius; Y <= CollisionRadius; ++Y)
|
|
||||||
for (int32 X = -CollisionRadius; X <= CollisionRadius; ++X)
|
|
||||||
ColdStartCollisionTiles.push_back({
|
|
||||||
{ColdStartCollisionCenter.X + X,
|
|
||||||
ColdStartCollisionCenter.Y + Y,
|
|
||||||
ColdStartCollisionCenter.Z + Z}, 0});
|
|
||||||
|
|
||||||
if (Arguments.bCoreHybridCliffOnly)
|
if (Arguments.bCoreHybridCliffOnly)
|
||||||
{
|
{
|
||||||
@@ -18684,47 +18332,6 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
Options.mesherRecipe = VoxelForgeCore::MesherRecipe::worldAssemblerHybridV1;
|
Options.mesherRecipe = VoxelForgeCore::MesherRecipe::worldAssemblerHybridV1;
|
||||||
else
|
else
|
||||||
Options.mesherRecipe = VoxelForgeCore::MesherRecipe::marchingCubesV1;
|
Options.mesherRecipe = VoxelForgeCore::MesherRecipe::marchingCubesV1;
|
||||||
|
|
||||||
// Keep collision hashes outside ExpectedInitialDesiredSet; the collision cube is an
|
|
||||||
// additional runtime request even where it overlaps the visual clipmap.
|
|
||||||
// Garder les hachages de collision hors de l'ensemble visuel initial, meme s'ils se
|
|
||||||
// recoupent avec le clipmap.
|
|
||||||
std::vector<VoxelForgeCore::TileKey> RetainedReferenceTiles = Desired;
|
|
||||||
RetainedReferenceTiles.insert(RetainedReferenceTiles.end(),
|
|
||||||
ColdStartCollisionTiles.begin(), ColdStartCollisionTiles.end());
|
|
||||||
ReferenceWorld.retainTileRegions(RetainedReferenceTiles);
|
|
||||||
if (!ReferenceWorld.compileTileRegion(ColdStartCollisionTiles, 16, Scheduler, CoreError))
|
|
||||||
{
|
|
||||||
OutError = FString::Printf(TEXT("Standalone collision reference compile failed: %s"),
|
|
||||||
UTF8_TO_TCHAR(CoreError.c_str()));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
std::size_t CollisionBodyTiles = 0;
|
|
||||||
for (const VoxelForgeCore::TileKey& Key : ColdStartCollisionTiles)
|
|
||||||
{
|
|
||||||
const std::optional<VoxelForgeCore::TileMesh> Mesh =
|
|
||||||
ReferenceWorld.generateTile(Key, Options, CoreError);
|
|
||||||
if (!Mesh)
|
|
||||||
{
|
|
||||||
OutError = FString::Printf(TEXT("Standalone collision tile generation failed: %s"),
|
|
||||||
UTF8_TO_TCHAR(CoreError.c_str()));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
FVoxelForgeCoreTileHashRecord Record;
|
|
||||||
Record.bEmpty = Mesh->indices.empty();
|
|
||||||
Record.bHasHashes = Mesh->hasDensityHash && Mesh->hasMeshHash
|
|
||||||
&& Mesh->hasPayloadHash && Mesh->hasTopologyHash;
|
|
||||||
if (!Record.bEmpty) ++CollisionBodyTiles;
|
|
||||||
ExpectedHashes.Add(FVoxelTileKey(
|
|
||||||
FIntVector(Key.coordinate.x, Key.coordinate.y, Key.coordinate.z), Key.lod), Record);
|
|
||||||
}
|
|
||||||
UE_LOG(LogTemp, Display,
|
|
||||||
TEXT("[VoxelForgeCoreWorldTest] standalone_collision_reference=1 center=(%d,%d,%d) tiles=%d body=%llu no_body=%llu"),
|
|
||||||
ColdStartCollisionCenter.X, ColdStartCollisionCenter.Y, ColdStartCollisionCenter.Z,
|
|
||||||
static_cast<int32>(ColdStartCollisionTiles.size()),
|
|
||||||
static_cast<unsigned long long>(CollisionBodyTiles),
|
|
||||||
static_cast<unsigned long long>(ColdStartCollisionTiles.size() - CollisionBodyTiles));
|
|
||||||
|
|
||||||
std::vector<FStandaloneCoreAdapterTileHashes> AggregateRows;
|
std::vector<FStandaloneCoreAdapterTileHashes> AggregateRows;
|
||||||
AggregateRows.reserve(Desired.size());
|
AggregateRows.reserve(Desired.size());
|
||||||
bool bFoundCarveVertex = false;
|
bool bFoundCarveVertex = false;
|
||||||
@@ -19657,8 +19264,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
if (World == nullptr || World->DesiredSorted.IsEmpty()) return false;
|
if (World == nullptr || World->DesiredSorted.IsEmpty()) return false;
|
||||||
for (const FVoxelTileKey& Key : World->DesiredSorted)
|
for (const FVoxelTileKey& Key : World->DesiredSorted)
|
||||||
{
|
{
|
||||||
if (!World->LoadedTiles.Contains(Key)
|
if (!World->LoadedTiles.Contains(Key)) return false;
|
||||||
&& !World->CollisionNotRequiredTiles.Contains(Key)) return false;
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
@@ -19761,7 +19367,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
for (int32 Y = -CollisionRadius; Y <= CollisionRadius; ++Y)
|
for (int32 Y = -CollisionRadius; Y <= CollisionRadius; ++Y)
|
||||||
for (int32 X = -CollisionRadius; X <= CollisionRadius; ++X)
|
for (int32 X = -CollisionRadius; X <= CollisionRadius; ++X)
|
||||||
{
|
{
|
||||||
const FVoxelTileKey Key(ColdStartCollisionCenter + FIntVector(X, Y, Z), 0);
|
const FVoxelTileKey Key(FIntVector(X, Y, Z), 0);
|
||||||
ExpectedCollisionSet.Add(Key);
|
ExpectedCollisionSet.Add(Key);
|
||||||
const FVoxelForgeCoreTileHashRecord* Record = ExpectedHashes.Find(Key);
|
const FVoxelForgeCoreTileHashRecord* Record = ExpectedHashes.Find(Key);
|
||||||
if (Record == nullptr)
|
if (Record == nullptr)
|
||||||
@@ -21755,44 +21361,6 @@ int32 RunBatch(const FString& Params)
|
|||||||
|
|
||||||
int32 UVoxelForgeExploreCommandlet::Main(const FString& Params)
|
int32 UVoxelForgeExploreCommandlet::Main(const FString& Params)
|
||||||
{
|
{
|
||||||
if (FParse::Param(*Params, TEXT("coreassetupgradeaudit")))
|
|
||||||
{
|
|
||||||
// Load the untouched BuildHost copies and emit their in-memory conversions; never save a package.
|
|
||||||
// Charge les copies BuildHost intactes et affiche les conversions en memoire, sans sauver de package.
|
|
||||||
UVoxelSettings* SettingsAsset = LoadObject<UVoxelSettings>(nullptr,
|
|
||||||
TEXT("/Game/VoxelForge/DA_Settings.DA_Settings"));
|
|
||||||
if (SettingsAsset == nullptr)
|
|
||||||
{
|
|
||||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeAssetMigrationAudit] missing DA_Settings copy"));
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
UE_LOG(LogTemp, Display,
|
|
||||||
TEXT("[VoxelForgeAssetMigrationAudit] asset=%s old_gap_chunks=%d new_gap_m=%.6f layout_chunk_m=%.6f"),
|
|
||||||
*SettingsAsset->GetPathName(), SettingsAsset->InterStrateGapChunks,
|
|
||||||
SettingsAsset->InterStrateGapMeters, SettingsAsset->StrateLayoutChunkSizeMeters);
|
|
||||||
|
|
||||||
const TCHAR* StratePaths[] = {
|
|
||||||
TEXT("/Game/VoxelForge/DA_Strate1.DA_Strate1"),
|
|
||||||
TEXT("/Game/VoxelForge/DA_Strate2.DA_Strate2"),
|
|
||||||
TEXT("/Game/VoxelForge/DA_Strate3.DA_Strate3"),
|
|
||||||
TEXT("/Game/VoxelForge/DA_Strate4.DA_Strate4")
|
|
||||||
};
|
|
||||||
for (const TCHAR* Path : StratePaths)
|
|
||||||
{
|
|
||||||
UVoxelStrateDefinition* Definition = LoadObject<UVoxelStrateDefinition>(nullptr, Path);
|
|
||||||
if (Definition == nullptr)
|
|
||||||
{
|
|
||||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeAssetMigrationAudit] missing strate copy %s"), Path);
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
UE_LOG(LogTemp, Display,
|
|
||||||
TEXT("[VoxelForgeAssetMigrationAudit] asset=%s old_height_chunks=%d new_height_m=%.6f old_transition_chunks=%d new_transition_m=%.6f"),
|
|
||||||
*Definition->GetPathName(), Definition->StrateHeightInChunks,
|
|
||||||
Definition->StrateHeightMeters, Definition->TransitionBlendChunks,
|
|
||||||
Definition->TransitionWidthMeters);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (FParse::Param(*Params, TEXT("step2f")))
|
if (FParse::Param(*Params, TEXT("step2f")))
|
||||||
{
|
{
|
||||||
return RunStep2f(Params);
|
return RunStep2f(Params);
|
||||||
|
|||||||
@@ -1,303 +0,0 @@
|
|||||||
#include "VoxelForgeLivingPlaneActor.h"
|
|
||||||
|
|
||||||
#include "Engine/TextureRenderTarget2D.h"
|
|
||||||
#include "Engine/TextureRenderTarget.h"
|
|
||||||
#include "Engine/StaticMesh.h"
|
|
||||||
#include "Engine/World.h"
|
|
||||||
#include "Materials/MaterialInstanceDynamic.h"
|
|
||||||
#include "Materials/MaterialInterface.h"
|
|
||||||
#include "RenderGraphBuilder.h"
|
|
||||||
#include "RenderGraphUtils.h"
|
|
||||||
#include "RHICommandList.h"
|
|
||||||
#include "RHIResources.h"
|
|
||||||
#include "DataDrivenShaderPlatformInfo.h"
|
|
||||||
#include "GlobalShader.h"
|
|
||||||
#include "ShaderParameterStruct.h"
|
|
||||||
#include "UObject/ConstructorHelpers.h"
|
|
||||||
#include "Components/StaticMeshComponent.h"
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
constexpr int32 LivingPlaneThreadGroupSize = 8;
|
|
||||||
|
|
||||||
struct FLivingPlaneStepInput
|
|
||||||
{
|
|
||||||
FVector4f ObjectPositionRadius[2] = {FVector4f(0, 0, 0, -1), FVector4f(0, 0, 0, -1)};
|
|
||||||
FVector4f ObjectVelocityPush[2] = {FVector4f(0, 0, 0, 0), FVector4f(0, 0, 0, 0)};
|
|
||||||
float TimeSeconds = 0.0f;
|
|
||||||
};
|
|
||||||
|
|
||||||
class FVFLivingPlaneStepCS final : public FGlobalShader
|
|
||||||
{
|
|
||||||
DECLARE_GLOBAL_SHADER(FVFLivingPlaneStepCS);
|
|
||||||
SHADER_USE_PARAMETER_STRUCT(FVFLivingPlaneStepCS, FGlobalShader);
|
|
||||||
|
|
||||||
BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
|
|
||||||
SHADER_PARAMETER(FIntPoint, GridSize)
|
|
||||||
SHADER_PARAMETER(float, Dt)
|
|
||||||
SHADER_PARAMETER(float, Dx)
|
|
||||||
SHADER_PARAMETER(float, PlaneLength)
|
|
||||||
SHADER_PARAMETER(float, WaveSpeed)
|
|
||||||
SHADER_PARAMETER(float, Damping)
|
|
||||||
SHADER_PARAMETER(float, FoundationStiffness)
|
|
||||||
SHADER_PARAMETER(float, TimeSeconds)
|
|
||||||
SHADER_PARAMETER(FVector2f, BreathingSettings)
|
|
||||||
SHADER_PARAMETER(FVector4f, ImprintSettings)
|
|
||||||
SHADER_PARAMETER_ARRAY(FVector4f, ObjectPositionRadius, [2])
|
|
||||||
SHADER_PARAMETER_ARRAY(FVector4f, ObjectVelocityPush, [2])
|
|
||||||
SHADER_PARAMETER_RDG_TEXTURE_SRV(Texture2D<float4>, SourceState)
|
|
||||||
SHADER_PARAMETER_RDG_TEXTURE_UAV(RWTexture2D<float4>, TargetState)
|
|
||||||
END_SHADER_PARAMETER_STRUCT()
|
|
||||||
|
|
||||||
public:
|
|
||||||
static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
|
|
||||||
{
|
|
||||||
return IsFeatureLevelSupported(Parameters.Platform, ERHIFeatureLevel::SM5);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
IMPLEMENT_GLOBAL_SHADER(FVFLivingPlaneStepCS,
|
|
||||||
"/Plugin/VoxelForgeLivingPlane/LivingPlane.usf", "MainCS", SF_Compute)
|
|
||||||
|
|
||||||
UTextureRenderTarget2D* CreateFieldTarget(UObject* Outer, int32 Size, FName Name)
|
|
||||||
{
|
|
||||||
UTextureRenderTarget2D* Target = NewObject<UTextureRenderTarget2D>(Outer, Name, RF_Transient);
|
|
||||||
Target->RenderTargetFormat = RTF_RGBA32f;
|
|
||||||
Target->bCanCreateUAV = true;
|
|
||||||
Target->bAutoGenerateMips = false;
|
|
||||||
Target->ClearColor = FLinearColor::Transparent;
|
|
||||||
Target->Filter = TF_Bilinear;
|
|
||||||
Target->AddressX = TA_Clamp;
|
|
||||||
Target->AddressY = TA_Clamp;
|
|
||||||
Target->InitAutoFormat(Size, Size);
|
|
||||||
Target->UpdateResourceImmediate(true);
|
|
||||||
return Target;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AVoxelForgeLivingPlaneActor::AVoxelForgeLivingPlaneActor()
|
|
||||||
{
|
|
||||||
PrimaryActorTick.bCanEverTick = true;
|
|
||||||
PlaneMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("LivingPlaneMesh"));
|
|
||||||
SetRootComponent(PlaneMesh);
|
|
||||||
PlaneMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
|
||||||
PlaneMesh->SetMobility(EComponentMobility::Movable);
|
|
||||||
static ConstructorHelpers::FObjectFinder<UStaticMesh> PlaneAsset(
|
|
||||||
TEXT("/Engine/BasicShapes/Plane.Plane"));
|
|
||||||
if (PlaneAsset.Succeeded())
|
|
||||||
{
|
|
||||||
PlaneMesh->SetStaticMesh(PlaneAsset.Object);
|
|
||||||
}
|
|
||||||
|
|
||||||
ObjectInputs.SetNum(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
void AVoxelForgeLivingPlaneActor::OnConstruction(const FTransform& Transform)
|
|
||||||
{
|
|
||||||
Super::OnConstruction(Transform);
|
|
||||||
if (PlaneMesh)
|
|
||||||
{
|
|
||||||
// /Engine/BasicShapes/Plane is 100 cm square; one Unreal scale unit
|
|
||||||
// therefore corresponds to one metre of authored plane length.
|
|
||||||
PlaneMesh->SetRelativeScale3D(FVector(PlaneLengthMeters, PlaneLengthMeters, 1.0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
float AVoxelForgeLivingPlaneActor::GetStableStepSeconds() const
|
|
||||||
{
|
|
||||||
const float Dx = PlaneLengthMeters / static_cast<float>(FMath::Max(GridSize, 16));
|
|
||||||
const float EigenFrequencySquared = 8.0f * WaveSpeedMetersPerSecond * WaveSpeedMetersPerSecond
|
|
||||||
/ FMath::Max(Dx * Dx, 1.0e-8f) + FoundationStiffnessPerSecondSquared;
|
|
||||||
return 1.30f / FMath::Sqrt(FMath::Max(EigenFrequencySquared, 1.0e-8f));
|
|
||||||
}
|
|
||||||
|
|
||||||
void AVoxelForgeLivingPlaneActor::BeginPlay()
|
|
||||||
{
|
|
||||||
Super::BeginPlay();
|
|
||||||
GridSize = FMath::Clamp(GridSize, 16, 2048);
|
|
||||||
PlaneLengthMeters = FMath::Max(PlaneLengthMeters, 1.0f);
|
|
||||||
FixedStepSeconds = GetStableStepSeconds();
|
|
||||||
CreateFieldTargets();
|
|
||||||
|
|
||||||
if (PlaneMaterial && PlaneMesh)
|
|
||||||
{
|
|
||||||
DynamicPlaneMaterial = PlaneMesh->CreateDynamicMaterialInstance(0, PlaneMaterial);
|
|
||||||
if (DynamicPlaneMaterial)
|
|
||||||
{
|
|
||||||
DynamicPlaneMaterial->SetScalarParameterValue(TEXT("PlaneLengthMeters"), PlaneLengthMeters);
|
|
||||||
DynamicPlaneMaterial->SetScalarParameterValue(TEXT("FieldSize"), static_cast<float>(GridSize));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
UpdateMaterialField();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AVoxelForgeLivingPlaneActor::CreateFieldTargets()
|
|
||||||
{
|
|
||||||
FieldTargets[0] = CreateFieldTarget(this, GridSize, TEXT("LivingPlaneStateA"));
|
|
||||||
FieldTargets[1] = CreateFieldTarget(this, GridSize, TEXT("LivingPlaneStateB"));
|
|
||||||
CurrentFieldIndex = 0;
|
|
||||||
SimulationStepCount = 0;
|
|
||||||
SimulationAccumulatorSeconds = 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void AVoxelForgeLivingPlaneActor::SetObjectState(int32 ObjectIndex,
|
|
||||||
const FVFLivingPlaneObjectInput& State)
|
|
||||||
{
|
|
||||||
if (!ObjectInputs.IsValidIndex(ObjectIndex) || ObjectIndex >= 2)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ObjectInputs[ObjectIndex] = State;
|
|
||||||
}
|
|
||||||
|
|
||||||
void AVoxelForgeLivingPlaneActor::ResetSimulation()
|
|
||||||
{
|
|
||||||
if (!IsInGameThread() || !FieldTargets[0] || !FieldTargets[1])
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
CreateFieldTargets();
|
|
||||||
FixedStepSeconds = GetStableStepSeconds();
|
|
||||||
UpdateMaterialField();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AVoxelForgeLivingPlaneActor::UpdateMaterialField()
|
|
||||||
{
|
|
||||||
if (DynamicPlaneMaterial && FieldTargets[CurrentFieldIndex])
|
|
||||||
{
|
|
||||||
DynamicPlaneMaterial->SetTextureParameterValue(TEXT("LivingField"), FieldTargets[CurrentFieldIndex]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void AVoxelForgeLivingPlaneActor::EnqueueSimulationSteps(int32 NumSteps)
|
|
||||||
{
|
|
||||||
if (NumSteps <= 0 || !FieldTargets[0] || !FieldTargets[1])
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
FTextureRenderTargetResource* ResourceA = FieldTargets[0]->GameThread_GetRenderTargetResource();
|
|
||||||
FTextureRenderTargetResource* ResourceB = FieldTargets[1]->GameThread_GetRenderTargetResource();
|
|
||||||
if (!ResourceA || !ResourceB)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
TArray<FLivingPlaneStepInput> Steps;
|
|
||||||
Steps.Reserve(NumSteps);
|
|
||||||
const FTransform PlaneTransform = GetActorTransform();
|
|
||||||
for (int32 Step = 0; Step < NumSteps; ++Step)
|
|
||||||
{
|
|
||||||
FLivingPlaneStepInput& Input = Steps.AddDefaulted_GetRef();
|
|
||||||
Input.TimeSeconds = static_cast<float>(SimulationStepCount + Step) * FixedStepSeconds;
|
|
||||||
for (int32 ObjectIndex = 0; ObjectIndex < 2; ++ObjectIndex)
|
|
||||||
{
|
|
||||||
if (!ObjectInputs.IsValidIndex(ObjectIndex) || !ObjectInputs[ObjectIndex].bActive)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FVFLivingPlaneObjectInput& Object = ObjectInputs[ObjectIndex];
|
|
||||||
const FVector LocalPosition = PlaneTransform.InverseTransformPosition(Object.WorldPositionCm);
|
|
||||||
const FVector LocalVelocity = PlaneTransform.InverseTransformVector(Object.WorldVelocityCmPerSecond);
|
|
||||||
const float RadiusMeters = FMath::Max(Object.RadiusCm * 0.01f, 0.001f);
|
|
||||||
Input.ObjectPositionRadius[ObjectIndex] = FVector4f(
|
|
||||||
static_cast<float>(LocalPosition.X * 0.01),
|
|
||||||
static_cast<float>(LocalPosition.Y * 0.01),
|
|
||||||
static_cast<float>(LocalPosition.Z * 0.01),
|
|
||||||
RadiusMeters);
|
|
||||||
Input.ObjectVelocityPush[ObjectIndex] = FVector4f(
|
|
||||||
static_cast<float>(LocalVelocity.X * 0.01),
|
|
||||||
static_cast<float>(LocalVelocity.Y * 0.01),
|
|
||||||
static_cast<float>(LocalVelocity.Z * 0.01),
|
|
||||||
FMath::Max(Object.PushAccelerationMetersPerSecondSquared, 0.0f));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const int32 Resolution = GridSize;
|
|
||||||
const int32 InitialFieldIndex = CurrentFieldIndex;
|
|
||||||
const float Dt = static_cast<float>(FixedStepSeconds);
|
|
||||||
const float Dx = PlaneLengthMeters / static_cast<float>(Resolution);
|
|
||||||
const float PlaneLength = PlaneLengthMeters;
|
|
||||||
const float WaveSpeed = WaveSpeedMetersPerSecond;
|
|
||||||
const float Damping = DampingPerSecond;
|
|
||||||
const float Foundation = FoundationStiffnessPerSecondSquared;
|
|
||||||
const float BreathingStrength = bEnableBreathing ? BreathingStrengthMetersPerSecondSquared : 0.0f;
|
|
||||||
const FVector2f BreathingSettings(bEnableBreathing ? 1.0f : 0.0f, BreathingStrength);
|
|
||||||
const FVector4f ImprintSettings(bEnableImprint ? 1.0f : 0.0f,
|
|
||||||
ImprintCaptureMetersPerSecond, FMath::Max(ImprintRecoverySeconds, 1.0e-4f),
|
|
||||||
FMath::Max(ImprintMaxDepthMeters, 0.0f));
|
|
||||||
|
|
||||||
ENQUEUE_RENDER_COMMAND(VoxelForgeLivingPlaneStep)(
|
|
||||||
[ResourceA, ResourceB, Steps = MoveTemp(Steps), InitialFieldIndex, Resolution, Dt, Dx, PlaneLength,
|
|
||||||
WaveSpeed, Damping, Foundation, BreathingSettings, ImprintSettings]
|
|
||||||
(FRHICommandListImmediate& RHICmdList) mutable
|
|
||||||
{
|
|
||||||
FRDGBuilder GraphBuilder(RHICmdList, RDG_EVENT_NAME("VoxelForgeLivingPlane"));
|
|
||||||
FRDGTextureRef Fields[2] = {
|
|
||||||
RegisterExternalTexture(GraphBuilder, ResourceA->GetRenderTargetTexture().GetReference(), TEXT("LivingPlaneStateA")),
|
|
||||||
RegisterExternalTexture(GraphBuilder, ResourceB->GetRenderTargetTexture().GetReference(), TEXT("LivingPlaneStateB"))};
|
|
||||||
TShaderMapRef<FVFLivingPlaneStepCS> ComputeShader(GetGlobalShaderMap(GMaxRHIFeatureLevel));
|
|
||||||
int32 CurrentIndex = InitialFieldIndex;
|
|
||||||
for (const FLivingPlaneStepInput& StepInput : Steps)
|
|
||||||
{
|
|
||||||
const int32 TargetIndex = 1 - CurrentIndex;
|
|
||||||
FVFLivingPlaneStepCS::FParameters* PassParameters =
|
|
||||||
GraphBuilder.AllocParameters<FVFLivingPlaneStepCS::FParameters>();
|
|
||||||
PassParameters->GridSize = FIntPoint(Resolution, Resolution);
|
|
||||||
PassParameters->Dt = Dt;
|
|
||||||
PassParameters->Dx = Dx;
|
|
||||||
PassParameters->PlaneLength = PlaneLength;
|
|
||||||
PassParameters->WaveSpeed = WaveSpeed;
|
|
||||||
PassParameters->Damping = Damping;
|
|
||||||
PassParameters->FoundationStiffness = Foundation;
|
|
||||||
PassParameters->TimeSeconds = StepInput.TimeSeconds;
|
|
||||||
PassParameters->BreathingSettings = BreathingSettings;
|
|
||||||
PassParameters->ImprintSettings = ImprintSettings;
|
|
||||||
for (int32 ObjectIndex = 0; ObjectIndex < 2; ++ObjectIndex)
|
|
||||||
{
|
|
||||||
PassParameters->ObjectPositionRadius[ObjectIndex] = StepInput.ObjectPositionRadius[ObjectIndex];
|
|
||||||
PassParameters->ObjectVelocityPush[ObjectIndex] = StepInput.ObjectVelocityPush[ObjectIndex];
|
|
||||||
}
|
|
||||||
PassParameters->SourceState = GraphBuilder.CreateSRV(Fields[CurrentIndex]);
|
|
||||||
PassParameters->TargetState = GraphBuilder.CreateUAV(Fields[TargetIndex]);
|
|
||||||
FComputeShaderUtils::AddPass(GraphBuilder,
|
|
||||||
RDG_EVENT_NAME("LivingPlaneStep"),
|
|
||||||
ComputeShader, PassParameters,
|
|
||||||
FIntVector(FMath::DivideAndRoundUp(Resolution, LivingPlaneThreadGroupSize),
|
|
||||||
FMath::DivideAndRoundUp(Resolution, LivingPlaneThreadGroupSize), 1));
|
|
||||||
CurrentIndex = TargetIndex;
|
|
||||||
}
|
|
||||||
GraphBuilder.Execute();
|
|
||||||
});
|
|
||||||
|
|
||||||
CurrentFieldIndex = (InitialFieldIndex + NumSteps) & 1;
|
|
||||||
SimulationStepCount += NumSteps;
|
|
||||||
UpdateMaterialField();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AVoxelForgeLivingPlaneActor::Tick(float DeltaSeconds)
|
|
||||||
{
|
|
||||||
Super::Tick(DeltaSeconds);
|
|
||||||
if (FixedStepSeconds <= 0.0 || !FieldTargets[0] || !FieldTargets[1])
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
SimulationAccumulatorSeconds += FMath::Max(static_cast<double>(DeltaSeconds), 0.0);
|
|
||||||
const int32 StepsToRun = FMath::Min(
|
|
||||||
static_cast<int32>(SimulationAccumulatorSeconds / FixedStepSeconds), 32);
|
|
||||||
if (StepsToRun > 0)
|
|
||||||
{
|
|
||||||
SimulationAccumulatorSeconds -= StepsToRun * FixedStepSeconds;
|
|
||||||
EnqueueSimulationSteps(StepsToRun);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void AVoxelForgeLivingPlaneActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
|
||||||
{
|
|
||||||
Super::EndPlay(EndPlayReason);
|
|
||||||
DynamicPlaneMaterial = nullptr;
|
|
||||||
FieldTargets[0] = nullptr;
|
|
||||||
FieldTargets[1] = nullptr;
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
#include "Modules/ModuleManager.h"
|
|
||||||
|
|
||||||
#include "Interfaces/IPluginManager.h"
|
|
||||||
#include "Misc/Paths.h"
|
|
||||||
#include "ShaderCore.h"
|
|
||||||
|
|
||||||
class FVoxelForgeLivingPlaneModule final : public IModuleInterface
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
virtual void StartupModule() override
|
|
||||||
{
|
|
||||||
const TSharedPtr<IPlugin> Plugin = IPluginManager::Get().FindPlugin(TEXT("VoxelForge"));
|
|
||||||
if (!Plugin.IsValid())
|
|
||||||
{
|
|
||||||
UE_LOG(LogTemp, Error, TEXT("VoxelForgeLivingPlane could not find its owning plugin."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
AddShaderSourceDirectoryMapping(
|
|
||||||
TEXT("/Plugin/VoxelForgeLivingPlane"),
|
|
||||||
FPaths::Combine(Plugin->GetBaseDir(), TEXT("Shaders")));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
IMPLEMENT_MODULE(FVoxelForgeLivingPlaneModule, VoxelForgeLivingPlane)
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
|
||||||
#include "GameFramework/Actor.h"
|
|
||||||
#include "VoxelForgeLivingPlaneActor.generated.h"
|
|
||||||
|
|
||||||
class UMaterialInterface;
|
|
||||||
class UMaterialInstanceDynamic;
|
|
||||||
class UStaticMeshComponent;
|
|
||||||
class UTextureRenderTarget2D;
|
|
||||||
|
|
||||||
USTRUCT(BlueprintType)
|
|
||||||
struct FVFLivingPlaneObjectInput
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Object")
|
|
||||||
FVector WorldPositionCm = FVector(0.0, 0.0, 43.0);
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Object")
|
|
||||||
FVector WorldVelocityCmPerSecond = FVector::ZeroVector;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Object", meta=(ClampMin="0.1"))
|
|
||||||
float RadiusCm = 43.0f;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Object", meta=(ClampMin="0.0"))
|
|
||||||
float PushAccelerationMetersPerSecondSquared = 36.0f;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Object")
|
|
||||||
bool bActive = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** A render-only, GPU-stepped heightfield plane. Object state is uploaded; the field is never read back. */
|
|
||||||
UCLASS(BlueprintType, Blueprintable)
|
|
||||||
class VOXELFORGELIVINGPLANE_API AVoxelForgeLivingPlaneActor : public AActor
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
AVoxelForgeLivingPlaneActor();
|
|
||||||
|
|
||||||
virtual void BeginPlay() override;
|
|
||||||
virtual void Tick(float DeltaSeconds) override;
|
|
||||||
virtual void OnConstruction(const FTransform& Transform) override;
|
|
||||||
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
|
|
||||||
|
|
||||||
UFUNCTION(BlueprintCallable, Category="Living Plane|Object")
|
|
||||||
void SetObjectState(int32 ObjectIndex, const FVFLivingPlaneObjectInput& State);
|
|
||||||
|
|
||||||
UFUNCTION(BlueprintCallable, Category="Living Plane")
|
|
||||||
void ResetSimulation();
|
|
||||||
|
|
||||||
UFUNCTION(BlueprintPure, Category="Living Plane")
|
|
||||||
int64 GetSimulationStepCount() const { return SimulationStepCount; }
|
|
||||||
|
|
||||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Living Plane")
|
|
||||||
TObjectPtr<UStaticMeshComponent> PlaneMesh;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Living Plane|Rendering")
|
|
||||||
TObjectPtr<UMaterialInterface> PlaneMaterial;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Simulation", meta=(ClampMin="16", ClampMax="2048"))
|
|
||||||
int32 GridSize = 256;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Simulation", meta=(ClampMin="1.0", ClampMax="100.0"))
|
|
||||||
float PlaneLengthMeters = 12.0f;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Simulation", meta=(ClampMin="0.0"))
|
|
||||||
float WaveSpeedMetersPerSecond = 4.5f;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Simulation", meta=(ClampMin="0.0"))
|
|
||||||
float DampingPerSecond = 1.35f;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Simulation", meta=(ClampMin="0.0"))
|
|
||||||
float FoundationStiffnessPerSecondSquared = 28.0f;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Simulation")
|
|
||||||
bool bEnableBreathing = true;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Simulation", meta=(ClampMin="0.0", ClampMax="30.0"))
|
|
||||||
float BreathingStrengthMetersPerSecondSquared = 4.8f;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Imprint")
|
|
||||||
bool bEnableImprint = true;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Imprint", meta=(ClampMin="0.0", ClampMax="1.0"))
|
|
||||||
float ImprintCaptureMetersPerSecond = 0.15f;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Imprint", meta=(ClampMin="0.1", ClampMax="60.0"))
|
|
||||||
float ImprintRecoverySeconds = 6.0f;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Imprint", meta=(ClampMin="0.0", ClampMax="1.0"))
|
|
||||||
float ImprintMaxDepthMeters = 0.34f;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Object")
|
|
||||||
TArray<FVFLivingPlaneObjectInput> ObjectInputs;
|
|
||||||
|
|
||||||
private:
|
|
||||||
void CreateFieldTargets();
|
|
||||||
void EnqueueSimulationSteps(int32 NumSteps);
|
|
||||||
void UpdateMaterialField();
|
|
||||||
float GetStableStepSeconds() const;
|
|
||||||
|
|
||||||
UPROPERTY(Transient)
|
|
||||||
TObjectPtr<UTextureRenderTarget2D> FieldTargets[2];
|
|
||||||
|
|
||||||
UPROPERTY(Transient)
|
|
||||||
TObjectPtr<UMaterialInstanceDynamic> DynamicPlaneMaterial;
|
|
||||||
|
|
||||||
int32 CurrentFieldIndex = 0;
|
|
||||||
int64 SimulationStepCount = 0;
|
|
||||||
double SimulationAccumulatorSeconds = 0.0;
|
|
||||||
double FixedStepSeconds = 0.0;
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
using UnrealBuildTool;
|
|
||||||
|
|
||||||
public class VoxelForgeLivingPlane : ModuleRules
|
|
||||||
{
|
|
||||||
public VoxelForgeLivingPlane(ReadOnlyTargetRules Target) : base(Target)
|
|
||||||
{
|
|
||||||
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
|
||||||
|
|
||||||
if (Target.Platform == UnrealTargetPlatform.Linux)
|
|
||||||
{
|
|
||||||
bUseUnity = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
PublicDependencyModuleNames.AddRange(new string[]
|
|
||||||
{
|
|
||||||
"Core",
|
|
||||||
"CoreUObject",
|
|
||||||
"Engine"
|
|
||||||
});
|
|
||||||
|
|
||||||
PrivateDependencyModuleNames.AddRange(new string[]
|
|
||||||
{
|
|
||||||
"Projects",
|
|
||||||
"RenderCore",
|
|
||||||
"RHI"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,14 +7,10 @@ fi
|
|||||||
|
|
||||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
ROOT="$SCRIPT_DIR"
|
ROOT="$SCRIPT_DIR"
|
||||||
PROJECT_ROOT="$(cd -- "$ROOT/.." && pwd)"
|
|
||||||
VERIFY_DIR="$ROOT/Tools/verify"
|
|
||||||
COMPILER="clang++-19"
|
COMPILER="clang++-19"
|
||||||
VARIANT="clang19"
|
VARIANT="clang19"
|
||||||
FP_CONTRACT="off"
|
FP_CONTRACT="off"
|
||||||
JOBS=12
|
JOBS=12
|
||||||
SANITIZERS=""
|
|
||||||
EXCLUDE_UNSIGNED_OVERFLOW=0
|
|
||||||
|
|
||||||
while (($#)); do
|
while (($#)); do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -22,8 +18,6 @@ while (($#)); do
|
|||||||
--variant=*) VARIANT="${1#*=}" ;;
|
--variant=*) VARIANT="${1#*=}" ;;
|
||||||
--fp-contract=*) FP_CONTRACT="${1#*=}" ;;
|
--fp-contract=*) FP_CONTRACT="${1#*=}" ;;
|
||||||
--jobs=*) JOBS="${1#*=}" ;;
|
--jobs=*) JOBS="${1#*=}" ;;
|
||||||
--sanitize=*) SANITIZERS="${1#*=}" ;;
|
|
||||||
--exclude-unsigned-integer-overflow) EXCLUDE_UNSIGNED_OVERFLOW=1 ;;
|
|
||||||
*) printf 'Unknown build option: %s\n' "$1" >&2; exit 2 ;;
|
*) printf 'Unknown build option: %s\n' "$1" >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
shift
|
shift
|
||||||
@@ -42,20 +36,10 @@ command -v ar >/dev/null || { printf 'ar not found\n' >&2; exit 2; }
|
|||||||
command -v pkg-config >/dev/null || { printf 'pkg-config not found\n' >&2; exit 2; }
|
command -v pkg-config >/dev/null || { printf 'pkg-config not found\n' >&2; exit 2; }
|
||||||
pkg-config --exists egl glesv2 libpng || { printf 'EGL, GLESv2, and libpng development files are required\n' >&2; exit 2; }
|
pkg-config --exists egl glesv2 libpng || { printf 'EGL, GLESv2, and libpng development files are required\n' >&2; exit 2; }
|
||||||
|
|
||||||
BUILD_ROOT="${VOXELFORGE_BUILD_ROOT:-$ROOT/Build}"
|
BUILD_DIR="$ROOT/Build/linux-$VARIANT"
|
||||||
BIN_DIR="${VOXELFORGE_BIN_DIR:-$ROOT/bin/linux}"
|
BIN_DIR="$ROOT/bin/linux"
|
||||||
BUILD_DIR="$BUILD_ROOT/linux-$VARIANT"
|
|
||||||
mkdir -p "$BUILD_DIR/wrappers" "$BUILD_DIR/objects" "$BIN_DIR"
|
mkdir -p "$BUILD_DIR/wrappers" "$BUILD_DIR/objects" "$BIN_DIR"
|
||||||
|
|
||||||
STAMP_HELPER="$VERIFY_DIR/source_stamp.py"
|
|
||||||
STAMP_ARGS=()
|
|
||||||
if [[ "${VOXELFORGE_INCLUDE_UNREAL_ADAPTER:-0}" == "1" ]]; then
|
|
||||||
STAMP_ARGS+=(--include-unreal-adapter)
|
|
||||||
fi
|
|
||||||
SOURCE_STAMP="$(python3 -B "$STAMP_HELPER" stamp --root "$PROJECT_ROOT" "${STAMP_ARGS[@]}")"
|
|
||||||
python3 -B "$STAMP_HELPER" write-manifest --root "$PROJECT_ROOT" --output "$BUILD_DIR/source-manifest.txt" "${STAMP_ARGS[@]}" >/dev/null
|
|
||||||
printf 'Source stamp: %s\n' "$SOURCE_STAMP"
|
|
||||||
|
|
||||||
INCLUDES=(
|
INCLUDES=(
|
||||||
"-I$ROOT/Core/include"
|
"-I$ROOT/Core/include"
|
||||||
"-I$ROOT/Core/src"
|
"-I$ROOT/Core/src"
|
||||||
@@ -64,32 +48,13 @@ INCLUDES=(
|
|||||||
"-I$ROOT/Source/VoxelForge/Public"
|
"-I$ROOT/Source/VoxelForge/Public"
|
||||||
"-I$ROOT/Source/VoxelForge/Private"
|
"-I$ROOT/Source/VoxelForge/Private"
|
||||||
)
|
)
|
||||||
OPT_LEVEL=-O2
|
|
||||||
LINK_FLAGS=()
|
|
||||||
if [[ -n "$SANITIZERS" ]]; then
|
|
||||||
OPT_LEVEL=-O1
|
|
||||||
LINK_FLAGS+=("-fsanitize=$SANITIZERS" -fno-sanitize-recover=undefined)
|
|
||||||
if ((EXCLUDE_UNSIGNED_OVERFLOW)); then
|
|
||||||
LINK_FLAGS+=(-fno-sanitize=unsigned-integer-overflow)
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
FLAGS=(
|
FLAGS=(
|
||||||
-std=c++20 "$OPT_LEVEL" -mavx2 -mfma -fno-fast-math
|
-std=c++20 -O2 -mavx2 -mfma -fno-fast-math
|
||||||
-fexceptions -pthread "${INCLUDES[@]}"
|
-fexceptions -pthread "${INCLUDES[@]}"
|
||||||
"-DVOXELFORGE_SOURCE_STAMP=\"$SOURCE_STAMP\""
|
|
||||||
)
|
)
|
||||||
FLAGS+=("${LINK_FLAGS[@]}")
|
|
||||||
if [[ "$FP_CONTRACT" != "default" ]]; then
|
if [[ "$FP_CONTRACT" != "default" ]]; then
|
||||||
FLAGS+=("-ffp-contract=$FP_CONTRACT")
|
FLAGS+=("-ffp-contract=$FP_CONTRACT")
|
||||||
fi
|
fi
|
||||||
if [[ "${VOXELFORGE_PROFILE_LEGACY_TERMS:-0}" == "1" ]]; then
|
|
||||||
FLAGS+=(-DVF_L76_PROFILE_LEGACY_TERMS=1)
|
|
||||||
fi
|
|
||||||
if [[ "${VOXELFORGE_PROFILE_L77:-0}" == "1" ]]; then
|
|
||||||
FLAGS+=(-DVF_L77_PROFILE=1 -g)
|
|
||||||
LINK_FLAGS+=(-no-pie)
|
|
||||||
fi
|
|
||||||
if [[ "$($COMPILER --version | head -n 1)" == *clang* ]]; then
|
if [[ "$($COMPILER --version | head -n 1)" == *clang* ]]; then
|
||||||
FLAGS+=(-Wno-c++11-narrowing)
|
FLAGS+=(-Wno-c++11-narrowing)
|
||||||
else
|
else
|
||||||
@@ -146,11 +111,6 @@ done < "$ROOT/Core/core_sources.txt"
|
|||||||
headless_object="$BUILD_DIR/objects/StreamBenchmarkHeadless.o"
|
headless_object="$BUILD_DIR/objects/StreamBenchmarkHeadless.o"
|
||||||
compile_background "$ROOT/Source/StreamBenchmarkHeadless.cpp" "$headless_object"
|
compile_background "$ROOT/Source/StreamBenchmarkHeadless.cpp" "$headless_object"
|
||||||
LEGACY_OBJECTS+=("$headless_object")
|
LEGACY_OBJECTS+=("$headless_object")
|
||||||
if [[ "${VOXELFORGE_PROFILE_L77:-0}" == "1" ]]; then
|
|
||||||
l77_profiler_object="$BUILD_DIR/objects/Linux77Profiler.o"
|
|
||||||
compile_background "$ROOT/Source/Linux77Profiler.cpp" "$l77_profiler_object"
|
|
||||||
LEGACY_OBJECTS+=("$l77_profiler_object")
|
|
||||||
fi
|
|
||||||
|
|
||||||
for pid in "${PIDS[@]}"; do wait "$pid"; done
|
for pid in "${PIDS[@]}"; do wait "$pid"; done
|
||||||
PIDS=()
|
PIDS=()
|
||||||
@@ -158,19 +118,11 @@ PIDS=()
|
|||||||
CORE_LIBRARY="$BUILD_DIR/libVoxelForgeCore.a"
|
CORE_LIBRARY="$BUILD_DIR/libVoxelForgeCore.a"
|
||||||
"$(command -v ar)" rcs "$CORE_LIBRARY" "${CORE_OBJECTS[@]}"
|
"$(command -v ar)" rcs "$CORE_LIBRARY" "${CORE_OBJECTS[@]}"
|
||||||
OUTPUT="$BIN_DIR/VoxelForgeCoreHeadless-$VARIANT"
|
OUTPUT="$BIN_DIR/VoxelForgeCoreHeadless-$VARIANT"
|
||||||
"$COMPILER" -pthread "${LINK_FLAGS[@]}" "${LEGACY_OBJECTS[@]}" "$CORE_LIBRARY" -o "$OUTPUT"
|
"$COMPILER" -pthread "${LEGACY_OBJECTS[@]}" "$CORE_LIBRARY" -lz -o "$OUTPUT"
|
||||||
CAPTURE_OUTPUT="$BIN_DIR/VoxelForgeOffscreenCapture-$VARIANT"
|
CAPTURE_OUTPUT="$BIN_DIR/VoxelForgeOffscreenCapture-$VARIANT"
|
||||||
"$COMPILER" -std=c++20 -O2 -fno-fast-math -pthread \
|
"$COMPILER" -std=c++20 -O2 -fno-fast-math -pthread \
|
||||||
"-DVOXELFORGE_SOURCE_STAMP=\"$SOURCE_STAMP\"" -I"$ROOT/Source" \
|
|
||||||
"$ROOT/Source/LinuxOffscreenCapture.cpp" -o "$CAPTURE_OUTPUT" \
|
"$ROOT/Source/LinuxOffscreenCapture.cpp" -o "$CAPTURE_OUTPUT" \
|
||||||
$(pkg-config --cflags --libs egl glesv2 libpng)
|
$(pkg-config --cflags --libs egl glesv2 libpng)
|
||||||
|
|
||||||
POST_BUILD_STAMP="$(python3 -B "$STAMP_HELPER" stamp --root "$PROJECT_ROOT" "${STAMP_ARGS[@]}")"
|
|
||||||
if [[ "$POST_BUILD_STAMP" != "$SOURCE_STAMP" ]]; then
|
|
||||||
printf 'Source changed during build: before=%s after=%s\n' "$SOURCE_STAMP" "$POST_BUILD_STAMP" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
printf 'Built Core library: %s\nBuilt headless benchmark: %s\n' "$CORE_LIBRARY" "$OUTPUT"
|
printf 'Built Core library: %s\nBuilt headless benchmark: %s\n' "$CORE_LIBRARY" "$OUTPUT"
|
||||||
printf 'Built EGL offscreen capture: %s\n' "$CAPTURE_OUTPUT"
|
printf 'Built EGL offscreen capture: %s\n' "$CAPTURE_OUTPUT"
|
||||||
printf 'Compiler: %s | variant: %s | fp-contract: %s | sanitizers: %s | jobs: %s\n' \
|
printf 'Compiler: %s | variant: %s | fp-contract: %s | jobs: %s\n' "$COMPILER" "$VARIANT" "$FP_CONTRACT" "$JOBS"
|
||||||
"$COMPILER" "$VARIANT" "$FP_CONTRACT" "${SANITIZERS:-off}" "$JOBS"
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <AssetRegistry/AssetData.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <AssetRegistry/AssetRegistryModule.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <Async/ParallelFor.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,10 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <CoreMinimal.h>
|
|
||||||
#else
|
|
||||||
|
|
||||||
// Deliberately small UE-style surface used by the copied VoxelForge CPU sources.
|
// Deliberately small UE-style surface used by the copied VoxelForge CPU sources.
|
||||||
// This is a value/container compatibility layer; it does not start or link Unreal.
|
// This is a value/container compatibility layer; it does not start or link Unreal.
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -50,9 +45,6 @@
|
|||||||
#define GENERATED_UCLASS_BODY()
|
#define GENERATED_UCLASS_BODY()
|
||||||
#define VOXELFORGE_API
|
#define VOXELFORGE_API
|
||||||
#define FORCEINLINE inline
|
#define FORCEINLINE inline
|
||||||
#ifdef WITH_EDITOR
|
|
||||||
#undef WITH_EDITOR
|
|
||||||
#endif
|
|
||||||
#define WITH_EDITOR 0
|
#define WITH_EDITOR 0
|
||||||
#define TRACE_CPUPROFILER_EVENT_SCOPE(...)
|
#define TRACE_CPUPROFILER_EVENT_SCOPE(...)
|
||||||
#define PRAGMA_DISABLE_SHADOW_VARIABLE_WARNINGS
|
#define PRAGMA_DISABLE_SHADOW_VARIABLE_WARNINGS
|
||||||
@@ -713,4 +705,3 @@ struct FParse
|
|||||||
struct FCrc { static uint32 MemCrc32(const void*Data,int32 Length,uint32 Crc=0){const auto*P=static_cast<const uint8*>(Data);uint32 V=~Crc;for(int32 I=0;I<Length;++I){V^=P[I];for(int J=0;J<8;++J)V=(V>>1)^(0xEDB88320u&static_cast<uint32>(-static_cast<int32>(V&1)));}return ~V;} static uint32 StrCrc32(const char*S,uint32 Crc=0){return MemCrc32(S,static_cast<int32>(std::strlen(S)),Crc);} };
|
struct FCrc { static uint32 MemCrc32(const void*Data,int32 Length,uint32 Crc=0){const auto*P=static_cast<const uint8*>(Data);uint32 V=~Crc;for(int32 I=0;I<Length;++I){V^=P[I];for(int J=0;J<8;++J)V=(V>>1)^(0xEDB88320u&static_cast<uint32>(-static_cast<int32>(V&1)));}return ~V;} static uint32 StrCrc32(const char*S,uint32 Crc=0){return MemCrc32(S,static_cast<int32>(std::strlen(S)),Crc);} };
|
||||||
template<class T> using TAtomic = std::atomic<T>;
|
template<class T> using TAtomic = std::atomic<T>;
|
||||||
class FThreadSafeCounter { public:int32 Increment(){return ++Value;}int32 Decrement(){return --Value;}int32 GetValue()const{return Value.load();}void Reset(){Value.store(0);}private:std::atomic<int32>Value{0};};
|
class FThreadSafeCounter { public:int32 Increment(){return ++Value;}int32 Decrement(){return --Value;}int32 GetValue()const{return Value.load();}void Reset(){Value.store(0);}private:std::atomic<int32>Value{0};};
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <Dom/JsonObject.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <Dom/JsonValue.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <Engine/DataAsset.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <GameplayTagContainer.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <HAL/CriticalSection.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <HAL/Event.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <HAL/FileManager.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <HAL/IConsoleManager.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <HAL/PlatformProcess.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <HAL/PlatformTime.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,10 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <Json.h>
|
|
||||||
#else
|
|
||||||
|
|
||||||
#include <charconv>
|
#include <charconv>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
@@ -167,4 +162,3 @@ namespace VFJson
|
|||||||
return Parser(std::move(Text)).Parse(Out, Error);
|
return Parser(std::move(Text)).Parse(Out, Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <Math/RandomStream.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <Misc/CommandLine.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <Misc/Crc.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <Misc/FileHelper.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <Misc/Parse.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <Misc/Paths.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <Misc/ScopeLock.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <Misc/ScopeRWLock.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <Misc/SecureHash.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <Modules/ModuleManager.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <ProfilingDebugging/CpuProfilerTrace.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <Serialization/CustomVersion.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <Serialization/JsonSerializer.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <Serialization/JsonWriter.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,9 +1,4 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <Stats/Stats.h>
|
|
||||||
#else
|
|
||||||
#define STATCAT_Advanced 0
|
#define STATCAT_Advanced 0
|
||||||
#define DECLARE_STATS_GROUP(Name,Group,Category) constexpr int32 Group=0
|
#define DECLARE_STATS_GROUP(Name,Group,Category) constexpr int32 Group=0
|
||||||
#define DECLARE_DWORD_COUNTER_STAT_EXTERN(Name,Stat,Group,Api) constexpr int32 Stat=0
|
#define DECLARE_DWORD_COUNTER_STAT_EXTERN(Name,Stat,Group,Api) constexpr int32 Stat=0
|
||||||
@@ -13,4 +8,3 @@
|
|||||||
#define SET_DWORD_STAT(Stat,Value) ((void)0)
|
#define SET_DWORD_STAT(Stat,Value) ((void)0)
|
||||||
#define SCOPE_CYCLE_COUNTER(Stat) ((void)0)
|
#define SCOPE_CYCLE_COUNTER(Stat) ((void)0)
|
||||||
#define QUICK_SCOPE_CYCLE_COUNTER(Stat) ((void)0)
|
#define QUICK_SCOPE_CYCLE_COUNTER(Stat) ((void)0)
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <Templates/SubclassOf.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <Templates/UniquePtr.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <UObject/EnumProperty.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <UObject/FieldIterator.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <UObject/Object.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <UObject/UObjectGlobals.h>
|
|
||||||
#endif
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
|
||||||
&& (defined(__clang__) || defined(__GNUC__))
|
|
||||||
#include_next <UObject/UnrealType.h>
|
|
||||||
#endif
|
|
||||||
@@ -153,13 +153,8 @@ radius range, top flattening, taper, bounded contour warp, roughness, blend, con
|
|||||||
route constraints. `PlanFloatingIslandCluster` emits stable island and connector IDs, a connected
|
route constraints. `PlanFloatingIslandCluster` emits stable island and connector IDs, a connected
|
||||||
cluster graph, conservative influence AABBs, and a centerline route. At least the primary chain is
|
cluster graph, conservative influence AABBs, and a centerline route. At least the primary chain is
|
||||||
always planned; `islandDensityQ16` controls up to two optional side sites. Connectors carry a sharp-tag
|
always planned; `islandDensityQ16` controls up to two optional side sites. Connectors carry a sharp-tag
|
||||||
ready bit for a later hybrid-mesher adapter. The schema accepts geometry versions 1 through 3.
|
ready bit for a later hybrid-mesher adapter. The schema accepts geometry version 1 and 2. Manifests
|
||||||
Version 3 keeps the version-2 island form, removes the all-XY lower seal, and obtains its ceiling from
|
without a geometry version retain version-1 evaluation and canonical hashes.
|
||||||
the shared upper-strate boundary seal. The resolved upper bound comes from the strate's own chunk
|
|
||||||
range; `boundarySealSubvoxels` sets the shared seal thickness and remains a planning margin for island
|
|
||||||
features. It does not define a FloatingIslands-specific ceiling height. Versions 1 and 2 retain their
|
|
||||||
recorded two-sided boundary seals. Manifests without a geometry version retain version-1 evaluation
|
|
||||||
and canonical hashes.
|
|
||||||
|
|
||||||
`FloatingIslandFieldV1::densitySubvoxels` evaluates the canonical island and connector field.
|
`FloatingIslandFieldV1::densitySubvoxels` evaluates the canonical island and connector field.
|
||||||
`interval` returns unknown for boxes overlapping any influence AABB and a certified all-air range
|
`interval` returns unknown for boxes overlapping any influence AABB and a certified all-air range
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
#include <algorithm>
|
|
||||||
#include <limits>
|
|
||||||
|
|
||||||
#if defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_X64))
|
|
||||||
#include <intrin.h>
|
|
||||||
#elif defined(__i386__) || defined(__x86_64__)
|
|
||||||
#include <immintrin.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace VoxelForgeCoreShared
|
|
||||||
{
|
|
||||||
inline std::uint64_t ReadCpuCycles() noexcept
|
|
||||||
{
|
|
||||||
#if defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_X64))
|
|
||||||
_mm_lfence();
|
|
||||||
const std::uint64_t value=__rdtsc();
|
|
||||||
_mm_lfence();
|
|
||||||
return value;
|
|
||||||
#elif defined(__i386__) || defined(__x86_64__)
|
|
||||||
_mm_lfence();
|
|
||||||
const std::uint64_t value=__rdtsc();
|
|
||||||
_mm_lfence();
|
|
||||||
return value;
|
|
||||||
#else
|
|
||||||
return 0;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
inline std::uint64_t CpuCycleReadPairOverhead() noexcept
|
|
||||||
{
|
|
||||||
static const std::uint64_t overhead=[]()
|
|
||||||
{
|
|
||||||
std::uint64_t minimum=std::numeric_limits<std::uint64_t>::max();
|
|
||||||
for(unsigned i=0;i<64;++i)
|
|
||||||
{
|
|
||||||
const std::uint64_t start=ReadCpuCycles();
|
|
||||||
const std::uint64_t end=ReadCpuCycles();
|
|
||||||
minimum=std::min(minimum,end-start);
|
|
||||||
}
|
|
||||||
return minimum==std::numeric_limits<std::uint64_t>::max()?0:minimum;
|
|
||||||
}();
|
|
||||||
return overhead;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline std::uint64_t AdjustedCpuCycleDelta(std::uint64_t start,
|
|
||||||
std::uint64_t end) noexcept
|
|
||||||
{
|
|
||||||
const std::uint64_t delta=end-start;
|
|
||||||
const std::uint64_t overhead=CpuCycleReadPairOverhead();
|
|
||||||
return delta>overhead?delta-overhead:0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "VoxelForgeCore/CpuCycleCounter.h"
|
|
||||||
|
|
||||||
#include <array>
|
|
||||||
#include <chrono>
|
|
||||||
#include <cstddef>
|
|
||||||
#include <cstdint>
|
|
||||||
|
|
||||||
namespace VoxelForgeCore::floating_island_detail
|
|
||||||
{
|
|
||||||
enum class EIslandFieldCaller : std::uint8_t
|
|
||||||
{
|
|
||||||
GridFill=0,Skirt=1,Boundary=2,PassageFilter=3,Other=4,Count=5
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class EIslandAttributionBucket : std::uint8_t
|
|
||||||
{
|
|
||||||
Field=0,Island=1,Root=2,Connector=3,Helix=4,Column=5,
|
|
||||||
LocalProbe=6,SharedCache=7,MouthFilter=8,Boundary=9,CallerGrid=10,
|
|
||||||
CallerSkirt=11,CallerBoundary=12,CallerPassageFilter=13,CallerOther=14,
|
|
||||||
Count=15
|
|
||||||
};
|
|
||||||
|
|
||||||
struct FIslandFieldAttributionInstrumentation
|
|
||||||
{
|
|
||||||
static constexpr std::uint64_t SampleMask=255;
|
|
||||||
std::array<std::uint64_t,static_cast<std::size_t>(EIslandFieldCaller::Count)> sampledFieldCalls{};
|
|
||||||
std::array<std::uint64_t,static_cast<std::size_t>(EIslandFieldCaller::Count)> fieldCycles{};
|
|
||||||
std::array<std::uint64_t,static_cast<std::size_t>(EIslandFieldCaller::Count)> fieldNanoseconds{};
|
|
||||||
std::array<std::uint64_t,static_cast<std::size_t>(EIslandFieldCaller::Count)> sampledNativeCalls{};
|
|
||||||
std::array<std::uint64_t,static_cast<std::size_t>(EIslandFieldCaller::Count)> nativeCycles{};
|
|
||||||
std::array<std::uint64_t,static_cast<std::size_t>(EIslandFieldCaller::Count)> nativeNanoseconds{};
|
|
||||||
std::array<std::uint64_t,static_cast<std::size_t>(EIslandAttributionBucket::Count)> exclusiveCycles{};
|
|
||||||
std::array<std::uint64_t,static_cast<std::size_t>(EIslandAttributionBucket::Count)> exclusiveNanoseconds{};
|
|
||||||
std::array<std::uint64_t,static_cast<std::size_t>(EIslandAttributionBucket::Count)> scopeSamples{};
|
|
||||||
std::uint64_t traceSampledMisses=0,traceSeenBefore=0,traceEvictions=0,
|
|
||||||
traceReusedEvictions=0;
|
|
||||||
std::array<std::uint64_t,8> traceSet{};
|
|
||||||
std::array<std::int64_t,8> traceX{},traceY{},traceEvictedX{},traceEvictedY{};
|
|
||||||
std::array<std::uint64_t,8> traceIsland{},traceEvictedIsland{};
|
|
||||||
std::array<std::uint8_t,8> traceFlags{};
|
|
||||||
std::size_t traceCount=0;
|
|
||||||
};
|
|
||||||
|
|
||||||
inline FIslandFieldAttributionInstrumentation*& ActiveIslandFieldAttribution() noexcept
|
|
||||||
{
|
|
||||||
static thread_local FIslandFieldAttributionInstrumentation* active=nullptr;
|
|
||||||
return active;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline FIslandFieldAttributionInstrumentation* SetActiveIslandFieldAttribution(
|
|
||||||
FIslandFieldAttributionInstrumentation* value) noexcept
|
|
||||||
{
|
|
||||||
auto* previous=ActiveIslandFieldAttribution();
|
|
||||||
ActiveIslandFieldAttribution()=value;
|
|
||||||
return previous;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline EIslandFieldCaller& ActiveIslandFieldCaller() noexcept
|
|
||||||
{
|
|
||||||
static thread_local EIslandFieldCaller caller=EIslandFieldCaller::Other;
|
|
||||||
return caller;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct FScopedIslandFieldCaller
|
|
||||||
{
|
|
||||||
explicit FScopedIslandFieldCaller(EIslandFieldCaller value) noexcept
|
|
||||||
:previous(ActiveIslandFieldCaller()),enabled(ActiveIslandFieldAttribution()!=nullptr)
|
|
||||||
{if(enabled)ActiveIslandFieldCaller()=value;}
|
|
||||||
~FScopedIslandFieldCaller(){if(enabled)ActiveIslandFieldCaller()=previous;}
|
|
||||||
EIslandFieldCaller previous;
|
|
||||||
bool enabled=false;
|
|
||||||
};
|
|
||||||
|
|
||||||
inline std::array<std::uint64_t,
|
|
||||||
static_cast<std::size_t>(EIslandAttributionBucket::Count)>&
|
|
||||||
IslandComponentSampleSequence() noexcept
|
|
||||||
{
|
|
||||||
static thread_local std::array<std::uint64_t,
|
|
||||||
static_cast<std::size_t>(EIslandAttributionBucket::Count)> sequence{};
|
|
||||||
return sequence;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline std::array<std::uint64_t,
|
|
||||||
static_cast<std::size_t>(EIslandFieldCaller::Count)>&
|
|
||||||
IslandFieldSampleSequence() noexcept
|
|
||||||
{
|
|
||||||
static thread_local std::array<std::uint64_t,
|
|
||||||
static_cast<std::size_t>(EIslandFieldCaller::Count)> sequence{};
|
|
||||||
return sequence;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline std::array<std::uint64_t,
|
|
||||||
static_cast<std::size_t>(EIslandFieldCaller::Count)>&
|
|
||||||
IslandNativeSampleSequence() noexcept
|
|
||||||
{
|
|
||||||
static thread_local std::array<std::uint64_t,
|
|
||||||
static_cast<std::size_t>(EIslandFieldCaller::Count)> sequence{};
|
|
||||||
return sequence;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct FScopedIslandAttributionTimer
|
|
||||||
{
|
|
||||||
explicit FScopedIslandAttributionTimer(EIslandAttributionBucket bucket) noexcept
|
|
||||||
:metrics(ActiveIslandFieldAttribution()),bucketIndex(static_cast<std::size_t>(bucket))
|
|
||||||
{
|
|
||||||
if(!metrics)return;
|
|
||||||
if(bucketIndex==static_cast<std::size_t>(EIslandAttributionBucket::Field))
|
|
||||||
{
|
|
||||||
const auto callerIndex=static_cast<std::size_t>(ActiveIslandFieldCaller());
|
|
||||||
const auto calls=++IslandFieldSampleSequence()[callerIndex];
|
|
||||||
const std::uint64_t phase=(callerIndex*43u+17u)
|
|
||||||
&FIslandFieldAttributionInstrumentation::SampleMask;
|
|
||||||
sampled=(((calls+phase)&FIslandFieldAttributionInstrumentation::SampleMask)==0);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
auto& calls=IslandComponentSampleSequence()[bucketIndex];
|
|
||||||
++calls;
|
|
||||||
const std::uint64_t phase=(static_cast<std::uint64_t>(bucketIndex)*29u+17u)
|
|
||||||
&FIslandFieldAttributionInstrumentation::SampleMask;
|
|
||||||
sampled=(((calls+phase)&FIslandFieldAttributionInstrumentation::SampleMask)==0);
|
|
||||||
}
|
|
||||||
if(sampled)
|
|
||||||
{
|
|
||||||
start=VoxelForgeCoreShared::ReadCpuCycles();
|
|
||||||
wallStart=std::chrono::steady_clock::now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
~FScopedIslandAttributionTimer(){Stop();}
|
|
||||||
std::uint64_t Stop() noexcept
|
|
||||||
{
|
|
||||||
if(!sampled)return 0;
|
|
||||||
sampled=false;
|
|
||||||
const std::uint64_t raw=VoxelForgeCoreShared::ReadCpuCycles()-start;
|
|
||||||
const std::uint64_t overhead=VoxelForgeCoreShared::CpuCycleReadPairOverhead();
|
|
||||||
const std::uint64_t cycles=raw>overhead?raw-overhead:0;
|
|
||||||
const auto elapsed=std::chrono::duration_cast<std::chrono::nanoseconds>(
|
|
||||||
std::chrono::steady_clock::now()-wallStart).count();
|
|
||||||
metrics->exclusiveCycles[bucketIndex]+=cycles;
|
|
||||||
metrics->exclusiveNanoseconds[bucketIndex]+=elapsed>0
|
|
||||||
?static_cast<std::uint64_t>(elapsed):0;
|
|
||||||
++metrics->scopeSamples[bucketIndex];
|
|
||||||
if(bucketIndex==static_cast<std::size_t>(EIslandAttributionBucket::Field))
|
|
||||||
{
|
|
||||||
const std::size_t callerIndex=static_cast<std::size_t>(ActiveIslandFieldCaller());
|
|
||||||
metrics->fieldCycles[callerIndex]+=cycles;
|
|
||||||
metrics->fieldNanoseconds[callerIndex]+=elapsed>0
|
|
||||||
?static_cast<std::uint64_t>(elapsed):0;
|
|
||||||
++metrics->sampledFieldCalls[callerIndex];
|
|
||||||
}
|
|
||||||
return cycles;
|
|
||||||
}
|
|
||||||
FIslandFieldAttributionInstrumentation* metrics=nullptr;
|
|
||||||
std::size_t bucketIndex=0;
|
|
||||||
std::uint64_t start=0;
|
|
||||||
std::chrono::steady_clock::time_point wallStart{};
|
|
||||||
bool sampled=false;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct FScopedIslandAttributionSample
|
|
||||||
{
|
|
||||||
explicit FScopedIslandAttributionSample() noexcept
|
|
||||||
{
|
|
||||||
auto* attribution=ActiveIslandFieldAttribution();
|
|
||||||
if(!attribution)return;
|
|
||||||
const auto caller=ActiveIslandFieldCaller();
|
|
||||||
const std::size_t callerIndex=static_cast<std::size_t>(caller);
|
|
||||||
const std::uint64_t sequence=++IslandNativeSampleSequence()[callerIndex];
|
|
||||||
const std::uint64_t phase=(callerIndex*43u+93u)
|
|
||||||
&FIslandFieldAttributionInstrumentation::SampleMask;
|
|
||||||
if(((sequence+phase)&FIslandFieldAttributionInstrumentation::SampleMask)!=0)return;
|
|
||||||
sampledAttribution=attribution;
|
|
||||||
sampledCaller=caller;
|
|
||||||
sampled=true;
|
|
||||||
start=VoxelForgeCoreShared::ReadCpuCycles();
|
|
||||||
wallStart=std::chrono::steady_clock::now();
|
|
||||||
++attribution->sampledNativeCalls[callerIndex];
|
|
||||||
}
|
|
||||||
~FScopedIslandAttributionSample()
|
|
||||||
{
|
|
||||||
if(!sampled)return;
|
|
||||||
const std::uint64_t raw=VoxelForgeCoreShared::ReadCpuCycles()-start;
|
|
||||||
const std::uint64_t overhead=VoxelForgeCoreShared::CpuCycleReadPairOverhead();
|
|
||||||
const std::uint64_t duration=raw>overhead?raw-overhead:0;
|
|
||||||
const auto elapsed=std::chrono::duration_cast<std::chrono::nanoseconds>(
|
|
||||||
std::chrono::steady_clock::now()-wallStart).count();
|
|
||||||
const std::size_t callerIndex=static_cast<std::size_t>(sampledCaller);
|
|
||||||
sampledAttribution->nativeCycles[callerIndex]+=duration;
|
|
||||||
sampledAttribution->nativeNanoseconds[callerIndex]+=elapsed>0
|
|
||||||
?static_cast<std::uint64_t>(elapsed):0;
|
|
||||||
}
|
|
||||||
FIslandFieldAttributionInstrumentation* sampledAttribution=nullptr;
|
|
||||||
EIslandFieldCaller sampledCaller=EIslandFieldCaller::Other;
|
|
||||||
std::uint64_t start=0;
|
|
||||||
std::chrono::steady_clock::time_point wallStart{};
|
|
||||||
bool sampled=false;
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <array>
|
|
||||||
#include <cstddef>
|
|
||||||
#include <cstdint>
|
|
||||||
|
|
||||||
namespace VoxelForgeCore::Linux77Profiler
|
|
||||||
{
|
|
||||||
enum class ERoute : std::uint8_t
|
|
||||||
{
|
|
||||||
Dispatch=0,PackedScalar=1,PackedX8=2,AnalyticScalar=3,
|
|
||||||
AnalyticBatch=4,Native=5,LegacySurface=6,SharedSource=7,Count=8
|
|
||||||
};
|
|
||||||
enum class EStage : std::uint8_t
|
|
||||||
{
|
|
||||||
Other=0,GridFill=1,MarchingCubes=2,Skirt=3,Refine=4,Count=5
|
|
||||||
};
|
|
||||||
enum class EPhase : std::uint8_t
|
|
||||||
{
|
|
||||||
Dispatch=0,BaseField=1,CompositionGraph=2,Bands=3,Passage=4,
|
|
||||||
Support=5,Caps=6,Stamps=7,Edit=8,Other=9,Count=10
|
|
||||||
};
|
|
||||||
inline constexpr std::size_t RouteCount=static_cast<std::size_t>(ERoute::Count);
|
|
||||||
inline constexpr std::size_t StageCount=static_cast<std::size_t>(EStage::Count);
|
|
||||||
inline constexpr std::size_t PhaseCount=static_cast<std::size_t>(EPhase::Count);
|
|
||||||
inline thread_local bool ThreadContextActive=false;
|
|
||||||
|
|
||||||
struct FThreadContext
|
|
||||||
{
|
|
||||||
std::uint8_t lod=0xff;
|
|
||||||
ERoute route=ERoute::Count;
|
|
||||||
EStage stage=EStage::Other;
|
|
||||||
EPhase phase=EPhase::Other;
|
|
||||||
bool halo=false;
|
|
||||||
};
|
|
||||||
|
|
||||||
#if defined(VF_L77_PROFILE)
|
|
||||||
bool IsEnabled() noexcept;
|
|
||||||
void SetEnabled(bool enabled) noexcept;
|
|
||||||
void RegisterThread();
|
|
||||||
void Start(std::uint32_t periodMicroseconds=1000);
|
|
||||||
void StopAndPrint();
|
|
||||||
inline thread_local FThreadContext CurrentContext{};
|
|
||||||
|
|
||||||
struct FScopedRoute
|
|
||||||
{
|
|
||||||
explicit FScopedRoute(ERoute value) noexcept
|
|
||||||
:active(ThreadContextActive),previous(CurrentContext.route)
|
|
||||||
{if(active)CurrentContext.route=value;}
|
|
||||||
~FScopedRoute(){if(active)CurrentContext.route=previous;}
|
|
||||||
bool active=false;ERoute previous=ERoute::Dispatch;
|
|
||||||
};
|
|
||||||
struct FScopedStage
|
|
||||||
{
|
|
||||||
explicit FScopedStage(EStage value) noexcept
|
|
||||||
:active(ThreadContextActive),previous(CurrentContext.stage)
|
|
||||||
{if(active)CurrentContext.stage=value;}
|
|
||||||
~FScopedStage(){Restore();}
|
|
||||||
void Restore() noexcept
|
|
||||||
{if(active){CurrentContext.stage=previous;active=false;}}
|
|
||||||
bool active=false;EStage previous=EStage::Other;
|
|
||||||
};
|
|
||||||
struct FScopedPhase
|
|
||||||
{
|
|
||||||
explicit FScopedPhase(EPhase value) noexcept
|
|
||||||
:active(ThreadContextActive),previous(CurrentContext.phase)
|
|
||||||
{if(active)CurrentContext.phase=value;}
|
|
||||||
~FScopedPhase(){if(active)CurrentContext.phase=previous;}
|
|
||||||
bool active=false;EPhase previous=EPhase::Dispatch;
|
|
||||||
};
|
|
||||||
struct FScopedLod
|
|
||||||
{
|
|
||||||
explicit FScopedLod(int value) noexcept
|
|
||||||
:active(IsEnabled()),previous(CurrentContext.lod),previousContextActive(ThreadContextActive)
|
|
||||||
{
|
|
||||||
if(active)
|
|
||||||
{
|
|
||||||
ThreadContextActive=true;
|
|
||||||
RegisterThread();
|
|
||||||
CurrentContext.lod=static_cast<std::uint8_t>(value<0||value>8?0xff:value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
~FScopedLod()
|
|
||||||
{if(active){CurrentContext.lod=previous;ThreadContextActive=previousContextActive;}}
|
|
||||||
bool active=false;std::uint8_t previous=0xff;
|
|
||||||
bool previousContextActive=false;
|
|
||||||
};
|
|
||||||
struct FScopedHalo
|
|
||||||
{
|
|
||||||
explicit FScopedHalo(bool value) noexcept
|
|
||||||
:active(ThreadContextActive),previous(CurrentContext.halo)
|
|
||||||
{if(active)CurrentContext.halo=value;}
|
|
||||||
~FScopedHalo(){if(active)CurrentContext.halo=previous;}
|
|
||||||
bool active=false;bool previous=false;
|
|
||||||
};
|
|
||||||
#else
|
|
||||||
inline bool IsEnabled() noexcept{return false;}
|
|
||||||
inline void SetEnabled(bool) noexcept{}
|
|
||||||
inline void RegisterThread(){}
|
|
||||||
inline void Start(std::uint32_t=1000){}
|
|
||||||
inline void StopAndPrint(){}
|
|
||||||
struct FScopedRoute{explicit FScopedRoute(ERoute) noexcept{}};
|
|
||||||
struct FScopedStage
|
|
||||||
{explicit FScopedStage(EStage) noexcept{}void Restore() noexcept{}};
|
|
||||||
struct FScopedPhase{explicit FScopedPhase(EPhase) noexcept{}};
|
|
||||||
struct FScopedLod{explicit FScopedLod(int) noexcept{}};
|
|
||||||
struct FScopedHalo{explicit FScopedHalo(bool) noexcept{}};
|
|
||||||
#endif
|
|
||||||
|
|
||||||
struct FDispatchCounter
|
|
||||||
{
|
|
||||||
std::uint64_t calls=0;
|
|
||||||
std::uint64_t points=0;
|
|
||||||
};
|
|
||||||
using FDispatchCounters=std::array<std::array<FDispatchCounter,RouteCount>,StageCount>;
|
|
||||||
|
|
||||||
inline const char* RouteName(ERoute value) noexcept
|
|
||||||
{
|
|
||||||
switch(value)
|
|
||||||
{
|
|
||||||
case ERoute::Dispatch:return "dispatch_lookup";
|
|
||||||
case ERoute::PackedScalar:return "packed_scalar";
|
|
||||||
case ERoute::PackedX8:return "packed_x8";
|
|
||||||
case ERoute::AnalyticScalar:return "analytic_scalar";
|
|
||||||
case ERoute::AnalyticBatch:return "analytic_batch";
|
|
||||||
case ERoute::Native:return "native";
|
|
||||||
case ERoute::LegacySurface:return "legacy_surface";
|
|
||||||
case ERoute::SharedSource:return "shared_source";
|
|
||||||
default:return "unknown";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
inline const char* StageName(EStage value) noexcept
|
|
||||||
{
|
|
||||||
switch(value)
|
|
||||||
{
|
|
||||||
case EStage::GridFill:return "grid_fill";
|
|
||||||
case EStage::MarchingCubes:return "marching_cubes";
|
|
||||||
case EStage::Skirt:return "skirt";
|
|
||||||
case EStage::Refine:return "refine";
|
|
||||||
default:return "other";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
inline const char* PhaseName(EPhase value) noexcept
|
|
||||||
{
|
|
||||||
switch(value)
|
|
||||||
{
|
|
||||||
case EPhase::Dispatch:return "dispatch_lookup";
|
|
||||||
case EPhase::BaseField:return "base_field";
|
|
||||||
case EPhase::CompositionGraph:return "composition_graph";
|
|
||||||
case EPhase::Bands:return "composition_bands";
|
|
||||||
case EPhase::Passage:return "passages";
|
|
||||||
case EPhase::Support:return "support";
|
|
||||||
case EPhase::Caps:return "caps";
|
|
||||||
case EPhase::Stamps:return "stamps";
|
|
||||||
case EPhase::Edit:return "edits";
|
|
||||||
default:return "other";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,255 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <array>
|
|
||||||
#include <cmath>
|
|
||||||
#include <cstddef>
|
|
||||||
#include <limits>
|
|
||||||
#include <span>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace VoxelForgeCore::PassageSpline
|
|
||||||
{
|
|
||||||
using FPoint = std::array<double,4>;
|
|
||||||
inline constexpr std::size_t ArcTableSteps = 32;
|
|
||||||
inline constexpr double UpperMouthFloorBlendRadiusMetres = 3.5;
|
|
||||||
|
|
||||||
struct FSegment
|
|
||||||
{
|
|
||||||
FPoint a{},b{};
|
|
||||||
double alongMetres=0.0,lengthMetres=0.0;
|
|
||||||
std::size_t controlSpan=0;
|
|
||||||
double tA=0.0,tB=1.0;
|
|
||||||
double plannedFloorA=0.0,plannedFloorB=0.0;
|
|
||||||
};
|
|
||||||
|
|
||||||
inline double KnotSpan(const FPoint& a,const FPoint& b,double unitsPerMetre)
|
|
||||||
{
|
|
||||||
const double dx=(b[0]-a[0])/unitsPerMetre;
|
|
||||||
const double dy=(b[1]-a[1])/unitsPerMetre;
|
|
||||||
return std::max(1.0e-6,std::sqrt(std::sqrt(dx*dx+dy*dy)));
|
|
||||||
}
|
|
||||||
|
|
||||||
inline FPoint EvaluateRaw(std::span<const FPoint> controls,std::size_t span,
|
|
||||||
double t,double unitsPerMetre)
|
|
||||||
{
|
|
||||||
const auto& p1=controls[span];
|
|
||||||
const auto& p2=controls[span+1];
|
|
||||||
const auto& p0=controls[span==0?0:span-1];
|
|
||||||
const auto& p3=controls[std::min(span+2,controls.size()-1)];
|
|
||||||
const double dt0=KnotSpan(p0,p1,unitsPerMetre);
|
|
||||||
const double dt1=KnotSpan(p1,p2,unitsPerMetre);
|
|
||||||
const double dt2=KnotSpan(p2,p3,unitsPerMetre);
|
|
||||||
const double t2=t*t,t3=t2*t;
|
|
||||||
FPoint result{};
|
|
||||||
for(const std::size_t axis:{0u,1u,3u})
|
|
||||||
{
|
|
||||||
const double m1=dt1*((p1[axis]-p0[axis])/dt0
|
|
||||||
-(p2[axis]-p0[axis])/(dt0+dt1)
|
|
||||||
+(p2[axis]-p1[axis])/dt1);
|
|
||||||
const double m2=dt1*((p2[axis]-p1[axis])/dt1
|
|
||||||
-(p3[axis]-p1[axis])/(dt1+dt2)
|
|
||||||
+(p3[axis]-p2[axis])/dt2);
|
|
||||||
result[axis]=(2.0*t3-3.0*t2+1.0)*p1[axis]
|
|
||||||
+(-2.0*t3+3.0*t2)*p2[axis]
|
|
||||||
+(t3-2.0*t2+t)*m1+(t3-t2)*m2;
|
|
||||||
}
|
|
||||||
result[2]=std::lerp(p1[2],p2[2],t);
|
|
||||||
result[3]=std::clamp(result[3],std::min(p1[3],p2[3]),
|
|
||||||
std::max(p1[3],p2[3]));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool ClosestRouteHeightAtXY(std::span<const std::array<double,3>> route,
|
|
||||||
double x,double y,double& height,double& distanceSquared)
|
|
||||||
{
|
|
||||||
if(route.empty())return false;
|
|
||||||
distanceSquared=std::numeric_limits<double>::infinity();
|
|
||||||
const auto consider=[&](const std::array<double,3>& point)
|
|
||||||
{
|
|
||||||
const double dx=x-point[0],dy=y-point[1];
|
|
||||||
const double candidate=dx*dx+dy*dy;
|
|
||||||
if(candidate<distanceSquared)
|
|
||||||
{distanceSquared=candidate;height=point[2];}
|
|
||||||
};
|
|
||||||
if(route.size()==1)
|
|
||||||
{
|
|
||||||
consider(route.front());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
for(std::size_t i=1;i<route.size();++i)
|
|
||||||
{
|
|
||||||
const auto& a=route[i-1];
|
|
||||||
const auto& b=route[i];
|
|
||||||
const double dx=b[0]-a[0],dy=b[1]-a[1];
|
|
||||||
const double lengthSquared=dx*dx+dy*dy;
|
|
||||||
const double t=lengthSquared>1.0e-12
|
|
||||||
?std::clamp(((x-a[0])*dx+(y-a[1])*dy)/lengthSquared,0.0,1.0):0.0;
|
|
||||||
const std::array<double,3> point={a[0]+t*dx,a[1]+t*dy,
|
|
||||||
std::lerp(a[2],b[2],t)};
|
|
||||||
consider(point);
|
|
||||||
}
|
|
||||||
return std::isfinite(distanceSquared);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline double BlendUpperMouthFloorAtXY(
|
|
||||||
std::span<const std::array<double,3>> receivingRoute,double x,double y,
|
|
||||||
double plannedFloor,double mouthX,double mouthY,double mouthFloor,
|
|
||||||
double tangentX,double tangentY,
|
|
||||||
double blendRadiusMetres,double playerCenterAboveFloorMetres,
|
|
||||||
double unitsPerMetre=1.0)
|
|
||||||
{
|
|
||||||
if(receivingRoute.empty()||!(blendRadiusMetres>0.0)
|
|
||||||
||!(unitsPerMetre>0.0))return plannedFloor;
|
|
||||||
const double dx=(x-mouthX)/unitsPerMetre;
|
|
||||||
const double dy=(y-mouthY)/unitsPerMetre;
|
|
||||||
if(dx*tangentX+dy*tangentY>=0.0)return plannedFloor;
|
|
||||||
const double dz=(plannedFloor-mouthFloor)/unitsPerMetre;
|
|
||||||
const double distance=std::sqrt(dx*dx+dy*dy+dz*dz);
|
|
||||||
double routeHeight=0.0,routeDistanceSquared=0.0;
|
|
||||||
if(distance>blendRadiusMetres
|
|
||||||
||!ClosestRouteHeightAtXY(receivingRoute,x,y,routeHeight,
|
|
||||||
routeDistanceSquared)
|
|
||||||
||routeDistanceSquared>std::pow(2.0*blendRadiusMetres*unitsPerMetre,2.0))
|
|
||||||
return plannedFloor;
|
|
||||||
const double t=std::clamp(distance/blendRadiusMetres,0.0,1.0);
|
|
||||||
const double blend=t*t*(3.0-2.0*t);
|
|
||||||
const double receivingFloor=routeHeight
|
|
||||||
-playerCenterAboveFloorMetres*unitsPerMetre;
|
|
||||||
return std::lerp(plannedFloor,receivingFloor,blend);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline std::vector<FSegment> BuildSegments(std::span<const FPoint> controls,
|
|
||||||
std::size_t spanCount,double spacingMetres,double unitsPerMetre=1.0,
|
|
||||||
std::span<const double> plannedFloorProfile={},
|
|
||||||
double maximumChordErrorMetres=0.0)
|
|
||||||
{
|
|
||||||
std::vector<FSegment> result;
|
|
||||||
if(controls.size()<2||!(spacingMetres>0.0)||!(unitsPerMetre>0.0))return result;
|
|
||||||
spanCount=std::min(spanCount,controls.size()-1);
|
|
||||||
double alongMetres=0.0;
|
|
||||||
for(std::size_t span=0;span<spanCount;++span)
|
|
||||||
{
|
|
||||||
std::array<double,ArcTableSteps+1> horizontalArc{};
|
|
||||||
auto previous=EvaluateRaw(controls,span,0.0,unitsPerMetre);
|
|
||||||
for(std::size_t step=1;step<=ArcTableSteps;++step)
|
|
||||||
{
|
|
||||||
const auto current=EvaluateRaw(controls,span,
|
|
||||||
static_cast<double>(step)/ArcTableSteps,unitsPerMetre);
|
|
||||||
const double dx=(current[0]-previous[0])/unitsPerMetre;
|
|
||||||
const double dy=(current[1]-previous[1])/unitsPerMetre;
|
|
||||||
horizontalArc[step]=horizontalArc[step-1]+std::sqrt(dx*dx+dy*dy);
|
|
||||||
previous=current;
|
|
||||||
}
|
|
||||||
const double spanLength=horizontalArc.back();
|
|
||||||
const std::size_t baseSegmentCount=std::max<std::size_t>(1,
|
|
||||||
static_cast<std::size_t>(std::ceil(spanLength/spacingMetres)));
|
|
||||||
std::vector<FPoint> points;
|
|
||||||
std::vector<double> parameters;
|
|
||||||
points.reserve(baseSegmentCount+1);
|
|
||||||
parameters.reserve(baseSegmentCount+1);
|
|
||||||
const auto parameterAtArc=[&](double targetArc)
|
|
||||||
{
|
|
||||||
const auto upper=std::lower_bound(horizontalArc.begin()+1,
|
|
||||||
horizontalArc.end(),targetArc);
|
|
||||||
const std::size_t upperIndex=static_cast<std::size_t>(
|
|
||||||
upper-horizontalArc.begin());
|
|
||||||
const std::size_t lowerIndex=upperIndex-1;
|
|
||||||
const double arcSpan=horizontalArc[upperIndex]-horizontalArc[lowerIndex];
|
|
||||||
const double arcT=arcSpan>1.0e-12
|
|
||||||
?std::clamp((targetArc-horizontalArc[lowerIndex])/arcSpan,0.0,1.0):0.0;
|
|
||||||
return (static_cast<double>(lowerIndex)+arcT)/ArcTableSteps;
|
|
||||||
};
|
|
||||||
parameters.push_back(0.0);
|
|
||||||
const double chordError=std::max(0.0,maximumChordErrorMetres);
|
|
||||||
const auto appendAdaptive=[&](auto&& self,double tA,double tB,
|
|
||||||
const FPoint& a,const FPoint& b,unsigned depth)->void
|
|
||||||
{
|
|
||||||
constexpr std::array<double,3> CheckFractions={0.25,0.5,0.75};
|
|
||||||
double maximumError=0.0,polylineLength=0.0;
|
|
||||||
FPoint previous=a;
|
|
||||||
for(const double fraction:CheckFractions)
|
|
||||||
{
|
|
||||||
const FPoint current=EvaluateRaw(controls,span,
|
|
||||||
std::lerp(tA,tB,fraction),unitsPerMetre);
|
|
||||||
const double chordX=std::lerp(a[0],b[0],fraction);
|
|
||||||
const double chordY=std::lerp(a[1],b[1],fraction);
|
|
||||||
const double dx=(current[0]-chordX)/unitsPerMetre;
|
|
||||||
const double dy=(current[1]-chordY)/unitsPerMetre;
|
|
||||||
const double dr=(current[3]-std::lerp(a[3],b[3],fraction))
|
|
||||||
/unitsPerMetre;
|
|
||||||
maximumError=std::max(maximumError,
|
|
||||||
std::sqrt(dx*dx+dy*dy+dr*dr));
|
|
||||||
const double segmentX=(current[0]-previous[0])/unitsPerMetre;
|
|
||||||
const double segmentY=(current[1]-previous[1])/unitsPerMetre;
|
|
||||||
polylineLength+=std::sqrt(segmentX*segmentX+segmentY*segmentY);
|
|
||||||
previous=current;
|
|
||||||
}
|
|
||||||
const double finalX=(b[0]-previous[0])/unitsPerMetre;
|
|
||||||
const double finalY=(b[1]-previous[1])/unitsPerMetre;
|
|
||||||
polylineLength+=std::sqrt(finalX*finalX+finalY*finalY);
|
|
||||||
const bool split=(polylineLength>spacingMetres*(1.0+1.0e-9)
|
|
||||||
||(chordError>0.0&&maximumError>chordError))&&depth<12;
|
|
||||||
if(split)
|
|
||||||
{
|
|
||||||
const double tMid=(tA+tB)*0.5;
|
|
||||||
const FPoint middle=EvaluateRaw(controls,span,tMid,unitsPerMetre);
|
|
||||||
self(self,tA,tMid,a,middle,depth+1);
|
|
||||||
self(self,tMid,tB,middle,b,depth+1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
parameters.push_back(tB);
|
|
||||||
};
|
|
||||||
for(std::size_t segment=1;segment<=baseSegmentCount;++segment)
|
|
||||||
{
|
|
||||||
const double targetA=spanLength*static_cast<double>(segment-1)
|
|
||||||
/baseSegmentCount;
|
|
||||||
const double targetB=spanLength*static_cast<double>(segment)
|
|
||||||
/baseSegmentCount;
|
|
||||||
const double tA=parameterAtArc(targetA);
|
|
||||||
const double tB=parameterAtArc(targetB);
|
|
||||||
const FPoint a=EvaluateRaw(controls,span,tA,unitsPerMetre);
|
|
||||||
const FPoint b=EvaluateRaw(controls,span,tB,unitsPerMetre);
|
|
||||||
appendAdaptive(appendAdaptive,tA,tB,a,b,0);
|
|
||||||
}
|
|
||||||
points.reserve(parameters.size());
|
|
||||||
for(const double t:parameters)
|
|
||||||
points.push_back(EvaluateRaw(controls,span,t,unitsPerMetre));
|
|
||||||
std::vector<double> sampledHorizontalArc(points.size(),0.0);
|
|
||||||
for(std::size_t i=1;i<points.size();++i)
|
|
||||||
{
|
|
||||||
const double dx=(points[i][0]-points[i-1][0])/unitsPerMetre;
|
|
||||||
const double dy=(points[i][1]-points[i-1][1])/unitsPerMetre;
|
|
||||||
sampledHorizontalArc[i]=sampledHorizontalArc[i-1]
|
|
||||||
+std::sqrt(dx*dx+dy*dy);
|
|
||||||
}
|
|
||||||
const double sampledSpanLength=sampledHorizontalArc.back();
|
|
||||||
for(std::size_t i=0;i<points.size();++i)
|
|
||||||
{
|
|
||||||
const double progress=sampledSpanLength>1.0e-12
|
|
||||||
?sampledHorizontalArc[i]/sampledSpanLength:parameters[i];
|
|
||||||
points[i][2]=std::lerp(controls[span][2],controls[span+1][2],progress);
|
|
||||||
}
|
|
||||||
for(std::size_t i=1;i<points.size();++i)
|
|
||||||
{
|
|
||||||
const double dx=(points[i][0]-points[i-1][0])/unitsPerMetre;
|
|
||||||
const double dy=(points[i][1]-points[i-1][1])/unitsPerMetre;
|
|
||||||
const double dz=(points[i][2]-points[i-1][2])/unitsPerMetre;
|
|
||||||
const double length=std::sqrt(dx*dx+dy*dy+dz*dz);
|
|
||||||
const auto floorAt=[&](std::size_t index)
|
|
||||||
{
|
|
||||||
if(plannedFloorProfile.size()!=controls.size())
|
|
||||||
return points[index][2];
|
|
||||||
const double progress=sampledSpanLength>1.0e-12
|
|
||||||
?sampledHorizontalArc[index]/sampledSpanLength:parameters[index];
|
|
||||||
return std::lerp(plannedFloorProfile[span],
|
|
||||||
plannedFloorProfile[span+1],progress);
|
|
||||||
};
|
|
||||||
result.push_back({points[i-1],points[i],alongMetres,length,span,
|
|
||||||
parameters[i-1],parameters[i],floorAt(i-1),floorAt(i)});
|
|
||||||
alongMetres+=length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "VoxelForgeCore/CpuCycleCounter.h"
|
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <chrono>
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
@@ -15,23 +12,6 @@
|
|||||||
|
|
||||||
namespace VoxelForgeCoreShared
|
namespace VoxelForgeCoreShared
|
||||||
{
|
{
|
||||||
struct FColumnCacheMetrics
|
|
||||||
{
|
|
||||||
std::uint64_t hits = 0;
|
|
||||||
std::uint64_t misses = 0;
|
|
||||||
std::uint64_t computes = 0;
|
|
||||||
std::uint64_t conflictReplacements = 0;
|
|
||||||
std::uint64_t lockWaitNanoseconds = 0;
|
|
||||||
std::uint64_t sampledInitialReadCycles = 0;
|
|
||||||
std::uint64_t sampledInitialReads = 0;
|
|
||||||
std::uint64_t sampledLockedReadCycles = 0;
|
|
||||||
std::uint64_t sampledLockedReads = 0;
|
|
||||||
std::uint64_t sampledLockCycles = 0;
|
|
||||||
std::uint64_t sampledLocks = 0;
|
|
||||||
std::uint64_t sampledBuildCycles = 0;
|
|
||||||
std::uint64_t sampledBuilds = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Exact input identity for a pure XY column value. LOD is deliberately not part of this key:
|
// Exact input identity for a pure XY column value. LOD is deliberately not part of this key:
|
||||||
// the same recipe/plan column has the same bits at every sampling step.
|
// the same recipe/plan column has the same bits at every sampling step.
|
||||||
struct FColumnCacheKey
|
struct FColumnCacheKey
|
||||||
@@ -89,28 +69,13 @@ namespace VoxelForgeCoreShared
|
|||||||
|
|
||||||
struct FSpinGuard
|
struct FSpinGuard
|
||||||
{
|
{
|
||||||
explicit FSpinGuard(std::atomic_flag& value, FColumnCacheMetrics* metrics,
|
explicit FSpinGuard(std::atomic_flag& value) : Flag(value)
|
||||||
bool sampleCycles)
|
|
||||||
: Flag(value), Metrics(metrics)
|
|
||||||
{
|
{
|
||||||
const auto started = Metrics ? std::chrono::steady_clock::now()
|
|
||||||
: std::chrono::steady_clock::time_point{};
|
|
||||||
const std::uint64_t cycleStart=sampleCycles?ReadCpuCycles():0;
|
|
||||||
while (Flag.test_and_set(std::memory_order_acquire))
|
while (Flag.test_and_set(std::memory_order_acquire))
|
||||||
std::this_thread::yield();
|
std::this_thread::yield();
|
||||||
if (Metrics)
|
|
||||||
Metrics->lockWaitNanoseconds += static_cast<std::uint64_t>(
|
|
||||||
std::chrono::duration_cast<std::chrono::nanoseconds>(
|
|
||||||
std::chrono::steady_clock::now() - started).count());
|
|
||||||
if(sampleCycles)
|
|
||||||
{
|
|
||||||
Metrics->sampledLockCycles+=AdjustedCpuCycleDelta(cycleStart,ReadCpuCycles());
|
|
||||||
++Metrics->sampledLocks;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
~FSpinGuard() { Flag.clear(std::memory_order_release); }
|
~FSpinGuard() { Flag.clear(std::memory_order_release); }
|
||||||
std::atomic_flag& Flag;
|
std::atomic_flag& Flag;
|
||||||
FColumnCacheMetrics* Metrics;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -129,7 +94,7 @@ namespace VoxelForgeCoreShared
|
|||||||
|
|
||||||
template <typename TFactory>
|
template <typename TFactory>
|
||||||
TValue getOrCompute(const FColumnCacheKey& key, TFactory&& factory,
|
TValue getOrCompute(const FColumnCacheKey& key, TFactory&& factory,
|
||||||
bool& computed, FColumnCacheMetrics* metrics = nullptr)
|
bool& computed)
|
||||||
{
|
{
|
||||||
computed = false;
|
computed = false;
|
||||||
const FKeyWords keyWords = key.words();
|
const FKeyWords keyWords = key.words();
|
||||||
@@ -137,47 +102,13 @@ namespace VoxelForgeCoreShared
|
|||||||
const std::size_t setIndex = static_cast<std::size_t>(tag) & (SetCount - 1);
|
const std::size_t setIndex = static_cast<std::size_t>(tag) & (SetCount - 1);
|
||||||
FSet& set = Sets[setIndex];
|
FSet& set = Sets[setIndex];
|
||||||
TValue value{};
|
TValue value{};
|
||||||
static thread_local std::uint64_t timingSequence=0;
|
if (TryReadSet(set, keyWords, tag, value)) return value;
|
||||||
const bool sampleCycles=metrics&&((++timingSequence&127u)==0);
|
|
||||||
const std::uint64_t initialReadStart=sampleCycles?ReadCpuCycles():0;
|
|
||||||
const bool initialHit=TryReadSet(set, keyWords, tag, value);
|
|
||||||
if(sampleCycles)
|
|
||||||
{
|
|
||||||
metrics->sampledInitialReadCycles+=AdjustedCpuCycleDelta(
|
|
||||||
initialReadStart,ReadCpuCycles());
|
|
||||||
++metrics->sampledInitialReads;
|
|
||||||
}
|
|
||||||
if (initialHit)
|
|
||||||
{
|
|
||||||
if (metrics) ++metrics->hits;
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
if (metrics) ++metrics->misses;
|
|
||||||
|
|
||||||
FSpinGuard lock(set.Writer, metrics,sampleCycles);
|
FSpinGuard lock(set.Writer);
|
||||||
const std::uint64_t lockedReadStart=sampleCycles?ReadCpuCycles():0;
|
if (TryReadSet(set, keyWords, tag, value)) return value;
|
||||||
const bool lockedHit=TryReadSet(set, keyWords, tag, value);
|
|
||||||
if(sampleCycles)
|
|
||||||
{
|
|
||||||
metrics->sampledLockedReadCycles+=AdjustedCpuCycleDelta(
|
|
||||||
lockedReadStart,ReadCpuCycles());
|
|
||||||
++metrics->sampledLockedReads;
|
|
||||||
}
|
|
||||||
if (lockedHit)
|
|
||||||
{
|
|
||||||
if (metrics) ++metrics->hits;
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::uint64_t buildStart=sampleCycles?ReadCpuCycles():0;
|
|
||||||
value = std::forward<TFactory>(factory)();
|
value = std::forward<TFactory>(factory)();
|
||||||
if(sampleCycles)
|
|
||||||
{
|
|
||||||
metrics->sampledBuildCycles+=AdjustedCpuCycleDelta(buildStart,ReadCpuCycles());
|
|
||||||
++metrics->sampledBuilds;
|
|
||||||
}
|
|
||||||
computed = true;
|
computed = true;
|
||||||
if (metrics) ++metrics->computes;
|
|
||||||
FValueWords valueWords{};
|
FValueWords valueWords{};
|
||||||
std::memcpy(valueWords.data(), &value, sizeof(TValue));
|
std::memcpy(valueWords.data(), &value, sizeof(TValue));
|
||||||
|
|
||||||
@@ -187,7 +118,6 @@ namespace VoxelForgeCoreShared
|
|||||||
{ replacement = way; break; }
|
{ replacement = way; break; }
|
||||||
if (replacement == Ways)
|
if (replacement == Ways)
|
||||||
{
|
{
|
||||||
if (metrics) ++metrics->conflictReplacements;
|
|
||||||
replacement = set.NextWay;
|
replacement = set.NextWay;
|
||||||
set.NextWay = static_cast<std::uint8_t>((set.NextWay + 1) % Ways);
|
set.NextWay = static_cast<std::uint8_t>((set.NextWay + 1) % Ways);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -1,9 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
// Core implementation TUs opt into the standalone Unreal-shaped compatibility headers. UE's
|
|
||||||
// Linux-generated module TUs leave this unset so those same header names forward to the engine.
|
|
||||||
#define VFCORE_USE_COMPAT 1
|
|
||||||
|
|
||||||
// Every standard or platform header used by the copied implementation must be included before
|
// Every standard or platform header used by the copied implementation must be included before
|
||||||
// its wrapper opens vfcore::detail. This prevents system declarations from being nested in the
|
// its wrapper opens vfcore::detail. This prevents system declarations from being nested in the
|
||||||
// private namespace when the implementation includes its Unreal-shaped compatibility headers.
|
// private namespace when the implementation includes its Unreal-shaped compatibility headers.
|
||||||
@@ -60,7 +56,6 @@
|
|||||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||||
#include "VoxelForgeCore/ThreadLocalStorage.h"
|
#include "VoxelForgeCore/ThreadLocalStorage.h"
|
||||||
#include "VoxelForgeCore/SharedColumnCache.h"
|
#include "VoxelForgeCore/SharedColumnCache.h"
|
||||||
#include "VoxelForgeCore/FloatingIslandInstrumentation.h"
|
|
||||||
|
|
||||||
// Keep Core's expression-level FMA behavior fixed regardless of the toolchain setting.
|
// Keep Core's expression-level FMA behavior fixed regardless of the toolchain setting.
|
||||||
// Clang's "fast" mode ignores the pragma, so Core's runtime self-test rejects that build.
|
// Clang's "fast" mode ignores the pragma, so Core's runtime self-test rejects that build.
|
||||||
|
|||||||
@@ -6,10 +6,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <bit>
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdio>
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@@ -157,8 +154,6 @@ inline bool ContainsFarTilePoint(const FVoxelTileKey& Key,const FVector& P)
|
|||||||
inline bool FindFarConnectorSurfaceRadius(const FStandaloneShapeWorld& Shape,const FVector& Center,
|
inline bool FindFarConnectorSurfaceRadius(const FStandaloneShapeWorld& Shape,const FVector& Center,
|
||||||
const FVector& Direction,double PreferredRadius,double& OutRadius)
|
const FVector& Direction,double PreferredRadius,double& OutRadius)
|
||||||
{
|
{
|
||||||
FStandaloneShapeWorld::FScopedPassageDensityQueryCaller queryCaller(
|
|
||||||
EPassageDensityQueryCaller::FarShellSurfaceQuery);
|
|
||||||
const double SearchLength=PreferredRadius*2.0+32.0;
|
const double SearchLength=PreferredRadius*2.0+32.0;
|
||||||
const double AcceptBand=std::max(16.0,PreferredRadius*0.75);
|
const double AcceptBand=std::max(16.0,PreferredRadius*0.75);
|
||||||
constexpr double Stride=8.0;
|
constexpr double Stride=8.0;
|
||||||
@@ -232,8 +227,6 @@ inline bool FindFarConnectorSurfaceRadius(const FStandaloneShapeWorld& Shape,con
|
|||||||
inline bool FindFarConnectorRadiusNearNominal(const FStandaloneShapeWorld& Shape,const FVector& Center,
|
inline bool FindFarConnectorRadiusNearNominal(const FStandaloneShapeWorld& Shape,const FVector& Center,
|
||||||
const FVector& Direction,double NominalRadius,double& OutRadius,double& OutUncertainty)
|
const FVector& Direction,double NominalRadius,double& OutRadius,double& OutUncertainty)
|
||||||
{
|
{
|
||||||
FStandaloneShapeWorld::FScopedPassageDensityQueryCaller queryCaller(
|
|
||||||
EPassageDensityQueryCaller::FarShellSurfaceQuery);
|
|
||||||
const double MatchTolerance=std::max(8.0,NominalRadius*0.35);
|
const double MatchTolerance=std::max(8.0,NominalRadius*0.35);
|
||||||
const double HalfWidth=std::min(16.0,MatchTolerance*0.75);
|
const double HalfWidth=std::min(16.0,MatchTolerance*0.75);
|
||||||
const double Lo=std::max(0.0,NominalRadius-HalfWidth),Hi=NominalRadius+HalfWidth;
|
const double Lo=std::max(0.0,NominalRadius-HalfWidth),Hi=NominalRadius+HalfWidth;
|
||||||
@@ -252,8 +245,6 @@ inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FV
|
|||||||
int32 Step,int32 Cells,FVoxelMeshData& Mesh)
|
int32 Step,int32 Cells,FVoxelMeshData& Mesh)
|
||||||
{
|
{
|
||||||
if(Key.Level<6||Step<=0)return;
|
if(Key.Level<6||Step<=0)return;
|
||||||
FStandaloneShapeWorld::FScopedPassageDensityQueryCaller queryCaller(
|
|
||||||
EPassageDensityQueryCaller::FarShellProjectionNormal);
|
|
||||||
// Four axial panels are the coarsest cross-section the survival audit samples.
|
// Four axial panels are the coarsest cross-section the survival audit samples.
|
||||||
constexpr int32 SideCount=4;
|
constexpr int32 SideCount=4;
|
||||||
const FIntVector TileOrigin=Key.OriginVoxels();
|
const FIntVector TileOrigin=Key.OriginVoxels();
|
||||||
@@ -826,18 +817,10 @@ inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FV
|
|||||||
struct FFarMeshColumnIndex
|
struct FFarMeshColumnIndex
|
||||||
{
|
{
|
||||||
FFarMeshColumnIndex(const FVoxelMeshData& InMesh,const FIntVector& InOrigin,int32 InExtent,
|
FFarMeshColumnIndex(const FVoxelMeshData& InMesh,const FIntVector& InOrigin,int32 InExtent,
|
||||||
int32 InCells,double InStep,double InVoxelSizeCm,int32 InLevel)
|
int32 InCells,double InStep)
|
||||||
:Mesh(InMesh),Origin(InOrigin),Extent(InExtent),Cells(InCells),Step(InStep),
|
:Mesh(InMesh),Origin(InOrigin),Extent(InExtent),Cells(InCells),Step(InStep),
|
||||||
VoxelSizeCm(InVoxelSizeCm),
|
ByColumn(static_cast<size_t>(InCells)*InCells)
|
||||||
Level(InLevel),ByColumn(static_cast<size_t>(InCells)*InCells),
|
|
||||||
bUseHitIndex(UseHitIndex()),bMeasureHitStats(MeasureHitStats())
|
|
||||||
{
|
{
|
||||||
if(bUseHitIndex)
|
|
||||||
{
|
|
||||||
const size_t CellCount=static_cast<size_t>(Cells)*Cells*Cells;
|
|
||||||
ByDownwardCell.resize(CellCount);
|
|
||||||
ByUpwardCell.resize(CellCount);
|
|
||||||
}
|
|
||||||
for(int32 Triangle=0;Triangle+2<Mesh.Triangles.Num();Triangle+=3)
|
for(int32 Triangle=0;Triangle+2<Mesh.Triangles.Num();Triangle+=3)
|
||||||
{
|
{
|
||||||
const int32 A=Mesh.Triangles[Triangle],B=Mesh.Triangles[Triangle+1],C=Mesh.Triangles[Triangle+2];
|
const int32 A=Mesh.Triangles[Triangle],B=Mesh.Triangles[Triangle+1],C=Mesh.Triangles[Triangle+2];
|
||||||
@@ -846,46 +829,14 @@ struct FFarMeshColumnIndex
|
|||||||
const double MaxX=std::max({PositionAxis(A,0),PositionAxis(B,0),PositionAxis(C,0)});
|
const double MaxX=std::max({PositionAxis(A,0),PositionAxis(B,0),PositionAxis(C,0)});
|
||||||
const double MinY=std::min({PositionAxis(A,1),PositionAxis(B,1),PositionAxis(C,1)});
|
const double MinY=std::min({PositionAxis(A,1),PositionAxis(B,1),PositionAxis(C,1)});
|
||||||
const double MaxY=std::max({PositionAxis(A,1),PositionAxis(B,1),PositionAxis(C,1)});
|
const double MaxY=std::max({PositionAxis(A,1),PositionAxis(B,1),PositionAxis(C,1)});
|
||||||
const double MinZ=std::min({PositionAxis(A,2),PositionAxis(B,2),PositionAxis(C,2)});
|
|
||||||
const double MaxZ=std::max({PositionAxis(A,2),PositionAxis(B,2),PositionAxis(C,2)});
|
|
||||||
const int32 X0=std::clamp(static_cast<int32>(std::floor((MinX-Origin.X)/Step)),0,Cells-1);
|
const int32 X0=std::clamp(static_cast<int32>(std::floor((MinX-Origin.X)/Step)),0,Cells-1);
|
||||||
const int32 X1=std::clamp(static_cast<int32>(std::floor((MaxX-Origin.X)/Step)),0,Cells-1);
|
const int32 X1=std::clamp(static_cast<int32>(std::floor((MaxX-Origin.X)/Step)),0,Cells-1);
|
||||||
const int32 Y0=std::clamp(static_cast<int32>(std::floor((MinY-Origin.Y)/Step)),0,Cells-1);
|
const int32 Y0=std::clamp(static_cast<int32>(std::floor((MinY-Origin.Y)/Step)),0,Cells-1);
|
||||||
const int32 Y1=std::clamp(static_cast<int32>(std::floor((MaxY-Origin.Y)/Step)),0,Cells-1);
|
const int32 Y1=std::clamp(static_cast<int32>(std::floor((MaxY-Origin.Y)/Step)),0,Cells-1);
|
||||||
for(int32 Y=Y0;Y<=Y1;++Y)for(int32 X=X0;X<=X1;++X)
|
for(int32 Y=Y0;Y<=Y1;++Y)for(int32 X=X0;X<=X1;++X)
|
||||||
ByColumn[static_cast<size_t>(Y)*Cells+X].push_back(Triangle/3);
|
ByColumn[static_cast<size_t>(Y)*Cells+X].push_back(Triangle/3);
|
||||||
if(!bUseHitIndex||A>=Mesh.Normals.Num()||B>=Mesh.Normals.Num()
|
|
||||||
||C>=Mesh.Normals.Num())continue;
|
|
||||||
const double NormalSum=Mesh.Normals[A].Z+Mesh.Normals[B].Z+Mesh.Normals[C].Z;
|
|
||||||
const bool bDown=!(NormalSum>=-1.5),bUp=!(NormalSum<=1.5);
|
|
||||||
if(!bDown&&!bUp)continue;
|
|
||||||
const double ZMargin=(MaxZ-MinZ)*2.0e-7+1.0e-8;
|
|
||||||
const int32 Z0=std::clamp(static_cast<int32>(std::floor((MinZ-ZMargin-Origin.Z)/Step)),0,Cells-1);
|
|
||||||
const int32 Z1=std::clamp(static_cast<int32>(std::floor((MaxZ+ZMargin-Origin.Z)/Step)),0,Cells-1);
|
|
||||||
for(int32 Z=Z0;Z<=Z1;++Z)for(int32 Y=Y0;Y<=Y1;++Y)for(int32 X=X0;X<=X1;++X)
|
|
||||||
{
|
|
||||||
const size_t Cell=(static_cast<size_t>(Z)*Cells+Y)*Cells+X;
|
|
||||||
if(bDown)ByDownwardCell[Cell].push_back(Triangle/3);
|
|
||||||
if(bUp)ByUpwardCell[Cell].push_back(Triangle/3);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
~FFarMeshColumnIndex()
|
|
||||||
{
|
|
||||||
if(!bMeasureHitStats||DownQueries+UpQueries==0)return;
|
|
||||||
std::fprintf(stderr,
|
|
||||||
"[FarHitQueryStats] lod=%d origin=(%d,%d,%d) "
|
|
||||||
"down_queries=%llu down_candidate_tests=%llu down_hit_queries=%llu "
|
|
||||||
"up_queries=%llu up_candidate_tests=%llu up_hit_queries=%llu\n",
|
|
||||||
Level,Origin.X,Origin.Y,Origin.Z,
|
|
||||||
static_cast<unsigned long long>(DownQueries),
|
|
||||||
static_cast<unsigned long long>(DownCandidateTests),
|
|
||||||
static_cast<unsigned long long>(DownHitQueries),
|
|
||||||
static_cast<unsigned long long>(UpQueries),
|
|
||||||
static_cast<unsigned long long>(UpCandidateTests),
|
|
||||||
static_cast<unsigned long long>(UpHitQueries));
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<double> VerticalHits(double X,double Y,double ZMin,double ZMax) const
|
std::vector<double> VerticalHits(double X,double Y,double ZMin,double ZMax) const
|
||||||
{
|
{
|
||||||
@@ -913,164 +864,22 @@ struct FFarMeshColumnIndex
|
|||||||
return Hits;
|
return Hits;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool HasDownwardHit(double X,double Y,double Z,double Tolerance) const
|
|
||||||
{
|
|
||||||
if(bMeasureHitStats)++DownQueries;
|
|
||||||
std::vector<int32> CandidateScratch;
|
|
||||||
const auto& Candidates=FindHitCandidates(X,Y,Z,Tolerance,true,CandidateScratch);
|
|
||||||
bool bHit=false;
|
|
||||||
for(const int32 Triangle:Candidates)
|
|
||||||
{
|
|
||||||
if(bMeasureHitStats)++DownCandidateTests;
|
|
||||||
const int32 IA=Mesh.Triangles[Triangle*3],IB=Mesh.Triangles[Triangle*3+1],
|
|
||||||
IC=Mesh.Triangles[Triangle*3+2];
|
|
||||||
if(IA<0||IB<0||IC<0||IA>=Mesh.Normals.Num()||IB>=Mesh.Normals.Num()
|
|
||||||
||IC>=Mesh.Normals.Num())continue;
|
|
||||||
if(Mesh.Normals[IA].Z+Mesh.Normals[IB].Z+Mesh.Normals[IC].Z>=-1.5)continue;
|
|
||||||
const double AX=PositionAxis(IA,0),AY=PositionAxis(IA,1),AZ=PositionAxis(IA,2);
|
|
||||||
const double BX=PositionAxis(IB,0),BY=PositionAxis(IB,1),BZ=PositionAxis(IB,2);
|
|
||||||
const double CXv=PositionAxis(IC,0),CYv=PositionAxis(IC,1),CZ=PositionAxis(IC,2);
|
|
||||||
const double Den=(BX-AX)*(CYv-AY)-(BY-AY)*(CXv-AX);
|
|
||||||
if(std::abs(Den)<1.0e-10)continue;
|
|
||||||
const double U=((X-AX)*(CYv-AY)-(Y-AY)*(CXv-AX))/Den;
|
|
||||||
const double V=((BX-AX)*(Y-AY)-(BY-AY)*(X-AX))/Den;
|
|
||||||
const double W=1.0-U-V;
|
|
||||||
if(U<-1.0e-7||V<-1.0e-7||W<-1.0e-7)continue;
|
|
||||||
const double Hit=AZ+U*(BZ-AZ)+V*(CZ-AZ);
|
|
||||||
if(std::abs(Hit-Z)<=Tolerance){bHit=true;break;}
|
|
||||||
}
|
|
||||||
if(bMeasureHitStats&&bHit)++DownHitQueries;
|
|
||||||
return bHit;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool HasDownwardCellCoverage(double X0,double Y0,double X1,double Y1,
|
|
||||||
double Z,double Tolerance) const
|
|
||||||
{
|
|
||||||
// Probe the perimeter as well as the interior: sparse interior-only probes can
|
|
||||||
// incorrectly hide a missing ceiling quad at an MC grid vertex.
|
|
||||||
// Sonder le perimetre et l'interieur: des sondes seulement interieures peuvent
|
|
||||||
// masquer un carre de plafond manquant sur un sommet de la grille MC.
|
|
||||||
constexpr double Samples[5]={0.0,0.25,0.5,0.75,1.0};
|
|
||||||
for(const double V:Samples)for(const double U:Samples)
|
|
||||||
{
|
|
||||||
const double X=X0+(X1-X0)*U;
|
|
||||||
const double Y=Y0+(Y1-Y0)*V;
|
|
||||||
if(!HasDownwardHit(X,Y,Z,Tolerance))return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool HasUpwardHit(double X,double Y,double Z,double Tolerance) const
|
|
||||||
{
|
|
||||||
if(bMeasureHitStats)++UpQueries;
|
|
||||||
std::vector<int32> CandidateScratch;
|
|
||||||
const auto& Candidates=FindHitCandidates(X,Y,Z,Tolerance,false,CandidateScratch);
|
|
||||||
bool bHit=false;
|
|
||||||
for(const int32 Triangle:Candidates)
|
|
||||||
{
|
|
||||||
if(bMeasureHitStats)++UpCandidateTests;
|
|
||||||
const int32 IA=Mesh.Triangles[Triangle*3],IB=Mesh.Triangles[Triangle*3+1],
|
|
||||||
IC=Mesh.Triangles[Triangle*3+2];
|
|
||||||
if(IA<0||IB<0||IC<0||IA>=Mesh.Normals.Num()||IB>=Mesh.Normals.Num()
|
|
||||||
||IC>=Mesh.Normals.Num())continue;
|
|
||||||
if(Mesh.Normals[IA].Z+Mesh.Normals[IB].Z+Mesh.Normals[IC].Z<=1.5)continue;
|
|
||||||
const double AX=PositionAxis(IA,0),AY=PositionAxis(IA,1),AZ=PositionAxis(IA,2);
|
|
||||||
const double BX=PositionAxis(IB,0),BY=PositionAxis(IB,1),BZ=PositionAxis(IB,2);
|
|
||||||
const double CXv=PositionAxis(IC,0),CYv=PositionAxis(IC,1),CZ=PositionAxis(IC,2);
|
|
||||||
const double Den=(BX-AX)*(CYv-AY)-(BY-AY)*(CXv-AX);
|
|
||||||
if(std::abs(Den)<1.0e-10)continue;
|
|
||||||
const double U=((X-AX)*(CYv-AY)-(Y-AY)*(CXv-AX))/Den;
|
|
||||||
const double V=((BX-AX)*(Y-AY)-(BY-AY)*(X-AX))/Den;
|
|
||||||
const double W=1.0-U-V;
|
|
||||||
if(U<-1.0e-7||V<-1.0e-7||W<-1.0e-7)continue;
|
|
||||||
const double Hit=AZ+U*(BZ-AZ)+V*(CZ-AZ);
|
|
||||||
if(std::abs(Hit-Z)<=Tolerance){bHit=true;break;}
|
|
||||||
}
|
|
||||||
if(bMeasureHitStats&&bHit)++UpHitQueries;
|
|
||||||
return bHit;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool HasUpwardCellCoverage(double X0,double Y0,double X1,double Y1,
|
|
||||||
double Z,double Tolerance) const
|
|
||||||
{
|
|
||||||
constexpr double Samples[5]={0.0,0.25,0.5,0.75,1.0};
|
|
||||||
for(const double V:Samples)for(const double U:Samples)
|
|
||||||
if(!HasUpwardHit(X0+(X1-X0)*U,Y0+(Y1-Y0)*V,Z,Tolerance))return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const std::vector<int32>& FindHitCandidates(double X,double Y,double Z,double Tolerance,
|
|
||||||
bool bDown,std::vector<int32>& Scratch) const
|
|
||||||
{
|
|
||||||
const int32 CX=std::clamp(static_cast<int32>(std::floor((X-Origin.X)/Step)),0,Cells-1);
|
|
||||||
const int32 CY=std::clamp(static_cast<int32>(std::floor((Y-Origin.Y)/Step)),0,Cells-1);
|
|
||||||
const size_t Column=static_cast<size_t>(CY)*Cells+CX;
|
|
||||||
if(!bUseHitIndex||!std::isfinite(Tolerance)||Tolerance<0.0||Step<=0.0)
|
|
||||||
return ByColumn[Column];
|
|
||||||
const int32 Z0=std::clamp(static_cast<int32>(std::floor((Z-Tolerance-Origin.Z)/Step)),
|
|
||||||
0,Cells-1);
|
|
||||||
const int32 Z1=std::clamp(static_cast<int32>(std::floor((Z+Tolerance-Origin.Z)/Step)),
|
|
||||||
0,Cells-1);
|
|
||||||
const auto& Buckets=bDown?ByDownwardCell:ByUpwardCell;
|
|
||||||
const size_t First=(static_cast<size_t>(Z0)*Cells+CY)*Cells+CX;
|
|
||||||
if(Z0==Z1)return Buckets[First];
|
|
||||||
Scratch.clear();
|
|
||||||
for(int32 BucketZ=Z0;BucketZ<=Z1;++BucketZ)
|
|
||||||
{
|
|
||||||
const size_t Cell=(static_cast<size_t>(BucketZ)*Cells+CY)*Cells+CX;
|
|
||||||
const auto& Candidates=Buckets[Cell];
|
|
||||||
Scratch.insert(Scratch.end(),Candidates.begin(),Candidates.end());
|
|
||||||
}
|
|
||||||
std::sort(Scratch.begin(),Scratch.end());
|
|
||||||
Scratch.erase(std::unique(Scratch.begin(),Scratch.end()),Scratch.end());
|
|
||||||
return Scratch;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool UseHitIndex()
|
|
||||||
{
|
|
||||||
static const bool Enabled=[]()
|
|
||||||
{
|
|
||||||
const char* Value=std::getenv("VOXELFORGE_L104_FAR_HIT_CACHE");
|
|
||||||
return Value==nullptr||Value[0]!='0'||Value[1]!='\0';
|
|
||||||
}();
|
|
||||||
return Enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool MeasureHitStats()
|
|
||||||
{
|
|
||||||
static const bool Enabled=[]()
|
|
||||||
{
|
|
||||||
const char* Value=std::getenv("VOXELFORGE_L104_FAR_HIT_STATS");
|
|
||||||
return Value&&Value[0]=='1'&&Value[1]=='\0';
|
|
||||||
}();
|
|
||||||
return Enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
double PositionAxis(int32 Index,int32 Axis) const
|
double PositionAxis(int32 Index,int32 Axis) const
|
||||||
{
|
{
|
||||||
const FVector& P=Mesh.Vertices[Index];
|
const FVector& P=Mesh.Vertices[Index];
|
||||||
const double WorldCentimetres=Axis==0?P.X:(Axis==1?P.Y:P.Z);
|
return (Axis==0?P.X:(Axis==1?P.Y:P.Z))*0.04;
|
||||||
return WorldCentimetres/VoxelSizeCm;
|
|
||||||
}
|
}
|
||||||
const FVoxelMeshData& Mesh;
|
const FVoxelMeshData& Mesh;
|
||||||
FIntVector Origin;
|
FIntVector Origin;
|
||||||
int32 Extent=0,Cells=0;
|
int32 Extent=0,Cells=0;
|
||||||
double Step=1.0;
|
double Step=1.0;
|
||||||
double VoxelSizeCm=25.0;
|
|
||||||
int32 Level=0;
|
|
||||||
std::vector<std::vector<int32>> ByColumn;
|
std::vector<std::vector<int32>> ByColumn;
|
||||||
std::vector<std::vector<int32>> ByDownwardCell,ByUpwardCell;
|
|
||||||
bool bUseHitIndex=false,bMeasureHitStats=false;
|
|
||||||
mutable std::uint64_t DownQueries=0,DownCandidateTests=0,DownHitQueries=0;
|
|
||||||
mutable std::uint64_t UpQueries=0,UpCandidateTests=0,UpHitQueries=0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
inline std::vector<double> FindFarStratumCrossings(const FStandaloneShapeWorld& Shape,
|
inline std::vector<double> FindFarStratumCrossings(const FStandaloneShapeWorld& Shape,
|
||||||
double X,double Y,double ZMin,double ZMax)
|
double X,double Y,double ZMin,double ZMax)
|
||||||
{
|
{
|
||||||
FStandaloneShapeWorld::FScopedPassageDensityQueryCaller queryCaller(
|
|
||||||
EPassageDensityQueryCaller::FarShellSurfaceQuery);
|
|
||||||
std::vector<double> Crossings;
|
std::vector<double> Crossings;
|
||||||
if(ZMax<=ZMin)return Crossings;
|
if(ZMax<=ZMin)return Crossings;
|
||||||
double PreviousZ=ZMin;
|
double PreviousZ=ZMin;
|
||||||
@@ -1098,8 +907,6 @@ inline std::vector<double> FindFarStratumCrossings(const FStandaloneShapeWorld&
|
|||||||
inline bool FindNearestFarStratumCrossing(const FStandaloneShapeWorld& Shape,
|
inline bool FindNearestFarStratumCrossing(const FStandaloneShapeWorld& Shape,
|
||||||
double X,double Y,double ZMin,double ZMax,double Prediction,double& OutCrossing)
|
double X,double Y,double ZMin,double ZMax,double Prediction,double& OutCrossing)
|
||||||
{
|
{
|
||||||
FStandaloneShapeWorld::FScopedPassageDensityQueryCaller queryCaller(
|
|
||||||
EPassageDensityQueryCaller::FarShellSurfaceQuery);
|
|
||||||
if(ZMax<=ZMin||!std::isfinite(Prediction))return false;
|
if(ZMax<=ZMin||!std::isfinite(Prediction))return false;
|
||||||
const int32 Center=std::clamp(static_cast<int32>(std::floor(Prediction)),
|
const int32 Center=std::clamp(static_cast<int32>(std::floor(Prediction)),
|
||||||
static_cast<int32>(std::ceil(ZMin)),static_cast<int32>(std::floor(ZMax)));
|
static_cast<int32>(std::ceil(ZMin)),static_cast<int32>(std::floor(ZMax)));
|
||||||
@@ -1162,18 +969,16 @@ inline void AppendFarStratumShells(const FStandaloneOwnerWorld& World,const FSta
|
|||||||
const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh)
|
const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh)
|
||||||
{
|
{
|
||||||
if(Key.Level<7||Step<4||!World.Manager)return;
|
if(Key.Level<7||Step<4||!World.Manager)return;
|
||||||
FStandaloneShapeWorld::FScopedPassageDensityQueryCaller queryCaller(
|
|
||||||
EPassageDensityQueryCaller::FarShellStratumRefinement);
|
|
||||||
struct FBoundary { double Z=0.0,HalfWidth=8.0; };
|
struct FBoundary { double Z=0.0,HalfWidth=8.0; };
|
||||||
const auto& Layout=World.Manager->GetLayout();
|
const auto& Layout=World.Manager->GetLayout();
|
||||||
std::vector<FBoundary> Boundaries;
|
std::vector<FBoundary> Boundaries;
|
||||||
for(const FStrateSlot& Slot:Layout)
|
for(const FStrateSlot& Slot:Layout)
|
||||||
{
|
{
|
||||||
FStandaloneShapeWorld::FResolvedStratumBounds Resolved;
|
const FStrateGenerationParams Params=World.Manager->GetGenerationParams(
|
||||||
if(!Shape.GetResolvedStratumBounds(Slot,Resolved))continue;
|
FIntVector(0,0,(Slot.TopChunkZ+Slot.BottomChunkZ)/2));
|
||||||
const double HalfWidth=std::max(8.0,Resolved.SealThickness+8.0);
|
const double HalfWidth=std::max(8.0,static_cast<double>(Params.BoundarySealThickness)+8.0);
|
||||||
Boundaries.push_back({Resolved.BottomZ,HalfWidth});
|
Boundaries.push_back({static_cast<double>(Slot.BottomChunkZ)*CHUNK_SIZE,HalfWidth});
|
||||||
Boundaries.push_back({Resolved.TopZ,HalfWidth});
|
Boundaries.push_back({static_cast<double>(Slot.TopChunkZ+1)*CHUNK_SIZE,HalfWidth});
|
||||||
}
|
}
|
||||||
std::sort(Boundaries.begin(),Boundaries.end(),[](const FBoundary& A,const FBoundary& B){return A.Z<B.Z;});
|
std::sort(Boundaries.begin(),Boundaries.end(),[](const FBoundary& A,const FBoundary& B){return A.Z<B.Z;});
|
||||||
Boundaries.erase(std::unique(Boundaries.begin(),Boundaries.end(),[](const FBoundary& A,const FBoundary& B)
|
Boundaries.erase(std::unique(Boundaries.begin(),Boundaries.end(),[](const FBoundary& A,const FBoundary& B)
|
||||||
@@ -1191,8 +996,7 @@ inline void AppendFarStratumShells(const FStandaloneOwnerWorld& World,const FSta
|
|||||||
const bool bHasColors=Mesh.Colors.Num()==OldVertexCount;
|
const bool bHasColors=Mesh.Colors.Num()==OldVertexCount;
|
||||||
const FColor Color=bHasColors&&OldVertexCount>0?Mesh.Colors[0]:FColor(160,160,160,255);
|
const FColor Color=bHasColors&&OldVertexCount>0?Mesh.Colors[0]:FColor(160,160,160,255);
|
||||||
const int32 Cells=std::max(1,World.CoarseTileCells);
|
const int32 Cells=std::max(1,World.CoarseTileCells);
|
||||||
FFarMeshColumnIndex MeshIndex(Mesh,Origin,Extent,Cells,Step,
|
FFarMeshColumnIndex MeshIndex(Mesh,Origin,Extent,Cells,Step);
|
||||||
static_cast<double>(World.CoreVoxelSizeMicrons)/10000.0,Key.Level);
|
|
||||||
std::vector<FVector> NewVertices,NewNormals;
|
std::vector<FVector> NewVertices,NewNormals;
|
||||||
std::vector<FVector2f> NewUVs;std::vector<FColor> NewColors;
|
std::vector<FVector2f> NewUVs;std::vector<FColor> NewColors;
|
||||||
std::vector<int32> NewTriangles;
|
std::vector<int32> NewTriangles;
|
||||||
@@ -1328,470 +1132,3 @@ inline void AppendFarStratumShells(const FStandaloneOwnerWorld& World,const FSta
|
|||||||
Mesh.TriangleSkirtFaceMasks=std::move(CombinedFaceMasks);
|
Mesh.TriangleSkirtFaceMasks=std::move(CombinedFaceMasks);
|
||||||
Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
|
Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill only underside cells that marching cubes missed. Active transitions use the
|
|
||||||
// same displaced face and passage-mouth fade as the signed-distance field.
|
|
||||||
struct FTransitionFaceReplacement
|
|
||||||
{
|
|
||||||
double BaseZVoxels=0.0;
|
|
||||||
double MaximumDisplacementVoxels=0.0;
|
|
||||||
std::vector<uint8> Cells;
|
|
||||||
};
|
|
||||||
|
|
||||||
inline int32 RemoveCoarseTransitionFaceTriangles(FVoxelMeshData& Mesh,
|
|
||||||
const FIntVector& Origin,int32 Step,int32 GridSide,double VoxelSizeCm,
|
|
||||||
const std::vector<FTransitionFaceReplacement>& Replacements,bool bUpward)
|
|
||||||
{
|
|
||||||
if(Step<=0||GridSide<2||VoxelSizeCm<=0.0||Replacements.empty())return 0;
|
|
||||||
const int32 TriangleCount=Mesh.Triangles.Num()/3;
|
|
||||||
const int32 OldCeilingStart=std::max(0,TriangleCount-Mesh.NumCeilingTriangles);
|
|
||||||
TArray<uint8> OldMasks=Mesh.TriangleSkirtFaceMasks;
|
|
||||||
OldMasks.SetNum(TriangleCount);
|
|
||||||
TArray<int32> KeptTriangles;
|
|
||||||
TArray<uint8> KeptMasks;
|
|
||||||
KeptTriangles.Reserve(Mesh.Triangles.Num());
|
|
||||||
KeptMasks.Reserve(TriangleCount);
|
|
||||||
int32 Removed=0,RemovedCeiling=0;
|
|
||||||
const int32 CellSide=GridSide-1;
|
|
||||||
for(int32 Triangle=0;Triangle<TriangleCount;++Triangle)
|
|
||||||
{
|
|
||||||
const int32 IA=Mesh.Triangles[Triangle*3];
|
|
||||||
const int32 IB=Mesh.Triangles[Triangle*3+1];
|
|
||||||
const int32 IC=Mesh.Triangles[Triangle*3+2];
|
|
||||||
bool bRemove=false;
|
|
||||||
if(OldMasks[Triangle]==0&&IA>=0&&IB>=0&&IC>=0
|
|
||||||
&&IA<Mesh.Vertices.Num()&&IB<Mesh.Vertices.Num()&&IC<Mesh.Vertices.Num())
|
|
||||||
{
|
|
||||||
const FVector& A=Mesh.Vertices[IA];
|
|
||||||
const FVector& B=Mesh.Vertices[IB];
|
|
||||||
const FVector& C=Mesh.Vertices[IC];
|
|
||||||
const FVector GeometricNormal=FVector::CrossProduct(B-A,C-A);
|
|
||||||
const double NormalLength=GeometricNormal.Size();
|
|
||||||
const double Facing=NormalLength>1.0e-12?GeometricNormal.Z/NormalLength:0.0;
|
|
||||||
if((bUpward&&Facing>0.35)||(!bUpward&&Facing< -0.35))
|
|
||||||
{
|
|
||||||
const double X=(A.X+B.X+C.X)/(3.0*VoxelSizeCm);
|
|
||||||
const double Y=(A.Y+B.Y+C.Y)/(3.0*VoxelSizeCm);
|
|
||||||
const double Z=(A.Z+B.Z+C.Z)/(3.0*VoxelSizeCm);
|
|
||||||
const int32 CellX=static_cast<int32>(std::floor((X-Origin.X)/Step));
|
|
||||||
const int32 CellY=static_cast<int32>(std::floor((Y-Origin.Y)/Step));
|
|
||||||
if(CellX>=0&&CellX<CellSide&&CellY>=0&&CellY<CellSide)
|
|
||||||
{
|
|
||||||
const std::size_t Cell=static_cast<std::size_t>(CellY)*CellSide+CellX;
|
|
||||||
for(const auto& Replacement:Replacements)
|
|
||||||
{
|
|
||||||
if(Cell>=Replacement.Cells.size()||!Replacement.Cells[Cell])continue;
|
|
||||||
const double Margin=Replacement.MaximumDisplacementVoxels+4.0;
|
|
||||||
if(Z>=Replacement.BaseZVoxels-Margin
|
|
||||||
&&Z<=Replacement.BaseZVoxels+Margin)
|
|
||||||
{bRemove=true;break;}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(bRemove)
|
|
||||||
{
|
|
||||||
++Removed;
|
|
||||||
if(Triangle>=OldCeilingStart)++RemovedCeiling;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
KeptTriangles.Add(IA);KeptTriangles.Add(IB);KeptTriangles.Add(IC);
|
|
||||||
KeptMasks.Add(OldMasks[Triangle]);
|
|
||||||
}
|
|
||||||
if(Removed==0)return 0;
|
|
||||||
Mesh.Triangles=std::move(KeptTriangles);
|
|
||||||
Mesh.TriangleSkirtFaceMasks=std::move(KeptMasks);
|
|
||||||
Mesh.NumCeilingTriangles=std::max(0,Mesh.NumCeilingTriangles-RemovedCeiling);
|
|
||||||
return Removed;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World,
|
|
||||||
const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh)
|
|
||||||
{
|
|
||||||
FStandaloneShapeWorld::FScopedPassageDensityQueryCaller queryCaller(
|
|
||||||
EPassageDensityQueryCaller::FarShellStratumRefinement);
|
|
||||||
if(Key.Level<0||Step<=0||!World.Manager)return;
|
|
||||||
const FIntVector Origin=Key.OriginVoxels();
|
|
||||||
const int32 Extent=Key.ExtentVoxels();
|
|
||||||
const int32 GridStep=Step;
|
|
||||||
if(Extent<=0||GridStep<=0||Extent%GridStep!=0)return;
|
|
||||||
const int32 GridSide=Extent/GridStep+1;
|
|
||||||
struct FCandidate
|
|
||||||
{
|
|
||||||
double FaceZ=0.0;
|
|
||||||
double Thickness=0.0;
|
|
||||||
double MaximumDisplacementVoxels=0.0;
|
|
||||||
std::vector<double> FaceHeights;
|
|
||||||
std::vector<uint8> SolidAbove;
|
|
||||||
const VoxelForgeCore::ResolvedLayoutStratumV2* Stratum=nullptr;
|
|
||||||
bool bTransition=false;
|
|
||||||
};
|
|
||||||
std::vector<FCandidate> Candidates;
|
|
||||||
std::vector<FTransitionFaceReplacement> Replacements;
|
|
||||||
const double CoreMicrons=std::max<std::uint32_t>(1,World.CoreVoxelSizeMicrons);
|
|
||||||
for(const FStrateSlot& Slot:World.Manager->GetLayout())
|
|
||||||
{
|
|
||||||
const auto* Stratum=Shape.FindResolvedLayoutStratum(Slot.StrateIndex);
|
|
||||||
if(!Stratum||!Stratum->ceiling.presentInCurrentField
|
|
||||||
||Stratum->ceiling.topMicrometres<=Stratum->ceiling.bottomMicrometres)continue;
|
|
||||||
FCandidate Candidate;
|
|
||||||
Candidate.FaceZ=static_cast<double>(Stratum->ceiling.bottomMicrometres)/CoreMicrons;
|
|
||||||
Candidate.Thickness=static_cast<double>(Stratum->ceiling.topMicrometres
|
|
||||||
-Stratum->ceiling.bottomMicrometres)/CoreMicrons;
|
|
||||||
Candidate.Stratum=Stratum;
|
|
||||||
const auto* transitionUpper=Shape.FindResolvedLayoutStratumByOrder(Stratum->order-1);
|
|
||||||
const bool bTransition=transitionUpper
|
|
||||||
&&Shape.IsResolvedLayoutTransitionActive(*transitionUpper);
|
|
||||||
const double displacement=bTransition?static_cast<double>(
|
|
||||||
Shape.ResolvedTransitionMaximumFaceDisplacementMicrometres(*transitionUpper,*Stratum))
|
|
||||||
/CoreMicrons:0.0;
|
|
||||||
if(Candidate.FaceZ<Origin.Z-displacement-1.0e-6
|
|
||||||
||Candidate.FaceZ>Origin.Z+Extent+displacement+1.0e-6)continue;
|
|
||||||
Candidate.bTransition=bTransition;
|
|
||||||
Candidate.MaximumDisplacementVoxels=displacement;
|
|
||||||
const double Epsilon=std::min(0.5,Candidate.Thickness*0.25);
|
|
||||||
if(Epsilon<=0.0)continue;
|
|
||||||
const std::size_t GridCount=static_cast<std::size_t>(GridSide)*GridSide;
|
|
||||||
Candidate.FaceHeights.resize(GridCount,Candidate.FaceZ);
|
|
||||||
Candidate.SolidAbove.resize(GridCount,0);
|
|
||||||
bool AnySolidAbove=false;
|
|
||||||
for(int32 Y=0;Y<GridSide;++Y)for(int32 X=0;X<GridSide;++X)
|
|
||||||
{
|
|
||||||
const double WorldX=Origin.X+X*GridStep;
|
|
||||||
const double WorldY=Origin.Y+Y*GridStep;
|
|
||||||
const std::size_t GridIndex=static_cast<std::size_t>(Y*GridSide+X);
|
|
||||||
Candidate.bTransition=Shape.ResolvedCeilingBottomAt(*Candidate.Stratum,
|
|
||||||
WorldX,WorldY,Candidate.FaceHeights[GridIndex])||Candidate.bTransition;
|
|
||||||
const float Above=Shape.DensityAt(static_cast<float>(WorldX),static_cast<float>(WorldY),
|
|
||||||
static_cast<float>(Candidate.FaceHeights[GridIndex]+Epsilon));
|
|
||||||
const bool bSolidAbove=std::isfinite(Above)&&Above<0.0f;
|
|
||||||
Candidate.SolidAbove[GridIndex]=bSolidAbove?1:0;
|
|
||||||
AnySolidAbove=AnySolidAbove||bSolidAbove;
|
|
||||||
}
|
|
||||||
if(AnySolidAbove)Candidates.push_back(std::move(Candidate));
|
|
||||||
}
|
|
||||||
if(Candidates.empty())return;
|
|
||||||
|
|
||||||
const int32 Cells=std::max(1,World.CoarseTileCells);
|
|
||||||
FFarMeshColumnIndex MeshIndex(Mesh,Origin,Extent,Cells,Step,
|
|
||||||
static_cast<double>(World.CoreVoxelSizeMicrons)/10000.0,Key.Level);
|
|
||||||
const int32 OldVertexCount=Mesh.Vertices.Num();
|
|
||||||
const int32 OldSurfaceVertices=std::clamp(Mesh.NumSurfaceVertices,0,OldVertexCount);
|
|
||||||
const bool bHasUVs=Mesh.UVs.Num()==OldVertexCount;
|
|
||||||
const bool bHasColors=Mesh.Colors.Num()==OldVertexCount;
|
|
||||||
const FColor Color=bHasColors&&OldVertexCount>0?Mesh.Colors[0]:FColor(160,160,160,255);
|
|
||||||
const double VoxelSizeCm=static_cast<double>(World.CoreVoxelSizeMicrons)/10000.0;
|
|
||||||
std::vector<FVector> NewVertices,NewNormals;
|
|
||||||
std::vector<FVector2f> NewUVs;
|
|
||||||
std::vector<FColor> NewColors;
|
|
||||||
std::vector<int32> NewTriangles;
|
|
||||||
for(const FCandidate& Candidate:Candidates)
|
|
||||||
{
|
|
||||||
const std::size_t GridCount=static_cast<std::size_t>(GridSide)*GridSide;
|
|
||||||
std::vector<int32> VertexMap(GridCount,INDEX_NONE);
|
|
||||||
FTransitionFaceReplacement Replacement;
|
|
||||||
Replacement.BaseZVoxels=Candidate.FaceZ;
|
|
||||||
Replacement.MaximumDisplacementVoxels=Candidate.MaximumDisplacementVoxels;
|
|
||||||
Replacement.Cells.resize(static_cast<std::size_t>(GridSide-1)*(GridSide-1),0);
|
|
||||||
const auto heightAt=[&](double X,double Y)
|
|
||||||
{
|
|
||||||
if(!Candidate.bTransition)return Candidate.FaceZ;
|
|
||||||
double height=Candidate.FaceZ;
|
|
||||||
Shape.ResolvedCeilingBottomAt(*Candidate.Stratum,X,Y,height);
|
|
||||||
return height;
|
|
||||||
};
|
|
||||||
const auto AddVertex=[&](int32 X,int32 Y)
|
|
||||||
{
|
|
||||||
const std::size_t GridIndex=static_cast<std::size_t>(Y*GridSide+X);
|
|
||||||
int32& Mapped=VertexMap[GridIndex];
|
|
||||||
if(Mapped!=INDEX_NONE)return Mapped;
|
|
||||||
Mapped=static_cast<int32>(NewVertices.size());
|
|
||||||
const double WorldX=Origin.X+X*GridStep,WorldY=Origin.Y+Y*GridStep;
|
|
||||||
const double FaceZ=Candidate.FaceHeights[GridIndex];
|
|
||||||
FVector Normal(0.0,0.0,-1.0);
|
|
||||||
if(Candidate.bTransition)
|
|
||||||
{
|
|
||||||
const double Delta=std::max(1.0,std::min<double>(GridStep,4.0));
|
|
||||||
const double DX=(heightAt(WorldX+Delta,WorldY)-heightAt(WorldX-Delta,WorldY))
|
|
||||||
/(2.0*Delta);
|
|
||||||
const double DY=(heightAt(WorldX,WorldY+Delta)-heightAt(WorldX,WorldY-Delta))
|
|
||||||
/(2.0*Delta);
|
|
||||||
Normal=FVector(DX,DY,-1.0);
|
|
||||||
Normal.Normalize();
|
|
||||||
}
|
|
||||||
NewVertices.emplace_back(WorldX*VoxelSizeCm,WorldY*VoxelSizeCm,FaceZ*VoxelSizeCm);
|
|
||||||
NewNormals.push_back(Normal);
|
|
||||||
if(bHasUVs)NewUVs.emplace_back(static_cast<float>(WorldX),static_cast<float>(WorldY));
|
|
||||||
if(bHasColors)NewColors.push_back(Color);
|
|
||||||
return Mapped;
|
|
||||||
};
|
|
||||||
for(int32 Y=0;Y+1<GridSide;++Y)for(int32 X=0;X+1<GridSide;++X)
|
|
||||||
{
|
|
||||||
const std::size_t I00=static_cast<std::size_t>(Y*GridSide+X);
|
|
||||||
const std::size_t I10=I00+1,I01=I00+GridSide,I11=I01+1;
|
|
||||||
if(!Candidate.SolidAbove[I00]||!Candidate.SolidAbove[I10]
|
|
||||||
||!Candidate.SolidAbove[I01]||!Candidate.SolidAbove[I11])continue;
|
|
||||||
const double CellX0=Origin.X+X*GridStep,CellY0=Origin.Y+Y*GridStep;
|
|
||||||
const double CellX1=CellX0+GridStep,CellY1=CellY0+GridStep;
|
|
||||||
bool fullyCovered=false;
|
|
||||||
if(!Candidate.bTransition)
|
|
||||||
fullyCovered=MeshIndex.HasDownwardCellCoverage(
|
|
||||||
CellX0,CellY0,CellX1,CellY1,Candidate.FaceZ,0.001);
|
|
||||||
else
|
|
||||||
{
|
|
||||||
fullyCovered=true;
|
|
||||||
constexpr double CoverageSamples[5][2]={
|
|
||||||
{0.0,0.0},{1.0,0.0},{0.0,1.0},{1.0,1.0},{0.5,0.5}};
|
|
||||||
for(const auto& Sample:CoverageSamples)
|
|
||||||
{
|
|
||||||
const double U=Sample[0],V=Sample[1];
|
|
||||||
const double SampleX=CellX0+(CellX1-CellX0)*U;
|
|
||||||
const double SampleY=CellY0+(CellY1-CellY0)*V;
|
|
||||||
const double SampleZ=heightAt(SampleX,SampleY);
|
|
||||||
if(!MeshIndex.HasDownwardHit(SampleX,SampleY,SampleZ,0.25))
|
|
||||||
fullyCovered=false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(fullyCovered)continue;
|
|
||||||
if(Key.Level>=4&&Candidate.bTransition)
|
|
||||||
Replacement.Cells[static_cast<std::size_t>(Y)*(GridSide-1)+X]=1;
|
|
||||||
const int32 A=AddVertex(X,Y),B=AddVertex(X+1,Y);
|
|
||||||
const int32 C=AddVertex(X+1,Y+1),D=AddVertex(X,Y+1);
|
|
||||||
NewTriangles.insert(NewTriangles.end(),{A,C,B,A,D,C});
|
|
||||||
}
|
|
||||||
if(Key.Level>=4&&Candidate.bTransition
|
|
||||||
&&std::any_of(Replacement.Cells.begin(),Replacement.Cells.end(),
|
|
||||||
[](uint8 Value){return Value!=0;}))
|
|
||||||
Replacements.push_back(std::move(Replacement));
|
|
||||||
}
|
|
||||||
if(NewTriangles.empty())return;
|
|
||||||
const int32 removedCoarse=RemoveCoarseTransitionFaceTriangles(Mesh,Origin,GridStep,
|
|
||||||
GridSide,VoxelSizeCm,Replacements,false);
|
|
||||||
|
|
||||||
const int32 AddedVertices=static_cast<int32>(NewVertices.size());
|
|
||||||
auto InsertVertices=[&](auto& Existing,const auto& Added)
|
|
||||||
{
|
|
||||||
using TArrayType=std::remove_reference_t<decltype(Existing)>;
|
|
||||||
TArrayType Combined;Combined.Reserve(Existing.Num()+static_cast<int32>(Added.size()));
|
|
||||||
for(int32 I=0;I<OldSurfaceVertices;++I)Combined.Add(Existing[I]);
|
|
||||||
for(const auto& Value:Added)Combined.Add(Value);
|
|
||||||
for(int32 I=OldSurfaceVertices;I<Existing.Num();++I)Combined.Add(Existing[I]);
|
|
||||||
Existing=std::move(Combined);
|
|
||||||
};
|
|
||||||
InsertVertices(Mesh.Vertices,NewVertices);InsertVertices(Mesh.Normals,NewNormals);
|
|
||||||
if(bHasUVs)InsertVertices(Mesh.UVs,NewUVs);
|
|
||||||
if(bHasColors)InsertVertices(Mesh.Colors,NewColors);
|
|
||||||
for(int32& Index:Mesh.Triangles)if(Index>=OldSurfaceVertices)Index+=AddedVertices;
|
|
||||||
const int32 CeilingStart=std::max(0,Mesh.Triangles.Num()-Mesh.NumCeilingTriangles*3);
|
|
||||||
TArray<int32> CombinedTriangles;CombinedTriangles.Reserve(
|
|
||||||
Mesh.Triangles.Num()+static_cast<int32>(NewTriangles.size()));
|
|
||||||
for(int32 I=0;I<CeilingStart;++I)CombinedTriangles.Add(Mesh.Triangles[I]);
|
|
||||||
for(const int32 Index:NewTriangles)CombinedTriangles.Add(OldSurfaceVertices+Index);
|
|
||||||
for(int32 I=CeilingStart;I<Mesh.Triangles.Num();++I)CombinedTriangles.Add(Mesh.Triangles[I]);
|
|
||||||
const int32 OldTriangleCount=Mesh.Triangles.Num()/3;
|
|
||||||
TArray<uint8> ExistingFaceMasks=Mesh.TriangleSkirtFaceMasks;
|
|
||||||
ExistingFaceMasks.SetNum(OldTriangleCount);
|
|
||||||
TArray<uint8> CombinedFaceMasks;CombinedFaceMasks.Reserve(
|
|
||||||
OldTriangleCount+static_cast<int32>(NewTriangles.size()/3));
|
|
||||||
for(int32 Triangle=0;Triangle<CeilingStart/3;++Triangle)
|
|
||||||
CombinedFaceMasks.Add(ExistingFaceMasks[Triangle]);
|
|
||||||
for(std::size_t Triangle=0;Triangle<NewTriangles.size()/3;++Triangle)
|
|
||||||
CombinedFaceMasks.Add(0);
|
|
||||||
for(int32 Triangle=CeilingStart/3;Triangle<OldTriangleCount;++Triangle)
|
|
||||||
CombinedFaceMasks.Add(ExistingFaceMasks[Triangle]);
|
|
||||||
Mesh.Triangles=std::move(CombinedTriangles);
|
|
||||||
Mesh.TriangleSkirtFaceMasks=std::move(CombinedFaceMasks);
|
|
||||||
Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
|
|
||||||
Mesh.NumCeilingTriangles+=static_cast<int32>(NewTriangles.size()/3);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fill only top cells that marching cubes missed. Active transitions use the same
|
|
||||||
// displaced face and passage-mouth fade as the signed-distance field.
|
|
||||||
inline void AppendStratumBoundaryFloors(const FStandaloneOwnerWorld& World,
|
|
||||||
const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh)
|
|
||||||
{
|
|
||||||
FStandaloneShapeWorld::FScopedPassageDensityQueryCaller queryCaller(
|
|
||||||
EPassageDensityQueryCaller::FarShellStratumRefinement);
|
|
||||||
if(Key.Level<0||Step<=0||!World.Manager||!Shape.ResolvedLayout)return;
|
|
||||||
const FIntVector Origin=Key.OriginVoxels();
|
|
||||||
const int32 Extent=Key.ExtentVoxels();
|
|
||||||
if(Extent<=0||Extent%Step!=0)return;
|
|
||||||
const int32 GridSide=Extent/Step+1;
|
|
||||||
struct FCandidate
|
|
||||||
{
|
|
||||||
double FloorZ=0.0;
|
|
||||||
double Thickness=0.0;
|
|
||||||
double MaximumDisplacementVoxels=0.0;
|
|
||||||
const VoxelForgeCore::ResolvedLayoutStratumV2* Stratum=nullptr;
|
|
||||||
bool bTransition=false;
|
|
||||||
};
|
|
||||||
std::vector<FCandidate> Candidates;
|
|
||||||
std::vector<FTransitionFaceReplacement> Replacements;
|
|
||||||
const double CoreMicrons=std::max<std::uint32_t>(1,World.CoreVoxelSizeMicrons);
|
|
||||||
for(const FStrateSlot& Slot:World.Manager->GetLayout())
|
|
||||||
{
|
|
||||||
const auto* Stratum=Shape.FindResolvedLayoutStratum(Slot.StrateIndex);
|
|
||||||
if(!Stratum||!Stratum->bedrock.presentInCurrentField
|
|
||||||
||Stratum->bedrock.topMicrometres<=Stratum->bedrock.bottomMicrometres)continue;
|
|
||||||
const double FloorZ=static_cast<double>(Stratum->bedrock.topMicrometres)/CoreMicrons;
|
|
||||||
const auto* transitionLower=Shape.FindResolvedLayoutStratumByOrder(Stratum->order+1);
|
|
||||||
const bool bTransition=transitionLower
|
|
||||||
&&Shape.IsResolvedLayoutTransitionActive(*Stratum);
|
|
||||||
const double displacement=bTransition
|
|
||||||
?static_cast<double>(Shape.ResolvedTransitionMaximumFaceDisplacementMicrometres(
|
|
||||||
*Stratum,*transitionLower))/CoreMicrons
|
|
||||||
:0.0;
|
|
||||||
if(FloorZ<Origin.Z-displacement-1.0e-6
|
|
||||||
||FloorZ>Origin.Z+Extent+displacement+1.0e-6)continue;
|
|
||||||
Candidates.push_back({FloorZ,static_cast<double>(Stratum->bedrock.topMicrometres
|
|
||||||
-Stratum->bedrock.bottomMicrometres)/CoreMicrons,displacement,Stratum,bTransition});
|
|
||||||
}
|
|
||||||
if(Candidates.empty())return;
|
|
||||||
|
|
||||||
const int32 Cells=std::max(1,World.CoarseTileCells);
|
|
||||||
FFarMeshColumnIndex MeshIndex(Mesh,Origin,Extent,Cells,Step,
|
|
||||||
static_cast<double>(World.CoreVoxelSizeMicrons)/10000.0,Key.Level);
|
|
||||||
const int32 OldVertexCount=Mesh.Vertices.Num();
|
|
||||||
const int32 OldSurfaceVertices=std::clamp(Mesh.NumSurfaceVertices,0,OldVertexCount);
|
|
||||||
const bool bHasUVs=Mesh.UVs.Num()==OldVertexCount;
|
|
||||||
const bool bHasColors=Mesh.Colors.Num()==OldVertexCount;
|
|
||||||
const FColor Color=bHasColors&&OldVertexCount>0?Mesh.Colors[0]:FColor(160,160,160,255);
|
|
||||||
const double VoxelSizeCm=static_cast<double>(World.CoreVoxelSizeMicrons)/10000.0;
|
|
||||||
std::vector<FVector> NewVertices,NewNormals;
|
|
||||||
std::vector<FVector2f> NewUVs;
|
|
||||||
std::vector<FColor> NewColors;
|
|
||||||
std::vector<int32> NewTriangles;
|
|
||||||
for(const FCandidate& Candidate:Candidates)
|
|
||||||
{
|
|
||||||
const double Epsilon=std::min(0.5,Candidate.Thickness*0.25);
|
|
||||||
if(Epsilon<=0.0)continue;
|
|
||||||
const std::size_t GridCount=static_cast<std::size_t>(GridSide)*GridSide;
|
|
||||||
std::vector<double> faceHeights(GridCount,Candidate.FloorZ);
|
|
||||||
std::vector<uint8> solidBelow(GridCount,0);
|
|
||||||
FTransitionFaceReplacement Replacement;
|
|
||||||
Replacement.BaseZVoxels=Candidate.FloorZ;
|
|
||||||
Replacement.MaximumDisplacementVoxels=Candidate.MaximumDisplacementVoxels;
|
|
||||||
Replacement.Cells.resize(static_cast<std::size_t>(GridSide-1)*(GridSide-1),0);
|
|
||||||
for(int32 Y=0;Y<GridSide;++Y)for(int32 X=0;X<GridSide;++X)
|
|
||||||
{
|
|
||||||
const double WorldX=Origin.X+X*Step,WorldY=Origin.Y+Y*Step;
|
|
||||||
const std::size_t GridIndex=static_cast<std::size_t>(Y*GridSide+X);
|
|
||||||
if(Candidate.bTransition)
|
|
||||||
Shape.ResolvedBedrockTopAt(*Candidate.Stratum,WorldX,WorldY,
|
|
||||||
faceHeights[GridIndex]);
|
|
||||||
const float Density=Shape.DensityAt(static_cast<float>(WorldX),static_cast<float>(WorldY),
|
|
||||||
static_cast<float>(faceHeights[GridIndex]-Epsilon));
|
|
||||||
solidBelow[GridIndex]=
|
|
||||||
std::isfinite(Density)&&Density<0.0f?1:0;
|
|
||||||
}
|
|
||||||
const auto heightAt=[&](double X,double Y)
|
|
||||||
{
|
|
||||||
if(!Candidate.bTransition)return Candidate.FloorZ;
|
|
||||||
double height=Candidate.FloorZ;
|
|
||||||
if(Candidate.bTransition)
|
|
||||||
Shape.ResolvedBedrockTopAt(*Candidate.Stratum,X,Y,height);
|
|
||||||
return height;
|
|
||||||
};
|
|
||||||
std::vector<int32> VertexMap(GridCount,INDEX_NONE);
|
|
||||||
const auto AddVertex=[&](int32 X,int32 Y)
|
|
||||||
{
|
|
||||||
const std::size_t GridIndex=static_cast<std::size_t>(Y*GridSide+X);
|
|
||||||
int32& Mapped=VertexMap[GridIndex];
|
|
||||||
if(Mapped!=INDEX_NONE)return Mapped;
|
|
||||||
Mapped=static_cast<int32>(NewVertices.size());
|
|
||||||
const double WorldX=Origin.X+X*Step,WorldY=Origin.Y+Y*Step;
|
|
||||||
FVector Normal(0.0,0.0,1.0);
|
|
||||||
if(Candidate.bTransition)
|
|
||||||
{
|
|
||||||
const double Delta=std::max(1.0,std::min<double>(Step,4.0));
|
|
||||||
const double DX=(heightAt(WorldX+Delta,WorldY)-heightAt(WorldX-Delta,WorldY))
|
|
||||||
/(2.0*Delta);
|
|
||||||
const double DY=(heightAt(WorldX,WorldY+Delta)-heightAt(WorldX,WorldY-Delta))
|
|
||||||
/(2.0*Delta);
|
|
||||||
Normal=FVector(-DX,-DY,1.0);
|
|
||||||
Normal.Normalize();
|
|
||||||
}
|
|
||||||
NewVertices.emplace_back(WorldX*VoxelSizeCm,WorldY*VoxelSizeCm,
|
|
||||||
faceHeights[GridIndex]*VoxelSizeCm);
|
|
||||||
NewNormals.push_back(Normal);
|
|
||||||
if(bHasUVs)NewUVs.emplace_back(static_cast<float>(WorldX),static_cast<float>(WorldY));
|
|
||||||
if(bHasColors)NewColors.push_back(Color);
|
|
||||||
return Mapped;
|
|
||||||
};
|
|
||||||
for(int32 Y=0;Y+1<GridSide;++Y)for(int32 X=0;X+1<GridSide;++X)
|
|
||||||
{
|
|
||||||
const std::size_t I00=static_cast<std::size_t>(Y*GridSide+X);
|
|
||||||
const std::size_t I10=I00+1,I01=I00+GridSide,I11=I01+1;
|
|
||||||
if(!solidBelow[I00]||!solidBelow[I10]||!solidBelow[I01]||!solidBelow[I11])continue;
|
|
||||||
const double X0=Origin.X+X*Step,Y0=Origin.Y+Y*Step;
|
|
||||||
const double X1=X0+Step,Y1=Y0+Step;
|
|
||||||
bool fullyCovered=false;
|
|
||||||
if(!Candidate.bTransition)
|
|
||||||
fullyCovered=MeshIndex.HasUpwardCellCoverage(
|
|
||||||
X0,Y0,X1,Y1,Candidate.FloorZ,0.001);
|
|
||||||
else
|
|
||||||
{
|
|
||||||
fullyCovered=true;
|
|
||||||
constexpr double CoverageSamples[5][2]={
|
|
||||||
{0.0,0.0},{1.0,0.0},{0.0,1.0},{1.0,1.0},{0.5,0.5}};
|
|
||||||
for(const auto& Sample:CoverageSamples)
|
|
||||||
{
|
|
||||||
const double U=Sample[0],V=Sample[1];
|
|
||||||
const double SampleX=X0+(X1-X0)*U;
|
|
||||||
const double SampleY=Y0+(Y1-Y0)*V;
|
|
||||||
const double SampleZ=heightAt(SampleX,SampleY);
|
|
||||||
if(!MeshIndex.HasUpwardHit(SampleX,SampleY,SampleZ,0.25))
|
|
||||||
fullyCovered=false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(fullyCovered)continue;
|
|
||||||
if(Key.Level>=4&&Candidate.bTransition)
|
|
||||||
Replacement.Cells[static_cast<std::size_t>(Y)*(GridSide-1)+X]=1;
|
|
||||||
const int32 A=AddVertex(X,Y),B=AddVertex(X+1,Y);
|
|
||||||
const int32 C=AddVertex(X+1,Y+1),D=AddVertex(X,Y+1);
|
|
||||||
NewTriangles.insert(NewTriangles.end(),{A,B,C,A,C,D});
|
|
||||||
}
|
|
||||||
if(Key.Level>=4&&Candidate.bTransition
|
|
||||||
&&std::any_of(Replacement.Cells.begin(),Replacement.Cells.end(),
|
|
||||||
[](uint8 Value){return Value!=0;}))
|
|
||||||
Replacements.push_back(std::move(Replacement));
|
|
||||||
}
|
|
||||||
if(NewTriangles.empty())return;
|
|
||||||
const int32 removedCoarse=RemoveCoarseTransitionFaceTriangles(Mesh,Origin,Step,
|
|
||||||
GridSide,VoxelSizeCm,Replacements,true);
|
|
||||||
|
|
||||||
const int32 AddedVertices=static_cast<int32>(NewVertices.size());
|
|
||||||
auto InsertVertices=[&](auto& Existing,const auto& Added)
|
|
||||||
{
|
|
||||||
using TArrayType=std::remove_reference_t<decltype(Existing)>;
|
|
||||||
TArrayType Combined;Combined.Reserve(Existing.Num()+static_cast<int32>(Added.size()));
|
|
||||||
for(int32 I=0;I<OldSurfaceVertices;++I)Combined.Add(Existing[I]);
|
|
||||||
for(const auto& Value:Added)Combined.Add(Value);
|
|
||||||
for(int32 I=OldSurfaceVertices;I<Existing.Num();++I)Combined.Add(Existing[I]);
|
|
||||||
Existing=std::move(Combined);
|
|
||||||
};
|
|
||||||
InsertVertices(Mesh.Vertices,NewVertices);InsertVertices(Mesh.Normals,NewNormals);
|
|
||||||
if(bHasUVs)InsertVertices(Mesh.UVs,NewUVs);
|
|
||||||
if(bHasColors)InsertVertices(Mesh.Colors,NewColors);
|
|
||||||
for(int32& Index:Mesh.Triangles)if(Index>=OldSurfaceVertices)Index+=AddedVertices;
|
|
||||||
const int32 CeilingStart=std::max(0,Mesh.Triangles.Num()-Mesh.NumCeilingTriangles*3);
|
|
||||||
TArray<int32> CombinedTriangles;CombinedTriangles.Reserve(
|
|
||||||
Mesh.Triangles.Num()+static_cast<int32>(NewTriangles.size()));
|
|
||||||
for(int32 I=0;I<CeilingStart;++I)CombinedTriangles.Add(Mesh.Triangles[I]);
|
|
||||||
for(const int32 Index:NewTriangles)CombinedTriangles.Add(OldSurfaceVertices+Index);
|
|
||||||
for(int32 I=CeilingStart;I<Mesh.Triangles.Num();++I)CombinedTriangles.Add(Mesh.Triangles[I]);
|
|
||||||
const int32 OldTriangleCount=Mesh.Triangles.Num()/3;
|
|
||||||
TArray<uint8> ExistingFaceMasks=Mesh.TriangleSkirtFaceMasks;
|
|
||||||
ExistingFaceMasks.SetNum(OldTriangleCount);
|
|
||||||
TArray<uint8> CombinedFaceMasks;CombinedFaceMasks.Reserve(
|
|
||||||
OldTriangleCount+static_cast<int32>(NewTriangles.size()/3));
|
|
||||||
for(int32 Triangle=0;Triangle<CeilingStart/3;++Triangle)
|
|
||||||
CombinedFaceMasks.Add(ExistingFaceMasks[Triangle]);
|
|
||||||
for(std::size_t Triangle=0;Triangle<NewTriangles.size()/3;++Triangle)
|
|
||||||
CombinedFaceMasks.Add(0);
|
|
||||||
for(int32 Triangle=CeilingStart/3;Triangle<OldTriangleCount;++Triangle)
|
|
||||||
CombinedFaceMasks.Add(ExistingFaceMasks[Triangle]);
|
|
||||||
Mesh.Triangles=std::move(CombinedTriangles);
|
|
||||||
Mesh.TriangleSkirtFaceMasks=std::move(CombinedFaceMasks);
|
|
||||||
Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "VoxelForgeCore/ThreadLocalStorage.h"
|
#include "VoxelForgeCore/ThreadLocalStorage.h"
|
||||||
#include "VoxelForgeCore/CpuCycleCounter.h"
|
|
||||||
#include "VoxelForgeCore/FloatingIslandInstrumentation.h"
|
|
||||||
#include "VoxelForgeCore/SharedColumnCache.h"
|
|
||||||
#include "Json.h"
|
#include "Json.h"
|
||||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||||
|
|
||||||
@@ -173,8 +170,6 @@ namespace floating_island_detail
|
|||||||
HashWord(hash, static_cast<std::uint64_t>(recipe.layoutCenterTopZSubvoxels), 8);
|
HashWord(hash, static_cast<std::uint64_t>(recipe.layoutCenterTopZSubvoxels), 8);
|
||||||
HashWord(hash, static_cast<std::uint64_t>(recipe.layoutMinimumZSubvoxels), 8);
|
HashWord(hash, static_cast<std::uint64_t>(recipe.layoutMinimumZSubvoxels), 8);
|
||||||
HashWord(hash, static_cast<std::uint64_t>(recipe.layoutMaximumZSubvoxels), 8);
|
HashWord(hash, static_cast<std::uint64_t>(recipe.layoutMaximumZSubvoxels), 8);
|
||||||
if(recipe.geometryVersion>=4)
|
|
||||||
HashWord(hash,static_cast<std::uint64_t>(recipe.layoutChunkWidthSubvoxels),8);
|
|
||||||
HashWord(hash, recipe.primaryIslandCount, 4);
|
HashWord(hash, recipe.primaryIslandCount, 4);
|
||||||
HashWord(hash, static_cast<std::uint64_t>(recipe.islandDensityQ16), 8);
|
HashWord(hash, static_cast<std::uint64_t>(recipe.islandDensityQ16), 8);
|
||||||
HashWord(hash, static_cast<std::uint64_t>(recipe.spacingSubvoxels), 8);
|
HashWord(hash, static_cast<std::uint64_t>(recipe.spacingSubvoxels), 8);
|
||||||
@@ -191,22 +186,12 @@ namespace floating_island_detail
|
|||||||
HashWord(hash, static_cast<std::uint64_t>(recipe.crustDepthBonusQ16), 8);
|
HashWord(hash, static_cast<std::uint64_t>(recipe.crustDepthBonusQ16), 8);
|
||||||
HashWord(hash, static_cast<std::uint64_t>(recipe.rootDepthRatioQ16), 8);
|
HashWord(hash, static_cast<std::uint64_t>(recipe.rootDepthRatioQ16), 8);
|
||||||
}
|
}
|
||||||
if (recipe.geometryVersion >= 5)
|
|
||||||
{
|
|
||||||
HashWord(hash, recipe.anchorIslandOrdinal, 4);
|
|
||||||
HashWord(hash, static_cast<std::uint64_t>(recipe.anchorPlacementRadiusSubvoxels), 8);
|
|
||||||
HashWord(hash, static_cast<std::uint64_t>(recipe.anchorRootRadiusSubvoxels), 8);
|
|
||||||
HashWord(hash, static_cast<std::uint64_t>(recipe.anchorIslandMinimumRadiusSubvoxels), 8);
|
|
||||||
HashWord(hash, static_cast<std::uint64_t>(recipe.anchorRootBottomZSubvoxels), 8);
|
|
||||||
HashWord(hash, static_cast<std::uint64_t>(recipe.anchorMouthOffsetSubvoxels), 8);
|
|
||||||
}
|
|
||||||
HashWord(hash, static_cast<std::uint64_t>(recipe.blendRadiusSubvoxels), 8);
|
HashWord(hash, static_cast<std::uint64_t>(recipe.blendRadiusSubvoxels), 8);
|
||||||
HashWord(hash, static_cast<std::uint64_t>(recipe.contourWarpSubvoxels), 8);
|
HashWord(hash, static_cast<std::uint64_t>(recipe.contourWarpSubvoxels), 8);
|
||||||
HashWord(hash, static_cast<std::uint64_t>(recipe.roughnessSubvoxels), 8);
|
HashWord(hash, static_cast<std::uint64_t>(recipe.roughnessSubvoxels), 8);
|
||||||
HashWord(hash, static_cast<std::uint64_t>(recipe.boundarySealSubvoxels), 8);
|
HashWord(hash, static_cast<std::uint64_t>(recipe.boundarySealSubvoxels), 8);
|
||||||
HashWord(hash, static_cast<std::uint64_t>(recipe.bridgeHalfWidthSubvoxels), 8);
|
HashWord(hash, static_cast<std::uint64_t>(recipe.bridgeHalfWidthSubvoxels), 8);
|
||||||
HashWord(hash, static_cast<std::uint64_t>(recipe.bridgeThicknessSubvoxels), 8);
|
HashWord(hash, static_cast<std::uint64_t>(recipe.bridgeThicknessSubvoxels), 8);
|
||||||
if(recipe.terrainFollowingRoute)HashWord(hash,1,1);
|
|
||||||
HashWord(hash, HashRouteConstraints(recipe.routeConstraints), 8);
|
HashWord(hash, HashRouteConstraints(recipe.routeConstraints), 8);
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
@@ -260,10 +245,10 @@ namespace floating_island_detail
|
|||||||
|
|
||||||
inline bool ValidateRecipe(const FloatingIslandRecipeV1& recipe, std::string& error)
|
inline bool ValidateRecipe(const FloatingIslandRecipeV1& recipe, std::string& error)
|
||||||
{
|
{
|
||||||
const std::array<std::int64_t, 32> values = {
|
const std::array<std::int64_t, 25> values = {
|
||||||
recipe.layoutOriginXSubvoxels, recipe.layoutOriginYSubvoxels,
|
recipe.layoutOriginXSubvoxels, recipe.layoutOriginYSubvoxels,
|
||||||
recipe.layoutCenterTopZSubvoxels, recipe.layoutMinimumZSubvoxels,
|
recipe.layoutCenterTopZSubvoxels, recipe.layoutMinimumZSubvoxels,
|
||||||
recipe.layoutMaximumZSubvoxels, recipe.layoutChunkWidthSubvoxels, recipe.islandDensityQ16,
|
recipe.layoutMaximumZSubvoxels, recipe.islandDensityQ16,
|
||||||
recipe.spacingSubvoxels, recipe.minimumRadiusSubvoxels,
|
recipe.spacingSubvoxels, recipe.minimumRadiusSubvoxels,
|
||||||
recipe.maximumRadiusSubvoxels, recipe.thicknessRatioQ16,
|
recipe.maximumRadiusSubvoxels, recipe.thicknessRatioQ16,
|
||||||
recipe.verticalJitterQ16, recipe.topFlattenQ16,
|
recipe.verticalJitterQ16, recipe.topFlattenQ16,
|
||||||
@@ -272,10 +257,6 @@ namespace floating_island_detail
|
|||||||
recipe.blendRadiusSubvoxels, recipe.contourWarpSubvoxels,
|
recipe.blendRadiusSubvoxels, recipe.contourWarpSubvoxels,
|
||||||
recipe.roughnessSubvoxels, recipe.boundarySealSubvoxels,
|
recipe.roughnessSubvoxels, recipe.boundarySealSubvoxels,
|
||||||
recipe.bridgeHalfWidthSubvoxels, recipe.bridgeThicknessSubvoxels,
|
recipe.bridgeHalfWidthSubvoxels, recipe.bridgeThicknessSubvoxels,
|
||||||
static_cast<std::int64_t>(recipe.anchorIslandOrdinal),
|
|
||||||
recipe.anchorPlacementRadiusSubvoxels, recipe.anchorRootRadiusSubvoxels,
|
|
||||||
recipe.anchorIslandMinimumRadiusSubvoxels, recipe.anchorRootBottomZSubvoxels,
|
|
||||||
recipe.anchorMouthOffsetSubvoxels,
|
|
||||||
recipe.routeConstraints.capsuleRadiusSubvoxels,
|
recipe.routeConstraints.capsuleRadiusSubvoxels,
|
||||||
recipe.routeConstraints.capsuleHeightSubvoxels,
|
recipe.routeConstraints.capsuleHeightSubvoxels,
|
||||||
recipe.routeConstraints.maximumGradeQ16};
|
recipe.routeConstraints.maximumGradeQ16};
|
||||||
@@ -285,7 +266,7 @@ namespace floating_island_detail
|
|||||||
? recipe.ownerVoxelSizeMicrons / ScaleGcd : 0;
|
? recipe.ownerVoxelSizeMicrons / ScaleGcd : 0;
|
||||||
const std::uint32_t ExpectedDenominator = ScaleGcd > 0
|
const std::uint32_t ExpectedDenominator = ScaleGcd > 0
|
||||||
? recipe.coreVoxelSizeMicrons / ScaleGcd : 0;
|
? recipe.coreVoxelSizeMicrons / ScaleGcd : 0;
|
||||||
if (recipe.schemaVersion != 1 || recipe.geometryVersion < 1 || recipe.geometryVersion > 8
|
if (recipe.schemaVersion != 1 || (recipe.geometryVersion != 1 && recipe.geometryVersion != 2)
|
||||||
|| (recipe.geometryVersion == 1
|
|| (recipe.geometryVersion == 1
|
||||||
&& (recipe.crustDepthBonusQ16 != 0 || recipe.rootDepthRatioQ16 != 0))
|
&& (recipe.crustDepthBonusQ16 != 0 || recipe.rootDepthRatioQ16 != 0))
|
||||||
|| recipe.unitsPerVoxel != Q
|
|| recipe.unitsPerVoxel != Q
|
||||||
@@ -294,11 +275,6 @@ namespace floating_island_detail
|
|||||||
|| recipe.ownerToCoreScaleNumerator != ExpectedNumerator
|
|| recipe.ownerToCoreScaleNumerator != ExpectedNumerator
|
||||||
|| recipe.ownerToCoreScaleDenominator != ExpectedDenominator
|
|| recipe.ownerToCoreScaleDenominator != ExpectedDenominator
|
||||||
|| recipe.primaryIslandCount < 3 || recipe.primaryIslandCount > 9
|
|| recipe.primaryIslandCount < 3 || recipe.primaryIslandCount > 9
|
||||||
|| recipe.layoutChunkWidthSubvoxels <= 0
|
|
||||||
|| static_cast<long double>(recipe.layoutChunkWidthSubvoxels)*recipe.coreVoxelSizeMicrons
|
|
||||||
< 250000.0L*Q
|
|
||||||
|| static_cast<long double>(recipe.layoutChunkWidthSubvoxels)*recipe.coreVoxelSizeMicrons
|
|
||||||
> 64000000.0L*Q
|
|
||||||
|| recipe.layoutMaximumZSubvoxels <= recipe.layoutMinimumZSubvoxels
|
|| recipe.layoutMaximumZSubvoxels <= recipe.layoutMinimumZSubvoxels
|
||||||
|| recipe.layoutMaximumZSubvoxels - recipe.layoutMinimumZSubvoxels > 2048 * Q
|
|| recipe.layoutMaximumZSubvoxels - recipe.layoutMinimumZSubvoxels > 2048 * Q
|
||||||
|| std::any_of(values.begin(), values.end(), [](auto value) { return !ValidScalar(value); }))
|
|| std::any_of(values.begin(), values.end(), [](auto value) { return !ValidScalar(value); }))
|
||||||
@@ -340,21 +316,6 @@ namespace floating_island_detail
|
|||||||
|| constraints.minimumClearanceSubvoxels < 0
|
|| constraints.minimumClearanceSubvoxels < 0
|
||||||
|| constraints.minimumSupportProbes == 0 || constraints.minimumSupportProbes > 5)
|
|| constraints.minimumSupportProbes == 0 || constraints.minimumSupportProbes > 5)
|
||||||
{ error = "floating-island shape or route parameters are outside the supported range"; return false; }
|
{ error = "floating-island shape or route parameters are outside the supported range"; return false; }
|
||||||
if (recipe.geometryVersion >= 5
|
|
||||||
&& (recipe.anchorIslandOrdinal != recipe.primaryIslandCount - 1
|
|
||||||
|| recipe.anchorPlacementRadiusSubvoxels <= 0
|
|
||||||
|| recipe.anchorRootRadiusSubvoxels < recipe.anchorMouthOffsetSubvoxels
|
|
||||||
+ recipe.routeConstraints.capsuleRadiusSubvoxels + Q
|
|
||||||
|| recipe.anchorIslandMinimumRadiusSubvoxels
|
|
||||||
< recipe.anchorMouthOffsetSubvoxels
|
|
||||||
+ recipe.routeConstraints.capsuleRadiusSubvoxels + Q
|
|
||||||
|| recipe.anchorIslandMinimumRadiusSubvoxels > 64 * Q
|
|
||||||
|| recipe.anchorRootBottomZSubvoxels < recipe.layoutMinimumZSubvoxels
|
|
||||||
+ recipe.boundarySealSubvoxels
|
|
||||||
|| recipe.anchorRootBottomZSubvoxels >= recipe.layoutCenterTopZSubvoxels
|
|
||||||
|| recipe.anchorMouthOffsetSubvoxels <= 0
|
|
||||||
|| recipe.anchorMouthOffsetSubvoxels > 64 * Q))
|
|
||||||
{ error = "floating-island bedrock anchor is outside its passage or rock-thickness bounds"; return false; }
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,69 +435,13 @@ namespace floating_island_detail
|
|||||||
double DeltaX=0.0,DeltaY=0.0,Radial=0.0,Angle=0.0,LobePhase=0.0,TopSurface=0.0;
|
double DeltaX=0.0,DeltaY=0.0,Radial=0.0,Angle=0.0,LobePhase=0.0,TopSurface=0.0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct FIslandColumnCacheInstrumentation
|
|
||||||
{
|
|
||||||
std::uint64_t localHits=0,localMisses=0,localConflictReplacements=0;
|
|
||||||
std::uint64_t localProbeCycles=0,localProbeSamples=0;
|
|
||||||
std::uint64_t tileStartCycles=0,tileCycles=0;
|
|
||||||
VoxelForgeCoreShared::FColumnCacheMetrics shared;
|
|
||||||
FIslandFieldAttributionInstrumentation attribution;
|
|
||||||
};
|
|
||||||
|
|
||||||
inline FIslandColumnCacheInstrumentation*& ActiveIslandColumnCacheInstrumentation() noexcept
|
|
||||||
{
|
|
||||||
static thread_local FIslandColumnCacheInstrumentation* active=nullptr;
|
|
||||||
return active;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline FIslandColumnCacheInstrumentation* SetIslandColumnCacheInstrumentation(
|
|
||||||
FIslandColumnCacheInstrumentation* value) noexcept
|
|
||||||
{
|
|
||||||
FIslandColumnCacheInstrumentation* previous=ActiveIslandColumnCacheInstrumentation();
|
|
||||||
ActiveIslandColumnCacheInstrumentation()=value;
|
|
||||||
SetActiveIslandFieldAttribution(value?&value->attribution:nullptr);
|
|
||||||
return previous;
|
|
||||||
}
|
|
||||||
|
|
||||||
using FSharedIslandColumnCache =
|
using FSharedIslandColumnCache =
|
||||||
VoxelForgeCoreShared::TSharedColumnValueCache<FIslandColumnCacheEntry,16384,4>;
|
VoxelForgeCoreShared::TSharedColumnValueCache<FIslandColumnCacheEntry,16384,4>;
|
||||||
|
|
||||||
struct FIslandColumnTraceKey
|
|
||||||
{
|
|
||||||
bool valid=false;
|
|
||||||
std::uint64_t recipeHash=0,planHash=0,island=0;
|
|
||||||
std::int64_t x=0,y=0;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct FIslandColumnReuseTrace
|
|
||||||
{
|
|
||||||
std::array<FIslandColumnTraceKey,128> recentMisses{};
|
|
||||||
std::array<FIslandColumnTraceKey,128> recentEvictions{};
|
|
||||||
std::uint64_t missSequence=0,printedEvents=0;
|
|
||||||
};
|
|
||||||
|
|
||||||
inline bool SameTraceKey(const FIslandColumnTraceKey& left,
|
|
||||||
const FIslandColumnTraceKey& right) noexcept
|
|
||||||
{
|
|
||||||
return left.valid&&right.valid&&left.recipeHash==right.recipeHash
|
|
||||||
&&left.planHash==right.planHash&&left.island==right.island
|
|
||||||
&&left.x==right.x&&left.y==right.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline std::size_t TraceKeyIndex(const FIslandColumnTraceKey& key) noexcept
|
|
||||||
{
|
|
||||||
std::uint64_t hash=key.island^static_cast<std::uint64_t>(key.x)*0x9e3779b97f4a7c15ull
|
|
||||||
^static_cast<std::uint64_t>(key.y)*0xc2b2ae3d27d4eb4full
|
|
||||||
^key.recipeHash^std::rotl(key.planHash,17);
|
|
||||||
hash^=hash>>33;hash*=0xff51afd7ed558ccdull;hash^=hash>>33;
|
|
||||||
return static_cast<std::size_t>(hash&127u);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline const FIslandColumnCacheEntry& IslandColumnCache(
|
inline const FIslandColumnCacheEntry& IslandColumnCache(
|
||||||
const FloatingIslandRecipeV1& recipe,const FloatingIslandPlanV1& plan,
|
const FloatingIslandRecipeV1& recipe,const FloatingIslandPlanV1& plan,
|
||||||
const FloatingIslandAnchorV1& island,double x,double y)
|
const FloatingIslandAnchorV1& island,double x,double y)
|
||||||
{
|
{
|
||||||
FScopedIslandAttributionTimer columnTimer(EIslandAttributionBucket::Column);
|
|
||||||
// Density samples arrive on integer Core lattice columns; reuse the exact column metrics
|
// Density samples arrive on integer Core lattice columns; reuse the exact column metrics
|
||||||
// across Z samples without changing the field value or touching shared world state.
|
// across Z samples without changing the field value or touching shared world state.
|
||||||
// Les echantillons suivent les colonnes Core entieres; reutiliser les memes metriques XY.
|
// Les echantillons suivent les colonnes Core entieres; reutiliser les memes metriques XY.
|
||||||
@@ -546,7 +451,6 @@ namespace floating_island_detail
|
|||||||
static thread_local VoxelForgeCoreThreadLocal::FLazy<
|
static thread_local VoxelForgeCoreThreadLocal::FLazy<
|
||||||
std::array<FIslandColumnCacheEntry,SetCount*Ways>> Cache;
|
std::array<FIslandColumnCacheEntry,SetCount*Ways>> Cache;
|
||||||
auto& cache=Cache.Get();
|
auto& cache=Cache.Get();
|
||||||
FScopedIslandAttributionTimer localProbeTimer(EIslandAttributionBucket::LocalProbe);
|
|
||||||
const std::int64_t SampleX=static_cast<std::int64_t>(x);
|
const std::int64_t SampleX=static_cast<std::int64_t>(x);
|
||||||
const std::int64_t SampleY=static_cast<std::int64_t>(y);
|
const std::int64_t SampleY=static_cast<std::int64_t>(y);
|
||||||
std::uint64_t IndexKey=island.stableId
|
std::uint64_t IndexKey=island.stableId
|
||||||
@@ -561,7 +465,6 @@ namespace floating_island_detail
|
|||||||
IndexKey^=IndexKey>>33;
|
IndexKey^=IndexKey>>33;
|
||||||
const std::size_t Base=static_cast<std::size_t>(IndexKey&(SetCount-1))*Ways;
|
const std::size_t Base=static_cast<std::size_t>(IndexKey&(SetCount-1))*Ways;
|
||||||
FIslandColumnCacheEntry* Replacement=&cache[Base+((IndexKey>>11)&(Ways-1))];
|
FIslandColumnCacheEntry* Replacement=&cache[Base+((IndexKey>>11)&(Ways-1))];
|
||||||
bool bFoundInvalid=false;
|
|
||||||
for(std::size_t Way=0;Way<Ways;++Way)
|
for(std::size_t Way=0;Way<Ways;++Way)
|
||||||
{
|
{
|
||||||
FIslandColumnCacheEntry& Entry=cache[Base+Way];
|
FIslandColumnCacheEntry& Entry=cache[Base+Way];
|
||||||
@@ -577,64 +480,8 @@ namespace floating_island_detail
|
|||||||
&&Entry.TopHalf==island.topHalfSubvoxels
|
&&Entry.TopHalf==island.topHalfSubvoxels
|
||||||
&&Entry.TopFlattenQ16==recipe.topFlattenQ16
|
&&Entry.TopFlattenQ16==recipe.topFlattenQ16
|
||||||
&&Entry.ContourWarp==recipe.contourWarpSubvoxels;
|
&&Entry.ContourWarp==recipe.contourWarpSubvoxels;
|
||||||
if(bKeyMatches)
|
if(bKeyMatches)return Entry;
|
||||||
{
|
if(!Entry.bValid)Replacement=&Entry;
|
||||||
if(auto* stats=ActiveIslandColumnCacheInstrumentation())++stats->localHits;
|
|
||||||
return Entry;
|
|
||||||
}
|
|
||||||
if(!Entry.bValid&&!bFoundInvalid)
|
|
||||||
{Replacement=&Entry;bFoundInvalid=true;}
|
|
||||||
}
|
|
||||||
localProbeTimer.Stop();
|
|
||||||
if(auto* stats=ActiveIslandColumnCacheInstrumentation())
|
|
||||||
{
|
|
||||||
++stats->localMisses;
|
|
||||||
if(!bFoundInvalid)++stats->localConflictReplacements;
|
|
||||||
static thread_local FIslandColumnReuseTrace trace;
|
|
||||||
if((++trace.missSequence&1023u)==0)
|
|
||||||
{
|
|
||||||
auto& a=stats->attribution;
|
|
||||||
++a.traceSampledMisses;
|
|
||||||
const FIslandColumnTraceKey requested{true,recipe.contentHash,
|
|
||||||
plan.contentHash,island.stableId,SampleX,SampleY};
|
|
||||||
const std::size_t requestSlot=TraceKeyIndex(requested);
|
|
||||||
const bool seenBefore=SameTraceKey(trace.recentMisses[requestSlot],requested);
|
|
||||||
const bool reusedEviction=SameTraceKey(
|
|
||||||
trace.recentEvictions[requestSlot],requested);
|
|
||||||
if(seenBefore)++a.traceSeenBefore;
|
|
||||||
if(reusedEviction)
|
|
||||||
{
|
|
||||||
++a.traceReusedEvictions;
|
|
||||||
trace.recentEvictions[requestSlot].valid=false;
|
|
||||||
}
|
|
||||||
FIslandColumnTraceKey evicted{};
|
|
||||||
std::size_t replacementWay=0;
|
|
||||||
if(!bFoundInvalid&&Replacement)
|
|
||||||
{
|
|
||||||
evicted={Replacement->bValid,Replacement->RecipeContentHash,
|
|
||||||
Replacement->PlanContentHash,Replacement->StableId,
|
|
||||||
Replacement->SampleX,Replacement->SampleY};
|
|
||||||
replacementWay=static_cast<std::size_t>(Replacement-&cache[Base]);
|
|
||||||
++a.traceEvictions;
|
|
||||||
if(evicted.valid)
|
|
||||||
trace.recentEvictions[TraceKeyIndex(evicted)]=evicted;
|
|
||||||
}
|
|
||||||
trace.recentMisses[requestSlot]=requested;
|
|
||||||
if(trace.printedEvents<16&&a.traceCount<a.traceSet.size())
|
|
||||||
{
|
|
||||||
const std::size_t event=a.traceCount++;
|
|
||||||
++trace.printedEvents;
|
|
||||||
a.traceSet[event]=Base/Ways;
|
|
||||||
a.traceX[event]=SampleX;a.traceY[event]=SampleY;
|
|
||||||
a.traceIsland[event]=island.stableId;
|
|
||||||
a.traceEvictedX[event]=evicted.valid?evicted.x:0;
|
|
||||||
a.traceEvictedY[event]=evicted.valid?evicted.y:0;
|
|
||||||
a.traceEvictedIsland[event]=evicted.valid?evicted.island:0;
|
|
||||||
a.traceFlags[event]=static_cast<std::uint8_t>(
|
|
||||||
(!bFoundInvalid?1u:0u)|(seenBefore?2u:0u)|(reusedEviction?4u:0u)
|
|
||||||
|(replacementWay<<3));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto build=[&]()
|
const auto build=[&]()
|
||||||
@@ -661,14 +508,9 @@ namespace floating_island_detail
|
|||||||
Entry.Radial=std::hypot(Entry.DeltaX,Entry.DeltaY);
|
Entry.Radial=std::hypot(Entry.DeltaX,Entry.DeltaY);
|
||||||
Entry.Angle=std::atan2(Entry.DeltaY,Entry.DeltaX);
|
Entry.Angle=std::atan2(Entry.DeltaY,Entry.DeltaX);
|
||||||
Entry.LobePhase=LobePhase(recipe,island);
|
Entry.LobePhase=LobePhase(recipe,island);
|
||||||
double contourOffset=0.0;
|
|
||||||
{
|
|
||||||
FScopedIslandAttributionTimer helixTimer(EIslandAttributionBucket::Helix);
|
|
||||||
contourOffset=LobeOffsetWithPhase(recipe,Entry.Angle,
|
|
||||||
static_cast<double>(island.topZSubvoxels),Entry.LobePhase);
|
|
||||||
}
|
|
||||||
const double EffectiveRadius=std::max(1.0,static_cast<double>(island.radiusSubvoxels)
|
const double EffectiveRadius=std::max(1.0,static_cast<double>(island.radiusSubvoxels)
|
||||||
+contourOffset);
|
+LobeOffsetWithPhase(recipe,Entry.Angle,static_cast<double>(island.topZSubvoxels),
|
||||||
|
Entry.LobePhase));
|
||||||
const double Edge=std::clamp(Entry.Radial/EffectiveRadius,0.0,1.0);
|
const double Edge=std::clamp(Entry.Radial/EffectiveRadius,0.0,1.0);
|
||||||
const double Drop=(1.0-Q16ToDouble(recipe.topFlattenQ16))
|
const double Drop=(1.0-Q16ToDouble(recipe.topFlattenQ16))
|
||||||
*static_cast<double>(island.topHalfSubvoxels)*2.0*Edge*Edge;
|
*static_cast<double>(island.topHalfSubvoxels)*2.0*Edge*Edge;
|
||||||
@@ -684,12 +526,7 @@ namespace floating_island_detail
|
|||||||
static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&plan)),
|
static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&plan)),
|
||||||
plan.contentHash,SampleX,SampleY,island.stableId};
|
plan.contentHash,SampleX,SampleY,island.stableId};
|
||||||
bool computed=false;
|
bool computed=false;
|
||||||
auto* stats=ActiveIslandColumnCacheInstrumentation();
|
Entry=shared->getOrCompute(key,build,computed);
|
||||||
{
|
|
||||||
FScopedIslandAttributionTimer sharedCacheTimer(EIslandAttributionBucket::SharedCache);
|
|
||||||
Entry=shared->getOrCompute(key,build,computed,
|
|
||||||
stats?&stats->shared:nullptr);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else Entry=build();
|
else Entry=build();
|
||||||
*Replacement=Entry;
|
*Replacement=Entry;
|
||||||
@@ -752,7 +589,6 @@ namespace floating_island_detail
|
|||||||
const FloatingIslandAnchorV1& island,
|
const FloatingIslandAnchorV1& island,
|
||||||
std::int32_t rootIndex, double x, double y, double z)
|
std::int32_t rootIndex, double x, double y, double z)
|
||||||
{
|
{
|
||||||
FScopedIslandAttributionTimer attributionTimer(EIslandAttributionBucket::Root);
|
|
||||||
// Cache derived root constants per worker. The complete immutable input key keeps this
|
// Cache derived root constants per worker. The complete immutable input key keeps this
|
||||||
// value-only cache safe across worlds, epochs and worker schedules.
|
// value-only cache safe across worlds, epochs and worker schedules.
|
||||||
// Mettre en cache les constantes par worker; la cle complete evite tout partage d'etat.
|
// Mettre en cache les constantes par worker; la cle complete evite tout partage d'etat.
|
||||||
@@ -823,38 +659,6 @@ namespace floating_island_detail
|
|||||||
return (std::sqrt(std::max(q, 0.0)) - 1.0) * Parameters.MinimumRadius;
|
return (std::sqrt(std::max(q, 0.0)) - 1.0) * Parameters.MinimumRadius;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline FRootLobeParameters RootLobeParametersForBounds(
|
|
||||||
const FloatingIslandRecipeV1& recipe,const FloatingIslandAnchorV1& island,
|
|
||||||
std::int32_t rootIndex)
|
|
||||||
{
|
|
||||||
constexpr std::array<std::array<double,2>,4> pattern{{
|
|
||||||
{{-0.92,-0.12}},{{0.28,0.98}},{{0.83,-0.42}},{{-0.16,-0.94}}}};
|
|
||||||
const std::uint64_t salt=0x524f4f540000ull+static_cast<std::uint64_t>(rootIndex);
|
|
||||||
const std::uint64_t roll=Roll(recipe.seed,island.stableId,salt);
|
|
||||||
const double radius=static_cast<double>(island.radiusSubvoxels);
|
|
||||||
const double rootDepth=radius*Q16ToDouble(recipe.rootDepthRatioQ16);
|
|
||||||
const double crustBottom=static_cast<double>(IslandCrustBottom(recipe,island));
|
|
||||||
const bool centralButtress=rootIndex==4;
|
|
||||||
const double orbit=centralButtress?0.0:radius*(0.17+0.07*UnitHash(roll));
|
|
||||||
const double rootX=static_cast<double>(island.centerSubvoxels[0])
|
|
||||||
+(centralButtress?0.0:pattern[static_cast<std::size_t>(rootIndex)][0]*orbit);
|
|
||||||
const double rootY=static_cast<double>(island.centerSubvoxels[1])
|
|
||||||
+(centralButtress?0.0:pattern[static_cast<std::size_t>(rootIndex)][1]*orbit);
|
|
||||||
const double shortRootVariation=UnitHash(Roll(recipe.seed,island.stableId,salt^0x4c454eull));
|
|
||||||
const double lengthRatio=centralButtress?1.0:(rootIndex==0?1.0
|
|
||||||
:Q16ToDouble(island.taperEndQ16)*(0.82+0.18*shortRootVariation));
|
|
||||||
const double lowerDepth=rootDepth*lengthRatio;
|
|
||||||
const double embedDepth=rootDepth*(centralButtress?0.22:(rootIndex==0?0.30:0.22));
|
|
||||||
const double verticalRadius=0.5*(lowerDepth+embedDepth);
|
|
||||||
const double centerZ=crustBottom+0.5*(embedDepth-lowerDepth);
|
|
||||||
const double rx=radius*(centralButtress?0.25
|
|
||||||
:0.15+0.055*UnitHash(Roll(recipe.seed,island.stableId,salt^0x5258ull)));
|
|
||||||
const double ry=radius*(centralButtress?0.25
|
|
||||||
:0.15+0.065*UnitHash(Roll(recipe.seed,island.stableId,salt^0x5259ull)));
|
|
||||||
return {rootX,rootY,centerZ,rx*rx,ry*ry,verticalRadius*verticalRadius,
|
|
||||||
std::min({rx,ry,verticalRadius})};
|
|
||||||
}
|
|
||||||
|
|
||||||
inline double ApplySurfaceDetail(const FloatingIslandRecipeV1& recipe,
|
inline double ApplySurfaceDetail(const FloatingIslandRecipeV1& recipe,
|
||||||
std::uint64_t featureId, double structural,
|
std::uint64_t featureId, double structural,
|
||||||
double x, double y, double z, double detailScale = 1.0)
|
double x, double y, double z, double detailScale = 1.0)
|
||||||
@@ -873,7 +677,6 @@ namespace floating_island_detail
|
|||||||
const FloatingIslandAnchorV1& island,
|
const FloatingIslandAnchorV1& island,
|
||||||
double x, double y, double z)
|
double x, double y, double z)
|
||||||
{
|
{
|
||||||
FScopedIslandAttributionTimer islandTimer(EIslandAttributionBucket::Island);
|
|
||||||
if (recipe.geometryVersion < 2)
|
if (recipe.geometryVersion < 2)
|
||||||
{
|
{
|
||||||
const double dx = x - static_cast<double>(island.centerSubvoxels[0]);
|
const double dx = x - static_cast<double>(island.centerSubvoxels[0]);
|
||||||
@@ -901,13 +704,8 @@ namespace floating_island_detail
|
|||||||
const double radial=Column.Radial;
|
const double radial=Column.Radial;
|
||||||
const double topZ = static_cast<double>(island.topZSubvoxels);
|
const double topZ = static_cast<double>(island.topZSubvoxels);
|
||||||
const double radius = static_cast<double>(island.radiusSubvoxels);
|
const double radius = static_cast<double>(island.radiusSubvoxels);
|
||||||
double contourOffset=0.0;
|
|
||||||
{
|
|
||||||
FScopedIslandAttributionTimer helixTimer(EIslandAttributionBucket::Helix);
|
|
||||||
contourOffset=LobeOffsetWithPhase(recipe,Column.Angle,z,Column.LobePhase);
|
|
||||||
}
|
|
||||||
const double effectiveRadius = std::max(0.0,
|
const double effectiveRadius = std::max(0.0,
|
||||||
radius + contourOffset);
|
radius + LobeOffsetWithPhase(recipe,Column.Angle,z,Column.LobePhase));
|
||||||
const double edge = std::clamp(radial / std::max(effectiveRadius, 1.0), 0.0, 1.0);
|
const double edge = std::clamp(radial / std::max(effectiveRadius, 1.0), 0.0, 1.0);
|
||||||
const double lowerSurface = static_cast<double>(IslandCrustBottom(recipe, island))
|
const double lowerSurface = static_cast<double>(IslandCrustBottom(recipe, island))
|
||||||
+ radius * 0.16 * edge * edge;
|
+ radius * 0.16 * edge * edge;
|
||||||
@@ -920,28 +718,6 @@ namespace floating_island_detail
|
|||||||
const double rootBlend = std::clamp(radius * 0.12, 2.0 * Q, 5.0 * Q);
|
const double rootBlend = std::clamp(radius * 0.12, 2.0 * Q, 5.0 * Q);
|
||||||
for (std::int32_t root = 0; root < rootCount; ++root)
|
for (std::int32_t root = 0; root < rootCount; ++root)
|
||||||
sdf = SmoothMin(sdf, RootLobeDensity(recipe, island, root, x, y, z), rootBlend);
|
sdf = SmoothMin(sdf, RootLobeDensity(recipe, island, root, x, y, z), rootBlend);
|
||||||
if (recipe.geometryVersion >= 5 && recipe.geometryVersion < 8
|
|
||||||
&& island.ordinal == recipe.anchorIslandOrdinal
|
|
||||||
&& recipe.anchorRootBottomZSubvoxels < island.bottomZSubvoxels)
|
|
||||||
{
|
|
||||||
// The central buttress must meet the island crust underside. IslandBottom
|
|
||||||
// also includes the four shorter root lobes and sits below that surface.
|
|
||||||
const double rootTop = static_cast<double>(IslandCrustBottom(recipe, island));
|
|
||||||
const double rootBottom = static_cast<double>(recipe.anchorRootBottomZSubvoxels);
|
|
||||||
const double depth = std::max(1.0, rootTop - rootBottom);
|
|
||||||
const double blend = std::clamp(static_cast<double>(recipe.anchorRootRadiusSubvoxels)
|
|
||||||
* 0.12, 3.0 * Q, 6.0 * Q);
|
|
||||||
// A slightly flared lower buttress joins the island's own root lobes to bedrock.
|
|
||||||
// Une base legerement evasee relie les racines de l'ile a la roche de fond.
|
|
||||||
const double t = std::clamp((rootTop - z) / depth, 0.0, 1.0);
|
|
||||||
const double rootRadius = static_cast<double>(recipe.anchorRootRadiusSubvoxels)
|
|
||||||
* (0.92 + 0.08 * SmoothStep(t));
|
|
||||||
double rootSdf = std::max({Column.Radial - rootRadius,
|
|
||||||
rootBottom - z, z - rootTop});
|
|
||||||
rootSdf = ApplySurfaceDetail(recipe, island.stableId ^ 0x414e43484f52524full,
|
|
||||||
rootSdf, x, y, z, 0.75);
|
|
||||||
sdf = SmoothMin(sdf, rootSdf, blend);
|
|
||||||
}
|
|
||||||
const double topDetailScale = topDistance >= std::max(sideDistance, undersideDistance)
|
const double topDetailScale = topDistance >= std::max(sideDistance, undersideDistance)
|
||||||
|| z >= static_cast<double>(IslandCrustBottom(recipe, island)) + radius * 0.45
|
|| z >= static_cast<double>(IslandCrustBottom(recipe, island)) + radius * 0.45
|
||||||
? 0.25 : 1.0;
|
? 0.25 : 1.0;
|
||||||
@@ -954,7 +730,6 @@ namespace floating_island_detail
|
|||||||
bool* topSurface = nullptr,
|
bool* topSurface = nullptr,
|
||||||
double* alongFraction = nullptr)
|
double* alongFraction = nullptr)
|
||||||
{
|
{
|
||||||
FScopedIslandAttributionTimer connectorTimer(EIslandAttributionBucket::Connector);
|
|
||||||
const double ax = static_cast<double>(edge.startSubvoxels[0]);
|
const double ax = static_cast<double>(edge.startSubvoxels[0]);
|
||||||
const double ay = static_cast<double>(edge.startSubvoxels[1]);
|
const double ay = static_cast<double>(edge.startSubvoxels[1]);
|
||||||
const double az = static_cast<double>(edge.startSubvoxels[2]);
|
const double az = static_cast<double>(edge.startSubvoxels[2]);
|
||||||
@@ -973,37 +748,7 @@ namespace floating_island_detail
|
|||||||
const double t = std::clamp(along / length, 0.0, 1.0);
|
const double t = std::clamp(along / length, 0.0, 1.0);
|
||||||
if (alongFraction) *alongFraction = t;
|
if (alongFraction) *alongFraction = t;
|
||||||
const double deck = az + (static_cast<double>(edge.endSubvoxels[2]) - az) * t;
|
const double deck = az + (static_cast<double>(edge.endSubvoxels[2]) - az) * t;
|
||||||
const std::uint64_t westernEdgeId = StableId(recipe.seed, 0x200000000ull,
|
|
||||||
0x425249444745ull);
|
|
||||||
const bool westernSupportConnector=edge.stableId==westernEdgeId;
|
|
||||||
if(recipe.geometryVersion>=2&&WorldAssemblyEvaluatorVersion>=183
|
|
||||||
&&westernSupportConnector)
|
|
||||||
{
|
|
||||||
const double phase=static_cast<double>(Roll(recipe.seed,edge.stableId,0x5749445448ull)
|
|
||||||
&0xffffffffull)*(2.0*Pi/4294967296.0);
|
|
||||||
const double localHalfWidth=std::max(
|
|
||||||
static_cast<double>(recipe.routeConstraints.capsuleRadiusSubvoxels+Q),
|
|
||||||
static_cast<double>(edge.halfWidthSubvoxels)
|
|
||||||
+0.48*static_cast<double>(Q)*std::sin(6.0*Pi*t+phase));
|
|
||||||
const double localThickness=static_cast<double>(edge.thicknessSubvoxels);
|
|
||||||
const double rounding=std::min({1.5*static_cast<double>(Q),
|
|
||||||
localHalfWidth*0.6,localThickness*0.45});
|
|
||||||
const double roundedX=side-(localHalfWidth-rounding);
|
|
||||||
const double roundedY=std::abs(along-0.5*length)-(0.5*length-rounding);
|
|
||||||
const double roundedZ=std::abs(z-(deck-0.5*localThickness))
|
|
||||||
-(0.5*localThickness-rounding);
|
|
||||||
const double roundedOutside=std::sqrt(
|
|
||||||
std::pow(std::max(roundedX,0.0),2)
|
|
||||||
+std::pow(std::max(roundedY,0.0),2)
|
|
||||||
+std::pow(std::max(roundedZ,0.0),2));
|
|
||||||
const double roundedInside=std::min(std::max(roundedX,
|
|
||||||
std::max(roundedY,roundedZ)),0.0);
|
|
||||||
if(topSurface)*topSurface=z-deck>=std::max(horizontal,
|
|
||||||
deck-localThickness-z);
|
|
||||||
return roundedOutside+roundedInside-rounding;
|
|
||||||
}
|
|
||||||
const double arch = recipe.geometryVersion >= 2
|
const double arch = recipe.geometryVersion >= 2
|
||||||
&& WorldAssemblyEvaluatorVersion < 172
|
|
||||||
? 2.0 * static_cast<double>(edge.thicknessSubvoxels) * (4.0 * t * (1.0 - t))
|
? 2.0 * static_cast<double>(edge.thicknessSubvoxels) * (4.0 * t * (1.0 - t))
|
||||||
: 0.0;
|
: 0.0;
|
||||||
const double upper = z - deck;
|
const double upper = z - deck;
|
||||||
@@ -1012,227 +757,6 @@ namespace floating_island_detail
|
|||||||
return std::max({horizontal, upper, lower});
|
return std::max({horizontal, upper, lower});
|
||||||
}
|
}
|
||||||
|
|
||||||
struct FScalarRange
|
|
||||||
{
|
|
||||||
double minimum=0.0,maximum=0.0;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct FDensityRange
|
|
||||||
{
|
|
||||||
double minimum=0.0,maximum=0.0;
|
|
||||||
};
|
|
||||||
|
|
||||||
inline double DistanceToRange(double minimum,double maximum,double value)
|
|
||||||
{
|
|
||||||
if(value<minimum)return minimum-value;
|
|
||||||
if(value>maximum)return value-maximum;
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline double MaximumDistanceToRange(double minimum,double maximum,double value)
|
|
||||||
{
|
|
||||||
return std::max(std::abs(minimum-value),std::abs(maximum-value));
|
|
||||||
}
|
|
||||||
|
|
||||||
inline FScalarRange AffineRange(double xMinimum,double xMaximum,double xOffset,
|
|
||||||
double xScale,double yMinimum,double yMaximum,double yOffset,double yScale)
|
|
||||||
{
|
|
||||||
const double xa=(xMinimum-xOffset)*xScale,xb=(xMaximum-xOffset)*xScale;
|
|
||||||
const double ya=(yMinimum-yOffset)*yScale,yb=(yMaximum-yOffset)*yScale;
|
|
||||||
return {std::min(xa,xb)+std::min(ya,yb),
|
|
||||||
std::max(xa,xb)+std::max(ya,yb)};
|
|
||||||
}
|
|
||||||
|
|
||||||
inline FScalarRange AbsoluteRange(double minimum,double maximum)
|
|
||||||
{
|
|
||||||
const double minimumAbsolute=minimum<=0.0&&maximum>=0.0
|
|
||||||
?0.0:std::min(std::abs(minimum),std::abs(maximum));
|
|
||||||
return {minimumAbsolute,std::max(std::abs(minimum),std::abs(maximum))};
|
|
||||||
}
|
|
||||||
|
|
||||||
inline double RectangleSdf(double x,double y)
|
|
||||||
{
|
|
||||||
return std::hypot(std::max(x,0.0),std::max(y,0.0))
|
|
||||||
+std::min(std::max(x,y),0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline double RoundedBoxSdf(double x,double y,double z)
|
|
||||||
{
|
|
||||||
return std::sqrt(std::max(x,0.0)*std::max(x,0.0)
|
|
||||||
+std::max(y,0.0)*std::max(y,0.0)
|
|
||||||
+std::max(z,0.0)*std::max(z,0.0))
|
|
||||||
+std::min(std::max(x,std::max(y,z)),0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline FDensityRange RootLobeRange(const FloatingIslandRecipeV1& recipe,
|
|
||||||
const FloatingIslandAnchorV1& island,std::int32_t rootIndex,
|
|
||||||
const std::array<double,3>& minimum,const std::array<double,3>& maximum)
|
|
||||||
{
|
|
||||||
const FRootLobeParameters root=RootLobeParametersForBounds(recipe,island,rootIndex);
|
|
||||||
const double dxMinimum=DistanceToRange(minimum[0],maximum[0],root.RootX);
|
|
||||||
const double dyMinimum=DistanceToRange(minimum[1],maximum[1],root.RootY);
|
|
||||||
const double dzMinimum=DistanceToRange(minimum[2],maximum[2],root.CenterZ);
|
|
||||||
const double dxMaximum=MaximumDistanceToRange(minimum[0],maximum[0],root.RootX);
|
|
||||||
const double dyMaximum=MaximumDistanceToRange(minimum[1],maximum[1],root.RootY);
|
|
||||||
const double dzMaximum=MaximumDistanceToRange(minimum[2],maximum[2],root.CenterZ);
|
|
||||||
const double qMinimum=dxMinimum*dxMinimum/root.RadiusXSquared
|
|
||||||
+dyMinimum*dyMinimum/root.RadiusYSquared
|
|
||||||
+dzMinimum*dzMinimum/root.VerticalRadiusSquared;
|
|
||||||
const double qMaximum=dxMaximum*dxMaximum/root.RadiusXSquared
|
|
||||||
+dyMaximum*dyMaximum/root.RadiusYSquared
|
|
||||||
+dzMaximum*dzMaximum/root.VerticalRadiusSquared;
|
|
||||||
return {(std::sqrt(std::max(qMinimum,0.0))-1.0)*root.MinimumRadius,
|
|
||||||
(std::sqrt(std::max(qMaximum,0.0))-1.0)*root.MinimumRadius};
|
|
||||||
}
|
|
||||||
|
|
||||||
inline FDensityRange IslandDensityRange(const FloatingIslandRecipeV1& recipe,
|
|
||||||
const FloatingIslandAnchorV1& island,const std::array<double,3>& minimum,
|
|
||||||
const std::array<double,3>& maximum)
|
|
||||||
{
|
|
||||||
const double radius=static_cast<double>(island.radiusSubvoxels);
|
|
||||||
const double contour=static_cast<double>(recipe.contourWarpSubvoxels);
|
|
||||||
const double radialMinimum=std::hypot(
|
|
||||||
DistanceToRange(minimum[0],maximum[0],
|
|
||||||
static_cast<double>(island.centerSubvoxels[0])),
|
|
||||||
DistanceToRange(minimum[1],maximum[1],
|
|
||||||
static_cast<double>(island.centerSubvoxels[1])));
|
|
||||||
const double radialMaximum=std::max({
|
|
||||||
std::hypot(minimum[0]-island.centerSubvoxels[0],minimum[1]-island.centerSubvoxels[1]),
|
|
||||||
std::hypot(minimum[0]-island.centerSubvoxels[0],maximum[1]-island.centerSubvoxels[1]),
|
|
||||||
std::hypot(maximum[0]-island.centerSubvoxels[0],minimum[1]-island.centerSubvoxels[1]),
|
|
||||||
std::hypot(maximum[0]-island.centerSubvoxels[0],maximum[1]-island.centerSubvoxels[1])});
|
|
||||||
|
|
||||||
const double top=static_cast<double>(island.topZSubvoxels);
|
|
||||||
const double topDrop=std::max(0.0,(1.0-Q16ToDouble(recipe.topFlattenQ16))
|
|
||||||
*static_cast<double>(island.topHalfSubvoxels)*2.0);
|
|
||||||
const double topDistanceMinimum=minimum[2]-top;
|
|
||||||
const double topDistanceMaximum=maximum[2]-(top-topDrop);
|
|
||||||
const double sideDistanceMinimum=radialMinimum-std::max(0.0,radius+contour);
|
|
||||||
const double sideDistanceMaximum=recipe.geometryVersion<2
|
|
||||||
?radialMaximum:radialMaximum-std::max(0.0,radius-contour);
|
|
||||||
double lowerMinimum=0.0,lowerMaximum=0.0;
|
|
||||||
if(recipe.geometryVersion<2)
|
|
||||||
{
|
|
||||||
const double bottom=static_cast<double>(island.bottomZSubvoxels);
|
|
||||||
lowerMinimum=bottom-maximum[2];
|
|
||||||
lowerMaximum=bottom-minimum[2];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
const double crust=static_cast<double>(IslandCrustBottom(recipe,island));
|
|
||||||
lowerMinimum=crust-maximum[2];
|
|
||||||
lowerMaximum=crust+radius*0.16-minimum[2];
|
|
||||||
}
|
|
||||||
double structuralMinimum=std::max({topDistanceMinimum,
|
|
||||||
sideDistanceMinimum,lowerMinimum});
|
|
||||||
double structuralMaximum=std::max({topDistanceMaximum,
|
|
||||||
sideDistanceMaximum,lowerMaximum});
|
|
||||||
|
|
||||||
if(recipe.geometryVersion>=2)
|
|
||||||
{
|
|
||||||
const double rootBlend=std::clamp(radius*0.12,2.0*Q,5.0*Q);
|
|
||||||
for(std::int32_t root=0;root<RootCount(recipe,island);++root)
|
|
||||||
{
|
|
||||||
const FDensityRange rootRange=RootLobeRange(recipe,island,root,minimum,maximum);
|
|
||||||
structuralMinimum=std::min(structuralMinimum,rootRange.minimum)-rootBlend*0.25;
|
|
||||||
structuralMaximum=std::min(structuralMaximum,rootRange.maximum);
|
|
||||||
}
|
|
||||||
if(recipe.geometryVersion>=5&&recipe.geometryVersion<8
|
|
||||||
&&island.ordinal==recipe.anchorIslandOrdinal
|
|
||||||
&&recipe.anchorRootBottomZSubvoxels<island.bottomZSubvoxels)
|
|
||||||
{
|
|
||||||
const double rootTop=static_cast<double>(IslandCrustBottom(recipe,island));
|
|
||||||
const double rootBottom=static_cast<double>(recipe.anchorRootBottomZSubvoxels);
|
|
||||||
const double rootRadius=static_cast<double>(recipe.anchorRootRadiusSubvoxels);
|
|
||||||
const double anchorMinimum=std::max({radialMinimum-rootRadius,
|
|
||||||
rootBottom-maximum[2],minimum[2]-rootTop});
|
|
||||||
const double anchorMaximum=std::max({radialMaximum-0.92*rootRadius,
|
|
||||||
rootBottom-minimum[2],maximum[2]-rootTop});
|
|
||||||
const double anchorDetail=0.75*static_cast<double>(recipe.roughnessSubvoxels);
|
|
||||||
const double anchorBlend=std::clamp(rootRadius*0.12,3.0*Q,6.0*Q);
|
|
||||||
structuralMinimum=std::min(structuralMinimum,anchorMinimum-anchorDetail)
|
|
||||||
-anchorBlend*0.25;
|
|
||||||
structuralMaximum=std::min(structuralMaximum,anchorMaximum+anchorDetail);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const double finalDetail=static_cast<double>(recipe.roughnessSubvoxels);
|
|
||||||
return {structuralMinimum-finalDetail,structuralMaximum+finalDetail};
|
|
||||||
}
|
|
||||||
|
|
||||||
inline FDensityRange ConnectorDensityRange(const FloatingIslandRecipeV1& recipe,
|
|
||||||
const FloatingIslandConnectorV1& edge,const std::array<double,3>& minimum,
|
|
||||||
const std::array<double,3>& maximum)
|
|
||||||
{
|
|
||||||
const double ax=static_cast<double>(edge.startSubvoxels[0]);
|
|
||||||
const double ay=static_cast<double>(edge.startSubvoxels[1]);
|
|
||||||
const double az=static_cast<double>(edge.startSubvoxels[2]);
|
|
||||||
const double dx=static_cast<double>(edge.endSubvoxels[0])-ax;
|
|
||||||
const double dy=static_cast<double>(edge.endSubvoxels[1])-ay;
|
|
||||||
const double dz=static_cast<double>(edge.endSubvoxels[2])-az;
|
|
||||||
const double length=std::hypot(dx,dy);
|
|
||||||
if(!(length>0.0))return {static_cast<double>(MaxDensity),static_cast<double>(MaxDensity)};
|
|
||||||
const double ux=dx/length,uy=dy/length;
|
|
||||||
const FScalarRange along=AffineRange(minimum[0],maximum[0],ax,ux,
|
|
||||||
minimum[1],maximum[1],ay,uy);
|
|
||||||
const FScalarRange signedSide=AffineRange(minimum[0],maximum[0],ax,-uy,
|
|
||||||
minimum[1],maximum[1],ay,ux);
|
|
||||||
const FScalarRange side=AbsoluteRange(signedSide.minimum,signedSide.maximum);
|
|
||||||
const double qxMinimum=side.minimum-static_cast<double>(edge.halfWidthSubvoxels);
|
|
||||||
const double qxMaximum=side.maximum-static_cast<double>(edge.halfWidthSubvoxels);
|
|
||||||
const FScalarRange alongCenter=AbsoluteRange(along.minimum-0.5*length,
|
|
||||||
along.maximum-0.5*length);
|
|
||||||
const double qyMinimum=alongCenter.minimum-0.5*length;
|
|
||||||
const double qyMaximum=alongCenter.maximum-0.5*length;
|
|
||||||
const double horizontalMinimum=RectangleSdf(qxMinimum,qyMinimum);
|
|
||||||
const double horizontalMaximum=RectangleSdf(qxMaximum,qyMaximum);
|
|
||||||
|
|
||||||
const double tMinimum=std::clamp(along.minimum/length,0.0,1.0);
|
|
||||||
const double tMaximum=std::clamp(along.maximum/length,0.0,1.0);
|
|
||||||
const double deckAtMinimum=az+dz*tMinimum;
|
|
||||||
const double deckAtMaximum=az+dz*tMaximum;
|
|
||||||
const double deckMinimum=std::min(deckAtMinimum,deckAtMaximum);
|
|
||||||
const double deckMaximum=std::max(deckAtMinimum,deckAtMaximum);
|
|
||||||
const std::uint64_t westernEdgeId=StableId(recipe.seed,0x200000000ull,
|
|
||||||
0x425249444745ull);
|
|
||||||
const bool westernSupportConnector=edge.stableId==westernEdgeId;
|
|
||||||
if(recipe.geometryVersion>=2&&WorldAssemblyEvaluatorVersion>=183
|
|
||||||
&&westernSupportConnector)
|
|
||||||
{
|
|
||||||
const double minimumHalfWidth=static_cast<double>(recipe.routeConstraints.capsuleRadiusSubvoxels+Q);
|
|
||||||
const double baseHalfWidth=static_cast<double>(edge.halfWidthSubvoxels);
|
|
||||||
const double halfWidthMinimum=std::max(minimumHalfWidth,baseHalfWidth-0.48*Q);
|
|
||||||
const double halfWidthMaximum=std::max(minimumHalfWidth,baseHalfWidth+0.48*Q);
|
|
||||||
const double roundingMinimum=std::min({1.5*Q,halfWidthMinimum*0.6,
|
|
||||||
static_cast<double>(edge.thicknessSubvoxels)*0.45});
|
|
||||||
const double roundingMaximum=std::min({1.5*Q,halfWidthMaximum*0.6,
|
|
||||||
static_cast<double>(edge.thicknessSubvoxels)*0.45});
|
|
||||||
const double roundedXMinimum=side.minimum-halfWidthMaximum+roundingMinimum;
|
|
||||||
const double roundedXMaximum=side.maximum-halfWidthMinimum+roundingMaximum;
|
|
||||||
const double roundedYMinimum=alongCenter.minimum-0.5*length+roundingMinimum;
|
|
||||||
const double roundedYMaximum=alongCenter.maximum-0.5*length+roundingMaximum;
|
|
||||||
const double halfThickness=0.5*static_cast<double>(edge.thicknessSubvoxels);
|
|
||||||
const double centerMinimum=deckMinimum-halfThickness;
|
|
||||||
const double centerMaximum=deckMaximum-halfThickness;
|
|
||||||
const double differenceMinimum=minimum[2]-centerMaximum;
|
|
||||||
const double differenceMaximum=maximum[2]-centerMinimum;
|
|
||||||
const FScalarRange absoluteDifference=AbsoluteRange(differenceMinimum,differenceMaximum);
|
|
||||||
const double roundedZMinimum=absoluteDifference.minimum-halfThickness+roundingMinimum;
|
|
||||||
const double roundedZMaximum=absoluteDifference.maximum-halfThickness+roundingMaximum;
|
|
||||||
return {RoundedBoxSdf(roundedXMinimum,roundedYMinimum,roundedZMinimum),
|
|
||||||
RoundedBoxSdf(roundedXMaximum,roundedYMaximum,roundedZMaximum)};
|
|
||||||
}
|
|
||||||
|
|
||||||
const double thickness=static_cast<double>(edge.thicknessSubvoxels);
|
|
||||||
const double maximumArch=recipe.geometryVersion>=2&&WorldAssemblyEvaluatorVersion<172
|
|
||||||
?2.0*thickness:0.0;
|
|
||||||
const double upperMinimum=minimum[2]-deckMaximum;
|
|
||||||
const double upperMaximum=maximum[2]-deckMinimum;
|
|
||||||
const double lowerMinimum=deckMinimum-thickness-maximumArch-maximum[2];
|
|
||||||
const double lowerMaximum=deckMaximum-thickness-minimum[2];
|
|
||||||
return {std::max({horizontalMinimum,upperMinimum,lowerMinimum}),
|
|
||||||
std::max({horizontalMaximum,upperMaximum,lowerMaximum})};
|
|
||||||
}
|
|
||||||
|
|
||||||
inline std::int64_t FieldDensity(const FloatingIslandRecipeV1& recipe,
|
inline std::int64_t FieldDensity(const FloatingIslandRecipeV1& recipe,
|
||||||
const FloatingIslandPlanV1& plan,
|
const FloatingIslandPlanV1& plan,
|
||||||
std::int64_t x, std::int64_t y, std::int64_t z)
|
std::int64_t x, std::int64_t y, std::int64_t z)
|
||||||
@@ -1364,15 +888,9 @@ namespace floating_island_detail
|
|||||||
FloatingIslandAnchorV1& island)
|
FloatingIslandAnchorV1& island)
|
||||||
{
|
{
|
||||||
const std::int64_t pad = IslandBlendMargin(recipe) + recipe.roughnessSubvoxels;
|
const std::int64_t pad = IslandBlendMargin(recipe) + recipe.roughnessSubvoxels;
|
||||||
const bool isAnchorRoot=recipe.geometryVersion>=5&&recipe.geometryVersion<8
|
const std::int64_t reach = island.radiusSubvoxels + recipe.contourWarpSubvoxels + pad;
|
||||||
&&island.ordinal==recipe.anchorIslandOrdinal;
|
|
||||||
const std::int64_t reach = std::max(island.radiusSubvoxels,
|
|
||||||
isAnchorRoot?recipe.anchorRootRadiusSubvoxels:std::int64_t{0})
|
|
||||||
+ recipe.contourWarpSubvoxels + pad;
|
|
||||||
island.influenceMinimumSubvoxels = {island.centerSubvoxels[0] - reach,
|
island.influenceMinimumSubvoxels = {island.centerSubvoxels[0] - reach,
|
||||||
island.centerSubvoxels[1] - reach,
|
island.centerSubvoxels[1] - reach, island.bottomZSubvoxels - pad};
|
||||||
(isAnchorRoot?std::min(island.bottomZSubvoxels,recipe.anchorRootBottomZSubvoxels)
|
|
||||||
:island.bottomZSubvoxels)-pad};
|
|
||||||
island.influenceMaximumSubvoxels = {island.centerSubvoxels[0] + reach,
|
island.influenceMaximumSubvoxels = {island.centerSubvoxels[0] + reach,
|
||||||
island.centerSubvoxels[1] + reach, island.topZSubvoxels + pad};
|
island.centerSubvoxels[1] + reach, island.topZSubvoxels + pad};
|
||||||
}
|
}
|
||||||
@@ -1417,19 +935,12 @@ namespace floating_island_detail
|
|||||||
|
|
||||||
std::unordered_set<std::uint64_t> islandIds;
|
std::unordered_set<std::uint64_t> islandIds;
|
||||||
std::unordered_set<std::uint32_t> ordinals;
|
std::unordered_set<std::uint32_t> ordinals;
|
||||||
const FloatingIslandAnchorV1* anchorIsland = nullptr;
|
|
||||||
for (const auto& island : plan.islands)
|
for (const auto& island : plan.islands)
|
||||||
{
|
{
|
||||||
const bool isAnchor=recipe.geometryVersion>=5
|
|
||||||
&&island.ordinal==recipe.anchorIslandOrdinal;
|
|
||||||
if(isAnchor)anchorIsland=&island;
|
|
||||||
if (island.stableId == 0 || !islandIds.insert(island.stableId).second
|
if (island.stableId == 0 || !islandIds.insert(island.stableId).second
|
||||||
|| !ordinals.insert(island.ordinal).second
|
|| !ordinals.insert(island.ordinal).second
|
||||||
|| island.radiusSubvoxels < (isAnchor?recipe.anchorIslandMinimumRadiusSubvoxels
|
|| island.radiusSubvoxels < recipe.minimumRadiusSubvoxels
|
||||||
:recipe.minimumRadiusSubvoxels)
|
|| island.radiusSubvoxels > recipe.maximumRadiusSubvoxels
|
||||||
|| island.radiusSubvoxels > (isAnchor
|
|
||||||
?std::max(recipe.anchorIslandMinimumRadiusSubvoxels,recipe.maximumRadiusSubvoxels)
|
|
||||||
:recipe.maximumRadiusSubvoxels)
|
|
||||||
|| island.topHalfSubvoxels <= 0 || island.topZSubvoxels <= island.bottomZSubvoxels
|
|| island.topHalfSubvoxels <= 0 || island.topZSubvoxels <= island.bottomZSubvoxels
|
||||||
|| island.taperEndQ16 < recipe.taperEndMinimumQ16
|
|| island.taperEndQ16 < recipe.taperEndMinimumQ16
|
||||||
|| island.taperEndQ16 > recipe.taperEndMaximumQ16
|
|| island.taperEndQ16 > recipe.taperEndMaximumQ16
|
||||||
@@ -1446,17 +957,12 @@ namespace floating_island_detail
|
|||||||
{ error = "floating-island anchor has invalid coordinates or an inverted bound"; return false; }
|
{ error = "floating-island anchor has invalid coordinates or an inverted bound"; return false; }
|
||||||
}
|
}
|
||||||
const std::int64_t pad = IslandBlendMargin(recipe) + recipe.roughnessSubvoxels;
|
const std::int64_t pad = IslandBlendMargin(recipe) + recipe.roughnessSubvoxels;
|
||||||
const bool hasAnchorRoot=recipe.geometryVersion>=5&&recipe.geometryVersion<8&&isAnchor;
|
const std::int64_t reach = island.radiusSubvoxels + recipe.contourWarpSubvoxels + pad;
|
||||||
const std::int64_t reach = std::max(island.radiusSubvoxels,
|
|
||||||
hasAnchorRoot?recipe.anchorRootRadiusSubvoxels:std::int64_t{0})
|
|
||||||
+ recipe.contourWarpSubvoxels + pad;
|
|
||||||
if (island.influenceMinimumSubvoxels[0] > island.centerSubvoxels[0] - reach
|
if (island.influenceMinimumSubvoxels[0] > island.centerSubvoxels[0] - reach
|
||||||
|| island.influenceMaximumSubvoxels[0] < island.centerSubvoxels[0] + reach
|
|| island.influenceMaximumSubvoxels[0] < island.centerSubvoxels[0] + reach
|
||||||
|| island.influenceMinimumSubvoxels[1] > island.centerSubvoxels[1] - reach
|
|| island.influenceMinimumSubvoxels[1] > island.centerSubvoxels[1] - reach
|
||||||
|| island.influenceMaximumSubvoxels[1] < island.centerSubvoxels[1] + reach
|
|| island.influenceMaximumSubvoxels[1] < island.centerSubvoxels[1] + reach
|
||||||
|| island.influenceMinimumSubvoxels[2] > (hasAnchorRoot
|
|| island.influenceMinimumSubvoxels[2] > island.bottomZSubvoxels - pad
|
||||||
?std::min(island.bottomZSubvoxels,recipe.anchorRootBottomZSubvoxels)
|
|
||||||
:island.bottomZSubvoxels) - pad
|
|
||||||
|| island.influenceMaximumSubvoxels[2] < island.topZSubvoxels + pad
|
|| island.influenceMaximumSubvoxels[2] < island.topZSubvoxels + pad
|
||||||
|| island.bottomZSubvoxels < recipe.layoutMinimumZSubvoxels + recipe.boundarySealSubvoxels
|
|| island.bottomZSubvoxels < recipe.layoutMinimumZSubvoxels + recipe.boundarySealSubvoxels
|
||||||
|| island.topZSubvoxels > recipe.layoutMaximumZSubvoxels - recipe.boundarySealSubvoxels)
|
|| island.topZSubvoxels > recipe.layoutMaximumZSubvoxels - recipe.boundarySealSubvoxels)
|
||||||
@@ -1465,19 +971,6 @@ namespace floating_island_detail
|
|||||||
for (std::uint32_t ordinal = 0; ordinal < recipe.primaryIslandCount; ++ordinal)
|
for (std::uint32_t ordinal = 0; ordinal < recipe.primaryIslandCount; ++ordinal)
|
||||||
if (!ordinals.contains(ordinal))
|
if (!ordinals.contains(ordinal))
|
||||||
{ error = "floating-island route chain is missing a primary seeded anchor"; return false; }
|
{ error = "floating-island route chain is missing a primary seeded anchor"; return false; }
|
||||||
if(recipe.geometryVersion>=5)
|
|
||||||
{
|
|
||||||
if(!anchorIsland)
|
|
||||||
{error="floating-island plan is missing its unique passage anchor island";return false;}
|
|
||||||
const double anchorDistance=std::hypot(
|
|
||||||
static_cast<double>(anchorIsland->centerSubvoxels[0]),
|
|
||||||
static_cast<double>(anchorIsland->centerSubvoxels[1]));
|
|
||||||
if(anchorDistance>recipe.anchorPlacementRadiusSubvoxels
|
|
||||||
||recipe.anchorRootBottomZSubvoxels>=anchorIsland->bottomZSubvoxels
|
|
||||||
||recipe.anchorMouthOffsetSubvoxels+recipe.routeConstraints.capsuleRadiusSubvoxels
|
|
||||||
>=anchorIsland->radiusSubvoxels)
|
|
||||||
{error="floating-island anchor island or its mouth leaves the passage radius or rock envelope";return false;}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::unordered_set<std::uint64_t> edgeIds;
|
std::unordered_set<std::uint64_t> edgeIds;
|
||||||
std::unordered_map<std::uint64_t, std::vector<std::uint64_t>> adjacency;
|
std::unordered_map<std::uint64_t, std::vector<std::uint64_t>> adjacency;
|
||||||
@@ -1593,8 +1086,8 @@ namespace floating_island_detail
|
|||||||
// Land each connector well inside the broad top so the full capsule footprint remains
|
// Land each connector well inside the broad top so the full capsule footprint remains
|
||||||
// supported across the bridge-to-island transition, including bounded surface roughness.
|
// supported across the bridge-to-island transition, including bounded surface roughness.
|
||||||
// Les connecteurs recouvrent le plateau pour soutenir toute la capsule au raccord.
|
// Les connecteurs recouvrent le plateau pour soutenir toute la capsule au raccord.
|
||||||
// Keep the capsule footprint supported while seating L37 connectors just below the native island tops.
|
// Keep the narrow span exposed to open sky; a smaller fraction buries most of it in the island tops.
|
||||||
// Garder l'appui de la capsule tout en enfouissant les connecteurs L37 juste sous les plateaux.
|
// Garder la passerelle etroite visible dans le vide ; une fraction plus faible l'enfouit dans les plateaux.
|
||||||
const double landingFraction = recipe.geometryVersion >= 2 ? 0.52 : 0.62;
|
const double landingFraction = recipe.geometryVersion >= 2 ? 0.52 : 0.62;
|
||||||
const double fromReach = static_cast<double>(from.radiusSubvoxels) * landingFraction;
|
const double fromReach = static_cast<double>(from.radiusSubvoxels) * landingFraction;
|
||||||
const double toReach = static_cast<double>(to.radiusSubvoxels) * landingFraction;
|
const double toReach = static_cast<double>(to.radiusSubvoxels) * landingFraction;
|
||||||
@@ -1602,19 +1095,7 @@ namespace floating_island_detail
|
|||||||
const double sy = from.centerSubvoxels[1] + uy * fromReach;
|
const double sy = from.centerSubvoxels[1] + uy * fromReach;
|
||||||
const double ex = to.centerSubvoxels[0] - ux * toReach;
|
const double ex = to.centerSubvoxels[0] - ux * toReach;
|
||||||
const double ey = to.centerSubvoxels[1] - uy * toReach;
|
const double ey = to.centerSubvoxels[1] - uy * toReach;
|
||||||
const bool craterMouthConnector=WorldAssemblyEvaluatorVersion>=174
|
const double routeDeckLift = recipe.geometryVersion >= 2 ? 1.0 * Q : 0.0;
|
||||||
&&(from.ordinal==recipe.anchorIslandOrdinal
|
|
||||||
||to.ordinal==recipe.anchorIslandOrdinal);
|
|
||||||
const bool westernSupportConnector=WorldAssemblyEvaluatorVersion>=178
|
|
||||||
&&((from.ordinal==0&&to.ordinal==1)||(from.ordinal==1&&to.ordinal==0));
|
|
||||||
const double routeDeckLift=recipe.geometryVersion>=2
|
|
||||||
?(WorldAssemblyEvaluatorVersion>=178
|
|
||||||
?(westernSupportConnector
|
|
||||||
?(WorldAssemblyEvaluatorVersion>=181?-1.0
|
|
||||||
:(WorldAssemblyEvaluatorVersion>=180?-1.25
|
|
||||||
:(WorldAssemblyEvaluatorVersion>=179?-1.5:-1.0))):-2.0)
|
|
||||||
:(craterMouthConnector?-2.0
|
|
||||||
:(WorldAssemblyEvaluatorVersion>=172?-1.0:1.0)))*Q:0.0;
|
|
||||||
const double sz = IslandTopSurface(recipe, from, sx, sy) + routeDeckLift;
|
const double sz = IslandTopSurface(recipe, from, sx, sy) + routeDeckLift;
|
||||||
const double ez = IslandTopSurface(recipe, to, ex, ey) + routeDeckLift;
|
const double ez = IslandTopSurface(recipe, to, ex, ey) + routeDeckLift;
|
||||||
edge.startSubvoxels = {RoundQ(sx), RoundQ(sy), RoundQ(sz)};
|
edge.startSubvoxels = {RoundQ(sx), RoundQ(sy), RoundQ(sz)};
|
||||||
@@ -1715,8 +1196,7 @@ namespace floating_island_detail
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::optional<FloatingIslandManifestV1> FloatingIslandManifestV1::FromJson(
|
std::optional<FloatingIslandManifestV1> FloatingIslandManifestV1::FromJson(
|
||||||
std::string_view json, std::string& error,
|
std::string_view json, std::string& error)
|
||||||
std::int64_t resolvedLayoutChunkWidthSubvoxels)
|
|
||||||
{
|
{
|
||||||
VFJson::Value root;
|
VFJson::Value root;
|
||||||
if (!VFJson::Parser(std::string(json)).Parse(root, error)) return std::nullopt;
|
if (!VFJson::Parser(std::string(json)).Parse(root, error)) return std::nullopt;
|
||||||
@@ -1742,32 +1222,6 @@ std::optional<FloatingIslandManifestV1> FloatingIslandManifestV1::FromJson(
|
|||||||
manifest.recipe.crustDepthBonusQ16 = 0;
|
manifest.recipe.crustDepthBonusQ16 = 0;
|
||||||
manifest.recipe.rootDepthRatioQ16 = 0;
|
manifest.recipe.rootDepthRatioQ16 = 0;
|
||||||
}
|
}
|
||||||
if(manifest.recipe.geometryVersion>=4)
|
|
||||||
{
|
|
||||||
if(!floating_island_detail::ReadInteger(recipeValue.Get("layout_chunk_width_q"),
|
|
||||||
manifest.recipe.layoutChunkWidthSubvoxels))
|
|
||||||
{error="floating-island recipe is missing its resolved layout chunk width";return std::nullopt;}
|
|
||||||
if(resolvedLayoutChunkWidthSubvoxels>0
|
|
||||||
&& manifest.recipe.layoutChunkWidthSubvoxels!=resolvedLayoutChunkWidthSubvoxels)
|
|
||||||
{error="floating-island recipe layout width disagrees with the resolved world layout";return std::nullopt;}
|
|
||||||
}
|
|
||||||
if(manifest.recipe.geometryVersion>=5)
|
|
||||||
{
|
|
||||||
if(!floating_island_detail::ReadU32(recipeValue.Get("anchor_island_ordinal"),
|
|
||||||
manifest.recipe.anchorIslandOrdinal)
|
|
||||||
||!floating_island_detail::ReadInteger(recipeValue.Get("anchor_placement_radius_q"),
|
|
||||||
manifest.recipe.anchorPlacementRadiusSubvoxels)
|
|
||||||
||!floating_island_detail::ReadInteger(recipeValue.Get("anchor_root_radius_q"),
|
|
||||||
manifest.recipe.anchorRootRadiusSubvoxels)
|
|
||||||
||!floating_island_detail::ReadInteger(recipeValue.Get("anchor_island_minimum_radius_q"),
|
|
||||||
manifest.recipe.anchorIslandMinimumRadiusSubvoxels)
|
|
||||||
||!floating_island_detail::ReadInteger(recipeValue.Get("anchor_root_bottom_z_q"),
|
|
||||||
manifest.recipe.anchorRootBottomZSubvoxels)
|
|
||||||
||!floating_island_detail::ReadInteger(recipeValue.Get("anchor_mouth_offset_q"),
|
|
||||||
manifest.recipe.anchorMouthOffsetSubvoxels))
|
|
||||||
{error="floating-island geometry version 7 is missing a passage-rooted anchor field";return std::nullopt;}
|
|
||||||
}
|
|
||||||
manifest.recipe.terrainFollowingRoute=recipeValue.Get("terrain_following_route").Boolean(false);
|
|
||||||
if (!floating_island_detail::ReadU32(root.Get("schema_version"), schema)
|
if (!floating_island_detail::ReadU32(root.Get("schema_version"), schema)
|
||||||
|| !floating_island_detail::ReadHash(root.Get("content_hash"), manifestHash)
|
|| !floating_island_detail::ReadHash(root.Get("content_hash"), manifestHash)
|
||||||
|| !floating_island_detail::ReadU32(recipeValue.Get("schema_version"), manifest.recipe.schemaVersion)
|
|| !floating_island_detail::ReadU32(recipeValue.Get("schema_version"), manifest.recipe.schemaVersion)
|
||||||
@@ -1803,10 +1257,6 @@ std::optional<FloatingIslandManifestV1> FloatingIslandManifestV1::FromJson(
|
|||||||
|| !floating_island_detail::ReadHash(planValue.Get("content_hash"), planHash)
|
|| !floating_island_detail::ReadHash(planValue.Get("content_hash"), planHash)
|
||||||
|| schema != 1)
|
|| schema != 1)
|
||||||
{ error = "floating-island manifest is missing a required versioned recipe or plan field"; return std::nullopt; }
|
{ error = "floating-island manifest is missing a required versioned recipe or plan field"; return std::nullopt; }
|
||||||
if(manifest.recipe.geometryVersion<4 && resolvedLayoutChunkWidthSubvoxels>0)
|
|
||||||
manifest.recipe.layoutChunkWidthSubvoxels=resolvedLayoutChunkWidthSubvoxels;
|
|
||||||
else if(manifest.recipe.geometryVersion<4)
|
|
||||||
{ error="legacy floating-island manifest requires the world's resolved layout chunk width"; return std::nullopt; }
|
|
||||||
manifest.recipe.contentHash = recipeHash;
|
manifest.recipe.contentHash = recipeHash;
|
||||||
manifest.contentHash = manifestHash;
|
manifest.contentHash = manifestHash;
|
||||||
manifest.plan.contentHash = planHash;
|
manifest.plan.contentHash = planHash;
|
||||||
@@ -1918,16 +1368,6 @@ std::string FloatingIslandManifestV1::ToJson() const
|
|||||||
out << ",\"geometry_version\":" << recipe.geometryVersion
|
out << ",\"geometry_version\":" << recipe.geometryVersion
|
||||||
<< ",\"crust_depth_bonus_q16\":" << recipe.crustDepthBonusQ16
|
<< ",\"crust_depth_bonus_q16\":" << recipe.crustDepthBonusQ16
|
||||||
<< ",\"root_depth_ratio_q16\":" << recipe.rootDepthRatioQ16;
|
<< ",\"root_depth_ratio_q16\":" << recipe.rootDepthRatioQ16;
|
||||||
if(recipe.geometryVersion>=4)
|
|
||||||
out << ",\"layout_chunk_width_q\":" << recipe.layoutChunkWidthSubvoxels;
|
|
||||||
if(recipe.geometryVersion>=5)
|
|
||||||
out << ",\"anchor_island_ordinal\":" << recipe.anchorIslandOrdinal
|
|
||||||
<< ",\"anchor_placement_radius_q\":" << recipe.anchorPlacementRadiusSubvoxels
|
|
||||||
<< ",\"anchor_root_radius_q\":" << recipe.anchorRootRadiusSubvoxels
|
|
||||||
<< ",\"anchor_island_minimum_radius_q\":" << recipe.anchorIslandMinimumRadiusSubvoxels
|
|
||||||
<< ",\"anchor_root_bottom_z_q\":" << recipe.anchorRootBottomZSubvoxels
|
|
||||||
<< ",\"anchor_mouth_offset_q\":" << recipe.anchorMouthOffsetSubvoxels;
|
|
||||||
if(recipe.terrainFollowingRoute)out << ",\"terrain_following_route\":true";
|
|
||||||
out << ",\"blend_radius_q\":" << recipe.blendRadiusSubvoxels
|
out << ",\"blend_radius_q\":" << recipe.blendRadiusSubvoxels
|
||||||
<< ",\"contour_warp_q\":" << recipe.contourWarpSubvoxels
|
<< ",\"contour_warp_q\":" << recipe.contourWarpSubvoxels
|
||||||
<< ",\"roughness_q\":" << recipe.roughnessSubvoxels
|
<< ",\"roughness_q\":" << recipe.roughnessSubvoxels
|
||||||
@@ -2089,9 +1529,6 @@ std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
|||||||
auto island = floating_island_detail::MakeIsland(recipe, i,
|
auto island = floating_island_detail::MakeIsland(recipe, i,
|
||||||
primaryXY[i][0], primaryXY[i][1], tops[i], site);
|
primaryXY[i][0], primaryXY[i][1], tops[i], site);
|
||||||
island.radiusSubvoxels = radii[i];
|
island.radiusSubvoxels = radii[i];
|
||||||
if(recipe.geometryVersion>=5&&i==recipe.anchorIslandOrdinal)
|
|
||||||
island.radiusSubvoxels=std::max(island.radiusSubvoxels,
|
|
||||||
recipe.anchorIslandMinimumRadiusSubvoxels);
|
|
||||||
island.topHalfSubvoxels = std::max<std::int64_t>(1, island.radiusSubvoxels / 5);
|
island.topHalfSubvoxels = std::max<std::int64_t>(1, island.radiusSubvoxels / 5);
|
||||||
island.centerSubvoxels[2] = island.topZSubvoxels - island.topHalfSubvoxels;
|
island.centerSubvoxels[2] = island.topZSubvoxels - island.topHalfSubvoxels;
|
||||||
island.bottomZSubvoxels = floating_island_detail::IslandBottom(recipe,
|
island.bottomZSubvoxels = floating_island_detail::IslandBottom(recipe,
|
||||||
@@ -2178,54 +1615,14 @@ std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
|||||||
static_cast<double>(a.centerSubvoxels[0]) + dx * t);
|
static_cast<double>(a.centerSubvoxels[0]) + dx * t);
|
||||||
const std::int64_t y = floating_island_detail::RoundQ(
|
const std::int64_t y = floating_island_detail::RoundQ(
|
||||||
static_cast<double>(a.centerSubvoxels[1]) + dy * t);
|
static_cast<double>(a.centerSubvoxels[1]) + dy * t);
|
||||||
std::int64_t routeX=x,routeY=y;
|
const auto surface = floating_island_detail::FindCapsuleFootingTopZ(
|
||||||
if(WorldAssemblyEvaluatorVersion>=192&&recipe.terrainFollowingRoute
|
recipe, manifest.plan, x, y);
|
||||||
&&recipe.geometryVersion>=5
|
|
||||||
&&segment+2==count&&pieces>1)
|
|
||||||
{
|
|
||||||
const auto& anchor=islands[recipe.anchorIslandOrdinal];
|
|
||||||
const std::uint32_t side=static_cast<std::uint32_t>(
|
|
||||||
floating_island_detail::UnitQ16(recipe.seed,anchor.stableId,
|
|
||||||
0x414e43484f524d50ull)&1u);
|
|
||||||
const double length=std::hypot(dx,dy);
|
|
||||||
const double bend=10.0*floating_island_detail::Q
|
|
||||||
*std::pow(std::sin(floating_island_detail::Pi*t),2.0);
|
|
||||||
const double sign=side?-1.0:1.0;
|
|
||||||
if(length>0.0)
|
|
||||||
{
|
|
||||||
routeX=floating_island_detail::RoundQ(
|
|
||||||
static_cast<double>(routeX)+sign*(-dy/length)*bend);
|
|
||||||
routeY=floating_island_detail::RoundQ(
|
|
||||||
static_cast<double>(routeY)+sign*(dx/length)*bend);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(recipe.geometryVersion>=5&&segment+2==count&&piece==pieces)
|
|
||||||
{
|
|
||||||
const auto& anchor=islands[recipe.anchorIslandOrdinal];
|
|
||||||
const std::uint32_t side=static_cast<std::uint32_t>(
|
|
||||||
floating_island_detail::UnitQ16(recipe.seed,anchor.stableId,
|
|
||||||
0x414e43484f524d50ull)&1u);
|
|
||||||
const double routeAngle=std::atan2(dy,dx);
|
|
||||||
const double angle=routeAngle+(side?1.0:-1.0)
|
|
||||||
*floating_island_detail::Pi/2.0;
|
|
||||||
const double mouthX=static_cast<double>(anchor.centerSubvoxels[0])
|
|
||||||
+static_cast<double>(recipe.anchorMouthOffsetSubvoxels)*std::cos(angle);
|
|
||||||
const double mouthY=static_cast<double>(anchor.centerSubvoxels[1])
|
|
||||||
+static_cast<double>(recipe.anchorMouthOffsetSubvoxels)*std::sin(angle);
|
|
||||||
routeX=floating_island_detail::RoundQ(mouthX);
|
|
||||||
routeY=floating_island_detail::RoundQ(mouthY);
|
|
||||||
}
|
|
||||||
const auto surface = WorldAssemblyEvaluatorVersion>=199
|
|
||||||
&&recipe.terrainFollowingRoute
|
|
||||||
?floating_island_detail::FindSurfaceZ(recipe,manifest.plan,routeX,routeY)
|
|
||||||
:floating_island_detail::FindCapsuleFootingTopZ(
|
|
||||||
recipe,manifest.plan,routeX,routeY);
|
|
||||||
if (!surface)
|
if (!surface)
|
||||||
{ error = "island planner could not find a full capsule-width support surface along its connector chain"; return std::nullopt; }
|
{ error = "island planner could not find a full capsule-width support surface along its connector chain"; return std::nullopt; }
|
||||||
WorldAssemblyRoutePointV1 point;
|
WorldAssemblyRoutePointV1 point;
|
||||||
point.stableFeatureId = floating_island_detail::StableId(recipe.seed,
|
point.stableFeatureId = floating_island_detail::StableId(recipe.seed,
|
||||||
0x500000000ull + routeOrdinal, 0x524f555445ull);
|
0x500000000ull + routeOrdinal, 0x524f555445ull);
|
||||||
point.positionSubvoxels = {routeX, routeY, *surface + capsuleHalf
|
point.positionSubvoxels = {x, y, *surface + capsuleHalf
|
||||||
+ recipe.routeConstraints.minimumClearanceSubvoxels
|
+ recipe.routeConstraints.minimumClearanceSubvoxels
|
||||||
+ (recipe.geometryVersion >= 2 ? floating_island_detail::Q / 8 : 0)};
|
+ (recipe.geometryVersion >= 2 ? floating_island_detail::Q / 8 : 0)};
|
||||||
point.radiusSubvoxels = routeRadius;
|
point.radiusSubvoxels = routeRadius;
|
||||||
@@ -2286,19 +1683,14 @@ bool FloatingIslandFieldV1::isValid() const noexcept
|
|||||||
std::int64_t FloatingIslandFieldV1::densitySubvoxels(std::int64_t x,
|
std::int64_t FloatingIslandFieldV1::densitySubvoxels(std::int64_t x,
|
||||||
std::int64_t y, std::int64_t z) const noexcept
|
std::int64_t y, std::int64_t z) const noexcept
|
||||||
{
|
{
|
||||||
floating_island_detail::FScopedIslandAttributionTimer fieldTimer(
|
|
||||||
floating_island_detail::EIslandAttributionBucket::Field);
|
|
||||||
// The selected evaluator validates once; do not scan the plan twice per scoped sample.
|
// The selected evaluator validates once; do not scan the plan twice per scoped sample.
|
||||||
// L'evaluateur choisi valide une seule fois; ne pas reparcourir le plan a chaque echantillon.
|
// L'evaluateur choisi valide une seule fois; ne pas reparcourir le plan a chaque echantillon.
|
||||||
std::int64_t density=floating_island_detail::MaxDensity;
|
|
||||||
if (const auto* list = ScopedWorldAssemblyFeatureQueriesV1::find(recipe_, plan_, x, y, z))
|
if (const auto* list = ScopedWorldAssemblyFeatureQueriesV1::find(recipe_, plan_, x, y, z))
|
||||||
density=densitySubvoxelsForTile(x, y, z, *list);
|
return densitySubvoxelsForTile(x, y, z, *list);
|
||||||
else if (!isValid() || !floating_island_detail::ValidScalar(x)
|
if (!isValid() || !floating_island_detail::ValidScalar(x)
|
||||||
|| !floating_island_detail::ValidScalar(y) || !floating_island_detail::ValidScalar(z))
|
|| !floating_island_detail::ValidScalar(y) || !floating_island_detail::ValidScalar(z))
|
||||||
density=floating_island_detail::MaxDensity;
|
return floating_island_detail::MaxDensity;
|
||||||
else density=floating_island_detail::FieldDensity(*recipe_, *plan_, x, y, z);
|
return floating_island_detail::FieldDensity(*recipe_, *plan_, x, y, z);
|
||||||
fieldTimer.Stop();
|
|
||||||
return density;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<WorldAssemblyFeatureBoundsV1> FloatingIslandFieldV1::solidSupportBounds() const
|
std::vector<WorldAssemblyFeatureBoundsV1> FloatingIslandFieldV1::solidSupportBounds() const
|
||||||
@@ -2497,46 +1889,19 @@ FieldInterval FloatingIslandFieldV1::interval(std::int64_t minX, std::int64_t mi
|
|||||||
|| !floating_island_detail::ValidScalar(minX) || !floating_island_detail::ValidScalar(minY)
|
|| !floating_island_detail::ValidScalar(minX) || !floating_island_detail::ValidScalar(minY)
|
||||||
|| !floating_island_detail::ValidScalar(minZ) || !floating_island_detail::ValidScalar(maxX)
|
|| !floating_island_detail::ValidScalar(minZ) || !floating_island_detail::ValidScalar(maxX)
|
||||||
|| !floating_island_detail::ValidScalar(maxY) || !floating_island_detail::ValidScalar(maxZ)) return result;
|
|| !floating_island_detail::ValidScalar(maxY) || !floating_island_detail::ValidScalar(maxZ)) return result;
|
||||||
using namespace floating_island_detail;
|
const std::array<std::int64_t, 3> minimum{minX, minY, minZ};
|
||||||
if (plan_->islands.empty())
|
const std::array<std::int64_t, 3> maximum{maxX, maxY, maxZ};
|
||||||
return {true,MaxDensity,MaxDensity};
|
|
||||||
|
|
||||||
const std::array<double,3> minimum{static_cast<double>(minX),
|
|
||||||
static_cast<double>(minY),static_cast<double>(minZ)};
|
|
||||||
const std::array<double,3> maximum{static_cast<double>(maxX),
|
|
||||||
static_cast<double>(maxY),static_cast<double>(maxZ)};
|
|
||||||
double fieldMinimum=static_cast<double>(MaxDensity);
|
|
||||||
double fieldMaximum=static_cast<double>(MaxDensity);
|
|
||||||
for (const auto& island : plan_->islands)
|
for (const auto& island : plan_->islands)
|
||||||
{
|
if (floating_island_detail::Intersects(minimum, maximum,
|
||||||
FDensityRange group=IslandDensityRange(*recipe_,island,minimum,maximum);
|
island.influenceMinimumSubvoxels, island.influenceMaximumSubvoxels)) return result;
|
||||||
for (const auto& edge : plan_->clusterEdges)
|
for (const auto& edge : plan_->clusterEdges)
|
||||||
{
|
if (floating_island_detail::Intersects(minimum, maximum,
|
||||||
if(edge.fromIslandId!=island.stableId)continue;
|
edge.influenceMinimumSubvoxels, edge.influenceMaximumSubvoxels)) return result;
|
||||||
const FDensityRange connector=ConnectorDensityRange(*recipe_,edge,minimum,maximum);
|
// Root is void. A box outside every lobe/taper/blend/connector influence bound proves air.
|
||||||
const double detail=recipe_->geometryVersion>=2
|
// La racine est le vide : une boite hors des influences bornees prouve l'air.
|
||||||
?static_cast<double>(recipe_->roughnessSubvoxels):0.0;
|
|
||||||
group.minimum=std::min(group.minimum,connector.minimum-detail)
|
|
||||||
-static_cast<double>(recipe_->blendRadiusSubvoxels)*0.25;
|
|
||||||
group.maximum=std::min(group.maximum,connector.maximum+detail);
|
|
||||||
}
|
|
||||||
fieldMinimum=std::min(fieldMinimum,group.minimum);
|
|
||||||
fieldMaximum=std::min(fieldMaximum,group.maximum);
|
|
||||||
}
|
|
||||||
if(!std::isfinite(fieldMinimum)||!std::isfinite(fieldMaximum))return result;
|
|
||||||
|
|
||||||
// These interval operations are shared by native and future generator classifiers: SDF
|
|
||||||
// intersections take max, unions take min, and smooth unions subtract at most k/4.
|
|
||||||
// Keep two subvoxels of slack for floating bounds followed by the field's integer rounding.
|
|
||||||
// Ces bornes servent les generateurs actuels et futurs : intersections=max, unions=min.
|
|
||||||
const double minimumRounded=std::clamp(std::floor(fieldMinimum)-2.0,
|
|
||||||
-static_cast<double>(MaxDensity),static_cast<double>(MaxDensity));
|
|
||||||
const double maximumRounded=std::clamp(std::ceil(fieldMaximum)+2.0,
|
|
||||||
-static_cast<double>(MaxDensity),static_cast<double>(MaxDensity));
|
|
||||||
if(minimumRounded>maximumRounded)return result;
|
|
||||||
result.known = true;
|
result.known = true;
|
||||||
result.minimumDensitySubvoxels=static_cast<std::int64_t>(minimumRounded);
|
result.minimumDensitySubvoxels = 0;
|
||||||
result.maximumDensitySubvoxels=static_cast<std::int64_t>(maximumRounded);
|
result.maximumDensitySubvoxels = floating_island_detail::MaxDensity;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
Loaded 100 of 1690 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user