diff --git a/OPSTACK-PROGRESS.md b/OPSTACK-PROGRESS.md index 55db6c8..9f6a026 100644 --- a/OPSTACK-PROGRESS.md +++ b/OPSTACK-PROGRESS.md @@ -1275,3 +1275,65 @@ members being double in UE5 (cast at every use); and the `ComputeSurfaceTerrainZ (bounded seed offsets) still open. --- + +## 2026-07-27 — height stack GREEN (bit-identical, both passes). Step 2a + a real bug in my own §3.1 work. + +``` +SurfaceWorld(defaults): bit-identical across 20000 samples +SurfaceWorld(all terrain ops on): bit-identical across 20000 samples +MaxDisplacement: claims 16.300, worst observed 4.441 (27% of claim) +``` + +**The second op family was the right call** — height space decomposes as cleanly as density did, +including with all four F20 terrain ops on. `MaxDisplacement` is loose (27% used) because it sums +each op's independent maximum and they never peak at the same XY. **Left loose deliberately:** a +loose bound only costs CPU when the heightfield eventually gets a `ClassifyBox`; a tight-but-wrong +one is a hole in the world. + +### ⚠️ `FSlabVoidSource::IsXYPure()` was `true`, and that was WRONG + +Found while writing `FSurfaceColumnSource` and having to decide the same flag. + +The contract is *"**`Eval`** does not depend on Z"*. `FSlabVoidSource::Eval` computes +`min(Z - floor, ceil - Z)` — Z-dependent in the most direct way possible. **§3.1 made the SURFACES +XY-pure; the DENSITY never was and cannot be** — it is a distance to a surface. I conflated the two +while writing the very operator that quotes the warning against doing so. + +**Latent only because nothing reads the flag yet** — and step 2b is exactly where it would have gone +live: a generic T1.a column cache keyed on (XY box, StrateKey, Seed) with **no ChunkZ** would have +shared one density value down the entire vertical chunk stack. `AUDIT §6.3` says this corrupts every +chunk silently and that `ValidateDeterminism` would not catch it, because it samples along an X +boundary. Fixed, with the distinction written at the site. + +**This is the clearest argument yet for the height-space split:** the thing that is XY-pure is the +HEIGHT, and in `VoxelHeightOp.h` it lives in a type with no Z to get wrong. The bug is unrepresentable +there. + +### Step 2a — the bridge into density space + +- `FSkyCapHeightSource` — the ceiling is an **altitude**, so it belongs in height space, not in + density space as `§5` had it. Same category slip as the terrain ops; the subtraction happens later, + in the combine. It gets tested window-invariance and type-enforced XY purity for free. +- `FSurfaceColumnSource` — consumes both height stacks, produces + `Density = max(TerrainZ - Z, Z - CeilSurf)`. `IsXYPure() = false`, correctly this time. +- `BuildSurfaceStack` — source + 3 structural. **No per-column memo inside the op, on purpose:** + T1.a already exists one level up in `GetDensityAt`, and a second cache key is a second thing to get + wrong in exactly the way described above. Step 2b reuses the existing cache rather than inventing a + second one. + +**What step 2a does NOT cover, and the test now says so at the top:** the **overhang** (found while +reading: `GetSurfaceDensity` passes `OverhangAmp = 0`, so it computes none — the only reference is +the *cached* path, `ComputeSurfaceColumn`) and **biome blending** (weight 0 here; it is the `Mask` +combiner and `§5`'s Phase 3 prototype). **Do not wire SurfaceWorld into a world with biomes or +overhangs until 2b**, because nothing currently in the tests would say it is wrong. + +**UNVERIFIED:** step 2a is not compiled. Likely spots: `FVoxelHeightStack` as a member of +`FSurfaceColumnSource` (move-only member ⇒ the enclosing op is move-only too, which is fine since it +lives behind `TUniquePtr`); the new `VoxelHeightOp.h` include in `VoxelDensityOpStack.cpp`; and +`Gen->GetSurfaceDensity` taking two param refs plus a weight. + +**Next single action:** build. Then step 2b — the overhang op (per-column gate + per-voxel union, +referenced against the cached path), biome blending as the `Mask` combiner, and the wiring that +reuses `GSurfColCache`. `§C1` still open. + +--- diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeHeightStackTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeHeightStackTest.cpp index 39f432c..c9bd1c7 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeHeightStackTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeHeightStackTest.cpp @@ -12,10 +12,24 @@ // 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 test ne touche PAS au chemin densité. `FHeightfieldSource` / `FSkyCapSource` / -// `FOverhangShelfMod` (OPSTACK-DECOMPOSITION §5) sont l'étape SUIVANTE, délibérément séparée : si -// l'espace-hauteur ne se décomposait pas proprement, on l'apprendrait ici, pour le prix d'un test, -// et pas après avoir écrit l'adaptateur, le cache de colonne et le branchement. +// ⚠️ CE QUE CE TEST COUVRE, ET SURTOUT CE QU'IL NE COUVRE PAS +// ✅ la pile de HAUTEUR du sol, contre `ComputeSurfaceTerrainZ` (2 passes : défauts, puis tous +// les ops F20 allumés — c'est la seconde qui porte le test) ; +// ✅ la pile de HAUTEUR de la voûte + le pont vers l'espace densité (`FSurfaceColumnSource`), +// contre `GetSurfaceDensity` ; +// ❌ **l'OVERHANG** — `GetSurfaceDensity` passe `OverhangAmp = 0`, donc il n'en calcule aucun. +// Sa seule référence est le chemin CACHÉ (`ComputeSurfaceColumn`), qui résout le gate et la +// direction amont par colonne ; +// ❌ **le MÉLANGE DE BIOMES** — ici `ParamsD == ParamsN`, poids 0. C'est le combiner `Mask`, et +// `§5` en fait le prototype de la Phase 3 : ça mérite son étape. +// +// Les deux manques sont l'étape 2b. **Ne pas brancher SurfaceWorld dans un monde à biomes ou à +// overhang avant**, parce que rien ici ne dirait que c'est faux. +// +// COVERED: the ground height stack vs ComputeSurfaceTerrainZ, and the ceiling stack + the bridge +// into density space vs GetSurfaceDensity. NOT COVERED: the overhang (GetSurfaceDensity passes +// OverhangAmp = 0, so only the cached path computes it) and biome blending (weight 0 here). Both +// are step 2b — do not wire SurfaceWorld into a world with biomes or overhangs before then. // // LA BARRE : **bit à bit.** Depuis `FPSemantics = Precise` (AUDIT §C9/§C10), Maze et Slab sont // bit-identiques à leur original ; il n'y a plus de « plancher ULP » à tolérer. Un écart ici est @@ -31,6 +45,7 @@ #include "VoxelForgeTestFixture.h" #include "VoxelHeightOp.h" +#include "VoxelDensityOpStack.h" #include @@ -277,6 +292,81 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters) EnableAllTerrainOps(AllOps); RunForParams(AllOps, TEXT("SurfaceWorld(all terrain ops on)"), 64); + //========================================================================= + // ÉTAPE 2a — LE PONT VERS L'ESPACE DENSITÉ + //========================================================================= + // `FSurfaceColumnSource` consomme les DEUX piles de hauteur (sol + voûte) et rend une densité. + // La référence est `GetSurfaceDensity`, qui est exactement la variante **sans overhang** + // (il passe `OverhangAmp = 0`) et **sans biomes** (ParamsD == ParamsN, poids 0) — donc la + // comparaison est nette plutôt qu'approximative. + // + // ⚠️ Ce que ce bloc NE teste PAS, et qu'il ne faut pas croire testé : l'overhang et le mélange + // de biomes. Tous deux arrivent à l'étape 2b, avec le chemin CACHÉ pour référence — c'est le + // seul qui les calcule. + { + const FSurfaceGenerationParams& P = AllOps; + + FVoxelOpStack Stack; + VoxelDensityOps::BuildSurfaceStack(Stack, P, World.Settings->Seed, + Gen->OriginSpineRadius, World.StrateManager.Get()); + + // 1 source + 3 structurels. Pas encore d'overhang : voir l'avertissement ci-dessus. + TestEqual(TEXT("the surface density stack is source + 3 structural (no overhang yet)"), + Stack.Num(), 4); + + FVoxelOpContext Ctx; + Ctx.Seed = (uint32)World.Settings->Seed; + Ctx.LayoutVersion = World.StrateManager->GetLayoutVersion(); + Ctx.StrateTopWorldZ = P.StrateTopWorldZ; + Ctx.StrateBottomWorldZ = P.StrateBottomWorldZ; + Stack.PrepareChunk(Ctx); + + int32 NumDiff = 0, NumSideDisagree = 0, WorstIdx = -1; + float WorstDelta = 0.0f; + + FRandomStream Rng(5150); + for (int32 i = 0; i < NumHeightSamples; ++i) + { + const float X = (float)Rng.RandRange(-4 * CHUNK_SIZE, 4 * CHUNK_SIZE); + const float Y = (float)Rng.RandRange(-4 * CHUNK_SIZE, 4 * CHUNK_SIZE); + const float Z = (float)Rng.RandRange(BottomVoxelZ, TopVoxelZ); + + // ParamsD == ParamsN, poids 0 ⇒ une seule évaluation, pas de biomes. + const float Old = Gen->GetSurfaceDensity(X, Y, Z, P, P, 0.0f); + const float New = Stack.EvalMC(X, Y, Z); + + if (!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; } + } + + if (NumDiff == 0) + { + AddInfo(FString::Printf( + TEXT("SurfaceWorld density stack: bit-identical to GetSurfaceDensity across %d ") + TEXT("samples. The height stacks feed the density space correctly."), + NumHeightSamples)); + } + else + { + AddError(FString::Printf( + TEXT("SurfaceWorld density stack: %d of %d samples differ (largest |delta| %.9g); ") + TEXT("%d cross the isosurface. Since /fp:precise the bar is bit-identity, so this ") + TEXT("is a real port error. Check, in order: the combine (Density = max(TerrainZ - Z, ") + TEXT("Z - CeilSurf)), the sky-cap transcription (warp offsets 0.71/2.3/3.3 and ") + TEXT("6.1/0.19/4.7, the abs() on roughness, the ridge *0.5+0.5), and the order of ") + TEXT("the structural post ops."), + NumDiff, NumHeightSamples, WorstDelta, NumSideDisagree)); + } + + TestEqual(TEXT("surface: no sample lands on the opposite side of the isosurface"), + NumSideDisagree, 0); + } + return true; } diff --git a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp index 46b17ce..c83126d 100644 --- a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp +++ b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp @@ -23,6 +23,7 @@ #include "VoxelDensityPrimitives.h" // VF_ApplyOriginSpine / Seal / PassageCarving #include "VoxelCaveMorphology.h" // VoxelSDF::Capsule, VoxelHash #include "VoxelGenerator.h" // VoxelGenLOD::Eff +#include "VoxelHeightOp.h" // FVoxelHeightStack — SurfaceWorld's two height stacks #include "VoxelNoise.h" // VoxelNoise::FBM #include "VoxelStrateManager.h" // EvaluateModifierSDF / AnyPassageNearBox #include "VoxelTypes.h" // SmoothStep01, VOXEL_NOISE_SCALE @@ -263,10 +264,29 @@ namespace EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; } void PrepareChunk(const FVoxelOpContext&) override {} - // ⚠️ LE point de §3.1. Faux avant le retrait du terme en Z ; le déclarer alors aurait - // corrompu silencieusement toute la pile verticale de chunks (voir l'avertissement sur - // `IsXYPure` dans VoxelDensityOp.h). - bool IsXYPure() const override { return true; } + // ⚠️⚠️ CORRIGÉ 2026-07-27 : c'était `true`, ET C'ÉTAIT FAUX. + // + // Le contrat de `IsXYPure` est « **`Eval`** ne dépend pas de Z » — pas « les surfaces ne + // dépendent pas de Z ». Or `Eval` calcule `min(Z - sol, plafond - Z)` : il dépend de Z de + // la façon la plus directe qui soit. §3.1 a rendu les SURFACES pures en XY ; la DENSITÉ, + // elle, ne l'a jamais été et ne peut pas l'être — c'est une distance à une surface. + // + // Latent seulement parce que personne ne lit encore ce drapeau. Le jour où le cache de + // colonnes T1.a devient générique (l'étape suivante), un `true` ici ferait partager UNE + // valeur de densité sur TOUTE la pile verticale de chunks — un monde silencieusement faux, + // que `ValidateDeterminism` ne verrait pas parce qu'il échantillonne le long d'un bord X. + // C'est exactement le piège que l'avertissement de `VoxelDensityOp.h` décrit, et je suis + // tombé dedans en écrivant l'opérateur qui le cite. + // + // ⚠️ FIXED: this said `true` and was WRONG. The contract is "**Eval** does not depend on Z", + // and Eval computes min(Z - floor, ceil - Z). §3.1 made the SURFACES XY-pure; the DENSITY + // never was and cannot be — it is a distance to a surface. Latent only because nothing reads + // the flag yet; a generic T1.a column cache would have shared one density down the whole + // vertical chunk stack. + // + // C'est précisément cette distinction qui justifie l'espace-hauteur (`VoxelHeightOp.h`) : + // ce qui est pur en XY, ce sont les HAUTEURS, et elles y sont dans un type qui n'a pas de Z. + bool IsXYPure() const override { return false; } void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { @@ -367,6 +387,68 @@ namespace float FloorAmp = 0.0f, CeilAmp = 0.0f; }; + //========================================================================= + // RÔLE 1 — SOURCE : COLONNE DE SURFACE / SURFACE COLUMN (SurfaceWorld) + //========================================================================= + // Le pont entre les deux espaces : consomme DEUX piles de hauteur (sol et voûte) et en fait une + // densité. C'est tout le combine de `SurfaceDensityFromColumn` : + // + // Density = TerrainZ - Z ← solide sous le sol + // Density = max(Density, Z - CeilSurf) ← solide au-dessus de la voûte + // + // ⚠️ `IsXYPure()` est **false**, et la distinction est LE point de tout ce découpage : les + // HAUTEURS sont pures en XY (elles vivent dans `VoxelHeightOp.h`, un type sans Z), la DENSITÉ + // ne l'est pas et ne peut pas l'être — c'est une distance à une surface. Confondre les deux est + // exactement le bug que `FSlabVoidSource` portait jusqu'à aujourd'hui. + // + // The bridge between the two spaces: consumes two HEIGHT stacks and turns them into density. + // IsXYPure is false — the heights are XY-pure, the density is a distance to them and never can be. + class FSurfaceColumnSource final : public IVoxelDensityOp + { + public: + FSurfaceColumnSource(const FSurfaceGenerationParams& P, int32 Seed) + { + VoxelHeightOps::BuildSurfaceHeightStack(TerrainStack, P, Seed); + VoxelHeightOps::BuildSurfaceCeilingStack(CeilingStack, P, Seed); + } + + EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; } + void PrepareChunk(const FVoxelOpContext&) override {} + bool IsXYPure() const override { return false; } // voir le bloc ci-dessus + + void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override + { + // ⚠️ PAS de mémo par colonne ICI, délibérément. Le cache T1.a existe déjà UN NIVEAU + // AU-DESSUS (`GSurfColCache` dans `GetDensityAt`), clé sur (boîte XY, StrateKey, Seed). + // En rajouter un ici demanderait une seconde clé de cache à tenir juste — et une clé de + // cache fausse dans un op partagé sur toute la pile verticale est précisément le mode de + // défaillance qu'`AUDIT §6.3` décrit. Le branchement (étape 2b) réutilise le cache + // existant plutôt que d'en inventer un second. + // No per-column memo here on purpose: T1.a already exists one level up, and a second + // cache key is a second thing to get wrong. + const float TerrainZ = TerrainStack.EvalHeight(WorldX, WorldY); + const float CeilSurf = CeilingStack.EvalHeight(WorldX, WorldY); + + float Density = TerrainZ - WorldZ; + Density = FMath::Max(Density, WorldZ - CeilSurf); + InOut.Density = Density; // Replace : interne, positif = solide + } + + // Mixed, honnêtement. Un verdict exact demanderait de borner le heightfield structurel sur + // la boîte XY (continents + montagnes + détail sous un domain-warp) — faisable, mais c'est + // une vraie borne à dériver, pas une constante à lire comme pour la dalle. Rendre Mixed ne + // coûte que du CPU ; rendre faux serait un trou. À faire quand `MaxDisplacement` saura + // répondre pour la source structurelle. + EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override + { + return EVoxelOpEffect::Both; + } + + private: + FVoxelHeightStack TerrainStack; + FVoxelHeightStack CeilingStack; + }; + //========================================================================= // RÔLE 3 — MODIFIER : RUGOSITÉ DE PAROI, ESPACE SDF //========================================================================= @@ -786,6 +868,30 @@ namespace VoxelDensityOps return MakeUnique(P, Seed); } + TUniquePtr MakeSurfaceColumnSource(const FSurfaceGenerationParams& P, int32 Seed) + { + return MakeUnique(P, Seed); + } + + void BuildSurfaceStack(FVoxelOpStack& OutStack, const FSurfaceGenerationParams& P, + int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager) + { + // ⚠️ PAS ENCORE L'ARCHÉTYPE COMPLET, et il faut le savoir avant de brancher : + // • pas d'OVERHANG — `FOverhangShelfMod`, le seul op vraiment 3D d'ici, a besoin d'une + // donnée PAR COLONNE (amp + direction amont) que `GetSurfaceDensity` ne calcule même + // pas (il passe `OverhangAmp = 0`). Sa référence est le chemin caché, pas celui-ci ; + // • pas de MÉLANGE DE BIOMES — le sol est évalué pour le biome dominant puis lerpé vers + // le voisin. En termes de pile c'est le combiner `Mask`, et c'est le prototype de la + // Phase 3 (§5) : ça mérite sa propre étape, pas un paramètre de plus ici. + // + // Donc cette pile == `GetSurfaceDensity` exactement, qui est la version SANS overhang et + // SANS biomes. C'est ce que le test compare, et c'est pour ça que la comparaison est nette. + OutStack.Add(MakeSurfaceColumnSource(P, Seed)); + + OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ, + P.BoundarySealThickness, P.BaseDensity, SpineRadius, StrateManager); + } + void BuildSlabStack(FVoxelOpStack& OutStack, const FSlabGenerationParams& P, int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager) { diff --git a/Source/VoxelForge/Private/VoxelHeightOpStack.cpp b/Source/VoxelForge/Private/VoxelHeightOpStack.cpp index 8ec0339..ff736b2 100644 --- a/Source/VoxelForge/Private/VoxelHeightOpStack.cpp +++ b/Source/VoxelForge/Private/VoxelHeightOpStack.cpp @@ -277,12 +277,100 @@ namespace }; } + //========================================================================= + // SOURCE — LE CIEL / SKY CAP (c'est une ALTITUDE, donc c'est un op de hauteur) + //========================================================================= + // `ComputeSurfaceCeiling` rend un Z, exactement comme le terrain. `OPSTACK-DECOMPOSITION §5` le + // range en `FSkyCapSource` côté DENSITÉ (« Subtract »), mais c'est le même glissement que pour + // les ops de terrain : ce que la fonction produit est une hauteur, et la soustraction n'arrive + // qu'après, dans le combine. Le mettre ici lui donne gratuitement l'invariance de fenêtre + // testée, la pureté XY garantie par le type, et le cache de colonne. + // The sky cap returns a Z, so it belongs in height space; the subtraction happens later, in the + // density-side combine. + class FSkyCapHeightSource final : public IVoxelHeightOp + { + public: + FSkyCapHeightSource(const FSurfaceGenerationParams& InP, int32 InSeed) + : P(InP), SeedF((float)InSeed) {} + + void Eval(float WorldX, float WorldY, FVoxelHeightSample& InOut) const override + { + const float H = P.StrateTopWorldZ - P.StrateBottomWorldZ; + float CeilZ = P.StrateBottomWorldZ + H * P.CeilingRelative; + + // Domain-warp des coords larges/ridge (miroir du HeightWarp du sol). Les bosses fines + // restent sur le vrai XY pour rester nettes et décorrélées. 0 ⇒ pas de warp. + float QX = WorldX, QY = WorldY; + if (P.CeilingWarpStrength > 0.0f) + { + const float WF = P.CeilingWarpFrequency; + const float wx = VoxelNoise::Perlin3D(FVector(WorldX * WF + SeedF * 0.71f, WorldY * WF + 2.3f, SeedF * 3.3f)); + const float wy = VoxelNoise::Perlin3D(FVector(WorldX * WF + 6.1f, WorldY * WF + SeedF * 0.19f, SeedF * 4.7f)); + QX += wx * VOXEL_NOISE_SCALE * P.CeilingWarpStrength; + QY += wy * VOXEL_NOISE_SCALE * P.CeilingWarpStrength; + } + + // Gonflement large SIGNÉ : monte/descend toute la voûte. + if (P.CeilingUndulation > 0.0f) + { + const float Swell = HFractalNoise3D(FVector( + QX * P.CeilingUndulationFrequency + SeedF * 1.9f, + QY * P.CeilingUndulationFrequency + 13.0f, + SeedF * 0.5f), 3); // [-1,1] + CeilZ += Swell * VOXEL_NOISE_SCALE * P.CeilingUndulation; + } + + // Pendage vers le BAS uniquement : tout est >= 0, donc rien ne perce vers le haut dans + // le seal. Bosses fines + lames ridgées s'additionnent. + float Hang = 0.0f; + if (P.CeilingRoughness > 0.0f) + { + Hang += FMath::Abs(HFractalNoise3D(FVector( + WorldX * P.CeilingRoughnessFrequency + 5.0f, + WorldY * P.CeilingRoughnessFrequency + 6.0f, + SeedF * 2.1f), 3)) * VOXEL_NOISE_SCALE * P.CeilingRoughness; + } + if (P.CeilingRidgeStrength > 0.0f) + { + float Ridge = HRidgedNoise3D(FVector( + QX * P.CeilingRidgeFrequency + 31.0f, + QY * P.CeilingRidgeFrequency + 47.0f, + SeedF * 1.1f), 4); // [-1,1] + Ridge = Ridge * 0.5f + 0.5f; // [0,1] lignes de crête pendantes + Hang += Ridge * P.CeilingRidgeStrength; + } + + InOut.Height = CeilZ - Hang; // Replace : racine de sa propre pile + // Relief laissé intact : le ciel n'en produit pas et personne ne le lui demande. + } + + float MaxDisplacement() const override { return FLT_MAX; } // source, pas modificateur + + private: + FSurfaceGenerationParams P; + float SeedF; + }; +} + //============================================================================= // FABRIQUES / FACTORIES //============================================================================= namespace VoxelHeightOps { + TUniquePtr MakeSkyCapHeightSource(const FSurfaceGenerationParams& P, int32 Seed) + { + return MakeUnique(P, Seed); + } + + void BuildSurfaceCeilingStack(FVoxelHeightStack& OutStack, const FSurfaceGenerationParams& P, int32 Seed) + { + // Un seul op aujourd'hui — et c'est une information, pas un manque : le plafond n'a pas + // d'équivalent des quatre modificateurs du sol. Le jour où on veut des terrasses au + // plafond, on ajoute la ligne ; c'est exactement le genre de composition que le refactor + // existe pour rendre possible. + OutStack.Add(MakeSkyCapHeightSource(P, Seed)); + } TUniquePtr MakeStructuralHeightSource(const FSurfaceGenerationParams& P, int32 Seed, const IVoxelHeightOp** OutSource) { diff --git a/Source/VoxelForge/Public/VoxelDensityOpStack.h b/Source/VoxelForge/Public/VoxelDensityOpStack.h index b3ae2ff..96e34d4 100644 --- a/Source/VoxelForge/Public/VoxelDensityOpStack.h +++ b/Source/VoxelForge/Public/VoxelDensityOpStack.h @@ -193,6 +193,24 @@ namespace VoxelDensityOps * `FillOnly` quand une colonne atteint la boîte, `Identity` (le cas courant) sinon. */ VOXELFORGE_API TUniquePtr MakeGridColumnMod(const FSlabGenerationParams& P, int32 Seed); + /** Rôle 1 — le pont entre les deux espaces : consomme les piles de HAUTEUR (sol + voûte, + * `VoxelHeightOp.h`) et en fait une densité. `IsXYPure()` est **false** — les hauteurs sont + * pures en XY, la densité est une distance à celles-ci et ne peut pas l'être. */ + VOXELFORGE_API TUniquePtr MakeSurfaceColumnSource(const FSurfaceGenerationParams& P, + int32 Seed); + + /** + * SurfaceWorld — ⚠️ PAS ENCORE COMPLET, et c'est délibéré. Équivaut exactement à + * `GetSurfaceDensity`, c.-à-d. la version **sans overhang** et **sans mélange de biomes** : + * • l'overhang a besoin d'une donnée par colonne que `GetSurfaceDensity` ne calcule pas + * (il passe `OverhangAmp = 0`) — sa référence est le chemin caché ; + * • le mélange de biomes est le combiner `Mask`, prototype de la Phase 3 (§5). + * Les deux arrivent à l'étape 2b. Ne pas brancher dans un monde à biomes avant. + */ + VOXELFORGE_API void BuildSurfaceStack(FVoxelOpStack& OutStack, const FSurfaceGenerationParams& P, + int32 Seed, float SpineRadius, + const UVoxelStrateManager* StrateManager); + /** * FlatPlain ET CrystalChamber — la même pile, **sans branchement sur le type** : * SlabVoidSource → GridColumnMod → [structural post ×3] diff --git a/Source/VoxelForge/Public/VoxelHeightOp.h b/Source/VoxelForge/Public/VoxelHeightOp.h index 28f39ff..1119789 100644 --- a/Source/VoxelForge/Public/VoxelHeightOp.h +++ b/Source/VoxelForge/Public/VoxelHeightOp.h @@ -199,4 +199,14 @@ namespace VoxelHeightOps */ VOXELFORGE_API void BuildSurfaceHeightStack(FVoxelHeightStack& OutStack, const FSurfaceGenerationParams& P, int32 Seed); + + /** La voûte : warp + gonflement signé + pendage vers le bas uniquement. C'est une ALTITUDE, + * donc un op de hauteur — la soustraction n'arrive qu'au combine côté densité. */ + VOXELFORGE_API TUniquePtr MakeSkyCapHeightSource(const FSurfaceGenerationParams& P, + int32 Seed); + + /** La pile de plafond de SurfaceWorld. Un seul op aujourd'hui, et c'est une information : le + * plafond n'a pas d'équivalent des quatre modificateurs du sol. */ + VOXELFORGE_API void BuildSurfaceCeilingStack(FVoxelHeightStack& OutStack, + const FSurfaceGenerationParams& P, int32 Seed); }