feat(opstack B3): port cave Overhang, Cliff, Scallop, Arch (STEP 4c)
STAGE B group 3 of 5. The stack is now 15 ops: ConstantRock -> RoomGraph -> SdfCarve -> Roughness -> Terrace -> LayerLines -> Ribbing -> Overhang -> Cliff -> Scallop -> Arch -> Worms -> [structural x3] Still NOT wired: UsesOperatorStackForChunk returns false for TunnelNetwork. TRANSCRIBED - FCaveOverhangMod: fBM with Z frequency at 0.15x of XY, positive lobe only (rock extends INTO the cave, never away), quadratic fade. Note this is the THIRD unrelated thing called "overhang" in the plugin -- FOverhangShelfMod (SurfaceWorld) borrows the uphill terrain height and shares nothing with it but the name. - FScallopMod: cellular noise, positive lobe carved out. Second consumer of VoxelNoise::Cellular3D, which is the second reason B1 shared that body instead of copying it. - FCaveArchMod: the first ROOM-RELATIVE modifier -- it needs the room's hash, centre and radii, so it is what makes FRoomGraphSource::GetNearestRoomIdx() necessary. Its gate is NOT the shared one: CaveSDF < SDFBlendRadius (inside the cave), not < SDFBlendRadius*3. PORTED AS-IS THOUGH THE ORIGINAL'S OWN COMMENT DISAGREES WITH ITS CODE FCaveCliffMod's comment promises "sample density at Z+1 and Z-1, compute vertical gradient". The code samples nothing: it draws a Perlin whose Z frequency is 3x its XY frequency and names the result VertGrad. It is a gradient-shaped PROXY, decorrelated from the wall's actual slope; the multiply by CaveSDF still gives it the right SIGN either side of the surface, which is why it produces steeper faces at all. Fixing it would change the world, and saying nothing would leave it to be "fixed" later by someone reading the comment instead of the code. Ported, documented in the op, recorded in OPSTACK-PROGRESS. ONE DELIBERATE ADDITION FCaveArchMod bounds-checks NearestRoomIdx with IsValidIndex before indexing; the original tests only >= 0. The index comes from EvaluateSDFCached so it is valid by construction, which is exactly why this can never change output -- only prevent a crash if that invariant ever breaks. Same class of decision as the params fingerprint in the cache key: err on cost, never on result. Overhang is FillOnly, Scallop is CarveOnly, Arch is FillOnly; only Cliff needs Both. Four of the eight modifiers ported so far keep a usable direction for the fold, which matters for the DECOMPOSITION 0.2 work later. TEST (same commit): op count 11 -> 15, four params groups move into EnableTunnelFeatures, four more per-operator coverage probes. ArchDensity is set to 0.9 rather than its 0.1 default and the reason is written down: at 0.1 a room draws ~0.3 arches and the probe would report zero with nothing wrong. Test coverage settings are not production values. IF THIS GROUP IS WRONG: diffs concentrate inside open cave (arch) or in a band |SDF| < range (the other three), and the roughness/terrace probes stay green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
// VoxelForgeOpStackTunnelTest.cpp
|
||||
// TunnelNetwork — ÉTAPE A (squelette SDF) + ÉTAPE B1 (rugosité de paroi, 4b).
|
||||
// TunnelNetwork — STAGE A (the SDF spine) + STAGE B1 (wall roughness, 4b).
|
||||
// TunnelNetwork — ÉTAPE A (squelette SDF) + ÉTAPES B1–B3 (huit modificateurs de détail, 4b–4c).
|
||||
// TunnelNetwork — STAGE A (the SDF spine) + STAGES B1-B3 (eight detail modifiers, 4b-4c).
|
||||
//
|
||||
// POURQUOI UN TEST D'UNE PILE INCOMPLÈTE
|
||||
// `GetDensityWithParams` fait ~1080 lignes et treize modificateurs de détail. Tout porter avant de
|
||||
@@ -19,11 +19,13 @@
|
||||
// de `DisableStageBModifiers` et entre dans `EnableTunnelFeatures`, avec (i) une sonde de couverture
|
||||
// qui prouve qu'il a réellement bougé quelque chose et (ii) le compte d'ops de la pile qui augmente.
|
||||
// • B1 : rugosité de paroi, STEP 4b.
|
||||
// • B2 (ce commit) : terrasses, lignes de strates, nervures — STEP 4c.
|
||||
// • B2 : terrasses, lignes de strates, nervures — STEP 4c.
|
||||
// • B3 (ce commit) : surplombs, falaise, festons, arches — STEP 4c.
|
||||
//
|
||||
// CE QUE CE TEST NE PROUVE PAS (et le dit) : rien sur les huit modificateurs restants (4c–4h), rien
|
||||
// sur l'override d'op par salle, et rien sur le saut de tuile — `FRoomGraphSource::EffectOverBox`
|
||||
// rend `Both`, donc aucun verdict n'est prouvable à ce stade. Ce sont les étapes B2–B5 et C.
|
||||
// CE QUE CE TEST NE PROUVE PAS (et le dit) : rien sur les quatre modificateurs restants (colonnes,
|
||||
// dômes, pincement, biais de sol), rien sur l'override d'op par salle, et rien sur le saut de
|
||||
// tuile — `FRoomGraphSource::EffectOverBox` rend `Both`, donc aucun verdict n'est prouvable à ce
|
||||
// stade. Ce sont les étapes B4, B5 et C.
|
||||
//
|
||||
// ⚠️ ÉCHANTILLONNAGE PAR GRAPPES, PAS UNIFORME. Le cache SDF se reconstruit quand la requête sort de
|
||||
// sa boîte de recherche ; 20 000 points uniformément aléatoires feraient ~20 000 `BuildChunkCache`
|
||||
@@ -67,10 +69,6 @@ namespace
|
||||
*/
|
||||
void DisableStageBModifiers(FStrateGenerationParams& P)
|
||||
{
|
||||
P.OverhangStrength = 0.0f;
|
||||
P.CliffStrength = 0.0f;
|
||||
P.ScallopStrength = 0.0f;
|
||||
P.ArchDensity = 0.0f;
|
||||
P.ColumnDensity = 0.0f; // ⚠️ celui-ci se cuit dans SDFCache.Columns, pas un `if`
|
||||
P.DomeDensity = 0.0f;
|
||||
P.PinchDensity = 0.0f;
|
||||
@@ -124,6 +122,21 @@ namespace
|
||||
P.LayerLineDepth = 0.35f;
|
||||
P.RibbingSpacing = 4.0f;
|
||||
P.RibbingDepth = 0.4f;
|
||||
|
||||
// ── ÉTAPE B3 : surplombs · falaise · festons · arches (4c) ───────────────────────────
|
||||
P.OverhangStrength = 0.6f;
|
||||
P.OverhangDepth = 5.0f;
|
||||
P.OverhangFrequency= 0.06f;
|
||||
P.CliffStrength = 0.5f;
|
||||
P.ScallopStrength = 0.8f;
|
||||
P.ScallopFrequency = 0.1f;
|
||||
// ⚠️ ArchDensity BEAUCOUP plus haut que le défaut (0.1) : chaque salle tire 3 arches et
|
||||
// n'en garde que celles dont `hash01 <= ArchDensity`. À 0.1, ~0.3 arche par salle et une
|
||||
// fenêtre d'influence de 2 voxels autour d'une capsule — la sonde de couverture rapporterait
|
||||
// zéro sans que rien ne soit faux. La couverture d'un test n'est pas la valeur de prod.
|
||||
P.ArchDensity = 0.9f;
|
||||
P.ArchMinRadius = 3.0f;
|
||||
P.ArchMaxRadius = 6.0f;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -217,6 +230,14 @@ namespace
|
||||
[](FStrateGenerationParams& Q) { Q.LayerLineSpacing = 0.0f; } },
|
||||
{ TEXT("B2 ribbing (STEP 4c)"),
|
||||
[](FStrateGenerationParams& Q) { Q.RibbingSpacing = 0.0f; } },
|
||||
{ TEXT("B3 cave overhangs (STEP 4c)"),
|
||||
[](FStrateGenerationParams& Q) { Q.OverhangStrength = 0.0f; } },
|
||||
{ TEXT("B3 cliff sharpening (STEP 4c)"),
|
||||
[](FStrateGenerationParams& Q) { Q.CliffStrength = 0.0f; } },
|
||||
{ TEXT("B3 scallop (STEP 4c)"),
|
||||
[](FStrateGenerationParams& Q) { Q.ScallopStrength = 0.0f; } },
|
||||
{ TEXT("B3 arches (STEP 4c)"),
|
||||
[](FStrateGenerationParams& Q) { Q.ArchDensity = 0.0f; } },
|
||||
// ⚠️ UNE SONDE PAR OPÉRATEUR, PAS UNE PAR GROUPE. Le groupe B2 en contient trois ; une seule
|
||||
// sonde « B2 » serait verte tant qu'UN des trois tire, et les deux autres pourraient être
|
||||
// faux sans que rien ne le dise. La granularité de la sonde est la granularité de la preuve.
|
||||
@@ -308,12 +329,11 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
|
||||
VoxelDensityOps::BuildTunnelNetworkStack(Stack, P, World.Settings->Seed,
|
||||
Gen->OriginSpineRadius, World.StrateManager.Get());
|
||||
|
||||
// rock + roomgraph + carve + **rugosité + terrasses + lignes + nervures (B1, B2)** + worms
|
||||
// + 3 structurels = 11.
|
||||
// Les huit modificateurs restants viendront s'insérer entre les nervures et les vers, donc ce
|
||||
// rock + roomgraph + carve + **8 modificateurs (B1 · B2 · B3)** + worms + 3 structurels = 15.
|
||||
// Les quatre modificateurs restants viendront s'insérer entre les arches et les vers, donc ce
|
||||
// nombre DOIT bouger à chaque groupe de l'étape B — c'est un compteur de progression, pas une
|
||||
// formalité : une pile qui ne grandit pas est une pile dont l'opérateur n'a pas été ajouté.
|
||||
TestEqual(TEXT("the stage-A+B1+B2 tunnel stack is decomposed into 11 ops"), Stack.Num(), 11);
|
||||
TestEqual(TEXT("the stage-A+B1..B3 tunnel stack is decomposed into 15 ops"), Stack.Num(), 15);
|
||||
|
||||
FVoxelOpContext Ctx;
|
||||
Ctx.Seed = (uint32)World.Settings->Seed;
|
||||
@@ -385,15 +405,15 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
|
||||
if (NumDiff == 0)
|
||||
{
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("TunnelNetwork STAGE A+B1: bit-identical across %d samples in %d chunks (%d in ")
|
||||
TEXT("TunnelNetwork STAGE A+B: bit-identical across %d samples in %d chunks (%d in ")
|
||||
TEXT("open cave, %d in rock, away from the seal bands). Exercised: vertical scale (1.35, ")
|
||||
TEXT("so effective Z differs from world Z everywhere), cave warp, the room/tunnel SDF via ")
|
||||
TEXT("the SHARED BuildChunkCache, the carve with its floored divisor, wall roughness ")
|
||||
TEXT("(4b, density-space variant), and the worm carve with its network mask. Pits, ")
|
||||
TEXT("chimneys and roughness are covered only insofar as the bake-coverage and ")
|
||||
TEXT("group-coverage lines below report non-zero -- this message used to CLAIM coverage ")
|
||||
TEXT("outright, and was wrong for a whole run. NOT covered at all: the eleven remaining ")
|
||||
TEXT("detail modifiers (4c-4h), the per-room op override, and any tile verdict."),
|
||||
TEXT("the SHARED BuildChunkCache, the carve with its floored divisor, the ported detail ")
|
||||
TEXT("modifiers of 4b-4c, and the worm carve with its network mask. Every one of those ")
|
||||
TEXT("is covered ONLY insofar as the bake-coverage and group-coverage lines below report ")
|
||||
TEXT("non-zero -- this message used to CLAIM coverage outright, and was wrong for a ")
|
||||
TEXT("whole run. NOT covered at all: whatever is still listed in ")
|
||||
TEXT("DisableStageBModifiers, the per-room op override, and any tile verdict."),
|
||||
NumTunnelSamples, NumTunnelChunks, NumInCave, NumInRock));
|
||||
|
||||
AddInfo(FString::Printf(
|
||||
@@ -406,7 +426,7 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
|
||||
else
|
||||
{
|
||||
AddError(FString::Printf(
|
||||
TEXT("TunnelNetwork STAGE A: %d of %d samples differ (largest |delta| %.9g at ")
|
||||
TEXT("TunnelNetwork STAGE A+B: %d of %d samples differ (largest |delta| %.9g at ")
|
||||
TEXT("(%.0f, %.0f, %.0f)); %d cross the isosurface. Check, in order: the carve's ")
|
||||
TEXT("MinDivisor (TunnelNetwork floors Blend*2 at 1.0 and the other archetypes do NOT ")
|
||||
TEXT("-- getting this wrong only shows up when SDFBlendRadius*2 < 1), then EffectiveZ ")
|
||||
|
||||
@@ -2456,6 +2456,285 @@ namespace
|
||||
FStrateGenerationParams P;
|
||||
};
|
||||
|
||||
//=========================================================================
|
||||
// RÔLE 3 — MODIFIER : SURPLOMBS DE GROTTE / CAVE OVERHANGS (TunnelNetwork, STEP 4c)
|
||||
//=========================================================================
|
||||
// ⚠️ TROISIÈME OPÉRATEUR NOMMÉ « OVERHANG » DANS CE FICHIER, et ils n'ont rien en commun :
|
||||
// • `FOverhangShelfMod` (SurfaceWorld) emprunte la hauteur de terrain amont — géométrique ;
|
||||
// • celui-ci est un bruit fBM à FRÉQUENCE EN Z RÉDUITE (×0.15), dont on ne garde que le lobe
|
||||
// POSITIF : la roche ne s'étend que VERS la grotte, jamais en creux. D'où des étagères
|
||||
// éparses au lieu d'un déplacement uniforme.
|
||||
// Même nom dans l'éditeur, deux idées différentes. `OPSTACK-DECOMPOSITION §1` les sépare déjà.
|
||||
class FCaveOverhangMod final : public IVoxelDensityOp
|
||||
{
|
||||
public:
|
||||
FCaveOverhangMod(const FStrateGenerationParams& InP, int32 Seed)
|
||||
: P(InP), SeedU((uint32)Seed) {}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
{
|
||||
const float CaveSDF = InOut.Sdf;
|
||||
if (!VF_NearCaveSurface(CaveSDF, P.SDFBlendRadius)) { return; }
|
||||
if (!(P.OverhangStrength > 0.0f && CaveSDF < FLT_MAX)) { return; }
|
||||
|
||||
const float DistFromSurface = FMath::Abs(CaveSDF);
|
||||
const float OverhangRange = P.OverhangDepth * 2.0f;
|
||||
if (!(DistFromSurface < OverhangRange)) { return; }
|
||||
|
||||
const float EffectiveZ = (P.VerticalScale != 1.0f && P.VerticalScale > 0.0f)
|
||||
? (WorldZ / P.VerticalScale) : WorldZ;
|
||||
|
||||
// Fréquence en Z à 0.15× celle de XY ⇒ les motifs s'étirent horizontalement.
|
||||
const float OverhangNoise = HFractal3D(FVector(
|
||||
WorldX * P.OverhangFrequency + VoxelHash::SeedOffset(SeedU, 53.1f),
|
||||
WorldY * P.OverhangFrequency + VoxelHash::SeedOffset(SeedU, 59.3f),
|
||||
EffectiveZ * P.OverhangFrequency * 0.15f + VoxelHash::SeedOffset(SeedU, 61.7f)
|
||||
), VoxelGenLOD::Eff(2)) * VOXEL_NOISE_SCALE;
|
||||
|
||||
if (OverhangNoise > 0.0f)
|
||||
{
|
||||
float Fade = 1.0f - (DistFromSurface / OverhangRange);
|
||||
Fade = Fade * Fade;
|
||||
|
||||
InOut.Density += OverhangNoise * P.OverhangDepth * P.OverhangStrength * Fade;
|
||||
}
|
||||
}
|
||||
|
||||
/** Lobe positif seulement ⇒ n'AJOUTE que du solide ⇒ `FillOnly`. */
|
||||
EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override
|
||||
{
|
||||
return (P.OverhangStrength > 0.0f && P.OverhangDepth > 0.0f)
|
||||
? EVoxelOpEffect::FillOnly : EVoxelOpEffect::Identity;
|
||||
}
|
||||
|
||||
private:
|
||||
FStrateGenerationParams P;
|
||||
uint32 SeedU;
|
||||
};
|
||||
|
||||
//=========================================================================
|
||||
// RÔLE 3 — MODIFIER : AFFÛTAGE DE FALAISE / CLIFF SHARPENING (TunnelNetwork, STEP 4c)
|
||||
//=========================================================================
|
||||
// ⚠️ TRANSCRIT TEL QUEL BIEN QUE LE COMMENTAIRE DE L'ORIGINAL DÉCRIVE AUTRE CHOSE.
|
||||
// Il annonce « échantillonner la densité en Z±1 et calculer le gradient vertical » ; le code, lui,
|
||||
// n'échantillonne RIEN : il tire un Perlin dont la fréquence en Z est 3× celle de XY et l'appelle
|
||||
// `VertGrad`. C'est un PROXY de gradient, pas un gradient — donc l'effet est décorrélé de la
|
||||
// pente réelle de la paroi. Le multiplier par `CaveSDF` lui donne quand même le bon SIGNE de part
|
||||
// et d'autre de la surface (plus solide côté roche, plus creusé côté air), ce qui suffit à
|
||||
// produire des faces plus raides.
|
||||
//
|
||||
// Corriger l'écart changerait le monde ; le taire le laisserait se faire « corriger » un jour par
|
||||
// quelqu'un qui lit le commentaire et pas le code. Noté ici ET dans OPSTACK-PROGRESS.
|
||||
//
|
||||
// Ported as written, not as commented: the original's comment promises a sampled vertical
|
||||
// gradient, the code uses a Z-stretched Perlin as a proxy. Fixing it would change the world.
|
||||
class FCaveCliffMod final : public IVoxelDensityOp
|
||||
{
|
||||
public:
|
||||
FCaveCliffMod(const FStrateGenerationParams& InP, int32 Seed)
|
||||
: P(InP), SeedU((uint32)Seed) {}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
{
|
||||
const float CaveSDF = InOut.Sdf;
|
||||
if (!VF_NearCaveSurface(CaveSDF, P.SDFBlendRadius)) { return; }
|
||||
if (!(P.CliffStrength > 0.0f && CaveSDF < FLT_MAX)) { return; }
|
||||
|
||||
const float DistFromSurface = FMath::Abs(CaveSDF);
|
||||
const float CliffRange = 8.0f; // constante en dur dans l'original
|
||||
if (!(DistFromSurface < CliffRange)) { return; }
|
||||
|
||||
const float EffectiveZ = (P.VerticalScale != 1.0f && P.VerticalScale > 0.0f)
|
||||
? (WorldZ / P.VerticalScale) : WorldZ;
|
||||
|
||||
const float VertGrad = VoxelNoise::Perlin3D(FVector(
|
||||
WorldX * 0.05f + VoxelHash::SeedOffset(SeedU, 71.3f),
|
||||
WorldY * 0.05f + VoxelHash::SeedOffset(SeedU, 73.7f),
|
||||
EffectiveZ * 0.15f + VoxelHash::SeedOffset(SeedU, 79.1f) // 3× plus vite en Z
|
||||
)) * VOXEL_NOISE_SCALE;
|
||||
|
||||
const float CliffEffect = VertGrad * CaveSDF * P.CliffStrength;
|
||||
|
||||
if (FMath::Abs(VertGrad) > 0.3f)
|
||||
{
|
||||
float Fade = 1.0f - (DistFromSurface / CliffRange);
|
||||
Fade = Fade * Fade;
|
||||
InOut.Density += CliffEffect * Fade * 3.0f;
|
||||
}
|
||||
}
|
||||
|
||||
/** `Both` : le signe suit celui de `VertGrad · CaveSDF`, donc les deux directions. */
|
||||
EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override
|
||||
{
|
||||
return (P.CliffStrength > 0.0f) ? EVoxelOpEffect::Both : EVoxelOpEffect::Identity;
|
||||
}
|
||||
|
||||
private:
|
||||
FStrateGenerationParams P;
|
||||
uint32 SeedU;
|
||||
};
|
||||
|
||||
//=========================================================================
|
||||
// RÔLE 3 — MODIFIER : FESTONS / SCALLOP (TunnelNetwork, STEP 4c)
|
||||
//=========================================================================
|
||||
// Cuvettes concaves d'érosion hydraulique. Bruit cellulaire : la valeur est haute AU CENTRE
|
||||
// d'une cellule (loin des points-features), donc on y creuse — d'où des rangées de coupelles
|
||||
// lisses, la signature des grottes calcaires.
|
||||
//
|
||||
// Deuxième client de `VoxelNoise::Cellular3D` (le premier est la rugosité en mode Cellular) —
|
||||
// c'est-à-dire la deuxième raison pour laquelle ce corps devait être PARTAGÉ et non recopié.
|
||||
class FScallopMod final : public IVoxelDensityOp
|
||||
{
|
||||
public:
|
||||
FScallopMod(const FStrateGenerationParams& InP, int32 Seed)
|
||||
: P(InP), SeedU((uint32)Seed) {}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
{
|
||||
const float CaveSDF = InOut.Sdf;
|
||||
if (!VF_NearCaveSurface(CaveSDF, P.SDFBlendRadius)) { return; }
|
||||
if (!(P.ScallopStrength > 0.0f && CaveSDF < FLT_MAX)) { return; }
|
||||
|
||||
const float DistFromSurface = FMath::Abs(CaveSDF);
|
||||
const float ScallopRange = P.ScallopStrength * 4.0f;
|
||||
if (!(DistFromSurface < ScallopRange)) { return; }
|
||||
|
||||
const float EffectiveZ = (P.VerticalScale != 1.0f && P.VerticalScale > 0.0f)
|
||||
? (WorldZ / P.VerticalScale) : WorldZ;
|
||||
|
||||
const float SF = P.ScallopFrequency;
|
||||
const float ScallopNoise = VoxelNoise::Cellular3D(FVector(
|
||||
WorldX * SF + VoxelHash::SeedOffset(SeedU, 83.1f),
|
||||
WorldY * SF + VoxelHash::SeedOffset(SeedU, 89.3f),
|
||||
EffectiveZ * SF + VoxelHash::SeedOffset(SeedU, 97.7f)
|
||||
));
|
||||
|
||||
if (ScallopNoise > 0.0f)
|
||||
{
|
||||
float Fade = 1.0f - (DistFromSurface / ScallopRange);
|
||||
Fade = Fade * Fade;
|
||||
|
||||
InOut.Density -= ScallopNoise * P.ScallopStrength * Fade;
|
||||
}
|
||||
}
|
||||
|
||||
/** Lobe positif seulement, SOUSTRAIT ⇒ `CarveOnly`. */
|
||||
EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override
|
||||
{
|
||||
return (P.ScallopStrength > 0.0f) ? EVoxelOpEffect::CarveOnly : EVoxelOpEffect::Identity;
|
||||
}
|
||||
|
||||
private:
|
||||
FStrateGenerationParams P;
|
||||
uint32 SeedU;
|
||||
};
|
||||
|
||||
//=========================================================================
|
||||
// RÔLE 3 — MODIFIER : ARCHES / PONTS DE ROCHE (TunnelNetwork, STEP 4c)
|
||||
//=========================================================================
|
||||
// ⚠️ PREMIER MODIFICATEUR RELATIF À LA SALLE. Il ne lit pas seulement `InOut.Sdf` : il lui faut
|
||||
// LA SALLE — son hash (pour tirer les arches de façon déterministe), son centre et ses rayons.
|
||||
// C'est l'unique consommateur de `NearestRoomIdx` avec les dômes, le pincement et le biais de
|
||||
// sol, et c'est ce qui rend `FRoomGraphSource::GetNearestRoomIdx()` nécessaire.
|
||||
//
|
||||
// ⚠️ SA PORTE N'EST PAS LE GATE COMMUN : `CaveSDF < SDFBlendRadius` (dans la grotte ou tout
|
||||
// près), pas `< SDFBlendRadius·3`. Une arche se pose dans le VIDE de la salle, pas dans sa paroi.
|
||||
class FCaveArchMod final : public IVoxelDensityOp
|
||||
{
|
||||
public:
|
||||
FCaveArchMod(const FStrateGenerationParams& InP, const FRoomGraphSource* InRooms)
|
||||
: P(InP), Rooms(InRooms) {}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
{
|
||||
const float CaveSDF = InOut.Sdf;
|
||||
if (!VF_NearCaveSurface(CaveSDF, P.SDFBlendRadius)) { return; }
|
||||
if (Rooms == nullptr) { return; }
|
||||
|
||||
const int32 NearestRoomIdx = Rooms->GetNearestRoomIdx();
|
||||
if (!(P.ArchDensity > 0.0f && CaveSDF < P.SDFBlendRadius && CaveSDF < FLT_MAX
|
||||
&& NearestRoomIdx >= 0))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// ⚠️ `IsValidIndex` AJOUTÉ : l'original indexe directement après le seul test `>= 0`.
|
||||
// L'index vient d'`EvaluateSDFCached` donc il est valide par construction — ce garde-fou
|
||||
// ne peut donc JAMAIS changer la sortie, seulement empêcher un crash si l'invariant se
|
||||
// cassait un jour. Même famille de décision que l'empreinte de params dans la clé de
|
||||
// cache : on se trompe du côté du coût, jamais du côté du résultat.
|
||||
const FChunkSDFCache& Cache = Rooms->GetCache();
|
||||
if (!Cache.Rooms.IsValidIndex(NearestRoomIdx)) { return; }
|
||||
const FCachedRoom& Room = Cache.Rooms[NearestRoomIdx];
|
||||
|
||||
const int32 MaxArches = 3;
|
||||
const FVector VoxPos(WorldX, WorldY, WorldZ);
|
||||
|
||||
for (int32 i = 0; i < MaxArches; i++)
|
||||
{
|
||||
const uint32 AH = VoxelHash::Mix(Room.Hash ^ (0xA4C400u + (uint32)i * 7369u));
|
||||
|
||||
if (VoxelHash::ToFloat01(AH) > P.ArchDensity) { continue; }
|
||||
|
||||
const uint32 AH2 = VoxelHash::Mix(AH ^ 0xA4C4u);
|
||||
const float ArcCX = Room.Center.X + VoxelHash::ToFloatSigned(AH2) * Room.RadiusXY * 0.3f;
|
||||
const float ArcCY = Room.Center.Y
|
||||
+ VoxelHash::ToFloatSigned(VoxelHash::Mix(AH2)) * Room.RadiusXY * 0.3f;
|
||||
|
||||
const uint32 AH3 = VoxelHash::Mix(AH2 ^ 0xB41Du);
|
||||
const float ArcCZ = Room.Center.Z + VoxelHash::ToFloatSigned(AH3) * Room.RadiusZ * 0.4f;
|
||||
|
||||
const uint32 AH4 = VoxelHash::Mix(AH3 ^ 0xCAFEu);
|
||||
const float Angle = VoxelHash::ToFloat01(AH4) * PI;
|
||||
const float HalfSpan = Room.RadiusXY
|
||||
* (0.5f + VoxelHash::ToFloat01(VoxelHash::Mix(AH4)) * 0.35f);
|
||||
|
||||
const float CosA = FMath::Cos(Angle);
|
||||
const float SinA = FMath::Sin(Angle);
|
||||
const FVector ArchA(ArcCX - CosA * HalfSpan, ArcCY - SinA * HalfSpan, ArcCZ);
|
||||
const FVector ArchB(ArcCX + CosA * HalfSpan, ArcCY + SinA * HalfSpan, ArcCZ);
|
||||
|
||||
const uint32 AH5 = VoxelHash::Mix(AH4 ^ 0xF00Du);
|
||||
const float ArchRadius = FMath::Lerp(P.ArchMinRadius, P.ArchMaxRadius,
|
||||
VoxelHash::ToFloat01(AH5));
|
||||
|
||||
const float ArchSDF = VoxelSDF::Capsule(VoxPos, ArchA, ArchB, ArchRadius);
|
||||
|
||||
const float ArchBlend = 2.0f;
|
||||
if (ArchSDF < ArchBlend)
|
||||
{
|
||||
float Fill = FMath::Clamp((ArchBlend - ArchSDF) / (ArchBlend * 2.0f), 0.0f, 1.0f);
|
||||
Fill = SmoothStep01(Fill);
|
||||
InOut.Density += Fill * P.BaseDensity * 1.5f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** N'AJOUTE que du solide ⇒ `FillOnly`. Une vraie borne spatiale existe (les arches vivent
|
||||
* dans le rayon d'une salle) mais elle demande le cache pour la boîte interrogée — même
|
||||
* dette que `FRoomGraphSource::EffectOverBox`, et elle se paiera au même moment. */
|
||||
EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override
|
||||
{
|
||||
return (P.ArchDensity > 0.0f) ? EVoxelOpEffect::FillOnly : EVoxelOpEffect::Identity;
|
||||
}
|
||||
|
||||
private:
|
||||
FStrateGenerationParams P;
|
||||
const FRoomGraphSource* Rooms; // NON possédant
|
||||
};
|
||||
|
||||
//=========================================================================
|
||||
// RÔLE 1 — SOURCE : VERS / WORM TUNNELS (TunnelNetwork)
|
||||
//=========================================================================
|
||||
@@ -2704,12 +2983,12 @@ namespace VoxelDensityOps
|
||||
void BuildTunnelNetworkStack(FVoxelOpStack& OutStack, const FStrateGenerationParams& P,
|
||||
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager)
|
||||
{
|
||||
// ⚠️ ÉTAPES A + B1 + B2 — LA PILE EST ENCORE INCOMPLÈTE, ET DÉLIBÉRÉMENT.
|
||||
// ⚠️ ÉTAPES A + B1 + B2 + B3 — LA PILE EST ENCORE INCOMPLÈTE, ET DÉLIBÉRÉMENT.
|
||||
// Sont portés : l'échelle verticale, le roc de base, le warp, le graphe de salles (+ pits
|
||||
// + cheminées), le carve, **la rugosité (4b), les terrasses, les lignes de strates, les
|
||||
// nervures**, les vers, le post structurel. **NE SONT PAS ENCORE PORTÉS** les huit
|
||||
// modificateurs restants (surplombs, falaise, festons, arches, colonnes, dômes, pincement,
|
||||
// biais de sol), ni l'override d'op PAR SALLE.
|
||||
// nervures, les surplombs, la falaise, les festons, les arches**, les vers, le post
|
||||
// structurel. **NE SONT PAS ENCORE PORTÉS** les quatre modificateurs restants (colonnes,
|
||||
// dômes, pincement, biais de sol), ni l'override d'op PAR SALLE.
|
||||
//
|
||||
// C'est pour cela que `UsesOperatorStackForChunk` rend encore **false** pour TunnelNetwork :
|
||||
// brancher une pile incomplète sur le monde en retirerait tout le détail. Le test compare
|
||||
@@ -2753,8 +3032,11 @@ namespace VoxelDensityOps
|
||||
OutStack.Add(MakeUnique<FCaveTerraceMod>(P, Seed, RoomPtr)); // 4c — terrasses
|
||||
OutStack.Add(MakeUnique<FLayerLineMod>(P)); // 4c — lignes de strates
|
||||
OutStack.Add(MakeUnique<FRibbingMod>(P)); // 4c — nervures
|
||||
// [ÉTAPES B3–B4 iront ici : surplombs, falaise, festons, arches, colonnes, dômes,
|
||||
// pincement, biais de sol]
|
||||
OutStack.Add(MakeUnique<FCaveOverhangMod>(P, Seed)); // 4c — surplombs
|
||||
OutStack.Add(MakeUnique<FCaveCliffMod>(P, Seed)); // 4c — falaise
|
||||
OutStack.Add(MakeUnique<FScallopMod>(P, Seed)); // 4c — festons
|
||||
OutStack.Add(MakeUnique<FCaveArchMod>(P, RoomPtr)); // 4c — arches
|
||||
// [ÉTAPE B4 ira ici : colonnes, dômes, pincement, biais de sol]
|
||||
OutStack.Add(MakeUnique<FWormFieldSource>(P, Seed));
|
||||
|
||||
OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ,
|
||||
|
||||
@@ -266,11 +266,12 @@ namespace VoxelDensityOps
|
||||
const UVoxelStrateManager* StrateManager);
|
||||
|
||||
/**
|
||||
* TunnelNetwork — **ÉTAPES A + B1 + B2, PILE ENCORE INCOMPLÈTE** :
|
||||
* TunnelNetwork — **ÉTAPES A + B1 + B2 + B3, PILE ENCORE INCOMPLÈTE** :
|
||||
* ConstantRock → RoomGraph(warp + pits + cheminées) → SdfCarve → CaveRoughness(4b)
|
||||
* → Terrace → LayerLines → Ribbing → Worms → [structural ×3]
|
||||
* → Terrace → LayerLines → Ribbing → Overhang → Cliff → Scallop → Arch
|
||||
* → Worms → [structural ×3]
|
||||
*
|
||||
* ⛔ NE PAS brancher cet archétype dans `UsesOperatorStackForChunk` avant l'étape C : les huit
|
||||
* ⛔ NE PAS brancher cet archétype dans `UsesOperatorStackForChunk` avant l'étape C : les quatre
|
||||
* modificateurs de détail restants (4c–4h) et l'override d'op par salle ne sont pas portés, donc
|
||||
* le monde y perdrait du détail. Le test compare avec ces amplitudes à zéro.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user