feat(shafts): the spine joins its neighbours -- necessary, not sufficient

Jahni chose: make the (0,0) column join the shafts it passes, so
VerticalShafts behaves like the other seven archetypes. Built, and it
works as far as it goes -- the spine is now the LARGEST air component in
13 of 16 seeds (share up to 0.81, was 0.096 and never largest).

⚠️ IT DOES NOT MEET THE ACCEPTANCE CRITERION. arrival->departure is still
1/16. Reported as a failure rather than dressed up, because the criterion
was the measured law, not "the code was written". The test now prints
ACCEPTANCE arrival->departure: FAIL (1/16 seeds pass) so it cannot be
quietly forgotten.

Why it is insufficient, and it is structural: the spine only joins its own
3x3 cell neighbourhood, while passage mouths land 60-200 voxels from
(0,0). The departure network is unreachable from the spine in all 16
seeds. Joining the hub to its neighbours does not help if the rest of the
graph never reaches the hub.

Part A diagnosis (seed 1337, margin 0, step 2, radius 192):
  baseline              30 components, largest .220
  roughness 0           17 components, largest .226
  connector radius 8    29 components, largest .229

H1 is real but partial: roughness fragments the space (13 of the 30
components are noise pockets), yet widening connectors past the proven
roughness bound barely moves it. So roughness is NOT what severs the
network -- the largest component sits at ~0.22 in every variant, which is
the signature of a graph below its percolation threshold. The lever is
topology, not radius. H2 (the hardcoded 1.6 pair cutoff) went untested.

Implementation notes: the spine is added ONLY as a connector endpoint, not
as a carving shaft -- VF_ApplyOriginSpine remains sole owner -- and is
excluded from ledge placement. Spine connectors use radius 6.625, above
the proven roughness supremum of 5.625 (sup|FBM| = 1.5, not 1.0).
SpineRadius is threaded through BuildVerticalShaftStack, not hardcoded.

Op-stack mirror bit-identical over 20,000 samples, 0 box-verdict
violations. LayoutOrderIndependence green.

Build clean, 19 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:
Fr0zkaandClaude Opus 5 committed 2026-08-30 16:52:57 +02:00
1 parent 5519b323c8
commit 58e61cdefa
5 files changed
+147 -40

No files matched your search

@@ -1714,7 +1714,7 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
SweepSeconds);
Summary += TEXT(
"Part A — margin artifact check (SampleStep 2, RadiusInVoxels 192; explicit margins):\n");
"Resolution control — margin artifact check (SampleStep 2, RadiusInVoxels 192; explicit margins):\n");
Summary += TEXT(
" margin | verdict | components | largest share | sampled cells | Z window | endpoint snaps\n");
for (const FRefinementSweepRow& Row : MarginRows)
@@ -1737,7 +1737,7 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
*SnapText);
}
Summary += FString::Printf(
TEXT("Part A verdict: %s. Margin sweep wall-clock: %.3f seconds.\n"),
TEXT("Resolution-control verdict: %s. Margin sweep wall-clock: %.3f seconds.\n"),
bMarginArtifact
? TEXT("ARTIFACT — at least one margin-0/2/4 query is CONNECTED; the default margin excluded the connection")
: TEXT("FINDING SURVIVES — no margin-0/2/4 query is CONNECTED; the full window is exonerated"),
@@ -1828,10 +1828,10 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
NumMouthToSpineQueries,
NumMouthToSpineCoarseLied);
Summary += TEXT(
"Interpretation: the spine is relevant and usually a backbone, but it is not a universal "
"connector. The VerticalShafts result supports a placement-first choice for this defect "
"because most fixture mouths reach the spine; it does not support treating the spine as "
"a guarantee for every archetype.\n");
"Interpretation: the structural spine remains a continuous column. After the "
"VerticalShafts connector change, the seed-1337 spine is the largest component, but the "
"distant shaft networks at the measured passage mouths are not thereby guaranteed to join; "
"the 16-seed arrival->departure sweep below is the acceptance measurement.\n");
Summary += TEXT(
"Ordering-dependency hazard: making passage i's departure mouth depend on passage i-1's "
"arrival mouth would make layout generation a sequence. That conflicts with "
@@ -1839,7 +1839,7 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
"coupled implementation would be expected to break that test. No such dependency was "
"implemented here.\n");
if (!bMarginArtifact && bMarginSweepUsable)
if (bMarginSweepUsable)
{
const FVoxelConnectivityDiagnostics Diagnostics = VF_DiagnoseConnectivity(
*World.Generator,
@@ -1868,7 +1868,7 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
const float CoarseStraightLineCells = MouthStraightLine
/ static_cast<float>(DiagnosticSettings.SampleStep);
Summary += TEXT("Part B — VerticalShafts diagnosis (seed 1337; margin 0; step 2; radius 192):\n");
Summary += TEXT("Part C — VerticalShafts post-fix measurement (seed 1337; margin 0; step 2; radius 192):\n");
Summary += FString::Printf(
TEXT(" arrival LowerPoint: (%.3f, %.3f, %.3f); departure UpperPoint: "
"(%.3f, %.3f, %.3f); XY separation %.3f voxels; Z delta %.3f voxels.\n"),
@@ -1904,6 +1904,20 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
Diagnostics.bStartSnapped ? TEXT("yes") : TEXT("no"),
Diagnostics.bGoalSnapped ? TEXT("yes") : TEXT("no"));
if (MarginRows.Num() > 0
&& SpineReports.IsValidIndex(VerticalShaftsIndex))
{
const FVoxelStrateMetrics& VerticalMetrics = MarginRows[0].Metrics;
const FSpineDiagnosisReport& VerticalSpine = SpineReports[VerticalShaftsIndex];
Summary += FString::Printf(
TEXT(" Part C VerticalShafts topology: components=%d; largest share=%.9g; "
"spine component share=%.9g; spine-is-largest=%s.\n"),
VerticalMetrics.NumAirComponents,
VerticalMetrics.LargestComponentShare,
VerticalSpine.SpineComponent.GoalComponentShare,
VerticalSpine.SpineComponent.bGoalComponentIsLargest ? TEXT("YES") : TEXT("NO"));
}
static constexpr int32 SeedCases[] = {
1337, 1, 2, 3, 4, 5, 6, 7,
8, 9, 10, 11, 12, 13, 14, 15,
@@ -2038,6 +2052,12 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
SeedFailures,
SeedArrivalToDepartureSnapEvents,
SeedSweepSeconds);
Summary += FString::Printf(
TEXT(" ACCEPTANCE arrival->departure: %s (%d/%d seeds pass).\n"),
SeedPasses == UE_ARRAY_COUNT(SeedCases) && SeedFailures == 0
? TEXT("PASS") : TEXT("FAIL"),
SeedPasses,
UE_ARRAY_COUNT(SeedCases));
Summary += FString::Printf(
TEXT(" spine column sweep: full-window run=%d/%d, one-open-run=%d/%d, "
"component-largest=%d/%d, no open component=%d; component share range "
@@ -2110,11 +2130,9 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
}
else
{
Summary += FString::Printf(
TEXT("Part B diagnosis and multi-seed sweep: SKIPPED because Part A %s.\n"),
bMarginArtifact
? TEXT("found the connection in the previously excluded band")
: TEXT("did not produce a usable four-row measurement/query set"));
Summary += TEXT(
"Part C post-fix measurement and multi-seed sweep: SKIPPED because the margin "
"measurement set was not usable.\n");
}
AddInfo(Summary);
@@ -1465,15 +1465,16 @@ namespace
class FShaftFieldSource final : public IVoxelDensityOp
{
public:
FShaftFieldSource(const FVerticalShaftParams& InP, int32 Seed, float InExtraReach)
FShaftFieldSource(const FVerticalShaftParams& InP, int32 Seed, float InExtraReach,
float InSpineRadius)
: P(InP), Salt((uint32)Seed ^ 0x53686674u) // 'Shft' — identique à GetVerticalShaftDensity
, ExtraReach(InExtraReach) {}
, ExtraReach(InExtraReach), SpineRadius(InSpineRadius) {}
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
void PrepareChunk(const FVoxelOpContext&) override {}
struct FShaft { float X, Y, R; };
struct FConn { FVector A, B; };
struct FShaft { float X, Y, R; 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
// moment où le COMPILATEUR lit la SIGNATURE — les corps de méthodes sont différés, pas les
@@ -1483,8 +1484,8 @@ namespace
// compiler reads the SIGNATURE — bodies are deferred, return types are not.
struct FCells
{
TArray<FShaft, TInlineAllocator<9>> Shafts;
TArray<FConn, TInlineAllocator<8>> Conns;
TArray<FShaft, TInlineAllocator<10>> Shafts;
TArray<FConn, TInlineAllocator<16>> Conns;
};
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
@@ -1494,6 +1495,7 @@ namespace
float CaveSDF = FLT_MAX;
for (const FShaft& Sh : C.Shafts)
{
if (Sh.bOriginSpine) continue; // VF_ApplyOriginSpine owns the structural column.
const float DX = WorldX - Sh.X;
const float DY = WorldY - Sh.Y;
CaveSDF = FMath::Min(CaveSDF, FMath::Sqrt(DX * DX + DY * DY) - Sh.R);
@@ -1501,7 +1503,7 @@ namespace
const FVector Pos(WorldX, WorldY, WorldZ);
for (const FConn& Cn : C.Conns)
{
CaveSDF = FMath::Min(CaveSDF, VoxelSDF::Capsule(Pos, Cn.A, Cn.B, P.ConnectorRadius));
CaveSDF = FMath::Min(CaveSDF, VoxelSDF::Capsule(Pos, Cn.A, Cn.B, Cn.Radius));
}
InOut.Sdf = CaveSDF;
}
@@ -1518,7 +1520,8 @@ namespace
// L'enveloppe doit couvrir les deux bornes de `Lerp(ShaftMinRadius, ShaftMaxRadius, t)`,
// pas `ShaftMaxRadius` seul si l'asset inverse les paramètres. The bound must cover
// both radius endpoints before adding connector and downstream reach.
const float Pad = FMath::Max3(P.ShaftMinRadius, P.ShaftMaxRadius, P.ConnectorRadius) + ExtraReach;
const float Pad = FMath::Max3(P.ShaftMinRadius, P.ShaftMaxRadius,
SpineConnectorRadius()) + ExtraReach;
const FBox Padded = VoxelBox.ExpandBy(Pad);
const float Spacing = FMath::Max(P.ShaftSpacing, 1.0f);
@@ -1588,7 +1591,16 @@ namespace
if (RollShaft(cx, cy, Sh)) { Near.Add(Sh); }
}
const float ConnReach = P.ConnectorRadius + ExtraReach;
// Match GetCells' rule conservatively: any query cell in the central 3x3 may see
// a spine connector. This is a superset for a box, so it can only make the tile
// less likely to be classified AllSolid.
if (SpineRadius > 0.0f
&& QX0 <= 1 && QX1 >= -1
&& QY0 <= 1 && QY1 >= -1)
{
Near.Add({0.0f, 0.0f, SpineRadius, true});
}
const float BottomZ = P.StrateBottomWorldZ + P.BoundarySealThickness;
const float TopZ = P.StrateTopWorldZ - P.BoundarySealThickness;
@@ -1612,7 +1624,15 @@ namespace
const uint32 PH = VoxelHash::Pair(
FMath::RoundToInt(A.X), FMath::RoundToInt(A.Y),
FMath::RoundToInt(B.X), FMath::RoundToInt(B.Y), Salt ^ 0xC04Eu);
if (VoxelHash::ToFloat01(PH) >= P.CrossConnectChance) { continue; }
const bool bSpineConnector = A.bOriginSpine || B.bOriginSpine;
if (VoxelHash::ToFloat01(PH) >= P.CrossConnectChance)
{
continue;
}
const float ConnRadius = bSpineConnector
? SpineConnectorRadius() : P.ConnectorRadius;
const float ConnReach = ConnRadius + ExtraReach;
const float Zc = FMath::Lerp(BottomZ, TopZ,
VoxelHash::ToFloat01(VoxelHash::Mix(PH)));
@@ -1644,9 +1664,20 @@ 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.bOriginSpine = false;
return true;
}
/** Spine links use a radius above the repository's proven sup|FBM|=1.5 roughness
* envelope, so the roughness pass cannot pinch their centreline shut. */
float SpineConnectorRadius() const
{
constexpr float RoughnessAbsBound = 1.5f;
const float RoughnessReach = FMath::Max(P.SurfaceRoughness, 0.0f)
* VOXEL_NOISE_SCALE * RoughnessAbsBound;
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
* 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. */
@@ -1660,18 +1691,21 @@ namespace
thread_local int32 VS_CX = INT32_MAX, VS_CY = INT32_MAX;
thread_local uint32 VS_Salt = 0xFFFFFFFFu;
thread_local float VS_Spacing = -1.0f, VS_Dens = -1.0f, VS_MinR = -1.0f,
VS_MaxR = -1.0f, VS_Cross = -1.0f,
VS_MaxR = -1.0f, VS_Cross = -1.0f, VS_ConnR = -1.0f,
VS_Rough = -1.0f, VS_SpineR = -1.0f,
VS_BotZ = FLT_MAX, VS_TopZ = FLT_MAX, VS_Seal = -1.0f;
if (CX != VS_CX || CY != VS_CY || Salt != VS_Salt || Spacing != VS_Spacing ||
P.ShaftDensity != VS_Dens || P.ShaftMinRadius != VS_MinR || P.ShaftMaxRadius != VS_MaxR ||
P.CrossConnectChance != VS_Cross ||
P.CrossConnectChance != VS_Cross || P.ConnectorRadius != VS_ConnR ||
P.SurfaceRoughness != VS_Rough || SpineRadius != VS_SpineR ||
P.StrateBottomWorldZ != VS_BotZ || P.StrateTopWorldZ != VS_TopZ ||
P.BoundarySealThickness != VS_Seal)
{
VS_CX = CX; VS_CY = CY; VS_Salt = Salt; VS_Spacing = Spacing;
VS_Dens = P.ShaftDensity; VS_MinR = P.ShaftMinRadius; VS_MaxR = P.ShaftMaxRadius;
VS_Cross = P.CrossConnectChance;
VS_Cross = P.CrossConnectChance; VS_ConnR = P.ConnectorRadius;
VS_Rough = P.SurfaceRoughness; VS_SpineR = SpineRadius;
VS_BotZ = P.StrateBottomWorldZ; VS_TopZ = P.StrateTopWorldZ;
VS_Seal = P.BoundarySealThickness;
Cache.Shafts.Reset();
@@ -1684,6 +1718,12 @@ namespace
if (RollShaft(CX + dx, CY + dy, Sh)) { 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});
}
if (P.CrossConnectChance > 0.0f && Cache.Shafts.Num() >= 2)
{
const float BottomZ = P.StrateBottomWorldZ + P.BoundarySealThickness;
@@ -1699,10 +1739,16 @@ namespace
const uint32 PH = VoxelHash::Pair(
FMath::RoundToInt(A.X), FMath::RoundToInt(A.Y),
FMath::RoundToInt(B.X), FMath::RoundToInt(B.Y), Salt ^ 0xC04Eu);
if (VoxelHash::ToFloat01(PH) >= P.CrossConnectChance) { continue; }
const bool bSpineConnector = A.bOriginSpine || B.bOriginSpine;
if (VoxelHash::ToFloat01(PH) >= P.CrossConnectChance)
{
continue;
}
const float Zc = FMath::Lerp(BottomZ, TopZ, VoxelHash::ToFloat01(VoxelHash::Mix(PH)));
Cache.Conns.Add({ FVector(A.X, A.Y, Zc), FVector(B.X, B.Y, Zc) });
const float ConnectorR = bSpineConnector
? SpineConnectorRadius() : P.ConnectorRadius;
Cache.Conns.Add({ FVector(A.X, A.Y, Zc), FVector(B.X, B.Y, Zc), ConnectorR });
}
}
}
@@ -1712,6 +1758,7 @@ namespace
FVerticalShaftParams P;
uint32 Salt;
float ExtraReach;
float SpineRadius;
};
//=========================================================================
@@ -1748,6 +1795,7 @@ namespace
float BestSq = FLT_MAX;
for (const FShaftFieldSource::FShaft& Sh : C.Shafts)
{
if (Sh.bOriginSpine) continue; // ledges belong to real shafts, not the post.
const float D2 = FMath::Square(WorldX - Sh.X) + FMath::Square(WorldY - Sh.Y);
if (D2 < BestSq) { BestSq = D2; Near = &Sh; }
}
@@ -4198,7 +4246,8 @@ namespace VoxelDensityOps
const float ExtraReach = FMath::Abs(P.SurfaceRoughness) * VOXEL_NOISE_SCALE
* VF_PerlinAbsBound + CarveBlend + 1.0f;
TUniquePtr<FShaftFieldSource> ShaftSource = MakeUnique<FShaftFieldSource>(P, Seed, ExtraReach);
TUniquePtr<FShaftFieldSource> ShaftSource =
MakeUnique<FShaftFieldSource>(P, Seed, ExtraReach, SpineRadius);
const FShaftFieldSource* ShaftPtr = ShaftSource.Get();
OutStack.Add(MakeConstantRockSource(P.BaseDensity));
+31 -9
View File
@@ -3701,23 +3701,26 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
// Shafts + cross-connectors of the 3×3 neighbourhood are pure functions of (cell, seed,
// params) yet were re-hashed PER VOXEL (9 cell rolls + a pair hash per shaft pair). Bake
// them once per centre cell (thread_local); per-voxel work = the cylinder/capsule SDFs.
struct FLocalShaft { float X, Y, R; };
struct FLocalConn { FVector A, B; };
thread_local TArray<FLocalShaft, TInlineAllocator<9>> Shafts;
thread_local TArray<FLocalConn, TInlineAllocator<8>> Conns;
struct FLocalShaft { float X, Y, R; bool bOriginSpine; };
struct FLocalConn { FVector A, B; float Radius; };
thread_local TArray<FLocalShaft, TInlineAllocator<10>> Shafts;
thread_local TArray<FLocalConn, TInlineAllocator<16>> Conns;
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,
VS_Cross = -1.0f, VS_BotZ = FLT_MAX, VS_TopZ = FLT_MAX;
VS_Cross = -1.0f, VS_ConnR = -1.0f, VS_Rough = -1.0f,
VS_SpineR = -1.0f, VS_BotZ = FLT_MAX, VS_TopZ = FLT_MAX;
if (CX != VS_CX || CY != VS_CY || S != VS_Seed || Spacing != VS_Spacing ||
Params.ShaftDensity != VS_Dens || Params.ShaftMinRadius != VS_MinR || Params.ShaftMaxRadius != VS_MaxR ||
Params.CrossConnectChance != VS_Cross ||
Params.CrossConnectChance != VS_Cross || Params.ConnectorRadius != VS_ConnR ||
Params.SurfaceRoughness != VS_Rough || OriginSpineRadius != VS_SpineR ||
Params.StrateBottomWorldZ != VS_BotZ || Params.StrateTopWorldZ != VS_TopZ)
{
VS_CX = CX; VS_CY = CY; VS_Seed = S; VS_Spacing = Spacing;
VS_Dens = Params.ShaftDensity; VS_MinR = Params.ShaftMinRadius; VS_MaxR = Params.ShaftMaxRadius;
VS_Cross = Params.CrossConnectChance;
VS_Cross = Params.CrossConnectChance; VS_ConnR = Params.ConnectorRadius;
VS_Rough = Params.SurfaceRoughness; VS_SpineR = OriginSpineRadius;
VS_BotZ = Params.StrateBottomWorldZ; VS_TopZ = Params.StrateTopWorldZ;
Shafts.Reset();
Conns.Reset();
@@ -3737,9 +3740,19 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
Sh.Y = (ny + 0.15f + JY * 0.7f) * Spacing;
Sh.R = FMath::Lerp(Params.ShaftMinRadius, Params.ShaftMaxRadius,
VoxelHash::ToFloat01(VoxelHash::Mix(Hh ^ 0xBEEFu)));
Sh.bOriginSpine = false;
Shafts.Add(Sh);
}
// 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.
const bool bOriginNearby = FMath::Abs(CX) <= 1 && FMath::Abs(CY) <= 1;
if (bOriginNearby && OriginSpineRadius > 0.0f)
{
Shafts.Add({0.0f, 0.0f, OriginSpineRadius, true});
}
// Horizontal connectors between nearby shaft pairs (hash-gated).
if (Params.CrossConnectChance > 0.0f && Shafts.Num() >= 2)
{
@@ -3750,6 +3763,7 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
{
const FLocalShaft& A = Shafts[i];
const FLocalShaft& B = Shafts[j];
const bool bSpineConnector = A.bOriginSpine || B.bOriginSpine;
const float DSq = FMath::Square(A.X - B.X) + FMath::Square(A.Y - B.Y);
if (DSq > FMath::Square(Spacing * 1.6f)) continue; // only neighbours
@@ -3760,7 +3774,13 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
if (VoxelHash::ToFloat01(PH) >= Params.CrossConnectChance) continue;
const float Zc = FMath::Lerp(BottomZ, TopZ, VoxelHash::ToFloat01(VoxelHash::Mix(PH)));
Conns.Add({ FVector(A.X, A.Y, Zc), FVector(B.X, B.Y, Zc) });
const float ConnectorR = bSpineConnector
? FMath::Max(
Params.ConnectorRadius,
FMath::Max(Params.SurfaceRoughness, 0.0f)
* VOXEL_NOISE_SCALE * 1.5f + 1.0f)
: Params.ConnectorRadius;
Conns.Add({ FVector(A.X, A.Y, Zc), FVector(B.X, B.Y, Zc), ConnectorR });
}
}
}
@@ -3770,13 +3790,14 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
// Vertical shafts as infinite cylinders (boundary seal handles the ends).
for (const FLocalShaft& Sh : Shafts)
{
if (Sh.bOriginSpine) continue; // VF_ApplyOriginSpine owns the structural column.
const float DX = WorldX - Sh.X;
const float DY = WorldY - Sh.Y;
CaveSDF = FMath::Min(CaveSDF, FMath::Sqrt(DX * DX + DY * DY) - Sh.R);
}
for (const FLocalConn& C : Conns)
{
CaveSDF = FMath::Min(CaveSDF, VoxelSDF::Capsule(Pos, C.A, C.B, Params.ConnectorRadius));
CaveSDF = FMath::Min(CaveSDF, VoxelSDF::Capsule(Pos, C.A, C.B, C.Radius));
}
// Wall roughness.
@@ -3807,6 +3828,7 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
const FLocalShaft* Near = nullptr; float BestSq = FLT_MAX;
for (const FLocalShaft& Sh : Shafts)
{
if (Sh.bOriginSpine) continue; // ledges belong to real shafts, not the post.
const float D2 = FMath::Square(WorldX - Sh.X) + FMath::Square(WorldY - Sh.Y);
if (D2 < BestSq) { BestSq = D2; Near = &Sh; }
}