feat: Phase 1 skeleton — the density operator stack contract (header only)

Public/VoxelDensityOp.h: IVoxelDensityOp (PrepareChunk / Eval / EffectOverBox /
ClassifyBox / IsXYPure), EVoxelOpEffect, EVoxelOpRole (the four roles),
EVoxelOpCombine, FVoxelOpContext, and the box-verdict fold.

Nothing is wired in: GetDensityAt is untouched, the archetype switch is intact,
no operator exists yet. This is the contract plus the reasoning behind it.

Two things worth flagging beyond OPSTACK-PLAN §3:

- ClassifyBox is NOT source-only. ApplyBoundarySeal does Max(D, SealFactor*Base)
  inside its band, i.e. it FORCES solid regardless of input. Pure direction
  (FillOnly) cannot express that: over a box that sits entirely in the top seal
  band above the terrain the source says AllAir, FillOnly then kills AllAir, both
  hypotheses die and the tile becomes Mixed — whereas ClassifyTile returns
  AllSolid there today. Not a hole, but a silent loss of exactly the trivial
  tiles T1.d exists to skip. So forcing ops override the fold, and ops after them
  still apply (a passage crossing that box takes the verdict back, as today).

- The fold reproduces the current hand-written ClassifyTile line for line; the
  mapping is written out in the header. That correspondence is the evidence the
  abstraction fits this codebase rather than being imposed on it.

Also moves EVoxelTileClass from VoxelGenerator.h to VoxelTypes.h (CODEMAP 3.2:
foundational, no UClass, everyone includes it) so the op header needs no UCLASS
dependency. All existing users reach it through VoxelTypes.h transitively.

Types are plain C++ on purpose — no UHT, no .generated.h. They become
UENUM/USTRUCT in Phase 3 when ops turn into data assets.

UNVERIFIED: not compiled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 01:55:05 +02:00
parent 6eec796403
commit d41d34ecd3
3 changed files with 417 additions and 17 deletions
+391
View File
@@ -0,0 +1,391 @@
// VoxelDensityOp.h
// LE CONTRAT de la pile d'opérateurs de densité / THE density operator stack CONTRACT.
// Phase 1 de OPSTACK-PLAN.md. HEADER SEUL — rien n'est encore branché dans GetDensityAt.
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// POURQUOI / WHY
// ─────────────────────────────────────────────────────────────────────────────────────────
// UVoxelGenerator::GetDensityAt est aujourd'hui un `switch` sur 8 ECaveGeneratorType, chacun
// possédant sa fonction de densité et son struct de params. Conséquences : une nouvelle idée de
// monde coûte ~6 sites d'édition, et surtout LES IDÉES NE PEUVENT PAS SE COMBINER — un archétype
// possède le voxel entier. On ne peut pas écrire « une strate de surface dont les montagnes
// contiennent un réseau de salles, avec des îles flottantes dans le vide au-dessus », à aucun prix.
//
// GetDensityAt is today a `switch` over 8 ECaveGeneratorType, each owning a bespoke density
// function and param struct. A new world idea costs ~6 edit sites, and — the real problem —
// IDEAS CANNOT COMBINE: one archetype owns the whole voxel.
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// ⚠️ CE N'EST PAS L'ANCIEN SYSTÈME DE « ROOM OPERATIONS » / THIS IS NOT THE OLD ROOM-OPS SYSTEM
// ─────────────────────────────────────────────────────────────────────────────────────────
// UVoxelTerrainOpDefinition (Terrace, Ribbing, Cliff, Scallop, Overhang, Arch, Column, Pit…) ne
// sait que PERTURBER une densité près d'une surface qui existe déjà. Il ne décide jamais ce que le
// champ EST — cette décision vit dans le `switch`. Une pile d'opérateurs qui se contenterait de
// cela aurait reconstruit le switch avec des étapes en plus.
//
// D'où QUATRE RÔLES, dont l'ancien système n'occupait que le troisième :
//
// 1. FIELD SOURCE — fabrique un champ À PARTIR DE RIEN. C'est ce qui fait qu'une grotte est
// une grotte et qu'un monde ouvert est un monde ouvert. Chaque archétype
// d'aujourd'hui est fondamentalement l'une de ces sources.
// 2. COMBINER — comment deux champs fusionnent (min/max/smooth/mask). C'EST le rôle qui
// achète la composition ; sans lui il n'y a pas de refactor.
// 3. DETAIL MODIFIER — l'ancien système, rétrogradé à un rôle sur quatre. Inchangé.
// 4. STRUCTURAL POST — spine (0,0) → seal → passages → diff layer. Des INVARIANTS de monde,
// pas des choix créatifs : toujours ajoutés, dans cet ordre, jamais
// omissibles par l'auteur.
//
// The old system only ever had role 3. Roles 1 and 2 are the new thing, and role 4 is what keeps
// the descent structure intact no matter what an author assembles.
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// LA CLÉ DE VOÛTE : EffectOverBox — direction, pas intervalle / THE KEYSTONE: direction, not intervals
// ─────────────────────────────────────────────────────────────────────────────────────────
// La version « complète » d'une borne rendrait un intervalle numérique. NE PAS COMMENCER LÀ.
// Presque tout opérateur existant est UNIDIRECTIONNEL : il ne fait que creuser, ou que remplir.
// Cela suffit à reproduire GÉNÉRIQUEMENT chaque garde écrite à la main dans ClassifyTile :
//
// « passages ⇒ bCanSolid = false » EST CarveOnly
// « ponts/arêtes ⇒ bCanAir = false » EST FillOnly
// « aucun passage près de cette boîte » EST Identity
//
// Donc la Phase 1 n'a besoin d'AUCUNE borne numérique et obtient déjà toute la propriété de
// sûreté. Les intervalles sont un resserrement ultérieur pour le coût de génération, pas un
// prérequis de correction. C'est ce qui rend le premier pas petit.
//
// Phase 1 needs NO numeric bounds and already gets the whole safety property. Intervals are a
// later tightening for gen cost, not a correctness prerequisite.
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// LE GROS LOT PERF : les strates de grotte ne sautent AUCUNE tuile aujourd'hui
// THE PERF PRIZE: cave strates skip ZERO tiles today
// ─────────────────────────────────────────────────────────────────────────────────────────
// ClassifyTile ne sait prouver que les gaps de bedrock et SurfaceWorld ; tout le reste tombe sur
// `return EVoxelTileClass::Mixed; // archétype cave […] pas prouvable en v1`. TunnelNetwork, Maze,
// VerticalShafts, FloatingIslands, FlatPlain, CrystalChamber et Underwater ne captent donc RIEN du
// gain T1.d (84 % des générations vides, 44 % de CPU worker). Écrire un prouveur sur mesure par
// archétype a toujours été trop cher — EffectOverBox EST le mécanisme générique qui le rend gratuit :
// une source à graphe de salles qui rend Identity quand aucune borne de salle ni de tunnel n'atteint
// la boîte rend le bedrock profond sautable pour la première fois.
//
// **Traiter cela comme un livrable explicite de chaque portage, pas comme un effet de bord.**
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// ÉTAT / STATUS
// ─────────────────────────────────────────────────────────────────────────────────────────
// Phase 1, header seul. Aucun opérateur n'existe encore, GetDensityAt n'a pas changé, le `switch`
// est intact. Prochaine étape : porter Maze en le DÉCOMPOSANT (source réseau de couloirs + modifier
// de rugosité), pas en l'emballant. Voir OPSTACK-DECOMPOSITION.md pour le plan par archétype.
//
// Phase 1, header only. No operator exists yet, GetDensityAt is unchanged, the switch is intact.
//
// NOTE sur les UENUM : ces types sont volontairement du C++ nu (pas d'UHT, pas de .generated.h).
// Ils deviendront UENUM/USTRUCT en Phase 3, quand les opérateurs deviendront des data assets et
// auront besoin d'être édités dans l'éditeur. Les promouvoir plus tôt n'achèterait rien et
// ajouterait une étape UHT à chaque itération.
#pragma once
#include "CoreMinimal.h"
#include "VoxelTypes.h" // CHUNK_SIZE, EVoxelTileClass
struct FBiomeContext;
//=============================================================================
// LES QUATRE RÔLES / THE FOUR ROLES
//=============================================================================
// Le rôle n'est pas décoratif : le compilateur de pile s'en sert pour ORDONNER. Les
// StructuralPost sont toujours ajoutés en dernier, dans l'ordre fixe spine → seal → passage →
// diff, quoi que l'auteur ait assemblé. Un auteur ne peut pas les omettre : la descente doit
// rester possible, les seals doivent tenir, les passages doivent percer, les éditions du joueur
// gagnent toujours.
enum class EVoxelOpRole : uint8
{
// Rôle 1 — fabrique un champ à partir de rien. Une pile en a au moins un (sa racine).
FieldSource,
// Rôle 2 — fusionne le champ précédent avec le suivant. Voir EVoxelOpCombine.
Combiner,
// Rôle 3 — perturbe un champ existant près de sa surface (l'ancien UVoxelTerrainOpDefinition).
DetailModifier,
// Rôle 4 — invariants de monde. Ajoutés automatiquement, ordre fixe, non omissibles.
StructuralPost,
};
//=============================================================================
// COMPOSITION / COMBINERS
//=============================================================================
// Vocabulaire délibérément petit, et il réutilise ce qui existe déjà
// (VoxelSDF::SmoothMin / SmoothMax).
//
// RAPPEL DE SIGNE — la source n°1 de confusion du plugin :
// au mesher, NÉGATIF = SOLIDE, POSITIF = AIR (IsoLevel 0).
// Donc « ajouter du solide » = prendre le MIN, « creuser de l'air » = prendre le MAX.
// SIGN REMINDER: at the mesher NEGATIVE = SOLID, POSITIVE = AIR. So "add solid" is min(),
// "carve air" is max(). Getting this backwards inverts the world.
enum class EVoxelOpCombine : uint8
{
Replace, // ignore l'entrée — racine de pile (heightfield, densité de base)
Union, // min() — ajoute du solide : ponts, îles, colonnes
Subtract, // max() — creuse de l'air : salles, tunnels, passages, spine
SmoothUnion, // VoxelSDF::SmoothMin(k) — jonctions organiques
SmoothSubtract, // VoxelSDF::SmoothMax(k)
Add, // accumulation scalaire — termes de bruit / rugosité
Mask, // met à l'échelle l'opérateur SUIVANT par un champ [0,1]
// (poids de biome, porte de pente, relief, profondeur).
// C'est Mask qui achète le plus d'expressivité : « cet opérateur, mais
// seulement dans les régions à fort relief » devient de la composition
// au lieu d'une garde codée en dur dans chaque opérateur.
};
//=============================================================================
// EFFET SUR UNE BOÎTE / EFFECT OVER A BOX
//=============================================================================
// CONSERVATIF PAR CONSTRUCTION. Rendre `Both` est TOUJOURS SÛR (ça ne coûte que du CPU) ;
// rendre le mauvais est un TROU — pas de géométrie, pas de collision, invisible jusqu'à ce
// qu'un joueur tombe au travers. En cas de doute : `Both`.
//
// CONSERVATIVE BY CONSTRUCTION. Returning `Both` is ALWAYS SAFE (it only costs CPU); returning
// the wrong one is a HOLE. When unsure: `Both`.
enum class EVoxelOpEffect : uint8
{
// Prouvablement aucun effet sur cette boîte. C'est le early-out qui rend la pile rapide,
// et c'est ce qui rendra le bedrock profond sautable pour les strates de grotte.
Identity,
// Ne peut que pousser la densité vers l'AIR ⇒ tue l'hypothèse « tout solide ».
CarveOnly,
// Ne peut que pousser la densité vers le SOLIDE ⇒ tue l'hypothèse « tout air ».
FillOnly,
// Non contraint ⇒ tue les deux hypothèses.
Both,
};
//=============================================================================
// CONTEXTE DE CHUNK / CHUNK CONTEXT
//=============================================================================
// Miroir de ce que le bloc thread_local CP_* résout aujourd'hui dans GetDensityAt.
//
// ⚠️ LayoutVersion est ici PAR CONSTRUCTION, pas par politesse. AUDIT C2 : trois caches
// existants (CP_Chunk, OC_Chunk, BM_Chunk) sont clés sur ChunkCoord SEUL, donc après un
// RebuildStrates ou une édition à chaud un worker dont le cache est encore chaud pour ce chunk
// saute le refetch et génère avec les ANCIENS params. En faisant porter LayoutVersion par le
// contexte, un nouvel opérateur ne PEUT PAS oublier de l'inclure dans sa clé.
//
// LayoutVersion is here BY CONSTRUCTION, not by politeness — see AUDIT C2. Carrying it in the
// context means a new op CANNOT forget to put it in its cache key.
struct FVoxelOpContext
{
FIntVector ChunkCoord = FIntVector::ZeroValue;
// Pas d'échantillonnage LOD (1/2/4…). Un opérateur a le droit de se simplifier quand Step
// est grand — c'est le contrat T2.b : le bruit volumétrique par voxel perd des octaves au
// loin, le bruit de champ XY délibérément non (il alimente des caches box-validés partagés).
int32 Step = 1;
uint32 Seed = 0;
// Compteur de génération du layout (UVoxelStrateManager::GetLayoutVersion()).
// DOIT faire partie de toute clé de cache. Voir AUDIT C2.
uint32 LayoutVersion = 0;
// Bornes Z de la strate en coords VOXEL (pas cm).
float StrateTopWorldZ = 0.0f;
float StrateBottomWorldZ = 0.0f;
// null = cette strate n'a pas de champ de biome.
const FBiomeContext* Biome = nullptr;
};
//=============================================================================
// L'INTERFACE / THE INTERFACE
//=============================================================================
// Trois méthodes, et elles FORMALISENT CE QUE LE CODE FAIT DÉJÀ À LA MAIN : chaque archétype
// hisse déjà son travail constant-par-chunk dans un cache thread_local (= PrepareChunk), évalue
// à bas coût par voxel (= Eval), et possède déjà dans ClassifyTile une déclaration écrite à la
// main de ce qu'il peut faire à une tuile (= EffectOverBox). Ce n'est pas une nouvelle
// discipline, c'est la discipline existante, nommée.
class IVoxelDensityOp
{
public:
virtual ~IVoxelDensityOp() = default;
virtual EVoxelOpRole GetRole() const = 0;
/**
* Hisser ici TOUT le travail constant sur le chunk : listes de salles, grilles de biome,
* caches de colonnes, cuissons de treillis. Appelé une fois par chunk et par worker.
* C'est ici que déménagent les caches thread_local d'aujourd'hui.
*
* THREADING : appelé sur des workers. L'opérateur ne doit écrire QUE son propre état
* par-chunk ; le Generator / le Mesher / le StrateManager restent en LECTURE SEULE
* (invariant ARCHITECTURE §8.10). Toute clé de cache DOIT inclure Ctx.LayoutVersion.
*/
virtual void PrepareChunk(const FVoxelOpContext& Ctx) = 0;
/**
* Par voxel. InDensity = ce que la pile a produit jusqu'ici, convention MC
* (négatif = solide, positif = air). Coordonnées en VOXELS, pas en cm.
*
* INVARIANCE DE FENÊTRE (ARCHITECTURE §8.4) : fonction PURE de (coords monde, seed, layout).
* Le même point évalué depuis une autre tuile, un autre ordre, un autre thread doit rendre le
* float BIT-IDENTIQUE. Pas « proche » : 1 ULP d'écart entre deux fenêtres est une couture
* visible, et en multijoueur une divergence de monde. Le test
* VoxelForge.Determinism.DensityPurity vérifie cela.
*/
virtual float Eval(float WorldX, float WorldY, float WorldZ, float InDensity) const = 0;
/**
* CONSERVATIF. Phase 1 : direction seule. Phase 3 : surcharge avec intervalle numérique.
* Rendre Both est toujours sûr ; rendre le mauvais est un trou.
*
* Le contrat est « conservatif », pas « forme close » : un opérateur A LE DROIT
* D'ÉCHANTILLONNER pour répondre. C'est exactement ce que fait ClassifyTile aujourd'hui pour
* SurfaceWorld — il évalue ComputeSurfaceColumn sur le treillis EXACT du mesher, mêmes
* fonctions, mêmes floats, donc verdict exact plutôt qu'estimé. Cela DOIT survivre au portage.
*
* The contract is "conservative", not "closed-form": an op MAY sample to answer.
*/
virtual EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const = 0;
/**
* OPÉRATEURS FORÇANTS. Certains opérateurs ne « déplacent » pas la densité d'entrée : ils
* l'ÉCRASENT. La question « dans quelle direction peux-tu bouger ce champ ? » n'a alors pas de
* sens ; la bonne question est « sais-tu prouver que toute cette boîte est d'un seul côté,
* QUELLE QUE SOIT l'entrée ? ».
*
* Deux familles répondent autre chose que Mixed :
* • les SOURCES (rôle 1) — elles posent le champ, donc elles le savent par construction ;
* • les op STRUCTURELS forçants — typiquement ApplyBoundarySeal, qui à l'intérieur de sa
* bande fait `Max(Density, SealFactor·BaseDensity)` avec SealFactor > 0 : le résultat est
* solide garanti quoi qu'il y ait eu avant. C'est exactement ce que ClassifyTile encode
* aujourd'hui avec « bande de seal ⇒ bCanAir = false » — et un simple FillOnly ne suffirait
* PAS à le reproduire (voir VF_FoldOp plus bas).
*
* Par défaut Mixed = « je ne sais pas », toujours sûr. Une source à primitives placées (graphe
* de salles, îles, puits) répond en testant ses bornes ; une source heightfield répond en
* échantillonnant ses colonnes sur le treillis exact, exactement comme aujourd'hui.
*
* FORCING OPS. Some ops do not *move* the input density, they *overwrite* it. Default Mixed =
* "I don't know", always safe. The boundary seal is the non-source example, and it is the
* reason this method exists at all rather than being folded into EffectOverBox.
*/
virtual EVoxelTileClass ClassifyBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const
{
return EVoxelTileClass::Mixed;
}
/**
* Déclare si Eval dépend de Z. Les opérateurs purs en XY reçoivent le traitement du cache de
* colonnes T1.a de façon GÉNÉRIQUE, au lieu que SurfaceWorld en ait un sur mesure.
*
* ⚠️ Le cache de colonnes est clé sur (boîte XY, StrateKey, Seed) SANS ChunkZ et il est
* partagé sur TOUTE la pile verticale de chunks. Mettre une donnée dépendante de Z dans un
* opérateur qui se déclare XY-pur corrompt silencieusement chaque chunk de la colonne, et
* ValidateDeterminism — qui échantillonne le long d'une frontière en X — ne le verrait pas.
*/
virtual bool IsXYPure() const { return false; }
};
//=============================================================================
// LE PLIAGE : comment la pile devient un verdict de tuile
// THE FOLD: how a stack becomes a tile verdict
//=============================================================================
// C'est l'algorithme générique qui remplace le ClassifyTile écrit à la main, et il REPRODUIT
// EXACTEMENT le comportement d'aujourd'hui — vérifié ligne à ligne contre VoxelGenerator.cpp :
//
// source gap bedrock → ClassifyBox = AllSolid
// source SurfaceWorld → ClassifyBox échantillonne les colonnes sur le treillis exact
// ApplyPassageCarving → CarveOnly (tue AllSolid) ≡ « AnyPassageNearBox ⇒ bCanSolid=false »
// ApplyOriginSpine → CarveOnly (tue AllSolid) ≡ le test cercle/boîte XY
// ApplyBoundarySeal → ClassifyBox = AllSolid DANS sa bande (opérateur forçant),
// FillOnly ailleurs ≡ « bande de seal ⇒ bCanAir=false »
// disturbances chasms → CarveOnly ≡ « ChasmDensity > 0 ⇒ bCanSolid=false »
// disturbances ponts/arêtes → FillOnly ≡ « Bridge/RidgeDensity > 0 ⇒ bCanAir=false »
// diff layer → Both si des mods touchent la boîte, sinon Identity
// ≡ « HasAnyModInChunkRange ⇒ Mixed »
//
// et le verdict final « exactement une hypothèse survit, sinon Mixed » est littéralement le
// `if (bCanSolid == bCanAir) return Mixed;` de la fin de ClassifyTile.
//
// This fold reproduces today's hand-written ClassifyTile exactly — verified line by line against
// VoxelGenerator.cpp. That correspondence is the evidence that the abstraction fits this codebase
// rather than being imposed on it.
/** Les deux hypothèses que ClassifyTile poursuit, sous forme d'état pliable. */
struct FVoxelBoxHypotheses
{
bool bCanBeAllSolid = true;
bool bCanBeAllAir = true;
bool IsDead() const { return !bCanBeAllSolid && !bCanBeAllAir; }
/** Verdict final : exactement une hypothèse doit survivre. Égalité = prudence ⇒ Mixed. */
EVoxelTileClass Resolve() const
{
if (bCanBeAllSolid == bCanBeAllAir) { return EVoxelTileClass::Mixed; }
return bCanBeAllSolid ? EVoxelTileClass::AllSolid : EVoxelTileClass::AllAir;
}
};
/** Poser l'état depuis un verdict FORÇANT (source, ou seal dans sa bande) : l'opérateur écrase
* l'entrée, donc il écrase aussi tout ce que la pile avait conclu avant lui. Un verdict « tout
* air » affirme du même coup « pas tout solide », et réciproquement. */
FORCEINLINE void VF_ForceHypotheses(FVoxelBoxHypotheses& H, EVoxelTileClass ForcedVerdict)
{
switch (ForcedVerdict)
{
case EVoxelTileClass::AllSolid: H.bCanBeAllSolid = true; H.bCanBeAllAir = false; break;
case EVoxelTileClass::AllAir: H.bCanBeAllSolid = false; H.bCanBeAllAir = true; break;
case EVoxelTileClass::Mixed:
default: H.bCanBeAllSolid = false; H.bCanBeAllAir = false; break;
}
}
/** Plier l'effet DIRECTIONNEL d'un opérateur dans l'état. Monotone : ne fait que tuer. */
FORCEINLINE void VF_FoldEffect(FVoxelBoxHypotheses& H, EVoxelOpEffect Effect)
{
switch (Effect)
{
case EVoxelOpEffect::Identity: break;
case EVoxelOpEffect::CarveOnly: H.bCanBeAllSolid = false; break;
case EVoxelOpEffect::FillOnly: H.bCanBeAllAir = false; break;
case EVoxelOpEffect::Both:
default: H.bCanBeAllSolid = false;
H.bCanBeAllAir = false; break;
}
}
/**
* Plier UN opérateur. L'ORDRE COMPTE ICI, et c'est délibéré : un opérateur forçant écrase ce que
* la pile avait conclu AVANT lui, tandis que les opérateurs qui suivent continuent de s'appliquer.
*
* Exemple à garder en tête, parce qu'il est le piège : sur une boîte entièrement dans la bande de
* seal, la source dit peut-être « tout air » (on est au-dessus du terrain), puis le seal FORCE
* « tout solide » — verdict AllSolid, comme aujourd'hui. Si le seal ne savait dire que FillOnly,
* on obtiendrait « les deux hypothèses mortes ⇒ Mixed » : pas un trou, mais la perte pure et
* simple d'une des tuiles triviales que T1.d sait sauter. C'est pour cela que ClassifyBox existe.
*
* Réciproquement, un passage qui traverse cette même boîte rend CarveOnly APRÈS le seal et retue
* l'hypothèse solide ⇒ Mixed. Identique au code actuel, où la garde passage et la garde seal se
* neutralisent en `bCanSolid == bCanAir`.
*
* ORDER MATTERS HERE, deliberately: a forcing op overwrites what the stack concluded before it,
* while ops after it still apply. This is what lets the seal recover an AllSolid verdict that a
* pure FillOnly would have thrown away, while still letting a passage take it back.
*/
FORCEINLINE void VF_FoldOp(FVoxelBoxHypotheses& H, const IVoxelDensityOp& Op,
const FBox& VoxelBox, const FVoxelOpContext& Ctx)
{
const EVoxelTileClass Forced = Op.ClassifyBox(VoxelBox, Ctx);
if (Forced != EVoxelTileClass::Mixed)
{
VF_ForceHypotheses(H, Forced);
return;
}
VF_FoldEffect(H, Op.EffectOverBox(VoxelBox, Ctx));
}
+3 -17
View File
@@ -50,23 +50,9 @@ namespace VoxelGenLOD
FORCEINLINE int32 Eff(int32 Octaves) { return FMath::Max(1, Octaves - OctaveBias); } FORCEINLINE int32 Eff(int32 Octaves) { return FMath::Max(1, Octaves - OctaveBias); }
} }
//============================================================================= // NOTE: EVoxelTileClass (le verdict T1.d) a déménagé dans VoxelTypes.h — inclus ci-dessus —
// TRIVIAL-TILE CLASSIFICATION (T1.d) // pour que VoxelDensityOp.h puisse le partager sans dépendre d'un header UCLASS.
//============================================================================= // EVoxelTileClass (the T1.d verdict) moved to VoxelTypes.h, included above.
// Verdict de ClassifyTile pour une tuile AVANT le pré-échantillonnage 33³+ :
// AllSolid / AllAir garantissent que CHAQUE point du treillis du mesher (marge
// ±1 incluse) est du même côté de l'iso ⇒ maillage vide, GenerateMesh est
// sautée. Mixed = "je ne peux pas le prouver" ⇒ génération normale. Un faux
// Mixed coûte juste du CPU ; un faux AllSolid/AllAir ferait un TROU — les
// verdicts ne sont donc émis que sur des bornes exactes (colonnes surface
// échantillonnées au MÊME treillis que le mesher) + gardes conservatives sur
// tout ce qui peut creuser/remplir (spine, passages, disturbances, diff layer).
enum class EVoxelTileClass : uint8
{
Mixed, // peut contenir une surface → mesher normalement
AllSolid, // chaque échantillon prouvé solide → maillage vide
AllAir, // chaque échantillon prouvé air → maillage vide
};
/** /**
* UVoxelGenerator * UVoxelGenerator
+23
View File
@@ -28,6 +28,29 @@ constexpr int32 CHUNK_VOLUME = CHUNK_SIZE * CHUNK_SIZE * CHUNK_SIZE; // 32
constexpr float VOXEL_SIZE = 25.0f; constexpr float VOXEL_SIZE = 25.0f;
//=============================================================================
// TRIVIAL-TILE CLASSIFICATION (T1.d)
//=============================================================================
// Verdict de ClassifyTile pour une tuile AVANT le pré-échantillonnage 33³+ :
// AllSolid / AllAir garantissent que CHAQUE point du treillis du mesher (marge
// ±1 incluse) est du même côté de l'iso ⇒ maillage vide, GenerateMesh est
// sautée. Mixed = "je ne peux pas le prouver" ⇒ génération normale. Un faux
// Mixed coûte juste du CPU ; un faux AllSolid/AllAir ferait un TROU — les
// verdicts ne sont donc émis que sur des bornes exactes (colonnes surface
// échantillonnées au MÊME treillis que le mesher) + gardes conservatives sur
// tout ce qui peut creuser/remplir (spine, passages, disturbances, diff layer).
//
// Vit ici plutôt que dans VoxelGenerator.h pour que VoxelDensityOp.h (le contrat
// de la pile d'opérateurs) puisse s'en servir sans tirer un header UCLASS.
// Lives here rather than in VoxelGenerator.h so VoxelDensityOp.h (the operator-stack
// contract) can use it without pulling in a UCLASS header.
enum class EVoxelTileClass : uint8
{
Mixed, // peut contenir une surface → mesher normalement
AllSolid, // chaque échantillon prouvé solide → maillage vide
AllAir, // chaque échantillon prouvé air → maillage vide
};
//============================================================================= //=============================================================================
// DENSITY → R8 QUANTIZATION (density clipmap / mini-sun shadows) // DENSITY → R8 QUANTIZATION (density clipmap / mini-sun shadows)
//============================================================================= //=============================================================================