From 6ccf3718ff74d456e647153f065c3a4a0987d4aa Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Mon, 14 Sep 2026 09:39:57 +0200 Subject: [PATCH] measure(perf): per-component ablation of the TunnelNetwork path; exact worms restored Round (Codex; reviewed by me against PERF-SAMPLED.md). Revert: voxel.WormNoiseMode / voxel.WormLatticeStep, their storage and their switches are REMOVED. VoxelWormField has one path again, exact Perlin with the N1 short circuit. Kept: bEnableWorms (asset), WormsForceOff (measurement only), the exact block skip (default 0). Ablation instrument: 14 development-only, world-changing force-off bits (voxel.TunnelAblate*), applied on the fused, op-stack/classifier, cached SDF/core and modifier/floor paths. They resolve once per process, are hashed into the generation fingerprint, and are compiled to zero in UE_BUILD_SHIPPING (VoxelDensityAblation.cpp). They are a measurement instrument, never a gameplay or multiplayer switch. Defaults exact: canonical export OBJ b3e5f4c3..9b377, worms-region 311CA42C / 75,482 triangles, capability 20,830 / 10,909, and 841/841 keyed tiles identical to NextHeadStaticTrace_20260913. Static worker seconds saved by removing ONE component (two off runs vs the interleaved all-on 33.06 s; baseline noise 0.27%): tunnel SDF 11.4%, cave warp 10.3%, passage carving 10.1%, landing posts 9.9%, passage structural posts 5.6%, worms 2.4%, native floor 2.2%, origin spine 1.6%, room SDF 1.6%, everything else <= 1.1% Moving (2+2 runs each) confirms the top five at 6.0-11.6%. Tunnel SDF off cuts LOD0 generation p95 104 -> 83 ms: it owns the slow tail. The positive savings sum to 58% of the static total; the other 42% is shared. The rows are not additive. Unexplained: tunnel-core OFF made the static total 2.9x SLOWER (95.5 s) while LOD0 generation got faster (p50 64.7 -> 60.6 ms) and triangles fell. The report blames the changed field, which does not fit; more likely a slow fallback path in coarse tiles. That row is not a cost measurement. Ledger: every Unreal process exited 0 and verified the loaded DLL; two PowerShell parser errors happened before any process started. No crashes or retries. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TaFdXhTdYMkW1xrV6ApT6Y --- PERF-SAMPLED.md | 247 ++++++++++- .../Private/VoxelCaveMorphology.cpp | 60 ++- .../Private/VoxelDensityAblation.cpp | 131 ++++++ .../Private/VoxelDensityOpStack.cpp | 259 +++++++----- Source/VoxelForge/Private/VoxelGenerator.cpp | 166 ++++---- .../VoxelForge/Private/VoxelStrateManager.cpp | 123 ++++-- Source/VoxelForge/Private/VoxelWormField.cpp | 391 +----------------- .../VoxelForge/Public/VoxelDensityAblation.h | 60 +++ .../Public/VoxelDensityPrimitives.h | 16 +- Source/VoxelForge/Public/VoxelNoise.h | 50 --- Source/VoxelForge/Public/VoxelWormField.h | 15 +- .../Private/VoxelForgeExploreCommandlet.cpp | 10 +- 12 files changed, 833 insertions(+), 695 deletions(-) create mode 100644 Source/VoxelForge/Private/VoxelDensityAblation.cpp create mode 100644 Source/VoxelForge/Public/VoxelDensityAblation.h diff --git a/PERF-SAMPLED.md b/PERF-SAMPLED.md index 77a3ae8..225c567 100644 --- a/PERF-SAMPLED.md +++ b/PERF-SAMPLED.md @@ -1226,21 +1226,21 @@ Both runs also reported `canonical_repeat_equal=true`. The disabled asset path t the hand-zeroed control byte-for-byte at the exported geometry hash, while exceeding the required 20,830 / 10,909 capability gate. -### Part 2 — measured cheap field +### Historical Part 2 — measured cheap field (reverted 2026-09-14) -The selected field is `voxel.WormNoiseMode=1`: a deterministic scalar value-noise evaluation -using the same worm coordinate mapping, threshold, N1 short-circuit, N2 offset, and strength -contract. The code default is mode 1. Mode 0 remains the legacy gradient-Perlin exact control. -`voxel.WormNoiseMode` and `voxel.WormLatticeStep` are documented development-only, -world-changing experiments; they are not the multiplayer off switch and must be identical on -all peers and on every regeneration of a world. +The historical build selected `voxel.WormNoiseMode=1`: a deterministic scalar value-noise +evaluation using the same worm coordinate mapping, threshold, N1 short-circuit, N2 offset, and +strength contract. That selection was reverted after the ablation review. The current source is +exact-only Perlin mode 0; `voxel.WormNoiseMode` and `voxel.WormLatticeStep`, their storage, and +their switches no longer exist. The run names and measurements below are retained as historical +evidence only. The coarser lattice was measured rather than assumed. The first lattice implementation was much slower because of cache churn; after the fixed tile-window cache, it returned to roughly the exact cost but did not improve it. Representative static worker seconds were exact 33.10, lattice-2 initial 160.86, lattice-2 cache-fixed 33.78, lattice-4 cache-fixed 33.25, and exact -block-skip 33.10. The lattice path is retained as a bounded experiment, but is not the selected -production mode. The exact block skip is also retained as a fail-closed exact-mode diagnostic; +block-skip 33.10. The lattice path was a bounded historical experiment and is not retained. The +exact block skip is retained as a fail-closed exact-mode diagnostic; it is not used as a correctness or performance claim for value noise because its Perlin proof does not cover the changed field. @@ -1255,7 +1255,7 @@ The capability and geometry candidates were: | `WormCheapValueLattice2Explore_20260914` | value, lattice 2 | `C47567A2` | 75,132 | 20,857 / 10,917 | pass; no speed gain | | `WormCheapFastHashExplore_20260914` | value, lattice 0 | `6204C7E6` | 75,956 | 20,851 / 10,917 | selected | -The selected mode changes the field, but the measured full-game gain is small rather than a +The historical selected mode changed the field, but the measured full-game gain was small rather than a claim of a dramatic speedup. In two interleaved static pairs, exact averaged 0.087128 / 0.148415 s request-to-ready p50/p95, 0.063042 / 0.105080 s generation p50/p95, and 33.213871 worker s; cheap averaged 0.087761 / 0.145897 s, 0.062863 / 0.106258 s, and 33.083774 worker s. In two @@ -1438,3 +1438,230 @@ capability or comparison gate. | `WormFinalForceOffBuildSamplerOfflineRetry_20260914` | PASS; retried with writable `Saved\ZenData`/`Saved\DDC`, 733 modules, 10 loaded, zero identity mismatches, all 7,795 unique PCs mapped. | | `PerfSampledBuild_FinalFingerprint.log` | PASS; final cache-key hardening rebuild, Result: Succeeded. | | `WormFinalFingerprintSmoke_20260914` | PASS; post-hardening clean game smoke, exact staged runtime verified, writable DDC, zero crash folders/reporters. | +## Cheap-worm revert and TunnelNetwork ablation round — 2026-09-14 + +This section supersedes the historical cheap-worm selection above. The earlier value-noise +experiment changed the field without producing a gain: static movement was within noise and the +moving worker result was about 2.1% slower. The production/default path therefore keeps exact +Perlin. + +### Revert and exactness + +- voxel.WormNoiseMode and voxel.WormLatticeStep, their storage, and their switches are removed. +- VoxelWormField now has one evaluation path: exact Perlin, with the existing N1 short circuit. +- The exact block skip remains available as voxel.WormBlockSkip, default 0. The asset-owned + bEnableWorms and the measurement-only voxel.WormsForceOff remain. +- The generation fingerprint still hashes bEnableWorms and WormsForceOff, and now also hashes the + resolved TunnelNetwork ablation mask. +- The new ablation controls are development-only, resolve once per process, are world-changing, + and are never gameplay or multiplayer switches. A nonzero bit means force that stage off; zero + means all stages on. Shipping builds resolve the mask to zero. + +The prescribed UBT command from this file was used after synchronizing the staged host copy under +the plugin Saved directory. The final build log is +E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Saved\PerfSampledBuild_Ablation.log and reports +Result: Succeeded. The loaded runtime was +E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Saved\BuildHost\HostProject\Plugins\VoxelForge\Binaries\Win64\UnrealEditor-VoxelForge.dll +with SHA-256 A70528AC82641D7D610D8BA9497D0322B1EBDE22978B1E826A97FC4C0D038149. The editor module +SHA-256 was 8EA2A71BB29F828C5DE7559EC200AC4E86CF61F7DB5C8470406208073E22456D. + +The two default commandlets both used +E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Saved\DDC and +E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Saved\ZenData: + +- AblationDefaultExact_20260914Out: mask 0; status ok; canonical 128^3 export SHA-256 + B3E5F4C398DD0547C6C55F415872058246D4A292C2F0D636EE1CD0829DE9B377; geometry CRC 07C14005; + 82,273 vertices and 153,346 triangles. +- AblationDefaultCapability_20260914Out: mask 0; geometry CRC 311CA42C; 75,482 triangles; + player_fit_volume_cells 20,830; reachable_player_fit_cells 10,909; walk-only reachable + 10,909; Connected; deterministic JSON. + +The current game startup trace +E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Saved\AblationDefaultStaticTrace_20260914.startup.jsonl +was compared by tile key to +E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Saved\NextHeadStaticTrace_20260913.startup.jsonl: +841 versus 841 tile records, zero missing, zero extra, zero verdict/empty/triangle mismatches, +and identical all-tile and LOD0 triangle totals of 669,834 and 325,204. The current trace +finished with desired_set_satisfied_and_queue_drained and the loaded runtime hash above. + +### Ablation controls + +| bit | CVar | stage forced off | +|---:|---|---| +| 0x0001 | voxel.TunnelAblateCaveWarp | the three cave-warp Perlin calls; CaveWarpStrength resolves to zero | +| 0x0002 | voxel.TunnelAblateDetailOps | detail noise, roughness, and terrain operations | +| 0x0004 | voxel.TunnelAblateRoomSDF | room SDF and room-SDF joins in EvaluateSDFCached | +| 0x0008 | voxel.TunnelAblateTunnelSDF | tunnel SDF in EvaluateSDFCached | +| 0x0010 | voxel.TunnelAblateTunnelCore | EvaluateTunnelCoreWorld, support/core SDF work, and its GetDensityAt tail | +| 0x0020 | voxel.TunnelAblatePassageCarving | EvaluateModifierSDF and passage carving | +| 0x0040 | voxel.TunnelAblatePassageStructuralPosts | passage structural posts and legacy structural floor support | +| 0x0080 | voxel.TunnelAblateNativeFloor | native floor composition | +| 0x0100 | voxel.TunnelAblateDisturbances | ApplyDisturbances | +| 0x0200 | voxel.TunnelAblateOriginSpine | origin spine | +| 0x0400 | voxel.TunnelAblateBoundarySeal | boundary seal | +| 0x0800 | voxel.TunnelAblateLandingPosts | landing posts and landing-floor posts | +| 0x1000 | voxel.TunnelAblateXYEdgeSeal | XY edge seal | +| 0x2000 | voxel.TunnelAblatePitChimneySDF | pit and chimney SDF work | + +Each override is applied in the fused evaluator, operator-stack source/classifier paths, cached +SDF/core paths, and modifier/floor paths where that stage exists. The resolved mask is emitted in +the explore JSON and is part of the generation fingerprint. This is an ablation instrument, not a +candidate field. + +### Measurement method and noise + +All game runs were sampler-off, NullRHI, operator block on, fused evaluator on, outer classifier +off, and used a 15-second clean session. Static runs produced 343 LOD0 samples and 841 applied +tiles. Each static row has two off runs; the two all-on static baselines are shared and interleaved +across the two passes. The all-on static mean was 33.064458 worker seconds, generation p50 +0.064657 s, generation p95 0.105786 s. Baseline worker range was 0.27%; baseline generation-p95 +range was 1.62%. + +The table reports the mean of the two off runs, the saving against that all-on mean, and the +off-run worker range as a compact noise indicator. Positive saving means the off run used fewer +worker seconds. All rows completed 841 tiles with zero obsolete aborts. + +| component removed | runs | off worker s | saving s (%) | off generation p50 s | off generation p95 s | off worker range | +|---|---:|---:|---:|---:|---:|---:| +| tunnel SDF | 2 | 29.304 | 3.760 (11.37%) | 0.060813 | 0.084811 | 0.53% | +| cave warp | 2 | 29.654 | 3.411 (10.31%) | 0.056881 | 0.098260 | 2.03% | +| passage carving | 2 | 29.730 | 3.334 (10.08%) | 0.056336 | 0.096892 | 0.88% | +| landing posts | 2 | 29.800 | 3.264 (9.87%) | 0.056776 | 0.097884 | 1.33% | +| passage structural posts | 2 | 31.205 | 1.859 (5.62%) | 0.060143 | 0.099723 | 0.50% | +| worms | 2 | 32.265 | 0.799 (2.42%) | 0.064232 | 0.099511 | 0.83% | +| native floor | 2 | 32.347 | 0.718 (2.17%) | 0.063590 | 0.103897 | 0.45% | +| origin spine | 2 | 32.529 | 0.535 (1.62%) | 0.063174 | 0.102446 | 0.61% | +| room SDF | 2 | 32.542 | 0.522 (1.58%) | 0.064194 | 0.100999 | 0.90% | +| pit/chimney SDF | 2 | 32.689 | 0.375 (1.13%) | 0.064599 | 0.103472 | 0.26% | +| boundary seal | 2 | 32.814 | 0.251 (0.76%) | 0.064863 | 0.104105 | 1.21% | +| detail ops | 2 | 32.838 | 0.227 (0.69%) | 0.065330 | 0.100864 | 0.69% | +| XY edge seal | 2 | 32.901 | 0.164 (0.49%) | 0.065021 | 0.104411 | 1.30% | +| disturbances | 2 | 33.208 | -0.144 (-0.43%) | 0.065100 | 0.106342 | 0.53% | +| tunnel core | 2 | 95.468 | -62.404 (-188.73%) | 0.060597 | 0.098965 | 0.07% | + +The field was intentionally allowed to change. For example, tunnel-SDF off produced 643,388 +triangles, cave-warp off 655,726, passage-carving off 654,932, and tunnel-core off 655,654, +against the all-on 669,834 static triangles. Some low-cost rows retained the same mesh totals in +this window; they are still world-changing overrides and are not correctness candidates. + +The sum of positive standalone static savings is 19.218608 worker seconds, 58.125% of the +33.064458-second baseline, leaving 13.845850 seconds (41.875%) as shared/unattributed cost in +this ablation accounting. The signed sum over every row is -43.329105 seconds because tunnel-core +removal is a large regression caused by the changed field; the rows are not additive and this is +why the signed sum is not a speedup prediction. + +Moving confirmation covered the five largest positive static savers. Each row has two fresh +all-on baselines interleaved with two off runs. Movement began at 3 seconds and ran at 800 cm/s; +each run produced 882 LOD0 samples and 1,680 applied tiles, with zero obsolete aborts. + +| component removed | runs | all-on worker s | off worker s | saving s (%) | all-on gen p50/p95 s | off gen p50/p95 s | baseline/off worker range | +|---|---:|---:|---:|---:|---:|---:|---:| +| tunnel SDF | 2+2 | 72.218 | 63.847 | 8.371 (11.59%) | 0.063238 / 0.104331 | 0.060117 / 0.082540 | 0.71% / 0.87% | +| cave warp | 2+2 | 72.810 | 65.010 | 7.800 (10.71%) | 0.064610 / 0.105986 | 0.055398 / 0.094423 | 0.46% / 1.14% | +| landing posts | 2+2 | 72.556 | 64.763 | 7.794 (10.74%) | 0.063820 / 0.105220 | 0.055539 / 0.096228 | 1.13% / 2.36% | +| passage carving | 2+2 | 72.043 | 64.552 | 7.492 (10.40%) | 0.063507 / 0.104761 | 0.055276 / 0.096373 | 1.07% / 1.72% | +| passage structural posts | 2+2 | 72.417 | 68.093 | 4.324 (5.97%) | 0.063779 / 0.104611 | 0.059068 / 0.101321 | 0.14% / 0.83% | + +The moving standalone saving sum is 35.780708 worker seconds. Because these are five separate +ablations with separate baseline pairs, it is not a combined-speedup forecast; against the mean +72.409-second baseline per row it is 49.4%, leaving 36.628 seconds of shared/unattributed work. + +### Ranking and plausible cheaper forms + +1. Tunnel SDF — the largest measured real cost in both paths. A plausible exact optimization is + sharing one cached swept-tunnel result between EvaluateSDFCached, tunnel-core metadata, and + the final density tail, while preserving the existing evaluation order and float values. That + would be field-preserving. A lower-resolution or analytic approximation would be field-changing + and would need a fresh capability/export gate. +2. Cave warp — the three Perlin calls are expensive in the ablation. Exact reuse of the same + coordinates/results across the evaluator, cached graph, and op-stack paths is plausible and + field-preserving. Reducing frequency or replacing the noise is field-changing. +3. Passage carving — exact memoization/reuse of modifier SDF and passage candidate results across + EvaluateModifierSDF, ApplyPassageCarvingOnly, and the structural/floor queries is plausible and + field-preserving. Simplifying the carving geometry is field-changing. +4. Landing posts — exact per-tile reuse of landing/post/floor metadata is plausible and + field-preserving. Removing or thinning landing posts is field-changing. +5. Passage structural posts — exact reuse of structural-post SDF/metadata is plausible and + field-preserving. Removing or merging posts is field-changing. + +Worms is sixth at 2.42% static and is now kept exact because the cheap alternatives had no gain. +Native floor and origin spine follow at 2.17% and 1.62%. Room SDF, detail ops, seals, +disturbances, and pit/chimney work are not attractive next-round targets on these measurements. +Tunnel-core removal is explicitly not a candidate: it increased worker time by about 2.9x. +These ablations explain why the old 19.4% line-level worm figure and the proposed tunnel-core +line targets were unsafe attribution guides. + +### Complete launch ledger for this round + +All game and commandlet logs below are under +E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Saved. Every commandlet used the writable DDC path +E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Saved\DDC. No offline symbolizer was needed or +launched in this round. Every Unreal process that ran exited 0, verified the loaded runtime +SHA-256 A70528AC82641D7D610D8BA9497D0322B1EBDE22978B1E826A97FC4C0D038149, and had no crash or +retry; the two parser-only attempts below launched no Unreal process. Peak observed private +memory was approximately 1.54–1.60 GiB per process, with no runaway growth. + +| launch | outcome | +|---|---| +| initial prescribed staged build | UP-TO-DATE report; staged host was stale, so no measurement was taken | +| PerfSampledBuild_Ablation | PASS; prescribed UBT build, Result: Succeeded | +| AblationDefaultExact_20260914 | PASS; canonical export exact | +| AblationDefaultCapability_20260914 | PASS; 311CA42C, 75,482 triangles, 20,830 / 10,909 | +| static ablation batch launch 1 | FAILED before process start; PowerShell parser error from a missing DDC-argument quote | +| static ablation batch launch 2 | FAILED before process start; same quoting error; no Unreal process | +| AblationStaticBaseA_20260914 | PASS | +| AblationStaticBaseB_20260914 | PASS | +| AblationStaticCaveWarpA_20260914 | PASS | +| AblationStaticCaveWarpB_20260914 | PASS | +| AblationStaticDetailOpsA_20260914 | PASS | +| AblationStaticDetailOpsB_20260914 | PASS | +| AblationStaticRoomSDFA_20260914 | PASS | +| AblationStaticRoomSDFB_20260914 | PASS | +| AblationStaticTunnelSDFA_20260914 | PASS | +| AblationStaticTunnelSDFB_20260914 | PASS | +| AblationStaticTunnelCoreA_20260914 | PASS; 95.436091 worker seconds | +| AblationStaticTunnelCoreB_20260914 | PASS; 95.500636 worker seconds | +| AblationStaticPassageCarvingA_20260914 | PASS | +| AblationStaticPassageCarvingB_20260914 | PASS | +| AblationStaticPassageStructuralPostsA_20260914 | PASS | +| AblationStaticPassageStructuralPostsB_20260914 | PASS | +| AblationStaticNativeFloorA_20260914 | PASS | +| AblationStaticNativeFloorB_20260914 | PASS | +| AblationStaticDisturbancesA_20260914 | PASS | +| AblationStaticDisturbancesB_20260914 | PASS | +| AblationStaticOriginSpineA_20260914 | PASS | +| AblationStaticOriginSpineB_20260914 | PASS | +| AblationStaticBoundarySealA_20260914 | PASS | +| AblationStaticBoundarySealB_20260914 | PASS | +| AblationStaticLandingPostsA_20260914 | PASS | +| AblationStaticLandingPostsB_20260914 | PASS | +| AblationStaticXYEdgeSealA_20260914 | PASS | +| AblationStaticXYEdgeSealB_20260914 | PASS | +| AblationStaticPitChimneySDFA_20260914 | PASS | +| AblationStaticPitChimneySDFB_20260914 | PASS | +| AblationStaticWormsOffA_20260914 | PASS | +| AblationStaticWormsOffB_20260914 | PASS | +| AblationMovingTunnelSDFBaseA_20260914 | PASS | +| AblationMovingTunnelSDFA_20260914 | PASS | +| AblationMovingTunnelSDFBaseB_20260914 | PASS | +| AblationMovingTunnelSDFB_20260914 | PASS | +| AblationMovingCaveWarpBaseA_20260914 | PASS | +| AblationMovingCaveWarpA_20260914 | PASS | +| AblationMovingCaveWarpBaseB_20260914 | PASS | +| AblationMovingCaveWarpB_20260914 | PASS | +| AblationMovingPassageCarvingBaseA_20260914 | PASS | +| AblationMovingPassageCarvingA_20260914 | PASS | +| AblationMovingPassageCarvingBaseB_20260914 | PASS | +| AblationMovingPassageCarvingB_20260914 | PASS | +| AblationMovingLandingPostsBaseA_20260914 | PASS | +| AblationMovingLandingPostsA_20260914 | PASS | +| AblationMovingLandingPostsBaseB_20260914 | PASS | +| AblationMovingLandingPostsB_20260914 | PASS | +| AblationMovingPassageStructuralPostsBaseA_20260914 | PASS | +| AblationMovingPassageStructuralPostsA_20260914 | PASS | +| AblationMovingPassageStructuralPostsBaseB_20260914 | PASS | +| AblationMovingPassageStructuralPostsB_20260914 | PASS | +| AblationDefaultStaticTrace_20260914 | PASS; current 841-tile trace, exact keyed comparison | + +No .uasset was edited. Engine source and binaries were not modified. No commit, push, or stash was +performed. diff --git a/Source/VoxelForge/Private/VoxelCaveMorphology.cpp b/Source/VoxelForge/Private/VoxelCaveMorphology.cpp index d4c90e1..d692ce8 100644 --- a/Source/VoxelForge/Private/VoxelCaveMorphology.cpp +++ b/Source/VoxelForge/Private/VoxelCaveMorphology.cpp @@ -30,6 +30,7 @@ #include "VoxelCaveMorphology.h" #include "VoxelDensityProfile.h" #include "VoxelDensityPrimitives.h" +#include "VoxelDensityAblation.h" #include "VoxelStrateMeasure.h" #include "VoxelTypes.h" // Pour VOXEL_NOISE_SCALE, SmoothStep01 #include "VoxelStrateTypes.h" @@ -3166,6 +3167,10 @@ namespace const FStrateGenerationParams& Params, uint32 Seed) { + if (VoxelDensityAblation::IsCaveWarpOff()) + { + return WorldPoint; + } float EffectiveZ = WorldPoint.Z; if (Params.VerticalScale > 0.0f && Params.VerticalScale != 1.0f) { @@ -7286,7 +7291,9 @@ void VoxelCaveMorphology::BuildChunkCache( CR.RoomOp->ApplyTo(OpParams, CR.RoomOpWeight); // PITS — downward shafts anchored in the room's lower half. - BakeRoomFeature(CR, /*Max*/2, OpParams.PitDensity, + if (!VoxelDensityAblation::IsPitChimneySDFOff()) + { + BakeRoomFeature(CR, /*Max*/2, OpParams.PitDensity, 0xDE1A7Eu, 6271u, 0xABCDu, 0x5EEDu, /*XYScale*/0.6f, OpParams.PitMinRadius, OpParams.PitMaxRadius, [&](float PX, float PY, float PitRadius, uint32 PH3) @@ -7308,8 +7315,8 @@ void VoxelCaveMorphology::BuildChunkCache( OutCache.Pits.Add(Pit); }); - // CHIMNEYS — mirror of pits: upward tubes anchored in the room's upper half. - BakeRoomFeature(CR, /*Max*/2, OpParams.ChimneyDensity, + // CHIMNEYS — mirror of pits: upward tubes anchored in the room's upper half. + BakeRoomFeature(CR, /*Max*/2, OpParams.ChimneyDensity, 0xC4F007u, 7919u, 0x1337u, 0xCAFEu, /*XYScale*/0.6f, OpParams.ChimneyMinRadius, OpParams.ChimneyMaxRadius, [&](float CX, float CY, float ChmRadius, uint32 CH3) @@ -7330,6 +7337,7 @@ void VoxelCaveMorphology::BuildChunkCache( Chim.BoundXYRadiusSq = MaxXYR * MaxXYR; OutCache.Chimneys.Add(Chim); }); + } // COLUMNS — full-height solid cylinders (no Z anchor, no flare). BakeRoomFeature(CR, /*Max*/4, OpParams.ColumnDensity, @@ -7481,9 +7489,13 @@ float VoxelCaveMorphology::EvaluateSDFCached( MinSDF = VoxelSDF::SmoothMin(MinSDF, RoomSDF, BlendK); }; - const int32 RoomCandidateCount = VF_ForEachSpatialCandidate( - Cache.RoomSpatialIndex, Cache.Rooms.Num(), WorldX, WorldY, EvaluateRoom, - bUseSpatialIndex); + int32 RoomCandidateCount = 0; + if (!VoxelDensityAblation::IsRoomSDFOff()) + { + RoomCandidateCount = VF_ForEachSpatialCandidate( + Cache.RoomSpatialIndex, Cache.Rooms.Num(), WorldX, WorldY, EvaluateRoom, + bUseSpatialIndex); + } if (VoxelDensityProfile::AreCountersEnabled()) { VoxelDensityProfile::AddCounter( @@ -7512,9 +7524,12 @@ float VoxelCaveMorphology::EvaluateSDFCached( const float JoinSDF = FMath::Max(HorizontalSDF, VerticalSDF); MinSDF = VoxelSDF::SmoothMin(MinSDF, JoinSDF, BlendK); }; - VF_ForEachSpatialCandidate( - Cache.RoomFloorJoinSpatialIndex, Cache.RoomFloorJoins.Num(), - WorldX, WorldY, EvaluateJoin, bUseSpatialIndex); + if (!VoxelDensityAblation::IsRoomSDFOff()) + { + VF_ForEachSpatialCandidate( + Cache.RoomFloorJoinSpatialIndex, Cache.RoomFloorJoins.Num(), + WorldX, WorldY, EvaluateJoin, bUseSpatialIndex); + } //========================================================================= // Tunnel SDFs @@ -7553,9 +7568,13 @@ float VoxelCaveMorphology::EvaluateSDFCached( MinSDF = VoxelSDF::SmoothMin(MinSDF, TunnelSDF, BlendK); }; - const int32 TunnelCandidateCount = VF_ForEachSpatialCandidate( - Cache.TunnelSpatialIndex, Cache.Tunnels.Num(), WorldX, WorldY, EvaluateTunnel, - bUseSpatialIndex); + int32 TunnelCandidateCount = 0; + if (!VoxelDensityAblation::IsTunnelSDFOff()) + { + TunnelCandidateCount = VF_ForEachSpatialCandidate( + Cache.TunnelSpatialIndex, Cache.Tunnels.Num(), WorldX, WorldY, EvaluateTunnel, + bUseSpatialIndex); + } if (VoxelDensityProfile::AreCountersEnabled()) { VoxelDensityProfile::AddCounter( @@ -7577,6 +7596,10 @@ float VoxelCaveMorphology::EvaluateTunnelCoreSDF( const FChunkSDFCache& Cache, bool bUseSpatialIndex) { + if (VoxelDensityAblation::IsTunnelCoreOff()) + { + return FLT_MAX; + } const FVector Pos(WorldX, WorldY, WorldZ); float MinSDF = FLT_MAX; const FCachedTunnel* TunnelData = Cache.Tunnels.GetData(); @@ -7618,6 +7641,10 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld( const FTunnelSupportFloorColumn* SupportColumn, bool bUseSpatialIndex) { + if (VoxelDensityAblation::IsTunnelCoreOff()) + { + return FTunnelCoreWorldEvaluation(); + } VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::TunnelCoreWorld); const FVector Pos(WorldX, WorldY, WorldZ); @@ -7756,6 +7783,10 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint( const FChunkSDFCache& Cache, bool bUseSpatialIndex) { + if (VoxelDensityAblation::IsTunnelCoreOff()) + { + return false; + } const FVector Pos(WorldX, WorldY, WorldZ); const FCachedTunnel* TunnelData = Cache.Tunnels.GetData(); @@ -7875,6 +7906,11 @@ void VoxelCaveMorphology::BuildTunnelSupportFloorColumn( FTunnelSupportFloorColumn& OutColumn, bool bUseSpatialIndex) { + if (VoxelDensityAblation::IsTunnelCoreOff()) + { + OutColumn.Reset(); + return; + } VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::TunnelCoreSupport); OutColumn.Reset(); diff --git a/Source/VoxelForge/Private/VoxelDensityAblation.cpp b/Source/VoxelForge/Private/VoxelDensityAblation.cpp new file mode 100644 index 0000000..b00dadc --- /dev/null +++ b/Source/VoxelForge/Private/VoxelDensityAblation.cpp @@ -0,0 +1,131 @@ +// VoxelDensityAblation.cpp +// Development-only TunnelNetwork ablation controls. + +#include "VoxelDensityAblation.h" + +#include "HAL/IConsoleManager.h" +#include "Misc/CommandLine.h" +#include "Misc/Parse.h" + +#include + +namespace +{ + constexpr uint32 UnresolvedMask = 0xFFFFFFFFu; + std::atomic GResolvedMask(UnresolvedMask); + +#if !UE_BUILD_SHIPPING + int32 GCaveWarp = 0; + int32 GDetailOps = 0; + int32 GRoomSDF = 0; + int32 GTunnelSDF = 0; + int32 GTunnelCore = 0; + int32 GPassageCarving = 0; + int32 GPassageStructuralPosts = 0; + int32 GNativeFloor = 0; + int32 GDisturbances = 0; + int32 GOriginSpine = 0; + int32 GBoundarySeal = 0; + int32 GLandingPosts = 0; + int32 GXYEdgeSeal = 0; + int32 GPitChimneySDF = 0; + +#define VF_ABLATION_CVAR(Name, Storage) \ + FAutoConsoleVariableRef CVar##Storage( \ + Name, Storage, \ + TEXT("WORLD-CHANGING DEVELOPMENT-ONLY measurement override. Forces this " \ + "TunnelNetwork density stage off; never use as a gameplay switch. " \ + "All peers and regenerated worlds must use the same value. Resolved once " \ + "per process. 0=on, 1=off."), ECVF_Default) + + VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateCaveWarp"), GCaveWarp); + VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateDetailOps"), GDetailOps); + VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateRoomSDF"), GRoomSDF); + VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateTunnelSDF"), GTunnelSDF); + VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateTunnelCore"), GTunnelCore); + VF_ABLATION_CVAR(TEXT("voxel.TunnelAblatePassageCarving"), GPassageCarving); + VF_ABLATION_CVAR(TEXT("voxel.TunnelAblatePassageStructuralPosts"), GPassageStructuralPosts); + VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateNativeFloor"), GNativeFloor); + VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateDisturbances"), GDisturbances); + VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateOriginSpine"), GOriginSpine); + VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateBoundarySeal"), GBoundarySeal); + VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateLandingPosts"), GLandingPosts); + VF_ABLATION_CVAR(TEXT("voxel.TunnelAblateXYEdgeSeal"), GXYEdgeSeal); + VF_ABLATION_CVAR(TEXT("voxel.TunnelAblatePitChimneySDF"), GPitChimneySDF); + +#undef VF_ABLATION_CVAR + + uint32 ResolveValue(const TCHAR* Name, int32& Storage) + { + int32 Value = Storage; + FParse::Value(FCommandLine::Get(), Name, Value); + Storage = Value != 0 ? 1 : 0; + return Storage != 0 ? 1u : 0u; + } + + uint32 ResolveMask() + { + uint32 Mask = 0; + Mask |= ResolveValue(TEXT("voxel.TunnelAblateCaveWarp="), GCaveWarp) + * static_cast(VoxelDensityAblation::EStage::CaveWarp); + Mask |= ResolveValue(TEXT("voxel.TunnelAblateDetailOps="), GDetailOps) + * static_cast(VoxelDensityAblation::EStage::DetailOps); + Mask |= ResolveValue(TEXT("voxel.TunnelAblateRoomSDF="), GRoomSDF) + * static_cast(VoxelDensityAblation::EStage::RoomSDF); + Mask |= ResolveValue(TEXT("voxel.TunnelAblateTunnelSDF="), GTunnelSDF) + * static_cast(VoxelDensityAblation::EStage::TunnelSDF); + Mask |= ResolveValue(TEXT("voxel.TunnelAblateTunnelCore="), GTunnelCore) + * static_cast(VoxelDensityAblation::EStage::TunnelCore); + Mask |= ResolveValue(TEXT("voxel.TunnelAblatePassageCarving="), GPassageCarving) + * static_cast(VoxelDensityAblation::EStage::PassageCarving); + Mask |= ResolveValue( + TEXT("voxel.TunnelAblatePassageStructuralPosts="), GPassageStructuralPosts) + * static_cast(VoxelDensityAblation::EStage::PassageStructuralPosts); + Mask |= ResolveValue(TEXT("voxel.TunnelAblateNativeFloor="), GNativeFloor) + * static_cast(VoxelDensityAblation::EStage::NativeFloor); + Mask |= ResolveValue(TEXT("voxel.TunnelAblateDisturbances="), GDisturbances) + * static_cast(VoxelDensityAblation::EStage::Disturbances); + Mask |= ResolveValue(TEXT("voxel.TunnelAblateOriginSpine="), GOriginSpine) + * static_cast(VoxelDensityAblation::EStage::OriginSpine); + Mask |= ResolveValue(TEXT("voxel.TunnelAblateBoundarySeal="), GBoundarySeal) + * static_cast(VoxelDensityAblation::EStage::BoundarySeal); + Mask |= ResolveValue(TEXT("voxel.TunnelAblateLandingPosts="), GLandingPosts) + * static_cast(VoxelDensityAblation::EStage::LandingPosts); + Mask |= ResolveValue(TEXT("voxel.TunnelAblateXYEdgeSeal="), GXYEdgeSeal) + * static_cast(VoxelDensityAblation::EStage::XYEdgeSeal); + Mask |= ResolveValue(TEXT("voxel.TunnelAblatePitChimneySDF="), GPitChimneySDF) + * static_cast(VoxelDensityAblation::EStage::PitChimneySDF); + return Mask; + } +#endif +} + +namespace VoxelDensityAblation +{ + uint32 GetResolvedMask() + { + uint32 Existing = GResolvedMask.load(std::memory_order_acquire); + if (Existing != UnresolvedMask) + { + return Existing; + } + +#if UE_BUILD_SHIPPING + constexpr uint32 Resolved = 0; +#else + const uint32 Resolved = ResolveMask(); +#endif + uint32 Expected = UnresolvedMask; + if (!GResolvedMask.compare_exchange_strong( + Expected, Resolved, std::memory_order_release, std::memory_order_acquire)) + { + return Expected; + } + + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeTunnelAblation] resolved mask=0x%08x " + "(world-changing development measurement overrides; 0=all on)"), + Resolved); + return Resolved; + } +} diff --git a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp index 4b322eb..d4093a6 100644 --- a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp +++ b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp @@ -22,6 +22,7 @@ #include "VoxelStrateComposer.h" #include "VoxelDensityPrimitives.h" // VF_ApplyOriginSpine / seals / PassageCarving +#include "VoxelDensityAblation.h" // fingerprinted development-only stage measurements #include "VoxelCaveMorphology.h" // VoxelSDF::Capsule, VoxelHash #include "VoxelGenerator.h" // VoxelGenLOD::Eff #include "VoxelHeightOp.h" // FVoxelHeightStack — SurfaceWorld's two height stacks @@ -1933,6 +1934,10 @@ namespace EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override { + if (VoxelDensityAblation::IsOriginSpineOff()) + { + return EVoxelOpEffect::Identity; + } const bool bRoomTouches = Ctx.bUseLatticeProof ? VoxelPassageGeometry::OriginLandingRoomTouchesLattice( VoxelBox, Ctx.LatticeOriginVoxels, Ctx.Step, @@ -2018,6 +2023,10 @@ namespace */ EVoxelTileClass ClassifyBox(const FBox& VoxelBox, const FVoxelOpContext&) const override { + if (VoxelDensityAblation::IsBoundarySealOff()) + { + return EVoxelTileClass::Mixed; + } if (Thickness <= 0.0f || Base <= 0.0f) { return EVoxelTileClass::Mixed; } constexpr float SafetyMargin = 1.0f; @@ -2039,6 +2048,10 @@ namespace // Hors de sa bande, le seal ne fait rien du tout ; à cheval, il ne peut qu'ajouter du solide. EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext&) const override { + if (VoxelDensityAblation::IsBoundarySealOff()) + { + return EVoxelOpEffect::Identity; + } if (Thickness <= 0.0f) { return EVoxelOpEffect::Identity; } const bool bTouchesTopBand = ((float)VoxelBox.Max.Z >= TopZ - Thickness) && ((float)VoxelBox.Min.Z <= TopZ); const bool bTouchesBotBand = ((float)VoxelBox.Min.Z <= BotZ + Thickness) && ((float)VoxelBox.Max.Z >= BotZ); @@ -2100,6 +2113,10 @@ namespace */ EVoxelTileClass ClassifyBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override { + if (VoxelDensityAblation::IsXYEdgeSealOff()) + { + return EVoxelTileClass::Mixed; + } return VF_XYEdgeSealBoxIsForcedSolid( VoxelBox, Ctx.WorldRadiusVoxels, Ctx.EdgeSealThickness, Base) ? EVoxelTileClass::AllSolid : EVoxelTileClass::Mixed; @@ -2114,6 +2131,10 @@ namespace // In the inner world it is Identity; a box that reaches the radial band can only gain rock. EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override { + if (VoxelDensityAblation::IsXYEdgeSealOff()) + { + return EVoxelOpEffect::Identity; + } return VF_XYEdgeSealBoxTouchesBand( VoxelBox, Ctx.WorldRadiusVoxels, Ctx.EdgeSealThickness) ? EVoxelOpEffect::FillOnly : EVoxelOpEffect::Identity; @@ -2177,29 +2198,39 @@ namespace EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override { + if (VoxelDensityAblation::IsPassageCarvingOff() + && VoxelDensityAblation::IsLandingPostsOff() + && VoxelDensityAblation::IsPassageStructuralPostsOff() + && VoxelDensityAblation::IsOriginSpineOff()) + { + return EVoxelOpEffect::Identity; + } const UVoxelStrateManager* LiveManager = Manager.Get(); if (!LiveManager) { return EVoxelOpEffect::Identity; } - const bool bOriginFloor = Ctx.bUseLatticeProof - ? VoxelPassageGeometry::OriginLandingFloorTouchesLattice( - VoxelBox, Ctx.LatticeOriginVoxels, Ctx.Step, - TopZ, BotZ, Seal, SpineRadius) - : VoxelPassageGeometry::OriginLandingFloorTouchesBox( - VoxelBox, TopZ, BotZ, Seal, SpineRadius); - const bool bPassageFloor = Ctx.bUseLatticeProof - ? LiveManager->AnyPassageLandingFloorNearLattice( - VoxelBox, Ctx.LatticeOriginVoxels, Ctx.Step) - : LiveManager->AnyPassageLandingFloorNearBox( - VoxelBox.Min, VoxelBox.Max); + const bool bOriginFloor = !VoxelDensityAblation::IsLandingPostsOff() + && (Ctx.bUseLatticeProof + ? VoxelPassageGeometry::OriginLandingFloorTouchesLattice( + VoxelBox, Ctx.LatticeOriginVoxels, Ctx.Step, + TopZ, BotZ, Seal, SpineRadius) + : VoxelPassageGeometry::OriginLandingFloorTouchesBox( + VoxelBox, TopZ, BotZ, Seal, SpineRadius)); + const bool bPassageFloor = !VoxelDensityAblation::IsLandingPostsOff() + && (Ctx.bUseLatticeProof + ? LiveManager->AnyPassageLandingFloorNearLattice( + VoxelBox, Ctx.LatticeOriginVoxels, Ctx.Step) + : LiveManager->AnyPassageLandingFloorNearBox( + VoxelBox.Min, VoxelBox.Max)); if (bOriginFloor || bPassageFloor) { // The room carves air, while its support slab force-writes solid. Both // hypotheses must therefore be killed for a box touching that slab. return EVoxelOpEffect::Both; } - const bool bPassageCarve = Ctx.bUseLatticeProof - ? LiveManager->AnyPassageNearLattice( - VoxelBox, Ctx.LatticeOriginVoxels, Ctx.Step) - : LiveManager->AnyPassageNearBox(VoxelBox.Min, VoxelBox.Max); + const bool bPassageCarve = !VoxelDensityAblation::IsPassageCarvingOff() + && (Ctx.bUseLatticeProof + ? LiveManager->AnyPassageNearLattice( + VoxelBox, Ctx.LatticeOriginVoxels, Ctx.Step) + : LiveManager->AnyPassageNearBox(VoxelBox.Min, VoxelBox.Max)); return bPassageCarve ? EVoxelOpEffect::CarveOnly : EVoxelOpEffect::Identity; } @@ -2212,6 +2243,10 @@ namespace float MaxCarveOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx, const FVoxelBoxHypotheses& H) const override { + if (VoxelDensityAblation::IsPassageCarvingOff()) + { + return 0.0f; + } const UVoxelStrateManager* LiveManager = Manager.Get(); if (!LiveManager) { return 0.0f; } if (!Ctx.bUseLatticeProof) @@ -3917,6 +3952,10 @@ namespace bool TryGetLastTunnelCoreWorldEvaluation( FTunnelCoreWorldEvaluation& OutEvaluation) const override { + if (VoxelDensityAblation::IsTunnelCoreOff()) + { + return false; + } FState& S = State(); if (!S.bLastTunnelCoreWorldEvaluationValid || S.LastTunnelCoreEvaluationPosition != S.LastWorldPosition) @@ -4078,7 +4117,7 @@ namespace // un « frame » : sa portée est exactement UN opérateur, donc elle appartient à cet // opérateur. float WarpedX = WorldX, WarpedY = WorldY, WarpedZ = EffectiveZ; - if (P.CaveWarpStrength > 0.0f) + if (!VoxelDensityAblation::IsCaveWarpOff() && P.CaveWarpStrength > 0.0f) { const float WF = P.CaveWarpFrequency; const float WS = P.CaveWarpStrength; @@ -4308,68 +4347,77 @@ namespace // mais à des coordonnées NON warpées. Sous un modèle de frames il aurait fallu les sortir // du frame tout en gardant le canal — exprimable, mais tordu. Dans un opérateur unique la // difficulté disparaît : le warp est une variable locale, pas un contexte hérité. - auto EvaluatePit = [&](int32 PitIndex) + if (!VoxelDensityAblation::IsPitChimneySDFOff()) { - const FCachedPit& Pit = GetCache().Pits[PitIndex]; - const float DZ = WorldZ - Pit.TopZ; - if (DZ >= Pit.BlendK) { return; } - if (-DZ > Pit.Depth + Pit.BlendK) { return; } - - const float DX = WorldX - Pit.CenterX; - const float DY = WorldY - Pit.CenterY; - const float XYDistSq = DX * DX + DY * DY; - if (XYDistSq > Pit.BoundXYRadiusSq) { return; } - - float PitSDF; - if (DZ <= 0.0f) + auto EvaluatePit = [&](int32 PitIndex) { - const float DepthBelow = -DZ; - float FlareFactor = FMath::Clamp(1.0f - DepthBelow / Pit.FlareDist, 0.0f, 1.0f); - FlareFactor = FlareFactor * FlareFactor; - const float EffRadius = Pit.Radius + Pit.FlareExtra * FlareFactor; - PitSDF = FMath::Sqrt(XYDistSq) - EffRadius; - } - else - { - PitSDF = FMath::Sqrt(XYDistSq) - (Pit.Radius + Pit.FlareExtra); - } + const FCachedPit& Pit = GetCache().Pits[PitIndex]; + const float DZ = WorldZ - Pit.TopZ; + if (DZ >= Pit.BlendK) { return; } + if (-DZ > Pit.Depth + Pit.BlendK) { return; } - CaveSDF = VoxelSDF::SmoothMin(CaveSDF, PitSDF, Pit.BlendK); - }; - VF_ForEachChunkSDFSpatialCandidate( - GetCache().PitSpatialIndex, GetCache().Pits.Num(), - WorldX, WorldY, EvaluatePit, bUseSpatialIndex); + const float DX = WorldX - Pit.CenterX; + const float DY = WorldY - Pit.CenterY; + const float XYDistSq = DX * DX + DY * DY; + if (XYDistSq > Pit.BoundXYRadiusSq) { return; } - auto EvaluateChimney = [&](int32 ChimneyIndex) + float PitSDF; + if (DZ <= 0.0f) + { + const float DepthBelow = -DZ; + float FlareFactor = FMath::Clamp(1.0f - DepthBelow / Pit.FlareDist, 0.0f, 1.0f); + FlareFactor = FlareFactor * FlareFactor; + const float EffRadius = Pit.Radius + Pit.FlareExtra * FlareFactor; + PitSDF = FMath::Sqrt(XYDistSq) - EffRadius; + } + else + { + PitSDF = FMath::Sqrt(XYDistSq) - (Pit.Radius + Pit.FlareExtra); + } + + CaveSDF = VoxelSDF::SmoothMin(CaveSDF, PitSDF, Pit.BlendK); + }; + if (!VoxelDensityAblation::IsPitChimneySDFOff()) { - const FCachedChimney& Chim = GetCache().Chimneys[ChimneyIndex]; - const float DZ = WorldZ - Chim.BottomZ; - if (-DZ >= Chim.BlendK) { return; } - if (DZ > Chim.Height + Chim.BlendK) { return; } + VF_ForEachChunkSDFSpatialCandidate( + GetCache().PitSpatialIndex, GetCache().Pits.Num(), + WorldX, WorldY, EvaluatePit, bUseSpatialIndex); + } - const float DX = WorldX - Chim.CenterX; - const float DY = WorldY - Chim.CenterY; - const float XYDistSq = DX * DX + DY * DY; - if (XYDistSq > Chim.BoundXYRadiusSq) { return; } - - float ChmSDF; - if (DZ >= 0.0f) + auto EvaluateChimney = [&](int32 ChimneyIndex) { - float FlareFactor = FMath::Clamp(1.0f - DZ / Chim.FlareDist, 0.0f, 1.0f); - FlareFactor = FlareFactor * FlareFactor; - const float EffRadius = Chim.Radius + Chim.FlareExtra * FlareFactor; - ChmSDF = FMath::Sqrt(XYDistSq) - EffRadius; - } - else - { - ChmSDF = FMath::Sqrt(XYDistSq) - (Chim.Radius + Chim.FlareExtra); - } + const FCachedChimney& Chim = GetCache().Chimneys[ChimneyIndex]; + const float DZ = WorldZ - Chim.BottomZ; + if (-DZ >= Chim.BlendK) { return; } + if (DZ > Chim.Height + Chim.BlendK) { return; } - CaveSDF = VoxelSDF::SmoothMin(CaveSDF, ChmSDF, Chim.BlendK); - }; - VF_ForEachChunkSDFSpatialCandidate( - GetCache().ChimneySpatialIndex, GetCache().Chimneys.Num(), - WorldX, WorldY, EvaluateChimney, bUseSpatialIndex); + const float DX = WorldX - Chim.CenterX; + const float DY = WorldY - Chim.CenterY; + const float XYDistSq = DX * DX + DY * DY; + if (XYDistSq > Chim.BoundXYRadiusSq) { return; } + + float ChmSDF; + if (DZ >= 0.0f) + { + float FlareFactor = FMath::Clamp(1.0f - DZ / Chim.FlareDist, 0.0f, 1.0f); + FlareFactor = FlareFactor * FlareFactor; + const float EffRadius = Chim.Radius + Chim.FlareExtra * FlareFactor; + ChmSDF = FMath::Sqrt(XYDistSq) - EffRadius; + } + else + { + ChmSDF = FMath::Sqrt(XYDistSq) - (Chim.Radius + Chim.FlareExtra); + } + + CaveSDF = VoxelSDF::SmoothMin(CaveSDF, ChmSDF, Chim.BlendK); + }; + if (!VoxelDensityAblation::IsPitChimneySDFOff()) + { + VF_ForEachChunkSDFSpatialCandidate( + GetCache().ChimneySpatialIndex, GetCache().Chimneys.Num(), + WorldX, WorldY, EvaluateChimney, bUseSpatialIndex); + } + } InOut.Sdf = CaveSDF; @@ -4378,40 +4426,46 @@ namespace // build-time ownership now so the stack can preserve the authored support through its // remaining modifiers without a second floor-writing pass. TryGet... below reuses the // exact same cached result for the generator's post-disturbance hand-off. - const FTunnelSupportFloorColumn* SupportColumn = GetSupportColumn( - S.LastWorldPosition); - S.LastTunnelCoreWorldEvaluation = VoxelCaveMorphology::EvaluateTunnelCoreWorld( - WorldX, WorldY, WorldZ, GetCache(), SupportColumn, - bUseSpatialIndex); - S.LastTunnelCoreEvaluationPosition = S.LastWorldPosition; - S.bLastTunnelCoreWorldEvaluationValid = true; - if (S.LastTunnelCoreWorldEvaluation.bSupportFloor - || S.LastTunnelCoreWorldEvaluation.bRoomFloor) + if (!VoxelDensityAblation::IsTunnelCoreOff()) { - const float StructuralFloorMinimumDensity = - FMath::Max(P.BaseDensity * 2.0f, 1.0f); - if (VoxelMath::IsFinite(StructuralFloorMinimumDensity) - && StructuralFloorMinimumDensity > 0.0f) + const FTunnelSupportFloorColumn* SupportColumn = GetSupportColumn( + S.LastWorldPosition); + S.LastTunnelCoreWorldEvaluation = VoxelCaveMorphology::EvaluateTunnelCoreWorld( + WorldX, WorldY, WorldZ, GetCache(), SupportColumn, + bUseSpatialIndex); + S.LastTunnelCoreEvaluationPosition = S.LastWorldPosition; + S.bLastTunnelCoreWorldEvaluationValid = true; + if (S.LastTunnelCoreWorldEvaluation.bSupportFloor + || S.LastTunnelCoreWorldEvaluation.bRoomFloor) { - InOut.bProtectedStructuralFloor = true; - InOut.StructuralFloorMinimumDensity = StructuralFloorMinimumDensity; - if (VoxelDensityProfile::AreCountersEnabled()) + const float StructuralFloorMinimumDensity = + FMath::Max(P.BaseDensity * 2.0f, 1.0f); + if (VoxelMath::IsFinite(StructuralFloorMinimumDensity) + && StructuralFloorMinimumDensity > 0.0f) { - VoxelDensityProfile::AddCounter( - VoxelDensityProfile::ECounter::TunnelAuthoredFloorSamples); + InOut.bProtectedStructuralFloor = true; + InOut.StructuralFloorMinimumDensity = StructuralFloorMinimumDensity; + if (VoxelDensityProfile::AreCountersEnabled()) + { + VoxelDensityProfile::AddCounter( + VoxelDensityProfile::ECounter::TunnelAuthoredFloorSamples); + } } } + + // These are the per-sample hand-offs needed by an op-major block. The scalar path + // continues to use FState directly; publishing them here only makes the dependency + // explicit for a later modifier and does not alter Density/Sdf. + InOut.bHasTunnelCoreWorldEvaluation = true; + InOut.TunnelCoreWorldSDF = S.LastTunnelCoreWorldEvaluation.SDF; + InOut.bTunnelCoreSupportFloor = S.LastTunnelCoreWorldEvaluation.bSupportFloor; + InOut.bTunnelCoreRoomFloor = S.LastTunnelCoreWorldEvaluation.bRoomFloor; } - // These are the per-sample hand-offs needed by an op-major block. The scalar path - // continues to use FState directly; publishing them here only makes the dependency - // explicit for a later modifier and does not alter Density/Sdf. + // These cache hand-offs remain valid when tunnel core is off; detail operators still + // need the room graph and nearest-room ownership. InOut.RoomCache = &GetCache(); InOut.NearestRoomIndex = S.NearestRoom; - InOut.bHasTunnelCoreWorldEvaluation = true; - InOut.TunnelCoreWorldSDF = S.LastTunnelCoreWorldEvaluation.SDF; - InOut.bTunnelCoreSupportFloor = S.LastTunnelCoreWorldEvaluation.bSupportFloor; - InOut.bTunnelCoreRoomFloor = S.LastTunnelCoreWorldEvaluation.bRoomFloor; } //--------------------------------------------------------------------- @@ -5418,7 +5472,7 @@ namespace && P.VerticalScale != 1.0f) ? WorldZ / P.VerticalScale : WorldZ; FVector Query(WorldX, WorldY, EffectiveZ); - if (P.CaveWarpStrength > 0.0f) + if (!VoxelDensityAblation::IsCaveWarpOff() && P.CaveWarpStrength > 0.0f) { const float Frequency = P.CaveWarpFrequency; Query.X += VoxelNoise::Perlin3D( @@ -8543,9 +8597,6 @@ namespace FWormFieldSource(const FStrateGenerationParams& InP, int32 Seed) : P(InP) , SeedU((uint32)Seed) - , WormLatticeStep( - InP.WormStrength > 0.0f && InP.WormThreshold > 0.0f - ? VoxelWormField::GetLatticeStep() : 0) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; } @@ -8585,7 +8636,7 @@ namespace SeedU}; const float WormValue = VoxelWormField::Evaluate( WorldX, WorldY, WorldZ, P.WormThreshold, - WormParameters, WormLatticeStep); + WormParameters); if (WormValue < P.WormThreshold) { const float t = 1.0f - (WormValue / P.WormThreshold); @@ -8656,9 +8707,7 @@ namespace // explicit count cap keeps this check bounded. The network mask still uses the SDF // interval's worst case, so this never assumes a room value that the preceding source // did not prove. - if (VoxelWormField::GetNoiseMode() == 0 - && WormLatticeStep <= 1 - && Ctx.bUseLatticeProof && Ctx.bTightenWarpProof && Ctx.Step >= 1 + if (Ctx.bUseLatticeProof && Ctx.bTightenWarpProof && Ctx.Step >= 1 && VoxelMath::IsFinite(P.WormFrequency) && VoxelMath::IsFinite(P.WormHorizontalBias) && VoxelMath::IsFinite(P.WormThreshold) @@ -8778,7 +8827,6 @@ namespace private: FStrateGenerationParams P; uint32 SeedU; - int32 WormLatticeStep = 0; }; #if WITH_EDITOR @@ -9722,6 +9770,8 @@ namespace VoxelDensityOps // `Sdf < SDFBlendRadius·3` via VF_NearCaveSurface. Voir la note de l'étape B5 là-bas. // L'ORDRE EST CELUI DE L'ORIGINAL et il compte : chacun lit la densité que le précédent // a laissée (le biais de sol est un ajout indépendant appliqué à cette accumulation). + if (!VoxelDensityAblation::IsDetailOpsOff()) + { OutStack.Add(MakeUnique(P, Seed)); // 4b OutStack.Add(MakeUnique(P, Seed, RoomPtr)); // 4c — terrasses OutStack.Add(MakeUnique(P, RoomPtr)); // 4c — lignes de strates @@ -9734,6 +9784,7 @@ namespace VoxelDensityOps OutStack.Add(MakeUnique(P, RoomPtr)); // 4g — dômes OutStack.Add(MakeUnique(P, RoomPtr)); // 4h — pincement OutStack.Add(MakeUnique(P, RoomPtr)); // fin 4h — biais de sol + } if (P.WormStrength > 0.0f && P.WormThreshold > 0.0f) { OutStack.Add(MakeUnique(P, Seed)); diff --git a/Source/VoxelForge/Private/VoxelGenerator.cpp b/Source/VoxelForge/Private/VoxelGenerator.cpp index cae4a69..97c323c 100644 --- a/Source/VoxelForge/Private/VoxelGenerator.cpp +++ b/Source/VoxelForge/Private/VoxelGenerator.cpp @@ -14,6 +14,7 @@ #include "VoxelBiomeDefinition.h" #include "VoxelNoise.h" // T2.a: float, SIMD-batched gradient-noise core #include "VoxelWormField.h" +#include "VoxelDensityAblation.h" // fingerprinted development-only stage measurements #include "VoxelDensityPrimitives.h" // spine / seals / passage — shared with the operator stack #include "VoxelDensityOpStack.h" // OPSTACK Phase 1: the opt-in per-strate operator stack #include "VoxelHeightOp.h" // IVoxelBiomeField — the adapter below implements it @@ -985,6 +986,10 @@ static void ApplyDisturbances(float& MC, float X, float Y, float Z, const FStrateDisturbanceParams& D, uint32 Seed, bool bProtectVerticalShaftAir, bool bProtectAuthoredTunnelFloor) { + if (VoxelDensityAblation::IsDisturbancesOff()) + { + return; + } VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::ApplyDisturbances); const float InnerTop = D.StrateTopWorldZ - D.BoundarySealThickness; @@ -2592,7 +2597,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co // Le seul point d'entrée de la pile dans le chemin de production. Elle rend la convention // MC (négatif = solide) comme les fonctions d'archétype, donc les disturbances et la couche // de diff qui suivent ne voient aucune différence. - if (CP_UseOpStack) + if (!VoxelDensityAblation::IsTunnelCoreOff() && CP_UseOpStack) { const bool bCanUseFusedEvaluator = GVoxelForgeUseFusedEvaluator != 0 @@ -2751,7 +2756,8 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co PreDisturbanceTunnelCore); } } - if (!bHavePreDisturbanceTunnelCore && ActiveTunnelCoreCache->bValid) + if (!VoxelDensityAblation::IsTunnelCoreOff() + && !bHavePreDisturbanceTunnelCore && ActiveTunnelCoreCache->bValid) { const FTunnelSupportFloorColumn* SupportColumn = nullptr; FTunnelSupportFloorColumn EmptySupportColumn; @@ -2891,7 +2897,8 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co // bridge over a graph tunnel. Reassert the native cached tunnel core here, after every // solid floor writer but before the global XY seal. This cache is built once per chunk, // never once per voxel. - if (ActiveTunnelCoreCache->bValid || CP_UseOpStack) + if (!VoxelDensityAblation::IsTunnelCoreOff() + && (ActiveTunnelCoreCache->bValid || CP_UseOpStack)) { FTunnelCoreWorldEvaluation TunnelCore; bool bHaveTunnelCore = bHavePreDisturbanceTunnelCore; @@ -3181,7 +3188,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo float WarpedY = WorldY; float WarpedZ = EffectiveZ; - if (Params.CaveWarpStrength > 0.0f) + if (!VoxelDensityAblation::IsCaveWarpOff() && Params.CaveWarpStrength > 0.0f) { const float WF = Params.CaveWarpFrequency; const float WS = Params.CaveWarpStrength; @@ -3472,74 +3479,77 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo // transition as tunnel-to-room (no hard seam at PitTopZ) // - bNearCaveSurface becomes true inside the shaft — roughness clamp // prevents fill-back, so this is safe - auto EvaluatePit = [&](int32 PitIndex) + if (!VoxelDensityAblation::IsPitChimneySDFOff()) { - const FCachedPit& Pit = SDFCache.Pits[PitIndex]; - const float DZ = WorldZ - Pit.TopZ; // Negative = below anchor (shaft) - if (DZ >= Pit.BlendK) return; // Above even the blend fringe - if (-DZ > Pit.Depth + Pit.BlendK) return; - - const float DX = WorldX - Pit.CenterX; - const float DY = WorldY - Pit.CenterY; - const float XYDistSq = DX * DX + DY * DY; - if (XYDistSq > Pit.BoundXYRadiusSq) return; - - float PitSDF; - if (DZ <= 0.0f) + auto EvaluatePit = [&](int32 PitIndex) { - // Shaft: tapered cylinder with flared opening - float DepthBelow = -DZ; - float FlareFactor = FMath::Clamp(1.0f - DepthBelow / Pit.FlareDist, 0.0f, 1.0f); - FlareFactor = FlareFactor * FlareFactor; - float EffRadius = Pit.Radius + Pit.FlareExtra * FlareFactor; - PitSDF = FMath::Sqrt(XYDistSq) - EffRadius; - } - else + const FCachedPit& Pit = SDFCache.Pits[PitIndex]; + const float DZ = WorldZ - Pit.TopZ; // Negative = below anchor (shaft) + if (DZ >= Pit.BlendK) return; // Above even the blend fringe + if (-DZ > Pit.Depth + Pit.BlendK) return; + + const float DX = WorldX - Pit.CenterX; + const float DY = WorldY - Pit.CenterY; + const float XYDistSq = DX * DX + DY * DY; + if (XYDistSq > Pit.BoundXYRadiusSq) return; + + float PitSDF; + if (DZ <= 0.0f) + { + // Shaft: tapered cylinder with flared opening + float DepthBelow = -DZ; + float FlareFactor = FMath::Clamp(1.0f - DepthBelow / Pit.FlareDist, 0.0f, 1.0f); + FlareFactor = FlareFactor * FlareFactor; + float EffRadius = Pit.Radius + Pit.FlareExtra * FlareFactor; + PitSDF = FMath::Sqrt(XYDistSq) - EffRadius; + } + else + { + // Above anchor: only the blend fringe matters here. + // We still pass a SDF so SmoothMin can soften the rim from above. + // Treat this zone as a flat disc at TopZ (XY cylinder, no Z factor) + // so the blend fades horizontally into the room floor. + PitSDF = FMath::Sqrt(XYDistSq) - (Pit.Radius + Pit.FlareExtra); + } + + CaveSDF = VoxelSDF::SmoothMin(CaveSDF, PitSDF, Pit.BlendK); + }; + VF_ForEachChunkSDFSpatialCandidate( + SDFCache.PitSpatialIndex, SDFCache.Pits.Num(), WorldX, WorldY, EvaluatePit, + bUseSpatialIndex); + + auto EvaluateChimney = [&](int32 ChimneyIndex) { - // Above anchor: only the blend fringe matters here. - // We still pass a SDF so SmoothMin can soften the rim from above. - // Treat this zone as a flat disc at TopZ (XY cylinder, no Z factor) - // so the blend fades horizontally into the room floor. - PitSDF = FMath::Sqrt(XYDistSq) - (Pit.Radius + Pit.FlareExtra); - } + const FCachedChimney& Chim = SDFCache.Chimneys[ChimneyIndex]; + const float DZ = WorldZ - Chim.BottomZ; // Positive = above anchor (shaft) + if (-DZ >= Chim.BlendK) return; // Below even the blend fringe + if (DZ > Chim.Height + Chim.BlendK) return; - CaveSDF = VoxelSDF::SmoothMin(CaveSDF, PitSDF, Pit.BlendK); - }; - VF_ForEachChunkSDFSpatialCandidate( - SDFCache.PitSpatialIndex, SDFCache.Pits.Num(), WorldX, WorldY, EvaluatePit, - bUseSpatialIndex); + const float DX = WorldX - Chim.CenterX; + const float DY = WorldY - Chim.CenterY; + const float XYDistSq = DX * DX + DY * DY; + if (XYDistSq > Chim.BoundXYRadiusSq) return; - auto EvaluateChimney = [&](int32 ChimneyIndex) - { - const FCachedChimney& Chim = SDFCache.Chimneys[ChimneyIndex]; - const float DZ = WorldZ - Chim.BottomZ; // Positive = above anchor (shaft) - if (-DZ >= Chim.BlendK) return; // Below even the blend fringe - if (DZ > Chim.Height + Chim.BlendK) return; + float ChmSDF; + if (DZ >= 0.0f) + { + float FlareFactor = FMath::Clamp(1.0f - DZ / Chim.FlareDist, 0.0f, 1.0f); + FlareFactor = FlareFactor * FlareFactor; + float EffRadius = Chim.Radius + Chim.FlareExtra * FlareFactor; + ChmSDF = FMath::Sqrt(XYDistSq) - EffRadius; + } + else + { + // Below anchor: flat disc blend into room ceiling + ChmSDF = FMath::Sqrt(XYDistSq) - (Chim.Radius + Chim.FlareExtra); + } - const float DX = WorldX - Chim.CenterX; - const float DY = WorldY - Chim.CenterY; - const float XYDistSq = DX * DX + DY * DY; - if (XYDistSq > Chim.BoundXYRadiusSq) return; - - float ChmSDF; - if (DZ >= 0.0f) - { - float FlareFactor = FMath::Clamp(1.0f - DZ / Chim.FlareDist, 0.0f, 1.0f); - FlareFactor = FlareFactor * FlareFactor; - float EffRadius = Chim.Radius + Chim.FlareExtra * FlareFactor; - ChmSDF = FMath::Sqrt(XYDistSq) - EffRadius; - } - else - { - // Below anchor: flat disc blend into room ceiling - ChmSDF = FMath::Sqrt(XYDistSq) - (Chim.Radius + Chim.FlareExtra); - } - - CaveSDF = VoxelSDF::SmoothMin(CaveSDF, ChmSDF, Chim.BlendK); - }; - VF_ForEachChunkSDFSpatialCandidate( - SDFCache.ChimneySpatialIndex, SDFCache.Chimneys.Num(), - WorldX, WorldY, EvaluateChimney, bUseSpatialIndex); + CaveSDF = VoxelSDF::SmoothMin(CaveSDF, ChmSDF, Chim.BlendK); + }; + VF_ForEachChunkSDFSpatialCandidate( + SDFCache.ChimneySpatialIndex, SDFCache.Chimneys.Num(), + WorldX, WorldY, EvaluateChimney, bUseSpatialIndex); + } // Convert SDF to density carving: // CaveSDF < 0 means we're inside a room/tunnel/pit → carve to air @@ -3563,6 +3573,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo // post-disturbance tail can consume the result without allocating an FVoxelOpSample or // invoking the interpreted stack a second time for this sample. if (OutTunnelCore != nullptr + && !VoxelDensityAblation::IsTunnelCoreOff() && Params.RoomDensity > 0.0f && Params.RoomSpacing > 0.0f) { // The no-column form is the exact scalar reference path: it performs the world-bound and @@ -3585,10 +3596,11 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo // seal / modifiers, so we jump to Step 5 instead of returning early. const float DetailThreshold = Params.SDFBlendRadius * 3.0f; const bool bNearCaveSurface = (CaveSDF < DetailThreshold) && (CaveSDF < FLT_MAX); + const bool bRunDetail = !VoxelDensityAblation::IsDetailOpsOff() && bNearCaveSurface; if (bCollectFusedDiagnostics && VoxelDensityProfile::AreCountersEnabled()) { VoxelDensityProfile::AddCounter( - bNearCaveSurface + bRunDetail ? VoxelDensityProfile::ECounter::FusedTunnelDetailSamples : VoxelDensityProfile::ECounter::FusedTunnelDetailSkipped); } @@ -3606,7 +3618,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo // The noise type (fBM, Ridged, Mixed) and optional domain warping are // per-strate settings, so each strate can have fundamentally different // wall character — smooth lava tubes vs craggy erosion cliffs. - if (bNearCaveSurface && Params.SurfaceRoughness > 0.0f) + if (bRunDetail && Params.SurfaceRoughness > 0.0f) { float RoughnessDepth = Params.SurfaceRoughness * 2.0f; float DistFromSurface = FMath::Abs(CaveSDF); @@ -3729,7 +3741,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo //========================================================================= // All terrain ops only affect density near cave walls. // Deep-solid voxels skip this entire block (bNearCaveSurface = false). - if (bNearCaveSurface) + if (bRunDetail) { //========================================================================= // PER-ROOM TERRAIN PARAMS @@ -4276,10 +4288,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo } bool bWormBlockProofComputed = false; - const int32 WormLatticeStep = VoxelWormField::GetLatticeStep(); - const bool bSkipWormBlock = VoxelWormField::GetNoiseMode() == 0 - && WormLatticeStep <= 1 - && GVoxelForgeWormBlockSkip != 0 + const bool bSkipWormBlock = GVoxelForgeWormBlockSkip != 0 && VF_TryGetWormBlockSkip( DensityCacheOwnerId, SeedU, ParamsFingerprint, LayoutVersion, WorldX, WorldY, WorldZ, Params, bWormBlockProofComputed); @@ -4305,7 +4314,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo SeedU}; const float WormValue = VoxelWormField::Evaluate( WorldX, WorldY, WorldZ, Params.WormThreshold, - WormParameters, WormLatticeStep); + WormParameters); if (WormValue < Params.WormThreshold) { @@ -4370,8 +4379,10 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo // the graph's own tunnel floor/core are structural route geometry. Direct callers of // GetDensityWithParams retain this historical tail; GetDensityAt skips it and applies the // same predicates once in its common MC-space post section below both generation paths. - const bool bTunnelSupportFloor = VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint( - WorldX, WorldY, WorldZ, SDFCache, bUseSpatialIndex); + const bool bTunnelCoreEnabled = !VoxelDensityAblation::IsTunnelCoreOff(); + const bool bTunnelSupportFloor = bTunnelCoreEnabled + && VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint( + WorldX, WorldY, WorldZ, SDFCache, bUseSpatialIndex); // Keep the direct legacy entry point capability-safe for callers outside the canonical // operator-stack path; its support band follows the same relieved swept floor. if (bTunnelSupportFloor) @@ -4388,7 +4399,8 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo } Density = FMath::Max(Density, StructuralSolidDensity); } - const float TunnelCoreSDF = (Params.RoomDensity > 0.0f && Params.RoomSpacing > 0.0f) + const float TunnelCoreSDF = bTunnelCoreEnabled + && (Params.RoomDensity > 0.0f && Params.RoomSpacing > 0.0f) ? VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF( WorldX, WorldY, WorldZ, SDFCache, bUseSpatialIndex) : FLT_MAX; diff --git a/Source/VoxelForge/Private/VoxelStrateManager.cpp b/Source/VoxelForge/Private/VoxelStrateManager.cpp index 498ee91..21727ec 100644 --- a/Source/VoxelForge/Private/VoxelStrateManager.cpp +++ b/Source/VoxelForge/Private/VoxelStrateManager.cpp @@ -8,11 +8,11 @@ #include "VoxelTypes.h" // For CHUNK_SIZE, VOXEL_SIZE, WorldToChunkCoord #include "VoxelCaveMorphology.h" // For VoxelSDF and VoxelHash #include "VoxelDensityPrimitives.h" // Shared passage carve polarity/strength +#include "VoxelDensityAblation.h" // Fingerprinted development-only stage overrides #include "VoxelDensityProfile.h" // Opt-in targeted per-voxel attribution #include "VoxelStartupTrace.h" #include "VoxelTerrainOpDefinition.h" // For UVoxelTerrainOpDefinition::ApplyTo #include "VoxelBiomeDefinition.h" // For UVoxelBiomeDefinition (biome context flatten) -#include "VoxelWormField.h" // World-changing worm evaluator mode/lattice identity #include "HAL/IConsoleManager.h" #include "Misc/CommandLine.h" #include "Misc/Crc.h" @@ -2487,6 +2487,11 @@ void UVoxelStrateManager::GeneratePassages() float UVoxelStrateManager::EvaluateModifierSDF(float WorldX, float WorldY, float WorldZ) const { + if (VoxelDensityAblation::IsPassageCarvingOff()) + { + return FLT_MAX; + } + const FIntVector ChunkCoord( FMath::FloorToInt(WorldX / (float)CHUNK_SIZE), FMath::FloorToInt(WorldY / (float)CHUNK_SIZE), @@ -2552,7 +2557,7 @@ float UVoxelStrateManager::EvaluateModifierSDF(float WorldX, float WorldY, float // projecting once here selects the nearest immutable floor segment for this sample. // Unknown/malformed descriptors deliberately retain the bare capsule and the legacy // diagnostic backstop remains available. - if (P.bNativeFloorEnabled) + if (P.bNativeFloorEnabled && !VoxelDensityAblation::IsNativeFloorOff()) { float NativeFloorZ = 0.0f; float NativeSupportRadius = 0.0f; @@ -2593,8 +2598,11 @@ void UVoxelStrateManager::ApplyPassageModifier( { VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::PassageModifier); - const float ModSDF = EvaluateModifierSDF(WorldX, WorldY, WorldZ); - VF_ApplyPassageCarving(Density, ModSDF, BaseDensity, SealThickness); + if (!VoxelDensityAblation::IsPassageCarvingOff()) + { + const float ModSDF = EvaluateModifierSDF(WorldX, WorldY, WorldZ); + VF_ApplyPassageCarving(Density, ModSDF, BaseDensity, SealThickness); + } ApplyPassageLandingAir(Density, WorldX, WorldY, WorldZ, BaseDensity, SealThickness); const FIntVector ChunkCoord( @@ -2607,15 +2615,15 @@ void UVoxelStrateManager::ApplyPassageModifier( for (const int32 PassageIndex : Nearby) { const FVoxelPassage& Passage = PassageData[PassageIndex]; - if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker() + const bool bLandingFloor = !VoxelDensityAblation::IsLandingPostsOff() && (VF_IsPassageLandingFloor(Position, Passage.UpperLanding) - || VF_IsPassageLandingFloor(Position, Passage.LowerLanding) - // A valid native profile owns its D-floor in the shape and in the final MC - // composition. Keep the old support slab only as a fail-closed fallback for a - // malformed/non-native descriptor; it must not patch a floor the shape already owns. - || (bPassageSupportFloorWritesEnabled - && !Passage.bNativeFloorEnabled - && VF_IsWalkableTunnelFloor(Passage, Position, this, PassageIndex)))) + || VF_IsPassageLandingFloor(Position, Passage.LowerLanding)); + const bool bLegacySupportFloor = !VoxelDensityAblation::IsPassageStructuralPostsOff() + && bPassageSupportFloorWritesEnabled + && !Passage.bNativeFloorEnabled + && VF_IsWalkableTunnelFloor(Passage, Position, this, PassageIndex); + if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker() + && (bLandingFloor || bLegacySupportFloor)) { // This is the one bidirectional part of PassageCarveOp: a floor is a proved solid // support slab. It is deliberately applied after the air carve so a tube can never @@ -2636,6 +2644,10 @@ void UVoxelStrateManager::ApplyPassageCarvingOnly( float& Density, float WorldX, float WorldY, float WorldZ, float BaseDensity, float SealThickness) const { + if (VoxelDensityAblation::IsPassageCarvingOff()) + { + return; + } VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::PassageModifier); const float ModSDF = EvaluateModifierSDF(WorldX, WorldY, WorldZ); @@ -2646,6 +2658,10 @@ void UVoxelStrateManager::ApplyPassageLandingAir( float& Density, float WorldX, float WorldY, float WorldZ, float BaseDensity, float SealThickness) const { + if (VoxelDensityAblation::IsLandingPostsOff()) + { + return; + } VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::PassageLandingAir); const FIntVector ChunkCoord( @@ -2710,6 +2726,10 @@ void UVoxelStrateManager::ApplyPassageTunnelAir( float& Density, float WorldX, float WorldY, float WorldZ, float BaseDensity, float SealThickness) const { + if (VoxelDensityAblation::IsPassageStructuralPostsOff()) + { + return; + } VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::PassageTunnelAir); const FVector Position(WorldX, WorldY, WorldZ); @@ -2740,6 +2760,11 @@ void UVoxelStrateManager::ApplyPassageTunnelAirMC( void UVoxelStrateManager::ApplyPassageLandingFloorMC( float& Density, float WorldX, float WorldY, float WorldZ, float BaseDensity) const { + if (VoxelDensityAblation::IsLandingPostsOff() + && VoxelDensityAblation::IsPassageStructuralPostsOff()) + { + return; + } VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::PassageLandingFloor); const FIntVector ChunkCoord( @@ -2752,12 +2777,15 @@ void UVoxelStrateManager::ApplyPassageLandingFloorMC( for (const int32 PassageIndex : Nearby) { const FVoxelPassage& Passage = PassageData[PassageIndex]; - if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker() + const bool bLandingFloor = !VoxelDensityAblation::IsLandingPostsOff() && (VF_IsPassageLandingFloor(Position, Passage.UpperLanding) - || VF_IsPassageLandingFloor(Position, Passage.LowerLanding) - || (bPassageSupportFloorWritesEnabled - && !Passage.bNativeFloorEnabled - && VF_IsWalkableTunnelFloor(Passage, Position, this, PassageIndex)))) + || VF_IsPassageLandingFloor(Position, Passage.LowerLanding)); + const bool bLegacySupportFloor = !VoxelDensityAblation::IsPassageStructuralPostsOff() + && bPassageSupportFloorWritesEnabled + && !Passage.bNativeFloorEnabled + && VF_IsWalkableTunnelFloor(Passage, Position, this, PassageIndex); + if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker() + && (bLandingFloor || bLegacySupportFloor)) { // Result is in MC convention here (negative = solid). This reassertion is the // structural floor backstop after the optional MC-space disturbance layer. @@ -2790,6 +2818,13 @@ void UVoxelStrateManager::ApplyPassageStructuralPostsMC( float BaseDensity, float SealThickness, bool bProtectAuthoredTunnelFloor) const { + const bool bDisableStructuralPosts = VoxelDensityAblation::IsPassageStructuralPostsOff(); + const bool bDisableLandingPosts = VoxelDensityAblation::IsLandingPostsOff(); + if (bDisableStructuralPosts && bDisableLandingPosts) + { + return; + } + VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::PassageStructuralPosts); @@ -2804,7 +2839,7 @@ void UVoxelStrateManager::ApplyPassageStructuralPostsMC( const FVector Position(WorldX, WorldY, WorldZ); const bool bSuppressFloor = VoxelPassageGeometry::VerticalShaftConnectorAirMarker(); - bool bAnyPassageFloor = false; + bool bAnyLandingFloor = false; bool bLegacyTunnelSupportFloor = false; bool bAnyRoomFloor = false; bool bWalkableAir = false; @@ -2819,36 +2854,37 @@ void UVoxelStrateManager::ApplyPassageStructuralPostsMC( const FVoxelPassage& Passage = PassageData[PassageIndex]; const bool bTunnelSupportFloor = - bPassageSupportFloorWritesEnabled + !bDisableStructuralPosts + && bPassageSupportFloorWritesEnabled && !Passage.bNativeFloorEnabled && VF_IsWalkableTunnelFloor(Passage, Position, this, PassageIndex); - if (!bSuppressFloor + if (!bSuppressFloor && !bDisableLandingPosts && (VF_IsPassageLandingFloor(Position, Passage.UpperLanding) - || VF_IsPassageLandingFloor(Position, Passage.LowerLanding) - || bTunnelSupportFloor)) + || VF_IsPassageLandingFloor(Position, Passage.LowerLanding))) { - bAnyPassageFloor = true; + bAnyLandingFloor = true; } - if (!bSuppressFloor && bTunnelSupportFloor) + if (!bSuppressFloor && !bDisableStructuralPosts && bTunnelSupportFloor) { bLegacyTunnelSupportFloor = true; } - if (!bSuppressFloor + if (!bSuppressFloor && !bDisableLandingPosts && (VF_IsPassageRoomFloor(Position, Passage.UpperLanding) || VF_IsPassageRoomFloor(Position, Passage.LowerLanding))) { bAnyRoomFloor = true; } - if (!bWalkableAir + if (!bDisableStructuralPosts && !bWalkableAir && FVector::DistSquared(Position, Passage.BoundCenter) <= Passage.BoundRadiusSq && VF_IsWalkableTunnelAir(Passage, Position, this, PassageIndex)) { bWalkableAir = true; } - if (FVector::DistSquared(Position, Passage.BoundCenter) <= Passage.BoundRadiusSq) + if (!bDisableLandingPosts + && FVector::DistSquared(Position, Passage.BoundCenter) <= Passage.BoundRadiusSq) { const FVoxelPassageLanding* Landings[] = { &Passage.UpperLanding, &Passage.LowerLanding }; @@ -2865,8 +2901,8 @@ void UVoxelStrateManager::ApplyPassageStructuralPostsMC( // budget. A neighbouring inter-strate landing must not reopen that finite support band; its // carve remains valid everywhere else. Legacy callers leave this false and retain the old // passage-owned behavior. - if (!bProtectAuthoredTunnelFloor - && !bAnyPassageFloor && MinLandingSDF < FLT_MAX) + if (!bDisableLandingPosts && !bProtectAuthoredTunnelFloor + && !bAnyLandingFloor && !bLegacyTunnelSupportFloor && MinLandingSDF < FLT_MAX) { float InternalDensity = -Density; VF_ApplyPassageLandingCarving( @@ -2874,7 +2910,7 @@ void UVoxelStrateManager::ApplyPassageStructuralPostsMC( Density = -InternalDensity; } - if (bAnyPassageFloor) + if (bAnyLandingFloor || bLegacyTunnelSupportFloor) { Density = FMath::Min(Density, -BaseDensity); } @@ -2901,6 +2937,10 @@ void UVoxelStrateManager::ApplyPassageNativeFloorMC( float& Density, float WorldX, float WorldY, float WorldZ, float BaseDensity) const { + if (VoxelDensityAblation::IsNativeFloorOff()) + { + return; + } const FIntVector ChunkCoord( FMath::FloorToInt(WorldX / (float)CHUNK_SIZE), FMath::FloorToInt(WorldY / (float)CHUNK_SIZE), @@ -2955,6 +2995,10 @@ void UVoxelStrateManager::ApplyPassageLandingRoomFloorMC( float& Density, float WorldX, float WorldY, float WorldZ, float BaseDensity) const { + if (VoxelDensityAblation::IsLandingPostsOff()) + { + return; + } VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::PassageLandingRoomFloor); const FIntVector ChunkCoord( @@ -2979,6 +3023,10 @@ void UVoxelStrateManager::ApplyPassageLandingRoomFloorMC( bool UVoxelStrateManager::AnyPassageNearBox(const FVector& MinVoxel, const FVector& MaxVoxel) const { + if (VoxelDensityAblation::IsPassageCarvingOff()) + { + return false; + } // Le carve d'un passage atteint ModSDF < PASSAGE_BLEND_RADIUS (4, VoxelGenerator.cpp) au-delà de // sa surface ; BoundRadius inclut déjà rayon + blend, on re-pad par sécurité (conservatif). constexpr float CarvePad = 4.0f; @@ -5530,7 +5578,12 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto { Result.WormStrength = 0.0f; } - return VF_ApplyRuntimeRoughnessOverrides(Result); + Result = VF_ApplyRuntimeRoughnessOverrides(Result); + if (VoxelDensityAblation::IsCaveWarpOff()) + { + Result.CaveWarpStrength = 0.0f; + } + return Result; }; #if WITH_EDITOR @@ -5885,11 +5938,9 @@ uint64 UVoxelStrateManager::GetGenerationParamsFingerprint() const HashValue(Slot.Definition->bUseOperatorStack); HashValue(Slot.Definition->bEnableWorms); HashValue(VF_WormsForceOff()); - // The selected evaluator is a world input even though it is deliberately not part of - // FStrateGenerationParams (the fixed 83-field memo key). Keep persistent/verdict keys - // from crossing an explicit WormNoiseMode/WormLatticeStep experiment. - HashValue(VoxelWormField::GetNoiseMode()); - HashValue(VoxelWormField::GetLatticeStep()); + // Development-only stage ablations change the field and therefore belong in every + // persistent/verdict key even though they are not authored generation parameters. + HashValue(VoxelDensityAblation::GetResolvedMask()); HashValue(Slot.Definition->StrateHeightInChunks); HashValue(Slot.Definition->TransitionType); HashValue(Slot.Definition->TransitionBlendChunks); diff --git a/Source/VoxelForge/Private/VoxelWormField.cpp b/Source/VoxelForge/Private/VoxelWormField.cpp index 1fbfb99..dd7d793 100644 --- a/Source/VoxelForge/Private/VoxelWormField.cpp +++ b/Source/VoxelForge/Private/VoxelWormField.cpp @@ -1,98 +1,14 @@ // VoxelWormField.cpp -// Shared deterministic worm-field evaluation and the measured lattice experiment. +// Shared deterministic exact worm-field evaluation. #include "VoxelWormField.h" #include "VoxelCaveMorphology.h" // VoxelHash::SeedOffset -#include "VoxelGenerator.h" // per-worker canonical tile window #include "VoxelNoise.h" #include "VoxelTypes.h" // VOXEL_NOISE_SCALE -#include "HAL/IConsoleManager.h" -#include "Misc/CommandLine.h" -#include "Misc/Parse.h" - -#include -#include -#include - namespace { - // The selected production mode is resolved once, before a generation stack is allowed to - // observe it, so all workers in a process use one world definition. Mode 0 remains the exact - // legacy control for measurements and compatibility investigations. - int32 GVoxelForgeWormLatticeStep = 0; - std::atomic GResolvedWormLatticeStep(-1); - int32 GVoxelForgeWormNoiseMode = 1; - std::atomic GResolvedWormNoiseMode(-1); - - // The value field has the same threshold/strength contract as legacy Perlin. This small - // amplitude retune keeps the authored TunnelNetwork player-fit margin after replacing the - // gradient evaluation with the cheaper scalar hash. It is a code default, not a CVar, so a - // client cannot silently choose a different world. - constexpr float CheapWormNoiseAmplitude = 1.0f; - - FAutoConsoleVariableRef CVarVoxelForgeWormLatticeStep( - TEXT("voxel.WormLatticeStep"), - GVoxelForgeWormLatticeStep, - TEXT("WORLD-CHANGING development experiment. 0=exact, 2/4=coarse worm lattice with " - "trilinear interpolation. All multiplayer peers and regenerated worlds must use " - "the same value. Default 0 until measured."), - ECVF_Default); - - FAutoConsoleVariableRef CVarVoxelForgeWormNoiseMode( - TEXT("voxel.WormNoiseMode"), - GVoxelForgeWormNoiseMode, - TEXT("WORLD-CHANGING development experiment. 0=legacy gradient Perlin, 1=cheap " - "deterministic value-noise worm fields. All multiplayer peers and regenerated " - "worlds must use the same value. Default 1 (the measured production mode)."), - ECVF_Default); - - int32 ResolveLatticeStep() - { - int32 Existing = GResolvedWormLatticeStep.load(std::memory_order_acquire); - if (Existing >= 0) - { - return Existing; - } - - int32 CommandLineValue = GVoxelForgeWormLatticeStep; - FParse::Value( - FCommandLine::Get(), TEXT("voxel.WormLatticeStep="), CommandLineValue); - const int32 Resolved = CommandLineValue == 2 || CommandLineValue == 4 - ? CommandLineValue : 0; - int32 Expected = -1; - if (!GResolvedWormLatticeStep.compare_exchange_strong( - Expected, Resolved, std::memory_order_release, std::memory_order_acquire)) - { - return Expected; - } - GVoxelForgeWormLatticeStep = Resolved; - return Resolved; - } - - int32 ResolveNoiseMode() - { - int32 Existing = GResolvedWormNoiseMode.load(std::memory_order_acquire); - if (Existing >= 0) - { - return Existing; - } - - int32 CommandLineValue = GVoxelForgeWormNoiseMode; - FParse::Value( - FCommandLine::Get(), TEXT("voxel.WormNoiseMode="), CommandLineValue); - const int32 Resolved = CommandLineValue == 1 ? 1 : 0; - int32 Expected = -1; - if (!GResolvedWormNoiseMode.compare_exchange_strong( - Expected, Resolved, std::memory_order_release, std::memory_order_acquire)) - { - return Expected; - } - GVoxelForgeWormNoiseMode = Resolved; - return Resolved; - } - FORCEINLINE FVector3f MakeWormNoiseBase( float WorldX, float WorldY, @@ -117,16 +33,11 @@ namespace float WorldY, float WorldZ, const VoxelWormField::FParameters& Parameters, - int32 NoiseMode, float NoiseScale) { const FVector3f NoiseBase = MakeWormNoiseBase( WorldX, WorldY, WorldZ, Parameters); - return FMath::Abs( - (NoiseMode == 0 - ? VoxelNoise::Perlin3D(NoiseBase) - : VoxelNoise::ValueNoise3D(NoiseBase, Parameters.Seed, 0x4D3A2F19u)) - * NoiseScale); + return FMath::Abs(VoxelNoise::Perlin3D(NoiseBase) * NoiseScale); } FORCEINLINE float EvaluateN2Field( @@ -134,17 +45,12 @@ namespace float WorldY, float WorldZ, const VoxelWormField::FParameters& Parameters, - int32 NoiseMode, float NoiseScale) { const FVector3f NoiseBase = MakeWormNoiseBase( WorldX, WorldY, WorldZ, Parameters); const FVector3f N2Position = NoiseBase + FVector3f(137.0f, 259.0f, 431.0f); - return FMath::Abs( - (NoiseMode == 0 - ? VoxelNoise::Perlin3D(N2Position) - : VoxelNoise::ValueNoise3D(N2Position, Parameters.Seed, 0xA7C15E2Du)) - * NoiseScale); + return FMath::Abs(VoxelNoise::Perlin3D(N2Position) * NoiseScale); } FORCEINLINE float EvaluateExactField( @@ -154,288 +60,22 @@ namespace float Threshold, const VoxelWormField::FParameters& Parameters) { - const int32 NoiseMode = ResolveNoiseMode(); - const float NoiseScale = VOXEL_NOISE_SCALE - * (NoiseMode == 1 ? CheapWormNoiseAmplitude : 1.0f); + const float NoiseScale = VOXEL_NOISE_SCALE; const float N1 = EvaluateN1Field( - WorldX, WorldY, WorldZ, Parameters, NoiseMode, NoiseScale); + WorldX, WorldY, WorldZ, Parameters, NoiseScale); - // N2 is non-negative, so this preserves the old exact evaluator's short-circuit. + // N2 is non-negative, so this preserves the exact evaluator's short-circuit. if (N1 >= Threshold) { return N1; } return N1 + EvaluateN2Field( - WorldX, WorldY, WorldZ, Parameters, NoiseMode, NoiseScale); - } - - struct FLatticeNode - { - int32 X = 0; - int32 Y = 0; - int32 Z = 0; - float Value = 0.0f; - bool bValid = false; - }; - - // The shared density grid is traversed z/y/x. Retaining two adjacent lattice planes is - // necessary for the z transition; 2048 entries evicted the previous plane and made the - // experiment slower than exact evaluation. This is fixed storage, not a heap allocation, and - // remains deterministic on every worker. - constexpr uint32 LatticeCacheSize = 32768; - static_assert((LatticeCacheSize & (LatticeCacheSize - 1u)) == 0, - "The lattice cache size must remain a power of two for the mask lookup."); - constexpr int32 TileLatticeAxis = 32; - static_assert(static_cast(TileLatticeAxis * TileLatticeAxis * TileLatticeAxis) - == LatticeCacheSize, - "The direct tile lattice must use the fixed cache storage exactly."); - - struct FLatticeCache - { - uint32 Seed = 0; - uint32 FrequencyBits = 0; - uint32 HorizontalBiasBits = 0; - uint32 VerticalScaleBits = 0; - int32 Step = 0; - bool bUseTileWindow = false; - FIntVector TileBaseGrid = FIntVector::ZeroValue; - int32 TileSampleStep = 0; - int32 TileCellsPerAxis = 0; - bool bContextValid = false; - FLatticeNode Nodes[LatticeCacheSize]; - - void SetContext( - const VoxelWormField::FParameters& Parameters, - int32 InStep, - bool InUseTileWindow, - const FIntVector& InTileOrigin, - int32 InTileSampleStep, - int32 InTileCellsPerAxis) - { - uint32 FrequencyBitsIn = 0; - uint32 HorizontalBiasBitsIn = 0; - uint32 VerticalScaleBitsIn = 0; - FMemory::Memcpy(&FrequencyBitsIn, &Parameters.Frequency, sizeof(uint32)); - FMemory::Memcpy(&HorizontalBiasBitsIn, &Parameters.HorizontalBias, sizeof(uint32)); - FMemory::Memcpy(&VerticalScaleBitsIn, &Parameters.VerticalScale, sizeof(uint32)); - const double WormStepD = static_cast(InStep); - const FIntVector InTileBaseGrid( - FMath::FloorToInt(static_cast(InTileOrigin.X) / WormStepD) - 2, - FMath::FloorToInt(static_cast(InTileOrigin.Y) / WormStepD) - 2, - FMath::FloorToInt(static_cast(InTileOrigin.Z) / WormStepD) - 2); - if (bContextValid - && Seed == Parameters.Seed - && FrequencyBits == FrequencyBitsIn - && HorizontalBiasBits == HorizontalBiasBitsIn - && VerticalScaleBits == VerticalScaleBitsIn - && Step == InStep - && bUseTileWindow == InUseTileWindow - && TileBaseGrid == InTileBaseGrid - && TileSampleStep == InTileSampleStep - && TileCellsPerAxis == InTileCellsPerAxis) - { - return; - } - - Seed = Parameters.Seed; - FrequencyBits = FrequencyBitsIn; - HorizontalBiasBits = HorizontalBiasBitsIn; - VerticalScaleBits = VerticalScaleBitsIn; - Step = InStep; - bUseTileWindow = InUseTileWindow; - TileBaseGrid = InTileBaseGrid; - TileSampleStep = InTileSampleStep; - TileCellsPerAxis = InTileCellsPerAxis; - bContextValid = true; - for (FLatticeNode& Node : Nodes) - { - Node.bValid = false; - } - } - - static uint32 Hash(int32 X, int32 Y, int32 Z) - { - uint32 H = static_cast(X) * 0x9E3779B1u; - H ^= static_cast(Y) * 0x85EBCA77u; - H ^= static_cast(Z) * 0xC2B2AE3Du; - H ^= H >> 16; - H *= 0x7FEB352Du; - H ^= H >> 15; - return H; - } - - float Get(int32 X, int32 Y, int32 Z, - const VoxelWormField::FParameters& Parameters, - int32 NoiseMode, - float NoiseScale) - { - const auto EvaluateNode = [this, &Parameters, NoiseMode, NoiseScale](FLatticeNode& Node, - int32 NodeX, int32 NodeY, - int32 NodeZ) -> float - { - if (Node.bValid && Node.X == NodeX && Node.Y == NodeY && Node.Z == NodeZ) - { - return Node.Value; - } - Node.X = NodeX; - Node.Y = NodeY; - Node.Z = NodeZ; - const double StepD = static_cast(Step); - const float WorldX = static_cast(static_cast(NodeX) * StepD); - const float WorldY = static_cast(static_cast(NodeY) * StepD); - const float WorldZ = static_cast(static_cast(NodeZ) * StepD); - // The lattice stores N1 only. The requested voxel uses this interpolated N1 for - // the threshold decision; N2 remains an exact point query only in the carve - // region. This removes the first noise call on the hot above-threshold majority - // without making the second noise unconditional. - Node.Value = EvaluateN1Field( - WorldX, WorldY, WorldZ, Parameters, NoiseMode, NoiseScale); - Node.bValid = true; - return Node.Value; - }; - - if (bUseTileWindow) - { - const int32 LocalX = X - TileBaseGrid.X; - const int32 LocalY = Y - TileBaseGrid.Y; - const int32 LocalZ = Z - TileBaseGrid.Z; - if (LocalX >= 0 && LocalX < TileLatticeAxis - && LocalY >= 0 && LocalY < TileLatticeAxis - && LocalZ >= 0 && LocalZ < TileLatticeAxis) - { - FLatticeNode& Node = Nodes[ - (LocalZ * TileLatticeAxis + LocalY) * TileLatticeAxis + LocalX]; - return EvaluateNode(Node, X, Y, Z); - } - - // A malformed or unusually large tile window must not alter the field's finite - // fallback semantics. It is outside the fixed direct table, so evaluate N1 once - // for this sample rather than attempting an unbounded allocation. - const double StepD = static_cast(Step); - return EvaluateN1Field( - static_cast(static_cast(X) * StepD), - static_cast(static_cast(Y) * StepD), - static_cast(static_cast(Z) * StepD), - Parameters, NoiseMode, NoiseScale); - } - - const uint32 Start = Hash(X, Y, Z) & (LatticeCacheSize - 1u); - for (uint32 Probe = 0; Probe < LatticeCacheSize; ++Probe) - { - FLatticeNode& Node = Nodes[(Start + Probe) & (LatticeCacheSize - 1u)]; - if (Node.bValid) - { - if (Node.X == X && Node.Y == Y && Node.Z == Z) - { - return Node.Value; - } - continue; - } - - Node.X = X; - Node.Y = Y; - Node.Z = Z; - return EvaluateNode(Node, X, Y, Z); - } - - // A saturated cache is exceptionally unlikely for one density block. Falling back to - // a direct N1 evaluation keeps the result bounded if a caller exceeds it. - const double StepD = static_cast(Step); - return EvaluateN1Field( - static_cast(static_cast(X) * StepD), - static_cast(static_cast(Y) * StepD), - static_cast(static_cast(Z) * StepD), - Parameters, NoiseMode, NoiseScale); - } - }; - - thread_local FLatticeCache GLatticeCache; - - FORCEINLINE bool CanUseLattice(float WorldX, float WorldY, float WorldZ, int32 Step) - { - if (Step <= 1 || !FMath::IsFinite(WorldX) || !FMath::IsFinite(WorldY) - || !FMath::IsFinite(WorldZ)) - { - return false; - } - - // The integer grid indices must survive the conversion back to a finite float world - // coordinate. Normal VoxelForge worlds are far inside this envelope; pathological input - // takes the exact path rather than changing overflow behaviour. - constexpr double MaxGridCoordinate = 1000000000.0; - return FMath::Abs(static_cast(WorldX) / static_cast(Step)) - <= MaxGridCoordinate - && FMath::Abs(static_cast(WorldY) / static_cast(Step)) - <= MaxGridCoordinate - && FMath::Abs(static_cast(WorldZ) / static_cast(Step)) - <= MaxGridCoordinate; - } - - FORCEINLINE float Lerp(float A, float B, float Alpha) - { - return A + (B - A) * Alpha; - } - - float EvaluateLattice( - float WorldX, - float WorldY, - float WorldZ, - const VoxelWormField::FParameters& Parameters, - int32 Step, - float NoiseScale) - { - const int32 NoiseMode = ResolveNoiseMode(); - const bool bUseTileWindow = VoxelGenLOD::TileCellsPerAxis > 0 - && VoxelGenLOD::TileCellsPerAxis <= CHUNK_SIZE - && VoxelGenLOD::SampleStep > 0; - GLatticeCache.SetContext( - Parameters, - Step, - bUseTileWindow, - VoxelGenLOD::TileOriginVoxels, - VoxelGenLOD::SampleStep, - VoxelGenLOD::TileCellsPerAxis); - - const double StepD = static_cast(Step); - const double GX = static_cast(WorldX) / StepD; - const double GY = static_cast(WorldY) / StepD; - const double GZ = static_cast(WorldZ) / StepD; - const int32 X0 = FMath::FloorToInt(GX); - const int32 Y0 = FMath::FloorToInt(GY); - const int32 Z0 = FMath::FloorToInt(GZ); - const float Tx = static_cast(GX - static_cast(X0)); - const float Ty = static_cast(GY - static_cast(Y0)); - const float Tz = static_cast(GZ - static_cast(Z0)); - - const float V000 = GLatticeCache.Get(X0, Y0, Z0, Parameters, NoiseMode, NoiseScale); - const float V100 = GLatticeCache.Get(X0 + 1, Y0, Z0, Parameters, NoiseMode, NoiseScale); - const float V010 = GLatticeCache.Get(X0, Y0 + 1, Z0, Parameters, NoiseMode, NoiseScale); - const float V110 = GLatticeCache.Get(X0 + 1, Y0 + 1, Z0, Parameters, NoiseMode, NoiseScale); - const float V001 = GLatticeCache.Get(X0, Y0, Z0 + 1, Parameters, NoiseMode, NoiseScale); - const float V101 = GLatticeCache.Get(X0 + 1, Y0, Z0 + 1, Parameters, NoiseMode, NoiseScale); - const float V011 = GLatticeCache.Get(X0, Y0 + 1, Z0 + 1, Parameters, NoiseMode, NoiseScale); - const float V111 = GLatticeCache.Get(X0 + 1, Y0 + 1, Z0 + 1, Parameters, NoiseMode, NoiseScale); - - const float X00 = Lerp(V000, V100, Tx); - const float X10 = Lerp(V010, V110, Tx); - const float X01 = Lerp(V001, V101, Tx); - const float X11 = Lerp(V011, V111, Tx); - return Lerp(Lerp(X00, X10, Ty), Lerp(X01, X11, Ty), Tz); + WorldX, WorldY, WorldZ, Parameters, NoiseScale); } } namespace VoxelWormField { - int32 GetLatticeStep() - { - return ResolveLatticeStep(); - } - - int32 GetNoiseMode() - { - return ResolveNoiseMode(); - } - float EvaluateExact( float WorldX, float WorldY, @@ -451,21 +91,8 @@ namespace VoxelWormField float WorldY, float WorldZ, float Threshold, - const FParameters& Parameters, - int32 LatticeStep) + const FParameters& Parameters) { - const int32 NoiseMode = ResolveNoiseMode(); - const bool bUseLattice = CanUseLattice(WorldX, WorldY, WorldZ, LatticeStep); - const float NoiseScale = VOXEL_NOISE_SCALE - * (NoiseMode == 1 ? CheapWormNoiseAmplitude : 1.0f); - const float N1 = bUseLattice - ? EvaluateLattice(WorldX, WorldY, WorldZ, Parameters, LatticeStep, NoiseScale) - : EvaluateN1Field(WorldX, WorldY, WorldZ, Parameters, NoiseMode, NoiseScale); - if (N1 >= Threshold) - { - return N1; - } - return N1 + EvaluateN2Field( - WorldX, WorldY, WorldZ, Parameters, NoiseMode, NoiseScale); + return EvaluateExactField(WorldX, WorldY, WorldZ, Threshold, Parameters); } } diff --git a/Source/VoxelForge/Public/VoxelDensityAblation.h b/Source/VoxelForge/Public/VoxelDensityAblation.h new file mode 100644 index 0000000..2667303 --- /dev/null +++ b/Source/VoxelForge/Public/VoxelDensityAblation.h @@ -0,0 +1,60 @@ +// VoxelDensityAblation.h +// Development-only, world-changing measurement overrides for TunnelNetwork density stages. + +#pragma once + +#include "CoreMinimal.h" + +#include + +namespace VoxelDensityAblation +{ + /** + * A bit in this mask means that the named stage is forced off. These are measurement + * overrides, not gameplay settings: they change the generated field, are resolved once per + * process, and must match across peers and regenerated worlds. + */ + enum class EStage : uint32 + { + CaveWarp = 1u << 0, + DetailOps = 1u << 1, + RoomSDF = 1u << 2, + TunnelSDF = 1u << 3, + TunnelCore = 1u << 4, + PassageCarving = 1u << 5, + PassageStructuralPosts= 1u << 6, + NativeFloor = 1u << 7, + Disturbances = 1u << 8, + OriginSpine = 1u << 9, + BoundarySeal = 1u << 10, + LandingPosts = 1u << 11, + XYEdgeSeal = 1u << 12, + PitChimneySDF = 1u << 13, + }; + + /** Resolve command-line/CVar values once and return the process-wide mask. */ + VOXELFORGE_API uint32 GetResolvedMask(); + + FORCEINLINE bool IsForcedOff(EStage Stage) + { + return (GetResolvedMask() & static_cast(Stage)) != 0; + } + + FORCEINLINE bool IsCaveWarpOff() { return IsForcedOff(EStage::CaveWarp); } + FORCEINLINE bool IsDetailOpsOff() { return IsForcedOff(EStage::DetailOps); } + FORCEINLINE bool IsRoomSDFOff() { return IsForcedOff(EStage::RoomSDF); } + FORCEINLINE bool IsTunnelSDFOff() { return IsForcedOff(EStage::TunnelSDF); } + FORCEINLINE bool IsTunnelCoreOff() { return IsForcedOff(EStage::TunnelCore); } + FORCEINLINE bool IsPassageCarvingOff() { return IsForcedOff(EStage::PassageCarving); } + FORCEINLINE bool IsPassageStructuralPostsOff() + { + return IsForcedOff(EStage::PassageStructuralPosts); + } + FORCEINLINE bool IsNativeFloorOff() { return IsForcedOff(EStage::NativeFloor); } + FORCEINLINE bool IsDisturbancesOff() { return IsForcedOff(EStage::Disturbances); } + FORCEINLINE bool IsOriginSpineOff() { return IsForcedOff(EStage::OriginSpine); } + FORCEINLINE bool IsBoundarySealOff() { return IsForcedOff(EStage::BoundarySeal); } + FORCEINLINE bool IsLandingPostsOff() { return IsForcedOff(EStage::LandingPosts); } + FORCEINLINE bool IsXYEdgeSealOff() { return IsForcedOff(EStage::XYEdgeSeal); } + FORCEINLINE bool IsPitChimneySDFOff() { return IsForcedOff(EStage::PitChimneySDF); } +} diff --git a/Source/VoxelForge/Public/VoxelDensityPrimitives.h b/Source/VoxelForge/Public/VoxelDensityPrimitives.h index b27c7f6..4b7e1d0 100644 --- a/Source/VoxelForge/Public/VoxelDensityPrimitives.h +++ b/Source/VoxelForge/Public/VoxelDensityPrimitives.h @@ -27,6 +27,7 @@ #include "CoreMinimal.h" #include "VoxelTypes.h" // SmoothStep01 #include "VoxelPassageGeometry.h" // shared body-sized landing/tunnel geometry +#include "VoxelDensityAblation.h" // development-only world-changing measurements //============================================================================= // SEAL DE FRONTIÈRE / BOUNDARY SEAL @@ -43,7 +44,7 @@ FORCEINLINE void VF_ApplyBoundarySeal(float& Density, float WorldZ, float StrateTopZ, float StrateBottomZ, float Thickness, float BaseDensity) { - if (Thickness <= 0.0f) return; + if (VoxelDensityAblation::IsBoundarySealOff() || Thickness <= 0.0f) return; const float DistTop = StrateTopZ - WorldZ; // + si on est sous le plafond const float DistBot = WorldZ - StrateBottomZ; // + si on est au-dessus du sol @@ -71,6 +72,7 @@ FORCEINLINE void VF_ApplyBoundarySeal(float& Density, float WorldZ, FORCEINLINE void VF_ApplyPassageCarving(float& Density, float ModSDF, float BaseDensity, float SealThickness) { + if (VoxelDensityAblation::IsPassageCarvingOff()) return; constexpr float PASSAGE_BLEND_RADIUS = 4.0f; if (ModSDF >= PASSAGE_BLEND_RADIUS) return; @@ -121,7 +123,7 @@ FORCEINLINE void VF_ApplyPassageLandingCarving(float& Density, float LandingSDF, FORCEINLINE void VF_ApplyOriginSpine(float& Density, float WorldX, float WorldY, float WorldZ, float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius) { - if (Radius <= 0.0f) return; + if (VoxelDensityAblation::IsOriginSpineOff() || Radius <= 0.0f) return; const VoxelPassageGeometry::FOriginLandingGeometry Geometry = VoxelPassageGeometry::BuildOriginLandingGeometry( @@ -153,7 +155,7 @@ FORCEINLINE void VF_ApplyOriginLandingAir(float& Density, float WorldX, float WorldY, float WorldZ, float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius) { - if (Radius <= 0.0f) return; + if (VoxelDensityAblation::IsLandingPostsOff() || Radius <= 0.0f) return; const VoxelPassageGeometry::FOriginLandingGeometry Geometry = VoxelPassageGeometry::BuildOriginLandingGeometry( StrateTopZ, StrateBottomZ, SealThickness, Radius); @@ -169,7 +171,7 @@ FORCEINLINE void VF_ApplyOriginLandingFloor(float& Density, float WorldX, float WorldY, float WorldZ, float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius) { - if (Radius <= 0.0f) return; + if (VoxelDensityAblation::IsLandingPostsOff() || Radius <= 0.0f) return; const VoxelPassageGeometry::FOriginLandingGeometry Geometry = VoxelPassageGeometry::BuildOriginLandingGeometry( StrateTopZ, StrateBottomZ, SealThickness, Radius); @@ -221,7 +223,8 @@ FORCEINLINE void VF_ApplyOriginLandingFloorMC(float& Density, FORCEINLINE void VF_ApplyXYEdgeSeal(float& Density, float WorldX, float WorldY, float WorldRadiusVoxels, float Thickness, float BaseDensity) { - if (!(WorldRadiusVoxels > 0.0f) || !(Thickness > 0.0f) || !(BaseDensity > 0.0f)) return; + if (VoxelDensityAblation::IsXYEdgeSealOff() + || !(WorldRadiusVoxels > 0.0f) || !(Thickness > 0.0f) || !(BaseDensity > 0.0f)) return; if (!VoxelMath::IsFinite(WorldX) || !VoxelMath::IsFinite(WorldY) || !VoxelMath::IsFinite(WorldRadiusVoxels) || !VoxelMath::IsFinite(Thickness) || !VoxelMath::IsFinite(BaseDensity)) return; @@ -256,7 +259,8 @@ FORCEINLINE void VF_ApplyXYEdgeSeal(float& Density, float WorldX, float WorldY, FORCEINLINE void VF_ApplyXYEdgeSealMC(float& Density, float WorldX, float WorldY, float WorldRadiusVoxels, float Thickness, float BaseDensity) { - if (!(WorldRadiusVoxels > 0.0f) || !(Thickness > 0.0f)) return; + if (VoxelDensityAblation::IsXYEdgeSealOff() + || !(WorldRadiusVoxels > 0.0f) || !(Thickness > 0.0f)) return; float InternalDensity = -Density; VF_ApplyXYEdgeSeal(InternalDensity, WorldX, WorldY, diff --git a/Source/VoxelForge/Public/VoxelNoise.h b/Source/VoxelForge/Public/VoxelNoise.h index b75ff0d..ecaaa3a 100644 --- a/Source/VoxelForge/Public/VoxelNoise.h +++ b/Source/VoxelForge/Public/VoxelNoise.h @@ -142,56 +142,6 @@ FORCEINLINE float Perlin3D(const FVector3f& P) return Perlin3D(P.X, P.Y, P.Z); } -// A smooth, deterministic value-noise sample for the worm-only cost experiment. It keeps the -// same unit lattice and quintic interpolation as Perlin3D, but uses one seeded scalar hash per -// corner instead of the eight gradient selections and dot products. It is intentionally not used -// by the terrain's other noise fields; WormNoiseMode gates it as an explicit world-changing test. -FORCEINLINE uint32 ValueNoiseHash(int32 X, int32 Y, int32 Z, uint32 Seed, uint32 Salt) -{ - uint32 H = Detail::HashCorner(X, Y, Z) ^ Seed ^ Salt; - H ^= H >> 16; - H *= 0x7FEB352Du; - H ^= H >> 15; - return H; -} - -FORCEINLINE float ValueNoise3D(const FVector3f& P, uint32 Seed, uint32 Salt) -{ - const float XFloor = FMath::FloorToFloat(P.X); - const float YFloor = FMath::FloorToFloat(P.Y); - const float ZFloor = FMath::FloorToFloat(P.Z); - const int32 X = static_cast(XFloor); - const int32 Y = static_cast(YFloor); - const int32 Z = static_cast(ZFloor); - const float Fx = P.X - XFloor; - const float Fy = P.Y - YFloor; - const float Fz = P.Z - ZFloor; - const float Sx = Detail::Fade(Fx); - const float Sy = Detail::Fade(Fy); - const float Sz = Detail::Fade(Fz); - constexpr float HashToSigned = 2.0f / 16777216.0f; - const auto Sample = [Seed, Salt, HashToSigned](int32 IX, int32 IY, int32 IZ) -> float - { - return static_cast(ValueNoiseHash(IX, IY, IZ, Seed, Salt) & 0x00FFFFFFu) - * HashToSigned - 1.0f; - }; - const float V000 = Sample(X, Y, Z); - const float V100 = Sample(X + 1, Y, Z); - const float V010 = Sample(X, Y + 1, Z); - const float V110 = Sample(X + 1, Y + 1, Z); - const float V001 = Sample(X, Y, Z + 1); - const float V101 = Sample(X + 1, Y, Z + 1); - const float V011 = Sample(X, Y + 1, Z + 1); - const float V111 = Sample(X + 1, Y + 1, Z + 1); - const float X00 = Detail::Lerp(V000, V100, Sx); - const float X10 = Detail::Lerp(V010, V110, Sx); - const float X01 = Detail::Lerp(V001, V101, Sx); - const float X11 = Detail::Lerp(V011, V111, Sx); - const float Y0 = Detail::Lerp(X00, X10, Sy); - const float Y1 = Detail::Lerp(X01, X11, Sy); - return Detail::Lerp(Y0, Y1, Sz); -} - //============================================================================= // LIPSCHITZ BOUND FOR THE ACTUAL 3D FIELD //============================================================================= diff --git a/Source/VoxelForge/Public/VoxelWormField.h b/Source/VoxelForge/Public/VoxelWormField.h index b2e23cf..c98406b 100644 --- a/Source/VoxelForge/Public/VoxelWormField.h +++ b/Source/VoxelForge/Public/VoxelWormField.h @@ -16,16 +16,6 @@ namespace VoxelWormField uint32 Seed = 0; }; - /** - * Return the selected development experiment mode. - * - * 0 is the exact field. 2 and 4 evaluate the exact field on that many-voxel lattice and - * trilinearly interpolate it. This is intentionally a WORLD-CHANGING development switch: - * it must not be changed between multiplayer peers or for an already generated world. - */ - VOXELFORGE_API int32 GetLatticeStep(); - VOXELFORGE_API int32 GetNoiseMode(); - /** Evaluate the exact two-noise field, retaining the N1 threshold short-circuit. */ VOXELFORGE_API float EvaluateExact( float WorldX, @@ -34,12 +24,11 @@ namespace VoxelWormField float Threshold, const FParameters& Parameters); - /** Evaluate the exact field or its selected cached lattice approximation. */ + /** Evaluate the exact two-noise field. */ VOXELFORGE_API float Evaluate( float WorldX, float WorldY, float WorldZ, float Threshold, - const FParameters& Parameters, - int32 LatticeStep); + const FParameters& Parameters); } diff --git a/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp b/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp index 28383f2..485c9ee 100644 --- a/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp +++ b/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp @@ -29,11 +29,11 @@ #include "VoxelCaveMorphology.h" #include "VoxelDiffLayer.h" #include "VoxelDensityOpStack.h" +#include "VoxelDensityAblation.h" #include "VoxelDensityProfile.h" #include "VoxelGenerator.h" #include "VoxelMarchingCubesMesher.h" #include "VoxelNoise.h" -#include "VoxelWormField.h" #include "VoxelSettings.h" #include "VoxelStackSampler.h" #include "VoxelStrateDefinition.h" @@ -4202,8 +4202,8 @@ FString BuildManifestJson( { Writer->WriteValue(TEXT("worm_strength"), static_cast(Arguments.WormStrength)); } - Writer->WriteValue(TEXT("worm_lattice_step"), VoxelWormField::GetLatticeStep()); - Writer->WriteValue(TEXT("worm_noise_mode"), VoxelWormField::GetNoiseMode()); + Writer->WriteValue(TEXT("tunnel_ablation_mask"), + static_cast(VoxelDensityAblation::GetResolvedMask())); WriteTunnelFloorOverrides(*Writer, Arguments); Writer->WriteValue(TEXT("room_mouth_rise_override"), Arguments.bRoomMouthRiseOverride); if (Arguments.bRoomMouthRiseOverride) @@ -4478,8 +4478,8 @@ FString BuildExploreJson( { Writer->WriteValue(TEXT("worm_strength"), static_cast(Arguments.WormStrength)); } - Writer->WriteValue(TEXT("worm_lattice_step"), VoxelWormField::GetLatticeStep()); - Writer->WriteValue(TEXT("worm_noise_mode"), VoxelWormField::GetNoiseMode()); + Writer->WriteValue(TEXT("tunnel_ablation_mask"), + static_cast(VoxelDensityAblation::GetResolvedMask())); WriteTunnelFloorOverrides(*Writer, Arguments); Writer->WriteValue(TEXT("room_mouth_rise_override"), Arguments.bRoomMouthRiseOverride); if (Arguments.bRoomMouthRiseOverride)