feat: SurfaceWorld step 2a — the bridge to density space; fix IsXYPure on the slab

Height stack is green: bit-identical to ComputeSurfaceTerrainZ on both passes,
including all four F20 terrain ops on. MaxDisplacement is loose (27% used) and left
that way — loose only costs CPU, tight-but-wrong is a hole.

FIX: FSlabVoidSource::IsXYPure() returned true and that was wrong. The contract is
"Eval does not depend on Z", and Eval computes min(Z - floor, ceil - Z). Section 3.1
made the SURFACES XY-pure; the density is a distance to them and never can be. I
conflated the two while writing the operator that quotes the warning against it.

Latent only because nothing reads the flag yet — and step 2b is where it would have
gone live, since a generic T1.a column cache keyed without ChunkZ would have shared
one density down the whole vertical chunk stack. AUDIT 6.3 says that corrupts every
chunk silently and ValidateDeterminism would not catch it.

That is also the clearest argument for the height-space split: what is XY-pure is
the HEIGHT, and in VoxelHeightOp.h it lives in a type with no Z to get wrong.

Step 2a:
- FSkyCapHeightSource: the ceiling is an altitude, so it belongs in height space
  rather than density space as section 5 had it — same category slip as the terrain
  ops. The subtraction happens later, in the combine.
- FSurfaceColumnSource: consumes both height stacks, IsXYPure false.
- BuildSurfaceStack: source + 3 structural, no per-column memo inside the op since
  T1.a already exists one level up and a second cache key is a second thing to get
  wrong.

NOT covered, and the test header now says so: the overhang (GetSurfaceDensity passes
OverhangAmp = 0, so only the cached path computes it) and biome blending. Both are
step 2b; do not wire SurfaceWorld into a biome or overhang world before then.

UNVERIFIED: not compiled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 16:14:39 +02:00
parent 921a9fb666
commit 2b2afacd9e
6 changed files with 382 additions and 8 deletions
+62
View File
@@ -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.
---
@@ -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 <atomic>
@@ -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;
}
@@ -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<FGridColumnMod>(P, Seed);
}
TUniquePtr<IVoxelDensityOp> MakeSurfaceColumnSource(const FSurfaceGenerationParams& P, int32 Seed)
{
return MakeUnique<FSurfaceColumnSource>(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)
{
@@ -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<IVoxelHeightOp> MakeSkyCapHeightSource(const FSurfaceGenerationParams& P, int32 Seed)
{
return MakeUnique<FSkyCapHeightSource>(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<IVoxelHeightOp> MakeStructuralHeightSource(const FSurfaceGenerationParams& P, int32 Seed,
const IVoxelHeightOp** OutSource)
{
@@ -193,6 +193,24 @@ namespace VoxelDensityOps
* `FillOnly` quand une colonne atteint la boîte, `Identity` (le cas courant) sinon. */
VOXELFORGE_API TUniquePtr<IVoxelDensityOp> 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<IVoxelDensityOp> 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]
+10
View File
@@ -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<IVoxelHeightOp> 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);
}