// VoxelDensityOpStack.cpp // Les opérateurs concrets : les décompositions d'archétypes + le post-traitement structurel. // The concrete operators: the archetype decompositions + the structural post-process. // // Ces opérateurs génèrent chaque strate du jeu (voir l'en-tête de VoxelDensityOpStack.h). // These operators generate every strate in the game (see the VoxelDensityOpStack.h header). // // Convention de signe INTERNE : positif = solide ; la pile négative une fois pour le MC. // Internal sign convention: positive = solid; the stack negates once for MC. #include "VoxelForgeCore/ThreadLocalStorage.h" #include "VoxelDensityOpStack.h" #include "VoxelStrateComposer.h" #include "VoxelDensityPrimitives.h" // VF_ApplyOriginSpine / seals / PassageCarving #include "VoxelDensityAblation.h" // fingerprinted development-only stage measurements #include "VoxelCaveMorphology.h" // VoxelSDF::Capsule, VoxelHash #include "VoxelGenerator.h" // VoxelGenLOD::Eff #include "VoxelHeightOp.h" // FVoxelHeightStack — SurfaceWorld's two height stacks #include "VoxelNoise.h" // VoxelNoise::FBM #include "VoxelWormField.h" #include "VoxelStrateDefinition.h" // TerrainOperations — le pool que BuildChunkCache tire par salle #include "VoxelTerrainOpDefinition.h" // ApplyTo — l'override d'op PAR SALLE (étape C1) #include "VoxelStrateManager.h" // EvaluateModifierSDF / AnyPassageNearBox #include "VoxelTypes.h" // SmoothStep01, VOXEL_NOISE_SCALE #include "VoxelStats.h" #include // l'id d'instance non recyclé du mémo de colonne #include "HAL/CriticalSection.h" #include "HAL/Event.h" #include "HAL/PlatformTime.h" #include "HAL/PlatformProcess.h" #include "Misc/ScopeLock.h" namespace { /** * Concrete block thunks use the scalar body with the concrete operator type visible to the * compiler. This is deliberately not a second numerical implementation: the scalar Eval is * still the single source of truth, but the op-major caller pays one virtual dispatch for the * block and the inner calls can inline/devirtualize. Operators with a block-native kernel can * replace this helper later without changing the stack contract. */ template FORCEINLINE void VF_EvalBlockByScalar(const TOp& Op, const FVoxelOpBlock& Block) { if (Block.Samples == nullptr || Block.Step <= 0 || Block.SizeX <= 0 || Block.SizeY <= 0 || Block.SizeZ <= 0) { return; } for (int32 Z = 0; Z < Block.SizeZ; ++Z) { for (int32 Y = 0; Y < Block.SizeY; ++Y) { for (int32 X = 0; X < Block.SizeX; ++X) { FVoxelOpSample& Sample = Block.At(X, Y, Z); Op.PrepareBlockSample(Sample); Op.Eval( static_cast(Block.OriginVoxels.X + X * Block.Step), static_cast(Block.OriginVoxels.Y + Y * Block.Step), static_cast(Block.OriginVoxels.Z + Z * Block.Step), Sample); } } } } /** * BORNE **PROUVABLE** DE `|Perlin3D|`, ET ELLE N'EST PAS 1.0. * * L'en-tête de `VoxelNoise::Perlin3D` annonce « ~[-1,1] (typiquement [-0.7,0.7]) ». Le `~` * est un aveu : c'est une observation, pas un théorème, et un verdict de boîte fondé sur une * observation est exactement le genre de trou que ce fichier passe son temps à éviter. * * Ce qui EST démontrable, en lisant `GradDot` : il rend `ru + rv` où `ru` et `rv` sont des * composantes de l'offset fractionnaire, donc chacune dans `[-1, 1]` ⇒ `|GradDot| ≤ 2`. La * valeur finale est une interpolation trilinéaire de huit `GradDot`, et une interpolation * convexe ne sort jamais de l'enveloppe de ses entrées ⇒ `|Perlin3D| ≤ 2`. (La vraie borne * de Perlin 3D est `√3/2 ≈ 0.87` ; on ne s'appuie pas dessus, elle dépend du jeu de * gradients.) Se tromper ici coûte une boîte de recherche un peu plus large, jamais un * verdict faux : plus large ⇒ SUR-ensemble de primitives ⇒ `Identity` plus rare. * * ⚠️⚠️ **CETTE CONSTANTE EST LE TERME DOMINANT DE TOUTE LA FONCTION.** À lire avant d'y * retoucher. * * La dilatation vaut `CaveWarpStrength · VOXEL_NOISE_SCALE · CETTE BORNE`. Avec les défauts * (`CaveWarpStrength = 8`, `SCALE = 1.25`), une borne de 2.0 vaut **20 voxels** — appliquée * des deux côtés de chaque axe d'une tuile de **10 voxels**, soit une boîte de requête de * 50 voxels, **125× le volume de la tuile** : `BoxHalfDiag` est alors dominé par cette * dilatation et non par la géométrie, et tout resserrement ailleurs ne gagne presque rien. * * ⚠️ LE RESTE DU PLUGIN EST MOINS PRUDENT : `BuildChunkCache` est appelée avec * `Expansion = CaveWarpStrength + 2` (ici comme dans `GetDensityWithParams`), ce qui suppose * `|Perlin3D| · SCALE ≤ CaveWarpStrength`, donc `|Perlin3D| ≤ 0.8`. Le code de production * parie déjà là-dessus. 2.0 serait 2,5× plus conservateur que l'hypothèse dont dépend déjà la * correction du cache. * * LA BORNE 1.5, DÉMONTRÉE (et non observée) : * 1. `GradDot` rend `±u ± v` où `u` et `v` sont deux composantes **distinctes** de l'offset * du coin — vérifié sur les quatre branches du `switch` de hash, pas supposé. * 2. Pour l'axe x : les coins à `i=0` portent le poids `(1−su)` et l'offset `fx`, ceux à * `i=1` le poids `su` et l'offset `1−fx`. Donc `Σ_c w_c·|dx_c| = (1−su)·fx + su·(1−fx)`, * dont le maximum sur `[0,1]` vaut **0.5** (atteint en `fx = 0.5`, où `su = 0.5` ; * 0.302 en 0.25 comme en 0.75). * 3. `|Perlin| ≤ Σ_c w_c(|a_c| + |b_c|) ≤ S_x + S_y + S_z ≤ 3 × 0.5 = 1.5.` * (Le vrai maximum est plus bas encore — seuls DEUX axes apparaissent par coin — mais 1.5 * est la borne qui se démontre sans analyse de cas sur les hash. `√3/2 ≈ 0.87`, la borne * classique de Perlin 3D, dépend du jeu de gradients : on ne s'appuie pas dessus.) * * This bound is the dominant term of the whole function: 2.0 would inflate a 10-voxel tile * into a 50-voxel query box (125x the volume). The rest of the plugin already assumes * |Perlin3D| <= 0.8 (BuildChunkCache's Expansion = CaveWarpStrength + 2). 1.5 is PROVED * above from GradDot's two-distinct-axes form and the per-axis weighted bound of 0.5. */ static constexpr float VF_PerlinAbsBound = 1.5f; struct FVFInterval { float Min = 0.0f; float Max = 0.0f; }; FORCEINLINE FVFInterval VF_AddInterval(const FVFInterval& A, const FVFInterval& B) { return {A.Min + B.Min, A.Max + B.Max}; } FORCEINLINE FVFInterval VF_NegateInterval(const FVFInterval& A) { return {-A.Max, -A.Min}; } FORCEINLINE FVFInterval VF_MultiplyNonNegativeInterval( const FVFInterval& A, const FVFInterval& B) { const float P0 = A.Min * B.Min; const float P1 = A.Min * B.Max; const float P2 = A.Max * B.Min; const float P3 = A.Max * B.Max; return {FMath::Min(FMath::Min(P0, P1), FMath::Min(P2, P3)), FMath::Max(FMath::Max(P0, P1), FMath::Max(P2, P3))}; } /** Exact interval for GradDot on one subdivision of one Perlin cell. */ FORCEINLINE FVFInterval VF_GradDotInterval( uint32 Hash, const FVFInterval& X, const FVFInterval& Y, const FVFInterval& Z) { const uint32 H = Hash & 15u; const FVFInterval* Axes[3] = {&X, &Y, &Z}; const FVFInterval& U = *Axes[(H & 8u) == 0u ? 0 : 1]; int32 VAxis = 2; if ((H & 12u) == 0u) { VAxis = 1; } else if ((H & 13u) == 12u) { VAxis = 0; } const FVFInterval SignedU = (H & 1u) == 0u ? U : VF_NegateInterval(U); const FVFInterval SignedV = (H & 2u) == 0u ? *Axes[VAxis] : VF_NegateInterval(*Axes[VAxis]); return VF_AddInterval(SignedU, SignedV); } /** * Conservative local envelope for the exact scalar Perlin3D implementation. * * Perlin3D is a trilinear blend of eight linear GradDot values in each integer cell. We split * every intersected cell into two pieces per axis and interval-evaluate the same fade/lerp * formula. The result is therefore a proof over the whole finite box, not a sample-based * guess. Large/invalid boxes fall back to the already-proven global envelope. */ FORCEINLINE float VF_PerlinAbsBoundOverBox( const FVector3f& InMin, const FVector3f& InMax) { if (!VoxelMath::IsFinite(InMin.X) || !VoxelMath::IsFinite(InMin.Y) || !VoxelMath::IsFinite(InMin.Z) || !VoxelMath::IsFinite(InMax.X) || !VoxelMath::IsFinite(InMax.Y) || !VoxelMath::IsFinite(InMax.Z) || InMin.X > InMax.X || InMin.Y > InMax.Y || InMin.Z > InMax.Z || FMath::Abs(InMin.X) > 1000000.0f || FMath::Abs(InMax.X) > 1000000.0f || FMath::Abs(InMin.Y) > 1000000.0f || FMath::Abs(InMax.Y) > 1000000.0f || FMath::Abs(InMin.Z) > 1000000.0f || FMath::Abs(InMax.Z) > 1000000.0f) { return VF_PerlinAbsBound; } const int32 X0 = FMath::FloorToInt(InMin.X); const int32 Y0 = FMath::FloorToInt(InMin.Y); const int32 Z0 = FMath::FloorToInt(InMin.Z); const int32 X1 = FMath::FloorToInt(InMax.X); const int32 Y1 = FMath::FloorToInt(InMax.Y); const int32 Z1 = FMath::FloorToInt(InMax.Z); const int64 CellCount = ((int64)X1 - X0 + 1) * ((int64)Y1 - Y0 + 1) * ((int64)Z1 - Z0 + 1); if (X1 < X0 || Y1 < Y0 || Z1 < Z0 || CellCount <= 0 || CellCount > 64) { return VF_PerlinAbsBound; } // The interval remains a proof for every subdivision size: each cell piece is evaluated // with interval arithmetic, so a coarser partition can only widen the envelope. Eight // pieces made a 32-voxel LOD0 root pay hundreds of thousands of tiny interval operations // before the graph proof even started. Four pieces are still bounded, but retain enough // finite-domain tightness at the 4-voxel refinement tail to discharge a proof without an // exact lattice walk. If the bound cannot discharge the proof, the caller returns Mixed // and the normal mesher remains the conservative fallback. constexpr int32 Subdivisions = 4; float Bound = 0.0f; for (int32 CellZ = Z0; CellZ <= Z1; ++CellZ) for (int32 CellY = Y0; CellY <= Y1; ++CellY) for (int32 CellX = X0; CellX <= X1; ++CellX) { const float TX0 = FMath::Clamp(InMin.X - (float)CellX, 0.0f, 1.0f); const float TY0 = FMath::Clamp(InMin.Y - (float)CellY, 0.0f, 1.0f); const float TZ0 = FMath::Clamp(InMin.Z - (float)CellZ, 0.0f, 1.0f); const float TX1 = FMath::Clamp(InMax.X - (float)CellX, 0.0f, 1.0f); const float TY1 = FMath::Clamp(InMax.Y - (float)CellY, 0.0f, 1.0f); const float TZ1 = FMath::Clamp(InMax.Z - (float)CellZ, 0.0f, 1.0f); for (int32 SZ = 0; SZ < Subdivisions; ++SZ) for (int32 SY = 0; SY < Subdivisions; ++SY) for (int32 SX = 0; SX < Subdivisions; ++SX) { const float AX = TX0 + (TX1 - TX0) * (float)SX / Subdivisions; const float BX = TX0 + (TX1 - TX0) * (float)(SX + 1) / Subdivisions; const float AY = TY0 + (TY1 - TY0) * (float)SY / Subdivisions; const float BY = TY0 + (TY1 - TY0) * (float)(SY + 1) / Subdivisions; const float AZ = TZ0 + (TZ1 - TZ0) * (float)SZ / Subdivisions; const float BZ = TZ0 + (TZ1 - TZ0) * (float)(SZ + 1) / Subdivisions; const float FX0 = VoxelNoise::Detail::Fade(AX); const float FX1 = VoxelNoise::Detail::Fade(BX); const float FY0 = VoxelNoise::Detail::Fade(AY); const float FY1 = VoxelNoise::Detail::Fade(BY); const float FZ0 = VoxelNoise::Detail::Fade(AZ); const float FZ1 = VoxelNoise::Detail::Fade(BZ); const float WXMin[2] = {1.0f - FX1, FX0}; const float WXMax[2] = {1.0f - FX0, FX1}; const float WYMin[2] = {1.0f - FY1, FY0}; const float WYMax[2] = {1.0f - FY0, FY1}; const float WZMin[2] = {1.0f - FZ1, FZ0}; const float WZMax[2] = {1.0f - FZ0, FZ1}; float ValueMin = 0.0f; float ValueMax = 0.0f; for (int32 CZ = 0; CZ < 2; ++CZ) for (int32 CY = 0; CY < 2; ++CY) for (int32 CX = 0; CX < 2; ++CX) { const FVFInterval Grad = VF_GradDotInterval( VoxelNoise::Detail::HashCorner(CellX + CX, CellY + CY, CellZ + CZ), {AX - (float)CX, BX - (float)CX}, {AY - (float)CY, BY - (float)CY}, {AZ - (float)CZ, BZ - (float)CZ}); const FVFInterval Weight = VF_MultiplyNonNegativeInterval( {WXMin[CX], WXMax[CX]}, VF_MultiplyNonNegativeInterval( {WYMin[CY], WYMax[CY]}, {WZMin[CZ], WZMax[CZ]})); const FVFInterval Contribution = VF_MultiplyNonNegativeInterval(Weight, Grad); ValueMin += Contribution.Min; ValueMax += Contribution.Max; } if (!VoxelMath::IsFinite(ValueMin) || !VoxelMath::IsFinite(ValueMax)) { return VF_PerlinAbsBound; } Bound = FMath::Max(Bound, FMath::Max(FMath::Abs(ValueMin), FMath::Abs(ValueMax))); } } // Cover the finite-precision rounding in the nested float lerps. If the interval proof is // looser than the global theorem, retaining the theorem is both cheaper and safer. Bound += 0.002f; return FMath::Min(FMath::Max(Bound, 0.0f), VF_PerlinAbsBound); } FORCEINLINE float VF_PerlinAbsBoundOverWorldBox( float WorldMinX, float WorldMaxX, float WorldMinY, float WorldMaxY, float EffectiveMinZ, float EffectiveMaxZ, float Frequency, float OffsetX, float OffsetY, float OffsetZ) { const float X0 = WorldMinX * Frequency + OffsetX; const float X1 = WorldMaxX * Frequency + OffsetX; const float Y0 = WorldMinY * Frequency + OffsetY; const float Y1 = WorldMaxY * Frequency + OffsetY; const float Z0 = EffectiveMinZ * Frequency + OffsetZ; const float Z1 = EffectiveMaxZ * Frequency + OffsetZ; return VF_PerlinAbsBoundOverBox( FVector3f(FMath::Min(X0, X1), FMath::Min(Y0, Y1), FMath::Min(Z0, Z1)), FVector3f(FMath::Max(X0, X1), FMath::Max(Y0, Y1), FMath::Max(Z0, Z1))); } // Cellular3D returns 2*(F2-F1)-1 without a clamp. Every searched feature point is at most // sqrt(12) from the query's unit cell, so F2-F1 <= sqrt(12) and the positive supremum is // 2*sqrt(12)-1 = 5.9282... . Round that up for float evaluation instead of trusting the // "~[-1,1]" comment in the noise implementation (which is an observation, not a proof). static constexpr float VF_CellularAbsBound = 6.0f; /** La même enveloppe que `FractalNoise3D` de VoxelGenerator.cpp (qui y est `static`, donc * invisible ici). Les coordonnées de bruit restent en float : FVector est double dans UE5 * et son aller-retour ne servait qu'à conserver l'ancien arrondi, exigence abandonnée. */ FORCEINLINE float HFractal3D(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); } /** Idem pour `RidgedNoise3D` (également `static` dans VoxelGenerator.cpp). */ FORCEINLINE float HRidged3D(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); } //========================================================================= // LE GATE `bNearCaveSurface` — ÉTAPE B //========================================================================= // ⚠️ DÉCISION DE L'ÉTAPE B5, ÉCRITE ICI PARCE QUE C'EST LE POINT OÙ ELLE SE LIT. // Dans l'original, les douze modificateurs de détail vivent dans UN SEUL `if (bNearCaveSurface)`. // Deux façons de porter ça : (a) un opérateur « conteneur » qui enveloppe ses enfants, (b) le // même early-out répété dans chaque opérateur. **C'est (b), délibérément :** // // • La pile est une LISTE PLATE, et `FVoxelOpStack::ClassifyBox` plie les opérateurs un par un. // Un conteneur devrait replier ses enfants lui-même — donc reproduire `VF_FoldOp` — et ses // enfants deviendraient invisibles au pliage. On paierait une abstraction pour en casser une. // • Un opérateur qui n'existe QUE dans un conteneur n'est pas composable, donc pas transposable // en asset (Phase 3). Le motif « chaque op teste son propre gate » est déjà celui de // `FSdfRoughnessMod` (`InOut.Sdf >= ApplyWithin`) et de `FShaftLedgeMod`. // • Le gate n'est de toute façon PAS uniforme : chaque modificateur a EN PLUS sa propre fenêtre // (`RoughnessDepth`, `TerraceRange`, `LineRange`…). Le gate partagé n'est qu'un early-out // commun, pas la condition réelle de chacun. // // ⚠️ CE QUE ÇA COÛTE, dit franchement : l'original teste UNE fois et saute les douze ; la pile // teste douze fois. Douze comparaisons flottantes parfaitement prédites par voxel de roc profond // — mesurable, mais c'est exactement le genre de chose que `AUDIT §C10` dit de MESURER avant // d'optimiser. C'est un poste de perf connu, pas à « corriger » à l'aveugle. // // STAGE B5 DECISION: repeated early-out in each op, NOT a scoping container — the stack is a flat // list that ClassifyBox folds op by op, and an op that only exists inside a container is not // composable. Cost stated honestly: twelve predictable compares instead of one branch. FORCEINLINE bool VF_NearCaveSurface(float Sdf, float SDFBlendRadius) { // Transcrit tel quel, ordre des comparaisons compris : // const float DetailThreshold = Params.SDFBlendRadius * 3.0f; // const bool bNearCaveSurface = (CaveSDF < DetailThreshold) && (CaveSDF < FLT_MAX); const float DetailThreshold = SDFBlendRadius * 3.0f; return (Sdf < DetailThreshold) && (Sdf < FLT_MAX); } /** Lower bound for the distance between two axis-aligned boxes. The segment's AABB contains * the segment, so this is a conservative lower bound for point-to-segment distance. */ FORCEINLINE float VF_DistanceBetweenBoxes(const FVector& AMin, const FVector& AMax, const FVector& BMin, const FVector& BMax) { const float DX = FMath::Max3((float)(AMin.X - BMax.X), (float)(BMin.X - AMax.X), 0.0f); const float DY = FMath::Max3((float)(AMin.Y - BMax.Y), (float)(BMin.Y - AMax.Y), 0.0f); const float DZ = FMath::Max3((float)(AMin.Z - BMax.Z), (float)(BMin.Z - AMax.Z), 0.0f); return FMath::Sqrt(DX * DX + DY * DY + DZ * DZ); } FORCEINLINE float VF_DistanceBoxToPointXY(const FBox& Box, float X, float Y) { const float DX = FMath::Max3((float)(Box.Min.X - X), 0.0f, (float)(X - Box.Max.X)); const float DY = FMath::Max3((float)(Box.Min.Y - Y), 0.0f, (float)(Y - Box.Max.Y)); return FMath::Sqrt(DX * DX + DY * DY); } /** Distance from the nearest exact lattice coordinate in one axis to a point. The continuous * box proof uses the interval [BoxMin, BoxMax]; the lattice proof uses only * Origin + integer * Step values inside that interval. Extra is a per-axis displacement * envelope (the cave warp); subtracting it keeps the result conservative. */ FORCEINLINE float VF_LatticeAxisDistanceToPoint( float BoxMin, float BoxMax, float Origin, float Step, float Target, float Extra) { if (!(Step > 0.0f) || !VoxelMath::IsFinite(BoxMin) || !VoxelMath::IsFinite(BoxMax) || !VoxelMath::IsFinite(Origin) || !VoxelMath::IsFinite(Target) || BoxMin > BoxMax) { return FLT_MAX; } const int32 GMin = FMath::CeilToInt((BoxMin - Origin) / Step - 1.0e-4f); const int32 GMax = FMath::FloorToInt((BoxMax - Origin) / Step + 1.0e-4f); if (GMin > GMax) { return FLT_MAX; } const int32 Near = FMath::FloorToInt((Target - Origin) / Step); float Distance = FLT_MAX; auto Consider = [&](int32 G) { G = FMath::Clamp(G, GMin, GMax); Distance = FMath::Min(Distance, FMath::Abs((Origin + static_cast(G) * Step) - Target)); }; Consider(GMin); Consider(GMax); Consider(Near); Consider(Near + 1); return FMath::Max(0.0f, Distance - FMath::Max(Extra, 0.0f)); } /** Same discrete distance, but to an axis-aligned interval. */ FORCEINLINE float VF_LatticeAxisDistanceToInterval( float BoxMin, float BoxMax, float Origin, float Step, float TargetMin, float TargetMax, float Extra) { if (!(Step > 0.0f) || !VoxelMath::IsFinite(TargetMin) || !VoxelMath::IsFinite(TargetMax) || TargetMin > TargetMax) { return FLT_MAX; } const int32 GMin = FMath::CeilToInt((BoxMin - Origin) / Step - 1.0e-4f); const int32 GMax = FMath::FloorToInt((BoxMax - Origin) / Step + 1.0e-4f); if (GMin > GMax) { return FLT_MAX; } const int32 NearMin = FMath::FloorToInt((TargetMin - Origin) / Step); const int32 NearMax = FMath::FloorToInt((TargetMax - Origin) / Step); float Distance = FLT_MAX; auto Consider = [&](int32 G) { G = FMath::Clamp(G, GMin, GMax); const float Value = Origin + static_cast(G) * Step; const float Delta = Value < TargetMin ? TargetMin - Value : Value > TargetMax ? Value - TargetMax : 0.0f; Distance = FMath::Min(Distance, Delta); }; Consider(GMin); Consider(GMax); Consider(NearMin); Consider(NearMin + 1); Consider(NearMax); Consider(NearMax + 1); return FMath::Max(0.0f, Distance - FMath::Max(Extra, 0.0f)); } FORCEINLINE float VF_SaturatingAdd(float A, float B) { if (!VoxelMath::IsFinite(A) || !VoxelMath::IsFinite(B)) { return FLT_MAX; } if (B > 0.0f && A > FLT_MAX - B) { return FLT_MAX; } if (B < 0.0f && A < -FLT_MAX - B) { return -FLT_MAX; } return A + B; } //========================================================================= // RÔLE 1 — SOURCE : CHAMP CONSTANT / CONSTANT FIELD (roc ET vide) //========================================================================= // `float Density = Params.BaseDensity; // start solid` — la première ligne de TunnelNetwork, // de Maze ET de VerticalShafts. Et `float Density = -Params.BaseDensity; // open air (void)` — // la première ligne de FloatingIslands. **C'est le MÊME opérateur au signe près**, et le signe // n'est pas un détail : il décide du verdict de boîte de départ (AllSolid contre AllAir), donc // de ce que la strate saura sauter. // // Quatre archétypes, une ligne, un opérateur. Deux fabriques (`MakeConstantRockSource` / // `MakeConstantVoidSource`) parce que « roc » et « vide » sont ce que l'auteur veut DIRE ; la // classe, elle, n'a aucune raison d'exister en deux exemplaires. // // One operator, two factories: rock and void are the same constant field with opposite signs, // and the sign is what decides the starting box verdict (AllSolid vs AllAir). class FConstantFieldSource final : public IVoxelDensityOp { public: explicit FConstantFieldSource(float InValue) : Value(InValue) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::None; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } bool IsAdditive() const override { return false; } void PrepareChunk(const FVoxelOpContext&) override {} bool IsXYPure() const override { return true; } // constant ⇒ trivialement sans Z void Eval(float, float, float, FVoxelOpSample& InOut) const override { InOut.Density = Value; // Replace : racine de pile, ignore l'entrée } // Exact et gratuit, dans les DEUX sens (convention interne : positif = solide). // Positif ⇒ AllSolid : c'est ce qui donne aux strates de grotte une hypothèse de départ // qu'elles n'ont jamais eue. Négatif ⇒ AllAir : c'est ce qui rend une strate d'îles // flottantes — un grand vide surtout vide — sautable là où aucune île n'arrive. EVoxelTileClass ClassifyBox(const FBox&, const FVoxelOpContext&) const override { if (Value > 0.0f) { return EVoxelTileClass::AllSolid; } if (Value < 0.0f) { return EVoxelTileClass::AllAir; } return EVoxelTileClass::Mixed; // exactement 0 : le mesher le compte du côté AIR, // mais un champ nul n'est pas une hypothèse utile. } EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override { return EVoxelOpEffect::Both; // jamais atteint sauf Value == 0 : ClassifyBox répond avant } /** ⚠️ LA MOITIÉ MANQUANTE DU PLIAGE NUMÉRIQUE (`OPSTACK-DECOMPOSITION §0.2`). * `MaxCarveOverBox` dit ce qu'un opérateur peut RETIRER ; ceci dit ce qu'il y avait à * retirer. Un champ constant est le seul opérateur du plugin qui connaisse cette marge * EXACTEMENT : la densité vaut `Value` partout, donc la marge est `|Value|`. Sans elle la * soustraction n'a pas de premier terme et tout carve borné tue quand même l'hypothèse. */ float ForcedMarginOverBox(const FBox&, const FVoxelOpContext&) const override { return FMath::Abs(Value); } const TCHAR* DebugName() const override { return TEXT("ConstantFieldSource"); } void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } private: float Value; }; //======================================================================= // TIER 4b — GENERIC ROLLED SOURCE/MODIFIER PRIMITIVES //======================================================================= // These two small primitives keep the non-room channel space large enough for the promised // k=4..8 draw. They are deliberately bounded: their box contracts are amplitude proofs, not // guesses. They do not carry any room/shaft/surface pointer, so the resource graph correctly // treats them as legal after every SDF shape source. class FNoiseRibbonSource final : public IVoxelDensityOp { public: FNoiseRibbonSource(const FMazeGenerationParams& InP, int32 InSeed) : CellSize(FMath::Max(InP.CellSize, 8.0f)) , CorridorRadius(FMath::Max(InP.CorridorRadius, 0.5f)) , SeedU(static_cast(InSeed) ^ 0x5249626Eu) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::None; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Sdf; } bool IsAdditive() const override { return false; } void PrepareChunk(const FVoxelOpContext&) override {} void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { const float Frequency = 1.0f / CellSize; const FVector3f NoisePos( WorldX * Frequency + VoxelHash::SeedOffset(SeedU, 3.17f), WorldY * Frequency + VoxelHash::SeedOffset(SeedU, 7.31f), WorldZ * Frequency + VoxelHash::SeedOffset(SeedU, 11.47f)); const float Ribbon = FMath::Clamp( FMath::Abs(VoxelNoise::FBM(NoisePos.X, NoisePos.Y, NoisePos.Z, VoxelGenLOD::Eff(3), 2.0f, 0.5f)) * VOXEL_NOISE_SCALE, 0.0f, 2.0f); InOut.Sdf = Ribbon * CellSize - CorridorRadius; } EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override { return EVoxelOpEffect::Identity; } void PropagateSdfOverBox(FVoxelBoxSdfInterval& InOut, const FBox&, const FVoxelOpContext&) const override { if (!VoxelMath::IsFinite(CellSize) || !VoxelMath::IsFinite(CorridorRadius) || CellSize <= 0.0f || CorridorRadius < 0.0f) { InOut.SetUnknown(); return; } // Eval clamps the FBM envelope to [0,2]. This interval is exact enough for safety // and independent of the queried box, so it never claims a false empty region. InOut.Set(-CorridorRadius, 2.0f * CellSize - CorridorRadius); } const TCHAR* DebugName() const override { return TEXT("NoiseRibbonSource"); } private: float CellSize; float CorridorRadius; uint32 SeedU; }; class FDensityNoiseMod final : public IVoxelDensityOp { public: FDensityNoiseMod(float InStrength, float InFrequency, int32 InOctaves, int32 InSeed, bool bInFill) : Strength(FMath::Max(InStrength, 0.0f)) , Frequency(FMath::Max(InFrequency, 0.0001f)) , Octaves(FMath::Clamp(InOctaves, 1, 8)) , SeedU(static_cast(InSeed) ^ (bInFill ? 0x46696C6Cu : 0x43617276u)) , bFill(bInFill) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } bool IsAdditive() const override { return true; } void PrepareChunk(const FVoxelOpContext&) override {} void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { if (!(Strength > 0.0f)) { return; } const FVector3f NoisePos( WorldX * Frequency + VoxelHash::SeedOffset(SeedU, 13.2f), WorldY * Frequency + VoxelHash::SeedOffset(SeedU, 17.8f), WorldZ * Frequency + VoxelHash::SeedOffset(SeedU, 23.4f)); const float Noise01 = FMath::Clamp( VoxelNoise::FBM(NoisePos.X, NoisePos.Y, NoisePos.Z, VoxelGenLOD::Eff(Octaves), 2.0f, 0.5f) * 0.5f + 0.5f, 0.0f, 1.0f); const float Delta = Strength * Noise01; InOut.Density += bFill ? Delta : -Delta; } EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override { if (!(Strength > 0.0f)) { return EVoxelOpEffect::Identity; } return bFill ? EVoxelOpEffect::FillOnly : EVoxelOpEffect::CarveOnly; } float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override { return bFill ? 0.0f : Strength; } float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override { return bFill ? Strength : 0.0f; } const TCHAR* DebugName() const override { return bFill ? TEXT("DensityNoiseFillMod") : TEXT("DensityNoiseCarveMod"); } private: float Strength; float Frequency; int32 Octaves; uint32 SeedU; bool bFill; }; //========================================================================= // RÔLE 1 — SOURCE : COULOIRS SUR TREILLIS 3D / 3D LATTICE CORRIDORS //========================================================================= // Chaque nœud du treillis est au centre d'une cellule. Il choisit exactement un parent parmi // les axes qui le rapprochent de l'origine ; l'arête parent abaisse donc |X|+|Y|+|Z| de un. // Le résultat est un arbre couvrant de tout le treillis infini. Quelques arêtes supplémentaires // sont hashées comme des boucles, avec des probabilités plafonnées qui ne participent jamais à // la connectivité. Le couloir est une capsule. // // La règle est locale et seam-safe : les huit nœuds-enfants du halo {-1,0}³ suffisent à émettre // toutes les arêtes qui peuvent toucher la cellule évaluée. Chaque décision est une fonction // pure du nœud, de l'axe et de la seed ; il n'y a ni collect global ni dépendance à la fenêtre // de chunk. class FLatticeCorridorSource final : public IVoxelDensityOp { public: FLatticeCorridorSource(const FMazeGenerationParams& P, int32 Seed) : CellSize(FMath::Max(P.CellSize, 1.0f)) , CorridorRadius(FMath::Max(P.CorridorRadius, 0.5f)) , BranchProbability(P.BranchProbability) , Verticality(P.Verticality) , Salt((uint32)Seed ^ 0x4D617A65u) // 'Maze' {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Sdf; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Sdf; } bool IsAdditive() const override { return false; } void PrepareChunk(const FVoxelOpContext&) override {} void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { const int32 CX = FMath::FloorToInt(WorldX / CellSize); const int32 CY = FMath::FloorToInt(WorldY / CellSize); const int32 CZ = FMath::FloorToInt(WorldZ / CellSize); const TArray>& Edges = GetCellEdges(FIntVector(CX, CY, CZ)); const FVector Pos(WorldX, WorldY, WorldZ); float Sdf = FLT_MAX; for (const FEdge& E : Edges) { Sdf = FMath::Min(Sdf, VoxelSDF::Capsule(Pos, E.A, E.B, CorridorRadius)); } // Union de formes ⇒ MIN sur le canal SDF (voir la note de signe dans VoxelDensityOp.h : // « min » ici veut dire l'inverse de ce qu'il veut dire sur le canal densité). InOut.Sdf = FMath::Min(InOut.Sdf, Sdf); } // This source writes SDF only. The following converter decides whether that SDF carves; // the source itself has no density effect in isolation. EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override { return EVoxelOpEffect::Identity; } /** Exact cell coverage plus a geometric lower bound for every open edge. The upper bound * is FLT_MAX because a far point can retain an arbitrarily positive capsule distance. */ void PropagateSdfOverBox(FVoxelBoxSdfInterval& InOut, const FBox& VoxelBox, const FVoxelOpContext&) const override { const int32 CX0 = FMath::FloorToInt((float)VoxelBox.Min.X / CellSize); const int32 CY0 = FMath::FloorToInt((float)VoxelBox.Min.Y / CellSize); const int32 CZ0 = FMath::FloorToInt((float)VoxelBox.Min.Z / CellSize); const int32 CX1 = FMath::FloorToInt((float)VoxelBox.Max.X / CellSize); const int32 CY1 = FMath::FloorToInt((float)VoxelBox.Max.Y / CellSize); const int32 CZ1 = FMath::FloorToInt((float)VoxelBox.Max.Z / CellSize); constexpr int64 MaxCellsScanned = 64 * 64 * 64; const int64 CellCount = (int64)(CX1 - CX0 + 1) * (CY1 - CY0 + 1) * (CZ1 - CZ0 + 1); if (CellCount <= 0 || CellCount > MaxCellsScanned) { InOut.SetUnknown(); return; } float Lower = FLT_MAX; bool bAnyEdge = false; for (int32 cz = CZ0; cz <= CZ1; ++cz) for (int32 cy = CY0; cy <= CY1; ++cy) for (int32 cx = CX0; cx <= CX1; ++cx) { const TArray>& Edges = GetCellEdges(FIntVector(cx, cy, cz)); for (const FEdge& E : Edges) { bAnyEdge = true; const FVector SegmentMin( FMath::Min(E.A.X, E.B.X), FMath::Min(E.A.Y, E.B.Y), FMath::Min(E.A.Z, E.B.Z)); const FVector SegmentMax( FMath::Max(E.A.X, E.B.X), FMath::Max(E.A.Y, E.B.Y), FMath::Max(E.A.Z, E.B.Z)); Lower = FMath::Min(Lower, VF_DistanceBetweenBoxes(VoxelBox.Min, VoxelBox.Max, SegmentMin, SegmentMax) - CorridorRadius); } } FVoxelBoxSdfInterval Own; if (!bAnyEdge) { Own.Set(FLT_MAX, FLT_MAX); } else { Own.Set(Lower, FLT_MAX); } InOut.MinWith(Own); } private: struct FEdge { FVector A, B; }; FVector NodeCenter(int32 X, int32 Y, int32 Z) const { return FVector((X + 0.5f) * CellSize, (Y + 0.5f) * CellSize, (Z + 0.5f) * CellSize); } /** * Le cache par CELLULE. Il est `thread_local` et non * membre parce que la pile est PARTAGÉE entre workers en lecture — un membre mutable serait * une course. La reconstruction seule inspecte le petit voisinage ; Eval ne refait aucune * décision de parent ou de boucle. * * ⚠️ PHASE 3 : quand les opérateurs deviendront des assets partagés, il faudra un objet * d'état PAR WORKER plutôt que ce `thread_local` (qui est global à la fonction, donc partagé * entre DEUX piles Maze différentes sur le même thread — la clé le rattrape, mais au prix * d'un rebuild à chaque alternance). */ const TArray>& GetCellEdges(const FIntVector& Cell) const { thread_local TArray> MZ_Edges; thread_local FIntVector MZ_Cell(INT32_MAX, INT32_MAX, INT32_MAX); thread_local uint32 MZ_Seed = 0xFFFFFFFFu; thread_local float MZ_CS = -1.0f, MZ_Branch = -1.0f, MZ_Vert = -1.0f; if (Cell != MZ_Cell || Salt != MZ_Seed || CellSize != MZ_CS || BranchProbability != MZ_Branch || Verticality != MZ_Vert) { MZ_Cell = Cell; MZ_Seed = Salt; MZ_CS = CellSize; MZ_Branch = BranchProbability; MZ_Vert = Verticality; MZ_Edges.Reset(); // Canonical lower-node edges in {-1,0} per axis cover every corridor that can // reach this voxel's cell. IsOpenEdge checks both endpoints, including a +1 // node's parent choice, without needing a wider cache window. for (int32 dz = -1; dz <= 0; dz++) for (int32 dy = -1; dy <= 0; dy++) for (int32 dx = -1; dx <= 0; dx++) { const int32 nx = Cell.X + dx, ny = Cell.Y + dy, nz = Cell.Z + dz; const FVector A = NodeCenter(nx, ny, nz); if (VoxelMazeTopology::IsOpenEdge( nx, ny, nz, VoxelMazeTopology::EAxis::X, Salt, BranchProbability, Verticality)) { MZ_Edges.Add({ A, NodeCenter(nx + 1, ny, nz) }); } if (VoxelMazeTopology::IsOpenEdge( nx, ny, nz, VoxelMazeTopology::EAxis::Y, Salt, BranchProbability, Verticality)) { MZ_Edges.Add({ A, NodeCenter(nx, ny + 1, nz) }); } if (VoxelMazeTopology::IsOpenEdge( nx, ny, nz, VoxelMazeTopology::EAxis::Z, Salt, BranchProbability, Verticality)) { MZ_Edges.Add({ A, NodeCenter(nx, ny, nz + 1) }); } } } return MZ_Edges; } float CellSize, CorridorRadius, BranchProbability, Verticality; uint32 Salt; }; //========================================================================= // RÔLE 1 — SOURCE : DALLE / SLAB VOID (FlatPlain ET CrystalChamber) //========================================================================= // Surface de sol, surface de plafond, puis `Density = -min(distAuSol, distAuPlafond)`. // // DEUX archétypes, UN opérateur, sans le moindre branchement sur le type — CrystalChamber // n'est rien d'autre que FlatPlain avec un `CeilingRoughness` plus grand (OPSTACK-PLAN §4) : // la différence entre eux est un jeu de valeurs par défaut. // // Two archetypes, ONE op, with no branch on the type. // CrystalChamber IS FlatPlain with a bigger CeilingRoughness. // // XY-PUR depuis §3.1 (le terme en Z des deux bruits est parti). C'est ce qui rend // `ClassifyBox` exact plutôt qu'estimé — voir plus bas. class FSlabVoidSource final : public IVoxelDensityOp { public: FSlabVoidSource(const FSlabGenerationParams& P, int32 Seed) : SeedU((uint32)Seed) , FloorRoughness(P.FloorRoughness) , FloorFrequency(P.FloorRoughnessFrequency) , CeilRoughness(P.CeilingRoughness) , CeilFrequency(P.CeilingRoughnessFrequency) { const float StrateHeight = P.StrateTopWorldZ - P.StrateBottomWorldZ; FloorZ = P.StrateBottomWorldZ + StrateHeight * P.FloorRelativeHeight; CeilZ = P.StrateBottomWorldZ + StrateHeight * P.CeilingRelativeHeight; // FBM's proved supremum is 1.5, not the parameter's nominal [-1,1] label. Bounds must // cover every authored value, so never normalise the roughness parameter to repair a // bound: multiply the actual maximum noise envelope instead. FloorAmp = VOXEL_NOISE_SCALE * VF_PerlinAbsBound * FMath::Abs(FloorRoughness); CeilAmp = VOXEL_NOISE_SCALE * VF_PerlinAbsBound * FMath::Abs(CeilRoughness); } EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::None; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } bool IsAdditive() const override { return false; } void PrepareChunk(const FVoxelOpContext&) override {} // ⚠️⚠️ `false`, ET NE PAS LE PASSER À `true`. // // Le contrat de `IsXYPure` est « **`Eval`** ne dépend pas de Z » — pas « les surfaces ne // dépendent pas de Z ». Or `Eval` calcule `min(Z - sol, plafond - Z)` : il dépend de Z de // la façon la plus directe qui soit. §3.1 a rendu les SURFACES pures en XY ; la DENSITÉ, // elle, ne l'a jamais été et ne peut pas l'être — c'est une distance à une surface. // // Le jour où un cache de colonnes générique lit ce drapeau, un `true` ici ferait partager // UNE valeur de densité sur TOUTE la pile verticale de chunks — un monde silencieusement // faux, que `ValidateDeterminism` ne verrait pas parce qu'il échantillonne le long d'un // bord X. C'est exactement le piège que l'avertissement de `VoxelDensityOp.h` décrit. // // ⚠️ Must stay `false`. The contract is "**Eval** does not depend on Z", and Eval computes // min(Z - floor, ceil - Z). §3.1 made the SURFACES XY-pure; the DENSITY never was and // cannot be — it is a distance to a surface. A generic column cache reading `true` here // would share one density down the whole vertical chunk stack. // // C'est précisément cette distinction qui justifie l'espace-hauteur (`VoxelHeightOp.h`) : // ce qui est pur en XY, ce sont les HAUTEURS, et elles y sont dans un type qui n'a pas de Z. bool IsXYPure() const override { return false; } void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { const float FloorSurface = SurfaceFloor(WorldX, WorldY); const float CeilSurface = SurfaceCeil(WorldX, WorldY, FloorSurface); const float DistAboveFloor = WorldZ - FloorSurface; const float DistBelowCeil = CeilSurface - WorldZ; const float VoidField = FMath::Min(DistAboveFloor, DistBelowCeil); InOut.Density = -VoidField; // Replace : interne, positif = solide } //--------------------------------------------------------------------- // LE VERDICT QUE FLATPLAIN N'A JAMAIS EU //--------------------------------------------------------------------- // `ClassifyTile` ne prouve AUCUNE tuile pour les archétypes de grotte aujourd'hui. Ici la // preuve est immédiate et n'exige aucun échantillonnage : les deux surfaces vivent dans des // BANDES en Z dont on connaît les bornes exactes, donc une boîte entièrement sous la bande // du sol est solide, et une boîte entièrement entre les deux bandes est de l'air. // // ⚠️ Conservatif dans le bon sens : rendre `Mixed` ne coûte que du CPU, rendre le mauvais // verdict est un TROU. Toutes les comparaisons ci-dessous sont donc strictes et prennent le // pire cas des deux bruits. EVoxelTileClass ClassifyBox(const FBox& VoxelBox, const FVoxelOpContext&) const override { const float ZMin = (float)VoxelBox.Min.Z; const float ZMax = (float)VoxelBox.Max.Z; // Bornes de la surface de sol : FloorZ ± FloorAmp. const float FloorLo = FloorZ - FloorAmp; const float FloorHi = FloorZ + FloorAmp; // Bornes du plafond. `CeilNoise = |bruit| · rugosité` ∈ [0, CeilAmp] ⇒ la surface ne // peut que DESCENDRE depuis CeilZ… sauf que le clamp `Max(…, FloorSurface + 2)` peut la // remonter. Le majorant honnête est donc le max des deux possibilités. const float CeilLo = CeilZ - CeilAmp; const float CeilHi = FMath::Max(CeilZ, FloorHi + 2.0f); // Sous le sol le plus bas possible ⇒ distAuSol < 0 partout ⇒ densité > 0 ⇒ SOLIDE. if (ZMax < FloorLo) { return EVoxelTileClass::AllSolid; } // Au-dessus du plafond le plus haut possible ⇒ distAuPlafond < 0 ⇒ SOLIDE. if (ZMin > CeilHi) { return EVoxelTileClass::AllSolid; } // Strictement entre les deux bandes ⇒ les deux distances sont > 0 ⇒ densité < 0 ⇒ AIR. // (Les colonnes peuvent re-remplir cet air : c'est FGridColumnMod qui le déclare, en // rendant FillOnly quand une colonne atteint la boîte. Le pliage s'en charge.) if (ZMin > FloorHi && ZMax < CeilLo) { return EVoxelTileClass::AllAir; } return EVoxelTileClass::Mixed; } EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override { return EVoxelOpEffect::Both; // jamais atteint : ClassifyBox répond avant } private: // The surface coordinates stay in float; an FVector (double) round-trip is not part of // the output contract. float SurfaceFloor(float WorldX, float WorldY) const { if (FloorRoughness <= 0.0f) { return FloorZ; } const float FF = FloorFrequency; const FVector3f NoisePos(WorldX * FF + VoxelHash::SeedOffset(SeedU, 7.3f), WorldY * FF + VoxelHash::SeedOffset(SeedU, 11.1f), 0.0f); const float N = VoxelNoise::FBM(NoisePos.X, NoisePos.Y, NoisePos.Z, VoxelGenLOD::Eff(3), 2.0f, 0.5f) * VOXEL_NOISE_SCALE * FloorRoughness; return FloorZ + N; } float SurfaceCeil(float WorldX, float WorldY, float FloorSurface) const { float CeilNoise = 0.0f; if (CeilRoughness > 0.0f) { const float CF = CeilFrequency; const FVector3f NoisePos(WorldX * CF + VoxelHash::SeedOffset(SeedU, 17.3f) + 1000.0f, WorldY * CF + VoxelHash::SeedOffset(SeedU, 19.7f) + 2000.0f, 3000.0f); const float Raw = VoxelNoise::FBM(NoisePos.X, NoisePos.Y, NoisePos.Z, VoxelGenLOD::Eff(3), 2.0f, 0.5f) * VOXEL_NOISE_SCALE; // abs() ⇒ les formations ne pendent QUE vers le bas. CeilNoise = FMath::Abs(Raw) * CeilRoughness; } return FMath::Max(CeilZ - CeilNoise, FloorSurface + 2.0f); } uint32 SeedU; float FloorZ = 0.0f, CeilZ = 0.0f; float FloorRoughness, FloorFrequency; float CeilRoughness, CeilFrequency; float FloorAmp = 0.0f, CeilAmp = 0.0f; }; //========================================================================= // RÔLE 1 — SOURCE : COLONNE DE SURFACE / SURFACE COLUMN (SurfaceWorld) //========================================================================= // Le pont entre les deux espaces : consomme DEUX piles de hauteur (sol et voûte) et en fait une // densité : // // Density = TerrainZ - Z ← solide sous le sol // Density = max(Density, Z - CeilSurf) ← solide au-dessus de la voûte // // ⚠️ `IsXYPure()` est **false**, et la distinction est LE point de tout ce découpage : les // HAUTEURS sont pures en XY (elles vivent dans `VoxelHeightOp.h`, un type sans Z), la DENSITÉ // ne l'est pas et ne peut pas l'être — c'est une distance à une surface. Confondre les deux est // exactement le bug que `FSlabVoidSource` portait jusqu'à aujourd'hui. // // The bridge between the two spaces: consumes two HEIGHT stacks and turns them into density. // IsXYPure is false — the heights are XY-pure, the density is a distance to them and never can be. class FSurfaceColumnSource final : public IVoxelDensityOp { public: /** * @param PerBiome vide ⇒ pas de biomes, chemin d'origine inchangé. Non vide ⇒ le sol est * mélangé et le plafond sélectionné par `InField`. * @param InField **POSSÉDÉ** — délibérément, plutôt qu'un pointeur nu. L'adaptateur réel * pointe vers des `thread_local` du générateur ; lier sa durée de vie à * celle de la pile (elle-même `thread_local`, reconstruite au même moment) * rend la question de survie structurelle au lieu de la laisser à une * convention que le prochain lecteur devrait deviner. * OWNED on purpose rather than borrowed: tying its lifetime to the stack's * makes the survival question structural instead of conventional. */ FSurfaceColumnSource(const FSurfaceGenerationParams& InP, int32 Seed, const TArray& PerBiome, TUniquePtr InField) : P(InP), BiomeParams(PerBiome), Field(MoveTemp(InField)) { if (BiomeParams.Num() > 0) { // Chemin BIOMES : une pile complète par biome, sol mélangé / plafond sélectionné. TerrainStack.Add(VoxelHeightOps::MakeBiomeBlendHeightSource(BiomeParams, Seed, Field.Get())); CeilingStack.Add(VoxelHeightOps::MakeBiomeSelectCeilingSource(BiomeParams, Seed, Field.Get())); // Un champ structurel PAR BIOME : la pente de l'overhang doit venir du champ du // biome DOMINANT (l'original échantillonne `*PD`), pas d'un champ moyen. PerBiomeStructural.Reserve(BiomeParams.Num()); for (const FSurfaceGenerationParams& BP : BiomeParams) { const IVoxelHeightOp* Raw = nullptr; PerBiomeStructural.Add(VoxelHeightOps::MakeStructuralHeightSource(BP, Seed, &Raw)); } Structural = PerBiomeStructural.Num() > 0 ? PerBiomeStructural[0].Get() : nullptr; } else { BuildSingleBiome(InP, Seed); } // Identité unique et NON RECYCLÉE — la clé du mémo par colonne. `this` ne suffirait // pas : une pile détruite puis une autre allouée à la même adresse avec d'autres params // donnerait un faux positif silencieux. Un compteur qui ne redescend jamais l'interdit. // Assignée ICI et nulle part ailleurs : l'autre constructeur délègue à celui-ci. // A unique, never-recycled id — assigned here only; the other ctor delegates. static std::atomic NextId{ 1 }; InstanceId = NextId.fetch_add(1, std::memory_order_relaxed); // Départ sur l'identité D'INSTANCE, pas sur 0 : les entrées du mémo s'initialisent à // `Key = 0`, donc une clé nulle ferait FAUSSEMENT toucher le slot vierge en (0,0). // `PrepareChunk` remplacera ceci par la clé partagée de strate ; sans lui, on garde un // cache par instance — moins de partage, mais correct. Dégrader, jamais mentir. // Starting at the INSTANCE id rather than 0: slots initialise to Key = 0, so a zero key // would falsely hit the pristine slot at (0,0). PrepareChunk upgrades this to the shared // strate key; without it we simply cache per instance. Degrade, never lie. ColumnKey = InstanceId; // Empreinte des params qui déterminent une colonne. `FSurfaceGenerationParams` est du // POD pur (que des float/int/bool, vérifié : aucun TArray, FString ni pointeur), donc // un CRC mémoire ne peut pas produire de FAUX POSITIF — au pire du padding non // initialisé donne un faux NÉGATIF, c'est-à-dire un recalcul. Se tromper du côté qui // coûte du CPU plutôt que du côté qui rend une mauvaise colonne. // Pure POD (verified: no TArray/FString/pointer), so a memory CRC cannot produce a false // HIT; at worst padding causes a false miss, i.e. a recompute. Err toward CPU, not lies. ParamsFingerprint = FCrc::MemCrc32(&P, sizeof(P)); for (const FSurfaceGenerationParams& BP : BiomeParams) { ParamsFingerprint = VoxelHash::Mix(ParamsFingerprint ^ FCrc::MemCrc32(&BP, sizeof(BP))); } } void BuildSingleBiome(const FSurfaceGenerationParams& InP, int32 Seed) { // Construite à la main (pas via BuildSurfaceHeightStack) pour GARDER le pointeur vers la // source structurelle : l'overhang en a besoin, pour son gradient de pente comme pour // son ré-échantillonnage amont. Même dépendance que le cliff, même raison. TerrainStack.Add(VoxelHeightOps::MakeStructuralHeightSource(InP, Seed, &Structural)); TerrainStack.Add(VoxelHeightOps::MakeCliffHeightMod(InP, Structural)); TerrainStack.Add(VoxelHeightOps::MakeTerraceHeightMod(InP)); TerrainStack.Add(VoxelHeightOps::MakeLayerLineHeightMod(InP)); TerrainStack.Add(VoxelHeightOps::MakeBeachHeightMod(InP)); VoxelHeightOps::BuildSurfaceCeilingStack(CeilingStack, InP, Seed); } /** La colonne complète, exactement les cinq sorties de `ComputeSurfaceColumn`. * Le mémo est un LRU spatial de six boîtes à index direct, comme `GSurfColCache` : la * pile évalue tous les Z d'une colonne au même XY, donc l'overhang lit la MÊME colonne * que la source, par construction plutôt que par convention. * * The memo is a six-box spatial LRU with direct XY indexing, matching `GSurfColCache`. * Six boxes retain interleaved strate regions at the cost of roughly 0.79 MiB of TLS for * the five-float column payload plus one computed flag per cell, before compiler padding. */ struct FColumn { float TerrainZ, CeilSurf, OverhangAmp, DirX, DirY; }; const FColumn& GetColumn(float WorldX, float WorldY) const { // Même schéma éprouvé que `GSurfColCache` : six boîtes à index direct dans XY, chacune // avec un drapeau `Computed` par cellule et une clé uint64 exacte. Une tuile MC pleine // résolution demande 35×35 = 1225 colonnes (anneau de marge inclus) ; une boîte de // Dim×Dim, recentrée sur le premier échantillon, les garde toutes sans éviction. // Same proven scheme as `GSurfColCache`: six direct-indexed XY boxes, each with one // `Computed` flag per cell and an exact uint64 key. A full-resolution MC tile needs // 35×35 = 1225 columns including its margin ring; one Dim×Dim box holds that tile. struct FColumnBox { enum : int32 { Halo = CHUNK_SIZE + 8, Dim = 2 * Halo + 1 }; int32 BaseX = 0, BaseY = 0; uint64 Key = 0; // strate + layout + seed + ParamsFingerprint uint32 LastUse = 0; // LRU stamp bool bValid = false; FColumn Cols[Dim * Dim]; bool Computed[Dim * Dim]; }; struct FColumnCache { enum : int32 { NumBoxes = 6 }; FColumnBox Boxes[NumBoxes]; uint32 Clock = 0; // Hit exact : clé complète + couverture XY complète. En cas de miss, seul le // victim LRU est recentré et invalidé ; les cinq autres boîtes restent chaudes. // Exact hit: full key + full XY coverage. On a miss, only the LRU victim is // recentered and invalidated; the other five boxes remain warm. FColumnBox& Acquire(int32 IX, int32 IY, uint64 InColumnKey) { ++Clock; for (FColumnBox& B : Boxes) { if (B.bValid && B.Key == InColumnKey && IX >= B.BaseX && IX < B.BaseX + FColumnBox::Dim && IY >= B.BaseY && IY < B.BaseY + FColumnBox::Dim) { B.LastUse = Clock; return B; } } // Miss d'acquisition : évincer/recentrer une seule boîte, jamais tout le cache. // Acquisition miss: evict/recenter one box only, never the whole cache. FColumnBox* Victim = &Boxes[0]; for (FColumnBox& B : Boxes) { if (B.LastUse < Victim->LastUse) Victim = &B; } Victim->BaseX = IX - FColumnBox::Halo; Victim->BaseY = IY - FColumnBox::Halo; Victim->Key = InColumnKey; Victim->LastUse = Clock; Victim->bValid = true; FMemory::Memzero(Victim->Computed, sizeof(Victim->Computed)); return *Victim; } }; thread_local VoxelForgeCoreThreadLocal::FLazy Cache; FColumnCache& columnCache = Cache.Get(); thread_local FColumn DirectColumn = {}; // The production mesher and the exact-lattice classifier use integer XY. Fractional // XY is still valid for the public density/equivalence probes: compute it directly so // no integer cell can ever be returned for a different full (WorldX, WorldY) pair. const bool bIntegerXY = WorldX == FMath::FloorToFloat(WorldX) && WorldY == FMath::FloorToFloat(WorldY); FColumn* MemoColumn = &DirectColumn; // ⚠️ La boîte acquise doit survivre au `if` : le drapeau `Computed` n'est posé qu'APRÈS // le calcul, plus bas, hors de cette portée. Non nul ⇔ chemin XY entier. // The acquired box must outlive the `if`: the `Computed` flag is only set AFTER the // column is computed, further down and outside this scope. Non-null <=> integer path. FColumnBox* AcquiredBox = nullptr; int32 CI = 0; bool bNeedsCompute = true; if (bIntegerXY) { const int32 IX = (int32)WorldX; const int32 IY = (int32)WorldY; // Acquire vérifie la clé uint64 complète et les bornes exactes avant de dériver CI. // Acquire checks the exact uint64 key and exact bounds before deriving CI. FColumnBox& Box = columnCache.Acquire(IX, IY, ColumnKey); AcquiredBox = &Box; CI = (IY - Box.BaseY) * FColumnBox::Dim + (IX - Box.BaseX); MemoColumn = &Box.Cols[CI]; if (Box.Computed[CI]) { INC_DWORD_STAT(STAT_VoxelForgeColumnMemoHit); bNeedsCompute = false; } else { INC_DWORD_STAT(STAT_VoxelForgeColumnMemoMiss); } } else { INC_DWORD_STAT(STAT_VoxelForgeColumnMemoMiss); } if (bNeedsCompute) { FColumn& C = *MemoColumn; C.TerrainZ = TerrainStack.EvalHeight(WorldX, WorldY); C.CeilSurf = CeilingStack.EvalHeight(WorldX, WorldY); C.OverhangAmp = 0.0f; C.DirX = 0.0f; C.DirY = 0.0f; // Gate d'overhang par colonne : pente issue d'une différence AVANT du champ // STRUCTUREL, à l'échelle de la portée mais CLAMPÉE à [4,16]. Sans ce clamp, une // grande `Reach` moyenne la pente sur une énorme portée et lit même une vraie // falaise comme plate — le bug « grande Reach = rien ». Transcrit tel quel. // Quel jeu de params gouverne cette colonne ? Sans biomes, `P`. Avec, le DOMINANT // pour la pente et le seuil, et une interpolation de l'AMPLITUDE vers le voisin — // exactement ce que fait `ComputeSurfaceColumn` (`Lerp(Amp(PD), Amp(PN), W)`, pente // depuis `*PD` seul). Interpoler la pente n'aurait pas de sens : c'est une mesure du // terrain, pas un réglage. const FSurfaceGenerationParams* PD = &P; const FSurfaceGenerationParams* PN = nullptr; float W = 0.0f; const IVoxelHeightOp* SlopeField = Structural; if (BiomeParams.Num() > 0 && Field) { const FVoxelBiomeWeights BW = Field->SampleAt(WorldX, WorldY); const int32 Di = BiomeParams.IsValidIndex(BW.Dominant) ? BW.Dominant : 0; PD = &BiomeParams[Di]; if (PerBiomeStructural.IsValidIndex(Di)) { SlopeField = PerBiomeStructural[Di].Get(); } if (BW.NeighborWeight > 0.0f && BiomeParams.IsValidIndex(BW.Neighbor)) { PN = &BiomeParams[BW.Neighbor]; W = BW.NeighborWeight; } } const bool bAnyOverhang = (PD->OverhangStrength > 0.0f) || (PN && PN->OverhangStrength > 0.0f); if (bAnyOverhang && SlopeField != nullptr) { const float SD = FMath::Clamp(PD->OverhangReach, 4.0f, 16.0f); const float Z0 = SampleStructuralOf(SlopeField, WorldX, WorldY); const float GX = (SampleStructuralOf(SlopeField, WorldX + SD, WorldY) - Z0) / SD; const float GY = (SampleStructuralOf(SlopeField, WorldX, WorldY + SD) - Z0) / SD; const float Slope = FMath::Sqrt(GX * GX + GY * GY); auto Amp = [Slope](const FSurfaceGenerationParams& Q) -> float { if (Q.OverhangStrength <= 0.0f) { return 0.0f; } const float Thr = FMath::Max(Q.OverhangSlopeThreshold, 0.05f); const float Gate = FMath::Clamp((Slope - Thr) / Thr, 0.0f, 1.0f); return Q.OverhangStrength * Gate; // [0,1] }; C.OverhangAmp = (W > 0.0f && PN) ? FMath::Lerp(Amp(*PD), Amp(*PN), W) : Amp(*PD); // Direction amont unitaire (le gradient pointe vers le haut). Dégénérée sur le // plat — mais l'amplitude y vaut 0 de toute façon. if (Slope > KINDA_SMALL_NUMBER) { C.DirX = GX / Slope; C.DirY = GY / Slope; } } if (AcquiredBox) { AcquiredBox->Computed[CI] = true; } } return *MemoColumn; } /** Le champ structurel nu — l'overhang s'en sert pour emprunter la roche amont. * Avec des biomes, c'est celui du biome DOMINANT en ce point (l'original emprunte à `*PD`). */ float SampleStructural(float WorldX, float WorldY) const { const IVoxelHeightOp* Src = Structural; if (BiomeParams.Num() > 0 && Field) { const FVoxelBiomeWeights BW = Field->SampleAt(WorldX, WorldY); const int32 Di = PerBiomeStructural.IsValidIndex(BW.Dominant) ? BW.Dominant : 0; if (PerBiomeStructural.IsValidIndex(Di)) { Src = PerBiomeStructural[Di].Get(); } } return SampleStructuralOf(Src, WorldX, WorldY); } static float SampleStructuralOf(const IVoxelHeightOp* Src, float WorldX, float WorldY) { if (!Src) { return 0.0f; } FVoxelHeightSample S; Src->Eval(WorldX, WorldY, S); return S.Height; } EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::None; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } EVoxelOpResourceMask ProvidedResources() const override { return VoxelOpResources::SurfaceColumn; } bool IsAdditive() const override { return false; } bool IsXYPure() const override { return false; } // voir le bloc ci-dessus /** * ⚠️ C'EST ICI QUE SE JOUE LA PERF DE CET ARCHÉTYPE. * * Ne PAS clé ce mémo sur `InstanceId` : il change à CHAQUE reconstruction de pile, * c'est-à-dire à chaque chunk, et une strate haute de 4 chunks recalculerait ses colonnes * **4 fois**, resamples du cliff compris. `GSurfColCache` est clé sur * `(boîte XY, StrateKey, Seed, LayoutVersion)` **SANS ChunkZ**, délibérément, « shared down * the whole vertical strate stack ». Cette pile reprend la même identité de * strate/layout/seed, en ajoutant l'empreinte obligatoire des params pour protéger ses * sorties propres ; son mémo est un LRU spatial de six boîtes. * * Donc la clé garde l'identité partagée : ce qui rend deux colonnes interchangeables, c'est * la STRATE, le seed, la version de layout et les params, pas le chunk. Le mémo étant * `thread_local`, il SURVIT à la reconstruction de la pile — seule la clé l'invalide. * * POURQUOI C'EST SÛR : les hauteurs sont XY-pures par construction (c'est tout l'objet de * `VoxelHeightOp.h`, où le type n'a pas de Z), et le champ de biomes est documenté * XY-pur — « ZERO Z dependence: the climate/Voronoi fields are pure-XY ». C'est exactement * la justification sur laquelle `GSurfColCache` repose déjà. * * Do not key the memo on InstanceId: it changes every chunk, so a 4-chunk strate would * recompute every column 4x. GSurfColCache deliberately omits ChunkZ and shares down the whole * vertical stack; this shares the same strate/layout/seed identity and adds the required * params fingerprint for its own outputs. The six-box LRU keeps independent XY regions alive. Safe * because heights are XY-pure by type and the biome field is documented Z-independent. */ void PrepareChunk(const FVoxelOpContext& Ctx) override { // `StrateBottomWorldZ` est unique par strate empilée — la même valeur que // `CP_StrateKey` utilise dans `GetDensityAt`. La version de layout entre dans la clé // (AUDIT §C2) : une édition à chaud qui change les params sans déplacer la strate doit // invalider, sinon on sert des colonnes périmées. const uint32 A = (uint32)FMath::RoundToInt(Ctx.StrateBottomWorldZ); const uint32 B = Ctx.LayoutVersion; const uint32 C = Ctx.Seed; // ⚠️ `ParamsFingerprint` EST OBLIGATOIRE, et son absence a été un vrai bug — attrapé par // `SurfaceHeightEquivalence` au build suivant (69/20000 écarts, 1 traversée d'iso). // // Sans lui, la clé ne contenait que (strate, layout, seed). Deux piles de la MÊME strate // avec des params DIFFÉRENTS obtenaient donc la même clé et se partageaient les colonnes : // la seconde lisait les colonnes de la première, calculées avec `OverhangAmp = 0`, et // l'overhang disparaissait purement et simplement. // // ET CE N'EST PAS QU'UN ARTEFACT DE TEST : c'est exactement la faiblesse que le code // documente déjà pour `GSurfColCache` (VoxelGenerator.cpp, note AUDIT §C2 étendue) — // « une édition à chaud qui change les params SANS déplacer la strate laisse la clé // identique et sert des colonnes périmées ». En production `LayoutVersion` bouge à chaque // `RebuildStrates`, ce qui masque le trou ; ma clé en avait hérité, et le test l'a trouvé // tout de suite. Empreinte incluse ⇒ le trou est fermé ici, pas seulement masqué. // // The fingerprint is REQUIRED: without it two stacks of the same strate with different // params shared columns, and the overhang silently vanished. Same weakness the codebase // already documents for GSurfColCache, which LayoutVersion merely masks. ColumnKey = ((uint64)VoxelHash::Mix(A ^ VoxelHash::Mix(B)) << 32) | (uint64)VoxelHash::Mix(C ^ VoxelHash::Mix(A) ^ ParamsFingerprint); if (ColumnKey == 0) { ColumnKey = 1; } // 0 = « jamais préparé » } void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { // Le mémo par colonne vit ici, dans six boîtes thread_local partagées par les instances // mais séparées par la clé, et lues par les Eval de cette source et FOverhangShelfMod. // Il est séparé de `GSurfColCache` : la pile possède ses propres sorties et sa clé // complète (strate + layout + seed + empreinte des params), donc réutiliser le cache // du générateur serait incorrect. // The per-column memo lives here in six thread-local boxes shared across instances but // separated by the key, and read by this source's Eval calls and FOverhangShelfMod. // It is separate from `GSurfColCache`: the stack owns its own outputs and full key. const FColumn& C = GetColumn(WorldX, WorldY); float Density = C.TerrainZ - WorldZ; Density = FMath::Max(Density, WorldZ - C.CeilSurf); InOut.Density = Density; // Replace : interne, positif = solide } // Mixed, honnêtement. Un verdict exact demanderait de borner le heightfield structurel sur // la boîte XY (continents + montagnes + détail sous un domain-warp) — faisable, mais c'est // une vraie borne à dériver, pas une constante à lire comme pour la dalle. Rendre Mixed ne // coûte que du CPU ; rendre faux serait un trou. À faire quand `MaxDisplacement` saura // répondre pour la source structurelle. EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override { return EVoxelOpEffect::Both; } private: FSurfaceGenerationParams P; FVoxelHeightStack TerrainStack; FVoxelHeightStack CeilingStack; const IVoxelHeightOp* Structural = nullptr; // NON possédant : la pile terrain le possède // Chemin BIOMES. Vide ⇒ chemin d'origine, inchangé bit pour bit. TArray BiomeParams; TUniquePtr Field; // POSSÉDÉ (voir le constructeur) TArray> PerBiomeStructural; // pente d'overhang par biome uint64 InstanceId = 0; // unique, jamais recyclée — le repli quand PrepareChunk n'a pas eu lieu uint64 ColumnKey = 0; // l'identité PARTAGÉE (strate + layout + seed + params) : voir PrepareChunk uint32 ParamsFingerprint = 0; // sans lui, deux piles de la même strate se volaient leurs colonnes }; //========================================================================= // RÔLE 3 — MODIFIER : ÉTAGÈRE D'OVERHANG / OVERHANG SHELF (le seul op vraiment 3D) //========================================================================= // Pour les voxels d'AIR dans une fenêtre juste au-dessus d'une pente raide, ré-échantillonne le // heightfield EN AMONT (vers la falaise) d'une distance qui CROÎT avec la hauteur, et fait // l'union de cette roche → la roche du haut de falaise déborde AU-DESSUS du vide, avec de l'air // EN DESSOUS : un vrai surplomb. // // ⚠️ Il dépend de Z de façon essentielle — `Frac` fait varier la portée avec l'altitude. C'est // le seul op de SurfaceWorld qui ne pouvait PAS vivre en espace-hauteur, et c'est exactement // pour ça que la frontière entre les deux espaces est utile : elle est passée là où le code // change de nature, pas là où c'était commode. // // The one op here that genuinely depends on Z (the uphill reach grows with height), which is // precisely why it could not live in height space. The boundary between the two spaces falls // where the code changes nature. class FOverhangShelfMod final : public IVoxelDensityOp { public: FOverhangShelfMod(const FSurfaceGenerationParams& InP, int32 Seed, const FSurfaceColumnSource* InColumn) : P(InP), SeedU((uint32)Seed), Column(InColumn) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::SurfaceColumn; } bool IsAdditive() const override { return false; } void PrepareChunk(const FVoxelOpContext&) override {} bool IsXYPure() const override { return false; } // franchement non : voir `Frac` void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { if (Column == nullptr || P.OverhangHeight <= 0.0f) { return; } // Même colonne que la source, garantie par le mémo : pas une seconde évaluation. const FSurfaceColumnSource::FColumn& C = Column->GetColumn(WorldX, WorldY); if (C.OverhangAmp <= 0.0f) { return; } // Gate dur, transcrit : seulement les voxels d'air dans `OverhangHeight` du sol local. if (!(WorldZ > C.TerrainZ && WorldZ <= C.TerrainZ + P.OverhangHeight)) { return; } const float f = P.OverhangFrequency; // Bruit de forme d'étagère [0,1] ; le terme en Z fait onduler la portée avec la hauteur // (déchiqueté, pas une lèvre lisse). const float Ns = HFractal3D(FVector3f( WorldX * f + VoxelHash::SeedOffset(SeedU, 17.3f), WorldY * f + VoxelHash::SeedOffset(SeedU, 23.9f), WorldZ * f * P.OverhangZScale + VoxelHash::SeedOffset(SeedU, 5.1f)), 3) * 0.5f + 0.5f; // [0,1] // LA CLÉ : la portée amont CROÎT avec la hauteur dans la fenêtre (Frac : 0 au sol → 1 // au plafond de la fenêtre). En bas le décalage est minuscule ⇒ on emprunte de la roche // basse voisine ⇒ ça reste de l'AIR au-dessus du vide ; en haut le décalage atteint la // falaise ⇒ solide ⇒ la lèvre se pose dessus avec de l'air DESSOUS = un vrai surplomb. const float Frac = (WorldZ - C.TerrainZ) / P.OverhangHeight; const float ShiftV = P.OverhangReach * C.OverhangAmp * Frac * Ns; if (ShiftV > 0.5f) { // On emprunte la hauteur STRUCTURELLE amont (pas la surface complète avec ops) : // le dessous de l'étagère n'a pas besoin du raffinement cliff/terrace, et ça évite // de relancer les 4 resamples du cliff par voxel de lèvre. const float ShiftedTZ = Column->SampleStructural(WorldX + C.DirX * ShiftV, WorldY + C.DirY * ShiftV); InOut.Density = FMath::Max(InOut.Density, ShiftedTZ - WorldZ); // union } } // N'ajoute que du solide (`Max`) ⇒ tue AllAir, jamais AllSolid. Conservateur : on ne sait // pas sans échantillonner si une colonne d'overhang touche la boîte, donc FillOnly partout // où l'archétype peut en produire, Identity quand il est éteint. EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override { return (P.OverhangStrength > 0.0f && P.OverhangHeight > 0.0f) ? EVoxelOpEffect::FillOnly : EVoxelOpEffect::Identity; } private: FSurfaceGenerationParams P; uint32 SeedU; const FSurfaceColumnSource* Column; // NON possédant : la pile possède la source }; //========================================================================= // RÔLE 3 — MODIFIER : RUGOSITÉ DE PAROI, ESPACE SDF //========================================================================= // La variante SDF (Maze / VerticalShafts / FloatingIslands) : `Sdf += bruit · échelle · force`. // Déplace la SURFACE. La variante densité de TunnelNetwork est un opérateur DIFFÉRENT (fade // quadratique, clamp anti-remplissage, 4 types de bruit) — voir OPSTACK-DECOMPOSITION §1. class FSdfRoughnessMod final : public IVoxelDensityOp { public: FSdfRoughnessMod(float InStrength, float InFrequency, int32 InBaseOctaves, float InApplyWithin) : Strength(InStrength), Frequency(InFrequency) , BaseOctaves(InBaseOctaves), ApplyWithin(InApplyWithin) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Sdf; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Sdf; } bool IsAdditive() const override { return false; } // gate depends on the SDF it writes void PrepareChunk(const FVoxelOpContext&) override {} void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { if (Strength <= 0.0f || InOut.Sdf >= ApplyWithin) { return; } // Keep hot noise coordinates in float. The old FVector round-trip only preserved // legacy rounding; output compatibility with that field is intentionally abandoned. const FVector3f NoisePos(WorldX * Frequency, WorldY * Frequency, WorldZ * Frequency); InOut.Sdf += VoxelNoise::FBM(NoisePos.X, NoisePos.Y, NoisePos.Z, VoxelGenLOD::Eff(BaseOctaves), 2.0f, 0.5f) * VOXEL_NOISE_SCALE * Strength; } // This modifier does not touch density itself. Its interval update is the only box-query // responsibility; the downstream converter consumes the resulting interval. EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override { return EVoxelOpEffect::Identity; } void PropagateSdfOverBox(FVoxelBoxSdfInterval& InOut, const FBox& VoxelBox, const FVoxelOpContext&) const override { if (!InOut.IsKnown()) { return; } if (!VoxelMath::IsFinite(Strength) || !VoxelMath::IsFinite(Frequency) || !VoxelMath::IsFinite(ApplyWithin) || BaseOctaves <= 0) { InOut.SetUnknown(); return; } if (Strength <= 0.0f) { return; } // If an authored frequency/box product overflows, Eval can publish a non-finite SDF // and no finite interval is a proof. Unknown costs the skip and protects the geometry. const float MaxAbsCoord = FMath::Max3( FMath::Max(FMath::Abs((float)VoxelBox.Min.X), FMath::Abs((float)VoxelBox.Max.X)), FMath::Max(FMath::Abs((float)VoxelBox.Min.Y), FMath::Abs((float)VoxelBox.Max.Y)), FMath::Max(FMath::Abs((float)VoxelBox.Min.Z), FMath::Abs((float)VoxelBox.Max.Z))); if (!VoxelMath::IsFinite(MaxAbsCoord) || !VoxelMath::IsFinite(MaxAbsCoord * FMath::Abs(Frequency))) { InOut.SetUnknown(); return; } // The gate is `Sdf < ApplyWithin`. If the whole input interval is outside it, the // modifier is exactly the identity. Otherwise add the proven FBM envelope to both // sides; conditional untouched points are still covered by [Min, Max] + envelope. if (InOut.Min >= ApplyWithin) { return; } const float Amplitude = FMath::Abs(Strength) * VOXEL_NOISE_SCALE * VF_PerlinAbsBound; InOut.Min = VF_SaturatingAdd(InOut.Min, -Amplitude); InOut.Max = VF_SaturatingAdd(InOut.Max, Amplitude); } private: float Strength, Frequency; int32 BaseOctaves; float ApplyWithin; }; //========================================================================= // RÔLE 3 — MODIFIER : COLONNES SUR GRILLE MONDE / WORLD-GRID COLUMNS //========================================================================= // Des cylindres de hauteur infinie posés sur une grille de // `ColumnSpacing`, un tirage d'existence et un jitter par cellule. Le champ de vide décide déjà // où est le solide, donc la colonne n'a qu'à AJOUTER de la densité le long de son XY — elle // n'est visible que là où le vide avait creusé autour d'elle. // // Le cache 3×3 par cellule est `thread_local`, ce que la note de threading de VoxelDensityOp.h // autorise. Sa clé // contient tous les paramètres qui influent sur le résultat + le seed, donc un changement de // layout qui change un param invalide bien ; un changement qui n'en touche aucun produirait // des colonnes identiques (cf. AUDIT C2 — la clé est complète, pas seulement le coord). class FGridColumnMod final : public IVoxelDensityOp { public: explicit FGridColumnMod(const FSlabGenerationParams& P, int32 InSeed) : Seed((uint32)InSeed) , Spacing(P.ColumnSpacing) , ColDensity(P.ColumnDensity) , MinRadius(P.ColumnMinRadius) , MaxRadius(P.ColumnMaxRadius) , BaseDensity(P.BaseDensity) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } bool IsAdditive() const override { return true; } void PrepareChunk(const FVoxelOpContext&) override {} bool IsXYPure() const override { return true; } // cylindres de hauteur infinie void Eval(float WorldX, float WorldY, float, FVoxelOpSample& InOut) const override { if (ColDensity <= 0.0f || Spacing <= 0.0f) { return; } const int32 ColCX = FMath::FloorToInt(WorldX / Spacing); const int32 ColCY = FMath::FloorToInt(WorldY / Spacing); const TArray>& Cols = GetCells(ColCX, ColCY); float ColumnSDF = FLT_MAX; for (const FSlabColumn& Col : Cols) { const float DX2D = WorldX - Col.X; const float DY2D = WorldY - Col.Y; ColumnSDF = FMath::Min(ColumnSDF, FMath::Sqrt(DX2D * DX2D + DY2D * DY2D) - Col.R); } if (ColumnSDF < ColBlend && ColumnSDF < FLT_MAX) { float Fill = FMath::Clamp((ColBlend - ColumnSDF) / (ColBlend * 2.0f), 0.0f, 1.0f); Fill = SmoothStep01(Fill); InOut.Density += Fill * BaseDensity * 1.5f; } } // N'AJOUTE que du solide ⇒ tue AllAir, jamais AllSolid. `Identity` dès qu'aucune colonne // n'atteint la boîte — ce qui, pour un `ColumnDensity` de 0.08, est l'écrasante majorité du // volume. C'est cet `Identity` qui laisse survivre le verdict AllAir de la source. EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext&) const override { if (ColDensity <= 0.0f || Spacing <= 0.0f) { return EVoxelOpEffect::Identity; } // Marge : l'enveloppe de `Lerp(MinRadius, MaxRadius, t)` est max(MinRadius, MaxRadius), // pas `MaxRadius` seul si l'asset inverse les paramètres. The bound must cover both // endpoints; using `MaxRadius` alone would leave a hole when the asset reverses them. const float Reach = FMath::Max3(MinRadius, MaxRadius, 0.0f) + ColBlend; const int32 CX0 = FMath::FloorToInt(((float)VoxelBox.Min.X - Reach) / Spacing); const int32 CX1 = FMath::FloorToInt(((float)VoxelBox.Max.X + Reach) / Spacing); const int32 CY0 = FMath::FloorToInt(((float)VoxelBox.Min.Y - Reach) / Spacing); const int32 CY1 = FMath::FloorToInt(((float)VoxelBox.Max.Y + Reach) / Spacing); for (int32 CY = CY0; CY <= CY1; ++CY) { for (int32 CX = CX0; CX <= CX1; ++CX) { FSlabColumn Col; if (!RollColumn(CX, CY, Col)) { continue; } // Cercle (rayon + blend) contre le rectangle XY de la boîte. const float R = Col.R + ColBlend; const float QX = FMath::Max(0.0f, FMath::Max((float)VoxelBox.Min.X - Col.X, Col.X - (float)VoxelBox.Max.X)); const float QY = FMath::Max(0.0f, FMath::Max((float)VoxelBox.Min.Y - Col.Y, Col.Y - (float)VoxelBox.Max.Y)); if (QX * QX + QY * QY < R * R) { return EVoxelOpEffect::FillOnly; } } } return EVoxelOpEffect::Identity; } private: struct FSlabColumn { float X, Y, R; }; static constexpr float ColBlend = 2.0f; /** Le tirage d'une cellule : existence, jitter, rayon. Fonction PURE de (cellule, seed, * params) — donc `Eval` et `EffectOverBox` voient forcément la même colonne. */ bool RollColumn(int32 CX, int32 CY, FSlabColumn& Out) const { const uint32 H = VoxelHash::Cell(CX, CY, Seed ^ 0xC01C01u); if (VoxelHash::ToFloat01(H) > ColDensity) { return false; } const float JX = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x12345678u)); const float JY = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x9ABCDEF0u)); Out.X = (CX + 0.15f + JX * 0.7f) * Spacing; Out.Y = (CY + 0.15f + JY * 0.7f) * Spacing; Out.R = FMath::Lerp(MinRadius, MaxRadius, VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0xBEEFu))); return true; } /** Le voisinage 3×3 de la cellule centrale, mémoïsé par worker. */ const TArray>& GetCells(int32 ColCX, int32 ColCY) const { thread_local TArray> SC_Cols; thread_local int32 SC_CX = INT32_MAX, SC_CY = INT32_MAX; thread_local uint32 SC_Seed = 0xFFFFFFFFu; thread_local float SC_Spacing = -1.0f, SC_Dens = -1.0f, SC_MinR = -1.0f, SC_MaxR = -1.0f; if (ColCX != SC_CX || ColCY != SC_CY || Seed != SC_Seed || Spacing != SC_Spacing || ColDensity != SC_Dens || MinRadius != SC_MinR || MaxRadius != SC_MaxR) { SC_CX = ColCX; SC_CY = ColCY; SC_Seed = Seed; SC_Spacing = Spacing; SC_Dens = ColDensity; SC_MinR = MinRadius; SC_MaxR = MaxRadius; SC_Cols.Reset(); for (int32 DY = -1; DY <= 1; DY++) { for (int32 DX = -1; DX <= 1; DX++) { FSlabColumn Col; if (RollColumn(ColCX + DX, ColCY + DY, Col)) { SC_Cols.Add(Col); } } } } return SC_Cols; } uint32 Seed; float Spacing, ColDensity, MinRadius, MaxRadius, BaseDensity; }; //========================================================================= // RÔLE 2 — COMBINER : SDF → DENSITÉ (CARVE et FILL) //========================================================================= // Les six mêmes lignes dans TunnelNetwork, Maze, VerticalShafts — et FloatingIslands, où le // SEUL changement est `Density += Fill·Base·2` au lieu de `Density -= Carve·Base·2`. // // Un archétype qui CREUSE dans du roc et un archétype qui REMPLIT du vide sont donc le même // opérateur au signe près, exactement comme la source constante au-dessus. // // ⚠️ `Sign` vaut ±1.0f et rien d'autre. La multiplication par ±1 est EXACTE en IEEE-754, donc // `D += (-1·F)·B·2` rend bit pour bit ce que `D -= F·B·2` rendait — l'égalité binaire des trois // portages déjà verts en dépend. // // Same operator, opposite sign. Multiplying by ±1 is exact in IEEE-754, so the carve path is // bit-for-bit what it was before this generalisation — the three green ports depend on that. class FSdfConvertOp final : public IVoxelDensityOp { public: FSdfConvertOp(float InBlend, float InBaseDensity, float InSign, float InMinDivisor) : Blend(InBlend), BaseDensity(InBaseDensity), Sign(InSign), MinDivisor(InMinDivisor) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::Combiner; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density | VoxelOpChannels::Sdf; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } bool IsAdditive() const override { return true; } void PrepareChunk(const FVoxelOpContext&) override {} void Eval(float, float, float, FVoxelOpSample& InOut) const override { if (InOut.Sdf >= Blend) { return; } // ⚠️ `MinDivisor` n'est PAS une précaution ajoutée : TunnelNetwork écrit // `/ FMath::Max(SDFBlendRadius * 2, 1.0f)` là où Maze/Shafts/Islands écrivent `/ (Blend*2)`. // Les deux formules DIVERGENT dès que `Blend·2 < 1`, donc les confondre serait une faute // de portage silencieuse. Avec `MinDivisor = 0` et un Blend positif, `Max(x, 0) == x` // exactement — les trois portages déjà verts ne bougent pas d'un bit. // Not a safety tweak: TunnelNetwork genuinely floors this divisor at 1 and the others // do not. Max(x, 0) is exactly x for positive Blend, so existing ports are untouched. float T = FMath::Clamp((Blend - InOut.Sdf) / FMath::Max(Blend * 2.0f, MinDivisor), 0.0f, 1.0f); T = SmoothStep01(T); InOut.Density += Sign * T * BaseDensity * 2.0f; // interne : monter = vers le solide } EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override { const float Coefficient = Sign * BaseDensity * 2.0f; if (!VoxelMath::IsFinite(Coefficient) || !VoxelMath::IsFinite(Blend) || !VoxelMath::IsFinite(MinDivisor) || Blend <= 0.0f) { return EVoxelOpEffect::Both; } if (Coefficient < 0.0f) { return EVoxelOpEffect::CarveOnly; } if (Coefficient > 0.0f) { return EVoxelOpEffect::FillOnly; } return EVoxelOpEffect::Identity; } EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx, const FVoxelBoxHypotheses& H) const override { const EVoxelOpEffect Intrinsic = EffectOverBox(VoxelBox, Ctx); if (Intrinsic == EVoxelOpEffect::Both || Intrinsic == EVoxelOpEffect::Identity) { return Intrinsic; } return H.Sdf.IsKnown() && H.Sdf.Min >= Blend ? EVoxelOpEffect::Identity : Intrinsic; } float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override { const float Coefficient = Sign * BaseDensity * 2.0f; if (!VoxelMath::IsFinite(Coefficient) || !VoxelMath::IsFinite(Blend) || !VoxelMath::IsFinite(MinDivisor) || Blend <= 0.0f) { return FLT_MAX; } return Coefficient < 0.0f ? FMath::Abs(Coefficient) : 0.0f; } float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override { const float Coefficient = Sign * BaseDensity * 2.0f; if (!VoxelMath::IsFinite(Coefficient) || !VoxelMath::IsFinite(Blend) || !VoxelMath::IsFinite(MinDivisor) || Blend <= 0.0f) { return FLT_MAX; } return Coefficient > 0.0f ? FMath::Abs(Coefficient) : 0.0f; } float MaxCarveOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx, const FVoxelBoxHypotheses& H) const override { const float Coefficient = Sign * BaseDensity * 2.0f; if (!VoxelMath::IsFinite(Coefficient)) { return FLT_MAX; } if (!VoxelMath::IsFinite(Blend) || !VoxelMath::IsFinite(MinDivisor) || Blend <= 0.0f) { return FLT_MAX; } if (Coefficient >= 0.0f) { return 0.0f; } return IsInactive(H) ? 0.0f : FMath::Abs(Coefficient) * MaxFactor(H); } float MaxFillOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx, const FVoxelBoxHypotheses& H) const override { const float Coefficient = Sign * BaseDensity * 2.0f; if (!VoxelMath::IsFinite(Coefficient)) { return FLT_MAX; } if (!VoxelMath::IsFinite(Blend) || !VoxelMath::IsFinite(MinDivisor) || Blend <= 0.0f) { return FLT_MAX; } if (Coefficient <= 0.0f) { return 0.0f; } return IsInactive(H) ? 0.0f : FMath::Abs(Coefficient) * MaxFactor(H); } const TCHAR* DebugName() const override { return TEXT("SdfConvertOp"); } void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } private: bool IsInactive(const FVoxelBoxHypotheses& H) const { return !H.Sdf.IsKnown() ? false : H.Sdf.Min >= Blend; } float MaxFactor(const FVoxelBoxHypotheses& H) const { if (!H.Sdf.IsKnown() || !VoxelMath::IsFinite(Blend) || !VoxelMath::IsFinite(MinDivisor) || Blend <= 0.0f) { return 1.0f; } const float Denom = FMath::Max(Blend * 2.0f, MinDivisor); if (!(Denom > 0.0f) || !VoxelMath::IsFinite(Denom)) { return 1.0f; } const float T = FMath::Clamp((Blend - H.Sdf.Min) / Denom, 0.0f, 1.0f); return SmoothStep01(T); } float Blend, BaseDensity, Sign, MinDivisor; }; //========================================================================= // RÔLE 4 — STRUCTUREL : SPINE (0,0) //========================================================================= class FOriginSpineOp final : public IVoxelDensityOp { public: FOriginSpineOp(float InTopZ, float InBotZ, float InSeal, float InBase, float InRadius) : TopZ(InTopZ), BotZ(InBotZ), Seal(InSeal), Base(InBase), Radius(InRadius) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::StructuralPost; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } bool IsAdditive() const override { return true; } void PrepareChunk(const FVoxelOpContext&) override {} void Eval(float X, float Y, float Z, FVoxelOpSample& InOut) const override { VF_ApplyOriginSpine(InOut.Density, X, Y, Z, TopZ, BotZ, Seal, Base, Radius); } // The room kills AllSolid; its support slab also kills AllAir. Identity when the room // misses the box or the box is wholly outside this strate's safe interior. EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override { if (VoxelDensityAblation::IsOriginSpineOff()) { return EVoxelOpEffect::Identity; } const bool bRoomTouches = Ctx.bUseLatticeProof ? VoxelPassageGeometry::OriginLandingRoomTouchesLattice( VoxelBox, Ctx.LatticeOriginVoxels, Ctx.Step, TopZ, BotZ, Seal, Radius) : VoxelPassageGeometry::OriginLandingRoomTouchesBox( VoxelBox, TopZ, BotZ, Seal, Radius); if (!bRoomTouches) { return EVoxelOpEffect::Identity; } const bool bFloorTouches = Ctx.bUseLatticeProof ? VoxelPassageGeometry::OriginLandingFloorTouchesLattice( VoxelBox, Ctx.LatticeOriginVoxels, Ctx.Step, TopZ, BotZ, Seal, Radius) : VoxelPassageGeometry::OriginLandingFloorTouchesBox( VoxelBox, TopZ, BotZ, Seal, Radius); return bFloorTouches ? EVoxelOpEffect::Both : EVoxelOpEffect::CarveOnly; } // The landing carve interpolates from the incoming density toward an air target, so its // delta is unbounded without an input-density interval. It is nevertheless exactly // inactive outside the geometric influence box; return zero there so an unrelated block // does not inherit the interface's conservative FLT_MAX sentinel. float MaxCarveOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override { const EVoxelOpEffect Effect = EffectOverBox(VoxelBox, Ctx); return (Effect == EVoxelOpEffect::CarveOnly || Effect == EVoxelOpEffect::Both) ? FLT_MAX : 0.0f; } float MaxFillOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override { return EffectOverBox(VoxelBox, Ctx) == EVoxelOpEffect::Both ? FLT_MAX : 0.0f; } const TCHAR* DebugName() const override { return TEXT("OriginSpineOp"); } void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } private: float TopZ, BotZ, Seal, Base, Radius; }; //========================================================================= // RÔLE 4 — STRUCTUREL : SEAL DE FRONTIÈRE (l'opérateur FORÇANT) //========================================================================= class FBoundarySealOp final : public IVoxelDensityOp { public: FBoundarySealOp(float InTopZ, float InBotZ, float InThickness, float InBase) : TopZ(InTopZ), BotZ(InBotZ), Thickness(InThickness), Base(InBase) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::StructuralPost; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } bool IsAdditive() const override { return false; } void PrepareChunk(const FVoxelOpContext&) override {} void Eval(float, float, float Z, FVoxelOpSample& InOut) const override { VF_ApplyBoundarySeal(InOut.Density, Z, TopZ, BotZ, Thickness, Base); } /** * Dans sa bande, le seal fait `Max(D, SealFactor·Base)` avec SealFactor > 0 : le résultat * est solide GARANTI quelle qu'ait été l'entrée. C'est un opérateur FORÇANT, et la raison * d'être de `ClassifyBox` (voir VoxelDensityOp.h). * * ⚠️ MARGE DE SÛRETÉ DÉLIBÉRÉE. Au bord INTÉRIEUR de la bande, `1 - Dist/Thickness` peut * arrondir à exactement 0.0f en float ; SealFactor·Base vaut alors 0, la densité interne * finit à 0, et le mesher (`D >= IsoLevel`) compte ce point du côté AIR. Prétendre AllSolid * là serait un TROU. On exige donc que la boîte soit dans la bande avec 1 voxel de marge * avant de forcer ; sinon on retombe sur le FillOnly, qui est toujours sûr. * * (Le `ClassifyTile` actuel n'a pas cette marge — il exclut simplement ces z du test de * colonne. La fenêtre est infime et demande que l'archétype produise de l'air pile à ce z, * mais elle est réelle.) */ EVoxelTileClass ClassifyBox(const FBox& VoxelBox, const FVoxelOpContext&) const override { if (VoxelDensityAblation::IsBoundarySealOff()) { return EVoxelTileClass::Mixed; } if (Thickness <= 0.0f || Base <= 0.0f) { return EVoxelTileClass::Mixed; } constexpr float SafetyMargin = 1.0f; const float Usable = Thickness - SafetyMargin; if (Usable <= 0.0f) { return EVoxelTileClass::Mixed; } const float MinDistTop = TopZ - (float)VoxelBox.Min.Z; // plus petite distance au plafond const float MaxDistTop = TopZ - (float)VoxelBox.Max.Z; const bool bWhollyInTopBand = (MaxDistTop >= 0.0f) && (MinDistTop < Usable); const float MinDistBot = (float)VoxelBox.Min.Z - BotZ; const float MaxDistBot = (float)VoxelBox.Max.Z - BotZ; const bool bWhollyInBotBand = (MinDistBot >= 0.0f) && (MaxDistBot < Usable); return (bWhollyInTopBand || bWhollyInBotBand) ? EVoxelTileClass::AllSolid : EVoxelTileClass::Mixed; } // Hors de sa bande, le seal ne fait rien du tout ; à cheval, il ne peut qu'ajouter du solide. EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext&) const override { if (VoxelDensityAblation::IsBoundarySealOff()) { return EVoxelOpEffect::Identity; } if (Thickness <= 0.0f) { return EVoxelOpEffect::Identity; } const bool bTouchesTopBand = ((float)VoxelBox.Max.Z >= TopZ - Thickness) && ((float)VoxelBox.Min.Z <= TopZ); const bool bTouchesBotBand = ((float)VoxelBox.Min.Z <= BotZ + Thickness) && ((float)VoxelBox.Max.Z >= BotZ); if (!bTouchesTopBand && !bTouchesBotBand) { return EVoxelOpEffect::Identity; } return EVoxelOpEffect::FillOnly; } // A partial seal can raise an arbitrarily negative incoming density, so its fill delta is // genuinely unbounded through this API. Outside both bands it is an exact identity. float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override { return 0.0f; } float MaxFillOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override { return EffectOverBox(VoxelBox, Ctx) == EVoxelOpEffect::Identity ? 0.0f : FLT_MAX; } const TCHAR* DebugName() const override { return TEXT("BoundarySealOp"); } void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } private: float TopZ, BotZ, Thickness, Base; }; //========================================================================= // RÔLE 4 — STRUCTUREL : SCELLEMENT DE LA LIMITE XY (opérateur FORÇANT) //========================================================================= class FXYEdgeSealOp final : public IVoxelDensityOp { public: explicit FXYEdgeSealOp(float InBase) : Base(InBase) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::StructuralPost; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } bool IsAdditive() const override { return false; } void PrepareChunk(const FVoxelOpContext& Ctx) override { // The settings are global, but PrepareChunk is the stack's one per-chunk hand-off. // Keeping the prepared copy makes Eval hot and keeps the op independent of the asset. WorldRadius = Ctx.WorldRadiusVoxels; Thickness = Ctx.EdgeSealThickness; } void Eval(float X, float Y, float, FVoxelOpSample& InOut) const override { VF_ApplyXYEdgeSeal(InOut.Density, X, Y, WorldRadius, Thickness, Base); } /** * The seal is forcing, not merely FillOnly: once every point in the box is at least one * voxel into the ramp (or beyond the radius), the result is positive-solid regardless of * all preceding source/carve operators. */ EVoxelTileClass ClassifyBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override { if (VoxelDensityAblation::IsXYEdgeSealOff()) { return EVoxelTileClass::Mixed; } return VF_XYEdgeSealBoxIsForcedSolid( VoxelBox, Ctx.WorldRadiusVoxels, Ctx.EdgeSealThickness, Base) ? EVoxelTileClass::AllSolid : EVoxelTileClass::Mixed; } float ForcedMarginOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override { return VF_XYEdgeSealForcedMarginOverBox( VoxelBox, Ctx.WorldRadiusVoxels, Ctx.EdgeSealThickness, Base); } // In the inner world it is Identity; a box that reaches the radial band can only gain rock. EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override { if (VoxelDensityAblation::IsXYEdgeSealOff()) { return EVoxelOpEffect::Identity; } return VF_XYEdgeSealBoxTouchesBand( VoxelBox, Ctx.WorldRadiusVoxels, Ctx.EdgeSealThickness) ? EVoxelOpEffect::FillOnly : EVoxelOpEffect::Identity; } float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override { return 0.0f; } float MaxFillOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override { return EffectOverBox(VoxelBox, Ctx) == EVoxelOpEffect::Identity ? 0.0f : FLT_MAX; } bool IsXYPure() const override { return true; } const TCHAR* DebugName() const override { return TEXT("XYEdgeSealOp"); } void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } private: float Base = 8.0f; mutable float WorldRadius = 8192.0f; mutable float Thickness = 64.0f; }; //========================================================================= // RÔLE 4 — STRUCTUREL : CARVE DE PASSAGE //========================================================================= class FPassageCarveOp final : public IVoxelDensityOp { public: FPassageCarveOp(const UVoxelStrateManager* InManager, float InBase, float InSeal, float InTopZ, float InBotZ, float InSpineRadius) : Manager(InManager), Base(InBase), Seal(InSeal), TopZ(InTopZ), BotZ(InBotZ), SpineRadius(InSpineRadius) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::StructuralPost; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } bool IsAdditive() const override { return false; } void PrepareChunk(const FVoxelOpContext&) override {} void Eval(float X, float Y, float Z, FVoxelOpSample& InOut) const override { if (const UVoxelStrateManager* LiveManager = Manager.Get()) { // Landing/tunnel support is a shared MC-space post after disturbances. Calling the // legacy full modifier here repeats those same writers before the shared post. LiveManager->ApplyPassageCarvingOnly(InOut.Density, X, Y, Z, Base, Seal); } // Keep the source and op-stack operation order identical: the origin room is carved // before the boundary seal, then its support floor is reasserted after the passage // post so no passage-air post can remove the standing surface. VF_ApplyOriginLandingFloor( InOut.Density, X, Y, Z, TopZ, BotZ, Seal, Base, SpineRadius); } // ≡ la garde `AnyPassageNearBox` écrite à la main dans ClassifyTile — déjà écrite, ici // simplement branchée au bon endroit au lieu d'être un cas particulier du classifieur. EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override { if (VoxelDensityAblation::IsPassageCarvingOff() && VoxelDensityAblation::IsLandingPostsOff() && VoxelDensityAblation::IsPassageStructuralPostsOff() && VoxelDensityAblation::IsOriginSpineOff()) { return EVoxelOpEffect::Identity; } const UVoxelStrateManager* LiveManager = Manager.Get(); if (!LiveManager) { return EVoxelOpEffect::Identity; } const bool bOriginFloor = !VoxelDensityAblation::IsLandingPostsOff() && (Ctx.bUseLatticeProof ? VoxelPassageGeometry::OriginLandingFloorTouchesLattice( VoxelBox, Ctx.LatticeOriginVoxels, Ctx.Step, TopZ, BotZ, Seal, SpineRadius) : VoxelPassageGeometry::OriginLandingFloorTouchesBox( VoxelBox, TopZ, BotZ, Seal, SpineRadius)); const bool bPassageFloor = !VoxelDensityAblation::IsLandingPostsOff() && (Ctx.bUseLatticeProof ? LiveManager->AnyPassageLandingFloorNearLattice( VoxelBox, Ctx.LatticeOriginVoxels, Ctx.Step) : LiveManager->AnyPassageLandingFloorNearBox( VoxelBox.Min, VoxelBox.Max)); if (bOriginFloor || bPassageFloor) { // The room carves air, while its support slab force-writes solid. Both // hypotheses must therefore be killed for a box touching that slab. return EVoxelOpEffect::Both; } const bool bPassageCarve = !VoxelDensityAblation::IsPassageCarvingOff() && (Ctx.bUseLatticeProof ? LiveManager->AnyPassageNearLattice( VoxelBox, Ctx.LatticeOriginVoxels, Ctx.Step) : LiveManager->AnyPassageNearBox(VoxelBox.Min, VoxelBox.Max)); return bPassageCarve ? EVoxelOpEffect::CarveOnly : EVoxelOpEffect::Identity; } // The current-density Lerp/Min passage carve has no finite delta bound for an arbitrary // incoming density. Once the preceding stack has proved a positive solid margin, // however, the monotone carve can be bounded exactly on the MC lattice: evaluate the // largest carve factor, apply it to the lower input bound, and keep the resulting positive // margin. This preserves the old conservative fallback for non-lattice callers. float MaxCarveOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx, const FVoxelBoxHypotheses& H) const override { if (VoxelDensityAblation::IsPassageCarvingOff()) { return 0.0f; } const UVoxelStrateManager* LiveManager = Manager.Get(); if (!LiveManager) { return 0.0f; } if (!Ctx.bUseLatticeProof) { const EVoxelOpEffect Effect = EffectOverBox(VoxelBox, Ctx); return (Effect == EVoxelOpEffect::CarveOnly || Effect == EVoxelOpEffect::Both) ? FLT_MAX : 0.0f; } const float MaxFactor = LiveManager->MaxPassageCarveFactorNearLattice( VoxelBox, Ctx.LatticeOriginVoxels, Ctx.Step); if (!(MaxFactor > 0.0f)) { return 0.0f; } if (!(H.SolidMargin > 0.0f) || !VoxelMath::IsFinite(H.SolidMargin) || !VoxelMath::IsFinite(Base) || !VoxelMath::IsFinite(Seal)) { return FLT_MAX; } const float AirTarget = -(Base * 2.0f + Seal + 4.0f); if (!VoxelMath::IsFinite(AirTarget)) { return FLT_MAX; } const float LowerAfterCarve = FMath::Min( H.SolidMargin, FMath::Lerp(H.SolidMargin, AirTarget, FMath::Clamp(MaxFactor, 0.0f, 1.0f))); if (!VoxelMath::IsFinite(LowerAfterCarve)) { return FLT_MAX; } return FMath::Max(0.0f, H.SolidMargin - LowerAfterCarve); } float MaxFillOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override { return EffectOverBox(VoxelBox, Ctx) == EVoxelOpEffect::Both ? FLT_MAX : 0.0f; } const TCHAR* DebugName() const override { return TEXT("PassageCarveOp"); } void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } private: TWeakObjectPtr Manager; float Base, Seal, TopZ, BotZ, SpineRadius; }; //========================================================================= // RÔLE 1 — SOURCE : PUITS VERTICAUX / VERTICAL SHAFTS //========================================================================= // Cylindres infinis sur une grille XY jitterée + un arbre de drainage déterministe, complété // par des connecteurs horizontaux entre paires proches ouverts par un hash de paire. Écrit le // canal SDF uniquement. // // ⚠️ ÉCART ASSUMÉ AVEC `OPSTACK-DECOMPOSITION §6`, qui suggérait DEUX sources (colonnes XY-pures // + connecteurs) pour que la moitié cylindrique reçoive le traitement du cache de colonne et un // `ClassifyBox` exact en XY. Gardé en UN opérateur, et voici pourquoi : // • les connecteurs se dérivent de la MÊME liste inner 3×3 que les puits (il faut les paires), donc // séparer imposerait soit de rouler les cellules deux fois, soit un cache partagé entre // deux ops — c'est-à-dire la complexité qu'on voulait éviter ; // • le `FShaftLedgeMod` en aval a de toute façon besoin de la liste des puits, donc il faut // l'exposer depuis une source ; l'exposer depuis deux serait pire. // Ce qui est perdu : le verdict de boîte exact sur la seule moitié cylindrique. Le compromis // actuel est une propagation d'intervalle qui couvre cercles ET capsules, tandis que le // `EffectOverBox` intrinsèque reste Identity : le convertisseur ou consommateur aval décide // séparément de son propre effet. // // Kept as ONE op against §6's suggestion: the per-voxel connectors derive from the same inner // 3×3 roll as the shafts, while a rebuild-only 9×9 collection resolves each 5×5 tree window // and fixed ±3 fallback; // the downstream ledge mod needs the inner shaft list anyway. What is forfeited is an exact XY // box verdict on the cylinder half alone. class FShaftFieldSource final : public IVoxelDensityOp { public: FShaftFieldSource(const FVerticalShaftParams& InP, int32 Seed, float InSpineRadius) : P(InP), Salt((uint32)Seed ^ 0x53686674u) // 'Shft' , SpineRadius(InSpineRadius) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::None; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Sdf; } EVoxelOpResourceMask ProvidedResources() const override { return VoxelOpResources::ShaftGeometry; } bool IsAdditive() const override { return false; } void PrepareChunk(const FVoxelOpContext&) override {} struct FShaft { float X, Y, R; int32 CellX, CellY; bool bOriginSpine; }; struct FConn { FVector A, B; float Radius = 0.0f; float FloorZ = 0.0f; bool bWalkableFloor = false; }; // ⚠️ DÉCLARÉE ICI, avant toute fonction qui la renvoie. Un type imbriqué doit exister au // moment où le COMPILATEUR lit la SIGNATURE — les corps de méthodes sont différés, pas les // types de retour. La mettre en bas de la classe donne un C4430 « int par défaut » suivi // d'une cascade illisible, ce qui masque une cause pourtant triviale. // Declared here, before any function returning it: a nested type must exist when the // compiler reads the SIGNATURE — bodies are deferred, return types are not. struct FCells { TArray> Shafts; TArray> Conns; }; void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { const FCells& C = GetCells(WorldX, WorldY); float CaveSDF = FLT_MAX; for (const FShaft& Sh : C.Shafts) { if (Sh.bOriginSpine) continue; // VF_ApplyOriginSpine owns the structural column. const float DX = WorldX - Sh.X; const float DY = WorldY - Sh.Y; CaveSDF = FMath::Min(CaveSDF, FMath::Sqrt(DX * DX + DY * DY) - Sh.R); } const FVector Pos(WorldX, WorldY, WorldZ); for (const FConn& Cn : C.Conns) { CaveSDF = FMath::Min(CaveSDF, VoxelSDF::Capsule(Pos, Cn.A, Cn.B, Cn.Radius)); } InOut.Sdf = CaveSDF; } /** La liste des puits proches — `FShaftLedgeMod` doit trouver le PLUS PROCHE pour ne * poser d'étagère que sur sa moitié +X/+Y. Même motif que colonne → overhang. */ const FCells& GetCellsAt(float WorldX, float WorldY) const { return GetCells(WorldX, WorldY); } EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override { // This source writes SDF only. A converter or other SDF consumer must inspect the // propagated interval; the source has no density effect in isolation. return EVoxelOpEffect::Identity; } /** * Propagate the source's own SDF interval. Eval selects the inner 3x3 cell neighbourhood, * so enumerating every cell touched by the box and asking GetCells at that cell centre is an * exact superset. Shaft axes are infinite in Z; connector capsules use their segment AABB * as a conservative distance lower bound. */ void PropagateSdfOverBox(FVoxelBoxSdfInterval& InOut, const FBox& VoxelBox, const FVoxelOpContext&) const override { if (!VoxelMath::IsFinite((float)VoxelBox.Min.X) || !VoxelMath::IsFinite((float)VoxelBox.Min.Y) || !VoxelMath::IsFinite((float)VoxelBox.Min.Z) || !VoxelMath::IsFinite((float)VoxelBox.Max.X) || !VoxelMath::IsFinite((float)VoxelBox.Max.Y) || !VoxelMath::IsFinite((float)VoxelBox.Max.Z) || VoxelBox.Min.X > VoxelBox.Max.X || VoxelBox.Min.Y > VoxelBox.Max.Y || VoxelBox.Min.Z > VoxelBox.Max.Z || !VoxelMath::IsFinite(P.ShaftSpacing) || P.ShaftSpacing <= 0.0f) { InOut.SetUnknown(); return; } const float Spacing = FMath::Max(P.ShaftSpacing, 1.0f); const int32 CX0 = FMath::FloorToInt((float)VoxelBox.Min.X / Spacing); const int32 CY0 = FMath::FloorToInt((float)VoxelBox.Min.Y / Spacing); const int32 CX1 = FMath::FloorToInt((float)VoxelBox.Max.X / Spacing); const int32 CY1 = FMath::FloorToInt((float)VoxelBox.Max.Y / Spacing); constexpr int64 MaxCellsScanned = 64 * 64; const int64 CellCount = ((int64)CX1 - CX0 + 1) * ((int64)CY1 - CY0 + 1); if (CellCount <= 0 || CellCount > MaxCellsScanned) { InOut.SetUnknown(); return; } float Lower = FLT_MAX; bool bAnyPrimitive = false; for (int32 cy = CY0; cy <= CY1; ++cy) for (int32 cx = CX0; cx <= CX1; ++cx) { const FCells& C = GetCells((cx + 0.5f) * Spacing, (cy + 0.5f) * Spacing); for (const FShaft& Sh : C.Shafts) { if (Sh.bOriginSpine) { continue; } bAnyPrimitive = true; Lower = FMath::Min(Lower, VF_DistanceBoxToPointXY(VoxelBox, Sh.X, Sh.Y) - FMath::Abs(Sh.R)); } for (const FConn& Conn : C.Conns) { bAnyPrimitive = true; const FVector ConnMin( FMath::Min(Conn.A.X, Conn.B.X), FMath::Min(Conn.A.Y, Conn.B.Y), FMath::Min(Conn.A.Z, Conn.B.Z)); const FVector ConnMax( FMath::Max(Conn.A.X, Conn.B.X), FMath::Max(Conn.A.Y, Conn.B.Y), FMath::Max(Conn.A.Z, Conn.B.Z)); Lower = FMath::Min(Lower, VF_DistanceBetweenBoxes(VoxelBox.Min, VoxelBox.Max, ConnMin, ConnMax) - FMath::Abs(Conn.Radius)); } } if (bAnyPrimitive) { InOut.Set(Lower, FLT_MAX); } else { InOut.Set(FLT_MAX, FLT_MAX); } } private: /** Tirage d'une cellule. PURE en (cellule, seed, params) ⇒ `Eval` et `EffectOverBox` ne * peuvent pas voir des puits différents. */ bool RollShaft(int32 nx, int32 ny, FShaft& Out) const { const float Spacing = FMath::Max(P.ShaftSpacing, 1.0f); const uint32 Hh = VoxelHash::Cell(nx, ny, Salt); if (VoxelHash::ToFloat01(Hh) > P.ShaftDensity) { return false; } const float JX = VoxelHash::ToFloat01(VoxelHash::Mix(Hh ^ 0x12345678u)); const float JY = VoxelHash::ToFloat01(VoxelHash::Mix(Hh ^ 0x9ABCDEF0u)); Out.X = (nx + 0.15f + JX * 0.7f) * Spacing; Out.Y = (ny + 0.15f + JY * 0.7f) * Spacing; Out.R = FMath::Lerp(P.ShaftMinRadius, P.ShaftMaxRadius, VoxelHash::ToFloat01(VoxelHash::Mix(Hh ^ 0xBEEFu))); Out.CellX = nx; Out.CellY = ny; Out.bOriginSpine = false; return true; } /** Spine links use a radius above the repository's proven sup|FBM|=1.5 roughness * envelope, so the roughness pass cannot pinch their centreline shut. */ float SpineConnectorRadius() const { constexpr float RoughnessAbsBound = 1.5f; const float RoughnessReach = FMath::Max(P.SurfaceRoughness, 0.0f) * VOXEL_NOISE_SCALE * RoughnessAbsBound; return FMath::Max(P.ConnectorRadius, RoughnessReach + 1.0f); } /** Le voisinage inner 3×3 + ses connecteurs, mémoïsés par worker. Le rebuild collecte un * halo 9×9 émetteur / 15×15 total pour résoudre les fenêtres parent 5×5 et le fallback * fixe ±3. Clé = cellule + tous les params qui influent : stable à travers les * reconstructions de pile, ce qui est la leçon retenue du mémo de colonne de SurfaceWorld. */ const FCells& GetCells(float WorldX, float WorldY) const { const float Spacing = FMath::Max(P.ShaftSpacing, 1.0f); const int32 CX = FMath::FloorToInt(WorldX / Spacing); const int32 CY = FMath::FloorToInt(WorldY / Spacing); thread_local FCells Cache; thread_local int32 VS_CX = INT32_MAX, VS_CY = INT32_MAX; thread_local uint32 VS_Salt = 0xFFFFFFFFu; thread_local float VS_Spacing = -1.0f, VS_Dens = -1.0f, VS_MinR = -1.0f, VS_MaxR = -1.0f, VS_Cross = -1.0f, VS_ConnR = -1.0f, VS_Rough = -1.0f, VS_SpineR = -1.0f, VS_BotZ = FLT_MAX, VS_TopZ = FLT_MAX, VS_Seal = -1.0f, VS_LedgeSpacing = -1.0f, VS_LedgeDepth = -1.0f; if (CX != VS_CX || CY != VS_CY || Salt != VS_Salt || Spacing != VS_Spacing || P.ShaftDensity != VS_Dens || P.ShaftMinRadius != VS_MinR || P.ShaftMaxRadius != VS_MaxR || P.CrossConnectChance != VS_Cross || P.ConnectorRadius != VS_ConnR || P.SurfaceRoughness != VS_Rough || SpineRadius != VS_SpineR || P.StrateBottomWorldZ != VS_BotZ || P.StrateTopWorldZ != VS_TopZ || P.BoundarySealThickness != VS_Seal || P.LedgeSpacing != VS_LedgeSpacing || P.LedgeDepth != VS_LedgeDepth) { VS_CX = CX; VS_CY = CY; VS_Salt = Salt; VS_Spacing = Spacing; VS_Dens = P.ShaftDensity; VS_MinR = P.ShaftMinRadius; VS_MaxR = P.ShaftMaxRadius; VS_Cross = P.CrossConnectChance; VS_ConnR = P.ConnectorRadius; VS_Rough = P.SurfaceRoughness; VS_SpineR = SpineRadius; VS_BotZ = P.StrateBottomWorldZ; VS_TopZ = P.StrateTopWorldZ; VS_Seal = P.BoundarySealThickness; VS_LedgeSpacing = P.LedgeSpacing; VS_LedgeDepth = P.LedgeDepth; Cache.Shafts.Reset(); Cache.Conns.Reset(); constexpr int32 EmitRadius = 1; // 3×3: shaft cylinders and random links constexpr int32 CandidateRadius = 2; // 5×5 parent window around an emitting shaft constexpr int32 FallbackRadius = 3; // fixed fallback window around an emitting shaft constexpr uint32 TreeSalt = 0x7A11u; const float BottomZ = P.StrateBottomWorldZ + P.BoundarySealThickness; const float TopZ = P.StrateTopWorldZ - P.BoundarySealThickness; const float TreeRadius = SpineConnectorRadius(); // A tree capsule can affect a cell even when neither endpoint is in that cell. // Keep a rebuild-only child halo for that geometric reach, then add the // parent-search halo. With the defaults this is 4 + 3 cells = 15×15 rolls. const int32 ConnectorCellPad = FMath::Max( 1, FMath::CeilToInt(TreeRadius / Spacing)); const int32 TreeEmitRadius = FallbackRadius + ConnectorCellPad; const int32 CollectRadius = TreeEmitRadius + FallbackRadius; const int32 CollectSide = CollectRadius * 2 + 1; TArray> ShaftGrid; TArray> ShaftPresent; TArray> TreeEmitShafts; ShaftGrid.SetNum(CollectSide * CollectSide); ShaftPresent.Init(0, CollectSide * CollectSide); // Rebuild-only wide collection. `Cache.Shafts` remains the inner 3×3 working set // used by Eval and the ledge modifier; the wider tree work is not paid per voxel. for (int32 dy = -CollectRadius; dy <= CollectRadius; dy++) for (int32 dx = -CollectRadius; dx <= CollectRadius; dx++) { FShaft Sh; if (!RollShaft(CX + dx, CY + dy, Sh)) { continue; } const int32 GridIndex = (dy + CollectRadius) * CollectSide + (dx + CollectRadius); ShaftGrid[GridIndex] = Sh; ShaftPresent[GridIndex] = 1; if (FMath::Abs(dx) <= EmitRadius && FMath::Abs(dy) <= EmitRadius) { Cache.Shafts.Add(Sh); } if (FMath::Abs(dx) <= TreeEmitRadius && FMath::Abs(dy) <= TreeEmitRadius) { TreeEmitShafts.Add(Sh); } } auto FindCollectedShaft = [&](int32 CellX, int32 CellY) -> const FShaft* { const int32 dx = CellX - CX; const int32 dy = CellY - CY; if (FMath::Abs(dx) > CollectRadius || FMath::Abs(dy) > CollectRadius) { return nullptr; } const int32 GridIndex = (dy + CollectRadius) * CollectSide + (dx + CollectRadius); return ShaftPresent[GridIndex] ? &ShaftGrid[GridIndex] : nullptr; }; const bool bOriginNearby = FMath::Abs(CX) <= 1 && FMath::Abs(CY) <= 1; if (bOriginNearby && SpineRadius > 0.0f) { Cache.Shafts.Add({0.0f, 0.0f, SpineRadius, 0, 0, true}); } const float CellMinX = static_cast(CX) * Spacing; const float CellMaxX = static_cast(CX + 1) * Spacing; const float CellMinY = static_cast(CY) * Spacing; const float CellMaxY = static_cast(CY + 1) * Spacing; auto ConnectorMayReachCell = [&](const FConn& Conn) { const float MinX = FMath::Min(Conn.A.X, Conn.B.X) - Conn.Radius; const float MaxX = FMath::Max(Conn.A.X, Conn.B.X) + Conn.Radius; const float MinY = FMath::Min(Conn.A.Y, Conn.B.Y) - Conn.Radius; const float MaxY = FMath::Max(Conn.A.Y, Conn.B.Y) + Conn.Radius; const float GapX = FMath::Max3(CellMinX - MaxX, MinX - CellMaxX, 0.0f); const float GapY = FMath::Max3(CellMinY - MaxY, MinY - CellMaxY, 0.0f); return GapX == 0.0f && GapY == 0.0f; }; auto EmitTreeConnector = [&](const FShaft& Child, const FShaft* Parent) { const float Zc = VoxelPassageGeometry::VerticalShaftConnectorCenterZ( P.StrateBottomWorldZ, BottomZ, TopZ, P.LedgeSpacing, P.LedgeDepth, TreeRadius); const float FloorZ = VoxelPassageGeometry::VerticalShaftConnectorFloorZ( P.StrateBottomWorldZ, BottomZ, TopZ, P.LedgeSpacing, P.LedgeDepth, TreeRadius); const FVector ParentPoint = Parent != nullptr ? FVector(Parent->X, Parent->Y, Zc) : FVector(0.0f, 0.0f, Zc); const FConn Conn{ FVector(Child.X, Child.Y, Zc), ParentPoint, TreeRadius, FloorZ, true }; if (ConnectorMayReachCell(Conn)) { Cache.Conns.Add(Conn); } }; // Structural drainage tree. Every real shaft in the rebuild-only child window // first chooses the nearest strictly more-central shaft in its complete 5×5 // window. A local minimum falls back to the nearest lower-origin shaft in the // deterministic halo; a window touching the origin uses the spine directly, with // a final direct-spine fallback for an unusually empty finite halo. Every parent // edge therefore either strictly decreases origin distance or terminates at the // spine: no cycles and no orphans. Spatial culling makes the same edge visible // throughout its capsule, independent of which cache cell evaluates it. for (const FShaft& Child : TreeEmitShafts) { if (Child.bOriginSpine) continue; const float ChildOriginSq = FMath::Square(Child.X) + FMath::Square(Child.Y); const FShaft* Parent = nullptr; float BestDistanceSq = FLT_MAX; for (int32 dy = -CandidateRadius; dy <= CandidateRadius; ++dy) for (int32 dx = -CandidateRadius; dx <= CandidateRadius; ++dx) { const FShaft* Candidate = FindCollectedShaft( Child.CellX + dx, Child.CellY + dy); if (Candidate == nullptr || (Candidate->CellX == Child.CellX && Candidate->CellY == Child.CellY)) { continue; } const float CandidateOriginSq = FMath::Square(Candidate->X) + FMath::Square(Candidate->Y); if (!(CandidateOriginSq < ChildOriginSq)) { continue; } const float DistanceSq = FMath::Square(Child.X - Candidate->X) + FMath::Square(Child.Y - Candidate->Y); const bool bLowerCell = Parent == nullptr || Candidate->CellY < Parent->CellY || (Candidate->CellY == Parent->CellY && Candidate->CellX < Parent->CellX); if (DistanceSq < BestDistanceSq || (DistanceSq == BestDistanceSq && bLowerCell)) { BestDistanceSq = DistanceSq; Parent = Candidate; } } if (Parent == nullptr && !(FMath::Abs(Child.CellX) <= CandidateRadius && FMath::Abs(Child.CellY) <= CandidateRadius)) { // Specified neighbour-shaft fallback. The lower-origin restriction is the // proof that this fallback cannot create a cycle. for (int32 dy = -FallbackRadius; dy <= FallbackRadius; ++dy) for (int32 dx = -FallbackRadius; dx <= FallbackRadius; ++dx) { const FShaft* Candidate = FindCollectedShaft( Child.CellX + dx, Child.CellY + dy); if (Candidate == nullptr || (Candidate->CellX == Child.CellX && Candidate->CellY == Child.CellY)) { continue; } const float CandidateOriginSq = FMath::Square(Candidate->X) + FMath::Square(Candidate->Y); if (!(CandidateOriginSq < ChildOriginSq)) { continue; } const float DistanceSq = FMath::Square(Child.X - Candidate->X) + FMath::Square(Child.Y - Candidate->Y); const bool bLowerCell = Parent == nullptr || Candidate->CellY < Parent->CellY || (Candidate->CellY == Parent->CellY && Candidate->CellX < Parent->CellX); if (DistanceSq < BestDistanceSq || (DistanceSq == BestDistanceSq && bLowerCell)) { BestDistanceSq = DistanceSq; Parent = Candidate; } } } EmitTreeConnector(Child, Parent); } // Existing random links remain the texture/loop layer and stay on the inner 3×3 // set. Only the deterministic tree build uses the wider 9×9/15×15 collection. if (P.CrossConnectChance > 0.0f && Cache.Shafts.Num() >= 2) { for (int32 i = 0; i < Cache.Shafts.Num(); i++) for (int32 j = i + 1; j < Cache.Shafts.Num(); j++) { const FShaft& A = Cache.Shafts[i]; const FShaft& B = Cache.Shafts[j]; const float DSq = FMath::Square(A.X - B.X) + FMath::Square(A.Y - B.Y); if (DSq > FMath::Square(Spacing * 1.6f)) { continue; } const uint32 PH = VoxelHash::Pair( FMath::RoundToInt(A.X), FMath::RoundToInt(A.Y), FMath::RoundToInt(B.X), FMath::RoundToInt(B.Y), Salt ^ 0xC04Eu); const bool bSpineConnector = A.bOriginSpine || B.bOriginSpine; if (VoxelHash::ToFloat01(PH) >= P.CrossConnectChance) { continue; } const float Zc = FMath::Lerp(BottomZ, TopZ, VoxelHash::ToFloat01(VoxelHash::Mix(PH))); const float ConnectorR = bSpineConnector ? SpineConnectorRadius() : P.ConnectorRadius; const FConn Conn{ FVector(A.X, A.Y, Zc), FVector(B.X, B.Y, Zc), ConnectorR, 0.0f, false }; if (ConnectorMayReachCell(Conn)) { Cache.Conns.Add(Conn); } } } } return Cache; } FVerticalShaftParams P; uint32 Salt; float SpineRadius; }; //========================================================================= // RÔLE 3 — MODIFIER : ÉTAGÈRES DE PUITS / SHAFT LEDGES //========================================================================= // Des tablettes fines à intervalles réguliers en Z, posées UNIQUEMENT sur la moitié +X/+Y du // puits le plus proche — pour que la moitié opposée reste libre et le puits franchissable. // C'est un op FORÇANT (`Max`) : il ne peut qu'ajouter du solide. class FShaftLedgeMod final : public IVoxelDensityOp { public: FShaftLedgeMod(const FVerticalShaftParams& InP, const FShaftFieldSource* InField) : P(InP), Field(InField) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density | VoxelOpChannels::Sdf; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::ShaftGeometry; } bool IsAdditive() const override { return false; } void PrepareChunk(const FVoxelOpContext&) override {} void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { if (P.LedgeSpacing <= 0.0f || P.LedgeDepth <= 0.0f || Field == nullptr) { return; } // ⚠️ La garde est `Sdf < 0` — le SDF APRÈS rugosité, tel que la pile l'a laissé. C'est // bien `InOut.Sdf` ici, pas une re-évaluation : re-calculer donnerait le SDF SANS // rugosité et déplacerait les étagères. if (InOut.Sdf >= 0.0f) { return; } const float Phase = FMath::Frac((WorldZ - P.StrateBottomWorldZ) / P.LedgeSpacing); const float BandT = FMath::Min(Phase, 1.0f - Phase) * P.LedgeSpacing; if (BandT >= P.LedgeDepth) { return; } const FShaftFieldSource::FCells& C = Field->GetCellsAt(WorldX, WorldY); if (C.Shafts.Num() == 0) { return; } const FShaftFieldSource::FShaft* Near = nullptr; float BestSq = FLT_MAX; for (const FShaftFieldSource::FShaft& Sh : C.Shafts) { if (Sh.bOriginSpine) continue; // ledges belong to real shafts, not the post. const float D2 = FMath::Square(WorldX - Sh.X) + FMath::Square(WorldY - Sh.Y); if (D2 < BestSq) { BestSq = D2; Near = &Sh; } } // The shaft axis is the structural tree's terminal point. Keep the mathematical // half-plane boundary open: tiny cancellation error must not turn an exact axis // landing into a solid ledge cap and sever the parent capsule. if (Near && (WorldX - Near->X) + (WorldY - Near->Y) > 1.0e-3f) { const float Shelf = 1.0f - SmoothStep01(BandT / P.LedgeDepth); InOut.Density = FMath::Max(InOut.Density, Shelf * P.BaseDensity); } } // N'ajoute que du solide ⇒ tue AllAir, jamais AllSolid. EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override { return (Field != nullptr && P.LedgeSpacing > 0.0f && P.LedgeDepth > 0.0f) ? EVoxelOpEffect::FillOnly : EVoxelOpEffect::Identity; } EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx, const FVoxelBoxHypotheses& H) const override { if (Field == nullptr || P.LedgeSpacing <= 0.0f || P.LedgeDepth <= 0.0f) { return EVoxelOpEffect::Identity; } return H.Sdf.IsKnown() && H.Sdf.Min >= 0.0f ? EVoxelOpEffect::Identity : EVoxelOpEffect::FillOnly; } private: FVerticalShaftParams P; const FShaftFieldSource* Field; // NON possédant : la pile possède la source }; // The deterministic shaft tree is a walkable network as well as an air topology. Reassert // its air core after partial shaft ledges, then add the same inset floor band as the native // path at the common first ledge level. This lets a player-fit route cross between shafts // without restoring the removed landing-to-origin roads. class FShaftConnectorAirMod final : public IVoxelDensityOp { public: explicit FShaftConnectorAirMod(const FShaftFieldSource* InField, float InBaseDensity) : Field(InField), BaseDensity(InBaseDensity) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::ShaftGeometry; } bool IsAdditive() const override { return false; } void PrepareChunk(const FVoxelOpContext&) override {} void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { if (Field == nullptr) return; const FShaftFieldSource::FCells& Cells = Field->GetCellsAt(WorldX, WorldY); for (const FShaftFieldSource::FConn& Conn : Cells.Conns) { if (!Conn.bWalkableFloor) continue; VoxelPassageGeometry::VF_ApplyVerticalShaftConnectorAir( InOut.Density, WorldX, WorldY, WorldZ, Conn.A, Conn.B, Conn.Radius, Conn.FloorZ, BaseDensity); } } EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override { return Field != nullptr ? EVoxelOpEffect::Both : EVoxelOpEffect::Identity; } const TCHAR* DebugName() const override { return TEXT("ShaftConnectorAirOp"); } private: const FShaftFieldSource* Field = nullptr; // NON possédant : la pile possède la source float BaseDensity = 8.0f; }; class FShaftConnectorFloorMod final : public IVoxelDensityOp { public: FShaftConnectorFloorMod(const FShaftFieldSource* InField, float InBaseDensity) : Field(InField), BaseDensity(InBaseDensity) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::ShaftGeometry; } bool IsAdditive() const override { return false; } void PrepareChunk(const FVoxelOpContext&) override {} void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { if (Field == nullptr) return; const FShaftFieldSource::FCells& Cells = Field->GetCellsAt(WorldX, WorldY); for (const FShaftFieldSource::FConn& Conn : Cells.Conns) { if (!Conn.bWalkableFloor) continue; VoxelPassageGeometry::VF_ApplyVerticalShaftConnectorFloor( InOut.Density, WorldX, WorldY, WorldZ, Conn.A, Conn.B, Conn.Radius, Conn.FloorZ, BaseDensity); } } EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override { return Field != nullptr ? EVoxelOpEffect::FillOnly : EVoxelOpEffect::Identity; } const TCHAR* DebugName() const override { return TEXT("ShaftConnectorFloorOp"); } private: const FShaftFieldSource* Field = nullptr; // NON possédant : la pile possède la source float BaseDensity = 8.0f; }; //========================================================================= // RÔLE 1 — SOURCE : ÎLES FLOTTANTES / FLOATING ISLAND BLOBS //========================================================================= // Le SEUL archétype dont la source est de l'AIR : `FConstantFieldSource(-BaseDensity)` pose un // grand vide, et cet opérateur y suspend des blobs. C'est ce qui en fait le bon test de // composition — tous les autres portages partent de roc et creusent. // // FORME D'UNE ÎLE : une dalle assez plate au-dessus du centre (`TopHalf = 0.20·Rxy`) et un // dessous qui s'effile vers une pointe (`ThicknessRatio·Rxy`). C'est l'asymétrie qui se lit // comme une île flottante plutôt que comme une sphère. // // ⚠️ ÉCART ASSUMÉ AVEC `OPSTACK-DECOMPOSITION §7`, qui décrivait un `FRAME IslandWarp` enveloppant // la source. Le warp reste À L'INTÉRIEUR de l'opérateur, et c'est délibéré : `§7` compte trois // usages de frames (îles, caves de TunnelNetwork, tunnels), et aucun n'en a besoin. Voir // `BuildTunnelNetworkStack` : `CaveWarp` n'enveloppe qu'UN opérateur (donc c'est une variable // locale, pas un frame) et `VerticalScale` est une fonction pure d'un scalaire. **Zéro frame // sur trois candidats.** Ne pas rouvrir : le warp reste local ICI pour la même raison qu'il // est resté local là-bas. On n'abstrait qu'à la deuxième occurrence réelle (cf. // `IVoxelBiomeField`, né d'un besoin réel). // // The warp stays INSIDE the op against §7's FRAME suggestion. TunnelNetwork is ported, and the // second real use dissolved the question rather than settling it. See BuildTunnelNetworkStack: // CaveWarp wraps exactly ONE operator (a local variable, not a frame) and VerticalScale is a // pure function of a scalar. ZERO frames out of three candidates. Do not reopen. class FIslandBlobSource final : public IVoxelDensityOp { public: FIslandBlobSource(const FFloatingIslandParams& InP, int32 Seed) : P(InP), Salt((uint32)Seed ^ 0x49736C64u) // 'Isld' {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::None; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Sdf; } bool IsAdditive() const override { return false; } void PrepareChunk(const FVoxelOpContext&) override {} struct FIsland { float X, Y, Rxy, TopHalf, TopZ, BotZ, TaperEnd; }; // ⚠️ DÉCLARÉE ICI, avant toute fonction qui la renvoie — même piège que `FShaftFieldSource` // (C4430 : les corps de méthodes sont différés, les types de retour non). struct FCells { TArray> Islands; }; void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { const float BlendK = FMath::Max(P.SDFBlendRadius, 0.01f); const FCells& C = GetCells(WorldX, WorldY); // CONTOUR IRRÉGULIER : on déforme la requête HORIZONTALE pour que les bords des îles // soient lobés au lieu d'être des cercles parfaits. Calculé une fois par voxel et // partagé par toutes les îles proches — chacune échantillonne une autre partie du champ, // d'où des silhouettes distinctes. const float WarpAmp = (P.IslandMinRadius + P.IslandMaxRadius) * 0.5f * 0.35f; const float WX = WorldX + HFractal3D(FVector3f(WorldX * 0.04f + VoxelHash::SeedOffset(Salt, 0.0007f), WorldY * 0.04f, WorldZ * 0.012f), VoxelGenLOD::Eff(3)) * VOXEL_NOISE_SCALE * WarpAmp; const float WY = WorldY + HFractal3D(FVector3f(WorldX * 0.04f + 31.0f, WorldY * 0.04f + 7.0f, WorldZ * 0.012f), VoxelGenLOD::Eff(3)) * VOXEL_NOISE_SCALE * WarpAmp; float IslandSDF = FLT_MAX; for (const FIsland& Isl : C.Islands) { // Distance horizontale dans le repère DÉFORMÉ, donc le contour n'est pas un cercle. const float Dxw = WX - Isl.X, Dyw = WY - Isl.Y; const float DistXY = FMath::Sqrt(Dxw * Dxw + Dyw * Dyw); // Enveloppe de rayon par la hauteur : pleine largeur en haut, resserrée jusqu'à une // pointe en bas (taper SmoothStep). const float Hgt = FMath::Clamp((WorldZ - Isl.BotZ) / FMath::Max(Isl.TopZ - Isl.BotZ, 1.0f), 0.0f, 1.0f); const float Taper = SmoothStep01(FMath::Clamp(Hgt / Isl.TaperEnd, 0.0f, 1.0f)); const float Env = Isl.Rxy * Taper; // Surface du dessus : plate par défaut ; les bords retombent en dôme si TopFlatten < 1. float TopSurf = Isl.TopZ; if (P.TopFlatten < 1.0f) { const float Edge = FMath::Clamp(DistXY / FMath::Max(Isl.Rxy, 1.0f), 0.0f, 1.0f); TopSurf = Isl.TopZ - (1.0f - P.TopFlatten) * Isl.TopHalf * 2.0f * Edge * Edge; } // Pseudo-SDF : dehors si au-delà de l'enveloppe radiale OU au-dessus du dessus. const float Sdf = FMath::Max(DistXY - Env, WorldZ - TopSurf); IslandSDF = VoxelSDF::SmoothMin(IslandSDF, Sdf, BlendK); } InOut.Sdf = IslandSDF; } /** * Cette source écrit uniquement le canal SDF : son `EffectOverBox` intrinsèque est donc * `Identity`. Le convertisseur de remplissage consomme ensuite l'intervalle publié ; sur une * strate d'îles, l'absence d'île prouvée est le cas COURANT et, combinée à l'`AllAir` de la * source constante, permet enfin de prouver « tout air » (`OPSTACK-DECOMPOSITION §7`). * * BORNE, et pourquoi elle est sûre dans les deux directions : * • en XY, `Sdf ≥ DistXY − Rxy` (l'enveloppe ne dépasse jamais `Rxy`), et le warp déplace * le POINT de `WarpAmp · VOXEL_NOISE_SCALE · √2` au plus (la borne prouvée de chaque * FBM est 1.5 sur DEUX axes indépendants — voir la note √2 dans le corps) ; * • en Z, `Sdf ≥ WorldZ − TopSurf ≥ WorldZ − TopZ`, donc au-dessus du sommet + marge il * n'y a plus rien à faire. **En dessous, il n'y a PAS de borne** : sous une île, le SDF * vaut ≈ `DistXY` à toute profondeur, donc un mince fil de matière descend le long de * l'axe. Le confondre avec « rien en dessous » serait un TROU, et c'est pourquoi seule * la borne HAUTE est testée. * • cette source ne couvre pas l'aval : elle publie seulement son propre intervalle SDF. * La rugosité, le blend du fill et le creux du SmoothMin sont bornés par leurs propres * opérateurs dans le pliage, chacun avec son contrat isolé. */ EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override { // This source writes SDF only. Island filling is the converter's responsibility. return EVoxelOpEffect::Identity; } /** * Propagate a source-only lower bound. The exact 3x3 neighbourhood used by Eval is * enumerated for every XY cell touched by the box. The warp, authored radius endpoints, * top envelope, and the bounded SmoothMin dip are all included in the bound. */ void PropagateSdfOverBox(FVoxelBoxSdfInterval& InOut, const FBox& VoxelBox, const FVoxelOpContext&) const override { if (!VoxelMath::IsFinite((float)VoxelBox.Min.X) || !VoxelMath::IsFinite((float)VoxelBox.Min.Y) || !VoxelMath::IsFinite((float)VoxelBox.Min.Z) || !VoxelMath::IsFinite((float)VoxelBox.Max.X) || !VoxelMath::IsFinite((float)VoxelBox.Max.Y) || !VoxelMath::IsFinite((float)VoxelBox.Max.Z) || VoxelBox.Min.X > VoxelBox.Max.X || VoxelBox.Min.Y > VoxelBox.Max.Y || VoxelBox.Min.Z > VoxelBox.Max.Z || !VoxelMath::IsFinite(P.IslandSpacing) || !VoxelMath::IsFinite(P.IslandDensity) || !VoxelMath::IsFinite(P.IslandMinRadius) || !VoxelMath::IsFinite(P.IslandMaxRadius) || !VoxelMath::IsFinite(P.ThicknessRatio) || !VoxelMath::IsFinite(P.VerticalJitter) || !VoxelMath::IsFinite(P.TopFlatten) || !VoxelMath::IsFinite(P.SDFBlendRadius) || P.IslandDensity < 0.0f) { InOut.SetUnknown(); return; } if (P.IslandDensity <= 0.0f) { InOut.Set(FLT_MAX, FLT_MAX); return; } const float Spacing = FMath::Max(P.IslandSpacing, 1.0f); const int32 CX0 = FMath::FloorToInt((float)VoxelBox.Min.X / Spacing); const int32 CY0 = FMath::FloorToInt((float)VoxelBox.Min.Y / Spacing); const int32 CX1 = FMath::FloorToInt((float)VoxelBox.Max.X / Spacing); const int32 CY1 = FMath::FloorToInt((float)VoxelBox.Max.Y / Spacing); constexpr int64 MaxCellsScanned = 64 * 64; const int64 CellCount = ((int64)CX1 - CX0 + 1) * ((int64)CY1 - CY0 + 1); if (CellCount <= 0 || CellCount > MaxCellsScanned) { InOut.SetUnknown(); return; } constexpr float Sqrt2 = 1.4142136f; const float WarpAmp = (P.IslandMinRadius + P.IslandMaxRadius) * 0.5f * 0.35f; const float WarpDiag = FMath::Abs(WarpAmp) * VOXEL_NOISE_SCALE * VF_PerlinAbsBound * Sqrt2; const float RadiusUpper = FMath::Max3(P.IslandMinRadius, P.IslandMaxRadius, 0.0f); // For the authored TopFlatten range [0,1], TopSurf never exceeds TopZ. Keeping the // positive general-range term makes the proof remain safe if a data asset is widened. const float TopSurfExtra = FMath::Max(P.TopFlatten - 1.0f, 0.0f) * RadiusUpper * 0.20f * 2.0f; float Lower = FLT_MAX; bool bAnyIsland = false; const float BlendK = FMath::Max(P.SDFBlendRadius, 0.01f); for (int32 cy = CY0; cy <= CY1; ++cy) for (int32 cx = CX0; cx <= CX1; ++cx) { const FCells& C = GetCells((cx + 0.5f) * Spacing, (cy + 0.5f) * Spacing); for (const FIsland& Isl : C.Islands) { bAnyIsland = true; const float RadialLower = VF_DistanceBoxToPointXY(VoxelBox, Isl.X, Isl.Y) - WarpDiag - FMath::Max(Isl.Rxy, 0.0f); const float TopLower = (float)VoxelBox.Min.Z - Isl.TopZ - TopSurfExtra; const float IslandLower = FMath::Max(RadialLower, TopLower); Lower = FMath::Min(Lower, IslandLower); } } if (!bAnyIsland) { InOut.Set(FLT_MAX, FLT_MAX); return; } // SmoothMin can dip below the smallest raw term, but its total deficit is bounded by // one blend radius: once the running minimum is a blend radius below another term, // the next penalty is zero. This is a lower bound for any number of islands. InOut.Set(VF_SaturatingAdd(Lower, -BlendK), FLT_MAX); } private: /** Tirage d'une cellule. PURE en (cellule, seed, params) ⇒ `Eval` et `EffectOverBox` ne * peuvent pas voir des îles différentes. */ bool RollIsland(int32 nx, int32 ny, FIsland& Out) const { const float H = P.StrateTopWorldZ - P.StrateBottomWorldZ; const float Spacing = FMath::Max(P.IslandSpacing, 1.0f); const float MidZ = (P.StrateTopWorldZ + P.StrateBottomWorldZ) * 0.5f; const uint32 Hh = VoxelHash::Cell(nx, ny, Salt); if (VoxelHash::ToFloat01(Hh) > P.IslandDensity) { return false; } const float JX = VoxelHash::ToFloat01(VoxelHash::Mix(Hh ^ 0x12345678u)); const float JY = VoxelHash::ToFloat01(VoxelHash::Mix(Hh ^ 0x9ABCDEF0u)); Out.X = (nx + 0.15f + JX * 0.7f) * Spacing; Out.Y = (ny + 0.15f + JY * 0.7f) * Spacing; Out.Rxy = FMath::Lerp(P.IslandMinRadius, P.IslandMaxRadius, VoxelHash::ToFloat01(VoxelHash::Mix(Hh ^ 0x5A5Au))); // PROFIL ASYMÉTRIQUE : dalle de terre au-dessus, dessous qui s'effile en pointe. Out.TopHalf = Out.Rxy * 0.20f; const float UnderDepth = Out.Rxy * FMath::Max(P.ThicknessRatio, 0.25f); const float SpreadZ = FMath::Max(H * 0.5f - FMath::Max(Out.TopHalf, UnderDepth) - P.BoundarySealThickness, 0.0f) * P.VerticalJitter; const float Cz = MidZ + VoxelHash::ToFloatSigned(VoxelHash::Mix(Hh ^ 0xB17Du)) * SpreadZ; Out.TopZ = Cz + Out.TopHalf; Out.BotZ = Cz - UnderDepth; // Netteté du taper par île (point d'arrivée du SmoothStep) → silhouettes variées. Out.TaperEnd = FMath::Lerp(0.45f, 0.7f, VoxelHash::ToFloat01(VoxelHash::Mix(Hh ^ 0x7A1Eu))); return true; } /** * Le voisinage 3×3, mémoïsé par worker (`thread_local`). * * ⚠️ LA CLÉ INCLUT `BoundarySealThickness` : `SpreadZ` s'en sert * (`H·0.5 − max(TopHalf, UnderDepth) − Seal`), donc sans lui une édition à chaud qui ne * change QUE l'épaisseur de seal servirait des îles périmées. Même famille que `AUDIT §C2` : * une clé de cache incomplète ne se voit pas, elle produit du terrain plausible. * * The key includes BoundarySealThickness because SpreadZ reads it; without it a live edit * of the seal thickness alone would serve stale islands. */ const FCells& GetCells(float WorldX, float WorldY) const { const float Spacing = FMath::Max(P.IslandSpacing, 1.0f); const int32 CX = FMath::FloorToInt(WorldX / Spacing); const int32 CY = FMath::FloorToInt(WorldY / Spacing); thread_local FCells Cache; thread_local int32 FI_CX = INT32_MAX, FI_CY = INT32_MAX; thread_local uint32 FI_Salt = 0xFFFFFFFFu; thread_local float FI_Spacing = -1.0f, FI_Dens = -1.0f, FI_MinR = -1.0f, FI_MaxR = -1.0f, FI_Thick = -1.0f, FI_VJit = -1.0f, FI_Seal = -1.0f, FI_BotZ = FLT_MAX, FI_TopZ = FLT_MAX; if (CX != FI_CX || CY != FI_CY || Salt != FI_Salt || Spacing != FI_Spacing || P.IslandDensity != FI_Dens || P.IslandMinRadius != FI_MinR || P.IslandMaxRadius != FI_MaxR || P.ThicknessRatio != FI_Thick || P.VerticalJitter != FI_VJit || P.BoundarySealThickness != FI_Seal || P.StrateBottomWorldZ != FI_BotZ || P.StrateTopWorldZ != FI_TopZ) { FI_CX = CX; FI_CY = CY; FI_Salt = Salt; FI_Spacing = Spacing; FI_Dens = P.IslandDensity; FI_MinR = P.IslandMinRadius; FI_MaxR = P.IslandMaxRadius; FI_Thick = P.ThicknessRatio; FI_VJit = P.VerticalJitter; FI_Seal = P.BoundarySealThickness; FI_BotZ = P.StrateBottomWorldZ; FI_TopZ = P.StrateTopWorldZ; Cache.Islands.Reset(); for (int32 dy = -1; dy <= 1; dy++) for (int32 dx = -1; dx <= 1; dx++) { FIsland Isl; if (RollIsland(CX + dx, CY + dy, Isl)) { Cache.Islands.Add(Isl); } } } return Cache; } FFloatingIslandParams P; uint32 Salt; }; // The mesher's coarse samples are sparse enough that one sample can land in a different // exact 32-voxel chunk every time. Keeping one SDF cache inside every exact-chunk op-stack // therefore turns L4 into thousands of BuildChunkCache calls. These worker-local entries // share the immutable room graph cache by its complete deterministic input key. A wider // region is safe because BuildChunkCache's collect/store construction is window-invariant; // the key still includes every value that can change the graph or its terrain-op rolls. // A coarse tile traverses several deterministic XY search windows while LODs are generated // concurrently. One mixed table made fine and coarse windows evict one another before the // next tile could reuse them. Keep a bounded table per region-size class: this is a cache // partition only, not a change to the graph key or to graph construction. constexpr int32 RoomGraphCacheSlotCount = 128; constexpr int32 RoomGraphCacheBankCount = 3; struct FRoomGraphCacheEntry { bool bValid = false; uint32 Seed = 0; int32 StrateIndex = INT32_MIN; uint32 ParamsFingerprint = 0xFFFFFFFFu; uint32 LayoutVersion = 0xFFFFFFFFu; uint64 ManagerLifetimeId = 0; const TArray* TerrainOps = nullptr; int32 RegionMinX = 0; int32 RegionMinY = 0; int32 RegionSize = 0; uint32 LastUse = 0; FChunkSDFCache Cache; }; using FRoomGraphCacheStorage = std::array< std::array, RoomGraphCacheBankCount>; static thread_local VoxelForgeCoreThreadLocal::FLazy GRoomGraphCache; static thread_local uint32 GRoomGraphCacheClock = 0; FORCEINLINE int32 RoomGraphCacheBank(int32 RegionSize) { return RegionSize <= CHUNK_SIZE ? 0 : (RegionSize <= 2 * CHUNK_SIZE ? 1 : 2); } FORCEINLINE int32 RoomGraphCacheSlot( uint32 Seed, int32 StrateIndex, uint32 ParamsFingerprint, uint32 LayoutVersion, uint64 ManagerLifetimeId, int32 RegionMinX, int32 RegionMinY, int32 RegionSize) { uint32 Hash = Seed * 0x9E3779B9u; Hash ^= static_cast(StrateIndex) * 0x85EBCA6Bu; Hash ^= ParamsFingerprint * 0xC2B2AE35u; Hash ^= LayoutVersion * 0x27D4EB2Fu; Hash ^= static_cast(ManagerLifetimeId) ^ static_cast(ManagerLifetimeId >> 32); Hash ^= static_cast(RegionMinX) * 0x165667B1u; Hash ^= static_cast(RegionMinY) * 0xD3A2646Cu; Hash ^= static_cast(RegionSize) * 0xFD7046C5u; Hash ^= Hash >> 16; return static_cast(Hash & (RoomGraphCacheSlotCount - 1)); } FRoomGraphCacheEntry* FindRoomGraphCacheEntry( uint32 Seed, int32 StrateIndex, uint32 ParamsFingerprint, uint32 LayoutVersion, uint64 ManagerLifetimeId, const TArray* TerrainOps, int32 RegionMinX, int32 RegionMinY, int32 RegionSize) { auto& RoomGraphCache = GRoomGraphCache.Get(); auto SelectForReuse = [](FRoomGraphCacheEntry* Selected, uint32 Use) -> FRoomGraphCacheEntry* { if (Selected != nullptr && Selected->bValid) { // Reset() deliberately preserves hot-path capacity. That is useful while the // same window is reused, but not when this slot is about to become a different // key: every worker could otherwise retain one full graph window per historical // streaming key. Release before invalidating the key so a long-lived game has a // bounded working set instead of a capacity leak. Selected->Cache.Release(); Selected->bValid = false; } if (Selected != nullptr) { Selected->LastUse = Use; } return Selected; }; const int32 Bank = RoomGraphCacheBank(RegionSize); const uint32 Use = ++GRoomGraphCacheClock; const int32 StartSlot = RoomGraphCacheSlot( Seed, StrateIndex, ParamsFingerprint, LayoutVersion, ManagerLifetimeId, RegionMinX, RegionMinY, RegionSize); FRoomGraphCacheEntry* ReuseEntry = nullptr; FRoomGraphCacheEntry* LeastRecentlyUsed = nullptr; for (int32 Probe = 0; Probe < RoomGraphCacheSlotCount; ++Probe) { FRoomGraphCacheEntry& Candidate = RoomGraphCache[Bank][ (StartSlot + Probe) & (RoomGraphCacheSlotCount - 1)]; if (!Candidate.bValid) { FRoomGraphCacheEntry* Selected = ReuseEntry != nullptr ? ReuseEntry : &Candidate; return SelectForReuse(Selected, Use); } if (Candidate.Seed == Seed && Candidate.StrateIndex == StrateIndex && Candidate.ParamsFingerprint == ParamsFingerprint && Candidate.LayoutVersion == LayoutVersion && Candidate.ManagerLifetimeId == ManagerLifetimeId && Candidate.TerrainOps == TerrainOps && Candidate.RegionMinX == RegionMinX && Candidate.RegionMinY == RegionMinY && Candidate.RegionSize == RegionSize) { Candidate.LastUse = Use; return &Candidate; } if (ReuseEntry == nullptr && Candidate.ManagerLifetimeId != ManagerLifetimeId) { ReuseEntry = &Candidate; } if (LeastRecentlyUsed == nullptr || Candidate.LastUse < LeastRecentlyUsed->LastUse) { LeastRecentlyUsed = &Candidate; } } FRoomGraphCacheEntry* Selected = ReuseEntry != nullptr ? ReuseEntry : LeastRecentlyUsed; return SelectForReuse( Selected != nullptr ? Selected : &RoomGraphCache[Bank][StartSlot], Use); } // Classification is dispatched across many workers before most of those workers have // evaluated a density sample. A thread-local cache prevents races, but it also makes every // worker rebuild the same deterministic collect/NN graph. Keep a small process-local set of // immutable classifier caches: construction is serialized, publication happens only after // BuildChunkCache has completed, and each consumer holds a shared reference while refining. // This cache never stores mutable manager state; all manager/terrain data are read while the // entry is built and the resulting FChunkSDFCache is read-only thereafter. struct FSharedRoomGraphCacheEntry { uint32 Seed = 0; int32 StrateIndex = INT32_MIN; uint32 ParamsFingerprint = 0xFFFFFFFFu; uint32 LayoutVersion = 0xFFFFFFFFu; uint64 ManagerLifetimeId = 0; const TArray* TerrainOps = nullptr; int32 RegionMinX = 0; int32 RegionMinY = 0; int32 RegionSize = 0; uint32 LastUse = 0; bool bBuilding = false; FEvent* ReadyEvent = nullptr; TSharedPtr Cache; uint64 AllocatedBytes = 0; ~FSharedRoomGraphCacheEntry() { if (ReadyEvent != nullptr) { FPlatformProcess::ReturnSynchEventToPool(ReadyEvent); } } }; static FCriticalSection GSharedRoomGraphCacheMutex; static TArray> GSharedRoomGraphCache; static uint32 GSharedRoomGraphCacheClock = 0; static uint64 GSharedRoomGraphCacheResidentBytes = 0; static FCriticalSection GSharedRoomGraphCacheBuildGateMutex; static int32 GSharedRoomGraphCacheActiveBuilds[2] = {0, 0}; // The byte budget is the actual memory guard. A 128-entry count is too small for the // player's streaming radius: it turns otherwise reusable 128-voxel windows into transient // rebuilds while only a few MiB are resident. Keep a generous bounded index so the budget, // rather than an arbitrary key count, controls retention. constexpr int32 SharedRoomGraphCacheMaxEntries = 4096; // Limit the number of graph allocations that can exist before their size is // known and published into the byte budget. This is the guard against a // burst of distinct coarse tiles retaining one large build per worker. // Fine LOD0 proof windows are small and latency-sensitive. Keep their build slots separate // from coarse streaming windows: a pair of 250 ms LOD4 graph builds must not make a 32/64 // voxel empty tile wait behind them. The fine slots are still bounded, and their graph // allocations are tiny compared with the coarse builds. constexpr int32 SharedRoomGraphCacheBuildBankCount = 2; constexpr int32 SharedRoomGraphCacheBuildConcurrency[SharedRoomGraphCacheBuildBankCount] = {4, 2}; // This is a proof-cache budget, not a geometry budget. A miss rebuilds an immutable cache; // unknown/evicted proof data therefore falls back to sampling and cannot change the mesh. constexpr uint64 SharedRoomGraphCacheBudgetBytes = 256ull * 1024ull * 1024ull; static TSharedPtr SnapshotSharedRoomGraphCache( const TSharedPtr& Entry) { FScopeLock Lock(&GSharedRoomGraphCacheMutex); return Entry.IsValid() ? Entry->Cache : TSharedPtr(); } int32 SharedRoomGraphCacheBuildBank(int32 RegionSize) { return RegionSize <= 2 * CHUNK_SIZE ? 0 : 1; } void AcquireSharedRoomGraphCacheBuildSlot(int32 RegionSize) { const int32 Bank = SharedRoomGraphCacheBuildBank(RegionSize); for (;;) { { FScopeLock Lock(&GSharedRoomGraphCacheBuildGateMutex); if (GSharedRoomGraphCacheActiveBuilds[Bank] < SharedRoomGraphCacheBuildConcurrency[Bank]) { ++GSharedRoomGraphCacheActiveBuilds[Bank]; return; } } FPlatformProcess::Yield(); } } void ReleaseSharedRoomGraphCacheBuildSlot(int32 RegionSize) { const int32 Bank = SharedRoomGraphCacheBuildBank(RegionSize); FScopeLock Lock(&GSharedRoomGraphCacheBuildGateMutex); check(GSharedRoomGraphCacheActiveBuilds[Bank] > 0); --GSharedRoomGraphCacheActiveBuilds[Bank]; } void EvictSharedRoomGraphCacheLocked(const FSharedRoomGraphCacheEntry* ProtectedEntry = nullptr) { while (GSharedRoomGraphCache.Num() > SharedRoomGraphCacheMaxEntries || GSharedRoomGraphCacheResidentBytes > SharedRoomGraphCacheBudgetBytes) { int32 OldestIndex = INDEX_NONE; for (int32 Index = 0; Index < GSharedRoomGraphCache.Num(); ++Index) { const TSharedPtr& Candidate = GSharedRoomGraphCache[Index]; if (Candidate->bBuilding || Candidate.Get() == ProtectedEntry) { continue; } if (OldestIndex == INDEX_NONE || Candidate->LastUse < GSharedRoomGraphCache[OldestIndex]->LastUse) { OldestIndex = Index; } } if (OldestIndex == INDEX_NONE) { // All remaining entries are in-flight or the protected oversized entry. Keep // them alive until their consumers finish; the next completed publish can evict. break; } const uint64 Bytes = GSharedRoomGraphCache[OldestIndex]->AllocatedBytes; GSharedRoomGraphCacheResidentBytes = Bytes >= GSharedRoomGraphCacheResidentBytes ? 0 : GSharedRoomGraphCacheResidentBytes - Bytes; GSharedRoomGraphCache.RemoveAtSwap(OldestIndex, 1, EAllowShrinking::No); } } void RemoveStaleSharedRoomGraphCachesLocked(uint64 KeepManagerLifetimeId) { for (int32 Index = GSharedRoomGraphCache.Num() - 1; Index >= 0; --Index) { const TSharedPtr& Candidate = GSharedRoomGraphCache[Index]; if (Candidate->bBuilding || (KeepManagerLifetimeId != 0 && Candidate->ManagerLifetimeId == KeepManagerLifetimeId)) { continue; } const uint64 Bytes = Candidate->AllocatedBytes; GSharedRoomGraphCacheResidentBytes = Bytes >= GSharedRoomGraphCacheResidentBytes ? 0 : GSharedRoomGraphCacheResidentBytes - Bytes; GSharedRoomGraphCache.RemoveAtSwap(Index, 1, EAllowShrinking::No); } EvictSharedRoomGraphCacheLocked(); } TSharedPtr FindOrBuildSharedRoomGraphCache( uint32 Seed, int32 StrateIndex, uint32 ParamsFingerprint, uint32 LayoutVersion, uint64 ManagerLifetimeId, const TArray* TerrainOps, int32 RegionMinX, int32 RegionMinY, int32 RegionSize, float SearchMinX, float SearchMinY, float SearchMaxX, float SearchMaxY, const FStrateGenerationParams& Params, ERoomGraphBuildSite BuildSite) { for (;;) { TSharedPtr Entry; bool bBuild = false; bool bBuildTransient = false; { FScopeLock Lock(&GSharedRoomGraphCacheMutex); const uint32 Use = ++GSharedRoomGraphCacheClock; for (const TSharedPtr& Candidate : GSharedRoomGraphCache) { if (Candidate->Seed == Seed && Candidate->StrateIndex == StrateIndex && Candidate->ParamsFingerprint == ParamsFingerprint && Candidate->LayoutVersion == LayoutVersion && Candidate->ManagerLifetimeId == ManagerLifetimeId && Candidate->TerrainOps == TerrainOps && Candidate->RegionMinX == RegionMinX && Candidate->RegionMinY == RegionMinY && Candidate->RegionSize == RegionSize) { Candidate->LastUse = Use; Entry = Candidate; break; } } if (!Entry.IsValid()) { // Evict completed entries before admitting a new key. If // the table is full of in-flight keys, do not append an // unbounded waiting entry: build this request transiently // under the build gate instead. The caller still gets the // same immutable cache, but it is released with that // classifier state rather than retained globally. EvictSharedRoomGraphCacheLocked(); if (GSharedRoomGraphCache.Num() >= SharedRoomGraphCacheMaxEntries) { bBuildTransient = true; } else { Entry = MakeShared(); Entry->Seed = Seed; Entry->StrateIndex = StrateIndex; Entry->ParamsFingerprint = ParamsFingerprint; Entry->LayoutVersion = LayoutVersion; Entry->ManagerLifetimeId = ManagerLifetimeId; Entry->TerrainOps = TerrainOps; Entry->RegionMinX = RegionMinX; Entry->RegionMinY = RegionMinY; Entry->RegionSize = RegionSize; Entry->LastUse = Use; Entry->bBuilding = true; Entry->ReadyEvent = FPlatformProcess::GetSynchEventFromPool(true); GSharedRoomGraphCache.Add(Entry); bBuild = true; } } } if (bBuild || bBuildTransient) { const double CacheBuildCallStart = FPlatformTime::Seconds(); AcquireSharedRoomGraphCacheBuildSlot(RegionSize); const double CacheBuildSlotWait = FPlatformTime::Seconds() - CacheBuildCallStart; TSharedPtr BuiltCache = MakeShared(); VoxelCaveMorphology::BuildChunkCache( *BuiltCache, SearchMinX, SearchMinY, SearchMaxX, SearchMaxY, Params, Seed, StrateIndex, TerrainOps, BuildSite); const double CacheBuildSeconds = FPlatformTime::Seconds() - CacheBuildCallStart; const uint64 AllocatedBytes = static_cast(sizeof(FSharedRoomGraphCacheEntry)) + static_cast(sizeof(FChunkSDFCache)) + static_cast(BuiltCache->GetAllocatedSize()); if (bBuildTransient) { Entry = MakeShared(); Entry->Seed = Seed; Entry->StrateIndex = StrateIndex; Entry->ParamsFingerprint = ParamsFingerprint; Entry->LayoutVersion = LayoutVersion; Entry->ManagerLifetimeId = ManagerLifetimeId; Entry->TerrainOps = TerrainOps; Entry->RegionMinX = RegionMinX; Entry->RegionMinY = RegionMinY; Entry->RegionSize = RegionSize; Entry->LastUse = 0; Entry->AllocatedBytes = AllocatedBytes; { FScopeLock Lock(&GSharedRoomGraphCacheMutex); Entry->Cache = BuiltCache; } } else { FScopeLock Lock(&GSharedRoomGraphCacheMutex); Entry->Cache = BuiltCache; Entry->AllocatedBytes = AllocatedBytes; Entry->bBuilding = false; Entry->ReadyEvent->Trigger(); if (AllocatedBytes <= SharedRoomGraphCacheBudgetBytes) { GSharedRoomGraphCacheResidentBytes += Entry->AllocatedBytes; EvictSharedRoomGraphCacheLocked(Entry.Get()); } else { // Publish to any waiters through the shared entry, but do not retain an // oversized graph in the process-wide table. The caller's shared pointer // keeps this one immutable build alive only for the current work item. for (int32 Index = 0; Index < GSharedRoomGraphCache.Num(); ++Index) { if (GSharedRoomGraphCache[Index].Get() == Entry.Get()) { GSharedRoomGraphCache.RemoveAtSwap( Index, 1, EAllowShrinking::No); break; } } EvictSharedRoomGraphCacheLocked(); UE_LOG(LogTemp, Warning, TEXT("[VoxelForgeSharedCacheMemory] oversized_entry_bytes=%llu " "budget_bytes=%llu retained=0"), static_cast(AllocatedBytes), static_cast(SharedRoomGraphCacheBudgetBytes)); } } ReleaseSharedRoomGraphCacheBuildSlot(RegionSize); if (!bBuildTransient) { VoxelDensityOps::ReportWorkerRoomGraphCacheFootprint(); } if (RegionSize <= 2 * CHUNK_SIZE || CacheBuildSeconds >= 0.25 || CacheBuildSlotWait >= 0.25) { UE_LOG(LogTemp, Warning, TEXT("[VoxelForgeSharedCacheTiming] action=build transient=%d " "region=(%d,%d,%d) slot_wait=%.6f build=%.6f bytes=%llu"), bBuildTransient ? 1 : 0, RegionMinX, RegionMinY, RegionSize, CacheBuildSlotWait, CacheBuildSeconds, static_cast(AllocatedBytes)); } return Entry; } if (SnapshotSharedRoomGraphCache(Entry).IsValid()) { return Entry; } // A different worker is building this exact immutable key. Waiting on its event // avoids duplicate graph work without serializing unrelated region keys. const double CacheWaitStart = FPlatformTime::Seconds(); Entry->ReadyEvent->Wait(); const double CacheWaitSeconds = FPlatformTime::Seconds() - CacheWaitStart; if (Entry->RegionSize <= 2 * CHUNK_SIZE || CacheWaitSeconds >= 0.25) { UE_LOG(LogTemp, Warning, TEXT("[VoxelForgeSharedCacheTiming] action=wait region=(%d,%d,%d) wait=%.6f"), Entry->RegionMinX, Entry->RegionMinY, Entry->RegionSize, CacheWaitSeconds); } } } //========================================================================= // RÔLE 1 — SOURCE : GRAPHE DE SALLES / ROOM GRAPH (TunnelNetwork) //========================================================================= // ⚠️⚠️ CET OPÉRATEUR N'A PAS RÉÉCRIT `BuildChunkCache` / `EvaluateSDFCached` : IL LES APPELLE. // // C'est LA décision de ce portage, et elle mérite d'être dite explicitement parce que la // tentation inverse est forte : les six autres portages sont des transcriptions littérales. // Celui-ci ne peut pas l'être. `BuildChunkCache` porte la discipline d'invariance de fenêtre à // deux régions (ARCHITECTURE §8.4) — la région COLLECT (plus large, décide QUELLES primitives // existent) et la région STORE (ce qu'on garde) — et c'est le code le plus délicat du plugin. // Le transcrire, ce serait le FORKER : deux copies d'un invariant qui dérivent, dont l'une n'est // testée que par un test d'équivalence qui compare... la copie à l'original. // // Ce qui EST transcrit ici, c'est la glu autour : la mémo d'index de strate, la clé de cache par // BOÎTE DE RECHERCHE (pas par chunk — voir plus bas), le warp, et les boucles pits/cheminées. // ~60 lignes déjà relues, contre ~400 lignes d'algorithme qu'on ne touche pas. // // This op CALLS the morphology cache rather than transcribing it: BuildChunkCache carries the // two-region window-invariance discipline (§8.4) and forking it would be the worst possible // outcome of a refactor whose whole point is to have ONE definition of each idea. class FRoomGraphSource final : public IVoxelDensityOp { public: FRoomGraphSource(const FStrateGenerationParams& InP, int32 InSeed, const UVoxelStrateManager* InManager) : P(InP), Seed(InSeed), SeedU((uint32)InSeed), Manager(InManager) , ManagerLifetimeId(InManager ? InManager->GetCacheLifetimeId() : 0) , ParamsFingerprint(FCrc::MemCrc32(&InP, sizeof(InP))) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::None; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Sdf; } EVoxelOpResourceMask ProvidedResources() const override { return VoxelOpResources::RoomGeometry; } bool IsAdditive() const override { return false; } void PrepareChunk(const FVoxelOpContext& Ctx) override { // La seule chose vraiment constante par chunk ET dépendante du contexte. Le reste // (index de strate, pool d'ops) est résolu paresseusement dans `Eval` comme l'original, // parce que le cache SDF se ré-clé sur une BOÎTE, pas sur un chunk. LayoutVersion = Ctx.LayoutVersion; } //--------------------------------------------------------------------- // L'ÉTAT PAR WORKER, ET POURQUOI IL EST SORTI DE `Eval` //--------------------------------------------------------------------- // Les modificateurs de détail de l'étape B ont besoin de ce que CETTE source a produit pour // CE voxel : le cache SDF (les terrasses le ré-interrogent en Z±1, les colonnes le // parcourent) et l'index de la salle la plus proche (arches, dômes, pincement, biais de sol). // Dans l'original tout cela vit dans des `thread_local` d'une seule fonction de 1080 lignes ; // ici la source les possède et les expose. // // The state belongs to this source instance. The outer tunnel cache retains the immutable // op stack, so retaining the source's prepared SDF window with that stack avoids rebuilding // the room graph every time a coarse tile switches between cached chunk keys. It remains // worker-local because each op stack lives in the thread-local density cache. struct FState { // Fallback only for the invalid-owner path. Normal evaluations point at the worker-local // shared cache bank below, so switching exact chunk op-stacks does not rebuild the room // graph merely because the stack object changed. FChunkSDFCache Cache; const FChunkSDFCache* ActiveCache = nullptr; // Coarse density samples use the process-local immutable graph cache. Keep the entry // alive for the whole sequence of detail operators that consume ActiveCache. TSharedPtr SharedCacheEntry; /** La salle de SDF minimal pour le dernier voxel évalué. -1 = aucune. */ int32 NearestRoom = -1; /** The warped SDF-space position belonging to the current voxel. */ FVector LastWarpedPosition = FVector::ZeroVector; /** The unwarped position belonging to the current voxel. */ FVector LastWorldPosition = FVector::ZeroVector; // The common generator tail runs after disturbances. The tunnel structural posts // already evaluate this immutable result inside the stack; retain the exact same // sample's answer so the tail does not scan the tunnel list a second time. FTunnelCoreWorldEvaluation LastTunnelCoreWorldEvaluation; FVector LastTunnelCoreEvaluationPosition = FVector::ZeroVector; bool bLastTunnelCoreWorldEvaluationValid = false; bool bLastRoomOwnsBottom = false; bool bLastRoomOwnsBottomValid = false; /** ÉTAPE C1 — les params de la strate avec l'op de CETTE salle appliqué par-dessus. * Mémo par voxel : invalidé au début de chaque `Eval`, calculé au PREMIER modificateur * qui le demande. C'est ce qui reproduit le coût de l'original (une copie de struct par * voxel PRÈS D'UNE SURFACE, pas partout) sans que onze opérateurs la refassent chacun. */ FStrateGenerationParams LocalParams; bool bLocalParamsValid = false; // Window invariance is a cache-policy proof, not a sample evaluator. Keep its result // beside the worker-local source state so LOD0 does not pay the full reach walk once // per voxel. bool bTileCachePolicyValid = false; bool bTileCachePolicyResult = false; bool bTileCachePolicyHasContext = false; int32 TileCachePolicySampleStep = 0; FIntVector TileCachePolicyOrigin = FIntVector::ZeroValue; int32 TileCachePolicyStep = 0; int32 TileCachePolicyCells = 0; uint32 TileCachePolicyFingerprint = 0xFFFFFFFFu; uint32 TileCachePolicyLayout = 0xFFFFFFFFu; uint64 TileCachePolicyManagerLifetimeId = 0; const TArray* TileCachePolicyTerrainOps = nullptr; }; FState& State() const { // The prepared SDF data lives in the worker-local keyed cache bank above. This state // is only scratch for the current EvalSample: Eval resets NearestRoom and the local // parameter memo before its detail ops consume them. A single TLS slot is therefore // safe even when this worker switches between immutable source instances, and avoids // a map lookup on every sample. A mutable member is not safe because stacks can be // shared by concurrent readers during generation. static thread_local FState Scratch; return Scratch; } /** Le cache que la source vient de bâtir/servir pour ce voxel. Lu par les colonnes (4d). */ const FChunkSDFCache& GetCache() const { return State().ActiveCache != nullptr ? *State().ActiveCache : State().Cache; } /** L'index de la salle la plus proche pour le dernier voxel évalué. -1 = aucune. * Lu par les arches, les dômes, le pincement et le biais de sol. */ int32 GetNearestRoomIdx() const { return State().NearestRoom; } /** Restore the source-owned state belonging to one sample before a later block op reads it. */ void RestoreBlockSample(const FVoxelOpSample& Sample) const { FState& S = State(); S.ActiveCache = Sample.RoomCache; S.NearestRoom = Sample.NearestRoomIndex; S.bLocalParamsValid = false; S.bLastRoomOwnsBottomValid = false; } bool TryGetLastTunnelCoreWorldEvaluation( FTunnelCoreWorldEvaluation& OutEvaluation) const override { if (VoxelDensityAblation::IsTunnelCoreOff()) { return false; } FState& S = State(); if (!S.bLastTunnelCoreWorldEvaluationValid || S.LastTunnelCoreEvaluationPosition != S.LastWorldPosition) { if (!(P.RoomDensity > 0.0f && P.RoomSpacing > 0.0f)) { return false; } // The common generator tail runs after EvalMC. The stack has already evaluated // the graph source for this exact sample, but the native tunnel-core post is // intentionally outside the stack because it must remain after disturbances. On // demand, publish the one combined result here so the tail can reuse it without // building/scanning a second native cache. const FVector& Position = S.LastWorldPosition; S.LastTunnelCoreWorldEvaluation = VoxelCaveMorphology::EvaluateTunnelCoreWorld( Position.X, Position.Y, Position.Z, GetCache(), nullptr, VoxelGenLOD::ShouldUseSpatialIndex(false), &S.LastWarpedPosition, S.bLastRoomOwnsBottomValid ? &S.bLastRoomOwnsBottom : nullptr); S.LastTunnelCoreEvaluationPosition = Position; S.bLastTunnelCoreWorldEvaluationValid = true; } OutEvaluation = S.LastTunnelCoreWorldEvaluation; return true; } /** * ÉTAPE C1 — L'OVERRIDE D'OP PAR SALLE. Les params de la strate avec l'op de terrain tiré * pour la salle la plus proche appliqué par-dessus. **ONZE des douze modificateurs de détail * lisent ceci au lieu de leurs propres params.** * * ⚠️ POURQUOI ÇA VIT ICI ET PAS DANS CHAQUE MODIFICATEUR. * `OPSTACK-DECOMPOSITION §2` disait que cette pièce n'a « pas de domicile propre » et * proposait de donner à chaque modificateur un prédicat « seulement dans la salle N ». La * difficulté venait d'une hypothèse : que chaque modificateur doive POSSÉDER ses params. Dès * qu'UN opérateur possède l'état partagé et que les autres le LISENT, elle disparaît — et ce * motif est déjà celui de `FOverhangShelfMod` ← `FSurfaceColumnSource` et de * `FShaftLedgeMod` ← `FShaftFieldSource`. C'est la résolution des pits/cheminées une * deuxième fois : ne pas inventer de mécanisme de portée, laisser une source publier. * * ⚠️ LA COPIE DE ~74 CHAMPS PAR VOXEL EST TRANSCRITE TELLE QUELLE. L'original écrit * `FStrateGenerationParams LocalTerrainParams = Params;` dans le bloc par voxel. C'est un * poste de perf réel, noté dans OPSTACK-PROGRESS ; le mémo ci-dessous garantit seulement * qu'on ne la fait pas ONZE fois là où l'original la fait une. * * ⚠️ **LA RUGOSITÉ (4b) N'APPELLE PAS CECI**, et c'est la lecture du code, pas une * simplification : dans l'original le shadow `const FStrateGenerationParams& Params = * LocalTerrainParams;` est déclaré DANS le bloc `if (bNearCaveSurface)` qui commence APRÈS * l'étape 4b. La rugosité lit les params de la strate. Douze modificateurs, onze lecteurs. * * One op owns the shared state and the rest read it — the same pattern the surface and shaft * ports already use, and the reason §2's "no clean home" problem evaporates. The ~74-field * per-voxel copy is the original's, kept. Roughness (4b) deliberately does NOT read this. */ const FStrateGenerationParams& LocalParams() const { FState& S = State(); if (!S.bLocalParamsValid) { S.LocalParams = P; const FChunkSDFCache& Cache = GetCache(); if (S.NearestRoom >= 0 && Cache.Rooms.IsValidIndex(S.NearestRoom)) { const FCachedRoom& NR = Cache.Rooms[S.NearestRoom]; if (NR.RoomOp) { // N'écrit que les champs propres au type de l'op ; tout le reste garde la // valeur de la strate. Exactement l'appel de l'original. NR.RoomOp->ApplyTo(S.LocalParams, NR.RoomOpWeight); } } S.bLocalParamsValid = true; } return S.LocalParams; } /** * Une requête SDF supplémentaire dans le cache courant, pour les sondes de gradient des * terrasses. * * ⚠️ TRANSCRIT TEL QUEL, Y COMPRIS CE QUI SEMBLE INCOHÉRENT : l'original sonde en * `(WorldX, WorldY, WorldZ ± 1)` — coordonnées NON warpées et Z NON divisé par * `VerticalScale` — alors que le champ qu'il sonde a été évalué en coordonnées WARPÉES et en * Z effectif. La sonde ne voit donc pas exactement le champ dont elle mesure la pente, et * elle ignore aussi les pits et les cheminées. C'est un écart réel de l'original ; le * corriger changerait le monde, donc il est NOTÉ (OPSTACK-PROGRESS) et porté à l'identique. * * Transcribed as-is including what looks wrong: the probe uses unwarped X/Y and raw Z while * the field it probes was evaluated warped, and it excludes pits/chimneys. */ float ProbeSdfUnwarped(float X, float Y, float Z) const { return VoxelCaveMorphology::EvaluateSDFCached( X, Y, Z, GetCache(), P.SDFBlendRadius, nullptr, VoxelGenLOD::ShouldUseSpatialIndex(false)); } /** Le Z « effectif » : `VerticalScale` étire le monde AVANT le bruit. Pure fonction de Z et * d'un param — c'est pourquoi ce portage n'a PAS eu besoin d'un opérateur « frame » * (voir la note de conception dans BuildTunnelNetworkStack). */ FORCEINLINE float EffZ(float WorldZ) const { return (P.VerticalScale != 1.0f && P.VerticalScale > 0.0f) ? (WorldZ / P.VerticalScale) : WorldZ; } void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::RoomGraphSource); FState& S = State(); S.LastWorldPosition = FVector(WorldX, WorldY, WorldZ); S.bLastTunnelCoreWorldEvaluationValid = false; S.bLastRoomOwnsBottomValid = false; // ⚠️ REMIS À -1 INCONDITIONNELLEMENT, ce que l'original ne fait pas : chez lui // `NearestRoomIdx` est un `thread_local` qui, quand `RoomDensity <= 0`, garde la valeur // du voxel PRÉCÉDENT. Inobservable là-bas (sans salles, `CaveSDF` reste FLT_MAX, donc // `bNearCaveSurface` est faux et aucun consommateur ne tourne) — mais ici les // consommateurs sont des objets séparés, et une valeur périmée qui traverse une frontière // d'opérateur est le genre de chose qu'on ne retrouve pas. On paie une écriture. S.NearestRoom = -1; // ÉTAPE C1 — le mémo d'override est PAR VOXEL. L'invalider ici, avant tout early-out, // est ce qui garantit qu'aucun modificateur ne lira les params de la salle du voxel // précédent. (Le mémo n'est PAS recalculé ici : le faire coûterait une copie de struct // sur chaque voxel de roc profond, ce que l'original ne paie pas.) S.bLocalParamsValid = false; const UVoxelStrateManager* LiveManager = Manager.Get(); // A cached stack may outlive its UObject owner when an editor world is stopped. The // generator's manager-lifetime key prevents selecting it normally; this guard is the // final boundary for a task that is already inside an old stack. Keep commandlet stacks // (which deliberately have no manager) functional, but never evaluate stale room data. if (ManagerLifetimeId != 0 && LiveManager == nullptr) { S.Cache = FChunkSDFCache(); S.SharedCacheEntry.Reset(); S.ActiveCache = &S.Cache; InOut.Sdf = FLT_MAX; return; } if (!(P.RoomDensity > 0.0f && P.RoomSpacing > 0.0f)) { S.SharedCacheEntry.Reset(); S.ActiveCache = &S.Cache; return; // Sdf reste FLT_MAX } const float EffectiveZ = EffZ(WorldZ); //--------------------------------------------------------------- // WARP DE CAVE — coordonnées de REQUÊTE uniquement //--------------------------------------------------------------- // ⚠️ Le warp ne s'applique QU'À la requête du graphe de salles. Les pits et les cheminées // plus bas lisent les coordonnées RÉELLES, et c'est délibéré dans l'original : leurs // ancres viennent de centres de salles NON warpés. C'est aussi pourquoi le warp n'est pas // un « frame » : sa portée est exactement UN opérateur, donc elle appartient à cet // opérateur. float WarpedX = WorldX, WarpedY = WorldY, WarpedZ = EffectiveZ; if (!VoxelDensityAblation::IsCaveWarpOff() && P.CaveWarpStrength > 0.0f) { const float WF = P.CaveWarpFrequency; const float WS = P.CaveWarpStrength; 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; } S.LastWarpedPosition = FVector(WarpedX, WarpedY, WarpedZ); //--------------------------------------------------------------- // LE CACHE PAR BOÎTE DE RECHERCHE //--------------------------------------------------------------- // ⚠️ CLÉ PAR BOÎTE, PAS PAR CHUNK, et c'est un INVARIANT DE PERF (§8.10) : les // échantillons de gradient interrogent `WorldX ± 1` et le warp déplace encore, donc une // clé « égalité de chunk » se retournait à chaque cellule de bord et reconstruisait le // cache (coûteux) en boucle. Comme le cache couvre la boîte + MaxInfluence, toute requête // DANS la boîte est correcte. Ne pas « simplifier » en clé de chunk. // (Les champs de cache vivent dans `FState`, au-dessus, pour que les modificateurs de // détail puissent les lire ; ce sont des `thread_local` partagés entre instances.) // ⚠️ LA CLÉ INCLUT LES PARAMS, CONTRAIREMENT À L'ORIGINAL. // L'original ne clé QUE sur (boîte, strate, seed) : deux jeux de params différents dans // la MÊME strate au MÊME seed se servent mutuellement leur cache. En production // `RebuildStrates` masque le trou en bougeant la strate ; en test, deux piles construites // côte à côte le déclenchent immédiatement. Empreinte CRC des params + LayoutVersion. // `FStrateGenerationParams` est du POD pur (aucun TArray/FString/pointeur), donc une CRC // mémoire ne peut pas donner un FAUX POSITIF ; au pire un padding donne un faux MANQUE, // c'est-à-dire un recalcul. On se trompe du côté du CPU, jamais du côté d'une salle fausse. // Index de strate — mémo (chunk-Z, version de layout), transcrit tel quel. La requête // vise le CENTRE de la bande, donc le résultat est une fonction pure de la clé. int32 StrateIdx = 0; if (LiveManager) { 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 = LiveManager->GetLayoutVersion(); if (QZ != SI_ChunkZ || LV != SI_Version) { SI_ChunkZ = QZ; SI_Version = LV; SI_Index = LiveManager->GetStrateIndex( ((float)QZ + 0.5f) * CHUNK_SIZE * VOXEL_SIZE); } StrateIdx = SI_Index; } // The graph is window-invariant, so a tile worker can use the exact requesting tile as // its deterministic XY cache region. The step/origin/cell extent is supplied by the // mesher through VoxelGenLOD TLS; ordinary point queries keep the historical region // policy. The cache key retains the exact params/strate/op-pool identity, so no // blended chunk can borrow another chunk's room graph. const int32 SampleStep = FMath::Max(VoxelGenLOD::SampleStep, 1); FIntVector RequestedTileOrigin = FIntVector::ZeroValue; int32 RequestedTileStep = SampleStep; int32 RequestedTileCells = 0; const TArray* TerrainOps = nullptr; if (LiveManager) { const int32 ChunkX = FMath::FloorToInt(WorldX / (float)CHUNK_SIZE); const int32 ChunkY = FMath::FloorToInt(WorldY / (float)CHUNK_SIZE); const int32 ChunkZ = FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE); UVoxelStrateDefinition* Def = LiveManager->GetStrateForChunk( FIntVector(ChunkX, ChunkY, ChunkZ)); if (Def) { TerrainOps = &Def->TerrainOperations; } } const bool bTileWindowEligible = VoxelGenLOD::IsTileCacheWindowEnabled(false); const bool bHasTileWindowContext = VoxelGenLOD::GetThreadTileCacheWindow( RequestedTileOrigin, RequestedTileStep, RequestedTileCells); const bool bTilePolicyKeyChanged = !S.bTileCachePolicyValid || S.bTileCachePolicyHasContext != (bTileWindowEligible && bHasTileWindowContext) || S.TileCachePolicySampleStep != RequestedTileStep || S.TileCachePolicyOrigin != RequestedTileOrigin || S.TileCachePolicyStep != RequestedTileStep || S.TileCachePolicyCells != RequestedTileCells || S.TileCachePolicyFingerprint != ParamsFingerprint || S.TileCachePolicyLayout != LayoutVersion || S.TileCachePolicyManagerLifetimeId != ManagerLifetimeId || S.TileCachePolicyTerrainOps != TerrainOps; if (bTilePolicyKeyChanged) { S.bTileCachePolicyValid = true; S.bTileCachePolicyHasContext = bTileWindowEligible && bHasTileWindowContext; S.TileCachePolicySampleStep = RequestedTileStep; S.TileCachePolicyOrigin = RequestedTileOrigin; S.TileCachePolicyStep = RequestedTileStep; S.TileCachePolicyCells = RequestedTileCells; S.TileCachePolicyFingerprint = ParamsFingerprint; S.TileCachePolicyLayout = LayoutVersion; S.TileCachePolicyManagerLifetimeId = ManagerLifetimeId; S.TileCachePolicyTerrainOps = TerrainOps; S.bTileCachePolicyResult = S.bTileCachePolicyHasContext && VoxelCaveMorphology::IsRoomGraphWindowInvariant(P, TerrainOps); } const bool bUseTileCacheWindow = S.bTileCachePolicyResult; int32 RegionSize = 0; int32 RegionMinX = 0; int32 RegionMinY = 0; if (bUseTileCacheWindow) { RegionSize = static_cast( static_cast(RequestedTileStep) * static_cast(RequestedTileCells)); RegionMinX = RequestedTileOrigin.X; RegionMinY = RequestedTileOrigin.Y; } else { // Fine requests retain the historical four-chunk worker window. The op-stack // tile-sized window is gated to LOD4+ (the fused path starts at LOD3), so // LOD0-LOD2 and the op-stack's LOD3 floor do not pay a full tile rebuild on their // critical path while still reusing neighbouring chunks on the same worker. const int32 RegionChunks = 4; RegionSize = CHUNK_SIZE * RegionChunks; const int32 RegionCellX = FMath::FloorToInt(WorldX / (float)RegionSize); const int32 RegionCellY = FMath::FloorToInt(WorldY / (float)RegionSize); RegionMinX = RegionCellX * RegionSize; RegionMinY = RegionCellY * RegionSize; } const float CacheExpansion = FMath::Abs(P.CaveWarpStrength) * VOXEL_NOISE_SCALE * VF_PerlinAbsBound + (bUseTileCacheWindow ? static_cast(SampleStep) : 0.0f) + 2.0f; if (SampleStep > 1) { TSharedPtr SharedRoomGraphCache = S.SharedCacheEntry.IsValid() ? SnapshotSharedRoomGraphCache(S.SharedCacheEntry) : TSharedPtr(); const bool bSharedCacheMatches = S.SharedCacheEntry.IsValid() && SharedRoomGraphCache.IsValid() && S.SharedCacheEntry->Seed == SeedU && S.SharedCacheEntry->StrateIndex == StrateIdx && S.SharedCacheEntry->ParamsFingerprint == ParamsFingerprint && S.SharedCacheEntry->LayoutVersion == LayoutVersion && S.SharedCacheEntry->ManagerLifetimeId == ManagerLifetimeId && S.SharedCacheEntry->TerrainOps == TerrainOps && S.SharedCacheEntry->RegionMinX == RegionMinX && S.SharedCacheEntry->RegionMinY == RegionMinY && S.SharedCacheEntry->RegionSize == RegionSize; if (!bSharedCacheMatches) { S.SharedCacheEntry = FindOrBuildSharedRoomGraphCache( SeedU, StrateIdx, ParamsFingerprint, LayoutVersion, ManagerLifetimeId, TerrainOps, RegionMinX, RegionMinY, RegionSize, (float)RegionMinX - CacheExpansion, (float)RegionMinY - CacheExpansion, (float)(RegionMinX + RegionSize) + CacheExpansion, (float)(RegionMinY + RegionSize) + CacheExpansion, P, ERoomGraphBuildSite::OpShared); SharedRoomGraphCache = SnapshotSharedRoomGraphCache(S.SharedCacheEntry); } S.ActiveCache = SharedRoomGraphCache.Get(); } else { S.SharedCacheEntry.Reset(); FRoomGraphCacheEntry* LocalCache = FindRoomGraphCacheEntry( SeedU, StrateIdx, ParamsFingerprint, LayoutVersion, ManagerLifetimeId, TerrainOps, RegionMinX, RegionMinY, RegionSize); const bool bCacheMatches = LocalCache->bValid && LocalCache->Seed == SeedU && LocalCache->StrateIndex == StrateIdx && LocalCache->ParamsFingerprint == ParamsFingerprint && LocalCache->LayoutVersion == LayoutVersion && LocalCache->ManagerLifetimeId == ManagerLifetimeId && LocalCache->TerrainOps == TerrainOps && LocalCache->RegionMinX == RegionMinX && LocalCache->RegionMinY == RegionMinY && LocalCache->RegionSize == RegionSize; if (!bCacheMatches) { LocalCache->bValid = false; LocalCache->Seed = SeedU; LocalCache->StrateIndex = StrateIdx; LocalCache->ParamsFingerprint = ParamsFingerprint; LocalCache->LayoutVersion = LayoutVersion; LocalCache->ManagerLifetimeId = ManagerLifetimeId; LocalCache->TerrainOps = TerrainOps; LocalCache->RegionMinX = RegionMinX; LocalCache->RegionMinY = RegionMinY; LocalCache->RegionSize = RegionSize; VoxelCaveMorphology::BuildChunkCache( LocalCache->Cache, (float)RegionMinX - CacheExpansion, (float)RegionMinY - CacheExpansion, (float)(RegionMinX + RegionSize) + CacheExpansion, (float)(RegionMinY + RegionSize) + CacheExpansion, P, SeedU, StrateIdx, TerrainOps, ERoomGraphBuildSite::OpLocal); LocalCache->bValid = true; VoxelDensityOps::ReportWorkerRoomGraphCacheFootprint(); } S.ActiveCache = &LocalCache->Cache; } const bool bUseSpatialIndex = VoxelGenLOD::ShouldUseSpatialIndex(false); const FVector WorldTunnelPosition(WorldX, WorldY, WorldZ); float CaveSDF = VoxelCaveMorphology::EvaluateSDFCached( WarpedX, WarpedY, WarpedZ, GetCache(), P.SDFBlendRadius, &S.NearestRoom, bUseSpatialIndex, &WorldTunnelPosition, &S.bLastRoomOwnsBottom); S.bLastRoomOwnsBottomValid = true; //--------------------------------------------------------------- // PITS & CHEMINÉES — coordonnées RÉELLES, SmoothMin dans le même canal SDF //--------------------------------------------------------------- // C'est le point que `OPSTACK-DECOMPOSITION §2` annonçait comme « le plus retors de toute // la décomposition » : deux primitives qui écrivent le MÊME canal que le graphe de salles // mais à des coordonnées NON warpées. Sous un modèle de frames il aurait fallu les sortir // du frame tout en gardant le canal — exprimable, mais tordu. Dans un opérateur unique la // difficulté disparaît : le warp est une variable locale, pas un contexte hérité. if (!VoxelDensityAblation::IsPitChimneySDFOff()) { auto EvaluatePit = [&](int32 PitIndex) { const FCachedPit& Pit = GetCache().Pits[PitIndex]; const float DZ = WorldZ - Pit.TopZ; if (DZ >= Pit.BlendK) { return; } if (-DZ > Pit.Depth + Pit.BlendK) { return; } const float DX = WorldX - Pit.CenterX; const float DY = WorldY - Pit.CenterY; const float XYDistSq = DX * DX + DY * DY; if (XYDistSq > Pit.BoundXYRadiusSq) { return; } float PitSDF; if (DZ <= 0.0f) { const float DepthBelow = -DZ; float FlareFactor = FMath::Clamp(1.0f - DepthBelow / Pit.FlareDist, 0.0f, 1.0f); FlareFactor = FlareFactor * FlareFactor; const float EffRadius = Pit.Radius + Pit.FlareExtra * FlareFactor; PitSDF = FMath::Sqrt(XYDistSq) - EffRadius; } else { PitSDF = FMath::Sqrt(XYDistSq) - (Pit.Radius + Pit.FlareExtra); } CaveSDF = VoxelSDF::SmoothMin(CaveSDF, PitSDF, Pit.BlendK); }; if (!VoxelDensityAblation::IsPitChimneySDFOff()) { VF_ForEachChunkSDFSpatialCandidate( GetCache().PitSpatialIndex, GetCache().Pits.Num(), WorldX, WorldY, EvaluatePit, bUseSpatialIndex); } auto EvaluateChimney = [&](int32 ChimneyIndex) { const FCachedChimney& Chim = GetCache().Chimneys[ChimneyIndex]; const float DZ = WorldZ - Chim.BottomZ; if (-DZ >= Chim.BlendK) { return; } 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; const float EffRadius = Chim.Radius + Chim.FlareExtra * FlareFactor; ChmSDF = FMath::Sqrt(XYDistSq) - EffRadius; } else { ChmSDF = FMath::Sqrt(XYDistSq) - (Chim.Radius + Chim.FlareExtra); } CaveSDF = VoxelSDF::SmoothMin(CaveSDF, ChmSDF, Chim.BlendK); }; if (!VoxelDensityAblation::IsPitChimneySDFOff()) { VF_ForEachChunkSDFSpatialCandidate( GetCache().ChimneySpatialIndex, GetCache().Chimneys.Num(), WorldX, WorldY, EvaluateChimney, bUseSpatialIndex); } } InOut.Sdf = CaveSDF; // Publish the same continuous tunnel-core field that the common generator tail will // reassert after generic writers. The stack does not publish or protect a separate // support slab. if (!VoxelDensityAblation::IsTunnelCoreOff()) { S.LastTunnelCoreWorldEvaluation = VoxelCaveMorphology::EvaluateTunnelCoreWorld( WorldX, WorldY, WorldZ, GetCache(), nullptr, bUseSpatialIndex, &S.LastWarpedPosition, &S.bLastRoomOwnsBottom); S.LastTunnelCoreEvaluationPosition = S.LastWorldPosition; S.bLastTunnelCoreWorldEvaluationValid = true; // Keep the room-owned bottom hand-off for block evaluation compatibility. The // authored floor remains part of the tunnel shape, but it must not win inside a // room's own volume. InOut.bHasTunnelCoreWorldEvaluation = true; InOut.TunnelCoreWorldSDF = S.LastTunnelCoreWorldEvaluation.SDF; InOut.bTunnelCoreRoomFloor = S.LastTunnelCoreWorldEvaluation.bRoomFloor; InOut.bHasTunnelCoreSweptFloor = S.LastTunnelCoreWorldEvaluation.bHasSweptFloor; InOut.TunnelCoreSweptFloorZ = S.LastTunnelCoreWorldEvaluation.SweptFloorZ; InOut.TunnelCoreSweptFloorRadius = S.LastTunnelCoreWorldEvaluation.SweptFloorRadius; } // These cache hand-offs remain valid when tunnel core is off; detail operators still // need the room graph and nearest-room ownership. InOut.RoomCache = &GetCache(); InOut.NearestRoomIndex = S.NearestRoom; } //--------------------------------------------------------------------- // L'ÉTAT PAR BOÎTE — SÉPARÉ DE `FState`, ET DÉLIBÉRÉMENT //--------------------------------------------------------------------- // `EffectOverBox` construit un cache pour la boîte INTERROGÉE, qui n'est pas la boîte de // recherche que `Eval` construit pour le voxel courant. Les faire partager `FState::Cache` // serait *correct* — la discipline d'invariance de fenêtre de §8.4 garantit qu'un cache bâti // sur une boîte PLUS LARGE donne le même SDF par voxel — mais ça rendrait `ClassifyTile` // capable de perturber le cache chaud d'une génération en cours, et un jour quelqu'un // paierait cette élégance très cher. Un deuxième cache par worker coûte une allocation // amortie ; on la paie. // // Le cache et l'intervalle sont mémoïsés, et ce n'est pas du confort : les DOUZE // modificateurs de détail consultent le même intervalle pour la même boîte. Sans mémo, // une tuile coûterait treize `BuildChunkCache` au lieu d'un. // // Second per-worker cache, on purpose: sharing FState::Cache would be sound but would let // tile classification disturb a live generation's hot cache. The verdict is memoised because // all twelve detail modifiers ask the same question about the same box. struct FBoxState { FChunkSDFCache Cache; const FChunkSDFCache* ActiveCache = nullptr; FBox KeyBox = FBox(ForceInit); int32 KeyStrate = INT32_MIN; uint32 KeySeed = 0; uint32 KeyFingerprint = 0xFFFFFFFFu; uint32 KeyLayout = 0xFFFFFFFFu; uint64 KeyManagerLifetimeId = 0; bool KeyUsesLatticeProof = false; bool KeyTightenWarpProof = false; FIntVector KeyLatticeOrigin = FIntVector::ZeroValue; int32 KeyLatticeStep = 1; bool bValid = false; // A refined tile proof asks the same source about a root box and then many child // boxes. The morphology cache is window-invariant, so keep the root's superset // cache alive for all children instead of rebuilding the graph once per child. FBox CacheWindowBox = FBox(ForceInit); int32 CacheWindowStrate = INT32_MIN; uint32 CacheWindowSeed = 0; uint32 CacheWindowFingerprint = 0xFFFFFFFFu; uint32 CacheWindowLayout = 0xFFFFFFFFu; uint64 CacheWindowManagerLifetimeId = 0; const TArray* CacheWindowTerrainOps = nullptr; bool CacheWindowUsesLatticeProof = false; bool CacheWindowTightenWarpProof = false; int32 CacheWindowLatticeStep = 1; TSharedPtr CacheWindowSharedEntry; int32 CacheWindowSharedRegionMinX = 0; int32 CacheWindowSharedRegionMinY = 0; int32 CacheWindowSharedRegionSize = 0; bool bCacheWindowUsesShared = false; bool CacheWindowUsesTileCacheWindow = false; FIntVector CacheWindowTileOrigin = FIntVector::ZeroValue; int32 CacheWindowTileStep = 0; int32 CacheWindowTileCells = 0; bool bCacheWindowValid = false; // Tight warp bounds are proofs over a box. Refinement asks about nested child boxes, // so one bound for the current root is a sound superset for all descendants. FBox TightWarpEnvelopeBox = FBox(ForceInit); FVector TightWarpEnvelope = FVector::ZeroVector; bool bTightWarpEnvelopeValid = false; // Exact lattice warp queries are shared by all refined children of one classifier // root. The old path rebuilt the same Perlin warp for every small child (64 times on // a uniform LOD0 tile). This is a bounded worker-local scratch array, not a process // cache: it is keyed by the current box-proof state and never retains data for a // different tile or parameter set. TArray ExactWarpedLatticeQueries; // Four-state results for the exact world-space tunnel-post check. This is a bounded // per-window point cache: refinement boxes share boundary vertices, but no interval or // tile verdict is retained here. TArray ExactTailStates; // Per-box exact proofs reuse these bounded working arrays. They are scratch only: the // arrays are reset before every propagation and never retain an interval or verdict. // Keeping their capacity on the worker removes a heap allocation for every refined // LOD0 child while preserving the existing 100000-sample safety cap below. TArray ExactWarpedLatticeScratch; TArray ExactWorldLatticeScratch; FBox ExactWarpedLatticeCacheBox = FBox(ForceInit); int32 ExactWarpedLatticeIX0 = 0; int32 ExactWarpedLatticeIY0 = 0; int32 ExactWarpedLatticeIZ0 = 0; int32 ExactWarpedLatticeNX = 0; int32 ExactWarpedLatticeNY = 0; int32 ExactWarpedLatticeNZ = 0; FIntVector ExactWarpedLatticeOrigin = FIntVector::ZeroValue; int32 ExactWarpedLatticeStep = 0; uint32 ExactWarpedLatticeSeed = 0xFFFFFFFFu; uint32 ExactWarpedLatticeFingerprint = 0xFFFFFFFFu; uint32 ExactWarpedLatticeLayout = 0xFFFFFFFFu; uint64 ExactWarpedLatticeManagerLifetimeId = 0; bool bExactWarpedLatticeCacheValid = false; EVoxelOpEffect Verdict = EVoxelOpEffect::Both; FVoxelBoxSdfInterval SdfInterval; /** DIAGNOSTIC — combien de primitives de chaque classe atteignent la dernière boîte * interrogée, et combien le cache en contenait. Lu par les tests via * `VoxelDensityOps::GetLastRoomBoxDiagnostic`. N'entre dans aucune décision. */ int32 HitRooms = 0, HitTunnels = 0, HitPits = 0, HitChimneys = 0; int32 NumRooms = 0, NumTunnels = 0, NumPits = 0, NumChimneys = 0; /** Les mêmes comptes avec une dilatation de warp NULLE, et de combien de voxels la * boîte est dilatée. C'est la mesure qui manquait pendant trois builds : sans elle, * « les tunnels bloquent » et « ma boîte est 125x trop grosse » sont indiscernables. */ int32 HitRoomsNoWarp = 0, HitTunnelsNoWarp = 0; float WarpDilation = 0.0f; bool bMayHaveTunnelCoreAir = false; bool bMayHaveTunnelSupportFloor = false; bool bMayHaveTunnelCoreTail = false; uint32 ExactTailQueries = 0; uint32 ExactTailEvaluated = 0; uint64 ExactTailCycles = 0; uint64 PropagateCycles = 0; uint64 ExactPrimitiveCycles = 0; uint64 CacheWindowCycles = 0; int32 NumRoomFloorJoins = 0; }; static FBoxState& BoxState() { thread_local FBoxState S; return S; } const FChunkSDFCache& GetBoxCache() const { const FBoxState& B = BoxState(); return B.ActiveCache != nullptr ? *B.ActiveCache : B.Cache; } /** * ✅ LA RÉPONSE SPATIALE. La dette annoncée ici pendant tout le portage est payée. * * Ce que ça débloque, en un mot : la source publie un intervalle SDF et `FSdfConvertOp` * ainsi que les douze modificateurs de détail décident séparément, à partir de cet * intervalle, s'ils sont identités. Une boîte éloignée peut donc éteindre toute la chaîne, * mais aucune source ne répond au nom d'un convertisseur ou d'un modificateur. * * LE CRITÈRE — **UNE PRIMITIVE NE COMPTE PAS SI ELLE RATE SON CULL *OU* SI SON SDF RESTE * AU-DESSUS DU SEUIL `T`.** Une disjonction, pas une seule règle, et chaque branche gagne sur * une classe différente. Le détail de `T` et sa condition de validité sont dans la note * « LE SEUIL T » à l'intérieur de la fonction — la lire avant toute modification. * * • branche CULL — `Eval` part de `MinSDF = FLT_MAX` et ne l'abaisse que via une primitive * qui SURVIT à son cull par voxel. Aucune survivante ⇒ `Sdf` reste `FLT_MAX`. C'est la * même inégalité que le cull, élevée du point à la boîte. **Meilleure pour les salles** : * leur cull (`Rmax + 3K`) est plus serré que le seuil (`Rmax + T + K`). * • branche SEUIL — une primitive peut survivre à son cull et rester malgré tout trop loin * pour qu'un consommateur s'allume. **Meilleure pour les tunnels**, dont le cull est la * sphère englobante d'une capsule : rayon ~107 pour un tube de rayon 7 long de 200. * * Mélanger les deux est sûr : les primitives de la branche cull ne contribuent RIEN, les * autres sont toutes ≥ `T + K`, donc le pli vaut ≥ `T` (voir la saturation de `SmoothMin`), * et les trois consommateurs sont éteints. Une seule raison d'échouer, dans les deux cas. * * LES TROIS CHOSES QUI RENDENT LE TEST CONSERVATIF DU BON CÔTÉ : * 1. le warp déplace la coordonnée de REQUÊTE, donc la boîte est dilatée de sa borne * prouvable avant d'être confrontée aux salles et aux tunnels ; * 2. les pits et les cheminées sont interrogés en coordonnées RÉELLES (voir `Eval`), donc * ils sont confrontés à la boîte NON dilatée — la dilater serait juste plus prudent, ne * pas la dilater pour eux serait faux ; * 3. la boîte de recherche du cache est PLUS LARGE que celle de `Eval`, ce qui donne un * SUR-ensemble de primitives : si rien n'atteint la boîte ici, rien ne l'atteint là-bas. * * The criterion is the per-voxel cull lifted from point to box. The source publishes only * its own SDF interval; the converter and the twelve modifiers consume that interval in * their own state-aware folds. */ EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override { // This source writes SDF only. It must never answer for a converter or modifier that // happens to follow it; the interval is the source's complete box-query contract. return EVoxelOpEffect::Identity; } // This operator publishes SDF and does not change Density. Reporting zero here is // important: FLT_MAX means "an unknown density delta", whereas this source has no density // delta at all. The SDF interval remains the source's separate box contract below. float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override { return 0.0f; } float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override { return 0.0f; } /** * State-aware detail gate support. A room terrain op can replace the strate-level * activation/strength fields for the room selected by Eval. Until the cache tells us * that the queried box contains no such override, a detail bound based on P alone would be * unsound. Returning true on an invalid box state deliberately loses a skip rather than * manufacturing one. */ bool HasRoomTerrainOverrideForLastBox() const { const FBoxState& B = BoxState(); if (!B.bValid) { return true; } for (const FCachedRoom& Room : GetBoxCache().Rooms) { if (Room.RoomOp != nullptr) { return true; } } return false; } float RoomColumnFillSupremumForLastBox() const { const FBoxState& B = BoxState(); if (!B.bValid) { return FLT_MAX; } float Supremum = 0.0f; for (const FCachedColumn& Column : GetBoxCache().Columns) { if (!VoxelMath::IsFinite(Column.BaseDensity) || Column.BaseDensity < 0.0f) { // Eval multiplies by BaseDensity. A negative authored value reverses the // declared FillOnly direction, so the state-aware caller must fall back to // Both/unknown rather than pretend this is a fill envelope. return FLT_MAX; } const float Contribution = Column.BaseDensity * 1.5f; if (!VoxelMath::IsFinite(Contribution) || Supremum > FLT_MAX - Contribution) { return FLT_MAX; } Supremum += Contribution; } return Supremum; } float FloorBiasFillSupremumForLastBox() const { const FBoxState& B = BoxState(); if (!B.bValid) { return FLT_MAX; } // Eval uses NormZ² only below the room centre. The largest value over the queried // box is therefore the squared distance from the room centre to Box.Min.Z, divided by // max(RadiusZ, 1)². Taking the maximum over cached rooms is conservative for whichever // room wins the nearest-SDF query, and is finite without assuming a global room height. float Supremum = 0.0f; const float BoxMinZ = static_cast(B.KeyBox.Min.Z); if (!VoxelMath::IsFinite(BoxMinZ)) { return FLT_MAX; } for (const FCachedRoom& Room : GetBoxCache().Rooms) { const float CenterZ = static_cast(Room.Center.Z); const float RadiusZ = Room.RadiusZ; if (!VoxelMath::IsFinite(CenterZ) || !VoxelMath::IsFinite(RadiusZ) || !VoxelMath::IsFinite(P.FloorBias) || P.FloorBias < 0.0f) { return FLT_MAX; } const float Denominator = FMath::Max(RadiusZ, 1.0f); const float Below = FMath::Max(CenterZ - BoxMinZ, 0.0f); const float Normalized = Below / Denominator; const float Contribution = Normalized * Normalized * P.FloorBias; if (!VoxelMath::IsFinite(Contribution)) { return FLT_MAX; } Supremum = FMath::Max(Supremum, Contribution); } return Supremum; } /** * Publish a conservative interval for this source alone. * * The cache is the same morphology implementation used by Eval, but the box proof never * samples a downstream density op. Rooms and tunnel segments are bounded by the exact * geometric support that Eval's culls admit; pits and chimneys are bounded in their real * (unwarped) coordinates. The final SmoothMin can lower the running minimum by at most K. * * Soundness is the priority: a failed validation, an overlarge scan, or a non-finite * intermediate returns Unknown. That can lose a tile skip; it cannot turn a tile into a * false AllSolid/AllAir result. */ void PropagateSdfOverBox(FVoxelBoxSdfInterval& InOut, const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override { FBoxState& B = BoxState(); const uint64 PropagateStartCycles = FPlatformTime::Cycles64(); B.ExactTailQueries = 0; B.ExactTailEvaluated = 0; B.ExactTailCycles = 0; B.PropagateCycles = 0; B.ExactPrimitiveCycles = 0; B.CacheWindowCycles = 0; B.NumRoomFloorJoins = 0; const UVoxelStrateManager* LiveManager = Manager.Get(); auto InvalidateExactLatticeScratch = [&]() { B.bExactWarpedLatticeCacheValid = false; B.ExactWarpedLatticeQueries.Reset(); B.ExactTailStates.Reset(); B.ExactWarpedLatticeScratch.Reset(); B.ExactWorldLatticeScratch.Reset(); B.ExactWarpedLatticeCacheBox = FBox(ForceInit); B.ExactWarpedLatticeIX0 = B.ExactWarpedLatticeIY0 = 0; B.ExactWarpedLatticeIZ0 = 0; B.ExactWarpedLatticeNX = B.ExactWarpedLatticeNY = 0; B.ExactWarpedLatticeNZ = 0; B.ExactWarpedLatticeOrigin = FIntVector::ZeroValue; B.ExactWarpedLatticeStep = 0; B.ExactWarpedLatticeSeed = 0xFFFFFFFFu; B.ExactWarpedLatticeFingerprint = 0xFFFFFFFFu; B.ExactWarpedLatticeLayout = 0xFFFFFFFFu; B.ExactWarpedLatticeManagerLifetimeId = 0; }; auto Unknown = [&]() { InvalidateExactLatticeScratch(); B.bValid = false; B.ActiveCache = nullptr; B.CacheWindowSharedEntry.Reset(); B.CacheWindowSharedRegionMinX = 0; B.CacheWindowSharedRegionMinY = 0; B.CacheWindowSharedRegionSize = 0; B.bCacheWindowUsesShared = false; B.CacheWindowUsesTileCacheWindow = false; B.CacheWindowTileOrigin = FIntVector::ZeroValue; B.CacheWindowTileStep = 0; B.CacheWindowTileCells = 0; B.bCacheWindowValid = false; B.bTightWarpEnvelopeValid = false; B.bMayHaveTunnelCoreAir = false; B.bMayHaveTunnelSupportFloor = false; B.bMayHaveTunnelCoreTail = false; B.SdfInterval.SetUnknown(); InOut.SetUnknown(); }; auto Finite = [](float V) { return VoxelMath::IsFinite(V); }; if (ManagerLifetimeId != 0 && LiveManager == nullptr) { Unknown(); return; } const float BoxMinX = (float)VoxelBox.Min.X; const float BoxMinY = (float)VoxelBox.Min.Y; const float BoxMinZ = (float)VoxelBox.Min.Z; const float BoxMaxX = (float)VoxelBox.Max.X; const float BoxMaxY = (float)VoxelBox.Max.Y; const float BoxMaxZ = (float)VoxelBox.Max.Z; if (!Finite(BoxMinX) || !Finite(BoxMinY) || !Finite(BoxMinZ) || !Finite(BoxMaxX) || !Finite(BoxMaxY) || !Finite(BoxMaxZ) || BoxMinX > BoxMaxX || BoxMinY > BoxMaxY || BoxMinZ > BoxMaxZ) { Unknown(); return; } // These are the fields that influence the source geometry or its cache window. If an // authored asset leaves the proven range, the safe answer is Unknown, never a guessed // "reasonable" radius. Non-positive room density/spacing is an exact Eval no-op. const float RelevantParams[] = { P.RoomDensity, P.RoomSpacing, P.MinRoomRadius, P.MaxRoomRadius, P.RoomHeightRatio, P.RoomFloorCutMin, P.RoomFloorCutMax, P.FloorReliefStrength, P.FloorReliefFrequency, P.RoomShapeVariety, P.OriginRoomRadius, P.TunnelMinRadius, P.TunnelMaxRadius, P.TunnelDensity, P.MaxTunnelLength, P.TunnelWarpStrength, P.TunnelHorizontalBias, P.TunnelEndpointZOffset, P.SDFBlendRadius, P.CaveWarpStrength, P.CaveWarpFrequency, P.VerticalScale }; for (const float V : RelevantParams) { if (!Finite(V)) { Unknown(); return; } } if (!(P.RoomDensity > 0.0f && P.RoomSpacing > 0.0f)) { InvalidateExactLatticeScratch(); B.bValid = false; B.ActiveCache = nullptr; B.CacheWindowSharedEntry.Reset(); B.bCacheWindowUsesShared = false; B.bCacheWindowValid = false; B.NumRooms = B.NumTunnels = B.NumPits = B.NumChimneys = 0; B.HitRooms = B.HitTunnels = B.HitPits = B.HitChimneys = 0; B.HitRoomsNoWarp = B.HitTunnelsNoWarp = 0; B.WarpDilation = 0.0f; B.bMayHaveTunnelCoreAir = false; B.bMayHaveTunnelSupportFloor = false; B.bMayHaveTunnelCoreTail = false; B.SdfInterval.Set(FLT_MAX, FLT_MAX); InOut = B.SdfInterval; return; } if (P.MinRoomRadius < 0.0f || P.MaxRoomRadius < 0.0f || P.RoomHeightRatio < 0.0f || P.OriginRoomRadius < 0.0f || P.TunnelMinRadius < 0.0f || P.TunnelMaxRadius < 0.0f || P.SDFBlendRadius < 0.0f) { Unknown(); return; } const int32 CX0 = FMath::FloorToInt(BoxMinX / (float)CHUNK_SIZE); const int32 CY0 = FMath::FloorToInt(BoxMinY / (float)CHUNK_SIZE); const int32 CZ0 = FMath::FloorToInt(BoxMinZ / (float)CHUNK_SIZE); const int32 CX1 = FMath::FloorToInt(BoxMaxX / (float)CHUNK_SIZE); const int32 CY1 = FMath::FloorToInt(BoxMaxY / (float)CHUNK_SIZE); const int32 CZ1 = FMath::FloorToInt(BoxMaxZ / (float)CHUNK_SIZE); const int64 SpanX = (int64)CX1 - (int64)CX0 + 1; const int64 SpanY = (int64)CY1 - (int64)CY0 + 1; const int64 SpanZ = (int64)CZ1 - (int64)CZ0 + 1; if (SpanX <= 0 || SpanY <= 0 || SpanZ <= 0) { Unknown(); return; } int32 StrateIdx = 0; const TArray* TerrainOps = nullptr; if (LiveManager) { StrateIdx = LiveManager->GetStrateIndex( ((float)CZ0 + 0.5f) * CHUNK_SIZE * VOXEL_SIZE); for (int32 CZ = CZ0 + 1; CZ <= CZ1; ++CZ) { if (LiveManager->GetStrateIndex( ((float)CZ + 0.5f) * CHUNK_SIZE * VOXEL_SIZE) != StrateIdx) { Unknown(); return; } } // A room operation is part of the graph cache. Every Z slice covered by this // proof must resolve to the same definition before one cache can represent it. UVoxelStrateDefinition* Def0 = nullptr; for (int32 CZ = CZ0; CZ <= CZ1; ++CZ) { UVoxelStrateDefinition* Def = LiveManager->GetStrateForChunk(FIntVector(0, 0, CZ)); if (CZ == CZ0) { Def0 = Def; } else if (Def != Def0) { Unknown(); return; } } if (Def0) { TerrainOps = &Def0->TerrainOperations; } } FIntVector RequestedTileOrigin = FIntVector::ZeroValue; int32 RequestedTileStep = FMath::Max(Ctx.Step, 1); int32 RequestedTileCells = 0; const bool bUseTileCacheWindow = VoxelGenLOD::IsTileCacheWindowEnabled(false) && VoxelGenLOD::GetThreadTileCacheWindow( RequestedTileOrigin, RequestedTileStep, RequestedTileCells) && VoxelCaveMorphology::IsRoomGraphWindowInvariant(P, TerrainOps); const float RequestedTileExtent = static_cast( static_cast(RequestedTileStep) * static_cast(RequestedTileCells)); // FillChunk includes one sample-step of mesher halo around the requested tile. // Keep the tile footprint as the shared-cache identity, but let every block in that // halo reuse the same cache window. The morphology search below includes the same // coverage, so this widens reuse without changing the set of values evaluated. const FBox CacheWindowCoverageBox = bUseTileCacheWindow ? FBox( FVector( static_cast(RequestedTileOrigin.X) - static_cast(RequestedTileStep), static_cast(RequestedTileOrigin.Y) - static_cast(RequestedTileStep), static_cast(RequestedTileOrigin.Z) - static_cast(RequestedTileStep)), FVector( static_cast(RequestedTileOrigin.X) + RequestedTileExtent, static_cast(RequestedTileOrigin.Y) + RequestedTileExtent, static_cast(RequestedTileOrigin.Z) + RequestedTileExtent) + FVector( static_cast(RequestedTileStep), static_cast(RequestedTileStep), static_cast(RequestedTileStep))) : VoxelBox; const int64 RequestedTileChunkSpan = bUseTileCacheWindow ? FMath::Max( 1, (static_cast(RequestedTileExtent) + CHUNK_SIZE - 1) / CHUNK_SIZE) : 0; // A coarse tile is allowed to warm the proof, but never by allocating a morphology // cache for the whole tile. At LOD3/4 the root can cover hundreds or thousands of // chunk keys; that is a proof-domain size, not a useful working set. Descendants // are still queried normally and can use the bounded shared windows below. Unknown // is deliberately the only answer here: it cannot authorize an empty-tile skip. constexpr int64 MaxLatticeProofChunkVolume = 512; if (Ctx.bUseLatticeProof && SpanX * SpanY * SpanZ > MaxLatticeProofChunkVolume && !(bUseTileCacheWindow && RequestedTileChunkSpan <= 64 && SpanZ <= 16)) { Unknown(); return; } // The ordinary (non-lattice) proof keeps its original bounded query contract. A // tile proof is different: its first query is the root box and its descendants are // all inside that box. Build the root cache once even when the root spans more than // 64 chunk keys, then let the bounded child boxes use that same superset. This is // what prevents a step-32 tile from paying one graph bake per coarse sample. constexpr int64 MaxNonLatticeChunkSpan = 64; if (!Ctx.bUseLatticeProof && SpanX * SpanY * SpanZ > MaxNonLatticeChunkSpan) { Unknown(); return; } const uint32 LV = Ctx.LayoutVersion; auto IsSharedCacheCurrent = [&]() -> bool { const FSharedRoomGraphCacheEntry* Entry = B.CacheWindowSharedEntry.Get(); return Entry != nullptr && Entry->Seed == SeedU && Entry->StrateIndex == StrateIdx && Entry->ParamsFingerprint == ParamsFingerprint && Entry->LayoutVersion == LayoutVersion && Entry->ManagerLifetimeId == ManagerLifetimeId && Entry->TerrainOps == TerrainOps && Entry->RegionMinX == B.CacheWindowSharedRegionMinX && Entry->RegionMinY == B.CacheWindowSharedRegionMinY && Entry->RegionSize == B.CacheWindowSharedRegionSize; }; if (B.bValid && B.KeyBox == VoxelBox && B.KeyStrate == StrateIdx && B.KeySeed == SeedU && B.KeyFingerprint == ParamsFingerprint && B.KeyLayout == LV && B.KeyManagerLifetimeId == ManagerLifetimeId && B.KeyUsesLatticeProof == Ctx.bUseLatticeProof && (!B.bCacheWindowUsesShared || IsSharedCacheCurrent()) && B.KeyTightenWarpProof == Ctx.bTightenWarpProof && (!Ctx.bUseLatticeProof || (B.KeyLatticeOrigin == Ctx.LatticeOriginVoxels && B.KeyLatticeStep == Ctx.Step))) { InOut = B.SdfInterval; return; } const float EffectiveMinZ = (P.VerticalScale > 0.0f && P.VerticalScale != 1.0f) ? BoxMinZ / P.VerticalScale : BoxMinZ; const float EffectiveMaxZ = (P.VerticalScale > 0.0f && P.VerticalScale != 1.0f) ? BoxMaxZ / P.VerticalScale : BoxMaxZ; const float GlobalWarp = (P.CaveWarpStrength > 0.0f) ? P.CaveWarpStrength * VOXEL_NOISE_SCALE * VF_PerlinAbsBound : 0.0f; if (!Finite(GlobalWarp) || !Finite(EffectiveMinZ) || !Finite(EffectiveMaxZ)) { Unknown(); return; } FVector WarpEnvelope(GlobalWarp, GlobalWarp, GlobalWarp); // The finite Perlin interval is useful at the root too when the root is one ordinary // LOD0 tile wide. Compute it once and retain it as a superset for every descendant; // otherwise each refined child repeats the same finite-domain envelope work. The // helper falls back to the global theorem for larger/non-finite noise domains, so // widening this gate cannot weaken soundness. const bool bUseFiniteWarpEnvelope = Ctx.bTightenWarpProof && FMath::Max3(BoxMaxX - BoxMinX, BoxMaxY - BoxMinY, BoxMaxZ - BoxMinZ) <= 32.0f * static_cast(FMath::Max(Ctx.Step, 1)); if (bUseFiniteWarpEnvelope && P.CaveWarpStrength > 0.0f && P.CaveWarpFrequency > 0.0f) { const bool bCanReuseTightEnvelope = B.bTightWarpEnvelopeValid && B.TightWarpEnvelopeBox.Min.X <= BoxMinX && B.TightWarpEnvelopeBox.Min.Y <= BoxMinY && B.TightWarpEnvelopeBox.Min.Z <= BoxMinZ && B.TightWarpEnvelopeBox.Max.X >= BoxMaxX && B.TightWarpEnvelopeBox.Max.Y >= BoxMaxY && B.TightWarpEnvelopeBox.Max.Z >= BoxMaxZ; if (bCanReuseTightEnvelope) { WarpEnvelope = B.TightWarpEnvelope; } else { const float WarpAmplitude = P.CaveWarpStrength * VOXEL_NOISE_SCALE; WarpEnvelope.X = WarpAmplitude * VF_PerlinAbsBoundOverWorldBox( BoxMinX, BoxMaxX, BoxMinY, BoxMaxY, EffectiveMinZ, EffectiveMaxZ, P.CaveWarpFrequency, VoxelHash::SeedOffset(SeedU, 0.37f), 1.3f, 5.7f); WarpEnvelope.Y = WarpAmplitude * VF_PerlinAbsBoundOverWorldBox( BoxMinX, BoxMaxX, BoxMinY, BoxMaxY, EffectiveMinZ, EffectiveMaxZ, P.CaveWarpFrequency, 7.1f, VoxelHash::SeedOffset(SeedU, 0.59f), 2.3f); WarpEnvelope.Z = WarpAmplitude * VF_PerlinAbsBoundOverWorldBox( BoxMinX, BoxMaxX, BoxMinY, BoxMaxY, EffectiveMinZ, EffectiveMaxZ, P.CaveWarpFrequency, 11.3f, 9.7f, VoxelHash::SeedOffset(SeedU, 0.41f)); B.TightWarpEnvelopeBox = VoxelBox; B.TightWarpEnvelope = WarpEnvelope; B.bTightWarpEnvelopeValid = true; } if (!Finite((float)WarpEnvelope.X) || !Finite((float)WarpEnvelope.Y) || !Finite((float)WarpEnvelope.Z)) { Unknown(); return; } } // A tile cache is a graph cache, not a proof-result cache. Its search envelope must // therefore be identical for the root and every block/child that asks about the // tile. In particular, a finite tight warp envelope is allowed to sharpen the // interval proof below, but it must not make the first classifier block publish a // smaller morphology cache for later blocks to reuse. const float CacheWarpX = bUseTileCacheWindow ? GlobalWarp : FMath::Max((float)WarpEnvelope.X, 0.0f); const float CacheWarpY = bUseTileCacheWindow ? GlobalWarp : FMath::Max((float)WarpEnvelope.Y, 0.0f); const float CacheWarpZ = bUseTileCacheWindow ? GlobalWarp : FMath::Max((float)WarpEnvelope.Z, 0.0f); const float CacheSearchMinX = bUseTileCacheWindow ? static_cast(RequestedTileOrigin.X) - CacheWarpX - static_cast(RequestedTileStep) - 2.0f : BoxMinX - CacheWarpX - 2.0f; const float CacheSearchMinY = bUseTileCacheWindow ? static_cast(RequestedTileOrigin.Y) - CacheWarpY - static_cast(RequestedTileStep) - 2.0f : BoxMinY - CacheWarpY - 2.0f; const float CacheSearchMaxX = bUseTileCacheWindow ? static_cast(RequestedTileOrigin.X) + RequestedTileExtent + CacheWarpX + static_cast(RequestedTileStep) + 2.0f : BoxMaxX + CacheWarpX + 2.0f; const float CacheSearchMaxY = bUseTileCacheWindow ? static_cast(RequestedTileOrigin.Y) + RequestedTileExtent + CacheWarpY + static_cast(RequestedTileStep) + 2.0f : BoxMaxY + CacheWarpY + 2.0f; const float CacheSearchMinZ = bUseTileCacheWindow ? static_cast(RequestedTileOrigin.Z) - CacheWarpZ - static_cast(RequestedTileStep) - 2.0f : BoxMinZ - CacheWarpZ - 2.0f; const float CacheSearchMaxZ = bUseTileCacheWindow ? static_cast(RequestedTileOrigin.Z) + RequestedTileExtent + CacheWarpZ + static_cast(RequestedTileStep) + 2.0f : BoxMaxZ + CacheWarpZ + 2.0f; // The search window is a superset of every warped point in the box, with the same // two-voxel gradient margin used by Eval's cache path. In lattice mode, retain the // first/root window and reuse it for every refined child. A larger morphology // window is sound for a child because BuildChunkCache's collect/store contract is // explicitly window-invariant; it is also the only way to keep refinement from // turning into one graph build per child box. const bool bWindowKeyMatches = B.bCacheWindowValid && B.CacheWindowStrate == StrateIdx && B.CacheWindowSeed == SeedU && B.CacheWindowFingerprint == ParamsFingerprint && B.CacheWindowLayout == LV && B.CacheWindowManagerLifetimeId == ManagerLifetimeId && B.CacheWindowTerrainOps == TerrainOps && B.CacheWindowUsesLatticeProof == Ctx.bUseLatticeProof && B.CacheWindowUsesTileCacheWindow == bUseTileCacheWindow && (!bUseTileCacheWindow || (B.CacheWindowTileOrigin == RequestedTileOrigin && B.CacheWindowTileStep == RequestedTileStep && B.CacheWindowTileCells == RequestedTileCells)) // A non-tight cache is built from the global warp envelope and is therefore a // superset cache for a later tight retry. The reverse is not safe: a tight cache // may omit primitives needed by the conservative global-envelope query. && (!Ctx.bUseLatticeProof // Tight queries may use either an equally tight cache or the larger global // envelope cache. A global query must not reuse a tight cache. || Ctx.bTightenWarpProof || !B.CacheWindowTightenWarpProof) // The graph window is independent of which block's lattice origin requested it; // retain only the step because it controls the conservative finite-envelope gate // for non-tile proof windows. && (!Ctx.bUseLatticeProof || B.CacheWindowLatticeStep == Ctx.Step) && (!B.bCacheWindowUsesShared || IsSharedCacheCurrent()) && B.CacheWindowBox.Min.X <= BoxMinX && B.CacheWindowBox.Min.Y <= BoxMinY && B.CacheWindowBox.Min.Z <= BoxMinZ && B.CacheWindowBox.Max.X >= BoxMaxX && B.CacheWindowBox.Max.Y >= BoxMaxY && B.CacheWindowBox.Max.Z >= BoxMaxZ; if (!bWindowKeyMatches) { const uint64 CacheWindowStartCycles = FPlatformTime::Cycles64(); InvalidateExactLatticeScratch(); B.ActiveCache = nullptr; B.CacheWindowSharedEntry.Reset(); B.CacheWindowSharedRegionMinX = 0; B.CacheWindowSharedRegionMinY = 0; B.CacheWindowSharedRegionSize = 0; B.bCacheWindowUsesShared = false; B.bTightWarpEnvelopeValid = false; // The morphology builder is window-invariant. For the bounded lattice boxes, // reuse the worker-local graph cache used by Eval, but at a 128/256-voxel region // granularity. A classifier root otherwise rebuilds the same collect/NN graph // for every neighbouring tile before the regular density path has a chance to // populate its cache. The shared window is always built from the global warp // envelope, so it is a superset of this tight query and can only add conservative // primitives to the interval. const bool bFeatureFree = !VoxelCaveMorphology::MayHaveFeatureInSearchBox( CacheSearchMinX, CacheSearchMinY, CacheSearchMaxX, CacheSearchMaxY, P, SeedU, StrateIdx, true, CacheSearchMinZ, CacheSearchMaxZ, TerrainOps); const int64 MaxSharedChunkSpan = Ctx.Step >= 32 ? 32 : 16; bool bUsedSharedCache = false; const bool bTileWithinSharedBudget = bUseTileCacheWindow && RequestedTileChunkSpan <= 64; if (!bFeatureFree && Ctx.bUseLatticeProof // Fine LOD0/LOD1 requests are latency-sensitive and each worker already has // a bounded graph cache. Sharing their tiny 64-voxel windows makes otherwise // sub-millisecond empty proofs wait behind a different worker's build (the // build itself is cheap). Coarse samples keep the shared window, where its // larger footprint prevents thousands of per-chunk rebuilds. && Ctx.Step > 1 && (bTileWithinSharedBudget || (SpanX <= MaxSharedChunkSpan && SpanY <= MaxSharedChunkSpan))) { // Coarser LODs retain a shared immutable cache. With the tile policy its key // is the exact tile footprint; the fallback keeps the historical expanding // four-chunk working set for point/non-tile callers. int32 SharedRegionSize = bUseTileCacheWindow ? static_cast(RequestedTileExtent) : 4 * CHUNK_SIZE; int32 SharedRegionMinX = bUseTileCacheWindow ? RequestedTileOrigin.X : 0; int32 SharedRegionMinY = bUseTileCacheWindow ? RequestedTileOrigin.Y : 0; bool bCoversBox = bUseTileCacheWindow ? (CacheWindowCoverageBox.Min.X <= BoxMinX && CacheWindowCoverageBox.Min.Y <= BoxMinY && CacheWindowCoverageBox.Max.X >= BoxMaxX && CacheWindowCoverageBox.Max.Y >= BoxMaxY) : false; for (int32 Attempt = 0; !bUseTileCacheWindow && Attempt < 6; ++Attempt) { SharedRegionMinX = FMath::FloorToInt( BoxMinX / (float)SharedRegionSize) * SharedRegionSize; SharedRegionMinY = FMath::FloorToInt( BoxMinY / (float)SharedRegionSize) * SharedRegionSize; bCoversBox = BoxMaxX <= (float)(SharedRegionMinX + SharedRegionSize) && BoxMaxY <= (float)(SharedRegionMinY + SharedRegionSize); if (bCoversBox) { break; } SharedRegionSize *= 2; } if (bCoversBox && (bUseTileCacheWindow || SharedRegionSize <= static_cast(MaxSharedChunkSpan) * CHUNK_SIZE)) { TSharedPtr SharedCache = FindOrBuildSharedRoomGraphCache( SeedU, StrateIdx, ParamsFingerprint, LayoutVersion, ManagerLifetimeId, TerrainOps, SharedRegionMinX, SharedRegionMinY, SharedRegionSize, bUseTileCacheWindow ? CacheSearchMinX : (float)SharedRegionMinX - (FMath::Abs(P.CaveWarpStrength) * VOXEL_NOISE_SCALE * VF_PerlinAbsBound + 2.0f), bUseTileCacheWindow ? CacheSearchMinY : (float)SharedRegionMinY - (FMath::Abs(P.CaveWarpStrength) * VOXEL_NOISE_SCALE * VF_PerlinAbsBound + 2.0f), bUseTileCacheWindow ? CacheSearchMaxX : (float)(SharedRegionMinX + SharedRegionSize) + (FMath::Abs(P.CaveWarpStrength) * VOXEL_NOISE_SCALE * VF_PerlinAbsBound + 2.0f), bUseTileCacheWindow ? CacheSearchMaxY : (float)(SharedRegionMinY + SharedRegionSize) + (FMath::Abs(P.CaveWarpStrength) * VOXEL_NOISE_SCALE * VF_PerlinAbsBound + 2.0f), P, ERoomGraphBuildSite::ClassifierShared); const TSharedPtr SharedRoomGraphCache = SnapshotSharedRoomGraphCache(SharedCache); B.ActiveCache = SharedRoomGraphCache.Get(); B.CacheWindowSharedEntry = SharedCache; B.CacheWindowSharedRegionMinX = SharedRegionMinX; B.CacheWindowSharedRegionMinY = SharedRegionMinY; B.CacheWindowSharedRegionSize = SharedRegionSize; B.bCacheWindowUsesShared = true; bUsedSharedCache = true; } } if (!bUsedSharedCache) { if (bFeatureFree) { // Keep an empty worker-local cache object as the active // window. Its arrays retain capacity, but no graph // build or player-fit bake is paid for this box. B.Cache.Release(); } else { VoxelCaveMorphology::BuildChunkCache( B.Cache, CacheSearchMinX, CacheSearchMinY, CacheSearchMaxX, CacheSearchMaxY, P, SeedU, StrateIdx, TerrainOps, ERoomGraphBuildSite::ClassifierLocal); } B.ActiveCache = &B.Cache; } B.CacheWindowBox = CacheWindowCoverageBox; B.CacheWindowStrate = StrateIdx; B.CacheWindowSeed = SeedU; B.CacheWindowFingerprint = ParamsFingerprint; B.CacheWindowLayout = LV; B.CacheWindowManagerLifetimeId = ManagerLifetimeId; B.CacheWindowTerrainOps = TerrainOps; B.CacheWindowUsesLatticeProof = Ctx.bUseLatticeProof; B.CacheWindowUsesTileCacheWindow = bUseTileCacheWindow; B.CacheWindowTileOrigin = bUseTileCacheWindow ? RequestedTileOrigin : FIntVector::ZeroValue; B.CacheWindowTileStep = bUseTileCacheWindow ? RequestedTileStep : 0; B.CacheWindowTileCells = bUseTileCacheWindow ? RequestedTileCells : 0; B.CacheWindowTightenWarpProof = bUseTileCacheWindow ? false : bUsedSharedCache ? false : Ctx.bTightenWarpProof; B.CacheWindowLatticeStep = Ctx.Step; B.bCacheWindowValid = true; B.CacheWindowCycles = FPlatformTime::Cycles64() - CacheWindowStartCycles; } else if (B.bCacheWindowUsesShared) { B.ActiveCache = SnapshotSharedRoomGraphCache( B.CacheWindowSharedEntry).Get(); } // Large root boxes are only a cache warm-up. Their interval is intentionally still // unknown; proof resumes at bounded descendants where all bounds below are finite. if (SpanX * SpanY * SpanZ > 64) { // Keep the freshly built window alive for the descendants. This is deliberately // different from Unknown(): the root is not invalid, it is merely too large to // publish a finite interval in one pass. B.bValid = false; B.bMayHaveTunnelCoreAir = false; B.bMayHaveTunnelSupportFloor = false; B.bMayHaveTunnelCoreTail = false; B.SdfInterval.SetUnknown(); InOut.SetUnknown(); return; } const FVector QMin(BoxMinX - (float)WarpEnvelope.X, BoxMinY - (float)WarpEnvelope.Y, EffectiveMinZ - (float)WarpEnvelope.Z); const FVector QMax(BoxMaxX + (float)WarpEnvelope.X, BoxMaxY + (float)WarpEnvelope.Y, EffectiveMaxZ + (float)WarpEnvelope.Z); const FVector RMin(BoxMinX, BoxMinY, BoxMinZ); const FVector RMax(BoxMaxX, BoxMaxY, BoxMaxZ); if (!Finite((float)QMin.X) || !Finite((float)QMin.Y) || !Finite((float)QMin.Z) || !Finite((float)QMax.X) || !Finite((float)QMax.Y) || !Finite((float)QMax.Z)) { Unknown(); return; } // In lattice mode the queried box is a proof domain for the exact MC samples, not // for every continuous point between them. The room/tunnel bounds below therefore // use the nearest lattice coordinate in each axis and subtract the same per-axis warp // envelope as the continuous proof. A sub-box containing no lattice point has no // samples to constrain and contributes the exact SDF identity. const bool bUseLatticeProof = Ctx.bUseLatticeProof; const float LatticeOriginX = static_cast(Ctx.LatticeOriginVoxels.X); const float LatticeOriginY = static_cast(Ctx.LatticeOriginVoxels.Y); const float LatticeOriginZ = (P.VerticalScale > 0.0f && P.VerticalScale != 1.0f) ? static_cast(Ctx.LatticeOriginVoxels.Z) / P.VerticalScale : static_cast(Ctx.LatticeOriginVoxels.Z); const float LatticeStepXY = static_cast(Ctx.Step); const float LatticeStepZ = (P.VerticalScale > 0.0f && P.VerticalScale != 1.0f) ? static_cast(Ctx.Step) / P.VerticalScale : static_cast(Ctx.Step); if (bUseLatticeProof) { auto HasAxisSample = [](float Min, float Max, float Origin, float Step) -> bool { return Step > 0.0f && FMath::CeilToInt((Min - Origin) / Step - 1.0e-4f) <= FMath::FloorToInt((Max - Origin) / Step + 1.0e-4f); }; if (!HasAxisSample(BoxMinX, BoxMaxX, LatticeOriginX, LatticeStepXY) || !HasAxisSample(BoxMinY, BoxMaxY, LatticeOriginY, LatticeStepXY) || !HasAxisSample(BoxMinZ, BoxMaxZ, LatticeOriginZ, LatticeStepZ)) { InvalidateExactLatticeScratch(); B.bValid = false; B.ActiveCache = nullptr; B.CacheWindowSharedEntry.Reset(); B.bCacheWindowUsesShared = false; B.bCacheWindowValid = false; B.SdfInterval.Set(FLT_MAX, FLT_MAX); InOut = B.SdfInterval; return; } } // A tight retry is a proof over the exact lattice the mesher will read. Keep both // coordinate spaces: the room graph is queried in warped SDF space, while pits and // chimneys deliberately use the original world-space sample. The old implementation // retained only an AABB at LOD0; that still admitted a tunnel when the warped samples // themselves missed it. The exact primitive minima below remove that false overlap // without changing the lattice or the density evaluator used by the mesher. TArray& ExactLatticeWarpedQueries = B.ExactWarpedLatticeScratch; TArray& ExactLatticeWorldQueries = B.ExactWorldLatticeScratch; ExactLatticeWarpedQueries.Reset(); ExactLatticeWorldQueries.Reset(); FVector ExactLatticeQueryMin(FLT_MAX, FLT_MAX, FLT_MAX); FVector ExactLatticeQueryMax(-FLT_MAX, -FLT_MAX, -FLT_MAX); int32 ExactIX0 = 0, ExactIY0 = 0, ExactIZ0 = 0; int32 ExactNX = 0, ExactNY = 0, ExactNZ = 0; struct FExactLatticeBlock { int32 IX0 = 0, IX1 = -1; int32 IY0 = 0, IY1 = -1; int32 IZ0 = 0, IZ1 = -1; FVector WarpedMin = FVector(FLT_MAX, FLT_MAX, FLT_MAX); FVector WarpedMax = FVector(-FLT_MAX, -FLT_MAX, -FLT_MAX); FVector WorldMin = FVector(FLT_MAX, FLT_MAX, FLT_MAX); FVector WorldMax = FVector(-FLT_MAX, -FLT_MAX, -FLT_MAX); }; TArray> ExactLatticeBlocks; const float MaxBoxExtent = FMath::Max3( BoxMaxX - BoxMinX, BoxMaxY - BoxMinY, BoxMaxZ - BoxMinZ); const float ExactChildExtent = 16.0f * (float)FMath::Max(Ctx.Step, 1); const bool bTryExactLatticeWarp = bUseLatticeProof && Ctx.bTightenWarpProof // The root is a cache warm-up. Exact warped lattice distances are only needed // once the refinement has reduced the domain to a child; this avoids rebuilding // a 33^3 query list before the eight 17^3 child proofs that actually decide the // residual solid tile. A non-exact root remains conservative and cannot create a // skip on its own. // Include a box exactly one coarse child wide. This lets a depth-one LOD0 node // use the exact warped lattice certificate before it fans out into eight smaller // proofs; the certificate remains conservative because it samples only the same // lattice points the mesher reads. && MaxBoxExtent <= ExactChildExtent; if (bTryExactLatticeWarp) { const int32 IX0 = FMath::CeilToInt( (BoxMinX - LatticeOriginX) / LatticeStepXY - 1.0e-4f); const int32 IY0 = FMath::CeilToInt( (BoxMinY - LatticeOriginY) / LatticeStepXY - 1.0e-4f); const int32 IZ0 = FMath::CeilToInt( (BoxMinZ - static_cast(Ctx.LatticeOriginVoxels.Z)) / static_cast(Ctx.Step) - 1.0e-4f); const int32 IX1 = FMath::FloorToInt( (BoxMaxX - LatticeOriginX) / LatticeStepXY + 1.0e-4f); const int32 IY1 = FMath::FloorToInt( (BoxMaxY - LatticeOriginY) / LatticeStepXY + 1.0e-4f); const int32 IZ1 = FMath::FloorToInt( (BoxMaxZ - static_cast(Ctx.LatticeOriginVoxels.Z)) / static_cast(Ctx.Step) + 1.0e-4f); const int64 Count = ((int64)IX1 - IX0 + 1) * ((int64)IY1 - IY0 + 1) * ((int64)IZ1 - IZ0 + 1); if (IX1 >= IX0 && IY1 >= IY0 && IZ1 >= IZ0 && Count > 0 && Count <= 100000) { ExactIX0 = IX0; ExactIY0 = IY0; ExactIZ0 = IZ0; ExactNX = IX1 - IX0 + 1; ExactNY = IY1 - IY0 + 1; ExactNZ = IZ1 - IZ0 + 1; ExactLatticeWarpedQueries.Reserve(static_cast(Count)); ExactLatticeWorldQueries.Reserve(static_cast(Count)); const float WarpAmplitude = P.CaveWarpStrength * VOXEL_NOISE_SCALE; auto MakeWarpedQuery = [&](int32 QueryIX, int32 QueryIY, int32 QueryIZ) -> FVector { const float WorldX = LatticeOriginX + (float)QueryIX * LatticeStepXY; const float WorldY = LatticeOriginY + (float)QueryIY * LatticeStepXY; const float WorldZ = static_cast(Ctx.LatticeOriginVoxels.Z) + (float)QueryIZ * (float)Ctx.Step; const float EffectiveZ = (P.VerticalScale > 0.0f && P.VerticalScale != 1.0f) ? WorldZ / P.VerticalScale : WorldZ; FVector Query(WorldX, WorldY, EffectiveZ); if (!VoxelDensityAblation::IsCaveWarpOff() && P.CaveWarpStrength > 0.0f) { const float Frequency = P.CaveWarpFrequency; float WarpX, WarpY, WarpZ; VoxelNoise::Perlin3D_x3( WorldX * Frequency + VoxelHash::SeedOffset(SeedU, 0.37f), WorldY * Frequency + 1.3f, EffectiveZ * Frequency + 5.7f, WorldX * Frequency + 7.1f, WorldY * Frequency + VoxelHash::SeedOffset(SeedU, 0.59f), EffectiveZ * Frequency + 2.3f, WorldX * Frequency + 11.3f, WorldY * Frequency + 9.7f, EffectiveZ * Frequency + VoxelHash::SeedOffset(SeedU, 0.41f), WarpX, WarpY, WarpZ); Query.X += WarpX * WarpAmplitude; Query.Y += WarpY * WarpAmplitude; Query.Z += WarpZ * WarpAmplitude; } return Query; }; // All exact child boxes are contained in the classifier root held by the // window cache. Cache only the deterministic warp samples, not any interval or // verdict, so a cache miss can only cost work. The 100000-sample cap bounds // this worker-local scratch allocation even for an unusually large root. bool bCanUseWarpedQueryCache = false; int32 CacheIX0 = 0; int32 CacheIY0 = 0; int32 CacheIZ0 = 0; int32 CacheNX = 0; int32 CacheNY = 0; int32 CacheNZ = 0; if (B.bCacheWindowValid && Finite((float)B.CacheWindowBox.Min.X) && Finite((float)B.CacheWindowBox.Min.Y) && Finite((float)B.CacheWindowBox.Min.Z) && Finite((float)B.CacheWindowBox.Max.X) && Finite((float)B.CacheWindowBox.Max.Y) && Finite((float)B.CacheWindowBox.Max.Z) && B.CacheWindowBox.Min.X <= B.CacheWindowBox.Max.X && B.CacheWindowBox.Min.Y <= B.CacheWindowBox.Max.Y && B.CacheWindowBox.Min.Z <= B.CacheWindowBox.Max.Z) { CacheIX0 = FMath::CeilToInt( ((float)B.CacheWindowBox.Min.X - LatticeOriginX) / LatticeStepXY - 1.0e-4f); CacheIY0 = FMath::CeilToInt( ((float)B.CacheWindowBox.Min.Y - LatticeOriginY) / LatticeStepXY - 1.0e-4f); CacheIZ0 = FMath::CeilToInt( ((float)B.CacheWindowBox.Min.Z - static_cast(Ctx.LatticeOriginVoxels.Z)) / static_cast(Ctx.Step) - 1.0e-4f); const int32 CacheIX1 = FMath::FloorToInt( ((float)B.CacheWindowBox.Max.X - LatticeOriginX) / LatticeStepXY + 1.0e-4f); const int32 CacheIY1 = FMath::FloorToInt( ((float)B.CacheWindowBox.Max.Y - LatticeOriginY) / LatticeStepXY + 1.0e-4f); const int32 CacheIZ1 = FMath::FloorToInt( ((float)B.CacheWindowBox.Max.Z - static_cast(Ctx.LatticeOriginVoxels.Z)) / static_cast(Ctx.Step) + 1.0e-4f); const int64 CacheCount = ((int64)CacheIX1 - CacheIX0 + 1) * ((int64)CacheIY1 - CacheIY0 + 1) * ((int64)CacheIZ1 - CacheIZ0 + 1); if (CacheIX1 >= CacheIX0 && CacheIY1 >= CacheIY0 && CacheIZ1 >= CacheIZ0 && CacheCount > 0 && CacheCount <= 100000 && IX0 >= CacheIX0 && IY0 >= CacheIY0 && IZ0 >= CacheIZ0 && IX1 <= CacheIX1 && IY1 <= CacheIY1 && IZ1 <= CacheIZ1) { CacheNX = CacheIX1 - CacheIX0 + 1; CacheNY = CacheIY1 - CacheIY0 + 1; CacheNZ = CacheIZ1 - CacheIZ0 + 1; const bool bCacheKeyMatches = B.bExactWarpedLatticeCacheValid && B.ExactWarpedLatticeCacheBox == B.CacheWindowBox && B.ExactWarpedLatticeOrigin == Ctx.LatticeOriginVoxels && B.ExactWarpedLatticeStep == Ctx.Step && B.ExactWarpedLatticeSeed == SeedU && B.ExactWarpedLatticeFingerprint == ParamsFingerprint && B.ExactWarpedLatticeLayout == LV && B.ExactWarpedLatticeManagerLifetimeId == ManagerLifetimeId && B.ExactWarpedLatticeIX0 == CacheIX0 && B.ExactWarpedLatticeIY0 == CacheIY0 && B.ExactWarpedLatticeIZ0 == CacheIZ0 && B.ExactWarpedLatticeNX == CacheNX && B.ExactWarpedLatticeNY == CacheNY && B.ExactWarpedLatticeNZ == CacheNZ && B.ExactWarpedLatticeQueries.Num() == CacheCount; if (!bCacheKeyMatches) { B.ExactWarpedLatticeQueries.Reset(); B.ExactWarpedLatticeQueries.SetNumUninitialized( static_cast(CacheCount)); B.ExactTailStates.Init(0, static_cast(CacheCount)); for (int32 CacheIZ = CacheIZ0; CacheIZ <= CacheIZ1; ++CacheIZ) for (int32 CacheIY = CacheIY0; CacheIY <= CacheIY1; ++CacheIY) for (int32 CacheIX = CacheIX0; CacheIX <= CacheIX1; ++CacheIX) { const int32 FlatIndex = ((CacheIZ - CacheIZ0) * CacheNY + (CacheIY - CacheIY0)) * CacheNX + (CacheIX - CacheIX0); B.ExactWarpedLatticeQueries[FlatIndex] = MakeWarpedQuery(CacheIX, CacheIY, CacheIZ); } B.ExactWarpedLatticeCacheBox = B.CacheWindowBox; B.ExactWarpedLatticeIX0 = CacheIX0; B.ExactWarpedLatticeIY0 = CacheIY0; B.ExactWarpedLatticeIZ0 = CacheIZ0; B.ExactWarpedLatticeNX = CacheNX; B.ExactWarpedLatticeNY = CacheNY; B.ExactWarpedLatticeNZ = CacheNZ; B.ExactWarpedLatticeOrigin = Ctx.LatticeOriginVoxels; B.ExactWarpedLatticeStep = Ctx.Step; B.ExactWarpedLatticeSeed = SeedU; B.ExactWarpedLatticeFingerprint = ParamsFingerprint; B.ExactWarpedLatticeLayout = LV; B.ExactWarpedLatticeManagerLifetimeId = ManagerLifetimeId; B.bExactWarpedLatticeCacheValid = true; } else if (B.ExactTailStates.Num() != static_cast(CacheCount)) { B.ExactTailStates.Init(0, static_cast(CacheCount)); } bCanUseWarpedQueryCache = B.bExactWarpedLatticeCacheValid; } } for (int32 IZ = IZ0; IZ <= IZ1; ++IZ) for (int32 IY = IY0; IY <= IY1; ++IY) for (int32 IX = IX0; IX <= IX1; ++IX) { const float WorldX = LatticeOriginX + (float)IX * LatticeStepXY; const float WorldY = LatticeOriginY + (float)IY * LatticeStepXY; const float WorldZ = static_cast(Ctx.LatticeOriginVoxels.Z) + (float)IZ * (float)Ctx.Step; ExactLatticeWorldQueries.Add(FVector(WorldX, WorldY, WorldZ)); if (bCanUseWarpedQueryCache) { const int32 FlatIndex = ((IZ - CacheIZ0) * CacheNY + (IY - CacheIY0)) * CacheNX + (IX - CacheIX0); ExactLatticeWarpedQueries.Add( B.ExactWarpedLatticeQueries[FlatIndex]); } else { ExactLatticeWarpedQueries.Add(MakeWarpedQuery(IX, IY, IZ)); } const FVector& Query = ExactLatticeWarpedQueries.Last(); ExactLatticeQueryMin.X = FMath::Min(ExactLatticeQueryMin.X, (float)Query.X); ExactLatticeQueryMin.Y = FMath::Min(ExactLatticeQueryMin.Y, (float)Query.Y); ExactLatticeQueryMin.Z = FMath::Min(ExactLatticeQueryMin.Z, (float)Query.Z); ExactLatticeQueryMax.X = FMath::Max(ExactLatticeQueryMax.X, (float)Query.X); ExactLatticeQueryMax.Y = FMath::Max(ExactLatticeQueryMax.Y, (float)Query.Y); ExactLatticeQueryMax.Z = FMath::Max(ExactLatticeQueryMax.Z, (float)Query.Z); } // One envelope contains every warped lattice point in this refined child. // The lower-bound helpers below are valid over any enclosing AABB, so using // one envelope removes the repeated overlapping block traversal that made a // single LOD0 classifier visit the same primitive geometry dozens of times. // It is deliberately looser than the old sub-block decomposition: a looser // lower bound can only retain Mixed and send work to the mesher; it cannot // authorize a false empty-tile skip. The exact lattice itself is unchanged. FExactLatticeBlock& Block = ExactLatticeBlocks.AddDefaulted_GetRef(); Block.IX0 = ExactIX0; Block.IX1 = IX1; Block.IY0 = ExactIY0; Block.IY1 = IY1; Block.IZ0 = ExactIZ0; Block.IZ1 = IZ1; Block.WarpedMin = ExactLatticeQueryMin; Block.WarpedMax = ExactLatticeQueryMax; Block.WorldMin = FVector( LatticeOriginX + (float)ExactIX0 * LatticeStepXY, LatticeOriginY + (float)ExactIY0 * LatticeStepXY, (float)Ctx.LatticeOriginVoxels.Z + (float)ExactIZ0 * (float)Ctx.Step); Block.WorldMax = FVector( LatticeOriginX + (float)IX1 * LatticeStepXY, LatticeOriginY + (float)IY1 * LatticeStepXY, (float)Ctx.LatticeOriginVoxels.Z + (float)IZ1 * (float)Ctx.Step); } } const bool bUseExactLatticeWarp = ExactLatticeWarpedQueries.Num() > 0; const bool bUseExactLatticeBox = !bUseExactLatticeWarp && ExactLatticeQueryMin.X <= ExactLatticeQueryMax.X && ExactLatticeQueryMin.Y <= ExactLatticeQueryMax.Y && ExactLatticeQueryMin.Z <= ExactLatticeQueryMax.Z; bool bInvalidBound = false; float ExactSdfLower = FLT_MAX; float CheapSdfLower = FLT_MAX; int32 ExactRoomRefinements = 0; int32 ExactJoinRefinements = 0; int32 ExactTunnelRefinements = 0; int32 ExactPitRefinements = 0; int32 ExactChimneyRefinements = 0; auto ExactLatticePointDistance = [&](const FVector& Target) -> float { float Distance = FLT_MAX; // The block AABB contains every warped lattice point in the block. Its distance // to the target is therefore a conservative lower bound for the exact point // minimum. Do not rescan all 17^3 points for every room just to obtain this // broad-phase value; the same 2x2x2 block lattice is already built below for the // exact primitive walk. for (const FExactLatticeBlock& Block : ExactLatticeBlocks) { Distance = FMath::Min( Distance, VF_DistanceBetweenBoxes( Block.WarpedMin, Block.WarpedMax, Target, Target)); } return FMath::Max(0.0f, Distance - 0.001f); }; auto ExactLatticeBoxDistance = [&](const FVector& TargetMin, const FVector& TargetMax) -> float { float Distance = FLT_MAX; // As above, the block AABB distance is no larger than the distance from the // contained exact points to the target interval. It can lose a skip, but can // never turn an unproven box into a proof. for (const FExactLatticeBlock& Block : ExactLatticeBlocks) { Distance = FMath::Min( Distance, VF_DistanceBetweenBoxes( Block.WarpedMin, Block.WarpedMax, TargetMin, TargetMax)); } return FMath::Max(0.0f, Distance - 0.001f); }; auto ForEachExactQueryInWorldBox = [&](const FVector& WorldMin, const FVector& WorldMax, auto&& Callback) { if (ExactNX <= 0 || ExactNY <= 0 || ExactNZ <= 0) { return; } const int32 IX0 = FMath::Max( ExactIX0, FMath::CeilToInt( (WorldMin.X - LatticeOriginX) / LatticeStepXY - 1.0e-4f)); const int32 IY0 = FMath::Max( ExactIY0, FMath::CeilToInt( (WorldMin.Y - LatticeOriginY) / LatticeStepXY - 1.0e-4f)); const int32 IZ0 = FMath::Max( ExactIZ0, FMath::CeilToInt( (WorldMin.Z - static_cast(Ctx.LatticeOriginVoxels.Z)) / static_cast(Ctx.Step) - 1.0e-4f)); const int32 IX1 = FMath::Min( ExactIX0 + ExactNX - 1, FMath::FloorToInt( (WorldMax.X - LatticeOriginX) / LatticeStepXY + 1.0e-4f)); const int32 IY1 = FMath::Min( ExactIY0 + ExactNY - 1, FMath::FloorToInt( (WorldMax.Y - LatticeOriginY) / LatticeStepXY + 1.0e-4f)); const int32 IZ1 = FMath::Min( ExactIZ0 + ExactNZ - 1, FMath::FloorToInt( (WorldMax.Z - static_cast(Ctx.LatticeOriginVoxels.Z)) / static_cast(Ctx.Step) + 1.0e-4f)); if (IX1 < IX0 || IY1 < IY0 || IZ1 < IZ0) { return; } for (int32 IZ = IZ0; IZ <= IZ1; ++IZ) for (int32 IY = IY0; IY <= IY1; ++IY) for (int32 IX = IX0; IX <= IX1; ++IX) { const int32 FlatIndex = ((IZ - ExactIZ0) * ExactNY + (IY - ExactIY0)) * ExactNX + (IX - ExactIX0); Callback( ExactLatticeWarpedQueries[FlatIndex], ExactLatticeWorldQueries[FlatIndex]); } }; auto DistanceBlockToInfiniteLineLower = [&](const FExactLatticeBlock& Block, const FVector& A, const FVector& BPoint) -> float { const FVector Direction = BPoint - A; const float LengthSq = Direction.SizeSquared(); if (!(LengthSq > KINDA_SMALL_NUMBER)) { return VF_DistanceBetweenBoxes( Block.WarpedMin, Block.WarpedMax, A, A); } auto LinearRange = [](float Coefficient, float MinValue, float MaxValue, float Offset, float& OutMin, float& OutMax) { const float AValue = Coefficient * (MinValue - Offset); const float BValue = Coefficient * (MaxValue - Offset); OutMin = FMath::Min(AValue, BValue); OutMax = FMath::Max(AValue, BValue); }; auto DistanceToZero = [](float MinValue, float MaxValue) -> float { return MinValue > 0.0f ? MinValue : MaxValue < 0.0f ? -MaxValue : 0.0f; }; float XYMin = 0.0f, XYMax = 0.0f; float ZYMin = 0.0f, ZYMax = 0.0f; float XZMin = 0.0f, XZMax = 0.0f; float YXMin = 0.0f, YXMax = 0.0f; float ZXMin = 0.0f, ZXMax = 0.0f; float ZY2Min = 0.0f, ZY2Max = 0.0f; LinearRange(Direction.Y, Block.WarpedMin.Z, Block.WarpedMax.Z, (float)A.Z, ZYMin, ZYMax); LinearRange(Direction.Z, Block.WarpedMin.Y, Block.WarpedMax.Y, (float)A.Y, YXMin, YXMax); LinearRange(Direction.Z, Block.WarpedMin.X, Block.WarpedMax.X, (float)A.X, XZMin, XZMax); LinearRange(Direction.X, Block.WarpedMin.Z, Block.WarpedMax.Z, (float)A.Z, ZXMin, ZXMax); LinearRange(Direction.X, Block.WarpedMin.Y, Block.WarpedMax.Y, (float)A.Y, XYMin, XYMax); LinearRange(Direction.Y, Block.WarpedMin.X, Block.WarpedMax.X, (float)A.X, ZY2Min, ZY2Max); const float CrossXMin = ZYMin - YXMax; const float CrossXMax = ZYMax - YXMin; const float CrossYMin = XZMin - ZXMax; const float CrossYMax = XZMax - ZXMin; const float CrossZMin = XYMin - ZY2Max; const float CrossZMax = XYMax - ZY2Min; const float DX = DistanceToZero(CrossXMin, CrossXMax); const float DY = DistanceToZero(CrossYMin, CrossYMax); const float DZ = DistanceToZero(CrossZMin, CrossZMax); return FMath::Sqrt((DX * DX + DY * DY + DZ * DZ) / LengthSq); }; auto LatticePointDistance = [&](const FVector& Target, const FVector& Extra) -> float { if (bUseExactLatticeWarp) { return ExactLatticePointDistance(Target); } if (bUseExactLatticeBox) { return VF_DistanceBetweenBoxes( ExactLatticeQueryMin, ExactLatticeQueryMax, Target, Target); } const float DX = VF_LatticeAxisDistanceToPoint( BoxMinX, BoxMaxX, LatticeOriginX, LatticeStepXY, static_cast(Target.X), (float)Extra.X); const float DY = VF_LatticeAxisDistanceToPoint( BoxMinY, BoxMaxY, LatticeOriginY, LatticeStepXY, static_cast(Target.Y), (float)Extra.Y); const float DZ = VF_LatticeAxisDistanceToPoint( BoxMinZ, BoxMaxZ, LatticeOriginZ, LatticeStepZ, static_cast(Target.Z), (float)Extra.Z); return FMath::Sqrt(DX * DX + DY * DY + DZ * DZ); }; auto LatticeBoxDistance = [&](const FVector& TargetMin, const FVector& TargetMax, const FVector& Extra) -> float { if (bUseExactLatticeWarp) { return ExactLatticeBoxDistance(TargetMin, TargetMax); } if (bUseExactLatticeBox) { return VF_DistanceBetweenBoxes( ExactLatticeQueryMin, ExactLatticeQueryMax, TargetMin, TargetMax); } const float DX = VF_LatticeAxisDistanceToInterval( BoxMinX, BoxMaxX, LatticeOriginX, LatticeStepXY, static_cast(TargetMin.X), static_cast(TargetMax.X), (float)Extra.X); const float DY = VF_LatticeAxisDistanceToInterval( BoxMinY, BoxMaxY, LatticeOriginY, LatticeStepXY, static_cast(TargetMin.Y), static_cast(TargetMax.Y), (float)Extra.Y); const float DZ = VF_LatticeAxisDistanceToInterval( BoxMinZ, BoxMaxZ, LatticeOriginZ, LatticeStepZ, static_cast(TargetMin.Z), static_cast(TargetMax.Z), (float)Extra.Z); return FMath::Sqrt(DX * DX + DY * DY + DZ * DZ); }; // The exact warped lattice list is also an exact AABB superset. Use that cheap // distance as a first test and pay the primitive-by-query walk only when this bound is // close enough to affect the source's active threshold. A far primitive can only // raise the lower bound; retaining its AABB bound is conservative and avoids scanning // 729 queries for every cached room/tunnel in every refined child. auto ExactLatticeAabbPointDistance = [&](const FVector& Target) -> float { return bUseExactLatticeWarp && ExactLatticeQueryMin.X <= ExactLatticeQueryMax.X && ExactLatticeQueryMin.Y <= ExactLatticeQueryMax.Y && ExactLatticeQueryMin.Z <= ExactLatticeQueryMax.Z ? VF_DistanceBetweenBoxes( ExactLatticeQueryMin, ExactLatticeQueryMax, Target, Target) : FLT_MAX; }; auto ExactLatticeAabbBoxDistance = [&](const FVector& TargetMin, const FVector& TargetMax) -> float { return bUseExactLatticeWarp && ExactLatticeQueryMin.X <= ExactLatticeQueryMax.X && ExactLatticeQueryMin.Y <= ExactLatticeQueryMax.Y && ExactLatticeQueryMin.Z <= ExactLatticeQueryMax.Z ? VF_DistanceBetweenBoxes( ExactLatticeQueryMin, ExactLatticeQueryMax, TargetMin, TargetMax) : FLT_MAX; }; // Conservative lower bounds for primitive values on each warped lattice block. // Earlier versions walked every exact point in every refined child here. That was // sound, but it made an empty LOD0 tile pay thousands of room/tunnel evaluations. // These bounds are over the whole block AABB, which contains every warped lattice // point in the block. They are therefore allowed to be looser than the finite-point // minimum: looser costs a proof, never geometry. The smooth-min fold below remains // monotone in every primitive lower bound. auto ExactRoomShapeLower = [&](const FCachedRoom& Room, const FExactLatticeBlock& Block) -> float { auto Invalid = [&]() -> float { bInvalidBound = true; return 0.0f; }; const FVector BlockMin = Block.WarpedMin; const FVector BlockMax = Block.WarpedMax; if (!Finite((float)BlockMin.X) || !Finite((float)BlockMin.Y) || !Finite((float)BlockMin.Z) || !Finite((float)BlockMax.X) || !Finite((float)BlockMax.Y) || !Finite((float)BlockMax.Z)) { return Invalid(); } switch (Room.ShapeType) { case 0: { // VoxelSDF::Ellipsoid uses the scaled Euclidean distance multiplied by // the average radius. The closest scaled point to an axis-aligned box is // obtained independently on each axis, so this is a direct lower bound for // the exact approximation used by Eval. const FVector& Radii = Room.ShapeA; if (!Finite((float)Radii.X) || !Finite((float)Radii.Y) || !Finite((float)Radii.Z) || Radii.X <= 0.0 || Radii.Y <= 0.0 || Radii.Z <= 0.0) { return Invalid(); } const float Cx = (float)Room.Center.X; const float Cy = (float)Room.Center.Y; const float Cz = (float)Room.Center.Z; const float Rx = (float)Radii.X; const float Ry = (float)Radii.Y; const float Rz = (float)Radii.Z; const float DX = BlockMin.X > Cx ? BlockMin.X - Cx : BlockMax.X < Cx ? Cx - BlockMax.X : 0.0f; const float DY = BlockMin.Y > Cy ? BlockMin.Y - Cy : BlockMax.Y < Cy ? Cy - BlockMax.Y : 0.0f; const float DZ = BlockMin.Z > Cz ? BlockMin.Z - Cz : BlockMax.Z < Cz ? Cz - BlockMax.Z : 0.0f; const float AverageRadius = (Rx + Ry + Rz) / 3.0f; const float ScaledDistance = FMath::Sqrt( (DX / Rx) * (DX / Rx) + (DY / Ry) * (DY / Ry) + (DZ / Rz) * (DZ / Rz)); const float Lower = (ScaledDistance - 1.0f) * AverageRadius; return Finite(Lower) ? Lower : Invalid(); } case 1: { // The rounded-box SDF is the exact SDF of a box dilated by a sphere. The // enclosing sphere gives a cheap, unconditional lower bound for its signed // distance, including points inside the box. const FVector& HalfExtent = Room.ShapeA; const float Rounding = (float)Room.ShapeR; if (!Finite((float)HalfExtent.X) || !Finite((float)HalfExtent.Y) || !Finite((float)HalfExtent.Z) || !Finite(Rounding) || HalfExtent.X < 0.0 || HalfExtent.Y < 0.0 || HalfExtent.Z < 0.0 || Rounding < 0.0f) { return Invalid(); } const FVector OuterExtent( (float)HalfExtent.X + Rounding, (float)HalfExtent.Y + Rounding, (float)HalfExtent.Z + Rounding); const float Lower = VF_DistanceBetweenBoxes( BlockMin, BlockMax, Room.Center, Room.Center) - OuterExtent.Size(); return Finite(Lower) ? Lower : Invalid(); } case 2: { if (!Finite((float)Room.ShapeA.X) || !Finite((float)Room.ShapeA.Y) || !Finite((float)Room.ShapeA.Z) || !Finite((float)Room.ShapeB.X) || !Finite((float)Room.ShapeB.Y) || !Finite((float)Room.ShapeB.Z) || !Finite((float)Room.ShapeR) || Room.ShapeR < 0.0f) { return Invalid(); } const float Lower = DistanceBlockToInfiniteLineLower( Block, Room.ShapeA, Room.ShapeB) - FMath::Abs((float)Room.ShapeR); return Finite(Lower) ? Lower : Invalid(); } default: return Invalid(); } }; auto ExactRoomLower = [&](const FCachedRoom& Room) -> float { float LowerBound = FLT_MAX; const float CullRadiusSq = Room.CullRadiusSq; if (!Finite(CullRadiusSq) || CullRadiusSq < 0.0f) { bInvalidBound = true; return LowerBound; } const float CullRadius = FMath::Sqrt(CullRadiusSq); for (const FExactLatticeBlock& Block : ExactLatticeBlocks) { const float BlockDistance = VF_DistanceBetweenBoxes( Block.WarpedMin, Block.WarpedMax, Room.Center, Room.Center); if (!Finite(BlockDistance)) { bInvalidBound = true; continue; } // Every point in this block fails Eval's sphere cull when the block itself // is farther than the cull radius. This reject is exact for that cull. if (BlockDistance > CullRadius) { continue; } float RoomLower = ExactRoomShapeLower(Room, Block); if (bInvalidBound) { continue; } if (Room.FloorCutZ > -FLT_MAX) { if (!Finite((float)Room.FloorCutZ) || !Finite((float)Room.FloorReliefStrength) || (Room.FloorReliefStrength > 0.0f && !Finite((float)Room.FloorReliefFrequency))) { bInvalidBound = true; continue; } const float ReliefBound = Room.FloorReliefStrength > 0.0f ? FMath::Abs((float)Room.FloorReliefStrength) * VOXEL_NOISE_SCALE * VF_PerlinAbsBound : 0.0f; // SmoothMax(a,b) is never below max(a,b), so independently bounded // shape and floor terms can be combined with an ordinary Max. const float FloorLower = (float)Room.FloorCutZ - ReliefBound - (float)Block.WarpedMax.Z; RoomLower = FMath::Max(RoomLower, FloorLower); } LowerBound = FMath::Min(LowerBound, RoomLower); } return LowerBound; }; auto ExactJoinLower = [&](const FCachedRoomFloorJoin& Join) -> float { float LowerBound = FLT_MAX; if (!Finite((float)Join.BoundRadiusSq) || Join.BoundRadiusSq < 0.0f || !Finite((float)Join.BoundCenter.X) || !Finite((float)Join.BoundCenter.Y) || !Finite((float)Join.BoundCenter.Z) || !Finite((float)Join.Start.X) || !Finite((float)Join.Start.Y) || !Finite((float)Join.Start.Z) || !Finite((float)Join.End.X) || !Finite((float)Join.End.Y) || !Finite((float)Join.End.Z) || !Finite((float)Join.Radius) || !Finite((float)Join.FloorZ) || !Finite((float)Join.CeilingZ) || Join.Radius < 0.0f) { bInvalidBound = true; return LowerBound; } const float BoundRadius = FMath::Sqrt(Join.BoundRadiusSq); for (const FExactLatticeBlock& Block : ExactLatticeBlocks) { const float BlockDistance = VF_DistanceBetweenBoxes( Block.WarpedMin, Block.WarpedMax, Join.BoundCenter, Join.BoundCenter); if (BlockDistance > BoundRadius) { continue; } const float HorizontalLower = DistanceBlockToInfiniteLineLower( Block, Join.Start, Join.End) - FMath::Abs(Join.Radius); const float VerticalLower = FMath::Max( Join.FloorZ - (float)Block.WarpedMax.Z, (float)Block.WarpedMin.Z - Join.CeilingZ); const float Lower = FMath::Max(HorizontalLower, VerticalLower); if (!Finite(Lower)) { bInvalidBound = true; continue; } LowerBound = FMath::Min(LowerBound, Lower); } return LowerBound; }; auto ExactTunnelLower = [&](const FCachedTunnel& Tunnel) -> float { float LowerBound = FLT_MAX; if (!Finite((float)Tunnel.BoundRadiusSq) || Tunnel.BoundRadiusSq < 0.0f || !Finite((float)Tunnel.BoundCenter.X) || !Finite((float)Tunnel.BoundCenter.Y) || !Finite((float)Tunnel.BoundCenter.Z) || !Finite((float)Tunnel.SDFInfluenceRadius) || !Finite((float)Tunnel.SDFCenterlineMin.X) || !Finite((float)Tunnel.SDFCenterlineMin.Y) || !Finite((float)Tunnel.SDFCenterlineMin.Z) || !Finite((float)Tunnel.SDFCenterlineMax.X) || !Finite((float)Tunnel.SDFCenterlineMax.Y) || !Finite((float)Tunnel.SDFCenterlineMax.Z) || Tunnel.SDFCenterlineMin.X > Tunnel.SDFCenterlineMax.X || Tunnel.SDFCenterlineMin.Y > Tunnel.SDFCenterlineMax.Y || Tunnel.SDFCenterlineMin.Z > Tunnel.SDFCenterlineMax.Z) { bInvalidBound = true; return LowerBound; } const float BoundRadius = FMath::Sqrt(Tunnel.BoundRadiusSq); auto EvaluateSegment = [&](const FVector& A, const FVector& BPoint, float RadiusA, float RadiusB) { if (!Finite((float)A.X) || !Finite((float)A.Y) || !Finite((float)A.Z) || !Finite((float)BPoint.X) || !Finite((float)BPoint.Y) || !Finite((float)BPoint.Z) || !Finite(RadiusA) || !Finite(RadiusB)) { bInvalidBound = true; return; } const float SegmentRadius = FMath::Max( FMath::Abs(RadiusA), FMath::Abs(RadiusB)); const FVector SegmentMin( FMath::Min(A.X, BPoint.X), FMath::Min(A.Y, BPoint.Y), FMath::Min(A.Z, BPoint.Z)); const FVector SegmentMax( FMath::Max(A.X, BPoint.X), FMath::Max(A.Y, BPoint.Y), FMath::Max(A.Z, BPoint.Z)); for (const FExactLatticeBlock& Block : ExactLatticeBlocks) { if (Tunnel.SDFInfluenceRadius > 0.0f && VF_DistanceBetweenBoxes( Block.WarpedMin, Block.WarpedMax, Tunnel.SDFCenterlineMin, Tunnel.SDFCenterlineMax) > Tunnel.SDFInfluenceRadius) { continue; } if (VF_DistanceBetweenBoxes( Block.WarpedMin, Block.WarpedMax, Tunnel.BoundCenter, Tunnel.BoundCenter) > BoundRadius) { continue; } // Both terms are lower bounds for distance to the finite segment: // the first uses the infinite supporting line, while the second uses // the segment's AABB. Their maximum is still a lower bound and is much // tighter near a segment endpoint than either term alone. const float LineLower = DistanceBlockToInfiniteLineLower( Block, A, BPoint); const float SegmentBoxLower = VF_DistanceBetweenBoxes( Block.WarpedMin, Block.WarpedMax, SegmentMin, SegmentMax); const float Lower = FMath::Max(LineLower, SegmentBoxLower) - SegmentRadius; if (!Finite(Lower)) { bInvalidBound = true; continue; } LowerBound = FMath::Min(LowerBound, Lower); } }; if (Tunnel.ControlPoints.Num() >= 2 && Tunnel.ControlRadii.Num() == Tunnel.ControlPoints.Num()) { for (int32 SegmentIndex = 0; SegmentIndex + 1 < Tunnel.ControlPoints.Num(); ++SegmentIndex) { EvaluateSegment( Tunnel.ControlPoints[SegmentIndex], Tunnel.ControlPoints[SegmentIndex + 1], Tunnel.ControlRadii[SegmentIndex], Tunnel.ControlRadii[SegmentIndex + 1]); } } else if (Tunnel.bHasMidpoint) { EvaluateSegment(Tunnel.EndpointA, Tunnel.Midpoint, Tunnel.RadiusA, Tunnel.RadiusMid); EvaluateSegment(Tunnel.Midpoint, Tunnel.EndpointB, Tunnel.RadiusMid, Tunnel.RadiusB); } else { EvaluateSegment(Tunnel.EndpointA, Tunnel.EndpointB, Tunnel.RadiusA, Tunnel.RadiusB); } return LowerBound; }; auto ExactPitLower = [&](const FCachedPit& Pit) -> float { float LowerBound = FLT_MAX; const float Radius = FMath::Sqrt(FMath::Max(Pit.BoundXYRadiusSq, 0.0f)); const FVector CandidateMin( Pit.CenterX - Radius, Pit.CenterY - Radius, Pit.TopZ - Pit.Depth - Pit.BlendK); const FVector CandidateMax( Pit.CenterX + Radius, Pit.CenterY + Radius, Pit.TopZ + Pit.BlendK); ForEachExactQueryInWorldBox( CandidateMin, CandidateMax, [&](const FVector&, const FVector& Query) { const float DZ = (float)Query.Z - Pit.TopZ; if (DZ >= Pit.BlendK || -DZ > Pit.Depth + Pit.BlendK) { return; } const float DX = (float)Query.X - Pit.CenterX; const float DY = (float)Query.Y - Pit.CenterY; const float XYDistSq = DX * DX + DY * DY; if (XYDistSq > Pit.BoundXYRadiusSq) { return; } float PitSDF; if (DZ <= 0.0f) { float FlareFactor = FMath::Clamp( 1.0f - (-DZ) / Pit.FlareDist, 0.0f, 1.0f); FlareFactor *= FlareFactor; PitSDF = FMath::Sqrt(XYDistSq) - (Pit.Radius + Pit.FlareExtra * FlareFactor); } else { PitSDF = FMath::Sqrt(XYDistSq) - (Pit.Radius + Pit.FlareExtra); } LowerBound = FMath::Min(LowerBound, PitSDF); }); return LowerBound; }; auto ExactChimneyLower = [&](const FCachedChimney& Chimney) -> float { float LowerBound = FLT_MAX; const float Radius = FMath::Sqrt(FMath::Max(Chimney.BoundXYRadiusSq, 0.0f)); const FVector CandidateMin( Chimney.CenterX - Radius, Chimney.CenterY - Radius, Chimney.BottomZ - Chimney.BlendK); const FVector CandidateMax( Chimney.CenterX + Radius, Chimney.CenterY + Radius, Chimney.BottomZ + Chimney.Height + Chimney.BlendK); ForEachExactQueryInWorldBox( CandidateMin, CandidateMax, [&](const FVector&, const FVector& Query) { const float DZ = (float)Query.Z - Chimney.BottomZ; if (-DZ >= Chimney.BlendK || DZ > Chimney.Height + Chimney.BlendK) { return; } const float DX = (float)Query.X - Chimney.CenterX; const float DY = (float)Query.Y - Chimney.CenterY; const float XYDistSq = DX * DX + DY * DY; if (XYDistSq > Chimney.BoundXYRadiusSq) { return; } float ChimneySDF; if (DZ >= 0.0f) { float FlareFactor = FMath::Clamp( 1.0f - DZ / Chimney.FlareDist, 0.0f, 1.0f); FlareFactor *= FlareFactor; ChimneySDF = FMath::Sqrt(XYDistSq) - (Chimney.Radius + Chimney.FlareExtra * FlareFactor); } else { ChimneySDF = FMath::Sqrt(XYDistSq) - (Chimney.Radius + Chimney.FlareExtra); } LowerBound = FMath::Min(LowerBound, ChimneySDF); }); return LowerBound; }; auto ConsiderExact = [&](float PrimitiveLower, float BlendRadius) { if (!Finite(PrimitiveLower) || !Finite(BlendRadius)) { bInvalidBound = true; return; } ExactSdfLower = VoxelSDF::SmoothMin( ExactSdfLower, PrimitiveLower, BlendRadius); }; auto ConsiderCheapExact = [&](float PrimitiveLower, float BlendRadius) { if (Finite(PrimitiveLower) && Finite(BlendRadius)) { CheapSdfLower = VoxelSDF::SmoothMin( CheapSdfLower, PrimitiveLower, BlendRadius); } }; auto LatticeXYPointDistance = [&](float X, float Y) -> float { const float DX = VF_LatticeAxisDistanceToPoint( BoxMinX, BoxMaxX, LatticeOriginX, LatticeStepXY, X, 0.0f); const float DY = VF_LatticeAxisDistanceToPoint( BoxMinY, BoxMaxY, LatticeOriginY, LatticeStepXY, Y, 0.0f); return FMath::Sqrt(DX * DX + DY * DY); }; const float K = P.SDFBlendRadius; const float WormThreshold = VoxelMath::IsFinite(P.WormNetworkRange) ? FMath::Max(3.0f * K, P.WormNetworkRange) : FLT_MAX; const float ThresholdWithBlend = VF_SaturatingAdd(WormThreshold, K); float Lower = FLT_MAX; bool bAnyPrimitive = false; auto Consider = [&](float Candidate) { if (!Finite(Candidate)) { bInvalidBound = true; return; } bAnyPrimitive = true; Lower = FMath::Min(Lower, Candidate); }; auto CountThreshold = [&](float Candidate, int32& Count) { if (Finite(Candidate) && Candidate < ThresholdWithBlend) { ++Count; } }; B.NumRooms = GetBoxCache().Rooms.Num(); B.NumTunnels = GetBoxCache().Tunnels.Num(); B.NumRoomFloorJoins = GetBoxCache().RoomFloorJoins.Num(); B.NumPits = GetBoxCache().Pits.Num(); B.NumChimneys = GetBoxCache().Chimneys.Num(); B.HitRooms = B.HitTunnels = B.HitPits = B.HitChimneys = 0; B.HitRoomsNoWarp = B.HitTunnelsNoWarp = 0; B.WarpDilation = FMath::Max3( (float)WarpEnvelope.X, (float)WarpEnvelope.Y, (float)WarpEnvelope.Z); B.bMayHaveTunnelCoreAir = false; B.bMayHaveTunnelSupportFloor = false; B.bMayHaveTunnelCoreTail = false; auto MayHaveTunnelCoreTail = [&](const FCachedTunnel& Tunnel) -> uint8 { const bool bHasWorldChain = Tunnel.WorldControlPoints.Num() >= 2 && Tunnel.WorldControlRadii.Num() == Tunnel.WorldControlPoints.Num(); const FVector& CenterlineMin = bHasWorldChain ? Tunnel.WorldCenterlineMin : Tunnel.SDFCenterlineMin; const FVector& CenterlineMax = bHasWorldChain ? Tunnel.WorldCenterlineMax : Tunnel.SDFCenterlineMax; const float InfluenceRadius = bHasWorldChain ? Tunnel.WorldInfluenceRadius : Tunnel.SDFInfluenceRadius; const FVector& BoundCenter = bHasWorldChain ? Tunnel.WorldBoundCenter : Tunnel.BoundCenter; const float BoundRadiusSq = bHasWorldChain ? Tunnel.WorldBoundRadiusSq : Tunnel.BoundRadiusSq; if (!Finite((float)CenterlineMin.X) || !Finite((float)CenterlineMin.Y) || !Finite((float)CenterlineMin.Z) || !Finite((float)CenterlineMax.X) || !Finite((float)CenterlineMax.Y) || !Finite((float)CenterlineMax.Z) || !Finite(InfluenceRadius) || InfluenceRadius < 0.0f || !Finite((float)BoundCenter.X) || !Finite((float)BoundCenter.Y) || !Finite((float)BoundCenter.Z) || !Finite(BoundRadiusSq) || BoundRadiusSq < 0.0f) { return 3u; } // A post-air sample must lie within a tapered capsule's maximum radius of one // world-chain segment. The segment AABB expanded by that radius is a cheap // superset of the exact capsule; unlike the old enclosing sphere it rejects a // long, thin tunnel from most unrelated LOD0 boxes. The floor is part of this // same continuous field, so no second support-band candidate is needed. if (Ctx.bUseLatticeProof) { const float WorldLatticeOriginX = static_cast(Ctx.LatticeOriginVoxels.X); const float WorldLatticeOriginY = static_cast(Ctx.LatticeOriginVoxels.Y); const float WorldLatticeOriginZ = static_cast(Ctx.LatticeOriginVoxels.Z); const float WorldLatticeStep = static_cast(Ctx.Step); if (!(WorldLatticeStep > 0.0f)) { return 3u; } auto TouchesLatticeAABB = [&](float MinX, float MaxX, float MinY, float MaxY, float MinZ, float MaxZ) -> bool { return VF_LatticeAxisDistanceToInterval( BoxMinX, BoxMaxX, WorldLatticeOriginX, WorldLatticeStep, MinX, MaxX, 0.0f) <= 0.0f && VF_LatticeAxisDistanceToInterval( BoxMinY, BoxMaxY, WorldLatticeOriginY, WorldLatticeStep, MinY, MaxY, 0.0f) <= 0.0f && VF_LatticeAxisDistanceToInterval( BoxMinZ, BoxMaxZ, WorldLatticeOriginZ, WorldLatticeStep, MinZ, MaxZ, 0.0f) <= 0.0f; }; uint8 Result = 0u; auto AddSegment = [&](const FVector& A, const FVector& BPoint, float RadiusA, float RadiusB, bool bWorldChain, int32 SegmentIndex) { if (!Finite((float)A.X) || !Finite((float)A.Y) || !Finite((float)A.Z) || !Finite((float)BPoint.X) || !Finite((float)BPoint.Y) || !Finite((float)BPoint.Z) || !Finite(RadiusA) || !Finite(RadiusB)) { Result = 3u; return; } const float SafeRadiusA = FMath::Abs(RadiusA); const float SafeRadiusB = FMath::Abs(RadiusB); const float MaxRadius = FMath::Max(SafeRadiusA, SafeRadiusB); if (!Finite(MaxRadius)) { Result = 3u; return; } if (TouchesLatticeAABB( FMath::Min(A.X, BPoint.X) - MaxRadius, FMath::Max(A.X, BPoint.X) + MaxRadius, FMath::Min(A.Y, BPoint.Y) - MaxRadius, FMath::Max(A.Y, BPoint.Y) + MaxRadius, FMath::Min(A.Z, BPoint.Z) - MaxRadius, FMath::Max(A.Z, BPoint.Z) + MaxRadius)) { Result |= 1u; } (void)bWorldChain; (void)SegmentIndex; }; if (bHasWorldChain) { for (int32 SegmentIndex = 0; SegmentIndex + 1 < Tunnel.WorldControlPoints.Num(); ++SegmentIndex) { AddSegment( Tunnel.WorldControlPoints[SegmentIndex], Tunnel.WorldControlPoints[SegmentIndex + 1], Tunnel.WorldControlRadii[SegmentIndex], Tunnel.WorldControlRadii[SegmentIndex + 1], true, SegmentIndex); if (Result == 3u) { return Result; } } } else if (Tunnel.ControlPoints.Num() >= 2 && Tunnel.ControlRadii.Num() == Tunnel.ControlPoints.Num()) { for (int32 SegmentIndex = 0; SegmentIndex + 1 < Tunnel.ControlPoints.Num(); ++SegmentIndex) { AddSegment( Tunnel.ControlPoints[SegmentIndex], Tunnel.ControlPoints[SegmentIndex + 1], Tunnel.ControlRadii[SegmentIndex], Tunnel.ControlRadii[SegmentIndex + 1], false, SegmentIndex); if (Result == 3u) { return Result; } } } else if (Tunnel.bHasMidpoint) { AddSegment(Tunnel.EndpointA, Tunnel.Midpoint, Tunnel.RadiusA, Tunnel.RadiusMid, false, INDEX_NONE); AddSegment(Tunnel.Midpoint, Tunnel.EndpointB, Tunnel.RadiusMid, Tunnel.RadiusB, false, INDEX_NONE); } else { AddSegment(Tunnel.EndpointA, Tunnel.EndpointB, Tunnel.RadiusA, Tunnel.RadiusB, false, INDEX_NONE); } return Result; } if (InfluenceRadius > 0.0f && VF_DistanceBetweenBoxes( VoxelBox.Min, VoxelBox.Max, CenterlineMin, CenterlineMax) > InfluenceRadius) { return 0u; } if (BoundRadiusSq > 0.0f && VF_DistanceBetweenBoxes( VoxelBox.Min, VoxelBox.Max, BoundCenter, BoundCenter) > FMath::Sqrt(BoundRadiusSq)) { return 0u; } return 3u; }; const FVector NoWarpMin(BoxMinX, BoxMinY, EffectiveMinZ); const FVector NoWarpMax(BoxMaxX, BoxMaxY, EffectiveMaxZ); const uint64 ExactPrimitiveStartCycles = FPlatformTime::Cycles64(); for (const FCachedRoom& Room : GetBoxCache().Rooms) { const float CullRadiusSq = Room.CullRadiusSq; if (!Finite((float)Room.Center.X) || !Finite((float)Room.Center.Y) || !Finite((float)Room.Center.Z) || !Finite(CullRadiusSq) || CullRadiusSq < 0.0f) { bInvalidBound = true; continue; } const float RoomRadius = FMath::Sqrt(CullRadiusSq); const float FastRoomLower = bUseExactLatticeWarp ? ExactLatticeAabbPointDistance(Room.Center) - RoomRadius : (bUseLatticeProof ? LatticePointDistance(Room.Center, WarpEnvelope) - RoomRadius : VF_DistanceBetweenBoxes(QMin, QMax, Room.Center, Room.Center) - RoomRadius); const float RoomLower = bUseExactLatticeWarp && FastRoomLower < ThresholdWithBlend ? (ExactRoomRefinements++, ExactRoomLower(Room)) : FastRoomLower; const float RoomLowerNoWarp = VF_DistanceBetweenBoxes( NoWarpMin, NoWarpMax, Room.Center, Room.Center) - RoomRadius; Consider(RoomLower); if (bUseExactLatticeWarp) { ConsiderCheapExact(FastRoomLower, K); } if (bUseExactLatticeWarp) { ConsiderExact(RoomLower, K); } CountThreshold(RoomLower, B.HitRooms); CountThreshold(RoomLowerNoWarp, B.HitRoomsNoWarp); } // Room-floor joins are part of EvaluateSDFCached's same smooth-min union. They were // absent from the old box proof, which could only lose skips; include them here so the // exact retry remains a proof of the complete source rather than just rooms/tunnels. for (const FCachedRoomFloorJoin& Join : GetBoxCache().RoomFloorJoins) { const float JoinRadius = FMath::Sqrt(FMath::Max(Join.BoundRadiusSq, 0.0f)); const float FastJoinLower = bUseExactLatticeWarp ? ExactLatticeAabbPointDistance(Join.BoundCenter) - JoinRadius : VF_DistanceBetweenBoxes( QMin, QMax, Join.BoundCenter, Join.BoundCenter) - JoinRadius; const float JoinLower = bUseExactLatticeWarp && FastJoinLower < ThresholdWithBlend ? (ExactJoinRefinements++, ExactJoinLower(Join)) : FastJoinLower; Consider(JoinLower); if (bUseExactLatticeWarp) { ConsiderCheapExact(FastJoinLower, K); } if (bUseExactLatticeWarp) { ConsiderExact(JoinLower, K); } } for (const FCachedTunnel& Tunnel : GetBoxCache().Tunnels) { const uint8 TailMask = MayHaveTunnelCoreTail(Tunnel); B.bMayHaveTunnelCoreAir = B.bMayHaveTunnelCoreAir || (TailMask & 1u) != 0; B.bMayHaveTunnelCoreTail = B.bMayHaveTunnelCoreTail || TailMask != 0u; float MaxRadius = FMath::Max( FMath::Abs(Tunnel.RadiusA), FMath::Abs(Tunnel.RadiusB)); float TunnelLower = FLT_MAX; if (bUseExactLatticeWarp) { if (Tunnel.ControlPoints.Num() >= 2 && Tunnel.ControlRadii.Num() == Tunnel.ControlPoints.Num()) { for (int32 SegmentIndex = 0; SegmentIndex + 1 < Tunnel.ControlPoints.Num(); ++SegmentIndex) { const FVector& A = Tunnel.ControlPoints[SegmentIndex]; const FVector& BPoint = Tunnel.ControlPoints[SegmentIndex + 1]; MaxRadius = FMath::Max( MaxRadius, FMath::Max(FMath::Abs(Tunnel.ControlRadii[SegmentIndex]), FMath::Abs(Tunnel.ControlRadii[SegmentIndex + 1]))); const FVector SegmentMin( FMath::Min(A.X, BPoint.X), FMath::Min(A.Y, BPoint.Y), FMath::Min(A.Z, BPoint.Z)); const FVector SegmentMax( FMath::Max(A.X, BPoint.X), FMath::Max(A.Y, BPoint.Y), FMath::Max(A.Z, BPoint.Z)); TunnelLower = FMath::Min( TunnelLower, ExactLatticeAabbBoxDistance(SegmentMin, SegmentMax)); } } else if (Tunnel.bHasMidpoint) { const FVector SegmentMin( FMath::Min3(Tunnel.EndpointA.X, Tunnel.Midpoint.X, Tunnel.EndpointB.X), FMath::Min3(Tunnel.EndpointA.Y, Tunnel.Midpoint.Y, Tunnel.EndpointB.Y), FMath::Min3(Tunnel.EndpointA.Z, Tunnel.Midpoint.Z, Tunnel.EndpointB.Z)); const FVector SegmentMax( FMath::Max3(Tunnel.EndpointA.X, Tunnel.Midpoint.X, Tunnel.EndpointB.X), FMath::Max3(Tunnel.EndpointA.Y, Tunnel.Midpoint.Y, Tunnel.EndpointB.Y), FMath::Max3(Tunnel.EndpointA.Z, Tunnel.Midpoint.Z, Tunnel.EndpointB.Z)); MaxRadius = FMath::Max( MaxRadius, FMath::Max(FMath::Abs(Tunnel.RadiusMid), FMath::Max(FMath::Abs(Tunnel.RadiusA), FMath::Abs(Tunnel.RadiusB)))); TunnelLower = ExactLatticeAabbBoxDistance(SegmentMin, SegmentMax); } else { const FVector SegmentMin( FMath::Min(Tunnel.EndpointA.X, Tunnel.EndpointB.X), FMath::Min(Tunnel.EndpointA.Y, Tunnel.EndpointB.Y), FMath::Min(Tunnel.EndpointA.Z, Tunnel.EndpointB.Z)); const FVector SegmentMax( FMath::Max(Tunnel.EndpointA.X, Tunnel.EndpointB.X), FMath::Max(Tunnel.EndpointA.Y, Tunnel.EndpointB.Y), FMath::Max(Tunnel.EndpointA.Z, Tunnel.EndpointB.Z)); TunnelLower = ExactLatticeAabbBoxDistance(SegmentMin, SegmentMax); } TunnelLower -= MaxRadius; const float FastTunnelLower = TunnelLower; if (TunnelLower < ThresholdWithBlend) { ++ExactTunnelRefinements; TunnelLower = ExactTunnelLower(Tunnel); } if (bUseExactLatticeWarp) { ConsiderCheapExact(FastTunnelLower, K); } } else if (Tunnel.ControlPoints.Num() >= 2 && Tunnel.ControlRadii.Num() == Tunnel.ControlPoints.Num()) { for (int32 SegmentIndex = 0; SegmentIndex + 1 < Tunnel.ControlPoints.Num(); ++SegmentIndex) { const FVector& A = Tunnel.ControlPoints[SegmentIndex]; const FVector& BPoint = Tunnel.ControlPoints[SegmentIndex + 1]; MaxRadius = FMath::Max( MaxRadius, FMath::Max(FMath::Abs(Tunnel.ControlRadii[SegmentIndex]), FMath::Abs(Tunnel.ControlRadii[SegmentIndex + 1]))); const FVector SegmentMin( FMath::Min(A.X, BPoint.X), FMath::Min(A.Y, BPoint.Y), FMath::Min(A.Z, BPoint.Z)); const FVector SegmentMax( FMath::Max(A.X, BPoint.X), FMath::Max(A.Y, BPoint.Y), FMath::Max(A.Z, BPoint.Z)); TunnelLower = FMath::Min( TunnelLower, bUseLatticeProof ? LatticeBoxDistance(SegmentMin, SegmentMax, WarpEnvelope) : VF_DistanceBetweenBoxes(QMin, QMax, SegmentMin, SegmentMax)); } TunnelLower -= MaxRadius; } else { const FVector SegmentMin( FMath::Min(Tunnel.EndpointA.X, Tunnel.EndpointB.X), FMath::Min(Tunnel.EndpointA.Y, Tunnel.EndpointB.Y), FMath::Min(Tunnel.EndpointA.Z, Tunnel.EndpointB.Z)); const FVector SegmentMax( FMath::Max(Tunnel.EndpointA.X, Tunnel.EndpointB.X), FMath::Max(Tunnel.EndpointA.Y, Tunnel.EndpointB.Y), FMath::Max(Tunnel.EndpointA.Z, Tunnel.EndpointB.Z)); TunnelLower = (bUseLatticeProof ? LatticeBoxDistance(SegmentMin, SegmentMax, WarpEnvelope) : VF_DistanceBetweenBoxes(QMin, QMax, SegmentMin, SegmentMax)) - MaxRadius; } const FVector NoWarpSegmentMin( FMath::Min(Tunnel.EndpointA.X, Tunnel.EndpointB.X), FMath::Min(Tunnel.EndpointA.Y, Tunnel.EndpointB.Y), FMath::Min(Tunnel.EndpointA.Z, Tunnel.EndpointB.Z)); const FVector NoWarpSegmentMax( FMath::Max(Tunnel.EndpointA.X, Tunnel.EndpointB.X), FMath::Max(Tunnel.EndpointA.Y, Tunnel.EndpointB.Y), FMath::Max(Tunnel.EndpointA.Z, Tunnel.EndpointB.Z)); const float TunnelLowerNoWarp = VF_DistanceBetweenBoxes( NoWarpMin, NoWarpMax, NoWarpSegmentMin, NoWarpSegmentMax) - MaxRadius; Consider(TunnelLower); if (bUseExactLatticeWarp) { ConsiderExact(TunnelLower, K); } CountThreshold(TunnelLower, B.HitTunnels); CountThreshold(TunnelLowerNoWarp, B.HitTunnelsNoWarp); } // The pointwise air probe above is intentionally narrower than the final generator // tail: a tunnel can write a solid swept floor at lattice points where its core SDF // is not negative. Use the morphology module's shared broad reach predicate for the // complete world-space core/floor envelope; it is the same bound used by the mesher's // post-reach decision and keeps this diagnostic conservative for both polarities. if (!B.bMayHaveTunnelCoreTail) { B.bMayHaveTunnelCoreTail = VoxelCaveMorphology::AnyTunnelCoreWorldNearLattice( GetBoxCache(), VoxelBox, 1.0f); } if (B.bMayHaveTunnelCoreAir && bUseLatticeProof && ExactLatticeWorldQueries.Num() > 0) { // The bound test above is only a broad candidate. The actual post is a // continuous writer: it opens a point when the world-core SDF is negative. This // is still a proof of the final lattice, never a density shortcut; if an exact // query is invalid, keep the conservative candidate. bool bExactAir = false; bool bExactTailUnknown = false; TArray> ExactTailDomains; bool bExactTailDomainCullSafe = true; const uint64 ExactTailStartCycles = FPlatformTime::Cycles64(); for (const FCachedTunnel& Tunnel : GetBoxCache().Tunnels) { const bool bHasWorldChain = Tunnel.WorldControlPoints.Num() >= 2 && Tunnel.WorldControlRadii.Num() == Tunnel.WorldControlPoints.Num(); const FVector& CenterlineMin = bHasWorldChain ? Tunnel.WorldCenterlineMin : Tunnel.SDFCenterlineMin; const FVector& CenterlineMax = bHasWorldChain ? Tunnel.WorldCenterlineMax : Tunnel.SDFCenterlineMax; const float InfluenceRadius = bHasWorldChain ? Tunnel.WorldInfluenceRadius : Tunnel.SDFInfluenceRadius; // EvaluateTunnel's first cull is exactly the distance to this AABB. Turning // it into a point-in-expanded-box test lets the exact tail reject points // before it builds a support column or scans every tunnel. A disabled, // inverted, or non-finite bound cannot authorize a reject: fall back to the // existing fully conservative evaluator for the whole query set. if (!Finite((float)CenterlineMin.X) || !Finite((float)CenterlineMin.Y) || !Finite((float)CenterlineMin.Z) || !Finite((float)CenterlineMax.X) || !Finite((float)CenterlineMax.Y) || !Finite((float)CenterlineMax.Z) || CenterlineMin.X > CenterlineMax.X || CenterlineMin.Y > CenterlineMax.Y || CenterlineMin.Z > CenterlineMax.Z || !Finite(InfluenceRadius) || InfluenceRadius <= 0.0f) { bExactTailDomainCullSafe = false; break; } const FVector Radius(InfluenceRadius, InfluenceRadius, InfluenceRadius); ExactTailDomains.Emplace(CenterlineMin - Radius, CenterlineMax + Radius); } ForEachExactQueryInWorldBox( VoxelBox.Min, VoxelBox.Max, [&](const FVector&, const FVector& World) { if (bExactAir || bExactTailUnknown) return; ++B.ExactTailQueries; if (bExactTailDomainCullSafe && ExactTailDomains.Num() > 0) { bool bInsideTailDomain = false; for (const FBox& Domain : ExactTailDomains) { if (World.X >= Domain.Min.X && World.X <= Domain.Max.X && World.Y >= Domain.Min.Y && World.Y <= Domain.Max.Y && World.Z >= Domain.Min.Z && World.Z <= Domain.Max.Z) { bInsideTailDomain = true; break; } } if (!bInsideTailDomain) return; } uint8* CachedTailState = nullptr; if (B.bExactWarpedLatticeCacheValid && B.ExactTailStates.Num() == B.ExactWarpedLatticeQueries.Num()) { const int32 TailIX = FMath::RoundToInt( (World.X - LatticeOriginX) / LatticeStepXY); const int32 TailIY = FMath::RoundToInt( (World.Y - LatticeOriginY) / LatticeStepXY); const int32 TailIZ = FMath::RoundToInt( (World.Z - static_cast(Ctx.LatticeOriginVoxels.Z)) / static_cast(Ctx.Step)); if (TailIX >= B.ExactWarpedLatticeIX0 && TailIX < B.ExactWarpedLatticeIX0 + B.ExactWarpedLatticeNX && TailIY >= B.ExactWarpedLatticeIY0 && TailIY < B.ExactWarpedLatticeIY0 + B.ExactWarpedLatticeNY && TailIZ >= B.ExactWarpedLatticeIZ0 && TailIZ < B.ExactWarpedLatticeIZ0 + B.ExactWarpedLatticeNZ) { const int32 TailIndex = ((TailIZ - B.ExactWarpedLatticeIZ0) * B.ExactWarpedLatticeNY + (TailIY - B.ExactWarpedLatticeIY0)) * B.ExactWarpedLatticeNX + (TailIX - B.ExactWarpedLatticeIX0); CachedTailState = &B.ExactTailStates[TailIndex]; } } if (CachedTailState != nullptr && (*CachedTailState & 0x80u) != 0) { const uint8 State = *CachedTailState; if ((State & 0x04u) != 0) { bExactTailUnknown = true; } else { bExactAir |= (State & 0x01u) != 0; } return; } const FTunnelCoreWorldEvaluation Evaluation = VoxelCaveMorphology::EvaluateTunnelCoreWorld( static_cast(World.X), static_cast(World.Y), static_cast(World.Z), GetBoxCache(), nullptr, VoxelGenLOD::ShouldUseSpatialIndex(false)); ++B.ExactTailEvaluated; if (!VoxelMath::IsFinite(Evaluation.SDF)) { bExactTailUnknown = true; if (CachedTailState != nullptr) { *CachedTailState = 0x84u; } } else { const bool bAir = Evaluation.SDF < 0.0f; bExactAir |= bAir; if (CachedTailState != nullptr) { *CachedTailState = static_cast( 0x80u | (bAir ? 0x01u : 0u)); } } }); B.ExactTailCycles = FPlatformTime::Cycles64() - ExactTailStartCycles; if (!bExactTailUnknown) { B.bMayHaveTunnelCoreAir = bExactAir; B.bMayHaveTunnelSupportFloor = false; // Keep the broad geometric tail candidate even when the exact lattice has // no core-air vertex. The final generator tail writes both polarities: // it reopens tunnel air, but it can also write a solid swept floor below an // otherwise-air stack box. Collapsing this flag to bExactAir erased the // latter reach and let the classifier certify a false AllAir child. } } for (const FCachedPit& Pit : GetBoxCache().Pits) { if (!(BoxMinZ < Pit.TopZ + Pit.BlendK) || !(BoxMaxZ >= Pit.TopZ - Pit.Depth - Pit.BlendK)) { continue; } const float MaxRadius = FMath::Abs(Pit.Radius) + FMath::Abs(Pit.FlareExtra); const float FastPitLower = bUseExactLatticeWarp ? LatticeXYPointDistance(Pit.CenterX, Pit.CenterY) - MaxRadius : (bUseLatticeProof ? LatticeXYPointDistance(Pit.CenterX, Pit.CenterY) : VF_DistanceBoxToPointXY(FBox(RMin, RMax), Pit.CenterX, Pit.CenterY)) - MaxRadius; const float PitLower = bUseExactLatticeWarp && FastPitLower < ThresholdWithBlend ? (ExactPitRefinements++, ExactPitLower(Pit)) : FastPitLower; Consider(PitLower); if (bUseExactLatticeWarp) { ConsiderCheapExact(FastPitLower, Pit.BlendK); } if (bUseExactLatticeWarp) { ConsiderExact(PitLower, Pit.BlendK); } CountThreshold(PitLower, B.HitPits); } for (const FCachedChimney& Chimney : GetBoxCache().Chimneys) { if (!(BoxMaxZ > Chimney.BottomZ - Chimney.BlendK) || !(BoxMinZ <= Chimney.BottomZ + Chimney.Height + Chimney.BlendK)) { continue; } const float MaxRadius = FMath::Abs(Chimney.Radius) + FMath::Abs(Chimney.FlareExtra); const float FastChimneyLower = bUseExactLatticeWarp ? LatticeXYPointDistance(Chimney.CenterX, Chimney.CenterY) - MaxRadius : (bUseLatticeProof ? LatticeXYPointDistance(Chimney.CenterX, Chimney.CenterY) : VF_DistanceBoxToPointXY(FBox(RMin, RMax), Chimney.CenterX, Chimney.CenterY)) - MaxRadius; const float ChimneyLower = bUseExactLatticeWarp && FastChimneyLower < ThresholdWithBlend ? (ExactChimneyRefinements++, ExactChimneyLower(Chimney)) : FastChimneyLower; Consider(ChimneyLower); if (bUseExactLatticeWarp) { ConsiderCheapExact(FastChimneyLower, Chimney.BlendK); } if (bUseExactLatticeWarp) { ConsiderExact(ChimneyLower, Chimney.BlendK); } CountThreshold(ChimneyLower, B.HitChimneys); } B.ExactPrimitiveCycles = FPlatformTime::Cycles64() - ExactPrimitiveStartCycles; if (bInvalidBound) { Unknown(); return; } FVoxelBoxSdfInterval Own; if (bUseExactLatticeWarp) { // Exact per-primitive scans are a tightening only. Their candidate-domain // filters can miss a warped lattice query near a primitive, so retain the // broad lower bound over the AABB that contains every warped sample. The // minimum stays useful when both agree and remains safe when an exact scan is // incomplete. Own.Set(FMath::Min(ExactSdfLower, CheapSdfLower), FLT_MAX); } else if (!bAnyPrimitive) { Own.Set(FLT_MAX, FLT_MAX); } else { Own.Set(VF_SaturatingAdd(Lower, -K), FLT_MAX); } B.SdfInterval = Own; B.Verdict = Own.IsKnown() && Own.Min >= WormThreshold ? EVoxelOpEffect::Identity : EVoxelOpEffect::Both; B.KeyBox = VoxelBox; B.KeyStrate = StrateIdx; B.KeySeed = SeedU; B.KeyFingerprint = ParamsFingerprint; B.KeyLayout = LV; B.KeyManagerLifetimeId = ManagerLifetimeId; B.KeyUsesLatticeProof = bUseLatticeProof; B.KeyTightenWarpProof = Ctx.bTightenWarpProof; B.KeyLatticeOrigin = Ctx.LatticeOriginVoxels; B.KeyLatticeStep = Ctx.Step; B.bValid = true; InOut = Own; B.PropagateCycles = FPlatformTime::Cycles64() - PropagateStartCycles; } const TCHAR* DebugName() const override { return TEXT("RoomGraphSource"); } void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } private: FStrateGenerationParams P; int32 Seed; uint32 SeedU; TWeakObjectPtr Manager; uint64 ManagerLifetimeId = 0; uint32 ParamsFingerprint; uint32 LayoutVersion = 0; }; //========================================================================= // LE GATE DE BOÎTE DES MODIFICATEURS DE CAVE //========================================================================= // The source publishes an SDF interval; detail modifiers consume that interval here. This is // deliberately a state test, never a call to the source's EffectOverBox: the latter is the // source-only answer and must remain valid when the composer chooses a different consumer. FORCEINLINE bool VF_CaveBoxIsFar(const FVoxelBoxHypotheses& H, float SDFBlendRadius) { const float Threshold = SDFBlendRadius * 3.0f; return VoxelMath::IsFinite(Threshold) && H.Sdf.IsKnown() && H.Sdf.Min >= Threshold; } FORCEINLINE EVoxelOpEffect VF_CaveDetailEffect(const FRoomGraphSource* Rooms, const FVoxelBoxHypotheses& H, float SDFBlendRadius, EVoxelOpEffect Intrinsic) { // A detail op with no room source has the same no-op guard as Eval. An unknown interval is // not a reason to claim Identity: uncertainty costs a skip, while a false skip is a hole. if (Rooms == nullptr || VF_CaveBoxIsFar(H, SDFBlendRadius)) { return EVoxelOpEffect::Identity; } // A terrain op is allowed to replace the strate-level activation fields for the room that // Eval selects. Its exact direction is not part of the generic cache proof, so preserve // safety with Both whenever one is present. The common no-override TunnelNetwork path // gets the real intrinsic direction and finite amplitude below. return Rooms->HasRoomTerrainOverrideForLastBox() ? EVoxelOpEffect::Both : Intrinsic; } FORCEINLINE float VF_CaveDetailMax(const FRoomGraphSource* Rooms, const FVoxelBoxHypotheses& H, float SDFBlendRadius, float IntrinsicMax) { // Per-room overrides can activate or enlarge a modifier even when the strate-level // parameter is zero. A valid cache with no override makes the strate-level supremum // exact for these modifiers; an unknown/overridden cache keeps the old safe default. if (Rooms == nullptr || VF_CaveBoxIsFar(H, SDFBlendRadius)) { return 0.0f; } if (Rooms->HasRoomTerrainOverrideForLastBox()) { return FLT_MAX; } return IntrinsicMax; } //========================================================================= // RÔLE 3 — MODIFIER : RUGOSITÉ DE PAROI, ESPACE DENSITÉ (TunnelNetwork, STEP 4b) //========================================================================= // ⚠️ CE N'EST PAS `FSdfRoughnessMod`, ET C'EST LE PIÈGE QUE `OPSTACK-DECOMPOSITION §1` SIGNALE. // Les deux s'appellent « rugosité de surface » et lisent le même champ de params, mais : // // • variante SDF (Maze / VerticalShafts / FloatingIslands) : `Sdf += bruit·SCALE·Force`. // Brut, sans fade, sans clamp, fréquence codée en dur au site d'appel. Déplace la SURFACE. // • variante DENSITÉ (ici) : DEUX jeux d'octaves (principal + fin ×3), warp de domaine // optionnel, QUATRE types de bruit, un `Min(…, 0)` anti-remplissage, et un fade QUADRATIQUE // par distance à la surface. Déplace la MATIÈRE, mise à l'échelle par le gradient local. // // Les fusionner sous un enum `Space` était la suggestion du §1 ; en les portant, ils n'ont // presque aucune ligne en commun (le clamp, le fade et le second jeu d'octaves n'ont pas // d'équivalent dans l'autre). Deux opérateurs, un nom partagé — comme `FGridColumnMod` et le // futur `FRoomColumnMod`, que le §1 sépare pour la même raison. // // ⚠️⚠️ CET OPÉRATEUR EST **HORS** DE L'OVERRIDE D'OP PAR SALLE, et ce n'est pas un oubli. // Dans l'original, le shadow `const FStrateGenerationParams& Params = LocalTerrainParams;` est // déclaré à l'INTÉRIEUR du bloc `if (bNearCaveSurface)` qui commence APRÈS l'étape 4b. La // rugosité lit donc les params de la STRATE, jamais ceux de la salle la plus proche. Onze // modificateurs sur douze lisent la copie par salle ; celui-ci non. À NE PAS « uniformiser » // à l'étape C1. // // This op reads STRATE params, not the per-room copy: the original's shadow is declared inside // the `if (bNearCaveSurface)` block that starts AFTER step 4b. Eleven of twelve modifiers read // the shadowed copy; this one does not. class FCaveRoughnessMod final : public IVoxelDensityOp { public: FCaveRoughnessMod(const FStrateGenerationParams& InP, int32 Seed) : P(InP), SeedU((uint32)Seed) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density | VoxelOpChannels::Sdf; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } bool IsAdditive() const override { return true; } void PrepareChunk(const FVoxelOpContext&) override {} const TCHAR* DebugName() const override { return TEXT("CaveRoughnessMod"); } void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { const float CaveSDF = InOut.Sdf; if (!VF_NearCaveSurface(CaveSDF, P.SDFBlendRadius)) { return; } if (!(P.SurfaceRoughness > 0.0f)) { return; } const float EffectiveZ = (P.VerticalScale != 1.0f && P.VerticalScale > 0.0f) ? (WorldZ / P.VerticalScale) : WorldZ; const float RoughnessDepth = P.SurfaceRoughness * 2.0f; const float DistFromSurface = FMath::Abs(CaveSDF); if (!(DistFromSurface < RoughnessDepth)) { return; } const float RF = P.RoughnessFrequency; // Keep both hot noise-coordinate chains in float. The old FVector round-trip only // preserved legacy rounding; output compatibility with that field is abandoned. 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 ); // WARP DE DOMAINE : le MÊME offset est ajouté aux DEUX positions (une seule // `FVector WarpOffset`, deux `+=`). Transcrit tel quel — appliquer deux warps // indépendants serait plus « propre » et donnerait un autre monde. if (P.DomainWarpStrength > 0.0f) { const float WF = P.DomainWarpFrequency; const float WS = P.DomainWarpStrength; float WarpX=0.0f,WarpY=0.0f,WarpZ=0.0f; VoxelNoise::Perlin3D_x3( WorldX*WF+VoxelHash::SeedOffset(SeedU,5.2f), WorldY*WF+VoxelHash::SeedOffset(SeedU,1.3f), EffectiveZ*WF+VoxelHash::SeedOffset(SeedU,9.7f), WorldX*WF+100.0f+VoxelHash::SeedOffset(SeedU,7.7f), WorldY*WF+200.0f+VoxelHash::SeedOffset(SeedU,3.1f), EffectiveZ*WF+300.0f, WorldX*WF+400.0f, WorldY*WF+500.0f+VoxelHash::SeedOffset(SeedU,11.9f), EffectiveZ*WF+600.0f+VoxelHash::SeedOffset(SeedU,13.3f), WarpX,WarpY,WarpZ); WarpX*=VOXEL_NOISE_SCALE*WS; WarpY*=VOXEL_NOISE_SCALE*WS; WarpZ*=VOXEL_NOISE_SCALE*WS; const FVector3f WarpOffset(WarpX, WarpY, WarpZ); MainPos += WarpOffset; FinePos += WarpOffset; } // Les comptes d'octaves passent par VoxelGenLOD::Eff — contrat T2.b, les tuiles // lointaines perdent les octaves sous-cellulaires. float RoughNoise, FineNoise; const int32 Oct3 = VoxelGenLOD::Eff(3); const int32 Oct2 = VoxelGenLOD::Eff(2); switch (P.RoughnessNoiseType) { case EVoxelNoiseType::Ridged: { float MainFBM=0.0f,MainRidged=0.0f,FineFBM=0.0f,FineRidged=0.0f; VoxelNoise::FractalPair(MainPos.X,MainPos.Y,MainPos.Z,Oct3, FinePos.X,FinePos.Y,FinePos.Z,Oct2, MainFBM,MainRidged,FineFBM,FineRidged); RoughNoise=MainRidged;FineNoise=FineRidged; break; } case EVoxelNoiseType::Mixed: { float MainFBM=0.0f,MainRidged=0.0f,FineFBM=0.0f,FineRidged=0.0f; VoxelNoise::FractalPair(MainPos.X,MainPos.Y,MainPos.Z,Oct3, FinePos.X,FinePos.Y,FinePos.Z,Oct2, MainFBM,MainRidged,FineFBM,FineRidged); RoughNoise = MainFBM * 0.5f + MainRidged * 0.5f; FineNoise = FineFBM * 0.5f + FineRidged * 0.5f; break; } case EVoxelNoiseType::Cellular: RoughNoise = VoxelNoise::Cellular3D(MainPos); FineNoise = VoxelNoise::Cellular3D(FinePos); break; case EVoxelNoiseType::FBM: default: { float MainFBM=0.0f,MainRidged=0.0f,FineFBM=0.0f,FineRidged=0.0f; VoxelNoise::FractalPair(MainPos.X,MainPos.Y,MainPos.Z,Oct3, FinePos.X,FinePos.Y,FinePos.Z,Oct2, MainFBM,MainRidged,FineFBM,FineRidged); RoughNoise=MainFBM;FineNoise=FineFBM; break; } } RoughNoise *= VOXEL_NOISE_SCALE; FineNoise *= VOXEL_NOISE_SCALE; float TotalRough = RoughNoise * P.SurfaceRoughness + FineNoise * P.SurfaceRoughness * 0.4f; // CLAMP ANTI-REMPLISSAGE : dans l'air certain (SDF < 0) la rugosité ne doit JAMAIS // rajouter du solide — sinon lucarnes, membranes, coutures aux jonctions et aux lèvres // de puits. Elle peut encore creuser plus loin dans la paroi. if (CaveSDF < 0.0f) { TotalRough = FMath::Min(TotalRough, 0.0f); } float SurfaceFade = 1.0f - (DistFromSurface / RoughnessDepth); SurfaceFade = SurfaceFade * SurfaceFade; // quadratique : concentre près de la surface InOut.Density += TotalRough * SurfaceFade; } /** * `Both` : la rugosité peut pousser dans les deux sens (le clamp ne s'applique que dans * l'air certain). Conservatif et donc correct, mais coûteux — comme les vers, c'est un * opérateur dont l'AMPLITUDE est bornée alors que sa DIRECTION ne l'est pas : * |TotalRough| ≤ 2.1 · SurfaceRoughness · VOXEL_NOISE_SCALE, fade ∈ [0,1]. * Deuxième client pour le pliage numérique de `OPSTACK-DECOMPOSITION §0.2`, noté au point * exact où la borne manque (le premier est `FWormFieldSource::MaxCarveAmplitude`). */ EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override { return (P.SurfaceRoughness > 0.0f) ? EVoxelOpEffect::Both : EVoxelOpEffect::Identity; } EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx, const FVoxelBoxHypotheses& H) const override { const EVoxelOpEffect Intrinsic = EffectOverBox(VoxelBox, Ctx); if (Intrinsic == EVoxelOpEffect::Identity) { return Intrinsic; } return IsInactiveFromSdf(H) ? EVoxelOpEffect::Identity : Intrinsic; } /** * ✅ Borne consommée par le pliage numérique. `RoughNoise` et `FineNoise` respectent tous * `FBM` est couvert par la borne prouvée `VF_PerlinAbsBound = 1.5`, mis à l'échelle par * `VOXEL_NOISE_SCALE`. `Ridged` is in [-1,1] after its square/fold, while the unclamped * cellular mapping needs its own finite envelope below. The two octaves contribute * `|Rough| + 0.4·|Fine|`; the fade is in `[0,1]`. The anti-fill clamp only reduces the * positive side, so the same envelope is valid in both directions. */ float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override { return MaxAmplitude(); } float MaxFillOverBox (const FBox&, const FVoxelOpContext&) const override { return MaxAmplitude(); } float MaxCarveOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx, const FVoxelBoxHypotheses& H) const override { return IsInactiveFromSdf(H) ? 0.0f : MaxCarveOverBox(VoxelBox, Ctx); } float MaxFillOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx, const FVoxelBoxHypotheses& H) const override { return IsInactiveFromSdf(H) ? 0.0f : MaxFillOverBox(VoxelBox, Ctx); } /** La borne d'amplitude, en unités de densité. */ float MaxAmplitude() const { if (!(P.SurfaceRoughness > 0.0f)) { return 0.0f; } if (!VoxelMath::IsFinite(P.SurfaceRoughness)) { return FLT_MAX; } float NoiseBound = VF_PerlinAbsBound; switch (P.RoughnessNoiseType) { case EVoxelNoiseType::Ridged: NoiseBound = 1.0f; break; case EVoxelNoiseType::Mixed: NoiseBound = 0.5f * (VF_PerlinAbsBound + 1.0f); break; case EVoxelNoiseType::Cellular: NoiseBound = VF_CellularAbsBound; break; case EVoxelNoiseType::FBM: default: NoiseBound = VF_PerlinAbsBound; break; } const float Result = 1.4f * NoiseBound * P.SurfaceRoughness * VOXEL_NOISE_SCALE; return VoxelMath::IsFinite(Result) ? Result : FLT_MAX; } private: bool IsInactiveFromSdf(const FVoxelBoxHypotheses& H) const { if (VF_CaveBoxIsFar(H, P.SDFBlendRadius)) { return true; } if (!(P.SurfaceRoughness > 0.0f) || !VoxelMath::IsFinite(P.SurfaceRoughness)) { return P.SurfaceRoughness <= 0.0f; } const float RoughnessDepth = P.SurfaceRoughness * 2.0f; // Eval's second gate is abs(CaveSDF) < SurfaceRoughness*2. A known positive // lower endpoint at or beyond that depth therefore proves the op is an identity even // when the broader shared near-surface gate (SDFBlendRadius*3) remains open. return VoxelMath::IsFinite(RoughnessDepth) && H.Sdf.IsKnown() && H.Sdf.Min >= RoughnessDepth; } FStrateGenerationParams P; uint32 SeedU; }; //========================================================================= // RÔLE 3 — MODIFIER : TERRASSES / TERRACING (TunnelNetwork, STEP 4c) //========================================================================= // Un escalier lissé en Z : `Offset = staircase(Z) − Z` ajouté à la densité. Positif ⇒ marche // solide sur laquelle marcher, négatif ⇒ vide sous la marche du dessus. // // ⚠️ LE SEUL MODIFICATEUR QUI RE-INTERROGE LE CHAMP SDF. Le facteur d'orientation vient de deux // sondes en Z±1 : un SDF a un gradient ≈ unitaire, donc |dSDF/dZ| EST déjà la composante // verticale normalisée — proche de 1 = sol/plafond, proche de 0 = paroi. Sans ce facteur, les // terrasses posent des bourrelets horizontaux dans les puits verticaux. // // C'est pour cette re-interrogation que `FRoomGraphSource` expose `ProbeSdfUnwarped` (et donc // que son cache est sorti de `Eval`) : le refaire ici voudrait dire un second cache SDF. class FCaveTerraceMod final : public IVoxelDensityOp { public: FCaveTerraceMod(const FStrateGenerationParams& InP, int32 Seed, const FRoomGraphSource* InRooms) : P(InP), SeedU((uint32)Seed), Rooms(InRooms) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density | VoxelOpChannels::Sdf; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } bool IsAdditive() const override { return true; } void PrepareChunk(const FVoxelOpContext&) override {} void PrepareBlockSample(const FVoxelOpSample& Sample) const override { if (Rooms != nullptr) { Rooms->RestoreBlockSample(Sample); } } const TCHAR* DebugName() const override { return TEXT("CaveTerraceMod"); } void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { const float CaveSDF = InOut.Sdf; if (!VF_NearCaveSurface(CaveSDF, P.SDFBlendRadius)) { return; } if (Rooms == nullptr) { return; } // `&& CaveSDF < FLT_MAX` est redondant sous le gate (qui le teste déjà) mais l'original // l'écrit, et une transcription littérale ne fait pas le tri. const FStrateGenerationParams& LP = Rooms->LocalParams(); // C1 : params PAR SALLE if (!(LP.TerraceStepHeight > 0.0f && CaveSDF < FLT_MAX)) { return; } const float StepH = LP.TerraceStepHeight; const float DistFromSurface = FMath::Abs(CaveSDF); const float TerraceRange = StepH * 3.0f; if (!(DistFromSurface < TerraceRange)) { return; } // Deux évaluations SDF de plus par voxel, seulement près d'une surface. Approximation // en Z seul : le gradient complet à 6 échantillons coûtait 3× pour le même signal. const float SDF_Zp1 = Rooms->ProbeSdfUnwarped(WorldX, WorldY, WorldZ + 1.0f); const float SDF_Zm1 = Rooms->ProbeSdfUnwarped(WorldX, WorldY, WorldZ - 1.0f); const float GZ = (SDF_Zp1 - SDF_Zm1) * 0.5f; const float SurfaceHorizontality = FMath::Clamp(FMath::Abs(GZ), 0.0f, 1.0f); const float TerraceOrientFactor = FMath::Clamp((SurfaceHorizontality - 0.3f) / 0.4f, 0.0f, 1.0f); // ⚠️ Z BRUT, PAS `EffectiveZ` : les terrasses sont géologiques, elles restent // horizontales quelle que soit l'échelle verticale de la strate. Idem pour les lignes de // strates et les nervures plus bas. La rugosité (4b), elle, utilise EffectiveZ. C'est // délibéré dans l'original et ça se lit dans son commentaire d'en-tête du STEP 3. float NoisedZ = WorldZ; if (LP.TerraceNoiseDisplacement > 0.0f) { const float DispNoise = HFractal3D(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 * LP.TerraceNoiseDisplacement * StepH; } const float K = NoisedZ / StepH; const float FloorK = FMath::FloorToFloat(K); const float Frac = K - FloorK; // toujours [0, 1) const float Edge = FMath::Lerp(0.45f, 0.02f, LP.TerraceHardness); float StairValue; if (Frac < 0.5f - Edge) { StairValue = 0.0f; } else if (Frac > 0.5f + Edge) { StairValue = 1.0f; } else { const float T = (Frac - (0.5f - Edge)) / (2.0f * Edge); StairValue = SmoothStep01(T); } const float TerracedZ = (FloorK + StairValue) * StepH; const float Offset = TerracedZ - NoisedZ; // ≈ [-StepH/2, +StepH/2] float Fade = 1.0f - (DistFromSurface / TerraceRange); Fade = Fade * Fade; InOut.Density += Offset * Fade * TerraceOrientFactor; } /** `Both` : `Offset` change de signe d'une demi-marche à l'autre. Amplitude bornée par * `StepH/2` — troisième client du pliage numérique de `OPSTACK-DECOMPOSITION §0.2`. */ EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override { return (P.TerraceStepHeight > 0.0f) ? EVoxelOpEffect::Both : EVoxelOpEffect::Identity; } EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx, const FVoxelBoxHypotheses& H) const override { return VF_CaveDetailEffect(Rooms, H, P.SDFBlendRadius, EffectOverBox(VoxelBox, Ctx)); } float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override { return MaxAmplitude(); } float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override { return MaxAmplitude(); } float MaxCarveOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx, const FVoxelBoxHypotheses& H) const override { return VF_CaveDetailMax(Rooms, H, P.SDFBlendRadius, MaxCarveOverBox(VoxelBox, Ctx)); } float MaxFillOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx, const FVoxelBoxHypotheses& H) const override { return VF_CaveDetailMax(Rooms, H, P.SDFBlendRadius, MaxFillOverBox(VoxelBox, Ctx)); } /** * `TerracedZ` is one staircase step below or above `NoisedZ`; the modulo phase is in * [0,1], so the safe supremum of `abs(TerracedZ - NoisedZ)` is one full step, not StepH/2. * The noise displacement changes which phase is selected but cannot enlarge that interval. */ float MaxAmplitude() const { return (P.TerraceStepHeight > 0.0f && VoxelMath::IsFinite(P.TerraceStepHeight) && VoxelMath::IsFinite(P.TerraceHardness) && P.TerraceHardness >= 0.0f && P.TerraceHardness <= 1.0f) ? P.TerraceStepHeight : (P.TerraceStepHeight > 0.0f ? FLT_MAX : 0.0f); } private: FStrateGenerationParams P; uint32 SeedU; const FRoomGraphSource* Rooms; // NON possédant : la pile possède la source }; //========================================================================= // RÔLE 3 — MODIFIER : LIGNES DE STRATES / LAYER LINES (TunnelNetwork, STEP 4c) //========================================================================= // Rainures horizontales dans les parois — la strate sédimentaire vue en coupe. Une sinusoïde en // Z, rectifiée puis CUBÉE : les bosses larges du sinus deviennent des pointes fines, donc des // rainures étroites au lieu d'une ondulation. class FLayerLineMod final : public IVoxelDensityOp { public: FLayerLineMod(const FStrateGenerationParams& InP, const FRoomGraphSource* InRooms) : P(InP), Rooms(InRooms) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density | VoxelOpChannels::Sdf; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } bool IsAdditive() const override { return true; } void PrepareChunk(const FVoxelOpContext&) override {} void PrepareBlockSample(const FVoxelOpSample& Sample) const override { if (Rooms != nullptr) { Rooms->RestoreBlockSample(Sample); } } const TCHAR* DebugName() const override { return TEXT("LayerLineMod"); } void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } void Eval(float, float, float WorldZ, FVoxelOpSample& InOut) const override { const float CaveSDF = InOut.Sdf; if (!VF_NearCaveSurface(CaveSDF, P.SDFBlendRadius)) { return; } if (Rooms == nullptr) { return; } const FStrateGenerationParams& LP = Rooms->LocalParams(); // C1 : params PAR SALLE if (!(LP.LayerLineSpacing > 0.0f && CaveSDF < FLT_MAX)) { return; } const float DistFromSurface = FMath::Abs(CaveSDF); const float LineRange = LP.LayerLineSpacing * 1.5f; if (!(DistFromSurface < LineRange)) { return; } const float LinePhase = WorldZ * (2.0f * PI) / LP.LayerLineSpacing; float LineValue = VoxelMath::DetSin(LinePhase); LineValue = FMath::Max(LineValue, 0.0f); LineValue = LineValue * LineValue * LineValue; // affûtage cubique float Fade = 1.0f - (DistFromSurface / LineRange); Fade = Fade * Fade; InOut.Density -= LineValue * LP.LayerLineDepth * Fade; } /** Ne SOUSTRAIT que (`LineValue ≥ 0`, `Depth ≥ 0`) ⇒ `CarveOnly`, jamais `Both`. Un des * rares modificateurs de détail qui garde une DIRECTION exploitable par le pliage. */ EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override { return (P.LayerLineSpacing > 0.0f && P.LayerLineDepth > 0.0f) ? EVoxelOpEffect::CarveOnly : EVoxelOpEffect::Identity; } EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx, const FVoxelBoxHypotheses& H) const override { return VF_CaveDetailEffect(Rooms, H, P.SDFBlendRadius, EffectOverBox(VoxelBox, Ctx)); } /** `LineValue = max(sin,0)³ ∈ [0,1]`, `Fade ∈ [0,1]` ⇒ retrait ≤ `LayerLineDepth`. * ⚠️ Borne calculée sur les params de la STRATE : un op `LayerLines` par salle peut écrire * une profondeur PLUS GRANDE, ce qui rendrait cette borne fausse dans le sens dangereux. * C'est la même dette que la note d'`EffectOverBox` juste au-dessus, et elle doit être * réglée par le même correctif — AVANT que `ClassifyTile` ne consomme `ClassifyBox`. */ float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override { return MaxAmplitude(); } float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override { return 0.0f; } float MaxCarveOverBox(const FBox&, const FVoxelOpContext&, const FVoxelBoxHypotheses& H) const override { return VF_CaveDetailMax(Rooms, H, P.SDFBlendRadius, MaxAmplitude()); } float MaxFillOverBox(const FBox&, const FVoxelOpContext&, const FVoxelBoxHypotheses&) const override { return 0.0f; } float MaxAmplitude() const { if (!(P.LayerLineSpacing > 0.0f)) { return 0.0f; } return VoxelMath::IsFinite(P.LayerLineDepth) ? FMath::Max(P.LayerLineDepth, 0.0f) : FLT_MAX; } private: FStrateGenerationParams P; const FRoomGraphSource* Rooms; // NON possédant }; //========================================================================= // RÔLE 3 — MODIFIER : NERVURES / RIBBING (TunnelNetwork, STEP 4c) //========================================================================= // La MÊME sinusoïde en Z que les lignes de strates, décalée d'un quart de période // (`+ PI · 0.5`), rectifiée puis CARRÉE au lieu de cubée, et AJOUTÉE au lieu d'être soustraite : // des bourrelets arrondis (tube de lave) au lieu de rainures fines. // // ⚠️ Deux opérateurs, pas un avec un signe : l'exposant diffère (3 contre 2), la phase diffère, // et le paramètre d'espacement est indépendant. Les fusionner demanderait trois paramètres pour // économiser dix lignes, et rendrait la correspondance avec l'original illisible. class FRibbingMod final : public IVoxelDensityOp { public: FRibbingMod(const FStrateGenerationParams& InP, const FRoomGraphSource* InRooms) : P(InP), Rooms(InRooms) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density | VoxelOpChannels::Sdf; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } bool IsAdditive() const override { return true; } void PrepareChunk(const FVoxelOpContext&) override {} void PrepareBlockSample(const FVoxelOpSample& Sample) const override { if (Rooms != nullptr) { Rooms->RestoreBlockSample(Sample); } } const TCHAR* DebugName() const override { return TEXT("RibbingMod"); } void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } void Eval(float, float, float WorldZ, FVoxelOpSample& InOut) const override { const float CaveSDF = InOut.Sdf; if (!VF_NearCaveSurface(CaveSDF, P.SDFBlendRadius)) { return; } if (Rooms == nullptr) { return; } const FStrateGenerationParams& LP = Rooms->LocalParams(); // C1 : params PAR SALLE if (!(LP.RibbingSpacing > 0.0f && CaveSDF < FLT_MAX)) { return; } const float DistFromSurface = FMath::Abs(CaveSDF); const float RibRange = LP.RibbingSpacing * 1.5f; if (!(DistFromSurface < RibRange)) { return; } const float RibPhase = WorldZ * (2.0f * PI) / LP.RibbingSpacing + PI * 0.5f; float RibValue = VoxelMath::DetSin(RibPhase); RibValue = FMath::Max(RibValue, 0.0f); RibValue = RibValue * RibValue; // profil de bosse arrondi float Fade = 1.0f - (DistFromSurface / RibRange); Fade = Fade * Fade; InOut.Density += RibValue * LP.RibbingDepth * Fade; } /** N'AJOUTE que du solide ⇒ `FillOnly`. */ EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override { return (P.RibbingSpacing > 0.0f && P.RibbingDepth > 0.0f) ? EVoxelOpEffect::FillOnly : EVoxelOpEffect::Identity; } EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx, const FVoxelBoxHypotheses& H) const override { return VF_CaveDetailEffect(Rooms, H, P.SDFBlendRadius, EffectOverBox(VoxelBox, Ctx)); } /** `RibValue = max(sin,0)² ∈ [0,1]`, `Fade ∈ [0,1]` ⇒ ajout ≤ `RibbingDepth`. * Même réserve « params de strate » que `FLayerLineMod::MaxCarveOverBox`. */ float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override { return 0.0f; } float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override { return MaxAmplitude(); } float MaxCarveOverBox(const FBox&, const FVoxelOpContext&, const FVoxelBoxHypotheses&) const override { return 0.0f; } float MaxFillOverBox(const FBox&, const FVoxelOpContext&, const FVoxelBoxHypotheses& H) const override { return VF_CaveDetailMax(Rooms, H, P.SDFBlendRadius, MaxAmplitude()); } float MaxAmplitude() const { if (!(P.RibbingSpacing > 0.0f)) { return 0.0f; } return VoxelMath::IsFinite(P.RibbingDepth) ? FMath::Max(P.RibbingDepth, 0.0f) : FLT_MAX; } private: FStrateGenerationParams P; const FRoomGraphSource* Rooms; // NON possédant }; //========================================================================= // RÔLE 3 — MODIFIER : SURPLOMBS DE GROTTE / CAVE OVERHANGS (TunnelNetwork, STEP 4c) //========================================================================= // ⚠️ TROISIÈME OPÉRATEUR NOMMÉ « OVERHANG » DANS CE FICHIER, et ils n'ont rien en commun : // • `FOverhangShelfMod` (SurfaceWorld) emprunte la hauteur de terrain amont — géométrique ; // • celui-ci est un bruit fBM à FRÉQUENCE EN Z RÉDUITE (×0.15), dont on ne garde que le lobe // POSITIF : la roche ne s'étend que VERS la grotte, jamais en creux. D'où des étagères // éparses au lieu d'un déplacement uniforme. // Même nom dans l'éditeur, deux idées différentes. `OPSTACK-DECOMPOSITION §1` les sépare déjà. class FCaveOverhangMod final : public IVoxelDensityOp { public: FCaveOverhangMod(const FStrateGenerationParams& InP, int32 Seed, const FRoomGraphSource* InRooms) : P(InP), SeedU((uint32)Seed), Rooms(InRooms) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density | VoxelOpChannels::Sdf; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } bool IsAdditive() const override { return true; } void PrepareChunk(const FVoxelOpContext&) override {} void PrepareBlockSample(const FVoxelOpSample& Sample) const override { if (Rooms != nullptr) { Rooms->RestoreBlockSample(Sample); } } const TCHAR* DebugName() const override { return TEXT("CaveOverhangMod"); } void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { const float CaveSDF = InOut.Sdf; if (!VF_NearCaveSurface(CaveSDF, P.SDFBlendRadius)) { return; } if (Rooms == nullptr) { return; } const FStrateGenerationParams& LP = Rooms->LocalParams(); // C1 : params PAR SALLE if (!(LP.OverhangStrength > 0.0f && CaveSDF < FLT_MAX)) { return; } const float DistFromSurface = FMath::Abs(CaveSDF); const float OverhangRange = LP.OverhangDepth * 2.0f; if (!(DistFromSurface < OverhangRange)) { return; } const float EffectiveZ = (P.VerticalScale != 1.0f && P.VerticalScale > 0.0f) ? (WorldZ / P.VerticalScale) : WorldZ; // Fréquence en Z à 0.15× celle de XY ⇒ les motifs s'étirent horizontalement. const float OverhangNoise = HFractal3D(FVector3f( WorldX * LP.OverhangFrequency + VoxelHash::SeedOffset(SeedU, 53.1f), WorldY * LP.OverhangFrequency + VoxelHash::SeedOffset(SeedU, 59.3f), EffectiveZ * LP.OverhangFrequency * 0.15f + VoxelHash::SeedOffset(SeedU, 61.7f) ), VoxelGenLOD::Eff(2)) * VOXEL_NOISE_SCALE; if (OverhangNoise > 0.0f) { float Fade = 1.0f - (DistFromSurface / OverhangRange); Fade = Fade * Fade; InOut.Density += OverhangNoise * LP.OverhangDepth * LP.OverhangStrength * Fade; } } /** Lobe positif seulement ⇒ n'AJOUTE que du solide ⇒ `FillOnly`. */ EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override { return (P.OverhangStrength > 0.0f && P.OverhangDepth > 0.0f) ? EVoxelOpEffect::FillOnly : EVoxelOpEffect::Identity; } EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx, const FVoxelBoxHypotheses& H) const override { return VF_CaveDetailEffect(Rooms, H, P.SDFBlendRadius, EffectOverBox(VoxelBox, Ctx)); } /** fBM ∈ [-1,1] × `VOXEL_NOISE_SCALE`, lobe positif seulement, `Fade ∈ [0,1]` ⇒ ajout * ≤ `SCALE · Depth · Strength`. Même réserve « params de strate ». */ float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override { return 0.0f; } float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override { return MaxAmplitude(); } float MaxCarveOverBox(const FBox&, const FVoxelOpContext&, const FVoxelBoxHypotheses&) const override { return 0.0f; } float MaxFillOverBox(const FBox&, const FVoxelOpContext&, const FVoxelBoxHypotheses& H) const override { return VF_CaveDetailMax(Rooms, H, P.SDFBlendRadius, MaxAmplitude()); } float MaxAmplitude() const { if (!(P.OverhangStrength > 0.0f && P.OverhangDepth > 0.0f)) { return 0.0f; } if (!VoxelMath::IsFinite(P.OverhangDepth) || !VoxelMath::IsFinite(P.OverhangStrength)) { return FLT_MAX; } const float Result = VOXEL_NOISE_SCALE * VF_PerlinAbsBound * P.OverhangDepth * P.OverhangStrength; return VoxelMath::IsFinite(Result) ? Result : FLT_MAX; } private: FStrateGenerationParams P; uint32 SeedU; const FRoomGraphSource* Rooms; // NON possédant }; //========================================================================= // RÔLE 3 — MODIFIER : AFFÛTAGE DE FALAISE / CLIFF SHARPENING (TunnelNetwork, STEP 4c) //========================================================================= // ⚠️ TRANSCRIT TEL QUEL BIEN QUE LE COMMENTAIRE DE L'ORIGINAL DÉCRIVE AUTRE CHOSE. // Il annonce « échantillonner la densité en Z±1 et calculer le gradient vertical » ; le code, lui, // n'échantillonne RIEN : il tire un Perlin dont la fréquence en Z est 3× celle de XY et l'appelle // `VertGrad`. C'est un PROXY de gradient, pas un gradient — donc l'effet est décorrélé de la // pente réelle de la paroi. Le multiplier par `CaveSDF` lui donne quand même le bon SIGNE de part // et d'autre de la surface (plus solide côté roche, plus creusé côté air), ce qui suffit à // produire des faces plus raides. // // Corriger l'écart changerait le monde ; le taire le laisserait se faire « corriger » un jour par // quelqu'un qui lit le commentaire et pas le code. Noté ici ET dans OPSTACK-PROGRESS. // // Ported as written, not as commented: the original's comment promises a sampled vertical // gradient, the code uses a Z-stretched Perlin as a proxy. Fixing it would change the world. class FCaveCliffMod final : public IVoxelDensityOp { public: FCaveCliffMod(const FStrateGenerationParams& InP, int32 Seed, const FRoomGraphSource* InRooms) : P(InP), SeedU((uint32)Seed), Rooms(InRooms) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density | VoxelOpChannels::Sdf; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } bool IsAdditive() const override { return true; } void PrepareChunk(const FVoxelOpContext&) override {} void PrepareBlockSample(const FVoxelOpSample& Sample) const override { if (Rooms != nullptr) { Rooms->RestoreBlockSample(Sample); } } const TCHAR* DebugName() const override { return TEXT("CaveCliffMod"); } void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { const float CaveSDF = InOut.Sdf; if (!VF_NearCaveSurface(CaveSDF, P.SDFBlendRadius)) { return; } if (Rooms == nullptr) { return; } const FStrateGenerationParams& LP = Rooms->LocalParams(); // C1 : params PAR SALLE if (!(LP.CliffStrength > 0.0f && CaveSDF < FLT_MAX)) { return; } const float DistFromSurface = FMath::Abs(CaveSDF); const float CliffRange = 8.0f; // constante en dur dans l'original if (!(DistFromSurface < CliffRange)) { return; } const float EffectiveZ = (P.VerticalScale != 1.0f && P.VerticalScale > 0.0f) ? (WorldZ / P.VerticalScale) : WorldZ; const 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) // 3× plus vite en Z )) * VOXEL_NOISE_SCALE; const float CliffEffect = VertGrad * CaveSDF * LP.CliffStrength; if (FMath::Abs(VertGrad) > 0.3f) { float Fade = 1.0f - (DistFromSurface / CliffRange); Fade = Fade * Fade; InOut.Density += CliffEffect * Fade * 3.0f; } } /** `Both` : le signe suit celui de `VertGrad · CaveSDF`, donc les deux directions. */ EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override { return (P.CliffStrength > 0.0f) ? EVoxelOpEffect::Both : EVoxelOpEffect::Identity; } EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx, const FVoxelBoxHypotheses& H) const override { return VF_CaveDetailEffect(Rooms, H, P.SDFBlendRadius, EffectOverBox(VoxelBox, Ctx)); } /** `|VertGrad| ≤ SCALE·1.5`, `|CaveSDF| < 8`, `Fade ≤ 1`, final multiplier `3`. */ float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override { return MaxAmplitude(); } float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override { return MaxAmplitude(); } float MaxCarveOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx, const FVoxelBoxHypotheses& H) const override { return VF_CaveDetailMax(Rooms, H, P.SDFBlendRadius, MaxCarveOverBox(VoxelBox, Ctx)); } float MaxFillOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx, const FVoxelBoxHypotheses& H) const override { return VF_CaveDetailMax(Rooms, H, P.SDFBlendRadius, MaxFillOverBox(VoxelBox, Ctx)); } float MaxAmplitude() const { if (!(P.CliffStrength > 0.0f)) { return 0.0f; } if (!VoxelMath::IsFinite(P.CliffStrength)) { return FLT_MAX; } const float Result = 8.0f * 3.0f * VOXEL_NOISE_SCALE * VF_PerlinAbsBound * P.CliffStrength; return VoxelMath::IsFinite(Result) ? Result : FLT_MAX; } private: FStrateGenerationParams P; uint32 SeedU; const FRoomGraphSource* Rooms; // NON possédant }; //========================================================================= // RÔLE 3 — MODIFIER : FESTONS / SCALLOP (TunnelNetwork, STEP 4c) //========================================================================= // Cuvettes concaves d'érosion hydraulique. Bruit cellulaire : la valeur est haute AU CENTRE // d'une cellule (loin des points-features), donc on y creuse — d'où des rangées de coupelles // lisses, la signature des grottes calcaires. // // Deuxième client de `VoxelNoise::Cellular3D` (le premier est la rugosité en mode Cellular) — // c'est-à-dire la deuxième raison pour laquelle ce corps devait être PARTAGÉ et non recopié. class FScallopMod final : public IVoxelDensityOp { public: FScallopMod(const FStrateGenerationParams& InP, int32 Seed, const FRoomGraphSource* InRooms) : P(InP), SeedU((uint32)Seed), Rooms(InRooms) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density | VoxelOpChannels::Sdf; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } bool IsAdditive() const override { return true; } void PrepareChunk(const FVoxelOpContext&) override {} void PrepareBlockSample(const FVoxelOpSample& Sample) const override { if (Rooms != nullptr) { Rooms->RestoreBlockSample(Sample); } } const TCHAR* DebugName() const override { return TEXT("ScallopMod"); } void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { const float CaveSDF = InOut.Sdf; if (!VF_NearCaveSurface(CaveSDF, P.SDFBlendRadius)) { return; } if (Rooms == nullptr) { return; } const FStrateGenerationParams& LP = Rooms->LocalParams(); // C1 : params PAR SALLE if (!(LP.ScallopStrength > 0.0f && CaveSDF < FLT_MAX)) { return; } const float DistFromSurface = FMath::Abs(CaveSDF); const float ScallopRange = LP.ScallopStrength * 4.0f; if (!(DistFromSurface < ScallopRange)) { return; } const float EffectiveZ = (P.VerticalScale != 1.0f && P.VerticalScale > 0.0f) ? (WorldZ / P.VerticalScale) : WorldZ; const float SF = LP.ScallopFrequency; const float ScallopNoise = VoxelNoise::Cellular3D(FVector3f( WorldX * SF + VoxelHash::SeedOffset(SeedU, 83.1f), WorldY * SF + VoxelHash::SeedOffset(SeedU, 89.3f), EffectiveZ * SF + VoxelHash::SeedOffset(SeedU, 97.7f) )); if (ScallopNoise > 0.0f) { float Fade = 1.0f - (DistFromSurface / ScallopRange); Fade = Fade * Fade; InOut.Density -= ScallopNoise * LP.ScallopStrength * Fade; } } /** Lobe positif seulement, SOUSTRAIT ⇒ `CarveOnly`. */ EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override { return (P.ScallopStrength > 0.0f) ? EVoxelOpEffect::CarveOnly : EVoxelOpEffect::Identity; } EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx, const FVoxelBoxHypotheses& H) const override { return VF_CaveDetailEffect(Rooms, H, P.SDFBlendRadius, EffectOverBox(VoxelBox, Ctx)); } /** The implementation maps an unclamped F2-F1 distance to `2*d-1`; the proven finite * envelope used here is `VF_CellularAbsBound`, not the approximate [-1,1] comment beside * the noise function. The positive lobe and `Fade ∈ [0,1]` then bound the carve. */ float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override { return MaxAmplitude(); } float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override { return 0.0f; } float MaxCarveOverBox(const FBox&, const FVoxelOpContext&, const FVoxelBoxHypotheses& H) const override { return VF_CaveDetailMax(Rooms, H, P.SDFBlendRadius, MaxAmplitude()); } float MaxFillOverBox(const FBox&, const FVoxelOpContext&, const FVoxelBoxHypotheses&) const override { return 0.0f; } float MaxAmplitude() const { if (!(P.ScallopStrength > 0.0f)) { return 0.0f; } if (!VoxelMath::IsFinite(P.ScallopStrength)) { return FLT_MAX; } const float Result = VF_CellularAbsBound * P.ScallopStrength; return VoxelMath::IsFinite(Result) ? Result : FLT_MAX; } private: FStrateGenerationParams P; uint32 SeedU; const FRoomGraphSource* Rooms; // NON possédant }; //========================================================================= // RÔLE 3 — MODIFIER : ARCHES / PONTS DE ROCHE (TunnelNetwork, STEP 4c) //========================================================================= // ⚠️ PREMIER MODIFICATEUR RELATIF À LA SALLE. Il ne lit pas seulement `InOut.Sdf` : il lui faut // LA SALLE — son hash (pour tirer les arches de façon déterministe), son centre et ses rayons. // C'est l'unique consommateur de `NearestRoomIdx` avec les dômes, le pincement et le biais de // sol, et c'est ce qui rend `FRoomGraphSource::GetNearestRoomIdx()` nécessaire. // // ⚠️ SA PORTE N'EST PAS LE GATE COMMUN : `CaveSDF < SDFBlendRadius` (dans la grotte ou tout // près), pas `< SDFBlendRadius·3`. Une arche se pose dans le VIDE de la salle, pas dans sa paroi. class FCaveArchMod final : public IVoxelDensityOp { public: FCaveArchMod(const FStrateGenerationParams& InP, const FRoomGraphSource* InRooms) : P(InP), Rooms(InRooms) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density | VoxelOpChannels::Sdf; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } bool IsAdditive() const override { return true; } void PrepareChunk(const FVoxelOpContext&) override {} void PrepareBlockSample(const FVoxelOpSample& Sample) const override { if (Rooms != nullptr) { Rooms->RestoreBlockSample(Sample); } } const TCHAR* DebugName() const override { return TEXT("CaveArchMod"); } void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { const float CaveSDF = InOut.Sdf; if (!VF_NearCaveSurface(CaveSDF, P.SDFBlendRadius)) { return; } if (Rooms == nullptr) { return; } const int32 NearestRoomIdx = Rooms->GetNearestRoomIdx(); const FStrateGenerationParams& LP = Rooms->LocalParams(); // C1 : params PAR SALLE if (!(LP.ArchDensity > 0.0f && CaveSDF < LP.SDFBlendRadius && CaveSDF < FLT_MAX && NearestRoomIdx >= 0)) { return; } // ⚠️ `IsValidIndex` AJOUTÉ : l'original indexe directement après le seul test `>= 0`. // L'index vient d'`EvaluateSDFCached` donc il est valide par construction — ce garde-fou // ne peut donc JAMAIS changer la sortie, seulement empêcher un crash si l'invariant se // cassait un jour. Même famille de décision que l'empreinte de params dans la clé de // cache : on se trompe du côté du coût, jamais du côté du résultat. const FChunkSDFCache& Cache = Rooms->GetCache(); if (!Cache.Rooms.IsValidIndex(NearestRoomIdx)) { return; } const FCachedRoom& Room = Cache.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); InOut.Density += Fill * Arch.BaseDensity * 1.5f; } } } /** N'AJOUTE que du solide ⇒ `FillOnly`. La décision spatiale est indépendante : elle * consomme l'intervalle SDF publié par la source, comme les autres détails de salle. */ EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override { if (!(P.ArchDensity > 0.0f)) { return EVoxelOpEffect::Identity; } if (P.BaseDensity > 0.0f) { return EVoxelOpEffect::FillOnly; } if (P.BaseDensity < 0.0f) { return EVoxelOpEffect::CarveOnly; } return EVoxelOpEffect::Identity; } EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx, const FVoxelBoxHypotheses& H) const override { return VF_CaveDetailEffect(Rooms, H, P.SDFBlendRadius, EffectOverBox(VoxelBox, Ctx)); } float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override { return P.BaseDensity < 0.0f ? MaxAmplitude() : 0.0f; } float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override { return P.BaseDensity > 0.0f ? MaxAmplitude() : 0.0f; } float MaxCarveOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx, const FVoxelBoxHypotheses& H) const override { return VF_CaveDetailMax(Rooms, H, P.SDFBlendRadius, MaxCarveOverBox(VoxelBox, Ctx)); } float MaxFillOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx, const FVoxelBoxHypotheses& H) const override { return VF_CaveDetailMax(Rooms, H, P.SDFBlendRadius, MaxFillOverBox(VoxelBox, Ctx)); } float MaxAmplitude() const { if (!(P.ArchDensity > 0.0f)) { return 0.0f; } if (!VoxelMath::IsFinite(P.BaseDensity)) { return FLT_MAX; } const float Result = FMath::Abs(P.BaseDensity) * 3.0f * 1.5f; return VoxelMath::IsFinite(Result) ? Result : FLT_MAX; } private: FStrateGenerationParams P; const FRoomGraphSource* Rooms; // NON possédant }; //========================================================================= // RÔLE 3 — MODIFIER : COLONNES DE SALLE / ROOM COLUMNS (TunnelNetwork, STEP 4d) //========================================================================= // ⚠️ CE N'EST PAS `FGridColumnMod` (FlatPlain / CrystalChamber). Celui-là pose des cylindres sur // une GRILLE MONDE et se tire par cellule ; celui-ci PARCOURT une liste PRÉ-CUITE par // `BuildChunkCache`, salle par salle. `OPSTACK-DECOMPOSITION §1` les sépare explicitement. // // ⚠️⚠️ IL N'A **AUCUN** PARAMÈTRE DE STRATE, ET C'EST LE PIÈGE DE CE GROUPE. // Le code d'origine n'écrit aucun `if (Params.ColumnDensity > 0)` : il itère la liste cuite, // point. `FStrateGenerationParams::ColumnDensity` n'est JAMAIS lu par la cuisson non plus (elle // lit `OpParams`, un struct NEUF où seul l'op de la salle a été appliqué). Donc : // • mettre `ColumnDensity = 0` dans les params N'ÉTEINT PAS les colonnes ; // • la seule façon d'avoir des colonnes est un `UVoxelTerrainOpDefinition` de type `Column` // dans le pool de la strate ; // • et la seule façon de PROUVER qu'elles ont tiré est de regarder `SDFCache.Columns.Num()`. // C'est exactement la leçon des pits, une troisième fois. Le test l'applique au contrôle 3b. class FRoomColumnMod final : public IVoxelDensityOp { public: FRoomColumnMod(const FStrateGenerationParams& InP, const FRoomGraphSource* InRooms) : P(InP), Rooms(InRooms) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density | VoxelOpChannels::Sdf; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } bool IsAdditive() const override { return true; } void PrepareChunk(const FVoxelOpContext&) override {} void PrepareBlockSample(const FVoxelOpSample& Sample) const override { if (Rooms != nullptr) { Rooms->RestoreBlockSample(Sample); } } const TCHAR* DebugName() const override { return TEXT("RoomColumnMod"); } void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } void Eval(float WorldX, float WorldY, float, FVoxelOpSample& InOut) const override { if (!VF_NearCaveSurface(InOut.Sdf, P.SDFBlendRadius)) { return; } if (Rooms == nullptr) { return; } auto EvaluateColumn = [&](int32 ColumnIndex) { const FCachedColumn& Col = Rooms->GetCache().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); InOut.Density += Fill * Col.BaseDensity * 1.5f; } }; VF_ForEachChunkSDFSpatialCandidate( Rooms->GetCache().ColumnSpatialIndex, Rooms->GetCache().Columns.Num(), WorldX, WorldY, EvaluateColumn, VoxelGenLOD::ShouldUseSpatialIndex(false)); } /** The cached column list is the complete source of this op's work. A valid empty list is * therefore an exact Identity result; an invalid box cache stays conservative. */ EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override { return EVoxelOpEffect::FillOnly; } EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx, const FVoxelBoxHypotheses& H) const override { if (Rooms == nullptr || VF_CaveBoxIsFar(H, P.SDFBlendRadius)) { return EVoxelOpEffect::Identity; } const float Bound = Rooms->RoomColumnFillSupremumForLastBox(); if (!(Bound > 0.0f)) { return EVoxelOpEffect::Identity; } return VoxelMath::IsFinite(Bound) ? EVoxelOpEffect::FillOnly : EVoxelOpEffect::Both; } float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override { return 0.0f; } float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override { return FLT_MAX; } float MaxCarveOverBox(const FBox&, const FVoxelOpContext&, const FVoxelBoxHypotheses& H) const override { if (Rooms == nullptr || VF_CaveBoxIsFar(H, P.SDFBlendRadius)) { return 0.0f; } const float Bound = Rooms->RoomColumnFillSupremumForLastBox(); return VoxelMath::IsFinite(Bound) ? 0.0f : Bound; } float MaxFillOverBox(const FBox&, const FVoxelOpContext&, const FVoxelBoxHypotheses& H) const override { if (Rooms == nullptr || VF_CaveBoxIsFar(H, P.SDFBlendRadius)) { return 0.0f; } return Rooms->RoomColumnFillSupremumForLastBox(); } private: FStrateGenerationParams P; const FRoomGraphSource* Rooms; // NON possédant }; //========================================================================= // RÔLE 3 — MODIFIER : DÔMES / CATHEDRAL CEILINGS (TunnelNetwork, STEP 4g) //========================================================================= // Un demi-ellipsoïde creusé VERS LE HAUT depuis un point ancré au-dessus du centre de la salle. // Relatif à la salle comme les arches, même porte `CaveSDF < SDFBlendRadius`. class FDomeMod final : public IVoxelDensityOp { public: FDomeMod(const FStrateGenerationParams& InP, const FRoomGraphSource* InRooms) : P(InP), Rooms(InRooms) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density | VoxelOpChannels::Sdf; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } bool IsAdditive() const override { return true; } void PrepareChunk(const FVoxelOpContext&) override {} void PrepareBlockSample(const FVoxelOpSample& Sample) const override { if (Rooms != nullptr) { Rooms->RestoreBlockSample(Sample); } } const TCHAR* DebugName() const override { return TEXT("DomeMod"); } void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { const float CaveSDF = InOut.Sdf; if (!VF_NearCaveSurface(CaveSDF, P.SDFBlendRadius)) { return; } if (Rooms == nullptr) { return; } const int32 NearestRoomIdx = Rooms->GetNearestRoomIdx(); const FStrateGenerationParams& LP = Rooms->LocalParams(); // C1 : params PAR SALLE if (!(LP.DomeDensity > 0.0f && CaveSDF < LP.SDFBlendRadius && CaveSDF < FLT_MAX && NearestRoomIdx >= 0)) { return; } const FChunkSDFCache& Cache = Rooms->GetCache(); if (!Cache.Rooms.IsValidIndex(NearestRoomIdx)) { return; } // cf. FCaveArchMod const FCachedRoom& Room = Cache.Rooms[NearestRoomIdx]; const int32 MaxDomes = 2; for (int32 i = 0; i < MaxDomes; i++) { const uint32 DH = VoxelHash::Mix(Room.Hash ^ (0xD0AE0u + (uint32)i * 8191u)); if (VoxelHash::ToFloat01(DH) > LP.DomeDensity) { continue; } const uint32 DH2 = VoxelHash::Mix(DH ^ 0xD0A0u); const float DmX = Room.Center.X + VoxelHash::ToFloatSigned(DH2) * Room.RadiusXY * 0.4f; const float DmY = Room.Center.Y + VoxelHash::ToFloatSigned(VoxelHash::Mix(DH2)) * Room.RadiusXY * 0.4f; const uint32 DH3 = VoxelHash::Mix(DH2 ^ 0x90DEu); const float DmRadius = FMath::Min( FMath::Lerp(LP.DomeMinRadius, LP.DomeMaxRadius, VoxelHash::ToFloat01(DH3)), Room.RadiusXY * 0.85f ); const uint32 DH4 = VoxelHash::Mix(DH3 ^ 0xCAFEu); const float DmCenterZ = Room.Center.Z + Room.RadiusZ * 0.2f + VoxelHash::ToFloat01(DH4) * Room.RadiusZ * 0.3f; const float DmHeight = DmRadius * LP.DomeHeightRatio; if (WorldZ > DmCenterZ + DmHeight + 3.0f || WorldZ < DmCenterZ - 3.0f) { continue; } const float DXDm = WorldX - DmX; const float DYDm = WorldY - DmY; const float DZDm = WorldZ - DmCenterZ; if (DZDm < 0.0f) { continue; } // ne creuse que vers le haut const float NormX = DXDm / DmRadius; const float NormY = DYDm / DmRadius; const float NormZ = DZDm / DmHeight; const float EllipDist = FMath::Sqrt(NormX * NormX + NormY * NormY + NormZ * NormZ) - 1.0f; const 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); InOut.Density -= Carve * LP.BaseDensity * 1.5f; } } } /** Ne SOUSTRAIT que ⇒ `CarveOnly`. */ EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override { return (P.DomeDensity > 0.0f) ? EVoxelOpEffect::CarveOnly : EVoxelOpEffect::Identity; } EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx, const FVoxelBoxHypotheses& H) const override { return VF_CaveDetailEffect(Rooms, H, P.SDFBlendRadius, EffectOverBox(VoxelBox, Ctx)); } float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override { return MaxAmplitude(); } float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override { return 0.0f; } float MaxCarveOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx, const FVoxelBoxHypotheses& H) const override { return VF_CaveDetailMax(Rooms, H, P.SDFBlendRadius, MaxCarveOverBox(VoxelBox, Ctx)); } float MaxFillOverBox(const FBox&, const FVoxelOpContext&, const FVoxelBoxHypotheses&) const override { return 0.0f; } float MaxAmplitude() const { if (!(P.DomeDensity > 0.0f)) { return 0.0f; } if (!VoxelMath::IsFinite(P.BaseDensity)) { return FLT_MAX; } const float Result = FMath::Abs(P.BaseDensity) * 2.0f * 1.5f; return VoxelMath::IsFinite(Result) ? Result : FLT_MAX; } private: FStrateGenerationParams P; const FRoomGraphSource* Rooms; // NON possédant }; //========================================================================= // RÔLE 3 — MODIFIER : PINCEMENT / BOTTLENECK (TunnelNetwork, STEP 4h) //========================================================================= // Resserre un passage PAR LES CÔTÉS. Placé sur le PÉRIMÈTRE de la salle (offset 0.85 · rayon) — // là où les tunnels débouchent — et pas au centre, sinon il boucherait la salle elle-même. // `SideFactor` (distance à l'axe, clampée) est ce qui laisse l'axe du passage libre : le // remplissage est nul sur l'axe et maximal sur les bords de l'ellipsoïde. class FPinchMod final : public IVoxelDensityOp { public: FPinchMod(const FStrateGenerationParams& InP, const FRoomGraphSource* InRooms) : P(InP), Rooms(InRooms) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density | VoxelOpChannels::Sdf; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } bool IsAdditive() const override { return true; } void PrepareChunk(const FVoxelOpContext&) override {} void PrepareBlockSample(const FVoxelOpSample& Sample) const override { if (Rooms != nullptr) { Rooms->RestoreBlockSample(Sample); } } const TCHAR* DebugName() const override { return TEXT("PinchMod"); } void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { const float CaveSDF = InOut.Sdf; if (!VF_NearCaveSurface(CaveSDF, P.SDFBlendRadius)) { return; } if (Rooms == nullptr) { return; } const int32 NearestRoomIdx = Rooms->GetNearestRoomIdx(); const FStrateGenerationParams& LP = Rooms->LocalParams(); // C1 : params PAR SALLE if (!(LP.PinchDensity > 0.0f && CaveSDF < LP.SDFBlendRadius && CaveSDF < FLT_MAX && NearestRoomIdx >= 0)) { return; } const FChunkSDFCache& Cache = Rooms->GetCache(); if (!Cache.Rooms.IsValidIndex(NearestRoomIdx)) { return; } // cf. FCaveArchMod const FCachedRoom& Room = Cache.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; 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); InOut.Density += Fill * SideFactor * Pinch.BaseDensity * 1.5f; } } } /** N'AJOUTE que du solide ⇒ `FillOnly`. */ EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override { return (P.PinchDensity > 0.0f) ? EVoxelOpEffect::FillOnly : EVoxelOpEffect::Identity; } EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx, const FVoxelBoxHypotheses& H) const override { return VF_CaveDetailEffect(Rooms, H, P.SDFBlendRadius, EffectOverBox(VoxelBox, Ctx)); } float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override { return 0.0f; } float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override { return MaxAmplitude(); } float MaxCarveOverBox(const FBox&, const FVoxelOpContext&, const FVoxelBoxHypotheses& H) const override { return VF_CaveDetailMax(Rooms, H, P.SDFBlendRadius, 0.0f); } float MaxFillOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx, const FVoxelBoxHypotheses& H) const override { return VF_CaveDetailMax(Rooms, H, P.SDFBlendRadius, MaxFillOverBox(VoxelBox, Ctx)); } float MaxAmplitude() const { if (!(P.PinchDensity > 0.0f)) { return 0.0f; } if (!VoxelMath::IsFinite(P.BaseDensity)) { return FLT_MAX; } const float Result = FMath::Abs(P.BaseDensity) * 3.0f * 1.5f; return VoxelMath::IsFinite(Result) ? Result : FLT_MAX; } private: FStrateGenerationParams P; const FRoomGraphSource* Rooms; // NON possédant }; //========================================================================= // RÔLE 3 — MODIFIER : BIAIS DE SOL / FLOOR BIAS (TunnelNetwork, fin de 4h) //========================================================================= // Rend de la densité dans la moitié BASSE de la salle pour contrer le relief que la rugosité // laisse sur les sols — un sol praticable au lieu d'un sol bosselé. Ne s'applique QUE dans l'air // certain (`CaveSDF < 0`) : dans la paroi, le clamp anti-remplissage de la rugosité tient déjà. // // ⚠️ DERNIER DE LA CHAÎNE, ET CE N'EST PAS INTERCHANGEABLE : il ajoute un biais indépendant à // la densité déjà accumulée par les étapes précédentes. Il ne lit ni ne soustrait la variation // de rugosité (4b). Le déplacer avant les autres étapes changerait donc leur entrée et leur // sortie, ce qui est la raison pour laquelle l'ordre de `BuildTunnelNetworkStack` reste celui // de l'original, ligne pour ligne. // // This op is intentionally separate from 4b. Ten operators lie // between 4b and this phase (4c through 4h), and every one reads+writes Density. Moving this // phase next to 4b would reorder those density updates, so a bit-identical fuse is not legal. class FFloorBiasMod final : public IVoxelDensityOp { public: FFloorBiasMod(const FStrateGenerationParams& InP, const FRoomGraphSource* InRooms) : P(InP), Rooms(InRooms) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density | VoxelOpChannels::Sdf; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } bool IsAdditive() const override { return true; } void PrepareChunk(const FVoxelOpContext&) override {} void PrepareBlockSample(const FVoxelOpSample& Sample) const override { if (Rooms != nullptr) { Rooms->RestoreBlockSample(Sample); } } const TCHAR* DebugName() const override { return TEXT("FloorBiasMod"); } void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } void Eval(float, float, float WorldZ, FVoxelOpSample& InOut) const override { const float CaveSDF = InOut.Sdf; if (!VF_NearCaveSurface(CaveSDF, P.SDFBlendRadius)) { return; } if (Rooms == nullptr) { return; } const int32 NearestRoomIdx = Rooms->GetNearestRoomIdx(); const FStrateGenerationParams& LP = Rooms->LocalParams(); // C1 : params PAR SALLE if (!(LP.FloorBias > 0.0f && NearestRoomIdx >= 0 && CaveSDF < 0.0f)) { return; } const FChunkSDFCache& Cache = Rooms->GetCache(); if (!Cache.Rooms.IsValidIndex(NearestRoomIdx)) { return; } // cf. FCaveArchMod const FCachedRoom& NR = Cache.Rooms[NearestRoomIdx]; // NormZ : -1 = sol de la salle, 0 = centre, +1 = plafond. const float NormZ = (WorldZ - NR.Center.Z) / FMath::Max(NR.RadiusZ, 1.0f); if (NormZ < 0.0f) { const float FloorFactor = NormZ * NormZ; // 0 au centre, 1 au sol InOut.Density += FloorFactor * LP.FloorBias; } } /** N'AJOUTE que du solide ⇒ `FillOnly`. */ EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override { return (P.FloorBias > 0.0f) ? EVoxelOpEffect::FillOnly : EVoxelOpEffect::Identity; } EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx, const FVoxelBoxHypotheses& H) const override { return VF_CaveDetailEffect(Rooms, H, P.SDFBlendRadius, EffectOverBox(VoxelBox, Ctx)); } float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override { return 0.0f; } float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override { return VoxelMath::IsFinite(P.FloorBias) && P.FloorBias >= 0.0f ? P.FloorBias : (P.FloorBias < 0.0f ? 0.0f : FLT_MAX); } float MaxCarveOverBox(const FBox&, const FVoxelOpContext&, const FVoxelBoxHypotheses& H) const override { return VF_CaveBoxIsFar(H, P.SDFBlendRadius) ? 0.0f : 0.0f; } float MaxFillOverBox(const FBox&, const FVoxelOpContext&, const FVoxelBoxHypotheses& H) const override { if (Rooms == nullptr || VF_CaveBoxIsFar(H, P.SDFBlendRadius)) { return 0.0f; } return Rooms->FloorBiasFillSupremumForLastBox(); } private: FStrateGenerationParams P; const FRoomGraphSource* Rooms; // NON possédant }; //========================================================================= // RÔLE 1 — SOURCE : VERS / WORM TUNNELS (TunnelNetwork) //========================================================================= // Un carve par SEUIL sur du bruit 3D, masqué par la distance au réseau de salles. Il écrit la // DENSITÉ directement (pas le canal SDF) : c'est une source « fieldée », pas une primitive // placée — la distinction que `AUDIT §6.2` pose et que `OPSTACK-DECOMPOSITION §0.2` chiffre. // // ⚠️ IL LIT `InOut.Sdf` : le masque de réseau est une fonction de `CaveSDF` APRÈS pits et // cheminées. C'est encore le canal SDF utilisé comme ce pour quoi il existe — transporter une // information géométrique entre deux opérateurs au lieu de la recalculer. class FWormFieldSource final : public IVoxelDensityOp { public: FWormFieldSource(const FStrateGenerationParams& InP, int32 Seed) : P(InP) , SeedU((uint32)Seed) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density | VoxelOpChannels::Sdf; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } bool IsAdditive() const override { return true; } void PrepareChunk(const FVoxelOpContext&) override {} void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { if (!(P.WormStrength > 0.0f && P.WormThreshold > 0.0f)) { return; } const float CaveSDF = InOut.Sdf; float NetworkMask = 1.0f; if (P.WormNetworkRange > 0.0f) { if (CaveSDF >= P.WormNetworkRange) // vrai aussi quand il n'y a pas de réseau (FLT_MAX) { NetworkMask = 0.0f; } else if (CaveSDF > 0.0f) { NetworkMask = 1.0f - SmoothStep01(CaveSDF / P.WormNetworkRange); } } if (NetworkMask <= 0.0f) { return; } const VoxelWormField::FParameters WormParameters{ P.WormFrequency, P.WormHorizontalBias, P.VerticalScale, SeedU}; const float WormValue = VoxelWormField::Evaluate( WorldX, WorldY, WorldZ, P.WormThreshold, WormParameters); if (WormValue < P.WormThreshold) { const float t = 1.0f - (WormValue / P.WormThreshold); InOut.Density -= t * P.WormStrength * NetworkMask; } } /** * The worm is a density consumer, not an SDF source. Its intrinsic response is `CarveOnly`. * The state-aware fold may prove it inactive only when the interval already published by a * preceding SDF writer proves `Sdf >= WormNetworkRange` throughout the box. An unknown or * unrelated interval leaves the carve active, which is conservative for free composition. * Its magnitude is independently bounded by `WormStrength` because both `t` and * `NetworkMask` are in [0, 1]. */ EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override { if (!(P.WormStrength > 0.0f && P.WormThreshold > 0.0f)) { return EVoxelOpEffect::Identity; } return EVoxelOpEffect::CarveOnly; } EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx, const FVoxelBoxHypotheses& H) const override { const EVoxelOpEffect Intrinsic = EffectOverBox(VoxelBox, Ctx); if (Intrinsic == EVoxelOpEffect::Identity) { return Intrinsic; } if (P.WormNetworkRange > 0.0f && H.Sdf.IsKnown() && H.Sdf.Min >= P.WormNetworkRange) { return EVoxelOpEffect::Identity; } return Intrinsic; } /** * ✅ **LA BORNE DE `§0.2`, MAINTENANT CONSOMMÉE.** Elle a passé plusieurs entrées de journal * écrite mais inutilisée, faute d'un pliage capable de porter un nombre ; ce pliage existe. * * La preuve tient en une ligne : `t = 1 − WormValue/WormThreshold ∈ [0,1]` (le bloc ne * s'exécute que sous le seuil) et `NetworkMask ∈ [0,1]` par construction, donc * `t · WormStrength · NetworkMask ≤ WormStrength`. C'est une borne PROUVÉE, pas prudente — * la seule sorte qui ait le droit d'être ici : sur-estimer coûte du CPU, sous-estimer fait * un trou. */ float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override { return MaxCarveAmplitude(); } float MaxCarveOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx, const FVoxelBoxHypotheses& H) const override { const float Strength = MaxCarveOverBox(VoxelBox, Ctx); if (!(Strength > 0.0f) || !VoxelMath::IsFinite(Strength)) { return Strength; } if (!(P.WormNetworkRange > 0.0f) || !VoxelMath::IsFinite(P.WormNetworkRange) || !H.Sdf.IsKnown() || !VoxelMath::IsFinite(H.Sdf.Min)) { return Strength; } // The classifier's lattice proof is about the exact MC vertices, not the continuum // between them. On a refined child, evaluate the worm's two threshold noises at // those vertices once and bound the actual carve factor there. The root is left on // the cheap interval path; a refined step-32 child has at most 17^3 vertices and the // explicit count cap keeps this check bounded. The network mask still uses the SDF // interval's worst case, so this never assumes a room value that the preceding source // did not prove. if (Ctx.bUseLatticeProof && Ctx.bTightenWarpProof && Ctx.Step >= 1 && VoxelMath::IsFinite(P.WormFrequency) && VoxelMath::IsFinite(P.WormHorizontalBias) && VoxelMath::IsFinite(P.WormThreshold) && P.WormThreshold > 0.0f && VoxelMath::IsFinite(P.VerticalScale)) { const int32 Step = FMath::Max(Ctx.Step, 1); const float OriginX = static_cast(Ctx.LatticeOriginVoxels.X); const float OriginY = static_cast(Ctx.LatticeOriginVoxels.Y); const float OriginZ = static_cast(Ctx.LatticeOriginVoxels.Z); const int32 IX0 = FMath::CeilToInt( ((float)VoxelBox.Min.X - OriginX) / (float)Step - 1.0e-4f); const int32 IY0 = FMath::CeilToInt( ((float)VoxelBox.Min.Y - OriginY) / (float)Step - 1.0e-4f); const int32 IZ0 = FMath::CeilToInt( ((float)VoxelBox.Min.Z - OriginZ) / (float)Step - 1.0e-4f); const int32 IX1 = FMath::FloorToInt( ((float)VoxelBox.Max.X - OriginX) / (float)Step + 1.0e-4f); const int32 IY1 = FMath::FloorToInt( ((float)VoxelBox.Max.Y - OriginY) / (float)Step + 1.0e-4f); const int32 IZ1 = FMath::FloorToInt( ((float)VoxelBox.Max.Z - OriginZ) / (float)Step + 1.0e-4f); const int64 Count = ((int64)IX1 - IX0 + 1) * ((int64)IY1 - IY0 + 1) * ((int64)IZ1 - IZ0 + 1); // A full LOD0 root is intentionally left on the cheap interval path. Refined // child boxes are at most 17^3 lattice vertices, so their exact threshold bound // couples the worm's spatial maximum to each child's SDF lower bound without // evaluating the room graph 35,937 times. if (IX1 >= IX0 && IY1 >= IY0 && IZ1 >= IZ0 && Count > 0 && Count <= 10000) { float MaxThresholdFactor = 0.0f; const float WormZFrequency = P.WormFrequency * P.WormHorizontalBias; for (int32 IZ = IZ0; IZ <= IZ1; ++IZ) for (int32 IY = IY0; IY <= IY1; ++IY) for (int32 IX = IX0; IX <= IX1; ++IX) { const float WorldX = OriginX + (float)IX * (float)Step; const float WorldY = OriginY + (float)IY * (float)Step; const float WorldZ = OriginZ + (float)IZ * (float)Step; const float EffectiveZ = (P.VerticalScale != 1.0f && P.VerticalScale > 0.0f) ? WorldZ / P.VerticalScale : WorldZ; const FVector3f NoiseBase( WorldX * P.WormFrequency + VoxelHash::SeedOffset(SeedU, 1.0f), WorldY * P.WormFrequency + VoxelHash::SeedOffset(SeedU, 1.7f), EffectiveZ * WormZFrequency + VoxelHash::SeedOffset(SeedU, 2.3f)); const float N1 = FMath::Abs(VoxelNoise::Perlin3D(NoiseBase) * VOXEL_NOISE_SCALE); if (N1 >= P.WormThreshold) { continue; } const FVector3f NoiseOffset( 137.0f, 259.0f, 431.0f); const float N2 = FMath::Abs(VoxelNoise::Perlin3D( NoiseBase + NoiseOffset) * VOXEL_NOISE_SCALE); const float WormValue = N1 + N2; if (WormValue < P.WormThreshold) { MaxThresholdFactor = FMath::Max( MaxThresholdFactor, 1.0f - WormValue / P.WormThreshold); } } if (!(MaxThresholdFactor > 0.0f)) { return 0.0f; } float MaxNetworkMask = 1.0f; if (H.Sdf.Min >= P.WormNetworkRange) { MaxNetworkMask = 0.0f; } else if (H.Sdf.Min > 0.0f) { MaxNetworkMask = 1.0f - SmoothStep01( FMath::Clamp(H.Sdf.Min / P.WormNetworkRange, 0.0f, 1.0f)); } return Strength * MaxThresholdFactor * FMath::Clamp(MaxNetworkMask, 0.0f, 1.0f); } } // NetworkMask is monotone non-increasing in CaveSDF. The source interval's lower // endpoint therefore gives a sound upper bound for the mask across the whole box, // not just the old all-or-nothing test at WormNetworkRange. This matters in deep // solid: a positive SDF can leave only a fraction of the worm amplitude, enough for // the constant-rock margin to survive even when the full WormStrength would not. float MaxNetworkMask = 1.0f; if (H.Sdf.Min >= P.WormNetworkRange) { MaxNetworkMask = 0.0f; } else if (H.Sdf.Min > 0.0f) { MaxNetworkMask = 1.0f - SmoothStep01( FMath::Clamp(H.Sdf.Min / P.WormNetworkRange, 0.0f, 1.0f)); } return Strength * FMath::Clamp(MaxNetworkMask, 0.0f, 1.0f); } /** Le ver ne REMPLIT jamais : `InOut.Density -= …` avec un terme positif. */ float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override { return 0.0f; } /** L'amplitude max de carve, en unités de densité. Borne BRUTE : elle ignore la portée du * réseau, c'est `MaxCarveOverBox` qui l'applique. */ float MaxCarveAmplitude() const { if (!(P.WormStrength > 0.0f && P.WormThreshold > 0.0f)) { return 0.0f; } return VoxelMath::IsFinite(P.WormStrength) ? P.WormStrength : FLT_MAX; } const TCHAR* DebugName() const override { return TEXT("WormFieldSource"); } void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } private: FStrateGenerationParams P; uint32 SeedU; }; #if WITH_EDITOR //============================================================================= // RÔLE 2 — COMBINER : RÉGIONS LATÉRALES //============================================================================= // The creative stacks stay independent. This op owns only their density-level junction; // the parent FVoxelOpStack appends the global structural posts after it. In particular, no // recipe can make the spine, vertical seal, passage carve, or XY edge seal region-local. class FLateralRegionBlendOp final : public IVoxelDensityOp { public: FLateralRegionBlendOp(const FVoxelStrateRegionManifest& InManifest, TArray&& InStacks) : RegionStacks(MoveTemp(InStacks)) { // Keep only partition metadata here. The native vectors/recipes have already been // consumed by the stacks and must not be duplicated in the voxel operator. PartitionManifest.bValid = true; PartitionManifest.Seed = InManifest.Seed; PartitionManifest.StrateIndex = InManifest.StrateIndex; PartitionManifest.RegionCount = InManifest.RegionCount; PartitionManifest.PartitionSeed = InManifest.PartitionSeed; PartitionManifest.LatticeCellSize = InManifest.LatticeCellSize; PartitionManifest.BlendWidth = InManifest.BlendWidth; PartitionManifest.Regions.SetNum(InManifest.RegionCount); } EVoxelOpRole GetRole() const override { return EVoxelOpRole::Combiner; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::None; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } bool IsAdditive() const override { return false; } void PrepareChunk(const FVoxelOpContext& Ctx) override { PartitionCache.PrepareForChunk(PartitionManifest, Ctx.ChunkCoord); bPrepared = true; for (FVoxelOpStack& Stack : RegionStacks) { Stack.PrepareChunk(Ctx); } } void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { if (RegionStacks.Num() == 0) { return; } const FVoxelStrateRegionQuery Query = bPrepared ? PartitionCache.Query(WorldX, WorldY) : VF_QueryStrateRegion(PartitionManifest, WorldX, WorldY); const int32 Primary = FMath::Clamp(Query.PrimaryRegion, 0, RegionStacks.Num() - 1); const float PrimaryDensity = RegionStacks[Primary].EvalInternal( WorldX, WorldY, WorldZ); float Density = PrimaryDensity; if (Query.NeighborWeight > 0.0f && Query.NeighborRegion != INDEX_NONE && RegionStacks.IsValidIndex(Query.NeighborRegion) && Query.NeighborRegion != Primary) { const float NeighborDensity = RegionStacks[Query.NeighborRegion].EvalInternal( WorldX, WorldY, WorldZ); Density = FMath::Lerp(PrimaryDensity, NeighborDensity, Query.NeighborWeight); } InOut.Density = Density; } /** * A box wholly outside the band can use its one creative stack. A box that may touch a * bisector or the band asks every region stack. If any is Mixed, the parent is Mixed. If * all possible stacks agree, their convex density blend has the same sign everywhere, so * the blend band is proved uniform too. This is deliberately stricter than sampling a * center point and is the protection against a cross-region false uniform tile. */ EVoxelTileClass ClassifyBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override { if (RegionStacks.Num() == 0) { return EVoxelTileClass::Mixed; } const FVoxelStrateRegionBoxProof Proof = VF_AnalyzeStrateRegionBox(PartitionManifest, VoxelBox); if (Proof.bProvablySingleRegion) { const FVoxelStrateRegionQuery Query = VF_QueryStrateRegion( PartitionManifest, ((float)VoxelBox.Min.X + (float)VoxelBox.Max.X) * 0.5f, ((float)VoxelBox.Min.Y + (float)VoxelBox.Max.Y) * 0.5f); const int32 Primary = FMath::Clamp( Query.PrimaryRegion, 0, RegionStacks.Num() - 1); return RegionStacks[Primary].ClassifyBox(VoxelBox, Ctx); } EVoxelTileClass CommonVerdict = EVoxelTileClass::Mixed; for (const FVoxelOpStack& Stack : RegionStacks) { const EVoxelTileClass Verdict = Stack.ClassifyBox(VoxelBox, Ctx); if (Verdict == EVoxelTileClass::Mixed) { return EVoxelTileClass::Mixed; } if (CommonVerdict == EVoxelTileClass::Mixed) { CommonVerdict = Verdict; } else if (CommonVerdict != Verdict) { return EVoxelTileClass::Mixed; } } return CommonVerdict; } EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override { return EVoxelOpEffect::Both; } const TCHAR* DebugName() const override { return TEXT("LateralRegionBlendOp"); } private: FVoxelStrateRegionManifest PartitionManifest; TArray RegionStacks; mutable FVoxelStrateRegionPartitionCache PartitionCache; mutable bool bPrepared = false; }; #endif } // ⚠️ FIN DU NAMESPACE ANONYME — TOUT NOUVEL OPÉRATEUR SE MET AU-DESSUS DE CETTE LIGNE. // Même piège que dans VoxelHeightOpStack.cpp : s'ancrer sur une bannière située plus bas // (« FVoxelOpStack », « FABRIQUES ») insère la classe HORS du namespace anonyme, et l'accolade // ajoutée avec elle ne ferme rien → C2059. // END OF THE ANONYMOUS NAMESPACE — new operators go ABOVE this line. //============================================================================= // DIAGNOSTIC — voir la déclaration dans VoxelDensityOpStack.h //============================================================================= VoxelDensityOps::FRoomBoxDiagnostic VoxelDensityOps::GetLastRoomBoxDiagnostic() { const FRoomGraphSource::FBoxState& B = FRoomGraphSource::BoxState(); FRoomBoxDiagnostic D; D.HitRooms = B.HitRooms; D.HitTunnels = B.HitTunnels; D.HitPits = B.HitPits; D.HitChimneys = B.HitChimneys; D.NumRooms = B.NumRooms; D.NumTunnels = B.NumTunnels; D.NumPits = B.NumPits; D.NumChimneys = B.NumChimneys; D.HitRoomsNoWarp = B.HitRoomsNoWarp; D.HitTunnelsNoWarp = B.HitTunnelsNoWarp; D.WarpDilation = B.WarpDilation; D.bMayHaveTunnelCoreAir = B.bMayHaveTunnelCoreAir; D.bMayHaveTunnelSupportFloor = B.bMayHaveTunnelSupportFloor; D.bMayHaveTunnelCoreTail = B.bMayHaveTunnelCoreTail; D.ExactTailQueries = B.ExactTailQueries; D.ExactTailEvaluated = B.ExactTailEvaluated; D.ExactTailCycles = B.ExactTailCycles; D.PropagateCycles = B.PropagateCycles; D.ExactPrimitiveCycles = B.ExactPrimitiveCycles; D.CacheWindowCycles = B.CacheWindowCycles; D.NumRooms = B.NumRooms; D.NumTunnels = B.NumTunnels; D.NumRoomFloorJoins = B.NumRoomFloorJoins; D.NumPits = B.NumPits; D.NumChimneys = B.NumChimneys; return D; } void VoxelDensityOps::ReportWorkerRoomGraphCacheFootprint() { if (!VoxelDensityProfile::IsEnabled()) { return; } uint64 DynamicBytes = 0; uint64 EntryBytes = 0; uint64 LargestEntryBytes = 0; uint64 ValidEntries = 0; VoxelDensityProfile::FCacheMemoryBreakdown Breakdown; auto& RoomGraphCache = GRoomGraphCache.Get(); for (int32 Bank = 0; Bank < RoomGraphCacheBankCount; ++Bank) { for (int32 Index = 0; Index < RoomGraphCacheSlotCount; ++Index) { const FRoomGraphCacheEntry& Entry = RoomGraphCache[Bank][Index]; if (!Entry.bValid) { continue; } ++ValidEntries; Breakdown += Entry.Cache.GetAllocatedSizeBreakdown(); const uint64 EntryDynamicBytes = static_cast(Entry.Cache.GetAllocatedSize()); const uint64 FullEntryBytes = static_cast(sizeof(FRoomGraphCacheEntry)) + EntryDynamicBytes; DynamicBytes += EntryDynamicBytes; EntryBytes += FullEntryBytes; LargestEntryBytes = FMath::Max(LargestEntryBytes, FullEntryBytes); } } Breakdown.SlotStorageBytes = static_cast(sizeof(RoomGraphCache)); VoxelDensityProfile::SetWorkerRoomGraphCacheFootprint( RoomGraphCacheBankCount * RoomGraphCacheSlotCount, ValidEntries, static_cast(sizeof(RoomGraphCache)), DynamicBytes, EntryBytes, LargestEntryBytes, ValidEntries, Breakdown); } void VoxelDensityOps::ReportSharedRoomGraphCacheFootprint() { uint64 ReadyEntries = 0; uint64 BuildingEntries = 0; uint64 LargestEntryBytes = 0; uint64 CacheBytes = 0; { FScopeLock Lock(&GSharedRoomGraphCacheMutex); for (const TSharedPtr& Entry : GSharedRoomGraphCache) { if (Entry->bBuilding) { ++BuildingEntries; } else { ++ReadyEntries; LargestEntryBytes = FMath::Max(LargestEntryBytes, Entry->AllocatedBytes); CacheBytes += Entry->AllocatedBytes; } } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeSharedCacheMemory] entries=%d ready=%llu building=%llu " "resident_bytes=%llu resident_mib=%.2f budget_bytes=%llu " "largest_entry_bytes=%llu"), GSharedRoomGraphCache.Num(), static_cast(ReadyEntries), static_cast(BuildingEntries), static_cast(CacheBytes), static_cast(CacheBytes) / (1024.0 * 1024.0), static_cast(SharedRoomGraphCacheBudgetBytes), static_cast(LargestEntryBytes)); } } void VoxelDensityOps::TrimSharedRoomGraphCache(uint64 KeepManagerLifetimeId) { uint64 BeforeBytes = 0; uint64 AfterBytes = 0; int32 BeforeEntries = 0; int32 AfterEntries = 0; { FScopeLock Lock(&GSharedRoomGraphCacheMutex); BeforeBytes = GSharedRoomGraphCacheResidentBytes; BeforeEntries = GSharedRoomGraphCache.Num(); RemoveStaleSharedRoomGraphCachesLocked(KeepManagerLifetimeId); AfterBytes = GSharedRoomGraphCacheResidentBytes; AfterEntries = GSharedRoomGraphCache.Num(); } if (BeforeBytes != AfterBytes || BeforeEntries != AfterEntries) { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeSharedCacheMemory] trim keep_lifetime=%llu entries=%d->%d " "resident_bytes=%llu->%llu"), static_cast(KeepManagerLifetimeId), BeforeEntries, AfterEntries, static_cast(BeforeBytes), static_cast(AfterBytes)); } } //============================================================================= // FVoxelOpStack //============================================================================= SIZE_T FVoxelOpStack::GetAllocatedSize() const { SIZE_T Bytes = static_cast(Ops.GetAllocatedSize()); for (const FOpEntry& Entry : Ops) { if (Entry.Op.IsValid()) { Bytes += FMemory::GetAllocSize(const_cast(Entry.Op.Get())); } } return Bytes; } bool FVoxelOpStack::TryGetLastTunnelCoreWorldEvaluation( FTunnelCoreWorldEvaluation& OutEvaluation) const { // The last provider wins in the same way the structural tail is ordered. At present the // tunnel source is the only provider; walking backwards keeps this safe if a composed stack // later adds another structural core provider. for (int32 Index = Ops.Num() - 1; Index >= 0; --Index) { if (Ops[Index].Op->TryGetLastTunnelCoreWorldEvaluation(OutEvaluation)) { return true; } } return false; } void FVoxelOpStack::DiagnoseBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx, int32 SampleStep, TArray& OutDiagnostics, EVoxelTileClass* OutVerdict) const { OutDiagnostics.Reset(); OutDiagnostics.SetNum(Ops.Num()); if (OutVerdict != nullptr) { *OutVerdict = EVoxelTileClass::Mixed; } if (!VoxelBox.IsValid || SampleStep <= 0) { return; } FVoxelBoxHypotheses H; for (int32 Index = 0; Index < Ops.Num(); ++Index) { const FOpEntry& Entry = Ops[Index]; FOpBoxDiagnostic& Diagnostic = OutDiagnostics[Index]; Diagnostic.Index = Index; Diagnostic.Name = Entry.Op.IsValid() ? FString(Entry.Op->DebugName()) : TEXT("(null op)"); Diagnostic.bWritesSdf = (Entry.Writes & VoxelOpChannels::Sdf) != 0; if (!Entry.Op.IsValid()) { continue; } // This is intentionally the same order and branch structure as VF_FoldOp. The diagnostic // must explain the production classifier, not an approximation of it. const EVoxelTileClass Forced = Entry.Op->ClassifyBox(VoxelBox, Ctx); Diagnostic.ForcedVerdict = Forced; Diagnostic.bForced = Forced != EVoxelTileClass::Mixed; if (Diagnostic.bForced) { Diagnostic.Effect = EVoxelOpEffect::Identity; Diagnostic.ForcedMargin = Entry.Op->ForcedMarginOverBox(VoxelBox, Ctx); VF_ForceHypotheses(H, Forced, Diagnostic.ForcedMargin); if (Diagnostic.bWritesSdf) { Entry.Op->PropagateSdfOverBox(H.Sdf, VoxelBox, Ctx); Diagnostic.bHasSdfBound = H.Sdf.IsKnown(); if (Diagnostic.bHasSdfBound) { Diagnostic.SdfBoundMin = H.Sdf.Min; Diagnostic.SdfBoundMax = H.Sdf.Max; } } } else { Diagnostic.Effect = Entry.Op->EffectOverBox(VoxelBox, Ctx, H); Diagnostic.MaxCarve = Entry.Op->MaxCarveOverBox(VoxelBox, Ctx, H); Diagnostic.MaxFill = Entry.Op->MaxFillOverBox(VoxelBox, Ctx, H); VF_FoldEffect(H, Diagnostic.Effect, Diagnostic.MaxCarve, Diagnostic.MaxFill); if (Diagnostic.bWritesSdf) { Entry.Op->PropagateSdfOverBox(H.Sdf, VoxelBox, Ctx); Diagnostic.bHasSdfBound = H.Sdf.IsKnown(); if (Diagnostic.bHasSdfBound) { Diagnostic.SdfBoundMin = H.Sdf.Min; Diagnostic.SdfBoundMax = H.Sdf.Max; } } } } if (OutVerdict != nullptr) { *OutVerdict = H.Resolve(); } const int32 MinX = FMath::CeilToInt(static_cast(VoxelBox.Min.X)); const int32 MinY = FMath::CeilToInt(static_cast(VoxelBox.Min.Y)); const int32 MinZ = FMath::CeilToInt(static_cast(VoxelBox.Min.Z)); const int32 MaxX = FMath::FloorToInt(static_cast(VoxelBox.Max.X)); const int32 MaxY = FMath::FloorToInt(static_cast(VoxelBox.Max.Y)); const int32 MaxZ = FMath::FloorToInt(static_cast(VoxelBox.Max.Z)); if (MinX > MaxX || MinY > MaxY || MinZ > MaxZ) { return; } constexpr float BoundEpsilon = 1.0e-4f; for (int32 Z = MinZ; Z <= MaxZ; ) { for (int32 Y = MinY; Y <= MaxY; ) { for (int32 X = MinX; X <= MaxX; ) { FVoxelOpSample Sample; for (int32 Index = 0; Index < Ops.Num(); ++Index) { const FOpEntry& Entry = Ops[Index]; FOpBoxDiagnostic& Diagnostic = OutDiagnostics[Index]; if (!Entry.Op.IsValid()) { continue; } const float DensityBefore = Sample.Density; Entry.Op->Eval(static_cast(X), static_cast(Y), static_cast(Z), Sample); const float DensityDelta = Sample.Density - DensityBefore; if (VoxelMath::IsFinite(DensityDelta)) { Diagnostic.bHasDensityDelta = true; Diagnostic.ActualDensityDeltaMin = FMath::Min(Diagnostic.ActualDensityDeltaMin, DensityDelta); Diagnostic.ActualDensityDeltaMax = FMath::Max(Diagnostic.ActualDensityDeltaMax, DensityDelta); // A forced classifier is a stronger statement than a directional delta // bound. Its density can legitimately replace an arbitrary incoming // value (the constant source is the canonical example), so do not call // that replacement a MaxFill/MaxCarve violation. ForcedMargin records the // proof margin separately for the report. if (!Diagnostic.bForced) { const float CarveEpsilon = BoundEpsilon * FMath::Max(1.0f, FMath::Abs(Diagnostic.MaxCarve)); const float FillEpsilon = BoundEpsilon * FMath::Max(1.0f, FMath::Abs(Diagnostic.MaxFill)); if (VoxelMath::IsFinite(Diagnostic.MaxCarve) && DensityDelta < -Diagnostic.MaxCarve - CarveEpsilon) { Diagnostic.bCarveBoundViolated = true; } if (VoxelMath::IsFinite(Diagnostic.MaxFill) && DensityDelta > Diagnostic.MaxFill + FillEpsilon) { Diagnostic.bFillBoundViolated = true; } } } if (Diagnostic.bWritesSdf && VoxelMath::IsFinite(Sample.Sdf)) { Diagnostic.bHasSdfValue = true; Diagnostic.ActualSdfMin = FMath::Min( Diagnostic.ActualSdfMin, Sample.Sdf); Diagnostic.ActualSdfMax = FMath::Max( Diagnostic.ActualSdfMax, Sample.Sdf); } ++Diagnostic.SampleCount; } if (X > MaxX - SampleStep) { break; } X += SampleStep; } if (Y > MaxY - SampleStep) { break; } Y += SampleStep; } if (Z > MaxZ - SampleStep) { break; } Z += SampleStep; } } bool FVoxelOpStack::ValidateChannelOrder(FString* OutError) const { if (OutError != nullptr) { OutError->Reset(); } auto DescribeMask = [](EVoxelOpChannelMask Mask) -> FString { FString Result; if ((Mask & VoxelOpChannels::Density) != 0) { Result += TEXT("Density"); } if ((Mask & VoxelOpChannels::Sdf) != 0) { if (!Result.IsEmpty()) { Result += TEXT(", "); } Result += TEXT("Sdf"); } return Result.IsEmpty() ? TEXT("none") : Result; }; auto DescribeResourceMask = [](EVoxelOpResourceMask Mask) -> FString { FString Result; if ((Mask & VoxelOpResources::RoomGeometry) != 0) { Result += TEXT("RoomGeometry"); } if ((Mask & VoxelOpResources::ShaftGeometry) != 0) { if (!Result.IsEmpty()) { Result += TEXT(", "); } Result += TEXT("ShaftGeometry"); } if ((Mask & VoxelOpResources::SurfaceColumn) != 0) { if (!Result.IsEmpty()) { Result += TEXT(", "); } Result += TEXT("SurfaceColumn"); } return Result.IsEmpty() ? TEXT("none") : Result; }; auto Fail = [&](int32 Index, const FOpEntry* Entry, const TCHAR* Rule) -> bool { if (OutError != nullptr) { const TCHAR* Name = (Entry != nullptr && Entry->Op.Get() != nullptr) ? Entry->Op->DebugName() : TEXT("(null op)"); const EVoxelOpChannelMask Reads = Entry != nullptr ? Entry->Reads : VoxelOpChannels::None; const EVoxelOpChannelMask Writes = Entry != nullptr ? Entry->Writes : VoxelOpChannels::None; *OutError = FString::Printf( TEXT("op %d (%s) violates stack DAG: %s; reads=[%s], writes=[%s], requires=[%s], provides=[%s]"), Index, Name, Rule, *DescribeMask(Reads), *DescribeMask(Writes), *DescribeResourceMask(Entry != nullptr ? Entry->RequiredResources : VoxelOpResources::None), *DescribeResourceMask(Entry != nullptr ? Entry->ProvidedResources : VoxelOpResources::None)); } return false; }; // The two array slots are deliberately explicit: FVoxelOpSample has exactly two fields, and // adding a third field requires extending EVoxelOpChannel and this validator together. int32 LastWriter[2] = { INDEX_NONE, INDEX_NONE }; EVoxelOpResourceMask AvailableResources = VoxelOpResources::None; auto ChannelIndex = [](EVoxelOpChannelMask Channel) -> int32 { return Channel == VoxelOpChannels::Density ? 0 : 1; }; for (int32 Index = 0; Index < Ops.Num(); ++Index) { const FOpEntry& Entry = Ops[Index]; if (Entry.Op.Get() == nullptr) { return Fail(Index, &Entry, TEXT("null operator")); } if ((Entry.RequiredResources & VoxelOpResources::All) != Entry.RequiredResources) { return Fail(Index, &Entry, TEXT("requires an unknown op resource")); } if ((Entry.ProvidedResources & VoxelOpResources::All) != Entry.ProvidedResources) { return Fail(Index, &Entry, TEXT("provides an unknown op resource")); } if ((Entry.RequiredResources & static_cast(~AvailableResources)) != 0) { return Fail(Index, &Entry, TEXT("requires op state before an earlier provider published it")); } if ((Entry.Reads & VoxelOpChannels::All) != Entry.Reads) { return Fail(Index, &Entry, TEXT("reads an unknown sample channel")); } if ((Entry.Writes & VoxelOpChannels::All) != Entry.Writes) { return Fail(Index, &Entry, TEXT("writes an unknown sample channel")); } if (Entry.bAdditive && Entry.Writes == VoxelOpChannels::None) { return Fail(Index, &Entry, TEXT("an additive operator must publish a channel")); } if (Entry.bAdditive && (Entry.Writes & static_cast(~Entry.Reads)) != 0) { return Fail(Index, &Entry, TEXT("an additive operator must read every channel it writes")); } const EVoxelOpChannelMask Channels[] = { VoxelOpChannels::Density, VoxelOpChannels::Sdf }; for (const EVoxelOpChannelMask Channel : Channels) { if ((Entry.Reads & Channel) == 0) { continue; } const int32 Writer = LastWriter[ChannelIndex(Channel)]; const bool bRootIdentityFold = Entry.Op->GetRole() == EVoxelOpRole::FieldSource && (Entry.Writes & Channel) != 0 && Writer == INDEX_NONE; if (Writer == INDEX_NONE && !bRootIdentityFold) { return Fail(Index, &Entry, TEXT("reads a channel before a producer has published it")); } } for (const EVoxelOpChannelMask Channel : Channels) { if ((Entry.Writes & Channel) == 0 || (Entry.Reads & Channel) != 0) { continue; } // A write-only op is an assignment/replacement. It may establish the first version, // but it may not erase a previously produced channel without declaring a read of it. if (LastWriter[ChannelIndex(Channel)] != INDEX_NONE) { return Fail(Index, &Entry, TEXT("a write-only replacement would clobber an existing channel")); } } for (const EVoxelOpChannelMask Channel : Channels) { if ((Entry.Writes & Channel) != 0) { // Every read above has either consumed this writer's predecessor or the explicit // root identity. Updating the version here makes all later consumer edges forward. LastWriter[ChannelIndex(Channel)] = Index; } } AvailableResources |= Entry.ProvidedResources; } return true; } void FVoxelOpStack::AppendStructuralPost(float StrateTopWorldZ, float StrateBottomWorldZ, float SealThickness, float BaseDensity, float SpineRadius, const UVoxelStrateManager* StrateManager, bool bAppendEdgeSeal) { // ORDRE NON NÉGOCIABLE : la spine creuse l'intérieur (et ne touche JAMAIS les bandes de seal), // le seal vertical re-solidifie ses bandes, les passages (tube + landing + support) percent // les seals, puis la limite XY gagne sur tout ce qui précède. Ainsi, même un passage placé // dans la rampe ou au-delà du rayon ne peut pas ouvrir la coque extérieure. Les éditions joueur // restent le dernier post de GetDensityAt, hors de cette pile, comme avant. Add(MakeUnique(StrateTopWorldZ, StrateBottomWorldZ, SealThickness, BaseDensity, SpineRadius)); Add(MakeUnique(StrateTopWorldZ, StrateBottomWorldZ, SealThickness, BaseDensity)); Add(MakeUnique(StrateManager, BaseDensity, SealThickness, StrateTopWorldZ, StrateBottomWorldZ, SpineRadius)); if (bAppendEdgeSeal) { Add(MakeUnique(BaseDensity)); } } //============================================================================= // FABRIQUES / FACTORIES //============================================================================= namespace VoxelDensityOps { bool GetStrateOpContract(EVoxelStrateOpClass OpClass, FVoxelStrateOpContract& OutContract) { const FStrateGenerationParams TunnelParams; const FSlabGenerationParams SlabParams; const FMazeGenerationParams MazeParams; const FSurfaceGenerationParams SurfaceParams; const FVerticalShaftParams ShaftParams; const FFloatingIslandParams IslandParams; TUniquePtr Probe; switch (OpClass) { case EVoxelStrateOpClass::ConstantRockSource: Probe = MakeConstantRockSource(TunnelParams.BaseDensity); break; case EVoxelStrateOpClass::ConstantVoidSource: Probe = MakeConstantVoidSource(IslandParams.BaseDensity); break; case EVoxelStrateOpClass::RoomGraphSource: Probe = MakeUnique(TunnelParams, 0, nullptr); break; case EVoxelStrateOpClass::LatticeCorridorSource: Probe = MakeUnique(MazeParams, 0); break; case EVoxelStrateOpClass::ShaftFieldSource: Probe = MakeUnique(ShaftParams, 0, 14.0f); break; case EVoxelStrateOpClass::IslandBlobSource: Probe = MakeUnique(IslandParams, 0); break; case EVoxelStrateOpClass::NoiseRibbonSource: Probe = MakeUnique(MazeParams, 0); break; case EVoxelStrateOpClass::SdfRoughnessMod: Probe = MakeSdfRoughnessMod(MazeParams.SurfaceRoughness, 0.12f, 3, 8.0f); break; case EVoxelStrateOpClass::SdfCarve: Probe = MakeSdfCarve(2.0f, TunnelParams.BaseDensity); break; case EVoxelStrateOpClass::SdfFill: Probe = MakeSdfFill(2.0f, IslandParams.BaseDensity); break; case EVoxelStrateOpClass::GridColumnMod: Probe = MakeUnique(SlabParams, 0); break; case EVoxelStrateOpClass::CaveRoughnessMod: Probe = MakeUnique(TunnelParams, 0); break; case EVoxelStrateOpClass::CaveTerraceMod: Probe = MakeUnique(TunnelParams, 0, nullptr); break; case EVoxelStrateOpClass::LayerLineMod: Probe = MakeUnique(TunnelParams, nullptr); break; case EVoxelStrateOpClass::RibbingMod: Probe = MakeUnique(TunnelParams, nullptr); break; case EVoxelStrateOpClass::CaveOverhangMod: Probe = MakeUnique(TunnelParams, 0, nullptr); break; case EVoxelStrateOpClass::CaveCliffMod: Probe = MakeUnique(TunnelParams, 0, nullptr); break; case EVoxelStrateOpClass::ScallopMod: Probe = MakeUnique(TunnelParams, 0, nullptr); break; case EVoxelStrateOpClass::CaveArchMod: Probe = MakeUnique(TunnelParams, nullptr); break; case EVoxelStrateOpClass::RoomColumnMod: Probe = MakeUnique(TunnelParams, nullptr); break; case EVoxelStrateOpClass::DomeMod: Probe = MakeUnique(TunnelParams, nullptr); break; case EVoxelStrateOpClass::PinchMod: Probe = MakeUnique(TunnelParams, nullptr); break; case EVoxelStrateOpClass::FloorBiasMod: Probe = MakeUnique(TunnelParams, nullptr); break; case EVoxelStrateOpClass::WormFieldSource: Probe = MakeUnique(TunnelParams, 0); break; case EVoxelStrateOpClass::ShaftLedgeMod: Probe = MakeUnique(ShaftParams, nullptr); break; case EVoxelStrateOpClass::DensityNoiseCarveMod: Probe = MakeUnique(1.0f, 0.02f, 3, 0, false); break; case EVoxelStrateOpClass::DensityNoiseFillMod: Probe = MakeUnique(1.0f, 0.02f, 3, 0, true); break; default: return false; } if (!Probe) { return false; } OutContract.Role = Probe->GetRole(); OutContract.Reads = Probe->ChannelReads(); OutContract.Writes = Probe->ChannelWrites(); OutContract.bAdditive = Probe->IsAdditive(); OutContract.RequiredResources = Probe->RequiredResources(); OutContract.ProvidedResources = Probe->ProvidedResources(); return true; } TUniquePtr MakeConstantRockSource(float BaseDensity) { return MakeUnique(BaseDensity); } TUniquePtr MakeConstantVoidSource(float BaseDensity) { // `Density = -BaseDensity` : on part du vide. La négation unaire est exacte. // Start as open air; unary negation is exact. return MakeUnique(-BaseDensity); } TUniquePtr MakeLatticeCorridorSource(const FMazeGenerationParams& P, int32 Seed) { return MakeUnique(P, Seed); } TUniquePtr MakeSdfRoughnessMod(float Strength, float Frequency, int32 BaseOctaves, float ApplyWithin) { return MakeUnique(Strength, Frequency, BaseOctaves, ApplyWithin); } TUniquePtr MakeSdfCarve(float Blend, float BaseDensity, float MinDivisor) { return MakeUnique(Blend, BaseDensity, -1.0f, MinDivisor); } TUniquePtr MakeSdfFill(float Blend, float BaseDensity) { return MakeUnique(Blend, BaseDensity, +1.0f, 0.0f); } TUniquePtr MakeSlabVoidSource(const FSlabGenerationParams& P, int32 Seed) { return MakeUnique(P, Seed); } TUniquePtr MakeGridColumnMod(const FSlabGenerationParams& P, int32 Seed) { return MakeUnique(P, Seed); } void BuildSurfaceStack(FVoxelOpStack& OutStack, const FSurfaceGenerationParams& P, int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager, const TArray& PerBiomeParams, TUniquePtr BiomeField, bool bAppendStructuralPosts) { // ARCHÉTYPE COMPLET : vide + overhang + mélange de biomes. // `PerBiomeParams` vide ⇒ chemin sans biomes, strictement inchangé. TUniquePtr ColumnSource = MakeUnique(P, Seed, PerBiomeParams, MoveTemp(BiomeField)); const FSurfaceColumnSource* ColumnPtr = ColumnSource.Get(); OutStack.Add(MoveTemp(ColumnSource)); // L'overhang lit la colonne de la source (mémo partagé, même XY par construction). Même // motif que cliff → structural : un modificateur qui a besoin de ce que la source a produit. OutStack.Add(MakeUnique(P, Seed, ColumnPtr)); if (bAppendStructuralPosts) { OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ, P.BoundarySealThickness, P.BaseDensity, SpineRadius, StrateManager); } } void BuildSlabStack(FVoxelOpStack& OutStack, const FSlabGenerationParams& P, int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager, bool bAppendStructuralPosts) { // DEUX archétypes entrent ici, aucun branchement ne les distingue : FlatPlain et // CrystalChamber ne diffèrent que par leurs valeurs par défaut. OutStack.Add(MakeSlabVoidSource(P, Seed)); OutStack.Add(MakeGridColumnMod(P, Seed)); if (bAppendStructuralPosts) { OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ, P.BoundarySealThickness, P.BaseDensity, SpineRadius, StrateManager); } } void BuildVerticalShaftStack(FVoxelOpStack& OutStack, const FVerticalShaftParams& P, int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager, bool bAppendStructuralPosts) { // TROIS des cinq opérateurs ci-dessous sont ceux de Maze, repris tels quels — // `ConstantRock`, `SdfRoughness`, `SdfCarve` — avec une source différente (`§2.5`). // // THREE of the five ops below are Maze's, reused unchanged, with a different source. constexpr float CarveBlend = 2.0f; TUniquePtr ShaftSource = MakeUnique(P, Seed, SpineRadius); const FShaftFieldSource* ShaftPtr = ShaftSource.Get(); OutStack.Add(MakeConstantRockSource(P.BaseDensity)); OutStack.Add(MoveTemp(ShaftSource)); // Fréquence 0.1 et fenêtre `SurfaceRoughness + 4` — PAS celles de Maze // (0.12 / `R + rough + 2`). Même opérateur, réglages différents. OutStack.Add(MakeSdfRoughnessMod(P.SurfaceRoughness, 0.1f, 3, P.SurfaceRoughness + 4.0f)); OutStack.Add(MakeSdfCarve(CarveBlend, P.BaseDensity)); OutStack.Add(MakeUnique(P, ShaftPtr)); OutStack.Add(MakeUnique(ShaftPtr, P.BaseDensity)); OutStack.Add(MakeUnique(ShaftPtr, P.BaseDensity)); if (bAppendStructuralPosts) { OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ, P.BoundarySealThickness, P.BaseDensity, SpineRadius, StrateManager); } } void BuildTunnelNetworkStack(FVoxelOpStack& OutStack, const FStrateGenerationParams& P, int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager, bool bAppendStructuralPosts) { // ⚠️ ÉTAPES A + B + C1 — LA PILE EST COMPLÈTE POUR CET ARCHÉTYPE. // Portés : échelle verticale, roc de base, warp, graphe de salles (+ pits + cheminées), // carve, LES DOUZE MODIFICATEURS DE DÉTAIL (4b–4h), l'override d'op PAR SALLE, les vers, // le post structurel. // // L'override (C1) n'ajoute AUCUN opérateur : il change ce que ONZE d'entre eux LISENT. // `FRoomGraphSource::LocalParams()` publie les params de la strate avec l'op de la salle la // plus proche appliqué ; les onze modificateurs concernés y lisent leurs champs au lieu des // leurs. La rugosité (4b) NON — dans l'original elle précède la déclaration du shadow. // //--------------------------------------------------------------------- // ⚠️ CE PORTAGE RETIRE L'IDÉE DE « FRAME OPS » (OPSTACK-DECOMPOSITION §1) //--------------------------------------------------------------------- // `§2` décrivait deux frames imbriqués : `VerticalScale` et `CaveWarp`. En les portant pour // de vrai, les deux se sont dissous : // • `CaveWarp` a une portée d'EXACTEMENT UN opérateur (le graphe de salles — pits et // cheminées lisent explicitement les coordonnées non warpées). Une transformation qui // n'enveloppe qu'un opérateur n'est pas un frame, c'est une variable locale. // • `VerticalScale` est `Z / Scale` : une fonction PURE d'un scalaire et d'un param, que // chaque opérateur qui en a besoin recalcule en une ligne. Un frame ne ferait // qu'ajouter un canal pour éviter une division. // Il restait le warp d'îles (§7), déjà gardé local pour la même raison. **Zéro frame sur // trois candidats** : ce n'était pas une infrastructure manquante, c'était trois fois la // même chose vue de loin. constexpr float CarveMinDivisor = 1.0f; // TunnelNetwork plancher son diviseur, cf. FSdfConvertOp // La source de salles est retenue par pointeur non possédant : les terrasses re-interrogent // son cache SDF en Z±1. Même motif que `FShaftFieldSource` → `FShaftLedgeMod`. TUniquePtr RoomSource = MakeUnique( P, Seed, StrateManager); const FRoomGraphSource* RoomPtr = RoomSource.Get(); OutStack.Add(MakeConstantRockSource(P.BaseDensity)); OutStack.Add(MoveTemp(RoomSource)); OutStack.Add(MakeSdfCarve(P.SDFBlendRadius, P.BaseDensity, CarveMinDivisor)); // ── ÉTAPE B : les modificateurs de détail (4b–4h), chacun gated sur // `Sdf < SDFBlendRadius·3` via VF_NearCaveSurface. Voir la note de l'étape B5 là-bas. // L'ORDRE EST CELUI DE L'ORIGINAL et il compte : chacun lit la densité que le précédent // a laissée (le biais de sol est un ajout indépendant appliqué à cette accumulation). if (!VoxelDensityAblation::IsDetailOpsOff()) { OutStack.Add(MakeUnique(P, Seed)); // 4b OutStack.Add(MakeUnique(P, Seed, RoomPtr)); // 4c — terrasses OutStack.Add(MakeUnique(P, RoomPtr)); // 4c — lignes de strates OutStack.Add(MakeUnique(P, RoomPtr)); // 4c — nervures OutStack.Add(MakeUnique(P, Seed, RoomPtr)); // 4c — surplombs OutStack.Add(MakeUnique(P, Seed, RoomPtr)); // 4c — falaise OutStack.Add(MakeUnique(P, Seed, RoomPtr)); // 4c — festons OutStack.Add(MakeUnique(P, RoomPtr)); // 4c — arches OutStack.Add(MakeUnique(P, RoomPtr)); // 4d — colonnes (pré-cuites) OutStack.Add(MakeUnique(P, RoomPtr)); // 4g — dômes OutStack.Add(MakeUnique(P, RoomPtr)); // 4h — pincement OutStack.Add(MakeUnique(P, RoomPtr)); // fin 4h — biais de sol } if (P.WormStrength > 0.0f && P.WormThreshold > 0.0f) { OutStack.Add(MakeUnique(P, Seed)); } if (bAppendStructuralPosts) { OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ, P.BoundarySealThickness, P.BaseDensity, SpineRadius, StrateManager, false); // The generator's shared MC post evaluates the prepared tunnel-core cache after // disturbances. Keeping these pre-disturbance graph scans here duplicated both the // support and air queries for every voxel; the feature remains in that shared post. OutStack.Add(MakeUnique(P.BaseDensity)); // Lower only the canonical native graph. The old scalar TunnelNetwork evaluator is // the fused form of exactly these authored stages: it keeps density/SDF/room state in // locals and gates the complete 4b-4h detail suite per sample. Marking the plan after // the final op is important: recipes and composer stacks keep the interpreted/block // fallback, even when they happen to contain a similar-looking subset of operators. OutStack.SetFusedEvaluator(EVoxelOpFusedEvaluator::TunnelNetwork); } } void BuildFloatingIslandStack(FVoxelOpStack& OutStack, const FFloatingIslandParams& P, int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager, bool bAppendStructuralPosts) { // ⚠️ LA PILE QUI S'INVERSE, et c'est la mesure que ce portage-ci ajoute : les quatre autres // archétypes partent de ROC et CREUSENT ; celui-ci part du VIDE et REMPLIT. Aucune des deux // extrémités n'a demandé un opérateur neuf — la source constante et la conversion SDF→densité // sont les MÊMES classes, au signe près (`FConstantFieldSource`, `FSdfConvertOp`). Un // opérateur qui se réutilise en s'inversant est une preuve plus forte qu'un opérateur qui se // réutilise à l'identique : ça veut dire que l'axe abstrait (le signe de la densité) est le // bon, pas seulement que deux archétypes se ressemblaient. // // The stack that runs BACKWARDS: four archetypes start from rock and carve, this one starts // from void and fills — and neither end needed a new operator, only the opposite sign. const float BlendK = FMath::Max(P.SDFBlendRadius, 0.01f); OutStack.Add(MakeConstantVoidSource(P.BaseDensity)); OutStack.Add(MakeUnique(P, Seed)); // Fréquence 0.08 et 4 octaves — les constantes de FloatingIslands (Maze 0.12/3, // VerticalShafts 0.1/3). OutStack.Add(MakeSdfRoughnessMod(P.SurfaceRoughness, 0.08f, 4, P.SurfaceRoughness + BlendK + 2.0f)); OutStack.Add(MakeSdfFill(BlendK, P.BaseDensity)); if (bAppendStructuralPosts) { OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ, P.BoundarySealThickness, P.BaseDensity, SpineRadius, StrateManager); } } void BuildMazeStack(FVoxelOpStack& OutStack, const FMazeGenerationParams& P, int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager, bool bAppendStructuralPosts) { // Constantes Maze, codées en dur. / Maze's hard-coded constants. constexpr float CarveBlend = 2.0f; constexpr float RoughFrequency = 0.12f; constexpr int32 RoughOctaves = 3; const float R = FMath::Max(P.CorridorRadius, 0.5f); // Fenêtre d'application de la rugosité : `MazeSDF < R + SurfaceRoughness + 2.0f`. const float RoughApplyWithin = R + P.SurfaceRoughness + 2.0f; OutStack.Add(MakeConstantRockSource(P.BaseDensity)); OutStack.Add(MakeLatticeCorridorSource(P, Seed)); OutStack.Add(MakeSdfRoughnessMod(P.SurfaceRoughness, RoughFrequency, RoughOctaves, RoughApplyWithin)); OutStack.Add(MakeSdfCarve(CarveBlend, P.BaseDensity)); if (bAppendStructuralPosts) { OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ, P.BoundarySealThickness, P.BaseDensity, SpineRadius, StrateManager); } } } namespace VoxelStrateRecipePrivate { const FStrateGenerationParams* Tunnel(const FVoxelStrateArchetypeParams& Params) { return &Params.TunnelNetworkParams; } const FSlabGenerationParams* Slab(const FVoxelStrateArchetypeParams& Params) { return &Params.SlabParams; } const FMazeGenerationParams* Maze(const FVoxelStrateArchetypeParams& Params) { return &Params.MazeParams; } const FSurfaceGenerationParams* Surface(const FVoxelStrateArchetypeParams& Params) { return &Params.SurfaceParams; } const FVerticalShaftParams* Shaft(const FVoxelStrateArchetypeParams& Params) { return &Params.VerticalShaftParams; } const FFloatingIslandParams* Island(const FVoxelStrateArchetypeParams& Params) { return &Params.FloatingIslandParams; } float BaseDensity(const FVoxelStrateArchetypeParams& Params, EVoxelStrateParamBlock Block) { switch (Block) { case EVoxelStrateParamBlock::TunnelNetwork: return Tunnel(Params)->BaseDensity; case EVoxelStrateParamBlock::Slab: return Slab(Params)->BaseDensity; case EVoxelStrateParamBlock::Maze: return Maze(Params)->BaseDensity; case EVoxelStrateParamBlock::Surface: return Surface(Params)->BaseDensity; case EVoxelStrateParamBlock::VerticalShaft: return Shaft(Params)->BaseDensity; case EVoxelStrateParamBlock::FloatingIsland: return Island(Params)->BaseDensity; default: return 8.0f; } } float BoundarySeal(const FVoxelStrateArchetypeParams& Params, EVoxelStrateParamBlock Block) { switch (Block) { case EVoxelStrateParamBlock::TunnelNetwork: return Tunnel(Params)->BoundarySealThickness; case EVoxelStrateParamBlock::Slab: return Slab(Params)->BoundarySealThickness; case EVoxelStrateParamBlock::Maze: return Maze(Params)->BoundarySealThickness; case EVoxelStrateParamBlock::Surface: return Surface(Params)->BoundarySealThickness; case EVoxelStrateParamBlock::VerticalShaft: return Shaft(Params)->BoundarySealThickness; case EVoxelStrateParamBlock::FloatingIsland: return Island(Params)->BoundarySealThickness; default: return 4.0f; } } float Top(const FVoxelStrateArchetypeParams& Params, EVoxelStrateParamBlock Block) { switch (Block) { case EVoxelStrateParamBlock::TunnelNetwork: return Tunnel(Params)->StrateTopWorldZ; case EVoxelStrateParamBlock::Slab: return Slab(Params)->StrateTopWorldZ; case EVoxelStrateParamBlock::Maze: return Maze(Params)->StrateTopWorldZ; case EVoxelStrateParamBlock::Surface: return Surface(Params)->StrateTopWorldZ; case EVoxelStrateParamBlock::VerticalShaft: return Shaft(Params)->StrateTopWorldZ; case EVoxelStrateParamBlock::FloatingIsland: return Island(Params)->StrateTopWorldZ; default: return 0.0f; } } float Bottom(const FVoxelStrateArchetypeParams& Params, EVoxelStrateParamBlock Block) { switch (Block) { case EVoxelStrateParamBlock::TunnelNetwork: return Tunnel(Params)->StrateBottomWorldZ; case EVoxelStrateParamBlock::Slab: return Slab(Params)->StrateBottomWorldZ; case EVoxelStrateParamBlock::Maze: return Maze(Params)->StrateBottomWorldZ; case EVoxelStrateParamBlock::Surface: return Surface(Params)->StrateBottomWorldZ; case EVoxelStrateParamBlock::VerticalShaft: return Shaft(Params)->StrateBottomWorldZ; case EVoxelStrateParamBlock::FloatingIsland: return Island(Params)->StrateBottomWorldZ; default: return 0.0f; } } EVoxelStrateParamBlock ShapeBlock(EVoxelStrateOpClass OpClass) { switch (OpClass) { case EVoxelStrateOpClass::RoomGraphSource: return EVoxelStrateParamBlock::TunnelNetwork; case EVoxelStrateOpClass::LatticeCorridorSource: case EVoxelStrateOpClass::NoiseRibbonSource: return EVoxelStrateParamBlock::Maze; case EVoxelStrateOpClass::ShaftFieldSource: return EVoxelStrateParamBlock::VerticalShaft; case EVoxelStrateOpClass::IslandBlobSource: return EVoxelStrateParamBlock::FloatingIsland; default: return EVoxelStrateParamBlock::None; } } EVoxelStrateParamBlock FixedParamBlock(EVoxelStrateOpClass OpClass) { switch (OpClass) { case EVoxelStrateOpClass::GridColumnMod: return EVoxelStrateParamBlock::Slab; case EVoxelStrateOpClass::CaveRoughnessMod: case EVoxelStrateOpClass::CaveTerraceMod: case EVoxelStrateOpClass::LayerLineMod: case EVoxelStrateOpClass::RibbingMod: case EVoxelStrateOpClass::CaveOverhangMod: case EVoxelStrateOpClass::CaveCliffMod: case EVoxelStrateOpClass::ScallopMod: case EVoxelStrateOpClass::CaveArchMod: case EVoxelStrateOpClass::RoomColumnMod: case EVoxelStrateOpClass::DomeMod: case EVoxelStrateOpClass::PinchMod: case EVoxelStrateOpClass::FloorBiasMod: case EVoxelStrateOpClass::WormFieldSource: return EVoxelStrateParamBlock::TunnelNetwork; case EVoxelStrateOpClass::ShaftLedgeMod: return EVoxelStrateParamBlock::VerticalShaft; default: return EVoxelStrateParamBlock::None; } } float SdfBlend(const FVoxelStrateArchetypeParams& Params, EVoxelStrateParamBlock Block) { switch (Block) { case EVoxelStrateParamBlock::TunnelNetwork: return Params.TunnelNetworkParams.SDFBlendRadius; case EVoxelStrateParamBlock::Maze: return 2.0f; case EVoxelStrateParamBlock::VerticalShaft: return 2.0f; case EVoxelStrateParamBlock::FloatingIsland: return FMath::Max(Params.FloatingIslandParams.SDFBlendRadius, 0.01f); default: return 2.0f; } } float SdfRoughnessStrength(const FVoxelStrateArchetypeParams& Params, EVoxelStrateParamBlock Block) { switch (Block) { case EVoxelStrateParamBlock::TunnelNetwork: return Params.TunnelNetworkParams.SurfaceRoughness; case EVoxelStrateParamBlock::Maze: return Params.MazeParams.SurfaceRoughness; case EVoxelStrateParamBlock::VerticalShaft: return Params.VerticalShaftParams.SurfaceRoughness; case EVoxelStrateParamBlock::FloatingIsland: return Params.FloatingIslandParams.SurfaceRoughness; default: return 2.0f; } } float SdfRoughnessFrequency(const FVoxelStrateArchetypeParams& Params, EVoxelStrateParamBlock Block) { switch (Block) { case EVoxelStrateParamBlock::TunnelNetwork: return Params.TunnelNetworkParams.RoughnessFrequency; case EVoxelStrateParamBlock::Maze: return 0.12f; case EVoxelStrateParamBlock::VerticalShaft: return 0.1f; case EVoxelStrateParamBlock::FloatingIsland: return 0.08f; default: return 0.1f; } } int32 SdfRoughnessOctaves(EVoxelStrateParamBlock Block) { return Block == EVoxelStrateParamBlock::FloatingIsland ? 4 : 3; } float SdfRoughnessWindow(const FVoxelStrateArchetypeParams& Params, EVoxelStrateParamBlock Block) { const float Strength = SdfRoughnessStrength(Params, Block); switch (Block) { case EVoxelStrateParamBlock::TunnelNetwork: return Params.TunnelNetworkParams.SDFBlendRadius * 3.0f + Strength + 2.0f; case EVoxelStrateParamBlock::Maze: return FMath::Max(Params.MazeParams.CorridorRadius, 0.5f) + Strength + 2.0f; case EVoxelStrateParamBlock::VerticalShaft: return Strength + 4.0f; case EVoxelStrateParamBlock::FloatingIsland: return Strength + SdfBlend(Params, Block) + 2.0f; default: return Strength + 4.0f; } } float GenericStrength(const FVoxelStrateArchetypeParams& Params, EVoxelStrateParamBlock Block) { return FMath::Max(0.25f, 0.35f * FMath::Abs(BaseDensity(Params, Block))); } float GenericFrequency(const FVoxelStrateArchetypeParams& Params, EVoxelStrateParamBlock Block) { switch (Block) { case EVoxelStrateParamBlock::Maze: return 1.0f / FMath::Max(Params.MazeParams.CellSize, 8.0f); case EVoxelStrateParamBlock::VerticalShaft: return 0.01f; case EVoxelStrateParamBlock::FloatingIsland: return 0.02f; case EVoxelStrateParamBlock::TunnelNetwork: return FMath::Max(Params.TunnelNetworkParams.RoughnessFrequency, 0.001f); default: return 0.02f; } } bool IsShapeSource(EVoxelStrateOpClass OpClass) { switch (OpClass) { case EVoxelStrateOpClass::RoomGraphSource: case EVoxelStrateOpClass::LatticeCorridorSource: case EVoxelStrateOpClass::ShaftFieldSource: case EVoxelStrateOpClass::IslandBlobSource: case EVoxelStrateOpClass::NoiseRibbonSource: return true; default: return false; } } bool ValidateRecipeContracts(const FVoxelOpStackRecipe& Recipe, FString& OutError) { TArray Classes; Classes.Reserve(3 + Recipe.Modifiers.Num()); Classes.Add(Recipe.Root.OpClass); Classes.Add(Recipe.ShapeSource.OpClass); Classes.Add(Recipe.Conversion.OpClass); for (const FVoxelOpRecipeEntry& Entry : Recipe.Modifiers) { Classes.Add(Entry.OpClass); } auto ValidateParamBlock = [&](const FVoxelOpRecipeEntry& Entry, int32 EntryIndex) { if (Entry.ParamBlock == EVoxelStrateParamBlock::None) { OutError = FString::Printf(TEXT("recipe op %d has no parameter block"), EntryIndex); return false; } const EVoxelStrateParamBlock Fixed = FixedParamBlock(Entry.OpClass); if (Fixed != EVoxelStrateParamBlock::None && Entry.ParamBlock != Fixed) { OutError = FString::Printf(TEXT("recipe op %d uses the wrong parameter block"), EntryIndex); return false; } return true; }; if (!ValidateParamBlock(Recipe.Root, 0) || !ValidateParamBlock(Recipe.ShapeSource, 1) || !ValidateParamBlock(Recipe.Conversion, 2)) { return false; } for (int32 ModifierIndex = 0; ModifierIndex < Recipe.Modifiers.Num(); ++ModifierIndex) { if (!ValidateParamBlock(Recipe.Modifiers[ModifierIndex], 3 + ModifierIndex)) { return false; } } int32 LastWriter[2] = { INDEX_NONE, INDEX_NONE }; EVoxelOpResourceMask AvailableResources = VoxelOpResources::None; const EVoxelOpChannelMask Channels[] = { VoxelOpChannels::Density, VoxelOpChannels::Sdf }; auto ChannelIndex = [](EVoxelOpChannelMask Channel) { return Channel == VoxelOpChannels::Density ? 0 : 1; }; for (int32 Index = 0; Index < Classes.Num(); ++Index) { FVoxelStrateOpContract Contract; if (!VoxelDensityOps::GetStrateOpContract(Classes[Index], Contract)) { OutError = FString::Printf(TEXT("recipe op %d has no declaration"), Index); return false; } if ((Contract.Reads & VoxelOpChannels::All) != Contract.Reads || (Contract.Writes & VoxelOpChannels::All) != Contract.Writes || (Contract.RequiredResources & VoxelOpResources::All) != Contract.RequiredResources || (Contract.ProvidedResources & VoxelOpResources::All) != Contract.ProvidedResources) { OutError = FString::Printf(TEXT("recipe op %d has an unknown declaration bit"), Index); return false; } if ((Contract.RequiredResources & static_cast(~AvailableResources)) != 0) { OutError = FString::Printf(TEXT("recipe op %d requires unavailable state"), Index); return false; } if (Contract.bAdditive && Contract.Writes == VoxelOpChannels::None) { OutError = FString::Printf(TEXT("recipe op %d is additive but writes no channel"), Index); return false; } if (Contract.bAdditive && (Contract.Writes & static_cast(~Contract.Reads)) != 0) { OutError = FString::Printf(TEXT("recipe op %d is additive without reading its writes"), Index); return false; } for (const EVoxelOpChannelMask Channel : Channels) { if ((Contract.Reads & Channel) == 0) { continue; } const int32 CI = ChannelIndex(Channel); const bool bRootIdentity = Contract.Role == EVoxelOpRole::FieldSource && (Contract.Writes & Channel) != 0 && LastWriter[CI] == INDEX_NONE; if (LastWriter[CI] == INDEX_NONE && !bRootIdentity) { OutError = FString::Printf(TEXT("recipe op %d reads an unpublished channel"), Index); return false; } } for (const EVoxelOpChannelMask Channel : Channels) { if ((Contract.Writes & Channel) == 0 || (Contract.Reads & Channel) != 0) { continue; } if (LastWriter[ChannelIndex(Channel)] != INDEX_NONE) { OutError = FString::Printf(TEXT("recipe op %d clobbers a channel"), Index); return false; } } for (const EVoxelOpChannelMask Channel : Channels) { if ((Contract.Writes & Channel) != 0) { LastWriter[ChannelIndex(Channel)] = Index; } } AvailableResources |= Contract.ProvidedResources; } return true; } TUniquePtr BuildRecipeOp(const FVoxelOpRecipeEntry& Entry, const FVoxelStrateArchetypeParams& Params, int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager, const FRoomGraphSource*& OutRoom, const FShaftFieldSource*& OutShaft) { const EVoxelStrateParamBlock Block = Entry.ParamBlock; switch (Entry.OpClass) { case EVoxelStrateOpClass::ConstantRockSource: return VoxelDensityOps::MakeConstantRockSource(BaseDensity(Params, Block)); case EVoxelStrateOpClass::ConstantVoidSource: return VoxelDensityOps::MakeConstantVoidSource(BaseDensity(Params, Block)); case EVoxelStrateOpClass::RoomGraphSource: { TUniquePtr Op = MakeUnique(*Tunnel(Params), Seed, StrateManager); OutRoom = Op.Get(); return Op; } case EVoxelStrateOpClass::LatticeCorridorSource: return MakeUnique(*Maze(Params), Seed); case EVoxelStrateOpClass::ShaftFieldSource: { TUniquePtr Op = MakeUnique(*Shaft(Params), Seed, SpineRadius); OutShaft = Op.Get(); return Op; } case EVoxelStrateOpClass::IslandBlobSource: return MakeUnique(*Island(Params), Seed); case EVoxelStrateOpClass::NoiseRibbonSource: return MakeUnique(*Maze(Params), Seed); case EVoxelStrateOpClass::SdfRoughnessMod: return VoxelDensityOps::MakeSdfRoughnessMod( SdfRoughnessStrength(Params, Block), SdfRoughnessFrequency(Params, Block), SdfRoughnessOctaves(Block), SdfRoughnessWindow(Params, Block)); case EVoxelStrateOpClass::SdfCarve: return VoxelDensityOps::MakeSdfCarve(SdfBlend(Params, Block), BaseDensity(Params, Block)); case EVoxelStrateOpClass::SdfFill: return VoxelDensityOps::MakeSdfFill(SdfBlend(Params, Block), BaseDensity(Params, Block)); case EVoxelStrateOpClass::GridColumnMod: return MakeUnique(*Slab(Params), Seed); case EVoxelStrateOpClass::CaveRoughnessMod: return MakeUnique(*Tunnel(Params), Seed); case EVoxelStrateOpClass::CaveTerraceMod: return MakeUnique(*Tunnel(Params), Seed, OutRoom); case EVoxelStrateOpClass::LayerLineMod: return MakeUnique(*Tunnel(Params), OutRoom); case EVoxelStrateOpClass::RibbingMod: return MakeUnique(*Tunnel(Params), OutRoom); case EVoxelStrateOpClass::CaveOverhangMod: return MakeUnique(*Tunnel(Params), Seed, OutRoom); case EVoxelStrateOpClass::CaveCliffMod: return MakeUnique(*Tunnel(Params), Seed, OutRoom); case EVoxelStrateOpClass::ScallopMod: return MakeUnique(*Tunnel(Params), Seed, OutRoom); case EVoxelStrateOpClass::CaveArchMod: return MakeUnique(*Tunnel(Params), OutRoom); case EVoxelStrateOpClass::RoomColumnMod: return MakeUnique(*Tunnel(Params), OutRoom); case EVoxelStrateOpClass::DomeMod: return MakeUnique(*Tunnel(Params), OutRoom); case EVoxelStrateOpClass::PinchMod: return MakeUnique(*Tunnel(Params), OutRoom); case EVoxelStrateOpClass::FloorBiasMod: return MakeUnique(*Tunnel(Params), OutRoom); case EVoxelStrateOpClass::WormFieldSource: return MakeUnique(*Tunnel(Params), Seed); case EVoxelStrateOpClass::ShaftLedgeMod: return MakeUnique(*Shaft(Params), OutShaft); case EVoxelStrateOpClass::DensityNoiseCarveMod: return MakeUnique(GenericStrength(Params, Block), GenericFrequency(Params, Block), 3, Seed, false); case EVoxelStrateOpClass::DensityNoiseFillMod: return MakeUnique(GenericStrength(Params, Block), GenericFrequency(Params, Block), 3, Seed, true); default: return nullptr; } } } bool VF_BuildStackFromRecipe(const FVoxelOpStackRecipe& Recipe, const FVoxelStrateArchetypeParams& Params, int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager, FVoxelOpStack& OutStack, FVoxelOpContext& OutContext, FString* OutError, bool bAppendStructuralPosts) { auto Fail = [&](const FString& Reason) -> bool { if (OutError != nullptr) { *OutError = Reason; } return false; }; if (OutError != nullptr) { OutError->Reset(); } if (Recipe.Modifiers.Num() < 4 || Recipe.Modifiers.Num() > 8) { return Fail(TEXT("A structure recipe must draw between 4 and 8 modifiers.")); } const EVoxelStrateOpClass ExpectedRoot = Recipe.RootPolarity == EVoxelStrateRootPolarity::VoidFill ? EVoxelStrateOpClass::ConstantVoidSource : EVoxelStrateOpClass::ConstantRockSource; const EVoxelStrateOpClass ExpectedConversion = Recipe.RootPolarity == EVoxelStrateRootPolarity::VoidFill ? EVoxelStrateOpClass::SdfFill : EVoxelStrateOpClass::SdfCarve; if (Recipe.Root.OpClass != ExpectedRoot || Recipe.Conversion.OpClass != ExpectedConversion) { return Fail(TEXT("Recipe polarity does not match its root/conversion ids.")); } if (!VoxelStrateRecipePrivate::IsShapeSource(Recipe.ShapeSource.OpClass)) { return Fail(TEXT("Recipe shape source is not a rollable SDF source.")); } const EVoxelStrateParamBlock ShapeBlock = VoxelStrateRecipePrivate::ShapeBlock(Recipe.ShapeSource.OpClass); if (ShapeBlock == EVoxelStrateParamBlock::None || Recipe.StructuralParamBlock != ShapeBlock || Recipe.Root.ParamBlock == EVoxelStrateParamBlock::None || Recipe.ShapeSource.ParamBlock != ShapeBlock || Recipe.Conversion.ParamBlock != ShapeBlock) { return Fail(TEXT("Recipe parameter blocks do not match its shape source.")); } FString ContractError; if (!VoxelStrateRecipePrivate::ValidateRecipeContracts(Recipe, ContractError)) { return Fail(ContractError); } FVoxelOpStack Candidate; const FRoomGraphSource* RoomPtr = nullptr; const FShaftFieldSource* ShaftPtr = nullptr; FVoxelOpRecipeEntry Root = Recipe.Root; TUniquePtr RootOp = VoxelStrateRecipePrivate::BuildRecipeOp(Root, Params, Seed, SpineRadius, StrateManager, RoomPtr, ShaftPtr); if (!RootOp) { return Fail(TEXT("Recipe root could not be materialised.")); } Candidate.Add(MoveTemp(RootOp)); TUniquePtr ShapeOp = VoxelStrateRecipePrivate::BuildRecipeOp(Recipe.ShapeSource, Params, Seed, SpineRadius, StrateManager, RoomPtr, ShaftPtr); if (!ShapeOp) { return Fail(TEXT("Recipe shape source could not be materialised.")); } Candidate.Add(MoveTemp(ShapeOp)); TUniquePtr ConversionOp = VoxelStrateRecipePrivate::BuildRecipeOp(Recipe.Conversion, Params, Seed, SpineRadius, StrateManager, RoomPtr, ShaftPtr); if (!ConversionOp) { return Fail(TEXT("Recipe conversion could not be materialised.")); } Candidate.Add(MoveTemp(ConversionOp)); for (const FVoxelOpRecipeEntry& Entry : Recipe.Modifiers) { if (Entry.OpClass == EVoxelStrateOpClass::WormFieldSource && !(Params.TunnelNetworkParams.WormStrength > 0.0f && Params.TunnelNetworkParams.WormThreshold > 0.0f)) { // A disabled strate has no worm operator at all. This makes the off switch cheap on // the interpreted path as well as exact in its interval/classifier fold. continue; } TUniquePtr Modifier = VoxelStrateRecipePrivate::BuildRecipeOp(Entry, Params, Seed, SpineRadius, StrateManager, RoomPtr, ShaftPtr); if (!Modifier) { return Fail(TEXT("Recipe modifier could not be materialised.")); } Candidate.Add(MoveTemp(Modifier)); } const float Top = VoxelStrateRecipePrivate::Top(Params, Recipe.StructuralParamBlock); const float Bottom = VoxelStrateRecipePrivate::Bottom(Params, Recipe.StructuralParamBlock); const float Seal = VoxelStrateRecipePrivate::BoundarySeal(Params, Recipe.StructuralParamBlock); const float Base = VoxelStrateRecipePrivate::BaseDensity(Params, Recipe.StructuralParamBlock); if (!VoxelMath::IsFinite(Top) || !VoxelMath::IsFinite(Bottom) || Top <= Bottom || !VoxelMath::IsFinite(Seal) || !VoxelMath::IsFinite(Base)) { return Fail(TEXT("Recipe structural parameters do not define a finite positive strate.")); } OutContext = FVoxelOpContext(); OutContext.Seed = Seed; OutContext.LayoutVersion = StrateManager != nullptr ? StrateManager->GetLayoutVersion() : 0; OutContext.WorldRadiusVoxels = 0.0f; OutContext.EdgeSealThickness = Seal; OutContext.StrateTopWorldZ = Top; OutContext.StrateBottomWorldZ = Bottom; // This is the only place a recipe can acquire world-law operators. There is no recipe field // for posts, so a serialised manifest cannot forget them or reorder them. Lateral callers // materialise this same creative recipe with posts disabled, then append the global post set // to the parent stack below. if (bAppendStructuralPosts) { Candidate.AppendStructuralPost(Top, Bottom, Seal, Base, SpineRadius, StrateManager); } FString ValidationError; if (!Candidate.ValidateChannelOrder(&ValidationError)) { return Fail(FString::Printf(TEXT("Materialised recipe failed ValidateChannelOrder: %s"), *ValidationError)); } OutStack = MoveTemp(Candidate); return true; } #if WITH_EDITOR namespace { static EVoxelStrateParamBlock VF_RegionParamBlock(ECaveGeneratorType Archetype) { switch (Archetype) { case ECaveGeneratorType::FlatPlain: case ECaveGeneratorType::CrystalChamber: return EVoxelStrateParamBlock::Slab; case ECaveGeneratorType::Maze: return EVoxelStrateParamBlock::Maze; case ECaveGeneratorType::SurfaceWorld: return EVoxelStrateParamBlock::Surface; case ECaveGeneratorType::VerticalShafts: return EVoxelStrateParamBlock::VerticalShaft; case ECaveGeneratorType::FloatingIslands:return EVoxelStrateParamBlock::FloatingIsland; case ECaveGeneratorType::Underwater: case ECaveGeneratorType::TunnelNetwork: default: return EVoxelStrateParamBlock::TunnelNetwork; } } static float VF_RegionTop(const FVoxelStrateArchetypeParams& Params, EVoxelStrateParamBlock Block) { switch (Block) { case EVoxelStrateParamBlock::Slab: return Params.SlabParams.StrateTopWorldZ; case EVoxelStrateParamBlock::Maze: return Params.MazeParams.StrateTopWorldZ; case EVoxelStrateParamBlock::Surface: return Params.SurfaceParams.StrateTopWorldZ; case EVoxelStrateParamBlock::VerticalShaft: return Params.VerticalShaftParams.StrateTopWorldZ; case EVoxelStrateParamBlock::FloatingIsland: return Params.FloatingIslandParams.StrateTopWorldZ; case EVoxelStrateParamBlock::TunnelNetwork: default: return Params.TunnelNetworkParams.StrateTopWorldZ; } } static float VF_RegionBottom(const FVoxelStrateArchetypeParams& Params, EVoxelStrateParamBlock Block) { switch (Block) { case EVoxelStrateParamBlock::Slab: return Params.SlabParams.StrateBottomWorldZ; case EVoxelStrateParamBlock::Maze: return Params.MazeParams.StrateBottomWorldZ; case EVoxelStrateParamBlock::Surface: return Params.SurfaceParams.StrateBottomWorldZ; case EVoxelStrateParamBlock::VerticalShaft: return Params.VerticalShaftParams.StrateBottomWorldZ; case EVoxelStrateParamBlock::FloatingIsland: return Params.FloatingIslandParams.StrateBottomWorldZ; case EVoxelStrateParamBlock::TunnelNetwork: default: return Params.TunnelNetworkParams.StrateBottomWorldZ; } } static float VF_RegionSeal(const FVoxelStrateArchetypeParams& Params, EVoxelStrateParamBlock Block) { switch (Block) { case EVoxelStrateParamBlock::Slab: return Params.SlabParams.BoundarySealThickness; case EVoxelStrateParamBlock::Maze: return Params.MazeParams.BoundarySealThickness; case EVoxelStrateParamBlock::Surface: return Params.SurfaceParams.BoundarySealThickness; case EVoxelStrateParamBlock::VerticalShaft: return Params.VerticalShaftParams.BoundarySealThickness; case EVoxelStrateParamBlock::FloatingIsland: return Params.FloatingIslandParams.BoundarySealThickness; case EVoxelStrateParamBlock::TunnelNetwork: default: return Params.TunnelNetworkParams.BoundarySealThickness; } } static float VF_RegionBase(const FVoxelStrateArchetypeParams& Params, EVoxelStrateParamBlock Block) { switch (Block) { case EVoxelStrateParamBlock::Slab: return Params.SlabParams.BaseDensity; case EVoxelStrateParamBlock::Maze: return Params.MazeParams.BaseDensity; case EVoxelStrateParamBlock::Surface: return Params.SurfaceParams.BaseDensity; case EVoxelStrateParamBlock::VerticalShaft: return Params.VerticalShaftParams.BaseDensity; case EVoxelStrateParamBlock::FloatingIsland: return Params.FloatingIslandParams.BaseDensity; case EVoxelStrateParamBlock::TunnelNetwork: default: return Params.TunnelNetworkParams.BaseDensity; } } static bool VF_BuildNativeRegionCore( const FVoxelStrateRegion& Region, const FVoxelStrateArchetypeParams& Params, float SpineRadius, const UVoxelStrateManager* StrateManager, bool bAppendStructuralPosts, FVoxelOpStack& OutStack, FVoxelOpContext& OutContext) { switch (Region.Archetype) { case ECaveGeneratorType::FlatPlain: case ECaveGeneratorType::CrystalChamber: if (Params.SlabParams.StrateTopWorldZ - Params.SlabParams.StrateBottomWorldZ <= 0.0f) { return false; } OutContext.StrateTopWorldZ = Params.SlabParams.StrateTopWorldZ; OutContext.StrateBottomWorldZ = Params.SlabParams.StrateBottomWorldZ; VoxelDensityOps::BuildSlabStack(OutStack, Params.SlabParams, Region.Seed, SpineRadius, StrateManager, bAppendStructuralPosts); return true; case ECaveGeneratorType::Maze: if (Params.MazeParams.StrateTopWorldZ - Params.MazeParams.StrateBottomWorldZ <= 0.0f) { return false; } OutContext.StrateTopWorldZ = Params.MazeParams.StrateTopWorldZ; OutContext.StrateBottomWorldZ = Params.MazeParams.StrateBottomWorldZ; VoxelDensityOps::BuildMazeStack(OutStack, Params.MazeParams, Region.Seed, SpineRadius, StrateManager, bAppendStructuralPosts); return true; case ECaveGeneratorType::SurfaceWorld: if (Params.SurfaceParams.StrateTopWorldZ - Params.SurfaceParams.StrateBottomWorldZ <= 0.0f) { return false; } OutContext.StrateTopWorldZ = Params.SurfaceParams.StrateTopWorldZ; OutContext.StrateBottomWorldZ = Params.SurfaceParams.StrateBottomWorldZ; VoxelDensityOps::BuildSurfaceStack( OutStack, Params.SurfaceParams, Region.Seed, SpineRadius, StrateManager, TArray(), nullptr, bAppendStructuralPosts); return true; case ECaveGeneratorType::VerticalShafts: if (Params.VerticalShaftParams.StrateTopWorldZ - Params.VerticalShaftParams.StrateBottomWorldZ <= 0.0f) { return false; } OutContext.StrateTopWorldZ = Params.VerticalShaftParams.StrateTopWorldZ; OutContext.StrateBottomWorldZ = Params.VerticalShaftParams.StrateBottomWorldZ; VoxelDensityOps::BuildVerticalShaftStack( OutStack, Params.VerticalShaftParams, Region.Seed, SpineRadius, StrateManager, bAppendStructuralPosts); return true; case ECaveGeneratorType::FloatingIslands: if (Params.FloatingIslandParams.StrateTopWorldZ - Params.FloatingIslandParams.StrateBottomWorldZ <= 0.0f) { return false; } OutContext.StrateTopWorldZ = Params.FloatingIslandParams.StrateTopWorldZ; OutContext.StrateBottomWorldZ = Params.FloatingIslandParams.StrateBottomWorldZ; VoxelDensityOps::BuildFloatingIslandStack( OutStack, Params.FloatingIslandParams, Region.Seed, SpineRadius, StrateManager, bAppendStructuralPosts); return true; case ECaveGeneratorType::Underwater: case ECaveGeneratorType::TunnelNetwork: OutContext.StrateTopWorldZ = Params.TunnelNetworkParams.StrateTopWorldZ; OutContext.StrateBottomWorldZ = Params.TunnelNetworkParams.StrateBottomWorldZ; VoxelDensityOps::BuildTunnelNetworkStack( OutStack, Params.TunnelNetworkParams, Region.Seed, SpineRadius, StrateManager, bAppendStructuralPosts); return true; default: return false; } } } bool VF_BuildStrateRegionStack( const FVoxelStrateRegionManifest& Manifest, float SpineRadius, const UVoxelStrateManager* StrateManager, FVoxelOpStack& OutStack, FVoxelOpContext& OutContext, FString* OutError) { auto Fail = [&](const FString& Reason) -> bool { if (OutError != nullptr) { *OutError = Reason; } return false; }; if (OutError != nullptr) { OutError->Reset(); } if (!Manifest.IsValid()) { return Fail(Manifest.FailureReason.IsEmpty() ? TEXT("lateral region manifest is invalid") : Manifest.FailureReason); } if (Manifest.PartitionSeed != VF_GetStrateRegionPartitionSeed( Manifest.Seed, Manifest.StrateIndex)) { return Fail(TEXT("lateral region partition seed is stale for its seed/strate index")); } EVoxelStrateParamBlock GlobalBlock = Manifest.StructuralParamBlock; if (GlobalBlock == EVoxelStrateParamBlock::None) { GlobalBlock = Manifest.Regions[0].bUsesRecipe ? Manifest.Regions[0].Recipe.StructuralParamBlock : VF_RegionParamBlock(Manifest.Regions[0].Archetype); } FVoxelStrateArchetypeParams FirstParams = Manifest.Regions[0].ArchetypeParams; float Top = Manifest.bHasGlobalStructuralParams ? Manifest.StrateTopWorldZ : VF_RegionTop(FirstParams, GlobalBlock); float Bottom = Manifest.bHasGlobalStructuralParams ? Manifest.StrateBottomWorldZ : VF_RegionBottom(FirstParams, GlobalBlock); float Seal = Manifest.bHasGlobalStructuralParams ? Manifest.BoundarySealThickness : VF_RegionSeal(FirstParams, GlobalBlock); float Base = Manifest.bHasGlobalStructuralParams ? Manifest.BaseDensity : VF_RegionBase(FirstParams, GlobalBlock); if (!VoxelMath::IsFinite(Top) || !VoxelMath::IsFinite(Bottom) || !(Top > Bottom) || !VoxelMath::IsFinite(Seal) || Seal < 0.0f || !VoxelMath::IsFinite(Base) || !(Base > 0.0f)) { return Fail(TEXT("lateral region structural parameters are not a finite positive strate")); } auto MakeRegionParams = [&](const FVoxelStrateRegion& Region) { FVoxelStrateArchetypeParams Params = Region.ArchetypeParams; VF_SetStrateArchetypeRuntimeBounds(Params, Top, Bottom); return Params; }; // The one-region form remains a normal stack. It is useful to callers that consume a region // manifest, while production keeps its old branch entirely untouched for the hard identity // gate. if (Manifest.RegionCount == 1) { const FVoxelStrateRegion& Region = Manifest.Regions[0]; const FVoxelStrateArchetypeParams Params = MakeRegionParams(Region); bool bBuilt = false; if (Region.bUsesRecipe) { bBuilt = VF_BuildStackFromRecipe(Region.Recipe, Params, Region.Seed, SpineRadius, StrateManager, OutStack, OutContext, OutError, true); } else { OutContext = FVoxelOpContext(); OutContext.Seed = static_cast(Region.Seed); OutContext.LayoutVersion = StrateManager != nullptr ? StrateManager->GetLayoutVersion() : 0; bBuilt = VF_BuildNativeRegionCore(Region, Params, SpineRadius, StrateManager, true, OutStack, OutContext); if (!bBuilt && OutError != nullptr) { *OutError = TEXT("single lateral region native stack could not be materialised"); } } if (!bBuilt) { return false; } OutContext.WorldRadiusVoxels = 0.0f; OutContext.EdgeSealThickness = Seal; return true; } TArray RegionStacks; RegionStacks.Reserve(Manifest.RegionCount); for (const FVoxelStrateRegion& Region : Manifest.Regions) { const FVoxelStrateArchetypeParams Params = MakeRegionParams(Region); FVoxelOpStack Core; FVoxelOpContext CoreContext; bool bBuilt = false; if (Region.bUsesRecipe) { bBuilt = VF_BuildStackFromRecipe(Region.Recipe, Params, Region.Seed, SpineRadius, StrateManager, Core, CoreContext, OutError, false); } else { CoreContext = FVoxelOpContext(); CoreContext.Seed = static_cast(Region.Seed); CoreContext.LayoutVersion = StrateManager != nullptr ? StrateManager->GetLayoutVersion() : 0; bBuilt = VF_BuildNativeRegionCore(Region, Params, SpineRadius, StrateManager, false, Core, CoreContext); if (!bBuilt && OutError != nullptr) { *OutError = FString::Printf(TEXT("region %d native stack could not be materialised"), Region.RegionIndex); } } if (!bBuilt) { return false; } RegionStacks.Add(MoveTemp(Core)); } FVoxelOpStack Parent; Parent.Add(MakeUnique(Manifest, MoveTemp(RegionStacks))); Parent.AppendStructuralPost(Top, Bottom, Seal, Base, SpineRadius, StrateManager); FString ValidationError; if (!Parent.ValidateChannelOrder(&ValidationError)) { return Fail(FString::Printf(TEXT("lateral region parent failed ValidateChannelOrder: %s"), *ValidationError)); } OutContext = FVoxelOpContext(); OutContext.Seed = static_cast(Manifest.Seed); OutContext.LayoutVersion = StrateManager != nullptr ? StrateManager->GetLayoutVersion() : 0; OutContext.WorldRadiusVoxels = 0.0f; OutContext.EdgeSealThickness = Seal; OutContext.StrateTopWorldZ = Top; OutContext.StrateBottomWorldZ = Bottom; OutStack = MoveTemp(Parent); return true; } #endif // WITH_EDITOR — lateral regions remain behind the failed shippability gate