// VoxelMarchingCubesMesher.cpp // Implémentation du marching cubes density-only. #include "VoxelMarchingCubesMesher.h" #include "MarchingCubesTables.h" #include "VoxelDensityProfile.h" #include "VoxelHybridSharpMesher.h" #if WITH_EDITOR #include "VoxelRedefinedDensityField.h" #endif #include "VoxelStrateManager.h" #include "VoxelTilePostReach.h" #include "VoxelPassageGeometry.h" #include "ProfilingDebugging/CpuProfilerTrace.h" #include "HAL/IConsoleManager.h" #include "Misc/CommandLine.h" #include namespace { int32 GVoxelForgeUseOperatorBlock = 1; FAutoConsoleVariableRef CVarVoxelForgeUseOperatorBlock( TEXT("voxel.UseOperatorBlock"), GVoxelForgeUseOperatorBlock, TEXT("Use the operator-major density block path for non-shared mesher grids.")); bool GVoxelForgeBlockSwitchParsed = false; int32 GVoxelForgeTilePostReachDebug = 0; FAutoConsoleVariableRef CVarVoxelForgeTilePostReachDebug( TEXT("voxel.TilePostReachDebug"), GVoxelForgeTilePostReachDebug, TEXT("Run skipped landing/structural posts in a bypass and compare their exact result; " "a nonzero difference fails the debug run.")); float GVoxelForgeTilePostReachScale = 1.0f; FAutoConsoleVariableRef CVarVoxelForgeTilePostReachScale( TEXT("voxel.TilePostReachScale"), GVoxelForgeTilePostReachScale, TEXT("Scale the per-tile post reach for deliberate soundness tests. Default 1.0 is exact; " "values below 1 intentionally shrink the guard.")); int32 GVoxelForgeTileReachCostDiagnostics = 0; FAutoConsoleVariableRef CVarVoxelForgeTileReachCostDiagnostics( TEXT("voxel.TileReachCostDiagnostics"), GVoxelForgeTileReachCostDiagnostics, TEXT("Measure core/passage density cost split by tile reach; no field gate.")); bool GVoxelForgeTilePostReachSwitchesParsed = false; void VF_ParseTilePostReachSwitches() { if (GVoxelForgeTilePostReachSwitchesParsed) { return; } GVoxelForgeTilePostReachSwitchesParsed = true; int32 DebugValue = GVoxelForgeTilePostReachDebug; float ScaleValue = GVoxelForgeTilePostReachScale; int32 CostDiagnosticsValue = GVoxelForgeTileReachCostDiagnostics; FParse::Value( FCommandLine::Get(), TEXT("voxel.TilePostReachDebug="), DebugValue); FParse::Value( FCommandLine::Get(), TEXT("voxel.TilePostReachScale="), ScaleValue); FParse::Value( FCommandLine::Get(), TEXT("voxel.TileReachCostDiagnostics="), CostDiagnosticsValue); GVoxelForgeTilePostReachDebug = DebugValue != 0 ? 1 : 0; if (!VoxelMath::IsFinite(ScaleValue) || ScaleValue <= 0.0f) { ScaleValue = 1.0f; } GVoxelForgeTilePostReachScale = FMath::Min(ScaleValue, 1.0f); GVoxelForgeTileReachCostDiagnostics = CostDiagnosticsValue != 0 ? 1 : 0; VoxelGenLOD::GTilePostReachDebugEnabled.store( GVoxelForgeTilePostReachDebug != 0, std::memory_order_relaxed); VoxelGenLOD::GTileReachCostDiagnosticsEnabled.store( GVoxelForgeTileReachCostDiagnostics != 0, std::memory_order_relaxed); } void VF_ParseOperatorBlockSwitch() { if (GVoxelForgeBlockSwitchParsed) { return; } GVoxelForgeBlockSwitchParsed = true; int32 CommandLineValue = GVoxelForgeUseOperatorBlock; if (FParse::Value(FCommandLine::Get(), TEXT("voxel.UseOperatorBlock="), CommandLineValue)) { GVoxelForgeUseOperatorBlock = CommandLineValue; } } } void UVoxelMarchingCubesMesher::InitializeConsoleSwitches() { VF_ParseOperatorBlockSwitch(); VF_ParseTilePostReachSwitches(); } //============================================================================= // MAIN ALGORITHM //============================================================================= // La grille pré-échantillonnée fournit positions ET gradients. FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, int32 Step, int32 InCellsPerAxis, TArray* OutCaptureGrid, int32 BandZMinVox, int32 BandZMaxVox, int64* OutDensitySampleCount, FVoxelSharedDensityGrid* OutPreEditDensityGrid, const FVoxelSharedDensityGrid* InSharedDensityGrid) { TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_MesherGenerateMesh); FVoxelMeshData MeshData; if (OutCaptureGrid) { OutCaptureGrid->Reset(); } if (OutPreEditDensityGrid) { OutPreEditDensityGrid->Reset(); } if (OutDensitySampleCount) { *OutDensitySampleCount = 0; } if (!Generator) return MeshData; VoxelDensityProfile::FScopedTimer MesherProfileTimer( VoxelDensityProfile::EBucket::MesherGenerateMesh); VoxelDensityProfile::FScopedTimer MesherSetupTimer( VoxelDensityProfile::EBucket::MesherOther); // Cell size in voxels. No upper clamp: coarse clipmap levels use bigger steps (the EXTENT // grows). Coarse tiles also use FEWER cells (InCellsPerAxis) for cheaper gen. Step = FMath::Max(1, Step); const float GeometryVoxelSize = VoxelMath::IsFinite(GeometryVoxelSizeCm) ? FMath::Max(1.0f, GeometryVoxelSizeCm) : VOXEL_SIZE; // T2.b — octave bias for THIS tile: drop LODOctaveDrop octaves per Step doubling from // the generator's per-voxel volumetric noise (sub-cell octaves can't shape a coarse // isosurface). TGuardValue restores 0 on every exit path, so nothing outside this tile // (deco snapping, density-volume fill, the next task on this pooled thread) sees a bias. const int32 OctaveBias = (LODOctaveDrop > 0 && Step > 1) ? LODOctaveDrop * (int32)FMath::FloorLog2((uint32)Step) : 0; TGuardValue OctaveBiasGuard(VoxelGenLOD::OctaveBias, OctaveBias); TGuardValue SampleStepGuard(VoxelGenLOD::SampleStep, Step); // World-cm origin of the tile's min corner (positions are built relative to this). const FVector ChunkWorldPos = FVector(OriginVoxels) * GeometryVoxelSize; //========================================================================= // VERTEX DEDUPLICATION MAP //========================================================================= // Clé = position quantifiée au 0.01 unité (FIntVector). // Valeur = index dans MeshData.Vertices. // Les vertices partagés permettent des normales lisses et réduisent le count ~3x. // Réutilisé d'une tuile à l'autre (thread_local) : Reset garde les buckets alloués → // plus de (ré)allocation de hash-map par tuile (chaque worker a son propre exemplaire). static thread_local TMap VertexMap; VertexMap.Reset(); static thread_local TMap> SplitNormalVertexMap; SplitNormalVertexMap.Reset(); //========================================================================= // F17 — CLASSE DE SURFACE (sol vs plafond sky-cap), par vertex → par triangle //========================================================================= // Le discriminant est SÉMANTIQUE, pas géométrique (fable-idea F17) : un vertex orienté vers // le bas est un sky-cap seulement s'il est proche de CeilSurf ; proche de TerrainZ c'est un // surplomb de terrain (reste "sol" — un vote par tuile mettrait le matériau ciel sous les // surplombs des tuiles majoritairement plafond). Un futur toit de grotte (aussi down- // facing, mais SOUS TerrainZ) tombera correctement côté "sol/roche" par la même règle. // Coût : GetSurfaceHeightAt (pile XY complète) UNIQUEMENT pour les vertex down-facing, // mémoïsé par colonne quantifiée au pas de la grille → ≤ (colonnes touchées) appels ; // ~zéro sur une tuile de sol pur, borné par lattice² sur une tuile de cap. static thread_local TArray VertexClasses; // 0 = sol, 1 = sky-cap static thread_local TMap SurfColMemo; // (Xq,Yq,chunkZ) → (TerrainZ, CeilSurf) ; X=FLT_MAX ⇒ pas SurfaceWorld VertexClasses.Reset(); SurfColMemo.Reset(); // Normale fournie par l'appelant (gradient lu dans la grille de densité, T1.b) — // plus d'échantillonnage de densité par vertex. RawNormal pointe solide→air ; on la // normalise ici (fallback up si dégénérée). auto GetOrCreateVertex = [&](const FVector& WorldPos, const FVector& RawNormal, uint64 NormalSplitKey = 0) -> int32 { const FIntVector Key( FMath::RoundToInt(WorldPos.X * 100.0f), FMath::RoundToInt(WorldPos.Y * 100.0f), FMath::RoundToInt(WorldPos.Z * 100.0f) ); if (NormalSplitKey != 0) { if (TMap* SplitMap = SplitNormalVertexMap.Find(Key)) { if (int32* Existing = SplitMap->Find(NormalSplitKey)) return *Existing; } } else if (int32* Existing = VertexMap.Find(Key)) { return *Existing; } const int32 NewIndex = MeshData.Vertices.Num(); if (NormalSplitKey != 0) { SplitNormalVertexMap.FindOrAdd(Key).Add(NormalSplitKey, NewIndex); } else { VertexMap.Add(Key, NewIndex); } MeshData.Vertices.Add(WorldPos); FVector Normal = RawNormal; if (!Normal.Normalize()) { Normal = FVector(0.0f, 0.0f, 1.0f); // dégénéré (zone plate) } MeshData.Normals.Add(Normal); // UVs planaires — le triplanar mapping se fait dans le matériau. MeshData.UVs.Add(FVector2D(WorldPos.X / VOXEL_SIZE, WorldPos.Y / VOXEL_SIZE)); //--------------------------------------------------------------------- // VERTEX COLOUR — masques pour le matériau triplanar maître (F6). // R = index de palette du biome DOMINANT (0-255) — re-skin par biome. // G = pente (0 = sol/plafond plat, 1 = paroi verticale) — roche sur falaises. // B = poids de fondu de bordure (0 au cœur d'un biome → ~0.5 à la frontière). // A = index de palette du biome VOISIN — le matériau lerp(R,A) par B → bords sans couture. // La hauteur/snow-line se déduit de WorldPosition.Z dans le matériau (pas besoin de canal). // Coût: une résolution biome par vertex UNIQUE (déduplication) ; nul si la strate n'a pas // de biomes (GetBiomeMaterialAt sort en O(1) → palette 0 partout). int32 PalD = 0, PalN = 0; float BlendW = 0.0f; Generator->GetBiomeMaterialAt(WorldPos.X / VOXEL_SIZE, WorldPos.Y / VOXEL_SIZE, WorldPos.Z / VOXEL_SIZE, PalD, PalN, BlendW); const uint8 R = (uint8)FMath::Clamp(PalD, 0, 255); const uint8 A = (uint8)FMath::Clamp(PalN, 0, 255); const uint8 G = (uint8)FMath::Clamp(FMath::RoundToInt((1.0f - FMath::Abs((float)Normal.Z)) * 255.0f), 0, 255); const uint8 Bc = (uint8)FMath::Clamp(FMath::RoundToInt(BlendW * 255.0f), 0, 255); MeshData.Colors.Add(FColor(R, G, Bc, A)); // F17 — classe de surface (voir le bloc en tête de fonction). Down-facing seulement : // les vertex de sol/falaise (N.Z ≥ -0.1) sont "sol" sans payer la requête colonne. uint8 SurfClass = 0; if (Normal.Z < -0.1f) { const float Zv = WorldPos.Z / VOXEL_SIZE; const float StepF = (float)Step; const FIntVector MemoKey( FMath::RoundToInt(WorldPos.X / (VOXEL_SIZE * StepF)), FMath::RoundToInt(WorldPos.Y / (VOXEL_SIZE * StepF)), FMath::FloorToInt(Zv / (float)CHUNK_SIZE)); FVector2f* Col = SurfColMemo.Find(MemoKey); if (!Col) { float TerrainZ = 0.0f, CeilSurf = 0.0f; // Un vertex du cap vit à la FRONTIÈRE HAUTE de la strate : arrondi/interpolation // peuvent le faire flotter dans le chunk gap/seal juste AU-DESSUS (hors // SurfaceWorld ⇒ sonde ratée ⇒ faux "sol"). On retente un chunk plus bas. const bool bSurf = Generator->GetSurfaceHeightAt( (float)(MemoKey.X * Step), (float)(MemoKey.Y * Step), MemoKey.Z, TerrainZ, CeilSurf) || Generator->GetSurfaceHeightAt( (float)(MemoKey.X * Step), (float)(MemoKey.Y * Step), MemoKey.Z - 1, TerrainZ, CeilSurf); Col = &SurfColMemo.Add(MemoKey, bSurf ? FVector2f(TerrainZ, CeilSurf) : FVector2f(FLT_MAX, -FLT_MAX)); } // Plus proche du plafond que du terrain ⇒ sky-cap. (Hors SurfaceWorld : sol.) // + garde : un vrai vertex de cap est AU niveau du cap (± slop d'interpolation) ; // loin AU-DESSUS = plancher/toit de la strate d'à côté atteint via le retry chunkZ-1 // (strates contiguës) → reste sol. if (Col->X != FLT_MAX && FMath::Abs(Zv - Col->Y) < FMath::Abs(Zv - Col->X) && Zv <= Col->Y + 2.0f * StepF) { SurfClass = 1; } } VertexClasses.Add(SurfClass); return NewIndex; }; //========================================================================= // CORNER + EDGE TABLES //========================================================================= // 4-------5 // /| /| // / | / | // 7-------6 | // | 0----|--1 // | / | / // |/ |/ // 3-------2 static const FIntVector CornerOffsets[8] = { {0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}, {0, 0, 1}, {1, 0, 1}, {1, 1, 1}, {0, 1, 1}, }; static const int32 EdgeCorners[12][2] = { {0, 1}, {1, 2}, {2, 3}, {3, 0}, // Bas (0-3) {4, 5}, {5, 6}, {6, 7}, {7, 4}, // Haut (4-7) {0, 4}, {1, 5}, {2, 6}, {3, 7}, // Verticales (8-11) }; //========================================================================= // PRÉ-CALCUL DE LA GRILLE DE DENSITÉ //========================================================================= // Les cellules adjacentes partagent leurs coins : en échantillonnant par // cellule (8 coins) on appelle GetDensityAt ~8× de trop pour chaque point. // On échantillonne donc chaque point de grille UNE SEULE FOIS dans un tableau // plat, puis le balayage des cellules y lit ses coins. GetDensityAt est une // fonction pure de la coordonnée monde, donc le maillage est identique au bit // près — c'est juste ~7× moins d'appels au LOD0 (33³ au lieu de 32³×8). // // CellsPerAxis = nombre de cellules par axe ; GridDim = points de grille (+1). // Le point de grille (gx,gy,gz) correspond au voxel monde (gx,gy,gz)*Step. // Ordre de remplissage z→y→x : garde le cache SDF (search-box) bien chaud. // On échantillonne avec un anneau de marge de 1 point de chaque côté (indices -1..GridDim) // pour pouvoir calculer les normales par GRADIENT DE GRILLE (T1.b) — différences centrales // sur la grille au lieu de 6 appels densité frais par vertex. La marge utilise les mêmes // échantillons monde purs qu'un chunk voisin, donc les normales restent continues aux bords // de chunk (pas de couture de shading). La géométrie est inchangée au bit près (mêmes // positions d'arête) ; seules les normales changent. const int32 CellsPerAxis = FMath::Clamp(InCellsPerAxis, 2, CHUNK_SIZE); // coarse tiles use fewer const int32 GridDim = CellsPerAxis + 1; const int32 MDim = GridDim + 2; // +1 marge de chaque côté TGuardValue TileOriginGuard( VoxelGenLOD::TileOriginVoxels, OriginVoxels); TGuardValue TileCellsGuard( VoxelGenLOD::TileCellsPerAxis, CellsPerAxis); // Every density sample in this tile sees the same post-reach decision. The one-point halo // matches the actual grid fill below (gx/gy/gz = -1..GridDim), so a false bit proves that no // landing or passage structural writer can reach any sample used by meshing or gradients. TGuardValue TilePostReachDebugGuard( VoxelGenLOD::bTilePostReachDebug, GVoxelForgeTilePostReachDebug != 0); TGuardValue TilePostReachBypassGuard( VoxelGenLOD::bTilePostReachBypass, false); TGuardValue TileReachDiagnosticGuard( VoxelGenLOD::bTileReachDiagnosticTile, true); TGuardValue TileCoreReachProofGuard( VoxelGenLOD::bTileCoreReachProofEnabled, false); TGuardValue TileCoreReachDecisionGuard( VoxelGenLOD::bTileCoreReachDecisionRecorded, false); TGuardValue TileReachScaleGuard( VoxelGenLOD::TileReachScale, GVoxelForgeTilePostReachScale); const float ReachScale = GVoxelForgeTilePostReachScale; const int32 ReachBlocksPerAxis = FMath::DivideAndRoundUp( CellsPerAxis, VoxelGenLOD::TileReachBlockCells); TGuardValue TileReachBlocksGuard( VoxelGenLOD::TileReachBlocksPerAxis, ReachBlocksPerAxis); TGuardValue TileReachBlockIndexGuard( VoxelGenLOD::TileReachBlockIndex, INDEX_NONE); TGuardValue TileBlockReachValidGuard( VoxelGenLOD::bTileBlockReachValid, false); uint8 TilePostReachFlags = VoxelGenLOD::AllTilePostReach; if (Generator->StrateManager != nullptr) { const int64 Extent = static_cast(CellsPerAxis + 1) * Step; const FBox LatticeWithHalo( FVector( static_cast(static_cast(OriginVoxels.X) - Step), static_cast(static_cast(OriginVoxels.Y) - Step), static_cast(static_cast(OriginVoxels.Z) - Step)), FVector( static_cast(static_cast(OriginVoxels.X) + Extent), static_cast(static_cast(OriginVoxels.Y) + Extent), static_cast(static_cast(OriginVoxels.Z) + Extent))); // Core stays conservative until the tile-window tunnel cache is built. Passage carving // is manager-owned and can be decided immediately from the same lattice+halo box. TilePostReachFlags = VoxelGenLOD::TunnelCoreReachable; if (Generator->StrateManager->AnyOriginLandingNearLattice( LatticeWithHalo, OriginVoxels, Step, ReachScale)) { TilePostReachFlags |= VoxelGenLOD::OriginLandingReachable; } if (Generator->StrateManager->AnyPassageLandingNearLattice( LatticeWithHalo, OriginVoxels, Step, ReachScale)) { TilePostReachFlags |= VoxelGenLOD::PassageLandingReachable; } if (Generator->StrateManager->AnyPassageCarvingNearLattice( LatticeWithHalo, OriginVoxels, Step, ReachScale)) { TilePostReachFlags |= VoxelGenLOD::PassageCarvingReachable; } if (Generator->StrateManager->AnyPassageStructuralPostNearLattice( LatticeWithHalo, OriginVoxels, Step, ReachScale)) { TilePostReachFlags |= VoxelGenLOD::PassageStructuralReachable; } } TGuardValue TilePostReachFlagsGuard( VoxelGenLOD::TilePostReachFlags, TilePostReachFlags); for (int32 BlockZ = 0; BlockZ < ReachBlocksPerAxis; ++BlockZ) { for (int32 BlockY = 0; BlockY < ReachBlocksPerAxis; ++BlockY) { for (int32 BlockX = 0; BlockX < ReachBlocksPerAxis; ++BlockX) { const int32 BlockIndex = ((BlockZ * ReachBlocksPerAxis) + BlockY) * ReachBlocksPerAxis + BlockX; uint8 BlockFlags = TilePostReachFlags; if (Generator->StrateManager != nullptr && (TilePostReachFlags & VoxelGenLOD::PassageCarvingReachable) != 0 && !Generator->StrateManager->AnyPassageCarvingNearLattice( VoxelGenLOD::MakeTileReachBlockBox( OriginVoxels, Step, CellsPerAxis, BlockX, BlockY, BlockZ), OriginVoxels, Step, ReachScale)) { BlockFlags &= ~VoxelGenLOD::PassageCarvingReachable; } VoxelGenLOD::TileBlockReachFlags[BlockIndex] = BlockFlags; } } } VoxelGenLOD::bTileBlockReachValid = true; VoxelGenLOD::RecordTileReachMask(TilePostReachFlags); // COUPE DE CONTENU PAR STRATE — restreint le maillage (et l'échantillonnage) aux cellules // dont l'intervalle Z chevauche la bande [BandZMinVox, BandZMaxVox] (voxels inclusifs). // Les tuiles à capture ne sont jamais bandées (niveau 0 — garde-fou ci-dessous). if (OutCaptureGrid || OutPreEditDensityGrid) { BandZMinVox = INT32_MIN; BandZMaxVox = INT32_MAX; } int32 CzLo = 0, CzHi = CellsPerAxis - 1; if (BandZMinVox > INT32_MIN || BandZMaxVox < INT32_MAX) { auto FloorDivI = [](int64 A, int64 B) -> int32 { const int64 Q = A / B; return (int32)(Q - (((A % B) != 0 && ((A < 0) != (B < 0))) ? 1 : 0)); }; // Cellule c couvre [O.Z + c*Step, O.Z + (c+1)*Step] : première/dernière cellule contenant // la borne (un contact purement tangent est exclu — sa traversée serait hors bande). CzLo = FMath::Max(CzLo, FloorDivI((int64)BandZMinVox - OriginVoxels.Z, Step)); CzHi = FMath::Min(CzHi, FloorDivI((int64)BandZMaxVox - OriginVoxels.Z, Step)); if (CzHi < CzLo) { return MeshData; } // tuile entièrement hors bande → vide } const int32 GzLo = CzLo - 1; // coins CzLo..CzHi+1, gradients ±1 const int32 GzHi = CzHi + 2; // (== -1..GridDim sans bande) MesherSetupTimer.End(); // Optional 8x8x8-cell block proof. The tile classifier has already returned Mixed, but // that is a conservative answer over the whole tile. A tile can still be uniformly solid // (or uniformly air) when its smaller blocks are each provable. Do this before allocating // or filling the density grid: the grid is only needed when at least one block can contain // a surface. A whole-tile early return is allowed only when every relevant block has the // SAME verdict; adjacent uniform blocks with opposite signs can contain a surface between // them. This is a proof-only optimization: ClassifyTile is conservative and no samples or // resolution are removed from a tile that reaches the mesher. TArray BlockSkip; int32 BlocksPerAxis = 0; // LOD2's 16-cell tile is the remaining false-Mixed empty case in the streamed game path. // Its step-8 lattice can use the same proof-only 8-cell block partition as LOD0. Keep the // gate off for steps 2/16/32: their meshing work is already smaller than repeating a full // tile classifier per block, and this preserves the established coarse-LOD latency profile. const bool bUseProofBlocks = bUseBlockEarlyOut && !OutCaptureGrid && (Step == 1 || Step == 8); if (bUseProofBlocks) { constexpr int32 BlockCells = 8; BlocksPerAxis = (CellsPerAxis + BlockCells - 1) / BlockCells; BlockSkip.Init(0, BlocksPerAxis * BlocksPerAxis * BlocksPerAxis); bool bHaveUniformBlock = false; EVoxelTileClass FirstUniformVerdict = EVoxelTileClass::Mixed; bool bAllRelevantBlocksUniform = true; bool bAllRelevantBlocksSame = true; TArray MixedBlockIndices; for (int32 BlockZ = 0; BlockZ < BlocksPerAxis; ++BlockZ) { const int32 BlockMinZ = BlockZ * BlockCells; const int32 BlockMaxZ = FMath::Min(CellsPerAxis - 1, BlockMinZ + BlockCells - 1); if (BlockMaxZ < CzLo || BlockMinZ > CzHi) { continue; } for (int32 BlockY = 0; BlockY < BlocksPerAxis; ++BlockY) { for (int32 BlockX = 0; BlockX < BlocksPerAxis; ++BlockX) { const FIntVector BlockOrigin = OriginVoxels + FIntVector( BlockX * BlockCells * Step, BlockY * BlockCells * Step, BlockMinZ * Step); if (VoxelDensityProfile::AreCountersEnabled()) { VoxelDensityProfile::AddCounter( VoxelDensityProfile::ECounter::MesherBlockTests); } const bool bAttributeClassifier = VoxelDensityProfile::GetMode() == VoxelDensityProfile::EMode::Attribution; const bool bSampleClassifier = bAttributeClassifier && VoxelDensityProfile::ShouldSample( VoxelDensityProfile::EBucket::ClassifierTotal); const uint64 ClassifierStartCycles = bSampleClassifier ? FPlatformTime::Cycles64() : 0; FVoxelTileClassificationStats ClassifierStats; if (bAttributeClassifier) { VoxelDensityProfile::RecordClassifierCall(bSampleClassifier); } const EVoxelTileClass Verdict = Generator->ClassifyTile( BlockOrigin, Step, BlockCells, bSampleClassifier ? &ClassifierStats : nullptr); if (bSampleClassifier) { VoxelDensityProfile::AddSampledMeasurement( VoxelDensityProfile::EBucket::ClassifierTotal, FPlatformTime::Cycles64() - ClassifierStartCycles); VoxelDensityProfile::FSnapshot::FClassifierStats ProfileStats; ProfileStats.RefineNodes = ClassifierStats.RefineNodes; ProfileStats.StackBoxCalls = ClassifierStats.StackBoxCalls; ProfileStats.WholeMixedNodes = ClassifierStats.WholeMixedNodes; ProfileStats.WholeSolidNodes = ClassifierStats.WholeSolidNodes; ProfileStats.WholeAirNodes = ClassifierStats.WholeAirNodes; ProfileStats.NeedsFinalFieldNodes = ClassifierStats.NeedsFinalFieldNodes; ProfileStats.SplitNodes = ClassifierStats.SplitNodes; ProfileStats.MaxRefinementDepth = ClassifierStats.MaxRefinementDepth; ProfileStats.ExactCoreSamples = ClassifierStats.ExactCoreSamples; ProfileStats.ExactFinalSamples = ClassifierStats.ExactFinalSamples; ProfileStats.ExactCoreCacheHits = ClassifierStats.ExactCoreCacheHits; ProfileStats.ExactFinalCacheHits = ClassifierStats.ExactFinalCacheHits; ProfileStats.ExactCoreLeaves = ClassifierStats.ExactCoreLeaves; ProfileStats.ExactFinalLeaves = ClassifierStats.ExactFinalLeaves; ProfileStats.StackBoxCycles = ClassifierStats.StackBoxCycles; ProfileStats.ExactCoreCycles = ClassifierStats.ExactCoreCycles; ProfileStats.ExactFinalCycles = ClassifierStats.ExactFinalCycles; ProfileStats.RoomTailQueries = ClassifierStats.RoomTailQueries; ProfileStats.RoomTailEvaluated = ClassifierStats.RoomTailEvaluated; ProfileStats.RoomTailCycles = ClassifierStats.RoomTailCycles; ProfileStats.RoomPropagateCycles = ClassifierStats.RoomPropagateCycles; ProfileStats.RoomExactPrimitiveCycles = ClassifierStats.RoomExactPrimitiveCycles; ProfileStats.RoomCacheWindowCycles = ClassifierStats.RoomCacheWindowCycles; ProfileStats.RoomNumRooms = FMath::Max(ClassifierStats.RoomNumRooms, 0); ProfileStats.RoomNumTunnels = FMath::Max(ClassifierStats.RoomNumTunnels, 0); ProfileStats.RoomNumRoomFloorJoins = FMath::Max( ClassifierStats.RoomNumRoomFloorJoins, 0); ProfileStats.RoomNumPits = FMath::Max(ClassifierStats.RoomNumPits, 0); ProfileStats.RoomNumChimneys = FMath::Max(ClassifierStats.RoomNumChimneys, 0); VoxelDensityProfile::AddClassifierStats(ProfileStats); if (ClassifierStats.StackBoxCalls > 0) { VoxelDensityProfile::AddMeasurement( VoxelDensityProfile::EBucket::ClassifierIntervalProof, 0, ClassifierStats.StackBoxCalls); VoxelDensityProfile::AddSampledMeasurement( VoxelDensityProfile::EBucket::ClassifierIntervalProof, ClassifierStats.StackBoxCycles); } if (ClassifierStats.ExactCoreLeaves > 0) { VoxelDensityProfile::AddMeasurement( VoxelDensityProfile::EBucket::ClassifierExactCore, 0, ClassifierStats.ExactCoreLeaves); VoxelDensityProfile::AddSampledMeasurement( VoxelDensityProfile::EBucket::ClassifierExactCore, ClassifierStats.ExactCoreCycles); } if (ClassifierStats.ExactFinalLeaves > 0) { VoxelDensityProfile::AddMeasurement( VoxelDensityProfile::EBucket::ClassifierExactFinal, 0, ClassifierStats.ExactFinalLeaves); VoxelDensityProfile::AddSampledMeasurement( VoxelDensityProfile::EBucket::ClassifierExactFinal, ClassifierStats.ExactFinalCycles); } if (ClassifierStats.RoomTailQueries > 0) { VoxelDensityProfile::AddMeasurement( VoxelDensityProfile::EBucket::ClassifierRoomTail, 0, ClassifierStats.RoomTailQueries); VoxelDensityProfile::AddSampledMeasurement( VoxelDensityProfile::EBucket::ClassifierRoomTail, ClassifierStats.RoomTailCycles); } } if (Verdict == EVoxelTileClass::Mixed) { MixedBlockIndices.Add( ((BlockZ * BlocksPerAxis) + BlockY) * BlocksPerAxis + BlockX); } if (Verdict == EVoxelTileClass::AllSolid || Verdict == EVoxelTileClass::AllAir) { // Keep the classifier result as a candidate until the exact density grid // used by this mesher has been filled. ClassifyTile is conservative by // contract, but its proof and the mesher share worker-local caches; a // cache replacement must never turn that contract into a cell skip by // itself. The post-grid sign check below is the final proof consumed by // the cell loop and clears any candidate whose actual MC vertices disagree. BlockSkip[((BlockZ * BlocksPerAxis) + BlockY) * BlocksPerAxis + BlockX] = Verdict == EVoxelTileClass::AllSolid ? 1 : 2; if (!bHaveUniformBlock) { FirstUniformVerdict = Verdict; bHaveUniformBlock = true; } else if (FirstUniformVerdict != Verdict) { bAllRelevantBlocksSame = false; } } else { bAllRelevantBlocksUniform = false; } } } } // A conservative whole-tile proof can leave a small block unresolved even when every // other block has the same verdict. Resolve that tail on the exact MC lattice. This uses // the final GetDensityAt field, checks every cell corner, treats zero/non-finite values // as unknown, and never changes the grid resolution or skips an uncertified block. if (bHaveUniformBlock && bAllRelevantBlocksSame && !bAllRelevantBlocksUniform && MixedBlockIndices.Num() <= 4) { int32 RemainingMixedBlocks = MixedBlockIndices.Num(); for (const int32 MixedBlockIndex : MixedBlockIndices) { const int32 BlockX = MixedBlockIndex % BlocksPerAxis; const int32 BlockY = (MixedBlockIndex / BlocksPerAxis) % BlocksPerAxis; const int32 BlockZ = MixedBlockIndex / (BlocksPerAxis * BlocksPerAxis); const FIntVector BlockOrigin = OriginVoxels + FIntVector( BlockX * BlockCells * Step, BlockY * BlockCells * Step, BlockZ * BlockCells * Step); bool bAllSolid = true; bool bAllAir = true; for (int32 Z = 0; Z <= BlockCells && (bAllSolid || bAllAir); ++Z) { for (int32 Y = 0; Y <= BlockCells && (bAllSolid || bAllAir); ++Y) { for (int32 X = 0; X <= BlockCells; ++X) { const float Density = Generator->GetDensityAt( BlockOrigin.X + X * Step, BlockOrigin.Y + Y * Step, BlockOrigin.Z + Z * Step); if (!VoxelMath::IsFinite(Density) || Density == 0.0f) { bAllSolid = false; bAllAir = false; break; } bAllSolid &= Density < 0.0f; bAllAir &= Density > 0.0f; if (!bAllSolid && !bAllAir) { break; } } } } const EVoxelTileClass ExactVerdict = bAllSolid ? EVoxelTileClass::AllSolid : (bAllAir ? EVoxelTileClass::AllAir : EVoxelTileClass::Mixed); if (ExactVerdict == EVoxelTileClass::AllSolid || ExactVerdict == EVoxelTileClass::AllAir) { BlockSkip[MixedBlockIndex] = ExactVerdict == EVoxelTileClass::AllSolid ? 1 : 2; --RemainingMixedBlocks; if (ExactVerdict != FirstUniformVerdict) { bAllRelevantBlocksSame = false; } } } bAllRelevantBlocksUniform = RemainingMixedBlocks == 0; } // Every relevant block has independently proved the same sign. This is a complete // certificate for the tile's MC lattice: the block boxes share their boundary vertices, // so equal signs on both sides cannot hide a surface between blocks. The whole-tile // ClassifyTile path already relies on this same non-Mixed proof contract. Return before // allocating the density grid; no samples are skipped for a tile that can contain // geometry, and the final geometry remains byte-identical because this path emits none. if (bHaveUniformBlock && bAllRelevantBlocksSame && bAllRelevantBlocksUniform) { return MeshData; } // A mixed or differently signed block set still needs the exact mesher grid. Classifier // results remain candidates only in that case; the grid validation below keeps the cell // skip independent of worker-local cache state and preserves geometry byte-for-byte. } // Réutilise le tampon entre tuiles (thread_local) : SetNumUninitialized garde la // capacité, donc plus de malloc/free de ~170 Ko (35³ floats) par tuile. static thread_local TArray DensityGrid; DensityGrid.SetNumUninitialized(MDim * MDim * MDim); const FVoxelSharedDensityGrid* ReuseGrid = InSharedDensityGrid != nullptr ? InSharedDensityGrid : SharedDensityGrid; const int32 SharedDeltaX = ReuseGrid ? (OriginVoxels.X - ReuseGrid->OriginVoxels.X) / Step : 0; const int32 SharedDeltaY = ReuseGrid ? (OriginVoxels.Y - ReuseGrid->OriginVoxels.Y) / Step : 0; const int32 SharedDeltaZ = ReuseGrid ? (OriginVoxels.Z - ReuseGrid->OriginVoxels.Z) / Step : 0; const bool bUseSharedDensityGrid = ReuseGrid && ReuseGrid->Step == Step && ReuseGrid->Dim > 0 && ReuseGrid->Samples.Num() == ReuseGrid->Dim * ReuseGrid->Dim * ReuseGrid->Dim && (OriginVoxels.X - ReuseGrid->OriginVoxels.X) % Step == 0 && (OriginVoxels.Y - ReuseGrid->OriginVoxels.Y) % Step == 0 && (OriginVoxels.Z - ReuseGrid->OriginVoxels.Z) % Step == 0 && SharedDeltaX - 1 >= 0 && SharedDeltaY - 1 >= 0 && SharedDeltaZ - 1 >= 0 && SharedDeltaX + GridDim < ReuseGrid->Dim && SharedDeltaY + GridDim < ReuseGrid->Dim && SharedDeltaZ + GridDim < ReuseGrid->Dim; const bool bCapturePreEditGrid = OutPreEditDensityGrid != nullptr && !bUseSharedDensityGrid && Step == 1 && CellsPerAxis == CHUNK_SIZE && CzLo == 0 && CzHi == CellsPerAxis - 1; if (bCapturePreEditGrid) { OutPreEditDensityGrid->OriginVoxels = OriginVoxels + FIntVector(-Step, -Step, -Step); OutPreEditDensityGrid->Step = Step; OutPreEditDensityGrid->Dim = MDim; OutPreEditDensityGrid->Samples.SetNumUninitialized(MDim * MDim * MDim); } // Bande de strate : seules les rangées Z réellement lues (cellules CzLo..CzHi + marges de // gradient) sont échantillonnées — le reste du tampon reste non initialisé et non lu. VoxelDensityProfile::FScopedTimer MesherDensityGridTimer( VoxelDensityProfile::EBucket::MesherDensityGrid); #if WITH_EDITOR const bool bUseParameterizedBrickEvaluator = VoxelForgeRedefinedDensityField::IsParameterizedEnabled() && !bUseSharedDensityGrid && !bCapturePreEditGrid && OutCaptureGrid == nullptr; #else constexpr bool bUseParameterizedBrickEvaluator = false; #endif const bool bUseOperatorBlock = !bUseSharedDensityGrid && !bUseParameterizedBrickEvaluator && GVoxelForgeUseOperatorBlock != 0; if (bUseOperatorBlock) { // The grid includes one sample of halo on every side. The generator groups these exact // lattice points by chunk key and lets the prepared op stack fill each group once. Generator->BeginDensityBlock( OriginVoxels + FIntVector(-Step, -Step, GzLo * Step), Step, MDim, MDim, GzHi - GzLo + 1); } if (bUseParameterizedBrickEvaluator) { #if WITH_EDITOR static std::atomic bLoggedParameterizedBrickRoute{false}; if (!bLoggedParameterizedBrickRoute.exchange(true, std::memory_order_relaxed)) { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeResearchBrickEvaluator] mesher_grid=1 brick_samples=%d detail_scale=%.6f"), VoxelForgeRedefinedDensityField::ParameterizedBrickSamples, VoxelForgeRedefinedDensityField::GetDetailScale()); } constexpr int32 ResearchBrickSamples = VoxelForgeRedefinedDensityField::ParameterizedBrickSamples; const float DetailScale = VoxelForgeRedefinedDensityField::GetDetailScale(); TArray DetailBrick; TArray BrickValues; TArray ColumnValues; TArray ProofKinds; VoxelForgeRedefinedDensityField::FParameterizedBrickContext BrickContext; const int32 GridZSamples = GzHi - GzLo + 1; const int32 ProofZCount = FMath::DivideAndRoundUp(GridZSamples, ResearchBrickSamples); ProofKinds.SetNumUninitialized(ProofZCount); for (int32 BrickY = 0; BrickY < MDim; BrickY += ResearchBrickSamples) for (int32 BrickX = 0; BrickX < MDim; BrickX += ResearchBrickSamples) { if (ShouldAbortWork()) { return FVoxelMeshData(); } const int32 BrickSizeX = FMath::Min(ResearchBrickSamples, MDim - BrickX); const int32 BrickSizeY = FMath::Min(ResearchBrickSamples, MDim - BrickY); for (int32 ProofZ = 0; ProofZ < ProofZCount; ++ProofZ) { if (ShouldAbortWork()) { return FVoxelMeshData(); } const int32 BrickZ = ProofZ * ResearchBrickSamples; const FIntVector ProofSize( BrickSizeX, BrickSizeY, FMath::Min(ResearchBrickSamples, GridZSamples - BrickZ)); const FIntVector ProofOrigin = OriginVoxels + FIntVector( (BrickX - 1) * Step, (BrickY - 1) * Step, (GzLo + BrickZ) * Step); bool bUniformAir = false; if (VoxelForgeRedefinedDensityField::TryClassifyParameterizedBrick( *Generator, ProofOrigin, ProofSize, bUniformAir, DetailScale, nullptr, nullptr, Step)) { ProofKinds[ProofZ] = bUniformAir ? 1 : 2; const float UniformDensity = bUniformAir ? VoxelForgeRedefinedDensityField::ParameterizedUniformDensity : -VoxelForgeRedefinedDensityField::ParameterizedUniformDensity; for (int32 Z = 0; Z < ProofSize.Z; ++Z) for (int32 Y = 0; Y < ProofSize.Y; ++Y) for (int32 X = 0; X < ProofSize.X; ++X) { const int32 GX = BrickX + X - 1; const int32 GY = BrickY + Y - 1; const int32 GZ = GzLo + BrickZ + Z; const int32 LocalIndex = ((GZ + 1) * MDim + (GY + 1)) * MDim + (GX + 1); DensityGrid[LocalIndex] = UniformDensity; } } else { ProofKinds[ProofZ] = 0; } } for (int32 ProofZ = 0; ProofZ < ProofZCount;) { if (ProofKinds[ProofZ] != 0) { ++ProofZ; continue; } if (ShouldAbortWork()) { return FVoxelMeshData(); } const int32 FirstMixedProofZ = ProofZ; const int32 BrickZ = FirstMixedProofZ * ResearchBrickSamples; const int32 FirstWorldZ = OriginVoxels.Z + (GzLo + BrickZ) * Step; const int32 FirstChunkZ = FMath::FloorToInt( static_cast(FirstWorldZ) / CHUNK_SIZE); int32 RunProofCount = 1; int32 RunSampleCount = FMath::Min( ResearchBrickSamples, GridZSamples - BrickZ); while (ProofZ + RunProofCount < ProofZCount && ProofKinds[ProofZ + RunProofCount] == 0) { const int32 CandidateSampleCount = FMath::Min( (RunProofCount + 1) * ResearchBrickSamples, GridZSamples - BrickZ); const int32 CandidateEndZ = FirstWorldZ + (CandidateSampleCount - 1) * Step; const int32 CandidateChunkZ = FMath::FloorToInt( static_cast(CandidateEndZ) / CHUNK_SIZE); if (CandidateSampleCount > FMath::Max(1, CHUNK_SIZE / Step) || CandidateChunkZ != FirstChunkZ) { break; } ++RunProofCount; RunSampleCount = CandidateSampleCount; } const FIntVector BrickSize(BrickSizeX, BrickSizeY, RunSampleCount); const FIntVector BrickOrigin = OriginVoxels + FIntVector( (BrickX - 1) * Step, (BrickY - 1) * Step, (GzLo + BrickZ) * Step); VoxelForgeRedefinedDensityField::EvaluateParameterizedDensityBrick( *Generator, BrickOrigin, BrickSize, DetailScale, BrickContext, DetailBrick, ColumnValues, BrickValues, Step); for (int32 Z = 0; Z < BrickSize.Z; ++Z) for (int32 Y = 0; Y < BrickSize.Y; ++Y) for (int32 X = 0; X < BrickSize.X; ++X) { const int32 GX = BrickX + X - 1; const int32 GY = BrickY + Y - 1; const int32 GZ = GzLo + BrickZ + Z; const int32 LocalIndex = ((GZ + 1) * MDim + (GY + 1)) * MDim + (GX + 1); DensityGrid[LocalIndex] = BrickValues[(Z * BrickSize.Y + Y) * BrickSize.X + X]; } if (OutDensitySampleCount != nullptr) { *OutDensitySampleCount += static_cast( BrickSize.X * BrickSize.Y * BrickSize.Z); } ProofZ += RunProofCount; } } #endif } else for (int32 gz = GzLo; gz <= GzHi; gz++) { if (ShouldAbortWork()) { if (bUseOperatorBlock) { Generator->EndDensityBlock(); } return FVoxelMeshData(); } for (int32 gy = -1; gy <= GridDim; gy++) { if (ShouldAbortWork()) { if (bUseOperatorBlock) { Generator->EndDensityBlock(); } return FVoxelMeshData(); } for (int32 gx = -1; gx <= GridDim; gx++) { const int32 LocalIndex = ((gz + 1) * MDim + (gy + 1)) * MDim + (gx + 1); if (bUseSharedDensityGrid) { const int32 SharedX = SharedDeltaX + gx; const int32 SharedY = SharedDeltaY + gy; const int32 SharedZ = SharedDeltaZ + gz; DensityGrid[LocalIndex] = ReuseGrid->Samples[ (SharedZ * ReuseGrid->Dim + SharedY) * ReuseGrid->Dim + SharedX]; } else { // World voxel = tile origin + grid offset scaled by the cell size (Step). const int32 ReachBlockX = FMath::Clamp(gx, 0, CellsPerAxis - 1) / VoxelGenLOD::TileReachBlockCells; const int32 ReachBlockY = FMath::Clamp(gy, 0, CellsPerAxis - 1) / VoxelGenLOD::TileReachBlockCells; const int32 ReachBlockZ = FMath::Clamp(gz, 0, CellsPerAxis - 1) / VoxelGenLOD::TileReachBlockCells; VoxelGenLOD::TileReachBlockIndex = ((ReachBlockZ * ReachBlocksPerAxis) + ReachBlockY) * ReachBlocksPerAxis + ReachBlockX; float PreEditDensity = 0.0f; DensityGrid[LocalIndex] = Generator->GetDensityAt( OriginVoxels.X + gx * Step, OriginVoxels.Y + gy * Step, OriginVoxels.Z + gz * Step, bCapturePreEditGrid ? &PreEditDensity : nullptr); if (bCapturePreEditGrid) { OutPreEditDensityGrid->Samples[LocalIndex] = PreEditDensity; } if (OutDensitySampleCount) { ++(*OutDensitySampleCount); } } } } } if (VoxelDensityProfile::AreCountersEnabled()) { const uint64 GridSampleCount = static_cast(GzHi - GzLo + 1) * static_cast(MDim) * static_cast(MDim); VoxelDensityProfile::AddCounter( VoxelDensityProfile::ECounter::MesherDensityGridBytes, GridSampleCount * sizeof(float)); if (bUseSharedDensityGrid) { VoxelDensityProfile::AddCounter( VoxelDensityProfile::ECounter::MesherSharedGridReadBytes, GridSampleCount * sizeof(float)); } } if (bUseOperatorBlock) { Generator->EndDensityBlock(); } MesherDensityGridTimer.End(); if (bUseProofBlocks) { // Finalize every block candidate against the exact density samples that the marching- // cubes loop will consume. This is intentionally redundant with ClassifyTile: it is a // cheap soundness fence around the proof/cache hand-off. Unknown, zero, or opposite-sign // vertices clear the candidate, so an unproven block pays the ordinary cell path. constexpr int32 BlockCells = 8; for (int32 BlockZ = 0; BlockZ < BlocksPerAxis; ++BlockZ) { const int32 BlockMinZ = BlockZ * BlockCells; const int32 BlockMaxZ = FMath::Min(CellsPerAxis - 1, BlockMinZ + BlockCells - 1); if (BlockMaxZ < CzLo || BlockMinZ > CzHi) { continue; } for (int32 BlockY = 0; BlockY < BlocksPerAxis; ++BlockY) for (int32 BlockX = 0; BlockX < BlocksPerAxis; ++BlockX) { const int32 BlockIndex = ((BlockZ * BlocksPerAxis) + BlockY) * BlocksPerAxis + BlockX; const uint8 Candidate = BlockSkip[BlockIndex]; if (Candidate == 0) { continue; } const int32 BlockMaxX = FMath::Min(CellsPerAxis - 1, BlockX * BlockCells + BlockCells - 1); const int32 BlockMaxY = FMath::Min(CellsPerAxis - 1, BlockY * BlockCells + BlockCells - 1); bool bUniform = true; for (int32 Z = BlockMinZ; Z <= BlockMaxZ + 1 && bUniform; ++Z) for (int32 Y = BlockY * BlockCells; Y <= BlockMaxY + 1 && bUniform; ++Y) for (int32 X = BlockX * BlockCells; X <= BlockMaxX + 1; ++X) { const float Density = DensityGrid[ ((Z + 1) * MDim + (Y + 1)) * MDim + (X + 1)]; if (!VoxelMath::IsFinite(Density) || Density == 0.0f || (Candidate == 1 ? Density >= 0.0f : Density <= 0.0f)) { bUniform = false; break; } } if (!bUniform) { BlockSkip[BlockIndex] = 0; continue; } if (VoxelDensityProfile::AreCountersEnabled()) { VoxelDensityProfile::AddCounter( Candidate == 1 ? VoxelDensityProfile::ECounter::MesherBlockAllSolid : VoxelDensityProfile::ECounter::MesherBlockAllAir); } } } } // ── CAPTURE-DURING-MESHING ── // Si demandé et que la tuile est pleine résolution (CellsPerAxis==CHUNK_SIZE ⇒ Step==1<SetNumUninitialized(CHUNK_SIZE * CHUNK_SIZE * CHUNK_SIZE); uint8* Cap = OutCaptureGrid->GetData(); int32 ci = 0; for (int32 gz = 0; gz < CHUNK_SIZE; ++gz) for (int32 gy = 0; gy < CHUNK_SIZE; ++gy) for (int32 gx = 0; gx < CHUNK_SIZE; ++gx) { Cap[ci++] = VF_QuantizeDensity(DensityGrid[((gz + 1) * MDim + (gy + 1)) * MDim + (gx + 1)]); } } if (OutPreEditDensityGrid && !bCapturePreEditGrid) { OutPreEditDensityGrid->Reset(); } // Lecture grille (avec offset de marge) + gradient central depuis la grille. auto SampleG = [&](int32 gx, int32 gy, int32 gz) -> float { return DensityGrid[((gz + 1) * MDim + (gy + 1)) * MDim + (gx + 1)]; }; auto GradAt = [&](int32 gx, int32 gy, int32 gz) -> FVector { // Densité négative=solide, positive=air → le gradient pointe vers l'air (sortant). return FVector( SampleG(gx + 1, gy, gz) - SampleG(gx - 1, gy, gz), SampleG(gx, gy + 1, gz) - SampleG(gx, gy - 1, gz), SampleG(gx, gy, gz + 1) - SampleG(gx, gy, gz - 1)); }; // DC is limited to tagged LOD0 patches. Coarse tiles and a connected patch that reaches a // tile edge use MC there, preserving the existing MC-to-MC clipmap transition and skirts. // Le DC reste limite aux surfaces LOD0 balisees; les patches connectes au bord restent MC. VoxelHybridSharpMesher::FResult HybridResult; MeshData.HybridFeatureCount = HybridSharpFeatures.Num(); const bool bHybridLod0Interior = Step == 1 && CellsPerAxis == CHUNK_SIZE && CzLo == 0 && CzHi == CellsPerAxis - 1 && !HybridSharpFeatures.IsEmpty(); if (bUseHybridSharpMesher && bHybridLod0Interior) { MeshData.bHybridMesherInvoked = true; HybridResult = VoxelHybridSharpMesher::Generate( CellsPerAxis, Step, IsoLevel, GeometryVoxelSize, OriginVoxels, ChunkWorldPos, HybridSharpFeatures, [&](int32 X, int32 Y, int32 Z) { return SampleG(X, Y, Z); }, [&](int32 X, int32 Y, int32 Z) { return GradAt(X, Y, Z); }); } const bool bHybridSharpSnap = bUseHybridSharpFeatureSnap && bHybridLod0Interior; const std::vector PreparedSharpFeatures = bHybridSharpSnap ? VoxelHybridSharpMesher::PrepareFeatureBoxes(HybridSharpFeatures) : std::vector{}; if (bHybridSharpSnap) MeshData.bHybridMesherInvoked = true; MeshData.HybridSharpCellCount = HybridResult.SharpCells; MeshData.HybridDualCellCount = HybridResult.DualCells; MeshData.HybridActiveEdgeRootCount = HybridResult.ActiveEdgeRoots; MeshData.HybridTaggedEdgeRootCount = HybridResult.TaggedEdgeRoots; MeshData.HybridMinimumFeatureRootAbsSdf = HybridResult.MinimumFeatureRootAbsSdf; MeshData.HybridFeatureRootSampleCount = HybridResult.FeatureRootSamples; MeshData.HybridNearFeatureRootSampleCount = HybridResult.NearFeatureRootSamples; MeshData.HybridBorderFallbackPatchCount = HybridResult.BorderFallbackPatches; MeshData.HybridDCTriangleCount = static_cast(HybridResult.Triangles.size()); //========================================================================= // ITÉRATION SUR LES CELLULES //========================================================================= // On itère sur les CELLULES (indices de grille), pas sur les voxels monde. // Coin i de la cellule = point de grille (cx+ox, cy+oy, cz+oz) ; coord voxel // monde = ce point × Step. LOD0 Step=1 → full res ; LOD1 Step=2 → ~4× moins. // F17 — les triangles sont émis dans DEUX seaux (sol / sky-cap, vote majoritaire des // classes de vertex) puis concaténés sol-puis-cap : RMC exige un run d'indices contigu // par polygroup. Géométrie inchangée au bit près — seul l'ORDRE des triangles bouge. static thread_local TArray GroundTris; static thread_local TArray CapTris; GroundTris.Reset(); CapTris.Reset(); const bool bProfileMesher = VoxelDensityProfile::IsCycleTimingEnabled(); uint64 CellClassificationCalls = 0; uint64 SurfaceCellCalls = 0; for (int32 cz = CzLo; cz <= CzHi; cz++) // bande de strate : cf. CzLo/CzHi plus haut { if (ShouldAbortWork()) return FVoxelMeshData(); for (int32 cy = 0; cy < CellsPerAxis; cy++) { if (ShouldAbortWork()) return FVoxelMeshData(); for (int32 cx = 0; cx < CellsPerAxis; cx++) { if (bUseProofBlocks && BlockSkip[((cz / 8) * BlocksPerAxis + (cy / 8)) * BlocksPerAxis + (cx / 8)] != 0) { continue; } const bool bSampleCell = bProfileMesher && VoxelDensityProfile::ShouldSample( VoxelDensityProfile::EBucket::MesherCellClassification); const uint64 ClassificationStart = bSampleCell ? FPlatformTime::Cycles64() : 0; // PASSE 1 : densités aux 8 coins + index de cas MC SEULEMENT. // ~70% des cellules d'un chunk sont tout-roc ou tout-air (aucune surface) ; // on les rejette ICI, AVANT de payer les 8 positions + 8 gradients (48 lectures // grille + maths vectorielles). Sortie bit-identique : positions et gradients ne // servent qu'aux cellules réellement traversées par l'isosurface. float Densities[8]; int32 CaseIndex = 0; for (int32 i = 0; i < 8; i++) { const float D = SampleG(cx + CornerOffsets[i].X, cy + CornerOffsets[i].Y, cz + CornerOffsets[i].Z); Densities[i] = D; if (D >= IsoLevel) CaseIndex |= (1 << i); } const bool bHasSurface = MCEdgeTable[CaseIndex] != 0; if (bSampleCell) { VoxelDensityProfile::AddSampledMeasurement( VoxelDensityProfile::EBucket::MesherCellClassification, FPlatformTime::Cycles64() - ClassificationStart); } ++CellClassificationCalls; if (!bHasSurface) continue; // Pas de surface ici → skip ++SurfaceCellCalls; // PASSE 2 : positions + gradients aux 8 coins (uniquement si surface présente). const uint64 GradientStart = bSampleCell ? FPlatformTime::Cycles64() : 0; FVector Positions[8]; FVector Gradients[8]; for (int32 i = 0; i < 8; i++) { const int32 GX = cx + CornerOffsets[i].X; const int32 GY = cy + CornerOffsets[i].Y; const int32 GZ = cz + CornerOffsets[i].Z; Positions[i] = ChunkWorldPos + FVector(GX * Step, GY * Step, GZ * Step) * GeometryVoxelSize; Gradients[i] = GradAt(GX, GY, GZ); } if (bSampleCell) { VoxelDensityProfile::AddSampledMeasurement( VoxelDensityProfile::EBucket::MesherGradientNormals, FPlatformTime::Cycles64() - GradientStart); } // Interpolation des positions + normales sur les arêtes traversées. t = point de // traversée de l'iso entre les deux coins (clampé, milieu si densités quasi-égales) ; // la normale interpole les gradients de coin par le même t. const uint64 VertexInterpolationStart = bSampleCell ? FPlatformTime::Cycles64() : 0; FVector EdgeVertices[12]; FVector EdgeNormals[12]; uint64 EdgeSplitKeys[12]{}; for (int32 i = 0; i < 12; i++) { if (MCEdgeTable[CaseIndex] & (1 << i)) { const int32 A = EdgeCorners[i][0]; const int32 B = EdgeCorners[i][1]; const float D1 = Densities[A], D2 = Densities[B]; const float T = (FMath::Abs(D2 - D1) < KINDA_SMALL_NUMBER) ? 0.5f : FMath::Clamp((IsoLevel - D1) / (D2 - D1), 0.0f, 1.0f); EdgeVertices[i] = Positions[A] + T * (Positions[B] - Positions[A]); EdgeNormals[i] = Gradients[A] + T * (Gradients[B] - Gradients[A]); if (bHybridSharpSnap) { const int32 Axis = CornerOffsets[A].X != CornerOffsets[B].X ? 0 : (CornerOffsets[A].Y != CornerOffsets[B].Y ? 1 : 2); const FIntVector LocalStart(cx + CornerOffsets[A].X, cy + CornerOffsets[A].Y, cz + CornerOffsets[A].Z); // Every cell incident to this lattice edge must be interior so all // copies of the shared root take the same exact or MC path. const bool bCanSnapEdge = bHybridSharpSnap && LocalStart.X >= 2 && LocalStart.Y >= 2 && LocalStart.Z >= 2 && LocalStart.X <= CellsPerAxis - 2 && LocalStart.Y <= CellsPerAxis - 2 && LocalStart.Z <= CellsPerAxis - 2; FVector AnalyticNormal; if (bCanSnapEdge && VoxelHybridSharpMesher::TrySnapFeatureEdge( PreparedSharpFeatures, OriginVoxels, LocalStart, Axis, ChunkWorldPos, GeometryVoxelSize, EdgeVertices[i], AnalyticNormal, EdgeSplitKeys[i])) { EdgeNormals[i] = AnalyticNormal; } } } } if (bSampleCell) { VoxelDensityProfile::AddSampledMeasurement( VoxelDensityProfile::EBucket::MesherVertexInterpolation, FPlatformTime::Cycles64() - VertexInterpolationStart); } // Génère les triangles avec vertices dédupliqués. // Ordre 0, 2, 1 (pas 0, 1, 2) pour le winding attendu par RealtimeMesh. const uint64 StreamBuildingStart = bSampleCell ? FPlatformTime::Cycles64() : 0; for (int32 i = 0; MCTriTable[CaseIndex][i] != -1; i += 3) { const int32 E0 = MCTriTable[CaseIndex][i]; const int32 E1 = MCTriTable[CaseIndex][i + 1]; const int32 E2 = MCTriTable[CaseIndex][i + 2]; if (HybridResult.UsesDCEdge(cx, cy, cz, E0, CellsPerAxis)) { continue; } const int32 Idx0 = GetOrCreateVertex( EdgeVertices[E0], EdgeNormals[E0], EdgeSplitKeys[E0]); const int32 Idx1 = GetOrCreateVertex( EdgeVertices[E1], EdgeNormals[E1], EdgeSplitKeys[E1]); const int32 Idx2 = GetOrCreateVertex( EdgeVertices[E2], EdgeNormals[E2], EdgeSplitKeys[E2]); // F17 — vote majoritaire (≥ 2 vertex sky-cap ⇒ triangle sky-cap). const int32 CapVotes = (int32)VertexClasses[Idx0] + (int32)VertexClasses[Idx1] + (int32)VertexClasses[Idx2]; TArray& Dst = (CapVotes >= 2) ? CapTris : GroundTris; Dst.Add(Idx0); Dst.Add(Idx2); Dst.Add(Idx1); } if (bSampleCell) { VoxelDensityProfile::AddSampledMeasurement( VoxelDensityProfile::EBucket::MesherStreamBuilding, FPlatformTime::Cycles64() - StreamBuildingStart); } } } } // Add DC geometry to the same material runs as MC. Analytic feature IDs split corner normals. // Ajouter la geometrie DC dans les memes groupes que MC; les IDs separent les normales nettes. for (const VoxelHybridSharpMesher::FTriangle& Triangle : HybridResult.Triangles) { const int32 Idx0 = GetOrCreateVertex( Triangle.Positions[0], Triangle.Normals[0], Triangle.SplitKeys[0]); const int32 Idx1 = GetOrCreateVertex( Triangle.Positions[1], Triangle.Normals[1], Triangle.SplitKeys[1]); const int32 Idx2 = GetOrCreateVertex( Triangle.Positions[2], Triangle.Normals[2], Triangle.SplitKeys[2]); const int32 CapVotes = static_cast(VertexClasses[Idx0]) + static_cast(VertexClasses[Idx1]) + static_cast(VertexClasses[Idx2]); TArray& Dst = (CapVotes >= 2) ? CapTris : GroundTris; Dst.Add(Idx0); Dst.Add(Idx2); Dst.Add(Idx1); } if (bProfileMesher) { VoxelDensityProfile::AddMeasurement( VoxelDensityProfile::EBucket::MesherCellClassification, 0, CellClassificationCalls); VoxelDensityProfile::AddMeasurement( VoxelDensityProfile::EBucket::MesherGradientNormals, 0, SurfaceCellCalls); VoxelDensityProfile::AddMeasurement( VoxelDensityProfile::EBucket::MesherVertexInterpolation, 0, SurfaceCellCalls); VoxelDensityProfile::AddMeasurement( VoxelDensityProfile::EBucket::MesherStreamBuilding, 0, SurfaceCellCalls); } VoxelDensityProfile::FScopedTimer MesherFinalisationTimer( VoxelDensityProfile::EBucket::MesherOther); //========================================================================= // SKIRTS — boucheurs de fissures aux coutures de LOD (clipmap) //========================================================================= // Deux tuiles de niveaux voisins maillent à des résolutions différentes : leurs iso-surfaces // ne se rejoignent pas parfaitement le long de la face partagée → une fine fissure traversante. // On la scelle en suspendant une courte « jupe » (mur) sous chaque arête de surface posée sur // l'une des 6 faces externes de la tuile, extrudée VERS LE SOLIDE le long de la normale inversée // sur ~une taille de cellule. Là où la surface du voisin est décalée, les jupes des deux tuiles // se recouvrent dans la roche et ferment le trou ; ailleurs la jupe est enterrée et invisible. // Émis en DOUBLE FACE (deux orientations) pour s'afficher quel que soit le côté caméra / le // matériau. Une arête de surface posée sur une face de frontière a sa coordonnée d'axe EXACTE // (l'interpolation MC garde fixe l'axe de la face) → comparaison flottante exacte fiable. MeshData.NumSurfaceVertices = MeshData.Vertices.Num(); if (bGenerateSkirts && (GroundTris.Num() + CapTris.Num()) > 0) { const float ExtentCm = (float)(CellsPerAxis * Step) * GeometryVoxelSize; const float MinX = ChunkWorldPos.X, MinY = ChunkWorldPos.Y, MinZ = ChunkWorldPos.Z; const float MaxX = MinX + ExtentCm, MaxY = MinY + ExtentCm, MaxZ = MinZ + ExtentCm; const float SkirtDepth = FMath::Max(1.0f, SkirtCells) * (float)Step * GeometryVoxelSize; auto OnBoundaryPlane = [&](const FVector& A, const FVector& B) -> bool { return (A.X == MinX && B.X == MinX) || (A.X == MaxX && B.X == MaxX) || (A.Y == MinY && B.Y == MinY) || (A.Y == MaxY && B.Y == MaxY) || (A.Z == MinZ && B.Z == MinZ) || (A.Z == MaxZ && B.Z == MaxZ); }; auto AddSkirtVert = [&](const FVector& Pos, const FVector& Nrm, const FColor& Col) -> int32 { const int32 Idx = MeshData.Vertices.Num(); MeshData.Vertices.Add(Pos); MeshData.Normals.Add(Nrm); MeshData.UVs.Add(FVector2D(Pos.X / VOXEL_SIZE, Pos.Y / VOXEL_SIZE)); MeshData.Colors.Add(Col); // hérite la couleur du vertex source (parallèle aux autres tableaux) return Idx; }; // F17 — jupes émises PAR SEAU : chaque jupe hérite la classe (donc le polygroup / // matériau) de son triangle source ; ajouter les jupes après coup casserait les runs // d'indices contigus par groupe qu'exige RMC. auto EmitSkirts = [&](TArray& Tris) { // On ajoute en itérant : on fige le nombre de triangles de surface et on n'ajoute qu'au-delà. const int32 BaseTriNum = Tris.Num(); for (int32 t = 0; t + 2 < BaseTriNum; t += 3) { const int32 Tri[3] = { Tris[t], Tris[t + 1], Tris[t + 2] }; for (int32 e = 0; e < 3; ++e) { const int32 iA = Tri[e], iB = Tri[(e + 1) % 3]; // COPIES par valeur — AddSkirtVert réalloue Vertices/Normals (invaliderait des refs). const FVector PA = MeshData.Vertices[iA]; const FVector PB = MeshData.Vertices[iB]; if (!OnBoundaryPlane(PA, PB)) continue; const FVector NA = MeshData.Normals[iA]; const FVector NB = MeshData.Normals[iB]; const FColor CA = MeshData.Colors[iA]; const FColor CB = MeshData.Colors[iB]; const int32 iA2 = AddSkirtVert(PA - NA * SkirtDepth, NA, CA); const int32 iB2 = AddSkirtVert(PB - NB * SkirtDepth, NB, CB); // Quad (iA, iB, iB2, iA2) → 2 triangles, émis dans LES DEUX orientations. Tris.Add(iA); Tris.Add(iB); Tris.Add(iB2); Tris.Add(iA); Tris.Add(iB2); Tris.Add(iA2); Tris.Add(iA); Tris.Add(iB2); Tris.Add(iB); Tris.Add(iA); Tris.Add(iA2); Tris.Add(iB2); } } MeshData.NumSkirtTriangles += (Tris.Num() - BaseTriNum) / 3; }; EmitSkirts(GroundTris); EmitSkirts(CapTris); } // F17 — concatène sol PUIS sky-cap : un run d'indices contigu par polygroup (voir // FVoxelMeshData::NumCeilingTriangles). Mêmes triangles qu'avant, seul l'ordre change. MeshData.Triangles.Reserve(GroundTris.Num() + CapTris.Num()); MeshData.Triangles.Append(GroundTris); MeshData.Triangles.Append(CapTris); MeshData.NumCeilingTriangles = CapTris.Num() / 3; if (VoxelDensityProfile::AreCountersEnabled()) { const uint64 OutputArrayBytes = static_cast(MeshData.Vertices.Num()) * sizeof(FVector) + static_cast(MeshData.Normals.Num()) * sizeof(FVector) + static_cast(MeshData.UVs.Num()) * sizeof(FVector2D) + static_cast(MeshData.Colors.Num()) * sizeof(FColor) + static_cast(MeshData.Triangles.Num()) * sizeof(int32); const uint64 OutputAllocatedBytes = static_cast(MeshData.Vertices.GetAllocatedSize()) + static_cast(MeshData.Normals.GetAllocatedSize()) + static_cast(MeshData.UVs.GetAllocatedSize()) + static_cast(MeshData.Colors.GetAllocatedSize()) + static_cast(MeshData.Triangles.GetAllocatedSize()); VoxelDensityProfile::AddCounter( VoxelDensityProfile::ECounter::MesherOutputArrayBytes, OutputArrayBytes); VoxelDensityProfile::AddCounter( VoxelDensityProfile::ECounter::MesherOutputAllocatedBytes, OutputAllocatedBytes); } return MeshData; }