Tiles with passage-overlap score >= 9 split grid fill over up to 4 helper lanes (disjoint rows, same per-sample order, join before MC) when workers are idle. FI junction tile fill 173 -> 99 ms; estimated serial floor ~0.55-0.60 s. Readiness essentially unchanged (FI/TN LOD0-2 ~2.15 s @W12): the limit is now aggregate work, not single tiles. Unreal: LOD0-2 ordered by passage overlap before travel/distance (L80 modes kept); far parents built after LOD0-1, LOD2 gated on them; helper budget via ParallelFor. Unreal null-RHI FI arrival: LOD0-2 resident at 3.65 s. All tile hashes identical to L84 (four whole views). Fast tier PASS; Linux UE host builds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
266 lines
13 KiB
C++
266 lines
13 KiB
C++
// VoxelMarchingCubesMesher.h
|
||
// Mesh lisse à partir du champ de densité du générateur.
|
||
//
|
||
// Principe:
|
||
// 1. Chaque cellule du chunk (cube 2x2x2 voxels) lit la densité aux 8 coins.
|
||
// 2. Un index 8 bits indique quels coins sont au-dessus du IsoLevel.
|
||
// 3. Les tables MC (EdgeTable + TriTable) donnent les triangles à générer.
|
||
// 4. On interpole la position du vertex le long de chaque arête traversée.
|
||
//
|
||
// LOD: Step > 1 échantillonne moins souvent → moins de triangles à distance.
|
||
|
||
#pragma once
|
||
|
||
#include "CoreMinimal.h"
|
||
#include <atomic>
|
||
#include <cstddef>
|
||
#include <vector>
|
||
#include "VoxelTypes.h" // Pour FVoxelMeshData, CHUNK_SIZE, VOXEL_SIZE, etc.
|
||
#include "VoxelGenerator.h"
|
||
#include "VoxelMarchingCubesMesher.generated.h"
|
||
|
||
class FStandaloneShapeWorld;
|
||
|
||
// A run-scoped, read-only density lattice. The explorer owns the storage and installs the
|
||
// pointer only while its canonical tiles are being meshed. The lattice includes the one-point
|
||
// halo needed by every tile, so each world sample is generated once and copied into both adjacent
|
||
// tile scratch grids. It is deliberately not a UObject or a cache with eviction: ownership,
|
||
// lifetime, and iteration order remain explicit and deterministic.
|
||
struct FVoxelSharedDensityGrid
|
||
{
|
||
FIntVector OriginVoxels = FIntVector::ZeroValue;
|
||
int32 Step = 1;
|
||
int32 Dim = 0;
|
||
TArray<float> Samples;
|
||
|
||
void Reset()
|
||
{
|
||
OriginVoxels = FIntVector::ZeroValue;
|
||
Step = 1;
|
||
Dim = 0;
|
||
Samples.Reset();
|
||
}
|
||
};
|
||
|
||
// Host-owned scheduling hooks for a predicted-heavy density-grid fill. Each task owns one
|
||
// deterministic Z slab and gets the source tile's worker-local sampling context.
|
||
struct FVoxelGridFillExecution
|
||
{
|
||
using FTask = void(*)(void* taskContext, std::size_t index, std::size_t workerIndex);
|
||
using FParallelFor = double(*)(void* schedulerContext, std::size_t count,
|
||
std::size_t workerLimit, void* taskContext, FTask task);
|
||
using FBeginWorkerContext = void*(*)(void* context, std::size_t workerIndex);
|
||
using FEndWorkerContext = void(*)(void* context, void* workerState,
|
||
std::size_t workerIndex);
|
||
void* SchedulerContext = nullptr;
|
||
FParallelFor ParallelFor = nullptr;
|
||
std::size_t WorkerLimit = 1;
|
||
void* WorkerContext = nullptr;
|
||
FBeginWorkerContext BeginWorkerContext = nullptr;
|
||
FEndWorkerContext EndWorkerContext = nullptr;
|
||
};
|
||
|
||
// Optional, sampled timing output for the research audit; null in production calls.
|
||
// Mesures echantillonnees facultatives pour l'audit; pointeur nul en production.
|
||
struct FVoxelMarchingCubesStageTimings
|
||
{
|
||
uint32 GridFillWorkerCount = 1;
|
||
double DensityGridMilliseconds = 0.0;
|
||
double CellClassificationMilliseconds = 0.0;
|
||
double GradientNormalsMilliseconds = 0.0;
|
||
double EdgeInterpolationMilliseconds = 0.0;
|
||
double VertexDedupStreamMilliseconds = 0.0;
|
||
double SkirtMilliseconds = 0.0;
|
||
double OtherMilliseconds = 0.0;
|
||
uint64 GradientFallbackDensityCalls = 0;
|
||
uint64 GradientCalls = 0;
|
||
uint64 EdgeMidpointDensityCalls = 0;
|
||
uint64 EdgeMidpointDensityMemoHits = 0;
|
||
uint64 GradientMemoHits = 0;
|
||
uint64 X8DensityCalls = 0;
|
||
uint64 X8ActiveLanes = 0;
|
||
uint64 DensityGridInteriorSamples = 0;
|
||
uint64 DensityGridHaloSamples = 0;
|
||
uint64 CandidateCells = 0;
|
||
uint64 ZBandRejectedCells = 0;
|
||
uint64 CellClassificationCalls = 0;
|
||
uint64 SurfaceCellCalls = 0;
|
||
uint64 ProofSkippedSolidCells = 0;
|
||
uint64 ProofSkippedAirCells = 0;
|
||
uint64 ProofSkippedDensitySamples = 0;
|
||
uint64 SkirtEdgeCandidates = 0;
|
||
uint64 SkirtBoundaryCandidates = 0;
|
||
uint64 SkirtDensityProbes = 0;
|
||
};
|
||
|
||
// Opt-in soundness audit for density blocks which the Core mesher would skip.
|
||
// Audit de justesse facultatif pour les blocs de densite que Core sauterait.
|
||
struct FVoxelUniformBlockProofAuditStats
|
||
{
|
||
uint64 CheckedBlocks = 0;
|
||
uint64 CheckedSamples = 0;
|
||
uint64 FallbackBlocks = 0;
|
||
uint64 UnknownBlocks = 0;
|
||
uint64 Violations = 0;
|
||
};
|
||
|
||
// Stable plan data copied onto one worker-owned mesher for hybrid LOD0 generation.
|
||
// Donnees stables du plan copiees dans un mailleur de worker pour le mode hybride LOD0.
|
||
struct FVoxelHybridSharpFeatureBox
|
||
{
|
||
uint64 StableId = 0;
|
||
int64 MinimumSubvoxels[3] = {};
|
||
int64 MaximumSubvoxels[3] = {};
|
||
};
|
||
|
||
UCLASS(BlueprintType)
|
||
class VOXELFORGE_API UVoxelMarchingCubesMesher : public UObject
|
||
{
|
||
GENERATED_BODY()
|
||
|
||
public:
|
||
// Parse command-line switches on the module's game-thread startup. GenerateMesh is a worker
|
||
// entry point and therefore only reads the resulting values.
|
||
static void InitializeConsoleSwitches();
|
||
|
||
/**
|
||
* Génère le mesh d'une TUILE de clipmap (voir FVoxelTileKey).
|
||
*
|
||
* @param OriginVoxels - Coin min de la tuile en coords VOXEL (= Tile.OriginVoxels()).
|
||
* @param Step - Taille de cellule en voxels. La tuile couvre CellsPerAxis*Step voxels.
|
||
* @param CellsPerAxis - Nombre de cellules par axe. Les tuiles GROSSIÈRES en utilisent MOINS
|
||
* (gen moins chère, maillage plus grossier au loin) tout en couvrant la
|
||
* même étendue (extent = CellsPerAxis*Step). Niveau 0 = CHUNK_SIZE.
|
||
* @param OutCaptureGrid - CAPTURE-DURING-MESHING (optionnel). Si non-null ET CellsPerAxis==CHUNK_SIZE
|
||
* (tuile pleine résolution, Step==1<<Level, donc 1:1 avec une cellule du clipmap
|
||
* de densité), on y recopie les CHUNK_SIZE³ points intérieurs de la grille de
|
||
* densité déjà échantillonnée, quantifiés via VF_QuantizeDensity. Cela évite à
|
||
* UVoxelDensityVolume de re-sampler GetDensityAt pour ces cellules (le mesher
|
||
* les a déjà calculées). Vidé puis rempli ; reste vide si non éligible.
|
||
* @param BandZMinVox / BandZMaxVox - COUPE DE CONTENU PAR STRATE (optionnel, voxels Z INCLUSIFS,
|
||
* cf. UVoxelSettings::StrateContentCutMinLevel) : seules les cellules dont
|
||
* l'intervalle Z chevauche la bande sont maillées. Le runtime ne passe
|
||
* jamais de bande aux tuiles LOD0, car elles possèdent la collision du pawn.
|
||
* Une tuile grossière qui
|
||
* chevauche une frontière de strate ne maille que la strate du joueur —
|
||
* supprime les trous d'aliasing (le bouchon seal/gap plus fin que Step
|
||
* tombait entre deux points du treillis) et le mélange de matériaux entre
|
||
* strates. Les cellules maillées restent identiques au bit près (mêmes
|
||
* échantillons monde purs). Jamais combiné avec OutCaptureGrid.
|
||
*/
|
||
FVoxelMeshData GenerateMesh(FIntVector OriginVoxels, int32 Step = 1, int32 CellsPerAxis = CHUNK_SIZE,
|
||
TArray<uint8>* OutCaptureGrid = nullptr,
|
||
int32 BandZMinVox = INT32_MIN, int32 BandZMaxVox = INT32_MAX,
|
||
int64* OutDensitySampleCount = nullptr,
|
||
FVoxelSharedDensityGrid* OutPreEditDensityGrid = nullptr,
|
||
const FVoxelSharedDensityGrid* InSharedDensityGrid = nullptr,
|
||
std::vector<float>* OutDebugDensitySamples = nullptr,
|
||
FVoxelMarchingCubesStageTimings* OutStageTimings = nullptr,
|
||
FVoxelUniformBlockProofAuditStats* OutBlockProofAudit = nullptr,
|
||
bool bForceFullDensityCapture = false);
|
||
|
||
//=========================================================================
|
||
// SERVICES (injectés par AVoxelWorld)
|
||
//=========================================================================
|
||
|
||
// Le générateur fournit la densité par voxel.
|
||
UPROPERTY()
|
||
const UVoxelGenerator* Generator = nullptr;
|
||
|
||
void SetGenerator(const UVoxelGenerator* InGenerator) { Generator = InGenerator; }
|
||
|
||
// Core tile coordinates use 25 cm voxels; legacy generation keeps 12.5 cm.
|
||
// Les coordonnees des tuiles Core utilisent des voxels de 25 cm; le legacy reste a 12,5 cm.
|
||
void SetGeometryVoxelSizeCm(float InVoxelSizeCm) { GeometryVoxelSizeCm = InVoxelSizeCm; }
|
||
|
||
// AVoxelWorld installs its bShuttingDown flag here. The commandlet path leaves it null.
|
||
// This is intentionally read-only: workers may observe shutdown, but never mutate world state.
|
||
void SetShutdownFlag(const std::atomic<bool>* InShutdownFlag) { ShutdownFlag = InShutdownFlag; }
|
||
|
||
// Optional run-scoped shared lattice. The caller must keep it alive and immutable until all
|
||
// GenerateMesh calls finish. A malformed/incomplete lattice is ignored and falls back to
|
||
// the canonical generator path rather than changing geometry.
|
||
void SetSharedDensityGrid(const FVoxelSharedDensityGrid* InGrid) { SharedDensityGrid = InGrid; }
|
||
void SetGridFillExecution(const FVoxelGridFillExecution& InExecution)
|
||
{ GridFillExecution = InExecution; }
|
||
|
||
// Standalone's interval world is an explicit immutable sampling context. Game builds do not
|
||
// set this pointer and continue through Generator.
|
||
void SetStandaloneShapeWorld(const FStandaloneShapeWorld* InWorld) { ShapeWorldContext = InWorld; }
|
||
|
||
// The caller owns this mesher instance for one tile task; no shared mutable feature state.
|
||
// L'appelant possede cette instance pour une tuile; aucune donnee partagee modifiable.
|
||
void SetHybridSharpFeatures(TArray<FVoxelHybridSharpFeatureBox>&& InFeatures)
|
||
{
|
||
HybridSharpFeatures = MoveTemp(InFeatures);
|
||
bUseHybridSharpMesher = true;
|
||
}
|
||
|
||
// Version 2 keeps MC topology and projects eligible LOD0 edge roots onto exact box planes.
|
||
void SetHybridSharpFeatureSnap(TArray<FVoxelHybridSharpFeatureBox>&& InFeatures)
|
||
{
|
||
HybridSharpFeatures = MoveTemp(InFeatures);
|
||
bUseHybridSharpFeatureSnap = true;
|
||
}
|
||
|
||
private:
|
||
FORCEINLINE bool ShouldAbortWork() const
|
||
{
|
||
return ShutdownFlag && ShutdownFlag->load(std::memory_order_relaxed);
|
||
}
|
||
|
||
const std::atomic<bool>* ShutdownFlag = nullptr;
|
||
const FVoxelSharedDensityGrid* SharedDensityGrid = nullptr;
|
||
FVoxelGridFillExecution GridFillExecution;
|
||
const FStandaloneShapeWorld* ShapeWorldContext = nullptr;
|
||
TArray<FVoxelHybridSharpFeatureBox> HybridSharpFeatures;
|
||
bool bUseHybridSharpMesher = false;
|
||
bool bUseHybridSharpFeatureSnap = false;
|
||
float GeometryVoxelSizeCm = VOXEL_SIZE;
|
||
|
||
public:
|
||
|
||
//=========================================================================
|
||
// SETTINGS
|
||
//=========================================================================
|
||
|
||
// Optional standalone benchmark switches. Runtime callers leave all false and retain the
|
||
// complete FVoxelMeshData contract consumed by the game materials and section setup.
|
||
bool bStandaloneSkipUvAndColors = false;
|
||
bool bStandaloneSkipUV = false;
|
||
bool bStandaloneSkipColors = false;
|
||
bool bStandaloneSkipMaterialGroups = false;
|
||
bool bStandaloneEnableGradientMemoization = true;
|
||
bool bStandaloneEnableExactMidpointDensityMemo = false;
|
||
bool bStandaloneEnableActiveLaneBatching = false;
|
||
int32 bStandaloneProofBlockCells = 8;
|
||
|
||
// Seuil de l'isosurface dans le champ de densité.
|
||
// Convention MC: densité < IsoLevel = solide, >= = air.
|
||
float IsoLevel = 0.0f;
|
||
|
||
// SKIRTS — bouchent les fissures aux frontières de tuiles entre niveaux de clipmap voisins
|
||
// (résolutions différentes → les iso-surfaces ne se rejoignent pas exactement). Une jupe
|
||
// (mur court) est extrudée vers le solide depuis chaque arête de surface posée sur une des 6
|
||
// faces externes de la tuile. Voir GenerateMesh.
|
||
bool bGenerateSkirts = true;
|
||
// Profondeur de la jupe, en CELLULES de la tuile (× Step × VOXEL_SIZE). Four L0 cells preserve
|
||
// the 0.5 m default extrusion at 12.5 cm per voxel.
|
||
float SkirtCells = 4.0f;
|
||
|
||
// Conservative 8x8x8 block classifier. Uniform blocks skip the marching-cubes cell loop;
|
||
// when every relevant block has the same verdict, the density grid is skipped as well.
|
||
// ClassifyTile is proof-only, so no samples, resolution, or geometry are removed.
|
||
// The tile classifier is the cheap, whole-tile proof. Keep the optional 8^3-block refinement
|
||
// available for focused diagnostics, but leave it disabled in streaming: repeating a proof on
|
||
// every mixed tile can cost more than the density grid it is meant to avoid.
|
||
bool bUseBlockEarlyOut = false;
|
||
|
||
// T2.b — LOD-aware octave reduction (opt-in, copied from UVoxelSettings::LODOctaveDrop).
|
||
// Octaves dropped from per-voxel volumetric noise PER Step doubling: a tile at Step=S
|
||
// drops LODOctaveDrop * log2(S) octaves (see VoxelGenLOD in VoxelGenerator.h).
|
||
// 0 (default) = off — every LOD samples full octaves, byte-identical to before.
|
||
// Réduction d'octaves sur les tuiles grossières ; 0 = désactivé.
|
||
int32 LODOctaveDrop = 0;
|
||
};
|