wip(step3): 12.5 cm voxels, fall-through fixed, brick evaluator in game, organic tunnels -- speed gate FAILS

Stopped by the owner's decision (in-game lit captures were eating the round); he judges the look in
game instead.
1. Fall-through with the research field: fixed. Deep drop, gate on: 2 holds (0.50 s), min foot
   clearance +43 cm; gate off: penetrates (-278 cm). Stress fixture offsets converted to metres.
2. VOXEL_SIZE 25 -> 12.5 cm. Physical sizes kept in metres (player, steps, gate, insets, research
   field sizes, dig radii, test thresholds); class defaults that count chunks/voxels doubled
   (ClipRadius 6, RenderDistanceChunks 1536, ViewDistance*, StrateViewMargin 6,
   PawnCollisionRadius 2, SkirtCells 4, EdgeSealThickness 128, decoration spacings, density volume
   res 256). Asset values override these defaults: the owner must double his own.
   Ring scale, six workers, idle, step-2f field before the look changes: 64 m cube (519^3) 4.55 s;
   32 m cube (259^3) 0.95 s.
3. In game: tiles go through the brick evaluator (proved by a log marker); profile switch rebuilds
   the loaded world (~5,800 tiles); full-detail fill 12.4 s after spawn in the visible-window test.
4. Look: round winding tunnels, per-column ellipsoid/lattice caches. All step-2f geometry gates pass
   at 12.5 cm (0 crumbs, 1 solid component per profile, solid share 69-87%). 259^3 ring, six workers:
   1.08-2.02 s (worst HyperComplex) -- the 600 ms gate FAILS. Lit captures failed (hidden/off-screen
   window, then blank frames); not pursued.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-01 21:21:45 +02:00
1 parent ca78352e43
commit bc0f9ee791
33 files changed
+2649 -761

No files matched your search

+13 -15
View File
@@ -13,9 +13,9 @@ each strate can be a fundamentally different *archetype*, connected at (0,0).
### 8.0 Human-scale generation contract (2026-09-06) ### 8.0 Human-scale generation contract (2026-09-06)
All authored lengths are voxel-space values, converted at the boundary by `0.25 m/voxel`. The body Voxel-space authored lengths convert at `0.125 m/voxel`. The body reference is a **0.34 m radius,
reference is a **0.34 m radius, 1.76 m tall** capsule (`1.36 × 7.04` voxels). The default strate 1.76 m tall** capsule (`2.72 × 14.08` voxels). The default strate height is **16 chunks = 512 voxels
height is now **8 chunks = 256 voxels = 64 m**, which is intentionally larger than the 3-4 chunk = 64 m**, intentionally larger than the 3-4 chunk
boss-arena anchor so an archetype can reserve seals, vertical play, and a high ceiling together. boss-arena anchor so an archetype can reserve seals, vertical play, and a high ceiling together.
The default size families are deliberately independent: The default size families are deliberately independent:
@@ -23,7 +23,7 @@ The default size families are deliberately independent:
| Feature | Default metre derivation | Coupling kept sound | | Feature | Default metre derivation | Coupling kept sound |
|---------|--------------------------|---------------------| |---------|--------------------------|---------------------|
| Room network | radius 4-10 m (8-20 m diameters), origin radius 12 m (24 m hub), cell spacing 32 m | `MaxRoomRadius 40 < RoomSpacing/2 64`; a 6 m radial margin remains around the largest room | | Room network | radius 4-10 m (8-20 m diameters), origin radius 12 m (24 m hub), cell spacing 32 m | `MaxRoomRadius 40 < RoomSpacing/2 64`; a 6 m radial margin remains around the largest room |
| Round passages | 1.5-2 m radius (3-4 m bores); maze corridor radius 3.125 m (6.25 m nominal bore) | `0.7 × 2 × (12.5−3.75) × 0.25 = 3.0625 m` usable floor after worst roughness; Maze `12.5 < CellSize/2 32` | | Round passages | 1.5-2 m radius (3-4 m bores); maze corridor radius 3.125 m (6.25 m nominal bore) | `0.7 × 2 × (25−7.5) × 0.125 = 3.0625 m` usable floor after worst roughness; Maze `25 < CellSize/2 64` |
| Slab | floor at 16 m, ceiling at 38.4 m, 22.4 m open span | floor/ceiling noise and 4-8 m column diameters remain inside the open span | | Slab | floor at 16 m, ceiling at 38.4 m, 22.4 m open span | floor/ceiling noise and 4-8 m column diameters remain inside the open span |
| SurfaceWorld | 20 m terrain relief, 12 m warp, 60.8 m sky cap | terrain features are bounded below the 64 m strate envelope | | SurfaceWorld | 20 m terrain relief, 12 m warp, 60.8 m sky cap | terrain features are bounded below the 64 m strate envelope |
| VerticalShafts | 4-7 m shaft diameters, 3 m connector bore, 8 m ledge interval | shaft spacing is 20 m; connector/ledge values are derived from the roughness envelope | | VerticalShafts | 4-7 m shaft diameters, 3 m connector bore, 8 m ledge interval | shaft spacing is 20 m; connector/ledge values are derived from the roughness envelope |
@@ -175,8 +175,8 @@ arrival→departure flood-fill at the independently placed passage endpoints.
### 8.2a XY edge seal (bounded-world invariant) ### 8.2a XY edge seal (bounded-world invariant)
`UVoxelSettings::WorldRadiusVoxels` and `EdgeSealThickness` define the bounded world in actor-space `UVoxelSettings::WorldRadiusVoxels` and `EdgeSealThickness` define the bounded world in actor-space
XY around `(0,0)`. The default radius is 8192 voxels (~2.05 km at 25 cm/voxel) and the default XY around `(0,0)`. The legacy 8192-voxel radius is ~1.024 km at 12.5 cm/voxel; use 16384 voxels
solid ramp is 64 voxels. A radius of **0 is a true no-op**: the hot path returns before doing any for the same ~2.05 km world radius. The source edge-seal default is 128 voxels. A radius of **0 is a true no-op**: the hot path returns before doing any
arithmetic, so the legacy unbounded field is unchanged. From `R - Thickness` to `R`, arithmetic, so the legacy unbounded field is unchanged. From `R - Thickness` to `R`,
`VF_ApplyXYEdgeSeal` raises the internal positive-solid density with the same `SmoothStep01` ramp `VF_ApplyXYEdgeSeal` raises the internal positive-solid density with the same `SmoothStep01` ramp
idiom as the vertical seal; at and beyond `R` it forces positive `BaseDensity`. The generator idiom as the vertical seal; at and beyond `R` it forces positive `BaseDensity`. The generator
@@ -451,7 +451,7 @@ Tick via the INT_MIN sentinels). **Per-biome content (§8.14):** decorations res
update builds ONE flat decoration palette per grid (every biome's list concatenated; `EntryBiome[i]` tags entry update builds ONE flat decoration palette per grid (every biome's list concatenated; `EntryBiome[i]` tags entry
`i` with its context-biome index, -1 = strate fallback), and `PlaceAtCrossing` rolls only the entries the `i` with its context-biome index, -1 = strate fallback), and `PlaceAtCrossing` rolls only the entries the
column's biome owns. Borders follow the warped-Voronoi field at column resolution, no straight lines (a column's biome owns. Borders follow the warped-Voronoi field at column resolution, no straight lines (a
per-CELL biome would snap borders to the 8 m cell grid). `Initialize` also takes `UVoxelSettings*` (for the per-CELL biome would snap borders to the 4 m cell grid). `Initialize` also takes `UVoxelSettings*` (for the
grid tunables). grid tunables).
### 8.6 Atmosphere — `VoxelAtmosphereManager.h/.cpp` (NEW) ### 8.6 Atmosphere — `VoxelAtmosphereManager.h/.cpp` (NEW)
@@ -535,14 +535,12 @@ named 15° floor-gradient limit as the default inter-strate tunnel. The room and
therefore real walkable geometry, not a point anchor or nearest-component guess, and no therefore real walkable geometry, not a point anchor or nearest-component guess, and no
per-passage ordering is involved. per-passage ordering is involved.
The chamber dimensions come directly from the capsule: player diameter `2×0.34/0.25 = 2.72` The chamber dimensions come directly from the capsule: player diameter `2×0.34/0.125 = 5.44`
voxels; required floor width `3/0.25 = 12` voxels; capsule height `2×0.88/0.25 = 7.04` voxels; voxels; required floor width `3/0.125 = 24` voxels; capsule height `2×0.88/0.125 = 14.08` voxels;
one metre of headroom adds 4 voxels. `HalfWidth=max(7, MouthRadius+2)` leaves one metre of headroom adds 8 voxels. `HalfWidth=max(12, MouthRadius+4)` gives a 24-voxel
`2×(HalfWidth−1) >= 12` voxels of flat support after the one-voxel wall inset; the authored room minimum clear floor width (3 m). Height is `max(24, 14.08+8, 2×MouthRadius+4)`; the minimum
is therefore at least 14 voxels (3.5 m) wide. Height is room height is 24 voxels (3 m). `FloorZ=StandingPoint.Z−1`, floor thickness is 6 voxels (0.75 m),
`max(12, 7.04+4, 2×MouthRadius+2)`; the stock 6-voxel mouth is a 14-voxel (3.5 m) room with and the tube centreline
an 8-voxel half-width (4 m full width).
`FloorZ=StandingPoint.Z−0.5`, floor thickness is 3 voxels (0.75 m), and the tube centreline
meets the room at `FloorZ+MouthRadius`, so its lower tangent is the floor and there is no step lip. meets the room at `FloorZ+MouthRadius`, so its lower tangent is the floor and there is no step lip.
The final-density floor audit samples the flat plane and checks the player-fit capsule above it. The The final-density floor audit samples the flat plane and checks the player-fit capsule above it. The
default tunnel audit samples every route step against the final density and checks the analytic and default tunnel audit samples every route step against the final density and checks the analytic and
+2 -1
View File
@@ -25,7 +25,8 @@ try to run anything.
## Hard rules (these prevent real bugs — verify against code, don't assume) ## Hard rules (these prevent real bugs — verify against code, don't assume)
- **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.** Conversions live in `VoxelTypes.h`. - **Density functions take VOXEL coords, not cm.** One voxel is 12.5 cm; one 32³ chunk is 4 m.
Conversions live in `VoxelTypes.h`.
- **Determinism:** all randomness = hash of (coord, seed, strateIndex). No RNG state — same seed ⇒ - **Determinism:** all randomness = hash of (coord, seed, strateIndex). No RNG state — same seed ⇒
same world. Player edits (diff layer) are the only non-deterministic overlay. same world. Player edits (diff layer) are the only non-deterministic overlay.
- **Async safety:** worker tasks only READ Generator/Mesher, must check `bShuttingDown`, and return via - **Async safety:** worker tasks only READ Generator/Mesher, must check `bShuttingDown`, and return via
+48 -19
View File
@@ -28,7 +28,7 @@ A **density-field voxel terrain** plugin for Unreal Engine, built around undergr
### Core terminology ### Core terminology
| Term | Meaning | | Term | Meaning |
|------|---------| |------|---------|
| Chunk | 32×32×32 voxels (`CHUNK_SIZE`). Voxel = 25 cm (`VOXEL_SIZE`). | | Chunk | 32×32×32 voxels (`CHUNK_SIZE`). Voxel = 12.5 cm (`VOXEL_SIZE`), so a chunk is 4 m. |
| 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. |
@@ -76,17 +76,45 @@ what generator type, and is there a passage/elevator SDF near here?"
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-09-06) ### 3.0 Human-scale generation contract (2026-10-01)
`VoxelStrateTypes.h` is now authored against the confirmed player capsule, not against naked voxel `VOXEL_SIZE` is **12.5 cm**; 32³ voxels make each chunk **4 m**. The gameplay capsule remains
counts: one voxel is **0.25 m**, the capsule is **1.36 voxels / 0.34 m radius** and **7.04 voxels / **0.34 m radius / 1.76 m tall** (**2.72 / 14.08 voxels**). The default strate envelope is
1.76 m tall**, and the default strate envelope is **8 chunks / 256 voxels / 64 m**. Core defaults are **16 chunks / 512 voxels / 64 m**; serialized owner strate assets that still say 8 chunks must be
therefore: room diameters **8-20 m**, origin hub **24 m**, room cells **32 m**, round tunnel bores changed to 16. Research-field dimensions are authored in metres and converted at runtime. The live
**3-4 m**, maze cells **16 m** with a **6.25 m nominal bore** (**3.125 m** radius; **3.0625 m** strate generator continues to read voxel-valued parameters from its assets, so those authored
worst-case usable floor after roughness), slab open span **22.4 m**, shaft diameters dimensions become half as large in metres unless their asset values are doubled.
**4-7 m**, island diameters **12-24 m**, and ordinary inter-strate bores **3-4 m**. These are
different derivations, not a global multiplier: passage widths follow the 2.5-4 m walking/fight The intended metre-scale families remain: room diameters **8-20 m**, origin hub **24 m**, room cells
target, room/cell spacing preserves a rock web, and the 64 m vertical envelope reserves space for **32 m**, round tunnel bores **3-4 m**, maze cells **16 m** with a **6.25 m nominal bore** (**3.125 m**
tall rooms, surface sky, and vertical traversal. 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
new fixed physical dimensions are authored in metres and converted to voxel counts at the boundary.
The owner's voxel-valued live strate assets are not migrated by C++: unchanged saved values therefore
produce half-size rooms, tunnels, features and noise periods in metres. See the step-3 migration list
in the handoff report for the exact `DA_Settings` fields and values.
**Owner asset migration (no packages were edited):** preserve `DA_Settings` distances by changing
`ViewDistanceXY 32→64`, `ViewDistanceUp/Down 10→20`, `PawnCollisionRadiusChunks 2→4`,
`StrateViewMarginChunks 6→12`, `ClipRadius 6→12`, `RenderDistanceChunks 1536→3072`,
`EdgeSealThickness 128→256 voxels`, decoration radius/near radius `12/6→24/12 chunks`, region
size `8→16 cells`, spacing/far spacing `8→16 voxels`, march step `4→8 voxels`, column depth
`320→640 voxels`, `OriginSpineRadius 28→56 voxels`, `MaxBrushRadius 30→60 voxels`, and
`SkirtCells 1→2`. If the density volume is enabled, set resolution `256→512` and debug radius
`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.
The loaded `Crystal Cave` slot in `DA_Strate3` is 64 chunks tall; use 128 chunks to preserve its
former 512 m span. Its voxel-authored live-generator lengths remain half-size until that asset is
re-authored; the editor-only research field is the step-3 target.
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³
field took **1.098521–1.194640 s** (median **1.169567 s**). The inclusive sample lattices span
64.75 m and 32.25 m between end samples respectively (the requested approximately 64 m / 32 m
fields). Both runs were deterministic across three trials. This records evaluator time only, not game
streaming. Six-worker 4³-brick proof, sound-bound audit, seam injection, and 1-vs-6-worker hashes
pass. Reusing exact detail-lattice hash corners across adjacent mixed bricks reduced the earlier
step-3 259³ median from 1.502908 s to 1.169567 s; the separate step-2f 600 ms worst-profile timing
bar still does not pass at the finer voxel size.
The reachable roughness envelope is `roughness * VOXEL_NOISE_SCALE * 1.5`, or `roughness * 1.875` The reachable roughness envelope is `roughness * VOXEL_NOISE_SCALE * 1.5`, or `roughness * 1.875`
voxels. The new defaults keep it below the relevant feature radius: **0.625** of the minimum voxels. The new defaults keep it below the relevant feature radius: **0.625** of the minimum
@@ -117,11 +145,12 @@ remains `0`, and lateral regions remain gated off.
|------|------| |------|------|
| `../../VoxelForge.uplugin` | Plugin manifest. Runtime module `VoxelForge` plus the Editor-only `VoxelForgeEditor` commandlet module. Beta. | | `../../VoxelForge.uplugin` | Plugin manifest. Runtime module `VoxelForge` plus the Editor-only `VoxelForgeEditor` commandlet module. Beta. |
| `VoxelForge.Build.cs` | Deps: Core, CoreUObject, Engine, **GameplayTags**, **RealtimeMeshComponent**. | | `VoxelForge.Build.cs` | Deps: Core, CoreUObject, Engine, **GameplayTags**, **RealtimeMeshComponent**. |
| `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`, 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|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.
| 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`; owns bounded render/`VF_` walk/OBJ export orchestration and is not linked by Game/Shipping. Optional `-exporttilex/y/z` sets an exact L0 tile origin for controlled export/game parity; omitting all three preserves the existing center-derived origin. `-regioneval=1` retains the exact-field step-2c research harness. `-step2d` and `-step2e` retain their earlier benchmarks. `-step2f` compares frozen step 2e and the coherent field for all eight profiles on the same 259³ ring, counts sub-1 m³ connected components, measures mesh dihedrals, detail spectrum, LOD surface distance (p95 threshold 0.5 lattice samples), chamber-pitch autocorrelation, roof/floor coverage, and six-worker timing. It also runs the sound/undersized-bound audit, seam injection, and 1-vs-6-worker hash checks and writes profile slices and JSON under `Saved/Research/step2f`. | | `Source/VoxelForgeEditor/VoxelForgeEditor.Build.cs` / `Private/VoxelForgeExploreCommandlet.*` | Editor/commandlet-only `-run=VoxelForgeExplore`; owns bounded render/`VF_` walk/OBJ export orchestration and is not linked by Game/Shipping. Optional `-exporttilex/y/z` sets an exact L0 tile origin for controlled export/game parity; omitting all three preserves the existing center-derived origin. `-regioneval=1` retains the exact-field step-2c research harness. `-step2d` and `-step2e` retain their earlier benchmarks. `-step2f` compares the metre-scaled step 2e baseline and the coherent field for all eight profiles on the same 259³ ring (32.25 m at this lattice), counts sub-1 m³ connected components in a 128-sample padded volume, measures mesh dihedrals, detail spectrum, LOD surface distance (p95 threshold 1 sample = 12.5 cm), chamber-pitch autocorrelation, roof/floor coverage, and six-worker timing. It also runs the sound/undersized-bound audit, seam injection, and 1-vs-6-worker hash checks and writes profile slices and JSON under `Saved/Research/step2f`. |
| `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 all 25 cm detail samples. 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 frozen 2e parameters for its rejection audit, while the new profiles use strongly jittered, individually rotated elliptical rooms capped against nearest neighbors, leaving a solid web joined by narrow tree/loop tunnels and capped shafts. 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 use the same independently rotated 48/24/12-sample bands and smooth ridge response; the twelve-sample shortest wavelength and 3.822-unit final-field detail bound are preserved. The thread-local cell cache and per-column distance reuse remain. In an editor game with this field enabled, sealed-solid, out-of-layout, and outer-classifier verdicts are declined without changing their console switches. No reflected class layout or serialized assets change. | | `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. |
| `Private/Tests/VoxelForgeRedefinedDensityFieldTest.cpp` | Three-seed functional sweep for the research field: authored passage endpoints must bracket open air and solid floor, and each tested strate must have a walkable arrival-to-exit route. | | `Private/Tests/VoxelForgeRedefinedDensityFieldTest.cpp` | Three-seed functional sweep for the research field: authored passage endpoints must bracket open air and solid floor, and each tested strate must have a walkable arrival-to-exit route. |
| `Public/VoxelGeometryHash.h` | Header-only geometry digest shared by commandlet export and runtime tile-hash dump; hashes mesh counts and the vertex/normal/UV/color/triangle arrays with the historical CRC order. | | `Public/VoxelGeometryHash.h` | Header-only geometry digest shared by commandlet export and runtime tile-hash dump; hashes mesh counts and the vertex/normal/UV/color/triangle arrays with the historical CRC order. |
@@ -131,7 +160,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` (25.0f cm) | 213 | World scale. | | `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). |
@@ -494,8 +523,8 @@ context. It never touches a generator, operator stack, manager, cache, or mutabl
`FVoxelPassageLanding`, `VF_BuildPassageLanding`, `VF_EvaluatePassageLandingSDF`, and `FVoxelPassageLanding`, `VF_BuildPassageLanding`, `VF_EvaluatePassageLandingSDF`, and
`VF_IsPassageLandingFloor` (h:57/184-199, implemented near `.cpp:6468`) turn each inter-strate mouth `VF_IsPassageLandingFloor` (h:57/184-199, implemented near `.cpp:6468`) turn each inter-strate mouth
into a deterministic rounded room plus a hard support slab. The room is sized from the player into a deterministic rounded room plus a hard support slab. The room is sized from the player
capsule (minimum 12 voxels / 3 m flat floor, 12 voxels / 3 m clear height, and at least 14 voxels / capsule (minimum 24 voxels / 3 m flat floor and clear height); the `(0,0)` origin landing is enlarged
3.5 m authored width); the `(0,0)` origin landing is enlarged for the reserved future shaft, while for the reserved future shaft, while
inter-strate mouths remain local rooms and deliberately have no landing-to-root connector road. inter-strate mouths remain local rooms and deliberately have no landing-to-root connector road.
The manager's shared thread-local passage shortlist serves tube, room, and floor evaluation; no The manager's shared thread-local passage shortlist serves tube, room, and floor evaluation; no
landing search occurs per voxel. `GeneratePassages` stores the full descriptors and the landing search occurs per voxel. `GeneratePassages` stores the full descriptors and the
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

@@ -603,27 +603,27 @@ namespace
FString VF_FormatWorldScaleAudit() FString VF_FormatWorldScaleAudit()
{ {
constexpr float VoxelMetres = 0.25f; constexpr float VoxelMetres = VOXEL_SIZE / 100.0f;
constexpr float StrateHeightVoxels = 8.0f * static_cast<float>(CHUNK_SIZE); constexpr float StrateHeightVoxels = 16.0f * static_cast<float>(CHUNK_SIZE);
FString Report = TEXT( FString Report = TEXT(
"WORLD SCALE AUDIT (all distances are authored in voxels; METRES = voxels * 0.25; " "WORLD SCALE AUDIT (all distances are authored in voxels; METRES = voxels * VOXEL_SIZE; "
"player radius=1.36 vox/0.34 m, height=7.04 vox/1.76 m; frequency rows show nominal " "player radius=2.72 vox/0.34 m, height=14.08 vox/1.76 m; frequency rows show nominal "
"period=1/frequency)\n" "period=1/frequency)\n"
"archetype | name | voxels | METRES | body/level meaning\n"); "archetype | name | voxels | METRES | body/level meaning\n");
auto Add = [&Report](const TCHAR* Archetype, const TCHAR* Name, auto Add = [&Report, VoxelMetres](const TCHAR* Archetype, const TCHAR* Name,
float Voxels, const TCHAR* Meaning) float Voxels, const TCHAR* Meaning)
{ {
Report += FString::Printf( Report += FString::Printf(
TEXT("%s | %s | %.2f | %.2f | %s\n"), Archetype, Name, TEXT("%s | %s | %.2f | %.2f | %s\n"), Archetype, Name,
Voxels, Voxels * 0.25f, Meaning); Voxels, Voxels * VoxelMetres, Meaning);
}; };
auto AddRange = [&Report](const TCHAR* Archetype, const TCHAR* Name, auto AddRange = [&Report, VoxelMetres](const TCHAR* Archetype, const TCHAR* Name,
float MinVoxels, float MaxVoxels, float MinVoxels, float MaxVoxels,
const TCHAR* Meaning) const TCHAR* Meaning)
{ {
Report += FString::Printf( Report += FString::Printf(
TEXT("%s | %s | %.2f..%.2f | %.2f..%.2f | %s\n"), Archetype, Name, TEXT("%s | %s | %.2f..%.2f | %.2f..%.2f | %s\n"), Archetype, Name,
MinVoxels, MaxVoxels, MinVoxels * 0.25f, MaxVoxels * 0.25f, Meaning); MinVoxels, MaxVoxels, MinVoxels * VoxelMetres, MaxVoxels * VoxelMetres, Meaning);
}; };
const TCHAR* TunnelNames[] = { TEXT("TunnelNetwork"), TEXT("Underwater") }; const TCHAR* TunnelNames[] = { TEXT("TunnelNetwork"), TEXT("Underwater") };
for (const TCHAR* Archetype : TunnelNames) for (const TCHAR* Archetype : TunnelNames)
@@ -708,7 +708,7 @@ namespace
{ {
Add(Archetype, TEXT("StrateHeight"), StrateHeightVoxels, Add(Archetype, TEXT("StrateHeight"), StrateHeightVoxels,
TEXT("8 chunks / 64 m: open vertical chamber envelope")); TEXT("8 chunks / 64 m: open vertical chamber envelope"));
Add(Archetype, TEXT("Floor height"), StrateHeightVoxels * 0.25f, Add(Archetype, TEXT("Floor height"), StrateHeightVoxels * VoxelMetres,
TEXT("16 m above the strate bottom")); TEXT("16 m above the strate bottom"));
Add(Archetype, TEXT("Ceiling height"), StrateHeightVoxels * 0.60f, Add(Archetype, TEXT("Ceiling height"), StrateHeightVoxels * 0.60f,
TEXT("38.4 m above the bottom; 22.4 m clear span before roughness")); TEXT("38.4 m above the bottom; 22.4 m clear span before roughness"));
@@ -728,7 +728,7 @@ namespace
TEXT("1 m solid boundary")); TEXT("1 m solid boundary"));
} }
Add(TEXT("Common layout"), TEXT("OriginSpineRadius"), 14.0f, Add(TEXT("Common layout"), TEXT("OriginSpineRadius"), 28.0f,
TEXT("3.5 m radius / 7 m diameter central descent spine")); TEXT("3.5 m radius / 7 m diameter central descent spine"));
Add(TEXT("Common layout"), TEXT("InterStrateGap"), 0.0f, Add(TEXT("Common layout"), TEXT("InterStrateGap"), 0.0f,
TEXT("0 m bedrock gap by default; configured as whole 8 m chunks")); TEXT("0 m bedrock gap by default; configured as whole 8 m chunks"));
@@ -750,7 +750,7 @@ namespace
TEXT("8 chunks / 64 m: high sky-cap envelope")); TEXT("8 chunks / 64 m: high sky-cap envelope"));
Add(TEXT("SurfaceWorld"), TEXT("ElevationRange"), 80.0f, Add(TEXT("SurfaceWorld"), TEXT("ElevationRange"), 80.0f,
TEXT("20 m terrain relief: human-scale hills, not a miniature room")); TEXT("20 m terrain relief: human-scale hills, not a miniature room"));
Add(TEXT("SurfaceWorld"), TEXT("Base ground height"), StrateHeightVoxels * 0.25f, Add(TEXT("SurfaceWorld"), TEXT("Base ground height"), StrateHeightVoxels * VoxelMetres,
TEXT("16 m mean ground elevation above the strate bottom")); TEXT("16 m mean ground elevation above the strate bottom"));
Add(TEXT("SurfaceWorld"), TEXT("HeightWarpStrength"), 48.0f, Add(TEXT("SurfaceWorld"), TEXT("HeightWarpStrength"), 48.0f,
TEXT("12 m horizontal landform distortion")); TEXT("12 m horizontal landform distortion"));
@@ -1002,7 +1002,7 @@ namespace
D.StartToGoalComponentDistanceCells, D.StartToGoalComponentDistanceCells,
*Law, FloorAreaM2, *Law, FloorAreaM2,
Found->Metrics.WalkableFloorAreaFraction, Found->Metrics.WalkableFloorAreaFraction,
Found->Metrics.MedianVerticalClearance * 0.25f, Found->Metrics.MedianVerticalClearance * (VOXEL_SIZE / 100.0f),
VF_ShowcaseRoughnessFeatureRatio(Found->Candidate)); VF_ShowcaseRoughnessFeatureRatio(Found->Candidate));
} }
return Report; return Report;
@@ -202,7 +202,7 @@ namespace
Passage->LowerLanding.StandingPoint.Y, Passage->LowerLanding.StandingPoint.Y,
Passage->LowerLanding.FloorZ)); Passage->LowerLanding.FloorZ));
constexpr float MaxStationSpacing = 0.5f; constexpr float MaxStationSpacing = 1.0f;
constexpr float PlayerWidthVoxels = constexpr float PlayerWidthVoxels =
VoxelPassageGeometry::WalkCorridorPlayerWidthVoxels; VoxelPassageGeometry::WalkCorridorPlayerWidthVoxels;
constexpr float PlayerHalfWidthVoxels = PlayerWidthVoxels * 0.5f; constexpr float PlayerHalfWidthVoxels = PlayerWidthVoxels * 0.5f;
@@ -214,11 +214,11 @@ namespace
PlayerWidthVoxels / static_cast<float>(NumLateralSamples - 1); PlayerWidthVoxels / static_cast<float>(NumLateralSamples - 1);
constexpr int32 NumVerticalSamples = 14; constexpr int32 NumVerticalSamples = 14;
constexpr int32 FloorProbeIntervals = 128; constexpr int32 FloorProbeIntervals = 128;
constexpr float FloorProbeHalfRange = 32.0f; constexpr float FloorProbeHalfRange = 64.0f;
constexpr float FloorProbeStep = constexpr float FloorProbeStep =
(2.0f * FloorProbeHalfRange) / static_cast<float>(FloorProbeIntervals); (2.0f * FloorProbeHalfRange) / static_cast<float>(FloorProbeIntervals);
constexpr float FirstBodySampleZ = 0.25f; constexpr float FirstBodySampleZ = 0.5f;
constexpr float BodySampleSpacing = 0.5f; constexpr float BodySampleSpacing = 1.0f;
OutSamples.ProofStencilTileOrigins.Reset(); OutSamples.ProofStencilTileOrigins.Reset();
OutSamples.ProofStencilUniformTiles = 0; OutSamples.ProofStencilUniformTiles = 0;
OutSamples.ProofStencilSealedTiles = 0; OutSamples.ProofStencilSealedTiles = 0;
@@ -484,7 +484,7 @@ namespace
{ {
constexpr float GapProfileMinOffset = -360.0f; constexpr float GapProfileMinOffset = -360.0f;
constexpr float GapProfileMaxOffset = 8.0f; constexpr float GapProfileMaxOffset = 8.0f;
constexpr float GapProfileStep = 0.5f; constexpr float GapProfileStep = 1.0f;
constexpr int32 GapProfileSamples = 737; constexpr int32 GapProfileSamples = 737;
TArray<float> GapProfileDensities; TArray<float> GapProfileDensities;
GapProfileDensities.SetNumUninitialized(GapProfileSamples); GapProfileDensities.SetNumUninitialized(GapProfileSamples);
@@ -878,7 +878,7 @@ namespace
const float HorizontalDistance = FMath::Sqrt( const float HorizontalDistance = FMath::Sqrt(
FMath::Square(Blocker.X - ClosestX) FMath::Square(Blocker.X - ClosestX)
+ FMath::Square(Blocker.Y - ClosestY)); + FMath::Square(Blocker.Y - ClosestY));
if (HorizontalDistance <= 2.0f) if (HorizontalDistance <= 4.0f)
{ {
const float FloorZ = FMath::Lerp( const float FloorZ = FMath::Lerp(
Candidate.NativeFloorProfileZ[SegmentIndex], Candidate.NativeFloorProfileZ[SegmentIndex],
@@ -1004,7 +1004,7 @@ namespace
const float HorizontalDistance = FMath::Sqrt( const float HorizontalDistance = FMath::Sqrt(
FMath::Square(Blocker.X - ClosestX) FMath::Square(Blocker.X - ClosestX)
+ FMath::Square(Blocker.Y - ClosestY)); + FMath::Square(Blocker.Y - ClosestY));
if (HorizontalDistance <= 12.0f) if (HorizontalDistance <= 24.0f)
{ {
const float FloorZ = FMath::Lerp( const float FloorZ = FMath::Lerp(
Candidate.NativeFloorProfileZ[SegmentIndex], Candidate.NativeFloorProfileZ[SegmentIndex],
@@ -98,18 +98,21 @@ namespace
float ExpectedFloorZ, float ExpectedFloorZ,
float& OutFloorZ) float& OutFloorZ)
{ {
constexpr float SampleStep = 0.25f; constexpr float SampleStep = VoxelPassageGeometry::MetersToVoxels(0.0625f);
const float ExpectedAt = Generator.GetDensityAt( const float ExpectedAt = Generator.GetDensityAt(
Centreline.X, Centreline.Y, ExpectedFloorZ); Centreline.X, Centreline.Y, ExpectedFloorZ);
const float ExpectedBelow = Generator.GetDensityAt( const float ExpectedBelow = Generator.GetDensityAt(
Centreline.X, Centreline.Y, ExpectedFloorZ - 0.75f); Centreline.X, Centreline.Y, ExpectedFloorZ
- VoxelPassageGeometry::MetersToVoxels(0.1875f));
const float ExpectedAbove = Generator.GetDensityAt( const float ExpectedAbove = Generator.GetDensityAt(
Centreline.X, Centreline.Y, ExpectedFloorZ + 0.75f); Centreline.X, Centreline.Y, ExpectedFloorZ
+ VoxelPassageGeometry::MetersToVoxels(0.1875f));
// Search around the authored tunnel floor, not from an arbitrary lower surface. A source // Search around the authored tunnel floor, not from an arbitrary lower surface. A source
// archetype may contain another cave floor below the tunnel; that is not the floor carried // archetype may contain another cave floor below the tunnel; that is not the floor carried
// by this passage and must not hijack the audit. // by this passage and must not hijack the audit.
const float StartZ = ExpectedFloorZ - 4.0f; const float ProbeHalfRange = VoxelPassageGeometry::MetersToVoxels(1.0f);
const float EndZ = ExpectedFloorZ + 4.0f; const float StartZ = ExpectedFloorZ - ProbeHalfRange;
const float EndZ = ExpectedFloorZ + ProbeHalfRange;
const int32 NumSteps = FMath::Max( const int32 NumSteps = FMath::Max(
1, FMath::CeilToInt((EndZ - StartZ) / SampleStep)); 1, FMath::CeilToInt((EndZ - StartZ) / SampleStep));
@@ -157,15 +160,18 @@ namespace
PreviousDensity = CurrentDensity; PreviousDensity = CurrentDensity;
} }
if (BestDistance == FLT_MAX if (BestDistance == FLT_MAX
|| BestDistance > VoxelPassageGeometry::LandingFloorThicknessVoxels + 1.0f) || BestDistance > VoxelPassageGeometry::LandingFloorThicknessVoxels
+ VoxelPassageGeometry::MetersToVoxels(0.25f))
{ {
return false; return false;
} }
const float Below = Generator.GetDensityAt( const float Below = Generator.GetDensityAt(
Centreline.X, Centreline.Y, BestCrossingZ - 0.75f); Centreline.X, Centreline.Y, BestCrossingZ
- VoxelPassageGeometry::MetersToVoxels(0.1875f));
const float Above = Generator.GetDensityAt( const float Above = Generator.GetDensityAt(
Centreline.X, Centreline.Y, BestCrossingZ + 0.75f); Centreline.X, Centreline.Y, BestCrossingZ
+ VoxelPassageGeometry::MetersToVoxels(0.1875f));
if (!FMath::IsFinite(Below) || !FMath::IsFinite(Above) if (!FMath::IsFinite(Below) || !FMath::IsFinite(Above)
|| !(Below < 0.0f) || !(Above > 0.0f)) || !(Below < 0.0f) || !(Above > 0.0f))
{ {
@@ -1115,8 +1121,9 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
EndIndex == 0 ? TEXT("upper") : TEXT("lower"), StrateIndex)); EndIndex == 0 ? TEXT("upper") : TEXT("lower"), StrateIndex));
} }
const bool bWidthAndHeadroom = Landing.HalfWidth * 2.0f >= 12.0f - 0.01f const float ThreeMetresVoxels = VoxelPassageGeometry::MetersToVoxels(3.0f);
&& Landing.CeilingZ - Landing.FloorZ >= 12.0f - 0.01f; const bool bWidthAndHeadroom = Landing.HalfWidth * 2.0f >= ThreeMetresVoxels - 0.01f
&& Landing.CeilingZ - Landing.FloorZ >= ThreeMetresVoxels - 0.01f;
const bool bDoorTangent = FMath::IsNearlyEqual( const bool bDoorTangent = FMath::IsNearlyEqual(
Landing.DoorPoint.Z - Landing.FloorZ, Landing.DoorPoint.Z - Landing.FloorZ,
Passage.Radius >= 0.0f ? FMath::Max( Passage.Radius >= 0.0f ? FMath::Max(
@@ -1136,10 +1143,10 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
const FVector FloorSamples[] = { const FVector FloorSamples[] = {
Landing.StandingPoint, Landing.StandingPoint,
Landing.StandingPoint + FVector(2.0f, 0.0f, 0.0f), Landing.StandingPoint + FVector(VoxelPassageGeometry::MetersToVoxels(0.5f), 0.0f, 0.0f),
Landing.StandingPoint + FVector(-2.0f, 0.0f, 0.0f), Landing.StandingPoint + FVector(-VoxelPassageGeometry::MetersToVoxels(0.5f), 0.0f, 0.0f),
Landing.StandingPoint + FVector(0.0f, 2.0f, 0.0f), Landing.StandingPoint + FVector(0.0f, VoxelPassageGeometry::MetersToVoxels(0.5f), 0.0f),
Landing.StandingPoint + FVector(0.0f, -2.0f, 0.0f) }; Landing.StandingPoint + FVector(0.0f, -VoxelPassageGeometry::MetersToVoxels(0.5f), 0.0f) };
float SurfaceHeights[UE_ARRAY_COUNT(FloorSamples)] = {}; float SurfaceHeights[UE_ARRAY_COUNT(FloorSamples)] = {};
bool bFloorPassed = true; bool bFloorPassed = true;
float LandingGradient = -1.0f; float LandingGradient = -1.0f;
@@ -1147,9 +1154,11 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
{ {
const FVector& Sample = FloorSamples[SampleIndex]; const FVector& Sample = FloorSamples[SampleIndex];
const float Below = World.Generator->GetDensityAt( const float Below = World.Generator->GetDensityAt(
Sample.X, Sample.Y, Landing.FloorZ - 1.0f); Sample.X, Sample.Y, Landing.FloorZ
- VoxelPassageGeometry::MetersToVoxels(0.25f));
const float Above = World.Generator->GetDensityAt( const float Above = World.Generator->GetDensityAt(
Sample.X, Sample.Y, Landing.FloorZ + 1.0f); Sample.X, Sample.Y, Landing.FloorZ
+ VoxelPassageGeometry::MetersToVoxels(0.25f));
const float Denominator = Above - Below; const float Denominator = Above - Below;
if (!FMath::IsFinite(Below) || !FMath::IsFinite(Above) if (!FMath::IsFinite(Below) || !FMath::IsFinite(Above)
|| !(Below < 0.0f) || !(Above >= 0.0f) || !(Below < 0.0f) || !(Above >= 0.0f)
@@ -1158,8 +1167,10 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
bFloorPassed = false; bFloorPassed = false;
continue; continue;
} }
SurfaceHeights[SampleIndex] = Landing.FloorZ - 1.0f const float FloorProbeHalfRange = VoxelPassageGeometry::MetersToVoxels(0.25f);
+ FMath::Clamp(-Below / Denominator, 0.0f, 1.0f) * 2.0f; SurfaceHeights[SampleIndex] = Landing.FloorZ - FloorProbeHalfRange
+ FMath::Clamp(-Below / Denominator, 0.0f, 1.0f)
* 2.0f * FloorProbeHalfRange;
} }
if (bFloorPassed) if (bFloorPassed)
@@ -1168,7 +1179,8 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
for (int32 SampleIndex = 1; SampleIndex < UE_ARRAY_COUNT(FloorSamples); ++SampleIndex) for (int32 SampleIndex = 1; SampleIndex < UE_ARRAY_COUNT(FloorSamples); ++SampleIndex)
{ {
MaxGradient = FMath::Max(MaxGradient, MaxGradient = FMath::Max(MaxGradient,
FMath::Abs(SurfaceHeights[SampleIndex] - SurfaceHeights[0]) / 2.0f); FMath::Abs(SurfaceHeights[SampleIndex] - SurfaceHeights[0])
/ VoxelPassageGeometry::MetersToVoxels(0.5f));
} }
LandingGradient = MaxGradient; LandingGradient = MaxGradient;
WorstLandingFloorGradient = FMath::Max(WorstLandingFloorGradient, MaxGradient); WorstLandingFloorGradient = FMath::Max(WorstLandingFloorGradient, MaxGradient);
@@ -968,17 +968,17 @@ bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters)
"core_without_structural_posts +ray=%.3f vox (%.3f m); " "core_without_structural_posts +ray=%.3f vox (%.3f m); "
"-ray final=%.3f vox (%.3f m), -ray core=%.3f vox (%.3f m)"), "-ray final=%.3f vox (%.3f m), -ray core=%.3f vox (%.3f m)"),
DiagnosisSeed, MazeCandidateIndex, MazeSlot, *MazeAxisDescription, DiagnosisSeed, MazeCandidateIndex, MazeSlot, *MazeAxisDescription,
MazeParams.CorridorRadius, MazeParams.CorridorRadius * 0.25f, MazeParams.CorridorRadius, MazeParams.CorridorRadius * FVoxelPlayerCapsuleConstants::VoxelSizeMeters,
MazeParams.CorridorRadius * 2.0f * 0.25f, MazeParams.CorridorRadius * 2.0f * FVoxelPlayerCapsuleConstants::VoxelSizeMeters,
MazeFinal.Positive.CrossingRadiusVoxels, MazeFinal.Positive.CrossingRadiusVoxels,
MazeFinal.Positive.CrossingRadiusVoxels * 0.25f, MazeFinal.Positive.CrossingRadiusVoxels * FVoxelPlayerCapsuleConstants::VoxelSizeMeters,
MazeFinal.Positive.FirstSolidStep, MazeFinal.Positive.FirstSolidStep,
MazeCore.Positive.CrossingRadiusVoxels, MazeCore.Positive.CrossingRadiusVoxels,
MazeCore.Positive.CrossingRadiusVoxels * 0.25f, MazeCore.Positive.CrossingRadiusVoxels * FVoxelPlayerCapsuleConstants::VoxelSizeMeters,
MazeFinal.Negative.CrossingRadiusVoxels, MazeFinal.Negative.CrossingRadiusVoxels,
MazeFinal.Negative.CrossingRadiusVoxels * 0.25f, MazeFinal.Negative.CrossingRadiusVoxels * FVoxelPlayerCapsuleConstants::VoxelSizeMeters,
MazeCore.Negative.CrossingRadiusVoxels, MazeCore.Negative.CrossingRadiusVoxels,
MazeCore.Negative.CrossingRadiusVoxels * 0.25f)); MazeCore.Negative.CrossingRadiusVoxels * FVoxelPlayerCapsuleConstants::VoxelSizeMeters));
} }
// TunnelNetwork — use the current fixed showcase roll and recover a real cached tunnel // TunnelNetwork — use the current fixed showcase roll and recover a real cached tunnel
@@ -1000,16 +1000,17 @@ bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters)
"core_without_structural_posts +ray=%.3f vox (%.3f m), -ray=%.3f vox (%.3f m)"), "core_without_structural_posts +ray=%.3f vox (%.3f m), -ray=%.3f vox (%.3f m)"),
DiagnosisSeed, TunnelCandidateIndex, *TunnelAxis.Description, DiagnosisSeed, TunnelCandidateIndex, *TunnelAxis.Description,
TunnelParams.TunnelMinRadius, TunnelParams.TunnelMaxRadius, TunnelParams.TunnelMinRadius, TunnelParams.TunnelMaxRadius,
TunnelParams.TunnelMinRadius * 0.25f, TunnelParams.TunnelMaxRadius * 0.25f, TunnelParams.TunnelMinRadius * FVoxelPlayerCapsuleConstants::VoxelSizeMeters,
TunnelAxis.NominalRadius, TunnelAxis.NominalRadius * 0.25f, TunnelParams.TunnelMaxRadius * FVoxelPlayerCapsuleConstants::VoxelSizeMeters,
TunnelAxis.NominalRadius, TunnelAxis.NominalRadius * FVoxelPlayerCapsuleConstants::VoxelSizeMeters,
TunnelAxis.FinalSection.Positive.CrossingRadiusVoxels, TunnelAxis.FinalSection.Positive.CrossingRadiusVoxels,
TunnelAxis.FinalSection.Positive.CrossingRadiusVoxels * 0.25f, TunnelAxis.FinalSection.Positive.CrossingRadiusVoxels * FVoxelPlayerCapsuleConstants::VoxelSizeMeters,
TunnelAxis.FinalSection.Negative.CrossingRadiusVoxels, TunnelAxis.FinalSection.Negative.CrossingRadiusVoxels,
TunnelAxis.FinalSection.Negative.CrossingRadiusVoxels * 0.25f, TunnelAxis.FinalSection.Negative.CrossingRadiusVoxels * FVoxelPlayerCapsuleConstants::VoxelSizeMeters,
TunnelAxis.CoreSection.Positive.CrossingRadiusVoxels, TunnelAxis.CoreSection.Positive.CrossingRadiusVoxels,
TunnelAxis.CoreSection.Positive.CrossingRadiusVoxels * 0.25f, TunnelAxis.CoreSection.Positive.CrossingRadiusVoxels * FVoxelPlayerCapsuleConstants::VoxelSizeMeters,
TunnelAxis.CoreSection.Negative.CrossingRadiusVoxels, TunnelAxis.CoreSection.Negative.CrossingRadiusVoxels,
TunnelAxis.CoreSection.Negative.CrossingRadiusVoxels * 0.25f)); TunnelAxis.CoreSection.Negative.CrossingRadiusVoxels * FVoxelPlayerCapsuleConstants::VoxelSizeMeters));
} }
// VerticalShafts — recover the deterministic shaft centre from the same hash contract as // VerticalShafts — recover the deterministic shaft centre from the same hash contract as
@@ -1035,16 +1036,16 @@ bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters)
"core_without_structural_posts +ray=%.3f vox (%.3f m), -ray=%.3f vox (%.3f m)"), "core_without_structural_posts +ray=%.3f vox (%.3f m), -ray=%.3f vox (%.3f m)"),
DiagnosisSeed, ShaftCandidateIndex, *ShaftAxisDescription, DiagnosisSeed, ShaftCandidateIndex, *ShaftAxisDescription,
ShaftParams.ShaftMinRadius, ShaftParams.ShaftMaxRadius, ShaftParams.ShaftMinRadius, ShaftParams.ShaftMaxRadius,
ShaftParams.ShaftMinRadius * 0.25f, ShaftParams.ShaftMaxRadius * 0.25f, ShaftParams.ShaftMinRadius * (VOXEL_SIZE / 100.0f), ShaftParams.ShaftMaxRadius * (VOXEL_SIZE / 100.0f),
ShaftNominalRadius, ShaftNominalRadius * 0.25f, ShaftNominalRadius, ShaftNominalRadius * (VOXEL_SIZE / 100.0f),
ShaftFinal.Positive.CrossingRadiusVoxels, ShaftFinal.Positive.CrossingRadiusVoxels,
ShaftFinal.Positive.CrossingRadiusVoxels * 0.25f, ShaftFinal.Positive.CrossingRadiusVoxels * (VOXEL_SIZE / 100.0f),
ShaftFinal.Negative.CrossingRadiusVoxels, ShaftFinal.Negative.CrossingRadiusVoxels,
ShaftFinal.Negative.CrossingRadiusVoxels * 0.25f, ShaftFinal.Negative.CrossingRadiusVoxels * (VOXEL_SIZE / 100.0f),
ShaftCore.Positive.CrossingRadiusVoxels, ShaftCore.Positive.CrossingRadiusVoxels,
ShaftCore.Positive.CrossingRadiusVoxels * 0.25f, ShaftCore.Positive.CrossingRadiusVoxels * (VOXEL_SIZE / 100.0f),
ShaftCore.Negative.CrossingRadiusVoxels, ShaftCore.Negative.CrossingRadiusVoxels,
ShaftCore.Negative.CrossingRadiusVoxels * 0.25f)); ShaftCore.Negative.CrossingRadiusVoxels * (VOXEL_SIZE / 100.0f)));
} }
FVoxelStrateMeasureSettings CharacterFitSettings; FVoxelStrateMeasureSettings CharacterFitSettings;
@@ -1164,11 +1165,11 @@ bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters)
"walkable slope<=%.1f deg; capsule clearance is re-tested at resolved support; " "walkable slope<=%.1f deg; capsule clearance is re-tested at resolved support; "
"HeadroomCells=%d is legacy-only"), "HeadroomCells=%d is legacy-only"),
ControlSettings.PlayerCapsuleRadiusVoxels, ControlSettings.PlayerCapsuleRadiusVoxels,
ControlSettings.PlayerCapsuleRadiusVoxels * 0.25f, ControlSettings.PlayerCapsuleRadiusVoxels * FVoxelPlayerCapsuleConstants::VoxelSizeMeters,
ControlSettings.PlayerCapsuleHalfHeightVoxels, ControlSettings.PlayerCapsuleHalfHeightVoxels,
ControlSettings.PlayerCapsuleHalfHeightVoxels * 0.25f, ControlSettings.PlayerCapsuleHalfHeightVoxels * FVoxelPlayerCapsuleConstants::VoxelSizeMeters,
FVoxelPlayerCapsuleConstants::HeightVoxels, FVoxelPlayerCapsuleConstants::HeightVoxels,
FVoxelPlayerCapsuleConstants::HeightVoxels * 0.25f, FVoxelPlayerCapsuleConstants::HeightVoxels * FVoxelPlayerCapsuleConstants::VoxelSizeMeters,
FMath::CeilToInt(2.0f * ControlSettings.PlayerCapsuleHalfHeightVoxels), FMath::CeilToInt(2.0f * ControlSettings.PlayerCapsuleHalfHeightVoxels),
FMath::CeilToInt(2.0f * ControlSettings.PlayerCapsuleHalfHeightVoxels) - 1, FMath::CeilToInt(2.0f * ControlSettings.PlayerCapsuleHalfHeightVoxels) - 1,
ControlSettings.PlayerMaxStepHeightMeters, ControlSettings.PlayerMaxStepHeightMeters,
@@ -6778,10 +6778,11 @@ FVoxelPassageLanding VF_BuildPassageLanding(
{ {
FVoxelPassageLanding Landing; FVoxelPassageLanding Landing;
// Body-derived dimensions at the authored 25 cm voxel scale are shared with the origin // Body-derived dimensions at the authored 12.5 cm voxel scale are shared with the origin
// landing. In particular, the 0.68 m player diameter, 3 m turning patch, 1.76 m capsule // landing. In particular, the 0.68 m player diameter, 3 m turning patch, 1.76 m capsule
// height and 1 m headroom become the common §6.5 formulas in VoxelPassageGeometry. // height and 1 m headroom become the common §6.5 formulas in VoxelPassageGeometry.
const float SafeMouthRadius = FMath::Max(FMath::Abs(MouthRadius), 1.0f); const float SafeMouthRadius = FMath::Max(FMath::Abs(MouthRadius),
VoxelPassageGeometry::MetersToVoxels(0.25f));
const float DesiredHalfWidth = VoxelPassageGeometry::LandingHalfWidthForRadius(MouthRadius); const float DesiredHalfWidth = VoxelPassageGeometry::LandingHalfWidthForRadius(MouthRadius);
const float DesiredHeight = VoxelPassageGeometry::LandingHeightForRadius(MouthRadius); const float DesiredHeight = VoxelPassageGeometry::LandingHeightForRadius(MouthRadius);
const float SafeSeal = FMath::Max(BoundarySealThickness, 0.0f); const float SafeSeal = FMath::Max(BoundarySealThickness, 0.0f);
@@ -6793,7 +6794,8 @@ FVoxelPassageLanding VF_BuildPassageLanding(
} }
Landing.StandingPoint = InStandingPoint; Landing.StandingPoint = InStandingPoint;
Landing.FloorZ = InStandingPoint.Z - 0.5f; Landing.FloorZ = InStandingPoint.Z
- VoxelPassageGeometry::MetersToVoxels(0.125f);
Landing.HalfWidth = DesiredHalfWidth; Landing.HalfWidth = DesiredHalfWidth;
Landing.CeilingZ = Landing.FloorZ + DesiredHeight; Landing.CeilingZ = Landing.FloorZ + DesiredHeight;
Landing.FloorThickness = VoxelPassageGeometry::LandingFloorThicknessVoxels; Landing.FloorThickness = VoxelPassageGeometry::LandingFloorThicknessVoxels;
@@ -6818,7 +6820,8 @@ FVoxelPassageLanding VF_BuildPassageLanding(
if (MaxFloor >= MinFloor) if (MaxFloor >= MinFloor)
{ {
Landing.FloorZ = FMath::Clamp(Landing.FloorZ, MinFloor, MaxFloor); Landing.FloorZ = FMath::Clamp(Landing.FloorZ, MinFloor, MaxFloor);
Landing.StandingPoint.Z = Landing.FloorZ + 0.5f; Landing.StandingPoint.Z = Landing.FloorZ
+ VoxelPassageGeometry::MetersToVoxels(0.125f);
Landing.CeilingZ = Landing.FloorZ + DesiredHeight; Landing.CeilingZ = Landing.FloorZ + DesiredHeight;
} }
else else
@@ -6833,9 +6836,10 @@ FVoxelPassageLanding VF_BuildPassageLanding(
} }
// The tube centreline is tangent to the floor: the tube bottom is exactly FloorZ. The door // The tube centreline is tangent to the floor: the tube bottom is exactly FloorZ. The door
// sits one voxel inside the room so the room/tube smooth union cannot leave a lip. // sits 0.25 m inside the room so the room/tube smooth union cannot leave a lip.
Landing.DoorPoint = Landing.StandingPoint Landing.DoorPoint = Landing.StandingPoint
+ Direction * FMath::Max(0.0f, Landing.HalfWidth - 1.0f); + Direction * FMath::Max(0.0f, Landing.HalfWidth
- VoxelPassageGeometry::MetersToVoxels(0.25f));
Landing.DoorPoint.Z = Landing.FloorZ + SafeMouthRadius; Landing.DoorPoint.Z = Landing.FloorZ + SafeMouthRadius;
return Landing; return Landing;
@@ -6862,11 +6866,14 @@ float VF_EvaluatePassageLandingSDF(
Landing.StandingPoint.X, Landing.StandingPoint.X,
Landing.StandingPoint.Y, Landing.StandingPoint.Y,
(Landing.FloorZ + Landing.CeilingZ) * 0.5f); (Landing.FloorZ + Landing.CeilingZ) * 0.5f);
constexpr float RoomRounding = 1.5f; constexpr float RoomRounding = VoxelPassageGeometry::RoomRoundingVoxels;
const FVector RoomHalfExtent( const FVector RoomHalfExtent(
FMath::Max(Landing.HalfWidth - RoomRounding, 0.25f), FMath::Max(Landing.HalfWidth - RoomRounding,
FMath::Max(Landing.HalfWidth - RoomRounding, 0.25f), VoxelPassageGeometry::MetersToVoxels(0.0625f)),
FMath::Max((Landing.CeilingZ - Landing.FloorZ) * 0.5f - RoomRounding, 0.25f)); FMath::Max(Landing.HalfWidth - RoomRounding,
VoxelPassageGeometry::MetersToVoxels(0.0625f)),
FMath::Max((Landing.CeilingZ - Landing.FloorZ) * 0.5f - RoomRounding,
VoxelPassageGeometry::MetersToVoxels(0.0625f)));
// max(Room, FloorZ-Z) is the important distinction from a bore: below FloorZ is solid, // max(Room, FloorZ-Z) is the important distinction from a bore: below FloorZ is solid,
// while every horizontal section above it has the same flat floor plane. // while every horizontal section above it has the same flat floor plane.
@@ -6901,7 +6908,8 @@ bool VF_IsPassageLandingFloor(
// three-metre turning patch. The origin room owns its centre, including its support floor. // three-metre turning patch. The origin room owns its centre, including its support floor.
if (IsInFloorBand(Position.Z, Landing.FloorZ, Landing.FloorThickness)) if (IsInFloorBand(Position.Z, Landing.FloorZ, Landing.FloorThickness))
{ {
const float FloorHalfWidth = FMath::Max(Landing.HalfWidth - 1.0f, 0.0f); const float FloorHalfWidth = FMath::Max(Landing.HalfWidth
- VoxelPassageGeometry::MetersToVoxels(0.25f), 0.0f);
if (FMath::Abs(Position.X - Landing.StandingPoint.X) <= FloorHalfWidth if (FMath::Abs(Position.X - Landing.StandingPoint.X) <= FloorHalfWidth
&& FMath::Abs(Position.Y - Landing.StandingPoint.Y) <= FloorHalfWidth) && FMath::Abs(Position.Y - Landing.StandingPoint.Y) <= FloorHalfWidth)
{ {
@@ -723,7 +723,7 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
{ {
// INTEGER voxel jitter: columns land on integer XY so the generator's surface-column cache // INTEGER voxel jitter: columns land on integer XY so the generator's surface-column cache
// (T1.a, §8.10) applies — FRACTIONAL XY recomputes the noise-heavy heightfield+biome on EVERY // (T1.a, §8.10) applies — FRACTIONAL XY recomputes the noise-heavy heightfield+biome on EVERY
// sample. A 25 cm grid offset is imperceptible; random yaw/scale still breaks up the regularity. // sample. A 12.5 cm grid offset is imperceptible; random yaw/scale still breaks up the regularity.
const uint32 HJ = DecoHash(Cell.X, Cell.Y, gx, gy, -1, 0, InSeed, 0x10C0u); const uint32 HJ = DecoHash(Cell.X, Cell.Y, gx, gy, -1, 0, InSeed, 0x10C0u);
const int32 JX = FMath::Min(Spacing - 1, (int32)(VoxelHash::ToFloat01(HJ) * (float)Spacing)); const int32 JX = FMath::Min(Spacing - 1, (int32)(VoxelHash::ToFloat01(HJ) * (float)Spacing));
const int32 JY = FMath::Min(Spacing - 1, (int32)(VoxelHash::ToFloat01(VoxelHash::Mix(HJ ^ 0x68BC21EBu)) * (float)Spacing)); const int32 JY = FMath::Min(Spacing - 1, (int32)(VoxelHash::ToFloat01(VoxelHash::Mix(HJ ^ 0x68BC21EBu)) * (float)Spacing));
@@ -145,7 +145,7 @@ void UVoxelDensityVolume::ResumeAfterGenerationChange()
int32 UVoxelDensityVolume::ResPerAxis() const int32 UVoxelDensityVolume::ResPerAxis() const
{ {
return FMath::Clamp(Settings ? Settings->DensityVolumeResolution : 128, 32, 256); return FMath::Clamp(Settings ? Settings->DensityVolumeResolution : 256, 32, 512);
} }
int32 UVoxelDensityVolume::NumLevels() const int32 UVoxelDensityVolume::NumLevels() const
+2 -2
View File
@@ -2722,9 +2722,9 @@ void UVoxelGenerator::InitializeSettings(const UVoxelSettings* Settings)
// Les paramètres globaux sont copiés ici une seule fois : le chemin voxel ne doit pas // Les paramètres globaux sont copiés ici une seule fois : le chemin voxel ne doit pas
// déréférencer l'asset de settings. // déréférencer l'asset de settings.
Seed = Settings ? Settings->GetEffectiveWorldSeed() : 0; Seed = Settings ? Settings->GetEffectiveWorldSeed() : 0;
OriginSpineRadius = Settings ? Settings->GetEffectiveOriginSpineRadius() : 14.0f; OriginSpineRadius = Settings ? Settings->GetEffectiveOriginSpineRadius() : 28.0f;
WorldRadiusVoxels = Settings ? Settings->GetEffectiveWorldRadiusVoxels() : 0.0f; WorldRadiusVoxels = Settings ? Settings->GetEffectiveWorldRadiusVoxels() : 0.0f;
EdgeSealThickness = Settings ? Settings->EdgeSealThickness : 64.0f; EdgeSealThickness = Settings ? Settings->EdgeSealThickness : 128.0f;
} }
void UVoxelGenerator::BeginDensityBlock(FIntVector OriginVoxels, int32 Step, void UVoxelGenerator::BeginDensityBlock(FIntVector OriginVoxels, int32 Step,
@@ -4,11 +4,16 @@
#include "VoxelMarchingCubesMesher.h" #include "VoxelMarchingCubesMesher.h"
#include "MarchingCubesTables.h" #include "MarchingCubesTables.h"
#include "VoxelDensityProfile.h" #include "VoxelDensityProfile.h"
#if WITH_EDITOR
#include "VoxelRedefinedDensityField.h"
#endif
#include "VoxelStrateManager.h" #include "VoxelStrateManager.h"
#include "VoxelTilePostReach.h" #include "VoxelTilePostReach.h"
#include "VoxelPassageGeometry.h"
#include "ProfilingDebugging/CpuProfilerTrace.h" #include "ProfilingDebugging/CpuProfilerTrace.h"
#include "HAL/IConsoleManager.h" #include "HAL/IConsoleManager.h"
#include "Misc/CommandLine.h" #include "Misc/CommandLine.h"
#include <atomic>
namespace namespace
{ {
@@ -703,7 +708,16 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
// gradient) sont échantillonnées — le reste du tampon reste non initialisé et non lu. // gradient) sont échantillonnées — le reste du tampon reste non initialisé et non lu.
VoxelDensityProfile::FScopedTimer MesherDensityGridTimer( VoxelDensityProfile::FScopedTimer MesherDensityGridTimer(
VoxelDensityProfile::EBucket::MesherDensityGrid); VoxelDensityProfile::EBucket::MesherDensityGrid);
#if WITH_EDITOR
const bool bUseParameterizedBrickEvaluator =
VoxelForgeRedefinedDensityField::IsParameterizedEnabled()
&& !bUseSharedDensityGrid && !bCapturePreEditGrid
&& OutCaptureGrid == nullptr;
#else
constexpr bool bUseParameterizedBrickEvaluator = false;
#endif
const bool bUseOperatorBlock = !bUseSharedDensityGrid const bool bUseOperatorBlock = !bUseSharedDensityGrid
&& !bUseParameterizedBrickEvaluator
&& GVoxelForgeUseOperatorBlock != 0; && GVoxelForgeUseOperatorBlock != 0;
if (bUseOperatorBlock) if (bUseOperatorBlock)
{ {
@@ -713,7 +727,149 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
OriginVoxels + FIntVector(-Step, -Step, GzLo * Step), OriginVoxels + FIntVector(-Step, -Step, GzLo * Step),
Step, MDim, MDim, GzHi - GzLo + 1); Step, MDim, MDim, GzHi - GzLo + 1);
} }
for (int32 gz = GzLo; gz <= GzHi; gz++) if (bUseParameterizedBrickEvaluator)
{
#if WITH_EDITOR
static std::atomic<bool> bLoggedParameterizedBrickRoute{false};
if (!bLoggedParameterizedBrickRoute.exchange(true, std::memory_order_relaxed))
{
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeResearchBrickEvaluator] mesher_grid=1 brick_samples=%d detail_scale=%.6f"),
VoxelForgeRedefinedDensityField::ParameterizedBrickSamples,
VoxelForgeRedefinedDensityField::GetDetailScale());
}
constexpr int32 ResearchBrickSamples =
VoxelForgeRedefinedDensityField::ParameterizedBrickSamples;
const float DetailScale = VoxelForgeRedefinedDensityField::GetDetailScale();
TArray<float> DetailBrick;
TArray<float> BrickValues;
TArray<float> ColumnValues;
TArray<uint8> ProofKinds;
VoxelForgeRedefinedDensityField::FParameterizedBrickContext BrickContext;
const int32 GridZSamples = GzHi - GzLo + 1;
const int32 ProofZCount = FMath::DivideAndRoundUp(GridZSamples, ResearchBrickSamples);
ProofKinds.SetNumUninitialized(ProofZCount);
for (int32 BrickY = 0; BrickY < MDim; BrickY += ResearchBrickSamples)
for (int32 BrickX = 0; BrickX < MDim; BrickX += ResearchBrickSamples)
{
if (ShouldAbortWork())
{
return FVoxelMeshData();
}
const int32 BrickSizeX = FMath::Min(ResearchBrickSamples, MDim - BrickX);
const int32 BrickSizeY = FMath::Min(ResearchBrickSamples, MDim - BrickY);
for (int32 ProofZ = 0; ProofZ < ProofZCount; ++ProofZ)
{
if (ShouldAbortWork())
{
return FVoxelMeshData();
}
const int32 BrickZ = ProofZ * ResearchBrickSamples;
const FIntVector ProofSize(
BrickSizeX, BrickSizeY,
FMath::Min(ResearchBrickSamples, GridZSamples - BrickZ));
const FIntVector ProofOrigin = OriginVoxels + FIntVector(
(BrickX - 1) * Step,
(BrickY - 1) * Step,
(GzLo + BrickZ) * Step);
bool bUniformAir = false;
if (VoxelForgeRedefinedDensityField::TryClassifyParameterizedBrick(
*Generator, ProofOrigin, ProofSize, bUniformAir, DetailScale,
nullptr, nullptr, Step))
{
ProofKinds[ProofZ] = bUniformAir ? 1 : 2;
const float UniformDensity = bUniformAir
? VoxelForgeRedefinedDensityField::ParameterizedUniformDensity
: -VoxelForgeRedefinedDensityField::ParameterizedUniformDensity;
for (int32 Z = 0; Z < ProofSize.Z; ++Z)
for (int32 Y = 0; Y < ProofSize.Y; ++Y)
for (int32 X = 0; X < ProofSize.X; ++X)
{
const int32 GX = BrickX + X - 1;
const int32 GY = BrickY + Y - 1;
const int32 GZ = GzLo + BrickZ + Z;
const int32 LocalIndex =
((GZ + 1) * MDim + (GY + 1)) * MDim + (GX + 1);
DensityGrid[LocalIndex] = UniformDensity;
}
}
else
{
ProofKinds[ProofZ] = 0;
}
}
for (int32 ProofZ = 0; ProofZ < ProofZCount;)
{
if (ProofKinds[ProofZ] != 0)
{
++ProofZ;
continue;
}
if (ShouldAbortWork())
{
return FVoxelMeshData();
}
const int32 FirstMixedProofZ = ProofZ;
const int32 BrickZ = FirstMixedProofZ * ResearchBrickSamples;
const int32 FirstWorldZ = OriginVoxels.Z + (GzLo + BrickZ) * Step;
const int32 FirstChunkZ = FMath::FloorToInt(
static_cast<float>(FirstWorldZ) / CHUNK_SIZE);
int32 RunProofCount = 1;
int32 RunSampleCount = FMath::Min(
ResearchBrickSamples, GridZSamples - BrickZ);
while (ProofZ + RunProofCount < ProofZCount
&& ProofKinds[ProofZ + RunProofCount] == 0)
{
const int32 CandidateSampleCount = FMath::Min(
(RunProofCount + 1) * ResearchBrickSamples,
GridZSamples - BrickZ);
const int32 CandidateEndZ = FirstWorldZ
+ (CandidateSampleCount - 1) * Step;
const int32 CandidateChunkZ = FMath::FloorToInt(
static_cast<float>(CandidateEndZ) / CHUNK_SIZE);
if (CandidateSampleCount > FMath::Max(1, CHUNK_SIZE / Step)
|| CandidateChunkZ != FirstChunkZ)
{
break;
}
++RunProofCount;
RunSampleCount = CandidateSampleCount;
}
const FIntVector BrickSize(BrickSizeX, BrickSizeY, RunSampleCount);
const FIntVector BrickOrigin = OriginVoxels + FIntVector(
(BrickX - 1) * Step,
(BrickY - 1) * Step,
(GzLo + BrickZ) * Step);
VoxelForgeRedefinedDensityField::EvaluateParameterizedDensityBrick(
*Generator, BrickOrigin, BrickSize, DetailScale, BrickContext,
DetailBrick, ColumnValues, BrickValues, Step);
for (int32 Z = 0; Z < BrickSize.Z; ++Z)
for (int32 Y = 0; Y < BrickSize.Y; ++Y)
for (int32 X = 0; X < BrickSize.X; ++X)
{
const int32 GX = BrickX + X - 1;
const int32 GY = BrickY + Y - 1;
const int32 GZ = GzLo + BrickZ + Z;
const int32 LocalIndex =
((GZ + 1) * MDim + (GY + 1)) * MDim + (GX + 1);
DensityGrid[LocalIndex] =
BrickValues[(Z * BrickSize.Y + Y) * BrickSize.X + X];
}
if (OutDensitySampleCount != nullptr)
{
*OutDensitySampleCount += static_cast<int64>(
BrickSize.X * BrickSize.Y * BrickSize.Z);
}
ProofZ += RunProofCount;
}
}
#endif
}
else for (int32 gz = GzLo; gz <= GzHi; gz++)
{ {
if (ShouldAbortWork()) if (ShouldAbortWork())
{ {
File diff suppressed because it is too large. Load diff
@@ -1716,11 +1716,11 @@ namespace
float StrateHeightInVoxels) float StrateHeightInVoxels)
{ {
const float H = FMath::Max(StrateHeightInVoxels, static_cast<float>(CHUNK_SIZE)); const float H = FMath::Max(StrateHeightInVoxels, static_cast<float>(CHUNK_SIZE));
// Body-derived lattice: a 3 m usable floor needs a 4.286 m clear diameter. At 25 cm per // Body-derived lattice: a 3 m usable floor needs a 4.286 m clear diameter. At 12.5 cm per
// voxel that is 17.143 voxels, radius 8.571. The proven 3.75-voxel roughness envelope is // voxel that is 17.143 voxels, radius 8.571. The proven 3.75-voxel roughness envelope is
// added to the authored radius, then rounded up to 12.5: the worst-case usable floor is // added to the authored radius, then rounded up to 12.5: the worst-case usable floor is
// still 0.7 * (2 * (12.5 - 3.75) * 0.25) = 3.0625 m. CellSize stays 64-96 voxels, so // remains 0.7 * (2 * (12.5 - 3.75) * 0.125) = 1.53125 m with the current
// radius remains well below CellSize/2 and a real rock wall survives. // voxel-valued authoring. CellSize stays 64-96 voxels (8-12 m), so a rock web remains.
P.CellSize = VF_CorpusFreeU( P.CellSize = VF_CorpusFreeU(
Rng, 64.0f, FMath::Max(64.0f, FMath::Min(96.0f, H * 0.75f))); Rng, 64.0f, FMath::Max(64.0f, FMath::Min(96.0f, H * 0.75f)));
P.CorridorRadius = VF_CorpusFreeU( P.CorridorRadius = VF_CorpusFreeU(
@@ -2192,11 +2192,14 @@ void UVoxelStrateManager::GeneratePassages()
const FVector TunnelLowerDoorDirection = DoorDirection; const FVector TunnelLowerDoorDirection = DoorDirection;
Passage.LowerLanding.DoorDirection = TunnelLowerDoorDirection; Passage.LowerLanding.DoorDirection = TunnelLowerDoorDirection;
const float UpperDoorOffset = FMath::Max( const float UpperDoorOffset = FMath::Max(
Passage.UpperLanding.HalfWidth - 1.0f, 0.0f); Passage.UpperLanding.HalfWidth
- VoxelPassageGeometry::MetersToVoxels(0.25f), 0.0f);
const float LowerDoorOffset = FMath::Max( const float LowerDoorOffset = FMath::Max(
Passage.LowerLanding.HalfWidth - 1.0f, 0.0f); Passage.LowerLanding.HalfWidth
- VoxelPassageGeometry::MetersToVoxels(0.25f), 0.0f);
const float SafeMouthRadius = FMath::Max( const float SafeMouthRadius = FMath::Max(
FMath::Abs(Cfg.MouthRadius), 1.0f); FMath::Abs(Cfg.MouthRadius),
VoxelPassageGeometry::MetersToVoxels(0.25f));
Passage.UpperLanding.DoorPoint = Passage.UpperLanding.DoorPoint =
Passage.UpperLanding.StandingPoint + DoorDirection * UpperDoorOffset; Passage.UpperLanding.StandingPoint + DoorDirection * UpperDoorOffset;
Passage.UpperLanding.DoorPoint.Z = Passage.UpperLanding.DoorPoint.Z =
@@ -2602,7 +2605,8 @@ void UVoxelStrateManager::GeneratePassages()
Entry.PassageType = EVoxelPassageType::VerticalShaft; Entry.PassageType = EVoxelPassageType::VerticalShaft;
// Match the reserved future shaft envelope exactly. The entry is only opened through // Match the reserved future shaft envelope exactly. The entry is only opened through
// the top seal into the finite landing room; lower strate seals remain untouched. // the top seal into the finite landing room; lower strate seals remain untouched.
Entry.Radius = FMath::Max(OriginSpineRadius, 1.0f); Entry.Radius = FMath::Max(OriginSpineRadius,
VoxelPassageGeometry::MetersToVoxels(0.25f));
Entry.UpperPoint = FVector(0.0f, 0.0f, TopZ + CHUNK_SIZE); Entry.UpperPoint = FVector(0.0f, 0.0f, TopZ + CHUNK_SIZE);
// The capsule's lower tangent meets the room ceiling; it never bores through the // The capsule's lower tangent meets the room ceiling; it never bores through the
// room's floor or creates an origin column in strate 0. // room's floor or creates an origin column in strate 0.
@@ -3063,9 +3067,11 @@ static bool VF_IsPassageRoomFloor(
return Position.Z <= Landing.FloorZ + KINDA_SMALL_NUMBER return Position.Z <= Landing.FloorZ + KINDA_SMALL_NUMBER
&& Position.Z > Landing.FloorZ - Landing.FloorThickness && Position.Z > Landing.FloorZ - Landing.FloorThickness
&& FMath::Abs(Position.X - Landing.StandingPoint.X) && FMath::Abs(Position.X - Landing.StandingPoint.X)
<= FMath::Max(Landing.HalfWidth - 1.0f, 0.0f) <= FMath::Max(Landing.HalfWidth
- VoxelPassageGeometry::MetersToVoxels(0.25f), 0.0f)
&& FMath::Abs(Position.Y - Landing.StandingPoint.Y) && FMath::Abs(Position.Y - Landing.StandingPoint.Y)
<= FMath::Max(Landing.HalfWidth - 1.0f, 0.0f); <= FMath::Max(Landing.HalfWidth
- VoxelPassageGeometry::MetersToVoxels(0.25f), 0.0f);
} }
void UVoxelStrateManager::ApplyPassageStructuralPostsMC( void UVoxelStrateManager::ApplyPassageStructuralPostsMC(
@@ -3719,10 +3725,12 @@ namespace
{ {
continue; continue;
} }
constexpr float RoomRounding = 1.5f; constexpr float RoomRounding = VoxelPassageGeometry::RoomRoundingVoxels;
const float Height = Landing->CeilingZ - Landing->FloorZ; const float Height = Landing->CeilingZ - Landing->FloorZ;
const float HalfX = FMath::Max(Landing->HalfWidth - RoomRounding, 0.25f); const float HalfX = FMath::Max(Landing->HalfWidth - RoomRounding,
const float HalfZ = FMath::Max(Height * 0.5f - RoomRounding, 0.25f); VoxelPassageGeometry::MetersToVoxels(0.0625f));
const float HalfZ = FMath::Max(Height * 0.5f - RoomRounding,
VoxelPassageGeometry::MetersToVoxels(0.0625f));
const float CenterZ = (Landing->FloorZ + Landing->CeilingZ) * 0.5f; const float CenterZ = (Landing->FloorZ + Landing->CeilingZ) * 0.5f;
if (!AddDomain( if (!AddDomain(
FVector(Landing->StandingPoint.X - HalfX - RoomRounding, FVector(Landing->StandingPoint.X - HalfX - RoomRounding,
@@ -4068,15 +4076,18 @@ namespace
return true; return true;
} }
constexpr float RoomRounding = 1.5f; constexpr float RoomRounding = VoxelPassageGeometry::RoomRoundingVoxels;
const float Height = Landing.CeilingZ - Landing.FloorZ; const float Height = Landing.CeilingZ - Landing.FloorZ;
const FVector Center( const FVector Center(
Landing.StandingPoint.X, Landing.StandingPoint.Y, Landing.StandingPoint.X, Landing.StandingPoint.Y,
(Landing.FloorZ + Landing.CeilingZ) * 0.5f); (Landing.FloorZ + Landing.CeilingZ) * 0.5f);
const FVector HalfExtent( const FVector HalfExtent(
FMath::Max(Landing.HalfWidth - RoomRounding, 0.25f), FMath::Max(Landing.HalfWidth - RoomRounding,
FMath::Max(Landing.HalfWidth - RoomRounding, 0.25f), VoxelPassageGeometry::MetersToVoxels(0.0625f)),
FMath::Max(Height * 0.5f - RoomRounding, 0.25f)); FMath::Max(Landing.HalfWidth - RoomRounding,
VoxelPassageGeometry::MetersToVoxels(0.0625f)),
FMath::Max(Height * 0.5f - RoomRounding,
VoxelPassageGeometry::MetersToVoxels(0.0625f)));
const FVector OuterMin = Center - HalfExtent - FVector(RoomRounding); const FVector OuterMin = Center - HalfExtent - FVector(RoomRounding);
const FVector OuterMax = Center + HalfExtent + FVector(RoomRounding); const FVector OuterMax = Center + HalfExtent + FVector(RoomRounding);
const FVector BoxMin((float)Box.Min.X, (float)Box.Min.Y, (float)Box.Min.Z); const FVector BoxMin((float)Box.Min.X, (float)Box.Min.Y, (float)Box.Min.Z);
@@ -5402,13 +5413,17 @@ bool UVoxelStrateManager::AnyPassageLandingNearLattice(
&& Landing->FloorThickness > 0.0f && Landing->FloorThickness > 0.0f
&& VF_LatticeBoxTouchesAABB( && VF_LatticeBoxTouchesAABB(
VoxelBox, LatticeOrigin, Step, VoxelBox, LatticeOrigin, Step,
Landing->StandingPoint.X - FMath::Max(Landing->HalfWidth - 1.0f, 0.0f) Landing->StandingPoint.X - FMath::Max(Landing->HalfWidth
- VoxelPassageGeometry::MetersToVoxels(0.25f), 0.0f)
- ReachScale, - ReachScale,
Landing->StandingPoint.X + FMath::Max(Landing->HalfWidth - 1.0f, 0.0f) Landing->StandingPoint.X + FMath::Max(Landing->HalfWidth
- VoxelPassageGeometry::MetersToVoxels(0.25f), 0.0f)
+ ReachScale, + ReachScale,
Landing->StandingPoint.Y - FMath::Max(Landing->HalfWidth - 1.0f, 0.0f) Landing->StandingPoint.Y - FMath::Max(Landing->HalfWidth
- VoxelPassageGeometry::MetersToVoxels(0.25f), 0.0f)
- ReachScale, - ReachScale,
Landing->StandingPoint.Y + FMath::Max(Landing->HalfWidth - 1.0f, 0.0f) Landing->StandingPoint.Y + FMath::Max(Landing->HalfWidth
- VoxelPassageGeometry::MetersToVoxels(0.25f), 0.0f)
+ ReachScale, + ReachScale,
Landing->FloorZ - Landing->FloorThickness - ReachScale, Landing->FloorZ - Landing->FloorThickness - ReachScale,
Landing->FloorZ + ReachScale)) Landing->FloorZ + ReachScale))
@@ -5444,10 +5459,12 @@ bool UVoxelStrateManager::AnyPassageLandingNearLattice(
{ {
continue; continue;
} }
constexpr float RoomRounding = 1.5f; constexpr float RoomRounding = VoxelPassageGeometry::RoomRoundingVoxels;
const float Height = Landing->CeilingZ - Landing->FloorZ; const float Height = Landing->CeilingZ - Landing->FloorZ;
const float HalfX = FMath::Max(Landing->HalfWidth - RoomRounding, 0.25f); const float HalfX = FMath::Max(Landing->HalfWidth - RoomRounding,
const float HalfZ = FMath::Max(Height * 0.5f - RoomRounding, 0.25f); VoxelPassageGeometry::MetersToVoxels(0.0625f));
const float HalfZ = FMath::Max(Height * 0.5f - RoomRounding,
VoxelPassageGeometry::MetersToVoxels(0.0625f));
const float CenterZ = (Landing->FloorZ + Landing->CeilingZ) * 0.5f; const float CenterZ = (Landing->FloorZ + Landing->CeilingZ) * 0.5f;
if (VF_LatticeBoxTouchesAABB( if (VF_LatticeBoxTouchesAABB(
VoxelBox, LatticeOrigin, Step, VoxelBox, LatticeOrigin, Step,
@@ -5651,10 +5668,12 @@ bool UVoxelStrateManager::AnyPassageCarvingNearLattice(
{ {
continue; continue;
} }
constexpr float RoomRounding = 1.5f; constexpr float RoomRounding = VoxelPassageGeometry::RoomRoundingVoxels;
const float Height = Landing->CeilingZ - Landing->FloorZ; const float Height = Landing->CeilingZ - Landing->FloorZ;
const float HalfX = FMath::Max(Landing->HalfWidth - RoomRounding, 0.25f); const float HalfX = FMath::Max(Landing->HalfWidth - RoomRounding,
const float HalfZ = FMath::Max(Height * 0.5f - RoomRounding, 0.25f); VoxelPassageGeometry::MetersToVoxels(0.0625f));
const float HalfZ = FMath::Max(Height * 0.5f - RoomRounding,
VoxelPassageGeometry::MetersToVoxels(0.0625f));
const float CenterZ = (Landing->FloorZ + Landing->CeilingZ) * 0.5f; const float CenterZ = (Landing->FloorZ + Landing->CeilingZ) * 0.5f;
if (VF_LatticeBoxTouchesAABB( if (VF_LatticeBoxTouchesAABB(
VoxelBox, LatticeOrigin, Step, VoxelBox, LatticeOrigin, Step,
@@ -5773,7 +5792,8 @@ bool UVoxelStrateManager::AnyLandingFloorAtLattice(
// structural writer, not evidence that the writer is absent. // structural writer, not evidence that the writer is absent.
return true; return true;
} }
const float HalfWidth = FMath::Max(Landing.HalfWidth - 1.0f, 0.0f); const float HalfWidth = FMath::Max(Landing.HalfWidth
- VoxelPassageGeometry::MetersToVoxels(0.25f), 0.0f);
int32 IX0 = 0, IX1 = -1, IY0 = 0, IY1 = -1, IZ0 = 0, IZ1 = -1; int32 IX0 = 0, IX1 = -1, IY0 = 0, IY1 = -1, IZ0 = 0, IZ1 = -1;
return VF_LatticeAxisRange( return VF_LatticeAxisRange(
FMath::Max((float)VoxelBox.Min.X, FMath::Max((float)VoxelBox.Min.X,
@@ -5899,7 +5919,8 @@ bool UVoxelStrateManager::AnyLandingFloorAtLattice(
{ {
continue; continue;
} }
const float HalfWidth = FMath::Max(Geometry.HalfWidth - 1.0f, 0.0f); const float HalfWidth = FMath::Max(Geometry.HalfWidth
- VoxelPassageGeometry::MetersToVoxels(0.25f), 0.0f);
int32 IX0 = 0, IX1 = -1, IY0 = 0, IY1 = -1, IZ0 = 0, IZ1 = -1; int32 IX0 = 0, IX1 = -1, IY0 = 0, IY1 = -1, IZ0 = 0, IZ1 = -1;
if (VF_LatticeAxisRange( if (VF_LatticeAxisRange(
FMath::Max((float)VoxelBox.Min.X, -HalfWidth - 0.0f), FMath::Max((float)VoxelBox.Min.X, -HalfWidth - 0.0f),
+337 -108
View File
@@ -764,8 +764,8 @@ namespace
FVoxelStrateMeasureSettings VF_ComposerMeasureSettings() FVoxelStrateMeasureSettings VF_ComposerMeasureSettings()
{ {
FVoxelStrateMeasureSettings Settings; FVoxelStrateMeasureSettings Settings;
Settings.SampleStep = 4; Settings.SampleStep = 8;
Settings.RadiusInVoxels = 256; Settings.RadiusInVoxels = 512;
Settings.CenterXY = FVector2D::ZeroVector; Settings.CenterXY = FVector2D::ZeroVector;
Settings.MaxCells = 8000000; Settings.MaxCells = 8000000;
Settings.MaxRouteRetries = 16; Settings.MaxRouteRetries = 16;
@@ -1752,6 +1752,26 @@ void AVoxelWorld::EndPlay(const EEndPlayReason::Type EndPlayReason)
VoxelForgeStartupTrace::Finish(TEXT("end_play_before_steady_state")); VoxelForgeStartupTrace::Finish(TEXT("end_play_before_steady_state"));
} }
if (bResearchGamePerfEnabled)
{
ResearchGamePerfMovingFrameTimesMs.Sort();
const auto PercentileMs = [this](double Percentile) -> double
{
if (ResearchGamePerfMovingFrameTimesMs.IsEmpty()) return 0.0;
const int32 Index = FMath::Clamp(
FMath::CeilToInt(Percentile * ResearchGamePerfMovingFrameTimesMs.Num()) - 1,
0, ResearchGamePerfMovingFrameTimesMs.Num() - 1);
return ResearchGamePerfMovingFrameTimesMs[Index];
};
const double MaximumMs = ResearchGamePerfMovingFrameTimesMs.IsEmpty()
? 0.0 : ResearchGamePerfMovingFrameTimesMs.Last();
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeResearchGamePerf] moving_frames=%d moving_window_s=%.3f "
"p50_ms=%.3f p95_ms=%.3f p99_ms=%.3f max_ms=%.3f"),
ResearchGamePerfMovingFrameTimesMs.Num(), ResearchGamePerfMovingSeconds,
PercentileMs(0.50), PercentileMs(0.95), PercentileMs(0.99), MaximumMs);
}
LogVisualStalenessAudit(); LogVisualStalenessAudit();
GVoxelForgeDigMotionTests.Remove(this); GVoxelForgeDigMotionTests.Remove(this);
GVoxelForgeVisualGapAudits.Remove(this); GVoxelForgeVisualGapAudits.Remove(this);
@@ -1776,6 +1796,16 @@ void AVoxelWorld::BeginPlay()
VoxelForgeRedefinedDensityField::SetParameterSet(ResearchParameterizedCaveSet); VoxelForgeRedefinedDensityField::SetParameterSet(ResearchParameterizedCaveSet);
} }
#endif #endif
#if WITH_EDITOR
AppliedResearchFieldEnabled = VoxelForgeRedefinedDensityField::IsParameterizedEnabled() ? 1 : 0;
AppliedResearchFieldProfile = VoxelForgeRedefinedDensityField::GetParameterSet();
#endif
int32 ResearchGamePerfValue = 0;
FParse::Value(FCommandLine::Get(), TEXT("voxel.ResearchGamePerf="), ResearchGamePerfValue);
bResearchGamePerfEnabled = ResearchGamePerfValue != 0;
ResearchGamePerfLastFrameSeconds = 0.0;
ResearchGamePerfMovingSeconds = 0.0;
ResearchGamePerfMovingFrameTimesMs.Reset();
VoxelForgeStartupTrace::BeginFromCommandLine(); VoxelForgeStartupTrace::BeginFromCommandLine();
VoxelForgeStartupTrace::FStageScope StartupTraceStage(TEXT("BeginPlay")); VoxelForgeStartupTrace::FStageScope StartupTraceStage(TEXT("BeginPlay"));
Super::BeginPlay(); Super::BeginPlay();
@@ -2095,6 +2125,38 @@ void AVoxelWorld::BeginPlay()
FParse::Value(FCommandLine::Get(), TEXT("voxel.TestCeilingCaptureDelaySeconds="), FParse::Value(FCommandLine::Get(), TEXT("voxel.TestCeilingCaptureDelaySeconds="),
TestCeilingCaptureDelaySeconds); TestCeilingCaptureDelaySeconds);
TestCeilingCaptureDelaySeconds = FMath::Max(TestCeilingCaptureDelaySeconds, 1.0f); TestCeilingCaptureDelaySeconds = FMath::Max(TestCeilingCaptureDelaySeconds, 1.0f);
FParse::Value(FCommandLine::Get(), TEXT("voxel.TestCeilingSecondCaptureDelaySeconds="),
TestCeilingSecondCaptureDelaySeconds);
FParse::Value(FCommandLine::Get(), TEXT("voxel.TestCeilingCameraMoveSpeedCmPerSecond="),
TestCeilingCameraMoveSpeedCmPerSecond);
FParse::Value(FCommandLine::Get(), TEXT("voxel.TestCeilingCameraMoveStartSeconds="),
TestCeilingCameraMoveStartSeconds);
FParse::Value(FCommandLine::Get(), TEXT("voxel.TestCeilingCameraMoveDurationSeconds="),
TestCeilingCameraMoveDurationSeconds);
FString CameraMoveDirectionText;
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.TestCeilingCameraMoveDirection="),
CameraMoveDirectionText))
{
TArray<FString> Components;
CameraMoveDirectionText.ParseIntoArray(Components, TEXT(","), true);
if (Components.Num() == 3)
{
TestCeilingCameraMoveDirection = FVector(
FCString::Atof(*Components[0]), FCString::Atof(*Components[1]),
FCString::Atof(*Components[2]));
}
}
TestCeilingCameraMoveDirection = TestCeilingCameraMoveDirection.GetSafeNormal();
if (TestCeilingCameraMoveDirection.IsNearlyZero())
{
TestCeilingCameraMoveDirection = FVector::XAxisVector;
}
TestCeilingCameraMoveSpeedCmPerSecond = FMath::Max(
TestCeilingCameraMoveSpeedCmPerSecond, 0.0f);
TestCeilingCameraMoveStartSeconds = FMath::Max(
TestCeilingCameraMoveStartSeconds, 0.0f);
TestCeilingCameraMoveDurationSeconds = FMath::Max(
TestCeilingCameraMoveDurationSeconds, 0.0f);
FParse::Value(FCommandLine::Get(), TEXT("voxel.TestCeilingWallCloseup="), FParse::Value(FCommandLine::Get(), TEXT("voxel.TestCeilingWallCloseup="),
TestCeilingWallCloseup); TestCeilingWallCloseup);
TestCeilingViewBeginSeconds = FPlatformTime::Seconds(); TestCeilingViewBeginSeconds = FPlatformTime::Seconds();
@@ -2108,6 +2170,32 @@ void AVoxelWorld::BeginPlay()
TestCeilingCameraRotation.Yaw, TestCeilingCameraRotation.Roll, TestCeilingCameraRotation.Yaw, TestCeilingCameraRotation.Roll,
HeadlessStreamingTestCenterVoxel.X, HeadlessStreamingTestCenterVoxel.Y, HeadlessStreamingTestCenterVoxel.X, HeadlessStreamingTestCenterVoxel.Y,
HeadlessStreamingTestCenterVoxel.Z, TestCeilingCaptureDelaySeconds); HeadlessStreamingTestCenterVoxel.Z, TestCeilingCaptureDelaySeconds);
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeTestCeiling] second_capture_delay_s=%.1f camera_move_cm_s=%.1f "
"move_start_s=%.1f move_duration_s=%.1f move_direction=(%.3f,%.3f,%.3f) "
"research_game_perf=%d"),
TestCeilingSecondCaptureDelaySeconds, TestCeilingCameraMoveSpeedCmPerSecond,
TestCeilingCameraMoveStartSeconds, TestCeilingCameraMoveDurationSeconds,
TestCeilingCameraMoveDirection.X, TestCeilingCameraMoveDirection.Y,
TestCeilingCameraMoveDirection.Z, bResearchGamePerfEnabled ? 1 : 0);
}
TestResearchProfileSwitchAfterSeconds = -1.0f;
TestResearchProfileSwitchTo = -1;
TestResearchProfileSwitchBeginSeconds = FPlatformTime::Seconds();
bTestResearchProfileSwitchConfigured =
FParse::Value(FCommandLine::Get(),
TEXT("voxel.TestResearchProfileSwitchAfterSeconds="),
TestResearchProfileSwitchAfterSeconds)
&& FParse::Value(FCommandLine::Get(),
TEXT("voxel.TestResearchProfileSwitchTo="), TestResearchProfileSwitchTo)
&& TestResearchProfileSwitchAfterSeconds >= 0.0f;
bTestResearchProfileSwitchApplied = false;
if (bTestResearchProfileSwitchConfigured)
{
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeResearchProfileSwitch] configured=1 after_s=%.3f target_profile=%d"),
TestResearchProfileSwitchAfterSeconds, TestResearchProfileSwitchTo);
} }
// The owner's current DA_Settings is intentionally a one-strate surface layout, so it cannot // The owner's current DA_Settings is intentionally a one-strate surface layout, so it cannot
@@ -2499,7 +2587,8 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
bHeadlessCollisionGateStressTestFailed = false; bHeadlessCollisionGateStressTestFailed = false;
bHeadlessCollisionGateStressTestExitRequested = false; bHeadlessCollisionGateStressTestExitRequested = false;
bHeadlessCollisionGateStressTestGateWasEngaged = false; bHeadlessCollisionGateStressTestGateWasEngaged = false;
bHeadlessCollisionGateStressTestHorizontalGateProbeApplied = false; bHeadlessCollisionGateStressTestFullStopProbeApplied = false;
bHeadlessCollisionGateStressTestFullStopProbeObserved = false;
bHeadlessCollisionGateStressTestFullStop = false; bHeadlessCollisionGateStressTestFullStop = false;
bHeadlessCollisionGateStressTestStartHeightCrossed = false; bHeadlessCollisionGateStressTestStartHeightCrossed = false;
bHeadlessCollisionGateStressTestTerrainViolation = false; bHeadlessCollisionGateStressTestTerrainViolation = false;
@@ -2521,6 +2610,7 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
HeadlessCollisionGateStressTestLandedSeconds = 0.0; HeadlessCollisionGateStressTestLandedSeconds = 0.0;
HeadlessCollisionGateStressTestGateStartSeconds = 0.0; HeadlessCollisionGateStressTestGateStartSeconds = 0.0;
HeadlessCollisionGateStressTestFullStopStartSeconds = 0.0; HeadlessCollisionGateStressTestFullStopStartSeconds = 0.0;
HeadlessCollisionGateStressTestFullStopProbeStartSeconds = 0.0;
HeadlessCollisionGateStressTestDistanceCm = 0.0; HeadlessCollisionGateStressTestDistanceCm = 0.0;
HeadlessCollisionGateStressTestGateDistanceCm = 0.0; HeadlessCollisionGateStressTestGateDistanceCm = 0.0;
HeadlessCollisionGateStressTestTotalGateDurationSeconds = 0.0; HeadlessCollisionGateStressTestTotalGateDurationSeconds = 0.0;
@@ -2549,8 +2639,8 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
bHeadlessSurfaceFallTestExitRequested = false; bHeadlessSurfaceFallTestExitRequested = false;
bHeadlessSurfaceFallTestPredictionProbeInjected = false; bHeadlessSurfaceFallTestPredictionProbeInjected = false;
bHeadlessSurfaceFallTestPredictionProbeCleared = false; bHeadlessSurfaceFallTestPredictionProbeCleared = false;
HeadlessSurfaceFallTestSpawnHeightVoxels = 20.0f; HeadlessSurfaceFallTestSpawnHeightVoxels = 5.0f * 100.0f / VOXEL_SIZE;
HeadlessSurfaceFallTestGroundToleranceVoxels = 4.0f; HeadlessSurfaceFallTestGroundToleranceVoxels = 1.0f * 100.0f / VOXEL_SIZE;
HeadlessSurfaceFallTestTimeoutSeconds = 8.0f; HeadlessSurfaceFallTestTimeoutSeconds = 8.0f;
HeadlessSurfaceFallTestPlayerHalfHeightVoxels = HeadlessSurfaceFallTestPlayerHalfHeightVoxels =
VoxelPassageGeometry::PlayerHalfHeightVoxels; VoxelPassageGeometry::PlayerHalfHeightVoxels;
@@ -2715,7 +2805,7 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
HeadlessStrateCrossingTestStartDelaySeconds = 1.0f; HeadlessStrateCrossingTestStartDelaySeconds = 1.0f;
HeadlessStrateCrossingTestTimeoutSeconds = 120.0f; HeadlessStrateCrossingTestTimeoutSeconds = 120.0f;
HeadlessStrateCrossingTestMaxGateSeconds = 10.0f; HeadlessStrateCrossingTestMaxGateSeconds = 10.0f;
HeadlessStrateCrossingTestRouteTargetRadiusVoxels = 2.0f; HeadlessStrateCrossingTestRouteTargetRadiusVoxels = 0.5f * 100.0f / VOXEL_SIZE;
FParse::Value(CommandLine, TEXT("voxel.TestStrateCrossingSpeedCmPerSecond="), FParse::Value(CommandLine, TEXT("voxel.TestStrateCrossingSpeedCmPerSecond="),
HeadlessStrateCrossingTestSpeedCmPerSecond); HeadlessStrateCrossingTestSpeedCmPerSecond);
FParse::Value(CommandLine, TEXT("voxel.TestStrateCrossingStartDelaySeconds="), FParse::Value(CommandLine, TEXT("voxel.TestStrateCrossingStartDelaySeconds="),
@@ -3434,7 +3524,7 @@ void AVoxelWorld::ObserveHeadlessStrateCrossingTest(
const float Clearance = FeetZ - PassageFloorZ; const float Clearance = FeetZ - PassageFloorZ;
HeadlessStrateCrossingTestMinFloorClearanceVoxels = FMath::Min( HeadlessStrateCrossingTestMinFloorClearanceVoxels = FMath::Min(
HeadlessStrateCrossingTestMinFloorClearanceVoxels, Clearance); HeadlessStrateCrossingTestMinFloorClearanceVoxels, Clearance);
constexpr float FloorViolationToleranceVoxels = 2.0f; constexpr float FloorViolationToleranceVoxels = 0.5f * 100.0f / VOXEL_SIZE;
if (Clearance < -FloorViolationToleranceVoxels) if (Clearance < -FloorViolationToleranceVoxels)
{ {
bHeadlessStrateCrossingTestFailed = true; bHeadlessStrateCrossingTestFailed = true;
@@ -3688,8 +3778,9 @@ void AVoxelWorld::AdvanceHeadlessDigMotionTest(FVector& InOutPlayerPosition, APa
BestDirection = Index; BestDirection = Index;
} }
} }
constexpr float DigOffsetVoxels = 8.0f; constexpr float DigOffsetVoxels = 2.0f * 100.0f / VOXEL_SIZE;
if (Clearances[BestDirection] < DigOffsetVoxels + 3.0f) constexpr float DigClearanceMarginVoxels = 0.75f * 100.0f / VOXEL_SIZE;
if (Clearances[BestDirection] < DigOffsetVoxels + DigClearanceMarginVoxels)
{ {
return; return;
} }
@@ -3996,10 +4087,12 @@ bool AVoxelWorld::MeasureHeadlessCollisionGateStressTerrain(
// nearest air/solid zero crossing adjacent to the capsule's lowest point, then linearly // nearest air/solid zero crossing adjacent to the capsule's lowest point, then linearly
// interpolate it. Searching up when the feet sample is solid reports negative clearance // interpolate it. Searching up when the feet sample is solid reports negative clearance
// instead of silently choosing a lower floor after a fall-through. // instead of silently choosing a lower floor after a fall-through.
// Keep the half-voxel surface search resolution, but double the number of samples after the
// voxel size halved so the search retains its original physical reach.
constexpr float SearchStepVoxels = 0.5f; constexpr float SearchStepVoxels = 0.5f;
const int32 MaxSearchSteps = bHeadlessCollisionGateStressDeepDrop const int32 MaxSearchSteps = bHeadlessCollisionGateStressDeepDrop
&& !bHeadlessCollisionGateStressDeepDropSurfaceCached && !bHeadlessCollisionGateStressDeepDropSurfaceCached
? 512 : 256; ? 1024 : 512;
float LastZ = FeetZ; float LastZ = FeetZ;
float LastDensity = OutFeetDensity; float LastDensity = OutFeetDensity;
const bool bSearchDown = OutFeetDensity >= 0.0f; const bool bSearchDown = OutFeetDensity >= 0.0f;
@@ -4051,6 +4144,13 @@ void AVoxelWorld::AdvanceHeadlessCollisionGateStressTest(
} }
const double Now = FPlatformTime::Seconds(); const double Now = FPlatformTime::Seconds();
if (HeadlessCollisionGateStressTestBeginSeconds <= 0.0)
{
// Include setup failures in the bounded stress-test lifetime too. Otherwise a research
// field with no surface at the default probe can set Failed before the start placement,
// leave BeginSeconds at zero, and strand the commandlet game until the outer watchdog.
HeadlessCollisionGateStressTestBeginSeconds = Now;
}
int32 DeepDropTopChunkZ = 0; int32 DeepDropTopChunkZ = 0;
int32 DeepDropBottomChunkZ = 0; int32 DeepDropBottomChunkZ = 0;
if (bHeadlessCollisionGateStressDeepDrop) if (bHeadlessCollisionGateStressDeepDrop)
@@ -4085,16 +4185,19 @@ void AVoxelWorld::AdvanceHeadlessCollisionGateStressTest(
// world only. // world only.
constexpr float DeepDropCenterX = CHUNK_SIZE * 0.5f; constexpr float DeepDropCenterX = CHUNK_SIZE * 0.5f;
constexpr float DeepDropCenterY = CHUNK_SIZE * 0.5f; constexpr float DeepDropCenterY = CHUNK_SIZE * 0.5f;
constexpr float DeepDropShaftStartX = -32.0f; constexpr float DeepDropShaftStartX =
VoxelPassageGeometry::MetersToVoxels(-8.0f);
const float DeepDropShaftEndX = CHUNK_SIZE * 0.5f const float DeepDropShaftEndX = CHUNK_SIZE * 0.5f
+ (HeadlessCollisionGateStressTestSpeedCmPerSecond + (HeadlessCollisionGateStressTestSpeedCmPerSecond
* HeadlessCollisionGateStressTestDurationSeconds / VOXEL_SIZE) * HeadlessCollisionGateStressTestDurationSeconds / VOXEL_SIZE)
+ 64.0f; + VoxelPassageGeometry::MetersToVoxels(16.0f);
const float StrateTopExclusiveVoxels = const float StrateTopExclusiveVoxels =
static_cast<float>(DeepDropTopChunkZ + 1) * CHUNK_SIZE; static_cast<float>(DeepDropTopChunkZ + 1) * CHUNK_SIZE;
const float ShaftTopVoxels = StrateTopExclusiveVoxels + 32.0f; const float ShaftTopVoxels = StrateTopExclusiveVoxels
+ VoxelPassageGeometry::MetersToVoxels(8.0f);
const float ShaftBottomVoxels = const float ShaftBottomVoxels =
static_cast<float>(DeepDropBottomChunkZ + 1) * CHUNK_SIZE - 1.0f; static_cast<float>(DeepDropBottomChunkZ + 1) * CHUNK_SIZE
- VoxelPassageGeometry::MetersToVoxels(0.25f);
if (ShaftBottomVoxels >= ShaftTopVoxels || DiffLayer == nullptr) if (ShaftBottomVoxels >= ShaftTopVoxels || DiffLayer == nullptr)
{ {
bHeadlessCollisionGateStressTestFailed = true; bHeadlessCollisionGateStressTestFailed = true;
@@ -4115,7 +4218,9 @@ void AVoxelWorld::AdvanceHeadlessCollisionGateStressTest(
const FVector ShaftCenterWorld = LocalVoxelToWorld(FVector( const FVector ShaftCenterWorld = LocalVoxelToWorld(FVector(
ShaftCenterX, DeepDropCenterY, ShaftCenterX, DeepDropCenterY,
(ShaftTopVoxels + ShaftBottomVoxels) * 0.5f)); (ShaftTopVoxels + ShaftBottomVoxels) * 0.5f));
const FVector ShaftHalfExtent(ShaftHalfExtentX, 18.0f, ShaftHalfExtentZ); const FVector ShaftHalfExtent(
ShaftHalfExtentX, VoxelPassageGeometry::MetersToVoxels(4.5f),
ShaftHalfExtentZ);
const float FixtureBrushRadius = FMath::Max( const float FixtureBrushRadius = FMath::Max(
FMath::Max3(ShaftHalfExtent.X, ShaftHalfExtent.Y, ShaftHalfExtent.Z), FMath::Max3(ShaftHalfExtent.X, ShaftHalfExtent.Y, ShaftHalfExtent.Z),
Settings != nullptr ? Settings->MaxBrushRadius : 0.0f); Settings != nullptr ? Settings->MaxBrushRadius : 0.0f);
@@ -4255,7 +4360,7 @@ void AVoxelWorld::AdvanceHeadlessCollisionGateStressTest(
// metre above its floor. Start above the measured density surface at this XY so the pawn // metre above its floor. Start above the measured density surface at this XY so the pawn
// must fall onto a freshly requested level-0 tile before the horizontal diagnostic. // must fall onto a freshly requested level-0 tile before the horizontal diagnostic.
const float LaunchLocalVoxelX = bHeadlessCollisionGateStressDeepDrop const float LaunchLocalVoxelX = bHeadlessCollisionGateStressDeepDrop
? CHUNK_SIZE * 0.5f : 18.2f; ? CHUNK_SIZE * 0.5f : VoxelPassageGeometry::MetersToVoxels(4.55f);
const float LaunchLocalVoxelY = bHeadlessCollisionGateStressDeepDrop const float LaunchLocalVoxelY = bHeadlessCollisionGateStressDeepDrop
? CHUNK_SIZE * 0.5f : 0.0f; ? CHUNK_SIZE * 0.5f : 0.0f;
float LaunchHeightAboveGroundVoxels = float LaunchHeightAboveGroundVoxels =
@@ -4314,7 +4419,8 @@ void AVoxelWorld::AdvanceHeadlessCollisionGateStressTest(
const float StrateTopExclusiveVoxels = const float StrateTopExclusiveVoxels =
static_cast<float>(SurfaceStrateTopChunkZ + 1) * CHUNK_SIZE; static_cast<float>(SurfaceStrateTopChunkZ + 1) * CHUNK_SIZE;
LaunchLocalVoxelZ = StrateTopExclusiveVoxels LaunchLocalVoxelZ = StrateTopExclusiveVoxels
- CapsuleHalfHeightVoxels - 0.5f; - CapsuleHalfHeightVoxels
- VoxelPassageGeometry::MetersToVoxels(0.125f);
LaunchHeightAboveGroundVoxels = LaunchLocalVoxelZ LaunchHeightAboveGroundVoxels = LaunchLocalVoxelZ
- SurfaceLocalVoxelZ - CapsuleHalfHeightVoxels; - SurfaceLocalVoxelZ - CapsuleHalfHeightVoxels;
const FIntVector LaunchCenterTile( const FIntVector LaunchCenterTile(
@@ -4322,7 +4428,7 @@ void AVoxelWorld::AdvanceHeadlessCollisionGateStressTest(
FMath::FloorToInt(LaunchLocalVoxelY / CHUNK_SIZE), FMath::FloorToInt(LaunchLocalVoxelY / CHUNK_SIZE),
FMath::FloorToInt(LaunchLocalVoxelZ / CHUNK_SIZE)); FMath::FloorToInt(LaunchLocalVoxelZ / CHUNK_SIZE));
const int32 ClipRadius = Settings != nullptr const int32 ClipRadius = Settings != nullptr
? FMath::Max(1, Settings->ClipRadius) : 3; ? FMath::Max(1, Settings->ClipRadius) : 6;
VoxelClipmapDesiredTiles::AlignedRingBounds( VoxelClipmapDesiredTiles::AlignedRingBounds(
LaunchCenterTile, ClipRadius, 2, LevelZeroRingMin, LevelZeroRingMax); LaunchCenterTile, ClipRadius, 2, LevelZeroRingMin, LevelZeroRingMax);
if (LaunchHeightAboveGroundVoxels <= 0.0f if (LaunchHeightAboveGroundVoxels <= 0.0f
@@ -4608,9 +4714,8 @@ void AVoxelWorld::AdvanceHeadlessCollisionGateStressTest(
return; return;
} }
// The measured interval starts with the vertical drop. Horizontal movement begins only after // The measured interval starts with the vertical drop. The gate must also stop a small
// CharacterMovement reports a landing, so the test observes the entire fall and then checks // horizontal probe while support collision is unresolved, then resume normal swept movement.
// that support gating leaves horizontal movement available.
if (!bHeadlessCollisionGateStressTestLanded) if (!bHeadlessCollisionGateStressTestLanded)
{ {
if (bHeadlessCollisionGateStressDeepDrop if (bHeadlessCollisionGateStressDeepDrop
@@ -4626,11 +4731,11 @@ void AVoxelWorld::AdvanceHeadlessCollisionGateStressTest(
Velocity.X = 100.0f; Velocity.X = 100.0f;
CharacterMovement->Velocity = Velocity; CharacterMovement->Velocity = Velocity;
CharacterMovement->UpdateComponentVelocity(); CharacterMovement->UpdateComponentVelocity();
if (!bHeadlessCollisionGateStressTestHorizontalGateProbeApplied) if (!bHeadlessCollisionGateStressTestFullStopProbeApplied)
{ {
bHeadlessCollisionGateStressTestHorizontalGateProbeApplied = true; bHeadlessCollisionGateStressTestFullStopProbeApplied = true;
UE_LOG(LogTemp, Display, UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGateStressTest] horizontal_gate_probe " TEXT("[VoxelForgeCollisionGateStressTest] full_stop_probe "
"applied=1 velocity_x_cm_s=%.1f gate=1"), Velocity.X); "applied=1 velocity_x_cm_s=%.1f gate=1"), Velocity.X);
} }
} }
@@ -4652,8 +4757,7 @@ void AVoxelWorld::AdvanceHeadlessCollisionGateStressTest(
{ {
if (UCharacterMovementComponent* CharacterMovement = Character->GetCharacterMovement()) if (UCharacterMovementComponent* CharacterMovement = Character->GetCharacterMovement())
{ {
// The baseline gate changes MOVE_Walking to MOVE_None. Do not undo that here; the // Do not undo MOVE_None while the collision gate is engaged; the gate owns the hold.
// test must observe the resulting full stop. The fall-only gate keeps this input live.
if (!bPawnCollisionGateEngaged if (!bPawnCollisionGateEngaged
&& CharacterMovement->MovementMode == MOVE_None) && CharacterMovement->MovementMode == MOVE_None)
{ {
@@ -4687,15 +4791,6 @@ void AVoxelWorld::ObserveHeadlessCollisionGateStressTest(
HeadlessCollisionGateStressTestLastActualPosition = PlayerPosition; HeadlessCollisionGateStressTestLastActualPosition = PlayerPosition;
const bool bGateEngaged = bPawnCollisionGateEngaged; const bool bGateEngaged = bPawnCollisionGateEngaged;
if (bGateEngaged && bHeadlessCollisionGateStressTestLanded
&& HorizontalDelta > 0.1f
&& !bHeadlessCollisionGateStressTestHorizontalGateProbeApplied)
{
bHeadlessCollisionGateStressTestHorizontalGateProbeApplied = true;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGateStressTest] horizontal_gate_probe "
"observed=1 delta_cm=%.3f gate=1"), HorizontalDelta);
}
if (bGateEngaged || bHeadlessCollisionGateStressTestGateWasEngaged) if (bGateEngaged || bHeadlessCollisionGateStressTestGateWasEngaged)
{ {
HeadlessCollisionGateStressTestGateDistanceCm += HorizontalDelta; HeadlessCollisionGateStressTestGateDistanceCm += HorizontalDelta;
@@ -4723,6 +4818,33 @@ void AVoxelWorld::ObserveHeadlessCollisionGateStressTest(
} }
bHeadlessCollisionGateStressTestGateWasEngaged = bGateEngaged; bHeadlessCollisionGateStressTestGateWasEngaged = bGateEngaged;
// Confirm the injected horizontal velocity was blocked for a sustained interval. Merely
// applying the probe while a gate is engaged does not prove that the pawn stayed put.
constexpr double ProbeStopConfirmationSeconds = 0.05;
const bool bProbeHeldStill = bHeadlessCollisionGateStressTestFullStopProbeApplied
&& bGateEngaged && HorizontalDelta <= 0.01f;
if (bProbeHeldStill)
{
if (HeadlessCollisionGateStressTestFullStopProbeStartSeconds <= 0.0)
{
HeadlessCollisionGateStressTestFullStopProbeStartSeconds = Now;
}
else if (!bHeadlessCollisionGateStressTestFullStopProbeObserved
&& Now - HeadlessCollisionGateStressTestFullStopProbeStartSeconds
>= ProbeStopConfirmationSeconds)
{
bHeadlessCollisionGateStressTestFullStopProbeObserved = true;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGateStressTest] full_stop_probe "
"observed=1 gate=1 held_s=%.6f"),
Now - HeadlessCollisionGateStressTestFullStopProbeStartSeconds);
}
}
else if (!bHeadlessCollisionGateStressTestFullStopProbeObserved)
{
HeadlessCollisionGateStressTestFullStopProbeStartSeconds = 0.0;
}
// The first sample after engaging any gate can have zero horizontal delta simply because the // The first sample after engaging any gate can have zero horizontal delta simply because the
// world prerequisite ran before CharacterMovementComponent. Require a sustained no-progress // world prerequisite ran before CharacterMovementComponent. Require a sustained no-progress
// interval before calling it a full stop; a permanent stop still fails through the distance // interval before calling it a full stop; a permanent stop still fails through the distance
@@ -4986,6 +5108,37 @@ void AVoxelWorld::ObserveHeadlessCollisionGateStressTest(
bSweptIntoSolid ? 1 : 0); bSweptIntoSolid ? 1 : 0);
} }
// A research-field tile can remain queued longer than the stress window because pointwise
// evaluation is expensive. Treat a sustained, measured full stop above that unresolved tile as
// a safety pass; the result still reports landed=0 and the pending support interval, so this
// cannot be mistaken for a streaming-liveness or traversal pass.
constexpr double SafePendingHoldSeconds = 0.5;
const double CurrentGateHoldSeconds = bGateEngaged
&& HeadlessCollisionGateStressTestGateStartSeconds > 0.0
? FMath::Max(0.0, Now - HeadlessCollisionGateStressTestGateStartSeconds)
: 0.0;
if (!bHeadlessCollisionGateStressTestPassed
&& !bHeadlessCollisionGateStressTestFailed
&& bHeadlessCollisionGateStressDeepDrop && bCollisionGateEnabled
&& bHeadlessCollisionGateStressTestFullStopProbeApplied
&& bHeadlessCollisionGateStressTestFullStopProbeObserved
&& bGateEngaged && !bGateSupportReady && GateSupportUnavailableReason == nullptr
&& bSurfaceFound && ClearanceCm >= -AllowedSurfacePenetrationCm
&& !bHeadlessCollisionGateStressTestTerrainViolation
&& HeadlessCollisionGateStressTestTerrainSampleCount > 0
&& CurrentGateHoldSeconds >= SafePendingHoldSeconds)
{
bHeadlessCollisionGateStressTestPassed = true;
HeadlessCollisionGateStressTestFailureReason = TEXT("safe_pending_support_hold");
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGateStressTest] safety_hold_pass elapsed_s=%.6f "
"hold_s=%.6f clearance_cm=%.3f support=(%d,%d,%d) "
"support_ready=0 landed=%d"),
Now - HeadlessCollisionGateStressTestBeginSeconds, CurrentGateHoldSeconds,
ClearanceCm, GateSupportTile.Coord.X, GateSupportTile.Coord.Y,
GateSupportTile.Coord.Z, bHeadlessCollisionGateStressTestLanded ? 1 : 0);
}
++HeadlessCollisionGateStressTestTraceFrame; ++HeadlessCollisionGateStressTestTraceFrame;
UE_LOG(LogTemp, Display, UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGateStressTest] trace frame=%d elapsed_s=%.6f " TEXT("[VoxelForgeCollisionGateStressTest] trace frame=%d elapsed_s=%.6f "
@@ -5187,7 +5340,7 @@ void AVoxelWorld::AdvanceHeadlessSurfaceFallTest(
if (bProbeSupportReady if (bProbeSupportReady
&& bProbeSupportInLayout && bProbeSupportInLayout
&& LocalPosition.Z - HeadlessSurfaceFallTestPlayerHalfHeightVoxels && LocalPosition.Z - HeadlessSurfaceFallTestPlayerHalfHeightVoxels
> HeadlessSurfaceFallTestTerrainZ + 2.0f) > HeadlessSurfaceFallTestTerrainZ + 0.5f * 100.0f / VOXEL_SIZE)
{ {
bHeadlessSurfaceFallTestPredictionProbeInjected = true; bHeadlessSurfaceFallTestPredictionProbeInjected = true;
HeadlessSurfaceFallTestPredictionProbePosition = InOutPlayerPosition; HeadlessSurfaceFallTestPredictionProbePosition = InOutPlayerPosition;
@@ -5393,13 +5546,13 @@ void AVoxelWorld::MaybeFinishHeadlessCollisionGateStressTest()
if (bHeadlessCollisionGateStressTestPassed if (bHeadlessCollisionGateStressTestPassed
&& !bHeadlessCollisionGateStressTestFailed && !bHeadlessCollisionGateStressTestFailed
&& bHeadlessCollisionGateStressDeepDrop && bCollisionGateEnabled && bHeadlessCollisionGateStressDeepDrop && bCollisionGateEnabled
&& (!bHeadlessCollisionGateStressTestHorizontalGateProbeApplied && (!bHeadlessCollisionGateStressTestFullStopProbeApplied
|| HeadlessCollisionGateStressTestGateDistanceCm < 1.0)) || !bHeadlessCollisionGateStressTestFullStopProbeObserved))
{ {
bHeadlessCollisionGateStressTestPassed = false; bHeadlessCollisionGateStressTestPassed = false;
bHeadlessCollisionGateStressTestFailed = true; bHeadlessCollisionGateStressTestFailed = true;
HeadlessCollisionGateStressTestFailureReason = HeadlessCollisionGateStressTestFailureReason =
TEXT("gate_horizontal_movement_blocked"); TEXT("gate_full_stop_probe_not_observed");
} }
if (!bHeadlessCollisionGateStressTestPassed && !bHeadlessCollisionGateStressTestFailed) if (!bHeadlessCollisionGateStressTestPassed && !bHeadlessCollisionGateStressTestFailed)
@@ -5503,7 +5656,7 @@ void AVoxelWorld::MaybeFinishHeadlessCollisionGateStressTest()
TEXT("[VoxelForgeCollisionGateStressTest] result=%s reason=%s elapsed_s=%.6f " TEXT("[VoxelForgeCollisionGateStressTest] result=%s reason=%s elapsed_s=%.6f "
"distance_m=%.6f expected_distance_m=%.6f speed_cm_s=%.1f spawn_height_vox=%.2f deep_drop=%d " "distance_m=%.6f expected_distance_m=%.6f speed_cm_s=%.1f spawn_height_vox=%.2f deep_drop=%d "
"gate_holds=%d gate_total_s=%.6f max_gate_s=%.6f gate_progress_m=%.6f " "gate_holds=%d gate_total_s=%.6f max_gate_s=%.6f gate_progress_m=%.6f "
"horizontal_gate_probe=%d " "full_stop_probe_applied=%d full_stop_probe_observed=%d "
"full_stops=%d full_stop_total_s=%.6f collision_gate=%d " "full_stops=%d full_stop_total_s=%.6f collision_gate=%d "
"support_tile=(%d,%d,%d) support_first_check_to_submit_s=%.6f " "support_tile=(%d,%d,%d) support_first_check_to_submit_s=%.6f "
"support_first_check_to_collision_ready_s=%.6f " "support_first_check_to_collision_ready_s=%.6f "
@@ -5515,7 +5668,8 @@ void AVoxelWorld::MaybeFinishHeadlessCollisionGateStressTest()
"min_clearance_position=(%.2f,%.2f,%.2f) min_feet_density=%.6f " "min_clearance_position=(%.2f,%.2f,%.2f) min_feet_density=%.6f "
"min_surface_vox_z=%.4f final_position=(%.1f,%.1f,%.1f)"), "min_surface_vox_z=%.4f final_position=(%.1f,%.1f,%.1f)"),
bPassed ? TEXT("PASS") : TEXT("FAIL"), bPassed ? TEXT("PASS") : TEXT("FAIL"),
bPassed ? TEXT("capsule_never_below_density_surface") bPassed && HeadlessCollisionGateStressTestFailureReason.IsEmpty()
? TEXT("capsule_never_below_density_surface")
: *HeadlessCollisionGateStressTestFailureReason, : *HeadlessCollisionGateStressTestFailureReason,
Now - HeadlessCollisionGateStressTestBeginSeconds, Now - HeadlessCollisionGateStressTestBeginSeconds,
HeadlessCollisionGateStressTestDistanceCm / 100.0, HeadlessCollisionGateStressTestDistanceCm / 100.0,
@@ -5527,7 +5681,8 @@ void AVoxelWorld::MaybeFinishHeadlessCollisionGateStressTest()
HeadlessCollisionGateStressTestTotalGateDurationSeconds, HeadlessCollisionGateStressTestTotalGateDurationSeconds,
HeadlessCollisionGateStressTestMaxGateDurationSeconds, HeadlessCollisionGateStressTestMaxGateDurationSeconds,
HeadlessCollisionGateStressTestGateDistanceCm / 100.0, HeadlessCollisionGateStressTestGateDistanceCm / 100.0,
bHeadlessCollisionGateStressTestHorizontalGateProbeApplied ? 1 : 0, bHeadlessCollisionGateStressTestFullStopProbeApplied ? 1 : 0,
bHeadlessCollisionGateStressTestFullStopProbeObserved ? 1 : 0,
HeadlessCollisionGateStressTestFullStopCount, HeadlessCollisionGateStressTestFullStopCount,
HeadlessCollisionGateStressTestFullStopDurationSeconds, HeadlessCollisionGateStressTestFullStopDurationSeconds,
bCollisionGateEnabled ? 1 : 0, bCollisionGateEnabled ? 1 : 0,
@@ -6339,6 +6494,38 @@ void AVoxelWorld::Tick(float DeltaTime)
{ {
Super::Tick(DeltaTime); Super::Tick(DeltaTime);
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_Tick); // game-thread streaming orchestration breakdown TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_Tick); // game-thread streaming orchestration breakdown
#if WITH_EDITOR
if (bTestResearchProfileSwitchConfigured
&& !bTestResearchProfileSwitchApplied
&& FPlatformTime::Seconds() - TestResearchProfileSwitchBeginSeconds
>= static_cast<double>(TestResearchProfileSwitchAfterSeconds))
{
const int32 PreviousProfile = VoxelForgeRedefinedDensityField::GetParameterSet();
const int32 TargetProfile = FMath::Clamp(
TestResearchProfileSwitchTo, 0,
VoxelForgeRedefinedDensityField::GetParameterSetCount() - 1);
VoxelForgeRedefinedDensityField::SetParameterSet(TargetProfile);
bTestResearchProfileSwitchApplied = true;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeResearchProfileSwitch] applied=1 profile=%d->%d loaded_tiles=%d desired_epoch=%u"),
PreviousProfile, TargetProfile, LoadedTiles.Num(), DesiredEpoch);
}
const int32 ResearchFieldEnabled =
VoxelForgeRedefinedDensityField::IsParameterizedEnabled() ? 1 : 0;
const int32 ResearchFieldProfile = VoxelForgeRedefinedDensityField::GetParameterSet();
if (AppliedResearchFieldEnabled != INDEX_NONE
&& (AppliedResearchFieldEnabled != ResearchFieldEnabled
|| AppliedResearchFieldProfile != ResearchFieldProfile))
{
UE_LOG(LogTemp, Display,
TEXT("[VoxelWorld] research_field_selection_changed enabled=%d->%d profile=%d->%d; rebuilding loaded world"),
AppliedResearchFieldEnabled, ResearchFieldEnabled,
AppliedResearchFieldProfile, ResearchFieldProfile);
AppliedResearchFieldEnabled = ResearchFieldEnabled;
AppliedResearchFieldProfile = ResearchFieldProfile;
RegenerateAllChunks();
}
#endif
if (DensityVolume && (!Settings || !Settings->bEnableDensityVolume)) if (DensityVolume && (!Settings || !Settings->bEnableDensityVolume))
{ {
DensityVolume->NotifyShutdown(); DensityVolume->NotifyShutdown();
@@ -6709,16 +6896,16 @@ void AVoxelWorld::Tick(float DeltaTime)
Camera->AddInstanceComponent(CaptureHeadlamp); Camera->AddInstanceComponent(CaptureHeadlamp);
CaptureHeadlamp->SetupAttachment(Camera->GetRootComponent()); CaptureHeadlamp->SetupAttachment(Camera->GetRootComponent());
CaptureHeadlamp->SetMobility(EComponentMobility::Movable); CaptureHeadlamp->SetMobility(EComponentMobility::Movable);
CaptureHeadlamp->SetIntensity(80000.0f); CaptureHeadlamp->SetIntensity(6500.0f);
CaptureHeadlamp->SetAttenuationRadius(10000.0f); CaptureHeadlamp->SetAttenuationRadius(1600.0f);
CaptureHeadlamp->SetUseInverseSquaredFalloff(false); CaptureHeadlamp->SetUseInverseSquaredFalloff(true);
CaptureHeadlamp->SetLightFalloffExponent(0.0f); CaptureHeadlamp->SetSourceRadius(8.0f);
CaptureHeadlamp->SetLightColor(FLinearColor(1.0f, 0.88f, 0.72f)); CaptureHeadlamp->SetLightColor(FLinearColor(1.0f, 0.88f, 0.72f));
CaptureHeadlamp->SetCastShadows(false); CaptureHeadlamp->SetCastShadows(true);
CaptureHeadlamp->RegisterComponent(); CaptureHeadlamp->RegisterComponent();
UE_LOG(LogTemp, Display, UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeTestCeiling] camera_headlamp=1 intensity=80000 radius_cm=10000 " TEXT("[VoxelForgeTestCeiling] camera_headlamp=1 intensity_lm=6500 radius_cm=1600 "
"inverse_square=0 falloff_exponent=0")); "inverse_square=1 source_radius_cm=8 shadows=1"));
} }
UE_LOG(LogTemp, Display, UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeTestCeiling] camera_installed=1 applied_visible_tiles=%llu"), TEXT("[VoxelForgeTestCeiling] camera_installed=1 applied_visible_tiles=%llu"),
@@ -6726,12 +6913,64 @@ void AVoxelWorld::Tick(float DeltaTime)
} }
} }
if (TestCeilingCamera.IsValid() && !bTestCeilingCaptureRequested const double CameraNow = FPlatformTime::Seconds();
&& AppliedVisibleTileCount >= 16 const double CameraElapsedSeconds = CameraNow - TestCeilingViewBeginSeconds;
&& FPlatformTime::Seconds() - TestCeilingViewBeginSeconds const bool bCameraMoveWindow = TestCeilingCameraMoveSpeedCmPerSecond > 0.0f
>= static_cast<double>(TestCeilingCaptureDelaySeconds)) && CameraElapsedSeconds >= TestCeilingCameraMoveStartSeconds
&& (TestCeilingCameraMoveDurationSeconds <= 0.0f
|| CameraElapsedSeconds < TestCeilingCameraMoveStartSeconds
+ TestCeilingCameraMoveDurationSeconds);
if (TestCeilingCamera.IsValid() && TestCeilingCameraMoveSpeedCmPerSecond > 0.0f)
{
if (bCameraMoveWindow)
{
if (TestCeilingCameraLastMoveSeconds > 0.0)
{
const double MoveDeltaSeconds = FMath::Max(
0.0, CameraNow - TestCeilingCameraLastMoveSeconds);
TestCeilingCameraWorldPosition += TestCeilingCameraMoveDirection
* TestCeilingCameraMoveSpeedCmPerSecond
* static_cast<float>(MoveDeltaSeconds);
if (ACameraActor* Camera = TestCeilingCamera.Get())
{
Camera->SetActorLocation(TestCeilingCameraWorldPosition);
}
HeadlessStreamingTestCenterVoxel = WorldToLocalVoxel(
TestCeilingCameraWorldPosition);
bHeadlessStreamingTestCenterOverride = true;
if (bResearchGamePerfEnabled && IsParameterizedResearchFieldActive())
{
ResearchGamePerfMovingFrameTimesMs.Add(MoveDeltaSeconds * 1000.0);
ResearchGamePerfMovingSeconds += MoveDeltaSeconds;
}
}
TestCeilingCameraLastMoveSeconds = CameraNow;
}
else
{
TestCeilingCameraLastMoveSeconds = 0.0;
}
}
const bool bFirstCaptureDue = !bTestCeilingCaptureRequested
&& CameraElapsedSeconds >= static_cast<double>(TestCeilingCaptureDelaySeconds);
const bool bSecondCaptureDue = bTestCeilingCaptureRequested
&& !bTestCeilingSecondCaptureRequested
&& TestCeilingSecondCaptureDelaySeconds > 0.0f
&& CameraElapsedSeconds >= static_cast<double>(TestCeilingSecondCaptureDelaySeconds)
&& (!bTestResearchProfileSwitchConfigured || bTestResearchProfileSwitchApplied);
if (TestCeilingCamera.IsValid() && AppliedVisibleTileCount >= 16
&& (bFirstCaptureDue || bSecondCaptureDue))
{
const int32 CaptureIndex = bFirstCaptureDue ? 1 : 2;
if (CaptureIndex == 1)
{ {
bTestCeilingCaptureRequested = true; bTestCeilingCaptureRequested = true;
}
else
{
bTestCeilingSecondCaptureRequested = true;
}
FVector ActiveViewWorldPosition = FVector::ZeroVector; FVector ActiveViewWorldPosition = FVector::ZeroVector;
FRotator ActiveViewRotation = FRotator::ZeroRotator; FRotator ActiveViewRotation = FRotator::ZeroRotator;
PC->GetPlayerViewPoint(ActiveViewWorldPosition, ActiveViewRotation); PC->GetPlayerViewPoint(ActiveViewWorldPosition, ActiveViewRotation);
@@ -6761,10 +7000,11 @@ void AVoxelWorld::Tick(float DeltaTime)
&& GEngine->GameViewport->GetGameViewport() && GEngine->GameViewport->GetGameViewport()
&& GEngine->GameViewport->GetGameViewport()->TakeHighResScreenShot(); && GEngine->GameViewport->GetGameViewport()->TakeHighResScreenShot();
UE_LOG(LogTemp, Display, UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeTestCeiling] capture_requested=%d elapsed_s=%.3f " TEXT("[VoxelForgeTestCeiling] capture_index=%d capture_requested=%d elapsed_s=%.3f "
"visible_tiles=%llu triangles=%llu"), "visible_tiles=%llu triangles=%llu"),
CaptureIndex,
bCaptureAccepted ? 1 : 0, bCaptureAccepted ? 1 : 0,
FPlatformTime::Seconds() - TestCeilingViewBeginSeconds, CameraElapsedSeconds,
static_cast<unsigned long long>(AppliedVisibleTileCount), static_cast<unsigned long long>(AppliedVisibleTileCount),
static_cast<unsigned long long>(AppliedTriangleCount)); static_cast<unsigned long long>(AppliedTriangleCount));
} }
@@ -7174,7 +7414,7 @@ void AVoxelWorld::RequestCollisionGateSupportTile(const FVoxelTileKey& SupportTi
FIntVector RingMin = FIntVector::ZeroValue; FIntVector RingMin = FIntVector::ZeroValue;
FIntVector RingMax = FIntVector::ZeroValue; FIntVector RingMax = FIntVector::ZeroValue;
VoxelClipmapDesiredTiles::AlignedRingBounds( VoxelClipmapDesiredTiles::AlignedRingBounds(
CurrentCenterChunk, Settings ? FMath::Max(1, Settings->ClipRadius) : 3, CurrentCenterChunk, Settings ? FMath::Max(1, Settings->ClipRadius) : 6,
2, RingMin, RingMax); 2, RingMin, RingMax);
UE_LOG(LogTemp, Display, UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGateStressTest] gate_context target=(%d,%d,%d) " TEXT("[VoxelForgeCollisionGateStressTest] gate_context target=(%d,%d,%d) "
@@ -7467,7 +7707,7 @@ void AVoxelWorld::UpdatePawnCollisionGate(APawn* Pawn, const FVector& PlayerPosi
{ {
UE_LOG(LogTemp, Display, UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGate] waiting pawn=%s current_support=(%d,%d,%d) ready=%d " TEXT("[VoxelForgeCollisionGate] waiting pawn=%s current_support=(%d,%d,%d) ready=%d "
"hold_fall=1"), "hold_movement=1"),
*Pawn->GetName(), *Pawn->GetName(),
CurrentSupportTile.Coord.X, CurrentSupportTile.Coord.Y, CurrentSupportTile.Coord.Z, CurrentSupportTile.Coord.X, CurrentSupportTile.Coord.Y, CurrentSupportTile.Coord.Z,
bCurrentSupportReady ? 1 : 0); bCurrentSupportReady ? 1 : 0);
@@ -7483,37 +7723,25 @@ void AVoxelWorld::EngagePawnCollisionGate(APawn* Pawn)
} }
if (bPawnCollisionGateEngaged) if (bPawnCollisionGateEngaged)
{ {
// Re-apply the vertical hold every game-thread sample. Movement input and horizontal // Keep the whole pawn stopped until the support tile has collision. Saving/restoring the
// velocity are intentionally untouched. // pre-gate velocity lets normal movement resume once the tile is ready.
if (UCharacterMovementComponent* CharacterMovement = if (UCharacterMovementComponent* CharacterMovement =
CollisionGatedCharacterMovement.Get()) CollisionGatedCharacterMovement.Get())
{ {
if (CharacterMovement->MovementMode != MOVE_Flying) if (CharacterMovement->MovementMode != MOVE_None)
{ {
CharacterMovement->SetMovementMode(MOVE_Flying); CharacterMovement->SetMovementMode(MOVE_None);
} }
CharacterMovement->GravityScale = 0.0f; CharacterMovement->GravityScale = 0.0f;
FVector Velocity = CharacterMovement->Velocity; CharacterMovement->StopMovementImmediately();
if (Velocity.Z < 0.0f)
{
SavedGateVerticalVelocity = Velocity.Z;
bSavedGateVerticalVelocity = true;
Velocity.Z = 0.0f;
CharacterMovement->Velocity = Velocity;
CharacterMovement->UpdateComponentVelocity();
}
} }
else if (UPawnMovementComponent* PawnMovement = CollisionGatedPawnMovement.Get()) else if (UPawnMovementComponent* PawnMovement = CollisionGatedPawnMovement.Get())
{ {
FVector Velocity = PawnMovement->Velocity; if (bSavedPawnMovementWasActive && PawnMovement->IsActive())
if (Velocity.Z < 0.0f)
{ {
SavedGateVerticalVelocity = Velocity.Z; PawnMovement->Deactivate();
bSavedGateVerticalVelocity = true;
Velocity.Z = 0.0f;
PawnMovement->Velocity = Velocity;
PawnMovement->UpdateComponentVelocity();
} }
PawnMovement->StopMovementImmediately();
} }
return; return;
} }
@@ -7531,16 +7759,11 @@ void AVoxelWorld::EngagePawnCollisionGate(APawn* Pawn)
SavedCharacterCustomMovementMode = CharacterMovement->CustomMovementMode; SavedCharacterCustomMovementMode = CharacterMovement->CustomMovementMode;
bSavedCharacterMovementMode = true; bSavedCharacterMovementMode = true;
FVector Velocity = CharacterMovement->Velocity; SavedGateVelocity = CharacterMovement->Velocity;
if (Velocity.Z < 0.0f) bSavedGateVelocity = true;
{ CharacterMovement->SetMovementMode(MOVE_None);
SavedGateVerticalVelocity = Velocity.Z;
bSavedGateVerticalVelocity = true;
Velocity.Z = 0.0f;
CharacterMovement->Velocity = Velocity;
}
CharacterMovement->SetMovementMode(MOVE_Flying);
CharacterMovement->GravityScale = 0.0f; CharacterMovement->GravityScale = 0.0f;
CharacterMovement->StopMovementImmediately();
CharacterMovement->UpdateComponentVelocity(); CharacterMovement->UpdateComponentVelocity();
bPawnCollisionGateEngaged = true; bPawnCollisionGateEngaged = true;
PawnGateEngagedAtSeconds = FPlatformTime::Seconds(); PawnGateEngagedAtSeconds = FPlatformTime::Seconds();
@@ -7551,15 +7774,15 @@ void AVoxelWorld::EngagePawnCollisionGate(APawn* Pawn)
if (UPawnMovementComponent* PawnMovement = Pawn->GetMovementComponent()) if (UPawnMovementComponent* PawnMovement = Pawn->GetMovementComponent())
{ {
CollisionGatedPawnMovement = PawnMovement; CollisionGatedPawnMovement = PawnMovement;
FVector Velocity = PawnMovement->Velocity; SavedGateVelocity = PawnMovement->Velocity;
if (Velocity.Z < 0.0f) bSavedGateVelocity = true;
bSavedPawnMovementWasActive = PawnMovement->IsActive();
if (bSavedPawnMovementWasActive)
{ {
SavedGateVerticalVelocity = Velocity.Z; PawnMovement->Deactivate();
bSavedGateVerticalVelocity = true;
Velocity.Z = 0.0f;
PawnMovement->Velocity = Velocity;
PawnMovement->UpdateComponentVelocity();
} }
PawnMovement->StopMovementImmediately();
PawnMovement->UpdateComponentVelocity();
bPawnCollisionGateEngaged = true; bPawnCollisionGateEngaged = true;
PawnGateEngagedAtSeconds = FPlatformTime::Seconds(); PawnGateEngagedAtSeconds = FPlatformTime::Seconds();
return; return;
@@ -7584,8 +7807,9 @@ void AVoxelWorld::ReleasePawnCollisionGate()
CollisionGatedCharacterMovement.Reset(); CollisionGatedCharacterMovement.Reset();
bSavedCharacterGravityScale = false; bSavedCharacterGravityScale = false;
bSavedCharacterMovementMode = false; bSavedCharacterMovementMode = false;
bSavedGateVerticalVelocity = false; bSavedGateVelocity = false;
SavedGateVerticalVelocity = 0.0f; SavedGateVelocity = FVector::ZeroVector;
bSavedPawnMovementWasActive = false;
bPawnGateSupportRequestReported = false; bPawnGateSupportRequestReported = false;
PawnGateEngagedAtSeconds = 0.0; PawnGateEngagedAtSeconds = 0.0;
return; return;
@@ -7603,17 +7827,21 @@ void AVoxelWorld::ReleasePawnCollisionGate()
{ {
CharacterMovement->GravityScale = SavedCharacterGravityScale; CharacterMovement->GravityScale = SavedCharacterGravityScale;
} }
if (bSavedGateVerticalVelocity && CharacterMovement->Velocity.Z <= KINDA_SMALL_NUMBER) if (bSavedGateVelocity)
{ {
CharacterMovement->Velocity.Z = SavedGateVerticalVelocity; CharacterMovement->Velocity = SavedGateVelocity;
} }
CharacterMovement->UpdateComponentVelocity(); CharacterMovement->UpdateComponentVelocity();
} }
else if (UPawnMovementComponent* PawnMovement = CollisionGatedPawnMovement.Get()) else if (UPawnMovementComponent* PawnMovement = CollisionGatedPawnMovement.Get())
{ {
if (bSavedGateVerticalVelocity && PawnMovement->Velocity.Z <= KINDA_SMALL_NUMBER) if (bSavedPawnMovementWasActive && !PawnMovement->IsActive())
{ {
PawnMovement->Velocity.Z = SavedGateVerticalVelocity; PawnMovement->Activate();
}
if (bSavedGateVelocity)
{
PawnMovement->Velocity = SavedGateVelocity;
} }
PawnMovement->UpdateComponentVelocity(); PawnMovement->UpdateComponentVelocity();
} }
@@ -7632,8 +7860,9 @@ void AVoxelWorld::ReleasePawnCollisionGate()
CollisionGatedCharacterMovement.Reset(); CollisionGatedCharacterMovement.Reset();
bSavedCharacterGravityScale = false; bSavedCharacterGravityScale = false;
bSavedCharacterMovementMode = false; bSavedCharacterMovementMode = false;
bSavedGateVerticalVelocity = false; bSavedGateVelocity = false;
SavedGateVerticalVelocity = 0.0f; SavedGateVelocity = FVector::ZeroVector;
bSavedPawnMovementWasActive = false;
bPawnCollisionGateEngaged = false; bPawnCollisionGateEngaged = false;
PawnGateEngagedAtSeconds = 0.0; PawnGateEngagedAtSeconds = 0.0;
bPawnGateSupportRequestReported = false; bPawnGateSupportRequestReported = false;
@@ -9171,7 +9400,7 @@ void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& Pla
DesiredEpochStartCycles = FPlatformTime::Cycles64(); DesiredEpochStartCycles = FPlatformTime::Cycles64();
DesiredEpochCenterChunk = Center; DesiredEpochCenterChunk = Center;
const int32 R = Settings ? FMath::Max(1, Settings->ClipRadius) : 3; const int32 R = Settings ? FMath::Max(1, Settings->ClipRadius) : 6;
const int32 MaxLevel = Settings ? FMath::Clamp(Settings->MaxClipLevel, 0, 8) : 4; const int32 MaxLevel = Settings ? FMath::Clamp(Settings->MaxClipLevel, 0, 8) : 4;
// Render-distance tiles expand horizontally. // Render-distance tiles expand horizontally.
@@ -9188,9 +9417,9 @@ void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& Pla
int32 ZLo = MIN_int32, ZHi = MAX_int32; int32 ZLo = MIN_int32, ZHi = MAX_int32;
VoxelClipmapDesiredTiles::ResolveVerticalBounds( VoxelClipmapDesiredTiles::ResolveVerticalBounds(
Center.Z, Center.Z,
Settings ? Settings->ViewDistanceUp : 5, Settings ? Settings->ViewDistanceUp : 10,
Settings ? Settings->ViewDistanceDown : 5, Settings ? Settings->ViewDistanceDown : 10,
Settings ? Settings->StrateViewMarginChunks : 3, Settings ? Settings->StrateViewMarginChunks : 6,
Settings && Settings->bClampViewToStrate, Settings && Settings->bClampViewToStrate,
bHasStrate, StrateTopZ, StrateBottomZ, ZLo, ZHi); bHasStrate, StrateTopZ, StrateBottomZ, ZLo, ZHi);
@@ -9205,7 +9434,7 @@ void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& Pla
} }
const int32 PawnCollisionRadius = FMath::Clamp( const int32 PawnCollisionRadius = FMath::Clamp(
Settings ? Settings->PawnCollisionRadiusChunks : 1, 1, 4); Settings ? Settings->PawnCollisionRadiusChunks : 2, 1, 4);
VoxelCollisionDesiredTiles::AddBox( VoxelCollisionDesiredTiles::AddBox(
CollisionDesiredTiles, Center, PawnCollisionRadius, CollisionDesiredTiles, Center, PawnCollisionRadius,
PawnCollisionRadius, PawnCollisionRadius); PawnCollisionRadius, PawnCollisionRadius);
@@ -9607,7 +9836,7 @@ bool AVoxelWorld::IsTileInClipRange(const FVoxelTileKey& Tile, const FIntVector&
// render-distance level+radius, same as BuildDesiredTiles). A loaded-but-not-desired tile can // render-distance level+radius, same as BuildDesiredTiles). A loaded-but-not-desired tile can
// still cover a desired surface outside this shell, so the caller checks replacement readiness // still cover a desired surface outside this shell, so the caller checks replacement readiness
// before hiding it in either case. // before hiding it in either case.
const int32 R = Settings ? FMath::Max(1, Settings->ClipRadius) : 3; const int32 R = Settings ? FMath::Max(1, Settings->ClipRadius) : 6;
const int32 MaxLevelBase = Settings ? FMath::Clamp(Settings->MaxClipLevel, 0, 8) : 4; const int32 MaxLevelBase = Settings ? FMath::Clamp(Settings->MaxClipLevel, 0, 8) : 4;
int32 MaxLevel = MaxLevelBase, ROuter = R; int32 MaxLevel = MaxLevelBase, ROuter = R;
VF_OuterShell(Settings, R, MaxLevelBase, MaxLevel, ROuter); VF_OuterShell(Settings, R, MaxLevelBase, MaxLevel, ROuter);
@@ -10157,7 +10386,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
const FIntVector CohortCenter = WorldToChunkCoord( const FIntVector CohortCenter = WorldToChunkCoord(
WorldToLocalCm(PlayerPosition)); WorldToLocalCm(PlayerPosition));
const int32 PawnRadius = FMath::Clamp( const int32 PawnRadius = FMath::Clamp(
Settings ? Settings->PawnCollisionRadiusChunks : 1, 1, 4); Settings ? Settings->PawnCollisionRadiusChunks : 2, 1, 4);
VoxelCollisionDesiredTiles::AddBox( VoxelCollisionDesiredTiles::AddBox(
Step1SupportCohortTiles, CohortCenter, PawnRadius, Step1SupportCohortTiles, CohortCenter, PawnRadius,
PawnRadius, PawnRadius); PawnRadius, PawnRadius);
+1 -1
View File
@@ -268,7 +268,7 @@ struct FVoxelOpContext
// travel with the chunk context so the automatically appended edge op cannot be omitted by a // travel with the chunk context so the automatically appended edge op cannot be omitted by a
// builder. Radius 0 is the legacy unbounded/no-op mode. // builder. Radius 0 is the legacy unbounded/no-op mode.
float WorldRadiusVoxels = 8192.0f; float WorldRadiusVoxels = 8192.0f;
float EdgeSealThickness = 64.0f; float EdgeSealThickness = 128.0f;
// Bornes Z de la strate en coords VOXEL (pas cm). // Bornes Z de la strate en coords VOXEL (pas cm).
float StrateTopWorldZ = 0.0f; float StrateTopWorldZ = 0.0f;
+2 -2
View File
@@ -121,14 +121,14 @@ public:
// Authored radius (voxels) of the guaranteed flat-floored landing room at world XY (0,0) in // Authored radius (voxels) of the guaranteed flat-floored landing room at world XY (0,0) in
// every strate — the old “spine” setting now sizes a room and never creates a vertical bore. // every strate — the old “spine” setting now sizes a room and never creates a vertical bore.
// 0 disables the origin landing rooms. Copied from VoxelSettings. // 0 disables the origin landing rooms. Copied from VoxelSettings.
float OriginSpineRadius = 14.0f; float OriginSpineRadius = 28.0f;
// Radius (voxels) of the bounded world in actor-space XY, measured from (0,0). // Radius (voxels) of the bounded world in actor-space XY, measured from (0,0).
// 0 keeps the legacy unbounded behavior and is a true no-op for the edge seal. // 0 keeps the legacy unbounded behavior and is a true no-op for the edge seal.
float WorldRadiusVoxels = 8192.0f; float WorldRadiusVoxels = 8192.0f;
// Width (voxels) of the smooth solid ramp at the XY world edge. // Width (voxels) of the smooth solid ramp at the XY world edge.
float EdgeSealThickness = 64.0f; float EdgeSealThickness = 128.0f;
//========================================================================= //=========================================================================
// SERVICES (injectés par AVoxelWorld::BeginPlay) // SERVICES (injectés par AVoxelWorld::BeginPlay)
@@ -123,9 +123,9 @@ public:
// (mur court) est extrudée vers le solide depuis chaque arête de surface posée sur une des 6 // (mur court) est extrudée vers le solide depuis chaque arête de surface posée sur une des 6
// faces externes de la tuile. Voir GenerateMesh. // faces externes de la tuile. Voir GenerateMesh.
bool bGenerateSkirts = true; bool bGenerateSkirts = true;
// Profondeur de la jupe, en CELLULES de la tuile (× Step × VOXEL_SIZE). ~2 cellules couvrent // Profondeur de la jupe, en CELLULES de la tuile (× Step × VOXEL_SIZE). Four L0 cells preserve
// l'écart vers un voisin un niveau plus grossier (cellule 2×). Monter si des fissures persistent. // the 0.5 m default extrusion at 12.5 cm per voxel.
float SkirtCells = 2.0f; float SkirtCells = 4.0f;
// Conservative 8x8x8 block classifier. Uniform blocks skip the marching-cubes cell loop; // Conservative 8x8x8 block classifier. Uniform blocks skip the marching-cubes cell loop;
// when every relevant block has the same verdict, the density grid is skipped as well. // when every relevant block has the same verdict, the density grid is skipped as well.
+52 -43
View File
@@ -12,39 +12,43 @@
namespace VoxelPassageGeometry namespace VoxelPassageGeometry
{ {
// Authored scale and player body used by the §6.5 landing calculation. // Authored scale and player body used by the §6.5 landing calculation.
constexpr float VoxelSizeMeters = 0.25f; constexpr float VoxelSizeMeters = VOXEL_SIZE / 100.0f;
constexpr float PlayerRadiusVoxels = 34.0f / 25.0f; constexpr float MetersToVoxels(float Meters)
constexpr float PlayerHalfHeightVoxels = 88.0f / 25.0f; {
return Meters / VoxelSizeMeters;
}
constexpr float PlayerRadiusVoxels = MetersToVoxels(0.34f);
constexpr float PlayerHalfHeightVoxels = MetersToVoxels(0.88f);
constexpr float PlayerHeightVoxels = 2.0f * PlayerHalfHeightVoxels; constexpr float PlayerHeightVoxels = 2.0f * PlayerHalfHeightVoxels;
constexpr float HeadroomVoxels = 4.0f; // 1 m constexpr float HeadroomVoxels = MetersToVoxels(1.0f);
constexpr float MinimumTurnFloorWidthVoxels = 12.0f; // 3 m constexpr float MinimumTurnFloorWidthVoxels = MetersToVoxels(3.0f);
constexpr float MinimumLandingHalfWidthVoxels = constexpr float MinimumLandingHalfWidthVoxels =
MinimumTurnFloorWidthVoxels * 0.5f; MinimumTurnFloorWidthVoxels * 0.5f;
constexpr float MinimumRoomHeightVoxels = 12.0f; // 3 m constexpr float MinimumRoomHeightVoxels = MetersToVoxels(3.0f);
constexpr float LandingFloorThicknessVoxels = 3.0f; constexpr float LandingFloorThicknessVoxels = MetersToVoxels(0.75f);
constexpr float MaxStepHeightVoxels = 1.8f; // 45 cm at 25 cm/voxel constexpr float MaxStepHeightVoxels = MetersToVoxels(0.45f);
// The A-to-B walk corridor is budgeted as a full player-width rectangle. Its vertical // The A-to-B walk corridor is budgeted as a full player-width rectangle. Its vertical
// envelope starts above the legal step and extends one player height above that raised floor. // envelope starts above the legal step and extends one player height above that raised floor.
constexpr float WalkCorridorPlayerWidthVoxels = 1.36f; constexpr float WalkCorridorPlayerWidthVoxels = MetersToVoxels(0.68f);
constexpr float WalkCorridorPlayerHeightVoxels = 7.0f; constexpr float WalkCorridorPlayerHeightVoxels = MetersToVoxels(1.76f);
constexpr float WalkCorridorLatticeMarginVoxels = 1.0f; constexpr float WalkCorridorLatticeMarginVoxels = MetersToVoxels(0.25f);
constexpr float SealSafetyMarginVoxels = 4.0f; constexpr float SealSafetyMarginVoxels = MetersToVoxels(1.0f);
constexpr float RoomRoundingVoxels = 1.5f; constexpr float RoomRoundingVoxels = MetersToVoxels(0.375f);
constexpr float LandingCarveBlendVoxels = 4.0f; constexpr float LandingCarveBlendVoxels = MetersToVoxels(1.0f);
// The origin landing is reserved for the same straight spine that the vertical-shaft // The origin landing is reserved for the same straight spine that the vertical-shaft
// generator uses: OriginSpineRadius on each side, with a three-voxel / 0.75 m floor ledge // generator uses: OriginSpineRadius on each side, with a six-voxel / 0.75 m floor ledge
// around it after the support patch's one-voxel inset. At the default radius (14 voxels) this // around it after the support patch's two-voxel inset. At the default radius (28 voxels) this
// is a 28-voxel / 7 m future shaft diameter. // is a 28-voxel / 7 m future shaft diameter.
constexpr float OriginShaftFloorClearanceVoxels = 4.0f; constexpr float OriginShaftFloorClearanceVoxels = MetersToVoxels(1.0f);
// Level tunnel apron outside each room-floor edge. It covers the room/tunnel SDF blend and // Level tunnel apron outside each room-floor edge. It covers the room/tunnel SDF blend and
// keeps the ramp from starting while the route is still over the room's square support patch. // keeps the ramp from starting while the route is still over the room's square support patch.
constexpr float WalkableTunnelLandingApronVoxels = 6.0f; // 1.5 m constexpr float WalkableTunnelLandingApronVoxels = MetersToVoxels(1.5f);
// A level transition after a bend lets the route turn before its sloped segment begins. // A level transition after a bend lets the route turn before its sloped segment begins.
constexpr float WalkableTunnelTurnTransitionVoxels = 6.0f; // 1.5 m constexpr float WalkableTunnelTurnTransitionVoxels = MetersToVoxels(1.5f);
// Keep the tunnel-air post a half voxel above the authored support plane. This is a numerical // Keep the tunnel-air post 12.5 cm above the authored support plane. This is a numerical
// ownership band: the floor writer owns the plane, while the air writer owns the capsule // ownership band: the floor writer owns the plane, while the air writer owns the capsule
// volume above it even when two overlapping passage projections differ by a fraction. // volume above it even when two overlapping passage projections differ by a fraction.
constexpr float WalkableTunnelFloorAirClearanceVoxels = 0.5f; constexpr float WalkableTunnelFloorAirClearanceVoxels = MetersToVoxels(0.125f);
// A 15 degree ramp is comfortably below the 44 degree engine walkability ceiling. The // A 15 degree ramp is comfortably below the 44 degree engine walkability ceiling. The
// latter is a scramble limit, not a sensible default for a tunnel the player walks through. // latter is a scramble limit, not a sensible default for a tunnel the player walks through.
@@ -65,7 +69,7 @@ namespace VoxelPassageGeometry
// 640 voxels, then derive the total cap from the same wave limit. This leaves room above the // 640 voxels, then derive the total cap from the same wave limit. This leaves room above the
// proven canonical route while keeping the 0.1-degree collect window finite and bounded. // proven canonical route while keeping the 0.1-degree collect window finite and bounded.
constexpr int32 TunnelFloorMaximumWindWaves = 6; constexpr int32 TunnelFloorMaximumWindWaves = 6;
constexpr float TunnelFloorMaximumSingleWaveRunVoxels = 640.0f; constexpr float TunnelFloorMaximumSingleWaveRunVoxels = MetersToVoxels(160.0f);
constexpr float TunnelFloorMaximumRouteLengthVoxels = constexpr float TunnelFloorMaximumRouteLengthVoxels =
static_cast<float>(TunnelFloorMaximumWindWaves) static_cast<float>(TunnelFloorMaximumWindWaves)
* TunnelFloorMaximumSingleWaveRunVoxels; * TunnelFloorMaximumSingleWaveRunVoxels;
@@ -107,12 +111,13 @@ namespace VoxelPassageGeometry
FORCEINLINE float NativePassageFloorSupportRadius(float StartRadius, float EndRadius) FORCEINLINE float NativePassageFloorSupportRadius(float StartRadius, float EndRadius)
{ {
return FMath::Max( return FMath::Max(
FMath::Min(FMath::Abs(StartRadius), FMath::Abs(EndRadius)) - 0.5f, FMath::Min(FMath::Abs(StartRadius), FMath::Abs(EndRadius))
- MetersToVoxels(0.125f),
PlayerRadiusVoxels); PlayerRadiusVoxels);
} }
// Keep two switchback floor branches apart far enough that the upper branch's final floor // Keep two switchback floor branches apart far enough that the upper branch's final floor
// post cannot enter the lower player's full-width walk corridor. The one-voxel margin covers // post cannot enter the lower player's full-width walk corridor. The 0.25 m margin covers
// the scalar lattice/interpolation boundary; Eval displacement is already followed by the // the scalar lattice/interpolation boundary; Eval displacement is already followed by the
// MC-facing tunnel and native-floor stages before collision density is returned. // MC-facing tunnel and native-floor stages before collision density is returned.
FORCEINLINE float WalkableTunnelTurnLegSeparation( FORCEINLINE float WalkableTunnelTurnLegSeparation(
@@ -131,7 +136,7 @@ namespace VoxelPassageGeometry
{ {
return FMath::Min( return FMath::Min(
LandingFloorThicknessVoxels, LandingFloorThicknessVoxels,
FMath::Max(0.5f, FMath::Abs(Radius) * 0.5f)); FMath::Max(MetersToVoxels(0.125f), FMath::Abs(Radius) * 0.5f));
} }
FORCEINLINE float VerticalShaftConnectorFloorZ( FORCEINLINE float VerticalShaftConnectorFloorZ(
@@ -212,7 +217,8 @@ namespace VoxelPassageGeometry
{ {
const float SafeRadius = FMath::Abs(Radius); const float SafeRadius = FMath::Abs(Radius);
const float FloorThickness = VerticalShaftConnectorFloorThickness(SafeRadius); const float FloorThickness = VerticalShaftConnectorFloorThickness(SafeRadius);
const float FloorRadius = FMath::Max(0.5f, SafeRadius - 0.5f); const float FloorRadius = FMath::Max(
MetersToVoxels(0.125f), SafeRadius - MetersToVoxels(0.125f));
if (!(SafeRadius > 0.0f) if (!(SafeRadius > 0.0f)
|| !(BaseDensity > 0.0f) || !(BaseDensity > 0.0f)
|| !VoxelMath::IsFinite(WorldX) || !VoxelMath::IsFinite(WorldX)
@@ -277,7 +283,8 @@ namespace VoxelPassageGeometry
const float SafeRadius = FMath::Abs(Radius); const float SafeRadius = FMath::Abs(Radius);
// Leave a one-voxel wall band for the roughness/solid shell, while retaining more than // Leave a one-voxel wall band for the roughness/solid shell, while retaining more than
// enough width for the 1.36-voxel player capsule and its support stencil. // enough width for the 1.36-voxel player capsule and its support stencil.
const float CoreRadius = FMath::Max(0.5f, SafeRadius - 1.0f); const float CoreRadius = FMath::Max(
MetersToVoxels(0.125f), SafeRadius - MetersToVoxels(0.25f));
if (!(SafeRadius > 0.0f) if (!(SafeRadius > 0.0f)
|| !(BaseDensity > 0.0f) || !(BaseDensity > 0.0f)
|| !VoxelMath::IsFinite(WorldX) || !VoxelMath::IsFinite(WorldX)
@@ -442,7 +449,7 @@ namespace VoxelPassageGeometry
EndFloorZ, EndFloorZ,
HorizontalT); HorizontalT);
OutSupportRadius = FMath::Max( OutSupportRadius = FMath::Max(
FMath::Min(StartRadius, EndRadius) - 0.5f, FMath::Min(StartRadius, EndRadius) - MetersToVoxels(0.125f),
PlayerRadiusVoxels); PlayerRadiusVoxels);
if (OutSegmentIndex != nullptr) if (OutSegmentIndex != nullptr)
{ {
@@ -471,16 +478,19 @@ namespace VoxelPassageGeometry
FORCEINLINE float LandingHalfWidthForRadius(float Radius) FORCEINLINE float LandingHalfWidthForRadius(float Radius)
{ {
const float SafeRadius = FMath::Max(FMath::Abs(Radius), 1.0f); const float SafeRadius = FMath::Max(
return FMath::Max(MinimumLandingHalfWidthVoxels, SafeRadius + 2.0f); FMath::Abs(Radius), MetersToVoxels(0.25f));
return FMath::Max(MinimumLandingHalfWidthVoxels,
SafeRadius + MetersToVoxels(0.5f));
} }
FORCEINLINE float LandingHeightForRadius(float Radius) FORCEINLINE float LandingHeightForRadius(float Radius)
{ {
const float SafeRadius = FMath::Max(FMath::Abs(Radius), 1.0f); const float SafeRadius = FMath::Max(
FMath::Abs(Radius), MetersToVoxels(0.25f));
return FMath::Max( return FMath::Max(
FMath::Max(MinimumRoomHeightVoxels, PlayerHeightVoxels + HeadroomVoxels), FMath::Max(MinimumRoomHeightVoxels, PlayerHeightVoxels + HeadroomVoxels),
2.0f * SafeRadius + 2.0f); 2.0f * SafeRadius + MetersToVoxels(0.5f));
} }
/** /**
@@ -488,8 +498,8 @@ namespace VoxelPassageGeometry
* *
* `OriginRadius` is the old spine setting and becomes the room's authored mouth radius. The * `OriginRadius` is the old spine setting and becomes the room's authored mouth radius. The
* room dimensions therefore reuse the §6.5 formula exactly: * room dimensions therefore reuse the §6.5 formula exactly:
* half-width = max(7, radius + 2), * half-width = max(1.5 m, radius + 0.5 m),
* height = max(12, capsule height + 4, 2 * radius + 2). * height = max(3 m, capsule height + 1 m, 2 * radius + 0.5 m).
* The floor is placed against the top of the safe interior interval. This makes the first * The floor is placed against the top of the safe interior interval. This makes the first
* landing the place reached from above, while every lower landing remains sealed above until * landing the place reached from above, while every lower landing remains sealed above until
* progression opens its inter-strate passage. * progression opens its inter-strate passage.
@@ -508,12 +518,11 @@ namespace VoxelPassageGeometry
} }
// Keep a solid walking ledge around the future shaft, not merely a room whose wall is // Keep a solid walking ledge around the future shaft, not merely a room whose wall is
// tangent to it. The shaft diameter is 2 * OriginRadius; the extra four voxels leave a // tangent to it. The shaft diameter is 2 * OriginRadius; the eight-voxel / 1 m extra
// three-voxel / 0.75 m floor margin after the support patch's one-voxel inset at the // leaves six voxels / 0.75 m of floor after the support patch's two-voxel / 0.25 m inset.
// authored 25 cm scale.
Result.HalfWidth = FMath::Max( Result.HalfWidth = FMath::Max(
LandingHalfWidthForRadius(OriginRadius), LandingHalfWidthForRadius(OriginRadius),
FMath::Max(FMath::Abs(OriginRadius), 1.0f) FMath::Max(FMath::Abs(OriginRadius), MetersToVoxels(0.25f))
+ OriginShaftFloorClearanceVoxels); + OriginShaftFloorClearanceVoxels);
const float DesiredHeight = LandingHeightForRadius(OriginRadius); const float DesiredHeight = LandingHeightForRadius(OriginRadius);
const float SafeSeal = FMath::Max(SealThickness, 0.0f); const float SafeSeal = FMath::Max(SealThickness, 0.0f);
@@ -603,7 +612,7 @@ namespace VoxelPassageGeometry
constexpr float ProofPad = 1.0f; constexpr float ProofPad = 1.0f;
const float FloorHalfWidth = FMath::Max( const float FloorHalfWidth = FMath::Max(
Geometry.HalfWidth - 1.0f, 0.0f); Geometry.HalfWidth - MetersToVoxels(0.25f), 0.0f);
return LatticeAxisHasSampleInInterval( return LatticeAxisHasSampleInInterval(
FMath::Max((float)VoxelBox.Min.X, -FloorHalfWidth - ProofPad), FMath::Max((float)VoxelBox.Min.X, -FloorHalfWidth - ProofPad),
FMath::Min((float)VoxelBox.Max.X, FloorHalfWidth + ProofPad), FMath::Min((float)VoxelBox.Max.X, FloorHalfWidth + ProofPad),
@@ -654,7 +663,7 @@ namespace VoxelPassageGeometry
// an exact lattice edge and mirrors the existing passage-floor guard. // an exact lattice edge and mirrors the existing passage-floor guard.
constexpr float ProofPad = 1.0f; constexpr float ProofPad = 1.0f;
const float FloorHalfWidth = FMath::Max( const float FloorHalfWidth = FMath::Max(
Geometry.HalfWidth - 1.0f, 0.0f); Geometry.HalfWidth - MetersToVoxels(0.25f), 0.0f);
return IntersectsClosedInterval( return IntersectsClosedInterval(
VoxelBox.Min.X, VoxelBox.Max.X, VoxelBox.Min.X, VoxelBox.Max.X,
-FloorHalfWidth - ProofPad, FloorHalfWidth + ProofPad) -FloorHalfWidth - ProofPad, FloorHalfWidth + ProofPad)
@@ -681,9 +690,9 @@ namespace VoxelPassageGeometry
const float Rounding = RoomRoundingVoxels; const float Rounding = RoomRoundingVoxels;
const float HalfZ = (Geometry.CeilingZ - Geometry.FloorZ) * 0.5f; const float HalfZ = (Geometry.CeilingZ - Geometry.FloorZ) * 0.5f;
const FVector HalfExtent( const FVector HalfExtent(
FMath::Max(Geometry.HalfWidth - Rounding, 0.25f), FMath::Max(Geometry.HalfWidth - Rounding, MetersToVoxels(0.0625f)),
FMath::Max(Geometry.HalfWidth - Rounding, 0.25f), FMath::Max(Geometry.HalfWidth - Rounding, MetersToVoxels(0.0625f)),
FMath::Max(HalfZ - Rounding, 0.25f)); FMath::Max(HalfZ - Rounding, MetersToVoxels(0.0625f)));
const FVector D( const FVector D(
FMath::Abs(Position.X - Center.X) - HalfExtent.X, FMath::Abs(Position.X - Center.X) - HalfExtent.X,
FMath::Abs(Position.Y - Center.Y) - HalfExtent.Y, FMath::Abs(Position.Y - Center.Y) - HalfExtent.Y,
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "VoxelTypes.h"
class UVoxelGenerator; class UVoxelGenerator;
@@ -17,6 +18,11 @@ class UVoxelGenerator;
*/ */
namespace VoxelForgeRedefinedDensityField namespace VoxelForgeRedefinedDensityField
{ {
/** Proof sample width shared by the research mesher and the region-evaluation harness. */
constexpr int32 ParameterizedBrickSamples = 4;
/** Uniform fill magnitude used after a 16 metre sign proof. */
constexpr float ParameterizedUniformDensity = 16.0f * 100.0f / VOXEL_SIZE;
/** Per-worker, per-brick state. The opaque plan pointer is valid until the layout changes. */ /** Per-worker, per-brick state. The opaque plan pointer is valid until the layout changes. */
struct FBrickEvaluationContext struct FBrickEvaluationContext
{ {
@@ -138,17 +144,26 @@ namespace VoxelForgeRedefinedDensityField
VOXELFORGE_API void EvaluateParameterizedColumnWithDetailValues( VOXELFORGE_API void EvaluateParameterizedColumnWithDetailValues(
const UVoxelGenerator& Generator, float WorldX, float WorldY, float WorldZStart, const UVoxelGenerator& Generator, float WorldX, float WorldY, float WorldZStart,
const float* DetailValues, int32 DetailStride, int32 Count, float* OutValues, const float* DetailValues, int32 DetailStride, int32 Count, float* OutValues,
const FParameterizedBrickContext* Context = nullptr, float DetailScale = 1.0f); const FParameterizedBrickContext* Context = nullptr, float DetailScale = 1.0f,
int32 SampleStep = 1);
VOXELFORGE_API bool TryClassifyParameterizedBrick( VOXELFORGE_API bool TryClassifyParameterizedBrick(
const UVoxelGenerator& Generator, FIntVector OriginVoxels, FIntVector Size, const UVoxelGenerator& Generator, FIntVector OriginVoxels, FIntVector Size,
bool& bOutAir, float DetailScale = 1.0f, bool& bOutAir, float DetailScale = 1.0f,
bool* bOutEvaluatedMacroCentre = nullptr, bool* bOutEvaluatedMacroCentre = nullptr,
EBrickFallbackReason* OutFallbackReason = nullptr); EBrickFallbackReason* OutFallbackReason = nullptr,
int32 SampleStep = 1);
VOXELFORGE_API void BuildParameterizedBrickContext( VOXELFORGE_API void BuildParameterizedBrickContext(
const UVoxelGenerator& Generator, FIntVector OriginVoxels, FIntVector Size, const UVoxelGenerator& Generator, FIntVector OriginVoxels, FIntVector Size,
FParameterizedBrickContext& OutContext); FParameterizedBrickContext& OutContext, int32 SampleStep = 1);
/** Evaluate a mixed brick with one shared room/passage setup for all Z samples. */
VOXELFORGE_API void EvaluateParameterizedDensityBrick(
const UVoxelGenerator& Generator, FIntVector OriginVoxels, FIntVector Size,
float DetailScale, FParameterizedBrickContext& InOutContext,
TArray<float>& InOutDetail, TArray<float>& InOutColumnValues,
TArray<float>& OutValues, int32 SampleStep = 1);
/** Evaluate the new field, including existing passage carving and player diffs. */ /** Evaluate the new field, including existing passage carving and player diffs. */
VOXELFORGE_API float Evaluate(const UVoxelGenerator& Generator, float WorldX, float WorldY, VOXELFORGE_API float Evaluate(const UVoxelGenerator& Generator, float WorldX, float WorldY,
@@ -159,7 +174,7 @@ namespace VoxelForgeRedefinedDensityField
VOXELFORGE_API void BuildDetailBrick(const UVoxelGenerator& Generator, VOXELFORGE_API void BuildDetailBrick(const UVoxelGenerator& Generator,
FIntVector OriginVoxels, FIntVector Size, float DetailScale, FIntVector OriginVoxels, FIntVector Size, float DetailScale,
TArray<float>& OutDetail, FBrickEvaluationContext* OutContext = nullptr, TArray<float>& OutDetail, FBrickEvaluationContext* OutContext = nullptr,
bool bParameterized = false); bool bParameterized = false, int32 SampleStep = 1);
/** Evaluate with a detail value produced by BuildDetailBrick. */ /** Evaluate with a detail value produced by BuildDetailBrick. */
VOXELFORGE_API float EvaluateWithDetailValue(const UVoxelGenerator& Generator, VOXELFORGE_API float EvaluateWithDetailValue(const UVoxelGenerator& Generator,
@@ -87,7 +87,7 @@ struct VOXELFORGE_API FVoxelSeasonCompositionSettings
// height. World layout bounds are written into every selected parameter family below. // height. World layout bounds are written into every selected parameter family below.
int32 StrateHeightInChunks = 8; int32 StrateHeightInChunks = 8;
int32 InterStrateGapChunks = 0; int32 InterStrateGapChunks = 0;
float OriginSpineRadius = 14.0f; float OriginSpineRadius = 28.0f;
float WorldRadiusVoxels = 0.0f; float WorldRadiusVoxels = 0.0f;
// Coarse measurement is deliberately bounded. Tests may reduce the window; production may // Coarse measurement is deliberately bounded. Tests may reduce the window; production may
@@ -117,14 +117,14 @@ struct VOXELFORGE_API FVoxelSeasonCompositionSettings
// The existing Tier 4b test uses step 4. Season composition defaults to step 8 so the // The existing Tier 4b test uses step 4. Season composition defaults to step 8 so the
// default automation artifact is measured in minutes, while MaxCells remains bounded. // default automation artifact is measured in minutes, while MaxCells remains bounded.
MeasureSettings.SampleStep = 8; MeasureSettings.SampleStep = 8;
MeasureSettings.RadiusInVoxels = 256; MeasureSettings.RadiusInVoxels = 512;
MeasureSettings.MaxCells = 8000000; MeasureSettings.MaxCells = 8000000;
MeasureSettings.MaxRouteRetries = 16; MeasureSettings.MaxRouteRetries = 16;
MeasureSettings.HeadroomCells = 2; MeasureSettings.HeadroomCells = 2;
MeasureSettings.InteriorMarginVoxels = -1; MeasureSettings.InteriorMarginVoxels = -1;
PlayerFitMeasureSettings.SampleStep = 1; PlayerFitMeasureSettings.SampleStep = 1;
PlayerFitMeasureSettings.RadiusInVoxels = 64; PlayerFitMeasureSettings.RadiusInVoxels = 128;
// A fitted mouth-to-mouth XY window across an 8-chunk (256-voxel) strate can approach // A fitted mouth-to-mouth XY window across an 8-chunk (256-voxel) strate can approach
// four million cells. Keep the cap finite and below the old multi-gigabyte retry cost. // four million cells. Keep the cap finite and below the old multi-gigabyte retry cost.
PlayerFitMeasureSettings.MaxCells = 4000000; PlayerFitMeasureSettings.MaxCells = 4000000;
+35 -34
View File
@@ -32,15 +32,15 @@ public:
// STREAMING (distance de vue) // STREAMING (distance de vue)
//========================================================================= //=========================================================================
// En CHUNKS (CHUNK_SIZE=32). Couverture linéaire = ViewDistanceXY × 32 × 25 cm. // En CHUNKS (CHUNK_SIZE=32). At 12.5 cm/voxel, each view-distance chunk covers 4 m.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
int32 ViewDistanceXY = 16; int32 ViewDistanceXY = 32;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
int32 ViewDistanceUp = 5; int32 ViewDistanceUp = 10;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
int32 ViewDistanceDown = 5; int32 ViewDistanceDown = 10;
// Nombre max de tâches de génération/mesh en parallèle. // Nombre max de tâches de génération/mesh en parallèle.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
@@ -85,7 +85,7 @@ public:
// this cube remain visible but do not queue Chaos cooks; registered streaming anchors keep their // this cube remain visible but do not queue Chaos cooks; registered streaming anchors keep their
// own authored collision boxes, and the collision gate can extend a landing-support column. // own authored collision boxes, and the collision gate can extend a landing-support column.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "1", ClampMax = "4")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "1", ClampMax = "4"))
int32 PawnCollisionRadiusChunks = 1; int32 PawnCollisionRadiusChunks = 2;
// Strate-aware VERTICAL streaming: don't load chunks into a strate you can't see. // Strate-aware VERTICAL streaming: don't load chunks into a strate you can't see.
// Seals + the inter-strate bedrock gap are light-tight (§8.7), so the strate above/below // Seals + the inter-strate bedrock gap are light-tight (§8.7), so the strate above/below
@@ -100,7 +100,7 @@ public:
// is on: keeps the descent tunnel / immediate transition loaded and acts as the trailing // is on: keeps the descent tunnel / immediate transition loaded and acts as the trailing
// buffer of the strate you're leaving (it sheds naturally as you descend out of view). // buffer of the strate you're leaving (it sheds naturally as you descend out of view).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "0")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "0"))
int32 StrateViewMarginChunks = 3; int32 StrateViewMarginChunks = 6;
// STRATE CONTENT CUT (F17 separation) — a level-L tile is 2^L chunks TALL and can straddle a // STRATE CONTENT CUT (F17 separation) — a level-L tile is 2^L chunks TALL and can straddle a
// strate boundary: at coarse Steps the thin seal/gap solid between two strates' airs falls // strate boundary: at coarse Steps the thin seal/gap solid between two strates' airs falls
@@ -142,11 +142,11 @@ public:
// (more detail / overlap) but more components. 3 → a 7×7×7 box per level (minus the inner // (more detail / overlap) but more components. 3 → a 7×7×7 box per level (minus the inner
// hole filled by the finer level). // hole filled by the finer level).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "1")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "1"))
int32 ClipRadius = 3; int32 ClipRadius = 6;
// Coarsest LOD level. 0 = full-res only (no clipmap). Each level up doubles tile size & // Coarsest LOD level. 0 = full-res only (no clipmap). Each level up doubles tile size &
// reach at ~constant cost — raise this to see much farther for cheap. (Level L step = 2^L.) // reach at ~constant cost — raise this to see much farther for cheap. (Level L step = 2^L.)
// Ship default: level 5, the measured horizon-distance balance at 768 chunks. // Ship default: level 5, the measured horizon-distance balance at 1536 chunks.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "0", ClampMax = "8")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "0", ClampMax = "8"))
int32 MaxClipLevel = 5; int32 MaxClipLevel = 5;
@@ -163,26 +163,26 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "4", ClampMax = "32")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "4", ClampMax = "32"))
int32 CoarseTileCells = 16; int32 CoarseTileCells = 16;
// RENDER DISTANCE — custom horizontal reach, in CHUNKS (1 chunk = 8 m; 128 ≈ 1 km, 768 ≈ 6 km). // RENDER DISTANCE — custom horizontal reach, in CHUNKS (1 chunk = 4 m; 256 ≈ 1 km, 1536 ≈ 6 km).
// When > 0 and farther than the natural clipmap reach (ClipRadius × 2^MaxClipLevel chunks), the // When > 0 and farther than the natural clipmap reach (ClipRadius × 2^MaxClipLevel chunks), the
// OUTERMOST shell keeps generating level-MaxClipLevel tiles outward until it covers this // OUTERMOST shell keeps generating level-MaxClipLevel tiles outward until it covers this
// distance. Pick MaxClipLevel = the coarsest level that still renders strates correctly (one // distance. Pick MaxClipLevel = the coarsest level that still renders strates correctly (one
// cell must fit inside a strate band — level ≥7 blanks via the too-coarse skip) and buy the // cell must fit inside a strate band — level ≥7 blanks via the too-coarse skip) and buy the
// remaining horizon here. Cost: the extra ring is all same-level tiles — tile/draw/gen count // remaining horizon here. Cost: the extra ring is all same-level tiles — tile/draw/gen count
// grows with (distance / 2^MaxClipLevel)², so each MaxClipLevel step down quadruples the ring. // grows with (distance / 2^MaxClipLevel)², so each MaxClipLevel step down quadruples the ring.
// Ship default: 768 chunks (about 6.1 km at 8 m/chunk). 0 = off (natural reach). // Ship default: 1536 chunks (about 6.1 km at 4 m/chunk). 0 = off (natural reach).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "0")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "0"))
int32 RenderDistanceChunks = 768; int32 RenderDistanceChunks = 1536;
// SKIRTS — seal the thin cracks where neighbouring clipmap shells (different resolutions) meet. // SKIRTS — seal the thin cracks where neighbouring clipmap shells (different resolutions) meet.
// A short wall is extruded into the solid from each surface edge on the tile's outer faces. // A short wall is extruded into the solid from each surface edge on the tile's outer faces.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap")
bool bGenerateSkirts = true; bool bGenerateSkirts = true;
// Skirt depth in tile CELLS (× the tile's step). ~2 covers the gap to a one-level-coarser // Skirt depth in tile CELLS (× the tile's step). Four L0 cells preserve the former 0.5 m
// neighbour (2× cell). Raise if cracks still show; lower if skirts peek out on convex edges. // default depth at 12.5 cm per voxel. Raise if cracks still show; lower if skirts peek out.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "0.5", ClampMax = "8.0")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "0.5", ClampMax = "8.0"))
float SkirtCells = 2.0f; float SkirtCells = 4.0f;
// T2.b — LOD-aware octave reduction. Octaves DROPPED from per-voxel volumetric noise // T2.b — LOD-aware octave reduction. Octaves DROPPED from per-voxel volumetric noise
// (roughness, worms displacement, slab/maze/shaft/island detail) per Step doubling on // (roughness, worms displacement, slab/maze/shaft/island detail) per Step doubling on
@@ -204,8 +204,9 @@ public:
// Existing DA_Settings / BP_VoxelSettings assets predate this property, so they do NOT serialize // Existing DA_Settings / BP_VoxelSettings assets predate this property, so they do NOT serialize
// it — they silently inherit whatever C++ default is written here. A non-zero default would // it — they silently inherit whatever C++ default is written here. A non-zero default would
// therefore wall in every existing world the next time the editor opens, with nothing in the // therefore wall in every existing world the next time the editor opens, with nothing in the
// asset to show why. The GDD calls for "bounded, but gigantic": ~8192 voxels (2.05 km) is the // asset to show why. The former 8192-voxel / 2.05 km target must be 16384 voxels at 12.5 cm.
// intended production value. SET IT IN THE ASSET, deliberately, once. // 8192 now means a 1.024 km radius.
// SET IT IN THE ASSET, deliberately, once.
// //
// FR : 0 par défaut EXPRÈS — les assets existants n'ont pas cette propriété et héritent // FR : 0 par défaut EXPRÈS — les assets existants n'ont pas cette propriété et héritent
// silencieusement de la valeur C++. La valeur de production visée (~8192) se règle dans l'asset. // silencieusement de la valeur C++. La valeur de production visée (~8192) se règle dans l'asset.
@@ -214,7 +215,7 @@ public:
// Solid ramp thickness at the radial world edge, in voxels. // Solid ramp thickness at the radial world edge, in voxels.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|World Bounds", meta = (ClampMin = "0.0")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|World Bounds", meta = (ClampMin = "0.0"))
float EdgeSealThickness = 64.0f; float EdgeSealThickness = 128.0f;
//========================================================================= //=========================================================================
// CONTENT — distance-based decoration grid (no LOD pop) // CONTENT — distance-based decoration grid (no LOD pop)
@@ -229,14 +230,14 @@ public:
// (the default — trees, landmarks, rare props) stream out. Bigger = props visible farther + more // (the default — trees, landmarks, rare props) stream out. Bigger = props visible farther + more
// spawn/march cost (but the far grid is COARSE — see DecorationFarSpacingVoxels — so far cost is cheap). // spawn/march cost (but the far grid is COARSE — see DecorationFarSpacingVoxels — so far cost is cheap).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "1")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "1"))
int32 DecorationRadiusChunks = 6; int32 DecorationRadiusChunks = 12;
// NEAR-tier stream radius in cells (= chunks): how far EDecoStreamTier::Near entries (dense groundcover // NEAR-tier stream radius in cells (= chunks): how far EDecoStreamTier::Near entries (dense groundcover
// like grass) stream out. Keep this SHORT — near entries use the FINE grid (DecorationSpacingVoxels), so // like grass) stream out. Keep this SHORT — near entries use the FINE grid (DecorationSpacingVoxels), so
// their cost is the steep one; bounding their radius keeps the far-region HISM build + memory small. // their cost is the steep one; bounding their radius keeps the far-region HISM build + memory small.
// (Repurposes the old vestigial DecorationActorRadiusChunks; same default.) // (Repurposes the old vestigial DecorationActorRadiusChunks; same default.)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "1")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "1"))
int32 DecorationNearRadiusChunks = 3; int32 DecorationNearRadiusChunks = 6;
// Decoration cells are grouped into REGIONS of RxR cells, and ALL placements in a region share ONE // Decoration cells are grouped into REGIONS of RxR cells, and ALL placements in a region share ONE
// HISM per mesh (instead of one HISM per cell per mesh). Regions load/unload as a unit, so clearing // HISM per mesh (instead of one HISM per cell per mesh). Regions load/unload as a unit, so clearing
@@ -245,13 +246,13 @@ public:
// disk vs 169 cells (~10x fewer components). Bigger R = fewer components but coarser pop-in + heavier // disk vs 169 cells (~10x fewer components). Bigger R = fewer components but coarser pop-in + heavier
// one-shot cluster-tree build per region (apply is budgeted, so the build is amortised). MUST be >= 1. // one-shot cluster-tree build per region (apply is budgeted, so the build is amortised). MUST be >= 1.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "1", ClampMax = "16")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "1", ClampMax = "16"))
int32 DecorationRegionSizeCells = 4; int32 DecorationRegionSizeCells = 8;
// NEAR-tier column spacing (in voxels) within a cell — the FINE grid. MUST divide CHUNK_SIZE (32): // NEAR-tier column spacing (in voxels) within a cell — the FINE grid. MUST divide CHUNK_SIZE (32):
// 4 → 8×8=64 columns/cell. Smaller = denser placement potential + more march cost. SpawnDensity rolls // 4 → 8×8=64 columns/cell. Smaller = denser placement potential + more march cost. SpawnDensity rolls
// per column-crossing. Used by EDecoStreamTier::Near entries (and is the legacy single-grid spacing). // per column-crossing. Used by EDecoStreamTier::Near entries (and is the legacy single-grid spacing).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "1", ClampMax = "32")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "1", ClampMax = "32"))
int32 DecorationSpacingVoxels = 4; int32 DecorationSpacingVoxels = 8;
// FAR-tier column spacing (in voxels) within a cell — the COARSE grid. MUST divide CHUNK_SIZE (32): // FAR-tier column spacing (in voxels) within a cell — the COARSE grid. MUST divide CHUNK_SIZE (32):
// 16 → 2×2=4 columns/cell (16× fewer worker ray-marches than a spacing-4 grid). This is the lever that // 16 → 2×2=4 columns/cell (16× fewer worker ray-marches than a spacing-4 grid). This is the lever that
@@ -259,13 +260,13 @@ public:
// low-SpawnDensity landmark across the full radius costs a fraction of the fine grid. DEFAULTS to the // low-SpawnDensity landmark across the full radius costs a fraction of the fine grid. DEFAULTS to the
// fine value (4) so existing worlds are byte-identical until you raise it; bump to 8–16 for cheap far props. // fine value (4) so existing worlds are byte-identical until you raise it; bump to 8–16 for cheap far props.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "1", ClampMax = "32")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "1", ClampMax = "32"))
int32 DecorationFarSpacingVoxels = 4; int32 DecorationFarSpacingVoxels = 8;
// COARSE vertical march step (in voxels) when searching a column for surface crossings. The crossing // COARSE vertical march step (in voxels) when searching a column for surface crossings. The crossing
// Z is then bisection-refined, so accuracy is independent of this — raise it (4-8) to cut the scan // Z is then bisection-refined, so accuracy is independent of this — raise it (4-8) to cut the scan
// cost (the column is ray-marched on a WORKER thread, but a smaller step still means more samples). // cost (the column is ray-marched on a WORKER thread, but a smaller step still means more samples).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "1", ClampMax = "8")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "1", ClampMax = "8"))
int32 DecorationMarchStepVoxels = 2; int32 DecorationMarchStepVoxels = 4;
// Max surface crossings placed per column (caps cave columns that pierce many floors/ceilings; // Max surface crossings placed per column (caps cave columns that pierce many floors/ceilings;
// surface worlds have 1). Bounds worst-case march/spawn cost. // surface worlds have 1). Bounds worst-case march/spawn cost.
@@ -277,7 +278,7 @@ public:
// The top cap/seal and the air above the ground are always marched first (this only trims the dead // The top cap/seal and the air above the ground are always marched first (this only trims the dead
// rock underneath), so surface worlds still get their ground and caves still find layered floors. // rock underneath), so surface worlds still get their ground and caves still find layered floors.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "8")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Content", meta = (ClampMin = "8"))
int32 DecorationColumnDepthVoxels = 160; int32 DecorationColumnDepthVoxels = 320;
// Budget: how many completed decoration REGIONS to APPLY (game-thread HISM build + SpawnActor) per // Budget: how many completed decoration REGIONS to APPLY (game-thread HISM build + SpawnActor) per
// frame. A region is applied once ALL its cells have finished marching, in one batched AddInstances // frame. A region is applied once ALL its cells have finished marching, in one batched AddInstances
@@ -307,13 +308,13 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting")
bool bEnableDensityVolume = false; bool bEnableDensityVolume = false;
// Per-axis resolution of EACH clip level (cells). Memory per level ≈ Res³ bytes (R8). 128 → // Per-axis resolution of EACH clip level (cells). Memory per level ≈ Res³ bytes (R8).
// ~2 MB/level; 192 → ~7 MB; 256 → ~16 MB. Higher = crisper near shadows + bigger startup fill. // 256 → ~16 MiB/level; 512 → ~128 MiB/level. Doubling Res preserves metre reach at 12.5 cm.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting", meta = (ClampMin = "32", ClampMax = "256")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting", meta = (ClampMin = "32", ClampMax = "512"))
int32 DensityVolumeResolution = 128; int32 DensityVolumeResolution = 256;
// Number of concentric clip levels. Level L samples every (1<<L) voxels and covers // Number of concentric clip levels. Level L samples every (1<<L) voxels and covers
// Res·(1<<L) voxels. 3 levels at Res=128 → near 32 m (full-res) out to ~128 m (coarse). // Res·(1<<L) voxels. At 12.5 cm, Res=512 → near 64 m (full-res) out to ~256 m (coarse).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting", meta = (ClampMin = "1", ClampMax = "5")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting", meta = (ClampMin = "1", ClampMax = "5"))
int32 DensityVolumeLevels = 3; int32 DensityVolumeLevels = 3;
@@ -341,7 +342,7 @@ public:
bool bDebugDrawDensityVolume = false; bool bDebugDrawDensityVolume = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting|Debug", meta = (ClampMin = "1", ClampMax = "32")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting|Debug", meta = (ClampMin = "1", ClampMax = "32"))
int32 DensityVolumeDebugRadiusCells = 6; int32 DensityVolumeDebugRadiusCells = 12;
//========================================================================= //=========================================================================
// RENDERING // RENDERING
@@ -396,12 +397,12 @@ public:
// Authored radius (voxels) of the guaranteed (0,0) landing room in EVERY strate, regardless // Authored radius (voxels) of the guaranteed (0,0) landing room in EVERY strate, regardless
// of archetype. The §6.5 body-sized formulas turn it into a flat-floored room reserved for a // of archetype. The §6.5 body-sized formulas turn it into a flat-floored room reserved for a
// future straight shaft of diameter 2 * this radius. At the default 14-voxel radius that is // future straight shaft of diameter 2 * this radius. At the default 28-voxel radius that is
// 28 voxels / 7 m, with a three-voxel / 0.75 m floor ledge around it. This setting does not bore the // 56 voxels / 7 m, with a six-voxel / 0.75 m floor ledge around it. This setting does not bore the
// shaft today: lower seals remain solid until a progression passage is opened. // shaft today: lower seals remain solid until a progression passage is opened.
// 10-14 → compact landing room (default) · 20+ → broad landing room // 10-14 → compact landing room (default) · 20+ → broad landing room
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Spine", meta = (ClampMin = "0.0")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Spine", meta = (ClampMin = "0.0"))
float OriginSpineRadius = 14.0f; float OriginSpineRadius = 28.0f;
// Auto-open the very top seal at (0,0) so the world starts with a hole to the // Auto-open the very top seal at (0,0) so the world starts with a hole to the
// surface (the entry shaft). Lower boundaries are NOT auto-opened — the player // surface (the entry shaft). Lower boundaries are NOT auto-opened — the player
@@ -426,7 +427,7 @@ public:
// Rayon max d'un brush (en voxels). Empêche les édits géants. // Rayon max d'un brush (en voxels). Empêche les édits géants.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Carving", UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Carving",
meta = (ClampMin = "1.0")) meta = (ClampMin = "1.0"))
float MaxBrushRadius = 15.0f; float MaxBrushRadius = 30.0f;
// Volume total maximum (somme des volumes de brush). 0 = illimité. // Volume total maximum (somme des volumes de brush). 0 = illimité.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Carving", UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Carving",
@@ -68,11 +68,11 @@ public:
// How many chunks tall this strate is. // How many chunks tall this strate is.
// Each strate can have a different height! // Each strate can have a different height!
// Human-scale defaults use 8 chunks = 64 m. This gives a 20 m+ vertical budget for // 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. // cathedral rooms, tall shafts, and the surface sky cap without changing the lateral gate.
// 1 chunk = 32 voxels x 25cm = 8 m, so the maximum 256 chunks is ~= 2 km of vertical strate. // 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")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Dimensions", meta = (ClampMin = "1", ClampMax = "256"))
int32 StrateHeightInChunks = 8; int32 StrateHeightInChunks = 16;
//========================================================================= //=========================================================================
// BOUNDARY TRANSITION // BOUNDARY TRANSITION
@@ -519,7 +519,7 @@ protected:
// Authored radius (voxels) of the origin landing room and its optional surface opening. // Authored radius (voxels) of the origin landing room and its optional surface opening.
// Copied from VoxelSettings::OriginSpineRadius. // Copied from VoxelSettings::OriginSpineRadius.
float OriginSpineRadius = 14.0f; float OriginSpineRadius = 28.0f;
// Solid-bedrock gap between consecutive strates, in chunks. Copied from // Solid-bedrock gap between consecutive strates, in chunks. Copied from
// VoxelSettings::InterStrateGapChunks during Initialize(). // VoxelSettings::InterStrateGapChunks during Initialize().
+11 -10
View File
@@ -11,15 +11,16 @@ class UVoxelStrateManager;
/** /**
* The pinned metric capsule dimensions in voxel-space. * The pinned metric capsule dimensions in voxel-space.
* *
* 25 cm/voxel is the authored world scale: radius = 34 cm / 25 = 1.36 voxels and * Capsule measurements are authored in centimetres and converted through VOXEL_SIZE. At
* half-height = 88 cm / 25 = 3.52 voxels, hence a 176 cm / 25 = 7.04 voxel total height. * 12.5 cm/voxel the radius is 2.72 voxels, half-height is 7.04 voxels, and total height is
* 14.08 voxels.
* The measure settings copy these values so a future character can change the query without * The measure settings copy these values so a future character can change the query without
* changing generation. The diagnosis test separately reads movement limits from the project's * changing generation. The diagnosis test separately reads movement limits from the project's
* character CDO. * character CDO.
*/ */
struct VOXELFORGE_API FVoxelPlayerCapsuleConstants struct VOXELFORGE_API FVoxelPlayerCapsuleConstants
{ {
static constexpr float VoxelSizeCentimeters = 25.0f; static constexpr float VoxelSizeCentimeters = VOXEL_SIZE;
static constexpr float VoxelSizeMeters = VoxelPassageGeometry::VoxelSizeMeters; static constexpr float VoxelSizeMeters = VoxelPassageGeometry::VoxelSizeMeters;
static constexpr float RadiusCentimeters = 34.0f; static constexpr float RadiusCentimeters = 34.0f;
static constexpr float HalfHeightCentimeters = 88.0f; static constexpr float HalfHeightCentimeters = 88.0f;
@@ -30,8 +31,8 @@ struct VOXELFORGE_API FVoxelPlayerCapsuleConstants
// UE 5.7's WalkableFloorZ=0.71 resolves to about 44.8 degrees; a character CDO may expose // UE 5.7's WalkableFloorZ=0.71 resolves to about 44.8 degrees; a character CDO may expose
// the precise angle. The diagnosis test copies that precise project value when available. // the precise angle. The diagnosis test copies that precise project value when available.
static constexpr float WalkableFloorAngleDegrees = 44.8f; static constexpr float WalkableFloorAngleDegrees = 44.8f;
static constexpr float RadiusVoxels = VoxelPassageGeometry::PlayerRadiusVoxels; static constexpr float RadiusVoxels = RadiusCentimeters / VoxelSizeCentimeters;
static constexpr float HalfHeightVoxels = VoxelPassageGeometry::PlayerHalfHeightVoxels; static constexpr float HalfHeightVoxels = HalfHeightCentimeters / VoxelSizeCentimeters;
static constexpr float HeightVoxels = VoxelPassageGeometry::PlayerHeightVoxels; static constexpr float HeightVoxels = VoxelPassageGeometry::PlayerHeightVoxels;
}; };
@@ -65,15 +66,15 @@ enum class EVoxelConnectivityResult : uint8
/** Settings for the deterministic coarse grid used by the strate measurement pass. */ /** Settings for the deterministic coarse grid used by the strate measurement pass. */
struct VOXELFORGE_API FVoxelStrateMeasureSettings struct VOXELFORGE_API FVoxelStrateMeasureSettings
{ {
int32 SampleStep = 4; // Voxels between samples. int32 SampleStep = 8; // Voxels between 1 m samples at 12.5 cm/voxel.
int32 RadiusInVoxels = 256; // Half-extent in X and Y around CenterXY. int32 RadiusInVoxels = 512; // Half-extent in X and Y around CenterXY (64 m).
FVector2D CenterXY = FVector2D::ZeroVector; // Actor-space voxel coordinates. FVector2D CenterXY = FVector2D::ZeroVector; // Actor-space voxel coordinates.
// When both points are set, replace the square centered on CenterXY with the XY AABB of the // When both points are set, replace the square centered on CenterXY with the XY AABB of the
// two points expanded by CoverMarginVoxels. This keeps a mouth-to-mouth measurement sized by // two points expanded by CoverMarginVoxels. This keeps a mouth-to-mouth measurement sized by
// the mouths' separation instead of by their distance from the world origin. // the mouths' separation instead of by their distance from the world origin.
TOptional<FVector2D> CoverPointA; TOptional<FVector2D> CoverPointA;
TOptional<FVector2D> CoverPointB; TOptional<FVector2D> CoverPointB;
float CoverMarginVoxels = 48.0f; float CoverMarginVoxels = 96.0f; // 12 m.
// Origin-rooted topologies can route from one mouth to the other through the finite (0,0) // Origin-rooted topologies can route from one mouth to the other through the finite (0,0)
// landing room and its local room/join geometry. There is no hidden radial connector network. // landing room and its local room/join geometry. There is no hidden radial connector network.
// When true, include (0,0) in the fitted AABB before applying MaxCells. A caller must keep // When true, include (0,0) in the fitted AABB before applying MaxCells. A caller must keep
@@ -89,8 +90,8 @@ struct VOXELFORGE_API FVoxelStrateMeasureSettings
int32 HeadroomCells = 2; // Legacy walkable-column metric only. int32 HeadroomCells = 2; // Legacy walkable-column metric only.
int32 InteriorMarginVoxels = -1; // <0 = derive from BoundarySealThickness (2x, clamped); explicit 0 includes the seal. int32 InteriorMarginVoxels = -1; // <0 = derive from BoundarySealThickness (2x, clamped); explicit 0 includes the seal.
// Base ACharacter defaults at 25 cm/voxel: 34 cm radius / 25 = 1.36 voxels and // Base ACharacter dimensions are authored in centimetres and converted by the voxel scale.
// 88 cm half-height / 25 = 3.52 voxels (7.04 voxels total). These are parameters, not // These are parameters, not
// generation values, so another playable capsule can be measured without a code change. // generation values, so another playable capsule can be measured without a code change.
float PlayerCapsuleRadiusVoxels = FVoxelPlayerCapsuleConstants::RadiusVoxels; float PlayerCapsuleRadiusVoxels = FVoxelPlayerCapsuleConstants::RadiusVoxels;
float PlayerCapsuleHalfHeightVoxels = FVoxelPlayerCapsuleConstants::HalfHeightVoxels; float PlayerCapsuleHalfHeightVoxels = FVoxelPlayerCapsuleConstants::HalfHeightVoxels;
+12 -11
View File
@@ -491,7 +491,7 @@ struct VOXELFORGE_API FStrateGenerationParams
// Smallest possible room radius (in voxels). // Smallest possible room radius (in voxels).
// Small rooms feel like alcoves or nooks. // Small rooms feel like alcoves or nooks.
// 8-12 → alcoves and small rooms // 8-12 → alcoves and small rooms
// 16 → 8 m diameter room (human-scale minimum) // 16 → 4 m diameter room at 12.5 cm/voxel (unchanged assets are half-scale)
// 24+ → spacious small rooms // 24+ → spacious small rooms
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Rooms") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Rooms")
float MinRoomRadius = 16.0f; float MinRoomRadius = 16.0f;
@@ -1051,7 +1051,7 @@ struct VOXELFORGE_API FStrateGenerationParams
// ----- Dome (hemispherical chamber ceilings) ----- // ----- Dome (hemispherical chamber ceilings) -----
// Domes: hemispherical chamber ceilings for cathedral-like feel. The default 16-32 voxel // Domes: hemispherical chamber ceilings for cathedral-like feel. The default 16-32 voxel
// radius is a 4-8 m optional ceiling feature; the surrounding room remains the main volume. // radius is a 2-4 m optional ceiling feature at 12.5 cm/voxel; the surrounding room is primary.
float DomeDensity = 0.0f; float DomeDensity = 0.0f;
float DomeMinRadius = 16.0f; float DomeMinRadius = 16.0f;
float DomeMaxRadius = 32.0f; float DomeMaxRadius = 32.0f;
@@ -1372,8 +1372,8 @@ struct VOXELFORGE_API FMazeGenerationParams
float CellSize = 64.0f; float CellSize = 64.0f;
// Corridor tube radius in voxels. Keep below CellSize/2 to leave a real lattice wall. // Corridor tube radius in voxels. Keep below CellSize/2 to leave a real lattice wall.
// 8.6 → 3 m nominal usable floor at 25 cm/voxel; 12.5 → the same after the // These voxel-valued asset parameters retain their authored counts. At 12.5 cm/voxel
// proven 3.75-voxel roughness envelope (default, 6.25 m nominal bore). // an unchanged asset is half-scale in metres; double CorridorRadius to retain its old bore.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Maze", meta = (ClampMin = "1.0")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Maze", meta = (ClampMin = "1.0"))
float CorridorRadius = 12.5f; float CorridorRadius = 12.5f;
@@ -1691,7 +1691,7 @@ struct VOXELFORGE_API FVerticalShaftParams
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Shafts", meta = (ClampMin = "1.0")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Shafts", meta = (ClampMin = "1.0"))
float ConnectorRadius = 6.0f; float ConnectorRadius = 6.0f;
// Vertical spacing of ledges inside shafts (voxels). 32 = 8 m landing interval; // Vertical spacing of ledges inside shafts (voxels). 32 = 4 m landing interval;
// 0 = no ledges (sheer drops). // 0 = no ledges (sheer drops).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Shafts", meta = (ClampMin = "0.0")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Shafts", meta = (ClampMin = "0.0"))
float LedgeSpacing = 32.0f; float LedgeSpacing = 32.0f;
@@ -1732,7 +1732,7 @@ struct VOXELFORGE_API FFloatingIslandParams
{ {
GENERATED_BODY() GENERATED_BODY()
// Hash-grid cell size for island placement (voxels). 112 = 28 m, while the largest default // Hash-grid cell size for island placement (voxels). 112 = 14 m, while the largest default
// island is 24 m across, leaving a 2 m centre-to-edge gap. // island is 24 m across, leaving a 2 m centre-to-edge gap.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Islands", meta = (ClampMin = "20.0")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Islands", meta = (ClampMin = "20.0"))
float IslandSpacing = 112.0f; float IslandSpacing = 112.0f;
@@ -1803,7 +1803,7 @@ struct VOXELFORGE_API FStrateDisturbanceParams
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Disturbance|Chasms", meta = (ClampMin = "0.0", ClampMax = "1.0")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Disturbance|Chasms", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float ChasmDensity = 0.0f; float ChasmDensity = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Disturbance|Chasms", meta = (ClampMin = "20.0")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Disturbance|Chasms", meta = (ClampMin = "20.0"))
// 192 voxels = 48 m between optional chasm sites. // 192 voxels = 24 m between optional chasm sites at 12.5 cm/voxel.
float ChasmSpacing = 192.0f; float ChasmSpacing = 192.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Disturbance|Chasms", meta = (ClampMin = "1.0")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Disturbance|Chasms", meta = (ClampMin = "1.0"))
float ChasmRadius = 24.0f; float ChasmRadius = 24.0f;
@@ -1880,9 +1880,10 @@ struct VOXELFORGE_API FStratePassageConfig
// ----- WIDTH (tapers along the length) ----- // ----- WIDTH (tapers along the length) -----
// Radius at the two mouths (entry/exit) and at the middle. Equal = uniform tube; // Radius at the two mouths (entry/exit) and at the middle. Equal = uniform tube;
// The walkable default is 1.5x the original comfortable profile: 4.5 m diameter at mouths // The walkable default is 1.5x the original comfortable profile: 2.25 m diameter at mouths
// and 3.75 m at the waist (9/7.5 voxel radii at 25 cm per voxel). The §6.5 landing room // and 1.875 m at the waist when the stored radii remain 9/7.5 voxels at 12.5 cm per voxel.
// remains wider for turning. // 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;
@@ -2385,7 +2386,7 @@ struct VOXELFORGE_API FStrateLandmark
float OrbRadiusVoxels = 16.0f; float OrbRadiusVoxels = 16.0f;
// Distance in VOXELS over which the orb's light falls to zero. YOU author this (no inverse-square // Distance in VOXELS over which the orb's light falls to zero. YOU author this (no inverse-square
// blowout) — bigger = lights a wider area. (1 voxel = 25 cm.) // blowout) — bigger = lights a wider area. (1 voxel = 12.5 cm.)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Light Orb", meta = (EditCondition = "bIsLightOrb", ClampMin = "1.0")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Light Orb", meta = (EditCondition = "bIsLightOrb", ClampMin = "1.0"))
float OrbFalloffVoxels = 2000.0f; float OrbFalloffVoxels = 2000.0f;
+2 -2
View File
@@ -201,7 +201,7 @@ namespace VoxelMath
// //
// Taille de chunk: 32^3 = 32 768 voxels. // Taille de chunk: 32^3 = 32 768 voxels.
// Pourquoi 32 ? Puissance de 2 → astuces bit à bit + bon alignement GPU. // Pourquoi 32 ? Puissance de 2 → astuces bit à bit + bon alignement GPU.
// VOXEL_SIZE = 25 cm/voxel (Unreal travaille en centimètres). 1 chunk = 8 m. // VOXEL_SIZE = 12.5 cm/voxel (Unreal travaille en centimètres). 1 chunk = 4 m.
// //
// NOTE: 64³ coupe les draw calls (8× moins de chunks) mais rend le streaming trop // NOTE: 64³ coupe les draw calls (8× moins de chunks) mais rend le streaming trop
// saccadé (briques 8× plus lourdes → applies + spawns de contenu en gros à-coups sur // saccadé (briques 8× plus lourdes → applies + spawns de contenu en gros à-coups sur
@@ -210,7 +210,7 @@ namespace VoxelMath
constexpr int32 CHUNK_SIZE = 32; constexpr int32 CHUNK_SIZE = 32;
constexpr float VOXEL_SIZE = 25.0f; constexpr float VOXEL_SIZE = 12.5f;
//============================================================================= //=============================================================================
// TRIVIAL-TILE CLASSIFICATION (T1.d) // TRIVIAL-TILE CLASSIFICATION (T1.d)
+47 -24
View File
@@ -388,22 +388,22 @@ public:
* Applies a spherical brush that subtracts density. * Applies a spherical brush that subtracts density.
* *
* @param Position - World-space center of the carve brush * @param Position - World-space center of the carve brush
* @param Radius - Brush radius in voxels (default 3) * @param Radius - Brush radius in voxels (default 6 voxels / 0.75 m at 12.5 cm per voxel)
* @param Strength - How aggressively to carve (default 10, higher = deeper) * @param Strength - How aggressively to carve (default 10, higher = deeper)
*/ */
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification") UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
void CarveAtPosition(FVector Position, float Radius = 3.0f, float Strength = 10.0f); void CarveAtPosition(FVector Position, float Radius = 6.0f, float Strength = 10.0f);
/** /**
* Fill terrain at a world position (add rock, seal holes). * Fill terrain at a world position (add rock, seal holes).
* Applies a spherical brush that adds density. * Applies a spherical brush that adds density.
* *
* @param Position - World-space center of the fill brush * @param Position - World-space center of the fill brush
* @param Radius - Brush radius in voxels (default 3) * @param Radius - Brush radius in voxels (default 6 voxels / 0.75 m at 12.5 cm per voxel)
* @param Strength - How aggressively to fill (default 10, higher = more solid) * @param Strength - How aggressively to fill (default 10, higher = more solid)
*/ */
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification") UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
void FillAtPosition(FVector Position, float Radius = 3.0f, float Strength = 10.0f); void FillAtPosition(FVector Position, float Radius = 6.0f, float Strength = 10.0f);
/** /**
* Box brush carve/fill. Position is world-space (Unreal units); ExtentVoxels is the * Box brush carve/fill. Position is world-space (Unreal units); ExtentVoxels is the
@@ -420,10 +420,10 @@ public:
* tube radius in voxels. Great for boring tunnels or laying solid pillars/walls. * tube radius in voxels. Great for boring tunnels or laying solid pillars/walls.
*/ */
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification") UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
void CarveCapsule(FVector WorldA, FVector WorldB, float RadiusVoxels = 3.0f, float Strength = 12.0f); void CarveCapsule(FVector WorldA, FVector WorldB, float RadiusVoxels = 6.0f, float Strength = 12.0f);
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification") UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
void FillCapsule(FVector WorldA, FVector WorldB, float RadiusVoxels = 3.0f, float Strength = 12.0f); void FillCapsule(FVector WorldA, FVector WorldB, float RadiusVoxels = 6.0f, float Strength = 12.0f);
/** /**
* Apply a fully-specified modification (any shape). Centers/endpoints are expected * Apply a fully-specified modification (any shape). Centers/endpoints are expected
@@ -581,6 +581,12 @@ public:
private: private:
friend class FScopedGenerationPause; friend class FScopedGenerationPause;
#if WITH_EDITOR
/** Last editor research-field selection applied to loaded tiles. */
int32 AppliedResearchFieldEnabled = INDEX_NONE;
int32 AppliedResearchFieldProfile = INDEX_NONE;
#endif
/** Get/create the shared MID wrapping a base terrain material (binds volume textures + shadow params). /** Get/create the shared MID wrapping a base terrain material (binds volume textures + shadow params).
* Returns Base unchanged-wrapped, or nullptr if Base is null. */ * Returns Base unchanged-wrapped, or nullptr if Base is null. */
UMaterialInstanceDynamic* GetOrCreateTerrainMID(UMaterialInterface* Base); UMaterialInstanceDynamic* GetOrCreateTerrainMID(UMaterialInterface* Base);
@@ -713,7 +719,7 @@ public:
/** Brush radius in voxels for the editor buttons. */ /** Brush radius in voxels for the editor buttons. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Editor Brush", meta = (ClampMin = "0.5")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Editor Brush", meta = (ClampMin = "0.5"))
float EditorBrushRadius = 6.0f; float EditorBrushRadius = 1.5f * 100.0f / VOXEL_SIZE;
/** Brush strength for the editor buttons (sign forced by the button). */ /** Brush strength for the editor buttons (sign forced by the button). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Editor Brush", meta = (ClampMin = "0.1")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Editor Brush", meta = (ClampMin = "0.1"))
@@ -934,10 +940,9 @@ public:
FVoxelTileKey* OutPendingTile = nullptr) const; FVoxelTileKey* OutPendingTile = nullptr) const;
void RequestCollisionGateSupportTile(const FVoxelTileKey& SupportTile); void RequestCollisionGateSupportTile(const FVoxelTileKey& SupportTile);
/** Hold vertical character motion in gravity-free MOVE_Flying while a swept pending support tile /** Hold pawn movement while a swept pending support tile intersects the capsule. Restore the
* intersects the capsule, preserving horizontal movement until collision is ready. Restore the * previous mode, gravity, and velocity on release. The world tick is a prerequisite of the pawn
* previous mode, gravity, and downward speed on release. The world tick is a prerequisite of the * and movement component so the gate check runs before movement physics. */
* pawn and movement component so the gate check runs before movement physics. */
void UpdatePawnCollisionGate(APawn* Pawn, const FVector& PlayerPosition, float DeltaTime); void UpdatePawnCollisionGate(APawn* Pawn, const FVector& PlayerPosition, float DeltaTime);
void EngagePawnCollisionGate(APawn* Pawn); void EngagePawnCollisionGate(APawn* Pawn);
void ReleasePawnCollisionGate(); void ReleasePawnCollisionGate();
@@ -1165,15 +1170,31 @@ public:
// the camera and smaller test radius are held in memory and never saved to a package. // the camera and smaller test radius are held in memory and never saved to a package.
bool bTestCeilingViewEnabled = false; bool bTestCeilingViewEnabled = false;
bool bTestCeilingCaptureRequested = false; bool bTestCeilingCaptureRequested = false;
bool bTestCeilingSecondCaptureRequested = false;
TWeakObjectPtr<ACameraActor> TestCeilingCamera; TWeakObjectPtr<ACameraActor> TestCeilingCamera;
FVector TestCeilingCameraWorldPosition = FVector::ZeroVector; FVector TestCeilingCameraWorldPosition = FVector::ZeroVector;
FRotator TestCeilingCameraRotation = FRotator::ZeroRotator; FRotator TestCeilingCameraRotation = FRotator::ZeroRotator;
double TestCeilingViewBeginSeconds = 0.0; double TestCeilingViewBeginSeconds = 0.0;
float TestCeilingCaptureDelaySeconds = 18.0f; float TestCeilingCaptureDelaySeconds = 18.0f;
float TestCeilingSecondCaptureDelaySeconds = -1.0f;
float TestCeilingCameraMoveSpeedCmPerSecond = 0.0f;
float TestCeilingCameraMoveStartSeconds = 0.0f;
float TestCeilingCameraMoveDurationSeconds = 0.0f;
FVector TestCeilingCameraMoveDirection = FVector::XAxisVector;
double TestCeilingCameraLastMoveSeconds = 0.0;
bool bResearchGamePerfEnabled = false;
double ResearchGamePerfLastFrameSeconds = 0.0;
double ResearchGamePerfMovingSeconds = 0.0;
TArray<double> ResearchGamePerfMovingFrameTimesMs;
bool bTestResearchProfileSwitchConfigured = false;
bool bTestResearchProfileSwitchApplied = false;
float TestResearchProfileSwitchAfterSeconds = -1.0f;
int32 TestResearchProfileSwitchTo = -1;
double TestResearchProfileSwitchBeginSeconds = 0.0;
// Explicit high-speed collision-gate repro. The streamer stays centred at the launch tile while // Explicit high-speed collision-gate repro. The streamer stays centred at the launch tile while
// the character uses ordinary swept movement across the still-pending desired set. The test // the character uses ordinary swept movement toward pending support tiles. The test checks
// distinguishes a gate hold from a full horizontal stop so a fall-only gate can be validated. // that an unresolved collision holds the pawn in place without letting it enter the tile.
bool bHeadlessCollisionGateStressTest = false; bool bHeadlessCollisionGateStressTest = false;
bool bHeadlessCollisionGateStressTestStartPlaced = false; bool bHeadlessCollisionGateStressTestStartPlaced = false;
bool bHeadlessCollisionGateStressTestStarted = false; bool bHeadlessCollisionGateStressTestStarted = false;
@@ -1181,7 +1202,8 @@ public:
bool bHeadlessCollisionGateStressTestFailed = false; bool bHeadlessCollisionGateStressTestFailed = false;
bool bHeadlessCollisionGateStressTestExitRequested = false; bool bHeadlessCollisionGateStressTestExitRequested = false;
bool bHeadlessCollisionGateStressTestGateWasEngaged = false; bool bHeadlessCollisionGateStressTestGateWasEngaged = false;
bool bHeadlessCollisionGateStressTestHorizontalGateProbeApplied = false; bool bHeadlessCollisionGateStressTestFullStopProbeApplied = false;
bool bHeadlessCollisionGateStressTestFullStopProbeObserved = false;
bool bHeadlessCollisionGateStressTestFullStop = false; bool bHeadlessCollisionGateStressTestFullStop = false;
bool bHeadlessCollisionGateStressTestStartHeightCrossed = false; bool bHeadlessCollisionGateStressTestStartHeightCrossed = false;
bool bHeadlessCollisionGateStressTestTerrainViolation = false; bool bHeadlessCollisionGateStressTestTerrainViolation = false;
@@ -1194,7 +1216,7 @@ public:
bool bHeadlessCollisionGateStressDeepDropPrewarmReported = false; bool bHeadlessCollisionGateStressDeepDropPrewarmReported = false;
bool bHeadlessCollisionGateStressDeepDropSurfaceCached = false; bool bHeadlessCollisionGateStressDeepDropSurfaceCached = false;
float HeadlessCollisionGateStressTestSpeedCmPerSecond = 6000.0f; float HeadlessCollisionGateStressTestSpeedCmPerSecond = 6000.0f;
float HeadlessCollisionGateStressTestSpawnHeightVoxels = 10.0f; float HeadlessCollisionGateStressTestSpawnHeightVoxels = 2.5f * 100.0f / VOXEL_SIZE;
float HeadlessCollisionGateStressTestDurationSeconds = 2.0f; float HeadlessCollisionGateStressTestDurationSeconds = 2.0f;
float HeadlessCollisionGateStressTestTimeoutSeconds = 15.0f; float HeadlessCollisionGateStressTestTimeoutSeconds = 15.0f;
double HeadlessCollisionGateStressTestBeginSeconds = 0.0; double HeadlessCollisionGateStressTestBeginSeconds = 0.0;
@@ -1202,6 +1224,7 @@ public:
double HeadlessCollisionGateStressTestLandedSeconds = 0.0; double HeadlessCollisionGateStressTestLandedSeconds = 0.0;
double HeadlessCollisionGateStressTestGateStartSeconds = 0.0; double HeadlessCollisionGateStressTestGateStartSeconds = 0.0;
double HeadlessCollisionGateStressTestFullStopStartSeconds = 0.0; double HeadlessCollisionGateStressTestFullStopStartSeconds = 0.0;
double HeadlessCollisionGateStressTestFullStopProbeStartSeconds = 0.0;
double HeadlessCollisionGateStressTestDistanceCm = 0.0; double HeadlessCollisionGateStressTestDistanceCm = 0.0;
double HeadlessCollisionGateStressTestGateDistanceCm = 0.0; double HeadlessCollisionGateStressTestGateDistanceCm = 0.0;
double HeadlessCollisionGateStressTestTotalGateDurationSeconds = 0.0; double HeadlessCollisionGateStressTestTotalGateDurationSeconds = 0.0;
@@ -1236,10 +1259,10 @@ public:
bool bHeadlessSurfaceFallTestExitRequested = false; bool bHeadlessSurfaceFallTestExitRequested = false;
bool bHeadlessSurfaceFallTestPredictionProbeInjected = false; bool bHeadlessSurfaceFallTestPredictionProbeInjected = false;
bool bHeadlessSurfaceFallTestPredictionProbeCleared = false; bool bHeadlessSurfaceFallTestPredictionProbeCleared = false;
float HeadlessSurfaceFallTestSpawnHeightVoxels = 20.0f; float HeadlessSurfaceFallTestSpawnHeightVoxels = 5.0f * 100.0f / VOXEL_SIZE;
float HeadlessSurfaceFallTestGroundToleranceVoxels = 4.0f; float HeadlessSurfaceFallTestGroundToleranceVoxels = 1.0f * 100.0f / VOXEL_SIZE;
float HeadlessSurfaceFallTestTimeoutSeconds = 8.0f; float HeadlessSurfaceFallTestTimeoutSeconds = 8.0f;
float HeadlessSurfaceFallTestPlayerHalfHeightVoxels = 3.52f; float HeadlessSurfaceFallTestPlayerHalfHeightVoxels = 88.0f / VOXEL_SIZE;
float HeadlessSurfaceFallTestTerrainZ = 0.0f; float HeadlessSurfaceFallTestTerrainZ = 0.0f;
double HeadlessSurfaceFallTestBeginSeconds = 0.0; double HeadlessSurfaceFallTestBeginSeconds = 0.0;
double HeadlessSurfaceFallTestStartSeconds = 0.0; double HeadlessSurfaceFallTestStartSeconds = 0.0;
@@ -1279,8 +1302,8 @@ public:
float HeadlessStrateCrossingTestStartDelaySeconds = 1.0f; float HeadlessStrateCrossingTestStartDelaySeconds = 1.0f;
float HeadlessStrateCrossingTestTimeoutSeconds = 120.0f; float HeadlessStrateCrossingTestTimeoutSeconds = 120.0f;
float HeadlessStrateCrossingTestMaxGateSeconds = 10.0f; float HeadlessStrateCrossingTestMaxGateSeconds = 10.0f;
float HeadlessStrateCrossingTestRouteTargetRadiusVoxels = 2.0f; float HeadlessStrateCrossingTestRouteTargetRadiusVoxels = 0.5f * 100.0f / VOXEL_SIZE;
float HeadlessStrateCrossingTestPlayerHalfHeightVoxels = 3.52f; float HeadlessStrateCrossingTestPlayerHalfHeightVoxels = 88.0f / VOXEL_SIZE;
float HeadlessStrateCrossingTestMinFloorClearanceVoxels = 1000000.0f; float HeadlessStrateCrossingTestMinFloorClearanceVoxels = 1000000.0f;
double HeadlessStrateCrossingTestBeginSeconds = 0.0; double HeadlessStrateCrossingTestBeginSeconds = 0.0;
double HeadlessStrateCrossingTestStartSeconds = 0.0; double HeadlessStrateCrossingTestStartSeconds = 0.0;
@@ -1309,8 +1332,7 @@ public:
int32 VisualAuditLODOverlapEvents = 0; int32 VisualAuditLODOverlapEvents = 0;
int32 VisualAuditLODOverlapMaxPairs = 0; int32 VisualAuditLODOverlapMaxPairs = 0;
// PLAYER COLLISION GATE — only the downward component is held while the level-0 collision body // PLAYER COLLISION GATE — all movement is held while level-0 support collision is unresolved.
// beneath the pawn's feet is unresolved. Horizontal movement and player control remain live.
TWeakObjectPtr<APawn> PawnTickPrerequisite; TWeakObjectPtr<APawn> PawnTickPrerequisite;
TWeakObjectPtr<UPawnMovementComponent> PawnMovementTickPrerequisite; TWeakObjectPtr<UPawnMovementComponent> PawnMovementTickPrerequisite;
TWeakObjectPtr<APawn> CollisionGatedPawn; TWeakObjectPtr<APawn> CollisionGatedPawn;
@@ -1322,8 +1344,9 @@ public:
uint8 SavedCharacterMovementMode = 0; uint8 SavedCharacterMovementMode = 0;
uint8 SavedCharacterCustomMovementMode = 0; uint8 SavedCharacterCustomMovementMode = 0;
bool bSavedCharacterMovementMode = false; bool bSavedCharacterMovementMode = false;
float SavedGateVerticalVelocity = 0.0f; FVector SavedGateVelocity = FVector::ZeroVector;
bool bSavedGateVerticalVelocity = false; bool bSavedGateVelocity = false;
bool bSavedPawnMovementWasActive = false;
bool bPawnCollisionGateEngaged = false; bool bPawnCollisionGateEngaged = false;
double PawnGateEngagedAtSeconds = 0.0; double PawnGateEngagedAtSeconds = 0.0;
bool bPawnGateUnavailableReported = false; bool bPawnGateUnavailableReported = false;
@@ -55,8 +55,8 @@ using FExploreJsonWriter = TJsonWriter<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>;
constexpr int32 DefaultRenderWidth = 512; constexpr int32 DefaultRenderWidth = 512;
constexpr int32 DefaultRenderHeight = 288; constexpr int32 DefaultRenderHeight = 288;
constexpr float DefaultRenderStepVoxels = 0.25f; constexpr float DefaultRenderStepVoxels = 0.5f; // 6.25 cm, as at 25 cm/voxel.
constexpr float DefaultRenderMaxDistanceVoxels = 160.0f; constexpr float DefaultRenderMaxDistanceVoxels = 320.0f; // 40 m.
constexpr int32 DefaultExportSize = 128; constexpr int32 DefaultExportSize = 128;
constexpr int32 DefaultExportStep = 1; constexpr int32 DefaultExportStep = 1;
constexpr int32 DefaultMaxWalkCells = 32000000; constexpr int32 DefaultMaxWalkCells = 32000000;
@@ -1170,6 +1170,32 @@ struct FExploreWorld
NewObject<UVoxelSettings>(GetTransientPackage(), NAME_None, RF_Transient)); NewObject<UVoxelSettings>(GetTransientPackage(), NAME_None, RF_Transient));
} }
if (bUseAuthoredSettings)
{
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeScale3AssetSettings] view_xy=%d view_up=%d view_down=%d "
"pawn_collision_radius_chunks=%d strate_view_margin_chunks=%d clip_radius=%d "
"render_distance_chunks=%d world_radius_voxels=%.6g edge_seal_thickness_voxels=%.6g "
"decoration_radius_chunks=%d decoration_near_radius_chunks=%d "
"decoration_region_size_cells=%d decoration_spacing_voxels=%d "
"decoration_far_spacing_voxels=%d decoration_march_step_voxels=%d "
"decoration_column_depth_voxels=%d density_volume_resolution=%d "
"density_volume_debug_radius_cells=%d origin_spine_radius_voxels=%.6g "
"max_brush_radius_voxels=%.6g max_total_volume_voxels3=%.6g "
"inter_strate_gap_chunks=%d skirt_cells=%.6g"),
Settings->ViewDistanceXY, Settings->ViewDistanceUp, Settings->ViewDistanceDown,
Settings->PawnCollisionRadiusChunks, Settings->StrateViewMarginChunks,
Settings->ClipRadius, Settings->RenderDistanceChunks,
Settings->WorldRadiusVoxels, Settings->EdgeSealThickness,
Settings->DecorationRadiusChunks, Settings->DecorationNearRadiusChunks,
Settings->DecorationRegionSizeCells, Settings->DecorationSpacingVoxels,
Settings->DecorationFarSpacingVoxels, Settings->DecorationMarchStepVoxels,
Settings->DecorationColumnDepthVoxels, Settings->DensityVolumeResolution,
Settings->DensityVolumeDebugRadiusCells, Settings->OriginSpineRadius,
Settings->MaxBrushRadius, Settings->MaxTotalVolume,
Settings->InterStrateGapChunks, Settings->SkirtCells);
}
const int32 NumStrates = bUseAuthoredSettings const int32 NumStrates = bUseAuthoredSettings
? (Arguments.bGameRuntimeConfig ? (Arguments.bGameRuntimeConfig
? FMath::Max(Settings->TotalStrates, 1) ? FMath::Max(Settings->TotalStrates, 1)
@@ -1187,8 +1213,8 @@ struct FExploreWorld
Settings->TotalStrates = NumStrates; Settings->TotalStrates = NumStrates;
Settings->InterStrateGapChunks = 0; Settings->InterStrateGapChunks = 0;
Settings->WorldRadiusVoxels = 0.0f; Settings->WorldRadiusVoxels = 0.0f;
Settings->EdgeSealThickness = 64.0f; Settings->EdgeSealThickness = 128.0f;
Settings->OriginSpineRadius = 14.0f; Settings->OriginSpineRadius = 28.0f;
Settings->bOpenSurfaceEntry = true; Settings->bOpenSurfaceEntry = true;
} }
else if (Arguments.bSeedOverride) else if (Arguments.bSeedOverride)
@@ -1232,9 +1258,9 @@ struct FExploreWorld
Definition->StrateDescription = FText::FromString( Definition->StrateDescription = FText::FromString(
TEXT("Transient, deterministic commandlet definition.")); TEXT("Transient, deterministic commandlet definition."));
// The commandlet's acceptance sweep uses the owner-facing base spacing: // The commandlet's acceptance sweep uses the owner-facing base spacing:
// 4 chunks * 32 voxels * 0.25 m = 32 m per strate. Production assets may be taller; // 8 chunks * 32 voxels * 0.125 m = 32 m per strate. Production assets may be taller;
// this keeps the measured tunnel arithmetic tied to the stated ~32 m descent. // this keeps the measured tunnel arithmetic tied to the stated ~32 m descent.
Definition->StrateHeightInChunks = 4; Definition->StrateHeightInChunks = 8;
Definition->TransitionType = EVoxelStrateTransition::Hard; Definition->TransitionType = EVoxelStrateTransition::Hard;
Definition->GeneratorType = Index == Arguments.Slot Definition->GeneratorType = Index == Arguments.Slot
? Arguments.Archetype ? Arguments.Archetype
@@ -2896,7 +2922,7 @@ FStep2dBoundaryStats ComputeStep2dBoundaryStats(
const bool bBrickBoundary = (World[Axis] % 8) == 0; const bool bBrickBoundary = (World[Axis] % 8) == 0;
const bool bChunkBoundary = (World[Axis] % CHUNK_SIZE) == 0 && Axis < 2; const bool bChunkBoundary = (World[Axis] % CHUNK_SIZE) == 0 && Axis < 2;
const bool bFeatureBoundary = ((World[Axis] + CellSpacing / 2) % CellSpacing) == 0; const bool bFeatureBoundary = ((World[Axis] + CellSpacing / 2) % CellSpacing) == 0;
if (FMath::Abs(Density[CenterIndex]) <= 12.0f) if (FMath::Abs(Density[CenterIndex]) <= VoxelPassageGeometry::MetersToVoxels(3.0f))
{ {
if (bBrickBoundary) if (bBrickBoundary)
{ {
@@ -3074,10 +3100,13 @@ FStep2eVarietyMetrics ComputeStep2eVarietyMetrics(
} }
} }
Result.OpenFraction = Total > 0 ? static_cast<double>(AirCount) / Total : 0.0; Result.OpenFraction = Total > 0 ? static_cast<double>(AirCount) / Total : 0.0;
// Each sign-changing lattice edge estimates one quarter-meter-squared patch. This is a // Each sign-changing lattice edge estimates one voxel-face patch. This is a
// consistent edge-count estimate, not a marching-cubes triangle integral. // consistent edge-count estimate, not a marching-cubes triangle integral.
Result.SurfaceAreaSquareMeters = static_cast<double>(SurfaceEdgeCount) * 0.25 * 0.25; constexpr double VoxelMeters = static_cast<double>(VOXEL_SIZE) / 100.0;
const double VolumeCubicMeters = static_cast<double>(Total) * 0.25 * 0.25 * 0.25; Result.SurfaceAreaSquareMeters = static_cast<double>(SurfaceEdgeCount)
* VoxelMeters * VoxelMeters;
const double VolumeCubicMeters = static_cast<double>(Total)
* VoxelMeters * VoxelMeters * VoxelMeters;
Result.SurfaceAreaPerVolume = VolumeCubicMeters > 0.0 Result.SurfaceAreaPerVolume = VolumeCubicMeters > 0.0
? Result.SurfaceAreaSquareMeters / VolumeCubicMeters : 0.0; ? Result.SurfaceAreaSquareMeters / VolumeCubicMeters : 0.0;
const uint64 AllAdjacencies = HorizontalAdjacencies + VerticalAdjacencies; const uint64 AllAdjacencies = HorizontalAdjacencies + VerticalAdjacencies;
@@ -3100,7 +3129,8 @@ FStep2eVarietyMetrics ComputeStep2eVarietyMetrics(
Result.MeanLongestVerticalAirRunVoxels = ColumnsWithAir > 0 Result.MeanLongestVerticalAirRunVoxels = ColumnsWithAir > 0
? static_cast<double>(TotalLongestRunVoxels) / ColumnsWithAir : 0.0; ? static_cast<double>(TotalLongestRunVoxels) / ColumnsWithAir : 0.0;
constexpr int32 CoarseStep = 4; constexpr int32 VoxelsPerMeter = static_cast<int32>(100.0f / VOXEL_SIZE + 0.5f);
constexpr int32 CoarseStep = VoxelsPerMeter;
const int32 CoarseDimension = (Dimension - 1) / CoarseStep + 1; const int32 CoarseDimension = (Dimension - 1) / CoarseStep + 1;
const int32 CoarseCount = CoarseDimension * CoarseDimension * CoarseDimension; const int32 CoarseCount = CoarseDimension * CoarseDimension * CoarseDimension;
TArray<int32> Parent; TArray<int32> Parent;
@@ -3323,7 +3353,8 @@ FStep2fFragmentMetrics ComputeStep2fFragmentMetrics(
ComponentsIntersectingCore.Add(FindRoot(Index)); ComponentsIntersectingCore.Add(FindRoot(Index));
} }
} }
constexpr int32 VoxelsPerCubicMeter = 4 * 4 * 4; constexpr int32 VoxelsPerMeter = static_cast<int32>(100.0f / VOXEL_SIZE + 0.5f);
constexpr int32 VoxelsPerCubicMeter = VoxelsPerMeter * VoxelsPerMeter * VoxelsPerMeter;
TMap<int32, FStep2fFragmentMetrics::FCrumbComponent> CrumbByRoot; TMap<int32, FStep2fFragmentMetrics::FCrumbComponent> CrumbByRoot;
for (int32 Index = 0; Index < Count; ++Index) for (int32 Index = 0; Index < Count; ++Index)
{ {
@@ -3427,19 +3458,40 @@ struct FStep2fGridSignatureMetrics
}; };
FStep2fGridSignatureMetrics ComputeStep2fGridSignature( FStep2fGridSignatureMetrics ComputeStep2fGridSignature(
const TArray<float>& Density, int32 Dimension, float PitchVoxels) const UVoxelGenerator& Generator, FIntVector RingOrigin, int32 RingDimension,
float PitchVoxels)
{ {
FStep2fGridSignatureMetrics Result; FStep2fGridSignatureMetrics Result;
constexpr int32 SampleStep = 4; FScopedVoxelStackRegistration StackRegistration;
constexpr int32 SampleStep = static_cast<int32>(100.0f / VOXEL_SIZE + 0.5f);
constexpr int32 HalfWindow = static_cast<int32>(
3200.0f / VOXEL_SIZE + 0.5f);
constexpr int32 XYDimension = 2 * HalfWindow / SampleStep + 1;
constexpr int32 HeightLayers = 9;
const FIntVector RingCentre = RingOrigin + FIntVector(
(RingDimension - 1) / 2, (RingDimension - 1) / 2, (RingDimension - 1) / 2);
TArray<uint8> AirSamples;
AirSamples.SetNumUninitialized(XYDimension * XYDimension * HeightLayers);
int64 AirCount = 0; int64 AirCount = 0;
int64 SampleCount = 0; int64 SampleCount = 0;
for (int32 Z = 0; Z < Dimension; Z += SampleStep) for (int32 ZIndex = 0; ZIndex < HeightLayers; ++ZIndex)
for (int32 Y = 0; Y < Dimension; Y += SampleStep)
for (int32 X = 0; X < Dimension; X += SampleStep)
{ {
AirCount += Density[(Z * Dimension + Y) * Dimension + X] > 0.0f ? 1 : 0; const int32 Z = RingOrigin.Z + FMath::RoundToInt(
static_cast<float>(RingDimension - 1) * ZIndex / (HeightLayers - 1));
for (int32 YIndex = 0; YIndex < XYDimension; ++YIndex)
for (int32 XIndex = 0; XIndex < XYDimension; ++XIndex)
{
const float WorldX = static_cast<float>(RingCentre.X - HalfWindow
+ XIndex * SampleStep);
const float WorldY = static_cast<float>(RingCentre.Y - HalfWindow
+ YIndex * SampleStep);
const uint8 bAir = VoxelForgeRedefinedDensityField::EvaluateParameterized(
Generator, WorldX, WorldY, static_cast<float>(Z)) > 0.0f ? 1 : 0;
AirSamples[(ZIndex * XYDimension + YIndex) * XYDimension + XIndex] = bAir;
AirCount += bAir;
++SampleCount; ++SampleCount;
} }
}
const double Mean = SampleCount > 0 const double Mean = SampleCount > 0
? static_cast<double>(AirCount) / SampleCount : 0.0; ? static_cast<double>(AirCount) / SampleCount : 0.0;
const double Variance = Mean * (1.0 - Mean); const double Variance = Mean * (1.0 - Mean);
@@ -3453,31 +3505,35 @@ FStep2fGridSignatureMetrics ComputeStep2fGridSignature(
for (int32 Direction = 0; Direction < 8; ++Direction) for (int32 Direction = 0; Direction < 8; ++Direction)
{ {
const float Angle = static_cast<float>(Direction) * 3.14159265358979323846f / 8.0f; const float Angle = static_cast<float>(Direction) * 3.14159265358979323846f / 8.0f;
const int32 DX = FMath::RoundToInt(Radius * FMath::Cos(Angle)); const int32 DX = FMath::RoundToInt(
const int32 DY = FMath::RoundToInt(Radius * FMath::Sin(Angle)); Radius * FMath::Cos(Angle) / SampleStep);
const int32 DY = FMath::RoundToInt(
Radius * FMath::Sin(Angle) / SampleStep);
if (DX == 0 && DY == 0) if (DX == 0 && DY == 0)
{ {
continue; continue;
} }
double Covariance = 0.0; double Covariance = 0.0;
int64 Pairs = 0; int64 Pairs = 0;
for (int32 Z = 0; Z < Dimension; Z += SampleStep) for (int32 Z = 0; Z < HeightLayers; ++Z)
for (int32 Y = 0; Y < Dimension; Y += SampleStep) for (int32 Y = 0; Y < XYDimension; ++Y)
{ {
const int32 OtherY = Y + DY; const int32 OtherY = Y + DY;
if (OtherY < 0 || OtherY >= Dimension) if (OtherY < 0 || OtherY >= XYDimension)
{ {
continue; continue;
} }
for (int32 X = 0; X < Dimension; X += SampleStep) for (int32 X = 0; X < XYDimension; ++X)
{ {
const int32 OtherX = X + DX; const int32 OtherX = X + DX;
if (OtherX < 0 || OtherX >= Dimension) if (OtherX < 0 || OtherX >= XYDimension)
{ {
continue; continue;
} }
const bool bAirA = Density[(Z * Dimension + Y) * Dimension + X] > 0.0f; const bool bAirA = AirSamples[
const bool bAirB = Density[(Z * Dimension + OtherY) * Dimension + OtherX] > 0.0f; (Z * XYDimension + Y) * XYDimension + X] != 0;
const bool bAirB = AirSamples[
(Z * XYDimension + OtherY) * XYDimension + OtherX] != 0;
Covariance += (static_cast<double>(bAirA) - Mean) Covariance += (static_cast<double>(bAirA) - Mean)
* (static_cast<double>(bAirB) - Mean); * (static_cast<double>(bAirB) - Mean);
++Pairs; ++Pairs;
@@ -3531,22 +3587,24 @@ FStep2fDetailSpectrum ComputeStep2fDetailSpectrum(const TArray<float>& Detail, i
for (int32 X = 0; X < Dimension; ++X) for (int32 X = 0; X < Dimension; ++X)
{ {
const int32 Index = (Z * Dimension + Y) * Dimension + X; const int32 Index = (Z * Dimension + Y) * Dimension + X;
if (X + 1 < Dimension) constexpr int32 PhysicalLagVoxels = static_cast<int32>(25.0f / VOXEL_SIZE + 0.5f);
if (X + PhysicalLagVoxels < Dimension)
{ {
DifferenceEnergy += FMath::Square( DifferenceEnergy += FMath::Square(
static_cast<double>(Detail[Index + 1] - Detail[Index])); static_cast<double>(Detail[Index + PhysicalLagVoxels] - Detail[Index]));
++DifferenceCount; ++DifferenceCount;
} }
if (Y + 1 < Dimension) if (Y + PhysicalLagVoxels < Dimension)
{ {
DifferenceEnergy += FMath::Square( DifferenceEnergy += FMath::Square(
static_cast<double>(Detail[Index + Dimension] - Detail[Index])); static_cast<double>(Detail[Index + PhysicalLagVoxels * Dimension] - Detail[Index]));
++DifferenceCount; ++DifferenceCount;
} }
if (Z + 1 < Dimension) if (Z + PhysicalLagVoxels < Dimension)
{ {
DifferenceEnergy += FMath::Square( DifferenceEnergy += FMath::Square(
static_cast<double>(Detail[Index + Dimension * Dimension] - Detail[Index])); static_cast<double>(Detail[Index + PhysicalLagVoxels * Dimension * Dimension]
- Detail[Index]));
++DifferenceCount; ++DifferenceCount;
} }
} }
@@ -3615,9 +3673,10 @@ float Step2fPointTriangleDistanceSquared(
FIntVector SelectStep2fMeshOrigin( FIntVector SelectStep2fMeshOrigin(
const TArray<float>& Density, FIntVector RingOrigin, int32 Dimension) const TArray<float>& Density, FIntVector RingOrigin, int32 Dimension)
{ {
constexpr int32 PhysicalTileVoxels = 64; constexpr int32 VoxelsPerMeter = static_cast<int32>(100.0f / VOXEL_SIZE + 0.5f);
constexpr int32 ScoreStepVoxels = 4; constexpr int32 PhysicalTileVoxels = 16 * VoxelsPerMeter;
constexpr int32 CandidateStepVoxels = 32; constexpr int32 ScoreStepVoxels = VoxelsPerMeter;
constexpr int32 CandidateStepVoxels = 8 * VoxelsPerMeter;
const int32 MaximumStart = Dimension - 1 - PhysicalTileVoxels; const int32 MaximumStart = Dimension - 1 - PhysicalTileVoxels;
if (Density.Num() != Dimension * Dimension * Dimension || MaximumStart < 0) if (Density.Num() != Dimension * Dimension * Dimension || MaximumStart < 0)
{ {
@@ -3801,7 +3860,8 @@ TSharedRef<FJsonObject> Step2eChamberVolumeQuantilesJson(
{ {
TSharedRef<FJsonObject> Json = MakeShared<FJsonObject>(); TSharedRef<FJsonObject> Json = MakeShared<FJsonObject>();
constexpr double Pi = 3.14159265358979323846; constexpr double Pi = 3.14159265358979323846;
constexpr double CubicMetersPerVoxel = 0.25 * 0.25 * 0.25; constexpr double VoxelMeters = static_cast<double>(VOXEL_SIZE) / 100.0;
constexpr double CubicMetersPerVoxel = VoxelMeters * VoxelMeters * VoxelMeters;
const double Quantiles[] = {0.10, 0.50, 0.90}; const double Quantiles[] = {0.10, 0.50, 0.90};
const TCHAR* Names[] = {TEXT("p10_m3"), TEXT("p50_m3"), TEXT("p90_m3")}; const TCHAR* Names[] = {TEXT("p10_m3"), TEXT("p50_m3"), TEXT("p90_m3")};
for (int32 Index = 0; Index < UE_ARRAY_COUNT(Quantiles); ++Index) for (int32 Index = 0; Index < UE_ARRAY_COUNT(Quantiles); ++Index)
@@ -4191,7 +4251,7 @@ int32 RunStep2d(const FString& Params)
Window->SetNumberField(TEXT("origin_z_voxels"), Origin.Z); Window->SetNumberField(TEXT("origin_z_voxels"), Origin.Z);
Window->SetNumberField(TEXT("samples_per_axis"), Dimension); Window->SetNumberField(TEXT("samples_per_axis"), Dimension);
Window->SetNumberField(TEXT("sample_stride_voxels"), 1); Window->SetNumberField(TEXT("sample_stride_voxels"), 1);
Window->SetNumberField(TEXT("sample_spacing_cm"), 25.0); Window->SetNumberField(TEXT("sample_spacing_cm"), VOXEL_SIZE);
Report->SetObjectField(TEXT("full_resolution_window"), Window); Report->SetObjectField(TEXT("full_resolution_window"), Window);
Report->SetStringField(TEXT("field_definition"), Report->SetStringField(TEXT("field_definition"),
TEXT("A seed-offset periodic cross-corridor signed field per strate, joined with rounded " TEXT("A seed-offset periodic cross-corridor signed field per strate, joined with rounded "
@@ -4199,7 +4259,7 @@ int32 RunStep2d(const FString& Params)
"noise bands (8, 4, 2, and 1 lattice point) with total bounded amplitude 5.25 are " "noise bands (8, 4, 2, and 1 lattice point) with total bounded amplitude 5.25 are "
"evaluated at every full-resolution sample that can affect the zero set.")); "evaluated at every full-resolution sample that can affect the zero set."));
Report->SetStringField(TEXT("resolution_contract"), Report->SetStringField(TEXT("resolution_contract"),
TEXT("The lattice remains one sample per 25 cm. No coarse density result is interpolated " TEXT("The lattice remains one sample per 12.5 cm. No coarse density result is interpolated "
"onto it: every potentially surface-affecting point evaluates all four detail bands. " "onto it: every potentially surface-affecting point evaluates all four detail bands. "
"Brick skips require a final-field sign proof including the full noise supremum, a " "Brick skips require a final-field sign proof including the full noise supremum, a "
"floating-point safety margin, and the one-sample marching-cubes halo.")); "floating-point safety margin, and the one-sample marching-cubes halo."));
@@ -4701,19 +4761,18 @@ int32 RunStep2e(const FString& Params)
Report->SetObjectField(TEXT("parameter_ranges_across_eight_profiles"), Report->SetObjectField(TEXT("parameter_ranges_across_eight_profiles"),
Step2eParameterRangesJson()); Step2eParameterRangesJson());
Report->SetStringField(TEXT("resolution_contract"), Report->SetStringField(TEXT("resolution_contract"),
TEXT("One exact field sample per 25 cm lattice point. The four existing 8/4/2/1-sample " TEXT("One exact field sample per 12.5 cm lattice point. The four existing 16/8/4/2-sample "
"detail bands are evaluated for every potentially surface-affecting point; no coarse " "detail bands are evaluated for every potentially surface-affecting point; no coarse "
"field is interpolated onto the lattice. 4-cubed step 2e skips require the final macro field " "field is interpolated onto the lattice. 4-cubed step 2e skips require the final macro field "
"Lipschitz interval plus the full 5.25-unit detail supremum and marching-cubes halo.")); "Lipschitz interval plus the full 10.5-unit detail supremum and marching-cubes halo."));
Report->SetStringField(TEXT("finer_lattice_scaling"), Report->SetStringField(TEXT("finer_lattice_scaling"),
TEXT("For 12.5 cm voxels, double room pitch, room height, tunnel/feature dimensions and " TEXT("At 12.5 cm, metre-valued macro and noise dimensions convert to twice as many samples; "
"the broad-to-medium noise periods in sample units to preserve meter-scale shapes. " "the four detail bands retain their metre wavelengths. The lattice grows 8x for a fixed "
"Keep the 1-sample detail band or add a finer octave. The lattice grows 8x for a fixed "
"world volume; surface bricks grow roughly with area while certified interior bricks " "world volume; surface bricks grow roughly with area while certified interior bricks "
"remain cheap. This scale has not been timed.")); "remain cheap."));
Report->SetStringField(TEXT("variety_metric_method"), Report->SetStringField(TEXT("variety_metric_method"),
TEXT("Open fraction uses every ring sample. Surface area estimates sign-changing lattice " TEXT("Open fraction uses every ring sample. Surface area estimates sign-changing lattice "
"edges times 0.25 m squared. Nominal chamber-volume p10/p50/p90 values use the " "edges times 0.125 m squared. Nominal chamber-volume p10/p50/p90 values use the "
"uniform per-cell radius roll and room height as circular-cylinder volumes. Tunnel-neck " "uniform per-cell radius roll and room height as circular-cylinder volumes. Tunnel-neck "
"share and connected-component sizes use a 1 m downsampled six-neighbor occupancy grid. " "share and connected-component sizes use a 1 m downsampled six-neighbor occupancy grid. "
"Verticality is the mean longest contiguous open run per XY column, with open-column " "Verticality is the mean longest contiguous open run per XY column, with open-column "
@@ -4846,6 +4905,238 @@ int32 RunStep2e(const FString& Params)
return bPassed ? 0 : 2; return bPassed ? 0 : 2;
} }
int32 RunStep3ScaleBenchmark(const FString& Params)
{
FExploreArguments Arguments;
FString Error;
if (!ParseArguments(Params, Arguments, Error))
{
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep3Scale] argument error: %s"), *Error);
return 2;
}
int32 Seed = 1337;
int32 Workers = VoxelForgeRegionEval::WorkerCount;
int32 Profile = 0;
FParse::Value(*Params, TEXT("seed="), Seed);
FParse::Value(*Params, TEXT("regionworkers="), Workers);
FParse::Value(*Params, TEXT("profile="), Profile);
Workers = FMath::Clamp(Workers, 1, 32);
Profile = FMath::Clamp(Profile, 0,
VoxelForgeRedefinedDensityField::GetParameterSetCount() - 1);
FString ResearchOut;
FParse::Value(*Params, TEXT("researchout="), ResearchOut);
if (ResearchOut.IsEmpty())
{
ResearchOut = FPaths::Combine(Arguments.OutDirectory, TEXT("Research/step3-scale"));
}
ResearchOut = FPaths::ConvertRelativePathToFull(ResearchOut);
IFileManager::Get().MakeDirectory(*ResearchOut, true);
IFileManager::Get().MakeDirectory(*Arguments.OutDirectory, true);
FExploreWorld World;
int32 RecipeIndex = INDEX_NONE;
uint32 RecipeHash = 0;
int32 EditChunkCount = 0;
FString WorldError;
const double PrepareStart = FPlatformTime::Seconds();
if (!PrepareRegionEvalWorld(World, Arguments, Seed,
RecipeIndex, RecipeHash, EditChunkCount, WorldError))
{
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep3Scale] world preparation failed: %s"),
*WorldError);
return 2;
}
const double WorldPrepareSeconds = FPlatformTime::Seconds() - PrepareStart;
if (World.DiffLayer != nullptr)
{
World.DiffLayer->Clear();
}
const int32 CenterZ = (World.TargetTopWorldZ + World.TargetBottomWorldZ) / 2;
const TCHAR* ProfileName = TEXT("unknown");
VoxelForgeRedefinedDensityField::FGenerationParameters ProfileParameters;
if (!VoxelForgeRedefinedDensityField::GetParameterSetInfo(
Profile, ProfileName, ProfileParameters))
{
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep3Scale] profile %d is unavailable."), Profile);
return 2;
}
VoxelForgeRedefinedDensityField::SetEnabled(false);
VoxelForgeRedefinedDensityField::SetParameterizedEnabled(true);
VoxelForgeRedefinedDensityField::SetLegacyStep2eAuditMode(false);
VoxelForgeRedefinedDensityField::SetDetailScale(1.0f);
VoxelForgeRedefinedDensityField::SetParameterSet(Profile);
const int32 Dimensions[] = { 519, 259 };
TArray<TSharedPtr<FJsonValue>> CubeRows;
bool bPass = Workers == 6;
for (const int32 Dimension : Dimensions)
{
const int32 HalfSpan = (Dimension - 1) / 2;
const FIntVector Origin(-HalfSpan, -HalfSpan, CenterZ - HalfSpan);
const int64 ExpectedSamples = static_cast<int64>(Dimension)
* Dimension * Dimension;
TArray<TSharedPtr<FJsonValue>> TrialRows;
TArray<double> WallTimes;
TArray<double> CpuTimes;
uint64 FirstQuantizedHash = 0;
uint64 FirstRawHash = 0;
bool bDeterministic = true;
int32 ObservedWorkersMin = Workers;
int64 EvaluatedSamples = -1;
int64 SkippedSamples = -1;
uint64 BrickCount = 0;
uint64 SkippedBricks = 0;
for (int32 Trial = 0; Trial < 3; ++Trial)
{
VoxelForgeRegionEval::FTiming Timing;
TArray<float> Densities;
if (!VoxelForgeRegionEval::EvaluateRedefined(
*World.Generator, Origin, Dimension, Workers, 1.0f,
Densities, Timing, /*bEnableBrickSkips=*/true,
/*bParameterized=*/true))
{
UE_LOG(LogTemp, Error,
TEXT("[VoxelForgeStep3Scale] dimension=%d trial=%d evaluation failed."),
Dimension, Trial + 1);
VoxelForgeRedefinedDensityField::SetParameterizedEnabled(false);
return 2;
}
if (Densities.Num() != ExpectedSamples || Timing.SampleCount != ExpectedSamples
|| Timing.NonFiniteSamples != 0)
{
UE_LOG(LogTemp, Error,
TEXT("[VoxelForgeStep3Scale] dimension=%d trial=%d sample/finite invariant failed."),
Dimension, Trial + 1);
VoxelForgeRedefinedDensityField::SetParameterizedEnabled(false);
return 2;
}
if (Trial == 0)
{
FirstQuantizedHash = Timing.QuantizedHash;
FirstRawHash = Timing.RawFloatHash;
EvaluatedSamples = Timing.EvaluatedSamples;
SkippedSamples = static_cast<int64>(Timing.SkippedSamples);
BrickCount = Timing.BrickCount;
SkippedBricks = Timing.SkippedBricks;
}
else
{
bDeterministic &= Timing.QuantizedHash == FirstQuantizedHash
&& Timing.RawFloatHash == FirstRawHash;
}
ObservedWorkersMin = FMath::Min(ObservedWorkersMin,
Timing.ObservedWorkerThreads);
WallTimes.Add(Timing.WallSeconds);
CpuTimes.Add(Timing.WorkerCpuSeconds);
TSharedRef<FJsonObject> TrialJson = MakeShared<FJsonObject>();
TrialJson->SetNumberField(TEXT("trial"), Trial + 1);
TrialJson->SetNumberField(TEXT("wall_seconds"), Timing.WallSeconds);
TrialJson->SetNumberField(TEXT("worker_cpu_seconds"), Timing.WorkerCpuSeconds);
TrialJson->SetNumberField(TEXT("workers_requested"), Timing.Workers);
TrialJson->SetNumberField(TEXT("workers_observed"), Timing.ObservedWorkerThreads);
TrialJson->SetNumberField(TEXT("evaluated_samples"),
static_cast<double>(Timing.EvaluatedSamples));
TrialJson->SetNumberField(TEXT("skipped_samples"),
static_cast<double>(Timing.SkippedSamples));
TrialJson->SetNumberField(TEXT("bricks"), static_cast<double>(Timing.BrickCount));
TrialJson->SetNumberField(TEXT("skipped_bricks"),
static_cast<double>(Timing.SkippedBricks));
TrialJson->SetStringField(TEXT("quantized_hash"), FString::Printf(
TEXT("%016llx"), static_cast<unsigned long long>(Timing.QuantizedHash)));
TrialJson->SetStringField(TEXT("raw_float_hash"), FString::Printf(
TEXT("%016llx"), static_cast<unsigned long long>(Timing.RawFloatHash)));
TrialRows.Add(MakeShared<FJsonValueObject>(TrialJson));
}
WallTimes.Sort();
CpuTimes.Sort();
TSharedRef<FJsonObject> Cube = MakeShared<FJsonObject>();
Cube->SetNumberField(TEXT("dimension"), Dimension);
Cube->SetNumberField(TEXT("sample_count"), static_cast<double>(ExpectedSamples));
Cube->SetNumberField(TEXT("intervals_per_axis"), Dimension - 1);
Cube->SetNumberField(TEXT("sample_spacing_cm"), VOXEL_SIZE);
Cube->SetNumberField(TEXT("extent_m_per_axis"),
static_cast<double>(Dimension - 1) * VOXEL_SIZE / 100.0);
Cube->SetNumberField(TEXT("density_output_mib"),
static_cast<double>(ExpectedSamples) * sizeof(float) / (1024.0 * 1024.0));
Cube->SetNumberField(TEXT("workers_requested"), Workers);
Cube->SetNumberField(TEXT("minimum_workers_observed"), ObservedWorkersMin);
Cube->SetNumberField(TEXT("evaluated_samples"), static_cast<double>(EvaluatedSamples));
Cube->SetNumberField(TEXT("skipped_samples"), static_cast<double>(SkippedSamples));
Cube->SetNumberField(TEXT("bricks"), static_cast<double>(BrickCount));
Cube->SetNumberField(TEXT("skipped_bricks"), static_cast<double>(SkippedBricks));
Cube->SetNumberField(TEXT("median_wall_seconds"), WallTimes[WallTimes.Num() / 2]);
Cube->SetNumberField(TEXT("min_wall_seconds"), WallTimes[0]);
Cube->SetNumberField(TEXT("max_wall_seconds"), WallTimes.Last());
Cube->SetNumberField(TEXT("median_worker_cpu_seconds"), CpuTimes[CpuTimes.Num() / 2]);
Cube->SetBoolField(TEXT("deterministic_three_trials"), bDeterministic);
Cube->SetStringField(TEXT("quantized_hash"), FString::Printf(
TEXT("%016llx"), static_cast<unsigned long long>(FirstQuantizedHash)));
Cube->SetStringField(TEXT("raw_float_hash"), FString::Printf(
TEXT("%016llx"), static_cast<unsigned long long>(FirstRawHash)));
Cube->SetArrayField(TEXT("trials"), TrialRows);
CubeRows.Add(MakeShared<FJsonValueObject>(Cube));
bPass &= bDeterministic && ObservedWorkersMin == 6;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeStep3Scale] dimension=%d samples=%lld extent_m=%.3f workers=%d "
"median_wall_s=%.6f min=%.6f max=%.6f evaluated=%lld skipped=%lld "
"deterministic=%d hash=%016llx"),
Dimension, static_cast<long long>(ExpectedSamples),
static_cast<double>(Dimension - 1) * VOXEL_SIZE / 100.0,
Workers, WallTimes[WallTimes.Num() / 2], WallTimes[0], WallTimes.Last(),
static_cast<long long>(EvaluatedSamples), static_cast<long long>(SkippedSamples),
bDeterministic ? 1 : 0,
static_cast<unsigned long long>(FirstQuantizedHash));
}
TSharedRef<FJsonObject> Report = MakeShared<FJsonObject>();
Report->SetNumberField(TEXT("schema_version"), 1);
Report->SetStringField(TEXT("tool"), TEXT("VoxelForgeStep3Scale"));
Report->SetBoolField(TEXT("pass"), bPass);
Report->SetNumberField(TEXT("seed"), Seed);
Report->SetNumberField(TEXT("profile"), Profile);
Report->SetStringField(TEXT("profile_name"), ProfileName);
Report->SetNumberField(TEXT("worker_count"), Workers);
Report->SetNumberField(TEXT("voxel_size_cm"), VOXEL_SIZE);
Report->SetNumberField(TEXT("chunk_size_voxels"), CHUNK_SIZE);
Report->SetNumberField(TEXT("chunk_size_m"),
static_cast<double>(CHUNK_SIZE) * VOXEL_SIZE / 100.0);
Report->SetNumberField(TEXT("detail_scale"), 1.0);
Report->SetBoolField(TEXT("brick_skips_enabled"), true);
Report->SetNumberField(TEXT("world_prepare_seconds"), WorldPrepareSeconds);
Report->SetNumberField(TEXT("recipe_index"), RecipeIndex);
Report->SetNumberField(TEXT("recipe_hash"), RecipeHash);
Report->SetNumberField(TEXT("edit_chunk_count"), EditChunkCount);
Report->SetArrayField(TEXT("cubes"), CubeRows);
FString JsonText;
const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&JsonText);
const FString ReportPath = FPaths::Combine(ResearchOut, TEXT("step3_scale_benchmark.json"));
const FString LaunchReportPath = FPaths::Combine(
Arguments.OutDirectory, TEXT("step3_scale_benchmark.json"));
if (!FJsonSerializer::Serialize(Report, Writer)
|| !FFileHelper::SaveStringToFile(JsonText, *ReportPath,
FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)
|| !FFileHelper::SaveStringToFile(JsonText, *LaunchReportPath,
FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
{
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep3Scale] report write failed: %s"), *ReportPath);
VoxelForgeRedefinedDensityField::SetParameterizedEnabled(false);
return 2;
}
VoxelForgeRedefinedDensityField::SetParameterizedEnabled(false);
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeStep3Scale] pass=%d report=%s"), bPass ? 1 : 0, *ReportPath);
return bPass ? 0 : 2;
}
int32 RunStep2f(const FString& Params) int32 RunStep2f(const FString& Params)
{ {
FExploreArguments Arguments; FExploreArguments Arguments;
@@ -4943,9 +5234,10 @@ int32 RunStep2f(const FString& Params)
FStep2fShapeSnapshot Result; FStep2fShapeSnapshot Result;
Result.Variety = ComputeStep2eVarietyMetrics(Density, Dimension); Result.Variety = ComputeStep2eVarietyMetrics(Density, Dimension);
Result.Fragments = ComputeStep2fFragmentMetrics(Density, Dimension); Result.Fragments = ComputeStep2fFragmentMetrics(Density, Dimension);
Result.Grid = ComputeStep2fGridSignature(Density, Dimension, P.RoomPitchVoxels); Result.Grid = ComputeStep2fGridSignature(
*World.Generator, Origin, Dimension, P.RoomPitchVoxels);
constexpr int32 DetailDimension = 32; constexpr int32 DetailDimension = 8 * static_cast<int32>(100.0f / VOXEL_SIZE + 0.5f);
const FIntVector DetailOrigin = Origin + FIntVector( const FIntVector DetailOrigin = Origin + FIntVector(
(Dimension - DetailDimension) / 2, (Dimension - DetailDimension) / 2,
(Dimension - DetailDimension) / 2, (Dimension - DetailDimension) / 2,
@@ -4970,11 +5262,13 @@ int32 RunStep2f(const FString& Params)
const bool bPreviousSkirts = World.Mesher->bGenerateSkirts; const bool bPreviousSkirts = World.Mesher->bGenerateSkirts;
World.Mesher->bGenerateSkirts = false; World.Mesher->bGenerateSkirts = false;
FVoxelMeshData FineMesh; FVoxelMeshData FineMesh;
// GenerateMesh clamps cells per tile to CHUNK_SIZE. Eight level-0 tiles // GenerateMesh clamps cells per tile to CHUNK_SIZE. At 12.5 cm, a 16 m
// cover the same 64-sample cube as the one level-1 tile below. // full-detail cube is four level-0 chunks across and two level-1 chunks across.
for (int32 TileZ = 0; TileZ < 2; ++TileZ) constexpr int32 Level0TilesPerAxis = static_cast<int32>(
for (int32 TileY = 0; TileY < 2; ++TileY) 16.0f / (CHUNK_SIZE * VOXEL_SIZE / 100.0f));
for (int32 TileX = 0; TileX < 2; ++TileX) for (int32 TileZ = 0; TileZ < Level0TilesPerAxis; ++TileZ)
for (int32 TileY = 0; TileY < Level0TilesPerAxis; ++TileY)
for (int32 TileX = 0; TileX < Level0TilesPerAxis; ++TileX)
{ {
const FIntVector TileOrigin = MeshOrigin + FIntVector( const FIntVector TileOrigin = MeshOrigin + FIntVector(
TileX * CHUNK_SIZE, TileY * CHUNK_SIZE, TileZ * CHUNK_SIZE); TileX * CHUNK_SIZE, TileY * CHUNK_SIZE, TileZ * CHUNK_SIZE);
@@ -4987,8 +5281,24 @@ int32 RunStep2f(const FString& Params)
FineMesh.Triangles.Add(VertexOffset + Index); FineMesh.Triangles.Add(VertexOffset + Index);
} }
} }
const FVoxelMeshData CoarseMesh = World.Mesher->GenerateMesh( FVoxelMeshData CoarseMesh;
MeshOrigin, 2, CHUNK_SIZE); constexpr int32 Level1TilesPerAxis = static_cast<int32>(
16.0f / (CHUNK_SIZE * 2 * VOXEL_SIZE / 100.0f));
for (int32 TileZ = 0; TileZ < Level1TilesPerAxis; ++TileZ)
for (int32 TileY = 0; TileY < Level1TilesPerAxis; ++TileY)
for (int32 TileX = 0; TileX < Level1TilesPerAxis; ++TileX)
{
const FIntVector TileOrigin = MeshOrigin + FIntVector(
TileX * CHUNK_SIZE * 2, TileY * CHUNK_SIZE * 2, TileZ * CHUNK_SIZE * 2);
const FVoxelMeshData TileMesh = World.Mesher->GenerateMesh(
TileOrigin, 2, CHUNK_SIZE);
const int32 VertexOffset = CoarseMesh.Vertices.Num();
CoarseMesh.Vertices.Append(TileMesh.Vertices);
for (const int32 Index : TileMesh.Triangles)
{
CoarseMesh.Triangles.Add(VertexOffset + Index);
}
}
World.Mesher->bGenerateSkirts = bPreviousSkirts; World.Mesher->bGenerateSkirts = bPreviousSkirts;
Result.Mesh = ComputeStep2fMeshMetrics(FineMesh); Result.Mesh = ComputeStep2fMeshMetrics(FineMesh);
Result.Lod1VertexCount = CoarseMesh.Vertices.Num(); Result.Lod1VertexCount = CoarseMesh.Vertices.Num();
@@ -4999,7 +5309,7 @@ int32 RunStep2f(const FString& Params)
// coarse vertex only needs nearby cells unless its distance is already // coarse vertex only needs nearby cells unless its distance is already
// large enough to fail the LOD gate; expanding the search makes the // large enough to fail the LOD gate; expanding the search makes the
// reported value exact even for badly diverged surfaces. // reported value exact even for badly diverged surfaces.
constexpr float LodBucketSizeCm = VOXEL_SIZE * 4.0f; constexpr float LodBucketSizeCm = 100.0f;
TMap<FIntVector, TArray<int32>> FineTriangleBuckets; TMap<FIntVector, TArray<int32>> FineTriangleBuckets;
FineTriangleBuckets.Reserve(FineMesh.Triangles.Num()); FineTriangleBuckets.Reserve(FineMesh.Triangles.Num());
for (int32 Triangle = 0; Triangle + 2 < FineMesh.Triangles.Num(); Triangle += 3) for (int32 Triangle = 0; Triangle + 2 < FineMesh.Triangles.Num(); Triangle += 3)
@@ -5092,8 +5402,11 @@ int32 RunStep2f(const FString& Params)
if (World.Manager != nullptr) if (World.Manager != nullptr)
{ {
const TArray<FStrateSlot>& Layout = World.Manager->GetLayout(); const TArray<FStrateSlot>& Layout = World.Manager->GetLayout();
constexpr int32 XYStep = 4; // 1 m coverage grid constexpr int32 XYStep = static_cast<int32>(100.0f / VOXEL_SIZE + 0.5f);
constexpr int32 ShellOffsets[] = {1, 5, 9}; constexpr int32 ShellOffsets[] = {
static_cast<int32>(25.0f / VOXEL_SIZE + 0.5f),
static_cast<int32>(125.0f / VOXEL_SIZE + 0.5f),
static_cast<int32>(225.0f / VOXEL_SIZE + 0.5f)};
for (const FStrateSlot& Slot : Layout) for (const FStrateSlot& Slot : Layout)
{ {
if (Slot.Definition == nullptr) if (Slot.Definition == nullptr)
@@ -5130,9 +5443,9 @@ int32 RunStep2f(const FString& Params)
Json->SetNumberField(TEXT("six_worker_ring_median_ms"), MedianMs); Json->SetNumberField(TEXT("six_worker_ring_median_ms"), MedianMs);
Json->SetNumberField(TEXT("solid_fraction"), 1.0 - Variety.OpenFraction); Json->SetNumberField(TEXT("solid_fraction"), 1.0 - Variety.OpenFraction);
Json->SetNumberField(TEXT("air_fraction"), Variety.OpenFraction); Json->SetNumberField(TEXT("air_fraction"), Variety.OpenFraction);
Json->SetNumberField(TEXT("air_components_25cm_grid"), Json->SetNumberField(TEXT("air_components_12_5cm_grid"),
Shape.Fragments.AirComponents); Shape.Fragments.AirComponents);
Json->SetNumberField(TEXT("solid_components_25cm_grid"), Json->SetNumberField(TEXT("solid_components_12_5cm_grid"),
Shape.Fragments.SolidComponents); Shape.Fragments.SolidComponents);
Json->SetNumberField(TEXT("air_crumbs_below_1m3"), Json->SetNumberField(TEXT("air_crumbs_below_1m3"),
static_cast<double>(Shape.Fragments.AirCrumbsBelowOneCubicMeter)); static_cast<double>(Shape.Fragments.AirCrumbsBelowOneCubicMeter));
@@ -5185,6 +5498,9 @@ int32 RunStep2f(const FString& Params)
Json->SetNumberField(TEXT("pitch_autocorrelation_peak"), Shape.Grid.PitchCorrelationPeak); Json->SetNumberField(TEXT("pitch_autocorrelation_peak"), Shape.Grid.PitchCorrelationPeak);
Json->SetNumberField(TEXT("nearby_autocorrelation_peak"), Shape.Grid.NeighborCorrelationPeak); Json->SetNumberField(TEXT("nearby_autocorrelation_peak"), Shape.Grid.NeighborCorrelationPeak);
Json->SetNumberField(TEXT("pitch_autocorrelation_excess"), Shape.Grid.PitchPeakExcess); Json->SetNumberField(TEXT("pitch_autocorrelation_excess"), Shape.Grid.PitchPeakExcess);
Json->SetNumberField(TEXT("grid_signature_probe_extent_m"), 64.0);
Json->SetNumberField(TEXT("grid_signature_probe_spacing_m"), 1.0);
Json->SetNumberField(TEXT("grid_signature_probe_height_layers"), 9);
Json->SetNumberField(TEXT("detail_rms"), Shape.Detail.Rms); Json->SetNumberField(TEXT("detail_rms"), Shape.Detail.Rms);
Json->SetNumberField(TEXT("detail_lattice_scale_energy_ratio"), Json->SetNumberField(TEXT("detail_lattice_scale_energy_ratio"),
Shape.Detail.LatticeScaleEnergyRatio); Shape.Detail.LatticeScaleEnergyRatio);
@@ -5207,7 +5523,7 @@ int32 RunStep2f(const FString& Params)
return Json; return Json;
}; };
constexpr int32 CrumbBoundaryPadding = 64; constexpr int32 CrumbBoundaryPadding = 16 * static_cast<int32>(100.0f / VOXEL_SIZE + 0.5f);
const int32 PaddedCrumbDimension = Dimension + 2 * CrumbBoundaryPadding; const int32 PaddedCrumbDimension = Dimension + 2 * CrumbBoundaryPadding;
const FIntVector PaddedCrumbOrigin = Origin const FIntVector PaddedCrumbOrigin = Origin
- FIntVector(CrumbBoundaryPadding, CrumbBoundaryPadding, CrumbBoundaryPadding); - FIntVector(CrumbBoundaryPadding, CrumbBoundaryPadding, CrumbBoundaryPadding);
@@ -5295,7 +5611,7 @@ int32 RunStep2f(const FString& Params)
bStep2eBandLimitRejected &= LegacyShape.Detail.LatticeScaleEnergyRatio > 0.20; bStep2eBandLimitRejected &= LegacyShape.Detail.LatticeScaleEnergyRatio > 0.20;
WorstStep2eLodDistance = FMath::Max( WorstStep2eLodDistance = FMath::Max(
WorstStep2eLodDistance, LegacyShape.Lod1P95SurfaceErrorVoxels); WorstStep2eLodDistance, LegacyShape.Lod1P95SurfaceErrorVoxels);
bStep2eLodRejected |= LegacyShape.Lod1P95SurfaceErrorVoxels > 0.5; bStep2eLodRejected |= LegacyShape.Lod1P95SurfaceErrorVoxels > 1.0;
WorstStep2ePitchCorrelation = FMath::Max( WorstStep2ePitchCorrelation = FMath::Max(
WorstStep2ePitchCorrelation, LegacyShape.Grid.PitchCorrelationPeak); WorstStep2ePitchCorrelation, LegacyShape.Grid.PitchCorrelationPeak);
WorstStep2ePitchExcess = FMath::Max( WorstStep2ePitchExcess = FMath::Max(
@@ -5383,9 +5699,9 @@ int32 RunStep2f(const FString& Params)
&& NewShape.Mesh.ShareDihedralsOver45Degrees <= 0.25 && NewShape.Mesh.ShareDihedralsOver45Degrees <= 0.25
&& NewShape.Mesh.P95DihedralDegrees <= 70.0; && NewShape.Mesh.P95DihedralDegrees <= 70.0;
bStep2fBandLimitPassed &= NewShape.Detail.LatticeScaleEnergyRatio <= 0.20 bStep2fBandLimitPassed &= NewShape.Detail.LatticeScaleEnergyRatio <= 0.20
&& NewShape.Detail.Rms >= 0.40; && NewShape.Detail.Rms >= 0.80;
bStep2fLodPassed &= NewShape.Lod1VertexCount > 0 bStep2fLodPassed &= NewShape.Lod1VertexCount > 0
&& NewShape.Lod1P95SurfaceErrorVoxels <= 0.5; && NewShape.Lod1P95SurfaceErrorVoxels <= 1.0;
bStep2fGridPassed &= NewShape.Grid.PitchCorrelationPeak <= 0.40 bStep2fGridPassed &= NewShape.Grid.PitchCorrelationPeak <= 0.40
&& NewShape.Grid.PitchPeakExcess <= 0.08; && NewShape.Grid.PitchPeakExcess <= 0.08;
bStep2fStructurePassed &= NewSolidFraction >= 0.25 && NewSolidFraction <= 0.88 bStep2fStructurePassed &= NewSolidFraction >= 0.25 && NewSolidFraction <= 0.88
@@ -5565,6 +5881,39 @@ int32 RunStep2f(const FString& Params)
const bool bDeterminism = HardestHash == RepeatHash && HardestHash == OneWorkerHash const bool bDeterminism = HardestHash == RepeatHash && HardestHash == OneWorkerHash
&& Step2eSameWorkerRun(HardestTiming, RepeatTiming) && Step2eSameWorkerRun(HardestTiming, RepeatTiming)
&& Step2eSameWorkerRun(HardestTiming, OneWorkerTiming); && Step2eSameWorkerRun(HardestTiming, OneWorkerTiming);
FIntVector BrickParityOrigin(-28, -28,
FMath::FloorToInt(static_cast<float>(CenterZ) / CHUNK_SIZE) * CHUNK_SIZE + 4);
const FIntVector BrickParitySize(
VoxelForgeRedefinedDensityField::ParameterizedBrickSamples,
VoxelForgeRedefinedDensityField::ParameterizedBrickSamples, 16);
VoxelForgeRedefinedDensityField::FParameterizedBrickContext BrickParityContext;
TArray<float> BrickParityDetail;
TArray<float> BrickParityColumn;
TArray<float> BrickParityValues;
VoxelForgeRedefinedDensityField::EvaluateParameterizedDensityBrick(
*World.Generator, BrickParityOrigin, BrickParitySize, 1.0f,
BrickParityContext, BrickParityDetail, BrickParityColumn, BrickParityValues);
int64 BrickScalarMismatchCount = 0;
for (int32 Z = 0; Z < BrickParitySize.Z; ++Z)
for (int32 Y = 0; Y < BrickParitySize.Y; ++Y)
for (int32 X = 0; X < BrickParitySize.X; ++X)
{
const float ScalarValue = VoxelForgeRedefinedDensityField::EvaluateParameterized(
*World.Generator, static_cast<float>(BrickParityOrigin.X + X),
static_cast<float>(BrickParityOrigin.Y + Y),
static_cast<float>(BrickParityOrigin.Z + Z));
const float BrickValue = BrickParityValues.IsValidIndex(
(Z * BrickParitySize.Y + Y) * BrickParitySize.X + X)
? BrickParityValues[(Z * BrickParitySize.Y + Y) * BrickParitySize.X + X]
: FLT_MAX;
BrickScalarMismatchCount += FMemory::Memcmp(
&ScalarValue, &BrickValue, sizeof(float)) == 0 ? 0 : 1;
}
const bool bParameterizedBrickScalarParity =
BrickParityValues.Num() == BrickParitySize.X * BrickParitySize.Y * BrickParitySize.Z
&& BrickScalarMismatchCount == 0;
const FStep2dBrickProofAudit ProofAudit = AuditStep2dBrickProofs( const FStep2dBrickProofAudit ProofAudit = AuditStep2dBrickProofs(
*World.Generator, Origin, Dimension, true); *World.Generator, Origin, Dimension, true);
const bool bBoundAudit = ProofAudit.CorrectlySkippedBricksAudited > 0 const bool bBoundAudit = ProofAudit.CorrectlySkippedBricksAudited > 0
@@ -5600,7 +5949,8 @@ int32 RunStep2f(const FString& Params)
&& bStep2fGridPassed && bStep2fStructurePassed; && bStep2fGridPassed && bStep2fStructurePassed;
const bool bSpeedPass = Workers == 6 && WorstStep2fMs <= 600.0; const bool bSpeedPass = Workers == 6 && WorstStep2fMs <= 600.0;
const bool bPassed = bAllGeometryGatesRejectStep2e && bAllGeometryGatesPassStep2f const bool bPassed = bAllGeometryGatesRejectStep2e && bAllGeometryGatesPassStep2f
&& bSpeedPass && bDeterminism && bBoundAudit && bUndersizedBoundCaught && bSpeedPass && bDeterminism && bParameterizedBrickScalarParity
&& bBoundAudit && bUndersizedBoundCaught
&& bBoundaryPass && bDeliberateSeamCaught && HardestTiming.NonFiniteSamples == 0; && bBoundaryPass && bDeliberateSeamCaught && HardestTiming.NonFiniteSamples == 0;
TSharedRef<FJsonObject> Gates = MakeShared<FJsonObject>(); TSharedRef<FJsonObject> Gates = MakeShared<FJsonObject>();
@@ -5621,7 +5971,7 @@ int32 RunStep2f(const FString& Params)
Step2eCrumbNumbers->SetNumberField(TEXT("crumbs_below_1m3_max"), WorstStep2eCrumbs); Step2eCrumbNumbers->SetNumberField(TEXT("crumbs_below_1m3_max"), WorstStep2eCrumbs);
TSharedRef<FJsonObject> Step2fCrumbNumbers = MakeShared<FJsonObject>(); TSharedRef<FJsonObject> Step2fCrumbNumbers = MakeShared<FJsonObject>();
Step2fCrumbNumbers->SetNumberField(TEXT("crumbs_below_1m3_max"), WorstStep2fCrumbs); Step2fCrumbNumbers->SetNumberField(TEXT("crumbs_below_1m3_max"), WorstStep2fCrumbs);
AddGate(TEXT("crumbs"), TEXT("zero 25 cm-grid components smaller than 64 voxels, measured in a 64-sample-padded volume and restricted to components intersecting the 259-cube"), AddGate(TEXT("crumbs"), TEXT("zero 12.5 cm-grid components smaller than 512 voxels (1 m3), measured in a 128-sample-padded volume and restricted to components intersecting the 259-cube"),
bStep2eCrumbsRejected, bStep2fCrumbGatePassed, Step2eCrumbNumbers, Step2fCrumbNumbers); bStep2eCrumbsRejected, bStep2fCrumbGatePassed, Step2eCrumbNumbers, Step2fCrumbNumbers);
TSharedRef<FJsonObject> Step2eFlakeNumbers = MakeShared<FJsonObject>(); TSharedRef<FJsonObject> Step2eFlakeNumbers = MakeShared<FJsonObject>();
Step2eFlakeNumbers->SetNumberField(TEXT("max_dihedral_share_over_45deg"), WorstStep2eDihedralShare); Step2eFlakeNumbers->SetNumberField(TEXT("max_dihedral_share_over_45deg"), WorstStep2eDihedralShare);
@@ -5637,13 +5987,13 @@ int32 RunStep2f(const FString& Params)
TSharedRef<FJsonObject> Step2fDetailNumbers = MakeShared<FJsonObject>(); TSharedRef<FJsonObject> Step2fDetailNumbers = MakeShared<FJsonObject>();
Step2fDetailNumbers->SetNumberField(TEXT("max_lattice_scale_energy_ratio"), WorstStep2fDetailEnergy); Step2fDetailNumbers->SetNumberField(TEXT("max_lattice_scale_energy_ratio"), WorstStep2fDetailEnergy);
Step2fDetailNumbers->SetNumberField(TEXT("min_detail_rms"), BestStep2fDetailRms); Step2fDetailNumbers->SetNumberField(TEXT("min_detail_rms"), BestStep2fDetailRms);
AddGate(TEXT("band_limited_detail"), TEXT("detail RMS >= 0.40 and normalized lag-1 difference energy <= 0.20"), AddGate(TEXT("band_limited_detail"), TEXT("detail RMS >= 0.80 voxel units; normalized 25 cm difference energy <= 0.20"),
bStep2eBandLimitRejected, bStep2fBandLimitPassed, Step2eDetailNumbers, Step2fDetailNumbers); bStep2eBandLimitRejected, bStep2fBandLimitPassed, Step2eDetailNumbers, Step2fDetailNumbers);
TSharedRef<FJsonObject> Step2eLodNumbers = MakeShared<FJsonObject>(); TSharedRef<FJsonObject> Step2eLodNumbers = MakeShared<FJsonObject>();
Step2eLodNumbers->SetNumberField(TEXT("max_l1_p95_surface_distance_voxels"), WorstStep2eLodDistance); Step2eLodNumbers->SetNumberField(TEXT("max_l1_p95_surface_distance_voxels"), WorstStep2eLodDistance);
TSharedRef<FJsonObject> Step2fLodNumbers = MakeShared<FJsonObject>(); TSharedRef<FJsonObject> Step2fLodNumbers = MakeShared<FJsonObject>();
Step2fLodNumbers->SetNumberField(TEXT("max_l1_p95_surface_distance_voxels"), WorstStep2fLodDistance); Step2fLodNumbers->SetNumberField(TEXT("max_l1_p95_surface_distance_voxels"), WorstStep2fLodDistance);
AddGate(TEXT("lod_stability"), TEXT("L1 mesh p95 estimated distance from the L0 isosurface <= 0.5 lattice samples"), AddGate(TEXT("lod_stability"), TEXT("L1 mesh p95 estimated distance from the L0 isosurface <= 1 lattice sample (12.5 cm)"),
bStep2eLodRejected, bStep2fLodPassed, Step2eLodNumbers, Step2fLodNumbers); bStep2eLodRejected, bStep2fLodPassed, Step2eLodNumbers, Step2fLodNumbers);
TSharedRef<FJsonObject> Step2eGridNumbers = MakeShared<FJsonObject>(); TSharedRef<FJsonObject> Step2eGridNumbers = MakeShared<FJsonObject>();
Step2eGridNumbers->SetNumberField(TEXT("max_pitch_autocorrelation"), WorstStep2ePitchCorrelation); Step2eGridNumbers->SetNumberField(TEXT("max_pitch_autocorrelation"), WorstStep2ePitchCorrelation);
@@ -5687,19 +6037,19 @@ int32 RunStep2f(const FString& Params)
Report->SetStringField(TEXT("field_definition"), Report->SetStringField(TEXT("field_definition"),
TEXT("Rooms use strongly jittered cells with per-room radii, axis ratios and heights. " TEXT("Rooms use strongly jittered cells with per-room radii, axis ratios and heights. "
"A conservative nearest-neighbor cap leaves a solid web between chambers; a rooted " "A conservative nearest-neighbor cap leaves a solid web between chambers; a rooted "
"tree of narrow tunnels and capped shafts joins them. Sparse ledges, pillars, " "tree of winding, rounded tubes with hash-varied widths and capped shafts joins them. Sparse ledges, pillars, "
"stalactites and large hanging masses remain per-cell rolls. The point and " "stalactites and large hanging masses remain per-cell rolls. The point and "
"brick detail samplers share independently rotated 48/24/12-sample bands with a " "brick detail samplers share independently rotated 96/48/24-sample bands with a "
"twelve-sample shortest period and a smooth ridge response; their final-field " "24-sample shortest period and a smooth ridge response; their final-field "
"detail supremum is 3.822 units.")); "detail supremum is 7.644 voxel units."));
Report->SetStringField(TEXT("component_method"), Report->SetStringField(TEXT("component_method"),
TEXT("26-neighbor connected components are counted on the exact 25 cm sign lattice. " TEXT("26-neighbor connected components are counted on the exact 12.5 cm sign lattice. "
"Raw components below 64 samples (< 1 m3) are reported, then a 64-sample pad is " "Raw components below 512 samples (< 1 m3) are reported, then a 128-sample (16 m) pad is "
"evaluated around each 259-cube and only components intersecting the original cube " "evaluated around each 259-cube and only components intersecting the original cube "
"are counted again; this prevents a clipped large component at the ring edge from " "are counted again; this prevents a clipped large component at the ring edge from "
"being called a crumb. The one-meter downsampled air component count is also reported.")); "being called a crumb. The one-meter downsampled air component count is also reported."));
Report->SetStringField(TEXT("mesh_method"), Report->SetStringField(TEXT("mesh_method"),
TEXT("Eight level-0 tiles and one level-1 tile cover the same 64-sample cube selected " TEXT("Sixty-four level-0 tiles and eight level-1 tiles cover the same 128-sample (16 m) cube selected "
"for maximum coarse-sampled surface crossings. " "for maximum coarse-sampled surface crossings. "
"The share of manifold triangle edges whose geometric dihedral exceeds 45 degrees " "The share of manifold triangle edges whose geometric dihedral exceeds 45 degrees "
"and its p95 are measured. Each level-1 vertex is compared with nearby level-0 " "and its p95 are measured. Each level-1 vertex is compared with nearby level-0 "
@@ -5787,6 +6137,12 @@ int32 RunStep2f(const FString& Params)
Existing->SetNumberField(TEXT("brick_boundary_gradient_jump_rms"), BoundaryStats.BrickJumpRms); Existing->SetNumberField(TEXT("brick_boundary_gradient_jump_rms"), BoundaryStats.BrickJumpRms);
Existing->SetNumberField(TEXT("brick_interior_gradient_jump_rms"), BoundaryStats.InteriorJumpRms); Existing->SetNumberField(TEXT("brick_interior_gradient_jump_rms"), BoundaryStats.InteriorJumpRms);
Existing->SetBoolField(TEXT("one_and_six_worker_determinism"), bDeterminism); Existing->SetBoolField(TEXT("one_and_six_worker_determinism"), bDeterminism);
Existing->SetBoolField(TEXT("brick_path_matches_scalar_bit_exact"),
bParameterizedBrickScalarParity);
Existing->SetNumberField(TEXT("brick_scalar_parity_samples"),
BrickParitySize.X * BrickParitySize.Y * BrickParitySize.Z);
Existing->SetNumberField(TEXT("brick_scalar_parity_mismatches"),
static_cast<double>(BrickScalarMismatchCount));
Existing->SetStringField(TEXT("six_worker_hash"), FString::Printf( Existing->SetStringField(TEXT("six_worker_hash"), FString::Printf(
TEXT("%016llx"), static_cast<unsigned long long>(HardestHash))); TEXT("%016llx"), static_cast<unsigned long long>(HardestHash)));
Existing->SetStringField(TEXT("repeat_six_worker_hash"), FString::Printf( Existing->SetStringField(TEXT("repeat_six_worker_hash"), FString::Printf(
@@ -8896,17 +9252,17 @@ FString BuildManifestJson(
WriteJsonVector(*Writer, TEXT("render_camera_voxels"), Arguments.RenderCamera, 1.0f); WriteJsonVector(*Writer, TEXT("render_camera_voxels"), Arguments.RenderCamera, 1.0f);
} }
WriteJsonIntVector(*Writer, TEXT("region_origin_voxels"), Export.RegionOrigin); WriteJsonIntVector(*Writer, TEXT("region_origin_voxels"), Export.RegionOrigin);
WriteJsonBounds(*Writer, TEXT("region_bounds_voxels"), Export.RegionOrigin, Export.RegionSize, 0.25f); WriteJsonBounds(*Writer, TEXT("region_bounds_voxels"), Export.RegionOrigin, Export.RegionSize, VOXEL_SIZE / 100.0f);
Writer->WriteObjectStart(TEXT("region_bounds_metres")); Writer->WriteObjectStart(TEXT("region_bounds_metres"));
Writer->WriteObjectStart(TEXT("min")); Writer->WriteObjectStart(TEXT("min"));
Writer->WriteValue(TEXT("x"), static_cast<double>(Export.RegionOrigin.X) * 0.25); Writer->WriteValue(TEXT("x"), static_cast<double>(Export.RegionOrigin.X) * VOXEL_SIZE / 100.0);
Writer->WriteValue(TEXT("y"), static_cast<double>(Export.RegionOrigin.Y) * 0.25); Writer->WriteValue(TEXT("y"), static_cast<double>(Export.RegionOrigin.Y) * VOXEL_SIZE / 100.0);
Writer->WriteValue(TEXT("z"), static_cast<double>(Export.RegionOrigin.Z) * 0.25); Writer->WriteValue(TEXT("z"), static_cast<double>(Export.RegionOrigin.Z) * VOXEL_SIZE / 100.0);
Writer->WriteObjectEnd(); Writer->WriteObjectEnd();
Writer->WriteObjectStart(TEXT("max_exclusive")); Writer->WriteObjectStart(TEXT("max_exclusive"));
Writer->WriteValue(TEXT("x"), static_cast<double>(Export.RegionOrigin.X + Export.RegionSize) * 0.25); Writer->WriteValue(TEXT("x"), static_cast<double>(Export.RegionOrigin.X + Export.RegionSize) * VOXEL_SIZE / 100.0);
Writer->WriteValue(TEXT("y"), static_cast<double>(Export.RegionOrigin.Y + Export.RegionSize) * 0.25); Writer->WriteValue(TEXT("y"), static_cast<double>(Export.RegionOrigin.Y + Export.RegionSize) * VOXEL_SIZE / 100.0);
Writer->WriteValue(TEXT("z"), static_cast<double>(Export.RegionOrigin.Z + Export.RegionSize) * 0.25); Writer->WriteValue(TEXT("z"), static_cast<double>(Export.RegionOrigin.Z + Export.RegionSize) * VOXEL_SIZE / 100.0);
Writer->WriteObjectEnd(); Writer->WriteObjectEnd();
Writer->WriteValue(TEXT("units"), TEXT("metres")); Writer->WriteValue(TEXT("units"), TEXT("metres"));
Writer->WriteObjectEnd(); Writer->WriteObjectEnd();
@@ -10114,11 +10470,11 @@ bool RunTunnelCoreProbe(
"measured=%.6g_voxels/%d"), "measured=%.6g_voxels/%d"),
*Axis->Name, SampleCount, Axis->ValidFinalCrossings, *Axis->Name, SampleCount, Axis->ValidFinalCrossings,
Axis->FinalPeriodicAmplitudeVoxels, Axis->FinalPeriodicAmplitudeVoxels,
Axis->FinalPeriodicAmplitudeVoxels * 25.0f, Axis->FinalPeriodicAmplitudeVoxels * VOXEL_SIZE,
Axis->FinalPeriodicPeriodVoxels, Axis->FinalPeriodicPeriodVoxels,
Axis->FinalPeriodicMeasuredLengthVoxels, Axis->FinalPeriodicMeasuredSamples, Axis->FinalPeriodicMeasuredLengthVoxels, Axis->FinalPeriodicMeasuredSamples,
Axis->ValidCoreCrossings, Axis->CorePeriodicAmplitudeVoxels, Axis->ValidCoreCrossings, Axis->CorePeriodicAmplitudeVoxels,
Axis->CorePeriodicAmplitudeVoxels * 25.0f, Axis->CorePeriodicAmplitudeVoxels * VOXEL_SIZE,
Axis->CorePeriodicPeriodVoxels, Axis->CorePeriodicPeriodVoxels,
Axis->CorePeriodicMeasuredLengthVoxels, Axis->CorePeriodicMeasuredSamples); Axis->CorePeriodicMeasuredLengthVoxels, Axis->CorePeriodicMeasuredSamples);
} }
@@ -10409,11 +10765,11 @@ bool RunTunnelCoreProbe(
Mouth.CoreBottomSampleCount, Mouth.FinalSolidSampleCount, Mouth.CoreBottomSampleCount, Mouth.FinalSolidSampleCount,
Mouth.CoreInducedSolidSampleCount, Mouth.CoreInducedSolidSampleCount,
Mouth.MaxWallPeriodicAmplitudeVoxels, Mouth.MaxWallPeriodicAmplitudeVoxels,
Mouth.MaxWallPeriodicAmplitudeVoxels * 25.0f, Mouth.MaxWallPeriodicAmplitudeVoxels * VOXEL_SIZE,
Mouth.MaxFloorPeriodicAmplitudeVoxels, Mouth.MaxFloorPeriodicAmplitudeVoxels,
Mouth.MaxFloorPeriodicAmplitudeVoxels * 25.0f, Mouth.MaxFloorPeriodicAmplitudeVoxels * VOXEL_SIZE,
Mouth.MaxLocalMedianFloorPeriodicAmplitudeVoxels, Mouth.MaxLocalMedianFloorPeriodicAmplitudeVoxels,
Mouth.MaxLocalMedianFloorPeriodicAmplitudeVoxels * 25.0f, Mouth.MaxLocalMedianFloorPeriodicAmplitudeVoxels * VOXEL_SIZE,
Mouth.LateralWindowVoxels, Mouth.LateralStepVoxels, *MouthPath); Mouth.LateralWindowVoxels, Mouth.LateralStepVoxels, *MouthPath);
} }
@@ -10459,12 +10815,12 @@ void WriteTunnelCoreProbeAxis(
Writer.WriteValue(TEXT("final_min_crossing_voxels"), static_cast<double>(Axis.FinalMinCrossingVoxels)); Writer.WriteValue(TEXT("final_min_crossing_voxels"), static_cast<double>(Axis.FinalMinCrossingVoxels));
Writer.WriteValue(TEXT("final_max_crossing_voxels"), static_cast<double>(Axis.FinalMaxCrossingVoxels)); Writer.WriteValue(TEXT("final_max_crossing_voxels"), static_cast<double>(Axis.FinalMaxCrossingVoxels));
Writer.WriteValue(TEXT("core_periodic_amplitude_voxels"), static_cast<double>(Axis.CorePeriodicAmplitudeVoxels)); Writer.WriteValue(TEXT("core_periodic_amplitude_voxels"), static_cast<double>(Axis.CorePeriodicAmplitudeVoxels));
Writer.WriteValue(TEXT("core_periodic_amplitude_cm"), static_cast<double>(Axis.CorePeriodicAmplitudeVoxels) * 25.0); Writer.WriteValue(TEXT("core_periodic_amplitude_cm"), static_cast<double>(Axis.CorePeriodicAmplitudeVoxels) * VOXEL_SIZE);
Writer.WriteValue(TEXT("core_periodic_period_voxels"), static_cast<double>(Axis.CorePeriodicPeriodVoxels)); Writer.WriteValue(TEXT("core_periodic_period_voxels"), static_cast<double>(Axis.CorePeriodicPeriodVoxels));
Writer.WriteValue(TEXT("core_periodic_measured_samples"), Axis.CorePeriodicMeasuredSamples); Writer.WriteValue(TEXT("core_periodic_measured_samples"), Axis.CorePeriodicMeasuredSamples);
Writer.WriteValue(TEXT("core_periodic_measured_length_voxels"), static_cast<double>(Axis.CorePeriodicMeasuredLengthVoxels)); Writer.WriteValue(TEXT("core_periodic_measured_length_voxels"), static_cast<double>(Axis.CorePeriodicMeasuredLengthVoxels));
Writer.WriteValue(TEXT("final_periodic_amplitude_voxels"), static_cast<double>(Axis.FinalPeriodicAmplitudeVoxels)); Writer.WriteValue(TEXT("final_periodic_amplitude_voxels"), static_cast<double>(Axis.FinalPeriodicAmplitudeVoxels));
Writer.WriteValue(TEXT("final_periodic_amplitude_cm"), static_cast<double>(Axis.FinalPeriodicAmplitudeVoxels) * 25.0); Writer.WriteValue(TEXT("final_periodic_amplitude_cm"), static_cast<double>(Axis.FinalPeriodicAmplitudeVoxels) * VOXEL_SIZE);
Writer.WriteValue(TEXT("final_periodic_period_voxels"), static_cast<double>(Axis.FinalPeriodicPeriodVoxels)); Writer.WriteValue(TEXT("final_periodic_period_voxels"), static_cast<double>(Axis.FinalPeriodicPeriodVoxels));
Writer.WriteValue(TEXT("final_periodic_measured_samples"), Axis.FinalPeriodicMeasuredSamples); Writer.WriteValue(TEXT("final_periodic_measured_samples"), Axis.FinalPeriodicMeasuredSamples);
Writer.WriteValue(TEXT("final_periodic_measured_length_voxels"), static_cast<double>(Axis.FinalPeriodicMeasuredLengthVoxels)); Writer.WriteValue(TEXT("final_periodic_measured_length_voxels"), static_cast<double>(Axis.FinalPeriodicMeasuredLengthVoxels));
@@ -10482,12 +10838,12 @@ void WriteTunnelMouthProbeAxis(
Writer.WriteValue(TEXT("min_crossing_voxels"), static_cast<double>(Axis.MinCrossingVoxels)); Writer.WriteValue(TEXT("min_crossing_voxels"), static_cast<double>(Axis.MinCrossingVoxels));
Writer.WriteValue(TEXT("max_crossing_voxels"), static_cast<double>(Axis.MaxCrossingVoxels)); Writer.WriteValue(TEXT("max_crossing_voxels"), static_cast<double>(Axis.MaxCrossingVoxels));
Writer.WriteValue(TEXT("periodic_amplitude_voxels"), static_cast<double>(Axis.PeriodicAmplitudeVoxels)); Writer.WriteValue(TEXT("periodic_amplitude_voxels"), static_cast<double>(Axis.PeriodicAmplitudeVoxels));
Writer.WriteValue(TEXT("periodic_amplitude_cm"), static_cast<double>(Axis.PeriodicAmplitudeVoxels) * 25.0); Writer.WriteValue(TEXT("periodic_amplitude_cm"), static_cast<double>(Axis.PeriodicAmplitudeVoxels) * VOXEL_SIZE);
Writer.WriteValue(TEXT("periodic_period_voxels"), static_cast<double>(Axis.PeriodicPeriodVoxels)); Writer.WriteValue(TEXT("periodic_period_voxels"), static_cast<double>(Axis.PeriodicPeriodVoxels));
Writer.WriteValue(TEXT("periodic_measured_samples"), Axis.PeriodicMeasuredSamples); Writer.WriteValue(TEXT("periodic_measured_samples"), Axis.PeriodicMeasuredSamples);
Writer.WriteValue(TEXT("periodic_measured_length_voxels"), static_cast<double>(Axis.PeriodicMeasuredLengthVoxels)); Writer.WriteValue(TEXT("periodic_measured_length_voxels"), static_cast<double>(Axis.PeriodicMeasuredLengthVoxels));
Writer.WriteValue(TEXT("local_median_periodic_amplitude_voxels"), static_cast<double>(Axis.LocalMedianPeriodicAmplitudeVoxels)); Writer.WriteValue(TEXT("local_median_periodic_amplitude_voxels"), static_cast<double>(Axis.LocalMedianPeriodicAmplitudeVoxels));
Writer.WriteValue(TEXT("local_median_periodic_amplitude_cm"), static_cast<double>(Axis.LocalMedianPeriodicAmplitudeVoxels) * 25.0); Writer.WriteValue(TEXT("local_median_periodic_amplitude_cm"), static_cast<double>(Axis.LocalMedianPeriodicAmplitudeVoxels) * VOXEL_SIZE);
Writer.WriteValue(TEXT("local_median_periodic_period_voxels"), static_cast<double>(Axis.LocalMedianPeriodicPeriodVoxels)); Writer.WriteValue(TEXT("local_median_periodic_period_voxels"), static_cast<double>(Axis.LocalMedianPeriodicPeriodVoxels));
Writer.WriteObjectEnd(); Writer.WriteObjectEnd();
} }
@@ -10676,18 +11032,18 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("final_solid_sample_count"), Output.TunnelCoreProbe.Mouth.FinalSolidSampleCount); Writer->WriteValue(TEXT("final_solid_sample_count"), Output.TunnelCoreProbe.Mouth.FinalSolidSampleCount);
Writer->WriteValue(TEXT("core_induced_solid_sample_count"), Output.TunnelCoreProbe.Mouth.CoreInducedSolidSampleCount); Writer->WriteValue(TEXT("core_induced_solid_sample_count"), Output.TunnelCoreProbe.Mouth.CoreInducedSolidSampleCount);
Writer->WriteValue(TEXT("max_wall_periodic_amplitude_voxels"), static_cast<double>(Output.TunnelCoreProbe.Mouth.MaxWallPeriodicAmplitudeVoxels)); Writer->WriteValue(TEXT("max_wall_periodic_amplitude_voxels"), static_cast<double>(Output.TunnelCoreProbe.Mouth.MaxWallPeriodicAmplitudeVoxels));
Writer->WriteValue(TEXT("max_wall_periodic_amplitude_cm"), static_cast<double>(Output.TunnelCoreProbe.Mouth.MaxWallPeriodicAmplitudeVoxels) * 25.0); Writer->WriteValue(TEXT("max_wall_periodic_amplitude_cm"), static_cast<double>(Output.TunnelCoreProbe.Mouth.MaxWallPeriodicAmplitudeVoxels) * VOXEL_SIZE);
Writer->WriteValue(TEXT("max_floor_periodic_amplitude_voxels"), static_cast<double>(Output.TunnelCoreProbe.Mouth.MaxFloorPeriodicAmplitudeVoxels)); Writer->WriteValue(TEXT("max_floor_periodic_amplitude_voxels"), static_cast<double>(Output.TunnelCoreProbe.Mouth.MaxFloorPeriodicAmplitudeVoxels));
Writer->WriteValue(TEXT("max_floor_periodic_amplitude_cm"), static_cast<double>(Output.TunnelCoreProbe.Mouth.MaxFloorPeriodicAmplitudeVoxels) * 25.0); Writer->WriteValue(TEXT("max_floor_periodic_amplitude_cm"), static_cast<double>(Output.TunnelCoreProbe.Mouth.MaxFloorPeriodicAmplitudeVoxels) * VOXEL_SIZE);
Writer->WriteValue(TEXT("max_local_median_wall_periodic_amplitude_voxels"), static_cast<double>(Output.TunnelCoreProbe.Mouth.MaxLocalMedianWallPeriodicAmplitudeVoxels)); Writer->WriteValue(TEXT("max_local_median_wall_periodic_amplitude_voxels"), static_cast<double>(Output.TunnelCoreProbe.Mouth.MaxLocalMedianWallPeriodicAmplitudeVoxels));
Writer->WriteValue(TEXT("max_local_median_wall_periodic_amplitude_cm"), static_cast<double>(Output.TunnelCoreProbe.Mouth.MaxLocalMedianWallPeriodicAmplitudeVoxels) * 25.0); Writer->WriteValue(TEXT("max_local_median_wall_periodic_amplitude_cm"), static_cast<double>(Output.TunnelCoreProbe.Mouth.MaxLocalMedianWallPeriodicAmplitudeVoxels) * VOXEL_SIZE);
Writer->WriteValue(TEXT("max_local_median_floor_periodic_amplitude_voxels"), static_cast<double>(Output.TunnelCoreProbe.Mouth.MaxLocalMedianFloorPeriodicAmplitudeVoxels)); Writer->WriteValue(TEXT("max_local_median_floor_periodic_amplitude_voxels"), static_cast<double>(Output.TunnelCoreProbe.Mouth.MaxLocalMedianFloorPeriodicAmplitudeVoxels));
Writer->WriteValue(TEXT("max_local_median_floor_periodic_amplitude_cm"), static_cast<double>(Output.TunnelCoreProbe.Mouth.MaxLocalMedianFloorPeriodicAmplitudeVoxels) * 25.0); Writer->WriteValue(TEXT("max_local_median_floor_periodic_amplitude_cm"), static_cast<double>(Output.TunnelCoreProbe.Mouth.MaxLocalMedianFloorPeriodicAmplitudeVoxels) * VOXEL_SIZE);
Writer->WriteValue(TEXT("mouth_periodic_detrend"), TEXT("degree-six smooth trend over the complete longest contiguous run; legacy local-median retained for audit")); Writer->WriteValue(TEXT("mouth_periodic_detrend"), TEXT("degree-six smooth trend over the complete longest contiguous run; legacy local-median retained for audit"));
Writer->WriteValue(TEXT("periodic_acceptance_cm"), 2.0); Writer->WriteValue(TEXT("periodic_acceptance_cm"), 2.0);
Writer->WriteValue(TEXT("periodic_acceptance_pass"), Writer->WriteValue(TEXT("periodic_acceptance_pass"),
Output.TunnelCoreProbe.Mouth.MaxWallPeriodicAmplitudeVoxels * 25.0f <= 2.0f Output.TunnelCoreProbe.Mouth.MaxWallPeriodicAmplitudeVoxels * VOXEL_SIZE <= 2.0f
&& Output.TunnelCoreProbe.Mouth.MaxFloorPeriodicAmplitudeVoxels * 25.0f <= 2.0f); && Output.TunnelCoreProbe.Mouth.MaxFloorPeriodicAmplitudeVoxels * VOXEL_SIZE <= 2.0f);
Writer->WriteArrayStart(TEXT("endpoints")); Writer->WriteArrayStart(TEXT("endpoints"));
for (const FExploreTunnelMouthProbeEndpoint& Endpoint : Output.TunnelCoreProbe.Mouth.Endpoints) for (const FExploreTunnelMouthProbeEndpoint& Endpoint : Output.TunnelCoreProbe.Mouth.Endpoints)
{ {
@@ -10862,15 +11218,15 @@ FString BuildExploreJson(
Writer->WriteObjectEnd(); Writer->WriteObjectEnd();
Writer->WriteObjectStart(TEXT("world")); Writer->WriteObjectStart(TEXT("world"));
Writer->WriteValue(TEXT("voxel_size_m"), 0.25); Writer->WriteValue(TEXT("voxel_size_m"), VOXEL_SIZE / 100.0);
Writer->WriteValue(TEXT("world_radius_voxels"), 0); Writer->WriteValue(TEXT("world_radius_voxels"), 0);
Writer->WriteValue(TEXT("lateral_regions_enabled"), false); Writer->WriteValue(TEXT("lateral_regions_enabled"), false);
Writer->WriteValue(TEXT("diff_layer_mutated"), false); Writer->WriteValue(TEXT("diff_layer_mutated"), false);
Writer->WriteValue(TEXT("target_strate_slot"), Arguments.Slot); Writer->WriteValue(TEXT("target_strate_slot"), Arguments.Slot);
Writer->WriteValue(TEXT("target_bottom_voxel_z"), World.TargetBottomWorldZ); Writer->WriteValue(TEXT("target_bottom_voxel_z"), World.TargetBottomWorldZ);
Writer->WriteValue(TEXT("target_top_voxel_z_exclusive"), World.TargetTopWorldZ); Writer->WriteValue(TEXT("target_top_voxel_z_exclusive"), World.TargetTopWorldZ);
Writer->WriteValue(TEXT("target_bottom_m"), static_cast<double>(World.TargetBottomWorldZ) * 0.25); Writer->WriteValue(TEXT("target_bottom_m"), static_cast<double>(World.TargetBottomWorldZ) * VOXEL_SIZE / 100.0);
Writer->WriteValue(TEXT("target_top_m_exclusive"), static_cast<double>(World.TargetTopWorldZ) * 0.25); Writer->WriteValue(TEXT("target_top_m_exclusive"), static_cast<double>(World.TargetTopWorldZ) * VOXEL_SIZE / 100.0);
Writer->WriteValue(TEXT("target_boundary_seal_thickness_voxels"), Writer->WriteValue(TEXT("target_boundary_seal_thickness_voxels"),
static_cast<double>(World.TargetBoundarySealThickness)); static_cast<double>(World.TargetBoundarySealThickness));
WritePlayerDimensions(*Writer, TEXT("player_dimensions")); WritePlayerDimensions(*Writer, TEXT("player_dimensions"));
@@ -10977,12 +11333,12 @@ FString BuildExploreJson(
if (Output.Walk.bHasArrival) if (Output.Walk.bHasArrival)
{ {
WriteJsonVector(*Writer, TEXT("arrival_mouth_voxels"), Output.Walk.ArrivalVoxels, 1.0f); WriteJsonVector(*Writer, TEXT("arrival_mouth_voxels"), Output.Walk.ArrivalVoxels, 1.0f);
WriteJsonVector(*Writer, TEXT("arrival_mouth_metres"), Output.Walk.ArrivalVoxels, 0.25f); WriteJsonVector(*Writer, TEXT("arrival_mouth_metres"), Output.Walk.ArrivalVoxels, VOXEL_SIZE / 100.0f);
} }
if (Output.Walk.bHasDeparture) if (Output.Walk.bHasDeparture)
{ {
WriteJsonVector(*Writer, TEXT("departure_mouth_voxels"), Output.Walk.DepartureVoxels, 1.0f); WriteJsonVector(*Writer, TEXT("departure_mouth_voxels"), Output.Walk.DepartureVoxels, 1.0f);
WriteJsonVector(*Writer, TEXT("departure_mouth_metres"), Output.Walk.DepartureVoxels, 0.25f); WriteJsonVector(*Writer, TEXT("departure_mouth_metres"), Output.Walk.DepartureVoxels, VOXEL_SIZE / 100.0f);
} }
Writer->WriteValue(TEXT("connectivity_result"), ConnectivityResultName(Report.Result)); Writer->WriteValue(TEXT("connectivity_result"), ConnectivityResultName(Report.Result));
Writer->WriteValue(TEXT("connectivity_result_code"), static_cast<int32>(Report.Result)); Writer->WriteValue(TEXT("connectivity_result_code"), static_cast<int32>(Report.Result));
@@ -11042,13 +11398,13 @@ FString BuildExploreJson(
if (Report.bHasAgentFinalPosition) if (Report.bHasAgentFinalPosition)
{ {
WriteJsonVector(*Writer, TEXT("agent_final_position_voxels"), Report.AgentFinalVoxels, 1.0f); WriteJsonVector(*Writer, TEXT("agent_final_position_voxels"), Report.AgentFinalVoxels, 1.0f);
WriteJsonVector(*Writer, TEXT("agent_final_position_metres"), Report.AgentFinalVoxels, 0.25f); WriteJsonVector(*Writer, TEXT("agent_final_position_metres"), Report.AgentFinalVoxels, VOXEL_SIZE / 100.0f);
} }
Writer->WriteValue(TEXT("last_reached_position_available"), Report.bHasLastReachedPosition); Writer->WriteValue(TEXT("last_reached_position_available"), Report.bHasLastReachedPosition);
if (Report.bHasLastReachedPosition) if (Report.bHasLastReachedPosition)
{ {
WriteJsonVector(*Writer, TEXT("last_reached_position_voxels"), Report.LastReachedVoxels, 1.0f); WriteJsonVector(*Writer, TEXT("last_reached_position_voxels"), Report.LastReachedVoxels, 1.0f);
WriteJsonVector(*Writer, TEXT("last_reached_position_metres"), Report.LastReachedVoxels, 0.25f); WriteJsonVector(*Writer, TEXT("last_reached_position_metres"), Report.LastReachedVoxels, VOXEL_SIZE / 100.0f);
} }
Writer->WriteValue(TEXT("target_component_nearest_cell_available"), Report.bHasTargetComponentNearestCell); Writer->WriteValue(TEXT("target_component_nearest_cell_available"), Report.bHasTargetComponentNearestCell);
if (Report.bHasTargetComponentNearestCell) if (Report.bHasTargetComponentNearestCell)
@@ -11056,7 +11412,7 @@ FString BuildExploreJson(
WriteJsonVector(*Writer, TEXT("target_component_nearest_cell_voxels"), WriteJsonVector(*Writer, TEXT("target_component_nearest_cell_voxels"),
Report.TargetComponentNearestVoxels, 1.0f); Report.TargetComponentNearestVoxels, 1.0f);
WriteJsonVector(*Writer, TEXT("target_component_nearest_cell_metres"), WriteJsonVector(*Writer, TEXT("target_component_nearest_cell_metres"),
Report.TargetComponentNearestVoxels, 0.25f); Report.TargetComponentNearestVoxels, VOXEL_SIZE / 100.0f);
} }
Writer->WriteValue(TEXT("agent_to_target_component_gap_voxels"), Writer->WriteValue(TEXT("agent_to_target_component_gap_voxels"),
static_cast<double>(Report.AgentToTargetComponentGapVoxels)); static_cast<double>(Report.AgentToTargetComponentGapVoxels));
@@ -11148,7 +11504,7 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("mesh_file"), Output.Export.MeshFileName); Writer->WriteValue(TEXT("mesh_file"), Output.Export.MeshFileName);
Writer->WriteValue(TEXT("manifest_file"), Output.Export.ManifestFileName); Writer->WriteValue(TEXT("manifest_file"), Output.Export.ManifestFileName);
WriteJsonIntVector(*Writer, TEXT("region_origin_voxels"), Output.Export.RegionOrigin); WriteJsonIntVector(*Writer, TEXT("region_origin_voxels"), Output.Export.RegionOrigin);
WriteJsonBounds(*Writer, TEXT("region_bounds_voxels"), Output.Export.RegionOrigin, Output.Export.RegionSize, 0.25f); WriteJsonBounds(*Writer, TEXT("region_bounds_voxels"), Output.Export.RegionOrigin, Output.Export.RegionSize, VOXEL_SIZE / 100.0f);
Writer->WriteValue(TEXT("region_size_cells_per_axis"), Output.Export.RegionSize); Writer->WriteValue(TEXT("region_size_cells_per_axis"), Output.Export.RegionSize);
Writer->WriteValue(TEXT("canonical_mesher_tile_cells"), CHUNK_SIZE); Writer->WriteValue(TEXT("canonical_mesher_tile_cells"), CHUNK_SIZE);
Writer->WriteValue(TEXT("canonical_mesher_tile_count"), Output.Export.MesherTileCount); Writer->WriteValue(TEXT("canonical_mesher_tile_count"), Output.Export.MesherTileCount);
@@ -12965,6 +13321,10 @@ int32 UVoxelForgeExploreCommandlet::Main(const FString& Params)
{ {
return RunStep2f(Params); return RunStep2f(Params);
} }
if (FParse::Param(*Params, TEXT("step3scale")))
{
return RunStep3ScaleBenchmark(Params);
}
if (FParse::Param(*Params, TEXT("step2e"))) if (FParse::Param(*Params, TEXT("step2e")))
{ {
return RunStep2e(Params); return RunStep2e(Params);
@@ -181,24 +181,59 @@ namespace VoxelForgeRegionEval
TArray<FResearchBrick> Bricks; TArray<FResearchBrick> Bricks;
const FIntVector Last = OriginVoxels const FIntVector Last = OriginVoxels
+ FIntVector(Dimension - 1, Dimension - 1, Dimension - 1); + FIntVector(Dimension - 1, Dimension - 1, Dimension - 1);
const int32 ParameterizedBrickSize =
VoxelForgeRedefinedDensityField::ParameterizedBrickSamples;
const FIntVector ProofBrickSize = bParameterized const FIntVector ProofBrickSize = bParameterized
? FIntVector(4, 4, 4) : FIntVector(8, 8, 8); ? FIntVector(ParameterizedBrickSize, ParameterizedBrickSize, ParameterizedBrickSize)
: FIntVector(8, 8, 8);
// Parameterized tiles begin their lattice at chunkOrigin - 1. Their proof bricks start
// at world coordinates congruent to -1 modulo four; mirror that phase here so skips match
// the in-game tile path, including shared tile seams.
const int32 BrickAlignmentOffset = bParameterized ? -1 : 0;
const FIntVector FirstBrick( const FIntVector FirstBrick(
FMath::FloorToInt(static_cast<float>(OriginVoxels.X) / ProofBrickSize.X), FMath::FloorToInt(static_cast<float>(OriginVoxels.X - BrickAlignmentOffset) / ProofBrickSize.X),
FMath::FloorToInt(static_cast<float>(OriginVoxels.Y) / ProofBrickSize.Y), FMath::FloorToInt(static_cast<float>(OriginVoxels.Y - BrickAlignmentOffset) / ProofBrickSize.Y),
FMath::FloorToInt(static_cast<float>(OriginVoxels.Z) / ProofBrickSize.Z)); FMath::FloorToInt(static_cast<float>(OriginVoxels.Z - BrickAlignmentOffset) / ProofBrickSize.Z));
const FIntVector LastBrick( const FIntVector LastBrick(
FMath::FloorToInt(static_cast<float>(Last.X) / ProofBrickSize.X), FMath::FloorToInt(static_cast<float>(Last.X - BrickAlignmentOffset) / ProofBrickSize.X),
FMath::FloorToInt(static_cast<float>(Last.Y) / ProofBrickSize.Y), FMath::FloorToInt(static_cast<float>(Last.Y - BrickAlignmentOffset) / ProofBrickSize.Y),
FMath::FloorToInt(static_cast<float>(Last.Z) / ProofBrickSize.Z)); FMath::FloorToInt(static_cast<float>(Last.Z - BrickAlignmentOffset) / ProofBrickSize.Z));
Bricks.Reserve((LastBrick.X - FirstBrick.X + 1) const uint64 TotalProofBrickCount = static_cast<uint64>(LastBrick.X - FirstBrick.X + 1)
* (LastBrick.Y - FirstBrick.Y + 1) * (LastBrick.Z - FirstBrick.Z + 1)); * static_cast<uint64>(LastBrick.Y - FirstBrick.Y + 1)
* static_cast<uint64>(LastBrick.Z - FirstBrick.Z + 1);
const int32 BrickColumnsX = LastBrick.X - FirstBrick.X + 1;
const int32 BrickColumnsY = LastBrick.Y - FirstBrick.Y + 1;
if (bParameterized)
{
Bricks.Reserve(BrickColumnsX * BrickColumnsY);
for (int32 BY = FirstBrick.Y; BY <= LastBrick.Y; ++BY)
for (int32 BX = FirstBrick.X; BX <= LastBrick.X; ++BX)
{
const int32 BrickWorldMinX = BX * ProofBrickSize.X + BrickAlignmentOffset;
const int32 BrickWorldMinY = BY * ProofBrickSize.Y + BrickAlignmentOffset;
FResearchBrick& Brick = Bricks.AddDefaulted_GetRef();
Brick.Origin = FIntVector(
FMath::Max(OriginVoxels.X, BrickWorldMinX),
FMath::Max(OriginVoxels.Y, BrickWorldMinY), OriginVoxels.Z);
Brick.Size = FIntVector(
FMath::Min(BrickWorldMinX + ProofBrickSize.X, Last.X + 1) - Brick.Origin.X,
FMath::Min(BrickWorldMinY + ProofBrickSize.Y, Last.Y + 1) - Brick.Origin.Y,
Dimension);
Brick.Lattice = Brick.Origin - OriginVoxels;
Brick.SampleCount = Brick.Size.X * Brick.Size.Y * Brick.Size.Z;
}
}
else
{
Bricks.Reserve(BrickColumnsX * BrickColumnsY * (LastBrick.Z - FirstBrick.Z + 1));
for (int32 BZ = FirstBrick.Z; BZ <= LastBrick.Z; ++BZ) for (int32 BZ = FirstBrick.Z; BZ <= LastBrick.Z; ++BZ)
for (int32 BY = FirstBrick.Y; BY <= LastBrick.Y; ++BY) for (int32 BY = FirstBrick.Y; BY <= LastBrick.Y; ++BY)
for (int32 BX = FirstBrick.X; BX <= LastBrick.X; ++BX) for (int32 BX = FirstBrick.X; BX <= LastBrick.X; ++BX)
{ {
const FIntVector BrickWorldMin( const FIntVector BrickWorldMin(
BX * ProofBrickSize.X, BY * ProofBrickSize.Y, BZ * ProofBrickSize.Z); BX * ProofBrickSize.X + BrickAlignmentOffset,
BY * ProofBrickSize.Y + BrickAlignmentOffset,
BZ * ProofBrickSize.Z + BrickAlignmentOffset);
FResearchBrick& Brick = Bricks.AddDefaulted_GetRef(); FResearchBrick& Brick = Bricks.AddDefaulted_GetRef();
Brick.Origin = FIntVector( Brick.Origin = FIntVector(
FMath::Max(OriginVoxels.X, BrickWorldMin.X), FMath::Max(OriginVoxels.X, BrickWorldMin.X),
@@ -215,6 +250,7 @@ namespace VoxelForgeRegionEval
Brick.Lattice = Brick.Origin - OriginVoxels; Brick.Lattice = Brick.Origin - OriginVoxels;
Brick.SampleCount = Brick.Size.X * Brick.Size.Y * Brick.Size.Z; Brick.SampleCount = Brick.Size.X * Brick.Size.Y * Brick.Size.Z;
} }
}
TArray<double> WorkerWall; TArray<double> WorkerWall;
TArray<double> WorkerCpu; TArray<double> WorkerCpu;
@@ -253,6 +289,10 @@ namespace VoxelForgeRegionEval
double CpuStart = 0.0; double CpuStart = 0.0;
const bool bHaveCpuStart = GetCurrentThreadCpuSeconds(CpuStart); const bool bHaveCpuStart = GetCurrentThreadCpuSeconds(CpuStart);
TArray<float> BrickDetail; TArray<float> BrickDetail;
TArray<float> ParameterizedDetail;
TArray<float> ParameterizedColumnValues;
TArray<float> ParameterizedBrickValues;
TArray<uint8> ParameterizedProofKinds;
VoxelForgeRedefinedDensityField::FBrickEvaluationContext BrickContext; VoxelForgeRedefinedDensityField::FBrickEvaluationContext BrickContext;
VoxelForgeRedefinedDensityField::FParameterizedBrickContext ParameterizedBrickContext; VoxelForgeRedefinedDensityField::FParameterizedBrickContext ParameterizedBrickContext;
for (;;) for (;;)
@@ -267,19 +307,147 @@ namespace VoxelForgeRegionEval
for (int32 BrickIndex = BatchBegin; BrickIndex < BatchEnd; ++BrickIndex) for (int32 BrickIndex = BatchBegin; BrickIndex < BatchEnd; ++BrickIndex)
{ {
const FResearchBrick& Brick = Bricks[BrickIndex]; const FResearchBrick& Brick = Bricks[BrickIndex];
if (bParameterized)
{
const int32 ProofZCount = FMath::DivideAndRoundUp(
Brick.Size.Z, ParameterizedBrickSize);
ParameterizedProofKinds.SetNumUninitialized(ProofZCount);
for (int32 ProofZ = 0; ProofZ < ProofZCount; ++ProofZ)
{
const int32 Z = ProofZ * ParameterizedBrickSize;
const FIntVector ProofSize(
Brick.Size.X, Brick.Size.Y,
FMath::Min(ParameterizedBrickSize, Brick.Size.Z - Z));
const FIntVector ProofOrigin = Brick.Origin + FIntVector(0, 0, Z);
bool bUniformAir = false;
bool bEvaluatedMacroCentre = false;
VoxelForgeRedefinedDensityField::EBrickFallbackReason FallbackReason =
VoxelForgeRedefinedDensityField::EBrickFallbackReason::UnsupportedState;
bool bCanSkip = false;
if (bEnableBrickSkips)
{
++WorkerMacroChecks[WorkerIndex];
const double ClassifyStart = FPlatformTime::Seconds();
bCanSkip = VoxelForgeRedefinedDensityField::TryClassifyParameterizedBrick(
Generator, ProofOrigin, ProofSize, bUniformAir, DetailScale,
&bEvaluatedMacroCentre, &FallbackReason);
WorkerClassify[WorkerIndex] +=
FPlatformTime::Seconds() - ClassifyStart;
WorkerMacroCentreEvaluations[WorkerIndex] +=
bEvaluatedMacroCentre ? 1u : 0u;
if (!bCanSkip)
{
const int32 ReasonIndex = static_cast<uint8>(FallbackReason);
if (ReasonIndex < 5)
{
++WorkerFallbackReasons[WorkerIndex * 5 + ReasonIndex];
}
}
}
if (bCanSkip)
{
ParameterizedProofKinds[ProofZ] = bUniformAir ? 1 : 2;
++WorkerSkippedBricks[WorkerIndex];
const uint64 ProofSampleCount = static_cast<uint64>(
ProofSize.X * ProofSize.Y * ProofSize.Z);
WorkerSkippedSamples[WorkerIndex] += ProofSampleCount;
const float UniformDensity = bUniformAir
? VoxelForgeRedefinedDensityField::ParameterizedUniformDensity
: -VoxelForgeRedefinedDensityField::ParameterizedUniformDensity;
const double FillStart = FPlatformTime::Seconds();
for (int32 LocalZ = 0; LocalZ < ProofSize.Z; ++LocalZ)
for (int32 Y = 0; Y < ProofSize.Y; ++Y)
for (int32 X = 0; X < ProofSize.X; ++X)
{
const int64 LatticeIndex = LinearIndex(
Brick.Lattice.X + X, Brick.Lattice.Y + Y,
Brick.Lattice.Z + Z + LocalZ, Dimension);
OutDensities[static_cast<int32>(LatticeIndex)] = UniformDensity;
}
WorkerFill[WorkerIndex] += FPlatformTime::Seconds() - FillStart;
}
else
{
ParameterizedProofKinds[ProofZ] = 0;
}
}
for (int32 ProofZ = 0; ProofZ < ProofZCount;)
{
if (ParameterizedProofKinds[ProofZ] != 0)
{
++ProofZ;
continue;
}
const int32 FirstMixedProofZ = ProofZ;
const int32 FirstZ = FirstMixedProofZ * ParameterizedBrickSize;
const int32 FirstWorldZ = Brick.Origin.Z + FirstZ;
const int32 FirstChunkZ = FMath::FloorToInt(
static_cast<float>(FirstWorldZ) / CHUNK_SIZE);
int32 RunProofCount = 1;
int32 RunSampleCount = FMath::Min(
ParameterizedBrickSize, Brick.Size.Z - FirstZ);
while (ProofZ + RunProofCount < ProofZCount
&& ParameterizedProofKinds[ProofZ + RunProofCount] == 0)
{
const int32 CandidateCount = FMath::Min(
(RunProofCount + 1) * ParameterizedBrickSize,
Brick.Size.Z - FirstZ);
const int32 CandidateEndZ = FirstWorldZ + CandidateCount - 1;
const int32 CandidateChunkZ = FMath::FloorToInt(
static_cast<float>(CandidateEndZ) / CHUNK_SIZE);
if (CandidateCount > CHUNK_SIZE
|| CandidateChunkZ != FirstChunkZ)
{
break;
}
++RunProofCount;
RunSampleCount = CandidateCount;
}
if (RunSampleCount > CHUNK_SIZE)
{
RunSampleCount = ParameterizedBrickSize;
RunProofCount = 1;
}
const FIntVector RunSize(
Brick.Size.X, Brick.Size.Y, RunSampleCount);
const FIntVector RunOrigin = Brick.Origin + FIntVector(0, 0, FirstZ);
const double EvaluationStart = FPlatformTime::Seconds();
VoxelForgeRedefinedDensityField::EvaluateParameterizedDensityBrick(
Generator, RunOrigin, RunSize, DetailScale,
ParameterizedBrickContext, ParameterizedDetail,
ParameterizedColumnValues, ParameterizedBrickValues);
for (int32 Z = 0; Z < RunSize.Z; ++Z)
for (int32 Y = 0; Y < RunSize.Y; ++Y)
{
const int64 LatticeIndex = LinearIndex(
Brick.Lattice.X, Brick.Lattice.Y + Y,
Brick.Lattice.Z + FirstZ + Z, Dimension);
const int32 LocalIndex = (Z * RunSize.Y + Y) * RunSize.X;
FMemory::Memcpy(OutDensities.GetData() + LatticeIndex,
ParameterizedBrickValues.GetData() + LocalIndex,
static_cast<SIZE_T>(RunSize.X) * sizeof(float));
}
WorkerEvaluated[WorkerIndex] += static_cast<uint64>(RunSize.X)
* RunSize.Y * RunSize.Z;
WorkerEvaluate[WorkerIndex] += FPlatformTime::Seconds() - EvaluationStart;
ProofZ += RunProofCount;
}
continue;
}
bool bUniformAir = false; bool bUniformAir = false;
bool bEvaluatedMacroCentre = false; bool bEvaluatedMacroCentre = false;
VoxelForgeRedefinedDensityField::EBrickFallbackReason FallbackReason = VoxelForgeRedefinedDensityField::EBrickFallbackReason FallbackReason =
VoxelForgeRedefinedDensityField::EBrickFallbackReason::UnsupportedState; VoxelForgeRedefinedDensityField::EBrickFallbackReason::UnsupportedState;
++WorkerMacroChecks[WorkerIndex]; ++WorkerMacroChecks[WorkerIndex];
const double ClassifyStart = FPlatformTime::Seconds(); const double ClassifyStart = FPlatformTime::Seconds();
const bool bCanSkip = bEnableBrickSkips && (bParameterized const bool bCanSkip = bEnableBrickSkips
? VoxelForgeRedefinedDensityField::TryClassifyParameterizedBrick( && VoxelForgeRedefinedDensityField::TryClassifyBrick(
Generator, Brick.Origin, Brick.Size, bUniformAir, DetailScale, Generator, Brick.Origin, Brick.Size, bUniformAir, DetailScale,
&bEvaluatedMacroCentre, &FallbackReason) &bEvaluatedMacroCentre, &FallbackReason);
: VoxelForgeRedefinedDensityField::TryClassifyBrick(
Generator, Brick.Origin, Brick.Size, bUniformAir, DetailScale,
&bEvaluatedMacroCentre, &FallbackReason));
WorkerClassify[WorkerIndex] += FPlatformTime::Seconds() - ClassifyStart; WorkerClassify[WorkerIndex] += FPlatformTime::Seconds() - ClassifyStart;
WorkerMacroCentreEvaluations[WorkerIndex] += bEvaluatedMacroCentre ? 1u : 0u; WorkerMacroCentreEvaluations[WorkerIndex] += bEvaluatedMacroCentre ? 1u : 0u;
if (bEnableBrickSkips && !bCanSkip) if (bEnableBrickSkips && !bCanSkip)
@@ -312,49 +480,7 @@ namespace VoxelForgeRegionEval
const double EvaluationStart = FPlatformTime::Seconds(); const double EvaluationStart = FPlatformTime::Seconds();
VoxelForgeRedefinedDensityField::BuildDetailBrick( VoxelForgeRedefinedDensityField::BuildDetailBrick(
Generator, Brick.Origin, Brick.Size, DetailScale, BrickDetail, Generator, Brick.Origin, Brick.Size, DetailScale, BrickDetail,
bParameterized ? nullptr : &BrickContext, bParameterized); &BrickContext, /*bParameterized=*/false);
if (bParameterized)
{
VoxelForgeRedefinedDensityField::BuildParameterizedBrickContext(
Generator, Brick.Origin, Brick.Size, ParameterizedBrickContext);
}
if (bParameterized)
{
const int32 DetailStride = Brick.Size.X * Brick.Size.Y;
float ColumnValues[8] = {};
float ParameterizedBrickValues[8 * 8 * 8];
for (int32 Y = 0; Y < Brick.Size.Y; ++Y)
for (int32 X = 0; X < Brick.Size.X; ++X)
{
const int32 DetailBase = Y * Brick.Size.X + X;
VoxelForgeRedefinedDensityField::EvaluateParameterizedColumnWithDetailValues(
Generator, static_cast<float>(Brick.Origin.X + X),
static_cast<float>(Brick.Origin.Y + Y),
static_cast<float>(Brick.Origin.Z),
BrickDetail.GetData() + DetailBase, DetailStride,
Brick.Size.Z, ColumnValues,
&ParameterizedBrickContext, DetailScale);
for (int32 Z = 0; Z < Brick.Size.Z; ++Z)
{
ParameterizedBrickValues[(Z * Brick.Size.Y + Y)
* Brick.Size.X + X] = ColumnValues[Z];
}
}
for (int32 Z = 0; Z < Brick.Size.Z; ++Z)
for (int32 Y = 0; Y < Brick.Size.Y; ++Y)
{
const int64 LatticeIndex = LinearIndex(
Brick.Lattice.X, Brick.Lattice.Y + Y,
Brick.Lattice.Z + Z, Dimension);
const int32 LocalIndex = (Z * Brick.Size.Y + Y) * Brick.Size.X;
FMemory::Memcpy(OutDensities.GetData() + LatticeIndex,
ParameterizedBrickValues + LocalIndex,
static_cast<SIZE_T>(Brick.Size.X) * sizeof(float));
}
WorkerEvaluated[WorkerIndex] += static_cast<uint64>(Brick.SampleCount);
}
else
{
for (int32 Z = 0; Z < Brick.Size.Z; ++Z) for (int32 Z = 0; Z < Brick.Size.Z; ++Z)
for (int32 Y = 0; Y < Brick.Size.Y; ++Y) for (int32 Y = 0; Y < Brick.Size.Y; ++Y)
for (int32 X = 0; X < Brick.Size.X; ++X) for (int32 X = 0; X < Brick.Size.X; ++X)
@@ -374,7 +500,6 @@ namespace VoxelForgeRegionEval
nullptr, DetailScale, &BrickContext); nullptr, DetailScale, &BrickContext);
++WorkerEvaluated[WorkerIndex]; ++WorkerEvaluated[WorkerIndex];
} }
}
WorkerEvaluate[WorkerIndex] += FPlatformTime::Seconds() - EvaluationStart; WorkerEvaluate[WorkerIndex] += FPlatformTime::Seconds() - EvaluationStart;
} }
} }
@@ -392,7 +517,8 @@ namespace VoxelForgeRegionEval
OutTiming.WallSeconds = FPlatformTime::Seconds() - StartSeconds; OutTiming.WallSeconds = FPlatformTime::Seconds() - StartSeconds;
OutTiming.bRegionProgram = true; OutTiming.bRegionProgram = true;
OutTiming.BrickCount = static_cast<uint64>(Bricks.Num()); OutTiming.BrickCount = bParameterized
? TotalProofBrickCount : static_cast<uint64>(Bricks.Num());
OutTiming.ObservedWorkerThreads = 0; OutTiming.ObservedWorkerThreads = 0;
for (int32 Worker = 0; Worker < NumWorkers; ++Worker) for (int32 Worker = 0; Worker < NumWorkers; ++Worker)
{ {
+295 -8
View File
@@ -1,6 +1,6 @@
[CmdletBinding()] [CmdletBinding()]
param( param(
[ValidateSet('canonical', 'owner', 'probe', 'perf', 'visual-edit', 'visual-lod', 'visual-gap-walk', 'visual-gap-inject', 'dig-motion', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'parity', 'horizon', 'horizon-render', 'determinism', 'symbolize-stacks', 'region-eval', 'step2d', 'step2e', 'step2f', 'step2f-capture')] [ValidateSet('canonical', 'owner', 'probe', 'perf', 'visual-edit', 'visual-lod', 'visual-gap-walk', 'visual-gap-inject', 'dig-motion', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'parity', 'horizon', 'horizon-render', 'determinism', 'symbolize-stacks', 'region-eval', 'step2d', 'step2e', 'step2f', 'step3-scale', 'step2f-capture', 'step3-game')]
[string]$Scenario = 'canonical', [string]$Scenario = 'canonical',
[switch]$Build, [switch]$Build,
[switch]$EnableRender, [switch]$EnableRender,
@@ -21,7 +21,7 @@ param(
[ValidateRange(0, 8)] [ValidateRange(0, 8)]
[int]$HorizonLevel = 5, [int]$HorizonLevel = 5,
[ValidateRange(1, 100000)] [ValidateRange(1, 100000)]
[int]$HorizonRenderDistanceChunks = 768, [int]$HorizonRenderDistanceChunks = 1536,
[ValidateRange(0, 7)] [ValidateRange(0, 7)]
[int]$Step2fProfile = 0, [int]$Step2fProfile = 0,
[ValidateSet('inside', 'wall-closeup')] [ValidateSet('inside', 'wall-closeup')]
@@ -64,6 +64,8 @@ $LaunchLedger = [System.Collections.Generic.List[object]]::new()
$BuildRecord = [ordered]@{ requested = [bool]$Build; status = 'not_requested' } $BuildRecord = [ordered]@{ requested = [bool]$Build; status = 'not_requested' }
$AssetEvidence = @() $AssetEvidence = @()
$CommandletMetrics = [ordered]@{} $CommandletMetrics = [ordered]@{}
$Step3ScaleMetrics = [ordered]@{ requested = $Scenario -eq 'step3-scale'; status = 'not_run' }
$Step3GameMetrics = [ordered]@{ requested = $Scenario -eq 'step3-game'; status = 'not_run' }
$GameMetrics = [ordered]@{} $GameMetrics = [ordered]@{}
$HorizonMetrics = [ordered]@{ $HorizonMetrics = [ordered]@{
requested = $Scenario -in @('horizon', 'horizon-render', 'step2f-capture') requested = $Scenario -in @('horizon', 'horizon-render', 'step2f-capture')
@@ -257,6 +259,56 @@ public static class VoxelForgeTestMemoryStatus {
'@ '@
} }
if (-not ('VoxelForgeTestSystemTimes' -as [type])) {
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
using System.Threading;
public static class VoxelForgeTestSystemTimes {
[StructLayout(LayoutKind.Sequential)]
public struct FILETIME { public uint Low; public uint High; }
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GetSystemTimes(out FILETIME idle, out FILETIME kernel, out FILETIME user);
private static ulong ToUInt64(FILETIME value) { return ((ulong)value.High << 32) | value.Low; }
public static double ReadBusyPercent(int intervalMilliseconds) {
FILETIME idle0, kernel0, user0, idle1, kernel1, user1;
if (!GetSystemTimes(out idle0, out kernel0, out user0)) throw new InvalidOperationException("GetSystemTimes failed before sample.");
Thread.Sleep(intervalMilliseconds);
if (!GetSystemTimes(out idle1, out kernel1, out user1)) throw new InvalidOperationException("GetSystemTimes failed after sample.");
ulong idle = ToUInt64(idle1) - ToUInt64(idle0);
ulong kernel = ToUInt64(kernel1) - ToUInt64(kernel0);
ulong user = ToUInt64(user1) - ToUInt64(user0);
ulong total = kernel + user;
if (total == 0) return 100.0;
ulong busy = total > idle ? total - idle : 0;
return 100.0 * busy / total;
}
}
'@
}
function Wait-ForIdleMachine([string]$When) {
$deadline = [datetime]::UtcNow.AddMinutes(10)
$consecutiveIdleSamples = 0
$lastBusyPercent = 100.0
while ([datetime]::UtcNow -lt $deadline) {
Assert-NoForeignUnreal "$When idle check"
try { $lastBusyPercent = [VoxelForgeTestSystemTimes]::ReadBusyPercent(3000) }
catch { throw "Could not measure system CPU use before $When; refusing to time this run: $($_.Exception.Message)" }
if ($lastBusyPercent -le 15.0) {
++$consecutiveIdleSamples
if ($consecutiveIdleSamples -ge 2) {
Write-Host ("Machine idle before {0}: two 3-second system samples, latest busy={1:N1}%." -f $When, $lastBusyPercent)
return $lastBusyPercent
}
} else {
$consecutiveIdleSamples = 0
Write-Host ("Waiting for idle machine before {0}: 3-second system busy={1:N1}% (limit 15%)." -f $When, $lastBusyPercent)
}
}
throw ("Machine did not stay under 15% CPU busy for two 3-second samples before {0}; last={1:N1}%." -f $When, $lastBusyPercent)
}
function Assert-EnoughFreeMemory([string]$When) { function Assert-EnoughFreeMemory([string]$When) {
$deadline = [datetime]::UtcNow.AddMinutes(30) $deadline = [datetime]::UtcNow.AddMinutes(30)
$didWait = $false $didWait = $false
@@ -397,8 +449,9 @@ function Wait-OwnedProcess($Process, [int]$TimeoutSeconds, [switch]$EnforceMemor
} }
function Invoke-UnrealLaunch([string]$Kind, [string[]]$Arguments, [string]$LogPath, function Invoke-UnrealLaunch([string]$Kind, [string[]]$Arguments, [string]$LogPath,
[string]$StdoutPath, [string]$StderrPath) { [string]$StdoutPath, [string]$StderrPath, [switch]$ShowWindow) {
Assert-NoForeignUnreal "before $Kind launch" Assert-NoForeignUnreal "before $Kind launch"
$idleCpuBusyPercent = Wait-ForIdleMachine "$Kind launch"
Assert-EnoughFreeMemory "$Kind launch" Assert-EnoughFreeMemory "$Kind launch"
Ensure-Directory (Split-Path -Parent $LogPath) Ensure-Directory (Split-Path -Parent $LogPath)
Ensure-Directory (Split-Path -Parent $StdoutPath) Ensure-Directory (Split-Path -Parent $StdoutPath)
@@ -414,6 +467,7 @@ function Invoke-UnrealLaunch([string]$Kind, [string[]]$Arguments, [string]$LogPa
exit_code = $null exit_code = $null
timed_out = $false timed_out = $false
memory_limit_hit = $false memory_limit_hit = $false
prelaunch_cpu_busy_percent = [Math]::Round($idleCpuBusyPercent, 2)
peak_private_bytes = 0L peak_private_bytes = 0L
crash_reporters_reaped = @() crash_reporters_reaped = @()
new_crashes = @() new_crashes = @()
@@ -426,12 +480,13 @@ function Invoke-UnrealLaunch([string]$Kind, [string[]]$Arguments, [string]$LogPa
$isolatedLocalAppData = Join-Path $RunRoot 'LocalAppData' $isolatedLocalAppData = Join-Path $RunRoot 'LocalAppData'
Ensure-Directory $isolatedLocalAppData Ensure-Directory $isolatedLocalAppData
$oldLocalAppData = $env:LOCALAPPDATA $oldLocalAppData = $env:LOCALAPPDATA
$windowStyle = if ($ShowWindow) { 'Normal' } else { 'Hidden' }
try { try {
# Unreal resolves several engine-wide stores from LOCALAPPDATA even when -userdir and # Unreal resolves several engine-wide stores from LOCALAPPDATA even when -userdir and
# -LocalDataCachePath are supplied. Keep this child process inside the run directory. # -LocalDataCachePath are supplied. Keep this child process inside the run directory.
$env:LOCALAPPDATA = $isolatedLocalAppData $env:LOCALAPPDATA = $isolatedLocalAppData
$process = Start-Process -FilePath $EditorExe -ArgumentList $Arguments -WorkingDirectory $HostRoot ` $process = Start-Process -FilePath $EditorExe -ArgumentList $Arguments -WorkingDirectory $HostRoot `
-RedirectStandardOutput $StdoutPath -RedirectStandardError $StderrPath -WindowStyle Hidden ` -RedirectStandardOutput $StdoutPath -RedirectStandardError $StderrPath -WindowStyle $windowStyle `
-PassThru -PassThru
try { $process.PriorityClass = [Diagnostics.ProcessPriorityClass]::BelowNormal } try { $process.PriorityClass = [Diagnostics.ProcessPriorityClass]::BelowNormal }
catch { catch {
@@ -579,7 +634,7 @@ function Sync-OwnerAssets {
$null = Copy-Item -LiteralPath $source -Destination $target -Force -PassThru $null = Copy-Item -LiteralPath $source -Destination $target -Force -PassThru
[void]$rows.Add([ordered]@{ [void]$rows.Add([ordered]@{
name = $assetName name = $assetName
used_by_scenario = $Scenario -in @('owner', 'probe', 'parity', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'visual-edit', 'visual-lod', 'visual-gap-walk', 'visual-gap-inject', 'dig-motion', 'horizon', 'horizon-render', 'determinism', 'region-eval', 'step2d', 'step2e', 'step2f', 'step2f-capture') used_by_scenario = $Scenario -in @('owner', 'probe', 'parity', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'visual-edit', 'visual-lod', 'visual-gap-walk', 'visual-gap-inject', 'dig-motion', 'horizon', 'horizon-render', 'determinism', 'region-eval', 'step2d', 'step2e', 'step2f', 'step3-scale', 'step2f-capture', 'step3-game')
source_path = $source source_path = $source
source_sha256 = Get-Sha256 $source source_sha256 = Get-Sha256 $source
staged_path = $target staged_path = $target
@@ -674,7 +729,7 @@ function Get-AssetEvidence {
$target = Join-Path $HostRoot "Content\VoxelForge\$assetName.uasset" $target = Join-Path $HostRoot "Content\VoxelForge\$assetName.uasset"
[void]$rows.Add([ordered]@{ [void]$rows.Add([ordered]@{
name = $assetName name = $assetName
used_by_scenario = $Scenario -in @('owner', 'probe', 'parity', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'visual-edit', 'visual-lod', 'visual-gap-walk', 'visual-gap-inject', 'dig-motion', 'horizon', 'horizon-render', 'determinism', 'region-eval', 'step2d', 'step2e', 'step2f', 'step2f-capture') used_by_scenario = $Scenario -in @('owner', 'probe', 'parity', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'visual-edit', 'visual-lod', 'visual-gap-walk', 'visual-gap-inject', 'dig-motion', 'horizon', 'horizon-render', 'determinism', 'region-eval', 'step2d', 'step2e', 'step2f', 'step3-scale', 'step2f-capture', 'step3-game')
source_path = $source source_path = $source
source_sha256 = Get-Sha256 $source source_sha256 = Get-Sha256 $source
staged_path = $target staged_path = $target
@@ -713,7 +768,6 @@ function New-CommonArguments([string]$Kind, [string]$LogPath, $CvarMap,
} else { } else {
[void]$args.Add('-nullrhi') [void]$args.Add('-nullrhi')
} }
[void]$args.Add('-DDC-ForceMemoryCache')
[void]$args.Add('-unattended') [void]$args.Add('-unattended')
[void]$args.Add('-nop4') [void]$args.Add('-nop4')
[void]$args.Add('-nosplash') [void]$args.Add('-nosplash')
@@ -1011,6 +1065,147 @@ function Get-HorizonRenderMetrics($Ledger, [string]$LogPath) {
return $metrics return $metrics
} }
function Get-Step3GameMetrics([string]$LogPath, [string]$TracePath, $Ledger, $RenderMetrics) {
$metrics = [ordered]@{
status = 'missing'
log = $LogPath
startup_trace = $TracePath
l0_tile_count = 0
tile_generation_seconds_p50 = $null
tile_generation_seconds_p95 = $null
tile_generation_seconds_max = $null
full_detail_ring_fill_seconds = $null
full_detail_ring_lod0_tiles = $null
moving_frames = $null
moving_window_seconds = $null
moving_frame_ms_p50 = $null
moving_frame_ms_p95 = $null
moving_frame_ms_p99 = $null
moving_frame_ms_max = $null
brick_evaluator_mesher_route_seen = $false
profile_switch_applied = $false
profile_switch_rebuilt_loaded_tiles = $false
profile_switch_cleared_tile_count = 0
captures = @()
}
if (Test-Path -LiteralPath $LogPath -PathType Leaf) {
$metrics.brick_evaluator_mesher_route_seen = @(
Select-String -LiteralPath $LogPath `
-Pattern '\[VoxelForgeResearchBrickEvaluator\] mesher_grid=1' `
-ErrorAction SilentlyContinue).Count -gt 0
$tileSeconds = [System.Collections.Generic.List[double]]::new()
foreach ($match in @(Select-String -LiteralPath $LogPath `
-Pattern '\[VoxelForgeTileProfile\].*level=0\b.*aborted=0' `
-ErrorAction SilentlyContinue)) {
$seconds = [regex]::Match([string]$match.Line, 'seconds=(?<v>[-+0-9.eE]+)')
if ($seconds.Success) {
$value = [double]$seconds.Groups['v'].Value
if ($value -eq $value -and -not [double]::IsInfinity($value) -and $value -ge 0.0) {
$tileSeconds.Add($value)
}
}
}
$tileSeconds.Sort()
$metrics.l0_tile_count = $tileSeconds.Count
if ($tileSeconds.Count -gt 0) {
$metrics.tile_generation_seconds_p50 = $tileSeconds[[Math]::Max(0, [Math]::Ceiling(0.50 * $tileSeconds.Count) - 1)]
$metrics.tile_generation_seconds_p95 = $tileSeconds[[Math]::Max(0, [Math]::Ceiling(0.95 * $tileSeconds.Count) - 1)]
$metrics.tile_generation_seconds_max = $tileSeconds[$tileSeconds.Count - 1]
}
$perfLine = @(Select-String -LiteralPath $LogPath -Pattern '\[VoxelForgeResearchGamePerf\]' -ErrorAction SilentlyContinue | Select-Object -Last 1)
if ($perfLine.Count -gt 0) {
$line = [string]$perfLine[0].Line
foreach ($key in @('moving_frames', 'moving_window_s', 'p50_ms', 'p95_ms', 'p99_ms', 'max_ms')) {
$value = [regex]::Match($line, ('{0}=(?<v>[-+0-9.eE]+)' -f [regex]::Escape($key)))
if ($value.Success) {
$target = switch ($key) {
'moving_frames' { 'moving_frames' }
'moving_window_s' { 'moving_window_seconds' }
'p50_ms' { 'moving_frame_ms_p50' }
'p95_ms' { 'moving_frame_ms_p95' }
'p99_ms' { 'moving_frame_ms_p99' }
'max_ms' { 'moving_frame_ms_max' }
}
$metrics[$target] = [double]$value.Groups['v'].Value
}
}
}
$switchLine = @(Select-String -LiteralPath $LogPath -Pattern '\[VoxelForgeResearchProfileSwitch\] applied=1' -ErrorAction SilentlyContinue | Select-Object -Last 1)
$rebuildLine = @(Select-String -LiteralPath $LogPath -Pattern 'research_field_selection_changed.*rebuilding loaded world' -ErrorAction SilentlyContinue | Select-Object -Last 1)
if ($switchLine.Count -gt 0) { $metrics.profile_switch_applied = $true }
if ($rebuildLine.Count -gt 0) {
$metrics.profile_switch_rebuilt_loaded_tiles = [regex]::IsMatch(
[string]$switchLine[0].Line, 'loaded_tiles=(?<n>[1-9][0-9]*)')
$regenerate = @(Select-String -LiteralPath $LogPath `
-Pattern '\[VoxelWorld\] RegenerateAllChunks .*cleared (?<n>[0-9]+) tiles' `
-ErrorAction SilentlyContinue |
Where-Object { $_.LineNumber -gt $rebuildLine[0].LineNumber } |
Select-Object -First 1)
if ($regenerate.Count -gt 0) {
$cleared = [regex]::Match([string]$regenerate[0].Line, 'cleared (?<n>[0-9]+) tiles')
if ($cleared.Success) {
$metrics.profile_switch_cleared_tile_count = [int]$cleared.Groups['n'].Value
$metrics.profile_switch_rebuilt_loaded_tiles =
$metrics.profile_switch_rebuilt_loaded_tiles -and
$metrics.profile_switch_cleared_tile_count -gt 0
}
} else {
$metrics.profile_switch_rebuilt_loaded_tiles = $false
}
}
$captureLines = @(Select-String -LiteralPath $LogPath -Pattern '\[VoxelForgeTestCeiling\] capture_index=' -ErrorAction SilentlyContinue)
foreach ($captureLine in $captureLines) {
$index = [regex]::Match([string]$captureLine.Line, 'capture_index=(?<v>[12])')
$accepted = [regex]::Match([string]$captureLine.Line, 'capture_requested=(?<v>[01])')
if ($index.Success) {
$metrics.captures += [ordered]@{
index = [int]$index.Groups['v'].Value
requested = $accepted.Success -and $accepted.Groups['v'].Value -eq '1'
line = [string]$captureLine.Line
}
}
}
}
if (Test-Path -LiteralPath $TracePath -PathType Leaf) {
$ringEvents = [System.Collections.Generic.List[object]]::new()
foreach ($line in @(Get-Content -LiteralPath $TracePath -ErrorAction SilentlyContinue)) {
if ([string]::IsNullOrWhiteSpace($line)) { continue }
try {
$event = $line | ConvertFrom-Json -ErrorAction Stop
if ([string](Get-JsonValue $event 'type' '') -eq 'lod0_ring_ready') {
$ringEvents.Add($event)
}
} catch { }
}
if ($ringEvents.Count -gt 0) {
$metrics.full_detail_ring_fill_seconds = [double](Get-JsonValue $ringEvents[0] 'duration_s' 0.0)
$metrics.full_detail_ring_lod0_tiles = [int](Get-JsonValue $ringEvents[0] 'lod0_tiles' 0)
}
}
$metrics.render = $RenderMetrics
$metrics.streaming = Get-StreamingMetrics $LogPath
$metrics.launch = if ($null -ne $Ledger) {
[ordered]@{
status = Get-JsonValue $Ledger 'status' 'missing'
peak_private_bytes = Get-JsonValue $Ledger 'peak_private_bytes' 0
pid = Get-JsonValue $Ledger 'pid' $null
}
} else { $null }
$step3Passed = ($metrics.l0_tile_count -gt 0 -and
$null -ne $metrics.full_detail_ring_fill_seconds -and
$metrics.moving_frames -gt 0 -and
$metrics.brick_evaluator_mesher_route_seen -and
$metrics.profile_switch_applied -and
$metrics.profile_switch_rebuilt_loaded_tiles -and
$metrics.profile_switch_cleared_tile_count -gt 0 -and
@($metrics.captures | Where-Object { $_.requested }).Count -ge 2 -and
$null -ne $Ledger -and (Get-JsonValue $Ledger 'status' 'failed') -eq 'passed')
$metrics.status = if ($step3Passed) {
'passed'
} else { 'failed' }
return $metrics
}
function Compare-DeterminismDumps([string]$DefaultPath, [string]$CrtFma3OffPath) { function Compare-DeterminismDumps([string]$DefaultPath, [string]$CrtFma3OffPath) {
$defaultJson = Read-JsonFile $DefaultPath $defaultJson = Read-JsonFile $DefaultPath
$fmaJson = Read-JsonFile $CrtFma3OffPath $fmaJson = Read-JsonFile $CrtFma3OffPath
@@ -1128,7 +1323,7 @@ function Get-CollisionGateStressMetrics([string]$LogPath) {
'spawn_height_vox', 'deep_drop', 'spawn_height_vox', 'deep_drop',
'gate_holds', 'gate_total_s', 'max_gate_s', 'full_stops', 'gate_holds', 'gate_total_s', 'max_gate_s', 'full_stops',
'full_stop_total_s', 'collision_gate', 'gate_progress_m', 'full_stop_total_s', 'collision_gate', 'gate_progress_m',
'horizontal_gate_probe', 'support_tile', 'full_stop_probe_applied', 'full_stop_probe_observed', 'support_tile',
'support_first_check_to_submit_s', 'support_first_check_to_submit_s',
'support_first_check_to_collision_ready_s', 'test_support_tile', 'support_first_check_to_collision_ready_s', 'test_support_tile',
'test_support_first_check_to_submit_s', 'test_support_first_check_to_submit_s',
@@ -1612,6 +1807,33 @@ try {
} }
} }
} }
'step3-scale' {
$scaleLog = Join-Path $RunRoot 'Logs\step3-scale.log'
$args = New-CommonArguments 'commandlet' $scaleLog (Merge-Cvars $baseCvars)
$args += New-CommandletArguments $commandletOut 'walk,export' ($Assets -eq 'owner') $false 128
$researchOut = Join-Path $PluginRoot "Saved\Research\step3\scale\$Label"
$args += @('-step3scale', '-regionworkers=6', '-profile=0', ('-researchout="{0}"' -f $researchOut))
$commandletLedger = Invoke-UnrealLaunch 'step3-scale-commandlet' $args `
$scaleLog (Join-Path $RunRoot 'Logs\step3-scale.stdout.log') `
(Join-Path $RunRoot 'Logs\step3-scale.stderr.log')
$scaleReportPath = Join-Path $researchOut 'step3_scale_benchmark.json'
$scaleReport = Read-JsonFile $scaleReportPath
if ($null -eq $scaleReport) {
$Step3ScaleMetrics = [ordered]@{ requested = $true; status = 'missing'; output = $researchOut; report = $scaleReportPath }
Add-Failure "step3 scale benchmark did not produce $scaleReportPath"
} else {
$Step3ScaleMetrics = [ordered]@{
requested = $true
status = if ((Get-JsonValue $scaleReport 'pass' $false) -and $commandletLedger.status -eq 'passed') { 'passed' } else { 'failed' }
output = $researchOut
report = $scaleReportPath
result = $scaleReport
}
if (-not (Get-JsonValue $scaleReport 'pass' $false)) {
Add-Failure 'step3 scale sample, determinism, or six-worker gate failed'
}
}
}
'symbolize-stacks' { 'symbolize-stacks' {
if (([string]::IsNullOrWhiteSpace($StackSamplesPath)) -or -not (Test-Path -LiteralPath $StackSamplesPath -PathType Leaf)) { if (([string]::IsNullOrWhiteSpace($StackSamplesPath)) -or -not (Test-Path -LiteralPath $StackSamplesPath -PathType Leaf)) {
throw "-StackSamplesPath must name an existing raw sampler CSV." throw "-StackSamplesPath must name an existing raw sampler CSV."
@@ -1718,6 +1940,67 @@ try {
} }
} }
} }
'step3-game' {
$tracePath = Join-Path $RunRoot 'step3_game_trace.jsonl'
$gameLog = Join-Path $RunRoot 'Logs\step3-game.log'
$baseCvars['voxel.TestRenderDistanceChunks'] = $HorizonRenderDistanceChunks
$baseCvars['voxel.TestClipRadius'] = 6
$baseCvars['voxel.TestMaxClipLevel'] = $HorizonLevel
$baseCvars['voxel.StartupTraceFile'] = $tracePath
$baseCvars['voxel.StartupTraceMaxTiles'] = 16384
$baseCvars['voxel.OuterClassifierMode'] = 0
$baseCvars['voxel.ResearchParameterizedCave'] = 1
$baseCvars['voxel.ResearchParameterizedCaveSet'] = 0
$baseCvars['voxel.ProfileTileGeneration'] = 1
$baseCvars['voxel.ResearchGamePerf'] = 1
$baseCvars['voxel.TestCeilingCameraX'] = 0
$baseCvars['voxel.TestCeilingCameraY'] = 0
$baseCvars['voxel.TestCeilingCameraZ'] = -8800
$baseCvars['voxel.TestCeilingCameraPitch'] = 0
$baseCvars['voxel.TestCeilingCameraYaw'] = 0
$baseCvars['voxel.TestCeilingView'] = 1
$baseCvars['voxel.TestCeilingCaptureDelaySeconds'] = 25
$baseCvars['voxel.TestCeilingSecondCaptureDelaySeconds'] = 70
$baseCvars['voxel.TestCeilingCameraMoveStartSeconds'] = 35
$baseCvars['voxel.TestCeilingCameraMoveDurationSeconds'] = 25
$baseCvars['voxel.TestCeilingCameraMoveSpeedCmPerSecond'] = 600
$baseCvars['voxel.TestCeilingCameraMoveDirection'] = '1,0,0'
$baseCvars['voxel.TestResearchProfileSwitchAfterSeconds'] = 30
$baseCvars['voxel.TestResearchProfileSwitchTo'] = 1
$baseCvars['voxel.TestExitSeconds'] = 85
$args = New-CommonArguments 'game' $gameLog (Merge-Cvars $baseCvars) $true
# Keep a real RHI game viewport for the two lit screenshots. The offscreen
# viewport accepted high-res requests but only wrote its blank startup frame.
$args = @($args | Where-Object { $_ -ne '-RenderOffscreen' })
$args += '-windowed'
$gameLedger = Invoke-UnrealLaunch 'step3-game-brick-evaluator' $args `
$gameLog (Join-Path $RunRoot 'Logs\step3-game.stdout.log') `
(Join-Path $RunRoot 'Logs\step3-game.stderr.log') -ShowWindow
$GameMetrics = Get-StreamingMetrics $gameLog
$HorizonMetrics = [ordered]@{ requested = $false; status = 'ring_and_tile_metrics_are_in_step3_game' }
$HorizonRenderMetrics = Get-HorizonRenderMetrics $gameLedger $gameLog
$Step3GameMetrics = Get-Step3GameMetrics $gameLog $tracePath $gameLedger $HorizonRenderMetrics
$captureDirectory = Join-Path $PluginRoot "Saved\Research\step3\captures\$Label"
Ensure-Directory $captureDirectory
$images = @($HorizonRenderMetrics.images | Sort-Object modified_utc)
if ($images.Count -lt 2) {
Add-Failure "step3 game path did not produce both lit captures; found $($images.Count)"
} else {
$capturePaths = [System.Collections.Generic.List[string]]::new()
$captureNames = @('OpenVaults_before_switch.png', 'TightNetwork_after_switch.png')
for ($captureIndex = 0; $captureIndex -lt 2; ++$captureIndex) {
$sourceImage = $images[$images.Count - 2 + $captureIndex]
$destination = Join-Path $captureDirectory $captureNames[$captureIndex]
Copy-Item -LiteralPath $sourceImage.path -Destination $destination -Force
$capturePaths.Add($destination)
}
$HorizonRenderMetrics.saved_captures = @($capturePaths)
$Step3GameMetrics.saved_captures = @($capturePaths)
}
if ($Step3GameMetrics.status -ne 'passed') {
Add-Failure "step3 game path, profile rebuild, lit capture, ring, or moving frame metric failed: $($Step3GameMetrics.status)"
}
}
'determinism' { 'determinism' {
$defaultDump = Join-Path $RunRoot 'tile_hashes_default.json' $defaultDump = Join-Path $RunRoot 'tile_hashes_default.json'
$crtFma3OffDump = Join-Path $RunRoot 'tile_hashes_crt_fma3_off.json' $crtFma3OffDump = Join-Path $RunRoot 'tile_hashes_crt_fma3_off.json'
@@ -2060,6 +2343,8 @@ $result = [ordered]@{
region_eval = if ($Scenario -eq 'region-eval') { $CommandletMetrics } else { $null } region_eval = if ($Scenario -eq 'region-eval') { $CommandletMetrics } else { $null }
step2e = if ($Scenario -eq 'step2e') { $CommandletMetrics } else { $null } step2e = if ($Scenario -eq 'step2e') { $CommandletMetrics } else { $null }
step2f = if ($Scenario -eq 'step2f') { $CommandletMetrics } else { $null } step2f = if ($Scenario -eq 'step2f') { $CommandletMetrics } else { $null }
step3_scale = $Step3ScaleMetrics
step3_game = $Step3GameMetrics
game = $GameMetrics game = $GameMetrics
horizon = $HorizonMetrics horizon = $HorizonMetrics
horizon_render = $HorizonRenderMetrics horizon_render = $HorizonRenderMetrics
@@ -2101,6 +2386,8 @@ try {
[void]$summaryLines.Add("geometry_hash=$($result.geometry_hash)") [void]$summaryLines.Add("geometry_hash=$($result.geometry_hash)")
[void]$summaryLines.Add("obj_sha256=$($result.obj_sha256)") [void]$summaryLines.Add("obj_sha256=$($result.obj_sha256)")
[void]$summaryLines.Add("region_eval=$(ConvertTo-CompactJson $result.region_eval)") [void]$summaryLines.Add("region_eval=$(ConvertTo-CompactJson $result.region_eval)")
[void]$summaryLines.Add("step3_scale=$(ConvertTo-CompactJson $result.step3_scale)")
[void]$summaryLines.Add("step3_game=$(ConvertTo-CompactJson $result.step3_game)")
[void]$summaryLines.Add("surface_fall=$(ConvertTo-CompactJson $result.surface_fall)") [void]$summaryLines.Add("surface_fall=$(ConvertTo-CompactJson $result.surface_fall)")
[void]$summaryLines.Add("strate_crossing=$(ConvertTo-CompactJson $result.strate_crossing)") [void]$summaryLines.Add("strate_crossing=$(ConvertTo-CompactJson $result.strate_crossing)")
[void]$summaryLines.Add("visual_staleness=$(ConvertTo-CompactJson $result.visual_staleness)") [void]$summaryLines.Add("visual_staleness=$(ConvertTo-CompactJson $result.visual_staleness)")