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)
All authored lengths are voxel-space values, converted at the boundary by `0.25 m/voxel`. The body
reference is a **0.34 m radius, 1.76 m tall** capsule (`1.36 × 7.04` voxels). The default strate
height is now **8 chunks = 256 voxels = 64 m**, which is intentionally larger than the 3-4 chunk
Voxel-space authored lengths convert at `0.125 m/voxel`. The body reference is a **0.34 m radius,
1.76 m tall** capsule (`2.72 × 14.08` voxels). The default strate height is **16 chunks = 512 voxels
= 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.
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 |
|---------|--------------------------|---------------------|
| 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 |
| 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 |
@@ -175,8 +175,8 @@ arrival→departure flood-fill at the independently placed passage endpoints.
### 8.2a XY edge seal (bounded-world invariant)
`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
solid ramp is 64 voxels. A radius of **0 is a true no-op**: the hot path returns before doing any
XY around `(0,0)`. The legacy 8192-voxel radius is ~1.024 km at 12.5 cm/voxel; use 16384 voxels
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`,
`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
@@ -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
`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
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).
### 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
per-passage ordering is involved.
The chamber dimensions come directly from the capsule: player diameter `2×0.34/0.25 = 2.72`
voxels; required floor width `3/0.25 = 12` voxels; capsule height `2×0.88/0.25 = 7.04` voxels;
one metre of headroom adds 4 voxels. `HalfWidth=max(7, MouthRadius+2)` leaves
`2×(HalfWidth−1) >= 12` voxels of flat support after the one-voxel wall inset; the authored room
is therefore at least 14 voxels (3.5 m) wide. Height is
`max(12, 7.04+4, 2×MouthRadius+2)`; the stock 6-voxel mouth is a 14-voxel (3.5 m) room with
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
The chamber dimensions come directly from the capsule: player diameter `2×0.34/0.125 = 5.44`
voxels; required floor width `3/0.125 = 24` voxels; capsule height `2×0.88/0.125 = 14.08` voxels;
one metre of headroom adds 8 voxels. `HalfWidth=max(12, MouthRadius+4)` gives a 24-voxel
minimum clear floor width (3 m). Height is `max(24, 14.08+8, 2×MouthRadius+4)`; the minimum
room height is 24 voxels (3 m). `FloorZ=StandingPoint.Z−1`, floor thickness is 6 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.
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
+2 -1
View File
@@ -25,7 +25,8 @@ try to run anything.
## Hard rules (these prevent real bugs — verify against code, don't assume)
- **Density sign: negative = solid, positive = air** (MC convention at the mesher).
`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 ⇒
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
+48 -19
View File
@@ -28,7 +28,7 @@ A **density-field voxel terrain** plugin for Unreal Engine, built around undergr
### Core terminology
| 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`). |
| 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. |
@@ -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.
### 3.0 Human-scale generation contract (2026-09-06)
`VoxelStrateTypes.h` is now authored against the confirmed player capsule, not against naked voxel
counts: one voxel is **0.25 m**, the capsule is **1.36 voxels / 0.34 m radius** and **7.04 voxels /
1.76 m tall**, and the default strate envelope is **8 chunks / 256 voxels / 64 m**. Core defaults are
therefore: room diameters **8-20 m**, origin hub **24 m**, room cells **32 m**, round tunnel bores
**3-4 m**, maze cells **16 m** with a **6.25 m nominal bore** (**3.125 m** 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**. These are
different derivations, not a global multiplier: passage widths follow the 2.5-4 m walking/fight
target, room/cell spacing preserves a rock web, and the 64 m vertical envelope reserves space for
tall rooms, surface sky, and vertical traversal.
### 3.0 Human-scale generation contract (2026-10-01)
`VOXEL_SIZE` is **12.5 cm**; 32³ voxels make each chunk **4 m**. The gameplay capsule remains
**0.34 m radius / 1.76 m tall** (**2.72 / 14.08 voxels**). The default strate envelope is
**16 chunks / 512 voxels / 64 m**; serialized owner strate assets that still say 8 chunks must be
changed to 16. Research-field dimensions are authored in metres and converted at runtime. The live
strate generator continues to read voxel-valued parameters from its assets, so those authored
dimensions become half as large in metres unless their asset values are doubled.
The intended metre-scale families remain: room diameters **8-20 m**, origin hub **24 m**, room cells
**32 m**, round tunnel bores **3-4 m**, maze cells **16 m** with a **6.25 m nominal bore** (**3.125 m**
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`
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.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). |
| `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/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. |
| `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. |
| `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 every 12.5 cm detail sample. Timings split classification, point evaluation, and uniform fill; step 2f compares all profiles and hashes workers 1 and 6. |
| `Public/VoxelRedefinedDensityField.h` / `Private/VoxelRedefinedDensityField.cpp` | Two default-off `WITH_EDITOR` research fields, selected by `voxel.ResearchRedefinedDensityField` (step 2d) and `voxel.ResearchParameterizedCave` (steps 2e/2f). Step 2f retains a metre-scaled 2e baseline for its rejection audit. New profiles use strongly jittered, individually rotated elliptical rooms capped against nearest neighbors and curved cubic-Bezier passages with deterministic centerline and width variation along each edge. Room heights and feature rolls vary by cell; ledges, occasional pillars, stalactites/stalagmites, basin floors, and bounded hanging masses remain features. Point and brick detail sampling share independently rotated 96/48/24-sample bands and a smooth ridge response, preserving 12/6/3 m wavelengths at 12.5 cm per sample. The parameterized mesher evaluates the exact field through prepared 4³ bricks and the same sound skip proof in commandlet and editor-game meshing. In-editor and in-game profile changes invalidate and rebuild all loaded tiles. In an editor game with this field enabled, sealed-solid, out-of-layout, and outer-classifier verdicts are declined without changing their console switches. |
| `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. |
| `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 |
|--------|------|-------|
| `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. |
| `WorldToChunkCoord` | 287 | World cm → chunk coord (handles negatives via floor). |
| `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
`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
capsule (minimum 12 voxels / 3 m flat floor, 12 voxels / 3 m clear height, and at least 14 voxels /
3.5 m authored width); the `(0,0)` origin landing is enlarged for the reserved future shaft, while
capsule (minimum 24 voxels / 3 m flat floor and clear height); the `(0,0)` origin landing is enlarged
for the reserved future shaft, while
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
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()
{
constexpr float VoxelMetres = 0.25f;
constexpr float StrateHeightVoxels = 8.0f * static_cast<float>(CHUNK_SIZE);
constexpr float VoxelMetres = VOXEL_SIZE / 100.0f;
constexpr float StrateHeightVoxels = 16.0f * static_cast<float>(CHUNK_SIZE);
FString Report = TEXT(
"WORLD SCALE AUDIT (all distances are authored in voxels; METRES = voxels * 0.25; "
"player radius=1.36 vox/0.34 m, height=7.04 vox/1.76 m; frequency rows show nominal "
"WORLD SCALE AUDIT (all distances are authored in voxels; METRES = voxels * VOXEL_SIZE; "
"player radius=2.72 vox/0.34 m, height=14.08 vox/1.76 m; frequency rows show nominal "
"period=1/frequency)\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)
{
Report += FString::Printf(
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,
const TCHAR* Meaning)
{
Report += FString::Printf(
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") };
for (const TCHAR* Archetype : TunnelNames)
@@ -708,7 +708,7 @@ namespace
{
Add(Archetype, TEXT("StrateHeight"), StrateHeightVoxels,
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"));
Add(Archetype, TEXT("Ceiling height"), StrateHeightVoxels * 0.60f,
TEXT("38.4 m above the bottom; 22.4 m clear span before roughness"));
@@ -728,7 +728,7 @@ namespace
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"));
Add(TEXT("Common layout"), TEXT("InterStrateGap"), 0.0f,
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"));
Add(TEXT("SurfaceWorld"), TEXT("ElevationRange"), 80.0f,
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"));
Add(TEXT("SurfaceWorld"), TEXT("HeightWarpStrength"), 48.0f,
TEXT("12 m horizontal landform distortion"));
@@ -1002,7 +1002,7 @@ namespace
D.StartToGoalComponentDistanceCells,
*Law, FloorAreaM2,
Found->Metrics.WalkableFloorAreaFraction,
Found->Metrics.MedianVerticalClearance * 0.25f,
Found->Metrics.MedianVerticalClearance * (VOXEL_SIZE / 100.0f),
VF_ShowcaseRoughnessFeatureRatio(Found->Candidate));
}
return Report;
@@ -202,7 +202,7 @@ namespace
Passage->LowerLanding.StandingPoint.Y,
Passage->LowerLanding.FloorZ));
constexpr float MaxStationSpacing = 0.5f;
constexpr float MaxStationSpacing = 1.0f;
constexpr float PlayerWidthVoxels =
VoxelPassageGeometry::WalkCorridorPlayerWidthVoxels;
constexpr float PlayerHalfWidthVoxels = PlayerWidthVoxels * 0.5f;
@@ -214,11 +214,11 @@ namespace
PlayerWidthVoxels / static_cast<float>(NumLateralSamples - 1);
constexpr int32 NumVerticalSamples = 14;
constexpr int32 FloorProbeIntervals = 128;
constexpr float FloorProbeHalfRange = 32.0f;
constexpr float FloorProbeHalfRange = 64.0f;
constexpr float FloorProbeStep =
(2.0f * FloorProbeHalfRange) / static_cast<float>(FloorProbeIntervals);
constexpr float FirstBodySampleZ = 0.25f;
constexpr float BodySampleSpacing = 0.5f;
constexpr float FirstBodySampleZ = 0.5f;
constexpr float BodySampleSpacing = 1.0f;
OutSamples.ProofStencilTileOrigins.Reset();
OutSamples.ProofStencilUniformTiles = 0;
OutSamples.ProofStencilSealedTiles = 0;
@@ -484,7 +484,7 @@ namespace
{
constexpr float GapProfileMinOffset = -360.0f;
constexpr float GapProfileMaxOffset = 8.0f;
constexpr float GapProfileStep = 0.5f;
constexpr float GapProfileStep = 1.0f;
constexpr int32 GapProfileSamples = 737;
TArray<float> GapProfileDensities;
GapProfileDensities.SetNumUninitialized(GapProfileSamples);
@@ -878,7 +878,7 @@ namespace
const float HorizontalDistance = FMath::Sqrt(
FMath::Square(Blocker.X - ClosestX)
+ FMath::Square(Blocker.Y - ClosestY));
if (HorizontalDistance <= 2.0f)
if (HorizontalDistance <= 4.0f)
{
const float FloorZ = FMath::Lerp(
Candidate.NativeFloorProfileZ[SegmentIndex],
@@ -1004,7 +1004,7 @@ namespace
const float HorizontalDistance = FMath::Sqrt(
FMath::Square(Blocker.X - ClosestX)
+ FMath::Square(Blocker.Y - ClosestY));
if (HorizontalDistance <= 12.0f)
if (HorizontalDistance <= 24.0f)
{
const float FloorZ = FMath::Lerp(
Candidate.NativeFloorProfileZ[SegmentIndex],
@@ -98,18 +98,21 @@ namespace
float ExpectedFloorZ,
float& OutFloorZ)
{
constexpr float SampleStep = 0.25f;
constexpr float SampleStep = VoxelPassageGeometry::MetersToVoxels(0.0625f);
const float ExpectedAt = Generator.GetDensityAt(
Centreline.X, Centreline.Y, ExpectedFloorZ);
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(
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
// archetype may contain another cave floor below the tunnel; that is not the floor carried
// by this passage and must not hijack the audit.
const float StartZ = ExpectedFloorZ - 4.0f;
const float EndZ = ExpectedFloorZ + 4.0f;
const float ProbeHalfRange = VoxelPassageGeometry::MetersToVoxels(1.0f);
const float StartZ = ExpectedFloorZ - ProbeHalfRange;
const float EndZ = ExpectedFloorZ + ProbeHalfRange;
const int32 NumSteps = FMath::Max(
1, FMath::CeilToInt((EndZ - StartZ) / SampleStep));
@@ -157,15 +160,18 @@ namespace
PreviousDensity = CurrentDensity;
}
if (BestDistance == FLT_MAX
|| BestDistance > VoxelPassageGeometry::LandingFloorThicknessVoxels + 1.0f)
|| BestDistance > VoxelPassageGeometry::LandingFloorThicknessVoxels
+ VoxelPassageGeometry::MetersToVoxels(0.25f))
{
return false;
}
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(
Centreline.X, Centreline.Y, BestCrossingZ + 0.75f);
Centreline.X, Centreline.Y, BestCrossingZ
+ VoxelPassageGeometry::MetersToVoxels(0.1875f));
if (!FMath::IsFinite(Below) || !FMath::IsFinite(Above)
|| !(Below < 0.0f) || !(Above > 0.0f))
{
@@ -1115,8 +1121,9 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
EndIndex == 0 ? TEXT("upper") : TEXT("lower"), StrateIndex));
}
const bool bWidthAndHeadroom = Landing.HalfWidth * 2.0f >= 12.0f - 0.01f
&& Landing.CeilingZ - Landing.FloorZ >= 12.0f - 0.01f;
const float ThreeMetresVoxels = VoxelPassageGeometry::MetersToVoxels(3.0f);
const bool bWidthAndHeadroom = Landing.HalfWidth * 2.0f >= ThreeMetresVoxels - 0.01f
&& Landing.CeilingZ - Landing.FloorZ >= ThreeMetresVoxels - 0.01f;
const bool bDoorTangent = FMath::IsNearlyEqual(
Landing.DoorPoint.Z - Landing.FloorZ,
Passage.Radius >= 0.0f ? FMath::Max(
@@ -1136,10 +1143,10 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
const FVector FloorSamples[] = {
Landing.StandingPoint,
Landing.StandingPoint + FVector(2.0f, 0.0f, 0.0f),
Landing.StandingPoint + FVector(-2.0f, 0.0f, 0.0f),
Landing.StandingPoint + FVector(0.0f, 2.0f, 0.0f),
Landing.StandingPoint + FVector(0.0f, -2.0f, 0.0f) };
Landing.StandingPoint + FVector(VoxelPassageGeometry::MetersToVoxels(0.5f), 0.0f, 0.0f),
Landing.StandingPoint + FVector(-VoxelPassageGeometry::MetersToVoxels(0.5f), 0.0f, 0.0f),
Landing.StandingPoint + FVector(0.0f, VoxelPassageGeometry::MetersToVoxels(0.5f), 0.0f),
Landing.StandingPoint + FVector(0.0f, -VoxelPassageGeometry::MetersToVoxels(0.5f), 0.0f) };
float SurfaceHeights[UE_ARRAY_COUNT(FloorSamples)] = {};
bool bFloorPassed = true;
float LandingGradient = -1.0f;
@@ -1147,9 +1154,11 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
{
const FVector& Sample = FloorSamples[SampleIndex];
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(
Sample.X, Sample.Y, Landing.FloorZ + 1.0f);
Sample.X, Sample.Y, Landing.FloorZ
+ VoxelPassageGeometry::MetersToVoxels(0.25f));
const float Denominator = Above - Below;
if (!FMath::IsFinite(Below) || !FMath::IsFinite(Above)
|| !(Below < 0.0f) || !(Above >= 0.0f)
@@ -1158,8 +1167,10 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
bFloorPassed = false;
continue;
}
SurfaceHeights[SampleIndex] = Landing.FloorZ - 1.0f
+ FMath::Clamp(-Below / Denominator, 0.0f, 1.0f) * 2.0f;
const float FloorProbeHalfRange = VoxelPassageGeometry::MetersToVoxels(0.25f);
SurfaceHeights[SampleIndex] = Landing.FloorZ - FloorProbeHalfRange
+ FMath::Clamp(-Below / Denominator, 0.0f, 1.0f)
* 2.0f * FloorProbeHalfRange;
}
if (bFloorPassed)
@@ -1168,7 +1179,8 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
for (int32 SampleIndex = 1; SampleIndex < UE_ARRAY_COUNT(FloorSamples); ++SampleIndex)
{
MaxGradient = FMath::Max(MaxGradient,
FMath::Abs(SurfaceHeights[SampleIndex] - SurfaceHeights[0]) / 2.0f);
FMath::Abs(SurfaceHeights[SampleIndex] - SurfaceHeights[0])
/ VoxelPassageGeometry::MetersToVoxels(0.5f));
}
LandingGradient = 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); "
"-ray final=%.3f vox (%.3f m), -ray core=%.3f vox (%.3f m)"),
DiagnosisSeed, MazeCandidateIndex, MazeSlot, *MazeAxisDescription,
MazeParams.CorridorRadius, MazeParams.CorridorRadius * 0.25f,
MazeParams.CorridorRadius * 2.0f * 0.25f,
MazeParams.CorridorRadius, MazeParams.CorridorRadius * FVoxelPlayerCapsuleConstants::VoxelSizeMeters,
MazeParams.CorridorRadius * 2.0f * FVoxelPlayerCapsuleConstants::VoxelSizeMeters,
MazeFinal.Positive.CrossingRadiusVoxels,
MazeFinal.Positive.CrossingRadiusVoxels * 0.25f,
MazeFinal.Positive.CrossingRadiusVoxels * FVoxelPlayerCapsuleConstants::VoxelSizeMeters,
MazeFinal.Positive.FirstSolidStep,
MazeCore.Positive.CrossingRadiusVoxels,
MazeCore.Positive.CrossingRadiusVoxels * 0.25f,
MazeCore.Positive.CrossingRadiusVoxels * FVoxelPlayerCapsuleConstants::VoxelSizeMeters,
MazeFinal.Negative.CrossingRadiusVoxels,
MazeFinal.Negative.CrossingRadiusVoxels * 0.25f,
MazeFinal.Negative.CrossingRadiusVoxels * FVoxelPlayerCapsuleConstants::VoxelSizeMeters,
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
@@ -1000,16 +1000,17 @@ bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters)
"core_without_structural_posts +ray=%.3f vox (%.3f m), -ray=%.3f vox (%.3f m)"),
DiagnosisSeed, TunnelCandidateIndex, *TunnelAxis.Description,
TunnelParams.TunnelMinRadius, TunnelParams.TunnelMaxRadius,
TunnelParams.TunnelMinRadius * 0.25f, TunnelParams.TunnelMaxRadius * 0.25f,
TunnelAxis.NominalRadius, TunnelAxis.NominalRadius * 0.25f,
TunnelParams.TunnelMinRadius * FVoxelPlayerCapsuleConstants::VoxelSizeMeters,
TunnelParams.TunnelMaxRadius * FVoxelPlayerCapsuleConstants::VoxelSizeMeters,
TunnelAxis.NominalRadius, TunnelAxis.NominalRadius * FVoxelPlayerCapsuleConstants::VoxelSizeMeters,
TunnelAxis.FinalSection.Positive.CrossingRadiusVoxels,
TunnelAxis.FinalSection.Positive.CrossingRadiusVoxels * 0.25f,
TunnelAxis.FinalSection.Positive.CrossingRadiusVoxels * FVoxelPlayerCapsuleConstants::VoxelSizeMeters,
TunnelAxis.FinalSection.Negative.CrossingRadiusVoxels,
TunnelAxis.FinalSection.Negative.CrossingRadiusVoxels * 0.25f,
TunnelAxis.FinalSection.Negative.CrossingRadiusVoxels * FVoxelPlayerCapsuleConstants::VoxelSizeMeters,
TunnelAxis.CoreSection.Positive.CrossingRadiusVoxels,
TunnelAxis.CoreSection.Positive.CrossingRadiusVoxels * 0.25f,
TunnelAxis.CoreSection.Positive.CrossingRadiusVoxels * FVoxelPlayerCapsuleConstants::VoxelSizeMeters,
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
@@ -1035,16 +1036,16 @@ bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters)
"core_without_structural_posts +ray=%.3f vox (%.3f m), -ray=%.3f vox (%.3f m)"),
DiagnosisSeed, ShaftCandidateIndex, *ShaftAxisDescription,
ShaftParams.ShaftMinRadius, ShaftParams.ShaftMaxRadius,
ShaftParams.ShaftMinRadius * 0.25f, ShaftParams.ShaftMaxRadius * 0.25f,
ShaftNominalRadius, ShaftNominalRadius * 0.25f,
ShaftParams.ShaftMinRadius * (VOXEL_SIZE / 100.0f), ShaftParams.ShaftMaxRadius * (VOXEL_SIZE / 100.0f),
ShaftNominalRadius, ShaftNominalRadius * (VOXEL_SIZE / 100.0f),
ShaftFinal.Positive.CrossingRadiusVoxels,
ShaftFinal.Positive.CrossingRadiusVoxels * 0.25f,
ShaftFinal.Positive.CrossingRadiusVoxels * (VOXEL_SIZE / 100.0f),
ShaftFinal.Negative.CrossingRadiusVoxels,
ShaftFinal.Negative.CrossingRadiusVoxels * 0.25f,
ShaftFinal.Negative.CrossingRadiusVoxels * (VOXEL_SIZE / 100.0f),
ShaftCore.Positive.CrossingRadiusVoxels,
ShaftCore.Positive.CrossingRadiusVoxels * 0.25f,
ShaftCore.Positive.CrossingRadiusVoxels * (VOXEL_SIZE / 100.0f),
ShaftCore.Negative.CrossingRadiusVoxels,
ShaftCore.Negative.CrossingRadiusVoxels * 0.25f));
ShaftCore.Negative.CrossingRadiusVoxels * (VOXEL_SIZE / 100.0f)));
}
FVoxelStrateMeasureSettings CharacterFitSettings;
@@ -1164,11 +1165,11 @@ bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters)
"walkable slope<=%.1f deg; capsule clearance is re-tested at resolved support; "
"HeadroomCells=%d is legacy-only"),
ControlSettings.PlayerCapsuleRadiusVoxels,
ControlSettings.PlayerCapsuleRadiusVoxels * 0.25f,
ControlSettings.PlayerCapsuleRadiusVoxels * FVoxelPlayerCapsuleConstants::VoxelSizeMeters,
ControlSettings.PlayerCapsuleHalfHeightVoxels,
ControlSettings.PlayerCapsuleHalfHeightVoxels * 0.25f,
ControlSettings.PlayerCapsuleHalfHeightVoxels * FVoxelPlayerCapsuleConstants::VoxelSizeMeters,
FVoxelPlayerCapsuleConstants::HeightVoxels,
FVoxelPlayerCapsuleConstants::HeightVoxels * 0.25f,
FVoxelPlayerCapsuleConstants::HeightVoxels * FVoxelPlayerCapsuleConstants::VoxelSizeMeters,
FMath::CeilToInt(2.0f * ControlSettings.PlayerCapsuleHalfHeightVoxels),
FMath::CeilToInt(2.0f * ControlSettings.PlayerCapsuleHalfHeightVoxels) - 1,
ControlSettings.PlayerMaxStepHeightMeters,
@@ -6778,10 +6778,11 @@ FVoxelPassageLanding VF_BuildPassageLanding(
{
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
// 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 DesiredHeight = VoxelPassageGeometry::LandingHeightForRadius(MouthRadius);
const float SafeSeal = FMath::Max(BoundarySealThickness, 0.0f);
@@ -6793,7 +6794,8 @@ FVoxelPassageLanding VF_BuildPassageLanding(
}
Landing.StandingPoint = InStandingPoint;
Landing.FloorZ = InStandingPoint.Z - 0.5f;
Landing.FloorZ = InStandingPoint.Z
- VoxelPassageGeometry::MetersToVoxels(0.125f);
Landing.HalfWidth = DesiredHalfWidth;
Landing.CeilingZ = Landing.FloorZ + DesiredHeight;
Landing.FloorThickness = VoxelPassageGeometry::LandingFloorThicknessVoxels;
@@ -6818,7 +6820,8 @@ FVoxelPassageLanding VF_BuildPassageLanding(
if (MaxFloor >= MinFloor)
{
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;
}
else
@@ -6833,9 +6836,10 @@ FVoxelPassageLanding VF_BuildPassageLanding(
}
// 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
+ 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;
return Landing;
@@ -6862,11 +6866,14 @@ float VF_EvaluatePassageLandingSDF(
Landing.StandingPoint.X,
Landing.StandingPoint.Y,
(Landing.FloorZ + Landing.CeilingZ) * 0.5f);
constexpr float RoomRounding = 1.5f;
constexpr float RoomRounding = VoxelPassageGeometry::RoomRoundingVoxels;
const FVector RoomHalfExtent(
FMath::Max(Landing.HalfWidth - RoomRounding, 0.25f),
FMath::Max(Landing.HalfWidth - RoomRounding, 0.25f),
FMath::Max((Landing.CeilingZ - Landing.FloorZ) * 0.5f - RoomRounding, 0.25f));
FMath::Max(Landing.HalfWidth - RoomRounding,
VoxelPassageGeometry::MetersToVoxels(0.0625f)),
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,
// 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.
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
&& 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
// (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 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));
@@ -145,7 +145,7 @@ void UVoxelDensityVolume::ResumeAfterGenerationChange()
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
+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
// déréférencer l'asset de settings.
Seed = Settings ? Settings->GetEffectiveWorldSeed() : 0;
OriginSpineRadius = Settings ? Settings->GetEffectiveOriginSpineRadius() : 14.0f;
OriginSpineRadius = Settings ? Settings->GetEffectiveOriginSpineRadius() : 28.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,
@@ -4,11 +4,16 @@
#include "VoxelMarchingCubesMesher.h"
#include "MarchingCubesTables.h"
#include "VoxelDensityProfile.h"
#if WITH_EDITOR
#include "VoxelRedefinedDensityField.h"
#endif
#include "VoxelStrateManager.h"
#include "VoxelTilePostReach.h"
#include "VoxelPassageGeometry.h"
#include "ProfilingDebugging/CpuProfilerTrace.h"
#include "HAL/IConsoleManager.h"
#include "Misc/CommandLine.h"
#include <atomic>
namespace
{
@@ -703,7 +708,16 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
// gradient) sont échantillonnées — le reste du tampon reste non initialisé et non lu.
VoxelDensityProfile::FScopedTimer MesherDensityGridTimer(
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
&& !bUseParameterizedBrickEvaluator
&& GVoxelForgeUseOperatorBlock != 0;
if (bUseOperatorBlock)
{
@@ -713,7 +727,149 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
OriginVoxels + FIntVector(-Step, -Step, GzLo * Step),
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())
{
File diff suppressed because it is too large. Load diff
@@ -1716,11 +1716,11 @@ namespace
float StrateHeightInVoxels)
{
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
// 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
// radius remains well below CellSize/2 and a real rock wall survives.
// remains 0.7 * (2 * (12.5 - 3.75) * 0.125) = 1.53125 m with the current
// voxel-valued authoring. CellSize stays 64-96 voxels (8-12 m), so a rock web remains.
P.CellSize = VF_CorpusFreeU(
Rng, 64.0f, FMath::Max(64.0f, FMath::Min(96.0f, H * 0.75f)));
P.CorridorRadius = VF_CorpusFreeU(
@@ -2192,11 +2192,14 @@ void UVoxelStrateManager::GeneratePassages()
const FVector TunnelLowerDoorDirection = DoorDirection;
Passage.LowerLanding.DoorDirection = TunnelLowerDoorDirection;
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(
Passage.LowerLanding.HalfWidth - 1.0f, 0.0f);
Passage.LowerLanding.HalfWidth
- VoxelPassageGeometry::MetersToVoxels(0.25f), 0.0f);
const float SafeMouthRadius = FMath::Max(
FMath::Abs(Cfg.MouthRadius), 1.0f);
FMath::Abs(Cfg.MouthRadius),
VoxelPassageGeometry::MetersToVoxels(0.25f));
Passage.UpperLanding.DoorPoint =
Passage.UpperLanding.StandingPoint + DoorDirection * UpperDoorOffset;
Passage.UpperLanding.DoorPoint.Z =
@@ -2602,7 +2605,8 @@ void UVoxelStrateManager::GeneratePassages()
Entry.PassageType = EVoxelPassageType::VerticalShaft;
// Match the reserved future shaft envelope exactly. The entry is only opened through
// the top seal into the finite landing room; lower strate seals remain untouched.
Entry.Radius = FMath::Max(OriginSpineRadius, 1.0f);
Entry.Radius = FMath::Max(OriginSpineRadius,
VoxelPassageGeometry::MetersToVoxels(0.25f));
Entry.UpperPoint = FVector(0.0f, 0.0f, TopZ + CHUNK_SIZE);
// 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.
@@ -3063,9 +3067,11 @@ static bool VF_IsPassageRoomFloor(
return Position.Z <= Landing.FloorZ + KINDA_SMALL_NUMBER
&& Position.Z > Landing.FloorZ - Landing.FloorThickness
&& 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::Max(Landing.HalfWidth - 1.0f, 0.0f);
<= FMath::Max(Landing.HalfWidth
- VoxelPassageGeometry::MetersToVoxels(0.25f), 0.0f);
}
void UVoxelStrateManager::ApplyPassageStructuralPostsMC(
@@ -3719,10 +3725,12 @@ namespace
{
continue;
}
constexpr float RoomRounding = 1.5f;
constexpr float RoomRounding = VoxelPassageGeometry::RoomRoundingVoxels;
const float Height = Landing->CeilingZ - Landing->FloorZ;
const float HalfX = FMath::Max(Landing->HalfWidth - RoomRounding, 0.25f);
const float HalfZ = FMath::Max(Height * 0.5f - RoomRounding, 0.25f);
const float HalfX = FMath::Max(Landing->HalfWidth - RoomRounding,
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;
if (!AddDomain(
FVector(Landing->StandingPoint.X - HalfX - RoomRounding,
@@ -4068,15 +4076,18 @@ namespace
return true;
}
constexpr float RoomRounding = 1.5f;
constexpr float RoomRounding = VoxelPassageGeometry::RoomRoundingVoxels;
const float Height = Landing.CeilingZ - Landing.FloorZ;
const FVector Center(
Landing.StandingPoint.X, Landing.StandingPoint.Y,
(Landing.FloorZ + Landing.CeilingZ) * 0.5f);
const FVector HalfExtent(
FMath::Max(Landing.HalfWidth - RoomRounding, 0.25f),
FMath::Max(Landing.HalfWidth - RoomRounding, 0.25f),
FMath::Max(Height * 0.5f - RoomRounding, 0.25f));
FMath::Max(Landing.HalfWidth - RoomRounding,
VoxelPassageGeometry::MetersToVoxels(0.0625f)),
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 OuterMax = Center + HalfExtent + FVector(RoomRounding);
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
&& VF_LatticeBoxTouchesAABB(
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,
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,
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,
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,
Landing->FloorZ - Landing->FloorThickness - ReachScale,
Landing->FloorZ + ReachScale))
@@ -5444,10 +5459,12 @@ bool UVoxelStrateManager::AnyPassageLandingNearLattice(
{
continue;
}
constexpr float RoomRounding = 1.5f;
constexpr float RoomRounding = VoxelPassageGeometry::RoomRoundingVoxels;
const float Height = Landing->CeilingZ - Landing->FloorZ;
const float HalfX = FMath::Max(Landing->HalfWidth - RoomRounding, 0.25f);
const float HalfZ = FMath::Max(Height * 0.5f - RoomRounding, 0.25f);
const float HalfX = FMath::Max(Landing->HalfWidth - RoomRounding,
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;
if (VF_LatticeBoxTouchesAABB(
VoxelBox, LatticeOrigin, Step,
@@ -5651,10 +5668,12 @@ bool UVoxelStrateManager::AnyPassageCarvingNearLattice(
{
continue;
}
constexpr float RoomRounding = 1.5f;
constexpr float RoomRounding = VoxelPassageGeometry::RoomRoundingVoxels;
const float Height = Landing->CeilingZ - Landing->FloorZ;
const float HalfX = FMath::Max(Landing->HalfWidth - RoomRounding, 0.25f);
const float HalfZ = FMath::Max(Height * 0.5f - RoomRounding, 0.25f);
const float HalfX = FMath::Max(Landing->HalfWidth - RoomRounding,
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;
if (VF_LatticeBoxTouchesAABB(
VoxelBox, LatticeOrigin, Step,
@@ -5773,7 +5792,8 @@ bool UVoxelStrateManager::AnyLandingFloorAtLattice(
// structural writer, not evidence that the writer is absent.
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;
return VF_LatticeAxisRange(
FMath::Max((float)VoxelBox.Min.X,
@@ -5899,7 +5919,8 @@ bool UVoxelStrateManager::AnyLandingFloorAtLattice(
{
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;
if (VF_LatticeAxisRange(
FMath::Max((float)VoxelBox.Min.X, -HalfWidth - 0.0f),
+337 -108
View File
@@ -764,8 +764,8 @@ namespace
FVoxelStrateMeasureSettings VF_ComposerMeasureSettings()
{
FVoxelStrateMeasureSettings Settings;
Settings.SampleStep = 4;
Settings.RadiusInVoxels = 256;
Settings.SampleStep = 8;
Settings.RadiusInVoxels = 512;
Settings.CenterXY = FVector2D::ZeroVector;
Settings.MaxCells = 8000000;
Settings.MaxRouteRetries = 16;
@@ -1752,6 +1752,26 @@ void AVoxelWorld::EndPlay(const EEndPlayReason::Type EndPlayReason)
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();
GVoxelForgeDigMotionTests.Remove(this);
GVoxelForgeVisualGapAudits.Remove(this);
@@ -1776,6 +1796,16 @@ void AVoxelWorld::BeginPlay()
VoxelForgeRedefinedDensityField::SetParameterSet(ResearchParameterizedCaveSet);
}
#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::FStageScope StartupTraceStage(TEXT("BeginPlay"));
Super::BeginPlay();
@@ -2095,6 +2125,38 @@ void AVoxelWorld::BeginPlay()
FParse::Value(FCommandLine::Get(), TEXT("voxel.TestCeilingCaptureDelaySeconds="),
TestCeilingCaptureDelaySeconds);
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="),
TestCeilingWallCloseup);
TestCeilingViewBeginSeconds = FPlatformTime::Seconds();
@@ -2108,6 +2170,32 @@ void AVoxelWorld::BeginPlay()
TestCeilingCameraRotation.Yaw, TestCeilingCameraRotation.Roll,
HeadlessStreamingTestCenterVoxel.X, HeadlessStreamingTestCenterVoxel.Y,
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
@@ -2499,7 +2587,8 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
bHeadlessCollisionGateStressTestFailed = false;
bHeadlessCollisionGateStressTestExitRequested = false;
bHeadlessCollisionGateStressTestGateWasEngaged = false;
bHeadlessCollisionGateStressTestHorizontalGateProbeApplied = false;
bHeadlessCollisionGateStressTestFullStopProbeApplied = false;
bHeadlessCollisionGateStressTestFullStopProbeObserved = false;
bHeadlessCollisionGateStressTestFullStop = false;
bHeadlessCollisionGateStressTestStartHeightCrossed = false;
bHeadlessCollisionGateStressTestTerrainViolation = false;
@@ -2521,6 +2610,7 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
HeadlessCollisionGateStressTestLandedSeconds = 0.0;
HeadlessCollisionGateStressTestGateStartSeconds = 0.0;
HeadlessCollisionGateStressTestFullStopStartSeconds = 0.0;
HeadlessCollisionGateStressTestFullStopProbeStartSeconds = 0.0;
HeadlessCollisionGateStressTestDistanceCm = 0.0;
HeadlessCollisionGateStressTestGateDistanceCm = 0.0;
HeadlessCollisionGateStressTestTotalGateDurationSeconds = 0.0;
@@ -2549,8 +2639,8 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
bHeadlessSurfaceFallTestExitRequested = false;
bHeadlessSurfaceFallTestPredictionProbeInjected = false;
bHeadlessSurfaceFallTestPredictionProbeCleared = false;
HeadlessSurfaceFallTestSpawnHeightVoxels = 20.0f;
HeadlessSurfaceFallTestGroundToleranceVoxels = 4.0f;
HeadlessSurfaceFallTestSpawnHeightVoxels = 5.0f * 100.0f / VOXEL_SIZE;
HeadlessSurfaceFallTestGroundToleranceVoxels = 1.0f * 100.0f / VOXEL_SIZE;
HeadlessSurfaceFallTestTimeoutSeconds = 8.0f;
HeadlessSurfaceFallTestPlayerHalfHeightVoxels =
VoxelPassageGeometry::PlayerHalfHeightVoxels;
@@ -2715,7 +2805,7 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
HeadlessStrateCrossingTestStartDelaySeconds = 1.0f;
HeadlessStrateCrossingTestTimeoutSeconds = 120.0f;
HeadlessStrateCrossingTestMaxGateSeconds = 10.0f;
HeadlessStrateCrossingTestRouteTargetRadiusVoxels = 2.0f;
HeadlessStrateCrossingTestRouteTargetRadiusVoxels = 0.5f * 100.0f / VOXEL_SIZE;
FParse::Value(CommandLine, TEXT("voxel.TestStrateCrossingSpeedCmPerSecond="),
HeadlessStrateCrossingTestSpeedCmPerSecond);
FParse::Value(CommandLine, TEXT("voxel.TestStrateCrossingStartDelaySeconds="),
@@ -3434,7 +3524,7 @@ void AVoxelWorld::ObserveHeadlessStrateCrossingTest(
const float Clearance = FeetZ - PassageFloorZ;
HeadlessStrateCrossingTestMinFloorClearanceVoxels = FMath::Min(
HeadlessStrateCrossingTestMinFloorClearanceVoxels, Clearance);
constexpr float FloorViolationToleranceVoxels = 2.0f;
constexpr float FloorViolationToleranceVoxels = 0.5f * 100.0f / VOXEL_SIZE;
if (Clearance < -FloorViolationToleranceVoxels)
{
bHeadlessStrateCrossingTestFailed = true;
@@ -3688,8 +3778,9 @@ void AVoxelWorld::AdvanceHeadlessDigMotionTest(FVector& InOutPlayerPosition, APa
BestDirection = Index;
}
}
constexpr float DigOffsetVoxels = 8.0f;
if (Clearances[BestDirection] < DigOffsetVoxels + 3.0f)
constexpr float DigOffsetVoxels = 2.0f * 100.0f / VOXEL_SIZE;
constexpr float DigClearanceMarginVoxels = 0.75f * 100.0f / VOXEL_SIZE;
if (Clearances[BestDirection] < DigOffsetVoxels + DigClearanceMarginVoxels)
{
return;
}
@@ -3996,10 +4087,12 @@ bool AVoxelWorld::MeasureHeadlessCollisionGateStressTerrain(
// 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
// 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;
const int32 MaxSearchSteps = bHeadlessCollisionGateStressDeepDrop
&& !bHeadlessCollisionGateStressDeepDropSurfaceCached
? 512 : 256;
? 1024 : 512;
float LastZ = FeetZ;
float LastDensity = OutFeetDensity;
const bool bSearchDown = OutFeetDensity >= 0.0f;
@@ -4051,6 +4144,13 @@ void AVoxelWorld::AdvanceHeadlessCollisionGateStressTest(
}
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 DeepDropBottomChunkZ = 0;
if (bHeadlessCollisionGateStressDeepDrop)
@@ -4085,16 +4185,19 @@ void AVoxelWorld::AdvanceHeadlessCollisionGateStressTest(
// world only.
constexpr float DeepDropCenterX = 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
+ (HeadlessCollisionGateStressTestSpeedCmPerSecond
* HeadlessCollisionGateStressTestDurationSeconds / VOXEL_SIZE)
+ 64.0f;
+ VoxelPassageGeometry::MetersToVoxels(16.0f);
const float StrateTopExclusiveVoxels =
static_cast<float>(DeepDropTopChunkZ + 1) * CHUNK_SIZE;
const float ShaftTopVoxels = StrateTopExclusiveVoxels + 32.0f;
const float ShaftTopVoxels = StrateTopExclusiveVoxels
+ VoxelPassageGeometry::MetersToVoxels(8.0f);
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)
{
bHeadlessCollisionGateStressTestFailed = true;
@@ -4115,7 +4218,9 @@ void AVoxelWorld::AdvanceHeadlessCollisionGateStressTest(
const FVector ShaftCenterWorld = LocalVoxelToWorld(FVector(
ShaftCenterX, DeepDropCenterY,
(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(
FMath::Max3(ShaftHalfExtent.X, ShaftHalfExtent.Y, ShaftHalfExtent.Z),
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
// must fall onto a freshly requested level-0 tile before the horizontal diagnostic.
const float LaunchLocalVoxelX = bHeadlessCollisionGateStressDeepDrop
? CHUNK_SIZE * 0.5f : 18.2f;
? CHUNK_SIZE * 0.5f : VoxelPassageGeometry::MetersToVoxels(4.55f);
const float LaunchLocalVoxelY = bHeadlessCollisionGateStressDeepDrop
? CHUNK_SIZE * 0.5f : 0.0f;
float LaunchHeightAboveGroundVoxels =
@@ -4314,7 +4419,8 @@ void AVoxelWorld::AdvanceHeadlessCollisionGateStressTest(
const float StrateTopExclusiveVoxels =
static_cast<float>(SurfaceStrateTopChunkZ + 1) * CHUNK_SIZE;
LaunchLocalVoxelZ = StrateTopExclusiveVoxels
- CapsuleHalfHeightVoxels - 0.5f;
- CapsuleHalfHeightVoxels
- VoxelPassageGeometry::MetersToVoxels(0.125f);
LaunchHeightAboveGroundVoxels = LaunchLocalVoxelZ
- SurfaceLocalVoxelZ - CapsuleHalfHeightVoxels;
const FIntVector LaunchCenterTile(
@@ -4322,7 +4428,7 @@ void AVoxelWorld::AdvanceHeadlessCollisionGateStressTest(
FMath::FloorToInt(LaunchLocalVoxelY / CHUNK_SIZE),
FMath::FloorToInt(LaunchLocalVoxelZ / CHUNK_SIZE));
const int32 ClipRadius = Settings != nullptr
? FMath::Max(1, Settings->ClipRadius) : 3;
? FMath::Max(1, Settings->ClipRadius) : 6;
VoxelClipmapDesiredTiles::AlignedRingBounds(
LaunchCenterTile, ClipRadius, 2, LevelZeroRingMin, LevelZeroRingMax);
if (LaunchHeightAboveGroundVoxels <= 0.0f
@@ -4608,9 +4714,8 @@ void AVoxelWorld::AdvanceHeadlessCollisionGateStressTest(
return;
}
// The measured interval starts with the vertical drop. Horizontal movement begins only after
// CharacterMovement reports a landing, so the test observes the entire fall and then checks
// that support gating leaves horizontal movement available.
// The measured interval starts with the vertical drop. The gate must also stop a small
// horizontal probe while support collision is unresolved, then resume normal swept movement.
if (!bHeadlessCollisionGateStressTestLanded)
{
if (bHeadlessCollisionGateStressDeepDrop
@@ -4626,11 +4731,11 @@ void AVoxelWorld::AdvanceHeadlessCollisionGateStressTest(
Velocity.X = 100.0f;
CharacterMovement->Velocity = Velocity;
CharacterMovement->UpdateComponentVelocity();
if (!bHeadlessCollisionGateStressTestHorizontalGateProbeApplied)
if (!bHeadlessCollisionGateStressTestFullStopProbeApplied)
{
bHeadlessCollisionGateStressTestHorizontalGateProbeApplied = true;
bHeadlessCollisionGateStressTestFullStopProbeApplied = true;
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);
}
}
@@ -4652,8 +4757,7 @@ void AVoxelWorld::AdvanceHeadlessCollisionGateStressTest(
{
if (UCharacterMovementComponent* CharacterMovement = Character->GetCharacterMovement())
{
// The baseline gate changes MOVE_Walking to MOVE_None. Do not undo that here; the
// test must observe the resulting full stop. The fall-only gate keeps this input live.
// Do not undo MOVE_None while the collision gate is engaged; the gate owns the hold.
if (!bPawnCollisionGateEngaged
&& CharacterMovement->MovementMode == MOVE_None)
{
@@ -4687,15 +4791,6 @@ void AVoxelWorld::ObserveHeadlessCollisionGateStressTest(
HeadlessCollisionGateStressTestLastActualPosition = PlayerPosition;
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)
{
HeadlessCollisionGateStressTestGateDistanceCm += HorizontalDelta;
@@ -4723,6 +4818,33 @@ void AVoxelWorld::ObserveHeadlessCollisionGateStressTest(
}
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
// 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
@@ -4986,6 +5108,37 @@ void AVoxelWorld::ObserveHeadlessCollisionGateStressTest(
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;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGateStressTest] trace frame=%d elapsed_s=%.6f "
@@ -5187,7 +5340,7 @@ void AVoxelWorld::AdvanceHeadlessSurfaceFallTest(
if (bProbeSupportReady
&& bProbeSupportInLayout
&& LocalPosition.Z - HeadlessSurfaceFallTestPlayerHalfHeightVoxels
> HeadlessSurfaceFallTestTerrainZ + 2.0f)
> HeadlessSurfaceFallTestTerrainZ + 0.5f * 100.0f / VOXEL_SIZE)
{
bHeadlessSurfaceFallTestPredictionProbeInjected = true;
HeadlessSurfaceFallTestPredictionProbePosition = InOutPlayerPosition;
@@ -5393,13 +5546,13 @@ void AVoxelWorld::MaybeFinishHeadlessCollisionGateStressTest()
if (bHeadlessCollisionGateStressTestPassed
&& !bHeadlessCollisionGateStressTestFailed
&& bHeadlessCollisionGateStressDeepDrop && bCollisionGateEnabled
&& (!bHeadlessCollisionGateStressTestHorizontalGateProbeApplied
|| HeadlessCollisionGateStressTestGateDistanceCm < 1.0))
&& (!bHeadlessCollisionGateStressTestFullStopProbeApplied
|| !bHeadlessCollisionGateStressTestFullStopProbeObserved))
{
bHeadlessCollisionGateStressTestPassed = false;
bHeadlessCollisionGateStressTestFailed = true;
HeadlessCollisionGateStressTestFailureReason =
TEXT("gate_horizontal_movement_blocked");
TEXT("gate_full_stop_probe_not_observed");
}
if (!bHeadlessCollisionGateStressTestPassed && !bHeadlessCollisionGateStressTestFailed)
@@ -5503,7 +5656,7 @@ void AVoxelWorld::MaybeFinishHeadlessCollisionGateStressTest()
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 "
"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 "
"support_tile=(%d,%d,%d) support_first_check_to_submit_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_surface_vox_z=%.4f final_position=(%.1f,%.1f,%.1f)"),
bPassed ? TEXT("PASS") : TEXT("FAIL"),
bPassed ? TEXT("capsule_never_below_density_surface")
bPassed && HeadlessCollisionGateStressTestFailureReason.IsEmpty()
? TEXT("capsule_never_below_density_surface")
: *HeadlessCollisionGateStressTestFailureReason,
Now - HeadlessCollisionGateStressTestBeginSeconds,
HeadlessCollisionGateStressTestDistanceCm / 100.0,
@@ -5527,7 +5681,8 @@ void AVoxelWorld::MaybeFinishHeadlessCollisionGateStressTest()
HeadlessCollisionGateStressTestTotalGateDurationSeconds,
HeadlessCollisionGateStressTestMaxGateDurationSeconds,
HeadlessCollisionGateStressTestGateDistanceCm / 100.0,
bHeadlessCollisionGateStressTestHorizontalGateProbeApplied ? 1 : 0,
bHeadlessCollisionGateStressTestFullStopProbeApplied ? 1 : 0,
bHeadlessCollisionGateStressTestFullStopProbeObserved ? 1 : 0,
HeadlessCollisionGateStressTestFullStopCount,
HeadlessCollisionGateStressTestFullStopDurationSeconds,
bCollisionGateEnabled ? 1 : 0,
@@ -6339,6 +6494,38 @@ void AVoxelWorld::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
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))
{
DensityVolume->NotifyShutdown();
@@ -6709,16 +6896,16 @@ void AVoxelWorld::Tick(float DeltaTime)
Camera->AddInstanceComponent(CaptureHeadlamp);
CaptureHeadlamp->SetupAttachment(Camera->GetRootComponent());
CaptureHeadlamp->SetMobility(EComponentMobility::Movable);
CaptureHeadlamp->SetIntensity(80000.0f);
CaptureHeadlamp->SetAttenuationRadius(10000.0f);
CaptureHeadlamp->SetUseInverseSquaredFalloff(false);
CaptureHeadlamp->SetLightFalloffExponent(0.0f);
CaptureHeadlamp->SetIntensity(6500.0f);
CaptureHeadlamp->SetAttenuationRadius(1600.0f);
CaptureHeadlamp->SetUseInverseSquaredFalloff(true);
CaptureHeadlamp->SetSourceRadius(8.0f);
CaptureHeadlamp->SetLightColor(FLinearColor(1.0f, 0.88f, 0.72f));
CaptureHeadlamp->SetCastShadows(false);
CaptureHeadlamp->SetCastShadows(true);
CaptureHeadlamp->RegisterComponent();
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeTestCeiling] camera_headlamp=1 intensity=80000 radius_cm=10000 "
"inverse_square=0 falloff_exponent=0"));
TEXT("[VoxelForgeTestCeiling] camera_headlamp=1 intensity_lm=6500 radius_cm=1600 "
"inverse_square=1 source_radius_cm=8 shadows=1"));
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeTestCeiling] camera_installed=1 applied_visible_tiles=%llu"),
@@ -6726,12 +6913,64 @@ void AVoxelWorld::Tick(float DeltaTime)
}
}
if (TestCeilingCamera.IsValid() && !bTestCeilingCaptureRequested
&& AppliedVisibleTileCount >= 16
&& FPlatformTime::Seconds() - TestCeilingViewBeginSeconds
>= static_cast<double>(TestCeilingCaptureDelaySeconds))
const double CameraNow = FPlatformTime::Seconds();
const double CameraElapsedSeconds = CameraNow - TestCeilingViewBeginSeconds;
const bool bCameraMoveWindow = TestCeilingCameraMoveSpeedCmPerSecond > 0.0f
&& 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;
}
else
{
bTestCeilingSecondCaptureRequested = true;
}
FVector ActiveViewWorldPosition = FVector::ZeroVector;
FRotator ActiveViewRotation = FRotator::ZeroRotator;
PC->GetPlayerViewPoint(ActiveViewWorldPosition, ActiveViewRotation);
@@ -6761,10 +7000,11 @@ void AVoxelWorld::Tick(float DeltaTime)
&& GEngine->GameViewport->GetGameViewport()
&& GEngine->GameViewport->GetGameViewport()->TakeHighResScreenShot();
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"),
CaptureIndex,
bCaptureAccepted ? 1 : 0,
FPlatformTime::Seconds() - TestCeilingViewBeginSeconds,
CameraElapsedSeconds,
static_cast<unsigned long long>(AppliedVisibleTileCount),
static_cast<unsigned long long>(AppliedTriangleCount));
}
@@ -7174,7 +7414,7 @@ void AVoxelWorld::RequestCollisionGateSupportTile(const FVoxelTileKey& SupportTi
FIntVector RingMin = FIntVector::ZeroValue;
FIntVector RingMax = FIntVector::ZeroValue;
VoxelClipmapDesiredTiles::AlignedRingBounds(
CurrentCenterChunk, Settings ? FMath::Max(1, Settings->ClipRadius) : 3,
CurrentCenterChunk, Settings ? FMath::Max(1, Settings->ClipRadius) : 6,
2, RingMin, RingMax);
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGateStressTest] gate_context target=(%d,%d,%d) "
@@ -7467,7 +7707,7 @@ void AVoxelWorld::UpdatePawnCollisionGate(APawn* Pawn, const FVector& PlayerPosi
{
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGate] waiting pawn=%s current_support=(%d,%d,%d) ready=%d "
"hold_fall=1"),
"hold_movement=1"),
*Pawn->GetName(),
CurrentSupportTile.Coord.X, CurrentSupportTile.Coord.Y, CurrentSupportTile.Coord.Z,
bCurrentSupportReady ? 1 : 0);
@@ -7483,37 +7723,25 @@ void AVoxelWorld::EngagePawnCollisionGate(APawn* Pawn)
}
if (bPawnCollisionGateEngaged)
{
// Re-apply the vertical hold every game-thread sample. Movement input and horizontal
// velocity are intentionally untouched.
// Keep the whole pawn stopped until the support tile has collision. Saving/restoring the
// pre-gate velocity lets normal movement resume once the tile is ready.
if (UCharacterMovementComponent* CharacterMovement =
CollisionGatedCharacterMovement.Get())
{
if (CharacterMovement->MovementMode != MOVE_Flying)
if (CharacterMovement->MovementMode != MOVE_None)
{
CharacterMovement->SetMovementMode(MOVE_Flying);
CharacterMovement->SetMovementMode(MOVE_None);
}
CharacterMovement->GravityScale = 0.0f;
FVector Velocity = CharacterMovement->Velocity;
if (Velocity.Z < 0.0f)
{
SavedGateVerticalVelocity = Velocity.Z;
bSavedGateVerticalVelocity = true;
Velocity.Z = 0.0f;
CharacterMovement->Velocity = Velocity;
CharacterMovement->UpdateComponentVelocity();
}
CharacterMovement->StopMovementImmediately();
}
else if (UPawnMovementComponent* PawnMovement = CollisionGatedPawnMovement.Get())
{
FVector Velocity = PawnMovement->Velocity;
if (Velocity.Z < 0.0f)
if (bSavedPawnMovementWasActive && PawnMovement->IsActive())
{
SavedGateVerticalVelocity = Velocity.Z;
bSavedGateVerticalVelocity = true;
Velocity.Z = 0.0f;
PawnMovement->Velocity = Velocity;
PawnMovement->UpdateComponentVelocity();
PawnMovement->Deactivate();
}
PawnMovement->StopMovementImmediately();
}
return;
}
@@ -7531,16 +7759,11 @@ void AVoxelWorld::EngagePawnCollisionGate(APawn* Pawn)
SavedCharacterCustomMovementMode = CharacterMovement->CustomMovementMode;
bSavedCharacterMovementMode = true;
FVector Velocity = CharacterMovement->Velocity;
if (Velocity.Z < 0.0f)
{
SavedGateVerticalVelocity = Velocity.Z;
bSavedGateVerticalVelocity = true;
Velocity.Z = 0.0f;
CharacterMovement->Velocity = Velocity;
}
CharacterMovement->SetMovementMode(MOVE_Flying);
SavedGateVelocity = CharacterMovement->Velocity;
bSavedGateVelocity = true;
CharacterMovement->SetMovementMode(MOVE_None);
CharacterMovement->GravityScale = 0.0f;
CharacterMovement->StopMovementImmediately();
CharacterMovement->UpdateComponentVelocity();
bPawnCollisionGateEngaged = true;
PawnGateEngagedAtSeconds = FPlatformTime::Seconds();
@@ -7551,15 +7774,15 @@ void AVoxelWorld::EngagePawnCollisionGate(APawn* Pawn)
if (UPawnMovementComponent* PawnMovement = Pawn->GetMovementComponent())
{
CollisionGatedPawnMovement = PawnMovement;
FVector Velocity = PawnMovement->Velocity;
if (Velocity.Z < 0.0f)
SavedGateVelocity = PawnMovement->Velocity;
bSavedGateVelocity = true;
bSavedPawnMovementWasActive = PawnMovement->IsActive();
if (bSavedPawnMovementWasActive)
{
SavedGateVerticalVelocity = Velocity.Z;
bSavedGateVerticalVelocity = true;
Velocity.Z = 0.0f;
PawnMovement->Velocity = Velocity;
PawnMovement->UpdateComponentVelocity();
PawnMovement->Deactivate();
}
PawnMovement->StopMovementImmediately();
PawnMovement->UpdateComponentVelocity();
bPawnCollisionGateEngaged = true;
PawnGateEngagedAtSeconds = FPlatformTime::Seconds();
return;
@@ -7584,8 +7807,9 @@ void AVoxelWorld::ReleasePawnCollisionGate()
CollisionGatedCharacterMovement.Reset();
bSavedCharacterGravityScale = false;
bSavedCharacterMovementMode = false;
bSavedGateVerticalVelocity = false;
SavedGateVerticalVelocity = 0.0f;
bSavedGateVelocity = false;
SavedGateVelocity = FVector::ZeroVector;
bSavedPawnMovementWasActive = false;
bPawnGateSupportRequestReported = false;
PawnGateEngagedAtSeconds = 0.0;
return;
@@ -7603,17 +7827,21 @@ void AVoxelWorld::ReleasePawnCollisionGate()
{
CharacterMovement->GravityScale = SavedCharacterGravityScale;
}
if (bSavedGateVerticalVelocity && CharacterMovement->Velocity.Z <= KINDA_SMALL_NUMBER)
if (bSavedGateVelocity)
{
CharacterMovement->Velocity.Z = SavedGateVerticalVelocity;
CharacterMovement->Velocity = SavedGateVelocity;
}
CharacterMovement->UpdateComponentVelocity();
}
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();
}
@@ -7632,8 +7860,9 @@ void AVoxelWorld::ReleasePawnCollisionGate()
CollisionGatedCharacterMovement.Reset();
bSavedCharacterGravityScale = false;
bSavedCharacterMovementMode = false;
bSavedGateVerticalVelocity = false;
SavedGateVerticalVelocity = 0.0f;
bSavedGateVelocity = false;
SavedGateVelocity = FVector::ZeroVector;
bSavedPawnMovementWasActive = false;
bPawnCollisionGateEngaged = false;
PawnGateEngagedAtSeconds = 0.0;
bPawnGateSupportRequestReported = false;
@@ -9171,7 +9400,7 @@ void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& Pla
DesiredEpochStartCycles = FPlatformTime::Cycles64();
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;
// 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;
VoxelClipmapDesiredTiles::ResolveVerticalBounds(
Center.Z,
Settings ? Settings->ViewDistanceUp : 5,
Settings ? Settings->ViewDistanceDown : 5,
Settings ? Settings->StrateViewMarginChunks : 3,
Settings ? Settings->ViewDistanceUp : 10,
Settings ? Settings->ViewDistanceDown : 10,
Settings ? Settings->StrateViewMarginChunks : 6,
Settings && Settings->bClampViewToStrate,
bHasStrate, StrateTopZ, StrateBottomZ, ZLo, ZHi);
@@ -9205,7 +9434,7 @@ void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& Pla
}
const int32 PawnCollisionRadius = FMath::Clamp(
Settings ? Settings->PawnCollisionRadiusChunks : 1, 1, 4);
Settings ? Settings->PawnCollisionRadiusChunks : 2, 1, 4);
VoxelCollisionDesiredTiles::AddBox(
CollisionDesiredTiles, Center, 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
// still cover a desired surface outside this shell, so the caller checks replacement readiness
// 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;
int32 MaxLevel = MaxLevelBase, ROuter = R;
VF_OuterShell(Settings, R, MaxLevelBase, MaxLevel, ROuter);
@@ -10157,7 +10386,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
const FIntVector CohortCenter = WorldToChunkCoord(
WorldToLocalCm(PlayerPosition));
const int32 PawnRadius = FMath::Clamp(
Settings ? Settings->PawnCollisionRadiusChunks : 1, 1, 4);
Settings ? Settings->PawnCollisionRadiusChunks : 2, 1, 4);
VoxelCollisionDesiredTiles::AddBox(
Step1SupportCohortTiles, CohortCenter, 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
// builder. Radius 0 is the legacy unbounded/no-op mode.
float WorldRadiusVoxels = 8192.0f;
float EdgeSealThickness = 64.0f;
float EdgeSealThickness = 128.0f;
// Bornes Z de la strate en coords VOXEL (pas cm).
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
// 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.
float OriginSpineRadius = 14.0f;
float OriginSpineRadius = 28.0f;
// 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.
float WorldRadiusVoxels = 8192.0f;
// 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)
@@ -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
// faces externes de la tuile. Voir GenerateMesh.
bool bGenerateSkirts = true;
// Profondeur de la jupe, en CELLULES de la tuile (× Step × VOXEL_SIZE). ~2 cellules couvrent
// l'écart vers un voisin un niveau plus grossier (cellule 2×). Monter si des fissures persistent.
float SkirtCells = 2.0f;
// Profondeur de la jupe, en CELLULES de la tuile (× Step × VOXEL_SIZE). Four L0 cells preserve
// the 0.5 m default extrusion at 12.5 cm per voxel.
float SkirtCells = 4.0f;
// 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.
+52 -43
View File
@@ -12,39 +12,43 @@
namespace VoxelPassageGeometry
{
// Authored scale and player body used by the §6.5 landing calculation.
constexpr float VoxelSizeMeters = 0.25f;
constexpr float PlayerRadiusVoxels = 34.0f / 25.0f;
constexpr float PlayerHalfHeightVoxels = 88.0f / 25.0f;
constexpr float VoxelSizeMeters = VOXEL_SIZE / 100.0f;
constexpr float MetersToVoxels(float Meters)
{
return Meters / VoxelSizeMeters;
}
constexpr float PlayerRadiusVoxels = MetersToVoxels(0.34f);
constexpr float PlayerHalfHeightVoxels = MetersToVoxels(0.88f);
constexpr float PlayerHeightVoxels = 2.0f * PlayerHalfHeightVoxels;
constexpr float HeadroomVoxels = 4.0f; // 1 m
constexpr float MinimumTurnFloorWidthVoxels = 12.0f; // 3 m
constexpr float HeadroomVoxels = MetersToVoxels(1.0f);
constexpr float MinimumTurnFloorWidthVoxels = MetersToVoxels(3.0f);
constexpr float MinimumLandingHalfWidthVoxels =
MinimumTurnFloorWidthVoxels * 0.5f;
constexpr float MinimumRoomHeightVoxels = 12.0f; // 3 m
constexpr float LandingFloorThicknessVoxels = 3.0f;
constexpr float MaxStepHeightVoxels = 1.8f; // 45 cm at 25 cm/voxel
constexpr float MinimumRoomHeightVoxels = MetersToVoxels(3.0f);
constexpr float LandingFloorThicknessVoxels = MetersToVoxels(0.75f);
constexpr float MaxStepHeightVoxels = MetersToVoxels(0.45f);
// 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.
constexpr float WalkCorridorPlayerWidthVoxels = 1.36f;
constexpr float WalkCorridorPlayerHeightVoxels = 7.0f;
constexpr float WalkCorridorLatticeMarginVoxels = 1.0f;
constexpr float SealSafetyMarginVoxels = 4.0f;
constexpr float RoomRoundingVoxels = 1.5f;
constexpr float LandingCarveBlendVoxels = 4.0f;
constexpr float WalkCorridorPlayerWidthVoxels = MetersToVoxels(0.68f);
constexpr float WalkCorridorPlayerHeightVoxels = MetersToVoxels(1.76f);
constexpr float WalkCorridorLatticeMarginVoxels = MetersToVoxels(0.25f);
constexpr float SealSafetyMarginVoxels = MetersToVoxels(1.0f);
constexpr float RoomRoundingVoxels = MetersToVoxels(0.375f);
constexpr float LandingCarveBlendVoxels = MetersToVoxels(1.0f);
// The origin landing is reserved for the same straight spine that the vertical-shaft
// generator uses: OriginSpineRadius on each side, with a three-voxel / 0.75 m floor ledge
// around it after the support patch's one-voxel inset. At the default radius (14 voxels) this
// generator uses: OriginSpineRadius on each side, with a six-voxel / 0.75 m floor ledge
// 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.
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
// 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.
constexpr float WalkableTunnelTurnTransitionVoxels = 6.0f; // 1.5 m
// Keep the tunnel-air post a half voxel above the authored support plane. This is a numerical
constexpr float WalkableTunnelTurnTransitionVoxels = MetersToVoxels(1.5f);
// 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
// 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
// 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
// proven canonical route while keeping the 0.1-degree collect window finite and bounded.
constexpr int32 TunnelFloorMaximumWindWaves = 6;
constexpr float TunnelFloorMaximumSingleWaveRunVoxels = 640.0f;
constexpr float TunnelFloorMaximumSingleWaveRunVoxels = MetersToVoxels(160.0f);
constexpr float TunnelFloorMaximumRouteLengthVoxels =
static_cast<float>(TunnelFloorMaximumWindWaves)
* TunnelFloorMaximumSingleWaveRunVoxels;
@@ -107,12 +111,13 @@ namespace VoxelPassageGeometry
FORCEINLINE float NativePassageFloorSupportRadius(float StartRadius, float EndRadius)
{
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);
}
// 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
// MC-facing tunnel and native-floor stages before collision density is returned.
FORCEINLINE float WalkableTunnelTurnLegSeparation(
@@ -131,7 +136,7 @@ namespace VoxelPassageGeometry
{
return FMath::Min(
LandingFloorThicknessVoxels,
FMath::Max(0.5f, FMath::Abs(Radius) * 0.5f));
FMath::Max(MetersToVoxels(0.125f), FMath::Abs(Radius) * 0.5f));
}
FORCEINLINE float VerticalShaftConnectorFloorZ(
@@ -212,7 +217,8 @@ namespace VoxelPassageGeometry
{
const float SafeRadius = FMath::Abs(Radius);
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)
|| !(BaseDensity > 0.0f)
|| !VoxelMath::IsFinite(WorldX)
@@ -277,7 +283,8 @@ namespace VoxelPassageGeometry
const float SafeRadius = FMath::Abs(Radius);
// Leave a one-voxel wall band for the roughness/solid shell, while retaining more than
// enough width for the 1.36-voxel player capsule and its support stencil.
const float CoreRadius = FMath::Max(0.5f, SafeRadius - 1.0f);
const float CoreRadius = FMath::Max(
MetersToVoxels(0.125f), SafeRadius - MetersToVoxels(0.25f));
if (!(SafeRadius > 0.0f)
|| !(BaseDensity > 0.0f)
|| !VoxelMath::IsFinite(WorldX)
@@ -442,7 +449,7 @@ namespace VoxelPassageGeometry
EndFloorZ,
HorizontalT);
OutSupportRadius = FMath::Max(
FMath::Min(StartRadius, EndRadius) - 0.5f,
FMath::Min(StartRadius, EndRadius) - MetersToVoxels(0.125f),
PlayerRadiusVoxels);
if (OutSegmentIndex != nullptr)
{
@@ -471,16 +478,19 @@ namespace VoxelPassageGeometry
FORCEINLINE float LandingHalfWidthForRadius(float Radius)
{
const float SafeRadius = FMath::Max(FMath::Abs(Radius), 1.0f);
return FMath::Max(MinimumLandingHalfWidthVoxels, SafeRadius + 2.0f);
const float SafeRadius = FMath::Max(
FMath::Abs(Radius), MetersToVoxels(0.25f));
return FMath::Max(MinimumLandingHalfWidthVoxels,
SafeRadius + MetersToVoxels(0.5f));
}
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(
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
* room dimensions therefore reuse the §6.5 formula exactly:
* half-width = max(7, radius + 2),
* height = max(12, capsule height + 4, 2 * radius + 2).
* half-width = max(1.5 m, radius + 0.5 m),
* 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
* landing the place reached from above, while every lower landing remains sealed above until
* 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
// tangent to it. The shaft diameter is 2 * OriginRadius; the extra four voxels leave a
// three-voxel / 0.75 m floor margin after the support patch's one-voxel inset at the
// authored 25 cm scale.
// tangent to it. The shaft diameter is 2 * OriginRadius; the eight-voxel / 1 m extra
// leaves six voxels / 0.75 m of floor after the support patch's two-voxel / 0.25 m inset.
Result.HalfWidth = FMath::Max(
LandingHalfWidthForRadius(OriginRadius),
FMath::Max(FMath::Abs(OriginRadius), 1.0f)
FMath::Max(FMath::Abs(OriginRadius), MetersToVoxels(0.25f))
+ OriginShaftFloorClearanceVoxels);
const float DesiredHeight = LandingHeightForRadius(OriginRadius);
const float SafeSeal = FMath::Max(SealThickness, 0.0f);
@@ -603,7 +612,7 @@ namespace VoxelPassageGeometry
constexpr float ProofPad = 1.0f;
const float FloorHalfWidth = FMath::Max(
Geometry.HalfWidth - 1.0f, 0.0f);
Geometry.HalfWidth - MetersToVoxels(0.25f), 0.0f);
return LatticeAxisHasSampleInInterval(
FMath::Max((float)VoxelBox.Min.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.
constexpr float ProofPad = 1.0f;
const float FloorHalfWidth = FMath::Max(
Geometry.HalfWidth - 1.0f, 0.0f);
Geometry.HalfWidth - MetersToVoxels(0.25f), 0.0f);
return IntersectsClosedInterval(
VoxelBox.Min.X, VoxelBox.Max.X,
-FloorHalfWidth - ProofPad, FloorHalfWidth + ProofPad)
@@ -681,9 +690,9 @@ namespace VoxelPassageGeometry
const float Rounding = RoomRoundingVoxels;
const float HalfZ = (Geometry.CeilingZ - Geometry.FloorZ) * 0.5f;
const FVector HalfExtent(
FMath::Max(Geometry.HalfWidth - Rounding, 0.25f),
FMath::Max(Geometry.HalfWidth - Rounding, 0.25f),
FMath::Max(HalfZ - Rounding, 0.25f));
FMath::Max(Geometry.HalfWidth - Rounding, MetersToVoxels(0.0625f)),
FMath::Max(Geometry.HalfWidth - Rounding, MetersToVoxels(0.0625f)),
FMath::Max(HalfZ - Rounding, MetersToVoxels(0.0625f)));
const FVector D(
FMath::Abs(Position.X - Center.X) - HalfExtent.X,
FMath::Abs(Position.Y - Center.Y) - HalfExtent.Y,
@@ -1,6 +1,7 @@
#pragma once
#include "CoreMinimal.h"
#include "VoxelTypes.h"
class UVoxelGenerator;
@@ -17,6 +18,11 @@ class UVoxelGenerator;
*/
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. */
struct FBrickEvaluationContext
{
@@ -138,17 +144,26 @@ namespace VoxelForgeRedefinedDensityField
VOXELFORGE_API void EvaluateParameterizedColumnWithDetailValues(
const UVoxelGenerator& Generator, float WorldX, float WorldY, float WorldZStart,
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(
const UVoxelGenerator& Generator, FIntVector OriginVoxels, FIntVector Size,
bool& bOutAir, float DetailScale = 1.0f,
bool* bOutEvaluatedMacroCentre = nullptr,
EBrickFallbackReason* OutFallbackReason = nullptr);
EBrickFallbackReason* OutFallbackReason = nullptr,
int32 SampleStep = 1);
VOXELFORGE_API void BuildParameterizedBrickContext(
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. */
VOXELFORGE_API float Evaluate(const UVoxelGenerator& Generator, float WorldX, float WorldY,
@@ -159,7 +174,7 @@ namespace VoxelForgeRedefinedDensityField
VOXELFORGE_API void BuildDetailBrick(const UVoxelGenerator& Generator,
FIntVector OriginVoxels, FIntVector Size, float DetailScale,
TArray<float>& OutDetail, FBrickEvaluationContext* OutContext = nullptr,
bool bParameterized = false);
bool bParameterized = false, int32 SampleStep = 1);
/** Evaluate with a detail value produced by BuildDetailBrick. */
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.
int32 StrateHeightInChunks = 8;
int32 InterStrateGapChunks = 0;
float OriginSpineRadius = 14.0f;
float OriginSpineRadius = 28.0f;
float WorldRadiusVoxels = 0.0f;
// 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
// default automation artifact is measured in minutes, while MaxCells remains bounded.
MeasureSettings.SampleStep = 8;
MeasureSettings.RadiusInVoxels = 256;
MeasureSettings.RadiusInVoxels = 512;
MeasureSettings.MaxCells = 8000000;
MeasureSettings.MaxRouteRetries = 16;
MeasureSettings.HeadroomCells = 2;
MeasureSettings.InteriorMarginVoxels = -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
// four million cells. Keep the cap finite and below the old multi-gigabyte retry cost.
PlayerFitMeasureSettings.MaxCells = 4000000;
+35 -34
View File
@@ -32,15 +32,15 @@ public:
// 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")
int32 ViewDistanceXY = 16;
int32 ViewDistanceXY = 32;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
int32 ViewDistanceUp = 5;
int32 ViewDistanceUp = 10;
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.
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
// 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"))
int32 PawnCollisionRadiusChunks = 1;
int32 PawnCollisionRadiusChunks = 2;
// 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
@@ -100,7 +100,7 @@ public:
// 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).
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 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
// hole filled by the finer level).
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 &
// 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"))
int32 MaxClipLevel = 5;
@@ -163,26 +163,26 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "4", ClampMax = "32"))
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
// OUTERMOST shell keeps generating level-MaxClipLevel tiles outward until it covers this
// 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
// 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.
// 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"))
int32 RenderDistanceChunks = 768;
int32 RenderDistanceChunks = 1536;
// 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.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap")
bool bGenerateSkirts = true;
// Skirt depth in tile CELLS (× the tile's step). ~2 covers the gap to a one-level-coarser
// neighbour (2× cell). Raise if cracks still show; lower if skirts peek out on convex edges.
// Skirt depth in tile CELLS (× the tile's step). Four L0 cells preserve the former 0.5 m
// 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"))
float SkirtCells = 2.0f;
float SkirtCells = 4.0f;
// 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
@@ -204,8 +204,9 @@ public:
// 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
// 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
// intended production value. SET IT IN THE ASSET, deliberately, once.
// asset to show why. The former 8192-voxel / 2.05 km target must be 16384 voxels at 12.5 cm.
// 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
// 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.
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)
@@ -229,14 +230,14 @@ public:
// (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).
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
// 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.
// (Repurposes the old vestigial DecorationActorRadiusChunks; same default.)
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
// 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
// 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"))
int32 DecorationRegionSizeCells = 4;
int32 DecorationRegionSizeCells = 8;
// 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
// 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"))
int32 DecorationSpacingVoxels = 4;
int32 DecorationSpacingVoxels = 8;
// 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
@@ -259,13 +260,13 @@ public:
// 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.
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
// 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).
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;
// 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
// rock underneath), so surface worlds still get their ground and caves still find layered floors.
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
// 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")
bool bEnableDensityVolume = false;
// Per-axis resolution of EACH clip level (cells). Memory per level ≈ Res³ bytes (R8). 128 →
// ~2 MB/level; 192 → ~7 MB; 256 → ~16 MB. Higher = crisper near shadows + bigger startup fill.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting", meta = (ClampMin = "32", ClampMax = "256"))
int32 DensityVolumeResolution = 128;
// Per-axis resolution of EACH clip level (cells). Memory per level ≈ Res³ bytes (R8).
// 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 = "512"))
int32 DensityVolumeResolution = 256;
// 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"))
int32 DensityVolumeLevels = 3;
@@ -341,7 +342,7 @@ public:
bool bDebugDrawDensityVolume = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting|Debug", meta = (ClampMin = "1", ClampMax = "32"))
int32 DensityVolumeDebugRadiusCells = 6;
int32 DensityVolumeDebugRadiusCells = 12;
//=========================================================================
// RENDERING
@@ -396,12 +397,12 @@ public:
// 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
// future straight shaft of diameter 2 * this radius. At the default 14-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
// future straight shaft of diameter 2 * this radius. At the default 28-voxel radius that is
// 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.
// 10-14 → compact landing room (default) · 20+ → broad landing room
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
// 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.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Carving",
meta = (ClampMin = "1.0"))
float MaxBrushRadius = 15.0f;
float MaxBrushRadius = 30.0f;
// Volume total maximum (somme des volumes de brush). 0 = illimité.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Carving",
@@ -68,11 +68,11 @@ public:
// How many chunks tall this strate is.
// 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.
// 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"))
int32 StrateHeightInChunks = 8;
int32 StrateHeightInChunks = 16;
//=========================================================================
// BOUNDARY TRANSITION
@@ -519,7 +519,7 @@ protected:
// Authored radius (voxels) of the origin landing room and its optional surface opening.
// Copied from VoxelSettings::OriginSpineRadius.
float OriginSpineRadius = 14.0f;
float OriginSpineRadius = 28.0f;
// Solid-bedrock gap between consecutive strates, in chunks. Copied from
// VoxelSettings::InterStrateGapChunks during Initialize().
+11 -10
View File
@@ -11,15 +11,16 @@ class UVoxelStrateManager;
/**
* The pinned metric capsule dimensions in voxel-space.
*
* 25 cm/voxel is the authored world scale: radius = 34 cm / 25 = 1.36 voxels and
* half-height = 88 cm / 25 = 3.52 voxels, hence a 176 cm / 25 = 7.04 voxel total height.
* Capsule measurements are authored in centimetres and converted through VOXEL_SIZE. At
* 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
* changing generation. The diagnosis test separately reads movement limits from the project's
* character CDO.
*/
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 RadiusCentimeters = 34.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
// the precise angle. The diagnosis test copies that precise project value when available.
static constexpr float WalkableFloorAngleDegrees = 44.8f;
static constexpr float RadiusVoxels = VoxelPassageGeometry::PlayerRadiusVoxels;
static constexpr float HalfHeightVoxels = VoxelPassageGeometry::PlayerHalfHeightVoxels;
static constexpr float RadiusVoxels = RadiusCentimeters / VoxelSizeCentimeters;
static constexpr float HalfHeightVoxels = HalfHeightCentimeters / VoxelSizeCentimeters;
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. */
struct VOXELFORGE_API FVoxelStrateMeasureSettings
{
int32 SampleStep = 4; // Voxels between samples.
int32 RadiusInVoxels = 256; // Half-extent in X and Y around CenterXY.
int32 SampleStep = 8; // Voxels between 1 m samples at 12.5 cm/voxel.
int32 RadiusInVoxels = 512; // Half-extent in X and Y around CenterXY (64 m).
FVector2D CenterXY = FVector2D::ZeroVector; // Actor-space voxel coordinates.
// When both points are set, replace the square centered on CenterXY with the XY AABB of the
// two points expanded by CoverMarginVoxels. This keeps a mouth-to-mouth measurement sized by
// the mouths' separation instead of by their distance from the world origin.
TOptional<FVector2D> CoverPointA;
TOptional<FVector2D> CoverPointB;
float CoverMarginVoxels = 48.0f;
float CoverMarginVoxels = 96.0f; // 12 m.
// 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.
// 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 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
// 88 cm half-height / 25 = 3.52 voxels (7.04 voxels total). These are parameters, not
// Base ACharacter dimensions are authored in centimetres and converted by the voxel scale.
// These are parameters, not
// generation values, so another playable capsule can be measured without a code change.
float PlayerCapsuleRadiusVoxels = FVoxelPlayerCapsuleConstants::RadiusVoxels;
float PlayerCapsuleHalfHeightVoxels = FVoxelPlayerCapsuleConstants::HalfHeightVoxels;
+12 -11
View File
@@ -491,7 +491,7 @@ struct VOXELFORGE_API FStrateGenerationParams
// Smallest possible room radius (in voxels).
// Small rooms feel like alcoves or nooks.
// 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
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Rooms")
float MinRoomRadius = 16.0f;
@@ -1051,7 +1051,7 @@ struct VOXELFORGE_API FStrateGenerationParams
// ----- Dome (hemispherical chamber ceilings) -----
// 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 DomeMinRadius = 16.0f;
float DomeMaxRadius = 32.0f;
@@ -1372,8 +1372,8 @@ struct VOXELFORGE_API FMazeGenerationParams
float CellSize = 64.0f;
// 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
// proven 3.75-voxel roughness envelope (default, 6.25 m nominal bore).
// These voxel-valued asset parameters retain their authored counts. At 12.5 cm/voxel
// an unchanged asset is half-scale in metres; double CorridorRadius to retain its old bore.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Maze", meta = (ClampMin = "1.0"))
float CorridorRadius = 12.5f;
@@ -1691,7 +1691,7 @@ struct VOXELFORGE_API FVerticalShaftParams
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Shafts", meta = (ClampMin = "1.0"))
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).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Shafts", meta = (ClampMin = "0.0"))
float LedgeSpacing = 32.0f;
@@ -1732,7 +1732,7 @@ struct VOXELFORGE_API FFloatingIslandParams
{
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.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Islands", meta = (ClampMin = "20.0"))
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"))
float ChasmDensity = 0.0f;
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;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Disturbance|Chasms", meta = (ClampMin = "1.0"))
float ChasmRadius = 24.0f;
@@ -1880,9 +1880,10 @@ struct VOXELFORGE_API FStratePassageConfig
// ----- WIDTH (tapers along the length) -----
// 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
// and 3.75 m at the waist (9/7.5 voxel radii at 25 cm per voxel). The §6.5 landing room
// remains wider for turning.
// The walkable default is 1.5x the original comfortable profile: 2.25 m diameter at mouths
// and 1.875 m at the waist when the stored radii remain 9/7.5 voxels at 12.5 cm per voxel.
// Double these asset values to retain the previous 4.5/3.75 m dimensions. The §6.5 landing
// room remains wider for turning.
// Mouth > Mid = chambers at the ends with a squeeze between; Mid > Mouth = a bulge.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Width", meta = (ClampMin = "1.0"))
float MouthRadius = 9.0f;
@@ -2385,7 +2386,7 @@ struct VOXELFORGE_API FStrateLandmark
float OrbRadiusVoxels = 16.0f;
// 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"))
float OrbFalloffVoxels = 2000.0f;
+2 -2
View File
@@ -201,7 +201,7 @@ namespace VoxelMath
//
// Taille de chunk: 32^3 = 32 768 voxels.
// 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
// 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 float VOXEL_SIZE = 25.0f;
constexpr float VOXEL_SIZE = 12.5f;
//=============================================================================
// TRIVIAL-TILE CLASSIFICATION (T1.d)
+47 -24
View File
@@ -388,22 +388,22 @@ public:
* Applies a spherical brush that subtracts density.
*
* @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)
*/
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).
* Applies a spherical brush that adds density.
*
* @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)
*/
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
@@ -420,10 +420,10 @@ public:
* tube radius in voxels. Great for boring tunnels or laying solid pillars/walls.
*/
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")
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
@@ -581,6 +581,12 @@ public:
private:
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).
* Returns Base unchanged-wrapped, or nullptr if Base is null. */
UMaterialInstanceDynamic* GetOrCreateTerrainMID(UMaterialInterface* Base);
@@ -713,7 +719,7 @@ public:
/** Brush radius in voxels for the editor buttons. */
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). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Editor Brush", meta = (ClampMin = "0.1"))
@@ -934,10 +940,9 @@ public:
FVoxelTileKey* OutPendingTile = nullptr) const;
void RequestCollisionGateSupportTile(const FVoxelTileKey& SupportTile);
/** Hold vertical character motion in gravity-free MOVE_Flying while a swept pending support tile
* intersects the capsule, preserving horizontal movement until collision is ready. Restore the
* previous mode, gravity, and downward speed on release. The world tick is a prerequisite of the
* pawn and movement component so the gate check runs before movement physics. */
/** Hold pawn movement while a swept pending support tile intersects the capsule. Restore the
* previous mode, gravity, and velocity on release. The world tick is a prerequisite of the pawn
* and movement component so the gate check runs before movement physics. */
void UpdatePawnCollisionGate(APawn* Pawn, const FVector& PlayerPosition, float DeltaTime);
void EngagePawnCollisionGate(APawn* Pawn);
void ReleasePawnCollisionGate();
@@ -1165,15 +1170,31 @@ public:
// the camera and smaller test radius are held in memory and never saved to a package.
bool bTestCeilingViewEnabled = false;
bool bTestCeilingCaptureRequested = false;
bool bTestCeilingSecondCaptureRequested = false;
TWeakObjectPtr<ACameraActor> TestCeilingCamera;
FVector TestCeilingCameraWorldPosition = FVector::ZeroVector;
FRotator TestCeilingCameraRotation = FRotator::ZeroRotator;
double TestCeilingViewBeginSeconds = 0.0;
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
// the character uses ordinary swept movement across the still-pending desired set. The test
// distinguishes a gate hold from a full horizontal stop so a fall-only gate can be validated.
// the character uses ordinary swept movement toward pending support tiles. The test checks
// that an unresolved collision holds the pawn in place without letting it enter the tile.
bool bHeadlessCollisionGateStressTest = false;
bool bHeadlessCollisionGateStressTestStartPlaced = false;
bool bHeadlessCollisionGateStressTestStarted = false;
@@ -1181,7 +1202,8 @@ public:
bool bHeadlessCollisionGateStressTestFailed = false;
bool bHeadlessCollisionGateStressTestExitRequested = false;
bool bHeadlessCollisionGateStressTestGateWasEngaged = false;
bool bHeadlessCollisionGateStressTestHorizontalGateProbeApplied = false;
bool bHeadlessCollisionGateStressTestFullStopProbeApplied = false;
bool bHeadlessCollisionGateStressTestFullStopProbeObserved = false;
bool bHeadlessCollisionGateStressTestFullStop = false;
bool bHeadlessCollisionGateStressTestStartHeightCrossed = false;
bool bHeadlessCollisionGateStressTestTerrainViolation = false;
@@ -1194,7 +1216,7 @@ public:
bool bHeadlessCollisionGateStressDeepDropPrewarmReported = false;
bool bHeadlessCollisionGateStressDeepDropSurfaceCached = false;
float HeadlessCollisionGateStressTestSpeedCmPerSecond = 6000.0f;
float HeadlessCollisionGateStressTestSpawnHeightVoxels = 10.0f;
float HeadlessCollisionGateStressTestSpawnHeightVoxels = 2.5f * 100.0f / VOXEL_SIZE;
float HeadlessCollisionGateStressTestDurationSeconds = 2.0f;
float HeadlessCollisionGateStressTestTimeoutSeconds = 15.0f;
double HeadlessCollisionGateStressTestBeginSeconds = 0.0;
@@ -1202,6 +1224,7 @@ public:
double HeadlessCollisionGateStressTestLandedSeconds = 0.0;
double HeadlessCollisionGateStressTestGateStartSeconds = 0.0;
double HeadlessCollisionGateStressTestFullStopStartSeconds = 0.0;
double HeadlessCollisionGateStressTestFullStopProbeStartSeconds = 0.0;
double HeadlessCollisionGateStressTestDistanceCm = 0.0;
double HeadlessCollisionGateStressTestGateDistanceCm = 0.0;
double HeadlessCollisionGateStressTestTotalGateDurationSeconds = 0.0;
@@ -1236,10 +1259,10 @@ public:
bool bHeadlessSurfaceFallTestExitRequested = false;
bool bHeadlessSurfaceFallTestPredictionProbeInjected = false;
bool bHeadlessSurfaceFallTestPredictionProbeCleared = false;
float HeadlessSurfaceFallTestSpawnHeightVoxels = 20.0f;
float HeadlessSurfaceFallTestGroundToleranceVoxels = 4.0f;
float HeadlessSurfaceFallTestSpawnHeightVoxels = 5.0f * 100.0f / VOXEL_SIZE;
float HeadlessSurfaceFallTestGroundToleranceVoxels = 1.0f * 100.0f / VOXEL_SIZE;
float HeadlessSurfaceFallTestTimeoutSeconds = 8.0f;
float HeadlessSurfaceFallTestPlayerHalfHeightVoxels = 3.52f;
float HeadlessSurfaceFallTestPlayerHalfHeightVoxels = 88.0f / VOXEL_SIZE;
float HeadlessSurfaceFallTestTerrainZ = 0.0f;
double HeadlessSurfaceFallTestBeginSeconds = 0.0;
double HeadlessSurfaceFallTestStartSeconds = 0.0;
@@ -1279,8 +1302,8 @@ public:
float HeadlessStrateCrossingTestStartDelaySeconds = 1.0f;
float HeadlessStrateCrossingTestTimeoutSeconds = 120.0f;
float HeadlessStrateCrossingTestMaxGateSeconds = 10.0f;
float HeadlessStrateCrossingTestRouteTargetRadiusVoxels = 2.0f;
float HeadlessStrateCrossingTestPlayerHalfHeightVoxels = 3.52f;
float HeadlessStrateCrossingTestRouteTargetRadiusVoxels = 0.5f * 100.0f / VOXEL_SIZE;
float HeadlessStrateCrossingTestPlayerHalfHeightVoxels = 88.0f / VOXEL_SIZE;
float HeadlessStrateCrossingTestMinFloorClearanceVoxels = 1000000.0f;
double HeadlessStrateCrossingTestBeginSeconds = 0.0;
double HeadlessStrateCrossingTestStartSeconds = 0.0;
@@ -1309,8 +1332,7 @@ public:
int32 VisualAuditLODOverlapEvents = 0;
int32 VisualAuditLODOverlapMaxPairs = 0;
// PLAYER COLLISION GATE — only the downward component is held while the level-0 collision body
// beneath the pawn's feet is unresolved. Horizontal movement and player control remain live.
// PLAYER COLLISION GATE — all movement is held while level-0 support collision is unresolved.
TWeakObjectPtr<APawn> PawnTickPrerequisite;
TWeakObjectPtr<UPawnMovementComponent> PawnMovementTickPrerequisite;
TWeakObjectPtr<APawn> CollisionGatedPawn;
@@ -1322,8 +1344,9 @@ public:
uint8 SavedCharacterMovementMode = 0;
uint8 SavedCharacterCustomMovementMode = 0;
bool bSavedCharacterMovementMode = false;
float SavedGateVerticalVelocity = 0.0f;
bool bSavedGateVerticalVelocity = false;
FVector SavedGateVelocity = FVector::ZeroVector;
bool bSavedGateVelocity = false;
bool bSavedPawnMovementWasActive = false;
bool bPawnCollisionGateEngaged = false;
double PawnGateEngagedAtSeconds = 0.0;
bool bPawnGateUnavailableReported = false;
@@ -55,8 +55,8 @@ using FExploreJsonWriter = TJsonWriter<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>;
constexpr int32 DefaultRenderWidth = 512;
constexpr int32 DefaultRenderHeight = 288;
constexpr float DefaultRenderStepVoxels = 0.25f;
constexpr float DefaultRenderMaxDistanceVoxels = 160.0f;
constexpr float DefaultRenderStepVoxels = 0.5f; // 6.25 cm, as at 25 cm/voxel.
constexpr float DefaultRenderMaxDistanceVoxels = 320.0f; // 40 m.
constexpr int32 DefaultExportSize = 128;
constexpr int32 DefaultExportStep = 1;
constexpr int32 DefaultMaxWalkCells = 32000000;
@@ -1170,6 +1170,32 @@ struct FExploreWorld
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
? (Arguments.bGameRuntimeConfig
? FMath::Max(Settings->TotalStrates, 1)
@@ -1187,8 +1213,8 @@ struct FExploreWorld
Settings->TotalStrates = NumStrates;
Settings->InterStrateGapChunks = 0;
Settings->WorldRadiusVoxels = 0.0f;
Settings->EdgeSealThickness = 64.0f;
Settings->OriginSpineRadius = 14.0f;
Settings->EdgeSealThickness = 128.0f;
Settings->OriginSpineRadius = 28.0f;
Settings->bOpenSurfaceEntry = true;
}
else if (Arguments.bSeedOverride)
@@ -1232,9 +1258,9 @@ struct FExploreWorld
Definition->StrateDescription = FText::FromString(
TEXT("Transient, deterministic commandlet definition."));
// 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.
Definition->StrateHeightInChunks = 4;
Definition->StrateHeightInChunks = 8;
Definition->TransitionType = EVoxelStrateTransition::Hard;
Definition->GeneratorType = Index == Arguments.Slot
? Arguments.Archetype
@@ -2896,7 +2922,7 @@ FStep2dBoundaryStats ComputeStep2dBoundaryStats(
const bool bBrickBoundary = (World[Axis] % 8) == 0;
const bool bChunkBoundary = (World[Axis] % CHUNK_SIZE) == 0 && Axis < 2;
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)
{
@@ -3074,10 +3100,13 @@ FStep2eVarietyMetrics ComputeStep2eVarietyMetrics(
}
}
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.
Result.SurfaceAreaSquareMeters = static_cast<double>(SurfaceEdgeCount) * 0.25 * 0.25;
const double VolumeCubicMeters = static_cast<double>(Total) * 0.25 * 0.25 * 0.25;
constexpr double VoxelMeters = static_cast<double>(VOXEL_SIZE) / 100.0;
Result.SurfaceAreaSquareMeters = static_cast<double>(SurfaceEdgeCount)
* VoxelMeters * VoxelMeters;
const double VolumeCubicMeters = static_cast<double>(Total)
* VoxelMeters * VoxelMeters * VoxelMeters;
Result.SurfaceAreaPerVolume = VolumeCubicMeters > 0.0
? Result.SurfaceAreaSquareMeters / VolumeCubicMeters : 0.0;
const uint64 AllAdjacencies = HorizontalAdjacencies + VerticalAdjacencies;
@@ -3100,7 +3129,8 @@ FStep2eVarietyMetrics ComputeStep2eVarietyMetrics(
Result.MeanLongestVerticalAirRunVoxels = ColumnsWithAir > 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 CoarseCount = CoarseDimension * CoarseDimension * CoarseDimension;
TArray<int32> Parent;
@@ -3323,7 +3353,8 @@ FStep2fFragmentMetrics ComputeStep2fFragmentMetrics(
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;
for (int32 Index = 0; Index < Count; ++Index)
{
@@ -3427,19 +3458,40 @@ struct FStep2fGridSignatureMetrics
};
FStep2fGridSignatureMetrics ComputeStep2fGridSignature(
const TArray<float>& Density, int32 Dimension, float PitchVoxels)
const UVoxelGenerator& Generator, FIntVector RingOrigin, int32 RingDimension,
float PitchVoxels)
{
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 SampleCount = 0;
for (int32 Z = 0; Z < Dimension; Z += SampleStep)
for (int32 Y = 0; Y < Dimension; Y += SampleStep)
for (int32 X = 0; X < Dimension; X += SampleStep)
for (int32 ZIndex = 0; ZIndex < HeightLayers; ++ZIndex)
{
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;
}
}
const double Mean = SampleCount > 0
? static_cast<double>(AirCount) / SampleCount : 0.0;
const double Variance = Mean * (1.0 - Mean);
@@ -3453,31 +3505,35 @@ FStep2fGridSignatureMetrics ComputeStep2fGridSignature(
for (int32 Direction = 0; Direction < 8; ++Direction)
{
const float Angle = static_cast<float>(Direction) * 3.14159265358979323846f / 8.0f;
const int32 DX = FMath::RoundToInt(Radius * FMath::Cos(Angle));
const int32 DY = FMath::RoundToInt(Radius * FMath::Sin(Angle));
const int32 DX = FMath::RoundToInt(
Radius * FMath::Cos(Angle) / SampleStep);
const int32 DY = FMath::RoundToInt(
Radius * FMath::Sin(Angle) / SampleStep);
if (DX == 0 && DY == 0)
{
continue;
}
double Covariance = 0.0;
int64 Pairs = 0;
for (int32 Z = 0; Z < Dimension; Z += SampleStep)
for (int32 Y = 0; Y < Dimension; Y += SampleStep)
for (int32 Z = 0; Z < HeightLayers; ++Z)
for (int32 Y = 0; Y < XYDimension; ++Y)
{
const int32 OtherY = Y + DY;
if (OtherY < 0 || OtherY >= Dimension)
if (OtherY < 0 || OtherY >= XYDimension)
{
continue;
}
for (int32 X = 0; X < Dimension; X += SampleStep)
for (int32 X = 0; X < XYDimension; ++X)
{
const int32 OtherX = X + DX;
if (OtherX < 0 || OtherX >= Dimension)
if (OtherX < 0 || OtherX >= XYDimension)
{
continue;
}
const bool bAirA = Density[(Z * Dimension + Y) * Dimension + X] > 0.0f;
const bool bAirB = Density[(Z * Dimension + OtherY) * Dimension + OtherX] > 0.0f;
const bool bAirA = AirSamples[
(Z * XYDimension + Y) * XYDimension + X] != 0;
const bool bAirB = AirSamples[
(Z * XYDimension + OtherY) * XYDimension + OtherX] != 0;
Covariance += (static_cast<double>(bAirA) - Mean)
* (static_cast<double>(bAirB) - Mean);
++Pairs;
@@ -3531,22 +3587,24 @@ FStep2fDetailSpectrum ComputeStep2fDetailSpectrum(const TArray<float>& Detail, i
for (int32 X = 0; X < 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(
static_cast<double>(Detail[Index + 1] - Detail[Index]));
static_cast<double>(Detail[Index + PhysicalLagVoxels] - Detail[Index]));
++DifferenceCount;
}
if (Y + 1 < Dimension)
if (Y + PhysicalLagVoxels < Dimension)
{
DifferenceEnergy += FMath::Square(
static_cast<double>(Detail[Index + Dimension] - Detail[Index]));
static_cast<double>(Detail[Index + PhysicalLagVoxels * Dimension] - Detail[Index]));
++DifferenceCount;
}
if (Z + 1 < Dimension)
if (Z + PhysicalLagVoxels < Dimension)
{
DifferenceEnergy += FMath::Square(
static_cast<double>(Detail[Index + Dimension * Dimension] - Detail[Index]));
static_cast<double>(Detail[Index + PhysicalLagVoxels * Dimension * Dimension]
- Detail[Index]));
++DifferenceCount;
}
}
@@ -3615,9 +3673,10 @@ float Step2fPointTriangleDistanceSquared(
FIntVector SelectStep2fMeshOrigin(
const TArray<float>& Density, FIntVector RingOrigin, int32 Dimension)
{
constexpr int32 PhysicalTileVoxels = 64;
constexpr int32 ScoreStepVoxels = 4;
constexpr int32 CandidateStepVoxels = 32;
constexpr int32 VoxelsPerMeter = static_cast<int32>(100.0f / VOXEL_SIZE + 0.5f);
constexpr int32 PhysicalTileVoxels = 16 * VoxelsPerMeter;
constexpr int32 ScoreStepVoxels = VoxelsPerMeter;
constexpr int32 CandidateStepVoxels = 8 * VoxelsPerMeter;
const int32 MaximumStart = Dimension - 1 - PhysicalTileVoxels;
if (Density.Num() != Dimension * Dimension * Dimension || MaximumStart < 0)
{
@@ -3801,7 +3860,8 @@ TSharedRef<FJsonObject> Step2eChamberVolumeQuantilesJson(
{
TSharedRef<FJsonObject> Json = MakeShared<FJsonObject>();
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 TCHAR* Names[] = {TEXT("p10_m3"), TEXT("p50_m3"), TEXT("p90_m3")};
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("samples_per_axis"), Dimension);
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->SetStringField(TEXT("field_definition"),
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 "
"evaluated at every full-resolution sample that can affect the zero set."));
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. "
"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."));
@@ -4701,19 +4761,18 @@ int32 RunStep2e(const FString& Params)
Report->SetObjectField(TEXT("parameter_ranges_across_eight_profiles"),
Step2eParameterRangesJson());
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 "
"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"),
TEXT("For 12.5 cm voxels, double room pitch, room height, tunnel/feature dimensions and "
"the broad-to-medium noise periods in sample units to preserve meter-scale shapes. "
"Keep the 1-sample detail band or add a finer octave. The lattice grows 8x for a fixed "
TEXT("At 12.5 cm, metre-valued macro and noise dimensions convert to twice as many samples; "
"the four detail bands retain their metre wavelengths. The lattice grows 8x for a fixed "
"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"),
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 "
"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 "
@@ -4846,6 +4905,238 @@ int32 RunStep2e(const FString& Params)
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)
{
FExploreArguments Arguments;
@@ -4943,9 +5234,10 @@ int32 RunStep2f(const FString& Params)
FStep2fShapeSnapshot Result;
Result.Variety = ComputeStep2eVarietyMetrics(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(
(Dimension - DetailDimension) / 2,
(Dimension - DetailDimension) / 2,
@@ -4970,11 +5262,13 @@ int32 RunStep2f(const FString& Params)
const bool bPreviousSkirts = World.Mesher->bGenerateSkirts;
World.Mesher->bGenerateSkirts = false;
FVoxelMeshData FineMesh;
// GenerateMesh clamps cells per tile to CHUNK_SIZE. Eight level-0 tiles
// cover the same 64-sample cube as the one level-1 tile below.
for (int32 TileZ = 0; TileZ < 2; ++TileZ)
for (int32 TileY = 0; TileY < 2; ++TileY)
for (int32 TileX = 0; TileX < 2; ++TileX)
// GenerateMesh clamps cells per tile to CHUNK_SIZE. At 12.5 cm, a 16 m
// full-detail cube is four level-0 chunks across and two level-1 chunks across.
constexpr int32 Level0TilesPerAxis = static_cast<int32>(
16.0f / (CHUNK_SIZE * VOXEL_SIZE / 100.0f));
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(
TileX * CHUNK_SIZE, TileY * CHUNK_SIZE, TileZ * CHUNK_SIZE);
@@ -4987,8 +5281,24 @@ int32 RunStep2f(const FString& Params)
FineMesh.Triangles.Add(VertexOffset + Index);
}
}
const FVoxelMeshData CoarseMesh = World.Mesher->GenerateMesh(
MeshOrigin, 2, CHUNK_SIZE);
FVoxelMeshData CoarseMesh;
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;
Result.Mesh = ComputeStep2fMeshMetrics(FineMesh);
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
// large enough to fail the LOD gate; expanding the search makes the
// 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;
FineTriangleBuckets.Reserve(FineMesh.Triangles.Num());
for (int32 Triangle = 0; Triangle + 2 < FineMesh.Triangles.Num(); Triangle += 3)
@@ -5092,8 +5402,11 @@ int32 RunStep2f(const FString& Params)
if (World.Manager != nullptr)
{
const TArray<FStrateSlot>& Layout = World.Manager->GetLayout();
constexpr int32 XYStep = 4; // 1 m coverage grid
constexpr int32 ShellOffsets[] = {1, 5, 9};
constexpr int32 XYStep = static_cast<int32>(100.0f / VOXEL_SIZE + 0.5f);
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)
{
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("solid_fraction"), 1.0 - 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);
Json->SetNumberField(TEXT("solid_components_25cm_grid"),
Json->SetNumberField(TEXT("solid_components_12_5cm_grid"),
Shape.Fragments.SolidComponents);
Json->SetNumberField(TEXT("air_crumbs_below_1m3"),
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("nearby_autocorrelation_peak"), Shape.Grid.NeighborCorrelationPeak);
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_lattice_scale_energy_ratio"),
Shape.Detail.LatticeScaleEnergyRatio);
@@ -5207,7 +5523,7 @@ int32 RunStep2f(const FString& Params)
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 FIntVector PaddedCrumbOrigin = Origin
- FIntVector(CrumbBoundaryPadding, CrumbBoundaryPadding, CrumbBoundaryPadding);
@@ -5295,7 +5611,7 @@ int32 RunStep2f(const FString& Params)
bStep2eBandLimitRejected &= LegacyShape.Detail.LatticeScaleEnergyRatio > 0.20;
WorstStep2eLodDistance = FMath::Max(
WorstStep2eLodDistance, LegacyShape.Lod1P95SurfaceErrorVoxels);
bStep2eLodRejected |= LegacyShape.Lod1P95SurfaceErrorVoxels > 0.5;
bStep2eLodRejected |= LegacyShape.Lod1P95SurfaceErrorVoxels > 1.0;
WorstStep2ePitchCorrelation = FMath::Max(
WorstStep2ePitchCorrelation, LegacyShape.Grid.PitchCorrelationPeak);
WorstStep2ePitchExcess = FMath::Max(
@@ -5383,9 +5699,9 @@ int32 RunStep2f(const FString& Params)
&& NewShape.Mesh.ShareDihedralsOver45Degrees <= 0.25
&& NewShape.Mesh.P95DihedralDegrees <= 70.0;
bStep2fBandLimitPassed &= NewShape.Detail.LatticeScaleEnergyRatio <= 0.20
&& NewShape.Detail.Rms >= 0.40;
&& NewShape.Detail.Rms >= 0.80;
bStep2fLodPassed &= NewShape.Lod1VertexCount > 0
&& NewShape.Lod1P95SurfaceErrorVoxels <= 0.5;
&& NewShape.Lod1P95SurfaceErrorVoxels <= 1.0;
bStep2fGridPassed &= NewShape.Grid.PitchCorrelationPeak <= 0.40
&& NewShape.Grid.PitchPeakExcess <= 0.08;
bStep2fStructurePassed &= NewSolidFraction >= 0.25 && NewSolidFraction <= 0.88
@@ -5565,6 +5881,39 @@ int32 RunStep2f(const FString& Params)
const bool bDeterminism = HardestHash == RepeatHash && HardestHash == OneWorkerHash
&& Step2eSameWorkerRun(HardestTiming, RepeatTiming)
&& 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(
*World.Generator, Origin, Dimension, true);
const bool bBoundAudit = ProofAudit.CorrectlySkippedBricksAudited > 0
@@ -5600,7 +5949,8 @@ int32 RunStep2f(const FString& Params)
&& bStep2fGridPassed && bStep2fStructurePassed;
const bool bSpeedPass = Workers == 6 && WorstStep2fMs <= 600.0;
const bool bPassed = bAllGeometryGatesRejectStep2e && bAllGeometryGatesPassStep2f
&& bSpeedPass && bDeterminism && bBoundAudit && bUndersizedBoundCaught
&& bSpeedPass && bDeterminism && bParameterizedBrickScalarParity
&& bBoundAudit && bUndersizedBoundCaught
&& bBoundaryPass && bDeliberateSeamCaught && HardestTiming.NonFiniteSamples == 0;
TSharedRef<FJsonObject> Gates = MakeShared<FJsonObject>();
@@ -5621,7 +5971,7 @@ int32 RunStep2f(const FString& Params)
Step2eCrumbNumbers->SetNumberField(TEXT("crumbs_below_1m3_max"), WorstStep2eCrumbs);
TSharedRef<FJsonObject> Step2fCrumbNumbers = MakeShared<FJsonObject>();
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);
TSharedRef<FJsonObject> Step2eFlakeNumbers = MakeShared<FJsonObject>();
Step2eFlakeNumbers->SetNumberField(TEXT("max_dihedral_share_over_45deg"), WorstStep2eDihedralShare);
@@ -5637,13 +5987,13 @@ int32 RunStep2f(const FString& Params)
TSharedRef<FJsonObject> Step2fDetailNumbers = MakeShared<FJsonObject>();
Step2fDetailNumbers->SetNumberField(TEXT("max_lattice_scale_energy_ratio"), WorstStep2fDetailEnergy);
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);
TSharedRef<FJsonObject> Step2eLodNumbers = MakeShared<FJsonObject>();
Step2eLodNumbers->SetNumberField(TEXT("max_l1_p95_surface_distance_voxels"), WorstStep2eLodDistance);
TSharedRef<FJsonObject> Step2fLodNumbers = MakeShared<FJsonObject>();
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);
TSharedRef<FJsonObject> Step2eGridNumbers = MakeShared<FJsonObject>();
Step2eGridNumbers->SetNumberField(TEXT("max_pitch_autocorrelation"), WorstStep2ePitchCorrelation);
@@ -5687,19 +6037,19 @@ int32 RunStep2f(const FString& Params)
Report->SetStringField(TEXT("field_definition"),
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 "
"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 "
"brick detail samplers share independently rotated 48/24/12-sample bands with a "
"twelve-sample shortest period and a smooth ridge response; their final-field "
"detail supremum is 3.822 units."));
"brick detail samplers share independently rotated 96/48/24-sample bands with a "
"24-sample shortest period and a smooth ridge response; their final-field "
"detail supremum is 7.644 voxel units."));
Report->SetStringField(TEXT("component_method"),
TEXT("26-neighbor connected components are counted on the exact 25 cm sign lattice. "
"Raw components below 64 samples (< 1 m3) are reported, then a 64-sample pad is "
TEXT("26-neighbor connected components are counted on the exact 12.5 cm sign lattice. "
"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 "
"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."));
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. "
"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 "
@@ -5787,6 +6137,12 @@ int32 RunStep2f(const FString& Params)
Existing->SetNumberField(TEXT("brick_boundary_gradient_jump_rms"), BoundaryStats.BrickJumpRms);
Existing->SetNumberField(TEXT("brick_interior_gradient_jump_rms"), BoundaryStats.InteriorJumpRms);
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(
TEXT("%016llx"), static_cast<unsigned long long>(HardestHash)));
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);
}
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("min"));
Writer->WriteValue(TEXT("x"), static_cast<double>(Export.RegionOrigin.X) * 0.25);
Writer->WriteValue(TEXT("y"), static_cast<double>(Export.RegionOrigin.Y) * 0.25);
Writer->WriteValue(TEXT("z"), static_cast<double>(Export.RegionOrigin.Z) * 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) * VOXEL_SIZE / 100.0);
Writer->WriteValue(TEXT("z"), static_cast<double>(Export.RegionOrigin.Z) * VOXEL_SIZE / 100.0);
Writer->WriteObjectEnd();
Writer->WriteObjectStart(TEXT("max_exclusive"));
Writer->WriteValue(TEXT("x"), static_cast<double>(Export.RegionOrigin.X + Export.RegionSize) * 0.25);
Writer->WriteValue(TEXT("y"), static_cast<double>(Export.RegionOrigin.Y + Export.RegionSize) * 0.25);
Writer->WriteValue(TEXT("z"), static_cast<double>(Export.RegionOrigin.Z + 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) * VOXEL_SIZE / 100.0);
Writer->WriteValue(TEXT("z"), static_cast<double>(Export.RegionOrigin.Z + Export.RegionSize) * VOXEL_SIZE / 100.0);
Writer->WriteObjectEnd();
Writer->WriteValue(TEXT("units"), TEXT("metres"));
Writer->WriteObjectEnd();
@@ -10114,11 +10470,11 @@ bool RunTunnelCoreProbe(
"measured=%.6g_voxels/%d"),
*Axis->Name, SampleCount, Axis->ValidFinalCrossings,
Axis->FinalPeriodicAmplitudeVoxels,
Axis->FinalPeriodicAmplitudeVoxels * 25.0f,
Axis->FinalPeriodicAmplitudeVoxels * VOXEL_SIZE,
Axis->FinalPeriodicPeriodVoxels,
Axis->FinalPeriodicMeasuredLengthVoxels, Axis->FinalPeriodicMeasuredSamples,
Axis->ValidCoreCrossings, Axis->CorePeriodicAmplitudeVoxels,
Axis->CorePeriodicAmplitudeVoxels * 25.0f,
Axis->CorePeriodicAmplitudeVoxels * VOXEL_SIZE,
Axis->CorePeriodicPeriodVoxels,
Axis->CorePeriodicMeasuredLengthVoxels, Axis->CorePeriodicMeasuredSamples);
}
@@ -10409,11 +10765,11 @@ bool RunTunnelCoreProbe(
Mouth.CoreBottomSampleCount, Mouth.FinalSolidSampleCount,
Mouth.CoreInducedSolidSampleCount,
Mouth.MaxWallPeriodicAmplitudeVoxels,
Mouth.MaxWallPeriodicAmplitudeVoxels * 25.0f,
Mouth.MaxWallPeriodicAmplitudeVoxels * VOXEL_SIZE,
Mouth.MaxFloorPeriodicAmplitudeVoxels,
Mouth.MaxFloorPeriodicAmplitudeVoxels * 25.0f,
Mouth.MaxFloorPeriodicAmplitudeVoxels * VOXEL_SIZE,
Mouth.MaxLocalMedianFloorPeriodicAmplitudeVoxels,
Mouth.MaxLocalMedianFloorPeriodicAmplitudeVoxels * 25.0f,
Mouth.MaxLocalMedianFloorPeriodicAmplitudeVoxels * VOXEL_SIZE,
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_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_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_measured_samples"), Axis.CorePeriodicMeasuredSamples);
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_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_measured_samples"), Axis.FinalPeriodicMeasuredSamples);
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("max_crossing_voxels"), static_cast<double>(Axis.MaxCrossingVoxels));
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_measured_samples"), Axis.PeriodicMeasuredSamples);
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_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.WriteObjectEnd();
}
@@ -10676,18 +11032,18 @@ FString BuildExploreJson(
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("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_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_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_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("periodic_acceptance_cm"), 2.0);
Writer->WriteValue(TEXT("periodic_acceptance_pass"),
Output.TunnelCoreProbe.Mouth.MaxWallPeriodicAmplitudeVoxels * 25.0f <= 2.0f
&& Output.TunnelCoreProbe.Mouth.MaxFloorPeriodicAmplitudeVoxels * 25.0f <= 2.0f);
Output.TunnelCoreProbe.Mouth.MaxWallPeriodicAmplitudeVoxels * VOXEL_SIZE <= 2.0f
&& Output.TunnelCoreProbe.Mouth.MaxFloorPeriodicAmplitudeVoxels * VOXEL_SIZE <= 2.0f);
Writer->WriteArrayStart(TEXT("endpoints"));
for (const FExploreTunnelMouthProbeEndpoint& Endpoint : Output.TunnelCoreProbe.Mouth.Endpoints)
{
@@ -10862,15 +11218,15 @@ FString BuildExploreJson(
Writer->WriteObjectEnd();
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("lateral_regions_enabled"), false);
Writer->WriteValue(TEXT("diff_layer_mutated"), false);
Writer->WriteValue(TEXT("target_strate_slot"), Arguments.Slot);
Writer->WriteValue(TEXT("target_bottom_voxel_z"), World.TargetBottomWorldZ);
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_top_m_exclusive"), static_cast<double>(World.TargetTopWorldZ) * 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) * VOXEL_SIZE / 100.0);
Writer->WriteValue(TEXT("target_boundary_seal_thickness_voxels"),
static_cast<double>(World.TargetBoundarySealThickness));
WritePlayerDimensions(*Writer, TEXT("player_dimensions"));
@@ -10977,12 +11333,12 @@ FString BuildExploreJson(
if (Output.Walk.bHasArrival)
{
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)
{
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_code"), static_cast<int32>(Report.Result));
@@ -11042,13 +11398,13 @@ FString BuildExploreJson(
if (Report.bHasAgentFinalPosition)
{
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);
if (Report.bHasLastReachedPosition)
{
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);
if (Report.bHasTargetComponentNearestCell)
@@ -11056,7 +11412,7 @@ FString BuildExploreJson(
WriteJsonVector(*Writer, TEXT("target_component_nearest_cell_voxels"),
Report.TargetComponentNearestVoxels, 1.0f);
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"),
static_cast<double>(Report.AgentToTargetComponentGapVoxels));
@@ -11148,7 +11504,7 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("mesh_file"), Output.Export.MeshFileName);
Writer->WriteValue(TEXT("manifest_file"), Output.Export.ManifestFileName);
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("canonical_mesher_tile_cells"), CHUNK_SIZE);
Writer->WriteValue(TEXT("canonical_mesher_tile_count"), Output.Export.MesherTileCount);
@@ -12965,6 +13321,10 @@ int32 UVoxelForgeExploreCommandlet::Main(const FString& Params)
{
return RunStep2f(Params);
}
if (FParse::Param(*Params, TEXT("step3scale")))
{
return RunStep3ScaleBenchmark(Params);
}
if (FParse::Param(*Params, TEXT("step2e")))
{
return RunStep2e(Params);
@@ -181,24 +181,59 @@ namespace VoxelForgeRegionEval
TArray<FResearchBrick> Bricks;
const FIntVector Last = OriginVoxels
+ FIntVector(Dimension - 1, Dimension - 1, Dimension - 1);
const int32 ParameterizedBrickSize =
VoxelForgeRedefinedDensityField::ParameterizedBrickSamples;
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(
FMath::FloorToInt(static_cast<float>(OriginVoxels.X) / ProofBrickSize.X),
FMath::FloorToInt(static_cast<float>(OriginVoxels.Y) / ProofBrickSize.Y),
FMath::FloorToInt(static_cast<float>(OriginVoxels.Z) / ProofBrickSize.Z));
FMath::FloorToInt(static_cast<float>(OriginVoxels.X - BrickAlignmentOffset) / ProofBrickSize.X),
FMath::FloorToInt(static_cast<float>(OriginVoxels.Y - BrickAlignmentOffset) / ProofBrickSize.Y),
FMath::FloorToInt(static_cast<float>(OriginVoxels.Z - BrickAlignmentOffset) / ProofBrickSize.Z));
const FIntVector LastBrick(
FMath::FloorToInt(static_cast<float>(Last.X) / ProofBrickSize.X),
FMath::FloorToInt(static_cast<float>(Last.Y) / ProofBrickSize.Y),
FMath::FloorToInt(static_cast<float>(Last.Z) / ProofBrickSize.Z));
Bricks.Reserve((LastBrick.X - FirstBrick.X + 1)
* (LastBrick.Y - FirstBrick.Y + 1) * (LastBrick.Z - FirstBrick.Z + 1));
FMath::FloorToInt(static_cast<float>(Last.X - BrickAlignmentOffset) / ProofBrickSize.X),
FMath::FloorToInt(static_cast<float>(Last.Y - BrickAlignmentOffset) / ProofBrickSize.Y),
FMath::FloorToInt(static_cast<float>(Last.Z - BrickAlignmentOffset) / ProofBrickSize.Z));
const uint64 TotalProofBrickCount = static_cast<uint64>(LastBrick.X - FirstBrick.X + 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 BY = FirstBrick.Y; BY <= LastBrick.Y; ++BY)
for (int32 BX = FirstBrick.X; BX <= LastBrick.X; ++BX)
{
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();
Brick.Origin = FIntVector(
FMath::Max(OriginVoxels.X, BrickWorldMin.X),
@@ -215,6 +250,7 @@ namespace VoxelForgeRegionEval
Brick.Lattice = Brick.Origin - OriginVoxels;
Brick.SampleCount = Brick.Size.X * Brick.Size.Y * Brick.Size.Z;
}
}
TArray<double> WorkerWall;
TArray<double> WorkerCpu;
@@ -253,6 +289,10 @@ namespace VoxelForgeRegionEval
double CpuStart = 0.0;
const bool bHaveCpuStart = GetCurrentThreadCpuSeconds(CpuStart);
TArray<float> BrickDetail;
TArray<float> ParameterizedDetail;
TArray<float> ParameterizedColumnValues;
TArray<float> ParameterizedBrickValues;
TArray<uint8> ParameterizedProofKinds;
VoxelForgeRedefinedDensityField::FBrickEvaluationContext BrickContext;
VoxelForgeRedefinedDensityField::FParameterizedBrickContext ParameterizedBrickContext;
for (;;)
@@ -267,19 +307,147 @@ namespace VoxelForgeRegionEval
for (int32 BrickIndex = BatchBegin; BrickIndex < BatchEnd; ++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 bEvaluatedMacroCentre = false;
VoxelForgeRedefinedDensityField::EBrickFallbackReason FallbackReason =
VoxelForgeRedefinedDensityField::EBrickFallbackReason::UnsupportedState;
++WorkerMacroChecks[WorkerIndex];
const double ClassifyStart = FPlatformTime::Seconds();
const bool bCanSkip = bEnableBrickSkips && (bParameterized
? VoxelForgeRedefinedDensityField::TryClassifyParameterizedBrick(
const bool bCanSkip = bEnableBrickSkips
&& VoxelForgeRedefinedDensityField::TryClassifyBrick(
Generator, Brick.Origin, Brick.Size, bUniformAir, DetailScale,
&bEvaluatedMacroCentre, &FallbackReason)
: VoxelForgeRedefinedDensityField::TryClassifyBrick(
Generator, Brick.Origin, Brick.Size, bUniformAir, DetailScale,
&bEvaluatedMacroCentre, &FallbackReason));
&bEvaluatedMacroCentre, &FallbackReason);
WorkerClassify[WorkerIndex] += FPlatformTime::Seconds() - ClassifyStart;
WorkerMacroCentreEvaluations[WorkerIndex] += bEvaluatedMacroCentre ? 1u : 0u;
if (bEnableBrickSkips && !bCanSkip)
@@ -312,49 +480,7 @@ namespace VoxelForgeRegionEval
const double EvaluationStart = FPlatformTime::Seconds();
VoxelForgeRedefinedDensityField::BuildDetailBrick(
Generator, Brick.Origin, Brick.Size, DetailScale, BrickDetail,
bParameterized ? nullptr : &BrickContext, bParameterized);
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
{
&BrickContext, /*bParameterized=*/false);
for (int32 Z = 0; Z < Brick.Size.Z; ++Z)
for (int32 Y = 0; Y < Brick.Size.Y; ++Y)
for (int32 X = 0; X < Brick.Size.X; ++X)
@@ -374,7 +500,6 @@ namespace VoxelForgeRegionEval
nullptr, DetailScale, &BrickContext);
++WorkerEvaluated[WorkerIndex];
}
}
WorkerEvaluate[WorkerIndex] += FPlatformTime::Seconds() - EvaluationStart;
}
}
@@ -392,7 +517,8 @@ namespace VoxelForgeRegionEval
OutTiming.WallSeconds = FPlatformTime::Seconds() - StartSeconds;
OutTiming.bRegionProgram = true;
OutTiming.BrickCount = static_cast<uint64>(Bricks.Num());
OutTiming.BrickCount = bParameterized
? TotalProofBrickCount : static_cast<uint64>(Bricks.Num());
OutTiming.ObservedWorkerThreads = 0;
for (int32 Worker = 0; Worker < NumWorkers; ++Worker)
{
+295 -8
View File
@@ -1,6 +1,6 @@
[CmdletBinding()]
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',
[switch]$Build,
[switch]$EnableRender,
@@ -21,7 +21,7 @@ param(
[ValidateRange(0, 8)]
[int]$HorizonLevel = 5,
[ValidateRange(1, 100000)]
[int]$HorizonRenderDistanceChunks = 768,
[int]$HorizonRenderDistanceChunks = 1536,
[ValidateRange(0, 7)]
[int]$Step2fProfile = 0,
[ValidateSet('inside', 'wall-closeup')]
@@ -64,6 +64,8 @@ $LaunchLedger = [System.Collections.Generic.List[object]]::new()
$BuildRecord = [ordered]@{ requested = [bool]$Build; status = 'not_requested' }
$AssetEvidence = @()
$CommandletMetrics = [ordered]@{}
$Step3ScaleMetrics = [ordered]@{ requested = $Scenario -eq 'step3-scale'; status = 'not_run' }
$Step3GameMetrics = [ordered]@{ requested = $Scenario -eq 'step3-game'; status = 'not_run' }
$GameMetrics = [ordered]@{}
$HorizonMetrics = [ordered]@{
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) {
$deadline = [datetime]::UtcNow.AddMinutes(30)
$didWait = $false
@@ -397,8 +449,9 @@ function Wait-OwnedProcess($Process, [int]$TimeoutSeconds, [switch]$EnforceMemor
}
function Invoke-UnrealLaunch([string]$Kind, [string[]]$Arguments, [string]$LogPath,
[string]$StdoutPath, [string]$StderrPath) {
[string]$StdoutPath, [string]$StderrPath, [switch]$ShowWindow) {
Assert-NoForeignUnreal "before $Kind launch"
$idleCpuBusyPercent = Wait-ForIdleMachine "$Kind launch"
Assert-EnoughFreeMemory "$Kind launch"
Ensure-Directory (Split-Path -Parent $LogPath)
Ensure-Directory (Split-Path -Parent $StdoutPath)
@@ -414,6 +467,7 @@ function Invoke-UnrealLaunch([string]$Kind, [string[]]$Arguments, [string]$LogPa
exit_code = $null
timed_out = $false
memory_limit_hit = $false
prelaunch_cpu_busy_percent = [Math]::Round($idleCpuBusyPercent, 2)
peak_private_bytes = 0L
crash_reporters_reaped = @()
new_crashes = @()
@@ -426,12 +480,13 @@ function Invoke-UnrealLaunch([string]$Kind, [string[]]$Arguments, [string]$LogPa
$isolatedLocalAppData = Join-Path $RunRoot 'LocalAppData'
Ensure-Directory $isolatedLocalAppData
$oldLocalAppData = $env:LOCALAPPDATA
$windowStyle = if ($ShowWindow) { 'Normal' } else { 'Hidden' }
try {
# Unreal resolves several engine-wide stores from LOCALAPPDATA even when -userdir and
# -LocalDataCachePath are supplied. Keep this child process inside the run directory.
$env:LOCALAPPDATA = $isolatedLocalAppData
$process = Start-Process -FilePath $EditorExe -ArgumentList $Arguments -WorkingDirectory $HostRoot `
-RedirectStandardOutput $StdoutPath -RedirectStandardError $StderrPath -WindowStyle Hidden `
-RedirectStandardOutput $StdoutPath -RedirectStandardError $StderrPath -WindowStyle $windowStyle `
-PassThru
try { $process.PriorityClass = [Diagnostics.ProcessPriorityClass]::BelowNormal }
catch {
@@ -579,7 +634,7 @@ function Sync-OwnerAssets {
$null = Copy-Item -LiteralPath $source -Destination $target -Force -PassThru
[void]$rows.Add([ordered]@{
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_sha256 = Get-Sha256 $source
staged_path = $target
@@ -674,7 +729,7 @@ function Get-AssetEvidence {
$target = Join-Path $HostRoot "Content\VoxelForge\$assetName.uasset"
[void]$rows.Add([ordered]@{
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_sha256 = Get-Sha256 $source
staged_path = $target
@@ -713,7 +768,6 @@ function New-CommonArguments([string]$Kind, [string]$LogPath, $CvarMap,
} else {
[void]$args.Add('-nullrhi')
}
[void]$args.Add('-DDC-ForceMemoryCache')
[void]$args.Add('-unattended')
[void]$args.Add('-nop4')
[void]$args.Add('-nosplash')
@@ -1011,6 +1065,147 @@ function Get-HorizonRenderMetrics($Ledger, [string]$LogPath) {
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) {
$defaultJson = Read-JsonFile $DefaultPath
$fmaJson = Read-JsonFile $CrtFma3OffPath
@@ -1128,7 +1323,7 @@ function Get-CollisionGateStressMetrics([string]$LogPath) {
'spawn_height_vox', 'deep_drop',
'gate_holds', 'gate_total_s', 'max_gate_s', 'full_stops',
'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_collision_ready_s', 'test_support_tile',
'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' {
if (([string]::IsNullOrWhiteSpace($StackSamplesPath)) -or -not (Test-Path -LiteralPath $StackSamplesPath -PathType Leaf)) {
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' {
$defaultDump = Join-Path $RunRoot 'tile_hashes_default.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 }
step2e = if ($Scenario -eq 'step2e') { $CommandletMetrics } else { $null }
step2f = if ($Scenario -eq 'step2f') { $CommandletMetrics } else { $null }
step3_scale = $Step3ScaleMetrics
step3_game = $Step3GameMetrics
game = $GameMetrics
horizon = $HorizonMetrics
horizon_render = $HorizonRenderMetrics
@@ -2101,6 +2386,8 @@ try {
[void]$summaryLines.Add("geometry_hash=$($result.geometry_hash)")
[void]$summaryLines.Add("obj_sha256=$($result.obj_sha256)")
[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("strate_crossing=$(ConvertTo-CompactJson $result.strate_crossing)")
[void]$summaryLines.Add("visual_staleness=$(ConvertTo-CompactJson $result.visual_staleness)")