Files
VoxelForge/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp
T
Fr0zkaandClaude Opus 5.5 2656d81ac9 standalone: whole view under one second -- 0.955 s cold @8 workers, full payload, world unchanged
- Eight-sample X batching of fine density rows, reuse of solved Z intervals in the candidate mask,
  AVX2 marching-cubes cell classification. LOD0/1 density-grid time -61/-62%; interval/sign stage
  140 -> 97 ms; candidate-axis allocation -18 MiB. Shape stage ~413 ms, detail band ~219 ms.
- W1/W8 hashes equal across 44 fields and equal to round 8; routes 5642/5642; look and far-survival
  audits pass; bad-shell control still fails; six fixed captures pixel-identical.
  Remote-view recapture pending (camera 500,0,0).
Verified by me, interleaved (8 workers, cold, BelowNormal 0xFF00, 2,226 tiles, 110.9 MiB payload):
1.012 / 0.927 / 0.926 s vs round 8 1.100 / 1.099 / 1.088 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
2026-10-05 05:15:19 +02:00

1604 lines
80 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// VoxelMarchingCubesMesher.cpp
// Implémentation du marching cubes density-only.
#include "VoxelMarchingCubesMesher.h"
#include "MarchingCubesTables.h"
#include "StandaloneShapeWorld.h"
#include "VoxelDensityProfile.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 <atomic>
#include <immintrin.h>
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<uint8>* OutCaptureGrid,
int32 BandZMinVox, int32 BandZMaxVox,
int64* OutDensitySampleCount,
FVoxelSharedDensityGrid* OutPreEditDensityGrid,
const FVoxelSharedDensityGrid* InSharedDensityGrid,
std::vector<float>* OutDebugDensitySamples)
{
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_MesherGenerateMesh);
FVoxelMeshData MeshData;
if (OutCaptureGrid) { OutCaptureGrid->Reset(); }
if (OutPreEditDensityGrid) { OutPreEditDensityGrid->Reset(); }
if (OutDensitySampleCount) { *OutDensitySampleCount = 0; }
if (OutDebugDensitySamples) { OutDebugDensitySamples->clear(); }
if (!Generator) return MeshData;
const bool bEmitUV = !bStandaloneSkipUvAndColors && !bStandaloneSkipUV;
const bool bEmitColors = !bStandaloneSkipUvAndColors && !bStandaloneSkipColors;
const bool bUseCoarseFarBiomeGrid = Step >= 64;
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);
// 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<int32> OctaveBiasGuard(VoxelGenLOD::OctaveBias, OctaveBias);
TGuardValue<int32> SampleStepGuard(VoxelGenLOD::SampleStep, Step);
// World-cm origin of the tile's min corner (positions are built relative to this).
const FVector ChunkWorldPos = FVector(OriginVoxels) * VOXEL_SIZE;
//=========================================================================
// 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<FIntVector, int32> VertexMap;
VertexMap.Reset();
const int32 VertexCapacityHint=FMath::Clamp(InCellsPerAxis,2,CHUNK_SIZE);
VertexMap.Reserve(VertexCapacityHint*VertexCapacityHint*2);
//=========================================================================
// 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<uint8> VertexClasses; // 0 = sol, 1 = sky-cap
static thread_local TMap<FIntVector, FVector2f> SurfColMemo; // (Xq,Yq,chunkZ) → (TerrainZ, CeilSurf)
if (!bStandaloneSkipMaterialGroups)
{
VertexClasses.Reset();
SurfColMemo.Reset();
}
struct FBiomeGridSample
{
int32 Dominant = 0;
int32 Neighbor = 0;
float Blend = 0.0f;
};
struct FBiomeTileGridSamples
{
FBiomeGridSample Corners[4];
};
static thread_local TMap<int32, FBiomeTileGridSamples> BiomeTileGridMemo;
const int32 BiomeGridSpacing = bUseCoarseFarBiomeGrid
? FMath::Clamp(InCellsPerAxis, 2, CHUNK_SIZE) * Step : 1;
if (bUseCoarseFarBiomeGrid && bEmitColors)
{
BiomeTileGridMemo.Reset();
}
auto GetBiomeTileGridSamples = [&](int32 ChunkZ) -> FBiomeTileGridSamples
{
if (const FBiomeTileGridSamples* Existing = BiomeTileGridMemo.Find(ChunkZ))
{
return *Existing;
}
FBiomeTileGridSamples Samples;
const float SampleZ = static_cast<float>(ChunkZ * CHUNK_SIZE + CHUNK_SIZE / 2);
const int32 TileMaxX = OriginVoxels.X + BiomeGridSpacing;
const int32 TileMaxY = OriginVoxels.Y + BiomeGridSpacing;
Generator->GetBiomeMaterialAt(static_cast<float>(OriginVoxels.X),
static_cast<float>(OriginVoxels.Y), SampleZ,
Samples.Corners[0].Dominant, Samples.Corners[0].Neighbor, Samples.Corners[0].Blend);
Generator->GetBiomeMaterialAt(static_cast<float>(TileMaxX),
static_cast<float>(OriginVoxels.Y), SampleZ,
Samples.Corners[1].Dominant, Samples.Corners[1].Neighbor, Samples.Corners[1].Blend);
Generator->GetBiomeMaterialAt(static_cast<float>(OriginVoxels.X),
static_cast<float>(TileMaxY), SampleZ,
Samples.Corners[2].Dominant, Samples.Corners[2].Neighbor, Samples.Corners[2].Blend);
Generator->GetBiomeMaterialAt(static_cast<float>(TileMaxX),
static_cast<float>(TileMaxY), SampleZ,
Samples.Corners[3].Dominant, Samples.Corners[3].Neighbor, Samples.Corners[3].Blend);
return BiomeTileGridMemo.Add(ChunkZ, Samples);
};
// 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) -> int32
{
const FIntVector Key(
FMath::RoundToInt(WorldPos.X * 100.0f),
FMath::RoundToInt(WorldPos.Y * 100.0f),
FMath::RoundToInt(WorldPos.Z * 100.0f)
);
if (int32* Existing = VertexMap.Find(Key))
{
return *Existing;
}
const int32 NewIndex = MeshData.Vertices.Num();
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);
if (bEmitUV)
{
// UVs planaires — le triplanar mapping se fait dans le matériau.
MeshData.UVs.Add(FVector2f(WorldPos.X / VOXEL_SIZE, WorldPos.Y / VOXEL_SIZE));
}
if (bEmitColors)
{
//-----------------------------------------------------------------
// VERTEX COLOUR — masks for biome palette, slope and border blend.
int32 PalD = 0, PalN = 0; float BlendW = 0.0f;
if (bUseCoarseFarBiomeGrid)
{
const float VoxelX = WorldPos.X / VOXEL_SIZE;
const float VoxelY = WorldPos.Y / VOXEL_SIZE;
const float VoxelZ = WorldPos.Z / VOXEL_SIZE;
const float TX = FMath::Clamp(
(VoxelX - static_cast<float>(OriginVoxels.X))
/ static_cast<float>(BiomeGridSpacing), 0.0f, 1.0f);
const float TY = FMath::Clamp(
(VoxelY - static_cast<float>(OriginVoxels.Y))
/ static_cast<float>(BiomeGridSpacing), 0.0f, 1.0f);
const int32 ChunkZ = FMath::FloorToInt(VoxelZ / static_cast<float>(CHUNK_SIZE));
const FBiomeTileGridSamples TileSamples = GetBiomeTileGridSamples(ChunkZ);
const FBiomeGridSample& S00 = TileSamples.Corners[0];
const FBiomeGridSample& S10 = TileSamples.Corners[1];
const FBiomeGridSample& S01 = TileSamples.Corners[2];
const FBiomeGridSample& S11 = TileSamples.Corners[3];
const FBiomeGridSample* Nearest = &S00;
if (TX >= 0.5f) Nearest = TY >= 0.5f ? &S11 : &S10;
else if (TY >= 0.5f) Nearest = &S01;
PalD = Nearest->Dominant;
PalN = Nearest->Neighbor;
BlendW = Nearest->Blend;
if (S00.Dominant == PalD && S00.Neighbor == PalN
&& S10.Dominant == PalD && S10.Neighbor == PalN
&& S01.Dominant == PalD && S01.Neighbor == PalN
&& S11.Dominant == PalD && S11.Neighbor == PalN)
{
const float B0 = FMath::Lerp(S00.Blend, S10.Blend, TX);
const float B1 = FMath::Lerp(S01.Blend, S11.Blend, TX);
BlendW = FMath::Lerp(B0, B1, TY);
}
}
else
{
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));
}
if (!bStandaloneSkipMaterialGroups)
{
// F17 — classify down-facing vertices for material-only ceiling grouping.
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;
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));
}
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.
// The grid fill order is selected below by tile extent to preserve room-cache locality.
// 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<FIntVector> TileOriginGuard(
VoxelGenLOD::TileOriginVoxels, OriginVoxels);
TGuardValue<int32> 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<bool> TilePostReachDebugGuard(
VoxelGenLOD::bTilePostReachDebug,
GVoxelForgeTilePostReachDebug != 0);
TGuardValue<bool> TilePostReachBypassGuard(
VoxelGenLOD::bTilePostReachBypass, false);
TGuardValue<bool> TileReachDiagnosticGuard(
VoxelGenLOD::bTileReachDiagnosticTile, true);
TGuardValue<bool> TileCoreReachProofGuard(
VoxelGenLOD::bTileCoreReachProofEnabled, false);
TGuardValue<bool> TileCoreReachDecisionGuard(
VoxelGenLOD::bTileCoreReachDecisionRecorded, false);
TGuardValue<float> TileReachScaleGuard(
VoxelGenLOD::TileReachScale, GVoxelForgeTilePostReachScale);
const float ReachScale = GVoxelForgeTilePostReachScale;
const int32 ReachBlocksPerAxis = FMath::DivideAndRoundUp(
CellsPerAxis, VoxelGenLOD::TileReachBlockCells);
TGuardValue<int32> TileReachBlocksGuard(
VoxelGenLOD::TileReachBlocksPerAxis, ReachBlocksPerAxis);
TGuardValue<int32> TileReachBlockIndexGuard(
VoxelGenLOD::TileReachBlockIndex, INDEX_NONE);
TGuardValue<bool> TileBlockReachValidGuard(
VoxelGenLOD::bTileBlockReachValid, false);
uint8 TilePostReachFlags = VoxelGenLOD::AllTilePostReach;
if (Generator->StrateManager != nullptr)
{
const int64 Extent = static_cast<int64>(CellsPerAxis + 1) * Step;
const FBox LatticeWithHalo(
FVector(
static_cast<float>(static_cast<int64>(OriginVoxels.X) - Step),
static_cast<float>(static_cast<int64>(OriginVoxels.Y) - Step),
static_cast<float>(static_cast<int64>(OriginVoxels.Z) - Step)),
FVector(
static_cast<float>(static_cast<int64>(OriginVoxels.X) + Extent),
static_cast<float>(static_cast<int64>(OriginVoxels.Y) + Extent),
static_cast<float>(static_cast<int64>(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<uint8> 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<uint8> 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<int32> 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<float> 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<bool> 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<float> DetailBrick;
TArray<float> BrickValues;
TArray<float> ColumnValues;
TArray<uint8> 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<float>(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<float>(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);
const float Density=BrickValues[(Z * BrickSize.Y + Y) * BrickSize.X + X];
DensityGrid[LocalIndex]=Density;
}
if (OutDensitySampleCount != nullptr)
{
*OutDensitySampleCount += static_cast<int64>(
BrickSize.X * BrickSize.Y * BrickSize.Z);
}
ProofZ += RunProofCount;
}
}
#endif
}
else
{
auto BuildAxisGroups = [&](int32 AxisOrigin, int32 RegionSize)
{
std::vector<std::vector<int32>> Groups;
int32 PreviousRegion = INT32_MIN;
for (int32 Index = -1; Index <= GridDim; ++Index)
{
const int64 WorldCoordinate = static_cast<int64>(AxisOrigin)
+ static_cast<int64>(Index) * Step;
const int32 Region = FMath::FloorToInt(
static_cast<float>(WorldCoordinate) / static_cast<float>(RegionSize));
if (Groups.empty() || Region != PreviousRegion)
{
Groups.emplace_back();
PreviousRegion = Region;
}
Groups.back().push_back(Index);
}
return Groups;
};
const int32 RoomCacheRegionSize = CHUNK_SIZE
* (VoxelGenLOD::IsUseFusedEvaluatorEnabled() ? 4 : 1);
const auto XGroups = BuildAxisGroups(OriginVoxels.X,RoomCacheRegionSize);
const auto YGroups = BuildAxisGroups(OriginVoxels.Y,RoomCacheRegionSize);
auto SampleGridPoint = [&](int32 gx, int32 gy, int32 gz)
{
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
{
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->GetDensityAtLattice(
OriginVoxels.X + gx * Step,
OriginVoxels.Y + gy * Step,
OriginVoxels.Z + gz * Step,
bCapturePreEditGrid ? &PreEditDensity : nullptr);
if (bCapturePreEditGrid)
{
OutPreEditDensityGrid->Samples[LocalIndex] = PreEditDensity;
}
if (OutDensitySampleCount)
{
++(*OutDensitySampleCount);
}
}
};
// Very coarse tiles revisit the same XY cache window across a tall Z run. Keep that
// vertical run contiguous; SampleGridPoint writes by lattice index, so output bits do
// not depend on this traversal. Finer tiles retain plane-first traversal, which has
// lower overhead when a tile spans only one room-graph region.
const int64 TileExtentVoxels = static_cast<int64>(Step) * CellsPerAxis;
const bool bUseVerticalCacheTraversal = TileExtentVoxels > CHUNK_SIZE * 16;
const FStandaloneShapeWorld* ActiveShapeWorld=FStandaloneShapeWorldAccess::GetActive();
const bool bUseShapeWorldXBatch=ActiveShapeWorld!=nullptr
&&!bUseSharedDensityGrid&&!bCapturePreEditGrid;
if (bUseVerticalCacheTraversal)
{
for (const std::vector<int32>& YGroup : YGroups)
for (const std::vector<int32>& XGroup : XGroups)
for (const int32 gy : YGroup)
for (const int32 gx : XGroup)
{
for (int32 gz = GzLo; gz <= GzHi; ++gz)
{
if (ShouldAbortWork())
{
if (bUseOperatorBlock) { Generator->EndDensityBlock(); }
return FVoxelMeshData();
}
SampleGridPoint(gx,gy,gz);
}
}
}
else
{
for (int32 gz = GzLo; gz <= GzHi; ++gz)
{
if (ShouldAbortWork())
{
if (bUseOperatorBlock) { Generator->EndDensityBlock(); }
return FVoxelMeshData();
}
for (const std::vector<int32>& YGroup : YGroups)
for (const std::vector<int32>& XGroup : XGroups)
for (const int32 gy : YGroup)
{
if (ShouldAbortWork())
{
if (bUseOperatorBlock) { Generator->EndDensityBlock(); }
return FVoxelMeshData();
}
if(bUseShapeWorldXBatch)
{
for(size_t Offset=0;Offset<XGroup.size();)
{
const int32 BatchCount=static_cast<int32>(std::min<size_t>(
8,XGroup.size()-Offset));
const int32 GX0=XGroup[Offset];
bool bContiguous=true;
for(int32 Lane=1;Lane<BatchCount;++Lane)
bContiguous&=XGroup[Offset+static_cast<size_t>(Lane)]==GX0+Lane;
if(!bContiguous)
{
for(int32 Lane=0;Lane<BatchCount;++Lane)
SampleGridPoint(XGroup[Offset+static_cast<size_t>(Lane)],gy,gz);
Offset+=static_cast<size_t>(BatchCount);
continue;
}
float Densities[8]{};
ActiveShapeWorld->DensityAtLatticeX8(
OriginVoxels.X+GX0*Step,Step,
OriginVoxels.Y+gy*Step,OriginVoxels.Z+gz*Step,
BatchCount,Densities);
for(int32 Lane=0;Lane<BatchCount;++Lane)
{
const int32 GX=GX0+Lane;
const int32 LocalIndex=
((gz+1)*MDim+(gy+1))*MDim+(GX+1);
DensityGrid[LocalIndex]=Densities[Lane];
}
const int32 LastGX=GX0+BatchCount-1;
const int32 ReachBlockX=FMath::Clamp(LastGX,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;
if(OutDensitySampleCount)
*OutDensitySampleCount+=BatchCount;
Offset+=static_cast<size_t>(BatchCount);
}
}
else
{
for(const int32 gx:XGroup)SampleGridPoint(gx,gy,gz);
}
}
}
}
}
if (VoxelDensityProfile::AreCountersEnabled())
{
const uint64 GridSampleCount = static_cast<uint64>(GzHi - GzLo + 1)
* static_cast<uint64>(MDim)
* static_cast<uint64>(MDim);
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::MesherDensityGridBytes,
GridSampleCount * sizeof(float));
if (bUseSharedDensityGrid)
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::MesherSharedGridReadBytes,
GridSampleCount * sizeof(float));
}
}
if (bUseOperatorBlock)
{
Generator->EndDensityBlock();
}
if (OutDebugDensitySamples)
{
const size_t SampleCount = static_cast<size_t>(GzHi - GzLo + 1)
* static_cast<size_t>(MDim) * static_cast<size_t>(MDim);
OutDebugDensitySamples->reserve(SampleCount);
for (int32 gz = GzLo; gz <= GzHi; ++gz)
for (int32 gy = -1; gy <= GridDim; ++gy)
for (int32 gx = -1; gx <= GridDim; ++gx)
{
const int32 LocalIndex = ((gz + 1) * MDim + (gy + 1)) * MDim + (gx + 1);
OutDebugDensitySamples->push_back(DensityGrid[LocalIndex]);
}
}
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<<Level,
// donc chaque point de grille = exactement une cellule du clipmap de densité), on recopie les
// CHUNK_SIZE³ points INTÉRIEURS (g=0..CHUNK_SIZE-1, on exclut le point frontière +1 — il
// appartient à la tuile voisine — et l'anneau de marge ±1) dans OutCaptureGrid, quantifiés.
// UVoxelDensityVolume réutilise ces octets au lieu de re-sampler GetDensityAt. Pure lecture de
// DensityGrid : la forme de grille, la boucle deux passes, l'anneau de marge et la réutilisation
// thread_local restent intacts (§8.10). Ordre X→Y→Z (x rapide) = layout attendu par l'ingest.
if (OutCaptureGrid && CellsPerAxis == CHUNK_SIZE)
{
OutCaptureGrid->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));
};
//=========================================================================
// 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<int32> GroundTris;
static thread_local TArray<int32> CapTris;
GroundTris.Reset();
CapTris.Reset();
const bool bProfileMesher = VoxelDensityProfile::IsCycleTimingEnabled();
uint64 CellClassificationCalls = 0;
uint64 SurfaceCellCalls = 0;
uint8 CaseIndexRow[CHUNK_SIZE]{};
uint8 SurfaceRow[CHUNK_SIZE]{};
uint8 SampleClassificationRow[CHUNK_SIZE]{};
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();
FMemory::Memzero(SampleClassificationRow, sizeof(SampleClassificationRow));
const uint64 ClassificationRowStart = bProfileMesher
? FPlatformTime::Cycles64() : 0;
int32 ClassificationRowCalls = 0;
for (int32 GroupX = 0; GroupX < CellsPerAxis; GroupX += 8)
{
const int32 GroupCount = FMath::Min(8, CellsPerAxis - GroupX);
if (bUseProofBlocks
&& BlockSkip[((cz / 8) * BlocksPerAxis + (cy / 8)) * BlocksPerAxis
+ (GroupX / 8)] != 0)
{
continue;
}
if (GroupCount == 8)
{
const float* Row00 = DensityGrid.GetData()
+ ((cz + 1) * MDim + (cy + 1)) * MDim + GroupX + 1;
const float* Row01 = Row00 + MDim;
const float* Row10 = Row00 + MDim * MDim;
const float* Row11 = Row10 + MDim;
const __m256 Iso = _mm256_set1_ps(IsoLevel);
const int32 Masks[8] = {
_mm256_movemask_ps(_mm256_cmp_ps(_mm256_loadu_ps(Row00), Iso, _CMP_GE_OQ)),
_mm256_movemask_ps(_mm256_cmp_ps(_mm256_loadu_ps(Row00 + 1), Iso, _CMP_GE_OQ)),
_mm256_movemask_ps(_mm256_cmp_ps(_mm256_loadu_ps(Row01 + 1), Iso, _CMP_GE_OQ)),
_mm256_movemask_ps(_mm256_cmp_ps(_mm256_loadu_ps(Row01), Iso, _CMP_GE_OQ)),
_mm256_movemask_ps(_mm256_cmp_ps(_mm256_loadu_ps(Row10), Iso, _CMP_GE_OQ)),
_mm256_movemask_ps(_mm256_cmp_ps(_mm256_loadu_ps(Row10 + 1), Iso, _CMP_GE_OQ)),
_mm256_movemask_ps(_mm256_cmp_ps(_mm256_loadu_ps(Row11 + 1), Iso, _CMP_GE_OQ)),
_mm256_movemask_ps(_mm256_cmp_ps(_mm256_loadu_ps(Row11), Iso, _CMP_GE_OQ)),
};
for (int32 Lane = 0; Lane < 8; ++Lane)
{
const int32 CellX = GroupX + Lane;
int32 CaseIndex = 0;
for (int32 Corner = 0; Corner < 8; ++Corner)
{
CaseIndex |= ((Masks[Corner] >> Lane) & 1) << Corner;
}
CaseIndexRow[CellX] = static_cast<uint8>(CaseIndex);
SurfaceRow[CellX] = MCEdgeTable[CaseIndex] != 0;
SampleClassificationRow[CellX] = bProfileMesher
&& VoxelDensityProfile::ShouldSample(
VoxelDensityProfile::EBucket::MesherCellClassification);
++ClassificationRowCalls;
++CellClassificationCalls;
}
}
else
{
for (int32 Lane = 0; Lane < GroupCount; ++Lane)
{
const int32 CellX = GroupX + Lane;
int32 CaseIndex = 0;
for (int32 Corner = 0; Corner < 8; ++Corner)
{
const float Density = SampleG(
CellX + CornerOffsets[Corner].X,
cy + CornerOffsets[Corner].Y,
cz + CornerOffsets[Corner].Z);
if (Density >= IsoLevel) CaseIndex |= 1 << Corner;
}
CaseIndexRow[CellX] = static_cast<uint8>(CaseIndex);
SurfaceRow[CellX] = MCEdgeTable[CaseIndex] != 0;
SampleClassificationRow[CellX] = bProfileMesher
&& VoxelDensityProfile::ShouldSample(
VoxelDensityProfile::EBucket::MesherCellClassification);
++ClassificationRowCalls;
++CellClassificationCalls;
}
}
}
if (bProfileMesher && ClassificationRowCalls > 0)
{
const uint64 RowCycles = FPlatformTime::Cycles64() - ClassificationRowStart;
const uint64 PerCellCycles = RowCycles
/ static_cast<uint64>(ClassificationRowCalls);
for (int32 cx = 0; cx < CellsPerAxis; ++cx)
{
if (SampleClassificationRow[cx])
{
VoxelDensityProfile::AddSampledMeasurement(
VoxelDensityProfile::EBucket::MesherCellClassification,
PerCellCycles);
}
}
}
for (int32 cx = 0; cx < CellsPerAxis; cx++)
{
if (bUseProofBlocks
&& BlockSkip[((cz / 8) * BlocksPerAxis + (cy / 8)) * BlocksPerAxis + (cx / 8)] != 0)
{
continue;
}
const bool bSampleCell = SampleClassificationRow[cx] != 0;
float Densities[8];
const int32 CaseIndex = CaseIndexRow[cx];
const bool bHasSurface = SurfaceRow[cx] != 0;
if (!bHasSurface) continue; // Pas de surface ici → skip
++SurfaceCellCalls;
// Density values are needed only after classification, for edge interpolation.
for (int32 i = 0; i < 8; i++)
{
Densities[i] = SampleG(cx + CornerOffsets[i].X,
cy + CornerOffsets[i].Y,
cz + CornerOffsets[i].Z);
}
// 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) * VOXEL_SIZE;
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];
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 (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];
const int32 Idx0 = GetOrCreateVertex(EdgeVertices[E0], EdgeNormals[E0]);
const int32 Idx1 = GetOrCreateVertex(EdgeVertices[E1], EdgeNormals[E1]);
const int32 Idx2 = GetOrCreateVertex(EdgeVertices[E2], EdgeNormals[E2]);
TArray<int32>* Dst = &GroundTris;
if (!bStandaloneSkipMaterialGroups)
{
// F17 — majority vote (at least two ceiling vertices => ceiling triangle).
const int32 CapVotes = (int32)VertexClasses[Idx0]
+ (int32)VertexClasses[Idx1]
+ (int32)VertexClasses[Idx2];
Dst = (CapVotes >= 2) ? &CapTris : &GroundTris;
}
Dst->Add(Idx0);
Dst->Add(Idx2);
Dst->Add(Idx1);
}
if (bSampleCell)
{
VoxelDensityProfile::AddSampledMeasurement(
VoxelDensityProfile::EBucket::MesherStreamBuilding,
FPlatformTime::Cycles64() - StreamBuildingStart);
}
}
}
}
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) * VOXEL_SIZE;
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 * VOXEL_SIZE;
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);
if (bEmitUV)
{
MeshData.UVs.Add(FVector2f(Pos.X / VOXEL_SIZE, Pos.Y / VOXEL_SIZE));
}
if (bEmitColors)
{
MeshData.Colors.Add(Col); // parallel with the other vertex arrays
}
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<int32>& 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 = bEmitColors ? MeshData.Colors[iA] : FColor::White;
const FColor CB = bEmitColors ? MeshData.Colors[iB] : FColor::White;
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<uint64>(MeshData.Vertices.Num()) * sizeof(FVector)
+ static_cast<uint64>(MeshData.Normals.Num()) * sizeof(FVector)
+ static_cast<uint64>(MeshData.UVs.Num()) * sizeof(FVector2D)
+ static_cast<uint64>(MeshData.Colors.Num()) * sizeof(FColor)
+ static_cast<uint64>(MeshData.Triangles.Num()) * sizeof(int32);
const uint64 OutputAllocatedBytes = static_cast<uint64>(MeshData.Vertices.GetAllocatedSize())
+ static_cast<uint64>(MeshData.Normals.GetAllocatedSize())
+ static_cast<uint64>(MeshData.UVs.GetAllocatedSize())
+ static_cast<uint64>(MeshData.Colors.GetAllocatedSize())
+ static_cast<uint64>(MeshData.Triangles.GetAllocatedSize());
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::MesherOutputArrayBytes,
OutputArrayBytes);
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::MesherOutputAllocatedBytes,
OutputAllocatedBytes);
}
return MeshData;
}