feat(opstack B4): port Columns (4d), Domes (4g), Pinch (4h), FloorBias + Column op in the test pool
STAGE B group 4 of 5 -- and with it, ALL TWELVE detail modifiers are ported. 19 ops:
ConstantRock -> RoomGraph -> SdfCarve -> Roughness -> Terrace -> LayerLines -> Ribbing
-> Overhang -> Cliff -> Scallop -> Arch -> RoomColumn -> Dome -> Pinch -> FloorBias
-> Worms -> [structural x3]
Still NOT wired: UsesOperatorStackForChunk returns false for TunnelNetwork. What is missing is C1,
the per-room op override -- which adds NO operator, it changes what eleven of them READ.
TRANSCRIBED
- FRoomColumnMod (4d): walks SDFCache.Columns. Not FGridColumnMod -- that one rolls cylinders on a
world grid for FlatPlain/CrystalChamber; this one iterates a per-room bake.
- FDomeMod (4g): upward half-ellipsoid, up to 2 per room, capped at 0.85 * room radius.
- FPinchMod (4h): perimeter-anchored ellipsoid with the SideFactor term that leaves the passage
axis clear.
- FFloorBiasMod: quadratic fill below room centre, only in definite air. LAST in the chain and not
interchangeable -- it exists to undo what roughness (4b) did to floors. That is why the operator
order in BuildTunnelNetworkStack is the original's order line for line.
THE TRAP IN THIS GROUP, WHICH IS THE PIT LESSON A THIRD TIME
Columns have NO strate parameter. The per-voxel loop has no `if (ColumnDensity > 0)`, and the bake
reads OpParams (a FRESH struct with only the room's op applied), so FStrateGenerationParams::
ColumnDensity is read by nothing. Consequences, all three of which shaped this commit:
1. The `ColumnDensity = 0` line in the old DisableStageBModifiers was inert. It was never what
kept columns off; the absence of a Column op in the pool was.
2. No FeatureProbes entry can cover columns -- there is no param to switch off. And a second
stack built on a pool without the Column op would SHARE the thread_local SDF cache (the pool
is not in its key) and return identical densities, i.e. it would lie. Exactly the trap that
was documented for pits.
3. So the stage-A guard that ERRORED on TotalColumns > 0 is inverted in this same commit: it now
REQUIRES TotalColumns > 0. Asking the bake what it baked is the only sound check here.
DisableStageBModifiers is deleted rather than left empty: its emptiness is the measure of stage B
being complete, and an empty function still being called is how a step gets forgotten.
TEST (same commit): op count 15 -> 19; MakeShaftOpPool renamed MakeRoomOpPool and given a third
entry (Column, Probability 1.0 on all three so every room draws one of the three evenly); three
more param probes (dome/pinch/floor bias); the column guard inverted.
⚠️ THE POOL MUST STAY Pit/Chimney/Column UNTIL C1. Eleven of the twelve modifiers read the per-room
param copy in the original and the strate params here; those agree only while no room carries a
DETAIL-type op, and ApplyTo(Pit/Chimney/Column) writes only pit/chimney/column fields. A Terrace
entry would break the equivalence -- which is precisely the test C1 adds, and the only possible
proof that the override works.
IF THIS GROUP IS WRONG: dome/pinch/arch diffs land inside open cave near room centres and
perimeters; a column error shows up as a ring of diffs at fixed XY through the whole room height.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
// VoxelForgeOpStackTunnelTest.cpp
|
||||
// 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).
|
||||
// TunnelNetwork — ÉTAPE A (squelette SDF) + ÉTAPE B COMPLÈTE (les douze modificateurs, 4b–4h).
|
||||
// TunnelNetwork — STAGE A (the SDF spine) + ALL OF STAGE B (the twelve detail modifiers, 4b-4h).
|
||||
//
|
||||
// POURQUOI UN TEST D'UNE PILE INCOMPLÈTE
|
||||
// `GetDensityWithParams` fait ~1080 lignes et treize modificateurs de détail. Tout porter avant de
|
||||
// `GetDensityWithParams` fait ~1080 lignes et douze modificateurs de détail (le chiffre « treize »
|
||||
// traînait dans les notes ; il y en a douze, et onze seulement lisent la copie de params par salle).
|
||||
// Tout porter avant de
|
||||
// pouvoir rien vérifier, ce serait écrire ~600 lignes non compilées par-dessus ~200 non vérifiées —
|
||||
// exactement le motif que `AUDIT §P3` documente et que ce refactor a évité six fois de suite.
|
||||
//
|
||||
@@ -15,17 +17,22 @@
|
||||
// Même discipline que la passe « défauts puis tous les ops ON » du test de pile de hauteur, prise
|
||||
// dans l'autre sens.
|
||||
//
|
||||
// L'ÉTAPE B REMONTE CES AMPLITUDES UN GROUPE À LA FOIS, dans l'autre sens : chaque groupe porté sort
|
||||
// de `DisableStageBModifiers` et entre dans `EnableTunnelFeatures`, avec (i) une sonde de couverture
|
||||
// L'ÉTAPE B A REMONTÉ CES AMPLITUDES UN GROUPE À LA FOIS, dans l'autre sens : chaque groupe porté sortait
|
||||
// de la liste des amplitudes éteintes pour entrer 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 : terrasses, lignes de strates, nervures — STEP 4c.
|
||||
// • B3 (ce commit) : surplombs, falaise, festons, arches — STEP 4c.
|
||||
// • B3 : surplombs, falaise, festons, arches — STEP 4c.
|
||||
// • B4 (ce commit) : colonnes (4d), dômes (4g), pincement (4h), biais de sol. La liste des
|
||||
// amplitudes éteintes est vide : **l'étape B est complète**.
|
||||
//
|
||||
// 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.
|
||||
// CE QUE CE TEST NE PROUVE PAS (et le dit) : rien sur l'override d'op PAR SALLE — onze des douze
|
||||
// modificateurs lisent, dans l'original, une copie des params où l'op de la salle la plus proche a
|
||||
// été appliqué, et la pile lit les params de la strate. Les deux coïncident **uniquement** parce que
|
||||
// le pool du test ne contient que Pit / Chimney / Column, dont les `ApplyTo` n'écrivent aucun champ
|
||||
// de détail. Un op `Terrace` dans ce pool casserait l'équivalence : c'est exactement le test que C1
|
||||
// ajoutera, et c'est la seule preuve possible de l'override. Rien non plus sur le saut de tuile —
|
||||
// `FRoomGraphSource::EffectOverBox` rend `Both`.
|
||||
//
|
||||
// ⚠️ É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`
|
||||
@@ -57,23 +64,21 @@ namespace
|
||||
constexpr int32 PointsPerChunk = 250;
|
||||
constexpr int32 NumTunnelSamples = NumTunnelChunks * PointsPerChunk;
|
||||
|
||||
/**
|
||||
* Met à zéro tout ce qui n'est PAS ENCORE porté, pour que l'original prenne le même chemin.
|
||||
* Ces champs-là sont déjà à zéro par défaut ; on les écrit quand même, parce qu'un test qui
|
||||
* dépend d'un défaut se casse le jour où le défaut change, et silencieusement.
|
||||
*
|
||||
* ⚠️ CETTE LISTE RÉTRÉCIT À CHAQUE GROUPE DE L'ÉTAPE B. Une ligne qui part d'ici doit arriver
|
||||
* dans `EnableTunnelFeatures` ET dans `FeatureProbes` : la déplacer sans la sonder rendrait le
|
||||
* groupe « activé » sans aucune preuve qu'il s'exécute. `SurfaceRoughness` (B1) est le premier
|
||||
* à avoir fait le trajet — c'était le seul non nul par défaut (5.0).
|
||||
*/
|
||||
void DisableStageBModifiers(FStrateGenerationParams& P)
|
||||
{
|
||||
P.ColumnDensity = 0.0f; // ⚠️ celui-ci se cuit dans SDFCache.Columns, pas un `if`
|
||||
P.DomeDensity = 0.0f;
|
||||
P.PinchDensity = 0.0f;
|
||||
P.FloorBias = 0.0f;
|
||||
}
|
||||
//=========================================================================
|
||||
// ⚠️ `DisableStageBModifiers` A DISPARU, ET SA DISPARITION EST LE RÉSULTAT DE L'ÉTAPE B
|
||||
//=========================================================================
|
||||
// Cette fonction mettait à zéro tout ce que le portage n'avait pas encore atteint, pour que
|
||||
// l'original emprunte le même chemin que la pile. À chaque groupe (B1…B4) une ligne en sortait
|
||||
// pour entrer dans `EnableTunnelFeatures` ET dans `FeatureProbes` ; à B4 il n'en restait plus.
|
||||
// Sa liste vide EST la mesure de l'avancement — d'où le fait de le dire ici plutôt que de
|
||||
// laisser une fonction vide qu'on continuerait d'appeler sans y penser.
|
||||
//
|
||||
// Une ligne y était de toute façon inopérante et mérite d'être notée : `ColumnDensity = 0`
|
||||
// n'éteignait rien. La cuisson des colonnes lit `OpParams` (un struct NEUF où seul l'op de la
|
||||
// salle a été appliqué), jamais le champ de la strate — même mécanisme que pour les pits. Ce qui
|
||||
// tenait les colonnes éteintes à l'étape A, c'était l'ABSENCE d'op `Column` dans le pool, et le
|
||||
// garde-fou qui ERREUR-ait si la cuisson en produisait quand même. B4 ajoute l'op au pool et
|
||||
// retourne ce garde-fou : il EXIGE maintenant des colonnes.
|
||||
|
||||
/**
|
||||
* ⚠️ DENSIFIÉ après le premier run vert. Aux défauts (`RoomSpacing = 80`, `RoomDensity = 0.35`)
|
||||
@@ -93,7 +98,7 @@ namespace
|
||||
*
|
||||
* NOT here: PitDensity / ChimneyDensity. Setting them was a mistake that check 3b caught — the
|
||||
* bake reads a FRESH param struct with only the room's op applied, so those fields are never
|
||||
* read. Pits exist only through a per-room terrain-op asset. See MakeShaftOpPool below.
|
||||
* read. Pits exist only through a per-room terrain-op asset. See MakeRoomOpPool below.
|
||||
*/
|
||||
void EnableTunnelFeatures(FStrateGenerationParams& P)
|
||||
{
|
||||
@@ -137,27 +142,47 @@ namespace
|
||||
P.ArchDensity = 0.9f;
|
||||
P.ArchMinRadius = 3.0f;
|
||||
P.ArchMaxRadius = 6.0f;
|
||||
|
||||
// ── ÉTAPE B4 : dômes · pincement · biais de sol (4g, 4h) ─────────────────────────────
|
||||
// ⚠️ LES COLONNES (4d) NE SONT PAS ICI, et ce n'est pas un oubli : elles n'ont AUCUN
|
||||
// paramètre de strate. Ni la cuisson ni la boucle par voxel ne lisent `ColumnDensity` ;
|
||||
// elles n'existent que par un op `Column` dans le pool (voir `MakeRoomOpPool`) et leur
|
||||
// couverture se prouve au contrôle 3b, pas par une sonde de params.
|
||||
P.DomeDensity = 0.9f; // 2 tirages par salle ⇒ il en faut beaucoup pour couvrir
|
||||
P.DomeMinRadius = 8.0f;
|
||||
P.DomeMaxRadius = 15.0f;
|
||||
P.DomeHeightRatio = 0.8f;
|
||||
P.PinchDensity = 0.9f; // idem, 3 tirages par salle
|
||||
P.PinchStrength = 5.0f;
|
||||
P.PinchLength = 12.0f;
|
||||
P.FloorBias = 0.8f; // ne s'applique que dans l'air certain (CaveSDF < 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Le SEUL moyen d'obtenir des pits et des cheminées : donner à la strate un pool d'ops de
|
||||
* terrain, que `BuildChunkCache` tire par salle. Deux entrées à `Probability = 0.5` ⇒ le pool
|
||||
* est entièrement réclamé, donc **chaque salle reçoit un op** (moitié pits, moitié cheminées).
|
||||
* Le SEUL moyen d'obtenir des pits, des cheminées et des colonnes : donner à la strate un pool
|
||||
* d'ops de terrain, que `BuildChunkCache` tire par salle. Trois entrées à `Probability = 1.0` ⇒
|
||||
* `TotalOpProb = 3`, `NormFactor = 1/3`, curseurs à 1/3, 2/3, 1 : **chaque salle reçoit un op**,
|
||||
* un tiers de chaque sorte.
|
||||
*
|
||||
* ⚠️ SÛR POUR L'ÉTAPE A, et ce n'est pas une évidence : l'override par salle du chemin d'origine
|
||||
* applique l'op de la salle sur une COPIE des params, laquelle pilote les 13 modificateurs de
|
||||
* détail — ceux que l'étape A n'a pas portés. Mais `ApplyTo` n'écrit, pour `Pit`, que les quatre
|
||||
* champs de pit (idem `Chimney`). Aucun champ de détail n'est touché, donc aucun modificateur ne
|
||||
* s'allume et l'équivalence de l'étape A tient. Un op `Terrace` ici la casserait — c'est
|
||||
* exactement ce que l'étape B ajoutera, exprès.
|
||||
* ⚠️⚠️ CE POOL DOIT RESTER Pit / Chimney / Column JUSQU'À L'ÉTAPE C1, ET LA RAISON EST SUBTILE.
|
||||
* L'override par salle du chemin d'origine applique l'op de la salle sur une COPIE des params,
|
||||
* et ONZE des douze modificateurs de détail lisent cette copie (la rugosité 4b, non : le shadow
|
||||
* est déclaré après elle). L'étape B les a portés contre les params de la STRATE. Les deux ne
|
||||
* coïncident que tant qu'aucune salle ne porte un op de type DÉTAIL — or `ApplyTo(Pit)` n'écrit
|
||||
* que les quatre champs de pit, `ApplyTo(Chimney)` que les quatre de cheminée, `ApplyTo(Column)`
|
||||
* que les trois de colonne. Aucun champ de détail touché ⇒ aucun modificateur ne diverge.
|
||||
*
|
||||
* Safe at stage A because ApplyTo(Pit) writes only the four pit fields: no detail modifier wakes
|
||||
* up. A Terrace op here WOULD break stage A — which is precisely what stage B will add.
|
||||
* **Mettre un op `Terrace` ici casserait l'équivalence — et c'est exactement le test que C1
|
||||
* ajoutera**, parce que c'est la seule preuve possible de l'override par salle.
|
||||
*
|
||||
* The pool must stay Pit/Chimney/Column until C1: eleven of the twelve modifiers read the
|
||||
* per-room param copy in the original and the strate params here, and those agree only while no
|
||||
* room carries a detail-type op. A Terrace entry is what C1 adds, deliberately.
|
||||
*
|
||||
* @param OutKeepAlive les assets transitoires, à garder vivants pour la durée du test.
|
||||
*/
|
||||
void MakeShaftOpPool(UVoxelStrateDefinition* Def,
|
||||
TArray<TStrongObjectPtr<UVoxelTerrainOpDefinition>>& OutKeepAlive)
|
||||
void MakeRoomOpPool(UVoxelStrateDefinition* Def,
|
||||
TArray<TStrongObjectPtr<UVoxelTerrainOpDefinition>>& OutKeepAlive)
|
||||
{
|
||||
UVoxelTerrainOpDefinition* PitOp = NewObject<UVoxelTerrainOpDefinition>(
|
||||
GetTransientPackage(), NAME_None, RF_Transient);
|
||||
@@ -177,19 +202,38 @@ namespace
|
||||
ChimOp->ChimneyHeight = 18.0f;
|
||||
OutKeepAlive.Add(TStrongObjectPtr<UVoxelTerrainOpDefinition>(ChimOp));
|
||||
|
||||
// ⚠️ AJOUTÉ À L'ÉTAPE B4 : sans cette entrée, `SDFCache.Columns` reste VIDE et l'opérateur
|
||||
// de colonnes ne s'exécute sur rien — ce que le contrôle 3b exigeait à l'étape A et qu'il
|
||||
// exige désormais dans l'autre sens.
|
||||
UVoxelTerrainOpDefinition* ColOp = NewObject<UVoxelTerrainOpDefinition>(
|
||||
GetTransientPackage(), NAME_None, RF_Transient);
|
||||
ColOp->Type = EVoxelTerrainOpType::Column;
|
||||
ColOp->ColumnDensity = 0.9f;
|
||||
ColOp->ColumnMinRadius = 2.0f;
|
||||
ColOp->ColumnMaxRadius = 5.0f;
|
||||
OutKeepAlive.Add(TStrongObjectPtr<UVoxelTerrainOpDefinition>(ColOp));
|
||||
|
||||
// `Probability = 1.0` sur les trois : la somme dépasse 1, donc la sélection est normalisée
|
||||
// et chaque salle tire exactement un op parmi les trois, à parts égales.
|
||||
FStrateTerrainOpEntry PitEntry;
|
||||
PitEntry.Operation = TSoftObjectPtr<UVoxelTerrainOpDefinition>(PitOp);
|
||||
PitEntry.Weight = 1.0f;
|
||||
PitEntry.Probability = 0.5f;
|
||||
PitEntry.Probability = 1.0f;
|
||||
|
||||
FStrateTerrainOpEntry ChimEntry;
|
||||
ChimEntry.Operation = TSoftObjectPtr<UVoxelTerrainOpDefinition>(ChimOp);
|
||||
ChimEntry.Weight = 1.0f;
|
||||
ChimEntry.Probability = 0.5f;
|
||||
ChimEntry.Probability = 1.0f;
|
||||
|
||||
FStrateTerrainOpEntry ColEntry;
|
||||
ColEntry.Operation = TSoftObjectPtr<UVoxelTerrainOpDefinition>(ColOp);
|
||||
ColEntry.Weight = 1.0f;
|
||||
ColEntry.Probability = 1.0f;
|
||||
|
||||
Def->TerrainOperations.Reset();
|
||||
Def->TerrainOperations.Add(PitEntry);
|
||||
Def->TerrainOperations.Add(ChimEntry);
|
||||
Def->TerrainOperations.Add(ColEntry);
|
||||
}
|
||||
|
||||
/** Fraction minimale d'échantillons devant tomber en grotte ouverte pour que l'équivalence
|
||||
@@ -238,6 +282,15 @@ namespace
|
||||
[](FStrateGenerationParams& Q) { Q.ScallopStrength = 0.0f; } },
|
||||
{ TEXT("B3 arches (STEP 4c)"),
|
||||
[](FStrateGenerationParams& Q) { Q.ArchDensity = 0.0f; } },
|
||||
{ TEXT("B4 domes (STEP 4g)"),
|
||||
[](FStrateGenerationParams& Q) { Q.DomeDensity = 0.0f; } },
|
||||
{ TEXT("B4 pinch (STEP 4h)"),
|
||||
[](FStrateGenerationParams& Q) { Q.PinchDensity = 0.0f; } },
|
||||
{ TEXT("B4 floor bias"),
|
||||
[](FStrateGenerationParams& Q) { Q.FloorBias = 0.0f; } },
|
||||
// ⚠️ PAS DE SONDE POUR LES COLONNES (STEP 4d), et ce n'est pas un oubli : elles n'ont aucun
|
||||
// paramètre de strate qu'on puisse éteindre. Leur couverture se prouve au contrôle 3b, en
|
||||
// demandant à la cuisson combien elle en a produit. Voir la note là-bas.
|
||||
// ⚠️ 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.
|
||||
@@ -295,7 +348,7 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
|
||||
if (World.Definitions.IsValidIndex(FTestWorld::SlotTunnelNetwork)
|
||||
&& World.Definitions[FTestWorld::SlotTunnelNetwork].IsValid())
|
||||
{
|
||||
MakeShaftOpPool(World.Definitions[FTestWorld::SlotTunnelNetwork].Get(), OpAssets);
|
||||
MakeRoomOpPool(World.Definitions[FTestWorld::SlotTunnelNetwork].Get(), OpAssets);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -322,18 +375,17 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
|
||||
return false;
|
||||
}
|
||||
|
||||
DisableStageBModifiers(P);
|
||||
EnableTunnelFeatures(P);
|
||||
|
||||
FVoxelOpStack Stack;
|
||||
VoxelDensityOps::BuildTunnelNetworkStack(Stack, P, World.Settings->Seed,
|
||||
Gen->OriginSpineRadius, World.StrateManager.Get());
|
||||
|
||||
// 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
|
||||
// rock + roomgraph + carve + **les 12 modificateurs de détail** + worms + 3 structurels = 19.
|
||||
// L'étape C n'ajoute AUCUN opérateur — elle change ce que onze d'entre eux LISENT — 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..B3 tunnel stack is decomposed into 15 ops"), Stack.Num(), 15);
|
||||
TestEqual(TEXT("the stage-A+B tunnel stack is decomposed into 19 ops"), Stack.Num(), 19);
|
||||
|
||||
FVoxelOpContext Ctx;
|
||||
Ctx.Seed = (uint32)World.Settings->Seed;
|
||||
@@ -413,7 +465,7 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
|
||||
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."),
|
||||
TEXT("the per-room op override (stage C1), and any tile verdict."),
|
||||
NumTunnelSamples, NumTunnelChunks, NumInCave, NumInRock));
|
||||
|
||||
AddInfo(FString::Printf(
|
||||
@@ -745,15 +797,17 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
|
||||
TestTrue(TEXT("the bake produced chimneys, so the chimney loop has data to run on"),
|
||||
TotalChimneys > 0);
|
||||
|
||||
if (TotalColumns > 0)
|
||||
{
|
||||
AddError(FString::Printf(
|
||||
TEXT("The bake produced %d columns, but stage A has NOT ported the column loop ")
|
||||
TEXT("(STEP 4d). The equivalence above should have failed; if it did not, the ")
|
||||
TEXT("sample points simply missed every column. Remove the column op from the pool ")
|
||||
TEXT("until stage B."),
|
||||
TotalColumns));
|
||||
}
|
||||
// ⚠️ CE GARDE-FOU S'EST RETOURNÉ À L'ÉTAPE B4, ET C'EST LE SEUL MOYEN DE COUVRIR LES
|
||||
// COLONNES. Il ERREUR-ait si la cuisson produisait des colonnes (l'étape A n'avait pas porté
|
||||
// `STEP 4d`) ; il EXIGE maintenant qu'elle en produise. Il n'y a pas d'alternative par
|
||||
// paramètre : ni la cuisson ni la boucle par voxel ne lisent `FStrateGenerationParams::
|
||||
// ColumnDensity`, donc aucune sonde de `FeatureProbes` ne peut éteindre les colonnes — et
|
||||
// une pile bâtie sur un pool SANS op `Column` partagerait le cache `thread_local` (le pool
|
||||
// n'est pas dans sa clé) et rendrait exactement la même densité, c'est-à-dire mentirait.
|
||||
// Reste à demander à la cuisson ce qu'elle a cuit. Troisième application de la même leçon.
|
||||
TestTrue(TEXT("the bake produced columns, so the STEP 4d loop has data to run on ")
|
||||
TEXT("(no params probe can cover this one -- see the comment above)"),
|
||||
TotalColumns > 0);
|
||||
}
|
||||
|
||||
//=========================================================================
|
||||
|
||||
@@ -2735,6 +2735,307 @@ namespace
|
||||
const FRoomGraphSource* Rooms; // NON possédant
|
||||
};
|
||||
|
||||
//=========================================================================
|
||||
// RÔLE 3 — MODIFIER : COLONNES DE SALLE / ROOM COLUMNS (TunnelNetwork, STEP 4d)
|
||||
//=========================================================================
|
||||
// ⚠️ CE N'EST PAS `FGridColumnMod` (FlatPlain / CrystalChamber). Celui-là pose des cylindres sur
|
||||
// une GRILLE MONDE et se tire par cellule ; celui-ci PARCOURT une liste PRÉ-CUITE par
|
||||
// `BuildChunkCache`, salle par salle. `OPSTACK-DECOMPOSITION §1` les sépare explicitement.
|
||||
//
|
||||
// ⚠️⚠️ IL N'A **AUCUN** PARAMÈTRE DE STRATE, ET C'EST LE PIÈGE DE CE GROUPE.
|
||||
// Le code d'origine n'écrit aucun `if (Params.ColumnDensity > 0)` : il itère la liste cuite,
|
||||
// point. `FStrateGenerationParams::ColumnDensity` n'est JAMAIS lu par la cuisson non plus (elle
|
||||
// lit `OpParams`, un struct NEUF où seul l'op de la salle a été appliqué). Donc :
|
||||
// • mettre `ColumnDensity = 0` dans les params N'ÉTEINT PAS les colonnes ;
|
||||
// • la seule façon d'avoir des colonnes est un `UVoxelTerrainOpDefinition` de type `Column`
|
||||
// dans le pool de la strate ;
|
||||
// • et la seule façon de PROUVER qu'elles ont tiré est de regarder `SDFCache.Columns.Num()`.
|
||||
// C'est exactement la leçon des pits, une troisième fois. Le test l'applique au contrôle 3b.
|
||||
class FRoomColumnMod final : public IVoxelDensityOp
|
||||
{
|
||||
public:
|
||||
FRoomColumnMod(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, FVoxelOpSample& InOut) const override
|
||||
{
|
||||
if (!VF_NearCaveSurface(InOut.Sdf, P.SDFBlendRadius)) { return; }
|
||||
if (Rooms == nullptr) { return; }
|
||||
|
||||
for (const FCachedColumn& Col : Rooms->GetCache().Columns)
|
||||
{
|
||||
const float DX = WorldX - Col.CenterX;
|
||||
const float DY = WorldY - Col.CenterY;
|
||||
const float XYDistSq = DX * DX + DY * DY;
|
||||
if (XYDistSq > Col.BoundXYRadiusSq) { continue; }
|
||||
|
||||
const float CylSDF = FMath::Sqrt(XYDistSq) - Col.Radius;
|
||||
|
||||
const float ColBlend = 3.0f;
|
||||
if (CylSDF < ColBlend)
|
||||
{
|
||||
float Fill = FMath::Clamp((ColBlend - CylSDF) / (ColBlend * 2.0f), 0.0f, 1.0f);
|
||||
Fill = SmoothStep01(Fill);
|
||||
InOut.Density += Fill * Col.BaseDensity * 1.5f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** N'AJOUTE que du solide ⇒ `FillOnly`. On ne peut pas rendre `Identity` sans consulter le
|
||||
* cache pour la boîte interrogée — même dette que le graphe de salles. */
|
||||
EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override
|
||||
{
|
||||
return EVoxelOpEffect::FillOnly;
|
||||
}
|
||||
|
||||
private:
|
||||
FStrateGenerationParams P;
|
||||
const FRoomGraphSource* Rooms; // NON possédant
|
||||
};
|
||||
|
||||
//=========================================================================
|
||||
// RÔLE 3 — MODIFIER : DÔMES / CATHEDRAL CEILINGS (TunnelNetwork, STEP 4g)
|
||||
//=========================================================================
|
||||
// Un demi-ellipsoïde creusé VERS LE HAUT depuis un point ancré au-dessus du centre de la salle.
|
||||
// Relatif à la salle comme les arches, même porte `CaveSDF < SDFBlendRadius`.
|
||||
class FDomeMod final : public IVoxelDensityOp
|
||||
{
|
||||
public:
|
||||
FDomeMod(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.DomeDensity > 0.0f && CaveSDF < P.SDFBlendRadius && CaveSDF < FLT_MAX
|
||||
&& NearestRoomIdx >= 0))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const FChunkSDFCache& Cache = Rooms->GetCache();
|
||||
if (!Cache.Rooms.IsValidIndex(NearestRoomIdx)) { return; } // cf. FCaveArchMod
|
||||
const FCachedRoom& Room = Cache.Rooms[NearestRoomIdx];
|
||||
|
||||
const int32 MaxDomes = 2;
|
||||
|
||||
for (int32 i = 0; i < MaxDomes; i++)
|
||||
{
|
||||
const uint32 DH = VoxelHash::Mix(Room.Hash ^ (0xD0AE0u + (uint32)i * 8191u));
|
||||
|
||||
if (VoxelHash::ToFloat01(DH) > P.DomeDensity) { continue; }
|
||||
|
||||
const uint32 DH2 = VoxelHash::Mix(DH ^ 0xD0A0u);
|
||||
const float DmX = Room.Center.X + VoxelHash::ToFloatSigned(DH2) * Room.RadiusXY * 0.4f;
|
||||
const float DmY = Room.Center.Y
|
||||
+ VoxelHash::ToFloatSigned(VoxelHash::Mix(DH2)) * Room.RadiusXY * 0.4f;
|
||||
|
||||
const uint32 DH3 = VoxelHash::Mix(DH2 ^ 0x90DEu);
|
||||
const float DmRadius = FMath::Min(
|
||||
FMath::Lerp(P.DomeMinRadius, P.DomeMaxRadius, VoxelHash::ToFloat01(DH3)),
|
||||
Room.RadiusXY * 0.85f
|
||||
);
|
||||
|
||||
const uint32 DH4 = VoxelHash::Mix(DH3 ^ 0xCAFEu);
|
||||
const float DmCenterZ = Room.Center.Z + Room.RadiusZ * 0.2f
|
||||
+ VoxelHash::ToFloat01(DH4) * Room.RadiusZ * 0.3f;
|
||||
|
||||
const float DmHeight = DmRadius * P.DomeHeightRatio;
|
||||
|
||||
if (WorldZ > DmCenterZ + DmHeight + 3.0f || WorldZ < DmCenterZ - 3.0f) { continue; }
|
||||
|
||||
const float DXDm = WorldX - DmX;
|
||||
const float DYDm = WorldY - DmY;
|
||||
const float DZDm = WorldZ - DmCenterZ;
|
||||
|
||||
if (DZDm < 0.0f) { continue; } // ne creuse que vers le haut
|
||||
|
||||
const float NormX = DXDm / DmRadius;
|
||||
const float NormY = DYDm / DmRadius;
|
||||
const float NormZ = DZDm / DmHeight;
|
||||
const float EllipDist = FMath::Sqrt(NormX * NormX + NormY * NormY + NormZ * NormZ) - 1.0f;
|
||||
const float DomeSDF = EllipDist * FMath::Min(DmRadius, DmHeight);
|
||||
|
||||
const float DmBlend = 3.0f;
|
||||
if (DomeSDF < DmBlend)
|
||||
{
|
||||
float Carve = FMath::Clamp((DmBlend - DomeSDF) / (DmBlend * 2.0f), 0.0f, 1.0f);
|
||||
Carve = SmoothStep01(Carve);
|
||||
InOut.Density -= Carve * P.BaseDensity * 1.5f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Ne SOUSTRAIT que ⇒ `CarveOnly`. */
|
||||
EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override
|
||||
{
|
||||
return (P.DomeDensity > 0.0f) ? EVoxelOpEffect::CarveOnly : EVoxelOpEffect::Identity;
|
||||
}
|
||||
|
||||
private:
|
||||
FStrateGenerationParams P;
|
||||
const FRoomGraphSource* Rooms; // NON possédant
|
||||
};
|
||||
|
||||
//=========================================================================
|
||||
// RÔLE 3 — MODIFIER : PINCEMENT / BOTTLENECK (TunnelNetwork, STEP 4h)
|
||||
//=========================================================================
|
||||
// Resserre un passage PAR LES CÔTÉS. Placé sur le PÉRIMÈTRE de la salle (offset 0.85 · rayon) —
|
||||
// là où les tunnels débouchent — et pas au centre, sinon il boucherait la salle elle-même.
|
||||
// `SideFactor` (distance à l'axe, clampée) est ce qui laisse l'axe du passage libre : le
|
||||
// remplissage est nul sur l'axe et maximal sur les bords de l'ellipsoïde.
|
||||
class FPinchMod final : public IVoxelDensityOp
|
||||
{
|
||||
public:
|
||||
FPinchMod(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.PinchDensity > 0.0f && CaveSDF < P.SDFBlendRadius && CaveSDF < FLT_MAX
|
||||
&& NearestRoomIdx >= 0))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const FChunkSDFCache& Cache = Rooms->GetCache();
|
||||
if (!Cache.Rooms.IsValidIndex(NearestRoomIdx)) { return; } // cf. FCaveArchMod
|
||||
const FCachedRoom& Room = Cache.Rooms[NearestRoomIdx];
|
||||
|
||||
const float Spread = 0.85f;
|
||||
const int32 MaxPinches = 3;
|
||||
|
||||
for (int32 i = 0; i < MaxPinches; i++)
|
||||
{
|
||||
const uint32 PnH = VoxelHash::Mix(Room.Hash ^ (0xF1C400u + (uint32)i * 5417u));
|
||||
|
||||
if (VoxelHash::ToFloat01(PnH) > P.PinchDensity) { continue; }
|
||||
|
||||
const uint32 PnH2 = VoxelHash::Mix(PnH ^ 0xF1C4u);
|
||||
const float PnX = Room.Center.X + VoxelHash::ToFloatSigned(PnH2) * Room.RadiusXY * Spread;
|
||||
const float PnY = Room.Center.Y
|
||||
+ VoxelHash::ToFloatSigned(VoxelHash::Mix(PnH2)) * Room.RadiusXY * Spread;
|
||||
|
||||
const uint32 PnH3 = VoxelHash::Mix(PnH2 ^ 0x5432u);
|
||||
const float PnZ = Room.Center.Z + VoxelHash::ToFloatSigned(PnH3) * Room.RadiusZ * 0.5f;
|
||||
|
||||
const uint32 PnH4 = VoxelHash::Mix(PnH3 ^ 0x9A3Bu);
|
||||
const float PnAngle = VoxelHash::ToFloat01(PnH4) * PI;
|
||||
const float CosPN = FMath::Cos(PnAngle);
|
||||
const float SinPN = FMath::Sin(PnAngle);
|
||||
|
||||
const float DXPn = WorldX - PnX;
|
||||
const float DYPn = WorldY - PnY;
|
||||
const float DZPn = WorldZ - PnZ;
|
||||
|
||||
const float MaxExtent = FMath::Max(P.PinchLength, P.PinchStrength) + 5.0f;
|
||||
if (FMath::Abs(DXPn) + FMath::Abs(DYPn) + FMath::Abs(DZPn) > MaxExtent) { continue; }
|
||||
|
||||
const float Along = DXPn * CosPN + DYPn * SinPN;
|
||||
const float Across = -DXPn * SinPN + DYPn * CosPN;
|
||||
|
||||
const float HalfLength = P.PinchLength * 0.5f;
|
||||
const float HalfNarrow = P.PinchStrength;
|
||||
const float HalfVertical = P.PinchStrength * 1.5f;
|
||||
|
||||
const float NAlong = Along / HalfLength;
|
||||
const float NAcross = Across / HalfNarrow;
|
||||
const float NUp = DZPn / HalfVertical;
|
||||
const float EllipDist = NAlong * NAlong + NAcross * NAcross + NUp * NUp;
|
||||
|
||||
if (EllipDist < 1.0f)
|
||||
{
|
||||
float Fill = 1.0f - EllipDist;
|
||||
Fill = SmoothStep01(Fill);
|
||||
const float AxisDist = FMath::Sqrt(NAcross * NAcross + NUp * NUp);
|
||||
const float SideFactor = FMath::Clamp(AxisDist * 2.0f, 0.0f, 1.0f);
|
||||
InOut.Density += Fill * SideFactor * P.BaseDensity * 1.5f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** N'AJOUTE que du solide ⇒ `FillOnly`. */
|
||||
EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override
|
||||
{
|
||||
return (P.PinchDensity > 0.0f) ? EVoxelOpEffect::FillOnly : EVoxelOpEffect::Identity;
|
||||
}
|
||||
|
||||
private:
|
||||
FStrateGenerationParams P;
|
||||
const FRoomGraphSource* Rooms; // NON possédant
|
||||
};
|
||||
|
||||
//=========================================================================
|
||||
// RÔLE 3 — MODIFIER : BIAIS DE SOL / FLOOR BIAS (TunnelNetwork, fin de 4h)
|
||||
//=========================================================================
|
||||
// Rend de la densité dans la moitié BASSE de la salle pour contrer le relief que la rugosité
|
||||
// laisse sur les sols — un sol praticable au lieu d'un sol bosselé. Ne s'applique QUE dans l'air
|
||||
// certain (`CaveSDF < 0`) : dans la paroi, le clamp anti-remplissage de la rugosité tient déjà.
|
||||
//
|
||||
// ⚠️ DERNIER DE LA CHAÎNE, ET CE N'EST PAS INTERCHANGEABLE : il corrige ce que la rugosité (4b)
|
||||
// a fait. Le déplacer avant elle le rendrait sans objet. C'est la raison pour laquelle l'ordre
|
||||
// des opérateurs dans `BuildTunnelNetworkStack` est celui de l'original, ligne pour ligne.
|
||||
class FFloorBiasMod final : public IVoxelDensityOp
|
||||
{
|
||||
public:
|
||||
FFloorBiasMod(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, float, 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.FloorBias > 0.0f && NearestRoomIdx >= 0 && CaveSDF < 0.0f)) { return; }
|
||||
|
||||
const FChunkSDFCache& Cache = Rooms->GetCache();
|
||||
if (!Cache.Rooms.IsValidIndex(NearestRoomIdx)) { return; } // cf. FCaveArchMod
|
||||
const FCachedRoom& NR = Cache.Rooms[NearestRoomIdx];
|
||||
|
||||
// NormZ : -1 = sol de la salle, 0 = centre, +1 = plafond.
|
||||
const float NormZ = (WorldZ - NR.Center.Z) / FMath::Max(NR.RadiusZ, 1.0f);
|
||||
|
||||
if (NormZ < 0.0f)
|
||||
{
|
||||
const float FloorFactor = NormZ * NormZ; // 0 au centre, 1 au sol
|
||||
InOut.Density += FloorFactor * P.FloorBias;
|
||||
}
|
||||
}
|
||||
|
||||
/** N'AJOUTE que du solide ⇒ `FillOnly`. */
|
||||
EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override
|
||||
{
|
||||
return (P.FloorBias > 0.0f) ? EVoxelOpEffect::FillOnly : EVoxelOpEffect::Identity;
|
||||
}
|
||||
|
||||
private:
|
||||
FStrateGenerationParams P;
|
||||
const FRoomGraphSource* Rooms; // NON possédant
|
||||
};
|
||||
|
||||
//=========================================================================
|
||||
// RÔLE 1 — SOURCE : VERS / WORM TUNNELS (TunnelNetwork)
|
||||
//=========================================================================
|
||||
@@ -2983,12 +3284,14 @@ namespace VoxelDensityOps
|
||||
void BuildTunnelNetworkStack(FVoxelOpStack& OutStack, const FStrateGenerationParams& P,
|
||||
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager)
|
||||
{
|
||||
// ⚠️ É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 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.
|
||||
// ⚠️ ÉTAPES A + B (B1 À B5) — LES DOUZE MODIFICATEURS DE DÉTAIL SONT PORTÉS.
|
||||
// **CE QUI RESTE, C'EST L'ÉTAPE C** : l'override d'op PAR SALLE. Tant qu'il manque, les
|
||||
// onze modificateurs qui, dans l'original, lisent la copie de params de la salle la plus
|
||||
// proche lisent ici les params de la STRATE. Les deux coïncident exactement tant qu'aucune
|
||||
// salle ne porte un op de type détail (Terrace, LayerLines, Ribbing, Cliff, Scallop,
|
||||
// Overhang, Arch, Dome, Pinch) — le test garde donc son pool à Pit/Cheminée/Colonne, dont
|
||||
// les `ApplyTo` n'écrivent aucun champ de détail. **C'est CETTE condition, et rien d'autre,
|
||||
// qui rend l'étape B vérifiable avant l'étape C.**
|
||||
//
|
||||
// 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
|
||||
@@ -3036,7 +3339,10 @@ namespace VoxelDensityOps
|
||||
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<FRoomColumnMod>(P, RoomPtr)); // 4d — colonnes (pré-cuites)
|
||||
OutStack.Add(MakeUnique<FDomeMod>(P, RoomPtr)); // 4g — dômes
|
||||
OutStack.Add(MakeUnique<FPinchMod>(P, RoomPtr)); // 4h — pincement
|
||||
OutStack.Add(MakeUnique<FFloorBiasMod>(P, RoomPtr)); // fin 4h — biais de sol
|
||||
OutStack.Add(MakeUnique<FWormFieldSource>(P, Seed));
|
||||
|
||||
OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ,
|
||||
|
||||
@@ -266,14 +266,15 @@ namespace VoxelDensityOps
|
||||
const UVoxelStrateManager* StrateManager);
|
||||
|
||||
/**
|
||||
* TunnelNetwork — **ÉTAPES A + B1 + B2 + B3, PILE ENCORE INCOMPLÈTE** :
|
||||
* TunnelNetwork — **ÉTAPES A + B COMPLÈTES, 19 ops** :
|
||||
* ConstantRock → RoomGraph(warp + pits + cheminées) → SdfCarve → CaveRoughness(4b)
|
||||
* → Terrace → LayerLines → Ribbing → Overhang → Cliff → Scallop → Arch
|
||||
* → Worms → [structural ×3]
|
||||
* → Terrace → LayerLines → Ribbing → Overhang → Cliff → Scallop → Arch → RoomColumn(4d)
|
||||
* → Dome(4g) → Pinch(4h) → FloorBias → Worms → [structural ×3]
|
||||
*
|
||||
* ⛔ 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.
|
||||
* ⛔ NE PAS brancher cet archétype dans `UsesOperatorStackForChunk` avant l'étape C :
|
||||
* l'override d'op PAR SALLE n'est pas porté, donc les onze modificateurs qui devraient lire les
|
||||
* params de la salle la plus proche lisent ceux de la strate. Identique tant qu'aucune salle ne
|
||||
* porte un op de type détail — ce que le test garantit et qu'un monde réel ne garantit pas.
|
||||
*
|
||||
* ⚠️ `FRoomGraphSource` **APPELLE** `BuildChunkCache`/`EvaluateSDFCached`, il ne les transcrit
|
||||
* pas : c'est là que vit la discipline d'invariance de fenêtre à deux régions (`ARCHITECTURE
|
||||
|
||||
Reference in New Issue
Block a user