From 239c037f35aeab48ebb9aa97646df03896935626 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Tue, 28 Jul 2026 03:55:39 +0200 Subject: [PATCH] feat(opstack): the twelve detail modifiers inherit the room source's box verdict MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-on to the numeric fold, and the reason that commit does not yet pay off on TunnelNetwork. Separate commit because it is a separate mechanism with a separate revert story: the fold change was about AMPLITUDES, this one is about IDENTITY. THE OBSERVATION Every one of the twelve detail modifiers is gated on bNearCaveSurface, i.e. on Sdf < BlendRadius*3, and Sdf only becomes finite if the room graph wrote something. So wherever the source proves that no room and no tunnel reaches the box, Sdf stays FLT_MAX across the whole box and all twelve are IDENTITY -- not merely bounded, not merely CarveOnly-with-a-small-number. Identity. Each of them already knew this per voxel (it is their first `if`) and still declared Both / FillOnly / CarveOnly per box, which killed deep bedrock's AllSolid hypothesis exactly as hard as an operator that genuinely acts there. Twelve over-cautious declarations, one cause: they were not asking the source they depend on, although since C1 they already hold a pointer to it. VF_NoCaveOverBox(Rooms, Box, Ctx) short-circuits each EffectOverBox to Identity when the source itself answers Identity. Strictly conservative: it never returns Identity on its own authority, only where the source already did. A null pointer also means Identity, and that is correct rather than convenient -- with no room source in the stack, Sdf is FLT_MAX everywhere and the gate never opens. FCaveRoughnessMod gains the pointer purely for this. It is named RoomsForBox and documented as NOT for Eval, because that op deliberately reads STRATE params rather than LocalParams() and mixing the two up is the exact mistake C1's note exists to prevent. ⚠️ WHAT THIS BUYS TODAY: almost nothing, and that is worth stating rather than implying. FRoomGraphSource::EffectOverBox still answers Identity only when RoomDensity <= 0. The short-circuit becomes the deep-bedrock switch on the day the source answers SPATIALLY -- its room and tunnel bounds are already in the SDF cache; what it costs is building that cache for the queried box, which only pays once ClassifyTile actually consumes ClassifyBox. The wiring is put in now so that day touches ONE place instead of thirteen. No test change: this can only turn Both/FillOnly/CarveOnly into Identity where the source already returned Identity, so no existing verdict can move. The TunnelNetwork test still asserts 0 proved verdicts over 40 tiles, and it should still hold. Co-Authored-By: Claude Opus 5 --- .../Private/VoxelDensityOpStack.cpp | 81 +++++++++++++++---- 1 file changed, 66 insertions(+), 15 deletions(-) diff --git a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp index 5230ff2..2269960 100644 --- a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp +++ b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp @@ -2154,6 +2154,40 @@ namespace uint32 LayoutVersion = 0; }; + //========================================================================= + // LA CLÉ QUI REND LE PLIAGE NUMÉRIQUE PAYANT : hériter du verdict de la source + //========================================================================= + // ⚠️ SANS CECI, LES BORNES D'AMPLITUDE NE SERVENT À RIEN SUR CET ARCHÉTYPE, et c'est le point + // que `OPSTACK-DECOMPOSITION §0.2` ne dit pas explicitement. + // + // Les douze modificateurs de détail sont TOUS gated sur `bNearCaveSurface`, c'est-à-dire sur + // `Sdf < SDFBlendRadius·3`. Or `Sdf` ne devient fini que si le graphe de salles a écrit quelque + // chose. **Là où la source prouve qu'aucune salle ni tunnel n'atteint la boîte, `Sdf` reste + // `FLT_MAX` sur toute la boîte, donc les douze sont l'IDENTITÉ** — et pas seulement « bornés ». + // + // Chacun le sait déjà par voxel (son premier `if`) mais le déclarait `Both`/`FillOnly` par boîte, + // ce qui tuait l'hypothèse `AllSolid` du roc profond aussi sûrement qu'un opérateur réellement + // actif. Douze déclarations trop prudentes, une seule cause : ils ne consultaient pas la source + // dont ils dépendent, alors qu'ils en tiennent déjà le pointeur (étape C1). + // + // ⚠️ AUJOURD'HUI CE SHORT-CIRCUIT NE TIRE PRESQUE JAMAIS : `FRoomGraphSource::EffectOverBox` + // rend `Identity` uniquement quand `RoomDensity <= 0`. Il devient l'interrupteur du bedrock + // profond le jour où la source répond SPATIALEMENT (ses bornes de salles et de tunnels sont déjà + // dans le cache — il faut le construire pour la boîte interrogée, ce qui ne se paie qu'une fois + // `ClassifyTile` branché sur `ClassifyBox`). Le câblage est posé maintenant pour que ce jour-là + // il n'y ait qu'UN endroit à changer, pas treize. + // + // Without this, the amplitude bounds buy nothing here: all twelve modifiers are gated on the SDF + // the room source writes, so where the source proves no cave reaches the box they are IDENTITY, + // not merely bounded. They already hold the pointer; they simply were not asking. + FORCEINLINE bool VF_NoCaveOverBox(const FRoomGraphSource* Rooms, const FBox& VoxelBox, + const FVoxelOpContext& Ctx) + { + // Pas de source ⇒ `Sdf` reste FLT_MAX ⇒ le gate est faux partout ⇒ identité. Conservatif + // dans le bon sens : on ne rend `Identity` que quand la source elle-même le rend. + return Rooms == nullptr || Rooms->EffectOverBox(VoxelBox, Ctx) == EVoxelOpEffect::Identity; + } + //========================================================================= // RÔLE 3 — MODIFIER : RUGOSITÉ DE PAROI, ESPACE DENSITÉ (TunnelNetwork, STEP 4b) //========================================================================= @@ -2184,8 +2218,12 @@ namespace class FCaveRoughnessMod final : public IVoxelDensityOp { public: - FCaveRoughnessMod(const FStrateGenerationParams& InP, int32 Seed) - : P(InP), SeedU((uint32)Seed) {} + /** @param InRoomsForBox ⚠️ UNIQUEMENT pour `EffectOverBox`. Cet opérateur lit délibérément + * les params de la STRATE et NON `LocalParams()` (voir la note d'en-tête) ; le pointeur ne + * sert qu'à hériter du verdict de boîte de la source. Ne pas s'en servir dans `Eval`. */ + FCaveRoughnessMod(const FStrateGenerationParams& InP, int32 Seed, + const FRoomGraphSource* InRoomsForBox) + : P(InP), SeedU((uint32)Seed), RoomsForBox(InRoomsForBox) {} EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; } void PrepareChunk(const FVoxelOpContext&) override {} @@ -2310,8 +2348,9 @@ namespace * Deuxième client pour le pliage numérique de `OPSTACK-DECOMPOSITION §0.2`, noté au point * exact où la borne manque (le premier est `FWormFieldSource::MaxCarveAmplitude`). */ - EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override + EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override { + if (VF_NoCaveOverBox(RoomsForBox, VoxelBox, Ctx)) { return EVoxelOpEffect::Identity; } return (P.SurfaceRoughness > 0.0f) ? EVoxelOpEffect::Both : EVoxelOpEffect::Identity; } @@ -2335,6 +2374,7 @@ namespace private: FStrateGenerationParams P; uint32 SeedU; + const FRoomGraphSource* RoomsForBox; // NON possédant, et NON lu par Eval }; //========================================================================= @@ -2429,9 +2469,10 @@ namespace /** `Both` : `Offset` change de signe d'une demi-marche à l'autre. Amplitude bornée par * `StepH/2` — troisième client du pliage numérique de `OPSTACK-DECOMPOSITION §0.2`. */ - EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override + EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override { // Borne au niveau STRATE — voir la note d'`FLayerLineMod::EffectOverBox`. + if (VF_NoCaveOverBox(Rooms, VoxelBox, Ctx)) { return EVoxelOpEffect::Identity; } return (P.TerraceStepHeight > 0.0f) ? EVoxelOpEffect::Both : EVoxelOpEffect::Identity; } @@ -2482,7 +2523,7 @@ namespace /** Ne SOUSTRAIT que (`LineValue ≥ 0`, `Depth ≥ 0`) ⇒ `CarveOnly`, jamais `Both`. Un des * rares modificateurs de détail qui garde une DIRECTION exploitable par le pliage. */ - EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override + EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override { // ⚠️ VERDICT DE BOÎTE ET OVERRIDE PAR SALLE : la borne se lit sur les params de la // STRATE, pas sur ceux d'une salle — une boîte couvre plusieurs salles, donc aucune @@ -2492,6 +2533,7 @@ namespace // être TROP OPTIMISTE. Aucun risque aujourd'hui : rien ne consomme `ClassifyBox` en // production (cf. la file d'attente post-8/8), et il faudra le régler AVANT que // `ClassifyTile` ne le consomme. Noté dans OPSTACK-PROGRESS. + if (VF_NoCaveOverBox(Rooms, VoxelBox, Ctx)) { return EVoxelOpEffect::Identity; } return (P.LayerLineSpacing > 0.0f && P.LayerLineDepth > 0.0f) ? EVoxelOpEffect::CarveOnly : EVoxelOpEffect::Identity; } @@ -2556,9 +2598,10 @@ namespace } /** N'AJOUTE que du solide ⇒ `FillOnly`. */ - EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override + EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override { // Borne au niveau STRATE — voir la note d'`FLayerLineMod::EffectOverBox`. + if (VF_NoCaveOverBox(Rooms, VoxelBox, Ctx)) { return EVoxelOpEffect::Identity; } return (P.RibbingSpacing > 0.0f && P.RibbingDepth > 0.0f) ? EVoxelOpEffect::FillOnly : EVoxelOpEffect::Identity; } @@ -2626,9 +2669,10 @@ namespace } /** Lobe positif seulement ⇒ n'AJOUTE que du solide ⇒ `FillOnly`. */ - EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override + EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override { // Borne au niveau STRATE — voir la note d'`FLayerLineMod::EffectOverBox`. + if (VF_NoCaveOverBox(Rooms, VoxelBox, Ctx)) { return EVoxelOpEffect::Identity; } return (P.OverhangStrength > 0.0f && P.OverhangDepth > 0.0f) ? EVoxelOpEffect::FillOnly : EVoxelOpEffect::Identity; } @@ -2705,9 +2749,10 @@ namespace } /** `Both` : le signe suit celui de `VertGrad · CaveSDF`, donc les deux directions. */ - EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override + EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override { // Borne au niveau STRATE — voir la note d'`FLayerLineMod::EffectOverBox`. + if (VF_NoCaveOverBox(Rooms, VoxelBox, Ctx)) { return EVoxelOpEffect::Identity; } return (P.CliffStrength > 0.0f) ? EVoxelOpEffect::Both : EVoxelOpEffect::Identity; } @@ -2767,9 +2812,10 @@ namespace } /** Lobe positif seulement, SOUSTRAIT ⇒ `CarveOnly`. */ - EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override + EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override { // Borne au niveau STRATE — voir la note d'`FLayerLineMod::EffectOverBox`. + if (VF_NoCaveOverBox(Rooms, VoxelBox, Ctx)) { return EVoxelOpEffect::Identity; } return (P.ScallopStrength > 0.0f) ? EVoxelOpEffect::CarveOnly : EVoxelOpEffect::Identity; } @@ -2875,9 +2921,10 @@ namespace /** 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 + EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override { // Borne au niveau STRATE — voir la note d'`FLayerLineMod::EffectOverBox`. + if (VF_NoCaveOverBox(Rooms, VoxelBox, Ctx)) { return EVoxelOpEffect::Identity; } return (P.ArchDensity > 0.0f) ? EVoxelOpEffect::FillOnly : EVoxelOpEffect::Identity; } @@ -2937,8 +2984,9 @@ namespace /** 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 + EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override { + if (VF_NoCaveOverBox(Rooms, VoxelBox, Ctx)) { return EVoxelOpEffect::Identity; } return EVoxelOpEffect::FillOnly; } @@ -3029,9 +3077,10 @@ namespace } /** Ne SOUSTRAIT que ⇒ `CarveOnly`. */ - EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override + EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override { // Borne au niveau STRATE — voir la note d'`FLayerLineMod::EffectOverBox`. + if (VF_NoCaveOverBox(Rooms, VoxelBox, Ctx)) { return EVoxelOpEffect::Identity; } return (P.DomeDensity > 0.0f) ? EVoxelOpEffect::CarveOnly : EVoxelOpEffect::Identity; } @@ -3127,9 +3176,10 @@ namespace } /** N'AJOUTE que du solide ⇒ `FillOnly`. */ - EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override + EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override { // Borne au niveau STRATE — voir la note d'`FLayerLineMod::EffectOverBox`. + if (VF_NoCaveOverBox(Rooms, VoxelBox, Ctx)) { return EVoxelOpEffect::Identity; } return (P.PinchDensity > 0.0f) ? EVoxelOpEffect::FillOnly : EVoxelOpEffect::Identity; } @@ -3182,9 +3232,10 @@ namespace } /** N'AJOUTE que du solide ⇒ `FillOnly`. */ - EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override + EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override { // Borne au niveau STRATE — voir la note d'`FLayerLineMod::EffectOverBox`. + if (VF_NoCaveOverBox(Rooms, VoxelBox, Ctx)) { return EVoxelOpEffect::Identity; } return (P.FloorBias > 0.0f) ? EVoxelOpEffect::FillOnly : EVoxelOpEffect::Identity; } @@ -3508,7 +3559,7 @@ namespace VoxelDensityOps // `Sdf < SDFBlendRadius·3` via VF_NearCaveSurface. Voir la note de l'étape B5 là-bas. // L'ORDRE EST CELUI DE L'ORIGINAL et il compte : chacun lit la densité que le précédent // a laissée (le biais de sol, en particulier, existe pour rattraper la rugosité). - OutStack.Add(MakeUnique(P, Seed)); // 4b + OutStack.Add(MakeUnique(P, Seed, RoomPtr)); // 4b OutStack.Add(MakeUnique(P, Seed, RoomPtr)); // 4c — terrasses OutStack.Add(MakeUnique(P, RoomPtr)); // 4c — lignes de strates OutStack.Add(MakeUnique(P, RoomPtr)); // 4c — nervures