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)"),
|
TestEqual(TEXT("no tile was classified uniform while containing a surface (a false verdict is a hole)"),
|
||||||
NumHoles, 0);
|
NumHoles, 0);
|
||||||
|
|
||||||
// ── Stabilité du verdict : ClassifyTile partage GSurfColCache avec GetDensityAt, donc le
|
// ── Stabilité du verdict : le premier ClassifyTile a réchauffé son cache de colonnes et le
|
||||||
// brute-force ci-dessus a réchauffé les caches. Re-classifier doit rendre le MÊME verdict.
|
// brute-force ci-dessus les caches de GetDensityAt. Re-classifier doit rendre le MÊME verdict.
|
||||||
// Verdict stability: ClassifyTile shares GSurfColCache with GetDensityAt, so the brute force
|
// Verdict stability: the first ClassifyTile warmed its column cache and the brute force
|
||||||
// above warmed the caches. Re-classifying must yield the SAME verdict.
|
// above warmed GetDensityAt's caches. Re-classifying must yield the SAME verdict.
|
||||||
for (const FTileSpec& Spec : ToVerify)
|
for (const FTileSpec& Spec : ToVerify)
|
||||||
{
|
{
|
||||||
const EVoxelTileClass A = Gen->ClassifyTile(Spec.Origin, Spec.Step, Spec.Cells);
|
const EVoxelTileClass A = Gen->ClassifyTile(Spec.Origin, Spec.Step, Spec.Cells);
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
// seam, and in multiplayer a world divergence.
|
// seam, and in multiplayer a world divergence.
|
||||||
//
|
//
|
||||||
// POURQUOI CE TEST EXISTE / WHY THIS TEST EXISTS:
|
// POURQUOI CE TEST EXISTE / WHY THIS TEST EXISTS:
|
||||||
// ~30 caches thread_local à clé manuelle vivent sous GetDensityAt (CP_*, GSurfColCache, les
|
// ~30 caches thread_local à clé manuelle vivent sous GetDensityAt (CP_*, les mémos par cellule
|
||||||
// slots de diff, le cache SDF). Chacun est correct exactement tant que sa CLÉ contient toutes
|
// 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 :
|
// 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 —
|
// 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
|
// 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("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("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("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("FChunkBiomeCache validity box (which says nothing about the FBiomeContext its ")
|
||||||
TEXT("cells were classified against)."), Probes.Num()));
|
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
|
// 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.
|
// 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
|
// D'où une seconde famille, `VoxelHeightOp.h`, mesurée ici contre `ComputeSurfaceTerrainZ`.
|
||||||
// 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.
|
|
||||||
//
|
//
|
||||||
// ⚠️ CE QUE CE TEST COUVRE
|
// ⚠️ CE QUE CE TEST COUVRE
|
||||||
// ✅ la pile de HAUTEUR du sol, contre `ComputeSurfaceTerrainZ` (défauts puis tous les ops F20) ;
|
// ✅ 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.
|
// ✅ le mélange synthétique à poids zéro.
|
||||||
//
|
//
|
||||||
// The old GetSurfaceDensity helper remains a differential diagnostic only: it does not carry the
|
// LA BARRE : **bit à bit** pour les opérations en espace hauteur.
|
||||||
// current common structural post. The assertions target the height stack and the density stack
|
// The bar is bit-identity for height-space operations.
|
||||||
// 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.
|
|
||||||
|
|
||||||
#if WITH_DEV_AUTOMATION_TESTS
|
#if WITH_DEV_AUTOMATION_TESTS
|
||||||
|
|
||||||
@@ -285,18 +277,9 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters)
|
|||||||
// ÉTAPE 2a — LE PONT VERS L'ESPACE DENSITÉ
|
// ÉTAPE 2a — LE PONT VERS L'ESPACE DENSITÉ
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// `FSurfaceColumnSource` consomme les DEUX piles de hauteur (sol + voûte) et rend une 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**
|
// Cette passe est la variante **sans overhang** et **sans biomes** ; l'overhang a sa propre
|
||||||
// (il passe `OverhangAmp = 0`) et **sans biomes** (ParamsD == ParamsN, poids 0) — donc la
|
// passe juste en dessous. / No-overhang, no-biome variant; the overhang has its own pass below.
|
||||||
// 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.
|
|
||||||
{
|
{
|
||||||
// ⚠️ `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;
|
FSurfaceGenerationParams P = AllOps;
|
||||||
P.OverhangStrength = 0.0f;
|
P.OverhangStrength = 0.0f;
|
||||||
|
|
||||||
@@ -318,8 +301,7 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters)
|
|||||||
Ctx.StrateBottomWorldZ = P.StrateBottomWorldZ;
|
Ctx.StrateBottomWorldZ = P.StrateBottomWorldZ;
|
||||||
Stack.PrepareChunk(Ctx);
|
Stack.PrepareChunk(Ctx);
|
||||||
|
|
||||||
int32 NumDiff = 0, NumSideDisagree = 0, WorstIdx = -1, NumNonFinite = 0;
|
int32 NumNonFinite = 0;
|
||||||
float WorstDelta = 0.0f;
|
|
||||||
|
|
||||||
FRandomStream Rng(5150);
|
FRandomStream Rng(5150);
|
||||||
for (int32 i = 0; i < NumHeightSamples; ++i)
|
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 X = (float)Rng.RandRange(-4 * CHUNK_SIZE, 4 * CHUNK_SIZE);
|
||||||
const float Y = (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);
|
const float Z = (float)Rng.RandRange(BottomVoxelZ, TopVoxelZ);
|
||||||
|
if (!FMath::IsFinite(Stack.EvalMC(X, Y, Z)))
|
||||||
// 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))
|
|
||||||
{
|
{
|
||||||
++NumNonFinite;
|
++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"),
|
TestEqual(TEXT("surface: current density stack has no non-finite samples"),
|
||||||
NumNonFinite, 0);
|
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` résout le gate et la direction amont par colonne ; la moitié des
|
||||||
// `ComputeSurfaceColumn` (qui résout le gate et la direction amont par colonne) suivi de
|
// échantillons est placée DANS la fenêtre d'overhang qu'il rapporte, pour que l'op overhang de
|
||||||
// `SurfaceDensityFromColumn` (qui applique l'union par voxel). Les deux viennent d'être
|
// la pile soit réellement exercé. / Half the samples are placed inside the overhang window that
|
||||||
// exposées pour ça.
|
// ComputeSurfaceColumn reports, so the stack's overhang op is actually exercised.
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
{
|
{
|
||||||
FSurfaceGenerationParams P = AllOps;
|
FSurfaceGenerationParams P = AllOps;
|
||||||
P.OverhangStrength = 0.8f;
|
P.OverhangStrength = 0.8f;
|
||||||
@@ -391,8 +352,7 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters)
|
|||||||
TArray<FSurfaceGenerationParams> NoBiomeParams;
|
TArray<FSurfaceGenerationParams> NoBiomeParams;
|
||||||
FChunkBiomeCache BiomeCache;
|
FChunkBiomeCache BiomeCache;
|
||||||
|
|
||||||
int32 NumDiff = 0, NumSideDisagree = 0, NumInWindow = 0, NumNonFinite = 0;
|
int32 NumInWindow = 0, NumNonFinite = 0;
|
||||||
float WorstDelta = 0.0f;
|
|
||||||
|
|
||||||
FRandomStream Rng(1337);
|
FRandomStream Rng(1337);
|
||||||
for (int32 i = 0; i < NumHeightSamples; ++i)
|
for (int32 i = 0; i < NumHeightSamples; ++i)
|
||||||
@@ -418,27 +378,16 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters)
|
|||||||
Z = (float)Rng.RandRange(BottomVoxelZ, TopVoxelZ);
|
Z = (float)Rng.RandRange(BottomVoxelZ, TopVoxelZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
const float Old = Gen->SurfaceDensityFromColumn(X, Y, Z, TerrainZ, CeilSurf,
|
if (!FMath::IsFinite(Stack.EvalMC(X, Y, Z)))
|
||||||
Amp, DirX, DirY, P);
|
|
||||||
const float New = Stack.EvalMC(X, Y, Z);
|
|
||||||
if (!FMath::IsFinite(New))
|
|
||||||
{
|
{
|
||||||
++NumNonFinite;
|
++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(
|
AddInfo(FString::Printf(
|
||||||
TEXT("Overhang legacy diagnostic: %d of %d samples differ from "
|
TEXT("Overhang: %d of %d samples deliberately inside the overhang window; current "
|
||||||
"SurfaceDensityFromColumn (largest |delta| %.9g), %d side disagreements, "
|
"stack non-finite samples=%d."),
|
||||||
"%d deliberately inside the overhang window; current stack non-finite samples=%d."),
|
NumInWindow, NumHeightSamples, NumNonFinite));
|
||||||
NumDiff, NumHeightSamples, WorstDelta, NumSideDisagree, NumInWindow, NumNonFinite));
|
|
||||||
TestEqual(TEXT("overhang: current density stack has no non-finite samples"),
|
TestEqual(TEXT("overhang: current density stack has no non-finite samples"),
|
||||||
NumNonFinite, 0);
|
NumNonFinite, 0);
|
||||||
|
|
||||||
|
|||||||
@@ -108,36 +108,37 @@ bool FVoxelForgeMazeSeamTest::RunTest(const FString& Parameters)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int32 Top = 0, Bottom = 0;
|
int32 MazeTopZ = 0, MazeBottomZ = 0;
|
||||||
if (!World.GetSlotVoxelZRange(FTestWorld::SlotMaze, Top, Bottom))
|
if (!World.GetSlotVoxelZRange(FTestWorld::SlotMaze, MazeTopZ, MazeBottomZ))
|
||||||
{
|
{
|
||||||
AddError(TEXT("The seam fixture has no Maze slot."));
|
AddError(TEXT("The seam fixture has no Maze slot."));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const int32 MidChunkZ = ((Top + Bottom) / 2) / CHUNK_SIZE;
|
const int32 MazeMidChunkZ = ((MazeTopZ + MazeBottomZ) / 2) / CHUNK_SIZE;
|
||||||
const FMazeGenerationParams Params = World.StrateManager->GetMazeParamsForChunk(
|
const FMazeGenerationParams MazeParams = World.StrateManager->GetMazeParamsForChunk(
|
||||||
FIntVector(0, 0, MidChunkZ));
|
FIntVector(0, 0, MazeMidChunkZ));
|
||||||
|
|
||||||
TArray<FVector> Probes;
|
TArray<FVector> SeamProbes;
|
||||||
AddMazeBoundaryProbes(Params, Bottom, Top, Probes);
|
AddMazeBoundaryProbes(MazeParams, MazeBottomZ, MazeTopZ, SeamProbes);
|
||||||
|
|
||||||
int32 First = INDEX_NONE;
|
int32 FirstProbe = INDEX_NONE;
|
||||||
float Before = 0.0f, After = 0.0f;
|
float FirstBefore = 0.0f, FirstAfter = 0.0f;
|
||||||
const int32 Mismatches = CountContextMismatches(World, Probes, First, Before, After);
|
const int32 NumMismatches = CountContextMismatches(
|
||||||
|
World, SeamProbes, FirstProbe, FirstBefore, FirstAfter);
|
||||||
|
|
||||||
TestEqual(TEXT("Maze has no chunk-context seam mismatches"), Mismatches, 0);
|
TestEqual(TEXT("Maze has no chunk-context seam mismatches"), NumMismatches, 0);
|
||||||
TestTrue(TEXT("Maze seam test sampled cell-boundary probes"), Probes.Num() > 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"),
|
AddError(FString::Printf(TEXT("Maze seam mismatch at probe %d: %.9g -> %.9g"),
|
||||||
First, Before, After));
|
FirstProbe, FirstBefore, FirstAfter));
|
||||||
}
|
}
|
||||||
|
|
||||||
AddInfo(FString::Printf(
|
AddInfo(FString::Printf(
|
||||||
TEXT("Maze seam freedom: %d boundary probes, mismatches=%d; "
|
TEXT("Maze seam freedom: %d boundary probes, mismatches=%d; "
|
||||||
"local window=2x2x2 child nodes ({-1,0}^3), no wide collect."),
|
"local window=2x2x2 child nodes ({-1,0}^3), no wide collect."),
|
||||||
Probes.Num(), Mismatches));
|
SeamProbes.Num(), NumMismatches));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,18 @@
|
|||||||
// VoxelForgeOpStackIslandTest.cpp
|
// VoxelForgeOpStackIslandTest.cpp
|
||||||
// FloatingIslands — le portage qui fait tourner la pile À L'ENVERS.
|
// FloatingIslands — la pile qui tourne À L'ENVERS / the stack that runs BACKWARDS.
|
||||||
// FloatingIslands — the port that runs the stack BACKWARDS.
|
|
||||||
//
|
//
|
||||||
// CE QUE CELUI-CI PROUVE EN PLUS DES AUTRES
|
// Les autres archétypes partent de ROC et CREUSENT ; FloatingIslands part du VIDE et REMPLIT, avec
|
||||||
// `VerticalShaftEquivalence` a mesuré la réutilisation À L'IDENTIQUE : trois opérateurs de Maze
|
// les MÊMES opérateurs au signe près (convention interne positif = solide) :
|
||||||
// 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 :
|
|
||||||
//
|
//
|
||||||
// FConstantFieldSource(+Base) ←→ FConstantFieldSource(-Base)
|
// FConstantFieldSource(+Base) ←→ FConstantFieldSource(-Base)
|
||||||
// FSdfConvertOp(Sign = -1) ←→ FSdfConvertOp(Sign = +1)
|
// 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
|
// Le seul opérateur propre à l'archétype est le blob d'île. / The only archetype-specific op is the
|
||||||
// réutilise en s'INVERSANT est une preuve plus forte qu'un archétype qui se réutilise à l'identique
|
// island blob; the rest is Maze's rock + carve with the opposite sign.
|
||||||
// — le premier dit que l'abstraction a trouvé le bon axe, le second seulement que deux archétypes
|
|
||||||
// se ressemblaient.
|
|
||||||
//
|
//
|
||||||
// ET LE VERDICT DE BOÎTE : c'est ici que `ClassifyBox` peut rendre **AllAir** pour la première fois
|
// ET LE VERDICT DE BOÎTE : une strate d'îles flottantes est surtout vide, donc `ClassifyBox` doit
|
||||||
// de tout le plugin. Une strate d'îles flottantes est, par construction, surtout vide ; aucun
|
// savoir rendre **AllAir** (`OPSTACK-DECOMPOSITION §7`). Le test compte AllSolid et AllAir
|
||||||
// archétype de grotte n'a jamais su prouver « tout air » (`OPSTACK-DECOMPOSITION §7`). Le test
|
// SÉPARÉMENT, parce qu'un total agrégé masquerait exactement ce verdict-là.
|
||||||
// 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).
|
|
||||||
|
|
||||||
#if WITH_DEV_AUTOMATION_TESTS
|
#if WITH_DEV_AUTOMATION_TESTS
|
||||||
|
|
||||||
@@ -92,8 +80,8 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters)
|
|||||||
|
|
||||||
if (P.StrateTopWorldZ - P.StrateBottomWorldZ <= 0.0f)
|
if (P.StrateTopWorldZ - P.StrateBottomWorldZ <= 0.0f)
|
||||||
{
|
{
|
||||||
AddError(TEXT("The FloatingIslands strate has degenerate Z bounds, which sends ")
|
AddError(TEXT("The FloatingIslands strate has degenerate Z bounds; GetDensityAt builds no ")
|
||||||
TEXT("GetFloatingIslandDensity down its early-out. The op stack has none by design."));
|
TEXT("stack for it. The op stack has no degenerate-strate early-out by design."));
|
||||||
return false;
|
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
|
// 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
|
// 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 InnerBot = P.StrateBottomWorldZ + P.BoundarySealThickness;
|
||||||
const float InnerTop = P.StrateTopWorldZ - P.BoundarySealThickness;
|
const float InnerTop = P.StrateTopWorldZ - P.BoundarySealThickness;
|
||||||
|
|
||||||
int32 NumDiff = 0, NumSideDisagree = 0, WorstIdx = -1;
|
|
||||||
int32 NumInsideIsland = 0, NumOpenVoid = 0;
|
int32 NumInsideIsland = 0, NumOpenVoid = 0;
|
||||||
float WorstDelta = 0.0f;
|
|
||||||
|
|
||||||
for (int32 i = 0; i < NumIslandSamples; ++i)
|
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 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 float New = Stack.EvalMC(X, Y, Z);
|
||||||
|
|
||||||
const bool bInterior = (Z > InnerBot && Z < InnerTop);
|
const bool bInterior = (Z > InnerBot && Z < InnerTop);
|
||||||
if (bInterior && Old < 0.0f) { ++NumInsideIsland; } // solide loin des seals ⇒ une île
|
if (bInterior && New < 0.0f) { ++NumInsideIsland; } // solide loin des seals ⇒ une île
|
||||||
if (bInterior && Old >= 0.0f) { ++NumOpenVoid; }
|
if (bInterior && New >= 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 (NumDiff == 0)
|
|
||||||
{
|
|
||||||
AddInfo(FString::Printf(
|
AddInfo(FString::Printf(
|
||||||
TEXT("FloatingIslands: bit-identical across %d samples (%d inside island rock away from ")
|
TEXT("FloatingIslands: %d of %d samples inside island rock away from the seal bands, %d in ")
|
||||||
TEXT("the seal bands, %d in open void, so the void source, the blobs, the roughness and ")
|
TEXT("open void."),
|
||||||
TEXT("the fill were all exercised). The stack runs BACKWARDS -- void source + fill ")
|
NumInsideIsland, NumIslandSamples, NumOpenVoid));
|
||||||
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);
|
|
||||||
|
|
||||||
if (NumInsideIsland == 0)
|
if (NumInsideIsland == 0)
|
||||||
{
|
{
|
||||||
AddWarning(TEXT("No sample landed inside island rock away from the seal bands, so the blob ")
|
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("source and the fill were never meaningfully exercised. Raise IslandDensity ")
|
||||||
TEXT("above then only proves that two empty voids agree. Raise IslandDensity or ")
|
TEXT("or IslandMaxRadius."));
|
||||||
TEXT("IslandMaxRadius."));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// 2. INVARIANCE DE FENÊTRE
|
// 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é
|
// 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
|
// inclut délibérément `BoundarySealThickness`, que `SpreadZ` lit (voir la note dans
|
||||||
// (voir la note dans FIslandBlobSource::GetCells).
|
// FIslandBlobSource::GetCells).
|
||||||
{
|
{
|
||||||
std::atomic<int32> Impure{ 0 };
|
std::atomic<int32> Impure{ 0 };
|
||||||
const int32 NumBlocks = FMath::Max(4, FMath::Min(16, FPlatformMisc::NumberOfCores()));
|
const int32 NumBlocks = FMath::Max(4, FMath::Min(16, FPlatformMisc::NumberOfCores()));
|
||||||
@@ -296,9 +244,8 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters)
|
|||||||
AddInfo(FString::Printf(
|
AddInfo(FString::Printf(
|
||||||
TEXT("Box verdicts over 60 FloatingIslands tiles (XY sampled from +/- %d voxels = %.1f x ")
|
TEXT("Box verdicts over 60 FloatingIslands tiles (XY sampled from +/- %d voxels = %.1f x ")
|
||||||
TEXT("IslandSpacing %.0f): %d proved AllSolid, %d proved AllAir, ")
|
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("%d Mixed, %d voxels checked, %d violations. The AllAir count matters most: a ")
|
||||||
TEXT("thing: no cave archetype has ever been able to prove 'all air', and a floating-")
|
TEXT("floating-island strate is mostly open air (OPSTACK-DECOMPOSITION 7)."),
|
||||||
TEXT("island strate is mostly exactly that (OPSTACK-DECOMPOSITION 7)."),
|
|
||||||
SpanVoxels, (float)SpanVoxels / FMath::Max(P.IslandSpacing, 1.0f), P.IslandSpacing,
|
SpanVoxels, (float)SpanVoxels / FMath::Max(P.IslandSpacing, 1.0f), P.IslandSpacing,
|
||||||
NumProvedSolid, NumProvedAir, NumMixed, NumBruteSamples, NumUnsound));
|
NumProvedSolid, NumProvedAir, NumMixed, NumBruteSamples, NumUnsound));
|
||||||
|
|
||||||
|
|||||||
@@ -1,73 +1,17 @@
|
|||||||
// VoxelForgeOpStackMazeTest.cpp
|
// VoxelForgeOpStackMazeTest.cpp
|
||||||
// PHASE 1, LE TEST QUI COMPTE — la pile d'opérateurs Maze contre GetMazeDensity.
|
// La pile d'opérateurs Maze / The Maze operator stack.
|
||||||
// PHASE 1'S LOAD-BEARING TEST — the Maze operator stack against GetMazeDensity.
|
|
||||||
//
|
//
|
||||||
// CE QUE LA PHASE 1 DEVAIT PROUVER / WHAT PHASE 1 HAD TO PROVE
|
// Ce que ce test vérifie / What this test checks:
|
||||||
// Le déclencheur d'arrêt de `OPSTACK-PLAN §4` : **« est-ce que la séparation source / modifier tombe
|
// • TOPOLOGIE — le graphe parent orienté vers l'origine est un arbre couvrant (N-1 arêtes, zéro
|
||||||
// naturellement du code existant ? »** Réponse mesurée : oui. Maze se décompose en huit opérateurs
|
// cellule déconnectée), les boucles optionnelles restent bornées, et le graphe est déterministe
|
||||||
// sans contorsion, le SDF est reproduit BIT POUR BIT, et aucun échantillon ne change de côté de
|
// sur 64 seeds. / The origin-directed parent graph is a spanning tree, deterministic per seed.
|
||||||
// l'isosurface.
|
// • 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
|
||||||
// ✅ MISE À JOUR 2026-07-27 : LE PLANCHER ULP N'EXISTE PLUS. C'ÉTAIT `/fp:fast`.
|
// 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
|
||||||
// `FPSemantics = Precise` sur le module (AUDIT §C9, posé pour le cross-play Linux/Windows) fait
|
// trou), y compris pour une pile source/convertisseur jamais écrite à la main.
|
||||||
// passer ce test à **BIT-IDENTIQUE**. La section ci-dessous décrit un état RÉVOLU ; elle est gardée
|
// Every uniform box verdict survives brute force, including for a novel source/converter stack.
|
||||||
// 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.
|
|
||||||
|
|
||||||
#if WITH_DEV_AUTOMATION_TESTS
|
#if WITH_DEV_AUTOMATION_TESTS
|
||||||
|
|
||||||
@@ -355,13 +299,13 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMazeDensity court-circuite sur une strate dégénérée (`return 1.0f`). Cette garde appartient
|
// La pile suppose une strate valide ; `GetDensityAt` rend de l'air sans pile sur une strate
|
||||||
// à la fonction d'archétype, pas à un opérateur ; la pile suppose une strate valide.
|
// 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)
|
if (MazeParams.StrateTopWorldZ - MazeParams.StrateBottomWorldZ <= 0.0f)
|
||||||
{
|
{
|
||||||
AddError(FString::Printf(
|
AddError(FString::Printf(
|
||||||
TEXT("The Maze strate has degenerate Z bounds (top %.1f, bottom %.1f), which sends ")
|
TEXT("The Maze strate has degenerate Z bounds (top %.1f, bottom %.1f); GetDensityAt ")
|
||||||
TEXT("GetMazeDensity down its early-out. The op stack has no such early-out by design."),
|
TEXT("builds no stack for it. The op stack has no degenerate-strate early-out by design."),
|
||||||
MazeParams.StrateTopWorldZ, MazeParams.StrateBottomWorldZ));
|
MazeParams.StrateTopWorldZ, MazeParams.StrateBottomWorldZ));
|
||||||
return false;
|
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.
|
// 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.
|
// 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,
|
// Pour un slot de cave, ClassifyTile plie `ClassifyBox` de cette pile : tout nombre > 0 ici est
|
||||||
// VerticalShafts, FloatingIslands, FlatPlain, CrystalChamber et Underwater ne captent RIEN du
|
// une tuile que le mesher saute. / For a cave slot ClassifyTile folds this stack's ClassifyBox:
|
||||||
// gain T1.d. Tout nombre > 0 ici est le saut de tuile propre à la pile, pas une décision du
|
// every proved box here is a tile the mesher skips.
|
||||||
// classifieur historique.
|
|
||||||
{
|
{
|
||||||
int32 NumProved = 0, NumMixed = 0, NumUnsound = 0, NumBruteSamples = 0;
|
int32 NumProved = 0, NumMixed = 0, NumUnsound = 0, NumBruteSamples = 0;
|
||||||
FRandomStream Rng(24680);
|
FRandomStream Rng(24680);
|
||||||
@@ -689,17 +568,14 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
|
|||||||
|
|
||||||
AddInfo(FString::Printf(
|
AddInfo(FString::Printf(
|
||||||
TEXT("Box verdicts over %d Maze tiles (XY sampled from +/- %d voxels = %.1f x ")
|
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("CellSize %.0f): %d proved uniform, %d Mixed, %d voxels checked, %d violations."),
|
||||||
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."),
|
|
||||||
NumMazeBoxTests, SpanVoxels, (float)SpanVoxels / FMath::Max(MazeParams.CellSize, 1.0f), MazeParams.CellSize,
|
NumMazeBoxTests, SpanVoxels, (float)SpanVoxels / FMath::Max(MazeParams.CellSize, 1.0f), MazeParams.CellSize,
|
||||||
NumProved, NumMixed, NumBruteSamples, NumUnsound));
|
NumProved, NumMixed, NumBruteSamples, NumUnsound));
|
||||||
|
|
||||||
if (NumProved == 0)
|
if (NumProved == 0)
|
||||||
{
|
{
|
||||||
AddWarning(TEXT("The stack proved no tile uniform, so it is not yet better than today's ")
|
AddWarning(TEXT("The stack proved no tile uniform, so ClassifyTile skips no Maze tile. ")
|
||||||
TEXT("classifier for Maze. Not a correctness problem, but the perf case for ")
|
TEXT("Not a correctness problem, but a perf regression."));
|
||||||
TEXT("the port rests on this number."));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
// VoxelForgeOpStackShaftTest.cpp
|
// VoxelForgeOpStackShaftTest.cpp
|
||||||
// VerticalShafts — le portage qui teste la RÉUTILISATION et le contrat de l'opérateur courant.
|
// VerticalShafts — la RÉUTILISATION d'opérateurs et le contrat de la pile.
|
||||||
// VerticalShafts — the port that tests REUSE and the current operator contract.
|
// VerticalShafts — operator REUSE and the stack's contract.
|
||||||
//
|
//
|
||||||
// CE QUE CELUI-CI PROUVE EN PLUS DES AUTRES
|
// Trois des cinq opérateurs de VerticalShafts sont ceux de Maze, repris tels quels :
|
||||||
// Les portages précédents demandaient « la décomposition reproduit-elle l'original ? ». Celui-ci
|
// `ConstantRock`, `SdfRoughness`, `SdfCarve` — mêmes ops, autre source et autres réglages
|
||||||
// demande **« les opérateurs se RÉUTILISENT-ils vraiment entre archétypes ? »**, qui est la thèse
|
// (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` et la seule raison de faire ce refactor plutôt que de nettoyer le `switch`.
|
// 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
|
// Three of the five VerticalShafts ops are Maze's, reused unchanged. The assertions below target
|
||||||
// changement** : `ConstantRock`, `SdfRoughness`, `SdfCarve`. Dans le `switch`, `GetMazeDensity` et
|
// the stack used in-game: channel count/order, feature liveness, window invariance, cache cost and
|
||||||
// `GetVerticalShaftDensity` sont deux fonctions de ~100 lignes qui n'ont rien en commun à l'œil.
|
// box-proof soundness.
|
||||||
// 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.
|
|
||||||
|
|
||||||
#if WITH_DEV_AUTOMATION_TESTS
|
#if WITH_DEV_AUTOMATION_TESTS
|
||||||
|
|
||||||
@@ -93,8 +87,8 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
|
|||||||
|
|
||||||
if (P.StrateTopWorldZ - P.StrateBottomWorldZ <= 0.0f)
|
if (P.StrateTopWorldZ - P.StrateBottomWorldZ <= 0.0f)
|
||||||
{
|
{
|
||||||
AddError(TEXT("The VerticalShafts strate has degenerate Z bounds, which sends ")
|
AddError(TEXT("The VerticalShafts strate has degenerate Z bounds; GetDensityAt builds no ")
|
||||||
TEXT("GetVerticalShaftDensity down its early-out. The op stack has none by design."));
|
TEXT("stack for it. The op stack has no degenerate-strate early-out by design."));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,49 +122,17 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
|
|||||||
}
|
}
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// 1. CURRENT-OWNER DIAGNOSTIC
|
// 1. LIVENESS — des échantillons tombent dans l'air des puits / samples land in shaft air
|
||||||
// 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.
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
int32 NumDiff = 0, NumSideDisagree = 0, WorstIdx = -1, NumInsideShaft = 0;
|
int32 NumInsideShaft = 0;
|
||||||
float WorstDelta = 0.0f;
|
|
||||||
|
|
||||||
for (int32 i = 0; i < NumShaftSamples; ++i)
|
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 X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (float)Points[i].Z;
|
||||||
|
// air ⇒ dans un puits/connecteur/étagère / air ⇒ inside a shaft, connector or ledge
|
||||||
const float Old = Gen->GetVerticalShaftDensity(X, Y, Z, P);
|
if (EvalStackMCForTest(Stack, X, Y, Z) >= 0.0f) { ++NumInsideShaft; }
|
||||||
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; }
|
AddInfo(FString::Printf(TEXT("VerticalShafts: %d of %d samples are air in the stack."),
|
||||||
}
|
NumInsideShaft, NumShaftSamples));
|
||||||
|
|
||||||
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));
|
|
||||||
TestTrue(TEXT("shaft feature sampling exercised the operator stack"), NumInsideShaft > 0);
|
TestTrue(TEXT("shaft feature sampling exercised the operator stack"), NumInsideShaft > 0);
|
||||||
|
|
||||||
if (NumInsideShaft == 0)
|
if (NumInsideShaft == 0)
|
||||||
@@ -268,14 +230,14 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
|
|||||||
const int32 CellY = (i / 16) - 8;
|
const int32 CellY = (i / 16) - 8;
|
||||||
LastX = (static_cast<float>(CellX) + 0.37f) * PerfSpacing;
|
LastX = (static_cast<float>(CellX) + 0.37f) * PerfSpacing;
|
||||||
LastY = (static_cast<float>(CellY) + 0.61f) * 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 RebuildSeconds = FPlatformTime::Seconds() - RebuildStart;
|
||||||
|
|
||||||
const double HotStart = FPlatformTime::Seconds();
|
const double HotStart = FPlatformTime::Seconds();
|
||||||
for (int32 i = 0; i < NumHotSamples; ++i)
|
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;
|
const double HotSeconds = FPlatformTime::Seconds() - HotStart;
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,20 @@
|
|||||||
// VoxelForgeOpStackSlabTest.cpp
|
// VoxelForgeOpStackSlabTest.cpp
|
||||||
// PHASE 2, PREMIER PORTAGE — la pile Slab courante, sur LES DEUX archétypes.
|
// La pile Slab, sur LES DEUX archétypes / The Slab operator stack, on BOTH archetypes.
|
||||||
// PHASE 2'S FIRST PORT — the current Slab operator stack, on BOTH archetypes.
|
|
||||||
//
|
//
|
||||||
// CE QUE CE TEST DOIT PROUVER / WHAT THIS TEST HAS TO PROVE
|
// 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
|
// 1. LE CHEMIN DU JEU — la pile rend une densité finie, invariante à la fenêtre (ordre et
|
||||||
// guard the path used by the game. `GetSlabDensity` is retained as a legacy diagnostic only:
|
// threads), et ses verdicts de boîte sont conservatifs.
|
||||||
// it still owns the former full passage/support tail, while the operator stack is followed by
|
// The stack's density is finite and window-invariant, and its box verdicts are conservative.
|
||||||
// the current common MC post.
|
// 2. UN OPÉRATEUR, DEUX ARCHÉTYPES — la MÊME pile est vérifiée pour FlatPlain ET CrystalChamber.
|
||||||
// 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.
|
|
||||||
// ⚠️ La fixture ne règle que `GeneratorType`, donc les deux slots portent des params PAR
|
// ⚠️ 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
|
// 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
|
// 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
|
// 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.
|
// 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
|
// 3. LE VERDICT DE BOÎTE — les deux surfaces sont XY-PURES, donc leurs bornes en Z sont connues
|
||||||
// FlatPlain et CrystalChamber aujourd'hui. Depuis que les deux surfaces sont XY-PURES, leurs
|
// exactement (contrat [-1,1] de FBM) : toute tuile entièrement sous le sol ou entre les deux
|
||||||
// bornes en Z sont connues exactement (contrat [-1,1] de FBM), donc toute tuile entièrement
|
// bandes se prouve SANS échantillonner.
|
||||||
// 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.
|
|
||||||
|
|
||||||
#if WITH_DEV_AUTOMATION_TESTS
|
#if WITH_DEV_AUTOMATION_TESTS
|
||||||
|
|
||||||
@@ -73,14 +61,13 @@ bool FVoxelForgeOpStackSlabTest::RunTest(const FString& Parameters)
|
|||||||
int32 TopVoxelZ, int32 BottomVoxelZ,
|
int32 TopVoxelZ, int32 BottomVoxelZ,
|
||||||
int32 SlotIndex, const TCHAR* SlotName)
|
int32 SlotIndex, const TCHAR* SlotName)
|
||||||
{
|
{
|
||||||
// `GetSlabDensity` court-circuite sur une strate dégénérée (`return 1.0f`). Cette garde
|
// La pile suppose une strate valide ; `GetDensityAt` rend de l'air sans pile sur une strate
|
||||||
// appartient à la fonction d'archétype, pas à un opérateur ; la pile suppose une strate
|
// dégénérée. / The stack assumes a valid strate; GetDensityAt returns air for a degenerate one.
|
||||||
// valide, et `GetDensityAt` retombe sur le `switch` dans ce cas.
|
|
||||||
if (SlabParams.StrateTopWorldZ - SlabParams.StrateBottomWorldZ <= 0.0f)
|
if (SlabParams.StrateTopWorldZ - SlabParams.StrateBottomWorldZ <= 0.0f)
|
||||||
{
|
{
|
||||||
AddError(FString::Printf(
|
AddError(FString::Printf(
|
||||||
TEXT("%s has degenerate Z bounds (top %.1f, bottom %.1f), which sends GetSlabDensity ")
|
TEXT("%s has degenerate Z bounds (top %.1f, bottom %.1f); GetDensityAt builds no ")
|
||||||
TEXT("down its early-out. The op stack has no such early-out by design."),
|
TEXT("stack for it. The op stack has no degenerate-strate early-out by design."),
|
||||||
SlotName, SlabParams.StrateTopWorldZ, SlabParams.StrateBottomWorldZ));
|
SlotName, SlabParams.StrateTopWorldZ, SlabParams.StrateBottomWorldZ));
|
||||||
return;
|
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;
|
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)
|
for (int32 i = 0; i < NumSlabSamples; ++i)
|
||||||
{
|
{
|
||||||
const float X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (float)Points[i].Z;
|
if (!FMath::IsFinite(Stack.EvalMC((float)Points[i].X, (float)Points[i].Y, (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))
|
|
||||||
{
|
{
|
||||||
++NumNonFinite;
|
++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),
|
TestEqual(*FString::Printf(TEXT("%s: current stack has no non-finite density"), SlotName),
|
||||||
NumNonFinite, 0);
|
NumNonFinite, 0);
|
||||||
|
|
||||||
@@ -300,9 +213,9 @@ bool FVoxelForgeOpStackSlabTest::RunTest(const FString& Parameters)
|
|||||||
|
|
||||||
AddInfo(FString::Printf(
|
AddInfo(FString::Printf(
|
||||||
TEXT("%s box verdicts over %d tiles (XY sampled from +/- %d voxels = %.1f x ")
|
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("ColumnSpacing %.0f): %d proved uniform, %d Mixed, %d voxels checked, %d violations. ")
|
||||||
TEXT("ClassifyTile proves ZERO of these. This number is the whole point of making ")
|
TEXT("This number is the whole point of making the slab surfaces XY-pure ")
|
||||||
TEXT("the slab surfaces XY-pure (OPSTACK-DECOMPOSITION 3.1)."),
|
TEXT("(OPSTACK-DECOMPOSITION 3.1)."),
|
||||||
SlotName, NumSlabTiles, SpanVoxels,
|
SlotName, NumSlabTiles, SpanVoxels,
|
||||||
(float)SpanVoxels / FMath::Max(SlabParams.ColumnSpacing, 1.0f), SlabParams.ColumnSpacing,
|
(float)SpanVoxels / FMath::Max(SlabParams.ColumnSpacing, 1.0f), SlabParams.ColumnSpacing,
|
||||||
NumProved, NumMixed, NumBruteSamples, NumUnsound));
|
NumProved, NumMixed, NumBruteSamples, NumUnsound));
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "Misc/AutomationTest.h"
|
#include "Misc/AutomationTest.h"
|
||||||
|
|
||||||
#include "VoxelCaveMorphology.h"
|
#include "VoxelCaveMorphology.h"
|
||||||
|
#include "VoxelDensityOpStack.h"
|
||||||
#include "VoxelDensityPrimitives.h"
|
#include "VoxelDensityPrimitives.h"
|
||||||
#include "VoxelForgeTestFixture.h"
|
#include "VoxelForgeTestFixture.h"
|
||||||
#include "VoxelPassageGeometry.h"
|
#include "VoxelPassageGeometry.h"
|
||||||
@@ -900,11 +901,27 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
|
|||||||
++NumFootingChecked;
|
++NumFootingChecked;
|
||||||
|
|
||||||
// The live world's passage modifier is intentionally absent here. The endpoint is
|
// 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
|
// already known to be passage-carved, so the archetype's own stack, built without a
|
||||||
// source itself: air at the landing and solid immediately below it.
|
// strate manager (hence without passage carving), checks the source itself: air at
|
||||||
TStrongObjectPtr<UVoxelGenerator> SourceOnlyGenerator(
|
// the landing and solid immediately below it.
|
||||||
NewObject<UVoxelGenerator>(GetTransientPackage(), NAME_None, RF_Transient));
|
const UVoxelGenerator& LiveGenerator = *World.Generator;
|
||||||
SourceOnlyGenerator->InitializeSettings(World.Settings.Get());
|
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 LandingDensity = 0.0f;
|
||||||
float FootingDensity = 0.0f;
|
float FootingDensity = 0.0f;
|
||||||
@@ -915,13 +932,16 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
|
|||||||
FMazeGenerationParams P = Definition->MazeParams;
|
FMazeGenerationParams P = Definition->MazeParams;
|
||||||
P.StrateTopWorldZ = StrateTopZ;
|
P.StrateTopWorldZ = StrateTopZ;
|
||||||
P.StrateBottomWorldZ = StrateBottomZ;
|
P.StrateBottomWorldZ = StrateBottomZ;
|
||||||
LandingDensity = SourceOnlyGenerator->GetMazeDensity(
|
VoxelDensityOps::BuildMazeStack(SourceStack, P, LiveGenerator.Seed,
|
||||||
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z, P);
|
LiveGenerator.OriginSpineRadius, /*StrateManager*/nullptr);
|
||||||
|
SourceStack.PrepareChunk(SourceContext);
|
||||||
|
LandingDensity = EvalSource(
|
||||||
|
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z);
|
||||||
const float FloorProbeZ = SuggestedPoint.Z
|
const float FloorProbeZ = SuggestedPoint.Z
|
||||||
- FMath::Max(P.CorridorRadius, 0.5f)
|
- FMath::Max(P.CorridorRadius, 0.5f)
|
||||||
- P.SurfaceRoughness * VOXEL_NOISE_SCALE * 1.5f - 3.0f;
|
- P.SurfaceRoughness * VOXEL_NOISE_SCALE * 1.5f - 3.0f;
|
||||||
FootingDensity = SourceOnlyGenerator->GetMazeDensity(
|
FootingDensity = EvalSource(
|
||||||
SuggestedPoint.X, SuggestedPoint.Y, FloorProbeZ, P);
|
SuggestedPoint.X, SuggestedPoint.Y, FloorProbeZ);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -930,11 +950,14 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
|
|||||||
FVerticalShaftParams P = Definition->VerticalShaftParams;
|
FVerticalShaftParams P = Definition->VerticalShaftParams;
|
||||||
P.StrateTopWorldZ = StrateTopZ;
|
P.StrateTopWorldZ = StrateTopZ;
|
||||||
P.StrateBottomWorldZ = StrateBottomZ;
|
P.StrateBottomWorldZ = StrateBottomZ;
|
||||||
LandingDensity = SourceOnlyGenerator->GetVerticalShaftDensity(
|
VoxelDensityOps::BuildVerticalShaftStack(SourceStack, P, LiveGenerator.Seed,
|
||||||
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z, P);
|
LiveGenerator.OriginSpineRadius, /*StrateManager*/nullptr);
|
||||||
|
SourceStack.PrepareChunk(SourceContext);
|
||||||
|
LandingDensity = EvalSource(
|
||||||
|
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z);
|
||||||
const float FloorProbeZ = StrateBottomZ + P.BoundarySealThickness * 0.5f;
|
const float FloorProbeZ = StrateBottomZ + P.BoundarySealThickness * 0.5f;
|
||||||
FootingDensity = SourceOnlyGenerator->GetVerticalShaftDensity(
|
FootingDensity = EvalSource(
|
||||||
SuggestedPoint.X, SuggestedPoint.Y, FloorProbeZ, P);
|
SuggestedPoint.X, SuggestedPoint.Y, FloorProbeZ);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -943,10 +966,13 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
|
|||||||
FFloatingIslandParams P = Definition->FloatingIslandParams;
|
FFloatingIslandParams P = Definition->FloatingIslandParams;
|
||||||
P.StrateTopWorldZ = StrateTopZ;
|
P.StrateTopWorldZ = StrateTopZ;
|
||||||
P.StrateBottomWorldZ = StrateBottomZ;
|
P.StrateBottomWorldZ = StrateBottomZ;
|
||||||
LandingDensity = SourceOnlyGenerator->GetFloatingIslandDensity(
|
VoxelDensityOps::BuildFloatingIslandStack(SourceStack, P, LiveGenerator.Seed,
|
||||||
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z, P);
|
LiveGenerator.OriginSpineRadius, /*StrateManager*/nullptr);
|
||||||
FootingDensity = SourceOnlyGenerator->GetFloatingIslandDensity(
|
SourceStack.PrepareChunk(SourceContext);
|
||||||
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z - 1.0f, P);
|
LandingDensity = EvalSource(
|
||||||
|
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z);
|
||||||
|
FootingDensity = EvalSource(
|
||||||
|
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z - 1.0f);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -230,8 +230,8 @@ namespace VoxelForgeTest
|
|||||||
/**
|
/**
|
||||||
* A spread of world sample points that deliberately crosses chunk boundaries, strate
|
* 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
|
* 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
|
* missing key input produces a wrong answer. Integer XY on purpose: that is the lattice the
|
||||||
* GetDensityAt's T1.a column cache actually takes (fractional XY bypasses the cache).
|
* mesher samples, and the one the per-column memos are keyed on.
|
||||||
*/
|
*/
|
||||||
inline void BuildSamplePoints(const FTestWorld& World, int32 Count, int32 Seed,
|
inline void BuildSamplePoints(const FTestWorld& World, int32 Count, int32 Seed,
|
||||||
TArray<FVector>& OutPoints)
|
TArray<FVector>& OutPoints)
|
||||||
|
|||||||
@@ -5303,9 +5303,9 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The slab source is an XY height band. Evaluate the same two pure height fields as
|
// 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
|
// 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(
|
bool VF_SuggestSlabLandingPoint(
|
||||||
const FSlabGenerationParams& Params,
|
const FSlabGenerationParams& Params,
|
||||||
int32 Seed,
|
int32 Seed,
|
||||||
@@ -5444,7 +5444,8 @@ namespace
|
|||||||
|
|
||||||
// Maze corridors are lattice edges. Only a horizontal edge is a useful landing source: a
|
// 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
|
// 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(
|
bool VF_SuggestMazeLandingPoint(
|
||||||
const FMazeGenerationParams& Params,
|
const FMazeGenerationParams& Params,
|
||||||
int32 Seed,
|
int32 Seed,
|
||||||
@@ -5879,7 +5880,7 @@ namespace
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Evaluate only the island source, in the same internal-density convention as
|
// 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
|
// 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
|
// 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.
|
// 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)
|
for (int32 IslandIndex = 0; IslandIndex < NumIslands; ++IslandIndex)
|
||||||
{
|
{
|
||||||
const FVFIslandSite& Island = Islands[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
|
// 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.
|
// can be found, but those distant blobs must not influence the returned footing.
|
||||||
if (FMath::Abs(Island.CellX - BaseCellX) > 1
|
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.
|
// Les opérateurs concrets : les décompositions d'archétypes + le post-traitement structurel.
|
||||||
// The concrete operators: the archetype decompositions + the structural post-process.
|
// 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
|
// Convention de signe INTERNE : positif = solide ; la pile négative une fois pour le MC.
|
||||||
// Chaque corps ci-dessous est une transcription LITTÉRALE du bloc correspondant de
|
// Internal sign convention: positive = solid; the stack negates once for MC.
|
||||||
// `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.
|
|
||||||
|
|
||||||
#include "VoxelDensityOpStack.h"
|
#include "VoxelDensityOpStack.h"
|
||||||
#include "VoxelStrateComposer.h"
|
#include "VoxelStrateComposer.h"
|
||||||
@@ -674,7 +664,7 @@ namespace
|
|||||||
, CorridorRadius(FMath::Max(P.CorridorRadius, 0.5f))
|
, CorridorRadius(FMath::Max(P.CorridorRadius, 0.5f))
|
||||||
, BranchProbability(P.BranchProbability)
|
, BranchProbability(P.BranchProbability)
|
||||||
, Verticality(P.Verticality)
|
, Verticality(P.Verticality)
|
||||||
, Salt((uint32)Seed ^ 0x4D617A65u) // 'Maze' — identique à GetMazeDensity
|
, Salt((uint32)Seed ^ 0x4D617A65u) // 'Maze'
|
||||||
{}
|
{}
|
||||||
|
|
||||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
|
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
|
* 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
|
* une course. La reconstruction seule inspecte le petit voisinage ; Eval ne refait aucune
|
||||||
* décision de parent ou de boucle.
|
* décision de parent ou de boucle.
|
||||||
@@ -829,16 +819,13 @@ namespace
|
|||||||
//=========================================================================
|
//=========================================================================
|
||||||
// RÔLE 1 — SOURCE : DALLE / SLAB VOID (FlatPlain ET CrystalChamber)
|
// RÔLE 1 — SOURCE : DALLE / SLAB VOID (FlatPlain ET CrystalChamber)
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// Transcription littérale des ÉTAPES 1-3 de `GetSlabDensity` : surface de sol, surface de
|
// Surface de sol, surface de plafond, puis `Density = -min(distAuSol, distAuPlafond)`.
|
||||||
// plafond, puis `Density = -min(distAuSol, distAuPlafond)`.
|
|
||||||
//
|
//
|
||||||
// DEUX archétypes, UN opérateur. `GetSlabDensity` est appelé pour FlatPlain et
|
// DEUX archétypes, UN opérateur, sans le moindre branchement sur le type — CrystalChamber
|
||||||
// CrystalChamber sans le moindre branchement sur le type — CrystalChamber n'est rien d'autre
|
// n'est rien d'autre que FlatPlain avec un `CeilingRoughness` plus grand (OPSTACK-PLAN §4) :
|
||||||
// que FlatPlain avec un `CeilingRoughness` plus grand. C'est le premier vrai gain du refactor
|
// la différence entre eux est un jeu de valeurs par défaut.
|
||||||
// (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.
|
|
||||||
//
|
//
|
||||||
// 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.
|
// 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
|
// 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)
|
// 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
|
// 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 = TerrainZ - Z ← solide sous le sol
|
||||||
// Density = max(Density, Z - CeilSurf) ← solide au-dessus de la voûte
|
// 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
|
// 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à
|
// `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
|
// 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.
|
// 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,
|
// Le cache 3×3 par cellule est `thread_local`, ce que la note de threading de VoxelDensityOp.h
|
||||||
// et c'est exactement ce que la note de threading de VoxelDensityOp.h autorise). Sa clé
|
// autorise. Sa clé
|
||||||
// contient tous les paramètres qui influent sur le résultat + le seed, donc un changement de
|
// 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
|
// 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).
|
// des colonnes identiques (cf. AUDIT C2 — la clé est complète, pas seulement le coord).
|
||||||
@@ -1657,7 +1644,7 @@ namespace
|
|||||||
private:
|
private:
|
||||||
struct FSlabColumn { float X, Y, R; };
|
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,
|
/** 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. */
|
* 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`.
|
// 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
|
// 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
|
// opérateur au signe près, exactement comme la source constante au-dessus.
|
||||||
// que le `switch` ne pouvait pas montrer : les deux blocs y sont à 900 lignes l'un de l'autre.
|
|
||||||
//
|
//
|
||||||
// ⚠️ `Sign` vaut ±1.0f et rien d'autre. La multiplication par ±1 est EXACTE en IEEE-754, donc
|
// ⚠️ `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
|
// `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:
|
public:
|
||||||
FShaftFieldSource(const FVerticalShaftParams& InP, int32 Seed, float InSpineRadius)
|
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) {}
|
, SpineRadius(InSpineRadius) {}
|
||||||
|
|
||||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
|
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
|
/** 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
|
* halo 9×9 émetteur / 15×15 total pour résoudre les fenêtres parent 5×5 et le fallback
|
||||||
* qui influent (comme l'original) : stable à travers les reconstructions de pile, ce qui
|
* fixe ±3. Clé = cellule + tous les params qui influent : stable à travers les
|
||||||
* est la leçon retenue du mémo de colonne de SurfaceWorld. */
|
* 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 FCells& GetCells(float WorldX, float WorldY) const
|
||||||
{
|
{
|
||||||
const float Spacing = FMath::Max(P.ShaftSpacing, 1.0f);
|
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
|
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||||
{
|
{
|
||||||
if (P.LedgeSpacing <= 0.0f || P.LedgeDepth <= 0.0f || Field == nullptr) { return; }
|
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
|
// ⚠️ La garde est `Sdf < 0` — le SDF APRÈS rugosité, tel que la pile l'a laissé. C'est
|
||||||
// l'a laissé. C'est bien `InOut.Sdf` ici, pas une re-évaluation : re-calculer donnerait
|
// bien `InOut.Sdf` ici, pas une re-évaluation : re-calculer donnerait le SDF SANS
|
||||||
// le SDF SANS rugosité et déplacerait les étagères.
|
// rugosité et déplacerait les étagères.
|
||||||
if (InOut.Sdf >= 0.0f) { return; }
|
if (InOut.Sdf >= 0.0f) { return; }
|
||||||
|
|
||||||
const float Phase = FMath::Frac((WorldZ - P.StrateBottomWorldZ) / P.LedgeSpacing);
|
const float Phase = FMath::Frac((WorldZ - P.StrateBottomWorldZ) / P.LedgeSpacing);
|
||||||
@@ -2886,7 +2872,7 @@ namespace
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
FIslandBlobSource(const FFloatingIslandParams& InP, int32 Seed)
|
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; }
|
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
|
* • 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
|
* 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
|
* 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 »
|
* l'axe. Le confondre avec « rien en dessous » serait un TROU, et c'est pourquoi seule
|
||||||
* serait un TROU, et c'est pourquoi seule la borne HAUTE est testée.
|
* la borne HAUTE est testée.
|
||||||
* • cette source ne couvre pas l'aval : elle publie seulement son propre intervalle SDF.
|
* • 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
|
* 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é.
|
* opérateurs dans le pliage, chacun avec son contrat isolé.
|
||||||
@@ -3059,8 +3045,7 @@ namespace
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
/** Tirage d'une cellule. PURE en (cellule, seed, params) ⇒ `Eval` et `EffectOverBox` ne
|
/** 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
|
* peuvent pas voir des îles différentes. */
|
||||||
* `GetFloatingIslandDensity`. */
|
|
||||||
bool RollIsland(int32 nx, int32 ny, FIsland& Out) const
|
bool RollIsland(int32 nx, int32 ny, FIsland& Out) const
|
||||||
{
|
{
|
||||||
const float H = P.StrateTopWorldZ - P.StrateBottomWorldZ;
|
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
|
* ⚠️ LA CLÉ INCLUT `BoundarySealThickness` : `SpreadZ` s'en sert
|
||||||
* (`H·0.5 − max(TopHalf, UnderDepth) − Seal`), donc dans `GetFloatingIslandDensity` une
|
* (`H·0.5 − max(TopHalf, UnderDepth) − Seal`), donc sans lui une édition à chaud qui ne
|
||||||
* édition à chaud qui ne change QUE l'épaisseur de seal sert des îles périmées. Même famille
|
* change QUE l'épaisseur de seal servirait des îles périmées. Même famille que `AUDIT §C2` :
|
||||||
* que `AUDIT §C2` : une clé de cache incomplète ne se voit pas, elle produit du terrain
|
* une clé de cache incomplète ne se voit pas, elle produit du terrain plausible.
|
||||||
* plausible. Ajouter le champ ne coûte
|
|
||||||
* qu'un recalcul, jamais une valeur différente — donc l'égalité binaire tient.
|
|
||||||
*
|
*
|
||||||
* The key includes BoundarySealThickness, which the original omits although SpreadZ reads it.
|
* The key includes BoundarySealThickness because SpreadZ reads it; without it a live edit
|
||||||
* Adding it can only cost a recompute, never change a value — bit-equality is unaffected.
|
* of the seal thickness alone would serve stale islands.
|
||||||
*/
|
*/
|
||||||
const FCells& GetCells(float WorldX, float WorldY) const
|
const FCells& GetCells(float WorldX, float WorldY) const
|
||||||
{
|
{
|
||||||
@@ -9326,8 +9309,8 @@ namespace VoxelDensityOps
|
|||||||
|
|
||||||
TUniquePtr<IVoxelDensityOp> MakeConstantVoidSource(float BaseDensity)
|
TUniquePtr<IVoxelDensityOp> MakeConstantVoidSource(float BaseDensity)
|
||||||
{
|
{
|
||||||
// `float Density = -Params.BaseDensity; // start as open air (void)` — la négation unaire
|
// `Density = -BaseDensity` : on part du vide. La négation unaire est exacte.
|
||||||
// est exacte, donc c'est littéralement la première ligne de GetFloatingIslandDensity.
|
// Start as open air; unary negation is exact.
|
||||||
return MakeUnique<FConstantFieldSource>(-BaseDensity);
|
return MakeUnique<FConstantFieldSource>(-BaseDensity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9391,10 +9374,8 @@ namespace VoxelDensityOps
|
|||||||
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
|
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
|
||||||
bool bAppendStructuralPosts)
|
bool bAppendStructuralPosts)
|
||||||
{
|
{
|
||||||
// DEUX archétypes entrent ici, aucun branchement ne les distingue — parce que
|
// DEUX archétypes entrent ici, aucun branchement ne les distingue : FlatPlain et
|
||||||
// `GetSlabDensity` n'en fait aucun non plus. FlatPlain et CrystalChamber ne diffèrent que
|
// CrystalChamber ne diffèrent que par leurs valeurs par défaut.
|
||||||
// par leurs valeurs par défaut, et c'est maintenant visible dans le code plutôt que dans
|
|
||||||
// un commentaire. 8 archétypes → 7.
|
|
||||||
OutStack.Add(MakeSlabVoidSource(P, Seed));
|
OutStack.Add(MakeSlabVoidSource(P, Seed));
|
||||||
OutStack.Add(MakeGridColumnMod(P, Seed));
|
OutStack.Add(MakeGridColumnMod(P, Seed));
|
||||||
|
|
||||||
@@ -9410,15 +9391,10 @@ namespace VoxelDensityOps
|
|||||||
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
|
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
|
||||||
bool bAppendStructuralPosts)
|
bool bAppendStructuralPosts)
|
||||||
{
|
{
|
||||||
// ⚠️ LA PREUVE QUE L'ABSTRACTION EST RÉELLE, et elle vaut d'être dite : TROIS des cinq
|
// TROIS des cinq opérateurs ci-dessous sont ceux de Maze, repris tels quels —
|
||||||
// opérateurs ci-dessous sont ceux de Maze, **repris sans une ligne de changement** —
|
// `ConstantRock`, `SdfRoughness`, `SdfCarve` — avec une source différente (`§2.5`).
|
||||||
// `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.
|
|
||||||
//
|
//
|
||||||
// THREE of the five ops below are Maze's, reused without a line changed. In the switch,
|
// THREE of the five ops below are Maze's, reused unchanged, with a different source.
|
||||||
// Maze and VerticalShafts are two unrelated ~100-line functions; as operators they are the
|
|
||||||
// same three ops with a different source.
|
|
||||||
constexpr float CarveBlend = 2.0f;
|
constexpr float CarveBlend = 2.0f;
|
||||||
|
|
||||||
TUniquePtr<FShaftFieldSource> ShaftSource =
|
TUniquePtr<FShaftFieldSource> ShaftSource =
|
||||||
@@ -9427,9 +9403,8 @@ namespace VoxelDensityOps
|
|||||||
|
|
||||||
OutStack.Add(MakeConstantRockSource(P.BaseDensity));
|
OutStack.Add(MakeConstantRockSource(P.BaseDensity));
|
||||||
OutStack.Add(MoveTemp(ShaftSource));
|
OutStack.Add(MoveTemp(ShaftSource));
|
||||||
// Fréquence 0.1 et fenêtre `SurfaceRoughness + 4` — les constantes de
|
// Fréquence 0.1 et fenêtre `SurfaceRoughness + 4` — PAS celles de Maze
|
||||||
// `GetVerticalShaftDensity`, PAS celles de Maze (0.12 / `R + rough + 2`). Même opérateur,
|
// (0.12 / `R + rough + 2`). Même opérateur, réglages différents.
|
||||||
// réglages différents : c'est le point.
|
|
||||||
OutStack.Add(MakeSdfRoughnessMod(P.SurfaceRoughness, 0.1f, 3, P.SurfaceRoughness + 4.0f));
|
OutStack.Add(MakeSdfRoughnessMod(P.SurfaceRoughness, 0.1f, 3, P.SurfaceRoughness + 4.0f));
|
||||||
OutStack.Add(MakeSdfCarve(CarveBlend, P.BaseDensity));
|
OutStack.Add(MakeSdfCarve(CarveBlend, P.BaseDensity));
|
||||||
OutStack.Add(MakeUnique<FShaftLedgeMod>(P, ShaftPtr));
|
OutStack.Add(MakeUnique<FShaftLedgeMod>(P, ShaftPtr));
|
||||||
@@ -9544,8 +9519,8 @@ namespace VoxelDensityOps
|
|||||||
|
|
||||||
OutStack.Add(MakeConstantVoidSource(P.BaseDensity));
|
OutStack.Add(MakeConstantVoidSource(P.BaseDensity));
|
||||||
OutStack.Add(MakeUnique<FIslandBlobSource>(P, Seed));
|
OutStack.Add(MakeUnique<FIslandBlobSource>(P, Seed));
|
||||||
// Fréquence 0.08 et 4 octaves — les constantes de `GetFloatingIslandDensity`. Quatrième
|
// Fréquence 0.08 et 4 octaves — les constantes de FloatingIslands (Maze 0.12/3,
|
||||||
// archétype à réutiliser cet opérateur (Maze 0.12/3, VerticalShafts 0.1/3).
|
// VerticalShafts 0.1/3).
|
||||||
OutStack.Add(MakeSdfRoughnessMod(P.SurfaceRoughness, 0.08f, 4,
|
OutStack.Add(MakeSdfRoughnessMod(P.SurfaceRoughness, 0.08f, 4,
|
||||||
P.SurfaceRoughness + BlendK + 2.0f));
|
P.SurfaceRoughness + BlendK + 2.0f));
|
||||||
OutStack.Add(MakeSdfFill(BlendK, P.BaseDensity));
|
OutStack.Add(MakeSdfFill(BlendK, P.BaseDensity));
|
||||||
@@ -9562,15 +9537,14 @@ namespace VoxelDensityOps
|
|||||||
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
|
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
|
||||||
bool bAppendStructuralPosts)
|
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 CarveBlend = 2.0f;
|
||||||
constexpr float RoughFrequency = 0.12f;
|
constexpr float RoughFrequency = 0.12f;
|
||||||
constexpr int32 RoughOctaves = 3;
|
constexpr int32 RoughOctaves = 3;
|
||||||
|
|
||||||
const float R = FMath::Max(P.CorridorRadius, 0.5f);
|
const float R = FMath::Max(P.CorridorRadius, 0.5f);
|
||||||
|
|
||||||
// Fenêtre d'application de la rugosité : `MazeSDF < R + SurfaceRoughness + 2.0f` dans
|
// Fenêtre d'application de la rugosité : `MazeSDF < R + SurfaceRoughness + 2.0f`.
|
||||||
// l'original. Reproduite à l'identique pour que l'égalité binaire tienne.
|
|
||||||
const float RoughApplyWithin = R + P.SurfaceRoughness + 2.0f;
|
const float RoughApplyWithin = R + P.SurfaceRoughness + 2.0f;
|
||||||
|
|
||||||
OutStack.Add(MakeConstantRockSource(P.BaseDensity));
|
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;
|
return EvalSample(WorldX, WorldY, WorldZ).Density;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** L'état COMPLET (densité + SDF) après toute la pile. Diagnostic : quand une comparaison
|
/** L'état COMPLET (densité + SDF) après toute la pile. Diagnostic : quand deux évaluations
|
||||||
* avec l'ancien chemin diverge, c'est le canal SDF qui dit si l'écart naît avant ou après
|
* divergent, c'est le canal SDF qui dit si l'écart naît avant ou après la conversion.
|
||||||
* la conversion. / The full state after the stack — the SDF channel is what says whether a
|
* / The full state after the stack — the SDF channel is what says whether a divergence is
|
||||||
* divergence is born before or after the carve. */
|
* born before or after the carve. */
|
||||||
FVoxelOpSample EvalSample(float WorldX, float WorldY, float WorldZ) const
|
FVoxelOpSample EvalSample(float WorldX, float WorldY, float WorldZ) const
|
||||||
{
|
{
|
||||||
FVoxelOpSample S;
|
FVoxelOpSample S;
|
||||||
@@ -578,9 +578,8 @@ namespace VoxelDensityOps
|
|||||||
* FlatPlain ET CrystalChamber — la même pile, **sans branchement sur le type** :
|
* FlatPlain ET CrystalChamber — la même pile, **sans branchement sur le type** :
|
||||||
* SlabVoidSource → GridColumnMod → [structural post ×4]
|
* SlabVoidSource → GridColumnMod → [structural post ×4]
|
||||||
*
|
*
|
||||||
* C'est le premier vrai gain du refactor (OPSTACK-PLAN §4) : deux des huit archétypes
|
* Deux des huit archétypes tiennent dans un opérateur (OPSTACK-PLAN §4) : leur différence est
|
||||||
* disparaissent dans un opérateur, et leur différence redevient ce qu'elle était déjà dans
|
* un jeu de valeurs par défaut, pas du code.
|
||||||
* `GetSlabDensity` — un jeu de valeurs par défaut, pas du code.
|
|
||||||
*/
|
*/
|
||||||
VOXELFORGE_API void BuildSlabStack(FVoxelOpStack& OutStack, const FSlabGenerationParams& P,
|
VOXELFORGE_API void BuildSlabStack(FVoxelOpStack& OutStack, const FSlabGenerationParams& P,
|
||||||
int32 Seed, float SpineRadius,
|
int32 Seed, float SpineRadius,
|
||||||
@@ -592,10 +591,8 @@ namespace VoxelDensityOps
|
|||||||
* ConstantRock → ShaftField → SdfRoughness → SdfCarve → ShaftLedge
|
* ConstantRock → ShaftField → SdfRoughness → SdfCarve → ShaftLedge
|
||||||
* → ShaftConnectorAir → ShaftConnectorFloor → [structural post ×4]
|
* → ShaftConnectorAir → ShaftConnectorFloor → [structural post ×4]
|
||||||
*
|
*
|
||||||
* C'est la démonstration que `§2.5` promettait : dans le `switch`, Maze et VerticalShafts sont
|
* Trois de ces ops sont ceux de Maze (`§2.5`), avec une source différente et d'autres réglages
|
||||||
* deux fonctions de ~100 lignes sans rien de commun à l'œil ; en opérateurs, ce sont les mêmes
|
* (fréquence 0.1 au lieu de 0.12, fenêtre `rough + 4` au lieu de `R + rough + 2`).
|
||||||
* 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`).
|
|
||||||
*/
|
*/
|
||||||
VOXELFORGE_API void BuildVerticalShaftStack(FVoxelOpStack& OutStack, const FVerticalShaftParams& P,
|
VOXELFORGE_API void BuildVerticalShaftStack(FVoxelOpStack& OutStack, const FVerticalShaftParams& P,
|
||||||
int32 Seed, float SpineRadius,
|
int32 Seed, float SpineRadius,
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
//
|
//
|
||||||
// Pipeline density-only: plus de grille de blocs, plus de surface heightfield.
|
// Pipeline density-only: plus de grille de blocs, plus de surface heightfield.
|
||||||
// Le mesher appelle GetDensityAt() par voxel pour reconstruire la géométrie.
|
// Le mesher appelle GetDensityAt() par voxel pour reconstruire la géométrie.
|
||||||
// Toute la logique de caves vit dans GetDensityWithParams / GetSlabDensity,
|
// Chaque strate est générée par sa pile d'opérateurs (VoxelDensityOpStack.h), pilotée par les
|
||||||
// pilotée par les params de strate récupérés auprès du StrateManager.
|
// params de strate récupérés auprès du StrateManager.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ public:
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Densité pour une strate TunnelNetwork (rooms + tunnels + worm noise).
|
* 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.
|
* Exposée pour permettre des tests isolés avec des params custom.
|
||||||
*
|
*
|
||||||
* ⚠️ `ParamsFingerprint` ET `LayoutVersion` SONT OBLIGATOIRES (`AUDIT §C2`). Le cache SDF
|
* ⚠️ `ParamsFingerprint` ET `LayoutVersion` SONT OBLIGATOIRES (`AUDIT §C2`). Le cache SDF
|
||||||
@@ -242,56 +242,8 @@ public:
|
|||||||
bool bCollectFusedDiagnostics = false,
|
bool bCollectFusedDiagnostics = false,
|
||||||
// Optional same-sample hand-off from EvaluateSDFCached. This is
|
// Optional same-sample hand-off from EvaluateSDFCached. This is
|
||||||
// only a room-shape ownership bit; it does not alter the scalar
|
// only a room-shape ownership bit; it does not alter the scalar
|
||||||
// result and avoids a duplicate room-index query in native paths.
|
// result and avoids a duplicate room-index query.
|
||||||
bool* OutRoomOwnsBottom = nullptr,
|
bool* OutRoomOwnsBottom = nullptr) const;
|
||||||
// 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;
|
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// CLIMATE & BIOME FIELDS (pure XY, deterministic, window-invariant)
|
// 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 →
|
* La chaîne de hauteur complète de SurfaceWorld : structural → cliff → terrace → layer lines →
|
||||||
* plage. Rend une ALTITUDE monde en voxels, pas une densité.
|
* plage. Rend une ALTITUDE monde en voxels, pas une densité.
|
||||||
*
|
*
|
||||||
* PUBLIQUE pour la même raison que `GetSlabDensity` / `GetMazeDensity` : permettre un test
|
* PUBLIQUE pour permettre un test isolé : c'est la référence de
|
||||||
* isolé. C'est la référence de `VoxelForge.OpStack.SurfaceHeightEquivalence`, qui compare la
|
* `VoxelForge.OpStack.SurfaceHeightEquivalence`, qui compare la pile d'opérateurs de hauteur
|
||||||
* pile d'opérateurs de hauteur (`VoxelHeightOp.h`) à cette fonction point par point.
|
* (`VoxelHeightOp.h`) à cette fonction point par point.
|
||||||
* Public so the height-op stack can be measured against it — same reason as GetSlabDensity.
|
* Public so the height-op stack can be measured against it.
|
||||||
*/
|
*/
|
||||||
float ComputeSurfaceTerrainZ(float WorldX, float WorldY, const FSurfaceGenerationParams& Params) const;
|
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
|
* 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 :
|
* (amplitude + direction amont). C'est la colonne que `ClassifyTile` teste pour SurfaceWorld ;
|
||||||
* c'est le seul chemin qui calcule l'overhang — `GetSurfaceDensity` passe `OverhangAmp = 0` —
|
* publique pour que les tests puissent placer des échantillons dans la fenêtre d'overhang.
|
||||||
* donc c'est la seule référence possible pour `FOverhangShelfMod`.
|
* The surface column ClassifyTile tests for SurfaceWorld; public so tests can aim samples at
|
||||||
* Public because this is the ONLY path that computes the overhang (GetSurfaceDensity passes 0),
|
* the overhang window.
|
||||||
* so it is the only possible reference for the ported op.
|
|
||||||
*/
|
*/
|
||||||
void ComputeSurfaceColumn(float WorldX, float WorldY, int32 ChunkZ,
|
void ComputeSurfaceColumn(float WorldX, float WorldY, int32 ChunkZ,
|
||||||
const FSurfaceGenerationParams& BaseSurface, const FBiomeContext& BiomeCtx,
|
const FSurfaceGenerationParams& BaseSurface, const FBiomeContext& BiomeCtx,
|
||||||
@@ -329,12 +280,6 @@ public:
|
|||||||
float& OutTerrainZ, float& OutCeilSurf,
|
float& OutTerrainZ, float& OutCeilSurf,
|
||||||
float& OutOverhangAmp, float& OutDirX, float& OutDirY) const;
|
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.
|
* Moisture field at a world XY → [0,1]. The second climate axis for biome placement.
|
||||||
*/
|
*/
|
||||||
@@ -467,11 +412,6 @@ private:
|
|||||||
FSurfaceGenerationParams& OutSurface, FBiomeContext& OutBiomeCtx,
|
FSurfaceGenerationParams& OutSurface, FBiomeContext& OutBiomeCtx,
|
||||||
TArray<FSurfaceGenerationParams>& OutBiomeParams) const;
|
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. */
|
/** (Re)build the per-chunk biome cell grid covering chunk (X,Y) footprint + margin. */
|
||||||
void RebuildBiomeGrid(int32 ChunkX, int32 ChunkY, int32 ChunkZ,
|
void RebuildBiomeGrid(int32 ChunkX, int32 ChunkY, int32 ChunkZ,
|
||||||
const FBiomeContext& Ctx, FChunkBiomeCache& Cache,
|
const FBiomeContext& Ctx, FChunkBiomeCache& Cache,
|
||||||
|
|||||||
Reference in new issue
Block a user