// VoxelGenerator.h // Fournit le champ de densité du monde (positif = solide, négatif = air). // // Pipeline density-only: plus de grille de blocs, plus de surface heightfield. // Le mesher appelle GetDensityAt() par voxel pour reconstruire la géométrie. // Chaque strate est générée par sa pile d'opérateurs (VoxelDensityOpStack.h), pilotée par les // params de strate récupérés auprès du StrateManager. #pragma once #include "CoreMinimal.h" #include "UObject/Object.h" #include "VoxelTypes.h" #include "VoxelStrateTypes.h" #include "VoxelBiomeTypes.h" #include "VoxelGenerator.generated.h" // Forward decls (évite les includes transitifs) class UVoxelSettings; class UVoxelStrateManager; class UVoxelDiffLayer; class UVoxelBiomeDefinition; struct FTunnelCoreWorldEvaluation; /** Guards which may be disabled only by the sealed-solid proof's soundness tests. */ enum class EVoxelSealedSolidProofGuard : uint8 { None = 0, DiffLayer = 1u << 0, Passages = 1u << 1, OriginLanding = 1u << 2, DisturbanceChasms = 1u << 3, }; //============================================================================= // LOD-AWARE OCTAVE REDUCTION (T2.b) //============================================================================= // At Step=2/4, noise octaves whose wavelength is smaller than the sampling cell // are pure aliasing cost — they can't shape the coarse isosurface, only shift it // by sub-cell noise. Réduction d'octaves sur les tuiles lointaines (LOD). // // OctaveBias = octaves dropped from PER-VOXEL volumetric noise for the tile // currently being meshed on THIS thread. Default 0 = full quality; set (via // TGuardValue) by UVoxelMarchingCubesMesher::GenerateMesh from its Step and the // opt-in UVoxelSettings::LODOctaveDrop, restored when the tile finishes — so // game-thread queries, deco snapping and the density-volume fill thread always // see 0. fBM/Ridged add octaves low→high frequency, so dropping the TAIL keeps // the coarse shape identical; only sub-cell detail (already unrepresentable at // that Step) disappears. LOD0 (Step=1) is byte-identical regardless. // // Deliberately NOT applied to XY-field noise (heightfield, ceiling, relief, // moisture): those feed box-validated caches that can outlive a tile task on // the same thread, and biome/climate must stay LOD-independent. namespace VoxelGenLOD { // The TLS variables themselves stay module-local because MSVC forbids exporting thread_local // data (C2492). These tiny accessors let the editor explorer build a shared LOD grid without // linking directly to the storage in the game module. extern thread_local int32 OctaveBias; // Sampling step of the tile currently being meshed on this worker. GetDensityAt itself has // no Step parameter because callers outside the mesher are point queries; this TLS is only a // read-only hint for caches that may safely widen their spatial window for coarse tiles. extern thread_local int32 SampleStep; // Exact lattice footprint of the tile currently being meshed. The cache-window switch // uses this rather than inferring a region from an individual sample coordinate, so every // sample in one tile shares one deterministic window even at tile/chunk boundaries. extern thread_local FIntVector TileOriginVoxels; extern thread_local int32 TileCellsPerAxis; VOXELFORGE_API int32 GetThreadOctaveBias(); VOXELFORGE_API void SetThreadOctaveBias(int32 Value); VOXELFORGE_API int32 GetThreadSampleStep(); VOXELFORGE_API void SetThreadSampleStep(int32 Value); // Called by the module's game-thread startup before any worker can enter GetDensityAt. VOXELFORGE_API void InitializeConsoleSwitches(); VOXELFORGE_API bool IsTileCacheWindowEnabled(bool bFusedPath = true); #if WITH_EDITOR /** Research-only override for the bounded editor region evaluator; default false per thread. */ VOXELFORGE_API void SetResearchRegionCacheWindowEnabled(bool bEnabled); #endif VOXELFORGE_API bool GetThreadTileCacheWindow( FIntVector& OutOriginVoxels, int32& OutStep, int32& OutCellsPerAxis); VOXELFORGE_API void SetThreadTileContext( const FIntVector& OriginVoxels, int32 CellsPerAxis); // Effective octave count for a per-voxel noise call site. // At least 1 octave always survives (the coarse base shape). 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 — // 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. /** * UVoxelGenerator * * Objet UObject léger — ne stocke pas de données lourdes. * Tient les paramètres globaux copiés depuis les settings (seed, spine, bord XY) et les services * dont il a besoin (StrateManager pour les params par chunk, DiffLayer pour * les carves du joueur). */ UCLASS(BlueprintType) class VOXELFORGE_API UVoxelGenerator : public UObject { GENERATED_BODY() public: UVoxelGenerator(); //========================================================================= // SEED (source unique: Settings->Seed) //========================================================================= // Les paramètres globaux sont copiés au démarrage pour éviter un déréférencement par voxel. // Les formes créatives viennent ensuite des params de strate. int32 Seed = 0; // Authored radius (voxels) of the guaranteed flat-floored landing room at world XY (0,0) in // every strate — the old “spine” setting now sizes a room and never creates a vertical bore. // 0 disables the origin landing rooms. Copied from VoxelSettings. float OriginSpineRadius = 28.0f; // Radius (voxels) of the bounded world in actor-space XY, measured from (0,0). // 0 keeps the legacy unbounded behavior and is a true no-op for the edge seal. float WorldRadiusVoxels = 8192.0f; // Width (voxels) of the smooth solid ramp at the XY world edge. float EdgeSealThickness = 128.0f; //========================================================================= // SERVICES (injectés par AVoxelWorld::BeginPlay) //========================================================================= // Manager des strates — fournit les params de cave par chunk. // Peut être nullptr: dans ce cas on utilise des params par défaut. UPROPERTY() const UVoxelStrateManager* StrateManager = nullptr; // Diff layer des modifications joueur (carve/fill). // Peut être nullptr: dans ce cas pas de modifs appliquées. UPROPERTY() const UVoxelDiffLayer* DiffLayer = nullptr; void SetStrateManager(const UVoxelStrateManager* InManager) { StrateManager = InManager; } void SetDiffLayer(const UVoxelDiffLayer* InDiffLayer) { DiffLayer = InDiffLayer; } // Copie le seed depuis les settings. Appelé au démarrage et lors d'un ChangeSeed. void InitializeSettings(const UVoxelSettings* Settings); //========================================================================= // API DE DENSITÉ //========================================================================= /** * Densité combinée (strates + diff du joueur). * * Convention de sortie (MC): négatif = solide, positif = air. * C'est ce que le marching cubes attend comme champ scalaire. * * @param WorldX, WorldY, WorldZ - coordonnées monde en voxels (pas cm) */ float GetDensityAt(float WorldX, float WorldY, float WorldZ, float* OutPreEditDensity = nullptr) const; /** * Open a worker-local lattice hand-off for the following GetDensityAt calls. The mesher uses * this around one tile's density-grid fill. The first sample in each resolved chunk lets the * generator run the operator stack once in EvalBlock; later samples consume the exact cached * per-sample result while the ordinary post-stack tail still runs in canonical order. * * This is intentionally a scoped service rather than a persistent world cache: its lifetime is * one tile task, it is thread-local, and EndDensityBlock must be called on every exit path. */ void BeginDensityBlock(FIntVector OriginVoxels, int32 Step, int32 SizeX, int32 SizeY, int32 SizeZ) const; void EndDensityBlock() const; #if WITH_EDITOR /** Editor-only chunk program; supported routes enter GetDensityAt once to seed a whole block. */ bool EvaluateDensityChunk(FIntVector OriginVoxels, int32 SizeX, int32 SizeY, int32 SizeZ, TArray& OutDensities, int32& OutFallbackSamples, uint64& OutGeneralPathEntries, uint64& OutSupportedBlockSetupEntries) const; #endif /** * Prove that the mesher's core lattice is entirely solid from the cheap, structural rules * that own inter-strate gaps and boundary seals. This never samples density. It is intended * for an empty-tile fast path; a false answer is harmless, while a true answer must make every * core lattice vertex negative in the final MC field. * * The proof is deliberately narrower than ClassifyTile: it accepts only a tile made entirely * of gap/seal lattice rows and rejects recipes/composer overrides plus every known air-writing * structural feature. The mesher's +/-1 normal halo is not part of the cell lattice, matching * ClassifyTile and GenerateMesh. */ bool TryProveSealedSolidTile(const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis) const; #if WITH_DEV_AUTOMATION_TESTS /** Test hook: disable named proof guards and require the brute-force oracle to catch it. */ bool TryProveSealedSolidTileForTest( const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis, uint8 DisabledGuards) const; #endif /** * Densité pour une strate TunnelNetwork (rooms + tunnels + worm noise). * C'est l'évaluateur fusionné de la pile TunnelNetwork/Underwater dans GetDensityAt. * Exposée pour permettre des tests isolés avec des params custom. * * ⚠️ `ParamsFingerprint` ET `LayoutVersion` SONT OBLIGATOIRES (`AUDIT §C2`). Le cache SDF * interne est clé sur (boîte XY, strate, seed) et PAS sur les params. Or `GetGenerationParams` BLENDE les params à l'intérieur d'une même * strate — `Alpha` dépend du chunk Z en mode `Gradient` (le DÉFAUT, avec * `TransitionBlendChunks = 2`) et du chunk XY en plus en mode `Interleaved`. Deux chunks de la * même strate, même seed, donc même clé, mais des params DIFFÉRENTS : le worker évalue le * deuxième chunk qu'il construit contre les salles du premier. Et comme *quel* chunk vient en * premier dépend de l'ordre des workers, **deux pairs divergent depuis la même seed** — ce que * `OPSTACK-PLAN §2.6.1` interdit explicitement. * * Pourquoi une empreinte PASSÉE plutôt qu'un `MemCrc32` calculé ici : ce serait ~300 octets de * CRC PAR VOXEL sur le chemin le plus chaud du plugin. L'appelant la calcule UNE fois par * chunk, là où le mémo de params vit déjà (`CP_*`), donc le coût par voxel est exactement deux * comparaisons d'entiers. Pas de valeur par défaut : un appelant qui oublie doit ne pas * compiler, pas hériter silencieusement du trou (la discipline de `FVoxelOpContext`). * * ⚠️ POUR LES TESTS : passez `FCrc::MemCrc32(&Params, sizeof(Params))`. Un oracle qui partage * le défaut qu'il teste ne prouve rien — c'est précisément ce que la note de * `VoxelForgeOpStackTunnelTest.cpp` (contrôle 3) décrivait comme le trou de l'original. * * The SDF cache key had neither the params nor anything that determines them, while the params * are blended per chunk INSIDE a strate — so a worker could evaluate one chunk against another * chunk's rooms, and which came first depends on worker order. Passing a once-per-chunk * fingerprint keeps the fix off the per-voxel path. No default: forgetting it must not compile. */ float GetDensityWithParams(float WorldX, float WorldY, float WorldZ, const FStrateGenerationParams& Params, uint32 ParamsFingerprint, uint32 LayoutVersion, bool bApplyLegacyStructuralPosts = true, // Optional hand-off used by a lowered canonical evaluator. When // non-null, this receives the pre-disturbance TunnelNetwork core; // it does not alter the scalar field result. FTunnelCoreWorldEvaluation* OutTunnelCore = nullptr, // Diagnostics are opt-in so ordinary/reference calls do not pay // for fused-path counters. bool bCollectFusedDiagnostics = false, // Optional same-sample hand-off from EvaluateSDFCached. This is // only a room-shape ownership bit; it does not alter the scalar // result and avoids a duplicate room-index query. bool* OutRoomOwnsBottom = nullptr) const; //========================================================================= // CLIMATE & BIOME FIELDS (pure XY, deterministic, window-invariant) //========================================================================= /** * Relief ("elevation") field at a world XY → [0,1], contrast-shaped & smoothed. * The single source of truth for the relief map M: SurfaceWorld terrain and the * biome climate map both call this, so they agree when given the same freq/contrast. */ float SampleRelief(float WorldX, float WorldY, float Frequency, float Contrast) const; /** * La chaîne de hauteur complète de SurfaceWorld : structural → cliff → terrace → layer lines → * plage. Rend une ALTITUDE monde en voxels, pas une densité. * * PUBLIQUE pour permettre un test isolé : c'est la référence de * `VoxelForge.OpStack.SurfaceHeightEquivalence`, qui compare la pile d'opérateurs de hauteur * (`VoxelHeightOp.h`) à cette fonction point par point. * Public so the height-op stack can be measured against it. */ float ComputeSurfaceTerrainZ(float WorldX, float WorldY, const FSurfaceGenerationParams& Params) const; /** * La colonne de surface : terrain Z, plafond, et le gate d'OVERHANG résolu par colonne * (amplitude + direction amont). C'est la colonne que `ClassifyTile` teste pour SurfaceWorld ; * publique pour que les tests puissent placer des échantillons dans la fenêtre d'overhang. * The surface column ClassifyTile tests for SurfaceWorld; public so tests can aim samples at * the overhang window. */ void ComputeSurfaceColumn(float WorldX, float WorldY, int32 ChunkZ, const FSurfaceGenerationParams& BaseSurface, const FBiomeContext& BiomeCtx, const TArray& BiomeParams, FChunkBiomeCache& BiomeCache, float& OutTerrainZ, float& OutCeilSurf, float& OutOverhangAmp, float& OutDirX, float& OutDirY) const; /** * Moisture field at a world XY → [0,1]. The second climate axis for biome placement. */ float SampleMoisture(float WorldX, float WorldY, float Frequency) const; /** * Resolve the biome at a world XY: dominant cell + nearest neighbour + blend weight. * Warped Voronoi over a jittered grid; each cell's biome is chosen by its site's * (relief, moisture) against the context's climate boxes. PURE function of (XY, seed, * Ctx) — no chunk-window dependence (CODEMAP §8.4). Returns an empty sample when the * context has no biomes. Used by the 2D preview bake and (next) the density path. */ FBiomeSample SampleBiomeAt(float WorldX, float WorldY, const FBiomeContext& Ctx) const; /** * Hot-path biome resolve: the dominant + neighbour biome and blend weight at a world * XY for the given strate slice. Uses (and lazily rebuilds) Cache — a box-validated * per-chunk grid — so the noise-heavy cell classification happens once per chunk, not * per voxel. Bit-identical to SampleBiomeAt, so the baked preview matches the terrain. */ FBiomeSample ResolveBiomeSampleAt(float WorldX, float WorldY, int32 ChunkZ, const FBiomeContext& Ctx, FChunkBiomeCache& Cache) const; /** * Dominant biome ASSET at a world XY for the given strate slice (chunk Z), or nullptr * when the strate has no biomes / the point is outside the strate range. Game-thread * helper for content + atmosphere selection — uses the same field as the density path. */ const UVoxelBiomeDefinition* GetDominantBiomeAt(float WorldX, float WorldY, int32 ChunkZ) const; /** * Rich biome probe at a world XY for a strate slice (ChunkZ): dominant + neighbour biome assets, * climate fields, border blend weight, and the dominant biome's EFFECTIVE decoration count. Mirrors * exactly what the decoration scatter resolves per column, so it is a faithful "what's under here?" * diagnostic (DominantDecorationCount == 0 explains an empty biome region). Game-thread, uncached. */ void QueryBiomeAt(float WorldX, float WorldY, int32 ChunkZ, FVoxelBiomeQuery& Out) const; /** * Per-vertex material data for the master triplanar palette material (F6). Resolves the * biome field at a world XY/Z and returns the dominant + neighbour MaterialPaletteIndex * and the border blend weight (0 deep in a cell → 0.5 at the border). The mesher packs * these into vertex colour so one material re-skins terrain per biome and cross-fades * across biome borders. No biomes ⇒ Dominant=Neighbour=0, Weight=0 (default palette). * Thread-safe: own per-chunk thread_local context + box cache (clustered tile queries * stay warm). Window-invariant (ResolveBiomeSampleAt is bit-identical to SampleBiomeAt). */ void GetBiomeMaterialAt(float WorldX, float WorldY, float WorldZ, int32& OutDominantPalette, int32& OutNeighborPalette, float& OutBlendWeight) const; /** * F7 AWARE PLACEMENT: evaluate an entry's relational placement conditions at a candidate voxel XY. * True = ALL conditions pass (AND); empty list = true (zero cost). Pure query of the analytic fields * (relief/moisture/biome-border) — deterministic + worker-safe. The caller passes the strate's * already-resolved biome context (freq/contrast + Voronoi map), so no re-resolve. Also the shared core * of the future quest FindFeature locator (same predicate, run as an outward search). */ bool EvaluateTerrainConditions(const TArray& Conditions, float WorldX, float WorldY, const FBiomeContext& BiomeCtx) const; /** * SurfaceWorld HEIGHT ORACLE: the terrain surface Z + sky-cap ceiling Z (voxel coords) at a world * XY for the given strate slice (ChunkZ), WITHOUT ray-marching the density column. Returns false * (outs untouched) when the chunk is NOT a SurfaceWorld heightfield — callers fall back to marching. * Shares the density path's surface helpers, so a decoration snapped to OutTerrainZ sits exactly on * the rendered ground. Does NOT include passage/spine/seal carving — verify with one GetDensityAt at * the result if a column might be carved. Thread-safe (own per-chunk thread_local cache). */ bool GetSurfaceHeightAt(float WorldX, float WorldY, int32 ChunkZ, float& OutTerrainZ, float& OutCeilSurf) const; /** * T1.d — classification conservative d'une tuile AVANT le pré-échantillonnage du mesher. * (OriginVoxels, Step, CellsPerAxis) = les MÊMES arguments que GenerateMesh ; le verdict * porte sur le treillis exact que le mesher échantillonnerait (marge ±1 incluse). * * GAP (bedrock), hors-layout (air constant) et SurfaceWorld plient leurs hypothèses par Z ; * SurfaceWorld évalue ses colonnes terrain/plafond avec le MÊME ComputeSurfaceColumn que le * chemin densité. Un unique slot cave peut ajouter le verdict conservatif de sa pile, * sous gardes d'archétype et de params bit-identiques. Spine/passages/disturbances/diff restent * des gardes conservatrices. Worker-safe (lecture seule + caches thread_local partagés avec * GetDensityAt — un verdict Mixed laisse les colonnes chaudes pour la génération qui suit). */ EVoxelTileClass ClassifyTile(const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis) const; EVoxelTileClass ClassifyTile(const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis, struct FVoxelTileClassificationStats* OutStats) const; /** * Prove the exact constant-air field above/below the strate layout for a mesher lattice. * This is separate from TryProveSealedSolidTile: it never supplies collision support and its * caller records AllAir, not AllSolid. It shares the layout/diff/structural guards with the * production classifier and returns false for every mixed or uncertain box. */ bool TryProveOutOfLayoutAirTile(const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis) const; private: /** Identité process-unique du propriétaire des caches `CP_*` thread_local. * Process-unique owner identity for the `CP_*` thread-local cache key. */ uint64 DensityCacheOwnerId = 0; EVoxelTileClass ClassifyTileUncached( const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis, struct FVoxelTileClassificationStats* OutStats) const; /** Pick the biome (index into Ctx.Biomes) for a Voronoi site, by its climate. */ int32 ClassifyBiomeAtSite(float SiteX, float SiteY, const FBiomeContext& Ctx, uint32 SiteHash) const; // ComputeSurfaceTerrainZ a été DÉPLACÉE en `public` (voir plus haut) pour que // VoxelForge.OpStack.SurfaceHeightEquivalence puisse s'y comparer. Une seule déclaration. // Moved to public above so the height-stack test can compare against it. One declaration only. /** F20 — the RAW structural heightfield (continents + mountains + detail), BEFORE any * terrain op (cliff/terrace/layer-lines/beach). Ops in ComputeSurfaceTerrainZ build on * this; Cliff re-samples it at an XY offset for a cheap analytic slope. OutM = the relief * "mountainous-ness" [0,1], reused to relief-condition the ops. */ float SampleSurfaceStructuralZ(float WorldX, float WorldY, const FSurfaceGenerationParams& Params, float& OutM) const; /** The SurfaceWorld sky-cap ceiling surface Z at a world XY (also pure per-XY). */ float ComputeSurfaceCeiling(float WorldX, float WorldY, const FSurfaceGenerationParams& Params) const; /** Resolve a chunk's SurfaceWorld params (strate base + per-biome overrides, structural fields * forced from the strate). Shared by GetDensityAt's per-chunk cache and the GetSurfaceHeightAt * oracle so both produce the SAME surface. */ void ResolveSurfaceChunkParams(const FIntVector& ChunkCoord, FSurfaceGenerationParams& OutSurface, FBiomeContext& OutBiomeCtx, TArray& OutBiomeParams) const; /** (Re)build the per-chunk biome cell grid covering chunk (X,Y) footprint + margin. */ void RebuildBiomeGrid(int32 ChunkX, int32 ChunkY, int32 ChunkZ, const FBiomeContext& Ctx, FChunkBiomeCache& Cache, uint64 OwnerId, uint64 ManagerLifetimeId) const; }; namespace VoxelGenLOD { // -1 uses the measured path/LOD policy; 0/1 force the spatial broad phase off/on for an A/B // run. The policy affects candidate enumeration only, never cache contents or ordering. VOXELFORGE_API bool ShouldUseSpatialIndex(bool bFusedPath); } /** * Optional diagnostics for one ClassifyTile call. This is populated only when the caller asks * for it (the streaming profiler does); it is never consulted by the proof and cannot affect a * verdict. Keeping the counters beside the classifier lets the game profile distinguish cache, * interval refinement, and exact-lattice work without sampling a different code path. */ struct FVoxelTileClassificationStats { uint32 RefineNodes = 0; uint32 StackBoxCalls = 0; uint32 WholeMixedNodes = 0; uint32 WholeSolidNodes = 0; uint32 WholeAirNodes = 0; uint32 NeedsFinalFieldNodes = 0; uint32 SplitNodes = 0; uint32 MaxRefinementDepth = 0; uint32 ExactCoreSamples = 0; uint32 ExactFinalSamples = 0; uint32 ExactCoreIntervalChecks = 0; uint32 ExactCoreIntervalDisagreements = 0; uint32 ExactCoreCacheHits = 0; uint32 ExactFinalCacheHits = 0; uint32 ExactCoreLeaves = 0; uint32 ExactFinalLeaves = 0; uint64 StackBoxCycles = 0; uint64 ExactCoreCycles = 0; uint64 ExactFinalCycles = 0; uint32 RoomTailQueries = 0; uint32 RoomTailEvaluated = 0; uint64 RoomTailCycles = 0; uint64 RoomPropagateCycles = 0; uint64 RoomExactPrimitiveCycles = 0; uint64 RoomCacheWindowCycles = 0; int32 RoomNumRooms = 0; int32 RoomNumTunnels = 0; int32 RoomNumRoomFloorJoins = 0; int32 RoomNumPits = 0; int32 RoomNumChimneys = 0; };