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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01URDGLsu7c4nunqNFPJEFPU
This commit is contained in:
15 files changed
+252
-1697
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
@@ -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<FSurfaceGenerationParams> 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);
|
||||
|
||||
|
||||
@@ -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<FVector> Probes;
|
||||
AddMazeBoundaryProbes(Params, Bottom, Top, Probes);
|
||||
TArray<FVector> 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (bInterior && New < 0.0f) { ++NumInsideIsland; } // solide loin des seals ⇒ une île
|
||||
if (bInterior && New >= 0.0f) { ++NumOpenVoid; }
|
||||
}
|
||||
if ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSideDisagree; }
|
||||
}
|
||||
|
||||
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);
|
||||
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<int32> 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));
|
||||
|
||||
|
||||
@@ -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."));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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; }
|
||||
// air ⇒ dans un puits/connecteur/étagère / air ⇒ inside a shaft, connector or ledge
|
||||
if (EvalStackMCForTest(Stack, X, Y, Z) >= 0.0f) { ++NumInsideShaft; }
|
||||
}
|
||||
if ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSideDisagree; }
|
||||
}
|
||||
|
||||
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<float>(CellX) + 0.37f) * PerfSpacing;
|
||||
LastY = (static_cast<float>(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;
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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<UVoxelGenerator> SourceOnlyGenerator(
|
||||
NewObject<UVoxelGenerator>(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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<FVector>& OutPoints)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<IVoxelDensityOp> 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<FConstantFieldSource>(-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<FShaftFieldSource> 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<FShaftLedgeMod>(P, ShaftPtr));
|
||||
@@ -9544,8 +9519,8 @@ namespace VoxelDensityOps
|
||||
|
||||
OutStack.Add(MakeConstantVoidSource(P.BaseDensity));
|
||||
OutStack.Add(MakeUnique<FIslandBlobSource>(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));
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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,
|
||||
|
||||
@@ -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<FSurfaceGenerationParams>& 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,
|
||||
|
||||
Reference in new issue
Block a user