From f83c4bd8c10b56d97e1d7a131176dc04c5d29b92 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 16:04:51 +0000 Subject: [PATCH] refactor(generator): delete the pre-operator-stack archetype functions With the operator stack as the only density path, the per-archetype density functions have no caller left in the game: - Delete UVoxelGenerator::GetSlabDensity, GetMazeDensity, GetSurfaceDensity, SurfaceDensityFromColumn, GetVerticalShaftDensity and GetFloatingIslandDensity (~1000 lines) with their declarations. - Drop GetDensityWithParams' OutWarpedPosition out-parameter; only the native GetDensityAt branch removed in the previous commit used it. - OpStack Slab/Maze/VerticalShaft/FloatingIsland and SurfaceHeight tests: remove the stack-vs-legacy comparisons; keep decomposition, finiteness, liveness, window-invariance, cache-cost and box-verdict checks (Shaft cache timing now measures the stack source). - PassageLandsInOpenSpace: the source-only footing probe evaluates the archetype stack built without a strate manager instead of the deleted functions. - Comments that cited the deleted functions now cite the operators (FSlabVoidSource, FLatticeCorridorSource, FIslandBlobSource, ...). GSurfColCache is documented as ClassifyTile's column cache. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01URDGLsu7c4nunqNFPJEFPU --- .../Tests/VoxelForgeClassifyTileTest.cpp | 8 +- .../Tests/VoxelForgeDensityPurityTest.cpp | 6 +- .../Tests/VoxelForgeHeightStackTest.cpp | 87 +- .../Private/Tests/VoxelForgeMazeSeamTest.cpp | 31 +- .../Tests/VoxelForgeOpStackIslandTest.cpp | 99 +- .../Tests/VoxelForgeOpStackMazeTest.cpp | 168 +-- .../Tests/VoxelForgeOpStackShaftTest.cpp | 76 +- .../Tests/VoxelForgeOpStackSlabTest.cpp | 121 +- .../Tests/VoxelForgePassageOpenSpaceTest.cpp | 60 +- .../Private/Tests/VoxelForgeTestFixture.h | 4 +- .../Private/VoxelCaveMorphology.cpp | 11 +- .../Private/VoxelDensityOpStack.cpp | 122 +- Source/VoxelForge/Private/VoxelGenerator.cpp | 1053 +---------------- .../VoxelForge/Public/VoxelDensityOpStack.h | 19 +- Source/VoxelForge/Public/VoxelGenerator.h | 86 +- 15 files changed, 253 insertions(+), 1698 deletions(-) diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeClassifyTileTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeClassifyTileTest.cpp index d63ecae..ef18c92 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeClassifyTileTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeClassifyTileTest.cpp @@ -211,10 +211,10 @@ bool FVoxelForgeClassifyTileTest::RunTest(const FString& Parameters) TestEqual(TEXT("no tile was classified uniform while containing a surface (a false verdict is a hole)"), NumHoles, 0); - // ── Stabilité du verdict : ClassifyTile partage GSurfColCache avec GetDensityAt, donc le - // brute-force ci-dessus a réchauffé les caches. Re-classifier doit rendre le MÊME verdict. - // Verdict stability: ClassifyTile shares GSurfColCache with GetDensityAt, so the brute force - // above warmed the caches. Re-classifying must yield the SAME verdict. + // ── Stabilité du verdict : le premier ClassifyTile a réchauffé son cache de colonnes et le + // brute-force ci-dessus les caches de GetDensityAt. Re-classifier doit rendre le MÊME verdict. + // Verdict stability: the first ClassifyTile warmed its column cache and the brute force + // above warmed GetDensityAt's caches. Re-classifying must yield the SAME verdict. for (const FTileSpec& Spec : ToVerify) { const EVoxelTileClass A = Gen->ClassifyTile(Spec.Origin, Spec.Step, Spec.Cells); diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeDensityPurityTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeDensityPurityTest.cpp index d3a303e..354446a 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeDensityPurityTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeDensityPurityTest.cpp @@ -13,8 +13,8 @@ // seam, and in multiplayer a world divergence. // // POURQUOI CE TEST EXISTE / WHY THIS TEST EXISTS: -// ~30 caches thread_local à clé manuelle vivent sous GetDensityAt (CP_*, GSurfColCache, les -// slots de diff, le cache SDF). Chacun est correct exactement tant que sa CLÉ contient toutes +// ~30 caches thread_local à clé manuelle vivent sous GetDensityAt (CP_*, les mémos par cellule +// et par colonne des piles, les slots de diff, le cache SDF). Chacun est correct exactement tant que sa CLÉ contient toutes // les entrées dont dépend la valeur cachée. Une entrée oubliée ne casse rien tout de suite : // elle produit une mauvaise valeur seulement quand le cache est chaud pour une AUTRE entrée — // c'est-à-dire de façon intermittente, dépendante de l'ordre, et invisible en jeu jusqu'à ce @@ -351,7 +351,7 @@ bool FVoxelForgeLiveEditInvalidationTest::RunTest(const FString& Parameters) TEXT("STALE PARAMS (AUDIT C2): the SurfaceWorld heightfield params were tripled and the ") TEXT("layout rebuilt, yet all %d probe densities are bit-identical. A per-chunk cache is ") TEXT("still keyed on ChunkCoord alone and skipped its refetch. Suspects, in order: ") - TEXT("CP_Chunk/CP_Version in GetDensityAt, the GSurfColCache box key, and the ") + TEXT("CP_Chunk/CP_Version in GetDensityAt, the FSurfaceColumnSource column memo key, and the ") TEXT("FChunkBiomeCache validity box (which says nothing about the FBiomeContext its ") TEXT("cells were classified against)."), Probes.Num())); } diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeHeightStackTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeHeightStackTest.cpp index 94836ee..ae9b953 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeHeightStackTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeHeightStackTest.cpp @@ -8,23 +8,15 @@ // voudrait dire soit un canal par-voxel pour une propriété de COLONNE, soit un seul opérateur // opaque — ce que `OPSTACK-PLAN §2.5` appelle exactement l'échec du refactor. // -// D'où une seconde famille, `VoxelHeightOp.h`. **Ce test est ce qui dit si elle était une bonne -// idée** — la même méthode que la Phase 1 a appliquée à la densité : décomposer, puis MESURER -// contre l'original, avant de construire par-dessus. +// D'où une seconde famille, `VoxelHeightOp.h`, mesurée ici contre `ComputeSurfaceTerrainZ`. // // ⚠️ CE QUE CE TEST COUVRE // ✅ la pile de HAUTEUR du sol, contre `ComputeSurfaceTerrainZ` (défauts puis tous les ops F20) ; -// ✅ les canaux de densité courants : ordre, fenêtre et overhang ; +// ✅ la pile de densité SurfaceWorld : décomposition, ordre des canaux, densité finie, overhang ; // ✅ le mélange synthétique à poids zéro. // -// The old GetSurfaceDensity helper remains a differential diagnostic only: it does not carry the -// current common structural post. The assertions target the height stack and the density stack -// used by the owner path, so room ownership and the authored-floor removal are not mistaken for -// a failed height port. -// -// LA BARRE : **bit à bit** remains the rule for height-space operations. Density comparisons to -// the retired direct helper are reported, not asserted, because room ownership and the common -// MC post intentionally make those paths different. +// LA BARRE : **bit à bit** pour les opérations en espace hauteur. +// The bar is bit-identity for height-space operations. #if WITH_DEV_AUTOMATION_TESTS @@ -285,18 +277,9 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters) // ÉTAPE 2a — LE PONT VERS L'ESPACE DENSITÉ //========================================================================= // `FSurfaceColumnSource` consomme les DEUX piles de hauteur (sol + voûte) et rend une densité. - // La référence est `GetSurfaceDensity`, qui est exactement la variante **sans overhang** - // (il passe `OverhangAmp = 0`) et **sans biomes** (ParamsD == ParamsN, poids 0) — donc la - // comparaison est nette plutôt qu'approximative. - // - // ⚠️ Ce que ce bloc NE teste PAS, et qu'il ne faut pas croire testé : l'overhang et le mélange - // de biomes. Tous deux arrivent à l'étape 2b, avec le chemin CACHÉ pour référence — c'est le - // seul qui les calcule. + // Cette passe est la variante **sans overhang** et **sans biomes** ; l'overhang a sa propre + // passe juste en dessous. / No-overhang, no-biome variant; the overhang has its own pass below. { - // ⚠️ `OverhangStrength = 0` EXPLICITEMENT : `GetSurfaceDensity` passe `OverhangAmp = 0`, - // donc il n'en calcule aucun. Comparer une pile qui en produit à une référence qui n'en - // produit pas ferait échouer le test pour la seule raison que la référence est incomplète. - // L'overhang a sa propre passe juste en dessous, avec le bon oracle. FSurfaceGenerationParams P = AllOps; P.OverhangStrength = 0.0f; @@ -318,8 +301,7 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters) Ctx.StrateBottomWorldZ = P.StrateBottomWorldZ; Stack.PrepareChunk(Ctx); - int32 NumDiff = 0, NumSideDisagree = 0, WorstIdx = -1, NumNonFinite = 0; - float WorstDelta = 0.0f; + int32 NumNonFinite = 0; FRandomStream Rng(5150); for (int32 i = 0; i < NumHeightSamples; ++i) @@ -327,44 +309,23 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters) const float X = (float)Rng.RandRange(-4 * CHUNK_SIZE, 4 * CHUNK_SIZE); const float Y = (float)Rng.RandRange(-4 * CHUNK_SIZE, 4 * CHUNK_SIZE); const float Z = (float)Rng.RandRange(BottomVoxelZ, TopVoxelZ); - - // ParamsD == ParamsN, poids 0 ⇒ une seule évaluation, pas de biomes. - const float Old = Gen->GetSurfaceDensity(X, Y, Z, P, P, 0.0f); - const float New = Stack.EvalMC(X, Y, Z); - if (!FMath::IsFinite(New)) + if (!FMath::IsFinite(Stack.EvalMC(X, Y, Z))) { ++NumNonFinite; } - - if (!BitEqual(Old, New)) - { - ++NumDiff; - const float Delta = FMath::Abs(Old - New); - if (Delta > WorstDelta) { WorstDelta = Delta; WorstIdx = i; } - } - if ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSideDisagree; } } - AddInfo(FString::Printf( - TEXT("SurfaceWorld density legacy diagnostic: %d of %d samples differ from " - "GetSurfaceDensity (largest |delta| %.9g), %d side disagreements; " - "current stack non-finite samples=%d."), - NumDiff, NumHeightSamples, WorstDelta, NumSideDisagree, NumNonFinite)); TestEqual(TEXT("surface: current density stack has no non-finite samples"), NumNonFinite, 0); } //========================================================================= - // ÉTAPE 2b — L'OVERHANG, contre le SEUL oracle qui le calcule + // ÉTAPE 2b — L'OVERHANG //========================================================================= - // `GetSurfaceDensity` passe `OverhangAmp = 0`. La seule référence est donc le chemin caché : - // `ComputeSurfaceColumn` (qui résout le gate et la direction amont par colonne) suivi de - // `SurfaceDensityFromColumn` (qui applique l'union par voxel). Les deux viennent d'être - // exposées pour ça. - // - // C'est aussi la passe qui vérifie le MÉMO DE COLONNE de `FSurfaceColumnSource` : l'op overhang - // lit la colonne produite par la source, et s'ils divergeaient d'un XY, l'union se ferait au - // mauvais endroit. Un mémo mal clé se verrait ici. + // `ComputeSurfaceColumn` résout le gate et la direction amont par colonne ; la moitié des + // échantillons est placée DANS la fenêtre d'overhang qu'il rapporte, pour que l'op overhang de + // la pile soit réellement exercé. / Half the samples are placed inside the overhang window that + // ComputeSurfaceColumn reports, so the stack's overhang op is actually exercised. { FSurfaceGenerationParams P = AllOps; P.OverhangStrength = 0.8f; @@ -391,8 +352,7 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters) TArray NoBiomeParams; FChunkBiomeCache BiomeCache; - int32 NumDiff = 0, NumSideDisagree = 0, NumInWindow = 0, NumNonFinite = 0; - float WorstDelta = 0.0f; + int32 NumInWindow = 0, NumNonFinite = 0; FRandomStream Rng(1337); for (int32 i = 0; i < NumHeightSamples; ++i) @@ -418,27 +378,16 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters) Z = (float)Rng.RandRange(BottomVoxelZ, TopVoxelZ); } - const float Old = Gen->SurfaceDensityFromColumn(X, Y, Z, TerrainZ, CeilSurf, - Amp, DirX, DirY, P); - const float New = Stack.EvalMC(X, Y, Z); - if (!FMath::IsFinite(New)) + if (!FMath::IsFinite(Stack.EvalMC(X, Y, Z))) { ++NumNonFinite; } - - if (!BitEqual(Old, New)) - { - ++NumDiff; - WorstDelta = FMath::Max(WorstDelta, FMath::Abs(Old - New)); - } - if ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSideDisagree; } } AddInfo(FString::Printf( - TEXT("Overhang legacy diagnostic: %d of %d samples differ from " - "SurfaceDensityFromColumn (largest |delta| %.9g), %d side disagreements, " - "%d deliberately inside the overhang window; current stack non-finite samples=%d."), - NumDiff, NumHeightSamples, WorstDelta, NumSideDisagree, NumInWindow, NumNonFinite)); + TEXT("Overhang: %d of %d samples deliberately inside the overhang window; current " + "stack non-finite samples=%d."), + NumInWindow, NumHeightSamples, NumNonFinite)); TestEqual(TEXT("overhang: current density stack has no non-finite samples"), NumNonFinite, 0); diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeMazeSeamTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeMazeSeamTest.cpp index d83ccd5..f6d432d 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeMazeSeamTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeMazeSeamTest.cpp @@ -108,36 +108,37 @@ bool FVoxelForgeMazeSeamTest::RunTest(const FString& Parameters) return false; } - int32 Top = 0, Bottom = 0; - if (!World.GetSlotVoxelZRange(FTestWorld::SlotMaze, Top, Bottom)) + int32 MazeTopZ = 0, MazeBottomZ = 0; + if (!World.GetSlotVoxelZRange(FTestWorld::SlotMaze, MazeTopZ, MazeBottomZ)) { AddError(TEXT("The seam fixture has no Maze slot.")); return false; } - const int32 MidChunkZ = ((Top + Bottom) / 2) / CHUNK_SIZE; - const FMazeGenerationParams Params = World.StrateManager->GetMazeParamsForChunk( - FIntVector(0, 0, MidChunkZ)); + const int32 MazeMidChunkZ = ((MazeTopZ + MazeBottomZ) / 2) / CHUNK_SIZE; + const FMazeGenerationParams MazeParams = World.StrateManager->GetMazeParamsForChunk( + FIntVector(0, 0, MazeMidChunkZ)); - TArray Probes; - AddMazeBoundaryProbes(Params, Bottom, Top, Probes); + TArray SeamProbes; + AddMazeBoundaryProbes(MazeParams, MazeBottomZ, MazeTopZ, SeamProbes); - int32 First = INDEX_NONE; - float Before = 0.0f, After = 0.0f; - const int32 Mismatches = CountContextMismatches(World, Probes, First, Before, After); + int32 FirstProbe = INDEX_NONE; + float FirstBefore = 0.0f, FirstAfter = 0.0f; + const int32 NumMismatches = CountContextMismatches( + World, SeamProbes, FirstProbe, FirstBefore, FirstAfter); - TestEqual(TEXT("Maze has no chunk-context seam mismatches"), Mismatches, 0); - TestTrue(TEXT("Maze seam test sampled cell-boundary probes"), Probes.Num() > 0); + TestEqual(TEXT("Maze has no chunk-context seam mismatches"), NumMismatches, 0); + TestTrue(TEXT("Maze seam test sampled cell-boundary probes"), SeamProbes.Num() > 0); - if (First != INDEX_NONE) + if (FirstProbe != INDEX_NONE) { AddError(FString::Printf(TEXT("Maze seam mismatch at probe %d: %.9g -> %.9g"), - First, Before, After)); + FirstProbe, FirstBefore, FirstAfter)); } AddInfo(FString::Printf( TEXT("Maze seam freedom: %d boundary probes, mismatches=%d; " "local window=2x2x2 child nodes ({-1,0}^3), no wide collect."), - Probes.Num(), Mismatches)); + SeamProbes.Num(), NumMismatches)); return true; } diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeOpStackIslandTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeOpStackIslandTest.cpp index 072476c..402726f 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeOpStackIslandTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeOpStackIslandTest.cpp @@ -1,30 +1,18 @@ // VoxelForgeOpStackIslandTest.cpp -// FloatingIslands — le portage qui fait tourner la pile À L'ENVERS. -// FloatingIslands — the port that runs the stack BACKWARDS. +// FloatingIslands — la pile qui tourne À L'ENVERS / the stack that runs BACKWARDS. // -// CE QUE CELUI-CI PROUVE EN PLUS DES AUTRES -// `VerticalShaftEquivalence` a mesuré la réutilisation À L'IDENTIQUE : trois opérateurs de Maze -// repris sans une ligne de changement. Celui-ci mesure quelque chose de plus fort, et de plus -// risqué pour l'abstraction : **la réutilisation PAR INVERSION**. -// -// Les quatre archétypes déjà portés partent tous de ROC et CREUSENT. FloatingIslands part du VIDE -// et REMPLIT. Si l'axe abstrait choisi (le SIGNE de la densité, convention interne positif = solide) -// est le bon, alors les deux extrémités de la pile doivent être les MÊMES opérateurs au signe près : +// Les autres archétypes partent de ROC et CREUSENT ; FloatingIslands part du VIDE et REMPLIT, avec +// les MÊMES opérateurs au signe près (convention interne positif = solide) : // // FConstantFieldSource(+Base) ←→ FConstantFieldSource(-Base) // FSdfConvertOp(Sign = -1) ←→ FSdfConvertOp(Sign = +1) // -// Et c'est le cas : le seul opérateur neuf de ce portage est le blob d'île. Un archétype qui se -// réutilise en s'INVERSANT est une preuve plus forte qu'un archétype qui se réutilise à l'identique -// — le premier dit que l'abstraction a trouvé le bon axe, le second seulement que deux archétypes -// se ressemblaient. +// Le seul opérateur propre à l'archétype est le blob d'île. / The only archetype-specific op is the +// island blob; the rest is Maze's rock + carve with the opposite sign. // -// ET LE VERDICT DE BOÎTE : c'est ici que `ClassifyBox` peut rendre **AllAir** pour la première fois -// de tout le plugin. Une strate d'îles flottantes est, par construction, surtout vide ; aucun -// archétype de grotte n'a jamais su prouver « tout air » (`OPSTACK-DECOMPOSITION §7`). Le test -// compte les deux verdicts SÉPARÉMENT, parce qu'un total agrégé masquerait exactement ce gain-là. -// -// LA BARRE : bit à bit, comme les autres depuis `FPSemantics = Precise` (AUDIT §C9/§C10). +// ET LE VERDICT DE BOÎTE : une strate d'îles flottantes est surtout vide, donc `ClassifyBox` doit +// savoir rendre **AllAir** (`OPSTACK-DECOMPOSITION §7`). Le test compte AllSolid et AllAir +// SÉPARÉMENT, parce qu'un total agrégé masquerait exactement ce verdict-là. #if WITH_DEV_AUTOMATION_TESTS @@ -92,8 +80,8 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters) if (P.StrateTopWorldZ - P.StrateBottomWorldZ <= 0.0f) { - AddError(TEXT("The FloatingIslands strate has degenerate Z bounds, which sends ") - TEXT("GetFloatingIslandDensity down its early-out. The op stack has none by design.")); + AddError(TEXT("The FloatingIslands strate has degenerate Z bounds; GetDensityAt builds no ") + TEXT("stack for it. The op stack has no degenerate-strate early-out by design.")); return false; } @@ -127,7 +115,7 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters) } //========================================================================= - // 1. ÉQUIVALENCE + // 1. LIVENESS — roche d'île et vide ouvert / island rock and open void //========================================================================= // On compte SÉPARÉMENT le solide d'intérieur et le solide de seal : sur cet archétype la // quasi-totalité du volume est de l'air, donc un « N solides » agrégé serait dominé par les @@ -135,74 +123,34 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters) const float InnerBot = P.StrateBottomWorldZ + P.BoundarySealThickness; const float InnerTop = P.StrateTopWorldZ - P.BoundarySealThickness; - int32 NumDiff = 0, NumSideDisagree = 0, WorstIdx = -1; int32 NumInsideIsland = 0, NumOpenVoid = 0; - float WorstDelta = 0.0f; - for (int32 i = 0; i < NumIslandSamples; ++i) { const float X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (float)Points[i].Z; - - const float Old = Gen->GetFloatingIslandDensity(X, Y, Z, P); const float New = Stack.EvalMC(X, Y, Z); const bool bInterior = (Z > InnerBot && Z < InnerTop); - if (bInterior && Old < 0.0f) { ++NumInsideIsland; } // solide loin des seals ⇒ une île - if (bInterior && Old >= 0.0f) { ++NumOpenVoid; } - - if (!BitEqual(Old, New)) - { - ++NumDiff; - const float D = FMath::Abs(Old - New); - if (D > WorstDelta) { WorstDelta = D; WorstIdx = i; } - } - if ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSideDisagree; } + if (bInterior && New < 0.0f) { ++NumInsideIsland; } // solide loin des seals ⇒ une île + if (bInterior && New >= 0.0f) { ++NumOpenVoid; } } - - if (NumDiff == 0) - { - AddInfo(FString::Printf( - TEXT("FloatingIslands: bit-identical across %d samples (%d inside island rock away from ") - TEXT("the seal bands, %d in open void, so the void source, the blobs, the roughness and ") - TEXT("the fill were all exercised). The stack runs BACKWARDS -- void source + fill ") - TEXT("instead of rock source + carve -- using the SAME operators with the opposite ") - TEXT("sign. Only the blob source is new (OPSTACK-PLAN 2.5)."), - NumIslandSamples, NumInsideIsland, NumOpenVoid)); - } - else - { - AddError(FString::Printf( - TEXT("FloatingIslands: %d of %d samples differ (largest |delta| %.9g at (%.0f, %.0f, ") - TEXT("%.0f)); %d cross the isosurface. Since /fp:precise the bar is bit-identity, so ") - TEXT("this is a real port error. Check, in order: the C1 warp fix (BOTH paths must now ") - TEXT("use VoxelHash::SeedOffset(S, 0.0007f) -- if only one was changed, EVERY warped ") - TEXT("sample differs), then the SdfConvert SIGN (+1 fills, -1 carves), then the 'Isld' ") - TEXT("salt (0x49736C64), the roughness frequency (0.08 / 4 octaves here, NOT Maze's ") - TEXT("0.12 / 3), the per-island TaperEnd and TopFlatten dome branch, and the ") - TEXT("SmoothMin blend K = max(SDFBlendRadius, 0.01)."), - NumDiff, NumIslandSamples, WorstDelta, - WorstIdx >= 0 ? Points[WorstIdx].X : 0.0f, - WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f, - WorstIdx >= 0 ? Points[WorstIdx].Z : 0.0f, - NumSideDisagree)); - } - - TestEqual(TEXT("no sample lands on the opposite side of the isosurface"), NumSideDisagree, 0); + AddInfo(FString::Printf( + TEXT("FloatingIslands: %d of %d samples inside island rock away from the seal bands, %d in ") + TEXT("open void."), + NumInsideIsland, NumIslandSamples, NumOpenVoid)); if (NumInsideIsland == 0) { AddWarning(TEXT("No sample landed inside island rock away from the seal bands, so the blob ") - TEXT("source and the fill were never meaningfully exercised -- the equivalence ") - TEXT("above then only proves that two empty voids agree. Raise IslandDensity or ") - TEXT("IslandMaxRadius.")); + TEXT("source and the fill were never meaningfully exercised. Raise IslandDensity ") + TEXT("or IslandMaxRadius.")); } //========================================================================= // 2. INVARIANCE DE FENÊTRE //========================================================================= // La source garde un cache 3×3 `thread_local` dont la clé est le jeu de params — et cette clé - // inclut délibérément `BoundarySealThickness`, que l'original omet alors que `SpreadZ` le lit - // (voir la note dans FIslandBlobSource::GetCells). + // inclut délibérément `BoundarySealThickness`, que `SpreadZ` lit (voir la note dans + // FIslandBlobSource::GetCells). { std::atomic Impure{ 0 }; const int32 NumBlocks = FMath::Max(4, FMath::Min(16, FPlatformMisc::NumberOfCores())); @@ -296,9 +244,8 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters) AddInfo(FString::Printf( TEXT("Box verdicts over 60 FloatingIslands tiles (XY sampled from +/- %d voxels = %.1f x ") TEXT("IslandSpacing %.0f): %d proved AllSolid, %d proved AllAir, ") - TEXT("%d Mixed, %d voxels checked, %d violations. Today's ClassifyTile proves ZERO of these. The AllAir count is the new ") - TEXT("thing: no cave archetype has ever been able to prove 'all air', and a floating-") - TEXT("island strate is mostly exactly that (OPSTACK-DECOMPOSITION 7)."), + TEXT("%d Mixed, %d voxels checked, %d violations. The AllAir count matters most: a ") + TEXT("floating-island strate is mostly open air (OPSTACK-DECOMPOSITION 7)."), SpanVoxels, (float)SpanVoxels / FMath::Max(P.IslandSpacing, 1.0f), P.IslandSpacing, NumProvedSolid, NumProvedAir, NumMixed, NumBruteSamples, NumUnsound)); diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeOpStackMazeTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeOpStackMazeTest.cpp index ca8d713..a9ed67e 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeOpStackMazeTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeOpStackMazeTest.cpp @@ -1,73 +1,17 @@ // VoxelForgeOpStackMazeTest.cpp -// PHASE 1, LE TEST QUI COMPTE — la pile d'opérateurs Maze contre GetMazeDensity. -// PHASE 1'S LOAD-BEARING TEST — the Maze operator stack against GetMazeDensity. +// La pile d'opérateurs Maze / The Maze operator stack. // -// CE QUE LA PHASE 1 DEVAIT PROUVER / WHAT PHASE 1 HAD TO PROVE -// Le déclencheur d'arrêt de `OPSTACK-PLAN §4` : **« est-ce que la séparation source / modifier tombe -// naturellement du code existant ? »** Réponse mesurée : oui. Maze se décompose en huit opérateurs -// sans contorsion, le SDF est reproduit BIT POUR BIT, et aucun échantillon ne change de côté de -// l'isosurface. -// -// ═════════════════════════════════════════════════════════════════════════════════════════ -// ✅ MISE À JOUR 2026-07-27 : LE PLANCHER ULP N'EXISTE PLUS. C'ÉTAIT `/fp:fast`. -// ═════════════════════════════════════════════════════════════════════════════════════════ -// `FPSemantics = Precise` sur le module (AUDIT §C9, posé pour le cross-play Linux/Windows) fait -// passer ce test à **BIT-IDENTIQUE**. La section ci-dessous décrit un état RÉVOLU ; elle est gardée -// parce qu'elle explique pourquoi les cinq expériences d'isolation avaient toutes échoué (sous -// `/fp:fast` le compilateur transforme selon le CONTEXTE — il n'y avait aucune variable à isoler) -// et parce qu'elle dit quoi regarder si la bit-identité régresse un jour. -// -// **Conséquence pratique : ce test est maintenant un instrument BEAUCOUP plus fin.** Le moindre -// écart est désormais une vraie trouvaille, pas du bruit à noter. La machinerie de gradation ULP -// est conservée exprès — c'est elle qui signalerait une régression du modèle flottant. -// -// UPDATE: the ULP floor is GONE — FPSemantics = Precise makes this test bit-identical. The section -// below describes a past state, kept because it explains why five isolation experiments all failed -// (under /fp:fast the compiler transforms by CONTEXT — there was no variable to isolate) and what to -// look at if bit-identity ever regresses. -// -// ───────────────────────────────────────────────────────────────────────────────────────── -// ⚠️ LE PLANCHER ULP (HISTORIQUE) — lire ceci avant de « corriger » un écart résiduel -// ───────────────────────────────────────────────────────────────────────────────────────── -// (HISTORIQUE, avant `FPSemantics = Precise`) la pile reproduisait `GetMazeDensity` à ~1-2 ULP près -// sur ~2 % des échantillons (ceux qui tombent -// dans la coquille de blend du SDF, où `Blend - Sdf` annule catastrophiquement et amplifie le -// dernier arrondi). **Zéro échantillon ne traverse l'isosurface**, donc pas un triangle ne bouge. -// -// L'origine exacte de ce dernier arrondi n'a PAS été identifiée, après six cycles de build et cinq -// hypothèses toutes réfutées par la mesure (aller-retour FVector · fenêtre de rugosité · `/fp:fast` -// entre unités de compilation · contexte d'inlining · constante de compilation vs donnée -// d'exécution). Ce qui EST établi par la mesure : -// -// • le SDF est bit-identique sur 126/126 des écarts — le treillis, les hashs, l'ensemble d'arêtes -// et `VoxelSDF::Capsule` sont donc exacts ; -// • l'écart naît entièrement dans la conversion SDF→densité, au dernier arrondi ; -// • il est DÉTERMINISTE (mêmes échantillons, même delta, même coordonnée à chaque run) ; -// • il ne dépend ni de l'unité de compilation, ni de l'inlining, ni du modèle flottant. -// -// **Décision (Jahni, 2026-07-27) : on l'accepte et on avance.** Aucune décision du projet ne dépend -// de la réponse, et la chasse coûtait plus que l'information. Consigné comme point ouvert dans -// `AUDIT-2026-07.md §C10`. -// -// ⚠️ LA RÈGLE QUI EN DÉCOULE, ELLE, EST IMPORTANTE : -// **ne jamais faire tourner les deux chemins (switch d'archétype et pile d'opérateurs) dans le même -// monde, et ne jamais comparer leurs sorties pour égalité.** Ce n'est PAS un risque de désync entre -// clients — dans un même binaire le champ est prouvé pur (`VoxelForge.Determinism.DensityPurity`, -// bit-identique entre threads et ordres de requête) et tous les pairs exécutent le même chemin. Mais -// une strate à moitié migrée produirait une couture. Le vrai sujet multijoueur est ailleurs : -// `AUDIT §C9` (le défaut FP d'UBT diffère selon la toolchain). -// -// Never run both paths in one world and never compare their outputs for equality. This is NOT a -// client-desync risk — within one binary the field is proven pure and every peer runs the same path — -// but a half-migrated strate would produce a seam. -// -// ───────────────────────────────────────────────────────────────────────────────────────── -// LA BARRE D'ACCEPTATION, ENCODÉE CI-DESSOUS / THE ACCEPTANCE BAR, ENCODED BELOW -// ───────────────────────────────────────────────────────────────────────────────────────── -// • ÉCHEC DUR : un seul échantillon qui change de côté de l'isosurface (la géométrie bouge). -// • INFO : des écarts à l'échelle de l'ULP (le plancher, attendu). -// • WARN : un écart plus grand — ÇA, c'est une vraie dérive de portage, et il faut chercher. -// Un test qui avertit à chaque portage serait ignoré par le portage qui compte. +// Ce que ce test vérifie / What this test checks: +// • TOPOLOGIE — le graphe parent orienté vers l'origine est un arbre couvrant (N-1 arêtes, zéro +// cellule déconnectée), les boucles optionnelles restent bornées, et le graphe est déterministe +// sur 64 seeds. / The origin-directed parent graph is a spanning tree, deterministic per seed. +// • DÉCOMPOSITION — huit opérateurs (roche + couloirs + rugosité + carve + 4 structurels), pas un +// `FMazeOp` monolithique. / Eight ops, not one wrapped monolith. +// • INVARIANCE DE FENÊTRE — le cache par cellule est `thread_local` : bit-identique quel que soit +// l'ordre des requêtes et le thread. / Bit-identical across query order and worker threads. +// • VERDICTS DE BOÎTE — chaque verdict uniforme survit à la force brute (un faux verdict est un +// trou), y compris pour une pile source/convertisseur jamais écrite à la main. +// Every uniform box verdict survives brute force, including for a novel source/converter stack. #if WITH_DEV_AUTOMATION_TESTS @@ -355,13 +299,13 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters) return false; } - // GetMazeDensity court-circuite sur une strate dégénérée (`return 1.0f`). Cette garde appartient - // à la fonction d'archétype, pas à un opérateur ; la pile suppose une strate valide. + // La pile suppose une strate valide ; `GetDensityAt` rend de l'air sans pile sur une strate + // dégénérée. / The stack assumes a valid strate; GetDensityAt returns air for a degenerate one. if (MazeParams.StrateTopWorldZ - MazeParams.StrateBottomWorldZ <= 0.0f) { AddError(FString::Printf( - TEXT("The Maze strate has degenerate Z bounds (top %.1f, bottom %.1f), which sends ") - TEXT("GetMazeDensity down its early-out. The op stack has no such early-out by design."), + TEXT("The Maze strate has degenerate Z bounds (top %.1f, bottom %.1f); GetDensityAt ") + TEXT("builds no stack for it. The op stack has no degenerate-strate early-out by design."), MazeParams.StrateTopWorldZ, MazeParams.StrateBottomWorldZ)); return false; } @@ -485,70 +429,6 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters) } } - //========================================================================= - // ÉQUIVALENCE — géométrie d'abord, bits ensuite. - //========================================================================= - int32 NumDiff = 0, WorstIdx = -1, NumBeyondUlpNoise = 0, NumSolidDisagreements = 0; - float WorstDelta = 0.0f; - for (int32 i = 0; i < NumMazeSamples; ++i) - { - const float X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (float)Points[i].Z; - - const float Old = Gen->GetMazeDensity(X, Y, Z, MazeParams); // MC : négatif = solide - const float New = Stack.EvalMC(X, Y, Z); - - if (!BitEqual(Old, New)) - { - ++NumDiff; - const float Delta = FMath::Abs(Old - New); - if (Delta > WorstDelta) { WorstDelta = Delta; WorstIdx = i; } - - // `Blend - Sdf` annule catastrophiquement au bord de la coquille de blend, donc un - // écart d'ULP sur le SDF ressort amplifié sur la densité : marge généreuse, mais bornée. - const float UlpNoise = 16.0f * FMath::Max(FMath::Abs(Old), 1.0f) * FLT_EPSILON; - if (Delta > UlpNoise) { ++NumBeyondUlpNoise; } - } - // Le mesher ne lit que le SIGNE (D >= IsoLevel ⇒ air). Un désaccord de CÔTÉ bouge la géométrie. - if ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSolidDisagreements; } - } - - if (NumDiff == 0) - { - AddInfo(FString::Printf(TEXT("Bit-identical across %d samples."), NumMazeSamples)); - } - else if (NumBeyondUlpNoise == 0) - { - AddInfo(FString::Printf( - TEXT("%d of %d samples differ, ALL at ULP scale (largest |delta| %.9g at (%.0f, %.0f, ") - TEXT("%.0f)), and 0 cross the isosurface -- not one triangle would move. This is the ") - TEXT("accepted floor; see the header comment and AUDIT-2026-07.md C10. The SDF itself is ") - TEXT("reproduced BIT FOR BIT, so the lattice, the hashes and VoxelSDF::Capsule are exact; ") - TEXT("only the final SDF->density rounding differs. Do not go hunting this again without ") - TEXT("reading C10 first -- five hypotheses have already been measured and refuted."), - NumDiff, NumMazeSamples, WorstDelta, - WorstIdx >= 0 ? Points[WorstIdx].X : 0.0f, - WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f, - WorstIdx >= 0 ? Points[WorstIdx].Z : 0.0f)); - } - else - { - AddWarning(FString::Printf( - TEXT("%d of %d samples differ and %d are TOO LARGE to be the accepted ULP floor (largest ") - TEXT("|delta| %.9g at (%.0f, %.0f, %.0f)); %d cross the isosurface. THIS one is real port ") - TEXT("drift, not the known floor. Check, in order: the roughness apply-window ") - TEXT("(R + SurfaceRoughness + 2), the carve blend (2.0), the noise frequency (0.12) and ") - TEXT("octave count (3), and the order of the structural post ops."), - NumDiff, NumMazeSamples, NumBeyondUlpNoise, WorstDelta, - WorstIdx >= 0 ? Points[WorstIdx].X : 0.0f, - WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f, - WorstIdx >= 0 ? Points[WorstIdx].Z : 0.0f, - NumSolidDisagreements)); - } - - // Le SEUL échec dur : un désaccord de côté d'iso EST une différence de géométrie. - TestEqual(TEXT("no sample lands on the opposite side of the isosurface from the original"), - NumSolidDisagreements, 0); - //========================================================================= // INVARIANCE DE FENÊTRE — la pile doit tenir les mêmes règles que le générateur. //========================================================================= @@ -624,10 +504,9 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters) //========================================================================= // LE VERDICT DE BOÎTE — le prix perf de l'intervalle SDF conservatif. //========================================================================= - // ClassifyTile renvoie Mixed pour tout archétype de grotte, donc TunnelNetwork, Maze, - // VerticalShafts, FloatingIslands, FlatPlain, CrystalChamber et Underwater ne captent RIEN du - // gain T1.d. Tout nombre > 0 ici est le saut de tuile propre à la pile, pas une décision du - // classifieur historique. + // Pour un slot de cave, ClassifyTile plie `ClassifyBox` de cette pile : tout nombre > 0 ici est + // une tuile que le mesher saute. / For a cave slot ClassifyTile folds this stack's ClassifyBox: + // every proved box here is a tile the mesher skips. { int32 NumProved = 0, NumMixed = 0, NumUnsound = 0, NumBruteSamples = 0; FRandomStream Rng(24680); @@ -689,17 +568,14 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters) AddInfo(FString::Printf( TEXT("Box verdicts over %d Maze tiles (XY sampled from +/- %d voxels = %.1f x ") - TEXT("CellSize %.0f): %d proved uniform, %d Mixed, %d voxels checked, %d violations. Today's ClassifyTile ") - TEXT("proves ZERO of these -- every cave archetype falls through to \"pas prouvable en ") - TEXT("v1\". Any number above zero here is tile-skipping Maze has never had."), + TEXT("CellSize %.0f): %d proved uniform, %d Mixed, %d voxels checked, %d violations."), NumMazeBoxTests, SpanVoxels, (float)SpanVoxels / FMath::Max(MazeParams.CellSize, 1.0f), MazeParams.CellSize, NumProved, NumMixed, NumBruteSamples, NumUnsound)); if (NumProved == 0) { - AddWarning(TEXT("The stack proved no tile uniform, so it is not yet better than today's ") - TEXT("classifier for Maze. Not a correctness problem, but the perf case for ") - TEXT("the port rests on this number.")); + AddWarning(TEXT("The stack proved no tile uniform, so ClassifyTile skips no Maze tile. ") + TEXT("Not a correctness problem, but a perf regression.")); } } diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeOpStackShaftTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeOpStackShaftTest.cpp index e87a124..753ed43 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeOpStackShaftTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeOpStackShaftTest.cpp @@ -1,21 +1,15 @@ // VoxelForgeOpStackShaftTest.cpp -// VerticalShafts — le portage qui teste la RÉUTILISATION et le contrat de l'opérateur courant. -// VerticalShafts — the port that tests REUSE and the current operator contract. +// VerticalShafts — la RÉUTILISATION d'opérateurs et le contrat de la pile. +// VerticalShafts — operator REUSE and the stack's contract. // -// CE QUE CELUI-CI PROUVE EN PLUS DES AUTRES -// Les portages précédents demandaient « la décomposition reproduit-elle l'original ? ». Celui-ci -// demande **« les opérateurs se RÉUTILISENT-ils vraiment entre archétypes ? »**, qui est la thèse -// de `OPSTACK-PLAN §2.5` et la seule raison de faire ce refactor plutôt que de nettoyer le `switch`. +// Trois des cinq opérateurs de VerticalShafts sont ceux de Maze, repris tels quels : +// `ConstantRock`, `SdfRoughness`, `SdfCarve` — mêmes ops, autre source et autres réglages +// (fréquence 0.1 au lieu de 0.12, fenêtre `rough + 4` au lieu de `R + rough + 2`). C'est la thèse +// de `OPSTACK-PLAN §2.5` : les opérateurs se réutilisent entre archétypes. // -// Trois des cinq opérateurs de VerticalShafts sont ceux de Maze, **repris sans une ligne de -// changement** : `ConstantRock`, `SdfRoughness`, `SdfCarve`. Dans le `switch`, `GetMazeDensity` et -// `GetVerticalShaftDensity` sont deux fonctions de ~100 lignes qui n'ont rien en commun à l'œil. -// En opérateurs, ce sont les mêmes trois ops avec une source différente et d'autres réglages -// (fréquence 0.1 au lieu de 0.12, fenêtre `rough + 4` au lieu de `R + rough + 2`). -// -// The direct helper remains useful migration telemetry, but it is not the owner path after the -// structural-post split. The assertions below target the operator stack actually used in-game: -// current channel count/order, feature liveness, cache isolation and box-proof soundness. +// Three of the five VerticalShafts ops are Maze's, reused unchanged. The assertions below target +// the stack used in-game: channel count/order, feature liveness, window invariance, cache cost and +// box-proof soundness. #if WITH_DEV_AUTOMATION_TESTS @@ -93,8 +87,8 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters) if (P.StrateTopWorldZ - P.StrateBottomWorldZ <= 0.0f) { - AddError(TEXT("The VerticalShafts strate has degenerate Z bounds, which sends ") - TEXT("GetVerticalShaftDensity down its early-out. The op stack has none by design.")); + AddError(TEXT("The VerticalShafts strate has degenerate Z bounds; GetDensityAt builds no ") + TEXT("stack for it. The op stack has no degenerate-strate early-out by design.")); return false; } @@ -128,49 +122,17 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters) } //========================================================================= - // 1. CURRENT-OWNER DIAGNOSTIC - // GetVerticalShaftDensity is the retired direct path. Its delta is reported so a future - // port can be audited, but it is not an owner-path failure: the game evaluates Stack plus - // the common MC tail. + // 1. LIVENESS — des échantillons tombent dans l'air des puits / samples land in shaft air //========================================================================= - int32 NumDiff = 0, NumSideDisagree = 0, WorstIdx = -1, NumInsideShaft = 0; - float WorstDelta = 0.0f; - + int32 NumInsideShaft = 0; for (int32 i = 0; i < NumShaftSamples; ++i) { const float X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (float)Points[i].Z; - - const float Old = Gen->GetVerticalShaftDensity(X, Y, Z, P); - const float New = EvalStackMCForTest(Stack, X, Y, Z); - - if (Old >= 0.0f) { ++NumInsideShaft; } // air ⇒ dans un puits/connecteur/étagère - - if (!BitEqual(Old, New)) - { - ++NumDiff; - const float D = FMath::Abs(Old - New); - if (D > WorstDelta) { WorstDelta = D; WorstIdx = i; } - } - if ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSideDisagree; } + // air ⇒ dans un puits/connecteur/étagère / air ⇒ inside a shaft, connector or ledge + if (EvalStackMCForTest(Stack, X, Y, Z) >= 0.0f) { ++NumInsideShaft; } } - - if (NumDiff == 0) - { - AddInfo(FString::Printf( - TEXT("VerticalShafts: bit-identical across %d samples (%d of them inside a shaft, so ") - TEXT("the cylinders, connectors, roughness, carve and ledges were all exercised). ") - TEXT("THREE of the five ops here are Maze's, reused unchanged -- operator reuse across ") - TEXT("archetypes is now measured rather than intended (OPSTACK-PLAN 2.5)."), - NumShaftSamples, NumInsideShaft)); - } - AddInfo(FString::Printf( - TEXT("VerticalShafts legacy diagnostic: %d of %d samples differ (largest |delta| %.9g at " - "(%.0f, %.0f, %.0f)); %d cross the isosurface; %d samples are air in the stack."), - NumDiff, NumShaftSamples, WorstDelta, - WorstIdx >= 0 ? Points[WorstIdx].X : 0.0f, - WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f, - WorstIdx >= 0 ? Points[WorstIdx].Z : 0.0f, - NumSideDisagree, NumInsideShaft)); + AddInfo(FString::Printf(TEXT("VerticalShafts: %d of %d samples are air in the stack."), + NumInsideShaft, NumShaftSamples)); TestTrue(TEXT("shaft feature sampling exercised the operator stack"), NumInsideShaft > 0); if (NumInsideShaft == 0) @@ -268,14 +230,14 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters) const int32 CellY = (i / 16) - 8; LastX = (static_cast(CellX) + 0.37f) * PerfSpacing; LastY = (static_cast(CellY) + 0.61f) * PerfSpacing; - Sink += Gen->GetVerticalShaftDensity(LastX, LastY, PerfZ, P); + Sink += EvalStackMCForTest(Stack, LastX, LastY, PerfZ); } const double RebuildSeconds = FPlatformTime::Seconds() - RebuildStart; const double HotStart = FPlatformTime::Seconds(); for (int32 i = 0; i < NumHotSamples; ++i) { - Sink += Gen->GetVerticalShaftDensity(LastX, LastY, PerfZ, P); + Sink += EvalStackMCForTest(Stack, LastX, LastY, PerfZ); } const double HotSeconds = FPlatformTime::Seconds() - HotStart; diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeOpStackSlabTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeOpStackSlabTest.cpp index e8e055a..774c16e 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeOpStackSlabTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeOpStackSlabTest.cpp @@ -1,32 +1,20 @@ // VoxelForgeOpStackSlabTest.cpp -// PHASE 2, PREMIER PORTAGE — la pile Slab courante, sur LES DEUX archétypes. -// PHASE 2'S FIRST PORT — the current Slab operator stack, on BOTH archetypes. +// La pile Slab, sur LES DEUX archétypes / The Slab operator stack, on BOTH archetypes. // // CE QUE CE TEST DOIT PROUVER / WHAT THIS TEST HAS TO PROVE -// Trois choses, et la troisième est la raison d'être du portage : // -// 1. CURRENT OWNER — the stack's channels, window invariance and conservative box verdicts -// guard the path used by the game. `GetSlabDensity` is retained as a legacy diagnostic only: -// it still owns the former full passage/support tail, while the operator stack is followed by -// the current common MC post. -// 2. UN OPÉRATEUR, DEUX ARCHÉTYPES — la MÊME pile est vérifiée pour FlatPlain ET -// CrystalChamber. The legacy helper is sampled for migration telemetry, not used as the -// owner oracle; if the current stack becomes non-finite or loses its channel contract, this -// test still fails. +// 1. LE CHEMIN DU JEU — la pile rend une densité finie, invariante à la fenêtre (ordre et +// threads), et ses verdicts de boîte sont conservatifs. +// The stack's density is finite and window-invariant, and its box verdicts are conservative. +// 2. UN OPÉRATEUR, DEUX ARCHÉTYPES — la MÊME pile est vérifiée pour FlatPlain ET CrystalChamber. // ⚠️ La fixture ne règle que `GeneratorType`, donc les deux slots portent des params PAR // DÉFAUT : à eux seuls ils exécutent la même configuration à deux profondeurs. C'est la // TROISIÈME passe (`CrystalChamber(tuned)`, `CeilingRoughness` 6 → 20) qui fait réellement // varier ce qui distingue les deux archétypes — et qui sert en même temps de pire cas aux // bornes d'amplitude de `ClassifyBox`. Voir le bloc en bas de fichier. -// 3. LE VERDICT DE BOÎTE — et c'est ici que §3.1 se paie. `ClassifyTile` prouve ZÉRO tuile pour -// FlatPlain et CrystalChamber aujourd'hui. Depuis que les deux surfaces sont XY-PURES, leurs -// bornes en Z sont connues exactement (contrat [-1,1] de FBM), donc toute tuile entièrement -// sous le sol ou entre les deux bandes se prouve SANS échantillonner. -// -// ───────────────────────────────────────────────────────────────────────────────────────── -// The old direct helper contains the removed support-floor/room tail. Room ownership and the -// authored-floor removal deliberately split that structural post from the operator stack, so its -// differences remain visible as diagnostics while the owner assertions below cover the live path. +// 3. LE VERDICT DE BOÎTE — les deux surfaces sont XY-PURES, donc leurs bornes en Z sont connues +// exactement (contrat [-1,1] de FBM) : toute tuile entièrement sous le sol ou entre les deux +// bandes se prouve SANS échantillonner. #if WITH_DEV_AUTOMATION_TESTS @@ -73,14 +61,13 @@ bool FVoxelForgeOpStackSlabTest::RunTest(const FString& Parameters) int32 TopVoxelZ, int32 BottomVoxelZ, int32 SlotIndex, const TCHAR* SlotName) { - // `GetSlabDensity` court-circuite sur une strate dégénérée (`return 1.0f`). Cette garde - // appartient à la fonction d'archétype, pas à un opérateur ; la pile suppose une strate - // valide, et `GetDensityAt` retombe sur le `switch` dans ce cas. + // La pile suppose une strate valide ; `GetDensityAt` rend de l'air sans pile sur une strate + // dégénérée. / The stack assumes a valid strate; GetDensityAt returns air for a degenerate one. if (SlabParams.StrateTopWorldZ - SlabParams.StrateBottomWorldZ <= 0.0f) { AddError(FString::Printf( - TEXT("%s has degenerate Z bounds (top %.1f, bottom %.1f), which sends GetSlabDensity ") - TEXT("down its early-out. The op stack has no such early-out by design."), + TEXT("%s has degenerate Z bounds (top %.1f, bottom %.1f); GetDensityAt builds no ") + TEXT("stack for it. The op stack has no degenerate-strate early-out by design."), SlotName, SlabParams.StrateTopWorldZ, SlabParams.StrateBottomWorldZ)); return; } @@ -114,90 +101,16 @@ bool FVoxelForgeOpStackSlabTest::RunTest(const FString& Parameters) } //===================================================================== - // 1. ÉQUIVALENCE — géométrie d'abord, bits ensuite. + // 1. DENSITÉ FINIE / FINITE DENSITY //===================================================================== - // ───────────────────────────────────────────────────────────────────── - // LE BON MÈTRE — corrigé 2026-07-27 après que la passe `tuned` a crié au loup - // ───────────────────────────────────────────────────────────────────── - // Première version : `16 · max(|Old|, 1) · FLT_EPSILON`, c.-à-d. l'ULP mesuré sur la - // DENSITÉ DE SORTIE. C'est le mauvais mètre, et il se trompe exactement là où le test - // regarde le plus : la densité vaut `min(Z - Sol, Plafond - Z)`, donc PRÈS DE L'ISOSURFACE - // la sortie tend vers 0 pendant que les intermédiaires (surfaces, Z monde, amplitudes de - // bruit) valent des CENTAINES. Un arrondi né à l'échelle 400 était jugé contre un mètre - // à l'échelle 1 — 400× trop serré. - // - // Mesuré : la passe `tuned` (rugosités ×2.25 et ×3.33) a vu ses écarts croître ×4.5, et - // son pire écart valait **0.345 ULP de |Z|**. Sous-ULP à l'échelle où l'erreur naît. - // L'erreur est donc proportionnelle à l'AMPLITUDE, ce qui est la signature d'un arrondi - // ordinaire, pas d'une transcription fausse. - // - // Le mètre correct est la magnitude des quantités D'OÙ VIENT l'erreur. Le test reste - // discriminant : une vraie dérive de portage (offset de bruit faux, `abs()` manquant, - // clamp oublié) déplace la surface de plusieurs VOXELS — 4 ordres de grandeur au-dessus - // de ce seuil, pas 4 fois. - // - // The first yardstick measured ULPs on the OUTPUT density, which tends to 0 near the - // isosurface while the intermediates are in the hundreds. Rounding born at scale ~400 was - // judged against a yardstick of scale 1. Real port drift moves the surface by voxels — - // four orders of magnitude above this bound, so the test stays discriminating. - const float SurfaceScale = FMath::Max(FMath::Abs(SlabParams.StrateTopWorldZ), - FMath::Abs(SlabParams.StrateBottomWorldZ)); - - int32 NumDiff = 0, WorstIdx = -1, NumBeyondUlpNoise = 0, NumSolidDisagreements = 0; int32 NumNonFinite = 0; - float WorstDelta = 0.0f, WorstOld = 0.0f, WorstUlpsOfScale = 0.0f; - // Le pire cas PARMI LES DÉPASSEMENTS — c'est lui qui dit si un WARN est du bruit ou une dérive. - float WorstOutlierDelta = 0.0f, WorstOutlierOld = 0.0f, WorstOutlierUlps = 0.0f; - for (int32 i = 0; i < NumSlabSamples; ++i) { - const float X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (float)Points[i].Z; - - const float Old = Gen->GetSlabDensity(X, Y, Z, SlabParams); // MC : négatif = solide - const float New = Stack.EvalMC(X, Y, Z); - if (!FMath::IsFinite(New)) + if (!FMath::IsFinite(Stack.EvalMC((float)Points[i].X, (float)Points[i].Y, (float)Points[i].Z))) { ++NumNonFinite; } - - if (!BitEqual(Old, New)) - { - ++NumDiff; - const float Delta = FMath::Abs(Old - New); - - // L'échelle à laquelle CET échantillon calcule : la sortie, sa propre altitude, et - // les bornes de la strate. C'est le plus grand des trois qui porte l'arrondi. - const float Scale = FMath::Max3(FMath::Abs(Old), FMath::Abs(Z), - FMath::Max(SurfaceScale, 1.0f)); - const float Ulps = Delta / (Scale * FLT_EPSILON); - - if (Delta > WorstDelta) - { - WorstDelta = Delta; WorstIdx = i; WorstOld = Old; WorstUlpsOfScale = Ulps; - } - - if (Delta > 16.0f * Scale * FLT_EPSILON) - { - ++NumBeyondUlpNoise; - if (Delta > WorstOutlierDelta) - { - WorstOutlierDelta = Delta; WorstOutlierOld = Old; WorstOutlierUlps = Ulps; - } - } - } - // Le mesher ne lit que le SIGNE. Un désaccord de CÔTÉ bouge la géométrie. - if ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSolidDisagreements; } } - - AddInfo(FString::Printf( - TEXT("%s legacy slab diagnostic: %d of %d samples differ, %d exceed the old ULP " - "bound, largest |delta| %.9g at (%.0f,%.0f,%.0f), %d side disagreements; " - "current stack non-finite samples=%d."), - SlotName, NumDiff, NumSlabSamples, NumBeyondUlpNoise, WorstDelta, - WorstIdx >= 0 ? Points[WorstIdx].X : 0.0f, - WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f, - WorstIdx >= 0 ? Points[WorstIdx].Z : 0.0f, - NumSolidDisagreements, NumNonFinite)); TestEqual(*FString::Printf(TEXT("%s: current stack has no non-finite density"), SlotName), NumNonFinite, 0); @@ -300,9 +213,9 @@ bool FVoxelForgeOpStackSlabTest::RunTest(const FString& Parameters) AddInfo(FString::Printf( TEXT("%s box verdicts over %d tiles (XY sampled from +/- %d voxels = %.1f x ") - TEXT("ColumnSpacing %.0f): %d proved uniform, %d Mixed, %d voxels checked, %d violations. Today's ") - TEXT("ClassifyTile proves ZERO of these. This number is the whole point of making ") - TEXT("the slab surfaces XY-pure (OPSTACK-DECOMPOSITION 3.1)."), + TEXT("ColumnSpacing %.0f): %d proved uniform, %d Mixed, %d voxels checked, %d violations. ") + TEXT("This number is the whole point of making the slab surfaces XY-pure ") + TEXT("(OPSTACK-DECOMPOSITION 3.1)."), SlotName, NumSlabTiles, SpanVoxels, (float)SpanVoxels / FMath::Max(SlabParams.ColumnSpacing, 1.0f), SlabParams.ColumnSpacing, NumProved, NumMixed, NumBruteSamples, NumUnsound)); diff --git a/Source/VoxelForge/Private/Tests/VoxelForgePassageOpenSpaceTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgePassageOpenSpaceTest.cpp index 8201f2b..cd864a1 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgePassageOpenSpaceTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgePassageOpenSpaceTest.cpp @@ -8,6 +8,7 @@ #include "Misc/AutomationTest.h" #include "VoxelCaveMorphology.h" +#include "VoxelDensityOpStack.h" #include "VoxelDensityPrimitives.h" #include "VoxelForgeTestFixture.h" #include "VoxelPassageGeometry.h" @@ -900,11 +901,27 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters) ++NumFootingChecked; // The live world's passage modifier is intentionally absent here. The endpoint is - // already known to be passage-carved, so this second generator checks the archetype - // source itself: air at the landing and solid immediately below it. - TStrongObjectPtr SourceOnlyGenerator( - NewObject(GetTransientPackage(), NAME_None, RF_Transient)); - SourceOnlyGenerator->InitializeSettings(World.Settings.Get()); + // already known to be passage-carved, so the archetype's own stack, built without a + // strate manager (hence without passage carving), checks the source itself: air at + // the landing and solid immediately below it. + const UVoxelGenerator& LiveGenerator = *World.Generator; + FVoxelOpStack SourceStack; + FVoxelOpContext SourceContext; + SourceContext.Seed = (uint32)LiveGenerator.Seed; + SourceContext.LayoutVersion = World.StrateManager->GetLayoutVersion(); + SourceContext.StrateTopWorldZ = StrateTopZ; + SourceContext.StrateBottomWorldZ = StrateBottomZ; + SourceContext.WorldRadiusVoxels = LiveGenerator.WorldRadiusVoxels; + SourceContext.EdgeSealThickness = LiveGenerator.EdgeSealThickness; + // The generator resets the shaft-connector hand-off around every density query; a + // direct stack evaluation must model that boundary too. + auto EvalSource = [&SourceStack](float QueryX, float QueryY, float QueryZ) + { + VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker(); + const float SourceDensity = SourceStack.EvalMC(QueryX, QueryY, QueryZ); + VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker(); + return SourceDensity; + }; float LandingDensity = 0.0f; float FootingDensity = 0.0f; @@ -915,13 +932,16 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters) FMazeGenerationParams P = Definition->MazeParams; P.StrateTopWorldZ = StrateTopZ; P.StrateBottomWorldZ = StrateBottomZ; - LandingDensity = SourceOnlyGenerator->GetMazeDensity( - SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z, P); + VoxelDensityOps::BuildMazeStack(SourceStack, P, LiveGenerator.Seed, + LiveGenerator.OriginSpineRadius, /*StrateManager*/nullptr); + SourceStack.PrepareChunk(SourceContext); + LandingDensity = EvalSource( + SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z); const float FloorProbeZ = SuggestedPoint.Z - FMath::Max(P.CorridorRadius, 0.5f) - P.SurfaceRoughness * VOXEL_NOISE_SCALE * 1.5f - 3.0f; - FootingDensity = SourceOnlyGenerator->GetMazeDensity( - SuggestedPoint.X, SuggestedPoint.Y, FloorProbeZ, P); + FootingDensity = EvalSource( + SuggestedPoint.X, SuggestedPoint.Y, FloorProbeZ); break; } @@ -930,11 +950,14 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters) FVerticalShaftParams P = Definition->VerticalShaftParams; P.StrateTopWorldZ = StrateTopZ; P.StrateBottomWorldZ = StrateBottomZ; - LandingDensity = SourceOnlyGenerator->GetVerticalShaftDensity( - SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z, P); + VoxelDensityOps::BuildVerticalShaftStack(SourceStack, P, LiveGenerator.Seed, + LiveGenerator.OriginSpineRadius, /*StrateManager*/nullptr); + SourceStack.PrepareChunk(SourceContext); + LandingDensity = EvalSource( + SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z); const float FloorProbeZ = StrateBottomZ + P.BoundarySealThickness * 0.5f; - FootingDensity = SourceOnlyGenerator->GetVerticalShaftDensity( - SuggestedPoint.X, SuggestedPoint.Y, FloorProbeZ, P); + FootingDensity = EvalSource( + SuggestedPoint.X, SuggestedPoint.Y, FloorProbeZ); break; } @@ -943,10 +966,13 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters) FFloatingIslandParams P = Definition->FloatingIslandParams; P.StrateTopWorldZ = StrateTopZ; P.StrateBottomWorldZ = StrateBottomZ; - LandingDensity = SourceOnlyGenerator->GetFloatingIslandDensity( - SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z, P); - FootingDensity = SourceOnlyGenerator->GetFloatingIslandDensity( - SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z - 1.0f, P); + VoxelDensityOps::BuildFloatingIslandStack(SourceStack, P, LiveGenerator.Seed, + LiveGenerator.OriginSpineRadius, /*StrateManager*/nullptr); + SourceStack.PrepareChunk(SourceContext); + LandingDensity = EvalSource( + SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z); + FootingDensity = EvalSource( + SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z - 1.0f); break; } diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeTestFixture.h b/Source/VoxelForge/Private/Tests/VoxelForgeTestFixture.h index f4da2b3..1a83d0d 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeTestFixture.h +++ b/Source/VoxelForge/Private/Tests/VoxelForgeTestFixture.h @@ -230,8 +230,8 @@ namespace VoxelForgeTest /** * A spread of world sample points that deliberately crosses chunk boundaries, strate * boundaries and bedrock gaps — the exact conditions under which a per-chunk cache with a - * missing key input produces a wrong answer. Integer XY on purpose: that is the branch - * GetDensityAt's T1.a column cache actually takes (fractional XY bypasses the cache). + * missing key input produces a wrong answer. Integer XY on purpose: that is the lattice the + * mesher samples, and the one the per-column memos are keyed on. */ inline void BuildSamplePoints(const FTestWorld& World, int32 Count, int32 Seed, TArray& OutPoints) diff --git a/Source/VoxelForge/Private/VoxelCaveMorphology.cpp b/Source/VoxelForge/Private/VoxelCaveMorphology.cpp index 302adc0..fe11ac0 100644 --- a/Source/VoxelForge/Private/VoxelCaveMorphology.cpp +++ b/Source/VoxelForge/Private/VoxelCaveMorphology.cpp @@ -5303,9 +5303,9 @@ namespace } // The slab source is an XY height band. Evaluate the same two pure height fields as - // GetSlabDensity, then choose their midpoint after intersecting the seal-free interior. + // FSlabVoidSource, then choose their midpoint after intersecting the seal-free interior. // La source slab est une bande de hauteurs XY : on recalcule les deux champs purs comme - // GetSlabDensity, puis on prend leur milieu après intersection avec l'intérieur sans seal. + // FSlabVoidSource, puis on prend leur milieu après intersection avec l'intérieur sans seal. bool VF_SuggestSlabLandingPoint( const FSlabGenerationParams& Params, int32 Seed, @@ -5444,7 +5444,8 @@ namespace // Maze corridors are lattice edges. Only a horizontal edge is a useful landing source: a // vertical edge can prove air, but cannot prove a place to stand. The edge set and Z levels - // below use the same origin-directed tree + loop contract as GetMazeDensity, with no cache. + // below use the same origin-directed tree + loop contract as FLatticeCorridorSource, with no + // cache. bool VF_SuggestMazeLandingPoint( const FMazeGenerationParams& Params, int32 Seed, @@ -5879,7 +5880,7 @@ namespace }; // Evaluate only the island source, in the same internal-density convention as - // GetFloatingIslandDensity (positive = solid, negative = void). This is intentionally a + // FIslandBlobSource (positive = solid, negative = void). This is intentionally a // local pure evaluator: it lets the query bracket the actual blob top instead of treating a // nominal island centre as footing. Full-octave noise is used because this query has no LOD // state by design and GeneratePassages runs before a tile's LOD is selected. @@ -5916,7 +5917,7 @@ namespace for (int32 IslandIndex = 0; IslandIndex < NumIslands; ++IslandIndex) { const FVFIslandSite& Island = Islands[IslandIndex]; - // Production GetFloatingIslandDensity evaluates exactly this 3x3 cell neighbourhood + // Production FIslandBlobSource evaluates exactly this 3x3 cell neighbourhood // for the queried XY. The outer search may collect more candidates so a lateral snap // can be found, but those distant blobs must not influence the returned footing. if (FMath::Abs(Island.CellX - BaseCellX) > 1 diff --git a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp index dffe8ae..b028912 100644 --- a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp +++ b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp @@ -2,21 +2,11 @@ // 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 alimentent le jeu derrière l'opt-in décrit dans l'en-tête de VoxelDensityOpStack.h. +// 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). // -// FIDÉLITÉ / FIDELITY -// Chaque corps ci-dessous est une transcription LITTÉRALE du bloc correspondant de -// `UVoxelGenerator::GetMazeDensity` — mêmes hashes, mêmes constantes, même ordre d'opérations -// flottantes, même convention de signe (INTERNE : positif = solide). L'objectif est -// l'égalité BIT à BIT, vérifiée par `VoxelForge.OpStack.MazeEquivalence`. -// -// `OPSTACK-PLAN §2.6` n'EXIGE pas l'identité binaire avec l'ancien système — mais Maze se -// décompose si proprement qu'on peut l'obtenir, et quand on peut l'obtenir il faut la prendre : -// une égalité binaire transforme « je crois que la décomposition est correcte » en preuve. -// -// §2.6 does not REQUIRE bit-identity with the old system — but Maze decomposes cleanly enough that -// it is achievable, and when it is achievable it should be taken: bit-equality turns "I believe the -// decomposition is right" into a proof. +// 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 "VoxelDensityOpStack.h" #include "VoxelStrateComposer.h" @@ -674,7 +664,7 @@ namespace , CorridorRadius(FMath::Max(P.CorridorRadius, 0.5f)) , BranchProbability(P.BranchProbability) , Verticality(P.Verticality) - , Salt((uint32)Seed ^ 0x4D617A65u) // 'Maze' — identique à GetMazeDensity + , Salt((uint32)Seed ^ 0x4D617A65u) // 'Maze' {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; } @@ -765,7 +755,7 @@ namespace } /** - * Le cache par CELLULE, partagé avec le même contrat que GetMazeDensity. Il est `thread_local` et non + * 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. @@ -829,16 +819,13 @@ namespace //========================================================================= // RÔLE 1 — SOURCE : DALLE / SLAB VOID (FlatPlain ET CrystalChamber) //========================================================================= - // Transcription littérale des ÉTAPES 1-3 de `GetSlabDensity` : surface de sol, surface de - // plafond, puis `Density = -min(distAuSol, distAuPlafond)`. + // Surface de sol, surface de plafond, puis `Density = -min(distAuSol, distAuPlafond)`. // - // DEUX archétypes, UN opérateur. `GetSlabDensity` est appelé pour FlatPlain et - // CrystalChamber sans le moindre branchement sur le type — CrystalChamber n'est rien d'autre - // que FlatPlain avec un `CeilingRoughness` plus grand. C'est le premier vrai gain du refactor - // (OPSTACK-PLAN §4) : deux des huit archétypes disparaissent dans un seul opérateur, et la - // différence entre eux redevient ce qu'elle a toujours été — un jeu de valeurs par défaut. + // 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: GetSlabDensity is called for both with no branch on the type. + // 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 @@ -993,7 +980,7 @@ namespace // 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é. C'est tout le combine de `SurfaceDensityFromColumn` : + // densité : // // Density = TerrainZ - Z ← solide sous le sol // Density = max(Density, Z - CeilSurf) ← solide au-dessus de la voûte @@ -1564,13 +1551,13 @@ namespace //========================================================================= // RÔLE 3 — MODIFIER : COLONNES SUR GRILLE MONDE / WORLD-GRID COLUMNS //========================================================================= - // ÉTAPE 4 de `GetSlabDensity`. Des cylindres de hauteur infinie posés sur une grille de + // 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 repris tel quel (il était déjà `thread_local` dans l'original, - // et c'est exactement ce que la note de threading de VoxelDensityOp.h autorise). Sa clé + // 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). @@ -1657,7 +1644,7 @@ namespace private: struct FSlabColumn { float X, Y, R; }; - static constexpr float ColBlend = 2.0f; // identique à GetSlabDensity + 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. */ @@ -1714,8 +1701,7 @@ namespace // 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. C'est la symétrie - // que le `switch` ne pouvait pas montrer : les deux blocs y sont à 900 lignes l'un de l'autre. + // 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 @@ -2267,7 +2253,7 @@ namespace { public: FShaftFieldSource(const FVerticalShaftParams& InP, int32 Seed, float InSpineRadius) - : P(InP), Salt((uint32)Seed ^ 0x53686674u) // 'Shft' — identique à GetVerticalShaftDensity + : P(InP), Salt((uint32)Seed ^ 0x53686674u) // 'Shft' , SpineRadius(InSpineRadius) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; } @@ -2427,9 +2413,9 @@ namespace } /** 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 (comme l'original) : stable à travers les reconstructions de pile, ce qui - * est la leçon retenue du mémo de colonne de SurfaceWorld. */ + * 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); @@ -2723,9 +2709,9 @@ namespace 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 de l'original est `CaveSDF < 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. + // ⚠️ 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); @@ -2886,7 +2872,7 @@ namespace { public: FIslandBlobSource(const FFloatingIslandParams& InP, int32 Seed) - : P(InP), Salt((uint32)Seed ^ 0x49736C64u) // 'Isld' — identique à GetFloatingIslandDensity + : P(InP), Salt((uint32)Seed ^ 0x49736C64u) // 'Isld' {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; } @@ -2962,8 +2948,8 @@ namespace * • 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. C'est le comportement de l'original ; le confondre avec « rien en dessous » - * serait un TROU, et c'est pourquoi seule la borne HAUTE est testée. + * 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é. @@ -3059,8 +3045,7 @@ namespace private: /** Tirage d'une cellule. PURE en (cellule, seed, params) ⇒ `Eval` et `EffectOverBox` ne - * peuvent pas voir des îles différentes. Transcription littérale du bloc de cuisson de - * `GetFloatingIslandDensity`. */ + * peuvent pas voir des îles différentes. */ bool RollIsland(int32 nx, int32 ny, FIsland& Out) const { const float H = P.StrateTopWorldZ - P.StrateBottomWorldZ; @@ -3094,17 +3079,15 @@ namespace } /** - * Le voisinage 3×3, mémoïsé par worker — la même cuisson `thread_local` que l'original. + * Le voisinage 3×3, mémoïsé par worker (`thread_local`). * - * ⚠️ LA CLÉ INCLUT `BoundarySealThickness`, QUE L'ORIGINAL OMET. `SpreadZ` s'en sert - * (`H·0.5 − max(TopHalf, UnderDepth) − Seal`), donc dans `GetFloatingIslandDensity` une - * édition à chaud qui ne change QUE l'épaisseur de seal sert 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. Ajouter le champ ne coûte - * qu'un recalcul, jamais une valeur différente — donc l'égalité binaire tient. + * ⚠️ 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, which the original omits although SpreadZ reads it. - * Adding it can only cost a recompute, never change a value — bit-equality is unaffected. + * 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 { @@ -9326,8 +9309,8 @@ namespace VoxelDensityOps TUniquePtr MakeConstantVoidSource(float BaseDensity) { - // `float Density = -Params.BaseDensity; // start as open air (void)` — la négation unaire - // est exacte, donc c'est littéralement la première ligne de GetFloatingIslandDensity. + // `Density = -BaseDensity` : on part du vide. La négation unaire est exacte. + // Start as open air; unary negation is exact. return MakeUnique(-BaseDensity); } @@ -9391,10 +9374,8 @@ namespace VoxelDensityOps int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager, bool bAppendStructuralPosts) { - // DEUX archétypes entrent ici, aucun branchement ne les distingue — parce que - // `GetSlabDensity` n'en fait aucun non plus. FlatPlain et CrystalChamber ne diffèrent que - // par leurs valeurs par défaut, et c'est maintenant visible dans le code plutôt que dans - // un commentaire. 8 archétypes → 7. + // 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)); @@ -9410,15 +9391,10 @@ namespace VoxelDensityOps int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager, bool bAppendStructuralPosts) { - // ⚠️ LA PREUVE QUE L'ABSTRACTION EST RÉELLE, et elle vaut d'être dite : TROIS des cinq - // opérateurs ci-dessous sont ceux de Maze, **repris sans une ligne de changement** — - // `ConstantRock`, `SdfRoughness`, `SdfCarve`. Dans le `switch`, Maze et VerticalShafts sont - // deux fonctions de ~100 lignes qui n'ont rien en commun à l'œil ; en opérateurs, ce sont - // les MÊMES trois ops avec une source différente. C'est exactement ce que `§2.5` prédisait. + // 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 without a line changed. In the switch, - // Maze and VerticalShafts are two unrelated ~100-line functions; as operators they are the - // same three ops with a different source. + // THREE of the five ops below are Maze's, reused unchanged, with a different source. constexpr float CarveBlend = 2.0f; TUniquePtr ShaftSource = @@ -9427,9 +9403,8 @@ namespace VoxelDensityOps OutStack.Add(MakeConstantRockSource(P.BaseDensity)); OutStack.Add(MoveTemp(ShaftSource)); - // Fréquence 0.1 et fenêtre `SurfaceRoughness + 4` — les constantes de - // `GetVerticalShaftDensity`, PAS celles de Maze (0.12 / `R + rough + 2`). Même opérateur, - // réglages différents : c'est le point. + // 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)); @@ -9544,8 +9519,8 @@ namespace VoxelDensityOps OutStack.Add(MakeConstantVoidSource(P.BaseDensity)); OutStack.Add(MakeUnique(P, Seed)); - // Fréquence 0.08 et 4 octaves — les constantes de `GetFloatingIslandDensity`. Quatrième - // archétype à réutiliser cet opérateur (Maze 0.12/3, VerticalShafts 0.1/3). + // 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)); @@ -9562,15 +9537,14 @@ namespace VoxelDensityOps int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager, bool bAppendStructuralPosts) { - // Les constantes viennent telles quelles de GetMazeDensity — elles y étaient codées en dur. + // 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` dans - // l'original. Reproduite à l'identique pour que l'égalité binaire tienne. + // 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)); diff --git a/Source/VoxelForge/Private/VoxelGenerator.cpp b/Source/VoxelForge/Private/VoxelGenerator.cpp index 157bd6b..e9d7c54 100644 --- a/Source/VoxelForge/Private/VoxelGenerator.cpp +++ b/Source/VoxelForge/Private/VoxelGenerator.cpp @@ -264,13 +264,12 @@ private: }; //============================================================================= -// SURFACE COLUMN CACHE (T1.a) — kill the per-Z heightfield redundancy +// SURFACE COLUMN CACHE — ClassifyTile's SurfaceWorld column proof (T1.d) //============================================================================= -// The SurfaceWorld heightfield + sky-cap + biome blend are functions of XY ONLY, but -// the mesher samples ~33 Z grid-points per column, each re-running that XY work. We cache -// the column (terrain Z + ceiling Z) once per integer XY and reuse it down the column. -// Used ONLY for integer-XY queries (the density grid); fractional queries (gradient -// normals at interpolated vertices) fall through and compute directly → bit-identical. +// The SurfaceWorld heightfield + sky-cap + biome blend are functions of XY ONLY, but a tile +// spans many Z grid-points per column. ClassifyTile caches the column (terrain Z + ceiling Z) +// once per integer XY and reuses it down the column. (The surface density stack keeps its own +// column memo in FSurfaceColumnSource.) // The surface heightfield (TerrainZ + sky-cap CeilSurf) is a PURE function of (worldX, worldY, // seed, strate) — ZERO Z dependence: the climate/Voronoi fields are pure-XY (see ResolveBiomeSampleAt: // "result is the pure function of XY either way") and surface params are constant within a strate (Hard @@ -283,7 +282,7 @@ private: // F20 phase 2 OVERHANG, resolved once per column (biome-blended, slope-gated) so the per-voxel // density path is cheap: OverhangAmp = strength · slope-gate (0 ⇒ no overhang here); (DirX,DirY) = // the UNIT UPHILL direction of the terrain gradient (the lip extends downhill, borrowing rock from -// uphill). See SurfaceDensityFromColumn for the warped-terrain union that makes the shelf. +// uphill). See FOverhangShelfMod for the warped-terrain union that makes the shelf. struct FSurfaceColumn { // Toutes les sorties sont écrites par ComputeSurfaceColumn avant Computed=true ; les valeurs @@ -362,9 +361,9 @@ struct FSurfaceColumnCache } }; -// Cache de colonnes PARTAGÉ par thread : GetDensityAt (hot path, T1.a) + ClassifyTile (T1.d). -// Les deux stockent des valeurs bit-identiques (même ComputeSurfaceColumn, champs purs en XY), -// donc un ClassifyTile qui rend Mixed laisse ses colonnes chaudes pour le GenerateMesh qui suit. +// Cache de colonnes par thread de ClassifyTile (T1.d) : les pré-passes et passes complètes +// successives d'une même tuile, et les tuiles voisines, réutilisent leurs colonnes. +// Per-thread column cache for ClassifyTile: successive passes and neighbouring tiles reuse columns. static thread_local FSurfaceColumnCache GSurfColCache; //============================================================================= @@ -3753,8 +3752,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo bool bApplyLegacyStructuralPosts, FTunnelCoreWorldEvaluation* OutTunnelCore, bool bCollectFusedDiagnostics, - bool* OutRoomOwnsBottom, - FVector* OutWarpedPosition) const + bool* OutRoomOwnsBottom) const { //========================================================================= // STRATE DENSITY FUNCTION (Morphology Pipeline) @@ -3789,10 +3787,6 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo { *OutRoomOwnsBottom = false; } - if (OutWarpedPosition != nullptr) - { - *OutWarpedPosition = FVector::ZeroVector; - } if (bCollectFusedDiagnostics && VoxelDensityProfile::AreCountersEnabled()) { VoxelDensityProfile::AddCounter( @@ -3864,10 +3858,6 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo WarpedY += WarpY * VOXEL_NOISE_SCALE * WS; WarpedZ += WarpZ * VOXEL_NOISE_SCALE * WS; } - if (OutWarpedPosition != nullptr) - { - *OutWarpedPosition = FVector(WarpedX, WarpedY, WarpedZ); - } //========================================================================= // STEP 4: SDF MORPHOLOGY (rooms + tunnels) — CACHED PER CHUNK @@ -5072,353 +5062,6 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo return -Density; } -//============================================================================= -// SLAB DENSITY (FlatPlain / CrystalChamber generator types) -//============================================================================= -// Produces a large horizontal void between a noisy floor and a noisy ceiling. -// No rooms, no tunnels, no worm noise — just two surfaces with noise displacement. -// -// Key difference from TunnelNetwork: ceiling uses abs(noise) so ALL formations -// point DOWNWARD. This creates the stalactite/crystal hanging-from-above effect. -// Floor uses signed noise for natural ground undulation (hills and valleys). -// -// Columns are placed on a world-space hash grid (no rooms to anchor them to). - -float UVoxelGenerator::GetSlabDensity(float WorldX, float WorldY, float WorldZ, - const FSlabGenerationParams& Params) const -{ - const float StrateHeight = Params.StrateTopWorldZ - Params.StrateBottomWorldZ; - - // Degenerate strate (zero or inverted bounds) — return solid. - if (StrateHeight <= 0.0f) return 1.0f; - - const uint32 SeedU = (uint32)Seed; - - //========================================================================= - // STEP 1: FLOOR SURFACE - //========================================================================= - // The floor is at FloorZ + signed noise displacement. - // Signed noise allows both hills (noise > 0 → floor rises) and - // valleys (noise < 0 → floor dips) for natural rolling ground. - // - // ⚠️ XY-PUR / XY-PURE (OPSTACK-DECOMPOSITION §3.1, tranché par Jahni 2026-07-27). - // La 3e coordonnée était `WorldZ * FF * 0.05f` : une hauteur de sol qui dépendait de - // l'altitude d'où on la demandait. Le coefficient était minuscule, donc ça se lisait comme un - // léger étirement vertical plutôt que comme un bug — mais ça bloquait le cache de colonnes T1.a - // et rendait toute classification de boîte inexacte. Constante ⇒ la surface est une vraie - // fonction de (X,Y). Le monde se re-tune une fois : on échantillonne une autre tranche du champ - // de bruit, donc la forme du sol change (elle ne se dégrade pas). - // - // The 3rd coord was WorldZ * FF * 0.05f — a floor height that depended on the altitude you - // asked from. Now a constant, so the surface is a genuine function of (X,Y): the T1.a column - // cache and an exact box verdict both become available. Worlds re-tune once. - - const float FloorZ = Params.StrateBottomWorldZ + StrateHeight * Params.FloorRelativeHeight; - - float FloorNoise = 0.0f; - if (Params.FloorRoughness > 0.0f) - { - float FF = Params.FloorRoughnessFrequency; - FloorNoise = FractalNoise3D(FVector3f( - WorldX * FF + VoxelHash::SeedOffset(SeedU, 7.3f), - WorldY * FF + VoxelHash::SeedOffset(SeedU, 11.1f), - 0.0f // XY-pur : plus aucune dépendance en Z / no Z dependence - ), VoxelGenLOD::Eff(3)) * VOXEL_NOISE_SCALE * Params.FloorRoughness; - } - - // Actual floor surface Z after noise displacement. - const float FloorSurface = FloorZ + FloorNoise; - - //========================================================================= - // STEP 2: CEILING SURFACE (formations hang DOWNWARD) - //========================================================================= - // The ceiling uses abs(noise) so ALL displacement pushes the ceiling DOWN. - // When abs(noise) is high, rock protrudes further into the void — stalactite. - // When abs(noise) is near 0, the ceiling is near the base CeilZ line. - // - // This asymmetry (only downward protrusions, never upward pockets) creates - // the crystal-forest / stalactite silhouette from below. - // - // XY-PUR, même raison que le sol ci-dessus (§3.1). Le `+ 3000.0f` RESTE : ce n'est pas un - // terme en Z, c'est le décalage qui décorrèle le champ du plafond de celui du sol. - // XY-pure for the same reason as the floor. The + 3000.0f STAYS — it is not a Z term, it is - // the offset that decorrelates the ceiling's noise field from the floor's. - - const float CeilZ = Params.StrateBottomWorldZ + StrateHeight * Params.CeilingRelativeHeight; - - float CeilNoise = 0.0f; - if (Params.CeilingRoughness > 0.0f) - { - float CF = Params.CeilingRoughnessFrequency; - float RawNoise = FractalNoise3D(FVector3f( - WorldX * CF + VoxelHash::SeedOffset(SeedU, 17.3f) + 1000.0f, - WorldY * CF + VoxelHash::SeedOffset(SeedU, 19.7f) + 2000.0f, - 3000.0f // XY-pur : décalage de décorrélation seul / offset only - ), VoxelGenLOD::Eff(3)) * VOXEL_NOISE_SCALE; - - // abs() → formations ONLY hang down, never push ceiling up into solid rock. - // Result: every noise peak creates a downward protrusion (crystal/stalactite). - CeilNoise = FMath::Abs(RawNoise) * Params.CeilingRoughness; - } - - // Actual ceiling surface Z (can only move downward due to abs above). - // Clamp so ceiling never drops below floor + 2 voxels of headroom. - // Without this clamp, extreme CeilingRoughness could completely fill the void. - const float CeilSurface = FMath::Max(CeilZ - CeilNoise, FloorSurface + 2.0f); - - //========================================================================= - // STEP 3: VOID FIELD → BASE DENSITY - //========================================================================= - // Each voxel is measured against both surfaces: - // DistAboveFloor > 0 → voxel is above the floor (possibly in the void) - // DistBelowCeil > 0 → voxel is below the ceiling (possibly in the void) - // - // VoidField = min of both distances. - // Positive inside the void (between floor and ceiling). - // Negative outside (below floor or above ceiling = solid rock). - // - // Density = -VoidField (internal convention: positive = solid, negative = air). - - const float DistAboveFloor = WorldZ - FloorSurface; // + when above floor - const float DistBelowCeil = CeilSurface - WorldZ; // + when below ceiling - - const float VoidField = FMath::Min(DistAboveFloor, DistBelowCeil); - - float Density = -VoidField; // Negative = air (inside void), positive = solid - - //========================================================================= - // STEP 4: COLUMNS (hash-based, world-space grid) - //========================================================================= - // Unlike TunnelNetwork columns (anchored to room centers), slab columns - // are placed on a regular world-space hash grid. They are infinite-height - // cylinders — the void field already defines where solid/air is, so the - // column SDF just adds density everywhere along its XY position. - // The column is only visible where the void field carved air around it. - if (Params.ColumnDensity > 0.0f && Params.ColumnSpacing > 0.0f) - { - const float Spacing = Params.ColumnSpacing; - - // Which cell are we in? - const int32 ColCX = FMath::FloorToInt(WorldX / Spacing); - const int32 ColCY = FMath::FloorToInt(WorldY / Spacing); - - // The 3×3 neighbourhood's columns (existence roll, jitter, radius) are a pure function of - // (cell, seed, params) yet were re-derived PER VOXEL — 9 hash rolls + mixes in the slab hot - // loop. Bake them once per centre cell (thread_local); rebuild only when the query crosses a - // cell border or the seed/params change. Bit-identical (same hashes, same math). - struct FSlabColumn { float X, Y, R; }; - 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 || (uint32)Seed != SC_Seed || Spacing != SC_Spacing || - Params.ColumnDensity != SC_Dens || Params.ColumnMinRadius != SC_MinR || Params.ColumnMaxRadius != SC_MaxR) - { - SC_CX = ColCX; SC_CY = ColCY; SC_Seed = (uint32)Seed; SC_Spacing = Spacing; - SC_Dens = Params.ColumnDensity; SC_MinR = Params.ColumnMinRadius; SC_MaxR = Params.ColumnMaxRadius; - SC_Cols.Reset(); - - for (int32 DY = -1; DY <= 1; DY++) - { - for (int32 DX = -1; DX <= 1; DX++) - { - const int32 NCX = ColCX + DX; - const int32 NCY = ColCY + DY; - - // Deterministic: same seed → same column pattern every session. - // XOR with a prime salt so columns don't correlate with room placement. - const uint32 H = VoxelHash::Cell(NCX, NCY, (uint32)Seed ^ 0xC01C01u); - - // ColumnDensity is the probability this cell has a column. - if (VoxelHash::ToFloat01(H) > Params.ColumnDensity) continue; - - // Jitter the column center within the cell (15%-85% of cell extent) - // to avoid a perfectly regular grid pattern. - const float JX = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x12345678u)); - const float JY = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x9ABCDEF0u)); - - FSlabColumn& Col = SC_Cols.AddDefaulted_GetRef(); - Col.X = (NCX + 0.15f + JX * 0.7f) * Spacing; - Col.Y = (NCY + 0.15f + JY * 0.7f) * Spacing; - // Column radius: hash-derived within configured range. - Col.R = FMath::Lerp(Params.ColumnMinRadius, Params.ColumnMaxRadius, - VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0xBEEFu))); - } - } - } - - float ColumnSDF = FLT_MAX; - for (const FSlabColumn& Col : SC_Cols) - { - // 2D cylinder SDF (infinite height — void field handles top/bottom). - const float DX2D = WorldX - Col.X; - const float DY2D = WorldY - Col.Y; - ColumnSDF = FMath::Min(ColumnSDF, FMath::Sqrt(DX2D * DX2D + DY2D * DY2D) - Col.R); - } - - // Smoothstep blend zone around the column edge (avoids hard MC aliasing). - const float ColBlend = 2.0f; - if (ColumnSDF < ColBlend && ColumnSDF < FLT_MAX) - { - float Fill = FMath::Clamp((ColBlend - ColumnSDF) / (ColBlend * 2.0f), 0.0f, 1.0f); - Fill = SmoothStep01(Fill); // Smoothstep - Density += Fill * Params.BaseDensity * 1.5f; - } - } - - //========================================================================= - // STEP 5: BOUNDARY SEAL + STEP 6: PASSAGES + STEP 7: XY EDGE SEAL - //========================================================================= - // Même logique que TunnelNetwork — factorisée dans les helpers ci-dessus. - ApplyOriginSpine(Density, WorldX, WorldY, WorldZ, - Params.StrateTopWorldZ, Params.StrateBottomWorldZ, - Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius); - - ApplyBoundarySeal(Density, WorldZ, - Params.StrateTopWorldZ, Params.StrateBottomWorldZ, - Params.BoundarySealThickness, Params.BaseDensity); - - if (StrateManager) - { - StrateManager->ApplyPassageModifier( - Density, WorldX, WorldY, WorldZ, - Params.BaseDensity, Params.BoundarySealThickness); - } - ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ, - Params.StrateTopWorldZ, Params.StrateBottomWorldZ, - Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius); - - VF_ApplyXYEdgeSeal(Density, WorldX, WorldY, - WorldRadiusVoxels, EdgeSealThickness, Params.BaseDensity); - - // Convention MC: négatif = solide. - return -Density; -} - -//============================================================================= -// MAZE GENERATOR (ECaveGeneratorType::Maze) -//============================================================================= -// Solid rock carved by a deterministic 3D lattice of corridors. Each non-origin lattice node -// chooses one parent toward (0,0,0), which is a spanning tree by construction. A small, capped -// hash-gated loop set adds alternate routes without being needed for connectivity. The local -// source evaluates the eight child nodes in the current cell's {-1,0} halo, so chunk seams cannot -// change an edge decision. - -float UVoxelGenerator::GetMazeDensity(float WorldX, float WorldY, float WorldZ, - const FMazeGenerationParams& Params) const -{ - const float StrateHeight = Params.StrateTopWorldZ - Params.StrateBottomWorldZ; - if (StrateHeight <= 0.0f) return 1.0f; - - const float CS = FMath::Max(Params.CellSize, 1.0f); - const FVector Pos(WorldX, WorldY, WorldZ); - const uint32 S = (uint32)Seed ^ 0x4D617A65u; // 'Maze' - - float Density = Params.BaseDensity; // start solid - - const int32 CX = FMath::FloorToInt(WorldX / CS); - const int32 CY = FMath::FloorToInt(WorldY / CS); - const int32 CZ = FMath::FloorToInt(WorldZ / CS); - - // The edge set reachable from this voxel's cell is a pure function of (cell, seed, params). - // Bake the parent and optional loop capsule endpoints once per cell (thread_local); the - // per-voxel work is just the capsule SDFs. Rebuilds only on a cell crossing / param change. - struct FMazeEdge { FVector A, B; }; - 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; - - const FIntVector Cell(CX, CY, CZ); - if (Cell != MZ_Cell || S != MZ_Seed || CS != MZ_CS || - Params.BranchProbability != MZ_Branch || Params.Verticality != MZ_Vert) - { - MZ_Cell = Cell; MZ_Seed = S; MZ_CS = CS; - MZ_Branch = Params.BranchProbability; MZ_Vert = Params.Verticality; - MZ_Edges.Reset(); - - auto NodeCenter = [CS](int32 X, int32 Y, int32 Z) - { - return FVector((X + 0.5f) * CS, (Y + 0.5f) * CS, (Z + 0.5f) * CS); - }; - // 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. - for (int32 dz = -1; dz <= 0; dz++) - for (int32 dy = -1; dy <= 0; dy++) - for (int32 dx = -1; dx <= 0; dx++) - { - const int32 nx = CX + dx, ny = CY + dy, nz = CZ + dz; - const FVector A = NodeCenter(nx, ny, nz); - - if (VoxelMazeTopology::IsOpenEdge( - nx, ny, nz, VoxelMazeTopology::EAxis::X, - S, Params.BranchProbability, Params.Verticality)) - { - MZ_Edges.Add({ A, NodeCenter(nx + 1, ny, nz) }); - } - if (VoxelMazeTopology::IsOpenEdge( - nx, ny, nz, VoxelMazeTopology::EAxis::Y, - S, Params.BranchProbability, Params.Verticality)) - { - MZ_Edges.Add({ A, NodeCenter(nx, ny + 1, nz) }); - } - if (VoxelMazeTopology::IsOpenEdge( - nx, ny, nz, VoxelMazeTopology::EAxis::Z, - S, Params.BranchProbability, Params.Verticality)) - { - MZ_Edges.Add({ A, NodeCenter(nx, ny, nz + 1) }); - } - } - } - - const float R = FMath::Max(Params.CorridorRadius, 0.5f); - float MazeSDF = FLT_MAX; - for (const FMazeEdge& E : MZ_Edges) - { - MazeSDF = FMath::Min(MazeSDF, VoxelSDF::Capsule(Pos, E.A, E.B, R)); - } - - // Wall roughness: perturb the corridor surface. - if (Params.SurfaceRoughness > 0.0f && MazeSDF < R + Params.SurfaceRoughness + 2.0f) - { - MazeSDF += FractalNoise3D(FVector3f(WorldX * 0.12f, WorldY * 0.12f, WorldZ * 0.12f), VoxelGenLOD::Eff(3)) - * VOXEL_NOISE_SCALE * Params.SurfaceRoughness; - } - - // Carve air where inside a corridor. - const float Blend = 2.0f; - if (MazeSDF < Blend) - { - float Carve = FMath::Clamp((Blend - MazeSDF) / (Blend * 2.0f), 0.0f, 1.0f); - Carve = SmoothStep01(Carve); - Density -= Carve * Params.BaseDensity * 2.0f; - } - - ApplyOriginSpine(Density, WorldX, WorldY, WorldZ, - Params.StrateTopWorldZ, Params.StrateBottomWorldZ, - Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius); - - ApplyBoundarySeal(Density, WorldZ, - Params.StrateTopWorldZ, Params.StrateBottomWorldZ, - Params.BoundarySealThickness, Params.BaseDensity); - - if (StrateManager) - { - StrateManager->ApplyPassageModifier( - Density, WorldX, WorldY, WorldZ, - Params.BaseDensity, Params.BoundarySealThickness); - } - ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ, - Params.StrateTopWorldZ, Params.StrateBottomWorldZ, - Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius); - - VF_ApplyXYEdgeSeal(Density, WorldX, WorldY, - WorldRadiusVoxels, EdgeSealThickness, Params.BaseDensity); - - return -Density; -} - //============================================================================= // SURFACE-WORLD GENERATOR (ECaveGeneratorType::SurfaceWorld) //============================================================================= @@ -5621,71 +5264,6 @@ float UVoxelGenerator::ComputeSurfaceCeiling(float WorldX, float WorldY, return CeilZ - Hang; } -float UVoxelGenerator::SurfaceDensityFromColumn(float WorldX, float WorldY, float WorldZ, - float TerrainZ, float CeilSurf, - float OverhangAmp, float DirX, float DirY, - const FSurfaceGenerationParams& S) const -{ - // Solid below the terrain surface; solid above the sky-cap ceiling. - float Density = TerrainZ - WorldZ; - Density = FMath::Max(Density, WorldZ - CeilSurf); - - // F20 phase 2 — OVERHANG shelf (warped-terrain union): for AIR voxels in a window just above a - // steep slope, re-sample the heightfield UPHILL (toward the cliff) by a height-varying amount and - // union that rock in → the cliff-top rock juts OUT over the void below, self-capping at the cliff's - // height. Genuine 3D (per-voxel re-eval), so it's gated hard: only steep overhang columns (amp>0), - // only air voxels within OverhangHeight of the local ground, only when the shift is ≥ a voxel. - // The lip is capped at TerrainZ+OverhangHeight, which ClassifyTile treats as ambiguous (no holes). - if (OverhangAmp > 0.0f && S.OverhangHeight > 0.0f - && WorldZ > TerrainZ && WorldZ <= TerrainZ + S.OverhangHeight) - { - const uint32 SeedU = (uint32)Seed; - const float f = S.OverhangFrequency; - // Shelf-shape noise [0,1]; the Z term makes the reach fold/curl with height (ragged, not a lip). - const float Ns = FractalNoise3D(FVector3f( - WorldX * f + VoxelHash::SeedOffset(SeedU, 17.3f), - WorldY * f + VoxelHash::SeedOffset(SeedU, 23.9f), - WorldZ * f * S.OverhangZScale + VoxelHash::SeedOffset(SeedU, 5.1f)), 3) * 0.5f + 0.5f; // [0,1] - // KEY: the uphill reach GROWS with height in the window (Frac: 0 at ground → 1 at the cap). Low - // down the shift is tiny ⇒ borrows nearby low rock ⇒ stays AIR over the void; high up the shift - // reaches the far cliff ⇒ solid ⇒ the lip sits on top with air UNDERNEATH = a real overhang. - const float Frac = (WorldZ - TerrainZ) / S.OverhangHeight; // (0,1] inside the window - const float ShiftV = S.OverhangReach * OverhangAmp * Frac * Ns; // uphill reach (voxels) - if (ShiftV > 0.5f) - { - // Borrow the uphill STRUCTURAL height (not the full op'd surface): the shelf underside doesn't - // need cliff/terrace refinement, and this avoids re-running Cliff's resamples per lip voxel. - float Ms; - const float ShiftedTZ = SampleSurfaceStructuralZ(WorldX + DirX * ShiftV, WorldY + DirY * ShiftV, S, Ms); - Density = FMath::Max(Density, ShiftedTZ - WorldZ); // union: solid where uphill rock covers Z - } - } - - // Structural fields (Z bounds, seal, base) are forced equal across biomes → S is safe. - ApplyOriginSpine(Density, WorldX, WorldY, WorldZ, - S.StrateTopWorldZ, S.StrateBottomWorldZ, - S.BoundarySealThickness, S.BaseDensity, OriginSpineRadius); - - ApplyBoundarySeal(Density, WorldZ, - S.StrateTopWorldZ, S.StrateBottomWorldZ, - S.BoundarySealThickness, S.BaseDensity); - - if (StrateManager) - { - StrateManager->ApplyPassageModifier( - Density, WorldX, WorldY, WorldZ, - S.BaseDensity, S.BoundarySealThickness); - } - ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ, - S.StrateTopWorldZ, S.StrateBottomWorldZ, - S.BoundarySealThickness, S.BaseDensity, OriginSpineRadius); - - VF_ApplyXYEdgeSeal(Density, WorldX, WorldY, - WorldRadiusVoxels, EdgeSealThickness, S.BaseDensity); - - return -Density; -} - void UVoxelGenerator::ResolveSurfaceChunkParams(const FIntVector& ChunkCoord, FSurfaceGenerationParams& OutSurface, FBiomeContext& OutBiomeCtx, TArray& OutBiomeParams) const @@ -7290,7 +6868,7 @@ EVoxelTileClass UVoxelGenerator::ClassifyTileUncached( // ── Balayage des colonnes XY sur le treillis exact du mesher (marge incluse). Une colonne // tranche chaque z intérieur : air côté MC ⇔ TerrainZ ≤ z ≤ CeilSurf (D = −interne ≥ 0, - // cf. SurfaceDensityFromColumn ; spine/passages ne font QUE de l'air → gardés plus haut). + // cf. FSurfaceColumnSource ; spine/passages ne font QUE de l'air → gardés plus haut). // Pré-passe clairsemée ~5×5 pour tuer vite les tuiles traversées par la surface, puis // passe complète — les colonnes recalculées par la pré-passe restent chaudes dans la boîte. auto TestColumn = [&](int32 gx, int32 gy) -> bool // false ⇒ les deux hypothèses sont mortes @@ -7361,27 +6939,6 @@ EVoxelTileClass UVoxelGenerator::ClassifyTileUncached( return bCanSolid ? EVoxelTileClass::AllSolid : EVoxelTileClass::AllAir; } -float UVoxelGenerator::GetSurfaceDensity(float WorldX, float WorldY, float WorldZ, - const FSurfaceGenerationParams& ParamsD, - const FSurfaceGenerationParams& ParamsN, - float NeighborWeight) const -{ - if (ParamsD.StrateTopWorldZ - ParamsD.StrateBottomWorldZ <= 0.0f) return 1.0f; - - // Heightfield: dominant biome, OUTPUT-BLENDED toward the nearest neighbour in the - // border band. Blending heights (not params) keeps borders seamless across any param - // difference. ParamsD == ParamsN, weight 0 ⇒ single eval (bit-identical, no biomes). - float TerrainZ = ComputeSurfaceTerrainZ(WorldX, WorldY, ParamsD); - if (NeighborWeight > 0.0f) - { - TerrainZ = FMath::Lerp(TerrainZ, ComputeSurfaceTerrainZ(WorldX, WorldY, ParamsN), NeighborWeight); - } - const float CeilSurf = ComputeSurfaceCeiling(WorldX, WorldY, ParamsD); - - return SurfaceDensityFromColumn(WorldX, WorldY, WorldZ, TerrainZ, CeilSurf, - /*OverhangAmp*/0.0f, /*DirX*/0.0f, /*DirY*/0.0f, ParamsD); -} - //============================================================================= // CLIMATE & BIOME FIELDS //============================================================================= @@ -7795,591 +7352,3 @@ void UVoxelGenerator::GetBiomeMaterialAt(float WorldX, float WorldY, float World OutBlendWeight = S.NeighborWeight; // 0 inside a cell → ~0.5 at the border } } - -//============================================================================= -// VERTICAL-SHAFT GENERATOR (ECaveGeneratorType::VerticalShafts) -//============================================================================= -// Solid rock carved by hash-placed full-height vertical shafts (cylinders), a deterministic -// drainage tree of horizontal connector tunnels rooted at the origin spine, additive random -// connector loops, and partial ledges inside them. Emphasises climbing and falling. - -float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float WorldZ, - const FVerticalShaftParams& Params) const -{ - const float StrateHeight = Params.StrateTopWorldZ - Params.StrateBottomWorldZ; - if (StrateHeight <= 0.0f) return 1.0f; - - const float Spacing = FMath::Max(Params.ShaftSpacing, 1.0f); - const FVector Pos(WorldX, WorldY, WorldZ); - const uint32 S = (uint32)Seed ^ 0x53686674u; // 'Shft' - - float Density = Params.BaseDensity; // start solid - - const int32 CX = FMath::FloorToInt(WorldX / Spacing); - const int32 CY = FMath::FloorToInt(WorldY / Spacing); - - // The shaft topology is pure, but it is deliberately split into two windows: - // * COLLECT a deterministic halo once during this thread-local cache rebuild. This is the - // source data needed to resolve every 5×5 parent window and the fixed ±3 fallback window. - // * EVALUATE only the INNER 3×3 shaft cylinders. Tree links are collected from a wider - // rebuild-only child window and culled to the current cell before the voxel loop sees - // them. That matters: a capsule from a child to a parent two or three cells away must be - // present in the cells along its segment, not only in the cache centred on the child. - struct FLocalShaft { float X, Y, R; int32 CellX, CellY; bool bOriginSpine; }; - struct FLocalConn - { - FVector A, B; - float Radius = 0.0f; - float FloorZ = 0.0f; - bool bWalkableFloor = false; - }; - thread_local TArray> Shafts; - thread_local TArray> Conns; - thread_local TArray> TreeEmitShafts; - thread_local int32 VS_CX = INT32_MAX, VS_CY = INT32_MAX; - thread_local uint32 VS_Seed = 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 || S != VS_Seed || Spacing != VS_Spacing || - Params.ShaftDensity != VS_Dens || Params.ShaftMinRadius != VS_MinR || Params.ShaftMaxRadius != VS_MaxR || - Params.CrossConnectChance != VS_Cross || Params.ConnectorRadius != VS_ConnR || - Params.SurfaceRoughness != VS_Rough || OriginSpineRadius != VS_SpineR || - Params.StrateBottomWorldZ != VS_BotZ || Params.StrateTopWorldZ != VS_TopZ || - Params.BoundarySealThickness != VS_Seal - || Params.LedgeSpacing != VS_LedgeSpacing || Params.LedgeDepth != VS_LedgeDepth) - { - VS_CX = CX; VS_CY = CY; VS_Seed = S; VS_Spacing = Spacing; - VS_Dens = Params.ShaftDensity; VS_MinR = Params.ShaftMinRadius; VS_MaxR = Params.ShaftMaxRadius; - VS_Cross = Params.CrossConnectChance; VS_ConnR = Params.ConnectorRadius; - VS_Rough = Params.SurfaceRoughness; VS_SpineR = OriginSpineRadius; - VS_BotZ = Params.StrateBottomWorldZ; VS_TopZ = Params.StrateTopWorldZ; - VS_Seal = Params.BoundarySealThickness; - VS_LedgeSpacing = Params.LedgeSpacing; - VS_LedgeDepth = Params.LedgeDepth; - Shafts.Reset(); - Conns.Reset(); - TreeEmitShafts.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 = Params.StrateBottomWorldZ + Params.BoundarySealThickness; - const float TopZ = Params.StrateTopWorldZ - Params.BoundarySealThickness; - const float RoughnessReach = FMath::Max(Params.SurfaceRoughness, 0.0f) - * VOXEL_NOISE_SCALE * 1.5f; - // A tree link must remain open at the centreline after the SDF roughness pass. The +1 - // margin makes the radius strictly greater than the proven roughness supremum. - const float TreeConnectorRadius = FMath::Max(Params.ConnectorRadius, RoughnessReach + 1.0f); - // 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. The - // default is a 4-cell tree-emission radius + 3-cell collection pad = 7-cell radius - // (15×15 rolls; the emitted tree-child box itself is 9×9). - const int32 ConnectorCellPad = FMath::Max( - 1, FMath::CeilToInt(TreeConnectorRadius / Spacing)); - const int32 TreeEmitRadius = FallbackRadius + ConnectorCellPad; - const int32 CollectRadius = TreeEmitRadius + FallbackRadius; - - const int32 CollectSide = CollectRadius * 2 + 1; - TArray> ShaftGrid; - TArray> ShaftPresent; - ShaftGrid.SetNum(CollectSide * CollectSide); - ShaftPresent.Init(0, CollectSide * CollectSide); - // Collect the wider deterministic neighbourhood once. Only the inner cells are retained - // in `Shafts` for the per-voxel cylinder/ledge work; `TreeEmitShafts` is still rebuilt - // before evaluation and its connectors are spatially culled below. - for (int32 dy = -CollectRadius; dy <= CollectRadius; dy++) - for (int32 dx = -CollectRadius; dx <= CollectRadius; dx++) - { - const int32 nx = CX + dx, ny = CY + dy; - const uint32 Hh = VoxelHash::Cell(nx, ny, S); - if (VoxelHash::ToFloat01(Hh) > Params.ShaftDensity) continue; - - const float JX = VoxelHash::ToFloat01(VoxelHash::Mix(Hh ^ 0x12345678u)); - const float JY = VoxelHash::ToFloat01(VoxelHash::Mix(Hh ^ 0x9ABCDEF0u)); - FLocalShaft Sh; - Sh.X = (nx + 0.15f + JX * 0.7f) * Spacing; - Sh.Y = (ny + 0.15f + JY * 0.7f) * Spacing; - Sh.R = FMath::Lerp(Params.ShaftMinRadius, Params.ShaftMaxRadius, - VoxelHash::ToFloat01(VoxelHash::Mix(Hh ^ 0xBEEFu))); - Sh.CellX = nx; - Sh.CellY = ny; - Sh.bOriginSpine = false; - const int32 GridIndex = (dy + CollectRadius) * CollectSide + (dx + CollectRadius); - ShaftGrid[GridIndex] = Sh; - ShaftPresent[GridIndex] = 1; - if (FMath::Abs(dx) <= EmitRadius && FMath::Abs(dy) <= EmitRadius) - { - Shafts.Add(Sh); - } - if (FMath::Abs(dx) <= TreeEmitRadius && FMath::Abs(dy) <= TreeEmitRadius) - { - TreeEmitShafts.Add(Sh); - } - } - - auto FindCollectedShaft = [&](int32 CellX, int32 CellY) -> const FLocalShaft* - { - 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; - }; - - // The structural (0,0) spine is not re-carved here. It participates only as a - // connector endpoint. The local 3x3 set is origin-adjacent when its centre cell is in - // the central 3x3; the structural post remains the sole owner of the vertical column. - const bool bOriginNearby = FMath::Abs(CX) <= 1 && FMath::Abs(CY) <= 1; - if (bOriginNearby && OriginSpineRadius > 0.0f) - { - Shafts.Add({0.0f, 0.0f, OriginSpineRadius, 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 FLocalConn& 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 FLocalShaft& Child, const FLocalShaft* Parent) - { - const float Zc = VoxelPassageGeometry::VerticalShaftConnectorCenterZ( - Params.StrateBottomWorldZ, - BottomZ, - TopZ, - Params.LedgeSpacing, - Params.LedgeDepth, - TreeConnectorRadius); - const float FloorZ = VoxelPassageGeometry::VerticalShaftConnectorFloorZ( - Params.StrateBottomWorldZ, - BottomZ, - TopZ, - Params.LedgeSpacing, - Params.LedgeDepth, - TreeConnectorRadius); - const FVector ParentPoint = Parent != nullptr - ? FVector(Parent->X, Parent->Y, Zc) - : FVector(0.0f, 0.0f, Zc); - const FLocalConn Conn{ - FVector(Child.X, Child.Y, Zc), ParentPoint, TreeConnectorRadius, - FloorZ, true }; - if (ConnectorMayReachCell(Conn)) - { - Conns.Add(Conn); - } - }; - - // Structural drainage tree: every real shaft in the rebuild-only child window first - // selects one strictly more-central shaft from its complete 5×5 window. If that window - // has a local minimum, select the nearest shaft in the collected deterministic halo whose - // origin distance is lower; that shaft is already on a strictly descending path by the - // same rule. A shaft whose window reaches the origin uses the spine directly, and the - // final direct-spine fallback covers an unusually empty finite halo. Thus every emitted - // parent edge 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 is evaluating it. - // The explicit cell-coordinate tie-break makes this independent of array iteration order; - // the synthetic spine is not a candidate shaft. - for (const FLocalShaft& Child : TreeEmitShafts) - { - if (Child.bOriginSpine) continue; - - const float ChildOriginSq = FMath::Square(Child.X) + FMath::Square(Child.Y); - const FLocalShaft* Parent = nullptr; - float BestDistanceSq = FLT_MAX; - for (int32 dy = -CandidateRadius; dy <= CandidateRadius; ++dy) - for (int32 dx = -CandidateRadius; dx <= CandidateRadius; ++dx) - { - const FLocalShaft* 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)) - { - // This is the specified neighbour-shaft fallback. Restricting it to a lower - // origin distance makes the parent graph well-founded even though the fallback - // candidate need not be in the child's 5×5 window. - for (int32 dy = -FallbackRadius; dy <= FallbackRadius; ++dy) - for (int32 dx = -FallbackRadius; dx <= FallbackRadius; ++dx) - { - const FLocalShaft* 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 probabilistic links remain as texture and loops. They intentionally stay on - // the cached inner 3×3 working set; only the structural tree build needs the 9×9/15×15 halo. - if (Params.CrossConnectChance > 0.0f && Shafts.Num() >= 2) - { - for (int32 i = 0; i < Shafts.Num(); i++) - for (int32 j = i + 1; j < Shafts.Num(); j++) - { - const FLocalShaft& A = Shafts[i]; - const FLocalShaft& B = Shafts[j]; - const bool bSpineConnector = A.bOriginSpine || B.bOriginSpine; - const float DSq = FMath::Square(A.X - B.X) + FMath::Square(A.Y - B.Y); - if (DSq > FMath::Square(Spacing * 1.6f)) continue; // only neighbours - - // Symmetric pair hash from quantised endpoints. - const uint32 PH = VoxelHash::Pair( - FMath::RoundToInt(A.X), FMath::RoundToInt(A.Y), - FMath::RoundToInt(B.X), FMath::RoundToInt(B.Y), S ^ 0xC04Eu); - if (VoxelHash::ToFloat01(PH) >= Params.CrossConnectChance) continue; - - const float Zc = FMath::Lerp(BottomZ, TopZ, VoxelHash::ToFloat01(VoxelHash::Mix(PH))); - const float ConnectorR = bSpineConnector - ? FMath::Max( - Params.ConnectorRadius, - RoughnessReach + 1.0f) - : Params.ConnectorRadius; - const FLocalConn Conn{ - FVector(A.X, A.Y, Zc), FVector(B.X, B.Y, Zc), ConnectorR, - 0.0f, false }; - if (ConnectorMayReachCell(Conn)) - { - Conns.Add(Conn); - } - } - } - } - - float CaveSDF = FLT_MAX; - - // Vertical shafts as infinite cylinders (boundary seal handles the ends). - for (const FLocalShaft& Sh : 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); - } - for (const FLocalConn& C : Conns) - { - CaveSDF = FMath::Min(CaveSDF, VoxelSDF::Capsule(Pos, C.A, C.B, C.Radius)); - } - - // Wall roughness. - if (Params.SurfaceRoughness > 0.0f && CaveSDF < Params.SurfaceRoughness + 4.0f) - { - CaveSDF += FractalNoise3D(FVector3f(WorldX * 0.1f, WorldY * 0.1f, WorldZ * 0.1f), VoxelGenLOD::Eff(3)) - * VOXEL_NOISE_SCALE * Params.SurfaceRoughness; - } - - // Carve air inside shafts/connectors. - const float Blend = 2.0f; - if (CaveSDF < Blend) - { - float Carve = FMath::Clamp((Blend - CaveSDF) / (Blend * 2.0f), 0.0f, 1.0f); - Carve = SmoothStep01(Carve); - Density -= Carve * Params.BaseDensity * 2.0f; - } - - // Partial ledges inside shafts: thin shelves on one side at LedgeSpacing intervals, - // leaving the opposite side open so the shaft stays traversable. - if (Params.LedgeSpacing > 0.0f && Params.LedgeDepth > 0.0f && CaveSDF < 0.0f && Shafts.Num() > 0) - { - const float Phase = FMath::Frac((WorldZ - Params.StrateBottomWorldZ) / Params.LedgeSpacing); - const float BandT = FMath::Min(Phase, 1.0f - Phase) * Params.LedgeSpacing; // dist to nearest band - if (BandT < Params.LedgeDepth) - { - // Nearest shaft center → only shelf the +X/+Y half so a climb path remains. - const FLocalShaft* Near = nullptr; float BestSq = FLT_MAX; - for (const FLocalShaft& Sh : 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) - { - float Shelf = 1.0f - SmoothStep01(BandT / Params.LedgeDepth); - Density = FMath::Max(Density, Shelf * Params.BaseDensity); - } - } - } - - // The drainage tree is a walkable shaft network, not only a connected air SDF. Reassert its - // air core after partial shaft ledges, then emit support bands at the common first ledge level - // so every parent edge can be traversed by the player-fit stencil. This remains local to - // shaft edges; no landing-to-origin road is added. - for (const FLocalConn& C : Conns) - { - if (C.bWalkableFloor) - { - VoxelPassageGeometry::VF_ApplyVerticalShaftConnectorAir( - Density, WorldX, WorldY, WorldZ, - C.A, C.B, C.Radius, C.FloorZ, Params.BaseDensity); - } - } - for (const FLocalConn& C : Conns) - { - if (C.bWalkableFloor) - { - VoxelPassageGeometry::VF_ApplyVerticalShaftConnectorFloor( - Density, WorldX, WorldY, WorldZ, - C.A, C.B, C.Radius, C.FloorZ, Params.BaseDensity); - } - } - - ApplyOriginSpine(Density, WorldX, WorldY, WorldZ, - Params.StrateTopWorldZ, Params.StrateBottomWorldZ, - Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius); - - ApplyBoundarySeal(Density, WorldZ, - Params.StrateTopWorldZ, Params.StrateBottomWorldZ, - Params.BoundarySealThickness, Params.BaseDensity); - - if (StrateManager) - { - StrateManager->ApplyPassageModifier( - Density, WorldX, WorldY, WorldZ, - Params.BaseDensity, Params.BoundarySealThickness); - } - ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ, - Params.StrateTopWorldZ, Params.StrateBottomWorldZ, - Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius); - - VF_ApplyXYEdgeSeal(Density, WorldX, WorldY, - WorldRadiusVoxels, EdgeSealThickness, Params.BaseDensity); - - return -Density; -} - -//============================================================================= -// FLOATING-ISLAND GENERATOR (ECaveGeneratorType::FloatingIslands) -//============================================================================= -// A large open void with hash-placed island blobs (flattened-top ellipsoids, rough -// undersides) floating at jittered heights. Top/bottom sealed so the void encloses. - -float UVoxelGenerator::GetFloatingIslandDensity(float WorldX, float WorldY, float WorldZ, - const FFloatingIslandParams& Params) const -{ - const float H = Params.StrateTopWorldZ - Params.StrateBottomWorldZ; - if (H <= 0.0f) return 1.0f; - - const float Spacing = FMath::Max(Params.IslandSpacing, 1.0f); - const uint32 S = (uint32)Seed ^ 0x49736C64u; // 'Isld' - const float BlendK = FMath::Max(Params.SDFBlendRadius, 0.01f); - - float Density = -Params.BaseDensity; // start as open air (void) - - const int32 CX = FMath::FloorToInt(WorldX / Spacing); - const int32 CY = FMath::FloorToInt(WorldY / Spacing); - - const float MidZ = (Params.StrateTopWorldZ + Params.StrateBottomWorldZ) * 0.5f; - - float IslandSDF = FLT_MAX; - - // IRREGULAR OUTLINE: domain-warp the horizontal query so island edges are lobed and - // organic instead of perfect circles. Computed once per voxel and shared by all nearby - // islands (each samples a different part of the field → distinct silhouettes). - const float WarpAmp = (Params.IslandMinRadius + Params.IslandMaxRadius) * 0.5f * 0.35f; - // ⚠️ AUDIT §C1 — ne pas écrire l'offset de seed `(float)S * K` : à Seed = 2e9 le terme atteint - // ~1.4e6, où l'ULP du float vaut 0.125 contre un pas de 0.04 par voxel — le warp s'aplatit et - // les îles redeviennent des cercles parfaits. `VoxelHash::SeedOffset` est utilisé dans les DEUX - // chemins (ici et FIslandBlobSource), pour que le test d'équivalence reste un oracle valable. - // NOTE : `SeedOffset` quantifie la clé de site par ×100, donc 0.0007 → site 0. Unique aujourd'hui - // (toutes les autres clés du plugin sont ≥ 0.19) ; la prochaine clé sous 0.005 devra en choisir - // une autre plutôt que de collisionner en silence. - const float WX = WorldX + FractalNoise3D(FVector3f(WorldX * 0.04f + VoxelHash::SeedOffset(S, 0.0007f), WorldY * 0.04f, WorldZ * 0.012f), VoxelGenLOD::Eff(3)) - * VOXEL_NOISE_SCALE * WarpAmp; - const float WY = WorldY + FractalNoise3D(FVector3f(WorldX * 0.04f + 31.0f, WorldY * 0.04f + 7.0f, WorldZ * 0.012f), VoxelGenLOD::Eff(3)) - * VOXEL_NOISE_SCALE * WarpAmp; - - // Per-island constants (existence roll, jitter, radius, Z anchor, taper) are pure functions - // of (cell, seed, params) yet were re-hashed PER VOXEL. Bake the 3×3 neighbourhood's islands - // once per centre cell (thread_local); the per-voxel work keeps only the warped-frame - // distance / taper / top-surface math (those genuinely vary per voxel). - struct FLocalIsland { float X, Y, Rxy, TopHalf, TopZ, BotZ, TaperEnd; }; - thread_local TArray> Islands; - thread_local int32 FI_CX = INT32_MAX, FI_CY = INT32_MAX; - thread_local uint32 FI_Seed = 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_BotZ = FLT_MAX, FI_TopZ = FLT_MAX, - FI_Seal = -1.0f; - - if (CX != FI_CX || CY != FI_CY || S != FI_Seed || Spacing != FI_Spacing || - Params.IslandDensity != FI_Dens || Params.IslandMinRadius != FI_MinR || Params.IslandMaxRadius != FI_MaxR || - Params.ThicknessRatio != FI_Thick || Params.VerticalJitter != FI_VJit || - Params.StrateBottomWorldZ != FI_BotZ || Params.StrateTopWorldZ != FI_TopZ - || Params.BoundarySealThickness != FI_Seal) - { - FI_CX = CX; FI_CY = CY; FI_Seed = S; FI_Spacing = Spacing; - FI_Dens = Params.IslandDensity; FI_MinR = Params.IslandMinRadius; FI_MaxR = Params.IslandMaxRadius; - FI_Thick = Params.ThicknessRatio; FI_VJit = Params.VerticalJitter; - FI_BotZ = Params.StrateBottomWorldZ; FI_TopZ = Params.StrateTopWorldZ; - FI_Seal = Params.BoundarySealThickness; - Islands.Reset(); - - for (int32 dy = -1; dy <= 1; dy++) - for (int32 dx = -1; dx <= 1; dx++) - { - const int32 nx = CX + dx, ny = CY + dy; - const uint32 Hh = VoxelHash::Cell(nx, ny, S); - if (VoxelHash::ToFloat01(Hh) > Params.IslandDensity) continue; - - const float JX = VoxelHash::ToFloat01(VoxelHash::Mix(Hh ^ 0x12345678u)); - const float JY = VoxelHash::ToFloat01(VoxelHash::Mix(Hh ^ 0x9ABCDEF0u)); - - FLocalIsland& Isl = Islands.AddDefaulted_GetRef(); - Isl.X = (nx + 0.15f + JX * 0.7f) * Spacing; - Isl.Y = (ny + 0.15f + JY * 0.7f) * Spacing; - Isl.Rxy = FMath::Lerp(Params.IslandMinRadius, Params.IslandMaxRadius, - VoxelHash::ToFloat01(VoxelHash::Mix(Hh ^ 0x5A5Au))); - - // ASYMMETRIC ISLAND PROFILE: a fairly flat land slab on TOP, and an underside that - // tapers DOWN to a rough point (the hanging "roots"). ThicknessRatio scales how deep - // the underside hangs. This is what reads as a floating island vs. a sphere. - Isl.TopHalf = Isl.Rxy * 0.20f; // land slab above centre - const float UnderDepth = Isl.Rxy * FMath::Max(Params.ThicknessRatio, 0.25f); // tapering underside - - const float SpreadZ = FMath::Max(H * 0.5f - FMath::Max(Isl.TopHalf, UnderDepth) - Params.BoundarySealThickness, 0.0f) - * Params.VerticalJitter; - const float Cz = MidZ + VoxelHash::ToFloatSigned(VoxelHash::Mix(Hh ^ 0xB17Du)) * SpreadZ; - Isl.TopZ = Cz + Isl.TopHalf; - Isl.BotZ = Cz - UnderDepth; - - // Per-island taper sharpness (SmoothStep end point) for varied silhouettes. - Isl.TaperEnd = FMath::Lerp(0.45f, 0.7f, VoxelHash::ToFloat01(VoxelHash::Mix(Hh ^ 0x7A1Eu))); - } - } - - for (const FLocalIsland& Isl : Islands) - { - // Horizontal distance in the WARPED (lobed) frame so the outline isn't a circle. - const float Dxw = WX - Isl.X, Dyw = WY - Isl.Y; - const float DistXY = FMath::Sqrt(Dxw * Dxw + Dyw * Dyw); - - // Radius envelope by height: full width across the top, narrowing to a point at the - // bottom tip (SmoothStep taper). - 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; - - // Top surface: flat by default; dome the edges down when TopFlatten < 1. - float TopSurf = Isl.TopZ; - if (Params.TopFlatten < 1.0f) - { - const float Edge = FMath::Clamp(DistXY / FMath::Max(Isl.Rxy, 1.0f), 0.0f, 1.0f); - TopSurf = Isl.TopZ - (1.0f - Params.TopFlatten) * Isl.TopHalf * 2.0f * Edge * Edge; - } - - // Pseudo-SDF: outside if beyond the radial envelope OR above the top surface. - const float Sdf = FMath::Max(DistXY - Env, WorldZ - TopSurf); - - IslandSDF = VoxelSDF::SmoothMin(IslandSDF, Sdf, BlendK); - } - - // Craggy shells. - if (Params.SurfaceRoughness > 0.0f && IslandSDF < Params.SurfaceRoughness + BlendK + 2.0f) - { - IslandSDF += FractalNoise3D(FVector3f(WorldX * 0.08f, WorldY * 0.08f, WorldZ * 0.08f), VoxelGenLOD::Eff(4)) - * VOXEL_NOISE_SCALE * Params.SurfaceRoughness; - } - - // Fill solid inside islands. - const float Blend = BlendK; - if (IslandSDF < Blend) - { - float Fill = FMath::Clamp((Blend - IslandSDF) / (Blend * 2.0f), 0.0f, 1.0f); - Fill = SmoothStep01(Fill); - Density += Fill * Params.BaseDensity * 2.0f; - } - - ApplyOriginSpine(Density, WorldX, WorldY, WorldZ, - Params.StrateTopWorldZ, Params.StrateBottomWorldZ, - Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius); - - ApplyBoundarySeal(Density, WorldZ, - Params.StrateTopWorldZ, Params.StrateBottomWorldZ, - Params.BoundarySealThickness, Params.BaseDensity); - - if (StrateManager) - { - StrateManager->ApplyPassageModifier( - Density, WorldX, WorldY, WorldZ, - Params.BaseDensity, Params.BoundarySealThickness); - } - ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ, - Params.StrateTopWorldZ, Params.StrateBottomWorldZ, - Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius); - - VF_ApplyXYEdgeSeal(Density, WorldX, WorldY, - WorldRadiusVoxels, EdgeSealThickness, Params.BaseDensity); - - return -Density; -} diff --git a/Source/VoxelForge/Public/VoxelDensityOpStack.h b/Source/VoxelForge/Public/VoxelDensityOpStack.h index de48e5d..73cc481 100644 --- a/Source/VoxelForge/Public/VoxelDensityOpStack.h +++ b/Source/VoxelForge/Public/VoxelDensityOpStack.h @@ -190,10 +190,10 @@ public: return EvalSample(WorldX, WorldY, WorldZ).Density; } - /** L'état COMPLET (densité + SDF) après toute la pile. Diagnostic : quand une comparaison - * avec l'ancien chemin diverge, c'est le canal SDF qui dit si l'écart naît avant ou après - * la conversion. / The full state after the stack — the SDF channel is what says whether a - * divergence is born before or after the carve. */ + /** L'état COMPLET (densité + SDF) après toute la pile. Diagnostic : quand deux évaluations + * divergent, c'est le canal SDF qui dit si l'écart naît avant ou après la conversion. + * / The full state after the stack — the SDF channel is what says whether a divergence is + * born before or after the carve. */ FVoxelOpSample EvalSample(float WorldX, float WorldY, float WorldZ) const { FVoxelOpSample S; @@ -578,9 +578,8 @@ namespace VoxelDensityOps * FlatPlain ET CrystalChamber — la même pile, **sans branchement sur le type** : * SlabVoidSource → GridColumnMod → [structural post ×4] * - * C'est le premier vrai gain du refactor (OPSTACK-PLAN §4) : deux des huit archétypes - * disparaissent dans un opérateur, et leur différence redevient ce qu'elle était déjà dans - * `GetSlabDensity` — un jeu de valeurs par défaut, pas du code. + * Deux des huit archétypes tiennent dans un opérateur (OPSTACK-PLAN §4) : leur différence est + * un jeu de valeurs par défaut, pas du code. */ VOXELFORGE_API void BuildSlabStack(FVoxelOpStack& OutStack, const FSlabGenerationParams& P, int32 Seed, float SpineRadius, @@ -592,10 +591,8 @@ namespace VoxelDensityOps * ConstantRock → ShaftField → SdfRoughness → SdfCarve → ShaftLedge * → ShaftConnectorAir → ShaftConnectorFloor → [structural post ×4] * - * C'est la démonstration que `§2.5` promettait : dans le `switch`, Maze et VerticalShafts sont - * deux fonctions de ~100 lignes sans rien de commun à l'œil ; en opérateurs, ce sont les mêmes - * trois ops avec une source différente et d'autres réglages (fréquence 0.1 au lieu de 0.12, - * fenêtre `rough + 4` au lieu de `R + rough + 2`). + * Trois de ces ops sont ceux de Maze (`§2.5`), avec une source différente et d'autres réglages + * (fréquence 0.1 au lieu de 0.12, fenêtre `rough + 4` au lieu de `R + rough + 2`). */ VOXELFORGE_API void BuildVerticalShaftStack(FVoxelOpStack& OutStack, const FVerticalShaftParams& P, int32 Seed, float SpineRadius, diff --git a/Source/VoxelForge/Public/VoxelGenerator.h b/Source/VoxelForge/Public/VoxelGenerator.h index c2baf04..11405ed 100644 --- a/Source/VoxelForge/Public/VoxelGenerator.h +++ b/Source/VoxelForge/Public/VoxelGenerator.h @@ -3,8 +3,8 @@ // // Pipeline density-only: plus de grille de blocs, plus de surface heightfield. // Le mesher appelle GetDensityAt() par voxel pour reconstruire la géométrie. -// Toute la logique de caves vit dans GetDensityWithParams / GetSlabDensity, -// pilotée par les params de strate récupérés auprès du StrateManager. +// Chaque strate est générée par sa pile d'opérateurs (VoxelDensityOpStack.h), pilotée par les +// params de strate récupérés auprès du StrateManager. #pragma once @@ -202,7 +202,7 @@ public: /** * Densité pour une strate TunnelNetwork (rooms + tunnels + worm noise). - * Utilisée en interne par GetDensityAt quand la strate est de ce type. + * C'est l'évaluateur fusionné de la pile TunnelNetwork/Underwater dans GetDensityAt. * Exposée pour permettre des tests isolés avec des params custom. * * ⚠️ `ParamsFingerprint` ET `LayoutVersion` SONT OBLIGATOIRES (`AUDIT §C2`). Le cache SDF @@ -242,56 +242,8 @@ public: bool bCollectFusedDiagnostics = false, // Optional same-sample hand-off from EvaluateSDFCached. This is // only a room-shape ownership bit; it does not alter the scalar - // result and avoids a duplicate room-index query in native paths. - bool* OutRoomOwnsBottom = nullptr, - // Optional same-sample warped position. Native GetDensityAt uses - // this to feed the core post without applying the cave warp twice. - FVector* OutWarpedPosition = nullptr) const; - - /** - * Densité pour une strate Slab (FlatPlain / CrystalChamber). - * Vide horizontal entre un sol bruité et un plafond bruité. - */ - float GetSlabDensity(float WorldX, float WorldY, float WorldZ, - const FSlabGenerationParams& Params) const; - - /** - * Densité pour une strate Maze — couloirs sur un treillis 3D déterministe, avec les décisions - * parent/boucle reconstruites par cellule dans un cache thread-local; le hot path ne parcourt - * que les capsules déjà émises. - */ - float GetMazeDensity(float WorldX, float WorldY, float WorldZ, - const FMazeGenerationParams& Params) const; - - /** - * Densité pour une strate SurfaceWorld — terrain à ciel ouvert (collines, - * montagnes, plages) sous un plafond solide, avec nappe d'eau optionnelle. - * - * Biome output-blend: the heightfield is evaluated with ParamsD (the voxel's dominant - * biome) and, when NeighborWeight > 0, also with ParamsN (its nearest neighbour); the - * two surface HEIGHTS are lerped. This stays seamless across ANY param difference - * (frequencies included). With no biomes, pass ParamsD == ParamsN and weight 0 → - * bit-identical to the single-param terrain. Structural fields (Z bounds, seal, base, - * water level) must be equal in both (forced from the strate). - */ - float GetSurfaceDensity(float WorldX, float WorldY, float WorldZ, - const FSurfaceGenerationParams& ParamsD, - const FSurfaceGenerationParams& ParamsN, - float NeighborWeight) const; - - /** - * Densité pour une strate VerticalShafts — puits verticaux pleine hauteur, - * vires horizontales, et connecteurs horizontaux occasionnels. - */ - float GetVerticalShaftDensity(float WorldX, float WorldY, float WorldZ, - const FVerticalShaftParams& Params) const; - - /** - * Densité pour une strate FloatingIslands — masses de terre suspendues dans - * un grand vide ouvert (îles flottantes), sommets aplatis, dessous rugueux. - */ - float GetFloatingIslandDensity(float WorldX, float WorldY, float WorldZ, - const FFloatingIslandParams& Params) const; + // result and avoids a duplicate room-index query. + bool* OutRoomOwnsBottom = nullptr) const; //========================================================================= // CLIMATE & BIOME FIELDS (pure XY, deterministic, window-invariant) @@ -308,20 +260,19 @@ public: * La chaîne de hauteur complète de SurfaceWorld : structural → cliff → terrace → layer lines → * plage. Rend une ALTITUDE monde en voxels, pas une densité. * - * PUBLIQUE pour la même raison que `GetSlabDensity` / `GetMazeDensity` : permettre un test - * isolé. C'est la référence de `VoxelForge.OpStack.SurfaceHeightEquivalence`, qui compare la - * pile d'opérateurs de hauteur (`VoxelHeightOp.h`) à cette fonction point par point. - * Public so the height-op stack can be measured against it — same reason as GetSlabDensity. + * PUBLIQUE pour permettre un test isolé : c'est la référence de + * `VoxelForge.OpStack.SurfaceHeightEquivalence`, qui compare la pile d'opérateurs de hauteur + * (`VoxelHeightOp.h`) à cette fonction point par point. + * Public so the height-op stack can be measured against it. */ float ComputeSurfaceTerrainZ(float WorldX, float WorldY, const FSurfaceGenerationParams& Params) const; /** * La colonne de surface : terrain Z, plafond, et le gate d'OVERHANG résolu par colonne - * (amplitude + direction amont). PUBLIQUES toutes deux pour la même raison que ci-dessus : - * c'est le seul chemin qui calcule l'overhang — `GetSurfaceDensity` passe `OverhangAmp = 0` — - * donc c'est la seule référence possible pour `FOverhangShelfMod`. - * Public because this is the ONLY path that computes the overhang (GetSurfaceDensity passes 0), - * so it is the only possible reference for the ported op. + * (amplitude + direction amont). C'est la colonne que `ClassifyTile` teste pour SurfaceWorld ; + * publique pour que les tests puissent placer des échantillons dans la fenêtre d'overhang. + * The surface column ClassifyTile tests for SurfaceWorld; public so tests can aim samples at + * the overhang window. */ void ComputeSurfaceColumn(float WorldX, float WorldY, int32 ChunkZ, const FSurfaceGenerationParams& BaseSurface, const FBiomeContext& BiomeCtx, @@ -329,12 +280,6 @@ public: float& OutTerrainZ, float& OutCeilSurf, float& OutOverhangAmp, float& OutDirX, float& OutDirY) const; - /** Le combine par voxel : colonne → densité, overhang compris, puis le post structurel. */ - float SurfaceDensityFromColumn(float WorldX, float WorldY, float WorldZ, - float TerrainZ, float CeilSurf, - float OverhangAmp, float DirX, float DirY, - const FSurfaceGenerationParams& S) const; - /** * Moisture field at a world XY → [0,1]. The second climate axis for biome placement. */ @@ -467,11 +412,6 @@ private: FSurfaceGenerationParams& OutSurface, FBiomeContext& OutBiomeCtx, TArray& OutBiomeParams) const; - // ComputeSurfaceColumn et SurfaceDensityFromColumn ont été DÉPLACÉES en `public` (voir plus - // haut) : c'est le seul chemin qui calcule l'overhang, donc la seule référence possible pour - // VoxelForge.OpStack.SurfaceHeightEquivalence. Une seule déclaration chacune. - // Moved to public above — the only path that computes the overhang, hence the only oracle. - /** (Re)build the per-chunk biome cell grid covering chunk (X,Y) footprint + margin. */ void RebuildBiomeGrid(int32 ChunkX, int32 ChunkY, int32 ChunkZ, const FBiomeContext& Ctx, FChunkBiomeCache& Cache,