feat: SurfaceWorld step 2b — the overhang shelf, the one op that is genuinely 3D
Step 2a green on all four counts, including FSurfaceColumnSource bit-identical to GetSurfaceDensity over 20000 samples. FOverhangShelfMod is the case that justifies where the two spaces were split: its uphill reach grows with altitude (Frac = (Z - TerrainZ) / OverhangHeight), so it is essentially Z-dependent and could not have lived in VoxelHeightOp.h. The boundary falls where the code changes nature. It needs TerrainZ plus a per-column gate (amp, uphill dir) the source computes. Recomputing per voxel would pay the cliff's four resamples per lip voxel; adding a third channel to FVoxelOpSample would put a COLUMN property in a per-voxel slot and pollute a shared contract (section 11 has that open). Instead the source memoises the column and the overhang reads it — the same shape as cliff -> structural. The memo is keyed on (InstanceId, X, Y) with InstanceId from a monotonic atomic counter, not on `this`: a freed stack and a newly allocated one can share an address, a never-decreasing counter cannot collide. The stack evaluates every Z of a column at one XY, so the hit rate is ~1 and this recovers per-column reuse without inventing a second cross-chunk cache. ComputeSurfaceColumn and SurfaceDensityFromColumn are now public: they are the only oracle for the overhang, since GetSurfaceDensity passes OverhangAmp = 0. Private declarations removed. The test's third pass places half its samples inside the overhang window on purpose — a uniform Z draw would almost never hit it and the test would pass having never run the op, the same trap as WaterLevelRelative in the height pass. The in-window count is reported and warns at zero. Still missing before wiring: biome blending (the Mask combiner, section 5's Phase 3 prototype). Do not tick bUseOperatorStack on a SurfaceWorld strate with biomes yet. UNVERIFIED: not compiled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1337,3 +1337,48 @@ referenced against the cached path), biome blending as the `Mask` combiner, and
|
|||||||
reuses `GSurfColCache`. `§C1` still open.
|
reuses `GSurfColCache`. `§C1` still open.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-07-27 — step 2b: the overhang, the one op that could NOT live in height space.
|
||||||
|
|
||||||
|
Step 2a came back green on all four counts, including the density-side bridge
|
||||||
|
(`FSurfaceColumnSource` bit-identical to `GetSurfaceDensity` over 20 000 samples).
|
||||||
|
|
||||||
|
**`FOverhangShelfMod` is the boundary case that justifies where the two spaces were split.** Its
|
||||||
|
uphill reach *grows with altitude* (`Frac = (Z - TerrainZ) / OverhangHeight`), so it is essentially
|
||||||
|
Z-dependent — it is the only SurfaceWorld op that could not have gone into `VoxelHeightOp.h`. The
|
||||||
|
line between the two spaces falls where the code changes nature, not where it was convenient.
|
||||||
|
|
||||||
|
**The per-column problem, and how it is solved.** The overhang needs `TerrainZ` plus a per-column
|
||||||
|
gate (`OverhangAmp`, `DirX`, `DirY`) that the source computes. Three ways to get it, two bad:
|
||||||
|
- recompute the height stack per voxel — correct but pays the cliff's four resamples per lip voxel;
|
||||||
|
- add a third channel to `FVoxelOpSample` — a per-voxel slot for a **column** property, and
|
||||||
|
archetype-specific pollution of a shared contract (`§11` has this open, unresolved);
|
||||||
|
- **chosen:** the source memoises the column and the overhang reads it, same as `cliff → structural`.
|
||||||
|
|
||||||
|
**The memo key is the part worth getting right.** Keyed on `(InstanceId, X, Y)` where `InstanceId`
|
||||||
|
comes from a monotonic atomic counter — **not** on `this`. A freed stack and a newly allocated one
|
||||||
|
can share an address; a counter that never goes backwards cannot collide. Since the stack evaluates
|
||||||
|
every Z of a column at the same XY, the hit rate is ~1, so this also recovers the per-column reuse
|
||||||
|
without inventing a second cross-chunk cache.
|
||||||
|
|
||||||
|
**Two functions exposed** (`ComputeSurfaceColumn`, `SurfaceDensityFromColumn`) because they are the
|
||||||
|
**only** oracle for the overhang — `GetSurfaceDensity` passes `OverhangAmp = 0` and computes none.
|
||||||
|
Their private declarations were removed; two declarations of one member will not compile.
|
||||||
|
|
||||||
|
**The test's third pass is written to avoid measuring nothing.** A uniform Z draw over the whole
|
||||||
|
strate would almost never land in the overhang window, so the test would pass green having never run
|
||||||
|
the op — the same trap as `WaterLevelRelative` in the height pass. Half the samples are now placed
|
||||||
|
*inside* the window deliberately, the count is reported, and it warns if it is zero.
|
||||||
|
|
||||||
|
**Still missing before wiring: BIOME BLENDING.** The ground is evaluated for the dominant biome and
|
||||||
|
lerped toward the neighbour — the `Mask` combiner, and `§5` calls it the Phase 3 prototype. **Do not
|
||||||
|
tick `bUseOperatorStack` on a SurfaceWorld strate with biomes until then.**
|
||||||
|
|
||||||
|
**UNVERIFIED:** step 2b is not compiled. Likely spots: `std::atomic` include; `HFractal3D` newly
|
||||||
|
added to the density TU; `MakeUnique<FSurfaceColumnSource>` then `MoveTemp` into the stack while
|
||||||
|
keeping a raw pointer; and the two newly-public generator methods.
|
||||||
|
|
||||||
|
**Next single action:** build. Then biome blending as the `Mask` combiner + the wiring that reuses
|
||||||
|
`GSurfColCache`. `§C1` still open.
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -304,15 +304,21 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters)
|
|||||||
// de biomes. Tous deux arrivent à l'étape 2b, avec le chemin CACHÉ pour référence — c'est le
|
// de biomes. Tous deux arrivent à l'étape 2b, avec le chemin CACHÉ pour référence — c'est le
|
||||||
// seul qui les calcule.
|
// seul qui les calcule.
|
||||||
{
|
{
|
||||||
const FSurfaceGenerationParams& P = AllOps;
|
// ⚠️ `OverhangStrength = 0` EXPLICITEMENT : `GetSurfaceDensity` passe `OverhangAmp = 0`,
|
||||||
|
// donc il n'en calcule aucun. Comparer une pile qui en produit à une référence qui n'en
|
||||||
|
// produit pas ferait échouer le test pour la seule raison que la référence est incomplète.
|
||||||
|
// L'overhang a sa propre passe juste en dessous, avec le bon oracle.
|
||||||
|
FSurfaceGenerationParams P = AllOps;
|
||||||
|
P.OverhangStrength = 0.0f;
|
||||||
|
|
||||||
FVoxelOpStack Stack;
|
FVoxelOpStack Stack;
|
||||||
VoxelDensityOps::BuildSurfaceStack(Stack, P, World.Settings->Seed,
|
VoxelDensityOps::BuildSurfaceStack(Stack, P, World.Settings->Seed,
|
||||||
Gen->OriginSpineRadius, World.StrateManager.Get());
|
Gen->OriginSpineRadius, World.StrateManager.Get());
|
||||||
|
|
||||||
// 1 source + 3 structurels. Pas encore d'overhang : voir l'avertissement ci-dessus.
|
// 1 source + 1 overhang + 3 structurels. L'op overhang est présent mais inerte ici
|
||||||
TestEqual(TEXT("the surface density stack is source + 3 structural (no overhang yet)"),
|
// (amp 0 ⇒ sortie immédiate) — la décomposition ne change pas selon les params.
|
||||||
Stack.Num(), 4);
|
TestEqual(TEXT("the surface density stack is source + overhang + 3 structural"),
|
||||||
|
Stack.Num(), 5);
|
||||||
|
|
||||||
FVoxelOpContext Ctx;
|
FVoxelOpContext Ctx;
|
||||||
Ctx.Seed = (uint32)World.Settings->Seed;
|
Ctx.Seed = (uint32)World.Settings->Seed;
|
||||||
@@ -367,6 +373,113 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters)
|
|||||||
NumSideDisagree, 0);
|
NumSideDisagree, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// ÉTAPE 2b — L'OVERHANG, contre le SEUL oracle qui le calcule
|
||||||
|
//=========================================================================
|
||||||
|
// `GetSurfaceDensity` passe `OverhangAmp = 0`. La seule référence est donc le chemin caché :
|
||||||
|
// `ComputeSurfaceColumn` (qui résout le gate et la direction amont par colonne) suivi de
|
||||||
|
// `SurfaceDensityFromColumn` (qui applique l'union par voxel). Les deux viennent d'être
|
||||||
|
// exposées pour ça.
|
||||||
|
//
|
||||||
|
// C'est aussi la passe qui vérifie le MÉMO DE COLONNE de `FSurfaceColumnSource` : l'op overhang
|
||||||
|
// lit la colonne produite par la source, et s'ils divergeaient d'un XY, l'union se ferait au
|
||||||
|
// mauvais endroit. Un mémo mal clé se verrait ici.
|
||||||
|
{
|
||||||
|
FSurfaceGenerationParams P = AllOps;
|
||||||
|
P.OverhangStrength = 0.8f;
|
||||||
|
P.OverhangSlopeThreshold = 0.12f;
|
||||||
|
P.OverhangHeight = 14.0f;
|
||||||
|
P.OverhangReach = 10.0f;
|
||||||
|
P.OverhangFrequency = 0.05f;
|
||||||
|
P.OverhangZScale = 0.6f;
|
||||||
|
|
||||||
|
FVoxelOpStack Stack;
|
||||||
|
VoxelDensityOps::BuildSurfaceStack(Stack, P, World.Settings->Seed,
|
||||||
|
Gen->OriginSpineRadius, World.StrateManager.Get());
|
||||||
|
|
||||||
|
FVoxelOpContext Ctx;
|
||||||
|
Ctx.Seed = (uint32)World.Settings->Seed;
|
||||||
|
Ctx.LayoutVersion = World.StrateManager->GetLayoutVersion();
|
||||||
|
Ctx.StrateTopWorldZ = P.StrateTopWorldZ;
|
||||||
|
Ctx.StrateBottomWorldZ = P.StrateBottomWorldZ;
|
||||||
|
Stack.PrepareChunk(Ctx);
|
||||||
|
|
||||||
|
// Pas de biomes : contexte vide ⇒ ComputeSurfaceColumn retombe sur BaseSurface pour les
|
||||||
|
// deux côtés, poids 0. C'est exactement ce que la pile fait aujourd'hui.
|
||||||
|
FBiomeContext EmptyCtx;
|
||||||
|
TArray<FSurfaceGenerationParams> NoBiomeParams;
|
||||||
|
FChunkBiomeCache BiomeCache;
|
||||||
|
|
||||||
|
int32 NumDiff = 0, NumSideDisagree = 0, NumInWindow = 0;
|
||||||
|
float WorstDelta = 0.0f;
|
||||||
|
|
||||||
|
FRandomStream Rng(1337);
|
||||||
|
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);
|
||||||
|
|
||||||
|
float TerrainZ = 0.0f, CeilSurf = 0.0f, Amp = 0.0f, DirX = 0.0f, DirY = 0.0f;
|
||||||
|
Gen->ComputeSurfaceColumn(X, Y, MidChunkZ, P, EmptyCtx, NoBiomeParams, BiomeCache,
|
||||||
|
TerrainZ, CeilSurf, Amp, DirX, DirY);
|
||||||
|
|
||||||
|
// Échantillonner DANS la fenêtre d'overhang la moitié du temps : un tirage uniforme sur
|
||||||
|
// toute la strate la raterait presque toujours, et le test serait vert sans avoir
|
||||||
|
// exercé l'op une seule fois — le même piège que `WaterLevelRelative` plus haut.
|
||||||
|
float Z;
|
||||||
|
if ((i & 1) && Amp > 0.0f)
|
||||||
|
{
|
||||||
|
Z = TerrainZ + P.OverhangHeight * ((float)(i % 97) / 97.0f);
|
||||||
|
++NumInWindow;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Z = (float)Rng.RandRange(BottomVoxelZ, TopVoxelZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
const float Old = Gen->SurfaceDensityFromColumn(X, Y, Z, TerrainZ, CeilSurf,
|
||||||
|
Amp, DirX, DirY, P);
|
||||||
|
const float New = Stack.EvalMC(X, Y, Z);
|
||||||
|
|
||||||
|
if (!BitEqual(Old, New))
|
||||||
|
{
|
||||||
|
++NumDiff;
|
||||||
|
WorstDelta = FMath::Max(WorstDelta, FMath::Abs(Old - New));
|
||||||
|
}
|
||||||
|
if ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSideDisagree; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NumDiff == 0)
|
||||||
|
{
|
||||||
|
AddInfo(FString::Printf(
|
||||||
|
TEXT("Overhang: bit-identical to SurfaceDensityFromColumn across %d samples, %d of ")
|
||||||
|
TEXT("them deliberately inside the overhang window. The per-column memo hands the ")
|
||||||
|
TEXT("source's column to the overhang op correctly."),
|
||||||
|
NumHeightSamples, NumInWindow));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AddError(FString::Printf(
|
||||||
|
TEXT("Overhang: %d of %d samples differ (largest |delta| %.9g); %d cross the ")
|
||||||
|
TEXT("isosurface; %d samples were inside the window. Check, in order: the column ")
|
||||||
|
TEXT("memo key (does the overhang op see the SAME column as the source?), the ")
|
||||||
|
TEXT("window gate (Z > TerrainZ && Z <= TerrainZ + OverhangHeight), Frac and the ")
|
||||||
|
TEXT("ShiftV > 0.5 threshold, the shelf noise offsets (17.3/23.9/5.1 with the ")
|
||||||
|
TEXT("OverhangZScale Z term), and that the shift resamples the STRUCTURAL height."),
|
||||||
|
NumDiff, NumHeightSamples, WorstDelta, NumSideDisagree, NumInWindow));
|
||||||
|
}
|
||||||
|
|
||||||
|
TestEqual(TEXT("overhang: no sample lands on the opposite side of the isosurface"),
|
||||||
|
NumSideDisagree, 0);
|
||||||
|
|
||||||
|
if (NumInWindow == 0)
|
||||||
|
{
|
||||||
|
AddWarning(TEXT("No sample landed inside the overhang window, so the op was never ")
|
||||||
|
TEXT("actually exercised. Raise OverhangStrength or lower ")
|
||||||
|
TEXT("OverhangSlopeThreshold until this is well above zero."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,19 @@
|
|||||||
#include "VoxelStrateManager.h" // EvaluateModifierSDF / AnyPassageNearBox
|
#include "VoxelStrateManager.h" // EvaluateModifierSDF / AnyPassageNearBox
|
||||||
#include "VoxelTypes.h" // SmoothStep01, VOXEL_NOISE_SCALE
|
#include "VoxelTypes.h" // SmoothStep01, VOXEL_NOISE_SCALE
|
||||||
|
|
||||||
|
#include <atomic> // l'id d'instance non recyclé du mémo de colonne
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
/** La même enveloppe que `FractalNoise3D` de VoxelGenerator.cpp (qui y est `static`, donc
|
||||||
|
* invisible ici). Le détour par `FVector` est délibéré — voir l'en-tête de ce fichier. */
|
||||||
|
FORCEINLINE float HFractal3D(const FVector& Position, int32 Octaves = 4,
|
||||||
|
float Lacunarity = 2.0f, float Persistence = 0.5f)
|
||||||
|
{
|
||||||
|
return VoxelNoise::FBM((float)Position.X, (float)Position.Y, (float)Position.Z,
|
||||||
|
Octaves, Lacunarity, Persistence);
|
||||||
|
}
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// RÔLE 1 — SOURCE : ROC CONSTANT / CONSTANT ROCK
|
// RÔLE 1 — SOURCE : ROC CONSTANT / CONSTANT ROCK
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
@@ -406,12 +417,84 @@ namespace
|
|||||||
class FSurfaceColumnSource final : public IVoxelDensityOp
|
class FSurfaceColumnSource final : public IVoxelDensityOp
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
FSurfaceColumnSource(const FSurfaceGenerationParams& P, int32 Seed)
|
FSurfaceColumnSource(const FSurfaceGenerationParams& InP, int32 Seed)
|
||||||
|
: P(InP)
|
||||||
{
|
{
|
||||||
VoxelHeightOps::BuildSurfaceHeightStack(TerrainStack, P, Seed);
|
// Construite à la main (pas via BuildSurfaceHeightStack) pour GARDER le pointeur vers la
|
||||||
VoxelHeightOps::BuildSurfaceCeilingStack(CeilingStack, P, Seed);
|
// source structurelle : l'overhang en a besoin, pour son gradient de pente comme pour
|
||||||
|
// son ré-échantillonnage amont. Même dépendance que le cliff, même raison.
|
||||||
|
TerrainStack.Add(VoxelHeightOps::MakeStructuralHeightSource(InP, Seed, &Structural));
|
||||||
|
TerrainStack.Add(VoxelHeightOps::MakeCliffHeightMod(InP, Structural));
|
||||||
|
TerrainStack.Add(VoxelHeightOps::MakeTerraceHeightMod(InP));
|
||||||
|
TerrainStack.Add(VoxelHeightOps::MakeLayerLineHeightMod(InP));
|
||||||
|
TerrainStack.Add(VoxelHeightOps::MakeBeachHeightMod(InP));
|
||||||
|
|
||||||
|
VoxelHeightOps::BuildSurfaceCeilingStack(CeilingStack, InP, Seed);
|
||||||
|
|
||||||
|
// Identité unique et NON RECYCLÉE. Clé du mémo par colonne ci-dessous : `this` ne
|
||||||
|
// suffirait pas — une pile détruite puis une autre allouée à la même adresse avec
|
||||||
|
// d'autres params donnerait un faux positif silencieux. Un compteur qui ne redescend
|
||||||
|
// jamais rend ça impossible.
|
||||||
|
// A unique, never-recycled id: `this` would allow a freed-then-reallocated stack to
|
||||||
|
// collide with the previous one's memo. A monotonic counter cannot.
|
||||||
|
static std::atomic<uint64> NextId{ 1 };
|
||||||
|
InstanceId = NextId.fetch_add(1, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** La colonne complète, exactement les cinq sorties de `ComputeSurfaceColumn`.
|
||||||
|
* Mémoïsée par (instance, X, Y) : la pile évalue tous les Z d'une colonne au même XY, donc
|
||||||
|
* le taux de succès est ~1 et l'overhang lit la MÊME colonne que la source, par
|
||||||
|
* construction plutôt que par convention. */
|
||||||
|
struct FColumn { float TerrainZ, CeilSurf, OverhangAmp, DirX, DirY; };
|
||||||
|
|
||||||
|
const FColumn& GetColumn(float WorldX, float WorldY) const
|
||||||
|
{
|
||||||
|
thread_local FColumn C{};
|
||||||
|
thread_local uint64 CachedId = 0;
|
||||||
|
thread_local float CachedX = FLT_MAX, CachedY = FLT_MAX;
|
||||||
|
|
||||||
|
if (CachedId != InstanceId || CachedX != WorldX || CachedY != WorldY)
|
||||||
|
{
|
||||||
|
CachedId = InstanceId; CachedX = WorldX; CachedY = WorldY;
|
||||||
|
|
||||||
|
C.TerrainZ = TerrainStack.EvalHeight(WorldX, WorldY);
|
||||||
|
C.CeilSurf = CeilingStack.EvalHeight(WorldX, WorldY);
|
||||||
|
C.OverhangAmp = 0.0f; C.DirX = 0.0f; C.DirY = 0.0f;
|
||||||
|
|
||||||
|
// Gate d'overhang par colonne : pente issue d'une différence AVANT du champ
|
||||||
|
// STRUCTUREL, à l'échelle de la portée mais CLAMPÉE à [4,16]. Sans ce clamp, une
|
||||||
|
// grande `Reach` moyenne la pente sur une énorme portée et lit même une vraie
|
||||||
|
// falaise comme plate — le bug « grande Reach = rien ». Transcrit tel quel.
|
||||||
|
if (P.OverhangStrength > 0.0f && Structural != nullptr)
|
||||||
|
{
|
||||||
|
const float SD = FMath::Clamp(P.OverhangReach, 4.0f, 16.0f);
|
||||||
|
const float Z0 = SampleStructural(WorldX, WorldY);
|
||||||
|
const float GX = (SampleStructural(WorldX + SD, WorldY) - Z0) / SD;
|
||||||
|
const float GY = (SampleStructural(WorldX, WorldY + SD) - Z0) / SD;
|
||||||
|
const float Slope = FMath::Sqrt(GX * GX + GY * GY);
|
||||||
|
|
||||||
|
const float Thr = FMath::Max(P.OverhangSlopeThreshold, 0.05f);
|
||||||
|
const float Gate = FMath::Clamp((Slope - Thr) / Thr, 0.0f, 1.0f);
|
||||||
|
C.OverhangAmp = P.OverhangStrength * Gate; // [0,1]
|
||||||
|
|
||||||
|
// Direction amont unitaire (le gradient pointe vers le haut). Dégénérée sur le
|
||||||
|
// plat — mais l'amplitude y vaut 0 de toute façon.
|
||||||
|
if (Slope > KINDA_SMALL_NUMBER) { C.DirX = GX / Slope; C.DirY = GY / Slope; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return C;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Le champ structurel nu — l'overhang s'en sert pour emprunter la roche amont. */
|
||||||
|
float SampleStructural(float WorldX, float WorldY) const
|
||||||
|
{
|
||||||
|
FVoxelHeightSample S;
|
||||||
|
Structural->Eval(WorldX, WorldY, S);
|
||||||
|
return S.Height;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FSurfaceGenerationParams& GetParams() const { return P; }
|
||||||
|
|
||||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
|
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
|
||||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||||
bool IsXYPure() const override { return false; } // voir le bloc ci-dessus
|
bool IsXYPure() const override { return false; } // voir le bloc ci-dessus
|
||||||
@@ -426,11 +509,10 @@ namespace
|
|||||||
// existant plutôt que d'en inventer un second.
|
// 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
|
// 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.
|
// cache key is a second thing to get wrong.
|
||||||
const float TerrainZ = TerrainStack.EvalHeight(WorldX, WorldY);
|
const FColumn& C = GetColumn(WorldX, WorldY);
|
||||||
const float CeilSurf = CeilingStack.EvalHeight(WorldX, WorldY);
|
|
||||||
|
|
||||||
float Density = TerrainZ - WorldZ;
|
float Density = C.TerrainZ - WorldZ;
|
||||||
Density = FMath::Max(Density, WorldZ - CeilSurf);
|
Density = FMath::Max(Density, WorldZ - C.CeilSurf);
|
||||||
InOut.Density = Density; // Replace : interne, positif = solide
|
InOut.Density = Density; // Replace : interne, positif = solide
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -445,8 +527,89 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
FSurfaceGenerationParams P;
|
||||||
FVoxelHeightStack TerrainStack;
|
FVoxelHeightStack TerrainStack;
|
||||||
FVoxelHeightStack CeilingStack;
|
FVoxelHeightStack CeilingStack;
|
||||||
|
const IVoxelHeightOp* Structural = nullptr; // NON possédant : la pile terrain le possède
|
||||||
|
uint64 InstanceId = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// RÔLE 3 — MODIFIER : ÉTAGÈRE D'OVERHANG / OVERHANG SHELF (le seul op vraiment 3D)
|
||||||
|
//=========================================================================
|
||||||
|
// Pour les voxels d'AIR dans une fenêtre juste au-dessus d'une pente raide, ré-échantillonne le
|
||||||
|
// heightfield EN AMONT (vers la falaise) d'une distance qui CROÎT avec la hauteur, et fait
|
||||||
|
// l'union de cette roche → la roche du haut de falaise déborde AU-DESSUS du vide, avec de l'air
|
||||||
|
// EN DESSOUS : un vrai surplomb.
|
||||||
|
//
|
||||||
|
// ⚠️ Il dépend de Z de façon essentielle — `Frac` fait varier la portée avec l'altitude. C'est
|
||||||
|
// le seul op de SurfaceWorld qui ne pouvait PAS vivre en espace-hauteur, et c'est exactement
|
||||||
|
// pour ça que la frontière entre les deux espaces est utile : elle est passée là où le code
|
||||||
|
// change de nature, pas là où c'était commode.
|
||||||
|
//
|
||||||
|
// The one op here that genuinely depends on Z (the uphill reach grows with height), which is
|
||||||
|
// precisely why it could not live in height space. The boundary between the two spaces falls
|
||||||
|
// where the code changes nature.
|
||||||
|
class FOverhangShelfMod final : public IVoxelDensityOp
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
FOverhangShelfMod(const FSurfaceGenerationParams& InP, int32 Seed,
|
||||||
|
const FSurfaceColumnSource* InColumn)
|
||||||
|
: P(InP), SeedF((float)Seed), Column(InColumn) {}
|
||||||
|
|
||||||
|
EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; }
|
||||||
|
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||||
|
bool IsXYPure() const override { return false; } // franchement non : voir `Frac`
|
||||||
|
|
||||||
|
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||||
|
{
|
||||||
|
if (Column == nullptr || P.OverhangHeight <= 0.0f) { return; }
|
||||||
|
|
||||||
|
// Même colonne que la source, garantie par le mémo : pas une seconde évaluation.
|
||||||
|
const FSurfaceColumnSource::FColumn& C = Column->GetColumn(WorldX, WorldY);
|
||||||
|
if (C.OverhangAmp <= 0.0f) { return; }
|
||||||
|
|
||||||
|
// Gate dur, transcrit : seulement les voxels d'air dans `OverhangHeight` du sol local.
|
||||||
|
if (!(WorldZ > C.TerrainZ && WorldZ <= C.TerrainZ + P.OverhangHeight)) { return; }
|
||||||
|
|
||||||
|
const float f = P.OverhangFrequency;
|
||||||
|
// Bruit de forme d'étagère [0,1] ; le terme en Z fait onduler la portée avec la hauteur
|
||||||
|
// (déchiqueté, pas une lèvre lisse).
|
||||||
|
const float Ns = HFractal3D(FVector(
|
||||||
|
WorldX * f + SeedF * 17.3f,
|
||||||
|
WorldY * f + SeedF * 23.9f,
|
||||||
|
WorldZ * f * P.OverhangZScale + SeedF * 5.1f), 3) * 0.5f + 0.5f; // [0,1]
|
||||||
|
|
||||||
|
// LA CLÉ : la portée amont CROÎT avec la hauteur dans la fenêtre (Frac : 0 au sol → 1
|
||||||
|
// au plafond de la fenêtre). En bas le décalage est minuscule ⇒ on emprunte de la roche
|
||||||
|
// basse voisine ⇒ ça reste de l'AIR au-dessus du vide ; en haut le décalage atteint la
|
||||||
|
// falaise ⇒ solide ⇒ la lèvre se pose dessus avec de l'air DESSOUS = un vrai surplomb.
|
||||||
|
const float Frac = (WorldZ - C.TerrainZ) / P.OverhangHeight;
|
||||||
|
const float ShiftV = P.OverhangReach * C.OverhangAmp * Frac * Ns;
|
||||||
|
if (ShiftV > 0.5f)
|
||||||
|
{
|
||||||
|
// On emprunte la hauteur STRUCTURELLE amont (pas la surface complète avec ops) :
|
||||||
|
// le dessous de l'étagère n'a pas besoin du raffinement cliff/terrace, et ça évite
|
||||||
|
// de relancer les 4 resamples du cliff par voxel de lèvre.
|
||||||
|
const float ShiftedTZ = Column->SampleStructural(WorldX + C.DirX * ShiftV,
|
||||||
|
WorldY + C.DirY * ShiftV);
|
||||||
|
InOut.Density = FMath::Max(InOut.Density, ShiftedTZ - WorldZ); // union
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// N'ajoute que du solide (`Max`) ⇒ tue AllAir, jamais AllSolid. Conservateur : on ne sait
|
||||||
|
// pas sans échantillonner si une colonne d'overhang touche la boîte, donc FillOnly partout
|
||||||
|
// où l'archétype peut en produire, Identity quand il est éteint.
|
||||||
|
EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override
|
||||||
|
{
|
||||||
|
return (P.OverhangStrength > 0.0f && P.OverhangHeight > 0.0f)
|
||||||
|
? EVoxelOpEffect::FillOnly : EVoxelOpEffect::Identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
FSurfaceGenerationParams P;
|
||||||
|
float SeedF;
|
||||||
|
const FSurfaceColumnSource* Column; // NON possédant : la pile possède la source
|
||||||
};
|
};
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
@@ -876,17 +1039,19 @@ namespace VoxelDensityOps
|
|||||||
void BuildSurfaceStack(FVoxelOpStack& OutStack, const FSurfaceGenerationParams& P,
|
void BuildSurfaceStack(FVoxelOpStack& OutStack, const FSurfaceGenerationParams& P,
|
||||||
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager)
|
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager)
|
||||||
{
|
{
|
||||||
// ⚠️ PAS ENCORE L'ARCHÉTYPE COMPLET, et il faut le savoir avant de brancher :
|
// ⚠️ PAS ENCORE L'ARCHÉTYPE COMPLET : il manque le **MÉLANGE DE BIOMES** — le sol est
|
||||||
// • pas d'OVERHANG — `FOverhangShelfMod`, le seul op vraiment 3D d'ici, a besoin d'une
|
// évalué pour le biome dominant puis lerpé vers le voisin. En termes de pile c'est le
|
||||||
// donnée PAR COLONNE (amp + direction amont) que `GetSurfaceDensity` ne calcule même
|
// combiner `Mask`, et `§5` en fait le prototype de la Phase 3 : ça mérite sa propre étape,
|
||||||
// pas (il passe `OverhangAmp = 0`). Sa référence est le chemin caché, pas celui-ci ;
|
// pas un paramètre de plus ici. **Ne pas brancher dans un monde à biomes avant.**
|
||||||
// • 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
|
// L'overhang, lui, est là depuis l'étape 2b.
|
||||||
// SANS biomes. C'est ce que le test compare, et c'est pour ça que la comparaison est nette.
|
TUniquePtr<FSurfaceColumnSource> ColumnSource = MakeUnique<FSurfaceColumnSource>(P, Seed);
|
||||||
OutStack.Add(MakeSurfaceColumnSource(P, Seed));
|
const FSurfaceColumnSource* ColumnPtr = ColumnSource.Get();
|
||||||
|
OutStack.Add(MoveTemp(ColumnSource));
|
||||||
|
|
||||||
|
// L'overhang lit la colonne de la source (mémo partagé, même XY par construction). Même
|
||||||
|
// motif que cliff → structural : un modificateur qui a besoin de ce que la source a produit.
|
||||||
|
OutStack.Add(MakeUnique<FOverhangShelfMod>(P, Seed, ColumnPtr));
|
||||||
|
|
||||||
OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ,
|
OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ,
|
||||||
P.BoundarySealThickness, P.BaseDensity, SpineRadius, StrateManager);
|
P.BoundarySealThickness, P.BaseDensity, SpineRadius, StrateManager);
|
||||||
|
|||||||
@@ -188,6 +188,26 @@ public:
|
|||||||
*/
|
*/
|
||||||
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
|
||||||
|
* (amplitude + direction amont). PUBLIQUES toutes deux pour la même raison que ci-dessus :
|
||||||
|
* c'est le seul chemin qui calcule l'overhang — `GetSurfaceDensity` passe `OverhangAmp = 0` —
|
||||||
|
* donc c'est la seule référence possible pour `FOverhangShelfMod`.
|
||||||
|
* Public because this is the ONLY path that computes the overhang (GetSurfaceDensity passes 0),
|
||||||
|
* so it is the only possible reference for the ported op.
|
||||||
|
*/
|
||||||
|
void ComputeSurfaceColumn(float WorldX, float WorldY, int32 ChunkZ,
|
||||||
|
const FSurfaceGenerationParams& BaseSurface, const FBiomeContext& BiomeCtx,
|
||||||
|
const TArray<FSurfaceGenerationParams>& BiomeParams, FChunkBiomeCache& BiomeCache,
|
||||||
|
float& OutTerrainZ, float& OutCeilSurf,
|
||||||
|
float& OutOverhangAmp, float& OutDirX, float& OutDirY) const;
|
||||||
|
|
||||||
|
/** Le combine par voxel : colonne → densité, overhang compris, puis le post structurel. */
|
||||||
|
float SurfaceDensityFromColumn(float WorldX, float WorldY, float WorldZ,
|
||||||
|
float TerrainZ, float CeilSurf,
|
||||||
|
float OverhangAmp, float DirX, float DirY,
|
||||||
|
const FSurfaceGenerationParams& S) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Moisture field at a world XY → [0,1]. The second climate axis for biome placement.
|
* Moisture field at a world XY → [0,1]. The second climate axis for biome placement.
|
||||||
*/
|
*/
|
||||||
@@ -299,22 +319,10 @@ private:
|
|||||||
FSurfaceGenerationParams& OutSurface, FBiomeContext& OutBiomeCtx,
|
FSurfaceGenerationParams& OutSurface, FBiomeContext& OutBiomeCtx,
|
||||||
TArray<FSurfaceGenerationParams>& OutBiomeParams) const;
|
TArray<FSurfaceGenerationParams>& OutBiomeParams) const;
|
||||||
|
|
||||||
/** Biome-blended terrain Z + sky-cap ceiling Z for one column (the XY-only surface field). Shared
|
// ComputeSurfaceColumn et SurfaceDensityFromColumn ont été DÉPLACÉES en `public` (voir plus
|
||||||
* by the density column cache (T1.a) and the oracle. F20 phase 2 overhang, resolved per column:
|
// haut) : c'est le seul chemin qui calcule l'overhang, donc la seule référence possible pour
|
||||||
* `OutOverhangAmp` = strength·slope-gate (0 = off), `(OutDirX,OutDirY)` = unit UPHILL gradient dir. */
|
// VoxelForge.OpStack.SurfaceHeightEquivalence. Une seule déclaration chacune.
|
||||||
void ComputeSurfaceColumn(float WorldX, float WorldY, int32 ChunkZ,
|
// Moved to public above — the only path that computes the overhang, hence the only oracle.
|
||||||
const FSurfaceGenerationParams& BaseSurface, const FBiomeContext& BiomeCtx,
|
|
||||||
const TArray<FSurfaceGenerationParams>& BiomeParams, FChunkBiomeCache& BiomeCache,
|
|
||||||
float& OutTerrainZ, float& OutCeilSurf,
|
|
||||||
float& OutOverhangAmp, float& OutDirX, float& OutDirY) const;
|
|
||||||
|
|
||||||
/** Final SurfaceWorld density from a column's precomputed terrain Z + ceiling: the cheap per-voxel
|
|
||||||
* Z-combine + F20 overhang shelf (warped-terrain union, uphill dir) + origin spine + seal + passages.
|
|
||||||
* The XY-only work (terrain/ceiling/overhang amp+dir) is done once per column and cached (T1.a). */
|
|
||||||
float SurfaceDensityFromColumn(float WorldX, float WorldY, float WorldZ,
|
|
||||||
float TerrainZ, float CeilSurf,
|
|
||||||
float OverhangAmp, float DirX, float DirY,
|
|
||||||
const FSurfaceGenerationParams& Structural) const;
|
|
||||||
|
|
||||||
/** (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,
|
||||||
|
|||||||
Reference in New Issue
Block a user