feat(shafts): a drainage tree -- the field goes from 22% to 97% connected
The plateau was the tell. Roughness changed the component COUNT (it
manufactures noise pockets) but the giant component sat at ~0.22 in every
variant, and widening connectors past the proven roughness bound barely
moved it. That is a random graph below its percolation threshold: 0.6
sites, 0.35 bonds, and a 1.6-cell pair cutoff that drops many neighbour
pairs once jitter spreads them 1.7 (orthogonal) to 2.4 (diagonal) cells
apart.
So the lever was topology. Every shaft now emits a deterministic connector
to the nearest shaft STRICTLY closer to the origin, so chains drain inward
and terminate at the spine. The probabilistic connectors stay as texture
and loops -- the tree guarantees you can get out, the noise keeps it from
reading as a diagram.
Deliberately NOT tuned: raising CrossConnectChance/ShaftDensity/the cutoff
toward the percolation threshold buys a world connected at one seed and
shattered at the next, which is exactly the 1-in-16 behaviour we had. A
phase transition is not a knob.
seed 1337 components 30 -> 17, largest share .220 -> .975
16 seeds spine is the largest component in 16/16 (was 8/16)
largest share .58 - .9999
Seam safety, because a parent computed from a 3x3 window would differ
between chunks and the connector would exist in one and not the other:
collect over 7x7, emit only for the inner 3x3, whose 5x5 candidate windows
are then fully contained. 154 seam probes per path, 0 mismatches.
Box verdicts re-checked against the new air: 27 proved, 33 mixed,
0 violations. Stack mirror bit-identical over 20,000 samples.
Perf: 1.634 us forced rebuild, 0.100 us hot call -- the 49 rolls happen
only on cache rebuild, never per voxel.
⚠️ ACCEPTANCE STILL FAILS: arrival->departure 4/16 (was 1/16). Reported,
not tuned. With 97% of the air in one component, two mouths that both land
IN the field would essentially always connect -- so the remaining failure
is placement, not topology. Next.
Build clean, 20 succeeded / 0 warnings / 0 failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bp7FxoQcSoiuhxjuLr6aBM
This commit is contained in:
1 parent
58e61cdefa
commit
7a20eeed0d
6 files changed
+608
-91
No files matched your search
@@ -1444,13 +1444,14 @@ namespace
|
||||
//=========================================================================
|
||||
// RÔLE 1 — SOURCE : PUITS VERTICAUX / VERTICAL SHAFTS
|
||||
//=========================================================================
|
||||
// Cylindres infinis sur une grille XY jitterée + connecteurs horizontaux entre paires proches,
|
||||
// ouverts par un hash de paire symétrique. Écrit le canal SDF uniquement.
|
||||
// Cylindres infinis sur une grille XY jitterée + un arbre de drainage déterministe, complété
|
||||
// par des connecteurs horizontaux entre paires proches ouverts par un hash de paire. Écrit le
|
||||
// canal SDF uniquement.
|
||||
//
|
||||
// ⚠️ ÉCART ASSUMÉ AVEC `OPSTACK-DECOMPOSITION §6`, qui suggérait DEUX sources (colonnes XY-pures
|
||||
// + connecteurs) pour que la moitié cylindrique reçoive le traitement du cache de colonne et un
|
||||
// `ClassifyBox` exact en XY. Gardé en UN opérateur, et voici pourquoi :
|
||||
// • les connecteurs se dérivent de la MÊME liste 3×3 que les puits (il faut les paires), donc
|
||||
// • les connecteurs se dérivent de la MÊME liste inner 3×3 que les puits (il faut les paires), donc
|
||||
// séparer imposerait soit de rouler les cellules deux fois, soit un cache partagé entre
|
||||
// deux ops — c'est-à-dire la complexité qu'on voulait éviter ;
|
||||
// • le `FShaftLedgeMod` en aval a de toute façon besoin de la liste des puits, donc il faut
|
||||
@@ -1459,9 +1460,10 @@ namespace
|
||||
// gardé : un `EffectOverBox` conservatif qui teste cercles ET capsules, ce que la version
|
||||
// séparée aurait dû faire aussi. À revoir si le profil montre que ça compte.
|
||||
//
|
||||
// Kept as ONE op against §6's suggestion: the connectors derive from the same 3×3 roll as the
|
||||
// shafts, and the downstream ledge mod needs the shaft list anyway. What is forfeited is an
|
||||
// exact XY box verdict on the cylinder half alone.
|
||||
// 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;
|
||||
// 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
|
||||
{
|
||||
public:
|
||||
@@ -1473,7 +1475,7 @@ namespace
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
struct FShaft { float X, Y, R; bool bOriginSpine; };
|
||||
struct FShaft { float X, Y, R; int32 CellX, CellY; bool bOriginSpine; };
|
||||
struct FConn { FVector A, B; float Radius; };
|
||||
|
||||
// ⚠️ DÉCLARÉE ICI, avant toute fonction qui la renvoie. Un type imbriqué doit exister au
|
||||
@@ -1485,7 +1487,7 @@ namespace
|
||||
struct FCells
|
||||
{
|
||||
TArray<FShaft, TInlineAllocator<10>> Shafts;
|
||||
TArray<FConn, TInlineAllocator<16>> Conns;
|
||||
TArray<FConn, TInlineAllocator<24>> Conns;
|
||||
};
|
||||
|
||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
@@ -1545,6 +1547,105 @@ namespace
|
||||
if (QX * QX + QY * QY < R * R) { return EVoxelOpEffect::CarveOnly; }
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------
|
||||
// THE STRUCTURAL TREE — enumerate every inner cell that the box can query, then
|
||||
// collect its complete 5×5 parent window. This is the EffectOverBox mirror of GetCells:
|
||||
// it may over-enumerate, but it must never miss a cached tree capsule.
|
||||
//-----------------------------------------------------------------
|
||||
constexpr int32 CandidateRadius = 2;
|
||||
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;
|
||||
|
||||
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)
|
||||
{
|
||||
FShaft Sh;
|
||||
if (!RollShaft(cx, cy, Sh)) { continue; }
|
||||
TreeCandidates.Add(Sh);
|
||||
if (cx >= EmitX0 && cx <= EmitX1 && cy >= EmitY0 && cy <= EmitY1)
|
||||
{
|
||||
TreeEmit.Add(Sh);
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
const float HalfDiagXY = 0.5f * FMath::Sqrt(
|
||||
FMath::Square((float)(VoxelBox.Max.X - VoxelBox.Min.X)) +
|
||||
FMath::Square((float)(VoxelBox.Max.Y - VoxelBox.Min.Y)));
|
||||
auto ConnectorMayReachBox = [&](const FConn& Conn)
|
||||
{
|
||||
const float ConnReach = Conn.Radius + ExtraReach;
|
||||
const float Zc = Conn.A.Z;
|
||||
if (RMinZ > Zc + ConnReach || RMaxZ < Zc - ConnReach) { return false; }
|
||||
const float DistXY = VF_DistPointSegment2D(
|
||||
CtrXY, FVector2D(Conn.A.X, Conn.A.Y), FVector2D(Conn.B.X, Conn.B.Y));
|
||||
return DistXY - HalfDiagXY < ConnReach;
|
||||
};
|
||||
|
||||
for (const FShaft& Child : TreeEmit)
|
||||
{
|
||||
const float ChildOriginSq = FMath::Square(Child.X) + FMath::Square(Child.Y);
|
||||
const FShaft* Parent = nullptr;
|
||||
float BestDistanceSq = FLT_MAX;
|
||||
for (const FShaft& Candidate : TreeCandidates)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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(
|
||||
P.StrateBottomWorldZ + P.BoundarySealThickness,
|
||||
P.StrateTopWorldZ - P.BoundarySealThickness,
|
||||
VoxelHash::ToFloat01(VoxelHash::Mix(LinkHash)));
|
||||
const FVector ParentPoint = Parent != nullptr
|
||||
? FVector(Parent->X, Parent->Y, Zc)
|
||||
: FVector(0.0f, 0.0f, Zc);
|
||||
const FConn TreeConn{
|
||||
FVector(Child.X, Child.Y, Zc), ParentPoint, TreeRadius };
|
||||
if (ConnectorMayReachBox(TreeConn)) { return EVoxelOpEffect::CarveOnly; }
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------
|
||||
// LES CONNECTEURS — LES VRAIES CAPSULES, PLUS « un puits existe dans le coin »
|
||||
//-----------------------------------------------------------------
|
||||
@@ -1558,11 +1659,11 @@ namespace
|
||||
// construit, et tester la capsule réelle.
|
||||
//
|
||||
// ⚠️ POURQUOI L'ÉNUMÉRATION EST UN SUR-ENSEMBLE (donc sûre). `Eval` lit les connecteurs
|
||||
// du voisinage 3×3 de la cellule DE SA REQUÊTE. Une paire visible depuis un point de la
|
||||
// boîte a donc ses deux puits dans un même 3×3 centré sur une cellule que la boîte
|
||||
// touche ⇒ les deux sont dans [cellules de la boîte] ± 1, qui est exactement la plage
|
||||
// balayée ici. On peut produire des paires que personne ne voit jamais : c'est du
|
||||
// `CarveOnly` en trop, pas un trou.
|
||||
// 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
|
||||
// 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
|
||||
// pris sur (A puis B) dans l'ordre d'insertion, et `GetCells` insère en `(dy, dx)`,
|
||||
@@ -1573,15 +1674,15 @@ namespace
|
||||
//
|
||||
// Was: return CarveOnly as soon as any shaft EXISTED within Spacing*1.6 + Pad, which at
|
||||
// ShaftSpacing 55 / ShaftDensity 0.6 is true everywhere -- 0 of 60 tiles proved. Now it
|
||||
// rebuilds the connectors the way GetCells does and tests the real capsule. The pair
|
||||
// rebuilds the random links the way GetCells does and tests the real capsule. The pair
|
||||
// enumeration is a superset (safe), and the row-major cell order reproduces GetCells'
|
||||
// insertion order, so each pair gets the same hash without assuming Pair() is symmetric.
|
||||
if (P.CrossConnectChance > 0.0f)
|
||||
{
|
||||
const int32 QX0 = FMath::FloorToInt((float)VoxelBox.Min.X / Spacing) - 1;
|
||||
const int32 QX1 = FMath::FloorToInt((float)VoxelBox.Max.X / Spacing) + 1;
|
||||
const int32 QY0 = FMath::FloorToInt((float)VoxelBox.Min.Y / Spacing) - 1;
|
||||
const int32 QY1 = FMath::FloorToInt((float)VoxelBox.Max.Y / Spacing) + 1;
|
||||
const int32 QX0 = EmitX0;
|
||||
const int32 QX1 = EmitX1;
|
||||
const int32 QY0 = EmitY0;
|
||||
const int32 QY1 = EmitY1;
|
||||
|
||||
TArray<FShaft, TInlineAllocator<32>> Near;
|
||||
for (int32 cy = QY0; cy <= QY1; ++cy)
|
||||
@@ -1598,21 +1699,11 @@ namespace
|
||||
&& QX0 <= 1 && QX1 >= -1
|
||||
&& QY0 <= 1 && QY1 >= -1)
|
||||
{
|
||||
Near.Add({0.0f, 0.0f, SpineRadius, true});
|
||||
Near.Add({0.0f, 0.0f, SpineRadius, 0, 0, true});
|
||||
}
|
||||
|
||||
const float BottomZ = P.StrateBottomWorldZ + P.BoundarySealThickness;
|
||||
const float TopZ = P.StrateTopWorldZ - P.BoundarySealThickness;
|
||||
|
||||
// Z est traité EXACTEMENT (le connecteur est une capsule horizontale à `Zc`), XY de
|
||||
// façon conservative. Séparer les deux est bien plus serré qu'une demi-diagonale 3D.
|
||||
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));
|
||||
const float HalfDiagXY = 0.5f * FMath::Sqrt(
|
||||
FMath::Square((float)(VoxelBox.Max.X - VoxelBox.Min.X)) +
|
||||
FMath::Square((float)(VoxelBox.Max.Y - VoxelBox.Min.Y)));
|
||||
|
||||
for (int32 i = 0; i < Near.Num(); ++i)
|
||||
for (int32 j = i + 1; j < Near.Num(); ++j)
|
||||
{
|
||||
@@ -1632,17 +1723,11 @@ namespace
|
||||
|
||||
const float ConnRadius = bSpineConnector
|
||||
? SpineConnectorRadius() : P.ConnectorRadius;
|
||||
const float ConnReach = ConnRadius + ExtraReach;
|
||||
|
||||
const float Zc = FMath::Lerp(BottomZ, TopZ,
|
||||
VoxelHash::ToFloat01(VoxelHash::Mix(PH)));
|
||||
if (RMinZ > Zc + ConnReach || RMaxZ < Zc - ConnReach) { continue; }
|
||||
|
||||
const float DistXY = VF_DistPointSegment2D(
|
||||
CtrXY, FVector2D(A.X, A.Y), FVector2D(B.X, B.Y));
|
||||
if (DistXY - HalfDiagXY >= ConnReach) { continue; }
|
||||
|
||||
return EVoxelOpEffect::CarveOnly;
|
||||
const FConn RandomConn{
|
||||
FVector(A.X, A.Y, Zc), FVector(B.X, B.Y, Zc), ConnRadius };
|
||||
if (ConnectorMayReachBox(RandomConn)) { return EVoxelOpEffect::CarveOnly; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1664,6 +1749,8 @@ namespace
|
||||
Out.Y = (ny + 0.15f + JY * 0.7f) * Spacing;
|
||||
Out.R = FMath::Lerp(P.ShaftMinRadius, P.ShaftMaxRadius,
|
||||
VoxelHash::ToFloat01(VoxelHash::Mix(Hh ^ 0xBEEFu)));
|
||||
Out.CellX = nx;
|
||||
Out.CellY = ny;
|
||||
Out.bOriginSpine = false;
|
||||
return true;
|
||||
}
|
||||
@@ -1678,7 +1765,8 @@ namespace
|
||||
return FMath::Max(P.ConnectorRadius, RoughnessReach + 1.0f);
|
||||
}
|
||||
|
||||
/** Le voisinage 3×3 + ses connecteurs, mémoïsés par worker. Clé = cellule + tous les params
|
||||
/** 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
|
||||
* 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
|
||||
@@ -1711,23 +1799,101 @@ namespace
|
||||
Cache.Shafts.Reset();
|
||||
Cache.Conns.Reset();
|
||||
|
||||
for (int32 dy = -1; dy <= 1; dy++)
|
||||
for (int32 dx = -1; dx <= 1; dx++)
|
||||
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 CandidateRadius = 2; // 5×5 parent window around an emitting shaft
|
||||
constexpr uint32 TreeSalt = 0x7A11u;
|
||||
|
||||
TArray<FShaft, TInlineAllocator<49>> CollectedShafts;
|
||||
// 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.
|
||||
for (int32 dy = -CollectRadius; dy <= CollectRadius; dy++)
|
||||
for (int32 dx = -CollectRadius; dx <= CollectRadius; dx++)
|
||||
{
|
||||
FShaft Sh;
|
||||
if (RollShaft(CX + dx, CY + dy, Sh)) { Cache.Shafts.Add(Sh); }
|
||||
if (!RollShaft(CX + dx, CY + dy, Sh)) { continue; }
|
||||
CollectedShafts.Add(Sh);
|
||||
if (FMath::Abs(dx) <= EmitRadius && FMath::Abs(dy) <= EmitRadius)
|
||||
{
|
||||
Cache.Shafts.Add(Sh);
|
||||
}
|
||||
}
|
||||
|
||||
const bool bOriginNearby = FMath::Abs(CX) <= 1 && FMath::Abs(CY) <= 1;
|
||||
if (bOriginNearby && SpineRadius > 0.0f)
|
||||
{
|
||||
Cache.Shafts.Add({0.0f, 0.0f, SpineRadius, true});
|
||||
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();
|
||||
|
||||
auto EmitTreeConnector = [&](const FShaft& Child, const FShaft* Parent)
|
||||
{
|
||||
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(
|
||||
BottomZ, TopZ, VoxelHash::ToFloat01(VoxelHash::Mix(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 });
|
||||
};
|
||||
|
||||
// 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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
EmitTreeConnector(Child, Parent);
|
||||
}
|
||||
|
||||
// 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.
|
||||
if (P.CrossConnectChance > 0.0f && Cache.Shafts.Num() >= 2)
|
||||
{
|
||||
const float BottomZ = P.StrateBottomWorldZ + P.BoundarySealThickness;
|
||||
const float TopZ = P.StrateTopWorldZ - P.BoundarySealThickness;
|
||||
for (int32 i = 0; i < Cache.Shafts.Num(); i++)
|
||||
for (int32 j = i + 1; j < Cache.Shafts.Num(); j++)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user