fix(tunnels): ribs removed at the cause; authored tunnel floor removed; slope acts on the route

The owner proved in game that the tunnel core caused the ribs (voxel.TunnelAblateTunnelCore=1 removes them). Root cause: the core's final reassertion replaced any sufficiently interior sample with a fixed +16 air density, destroying the distance gradient, so marching cubes snapped the surface to the voxel grid (all 385 centre samples saturated). The core now reasserts the continuous -Core.SDF, with bottom polarity from the swept shape. The Catmull-Rom sweep from 0231072 is reverted (it cost 18-30% and did not help); the linear tapered-capsule sweep is restored.

Owner decisions implemented: bTunnelFloorEnabled has no runtime effect (reflected only for asset compatibility, ignored on PostLoad), and the authored slab / graph support-floor paths are detached from generation, so the tunnel's own carved shape is the floor. Winding and dramatic ledges no longer depend on the floor switch. Degrees authoring, its migration, and the 3840-voxel route cap stay. Mouth trim stays behind voxel.TunnelMouthTrim (default off).

Evidence: along owner tunnel 15, 385 samples at 0.25 voxel, periodic surface amplitude floor 3.55 -> 0.50 cm, wall 16.9 -> 1.10 cm, ceiling 8.80 -> 0.69 cm (acceptance < 2 cm); 0/385 samples saturated. Matched-camera render: ribs absent. Canonical walk: Connected, player-fit 41,184 (was 20,830), walk-only 13,879 (was 10,909), mouth gap 0; mesh identical with -voxel.CrtFma3=0. TilePostReachProof, DetSinCos, strate connectivity 16/16, strate crossing: pass. Perf, one S/M/S/M pair vs a rebuilt bf9384a DLL: static 26.10 vs 25.41 s (+2.7%), moving 55.34 vs 53.08 s (+4.2%), request p95 about equal. Treat as a possible small cost to re-measure, not as noise.

Not merged into experimental: waiting for the owner's in-game look.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-09-17 02:48:20 +02:00
1 parent 0231072cc4
commit 4fa3931176
11 files changed
+1237 -855

No files matched your search

+107 -293
View File
@@ -82,6 +82,9 @@ namespace
{
float SDF = FLT_MAX;
bool bSupportFloor = false;
bool bHasSweptFloor = false;
float SweptFloorZ = -FLT_MAX;
float SweptFloorRadius = 0.0f;
};
// FMath::PerlinNoise2D is the same two-octave field used by room floors. The derivative bound
@@ -621,6 +624,11 @@ namespace
struct FVFRoomFloorOwnership
{
bool bValid = false;
// The room-floor blend is intentionally short, but the tunnel capsule and the fitted
// player envelope can still overlap the room beyond that blend. This separate hand-off
// bit prevents the graph tunnel's lower-side polarity from turning that overlap into a
// wall without extending the room floor itself into the corridor.
bool bSuppressTunnelBottom = false;
float FloorZ = 0.0f;
float Weight = 0.0f;
uint32 RoomHash = 0;
@@ -670,7 +678,13 @@ namespace
: (RelativeDistance < Fade
? 1.0f - SmoothStep01(RelativeDistance / Fade)
: 0.0f);
if (!(Weight > 0.0f))
const float EndpointRadius = FMath::Abs(ControlRadiusData[ControlIndex]);
const float BottomHandoffRadius = BlendRadius
+ VF_TunnelMouthPlayerFitOverlapVoxels
+ EndpointRadius;
const bool bInBottomHandoff = VoxelMath::IsFinite(BottomHandoffRadius)
&& Distance <= BottomHandoffRadius;
if (!(Weight > 0.0f) && !bInBottomHandoff)
{
continue;
}
@@ -694,7 +708,8 @@ namespace
continue;
}
BestScore = Score;
Result.bValid = true;
Result.bValid = Weight > 0.0f;
Result.bSuppressTunnelBottom = bInBottomHandoff;
Result.FloorZ = FloorZ;
Result.Weight = Weight;
Result.RoomHash = Endpoint == 0 ? FloorRoomHashA : FloorRoomHashB;
@@ -897,146 +912,6 @@ namespace
return true;
}
// A world-chain handoff is sampled after the cave-warped SDF chain has already carved the
// tunnel. It must follow the same continuous centreline, not union another tapered capsule
// at every control point. A fixed Catmull-Rom span gives adjacent spans a shared tangent;
// the fixed projection iterations keep the result deterministic and bounded.
struct FVFTunnelCubicSpan
{
FVector P0 = FVector::ZeroVector;
FVector P1 = FVector::ZeroVector;
FVector P2 = FVector::ZeroVector;
FVector P3 = FVector::ZeroVector;
float R0 = 0.0f;
float R1 = 0.0f;
float R2 = 0.0f;
float R3 = 0.0f;
FORCEINLINE FVector Position(float T) const
{
const float T2 = T * T;
const float T3 = T2 * T;
return (P1 * 2.0f
+ (P2 - P0) * T
+ (P0 * 2.0f - P1 * 5.0f + P2 * 4.0f - P3) * T2
+ (-P0 + P1 * 3.0f - P2 * 3.0f + P3) * T3) * 0.5f;
}
FORCEINLINE FVector FirstDerivative(float T) const
{
const float T2 = T * T;
return ((P2 - P0)
+ (P0 * 2.0f - P1 * 5.0f + P2 * 4.0f - P3) * (2.0f * T)
+ (-P0 + P1 * 3.0f - P2 * 3.0f + P3) * (3.0f * T2)) * 0.5f;
}
FORCEINLINE FVector SecondDerivative(float T) const
{
return ((P0 * 2.0f - P1 * 5.0f + P2 * 4.0f - P3) * 2.0f
+ (-P0 + P1 * 3.0f - P2 * 3.0f + P3) * (6.0f * T)) * 0.5f;
}
FORCEINLINE float Radius(float T) const
{
const float T2 = T * T;
const float T3 = T2 * T;
const float Value = (R1 * 2.0f
+ (R2 - R0) * T
+ (R0 * 2.0f - R1 * 5.0f + R2 * 4.0f - R3) * T2
+ (-R0 + R1 * 3.0f - R2 * 3.0f + R3) * T3) * 0.5f;
return FMath::Max(Value, 0.5f);
}
};
static FVFTunnelCubicSpan VF_MakeTunnelCubicSpan(
const TArray<FVector>& ControlPoints,
const TArray<float>& ControlRadii,
int32 SegmentIndex)
{
const int32 LastPoint = ControlPoints.Num() - 1;
const FVector& P1 = ControlPoints[SegmentIndex];
const FVector& P2 = ControlPoints[SegmentIndex + 1];
const FVector P0 = SegmentIndex > 0
? ControlPoints[SegmentIndex - 1] : P1 + (P1 - P2);
const FVector P3 = SegmentIndex + 2 <= LastPoint
? ControlPoints[SegmentIndex + 2] : P2 + (P2 - P1);
const float R1 = ControlRadii[SegmentIndex];
const float R2 = ControlRadii[SegmentIndex + 1];
const float R0 = SegmentIndex > 0
? ControlRadii[SegmentIndex - 1] : R1 + (R1 - R2);
const float R3 = SegmentIndex + 2 <= LastPoint
? ControlRadii[SegmentIndex + 2] : R2 + (R2 - R1);
FVFTunnelCubicSpan Span;
Span.P0 = P0;
Span.P1 = P1;
Span.P2 = P2;
Span.P3 = P3;
Span.R0 = R0;
Span.R1 = R1;
Span.R2 = R2;
Span.R3 = R3;
return Span;
}
static bool VF_ProjectTunnelCubicXY(
const FVector& Position,
const FVFTunnelCubicSpan& Span,
float& OutT,
float& OutDistanceSquared)
{
float BestT = 0.0f;
float BestDistanceSquared = FLT_MAX;
auto Consider = [&](float T)
{
const FVector Candidate = Span.Position(T);
const float DX = static_cast<float>(Candidate.X - Position.X);
const float DY = static_cast<float>(Candidate.Y - Position.Y);
const float DistanceSquared = DX * DX + DY * DY;
if (DistanceSquared < BestDistanceSquared)
{
BestDistanceSquared = DistanceSquared;
BestT = T;
}
};
constexpr int32 InitialSamples = 8;
for (int32 Sample = 0; Sample <= InitialSamples; ++Sample)
{
Consider(static_cast<float>(Sample) / InitialSamples);
}
float CandidateT = BestT;
for (int32 Iteration = 0; Iteration < 6; ++Iteration)
{
const FVector CurvePoint = Span.Position(CandidateT);
const FVector First = Span.FirstDerivative(CandidateT);
const FVector Second = Span.SecondDerivative(CandidateT);
const float DX = static_cast<float>(CurvePoint.X - Position.X);
const float DY = static_cast<float>(CurvePoint.Y - Position.Y);
const float FirstX = static_cast<float>(First.X);
const float FirstY = static_cast<float>(First.Y);
const float SecondX = static_cast<float>(Second.X);
const float SecondY = static_cast<float>(Second.Y);
const float Numerator = DX * FirstX + DY * FirstY;
const float Denominator = FirstX * FirstX + FirstY * FirstY
+ DX * SecondX + DY * SecondY;
if (!VoxelMath::IsFinite(Numerator)
|| !VoxelMath::IsFinite(Denominator)
|| FMath::Abs(Denominator) <= KINDA_SMALL_NUMBER)
{
break;
}
CandidateT = FMath::Clamp(
CandidateT - Numerator / Denominator, 0.0f, 1.0f);
Consider(CandidateT);
}
OutT = BestT;
OutDistanceSquared = BestDistanceSquared;
return VoxelMath::IsFinite(OutT)
&& VoxelMath::IsFinite(OutDistanceSquared);
}
static FVFTunnelShapeEvaluation VF_EvaluateSweptTunnelChain(
const FVector& Position,
const TArray<FVector>& ControlPoints,
@@ -1044,7 +919,6 @@ namespace
const FCachedTunnel& Tunnel,
float SDFBlendRadius,
bool bApplyFloorCut,
bool bUseContinuousCurve,
const void* CacheIdentity, int32 TunnelIndex,
const FVFFloorReliefColumnKey& ReliefColumn,
const TArray<FTunnelFloorSegmentProfile>* FloorProfiles,
@@ -1077,13 +951,7 @@ namespace
const float RadiusB = ControlRadiusData[SegmentIndex + 1];
float T = 0.0f;
float HorizontalDistanceSquared = FLT_MAX;
const FVFTunnelCubicSpan Span = bUseContinuousCurve
? VF_MakeTunnelCubicSpan(ControlPoints, ControlRadii, SegmentIndex)
: FVFTunnelCubicSpan();
const bool bHasHorizontalProjection = bUseContinuousCurve
? VF_ProjectTunnelCubicXY(
Position, Span, T, HorizontalDistanceSquared)
: VF_ProjectTunnelSegmentXY(
const bool bHasHorizontalProjection = VF_ProjectTunnelSegmentXY(
Position, A, B, T, HorizontalDistanceSquared);
if (!bHasHorizontalProjection)
{
@@ -1095,10 +963,6 @@ namespace
continue;
}
const FVector CurvePoint = bUseContinuousCurve
? Span.Position(T) : FMath::Lerp(A, B, T);
const float CurveRadius = bUseContinuousCurve
? Span.Radius(T) : FMath::Lerp(RadiusA, RadiusB, T);
const FTunnelFloorSegmentProfile* Profile = FloorProfileData != nullptr
? FloorProfileData + SegmentIndex : nullptr;
const FVFTunnelFloorTerms FloorTerms = VF_TunnelFloorTerms(
@@ -1137,13 +1001,12 @@ namespace
// headroom in every gentle tunnel merely because a profile is now always present.
const bool bUseAuthoredProfile = bApplyFloorCut
&& Profile != nullptr
&& Tunnel.bTunnelFloorEnabled
&& (Tunnel.bFloorRouteWasWound || Tunnel.bDramaticLedge);
const bool bAnchorCapsule = bUseAuthoredProfile
&& VoxelMath::IsFinite(NaturalFloorZ);
const auto EvaluateAnchoredShape = [
&Position, &A, &B, RadiusA, RadiusB, CurvePoint, CurveRadius,
NaturalFloorZ, bAnchorCapsule, bUseContinuousCurve]
&Position, &A, &B, RadiusA, RadiusB,
NaturalFloorZ, bAnchorCapsule]
(float FloorZ) -> float
{
const float Shift = FloorZ - NaturalFloorZ;
@@ -1151,15 +1014,11 @@ namespace
|| !VoxelMath::IsFinite(Shift)
|| FMath::Abs(Shift) <= KINDA_SMALL_NUMBER)
{
return bUseContinuousCurve
? FVector::Dist(Position, CurvePoint) - CurveRadius
: VoxelSDF::TaperedCapsule(
return VoxelSDF::TaperedCapsule(
Position, A, B, RadiusA, RadiusB);
}
const FVector Offset(0.0f, 0.0f, Shift);
return bUseContinuousCurve
? FVector::Dist(Position, CurvePoint + Offset) - CurveRadius
: VoxelSDF::TaperedCapsule(
return VoxelSDF::TaperedCapsule(
Position, A + Offset, B + Offset, RadiusA, RadiusB);
};
float SegmentSDF = EvaluateAnchoredShape(FloorTerms.BaseFloorZ);
@@ -1211,18 +1070,26 @@ namespace
? VoxelSDF::SmoothMax(
SegmentSDF, FloorZ - static_cast<float>(Position.Z), FloorBlend)
: SegmentSDF;
Result.SDF = FMath::Min(Result.SDF, SegmentResultSDF);
if (bApplyFloorCut && Tunnel.bTunnelFloorEnabled
&& SegmentSDF <= 0.0f
&& HorizontalDistanceSquared <= FMath::Square(SupportRadius)
&& Position.Z >= FloorZ
- VoxelPassageGeometry::LandingFloorThicknessVoxels
&& Position.Z <= FloorZ
+ VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels)
if (SegmentResultSDF < Result.SDF)
{
Result.bSupportFloor = true;
Result.SDF = SegmentResultSDF;
// This is shape metadata, not a second floor field. It records which side of
// the winning swept SDF is its own bottom so the final MC hand-off can restore
// that side when a room's generic air field would otherwise erase it.
Result.bHasSweptFloor = bApplyFloorCut
&& VoxelMath::IsFinite(FloorZ)
&& VoxelMath::IsFinite(SupportRadius)
&& VoxelMath::IsFinite(HorizontalDistanceSquared)
&& HorizontalDistanceSquared <= FMath::Square(SupportRadius)
// The room owns the finite mouth apron. Do not let the tunnel's bottom
// polarity turn that overlap into a wall; the corridor interior remains
// owned by this swept shape.
&& !(RoomFloorOwnership != nullptr
&& RoomFloorOwnership->bSuppressTunnelBottom);
Result.SweptFloorZ = Result.bHasSweptFloor ? FloorZ : -FLT_MAX;
Result.SweptFloorRadius = Result.bHasSweptFloor ? SupportRadius : 0.0f;
}
}
return Result;
}
@@ -1249,7 +1116,6 @@ namespace
return VF_EvaluateSweptTunnelChain(
Position, ShapeControlPoints, ShapeControlRadii,
Tunnel, SDFBlendRadius, bApplyFloorCut,
bWorldChain,
CacheIdentity, TunnelIndex, ReliefColumn,
bWorldChain ? &Tunnel.WorldFloorProfiles : &Tunnel.FloorProfiles,
RoomFloorOwnership);
@@ -1270,7 +1136,6 @@ namespace
return VF_EvaluateSweptTunnelChain(
Position, Points, Radii,
Tunnel, SDFBlendRadius, bApplyFloorCut,
bWorldChain,
CacheIdentity, TunnelIndex, ReliefColumn,
bWorldChain ? &Tunnel.WorldFloorProfiles : &Tunnel.FloorProfiles,
RoomFloorOwnership);
@@ -1285,7 +1150,6 @@ namespace
return VF_EvaluateSweptTunnelChain(
Position, Points, Radii,
Tunnel, SDFBlendRadius, bApplyFloorCut,
bWorldChain,
CacheIdentity, TunnelIndex, ReliefColumn,
bWorldChain ? &Tunnel.WorldFloorProfiles : &Tunnel.FloorProfiles,
RoomFloorOwnership);
@@ -1300,10 +1164,6 @@ namespace
const void* CacheIdentity, int32 TunnelIndex,
const FVFRoomFloorOwnership* RoomFloorOwnership = nullptr)
{
if (!Tunnel.bTunnelFloorEnabled)
{
return false;
}
const FVFFloorReliefColumnKey ReliefColumn =
VF_MakeFloorReliefColumnKey(
static_cast<float>(Position.X), static_cast<float>(Position.Y));
@@ -1780,12 +1640,6 @@ void FChunkSDFCache::Reset()
PitSpatialIndex.Reset();
ChimneySpatialIndex.Reset();
ColumnSpatialIndex.Reset();
SupportColumnMinX = 0;
SupportColumnMinY = 0;
SupportColumnCellsX = 0;
SupportColumnCellsY = 0;
SupportColumnEntries.Reset();
SupportColumns.Reset();
SDFBlendRadius = 0.0f;
}
@@ -1803,12 +1657,6 @@ void FChunkSDFCache::Release()
PitSpatialIndex.Release();
ChimneySpatialIndex.Release();
ColumnSpatialIndex.Release();
SupportColumnEntries.Empty();
SupportColumns.Empty();
SupportColumnMinX = 0;
SupportColumnMinY = 0;
SupportColumnCellsX = 0;
SupportColumnCellsY = 0;
}
SIZE_T FChunkSDFCache::GetAllocatedSize() const
@@ -1830,8 +1678,6 @@ VoxelDensityProfile::FCacheMemoryBreakdown FChunkSDFCache::GetAllocatedSizeBreak
Breakdown.PitsBytes = ArrayBytes(Pits);
Breakdown.ChimneysBytes = ArrayBytes(Chimneys);
Breakdown.ColumnsBytes = ArrayBytes(Columns);
Breakdown.SupportColumnEntriesBytes = ArrayBytes(SupportColumnEntries);
Breakdown.SupportColumnsBytes = ArrayBytes(SupportColumns);
Breakdown.SpatialIndexBytes = static_cast<uint64>(
RoomSpatialIndex.GetAllocatedSize()
+ RoomFloorJoinSpatialIndex.GetAllocatedSize()
@@ -1839,11 +1685,6 @@ VoxelDensityProfile::FCacheMemoryBreakdown FChunkSDFCache::GetAllocatedSizeBreak
+ PitSpatialIndex.GetAllocatedSize()
+ ChimneySpatialIndex.GetAllocatedSize()
+ ColumnSpatialIndex.GetAllocatedSize());
for (const FTunnelSupportFloorColumn& Column : SupportColumns)
{
Breakdown.SupportColumnIntervalsBytes += ArrayBytes(Column.Intervals);
}
for (const FCachedTunnel& Tunnel : Tunnels)
{
Breakdown.TunnelsBytes += ArrayBytes(Tunnel.ControlPoints);
@@ -2387,12 +2228,10 @@ static bool VF_ComputeRoomGraphReach(
const float GentleSlopeThreshold = VoxelPassageGeometry::TunnelFloorGradientFromDegrees(
Params.TunnelFloorGentleSlopeThreshold);
const float MaxFloorDeltaEnvelope = 2.0f * EndpointVertical;
const float RequiredRouteRunEnvelope = Params.bTunnelFloorEnabled
? FMath::Min(
const float RequiredRouteRunEnvelope = FMath::Min(
VoxelPassageGeometry::TunnelFloorMaximumRouteLengthVoxels,
MaxFloorDeltaEnvelope / FMath::Max(
GentleSlopeThreshold, KINDA_SMALL_NUMBER))
: 0.0f;
GentleSlopeThreshold, KINDA_SMALL_NUMBER));
const float OrganicLateralEnvelope = FMath::Min(
FMath::Abs(Params.TunnelWarpStrength), TunnelLengthBound * 0.25f);
const float RouteLateralEnvelope = FMath::Max(
@@ -7023,7 +6862,6 @@ void VoxelCaveMorphology::BuildChunkCache(
CT.bHasFloorRoomOwnership = true;
CT.FloorRoomHashA = RoomA.Hash;
CT.FloorRoomHashB = RoomB.Hash;
CT.bTunnelFloorEnabled = Params.bTunnelFloorEnabled;
CT.bTunnelFloorTerracingEnabled = Params.bTunnelFloorTerracingEnabled;
CT.TunnelFloorTerraceStepHeight = Params.TunnelFloorTerraceStepHeight;
CT.TunnelFloorMaxLedgeHeight = Params.TunnelFloorMaxLedgeHeight;
@@ -7067,8 +6905,7 @@ void VoxelCaveMorphology::BuildChunkCache(
const uint32 LedgeChoiceHash = VoxelHash::Mix(
TunnelHash ^ 0xC1A0E5EDu);
constexpr float DramaticLedgeChance = 0.35f;
const bool bDramaticLedge = Params.bTunnelFloorEnabled
&& Params.bTunnelFloorTerracingEnabled
const bool bDramaticLedge = Params.bTunnelFloorTerracingEnabled
&& bDirectSteep
&& bGraphLedgeEligible
&& VoxelHash::ToFloat01(LedgeChoiceHash) < DramaticLedgeChance;
@@ -7116,8 +6953,7 @@ void VoxelCaveMorphology::BuildChunkCache(
// supplies the horizontal run for the authored floor plane and keeps the room-to-room
// graph connected. The rings observed in the owner asset came from the separate
// world-core handoff exposing span joins, not from this smooth route decision.
const bool bNeedsWind = Params.bTunnelFloorEnabled
&& bDirectSteep
const bool bNeedsWind = bDirectSteep
&& !bDramaticLedge
&& GentleThreshold > KINDA_SMALL_NUMBER;
CT.bFloorRouteWasWound = bNeedsWind;
@@ -7984,7 +7820,8 @@ float VoxelCaveMorphology::EvaluateSDFCached(
const FChunkSDFCache& Cache,
float SDFBlendRadius,
int32* OutNearestRoomIdx,
bool bUseSpatialIndex)
bool bUseSpatialIndex,
const FVector* WorldTunnelPosition)
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::RoomGraphSdf);
@@ -8144,20 +7981,41 @@ float VoxelCaveMorphology::EvaluateSDFCached(
//=========================================================================
// Tunnel SDFs
//=========================================================================
// Keep the tunnel source in the authored world frame when the caller supplies it. Rooms
// intentionally remain at the warped query position; the world-chain tunnel is the same
// continuous field used by the core hand-off below, so the source cannot form a second,
// differently-shaped surface around the route.
const bool bUseWorldTunnel = WorldTunnelPosition != nullptr;
const FVector TunnelPosition = bUseWorldTunnel ? *WorldTunnelPosition : Pos;
auto EvaluateTunnel = [&](int32 TunnelIdx)
{
const FCachedTunnel& Tunnel = TunnelData[TunnelIdx];
if (Tunnel.SDFInfluenceRadius > 0.0f
const bool bHasWorldChain = Tunnel.WorldControlPoints.Num() >= 2
&& Tunnel.WorldControlRadii.Num() == Tunnel.WorldControlPoints.Num();
const bool bUseWorldChain = bUseWorldTunnel && bHasWorldChain;
const FVector& QueryPosition = bUseWorldChain ? TunnelPosition : Pos;
const FVector& CenterlineMin = bUseWorldChain
? Tunnel.WorldCenterlineMin : Tunnel.SDFCenterlineMin;
const FVector& CenterlineMax = bUseWorldChain
? Tunnel.WorldCenterlineMax : Tunnel.SDFCenterlineMax;
const float InfluenceRadius = bUseWorldChain
? Tunnel.WorldInfluenceRadius : Tunnel.SDFInfluenceRadius;
const FVector& BoundCenter = bUseWorldChain
? Tunnel.WorldBoundCenter : Tunnel.BoundCenter;
const float BoundRadiusSq = bUseWorldChain
? Tunnel.WorldBoundRadiusSq : Tunnel.BoundRadiusSq;
if (InfluenceRadius > 0.0f
&& VF_DistanceSquaredToAabb(
WorldX, WorldY, WorldZ,
Tunnel.SDFCenterlineMin, Tunnel.SDFCenterlineMax)
> FMath::Square(Tunnel.SDFInfluenceRadius))
QueryPosition.X, QueryPosition.Y, QueryPosition.Z,
CenterlineMin, CenterlineMax)
> FMath::Square(InfluenceRadius))
{
return;
}
// --- BOUNDING SPHERE CULL ---
const float DistSq = VF_FloatDistSquared(WorldX, WorldY, WorldZ, Tunnel.BoundCenter);
if (DistSq > Tunnel.BoundRadiusSq) return;
const float DistSq = VF_FloatDistSquared(
QueryPosition.X, QueryPosition.Y, QueryPosition.Z, BoundCenter);
if (DistSq > BoundRadiusSq) return;
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
@@ -8168,11 +8026,12 @@ float VoxelCaveMorphology::EvaluateSDFCached(
// its build-time floor profile before the tunnel union is blended into rooms. The room's
// finite mouth ownership is also passed into that same shape evaluation; this is the SDF
// side of A > B, while the world-space post keeps the disturbance path consistent.
const FVFRoomFloorOwnership RoomMouthOwnership =
VF_FindSDFTunnelMouthOwnership(Pos, Tunnel, BlendK);
const FVFRoomFloorOwnership RoomMouthOwnership = bUseWorldChain
? VF_FindWorldTunnelMouthOwnership(QueryPosition, Tunnel, BlendK)
: VF_FindSDFTunnelMouthOwnership(Pos, Tunnel, BlendK);
const float TunnelSDF = VF_EvaluateSweptTunnel(
Pos, Tunnel, /*bWorldChain=*/false, BlendK,
/*bApplyFloorCut=*/Tunnel.bTunnelFloorEnabled, &Cache, TunnelIdx,
QueryPosition, Tunnel, bUseWorldChain, BlendK,
/*bApplyFloorCut=*/true, &Cache, TunnelIdx,
&RoomMouthOwnership).SDF;
MinSDF = VoxelSDF::SmoothMin(MinSDF, TunnelSDF, BlendK);
@@ -8182,7 +8041,8 @@ float VoxelCaveMorphology::EvaluateSDFCached(
if (!VoxelDensityAblation::IsTunnelSDFOff())
{
TunnelCandidateCount = VF_ForEachSpatialCandidate(
Cache.TunnelSpatialIndex, Cache.Tunnels.Num(), WorldX, WorldY, EvaluateTunnel,
Cache.TunnelSpatialIndex, Cache.Tunnels.Num(), TunnelPosition.X, TunnelPosition.Y,
EvaluateTunnel,
bUseSpatialIndex);
}
if (VoxelDensityProfile::AreCountersEnabled())
@@ -8260,7 +8120,14 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
// GenerateMesh installs the exact tile/block reach mask after building the complete tile
// cache. Point queries retain the canonical all-reachable TLS default; debug proof calls set
// the bypass bit so the same sample can be evaluated as the reference field.
if (!VoxelGenLOD::IsTilePostReachBypassActive()
// A reach mask is meaningful only inside the mesher's proven tile session. Point queries
// (including the owner-value probe and gameplay queries) must retain the canonical core even
// if another caller left a conservative flag in this worker's TLS. The mesher enables the
// proof bit only after it has installed the exact tile-window decision, so this guard cannot
// turn an ordinary query into a skipped core.
if (VoxelGenLOD::bTileCoreReachProofEnabled
&& VoxelGenLOD::bTileReachDiagnosticTile
&& !VoxelGenLOD::IsTilePostReachBypassActive()
&& !VoxelGenLOD::IsTunnelCoreReachable())
{
return FTunnelCoreWorldEvaluation();
@@ -8328,73 +8195,23 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
const FVFRoomFloorOwnership RoomMouthOwnership =
VF_FindWorldTunnelMouthOwnership(Pos, Tunnel, Cache.SDFBlendRadius);
const bool bRoomOwnsThisTunnel = RoomMouthOwnership.bValid;
bool bRaisedTunnelFloor = false;
// The world chain is used only for its finite support-floor ownership band. Its air
// surface follows a continuous sweep, while the ordinary cave-warped chain remains the
// authoritative carved tunnel. Evaluating a second per-span capsule union creates a
// segmented zero set at every control-span join.
if (bHasWorldChain && Tunnel.bTunnelFloorEnabled)
{
if (SupportColumn != nullptr && VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelSupportFloorChecks);
}
float FloorZ = 0.0f;
float SupportRadius = 0.0f;
float SupportMinZ = 0.0f;
float SupportMaxZ = 0.0f;
const bool bProjectedFloor = SupportColumn != nullptr
? VoxelCaveMorphology::GetTunnelSupportFloorColumnBand(
TunnelIdx, *SupportColumn, FloorZ, SupportMinZ, SupportMaxZ)
: VF_ProjectSweptTunnelFloor(
Pos, Tunnel, /*bWorldChain=*/true, FloorZ, SupportRadius,
&Cache, TunnelIdx, &RoomMouthOwnership);
bRaisedTunnelFloor = bRoomOwnsThisTunnel
&& bProjectedFloor
&& FloorZ > RoomMouthOwnership.FloorZ
+ KINDA_SMALL_NUMBER;
if (bRaisedTunnelFloor
&& WorldZ <= RoomMouthOwnership.FloorZ
+ VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels)
{
if (WorldZ > RoomMouthOwnership.FloorZ
- VoxelPassageGeometry::LandingFloorThicknessVoxels)
{
Result.bRoomFloor = true;
}
// A raised tunnel apron is the artifact being clipped. Keep the room floor in
// its finite support band and do not reopen the room below it.
return;
}
if (!bRaisedTunnelFloor
&& bProjectedFloor
&& WorldZ <= FloorZ
+ VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels
&& (SupportColumn == nullptr
? WorldZ >= FloorZ
- VoxelPassageGeometry::LandingFloorThicknessVoxels
: WorldZ >= SupportMinZ && WorldZ <= SupportMaxZ))
{
Result.bSupportFloor = true;
}
if (!bRaisedTunnelFloor
&& bProjectedFloor && WorldZ <= FloorZ
+ VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels)
{
// Preserve the established ownership contract while the SDF-space evaluator is
// still raw: the floor owns the lower half of the corridor, and the capsule owns
// only the air above it. This is a shape-level ownership hand-off, not a later slab.
return;
}
}
// The world chain is the authored tunnel shape. Its floor is evaluated by the same
// SmoothMax profile as the cave-warped chain; there is no second support-column field and
// no hard floor hand-off at a segment boundary. Keep the legacy argument source-compatible
// for callers compiled against the previous probe API, but deliberately ignore it.
(void)SupportColumn;
const FVFTunnelShapeEvaluation TunnelShape = VF_EvaluateSweptTunnel(
Pos, Tunnel, bHasWorldChain, Cache.SDFBlendRadius,
/*bApplyFloorCut=*/Tunnel.bTunnelFloorEnabled && !bRaisedTunnelFloor,
/*bApplyFloorCut=*/true,
&Cache, TunnelIdx, &RoomMouthOwnership);
Result.SDF = FMath::Min(Result.SDF, TunnelShape.SDF);
if (TunnelShape.SDF < Result.SDF)
{
Result.SDF = TunnelShape.SDF;
Result.bHasSweptFloor = TunnelShape.bHasSweptFloor;
Result.SweptFloorZ = TunnelShape.SweptFloorZ;
Result.SweptFloorRadius = TunnelShape.SweptFloorRadius;
}
};
const int32 TunnelCandidateCount = VF_ForEachSpatialCandidate(
@@ -8604,8 +8421,7 @@ bool VoxelCaveMorphology::AnyTunnelCoreWorldNearLattice(
float ProfileFloorDelta = 0.0f;
const TArray<FTunnelFloorSegmentProfile>& Profiles = bHasWorldChain
? Tunnel.WorldFloorProfiles : Tunnel.FloorProfiles;
if (Tunnel.bTunnelFloorEnabled
&& Profiles.Num() == ChainPoints.Num() - 1)
if (Profiles.Num() == ChainPoints.Num() - 1)
{
for (int32 SegmentIndex = 0;
SegmentIndex < Profiles.Num(); ++SegmentIndex)
@@ -8632,11 +8448,9 @@ bool VoxelCaveMorphology::AnyTunnelCoreWorldNearLattice(
}
if (bHasTightChain)
{
const float FloorPad = Tunnel.bTunnelFloorEnabled
? FMath::Max(
const float FloorPad = FMath::Max(
VoxelPassageGeometry::LandingFloorThicknessVoxels,
FMath::Max(Cache.SDFBlendRadius, 0.0f) * 0.35f)
: 0.0f;
FMath::Max(Cache.SDFBlendRadius, 0.0f) * 0.35f);
const float SegmentPad = MaxRadius + FloorPad
+ ProfileFloorDelta + ReliefBound;
if (!VoxelMath::IsFinite(SegmentPad))
@@ -8667,8 +8481,8 @@ bool VoxelCaveMorphology::AnyTunnelCoreWorldNearLattice(
}
}
if (!bTightTouch && Tunnel.bTunnelFloorEnabled
&& bHasWorldChain && Tunnel.bHasFloorRoomOwnership)
if (!bTightTouch && bHasWorldChain
&& Tunnel.bHasFloorRoomOwnership)
{
const float MouthHorizontal = MouthReach;
const float MouthVertical = FMath::Max(
@@ -8792,7 +8606,7 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
return VF_EvaluateSweptTunnel(
Pos, Tunnel, bHasWorldChain, Cache.SDFBlendRadius,
/*bApplyFloorCut=*/Tunnel.bTunnelFloorEnabled, &Cache, TunnelIdx,
/*bApplyFloorCut=*/true, &Cache, TunnelIdx,
&RoomMouthOwnership).bSupportFloor;
};
+33 -275
View File
@@ -3748,110 +3748,6 @@ namespace
FVector LastTunnelCoreEvaluationPosition = FVector::ZeroVector;
bool bLastTunnelCoreWorldEvaluationValid = false;
// Support-floor projection depends only on XY for one immutable SDF cache. The
// mesher traverses Z outside X/Y, so keep a small worker-local LRU of XY boxes rather
// than only the immediately previous column; each integer column is then projected
// once and both structural posts reuse its interval for every Z sample.
struct FSupportColumnBox
{
static constexpr int32 Halo = CHUNK_SIZE + 8;
static constexpr int32 Dim = 2 * Halo + 1;
const FChunkSDFCache* Cache = nullptr;
int32 BaseX = 0;
int32 BaseY = 0;
uint32 LastUse = 0;
bool bValid = false;
TArray<int32> SlotToEntry;
TArray<FTunnelSupportFloorColumn> Columns;
void Clear()
{
Cache = nullptr;
bValid = false;
LastUse = 0;
SlotToEntry.Reset();
Columns.Reset();
}
FTunnelSupportFloorColumn* AcquireColumn(
int32 X, int32 Y, const FChunkSDFCache* InCache,
uint32 InUse, bool& bOutNew)
{
bOutNew = false;
if (!bValid || Cache != InCache
|| X < BaseX || X >= BaseX + Dim
|| Y < BaseY || Y >= BaseY + Dim)
{
Cache = InCache;
BaseX = X - Halo;
BaseY = Y - Halo;
LastUse = InUse;
bValid = true;
SlotToEntry.Init(INDEX_NONE, Dim * Dim);
Columns.Reset();
}
else
{
LastUse = InUse;
}
const int32 Slot = (Y - BaseY) * Dim + (X - BaseX);
int32& EntryIndex = SlotToEntry[Slot];
if (EntryIndex == INDEX_NONE)
{
EntryIndex = Columns.AddDefaulted();
bOutNew = true;
}
return &Columns[EntryIndex];
}
};
struct FSupportColumnCache
{
static constexpr int32 NumBoxes = 4;
FSupportColumnBox Boxes[NumBoxes];
uint32 Clock = 0;
void Invalidate()
{
for (FSupportColumnBox& Box : Boxes)
{
Box.Clear();
}
Clock = 0;
}
FSupportColumnBox& AcquireBox(
int32 X, int32 Y, const FChunkSDFCache* Cache)
{
++Clock;
for (FSupportColumnBox& Box : Boxes)
{
if (Box.bValid && Box.Cache == Cache
&& X >= Box.BaseX && X < Box.BaseX + FSupportColumnBox::Dim
&& Y >= Box.BaseY && Y < Box.BaseY + FSupportColumnBox::Dim)
{
Box.LastUse = Clock;
return Box;
}
}
FSupportColumnBox* Victim = &Boxes[0];
for (FSupportColumnBox& Box : Boxes)
{
if (!Box.bValid || Box.LastUse < Victim->LastUse)
{
Victim = &Box;
}
}
Victim->Clear();
return *Victim;
}
} SupportColumns;
FTunnelSupportFloorColumn FractionalSupportColumn;
/** ÉTAPE C1 — les params de la strate avec l'op de CETTE salle appliqué par-dessus.
* Mémo par voxel : invalidé au début de chaque `Eval`, calculé au PREMIER modificateur
* qui le demande. C'est ce qui reproduit le coût de l'original (une copie de struct par
@@ -3906,49 +3802,6 @@ namespace
S.bLocalParamsValid = false;
}
void BuildSupportColumn(
float WorldX, float WorldY,
const FChunkSDFCache& Cache,
FTunnelSupportFloorColumn& OutColumn) const
{
VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
WorldX, WorldY, Cache, OutColumn,
VoxelGenLOD::ShouldUseSpatialIndex(false));
}
const FTunnelSupportFloorColumn* GetSupportColumn(const FVector& Position) const
{
FState& S = State();
const bool bIntegerXY =
Position.X == FMath::FloorToFloat(static_cast<float>(Position.X))
&& Position.Y == FMath::FloorToFloat(static_cast<float>(Position.Y));
if (bIntegerXY)
{
const int32 IX = FMath::FloorToInt(static_cast<float>(Position.X));
const int32 IY = FMath::FloorToInt(static_cast<float>(Position.Y));
const FChunkSDFCache& Cache = GetCache();
FState::FSupportColumnBox& Box = S.SupportColumns.AcquireBox(
IX, IY, &Cache);
bool bNewColumn = false;
FTunnelSupportFloorColumn* Column = Box.AcquireColumn(
IX, IY, &Cache, S.SupportColumns.Clock, bNewColumn);
if (bNewColumn)
{
BuildSupportColumn(
static_cast<float>(Position.X),
static_cast<float>(Position.Y),
Cache,
*Column);
}
return Column;
}
BuildSupportColumn(
static_cast<float>(Position.X), static_cast<float>(Position.Y),
GetCache(), S.FractionalSupportColumn);
return &S.FractionalSupportColumn;
}
bool TryGetLastTunnelCoreWorldEvaluation(
FTunnelCoreWorldEvaluation& OutEvaluation) const override
{
@@ -3971,9 +3824,8 @@ namespace
// demand, publish the one combined result here so the tail can reuse it without
// building/scanning a second native cache.
const FVector& Position = S.LastWorldPosition;
const FTunnelSupportFloorColumn* SupportColumn = GetSupportColumn(Position);
S.LastTunnelCoreWorldEvaluation = VoxelCaveMorphology::EvaluateTunnelCoreWorld(
Position.X, Position.Y, Position.Z, GetCache(), SupportColumn,
Position.X, Position.Y, Position.Z, GetCache(), nullptr,
VoxelGenLOD::ShouldUseSpatialIndex(false));
S.LastTunnelCoreEvaluationPosition = Position;
S.bLastTunnelCoreWorldEvaluationValid = true;
@@ -4093,7 +3945,6 @@ namespace
S.Cache = FChunkSDFCache();
S.SharedCacheEntry.Reset();
S.ActiveCache = &S.Cache;
S.SupportColumns.Invalidate();
InOut.Sdf = FLT_MAX;
return;
}
@@ -4102,7 +3953,6 @@ namespace
{
S.SharedCacheEntry.Reset();
S.ActiveCache = &S.Cache;
S.SupportColumns.Invalidate();
return; // Sdf reste FLT_MAX
}
@@ -4258,8 +4108,6 @@ namespace
+ (bUseTileCacheWindow ? static_cast<float>(SampleStep) : 0.0f)
+ 2.0f;
const FChunkSDFCache* PreviousActiveCache = S.ActiveCache;
bool bRebuiltLocalCache = false;
if (SampleStep > 1)
{
const bool bSharedCacheMatches =
@@ -4308,7 +4156,6 @@ namespace
&& LocalCache->RegionSize == RegionSize;
if (!bCacheMatches)
{
bRebuiltLocalCache = true;
LocalCache->bValid = false;
LocalCache->Seed = SeedU;
LocalCache->StrateIndex = StrateIdx;
@@ -4332,15 +4179,11 @@ namespace
}
S.ActiveCache = &LocalCache->Cache;
}
if (bRebuiltLocalCache || S.ActiveCache != PreviousActiveCache)
{
S.SupportColumns.Invalidate();
}
const bool bUseSpatialIndex = VoxelGenLOD::ShouldUseSpatialIndex(false);
const FVector WorldTunnelPosition(WorldX, WorldY, WorldZ);
float CaveSDF = VoxelCaveMorphology::EvaluateSDFCached(
WarpedX, WarpedY, WarpedZ, GetCache(), P.SDFBlendRadius,
&S.NearestRoom, bUseSpatialIndex);
&S.NearestRoom, bUseSpatialIndex, &WorldTunnelPosition);
//---------------------------------------------------------------
// PITS & CHEMINÉES — coordonnées RÉELLES, SmoothMin dans le même canal SDF
@@ -4424,45 +4267,31 @@ namespace
InOut.Sdf = CaveSDF;
// The swept floor is part of the cached tunnel shape, but the common structural tail
// used to discover the same band only after all detail ops had run. Publish that
// build-time ownership now so the stack can preserve the authored support through its
// remaining modifiers without a second floor-writing pass. TryGet... below reuses the
// exact same cached result for the generator's post-disturbance hand-off.
// Publish the same continuous tunnel-core field that the common generator tail will
// reassert after generic writers. The stack does not publish or protect a separate
// support slab.
if (!VoxelDensityAblation::IsTunnelCoreOff())
{
const FTunnelSupportFloorColumn* SupportColumn = GetSupportColumn(
S.LastWorldPosition);
S.LastTunnelCoreWorldEvaluation = VoxelCaveMorphology::EvaluateTunnelCoreWorld(
WorldX, WorldY, WorldZ, GetCache(), SupportColumn,
WorldX, WorldY, WorldZ, GetCache(), nullptr,
bUseSpatialIndex);
S.LastTunnelCoreEvaluationPosition = S.LastWorldPosition;
S.bLastTunnelCoreWorldEvaluationValid = true;
if (S.LastTunnelCoreWorldEvaluation.bSupportFloor
|| S.LastTunnelCoreWorldEvaluation.bRoomFloor)
{
const float StructuralFloorMinimumDensity =
FMath::Max(P.BaseDensity * 2.0f, 1.0f);
if (VoxelMath::IsFinite(StructuralFloorMinimumDensity)
&& StructuralFloorMinimumDensity > 0.0f)
{
InOut.bProtectedStructuralFloor = true;
InOut.StructuralFloorMinimumDensity = StructuralFloorMinimumDensity;
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelAuthoredFloorSamples);
}
}
}
// These are the per-sample hand-offs needed by an op-major block. The scalar path
// continues to use FState directly; publishing them here only makes the dependency
// explicit for a later modifier and does not alter Density/Sdf.
// Keep the hand-off for block evaluation compatibility. Both floor flags are
// permanently false; the authored floor is part of the tunnel shape itself.
InOut.bHasTunnelCoreWorldEvaluation = true;
InOut.TunnelCoreWorldSDF = S.LastTunnelCoreWorldEvaluation.SDF;
InOut.bTunnelCoreSupportFloor = S.LastTunnelCoreWorldEvaluation.bSupportFloor;
InOut.bTunnelCoreRoomFloor = S.LastTunnelCoreWorldEvaluation.bRoomFloor;
InOut.bTunnelCoreSupportFloor = false;
InOut.bTunnelCoreRoomFloor = false;
InOut.bHasTunnelCoreSweptFloor =
S.LastTunnelCoreWorldEvaluation.bHasSweptFloor;
InOut.TunnelCoreSweptFloorZ =
S.LastTunnelCoreWorldEvaluation.SweptFloorZ;
InOut.TunnelCoreSweptFloorRadius =
S.LastTunnelCoreWorldEvaluation.SweptFloorRadius;
InOut.bProtectedStructuralFloor = false;
InOut.StructuralFloorMinimumDensity = 0.0f;
}
// These cache hand-offs remain valid when tunnel core is off; detail operators still
@@ -6384,8 +6213,8 @@ namespace
// A post-air sample must lie within a tapered capsule's maximum radius of one
// world-chain segment. The segment AABB expanded by that radius is a cheap
// superset of the exact capsule; unlike the old enclosing sphere it rejects a
// long, thin tunnel from most unrelated LOD0 boxes. Support uses its own
// projected-floor AABB, because AllAir is killed by the finite floor band.
// long, thin tunnel from most unrelated LOD0 boxes. The floor is part of this
// same continuous field, so no second support-band candidate is needed.
if (Ctx.bUseLatticeProof)
{
const float WorldLatticeOriginX =
@@ -6441,53 +6270,8 @@ namespace
Result |= 1u;
}
if (bWorldChain)
{
const float NaturalFloorA = VoxelPassageGeometry::TunnelFloorZ(
A, SafeRadiusA);
const float NaturalFloorB = VoxelPassageGeometry::TunnelFloorZ(
BPoint, SafeRadiusB);
float FloorMin = FMath::Min(NaturalFloorA, NaturalFloorB);
float FloorMax = FMath::Max(NaturalFloorA, NaturalFloorB);
const TArray<FTunnelFloorSegmentProfile>* FloorProfiles =
&Tunnel.WorldFloorProfiles;
if (FloorProfiles->Num() == Tunnel.WorldControlPoints.Num() - 1
&& SegmentIndex >= 0
&& SegmentIndex < FloorProfiles->Num())
{
// The interval proof must include an explicit ledge's vertical
// riser. The profile transition is evaluated at the segment
// boundary, so its endpoint interval is a conservative superset
// of both plateau sides and remains valid for the lattice proof.
const FTunnelFloorSegmentProfile& Profile =
(*FloorProfiles)[SegmentIndex];
FloorMin = FMath::Min(FloorMin, FMath::Min(
Profile.StartFloorZ, Profile.EndFloorZ));
FloorMax = FMath::Max(FloorMax, FMath::Max(
Profile.StartFloorZ, Profile.EndFloorZ));
}
const float SupportRadius = FMath::Max(
FMath::Min(SafeRadiusA, SafeRadiusB) - 0.5f,
VoxelPassageGeometry::PlayerRadiusVoxels);
if (!Finite(FloorMin) || !Finite(FloorMax)
|| !Finite(SupportRadius))
{
Result = 3u;
return;
}
if (TouchesLatticeAABB(
FMath::Min(A.X, BPoint.X) - SupportRadius,
FMath::Max(A.X, BPoint.X) + SupportRadius,
FMath::Min(A.Y, BPoint.Y) - SupportRadius,
FMath::Max(A.Y, BPoint.Y) + SupportRadius,
FloorMin
- VoxelPassageGeometry::LandingFloorThicknessVoxels,
FloorMax
+ VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels))
{
Result |= 2u;
}
}
(void)bWorldChain;
(void)SegmentIndex;
};
if (bHasWorldChain)
@@ -6607,8 +6391,6 @@ namespace
const uint8 TailMask = MayHaveTunnelCoreTail(Tunnel);
B.bMayHaveTunnelCoreAir = B.bMayHaveTunnelCoreAir
|| (TailMask & 1u) != 0;
B.bMayHaveTunnelSupportFloor = B.bMayHaveTunnelSupportFloor
|| (TailMask & 2u) != 0;
B.bMayHaveTunnelCoreTail = B.bMayHaveTunnelCoreTail
|| TailMask != 0u;
float MaxRadius = FMath::Max(
@@ -6750,18 +6532,15 @@ namespace
CountThreshold(TunnelLowerNoWarp, B.HitTunnelsNoWarp);
}
if ((B.bMayHaveTunnelCoreAir || B.bMayHaveTunnelSupportFloor)
if (B.bMayHaveTunnelCoreAir
&& bUseLatticeProof
&& ExactLatticeWorldQueries.Num() > 0)
{
// The bound test above is only a broad candidate. The actual post is a
// discrete writer: it opens a point when the world-core SDF is below the inset
// without a support floor, or makes it solid in the support-floor band. Keep the
// two polarities separate: an AllSolid box only needs air, while an AllAir box
// only needs support. This is still a proof of the final lattice, never a density
// shortcut; if an exact query is invalid, keep both conservative candidates.
// continuous writer: it opens a point when the world-core SDF is negative. This
// is still a proof of the final lattice, never a density shortcut; if an exact
// query is invalid, keep the conservative candidate.
bool bExactAir = false;
bool bExactSupport = false;
bool bExactTailUnknown = false;
TArray<FBox, TInlineAllocator<16>> ExactTailDomains;
bool bExactTailDomainCullSafe = true;
@@ -6799,7 +6578,7 @@ namespace
VoxelBox.Min, VoxelBox.Max,
[&](const FVector&, const FVector& World)
{
if ((bExactAir && bExactSupport) || bExactTailUnknown) return;
if (bExactAir || bExactTailUnknown) return;
++B.ExactTailQueries;
if (bExactTailDomainCullSafe && ExactTailDomains.Num() > 0)
{
@@ -6853,33 +6632,15 @@ namespace
}
else
{
bExactSupport |= (State & 0x02u) != 0;
bExactAir |= (State & 0x01u) != 0;
}
return;
}
FTunnelSupportFloorColumn EmptySupportColumn;
const FTunnelSupportFloorColumn* SupportColumn = nullptr;
const bool bIntegerXY =
World.X == FMath::FloorToFloat(static_cast<float>(World.X))
&& World.Y == FMath::FloorToFloat(static_cast<float>(World.Y));
if (bIntegerXY)
{
SupportColumn = GetSupportColumn(World);
}
else
{
VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
static_cast<float>(World.X), static_cast<float>(World.Y),
GetBoxCache(), EmptySupportColumn,
VoxelGenLOD::ShouldUseSpatialIndex(false));
SupportColumn = &EmptySupportColumn;
}
const FTunnelCoreWorldEvaluation Evaluation =
VoxelCaveMorphology::EvaluateTunnelCoreWorld(
static_cast<float>(World.X), static_cast<float>(World.Y),
static_cast<float>(World.Z), GetBoxCache(), SupportColumn,
static_cast<float>(World.Z), GetBoxCache(), nullptr,
VoxelGenLOD::ShouldUseSpatialIndex(false));
++B.ExactTailEvaluated;
if (!VoxelMath::IsFinite(Evaluation.SDF))
@@ -6889,15 +6650,12 @@ namespace
}
else
{
const bool bAir = !Evaluation.bSupportFloor
&& Evaluation.SDF
< -VoxelPassageGeometry::CaveTunnelAirCoreInsetVoxels;
bExactSupport |= Evaluation.bSupportFloor;
const bool bAir = Evaluation.SDF < 0.0f;
bExactAir |= bAir;
if (CachedTailState != nullptr)
{
*CachedTailState = static_cast<uint8>(
0x80u | (Evaluation.bSupportFloor ? 0x02u : 0u)
0x80u
| (bAir ? 0x01u : 0u));
}
}
@@ -6906,8 +6664,8 @@ namespace
if (!bExactTailUnknown)
{
B.bMayHaveTunnelCoreAir = bExactAir;
B.bMayHaveTunnelSupportFloor = bExactSupport;
B.bMayHaveTunnelCoreTail = bExactAir || bExactSupport;
B.bMayHaveTunnelSupportFloor = false;
B.bMayHaveTunnelCoreTail = bExactAir;
}
}
+68 -222
View File
@@ -1021,8 +1021,7 @@ static FORCEINLINE void ApplyOriginLandingFloor(float& Density,
// it works uniformly for every generator type. Stays inside the seal bands so it can
// never breach a strate boundary. All features are hash-placed and deterministic.
static void ApplyDisturbances(float& MC, float X, float Y, float Z,
const FStrateDisturbanceParams& D, uint32 Seed, bool bProtectVerticalShaftAir,
bool bProtectAuthoredTunnelFloor)
const FStrateDisturbanceParams& D, uint32 Seed, bool bProtectVerticalShaftAir)
{
if (VoxelDensityAblation::IsDisturbancesOff())
{
@@ -1034,20 +1033,6 @@ static void ApplyDisturbances(float& MC, float X, float Y, float Z,
const float InnerBot = D.StrateBottomWorldZ + D.BoundarySealThickness;
if (Z <= InnerBot || Z >= InnerTop) return;
// A tunnel floor is now authored into the cached shape with its clearance budget. The generic
// disturbance layer is deliberately outside that shape, so it must not reopen or bridge the
// finite support band after the shape has proved it. This is an ownership hand-off, not a
// per-sample natural-floor validation.
if (bProtectAuthoredTunnelFloor)
{
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelAuthoredFloorDisturbanceSkips);
}
return;
}
const float Solid = D.BaseDensity * 2.0f;
const float Blend = 3.0f;
const FVector P(X, Y, Z);
@@ -2301,7 +2286,6 @@ namespace
uint64 ManagerLifetimeId = 0;
FIntVector Chunk = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX);
FIntVector TileOrigin = FIntVector::ZeroValue;
FIntVector SupportChunk = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX);
int32 TileStep = 0;
int32 TileCells = 0;
int32 StrateIndex = INT32_MIN;
@@ -2314,17 +2298,6 @@ namespace
static thread_local FTunnelCoreCacheState GTunnelCoreCache;
static void BuildTunnelSupportColumnsForChunk(
FChunkSDFCache& Cache,
const FIntVector& ChunkCoord,
bool bUseSpatialIndex);
static const FTunnelSupportFloorColumn* FindTunnelSupportColumn(
const FChunkSDFCache& Cache,
int32 X,
int32 Y,
const FTunnelSupportFloorColumn& EmptyColumn);
// A mesher tile samples an expanded grid (-1..Cells+1), so one worker visits several exact
// chunk coordinates even while it is generating one tile. The old CP_* cache retained only
// the last coordinate and consequently rebuilt the whole TunnelNetwork stack whenever the
@@ -2507,15 +2480,6 @@ namespace
&& OutState.TileCells == RequestedTileCells));
if (bSameGraphWindow)
{
// The graph is shared by the whole tile, but the sparse support table is deliberately
// kept for the currently queried exact chunk. Rebuild only that small table when the
// density cursor crosses a chunk; the room/tunnel graph itself is not rebuilt.
if (OutState.SupportChunk != ChunkCoord)
{
BuildTunnelSupportColumnsForChunk(
OutState.Cache, ChunkCoord, bUseSpatialIndex);
OutState.SupportChunk = ChunkCoord;
}
OutState.Chunk = ChunkCoord;
return;
}
@@ -2552,8 +2516,6 @@ namespace
StrateIndex, nullptr,
ERoomGraphBuildSite::GeneratorTunnelCore);
UpdateTunnelCoreTileReach(OutState.Cache, bUseTileCacheWindow);
BuildTunnelSupportColumnsForChunk(
OutState.Cache, ChunkCoord, bUseSpatialIndex);
OutState.OwnerId = OwnerId;
OutState.ManagerLifetimeId = ManagerLifetimeId;
OutState.Chunk = ChunkCoord;
@@ -2561,7 +2523,6 @@ namespace
? RequestedTileOrigin : FIntVector::ZeroValue;
OutState.TileStep = bUseTileCacheWindow ? RequestedTileStep : 0;
OutState.TileCells = bUseTileCacheWindow ? RequestedTileCells : 0;
OutState.SupportChunk = ChunkCoord;
OutState.StrateIndex = StrateIndex;
OutState.WorldSeed = WorldSeed;
OutState.ParamsFingerprint = ParamsFingerprint;
@@ -2570,69 +2531,6 @@ namespace
OutState.bValid = true;
}
static void BuildTunnelSupportColumnsForChunk(
FChunkSDFCache& Cache,
const FIntVector& ChunkCoord,
bool bUseSpatialIndex)
{
const int32 MinX = ChunkCoord.X * CHUNK_SIZE - 1;
const int32 MinY = ChunkCoord.Y * CHUNK_SIZE - 1;
const int32 Cells = CHUNK_SIZE + 2;
Cache.SupportColumnMinX = MinX;
Cache.SupportColumnMinY = MinY;
Cache.SupportColumnCellsX = Cells;
Cache.SupportColumnCellsY = Cells;
Cache.SupportColumnEntries.Init(INDEX_NONE, Cells * Cells);
Cache.SupportColumns.Reset();
// Empty columns are represented only by INDEX_NONE in the slot map, so
// each stored column retains the inline bands for actual graph tubes.
for (int32 Y = 0; Y < Cells; ++Y)
{
for (int32 X = 0; X < Cells; ++X)
{
FTunnelSupportFloorColumn Column;
VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
static_cast<float>(MinX + X),
static_cast<float>(MinY + Y),
Cache,
Column,
bUseSpatialIndex);
if (Column.Intervals.Num() == 0)
{
continue;
}
const int32 EntryIndex = Cache.SupportColumns.Add(MoveTemp(Column));
Cache.SupportColumnEntries[Y * Cells + X] = EntryIndex;
}
}
}
static const FTunnelSupportFloorColumn* FindTunnelSupportColumn(
const FChunkSDFCache& Cache,
int32 X,
int32 Y,
const FTunnelSupportFloorColumn& EmptyColumn)
{
if (Cache.SupportColumnCellsX <= 0
|| X < Cache.SupportColumnMinX
|| X >= Cache.SupportColumnMinX + Cache.SupportColumnCellsX
|| Y < Cache.SupportColumnMinY
|| Y >= Cache.SupportColumnMinY + Cache.SupportColumnCellsY)
{
return nullptr;
}
const int32 Slot =
(Y - Cache.SupportColumnMinY) * Cache.SupportColumnCellsX
+ (X - Cache.SupportColumnMinX);
const int32 EntryIndex = Cache.SupportColumnEntries[Slot];
return EntryIndex == INDEX_NONE
? &EmptyColumn
: &Cache.SupportColumns[EntryIndex];
}
struct FVoxelStackParamRefs
{
const FSlabGenerationParams* Slab = nullptr;
@@ -2842,7 +2740,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
VoxelDensityProfile::EBucket::DensityPrologue);
// ── STRATE SYSTEM ──
// Query per-chunk params from the manager so each strate has different caves.
float Result;
float Result = 0.0f;
// A vertical tree connector may need to stay open through the shared disturbance and
// structural-post tail below. This is one bit for this density call, not cached voxel data.
@@ -3391,6 +3289,9 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
float BlockTunnelCoreSDF = FLT_MAX;
bool bBlockTunnelCoreSupportFloor = false;
bool bBlockTunnelCoreRoomFloor = false;
bool bBlockHasTunnelCoreSweptFloor = false;
float BlockTunnelCoreSweptFloorZ = -FLT_MAX;
float BlockTunnelCoreSweptFloorRadius = 0.0f;
bool bUsedFusedEvaluator = false;
FTunnelCoreWorldEvaluation FusedTunnelCore;
@@ -3457,6 +3358,12 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
BlockTunnelCoreSDF = BlockCoreSample.TunnelCoreWorldSDF;
bBlockTunnelCoreSupportFloor = BlockCoreSample.bTunnelCoreSupportFloor;
bBlockTunnelCoreRoomFloor = BlockCoreSample.bTunnelCoreRoomFloor;
bBlockHasTunnelCoreSweptFloor =
BlockCoreSample.bHasTunnelCoreSweptFloor;
BlockTunnelCoreSweptFloorZ =
BlockCoreSample.TunnelCoreSweptFloorZ;
BlockTunnelCoreSweptFloorRadius =
BlockCoreSample.TunnelCoreSweptFloorRadius;
}
}
if (!bUsedFusedEvaluator)
@@ -3550,6 +3457,11 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
PreDisturbanceTunnelCore.SDF = BlockTunnelCoreSDF;
PreDisturbanceTunnelCore.bSupportFloor = bBlockTunnelCoreSupportFloor;
PreDisturbanceTunnelCore.bRoomFloor = bBlockTunnelCoreRoomFloor;
PreDisturbanceTunnelCore.bHasSweptFloor =
bBlockHasTunnelCoreSweptFloor;
PreDisturbanceTunnelCore.SweptFloorZ = BlockTunnelCoreSweptFloorZ;
PreDisturbanceTunnelCore.SweptFloorRadius =
BlockTunnelCoreSweptFloorRadius;
bHavePreDisturbanceTunnelCore = true;
}
else
@@ -3561,30 +3473,14 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
if (!VoxelDensityAblation::IsTunnelCoreOff()
&& !bHavePreDisturbanceTunnelCore && ActiveTunnelCoreCache->bValid)
{
const FTunnelSupportFloorColumn* SupportColumn = nullptr;
FTunnelSupportFloorColumn EmptySupportColumn;
const bool bIntegerXY =
WorldX == FMath::FloorToFloat(WorldX)
&& WorldY == FMath::FloorToFloat(WorldY);
if (bIntegerXY)
{
const int32 IX = FMath::FloorToInt(WorldX);
const int32 IY = FMath::FloorToInt(WorldY);
SupportColumn = FindTunnelSupportColumn(
ActiveTunnelCoreCache->Cache, IX, IY, EmptySupportColumn);
}
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::TunnelCorePosts);
PreDisturbanceTunnelCore = VoxelCaveMorphology::EvaluateTunnelCoreWorld(
WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache, SupportColumn,
WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache, nullptr,
VoxelGenLOD::ShouldUseSpatialIndex(
CP_UseOpStack ? bUsedFusedEvaluator : true));
bHavePreDisturbanceTunnelCore = true;
}
const bool bProtectAuthoredTunnelFloor = bHavePreDisturbanceTunnelCore
&& (PreDisturbanceTunnelCore.bSupportFloor
|| PreDisturbanceTunnelCore.bRoomFloor);
// Disturbance layer (the "wow" post-process) — cached params, MC convention. The
// vertical shaft tree opens a walkable route before this shared pass, so a bridge/ridge
@@ -3594,7 +3490,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
const bool bProtectVerticalShaftAir =
VoxelPassageGeometry::VerticalShaftConnectorAirMarker();
ApplyDisturbances(Result, WorldX, WorldY, WorldZ, CP_Dist, (uint32)Seed,
bProtectVerticalShaftAir, bProtectAuthoredTunnelFloor);
bProtectVerticalShaftAir);
DensityDisturbancesTimer.End();
// A disturbance is allowed to add visual detail, but it must not refill the landing's
@@ -3670,6 +3566,27 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
break;
}
}
auto ApplyTunnelCoreShape = [&](const FTunnelCoreWorldEvaluation& Core)
{
if (!VoxelMath::IsFinite(Core.SDF))
{
return;
}
const bool bOwnsSweptBottom =
Core.bHasSweptFloor
&& VoxelMath::IsFinite(Core.SweptFloorZ)
&& VoxelMath::IsFinite(Core.SweptFloorRadius)
&& WorldZ <= Core.SweptFloorZ
+ VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels;
if (bOwnsSweptBottom)
{
Result = FMath::Min(Result, -Core.SDF);
}
else if (Core.SDF < 0.0f)
{
Result = FMath::Max(Result, -Core.SDF);
}
};
// Origin rooms are structural too. Reassert their air before either support writer so a
// bridge/ridge cannot plug a landing, while the two floor writers remain last.
{
@@ -3693,7 +3610,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
VoxelDensityProfile::EBucket::PassageStructuralPosts);
StrateManager->ApplyPassageStructuralPostsMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity,
CP_Dist.BoundarySealThickness, bProtectAuthoredTunnelFloor);
CP_Dist.BoundarySealThickness);
}
// Disturbance features are authored as a generic MC-space post and may add a ridge or
// bridge over a graph tunnel. Reassert the native cached tunnel core here, after every
@@ -3705,8 +3622,10 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
const bool bTunnelCoreBypass = VoxelGenLOD::IsTilePostReachBypassActive();
auto ApplyGraphLandingFloors = [&]()
{
// These are passage/origin landing posts, not tunnel-core writes. Keep them alive when
// the core bit is clear because their own per-tile reach bits may still be set.
// These are passage/origin landing posts, not tunnel-core writes. Their air and
// structural post already ran before disturbances; the tunnel shape below reopens
// its own continuous interior after the disturbance pass. Keep only the floor
// reassertions here, in the existing order.
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::PassageLandingFloor);
@@ -3733,7 +3652,9 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
{
FTunnelCoreWorldEvaluation TunnelCore;
bool bHaveTunnelCore = bHavePreDisturbanceTunnelCore;
if (!bTunnelCoreReachable && !bTunnelCoreBypass)
if (VoxelGenLOD::bTileCoreReachProofEnabled
&& VoxelGenLOD::bTileReachDiagnosticTile
&& !bTunnelCoreReachable && !bTunnelCoreBypass)
{
// The fused/source hand-off is also empty on the skipped path. Do not let the
// default object turn this into a second tail evaluation.
@@ -3746,83 +3667,26 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
if (!bHaveTunnelCore && ActiveTunnelCoreCache->bValid
&& (bTunnelCoreReachable || bTunnelCoreBypass))
{
const FTunnelSupportFloorColumn* SupportColumn = nullptr;
FTunnelSupportFloorColumn EmptySupportColumn;
const bool bIntegerXY =
WorldX == FMath::FloorToFloat(WorldX)
&& WorldY == FMath::FloorToFloat(WorldY);
if (bIntegerXY)
{
const int32 IX = FMath::FloorToInt(WorldX);
const int32 IY = FMath::FloorToInt(WorldY);
SupportColumn = FindTunnelSupportColumn(
ActiveTunnelCoreCache->Cache, IX, IY, EmptySupportColumn);
}
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::TunnelCorePosts);
TunnelCore = VoxelCaveMorphology::EvaluateTunnelCoreWorld(
WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache,
SupportColumn,
nullptr,
VoxelGenLOD::ShouldUseSpatialIndex(
CP_UseOpStack ? bUsedFusedEvaluator : true));
bHaveTunnelCore = true;
}
if (bHaveTunnelCore)
{
const bool bTunnelSupportFloor = TunnelCore.bSupportFloor;
// The graph floor is now composed here, after disturbances and passage writers.
// The source has already baked the floor profile and published the exact same
// core result; this final ownership step only changes an actually-air sample.
// Previously the operator stack raised every authored floor sample to a strong
// internal density before those writers ran, which made the protection counter
// fire for solid samples as well as genuine breaches.
if (TunnelCore.bRoomFloor)
{
const float StructuralSolidDensity =
-FMath::Max(LandingBaseDensity * 2.0f, 1.0f);
if (VoxelDensityProfile::AreCountersEnabled())
{
if (Result > 0.0f)
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelRoomFloorBackstopFires);
}
}
// A tunnel that penetrates a room cannot reassert its own floor through the
// room's floor. The morphology query marks only the finite room support band;
// preserve it after every generic post-disturbance writer.
Result = FMath::Min(
Result,
StructuralSolidDensity);
}
// The swept capsule owns the relief profile. This finite support band is now the
// final composition of that authored shape, after every writer, rather than a
// pre-disturbance clamp in FVoxelOpStack::EvalSample.
if (bTunnelSupportFloor)
{
const float StructuralSolidDensity =
-FMath::Max(LandingBaseDensity * 2.0f, 1.0f);
if (VoxelDensityProfile::AreCountersEnabled())
{
if (Result > 0.0f)
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelSupportFloorBackstopFires);
}
}
Result = FMath::Min(
Result,
StructuralSolidDensity);
}
const float CoreSDF = TunnelCore.SDF;
if (!bTunnelSupportFloor
&& CoreSDF < -VoxelPassageGeometry::CaveTunnelAirCoreInsetVoxels)
{
Result = FMath::Max(
Result,
FMath::Max(LandingBaseDensity * 2.0f, 1.0f));
}
// Reassert the actual swept-core field after generic writers. The old tail used a
// fixed positive air value once the core was deeper than an inset; that hard
// switch discarded the SDF magnitude and made marching-cubes intersections snap
// to the sample lattice. The source now contains the same continuous world-chain
// tunnel field. Union its interior air back over disturbances, and use the same
// signed field only on the bottom side identified by the swept shape itself. This
// restores the tunnel's carved floor when the surrounding room is already air,
// without adding an authored slab or turning the whole room into tunnel geometry.
ApplyTunnelCoreShape(TunnelCore);
}
if (bHaveTunnelCore || (!bTunnelCoreReachable && !bTunnelCoreBypass))
@@ -4292,11 +4156,13 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
// Also writes NearestRoomIdx — the room with minimum SDF contribution
// at this voxel's position. Used by terrain ops below to pick per-room params.
NearestRoomIdx = -1;
const FVector WorldTunnelPosition(WorldX, WorldY, WorldZ);
CaveSDF = VoxelCaveMorphology::EvaluateSDFCached(
WarpedX, WarpedY, WarpedZ,
SDFCache, Params.SDFBlendRadius,
&NearestRoomIdx,
bUseSpatialIndex
bUseSpatialIndex,
&WorldTunnelPosition
);
// ── PIT & CHIMNEY SDF INTEGRATION ──
@@ -4640,12 +4506,14 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
//
// We use the cached SDF so this costs two extra SDF evaluations per voxel,
// only when near a surface — the common case is cheap (DistFromSurface > TerraceRange).
const FVector WorldTunnelPositionZp1(WorldX, WorldY, WorldZ + 1.0f);
const FVector WorldTunnelPositionZm1(WorldX, WorldY, WorldZ - 1.0f);
float SDF_Zp1 = VoxelCaveMorphology::EvaluateSDFCached(
WorldX, WorldY, WorldZ + 1.0f, SDFCache, Params.SDFBlendRadius,
nullptr, bUseSpatialIndex);
nullptr, bUseSpatialIndex, &WorldTunnelPositionZp1);
float SDF_Zm1 = VoxelCaveMorphology::EvaluateSDFCached(
WorldX, WorldY, WorldZ - 1.0f, SDFCache, Params.SDFBlendRadius,
nullptr, bUseSpatialIndex);
nullptr, bUseSpatialIndex, &WorldTunnelPositionZm1);
float GZ = (SDF_Zp1 - SDF_Zm1) * 0.5f;
// Normalized vertical component: 1 = perfectly horizontal surface (floor/ceiling)
// 0 = perfectly vertical surface (wall)
@@ -5208,41 +5076,19 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
if (bApplyLegacyStructuralPosts)
{
// Terrain operations and passage support floors are allowed to write solid density, but
// the graph's own tunnel floor/core are structural route geometry. Direct callers of
// GetDensityWithParams retain this historical tail; GetDensityAt skips it and applies the
// same predicates once in its common MC-space post section below both generation paths.
// Direct callers retain the legacy structural tail. The tunnel core itself is still
// composed here as a continuous field; it is not converted into a fixed-density slab.
const bool bTunnelCoreEnabled = !VoxelDensityAblation::IsTunnelCoreOff();
const bool bTunnelSupportFloor = bTunnelCoreEnabled
&& VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
WorldX, WorldY, WorldZ, SDFCache, bUseSpatialIndex);
// Keep the direct legacy entry point capability-safe for callers outside the canonical
// operator-stack path; its support band follows the same relieved swept floor.
if (bTunnelSupportFloor)
{
const float StructuralSolidDensity =
FMath::Max(Params.BaseDensity * 2.0f, 1.0f);
if (VoxelDensityProfile::AreCountersEnabled())
{
if (Density < 0.0f)
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelSupportFloorBackstopFires);
}
}
Density = FMath::Max(Density, StructuralSolidDensity);
}
const float TunnelCoreSDF = bTunnelCoreEnabled
&& (Params.RoomDensity > 0.0f && Params.RoomSpacing > 0.0f)
? VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF(
WorldX, WorldY, WorldZ, SDFCache, bUseSpatialIndex)
: FLT_MAX;
if (!bTunnelSupportFloor
&& TunnelCoreSDF < -VoxelPassageGeometry::CaveTunnelAirCoreInsetVoxels)
if (VoxelMath::IsFinite(TunnelCoreSDF) && TunnelCoreSDF < 0.0f)
{
Density = FMath::Min(
Density,
-FMath::Max(Params.BaseDensity * 2.0f, 1.0f));
TunnelCoreSDF);
}
ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ,
Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
@@ -1371,13 +1371,6 @@ namespace
Params.bTunnelsFlowTowardOrigin = CategoryRng.FRand() < 0.5f;
return true;
}
if (FCString::Strcmp(FieldName, TEXT("bTunnelFloorEnabled")) == 0)
{
// The floor is a capability feature, not a corpus-free variation axis. Keep it on
// for every generated candidate so a roll cannot manufacture an invalid bare tube.
Params.bTunnelFloorEnabled = true;
return true;
}
if (FCString::Strcmp(FieldName, TEXT("bTunnelFloorTerracingEnabled")) == 0)
{
Params.bTunnelFloorTerracingEnabled = CategoryRng.FRand() < 0.75f;
@@ -62,6 +62,11 @@ void UVoxelStrateDefinition::PostLoad()
{
Super::PostLoad();
// The field remains reflected solely so old packages deserialize without shifting the
// following members. Tunnel geometry owns the floor now, so every serialized legacy value
// is normalized to the ignored compatibility default before any runtime copy is made.
GenerationParams.bTunnelFloorEnabled = true;
if (GetLinkerCustomVersion(GVoxelForgeStrateSerializationVersion)
>= GVoxelForgeStrateGentleSlopeDegreesVersion)
{
@@ -2967,8 +2967,7 @@ static bool VF_IsPassageRoomFloor(
void UVoxelStrateManager::ApplyPassageStructuralPostsMC(
float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness,
bool bProtectAuthoredTunnelFloor) const
float BaseDensity, float SealThickness) const
{
const bool bDisableStructuralPosts = VoxelDensityAblation::IsPassageStructuralPostsOff();
const bool bDisableLandingPosts = VoxelDensityAblation::IsLandingPostsOff();
@@ -2989,7 +2988,7 @@ void UVoxelStrateManager::ApplyPassageStructuralPostsMC(
VoxelGenLOD::bTilePostReachBypass = true;
ApplyPassageStructuralPostsMC(
CanonicalDensity, WorldX, WorldY, WorldZ,
BaseDensity, SealThickness, bProtectAuthoredTunnelFloor);
BaseDensity, SealThickness);
VoxelGenLOD::bTilePostReachBypass = PreviousBypass;
}
if (!bLandingReachable && !bStructuralReachable)
@@ -3076,11 +3075,9 @@ void UVoxelStrateManager::ApplyPassageStructuralPostsMC(
}
}
// A graph tunnel floor is authored by the room-graph source with its complete clearance
// budget. A neighbouring inter-strate landing must not reopen that finite support band; its
// carve remains valid everywhere else. Legacy callers leave this false and retain the old
// passage-owned behavior.
if (!bDisableLandingPosts && !bProtectAuthoredTunnelFloor
// The graph tunnel no longer exposes a separate support slab. Inter-strate landing carving
// remains independent and is still composed here when its own reach proof allows it.
if (!bDisableLandingPosts
&& !bAnyLandingFloor && !bLegacyTunnelSupportFloor && MinLandingSDF < FLT_MAX)
{
float InternalDensity = -Density;
@@ -3094,7 +3091,7 @@ void UVoxelStrateManager::ApplyPassageStructuralPostsMC(
Density = FMath::Min(Density, -BaseDensity);
}
if (bWalkableAir && !bProtectAuthoredTunnelFloor)
if (bWalkableAir)
{
Density = FMath::Max(
Density, BaseDensity * 2.0f + SealThickness + 4.0f);
+24 -25
View File
@@ -712,7 +712,6 @@ struct FCachedTunnel
float SDFMouthFloorZB = -FLT_MAX;
float WorldMouthFloorZA = -FLT_MAX;
float WorldMouthFloorZB = -FLT_MAX;
bool bTunnelFloorEnabled = true;
bool bTunnelFloorTerracingEnabled = true;
float TunnelFloorTerraceStepHeight = 1.71f;
float TunnelFloorMaxLedgeHeight = 12.0f;
@@ -915,17 +914,8 @@ struct FChunkSDFCache
FChunkSDFSpatialIndex ChimneySpatialIndex;
FChunkSDFSpatialIndex ColumnSpatialIndex;
// Sparse immutable support-column table. SupportColumnEntries is a dense
// integer-XY slot map whose values index only non-empty SupportColumns.
int32 SupportColumnMinX = 0;
int32 SupportColumnMinY = 0;
int32 SupportColumnCellsX = 0;
int32 SupportColumnCellsY = 0;
TArray<int32> SupportColumnEntries;
TArray<FTunnelSupportFloorColumn> SupportColumns;
// Kept with the cache because the world-space structural evaluator has no params argument.
// A zero value is a valid hard-intersection fallback for legacy/empty caches.
// A zero value is a valid fallback for legacy/empty caches.
float SDFBlendRadius = 0.0f;
// Allocator-backed bytes owned by this cache, including the nested tunnel chains. The returned
@@ -943,14 +933,20 @@ struct FChunkSDFCache
VoxelDensityProfile::FCacheMemoryBreakdown GetAllocatedSizeBreakdown() const;
};
/** Combined world-space tunnel post query; one cache scan supplies both support and air tests. */
/** World-space tunnel shape query. Legacy support-floor fields remain source-compatible but are
* no longer produced or consumed by the graph-tunnel generation path. */
struct FTunnelCoreWorldEvaluation
{
float SDF = FLT_MAX;
// Deprecated compatibility output. The separate graph-tunnel support slab was removed.
bool bSupportFloor = false;
// The room floor owns the overlap band. The common post uses this marker to restore
// the room's support after the raw tunnel-air backstop has run.
// Deprecated compatibility output. Room/landing floors are composed by their own posts.
bool bRoomFloor = false;
// Shape-level floor ownership. This is not an authored slab: it identifies the bottom side
// of the swept tunnel SDF so a room that is already air cannot erase the tunnel's own floor.
bool bHasSweptFloor = false;
float SweptFloorZ = -FLT_MAX;
float SweptFloorRadius = 0.0f;
};
//=============================================================================
@@ -1040,13 +1036,17 @@ namespace VoxelCaveMorphology
// SmoothMin. Includes per-room/tunnel distance culling to skip primitives
// that are too far to contribute.
//
// @param WorldX, WorldY, WorldZ — position in voxel coordinates (may be warped)
// @param WorldX, WorldY, WorldZ — room/query position in voxel coordinates (may be warped)
// @param Cache — pre-built cache from BuildChunkCache
// @param SDFBlendRadius — SmoothMin blend radius (from Params.SDFBlendRadius)
// @param OutNearestRoomIdx — optional out: index of the room with minimum SDF
// contribution. -1 if no room passed the cull test.
// Used by the terrain ops system to look up the
// per-room terrain op assigned to this voxel's room.
// @param WorldTunnelPosition — optional unwarped position for the tunnel primitive. When
// supplied, rooms remain at the query position but the tunnel
// uses its authored world chain. Null preserves the legacy
// all-query behavior.
// (Room shape variety is baked into FCachedRoom by BuildChunkCache — no per-voxel roll.)
// @return negative = inside cave, positive = solid rock
VOXELFORGE_API float EvaluateSDFCached(
@@ -1054,7 +1054,8 @@ namespace VoxelCaveMorphology
const FChunkSDFCache& Cache,
float SDFBlendRadius,
int32* OutNearestRoomIdx = nullptr,
bool bUseSpatialIndex = true
bool bUseSpatialIndex = true,
const FVector* WorldTunnelPosition = nullptr
);
// Evaluate the raw union of cached graph-tunnel capsules. This is intentionally separate from
@@ -1078,6 +1079,7 @@ namespace VoxelCaveMorphology
VOXELFORGE_API FTunnelCoreWorldEvaluation EvaluateTunnelCoreWorld(
float WorldX, float WorldY, float WorldZ,
const FChunkSDFCache& Cache,
// Deprecated legacy argument; graph-tunnel evaluation ignores the old support column.
const FTunnelSupportFloorColumn* SupportColumn = nullptr,
bool bUseSpatialIndex = true
);
@@ -1090,18 +1092,16 @@ namespace VoxelCaveMorphology
float ReachScale = 1.0f
);
// True for the finite support slab beneath a world-space graph tunnel. The native density
// path and the operator stack use this same predicate before reopening the air core, keeping
// the rounded capsule's bottom from becoming an unsupported point.
// Deprecated compatibility query for the removed graph-tunnel support slab. Production
// generation does not call this predicate; the tunnel's own swept shape owns its floor.
VOXELFORGE_API bool IsTunnelSupportFloorWorldPoint(
float WorldX, float WorldY, float WorldZ,
const FChunkSDFCache& Cache,
bool bUseSpatialIndex = true
);
// Build/query the same support-floor predicate at column granularity. The column stores its
// finite support interval, so every Z sample in a chunk reuses the same deterministic
// projection and never re-runs a natural-floor validator.
// Deprecated compatibility helper for callers that still inspect the old support-band
// diagnostic. It is detached from production density generation.
VOXELFORGE_API void BuildTunnelSupportFloorColumn(
float WorldX, float WorldY,
const FChunkSDFCache& Cache,
@@ -1114,9 +1114,8 @@ namespace VoxelCaveMorphology
const FTunnelSupportFloorColumn& Column
);
// Returns the exact projected floor band for one tunnel at this XY column.
// The world evaluator uses FloorZ for its per-tunnel air ownership rule and
// Min/Max for the support slab itself.
// Returns the legacy projected band for one tunnel at this XY column. The world evaluator no
// longer uses it to write a support slab.
VOXELFORGE_API bool GetTunnelSupportFloorColumnBand(
int32 TunnelIndex,
const FTunnelSupportFloorColumn& Column,
+8 -8
View File
@@ -337,10 +337,8 @@ struct FVoxelOpSample
// FLT_MAX = "no surface nearby" — the initial state, and the early-out placed sources use.
float Sdf = FLT_MAX;
// The room-graph source can author a finite tunnel/room floor band while it is building the
// shape. This structural metadata is not a third composable field: it tells the stack that
// later detail operators must not turn the already-authored support back into air. Carrying it
// with the sample keeps the rule true even when a detail op is unaware of the room graph.
// Deprecated structural-floor compatibility fields. Graph tunnels now carry their floor in
// the swept shape itself; the production stack does not use these fields to write a slab.
bool bProtectedStructuralFloor = false;
float StructuralFloorMinimumDensity = 0.0f;
@@ -350,14 +348,16 @@ struct FVoxelOpSample
const FChunkSDFCache* RoomCache = nullptr;
int32 NearestRoomIndex = INDEX_NONE;
// The common generator tail consumes these values after the block has left the stack. They
// are the small, exact subset of FTunnelCoreWorldEvaluation that the tail needs; keeping them
// in the sample avoids re-running the world-space tunnel scan or consulting a last-sample TLS
// value after an op-major evaluation.
// The common generator tail consumes the continuous swept-shape metadata after the block has
// left the stack. Keeping it in the sample avoids re-running the world-space tunnel scan or
// consulting a last-sample TLS value after an op-major evaluation.
bool bHasTunnelCoreWorldEvaluation = false;
float TunnelCoreWorldSDF = FLT_MAX;
bool bTunnelCoreSupportFloor = false;
bool bTunnelCoreRoomFloor = false;
bool bHasTunnelCoreSweptFloor = false;
float TunnelCoreSweptFloorZ = -FLT_MAX;
float TunnelCoreSweptFloorRadius = 0.0f;
};
/**
@@ -429,8 +429,7 @@ public:
/** Apply the landing/tunnel MC post in one cached passage scan after disturbances. */
void ApplyPassageStructuralPostsMC(float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness,
bool bProtectAuthoredTunnelFloor = false) const;
float BaseDensity, float SealThickness) const;
/**
* Compose the build-authored passage D-floor after generic MC writers. This is the native
+6 -7
View File
@@ -285,7 +285,6 @@ enum class EVoxelStrateTransition : uint8
LERPF(TunnelHorizontalBias) \
SNAPF(bTunnelsFlowTowardOrigin) \
LERPF(TunnelEndpointZOffset) \
SNAPF(bTunnelFloorEnabled) \
SNAPF(bTunnelFloorTerracingEnabled) \
LERPF(TunnelFloorTerraceStepHeight) \
LERPF(TunnelFloorMaxLedgeHeight) \
@@ -686,10 +685,10 @@ struct VOXELFORGE_API FStrateGenerationParams
// terraces walls after morphology, while these fields describe the tunnel's own floor profile.
// FloorReliefStrength/Frequency above are shared with room floors and are reused here.
// Whether the swept tunnel receives its authored floor cut. Disable only for an archetype that
// deliberately wants the bare capsule; the structural support backstop remains a measured
// fallback for malformed/legacy caches and is not a replacement for this shape.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels|Floor")
// Deprecated compatibility field. The tunnel's carved geometry always owns its floor;
// serialized values continue to load but are deliberately ignored.
UPROPERTY(meta = (DeprecatedProperty,
DeprecationMessage = "Ignored; tunnel floor is always authored by tunnel geometry."))
bool bTunnelFloorEnabled = true;
// Enables deterministic selection of a few dramatic ledges on steep graph-redundant or leaf
@@ -1137,10 +1136,10 @@ constexpr int32 VF_StrateParamListedFieldCount =
#undef VF_STRATE_PARAM_COUNT_FIELD
static_assert(
VF_StrateParamListedFieldCount == 83,
VF_StrateParamListedFieldCount == 82,
"FStrateGenerationParams field count changed; update the struct and VF_STRATE_PARAM_FIELDS together.");
static_assert(
sizeof(FStrateGenerationParams) == VF_StrateParamListedBytes + 8,
sizeof(FStrateGenerationParams) == VF_StrateParamListedBytes + 9,
"FStrateGenerationParams and VF_STRATE_PARAM_FIELDS disagree; update both places and recheck the expected padding.");
//=============================================================================
File diff suppressed because it is too large. Load diff