diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index af2a173..d72eea7 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index c19c974..44df6a4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/CODEMAP.md b/CODEMAP.md index 64694db..9b05ead 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -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 diff --git a/Saved/Research/step2f/reject/ingame_box_tunnels.png b/Saved/Research/step2f/reject/ingame_box_tunnels.png new file mode 100644 index 0000000..ec4c26d Binary files /dev/null and b/Saved/Research/step2f/reject/ingame_box_tunnels.png differ diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeComposerShowcaseTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeComposerShowcaseTest.cpp index a3f8ff1..d9425cf 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeComposerShowcaseTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeComposerShowcaseTest.cpp @@ -603,27 +603,27 @@ namespace FString VF_FormatWorldScaleAudit() { - constexpr float VoxelMetres = 0.25f; - constexpr float StrateHeightVoxels = 8.0f * static_cast(CHUNK_SIZE); + constexpr float VoxelMetres = VOXEL_SIZE / 100.0f; + constexpr float StrateHeightVoxels = 16.0f * static_cast(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; diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeDescentCorridorTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeDescentCorridorTest.cpp index 16f3d4f..7b084c1 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeDescentCorridorTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeDescentCorridorTest.cpp @@ -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(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(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 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], diff --git a/Source/VoxelForge/Private/Tests/VoxelForgePassageOpenSpaceTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgePassageOpenSpaceTest.cpp index cd864a1..fa3015e 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgePassageOpenSpaceTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgePassageOpenSpaceTest.cpp @@ -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); diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeScaleDiagnosisTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeScaleDiagnosisTest.cpp index 143f3d4..5248e93 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeScaleDiagnosisTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeScaleDiagnosisTest.cpp @@ -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, diff --git a/Source/VoxelForge/Private/VoxelCaveMorphology.cpp b/Source/VoxelForge/Private/VoxelCaveMorphology.cpp index 91d8421..b9877ba 100644 --- a/Source/VoxelForge/Private/VoxelCaveMorphology.cpp +++ b/Source/VoxelForge/Private/VoxelCaveMorphology.cpp @@ -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) { diff --git a/Source/VoxelForge/Private/VoxelContentManager.cpp b/Source/VoxelForge/Private/VoxelContentManager.cpp index 26913ba..c1792d1 100644 --- a/Source/VoxelForge/Private/VoxelContentManager.cpp +++ b/Source/VoxelForge/Private/VoxelContentManager.cpp @@ -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)); diff --git a/Source/VoxelForge/Private/VoxelDensityVolume.cpp b/Source/VoxelForge/Private/VoxelDensityVolume.cpp index a887bea..044b773 100644 --- a/Source/VoxelForge/Private/VoxelDensityVolume.cpp +++ b/Source/VoxelForge/Private/VoxelDensityVolume.cpp @@ -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 diff --git a/Source/VoxelForge/Private/VoxelGenerator.cpp b/Source/VoxelForge/Private/VoxelGenerator.cpp index 39b3790..9a2ac1a 100644 --- a/Source/VoxelForge/Private/VoxelGenerator.cpp +++ b/Source/VoxelForge/Private/VoxelGenerator.cpp @@ -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, diff --git a/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp b/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp index 27b5500..1f978f5 100644 --- a/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp +++ b/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp @@ -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 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 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 DetailBrick; + TArray BrickValues; + TArray ColumnValues; + TArray 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(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(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( + BrickSize.X * BrickSize.Y * BrickSize.Z); + } + ProofZ += RunProofCount; + } + } +#endif + } + else for (int32 gz = GzLo; gz <= GzHi; gz++) { if (ShouldAbortWork()) { diff --git a/Source/VoxelForge/Private/VoxelRedefinedDensityField.cpp b/Source/VoxelForge/Private/VoxelRedefinedDensityField.cpp index 59849ff..8fa58fa 100644 --- a/Source/VoxelForge/Private/VoxelRedefinedDensityField.cpp +++ b/Source/VoxelForge/Private/VoxelRedefinedDensityField.cpp @@ -18,20 +18,25 @@ namespace VoxelForgeRedefinedDensityField { namespace { - constexpr float LegacyDetailAmplitude = 1.4f + 0.9f + 0.45f + 2.5f; - constexpr float CoherentDetailAmplitude = 1.82f + 1.092f + 0.546f + 0.364f; - constexpr float CoherentRockDensityBias = 0.10f; - constexpr float DetailBoundNumericalMargin = 1.0e-3f; + constexpr float DensityUnitsPerLegacyVoxel = 25.0f / VOXEL_SIZE; + constexpr float LegacyDetailAmplitude = + (1.4f + 0.9f + 0.45f + 2.5f) * DensityUnitsPerLegacyVoxel; + constexpr float CoherentDetailAmplitude = + (1.82f + 1.092f + 0.546f + 0.364f) * DensityUnitsPerLegacyVoxel; + constexpr float CoherentRockDensityBias = + VoxelPassageGeometry::MetersToVoxels(0.025f); + constexpr float DetailBoundNumericalMargin = + VoxelPassageGeometry::MetersToVoxels(0.00025f); constexpr float BaseFieldLipschitzBound = 1.25f; constexpr float ParameterizedFieldLipschitzBound = 1.60f; constexpr float QuinticFadeSlopeBound = 1.875f; - constexpr float MinimumCaveHeight = 48.0f; - constexpr float LandingFloorBlendVoxels = 96.0f; - constexpr float LandingBridgeRadius = 48.0f; - constexpr float PassageBoundPadding = 16.0f; - constexpr float VerticalShaftSpineRadius = 24.0f; + constexpr float MinimumCaveHeight = VoxelPassageGeometry::MetersToVoxels(12.0f); + constexpr float LandingFloorBlendVoxels = VoxelPassageGeometry::MetersToVoxels(24.0f); + constexpr float LandingBridgeRadius = VoxelPassageGeometry::MetersToVoxels(12.0f); + constexpr float PassageBoundPadding = VoxelPassageGeometry::MetersToVoxels(4.0f); + constexpr float VerticalShaftSpineRadius = VoxelPassageGeometry::MetersToVoxels(6.0f); constexpr int32 VerticalShaftConnectorChildReach = 4; - constexpr float LandingApproachRadius = 32.0f; + constexpr float LandingApproachRadius = VoxelPassageGeometry::MetersToVoxels(8.0f); constexpr uint32 VerticalShaftTreeSalt = 0x7A11u; TAutoConsoleVariable CVarResearchRedefinedDensityField( @@ -59,41 +64,96 @@ namespace // Frozen step 2e profiles are retained only for the step 2f before/after audit. const FParameterProfile GLegacyStep2eProfiles[] = { - { TEXT("OpenVaults"), { 176.0f, 0.62f, 0.76f, 340.0f, 12.0f, 40.0f, 36.0f, - 0.35f, 0.14f, 22.0f, 0.04f, 0.12f, 0.08f, 0.15f, 0.00f, 0.0f, 0.50f } }, - { TEXT("TightNetwork"), { 104.0f, 0.52f, 0.64f, 256.0f, 8.0f, 20.0f, 22.0f, - 0.12f, 0.03f, 14.0f, 0.06f, 0.08f, 0.02f, 0.12f, 0.00f, 0.0f, 0.50f } }, - { TEXT("ChamberHalls"), { 144.0f, 0.55f, 0.72f, 320.0f, 18.0f, 34.0f, 32.0f, - 0.28f, 0.18f, 22.0f, 0.08f, 0.22f, 0.10f, 0.22f, 0.05f, 20.0f, 0.40f } }, - { TEXT("ShaftForest"), { 128.0f, 0.50f, 0.62f, 272.0f, 12.0f, 26.0f, 28.0f, - 0.12f, 0.92f, 28.0f, 0.05f, 0.18f, 0.04f, 0.20f, 0.00f, 0.0f, 0.50f } }, - { TEXT("FloatingArchipelago"), { 136.0f, 0.55f, 0.68f, 216.0f, 10.0f, 28.0f, 28.0f, - 0.15f, 0.10f, 16.0f, 0.05f, 0.10f, 0.72f, 0.28f, 0.00f, 0.0f, 0.50f } }, - { TEXT("PillaredCathedral"), { 160.0f, 0.55f, 0.72f, 248.0f, 12.0f, 32.0f, 34.0f, - 0.25f, 0.08f, 18.0f, 0.72f, 0.30f, 0.08f, 0.45f, 0.00f, 0.0f, 0.50f } }, - { TEXT("TerracedBasin"), { 136.0f, 0.52f, 0.65f, 264.0f, 14.0f, 28.0f, 30.0f, - 0.20f, 0.10f, 18.0f, 0.10f, 0.84f, 0.05f, 0.25f, 0.85f, 48.0f, 0.30f } }, - { TEXT("HyperComplex"), { 96.0f, 0.60f, 0.74f, 280.0f, 20.0f, 30.0f, 28.0f, - 0.40f, 0.55f, 20.0f, 0.45f, 0.68f, 0.50f, 0.75f, 0.42f, 28.0f, 0.40f } }, + { TEXT("OpenVaults"), { VoxelPassageGeometry::MetersToVoxels(44.0f), 0.62f, 0.76f, + VoxelPassageGeometry::MetersToVoxels(85.0f), VoxelPassageGeometry::MetersToVoxels(3.0f), + VoxelPassageGeometry::MetersToVoxels(10.0f), VoxelPassageGeometry::MetersToVoxels(9.0f), + 0.35f, 0.14f, VoxelPassageGeometry::MetersToVoxels(5.5f), + 0.04f, 0.12f, 0.08f, 0.15f, 0.00f, 0.0f, 0.50f } }, + { TEXT("TightNetwork"), { VoxelPassageGeometry::MetersToVoxels(26.0f), 0.52f, 0.64f, + VoxelPassageGeometry::MetersToVoxels(64.0f), VoxelPassageGeometry::MetersToVoxels(2.0f), + VoxelPassageGeometry::MetersToVoxels(5.0f), VoxelPassageGeometry::MetersToVoxels(5.5f), + 0.12f, 0.03f, VoxelPassageGeometry::MetersToVoxels(3.5f), + 0.06f, 0.08f, 0.02f, 0.12f, 0.00f, 0.0f, 0.50f } }, + { TEXT("ChamberHalls"), { VoxelPassageGeometry::MetersToVoxels(36.0f), 0.55f, 0.72f, + VoxelPassageGeometry::MetersToVoxels(80.0f), VoxelPassageGeometry::MetersToVoxels(4.5f), + VoxelPassageGeometry::MetersToVoxels(8.5f), VoxelPassageGeometry::MetersToVoxels(8.0f), + 0.28f, 0.18f, VoxelPassageGeometry::MetersToVoxels(5.5f), + 0.08f, 0.22f, 0.10f, 0.22f, 0.05f, + VoxelPassageGeometry::MetersToVoxels(5.0f), 0.40f } }, + { TEXT("ShaftForest"), { VoxelPassageGeometry::MetersToVoxels(32.0f), 0.50f, 0.62f, + VoxelPassageGeometry::MetersToVoxels(68.0f), VoxelPassageGeometry::MetersToVoxels(3.0f), + VoxelPassageGeometry::MetersToVoxels(6.5f), VoxelPassageGeometry::MetersToVoxels(7.0f), + 0.12f, 0.92f, VoxelPassageGeometry::MetersToVoxels(7.0f), + 0.05f, 0.18f, 0.04f, 0.20f, 0.00f, 0.0f, 0.50f } }, + { TEXT("FloatingArchipelago"), { VoxelPassageGeometry::MetersToVoxels(34.0f), 0.55f, 0.68f, + VoxelPassageGeometry::MetersToVoxels(54.0f), VoxelPassageGeometry::MetersToVoxels(2.5f), + VoxelPassageGeometry::MetersToVoxels(7.0f), VoxelPassageGeometry::MetersToVoxels(7.0f), + 0.15f, 0.10f, VoxelPassageGeometry::MetersToVoxels(4.0f), + 0.05f, 0.10f, 0.72f, 0.28f, 0.00f, 0.0f, 0.50f } }, + { TEXT("PillaredCathedral"), { VoxelPassageGeometry::MetersToVoxels(40.0f), 0.55f, 0.72f, + VoxelPassageGeometry::MetersToVoxels(62.0f), VoxelPassageGeometry::MetersToVoxels(3.0f), + VoxelPassageGeometry::MetersToVoxels(8.0f), VoxelPassageGeometry::MetersToVoxels(8.5f), + 0.25f, 0.08f, VoxelPassageGeometry::MetersToVoxels(4.5f), + 0.72f, 0.30f, 0.08f, 0.45f, 0.00f, 0.0f, 0.50f } }, + { TEXT("TerracedBasin"), { VoxelPassageGeometry::MetersToVoxels(34.0f), 0.52f, 0.65f, + VoxelPassageGeometry::MetersToVoxels(66.0f), VoxelPassageGeometry::MetersToVoxels(3.5f), + VoxelPassageGeometry::MetersToVoxels(7.0f), VoxelPassageGeometry::MetersToVoxels(7.5f), + 0.20f, 0.10f, VoxelPassageGeometry::MetersToVoxels(4.5f), + 0.10f, 0.84f, 0.05f, 0.25f, 0.85f, + VoxelPassageGeometry::MetersToVoxels(12.0f), 0.30f } }, + { TEXT("HyperComplex"), { VoxelPassageGeometry::MetersToVoxels(24.0f), 0.60f, 0.74f, + VoxelPassageGeometry::MetersToVoxels(70.0f), VoxelPassageGeometry::MetersToVoxels(5.0f), + VoxelPassageGeometry::MetersToVoxels(7.5f), VoxelPassageGeometry::MetersToVoxels(7.0f), + 0.40f, 0.55f, VoxelPassageGeometry::MetersToVoxels(5.0f), + 0.45f, 0.68f, 0.50f, 0.75f, 0.42f, + VoxelPassageGeometry::MetersToVoxels(7.0f), 0.40f } }, }; const FParameterProfile GParameterProfiles[] = { - { TEXT("OpenVaults"), { 176.0f, 0.31f, 0.40f, 132.0f, 8.0f, 22.0f, 18.0f, - 0.12f, 0.08f, 16.0f, 0.02f, 0.06f, 0.04f, 0.22f, 0.00f, 0.0f, 0.50f } }, - { TEXT("TightNetwork"), { 104.0f, 0.27f, 0.35f, 104.0f, 5.0f, 18.0f, 14.0f, - 0.28f, 0.12f, 15.0f, 0.02f, 0.06f, 0.02f, 0.14f, 0.00f, 0.0f, 0.50f } }, - { TEXT("ChamberHalls"), { 144.0f, 0.32f, 0.41f, 136.0f, 10.0f, 21.0f, 17.0f, - 0.18f, 0.10f, 16.0f, 0.03f, 0.06f, 0.06f, 0.20f, 0.05f, 16.0f, 0.40f } }, - { TEXT("ShaftForest"), { 128.0f, 0.28f, 0.36f, 120.0f, 7.0f, 19.0f, 15.0f, - 0.16f, 0.42f, 20.0f, 0.03f, 0.06f, 0.03f, 0.22f, 0.00f, 0.0f, 0.50f } }, - { TEXT("FloatingArchipelago"), { 136.0f, 0.29f, 0.37f, 116.0f, 6.0f, 19.0f, 15.0f, - 0.12f, 0.08f, 15.0f, 0.02f, 0.06f, 0.08f, 0.20f, 0.00f, 0.0f, 0.50f } }, - { TEXT("PillaredCathedral"), { 160.0f, 0.31f, 0.40f, 132.0f, 8.0f, 22.0f, 17.0f, - 0.20f, 0.08f, 16.0f, 0.16f, 0.06f, 0.04f, 0.22f, 0.00f, 0.0f, 0.50f } }, - { TEXT("TerracedBasin"), { 136.0f, 0.30f, 0.38f, 124.0f, 8.0f, 20.0f, 16.0f, - 0.14f, 0.10f, 16.0f, 0.04f, 0.06f, 0.04f, 0.18f, 0.46f, 24.0f, 0.30f } }, - { TEXT("HyperComplex"), { 96.0f, 0.28f, 0.37f, 104.0f, 9.0f, 18.0f, 14.0f, - 0.26f, 0.32f, 17.0f, 0.08f, 0.06f, 0.05f, 0.18f, 0.12f, 12.0f, 0.40f } }, + // Geometry is specified in metres, then converted to voxel-space for field math. + { TEXT("OpenVaults"), { VoxelPassageGeometry::MetersToVoxels(44.0f), 0.31f, 0.41f, + VoxelPassageGeometry::MetersToVoxels(33.0f), VoxelPassageGeometry::MetersToVoxels(2.0f), + VoxelPassageGeometry::MetersToVoxels(5.5f), VoxelPassageGeometry::MetersToVoxels(4.5f), + 0.12f, 0.08f, VoxelPassageGeometry::MetersToVoxels(4.0f), + 0.02f, 0.06f, 0.04f, 0.22f, 0.00f, 0.0f, 0.50f } }, + { TEXT("TightNetwork"), { VoxelPassageGeometry::MetersToVoxels(26.0f), 0.27f, 0.35f, + VoxelPassageGeometry::MetersToVoxels(26.0f), VoxelPassageGeometry::MetersToVoxels(1.25f), + VoxelPassageGeometry::MetersToVoxels(4.5f), VoxelPassageGeometry::MetersToVoxels(3.5f), + 0.28f, 0.12f, VoxelPassageGeometry::MetersToVoxels(3.75f), + 0.02f, 0.06f, 0.02f, 0.14f, 0.00f, 0.0f, 0.50f } }, + { TEXT("ChamberHalls"), { VoxelPassageGeometry::MetersToVoxels(36.0f), 0.32f, 0.41f, + VoxelPassageGeometry::MetersToVoxels(34.0f), VoxelPassageGeometry::MetersToVoxels(2.5f), + VoxelPassageGeometry::MetersToVoxels(5.25f), VoxelPassageGeometry::MetersToVoxels(4.25f), + 0.18f, 0.10f, VoxelPassageGeometry::MetersToVoxels(4.0f), + 0.03f, 0.06f, 0.06f, 0.20f, 0.05f, + VoxelPassageGeometry::MetersToVoxels(4.0f), 0.40f } }, + { TEXT("ShaftForest"), { VoxelPassageGeometry::MetersToVoxels(32.0f), 0.28f, 0.36f, + VoxelPassageGeometry::MetersToVoxels(30.0f), VoxelPassageGeometry::MetersToVoxels(1.75f), + VoxelPassageGeometry::MetersToVoxels(4.75f), VoxelPassageGeometry::MetersToVoxels(3.75f), + 0.16f, 0.42f, VoxelPassageGeometry::MetersToVoxels(5.0f), + 0.03f, 0.06f, 0.03f, 0.22f, 0.00f, 0.0f, 0.50f } }, + { TEXT("FloatingArchipelago"), { VoxelPassageGeometry::MetersToVoxels(34.0f), 0.29f, 0.37f, + VoxelPassageGeometry::MetersToVoxels(29.0f), VoxelPassageGeometry::MetersToVoxels(1.5f), + VoxelPassageGeometry::MetersToVoxels(4.75f), VoxelPassageGeometry::MetersToVoxels(3.75f), + 0.12f, 0.08f, VoxelPassageGeometry::MetersToVoxels(3.75f), + 0.02f, 0.06f, 0.08f, 0.20f, 0.00f, 0.0f, 0.50f } }, + { TEXT("PillaredCathedral"), { VoxelPassageGeometry::MetersToVoxels(40.0f), 0.31f, 0.40f, + VoxelPassageGeometry::MetersToVoxels(33.0f), VoxelPassageGeometry::MetersToVoxels(2.0f), + VoxelPassageGeometry::MetersToVoxels(5.5f), VoxelPassageGeometry::MetersToVoxels(4.25f), + 0.20f, 0.08f, VoxelPassageGeometry::MetersToVoxels(4.0f), + 0.16f, 0.06f, 0.04f, 0.22f, 0.00f, 0.0f, 0.50f } }, + { TEXT("TerracedBasin"), { VoxelPassageGeometry::MetersToVoxels(34.0f), 0.30f, 0.38f, + VoxelPassageGeometry::MetersToVoxels(31.0f), VoxelPassageGeometry::MetersToVoxels(2.0f), + VoxelPassageGeometry::MetersToVoxels(5.0f), VoxelPassageGeometry::MetersToVoxels(4.0f), + 0.14f, 0.10f, VoxelPassageGeometry::MetersToVoxels(4.0f), + 0.04f, 0.06f, 0.04f, 0.18f, 0.46f, + VoxelPassageGeometry::MetersToVoxels(6.0f), 0.30f } }, + { TEXT("HyperComplex"), { VoxelPassageGeometry::MetersToVoxels(24.0f), 0.28f, 0.37f, + VoxelPassageGeometry::MetersToVoxels(26.0f), VoxelPassageGeometry::MetersToVoxels(2.25f), + VoxelPassageGeometry::MetersToVoxels(4.5f), VoxelPassageGeometry::MetersToVoxels(3.5f), + 0.26f, 0.32f, VoxelPassageGeometry::MetersToVoxels(4.25f), + 0.08f, 0.06f, 0.05f, 0.18f, 0.12f, + VoxelPassageGeometry::MetersToVoxels(3.0f), 0.40f } }, }; std::atomic GUseLegacyStep2eAuditMode{false}; @@ -171,10 +231,10 @@ namespace ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork; float FloorZ = 0.0f; float CeilingZ = 0.0f; - float CellSpacing = 128.0f; + float CellSpacing = VoxelPassageGeometry::MetersToVoxels(32.0f); float OffsetX = 0.0f; float OffsetY = 0.0f; - float CorridorHalfWidth = 40.0f; + float CorridorHalfWidth = VoxelPassageGeometry::MetersToVoxels(10.0f); FGenerationParameters Generation; int32 GenerationProfile = 0; uint32 GenerationSeed = 0; @@ -260,19 +320,230 @@ namespace return FMath::Lerp(Z0Y0, Z0Y1, AZ); } + struct FNoisePoint3 + { + float X = 0.0f; + float Y = 0.0f; + float Z = 0.0f; + }; + + struct FNoiseLatticeCache + { + FIntVector MinCell = FIntVector::ZeroValue; + FIntVector Size = FIntVector::ZeroValue; + TArray Values; + + FORCEINLINE float Get(int32 X, int32 Y, int32 Z) const + { + const int32 LocalX = X - MinCell.X; + const int32 LocalY = Y - MinCell.Y; + const int32 LocalZ = Z - MinCell.Z; + return Values[(LocalZ * Size.Y + LocalY) * Size.X + LocalX]; + } + }; + + constexpr float BroadDetailTransform[9] = { + 0.838671f, -0.493587f, 0.230129f, + 0.544639f, 0.760095f, -0.354003f, + 0.0f, 0.422618f, 0.906308f}; + constexpr float MediumDetailTransform[9] = { + 0.956305f, 0.220661f, 0.191825f, + -0.292372f, 0.721484f, 0.627548f, + 0.0f, 0.656059f, 0.754710f}; + constexpr float FineDetailTransform[9] = { + 0.325568f, -0.834725f, -0.443914f, + 0.945519f, 0.287381f, 0.152824f, + 0.0f, -0.469472f, 0.882948f}; + + FORCEINLINE FNoisePoint3 TransformNoisePoint( + const float* Matrix, float X, float Y, float Z) + { + return FNoisePoint3{ + Matrix[0] * X + Matrix[1] * Y + Matrix[2] * Z, + Matrix[3] * X + Matrix[4] * Y + Matrix[5] * Z, + Matrix[6] * X + Matrix[7] * Y + Matrix[8] * Z}; + } + + void BuildNoiseLatticeCache(const FIntVector& Origin, const FIntVector& Size, + int32 SampleStep, float CellSize, uint32 Seed, const float* Transform, + FNoiseLatticeCache& OutCache) + { + const float InvCellSize = 1.0f / CellSize; + const FIntVector End = Origin + FIntVector( + FMath::Max(Size.X - 1, 0), FMath::Max(Size.Y - 1, 0), + FMath::Max(Size.Z - 1, 0)) * SampleStep; + FIntVector MinCell(MAX_int32, MAX_int32, MAX_int32); + FIntVector MaxCell(MIN_int32, MIN_int32, MIN_int32); + for (int32 Corner = 0; Corner < 8; ++Corner) + { + const float X = static_cast((Corner & 1) ? End.X : Origin.X); + const float Y = static_cast((Corner & 2) ? End.Y : Origin.Y); + const float Z = static_cast((Corner & 4) ? End.Z : Origin.Z); + const FNoisePoint3 Point = TransformNoisePoint(Transform, X, Y, Z); + const FIntVector Cell( + FMath::FloorToInt(Point.X * InvCellSize), + FMath::FloorToInt(Point.Y * InvCellSize), + FMath::FloorToInt(Point.Z * InvCellSize)); + MinCell.X = FMath::Min(MinCell.X, Cell.X); + MinCell.Y = FMath::Min(MinCell.Y, Cell.Y); + MinCell.Z = FMath::Min(MinCell.Z, Cell.Z); + MaxCell.X = FMath::Max(MaxCell.X, Cell.X); + MaxCell.Y = FMath::Max(MaxCell.Y, Cell.Y); + MaxCell.Z = FMath::Max(MaxCell.Z, Cell.Z); + } + + // Include a spare lattice layer on either side so floating point rounding at a rotated + // brick edge cannot make a later sample address outside the exact cached hash range. + OutCache.MinCell = MinCell - FIntVector(2, 2, 2); + const FIntVector MaxWithMargin = MaxCell + FIntVector(2, 2, 2); + OutCache.Size = MaxWithMargin - OutCache.MinCell + FIntVector(1, 1, 1); + const int64 ValueCount = static_cast(OutCache.Size.X) + * OutCache.Size.Y * OutCache.Size.Z; + check(ValueCount > 0 && ValueCount <= MAX_int32); + OutCache.Values.SetNumUninitialized(static_cast(ValueCount)); + for (int32 Z = 0; Z < OutCache.Size.Z; ++Z) + for (int32 Y = 0; Y < OutCache.Size.Y; ++Y) + for (int32 X = 0; X < OutCache.Size.X; ++X) + { + OutCache.Values[(Z * OutCache.Size.Y + Y) * OutCache.Size.X + X] = HashValue( + OutCache.MinCell.X + X, OutCache.MinCell.Y + Y, + OutCache.MinCell.Z + Z, Seed); + } + } + + FORCEINLINE float Noise3DWithLatticeCache(float X, float Y, float Z, + float CellSize, const FNoiseLatticeCache& Cache) + { + const float Inv = 1.0f / CellSize; + const float SX = X * Inv; + const float SY = Y * Inv; + const float SZ = Z * Inv; + const int32 IX = FMath::FloorToInt(SX); + const int32 IY = FMath::FloorToInt(SY); + const int32 IZ = FMath::FloorToInt(SZ); + check(IX >= Cache.MinCell.X && IX + 1 < Cache.MinCell.X + Cache.Size.X + && IY >= Cache.MinCell.Y && IY + 1 < Cache.MinCell.Y + Cache.Size.Y + && IZ >= Cache.MinCell.Z && IZ + 1 < Cache.MinCell.Z + Cache.Size.Z); + const float AX = Fade(SX - static_cast(IX)); + const float AY = Fade(SY - static_cast(IY)); + const float AZ = Fade(SZ - static_cast(IZ)); + + // All eight lattice values are adjacent in the cache. Resolve the sample base once + // instead of recalculating three local coordinates and a 3D index for each corner. + const int32 LocalX = IX - Cache.MinCell.X; + const int32 LocalY = IY - Cache.MinCell.Y; + const int32 LocalZ = IZ - Cache.MinCell.Z; + const int32 RowStride = Cache.Size.X; + const int32 PlaneStride = RowStride * Cache.Size.Y; + const float* const Lattice = Cache.Values.GetData() + + (LocalZ * Cache.Size.Y + LocalY) * RowStride + LocalX; + + const float Z0Y0 = FMath::Lerp( + FMath::Lerp(Lattice[0], Lattice[1], AX), + FMath::Lerp(Lattice[RowStride], Lattice[RowStride + 1], AX), AY); + const float Z0Y1 = FMath::Lerp( + FMath::Lerp(Lattice[PlaneStride], Lattice[PlaneStride + 1], AX), + FMath::Lerp(Lattice[PlaneStride + RowStride], + Lattice[PlaneStride + RowStride + 1], AX), AY); + return FMath::Lerp(Z0Y0, Z0Y1, AZ); + } + + FORCEINLINE float DetailAtWithLatticeCaches(float X, float Y, float Z, uint32 Seed, + const FNoiseLatticeCache& BroadCache, const FNoiseLatticeCache& MediumCache, + const FNoiseLatticeCache& FineCache) + { + const FNoisePoint3 BroadPoint = TransformNoisePoint(BroadDetailTransform, X, Y, Z); + const FNoisePoint3 MediumPoint = TransformNoisePoint(MediumDetailTransform, X, Y, Z); + const FNoisePoint3 FinePoint = TransformNoisePoint(FineDetailTransform, X, Y, Z); + const float Broad = Noise3DWithLatticeCache(BroadPoint.X, BroadPoint.Y, BroadPoint.Z, + VoxelPassageGeometry::MetersToVoxels(12.0f), BroadCache); + const float Medium = Noise3DWithLatticeCache(MediumPoint.X, MediumPoint.Y, MediumPoint.Z, + VoxelPassageGeometry::MetersToVoxels(6.0f), MediumCache); + const float Fine = Noise3DWithLatticeCache(FinePoint.X, FinePoint.Y, FinePoint.Z, + VoxelPassageGeometry::MetersToVoxels(3.0f), FineCache); + const float SmoothRidge = FMath::Sqrt(Fine * Fine + 0.0625f) - 0.25f; + return (Broad * 1.82f + Medium * 1.092f + Fine * 0.546f + + SmoothRidge * 0.364f) * DensityUnitsPerLegacyVoxel; + } + + struct FThreadParameterizedDetailCache + { + static constexpr int32 RegionSpanVoxels = 512; + static constexpr int32 MarginVoxels = 64; + bool bReady = false; + uint32 Seed = 0; + FIntVector RegionMin = FIntVector::ZeroValue; + FNoiseLatticeCache Broad; + FNoiseLatticeCache Medium; + FNoiseLatticeCache Fine; + + bool Prepare(FIntVector Origin, FIntVector Size, int32 SampleStep, uint32 InSeed) + { + const int32 Step = FMath::Max(1, SampleStep); + if (static_cast(FMath::Max(0, Size.X - 1)) * Step > MarginVoxels + || static_cast(FMath::Max(0, Size.Y - 1)) * Step > MarginVoxels + || static_cast(FMath::Max(0, Size.Z - 1)) * Step > MarginVoxels) + { + return false; + } + + const auto GetCenteredRegionMin = [](int32 Value) + { + const float Shifted = static_cast(Value) + + static_cast(RegionSpanVoxels / 2); + const int32 RegionIndex = FMath::FloorToInt( + Shifted / static_cast(RegionSpanVoxels)); + return RegionIndex * RegionSpanVoxels - RegionSpanVoxels / 2; + }; + const FIntVector RequestedRegion( + GetCenteredRegionMin(Origin.X), + GetCenteredRegionMin(Origin.Y), + GetCenteredRegionMin(Origin.Z)); + if (bReady && Seed == InSeed && RegionMin == RequestedRegion) + { + return true; + } + + Seed = InSeed; + RegionMin = RequestedRegion; + const FIntVector CacheOrigin = RegionMin - FIntVector( + MarginVoxels, MarginVoxels, MarginVoxels); + const int32 CacheSpan = RegionSpanVoxels + MarginVoxels * 2; + const FIntVector CacheSize(CacheSpan, CacheSpan, CacheSpan); + BuildNoiseLatticeCache(CacheOrigin, CacheSize, 1, + VoxelPassageGeometry::MetersToVoxels(12.0f), + Seed ^ 0x68BC21EBu, BroadDetailTransform, Broad); + BuildNoiseLatticeCache(CacheOrigin, CacheSize, 1, + VoxelPassageGeometry::MetersToVoxels(6.0f), + Seed ^ 0x02E5BE93u, MediumDetailTransform, Medium); + BuildNoiseLatticeCache(CacheOrigin, CacheSize, 1, + VoxelPassageGeometry::MetersToVoxels(3.0f), + Seed ^ 0x967A889Bu, FineDetailTransform, Fine); + bReady = true; + return true; + } + }; + + thread_local FThreadParameterizedDetailCache GParameterizedDetailCache; + FORCEINLINE float DetailAt( float X, float Y, float Z, uint32 Seed, bool bCoherentStep2f) { if (!bCoherentStep2f || IsLegacyStep2eAuditMode()) { - const float Broad = Noise3D(X, Y, Z, 8.0f, Seed ^ 0x68BC21EBu); - const float Medium = Noise3D(X, Y, Z, 4.0f, Seed ^ 0x02E5BE93u); - const float Fine = Noise3D(X, Y, Z, 2.0f, Seed ^ 0x967A889Bu); - const float Micro = Noise3D(X, Y, Z, 1.0f, Seed ^ 0xA511E9B3u); - return Broad * 1.4f + Medium * 0.9f + Fine * 0.45f + Micro * 2.5f; + const float Broad = Noise3D(X, Y, Z, + VoxelPassageGeometry::MetersToVoxels(2.0f), Seed ^ 0x68BC21EBu); + const float Medium = Noise3D(X, Y, Z, + VoxelPassageGeometry::MetersToVoxels(1.0f), Seed ^ 0x02E5BE93u); + const float Fine = Noise3D(X, Y, Z, + VoxelPassageGeometry::MetersToVoxels(0.5f), Seed ^ 0x967A889Bu); + const float Micro = Noise3D(X, Y, Z, + VoxelPassageGeometry::MetersToVoxels(0.25f), Seed ^ 0xA511E9B3u); + return (Broad * 1.4f + Medium * 0.9f + Fine * 0.45f + Micro * 2.5f) + * DensityUnitsPerLegacyVoxel; } - // The shortest wavelength is twelve lattice samples (three meters). Three + // The shortest wavelength is three metres: 24 lattice samples at 12.5 cm/voxel. Three // independently rotated, interpolated scales and the smooth ridge response keep broad folds // while excluding the lattice-scale noise used by the frozen 2e field. // Independent orthonormal frames keep the value-noise bands smooth and bounded without @@ -286,11 +557,15 @@ namespace const float FineX = 0.325568f * X - 0.834725f * Y - 0.443914f * Z; const float FineY = 0.945519f * X + 0.287381f * Y + 0.152824f * Z; const float FineZ = -0.469472f * Y + 0.882948f * Z; - const float Broad = Noise3D(BroadX, BroadY, BroadZ, 48.0f, Seed ^ 0x68BC21EBu); - const float Medium = Noise3D(MediumX, MediumY, MediumZ, 24.0f, Seed ^ 0x02E5BE93u); - const float Fine = Noise3D(FineX, FineY, FineZ, 12.0f, Seed ^ 0x967A889Bu); + const float Broad = Noise3D(BroadX, BroadY, BroadZ, + VoxelPassageGeometry::MetersToVoxels(12.0f), Seed ^ 0x68BC21EBu); + const float Medium = Noise3D(MediumX, MediumY, MediumZ, + VoxelPassageGeometry::MetersToVoxels(6.0f), Seed ^ 0x02E5BE93u); + const float Fine = Noise3D(FineX, FineY, FineZ, + VoxelPassageGeometry::MetersToVoxels(3.0f), Seed ^ 0x967A889Bu); const float SmoothRidge = FMath::Sqrt(Fine * Fine + 0.0625f) - 0.25f; - return Broad * 1.82f + Medium * 1.092f + Fine * 0.546f + SmoothRidge * 0.364f; + return (Broad * 1.82f + Medium * 1.092f + Fine * 0.546f + + SmoothRidge * 0.364f) * DensityUnitsPerLegacyVoxel; } FORCEINLINE float DistanceToPeriodicLine(float Position, float Spacing, float Offset) @@ -456,10 +731,14 @@ namespace GShaftEdgeCache.Edges.Reset(); const uint32 ShaftSeed = static_cast(GPlanCache.Seed) ^ 0x53686674u; - // A 36-voxel bore (9 m at 25 cm) keeps the deterministic route tree visible to the - // 2-voxel navigation/refinement probes while still allowing full-strength wall noise. + // Keep at least a 9 m diameter for the deterministic route tree, and retain room for + // the full wall-detail envelope at the current voxel scale. + const float VerticalShaftDetailPad = LegacyDetailAmplitude + * VoxelPassageGeometry::VoxelSizeMeters + + VoxelPassageGeometry::MetersToVoxels(0.25f); const float ConnectorRadius = FMath::Max3( - Params.ConnectorRadius, LegacyDetailAmplitude + 1.0f, 18.0f); + Params.ConnectorRadius, VerticalShaftDetailPad, + VoxelPassageGeometry::MetersToVoxels(4.5f)); const float CellMinX = CellX * Spacing; const float CellMaxX = CellMinX + Spacing; const float CellMinY = CellY * Spacing; @@ -508,7 +787,9 @@ namespace const float Spacing = FMath::Max(Params.ShaftSpacing, 1.0f); const int32 BaseCellX = FMath::FloorToInt(X / Spacing); const int32 BaseCellY = FMath::FloorToInt(Y / Spacing); - const float VerticalPad = LegacyDetailAmplitude + 1.0f; + const float VerticalPad = LegacyDetailAmplitude + * VoxelPassageGeometry::VoxelSizeMeters + + VoxelPassageGeometry::MetersToVoxels(0.25f); const float BottomZ = Params.StrateBottomWorldZ + Params.BoundarySealThickness; const float TopZ = Params.StrateTopWorldZ - Params.BoundarySealThickness; const float ShaftVertical = FMath::Min(Z - BottomZ, TopZ - Z); @@ -591,37 +872,38 @@ namespace } const float Bottom = static_cast(Plan.MinChunkZ * CHUNK_SIZE); const float Top = static_cast(Plan.MaxChunkZExclusive * CHUNK_SIZE); - Plan.FloorZ = (Bottom + Top) * 0.5f - 24.0f; - Plan.CeilingZ = Plan.FloorZ + 48.0f; + Plan.FloorZ = (Bottom + Top) * 0.5f + - VoxelPassageGeometry::MetersToVoxels(6.0f); + Plan.CeilingZ = Plan.FloorZ + MinimumCaveHeight; switch (Plan.Archetype) { case ECaveGeneratorType::FlatPlain: - Plan.CellSpacing = 160.0f; - Plan.CorridorHalfWidth = 54.0f; + Plan.CellSpacing = VoxelPassageGeometry::MetersToVoxels(40.0f); + Plan.CorridorHalfWidth = VoxelPassageGeometry::MetersToVoxels(13.5f); break; case ECaveGeneratorType::CrystalChamber: - Plan.CellSpacing = 120.0f; - Plan.CorridorHalfWidth = 42.0f; + Plan.CellSpacing = VoxelPassageGeometry::MetersToVoxels(30.0f); + Plan.CorridorHalfWidth = VoxelPassageGeometry::MetersToVoxels(10.5f); break; case ECaveGeneratorType::Maze: - Plan.CellSpacing = 96.0f; - Plan.CorridorHalfWidth = 30.0f; + Plan.CellSpacing = VoxelPassageGeometry::MetersToVoxels(24.0f); + Plan.CorridorHalfWidth = VoxelPassageGeometry::MetersToVoxels(7.5f); break; case ECaveGeneratorType::VerticalShafts: - Plan.CellSpacing = 112.0f; - Plan.CorridorHalfWidth = 36.0f; + Plan.CellSpacing = VoxelPassageGeometry::MetersToVoxels(28.0f); + Plan.CorridorHalfWidth = VoxelPassageGeometry::MetersToVoxels(9.0f); break; case ECaveGeneratorType::FloatingIslands: - Plan.CellSpacing = 112.0f; - Plan.CorridorHalfWidth = 34.0f; + Plan.CellSpacing = VoxelPassageGeometry::MetersToVoxels(28.0f); + Plan.CorridorHalfWidth = VoxelPassageGeometry::MetersToVoxels(8.5f); break; case ECaveGeneratorType::SurfaceWorld: - Plan.CellSpacing = 144.0f; - Plan.CorridorHalfWidth = 46.0f; + Plan.CellSpacing = VoxelPassageGeometry::MetersToVoxels(36.0f); + Plan.CorridorHalfWidth = VoxelPassageGeometry::MetersToVoxels(11.5f); break; default: - Plan.CellSpacing = 128.0f; - Plan.CorridorHalfWidth = 40.0f; + Plan.CellSpacing = VoxelPassageGeometry::MetersToVoxels(32.0f); + Plan.CorridorHalfWidth = VoxelPassageGeometry::MetersToVoxels(10.0f); break; } const uint32 StrateSeed = static_cast(Generator.Seed) @@ -636,12 +918,16 @@ namespace ? GLegacyStep2eProfiles[Plan.GenerationProfile] : GParameterProfiles[Plan.GenerationProfile]).Parameters; const float StrateHeight = bLegacyStep2eAuditMode - ? FMath::Max(48.0f, Top - Bottom - 8.0f) - : FMath::Max(48.0f, Top - Bottom - 64.0f); + ? FMath::Max(MinimumCaveHeight, + Top - Bottom - VoxelPassageGeometry::MetersToVoxels(2.0f)) + : FMath::Max(MinimumCaveHeight, + Top - Bottom - VoxelPassageGeometry::MetersToVoxels(16.0f)); Plan.Generation.RoomHeightVoxels = FMath::Clamp( - Plan.Generation.RoomHeightVoxels, 48.0f, StrateHeight); + Plan.Generation.RoomHeightVoxels, + MinimumCaveHeight, StrateHeight); Plan.Generation.RoomPitchVoxels = FMath::Max( - Plan.Generation.RoomPitchVoxels, 80.0f); + Plan.Generation.RoomPitchVoxels, + VoxelPassageGeometry::MetersToVoxels(20.0f)); Plan.Generation.MinimumRoomRadiusFraction = FMath::Clamp( Plan.Generation.MinimumRoomRadiusFraction, 0.20f, 0.72f); Plan.Generation.MaximumRoomRadiusFraction = FMath::Clamp( @@ -662,7 +948,8 @@ namespace Plan.Generation.BasinProbability = FMath::Clamp( Plan.Generation.BasinProbability, 0.0f, 1.0f); Plan.Generation.BasinDepthVoxels = FMath::Clamp( - Plan.Generation.BasinDepthVoxels, 0.0f, 48.0f); + Plan.Generation.BasinDepthVoxels, 0.0f, + VoxelPassageGeometry::MetersToVoxels(12.0f)); Plan.Generation.WaterLevelFraction = FMath::Clamp( Plan.Generation.WaterLevelFraction, 0.0f, 1.0f); Plan.GenerationFloorZ = (Bottom + Top - Plan.Generation.RoomHeightVoxels) * 0.5f; @@ -705,7 +992,8 @@ namespace const FVector& A, const FVector& B, float Radius) { FPassageBound& Bounds = GPlanCache.PassageBounds.AddDefaulted_GetRef(); - const float R = FMath::Max(Radius, 8.0f) + PassageBoundPadding; + const float R = FMath::Max(Radius, + VoxelPassageGeometry::MetersToVoxels(2.0f)) + PassageBoundPadding; Bounds.SegmentA = A; Bounds.SegmentB = B; Bounds.SupportRadius = R; @@ -786,14 +1074,18 @@ namespace } FloorSum += Site.FloorZ; ++FloorCount; - Site.PadRadius = FMath::Max(Site.HalfWidth + 24.0f, 40.0f); + Site.PadRadius = FMath::Max( + Site.HalfWidth + VoxelPassageGeometry::MetersToVoxels(6.0f), + VoxelPassageGeometry::MetersToVoxels(10.0f)); } if (FloorCount > 0) { const float Bottom = static_cast(Plan.MinChunkZ * CHUNK_SIZE); const float Top = static_cast(Plan.MaxChunkZExclusive * CHUNK_SIZE); Plan.FloorZ = FMath::Clamp( - static_cast(FloorSum / FloorCount), Bottom + 16.0f, Top - 72.0f); + static_cast(FloorSum / FloorCount), + Bottom + VoxelPassageGeometry::MetersToVoxels(4.0f), + Top - VoxelPassageGeometry::MetersToVoxels(18.0f)); Plan.CeilingZ = Plan.FloorZ + MinimumCaveHeight; // Parameterized chambers keep their profile-defined vertical span. The // passage landing is connected into that graph by its generated anchor. @@ -820,7 +1112,8 @@ namespace } const float FloorDelta = FMath::Abs(Site.FloorZ - Plan.FloorZ); Site.BlendRadius = FMath::Max( - LandingFloorBlendVoxels, Site.PadRadius + FloorDelta * 2.0f + 8.0f); + LandingFloorBlendVoxels, Site.PadRadius + FloorDelta * 2.0f + + VoxelPassageGeometry::MetersToVoxels(2.0f)); } Plan.LandingHubIndex = INDEX_NONE; for (int32 SiteIndex = 0; SiteIndex < Plan.Landings.Num(); ++SiteIndex) @@ -877,6 +1170,14 @@ namespace uint8 OpenTunnelEdges = 0; }; + struct FGeneratedTunnelColumnProfile + { + float FloorZ = 0.0f; + float Radius = 0.0f; + float HalfHeight = 0.0f; + float VerticalWave = 0.0f; + }; + struct FParameterizedColumnContext { FGeneratedCaveCell GeneratedCells[9]; @@ -891,12 +1192,15 @@ namespace float RoomFloors[9] = {}; float PillarHorizontals[9] = {}; float FloatingXYDistanceSquared[9][2] = {}; + float FloatingScaledLengthXY[9][2] = {}; + float FloatingScaledGradientXY[9][2] = {}; float SpireBaseZ[9] = {}; float SpireTipZ[9] = {}; float SpireBaseTopZ[9] = {}; float SpireTipBottomZ[9] = {}; float TunnelT[24] = {}; float TunnelHorizontal[24] = {}; + FGeneratedTunnelColumnProfile TunnelProfiles[24] = {}; float LandingDistanceSquared[8] = {}; float LandingDistance[8] = {}; float LandingBridgeT[8] = {}; @@ -989,7 +1293,7 @@ namespace } // Keep a measurable rock web between chambers. Their graph edges open only narrow // connecting tunnels, so the room lattice no longer merges into a forest of pillars. - Cell.RadiusX = FMath::Min(Cell.RadiusX, NearestCenterSpacing * 0.43f); + Cell.RadiusX = FMath::Min(Cell.RadiusX, NearestCenterSpacing * 0.47f); const float Angle = ParameterRoll(Hash, 0xE17Du) * 6.283185307179586f; VoxelMath::DetSinCos(Cell.RoomSin, Cell.RoomCos, Angle); Cell.RadiusY = Cell.RadiusX * FMath::Lerp( @@ -1036,7 +1340,8 @@ namespace * Cell.RadiusX * 0.30f; Cell.PillarY = Cell.CenterY + (ParameterRoll(Hash, 0x2F13u) - 0.5f) * Cell.RadiusY * 0.30f; - Cell.PillarRadius = FMath::Min(14.0f, Cell.RadiusX * FMath::Lerp( + Cell.PillarRadius = FMath::Min( + VoxelPassageGeometry::MetersToVoxels(3.5f), Cell.RadiusX * FMath::Lerp( 0.10f, 0.22f, ParameterRoll(Hash, 0x3467u))); } if (ParameterRoll(Hash, 0x73BDu) < P.LedgeProbability) @@ -1064,7 +1369,9 @@ namespace ? Cell.RadiusX * FMath::Lerp( 0.07f, 0.15f, ParameterRoll(Hash, BlobSalt + 3u)) : FMath::Clamp(Cell.RadiusX * FMath::Lerp( - 0.16f, 0.28f, ParameterRoll(Hash, BlobSalt + 3u)), 8.0f, 18.0f); + 0.16f, 0.28f, ParameterRoll(Hash, BlobSalt + 3u)), + VoxelPassageGeometry::MetersToVoxels(2.0f), + VoxelPassageGeometry::MetersToVoxels(4.5f)); Cell.FloatingRadiusY[Blob] = bLegacyStep2eAuditMode ? Cell.FloatingRadius[Blob] : Cell.FloatingRadius[Blob] * FMath::Lerp( @@ -1078,8 +1385,11 @@ namespace if (ParameterRoll(Hash, 0xC6A9u) < P.SpireProbability) { Cell.FeatureFlags |= HasSpireFlag; - Cell.SpireLength = FMath::Min(56.0f, P.RoomHeightVoxels * 0.30f); - Cell.SpireRadius = FMath::Min(14.0f, Cell.RadiusX * 0.22f); + Cell.SpireLength = FMath::Min( + VoxelPassageGeometry::MetersToVoxels(14.0f), + P.RoomHeightVoxels * 0.30f); + Cell.SpireRadius = FMath::Min( + VoxelPassageGeometry::MetersToVoxels(3.5f), Cell.RadiusX * 0.22f); } if (ParameterRoll(Hash, 0x4537u) < P.BasinProbability) { @@ -1091,7 +1401,8 @@ namespace void PrepareParameterizedAnchors(FStratePlan& Plan) { const float AnchorReach = Plan.Generation.RoomPitchVoxels * 1.25f - + FMath::Max(32.0f, Plan.Generation.TunnelRadiusVoxels); + + FMath::Max(VoxelPassageGeometry::MetersToVoxels(8.0f), + Plan.Generation.TunnelRadiusVoxels); for (FLandingSite& Site : Plan.Landings) { const FIntPoint LandingCell = GenerationCellAt( @@ -1113,7 +1424,8 @@ namespace } } Site.GeneratedAnchorStart = FVector( - Site.Position.X, Site.Position.Y, Site.FloorZ + 24.0f); + Site.Position.X, Site.Position.Y, + Site.FloorZ + VoxelPassageGeometry::MetersToVoxels(6.0f)); const FVector AnchorEnd( NearestCell.CenterX, NearestCell.CenterY, NearestCell.FloorZ + Plan.Generation.TunnelHalfHeightVoxels); @@ -1153,8 +1465,11 @@ namespace { const float FloorZ = Column.RoomFloors[CellIndex]; const float Vertical = FMath::Min(Z - FloorZ, Cell.CeilingZ - Z); - float Room = SmoothMinimum(Horizontal, Vertical, 8.0f); - if ((Cell.FeatureFlags & HasPillarFlag) != 0 && Room > -14.0f) + const bool bLegacyStep2eAuditMode = IsLegacyStep2eAuditMode(); + float Room = SmoothMinimum(Horizontal, Vertical, + VoxelPassageGeometry::MetersToVoxels(2.0f)); + const float FeatureWakeDistance = VoxelPassageGeometry::MetersToVoxels(3.5f); + if ((Cell.FeatureFlags & HasPillarFlag) != 0 && Room > -FeatureWakeDistance) { const float Pillar = FMath::Max3( Column.PillarHorizontals[CellIndex], @@ -1167,9 +1482,12 @@ namespace if ((Cell.FeatureFlags & HasLedgeFlag) != 0) { const float Inner = Cell.RadiusX * 0.58f; - const float Outer = Cell.RadiusX + 1.0f; + const float Outer = Cell.RadiusX + + VoxelPassageGeometry::MetersToVoxels(0.25f); const float Ledge = FMath::Max3( - Inner - Radius, Radius - Outer, FMath::Abs(Z - Cell.LedgeZ) - 2.5f); + Inner - Radius, Radius - Outer, + FMath::Abs(Z - Cell.LedgeZ) + - VoxelPassageGeometry::MetersToVoxels(0.625f)); if (Ledge < Room) { Room = Ledge; @@ -1198,18 +1516,17 @@ namespace } else { - const float BX = X - Cell.FloatingX[Blob]; - const float BY = Y - Cell.FloatingY[Blob]; - const float RadiusX = FMath::Max(Cell.FloatingRadius[Blob], 1.0f); - const float RadiusY = FMath::Max(Cell.FloatingRadiusY[Blob], 1.0f); - const float RadiusZ = FMath::Max(Cell.FloatingRadiusZ[Blob], 1.0f); + const float RadiusX = FMath::Max(Cell.FloatingRadius[Blob], + VoxelPassageGeometry::MetersToVoxels(0.25f)); + const float RadiusY = FMath::Max(Cell.FloatingRadiusY[Blob], + VoxelPassageGeometry::MetersToVoxels(0.25f)); + const float RadiusZ = FMath::Max(Cell.FloatingRadiusZ[Blob], + VoxelPassageGeometry::MetersToVoxels(0.25f)); const float ScaledLength = FMath::Sqrt( - FMath::Square(BX / RadiusX) - + FMath::Square(BY / RadiusY) + Column.FloatingScaledLengthXY[CellIndex][Blob] + FMath::Square(BZ / RadiusZ)); const float ScaledGradient = FMath::Sqrt( - FMath::Square(BX / FMath::Square(RadiusX)) - + FMath::Square(BY / FMath::Square(RadiusY)) + Column.FloatingScaledGradientXY[CellIndex][Blob] + FMath::Square(BZ / FMath::Square(RadiusZ))); BlobSolid = ScaledGradient > 1.0e-3f ? ScaledLength * (ScaledLength - 1.0f) / ScaledGradient @@ -1221,7 +1538,7 @@ namespace } } } - if ((Cell.FeatureFlags & HasSpireFlag) != 0 && Room > -14.0f) + if ((Cell.FeatureFlags & HasSpireFlag) != 0 && Room > -FeatureWakeDistance) { const float BaseZ = Column.SpireBaseZ[CellIndex]; const float TipZ = Column.SpireTipZ[CellIndex]; @@ -1229,7 +1546,8 @@ namespace { const float T = FMath::Clamp( (Z - BaseZ) / FMath::Max(TipZ - BaseZ, 1.0f), 0.0f, 1.0f); - const float RadiusAtZ = FMath::Lerp(Cell.SpireRadius, 1.25f, T); + const float RadiusAtZ = FMath::Lerp(Cell.SpireRadius, + VoxelPassageGeometry::MetersToVoxels(0.3125f), T); const float Spire = FMath::Max3( Radius - RadiusAtZ, BaseZ - Z, Z - TipZ); if (Spire < Room) @@ -1243,7 +1561,8 @@ namespace { const float T = FMath::Clamp( (BaseTop - Z) / FMath::Max(BaseTop - TipBottom, 1.0f), 0.0f, 1.0f); - const float RadiusAtZ = FMath::Lerp(Cell.SpireRadius, 1.25f, T); + const float RadiusAtZ = FMath::Lerp(Cell.SpireRadius, + VoxelPassageGeometry::MetersToVoxels(0.3125f), T); const float Stalactite = FMath::Max3( Radius - RadiusAtZ, Z - BaseTop, TipBottom - Z); if (Stalactite < Room) @@ -1302,10 +1621,49 @@ namespace float Z, float T, float Horizontal) { const float FloorZ = FMath::Lerp(A.FloorZ, B.FloorZ, T); + if (!IsLegacyStep2eAuditMode()) + { + const float MinCellX = static_cast(FMath::Min(A.CellX, B.CellX)); + const float MinCellY = static_cast(FMath::Min(A.CellY, B.CellY)); + const int32 Axis = A.CellX != B.CellX ? 0 : 1; + const uint32 EdgeHash = Hash3( + static_cast(MinCellX), static_cast(MinCellY), + Axis ^ 0x2Du, Plan.GenerationSeed); + const float RadiusSeed = VoxelHash::ToFloat01(EdgeHash ^ 0xA511E9B3u); + const float Phase = VoxelHash::ToFloat01(EdgeHash ^ 0x63D83595u) + * 6.28318530717958647692f; + const float Radius = Plan.Generation.TunnelRadiusVoxels + * (0.82f + 0.36f * RadiusSeed) + * (1.0f + 0.22f * FMath::Sin(6.28318530717958647692f * T + Phase)); + const float HalfHeight = Plan.Generation.TunnelHalfHeightVoxels; + const float VerticalWave = VoxelPassageGeometry::MetersToVoxels(1.0f) + * FMath::Sin(3.14159265358979323846f * T) + * FMath::Sin(6.28318530717958647692f * T + Phase); + const float VerticalOffset = Z - (FloorZ + HalfHeight + VerticalWave); + const float HorizontalDistance = FMath::Max(0.0f, Radius - Horizontal); + const float NormalizedDistance = FMath::Sqrt( + FMath::Square(HorizontalDistance / FMath::Max(Radius, 1.0f)) + + FMath::Square(VerticalOffset / FMath::Max(HalfHeight, 1.0f))); + return (1.0f - NormalizedDistance) * FMath::Min(Radius, HalfHeight); + } const float Vertical = FMath::Min( Z - FloorZ, FloorZ + 2.0f * Plan.Generation.TunnelHalfHeightVoxels - Z); - return SmoothMinimum(Horizontal, Vertical, 5.0f); + return SmoothMinimum(Horizontal, Vertical, + VoxelPassageGeometry::MetersToVoxels(1.25f)); + } + + FORCEINLINE float GeneratedTunnelEdgeFromColumnProfile( + float Z, float Horizontal, const FGeneratedTunnelColumnProfile& Profile) + { + const float VerticalOffset = Z - (Profile.FloorZ + + Profile.HalfHeight + Profile.VerticalWave); + const float HorizontalDistance = FMath::Max(0.0f, Profile.Radius - Horizontal); + const float NormalizedDistance = FMath::Sqrt( + FMath::Square(HorizontalDistance / FMath::Max(Profile.Radius, 1.0f)) + + FMath::Square(VerticalOffset / FMath::Max(Profile.HalfHeight, 1.0f))); + return (1.0f - NormalizedDistance) + * FMath::Min(Profile.Radius, Profile.HalfHeight); } const FGeneratedCaveCell* FindParameterizedBrickRoomCell( @@ -1413,17 +1771,36 @@ namespace const float BX = X - Cell.FloatingX[Blob]; const float BY = Y - Cell.FloatingY[Blob]; OutColumn.FloatingXYDistanceSquared[Index][Blob] = BX * BX + BY * BY; + if (!IsLegacyStep2eAuditMode()) + { + const float RadiusX = FMath::Max(Cell.FloatingRadius[Blob], + VoxelPassageGeometry::MetersToVoxels(0.25f)); + const float RadiusY = FMath::Max(Cell.FloatingRadiusY[Blob], + VoxelPassageGeometry::MetersToVoxels(0.25f)); + const float ScaledX = BX / RadiusX; + const float ScaledY = BY / RadiusY; + const float RadiusX2 = FMath::Square(RadiusX); + const float RadiusY2 = FMath::Square(RadiusY); + const float GradientX = BX / RadiusX2; + const float GradientY = BY / RadiusY2; + OutColumn.FloatingScaledLengthXY[Index][Blob] = + FMath::Square(ScaledX) + FMath::Square(ScaledY); + OutColumn.FloatingScaledGradientXY[Index][Blob] = + FMath::Square(GradientX) + FMath::Square(GradientY); + } } } if ((Cell.FeatureFlags & HasSpireFlag) != 0) { - OutColumn.SpireBaseZ[Index] = Cell.FloorZ - 2.0f; + OutColumn.SpireBaseZ[Index] = Cell.FloorZ + - VoxelPassageGeometry::MetersToVoxels(0.5f); OutColumn.SpireTipZ[Index] = FMath::Min( - Cell.CeilingZ - 2.0f, + Cell.CeilingZ - VoxelPassageGeometry::MetersToVoxels(0.5f), OutColumn.SpireBaseZ[Index] + Cell.SpireLength); - OutColumn.SpireBaseTopZ[Index] = Cell.CeilingZ + 2.0f; + OutColumn.SpireBaseTopZ[Index] = Cell.CeilingZ + + VoxelPassageGeometry::MetersToVoxels(0.5f); OutColumn.SpireTipBottomZ[Index] = FMath::Max( - Cell.FloorZ - 2.0f, + Cell.FloorZ - VoxelPassageGeometry::MetersToVoxels(0.5f), OutColumn.SpireBaseTopZ[Index] - Cell.SpireLength); } } @@ -1487,9 +1864,11 @@ namespace // Beyond this horizontal reach, even the largest supported detail and tunnel // smooth-union rise cannot turn rock into air. Reject before calculating the // point-to-segment distance. - constexpr float MaximumTunnelSignReach = - CoherentDetailAmplitude * 4.0f + 1.25f + 2.0f; + constexpr float MaximumTunnelSignReach = CoherentDetailAmplitude * 4.0f + + VoxelPassageGeometry::MetersToVoxels(0.3125f) + + VoxelPassageGeometry::MetersToVoxels(0.5f); const float Reach = Plan.Generation.TunnelRadiusVoxels + * 1.45f + Plan.Generation.RoomPitchVoxels * 0.16f + MaximumTunnelSignReach; const float MinX = FMath::Min(A.CenterX, B.CenterX); const float MaxX = FMath::Max(A.CenterX, B.CenterX); @@ -1505,14 +1884,92 @@ namespace const float ABX = B.CenterX - A.CenterX; const float ABY = B.CenterY - A.CenterY; const float LengthSquared = ABX * ABX + ABY * ABY; - const float T = LengthSquared > 1.0e-8f + float T = LengthSquared > 1.0e-8f ? FMath::Clamp(((X - A.CenterX) * ABX + (Y - A.CenterY) * ABY) / LengthSquared, 0.0f, 1.0f) : 0.0f; - const float DX = X - (A.CenterX + T * ABX); - const float DY = Y - (A.CenterY + T * ABY); - const float Horizontal = Plan.Generation.TunnelRadiusVoxels - - FMath::Sqrt(DX * DX + DY * DY); + float Horizontal = 0.0f; + FGeneratedTunnelColumnProfile TunnelProfile; + if (IsLegacyStep2eAuditMode()) + { + const float DX = X - (A.CenterX + T * ABX); + const float DY = Y - (A.CenterY + T * ABY); + Horizontal = Plan.Generation.TunnelRadiusVoxels + - FMath::Sqrt(DX * DX + DY * DY); + } + else + { + const float MinCellX = static_cast(FMath::Min(A.CellX, B.CellX)); + const float MinCellY = static_cast(FMath::Min(A.CellY, B.CellY)); + const int32 Axis = A.CellX != B.CellX ? 0 : 1; + const uint32 EdgeHash = Hash3( + static_cast(MinCellX), static_cast(MinCellY), + Axis ^ 0x2Du, Plan.GenerationSeed); + const float RadiusSeed = VoxelHash::ToFloat01(EdgeHash ^ 0xA511E9B3u); + const float Phase = VoxelHash::ToFloat01(EdgeHash ^ 0x63D83595u) + * 6.28318530717958647692f; + const float BendA = (VoxelHash::ToFloat01(EdgeHash ^ 0xB5297A4Du) + * 2.0f - 1.0f) * Plan.Generation.RoomPitchVoxels * 0.16f; + const float BendB = (VoxelHash::ToFloat01(EdgeHash ^ 0x68E31DA4u) + * 2.0f - 1.0f) * Plan.Generation.RoomPitchVoxels * 0.16f; + const float Length = FMath::Sqrt(LengthSquared); + const float NX = Length > 1.0e-4f ? -ABY / Length : 0.0f; + const float NY = Length > 1.0e-4f ? ABX / Length : 0.0f; + const float C1X = A.CenterX + ABX / 3.0f + NX * BendA; + const float C1Y = A.CenterY + ABY / 3.0f + NY * BendA; + const float C2X = A.CenterX + ABX * (2.0f / 3.0f) + NX * BendB; + const float C2Y = A.CenterY + ABY * (2.0f / 3.0f) + NY * BendB; + // Refine the straight-edge projection against the cubic centerline. Four + // deterministic Newton steps are sufficient at the authored bend limit. + for (int32 Iteration = 0; Iteration < 4; ++Iteration) + { + const float U = T; + const float OneMinusU = 1.0f - U; + const float PX = OneMinusU * OneMinusU * OneMinusU * A.CenterX + + 3.0f * OneMinusU * OneMinusU * U * C1X + + 3.0f * OneMinusU * U * U * C2X + U * U * U * B.CenterX; + const float PY = OneMinusU * OneMinusU * OneMinusU * A.CenterY + + 3.0f * OneMinusU * OneMinusU * U * C1Y + + 3.0f * OneMinusU * U * U * C2Y + U * U * U * B.CenterY; + const float DX = 3.0f * OneMinusU * OneMinusU * (C1X - A.CenterX) + + 6.0f * OneMinusU * U * (C2X - C1X) + + 3.0f * U * U * (B.CenterX - C2X); + const float DY = 3.0f * OneMinusU * OneMinusU * (C1Y - A.CenterY) + + 6.0f * OneMinusU * U * (C2Y - C1Y) + + 3.0f * U * U * (B.CenterY - C2Y); + const float DDX = 6.0f * OneMinusU * (C2X - 2.0f * C1X + A.CenterX) + + 6.0f * U * (B.CenterX - 2.0f * C2X + C1X); + const float DDY = 6.0f * OneMinusU * (C2Y - 2.0f * C1Y + A.CenterY) + + 6.0f * U * (B.CenterY - 2.0f * C2Y + C1Y); + const float Denominator = DX * DX + DY * DY + + (PX - X) * DDX + (PY - Y) * DDY; + if (FMath::Abs(Denominator) > 1.0e-4f) + { + T = FMath::Clamp(U - ((PX - X) * DX + (PY - Y) * DY) + / Denominator, 0.0f, 1.0f); + } + } + const float OneMinusT = 1.0f - T; + const float PX = OneMinusT * OneMinusT * OneMinusT * A.CenterX + + 3.0f * OneMinusT * OneMinusT * T * C1X + + 3.0f * OneMinusT * T * T * C2X + T * T * T * B.CenterX; + const float PY = OneMinusT * OneMinusT * OneMinusT * A.CenterY + + 3.0f * OneMinusT * OneMinusT * T * C1Y + + 3.0f * OneMinusT * T * T * C2Y + T * T * T * B.CenterY; + const float DX = X - PX; + const float DY = Y - PY; + const float Distance = FMath::Sqrt(DX * DX + DY * DY); + const float Radius = Plan.Generation.TunnelRadiusVoxels + * (0.82f + 0.36f * RadiusSeed) + * (1.0f + 0.22f * FMath::Sin(6.28318530717958647692f * T + Phase)); + Horizontal = Radius - Distance; + TunnelProfile.FloorZ = FMath::Lerp(A.FloorZ, B.FloorZ, T); + TunnelProfile.Radius = Radius; + TunnelProfile.HalfHeight = Plan.Generation.TunnelHalfHeightVoxels; + TunnelProfile.VerticalWave = VoxelPassageGeometry::MetersToVoxels(1.0f) + * FMath::Sin(3.14159265358979323846f * T) + * FMath::Sin(6.28318530717958647692f * T + Phase); + } int32 InsertAt = OutColumn.TunnelEdgeCount; while (InsertAt > 0 && OutColumn.TunnelHorizontal[InsertAt - 1] < Horizontal) @@ -1524,12 +1981,15 @@ namespace OutColumn.TunnelT[InsertAt] = OutColumn.TunnelT[InsertAt - 1]; OutColumn.TunnelHorizontal[InsertAt] = OutColumn.TunnelHorizontal[InsertAt - 1]; + OutColumn.TunnelProfiles[InsertAt] = + OutColumn.TunnelProfiles[InsertAt - 1]; --InsertAt; } OutColumn.TunnelEdgeCellA[InsertAt] = CellAIndex; OutColumn.TunnelEdgeCellB[InsertAt] = CellBIndex; OutColumn.TunnelT[InsertAt] = T; OutColumn.TunnelHorizontal[InsertAt] = Horizontal; + OutColumn.TunnelProfiles[InsertAt] = TunnelProfile; ++OutColumn.TunnelEdgeCount; }; // Every undirected edge whose endpoints lie in the room-query 3x3 neighborhood is @@ -1588,8 +2048,10 @@ namespace } float Macro = -FLT_MAX; - const float Bottom = static_cast(Plan.MinChunkZ * CHUNK_SIZE) + 8.0f; - const float Top = static_cast(Plan.MaxChunkZExclusive * CHUNK_SIZE) - 8.0f; + const float Bottom = static_cast(Plan.MinChunkZ * CHUNK_SIZE) + + VoxelPassageGeometry::MetersToVoxels(2.0f); + const float Top = static_cast(Plan.MaxChunkZExclusive * CHUNK_SIZE) + - VoxelPassageGeometry::MetersToVoxels(2.0f); static constexpr int32 CellEvaluationOrder[] = {4, 1, 3, 5, 7, 0, 2, 6, 8}; for (int32 OrderIndex = 0; OrderIndex < UE_ARRAY_COUNT(CellEvaluationOrder); ++OrderIndex) @@ -1599,7 +2061,8 @@ namespace const float DistanceSquared = ColumnContext->RoomDistanceSquared[Index]; if (Macro > -FLT_MAX * 0.5f) { - const float MaximumDistance = Cell.RadiusX - Macro + 6.0f; + const float MaximumDistance = Cell.RadiusX - Macro + + VoxelPassageGeometry::MetersToVoxels(1.5f); if (MaximumDistance < 0.0f || DistanceSquared > FMath::Square(MaximumDistance + 1.0e-3f)) { @@ -1607,7 +2070,8 @@ namespace } const float VerticalUpperBound = FMath::Min( Z - ColumnContext->RoomFloors[Index], Cell.CeilingZ - Z); - if (VerticalUpperBound <= Macro - 6.0f) + if (VerticalUpperBound <= Macro + - VoxelPassageGeometry::MetersToVoxels(1.5f)) { continue; } @@ -1616,7 +2080,7 @@ namespace GeneratedRoomCell(Cell, X, Y, Z, ColumnContext->RoomRadii[Index], ColumnContext->RoomHorizontals[Index], - *ColumnContext, Index), 6.0f); + *ColumnContext, Index), VoxelPassageGeometry::MetersToVoxels(1.5f)); } const bool bLegacyStep2eAuditMode = IsLegacyStep2eAuditMode(); if (bLegacyStep2eAuditMode) @@ -1629,7 +2093,8 @@ namespace ColumnContext->TunnelEdgeCellB[EdgeIndex]]; Macro = SmoothMaximum(Macro, GeneratedTunnelEdge( Plan, A, B, Z, ColumnContext->TunnelT[EdgeIndex], - ColumnContext->TunnelHorizontal[EdgeIndex]), 5.0f); + ColumnContext->TunnelHorizontal[EdgeIndex]), + VoxelPassageGeometry::MetersToVoxels(1.25f)); } } else @@ -1637,7 +2102,7 @@ namespace // Far inside solid rock, density magnitude is only a proof margin. Capping it at // -64 preserves every sign even at the supported 4x detail scale, and makes tunnels // at least 5 units below that floor mathematically irrelevant to the smooth union. - Macro = FMath::Max(Macro, -64.0f); + Macro = FMath::Max(Macro, -VoxelPassageGeometry::MetersToVoxels(16.0f)); const float MinimumTunnelFloor = Plan.GenerationFloorZ - Plan.Generation.FloorReliefVoxels; const float MaximumTunnelFloor = @@ -1646,10 +2111,14 @@ namespace Z - MinimumTunnelFloor, MaximumTunnelFloor + 2.0f * Plan.Generation.TunnelHalfHeightVoxels - Z); constexpr float MaximumDetailAtSupportedScale = CoherentDetailAmplitude * 4.0f; - constexpr float MaximumSmoothUnionRise = 1.25f; + constexpr float MaximumSmoothUnionRise = + VoxelPassageGeometry::MetersToVoxels(0.3125f); constexpr float SolidTunnelMargin = - MaximumDetailAtSupportedScale + MaximumSmoothUnionRise + 2.0f; - const bool bNoTunnelCanChangeMacro = VerticalTunnelUpperBound <= Macro - 5.0f; + MaximumDetailAtSupportedScale + MaximumSmoothUnionRise + + VoxelPassageGeometry::MetersToVoxels(0.5f); + const float TunnelSmoothRadius = VoxelPassageGeometry::MetersToVoxels(1.25f); + const bool bNoTunnelCanChangeMacro = + VerticalTunnelUpperBound <= Macro - TunnelSmoothRadius; const bool bNoTunnelCanChangeSign = Macro <= -SolidTunnelMargin && VerticalTunnelUpperBound <= -SolidTunnelMargin; float TunnelMacro = -FLT_MAX; @@ -1659,7 +2128,8 @@ namespace EdgeIndex < ColumnContext->TunnelEdgeCount; ++EdgeIndex) { const float Horizontal = ColumnContext->TunnelHorizontal[EdgeIndex]; - if (Horizontal <= Macro - 5.0f || Horizontal <= TunnelMacro) + if (Horizontal <= Macro - TunnelSmoothRadius + || Horizontal <= TunnelMacro) { // Remaining tunnel SDFs cannot change either the selected maximum or the // room/tunnel smooth union; edges are ordered by descending horizontal bound. @@ -1669,13 +2139,17 @@ namespace ColumnContext->TunnelEdgeCellA[EdgeIndex]]; const FGeneratedCaveCell& B = *ColumnContext->TunnelCells[ ColumnContext->TunnelEdgeCellB[EdgeIndex]]; - TunnelMacro = FMath::Max(TunnelMacro, GeneratedTunnelEdge( - Plan, A, B, Z, ColumnContext->TunnelT[EdgeIndex], Horizontal)); + const float EdgeDensity = bLegacyStep2eAuditMode + ? GeneratedTunnelEdge(Plan, A, B, Z, + ColumnContext->TunnelT[EdgeIndex], Horizontal) + : GeneratedTunnelEdgeFromColumnProfile( + Z, Horizontal, ColumnContext->TunnelProfiles[EdgeIndex]); + TunnelMacro = FMath::Max(TunnelMacro, EdgeDensity); } } if (TunnelMacro > -FLT_MAX * 0.5f) { - Macro = SmoothMaximum(Macro, TunnelMacro, 5.0f); + Macro = SmoothMaximum(Macro, TunnelMacro, TunnelSmoothRadius); } } @@ -1687,7 +2161,9 @@ namespace continue; } const float DistanceSquared = ColumnContext->RoomDistanceSquared[Index]; - const float MaximumDistance = Plan.Generation.ShaftRadiusVoxels - Macro + 5.0f; + const float ShaftSmoothRadius = VoxelPassageGeometry::MetersToVoxels(1.25f); + const float MaximumDistance = + Plan.Generation.ShaftRadiusVoxels - Macro + ShaftSmoothRadius; if (MaximumDistance < 0.0f || DistanceSquared > FMath::Square(MaximumDistance + 1.0e-3f)) { @@ -1696,11 +2172,13 @@ namespace const float Horizontal = Plan.Generation.ShaftRadiusVoxels - ColumnContext->RoomRadii[Index]; const float ShaftInset = IsLegacyStep2eAuditMode() - ? 0.0f : FMath::Min(48.0f, (Top - Bottom) * 0.20f); + ? 0.0f : FMath::Min( + VoxelPassageGeometry::MetersToVoxels(12.0f), + (Top - Bottom) * 0.20f); const float Vertical = FMath::Min( Z - (Bottom + ShaftInset), (Top - ShaftInset) - Z); Macro = SmoothMaximum(Macro, - SmoothMinimum(Horizontal, Vertical, 5.0f), 5.0f); + SmoothMinimum(Horizontal, Vertical, ShaftSmoothRadius), ShaftSmoothRadius); } float DetailFade = 1.0f; @@ -1724,8 +2202,10 @@ namespace const float RoomAir = FMath::Min( Site.PadRadius - Distance, FMath::Min(Z - Site.FloorZ, Site.FloorZ + MinimumCaveHeight - Z)); - Macro = SmoothMaximum(Macro, RoomAir, 6.0f); - if (Distance < FMath::Max(0.0f, Site.PadRadius - 3.0f)) + Macro = SmoothMaximum(Macro, RoomAir, + VoxelPassageGeometry::MetersToVoxels(1.5f)); + if (Distance < FMath::Max(0.0f, Site.PadRadius + - VoxelPassageGeometry::MetersToVoxels(0.75f))) { DetailFade = 0.0f; } @@ -1742,9 +2222,12 @@ namespace / Site.GeneratedAnchorLengthSquared, 0.0f, 1.0f) : 0.0f; const FVector AnchorDelta = Point - (Site.GeneratedAnchorStart + Site.GeneratedAnchorDelta * T); - const float Anchor = FMath::Max(32.0f, Plan.Generation.TunnelRadiusVoxels) + const float Anchor = FMath::Max( + VoxelPassageGeometry::MetersToVoxels(8.0f), + Plan.Generation.TunnelRadiusVoxels) - AnchorDelta.Size(); - Macro = SmoothMaximum(Macro, Anchor, 5.0f); + Macro = SmoothMaximum(Macro, Anchor, + VoxelPassageGeometry::MetersToVoxels(1.25f)); } if (Plan.LandingHubIndex != INDEX_NONE && Plan.Landings.Num() > 1) @@ -1768,7 +2251,9 @@ namespace const float Vertical = FMath::Min( Z - FloorZ, FloorZ + MinimumCaveHeight - Z); Macro = SmoothMaximum(Macro, - SmoothMinimum(Horizontal, Vertical, 5.0f), 5.0f); + SmoothMinimum(Horizontal, Vertical, + VoxelPassageGeometry::MetersToVoxels(1.25f)), + VoxelPassageGeometry::MetersToVoxels(1.25f)); } } @@ -1776,7 +2261,8 @@ namespace { // Keep a four-meter roof and floor through rooms, shafts, and authored passage // modifiers. The same cap is applied again after passage edits in EvaluateInternal. - constexpr float StrateSealThickness = 16.0f; + constexpr float StrateSealThickness = + VoxelPassageGeometry::MetersToVoxels(4.0f); const float StrateBottom = static_cast(Plan.MinChunkZ * CHUNK_SIZE); const float StrateTop = static_cast(Plan.MaxChunkZExclusive * CHUNK_SIZE); const float BoundaryLimit = FMath::Min( @@ -1834,7 +2320,8 @@ namespace if (OutDetailScale != nullptr) { - const float FlatRadius = FMath::Max(0.0f, Site.PadRadius - 3.0f); + const float FlatRadius = FMath::Max(0.0f, Site.PadRadius + - VoxelPassageGeometry::MetersToVoxels(0.75f)); if (DistanceSquared < FlatRadius * FlatRadius) { Scale = 0.0f; @@ -2098,6 +2585,7 @@ static float EvaluateInternal(const UVoxelGenerator& Generator, const FParameterizedBrickContext* ParameterizedContext = nullptr, const FParameterizedColumnContext* ParameterizedColumnContext = nullptr) { + VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker(); if (Context == nullptr && ParameterizedContext == nullptr) { EnsurePlan(Generator); @@ -2129,14 +2617,16 @@ static float EvaluateInternal(const UVoxelGenerator& Generator, : Manager->IsGapChunk(Chunk)); if (bIsGapChunk) { - float Internal = 8.0f; - Manager->ApplyPassageModifier(Internal, WorldX, WorldY, WorldZ, 8.0f, 0.0f); + float Internal = 8.0f * DensityUnitsPerLegacyVoxel; + Manager->ApplyPassageModifier(Internal, WorldX, WorldY, WorldZ, + 8.0f * DensityUnitsPerLegacyVoxel, 0.0f); Result = -Internal; - Manager->ApplyPassageNativeFloorMC(Result, WorldX, WorldY, WorldZ, 8.0f); + Manager->ApplyPassageNativeFloorMC(Result, WorldX, WorldY, WorldZ, + 8.0f * DensityUnitsPerLegacyVoxel); } else { - Result = 8.0f; + Result = 8.0f * DensityUnitsPerLegacyVoxel; } } else @@ -2199,9 +2689,11 @@ static float EvaluateInternal(const UVoxelGenerator& Generator, if (bNearPassage) { float Internal = -Result; - Manager->ApplyPassageModifier(Internal, WorldX, WorldY, WorldZ, 8.0f, 0.0f); + Manager->ApplyPassageModifier(Internal, WorldX, WorldY, WorldZ, + 8.0f * DensityUnitsPerLegacyVoxel, 0.0f); Result = -Internal; - Manager->ApplyPassageNativeFloorMC(Result, WorldX, WorldY, WorldZ, 8.0f); + Manager->ApplyPassageNativeFloorMC(Result, WorldX, WorldY, WorldZ, + 8.0f * DensityUnitsPerLegacyVoxel); } } } @@ -2218,13 +2710,15 @@ static float EvaluateInternal(const UVoxelGenerator& Generator, && Radial > Generator.WorldRadiusVoxels - Generator.EdgeSealThickness) { const float Alpha = (Generator.WorldRadiusVoxels - Radial) / Generator.EdgeSealThickness; - Result = FMath::Min(Result, Alpha * 8.0f - 8.0f); + Result = FMath::Min(Result, + (Alpha * 8.0f - 8.0f) * DensityUnitsPerLegacyVoxel); } } if (bParameterized && Plan != nullptr && !IsLegacyStep2eAuditMode()) { - constexpr float StrateSealThickness = 16.0f; + constexpr float StrateSealThickness = + VoxelPassageGeometry::MetersToVoxels(4.0f); const float StrateBottom = static_cast(Plan->MinChunkZ * CHUNK_SIZE); const float StrateTop = static_cast(Plan->MaxChunkZExclusive * CHUNK_SIZE); const float BoundaryLimit = FMath::Min( @@ -2246,6 +2740,7 @@ static float EvaluateInternal(const UVoxelGenerator& Generator, { Result -= Generator.DiffLayer->GetDensityOffset(Chunk, WorldX, WorldY, WorldZ); } + VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker(); return Result; } @@ -2356,8 +2851,9 @@ float EvaluateParameterizedWithDetailValue(const UVoxelGenerator& Generator, void EvaluateParameterizedColumnWithDetailValues( const UVoxelGenerator& Generator, float WorldX, float WorldY, float WorldZStart, const float* DetailValues, int32 DetailStride, int32 Count, float* OutValues, - const FParameterizedBrickContext* Context, float DetailScale) + const FParameterizedBrickContext* Context, float DetailScale, int32 SampleStep) { + SampleStep = FMath::Max(1, SampleStep); if (DetailValues == nullptr || OutValues == nullptr || Count <= 0 || DetailStride <= 0) { return; @@ -2374,13 +2870,14 @@ void EvaluateParameterizedColumnWithDetailValues( Plan = FindStrate(Chunk); } const int32 EndChunkZ = FMath::FloorToInt( - (WorldZStart + static_cast(Count - 1)) / CHUNK_SIZE); + (WorldZStart + static_cast(Count - 1) * SampleStep) / CHUNK_SIZE); if (Plan == nullptr || Chunk.Z != EndChunkZ) { for (int32 Z = 0; Z < Count; ++Z) { OutValues[Z] = EvaluateInternal( - Generator, WorldX, WorldY, WorldZStart + static_cast(Z), + Generator, WorldX, WorldY, + WorldZStart + static_cast(Z) * SampleStep, DetailValues[Z * DetailStride], true, nullptr, DetailScale, nullptr, true, Context); } @@ -2392,20 +2889,23 @@ void EvaluateParameterizedColumnWithDetailValues( for (int32 Z = 0; Z < Count; ++Z) { OutValues[Z] = EvaluateInternal( - Generator, WorldX, WorldY, WorldZStart + static_cast(Z), - DetailValues[Z * DetailStride], true, nullptr, DetailScale, + Generator, WorldX, WorldY, + WorldZStart + static_cast(Z) * SampleStep, + DetailValues[Z * DetailStride], true, nullptr, DetailScale, nullptr, true, Context, &ColumnContext); } } void BuildParameterizedBrickContext( const UVoxelGenerator& Generator, FIntVector OriginVoxels, FIntVector Size, - FParameterizedBrickContext& OutContext) + FParameterizedBrickContext& OutContext, int32 SampleStep) { + SampleStep = FMath::Max(1, SampleStep); OutContext = FParameterizedBrickContext(); EnsurePlan(Generator); const FIntVector End = OriginVoxels + FIntVector( - FMath::Max(Size.X - 1, 0), FMath::Max(Size.Y - 1, 0), FMath::Max(Size.Z - 1, 0)); + FMath::Max(Size.X - 1, 0), FMath::Max(Size.Y - 1, 0), + FMath::Max(Size.Z - 1, 0)) * SampleStep; const FIntVector Chunk( FMath::FloorToInt(static_cast(OriginVoxels.X) / CHUNK_SIZE), FMath::FloorToInt(static_cast(OriginVoxels.Y) / CHUNK_SIZE), @@ -2428,13 +2928,13 @@ void BuildParameterizedBrickContext( OutContext.bHasDiffMods = Generator.DiffLayer != nullptr && Generator.DiffLayer->HasAnyMods(); const FVector Min( - static_cast(OriginVoxels.X - 1), - static_cast(OriginVoxels.Y - 1), - static_cast(OriginVoxels.Z - 1)); + static_cast(OriginVoxels.X - SampleStep), + static_cast(OriginVoxels.Y - SampleStep), + static_cast(OriginVoxels.Z - SampleStep)); const FVector Max( - static_cast(OriginVoxels.X + Size.X), - static_cast(OriginVoxels.Y + Size.Y), - static_cast(OriginVoxels.Z + Size.Z)); + static_cast(OriginVoxels.X + Size.X * SampleStep), + static_cast(OriginVoxels.Y + Size.Y * SampleStep), + static_cast(OriginVoxels.Z + Size.Z * SampleStep)); FindPassageCandidates(FBox(Min, Max), OutContext.PassageCandidates); OutContext.bMayContainPassage = !OutContext.PassageCandidates.IsEmpty(); @@ -2491,11 +2991,56 @@ void BuildParameterizedBrickContext( } } +void EvaluateParameterizedDensityBrick( + const UVoxelGenerator& Generator, FIntVector OriginVoxels, FIntVector Size, + float DetailScale, FParameterizedBrickContext& InOutContext, + TArray& InOutDetail, TArray& InOutColumnValues, + TArray& OutValues, int32 SampleStep) +{ + SampleStep = FMath::Max(1, SampleStep); + if (Size.X <= 0 || Size.Y <= 0 || Size.Z <= 0) + { + OutValues.Reset(); + return; + } + const int64 SampleCount = static_cast(Size.X) * Size.Y * Size.Z; + if (SampleCount > MAX_int32) + { + OutValues.Reset(); + return; + } + + BuildDetailBrick(Generator, OriginVoxels, Size, DetailScale, + InOutDetail, nullptr, /*bParameterized=*/true, SampleStep); + BuildParameterizedBrickContext( + Generator, OriginVoxels, Size, InOutContext, SampleStep); + InOutColumnValues.SetNumUninitialized(Size.Z); + OutValues.SetNumUninitialized(static_cast(SampleCount)); + const int32 DetailStride = Size.X * Size.Y; + for (int32 Y = 0; Y < Size.Y; ++Y) + for (int32 X = 0; X < Size.X; ++X) + { + const int32 DetailBase = Y * Size.X + X; + EvaluateParameterizedColumnWithDetailValues( + Generator, + static_cast(OriginVoxels.X + X * SampleStep), + static_cast(OriginVoxels.Y + Y * SampleStep), + static_cast(OriginVoxels.Z), + InOutDetail.GetData() + DetailBase, DetailStride, Size.Z, + InOutColumnValues.GetData(), &InOutContext, DetailScale, SampleStep); + for (int32 Z = 0; Z < Size.Z; ++Z) + { + OutValues[(Z * Size.Y + Y) * Size.X + X] = InOutColumnValues[Z]; + } + } +} + void BuildDetailBrick(const UVoxelGenerator& Generator, FIntVector OriginVoxels, FIntVector Size, float DetailScale, TArray& OutDetail, - FBrickEvaluationContext* OutContext, bool bParameterized) + FBrickEvaluationContext* OutContext, bool bParameterized, int32 SampleStep) { (void)DetailScale; + SampleStep = FMath::Max(1, SampleStep); const int32 SampleCount = Size.X * Size.Y * Size.Z; OutDetail.SetNumUninitialized(SampleCount); FMemory::Memzero(OutDetail.GetData(), static_cast(SampleCount) * sizeof(float)); @@ -2518,7 +3063,7 @@ void BuildDetailBrick(const UVoxelGenerator& Generator, FIntVector OriginVoxels, OutContext->PassageCandidates.Reset(); } if (Size.X <= 0 || Size.Y <= 0 || Size.Z <= 0 - || Size.X > 8 || Size.Y > 8 || Size.Z > 8 + || (!bParameterized && (Size.X > 8 || Size.Y > 8 || Size.Z > 8)) || Generator.StrateManager == nullptr) { return; @@ -2530,9 +3075,9 @@ void BuildDetailBrick(const UVoxelGenerator& Generator, FIntVector OriginVoxels, FMath::FloorToInt(static_cast(OriginVoxels.Y) / CHUNK_SIZE), FMath::FloorToInt(static_cast(OriginVoxels.Z) / CHUNK_SIZE)); const FIntVector EndChunk( - FMath::FloorToInt(static_cast(OriginVoxels.X + Size.X - 1) / CHUNK_SIZE), - FMath::FloorToInt(static_cast(OriginVoxels.Y + Size.Y - 1) / CHUNK_SIZE), - FMath::FloorToInt(static_cast(OriginVoxels.Z + Size.Z - 1) / CHUNK_SIZE)); + FMath::FloorToInt(static_cast(OriginVoxels.X + (Size.X - 1) * SampleStep) / CHUNK_SIZE), + FMath::FloorToInt(static_cast(OriginVoxels.Y + (Size.Y - 1) * SampleStep) / CHUNK_SIZE), + FMath::FloorToInt(static_cast(OriginVoxels.Z + (Size.Z - 1) * SampleStep) / CHUNK_SIZE)); const bool bStableStrate = Chunk.Z == EndChunk.Z; const FStratePlan* Plan = FindStrate(Chunk); if (OutContext != nullptr) @@ -2566,7 +3111,7 @@ void BuildDetailBrick(const UVoxelGenerator& Generator, FIntVector OriginVoxels, for (int32 Y = 0; Y < Size.Y; ++Y) for (int32 X = 0; X < Size.X; ++X) { - const int32 WorldZ = OriginVoxels.Z + Z; + const int32 WorldZ = OriginVoxels.Z + Z * SampleStep; const FIntVector SampleChunk(0, 0, FMath::FloorToInt(static_cast(WorldZ) / CHUNK_SIZE)); const FStratePlan* SamplePlan = FindStrate(SampleChunk); @@ -2575,8 +3120,8 @@ void BuildDetailBrick(const UVoxelGenerator& Generator, FIntVector OriginVoxels, const uint32 SampleSeed = static_cast(Generator.Seed) ^ static_cast(SamplePlan->StrateIndex * 7919 + 104729); OutDetail[(Z * Size.Y + Y) * Size.X + X] = DetailAt( - static_cast(OriginVoxels.X + X), - static_cast(OriginVoxels.Y + Y), + static_cast(OriginVoxels.X + X * SampleStep), + static_cast(OriginVoxels.Y + Y * SampleStep), static_cast(WorldZ), SampleSeed, bParameterized); } } @@ -2601,8 +3146,8 @@ void BuildDetailBrick(const UVoxelGenerator& Generator, FIntVector OriginVoxels, for (int32 X = 0; X < Size.X; ++X) { const int32 XYIndex = Y * Size.X + X; - const float WorldX = static_cast(OriginVoxels.X + X); - const float WorldY = static_cast(OriginVoxels.Y + Y); + const float WorldX = static_cast(OriginVoxels.X + X * SampleStep); + const float WorldY = static_cast(OriginVoxels.Y + Y * SampleStep); const float XLane = DistanceToPeriodicLine( WorldX, Plan->CellSpacing, Plan->OffsetX); const float YLane = DistanceToPeriodicLine( @@ -2646,7 +3191,8 @@ void BuildDetailBrick(const UVoxelGenerator& Generator, FIntVector OriginVoxels, OutContext->LandingBridgeHorizontalByXY[SiteIndex * XYCount + XYIndex] = BridgeHorizontal; OutContext->LandingBridgeFloorByXY[SiteIndex * XYCount + XYIndex] = BridgeFloor; - const float FlatRadius = FMath::Max(0.0f, Site.PadRadius - 3.0f); + const float FlatRadius = FMath::Max(0.0f, Site.PadRadius + - VoxelPassageGeometry::MetersToVoxels(0.75f)); if (DistanceSquared < FlatRadius * FlatRadius) { DetailFade = 0.0f; @@ -2661,6 +3207,61 @@ void BuildDetailBrick(const UVoxelGenerator& Generator, FIntVector OriginVoxels, const uint32 Seed = static_cast(Generator.Seed) ^ static_cast(Plan->StrateIndex * 7919 + 104729); + if (bParameterized) + { + if (SampleCount < 4096) + { + // Small mixed proof bricks do not repay their own three lookup grids, but adjacent + // bricks reuse a worker-local cache spanning the surrounding 64 m region. Keep the + // scalar interpolation unchanged while hashing each lattice corner only once there. + const bool bUseSharedDetailCache = bParameterized + && !IsLegacyStep2eAuditMode() + && GParameterizedDetailCache.Prepare( + OriginVoxels, Size, SampleStep, Seed); + for (int32 Z = 0; Z < Size.Z; ++Z) + for (int32 Y = 0; Y < Size.Y; ++Y) + for (int32 X = 0; X < Size.X; ++X) + { + const float WorldX = static_cast(OriginVoxels.X + X * SampleStep); + const float WorldY = static_cast(OriginVoxels.Y + Y * SampleStep); + const float WorldZ = static_cast(OriginVoxels.Z + Z * SampleStep); + OutDetail[(Z * Size.Y + Y) * Size.X + X] = bUseSharedDetailCache + ? DetailAtWithLatticeCaches(WorldX, WorldY, WorldZ, Seed, + GParameterizedDetailCache.Broad, + GParameterizedDetailCache.Medium, + GParameterizedDetailCache.Fine) + : DetailAt(WorldX, WorldY, WorldZ, Seed, true); + } + return; + } + + // Cache each rotated band’s exact integer lattice hashes once for this brick. Every + // retained sample still uses the same point interpolation and arithmetic as DetailAt; + // adjacent samples simply reuse the same deterministic corner values. + FNoiseLatticeCache BroadCache; + FNoiseLatticeCache MediumCache; + FNoiseLatticeCache FineCache; + BuildNoiseLatticeCache(OriginVoxels, Size, SampleStep, + VoxelPassageGeometry::MetersToVoxels(12.0f), + Seed ^ 0x68BC21EBu, BroadDetailTransform, BroadCache); + BuildNoiseLatticeCache(OriginVoxels, Size, SampleStep, + VoxelPassageGeometry::MetersToVoxels(6.0f), + Seed ^ 0x02E5BE93u, MediumDetailTransform, MediumCache); + BuildNoiseLatticeCache(OriginVoxels, Size, SampleStep, + VoxelPassageGeometry::MetersToVoxels(3.0f), + Seed ^ 0x967A889Bu, FineDetailTransform, FineCache); + for (int32 Z = 0; Z < Size.Z; ++Z) + for (int32 Y = 0; Y < Size.Y; ++Y) + for (int32 X = 0; X < Size.X; ++X) + { + OutDetail[(Z * Size.Y + Y) * Size.X + X] = DetailAtWithLatticeCaches( + static_cast(OriginVoxels.X + X * SampleStep), + static_cast(OriginVoxels.Y + Y * SampleStep), + static_cast(OriginVoxels.Z + Z * SampleStep), Seed, + BroadCache, MediumCache, FineCache); + } + return; + } const int32 CountX = Size.X; const int32 CountY = Size.Y; const auto SampleIndex = [CountX, CountY](int32 X, int32 Y, int32 Z) @@ -2681,11 +3282,11 @@ void BuildDetailBrick(const UVoxelGenerator& Generator, FIntVector OriginVoxels, const int32 MinCellZ = FMath::FloorToInt( static_cast(OriginVoxels.Z) / static_cast(CellSize)); const int32 MaxCellX = FMath::FloorToInt( - static_cast(OriginVoxels.X + Size.X - 1) / static_cast(CellSize)); + static_cast(OriginVoxels.X + (Size.X - 1) * SampleStep) / static_cast(CellSize)); const int32 MaxCellY = FMath::FloorToInt( - static_cast(OriginVoxels.Y + Size.Y - 1) / static_cast(CellSize)); + static_cast(OriginVoxels.Y + (Size.Y - 1) * SampleStep) / static_cast(CellSize)); const int32 MaxCellZ = FMath::FloorToInt( - static_cast(OriginVoxels.Z + Size.Z - 1) / static_cast(CellSize)); + static_cast(OriginVoxels.Z + (Size.Z - 1) * SampleStep) / static_cast(CellSize)); const int32 GridX = MaxCellX - MinCellX + 2; const int32 GridY = MaxCellY - MinCellY + 2; const int32 GridZ = MaxCellZ - MinCellZ + 2; @@ -2707,7 +3308,7 @@ void BuildDetailBrick(const UVoxelGenerator& Generator, FIntVector OriginVoxels, float FadeZ[8]; for (int32 X = 0; X < Size.X; ++X) { - const float SX = static_cast(OriginVoxels.X + X) + const float SX = static_cast(OriginVoxels.X + X * SampleStep) / static_cast(CellSize); const int32 Cell = FMath::FloorToInt(SX); CellX[X] = Cell - MinCellX; @@ -2715,7 +3316,7 @@ void BuildDetailBrick(const UVoxelGenerator& Generator, FIntVector OriginVoxels, } for (int32 Y = 0; Y < Size.Y; ++Y) { - const float SY = static_cast(OriginVoxels.Y + Y) + const float SY = static_cast(OriginVoxels.Y + Y * SampleStep) / static_cast(CellSize); const int32 Cell = FMath::FloorToInt(SY); CellY[Y] = Cell - MinCellY; @@ -2723,7 +3324,7 @@ void BuildDetailBrick(const UVoxelGenerator& Generator, FIntVector OriginVoxels, } for (int32 Z = 0; Z < Size.Z; ++Z) { - const float SZ = static_cast(OriginVoxels.Z + Z) + const float SZ = static_cast(OriginVoxels.Z + Z * SampleStep) / static_cast(CellSize); const int32 Cell = FMath::FloorToInt(SZ); CellZ[Z] = Cell - MinCellZ; @@ -2769,9 +3370,12 @@ void BuildDetailBrick(const UVoxelGenerator& Generator, FIntVector OriginVoxels, TArray Broad; TArray Medium; TArray Fine; - AddNoiseBand(32, Seed ^ 0x68BC21EBu, 1.0f, &Broad); - AddNoiseBand(16, Seed ^ 0x02E5BE93u, 1.0f, &Medium); - AddNoiseBand(8, Seed ^ 0x967A889Bu, 1.0f, &Fine); + AddNoiseBand(VoxelPassageGeometry::MetersToVoxels(12.0f), + Seed ^ 0x68BC21EBu, 1.0f, &Broad); + AddNoiseBand(VoxelPassageGeometry::MetersToVoxels(6.0f), + Seed ^ 0x02E5BE93u, 1.0f, &Medium); + AddNoiseBand(VoxelPassageGeometry::MetersToVoxels(3.0f), + Seed ^ 0x967A889Bu, 1.0f, &Fine); for (int32 Index = 0; Index < SampleCount; ++Index) { const float Ridge = FMath::Sqrt(Fine[Index] * Fine[Index] + 0.0625f) - 0.25f; @@ -2781,10 +3385,14 @@ void BuildDetailBrick(const UVoxelGenerator& Generator, FIntVector OriginVoxels, return; } - AddNoiseBand(8, Seed ^ 0x68BC21EBu, 1.4f, nullptr); - AddNoiseBand(4, Seed ^ 0x02E5BE93u, 0.9f, nullptr); - AddNoiseBand(2, Seed ^ 0x967A889Bu, 0.45f, nullptr); - AddNoiseBand(1, Seed ^ 0xA511E9B3u, 2.5f, nullptr); + AddNoiseBand(VoxelPassageGeometry::MetersToVoxels(2.0f), + Seed ^ 0x68BC21EBu, 1.4f * DensityUnitsPerLegacyVoxel, nullptr); + AddNoiseBand(VoxelPassageGeometry::MetersToVoxels(1.0f), + Seed ^ 0x02E5BE93u, 0.9f * DensityUnitsPerLegacyVoxel, nullptr); + AddNoiseBand(VoxelPassageGeometry::MetersToVoxels(0.5f), + Seed ^ 0x967A889Bu, 0.45f * DensityUnitsPerLegacyVoxel, nullptr); + AddNoiseBand(VoxelPassageGeometry::MetersToVoxels(0.25f), + Seed ^ 0xA511E9B3u, 2.5f * DensityUnitsPerLegacyVoxel, nullptr); } static bool TryClassifyBrickInternal( @@ -2792,8 +3400,9 @@ static bool TryClassifyBrickInternal( FIntVector Size, bool& bOutAir, float DetailScale, bool* bOutEvaluatedMacroCentre, EBrickFallbackReason* OutFallbackReason, - bool bParameterized) + bool bParameterized, int32 SampleStep) { + SampleStep = FMath::Max(1, SampleStep); if (OutFallbackReason != nullptr) { *OutFallbackReason = EBrickFallbackReason::UnsupportedState; @@ -2812,17 +3421,17 @@ static bool TryClassifyBrickInternal( } EnsurePlan(Generator); const int32 MinChunkX = FMath::FloorToInt( - static_cast(OriginVoxels.X - 1) / CHUNK_SIZE); + static_cast(OriginVoxels.X - SampleStep) / CHUNK_SIZE); const int32 MaxChunkX = FMath::FloorToInt( - static_cast(OriginVoxels.X + Size.X) / CHUNK_SIZE); + static_cast(OriginVoxels.X + Size.X * SampleStep) / CHUNK_SIZE); const int32 MinChunkY = FMath::FloorToInt( - static_cast(OriginVoxels.Y - 1) / CHUNK_SIZE); + static_cast(OriginVoxels.Y - SampleStep) / CHUNK_SIZE); const int32 MaxChunkY = FMath::FloorToInt( - static_cast(OriginVoxels.Y + Size.Y) / CHUNK_SIZE); + static_cast(OriginVoxels.Y + Size.Y * SampleStep) / CHUNK_SIZE); const int32 MinChunkZ = FMath::FloorToInt( - static_cast(OriginVoxels.Z - 1) / CHUNK_SIZE); + static_cast(OriginVoxels.Z - SampleStep) / CHUNK_SIZE); const int32 MaxChunkZ = FMath::FloorToInt( - static_cast(OriginVoxels.Z + Size.Z) / CHUNK_SIZE); + static_cast(OriginVoxels.Z + Size.Z * SampleStep) / CHUNK_SIZE); const FIntVector Chunk( MinChunkX, MinChunkY, MinChunkZ); const FStratePlan* Plan = FindStrate(Chunk); @@ -2838,13 +3447,13 @@ static bool TryClassifyBrickInternal( } } const FVector Min( - static_cast(OriginVoxels.X - 1), - static_cast(OriginVoxels.Y - 1), - static_cast(OriginVoxels.Z - 1)); + static_cast(OriginVoxels.X - SampleStep), + static_cast(OriginVoxels.Y - SampleStep), + static_cast(OriginVoxels.Z - SampleStep)); const FVector Max( - static_cast(OriginVoxels.X + Size.X), - static_cast(OriginVoxels.Y + Size.Y), - static_cast(OriginVoxels.Z + Size.Z)); + static_cast(OriginVoxels.X + Size.X * SampleStep), + static_cast(OriginVoxels.Y + Size.Y * SampleStep), + static_cast(OriginVoxels.Z + Size.Z * SampleStep)); const FBox Expanded(Min, Max); if (IntersectsPassageBounds(Expanded)) { @@ -2991,17 +3600,18 @@ bool TryClassifyBrick(const UVoxelGenerator& Generator, FIntVector OriginVoxels, EBrickFallbackReason* OutFallbackReason) { return TryClassifyBrickInternal(Generator, OriginVoxels, Size, bOutAir, - DetailScale, bOutEvaluatedMacroCentre, OutFallbackReason, false); + DetailScale, bOutEvaluatedMacroCentre, OutFallbackReason, false, 1); } bool TryClassifyParameterizedBrick(const UVoxelGenerator& Generator, FIntVector OriginVoxels, FIntVector Size, bool& bOutAir, float DetailScale, bool* bOutEvaluatedMacroCentre, - EBrickFallbackReason* OutFallbackReason) + EBrickFallbackReason* OutFallbackReason, + int32 SampleStep) { return TryClassifyBrickInternal(Generator, OriginVoxels, Size, bOutAir, - DetailScale, bOutEvaluatedMacroCentre, OutFallbackReason, true); + DetailScale, bOutEvaluatedMacroCentre, OutFallbackReason, true, SampleStep); } } diff --git a/Source/VoxelForge/Private/VoxelStrateComposer.cpp b/Source/VoxelForge/Private/VoxelStrateComposer.cpp index 9bc9877..4c3582b 100644 --- a/Source/VoxelForge/Private/VoxelStrateComposer.cpp +++ b/Source/VoxelForge/Private/VoxelStrateComposer.cpp @@ -1716,11 +1716,11 @@ namespace float StrateHeightInVoxels) { const float H = FMath::Max(StrateHeightInVoxels, static_cast(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( diff --git a/Source/VoxelForge/Private/VoxelStrateManager.cpp b/Source/VoxelForge/Private/VoxelStrateManager.cpp index 5fc1413..e3bc290 100644 --- a/Source/VoxelForge/Private/VoxelStrateManager.cpp +++ b/Source/VoxelForge/Private/VoxelStrateManager.cpp @@ -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), diff --git a/Source/VoxelForge/Private/VoxelWorld.cpp b/Source/VoxelForge/Private/VoxelWorld.cpp index b8c8055..b0dcf2f 100644 --- a/Source/VoxelForge/Private/VoxelWorld.cpp +++ b/Source/VoxelForge/Private/VoxelWorld.cpp @@ -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 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(DeepDropTopChunkZ + 1) * CHUNK_SIZE; - const float ShaftTopVoxels = StrateTopExclusiveVoxels + 32.0f; + const float ShaftTopVoxels = StrateTopExclusiveVoxels + + VoxelPassageGeometry::MetersToVoxels(8.0f); const float ShaftBottomVoxels = - static_cast(DeepDropBottomChunkZ + 1) * CHUNK_SIZE - 1.0f; + static_cast(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(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(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(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) { - bTestCeilingCaptureRequested = true; + if (bCameraMoveWindow) + { + if (TestCeilingCameraLastMoveSeconds > 0.0) + { + const double MoveDeltaSeconds = FMath::Max( + 0.0, CameraNow - TestCeilingCameraLastMoveSeconds); + TestCeilingCameraWorldPosition += TestCeilingCameraMoveDirection + * TestCeilingCameraMoveSpeedCmPerSecond + * static_cast(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(TestCeilingCaptureDelaySeconds); + const bool bSecondCaptureDue = bTestCeilingCaptureRequested + && !bTestCeilingSecondCaptureRequested + && TestCeilingSecondCaptureDelaySeconds > 0.0f + && CameraElapsedSeconds >= static_cast(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(AppliedVisibleTileCount), static_cast(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); diff --git a/Source/VoxelForge/Public/VoxelDensityOp.h b/Source/VoxelForge/Public/VoxelDensityOp.h index 2b82d91..a25aaa2 100644 --- a/Source/VoxelForge/Public/VoxelDensityOp.h +++ b/Source/VoxelForge/Public/VoxelDensityOp.h @@ -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; diff --git a/Source/VoxelForge/Public/VoxelGenerator.h b/Source/VoxelForge/Public/VoxelGenerator.h index abd7b8a..d51da90 100644 --- a/Source/VoxelForge/Public/VoxelGenerator.h +++ b/Source/VoxelForge/Public/VoxelGenerator.h @@ -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) diff --git a/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h b/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h index ce1d623..c0ad880 100644 --- a/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h +++ b/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h @@ -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. diff --git a/Source/VoxelForge/Public/VoxelPassageGeometry.h b/Source/VoxelForge/Public/VoxelPassageGeometry.h index b4c6a12..02e47f8 100644 --- a/Source/VoxelForge/Public/VoxelPassageGeometry.h +++ b/Source/VoxelForge/Public/VoxelPassageGeometry.h @@ -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(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, diff --git a/Source/VoxelForge/Public/VoxelRedefinedDensityField.h b/Source/VoxelForge/Public/VoxelRedefinedDensityField.h index 1282215..6f434ab 100644 --- a/Source/VoxelForge/Public/VoxelRedefinedDensityField.h +++ b/Source/VoxelForge/Public/VoxelRedefinedDensityField.h @@ -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& InOutDetail, TArray& InOutColumnValues, + TArray& 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& 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, diff --git a/Source/VoxelForge/Public/VoxelSeasonManifest.h b/Source/VoxelForge/Public/VoxelSeasonManifest.h index c6a5c5d..736ef33 100644 --- a/Source/VoxelForge/Public/VoxelSeasonManifest.h +++ b/Source/VoxelForge/Public/VoxelSeasonManifest.h @@ -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; diff --git a/Source/VoxelForge/Public/VoxelSettings.h b/Source/VoxelForge/Public/VoxelSettings.h index 70ca9c5..6d49a84 100644 --- a/Source/VoxelForge/Public/VoxelSettings.h +++ b/Source/VoxelForge/Public/VoxelSettings.h @@ -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< CoverPointA; TOptional 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; diff --git a/Source/VoxelForge/Public/VoxelStrateTypes.h b/Source/VoxelForge/Public/VoxelStrateTypes.h index b7b514b..5578cf4 100644 --- a/Source/VoxelForge/Public/VoxelStrateTypes.h +++ b/Source/VoxelForge/Public/VoxelStrateTypes.h @@ -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; diff --git a/Source/VoxelForge/Public/VoxelTypes.h b/Source/VoxelForge/Public/VoxelTypes.h index 418fcca..b6c7617 100644 --- a/Source/VoxelForge/Public/VoxelTypes.h +++ b/Source/VoxelForge/Public/VoxelTypes.h @@ -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) diff --git a/Source/VoxelForge/Public/VoxelWorld.h b/Source/VoxelForge/Public/VoxelWorld.h index 0302859..af29cf3 100644 --- a/Source/VoxelForge/Public/VoxelWorld.h +++ b/Source/VoxelForge/Public/VoxelWorld.h @@ -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 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 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 PawnTickPrerequisite; TWeakObjectPtr PawnMovementTickPrerequisite; TWeakObjectPtr 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; diff --git a/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp b/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp index 624282a..90f7258 100644 --- a/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp +++ b/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp @@ -55,8 +55,8 @@ using FExploreJsonWriter = TJsonWriter>; 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(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(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(SurfaceEdgeCount) * 0.25 * 0.25; - const double VolumeCubicMeters = static_cast(Total) * 0.25 * 0.25 * 0.25; + constexpr double VoxelMeters = static_cast(VOXEL_SIZE) / 100.0; + Result.SurfaceAreaSquareMeters = static_cast(SurfaceEdgeCount) + * VoxelMeters * VoxelMeters; + const double VolumeCubicMeters = static_cast(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(TotalLongestRunVoxels) / ColumnsWithAir : 0.0; - constexpr int32 CoarseStep = 4; + constexpr int32 VoxelsPerMeter = static_cast(100.0f / VOXEL_SIZE + 0.5f); + constexpr int32 CoarseStep = VoxelsPerMeter; const int32 CoarseDimension = (Dimension - 1) / CoarseStep + 1; const int32 CoarseCount = CoarseDimension * CoarseDimension * CoarseDimension; TArray Parent; @@ -3323,7 +3353,8 @@ FStep2fFragmentMetrics ComputeStep2fFragmentMetrics( ComponentsIntersectingCore.Add(FindRoot(Index)); } } - constexpr int32 VoxelsPerCubicMeter = 4 * 4 * 4; + constexpr int32 VoxelsPerMeter = static_cast(100.0f / VOXEL_SIZE + 0.5f); + constexpr int32 VoxelsPerCubicMeter = VoxelsPerMeter * VoxelsPerMeter * VoxelsPerMeter; TMap CrumbByRoot; for (int32 Index = 0; Index < Count; ++Index) { @@ -3427,18 +3458,39 @@ struct FStep2fGridSignatureMetrics }; FStep2fGridSignatureMetrics ComputeStep2fGridSignature( - const TArray& 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(100.0f / VOXEL_SIZE + 0.5f); + constexpr int32 HalfWindow = static_cast( + 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 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; - ++SampleCount; + const int32 Z = RingOrigin.Z + FMath::RoundToInt( + static_cast(RingDimension - 1) * ZIndex / (HeightLayers - 1)); + for (int32 YIndex = 0; YIndex < XYDimension; ++YIndex) + for (int32 XIndex = 0; XIndex < XYDimension; ++XIndex) + { + const float WorldX = static_cast(RingCentre.X - HalfWindow + + XIndex * SampleStep); + const float WorldY = static_cast(RingCentre.Y - HalfWindow + + YIndex * SampleStep); + const uint8 bAir = VoxelForgeRedefinedDensityField::EvaluateParameterized( + Generator, WorldX, WorldY, static_cast(Z)) > 0.0f ? 1 : 0; + AirSamples[(ZIndex * XYDimension + YIndex) * XYDimension + XIndex] = bAir; + AirCount += bAir; + ++SampleCount; + } } const double Mean = SampleCount > 0 ? static_cast(AirCount) / SampleCount : 0.0; @@ -3453,31 +3505,35 @@ FStep2fGridSignatureMetrics ComputeStep2fGridSignature( for (int32 Direction = 0; Direction < 8; ++Direction) { const float Angle = static_cast(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(bAirA) - Mean) * (static_cast(bAirB) - Mean); ++Pairs; @@ -3531,22 +3587,24 @@ FStep2fDetailSpectrum ComputeStep2fDetailSpectrum(const TArray& 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(25.0f / VOXEL_SIZE + 0.5f); + if (X + PhysicalLagVoxels < Dimension) { DifferenceEnergy += FMath::Square( - static_cast(Detail[Index + 1] - Detail[Index])); + static_cast(Detail[Index + PhysicalLagVoxels] - Detail[Index])); ++DifferenceCount; } - if (Y + 1 < Dimension) + if (Y + PhysicalLagVoxels < Dimension) { DifferenceEnergy += FMath::Square( - static_cast(Detail[Index + Dimension] - Detail[Index])); + static_cast(Detail[Index + PhysicalLagVoxels * Dimension] - Detail[Index])); ++DifferenceCount; } - if (Z + 1 < Dimension) + if (Z + PhysicalLagVoxels < Dimension) { DifferenceEnergy += FMath::Square( - static_cast(Detail[Index + Dimension * Dimension] - Detail[Index])); + static_cast(Detail[Index + PhysicalLagVoxels * Dimension * Dimension] + - Detail[Index])); ++DifferenceCount; } } @@ -3615,9 +3673,10 @@ float Step2fPointTriangleDistanceSquared( FIntVector SelectStep2fMeshOrigin( const TArray& Density, FIntVector RingOrigin, int32 Dimension) { - constexpr int32 PhysicalTileVoxels = 64; - constexpr int32 ScoreStepVoxels = 4; - constexpr int32 CandidateStepVoxels = 32; + constexpr int32 VoxelsPerMeter = static_cast(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 Step2eChamberVolumeQuantilesJson( { TSharedRef Json = MakeShared(); constexpr double Pi = 3.14159265358979323846; - constexpr double CubicMetersPerVoxel = 0.25 * 0.25 * 0.25; + constexpr double VoxelMeters = static_cast(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> 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(Dimension) + * Dimension * Dimension; + TArray> TrialRows; + TArray WallTimes; + TArray 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 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(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 TrialJson = MakeShared(); + 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(Timing.EvaluatedSamples)); + TrialJson->SetNumberField(TEXT("skipped_samples"), + static_cast(Timing.SkippedSamples)); + TrialJson->SetNumberField(TEXT("bricks"), static_cast(Timing.BrickCount)); + TrialJson->SetNumberField(TEXT("skipped_bricks"), + static_cast(Timing.SkippedBricks)); + TrialJson->SetStringField(TEXT("quantized_hash"), FString::Printf( + TEXT("%016llx"), static_cast(Timing.QuantizedHash))); + TrialJson->SetStringField(TEXT("raw_float_hash"), FString::Printf( + TEXT("%016llx"), static_cast(Timing.RawFloatHash))); + TrialRows.Add(MakeShared(TrialJson)); + } + + WallTimes.Sort(); + CpuTimes.Sort(); + TSharedRef Cube = MakeShared(); + Cube->SetNumberField(TEXT("dimension"), Dimension); + Cube->SetNumberField(TEXT("sample_count"), static_cast(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(Dimension - 1) * VOXEL_SIZE / 100.0); + Cube->SetNumberField(TEXT("density_output_mib"), + static_cast(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(EvaluatedSamples)); + Cube->SetNumberField(TEXT("skipped_samples"), static_cast(SkippedSamples)); + Cube->SetNumberField(TEXT("bricks"), static_cast(BrickCount)); + Cube->SetNumberField(TEXT("skipped_bricks"), static_cast(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(FirstQuantizedHash))); + Cube->SetStringField(TEXT("raw_float_hash"), FString::Printf( + TEXT("%016llx"), static_cast(FirstRawHash))); + Cube->SetArrayField(TEXT("trials"), TrialRows); + CubeRows.Add(MakeShared(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(ExpectedSamples), + static_cast(Dimension - 1) * VOXEL_SIZE / 100.0, + Workers, WallTimes[WallTimes.Num() / 2], WallTimes[0], WallTimes.Last(), + static_cast(EvaluatedSamples), static_cast(SkippedSamples), + bDeterministic ? 1 : 0, + static_cast(FirstQuantizedHash)); + } + + TSharedRef Report = MakeShared(); + 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(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> 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(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( + 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( + 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> 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& Layout = World.Manager->GetLayout(); - constexpr int32 XYStep = 4; // 1 m coverage grid - constexpr int32 ShellOffsets[] = {1, 5, 9}; + constexpr int32 XYStep = static_cast(100.0f / VOXEL_SIZE + 0.5f); + constexpr int32 ShellOffsets[] = { + static_cast(25.0f / VOXEL_SIZE + 0.5f), + static_cast(125.0f / VOXEL_SIZE + 0.5f), + static_cast(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(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(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(CenterZ) / CHUNK_SIZE) * CHUNK_SIZE + 4); + const FIntVector BrickParitySize( + VoxelForgeRedefinedDensityField::ParameterizedBrickSamples, + VoxelForgeRedefinedDensityField::ParameterizedBrickSamples, 16); + VoxelForgeRedefinedDensityField::FParameterizedBrickContext BrickParityContext; + TArray BrickParityDetail; + TArray BrickParityColumn; + TArray 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(BrickParityOrigin.X + X), + static_cast(BrickParityOrigin.Y + Y), + static_cast(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 Gates = MakeShared(); @@ -5621,7 +5971,7 @@ int32 RunStep2f(const FString& Params) Step2eCrumbNumbers->SetNumberField(TEXT("crumbs_below_1m3_max"), WorstStep2eCrumbs); TSharedRef Step2fCrumbNumbers = MakeShared(); 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 Step2eFlakeNumbers = MakeShared(); Step2eFlakeNumbers->SetNumberField(TEXT("max_dihedral_share_over_45deg"), WorstStep2eDihedralShare); @@ -5637,13 +5987,13 @@ int32 RunStep2f(const FString& Params) TSharedRef Step2fDetailNumbers = MakeShared(); 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 Step2eLodNumbers = MakeShared(); Step2eLodNumbers->SetNumberField(TEXT("max_l1_p95_surface_distance_voxels"), WorstStep2eLodDistance); TSharedRef Step2fLodNumbers = MakeShared(); 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 Step2eGridNumbers = MakeShared(); 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(BrickScalarMismatchCount)); Existing->SetStringField(TEXT("six_worker_hash"), FString::Printf( TEXT("%016llx"), static_cast(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(Export.RegionOrigin.X) * 0.25); - Writer->WriteValue(TEXT("y"), static_cast(Export.RegionOrigin.Y) * 0.25); - Writer->WriteValue(TEXT("z"), static_cast(Export.RegionOrigin.Z) * 0.25); + Writer->WriteValue(TEXT("x"), static_cast(Export.RegionOrigin.X) * VOXEL_SIZE / 100.0); + Writer->WriteValue(TEXT("y"), static_cast(Export.RegionOrigin.Y) * VOXEL_SIZE / 100.0); + Writer->WriteValue(TEXT("z"), static_cast(Export.RegionOrigin.Z) * VOXEL_SIZE / 100.0); Writer->WriteObjectEnd(); Writer->WriteObjectStart(TEXT("max_exclusive")); - Writer->WriteValue(TEXT("x"), static_cast(Export.RegionOrigin.X + Export.RegionSize) * 0.25); - Writer->WriteValue(TEXT("y"), static_cast(Export.RegionOrigin.Y + Export.RegionSize) * 0.25); - Writer->WriteValue(TEXT("z"), static_cast(Export.RegionOrigin.Z + Export.RegionSize) * 0.25); + Writer->WriteValue(TEXT("x"), static_cast(Export.RegionOrigin.X + Export.RegionSize) * VOXEL_SIZE / 100.0); + Writer->WriteValue(TEXT("y"), static_cast(Export.RegionOrigin.Y + Export.RegionSize) * VOXEL_SIZE / 100.0); + Writer->WriteValue(TEXT("z"), static_cast(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(Axis.FinalMinCrossingVoxels)); Writer.WriteValue(TEXT("final_max_crossing_voxels"), static_cast(Axis.FinalMaxCrossingVoxels)); Writer.WriteValue(TEXT("core_periodic_amplitude_voxels"), static_cast(Axis.CorePeriodicAmplitudeVoxels)); - Writer.WriteValue(TEXT("core_periodic_amplitude_cm"), static_cast(Axis.CorePeriodicAmplitudeVoxels) * 25.0); + Writer.WriteValue(TEXT("core_periodic_amplitude_cm"), static_cast(Axis.CorePeriodicAmplitudeVoxels) * VOXEL_SIZE); Writer.WriteValue(TEXT("core_periodic_period_voxels"), static_cast(Axis.CorePeriodicPeriodVoxels)); Writer.WriteValue(TEXT("core_periodic_measured_samples"), Axis.CorePeriodicMeasuredSamples); Writer.WriteValue(TEXT("core_periodic_measured_length_voxels"), static_cast(Axis.CorePeriodicMeasuredLengthVoxels)); Writer.WriteValue(TEXT("final_periodic_amplitude_voxels"), static_cast(Axis.FinalPeriodicAmplitudeVoxels)); - Writer.WriteValue(TEXT("final_periodic_amplitude_cm"), static_cast(Axis.FinalPeriodicAmplitudeVoxels) * 25.0); + Writer.WriteValue(TEXT("final_periodic_amplitude_cm"), static_cast(Axis.FinalPeriodicAmplitudeVoxels) * VOXEL_SIZE); Writer.WriteValue(TEXT("final_periodic_period_voxels"), static_cast(Axis.FinalPeriodicPeriodVoxels)); Writer.WriteValue(TEXT("final_periodic_measured_samples"), Axis.FinalPeriodicMeasuredSamples); Writer.WriteValue(TEXT("final_periodic_measured_length_voxels"), static_cast(Axis.FinalPeriodicMeasuredLengthVoxels)); @@ -10482,12 +10838,12 @@ void WriteTunnelMouthProbeAxis( Writer.WriteValue(TEXT("min_crossing_voxels"), static_cast(Axis.MinCrossingVoxels)); Writer.WriteValue(TEXT("max_crossing_voxels"), static_cast(Axis.MaxCrossingVoxels)); Writer.WriteValue(TEXT("periodic_amplitude_voxels"), static_cast(Axis.PeriodicAmplitudeVoxels)); - Writer.WriteValue(TEXT("periodic_amplitude_cm"), static_cast(Axis.PeriodicAmplitudeVoxels) * 25.0); + Writer.WriteValue(TEXT("periodic_amplitude_cm"), static_cast(Axis.PeriodicAmplitudeVoxels) * VOXEL_SIZE); Writer.WriteValue(TEXT("periodic_period_voxels"), static_cast(Axis.PeriodicPeriodVoxels)); Writer.WriteValue(TEXT("periodic_measured_samples"), Axis.PeriodicMeasuredSamples); Writer.WriteValue(TEXT("periodic_measured_length_voxels"), static_cast(Axis.PeriodicMeasuredLengthVoxels)); Writer.WriteValue(TEXT("local_median_periodic_amplitude_voxels"), static_cast(Axis.LocalMedianPeriodicAmplitudeVoxels)); - Writer.WriteValue(TEXT("local_median_periodic_amplitude_cm"), static_cast(Axis.LocalMedianPeriodicAmplitudeVoxels) * 25.0); + Writer.WriteValue(TEXT("local_median_periodic_amplitude_cm"), static_cast(Axis.LocalMedianPeriodicAmplitudeVoxels) * VOXEL_SIZE); Writer.WriteValue(TEXT("local_median_periodic_period_voxels"), static_cast(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(Output.TunnelCoreProbe.Mouth.MaxWallPeriodicAmplitudeVoxels)); - Writer->WriteValue(TEXT("max_wall_periodic_amplitude_cm"), static_cast(Output.TunnelCoreProbe.Mouth.MaxWallPeriodicAmplitudeVoxels) * 25.0); + Writer->WriteValue(TEXT("max_wall_periodic_amplitude_cm"), static_cast(Output.TunnelCoreProbe.Mouth.MaxWallPeriodicAmplitudeVoxels) * VOXEL_SIZE); Writer->WriteValue(TEXT("max_floor_periodic_amplitude_voxels"), static_cast(Output.TunnelCoreProbe.Mouth.MaxFloorPeriodicAmplitudeVoxels)); - Writer->WriteValue(TEXT("max_floor_periodic_amplitude_cm"), static_cast(Output.TunnelCoreProbe.Mouth.MaxFloorPeriodicAmplitudeVoxels) * 25.0); + Writer->WriteValue(TEXT("max_floor_periodic_amplitude_cm"), static_cast(Output.TunnelCoreProbe.Mouth.MaxFloorPeriodicAmplitudeVoxels) * VOXEL_SIZE); Writer->WriteValue(TEXT("max_local_median_wall_periodic_amplitude_voxels"), static_cast(Output.TunnelCoreProbe.Mouth.MaxLocalMedianWallPeriodicAmplitudeVoxels)); - Writer->WriteValue(TEXT("max_local_median_wall_periodic_amplitude_cm"), static_cast(Output.TunnelCoreProbe.Mouth.MaxLocalMedianWallPeriodicAmplitudeVoxels) * 25.0); + Writer->WriteValue(TEXT("max_local_median_wall_periodic_amplitude_cm"), static_cast(Output.TunnelCoreProbe.Mouth.MaxLocalMedianWallPeriodicAmplitudeVoxels) * VOXEL_SIZE); Writer->WriteValue(TEXT("max_local_median_floor_periodic_amplitude_voxels"), static_cast(Output.TunnelCoreProbe.Mouth.MaxLocalMedianFloorPeriodicAmplitudeVoxels)); - Writer->WriteValue(TEXT("max_local_median_floor_periodic_amplitude_cm"), static_cast(Output.TunnelCoreProbe.Mouth.MaxLocalMedianFloorPeriodicAmplitudeVoxels) * 25.0); + Writer->WriteValue(TEXT("max_local_median_floor_periodic_amplitude_cm"), static_cast(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(World.TargetBottomWorldZ) * 0.25); - Writer->WriteValue(TEXT("target_top_m_exclusive"), static_cast(World.TargetTopWorldZ) * 0.25); + Writer->WriteValue(TEXT("target_bottom_m"), static_cast(World.TargetBottomWorldZ) * VOXEL_SIZE / 100.0); + Writer->WriteValue(TEXT("target_top_m_exclusive"), static_cast(World.TargetTopWorldZ) * VOXEL_SIZE / 100.0); Writer->WriteValue(TEXT("target_boundary_seal_thickness_voxels"), static_cast(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(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(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); diff --git a/Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.cpp b/Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.cpp index 5ad7656..6ccfc02 100644 --- a/Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.cpp +++ b/Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.cpp @@ -181,39 +181,75 @@ namespace VoxelForgeRegionEval TArray 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(OriginVoxels.X) / ProofBrickSize.X), - FMath::FloorToInt(static_cast(OriginVoxels.Y) / ProofBrickSize.Y), - FMath::FloorToInt(static_cast(OriginVoxels.Z) / ProofBrickSize.Z)); + FMath::FloorToInt(static_cast(OriginVoxels.X - BrickAlignmentOffset) / ProofBrickSize.X), + FMath::FloorToInt(static_cast(OriginVoxels.Y - BrickAlignmentOffset) / ProofBrickSize.Y), + FMath::FloorToInt(static_cast(OriginVoxels.Z - BrickAlignmentOffset) / ProofBrickSize.Z)); const FIntVector LastBrick( - FMath::FloorToInt(static_cast(Last.X) / ProofBrickSize.X), - FMath::FloorToInt(static_cast(Last.Y) / ProofBrickSize.Y), - FMath::FloorToInt(static_cast(Last.Z) / ProofBrickSize.Z)); - Bricks.Reserve((LastBrick.X - FirstBrick.X + 1) - * (LastBrick.Y - FirstBrick.Y + 1) * (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) + FMath::FloorToInt(static_cast(Last.X - BrickAlignmentOffset) / ProofBrickSize.X), + FMath::FloorToInt(static_cast(Last.Y - BrickAlignmentOffset) / ProofBrickSize.Y), + FMath::FloorToInt(static_cast(Last.Z - BrickAlignmentOffset) / ProofBrickSize.Z)); + const uint64 TotalProofBrickCount = static_cast(LastBrick.X - FirstBrick.X + 1) + * static_cast(LastBrick.Y - FirstBrick.Y + 1) + * static_cast(LastBrick.Z - FirstBrick.Z + 1); + const int32 BrickColumnsX = LastBrick.X - FirstBrick.X + 1; + const int32 BrickColumnsY = LastBrick.Y - FirstBrick.Y + 1; + if (bParameterized) { - const FIntVector BrickWorldMin( - BX * ProofBrickSize.X, BY * ProofBrickSize.Y, BZ * ProofBrickSize.Z); - FResearchBrick& Brick = Bricks.AddDefaulted_GetRef(); - Brick.Origin = FIntVector( - FMath::Max(OriginVoxels.X, BrickWorldMin.X), - FMath::Max(OriginVoxels.Y, BrickWorldMin.Y), - FMath::Max(OriginVoxels.Z, BrickWorldMin.Z)); - const FIntVector BrickEnd = BrickWorldMin + ProofBrickSize; - const FIntVector RingEnd = OriginVoxels - + FIntVector(Dimension, Dimension, Dimension); - const FIntVector End( - FMath::Min(BrickEnd.X, RingEnd.X), - FMath::Min(BrickEnd.Y, RingEnd.Y), - FMath::Min(BrickEnd.Z, RingEnd.Z)); - Brick.Size = End - Brick.Origin; - Brick.Lattice = Brick.Origin - OriginVoxels; - Brick.SampleCount = Brick.Size.X * Brick.Size.Y * Brick.Size.Z; + 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 + BrickAlignmentOffset, + BY * ProofBrickSize.Y + BrickAlignmentOffset, + BZ * ProofBrickSize.Z + BrickAlignmentOffset); + FResearchBrick& Brick = Bricks.AddDefaulted_GetRef(); + Brick.Origin = FIntVector( + FMath::Max(OriginVoxels.X, BrickWorldMin.X), + FMath::Max(OriginVoxels.Y, BrickWorldMin.Y), + FMath::Max(OriginVoxels.Z, BrickWorldMin.Z)); + const FIntVector BrickEnd = BrickWorldMin + ProofBrickSize; + const FIntVector RingEnd = OriginVoxels + + FIntVector(Dimension, Dimension, Dimension); + const FIntVector End( + FMath::Min(BrickEnd.X, RingEnd.X), + FMath::Min(BrickEnd.Y, RingEnd.Y), + FMath::Min(BrickEnd.Z, RingEnd.Z)); + Brick.Size = End - Brick.Origin; + Brick.Lattice = Brick.Origin - OriginVoxels; + Brick.SampleCount = Brick.Size.X * Brick.Size.Y * Brick.Size.Z; + } } TArray WorkerWall; @@ -253,6 +289,10 @@ namespace VoxelForgeRegionEval double CpuStart = 0.0; const bool bHaveCpuStart = GetCurrentThreadCpuSeconds(CpuStart); TArray BrickDetail; + TArray ParameterizedDetail; + TArray ParameterizedColumnValues; + TArray ParameterizedBrickValues; + TArray 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(FallbackReason); + if (ReasonIndex < 5) + { + ++WorkerFallbackReasons[WorkerIndex * 5 + ReasonIndex]; + } + } + } + + if (bCanSkip) + { + ParameterizedProofKinds[ProofZ] = bUniformAir ? 1 : 2; + ++WorkerSkippedBricks[WorkerIndex]; + const uint64 ProofSampleCount = static_cast( + 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(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(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(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(RunSize.X) * sizeof(float)); + } + WorkerEvaluated[WorkerIndex] += static_cast(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,68 +480,25 @@ namespace VoxelForgeRegionEval const double EvaluationStart = FPlatformTime::Seconds(); VoxelForgeRedefinedDensityField::BuildDetailBrick( Generator, Brick.Origin, Brick.Size, DetailScale, BrickDetail, - bParameterized ? nullptr : &BrickContext, bParameterized); - if (bParameterized) + &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) { - 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(Brick.Origin.X + X), - static_cast(Brick.Origin.Y + Y), - static_cast(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(Brick.Size.X) * sizeof(float)); - } - WorkerEvaluated[WorkerIndex] += static_cast(Brick.SampleCount); - } - else - { - 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) - { - const int32 WorldX = Brick.Origin.X + X; - const int32 WorldY = Brick.Origin.Y + Y; - const int32 WorldZ = Brick.Origin.Z + Z; - const int64 LatticeIndex = LinearIndex( - Brick.Lattice.X + X, Brick.Lattice.Y + Y, - Brick.Lattice.Z + Z, Dimension); - const float DetailValue = BrickDetail[ - (Z * Brick.Size.Y + Y) * Brick.Size.X + X]; - OutDensities[static_cast(LatticeIndex)] = - VoxelForgeRedefinedDensityField::EvaluateWithDetailValue( - Generator, static_cast(WorldX), static_cast(WorldY), - static_cast(WorldZ), DetailValue, - nullptr, DetailScale, &BrickContext); - ++WorkerEvaluated[WorkerIndex]; - } + const int32 WorldX = Brick.Origin.X + X; + const int32 WorldY = Brick.Origin.Y + Y; + const int32 WorldZ = Brick.Origin.Z + Z; + const int64 LatticeIndex = LinearIndex( + Brick.Lattice.X + X, Brick.Lattice.Y + Y, + Brick.Lattice.Z + Z, Dimension); + const float DetailValue = BrickDetail[ + (Z * Brick.Size.Y + Y) * Brick.Size.X + X]; + OutDensities[static_cast(LatticeIndex)] = + VoxelForgeRedefinedDensityField::EvaluateWithDetailValue( + Generator, static_cast(WorldX), static_cast(WorldY), + static_cast(WorldZ), DetailValue, + 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(Bricks.Num()); + OutTiming.BrickCount = bParameterized + ? TotalProofBrickCount : static_cast(Bricks.Num()); OutTiming.ObservedWorkerThreads = 0; for (int32 Worker = 0; Worker < NumWorkers; ++Worker) { diff --git a/Tools/VoxelForgeTest.ps1 b/Tools/VoxelForgeTest.ps1 index fe6ffda..8dd4a08 100644 --- a/Tools/VoxelForgeTest.ps1 +++ b/Tools/VoxelForgeTest.ps1 @@ -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=(?[-+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}=(?[-+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=(?[1-9][0-9]*)') + $regenerate = @(Select-String -LiteralPath $LogPath ` + -Pattern '\[VoxelWorld\] RegenerateAllChunks .*cleared (?[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 (?[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=(?[12])') + $accepted = [regex]::Match([string]$captureLine.Line, 'capture_requested=(?[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)")