fix(shafts): the primordial law holds -- 0 trusted negatives across 16 seeds
Closes the VerticalShafts bug. Two remaining failures were investigated
and neither was a broken world.
PART A -- my measurement window was binding, not the world.
The window was a square centred on (0,0) with a radius, while mouths sit
up to ~200 voxels out in arbitrary directions, so a box holding both was
mostly empty space paid for at full price. Seed 6 at step 1 needed
16,515,072 cells and blew the cap; fitted to the two mouths it needs
6,553,344 and measures fine.
seed 6 step 2 NOT_CONNECTED -> CONNECTED at doubled margin
seed 6 step 1 NOT_CONNECTED -> CONNECTED at doubled margin
seed 14 step 2 CONNECTED
seed 14 step 1 CONNECTED
A fitted box can cut off a route that detours outside it, so every
negative is re-run at doubled margin. 5 of 16 were binding negatives and
all 5 flipped. TRUSTED NEGATIVES: 0. There is no evidence of a single
unescapable strate in 16 seeds.
PART B -- the drainage tree has no orphans, and a third cause existed.
0 / 10,372 unreachable shaft paths (0% orphan rate)
16 strict local minima, all caught by the direct-spine fallback
neighbour fallback fired 0 times; emergency fallback 0
91.8% of non-largest components are smaller than one shaft volume --
they are roughness bubbles, not geometry
So a mouth could land in a noise bubble that is open but unreachable --
worse than useless, exactly the trap the law forbids.
VF_SuggestLandingPoint now returns the selected shaft AXIS, which is on
the drainage tree by construction.
⚠️ Codex found a third cause neither of us listed: tree capsules could
cross ledge bands, so a guaranteed connector could be blocked by a ledge.
Connector Z is now drawn from a deterministic ledge-free interval,
mirrored in the op stack.
⚠️ And an 8th error in my spec: "connect to any neighbour shaft" as a
fallback can create CYCLES -- a loop of shafts that drains nowhere and is
disconnected from the spine. The fallback must preserve strictly
decreasing origin-distance. Implemented that way.
per-archetype law 6/6 · box-verdict violations 0 · stack equivalence
bit-identical over 20,000 samples · sweep 45.3 -> 20.4 s
cache 7.970 us rebuild, 0.073 us hot call (225-cell rolls on rebuild only)
Build clean, 21 succeeded / 0 failed, verified by me in VoxelM.log.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
This commit is contained in:
1 parent
9d2324879d
commit
4f11ce81a4
10 files changed
+1813
-174
No files matched your search
@@ -174,8 +174,8 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
|
||||
//=========================================================================
|
||||
// 2. INVARIANCE DE FENÊTRE
|
||||
//=========================================================================
|
||||
// La source garde un cache inner 3×3 `thread_local`; chaque rebuild collecte 7×7 pour les
|
||||
// fenêtres d'arbre 5×5. La clé est le jeu de params : c'est exactement le genre d'endroit où
|
||||
// La source garde un cache inner 3×3 `thread_local`; chaque rebuild collecte un halo 9×9/15×15
|
||||
// pour les fenêtres d'arbre 5×5 et le fallback ±3. La clé est le jeu de params : c'est exactement le genre d'endroit où
|
||||
// une clé incomplète produit une couture (AUDIT §C2).
|
||||
{
|
||||
std::atomic<int32> Impure{ 0 };
|
||||
@@ -208,9 +208,9 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
|
||||
//==========================================================================
|
||||
{
|
||||
// Force one distinct centre cell per call, then hold one cell constant. This measures the
|
||||
// widened collection where it belongs (cache rebuild), separately from the hot per-voxel
|
||||
// path. The implementation deliberately performs 49 cell rolls for the former and zero
|
||||
// cell rolls for the latter; only the cached inner 3×3/capsule SDFs remain hot.
|
||||
// widened collection where it belongs (cache rebuild), separately from the hot per-voxel
|
||||
// path. The implementation deliberately performs 225 cell rolls for the former and zero
|
||||
// cell rolls for the latter; only the cached inner 3×3/capsule SDFs remain hot.
|
||||
constexpr int32 NumRebuildSamples = 256;
|
||||
constexpr int32 NumHotSamples = 20000;
|
||||
const float PerfSpacing = FMath::Max(P.ShaftSpacing, 1.0f);
|
||||
@@ -239,7 +239,7 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
|
||||
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("VerticalShafts cache perf: %d forced rebuilds at %.3f us/call and %d hot calls "
|
||||
"at %.3f us/call (7x7=49 cell rolls only on rebuild; inner 3x3/capsule SDF "
|
||||
"at %.3f us/call (15x15=225 cell rolls only on rebuild; inner 3x3/capsule SDF "
|
||||
"only on hot calls; sink=%.9g)."),
|
||||
NumRebuildSamples,
|
||||
RebuildSeconds * 1.0e6 / static_cast<double>(NumRebuildSamples),
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -672,8 +672,8 @@ namespace
|
||||
(MaxLateralSnap + FMath::Max(Params.ShaftMaxRadius, 1.0f)) / Spacing) + 2;
|
||||
|
||||
float BestDistSq = FLT_MAX;
|
||||
float BestX = 0.0f;
|
||||
float BestY = 0.0f;
|
||||
float BestAxisX = 0.0f;
|
||||
float BestAxisY = 0.0f;
|
||||
bool bFound = false;
|
||||
int32 BestCellX = INT32_MAX;
|
||||
int32 BestCellY = INT32_MAX;
|
||||
@@ -711,17 +711,12 @@ namespace
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Distance <= SafeRadius || Distance <= KINDA_SMALL_NUMBER)
|
||||
{
|
||||
BestX = WorldX;
|
||||
BestY = WorldY;
|
||||
}
|
||||
else
|
||||
{
|
||||
const float Scale = SafeRadius / Distance;
|
||||
BestX = ShaftX + DXWorld * Scale;
|
||||
BestY = ShaftY + DYWorld * Scale;
|
||||
}
|
||||
// A VerticalShafts landing must identify the topology, not merely an open point
|
||||
// inside its radius. The safe-radius distance still chooses the nearest confident
|
||||
// site under the existing snap budget, but the returned XY is the selected shaft's
|
||||
// exact axis so it is on the deterministic drainage tree by construction.
|
||||
BestAxisX = ShaftX;
|
||||
BestAxisY = ShaftY;
|
||||
BestDistSq = DistSq;
|
||||
BestCellX = CellX;
|
||||
BestCellY = CellY;
|
||||
@@ -729,13 +724,13 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
if (!bFound || BestDistSq > MaxSnapSq || !FMath::IsFinite(BestX)
|
||||
|| !FMath::IsFinite(BestY))
|
||||
if (!bFound || BestDistSq > MaxSnapSq || !FMath::IsFinite(BestAxisX)
|
||||
|| !FMath::IsFinite(BestAxisY))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OutPoint = FVector(BestX, BestY, LandingZ);
|
||||
OutPoint = FVector(BestAxisX, BestAxisY, LandingZ);
|
||||
return !OutPoint.ContainsNaN() && FMath::IsFinite(OutPoint.X)
|
||||
&& FMath::IsFinite(OutPoint.Y) && FMath::IsFinite(OutPoint.Z);
|
||||
}
|
||||
|
||||
@@ -169,6 +169,42 @@ namespace
|
||||
return (Sdf < DetailThreshold) && (Sdf < FLT_MAX);
|
||||
}
|
||||
|
||||
// Structural tree capsules must meet shaft axes without landing inside a ledge band. This is
|
||||
// the same pure helper as the generator path: one deterministic safe interval is shared by the
|
||||
// whole strate, so a link cannot be blocked when it crosses an unrelated shaft's ledge.
|
||||
FORCEINLINE float VF_SelectVerticalTreeConnectorZ(
|
||||
const FVerticalShaftParams& Params,
|
||||
float BottomZ,
|
||||
float TopZ,
|
||||
uint32 LinkHash)
|
||||
{
|
||||
if (Params.LedgeSpacing > 0.0f && Params.LedgeDepth > 0.0f)
|
||||
{
|
||||
const float Period = Params.LedgeSpacing;
|
||||
const float RelativeBottom = BottomZ - Params.StrateBottomWorldZ;
|
||||
const float RelativeTop = TopZ - Params.StrateBottomWorldZ;
|
||||
const int32 FirstPeriod = FMath::FloorToInt(RelativeBottom / Period);
|
||||
const float Random01 = VoxelHash::ToFloat01(VoxelHash::Mix(LinkHash));
|
||||
|
||||
for (int32 PeriodOffset = 0; PeriodOffset <= 1; ++PeriodOffset)
|
||||
{
|
||||
const float PeriodStart = static_cast<float>(FirstPeriod + PeriodOffset) * Period;
|
||||
const float SafeStart = FMath::Max(
|
||||
RelativeBottom, PeriodStart + Params.LedgeDepth + 0.01f);
|
||||
const float SafeEnd = FMath::Min(
|
||||
RelativeTop, PeriodStart + Period - Params.LedgeDepth - 0.01f);
|
||||
if (SafeEnd > SafeStart)
|
||||
{
|
||||
return Params.StrateBottomWorldZ
|
||||
+ FMath::Lerp(SafeStart, SafeEnd, Random01);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return FMath::Lerp(BottomZ, TopZ,
|
||||
VoxelHash::ToFloat01(VoxelHash::Mix(LinkHash)));
|
||||
}
|
||||
|
||||
//=========================================================================
|
||||
// RÔLE 1 — SOURCE : CHAMP CONSTANT / CONSTANT FIELD (roc ET vide)
|
||||
//=========================================================================
|
||||
@@ -1520,7 +1556,8 @@ namespace
|
||||
// séparée aurait dû faire aussi. À revoir si le profil montre que ça compte.
|
||||
//
|
||||
// Kept as ONE op against §6's suggestion: the per-voxel connectors derive from the same inner
|
||||
// 3×3 roll as the shafts, while a rebuild-only 7×7 collection resolves each 5×5 tree window;
|
||||
// 3×3 roll as the shafts, while a rebuild-only 9×9 collection resolves each 5×5 tree window
|
||||
// and fixed ±3 fallback;
|
||||
// the downstream ledge mod needs the inner shaft list anyway. What is forfeited is an exact XY
|
||||
// box verdict on the cylinder half alone.
|
||||
class FShaftFieldSource final : public IVoxelDensityOp
|
||||
@@ -1546,7 +1583,7 @@ namespace
|
||||
struct FCells
|
||||
{
|
||||
TArray<FShaft, TInlineAllocator<10>> Shafts;
|
||||
TArray<FConn, TInlineAllocator<24>> Conns;
|
||||
TArray<FConn, TInlineAllocator<32>> Conns;
|
||||
};
|
||||
|
||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
@@ -1612,21 +1649,37 @@ namespace
|
||||
// it may over-enumerate, but it must never miss a cached tree capsule.
|
||||
//-----------------------------------------------------------------
|
||||
constexpr int32 CandidateRadius = 2;
|
||||
constexpr int32 FallbackRadius = 3;
|
||||
constexpr uint32 TreeSalt = 0x7A11u;
|
||||
const int32 EmitX0 = FMath::FloorToInt((float)VoxelBox.Min.X / Spacing) - 1;
|
||||
const int32 EmitX1 = FMath::FloorToInt((float)VoxelBox.Max.X / Spacing) + 1;
|
||||
const int32 EmitY0 = FMath::FloorToInt((float)VoxelBox.Min.Y / Spacing) - 1;
|
||||
const int32 EmitY1 = FMath::FloorToInt((float)VoxelBox.Max.Y / Spacing) + 1;
|
||||
const float TreeRadius = SpineConnectorRadius();
|
||||
const int32 ConnectorCellPad = FMath::Max(
|
||||
1, FMath::CeilToInt(TreeRadius / Spacing));
|
||||
const int32 TreeEmitRadius = FallbackRadius + ConnectorCellPad;
|
||||
const int32 QueryX0 = FMath::FloorToInt((float)VoxelBox.Min.X / Spacing) - 1;
|
||||
const int32 QueryX1 = FMath::FloorToInt((float)VoxelBox.Max.X / Spacing) + 1;
|
||||
const int32 QueryY0 = FMath::FloorToInt((float)VoxelBox.Min.Y / Spacing) - 1;
|
||||
const int32 QueryY1 = FMath::FloorToInt((float)VoxelBox.Max.Y / Spacing) + 1;
|
||||
const int32 TreeEmitX0 = FMath::FloorToInt((float)VoxelBox.Min.X / Spacing)
|
||||
- TreeEmitRadius;
|
||||
const int32 TreeEmitX1 = FMath::FloorToInt((float)VoxelBox.Max.X / Spacing)
|
||||
+ TreeEmitRadius;
|
||||
const int32 TreeEmitY0 = FMath::FloorToInt((float)VoxelBox.Min.Y / Spacing)
|
||||
- TreeEmitRadius;
|
||||
const int32 TreeEmitY1 = FMath::FloorToInt((float)VoxelBox.Max.Y / Spacing)
|
||||
+ TreeEmitRadius;
|
||||
|
||||
TArray<FShaft, TInlineAllocator<64>> TreeCandidates;
|
||||
TArray<FShaft, TInlineAllocator<32>> TreeEmit;
|
||||
for (int32 cy = EmitY0 - CandidateRadius; cy <= EmitY1 + CandidateRadius; ++cy)
|
||||
for (int32 cx = EmitX0 - CandidateRadius; cx <= EmitX1 + CandidateRadius; ++cx)
|
||||
TArray<FShaft, TInlineAllocator<225>> TreeCandidates;
|
||||
TArray<FShaft, TInlineAllocator<81>> TreeEmit;
|
||||
for (int32 cy = TreeEmitY0 - FallbackRadius;
|
||||
cy <= TreeEmitY1 + FallbackRadius; ++cy)
|
||||
for (int32 cx = TreeEmitX0 - FallbackRadius;
|
||||
cx <= TreeEmitX1 + FallbackRadius; ++cx)
|
||||
{
|
||||
FShaft Sh;
|
||||
if (!RollShaft(cx, cy, Sh)) { continue; }
|
||||
TreeCandidates.Add(Sh);
|
||||
if (cx >= EmitX0 && cx <= EmitX1 && cy >= EmitY0 && cy <= EmitY1)
|
||||
if (cx >= TreeEmitX0 && cx <= TreeEmitX1
|
||||
&& cy >= TreeEmitY0 && cy <= TreeEmitY1)
|
||||
{
|
||||
TreeEmit.Add(Sh);
|
||||
}
|
||||
@@ -1634,7 +1687,6 @@ namespace
|
||||
|
||||
const float BottomZ = P.StrateBottomWorldZ + P.BoundarySealThickness;
|
||||
const float TopZ = P.StrateTopWorldZ - P.BoundarySealThickness;
|
||||
const float TreeRadius = SpineConnectorRadius();
|
||||
const float RMinZ = (float)VoxelBox.Min.Z, RMaxZ = (float)VoxelBox.Max.Z;
|
||||
const FVector2D CtrXY(0.5f * (float)(VoxelBox.Min.X + VoxelBox.Max.X),
|
||||
0.5f * (float)(VoxelBox.Min.Y + VoxelBox.Max.Y));
|
||||
@@ -1689,14 +1741,58 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
if (Parent == nullptr
|
||||
&& !(FMath::Abs(Child.CellX) <= CandidateRadius
|
||||
&& FMath::Abs(Child.CellY) <= CandidateRadius))
|
||||
{
|
||||
// Keep EffectOverBox's parent resolver identical to GetCells. A local
|
||||
// minimum chooses the nearest lower-origin shaft in the collected halo; the
|
||||
// monotone origin-distance key proves that the resulting tree reaches the
|
||||
// spine and cannot cycle. If the finite halo is empty, the direct spine is the
|
||||
// conservative final fallback.
|
||||
for (const FShaft& Candidate : TreeCandidates)
|
||||
{
|
||||
if (Candidate.CellX == Child.CellX && Candidate.CellY == Child.CellY)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (FMath::Abs(Candidate.CellX - Child.CellX) > FallbackRadius
|
||||
|| FMath::Abs(Candidate.CellY - Child.CellY) > FallbackRadius)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const float CandidateOriginSq = FMath::Square(Candidate.X)
|
||||
+ FMath::Square(Candidate.Y);
|
||||
if (!(CandidateOriginSq < ChildOriginSq))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const float DistanceSq = FMath::Square(Child.X - Candidate.X)
|
||||
+ FMath::Square(Child.Y - Candidate.Y);
|
||||
const bool bLowerCell = Parent == nullptr
|
||||
|| Candidate.CellY < Parent->CellY
|
||||
|| (Candidate.CellY == Parent->CellY
|
||||
&& Candidate.CellX < Parent->CellX);
|
||||
if (DistanceSq < BestDistanceSq
|
||||
|| (DistanceSq == BestDistanceSq && bLowerCell))
|
||||
{
|
||||
BestDistanceSq = DistanceSq;
|
||||
Parent = &Candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const int32 ParentCellX = Parent != nullptr ? Parent->CellX : 0;
|
||||
const int32 ParentCellY = Parent != nullptr ? Parent->CellY : 0;
|
||||
const uint32 LinkHash = VoxelHash::Pair(
|
||||
Child.CellX, Child.CellY, ParentCellX, ParentCellY, Salt ^ TreeSalt);
|
||||
const float Zc = FMath::Lerp(
|
||||
const float Zc = VF_SelectVerticalTreeConnectorZ(
|
||||
P,
|
||||
P.StrateBottomWorldZ + P.BoundarySealThickness,
|
||||
P.StrateTopWorldZ - P.BoundarySealThickness,
|
||||
VoxelHash::ToFloat01(VoxelHash::Mix(LinkHash)));
|
||||
LinkHash);
|
||||
const FVector ParentPoint = Parent != nullptr
|
||||
? FVector(Parent->X, Parent->Y, Zc)
|
||||
: FVector(0.0f, 0.0f, Zc);
|
||||
@@ -1721,7 +1817,7 @@ namespace
|
||||
// du voisinage inner 3×3 de la cellule DE SA REQUÊTE. Une paire random visible depuis
|
||||
// un point de la boîte a donc ses deux puits dans [cellules de la boîte] ± 1, qui est
|
||||
// exactement la plage balayée ici. Les connecteurs de l'arbre sont testés séparément
|
||||
// au-dessus avec leur collecte 7×7/émission inner 3×3. On peut produire des paires que
|
||||
// au-dessus avec leur collecte élargie/émission du halo géométrique. On peut produire des paires que
|
||||
// personne ne voit jamais : c'est du `CarveOnly` en trop, pas un trou.
|
||||
//
|
||||
// ⚠️ ET POURQUOI L'ORDRE (A,B) EST LE MÊME QUE CELUI DE `GetCells`. Le hash de paire est
|
||||
@@ -1738,10 +1834,10 @@ namespace
|
||||
// insertion order, so each pair gets the same hash without assuming Pair() is symmetric.
|
||||
if (P.CrossConnectChance > 0.0f)
|
||||
{
|
||||
const int32 QX0 = EmitX0;
|
||||
const int32 QX1 = EmitX1;
|
||||
const int32 QY0 = EmitY0;
|
||||
const int32 QY1 = EmitY1;
|
||||
const int32 QX0 = QueryX0;
|
||||
const int32 QX1 = QueryX1;
|
||||
const int32 QY0 = QueryY0;
|
||||
const int32 QY1 = QueryY1;
|
||||
|
||||
TArray<FShaft, TInlineAllocator<32>> Near;
|
||||
for (int32 cy = QY0; cy <= QY1; ++cy)
|
||||
@@ -1825,7 +1921,7 @@ namespace
|
||||
}
|
||||
|
||||
/** Le voisinage inner 3×3 + ses connecteurs, mémoïsés par worker. Le rebuild collecte un
|
||||
* 7×7 temporaire pour résoudre les fenêtres parent 5×5. Clé = cellule + tous les params
|
||||
* halo 9×9 émetteur / 15×15 total pour résoudre les fenêtres parent 5×5 et le fallback fixe ±3. Clé = cellule + tous les params
|
||||
* qui influent (comme l'original) : stable à travers les reconstructions de pile, ce qui
|
||||
* est la leçon retenue du mémo de colonne de SurfaceWorld. */
|
||||
const FCells& GetCells(float WorldX, float WorldY) const
|
||||
@@ -1858,35 +1954,80 @@ namespace
|
||||
Cache.Shafts.Reset();
|
||||
Cache.Conns.Reset();
|
||||
|
||||
constexpr int32 CollectRadius = 3; // 7×7: inner 3×3 + one 5×5 candidate halo
|
||||
constexpr int32 EmitRadius = 1; // 3×3: the only cells allowed to emit tree links
|
||||
constexpr int32 EmitRadius = 1; // 3×3: shaft cylinders and random links
|
||||
constexpr int32 CandidateRadius = 2; // 5×5 parent window around an emitting shaft
|
||||
constexpr int32 FallbackRadius = 3; // fixed fallback window around an emitting shaft
|
||||
constexpr uint32 TreeSalt = 0x7A11u;
|
||||
|
||||
TArray<FShaft, TInlineAllocator<49>> CollectedShafts;
|
||||
const float BottomZ = P.StrateBottomWorldZ + P.BoundarySealThickness;
|
||||
const float TopZ = P.StrateTopWorldZ - P.BoundarySealThickness;
|
||||
const float TreeRadius = SpineConnectorRadius();
|
||||
// A tree capsule can affect a cell even when neither endpoint is in that cell.
|
||||
// Keep a rebuild-only child halo for that geometric reach, then add the
|
||||
// parent-search halo. With the defaults this is 4 + 3 cells = 15×15 rolls.
|
||||
const int32 ConnectorCellPad = FMath::Max(
|
||||
1, FMath::CeilToInt(TreeRadius / Spacing));
|
||||
const int32 TreeEmitRadius = FallbackRadius + ConnectorCellPad;
|
||||
const int32 CollectRadius = TreeEmitRadius + FallbackRadius;
|
||||
|
||||
const int32 CollectSide = CollectRadius * 2 + 1;
|
||||
TArray<FShaft, TInlineAllocator<225>> ShaftGrid;
|
||||
TArray<uint8, TInlineAllocator<225>> ShaftPresent;
|
||||
TArray<FShaft, TInlineAllocator<81>> TreeEmitShafts;
|
||||
ShaftGrid.SetNum(CollectSide * CollectSide);
|
||||
ShaftPresent.Init(0, CollectSide * CollectSide);
|
||||
// Rebuild-only wide collection. `Cache.Shafts` remains the inner 3×3 working set
|
||||
// used by Eval and the ledge modifier, so the 7×7 cost is not paid per voxel.
|
||||
// used by Eval and the ledge modifier; the wider tree work is not paid per voxel.
|
||||
for (int32 dy = -CollectRadius; dy <= CollectRadius; dy++)
|
||||
for (int32 dx = -CollectRadius; dx <= CollectRadius; dx++)
|
||||
{
|
||||
FShaft Sh;
|
||||
if (!RollShaft(CX + dx, CY + dy, Sh)) { continue; }
|
||||
CollectedShafts.Add(Sh);
|
||||
const int32 GridIndex = (dy + CollectRadius) * CollectSide + (dx + CollectRadius);
|
||||
ShaftGrid[GridIndex] = Sh;
|
||||
ShaftPresent[GridIndex] = 1;
|
||||
if (FMath::Abs(dx) <= EmitRadius && FMath::Abs(dy) <= EmitRadius)
|
||||
{
|
||||
Cache.Shafts.Add(Sh);
|
||||
}
|
||||
if (FMath::Abs(dx) <= TreeEmitRadius && FMath::Abs(dy) <= TreeEmitRadius)
|
||||
{
|
||||
TreeEmitShafts.Add(Sh);
|
||||
}
|
||||
}
|
||||
|
||||
auto FindCollectedShaft = [&](int32 CellX, int32 CellY) -> const FShaft*
|
||||
{
|
||||
const int32 dx = CellX - CX;
|
||||
const int32 dy = CellY - CY;
|
||||
if (FMath::Abs(dx) > CollectRadius || FMath::Abs(dy) > CollectRadius)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
const int32 GridIndex = (dy + CollectRadius) * CollectSide + (dx + CollectRadius);
|
||||
return ShaftPresent[GridIndex] ? &ShaftGrid[GridIndex] : nullptr;
|
||||
};
|
||||
|
||||
const bool bOriginNearby = FMath::Abs(CX) <= 1 && FMath::Abs(CY) <= 1;
|
||||
if (bOriginNearby && SpineRadius > 0.0f)
|
||||
{
|
||||
Cache.Shafts.Add({0.0f, 0.0f, SpineRadius, 0, 0, true});
|
||||
}
|
||||
|
||||
const float BottomZ = P.StrateBottomWorldZ + P.BoundarySealThickness;
|
||||
const float TopZ = P.StrateTopWorldZ - P.BoundarySealThickness;
|
||||
const float TreeRadius = SpineConnectorRadius();
|
||||
const float CellMinX = static_cast<float>(CX) * Spacing;
|
||||
const float CellMaxX = static_cast<float>(CX + 1) * Spacing;
|
||||
const float CellMinY = static_cast<float>(CY) * Spacing;
|
||||
const float CellMaxY = static_cast<float>(CY + 1) * Spacing;
|
||||
auto ConnectorMayReachCell = [&](const FConn& Conn)
|
||||
{
|
||||
const float MinX = FMath::Min(Conn.A.X, Conn.B.X) - Conn.Radius;
|
||||
const float MaxX = FMath::Max(Conn.A.X, Conn.B.X) + Conn.Radius;
|
||||
const float MinY = FMath::Min(Conn.A.Y, Conn.B.Y) - Conn.Radius;
|
||||
const float MaxY = FMath::Max(Conn.A.Y, Conn.B.Y) + Conn.Radius;
|
||||
const float GapX = FMath::Max3(CellMinX - MaxX, MinX - CellMaxX, 0.0f);
|
||||
const float GapY = FMath::Max3(CellMinY - MaxY, MinY - CellMaxY, 0.0f);
|
||||
return GapX == 0.0f && GapY == 0.0f;
|
||||
};
|
||||
|
||||
auto EmitTreeConnector = [&](const FShaft& Child, const FShaft* Parent)
|
||||
{
|
||||
@@ -1894,55 +2035,103 @@ namespace
|
||||
const int32 ParentCellY = Parent != nullptr ? Parent->CellY : 0;
|
||||
const uint32 LinkHash = VoxelHash::Pair(
|
||||
Child.CellX, Child.CellY, ParentCellX, ParentCellY, Salt ^ TreeSalt);
|
||||
const float Zc = FMath::Lerp(
|
||||
BottomZ, TopZ, VoxelHash::ToFloat01(VoxelHash::Mix(LinkHash)));
|
||||
const float Zc = VF_SelectVerticalTreeConnectorZ(
|
||||
P, BottomZ, TopZ, LinkHash);
|
||||
const FVector ParentPoint = Parent != nullptr
|
||||
? FVector(Parent->X, Parent->Y, Zc)
|
||||
: FVector(0.0f, 0.0f, Zc);
|
||||
Cache.Conns.Add({ FVector(Child.X, Child.Y, Zc), ParentPoint, TreeRadius });
|
||||
const FConn Conn{
|
||||
FVector(Child.X, Child.Y, Zc), ParentPoint, TreeRadius };
|
||||
if (ConnectorMayReachCell(Conn))
|
||||
{
|
||||
Cache.Conns.Add(Conn);
|
||||
}
|
||||
};
|
||||
|
||||
// Structural drainage tree. Every real shaft emitted by this cache chooses the
|
||||
// nearest strictly more-central shaft in its complete 5×5 window; a local minimum
|
||||
// drains to the spine. The explicit row-major cell tie-break is independent of
|
||||
// array iteration order, and the synthetic spine is not a candidate shaft.
|
||||
for (const FShaft& Child : Cache.Shafts)
|
||||
// Structural drainage tree. Every real shaft in the rebuild-only child window
|
||||
// first chooses the nearest strictly more-central shaft in its complete 5×5
|
||||
// window. A local minimum falls back to the nearest lower-origin shaft in the
|
||||
// deterministic halo; a window touching the origin uses the spine directly, with
|
||||
// a final direct-spine fallback for an unusually empty finite halo. Every parent
|
||||
// edge therefore either strictly decreases origin distance or terminates at the
|
||||
// spine: no cycles and no orphans. Spatial culling makes the same edge visible
|
||||
// throughout its capsule, independent of which cache cell evaluates it.
|
||||
for (const FShaft& Child : TreeEmitShafts)
|
||||
{
|
||||
if (Child.bOriginSpine) continue;
|
||||
|
||||
const float ChildOriginSq = FMath::Square(Child.X) + FMath::Square(Child.Y);
|
||||
const FShaft* Parent = nullptr;
|
||||
float BestDistanceSq = FLT_MAX;
|
||||
for (const FShaft& Candidate : CollectedShafts)
|
||||
for (int32 dy = -CandidateRadius; dy <= CandidateRadius; ++dy)
|
||||
for (int32 dx = -CandidateRadius; dx <= CandidateRadius; ++dx)
|
||||
{
|
||||
if (Candidate.CellX == Child.CellX && Candidate.CellY == Child.CellY)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (FMath::Abs(Candidate.CellX - Child.CellX) > CandidateRadius
|
||||
|| FMath::Abs(Candidate.CellY - Child.CellY) > CandidateRadius)
|
||||
const FShaft* Candidate = FindCollectedShaft(
|
||||
Child.CellX + dx, Child.CellY + dy);
|
||||
if (Candidate == nullptr
|
||||
|| (Candidate->CellX == Child.CellX && Candidate->CellY == Child.CellY))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const float CandidateOriginSq = FMath::Square(Candidate.X)
|
||||
+ FMath::Square(Candidate.Y);
|
||||
const float CandidateOriginSq = FMath::Square(Candidate->X)
|
||||
+ FMath::Square(Candidate->Y);
|
||||
if (!(CandidateOriginSq < ChildOriginSq))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const float DistanceSq = FMath::Square(Child.X - Candidate.X)
|
||||
+ FMath::Square(Child.Y - Candidate.Y);
|
||||
const float DistanceSq = FMath::Square(Child.X - Candidate->X)
|
||||
+ FMath::Square(Child.Y - Candidate->Y);
|
||||
const bool bLowerCell = Parent == nullptr
|
||||
|| Candidate.CellY < Parent->CellY
|
||||
|| (Candidate.CellY == Parent->CellY
|
||||
&& Candidate.CellX < Parent->CellX);
|
||||
|| Candidate->CellY < Parent->CellY
|
||||
|| (Candidate->CellY == Parent->CellY
|
||||
&& Candidate->CellX < Parent->CellX);
|
||||
if (DistanceSq < BestDistanceSq
|
||||
|| (DistanceSq == BestDistanceSq && bLowerCell))
|
||||
{
|
||||
BestDistanceSq = DistanceSq;
|
||||
Parent = &Candidate;
|
||||
Parent = Candidate;
|
||||
}
|
||||
}
|
||||
|
||||
if (Parent == nullptr
|
||||
&& !(FMath::Abs(Child.CellX) <= CandidateRadius
|
||||
&& FMath::Abs(Child.CellY) <= CandidateRadius))
|
||||
{
|
||||
// Specified neighbour-shaft fallback. The lower-origin restriction is the
|
||||
// proof that this fallback cannot create a cycle.
|
||||
for (int32 dy = -FallbackRadius; dy <= FallbackRadius; ++dy)
|
||||
for (int32 dx = -FallbackRadius; dx <= FallbackRadius; ++dx)
|
||||
{
|
||||
const FShaft* Candidate = FindCollectedShaft(
|
||||
Child.CellX + dx, Child.CellY + dy);
|
||||
if (Candidate == nullptr
|
||||
|| (Candidate->CellX == Child.CellX
|
||||
&& Candidate->CellY == Child.CellY))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const float CandidateOriginSq = FMath::Square(Candidate->X)
|
||||
+ FMath::Square(Candidate->Y);
|
||||
if (!(CandidateOriginSq < ChildOriginSq))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const float DistanceSq = FMath::Square(Child.X - Candidate->X)
|
||||
+ FMath::Square(Child.Y - Candidate->Y);
|
||||
const bool bLowerCell = Parent == nullptr
|
||||
|| Candidate->CellY < Parent->CellY
|
||||
|| (Candidate->CellY == Parent->CellY
|
||||
&& Candidate->CellX < Parent->CellX);
|
||||
if (DistanceSq < BestDistanceSq
|
||||
|| (DistanceSq == BestDistanceSq && bLowerCell))
|
||||
{
|
||||
BestDistanceSq = DistanceSq;
|
||||
Parent = Candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1950,7 +2139,7 @@ namespace
|
||||
}
|
||||
|
||||
// Existing random links remain the texture/loop layer and stay on the inner 3×3
|
||||
// set. Only the deterministic parent lookup uses the 7×7 collection.
|
||||
// set. Only the deterministic tree build uses the wider 9×9/15×15 collection.
|
||||
if (P.CrossConnectChance > 0.0f && Cache.Shafts.Num() >= 2)
|
||||
{
|
||||
for (int32 i = 0; i < Cache.Shafts.Num(); i++)
|
||||
@@ -1973,7 +2162,12 @@ namespace
|
||||
const float Zc = FMath::Lerp(BottomZ, TopZ, VoxelHash::ToFloat01(VoxelHash::Mix(PH)));
|
||||
const float ConnectorR = bSpineConnector
|
||||
? SpineConnectorRadius() : P.ConnectorRadius;
|
||||
Cache.Conns.Add({ FVector(A.X, A.Y, Zc), FVector(B.X, B.Y, Zc), ConnectorR });
|
||||
const FConn Conn{
|
||||
FVector(A.X, A.Y, Zc), FVector(B.X, B.Y, Zc), ConnectorR };
|
||||
if (ConnectorMayReachCell(Conn))
|
||||
{
|
||||
Cache.Conns.Add(Conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2024,7 +2218,10 @@ namespace
|
||||
const float D2 = FMath::Square(WorldX - Sh.X) + FMath::Square(WorldY - Sh.Y);
|
||||
if (D2 < BestSq) { BestSq = D2; Near = &Sh; }
|
||||
}
|
||||
if (Near && (WorldX - Near->X) + (WorldY - Near->Y) > 0.0f)
|
||||
// The shaft axis is the structural tree's terminal point. Keep the mathematical
|
||||
// half-plane boundary open: tiny cancellation error must not turn an exact axis
|
||||
// landing into a solid ledge cap and sever the parent capsule.
|
||||
if (Near && (WorldX - Near->X) + (WorldY - Near->Y) > 1.0e-3f)
|
||||
{
|
||||
const float Shelf = 1.0f - SmoothStep01(BandT / P.LedgeDepth);
|
||||
InOut.Density = FMath::Max(InOut.Density, Shelf * P.BaseDensity);
|
||||
|
||||
@@ -226,6 +226,44 @@ static float CellularNoise3D(const FVector& Position)
|
||||
return VoxelNoise::Cellular3D(Position);
|
||||
}
|
||||
|
||||
// Structural tree capsules must meet shaft axes without landing inside a ledge band. A single
|
||||
// deterministic safe interval is enough for the whole tree: every shaft in one strate shares the
|
||||
// same ledge phase, and keeping every tree link in that interval also keeps crossings of unrelated
|
||||
// shafts out of their additive ledges. The fallback is only for malformed settings with no safe
|
||||
// interval; the stock params always take the proven branch.
|
||||
static float VF_SelectVerticalTreeConnectorZ(
|
||||
const FVerticalShaftParams& Params,
|
||||
float BottomZ,
|
||||
float TopZ,
|
||||
uint32 LinkHash)
|
||||
{
|
||||
if (Params.LedgeSpacing > 0.0f && Params.LedgeDepth > 0.0f)
|
||||
{
|
||||
const float Period = Params.LedgeSpacing;
|
||||
const float RelativeBottom = BottomZ - Params.StrateBottomWorldZ;
|
||||
const float RelativeTop = TopZ - Params.StrateBottomWorldZ;
|
||||
const int32 FirstPeriod = FMath::FloorToInt(RelativeBottom / Period);
|
||||
const float Random01 = VoxelHash::ToFloat01(VoxelHash::Mix(LinkHash));
|
||||
|
||||
for (int32 PeriodOffset = 0; PeriodOffset <= 1; ++PeriodOffset)
|
||||
{
|
||||
const float PeriodStart = static_cast<float>(FirstPeriod + PeriodOffset) * Period;
|
||||
const float SafeStart = FMath::Max(
|
||||
RelativeBottom, PeriodStart + Params.LedgeDepth + 0.01f);
|
||||
const float SafeEnd = FMath::Min(
|
||||
RelativeTop, PeriodStart + Period - Params.LedgeDepth - 0.01f);
|
||||
if (SafeEnd > SafeStart)
|
||||
{
|
||||
return Params.StrateBottomWorldZ
|
||||
+ FMath::Lerp(SafeStart, SafeEnd, Random01);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return FMath::Lerp(BottomZ, TopZ,
|
||||
VoxelHash::ToFloat01(VoxelHash::Mix(LinkHash)));
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// DENSITY PIPELINE HELPERS (partagés entre TunnelNetwork et Slab)
|
||||
//=============================================================================
|
||||
@@ -3739,16 +3777,17 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
|
||||
const int32 CY = FMath::FloorToInt(WorldY / Spacing);
|
||||
|
||||
// The shaft topology is pure, but it is deliberately split into two windows:
|
||||
// * COLLECT 7×7 cells once during this thread-local cache rebuild. This is the source data
|
||||
// needed to resolve every 5×5 parent window for the emitted cells below.
|
||||
// * EMIT/EVALUATE only the INNER 3×3 cells. A shaft in that inner window has its whole
|
||||
// 5×5 candidate window inside the collected 7×7, so two neighbouring evaluation cells
|
||||
// compute the same parent for the same shaft. The 7×7 work is therefore rebuild-only;
|
||||
// the per-voxel loop still sees at most the inner 3×3 shafts plus their cached capsules.
|
||||
// * COLLECT a deterministic halo once during this thread-local cache rebuild. This is the
|
||||
// source data needed to resolve every 5×5 parent window and the fixed ±3 fallback window.
|
||||
// * EVALUATE only the INNER 3×3 shaft cylinders. Tree links are collected from a wider
|
||||
// rebuild-only child window and culled to the current cell before the voxel loop sees
|
||||
// them. That matters: a capsule from a child to a parent two or three cells away must be
|
||||
// present in the cells along its segment, not only in the cache centred on the child.
|
||||
struct FLocalShaft { float X, Y, R; int32 CellX, CellY; bool bOriginSpine; };
|
||||
struct FLocalConn { FVector A, B; float Radius; };
|
||||
thread_local TArray<FLocalShaft, TInlineAllocator<10>> Shafts;
|
||||
thread_local TArray<FLocalConn, TInlineAllocator<24>> Conns;
|
||||
thread_local TArray<FLocalConn, TInlineAllocator<32>> Conns;
|
||||
thread_local TArray<FLocalShaft, TInlineAllocator<81>> TreeEmitShafts;
|
||||
thread_local int32 VS_CX = INT32_MAX, VS_CY = INT32_MAX;
|
||||
thread_local uint32 VS_Seed = 0xFFFFFFFFu;
|
||||
thread_local float VS_Spacing = -1.0f, VS_Dens = -1.0f, VS_MinR = -1.0f, VS_MaxR = -1.0f,
|
||||
@@ -3771,15 +3810,37 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
|
||||
VS_Seal = Params.BoundarySealThickness;
|
||||
Shafts.Reset();
|
||||
Conns.Reset();
|
||||
TreeEmitShafts.Reset();
|
||||
|
||||
constexpr int32 CollectRadius = 3; // 7×7: inner 3×3 + one 5×5 candidate halo
|
||||
constexpr int32 EmitRadius = 1; // 3×3: the only cells allowed to emit tree links
|
||||
constexpr int32 EmitRadius = 1; // 3×3: shaft cylinders and random links
|
||||
constexpr int32 CandidateRadius = 2; // 5×5 parent window around an emitting shaft
|
||||
constexpr int32 FallbackRadius = 3; // fixed fallback window around an emitting shaft
|
||||
constexpr uint32 TreeSalt = 0x7A11u;
|
||||
|
||||
TArray<FLocalShaft, TInlineAllocator<49>> CollectedShafts;
|
||||
const float BottomZ = Params.StrateBottomWorldZ + Params.BoundarySealThickness;
|
||||
const float TopZ = Params.StrateTopWorldZ - Params.BoundarySealThickness;
|
||||
const float RoughnessReach = FMath::Max(Params.SurfaceRoughness, 0.0f)
|
||||
* VOXEL_NOISE_SCALE * 1.5f;
|
||||
// A tree link must remain open at the centreline after the SDF roughness pass. The +1
|
||||
// margin makes the radius strictly greater than the proven roughness supremum.
|
||||
const float TreeConnectorRadius = FMath::Max(Params.ConnectorRadius, RoughnessReach + 1.0f);
|
||||
// A tree capsule can affect a cell even when neither endpoint is in that cell. Keep a
|
||||
// rebuild-only child halo for that geometric reach, then add the parent-search halo. The
|
||||
// default is a 4-cell tree-emission radius + 3-cell collection pad = 7-cell radius
|
||||
// (15×15 rolls; the emitted tree-child box itself is 9×9).
|
||||
const int32 ConnectorCellPad = FMath::Max(
|
||||
1, FMath::CeilToInt(TreeConnectorRadius / Spacing));
|
||||
const int32 TreeEmitRadius = FallbackRadius + ConnectorCellPad;
|
||||
const int32 CollectRadius = TreeEmitRadius + FallbackRadius;
|
||||
|
||||
const int32 CollectSide = CollectRadius * 2 + 1;
|
||||
TArray<FLocalShaft, TInlineAllocator<225>> ShaftGrid;
|
||||
TArray<uint8, TInlineAllocator<225>> ShaftPresent;
|
||||
ShaftGrid.SetNum(CollectSide * CollectSide);
|
||||
ShaftPresent.Init(0, CollectSide * CollectSide);
|
||||
// Collect the wider deterministic neighbourhood once. Only the inner cells are retained
|
||||
// in `Shafts`, because that is the per-voxel working set and the seam-free emission set.
|
||||
// in `Shafts` for the per-voxel cylinder/ledge work; `TreeEmitShafts` is still rebuilt
|
||||
// before evaluation and its connectors are spatially culled below.
|
||||
for (int32 dy = -CollectRadius; dy <= CollectRadius; dy++)
|
||||
for (int32 dx = -CollectRadius; dx <= CollectRadius; dx++)
|
||||
{
|
||||
@@ -3797,13 +3858,31 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
|
||||
Sh.CellX = nx;
|
||||
Sh.CellY = ny;
|
||||
Sh.bOriginSpine = false;
|
||||
CollectedShafts.Add(Sh);
|
||||
const int32 GridIndex = (dy + CollectRadius) * CollectSide + (dx + CollectRadius);
|
||||
ShaftGrid[GridIndex] = Sh;
|
||||
ShaftPresent[GridIndex] = 1;
|
||||
if (FMath::Abs(dx) <= EmitRadius && FMath::Abs(dy) <= EmitRadius)
|
||||
{
|
||||
Shafts.Add(Sh);
|
||||
}
|
||||
if (FMath::Abs(dx) <= TreeEmitRadius && FMath::Abs(dy) <= TreeEmitRadius)
|
||||
{
|
||||
TreeEmitShafts.Add(Sh);
|
||||
}
|
||||
}
|
||||
|
||||
auto FindCollectedShaft = [&](int32 CellX, int32 CellY) -> const FLocalShaft*
|
||||
{
|
||||
const int32 dx = CellX - CX;
|
||||
const int32 dy = CellY - CY;
|
||||
if (FMath::Abs(dx) > CollectRadius || FMath::Abs(dy) > CollectRadius)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
const int32 GridIndex = (dy + CollectRadius) * CollectSide + (dx + CollectRadius);
|
||||
return ShaftPresent[GridIndex] ? &ShaftGrid[GridIndex] : nullptr;
|
||||
};
|
||||
|
||||
// The structural (0,0) spine is not re-carved here. It participates only as a
|
||||
// connector endpoint. The local 3x3 set is origin-adjacent when its centre cell is in
|
||||
// the central 3x3; the structural post remains the sole owner of the vertical column.
|
||||
@@ -3813,13 +3892,20 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
|
||||
Shafts.Add({0.0f, 0.0f, OriginSpineRadius, 0, 0, true});
|
||||
}
|
||||
|
||||
const float BottomZ = Params.StrateBottomWorldZ + Params.BoundarySealThickness;
|
||||
const float TopZ = Params.StrateTopWorldZ - Params.BoundarySealThickness;
|
||||
const float RoughnessReach = FMath::Max(Params.SurfaceRoughness, 0.0f)
|
||||
* VOXEL_NOISE_SCALE * 1.5f;
|
||||
// A tree link must remain open at the centreline after the SDF roughness pass. The +1
|
||||
// margin makes the radius strictly greater than the proven roughness supremum.
|
||||
const float TreeConnectorRadius = FMath::Max(Params.ConnectorRadius, RoughnessReach + 1.0f);
|
||||
const float CellMinX = static_cast<float>(CX) * Spacing;
|
||||
const float CellMaxX = static_cast<float>(CX + 1) * Spacing;
|
||||
const float CellMinY = static_cast<float>(CY) * Spacing;
|
||||
const float CellMaxY = static_cast<float>(CY + 1) * Spacing;
|
||||
auto ConnectorMayReachCell = [&](const FLocalConn& Conn)
|
||||
{
|
||||
const float MinX = FMath::Min(Conn.A.X, Conn.B.X) - Conn.Radius;
|
||||
const float MaxX = FMath::Max(Conn.A.X, Conn.B.X) + Conn.Radius;
|
||||
const float MinY = FMath::Min(Conn.A.Y, Conn.B.Y) - Conn.Radius;
|
||||
const float MaxY = FMath::Max(Conn.A.Y, Conn.B.Y) + Conn.Radius;
|
||||
const float GapX = FMath::Max3(CellMinX - MaxX, MinX - CellMaxX, 0.0f);
|
||||
const float GapY = FMath::Max3(CellMinY - MaxY, MinY - CellMaxY, 0.0f);
|
||||
return GapX == 0.0f && GapY == 0.0f;
|
||||
};
|
||||
|
||||
auto EmitTreeConnector = [&](const FLocalShaft& Child, const FLocalShaft* Parent)
|
||||
{
|
||||
@@ -3827,54 +3913,105 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
|
||||
const int32 ParentCellY = Parent != nullptr ? Parent->CellY : 0;
|
||||
const uint32 LinkHash = VoxelHash::Pair(
|
||||
Child.CellX, Child.CellY, ParentCellX, ParentCellY, S ^ TreeSalt);
|
||||
const float Zc = FMath::Lerp(BottomZ, TopZ,
|
||||
VoxelHash::ToFloat01(VoxelHash::Mix(LinkHash)));
|
||||
const float Zc = VF_SelectVerticalTreeConnectorZ(
|
||||
Params, BottomZ, TopZ, LinkHash);
|
||||
const FVector ParentPoint = Parent != nullptr
|
||||
? FVector(Parent->X, Parent->Y, Zc)
|
||||
: FVector(0.0f, 0.0f, Zc);
|
||||
Conns.Add({ FVector(Child.X, Child.Y, Zc), ParentPoint, TreeConnectorRadius });
|
||||
const FLocalConn Conn{
|
||||
FVector(Child.X, Child.Y, Zc), ParentPoint, TreeConnectorRadius };
|
||||
if (ConnectorMayReachCell(Conn))
|
||||
{
|
||||
Conns.Add(Conn);
|
||||
}
|
||||
};
|
||||
|
||||
// Structural drainage tree: every real shaft in the inner 3×3 selects one strictly
|
||||
// more-central shaft from its complete 5×5 window. A local minimum has no such parent and
|
||||
// drains directly to the (0,0) spine. The explicit cell-coordinate tie-break makes this
|
||||
// independent of array/iteration order; the synthetic spine is not a candidate shaft.
|
||||
for (const FLocalShaft& Child : Shafts)
|
||||
// Structural drainage tree: every real shaft in the rebuild-only child window first
|
||||
// selects one strictly more-central shaft from its complete 5×5 window. If that window
|
||||
// has a local minimum, select the nearest shaft in the collected deterministic halo whose
|
||||
// origin distance is lower; that shaft is already on a strictly descending path by the
|
||||
// same rule. A shaft whose window reaches the origin uses the spine directly, and the
|
||||
// final direct-spine fallback covers an unusually empty finite halo. Thus every emitted
|
||||
// parent edge either strictly decreases origin distance or terminates at the spine: no
|
||||
// cycles and no orphans. Spatial culling makes the same edge visible throughout its
|
||||
// capsule, independent of which cache cell is evaluating it.
|
||||
// The explicit cell-coordinate tie-break makes this independent of array iteration order;
|
||||
// the synthetic spine is not a candidate shaft.
|
||||
for (const FLocalShaft& Child : TreeEmitShafts)
|
||||
{
|
||||
if (Child.bOriginSpine) continue;
|
||||
|
||||
const float ChildOriginSq = FMath::Square(Child.X) + FMath::Square(Child.Y);
|
||||
const FLocalShaft* Parent = nullptr;
|
||||
float BestDistanceSq = FLT_MAX;
|
||||
for (const FLocalShaft& Candidate : CollectedShafts)
|
||||
for (int32 dy = -CandidateRadius; dy <= CandidateRadius; ++dy)
|
||||
for (int32 dx = -CandidateRadius; dx <= CandidateRadius; ++dx)
|
||||
{
|
||||
if (Candidate.CellX == Child.CellX && Candidate.CellY == Child.CellY)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (FMath::Abs(Candidate.CellX - Child.CellX) > CandidateRadius
|
||||
|| FMath::Abs(Candidate.CellY - Child.CellY) > CandidateRadius)
|
||||
const FLocalShaft* Candidate = FindCollectedShaft(
|
||||
Child.CellX + dx, Child.CellY + dy);
|
||||
if (Candidate == nullptr
|
||||
|| (Candidate->CellX == Child.CellX && Candidate->CellY == Child.CellY))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const float CandidateOriginSq = FMath::Square(Candidate.X)
|
||||
+ FMath::Square(Candidate.Y);
|
||||
const float CandidateOriginSq = FMath::Square(Candidate->X)
|
||||
+ FMath::Square(Candidate->Y);
|
||||
if (!(CandidateOriginSq < ChildOriginSq))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const float DistanceSq = FMath::Square(Child.X - Candidate.X)
|
||||
+ FMath::Square(Child.Y - Candidate.Y);
|
||||
const float DistanceSq = FMath::Square(Child.X - Candidate->X)
|
||||
+ FMath::Square(Child.Y - Candidate->Y);
|
||||
const bool bLowerCell = Parent == nullptr
|
||||
|| Candidate.CellY < Parent->CellY
|
||||
|| (Candidate.CellY == Parent->CellY && Candidate.CellX < Parent->CellX);
|
||||
|| Candidate->CellY < Parent->CellY
|
||||
|| (Candidate->CellY == Parent->CellY && Candidate->CellX < Parent->CellX);
|
||||
if (DistanceSq < BestDistanceSq
|
||||
|| (DistanceSq == BestDistanceSq && bLowerCell))
|
||||
{
|
||||
BestDistanceSq = DistanceSq;
|
||||
Parent = &Candidate;
|
||||
Parent = Candidate;
|
||||
}
|
||||
}
|
||||
|
||||
if (Parent == nullptr
|
||||
&& !(FMath::Abs(Child.CellX) <= CandidateRadius
|
||||
&& FMath::Abs(Child.CellY) <= CandidateRadius))
|
||||
{
|
||||
// This is the specified neighbour-shaft fallback. Restricting it to a lower
|
||||
// origin distance makes the parent graph well-founded even though the fallback
|
||||
// candidate need not be in the child's 5×5 window.
|
||||
for (int32 dy = -FallbackRadius; dy <= FallbackRadius; ++dy)
|
||||
for (int32 dx = -FallbackRadius; dx <= FallbackRadius; ++dx)
|
||||
{
|
||||
const FLocalShaft* Candidate = FindCollectedShaft(
|
||||
Child.CellX + dx, Child.CellY + dy);
|
||||
if (Candidate == nullptr
|
||||
|| (Candidate->CellX == Child.CellX && Candidate->CellY == Child.CellY))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const float CandidateOriginSq = FMath::Square(Candidate->X)
|
||||
+ FMath::Square(Candidate->Y);
|
||||
if (!(CandidateOriginSq < ChildOriginSq))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const float DistanceSq = FMath::Square(Child.X - Candidate->X)
|
||||
+ FMath::Square(Child.Y - Candidate->Y);
|
||||
const bool bLowerCell = Parent == nullptr
|
||||
|| Candidate->CellY < Parent->CellY
|
||||
|| (Candidate->CellY == Parent->CellY
|
||||
&& Candidate->CellX < Parent->CellX);
|
||||
if (DistanceSq < BestDistanceSq
|
||||
|| (DistanceSq == BestDistanceSq && bLowerCell))
|
||||
{
|
||||
BestDistanceSq = DistanceSq;
|
||||
Parent = Candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3882,7 +4019,7 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
|
||||
}
|
||||
|
||||
// Existing probabilistic links remain as texture and loops. They intentionally stay on
|
||||
// the cached inner 3×3 working set; only the structural parent lookup needs the 7×7 halo.
|
||||
// the cached inner 3×3 working set; only the structural tree build needs the 9×9/15×15 halo.
|
||||
if (Params.CrossConnectChance > 0.0f && Shafts.Num() >= 2)
|
||||
{
|
||||
for (int32 i = 0; i < Shafts.Num(); i++)
|
||||
@@ -3906,7 +4043,12 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
|
||||
Params.ConnectorRadius,
|
||||
RoughnessReach + 1.0f)
|
||||
: Params.ConnectorRadius;
|
||||
Conns.Add({ FVector(A.X, A.Y, Zc), FVector(B.X, B.Y, Zc), ConnectorR });
|
||||
const FLocalConn Conn{
|
||||
FVector(A.X, A.Y, Zc), FVector(B.X, B.Y, Zc), ConnectorR };
|
||||
if (ConnectorMayReachCell(Conn))
|
||||
{
|
||||
Conns.Add(Conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3958,7 +4100,10 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
|
||||
const float D2 = FMath::Square(WorldX - Sh.X) + FMath::Square(WorldY - Sh.Y);
|
||||
if (D2 < BestSq) { BestSq = D2; Near = &Sh; }
|
||||
}
|
||||
if (Near && (WorldX - Near->X) + (WorldY - Near->Y) > 0.0f)
|
||||
// The shaft axis is the structural tree's terminal point. Keep the mathematical
|
||||
// half-plane boundary open: tiny cancellation error must not turn an exact axis
|
||||
// landing into a solid ledge cap and sever the parent capsule.
|
||||
if (Near && (WorldX - Near->X) + (WorldY - Near->Y) > 1.0e-3f)
|
||||
{
|
||||
float Shelf = 1.0f - SmoothStep01(BandT / Params.LedgeDepth);
|
||||
Density = FMath::Max(Density, Shelf * Params.BaseDensity);
|
||||
|
||||
@@ -97,7 +97,14 @@ namespace VoxelStrateMeasurePrivate
|
||||
{
|
||||
return Refuse(OutReason, TEXT("SampleStep must be greater than zero."));
|
||||
}
|
||||
if (Settings.RadiusInVoxels <= 0)
|
||||
const bool bHasCoverPointA = Settings.CoverPointA.IsSet();
|
||||
const bool bHasCoverPointB = Settings.CoverPointB.IsSet();
|
||||
if (bHasCoverPointA != bHasCoverPointB)
|
||||
{
|
||||
return Refuse(OutReason, TEXT("CoverPointA and CoverPointB must be set together."));
|
||||
}
|
||||
const bool bUseFittedWindow = bHasCoverPointA && bHasCoverPointB;
|
||||
if (!bUseFittedWindow && Settings.RadiusInVoxels <= 0)
|
||||
{
|
||||
return Refuse(OutReason, TEXT("RadiusInVoxels must be greater than zero."));
|
||||
}
|
||||
@@ -109,10 +116,26 @@ namespace VoxelStrateMeasurePrivate
|
||||
{
|
||||
return Refuse(OutReason, TEXT("HeadroomCells cannot be negative."));
|
||||
}
|
||||
if (!FMath::IsFinite(Settings.CenterXY.X) || !FMath::IsFinite(Settings.CenterXY.Y))
|
||||
if (!bUseFittedWindow
|
||||
&& (!FMath::IsFinite(Settings.CenterXY.X) || !FMath::IsFinite(Settings.CenterXY.Y)))
|
||||
{
|
||||
return Refuse(OutReason, TEXT("CenterXY must contain finite voxel coordinates."));
|
||||
}
|
||||
if (bUseFittedWindow)
|
||||
{
|
||||
const FVector2D& PointA = Settings.CoverPointA.GetValue();
|
||||
const FVector2D& PointB = Settings.CoverPointB.GetValue();
|
||||
if (!FMath::IsFinite(PointA.X) || !FMath::IsFinite(PointA.Y)
|
||||
|| !FMath::IsFinite(PointB.X) || !FMath::IsFinite(PointB.Y))
|
||||
{
|
||||
return Refuse(OutReason, TEXT("Cover points must contain finite voxel coordinates."));
|
||||
}
|
||||
if (!FMath::IsFinite(Settings.CoverMarginVoxels)
|
||||
|| Settings.CoverMarginVoxels < 0.0f)
|
||||
{
|
||||
return Refuse(OutReason, TEXT("CoverMarginVoxels must be finite and non-negative."));
|
||||
}
|
||||
}
|
||||
if (Slot.TopChunkZ <= Slot.BottomChunkZ)
|
||||
{
|
||||
return Refuse(OutReason, TEXT("The requested strate has no positive vertical extent."));
|
||||
@@ -128,13 +151,52 @@ namespace VoxelStrateMeasurePrivate
|
||||
|
||||
// Resolve the margin from the same blended parameter query used by generation. The
|
||||
// representative chunk is deliberately in the middle of this slot and uses the sample
|
||||
// centre's XY chunk, so a future non-Hard transition resolves through the manager exactly
|
||||
// as generation does at that representative location.
|
||||
// window centre's XY chunk, so a future non-Hard transition resolves through the manager
|
||||
// exactly as generation does at that representative location.
|
||||
FVector2D WindowCenter = Settings.CenterXY;
|
||||
double MinX64 = 0.0;
|
||||
double MaxX64 = 0.0;
|
||||
double MinY64 = 0.0;
|
||||
double MaxY64 = 0.0;
|
||||
if (bUseFittedWindow)
|
||||
{
|
||||
const FVector2D& PointA = Settings.CoverPointA.GetValue();
|
||||
const FVector2D& PointB = Settings.CoverPointB.GetValue();
|
||||
const double Margin = static_cast<double>(Settings.CoverMarginVoxels);
|
||||
MinX64 = FMath::Min(static_cast<double>(PointA.X), static_cast<double>(PointB.X)) - Margin;
|
||||
MaxX64 = FMath::Max(static_cast<double>(PointA.X), static_cast<double>(PointB.X)) + Margin;
|
||||
MinY64 = FMath::Min(static_cast<double>(PointA.Y), static_cast<double>(PointB.Y)) - Margin;
|
||||
MaxY64 = FMath::Max(static_cast<double>(PointA.Y), static_cast<double>(PointB.Y)) + Margin;
|
||||
WindowCenter = FVector2D(
|
||||
static_cast<float>(0.5 * (MinX64 + MaxX64)),
|
||||
static_cast<float>(0.5 * (MinY64 + MaxY64)));
|
||||
}
|
||||
else
|
||||
{
|
||||
MinX64 = static_cast<double>(Settings.CenterXY.X)
|
||||
- static_cast<double>(Settings.RadiusInVoxels);
|
||||
MaxX64 = static_cast<double>(Settings.CenterXY.X)
|
||||
+ static_cast<double>(Settings.RadiusInVoxels);
|
||||
MinY64 = static_cast<double>(Settings.CenterXY.Y)
|
||||
- static_cast<double>(Settings.RadiusInVoxels);
|
||||
MaxY64 = static_cast<double>(Settings.CenterXY.Y)
|
||||
+ static_cast<double>(Settings.RadiusInVoxels);
|
||||
}
|
||||
const double XYExtentX = MaxX64 - MinX64;
|
||||
const double XYExtentY = MaxY64 - MinY64;
|
||||
if (!FMath::IsFinite(MinX64) || !FMath::IsFinite(MaxX64)
|
||||
|| !FMath::IsFinite(MinY64) || !FMath::IsFinite(MaxY64)
|
||||
|| !FMath::IsFinite(XYExtentX) || !FMath::IsFinite(XYExtentY)
|
||||
|| XYExtentX < 1.0 || XYExtentY < 1.0)
|
||||
{
|
||||
return Refuse(OutReason, TEXT("The requested XY window is not a finite positive voxel box."));
|
||||
}
|
||||
|
||||
const int32 MidChunkZ = Slot.BottomChunkZ
|
||||
+ (Slot.TopChunkZ - Slot.BottomChunkZ) / 2;
|
||||
const FIntVector RepresentativeChunk(
|
||||
FMath::FloorToInt(Settings.CenterXY.X / static_cast<float>(CHUNK_SIZE)),
|
||||
FMath::FloorToInt(Settings.CenterXY.Y / static_cast<float>(CHUNK_SIZE)),
|
||||
FMath::FloorToInt(WindowCenter.X / static_cast<float>(CHUNK_SIZE)),
|
||||
FMath::FloorToInt(WindowCenter.Y / static_cast<float>(CHUNK_SIZE)),
|
||||
MidChunkZ);
|
||||
const FStrateGenerationParams RepresentativeParams =
|
||||
Manager.GetGenerationParams(RepresentativeChunk);
|
||||
@@ -188,10 +250,18 @@ namespace VoxelStrateMeasurePrivate
|
||||
return Refuse(OutReason, TEXT("The sampled Z window is outside the metrics struct's int32 range."));
|
||||
}
|
||||
|
||||
const int64 XYExtent = static_cast<int64>(Settings.RadiusInVoxels) * 2;
|
||||
const int64 Step = static_cast<int64>(Settings.SampleStep);
|
||||
const int64 NumX64 = CeilDivPositive(XYExtent, Step);
|
||||
const int64 NumY64 = NumX64;
|
||||
const double NumXReal = XYExtentX / static_cast<double>(Step);
|
||||
const double NumYReal = XYExtentY / static_cast<double>(Step);
|
||||
if (!FMath::IsFinite(NumXReal) || !FMath::IsFinite(NumYReal)
|
||||
|| NumXReal <= 0.0 || NumYReal <= 0.0
|
||||
|| NumXReal > static_cast<double>(INT32_MAX)
|
||||
|| NumYReal > static_cast<double>(INT32_MAX))
|
||||
{
|
||||
return Refuse(OutReason, TEXT("A sample-grid dimension exceeds the supported array size."));
|
||||
}
|
||||
const int64 NumX64 = FMath::CeilToInt64(NumXReal);
|
||||
const int64 NumY64 = FMath::CeilToInt64(NumYReal);
|
||||
const int64 NumZ64 = CeilDivPositive(InteriorHeight, Step);
|
||||
const int64 MaxCells = static_cast<int64>(Settings.MaxCells);
|
||||
|
||||
@@ -216,14 +286,6 @@ namespace VoxelStrateMeasurePrivate
|
||||
return Refuse(OutReason, TEXT("The requested sample grid exceeds the bounded cell count."));
|
||||
}
|
||||
|
||||
const double MinX64 = static_cast<double>(Settings.CenterXY.X)
|
||||
- static_cast<double>(Settings.RadiusInVoxels);
|
||||
const double MaxX64 = static_cast<double>(Settings.CenterXY.X)
|
||||
+ static_cast<double>(Settings.RadiusInVoxels);
|
||||
const double MinY64 = static_cast<double>(Settings.CenterXY.Y)
|
||||
- static_cast<double>(Settings.RadiusInVoxels);
|
||||
const double MaxY64 = static_cast<double>(Settings.CenterXY.Y)
|
||||
+ static_cast<double>(Settings.RadiusInVoxels);
|
||||
const float MinX = static_cast<float>(MinX64);
|
||||
const float MaxX = static_cast<float>(MaxX64);
|
||||
const float MinY = static_cast<float>(MinY64);
|
||||
@@ -301,7 +363,8 @@ namespace VoxelStrateMeasurePrivate
|
||||
int64 NumAir,
|
||||
int64& OutLargestComponentCells,
|
||||
int32& OutLargestComponentLowestCell,
|
||||
int32& OutNumComponentsAtLeast1Pct)
|
||||
int32& OutNumComponentsAtLeast1Pct,
|
||||
TArray<int64>* OutComponentCells)
|
||||
{
|
||||
OutComponents.SetNumUninitialized(Grid.CellCount);
|
||||
for (int32 Index = 0; Index < Grid.CellCount; ++Index)
|
||||
@@ -315,6 +378,10 @@ namespace VoxelStrateMeasurePrivate
|
||||
OutLargestComponentCells = 0;
|
||||
OutLargestComponentLowestCell = INDEX_NONE;
|
||||
OutNumComponentsAtLeast1Pct = 0;
|
||||
if (OutComponentCells != nullptr)
|
||||
{
|
||||
OutComponentCells->Reset();
|
||||
}
|
||||
|
||||
for (int32 Start = 0; Start < Grid.CellCount; ++Start)
|
||||
{
|
||||
@@ -359,6 +426,10 @@ namespace VoxelStrateMeasurePrivate
|
||||
}
|
||||
|
||||
const int64 ComponentCells64 = static_cast<int64>(ComponentCells);
|
||||
if (OutComponentCells != nullptr)
|
||||
{
|
||||
OutComponentCells->Add(ComponentCells64);
|
||||
}
|
||||
if (NumAir > 0 && ComponentCells64 * 100 >= NumAir)
|
||||
{
|
||||
++OutNumComponentsAtLeast1Pct;
|
||||
@@ -457,6 +528,7 @@ namespace VoxelStrateMeasurePrivate
|
||||
int64 LargestComponentCells = 0;
|
||||
int32 LargestComponentLowestCell = INDEX_NONE;
|
||||
int32 NumComponentsAtLeast1Pct = 0;
|
||||
TArray<int64> ComponentCells;
|
||||
FloodFillAir(
|
||||
Grid,
|
||||
Components,
|
||||
@@ -464,7 +536,8 @@ namespace VoxelStrateMeasurePrivate
|
||||
NumAir,
|
||||
LargestComponentCells,
|
||||
LargestComponentLowestCell,
|
||||
NumComponentsAtLeast1Pct);
|
||||
NumComponentsAtLeast1Pct,
|
||||
&ComponentCells);
|
||||
|
||||
InOutMetrics.NumSampled = Grid.CellCount;
|
||||
InOutMetrics.NumAir = NumAir;
|
||||
@@ -472,6 +545,7 @@ namespace VoxelStrateMeasurePrivate
|
||||
InOutMetrics.AirFraction = static_cast<float>(
|
||||
static_cast<double>(NumAir) / static_cast<double>(Grid.CellCount));
|
||||
InOutMetrics.NumAirComponents = NumComponents;
|
||||
InOutMetrics.AirComponentCells = MoveTemp(ComponentCells);
|
||||
InOutMetrics.LargestComponentCells = LargestComponentCells;
|
||||
InOutMetrics.NumComponentsAtLeast1Pct = NumComponentsAtLeast1Pct;
|
||||
InOutMetrics.LargestComponentShare = NumAir > 0
|
||||
@@ -1005,7 +1079,8 @@ namespace VoxelStrateMeasurePrivate
|
||||
NumAir,
|
||||
LargestComponentCells,
|
||||
LargestComponentLowestCell,
|
||||
NumComponentsAtLeast1Pct);
|
||||
NumComponentsAtLeast1Pct,
|
||||
nullptr);
|
||||
|
||||
const int32 StartComponent = Components[Start];
|
||||
const int32 GoalComponent = Components[Goal];
|
||||
@@ -1141,6 +1216,13 @@ FVoxelStrateMetrics VF_MeasureStrate(
|
||||
Result.ResolvedMarginVoxels = Grid.ResolvedMarginVoxels;
|
||||
Result.SampledMinZ = Grid.SampledMinZ;
|
||||
Result.SampledMaxZ = Grid.SampledMaxZ;
|
||||
Result.SampledNumX = Grid.NumX;
|
||||
Result.SampledNumY = Grid.NumY;
|
||||
Result.SampledNumZ = Grid.NumZ;
|
||||
Result.SampledMinX = Grid.MinX;
|
||||
Result.SampledMaxX = Grid.MaxX;
|
||||
Result.SampledMinY = Grid.MinY;
|
||||
Result.SampledMaxY = Grid.MaxY;
|
||||
VoxelStrateMeasurePrivate::BuildMetricsFromGrid(Grid, Settings, Result);
|
||||
Result.bValid = Result.NumSampled > 0;
|
||||
if (!Result.bValid)
|
||||
|
||||
@@ -553,6 +553,8 @@ namespace VoxelCaveMorphology
|
||||
*
|
||||
* Propose un point d'atterrissage déterministe complet pour la bouche d'un passage inter-strates.
|
||||
* Cette requête reste pure et le déplacement latéral est strictement borné par MaxLateralSnap.
|
||||
* Pour VerticalShafts, la réponse XY est exactement l'axe d'un puits sélectionné; elle n'est pas
|
||||
* un point ouvert arbitraire à l'intérieur de son disque.
|
||||
*
|
||||
* @return true when the archetype can provide a confident point; false when it cannot.
|
||||
*/
|
||||
@@ -573,7 +575,8 @@ VOXELFORGE_API bool VF_SuggestLandingPoint(
|
||||
* Complete source-query overload for archetypes whose placement parameters are not part of
|
||||
* FStrateGenerationParams/FSlabGenerationParams. The extra structs are read-only inputs only;
|
||||
* the function remains pure and SurfaceWorld still returns false for the reason above. The
|
||||
* returned point is either at the requested XY or within MaxLateralSnap of it.
|
||||
* returned point is either at the requested XY or within MaxLateralSnap of it. VerticalShafts
|
||||
* returns the selected shaft axis so the answer belongs to the connected shaft tree.
|
||||
*/
|
||||
VOXELFORGE_API bool VF_SuggestLandingPoint(
|
||||
ECaveGeneratorType Archetype,
|
||||
|
||||
@@ -24,6 +24,12 @@ struct VOXELFORGE_API FVoxelStrateMeasureSettings
|
||||
int32 SampleStep = 4; // Voxels between samples.
|
||||
int32 RadiusInVoxels = 256; // Half-extent in X and Y around CenterXY.
|
||||
FVector2D CenterXY = FVector2D::ZeroVector; // Actor-space voxel coordinates.
|
||||
// When both points are set, replace the square centered on CenterXY with the XY AABB of the
|
||||
// two points expanded by CoverMarginVoxels. This keeps a mouth-to-mouth measurement sized by
|
||||
// the mouths' separation instead of by their distance from the world origin.
|
||||
TOptional<FVector2D> CoverPointA;
|
||||
TOptional<FVector2D> CoverPointB;
|
||||
float CoverMarginVoxels = 48.0f;
|
||||
int32 MaxCells = 8000000; // Refuse a grid larger than this many cells.
|
||||
int32 MaxRouteRetries = 16; // Alternate coarse routes to full-resolution-check after the first route.
|
||||
int32 HeadroomCells = 2; // Air cells above a floor cell required for walkable.
|
||||
@@ -54,6 +60,21 @@ struct VOXELFORGE_API FVoxelStrateMetrics
|
||||
int32 ResolvedMarginVoxels = 0;
|
||||
int32 SampledMinZ = 0;
|
||||
int32 SampledMaxZ = 0;
|
||||
|
||||
// The exact sampled XY box and dimensions. For a fitted window these are the AABB-derived
|
||||
// dimensions; for the legacy window they describe CenterXY +/- RadiusInVoxels.
|
||||
int32 SampledNumX = 0;
|
||||
int32 SampledNumY = 0;
|
||||
int32 SampledNumZ = 0;
|
||||
float SampledMinX = 0.0f;
|
||||
float SampledMaxX = 0.0f;
|
||||
float SampledMinY = 0.0f;
|
||||
float SampledMaxY = 0.0f;
|
||||
|
||||
// Air-component sizes in deterministic flood-fill discovery order. Keeping these facts from
|
||||
// the one required flood fill lets diagnostics classify small non-largest components without
|
||||
// sampling the source a second time.
|
||||
TArray<int64> AirComponentCells;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user