// VoxelGenerator.cpp // Champ de densité du monde. Toute la géométrie des grottes sort d'ici. // // Convention INTERNE: positif = solide, négatif = air (plus lisible). // Convention de SORTIE (marching cubes): on négate → négatif = solide. #include "VoxelGenerator.h" #include "VoxelSettings.h" #include "VoxelStrateManager.h" #include "VoxelStrateDefinition.h" #include "VoxelTerrainOpDefinition.h" #include "VoxelCaveMorphology.h" #include "VoxelDiffLayer.h" #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" // the per-strate operator stack #include "VoxelRedefinedDensityField.h" // default-off editor research evaluator #include "VoxelHeightOp.h" // IVoxelBiomeField — the adapter below implements it #include "VoxelStats.h" #include "VoxelDensityProfile.h" #include "ProfilingDebugging/CpuProfilerTrace.h" #include "HAL/PlatformTime.h" #include "HAL/IConsoleManager.h" #include "Misc/CommandLine.h" #if WITH_EDITOR #include "VoxelStrateComposer.h" #endif #include namespace VoxelMath { VOXELFORGE_API int32 GFastIsFinite = 1; } namespace { #if WITH_EDITOR thread_local bool GVoxelResearchRegionCacheWindowEnabled = false; #endif // The default-on branch uses VoxelMath::IsFiniteFast, while 0 retains the original // FMath::IsFinite call for a real within-round A/B and sampler comparison. FAutoConsoleVariableRef CVarVoxelForgeFastIsFinite( TEXT("voxel.FastIsFinite"), VoxelMath::GFastIsFinite, TEXT("Use the exact inline IEEE-754 bit test instead of the CRT-backed FMath::IsFinite.")); bool GVoxelForgeFastIsFiniteSwitchParsed = false; void VF_ParseFastIsFiniteSwitch() { if (GVoxelForgeFastIsFiniteSwitchParsed) { return; } GVoxelForgeFastIsFiniteSwitchParsed = true; int32 CommandLineValue = VoxelMath::GFastIsFinite; if (FParse::Value( FCommandLine::Get(), TEXT("voxel.FastIsFinite="), CommandLineValue)) { VoxelMath::GFastIsFinite = CommandLineValue; } VoxelMath::GFastIsFinite = VoxelMath::GFastIsFinite != 0 ? 1 : 0; } // The canonical native TunnelNetwork/Underwater graph has a hand-lowered evaluator. Keep a // command-line switch so every change can be A/B'd against the existing interpreted/block // path without changing the authored graph or rebuilding a different world definition. int32 GVoxelForgeUseFusedEvaluator = 1; FAutoConsoleVariableRef CVarVoxelForgeUseFusedEvaluator( TEXT("voxel.UseFusedEvaluator"), GVoxelForgeUseFusedEvaluator, TEXT("Use the lowered fused evaluator for canonical TunnelNetwork operator stacks.")); bool GVoxelForgeFusedEvaluatorSwitchParsed = false; void VF_ParseFusedEvaluatorSwitch() { if (GVoxelForgeFusedEvaluatorSwitchParsed) { return; } GVoxelForgeFusedEvaluatorSwitchParsed = true; int32 CommandLineValue = GVoxelForgeUseFusedEvaluator; if (FParse::Value( FCommandLine::Get(), TEXT("voxel.UseFusedEvaluator="), CommandLineValue)) { GVoxelForgeUseFusedEvaluator = CommandLineValue; } } // A proved N1-above-threshold block skips both worm noise calls. The switch is parsed once // on the same scalar entry point as the other generation A/B switches; 0 is the exact legacy // path and remains the default until a tighter skip is justified by a future measurement. int32 GVoxelForgeWormBlockSkip = 0; FAutoConsoleVariableRef CVarVoxelForgeWormBlockSkip( TEXT("voxel.WormBlockSkip"), GVoxelForgeWormBlockSkip, TEXT("Skip worm N1/N2 when a worker-local noise-space block proof says N1 >= threshold.")); bool GVoxelForgeWormBlockSkipSwitchParsed = false; void VF_ParseWormBlockSkipSwitch() { if (GVoxelForgeWormBlockSkipSwitchParsed) { return; } GVoxelForgeWormBlockSkipSwitchParsed = true; int32 CommandLineValue = GVoxelForgeWormBlockSkip; if (FParse::Value( FCommandLine::Get(), TEXT("voxel.WormBlockSkip="), CommandLineValue)) { GVoxelForgeWormBlockSkip = CommandLineValue; } GVoxelForgeWormBlockSkip = GVoxelForgeWormBlockSkip != 0 ? 1 : 0; } // A tile-sized cache window is the production default on the coarse paths where it wins. // Keep the switches in the generator module so native and op-stack sources make the same A/B // decision, including when a headless harness applies command-line CVars after module startup. int32 GVoxelForgeTileCacheWindow = 1; FAutoConsoleVariableRef CVarVoxelForgeTileCacheWindow( TEXT("voxel.TileCacheWindow"), GVoxelForgeTileCacheWindow, TEXT("Use the requesting tile's aligned footprint plus the deterministic warp/halo margin for room caches.")); bool GVoxelForgeTileCacheWindowSwitchParsed = false; // Refreshed profiles put the fused crossover at LOD3 and the op-stack crossover at LOD4. // Separate defaults keep a path that loses at LOD3 from paying for a larger candidate set. int32 GVoxelForgeTileCacheWindowMinLOD = 3; FAutoConsoleVariableRef CVarVoxelForgeTileCacheWindowMinLOD( TEXT("voxel.TileCacheWindowMinLOD"), GVoxelForgeTileCacheWindowMinLOD, TEXT("Minimum SampleStep LOD for the fused tile-sized room cache window.")); int32 GVoxelForgeTileCacheWindowOpStackMinLOD = 4; FAutoConsoleVariableRef CVarVoxelForgeTileCacheWindowOpStackMinLOD( TEXT("voxel.TileCacheWindowOpStackMinLOD"), GVoxelForgeTileCacheWindowOpStackMinLOD, TEXT("Minimum SampleStep LOD for the op-stack tile-sized room cache window.")); int32 GVoxelForgeSpatialIndex = 1; FAutoConsoleVariableRef CVarVoxelForgeSpatialIndex( TEXT("voxel.SpatialIndex"), GVoxelForgeSpatialIndex, TEXT("Room-cache spatial broad phase: -1 measured auto policy, 0 off, 1 on.")); bool GVoxelForgeSpatialIndexSwitchParsed = false; void VF_ParseTileCacheWindowSwitch() { if (GVoxelForgeTileCacheWindowSwitchParsed) { return; } GVoxelForgeTileCacheWindowSwitchParsed = true; int32 CommandLineValue = GVoxelForgeTileCacheWindow; if (FParse::Value( FCommandLine::Get(), TEXT("voxel.TileCacheWindow="), CommandLineValue)) { GVoxelForgeTileCacheWindow = CommandLineValue; } GVoxelForgeTileCacheWindow = GVoxelForgeTileCacheWindow != 0 ? 1 : 0; int32 MinLODCommandLineValue = GVoxelForgeTileCacheWindowMinLOD; if (FParse::Value( FCommandLine::Get(), TEXT("voxel.TileCacheWindowMinLOD="), MinLODCommandLineValue)) { GVoxelForgeTileCacheWindowMinLOD = MinLODCommandLineValue; } GVoxelForgeTileCacheWindowMinLOD = FMath::Clamp( GVoxelForgeTileCacheWindowMinLOD, 0, 5); int32 OpStackMinLODCommandLineValue = GVoxelForgeTileCacheWindowOpStackMinLOD; if (FParse::Value( FCommandLine::Get(), TEXT("voxel.TileCacheWindowOpStackMinLOD="), OpStackMinLODCommandLineValue)) { GVoxelForgeTileCacheWindowOpStackMinLOD = OpStackMinLODCommandLineValue; } GVoxelForgeTileCacheWindowOpStackMinLOD = FMath::Clamp( GVoxelForgeTileCacheWindowOpStackMinLOD, 0, 5); } void VF_ParseSpatialIndexSwitch() { if (GVoxelForgeSpatialIndexSwitchParsed) { return; } GVoxelForgeSpatialIndexSwitchParsed = true; int32 CommandLineValue = GVoxelForgeSpatialIndex; if (FParse::Value( FCommandLine::Get(), TEXT("voxel.SpatialIndex="), CommandLineValue)) { GVoxelForgeSpatialIndex = CommandLineValue; } GVoxelForgeSpatialIndex = FMath::Clamp(GVoxelForgeSpatialIndex, -1, 1); } int32 GVoxelForgeChunkRoutingCache = 1; FAutoConsoleVariableRef CVarVoxelForgeChunkRoutingCache( TEXT("voxel.ChunkRoutingCache"), GVoxelForgeChunkRoutingCache, TEXT("Memoize chunk-Z gap/generator/operator-stack routing for density samples.")); bool GVoxelForgeChunkRoutingCacheSwitchParsed = false; void VF_ParseChunkRoutingCacheSwitch() { if (GVoxelForgeChunkRoutingCacheSwitchParsed) { return; } GVoxelForgeChunkRoutingCacheSwitchParsed = true; int32 CommandLineValue = GVoxelForgeChunkRoutingCache; if (FParse::Value( FCommandLine::Get(), TEXT("voxel.ChunkRoutingCache="), CommandLineValue)) { GVoxelForgeChunkRoutingCache = CommandLineValue; } GVoxelForgeChunkRoutingCache = GVoxelForgeChunkRoutingCache != 0 ? 1 : 0; } } //============================================================================= // L'ADAPTATEUR DE CHAMP DE BIOMES / THE BIOME FIELD ADAPTER //============================================================================= // Il vit ICI, du côté qui connaît le générateur, et PAS dans la pile d'opérateurs. C'est tout // l'intérêt de `IVoxelBiomeField` : le résolveur réel est une Voronoï warpée avec un cache par // chunk sur `UVoxelGenerator`, et un opérateur qui tiendrait ce pointeur ne pourrait jamais devenir // un asset (Phase 3). En le confinant ici, l'opérateur ne connaît qu'une capacité, pas un // propriétaire. // // ⚠️ DURÉES DE VIE : cet adaptateur pointe vers les `thread_local` `CP_BiomeCtx` / `CP_BiomeCache` // de `GetDensityAt`. Il est POSSÉDÉ par la pile, elle-même `thread_local` et reconstruite dans le // MÊME bloc de refetch que ces deux caches — les trois naissent et meurent ensemble, sur le même // thread. Un pointeur vers un thread_local depuis un objet thread_local du même bloc est sûr ; // l'échapper ailleurs ne le serait pas. // // Lives here, not in the op stack: the real resolver is generator state, and an op holding that // pointer could never become an asset. LIFETIME: it points at GetDensityAt's thread_locals and is // owned by the stack, which is itself thread_local and rebuilt in the same refetch block. class FGeneratorBiomeField final : public IVoxelBiomeField { public: FGeneratorBiomeField(const UVoxelGenerator* InGen, const FBiomeContext* InCtx, FChunkBiomeCache* InCache, int32 InChunkZ) : Gen(InGen), Ctx(InCtx), Cache(InCache), ChunkZ(InChunkZ) {} FVoxelBiomeWeights SampleAt(float WorldX, float WorldY) const override { FVoxelBiomeWeights Out; if (!Gen || !Ctx || !Cache) { return Out; } // dégradation sûre : biome 0 partout const FBiomeSample S = Gen->ResolveBiomeSampleAt(WorldX, WorldY, ChunkZ, *Ctx, *Cache); Out.Dominant = FMath::Max(S.DominantIndex, 0); // -1 ⇒ 0, comme le chemin d'origine Out.Neighbor = S.NeighborIndex; Out.NeighborWeight = S.NeighborWeight; return Out; } private: const UVoxelGenerator* Gen; const FBiomeContext* Ctx; FChunkBiomeCache* Cache; int32 ChunkZ; }; //============================================================================= // SURFACE COLUMN CACHE — ClassifyTile's SurfaceWorld column proof (T1.d) //============================================================================= // The SurfaceWorld heightfield + sky-cap + biome blend are functions of XY ONLY, but a tile // spans many Z grid-points per column. ClassifyTile caches the column (terrain Z + ceiling Z) // once per integer XY and reuses it down the column. (The surface density stack keeps its own // column memo in FSurfaceColumnSource.) // The surface heightfield (TerrainZ + sky-cap CeilSurf) is a PURE function of (worldX, worldY, // seed, strate) — ZERO Z dependence: the climate/Voronoi fields are pure-XY (see ResolveBiomeSampleAt: // "result is the pure function of XY either way") and surface params are constant within a strate (Hard // transitions). So a column computed for one chunk is valid for EVERY chunk stacked above/below it in // the same strate. This cache is therefore keyed by (XY box, StrateKey, Seed) — NOT ChunkZ — and held as // a small LRU of boxes so the whole vertical view-distance stack (and XY neighbours interleaved by the // task scheduler) reuse one another's heavy column noise instead of each recomputing it ~once per // vertical chunk in view. Validity stays a world-XY BOX (chunk footprint + margin) so the ±1 gradient / // +X/+Y boundary samples don't thrash it (same discipline as the SDF/biome caches, §8.10). // F20 phase 2 OVERHANG, resolved once per column (biome-blended, slope-gated) so the per-voxel // density path is cheap: OverhangAmp = strength · slope-gate (0 ⇒ no overhang here); (DirX,DirY) = // the UNIT UPHILL direction of the terrain gradient (the lip extends downhill, borrowing rock from // uphill). See FOverhangShelfMod for the warped-terrain union that makes the shelf. struct FSurfaceColumn { // Toutes les sorties sont écrites par ComputeSurfaceColumn avant Computed=true ; les valeurs // par défaut ne sont donc jamais lues et leur constructeur implicite coûte dans le TLS. // Every output is written by ComputeSurfaceColumn before Computed=true; the defaults are never // read, and removing them keeps FSurfaceColumn trivial so TLS construction has no leaf work. float TerrainZ, CeilSurf; float OverhangAmp, DirX, DirY; }; struct FSurfaceColumnBox { // Covers a chunk footprint + a margin on every side (≥ the LOD margin ring the mesher samples for // grid-based normals). Symmetric around the first (allocating) sample so a whole chunk's column // queries — including the ±Step margin ring — stay inside one box. static constexpr int32 Halo = CHUNK_SIZE + 8; static constexpr int32 Dim = 2 * Halo + 1; int32 BaseX = 0, BaseY = 0; // box origin (voxel coords) int32 StrateKey = MIN_int32, Seed = MIN_int32; // key: same strate ⇒ identical heightfield params uint64 OwnerId = 0; // generator/world identity uint64 ManagerLifetimeId = 0; // strate-manager lifetime identity // AUDIT C2, étendu : StrateKey vaut round(StrateBottomWorldZ), donc une édition à chaud qui // change les params de terrain SANS déplacer la strate (fréquence de bruit, hauteur de // montagne, un biome…) laisse la clé identique et sert des colonnes périmées. C'est la forme // la plus visible du bug : « j'ai retouché le terrain et une zone a gardé l'ancienne forme ». // StrateKey is round(StrateBottomWorldZ), so a live edit that changes terrain params WITHOUT // moving the strate leaves the key unchanged and serves stale columns. uint32 LayoutVersion = 0xFFFFFFFFu; uint32 LastUse = 0; // LRU stamp bool bValid = false; FSurfaceColumn Cols[Dim * Dim]; bool Computed[Dim * Dim]; }; struct FSurfaceColumnCache { // A handful of boxes keeps the player's column + a few XY neighbours warm across interleaved tasks, // so vertical reuse survives whatever order the worker pulls chunks in. ~59 KB/box. static constexpr int32 NumBoxes = 6; FSurfaceColumnBox Boxes[NumBoxes]; uint32 Clock = 0; // Return the box covering (IX,IY) for this (owner,manager,StrateKey,Seed,LayoutVersion); allocate by // evicting the LRU box on miss. FSurfaceColumnBox& Acquire(int32 IX, int32 IY, int32 InStrateKey, int32 InSeed, uint32 InLayoutVersion, uint64 InOwnerId, uint64 InManagerLifetimeId) { ++Clock; for (FSurfaceColumnBox& B : Boxes) { if (B.bValid && B.OwnerId == InOwnerId && B.ManagerLifetimeId == InManagerLifetimeId && B.StrateKey == InStrateKey && B.Seed == InSeed && B.LayoutVersion == InLayoutVersion && IX >= B.BaseX && IX < B.BaseX + FSurfaceColumnBox::Dim && IY >= B.BaseY && IY < B.BaseY + FSurfaceColumnBox::Dim) { B.LastUse = Clock; return B; } } // Miss → reuse the least-recently-used box, recentred on this sample. FSurfaceColumnBox* Victim = &Boxes[0]; for (FSurfaceColumnBox& B : Boxes) { if (B.LastUse < Victim->LastUse) Victim = &B; } Victim->BaseX = IX - FSurfaceColumnBox::Halo; Victim->BaseY = IY - FSurfaceColumnBox::Halo; Victim->StrateKey = InStrateKey; Victim->Seed = InSeed; Victim->OwnerId = InOwnerId; Victim->ManagerLifetimeId = InManagerLifetimeId; Victim->LayoutVersion = InLayoutVersion; Victim->bValid = true; Victim->LastUse = Clock; FMemory::Memzero(Victim->Computed, sizeof(Victim->Computed)); return *Victim; } }; // Cache de colonnes par thread de ClassifyTile (T1.d) : les pré-passes et passes complètes // successives d'une même tuile, et les tuiles voisines, réutilisent leurs colonnes. // Per-thread column cache for ClassifyTile: successive passes and neighbouring tiles reuse columns. static thread_local FSurfaceColumnCache GSurfColCache; //============================================================================= // WORM N1 BLOCK PROOF (worker-local, scalar GetDensityAt hand-off) //============================================================================= // GenerateMesh installs TileOriginVoxels/SampleStep/TileCellsPerAxis around every density-grid // fill. Keep the verdicts here, rather than in the mesher, because the scalar GetDensityAt path // owns the actual worm call and is also used by standalone/classifier queries. The cache is // thread_local, direct-indexed by the block coordinate, and invalidated by every field identity // that can affect N1. A miss or an invalid context always returns false and therefore executes // the old N1/N2 code. namespace { constexpr int32 VF_WormBlockCacheAxis = (CHUNK_SIZE + 3 + VoxelNoise::WormBlockSampleSide - 1) / VoxelNoise::WormBlockSampleSide; constexpr int32 VF_WormBlockCacheEntryCount = VF_WormBlockCacheAxis * VF_WormBlockCacheAxis * VF_WormBlockCacheAxis; static_assert(VF_WormBlockCacheAxis == 9, "The fixed worm block table must cover the 32-cell mesher lattice plus its halo."); FORCEINLINE uint32 VF_FloatBits(float Value) { uint32 Bits = 0; FMemory::Memcpy(&Bits, &Value, sizeof(Bits)); return Bits; } struct FWormBlockSkipEntry { bool bValid = false; bool bSkip = false; }; struct FWormBlockSkipCache { bool bContextValid = false; uint64 OwnerId = 0; uint32 Seed = 0; uint32 ParamsFingerprint = 0xFFFFFFFFu; uint32 LayoutVersion = 0xFFFFFFFFu; FIntVector TileOrigin = FIntVector::ZeroValue; int32 Step = 0; int32 Cells = 0; uint32 WormFrequencyBits = 0; uint32 WormHorizontalBiasBits = 0; uint32 WormThresholdBits = 0; uint32 VerticalScaleBits = 0; FWormBlockSkipEntry Entries[VF_WormBlockCacheEntryCount]; void SetContext( uint64 InOwnerId, uint32 InSeed, uint32 InParamsFingerprint, uint32 InLayoutVersion, const FIntVector& InTileOrigin, int32 InStep, int32 InCells, const FStrateGenerationParams& Params) { const uint32 InWormFrequencyBits = VF_FloatBits(Params.WormFrequency); const uint32 InWormHorizontalBiasBits = VF_FloatBits(Params.WormHorizontalBias); const uint32 InWormThresholdBits = VF_FloatBits(Params.WormThreshold); const uint32 InVerticalScaleBits = VF_FloatBits(Params.VerticalScale); const bool bChanged = !bContextValid || OwnerId != InOwnerId || Seed != InSeed || ParamsFingerprint != InParamsFingerprint || LayoutVersion != InLayoutVersion || TileOrigin != InTileOrigin || Step != InStep || Cells != InCells || WormFrequencyBits != InWormFrequencyBits || WormHorizontalBiasBits != InWormHorizontalBiasBits || WormThresholdBits != InWormThresholdBits || VerticalScaleBits != InVerticalScaleBits; if (!bChanged) { return; } bContextValid = true; OwnerId = InOwnerId; Seed = InSeed; ParamsFingerprint = InParamsFingerprint; LayoutVersion = InLayoutVersion; TileOrigin = InTileOrigin; Step = InStep; Cells = InCells; WormFrequencyBits = InWormFrequencyBits; WormHorizontalBiasBits = InWormHorizontalBiasBits; WormThresholdBits = InWormThresholdBits; VerticalScaleBits = InVerticalScaleBits; FMemory::Memzero(Entries, sizeof(Entries)); } }; static thread_local FWormBlockSkipCache GWormBlockSkipCache; FORCEINLINE FVector3f VF_WormN1NoisePosition( float WorldX, float WorldY, float WorldZ, const FStrateGenerationParams& Params, uint32 SeedU) { float EffectiveZ = WorldZ; if (Params.VerticalScale != 1.0f && Params.VerticalScale > 0.0f) { EffectiveZ = WorldZ / Params.VerticalScale; } const float WormZFreq = Params.WormFrequency * Params.WormHorizontalBias; return FVector3f( WorldX * Params.WormFrequency + VoxelHash::SeedOffset(SeedU, 1.0f), WorldY * Params.WormFrequency + VoxelHash::SeedOffset(SeedU, 1.7f), EffectiveZ * WormZFreq + VoxelHash::SeedOffset(SeedU, 2.3f)); } FORCEINLINE bool VF_TryGetWormBlockSkip( uint64 OwnerId, uint32 SeedU, uint32 ParamsFingerprint, uint32 LayoutVersion, float WorldX, float WorldY, float WorldZ, const FStrateGenerationParams& Params, bool& bOutProofComputed) { bOutProofComputed = false; // Point queries and malformed authored parameters stay on the exact original path. In // particular, VerticalScale <= 0 has legacy "no division" semantics; declining the proof // here avoids making a new promise for malformed data. if (!FMath::IsFinite(WorldX) || !FMath::IsFinite(WorldY) || !FMath::IsFinite(WorldZ) || WorldX != FMath::FloorToFloat(WorldX) || WorldY != FMath::FloorToFloat(WorldY) || WorldZ != FMath::FloorToFloat(WorldZ) || !FMath::IsFinite(Params.WormFrequency) || !FMath::IsFinite(Params.WormHorizontalBias) || !FMath::IsFinite(Params.WormThreshold) || !FMath::IsFinite(Params.VerticalScale) || !(Params.WormThreshold > 0.0f) || !(Params.VerticalScale > 0.0f) || !FMath::IsFinite(Params.WormFrequency * Params.WormHorizontalBias)) { return false; } const int32 Step = VoxelGenLOD::SampleStep; const int32 Cells = VoxelGenLOD::TileCellsPerAxis; if (Step <= 0 || Cells <= 0 || Cells > CHUNK_SIZE) { return false; } // FMath::RoundToInt is only reached after the finite/integer check and a range guard. The // tile context itself is int32, so coordinates outside this range cannot belong to it. constexpr float SafeInt32Min = -2147483008.0f; constexpr float SafeInt32Max = 2147483008.0f; if (WorldX < SafeInt32Min || WorldX > SafeInt32Max || WorldY < SafeInt32Min || WorldY > SafeInt32Max || WorldZ < SafeInt32Min || WorldZ > SafeInt32Max) { return false; } const int32 IX = FMath::RoundToInt(WorldX); const int32 IY = FMath::RoundToInt(WorldY); const int32 IZ = FMath::RoundToInt(WorldZ); const FIntVector TileOrigin = VoxelGenLOD::TileOriginVoxels; const int64 DX = static_cast(IX) - static_cast(TileOrigin.X); const int64 DY = static_cast(IY) - static_cast(TileOrigin.Y); const int64 DZ = static_cast(IZ) - static_cast(TileOrigin.Z); if (DX % Step != 0 || DY % Step != 0 || DZ % Step != 0) { return false; } const int32 LocalX = static_cast(DX / Step); const int32 LocalY = static_cast(DY / Step); const int32 LocalZ = static_cast(DZ / Step); const int32 MinLocalLattice = -1; const int32 MaxLocalLattice = Cells + 1; if (LocalX < MinLocalLattice || LocalX > MaxLocalLattice || LocalY < MinLocalLattice || LocalY > MaxLocalLattice || LocalZ < MinLocalLattice || LocalZ > MaxLocalLattice) { return false; } const int32 BlockX = (LocalX + 1) / VoxelNoise::WormBlockSampleSide; const int32 BlockY = (LocalY + 1) / VoxelNoise::WormBlockSampleSide; const int32 BlockZ = (LocalZ + 1) / VoxelNoise::WormBlockSampleSide; if (BlockX < 0 || BlockX >= VF_WormBlockCacheAxis || BlockY < 0 || BlockY >= VF_WormBlockCacheAxis || BlockZ < 0 || BlockZ >= VF_WormBlockCacheAxis) { return false; } GWormBlockSkipCache.SetContext( OwnerId, SeedU, ParamsFingerprint, LayoutVersion, TileOrigin, Step, Cells, Params); FWormBlockSkipEntry& Entry = GWormBlockSkipCache.Entries[ (BlockZ * VF_WormBlockCacheAxis + BlockY) * VF_WormBlockCacheAxis + BlockX]; if (Entry.bValid) { return Entry.bSkip; } const int32 BlockMinX = BlockX * VoxelNoise::WormBlockSampleSide - 1; const int32 BlockMinY = BlockY * VoxelNoise::WormBlockSampleSide - 1; const int32 BlockMinZ = BlockZ * VoxelNoise::WormBlockSampleSide - 1; const int32 BlockMaxX = FMath::Min(MaxLocalLattice, BlockMinX + VoxelNoise::WormBlockSampleSide - 1); const int32 BlockMaxY = FMath::Min(MaxLocalLattice, BlockMinY + VoxelNoise::WormBlockSampleSide - 1); const int32 BlockMaxZ = FMath::Min(MaxLocalLattice, BlockMinZ + VoxelNoise::WormBlockSampleSide - 1); const auto WorldAt = [Step, &TileOrigin](int32 Local, int32 Origin) -> int64 { return static_cast(Origin) + static_cast(Local) * Step; }; const int64 MinWorldX64 = WorldAt(BlockMinX, TileOrigin.X); const int64 MinWorldY64 = WorldAt(BlockMinY, TileOrigin.Y); const int64 MinWorldZ64 = WorldAt(BlockMinZ, TileOrigin.Z); const int64 MaxWorldX64 = WorldAt(BlockMaxX, TileOrigin.X); const int64 MaxWorldY64 = WorldAt(BlockMaxY, TileOrigin.Y); const int64 MaxWorldZ64 = WorldAt(BlockMaxZ, TileOrigin.Z); if (MinWorldX64 < MIN_int32 || MinWorldX64 > MAX_int32 || MinWorldY64 < MIN_int32 || MinWorldY64 > MAX_int32 || MinWorldZ64 < MIN_int32 || MinWorldZ64 > MAX_int32 || MaxWorldX64 < MIN_int32 || MaxWorldX64 > MAX_int32 || MaxWorldY64 < MIN_int32 || MaxWorldY64 > MAX_int32 || MaxWorldZ64 < MIN_int32 || MaxWorldZ64 > MAX_int32) { return false; } const FVector3f Q0 = VF_WormN1NoisePosition( static_cast(static_cast(MinWorldX64)), static_cast(static_cast(MinWorldY64)), static_cast(static_cast(MinWorldZ64)), Params, SeedU); const FVector3f Q1 = VF_WormN1NoisePosition( static_cast(static_cast(MaxWorldX64)), static_cast(static_cast(MaxWorldY64)), static_cast(static_cast(MaxWorldZ64)), Params, SeedU); const FVector3f QMin( FMath::Min(Q0.X, Q1.X), FMath::Min(Q0.Y, Q1.Y), FMath::Min(Q0.Z, Q1.Z)); const FVector3f QMax( FMath::Max(Q0.X, Q1.X), FMath::Max(Q0.Y, Q1.Y), FMath::Max(Q0.Z, Q1.Z)); const FVector3f QCenter( static_cast((static_cast(QMin.X) + static_cast(QMax.X)) * 0.5), static_cast((static_cast(QMin.Y) + static_cast(QMax.Y)) * 0.5), static_cast((static_cast(QMin.Z) + static_cast(QMax.Z)) * 0.5)); Entry.bSkip = VoxelNoise::ProvesScaledAbsPerlin3DAbove( QCenter, QMin, QMax, Params.WormThreshold, VoxelNoise::WormN1NoiseSpaceLipschitzBound); Entry.bValid = true; bOutProofComputed = true; return Entry.bSkip; } } //============================================================================= // FRACTAL NOISE (fBm — fractional Brownian motion) //============================================================================= // Empile plusieurs octaves de Perlin: freq x2 et amp /2 à chaque octave. // Octave 1: freq=f, amp=1.0 → grandes collines lisses // Octave 2: freq=2f, amp=0.5 → bosses moyennes // Octave 3: freq=4f, amp=0.25 → petits cailloux // Lacunarity = x freq par octave (2 = double à chaque fois) // Persistence = x amp par octave (0.5 = moitié) // T2.b — per-thread octave bias for the tile being meshed (see VoxelGenerator.h). // 0 = full quality; set by the mesher per tile from Step + Settings->LODOctaveDrop. thread_local int32 VoxelGenLOD::OctaveBias = 0; //============================================================================= // OP-MAJOR DENSITY BLOCK HAND-OFF //============================================================================= // The public Begin/End pair is only a lifetime marker. The actual storage is worker-local and // bounded by the current mesher lattice. A tile may cross several chunk keys; each key is filled // by the already-prepared stack that GetDensityAt selected for that key, so no mutable stack state // is shared across chunk boundaries. struct FVoxelDensityBlockSession { bool bActive = false; bool bBatchRequested = false; bool bBatchSucceeded = false; FIntVector Origin = FIntVector::ZeroValue; int32 Step = 1; int32 SizeX = 0; int32 SizeY = 0; int32 SizeZ = 0; TArray Samples; TArray Valid; TArray BlockScratch; TArray ActiveOps; TArray BatchResults; void Begin(FIntVector InOrigin, int32 InStep, int32 InSizeX, int32 InSizeY, int32 InSizeZ) { bActive = InStep > 0 && InSizeX > 0 && InSizeY > 0 && InSizeZ > 0; Origin = InOrigin; Step = FMath::Max(InStep, 1); SizeX = FMath::Max(InSizeX, 0); SizeY = FMath::Max(InSizeY, 0); SizeZ = FMath::Max(InSizeZ, 0); if (!bActive) { Samples.Reset(); Valid.Reset(); BlockScratch.Reset(); ActiveOps.Reset(); BatchResults.Reset(); bBatchRequested = false; bBatchSucceeded = false; return; } // Allocate lazily on the first operator-stack sample. Chunks that never reach a stack // (gaps, out-of-layout, degenerate strates) use the ordinary scalar path and should not // pay for a block scratch lattice merely because the mesher opened the scoped hand-off. BlockScratch.Reset(); BatchResults.Reset(); bBatchRequested = false; bBatchSucceeded = false; } void End() { bActive = false; // Keep capacity for the next tile on this worker, but never retain a verdict between // sessions. A stale sample can only cost the next task a block fill, never change it. Samples.Reset(); Valid.Reset(); BlockScratch.Reset(); ActiveOps.Reset(); BatchResults.Reset(); bBatchRequested = false; bBatchSucceeded = false; } int32 Index(int32 X, int32 Y, int32 Z) const { return (Z * SizeY + Y) * SizeX + X; } bool LocalIndex(FIntVector World, int32& OutX, int32& OutY, int32& OutZ) const { const int32 DX = World.X - Origin.X; const int32 DY = World.Y - Origin.Y; const int32 DZ = World.Z - Origin.Z; if (DX % Step != 0 || DY % Step != 0 || DZ % Step != 0) { return false; } OutX = DX / Step; OutY = DY / Step; OutZ = DZ / Step; return OutX >= 0 && OutX < SizeX && OutY >= 0 && OutY < SizeY && OutZ >= 0 && OutZ < SizeZ; } bool FindAxisRange(int32 Axis, int32 TargetChunk, int32& OutMin, int32& OutMax) const { OutMin = INT32_MAX; OutMax = INT32_MIN; const int32 Limit = Axis == 0 ? SizeX : (Axis == 1 ? SizeY : SizeZ); const int32 Base = Axis == 0 ? Origin.X : (Axis == 1 ? Origin.Y : Origin.Z); for (int32 I = 0; I < Limit; ++I) { const int32 Coordinate = Base + I * Step; if (FMath::FloorToInt(static_cast(Coordinate) / CHUNK_SIZE) == TargetChunk) { OutMin = FMath::Min(OutMin, I); OutMax = FMath::Max(OutMax, I); } } return OutMin != INT32_MAX; } bool FillChunk(const FVoxelOpStack& Stack, FIntVector ChunkCoord, FIntVector World, FVoxelOpSample& OutSample) { int32 X = 0, Y = 0, Z = 0; if (!LocalIndex(World, X, Y, Z)) { return false; } const int32 Count = SizeX * SizeY * SizeZ; if (Samples.Num() != Count || Valid.Num() != Count) { Samples.SetNum(Count); Valid.Init(0, Count); } const int32 GlobalIndex = Index(X, Y, Z); if (Valid[GlobalIndex] != 0) { OutSample = Samples[GlobalIndex]; return true; } int32 MinX = 0, MaxX = 0, MinY = 0, MaxY = 0, MinZ = 0, MaxZ = 0; if (!FindAxisRange(0, ChunkCoord.X, MinX, MaxX) || !FindAxisRange(1, ChunkCoord.Y, MinY, MaxY) || !FindAxisRange(2, ChunkCoord.Z, MinZ, MaxZ)) { return false; } const int32 BlockSizeX = MaxX - MinX + 1; const int32 BlockSizeY = MaxY - MinY + 1; const int32 BlockSizeZ = MaxZ - MinZ + 1; const int32 BlockCount = BlockSizeX * BlockSizeY * BlockSizeZ; VoxelDensityProfile::FScopedTimer OperatorBlockTimer( VoxelDensityProfile::EBucket::OperatorBlock); BlockScratch.SetNum(BlockCount); if (VoxelDensityProfile::AreCountersEnabled()) { VoxelDensityProfile::AddCounter( VoxelDensityProfile::ECounter::OpBlockScratchBytes, static_cast(BlockCount) * sizeof(FVoxelOpSample)); } for (FVoxelOpSample& Sample : BlockScratch) { Sample = FVoxelOpSample(); } FVoxelOpBlock Block; Block.OriginVoxels = Origin + FIntVector(MinX * Step, MinY * Step, MinZ * Step); Block.Step = Step; Block.SizeX = BlockSizeX; Block.SizeY = BlockSizeY; Block.SizeZ = BlockSizeZ; Block.Samples = BlockScratch.GetData(); const FIntVector BlockLast = Block.OriginVoxels + FIntVector((BlockSizeX - 1) * Step, (BlockSizeY - 1) * Step, (BlockSizeZ - 1) * Step); const FBox BlockBox( FVector(static_cast(Block.OriginVoxels.X), static_cast(Block.OriginVoxels.Y), static_cast(Block.OriginVoxels.Z)), FVector(static_cast(BlockLast.X), static_cast(BlockLast.Y), static_cast(BlockLast.Z))); const int32 ActiveCount = Stack.BuildActiveOpList( BlockBox, Step, Block.OriginVoxels, ActiveOps); Stack.EvalBlock(Block, ActiveOps); for (int32 Bz = 0; Bz < BlockSizeZ; ++Bz) { for (int32 By = 0; By < BlockSizeY; ++By) { for (int32 Bx = 0; Bx < BlockSizeX; ++Bx) { const int32 Global = Index(MinX + Bx, MinY + By, MinZ + Bz); const int32 Local = (Bz * BlockSizeY + By) * BlockSizeX + Bx; Samples[Global] = BlockScratch[Local]; Valid[Global] = 1; } } } if (VoxelDensityProfile::AreCountersEnabled()) { VoxelDensityProfile::AddCounter( VoxelDensityProfile::ECounter::OpBlockCopyBytes, static_cast(BlockCount) * (sizeof(FVoxelOpSample) + sizeof(uint8))); } if (VoxelDensityProfile::AreCountersEnabled()) { VoxelDensityProfile::AddCounter(VoxelDensityProfile::ECounter::OpBlockBuilds); VoxelDensityProfile::AddCounter( VoxelDensityProfile::ECounter::OpBlockSamples, static_cast(BlockCount)); VoxelDensityProfile::AddCounter( VoxelDensityProfile::ECounter::OpBlockOperators, static_cast(Stack.Num())); VoxelDensityProfile::AddCounter( VoxelDensityProfile::ECounter::OpBlockActiveOperators, static_cast(ActiveCount)); VoxelDensityProfile::AddCounter( VoxelDensityProfile::ECounter::OpBlockPrunedOperators, static_cast(FMath::Max(Stack.Num() - ActiveCount, 0))); } OutSample = Samples[GlobalIndex]; return true; } }; static thread_local FVoxelDensityBlockSession GVoxelDensityBlockSession; thread_local int32 VoxelGenLOD::SampleStep = 1; thread_local FIntVector VoxelGenLOD::TileOriginVoxels = FIntVector::ZeroValue; thread_local int32 VoxelGenLOD::TileCellsPerAxis = 0; int32 VoxelGenLOD::GetThreadOctaveBias() { return OctaveBias; } void VoxelGenLOD::SetThreadOctaveBias(int32 Value) { OctaveBias = Value; } int32 VoxelGenLOD::GetThreadSampleStep() { return SampleStep; } void VoxelGenLOD::SetThreadSampleStep(int32 Value) { SampleStep = Value; } void VoxelGenLOD::InitializeConsoleSwitches() { VF_ParseFastIsFiniteSwitch(); VF_ParseFusedEvaluatorSwitch(); VF_ParseWormBlockSkipSwitch(); VF_ParseTileCacheWindowSwitch(); VF_ParseSpatialIndexSwitch(); VF_ParseChunkRoutingCacheSwitch(); } bool VoxelGenLOD::IsTileCacheWindowEnabled(bool bFusedPath) { if (GVoxelForgeTileCacheWindow == 0) { return false; } #if WITH_EDITOR if (GVoxelResearchRegionCacheWindowEnabled) { return true; } #endif const int32 RequiredMinLOD = bFusedPath ? GVoxelForgeTileCacheWindowMinLOD : GVoxelForgeTileCacheWindowOpStackMinLOD; const int32 RequiredStep = 1 << FMath::Clamp(RequiredMinLOD, 0, 5); return FMath::Max(SampleStep, 1) >= RequiredStep; } #if WITH_EDITOR void VoxelGenLOD::SetResearchRegionCacheWindowEnabled(bool bEnabled) { GVoxelResearchRegionCacheWindowEnabled = bEnabled; } #endif bool VoxelGenLOD::GetThreadTileCacheWindow( FIntVector& OutOriginVoxels, int32& OutStep, int32& OutCellsPerAxis) { OutOriginVoxels = TileOriginVoxels; OutStep = FMath::Max(SampleStep, 1); OutCellsPerAxis = TileCellsPerAxis; if (TileCellsPerAxis <= 0) { return false; } const int64 Extent = static_cast(OutStep) * static_cast(OutCellsPerAxis); // The graph build is quadratic in the collected room count. Keep the tile policy bounded // to a 64-chunk side; the field's LOD4 tile is 32 chunks, while an accidentally giant request // falls back to the older bounded window instead of turning one worker into an unbounded bake. return Extent > 0 && Extent <= MAX_int32 && Extent <= static_cast(CHUNK_SIZE) * 64; } void VoxelGenLOD::SetThreadTileContext( const FIntVector& OriginVoxels, int32 CellsPerAxis) { TileOriginVoxels = OriginVoxels; TileCellsPerAxis = CellsPerAxis; } bool VoxelGenLOD::ShouldUseSpatialIndex(bool bFusedPath) { if (GVoxelForgeSpatialIndex >= 0) { return GVoxelForgeSpatialIndex != 0; } // The full game sweep makes the fused LOD0 floor faster without the broad phase. Keep it // enabled for LOD1+ where the candidate reduction pays for GetRange, and keep the measured // op-stack policy enabled at every tested LOD. The explicit override remains useful for // field/determinism A/B runs. if (bFusedPath && FMath::Max(SampleStep, 1) <= 1) { return false; } return true; } // NOTE (T2.a): the fBm/Ridged bodies moved to VoxelNoise.h, where octaves are evaluated // 4-wide via SSE (Perlin3D_x4). These thin wrappers keep every call site unchanged. They // sample a DIFFERENT (float hash-gradient) noise field than the old FMath::PerlinNoise3D, // so worlds re-tune once — but the fBm/Ridged math/contracts ([-1,1]) are identical. static float FractalNoise3D(const FVector3f& Position, int32 Octaves = 4, float Lacunarity = 2.0f, float Persistence = 0.5f) { return VoxelNoise::FBM(Position.X, Position.Y, Position.Z, Octaves, Lacunarity, Persistence); } //============================================================================= // RIDGED MULTIFRACTAL NOISE //============================================================================= // Creates sharp, ridge-like features by folding the noise at zero-crossings. // The absolute value creates "creases" where the noise crosses zero, // and the 1-abs inverts them into ridges. Weight feedback from each // octave makes ridges sharper and more detailed. // // Returns approximately [-1, 1] to match FractalNoise3D's range. // Character: craggy cliffs, natural erosion patterns, sharp corridors. static float RidgedNoise3D(const FVector3f& Position, int32 Octaves = 4, float Lacunarity = 2.0f, float Persistence = 0.5f) { return VoxelNoise::Ridged(Position.X, Position.Y, Position.Z, Octaves, Lacunarity, Persistence); } //============================================================================= // WORLEY / CELLULAR NOISE (3D) //============================================================================= // Distance to nearest feature point in a hash grid. // Creates rounded, cell-like patterns: bubble walls, grotto pockets, // honeycomb textures. Very different character from Perlin-based noise. // // Algorithm: // 1. Find which grid cell the point is in // 2. Check the 3x3x3 neighborhood for feature points // 3. Return (2nd nearest - 1st nearest) distance, normalized to ~[-1, 1] // // Using F2-F1 (difference of two closest distances) gives smooth cell // boundaries with ridges between cells — more interesting than raw distance. // Le CORPS vit dans Public/VoxelCaveMorphology.h (namespace VoxelNoise), au plus bas point // qui voit déjà `VoxelHash` : la pile d'opérateurs a besoin exactement du même bruit pour la // rugosité (4b, type Cellular) et pour les festons (4f), et deux copies d'une fonction pure finissent // par diverger — c'est littéralement `AUDIT §C1`. Ce forwarder garde les ~3 sites d'appel ci-dessous // inchangés, comme FractalNoise3D et RidgedNoise3D. Le chemin chaud garde les coordonnées en float. static float CellularNoise3D(const FVector3f& Position) { return VoxelNoise::Cellular3D(Position); } //============================================================================= // DENSITY PIPELINE HELPERS (partagés entre TunnelNetwork et Slab) //============================================================================= // Les CORPS de ces trois helpers ont déménagé dans Public/VoxelDensityPrimitives.h : la pile // d'opérateurs (VoxelDensityOpStack) a besoin exactement des mêmes, et deux copies des INVARIANTS // de monde (descente possible, seals qui tiennent, passages qui percent, bord fermé) finiraient par // diverger. Ces trois lignes gardent les noms locaux pour que les ~20 sites d'appel ci-dessous ne // bougent pas d'un caractère — le déplacement ne change AUCUN comportement. // // The BODIES moved to Public/VoxelDensityPrimitives.h; the operator stack needs the same primitives, // and duplicate world invariants would eventually drift. These forwarders keep the local names // so not one of the ~20 call sites below changes. No behavioural change. // // Convention INTERNE ici : positif = SOLIDE. La négation vers MC se fait sur le `return`. static FORCEINLINE void ApplyBoundarySeal(float& Density, float WorldZ, float StrateTopZ, float StrateBottomZ, float Thickness, float BaseDensity) { VF_ApplyBoundarySeal(Density, WorldZ, StrateTopZ, StrateBottomZ, Thickness, BaseDensity); } static FORCEINLINE void ApplyOriginSpine(float& Density, float WorldX, float WorldY, float WorldZ, float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius) { VF_ApplyOriginSpine(Density, WorldX, WorldY, WorldZ, StrateTopZ, StrateBottomZ, SealThickness, BaseDensity, Radius); } static FORCEINLINE void ApplyOriginLandingFloor(float& Density, float WorldX, float WorldY, float WorldZ, float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius) { VF_ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ, StrateTopZ, StrateBottomZ, SealThickness, BaseDensity, Radius); } // DISTURBANCE LAYER — the "wow" post-process. Operates on the FINAL MC density // (negative = solid, positive = air), AFTER the archetype produced its terrain, so // it works uniformly for every generator type. Stays inside the seal bands so it can // never breach a strate boundary. All features are hash-placed and deterministic. static void ApplyDisturbances(float& MC, float X, float Y, float Z, const FStrateDisturbanceParams& D, uint32 Seed, bool bProtectVerticalShaftAir) { if (VoxelDensityAblation::IsDisturbancesOff()) { return; } VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::ApplyDisturbances); const float InnerTop = D.StrateTopWorldZ - D.BoundarySealThickness; const float InnerBot = D.StrateBottomWorldZ + D.BoundarySealThickness; if (Z <= InnerBot || Z >= InnerTop) return; const float Solid = D.BaseDensity * 2.0f; const float Blend = 3.0f; const FVector P(X, Y, Z); // 2D point-to-segment distance helper (XY plane). auto Dist2DSeg = [](float px, float py, float ax, float ay, float bx, float by) -> float { const float abx = bx - ax, aby = by - ay; const float apx = px - ax, apy = py - ay; const float denom = FMath::Max(abx * abx + aby * aby, KINDA_SMALL_NUMBER); float t = FMath::Clamp((apx * abx + apy * aby) / denom, 0.0f, 1.0f); const float cx = ax + abx * t, cy = ay + aby * t; return FMath::Sqrt(FMath::Square(px - cx) + FMath::Square(py - cy)); }; // Each feature's 3×3 lattice candidates (existence roll, jitter, angle trig, Z anchor) are pure // functions of (cell, seed, params) yet were re-hashed PER VOXEL — including Cos/Sin per bridge/ // ridge candidate. Bake each feature's nearby primitives once per centre cell (thread_local); // the per-voxel work is just the distance math. Bit-identical (same hashes, same math). // --- CHASMS: vertical rifts carve open air --- if (D.ChasmDensity > 0.0f && D.ChasmSpacing > 0.0f) { const float Sp = D.ChasmSpacing; const int32 cx = FMath::FloorToInt(X / Sp), cy = FMath::FloorToInt(Y / Sp); struct FChasm { float X, Y; }; thread_local TArray> Chasms; thread_local int32 CH_CX = INT32_MAX, CH_CY = INT32_MAX; thread_local uint32 CH_Seed = 0xFFFFFFFFu; thread_local float CH_Sp = -1.0f, CH_Dens = -1.0f; if (cx != CH_CX || cy != CH_CY || Seed != CH_Seed || Sp != CH_Sp || D.ChasmDensity != CH_Dens) { CH_CX = cx; CH_CY = cy; CH_Seed = Seed; CH_Sp = Sp; CH_Dens = D.ChasmDensity; Chasms.Reset(); for (int32 dy = -1; dy <= 1; dy++) for (int32 dx = -1; dx <= 1; dx++) { const int32 nx = cx + dx, ny = cy + dy; const uint32 h = VoxelHash::Cell(nx, ny, Seed ^ 0x43480001u); if (VoxelHash::ToFloat01(h) > D.ChasmDensity) continue; const float jx = VoxelHash::ToFloat01(VoxelHash::Mix(h ^ 0x12345678u)); const float jy = VoxelHash::ToFloat01(VoxelHash::Mix(h ^ 0x9ABCDEF0u)); Chasms.Add({ (nx + 0.15f + jx * 0.7f) * Sp, (ny + 0.15f + jy * 0.7f) * Sp }); } } float sdf = FLT_MAX; for (const FChasm& C : Chasms) { sdf = FMath::Min(sdf, FMath::Sqrt(FMath::Square(X - C.X) + FMath::Square(Y - C.Y)) - D.ChasmRadius); } if (sdf < Blend) { float c = FMath::Clamp((Blend - sdf) / (Blend * 2.0f), 0.0f, 1.0f); c = SmoothStep01(c); MC = FMath::Max(MC, c * Solid); // force air } } // --- BRIDGES: horizontal solid spans across open space --- if (D.BridgeDensity > 0.0f && D.BridgeSpacing > 0.0f) { const float Sp = D.BridgeSpacing; const int32 cx = FMath::FloorToInt(X / Sp), cy = FMath::FloorToInt(Y / Sp); struct FBridge { FVector A, B; }; thread_local TArray> Bridges; thread_local int32 BR_CX = INT32_MAX, BR_CY = INT32_MAX; thread_local uint32 BR_Seed = 0xFFFFFFFFu; thread_local float BR_Sp = -1.0f, BR_Dens = -1.0f, BR_Bot = FLT_MAX, BR_Top = FLT_MAX; if (cx != BR_CX || cy != BR_CY || Seed != BR_Seed || Sp != BR_Sp || D.BridgeDensity != BR_Dens || InnerBot != BR_Bot || InnerTop != BR_Top) { BR_CX = cx; BR_CY = cy; BR_Seed = Seed; BR_Sp = Sp; BR_Dens = D.BridgeDensity; BR_Bot = InnerBot; BR_Top = InnerTop; Bridges.Reset(); for (int32 dy = -1; dy <= 1; dy++) for (int32 dx = -1; dx <= 1; dx++) { const int32 nx = cx + dx, ny = cy + dy; const uint32 h = VoxelHash::Cell(nx, ny, Seed ^ 0x42520001u); if (VoxelHash::ToFloat01(h) > D.BridgeDensity) continue; const float zc = FMath::Lerp(InnerBot + 8.0f, InnerTop - 8.0f, VoxelHash::ToFloat01(VoxelHash::Mix(h ^ 0xB1u))); const float ang = VoxelHash::ToFloat01(VoxelHash::Mix(h ^ 0xB2u)) * PI; float sinAng = 0.0f, cosAng = 0.0f; VoxelMath::DetSinCos(sinAng, cosAng, ang); const float dxu = cosAng, dyu = sinAng; const float bx = (nx + 0.5f) * Sp, by = (ny + 0.5f) * Sp; const float half = Sp * 0.6f; Bridges.Add({ FVector(bx - dxu * half, by - dyu * half, zc), FVector(bx + dxu * half, by + dyu * half, zc) }); } } float sdf = FLT_MAX; for (const FBridge& Br : Bridges) { sdf = FMath::Min(sdf, VoxelSDF::Capsule(P, Br.A, Br.B, D.BridgeRadius)); } if (sdf < Blend) { float f = FMath::Clamp((Blend - sdf) / (Blend * 2.0f), 0.0f, 1.0f); f = SmoothStep01(f); if (!bProtectVerticalShaftAir) { MC = FMath::Min(MC, -f * Solid); // force solid } } } // --- RIDGES: thin solid blades rising from the floor --- if (D.RidgeDensity > 0.0f && D.RidgeSpacing > 0.0f && D.RidgeHeight > 0.0f) { const float Sp = D.RidgeSpacing; const float TopZ = FMath::Min(InnerBot + D.RidgeHeight, InnerTop); if (Z < TopZ) { const int32 cx = FMath::FloorToInt(X / Sp), cy = FMath::FloorToInt(Y / Sp); struct FRidge { float AX, AY, BX, BY; }; thread_local TArray> Ridges; thread_local int32 RG_CX = INT32_MAX, RG_CY = INT32_MAX; thread_local uint32 RG_Seed = 0xFFFFFFFFu; thread_local float RG_Sp = -1.0f, RG_Dens = -1.0f; if (cx != RG_CX || cy != RG_CY || Seed != RG_Seed || Sp != RG_Sp || D.RidgeDensity != RG_Dens) { RG_CX = cx; RG_CY = cy; RG_Seed = Seed; RG_Sp = Sp; RG_Dens = D.RidgeDensity; Ridges.Reset(); for (int32 dy = -1; dy <= 1; dy++) for (int32 dx = -1; dx <= 1; dx++) { const int32 nx = cx + dx, ny = cy + dy; const uint32 h = VoxelHash::Cell(nx, ny, Seed ^ 0x52470001u); if (VoxelHash::ToFloat01(h) > D.RidgeDensity) continue; const float ang = VoxelHash::ToFloat01(VoxelHash::Mix(h ^ 0x9001u)) * PI; float sinAng = 0.0f, cosAng = 0.0f; VoxelMath::DetSinCos(sinAng, cosAng, ang); const float dxu = cosAng, dyu = sinAng; const float bx = (nx + 0.5f) * Sp, by = (ny + 0.5f) * Sp; const float half = Sp * 0.45f; Ridges.Add({ bx - dxu * half, by - dyu * half, bx + dxu * half, by + dyu * half }); } } float best = -FLT_MAX; // strongest fill across nearby blades for (const FRidge& R : Ridges) { const float d2d = Dist2DSeg(X, Y, R.AX, R.AY, R.BX, R.BY); const float wallSDF = d2d - D.RidgeThickness; // <0 inside the blade footprint if (wallSDF >= Blend) continue; const float zFade = 1.0f - FMath::Clamp((Z - InnerBot) / FMath::Max(D.RidgeHeight, 1.0f), 0.0f, 1.0f); float f = FMath::Clamp((Blend - wallSDF) / (Blend * 2.0f), 0.0f, 1.0f); f = SmoothStep01(f) * zFade; best = FMath::Max(best, f); } if (best > 0.0f && !bProtectVerticalShaftAir) { MC = FMath::Min(MC, -best * Solid); // force solid } } } } // The disturbance pass is deterministic and spatially sparse. Treating a non-zero density as a // world-wide influence would make an otherwise proven all-solid/all-air LOD0 box fall back to the // full mesher. These predicates are deliberately one-sided: they only // prove that no hash-placed primitive can touch the queried box. An intersecting candidate, an // invalid range, or an unknown parameter keeps the conservative Mixed result. static FORCEINLINE bool VF_DisturbanceBandTouchesBox( const FBox& Box, const FStrateDisturbanceParams& D) { const float InnerTop = D.StrateTopWorldZ - D.BoundarySealThickness; const float InnerBot = D.StrateBottomWorldZ + D.BoundarySealThickness; if (!VoxelMath::IsFinite(InnerTop) || !VoxelMath::IsFinite(InnerBot)) { return true; // Unknown boundary placement must not discharge a proof. } return Box.Max.Z > InnerBot && Box.Min.Z < InnerTop; } static FORCEINLINE float VF_DistanceSquaredToXYBox( float X, float Y, const FBox& Box) { const float DX = X < Box.Min.X ? Box.Min.X - X : X > Box.Max.X ? X - Box.Max.X : 0.0f; const float DY = Y < Box.Min.Y ? Box.Min.Y - Y : Y > Box.Max.Y ? Y - Box.Max.Y : 0.0f; return DX * DX + DY * DY; } static FORCEINLINE bool VF_ClosedIntervalsOverlap( float AMin, float AMax, float BMin, float BMax) { return AMax >= BMin && BMax >= AMin; } static FORCEINLINE bool VF_InflatedSegmentAabbTouchesBox( const FVector& A, const FVector& B, float Inflation, const FBox& Box) { const float MinX = FMath::Min(A.X, B.X) - Inflation; const float MaxX = FMath::Max(A.X, B.X) + Inflation; const float MinY = FMath::Min(A.Y, B.Y) - Inflation; const float MaxY = FMath::Max(A.Y, B.Y) + Inflation; const float MinZ = FMath::Min(A.Z, B.Z) - Inflation; const float MaxZ = FMath::Max(A.Z, B.Z) + Inflation; return VF_ClosedIntervalsOverlap(MinX, MaxX, Box.Min.X, Box.Max.X) && VF_ClosedIntervalsOverlap(MinY, MaxY, Box.Min.Y, Box.Max.Y) && VF_ClosedIntervalsOverlap(MinZ, MaxZ, Box.Min.Z, Box.Max.Z); } static FORCEINLINE float VF_DistanceSquaredToXYSegment( float X, float Y, float AX, float AY, float BX, float BY) { const float ABX = BX - AX, ABY = BY - AY; const float APX = X - AX, APY = Y - AY; const float Denom = FMath::Max(ABX * ABX + ABY * ABY, KINDA_SMALL_NUMBER); const float T = FMath::Clamp((APX * ABX + APY * ABY) / Denom, 0.0f, 1.0f); const float CX = AX + ABX * T, CY = AY + ABY * T; return FMath::Square(X - CX) + FMath::Square(Y - CY); } static FORCEINLINE bool VF_AnyLatticeZInOpenBand( const FIntVector& Origin, int32 Step, int32 Cells, float MinZ, float MaxZ) { for (int32 GZ = 0; GZ <= Cells; ++GZ) { const float Z = static_cast(Origin.Z + GZ * Step); if (Z > MinZ && Z < MaxZ) { return true; } } return false; } static bool VF_AnyChasmCanTouchLattice( const FIntVector& Origin, int32 Step, int32 Cells, const FStrateDisturbanceParams& D, uint32 Seed) { if (D.ChasmDensity == 0.0f) { return false; } if (!VoxelMath::IsFinite(D.ChasmDensity) || D.ChasmDensity < 0.0f || D.ChasmDensity > 1.0f || !VoxelMath::IsFinite(D.ChasmSpacing) || !VoxelMath::IsFinite(D.ChasmRadius) || D.ChasmSpacing <= 0.0f || D.ChasmRadius < 0.0f) { return true; } const float InnerTop = D.StrateTopWorldZ - D.BoundarySealThickness; const float InnerBot = D.StrateBottomWorldZ + D.BoundarySealThickness; if (!VoxelMath::IsFinite(InnerTop) || !VoxelMath::IsFinite(InnerBot)) { return true; } if (!VF_AnyLatticeZInOpenBand(Origin, Step, Cells, InnerBot, InnerTop)) { return false; } const float Spacing = D.ChasmSpacing; const float InfluenceRadius = D.ChasmRadius + 3.0f; const float InfluenceRadiusSq = InfluenceRadius * InfluenceRadius; for (int32 GY = 0; GY <= Cells; ++GY) for (int32 GX = 0; GX <= Cells; ++GX) { const float X = static_cast(Origin.X + GX * Step); const float Y = static_cast(Origin.Y + GY * Step); const int32 CX = FMath::FloorToInt(X / Spacing); const int32 CY = FMath::FloorToInt(Y / Spacing); for (int32 DY = -1; DY <= 1; ++DY) for (int32 DX = -1; DX <= 1; ++DX) { const int32 NX = CX + DX, NY = CY + DY; const uint32 H = VoxelHash::Cell(NX, NY, Seed ^ 0x43480001u); if (VoxelHash::ToFloat01(H) > D.ChasmDensity) { continue; } const float ChasmX = (NX + 0.15f + VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x12345678u)) * 0.7f) * Spacing; const float ChasmY = (NY + 0.15f + VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x9ABCDEF0u)) * 0.7f) * Spacing; if (FMath::Square(X - ChasmX) + FMath::Square(Y - ChasmY) <= InfluenceRadiusSq) { return true; } } } return false; } static bool VF_AnyBridgeCanTouchLattice( const FIntVector& Origin, int32 Step, int32 Cells, const FStrateDisturbanceParams& D, uint32 Seed) { if (D.BridgeDensity == 0.0f) { return false; } if (!VoxelMath::IsFinite(D.BridgeDensity) || D.BridgeDensity < 0.0f || D.BridgeDensity > 1.0f || !VoxelMath::IsFinite(D.BridgeSpacing) || !VoxelMath::IsFinite(D.BridgeRadius) || D.BridgeSpacing <= 0.0f || D.BridgeRadius < 0.0f) { return true; } const float InnerTop = D.StrateTopWorldZ - D.BoundarySealThickness; const float InnerBot = D.StrateBottomWorldZ + D.BoundarySealThickness; if (!VoxelMath::IsFinite(InnerTop) || !VoxelMath::IsFinite(InnerBot)) { return true; } const float Spacing = D.BridgeSpacing; const float Inflation = D.BridgeRadius + 3.0f; const float InflationSq = Inflation * Inflation; for (int32 GY = 0; GY <= Cells; ++GY) for (int32 GX = 0; GX <= Cells; ++GX) { const float X = static_cast(Origin.X + GX * Step); const float Y = static_cast(Origin.Y + GY * Step); const int32 CX = FMath::FloorToInt(X / Spacing); const int32 CY = FMath::FloorToInt(Y / Spacing); for (int32 DY = -1; DY <= 1; ++DY) for (int32 DX = -1; DX <= 1; ++DX) { const int32 NX = CX + DX, NY = CY + DY; const uint32 H = VoxelHash::Cell(NX, NY, Seed ^ 0x42520001u); if (VoxelHash::ToFloat01(H) > D.BridgeDensity) { continue; } const float ZC = FMath::Lerp( InnerBot + 8.0f, InnerTop - 8.0f, VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0xB1u))); const float Angle = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0xB2u)) * PI; float SinAngle = 0.0f, CosAngle = 0.0f; VoxelMath::DetSinCos(SinAngle, CosAngle, Angle); const float DXU = CosAngle, DYU = SinAngle; const float BX = (NX + 0.5f) * Spacing, BY = (NY + 0.5f) * Spacing; const float Half = Spacing * 0.6f; const float HorizontalSq = VF_DistanceSquaredToXYSegment( X, Y, BX - DXU * Half, BY - DYU * Half, BX + DXU * Half, BY + DYU * Half); if (HorizontalSq > InflationSq) { continue; } for (int32 GZ = 0; GZ <= Cells; ++GZ) { const float Z = static_cast(Origin.Z + GZ * Step); if (FMath::Square(Z - ZC) + HorizontalSq <= InflationSq) { return true; } } } } return false; } static bool VF_AnyRidgeCanTouchLattice( const FIntVector& Origin, int32 Step, int32 Cells, const FStrateDisturbanceParams& D, uint32 Seed) { if (D.RidgeDensity == 0.0f) { return false; } if (!VoxelMath::IsFinite(D.RidgeDensity) || D.RidgeDensity < 0.0f || D.RidgeDensity > 1.0f || !VoxelMath::IsFinite(D.RidgeSpacing) || !VoxelMath::IsFinite(D.RidgeHeight) || !VoxelMath::IsFinite(D.RidgeThickness) || D.RidgeSpacing <= 0.0f || D.RidgeHeight <= 0.0f || D.RidgeThickness < 0.0f) { return true; } const float InnerTop = D.StrateTopWorldZ - D.BoundarySealThickness; const float InnerBot = D.StrateBottomWorldZ + D.BoundarySealThickness; if (!VoxelMath::IsFinite(InnerTop) || !VoxelMath::IsFinite(InnerBot)) { return true; } const float TopZ = FMath::Min(InnerBot + D.RidgeHeight, InnerTop); bool bHasRidgeZ = false; for (int32 GZ = 0; GZ <= Cells; ++GZ) { if (static_cast(Origin.Z + GZ * Step) < TopZ) { bHasRidgeZ = true; break; } } if (!bHasRidgeZ) { return false; } const float Spacing = D.RidgeSpacing; const float InfluenceRadius = D.RidgeThickness + 3.0f; const float InfluenceRadiusSq = InfluenceRadius * InfluenceRadius; for (int32 GY = 0; GY <= Cells; ++GY) for (int32 GX = 0; GX <= Cells; ++GX) { const float X = static_cast(Origin.X + GX * Step); const float Y = static_cast(Origin.Y + GY * Step); const int32 CX = FMath::FloorToInt(X / Spacing); const int32 CY = FMath::FloorToInt(Y / Spacing); for (int32 DY = -1; DY <= 1; ++DY) for (int32 DX = -1; DX <= 1; ++DX) { const int32 NX = CX + DX, NY = CY + DY; const uint32 H = VoxelHash::Cell(NX, NY, Seed ^ 0x52470001u); if (VoxelHash::ToFloat01(H) > D.RidgeDensity) { continue; } const float Angle = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x9001u)) * PI; float SinAngle = 0.0f, CosAngle = 0.0f; VoxelMath::DetSinCos(SinAngle, CosAngle, Angle); const float DXU = CosAngle, DYU = SinAngle; const float BX = (NX + 0.5f) * Spacing, BY = (NY + 0.5f) * Spacing; const float Half = Spacing * 0.45f; if (VF_DistanceSquaredToXYSegment( X, Y, BX - DXU * Half, BY - DYU * Half, BX + DXU * Half, BY + DYU * Half) <= InfluenceRadiusSq) { return true; } } } return false; } static bool VF_AnyChasmCanTouchBox( const FBox& Box, const FStrateDisturbanceParams& D, uint32 Seed) { if (D.ChasmDensity == 0.0f) { return false; } if (!VoxelMath::IsFinite(D.ChasmDensity) || D.ChasmDensity < 0.0f || D.ChasmDensity > 1.0f || !VoxelMath::IsFinite(D.ChasmSpacing) || !VoxelMath::IsFinite(D.ChasmRadius) || D.ChasmSpacing <= 0.0f || D.ChasmRadius < 0.0f) { return true; } if (!VF_DisturbanceBandTouchesBox(Box, D)) { return false; } const float Spacing = D.ChasmSpacing; const int32 CellX0 = FMath::FloorToInt(Box.Min.X / Spacing) - 1; const int32 CellX1 = FMath::FloorToInt(Box.Max.X / Spacing) + 1; const int32 CellY0 = FMath::FloorToInt(Box.Min.Y / Spacing) - 1; const int32 CellY1 = FMath::FloorToInt(Box.Max.Y / Spacing) + 1; const float InfluenceRadius = D.ChasmRadius + 3.0f; // ApplyDisturbances' Blend. const float InfluenceRadiusSq = InfluenceRadius * InfluenceRadius; for (int32 NY = CellY0; NY <= CellY1; ++NY) for (int32 NX = CellX0; NX <= CellX1; ++NX) { const uint32 H = VoxelHash::Cell(NX, NY, Seed ^ 0x43480001u); if (VoxelHash::ToFloat01(H) > D.ChasmDensity) { continue; } const float CX = (NX + 0.15f + VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x12345678u)) * 0.7f) * Spacing; const float CY = (NY + 0.15f + VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x9ABCDEF0u)) * 0.7f) * Spacing; if (VF_DistanceSquaredToXYBox(CX, CY, Box) <= InfluenceRadiusSq) { return true; } } return false; } static bool VF_AnyBridgeCanTouchBox( const FBox& Box, const FStrateDisturbanceParams& D, uint32 Seed) { if (D.BridgeDensity == 0.0f) { return false; } if (!VoxelMath::IsFinite(D.BridgeDensity) || D.BridgeDensity < 0.0f || D.BridgeDensity > 1.0f || !VoxelMath::IsFinite(D.BridgeSpacing) || !VoxelMath::IsFinite(D.BridgeRadius) || D.BridgeSpacing <= 0.0f || D.BridgeRadius < 0.0f) { return true; } if (!VF_DisturbanceBandTouchesBox(Box, D)) { return false; } const float Spacing = D.BridgeSpacing; const int32 CellX0 = FMath::FloorToInt(Box.Min.X / Spacing) - 1; const int32 CellX1 = FMath::FloorToInt(Box.Max.X / Spacing) + 1; const int32 CellY0 = FMath::FloorToInt(Box.Min.Y / Spacing) - 1; const int32 CellY1 = FMath::FloorToInt(Box.Max.Y / Spacing) + 1; const float Inflation = D.BridgeRadius + 3.0f; // Capsule radius plus Blend. const float InnerTop = D.StrateTopWorldZ - D.BoundarySealThickness; const float InnerBot = D.StrateBottomWorldZ + D.BoundarySealThickness; for (int32 NY = CellY0; NY <= CellY1; ++NY) for (int32 NX = CellX0; NX <= CellX1; ++NX) { const uint32 H = VoxelHash::Cell(NX, NY, Seed ^ 0x42520001u); if (VoxelHash::ToFloat01(H) > D.BridgeDensity) { continue; } const float ZC = FMath::Lerp( InnerBot + 8.0f, InnerTop - 8.0f, VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0xB1u))); const float Angle = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0xB2u)) * PI; float SinAngle = 0.0f, CosAngle = 0.0f; VoxelMath::DetSinCos(SinAngle, CosAngle, Angle); const float DX = CosAngle, DY = SinAngle; const float BX = (NX + 0.5f) * Spacing, BY = (NY + 0.5f) * Spacing; const float Half = Spacing * 0.6f; const FVector A(BX - DX * Half, BY - DY * Half, ZC); const FVector B(BX + DX * Half, BY + DY * Half, ZC); if (VF_InflatedSegmentAabbTouchesBox(A, B, Inflation, Box)) { return true; } } return false; } static bool VF_AnyRidgeCanTouchBox( const FBox& Box, const FStrateDisturbanceParams& D, uint32 Seed) { if (D.RidgeDensity == 0.0f) { return false; } if (!VoxelMath::IsFinite(D.RidgeDensity) || D.RidgeDensity < 0.0f || D.RidgeDensity > 1.0f || !VoxelMath::IsFinite(D.RidgeSpacing) || !VoxelMath::IsFinite(D.RidgeHeight) || !VoxelMath::IsFinite(D.RidgeThickness) || D.RidgeSpacing <= 0.0f || D.RidgeHeight <= 0.0f || D.RidgeThickness < 0.0f) { return true; } if (!VF_DisturbanceBandTouchesBox(Box, D)) { return false; } const float Spacing = D.RidgeSpacing; const int32 CellX0 = FMath::FloorToInt(Box.Min.X / Spacing) - 1; const int32 CellX1 = FMath::FloorToInt(Box.Max.X / Spacing) + 1; const int32 CellY0 = FMath::FloorToInt(Box.Min.Y / Spacing) - 1; const int32 CellY1 = FMath::FloorToInt(Box.Max.Y / Spacing) + 1; const float Inflation = D.RidgeThickness + 3.0f; // Footprint radius plus Blend. const float InnerTop = D.StrateTopWorldZ - D.BoundarySealThickness; const float InnerBot = D.StrateBottomWorldZ + D.BoundarySealThickness; const float TopZ = FMath::Min(InnerBot + D.RidgeHeight, InnerTop); if (Box.Min.Z >= TopZ) { return false; } for (int32 NY = CellY0; NY <= CellY1; ++NY) for (int32 NX = CellX0; NX <= CellX1; ++NX) { const uint32 H = VoxelHash::Cell(NX, NY, Seed ^ 0x52470001u); if (VoxelHash::ToFloat01(H) > D.RidgeDensity) { continue; } const float Angle = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x9001u)) * PI; float SinAngle = 0.0f, CosAngle = 0.0f; VoxelMath::DetSinCos(SinAngle, CosAngle, Angle); const float DX = CosAngle, DY = SinAngle; const float BX = (NX + 0.5f) * Spacing, BY = (NY + 0.5f) * Spacing; const float Half = Spacing * 0.45f; const FVector A(BX - DX * Half, BY - DY * Half, InnerBot); const FVector B(BX + DX * Half, BY + DY * Half, InnerBot); if (VF_InflatedSegmentAabbTouchesBox(A, B, Inflation, Box)) { return true; } } return false; } static FORCEINLINE bool VF_AnyDisturbanceCanTouchBox( const FBox& Box, const FStrateDisturbanceParams& D, uint32 Seed) { return VF_AnyChasmCanTouchBox(Box, D, Seed) || VF_AnyBridgeCanTouchBox(Box, D, Seed) || VF_AnyRidgeCanTouchBox(Box, D, Seed); } static FORCEINLINE bool VF_SealedSolidProofGuardDisabled( uint8 DisabledGuards, EVoxelSealedSolidProofGuard Guard) { return (DisabledGuards & static_cast(Guard)) != 0; } static FORCEINLINE bool VF_IsSafeBoundarySealLatticeZ( float WorldZ, float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity) { // ApplyBoundarySeal is a positive internal-density lower bound. At an exact lattice point, // the original inequalities are sufficient: a strictly positive seal factor times a finite, // positive base density makes the final MC density negative. Do not widen the band here; // false Mixed costs time, while the brute-force test below is the authority for this shortcut. if (!VoxelMath::IsFinite(WorldZ) || !VoxelMath::IsFinite(StrateTopZ) || !VoxelMath::IsFinite(StrateBottomZ) || !VoxelMath::IsFinite(SealThickness) || !VoxelMath::IsFinite(BaseDensity) || !(StrateTopZ > StrateBottomZ) || !(SealThickness > 0.0f) || !(BaseDensity > 0.0f)) { return false; } const float DistTop = StrateTopZ - WorldZ; const float DistBottom = WorldZ - StrateBottomZ; return (DistTop >= 0.0f && DistTop < SealThickness) || (DistBottom >= 0.0f && DistBottom < SealThickness); } static bool VF_SealedSolidProofPassageInputsAreValid(const UVoxelStrateManager& Manager) { // AnyPassageNearBox is intentionally a cheap bounding-sphere test. Its normal runtime inputs // are generated data, but a malformed bound must fail closed: NaN distance comparisons would // otherwise turn an unknown passage into a false "no passage" result. for (const FVoxelPassage& Passage : Manager.GetPassages()) { if (!VoxelMath::IsFinite(Passage.BoundCenter.X) || !VoxelMath::IsFinite(Passage.BoundCenter.Y) || !VoxelMath::IsFinite(Passage.BoundCenter.Z) || !VoxelMath::IsFinite(Passage.BoundRadius) || Passage.BoundRadius < 0.0f || !VoxelMath::IsFinite(Passage.Radius) || Passage.Radius < 0.0f) { return false; } for (const FVector& Point : Passage.ControlPoints) { if (!VoxelMath::IsFinite(Point.X) || !VoxelMath::IsFinite(Point.Y) || !VoxelMath::IsFinite(Point.Z)) { return false; } } for (const float Radius : Passage.ControlRadii) { if (!VoxelMath::IsFinite(Radius) || Radius < 0.0f) { return false; } } const FVoxelPassageLanding* Landings[] = { &Passage.UpperLanding, &Passage.LowerLanding }; for (const FVoxelPassageLanding* Landing : Landings) { if (!VoxelMath::IsFinite(Landing->StandingPoint.X) || !VoxelMath::IsFinite(Landing->StandingPoint.Y) || !VoxelMath::IsFinite(Landing->StandingPoint.Z) || !VoxelMath::IsFinite(Landing->DoorPoint.X) || !VoxelMath::IsFinite(Landing->DoorPoint.Y) || !VoxelMath::IsFinite(Landing->DoorPoint.Z) || !VoxelMath::IsFinite(Landing->FloorZ) || !VoxelMath::IsFinite(Landing->CeilingZ) || !VoxelMath::IsFinite(Landing->HalfWidth) || !VoxelMath::IsFinite(Landing->FloorThickness) || Landing->HalfWidth < 0.0f || Landing->FloorThickness < 0.0f) { return false; } } if (Passage.bWalkableTunnelContract && (Passage.ControlPoints.Num() < 2 || Passage.ControlRadii.Num() != Passage.ControlPoints.Num())) { return false; } } return true; } static bool VF_TryProveSealedSolidTile( const UVoxelGenerator& Generator, const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis, uint8 DisabledGuards) { const UVoxelStrateManager* Manager = Generator.StrateManager; if (Manager == nullptr) { return false; } Step = FMath::Max(1, Step); const int32 CPA = FMath::Clamp(CellsPerAxis, 2, CHUNK_SIZE); // Keep the integer arithmetic that defines the MC lattice identical to ClassifyTile, but // reject a caller whose endpoint would overflow before any floor division or FBox creation. const int64 MaxX64 = static_cast(OriginVoxels.X) + static_cast(CPA) * static_cast(Step); const int64 MaxY64 = static_cast(OriginVoxels.Y) + static_cast(CPA) * static_cast(Step); const int64 MaxZ64 = static_cast(OriginVoxels.Z) + static_cast(CPA) * static_cast(Step); if (MaxX64 < MIN_int32 || MaxX64 > MAX_int32 || MaxY64 < MIN_int32 || MaxY64 > MAX_int32 || MaxZ64 < MIN_int32 || MaxZ64 > MAX_int32) { return false; } const int32 MinX = OriginVoxels.X; const int32 MinY = OriginVoxels.Y; const int32 MinZ = OriginVoxels.Z; const int32 MaxX = static_cast(MaxX64); const int32 MaxY = static_cast(MaxY64); const int32 MaxZ = static_cast(MaxZ64); const FBox TileVoxelBox( FVector(static_cast(MinX), static_cast(MinY), static_cast(MinZ)), FVector(static_cast(MaxX), static_cast(MaxY), static_cast(MaxZ))); auto FloorDivC = [](int32 A, int32 B) -> int32 { const int32 Q = A / B; const int32 R = A % B; return (R != 0 && ((R < 0) != (B < 0))) ? Q - 1 : Q; }; // First pass is deliberately Z-only. Most streaming tiles are neither gap nor seal, so they // leave in this cheap loop without taking a diff lock, scanning passages, or copying params. enum : uint8 { GapRow = 1u, SealRow = 2u }; int32 SampleChunkZ[CHUNK_SIZE + 1]; uint8 SampleKind[CHUNK_SIZE + 1]; bool bSawSeal = false; bool bSawSolidRegion = false; for (int32 G = 0; G <= CPA; ++G) { const int64 Z64 = static_cast(OriginVoxels.Z) + static_cast(G) * static_cast(Step); const int32 Z = static_cast(Z64); const int32 ChunkZ = FloorDivC(Z, CHUNK_SIZE); SampleChunkZ[G] = ChunkZ; if (Manager->IsGapChunk(FIntVector(0, 0, ChunkZ))) { SampleKind[G] = GapRow; bSawSolidRegion = true; continue; } int32 UnusedTopChunkZ = 0; int32 UnusedBottomChunkZ = 0; if (!Manager->GetStrateChunkZBounds( ChunkZ, UnusedTopChunkZ, UnusedBottomChunkZ)) { // Above/below the stack is open air, not a solid gap. A tile that touches it is not // a sealed-solid candidate even if another row happens to be bedrock. return false; } SampleKind[G] = SealRow; bSawSeal = true; bSawSolidRegion = true; } if (!bSawSolidRegion) { return false; } if (bSawSeal && VoxelDensityAblation::IsBoundarySealOff()) { return false; } // Player edits are the final writer and can carve a tile that was structurally solid. This // is intentionally checked only after the Z pre-pass: ordinary mixed tiles avoid the lock. const int32 MinChunkX = FloorDivC(MinX, CHUNK_SIZE); const int32 MaxChunkX = FloorDivC(MaxX, CHUNK_SIZE); const int32 MinChunkY = FloorDivC(MinY, CHUNK_SIZE); const int32 MaxChunkY = FloorDivC(MaxY, CHUNK_SIZE); const int32 MinChunkZ = FloorDivC(MinZ, CHUNK_SIZE); const int32 MaxChunkZ = FloorDivC(MaxZ, CHUNK_SIZE); if (!VF_SealedSolidProofGuardDisabled( DisabledGuards, EVoxelSealedSolidProofGuard::DiffLayer) && Generator.DiffLayer != nullptr && Generator.DiffLayer->HasAnyMods()) { if (Generator.DiffLayer->HasAnyModInChunkRange( FIntVector(MinChunkX, MinChunkY, MinChunkZ), FIntVector(MaxChunkX, MaxChunkY, MaxChunkZ))) { return false; } } // PassageCarving's public box guard includes the 4-voxel carve blend pad on top of each // generated passage bound. Landing and structural-post air are also handled conservatively: // if carving is ablated but another passage air writer remains active, reject any passage data // rather than trusting an API whose historical early-out is specific to carving. if (!VF_SealedSolidProofGuardDisabled( DisabledGuards, EVoxelSealedSolidProofGuard::Passages)) { const bool bPassageAirWriterActive = !VoxelDensityAblation::IsPassageCarvingOff() || !VoxelDensityAblation::IsLandingPostsOff() || !VoxelDensityAblation::IsPassageStructuralPostsOff(); if (bPassageAirWriterActive) { if (!VF_SealedSolidProofPassageInputsAreValid(*Manager)) { return false; } if (!VoxelDensityAblation::IsPassageCarvingOff()) { if (Manager->AnyPassageNearBox( TileVoxelBox.Min, TileVoxelBox.Max)) { return false; } } else if (Manager->GetPassages().Num() > 0) { // AnyPassageNearBox deliberately returns false when the carve stage is off. The // remaining landing/tunnel posts have no equivalent box API, so this is the // capability-first answer under that development ablation. return false; } } } // The origin landing is the (0,0) spine's current room-shaped air writer. Its box guard is // broader than the exact lattice guard used by ClassifyTile; false Mixed is cheap here. if (!VF_SealedSolidProofGuardDisabled( DisabledGuards, EVoxelSealedSolidProofGuard::OriginLanding) && (!VoxelDensityAblation::IsOriginSpineOff() || !VoxelDensityAblation::IsLandingPostsOff()) && Manager->AnyOriginLandingNearBox(TileVoxelBox.Min, TileVoxelBox.Max)) { return false; } const int64 XYChunkCount = (static_cast(MaxChunkX) - static_cast(MinChunkX) + 1) * (static_cast(MaxChunkY) - static_cast(MinChunkY) + 1); const int64 ZChunkCount = static_cast(MaxChunkZ) - static_cast(MinChunkZ) + 1; // This is a per-tile proof, not a second coarse mesher. A normal LOD0/LOD1 tile touches at // most a handful of chunks. Refuse pathological caller boxes before a nested loop can turn // the cheap path into a large param walk. constexpr int64 MaxProofChunkCoordinates = 65536; if (XYChunkCount <= 0 || ZChunkCount <= 0 || XYChunkCount > MaxProofChunkCoordinates || XYChunkCount * ZChunkCount > MaxProofChunkCoordinates) { return false; } auto ResolveSealFields = [&](const FIntVector& ChunkCoord, float& OutTop, float& OutBottom, float& OutThickness, float& OutBase) -> bool { switch (Manager->GetGeneratorTypeForChunk(ChunkCoord)) { case ECaveGeneratorType::FlatPlain: case ECaveGeneratorType::CrystalChamber: { const FSlabGenerationParams P = Manager->GetSlabParamsForChunk(ChunkCoord); OutTop = P.StrateTopWorldZ; OutBottom = P.StrateBottomWorldZ; OutThickness = P.BoundarySealThickness; OutBase = P.BaseDensity; return true; } case ECaveGeneratorType::Maze: { const FMazeGenerationParams P = Manager->GetMazeParamsForChunk(ChunkCoord); OutTop = P.StrateTopWorldZ; OutBottom = P.StrateBottomWorldZ; OutThickness = P.BoundarySealThickness; OutBase = P.BaseDensity; return true; } case ECaveGeneratorType::SurfaceWorld: { const FSurfaceGenerationParams P = Manager->GetSurfaceParamsForChunk(ChunkCoord); OutTop = P.StrateTopWorldZ; OutBottom = P.StrateBottomWorldZ; OutThickness = P.BoundarySealThickness; OutBase = P.BaseDensity; return true; } case ECaveGeneratorType::VerticalShafts: { const FVerticalShaftParams P = Manager->GetVerticalShaftParamsForChunk(ChunkCoord); OutTop = P.StrateTopWorldZ; OutBottom = P.StrateBottomWorldZ; OutThickness = P.BoundarySealThickness; OutBase = P.BaseDensity; return true; } case ECaveGeneratorType::FloatingIslands: { const FFloatingIslandParams P = Manager->GetFloatingIslandParamsForChunk(ChunkCoord); OutTop = P.StrateTopWorldZ; OutBottom = P.StrateBottomWorldZ; OutThickness = P.BoundarySealThickness; OutBase = P.BaseDensity; return true; } case ECaveGeneratorType::TunnelNetwork: case ECaveGeneratorType::Underwater: { const FStrateGenerationParams P = Manager->GetGenerationParams(ChunkCoord); OutTop = P.StrateTopWorldZ; OutBottom = P.StrateBottomWorldZ; OutThickness = P.BoundarySealThickness; OutBase = P.BaseDensity; return true; } default: return false; } }; for (int32 ChunkZ = MinChunkZ; ChunkZ <= MaxChunkZ; ++ChunkZ) { bool bThisChunkHasSealRow = false; for (int32 G = 0; G <= CPA; ++G) { if (SampleKind[G] == SealRow && SampleChunkZ[G] == ChunkZ) { bThisChunkHasSealRow = true; break; } } if (!bThisChunkHasSealRow) { continue; } for (int32 ChunkY = MinChunkY; ChunkY <= MaxChunkY; ++ChunkY) for (int32 ChunkX = MinChunkX; ChunkX <= MaxChunkX; ++ChunkX) { const FIntVector ChunkCoord(ChunkX, ChunkY, ChunkZ); #if WITH_EDITOR // A plain composer override changes the archetype/params, while a region override // changes the lateral field. Both must remain on the ordinary mesher/classifier path. int32 ComposerSeed = 0; ECaveGeneratorType ComposerArchetype = ECaveGeneratorType::TunnelNetwork; FVoxelStrateArchetypeParams ComposerParams; bool bComposerRecipe = false; FVoxelOpStackRecipe ComposerRecipe; if (Manager->GetComposerOverrideForChunk( ChunkCoord, ComposerSeed, ComposerArchetype, ComposerParams, bComposerRecipe, ComposerRecipe)) { return false; } FVoxelStrateRegionManifest ComposerRegions; if (Manager->GetComposerRegionOverrideForChunk(ChunkCoord, ComposerRegions)) { return false; } #endif // Cooked seasons can materialise a custom recipe for a chunk. The cheap proof only // knows the native structural seal contract, so selected recipes fail closed. int32 RecipeSeed = 0; ECaveGeneratorType RecipeArchetype = ECaveGeneratorType::TunnelNetwork; FVoxelStrateArchetypeParams RecipeParams; FVoxelOpStackRecipe Recipe; if (Manager->GetRecipeForChunk( ChunkCoord, RecipeSeed, RecipeArchetype, RecipeParams, Recipe)) { return false; } float Top = 0.0f; float Bottom = 0.0f; float Thickness = 0.0f; float BaseDensity = 0.0f; if (!ResolveSealFields(ChunkCoord, Top, Bottom, Thickness, BaseDensity)) { return false; } if (!VF_SealedSolidProofGuardDisabled( DisabledGuards, EVoxelSealedSolidProofGuard::DisturbanceChasms) && !VoxelDensityAblation::IsDisturbancesOff()) { const FStrateDisturbanceParams D = Manager->GetDisturbanceParamsForChunk(ChunkCoord); if (VF_AnyChasmCanTouchBox(TileVoxelBox, D, static_cast(Generator.Seed))) { return false; } } for (int32 G = 0; G <= CPA; ++G) { if (SampleKind[G] != SealRow || SampleChunkZ[G] != ChunkZ) { continue; } const float Z = static_cast( static_cast(OriginVoxels.Z) + static_cast(G) * static_cast(Step)); if (!VF_IsSafeBoundarySealLatticeZ( Z, Top, Bottom, Thickness, BaseDensity)) { return false; } } } } return true; } static bool VF_TryProveOutOfLayoutAirTile( const UVoxelGenerator& Generator, const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis) { const UVoxelStrateManager* Manager = Generator.StrateManager; if (Manager == nullptr) { return false; } Step = FMath::Max(1, Step); const int32 CPA = FMath::Clamp(CellsPerAxis, 2, CHUNK_SIZE); const int64 MaxX64 = static_cast(OriginVoxels.X) + static_cast(CPA) * static_cast(Step); const int64 MaxY64 = static_cast(OriginVoxels.Y) + static_cast(CPA) * static_cast(Step); const int64 MaxZ64 = static_cast(OriginVoxels.Z) + static_cast(CPA) * static_cast(Step); if (MaxX64 < MIN_int32 || MaxX64 > MAX_int32 || MaxY64 < MIN_int32 || MaxY64 > MAX_int32 || MaxZ64 < MIN_int32 || MaxZ64 > MAX_int32) { return false; } const int32 MinX = OriginVoxels.X; const int32 MinY = OriginVoxels.Y; const int32 MinZ = OriginVoxels.Z; const int32 MaxX = static_cast(MaxX64); const int32 MaxY = static_cast(MaxY64); const int32 MaxZ = static_cast(MaxZ64); const FBox TileVoxelBox( FVector(static_cast(MinX), static_cast(MinY), static_cast(MinZ)), FVector(static_cast(MaxX), static_cast(MaxY), static_cast(MaxZ))); auto FloorDivC = [](int32 A, int32 B) -> int32 { const int32 Q = A / B; const int32 R = A % B; return (R != 0 && ((R < 0) != (B < 0))) ? Q - 1 : Q; }; // All core lattice rows must be outside the layout. A gap or a real strate row means that // this is no longer the constant-air case, even when another row is open air. for (int32 G = 0; G <= CPA; ++G) { const int64 Z64 = static_cast(OriginVoxels.Z) + static_cast(G) * static_cast(Step); const int32 ChunkZ = FloorDivC(static_cast(Z64), CHUNK_SIZE); if (Manager->IsGapChunk(FIntVector(0, 0, ChunkZ))) { return false; } int32 UnusedTopChunkZ = 0; int32 UnusedBottomChunkZ = 0; if (Manager->GetStrateChunkZBounds( ChunkZ, UnusedTopChunkZ, UnusedBottomChunkZ)) { return false; } } const int32 MinChunkX = FloorDivC(MinX, CHUNK_SIZE); const int32 MaxChunkX = FloorDivC(MaxX, CHUNK_SIZE); const int32 MinChunkY = FloorDivC(MinY, CHUNK_SIZE); const int32 MaxChunkY = FloorDivC(MaxY, CHUNK_SIZE); const int32 MinChunkZ = FloorDivC(MinZ, CHUNK_SIZE); const int32 MaxChunkZ = FloorDivC(MaxZ, CHUNK_SIZE); if (Generator.DiffLayer != nullptr && Generator.DiffLayer->HasAnyMods() && Generator.DiffLayer->HasAnyModInChunkRange( FIntVector(MinChunkX, MinChunkY, MinChunkZ), FIntVector(MaxChunkX, MaxChunkY, MaxChunkZ))) { return false; } // The final XY pass can add rock to open air. Radius == 0 (and other non-positive inputs) is // the documented no-op, so only validate and inspect the band when that writer is live. if (!VoxelDensityAblation::IsXYEdgeSealOff() && Generator.WorldRadiusVoxels > 0.0f && Generator.EdgeSealThickness > 0.0f) { if (!VF_IsValidXYEdgeSealProofInput( TileVoxelBox, Generator.WorldRadiusVoxels, Generator.EdgeSealThickness, 1.0f)) { return false; } // Use double arithmetic here: the production helper is float-based, while this proof // must not turn a large finite coordinate into an overflowed squared-distance false // negative. Equality is safe because the production inner test is <= as well. const double FarthestX = FMath::Max( FMath::Abs(static_cast(TileVoxelBox.Min.X)), FMath::Abs(static_cast(TileVoxelBox.Max.X))); const double FarthestY = FMath::Max( FMath::Abs(static_cast(TileVoxelBox.Min.Y)), FMath::Abs(static_cast(TileVoxelBox.Max.Y))); const double InnerRadius = static_cast(Generator.WorldRadiusVoxels) - static_cast(Generator.EdgeSealThickness); if (!(InnerRadius > 0.0) || FarthestX * FarthestX + FarthestY * FarthestY > InnerRadius * InnerRadius) { return false; } } if (Manager->GetPassages().Num() > 0) { if (!VF_SealedSolidProofPassageInputsAreValid(*Manager)) { return false; } // Passage carving is air-monotone and therefore cannot invalidate an AllAir proof. Only // the solid floor/support writers need a guard here. Use the same exact lattice floor // predicate as ClassifyTile: its padded AABBs stay cheap, but do not reject an open-air // tile merely because a harmless carve bound passes near it. const bool bPassageFloorWriterActive = !VoxelDensityAblation::IsPassageStructuralPostsOff() || !VoxelDensityAblation::IsNativeFloorOff() || !VoxelDensityAblation::IsLandingPostsOff(); if (bPassageFloorWriterActive && Manager->AnyPassageLandingFloorNearLattice( TileVoxelBox, OriginVoxels, Step)) { return false; } } // Origin rooms only carve open air, but their landing floors reassert solid. The manager's // floor box includes every per-strate room; this is intentionally broader than the exact // lattice query used by ClassifyTile because a false Mixed is cheaper than a dropped floor. if (!VoxelDensityAblation::IsOriginSpineOff() || !VoxelDensityAblation::IsLandingPostsOff()) { if (!VoxelMath::IsFinite(Generator.OriginSpineRadius) || Generator.OriginSpineRadius <= 0.0f) { return false; } if (Manager->AnyOriginLandingFloorNearLattice( TileVoxelBox, OriginVoxels, Step)) { return false; } } if (!VoxelDensityAblation::IsDisturbancesOff()) { const int64 XYChunkCount = (static_cast(MaxChunkX) - static_cast(MinChunkX) + 1) * (static_cast(MaxChunkY) - static_cast(MinChunkY) + 1); const int64 ZChunkCount = static_cast(MaxChunkZ) - static_cast(MinChunkZ) + 1; constexpr int64 MaxProofChunkCoordinates = 65536; if (XYChunkCount <= 0 || ZChunkCount <= 0 || XYChunkCount > MaxProofChunkCoordinates || XYChunkCount * ZChunkCount > MaxProofChunkCoordinates) { return false; } // Out-of-layout chunks normally return disabled disturbance params. Still ask the exact // bridge/ridge predicates for every touched chunk so a future out-of-layout decoration // cannot silently turn this air proof into an invisible solid patch. for (int32 ChunkZ = MinChunkZ; ChunkZ <= MaxChunkZ; ++ChunkZ) for (int32 ChunkY = MinChunkY; ChunkY <= MaxChunkY; ++ChunkY) for (int32 ChunkX = MinChunkX; ChunkX <= MaxChunkX; ++ChunkX) { const FStrateDisturbanceParams D = Manager->GetDisturbanceParamsForChunk(FIntVector(ChunkX, ChunkY, ChunkZ)); if (VF_AnyBridgeCanTouchBox(TileVoxelBox, D, static_cast(Generator.Seed)) || VF_AnyRidgeCanTouchBox(TileVoxelBox, D, static_cast(Generator.Seed))) { return false; } } } return true; } //============================================================================= // LE MAPPING « ARCHÉTYPE → PILE D’OPÉRATEURS » — UNE SEULE DÉFINITION //============================================================================= // ⚠️ EXTRAIT DE `GetDensityAt` PARCE QUE `ClassifyTile` EN A BESOIN AUSSI, ET QU'UNE DEUXIÈME COPIE // SERAIT LA PIRE FORME DE BUG DISPONIBLE ICI. // // `ClassifyTile` décide si une tuile est maillée DU TOUT. Si son verdict venait d'une pile // construite autrement que celle qui produit la densité — ne serait-ce qu'un paramètre de // construction différent — la tuile serait sautée sur la foi d'un champ qui n'est pas celui que le // mesher aurait vu. C'est-à-dire un TROU : pas de géométrie, pas de collision, invisible. // Un commentaire « garder les deux en phase » n'aurait pas suffi ; il fallait qu'il n'y en ait // qu'une. // // Les params ne sont PAS cherchés ici : les deux appelants les ont déjà (le chemin densité les tient // dans ses `CP_*`, le classifieur les cherche pour son slot). On ne passe que des pointeurs. // // ONE definition of the archetype → stack mapping, because ClassifyTile decides whether a tile is // meshed at all: a verdict from a differently-built stack would be a hole. Params are passed in, // never fetched here — both callers already have them. namespace { // Une identité monotone évite qu'un worker réutilise les CP_* d'un monde détruit même si // l'allocateur UObject recycle plus tard la même adresse. Relaxed suffit : on ne publie aucune // donnée, on alloue seulement une valeur distincte par instance. // A monotonic identity prevents stale CP_* reuse even if UObject allocation later recycles an // address. Relaxed ordering is sufficient: this allocates uniqueness, it publishes no data. std::atomic GNextDensityCacheOwnerId { 0 }; struct FTunnelCoreCacheState { FChunkSDFCache Cache; uint64 OwnerId = 0; uint64 ManagerLifetimeId = 0; FIntVector Chunk = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX); FIntVector TileOrigin = FIntVector::ZeroValue; int32 TileStep = 0; int32 TileCells = 0; int32 StrateIndex = INT32_MIN; uint32 WorldSeed = 0xFFFFFFFFu; uint32 ParamsFingerprint = 0xFFFFFFFFu; uint32 LayoutVersion = 0xFFFFFFFFu; bool bUsesTileCacheWindow = false; bool bValid = false; }; static thread_local FTunnelCoreCacheState GTunnelCoreCache; // A mesher tile samples an expanded grid (-1..Cells+1), so one worker visits several exact // chunk coordinates even while it is generating one tile. The old CP_* cache retained only // the last coordinate and consequently rebuilt the whole TunnelNetwork stack whenever the // sampling cursor crossed a chunk boundary. Keep a deterministic direct-mapped set of states // instead: collisions only cause a safe rebuild, never a stale read or an order-dependent // result. Native TunnelNetwork/Underwater states are immutable after construction; the active // pointer below lets the hot sample path read the stored stack and tunnel-core cache without // moving them on every voxel. // The cache is open-addressed and bounded. Keep the worker footprint at the established // 128-entry cap; the expensive native tunnel-core cache is not prepared for an operator-stack // chunk because the stack publishes the same result to the common post below. constexpr int32 TunnelDensityCacheSlotCount = 128; struct FTunnelNetworkDensityCacheEntry { bool bValid = false; uint64 OwnerId = 0; uint64 ManagerLifetimeId = 0; FIntVector Chunk = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX); uint32 LayoutVersion = 0xFFFFFFFFu; ECaveGeneratorType GenType = ECaveGeneratorType::TunnelNetwork; FStrateGenerationParams Tunnel; uint32 TunnelFingerprint = 0xFFFFFFFFu; FStrateDisturbanceParams Disturbance; bool bUseOpStack = false; bool bUsesTileCacheWindow = false; FVoxelOpStack OpStack; FTunnelCoreCacheState TunnelCore; }; void ReportTunnelDensityCacheFootprint( const FTunnelNetworkDensityCacheEntry* Entries) { if (!VoxelDensityProfile::IsEnabled() || Entries == nullptr) { return; } uint64 DynamicBytes = 0; uint64 EntryBytes = 0; uint64 LargestEntryBytes = 0; uint64 ValidEntries = 0; VoxelDensityProfile::FCacheMemoryBreakdown Breakdown; for (int32 Index = 0; Index < TunnelDensityCacheSlotCount; ++Index) { const FTunnelNetworkDensityCacheEntry& Entry = Entries[Index]; if (!Entry.bValid) { continue; } ++ValidEntries; const uint64 StackBytes = static_cast(Entry.OpStack.GetAllocatedSize()); const uint64 TunnelCoreBytes = static_cast( Entry.TunnelCore.Cache.GetAllocatedSize()); Breakdown.OpStackBytes += StackBytes; Breakdown += Entry.TunnelCore.Cache.GetAllocatedSizeBreakdown(); const uint64 EntryDynamicBytes = StackBytes + TunnelCoreBytes; const uint64 FullEntryBytes = static_cast( sizeof(FTunnelNetworkDensityCacheEntry)) + EntryDynamicBytes; DynamicBytes += EntryDynamicBytes; EntryBytes += FullEntryBytes; LargestEntryBytes = FMath::Max(LargestEntryBytes, FullEntryBytes); } Breakdown.SlotStorageBytes = static_cast(sizeof(FTunnelNetworkDensityCacheEntry)) * TunnelDensityCacheSlotCount; VoxelDensityProfile::SetWorkerTunnelCacheFootprint( TunnelDensityCacheSlotCount, ValidEntries, static_cast(sizeof(FTunnelNetworkDensityCacheEntry)) * TunnelDensityCacheSlotCount, DynamicBytes, EntryBytes, LargestEntryBytes, ValidEntries, Breakdown); } FORCEINLINE int32 TunnelDensityCacheSlot(const FIntVector& Chunk) { uint32 Hash = static_cast(Chunk.X) * 0x9E3779B9u; Hash ^= static_cast(Chunk.Y) * 0x85EBCA6Bu; Hash ^= static_cast(Chunk.Z) * 0xC2B2AE35u; Hash ^= Hash >> 16; return static_cast(Hash & (TunnelDensityCacheSlotCount - 1)); } static void UpdateTunnelCoreTileReach( const FChunkSDFCache& Cache, bool bUseTileCacheWindow) { if (!bUseTileCacheWindow || !VoxelGenLOD::bTileReachDiagnosticTile || VoxelGenLOD::TileCellsPerAxis <= 0) { return; } const int32 Step = FMath::Max(VoxelGenLOD::GetThreadSampleStep(), 1); const int64 Extent = static_cast(VoxelGenLOD::TileCellsPerAxis + 1) * static_cast(Step); if (Extent <= 0 || Extent > MAX_int32) { VoxelGenLOD::RecordTunnelCoreReachDecision(true); return; } const FIntVector& Origin = VoxelGenLOD::TileOriginVoxels; const FBox LatticeWithHalo( FVector( static_cast(static_cast(Origin.X) - Step), static_cast(static_cast(Origin.Y) - Step), static_cast(static_cast(Origin.Z) - Step)), FVector( static_cast(static_cast(Origin.X) + Extent), static_cast(static_cast(Origin.Y) + Extent), static_cast(static_cast(Origin.Z) + Extent))); const bool bNear = VoxelCaveMorphology::AnyTunnelCoreWorldNearLattice( Cache, LatticeWithHalo, VoxelGenLOD::TileReachScale); if (bNear) { VoxelGenLOD::TilePostReachFlags |= VoxelGenLOD::TunnelCoreReachable; } else { VoxelGenLOD::TilePostReachFlags &= ~VoxelGenLOD::TunnelCoreReachable; } VoxelGenLOD::bTileCoreReachProofEnabled = true; VoxelGenLOD::RecordTunnelCoreReachDecision(bNear); } void PrepareTunnelCoreCache( const UVoxelStrateManager& Manager, const FIntVector& ChunkCoord, const FStrateGenerationParams& Params, uint32 ParamsFingerprint, uint32 LayoutVersion, uint64 ManagerLifetimeId, uint64 OwnerId, FTunnelCoreCacheState& OutState) { FIntVector RequestedTileOrigin = FIntVector::ZeroValue; int32 RequestedTileStep = 1; int32 RequestedTileCells = 0; const TArray* TerrainOps = nullptr; if (UVoxelStrateDefinition* Def = Manager.GetStrateForChunk(ChunkCoord)) { TerrainOps = &Def->TerrainOperations; } const bool bUseTileCacheWindow = VoxelGenLOD::IsTileCacheWindowEnabled(true) && VoxelGenLOD::GetThreadTileCacheWindow( RequestedTileOrigin, RequestedTileStep, RequestedTileCells) && VoxelCaveMorphology::IsRoomGraphWindowInvariant(Params, TerrainOps); const bool bUseSpatialIndex = VoxelGenLOD::ShouldUseSpatialIndex(true); const int32 StrateIndex = Manager.GetStrateIndex( (static_cast(ChunkCoord.Z) + 0.5f) * static_cast(CHUNK_SIZE) * VOXEL_SIZE); const uint32 WorldSeed = static_cast(Manager.GetWorldSeed()); // A tile-window cache contains the complete graph reach for this tile. Chunk-local // caches deliberately leave the core bit conservative; updating it there would make the // first chunk's false result hide a tunnel that enters through a later cache window. if (bUseTileCacheWindow && OutState.bValid) { UpdateTunnelCoreTileReach(OutState.Cache, bUseTileCacheWindow); } const bool bSameGraphWindow = OutState.bValid && OutState.OwnerId == OwnerId && OutState.ManagerLifetimeId == ManagerLifetimeId && OutState.StrateIndex == StrateIndex && OutState.WorldSeed == WorldSeed && OutState.ParamsFingerprint == ParamsFingerprint && OutState.LayoutVersion == LayoutVersion && OutState.bUsesTileCacheWindow == bUseTileCacheWindow && (!bUseTileCacheWindow ? OutState.Chunk == ChunkCoord : (OutState.TileOrigin == RequestedTileOrigin && OutState.TileStep == RequestedTileStep && OutState.TileCells == RequestedTileCells)); if (bSameGraphWindow) { OutState.Chunk = ChunkCoord; return; } const float ChunkMinX = static_cast(ChunkCoord.X * CHUNK_SIZE); const float ChunkMinY = static_cast(ChunkCoord.Y * CHUNK_SIZE); const float ChunkMaxX = ChunkMinX + static_cast(CHUNK_SIZE); const float ChunkMaxY = ChunkMinY + static_cast(CHUNK_SIZE); const float WarpMargin = FMath::Abs(Params.CaveWarpStrength) * VOXEL_NOISE_SCALE * 1.5f + 2.0f; float SearchMinX = ChunkMinX - WarpMargin; float SearchMinY = ChunkMinY - WarpMargin; float SearchMaxX = ChunkMaxX + WarpMargin; float SearchMaxY = ChunkMaxY + WarpMargin; if (bUseTileCacheWindow) { const float TileMinX = static_cast(RequestedTileOrigin.X); const float TileMinY = static_cast(RequestedTileOrigin.Y); const float TileExtent = static_cast( static_cast(RequestedTileStep) * static_cast(RequestedTileCells)); const float TileHalo = static_cast(RequestedTileStep) + 2.0f; const float Expansion = FMath::Abs(Params.CaveWarpStrength) * VOXEL_NOISE_SCALE * 1.5f + TileHalo; SearchMinX = TileMinX - Expansion; SearchMinY = TileMinY - Expansion; SearchMaxX = TileMinX + TileExtent + Expansion; SearchMaxY = TileMinY + TileExtent + Expansion; } VoxelCaveMorphology::BuildChunkCache( OutState.Cache, SearchMinX, SearchMinY, SearchMaxX, SearchMaxY, Params, WorldSeed, StrateIndex, nullptr, ERoomGraphBuildSite::GeneratorTunnelCore); UpdateTunnelCoreTileReach(OutState.Cache, bUseTileCacheWindow); OutState.OwnerId = OwnerId; OutState.ManagerLifetimeId = ManagerLifetimeId; OutState.Chunk = ChunkCoord; OutState.TileOrigin = bUseTileCacheWindow ? RequestedTileOrigin : FIntVector::ZeroValue; OutState.TileStep = bUseTileCacheWindow ? RequestedTileStep : 0; OutState.TileCells = bUseTileCacheWindow ? RequestedTileCells : 0; OutState.StrateIndex = StrateIndex; OutState.WorldSeed = WorldSeed; OutState.ParamsFingerprint = ParamsFingerprint; OutState.LayoutVersion = LayoutVersion; OutState.bUsesTileCacheWindow = bUseTileCacheWindow; OutState.bValid = true; } struct FVoxelStackParamRefs { const FSlabGenerationParams* Slab = nullptr; const FMazeGenerationParams* Maze = nullptr; const FVerticalShaftParams* Vert = nullptr; const FFloatingIslandParams* Float = nullptr; const FStrateGenerationParams* Tunnel = nullptr; // SurfaceWorld uniquement. `Surface == nullptr` ⇒ la fabrique REFUSE cet archétype, ce qui // est exactement ce que veut `ClassifyTile` : il prouve SurfaceWorld lui-même, sur le // treillis exact du mesher, et n'a aucune raison de passer par la pile pour ça. const FSurfaceGenerationParams* Surface = nullptr; const TArray* SurfaceBiomeParams = nullptr; TUniquePtr BiomeField; }; /** * Construit la pile de cet archétype dans `OutStack` et remplit les bornes Z de `OutCtx`. * * @return false quand la pile NE DOIT PAS être utilisée — archétype inconnu, params absents, * ou **strate dégénérée** (hauteur nulle). La pile n'a pas d'early-out pour ce dernier * cas, par conception : l'appelant rend de l'air (`GetDensityAt`) ou refuse de prouver * la tuile (`ClassifyTile`). (`GetDensityWithParams` n'a aucun early-out de ce genre : * TunnelNetwork/Underwater n'ont donc pas cette garde.) */ bool VF_BuildOpStackForChunk(ECaveGeneratorType Type, FVoxelStackParamRefs& Refs, int32 Seed, float SpineRadius, const UVoxelStrateManager* SM, FVoxelOpStack& OutStack, FVoxelOpContext& OutCtx) { switch (Type) { case ECaveGeneratorType::Maze: if (!Refs.Maze) { return false; } if (Refs.Maze->StrateTopWorldZ - Refs.Maze->StrateBottomWorldZ <= 0.0f) { return false; } OutCtx.StrateTopWorldZ = Refs.Maze->StrateTopWorldZ; OutCtx.StrateBottomWorldZ = Refs.Maze->StrateBottomWorldZ; VoxelDensityOps::BuildMazeStack(OutStack, *Refs.Maze, Seed, SpineRadius, SM); return true; case ECaveGeneratorType::FlatPlain: case ECaveGeneratorType::CrystalChamber: // UN SEUL cas pour les deux : ils ne diffèrent que par leurs valeurs par défaut. // Voir BuildSlabStack. if (!Refs.Slab) { return false; } if (Refs.Slab->StrateTopWorldZ - Refs.Slab->StrateBottomWorldZ <= 0.0f) { return false; } OutCtx.StrateTopWorldZ = Refs.Slab->StrateTopWorldZ; OutCtx.StrateBottomWorldZ = Refs.Slab->StrateBottomWorldZ; VoxelDensityOps::BuildSlabStack(OutStack, *Refs.Slab, Seed, SpineRadius, SM); return true; case ECaveGeneratorType::SurfaceWorld: if (!Refs.Surface) { return false; } if (Refs.Surface->StrateTopWorldZ - Refs.Surface->StrateBottomWorldZ <= 0.0f) { return false; } OutCtx.StrateTopWorldZ = Refs.Surface->StrateTopWorldZ; OutCtx.StrateBottomWorldZ = Refs.Surface->StrateBottomWorldZ; VoxelDensityOps::BuildSurfaceStack( OutStack, *Refs.Surface, Seed, SpineRadius, SM, Refs.SurfaceBiomeParams ? *Refs.SurfaceBiomeParams : TArray(), MoveTemp(Refs.BiomeField)); return true; case ECaveGeneratorType::VerticalShafts: if (!Refs.Vert) { return false; } if (Refs.Vert->StrateTopWorldZ - Refs.Vert->StrateBottomWorldZ <= 0.0f) { return false; } OutCtx.StrateTopWorldZ = Refs.Vert->StrateTopWorldZ; OutCtx.StrateBottomWorldZ = Refs.Vert->StrateBottomWorldZ; VoxelDensityOps::BuildVerticalShaftStack(OutStack, *Refs.Vert, Seed, SpineRadius, SM); return true; case ECaveGeneratorType::FloatingIslands: if (!Refs.Float) { return false; } if (Refs.Float->StrateTopWorldZ - Refs.Float->StrateBottomWorldZ <= 0.0f) { return false; } OutCtx.StrateTopWorldZ = Refs.Float->StrateTopWorldZ; OutCtx.StrateBottomWorldZ = Refs.Float->StrateBottomWorldZ; VoxelDensityOps::BuildFloatingIslandStack(OutStack, *Refs.Float, Seed, SpineRadius, SM); return true; case ECaveGeneratorType::Underwater: case ECaveGeneratorType::TunnelNetwork: // Underwater EST TunnelNetwork plus un drapeau d'eau consommé côté rendu. Pas de garde // de strate dégénérée : `GetDensityWithParams` n'a pas d'early-out à reproduire. if (!Refs.Tunnel) { return false; } OutCtx.StrateTopWorldZ = Refs.Tunnel->StrateTopWorldZ; OutCtx.StrateBottomWorldZ = Refs.Tunnel->StrateBottomWorldZ; VoxelDensityOps::BuildTunnelNetworkStack( OutStack, *Refs.Tunnel, Seed, SpineRadius, SM, /*bAppendStructuralPosts=*/true); return true; default: // `UsesOperatorStackForChunk` ne rend true que pour les 8 archétypes, donc on ne devrait // jamais arriver ici (valeur d'enum invalide). return false; } } } #if WITH_EDITOR bool VF_BuildNativeStrateStackForCandidate( ECaveGeneratorType Archetype, const FVoxelStrateArchetypeParams& Params, int32 InSeed, float SpineRadius, float InWorldRadiusVoxels, float InEdgeSealThickness, const UVoxelStrateManager* StrateManager, FVoxelOpStack& OutStack, FVoxelOpContext& OutContext) { FVoxelStackParamRefs Refs; switch (Archetype) { case ECaveGeneratorType::FlatPlain: case ECaveGeneratorType::CrystalChamber: Refs.Slab = &Params.SlabParams; break; case ECaveGeneratorType::Maze: Refs.Maze = &Params.MazeParams; break; case ECaveGeneratorType::SurfaceWorld: // The live surface path can additionally carry biome-specific parameter arrays and a // generator-owned biome field. The editor sanity check deliberately handles the exact // no-biome/native case only; the caller reports a biome target as unavailable rather than // comparing unlike fields. Refs.Surface = &Params.SurfaceParams; break; case ECaveGeneratorType::VerticalShafts: Refs.Vert = &Params.VerticalShaftParams; break; case ECaveGeneratorType::FloatingIslands: Refs.Float = &Params.FloatingIslandParams; break; case ECaveGeneratorType::Underwater: case ECaveGeneratorType::TunnelNetwork: Refs.Tunnel = &Params.TunnelNetworkParams; break; default: return false; } OutContext = FVoxelOpContext(); OutContext.Seed = static_cast(InSeed); OutContext.LayoutVersion = StrateManager != nullptr ? StrateManager->GetLayoutVersion() : 0; OutContext.WorldRadiusVoxels = InWorldRadiusVoxels; OutContext.EdgeSealThickness = InEdgeSealThickness; if (!VF_BuildOpStackForChunk(Archetype, Refs, InSeed, SpineRadius, StrateManager, OutStack, OutContext)) { return false; } return true; } #endif UVoxelGenerator::UVoxelGenerator() : DensityCacheOwnerId(GNextDensityCacheOwnerId.fetch_add(1, std::memory_order_relaxed) + 1) { } void UVoxelGenerator::InitializeSettings(const UVoxelSettings* Settings) { // Les paramètres globaux sont copiés ici une seule fois : le chemin voxel ne doit pas // déréférencer l'asset de settings. Seed = Settings ? Settings->GetEffectiveWorldSeed() : 0; OriginSpineRadius = Settings ? Settings->GetEffectiveOriginSpineRadius() : 28.0f; WorldRadiusVoxels = Settings ? Settings->GetEffectiveWorldRadiusVoxels() : 0.0f; EdgeSealThickness = Settings ? Settings->EdgeSealThickness : 128.0f; } void UVoxelGenerator::BeginDensityBlock(FIntVector OriginVoxels, int32 Step, int32 SizeX, int32 SizeY, int32 SizeZ) const { GVoxelDensityBlockSession.Begin(OriginVoxels, Step, SizeX, SizeY, SizeZ); } void UVoxelGenerator::EndDensityBlock() const { GVoxelDensityBlockSession.End(); } #if WITH_EDITOR bool UVoxelGenerator::EvaluateDensityChunk( FIntVector OriginVoxels, int32 SizeX, int32 SizeY, int32 SizeZ, TArray& OutDensities, int32& OutFallbackSamples, uint64& OutGeneralPathEntries, uint64& OutSupportedBlockSetupEntries) const { OutDensities.Reset(); OutFallbackSamples = 0; OutGeneralPathEntries = 0; OutSupportedBlockSetupEntries = 0; const int64 SampleCount64 = static_cast(SizeX) * SizeY * SizeZ; if (SizeX <= 0 || SizeY <= 0 || SizeZ <= 0 || SizeX > CHUNK_SIZE || SizeY > CHUNK_SIZE || SizeZ > CHUNK_SIZE || SampleCount64 > static_cast(CHUNK_SIZE) * CHUNK_SIZE * CHUNK_SIZE) { return false; } const FIntVector Last = OriginVoxels + FIntVector(SizeX - 1, SizeY - 1, SizeZ - 1); const auto SameCell = [](int32 A, int32 B, int32 CellSize) { return FMath::FloorToInt(static_cast(A) / static_cast(CellSize)) == FMath::FloorToInt(static_cast(B) / static_cast(CellSize)); }; if (!SameCell(OriginVoxels.X, Last.X, CHUNK_SIZE) || !SameCell(OriginVoxels.Y, Last.Y, CHUNK_SIZE) || !SameCell(OriginVoxels.Z, Last.Z, CHUNK_SIZE)) { return false; } const int32 SampleCount = static_cast(SampleCount64); BeginDensityBlock(OriginVoxels, 1, SizeX, SizeY, SizeZ); GVoxelDensityBlockSession.bBatchRequested = true; GVoxelDensityBlockSession.bBatchSucceeded = false; GVoxelDensityBlockSession.BatchResults.SetNumUninitialized(SampleCount); const float FirstDensity = GetDensityAt( static_cast(OriginVoxels.X), static_cast(OriginVoxels.Y), static_cast(OriginVoxels.Z)); OutGeneralPathEntries = 1; if (GVoxelDensityBlockSession.bBatchSucceeded) { OutDensities.SetNumUninitialized(SampleCount); FMemory::Memcpy( OutDensities.GetData(), GVoxelDensityBlockSession.BatchResults.GetData(), static_cast(SampleCount) * sizeof(float)); OutSupportedBlockSetupEntries = 1; EndDensityBlock(); return OutDensities.Num() == SampleCount; } // Routes without the editor block program retain the canonical point evaluator. The first // call already produced point zero; the remaining samples are explicit counted fallbacks. GVoxelDensityBlockSession.bBatchRequested = false; OutDensities.SetNumUninitialized(SampleCount); OutDensities[0] = FirstDensity; for (int32 Index = 1; Index < SampleCount; ++Index) { const int32 X = Index % SizeX; const int32 Y = (Index / SizeX) % SizeY; const int32 Z = Index / (SizeX * SizeY); OutDensities[Index] = GetDensityAt( static_cast(OriginVoxels.X + X), static_cast(OriginVoxels.Y + Y), static_cast(OriginVoxels.Z + Z)); } OutFallbackSamples = SampleCount; OutGeneralPathEntries = static_cast(SampleCount); EndDensityBlock(); return true; } #endif float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ, float* OutPreEditDensity) const { #if WITH_EDITOR // Step 2e and step 2d are separate editor-only, default-off side paths. The owner field stays // authoritative unless a research commandlet or automation test explicitly selects one. if (VoxelForgeRedefinedDensityField::IsParameterizedEnabled()) { VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker(); const float ResearchDensity = VoxelForgeRedefinedDensityField::EvaluateParameterized( *this, WorldX, WorldY, WorldZ, OutPreEditDensity, VoxelForgeRedefinedDensityField::GetDetailScale()); VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker(); return ResearchDensity; } if (VoxelForgeRedefinedDensityField::IsEnabled()) { VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker(); const float ResearchDensity = VoxelForgeRedefinedDensityField::Evaluate( *this, WorldX, WorldY, WorldZ, OutPreEditDensity, VoxelForgeRedefinedDensityField::GetDetailScale()); VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker(); return ResearchDensity; } #endif // Soundness mode: for a sample in a block whose core or passage-carving reach bit is clear, // evaluate the complete canonical field once with the bypass bit set. The normal call then // takes the fast path below and compares the final MC field after the boundary seal and diff // layer—not an intermediate SDF. This is deliberately debug-only; production pays no // recursive call and only the evaluator/tail flag reads. const bool bCompareFinalField = VoxelGenLOD::bTilePostReachDebug && !VoxelGenLOD::IsTilePostReachBypassActive() && VoxelGenLOD::bTileReachDiagnosticTile && (!VoxelGenLOD::IsTunnelCoreReachable() || !VoxelGenLOD::IsPassageCarvingReachable()); float CanonicalFinalField = 0.0f; if (bCompareFinalField) { const bool PreviousBypass = VoxelGenLOD::bTilePostReachBypass; VoxelGenLOD::bTilePostReachBypass = true; CanonicalFinalField = GetDensityAt(WorldX, WorldY, WorldZ); VoxelGenLOD::bTilePostReachBypass = PreviousBypass; } TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_GetDensityAt); VoxelDensityProfile::FScopedTimer DensityProfileTimer( VoxelDensityProfile::EBucket::GetDensityAt); VoxelDensityProfile::FScopedTimer DensityPrologueTimer( VoxelDensityProfile::EBucket::DensityPrologue); // ── STRATE SYSTEM ── // Query per-chunk params from the manager so each strate has different caves. float Result = 0.0f; // A vertical tree connector may need to stay open through the shared disturbance and // structural-post tail below. This is one bit for this density call, not cached voxel data. VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker(); FIntVector ChunkCoord( FMath::FloorToInt(WorldX / CHUNK_SIZE), FMath::FloorToInt(WorldY / CHUNK_SIZE), FMath::FloorToInt(WorldZ / CHUNK_SIZE) ); const bool bUseChunkRoutingCache = GVoxelForgeChunkRoutingCache != 0; thread_local uint64 CP_RouteGeneratorOwnerId = 0; thread_local const UVoxelStrateManager* CP_RouteManager = nullptr; thread_local uint64 CP_RouteManagerLifetimeId = 0; thread_local uint32 CP_RouteLayoutVersion = 0xFFFFFFFFu; thread_local FIntVector CP_RouteChunk(INT32_MAX, INT32_MAX, INT32_MAX); thread_local bool CP_RouteIsGapChunk = false; thread_local ECaveGeneratorType CP_RouteGeneratorType = ECaveGeneratorType::TunnelNetwork; thread_local bool CP_RouteUsesOperatorStack = false; uint32 RouteLayoutVersion = 0; uint64 RouteManagerLifetimeId = 0; bool bIsGapChunk = false; if (StrateManager && bUseChunkRoutingCache) { RouteLayoutVersion = StrateManager->GetLayoutVersion(); RouteManagerLifetimeId = StrateManager->GetCacheLifetimeId(); const bool bRouteKeyChanged = CP_RouteGeneratorOwnerId != DensityCacheOwnerId || CP_RouteManager != StrateManager || CP_RouteManagerLifetimeId != RouteManagerLifetimeId || CP_RouteLayoutVersion != RouteLayoutVersion || CP_RouteChunk != ChunkCoord; if (bRouteKeyChanged) { CP_RouteGeneratorOwnerId = DensityCacheOwnerId; CP_RouteManager = StrateManager; CP_RouteManagerLifetimeId = RouteManagerLifetimeId; CP_RouteLayoutVersion = RouteLayoutVersion; CP_RouteChunk = ChunkCoord; CP_RouteIsGapChunk = StrateManager->IsGapChunk(ChunkCoord); CP_RouteGeneratorType = StrateManager->GetGeneratorTypeForChunk(ChunkCoord); CP_RouteUsesOperatorStack = StrateManager->UsesOperatorStackForChunk(ChunkCoord); } bIsGapChunk = CP_RouteIsGapChunk; } else if (StrateManager) { bIsGapChunk = StrateManager->IsGapChunk(ChunkCoord); } #if WITH_EDITOR bool bEvaluateDensityBatch = false; int32 BatchSampleCount = 0; int32 DensityBatchCount = 1; bool bBrickHasDiffMods = false; #endif if (StrateManager && bIsGapChunk) { // SOLID BEDROCK gap between two strates. The auto-carved passages still tunnel // through it, but the (0,0) descent stays solid here so the player digs the gap // to reach the next layer. No caves, no spine, no vertical seal — just rock + passage // tube/landing; // the global XY edge seal is applied below for bounded worlds. DensityPrologueTimer.End(); VoxelDensityProfile::FScopedTimer DensityCoreTimer( VoxelDensityProfile::EBucket::DensityCore); float Density = 8.0f; // bedrock solidity (positive = solid) StrateManager->ApplyPassageModifier( Density, WorldX, WorldY, WorldZ, 8.0f, 0.0f); // Gap chunks use the compact bedrock path above instead of the ordinary strate tail. // Native walkable passages still cross those chunks, so compose their build-authored // D-floor here as well; otherwise the floor disappears exactly at the gap/strate seam. Result = -Density; StrateManager->ApplyPassageNativeFloorMC( Result, WorldX, WorldY, WorldZ, 8.0f); VF_ApplyXYEdgeSealMC( Result, WorldX, WorldY, WorldRadiusVoxels, EdgeSealThickness, 8.0f); } else if (StrateManager) { // ── PER-CHUNK PARAM CACHE ── // GetDensityAt runs per voxel AND ~6× more per surface vertex (gradient normals). // The generator type, the (boundary-blended) param struct, and the disturbance // params are identical for the whole chunk, yet resolving them re-runs a strate // lookup + copies large structs (and a ~60-field Lerp for blended cave chunks). // Cache them thread-locally, keyed by owner + chunk coord + layout version — refetch only // when one of those integer identities changes. thread_local uint64 CP_OwnerId = 0; thread_local FIntVector CP_Chunk(INT32_MAX, INT32_MAX, INT32_MAX); thread_local ECaveGeneratorType CP_GenType = ECaveGeneratorType::TunnelNetwork; thread_local FStrateGenerationParams CP_Tunnel; // AUDIT §C2 — empreinte de `CP_Tunnel`, rafraîchie avec lui. Elle voyage jusqu'à la clé du // cache SDF de `GetDensityWithParams` pour qu'un chunk ne puisse plus être évalué contre // les salles d'un chunk voisin aux params blendés différemment. thread_local uint32 CP_TunnelFP = 0xFFFFFFFFu; thread_local FSlabGenerationParams CP_Slab; thread_local FMazeGenerationParams CP_Maze; thread_local FSurfaceGenerationParams CP_Surface; thread_local FVerticalShaftParams CP_Vert; thread_local FFloatingIslandParams CP_Float; thread_local FStrateDisturbanceParams CP_Dist; // Biomes (SurfaceWorld for now): CP_BiomeCtx is the cheap per-chunk flatten; // CP_BiomeCache is the box-validated grid; CP_SurfaceBiomeParams holds each biome's // resolved surface params (override or strate fallback) parallel to CP_BiomeCtx.Biomes. thread_local FBiomeContext CP_BiomeCtx; thread_local FChunkBiomeCache CP_BiomeCache; thread_local TArray CP_SurfaceBiomeParams; // AUDIT C2 — la clé DOIT contenir la version de layout, pas seulement le chunk. Après un // RebuildStrates / une édition à chaud, StrateManager reconstruit le layout et bumpe la // version ; un worker dont CP_Chunk vaut encore ce chunk sauterait le refetch et // générerait avec les ANCIENS params. Comme RegenerateAllChunks recharge les MÊMES coords // de tuile, souvent sur les MÊMES workers, le cas est probable plutôt qu'exotique. // The key MUST include the layout version, not just the chunk coord. Symptom without it: // "I tweaked the strate asset, regenerated, and one patch kept the old shape." thread_local uint32 CP_Version = 0xFFFFFFFFu; // The tunnel cache key also includes whether the current tile requested the coarse // tile-window context. A cache hit moves the prepared stack/core out of these TLS values; // when the other context misses, this bit must force the rebuild guard below to run. thread_local bool CP_UsesTileCacheWindow = false; // OPSTACK — la pile d'opérateurs, construite dans le MÊME bloc de refetch que les // params (donc même clé owner+chunk+version, aucune logique d'invalidation en plus). Vide // seulement pour une strate dégénérée (hauteur nulle). thread_local FVoxelOpStack CP_OpStack; thread_local bool CP_UseOpStack = false; thread_local uint64 CP_ManagerLifetimeId = 0; thread_local FTunnelNetworkDensityCacheEntry CP_TunnelDensityCache[TunnelDensityCacheSlotCount]; thread_local FTunnelNetworkDensityCacheEntry* CP_ActiveTunnelDensityCache = nullptr; // Cooked-season recipes and editor candidate recipes share this immutable worker-local // hand-off. Recipe materialisation is runtime; rolling/selection remains editor-only. thread_local bool CP_UseCustomRecipe = false; thread_local int32 CP_CustomRecipeSeed = 0; thread_local FVoxelStrateArchetypeParams CP_CustomParams; thread_local FVoxelOpStackRecipe CP_CustomRecipe; #if WITH_EDITOR // Editor-only composer override. The candidate recipe is copied into this worker-local // cache on the same versioned refetch as the native params; no worker reads mutable editor // state while the world is applying a new candidate. thread_local bool CP_UseComposerRegions = false; thread_local int32 CP_ComposerSeed = 0; thread_local FVoxelStrateRegionManifest CP_ComposerRegions; #endif const uint32 LayoutVersion = bUseChunkRoutingCache ? RouteLayoutVersion : StrateManager->GetLayoutVersion(); const uint64 ManagerLifetimeId = bUseChunkRoutingCache ? RouteManagerLifetimeId : StrateManager->GetCacheLifetimeId(); // TunnelNetwork/Underwater operator-stack parameters are selected from chunk Z only. // A coarse tile samples thousands of exact XY chunk keys, but those keys describe the // same immutable stack whenever the selected archetype is operator-stack backed. Share // that prepared state by Z; the stack still queries the actual world position, and its // room-graph cache remains keyed by the real XY search window. Chunks outside the strate // layout (constant air) keep the full XYZ key. const ECaveGeneratorType QueryGeneratorType = bUseChunkRoutingCache ? CP_RouteGeneratorType : StrateManager->GetGeneratorTypeForChunk(ChunkCoord); const bool bShareTunnelStackByZ = !bIsGapChunk && (QueryGeneratorType == ECaveGeneratorType::TunnelNetwork || QueryGeneratorType == ECaveGeneratorType::Underwater) && (bUseChunkRoutingCache ? CP_RouteUsesOperatorStack : StrateManager->UsesOperatorStackForChunk(ChunkCoord)); const FIntVector TunnelCacheKey = bShareTunnelStackByZ ? FIntVector(0, 0, ChunkCoord.Z) : ChunkCoord; FIntVector RequestedTileOrigin = FIntVector::ZeroValue; int32 RequestedTileStep = 1; int32 RequestedTileCells = 0; const bool bTileCacheContext = VoxelGenLOD::IsTileCacheWindowEnabled( !bShareTunnelStackByZ) && VoxelGenLOD::GetThreadTileCacheWindow( RequestedTileOrigin, RequestedTileStep, RequestedTileCells); FVoxelOpStack* ActiveOpStack = &CP_OpStack; FTunnelCoreCacheState* ActiveTunnelCoreCache = >unnelCoreCache; bool bLoadedTunnelDensityCache = false; FTunnelNetworkDensityCacheEntry* TunnelDensityCacheEntryForWrite = nullptr; // The table is open-addressed rather than one-slot direct-mapped. A direct map is very // cheap until a coarse tile's regular lattice happens to put two chunk keys in the same // bucket; then the two keys evict each other on every row and rebuild the prepared graph. // Probe deterministically to the first empty/stale slot, retaining the bounded memory cap. auto FindTunnelDensityCacheEntry = [&](const FIntVector& QueryChunk, FTunnelNetworkDensityCacheEntry*& OutEntry) -> bool { const int32 StartSlot = TunnelDensityCacheSlot(TunnelCacheKey); FTunnelNetworkDensityCacheEntry* ReuseEntry = nullptr; for (int32 Probe = 0; Probe < TunnelDensityCacheSlotCount; ++Probe) { const int32 Slot = (StartSlot + Probe) & (TunnelDensityCacheSlotCount - 1); FTunnelNetworkDensityCacheEntry& Candidate = CP_TunnelDensityCache[Slot]; if (!Candidate.bValid || Candidate.OwnerId != DensityCacheOwnerId) { if (ReuseEntry == nullptr) { ReuseEntry = &Candidate; } if (!Candidate.bValid) { break; } continue; } const bool bKeyMatches = Candidate.ManagerLifetimeId == ManagerLifetimeId && Candidate.Chunk == TunnelCacheKey && Candidate.LayoutVersion == LayoutVersion && Candidate.bUseOpStack == bShareTunnelStackByZ && Candidate.bUsesTileCacheWindow == bTileCacheContext && (Candidate.GenType == ECaveGeneratorType::TunnelNetwork || Candidate.GenType == ECaveGeneratorType::Underwater); if (bKeyMatches) { OutEntry = &Candidate; return true; } } OutEntry = ReuseEntry != nullptr ? ReuseEntry : &CP_TunnelDensityCache[StartSlot]; return false; }; if (!bIsGapChunk) { if (VoxelDensityProfile::AreCountersEnabled()) { VoxelDensityProfile::AddCounter( VoxelDensityProfile::ECounter::TunnelCacheLookup); } FTunnelNetworkDensityCacheEntry* CachedEntryPtr = nullptr; const bool bCacheKeyMatches = FindTunnelDensityCacheEntry(ChunkCoord, CachedEntryPtr); FTunnelNetworkDensityCacheEntry& CachedEntry = *CachedEntryPtr; TunnelDensityCacheEntryForWrite = CachedEntryPtr; if (bCacheKeyMatches) { if (VoxelDensityProfile::AreCountersEnabled()) { VoxelDensityProfile::AddCounter( VoxelDensityProfile::ECounter::TunnelCacheHit); } if (CP_ActiveTunnelDensityCache != &CachedEntry) { CP_OwnerId = CachedEntry.OwnerId; CP_ManagerLifetimeId = CachedEntry.ManagerLifetimeId; CP_Version = CachedEntry.LayoutVersion; CP_Chunk = CachedEntry.Chunk; CP_GenType = CachedEntry.GenType; CP_Tunnel = CachedEntry.Tunnel; CP_TunnelFP = CachedEntry.TunnelFingerprint; CP_Dist = CachedEntry.Disturbance; CP_UseOpStack = CachedEntry.bUseOpStack; CP_UsesTileCacheWindow = CachedEntry.bUsesTileCacheWindow; CP_UseCustomRecipe = false; #if WITH_EDITOR CP_UseComposerRegions = false; #endif CP_ActiveTunnelDensityCache = &CachedEntry; } ActiveOpStack = &CachedEntry.OpStack; ActiveTunnelCoreCache = &CachedEntry.TunnelCore; bLoadedTunnelDensityCache = true; } else if (VoxelDensityProfile::AreCountersEnabled()) { VoxelDensityProfile::AddCounter( VoxelDensityProfile::ECounter::TunnelCacheMiss); } } const bool bOwnerChanged = DensityCacheOwnerId != CP_OwnerId; const bool bManagerChanged = ManagerLifetimeId != CP_ManagerLifetimeId; // A cache hit may have moved CP_OpStack/GTunnelCoreCache into the shared entry. If the // next lookup misses (including an eviction/collision for the same shared Z key), the // fallback TLS objects are moved-from and must never be evaluated as if they were valid. // The active-entry pointer is the explicit moved-out marker; a miss therefore forces the // normal rebuild even when the shared key happens to equal ChunkCoord at the origin. const bool bMovedOutTunnelState = CP_ActiveTunnelDensityCache != nullptr; if (!bLoadedTunnelDensityCache && (bOwnerChanged || bManagerChanged || ChunkCoord != CP_Chunk || LayoutVersion != CP_Version || bTileCacheContext != CP_UsesTileCacheWindow || bMovedOutTunnelState)) { CP_ActiveTunnelDensityCache = nullptr; // La grille de biome est validée par une BOÎTE XY, qui ne dit rien du FBiomeContext // ayant servi à classer ses cellules : sur un changement de version elle est périmée // même si la boîte couvre encore la requête. Même invalidation quand le propriétaire // change : deux mondes peuvent partager version et coordonnées, jamais leur contexte. // The biome grid's XY box says nothing about its context. Owner changes invalidate it // too: two worlds may share version and coordinates, never cached params/context. if (bOwnerChanged || bManagerChanged || LayoutVersion != CP_Version) { CP_BiomeCache.Invalidate(); } CP_OwnerId = DensityCacheOwnerId; CP_ManagerLifetimeId = ManagerLifetimeId; CP_Version = LayoutVersion; CP_Chunk = ChunkCoord; CP_UsesTileCacheWindow = bTileCacheContext; CP_GenType = bUseChunkRoutingCache ? CP_RouteGeneratorType : StrateManager->GetGeneratorTypeForChunk(ChunkCoord); CP_UseCustomRecipe = false; bool bAllowCustomRecipe = true; #if WITH_EDITOR CP_UseComposerRegions = false; if (StrateManager->GetComposerRegionOverrideForChunk( ChunkCoord, CP_ComposerRegions)) { CP_UseComposerRegions = true; bAllowCustomRecipe = false; CP_ComposerSeed = CP_ComposerRegions.Seed; if (CP_ComposerRegions.Regions.Num() > 0) { CP_GenType = CP_ComposerRegions.Regions[0].Archetype; } } #endif if (bAllowCustomRecipe && StrateManager->GetRecipeForChunk( ChunkCoord, CP_CustomRecipeSeed, CP_GenType, CP_CustomParams, CP_CustomRecipe)) { CP_UseCustomRecipe = true; } switch (CP_GenType) { case ECaveGeneratorType::FlatPlain: case ECaveGeneratorType::CrystalChamber: CP_Slab = StrateManager->GetSlabParamsForChunk(ChunkCoord); break; case ECaveGeneratorType::Maze: CP_Maze = StrateManager->GetMazeParamsForChunk(ChunkCoord); break; case ECaveGeneratorType::SurfaceWorld: ResolveSurfaceChunkParams(ChunkCoord, CP_Surface, CP_BiomeCtx, CP_SurfaceBiomeParams); break; case ECaveGeneratorType::VerticalShafts: CP_Vert = StrateManager->GetVerticalShaftParamsForChunk(ChunkCoord); break; case ECaveGeneratorType::FloatingIslands: CP_Float = StrateManager->GetFloatingIslandParamsForChunk(ChunkCoord); break; default: // TunnelNetwork / Underwater CP_Tunnel = StrateManager->GetGenerationParams(ChunkCoord); // AUDIT §C2 — l'empreinte est calculée ICI, une fois par chunk, au seul endroit où // les params changent. `FStrateGenerationParams` est du POD pur (aucun TArray / // FString / pointeur), donc une CRC mémoire peut donner un FAUX POSITIF par // collision CRC32 ; au pire un octet de padding donne aussi un faux MANQUE, c'est- // à-dire une reconstruction de cache. Le risque pratique est une collision, jamais // une salle fausse créée par la seule différence de padding. CP_TunnelFP = FCrc::MemCrc32(&CP_Tunnel, sizeof(CP_Tunnel)); break; } CP_Dist = StrateManager->GetDisturbanceParamsForChunk(ChunkCoord); // ── OPSTACK : (re)construire la pile de ce chunk. ── // Toute strate est générée par une pile d'opérateurs. La construction est par chunk // (comme le refetch de params juste au-dessus), jamais par voxel. // Every strate is generated by an operator stack. An editor composer candidate or a // cooked-season recipe supplies its own stack; otherwise — or if that stack cannot be // built — the archetype's native stack is used. CP_OpStack = FVoxelOpStack(); // move-assign : libère l'ancienne pile CP_UseOpStack = false; #if WITH_EDITOR if (CP_UseComposerRegions) { FVoxelOpContext RegionContext; if (VF_BuildStrateRegionStack( CP_ComposerRegions, OriginSpineRadius, StrateManager, CP_OpStack, RegionContext, nullptr)) { RegionContext.ChunkCoord = ChunkCoord; RegionContext.Step = 1; RegionContext.LayoutVersion = LayoutVersion; RegionContext.WorldRadiusVoxels = WorldRadiusVoxels; RegionContext.EdgeSealThickness = EdgeSealThickness; CP_OpStack.PrepareChunk(RegionContext); CP_UseOpStack = true; } else { CP_OpStack = FVoxelOpStack(); CP_UseComposerRegions = false; } } #endif if (!CP_UseOpStack && CP_UseCustomRecipe) { FVoxelOpContext RecipeContext; if (VF_BuildStackFromRecipe( CP_CustomRecipe, CP_CustomParams, CP_CustomRecipeSeed, OriginSpineRadius, StrateManager, CP_OpStack, RecipeContext, nullptr)) { RecipeContext.ChunkCoord = ChunkCoord; RecipeContext.Step = 1; RecipeContext.LayoutVersion = LayoutVersion; // The recipe builder supplies the candidate's vertical seal to the // structural post. The XY edge seal is global and uses live settings. RecipeContext.WorldRadiusVoxels = WorldRadiusVoxels; RecipeContext.EdgeSealThickness = EdgeSealThickness; CP_OpStack.PrepareChunk(RecipeContext); CP_UseOpStack = true; } else { // Import/load validates recipes before a season becomes active. If a future // schema still fails here, fall back to the archetype's native stack below. CP_OpStack = FVoxelOpStack(); CP_UseCustomRecipe = false; } } if (!CP_UseOpStack) { FVoxelOpContext OpCtx; OpCtx.ChunkCoord = ChunkCoord; OpCtx.Seed = (uint32)Seed; OpCtx.LayoutVersion = LayoutVersion; OpCtx.WorldRadiusVoxels = WorldRadiusVoxels; OpCtx.EdgeSealThickness = EdgeSealThickness; // ⚠️ LE MAPPING VIT DANS `VF_BuildOpStackForChunk` (haut de ce fichier) ET NULLE // PART AILLEURS — `ClassifyTile` appelle la MÊME fabrique. Un verdict de tuile issu // d'une pile construite autrement serait un trou. Ici on ne fait que fournir les // params déjà cherchés juste au-dessus. FVoxelStackParamRefs Refs; Refs.Slab = &CP_Slab; Refs.Maze = &CP_Maze; Refs.Vert = &CP_Vert; Refs.Float = &CP_Float; Refs.Tunnel = &CP_Tunnel; // SurfaceWorld : le champ de biomes est fabriqué ICI, du côté qui connaît le // générateur, et TRANSFÉRÉ à la pile. L'opérateur ne voit qu'une `IVoxelBiomeField`, // ce qui lui permet de devenir un asset en Phase 3 sans traîner le générateur. TArray PerBiome; if (CP_GenType == ECaveGeneratorType::SurfaceWorld) { Refs.Surface = &CP_Surface; if (CP_BiomeCtx.IsValid() && CP_SurfaceBiomeParams.Num() > 0) { PerBiome = CP_SurfaceBiomeParams; Refs.SurfaceBiomeParams = &PerBiome; Refs.BiomeField = MakeUnique( this, &CP_BiomeCtx, &CP_BiomeCache, ChunkCoord.Z); } } CP_UseOpStack = VF_BuildOpStackForChunk( CP_GenType, Refs, Seed, OriginSpineRadius, StrateManager, CP_OpStack, OpCtx); // PrepareChunk runs here exactly as in the tests, so an op that hoists real // per-chunk work behaves the same in game. `Step` reste 1 : GetDensityAt ne connaît // pas le pas d'échantillonnage du mesher (voir le contrat T2.b dans VoxelDensityOp.h). if (CP_UseOpStack) { CP_OpStack.PrepareChunk(OpCtx); } } // An operator-stack tunnel source publishes the world-space core after EvalMC, so // preparing a second full graph/core cache here would only duplicate the work that made // coarse streaming expensive in the first place. Only an editor composer region stack, // which does not publish that hand-off, still gets the prepared native core. const bool bNativeTunnelCore = !CP_UseCustomRecipe && (CP_GenType == ECaveGeneratorType::TunnelNetwork || CP_GenType == ECaveGeneratorType::Underwater); #if WITH_EDITOR const bool bCanPrepareNativeTunnelCore = bNativeTunnelCore && (!CP_UseOpStack || CP_UseComposerRegions); #else const bool bCanPrepareNativeTunnelCore = bNativeTunnelCore && !CP_UseOpStack; #endif if (bCanPrepareNativeTunnelCore) { PrepareTunnelCoreCache( *StrateManager, ChunkCoord, CP_Tunnel, CP_TunnelFP, LayoutVersion, ManagerLifetimeId, DensityCacheOwnerId, GTunnelCoreCache); } else { GTunnelCoreCache.bValid = false; } const bool bNativeTunnelCacheCandidate = !CP_UseCustomRecipe && (CP_GenType == ECaveGeneratorType::TunnelNetwork || CP_GenType == ECaveGeneratorType::Underwater); #if WITH_EDITOR const bool bCanStoreTunnelDensityCache = bNativeTunnelCacheCandidate && !CP_UseComposerRegions // A native direct core uses the worker-global state below. Keeping it in one // per-exact-chunk entry would move the tile graph out and force the next chunk to // rebuild it. Operator stacks do not prepare this native core, so they retain the // existing prepared-stack cache even when the tile context is active. && (!bTileCacheContext || CP_UseOpStack); #else const bool bCanStoreTunnelDensityCache = bNativeTunnelCacheCandidate && (!bTileCacheContext || CP_UseOpStack); #endif if (!bLoadedTunnelDensityCache && bCanStoreTunnelDensityCache) { FTunnelNetworkDensityCacheEntry& CachedEntry = TunnelDensityCacheEntryForWrite ? *TunnelDensityCacheEntryForWrite : CP_TunnelDensityCache[TunnelDensityCacheSlot(ChunkCoord)]; CachedEntry.bValid = false; CachedEntry.OwnerId = DensityCacheOwnerId; CachedEntry.ManagerLifetimeId = ManagerLifetimeId; CachedEntry.Chunk = TunnelCacheKey; CachedEntry.LayoutVersion = LayoutVersion; CachedEntry.GenType = CP_GenType; CachedEntry.Tunnel = CP_Tunnel; CachedEntry.TunnelFingerprint = CP_TunnelFP; CachedEntry.Disturbance = CP_Dist; CachedEntry.bUseOpStack = CP_UseOpStack; CachedEntry.bUsesTileCacheWindow = bTileCacheContext; CachedEntry.OpStack = MoveTemp(CP_OpStack); CachedEntry.TunnelCore = MoveTemp(GTunnelCoreCache); GTunnelCoreCache = FTunnelCoreCacheState(); CachedEntry.bValid = true; CP_ActiveTunnelDensityCache = &CachedEntry; ActiveOpStack = &CachedEntry.OpStack; ActiveTunnelCoreCache = &CachedEntry.TunnelCore; ReportTunnelDensityCacheFootprint(CP_TunnelDensityCache); } else if (!bLoadedTunnelDensityCache) { ActiveOpStack = &CP_OpStack; ActiveTunnelCoreCache = >unnelCoreCache; } } DensityPrologueTimer.End(); VoxelDensityProfile::FScopedTimer DensityCoreTimer( VoxelDensityProfile::EBucket::DensityCore); bool bUsedOpBlockSample = false; float BlockDensity = 0.f; bool bBlockHasTunnelCore = false; float BlockTunnelCoreSDF = FLT_MAX; bool bBlockTunnelCoreRoomFloor = false; bool bBlockHasTunnelCoreSweptFloor = false; float BlockTunnelCoreSweptFloorZ = -FLT_MAX; float BlockTunnelCoreSweptFloorRadius = 0.0f; bool bUsedFusedEvaluator = false; FTunnelCoreWorldEvaluation FusedTunnelCore; bool bRoomOwnsBottom = false; bool bRoomOwnsBottomValid = false; // Le seul point d'entrée de la pile dans le chemin de production. Elle rend la convention // MC (négatif = solide), que les disturbances et la couche de diff qui suivent attendent. if (CP_UseOpStack) { const bool bCanUseFusedEvaluator = GVoxelForgeUseFusedEvaluator != 0 && !CP_UseCustomRecipe && (CP_GenType == ECaveGeneratorType::TunnelNetwork || CP_GenType == ECaveGeneratorType::Underwater) #if WITH_EDITOR && !CP_UseComposerRegions #endif && ActiveOpStack->GetFusedEvaluator() == EVoxelOpFusedEvaluator::TunnelNetwork; if (bCanUseFusedEvaluator) { // This is the lowered canonical graph. It returns the same MC core as the // interpreted stack, plus the source's structural hand-off, without allocating // FVoxelOpSample or traversing the graph one operator at a time for this voxel. bUsedFusedEvaluator = true; VoxelDensityProfile::FScopedTimer FusedEvaluatorTimer( VoxelDensityProfile::EBucket::FusedEvaluator); Result = GetDensityWithParams( WorldX, WorldY, WorldZ, CP_Tunnel, CP_TunnelFP, LayoutVersion, /*bApplyLegacyStructuralPosts=*/false, &FusedTunnelCore, /*bCollectFusedDiagnostics=*/true, &bRoomOwnsBottom); bRoomOwnsBottomValid = true; } const bool bIntegerLatticePoint = WorldX == FMath::FloorToFloat(WorldX) && WorldY == FMath::FloorToFloat(WorldY) && WorldZ == FMath::FloorToFloat(WorldZ); const bool bBlockSafeGenerator = CP_GenType == ECaveGeneratorType::TunnelNetwork || CP_GenType == ECaveGeneratorType::Underwater; if (!bUsedFusedEvaluator && bIntegerLatticePoint && bBlockSafeGenerator && GVoxelDensityBlockSession.bActive) { // Keep the rich hand-off object in the fallback branch. The lowered evaluator // must not construct a per-sample FVoxelOpSample merely because this compatibility // path still needs one when the block session is enabled. FVoxelOpSample BlockCoreSample; { VoxelDensityProfile::FScopedTimer OperatorBlockTimer( VoxelDensityProfile::EBucket::OperatorBlock); bUsedOpBlockSample = GVoxelDensityBlockSession.FillChunk( *ActiveOpStack, ChunkCoord, FIntVector( FMath::RoundToInt(WorldX), FMath::RoundToInt(WorldY), FMath::RoundToInt(WorldZ)), BlockCoreSample); } if (bUsedOpBlockSample) { BlockDensity = BlockCoreSample.Density; bBlockHasTunnelCore = BlockCoreSample.bHasTunnelCoreWorldEvaluation; BlockTunnelCoreSDF = BlockCoreSample.TunnelCoreWorldSDF; bBlockTunnelCoreRoomFloor = BlockCoreSample.bTunnelCoreRoomFloor; bBlockHasTunnelCoreSweptFloor = BlockCoreSample.bHasTunnelCoreSweptFloor; BlockTunnelCoreSweptFloorZ = BlockCoreSample.TunnelCoreSweptFloorZ; BlockTunnelCoreSweptFloorRadius = BlockCoreSample.TunnelCoreSweptFloorRadius; } } if (!bUsedFusedEvaluator) { if (bUsedOpBlockSample) { Result = -BlockDensity; } else { VoxelDensityProfile::FScopedTimer InterpretedOpStackTimer( VoxelDensityProfile::EBucket::InterpretedOpStack); Result = ActiveOpStack->EvalMC(WorldX, WorldY, WorldZ); } } } else { // Only a degenerate strate (zero height) has no stack. It is open air, like the // archetype functions the stack replaced. Result = 1.0f; } DensityCoreTimer.End(); #if WITH_EDITOR // The editor region program asks this one routed sample to fill every point in the // already-evaluated operator block. Its bricks are contained by one 8-voxel research // bin and one 32-voxel chunk, so the stack, params, route, and cache key above are shared. BatchSampleCount = GVoxelDensityBlockSession.SizeX * GVoxelDensityBlockSession.SizeY * GVoxelDensityBlockSession.SizeZ; bEvaluateDensityBatch = GVoxelDensityBlockSession.bBatchRequested && bUsedOpBlockSample && !bUsedFusedEvaluator && CP_UseOpStack && !bIsGapChunk && StrateManager != nullptr && (CP_GenType == ECaveGeneratorType::TunnelNetwork || CP_GenType == ECaveGeneratorType::Underwater) && GVoxelDensityBlockSession.Samples.Num() == BatchSampleCount && GVoxelDensityBlockSession.BatchResults.Num() == BatchSampleCount; DensityBatchCount = bEvaluateDensityBatch ? BatchSampleCount : 1; bool bBrickHasDisturbanceCandidate = true; bool bBrickHasPassagePostCandidate = true; if (bEvaluateDensityBatch) { const FIntVector BrickLast = GVoxelDensityBlockSession.Origin + FIntVector( (GVoxelDensityBlockSession.SizeX - 1) * GVoxelDensityBlockSession.Step, (GVoxelDensityBlockSession.SizeY - 1) * GVoxelDensityBlockSession.Step, (GVoxelDensityBlockSession.SizeZ - 1) * GVoxelDensityBlockSession.Step); const FBox BrickBox( FVector(GVoxelDensityBlockSession.Origin.X, GVoxelDensityBlockSession.Origin.Y, GVoxelDensityBlockSession.Origin.Z), FVector(BrickLast.X, BrickLast.Y, BrickLast.Z)); bBrickHasDisturbanceCandidate = !VoxelDensityAblation::IsDisturbancesOff() && VF_AnyDisturbanceCanTouchBox( BrickBox, CP_Dist, static_cast(Seed)); const bool bAllPassagePostsOff = VoxelDensityAblation::IsPassageStructuralPostsOff() && VoxelDensityAblation::IsNativeFloorOff() && VoxelDensityAblation::IsLandingPostsOff(); if (bAllPassagePostsOff) { bBrickHasPassagePostCandidate = false; } else if (!VoxelDensityAblation::IsPassageCarvingOff()) { bBrickHasPassagePostCandidate = StrateManager->AnyPassageNearBox( BrickBox.Min, BrickBox.Max); } bBrickHasDiffMods = DiffLayer != nullptr && DiffLayer->HasAnyMods(); } const bool bApplyBrickDisturbances = !bEvaluateDensityBatch || bBrickHasDisturbanceCandidate; const bool bApplyBrickPassagePosts = !bEvaluateDensityBatch || bBrickHasPassagePostCandidate; #else const bool bApplyBrickDisturbances = true; const bool bApplyBrickPassagePosts = true; #endif // Chunk parameters and composer/custom recipe structure are fixed by the route above. // Resolve the landing writer's base once for the whole brick, including edge bricks. float BrickLandingBaseDensity = CP_Dist.BaseDensity; #if WITH_EDITOR if (CP_UseComposerRegions && CP_ComposerRegions.bHasGlobalStructuralParams) { BrickLandingBaseDensity = CP_ComposerRegions.BaseDensity; } else #endif if (CP_UseCustomRecipe) { switch (CP_CustomRecipe.StructuralParamBlock) { case EVoxelStrateParamBlock::TunnelNetwork: BrickLandingBaseDensity = CP_CustomParams.TunnelNetworkParams.BaseDensity; break; case EVoxelStrateParamBlock::Slab: BrickLandingBaseDensity = CP_CustomParams.SlabParams.BaseDensity; break; case EVoxelStrateParamBlock::Maze: BrickLandingBaseDensity = CP_CustomParams.MazeParams.BaseDensity; break; case EVoxelStrateParamBlock::Surface: BrickLandingBaseDensity = CP_CustomParams.SurfaceParams.BaseDensity; break; case EVoxelStrateParamBlock::VerticalShaft: BrickLandingBaseDensity = CP_CustomParams.VerticalShaftParams.BaseDensity; break; case EVoxelStrateParamBlock::FloatingIsland: BrickLandingBaseDensity = CP_CustomParams.FloatingIslandParams.BaseDensity; break; default: break; } } else { switch (CP_GenType) { case ECaveGeneratorType::FlatPlain: case ECaveGeneratorType::CrystalChamber: BrickLandingBaseDensity = CP_Slab.BaseDensity; break; case ECaveGeneratorType::Maze: BrickLandingBaseDensity = CP_Maze.BaseDensity; break; case ECaveGeneratorType::SurfaceWorld: BrickLandingBaseDensity = CP_Surface.BaseDensity; break; case ECaveGeneratorType::VerticalShafts: BrickLandingBaseDensity = CP_Vert.BaseDensity; break; case ECaveGeneratorType::FloatingIslands: BrickLandingBaseDensity = CP_Float.BaseDensity; break; case ECaveGeneratorType::TunnelNetwork: case ECaveGeneratorType::Underwater: default: BrickLandingBaseDensity = CP_Tunnel.BaseDensity; break; } } for (int32 DensityBatchIndex = 0; #if WITH_EDITOR DensityBatchIndex < DensityBatchCount; #else DensityBatchIndex < 1; #endif ++DensityBatchIndex) { #if WITH_EDITOR if (bEvaluateDensityBatch) { const int32 BatchX = DensityBatchIndex % GVoxelDensityBlockSession.SizeX; const int32 BatchY = (DensityBatchIndex / GVoxelDensityBlockSession.SizeX) % GVoxelDensityBlockSession.SizeY; const int32 BatchZ = DensityBatchIndex / (GVoxelDensityBlockSession.SizeX * GVoxelDensityBlockSession.SizeY); WorldX = static_cast(GVoxelDensityBlockSession.Origin.X + BatchX * GVoxelDensityBlockSession.Step); WorldY = static_cast(GVoxelDensityBlockSession.Origin.Y + BatchY * GVoxelDensityBlockSession.Step); WorldZ = static_cast(GVoxelDensityBlockSession.Origin.Z + BatchZ * GVoxelDensityBlockSession.Step); const FVoxelOpSample& BatchSample = GVoxelDensityBlockSession.Samples[DensityBatchIndex]; Result = -BatchSample.Density; bUsedOpBlockSample = true; bBlockHasTunnelCore = BatchSample.bHasTunnelCoreWorldEvaluation; BlockTunnelCoreSDF = BatchSample.TunnelCoreWorldSDF; bBlockTunnelCoreRoomFloor = BatchSample.bTunnelCoreRoomFloor; bBlockHasTunnelCoreSweptFloor = BatchSample.bHasTunnelCoreSweptFloor; BlockTunnelCoreSweptFloorZ = BatchSample.TunnelCoreSweptFloorZ; BlockTunnelCoreSweptFloorRadius = BatchSample.TunnelCoreSweptFloorRadius; bUsedFusedEvaluator = false; bRoomOwnsBottomValid = false; } #endif // Resolve the graph's authored floor before the disturbance pass. The operator-stack // source has already computed this exact result while publishing its sample; native // tunnel generation uses the prepared immutable cache. Keeping the result here means the // disturbance layer can respect the shape-level floor and the common tail can reuse the // same query instead of discovering a floor only after it has been damaged. FTunnelCoreWorldEvaluation PreDisturbanceTunnelCore; bool bHavePreDisturbanceTunnelCore = false; if (CP_UseOpStack) { if (bUsedFusedEvaluator) { PreDisturbanceTunnelCore = FusedTunnelCore; bHavePreDisturbanceTunnelCore = true; } else if (bUsedOpBlockSample && bBlockHasTunnelCore) { PreDisturbanceTunnelCore.SDF = BlockTunnelCoreSDF; PreDisturbanceTunnelCore.bRoomFloor = bBlockTunnelCoreRoomFloor; PreDisturbanceTunnelCore.bHasSweptFloor = bBlockHasTunnelCoreSweptFloor; PreDisturbanceTunnelCore.SweptFloorZ = BlockTunnelCoreSweptFloorZ; PreDisturbanceTunnelCore.SweptFloorRadius = BlockTunnelCoreSweptFloorRadius; bHavePreDisturbanceTunnelCore = true; } else { bHavePreDisturbanceTunnelCore = ActiveOpStack->TryGetLastTunnelCoreWorldEvaluation( PreDisturbanceTunnelCore); } } if (!VoxelDensityAblation::IsTunnelCoreOff() && !bHavePreDisturbanceTunnelCore && ActiveTunnelCoreCache->bValid) { VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::TunnelCorePosts); FVector RoomQueryPosition(WorldX, WorldY, WorldZ); if (CP_Tunnel.VerticalScale != 1.0f && CP_Tunnel.VerticalScale > 0.0f) { RoomQueryPosition.Z = WorldZ / CP_Tunnel.VerticalScale; } RoomQueryPosition = VoxelCaveMorphology::ApplyCaveWarp( RoomQueryPosition, CP_Tunnel, static_cast(Seed)); PreDisturbanceTunnelCore = VoxelCaveMorphology::EvaluateTunnelCoreWorld( WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache, nullptr, VoxelGenLOD::ShouldUseSpatialIndex( CP_UseOpStack ? bUsedFusedEvaluator : true), &RoomQueryPosition, bRoomOwnsBottomValid ? &bRoomOwnsBottom : nullptr); bHavePreDisturbanceTunnelCore = true; } // Disturbance layer (the "wow" post-process) — cached params, MC convention. The // vertical shaft tree opens a walkable route before this shared pass, so a bridge/ridge // must not refill it. Both native and operator-stack paths use the same marker helper. VoxelDensityProfile::FScopedTimer DensityDisturbancesTimer( VoxelDensityProfile::EBucket::DensityDisturbances); const bool bProtectVerticalShaftAir = VoxelPassageGeometry::VerticalShaftConnectorAirMarker(); if (bApplyBrickDisturbances) { ApplyDisturbances(Result, WorldX, WorldY, WorldZ, CP_Dist, (uint32)Seed, bProtectVerticalShaftAir); } DensityDisturbancesTimer.End(); // A disturbance is allowed to add visual detail, but it must not refill the landing's // measured air volume or turn its support slab back into a hole. Reassert the landing // air first, then the structural floor, in MC space before the final XY seal. The two // calls use the same landing geometry already evaluated by the op-stack structural post. VoxelDensityProfile::FScopedTimer DensityStructuralPostsTimer( VoxelDensityProfile::EBucket::DensityStructuralPosts); VoxelDensityProfile::FScopedTimer StructuralTailTimer( VoxelDensityProfile::EBucket::StructuralTail); const float LandingBaseDensity = BrickLandingBaseDensity; auto ApplyTunnelCoreShape = [&](const FTunnelCoreWorldEvaluation& Core) { if (!VoxelMath::IsFinite(Core.SDF)) { return; } const bool bOwnsSweptBottom = !Core.bRoomFloor && Core.bHasSweptFloor && VoxelMath::IsFinite(Core.SweptFloorZ) && VoxelMath::IsFinite(Core.SweptFloorRadius) && WorldZ <= Core.SweptFloorZ + VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels; if (bOwnsSweptBottom) { Result = FMath::Min(Result, -Core.SDF); } else if (Core.SDF < 0.0f) { Result = FMath::Max(Result, -Core.SDF); } }; // Origin rooms are structural too. Reassert their air before either support writer so a // bridge/ridge cannot plug a landing, while the two floor writers remain last. { VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::PassageLandingAir); VF_ApplyOriginLandingAirMC( Result, WorldX, WorldY, WorldZ, CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ, CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius); } { VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::PassageLandingFloor); VF_ApplyOriginLandingFloorMC( Result, WorldX, WorldY, WorldZ, CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ, CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius); } if (bApplyBrickPassagePosts) { VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::PassageStructuralPosts); StrateManager->ApplyPassageStructuralPostsMC( Result, WorldX, WorldY, WorldZ, LandingBaseDensity, CP_Dist.BoundarySealThickness); } // Disturbance features are authored as a generic MC-space post and may add a ridge or // 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. VoxelGenLOD::FScopedReachCost TunnelCoreTailReachCost( VoxelGenLOD::ETileReachCostKind::TunnelCoreTail); const bool bTunnelCoreReachable = VoxelGenLOD::IsTunnelCoreReachable(); const bool bTunnelCoreBypass = VoxelGenLOD::IsTilePostReachBypassActive(); auto ApplyGraphLandingFloors = [&]() { // These are passage/origin landing posts, not tunnel-core writes. Their air and // structural post already ran before disturbances; the tunnel shape below reopens // its own continuous interior after the disturbance pass. Keep only the floor // reassertions here, in the existing order. { VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::PassageLandingFloor); VF_ApplyOriginLandingFloorMC( Result, WorldX, WorldY, WorldZ, CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ, CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius); } if (bApplyBrickPassagePosts) { VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::PassageLandingFloor); StrateManager->ApplyPassageLandingFloorMC( Result, WorldX, WorldY, WorldZ, LandingBaseDensity); } if (bApplyBrickPassagePosts) { VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::PassageLandingRoomFloor); StrateManager->ApplyPassageLandingRoomFloorMC( Result, WorldX, WorldY, WorldZ, LandingBaseDensity); } }; if (!VoxelDensityAblation::IsTunnelCoreOff() && (ActiveTunnelCoreCache->bValid || CP_UseOpStack)) { FTunnelCoreWorldEvaluation TunnelCore; bool bHaveTunnelCore = bHavePreDisturbanceTunnelCore; if (VoxelGenLOD::bTileCoreReachProofEnabled && VoxelGenLOD::bTileReachDiagnosticTile && !bTunnelCoreReachable && !bTunnelCoreBypass) { // The fused/source hand-off is also empty on the skipped path. Do not let the // default object turn this into a second tail evaluation. bHaveTunnelCore = false; } if (bHaveTunnelCore) { TunnelCore = PreDisturbanceTunnelCore; } if (!bHaveTunnelCore && ActiveTunnelCoreCache->bValid && (bTunnelCoreReachable || bTunnelCoreBypass)) { VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::TunnelCorePosts); FVector RoomQueryPosition(WorldX, WorldY, WorldZ); if (CP_Tunnel.VerticalScale != 1.0f && CP_Tunnel.VerticalScale > 0.0f) { RoomQueryPosition.Z = WorldZ / CP_Tunnel.VerticalScale; } RoomQueryPosition = VoxelCaveMorphology::ApplyCaveWarp( RoomQueryPosition, CP_Tunnel, static_cast(Seed)); TunnelCore = VoxelCaveMorphology::EvaluateTunnelCoreWorld( WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache, nullptr, VoxelGenLOD::ShouldUseSpatialIndex( CP_UseOpStack ? bUsedFusedEvaluator : true), &RoomQueryPosition, bRoomOwnsBottomValid ? &bRoomOwnsBottom : nullptr); bHaveTunnelCore = true; } if (bHaveTunnelCore) { // Reassert the actual swept-core field after generic writers. The old tail used a // fixed positive air value once the core was deeper than an inset; that hard // switch discarded the SDF magnitude and made marching-cubes intersections snap // to the sample lattice. The source now contains the same continuous world-chain // tunnel field. Union its interior air back over disturbances, and use the same // signed field only on the bottom side identified by the swept shape itself. This // restores the tunnel's carved floor when the surrounding room is already air, // without adding an authored slab or turning the whole room into tunnel geometry. ApplyTunnelCoreShape(TunnelCore); } if (bHaveTunnelCore) { if (!TunnelCore.bRoomFloor) { ApplyGraphLandingFloors(); } } else if (!bTunnelCoreReachable && !bTunnelCoreBypass) { ApplyGraphLandingFloors(); } } // Inter-strate passages own the same final D-floor contract as graph tunnels. This is // composed from the immutable construction-time profile after generic writers and the // graph overlap, so the legacy passage support slab can be disabled without losing the // floor to a disturbance refill. if (bApplyBrickPassagePosts) { VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::PassageLandingFloor); StrateManager->ApplyPassageNativeFloorMC( Result, WorldX, WorldY, WorldZ, LandingBaseDensity); } StructuralTailTimer.End(); DensityStructuralPostsTimer.End(); #if WITH_EDITOR if (bEvaluateDensityBatch) { GVoxelDensityBlockSession.BatchResults[DensityBatchIndex] = Result; } #endif } } else { // ── FALLBACK (no strate manager) ── // Use default TunnelNetwork params — produces generic caves. // `static` : ces params sont constants (construction par défaut), donc leur empreinte l'est // aussi. La calculer une fois évite un CRC par voxel sur un chemin qui n'en a aucun besoin. // `LayoutVersion = 0` : sans `StrateManager` il n'y a pas de layout, donc rien qui puisse // périmer — et l'empreinte constante suffit à distinguer ce cache de tous les autres. DensityPrologueTimer.End(); VoxelDensityProfile::FScopedTimer DensityCoreTimer( VoxelDensityProfile::EBucket::DensityCore); static const FStrateGenerationParams FallbackParams; static const uint32 FallbackFP = FCrc::MemCrc32(&FallbackParams, sizeof(FallbackParams)); Result = GetDensityWithParams(WorldX, WorldY, WorldZ, FallbackParams, FallbackFP, 0); DensityCoreTimer.End(); } #if WITH_EDITOR const int32 FinalDensityBatchCount = bEvaluateDensityBatch ? BatchSampleCount : 1; #else const int32 FinalDensityBatchCount = 1; #endif for (int32 FinalDensityBatchIndex = 0; FinalDensityBatchIndex < FinalDensityBatchCount; ++FinalDensityBatchIndex) { #if WITH_EDITOR if (bEvaluateDensityBatch) { const int32 BatchX = FinalDensityBatchIndex % GVoxelDensityBlockSession.SizeX; const int32 BatchY = (FinalDensityBatchIndex / GVoxelDensityBlockSession.SizeX) % GVoxelDensityBlockSession.SizeY; const int32 BatchZ = FinalDensityBatchIndex / (GVoxelDensityBlockSession.SizeX * GVoxelDensityBlockSession.SizeY); WorldX = static_cast(GVoxelDensityBlockSession.Origin.X + BatchX * GVoxelDensityBlockSession.Step); WorldY = static_cast(GVoxelDensityBlockSession.Origin.Y + BatchY * GVoxelDensityBlockSession.Step); WorldZ = static_cast(GVoxelDensityBlockSession.Origin.Z + BatchZ * GVoxelDensityBlockSession.Step); Result = GVoxelDensityBlockSession.BatchResults[FinalDensityBatchIndex]; VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker(); } #endif // The edge is the final generated structural invariant. Passage carving is ordered before it // in every archetype/stack, and this MC-facing pass also covers out-of-layout air plus any // disturbance that might otherwise carve back into the shell. Player edits remain separate // below and are deliberately still the user override path. VoxelDensityProfile::FScopedTimer DensityBoundarySealTimer( VoxelDensityProfile::EBucket::DensityBoundarySeal); { VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::XYEdgeSealOp); VF_ApplyXYEdgeSealMC(Result, WorldX, WorldY, WorldRadiusVoxels, EdgeSealThickness, 8.0f); } DensityBoundarySealTimer.End(); //========================================================================= // PLAYER MODIFICATIONS (diff layer) //========================================================================= // Applied LAST — player carving/filling overrides everything. // The diff layer returns density in internal convention (negative = carve). // Since Result is already in MC convention (negative = solid, positive = air), // we subtract the diff offset: // Carve (diff < 0) → Result -= negative → Result increases → more air ✓ // Fill (diff > 0) → Result -= positive → Result decreases → more solid ✓ VoxelDensityProfile::FScopedTimer DensityDiffLayerTimer( VoxelDensityProfile::EBucket::DensityDiffLayer); // The sync-remesh cache keeps the exact procedural field after every structural post, but // before player edits. The cached lattice is replayed with the current DiffLayer snapshot so // a carve sees the same subtraction order and float result as this canonical path. if (OutPreEditDensity != nullptr) { *OutPreEditDensity = Result; } const bool bDiffLayerHasModsForSample = DiffLayer != nullptr && #if WITH_EDITOR (bEvaluateDensityBatch ? bBrickHasDiffMods : DiffLayer->HasAnyMods()); #else DiffLayer->HasAnyMods(); #endif if (DiffLayer && bDiffLayerHasModsForSample) { // ── PER-CHUNK MOD SNAPSHOT ── // The old HasModifications + GetDensityOffset pair took the diff layer's RWLock + a TMap find // TWICE per voxel once any carve existed (~86k lock ops per tile task — during carve gameplay, // exactly when re-mesh latency matters). Snapshot a chunk's mod list ONCE per (chunk, version) // on this worker and evaluate it lock-free. DIRECT-MAPPED by the chunk coord's low bits (2 bits // per axis → 64 slots): the mesher's ±1 margin ring touches up to 27 neighbouring chunk coords // per tile, and any two coords within ±3 of each other land in DIFFERENT slots — so a tile task // never thrashes its own working set. Version bump (carve / clear) invalidates lazily per slot. struct FDiffSlot { FIntVector Chunk = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX); uint64 OwnerId = 0; uint32 Version = 0; TArray Mods; }; static thread_local FDiffSlot DiffSlots[64]; const uint64 DiffLayerOwnerId = DiffLayer->GetCacheLifetimeId(); const uint32 V = DiffLayer->GetModsVersion(); FDiffSlot& Slot = DiffSlots[(ChunkCoord.X & 3) | ((ChunkCoord.Y & 3) << 2) | ((ChunkCoord.Z & 3) << 4)]; if (Slot.OwnerId != DiffLayerOwnerId || Slot.Chunk != ChunkCoord || Slot.Version != V) { Slot.OwnerId = DiffLayerOwnerId; Slot.Chunk = ChunkCoord; Slot.Version = V; DiffLayer->GetChunkModsSnapshot(ChunkCoord, Slot.Mods); // ONE lock per chunk, not per voxel } if (Slot.Mods.Num() > 0) { Result -= UVoxelDiffLayer::EvaluateMods(Slot.Mods, WorldX, WorldY, WorldZ); } } DensityDiffLayerTimer.End(); #if WITH_EDITOR if (bEvaluateDensityBatch) { GVoxelDensityBlockSession.BatchResults[FinalDensityBatchIndex] = Result; } #endif } #if WITH_EDITOR if (bEvaluateDensityBatch) { GVoxelDensityBlockSession.bBatchSucceeded = true; } #endif VoxelDensityProfile::FScopedTimer DensityTailTimer( VoxelDensityProfile::EBucket::DensityTail); VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker(); if (bCompareFinalField) { const bool bFinalFieldDifferent = !VoxelGenLOD::SameFloatBits(Result, CanonicalFinalField); VoxelGenLOD::RecordSkippedPostComparison( VoxelGenLOD::ETilePostComparisonKind::FinalField, bFinalFieldDifferent); } return Result; } float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float WorldZ, const FStrateGenerationParams& Params, uint32 ParamsFingerprint, uint32 LayoutVersion, bool bApplyLegacyStructuralPosts, FTunnelCoreWorldEvaluation* OutTunnelCore, bool bCollectFusedDiagnostics, bool* OutRoomOwnsBottom) const { //========================================================================= // STRATE DENSITY FUNCTION (Morphology Pipeline) //========================================================================= // The density pipeline for underground caves: // // 1. Vertical scale // 2. Base density (everything starts solid) // 3. Cave warp: domain warp coordinates before SDF (bends rooms/tunnels organically) // 4. SDF morphology: rooms + tunnels carve the cave structure (using warped coords) // 4b. Surface roughness: 3D noise near cave walls (fBM/Ridged/Cellular + domain warp) // 4c. Terrain operations: terracing, layer lines, ribbing, cliff, scallop, arch, overhangs // 4d. Columns/Pillars: hash-placed vertical cylinders adding solid rock // 4e. Pits/Shafts: hash-placed tapered vertical voids carved into cave floors // 4f. Chimneys/Shafts: hash-placed tapered vertical voids carved UPWARD // 4g. Domes: hemispherical ceiling sculpting in cave chambers // 4h. Pinch/Bottleneck: ellipsoidal passage narrowing for chokepoints // 5. Worm tunnels: additional organic connectivity // 6. Boundary seal: solid rock at strate top/bottom // 7. Modifiers: passages between strates (punch through vertical seals) // 8. XY edge seal: global bounded-world shell wins over passages at the rim // // Convention: positive density = solid, negative = air (internally). // At the end, we negate for the MC table (negative = solid there). //========================================================================= if (OutTunnelCore != nullptr) { *OutTunnelCore = FTunnelCoreWorldEvaluation(); } if (OutRoomOwnsBottom != nullptr) { *OutRoomOwnsBottom = false; } if (bCollectFusedDiagnostics && VoxelDensityProfile::AreCountersEnabled()) { VoxelDensityProfile::AddCounter( VoxelDensityProfile::ECounter::FusedTunnelSamples); } const uint32 SeedU = (uint32)Seed; //========================================================================= // STEP 1: VERTICAL SCALE //========================================================================= float EffectiveZ = WorldZ; if (Params.VerticalScale != 1.0f && Params.VerticalScale > 0.0f) { EffectiveZ = WorldZ / Params.VerticalScale; } //========================================================================= // STEP 2: BASE DENSITY (everything starts solid) //========================================================================= float Density = Params.BaseDensity; //========================================================================= // STEP 3: CAVE WARP (domain warp the SDF skeleton) //========================================================================= // This is the KEY step that makes caves look natural instead of graph-like. // // The SDF morphology places rooms as geometric primitives (ellipsoids, boxes) // connected by straight capsule tunnels. Without warping, you can clearly see // the room-corridor-room graph structure — it looks artificial. // // By warping the world coordinates BEFORE evaluating the SDF, we bend the // entire cave field. Rooms become irregular blobs, tunnels become winding // passages. The skeleton is still there as the backbone, but noise pushes // and pulls it into shapes that look carved by geological forces. // // Two octaves: // - Large: CaveWarpFrequency → broad sweeping bends (room-scale) // - Medium: 3x frequency, 0.3x strength → wall-scale irregularity // // IMPORTANT: Only the SDF query uses warped coordinates. Roughness, terrain // ops, and hash-placed features use the REAL world position so they stay // geologically correct (terracing stays horizontal, columns stay vertical, etc.) float WarpedX = WorldX; float WarpedY = WorldY; float WarpedZ = EffectiveZ; if (!VoxelDensityAblation::IsCaveWarpOff() && Params.CaveWarpStrength > 0.0f) { const float WF = Params.CaveWarpFrequency; const float WS = Params.CaveWarpStrength; // Three independent Perlin fields offset by irrational-ish numbers keep the X/Y/Z warp // channels decorrelated. Perlin3D_x3 evaluates these same three scalar channels in one // SIMD batch on x86; its non-SIMD fallback is the original three-call expression. float WarpX, WarpY, WarpZ; VoxelNoise::Perlin3D_x3( WorldX * WF + VoxelHash::SeedOffset(SeedU, 0.37f), WorldY * WF + 1.3f, EffectiveZ * WF + 5.7f, WorldX * WF + 7.1f, WorldY * WF + VoxelHash::SeedOffset(SeedU, 0.59f), EffectiveZ * WF + 2.3f, WorldX * WF + 11.3f, WorldY * WF + 9.7f, EffectiveZ * WF + VoxelHash::SeedOffset(SeedU, 0.41f), WarpX, WarpY, WarpZ); WarpedX += WarpX * VOXEL_NOISE_SCALE * WS; WarpedY += WarpY * VOXEL_NOISE_SCALE * WS; WarpedZ += WarpZ * VOXEL_NOISE_SCALE * WS; } //========================================================================= // STEP 4: SDF MORPHOLOGY (rooms + tunnels) — CACHED PER CHUNK //========================================================================= // The room list and tunnel connections are IDENTICAL for all voxels in a // chunk. Without caching, we rebuild them 32,768 times (32³ voxels). // With thread_local caching: build once, evaluate 32K times with just SDF math. // // The cache is keyed on (ChunkX, ChunkY, StrateIndex, Seed). When the key // changes (new chunk or strate), the cache is rebuilt. thread_local ensures // each task thread has its own cache — no locking needed. // // The warped position is used for SDF evaluation (bends rooms/tunnels), // but the cache search area is expanded by CaveWarpStrength to ensure // all reachable rooms are included regardless of warp displacement. // // Declared before the RoomDensity if-block so SDFCache and NearestRoomIdx // are also visible to the terrain ops block further below. thread_local FChunkSDFCache SDFCache; // Cache validity is tracked by the SEARCH BOX the cache was built for, NOT by chunk // equality. Gradient-normal sampling queries at WorldX±1 (and warp displacement) can // step a voxel outside the chunk; with chunk-equality keying that flipped the key and // rebuilt the (now expensive) cache every boundary cell. Since the stored rooms cover // the search box + MaxInfluence, any query INSIDE the box is correct — so we only // rebuild when the query actually leaves the box. Result: one build per chunk, no thrash. thread_local float CachedSMinX = 1.0f, CachedSMaxX = -1.0f; // start invalid (min > max) thread_local float CachedSMinY = 0.0f, CachedSMaxY = 0.0f; thread_local int32 CachedStrate = INT32_MIN; thread_local uint32 CachedSeed = 0; // ⚠️ AUDIT §C2 — `CachedFingerprint` et `CachedLayout` ci-dessous sont OBLIGATOIRES dans cette clé. // La clé ci-dessus décrit la GÉOMÉTRIE de la fenêtre (boîte, strate, seed) et rien de ce qui // détermine les PARAMS avec lesquels les salles ont été cuites. Comme `GetGenerationParams` // blende à l'intérieur d'une strate (`Alpha` = f(chunk Z), et f(chunk XY) aussi en // `Interleaved`), deux chunks voisins produisent la MÊME clé avec des params DIFFÉRENTS, et le // deuxième se sert des salles du premier. Non déterministe entre pairs, parce que l'ordre des // workers décide lequel est « le premier » — exactement ce que §2.6.1 interdit. // // Pourquoi ça ne casse PAS l'invariant de perf de §8.10 : la clé reste une BOÎTE, donc les // sondes de gradient à `WorldX ± 1` ne font toujours pas tourner le cache. Ce qui le fait // tourner en plus, c'est un changement RÉEL de params — une fois par chunk dans une bande de // transition, ce qui est exactement le nombre de reconstructions nécessaire. thread_local uint32 CachedFingerprint = 0xFFFFFFFFu; thread_local uint32 CachedLayout = 0xFFFFFFFFu; thread_local bool CachedUsesFusedCacheWindow = false; thread_local bool CachedUsesTileCacheWindow = false; thread_local FIntVector CachedTileOrigin = FIntVector::ZeroValue; thread_local int32 CachedTileStep = 0; thread_local int32 CachedTileCells = 0; thread_local uint64 CachedManagerLifetimeId = 0; thread_local const TArray* CachedTerrainOps = nullptr; // The envelope proof is constant for a cache-policy key. Do not run the complete room/edge // reach calculation for every density sample: that would turn a build-time guard into a // per-sample cost on the LOD0 floor. thread_local bool CachedTilePolicyValid = false; thread_local bool CachedTilePolicyResult = false; thread_local bool CachedTilePolicyHasContext = false; thread_local bool CachedTilePolicyFusedPath = false; thread_local int32 CachedTilePolicySampleStep = 0; thread_local FIntVector CachedTilePolicyOrigin = FIntVector::ZeroValue; thread_local int32 CachedTilePolicyStep = 0; thread_local int32 CachedTilePolicyCells = 0; thread_local uint32 CachedTilePolicyFingerprint = 0xFFFFFFFFu; thread_local uint32 CachedTilePolicyLayout = 0xFFFFFFFFu; thread_local uint64 CachedTilePolicyManagerLifetimeId = 0; thread_local const TArray* CachedTilePolicyTerrainOps = nullptr; const bool bUseFusedCacheWindow = OutTunnelCore != nullptr; const bool bUseSpatialIndex = VoxelGenLOD::ShouldUseSpatialIndex(bUseFusedCacheWindow); FIntVector RequestedTileOrigin = FIntVector::ZeroValue; int32 RequestedTileStep = 1; int32 RequestedTileCells = 0; const uint64 CurrentManagerLifetimeId = StrateManager ? StrateManager->GetCacheLifetimeId() : 0; const int32 QueryChunkX = FMath::FloorToInt(WorldX / (float)CHUNK_SIZE); const int32 QueryChunkY = FMath::FloorToInt(WorldY / (float)CHUNK_SIZE); const int32 QueryChunkZ = FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE); const int32 CacheRegionChunks = bUseFusedCacheWindow ? 4 : 1; const int32 CacheRegionSize = CHUNK_SIZE * CacheRegionChunks; const int32 CacheRegionX = FMath::FloorToInt( WorldX / static_cast(CacheRegionSize)); const int32 CacheRegionY = FMath::FloorToInt( WorldY / static_cast(CacheRegionSize)); // BuildChunkCache bakes the selected immutable terrain-operation pool into each stored room. // A tile may cross a strate-definition boundary in XY, so the pool identity is part of the // cache key just like the generation-parameter fingerprint. The manager lifetime guards an // allocator-reused definition address after a live layout/world change. const TArray* TerrainOps = nullptr; if (StrateManager) { UVoxelStrateDefinition* Def = StrateManager->GetStrateForChunk( FIntVector( bUseFusedCacheWindow ? QueryChunkX : CacheRegionX, bUseFusedCacheWindow ? QueryChunkY : CacheRegionY, QueryChunkZ)); if (Def) TerrainOps = &Def->TerrainOperations; } const bool bTileWindowEligible = VoxelGenLOD::IsTileCacheWindowEnabled( bUseFusedCacheWindow); const bool bHasTileWindowContext = VoxelGenLOD::GetThreadTileCacheWindow( RequestedTileOrigin, RequestedTileStep, RequestedTileCells); const bool bTilePolicyKeyChanged = !CachedTilePolicyValid || CachedTilePolicyHasContext != (bTileWindowEligible && bHasTileWindowContext) || CachedTilePolicyFusedPath != bUseFusedCacheWindow || CachedTilePolicySampleStep != RequestedTileStep || CachedTilePolicyOrigin != RequestedTileOrigin || CachedTilePolicyStep != RequestedTileStep || CachedTilePolicyCells != RequestedTileCells || CachedTilePolicyFingerprint != ParamsFingerprint || CachedTilePolicyLayout != LayoutVersion || CachedTilePolicyManagerLifetimeId != CurrentManagerLifetimeId || CachedTilePolicyTerrainOps != TerrainOps; if (bTilePolicyKeyChanged) { CachedTilePolicyValid = true; CachedTilePolicyHasContext = bTileWindowEligible && bHasTileWindowContext; CachedTilePolicyFusedPath = bUseFusedCacheWindow; CachedTilePolicySampleStep = RequestedTileStep; CachedTilePolicyOrigin = RequestedTileOrigin; CachedTilePolicyStep = RequestedTileStep; CachedTilePolicyCells = RequestedTileCells; CachedTilePolicyFingerprint = ParamsFingerprint; CachedTilePolicyLayout = LayoutVersion; CachedTilePolicyManagerLifetimeId = CurrentManagerLifetimeId; CachedTilePolicyTerrainOps = TerrainOps; CachedTilePolicyResult = CachedTilePolicyHasContext && VoxelCaveMorphology::IsRoomGraphWindowInvariant(Params, TerrainOps); } const bool bUseTileCacheWindow = CachedTilePolicyResult; // Index of the room with the smallest (most-inside) SDF for this voxel. // Written by EvaluateSDFCached, read by the terrain ops block to pick the // per-room terrain op. -1 means no room is nearby (deep-solid or no rooms). thread_local int32 NearestRoomIdx = -1; float CaveSDF = FLT_MAX; if (Params.RoomDensity > 0.0f && Params.RoomSpacing > 0.0f) { // Get strate index for unique caves per strate. // MEMOISED: the index only changes at chunk-Z boundaries, yet this ran the strate layout's // linear scan PER VOXEL. Key = (chunk-Z, layout version) — the version guards against editor // rebuilds (RebuildStrates / live edit) serving a stale index. The lookup queries the BAND // CENTRE so the result is a pure function of the key (order-independent, window-invariant). int32 StrateIdx = 0; if (StrateManager) { thread_local int32 SI_ChunkZ = INT32_MAX; thread_local uint32 SI_Version = 0xFFFFFFFFu; thread_local int32 SI_Index = 0; const int32 QZ = FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE); const uint32 LV = StrateManager->GetLayoutVersion(); if (QZ != SI_ChunkZ || LV != SI_Version) { SI_ChunkZ = QZ; SI_Version = LV; // GetStrateIndex expects Unreal world units (divides by VOXEL_SIZE internally); // WorldZ here is in voxels, so multiply by VOXEL_SIZE. SI_Index = StrateManager->GetStrateIndex(((float)QZ + 0.5f) * CHUNK_SIZE * VOXEL_SIZE); } StrateIdx = SI_Index; } // Rebuild only when the WARPED query (what EvaluateSDFCached uses) leaves the // cached search box, or the strate/seed changed. const bool bNeedRebuild = StrateIdx != CachedStrate || (uint32)Seed != CachedSeed || ParamsFingerprint != CachedFingerprint || LayoutVersion != CachedLayout || CurrentManagerLifetimeId != CachedManagerLifetimeId || TerrainOps != CachedTerrainOps || bUseFusedCacheWindow != CachedUsesFusedCacheWindow || bUseTileCacheWindow != CachedUsesTileCacheWindow || (bUseTileCacheWindow && (RequestedTileOrigin != CachedTileOrigin || RequestedTileStep != CachedTileStep || RequestedTileCells != CachedTileCells)) || WarpedX < CachedSMinX || WarpedX > CachedSMaxX || WarpedY < CachedSMinY || WarpedY > CachedSMaxY; if (bNeedRebuild) { // The lowered evaluator uses the same window-invariance contract as the prepared // room-graph source. With the switch enabled, the search box is the exact requesting // tile footprint plus the one-sample normal halo and deterministic warp margin. A // tile worker can visit neighbouring chunk keys in an arbitrary order without // rebuilding the graph at every coarse sample. Point queries without a tile context // retain the historical fused four-chunk / ordinary one-chunk fallback. const float ChunkMinX = CacheRegionX * (float)CacheRegionSize; const float ChunkMinY = CacheRegionY * (float)CacheRegionSize; const float ChunkMaxX = ChunkMinX + (float)CacheRegionSize; const float ChunkMaxY = ChunkMinY + (float)CacheRegionSize; const float WarpMargin = FMath::Abs(Params.CaveWarpStrength) * VOXEL_NOISE_SCALE * 1.5f; float SMinX = ChunkMinX; float SMinY = ChunkMinY; float SMaxX = ChunkMaxX; float SMaxY = ChunkMaxY; if (bUseTileCacheWindow) { const float TileMinX = static_cast(RequestedTileOrigin.X); const float TileMinY = static_cast(RequestedTileOrigin.Y); const float TileExtent = static_cast( static_cast(RequestedTileStep) * static_cast(RequestedTileCells)); const float TileMaxX = TileMinX + TileExtent; const float TileMaxY = TileMinY + TileExtent; const float TileHalo = static_cast(RequestedTileStep) + 2.0f; const float Expansion = WarpMargin + TileHalo; SMinX = TileMinX - Expansion; SMinY = TileMinY - Expansion; SMaxX = TileMaxX + Expansion; SMaxY = TileMaxY + Expansion; } else { const float Expansion = WarpMargin + 2.0f; SMinX = ChunkMinX - Expansion; SMinY = ChunkMinY - Expansion; SMaxX = ChunkMaxX + Expansion; SMaxY = ChunkMaxY + Expansion; } VoxelCaveMorphology::BuildChunkCache( SDFCache, SMinX, SMinY, SMaxX, SMaxY, Params, (uint32)Seed, StrateIdx, TerrainOps, ERoomGraphBuildSite::GeneratorTile ); UpdateTunnelCoreTileReach(SDFCache, bUseTileCacheWindow); CachedSMinX = SMinX; CachedSMaxX = SMaxX; CachedSMinY = SMinY; CachedSMaxY = SMaxY; CachedStrate = StrateIdx; CachedSeed = (uint32)Seed; CachedFingerprint = ParamsFingerprint; CachedLayout = LayoutVersion; CachedUsesFusedCacheWindow = bUseFusedCacheWindow; CachedUsesTileCacheWindow = bUseTileCacheWindow; CachedTileOrigin = bUseTileCacheWindow ? RequestedTileOrigin : FIntVector::ZeroValue; CachedTileStep = bUseTileCacheWindow ? RequestedTileStep : 0; CachedTileCells = bUseTileCacheWindow ? RequestedTileCells : 0; CachedManagerLifetimeId = CurrentManagerLifetimeId; CachedTerrainOps = TerrainOps; } // Evaluate SDF using cached rooms and tunnels (WARPED coordinates). // Also writes NearestRoomIdx — the room with minimum SDF contribution // at this voxel's position. Used by terrain ops below to pick per-room params. NearestRoomIdx = -1; const FVector WorldTunnelPosition(WorldX, WorldY, WorldZ); CaveSDF = VoxelCaveMorphology::EvaluateSDFCached( WarpedX, WarpedY, WarpedZ, SDFCache, Params.SDFBlendRadius, &NearestRoomIdx, bUseSpatialIndex, &WorldTunnelPosition, OutRoomOwnsBottom ); // ── PIT & CHIMNEY SDF INTEGRATION ── // Pits and chimneys are SmoothMin'd into CaveSDF here, using REAL // (unwarped) world coordinates. This is intentional: pit positions come // from unwarped room centers, so evaluating them in warped space would // misalign them. Rooms also use unwarped positions — warping is purely a // query-coordinate bend, not a data-space transform. // // Including pits in CaveSDF means: // - The CarveFactor block below handles their density naturally // - SmoothMin at the pit-to-room junction creates the same organic // 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 if (!VoxelDensityAblation::IsPitChimneySDFOff()) { auto EvaluatePit = [&](int32 PitIndex) { 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) { 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; 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); } // Convert SDF to density carving: // CaveSDF < 0 means we're inside a room/tunnel/pit → carve to air // CaveSDF > 0 means we're in solid rock → no change // The transition zone around SDF=0 gives smooth cave walls if (CaveSDF < Params.SDFBlendRadius) { // Smooth carving factor: 1.0 deep inside cave, 0.0 at blend edge float CarveFactor = FMath::Clamp( (Params.SDFBlendRadius - CaveSDF) / FMath::Max(Params.SDFBlendRadius * 2.0f, 1.0f), 0.0f, 1.0f ); // Smoothstep for less abrupt transitions CarveFactor = SmoothStep01(CarveFactor); Density -= CarveFactor * Params.BaseDensity * 2.0f; } } // The canonical stack's source publishes this same structural result before its detail // modifiers run. The lowered path asks the scalar evaluator for it directly, so the common // 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 // floor projection for the current point without eagerly materialising a whole XY table. // The interpreted source has a lazy worker LRU for block evaluation; building that table // here would turn a cache-window change into hundreds of unnecessary column builds. const FVector RoomQueryPosition(WarpedX, WarpedY, WarpedZ); *OutTunnelCore = VoxelCaveMorphology::EvaluateTunnelCoreWorld( WorldX, WorldY, WorldZ, SDFCache, nullptr, bUseSpatialIndex, &RoomQueryPosition, OutRoomOwnsBottom); } //========================================================================= // EARLY-OUT: Skip detail work for deep-solid voxels //========================================================================= // ~70% of voxels in a chunk are deep inside solid rock, far from any cave. // Roughness, terrain ops, columns, pits, domes, pinch — ALL of these only // matter near cave surfaces. Skipping them for deep-solid voxels is the // single biggest performance win. // // We still need to run worm tunnels (they carve independently) and boundary // 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( bRunDetail ? VoxelDensityProfile::ECounter::FusedTunnelDetailSamples : VoxelDensityProfile::ECounter::FusedTunnelDetailSkipped); } VoxelDensityProfile::FScopedTimer FusedDetailTimer( VoxelDensityProfile::EBucket::FusedDetail); //========================================================================= // STEP 4b: SURFACE ROUGHNESS (volumetric, SDF-based) //========================================================================= // 3D noise near cave surfaces (where SDF ≈ 0) creates rocky detail: // overhangs, ledges, bumps, cracks. The SDF value directly tells us // how far from the surface we are — no need for separate floor/ceiling tracking. // // 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 (bRunDetail && Params.SurfaceRoughness > 0.0f) { float RoughnessDepth = Params.SurfaceRoughness * 2.0f; float DistFromSurface = FMath::Abs(CaveSDF); if (DistFromSurface < RoughnessDepth) { float RF = Params.RoughnessFrequency; // Base noise input positions (with seed offsets for uniqueness) FVector3f MainPos( WorldX * RF + VoxelHash::SeedOffset(SeedU, 11.3f), WorldY * RF + VoxelHash::SeedOffset(SeedU, 13.7f), EffectiveZ * RF + VoxelHash::SeedOffset(SeedU, 17.1f) ); FVector3f FinePos( WorldX * RF * 3.0f + VoxelHash::SeedOffset(SeedU, 19.1f) + 2000.0f, WorldY * RF * 3.0f + VoxelHash::SeedOffset(SeedU, 23.7f) + 2500.0f, EffectiveZ * RF * 3.0f + VoxelHash::SeedOffset(SeedU, 29.3f) + 3000.0f ); // DOMAIN WARPING: distort noise coordinates with a secondary field. // This breaks up repetitive patterns — coordinates are "bent" by noise, // making walls look like they were shaped by flowing water or pressure. // Each axis uses a different seed offset for independent warping. if (Params.DomainWarpStrength > 0.0f) { float WF = Params.DomainWarpFrequency; float WS = Params.DomainWarpStrength; // Sample three independent noise fields for X, Y, Z warp float WarpX = VoxelNoise::Perlin3D(FVector3f( WorldX * WF + VoxelHash::SeedOffset(SeedU, 5.2f), WorldY * WF + VoxelHash::SeedOffset(SeedU, 1.3f), EffectiveZ * WF + VoxelHash::SeedOffset(SeedU, 9.7f) )) * VOXEL_NOISE_SCALE * WS; float WarpY = VoxelNoise::Perlin3D(FVector3f( WorldX * WF + 100.0f + VoxelHash::SeedOffset(SeedU, 7.7f), WorldY * WF + 200.0f + VoxelHash::SeedOffset(SeedU, 3.1f), EffectiveZ * WF + 300.0f )) * VOXEL_NOISE_SCALE * WS; float WarpZ = VoxelNoise::Perlin3D(FVector3f( WorldX * WF + 400.0f, WorldY * WF + 500.0f + VoxelHash::SeedOffset(SeedU, 11.9f), EffectiveZ * WF + 600.0f + VoxelHash::SeedOffset(SeedU, 13.3f) )) * VOXEL_NOISE_SCALE * WS; // Apply warp to both noise positions FVector3f WarpOffset(WarpX, WarpY, WarpZ); MainPos += WarpOffset; FinePos += WarpOffset; } // NOISE TYPE SELECTION: sample the right noise function. // Octave counts go through VoxelGenLOD::Eff — far tiles (Step>1) drop // the sub-cell tail octaves (T2.b); LOD0 keeps the full counts. float RoughNoise, FineNoise; const int32 Oct3 = VoxelGenLOD::Eff(3); const int32 Oct2 = VoxelGenLOD::Eff(2); switch (Params.RoughnessNoiseType) { case EVoxelNoiseType::Ridged: // Ridged multifractal: sharp, craggy features RoughNoise = RidgedNoise3D(MainPos, Oct3); FineNoise = RidgedNoise3D(FinePos, Oct2); break; case EVoxelNoiseType::Mixed: // Blend: ridged structure softened by fBM RoughNoise = FractalNoise3D(MainPos, Oct3) * 0.5f + RidgedNoise3D(MainPos, Oct3) * 0.5f; FineNoise = FractalNoise3D(FinePos, Oct2) * 0.5f + RidgedNoise3D(FinePos, Oct2) * 0.5f; break; case EVoxelNoiseType::Cellular: // Worley/cellular: grotto, bubble-like patterns // Scale down because cellular noise has different frequency behavior RoughNoise = CellularNoise3D(MainPos); FineNoise = CellularNoise3D(FinePos); break; case EVoxelNoiseType::FBM: default: // Standard fBM: smooth, organic RoughNoise = FractalNoise3D(MainPos, Oct3); FineNoise = FractalNoise3D(FinePos, Oct2); break; } // Scale by VOXEL_NOISE_SCALE (all noise functions return ~[-1,1] but UE // Perlin internally returns ~[-0.8,0.8]) RoughNoise *= VOXEL_NOISE_SCALE; FineNoise *= VOXEL_NOISE_SCALE; float TotalRough = RoughNoise * Params.SurfaceRoughness + FineNoise * Params.SurfaceRoughness * 0.4f; // Inside definite cave air (CaveSDF < 0), roughness must never add solid back. // Without this clamp, barely-negative voxels near tunnel junctions or pit rims // get pushed back to solid by roughness → thin lids, membrane walls, bad seams. // Roughness can still carve further into walls (negative values), just not fill. if (CaveSDF < 0.0f) { TotalRough = FMath::Min(TotalRough, 0.0f); } // Fade out toward cave center (SurfaceFade = 1.0 at surface, 0.0 far away) float SurfaceFade = 1.0f - (DistFromSurface / RoughnessDepth); SurfaceFade = SurfaceFade * SurfaceFade; // Quadratic: concentrate near surface Density += TotalRough * SurfaceFade; } } //========================================================================= // STEP 4c-4h: TERRAIN OPERATIONS (only near cave surfaces) //========================================================================= // All terrain ops only affect density near cave walls. // Deep-solid voxels skip this entire block (bNearCaveSurface = false). if (bRunDetail) { //========================================================================= // PER-ROOM TERRAIN PARAMS //========================================================================= // Terrain ops are now per-room (not global). NearestRoomIdx was written by // EvaluateSDFCached above — it's the room with minimum SDF at this voxel. // // We copy Params and apply the nearest room's terrain op on top. // Base Params has all terrain op fields = 0 (disabled) since // BuildParamsFromDefinition does not merge them globally. // // Result: voxels inside different rooms see different terrain ops. // Rooms with no assigned op leave terrain fields at 0 → no terrain op. Clean. FStrateGenerationParams LocalTerrainParams = Params; if (NearestRoomIdx >= 0 && SDFCache.Rooms.IsValidIndex(NearestRoomIdx)) { const FCachedRoom& NR = SDFCache.Rooms[NearestRoomIdx]; if (NR.RoomOp) { // Writes only the op's specific fields (e.g. TerraceStepHeight for Terrace). // All other fields keep Params values unchanged. NR.RoomOp->ApplyTo(LocalTerrainParams, NR.RoomOpWeight); } } // Shadow outer Params inside this block so all terrain op code below // automatically uses the per-room values without any other changes. // The shadow is deliberate (per-room override), so silence C4457 here only. PRAGMA_DISABLE_SHADOW_VARIABLE_WARNINGS const FStrateGenerationParams& Params = LocalTerrainParams; PRAGMA_ENABLE_SHADOW_VARIABLE_WARNINGS // Geological features applied after roughness. These modify the density // field near cave surfaces to create specific shapes: terracing (step-like // ledges), layer lines (horizontal grooves), and overhangs (horizontal // shelf protrusions). Each operation is controlled by params in the strate // definition — set the main param to 0 to disable any operation. // --- TERRACING --- // Creates a staircase pattern on cave walls by offsetting density with // a smooth staircase function of world Z. The staircase quantizes the // cave surface into flat shelves connected by short cliff faces. // // Math: terracedZ = staircase(worldZ, stepH, hardness) // offset = terracedZ - worldZ // Where offset > 0 → more solid (shelf floor to walk on) // Where offset < 0 → more air (gap under the shelf above) if (Params.TerraceStepHeight > 0.0f && CaveSDF < FLT_MAX) { const float StepH = Params.TerraceStepHeight; const float DistFromSurface = FMath::Abs(CaveSDF); const float TerraceRange = StepH * 3.0f; // How far from surface the effect reaches if (DistFromSurface < TerraceRange) { // Surface orientation test: terracing only makes sense on horizontal surfaces // (cave floors). On vertical walls (pit shafts, tunnel sides) it creates ugly // horizontal ridges. We sample the SDF gradient in Z by querying Z±1: // an SDF has ≈unit gradient, so |dSDF/dZ| alone IS the normalized vertical // component — near 1 = floor/ceiling, near 0 = wall. (Z-only approximation: // the full 6-sample gradient normalization was 3× the SDF cost for the same // orientation signal; SmoothMin regions where |∇SDF| < 1 read slightly // "flatter", shifting where terraces fade on slopes — accepted visual delta.) // // We use the cached SDF so this costs two extra SDF evaluations per voxel, // only when near a surface — the common case is cheap (DistFromSurface > TerraceRange). const FVector WorldTunnelPositionZp1(WorldX, WorldY, WorldZ + 1.0f); const FVector WorldTunnelPositionZm1(WorldX, WorldY, WorldZ - 1.0f); float SDF_Zp1 = VoxelCaveMorphology::EvaluateSDFCached( WorldX, WorldY, WorldZ + 1.0f, SDFCache, Params.SDFBlendRadius, nullptr, bUseSpatialIndex, &WorldTunnelPositionZp1); float SDF_Zm1 = VoxelCaveMorphology::EvaluateSDFCached( WorldX, WorldY, WorldZ - 1.0f, SDFCache, Params.SDFBlendRadius, nullptr, bUseSpatialIndex, &WorldTunnelPositionZm1); float GZ = (SDF_Zp1 - SDF_Zm1) * 0.5f; // Normalized vertical component: 1 = perfectly horizontal surface (floor/ceiling) // 0 = perfectly vertical surface (wall) float SurfaceHorizontality = FMath::Clamp(FMath::Abs(GZ), 0.0f, 1.0f); // Only apply terrace where the surface is mostly horizontal (> ~45 degrees) // Smooth transition to avoid a hard cutoff at exactly 45 degrees float TerraceOrientFactor = FMath::Clamp((SurfaceHorizontality - 0.3f) / 0.4f, 0.0f, 1.0f); // Noise displacement: perturb Z before staircase to break up straight edges float NoisedZ = WorldZ; if (Params.TerraceNoiseDisplacement > 0.0f) { float DispNoise = FractalNoise3D(FVector3f( WorldX * 0.04f + VoxelHash::SeedOffset(SeedU, 31.1f), WorldY * 0.04f + VoxelHash::SeedOffset(SeedU, 37.3f), WorldZ * 0.02f + VoxelHash::SeedOffset(SeedU, 41.7f) ), VoxelGenLOD::Eff(2)) * VOXEL_NOISE_SCALE; NoisedZ += DispNoise * Params.TerraceNoiseDisplacement * StepH; } // Smooth staircase function: // K = NoisedZ / StepH (which "step" are we in?) // Frac = fractional part [0, 1) — position within the step // Edge controls transition sharpness (narrow edge = sharp cliff face) float K = NoisedZ / StepH; float FloorK = FMath::FloorToFloat(K); float Frac = K - FloorK; // Always [0, 1) // Build the stair profile: 0 in lower half, 1 in upper half, smooth transition float Edge = FMath::Lerp(0.45f, 0.02f, Params.TerraceHardness); float StairValue; if (Frac < 0.5f - Edge) { StairValue = 0.0f; // Lower flat region (below transition) } else if (Frac > 0.5f + Edge) { StairValue = 1.0f; // Upper flat region (above transition) } else { // Smoothstep through the transition zone float T = (Frac - (0.5f - Edge)) / (2.0f * Edge); StairValue = SmoothStep01(T); } // Reconstruct the terraced Z and compute offset from real Z float TerracedZ = (FloorK + StairValue) * StepH; float Offset = TerracedZ - NoisedZ; // Offset range: approximately [-StepH/2, +StepH/2] // Positive → below a shelf surface → add density (solid floor) // Negative → above a shelf → subtract density (air gap under next shelf) // Fade based on distance from cave surface (no effect deep in rock) float Fade = 1.0f - (DistFromSurface / TerraceRange); Fade = Fade * Fade; // Quadratic: concentrate near surface // TerraceOrientFactor suppresses terrace on vertical walls (pit shafts, etc.) Density += Offset * Fade * TerraceOrientFactor; } } // --- LAYER LINES --- // Horizontal grooves in cave walls — visible geological strata. // A sine wave along Z, sharpened to create thin lines, subtracts density // near cave surfaces. This carves narrow horizontal channels into walls // at regular intervals, like sedimentary rock layers in cross-section. if (Params.LayerLineSpacing > 0.0f && CaveSDF < FLT_MAX) { const float DistFromSurface = FMath::Abs(CaveSDF); const float LineRange = Params.LayerLineSpacing * 1.5f; // Influence depth into rock if (DistFromSurface < LineRange) { // Sine wave along Z: peaks at each line position float LinePhase = WorldZ * (2.0f * PI) / Params.LayerLineSpacing; float LineValue = VoxelMath::DetSin(LinePhase); // Sharpen to thin grooves: only carve where sine > 0, then cube it. // sin → max(sin, 0) → pow(_, 3) turns broad sine humps into thin spikes LineValue = FMath::Max(LineValue, 0.0f); LineValue = LineValue * LineValue * LineValue; // Cubic sharpening // Fade near surface float Fade = 1.0f - (DistFromSurface / LineRange); Fade = Fade * Fade; // Subtract density to carve the groove Density -= LineValue * Params.LayerLineDepth * Fade; } } // --- RIBBING --- // Parallel ridge patterns on walls/ceiling (like lava tubes). // Uses the same sine-along-Z approach as layer lines, but ADDS density // (protruding ribs) instead of subtracting (grooves). The sine is // half-wave rectified and smoothed to create rounded bumps. if (Params.RibbingSpacing > 0.0f && CaveSDF < FLT_MAX) { const float DistFromSurface = FMath::Abs(CaveSDF); const float RibRange = Params.RibbingSpacing * 1.5f; if (DistFromSurface < RibRange) { // Sine wave along Z, offset by half-period from layer lines float RibPhase = WorldZ * (2.0f * PI) / Params.RibbingSpacing + PI * 0.5f; float RibValue = VoxelMath::DetSin(RibPhase); // Half-wave rectify (only positive → ribs, not grooves) then smooth RibValue = FMath::Max(RibValue, 0.0f); RibValue = RibValue * RibValue; // Quadratic: rounder bump profile // Fade near surface float Fade = 1.0f - (DistFromSurface / RibRange); Fade = Fade * Fade; // Add density to create protruding ribs Density += RibValue * Params.RibbingDepth * Fade; } } // --- OVERHANGS --- // Horizontal shelf-like protrusions from cave walls. // Uses 3D noise with much lower Z frequency than XY, so features extend // horizontally for long stretches before varying vertically. This creates // natural rocky overhangs and ledges independent of the terracing system. // // Only positive noise values create protrusions (asymmetric: rock extends // INTO the cave, never away). This gives scattered shelf-like features // rather than uniform displacement. if (Params.OverhangStrength > 0.0f && CaveSDF < FLT_MAX) { const float DistFromSurface = FMath::Abs(CaveSDF); const float OverhangRange = Params.OverhangDepth * 2.0f; if (DistFromSurface < OverhangRange) { // Low Z frequency (0.15x of XY) → features extend horizontally float OverhangNoise = FractalNoise3D(FVector3f( WorldX * Params.OverhangFrequency + VoxelHash::SeedOffset(SeedU, 53.1f), WorldY * Params.OverhangFrequency + VoxelHash::SeedOffset(SeedU, 59.3f), EffectiveZ * Params.OverhangFrequency * 0.15f + VoxelHash::SeedOffset(SeedU, 61.7f) ), VoxelGenLOD::Eff(2)) * VOXEL_NOISE_SCALE; // Only where noise is positive → protrusions (not recesses) if (OverhangNoise > 0.0f) { float Fade = 1.0f - (DistFromSurface / OverhangRange); Fade = Fade * Fade; // Add density = extend solid rock into cave = overhang shelf Density += OverhangNoise * Params.OverhangDepth * Params.OverhangStrength * Fade; } } } // --- CLIFF SHARPENING --- // Steepens vertical faces by amplifying the Z-axis density gradient. // Where the cave surface is already somewhat vertical (density changes // quickly along Z), this pushes it toward a sheer cliff face. // // Math: sample density at Z+1 and Z-1, compute vertical gradient. // Where gradient is steep AND we're near the cave surface: // If we're in the UPPER portion of the cliff → subtract density (more air) // If we're in the LOWER portion → add density (more solid) // This squeezes the transition zone, making it near-vertical. if (Params.CliffStrength > 0.0f && CaveSDF < FLT_MAX) { const float DistFromSurface = FMath::Abs(CaveSDF); const float CliffRange = 8.0f; // Only affect voxels within 8 of surface if (DistFromSurface < CliffRange) { // Approximate vertical gradient via the CaveSDF sign and position. // Near the surface (CaveSDF ≈ 0), the sign of CaveSDF tells us which // side we're on: negative = inside cave, positive = solid rock. // We use a noise-modulated vertical gradient to detect steep faces. float VertGrad = VoxelNoise::Perlin3D(FVector3f( WorldX * 0.05f + VoxelHash::SeedOffset(SeedU, 71.3f), WorldY * 0.05f + VoxelHash::SeedOffset(SeedU, 73.7f), EffectiveZ * 0.15f + VoxelHash::SeedOffset(SeedU, 79.1f) // 3x faster in Z → detects vertical features )) * VOXEL_NOISE_SCALE; // VertGrad near ±1 means terrain is changing fast vertically. // Multiply by sign of CaveSDF to get direction: // Positive result (solid side, gradient pointing up) → add more solid // Negative result (air side) → carve more air float CliffEffect = VertGrad * CaveSDF * Params.CliffStrength; // Only apply where gradient is significant (abs > 0.3) // and fade with distance from surface if (FMath::Abs(VertGrad) > 0.3f) { float Fade = 1.0f - (DistFromSurface / CliffRange); Fade = Fade * Fade; Density += CliffEffect * Fade * 3.0f; } } } // --- SCALLOP --- // Water-erosion-like concave patterns on cave walls. // Uses cellular (Worley) noise near cave surfaces — the distance-to-nearest // feature point creates natural bowl-shaped indentations. Where the cellular // noise value is high (far from feature points = center of a cell), we // subtract density to carve shallow bowls into the wall. // // This gives limestone caves their characteristic scalloped appearance — // rows of smooth, concave depressions covering the walls. if (Params.ScallopStrength > 0.0f && CaveSDF < FLT_MAX) { const float DistFromSurface = FMath::Abs(CaveSDF); const float ScallopRange = Params.ScallopStrength * 4.0f; if (DistFromSurface < ScallopRange) { // Cellular noise: returns ~[-1, 1] where positive = cell interior (bowl) float SF = Params.ScallopFrequency; float ScallopNoise = CellularNoise3D(FVector3f( WorldX * SF + VoxelHash::SeedOffset(SeedU, 83.1f), WorldY * SF + VoxelHash::SeedOffset(SeedU, 89.3f), EffectiveZ * SF + VoxelHash::SeedOffset(SeedU, 97.7f) )); // Only carve where noise is positive (cell interiors = bowl centers) if (ScallopNoise > 0.0f) { float Fade = 1.0f - (DistFromSurface / ScallopRange); Fade = Fade * Fade; // Subtract density to carve concave bowls Density -= ScallopNoise * Params.ScallopStrength * Fade; } } } // --- ARCH / BRIDGE (room-relative) --- // Horizontal rock bridges spanning the room interior. // Anchored to the nearest room: each arch spans from one side of the room // to the other at a hash-derived height within the room's Z range. // Up to MaxArches per room; ArchDensity = probability per slot. if (Params.ArchDensity > 0.0f && CaveSDF < Params.SDFBlendRadius && CaveSDF < FLT_MAX && NearestRoomIdx >= 0) { const FCachedRoom& Room = SDFCache.Rooms[NearestRoomIdx]; const FVector VoxPos(WorldX, WorldY, WorldZ); for (int32 i = 0; i < 3; i++) { const FCachedArch& Arch = Room.Arches[i]; if (!Arch.bActive) { continue; } const float ArchSDF = VoxelSDF::Capsule( VoxPos, Arch.EndpointA, Arch.EndpointB, Arch.Radius); const float ArchBlend = 2.0f; if (ArchSDF < ArchBlend) { float Fill = FMath::Clamp((ArchBlend - ArchSDF) / (ArchBlend * 2.0f), 0.0f, 1.0f); Fill = SmoothStep01(Fill); Density += Fill * Arch.BaseDensity * 1.5f; } } } //========================================================================= // STEP 4d: COLUMNS (pre-baked from BuildChunkCache) //========================================================================= // Columns are now pre-baked into SDFCache.Columns — no NearestRoomIdx needed. // The old per-voxel approach had columns appear/disappear mid-height when // the owning room changed (NearestRoomIdx switch). Pre-baking fixes this. auto EvaluateColumn = [&](int32 ColumnIndex) { const FCachedColumn& Col = SDFCache.Columns[ColumnIndex]; const float DX = WorldX - Col.CenterX; const float DY = WorldY - Col.CenterY; const float XYDistSq = DX * DX + DY * DY; if (XYDistSq > Col.BoundXYRadiusSq) return; const float CylSDF = FMath::Sqrt(XYDistSq) - Col.Radius; const float ColBlend = 3.0f; if (CylSDF < ColBlend) { float Fill = FMath::Clamp((ColBlend - CylSDF) / (ColBlend * 2.0f), 0.0f, 1.0f); Fill = SmoothStep01(Fill); Density += Fill * Col.BaseDensity * 1.5f; } }; VF_ForEachChunkSDFSpatialCandidate( SDFCache.ColumnSpatialIndex, SDFCache.Columns.Num(), WorldX, WorldY, EvaluateColumn, bUseSpatialIndex); //========================================================================= // STEP 4g: DOMES (room-relative hemispherical ceilings) //========================================================================= // Domes carve upward from the room's upper area, creating cathedral ceilings. // DmCenterZ anchored to the room's Z center so the dome sits naturally // in the ceiling zone rather than floating at arbitrary strate heights. if (Params.DomeDensity > 0.0f && CaveSDF < Params.SDFBlendRadius && CaveSDF < FLT_MAX && NearestRoomIdx >= 0) { const FCachedRoom& Room = SDFCache.Rooms[NearestRoomIdx]; // Up to 2 domes per room (large rooms can have multiple cathedral pockets) const int32 MaxDomes = 2; for (int32 i = 0; i < MaxDomes; i++) { uint32 DH = VoxelHash::Mix(Room.Hash ^ (0xD0AE0u + (uint32)i * 8191u)); if (VoxelHash::ToFloat01(DH) > Params.DomeDensity) continue; // XY: near room center (domes are wide, keep them centered) uint32 DH2 = VoxelHash::Mix(DH ^ 0xD0A0u); float DmX = Room.Center.X + VoxelHash::ToFloatSigned(DH2) * Room.RadiusXY * 0.4f; float DmY = Room.Center.Y + VoxelHash::ToFloatSigned(VoxelHash::Mix(DH2)) * Room.RadiusXY * 0.4f; // Radius: cap at room radius so dome fits inside the room uint32 DH3 = VoxelHash::Mix(DH2 ^ 0x90DEu); float DmRadius = FMath::Min( FMath::Lerp(Params.DomeMinRadius, Params.DomeMaxRadius, VoxelHash::ToFloat01(DH3)), Room.RadiusXY * 0.85f ); // Dome center Z: upper portion of the room (ceiling area) uint32 DH4 = VoxelHash::Mix(DH3 ^ 0xCAFEu); float DmCenterZ = Room.Center.Z + Room.RadiusZ * 0.2f + VoxelHash::ToFloat01(DH4) * Room.RadiusZ * 0.3f; float DmHeight = DmRadius * Params.DomeHeightRatio; // Quick Z reject if (WorldZ > DmCenterZ + DmHeight + 3.0f || WorldZ < DmCenterZ - 3.0f) continue; float DXDm = WorldX - DmX; float DYDm = WorldY - DmY; float DZDm = WorldZ - DmCenterZ; // Only carve upward from the dome center anchor if (DZDm < 0.0f) continue; // Half-ellipsoid SDF (upward only) float NormX = DXDm / DmRadius; float NormY = DYDm / DmRadius; float NormZ = DZDm / DmHeight; float EllipDist = FMath::Sqrt(NormX * NormX + NormY * NormY + NormZ * NormZ) - 1.0f; float DomeSDF = EllipDist * FMath::Min(DmRadius, DmHeight); const float DmBlend = 3.0f; if (DomeSDF < DmBlend) { float Carve = FMath::Clamp((DmBlend - DomeSDF) / (DmBlend * 2.0f), 0.0f, 1.0f); Carve = SmoothStep01(Carve); Density -= Carve * Params.BaseDensity * 1.5f; } } } //========================================================================= // STEP 4h: PINCH / BOTTLENECK (room-relative passage narrowing) //========================================================================= // Pinches squeeze a passage from the sides. Placed at the room's perimeter // area (high-radius offset from center) so they act on tunnel entrances // and room edges, not the open center of the room. if (Params.PinchDensity > 0.0f && CaveSDF < Params.SDFBlendRadius && CaveSDF < FLT_MAX && NearestRoomIdx >= 0) { const FCachedRoom& Room = SDFCache.Rooms[NearestRoomIdx]; for (int32 i = 0; i < 3; i++) { const FCachedPinch& Pinch = Room.Pinches[i]; if (!Pinch.bActive) { continue; } const float DXPn = WorldX - Pinch.CenterX; const float DYPn = WorldY - Pinch.CenterY; const float DZPn = WorldZ - Pinch.CenterZ; // Quick reject if (FMath::Abs(DXPn) + FMath::Abs(DYPn) + FMath::Abs(DZPn) > Pinch.MaxExtent) continue; const float Along = DXPn * Pinch.CosAngle + DYPn * Pinch.SinAngle; const float Across = -DXPn * Pinch.SinAngle + DYPn * Pinch.CosAngle; const float NAlong = Along / Pinch.HalfLength; const float NAcross = Across / Pinch.HalfNarrow; const float NUp = DZPn / Pinch.HalfVertical; const float EllipDist = NAlong * NAlong + NAcross * NAcross + NUp * NUp; if (EllipDist < 1.0f) { float Fill = 1.0f - EllipDist; Fill = SmoothStep01(Fill); const float AxisDist = FMath::Sqrt(NAcross * NAcross + NUp * NUp); const float SideFactor = FMath::Clamp(AxisDist * 2.0f, 0.0f, 1.0f); Density += Fill * SideFactor * Pinch.BaseDensity * 1.5f; } } } //========================================================================= // FLOOR BIAS //========================================================================= // Adds density in the lower portion of rooms to counteract surface roughness // making floors bumpy and hard to walk on. Works by knowing how far below // the nearest room center we are — the closer to the floor, the more density // is added back, smoothing the roughness-induced relief. // // Only applies inside cave air (CaveSDF < 0) so it doesn't re-solidify walls. // The roughness clamp already prevents fill-back in confirmed air; this works // WITH that to give floors a gentler character than walls/ceilings. if (Params.FloorBias > 0.0f && NearestRoomIdx >= 0 && CaveSDF < 0.0f) { const FCachedRoom& NR = SDFCache.Rooms[NearestRoomIdx]; // NormZ: -1 = at room bottom, 0 = center, +1 = at ceiling const float NormZ = (WorldZ - NR.Center.Z) / FMath::Max(NR.RadiusZ, 1.0f); // Only apply below room center (floor zone). // Quadratic fade: strongest at floor, zero at center. if (NormZ < 0.0f) { float FloorFactor = NormZ * NormZ; // 0 at center, 1 at bottom Density += FloorFactor * Params.FloorBias; } } } // end bNearCaveSurface (terrain ops) FusedDetailTimer.End(); // Steps 4e / 4f (pits and chimneys) are now handled by the CaveSDF SmoothMin // block inserted above (between EvaluateSDFCached and the CarveFactor section). // The CarveFactor system carves them with the same density logic as rooms and // tunnels. No separate density subtraction here — that would double-carve. //========================================================================= // STEP 5: WORM TUNNELS (additional organic connectivity) //========================================================================= // Worm tunnels add secondary passages and organic connections // that the SDF graph doesn't create. They're noise-based, so they // produce natural winding paths that complement the room-and-corridor structure. // // HORIZONTAL BIAS: Z frequency is scaled up so tunnels prefer horizontal paths. // // NETWORK MASK: |N1|+|N2| < threshold carves tubes near the intersection of two noise // zero-surfaces — but wherever the fields merely GRAZE the threshold it leaves pinhole // pockets, and none of it is connected to anything (the far-field "confetti"). // WormNetworkRange masks the carve by distance to the room/tunnel network (CaveSDF is // already computed by Step 4 — free): full strength at the network, smooth fade to zero // at Range. Worms become braids/shortcuts hugging the cave system; no isolated speckle. // Range = 0 → unmasked legacy behaviour. Bonus: fully-masked voxels skip both Perlins. if (Params.WormStrength > 0.0f && Params.WormThreshold > 0.0f) { VoxelDensityProfile::FScopedTimer FusedWormTimer( VoxelDensityProfile::EBucket::FusedWorm); float NetworkMask = 1.0f; if (Params.WormNetworkRange > 0.0f) { if (CaveSDF >= Params.WormNetworkRange) // also true when no network (FLT_MAX) { NetworkMask = 0.0f; } else if (CaveSDF > 0.0f) { NetworkMask = 1.0f - SmoothStep01(CaveSDF / Params.WormNetworkRange); } } if (NetworkMask > 0.0f) { const bool bCollectWormDiagnostics = bCollectFusedDiagnostics && VoxelDensityProfile::AreCountersEnabled(); if (bCollectWormDiagnostics) { VoxelDensityProfile::AddCounter( VoxelDensityProfile::ECounter::WormEligibleSamples); } bool bWormBlockProofComputed = false; const bool bSkipWormBlock = GVoxelForgeWormBlockSkip != 0 && VF_TryGetWormBlockSkip( DensityCacheOwnerId, SeedU, ParamsFingerprint, LayoutVersion, WorldX, WorldY, WorldZ, Params, bWormBlockProofComputed); if (bWormBlockProofComputed && bSkipWormBlock && bCollectWormDiagnostics) { VoxelDensityProfile::AddCounter( VoxelDensityProfile::ECounter::WormBlockProofs); } if (bSkipWormBlock) { if (bCollectWormDiagnostics) { VoxelDensityProfile::AddCounter( VoxelDensityProfile::ECounter::WormBlockSkippedSamples); } } else { const VoxelWormField::FParameters WormParameters{ Params.WormFrequency, Params.WormHorizontalBias, Params.VerticalScale, SeedU}; const float WormValue = VoxelWormField::Evaluate( WorldX, WorldY, WorldZ, Params.WormThreshold, WormParameters); if (WormValue < Params.WormThreshold) { const float t = 1.0f - (WormValue / Params.WormThreshold); Density -= t * Params.WormStrength * NetworkMask; } } } FusedWormTimer.End(); } //========================================================================= // STEP 6: STRATE BOUNDARY SEAL (haut + bas) //========================================================================= VoxelDensityProfile::FScopedTimer FusedStructuralTimer( VoxelDensityProfile::EBucket::FusedStructural); { VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::OriginSpineOp); ApplyOriginSpine(Density, WorldX, WorldY, WorldZ, Params.StrateTopWorldZ, Params.StrateBottomWorldZ, Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius); } { VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::BoundarySealOp); ApplyBoundarySeal(Density, WorldZ, Params.StrateTopWorldZ, Params.StrateBottomWorldZ, Params.BoundarySealThickness, Params.BaseDensity); } //========================================================================= // STEP 7: INTER-STRATE PASSAGES (perce le seal) //========================================================================= if (StrateManager) { if (bApplyLegacyStructuralPosts) { VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::PassageModifier); StrateManager->ApplyPassageModifier( Density, WorldX, WorldY, WorldZ, Params.BaseDensity, Params.BoundarySealThickness); } else { // GetDensityAt owns one common MC-space structural tail after either the legacy or // operator-stack branch. Keep the actual tube carve here, but do not repeat its // landing air/floor/tunnel-air posts before the shared disturbance/post sequence. VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::PassageCarveOp); StrateManager->ApplyPassageCarvingOnly( Density, WorldX, WorldY, WorldZ, Params.BaseDensity, Params.BoundarySealThickness); } } if (bApplyLegacyStructuralPosts) { // Direct callers retain the legacy structural tail. The tunnel core itself is still // composed here as a continuous field; it is not converted into a fixed-density slab. const bool bTunnelCoreEnabled = !VoxelDensityAblation::IsTunnelCoreOff(); const float TunnelCoreSDF = bTunnelCoreEnabled && (Params.RoomDensity > 0.0f && Params.RoomSpacing > 0.0f) ? VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF( WorldX, WorldY, WorldZ, SDFCache, bUseSpatialIndex) : FLT_MAX; if (VoxelMath::IsFinite(TunnelCoreSDF) && TunnelCoreSDF < 0.0f) { Density = FMath::Min( Density, TunnelCoreSDF); } ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ, Params.StrateTopWorldZ, Params.StrateBottomWorldZ, Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius); // Fourth structural post: the XY edge wins over a passage near the rim. VF_ApplyXYEdgeSeal(Density, WorldX, WorldY, WorldRadiusVoxels, EdgeSealThickness, Params.BaseDensity); } FusedStructuralTimer.End(); // Convention MC: négatif = solide, positif = air. // La logique interne utilise positif = solide (plus lisible), donc on négate. return -Density; } //============================================================================= // SURFACE-WORLD GENERATOR (ECaveGeneratorType::SurfaceWorld) //============================================================================= // A heightfield terrain (fBM continents + ridged mountains + fine detail) under a // high solid "sky cap" ceiling, with a flattened beach band around the water line. // Open air fills the gap between ground and ceiling; water is a render-side overlay. float UVoxelGenerator::SampleSurfaceStructuralZ(float WorldX, float WorldY, const FSurfaceGenerationParams& Params, float& OutM) const { const float H = Params.StrateTopWorldZ - Params.StrateBottomWorldZ; const uint32 SeedU = (uint32)Seed; const float BottomZ = Params.StrateBottomWorldZ; // --- Heightfield (a function of XY only — Z is a fixed seed slice) --- const float GroundBase = BottomZ + H * Params.BaseGroundRelative; // Domain-warp the STRUCTURAL query coords (continents + mountains) so coastlines and // ridgelines wind organically instead of looking like axis-aligned noise blobs. Detail // noise stays on the real XY so fine bumps remain crisp and uncorrelated with the warp. float QX = WorldX, QY = WorldY; if (Params.HeightWarpStrength > 0.0f) { const float WF = Params.HeightWarpFrequency; const float wx = VoxelNoise::Perlin3D(FVector3f(WorldX * WF + VoxelHash::SeedOffset(SeedU, 0.31f), WorldY * WF + 4.2f, VoxelHash::SeedOffset(SeedU, 1.7f))); const float wy = VoxelNoise::Perlin3D(FVector3f(WorldX * WF + 8.6f, WorldY * WF + VoxelHash::SeedOffset(SeedU, 0.53f), VoxelHash::SeedOffset(SeedU, 2.9f))); QX += wx * VOXEL_NOISE_SCALE * Params.HeightWarpStrength; QY += wy * VOXEL_NOISE_SCALE * Params.HeightWarpStrength; } // Macro relief map [0,1]: where this XY sits on the plains <-> mountains spectrum. // M is the "mountainous-ness"; ReliefStrength=0 → M=1 everywhere (uniform terrain). const float Relief = SampleRelief(WorldX, WorldY, Params.ReliefFrequency, Params.ReliefContrast); const float M = FMath::Lerp(1.0f, Relief, Params.ReliefStrength); float Cont = FractalNoise3D(FVector3f( QX * Params.ContinentFrequency + VoxelHash::SeedOffset(SeedU, 3.1f), QY * Params.ContinentFrequency + VoxelHash::SeedOffset(SeedU, 5.7f), VoxelHash::SeedOffset(SeedU, 0.7f)), 4); // [-1,1] float Detail = FractalNoise3D(FVector3f( WorldX * Params.DetailFrequency + 11.0f, WorldY * Params.DetailFrequency + 22.0f, VoxelHash::SeedOffset(SeedU, 1.3f)), 3); // [-1,1] float Mountain = 0.0f; if (Params.MountainStrength > 0.0f) { float Ridge = RidgedNoise3D(FVector3f( QX * Params.MountainFrequency + 99.0f, QY * Params.MountainFrequency + 77.0f, VoxelHash::SeedOffset(SeedU, 0.9f)), 4); // [-1,1] Ridge = Ridge * 0.5f + 0.5f; // [0,1] peaks Mountain = Ridge * Params.MountainStrength * M; // mountains rise only in high-relief regions } // Plains keep a fraction of the continental swell; highlands get the full range. const float ContScale = FMath::Lerp(0.45f, 1.0f, M); float Terrain = GroundBase + Cont * Params.ElevationRange * 0.5f * ContScale + Mountain * Params.ElevationRange + Detail * Params.SurfaceRoughness; OutM = M; return Terrain; } float UVoxelGenerator::ComputeSurfaceTerrainZ(float WorldX, float WorldY, const FSurfaceGenerationParams& Params) const { // Raw structural ground + the relief "mountainous-ness" M (reused to relief-gate the ops). float M = 1.0f; float Terrain = SampleSurfaceStructuralZ(WorldX, WorldY, Params, M); // --- F20 heightfield terrain ops (all off by default → byte-identical when unset) --- // CLIFF (slope-conditioned STEEPENING): where the surface is already steep, push the height // AWAY from the local mean so gentle slopes become sheer walls / canyon faces; gentle ground // stays untouched. Slope + local mean come from central differences of the STRUCTURAL field // (no op feedback), so it tracks the real landform. The one op that costs extra samples — // only when enabled (4 structural resamples, per-column-cheap). if (Params.CliffStrength > 0.0f) { const float D = FMath::Max(Params.CliffSampleDist, 0.5f); float Ms; // relief scratch (unused — we only need the heights) const float Zxp = SampleSurfaceStructuralZ(WorldX + D, WorldY, Params, Ms); const float Zxm = SampleSurfaceStructuralZ(WorldX - D, WorldY, Params, Ms); const float Zyp = SampleSurfaceStructuralZ(WorldX, WorldY + D, Params, Ms); const float Zym = SampleSurfaceStructuralZ(WorldX, WorldY - D, Params, Ms); const float dZdX = (Zxp - Zxm) / (2.0f * D); const float dZdY = (Zyp - Zym) / (2.0f * D); const float Slope = FMath::Sqrt(dZdX * dZdX + dZdY * dZdY); // rise per voxel of XY const float Thr = FMath::Max(Params.CliffSlopeThreshold, 0.05f); // 0 below the threshold, ramps to 1 within one threshold-width above it. const float SlopeGate = FMath::Clamp((Slope - Thr) / Thr, 0.0f, 1.0f); if (SlopeGate > 0.0f) { // Local smoothed reference; push the true height away from it → steepen the wall. const float Ref = 0.25f * (Zxp + Zxm + Zyp + Zym); const float Gain = Params.CliffStrength * SlopeGate * Params.CliffSharpness; Terrain += (Terrain - Ref) * Gain; } } // TERRACE (relief-gated): quantize height into plateaus, only in high-relief regions and only // as strongly as TerraceStrength asks. TerraceHardness controls the riser: soft rounded steps // (0) → crisp flat-topped mesas with near-vertical walls (1). Layered cliffs/mesas up top, // smooth lowlands. if (Params.TerraceStrength > 0.0f && Params.TerraceHeight > 0.0f) { const float StepH = Params.TerraceHeight; const float T = Terrain / StepH; const float K = FMath::FloorToFloat(T); const float Frac = T - K; // [0,1) position within the step // Hardness widens the flat plateau and sharpens the riser: half-transition width goes // from 0.5 (a smooth S-curve, no plateau) at Hardness=0 to ~0.03 (sharp wall) at 1. const float W = FMath::Lerp(0.5f, 0.03f, FMath::Clamp(Params.TerraceHardness, 0.0f, 1.0f)); const float Fs = SmoothStep01(FMath::Clamp((Frac - (0.5f - W)) / (2.0f * W), 0.0f, 1.0f)); const float Stepped = (K + Fs) * StepH; Terrain = FMath::Lerp(Terrain, Stepped, Params.TerraceStrength * M); } // LAYER LINES: fine sedimentary shelves cut into slopes (exposed rock strata). Pull the // surface weakly toward each band plane; reads on slopes, invisible on flats (uniform shift). if (Params.LayerLineDepth > 0.0f && Params.LayerLineSpacing > 0.0f) { const float Phase = Terrain * (2.0f * PI / Params.LayerLineSpacing); Terrain -= VoxelMath::DetSin(Phase) * Params.LayerLineDepth; } // Beach: flatten terrain toward the water line within BeachWidth. (Water level is // strate-global — forced from the strate — so the water plane stays continuous.) const float H = Params.StrateTopWorldZ - Params.StrateBottomWorldZ; const float WaterZ = Params.StrateBottomWorldZ + H * Params.WaterLevelRelative; if (Params.WaterLevelRelative > 0.0f && Params.BeachWidth > 0.0f) { const float DAbs = FMath::Abs(Terrain - WaterZ); if (DAbs < Params.BeachWidth) { float T = SmoothStep01(DAbs / Params.BeachWidth); Terrain = FMath::Lerp(WaterZ, Terrain, T); } } return Terrain; } float UVoxelGenerator::ComputeSurfaceCeiling(float WorldX, float WorldY, const FSurfaceGenerationParams& Params) const { const float H = Params.StrateTopWorldZ - Params.StrateBottomWorldZ; const uint32 SeedU = (uint32)Seed; float CeilZ = Params.StrateBottomWorldZ + H * Params.CeilingRelative; // Domain-warp the broad/ridge query coords so ceiling ridgelines and valleys wind // organically (mirrors the ground heightfield's HeightWarp). Fine bumps below stay on the // true XY so detail remains crisp and uncorrelated. 0 ⇒ no warp. float QX = WorldX, QY = WorldY; if (Params.CeilingWarpStrength > 0.0f) { const float WF = Params.CeilingWarpFrequency; const float wx = VoxelNoise::Perlin3D(FVector3f(WorldX * WF + VoxelHash::SeedOffset(SeedU, 0.71f), WorldY * WF + 2.3f, VoxelHash::SeedOffset(SeedU, 3.3f))); const float wy = VoxelNoise::Perlin3D(FVector3f(WorldX * WF + 6.1f, WorldY * WF + VoxelHash::SeedOffset(SeedU, 0.19f), VoxelHash::SeedOffset(SeedU, 4.7f))); QX += wx * VOXEL_NOISE_SCALE * Params.CeilingWarpStrength; QY += wy * VOXEL_NOISE_SCALE * Params.CeilingWarpStrength; } // Broad SIGNED swell: raises/lowers the whole cap → big inverted hills and valleys. if (Params.CeilingUndulation > 0.0f) { const float Swell = FractalNoise3D(FVector3f( QX * Params.CeilingUndulationFrequency + VoxelHash::SeedOffset(SeedU, 1.9f), QY * Params.CeilingUndulationFrequency + 13.0f, VoxelHash::SeedOffset(SeedU, 0.5f)), 3); // [-1,1] CeilZ += Swell * VOXEL_NOISE_SCALE * Params.CeilingUndulation; } // Downward hang: everything here is >= 0 so features only protrude into the void (never // punch up into the seal). Fine bumps + sharp ridged blades sum together. float Hang = 0.0f; if (Params.CeilingRoughness > 0.0f) { Hang += FMath::Abs(FractalNoise3D(FVector3f( WorldX * Params.CeilingRoughnessFrequency + 5.0f, WorldY * Params.CeilingRoughnessFrequency + 6.0f, VoxelHash::SeedOffset(SeedU, 2.1f)), 3)) * VOXEL_NOISE_SCALE * Params.CeilingRoughness; } if (Params.CeilingRidgeStrength > 0.0f) { float Ridge = RidgedNoise3D(FVector3f( QX * Params.CeilingRidgeFrequency + 31.0f, QY * Params.CeilingRidgeFrequency + 47.0f, VoxelHash::SeedOffset(SeedU, 1.1f)), 4); // [-1,1] Ridge = Ridge * 0.5f + 0.5f; // [0,1] hanging ridgelines Hang += Ridge * Params.CeilingRidgeStrength; } return CeilZ - Hang; } void UVoxelGenerator::ResolveSurfaceChunkParams(const FIntVector& ChunkCoord, FSurfaceGenerationParams& OutSurface, FBiomeContext& OutBiomeCtx, TArray& OutBiomeParams) const { OutSurface = StrateManager->GetSurfaceParamsForChunk(ChunkCoord); OutBiomeCtx = StrateManager->GetBiomeContextForChunk(ChunkCoord); // Per-biome surface params: each biome's override (when it overrides SurfaceWorld) else the // strate's, with STRUCTURAL fields forced from the strate (Z bounds, seal, base, water level) // so seals/spine/water stay intact. OutBiomeParams.Reset(); if (OutBiomeCtx.IsValid()) { const UVoxelStrateDefinition* Def = StrateManager->GetStrateForChunk(ChunkCoord); OutBiomeParams.Reserve(OutBiomeCtx.Biomes.Num()); for (const FBiomeResolved& BR : OutBiomeCtx.Biomes) { FSurfaceGenerationParams P = OutSurface; // strate base (+ structural) const UVoxelBiomeDefinition* B = (Def && Def->Biomes.IsValidIndex(BR.Index)) ? Def->Biomes[BR.Index] : nullptr; if (B && B->bOverrideTerrain && B->GeneratorType == ECaveGeneratorType::SurfaceWorld) { P = B->SurfaceParams; // biome shape P.StrateTopWorldZ = OutSurface.StrateTopWorldZ; P.StrateBottomWorldZ = OutSurface.StrateBottomWorldZ; P.BoundarySealThickness = OutSurface.BoundarySealThickness; P.BaseDensity = OutSurface.BaseDensity; P.WaterLevelRelative = OutSurface.WaterLevelRelative; // shared water plane } OutBiomeParams.Add(P); } } } void UVoxelGenerator::ComputeSurfaceColumn(float WorldX, float WorldY, int32 ChunkZ, const FSurfaceGenerationParams& BaseSurface, const FBiomeContext& BiomeCtx, const TArray& BiomeParams, FChunkBiomeCache& BiomeCache, float& OutTerrainZ, float& OutCeilSurf, float& OutOverhangAmp, float& OutDirX, float& OutDirY) const { const FSurfaceGenerationParams* PD = &BaseSurface; const FSurfaceGenerationParams* PN = &BaseSurface; float W = 0.0f; if (BiomeCtx.IsValid() && BiomeParams.Num() > 0) { const FBiomeSample Smp = ResolveBiomeSampleAt(WorldX, WorldY, ChunkZ, BiomeCtx, BiomeCache); const int32 Di = BiomeParams.IsValidIndex(Smp.DominantIndex) ? Smp.DominantIndex : 0; PD = &BiomeParams[Di]; if (Smp.NeighborWeight > 0.0f && BiomeParams.IsValidIndex(Smp.NeighborIndex)) { PN = &BiomeParams[Smp.NeighborIndex]; W = Smp.NeighborWeight; } } OutTerrainZ = ComputeSurfaceTerrainZ(WorldX, WorldY, *PD); if (W > 0.0f) OutTerrainZ = FMath::Lerp(OutTerrainZ, ComputeSurfaceTerrainZ(WorldX, WorldY, *PN), W); OutCeilSurf = ComputeSurfaceCeiling(WorldX, WorldY, *PD); // F20 phase 2 — resolve the per-column OVERHANG gate (strength·slope-gate, biome-blended) and the // UPHILL direction. Slope + gradient come from a forward-diff of the STRUCTURAL field (the real // landform), computed ONCE here. Off ⇒ amp 0 and the whole block is skipped (no extra samples). OutOverhangAmp = 0.0f; OutDirX = 0.0f; OutDirY = 0.0f; if (PD->OverhangStrength > 0.0f || (W > 0.0f && PN->OverhangStrength > 0.0f)) { // Sample the gradient at ~reach scale (a spot over the void must "see" the nearby cliff to know // uphill), but CLAMPED to [4,16] — an un-clamped large Reach would average the slope over a huge // span and read even a real cliff as flat (killing the gate; the "big Reach = nothing" bug). const float SD = FMath::Clamp(PD->OverhangReach, 4.0f, 16.0f); float Ms; const float Z0 = SampleSurfaceStructuralZ(WorldX, WorldY, *PD, Ms); const float GX = (SampleSurfaceStructuralZ(WorldX + SD, WorldY, *PD, Ms) - Z0) / SD; // dZ/dX const float GY = (SampleSurfaceStructuralZ(WorldX, WorldY + SD, *PD, Ms) - Z0) / SD; // dZ/dY const float Slope = FMath::Sqrt(GX * GX + GY * GY); auto Amp = [Slope](const FSurfaceGenerationParams& P) -> float { if (P.OverhangStrength <= 0.0f) return 0.0f; const float Thr = FMath::Max(P.OverhangSlopeThreshold, 0.05f); const float Gate = FMath::Clamp((Slope - Thr) / Thr, 0.0f, 1.0f); return P.OverhangStrength * Gate; // [0,1]; reach comes from the param at apply time }; OutOverhangAmp = (W > 0.0f) ? FMath::Lerp(Amp(*PD), Amp(*PN), W) : Amp(*PD); // Unit UPHILL direction (the gradient points uphill). The shelf borrows rock from uphill and // extends it downhill over the void. Degenerate on flats — amp is 0 there anyway. if (Slope > KINDA_SMALL_NUMBER) { OutDirX = GX / Slope; OutDirY = GY / Slope; } } } bool UVoxelGenerator::GetSurfaceHeightAt(float WorldX, float WorldY, int32 ChunkZ, float& OutTerrainZ, float& OutCeilSurf) const { if (!StrateManager) return false; const FIntVector ChunkCoord( FMath::FloorToInt(WorldX / (float)CHUNK_SIZE), FMath::FloorToInt(WorldY / (float)CHUNK_SIZE), ChunkZ); if (StrateManager->GetGeneratorTypeForChunk(ChunkCoord) != ECaveGeneratorType::SurfaceWorld) { return false; } // Own per-chunk cache (independent of GetDensityAt's CP_*) so interleaved gen/deco tasks on the // same worker don't thrash each other. All columns of one deco cell share one chunk ⇒ resolve once. thread_local FIntVector OC_Chunk(INT32_MAX, INT32_MAX, INT32_MAX); thread_local FSurfaceGenerationParams OC_Surface; thread_local FBiomeContext OC_BiomeCtx; thread_local TArray OC_BiomeParams; thread_local FChunkBiomeCache OC_BiomeCache; thread_local uint32 OC_Version = 0xFFFFFFFFu; // AUDIT C2 thread_local uint64 OC_OwnerId = 0; thread_local uint64 OC_ManagerLifetimeId = 0; const uint32 OC_LayoutVersion = StrateManager->GetLayoutVersion(); const uint64 OC_CurrentOwnerId = DensityCacheOwnerId; const uint64 OC_CurrentManagerLifetimeId = StrateManager->GetCacheLifetimeId(); if (ChunkCoord != OC_Chunk || OC_LayoutVersion != OC_Version || OC_CurrentOwnerId != OC_OwnerId || OC_CurrentManagerLifetimeId != OC_ManagerLifetimeId) { if (OC_LayoutVersion != OC_Version || OC_CurrentOwnerId != OC_OwnerId || OC_CurrentManagerLifetimeId != OC_ManagerLifetimeId) { OC_BiomeCache.Invalidate(); } OC_Version = OC_LayoutVersion; OC_OwnerId = OC_CurrentOwnerId; OC_ManagerLifetimeId = OC_CurrentManagerLifetimeId; OC_Chunk = ChunkCoord; ResolveSurfaceChunkParams(ChunkCoord, OC_Surface, OC_BiomeCtx, OC_BiomeParams); } float OcAmp, OcDirX, OcDirY; // overhang is a density-only 3D shelf; the height oracle ignores it ComputeSurfaceColumn(WorldX, WorldY, ChunkZ, OC_Surface, OC_BiomeCtx, OC_BiomeParams, OC_BiomeCache, OutTerrainZ, OutCeilSurf, OcAmp, OcDirX, OcDirY); return true; } //============================================================================= // TRIVIAL-TILE CLASSIFICATION (T1.d) //============================================================================= // ~84 % des tuiles générées sortent VIDES (tout-roc sous le terrain, tout-air // au-dessus, cap solide) mais payaient quand même le pré-échantillonnage 33³+ // complet (une trace : 83 925 GenerateMesh pour 13 227 maillages réels). // ⚠️ Pas de borne analytique GLOBALE du plafond : pas assez conservative (cap // bas ⇒ trous dans le toit). Les colonnes sont donc ÉCHANTILLONNÉES sur le treillis // exact du mesher (mêmes fonctions ⇒ mêmes floats ⇒ verdict exact, pas une // estimation), et le cas tout-solide porte des gardes spine/passages/ // disturbances/diff. Tout ce qui n'est pas prouvable ⇒ Mixed (le seul coût d'un // faux Mixed est du CPU ; un faux AllSolid/AllAir serait un trou). // A full tile can be Mixed even when every smaller region is provably the same class. This is // particularly common at level 0: broad operator bounds overlap an unbounded carve, while the // actual tile-sized sub-boxes are all air. Refine only an already-Mixed proof. Every child must // resolve, and all resolved children must agree; an unresolved or conflicting child remains Mixed. // This is a proof refinement, never a density/sample shortcut. static EVoxelTileClass VF_ClassifyBoxRefined(const FVoxelOpStack& Stack, const FBox& VoxelBox, const FVoxelOpContext& Context, const UVoxelGenerator* Generator, const UVoxelStrateManager* StructuralManager, float StructuralBaseDensity = 8.0f, FVoxelTileClassificationStats* Stats = nullptr) { // The ordinary proof stays shallow. The finite-domain warp retry is paid only by a // tile that already failed the cheap proof, and can afford one more split level because // its purpose is to discharge the LOD0 false-Mixed cases without sampling the field. const int32 MaxRefinementDepth = Context.bTightenWarpProof ? 5 : 2; // Exact certificates are still the mesher's lattice, never a reduced-resolution shortcut. // Keep the samples local to this one classifier call. The old recursive fallback evaluated // the same shared lattice vertices once per depth-3 leaf (up to 64,000 calls for one LOD0 // tile); besides being wasteful, that made an empty tile look like a brute-force generation. // A byte validity plane is intentional: it is transient, bounded by one tile, and must not // become another process-wide cache. int32 ExactCacheX0 = 0, ExactCacheY0 = 0, ExactCacheZ0 = 0; int32 ExactCacheNX = 0, ExactCacheNY = 0, ExactCacheNZ = 0; TArray ExactCoreValues; TArray ExactFinalValues; TArray ExactCoreValid; TArray ExactFinalValid; // A negative structural result is monotone over nested boxes. Compute it once for the root // lattice and carry that absence through the refinement tree; otherwise every AllSolid child // repeats the same 33³ post-air scan even though a false root result already excludes all of // its descendants. A positive result is not cached as absent and keeps the old conservative // per-child checks. bool bRootPassageAirKnownAbsent = false; bool bRootOriginAirKnownAbsent = false; const bool bUseExactCache = Context.bUseLatticeProof && Context.Step > 0 && [&]() { const float Step = static_cast(Context.Step); const FVector Origin(static_cast(Context.LatticeOriginVoxels.X), static_cast(Context.LatticeOriginVoxels.Y), static_cast(Context.LatticeOriginVoxels.Z)); ExactCacheX0 = FMath::CeilToInt((static_cast(VoxelBox.Min.X) - Origin.X) / Step - 1.0e-4f); ExactCacheY0 = FMath::CeilToInt((static_cast(VoxelBox.Min.Y) - Origin.Y) / Step - 1.0e-4f); ExactCacheZ0 = FMath::CeilToInt((static_cast(VoxelBox.Min.Z) - Origin.Z) / Step - 1.0e-4f); const int32 X1 = FMath::FloorToInt((static_cast(VoxelBox.Max.X) - Origin.X) / Step + 1.0e-4f); const int32 Y1 = FMath::FloorToInt((static_cast(VoxelBox.Max.Y) - Origin.Y) / Step + 1.0e-4f); const int32 Z1 = FMath::FloorToInt((static_cast(VoxelBox.Max.Z) - Origin.Z) / Step + 1.0e-4f); ExactCacheNX = X1 - ExactCacheX0 + 1; ExactCacheNY = Y1 - ExactCacheY0 + 1; ExactCacheNZ = Z1 - ExactCacheZ0 + 1; const int64 Count = static_cast(ExactCacheNX) * static_cast(ExactCacheNY) * static_cast(ExactCacheNZ); if (ExactCacheNX <= 0 || ExactCacheNY <= 0 || ExactCacheNZ <= 0 || Count <= 0 || Count > 100000) { ExactCacheNX = ExactCacheNY = ExactCacheNZ = 0; return false; } const int32 NumVertices = static_cast(Count); ExactCoreValues.SetNumUninitialized(NumVertices); ExactFinalValues.SetNumUninitialized(NumVertices); ExactCoreValid.Init(0, NumVertices); ExactFinalValid.Init(0, NumVertices); return true; }(); auto GetLatticeBounds = [&](const FBox& Box, int32& OutX0, int32& OutY0, int32& OutZ0, int32& OutX1, int32& OutY1, int32& OutZ1, int64& OutCount) -> bool { if (!Context.bUseLatticeProof || Context.Step <= 0) { return false; } const float Step = static_cast(Context.Step); const FVector Origin(static_cast(Context.LatticeOriginVoxels.X), static_cast(Context.LatticeOriginVoxels.Y), static_cast(Context.LatticeOriginVoxels.Z)); OutX0 = FMath::CeilToInt((static_cast(Box.Min.X) - Origin.X) / Step - 1.0e-4f); OutY0 = FMath::CeilToInt((static_cast(Box.Min.Y) - Origin.Y) / Step - 1.0e-4f); OutZ0 = FMath::CeilToInt((static_cast(Box.Min.Z) - Origin.Z) / Step - 1.0e-4f); OutX1 = FMath::FloorToInt((static_cast(Box.Max.X) - Origin.X) / Step + 1.0e-4f); OutY1 = FMath::FloorToInt((static_cast(Box.Max.Y) - Origin.Y) / Step + 1.0e-4f); OutZ1 = FMath::FloorToInt((static_cast(Box.Max.Z) - Origin.Z) / Step + 1.0e-4f); OutCount = static_cast(OutX1 - OutX0 + 1) * static_cast(OutY1 - OutY0 + 1) * static_cast(OutZ1 - OutZ0 + 1); return OutX1 >= OutX0 && OutY1 >= OutY0 && OutZ1 >= OutZ0 && OutCount > 0; }; auto EvaluateExactLatticeValue = [&](int32 IX, int32 IY, int32 IZ, bool bUseFinalField) -> float { const float Step = static_cast(Context.Step); const FVector Origin(static_cast(Context.LatticeOriginVoxels.X), static_cast(Context.LatticeOriginVoxels.Y), static_cast(Context.LatticeOriginVoxels.Z)); TArray& Values = bUseFinalField ? ExactFinalValues : ExactCoreValues; TArray& Valid = bUseFinalField ? ExactFinalValid : ExactCoreValid; int32 CacheIndex = INDEX_NONE; if (bUseExactCache && IX >= ExactCacheX0 && IX < ExactCacheX0 + ExactCacheNX && IY >= ExactCacheY0 && IY < ExactCacheY0 + ExactCacheNY && IZ >= ExactCacheZ0 && IZ < ExactCacheZ0 + ExactCacheNZ) { CacheIndex = (IZ - ExactCacheZ0) * ExactCacheNY * ExactCacheNX + (IY - ExactCacheY0) * ExactCacheNX + (IX - ExactCacheX0); if (Valid[CacheIndex] != 0) { if (Stats) { if (bUseFinalField) { ++Stats->ExactFinalCacheHits; } else { ++Stats->ExactCoreCacheHits; } } return Values[CacheIndex]; } } const float WorldX = Origin.X + static_cast(IX) * Step; const float WorldY = Origin.Y + static_cast(IY) * Step; const float WorldZ = Origin.Z + static_cast(IZ) * Step; // The operator stack is the cheap proof field. A passage/landing post is applied after // that stack by GetDensityAt; for a child intersecting one of those modifiers, certify // the final MC-facing field rather than trusting the conservative core proof. const float Density = bUseFinalField ? -Generator->GetDensityAt(WorldX, WorldY, WorldZ) : Stack.EvalInternal(WorldX, WorldY, WorldZ); if (Stats) { if (bUseFinalField) { ++Stats->ExactFinalSamples; } else { ++Stats->ExactCoreSamples; } } if (CacheIndex != INDEX_NONE) { Values[CacheIndex] = Density; Valid[CacheIndex] = 1; } return Density; }; auto CornersHaveOneSign = [&](const FBox& Box, bool bUseFinalField) -> bool { int32 IX0 = 0, IY0 = 0, IZ0 = 0, IX1 = 0, IY1 = 0, IZ1 = 0; int64 Count = 0; if (!GetLatticeBounds(Box, IX0, IY0, IZ0, IX1, IY1, IZ1, Count)) { return false; } bool bHaveSign = false; bool bPositive = false; const int32 Xs[2] = { IX0, IX1 }; const int32 Ys[2] = { IY0, IY1 }; const int32 Zs[2] = { IZ0, IZ1 }; for (const int32 IZ : Zs) for (const int32 IY : Ys) for (const int32 IX : Xs) { const float Density = EvaluateExactLatticeValue(IX, IY, IZ, bUseFinalField); if (!VoxelMath::IsFinite(Density) || Density == 0.0f) { return false; } const bool bThisPositive = Density > 0.0f; if (!bHaveSign) { bHaveSign = true; bPositive = bThisPositive; } else if (bPositive != bThisPositive) { return false; } } return bHaveSign; }; auto ClassifyExactLatticeLeaf = [&](const FBox& Box, bool bUseFinalField) -> EVoxelTileClass { const uint64 ExactStartCycles = Stats ? FPlatformTime::Cycles64() : 0; int32 IX0 = 0, IY0 = 0, IZ0 = 0, IX1 = 0, IY1 = 0, IZ1 = 0; int64 Count = 0; if (!GetLatticeBounds(Box, IX0, IY0, IZ0, IX1, IY1, IZ1, Count) || Count > 8192) { return EVoxelTileClass::Mixed; } if (Stats) { if (bUseFinalField) { ++Stats->ExactFinalLeaves; } else { ++Stats->ExactCoreLeaves; } } bool bAllSolid = true; bool bAllAir = true; int32 PositiveCount = 0; int32 NegativeCount = 0; int32 ZeroOrInvalidCount = 0; for (int32 IZ = IZ0; IZ <= IZ1; ++IZ) for (int32 IY = IY0; IY <= IY1; ++IY) for (int32 IX = IX0; IX <= IX1; ++IX) { const float Density = EvaluateExactLatticeValue(IX, IY, IZ, bUseFinalField); if (!VoxelMath::IsFinite(Density) || Density == 0.0f) { ++ZeroOrInvalidCount; IZ = IZ1; IY = IY1; break; } if (Density > 0.0f) { ++PositiveCount; bAllAir = false; } else { ++NegativeCount; bAllSolid = false; } if (!bAllSolid && !bAllAir) { IZ = IZ1; IY = IY1; break; } } const EVoxelTileClass Verdict = ZeroOrInvalidCount > 0 || (PositiveCount > 0 && NegativeCount > 0) ? EVoxelTileClass::Mixed : (bAllSolid ? EVoxelTileClass::AllSolid : EVoxelTileClass::AllAir); if (Stats) { const uint64 Elapsed = FPlatformTime::Cycles64() - ExactStartCycles; if (bUseFinalField) { Stats->ExactFinalCycles += Elapsed; } else { Stats->ExactCoreCycles += Elapsed; } } return Verdict; }; auto Refine = [&](auto&& Self, const FBox& Box, int32 Depth) -> EVoxelTileClass { if (Stats) { ++Stats->RefineNodes; } const uint64 StackStartCycles = Stats ? FPlatformTime::Cycles64() : 0; const EVoxelTileClass Whole = Stack.ClassifyBox(Box, Context); if (Stats) { ++Stats->StackBoxCalls; Stats->StackBoxCycles += FPlatformTime::Cycles64() - StackStartCycles; switch (Whole) { case EVoxelTileClass::Mixed: ++Stats->WholeMixedNodes; break; case EVoxelTileClass::AllSolid: ++Stats->WholeSolidNodes; break; case EVoxelTileClass::AllAir: ++Stats->WholeAirNodes; break; default: break; } Stats->MaxRefinementDepth = FMath::Max( Stats->MaxRefinementDepth, static_cast(Depth)); } // The graph stack is not the complete MC field: GetDensityAt reasserts the walkable // tunnel core after the stack and after the disturbance/landing posts. The source's // box audit tells us whether that post can have a tunnel candidate in this exact box. // Keep this test local to room-graph stacks; the diagnostic is thread-local and would be // stale for a slab/maze stack. A candidate only widens the work, never grants a skip. if (Depth == 0 && Context.bUseLatticeProof && StructuralManager != nullptr) { bRootPassageAirKnownAbsent = !StructuralManager->AnyPassageAirPostNearLattice( VoxelBox, Context.LatticeOriginVoxels, Context.Step, StructuralBaseDensity, Context.EdgeSealThickness); bRootOriginAirKnownAbsent = !StructuralManager->AnyOriginLandingAirNearLattice( VoxelBox, Context.LatticeOriginVoxels, Context.Step, StructuralBaseDensity, Context.EdgeSealThickness); } const bool bHasRoomGeometry = Stack.ProvidesResource(VoxelOpResources::RoomGeometry); const VoxelDensityOps::FRoomBoxDiagnostic RoomBox = bHasRoomGeometry ? VoxelDensityOps::GetLastRoomBoxDiagnostic() : VoxelDensityOps::FRoomBoxDiagnostic(); if (Stats && bHasRoomGeometry) { Stats->RoomTailQueries += RoomBox.ExactTailQueries; Stats->RoomTailEvaluated += RoomBox.ExactTailEvaluated; Stats->RoomTailCycles += RoomBox.ExactTailCycles; Stats->RoomPropagateCycles += RoomBox.PropagateCycles; Stats->RoomExactPrimitiveCycles += RoomBox.ExactPrimitiveCycles; Stats->RoomCacheWindowCycles += RoomBox.CacheWindowCycles; Stats->RoomNumRooms = RoomBox.NumRooms; Stats->RoomNumTunnels = RoomBox.NumTunnels; Stats->RoomNumRoomFloorJoins = RoomBox.NumRoomFloorJoins; Stats->RoomNumPits = RoomBox.NumPits; Stats->RoomNumChimneys = RoomBox.NumChimneys; } const bool bHasTunnelAirCandidate = bHasRoomGeometry && RoomBox.bMayHaveTunnelCoreAir; const bool bHasTunnelSupportCandidate = bHasRoomGeometry && RoomBox.bMayHaveTunnelSupportFloor; const bool bHasTunnelCoreTailCandidate = bHasRoomGeometry && RoomBox.bMayHaveTunnelCoreTail; // The final graph tail has two polarities: it can reopen tunnel air and it can write a // solid swept floor below an otherwise-air stack box. Resolved children use the broad // reach candidate directly. A Mixed ancestor remains on the cheap interval path until // the leaf-sized refinement, where the same candidate requests an exact final-field // certificate; this avoids paying the final evaluator at every ancestor node. const bool bHasTunnelCoreCandidate = Whole == EVoxelTileClass::AllSolid ? bHasTunnelAirCandidate : Whole == EVoxelTileClass::AllAir ? (bHasTunnelSupportCandidate || bHasTunnelAirCandidate || bHasTunnelCoreTailCandidate) : false; const bool bHasMixedTunnelCoreCandidate = Whole == EVoxelTileClass::Mixed && Depth >= 3 && bHasTunnelCoreTailCandidate; // FPassageCarveOp and FOriginSpine are already in the stack. Reusing their broad spatial // candidates here was the LOD0 bug: a passage that the stack had already proved to miss // the sampled sign still forced a final-field refinement of every mixed parent. Only the // writers that run after the stack remain candidates in this section: post-disturbance // air, and support floors that can add solid. Unknown/non-finite input remains a // candidate in each exact helper, so this separation cannot authorize a false skip. // A Mixed core is not a verdict yet. Descend until the interval proof resolves its // polarity; an air post matters to an AllSolid child, while a solid floor/support post // matters to an AllAir child. Checking both writers on every Mixed ancestor was the // remaining LOD0 classifier tax and could not improve the answer before the child split. const bool bNeedsPostAirCandidate = Whole == EVoxelTileClass::AllSolid; const bool bPassageAirCandidate = bNeedsPostAirCandidate && !bRootPassageAirKnownAbsent && StructuralManager != nullptr && StructuralManager->AnyPassageAirPostNearLattice( Box, Context.LatticeOriginVoxels, Context.Step, StructuralBaseDensity, Context.EdgeSealThickness); const bool bOriginAirCandidate = bNeedsPostAirCandidate && !bRootOriginAirKnownAbsent && StructuralManager != nullptr && StructuralManager->AnyOriginLandingAirNearLattice( Box, Context.LatticeOriginVoxels, Context.Step, StructuralBaseDensity, Context.EdgeSealThickness); // Solid structural writers matter not only when the interval is already AllAir: the // refinement may first see Mixed, then prove AllAir with its exact *core* lattice // certificate. If that child is near a landing/native floor, the core certificate is not // the final MC field and would skip a solid sample. Keep the query conservative on every // non-AllSolid child; only small children pay the final-field walk below. const bool bHasCarveCandidate = bPassageAirCandidate || bOriginAirCandidate; const bool bHasSolidStructuralCandidate = StructuralManager != nullptr && (StructuralManager->AnyLandingFloorAtLattice( Box, Context.LatticeOriginVoxels, Context.Step) // The passage's authored D-floor is a final MC-space writer, not part of the // interpreted stack, and no support-floor query covers it; omitting the // native-floor reach here lets an exact core-lattice AllAir claim skip a tile // whose final density is solid. || StructuralManager->AnyPassageStructuralPostNearLattice( Box, Context.LatticeOriginVoxels, Context.Step)); const bool bHasStructuralCandidate = Whole == EVoxelTileClass::AllSolid ? bHasCarveCandidate : bHasSolidStructuralCandidate; // A resolved core box still needs a finite-lattice certificate when a common MC tail can // rewrite it (passage/landing posts or the graph tunnel-core backstop). Without this, // the interval proves the stack's "all solid" field and silently skips a core tunnel // that GetDensityAt opens afterwards. Boxes with no tail candidate retain the cheap // interval path. const bool bNeedsFinalField = Generator != nullptr && (bHasStructuralCandidate || bHasTunnelCoreCandidate || bHasMixedTunnelCoreCandidate); if (Stats && bNeedsFinalField) { ++Stats->NeedsFinalFieldNodes; } // A final-field certificate is exact on the same MC lattice as GenerateMesh. Use it for // the small block-sized tail-risk boxes, including an already-resolved AllSolid/AllAir // box. The common streamed tile is deliberately not a final-field sampling job: a // coarse tile can contain thousands of MC vertices, and its proof must stay interval-only. // Larger tail-risk boxes remain conservative Mixed until the mesher's normal sampling. bool bAttemptedExact = false; const float FinalFieldBoxExtent = FMath::Max3( static_cast(Box.Max.X - Box.Min.X), static_cast(Box.Max.Y - Box.Min.Y), static_cast(Box.Max.Z - Box.Min.Z)); const bool bSmallFinalFieldBox = bNeedsFinalField && FinalFieldBoxExtent <= 8.0f; // A room-graph SDF interval can be conservative at the parent while a fine child still // resolves uniformly through a state-aware SDF writer (for example the worm mask). Before // using that fine interval to skip geometry, certify the exact MC lattice in the child. // This runs only after a mixed parent has been refined to a small room-graph child; clear // uniform roots and all non-room stacks keep the interval-only fast path. const bool bNeedsExactRoomIntervalCheck = bHasRoomGeometry && Context.bUseLatticeProof && Context.bTightenWarpProof && !bNeedsFinalField && Whole != EVoxelTileClass::Mixed && Depth >= 3 && FinalFieldBoxExtent <= 8.0f; if (bNeedsExactRoomIntervalCheck) { int32 ExactIX0 = 0, ExactIY0 = 0, ExactIZ0 = 0; int32 ExactIX1 = 0, ExactIY1 = 0, ExactIZ1 = 0; int64 ExactCount = 0; if (GetLatticeBounds(Box, ExactIX0, ExactIY0, ExactIZ0, ExactIX1, ExactIY1, ExactIZ1, ExactCount) && ExactCount <= 8192) { if (Stats) { ++Stats->ExactCoreIntervalChecks; } const EVoxelTileClass Exact = ClassifyExactLatticeLeaf(Box, false); if (Exact != Whole && Stats) { ++Stats->ExactCoreIntervalDisagreements; } return Exact; } } if (bSmallFinalFieldBox) { int32 ExactIX0 = 0, ExactIY0 = 0, ExactIZ0 = 0; int32 ExactIX1 = 0, ExactIY1 = 0, ExactIZ1 = 0; int64 ExactCount = 0; const bool bHasExactBounds = GetLatticeBounds( Box, ExactIX0, ExactIY0, ExactIZ0, ExactIX1, ExactIY1, ExactIZ1, ExactCount) && ExactCount <= 8192; if (bHasExactBounds) { bAttemptedExact = true; const EVoxelTileClass Exact = ClassifyExactLatticeLeaf(Box, true); if (Exact != EVoxelTileClass::Mixed) { return Exact; } return EVoxelTileClass::Mixed; } } if (Whole != EVoxelTileClass::Mixed && !bNeedsFinalField) { return Whole; } // At this size the box contains at most 5^3 exact MC vertices. Evaluating those same // vertices is a small, sound certificate for the lattice field the mesher will consume; // it is not a lower-resolution fallback and never changes GenerateMesh's sample count. // This discharges the last conservative SDF interval misses without walking another // refinement level or rebuilding the morphology bounds for every half-box. // The corner check is only a work gate. It is not a proof: matching corners still pay the // complete exact lattice certificate, so a thin cave or a zero/invalid sample cannot be // skipped accidentally. For a genuinely uniform LOD0 empty box this collapses the old // 585-node/64k-sample fallback to at most the 33^3 unique lattice vertices. // A depth-1 box still contains 17^3 MC vertices; probing its corners and then certifying // the whole child is more expensive than the interval refinement it was meant to avoid. // Wait until the depth-3 tail (at most 9^3 vertices) before paying an exact core walk. const bool bCornersHaveOneSign = !bNeedsFinalField && Depth >= 3 && CornersHaveOneSign(Box, false); if (bCornersHaveOneSign) { bAttemptedExact = true; const EVoxelTileClass Exact = ClassifyExactLatticeLeaf(Box, false); if (Exact != EVoxelTileClass::Mixed) { return Exact; } } // A two-voxel unresolved tail is still cheap to certify on the final MC lattice (3^3 // points). Doing this before the last split avoids eight more interval folds for the // localized passage/post boundary that caused the original LOD0 false-Mixed bail. if (!bAttemptedExact && Whole == EVoxelTileClass::Mixed && !bNeedsFinalField && Context.bUseLatticeProof && Depth >= 4 && FinalFieldBoxExtent <= 2.0f) { const EVoxelTileClass ExactFinal = ClassifyExactLatticeLeaf(Box, true); if (ExactFinal != EVoxelTileClass::Mixed) { return ExactFinal; } bAttemptedExact = true; } if (!bAttemptedExact && Depth >= 3 && (!bNeedsFinalField || bSmallFinalFieldBox)) { const EVoxelTileClass Exact = ClassifyExactLatticeLeaf(Box, bSmallFinalFieldBox); if (Exact != EVoxelTileClass::Mixed) { return Exact; } // The final-field leaf is deliberately a conservative sampling certificate. A // mixed result means this child really contains both final signs (or an invalid // sample); splitting it again cannot prove the parent uniform and only repeats the // expensive post-aware evaluator. if (bNeedsFinalField) { return EVoxelTileClass::Mixed; } } if (Depth >= MaxRefinementDepth) { // The operator interval can remain Mixed at a one-voxel tail even though the final // MC-facing field is uniform after a structural post. Certify that tiny tail on the // exact lattice before conceding the whole tile to the mesher. This is a proof over // all corners the mesher would consume; zero/non-finite values remain Mixed. if (Whole == EVoxelTileClass::Mixed && !bNeedsFinalField && Context.bUseLatticeProof && FinalFieldBoxExtent <= 1.0f) { const EVoxelTileClass ExactFinal = ClassifyExactLatticeLeaf(Box, true); if (ExactFinal != EVoxelTileClass::Mixed) { return ExactFinal; } } return bNeedsFinalField ? EVoxelTileClass::Mixed : Whole; } const FVector Mid = (Box.Min + Box.Max) * 0.5f; if (Stats) { ++Stats->SplitNodes; } bool bSawSolid = false; bool bSawAir = false; for (int32 Z = 0; Z < 2; ++Z) for (int32 Y = 0; Y < 2; ++Y) for (int32 X = 0; X < 2; ++X) { const FVector ChildMin( X == 0 ? Box.Min.X : Mid.X, Y == 0 ? Box.Min.Y : Mid.Y, Z == 0 ? Box.Min.Z : Mid.Z); const FVector ChildMax( X == 0 ? Mid.X : Box.Max.X, Y == 0 ? Mid.Y : Box.Max.Y, Z == 0 ? Mid.Z : Box.Max.Z); const EVoxelTileClass Child = Self(Self, FBox(ChildMin, ChildMax), Depth + 1); if (Child == EVoxelTileClass::Mixed) { return EVoxelTileClass::Mixed; } bSawSolid |= Child == EVoxelTileClass::AllSolid; bSawAir |= Child == EVoxelTileClass::AllAir; if (bSawSolid && bSawAir) { return EVoxelTileClass::Mixed; } } return bSawSolid ? EVoxelTileClass::AllSolid : EVoxelTileClass::AllAir; }; const EVoxelTileClass Result = Refine(Refine, VoxelBox, 0); return Result; } // The ordinary proof is deliberately cheap. Only a box that remains Mixed after its bounded // spatial refinement pays for the finite-domain warp interval; this keeps the common empty-tile // path at its existing cost while giving the few conservative misses a second sound chance. static EVoxelTileClass VF_ClassifyBoxWithWarpRetry(const FVoxelOpStack& Stack, const FBox& VoxelBox, const FVoxelOpContext& Context, const UVoxelGenerator* Generator = nullptr, const UVoxelStrateManager* StructuralManager = nullptr, float StructuralBaseDensity = 8.0f, FVoxelTileClassificationStats* Stats = nullptr) { const EVoxelTileClass Initial = VF_ClassifyBoxRefined( Stack, VoxelBox, Context, Generator, StructuralManager, StructuralBaseDensity, Stats); // A Mixed fine tile is not evidence of geometry: it can be the conservative result of a // coarse operator interval. Keep the bounded retry for every LOD; skipping it recreates // the false-Mixed empty tiles that send the mesher through the expensive path. if (Initial != EVoxelTileClass::Mixed || Context.bTightenWarpProof) { return Initial; } FVoxelOpContext TightContext = Context; TightContext.bTightenWarpProof = true; return VF_ClassifyBoxRefined( Stack, VoxelBox, TightContext, Generator, StructuralManager, StructuralBaseDensity, Stats); } EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis) const { return ClassifyTile(OriginVoxels, Step, CellsPerAxis, nullptr); } bool UVoxelGenerator::TryProveSealedSolidTile( const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis) const { return VF_TryProveSealedSolidTile( *this, OriginVoxels, Step, CellsPerAxis, /*DisabledGuards=*/0); } bool UVoxelGenerator::TryProveOutOfLayoutAirTile( const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis) const { return VF_TryProveOutOfLayoutAirTile(*this, OriginVoxels, Step, CellsPerAxis); } #if WITH_DEV_AUTOMATION_TESTS bool UVoxelGenerator::TryProveSealedSolidTileForTest( const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis, uint8 DisabledGuards) const { return VF_TryProveSealedSolidTile( *this, OriginVoxels, Step, CellsPerAxis, DisabledGuards); } #endif EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis, FVoxelTileClassificationStats* OutStats) const { if (OutStats) { *OutStats = FVoxelTileClassificationStats(); } const int32 NormalizedStep = FMath::Max(1, Step); const int32 NormalizedCellsPerAxis = FMath::Clamp(CellsPerAxis, 2, CHUNK_SIZE); VoxelDensityProfile::AddCounter( VoxelDensityProfile::ECounter::TileClassifyCalls); #if WITH_EDITOR // The ordinary tile classifier proves the live field's interval bounds. While the separate // research field is selected, decline those proofs until its own complete final-field interval // contract is wired into streaming; a conservative Mixed verdict keeps the test mesh complete. if (VoxelForgeRedefinedDensityField::IsEnabled()) { return EVoxelTileClass::Mixed; } #endif const EVoxelTileClass Verdict = ClassifyTileUncached( OriginVoxels, NormalizedStep, NormalizedCellsPerAxis, OutStats); if (Verdict == EVoxelTileClass::AllSolid) { VoxelDensityProfile::AddCounter( VoxelDensityProfile::ECounter::TileClassifyAllSolid); } else if (Verdict == EVoxelTileClass::AllAir) { VoxelDensityProfile::AddCounter( VoxelDensityProfile::ECounter::TileClassifyAllAir); } else { VoxelDensityProfile::AddCounter( VoxelDensityProfile::ECounter::TileClassifyMixed); } return Verdict; } EVoxelTileClass UVoxelGenerator::ClassifyTileUncached( const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis, FVoxelTileClassificationStats* OutStats) const { if (OutStats) { *OutStats = FVoxelTileClassificationStats(); } // Mêmes clamps que GenerateMesh — le verdict doit couvrir le treillis réellement échantillonné. Step = FMath::Max(1, Step); // ClassifyTile runs before GenerateMesh installs its LOD TLS. The room-graph source uses this // value only to choose a sound, window-invariant cache region; leaving the previous worker // value (normally 1) makes an LOD4 proof rebuild fine per-chunk graph windows across the // coarse tile. Scope it exactly like the mesher does so standalone classifier calls are also // keyed by their actual lattice step. TGuardValue ClassifySampleStepGuard(VoxelGenLOD::SampleStep, Step); const int32 CPA = FMath::Clamp(CellsPerAxis, 2, CHUNK_SIZE); TGuardValue ClassifyTileOriginGuard( VoxelGenLOD::TileOriginVoxels, OriginVoxels); TGuardValue ClassifyTileCellsGuard( VoxelGenLOD::TileCellsPerAxis, CPA); const int32 GridDim = CPA + 1; // cell-vertex count: g ∈ [0, CPA] // The mesher's outer one-sample halo is used only for central-difference normals. It cannot // create a marching-cubes cell: every case index reads vertices g ∈ [0, CPA]. Classify those // actual cell vertices, not the halo. Including g=-1/CPA+1 made a coarse tile look Mixed when // the halo crossed a surface outside the tile while every cell in the tile was uniform. const int32 MinX = OriginVoxels.X, MaxX = OriginVoxels.X + CPA * Step; const int32 MinY = OriginVoxels.Y, MaxY = OriginVoxels.Y + CPA * Step; const int32 MinZ = OriginVoxels.Z, MaxZ = OriginVoxels.Z + CPA * Step; // Division entière PLANCHER (les coords négatives tronquent vers 0 en C++ — pas floor). auto FloorDivC = [](int32 A, int32 B) -> int32 { const int32 Q = A / B, R = A % B; return (R != 0 && ((R < 0) != (B < 0))) ? Q - 1 : Q; }; // ── Garde diff layer : un mod joueur peut creuser OU remplir n'importe où. ── if (DiffLayer && DiffLayer->HasAnyMods()) { const FIntVector MinChunk(FloorDivC(MinX, CHUNK_SIZE), FloorDivC(MinY, CHUNK_SIZE), FloorDivC(MinZ, CHUNK_SIZE)); const FIntVector MaxChunk(FloorDivC(MaxX, CHUNK_SIZE), FloorDivC(MaxY, CHUNK_SIZE), FloorDivC(MaxZ, CHUNK_SIZE)); if (DiffLayer->HasAnyModInChunkRange(MinChunk, MaxChunk)) return EVoxelTileClass::Mixed; } // The bounded shell is global: it also covers gaps and out-of-layout air, so prove it before // consulting the strate layout. The final MC edge pass uses a positive base, hence 1.0 is a // deliberately conservative proof input here; the stack op performs the same proof with the // archetype base for its own sub-box. const FBox TileVoxelBox(FVector((float)MinX, (float)MinY, (float)MinZ), FVector((float)MaxX, (float)MaxY, (float)MaxZ)); if (VF_XYEdgeSealBoxIsForcedSolid(TileVoxelBox, WorldRadiusVoxels, EdgeSealThickness, 1.0f)) { return EVoxelTileClass::AllSolid; } if (!StrateManager) return EVoxelTileClass::Mixed; // A selected recipe must be classified by the exact same materialised stack that supplies // density. Search every touched chunk-Z first: the minimum corner may be in a gap or adjacent // slot, and letting that fall through to the native factory would prove the wrong field. int32 RecipeSeed = 0; ECaveGeneratorType RecipeArchetype = ECaveGeneratorType::TunnelNetwork; FVoxelStrateArchetypeParams RecipeParams; FVoxelOpStackRecipe Recipe; bool bTouchesRecipe = false; const int32 RecipeMinChunkZ = FloorDivC(MinZ, CHUNK_SIZE); const int32 RecipeMaxChunkZ = FloorDivC(MaxZ, CHUNK_SIZE); for (int32 CZ = RecipeMinChunkZ; CZ <= RecipeMaxChunkZ; ++CZ) { if (StrateManager->GetRecipeForChunk( FIntVector(0, 0, CZ), RecipeSeed, RecipeArchetype, RecipeParams, Recipe)) { bTouchesRecipe = true; break; } } if (bTouchesRecipe) { int32 RecipeTopChunkZ = 0; int32 RecipeBottomChunkZ = 0; if (!StrateManager->GetStrateChunkZBounds( RecipeMinChunkZ, RecipeTopChunkZ, RecipeBottomChunkZ) || RecipeMinChunkZ < RecipeBottomChunkZ || RecipeMaxChunkZ > RecipeTopChunkZ) { return EVoxelTileClass::Mixed; } // Recipes and disturbances are selected from the strate slot's Z only. The manager // copies the recipe arrays into the output parameters, so repeating this check for every // XY key in the sampled box turns one coarse tile into thousands of identical deep copies. // Keep one representative per distinct Z: this is equivalent for the APIs above and still // rejects a tile when any touched Z changes recipe, bounds, or disturbance state. for (int32 CZ = RecipeMinChunkZ; CZ <= RecipeMaxChunkZ; ++CZ) { int32 TouchedTop = 0; int32 TouchedBottom = 0; if (!StrateManager->GetStrateChunkZBounds(CZ, TouchedTop, TouchedBottom) || TouchedTop != RecipeTopChunkZ || TouchedBottom != RecipeBottomChunkZ) { return EVoxelTileClass::Mixed; } int32 TouchedSeed = 0; ECaveGeneratorType TouchedArchetype = ECaveGeneratorType::TunnelNetwork; FVoxelStrateArchetypeParams TouchedParams; FVoxelOpStackRecipe TouchedRecipe; const FIntVector TouchedChunk(0, 0, CZ); if (!StrateManager->GetRecipeForChunk( TouchedChunk, TouchedSeed, TouchedArchetype, TouchedParams, TouchedRecipe) || TouchedSeed != RecipeSeed || TouchedArchetype != RecipeArchetype) { return EVoxelTileClass::Mixed; } const FStrateDisturbanceParams Disturbances = StrateManager->GetDisturbanceParamsForChunk(TouchedChunk); if (VF_AnyDisturbanceCanTouchBox(TileVoxelBox, Disturbances, (uint32)Seed)) { return EVoxelTileClass::Mixed; } } FVoxelOpStack RecipeStack; FVoxelOpContext RecipeContext; if (!VF_BuildStackFromRecipe( Recipe, RecipeParams, RecipeSeed, OriginSpineRadius, StrateManager, RecipeStack, RecipeContext, nullptr)) { return EVoxelTileClass::Mixed; } RecipeContext.ChunkCoord = FIntVector( FloorDivC(MinX, CHUNK_SIZE), FloorDivC(MinY, CHUNK_SIZE), RecipeMinChunkZ); RecipeContext.Step = Step; RecipeContext.LayoutVersion = StrateManager->GetLayoutVersion(); RecipeContext.WorldRadiusVoxels = WorldRadiusVoxels; RecipeContext.EdgeSealThickness = EdgeSealThickness; RecipeContext.bUseLatticeProof = true; RecipeContext.LatticeOriginVoxels = OriginVoxels; RecipeStack.PrepareChunk(RecipeContext); const EVoxelTileClass RecipeVerdict = VF_ClassifyBoxWithWarpRetry(RecipeStack, TileVoxelBox, RecipeContext, this, StrateManager, 8.0f, OutStats); if (RecipeVerdict == EVoxelTileClass::Mixed) { } return RecipeVerdict; } #if WITH_EDITOR // A multi-region composer override is already a complete parent stack: it owns the lateral // blend and the four global structural posts. Classify it as one field only when the complete // mesher box stays inside that one strate; otherwise the Z-category pass below deliberately // returns Mixed. Disturbances and the diff layer remain outside the stack, so either one also // vetoes a skip. FVoxelStrateRegionManifest ComposerRegions; const FIntVector RegionProbeChunk( FloorDivC(MinX, CHUNK_SIZE), FloorDivC(MinY, CHUNK_SIZE), FloorDivC(MinZ, CHUNK_SIZE)); if (StrateManager->GetComposerRegionOverrideForChunk(RegionProbeChunk, ComposerRegions)) { int32 RegionTopChunkZ = 0; int32 RegionBottomChunkZ = 0; const int32 RegionMinChunkZ = FloorDivC(MinZ, CHUNK_SIZE); const int32 RegionMaxChunkZ = FloorDivC(MaxZ, CHUNK_SIZE); if (!StrateManager->GetStrateChunkZBounds(RegionProbeChunk.Z, RegionTopChunkZ, RegionBottomChunkZ) || RegionMinChunkZ < RegionBottomChunkZ || RegionMaxChunkZ > RegionTopChunkZ) { return EVoxelTileClass::Mixed; } // A representative chunk is not enough for a proof: the mesher box can cross an XY // chunk edge, and disturbances are chunk-local. Require every touched chunk to remain // in the same slot and to have no active disturbance before delegating the proof to the // complete lateral stack. Any uncertainty is Mixed; a false uniform answer here can // skip a real seam or carve. const int32 RegionMinChunkX = FloorDivC(MinX, CHUNK_SIZE); const int32 RegionMaxChunkX = FloorDivC(MaxX, CHUNK_SIZE); const int32 RegionMinChunkY = FloorDivC(MinY, CHUNK_SIZE); const int32 RegionMaxChunkY = FloorDivC(MaxY, CHUNK_SIZE); for (int32 CZ = RegionMinChunkZ; CZ <= RegionMaxChunkZ; ++CZ) { int32 TouchedTopChunkZ = 0; int32 TouchedBottomChunkZ = 0; if (!StrateManager->GetStrateChunkZBounds( CZ, TouchedTopChunkZ, TouchedBottomChunkZ) || TouchedTopChunkZ != RegionTopChunkZ || TouchedBottomChunkZ != RegionBottomChunkZ) { return EVoxelTileClass::Mixed; } for (int32 CY = RegionMinChunkY; CY <= RegionMaxChunkY; ++CY) { for (int32 CX = RegionMinChunkX; CX <= RegionMaxChunkX; ++CX) { const FIntVector TouchedChunk(CX, CY, CZ); FVoxelStrateRegionManifest TouchedRegions; if (!StrateManager->GetComposerRegionOverrideForChunk( TouchedChunk, TouchedRegions) || TouchedRegions.StrateIndex != ComposerRegions.StrateIndex || TouchedRegions.Seed != ComposerRegions.Seed || TouchedRegions.RegionCount != ComposerRegions.RegionCount || TouchedRegions.PartitionSeed != ComposerRegions.PartitionSeed || TouchedRegions.LatticeCellSize != ComposerRegions.LatticeCellSize || TouchedRegions.BlendWidth != ComposerRegions.BlendWidth) { return EVoxelTileClass::Mixed; } const FStrateDisturbanceParams Disturbances = StrateManager->GetDisturbanceParamsForChunk(TouchedChunk); if (VF_AnyDisturbanceCanTouchBox(TileVoxelBox, Disturbances, (uint32)Seed)) { return EVoxelTileClass::Mixed; } } } } FVoxelOpStack RegionStack; FVoxelOpContext RegionContext; if (!VF_BuildStrateRegionStack(ComposerRegions, OriginSpineRadius, StrateManager, RegionStack, RegionContext, nullptr)) { return EVoxelTileClass::Mixed; } RegionContext.ChunkCoord = RegionProbeChunk; RegionContext.Step = Step; RegionContext.LayoutVersion = StrateManager->GetLayoutVersion(); RegionContext.WorldRadiusVoxels = WorldRadiusVoxels; RegionContext.EdgeSealThickness = EdgeSealThickness; RegionContext.bUseLatticeProof = true; RegionContext.LatticeOriginVoxels = OriginVoxels; RegionStack.PrepareChunk(RegionContext); const EVoxelTileClass RegionVerdict = VF_ClassifyBoxWithWarpRetry(RegionStack, TileVoxelBox, RegionContext, this, StrateManager, 8.0f, OutStats); if (RegionVerdict == EVoxelTileClass::Mixed) { } return RegionVerdict; } #endif bool bCanSolid = true; // "tout le treillis est solide" encore prouvable bool bCanAir = true; // "tout le treillis est air" encore prouvable // ── Catégorisation par Z du treillis : gap bedrock = solide ; hors layout = air constant ; // SurfaceWorld = test colonne ; un slot cave = verdict de pile sur sa sous-boîte. ── struct FSurfSlot { int32 BotChunkZ = INT32_MAX; // identité du slot (borne basse de la strate, en chunks) int32 RepChunkZ = 0; // chunk Z représentatif pour la résolution des params int32 StrateKey = MIN_int32; // même clé que GetDensityAt (StrateBottomWorldZ arrondi) float OverhangMargin = 0.0f; // F20 : hauteur max de corniche (= max OverhangHeight strate+biomes) FSurfaceGenerationParams Params; FBiomeContext BiomeCtx; TArray BiomeParams; TArray InteriorZ; // z hors bandes de seal → testés par colonne }; // thread_local : les TArray gardent leur capacité d'un appel à l'autre (zéro malloc/tuile). static thread_local FSurfSlot Slots[2]; static thread_local FChunkBiomeCache TC_BiomeCache; // biome grid du classifieur (valeurs ≡ CP_BiomeCache) // AUDIT C2 — même discipline que le chemin densité : la grille de biome du classifieur survit // d'un appel à l'autre et sa boîte de validité ne dit rien du contexte qui l'a produite. static thread_local uint32 TC_SeenVersion = 0xFFFFFFFFu; static thread_local uint64 TC_SeenOwnerId = 0; static thread_local uint64 TC_SeenManagerLifetimeId = 0; const uint32 TC_LayoutVersion = StrateManager->GetLayoutVersion(); const uint64 TC_OwnerId = DensityCacheOwnerId; const uint64 TC_ManagerLifetimeId = StrateManager->GetCacheLifetimeId(); if (TC_LayoutVersion != TC_SeenVersion || TC_OwnerId != TC_SeenOwnerId || TC_ManagerLifetimeId != TC_SeenManagerLifetimeId) { TC_SeenVersion = TC_LayoutVersion; TC_SeenOwnerId = TC_OwnerId; TC_SeenManagerLifetimeId = TC_ManagerLifetimeId; TC_BiomeCache.Invalidate(); } int32 NumSlots = 0; // ── T1.d GÉNÉRIQUE : la pile d'opérateurs classe les archétypes de CAVE ── // Ces valeurs suivent l'UNIQUE slot de cave que la tuile touche. La pile classera seulement la // sous-boîte Z de ses échantillons ; les catégories gap/surface/hors-layout plient séparément // leurs hypothèses dans `bCanSolid` / `bCanAir`. // These values track the ONE cave slot touched by the tile. The stack classifies only its // sampled Z sub-box; gap/surface/out-of-layout fold their hypotheses separately. int32 CaveBotChunkZ = INT32_MAX; // identité du slot de cave (borne basse, en chunks) int32 CaveRepChunkZ = 0; int32 CaveMinZ = MAX_int32; int32 CaveMaxZ = MIN_int32; bool bAnyCave = false; bool bAnyGap = false; bool bAnySurface = false; bool bAnyOutOfLayout = false; int32 MemoChunkZ = INT32_MAX; int32 MemoCat = -1; // 0 = gap, 1 = surface, 2 = cave (pile), 3 = hors layout int32 MemoSlotIdx = -1; // « Ce chunk Z appartient-il à une strate ? » sous forme publique : `FindSlotIndexForChunkZ` // est `protected`, `GetStrateChunkZBounds` rend false pour exactement le même cas. auto VF_ChunkZHasSlot = [&](int32 Z) -> bool { int32 UnusedTopCZ = 0, UnusedBotCZ = 0; return StrateManager->GetStrateChunkZBounds(Z, UnusedTopCZ, UnusedBotCZ); }; for (int32 g = 0; g <= CPA; ++g) { const int32 Zi = OriginVoxels.Z + g * Step; const int32 ChunkZ = FloorDivC(Zi, CHUNK_SIZE); if (ChunkZ != MemoChunkZ) { MemoChunkZ = ChunkZ; const FIntVector CC(0, 0, ChunkZ); // les requêtes de layout ne dépendent que de Z if (StrateManager->IsGapChunk(CC)) { MemoCat = 0; bAnyGap = true; } //================================================================= // ⛔ HORS LAYOUT = AIR CONSTANT. C'ÉTAIT LE BLOCAGE DE T1.d. //================================================================= // `GetGeneratorTypeForChunk` rend `TunnelNetwork` pour tout chunk hors de la pile de // strates (« le chemin de repli produit de la roche de toute façon » — CE COMMENTAIRE // EST FAUX) et `IsGapChunk` rend false au-dessus du sommet (« open air, NOT a gap »). // Résultat : chaque tuile touchant l'air libre au-dessus du monde entrait dans la // BRANCHE DE CAVE, n'y trouvait aucun slot, et abandonnait — mesuré en jeu à 83 % des // tuiles classées. // // La vérité est dans `GetGenerationParams` : hors layout il rend `BaseDensity = -1`, // `RoomDensity = 0`, `WormStrength = 0` — un champ CONSTANT, donc de l'air, sans salle // ni ver pour le percer. Une telle tuile est prouvable sans échantillonner. // // Out-of-layout is a CONSTANT AIR field, not a cave archetype. Every tile touching the // open air above the world was being routed into the cave branch and bailing there. // `GetStrateChunkZBounds` (PUBLIC) rend false exactement quand `FindSlotIndexForChunkZ` // rend -1 — ce dernier est `protected`, et cette fonction l'utilise déjà deux fois pour // la même question. Pas de nouvelle surface d'API pour un prédicat qui existe. // GetStrateChunkZBounds is the public form of "has a layout slot"; the index accessor // is protected and this function already uses the bounds call twice for the same test. else if (!VF_ChunkZHasSlot(ChunkZ)) { MemoCat = 3; bAnyOutOfLayout = true; // Les disturbances sont appliquées APRÈS la densité d'archétype et peuvent AJOUTER // de la roche (ponts, arêtes). Même prudence que les branches gap et cave : si // l'une peut agir ici, on ne prouve rien. Les chasms ne font que creuser ⇒ ils ne // menacent pas un verdict d'air. const FStrateDisturbanceParams DOut = StrateManager->GetDisturbanceParamsForChunk(CC); if (VF_AnyBridgeCanTouchBox(TileVoxelBox, DOut, (uint32)Seed) || VF_AnyRidgeCanTouchBox(TileVoxelBox, DOut, (uint32)Seed)) { return EVoxelTileClass::Mixed; } } else if (StrateManager->GetGeneratorTypeForChunk(CC) == ECaveGeneratorType::SurfaceWorld) { // Retrouve (ou résout) le slot de strate — l'identité vient des bornes chunk-Z du layout. int32 TopCZ = 0, BotCZ = 0; if (!StrateManager->GetStrateChunkZBounds(ChunkZ, TopCZ, BotCZ)) { return EVoxelTileClass::Mixed; } MemoSlotIdx = -1; for (int32 s = 0; s < NumSlots; ++s) { if (Slots[s].BotChunkZ == BotCZ) { MemoSlotIdx = s; break; } } if (MemoSlotIdx < 0) { if (NumSlots >= 2) { return EVoxelTileClass::Mixed; // >2 strates surface dans une tuile : improbable } MemoSlotIdx = NumSlots++; FSurfSlot& S = Slots[MemoSlotIdx]; S.BotChunkZ = BotCZ; S.RepChunkZ = ChunkZ; S.InteriorZ.Reset(); ResolveSurfaceChunkParams(CC, S.Params, S.BiomeCtx, S.BiomeParams); S.StrateKey = FMath::RoundToInt(S.Params.StrateBottomWorldZ); // F20 : marge d'overhang = hauteur max (OverhangHeight) sur laquelle une corniche // peut AJOUTER de la roche AU-DESSUS du sol. La corniche est plafonnée à // TerrainZ+Height ⇒ au-delà, air prouvable ; dans (TerrainZ, TerrainZ+Height], ni air // ni solide prouvable → colonne forcée en Mixed. (L'union n'enlève jamais de roche ⇒ // sous le terrain reste solide prouvable : marge vers le HAUT uniquement.) S.OverhangMargin = (S.Params.OverhangStrength > 0.0f) ? S.Params.OverhangHeight : 0.0f; for (const FSurfaceGenerationParams& BP : S.BiomeParams) { if (BP.OverhangStrength > 0.0f) S.OverhangMargin = FMath::Max(S.OverhangMargin, BP.OverhangHeight); } // Disturbances de cette strate : les chasms CREUSENT (cassent AllSolid), les // ponts/arêtes AJOUTENT de la roche dans l'intérieur (cassent AllAir). const FStrateDisturbanceParams D = StrateManager->GetDisturbanceParamsForChunk(CC); if (D.ChasmDensity > 0.0f) bCanSolid = false; if (D.BridgeDensity > 0.0f || D.RidgeDensity > 0.0f) bCanAir = false; } MemoCat = 1; bAnySurface = true; } else { // ── ARCHÉTYPE DE CAVE ── Jusqu'ici : `return Mixed`, sans appel. Désormais on tente // le pliage générique de la pile — mais SEULEMENT sous des conditions vérifiables, // parce qu'un faux verdict ici est un trou (pas de géométrie, pas de collision). // // Condition 1 : le slot doit avoir une pile (une définition d'archétype connue). // Sinon on classerait un champ que le mesher ne produira pas. if (!StrateManager->UsesOperatorStackForChunk(CC)) { INC_DWORD_STAT(STAT_VoxelForgeCaveBailNoStack); return EVoxelTileClass::Mixed; } // Condition 2 : un seul slot de cave par tuile. Deux slots = deux jeux de params = // deux piles, et une pile ne sait répondre que pour SA strate. int32 CaveTopCZ = 0, CaveBotCZ = 0; if (!StrateManager->GetStrateChunkZBounds(ChunkZ, CaveTopCZ, CaveBotCZ)) { INC_DWORD_STAT(STAT_VoxelForgeCaveMixOutOfLayout); return EVoxelTileClass::Mixed; // hors layout } if (CaveBotChunkZ != INT32_MAX && CaveBotChunkZ != CaveBotCZ) { INC_DWORD_STAT(STAT_VoxelForgeCaveBailTwoCaveSlots); return EVoxelTileClass::Mixed; } CaveBotChunkZ = CaveBotCZ; CaveRepChunkZ = ChunkZ; bAnyCave = true; MemoCat = 2; } } if (MemoCat == 2) { // La pile répond après la boucle, sur la sous-boîte Z contenant exactement les // échantillons cave (XY reste la boîte complète du treillis). CaveMinZ = FMath::Min(CaveMinZ, Zi); CaveMaxZ = FMath::Max(CaveMaxZ, Zi); } else if (MemoCat == 0) { bCanAir = false; // bedrock du gap = solide (le carve des passages est déjà gardé) } else if (MemoCat == 3) { // Hors layout = air constant (BaseDensity = -1, aucune salle, aucun ver). L'hypothèse // « tout solide » meurt ; « tout air » survit. Les passages et la spine ne font que // creuser — ils sont déjà gardés plus haut et ne peuvent pas rendre ce z solide. // Out of layout = constant air: AllSolid dies, AllAir survives. bCanSolid = false; } else { FSurfSlot& S = Slots[MemoSlotIdx]; // Bande de seal ? Mêmes inégalités qu'ApplyBoundarySeal : à l'intérieur, la densité est // Max(…, SealFactor·BaseDensity) avec SealFactor > 0 ⇒ solide garanti si BaseDensity > 0. const float Z = (float)Zi; const float DistTop = S.Params.StrateTopWorldZ - Z; const float DistBot = Z - S.Params.StrateBottomWorldZ; const float Th = S.Params.BoundarySealThickness; const bool bInBand = Th > 0.0f && S.Params.BaseDensity > 0.0f && ((DistTop >= 0.0f && DistTop < Th) || (DistBot >= 0.0f && DistBot < Th)); if (bInBand) { bCanAir = false; } else { S.InteriorZ.Add(Z); } } if (!bCanSolid && !bCanAir) { return EVoxelTileClass::Mixed; } } // Out-of-layout is a constant-air field. Passage tubes and origin rooms can only carve that // field, so they cannot invalidate AllAir; only their guaranteed support floors can add solid // voxels. Do this after the cheap Z categorisation so open-air LOD tiles avoid the exact // passage-carve lattice walk and the room scan entirely. A floor-only tile remains Mixed and // is meshed normally, preserving the support backstops. if (bAnyOutOfLayout && !bAnyCave && !bAnySurface && !bAnyGap) { if (StrateManager->AnyPassageLandingFloorNearLattice( TileVoxelBox, OriginVoxels, Step) || StrateManager->AnyOriginLandingFloorNearLattice( TileVoxelBox, OriginVoxels, Step) || StrateManager->AnyPassageStructuralPostNearLattice( TileVoxelBox, OriginVoxels, Step)) { return EVoxelTileClass::Mixed; } return EVoxelTileClass::AllAir; } // ── Structural passage/origin guards for non-open-air categories. ── // Carvers kill AllSolid but never add rock; landing and origin support floors kill AllAir. const bool bHasPassageCarvingCandidate = StrateManager->AnyPassageNearLattice( TileVoxelBox, OriginVoxels, Step); if (bHasPassageCarvingCandidate) { bCanSolid = false; } const bool bHasPassageLandingFloorCandidate = StrateManager->AnyPassageLandingFloorNearLattice( TileVoxelBox, OriginVoxels, Step); const bool bHasOriginLandingFloorCandidate = StrateManager->AnyOriginLandingFloorNearLattice( TileVoxelBox, OriginVoxels, Step); // These final solid writers are outside the core operator verdict. Keep their candidate // state after the stack fold too: the AllAir branch below must not re-enable a polarity that // an earlier structural guard already killed. const bool bHasPassageStructuralPostCandidate = StrateManager->AnyPassageStructuralPostNearLattice( TileVoxelBox, OriginVoxels, Step); const bool bHasSolidStructuralCandidate = bHasPassageLandingFloorCandidate || bHasOriginLandingFloorCandidate || bHasPassageStructuralPostCandidate; if (bHasPassageLandingFloorCandidate) { bCanAir = false; } if (StrateManager->AnyOriginLandingNearLattice( TileVoxelBox, OriginVoxels, Step)) { bCanSolid = false; } if (bHasOriginLandingFloorCandidate) { bCanAir = false; } // Native passage D-floors are authored structural posts applied after the operator stack. // They add solid MC samples and therefore must kill the AllAir hypothesis before a tile can // be skipped. if (bHasPassageStructuralPostCandidate) { bCanAir = false; } // ── LE PLIAGE DE LA PILE D'OPÉRATEURS, POUR LES ARCHÉTYPES DE CAVE ── //========================================================================= // ⚠️ ERREUR ICI = TROU, PAS RÉGRESSION. Un verdict non-Mixed fait SAUTER `GenerateMesh` : pas // de triangles, pas de collision, invisible jusqu'à ce qu'un joueur tombe au travers. Toutes les // gardes ci-dessous ÉCHOUENT EN MIXED ; aucune ne donne le bénéfice du doute. // // Le verdict lui-même ne peut pas être meilleur que la pile : `FVoxelOpStack::ClassifyBox` plie // chaque opérateur avec `VF_FoldOp` et rend `Mixed` dès que les deux hypothèses meurent. Ce que // ce bloc ajoute, c'est la vérification que la pile interrogée est bien CELLE QUI PRODUIRA LA // DENSITÉ de cette tuile — même fabrique, mêmes params, même drapeau. if (bAnyCave) { // Diagnostic de PRÉSENCE avant les gardes : le signal reste visible même si le pliage rend // finalement AllSolid/AllAir. Ces compteurs ne sont pas exclusifs entre eux sur une tuile // très haute ; chacun répond exactement à « cette catégorie était-elle aussi présente ? ». // Presence diagnostics run before the guards, so a successful fold cannot hide the mix. // They are not mutually exclusive for a very tall tile; each answers one exact question. if (bAnyOutOfLayout || bAnyGap || bAnySurface) { if (bAnyOutOfLayout) { INC_DWORD_STAT(STAT_VoxelForgeCaveMixOutOfLayout); } if (bAnyGap) { INC_DWORD_STAT(STAT_VoxelForgeCaveMixGap); } if (bAnySurface) { INC_DWORD_STAT(STAT_VoxelForgeCaveMixSurfaceWorld); } } const FIntVector RepCC(0, 0, CaveRepChunkZ); const ECaveGeneratorType CaveType = StrateManager->GetGeneratorTypeForChunk(RepCC); //--------------------------------------------------------------------- // ⚠️ LA GARDE QUI COMPTE : LES PARAMS DOIVENT ÊTRE LES MÊMES SUR TOUTE LA SOUS-BOÎTE CAVE //--------------------------------------------------------------------- // `GetGenerationParams` et ses homologues BLENDENT les params dans les bandes de transition : // `Alpha` dépend du chunk Z pour `Gradient`, et du chunk XY EN PLUS pour `Interleaved`. Deux // chunks d'une même sous-boîte peuvent donc porter des params différents — c'est le constat de // `AUDIT §C2` — et UNE pile ne peut pas représenter DEUX // champs. On construit donc les params pour CHAQUE coordonnée de chunk que la boîte touche et // on exige qu'ils soient identiques bit à bit. // // `Memcmp` sur un POD : un padding différent ne peut produire qu'un FAUX ÉCART, donc un // `Mixed` de trop. On se trompe du côté du CPU, jamais du côté du trou. const int32 CX0 = FloorDivC(MinX, CHUNK_SIZE), CX1 = FloorDivC(MaxX, CHUNK_SIZE); const int32 CY0 = FloorDivC(MinY, CHUNK_SIZE), CY1 = FloorDivC(MaxY, CHUNK_SIZE); // IMPORTANT : les gardes restent complètes, mais seulement sur les chunks où le mesher // échantillonne réellement CETTE strate cave. Inclure gap/surface/hors-layout ici ferait // échouer la garde d'archétype avant de pouvoir plier leurs hypothèses indépendantes. // The guards stay exhaustive over the cave samples. Non-cave chunks are intentionally not // represented by this stack; their hypotheses were folded separately in the Z pass. const int32 CZ0 = FloorDivC(CaveMinZ, CHUNK_SIZE), CZ1 = FloorDivC(CaveMaxZ, CHUNK_SIZE); // Keep the parameter identity check bounded, but do not use the old 27-key cutoff here: // LOD4 legitimately covers 19^3 = 6,859 chunk keys. Returning Mixed at that point sent // the tile straight to GenerateMesh, where GetDensityAt rebuilt one exact-chunk state for // almost every coarse sample. The operator proof now shares its root morphology cache // across refined children, so this scan is cheap compared with the fallback it prevents. const int64 NumChunkCoords = (int64)(CX1 - CX0 + 1) * (int64)(CY1 - CY0 + 1) * (int64)(CZ1 - CZ0 + 1); constexpr int64 MaxClassifiedChunkCoords = 262144; if (NumChunkCoords > MaxClassifiedChunkCoords) { INC_DWORD_STAT(STAT_VoxelForgeCaveBailParams); return EVoxelTileClass::Mixed; } FSlabGenerationParams TileSlab; FMazeGenerationParams TileMaze; FVerticalShaftParams TileVert; FFloatingIslandParams TileFloat; FStrateGenerationParams TileTunnel; bool bFirst = true; for (int32 cz = CZ0; cz <= CZ1; ++cz) for (int32 cy = CY0; cy <= CY1; ++cy) for (int32 cx = CX0; cx <= CX1; ++cx) { const FIntVector CC(cx, cy, cz); if (StrateManager->GetGeneratorTypeForChunk(CC) != CaveType) { INC_DWORD_STAT(STAT_VoxelForgeCaveBailParams); return EVoxelTileClass::Mixed; // la boîte déborde sur un autre archétype } // Every chunk of the box must have a stack, not only the one that triggered the // attempt (redundant with the Z pass today, kept hole-safe). if (!StrateManager->UsesOperatorStackForChunk(CC)) { INC_DWORD_STAT(STAT_VoxelForgeCaveBailNoStack); return EVoxelTileClass::Mixed; } switch (CaveType) { case ECaveGeneratorType::FlatPlain: case ECaveGeneratorType::CrystalChamber: { const FSlabGenerationParams Q = StrateManager->GetSlabParamsForChunk(CC); if (bFirst) { TileSlab = Q; } else if (FMemory::Memcmp(&Q, &TileSlab, sizeof(Q)) != 0) { INC_DWORD_STAT(STAT_VoxelForgeCaveBailParams); return EVoxelTileClass::Mixed; } break; } case ECaveGeneratorType::Maze: { const FMazeGenerationParams Q = StrateManager->GetMazeParamsForChunk(CC); if (bFirst) { TileMaze = Q; } else if (FMemory::Memcmp(&Q, &TileMaze, sizeof(Q)) != 0) { INC_DWORD_STAT(STAT_VoxelForgeCaveBailParams); return EVoxelTileClass::Mixed; } break; } case ECaveGeneratorType::VerticalShafts: { const FVerticalShaftParams Q = StrateManager->GetVerticalShaftParamsForChunk(CC); if (bFirst) { TileVert = Q; } else if (FMemory::Memcmp(&Q, &TileVert, sizeof(Q)) != 0) { INC_DWORD_STAT(STAT_VoxelForgeCaveBailParams); return EVoxelTileClass::Mixed; } break; } case ECaveGeneratorType::FloatingIslands: { const FFloatingIslandParams Q = StrateManager->GetFloatingIslandParamsForChunk(CC); if (bFirst) { TileFloat = Q; } else if (FMemory::Memcmp(&Q, &TileFloat, sizeof(Q)) != 0) { INC_DWORD_STAT(STAT_VoxelForgeCaveBailParams); return EVoxelTileClass::Mixed; } break; } case ECaveGeneratorType::Underwater: case ECaveGeneratorType::TunnelNetwork: { const FStrateGenerationParams Q = StrateManager->GetGenerationParams(CC); if (bFirst) { TileTunnel = Q; } else if (FMemory::Memcmp(&Q, &TileTunnel, sizeof(Q)) != 0) { INC_DWORD_STAT(STAT_VoxelForgeCaveBailParams); return EVoxelTileClass::Mixed; } break; } default: INC_DWORD_STAT(STAT_VoxelForgeCaveBailParams); return EVoxelTileClass::Mixed; // SurfaceWorld ne peut pas être le type du slot cave } bFirst = false; } //--------------------------------------------------------------------- // La pile — construite par la MÊME fabrique que `GetDensityAt`. //--------------------------------------------------------------------- FVoxelStackParamRefs Refs; Refs.Slab = &TileSlab; Refs.Maze = &TileMaze; Refs.Vert = &TileVert; Refs.Float = &TileFloat; Refs.Tunnel = &TileTunnel; // `Refs.Surface` reste nul — la fabrique refuse alors SurfaceWorld, et c'est voulu : cette // fonction le prouve elle-même sur le treillis EXACT du mesher, ce qu'aucune borne de boîte // ne fera mieux. FVoxelOpContext OpCtx; OpCtx.ChunkCoord = RepCC; OpCtx.Seed = (uint32)Seed; OpCtx.LayoutVersion = TC_LayoutVersion; OpCtx.Step = Step; OpCtx.WorldRadiusVoxels = WorldRadiusVoxels; OpCtx.EdgeSealThickness = EdgeSealThickness; OpCtx.bUseLatticeProof = true; OpCtx.bTightenWarpProof = Step <= 2; OpCtx.LatticeOriginVoxels = OriginVoxels; FVoxelOpStack TileStack; if (!VF_BuildOpStackForChunk(CaveType, Refs, Seed, OriginSpineRadius, StrateManager, TileStack, OpCtx)) { // Strate dégénérée : `GetDensityAt` y rend de l'air sans pile, donc la pile ne décrit // pas ce que le mesher verra. Aucun verdict. INC_DWORD_STAT(STAT_VoxelForgeCaveBailNoStack); return EVoxelTileClass::Mixed; } TileStack.PrepareChunk(OpCtx); const FBox CaveBox(FVector((float)MinX, (float)MinY, (float)CaveMinZ), FVector((float)MaxX, (float)MaxY, (float)CaveMaxZ)); const EVoxelTileClass StackVerdict = VF_ClassifyBoxWithWarpRetry( TileStack, CaveBox, OpCtx, this, StrateManager, TileTunnel.BaseDensity, OutStats); if (StackVerdict == EVoxelTileClass::Mixed) { INC_DWORD_STAT(STAT_VoxelForgeCaveBailStackVerdict); return EVoxelTileClass::Mixed; } const bool bCaveOnlyTile = bAnyCave && !bAnyGap && !bAnySurface && !bAnyOutOfLayout && CaveMinZ == MinZ && CaveMaxZ == MaxZ; if (bCaveOnlyTile && StackVerdict == EVoxelTileClass::AllSolid) { // The operator stack does not own the final passage carve. Keep // its AllSolid shortcut only when the exact-lattice passage guard // found no modifier or body-envelope write in this tile. bCanSolid = !bHasPassageCarvingCandidate; bCanAir = false; } else if (bCaveOnlyTile && StackVerdict == EVoxelTileClass::AllAir) { bCanSolid = false; bCanAir = true; } else if (StackVerdict == EVoxelTileClass::AllSolid) { bCanAir = false; } else { bCanSolid = false; } // The stack verdict does not own these final MC-space solid writers. In particular, an // AllAir core result must not restore bCanAir after a native D-floor or landing-floor // candidate has already made the full field potentially solid. if (bHasSolidStructuralCandidate) { bCanAir = false; } // Le verdict cave se plie avec gap=solide, hors-layout=air, seals surface=solide. Si les // deux hypothèses sont mortes ici, les catégories se contredisent : ce n'est PAS un échec // de borne de la pile ni une disturbance. // Fold the cave verdict with gap=solid, out-of-layout=air, and solid surface seals. If both // hypotheses die here, the categories conflict; this is not a stack-bound/disturbance bail. if (!bCanSolid && !bCanAir) { INC_DWORD_STAT(STAT_VoxelForgeCaveBailFoldConflict); return EVoxelTileClass::Mixed; } //--------------------------------------------------------------------- // ⚠️ LES DISTURBANCES NE SONT PAS DANS LA PILE (`OPSTACK-DECOMPOSITION §10.2`) : // `GetDensityAt` les applique APRÈS, sur la densité déjà négatée. Un verdict qui les // ignorerait serait faux exactement là où elles agissent. Mêmes inégalités que la branche // SurfaceWorld plus haut, pour la même raison. //--------------------------------------------------------------------- const FStrateDisturbanceParams D = StrateManager->GetDisturbanceParamsForChunk(RepCC); // A non-zero strate density is only a possibility. Discharge it when no deterministic // hash-placed primitive can touch this exact tile; a hit keeps the old polarity-specific // conservative result (chasms kill AllSolid, bridges/ridges kill AllAir). if (VF_AnyChasmCanTouchLattice( OriginVoxels, Step, CPA, D, (uint32)Seed)) { bCanSolid = false; } if (VF_AnyBridgeCanTouchLattice( OriginVoxels, Step, CPA, D, (uint32)Seed) || VF_AnyRidgeCanTouchLattice( OriginVoxels, Step, CPA, D, (uint32)Seed)) { bCanAir = false; } if (!bCanSolid && !bCanAir) { INC_DWORD_STAT(STAT_VoxelForgeCaveBailDisturbance); return EVoxelTileClass::Mixed; } } // ── Balayage des colonnes XY sur le treillis exact du mesher (marge incluse). Une colonne // tranche chaque z intérieur : air côté MC ⇔ TerrainZ ≤ z ≤ CeilSurf (D = −interne ≥ 0, // cf. FSurfaceColumnSource ; spine/passages ne font QUE de l'air → gardés plus haut). // Pré-passe clairsemée ~5×5 pour tuer vite les tuiles traversées par la surface, puis // passe complète — les colonnes recalculées par la pré-passe restent chaudes dans la boîte. auto TestColumn = [&](int32 gx, int32 gy) -> bool // false ⇒ les deux hypothèses sont mortes { const int32 Xi = OriginVoxels.X + gx * Step; const int32 Yi = OriginVoxels.Y + gy * Step; for (int32 s = 0; s < NumSlots; ++s) { FSurfSlot& S = Slots[s]; if (S.InteriorZ.Num() == 0) continue; FSurfaceColumnBox& Box = GSurfColCache.Acquire( Xi, Yi, S.StrateKey, Seed, TC_LayoutVersion, TC_OwnerId, TC_ManagerLifetimeId); const int32 CI = (Yi - Box.BaseY) * FSurfaceColumnBox::Dim + (Xi - Box.BaseX); if (!Box.Computed[CI]) { ComputeSurfaceColumn((float)Xi, (float)Yi, S.RepChunkZ, S.Params, S.BiomeCtx, S.BiomeParams, TC_BiomeCache, Box.Cols[CI].TerrainZ, Box.Cols[CI].CeilSurf, Box.Cols[CI].OverhangAmp, Box.Cols[CI].DirX, Box.Cols[CI].DirY); Box.Computed[CI] = true; } const float T = Box.Cols[CI].TerrainZ; const float C = Box.Cols[CI].CeilSurf; const float M = S.OverhangMargin; for (const float Z : S.InteriorZ) { // F20 : juste au-dessus du sol (jusqu'à +OverhangHeight) une corniche peut ajouter de la // roche ⇒ ni air ni solide prouvable → tuile Mixed. (Vers le haut uniquement : l'union // n'enlève rien sous le terrain ; le cap n'est pas affecté.) if (M > 0.0f && Z > T && Z <= T + M) return false; if (Z >= T && Z <= C) { bCanSolid = false; } // point côté air (surface incluse) else { bCanAir = false; } // sous le terrain / dans le cap if (!bCanSolid && !bCanAir) return false; } } return true; }; const bool bNeedColumns = (NumSlots > 0) && (Slots[0].InteriorZ.Num() > 0 || (NumSlots > 1 && Slots[1].InteriorZ.Num() > 0)); if (bNeedColumns) { const int32 PreStride = FMath::Max(1, GridDim / 4); for (int32 gy = 0; gy <= CPA; gy += PreStride) for (int32 gx = 0; gx <= CPA; gx += PreStride) if (!TestColumn(gx, gy)) return EVoxelTileClass::Mixed; for (int32 gy = 0; gy <= CPA; ++gy) for (int32 gx = 0; gx <= CPA; ++gx) if (!TestColumn(gx, gy)) return EVoxelTileClass::Mixed; } // Ici exactement UNE hypothèse doit survivre (chaque point testé en tue une ; les tuiles // sans point intérieur ont tué AllAir via gap/seal). Égalité = prudence → Mixed. if (bCanSolid == bCanAir) { return EVoxelTileClass::Mixed; } if (bAnyCave) { // Compte seulement les verdicts FINAUX qui sautent réellement une tuile. Une pile peut avoir // prouvé sa sous-boîte cave puis perdre l'hypothèse sur une colonne SurfaceWorld adjacente. // Count only final verdicts that actually skip a tile; a later surface column may still // invalidate the hypothesis proved for the cave sub-box. if (bCanSolid) { INC_DWORD_STAT(STAT_VoxelForgeTilesOpStackSolid); } else { INC_DWORD_STAT(STAT_VoxelForgeTilesOpStackAir); } } return bCanSolid ? EVoxelTileClass::AllSolid : EVoxelTileClass::AllAir; } //============================================================================= // CLIMATE & BIOME FIELDS //============================================================================= // Pure functions of world XY (+ seed). The relief field is shared with SurfaceWorld // terrain so the biome map and the terrain it modulates stay in agreement. The biome // field is a warped Voronoi whose cells are assigned a biome by climate — coherent // geography (mountains cluster in high relief), window-invariant by construction. float UVoxelGenerator::SampleRelief(float WorldX, float WorldY, float Frequency, float Contrast) const { const uint32 SeedU = (uint32)Seed; // Same offsets/octaves as the original SurfaceWorld relief so existing worlds are // unchanged (this is the function that code path now calls). float R = FractalNoise3D(FVector3f( WorldX * Frequency + VoxelHash::SeedOffset(SeedU, 7.3f), WorldY * Frequency + VoxelHash::SeedOffset(SeedU, 2.1f), VoxelHash::SeedOffset(SeedU, 0.5f)), 2) * 0.5f + 0.5f; // [0,1] R = FMath::Clamp((R - 0.5f) * Contrast + 0.5f, 0.0f, 1.0f); return SmoothStep01(R); } float UVoxelGenerator::SampleMoisture(float WorldX, float WorldY, float Frequency) const { const uint32 SeedU = (uint32)Seed; const float N = FractalNoise3D(FVector3f( WorldX * Frequency + VoxelHash::SeedOffset(SeedU, 4.7f), WorldY * Frequency + VoxelHash::SeedOffset(SeedU, 8.9f), VoxelHash::SeedOffset(SeedU, 1.3f)), 2) * 0.5f + 0.5f; // [0,1] return FMath::Clamp(N, 0.0f, 1.0f); } bool UVoxelGenerator::EvaluateTerrainConditions(const TArray& Conditions, float WorldX, float WorldY, const FBiomeContext& BiomeCtx) const { if (Conditions.Num() == 0) return true; // zero-cost default (the common case) // Freq/contrast come from the strate's biome map (defaults when the strate has no biomes) — the same // params SampleRelief/SampleMoisture are given everywhere else, so a condition agrees with the terrain. const FBiomeMapParams MP = BiomeCtx.IsValid() ? BiomeCtx.Map : FBiomeMapParams(); // Lazily sample each field only if a condition references it (BiomeBorder's Voronoi is the pricey one). float Relief = 0.0f, Moisture = 0.0f, Border = 0.0f; bool bHaveRelief = false, bHaveMoisture = false, bHaveBorder = false; for (const FTerrainCondition& C : Conditions) { float V = 0.0f; switch (C.Type) { case ETerrainConditionType::Relief: if (!bHaveRelief) { Relief = SampleRelief(WorldX, WorldY, MP.ReliefFrequency, MP.ReliefContrast); bHaveRelief = true; } V = Relief; break; case ETerrainConditionType::Moisture: if (!bHaveMoisture) { Moisture = SampleMoisture(WorldX, WorldY, MP.MoistureFrequency); bHaveMoisture = true; } V = Moisture; break; case ETerrainConditionType::BiomeBorder: if (!bHaveBorder) { Border = SampleBiomeAt(WorldX, WorldY, BiomeCtx).NeighborWeight; bHaveBorder = true; } V = Border; break; default: break; } bool bPass = (V >= C.Min && V <= C.Max); if (C.bInvert) bPass = !bPass; if (!bPass) return false; // AND semantics: any failing condition rejects the candidate } return true; } int32 UVoxelGenerator::ClassifyBiomeAtSite(float SiteX, float SiteY, const FBiomeContext& Ctx, uint32 SiteHash) const { const float R = SampleRelief(SiteX, SiteY, Ctx.Map.ReliefFrequency, Ctx.Map.ReliefContrast); const float Mo = SampleMoisture(SiteX, SiteY, Ctx.Map.MoistureFrequency); int32 Best = 0; float BestScore = FLT_MAX; for (int32 i = 0; i < Ctx.Biomes.Num(); ++i) { const FBiomeResolved& B = Ctx.Biomes[i]; // Distance in climate space to the biome's box (0 when inside it). const float dr = (R < B.ReliefMin) ? (B.ReliefMin - R) : (R > B.ReliefMax) ? (R - B.ReliefMax) : 0.0f; const float dm = (Mo < B.MoistureMin) ? (B.MoistureMin - Mo) : (Mo > B.MoistureMax) ? (Mo - B.MoistureMax) : 0.0f; float Score = dr * dr + dm * dm; // Tiny deterministic jitter so overlapping boxes don't all collapse to biome 0. Score += VoxelHash::ToFloat01(VoxelHash::Mix(SiteHash ^ (0x9e3779b9u * (uint32)(i + 1)))) * 1.0e-4f; if (Score < BestScore) { BestScore = Score; Best = i; } } return Best; } FBiomeSample UVoxelGenerator::SampleBiomeAt(float WorldX, float WorldY, const FBiomeContext& Ctx) const { FBiomeSample Out; if (Ctx.Biomes.Num() == 0) return Out; // biomes disabled if (Ctx.Biomes.Num() == 1) { Out.DominantIndex = 0; return Out; } const FBiomeMapParams& MP = Ctx.Map; const float Cell = FMath::Max(MP.CellSize, 1.0f); const uint32 S = (uint32)Seed ^ 0x42494f4du; // 'BIOM' // Domain-warp the query so cell borders wind organically (not a hex grid). float QX = WorldX, QY = WorldY; if (MP.WarpStrength > 0.0f) { const uint32 SeedU = (uint32)Seed; const float WF = MP.WarpFrequency; const float wx = VoxelNoise::Perlin3D(FVector3f(WorldX * WF + VoxelHash::SeedOffset(SeedU, 0.27f), WorldY * WF + 3.1f, VoxelHash::SeedOffset(SeedU, 1.1f))); const float wy = VoxelNoise::Perlin3D(FVector3f(WorldX * WF + 7.7f, WorldY * WF + VoxelHash::SeedOffset(SeedU, 0.61f), VoxelHash::SeedOffset(SeedU, 2.3f))); QX += wx * VOXEL_NOISE_SCALE * MP.WarpStrength; QY += wy * VOXEL_NOISE_SCALE * MP.WarpStrength; } const int32 CX = FMath::FloorToInt(QX / Cell); const int32 CY = FMath::FloorToInt(QY / Cell); // Worley F1/F2 over the 3x3 neighbourhood of the (warped) cell. With jitter confined // to [0,1) of a cell, the nearest site is always within this neighbourhood. float BestD2 = FLT_MAX, SecondD2 = FLT_MAX; int32 BestBiome = 0, SecondBiome = 0; for (int32 dy = -1; dy <= 1; ++dy) for (int32 dx = -1; dx <= 1; ++dx) { const int32 nx = CX + dx, ny = CY + dy; const uint32 h = VoxelHash::Cell(nx, ny, S); const float jx = VoxelHash::ToFloat01(h); const float jy = VoxelHash::ToFloat01(VoxelHash::Mix(h ^ 0x68bc21ebu)); const float sx = (nx + jx) * Cell; const float sy = (ny + jy) * Cell; const float ddx = sx - QX, ddy = sy - QY; const float d2 = ddx * ddx + ddy * ddy; if (d2 < BestD2) { SecondD2 = BestD2; SecondBiome = BestBiome; BestD2 = d2; BestBiome = ClassifyBiomeAtSite(sx, sy, Ctx, h); } else if (d2 < SecondD2) { SecondD2 = d2; SecondBiome = ClassifyBiomeAtSite(sx, sy, Ctx, h); } } Out.DominantIndex = BestBiome; Out.NeighborIndex = SecondBiome; // Blend weight: 0.5 at the shared border (d1≈d2), fading to 0 a BorderBlend-wide // band inside the dominant cell. Only blend when the neighbour is a DIFFERENT biome. if (MP.BorderBlend > 0.0f && SecondD2 < FLT_MAX && BestBiome != SecondBiome) { const float d1 = FMath::Sqrt(BestD2); const float d2 = FMath::Sqrt(SecondD2); const float t = FMath::Clamp((d2 - d1) / FMath::Max(MP.BorderBlend, 1.0f), 0.0f, 1.0f); Out.NeighborWeight = 0.5f * (1.0f - SmoothStep01(t)); } return Out; } void UVoxelGenerator::RebuildBiomeGrid(int32 ChunkX, int32 ChunkY, int32 ChunkZ, const FBiomeContext& Ctx, FChunkBiomeCache& Cache, uint64 OwnerId, uint64 ManagerLifetimeId) const { Cache.ChunkZ = ChunkZ; Cache.Seed = Seed; Cache.OwnerId = OwnerId; Cache.ManagerLifetimeId = ManagerLifetimeId; Cache.bActive = Ctx.IsValid(); Cache.Ctx = Ctx; Cache.CellBiome.Reset(); if (!Cache.bActive) { // Mark the whole chunk footprint valid so non-biome chunks don't rebuild per voxel. Cache.ValidMinX = (float)ChunkX * CHUNK_SIZE - (float)CHUNK_SIZE; Cache.ValidMaxX = (float)(ChunkX + 1) * CHUNK_SIZE + (float)CHUNK_SIZE; Cache.ValidMinY = (float)ChunkY * CHUNK_SIZE - (float)CHUNK_SIZE; Cache.ValidMaxY = (float)(ChunkY + 1) * CHUNK_SIZE + (float)CHUNK_SIZE; return; } const FBiomeMapParams& MP = Ctx.Map; const float Cell = FMath::Max(MP.CellSize, 1.0f); const uint32 S = (uint32)Seed ^ 0x42494f4du; // 'BIOM' (must match SampleBiomeAt) // Validity halo: one chunk beyond the footprint, so the +X/+Y boundary corners and // ±1 gradient-normal samples stay inside the valid box (no rebuild thrash, §8.10). const float Halo = (float)CHUNK_SIZE; Cache.ValidMinX = (float)ChunkX * CHUNK_SIZE - Halo; Cache.ValidMaxX = (float)(ChunkX + 1) * CHUNK_SIZE + Halo; Cache.ValidMinY = (float)ChunkY * CHUNK_SIZE - Halo; Cache.ValidMaxY = (float)(ChunkY + 1) * CHUNK_SIZE + Halo; // Cell-grid coverage: the valid box expanded by the max warp displacement + one cell, // so the 3x3 search around any in-box query's warped point is fully present. const float CellMargin = MP.WarpStrength * VOXEL_NOISE_SCALE + Cell + 1.0f; Cache.BaseCellX = FMath::FloorToInt((Cache.ValidMinX - CellMargin) / Cell); Cache.BaseCellY = FMath::FloorToInt((Cache.ValidMinY - CellMargin) / Cell); const int32 MaxCellX = FMath::FloorToInt((Cache.ValidMaxX + CellMargin) / Cell); const int32 MaxCellY = FMath::FloorToInt((Cache.ValidMaxY + CellMargin) / Cell); Cache.CellsX = MaxCellX - Cache.BaseCellX + 1; Cache.CellsY = MaxCellY - Cache.BaseCellY + 1; Cache.CellBiome.SetNumUninitialized(Cache.CellsX * Cache.CellsY); for (int32 cy = 0; cy < Cache.CellsY; ++cy) for (int32 cx = 0; cx < Cache.CellsX; ++cx) { const int32 nx = Cache.BaseCellX + cx; const int32 ny = Cache.BaseCellY + cy; const uint32 h = VoxelHash::Cell(nx, ny, S); const float jx = VoxelHash::ToFloat01(h); const float jy = VoxelHash::ToFloat01(VoxelHash::Mix(h ^ 0x68bc21ebu)); const float sx = (nx + jx) * Cell; const float sy = (ny + jy) * Cell; Cache.CellBiome[cy * Cache.CellsX + cx] = ClassifyBiomeAtSite(sx, sy, Ctx, h); } } FBiomeSample UVoxelGenerator::ResolveBiomeSampleAt(float WorldX, float WorldY, int32 ChunkZ, const FBiomeContext& Ctx, FChunkBiomeCache& Cache) const { FBiomeSample Out; const uint64 OwnerId = DensityCacheOwnerId; const uint64 ManagerLifetimeId = StrateManager ? StrateManager->GetCacheLifetimeId() : 0; // Box-validated rebuild (perf-only; result is the pure function of XY either way). if (!Cache.Contains(WorldX, WorldY, ChunkZ, Seed, OwnerId, ManagerLifetimeId)) { RebuildBiomeGrid(FMath::FloorToInt(WorldX / CHUNK_SIZE), FMath::FloorToInt(WorldY / CHUNK_SIZE), ChunkZ, Ctx, Cache, OwnerId, ManagerLifetimeId); } if (!Cache.bActive) return Out; if (Cache.Ctx.Biomes.Num() == 1) { Out.DominantIndex = 0; return Out; } const FBiomeMapParams& MP = Cache.Ctx.Map; const float Cell = FMath::Max(MP.CellSize, 1.0f); const uint32 S = (uint32)Seed ^ 0x42494f4du; // Same warp + 3x3 Worley as SampleBiomeAt → identical assignment (matches the preview). float QX = WorldX, QY = WorldY; if (MP.WarpStrength > 0.0f) { const uint32 SeedU = (uint32)Seed; const float WF = MP.WarpFrequency; const float wx = VoxelNoise::Perlin3D(FVector3f(WorldX * WF + VoxelHash::SeedOffset(SeedU, 0.27f), WorldY * WF + 3.1f, VoxelHash::SeedOffset(SeedU, 1.1f))); const float wy = VoxelNoise::Perlin3D(FVector3f(WorldX * WF + 7.7f, WorldY * WF + VoxelHash::SeedOffset(SeedU, 0.61f), VoxelHash::SeedOffset(SeedU, 2.3f))); QX += wx * VOXEL_NOISE_SCALE * MP.WarpStrength; QY += wy * VOXEL_NOISE_SCALE * MP.WarpStrength; } const int32 CX = FMath::FloorToInt(QX / Cell); const int32 CY = FMath::FloorToInt(QY / Cell); float BestD2 = FLT_MAX, SecondD2 = FLT_MAX; int32 BestBiome = 0, SecondBiome = 0; for (int32 dy = -1; dy <= 1; ++dy) for (int32 dx = -1; dx <= 1; ++dx) { const int32 nx = CX + dx, ny = CY + dy; const uint32 h = VoxelHash::Cell(nx, ny, S); const float jx = VoxelHash::ToFloat01(h); const float jy = VoxelHash::ToFloat01(VoxelHash::Mix(h ^ 0x68bc21ebu)); const float sx = (nx + jx) * Cell; const float sy = (ny + jy) * Cell; const float ddx = sx - QX, ddy = sy - QY; const float d2 = ddx * ddx + ddy * ddy; // Cached biome index (fallback to classify on the rare margin miss → still correct). const int32 gx = nx - Cache.BaseCellX; const int32 gy = ny - Cache.BaseCellY; const int32 BiomeIdx = (gx >= 0 && gx < Cache.CellsX && gy >= 0 && gy < Cache.CellsY) ? Cache.CellBiome[gy * Cache.CellsX + gx] : ClassifyBiomeAtSite(sx, sy, Cache.Ctx, h); if (d2 < BestD2) { SecondD2 = BestD2; SecondBiome = BestBiome; BestD2 = d2; BestBiome = BiomeIdx; } else if (d2 < SecondD2) { SecondD2 = d2; SecondBiome = BiomeIdx; } } Out.DominantIndex = BestBiome; Out.NeighborIndex = SecondBiome; if (MP.BorderBlend > 0.0f && SecondD2 < FLT_MAX && BestBiome != SecondBiome) { const float d1 = FMath::Sqrt(BestD2); const float d2 = FMath::Sqrt(SecondD2); const float t = FMath::Clamp((d2 - d1) / FMath::Max(MP.BorderBlend, 1.0f), 0.0f, 1.0f); Out.NeighborWeight = 0.5f * (1.0f - SmoothStep01(t)); } return Out; } const UVoxelBiomeDefinition* UVoxelGenerator::GetDominantBiomeAt(float WorldX, float WorldY, int32 ChunkZ) const { if (!StrateManager) return nullptr; const FIntVector Coord(FMath::FloorToInt(WorldX / CHUNK_SIZE), FMath::FloorToInt(WorldY / CHUNK_SIZE), ChunkZ); const FBiomeContext Ctx = StrateManager->GetBiomeContextForChunk(Coord); if (!Ctx.IsValid()) return nullptr; const FBiomeSample S = SampleBiomeAt(WorldX, WorldY, Ctx); if (!Ctx.Biomes.IsValidIndex(S.DominantIndex)) return nullptr; // Map the context position back to the strate's Biomes[] asset. const int32 StrateBiomeIdx = Ctx.Biomes[S.DominantIndex].Index; const UVoxelStrateDefinition* Def = StrateManager->GetStrateForChunk(Coord); return (Def && Def->Biomes.IsValidIndex(StrateBiomeIdx)) ? Def->Biomes[StrateBiomeIdx] : nullptr; } void UVoxelGenerator::QueryBiomeAt(float WorldX, float WorldY, int32 ChunkZ, FVoxelBiomeQuery& Out) const { Out = FVoxelBiomeQuery(); if (!StrateManager) return; const FIntVector Coord(FMath::FloorToInt(WorldX / CHUNK_SIZE), FMath::FloorToInt(WorldY / CHUNK_SIZE), ChunkZ); const FBiomeContext Ctx = StrateManager->GetBiomeContextForChunk(Coord); // Climate fields are always meaningful (use the strate's map freqs, or defaults when no biomes). const FBiomeMapParams MP = Ctx.IsValid() ? Ctx.Map : FBiomeMapParams(); Out.Relief = SampleRelief(WorldX, WorldY, MP.ReliefFrequency, MP.ReliefContrast); Out.Moisture = SampleMoisture(WorldX, WorldY, MP.MoistureFrequency); if (!Ctx.IsValid()) return; // bHasBiomes stays false → BP knows this strate has no biome field Out.bHasBiomes = true; const FBiomeSample S = SampleBiomeAt(WorldX, WorldY, Ctx); Out.NeighborWeight = S.NeighborWeight; Out.DominantContextIndex = S.DominantIndex; const UVoxelStrateDefinition* Def = StrateManager->GetStrateForChunk(Coord); if (Ctx.Biomes.IsValidIndex(S.DominantIndex)) { const int32 DomStrateIdx = Ctx.Biomes[S.DominantIndex].Index; if (Def && Def->Biomes.IsValidIndex(DomStrateIdx)) { UVoxelBiomeDefinition* Bio = Def->Biomes[DomStrateIdx]; Out.DominantBiome = Bio; if (Bio) { Out.DebugColor = Bio->DebugColor; Out.DominantName = Bio->BiomeName.IsEmpty() ? FText::FromName(Bio->GetFName()) : Bio->BiomeName; // Effective list = the biome's decorations, or the strate's when the biome has none // (this is exactly what the per-column scatter falls back to). 0 ⇒ empty band. Out.DominantDecorationCount = (Bio->Decorations.Num() > 0) ? Bio->Decorations.Num() : Def->Decorations.Num(); } } } if (Ctx.Biomes.IsValidIndex(S.NeighborIndex)) { const int32 NbStrateIdx = Ctx.Biomes[S.NeighborIndex].Index; if (Def && Def->Biomes.IsValidIndex(NbStrateIdx)) { Out.NeighborBiome = Def->Biomes[NbStrateIdx]; } } } void UVoxelGenerator::GetBiomeMaterialAt(float WorldX, float WorldY, float WorldZ, int32& OutDominantPalette, int32& OutNeighborPalette, float& OutBlendWeight) const { OutDominantPalette = 0; OutNeighborPalette = 0; OutBlendWeight = 0.0f; if (!StrateManager) return; const FIntVector ChunkCoord( FMath::FloorToInt(WorldX / CHUNK_SIZE), FMath::FloorToInt(WorldY / CHUNK_SIZE), FMath::FloorToInt(WorldZ / CHUNK_SIZE)); // Per-chunk biome context cache, mirroring GetDensityAt: mesher vertices cluster by chunk, // so the (cheap) flatten + (noise-heavy, box-validated) grid stay warm across a tile. thread_local FIntVector BM_Chunk(INT32_MAX, INT32_MAX, INT32_MAX); thread_local FBiomeContext BM_Ctx; thread_local FChunkBiomeCache BM_Cache; thread_local uint32 BM_Version = 0xFFFFFFFFu; // AUDIT C2 thread_local uint64 BM_OwnerId = 0; thread_local uint64 BM_ManagerLifetimeId = 0; const uint32 BM_LayoutVersion = StrateManager->GetLayoutVersion(); const uint64 BM_CurrentOwnerId = DensityCacheOwnerId; const uint64 BM_CurrentManagerLifetimeId = StrateManager->GetCacheLifetimeId(); if (ChunkCoord != BM_Chunk || BM_LayoutVersion != BM_Version || BM_CurrentOwnerId != BM_OwnerId || BM_CurrentManagerLifetimeId != BM_ManagerLifetimeId) { if (BM_LayoutVersion != BM_Version || BM_CurrentOwnerId != BM_OwnerId || BM_CurrentManagerLifetimeId != BM_ManagerLifetimeId) { BM_Cache.Invalidate(); } BM_Version = BM_LayoutVersion; BM_OwnerId = BM_CurrentOwnerId; BM_ManagerLifetimeId = BM_CurrentManagerLifetimeId; BM_Chunk = ChunkCoord; BM_Ctx = StrateManager->GetBiomeContextForChunk(ChunkCoord); } if (!BM_Ctx.IsValid()) return; // strate has no biomes → default palette const FBiomeSample S = ResolveBiomeSampleAt(WorldX, WorldY, ChunkCoord.Z, BM_Ctx, BM_Cache); if (BM_Ctx.Biomes.IsValidIndex(S.DominantIndex)) { OutDominantPalette = BM_Ctx.Biomes[S.DominantIndex].MaterialPaletteIndex; // Neighbour defaults to the dominant so an interior vertex blends to itself (no seam). OutNeighborPalette = BM_Ctx.Biomes.IsValidIndex(S.NeighborIndex) ? BM_Ctx.Biomes[S.NeighborIndex].MaterialPaletteIndex : OutDominantPalette; OutBlendWeight = S.NeighborWeight; // 0 inside a cell → ~0.5 at the border } }