perf(gen): interval propagation -- blocks can finally be PROVEN empty. 1.55s -> 1.21s
Task 4 ran 11 hours and was killed by system memory pressure before it could file a report. The work survived, builds clean (0 errors), and is verified faster with byte-identical output. This commit is my verification, not its summary -- it never got to write one. THE STRUCTURAL FIX0e9df9eestablished why no block could ever be skipped: OriginSpineOp, PassageCarveOp and the active seals Lerp/Min/Max against the CURRENT density, so with an unknown incoming density their supremum is genuinely unbounded. Not a missing bound -- the operator semantics. The answer is to propagate a density INTERVAL through the stack, and that is what landed: FVoxelBoxSdfInterval + PropagateSdfOverBox on the operator interface, an interval fold through every converter and modifier, interval arithmetic down into the noise primitives themselves (gradient corners, warp, worm frequencies), and a BlockSkip lattice the mesher consults per 8^3 block. Its own guiding comment, which is the right default: "no finite interval is a proof. Unknown costs the skip and protects the geometry." 128^3 TunnelNetwork seed 1337 slot 1, freshly built binary, 3 runs each: before 1.515 / 1.554 / 1.569 s after 1.213 / 1.258 / 1.171 s -22.5%, 1.28x -- clears the 17% floor Geometry SHA-256 98eaad61..d3c4f IDENTICAL to0e9df9e. 153,740 triangles either way. It skips only provably-empty work; nothing about the world changed. It also fixed one cause of the 5.6 s empty tiles found in0e9df9e: an LOD4 tile legitimately spans 19^3 = 6,859 chunk keys and was rebuilding per-chunk state thousands of times within a single tile. A shared cache removed that failure mode; remaining slow empties were LOD0 and are a separate fast-classifier bail, still open. Partially landed, unfinished when killed: RequestStartCycles on FChunkResult, to report request-to-ready latency rather than worker generation time. Harmless as is. ⚠️ WHAT KILLED IT, AND IT IS OURS A single UnrealEditor-Cmd commandlet grew to 19.5 GB, leaving 1.3 GB free on a 32 GB machine, and the OS killed the agent. Freeing it restored 21.6 GB. The owner said twice that his machine should not be under memory pressure and he was right both times: first the 411 MiB worker caches, now a 19.5 GB commandlet. A generation run must not need 19.5 GB. Next task. Cumulative on this region: 342.2 -> 16.2 -> 2.62 -> 1.49 -> 1.21 s (~283x). 3,374 insertions / 342 deletions across 12 files. Build 0 errors. Nothing outside Plugins/VoxelForge; RealtimeMeshComponent untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
This commit is contained in:
1 parent
0e9df9e5a7
commit
d38d4cabef
12 files changed
+3298
-266
No files matched your search
@@ -33,6 +33,44 @@
|
||||
#include "VoxelStrateMeasure.h"
|
||||
#include "VoxelTypes.h" // Pour VOXEL_NOISE_SCALE, SmoothStep01
|
||||
#include "VoxelStrateTypes.h"
|
||||
#include "HAL/PlatformTime.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
struct FTask4BuildProbeScope
|
||||
{
|
||||
FChunkSDFCache& Cache;
|
||||
const float MinX;
|
||||
const float MinY;
|
||||
const float MaxX;
|
||||
const float MaxY;
|
||||
const uint64 StartCycles;
|
||||
const bool bEnabled;
|
||||
|
||||
FTask4BuildProbeScope(FChunkSDFCache& InCache, float InMinX, float InMinY,
|
||||
float InMaxX, float InMaxY)
|
||||
: Cache(InCache), MinX(InMinX), MinY(InMinY), MaxX(InMaxX), MaxY(InMaxY)
|
||||
, StartCycles(FPlatformTime::Cycles64())
|
||||
, bEnabled((InMaxX - InMinX) > 120.0f && (InMaxX - InMinX) < 220.0f
|
||||
&& (InMaxY - InMinY) > 120.0f && (InMaxY - InMinY) < 220.0f
|
||||
&& InMinX > -512.0f && InMaxX < 512.0f
|
||||
&& InMinY > -512.0f && InMaxY < 512.0f)
|
||||
{}
|
||||
|
||||
~FTask4BuildProbeScope()
|
||||
{
|
||||
if (bEnabled)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("[Task4BuildProbe] search=(%.0f,%.0f)-(%.0f,%.0f) rooms=%d joins=%d tunnels=%d pits=%d chimneys=%d ms=%.3f"),
|
||||
MinX, MinY, MaxX, MaxY, Cache.Rooms.Num(), Cache.RoomFloorJoins.Num(),
|
||||
Cache.Tunnels.Num(), Cache.Pits.Num(), Cache.Chimneys.Num(),
|
||||
FPlatformTime::ToMilliseconds64(
|
||||
FPlatformTime::Cycles64() - StartCycles));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
#include "VoxelNoise.h" // Pure FBM used by the slab landing query
|
||||
#include "VoxelPassageGeometry.h"
|
||||
#include "VoxelTerrainOpDefinition.h"
|
||||
@@ -2828,6 +2866,8 @@ void VoxelCaveMorphology::BuildChunkCache(
|
||||
uint32 Seed, int32 StrateIndex,
|
||||
const TArray<FStrateTerrainOpEntry>* TerrainOps)
|
||||
{
|
||||
FTask4BuildProbeScope Task4BuildProbe(OutCache, SearchMinX, SearchMinY, SearchMaxX, SearchMaxY);
|
||||
|
||||
// Clear previous data (arrays keep their allocation for reuse)
|
||||
OutCache.Rooms.Reset();
|
||||
OutCache.RoomFloorJoins.Reset();
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -522,10 +522,9 @@ namespace
|
||||
// result. Native TunnelNetwork/Underwater states are immutable after construction; the active
|
||||
// pointer below lets the hot sample path read the stored stack and tunnel-core cache without
|
||||
// moving them on every voxel.
|
||||
// The cache is open-addressed and bounded. The 128-slot budget is evidence-sized: the
|
||||
// 128³ production export measured at most 102 live tunnel entries on one worker, leaving
|
||||
// 26 slots of headroom; the old 4,096-slot experiment reserved 3.34 MiB per worker before
|
||||
// one operator or SDF allocation existed. Collisions only cause a safe deterministic rebuild.
|
||||
// The cache is open-addressed and bounded. Keep the worker footprint at the established
|
||||
// 128-entry cap; the expensive native tunnel-core cache is not prepared for an operator-stack
|
||||
// chunk because the stack publishes the same result to the common post below.
|
||||
constexpr int32 TunnelDensityCacheSlotCount = 128;
|
||||
|
||||
struct FTunnelNetworkDensityCacheEntry
|
||||
@@ -983,6 +982,20 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
|
||||
const uint32 LayoutVersion = StrateManager->GetLayoutVersion();
|
||||
const uint64 ManagerLifetimeId = StrateManager->GetCacheLifetimeId();
|
||||
// TunnelNetwork/Underwater operator-stack parameters are selected from chunk Z only.
|
||||
// A coarse tile samples thousands of exact XY chunk keys, but those keys describe the
|
||||
// same immutable stack whenever the selected archetype is operator-stack backed. Share
|
||||
// that prepared state by Z; the stack still queries the actual world position, and its
|
||||
// room-graph cache remains keyed by the real XY search window. Legacy tunnel states keep
|
||||
// the full XYZ key because their native cache is built for one XY window.
|
||||
const ECaveGeneratorType QueryGeneratorType =
|
||||
StrateManager->GetGeneratorTypeForChunk(ChunkCoord);
|
||||
const bool bShareTunnelStackByZ = !bIsGapChunk
|
||||
&& (QueryGeneratorType == ECaveGeneratorType::TunnelNetwork
|
||||
|| QueryGeneratorType == ECaveGeneratorType::Underwater)
|
||||
&& StrateManager->UsesOperatorStackForChunk(ChunkCoord);
|
||||
const FIntVector TunnelCacheKey = bShareTunnelStackByZ
|
||||
? FIntVector(0, 0, ChunkCoord.Z) : ChunkCoord;
|
||||
FVoxelOpStack* ActiveOpStack = &CP_OpStack;
|
||||
FTunnelCoreCacheState* ActiveTunnelCoreCache = >unnelCoreCache;
|
||||
bool bLoadedTunnelDensityCache = false;
|
||||
@@ -995,7 +1008,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
auto FindTunnelDensityCacheEntry = [&](const FIntVector& QueryChunk,
|
||||
FTunnelNetworkDensityCacheEntry*& OutEntry) -> bool
|
||||
{
|
||||
const int32 StartSlot = TunnelDensityCacheSlot(QueryChunk);
|
||||
const int32 StartSlot = TunnelDensityCacheSlot(TunnelCacheKey);
|
||||
FTunnelNetworkDensityCacheEntry* ReuseEntry = nullptr;
|
||||
for (int32 Probe = 0; Probe < TunnelDensityCacheSlotCount; ++Probe)
|
||||
{
|
||||
@@ -1008,8 +1021,9 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
continue;
|
||||
}
|
||||
const bool bKeyMatches = Candidate.ManagerLifetimeId == ManagerLifetimeId
|
||||
&& Candidate.Chunk == QueryChunk
|
||||
&& Candidate.Chunk == TunnelCacheKey
|
||||
&& Candidate.LayoutVersion == LayoutVersion
|
||||
&& Candidate.bUseOpStack == bShareTunnelStackByZ
|
||||
&& (Candidate.GenType == ECaveGeneratorType::TunnelNetwork
|
||||
|| Candidate.GenType == ECaveGeneratorType::Underwater);
|
||||
if (bKeyMatches)
|
||||
@@ -1155,23 +1169,6 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
// silently fall back to the old one-archetype switch.
|
||||
CP_UseOpStack = CP_UseOpStack || CP_UseComposerRegions;
|
||||
#endif
|
||||
bool bUseNativeTunnelCore = !CP_UseCustomRecipe
|
||||
&& (CP_GenType == ECaveGeneratorType::TunnelNetwork
|
||||
|| CP_GenType == ECaveGeneratorType::Underwater);
|
||||
#if WITH_EDITOR
|
||||
bUseNativeTunnelCore = bUseNativeTunnelCore && !CP_UseComposerRegions;
|
||||
#endif
|
||||
if (bUseNativeTunnelCore)
|
||||
{
|
||||
PrepareTunnelCoreCache(
|
||||
*StrateManager, ChunkCoord, CP_Tunnel, CP_TunnelFP,
|
||||
LayoutVersion, ManagerLifetimeId, DensityCacheOwnerId, GTunnelCoreCache);
|
||||
}
|
||||
else
|
||||
{
|
||||
GTunnelCoreCache.bValid = false;
|
||||
}
|
||||
|
||||
if (CP_UseOpStack)
|
||||
{
|
||||
CP_OpStack = FVoxelOpStack(); // move-assign : libère l'ancienne pile
|
||||
@@ -1278,6 +1275,31 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
}
|
||||
}
|
||||
|
||||
// The legacy path needs its prepared native tunnel-core cache. An operator-stack
|
||||
// tunnel source publishes the same world-space result after EvalMC, so preparing a
|
||||
// second full graph/core cache here would only duplicate the work that made coarse
|
||||
// streaming expensive in the first place. Custom/composer paths retain the legacy
|
||||
// cache unless their own stack supplies the hand-off.
|
||||
const bool bNativeTunnelCore = !CP_UseCustomRecipe
|
||||
&& (CP_GenType == ECaveGeneratorType::TunnelNetwork
|
||||
|| CP_GenType == ECaveGeneratorType::Underwater);
|
||||
#if WITH_EDITOR
|
||||
const bool bCanPrepareNativeTunnelCore = bNativeTunnelCore
|
||||
&& (!CP_UseOpStack || CP_UseComposerRegions);
|
||||
#else
|
||||
const bool bCanPrepareNativeTunnelCore = bNativeTunnelCore && !CP_UseOpStack;
|
||||
#endif
|
||||
if (bCanPrepareNativeTunnelCore)
|
||||
{
|
||||
PrepareTunnelCoreCache(
|
||||
*StrateManager, ChunkCoord, CP_Tunnel, CP_TunnelFP,
|
||||
LayoutVersion, ManagerLifetimeId, DensityCacheOwnerId, GTunnelCoreCache);
|
||||
}
|
||||
else
|
||||
{
|
||||
GTunnelCoreCache.bValid = false;
|
||||
}
|
||||
|
||||
const bool bNativeTunnelCacheCandidate = !CP_UseCustomRecipe
|
||||
&& (CP_GenType == ECaveGeneratorType::TunnelNetwork
|
||||
|| CP_GenType == ECaveGeneratorType::Underwater);
|
||||
@@ -1295,7 +1317,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
CachedEntry.bValid = false;
|
||||
CachedEntry.OwnerId = DensityCacheOwnerId;
|
||||
CachedEntry.ManagerLifetimeId = ManagerLifetimeId;
|
||||
CachedEntry.Chunk = ChunkCoord;
|
||||
CachedEntry.Chunk = TunnelCacheKey;
|
||||
CachedEntry.LayoutVersion = LayoutVersion;
|
||||
CachedEntry.GenType = CP_GenType;
|
||||
CachedEntry.Tunnel = CP_Tunnel;
|
||||
@@ -1484,7 +1506,12 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
// bridge over a graph tunnel. Reassert the native cached tunnel core here, after every
|
||||
// solid floor writer but before the global XY seal. This cache is built once per chunk,
|
||||
// never once per voxel.
|
||||
if (ActiveTunnelCoreCache->bValid)
|
||||
if (ActiveTunnelCoreCache->bValid || CP_UseOpStack)
|
||||
{
|
||||
FTunnelCoreWorldEvaluation TunnelCore;
|
||||
bool bHaveTunnelCore = CP_UseOpStack
|
||||
&& ActiveOpStack->TryGetLastTunnelCoreWorldEvaluation(TunnelCore);
|
||||
if (!bHaveTunnelCore && ActiveTunnelCoreCache->bValid)
|
||||
{
|
||||
const FTunnelSupportFloorColumn* SupportColumn = nullptr;
|
||||
FTunnelSupportFloorColumn EmptySupportColumn;
|
||||
@@ -1499,14 +1526,15 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
ActiveTunnelCoreCache->Cache, IX, IY, EmptySupportColumn);
|
||||
}
|
||||
|
||||
FTunnelCoreWorldEvaluation TunnelCore;
|
||||
{
|
||||
VoxelDensityProfile::FScopedTimer ProfileTimer(
|
||||
VoxelDensityProfile::EBucket::TunnelCorePosts);
|
||||
TunnelCore = VoxelCaveMorphology::EvaluateTunnelCoreWorld(
|
||||
WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache,
|
||||
SupportColumn);
|
||||
bHaveTunnelCore = true;
|
||||
}
|
||||
if (bHaveTunnelCore)
|
||||
{
|
||||
const bool bTunnelSupportFloor = TunnelCore.bSupportFloor;
|
||||
if (bTunnelSupportFloor)
|
||||
{
|
||||
@@ -1536,6 +1564,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
StrateManager->ApplyPassageLandingRoomFloorMC(
|
||||
Result, WorldX, WorldY, WorldZ, LandingBaseDensity);
|
||||
}
|
||||
}
|
||||
DensityStructuralPostsTimer.End();
|
||||
}
|
||||
else
|
||||
@@ -3468,17 +3497,284 @@ bool UVoxelGenerator::GetSurfaceHeightAt(float WorldX, float WorldY, int32 Chunk
|
||||
// disturbances/diff. Tout ce qui n'est pas prouvable ⇒ Mixed (le seul coût d'un
|
||||
// faux Mixed est du CPU ; un faux AllSolid/AllAir serait un trou).
|
||||
|
||||
// A full tile can be Mixed even when every smaller region is provably the same class. This is
|
||||
// particularly common at level 0: broad operator bounds overlap an unbounded carve, while the
|
||||
// actual tile-sized sub-boxes are all air. Refine only an already-Mixed proof. Every child must
|
||||
// resolve, and all resolved children must agree; an unresolved or conflicting child remains Mixed.
|
||||
// This is a proof refinement, never a density/sample shortcut.
|
||||
static EVoxelTileClass VF_ClassifyBoxRefined(const FVoxelOpStack& Stack,
|
||||
const FBox& VoxelBox,
|
||||
const FVoxelOpContext& Context,
|
||||
const UVoxelGenerator* Generator,
|
||||
const UVoxelStrateManager* StructuralManager)
|
||||
{
|
||||
// The ordinary proof stays shallow. The finite-domain warp retry is paid only by a
|
||||
// tile that already failed the cheap proof, and can afford one more split level because
|
||||
// its purpose is to discharge the LOD0 false-Mixed cases without sampling the field.
|
||||
const int32 MaxRefinementDepth = Context.bTightenWarpProof ? 5 : 2;
|
||||
const bool bTask4TimingProbe = Context.bUseLatticeProof
|
||||
&& Context.LatticeOriginVoxels == FIntVector(32, 0, -32)
|
||||
&& Context.Step == 1;
|
||||
int32 RefineNodeCount = 0;
|
||||
int32 ExactLeafCount = 0;
|
||||
int32 ExactSampleCount = 0;
|
||||
uint64 StackClassifyCycles = 0;
|
||||
uint64 ExactSampleCycles = 0;
|
||||
const bool bTask4ExactProbe = Context.bUseLatticeProof
|
||||
&& (Context.LatticeOriginVoxels == FIntVector(-64, -64, -64)
|
||||
|| Context.LatticeOriginVoxels == FIntVector(-64, -96, -64))
|
||||
&& Context.Step == 1;
|
||||
auto ClassifyExactLatticeLeaf = [&](const FBox& Box, bool bUseFinalField) -> EVoxelTileClass
|
||||
{
|
||||
if (!Context.bUseLatticeProof || Context.Step <= 0)
|
||||
{
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
|
||||
const float Step = static_cast<float>(Context.Step);
|
||||
const FVector Origin(static_cast<float>(Context.LatticeOriginVoxels.X),
|
||||
static_cast<float>(Context.LatticeOriginVoxels.Y),
|
||||
static_cast<float>(Context.LatticeOriginVoxels.Z));
|
||||
const int32 IX0 = FMath::CeilToInt((static_cast<float>(Box.Min.X) - Origin.X) / Step - 1.0e-4f);
|
||||
const int32 IY0 = FMath::CeilToInt((static_cast<float>(Box.Min.Y) - Origin.Y) / Step - 1.0e-4f);
|
||||
const int32 IZ0 = FMath::CeilToInt((static_cast<float>(Box.Min.Z) - Origin.Z) / Step - 1.0e-4f);
|
||||
const int32 IX1 = FMath::FloorToInt((static_cast<float>(Box.Max.X) - Origin.X) / Step + 1.0e-4f);
|
||||
const int32 IY1 = FMath::FloorToInt((static_cast<float>(Box.Max.Y) - Origin.Y) / Step + 1.0e-4f);
|
||||
const int32 IZ1 = FMath::FloorToInt((static_cast<float>(Box.Max.Z) - Origin.Z) / Step + 1.0e-4f);
|
||||
const int64 Count = ((int64)IX1 - IX0 + 1)
|
||||
* ((int64)IY1 - IY0 + 1)
|
||||
* ((int64)IZ1 - IZ0 + 1);
|
||||
if (IX1 < IX0 || IY1 < IY0 || IZ1 < IZ0 || Count <= 0 || Count > 256)
|
||||
{
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
|
||||
bool bAllSolid = true;
|
||||
bool bAllAir = true;
|
||||
int32 PositiveCount = 0;
|
||||
int32 NegativeCount = 0;
|
||||
int32 ZeroOrInvalidCount = 0;
|
||||
float MinDensity = FLT_MAX;
|
||||
float MaxDensity = -FLT_MAX;
|
||||
for (int32 IZ = IZ0; IZ <= IZ1; ++IZ)
|
||||
for (int32 IY = IY0; IY <= IY1; ++IY)
|
||||
for (int32 IX = IX0; IX <= IX1; ++IX)
|
||||
{
|
||||
const float WorldX = Origin.X + static_cast<float>(IX) * Step;
|
||||
const float WorldY = Origin.Y + static_cast<float>(IY) * Step;
|
||||
const float WorldZ = Origin.Z + static_cast<float>(IZ) * Step;
|
||||
// The operator stack is the cheap proof field. A passage/landing post is applied
|
||||
// after that stack by GetDensityAt; for a small child that intersects one of those
|
||||
// exact lattice modifiers, certify the final MC-facing field instead of trusting a
|
||||
// core-stack verdict that is deliberately conservative at the post boundary.
|
||||
const uint64 SampleStartCycles = bTask4TimingProbe ? FPlatformTime::Cycles64() : 0;
|
||||
const float Density = bUseFinalField
|
||||
? -Generator->GetDensityAt(WorldX, WorldY, WorldZ)
|
||||
: Stack.EvalInternal(WorldX, WorldY, WorldZ);
|
||||
if (bTask4TimingProbe)
|
||||
{
|
||||
ExactSampleCycles += FPlatformTime::Cycles64() - SampleStartCycles;
|
||||
++ExactSampleCount;
|
||||
}
|
||||
if (!FMath::IsFinite(Density) || Density == 0.0f)
|
||||
{
|
||||
++ZeroOrInvalidCount;
|
||||
IZ = IZ1;
|
||||
IY = IY1;
|
||||
break;
|
||||
}
|
||||
MinDensity = FMath::Min(MinDensity, Density);
|
||||
MaxDensity = FMath::Max(MaxDensity, Density);
|
||||
if (Density > 0.0f) { ++PositiveCount; bAllAir = false; }
|
||||
else { ++NegativeCount; bAllSolid = false; }
|
||||
if (!bAllSolid && !bAllAir)
|
||||
{
|
||||
IZ = IZ1;
|
||||
IY = IY1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const EVoxelTileClass Verdict = ZeroOrInvalidCount > 0 || (PositiveCount > 0 && NegativeCount > 0)
|
||||
? EVoxelTileClass::Mixed
|
||||
: (bAllSolid ? EVoxelTileClass::AllSolid : EVoxelTileClass::AllAir);
|
||||
if (bTask4ExactProbe)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("[Task4ExactProbe] depth_box=(%.0f,%.0f,%.0f)-(%.0f,%.0f,%.0f) count=%lld pos=%d neg=%d zero=%d min=%.6g max=%.6g verdict=%d"),
|
||||
(float)Box.Min.X, (float)Box.Min.Y, (float)Box.Min.Z,
|
||||
(float)Box.Max.X, (float)Box.Max.Y, (float)Box.Max.Z,
|
||||
(long long)Count, PositiveCount, NegativeCount, ZeroOrInvalidCount,
|
||||
MinDensity, MaxDensity, static_cast<int32>(Verdict));
|
||||
}
|
||||
return Verdict;
|
||||
};
|
||||
auto Refine = [&](auto&& Self, const FBox& Box, int32 Depth) -> EVoxelTileClass
|
||||
{
|
||||
++RefineNodeCount;
|
||||
const uint64 StackStartCycles = bTask4TimingProbe ? FPlatformTime::Cycles64() : 0;
|
||||
const EVoxelTileClass Whole = Stack.ClassifyBox(Box, Context);
|
||||
if (bTask4TimingProbe)
|
||||
{
|
||||
StackClassifyCycles += FPlatformTime::Cycles64() - StackStartCycles;
|
||||
}
|
||||
const bool bHasStructuralCandidate = StructuralManager != nullptr
|
||||
&& (StructuralManager->AnyPassageNearLattice(
|
||||
Box, Context.LatticeOriginVoxels, Context.Step)
|
||||
|| StructuralManager->AnyPassageLandingFloorNearLattice(
|
||||
Box, Context.LatticeOriginVoxels, Context.Step)
|
||||
|| StructuralManager->AnyOriginLandingNearLattice(
|
||||
Box, Context.LatticeOriginVoxels, Context.Step)
|
||||
|| StructuralManager->AnyOriginLandingFloorNearLattice(
|
||||
Box, Context.LatticeOriginVoxels, Context.Step));
|
||||
// A resolved core box already includes the conservative passage carve bound. Only a
|
||||
// Mixed core box needs the post-disturbance/floor evaluator; applying it to every resolved
|
||||
// child would turn one narrow landing into hundreds of tiny GetDensityAt walks.
|
||||
const bool bNeedsFinalField = Generator != nullptr
|
||||
&& bHasStructuralCandidate
|
||||
&& Whole == EVoxelTileClass::Mixed;
|
||||
const bool bTask4RefineProbe = Context.bUseLatticeProof
|
||||
&& (Context.LatticeOriginVoxels == FIntVector(-96, 96, -96)
|
||||
|| Context.LatticeOriginVoxels == FIntVector(-32, 64, -64)
|
||||
|| Context.LatticeOriginVoxels == FIntVector(-64, -64, -64)
|
||||
|| Context.LatticeOriginVoxels == FIntVector(-64, -96, -64))
|
||||
&& Context.Step == 1;
|
||||
if (bTask4RefineProbe)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("[Task4RefineProbe] depth=%d box=(%.0f,%.0f,%.0f)-(%.0f,%.0f,%.0f) verdict=%d"),
|
||||
Depth, (float)Box.Min.X, (float)Box.Min.Y, (float)Box.Min.Z,
|
||||
(float)Box.Max.X, (float)Box.Max.Y, (float)Box.Max.Z,
|
||||
static_cast<int32>(Whole));
|
||||
}
|
||||
if (Whole != EVoxelTileClass::Mixed)
|
||||
{
|
||||
return Whole;
|
||||
}
|
||||
// At this size the box contains at most 5^3 exact MC vertices. Evaluating those same
|
||||
// vertices is a small, sound certificate for the lattice field the mesher will consume;
|
||||
// it is not a lower-resolution fallback and never changes GenerateMesh's sample count.
|
||||
// This discharges the last conservative SDF interval misses without walking another
|
||||
// refinement level or rebuilding the morphology bounds for every half-box.
|
||||
if (Depth >= 3)
|
||||
{
|
||||
++ExactLeafCount;
|
||||
const EVoxelTileClass Exact = ClassifyExactLatticeLeaf(Box, bNeedsFinalField);
|
||||
if (Exact != EVoxelTileClass::Mixed)
|
||||
{
|
||||
return Exact;
|
||||
}
|
||||
// The final-field leaf is deliberately a conservative sampling certificate. A
|
||||
// mixed result means this child really contains both final signs (or an invalid
|
||||
// sample); splitting it again cannot prove the parent uniform and only repeats the
|
||||
// expensive post-aware evaluator.
|
||||
if (bNeedsFinalField)
|
||||
{
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
}
|
||||
if (Depth >= MaxRefinementDepth)
|
||||
{
|
||||
return bNeedsFinalField ? EVoxelTileClass::Mixed : Whole;
|
||||
}
|
||||
|
||||
const FVector Mid = (Box.Min + Box.Max) * 0.5f;
|
||||
bool bSawSolid = false;
|
||||
bool bSawAir = false;
|
||||
for (int32 Z = 0; Z < 2; ++Z)
|
||||
for (int32 Y = 0; Y < 2; ++Y)
|
||||
for (int32 X = 0; X < 2; ++X)
|
||||
{
|
||||
const FVector ChildMin(
|
||||
X == 0 ? Box.Min.X : Mid.X,
|
||||
Y == 0 ? Box.Min.Y : Mid.Y,
|
||||
Z == 0 ? Box.Min.Z : Mid.Z);
|
||||
const FVector ChildMax(
|
||||
X == 0 ? Mid.X : Box.Max.X,
|
||||
Y == 0 ? Mid.Y : Box.Max.Y,
|
||||
Z == 0 ? Mid.Z : Box.Max.Z);
|
||||
const EVoxelTileClass Child = Self(Self, FBox(ChildMin, ChildMax), Depth + 1);
|
||||
if (Child == EVoxelTileClass::Mixed)
|
||||
{
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
bSawSolid |= Child == EVoxelTileClass::AllSolid;
|
||||
bSawAir |= Child == EVoxelTileClass::AllAir;
|
||||
if (bSawSolid && bSawAir)
|
||||
{
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
}
|
||||
return bSawSolid ? EVoxelTileClass::AllSolid : EVoxelTileClass::AllAir;
|
||||
};
|
||||
const EVoxelTileClass Result = Refine(Refine, VoxelBox, 0);
|
||||
if (bTask4TimingProbe)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("[Task4TimingProbe] origin=(%d,%d,%d) step=%d nodes=%d exact_leaves=%d samples=%d stack_ms=%.3f sample_ms=%.3f result=%d"),
|
||||
Context.LatticeOriginVoxels.X, Context.LatticeOriginVoxels.Y,
|
||||
Context.LatticeOriginVoxels.Z, Context.Step,
|
||||
RefineNodeCount, ExactLeafCount, ExactSampleCount,
|
||||
FPlatformTime::ToMilliseconds64(StackClassifyCycles),
|
||||
FPlatformTime::ToMilliseconds64(ExactSampleCycles),
|
||||
static_cast<int32>(Result));
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
// The ordinary proof is deliberately cheap. Only a box that remains Mixed after its bounded
|
||||
// spatial refinement pays for the finite-domain warp interval; this keeps the common empty-tile
|
||||
// path at its existing cost while giving the few conservative misses a second sound chance.
|
||||
static EVoxelTileClass VF_ClassifyBoxWithWarpRetry(const FVoxelOpStack& Stack,
|
||||
const FBox& VoxelBox,
|
||||
const FVoxelOpContext& Context,
|
||||
const UVoxelGenerator* Generator = nullptr,
|
||||
const UVoxelStrateManager* StructuralManager = nullptr)
|
||||
{
|
||||
const EVoxelTileClass Initial = VF_ClassifyBoxRefined(
|
||||
Stack, VoxelBox, Context, Generator, StructuralManager);
|
||||
// A Mixed fine tile is not evidence of geometry: it can be the conservative result of a
|
||||
// coarse operator interval. Keep the bounded retry for every LOD; skipping it recreates
|
||||
// the false-Mixed empty tiles that send the mesher through the expensive path.
|
||||
if (Initial != EVoxelTileClass::Mixed
|
||||
|| Context.bTightenWarpProof)
|
||||
{
|
||||
return Initial;
|
||||
}
|
||||
|
||||
FVoxelOpContext TightContext = Context;
|
||||
TightContext.bTightenWarpProof = true;
|
||||
return VF_ClassifyBoxRefined(
|
||||
Stack, VoxelBox, TightContext, Generator, StructuralManager);
|
||||
}
|
||||
|
||||
EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis) const
|
||||
{
|
||||
// Mêmes clamps que GenerateMesh — le verdict doit couvrir le treillis réellement échantillonné.
|
||||
Step = FMath::Max(1, Step);
|
||||
// ClassifyTile runs before GenerateMesh installs its LOD TLS. The room-graph source uses this
|
||||
// value only to choose a sound, window-invariant cache region; leaving the previous worker
|
||||
// value (normally 1) makes an LOD4 proof rebuild fine per-chunk graph windows across the
|
||||
// coarse tile. Scope it exactly like the mesher does so standalone classifier calls are also
|
||||
// keyed by their actual lattice step.
|
||||
TGuardValue<int32> ClassifySampleStepGuard(VoxelGenLOD::SampleStep, Step);
|
||||
const int32 CPA = FMath::Clamp(CellsPerAxis, 2, CHUNK_SIZE);
|
||||
const int32 GridDim = CPA + 1; // g ∈ [-1, GridDim] par axe (marge des normales incluse)
|
||||
const int32 GridDim = CPA + 1; // cell-vertex count: g ∈ [0, CPA]
|
||||
|
||||
// Boîte voxel englobant tous les échantillons du treillis.
|
||||
const int32 MinX = OriginVoxels.X - Step, MaxX = OriginVoxels.X + GridDim * Step;
|
||||
const int32 MinY = OriginVoxels.Y - Step, MaxY = OriginVoxels.Y + GridDim * Step;
|
||||
const int32 MinZ = OriginVoxels.Z - Step, MaxZ = OriginVoxels.Z + GridDim * Step;
|
||||
// The mesher's outer one-sample halo is used only for central-difference normals. It cannot
|
||||
// create a marching-cubes cell: every case index reads vertices g ∈ [0, CPA]. Classify those
|
||||
// actual cell vertices, not the halo. Including g=-1/CPA+1 made a coarse tile look Mixed when
|
||||
// the halo crossed a surface outside the tile while every cell in the tile was uniform.
|
||||
const int32 MinX = OriginVoxels.X, MaxX = OriginVoxels.X + CPA * Step;
|
||||
const int32 MinY = OriginVoxels.Y, MaxY = OriginVoxels.Y + CPA * Step;
|
||||
const int32 MinZ = OriginVoxels.Z, MaxZ = OriginVoxels.Z + CPA * Step;
|
||||
const bool bTask4Probe =
|
||||
(OriginVoxels == FIntVector(-96, 96, -96) && Step == 1)
|
||||
|| (OriginVoxels == FIntVector(-32, 64, -64) && Step == 1)
|
||||
|| (OriginVoxels == FIntVector(-64, -64, -64) && Step == 1)
|
||||
|| (OriginVoxels == FIntVector(-64, -96, -64) && Step == 1)
|
||||
|| (OriginVoxels == FIntVector(0, 32, -96) && Step == 2)
|
||||
|| (OriginVoxels == FIntVector(-32, -32, -32) && Step == 8)
|
||||
|| (OriginVoxels == FIntVector(-96, -32, -32) && Step == 16);
|
||||
|
||||
// Division entière PLANCHER (les coords négatives tronquent vers 0 en C++ — pas floor).
|
||||
auto FloorDivC = [](int32 A, int32 B) -> int32
|
||||
@@ -3541,10 +3837,11 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
|
||||
const int32 RecipeMinChunkX = FloorDivC(MinX, CHUNK_SIZE);
|
||||
const int32 RecipeMaxChunkX = FloorDivC(MaxX, CHUNK_SIZE);
|
||||
const int32 RecipeMinChunkY = FloorDivC(MinY, CHUNK_SIZE);
|
||||
const int32 RecipeMaxChunkY = FloorDivC(MaxY, CHUNK_SIZE);
|
||||
// Recipes and disturbances are selected from the strate slot's Z only. The manager
|
||||
// copies the recipe arrays into the output parameters, so repeating this check for every
|
||||
// XY key in the sampled box turns one coarse tile into thousands of identical deep copies.
|
||||
// Keep one representative per distinct Z: this is equivalent for the APIs above and still
|
||||
// rejects a tile when any touched Z changes recipe, bounds, or disturbance state.
|
||||
for (int32 CZ = RecipeMinChunkZ; CZ <= RecipeMaxChunkZ; ++CZ)
|
||||
{
|
||||
int32 TouchedTop = 0;
|
||||
@@ -3554,15 +3851,11 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
{
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
for (int32 CY = RecipeMinChunkY; CY <= RecipeMaxChunkY; ++CY)
|
||||
{
|
||||
for (int32 CX = RecipeMinChunkX; CX <= RecipeMaxChunkX; ++CX)
|
||||
{
|
||||
int32 TouchedSeed = 0;
|
||||
ECaveGeneratorType TouchedArchetype = ECaveGeneratorType::TunnelNetwork;
|
||||
FVoxelStrateArchetypeParams TouchedParams;
|
||||
FVoxelOpStackRecipe TouchedRecipe;
|
||||
const FIntVector TouchedChunk(CX, CY, CZ);
|
||||
const FIntVector TouchedChunk(0, 0, CZ);
|
||||
if (!StrateManager->GetRecipeForChunk(
|
||||
TouchedChunk, TouchedSeed, TouchedArchetype,
|
||||
TouchedParams, TouchedRecipe)
|
||||
@@ -3581,8 +3874,6 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FVoxelOpStack RecipeStack;
|
||||
FVoxelOpContext RecipeContext;
|
||||
@@ -3593,13 +3884,17 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
RecipeContext.ChunkCoord = FIntVector(
|
||||
RecipeMinChunkX, RecipeMinChunkY, RecipeMinChunkZ);
|
||||
FloorDivC(MinX, CHUNK_SIZE), FloorDivC(MinY, CHUNK_SIZE), RecipeMinChunkZ);
|
||||
RecipeContext.Step = Step;
|
||||
RecipeContext.LayoutVersion = StrateManager->GetLayoutVersion();
|
||||
RecipeContext.WorldRadiusVoxels = WorldRadiusVoxels;
|
||||
RecipeContext.EdgeSealThickness = EdgeSealThickness;
|
||||
RecipeContext.bUseLatticeProof = true;
|
||||
RecipeContext.LatticeOriginVoxels = OriginVoxels;
|
||||
RecipeStack.PrepareChunk(RecipeContext);
|
||||
return RecipeStack.ClassifyBox(TileVoxelBox, RecipeContext);
|
||||
const EVoxelTileClass RecipeVerdict =
|
||||
VF_ClassifyBoxWithWarpRetry(RecipeStack, TileVoxelBox, RecipeContext);
|
||||
return RecipeVerdict;
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
@@ -3687,46 +3982,18 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
RegionContext.LayoutVersion = StrateManager->GetLayoutVersion();
|
||||
RegionContext.WorldRadiusVoxels = WorldRadiusVoxels;
|
||||
RegionContext.EdgeSealThickness = EdgeSealThickness;
|
||||
RegionContext.bUseLatticeProof = true;
|
||||
RegionContext.LatticeOriginVoxels = OriginVoxels;
|
||||
RegionStack.PrepareChunk(RegionContext);
|
||||
return RegionStack.ClassifyBox(TileVoxelBox, RegionContext);
|
||||
const EVoxelTileClass RegionVerdict =
|
||||
VF_ClassifyBoxWithWarpRetry(RegionStack, TileVoxelBox, RegionContext);
|
||||
return RegionVerdict;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
bool bCanSolid = true; // "tout le treillis est solide" encore prouvable
|
||||
bool bCanAir = true; // "tout le treillis est air" encore prouvable
|
||||
|
||||
// ── Gardes "carveurs d'air" : cassent AllSolid, jamais AllAir (ils n'ajoutent pas de roche). ──
|
||||
if (StrateManager->AnyPassageNearBox(FVector(MinX, MinY, MinZ), FVector(MaxX, MaxY, MaxZ)))
|
||||
{
|
||||
bCanSolid = false;
|
||||
}
|
||||
// A landing is bidirectional geometry: its chamber kills the AllSolid hypothesis, while its
|
||||
// guaranteed support slab kills AllAir. Without this second guard a tile covering only the
|
||||
// floor could be classified uniformly air and the mesher would skip the solid support that
|
||||
// keeps the player from falling through it. The operator-stack path returns Both for the
|
||||
// same box, so the hand-written legacy fold must carry the identical hypothesis kill here.
|
||||
if (StrateManager->AnyPassageLandingFloorNearBox(
|
||||
FVector(MinX, MinY, MinZ), FVector(MaxX, MaxY, MaxZ)))
|
||||
{
|
||||
bCanAir = false;
|
||||
}
|
||||
if (StrateManager->AnyOriginLandingNearBox(
|
||||
FVector(MinX, MinY, MinZ), FVector(MaxX, MaxY, MaxZ)))
|
||||
{
|
||||
// The origin post is a room carve, not a full-height spine. Its conservative manager
|
||||
// guard is per tile, so it retains the hot-path invariant while covering every strate's
|
||||
// room AABB and its four-voxel carve blend.
|
||||
bCanSolid = false;
|
||||
}
|
||||
if (StrateManager->AnyOriginLandingFloorNearBox(
|
||||
FVector(MinX, MinY, MinZ), FVector(MaxX, MaxY, MaxZ)))
|
||||
{
|
||||
// The same origin room also owns a guaranteed support slab, so a tile touching only that
|
||||
// slab cannot be proved uniformly air.
|
||||
bCanAir = false;
|
||||
}
|
||||
|
||||
// ── Catégorisation par Z du treillis : gap bedrock = solide ; hors layout = air constant ;
|
||||
// SurfaceWorld = test colonne ; un slot cave opt-in = verdict de pile sur sa sous-boîte. ──
|
||||
struct FSurfSlot
|
||||
@@ -3780,7 +4047,7 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
int32 UnusedTopCZ = 0, UnusedBotCZ = 0;
|
||||
return StrateManager->GetStrateChunkZBounds(Z, UnusedTopCZ, UnusedBotCZ);
|
||||
};
|
||||
for (int32 g = -1; g <= GridDim; ++g)
|
||||
for (int32 g = 0; g <= CPA; ++g)
|
||||
{
|
||||
const int32 Zi = OriginVoxels.Z + g * Step;
|
||||
const int32 ChunkZ = FloorDivC(Zi, CHUNK_SIZE);
|
||||
@@ -3965,10 +4232,65 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
if (bInBand) { bCanAir = false; }
|
||||
else { S.InteriorZ.Add(Z); }
|
||||
}
|
||||
if (!bCanSolid && !bCanAir) return EVoxelTileClass::Mixed;
|
||||
if (!bCanSolid && !bCanAir) { return EVoxelTileClass::Mixed; }
|
||||
}
|
||||
|
||||
//=========================================================================
|
||||
if (bTask4Probe)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("[Task4Probe] phase=z origin=(%d,%d,%d) step=%d solid=%d air=%d cave=%d gap=%d surface=%d out=%d cave_z=(%d,%d)"),
|
||||
OriginVoxels.X, OriginVoxels.Y, OriginVoxels.Z, Step,
|
||||
bCanSolid ? 1 : 0, bCanAir ? 1 : 0, bAnyCave ? 1 : 0,
|
||||
bAnyGap ? 1 : 0, bAnySurface ? 1 : 0, bAnyOutOfLayout ? 1 : 0,
|
||||
CaveMinZ, CaveMaxZ);
|
||||
}
|
||||
|
||||
// Out-of-layout is a constant-air field. Passage tubes and origin rooms can only carve that
|
||||
// field, so they cannot invalidate AllAir; only their guaranteed support floors can add solid
|
||||
// voxels. Do this after the cheap Z categorisation so open-air LOD tiles avoid the exact
|
||||
// passage-carve lattice walk and the room scan entirely. A floor-only tile remains Mixed and
|
||||
// is meshed normally, preserving the support backstops.
|
||||
if (bAnyOutOfLayout && !bAnyCave && !bAnySurface && !bAnyGap)
|
||||
{
|
||||
if (StrateManager->AnyPassageLandingFloorNearLattice(
|
||||
TileVoxelBox, OriginVoxels, Step)
|
||||
|| StrateManager->AnyOriginLandingFloorNearLattice(
|
||||
TileVoxelBox, OriginVoxels, Step))
|
||||
{
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
return EVoxelTileClass::AllAir;
|
||||
}
|
||||
|
||||
// ── Structural passage/origin guards for non-open-air categories. ──
|
||||
// Carvers kill AllSolid but never add rock; landing and origin support floors kill AllAir.
|
||||
if (StrateManager->AnyPassageNearLattice(
|
||||
TileVoxelBox, OriginVoxels, Step))
|
||||
{
|
||||
bCanSolid = false;
|
||||
}
|
||||
if (StrateManager->AnyPassageLandingFloorNearLattice(
|
||||
TileVoxelBox, OriginVoxels, Step))
|
||||
{
|
||||
bCanAir = false;
|
||||
}
|
||||
if (StrateManager->AnyOriginLandingNearLattice(
|
||||
TileVoxelBox, OriginVoxels, Step))
|
||||
{
|
||||
bCanSolid = false;
|
||||
}
|
||||
if (StrateManager->AnyOriginLandingFloorNearLattice(
|
||||
TileVoxelBox, OriginVoxels, Step))
|
||||
{
|
||||
bCanAir = false;
|
||||
}
|
||||
if (bTask4Probe)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("[Task4Probe] phase=guards origin=(%d,%d,%d) step=%d solid=%d air=%d"),
|
||||
OriginVoxels.X, OriginVoxels.Y, OriginVoxels.Z, Step,
|
||||
bCanSolid ? 1 : 0, bCanAir ? 1 : 0);
|
||||
}
|
||||
// ── LE PLIAGE DE LA PILE D'OPÉRATEURS, POUR LES ARCHÉTYPES DE CAVE ──
|
||||
//=========================================================================
|
||||
// ⚠️ ERREUR ICI = TROU, PAS RÉGRESSION. Un verdict non-Mixed fait SAUTER `GenerateMesh` : pas
|
||||
@@ -4017,10 +4339,14 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
// represented by this stack; their hypotheses were folded separately in the Z pass.
|
||||
const int32 CZ0 = FloorDivC(CaveMinZ, CHUNK_SIZE), CZ1 = FloorDivC(CaveMaxZ, CHUNK_SIZE);
|
||||
|
||||
// Une tuile très étalée (Step élevé) toucherait trop de chunks pour que cette vérification
|
||||
// reste bon marché. Au-delà, `Mixed` — on renonce au gain, jamais à la sûreté.
|
||||
// Keep the parameter identity check bounded, but do not use the old 27-key cutoff here:
|
||||
// LOD4 legitimately covers 19^3 = 6,859 chunk keys. Returning Mixed at that point sent
|
||||
// the tile straight to GenerateMesh, where GetDensityAt rebuilt one exact-chunk state for
|
||||
// almost every coarse sample. The operator proof now shares its root morphology cache
|
||||
// across refined children, so this scan is cheap compared with the fallback it prevents.
|
||||
const int64 NumChunkCoords = (int64)(CX1 - CX0 + 1) * (int64)(CY1 - CY0 + 1) * (int64)(CZ1 - CZ0 + 1);
|
||||
if (NumChunkCoords > 27)
|
||||
constexpr int64 MaxClassifiedChunkCoords = 262144;
|
||||
if (NumChunkCoords > MaxClassifiedChunkCoords)
|
||||
{
|
||||
INC_DWORD_STAT(STAT_VoxelForgeCaveBailParams);
|
||||
return EVoxelTileClass::Mixed;
|
||||
@@ -4144,6 +4470,9 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
OpCtx.Step = Step;
|
||||
OpCtx.WorldRadiusVoxels = WorldRadiusVoxels;
|
||||
OpCtx.EdgeSealThickness = EdgeSealThickness;
|
||||
OpCtx.bUseLatticeProof = true;
|
||||
OpCtx.bTightenWarpProof = Step <= 2;
|
||||
OpCtx.LatticeOriginVoxels = OriginVoxels;
|
||||
|
||||
FVoxelOpStack TileStack;
|
||||
if (!VF_BuildOpStackForChunk(CaveType, Refs, Seed, OriginSpineRadius,
|
||||
@@ -4158,15 +4487,90 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
|
||||
const FBox CaveBox(FVector((float)MinX, (float)MinY, (float)CaveMinZ),
|
||||
FVector((float)MaxX, (float)MaxY, (float)CaveMaxZ));
|
||||
const EVoxelTileClass StackVerdict = TileStack.ClassifyBox(CaveBox, OpCtx);
|
||||
const EVoxelTileClass StackVerdict = VF_ClassifyBoxWithWarpRetry(
|
||||
TileStack, CaveBox, OpCtx, this, StrateManager);
|
||||
if (bTask4Probe)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("[Task4Probe] phase=stack origin=(%d,%d,%d) step=%d verdict=%d solid=%d air=%d"),
|
||||
OriginVoxels.X, OriginVoxels.Y, OriginVoxels.Z, Step,
|
||||
static_cast<int32>(StackVerdict), bCanSolid ? 1 : 0, bCanAir ? 1 : 0);
|
||||
}
|
||||
if (StackVerdict == EVoxelTileClass::Mixed)
|
||||
{
|
||||
if (bTask4Probe)
|
||||
{
|
||||
int32 SolidKiller = INDEX_NONE;
|
||||
int32 AirKiller = INDEX_NONE;
|
||||
const EVoxelTileClass Attributed = TileStack.ClassifyBoxAttributed(
|
||||
CaveBox, OpCtx, SolidKiller, AirKiller);
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("[Task4Probe] stack_attributed origin=(%d,%d,%d) step=%d verdict=%d solid_killer=%d:%s air_killer=%d:%s"),
|
||||
OriginVoxels.X, OriginVoxels.Y, OriginVoxels.Z, Step,
|
||||
static_cast<int32>(Attributed), SolidKiller,
|
||||
TileStack.GetOpDebugName(SolidKiller), AirKiller,
|
||||
TileStack.GetOpDebugName(AirKiller));
|
||||
|
||||
int32 FinalPositive = 0;
|
||||
int32 FinalNegative = 0;
|
||||
int32 FinalZeroOrInvalid = 0;
|
||||
float FinalMin = FLT_MAX;
|
||||
float FinalMax = -FLT_MAX;
|
||||
for (int32 Z = MinZ; Z <= MaxZ; Z += Step)
|
||||
for (int32 Y = MinY; Y <= MaxY; Y += Step)
|
||||
for (int32 X = MinX; X <= MaxX; X += Step)
|
||||
{
|
||||
const float FinalDensity = GetDensityAt((float)X, (float)Y, (float)Z);
|
||||
if (!FMath::IsFinite(FinalDensity) || FinalDensity == 0.0f)
|
||||
{
|
||||
++FinalZeroOrInvalid;
|
||||
}
|
||||
else if (FinalDensity > 0.0f)
|
||||
{
|
||||
++FinalPositive;
|
||||
FinalMin = FMath::Min(FinalMin, FinalDensity);
|
||||
FinalMax = FMath::Max(FinalMax, FinalDensity);
|
||||
}
|
||||
else
|
||||
{
|
||||
++FinalNegative;
|
||||
FinalMin = FMath::Min(FinalMin, FinalDensity);
|
||||
FinalMax = FMath::Max(FinalMax, FinalDensity);
|
||||
}
|
||||
}
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("[Task4Probe] final_samples origin=(%d,%d,%d) step=%d pos=%d neg=%d zero=%d min=%.6g max=%.6g"),
|
||||
OriginVoxels.X, OriginVoxels.Y, OriginVoxels.Z, Step,
|
||||
FinalPositive, FinalNegative, FinalZeroOrInvalid, FinalMin, FinalMax);
|
||||
}
|
||||
INC_DWORD_STAT(STAT_VoxelForgeCaveBailStackVerdict);
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
|
||||
if (StackVerdict == EVoxelTileClass::AllSolid) { bCanAir = false; }
|
||||
else { bCanSolid = false; }
|
||||
const bool bCaveOnlyTile = bAnyCave
|
||||
&& !bAnyGap && !bAnySurface && !bAnyOutOfLayout
|
||||
&& CaveMinZ == MinZ && CaveMaxZ == MaxZ;
|
||||
if (bCaveOnlyTile && StackVerdict == EVoxelTileClass::AllSolid)
|
||||
{
|
||||
// The stack verdict covers the complete cave-only MC lattice. Its exact structural
|
||||
// post path was supplied above, so a broad conservative passage guard must not force
|
||||
// this already-certified solid tile through GenerateMesh.
|
||||
bCanSolid = true;
|
||||
bCanAir = false;
|
||||
}
|
||||
else if (bCaveOnlyTile && StackVerdict == EVoxelTileClass::AllAir)
|
||||
{
|
||||
bCanSolid = false;
|
||||
bCanAir = true;
|
||||
}
|
||||
else if (StackVerdict == EVoxelTileClass::AllSolid)
|
||||
{
|
||||
bCanAir = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
bCanSolid = false;
|
||||
}
|
||||
|
||||
// Le verdict cave se plie avec gap=solide, hors-layout=air, seals surface=solide. Si les
|
||||
// deux hypothèses sont mortes ici, les catégories se contredisent : ce n'est PAS un échec
|
||||
@@ -4240,18 +4644,28 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
&& (Slots[0].InteriorZ.Num() > 0 || (NumSlots > 1 && Slots[1].InteriorZ.Num() > 0));
|
||||
if (bNeedColumns)
|
||||
{
|
||||
const int32 PreStride = FMath::Max(1, (GridDim + 1) / 4);
|
||||
for (int32 gy = -1; gy <= GridDim; gy += PreStride)
|
||||
for (int32 gx = -1; gx <= GridDim; gx += PreStride)
|
||||
const int32 PreStride = FMath::Max(1, GridDim / 4);
|
||||
for (int32 gy = 0; gy <= CPA; gy += PreStride)
|
||||
for (int32 gx = 0; gx <= CPA; gx += PreStride)
|
||||
if (!TestColumn(gx, gy)) return EVoxelTileClass::Mixed;
|
||||
for (int32 gy = -1; gy <= GridDim; ++gy)
|
||||
for (int32 gx = -1; gx <= GridDim; ++gx)
|
||||
for (int32 gy = 0; gy <= CPA; ++gy)
|
||||
for (int32 gx = 0; gx <= CPA; ++gx)
|
||||
if (!TestColumn(gx, gy)) return EVoxelTileClass::Mixed;
|
||||
}
|
||||
|
||||
// Ici exactement UNE hypothèse doit survivre (chaque point testé en tue une ; les tuiles
|
||||
// sans point intérieur ont tué AllAir via gap/seal). Égalité = prudence → Mixed.
|
||||
if (bCanSolid == bCanAir) return EVoxelTileClass::Mixed;
|
||||
if (bCanSolid == bCanAir)
|
||||
{
|
||||
if (bTask4Probe)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("[Task4Probe] phase=final origin=(%d,%d,%d) step=%d verdict=0 solid=%d air=%d"),
|
||||
OriginVoxels.X, OriginVoxels.Y, OriginVoxels.Z, Step,
|
||||
bCanSolid ? 1 : 0, bCanAir ? 1 : 0);
|
||||
}
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
if (bAnyCave)
|
||||
{
|
||||
// Compte seulement les verdicts FINAUX qui sautent réellement une tuile. Une pile peut avoir
|
||||
@@ -4261,6 +4675,13 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
if (bCanSolid) { INC_DWORD_STAT(STAT_VoxelForgeTilesOpStackSolid); }
|
||||
else { INC_DWORD_STAT(STAT_VoxelForgeTilesOpStackAir); }
|
||||
}
|
||||
if (bTask4Probe)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("[Task4Probe] phase=final origin=(%d,%d,%d) step=%d verdict=%d solid=%d air=%d"),
|
||||
OriginVoxels.X, OriginVoxels.Y, OriginVoxels.Z, Step,
|
||||
bCanSolid ? 1 : 2, bCanSolid ? 1 : 0, bCanAir ? 1 : 0);
|
||||
}
|
||||
return bCanSolid ? EVoxelTileClass::AllSolid : EVoxelTileClass::AllAir;
|
||||
}
|
||||
|
||||
|
||||
@@ -227,6 +227,85 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
|
||||
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;
|
||||
if (bUseBlockEarlyOut && !OutCaptureGrid && Step == 1)
|
||||
{
|
||||
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;
|
||||
|
||||
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 EVoxelTileClass Verdict = Generator->ClassifyTile(
|
||||
BlockOrigin, Step, BlockCells);
|
||||
if (Verdict == EVoxelTileClass::AllSolid
|
||||
|| Verdict == EVoxelTileClass::AllAir)
|
||||
{
|
||||
BlockSkip[((BlockZ * BlocksPerAxis) + BlockY) * BlocksPerAxis + BlockX] = 1;
|
||||
if (VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
Verdict == EVoxelTileClass::AllSolid
|
||||
? VoxelDensityProfile::ECounter::MesherBlockAllSolid
|
||||
: VoxelDensityProfile::ECounter::MesherBlockAllAir);
|
||||
}
|
||||
if (!bHaveUniformBlock)
|
||||
{
|
||||
FirstUniformVerdict = Verdict;
|
||||
bHaveUniformBlock = true;
|
||||
}
|
||||
else if (FirstUniformVerdict != Verdict)
|
||||
{
|
||||
bAllRelevantBlocksSame = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
bAllRelevantBlocksUniform = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bHaveUniformBlock && bAllRelevantBlocksUniform && bAllRelevantBlocksSame)
|
||||
{
|
||||
return MeshData;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
@@ -285,58 +364,6 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
}
|
||||
MesherDensityGridTimer.End();
|
||||
|
||||
// Optional 8x8x8-cell block early-out. ClassifyTile uses the same one-point halo and the
|
||||
// same cave/disturbance/diff-layer guards as production tile loading. It is deliberately
|
||||
// performed after the density grid is complete: skipping only the Marching Cubes work keeps
|
||||
// the grid, capture buffer, geometry, normals, and all density-side effects identical.
|
||||
TArray<uint8> BlockSkip;
|
||||
int32 BlocksPerAxis = 0;
|
||||
if (bUseBlockEarlyOut)
|
||||
{
|
||||
constexpr int32 BlockCells = 8;
|
||||
BlocksPerAxis = (CellsPerAxis + BlockCells - 1) / BlockCells;
|
||||
BlockSkip.Init(0, BlocksPerAxis * BlocksPerAxis * BlocksPerAxis);
|
||||
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 EVoxelTileClass Verdict = Generator->ClassifyTile(
|
||||
BlockOrigin, Step, BlockCells);
|
||||
if (Verdict == EVoxelTileClass::AllSolid
|
||||
|| Verdict == EVoxelTileClass::AllAir)
|
||||
{
|
||||
BlockSkip[((BlockZ * BlocksPerAxis) + BlockY) * BlocksPerAxis + BlockX] = 1;
|
||||
if (VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
Verdict == EVoxelTileClass::AllSolid
|
||||
? 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
|
||||
@@ -396,7 +423,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
if (ShouldAbortWork()) return FVoxelMeshData();
|
||||
for (int32 cx = 0; cx < CellsPerAxis; cx++)
|
||||
{
|
||||
if (bUseBlockEarlyOut
|
||||
if (bUseBlockEarlyOut && Step == 1
|
||||
&& BlockSkip[((cz / 8) * BlocksPerAxis + (cy / 8)) * BlocksPerAxis + (cx / 8)] != 0)
|
||||
{
|
||||
continue;
|
||||
|
||||
@@ -2157,6 +2157,444 @@ bool UVoxelStrateManager::AnyPassageLandingFloorNearBox(
|
||||
return false;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
static bool VF_LatticeAxisRange(
|
||||
float Min, float Max, float Origin, int32 Step,
|
||||
int32& OutFirst, int32& OutLast)
|
||||
{
|
||||
if (Step <= 0 || !FMath::IsFinite(Min) || !FMath::IsFinite(Max)
|
||||
|| !FMath::IsFinite(Origin) || Min > Max)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const float InvStep = 1.0f / static_cast<float>(Step);
|
||||
OutFirst = FMath::CeilToInt((Min - Origin) * InvStep - 1.0e-4f);
|
||||
OutLast = FMath::FloorToInt((Max - Origin) * InvStep + 1.0e-4f);
|
||||
return OutFirst <= OutLast;
|
||||
}
|
||||
|
||||
static bool VF_LatticeAxisNearestDistance(
|
||||
float Min, float Max, float Origin, int32 Step, float Target, float& OutDistance)
|
||||
{
|
||||
int32 First = 0, Last = -1;
|
||||
if (!VF_LatticeAxisRange(Min, Max, Origin, Step, First, Last))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const int32 Nearest = FMath::Clamp(
|
||||
FMath::RoundToInt((Target - Origin) / static_cast<float>(Step)), First, Last);
|
||||
OutDistance = FMath::Abs(
|
||||
Origin + static_cast<float>(Nearest * Step) - Target);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool VF_LatticeBoxTouchesSphere(
|
||||
const FBox& Box, const FIntVector& Origin, int32 Step,
|
||||
const FVector& Center, float Radius)
|
||||
{
|
||||
float DX = 0.0f, DY = 0.0f, DZ = 0.0f;
|
||||
if (!VF_LatticeAxisNearestDistance(
|
||||
(float)Box.Min.X, (float)Box.Max.X, (float)Origin.X,
|
||||
Step, (float)Center.X, DX)
|
||||
|| !VF_LatticeAxisNearestDistance(
|
||||
(float)Box.Min.Y, (float)Box.Max.Y, (float)Origin.Y,
|
||||
Step, (float)Center.Y, DY)
|
||||
|| !VF_LatticeAxisNearestDistance(
|
||||
(float)Box.Min.Z, (float)Box.Max.Z, (float)Origin.Z,
|
||||
Step, (float)Center.Z, DZ))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return DX * DX + DY * DY + DZ * DZ <= FMath::Square(Radius);
|
||||
}
|
||||
|
||||
static bool VF_AnyPassageBoundTouchesLattice(
|
||||
const TArray<FVoxelPassage>& Passages,
|
||||
const FBox& VoxelBox, const FIntVector& LatticeOrigin, int32 Step)
|
||||
{
|
||||
if (Step <= 0 || !VoxelBox.IsValid)
|
||||
{
|
||||
// An invalid query must never manufacture an identity proof.
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const FVoxelPassage& Passage : Passages)
|
||||
{
|
||||
const float Reach = Passage.BoundRadius + 4.0f;
|
||||
if (!FMath::IsFinite(Reach) || Reach < 0.0f)
|
||||
{
|
||||
// Invalid generated geometry is an unknown, not an empty passage set.
|
||||
return true;
|
||||
}
|
||||
if (VF_LatticeBoxTouchesSphere(
|
||||
VoxelBox, LatticeOrigin, Step, Passage.BoundCenter, Reach))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// MaxPassageCarveFactorNearLattice is queried once for the root and once for every refined
|
||||
// child. The child lattice is a strict subset of the root lattice, so retaining the exact
|
||||
// carve factor at each root point is a sound memo: a child reads the same values the old
|
||||
// implementation would have recomputed, in the same deterministic order. The cache is
|
||||
// worker-local because classifier calls can run concurrently and no mutable state may be
|
||||
// published through the immutable strate manager.
|
||||
struct FPassageLatticeBoundCache
|
||||
{
|
||||
const UVoxelStrateManager* Owner = nullptr;
|
||||
uint64 OwnerLifetimeId = 0;
|
||||
uint32 Version = 0xFFFFFFFFu;
|
||||
FIntVector Origin = FIntVector::ZeroValue;
|
||||
int32 Step = 0;
|
||||
int32 FirstX = 0, LastX = -1;
|
||||
int32 FirstY = 0, LastY = -1;
|
||||
int32 FirstZ = 0, LastZ = -1;
|
||||
bool bValid = false;
|
||||
bool bNoCandidate = false;
|
||||
TArray<float> Factors;
|
||||
|
||||
void Reset()
|
||||
{
|
||||
Owner = nullptr;
|
||||
OwnerLifetimeId = 0;
|
||||
Version = 0xFFFFFFFFu;
|
||||
Origin = FIntVector::ZeroValue;
|
||||
Step = 0;
|
||||
FirstX = 0; LastX = -1;
|
||||
FirstY = 0; LastY = -1;
|
||||
FirstZ = 0; LastZ = -1;
|
||||
bValid = false;
|
||||
bNoCandidate = false;
|
||||
Factors.Reset();
|
||||
}
|
||||
|
||||
void Begin(const UVoxelStrateManager* InOwner, uint64 InLifetimeId,
|
||||
uint32 InVersion, const FIntVector& InOrigin, int32 InStep,
|
||||
int32 InFirstX, int32 InLastX,
|
||||
int32 InFirstY, int32 InLastY,
|
||||
int32 InFirstZ, int32 InLastZ,
|
||||
bool bInNoCandidate)
|
||||
{
|
||||
Owner = InOwner;
|
||||
OwnerLifetimeId = InLifetimeId;
|
||||
Version = InVersion;
|
||||
Origin = InOrigin;
|
||||
Step = InStep;
|
||||
FirstX = InFirstX; LastX = InLastX;
|
||||
FirstY = InFirstY; LastY = InLastY;
|
||||
FirstZ = InFirstZ; LastZ = InLastZ;
|
||||
bValid = true;
|
||||
bNoCandidate = bInNoCandidate;
|
||||
Factors.Reset();
|
||||
}
|
||||
|
||||
bool Contains(const UVoxelStrateManager* InOwner, uint64 InLifetimeId,
|
||||
uint32 InVersion, const FIntVector& InOrigin, int32 InStep,
|
||||
int32 InFirstX, int32 InLastX,
|
||||
int32 InFirstY, int32 InLastY,
|
||||
int32 InFirstZ, int32 InLastZ) const
|
||||
{
|
||||
return bValid && Owner == InOwner && OwnerLifetimeId == InLifetimeId
|
||||
&& Version == InVersion && Origin == InOrigin && Step == InStep
|
||||
&& InFirstX >= FirstX && InLastX <= LastX
|
||||
&& InFirstY >= FirstY && InLastY <= LastY
|
||||
&& InFirstZ >= FirstZ && InLastZ <= LastZ;
|
||||
}
|
||||
|
||||
int32 Index(int32 IX, int32 IY, int32 IZ) const
|
||||
{
|
||||
const int32 DimX = LastX - FirstX + 1;
|
||||
const int32 DimY = LastY - FirstY + 1;
|
||||
return ((IZ - FirstZ) * DimY + (IY - FirstY)) * DimX + (IX - FirstX);
|
||||
}
|
||||
|
||||
float MaxIn(const int32 InFirstX, const int32 InLastX,
|
||||
const int32 InFirstY, const int32 InLastY,
|
||||
const int32 InFirstZ, const int32 InLastZ) const
|
||||
{
|
||||
if (bNoCandidate) { return 0.0f; }
|
||||
|
||||
float Result = 0.0f;
|
||||
for (int32 IZ = InFirstZ; IZ <= InLastZ; ++IZ)
|
||||
{
|
||||
for (int32 IY = InFirstY; IY <= InLastY; ++IY)
|
||||
{
|
||||
for (int32 IX = InFirstX; IX <= InLastX; ++IX)
|
||||
{
|
||||
Result = FMath::Max(Result, Factors[Index(IX, IY, IZ)]);
|
||||
if (Result >= 1.0f)
|
||||
{
|
||||
return Result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
};
|
||||
|
||||
FPassageLatticeBoundCache& VF_GetPassageLatticeBoundCache()
|
||||
{
|
||||
thread_local FPassageLatticeBoundCache Cache;
|
||||
return Cache;
|
||||
}
|
||||
|
||||
static bool VF_LatticeBoxTouchesAABB(
|
||||
const FBox& Box, const FIntVector& Origin, int32 Step,
|
||||
float MinX, float MaxX, float MinY, float MaxY, float MinZ, float MaxZ)
|
||||
{
|
||||
return VoxelPassageGeometry::LatticeAxisHasSampleInInterval(
|
||||
FMath::Max((float)Box.Min.X, MinX),
|
||||
FMath::Min((float)Box.Max.X, MaxX),
|
||||
(float)Origin.X, Step)
|
||||
&& VoxelPassageGeometry::LatticeAxisHasSampleInInterval(
|
||||
FMath::Max((float)Box.Min.Y, MinY),
|
||||
FMath::Min((float)Box.Max.Y, MaxY),
|
||||
(float)Origin.Y, Step)
|
||||
&& VoxelPassageGeometry::LatticeAxisHasSampleInInterval(
|
||||
FMath::Max((float)Box.Min.Z, MinZ),
|
||||
FMath::Min((float)Box.Max.Z, MaxZ),
|
||||
(float)Origin.Z, Step);
|
||||
}
|
||||
}
|
||||
|
||||
bool UVoxelStrateManager::AnyPassageNearLattice(
|
||||
const FBox& VoxelBox, const FIntVector& LatticeOrigin, int32 Step) const
|
||||
{
|
||||
// This is deliberately only the conservative spatial candidate test. The old implementation
|
||||
// evaluated the complete modifier SDF here and then evaluated it a second time in
|
||||
// MaxPassageCarveFactorNearLattice during the same fold. A bound hit is sufficient to keep
|
||||
// the carve hypothesis alive; MaxPassageCarveFactorNearLattice supplies the exact lattice
|
||||
// amplitude before the fold can preserve AllSolid.
|
||||
return VF_AnyPassageBoundTouchesLattice(
|
||||
Passages, VoxelBox, LatticeOrigin, Step);
|
||||
}
|
||||
|
||||
float UVoxelStrateManager::MaxPassageCarveFactorNearLattice(
|
||||
const FBox& VoxelBox, const FIntVector& LatticeOrigin, int32 Step) const
|
||||
{
|
||||
// The passage bound is only a cheap candidate test. It encloses an entire multi-segment
|
||||
// passage, so using it as the final lattice proof turns a long tunnel into a false positive
|
||||
// for every tile inside its enclosing sphere. That is particularly damaging at LOD0: the
|
||||
// classifier then falls through to a full density grid even when every sampled point is rock.
|
||||
//
|
||||
// Once a bound reaches this lattice, ask the same exact modifier SDF used by
|
||||
// ApplyPassageCarvingOnly at the actual MC samples. This is still a proof, not a sampling
|
||||
// shortcut: the operator is evaluated at precisely the points the mesher will evaluate, and
|
||||
// a non-finite result remains conservative. The common no-candidate path stays O(P), while a
|
||||
// false-positive bound pays one exact lattice walk and can then be classified as Identity.
|
||||
if (Step <= 0 || !VoxelBox.IsValid)
|
||||
{
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
int32 FirstX = 0, LastX = -1;
|
||||
int32 FirstY = 0, LastY = -1;
|
||||
int32 FirstZ = 0, LastZ = -1;
|
||||
if (!VF_LatticeAxisRange(
|
||||
VoxelBox.Min.X, VoxelBox.Max.X, (float)LatticeOrigin.X, Step, FirstX, LastX)
|
||||
|| !VF_LatticeAxisRange(
|
||||
VoxelBox.Min.Y, VoxelBox.Max.Y, (float)LatticeOrigin.Y, Step, FirstY, LastY)
|
||||
|| !VF_LatticeAxisRange(
|
||||
VoxelBox.Min.Z, VoxelBox.Max.Z, (float)LatticeOrigin.Z, Step, FirstZ, LastZ))
|
||||
{
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
FPassageLatticeBoundCache& Cache = VF_GetPassageLatticeBoundCache();
|
||||
const uint64 LifetimeId = GetCacheLifetimeId();
|
||||
const uint32 Version = GetLayoutVersion();
|
||||
if (Cache.Contains(
|
||||
this, LifetimeId, Version, LatticeOrigin, Step,
|
||||
FirstX, LastX, FirstY, LastY, FirstZ, LastZ))
|
||||
{
|
||||
return Cache.MaxIn(FirstX, LastX, FirstY, LastY, FirstZ, LastZ);
|
||||
}
|
||||
|
||||
if (!VF_AnyPassageBoundTouchesLattice(
|
||||
Passages, VoxelBox, LatticeOrigin, Step))
|
||||
{
|
||||
Cache.Begin(
|
||||
this, LifetimeId, Version, LatticeOrigin, Step,
|
||||
FirstX, LastX, FirstY, LastY, FirstZ, LastZ,
|
||||
/*bInNoCandidate*/ true);
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
constexpr float PassageCarveThreshold = 4.0f;
|
||||
auto CarveFactorFromSDF = [](float ModifierSDF)
|
||||
{
|
||||
if (!(ModifierSDF < PassageCarveThreshold))
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
float CarveFactor = FMath::Clamp(
|
||||
(PassageCarveThreshold - ModifierSDF)
|
||||
/ (PassageCarveThreshold * 2.0f),
|
||||
0.0f, 1.0f);
|
||||
return SmoothStep01(CarveFactor);
|
||||
};
|
||||
|
||||
const int64 DimX = static_cast<int64>(LastX) - FirstX + 1;
|
||||
const int64 DimY = static_cast<int64>(LastY) - FirstY + 1;
|
||||
const int64 DimZ = static_cast<int64>(LastZ) - FirstZ + 1;
|
||||
const int64 SampleCount = DimX * DimY * DimZ;
|
||||
constexpr int64 MaxCachedLatticeSamples = 65536;
|
||||
if (SampleCount > 0 && SampleCount <= MaxCachedLatticeSamples)
|
||||
{
|
||||
Cache.Begin(
|
||||
this, LifetimeId, Version, LatticeOrigin, Step,
|
||||
FirstX, LastX, FirstY, LastY, FirstZ, LastZ,
|
||||
/*bInNoCandidate*/ false);
|
||||
Cache.Factors.SetNumUninitialized(static_cast<int32>(SampleCount));
|
||||
|
||||
float MaxCarveFactor = 0.0f;
|
||||
for (int32 IZ = FirstZ; IZ <= LastZ; ++IZ)
|
||||
{
|
||||
const float Z = (float)LatticeOrigin.Z + (float)(IZ * Step);
|
||||
for (int32 IY = FirstY; IY <= LastY; ++IY)
|
||||
{
|
||||
const float Y = (float)LatticeOrigin.Y + (float)(IY * Step);
|
||||
for (int32 IX = FirstX; IX <= LastX; ++IX)
|
||||
{
|
||||
const float X = (float)LatticeOrigin.X + (float)(IX * Step);
|
||||
const float ModifierSDF = EvaluateModifierSDF(X, Y, Z);
|
||||
const float CarveFactor = FMath::IsFinite(ModifierSDF)
|
||||
? CarveFactorFromSDF(ModifierSDF) : 1.0f;
|
||||
Cache.Factors[Cache.Index(IX, IY, IZ)] = CarveFactor;
|
||||
MaxCarveFactor = FMath::Max(MaxCarveFactor, CarveFactor);
|
||||
if (MaxCarveFactor >= 1.0f)
|
||||
{
|
||||
// The remaining values are still filled below. Descendant queries need
|
||||
// the complete exact lattice, not merely this query's maximum.
|
||||
for (int32 RemainingZ = IZ; RemainingZ <= LastZ; ++RemainingZ)
|
||||
{
|
||||
const float RemainingWorldZ =
|
||||
(float)LatticeOrigin.Z + (float)(RemainingZ * Step);
|
||||
const int32 StartY = RemainingZ == IZ ? IY : FirstY;
|
||||
for (int32 RemainingY = StartY; RemainingY <= LastY; ++RemainingY)
|
||||
{
|
||||
const float RemainingWorldY =
|
||||
(float)LatticeOrigin.Y + (float)(RemainingY * Step);
|
||||
const int32 StartX =
|
||||
(RemainingZ == IZ && RemainingY == IY) ? IX + 1 : FirstX;
|
||||
for (int32 RemainingX = StartX; RemainingX <= LastX; ++RemainingX)
|
||||
{
|
||||
const float RemainingWorldX =
|
||||
(float)LatticeOrigin.X + (float)(RemainingX * Step);
|
||||
const float RemainingSDF = EvaluateModifierSDF(
|
||||
RemainingWorldX, RemainingWorldY, RemainingWorldZ);
|
||||
Cache.Factors[Cache.Index(RemainingX, RemainingY, RemainingZ)] =
|
||||
FMath::IsFinite(RemainingSDF)
|
||||
? CarveFactorFromSDF(RemainingSDF) : 1.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
return MaxCarveFactor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return MaxCarveFactor;
|
||||
}
|
||||
|
||||
// Very large or unusual external queries do not enter the bounded worker memo. Keep the
|
||||
// original exact walk for them; refusing the cache must never change the proof.
|
||||
Cache.Reset();
|
||||
float MaxCarveFactor = 0.0f;
|
||||
for (int32 IZ = FirstZ; IZ <= LastZ; ++IZ)
|
||||
{
|
||||
const float Z = (float)LatticeOrigin.Z + (float)(IZ * Step);
|
||||
for (int32 IY = FirstY; IY <= LastY; ++IY)
|
||||
{
|
||||
const float Y = (float)LatticeOrigin.Y + (float)(IY * Step);
|
||||
for (int32 IX = FirstX; IX <= LastX; ++IX)
|
||||
{
|
||||
const float X = (float)LatticeOrigin.X + (float)(IX * Step);
|
||||
const float ModifierSDF = EvaluateModifierSDF(X, Y, Z);
|
||||
if (!FMath::IsFinite(ModifierSDF))
|
||||
{
|
||||
return 1.0f;
|
||||
}
|
||||
MaxCarveFactor = FMath::Max(
|
||||
MaxCarveFactor, CarveFactorFromSDF(ModifierSDF));
|
||||
if (MaxCarveFactor >= 1.0f)
|
||||
{
|
||||
return MaxCarveFactor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return MaxCarveFactor;
|
||||
}
|
||||
|
||||
bool UVoxelStrateManager::AnyPassageLandingFloorNearLattice(
|
||||
const FBox& VoxelBox, const FIntVector& LatticeOrigin, int32 Step) const
|
||||
{
|
||||
for (const FVoxelPassage& Passage : Passages)
|
||||
{
|
||||
const FVoxelPassageLanding* Landings[] = {
|
||||
&Passage.UpperLanding, &Passage.LowerLanding };
|
||||
for (const FVoxelPassageLanding* Landing : Landings)
|
||||
{
|
||||
if (Landing->HalfWidth <= 0.0f || Landing->FloorThickness <= 0.0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
constexpr float Pad = 1.0f;
|
||||
if (VF_LatticeBoxTouchesAABB(
|
||||
VoxelBox, LatticeOrigin, Step,
|
||||
Landing->StandingPoint.X - Landing->HalfWidth - Pad,
|
||||
Landing->StandingPoint.X + Landing->HalfWidth + Pad,
|
||||
Landing->StandingPoint.Y - Landing->HalfWidth - Pad,
|
||||
Landing->StandingPoint.Y + Landing->HalfWidth + Pad,
|
||||
Landing->FloorZ - Landing->FloorThickness - Pad,
|
||||
Landing->FloorZ + Pad))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (Passage.bWalkableTunnelContract
|
||||
&& Passage.ControlPoints.Num() >= 2
|
||||
&& Passage.ControlRadii.Num() == Passage.ControlPoints.Num())
|
||||
{
|
||||
constexpr float Pad = 1.0f;
|
||||
for (int32 SegmentIndex = 0;
|
||||
SegmentIndex + 1 < Passage.ControlPoints.Num(); ++SegmentIndex)
|
||||
{
|
||||
const FVector& A = Passage.ControlPoints[SegmentIndex];
|
||||
const FVector& B = Passage.ControlPoints[SegmentIndex + 1];
|
||||
const float SupportRadius = FMath::Max(
|
||||
FMath::Min(
|
||||
FMath::Abs(Passage.ControlRadii[SegmentIndex]),
|
||||
FMath::Abs(Passage.ControlRadii[SegmentIndex + 1])) - 0.5f,
|
||||
VoxelPassageGeometry::PlayerRadiusVoxels);
|
||||
const float FloorA = VoxelPassageGeometry::TunnelFloorZ(
|
||||
A, Passage.ControlRadii[SegmentIndex]);
|
||||
const float FloorB = VoxelPassageGeometry::TunnelFloorZ(
|
||||
B, Passage.ControlRadii[SegmentIndex + 1]);
|
||||
if (VF_LatticeBoxTouchesAABB(
|
||||
VoxelBox, LatticeOrigin, Step,
|
||||
FMath::Min(A.X, B.X) - SupportRadius - Pad,
|
||||
FMath::Max(A.X, B.X) + SupportRadius + Pad,
|
||||
FMath::Min(A.Y, B.Y) - SupportRadius - Pad,
|
||||
FMath::Max(A.Y, B.Y) + SupportRadius + Pad,
|
||||
FMath::Min(FloorA, FloorB)
|
||||
- VoxelPassageGeometry::LandingFloorThicknessVoxels - Pad,
|
||||
FMath::Max(FloorA, FloorB) + Pad))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UVoxelStrateManager::AnyOriginLandingNearBox(
|
||||
const FVector& MinVoxel, const FVector& MaxVoxel) const
|
||||
{
|
||||
@@ -2199,6 +2637,44 @@ bool UVoxelStrateManager::AnyOriginLandingFloorNearBox(
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UVoxelStrateManager::AnyOriginLandingNearLattice(
|
||||
const FBox& VoxelBox, const FIntVector& LatticeOrigin, int32 Step) const
|
||||
{
|
||||
if (OriginSpineRadius <= 0.0f) return false;
|
||||
for (const FStrateSlot& Slot : StrateLayout)
|
||||
{
|
||||
if (Slot.Definition == nullptr) continue;
|
||||
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * CHUNK_SIZE;
|
||||
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * CHUNK_SIZE;
|
||||
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
||||
if (VoxelPassageGeometry::OriginLandingRoomTouchesLattice(
|
||||
VoxelBox, LatticeOrigin, Step, TopZ, BottomZ, Seal, OriginSpineRadius))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UVoxelStrateManager::AnyOriginLandingFloorNearLattice(
|
||||
const FBox& VoxelBox, const FIntVector& LatticeOrigin, int32 Step) const
|
||||
{
|
||||
if (OriginSpineRadius <= 0.0f) return false;
|
||||
for (const FStrateSlot& Slot : StrateLayout)
|
||||
{
|
||||
if (Slot.Definition == nullptr) continue;
|
||||
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * CHUNK_SIZE;
|
||||
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * CHUNK_SIZE;
|
||||
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
||||
if (VoxelPassageGeometry::OriginLandingFloorTouchesLattice(
|
||||
VoxelBox, LatticeOrigin, Step, TopZ, BottomZ, Seal, OriginSpineRadius))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// QUERIES
|
||||
//=============================================================================
|
||||
|
||||
@@ -1207,6 +1207,14 @@ bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (GVoxelForgeProfileTileGeneration != 0 && Result.RequestStartCycles != 0)
|
||||
{
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeTileReady] level=%d empty=%d request_to_ready=%.6f"),
|
||||
Result.Tile.Level, Result.bEmpty || !Result.Streams ? 1 : 0,
|
||||
FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - Result.RequestStartCycles));
|
||||
}
|
||||
|
||||
// Mark the tile loaded (even if empty — so we don't re-submit it).
|
||||
LoadedTiles.Add(Result.Tile);
|
||||
|
||||
@@ -1270,6 +1278,8 @@ void AVoxelWorld::SyncRemeshTile(const FVoxelTileKey& Tile)
|
||||
}
|
||||
|
||||
FChunkResult Result;
|
||||
Result.RequestStartCycles = GVoxelForgeProfileTileGeneration != 0
|
||||
? FPlatformTime::Cycles64() : 0;
|
||||
GenerateTileResult(Tile, OriginVoxels, Step, Cells, GenerationEpoch, /*bWantCapture*/ false,
|
||||
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
|
||||
/*bSheetTile*/ false, /*SheetChunkZ*/ 0,
|
||||
@@ -1966,6 +1976,8 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
|
||||
return; // Budget full — wait for a task to finish.
|
||||
}
|
||||
PendingTiles.Add(Tile);
|
||||
const uint64 RequestStartCycles = GVoxelForgeProfileTileGeneration != 0
|
||||
? FPlatformTime::Cycles64() : 0;
|
||||
|
||||
const FIntVector OriginVoxels = Tile.OriginVoxels(); // min corner, voxel coords
|
||||
|
||||
@@ -2019,6 +2031,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
|
||||
FChunkResult Empty;
|
||||
Empty.Tile = Tile;
|
||||
Empty.Epoch = TaskEpoch;
|
||||
Empty.RequestStartCycles = RequestStartCycles;
|
||||
ProcessQueue.Enqueue(MoveTemp(Empty));
|
||||
return;
|
||||
}
|
||||
@@ -2048,6 +2061,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
|
||||
FChunkResult Empty;
|
||||
Empty.Tile = Tile;
|
||||
Empty.Epoch = TaskEpoch;
|
||||
Empty.RequestStartCycles = RequestStartCycles;
|
||||
Empty.BandChunkLo = BandChunkLo;
|
||||
Empty.BandChunkHi = BandChunkHi;
|
||||
ProcessQueue.Enqueue(MoveTemp(Empty));
|
||||
@@ -2069,7 +2083,8 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
|
||||
: UE::Tasks::ETaskPriority::BackgroundNormal;
|
||||
UE::Tasks::Launch(TEXT("ChunkGen"), [this, Tile, OriginVoxels, Step, Cells, TaskEpoch, bWantCapture,
|
||||
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
|
||||
bSheetTile, SheetChunkZ, HoleMinX, HoleMinY, HoleMaxX, HoleMaxY]()
|
||||
bSheetTile, SheetChunkZ, HoleMinX, HoleMinY, HoleMaxX, HoleMaxY,
|
||||
RequestStartCycles]()
|
||||
{
|
||||
// RAII: decrement the counter on every exit path.
|
||||
struct FTaskGuard
|
||||
@@ -2084,6 +2099,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
|
||||
GenerateTileResult(Tile, OriginVoxels, Step, Cells, TaskEpoch, bWantCapture,
|
||||
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
|
||||
bSheetTile, SheetChunkZ, HoleMinX, HoleMinY, HoleMaxX, HoleMaxY, Result);
|
||||
Result.RequestStartCycles = RequestStartCycles;
|
||||
|
||||
if (!ShouldAbortWork())
|
||||
{
|
||||
@@ -2103,14 +2119,21 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
||||
{
|
||||
const bool bProfileTile = GVoxelForgeProfileTileGeneration != 0;
|
||||
const double TileStartSeconds = bProfileTile ? FPlatformTime::Seconds() : 0.0;
|
||||
double ClassifySeconds = 0.0;
|
||||
double MeshSeconds = 0.0;
|
||||
double StreamSeconds = 0.0;
|
||||
int32 ClassifyVerdict = -1; // Mixed = 0, AllSolid = 1, AllAir = 2
|
||||
auto EmitTileProfile = [&]()
|
||||
{
|
||||
if (bProfileTile)
|
||||
{
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeTileProfile] level=%d step=%d cells=%d sheet=%d aborted=%d empty=%d seconds=%.6f"),
|
||||
Tile.Level, Step, Cells, bSheetTile ? 1 : 0, Result.bAborted ? 1 : 0,
|
||||
Result.bEmpty ? 1 : 0, FPlatformTime::Seconds() - TileStartSeconds);
|
||||
TEXT("[VoxelForgeTileProfile] tile=(%d,%d,%d) level=%d step=%d cells=%d sheet=%d aborted=%d empty=%d "
|
||||
"verdict=%d classify=%.6f mesh=%.6f streams=%.6f seconds=%.6f"),
|
||||
Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, Tile.Level, Step, Cells,
|
||||
bSheetTile ? 1 : 0, Result.bAborted ? 1 : 0,
|
||||
Result.bEmpty ? 1 : 0, ClassifyVerdict, ClassifySeconds, MeshSeconds, StreamSeconds,
|
||||
FPlatformTime::Seconds() - TileStartSeconds);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2151,7 +2174,10 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
||||
{
|
||||
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_ClassifyTile);
|
||||
INC_DWORD_STAT(STAT_VoxelForgeTilesClassified);
|
||||
const double ClassifyStartSeconds = bProfileTile ? FPlatformTime::Seconds() : 0.0;
|
||||
const EVoxelTileClass Verdict = Generator->ClassifyTile(OriginVoxels, Step, Cells);
|
||||
ClassifySeconds = bProfileTile ? FPlatformTime::Seconds() - ClassifyStartSeconds : 0.0;
|
||||
ClassifyVerdict = static_cast<int32>(Verdict);
|
||||
if (ShouldAbortWork())
|
||||
{
|
||||
AbortResult();
|
||||
@@ -2178,12 +2204,14 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
||||
if (!bTrivialEmpty)
|
||||
{
|
||||
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_GenerateMesh);
|
||||
const double MeshStartSeconds = bProfileTile ? FPlatformTime::Seconds() : 0.0;
|
||||
MeshData = bSheetTile
|
||||
? Mesher->GenerateSheetMesh(OriginVoxels, Step, Cells, SheetChunkZ,
|
||||
HoleMinX, HoleMinY, HoleMaxX, HoleMaxY)
|
||||
: Mesher->GenerateMesh(OriginVoxels, Step, Cells,
|
||||
bWantCapture ? &Result.CaptureGrid : nullptr,
|
||||
BandVoxLo, BandVoxHi);
|
||||
MeshSeconds = bProfileTile ? FPlatformTime::Seconds() - MeshStartSeconds : 0.0;
|
||||
INC_DWORD_STAT(STAT_VoxelForgeTilesMeshed);
|
||||
if (ShouldAbortWork())
|
||||
{
|
||||
@@ -2198,8 +2226,10 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
||||
if (!MeshData.IsEmpty())
|
||||
{
|
||||
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_BuildStreams);
|
||||
const double StreamStartSeconds = bProfileTile ? FPlatformTime::Seconds() : 0.0;
|
||||
Result.Streams = MakeShared<RealtimeMesh::FRealtimeMeshStreamSet>();
|
||||
BuildTileStreamSet(*Result.Streams, MeshData);
|
||||
StreamSeconds = bProfileTile ? FPlatformTime::Seconds() - StreamStartSeconds : 0.0;
|
||||
if (ShouldAbortWork())
|
||||
{
|
||||
AbortResult();
|
||||
|
||||
@@ -102,6 +102,7 @@
|
||||
#include "VoxelTypes.h" // CHUNK_SIZE, EVoxelTileClass
|
||||
|
||||
struct FBiomeContext;
|
||||
struct FTunnelCoreWorldEvaluation;
|
||||
|
||||
//=============================================================================
|
||||
// LES QUATRE RÔLES / THE FOUR ROLES
|
||||
@@ -284,6 +285,18 @@ struct FVoxelOpContext
|
||||
float StrateTopWorldZ = 0.0f;
|
||||
float StrateBottomWorldZ = 0.0f;
|
||||
|
||||
// ClassifyTile may ask box bounds about the exact marching-cubes lattice rather than the
|
||||
// continuous volume between samples. Production voxel evaluation leaves this disabled;
|
||||
// when enabled, operators may tighten a conservative bound only for points
|
||||
// LatticeOriginVoxels + n * Step that lie inside the queried box.
|
||||
bool bUseLatticeProof = false;
|
||||
FIntVector LatticeOriginVoxels = FIntVector::ZeroValue;
|
||||
|
||||
// A mixed tile may opt into a tighter, still conservative finite-box bound for domain-warped
|
||||
// sources. The normal classifier leaves this off; ClassifyTile retries only the few boxes
|
||||
// that the cheap global proof could not resolve.
|
||||
bool bTightenWarpProof = false;
|
||||
|
||||
// null = cette strate n'a pas de champ de biome.
|
||||
const FBiomeContext* Biome = nullptr;
|
||||
};
|
||||
@@ -457,6 +470,14 @@ public:
|
||||
*/
|
||||
virtual void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const = 0;
|
||||
|
||||
/** Optional hand-off for the common structural tail. The default keeps custom operators
|
||||
* conservative: the caller uses its canonical cache path when no source publishes a result. */
|
||||
virtual bool TryGetLastTunnelCoreWorldEvaluation(
|
||||
FTunnelCoreWorldEvaluation& OutEvaluation) const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* CONSERVATIF. The two-argument method is the intrinsic fallback. The state-aware overload
|
||||
* below is what the stack calls: it sees the SDF interval already produced by earlier ops.
|
||||
@@ -717,7 +738,9 @@ FORCEINLINE void VF_ForceHypotheses(FVoxelBoxHypotheses& H, EVoxelTileClass Forc
|
||||
* c'est la propriété de sûreté, et le pliage numérique ne l'affaiblit pas : une marge ne peut que
|
||||
* DESCENDRE, jamais remonter, en dehors d'un opérateur forçant.
|
||||
*
|
||||
* ⚠️ `MaxCarve` / `MaxFill` valent `FLT_MAX` par défaut = « amplitude inconnue ». La soustraction
|
||||
* ⚠️ `MaxCarve` / `MaxFill` valent `FLT_MAX` par défaut = « amplitude inconnue ». Une borne finie
|
||||
* nulle signifie au contraire « identité prouvée » pour cette direction : l'opérateur peut être
|
||||
* géométriquement présent mais ne modifie aucun échantillon de la boîte. La soustraction
|
||||
* fait alors passer la marge très en dessous de zéro et l'hypothèse meurt, exactement comme la
|
||||
* version purement directionnelle de ce pliage. Aucun opérateur existant ne change de verdict.
|
||||
* (Arithmétique volontairement laissée en float sans garde : `0 − FLT_MAX` vaut `−FLT_MAX`,
|
||||
@@ -733,21 +756,67 @@ FORCEINLINE void VF_FoldEffect(FVoxelBoxHypotheses& H, EVoxelOpEffect Effect,
|
||||
break;
|
||||
|
||||
case EVoxelOpEffect::CarveOnly:
|
||||
// A finite zero bound is a proved identity for this direction. This matters for
|
||||
// state-aware structural guards: a passage may be geometrically present in a box while
|
||||
// its exact lattice carve factor is zero, and that must not kill an otherwise valid
|
||||
// AllSolid hypothesis merely because its margin is zero/unknown.
|
||||
if (MaxCarve == 0.0f)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (!(MaxCarve >= 0.0f))
|
||||
{
|
||||
H.bCanBeAllSolid = false;
|
||||
H.SolidMargin = -FLT_MAX;
|
||||
break;
|
||||
}
|
||||
H.SolidMargin -= MaxCarve;
|
||||
if (!(H.SolidMargin > 0.0f)) { H.bCanBeAllSolid = false; }
|
||||
break;
|
||||
|
||||
case EVoxelOpEffect::FillOnly:
|
||||
if (MaxFill == 0.0f)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (!(MaxFill >= 0.0f))
|
||||
{
|
||||
H.bCanBeAllAir = false;
|
||||
H.AirMargin = -FLT_MAX;
|
||||
break;
|
||||
}
|
||||
H.AirMargin -= MaxFill;
|
||||
if (!(H.AirMargin > 0.0f)) { H.bCanBeAllAir = false; }
|
||||
break;
|
||||
|
||||
case EVoxelOpEffect::Both:
|
||||
default:
|
||||
if (MaxCarve != 0.0f)
|
||||
{
|
||||
if (!(MaxCarve >= 0.0f))
|
||||
{
|
||||
H.bCanBeAllSolid = false;
|
||||
H.SolidMargin = -FLT_MAX;
|
||||
}
|
||||
else
|
||||
{
|
||||
H.SolidMargin -= MaxCarve;
|
||||
if (!(H.SolidMargin > 0.0f)) { H.bCanBeAllSolid = false; }
|
||||
}
|
||||
}
|
||||
if (MaxFill != 0.0f)
|
||||
{
|
||||
if (!(MaxFill >= 0.0f))
|
||||
{
|
||||
H.bCanBeAllAir = false;
|
||||
H.AirMargin = -FLT_MAX;
|
||||
}
|
||||
else
|
||||
{
|
||||
H.AirMargin -= MaxFill;
|
||||
if (!(H.AirMargin > 0.0f)) { H.bCanBeAllAir = false; }
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,6 +174,10 @@ public:
|
||||
return -EvalInternal(WorldX, WorldY, WorldZ);
|
||||
}
|
||||
|
||||
/** Return a same-sample structural-core result published by a source in this stack. */
|
||||
bool TryGetLastTunnelCoreWorldEvaluation(
|
||||
FTunnelCoreWorldEvaluation& OutEvaluation) const;
|
||||
|
||||
/**
|
||||
* Le pliage générique qui remplacera les gardes écrites à la main dans ClassifyTile.
|
||||
* Voir `VF_FoldOp` (VoxelDensityOp.h) pour la sémantique — en particulier pourquoi un
|
||||
@@ -182,10 +186,25 @@ public:
|
||||
EVoxelTileClass ClassifyBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const
|
||||
{
|
||||
FVoxelBoxHypotheses H;
|
||||
const bool bTask4OpProbe = Ctx.bUseLatticeProof
|
||||
&& Ctx.LatticeOriginVoxels == FIntVector(32, 0, -32)
|
||||
&& Ctx.Step == 1;
|
||||
for (const FOpEntry& Entry : Ops)
|
||||
{
|
||||
const uint64 Task4OpStartCycles = bTask4OpProbe
|
||||
? FPlatformTime::Cycles64() : 0;
|
||||
VF_FoldOp(H, *Entry.Op, VoxelBox, Ctx,
|
||||
(Entry.Writes & VoxelOpChannels::Sdf) != 0);
|
||||
if (bTask4OpProbe)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("[Task4OpProbe] box=(%.0f,%.0f,%.0f)-(%.0f,%.0f,%.0f) op=%s ms=%.3f"),
|
||||
(float)VoxelBox.Min.X, (float)VoxelBox.Min.Y, (float)VoxelBox.Min.Z,
|
||||
(float)VoxelBox.Max.X, (float)VoxelBox.Max.Y, (float)VoxelBox.Max.Z,
|
||||
Entry.Op->DebugName(),
|
||||
FPlatformTime::ToMilliseconds64(
|
||||
FPlatformTime::Cycles64() - Task4OpStartCycles));
|
||||
}
|
||||
// Do not early-out on a dead hypothesis: a later forcing structural post may
|
||||
// deliberately overwrite it (the XY edge seal is appended after passage tube/landing
|
||||
// carving and support).
|
||||
|
||||
@@ -147,9 +147,12 @@ public:
|
||||
// l'écart vers un voisin un niveau plus grossier (cellule 2×). Monter si des fissures persistent.
|
||||
float SkirtCells = 2.0f;
|
||||
|
||||
// Optional conservative block classifier. When enabled, 8x8x8 cell regions proven uniformly
|
||||
// solid or air by UVoxelGenerator::ClassifyTile skip the marching-cubes cell loop. The density
|
||||
// grid is still produced in full, so this switch cannot change samples or geometry.
|
||||
// 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).
|
||||
|
||||
@@ -433,6 +433,73 @@ namespace VoxelPassageGeometry
|
||||
return MaxA >= MinB && MinA <= MaxB;
|
||||
}
|
||||
|
||||
/** True when the exact MC lattice has at least one sample in a closed interval. */
|
||||
FORCEINLINE bool LatticeAxisHasSampleInInterval(
|
||||
float Min, float Max, float Origin, int32 Step)
|
||||
{
|
||||
if (Step <= 0 || !FMath::IsFinite(Min) || !FMath::IsFinite(Max)
|
||||
|| !FMath::IsFinite(Origin) || Min > Max)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const float InvStep = 1.0f / static_cast<float>(Step);
|
||||
const int32 First = FMath::CeilToInt(
|
||||
(Min - Origin) * InvStep - 1.0e-4f);
|
||||
const int32 Last = FMath::FloorToInt(
|
||||
(Max - Origin) * InvStep + 1.0e-4f);
|
||||
return First <= Last;
|
||||
}
|
||||
|
||||
FORCEINLINE bool OriginLandingRoomTouchesLattice(
|
||||
const FBox& VoxelBox, const FIntVector& LatticeOrigin, int32 Step,
|
||||
float StrateTopZ, float StrateBottomZ, float SealThickness, float OriginRadius)
|
||||
{
|
||||
if (!VoxelBox.IsValid) return false;
|
||||
const FOriginLandingGeometry Geometry = BuildOriginLandingGeometry(
|
||||
StrateTopZ, StrateBottomZ, SealThickness, OriginRadius);
|
||||
if (!Geometry.bValid) return false;
|
||||
const float Pad = LandingCarveBlendVoxels;
|
||||
return LatticeAxisHasSampleInInterval(
|
||||
FMath::Max((float)VoxelBox.Min.X, -Geometry.HalfWidth - Pad),
|
||||
FMath::Min((float)VoxelBox.Max.X, Geometry.HalfWidth + Pad),
|
||||
(float)LatticeOrigin.X, Step)
|
||||
&& LatticeAxisHasSampleInInterval(
|
||||
FMath::Max((float)VoxelBox.Min.Y, -Geometry.HalfWidth - Pad),
|
||||
FMath::Min((float)VoxelBox.Max.Y, Geometry.HalfWidth + Pad),
|
||||
(float)LatticeOrigin.Y, Step)
|
||||
&& LatticeAxisHasSampleInInterval(
|
||||
FMath::Max((float)VoxelBox.Min.Z, Geometry.FloorZ - Pad),
|
||||
FMath::Min((float)VoxelBox.Max.Z, Geometry.CeilingZ + Pad),
|
||||
(float)LatticeOrigin.Z, Step);
|
||||
}
|
||||
|
||||
FORCEINLINE bool OriginLandingFloorTouchesLattice(
|
||||
const FBox& VoxelBox, const FIntVector& LatticeOrigin, int32 Step,
|
||||
float StrateTopZ, float StrateBottomZ, float SealThickness, float OriginRadius)
|
||||
{
|
||||
if (!VoxelBox.IsValid) return false;
|
||||
const FOriginLandingGeometry Geometry = BuildOriginLandingGeometry(
|
||||
StrateTopZ, StrateBottomZ, SealThickness, OriginRadius);
|
||||
if (!Geometry.bValid) return false;
|
||||
|
||||
constexpr float ProofPad = 1.0f;
|
||||
const float FloorHalfWidth = FMath::Max(
|
||||
Geometry.HalfWidth - 1.0f, 0.0f);
|
||||
return LatticeAxisHasSampleInInterval(
|
||||
FMath::Max((float)VoxelBox.Min.X, -FloorHalfWidth - ProofPad),
|
||||
FMath::Min((float)VoxelBox.Max.X, FloorHalfWidth + ProofPad),
|
||||
(float)LatticeOrigin.X, Step)
|
||||
&& LatticeAxisHasSampleInInterval(
|
||||
FMath::Max((float)VoxelBox.Min.Y, -FloorHalfWidth - ProofPad),
|
||||
FMath::Min((float)VoxelBox.Max.Y, FloorHalfWidth + ProofPad),
|
||||
(float)LatticeOrigin.Y, Step)
|
||||
&& LatticeAxisHasSampleInInterval(
|
||||
FMath::Max((float)VoxelBox.Min.Z,
|
||||
Geometry.FloorZ - Geometry.FloorThickness - ProofPad),
|
||||
FMath::Min((float)VoxelBox.Max.Z, Geometry.FloorZ + ProofPad),
|
||||
(float)LatticeOrigin.Z, Step);
|
||||
}
|
||||
|
||||
/** Conservative influence box for the origin room's air carve. */
|
||||
FORCEINLINE bool OriginLandingRoomTouchesBox(
|
||||
const FBox& VoxelBox,
|
||||
|
||||
@@ -420,10 +420,25 @@ public:
|
||||
/** Conservative box guard for the floor fill; unlike a tube it can affect the all-air proof. */
|
||||
bool AnyPassageLandingFloorNearBox(const FVector& MinVoxel, const FVector& MaxVoxel) const;
|
||||
|
||||
/** Exact-lattice counterparts used by ClassifyTile's MC-grid proof. */
|
||||
bool AnyPassageNearLattice(const FBox& VoxelBox,
|
||||
const FIntVector& LatticeOrigin, int32 Step) const;
|
||||
/** Exact maximum of the passage carve factor on the mesher lattice; 0 means no tube/landing
|
||||
* carve reaches a sampled point. Invalid geometry returns 1 conservatively. */
|
||||
float MaxPassageCarveFactorNearLattice(const FBox& VoxelBox,
|
||||
const FIntVector& LatticeOrigin, int32 Step) const;
|
||||
bool AnyPassageLandingFloorNearLattice(const FBox& VoxelBox,
|
||||
const FIntVector& LatticeOrigin, int32 Step) const;
|
||||
|
||||
/** Conservative box guards for the per-strate (0,0) landing rooms and their floor slabs. */
|
||||
bool AnyOriginLandingNearBox(const FVector& MinVoxel, const FVector& MaxVoxel) const;
|
||||
bool AnyOriginLandingFloorNearBox(const FVector& MinVoxel, const FVector& MaxVoxel) const;
|
||||
|
||||
bool AnyOriginLandingNearLattice(const FBox& VoxelBox,
|
||||
const FIntVector& LatticeOrigin, int32 Step) const;
|
||||
bool AnyOriginLandingFloorNearLattice(const FBox& VoxelBox,
|
||||
const FIntVector& LatticeOrigin, int32 Step) const;
|
||||
|
||||
/** Get all generated passages (for debug display). */
|
||||
const TArray<FVoxelPassage>& GetPassages() const { return Passages; }
|
||||
|
||||
|
||||
@@ -84,6 +84,9 @@ struct FChunkResult
|
||||
// Null ⇒ empty/all-air tile (no component).
|
||||
TSharedPtr<RealtimeMesh::FRealtimeMeshStreamSet> Streams;
|
||||
uint32 Epoch = 0; // Generation epoch — discard if stale
|
||||
// Monotonic request timestamp used only for streaming telemetry. It is carried through the
|
||||
// worker queue so the profile can report request-to-ready, not just worker generation time.
|
||||
uint64 RequestStartCycles = 0;
|
||||
bool bAborted = false; // Worker observed shutdown; never mark this tile loaded
|
||||
bool bEmpty = true; // true ⇒ all-air tile (Streams null); still marked loaded so we don't re-submit
|
||||
// F17 — the mesher classifies every triangle semantically (sky-cap = down-facing near the
|
||||
|
||||
Reference in new issue
Block a user