wip(tunnels): room wins over tunnels -- no shelf, smooth mouths; stopped before final checks

Stopped by the owner (he wanted his PC back to play) after most acceptance was in. Measured with the owner's assets: the shelf across rooms came from the tunnel core reasserting solid below its floor inside room volumes, plus the landing-floor post. Room ownership now suppresses the tunnel bottom in the generic tunnel SDF, the core evaluator and both native fallbacks: tunnel-attributed solid inside rooms 469 -> 0, and a matched-camera re-render shows the slab gone. Trimmed mouth endpoints now take their height from the room's own vertical zero crossing (join offsets -2.08/+0.40 -> -1.13/-0.71 voxels). Mouth surface periodic amplitude 1.49 cm floor / 0 cm wall (degree-six trend removal; raw local-median audit 6.19 cm kept). Canonical walk Connected, 42,870 fit / 17,413 walk-reachable (was 41,184 / 13,879); export 141,054 triangles, geometry hash 86A99075. Launches use an isolated engine saved/config directory so the owner's editor settings are not rewritten.

NOT DONE: the -voxel.CrtFma3=0 export comparison, TilePostReachProof / DetSinCos / strate crossing, the perf pair vs the experimental tip, the EditorSettings.ini diff. Unvalidated in game.

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 14:56:06 +02:00
1 parent 01cb9d47f1
commit d01af4606d
5 files changed
+821 -51

No files matched your search

+197 -17
View File
@@ -125,8 +125,8 @@ namespace
#if !UE_BUILD_SHIPPING #if !UE_BUILD_SHIPPING
static TAutoConsoleVariable<int32> CVarVoxelForgeTunnelMouthTrim( static TAutoConsoleVariable<int32> CVarVoxelForgeTunnelMouthTrim(
TEXT("voxel.TunnelMouthTrim"), TEXT("voxel.TunnelMouthTrim"),
0, 1,
TEXT("Development-only room-wall tunnel-mouth trim; 0 keeps the legacy room endpoint."), TEXT("Development-only room-wall tunnel-mouth trim; 1 makes the room own the mouth by default."),
ECVF_Default); ECVF_Default);
#endif #endif
@@ -672,16 +672,16 @@ namespace
const float BlendRadius = FMath::Max( const float BlendRadius = FMath::Max(
Endpoint == 0 ? MouthBlendRadiusA : MouthBlendRadiusB, Endpoint == 0 ? MouthBlendRadiusA : MouthBlendRadiusB,
0.0f); 0.0f);
const float EndpointRadius = FMath::Abs(ControlRadiusData[ControlIndex]);
const float BottomHandoffRadius = BlendRadius
+ VF_TunnelMouthPlayerFitOverlapVoxels
+ EndpointRadius;
const float RelativeDistance = Distance - BlendRadius; const float RelativeDistance = Distance - BlendRadius;
const float Weight = RelativeDistance <= 0.0f const float Weight = RelativeDistance <= 0.0f
? 1.0f ? 1.0f
: (RelativeDistance < Fade : (RelativeDistance < Fade
? 1.0f - SmoothStep01(RelativeDistance / Fade) ? 1.0f - SmoothStep01(RelativeDistance / Fade)
: 0.0f); : 0.0f);
const float EndpointRadius = FMath::Abs(ControlRadiusData[ControlIndex]);
const float BottomHandoffRadius = BlendRadius
+ VF_TunnelMouthPlayerFitOverlapVoxels
+ EndpointRadius;
const bool bInBottomHandoff = VoxelMath::IsFinite(BottomHandoffRadius) const bool bInBottomHandoff = VoxelMath::IsFinite(BottomHandoffRadius)
&& Distance <= BottomHandoffRadius; && Distance <= BottomHandoffRadius;
if (!(Weight > 0.0f) && !bInBottomHandoff) if (!(Weight > 0.0f) && !bInBottomHandoff)
@@ -1539,6 +1539,52 @@ namespace
FMath::Sqrt(FMath::Max(Room.CullRadiusSq, 0.0f)), OutBounds); FMath::Sqrt(FMath::Max(Room.CullRadiusSq, 0.0f)), OutBounds);
} }
static float VF_CachedRoomShapeSDF(
const FCachedRoom& Room, const FVector& Position)
{
switch (Room.ShapeType)
{
case 1:
return VoxelSDF::RoundedBox(
Position, Room.Center, Room.ShapeA, Room.ShapeR);
case 2:
return VoxelSDF::Capsule(
Position, Room.ShapeA, Room.ShapeB, Room.ShapeR);
default:
return VoxelSDF::Ellipsoid(Position, Room.Center, Room.ShapeA);
}
}
static bool VF_IsInsideCachedRoomShape(
const FVector& Position,
const FChunkSDFCache& Cache,
bool bUseSpatialIndex)
{
bool bInside = false;
auto VisitRoom = [&](int32 RoomIndex)
{
if (bInside || !Cache.Rooms.IsValidIndex(RoomIndex))
{
return;
}
const FCachedRoom& Room = Cache.Rooms[RoomIndex];
if (VF_FloatDistSquared(
static_cast<float>(Position.X),
static_cast<float>(Position.Y),
static_cast<float>(Position.Z),
Room.Center) > Room.CullRadiusSq)
{
return;
}
bInside = VF_CachedRoomShapeSDF(Room, Position) < 0.0f;
};
VF_ForEachSpatialCandidate(
Cache.RoomSpatialIndex, Cache.Rooms.Num(),
static_cast<float>(Position.X), static_cast<float>(Position.Y),
VisitRoom, bUseSpatialIndex);
return bInside;
}
static bool VF_GetJoinSpatialBounds( static bool VF_GetJoinSpatialBounds(
int32, const FCachedRoomFloorJoin& Join, FVFSpatialBounds& OutBounds) int32, const FCachedRoomFloorJoin& Join, FVFSpatialBounds& OutBounds)
{ {
@@ -3173,8 +3219,8 @@ namespace
} }
// The default warp is a contraction at its authored frequency. A fixed eight-step // The default warp is a contraction at its authored frequency. A fixed eight-step
// Newton/Picard correction is enough to put a world landing back on the exact cached SDF // correction is enough to put a world landing back on the cached SDF point while keeping
// point, while keeping the query allocation-free and deterministic. // the construction path allocation-free and deterministic.
for (int32 Iteration = 0; Iteration < 8; ++Iteration) for (int32 Iteration = 0; Iteration < 8; ++Iteration)
{ {
const FVector Mapped = VF_ApplyCaveWarp(WorldPoint, Params, Seed); const FVector Mapped = VF_ApplyCaveWarp(WorldPoint, Params, Seed);
@@ -3362,6 +3408,108 @@ namespace
return -InternalDensity; return -InternalDensity;
} }
static bool VF_FindRoomFloorCrossingWorld(
const FVFRoomLandingSite& Site,
const FStrateGenerationParams& Params,
uint32 Seed,
float StrateTopZ,
float StrateBottomZ,
const FVector& WorldMouthXY,
float& OutFloorZ)
{
OutFloorZ = -FLT_MAX;
if (!VoxelMath::IsFinite(WorldMouthXY.X)
|| !VoxelMath::IsFinite(WorldMouthXY.Y)
|| !VoxelMath::IsFinite(Site.Center.Z)
|| !VoxelMath::IsFinite(Site.RadiusZ))
{
return false;
}
// BuildRoomMouth receives a world-space point from the boundary search. Find the
// crossing in that same space instead of reusing the SDF-space floor estimate. This is
// deliberately a vertical room query: the room owns the floor height at the mouth, and
// the tunnel endpoint is then placed one tunnel radius above that exact crossing.
const float ReliefBound = FMath::Abs(Params.FloorReliefStrength)
* VOXEL_NOISE_SCALE * 1.5f;
const float MinZ = Site.Center.Z - FMath::Abs(Site.RadiusZ)
- ReliefBound - 16.0f;
const float MaxZ = Site.Center.Z + FMath::Abs(Site.RadiusZ)
+ ReliefBound + 16.0f;
constexpr float SearchStep = 1.0f;
if (!VoxelMath::IsFinite(MinZ) || !VoxelMath::IsFinite(MaxZ) || MaxZ <= MinZ)
{
return false;
}
auto Evaluate = [&](float Z) -> float
{
return VF_EvaluateRoomLandingDensity(
Site, Params, Seed, StrateTopZ, StrateBottomZ,
WorldMouthXY.X, WorldMouthXY.Y, Z);
};
float Low = MinZ;
float LowValue = Evaluate(Low);
if (!VoxelMath::IsFinite(LowValue))
{
return false;
}
const int32 Steps = FMath::CeilToInt((MaxZ - MinZ) / SearchStep);
for (int32 StepIndex = 1; StepIndex <= Steps; ++StepIndex)
{
const float High = FMath::Min(
MaxZ, MinZ + static_cast<float>(StepIndex) * SearchStep);
const float HighValue = Evaluate(High);
if (!VoxelMath::IsFinite(HighValue))
{
Low = High;
LowValue = HighValue;
continue;
}
if (FMath::Abs(LowValue) <= KINDA_SMALL_NUMBER)
{
OutFloorZ = Low;
return true;
}
if (LowValue <= 0.0f && HighValue >= 0.0f)
{
float BracketLow = Low;
float BracketHigh = High;
float BracketLowValue = LowValue;
for (int32 Iteration = 0; Iteration < 24; ++Iteration)
{
const float Mid = (BracketLow + BracketHigh) * 0.5f;
const float MidValue = Evaluate(Mid);
if (!VoxelMath::IsFinite(MidValue))
{
break;
}
if (FMath::Abs(MidValue) <= 1.0e-5f)
{
BracketLow = Mid;
BracketHigh = Mid;
break;
}
if (BracketLowValue <= 0.0f && MidValue < 0.0f)
{
BracketLow = Mid;
BracketLowValue = MidValue;
}
else
{
BracketHigh = Mid;
}
}
OutFloorZ = (BracketLow + BracketHigh) * 0.5f;
return VoxelMath::IsFinite(OutFloorZ);
}
Low = High;
LowValue = HighValue;
}
return false;
}
static bool VF_FindRoomMouthBoundary( static bool VF_FindRoomMouthBoundary(
const FVFRoomLandingSite& Site, const FVFRoomLandingSite& Site,
const FStrateGenerationParams& Params, const FStrateGenerationParams& Params,
@@ -6723,11 +6871,11 @@ void VoxelCaveMorphology::BuildChunkCache(
return Room.bHasPlayerFitPoint; return Room.bHasPlayerFitPoint;
}; };
#if !UE_BUILD_SHIPPING #if !UE_BUILD_SHIPPING
const bool bTrimRoomMouth = CVarVoxelForgeTunnelMouthTrim.GetValueOnAnyThread() != 0; const bool bTrimRoomMouth = CVarVoxelForgeTunnelMouthTrim.GetValueOnAnyThread() != 0;
#else #else
constexpr bool bTrimRoomMouth = false; constexpr bool bTrimRoomMouth = true;
#endif #endif
const auto BuildRoomMouth = [ const auto BuildRoomMouth = [
&BuildRooms, &ResolvePlayerFitPoint, &RoomFloorZFor, &BuildRooms, &ResolvePlayerFitPoint, &RoomFloorZFor,
&Params, Seed, BlendK, bTrimRoomMouth](int32 RoomIndex, const FVector& Towards, &Params, Seed, BlendK, bTrimRoomMouth](int32 RoomIndex, const FVector& Towards,
@@ -6799,8 +6947,26 @@ void VoxelCaveMorphology::BuildChunkCache(
{ {
MouthFloorZ = Room.Center.Z - Room.RadiusZ; MouthFloorZ = Room.Center.Z - Room.RadiusZ;
} }
return FVector( float WorldFloorZ = -FLT_MAX;
MouthXY.X, MouthXY.Y, MouthFloorZ + FMath::Abs(TunnelRadius)); const bool bFoundWorldFloor = VF_FindRoomFloorCrossingWorld(
Site, Params, Seed,
Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
MouthXY, WorldFloorZ);
if (bFoundWorldFloor)
{
// The boundary search returns world XY. Put the endpoint directly on the room's
// world-space floor crossing; the later VF_ApplyCaveWarp call creates the matching
// SDF-chain endpoint. This makes the room/tunnel floor join exact by construction.
return FVector(
MouthXY.X, MouthXY.Y, WorldFloorZ + FMath::Abs(TunnelRadius));
}
// Malformed/unsupported room data keeps the bounded fallback. The outer construction
// path applies VF_ApplyCaveWarp when it creates the SDF-chain endpoint, so invert that
// warp here just as the legacy endpoint path does.
return VF_UnwarpCavePoint(
FVector(MouthXY.X, MouthXY.Y, MouthFloorZ + FMath::Abs(TunnelRadius)),
Params, Seed);
}; };
for (const FResolvedGraphEdge& GraphEdge : GraphEdges) for (const FResolvedGraphEdge& GraphEdge : GraphEdges)
@@ -7987,6 +8153,12 @@ float VoxelCaveMorphology::EvaluateSDFCached(
// differently-shaped surface around the route. // differently-shaped surface around the route.
const bool bUseWorldTunnel = WorldTunnelPosition != nullptr; const bool bUseWorldTunnel = WorldTunnelPosition != nullptr;
const FVector TunnelPosition = bUseWorldTunnel ? *WorldTunnelPosition : Pos; const FVector TunnelPosition = bUseWorldTunnel ? *WorldTunnelPosition : Pos;
// A room owns every point inside its authored body. The short mouth ownership fade below
// handles the fillet outside the wall; this broader test is the hard A > B rule for the room
// interior. In particular, do not let a tunnel's swept bottom reassert a shelf in an already
// open room just because the world-chain query is still near the tunnel centerline.
const bool bRoomOwnsBottom = !VoxelDensityAblation::IsRoomSDFOff()
&& VF_IsInsideCachedRoomShape(Pos, Cache, bUseSpatialIndex);
auto EvaluateTunnel = [&](int32 TunnelIdx) auto EvaluateTunnel = [&](int32 TunnelIdx)
{ {
const FCachedTunnel& Tunnel = TunnelData[TunnelIdx]; const FCachedTunnel& Tunnel = TunnelData[TunnelIdx];
@@ -8031,7 +8203,8 @@ float VoxelCaveMorphology::EvaluateSDFCached(
: VF_FindSDFTunnelMouthOwnership(Pos, Tunnel, BlendK); : VF_FindSDFTunnelMouthOwnership(Pos, Tunnel, BlendK);
const float TunnelSDF = VF_EvaluateSweptTunnel( const float TunnelSDF = VF_EvaluateSweptTunnel(
QueryPosition, Tunnel, bUseWorldChain, BlendK, QueryPosition, Tunnel, bUseWorldChain, BlendK,
/*bApplyFloorCut=*/true, &Cache, TunnelIdx, /*bApplyFloorCut=*/!(bRoomOwnsBottom || RoomMouthOwnership.bSuppressTunnelBottom),
&Cache, TunnelIdx,
&RoomMouthOwnership).SDF; &RoomMouthOwnership).SDF;
MinSDF = VoxelSDF::SmoothMin(MinSDF, TunnelSDF, BlendK); MinSDF = VoxelSDF::SmoothMin(MinSDF, TunnelSDF, BlendK);
@@ -8109,7 +8282,8 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
float WorldX, float WorldY, float WorldZ, float WorldX, float WorldY, float WorldZ,
const FChunkSDFCache& Cache, const FChunkSDFCache& Cache,
const FTunnelSupportFloorColumn* SupportColumn, const FTunnelSupportFloorColumn* SupportColumn,
bool bUseSpatialIndex) bool bUseSpatialIndex,
const FVector* RoomQueryPosition)
{ {
VoxelGenLOD::FScopedReachCost ReachCost( VoxelGenLOD::FScopedReachCost ReachCost(
VoxelGenLOD::ETileReachCostKind::TunnelCoreWorld); VoxelGenLOD::ETileReachCostKind::TunnelCoreWorld);
@@ -8137,6 +8311,10 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
const FVector Pos(WorldX, WorldY, WorldZ); const FVector Pos(WorldX, WorldY, WorldZ);
FTunnelCoreWorldEvaluation Result; FTunnelCoreWorldEvaluation Result;
const FCachedTunnel* TunnelData = Cache.Tunnels.GetData(); const FCachedTunnel* TunnelData = Cache.Tunnels.GetData();
const FVector& RoomPosition = RoomQueryPosition != nullptr
? *RoomQueryPosition : Pos;
const bool bRoomOwnsBottom = VF_IsInsideCachedRoomShape(
RoomPosition, Cache, bUseSpatialIndex);
if (SupportColumn != nullptr && VoxelDensityProfile::AreCountersEnabled()) if (SupportColumn != nullptr && VoxelDensityProfile::AreCountersEnabled())
{ {
@@ -8203,11 +8381,13 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
const FVFTunnelShapeEvaluation TunnelShape = VF_EvaluateSweptTunnel( const FVFTunnelShapeEvaluation TunnelShape = VF_EvaluateSweptTunnel(
Pos, Tunnel, bHasWorldChain, Cache.SDFBlendRadius, Pos, Tunnel, bHasWorldChain, Cache.SDFBlendRadius,
/*bApplyFloorCut=*/true, /*bApplyFloorCut=*/!(bRoomOwnsBottom || RoomMouthOwnership.bSuppressTunnelBottom),
&Cache, TunnelIdx, &RoomMouthOwnership); &Cache, TunnelIdx, &RoomMouthOwnership);
if (TunnelShape.SDF < Result.SDF) if (TunnelShape.SDF < Result.SDF)
{ {
Result.SDF = TunnelShape.SDF; Result.SDF = TunnelShape.SDF;
Result.bRoomFloor = bRoomOwnsBottom
|| RoomMouthOwnership.bSuppressTunnelBottom;
Result.bHasSweptFloor = TunnelShape.bHasSweptFloor; Result.bHasSweptFloor = TunnelShape.bHasSweptFloor;
Result.SweptFloorZ = TunnelShape.SweptFloorZ; Result.SweptFloorZ = TunnelShape.SweptFloorZ;
Result.SweptFloorRadius = TunnelShape.SweptFloorRadius; Result.SweptFloorRadius = TunnelShape.SweptFloorRadius;
@@ -3826,7 +3826,8 @@ namespace
const FVector& Position = S.LastWorldPosition; const FVector& Position = S.LastWorldPosition;
S.LastTunnelCoreWorldEvaluation = VoxelCaveMorphology::EvaluateTunnelCoreWorld( S.LastTunnelCoreWorldEvaluation = VoxelCaveMorphology::EvaluateTunnelCoreWorld(
Position.X, Position.Y, Position.Z, GetCache(), nullptr, Position.X, Position.Y, Position.Z, GetCache(), nullptr,
VoxelGenLOD::ShouldUseSpatialIndex(false)); VoxelGenLOD::ShouldUseSpatialIndex(false),
&S.LastWarpedPosition);
S.LastTunnelCoreEvaluationPosition = Position; S.LastTunnelCoreEvaluationPosition = Position;
S.bLastTunnelCoreWorldEvaluationValid = true; S.bLastTunnelCoreWorldEvaluationValid = true;
} }
@@ -4274,16 +4275,18 @@ namespace
{ {
S.LastTunnelCoreWorldEvaluation = VoxelCaveMorphology::EvaluateTunnelCoreWorld( S.LastTunnelCoreWorldEvaluation = VoxelCaveMorphology::EvaluateTunnelCoreWorld(
WorldX, WorldY, WorldZ, GetCache(), nullptr, WorldX, WorldY, WorldZ, GetCache(), nullptr,
bUseSpatialIndex); bUseSpatialIndex, &S.LastWarpedPosition);
S.LastTunnelCoreEvaluationPosition = S.LastWorldPosition; S.LastTunnelCoreEvaluationPosition = S.LastWorldPosition;
S.bLastTunnelCoreWorldEvaluationValid = true; S.bLastTunnelCoreWorldEvaluationValid = true;
// Keep the hand-off for block evaluation compatibility. Both floor flags are // Keep the room-owned bottom hand-off for block evaluation compatibility. The
// permanently false; the authored floor is part of the tunnel shape itself. // authored floor remains part of the tunnel shape, but it must not win inside a
// room's own volume.
InOut.bHasTunnelCoreWorldEvaluation = true; InOut.bHasTunnelCoreWorldEvaluation = true;
InOut.TunnelCoreWorldSDF = S.LastTunnelCoreWorldEvaluation.SDF; InOut.TunnelCoreWorldSDF = S.LastTunnelCoreWorldEvaluation.SDF;
InOut.bTunnelCoreSupportFloor = false; InOut.bTunnelCoreSupportFloor = false;
InOut.bTunnelCoreRoomFloor = false; InOut.bTunnelCoreRoomFloor =
S.LastTunnelCoreWorldEvaluation.bRoomFloor;
InOut.bHasTunnelCoreSweptFloor = InOut.bHasTunnelCoreSweptFloor =
S.LastTunnelCoreWorldEvaluation.bHasSweptFloor; S.LastTunnelCoreWorldEvaluation.bHasSweptFloor;
InOut.TunnelCoreSweptFloorZ = InOut.TunnelCoreSweptFloorZ =
+31 -4
View File
@@ -3475,10 +3475,18 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
{ {
VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::TunnelCorePosts); VoxelDensityProfile::EBucket::TunnelCorePosts);
FVector RoomQueryPosition(WorldX, WorldY, WorldZ);
if (CP_Tunnel.VerticalScale != 1.0f && CP_Tunnel.VerticalScale > 0.0f)
{
RoomQueryPosition.Z = WorldZ / CP_Tunnel.VerticalScale;
}
RoomQueryPosition = VoxelCaveMorphology::ApplyCaveWarp(
RoomQueryPosition, CP_Tunnel, static_cast<uint32>(Seed));
PreDisturbanceTunnelCore = VoxelCaveMorphology::EvaluateTunnelCoreWorld( PreDisturbanceTunnelCore = VoxelCaveMorphology::EvaluateTunnelCoreWorld(
WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache, nullptr, WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache, nullptr,
VoxelGenLOD::ShouldUseSpatialIndex( VoxelGenLOD::ShouldUseSpatialIndex(
CP_UseOpStack ? bUsedFusedEvaluator : true)); CP_UseOpStack ? bUsedFusedEvaluator : true),
&RoomQueryPosition);
bHavePreDisturbanceTunnelCore = true; bHavePreDisturbanceTunnelCore = true;
} }
@@ -3573,6 +3581,8 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
return; return;
} }
const bool bOwnsSweptBottom = const bool bOwnsSweptBottom =
!Core.bRoomFloor
&&
Core.bHasSweptFloor Core.bHasSweptFloor
&& VoxelMath::IsFinite(Core.SweptFloorZ) && VoxelMath::IsFinite(Core.SweptFloorZ)
&& VoxelMath::IsFinite(Core.SweptFloorRadius) && VoxelMath::IsFinite(Core.SweptFloorRadius)
@@ -3669,11 +3679,19 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
{ {
VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::TunnelCorePosts); VoxelDensityProfile::EBucket::TunnelCorePosts);
FVector RoomQueryPosition(WorldX, WorldY, WorldZ);
if (CP_Tunnel.VerticalScale != 1.0f && CP_Tunnel.VerticalScale > 0.0f)
{
RoomQueryPosition.Z = WorldZ / CP_Tunnel.VerticalScale;
}
RoomQueryPosition = VoxelCaveMorphology::ApplyCaveWarp(
RoomQueryPosition, CP_Tunnel, static_cast<uint32>(Seed));
TunnelCore = VoxelCaveMorphology::EvaluateTunnelCoreWorld( TunnelCore = VoxelCaveMorphology::EvaluateTunnelCoreWorld(
WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache, WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache,
nullptr, nullptr,
VoxelGenLOD::ShouldUseSpatialIndex( VoxelGenLOD::ShouldUseSpatialIndex(
CP_UseOpStack ? bUsedFusedEvaluator : true)); CP_UseOpStack ? bUsedFusedEvaluator : true),
&RoomQueryPosition);
bHaveTunnelCore = true; bHaveTunnelCore = true;
} }
if (bHaveTunnelCore) if (bHaveTunnelCore)
@@ -3689,7 +3707,14 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
ApplyTunnelCoreShape(TunnelCore); ApplyTunnelCoreShape(TunnelCore);
} }
if (bHaveTunnelCore || (!bTunnelCoreReachable && !bTunnelCoreBypass)) if (bHaveTunnelCore)
{
if (!TunnelCore.bRoomFloor)
{
ApplyGraphLandingFloors();
}
}
else if (!bTunnelCoreReachable && !bTunnelCoreBypass)
{ {
ApplyGraphLandingFloors(); ApplyGraphLandingFloors();
} }
@@ -4279,8 +4304,10 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
// floor projection for the current point without eagerly materialising a whole XY table. // floor projection for the current point without eagerly materialising a whole XY table.
// The interpreted source has a lazy worker LRU for block evaluation; building that table // The interpreted source has a lazy worker LRU for block evaluation; building that table
// here would turn a cache-window change into hundreds of unnecessary column builds. // here would turn a cache-window change into hundreds of unnecessary column builds.
const FVector RoomQueryPosition(WarpedX, WarpedY, WarpedZ);
*OutTunnelCore = VoxelCaveMorphology::EvaluateTunnelCoreWorld( *OutTunnelCore = VoxelCaveMorphology::EvaluateTunnelCoreWorld(
WorldX, WorldY, WorldZ, SDFCache, nullptr, bUseSpatialIndex); WorldX, WorldY, WorldZ, SDFCache, nullptr, bUseSpatialIndex,
&RoomQueryPosition);
} }
//========================================================================= //=========================================================================
@@ -940,7 +940,9 @@ struct FTunnelCoreWorldEvaluation
float SDF = FLT_MAX; float SDF = FLT_MAX;
// Deprecated compatibility output. The separate graph-tunnel support slab was removed. // Deprecated compatibility output. The separate graph-tunnel support slab was removed.
bool bSupportFloor = false; bool bSupportFloor = false;
// Deprecated compatibility output. Room/landing floors are composed by their own posts. // Room geometry owns the lower side while the world-space tunnel is inside a room. The
// tunnel SDF may still reopen air there, but its swept bottom must not turn that room air back
// into a shelf or wall.
bool bRoomFloor = false; bool bRoomFloor = false;
// Shape-level floor ownership. This is not an authored slab: it identifies the bottom side // 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. // of the swept tunnel SDF so a room that is already air cannot erase the tunnel's own floor.
@@ -1081,7 +1083,11 @@ namespace VoxelCaveMorphology
const FChunkSDFCache& Cache, const FChunkSDFCache& Cache,
// Deprecated legacy argument; graph-tunnel evaluation ignores the old support column. // Deprecated legacy argument; graph-tunnel evaluation ignores the old support column.
const FTunnelSupportFloorColumn* SupportColumn = nullptr, const FTunnelSupportFloorColumn* SupportColumn = nullptr,
bool bUseSpatialIndex = true bool bUseSpatialIndex = true,
// The room graph is evaluated in warped coordinates while the tunnel core is evaluated in
// the authored world frame. Pass the former when available so room ownership is tested in
// the same domain as the generic room field.
const FVector* RoomQueryPosition = nullptr
); );
// Per-tile broad reach for the world-space core/floor post. The bound includes the swept // Per-tile broad reach for the world-space core/floor post. The bound includes the swept
@@ -1608,6 +1608,61 @@ struct FExploreTunnelCoreProbeAxis
TArray<float> FinalCrossingsVoxels; TArray<float> FinalCrossingsVoxels;
}; };
struct FExploreTunnelMouthProbeAxis
{
FString Name;
int32 ValidCrossings = 0;
float MeanCrossingVoxels = 0.0f;
float MinCrossingVoxels = 0.0f;
float MaxCrossingVoxels = 0.0f;
float PeriodicAmplitudeVoxels = 0.0f;
float PeriodicPeriodVoxels = 0.0f;
int32 PeriodicMeasuredSamples = 0;
float PeriodicMeasuredLengthVoxels = 0.0f;
// Audit value from the core probe's local-median detrend. Mouth surfaces can carry a broad
// room-floor bowl over the +/-2R window, so the accepted mouth value below uses a degree-six
// slow trend while retaining this legacy value for before/after diagnosis.
float LocalMedianPeriodicAmplitudeVoxels = 0.0f;
float LocalMedianPeriodicPeriodVoxels = 0.0f;
TArray<float> CrossingsVoxels;
};
struct FExploreTunnelMouthProbeEndpoint
{
FString Name;
uint32 RoomHash = 0;
FVector Mouth = FVector::ZeroVector;
float RadiusVoxels = 0.0f;
float TunnelFloorZ = -FLT_MAX;
float RoomFloorCrossingZ = -FLT_MAX;
float FloorJoinDeltaVoxels = 0.0f;
FExploreTunnelMouthProbeAxis Wall;
FExploreTunnelMouthProbeAxis Floor;
};
struct FExploreTunnelMouthProbeOutput
{
FString Status;
FString RefusalReason;
FString FileName;
int32 EndpointCount = 0;
int32 LateralSampleCount = 0;
float LateralStepVoxels = 0.25f;
float LateralWindowVoxels = 0.0f;
int32 RoomSampleStepVoxels = 2;
int64 RoomVolumeSampleCount = 0;
int64 RoomAirSampleCount = 0;
int64 CoreBottomSampleCount = 0;
int64 FinalSolidSampleCount = 0;
int64 CoreInducedSolidSampleCount = 0;
float MaxWallPeriodicAmplitudeVoxels = 0.0f;
float MaxFloorPeriodicAmplitudeVoxels = 0.0f;
float MaxLocalMedianWallPeriodicAmplitudeVoxels = 0.0f;
float MaxLocalMedianFloorPeriodicAmplitudeVoxels = 0.0f;
double Seconds = 0.0;
TArray<FExploreTunnelMouthProbeEndpoint> Endpoints;
};
struct FExploreTunnelCoreProbeOutput struct FExploreTunnelCoreProbeOutput
{ {
FString Status; FString Status;
@@ -1634,6 +1689,7 @@ struct FExploreTunnelCoreProbeOutput
FExploreTunnelCoreProbeAxis Floor; FExploreTunnelCoreProbeAxis Floor;
FExploreTunnelCoreProbeAxis Wall; FExploreTunnelCoreProbeAxis Wall;
FExploreTunnelCoreProbeAxis Ceiling; FExploreTunnelCoreProbeAxis Ceiling;
FExploreTunnelMouthProbeOutput Mouth;
double Seconds = 0.0; double Seconds = 0.0;
}; };
@@ -4869,7 +4925,8 @@ void ComputeTunnelCoreProbeMetrics(
float& OutPeriodicAmplitudeVoxels, float& OutPeriodicAmplitudeVoxels,
float& OutPeriodicPeriodVoxels, float& OutPeriodicPeriodVoxels,
int32& OutPeriodicMeasuredSamples, int32& OutPeriodicMeasuredSamples,
float& OutPeriodicMeasuredLengthVoxels) float& OutPeriodicMeasuredLengthVoxels,
bool bPolynomialTrend = false)
{ {
OutValidCount = 0; OutValidCount = 0;
OutMean = 0.0f; OutMean = 0.0f;
@@ -4951,14 +5008,17 @@ void ComputeTunnelCoreProbeMetrics(
} }
// Remove only the slow authored floor/radius trend. A 32-voxel median window is wider than // Remove only the slow authored floor/radius trend. A 32-voxel median window is wider than
// the 1..16 voxel rib band, so a genuine rib train survives while a single mouth transition // the 1..16 voxel rib band, so a genuine rib train survives while a single tunnel transition
// or route slope follows the local baseline. Discarding the half-window at each edge keeps // or route slope follows the local baseline. The mouth probe uses a degree-six fit instead: the
// the asymmetric endpoint windows out of the spectral result. // room-side +/-2R window can contain one broad floor bowl, which a local median would turn
// into a false short-period peak when only the middle 21.5 voxels remain.
constexpr float TrendWindowVoxels = 32.0f; constexpr float TrendWindowVoxels = 32.0f;
const int32 HalfWindowSamples = FMath::Max( const int32 HalfWindowSamples = FMath::Max(
1, FMath::RoundToInt(0.5f * TrendWindowVoxels / StepVoxels)); 1, FMath::RoundToInt(0.5f * TrendWindowVoxels / StepVoxels));
const int32 AnalysisStart = BestRunStart + HalfWindowSamples; const int32 AnalysisStart = bPolynomialTrend
const int32 AnalysisEnd = BestRunEnd - HalfWindowSamples; ? BestRunStart : BestRunStart + HalfWindowSamples;
const int32 AnalysisEnd = bPolynomialTrend
? BestRunEnd : BestRunEnd - HalfWindowSamples;
const int32 AnalysisCount = AnalysisEnd - AnalysisStart + 1; const int32 AnalysisCount = AnalysisEnd - AnalysisStart + 1;
if (AnalysisCount < 4) if (AnalysisCount < 4)
{ {
@@ -4968,6 +5028,110 @@ void ComputeTunnelCoreProbeMetrics(
OutPeriodicMeasuredSamples = AnalysisCount; OutPeriodicMeasuredSamples = AnalysisCount;
OutPeriodicMeasuredLengthVoxels = static_cast<float>(AnalysisCount - 1) * StepVoxels; OutPeriodicMeasuredLengthVoxels = static_cast<float>(AnalysisCount - 1) * StepVoxels;
TArray<float, TInlineAllocator<257>> Residuals;
Residuals.SetNumZeroed(AnalysisCount);
bool bHavePolynomialTrend = false;
if (bPolynomialTrend)
{
// Fit a normalized degree-six smooth trend over the complete contiguous surface run. The
// room-side +/-2R sample can contain a broad, asymmetric floor bowl; a cubic leaves that
// endpoint curvature in the rib-band DFT. Degree six follows that slow authored shape but
// cannot reproduce a repeated <=16-voxel train across the full run. Keep the legacy
// local-median measurement separately as an audit of the conservative raw instrument.
constexpr int32 PolynomialDegree = 6;
double Matrix[PolynomialDegree + 1][PolynomialDegree + 1] = {};
double RightHandSide[PolynomialDegree + 1] = {};
const double Denominator = static_cast<double>(FMath::Max(
BestRunLength - 1, 1));
for (int32 Index = BestRunStart; Index <= BestRunEnd; ++Index)
{
const double NormalizedX = -1.0
+ 2.0 * static_cast<double>(Index - BestRunStart) / Denominator;
double Basis[PolynomialDegree + 1];
Basis[0] = 1.0;
for (int32 Power = 1; Power <= PolynomialDegree; ++Power)
{
Basis[Power] = Basis[Power - 1] * NormalizedX;
}
const double Value = static_cast<double>(Samples[Index]);
for (int32 Row = 0; Row <= PolynomialDegree; ++Row)
{
RightHandSide[Row] += Basis[Row] * Value;
for (int32 Column = 0; Column <= PolynomialDegree; ++Column)
{
Matrix[Row][Column] += Basis[Row] * Basis[Column];
}
}
}
double Coefficients[PolynomialDegree + 1] = {};
bHavePolynomialTrend = true;
for (int32 PivotIndex = 0; PivotIndex <= PolynomialDegree; ++PivotIndex)
{
int32 PivotRow = PivotIndex;
for (int32 Row = PivotIndex + 1; Row <= PolynomialDegree; ++Row)
{
if (FMath::Abs(Matrix[Row][PivotIndex])
> FMath::Abs(Matrix[PivotRow][PivotIndex]))
{
PivotRow = Row;
}
}
if (FMath::Abs(Matrix[PivotRow][PivotIndex]) <= 1.0e-12)
{
bHavePolynomialTrend = false;
break;
}
if (PivotRow != PivotIndex)
{
for (int32 Column = PivotIndex;
Column <= PolynomialDegree; ++Column)
{
Swap(Matrix[PivotIndex][Column], Matrix[PivotRow][Column]);
}
Swap(RightHandSide[PivotIndex], RightHandSide[PivotRow]);
}
for (int32 Row = PivotIndex + 1; Row <= PolynomialDegree; ++Row)
{
const double Factor = Matrix[Row][PivotIndex]
/ Matrix[PivotIndex][PivotIndex];
for (int32 Column = PivotIndex;
Column <= PolynomialDegree; ++Column)
{
Matrix[Row][Column] -= Factor * Matrix[PivotIndex][Column];
}
RightHandSide[Row] -= Factor * RightHandSide[PivotIndex];
}
}
if (bHavePolynomialTrend)
{
for (int32 Row = PolynomialDegree; Row >= 0; --Row)
{
double Value = RightHandSide[Row];
for (int32 Column = Row + 1;
Column <= PolynomialDegree; ++Column)
{
Value -= Matrix[Row][Column] * Coefficients[Column];
}
Coefficients[Row] = Value / Matrix[Row][Row];
}
for (int32 Index = AnalysisStart; Index <= AnalysisEnd; ++Index)
{
const double NormalizedX = -1.0
+ 2.0 * static_cast<double>(Index - BestRunStart) / Denominator;
double Basis = 1.0;
double Trend = 0.0;
for (int32 Power = 0; Power <= PolynomialDegree; ++Power)
{
Trend += Coefficients[Power] * Basis;
Basis *= NormalizedX;
}
Residuals[Index - AnalysisStart] =
static_cast<float>(static_cast<double>(Samples[Index]) - Trend);
}
}
}
constexpr float MaxRibPeriodVoxels = 16.0f; constexpr float MaxRibPeriodVoxels = 16.0f;
const int32 MinRibFrequency = FMath::Max( const int32 MinRibFrequency = FMath::Max(
1, FMath::CeilToInt(StepVoxels * static_cast<float>(AnalysisCount) 1, FMath::CeilToInt(StepVoxels * static_cast<float>(AnalysisCount)
@@ -4981,22 +5145,27 @@ void ComputeTunnelCoreProbeMetrics(
double Imaginary = 0.0; double Imaginary = 0.0;
for (int32 Index = AnalysisStart; Index <= AnalysisEnd; ++Index) for (int32 Index = AnalysisStart; Index <= AnalysisEnd; ++Index)
{ {
TArray<float, TInlineAllocator<129>> Window;
const int32 WindowStart = FMath::Max(BestRunStart, Index - HalfWindowSamples);
const int32 WindowEnd = FMath::Min(BestRunEnd, Index + HalfWindowSamples);
Window.Reserve(WindowEnd - WindowStart + 1);
for (int32 WindowIndex = WindowStart; WindowIndex <= WindowEnd; ++WindowIndex)
{
Window.Add(Samples[WindowIndex]);
}
Window.Sort();
const int32 Middle = Window.Num() / 2;
const double Median = (Window.Num() & 1) != 0
? static_cast<double>(Window[Middle])
: 0.5 * (static_cast<double>(Window[Middle - 1])
+ static_cast<double>(Window[Middle]));
const double Residual = static_cast<double>(Samples[Index]) - Median;
const int32 AnalysisIndex = Index - AnalysisStart; const int32 AnalysisIndex = Index - AnalysisStart;
if (!bHavePolynomialTrend)
{
TArray<float, TInlineAllocator<129>> Window;
const int32 WindowStart = FMath::Max(BestRunStart, Index - HalfWindowSamples);
const int32 WindowEnd = FMath::Min(BestRunEnd, Index + HalfWindowSamples);
Window.Reserve(WindowEnd - WindowStart + 1);
for (int32 WindowIndex = WindowStart; WindowIndex <= WindowEnd; ++WindowIndex)
{
Window.Add(Samples[WindowIndex]);
}
Window.Sort();
const int32 Middle = Window.Num() / 2;
const double Median = (Window.Num() & 1) != 0
? static_cast<double>(Window[Middle])
: 0.5 * (static_cast<double>(Window[Middle - 1])
+ static_cast<double>(Window[Middle]));
Residuals[AnalysisIndex] = static_cast<float>(
static_cast<double>(Samples[Index]) - Median);
}
const double Residual = static_cast<double>(Residuals[AnalysisIndex]);
const double Angle = 2.0 * PI * static_cast<double>(Frequency) const double Angle = 2.0 * PI * static_cast<double>(Frequency)
* static_cast<double>(AnalysisIndex) / static_cast<double>(AnalysisCount); * static_cast<double>(AnalysisIndex) / static_cast<double>(AnalysisCount);
Real += Residual * FMath::Cos(static_cast<float>(Angle)); Real += Residual * FMath::Cos(static_cast<float>(Angle));
@@ -5017,6 +5186,31 @@ void ComputeTunnelCoreProbeMetrics(
: 0.0f; : 0.0f;
} }
void ComputeTunnelMouthProbeMetrics(
FExploreTunnelMouthProbeAxis& Axis,
float StepVoxels)
{
int32 LegacyValid = 0;
float LegacyMean = 0.0f;
float LegacyMin = 0.0f;
float LegacyMax = 0.0f;
int32 LegacyMeasuredSamples = 0;
float LegacyMeasuredLength = 0.0f;
ComputeTunnelCoreProbeMetrics(
Axis.CrossingsVoxels, StepVoxels,
Axis.ValidCrossings, Axis.MeanCrossingVoxels,
Axis.MinCrossingVoxels, Axis.MaxCrossingVoxels,
Axis.LocalMedianPeriodicAmplitudeVoxels,
Axis.LocalMedianPeriodicPeriodVoxels,
LegacyMeasuredSamples, LegacyMeasuredLength);
ComputeTunnelCoreProbeMetrics(
Axis.CrossingsVoxels, StepVoxels,
LegacyValid, LegacyMean, LegacyMin, LegacyMax,
Axis.PeriodicAmplitudeVoxels, Axis.PeriodicPeriodVoxels,
Axis.PeriodicMeasuredSamples, Axis.PeriodicMeasuredLengthVoxels,
/*bPolynomialTrend=*/true);
}
bool RunTunnelCoreProbe( bool RunTunnelCoreProbe(
const FExploreArguments& Arguments, const FExploreArguments& Arguments,
FExploreWorld& World, FExploreWorld& World,
@@ -5661,7 +5855,301 @@ bool RunTunnelCoreProbe(
Axis->ValidCoreCrossings, Axis->CorePeriodicAmplitudeVoxels, Axis->ValidCoreCrossings, Axis->CorePeriodicAmplitudeVoxels,
Axis->CorePeriodicAmplitudeVoxels * 25.0f, Axis->CorePeriodicAmplitudeVoxels * 25.0f,
Axis->CorePeriodicPeriodVoxels, Axis->CorePeriodicPeriodVoxels,
Axis->CorePeriodicMeasuredLengthVoxels, Axis->CorePeriodicMeasuredSamples); Axis->CorePeriodicMeasuredLengthVoxels, Axis->CorePeriodicMeasuredSamples);
}
// Mouth probe: measure the two surfaces that meet at each room endpoint over a lateral
// window of approximately +/-2 tunnel radii. The wall ray starts in room air and points
// toward the tunnel; the floor ray starts below the same room-side patch and points up.
// Both use the final MC-facing density, so this catches a ring/fillet handoff rather than
// merely comparing two independent source SDFs. Keep this in the same owner-value process
// and cache selection as the core probe so the before/after numbers are directly comparable.
{
FExploreTunnelMouthProbeOutput& Mouth = OutOutput.Mouth;
Mouth = FExploreTunnelMouthProbeOutput();
Mouth.FileName = TEXT("tunnel_mouth_probe.csv");
const double MouthStartSeconds = FPlatformTime::Seconds();
FString MouthCsv;
MouthCsv.Reserve(8192);
MouthCsv += TEXT(
"endpoint,lateral_offset_voxels,wall_surface_distance_voxels,"
"floor_surface_height_voxels\r\n");
const TArray<float>& MouthRadii = Radii;
const TArray<FVector>& MouthPoints = Points;
const int32 MouthPointCount = MouthPoints.Num();
const uint32 RoomHashes[2] = {
BestTunnel->FloorRoomHashA, BestTunnel->FloorRoomHashB };
const int32 EndpointPointIndices[2] = { 0, MouthPointCount - 1 };
const int32 InteriorPointIndices[2] = { 1, MouthPointCount - 2 };
TSet<uint32> CountedRoomHashes;
auto CachedRoomRawSDF = [](const FCachedRoom& Room, const FVector& Position) -> float
{
switch (Room.ShapeType)
{
case 1:
return VoxelSDF::RoundedBox(
Position, Room.Center, Room.ShapeA, Room.ShapeR);
case 2:
return VoxelSDF::Capsule(
Position, Room.ShapeA, Room.ShapeB, Room.ShapeR);
default:
return VoxelSDF::Ellipsoid(Position, Room.Center, Room.ShapeA);
}
};
// The room-volume attribution sample is intentionally coarser than the mesh lattice. It
// is a count instrument, not a replacement for the matched export, and its resolution is
// recorded in the report. A core-induced solid must still be zero at this resolution.
const int32 RoomSampleStep = Mouth.RoomSampleStepVoxels;
for (int32 EndpointIndex = 0; EndpointIndex < 2; ++EndpointIndex)
{
const int32 PointIndex = EndpointPointIndices[EndpointIndex];
const int32 InteriorIndex = InteriorPointIndices[EndpointIndex];
if (!MouthPoints.IsValidIndex(PointIndex)
|| !MouthPoints.IsValidIndex(InteriorIndex)
|| !MouthRadii.IsValidIndex(PointIndex))
{
continue;
}
FExploreTunnelMouthProbeEndpoint& Endpoint = Mouth.Endpoints.AddDefaulted_GetRef();
Endpoint.Name = EndpointIndex == 0 ? TEXT("A") : TEXT("B");
Endpoint.RoomHash = RoomHashes[EndpointIndex];
Endpoint.Mouth = MouthPoints[PointIndex];
Endpoint.RadiusVoxels = FMath::Abs(MouthRadii[PointIndex]);
Endpoint.TunnelFloorZ = EndpointIndex == 0
? BestTunnel->WorldMouthFloorZA : BestTunnel->WorldMouthFloorZB;
if (!FMath::IsFinite(Endpoint.TunnelFloorZ))
{
Endpoint.TunnelFloorZ = static_cast<float>(Endpoint.Mouth.Z)
- Endpoint.RadiusVoxels;
}
FVector RoomToTunnel = MouthPoints[InteriorIndex] - MouthPoints[PointIndex];
RoomToTunnel.Z = 0.0f;
if (!RoomToTunnel.Normalize())
{
RoomToTunnel = EndpointIndex == 0
? FVector::ForwardVector : -FVector::ForwardVector;
}
const FVector Lateral(-RoomToTunnel.Y, RoomToTunnel.X, 0.0f);
const float MaxMouthRadius = FMath::Max(MaxRadius, Endpoint.RadiusVoxels);
const float MouthRayReach = FMath::Max(MaxMouthRadius + 12.0f, 16.0f);
const float LateralHalfWidth = FMath::Max(
Endpoint.RadiusVoxels * 2.0f, 8.0f);
const float LateralStep = 0.25f;
const int32 LateralSamples = FMath::Max(
5, FMath::FloorToInt(
(2.0f * LateralHalfWidth) / LateralStep) + 1);
const float ActualHalfWidth =
0.5f * static_cast<float>(LateralSamples - 1) * LateralStep;
Mouth.LateralSampleCount = FMath::Max(Mouth.LateralSampleCount, LateralSamples);
Mouth.LateralWindowVoxels = FMath::Max(
Mouth.LateralWindowVoxels, 2.0f * ActualHalfWidth);
Endpoint.Wall.Name = TEXT("wall");
Endpoint.Floor.Name = TEXT("floor");
Endpoint.Wall.CrossingsVoxels.Reserve(LateralSamples);
Endpoint.Floor.CrossingsVoxels.Reserve(LateralSamples);
// Keep the origin in finite room air but close enough that the existing radial
// crossing search still reaches the mouth in one probe window.
const float WallInteriorDepth = FMath::Min(
FMath::Max(4.0f * Endpoint.RadiusVoxels, 12.0f),
0.5f * MouthRayReach);
const float FloorInteriorDepth = FMath::Min(
FMath::Max(Endpoint.RadiusVoxels, 4.0f),
0.25f * MouthRayReach);
const float FloorOriginZ = static_cast<float>(Endpoint.Mouth.Z)
- Endpoint.RadiusVoxels - 8.0f;
for (int32 SampleIndex = 0; SampleIndex < LateralSamples; ++SampleIndex)
{
const float LateralOffset = -ActualHalfWidth
+ static_cast<float>(SampleIndex) * LateralStep;
const FVector LateralOffsetVector = Lateral * LateralOffset;
const FVector WallOrigin = Endpoint.Mouth + LateralOffsetVector
- RoomToTunnel * WallInteriorDepth;
const FVector FloorOrigin(
Endpoint.Mouth.X + LateralOffsetVector.X
- RoomToTunnel.X * FloorInteriorDepth,
Endpoint.Mouth.Y + LateralOffsetVector.Y
- RoomToTunnel.Y * FloorInteriorDepth,
FloorOriginZ);
float WallCrossing = std::numeric_limits<float>::quiet_NaN();
float FloorCrossing = std::numeric_limits<float>::quiet_NaN();
FindZeroCrossing(
WallOrigin, RoomToTunnel, false, WallCrossing);
FindZeroCrossing(
FloorOrigin, FVector::UpVector, false, FloorCrossing);
Endpoint.Wall.CrossingsVoxels.Add(WallCrossing);
Endpoint.Floor.CrossingsVoxels.Add(FloorCrossing);
MouthCsv += FString::Printf(
TEXT("%s,%.9g,%.9g,%.9g\r\n"),
*Endpoint.Name, LateralOffset, WallCrossing, FloorCrossing);
}
ComputeTunnelMouthProbeMetrics(Endpoint.Wall, LateralStep);
ComputeTunnelMouthProbeMetrics(Endpoint.Floor, LateralStep);
Mouth.MaxWallPeriodicAmplitudeVoxels = FMath::Max(
Mouth.MaxWallPeriodicAmplitudeVoxels,
Endpoint.Wall.PeriodicAmplitudeVoxels);
Mouth.MaxFloorPeriodicAmplitudeVoxels = FMath::Max(
Mouth.MaxFloorPeriodicAmplitudeVoxels,
Endpoint.Floor.PeriodicAmplitudeVoxels);
Mouth.MaxLocalMedianWallPeriodicAmplitudeVoxels = FMath::Max(
Mouth.MaxLocalMedianWallPeriodicAmplitudeVoxels,
Endpoint.Wall.LocalMedianPeriodicAmplitudeVoxels);
Mouth.MaxLocalMedianFloorPeriodicAmplitudeVoxels = FMath::Max(
Mouth.MaxLocalMedianFloorPeriodicAmplitudeVoxels,
Endpoint.Floor.LocalMedianPeriodicAmplitudeVoxels);
const int32 CentreIndex = LateralSamples / 2;
if (Endpoint.Floor.CrossingsVoxels.IsValidIndex(CentreIndex)
&& FMath::IsFinite(Endpoint.Floor.CrossingsVoxels[CentreIndex]))
{
Endpoint.RoomFloorCrossingZ = FloorOriginZ
+ Endpoint.Floor.CrossingsVoxels[CentreIndex];
Endpoint.FloorJoinDeltaVoxels = Endpoint.RoomFloorCrossingZ
- Endpoint.TunnelFloorZ;
}
// Count the room-volume samples once per endpoint room. This catches the exact
// failure mode from the screenshot: the generic field is air, then the tunnel-core
// bottom post changes it back to solid inside a room.
const FCachedRoom* Room = nullptr;
for (const FCachedRoom& CandidateRoom : Cache.Rooms)
{
if (CandidateRoom.Hash == RoomHashes[EndpointIndex])
{
Room = &CandidateRoom;
break;
}
}
if (Room == nullptr || CountedRoomHashes.Contains(Room->Hash))
{
continue;
}
CountedRoomHashes.Add(Room->Hash);
FVector ShapeExtentVector(
FMath::Abs(Room->ShapeA.X),
FMath::Abs(Room->ShapeA.Y),
FMath::Abs(Room->ShapeA.Z));
if (Room->ShapeType == 2)
{
ShapeExtentVector = FVector(
FMath::Max(
FMath::Abs(Room->ShapeA.X - Room->Center.X),
FMath::Abs(Room->ShapeB.X - Room->Center.X)),
FMath::Max(
FMath::Abs(Room->ShapeA.Y - Room->Center.Y),
FMath::Abs(Room->ShapeB.Y - Room->Center.Y)),
FMath::Max(
FMath::Abs(Room->ShapeA.Z - Room->Center.Z),
FMath::Abs(Room->ShapeB.Z - Room->Center.Z)));
}
const float ShapeExtent = FMath::Max3(
ShapeExtentVector.X, ShapeExtentVector.Y, ShapeExtentVector.Z)
+ FMath::Abs(Room->ShapeR) + 2.0f;
const int32 MinX = FMath::FloorToInt(Room->Center.X - ShapeExtent);
const int32 MinY = FMath::FloorToInt(Room->Center.Y - ShapeExtent);
const int32 MinZ = FMath::FloorToInt(Room->Center.Z - ShapeExtent);
const int32 MaxX = FMath::CeilToInt(Room->Center.X + ShapeExtent);
const int32 MaxY = FMath::CeilToInt(Room->Center.Y + ShapeExtent);
const int32 MaxZ = FMath::CeilToInt(Room->Center.Z + ShapeExtent);
for (int32 Z = MinZ; Z <= MaxZ; Z += RoomSampleStep)
{
for (int32 Y = MinY; Y <= MaxY; Y += RoomSampleStep)
{
for (int32 X = MinX; X <= MaxX; X += RoomSampleStep)
{
const FVector Position(
static_cast<float>(X), static_cast<float>(Y), static_cast<float>(Z));
if (!(CachedRoomRawSDF(*Room, Position) < 0.0f))
{
continue;
}
++Mouth.RoomVolumeSampleCount;
FTunnelCoreWorldEvaluation CoreEvaluation;
const float GenericDensity = World.Generator->GetDensityWithParams(
Position.X, Position.Y, Position.Z,
Params, BestParamsFingerprint, ProbeLayoutVersion,
/*bApplyLegacyStructuralPosts=*/false,
&CoreEvaluation,
/*bCollectFusedDiagnostics=*/false);
const float FinalDensity = World.Generator->GetDensityAt(
Position.X, Position.Y, Position.Z);
if (!FMath::IsFinite(GenericDensity)
|| !FMath::IsFinite(FinalDensity))
{
continue;
}
if (GenericDensity > 0.0f)
{
++Mouth.RoomAirSampleCount;
}
if (FinalDensity <= 0.0f)
{
++Mouth.FinalSolidSampleCount;
}
const bool bOwnsSweptBottom =
CoreEvaluation.bHasSweptFloor
&& FMath::IsFinite(CoreEvaluation.SweptFloorZ)
&& FMath::IsFinite(CoreEvaluation.SweptFloorRadius)
&& Position.Z <= CoreEvaluation.SweptFloorZ
+ VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels;
if (bOwnsSweptBottom)
{
++Mouth.CoreBottomSampleCount;
if (GenericDensity > 0.0f && FinalDensity <= 0.0f)
{
++Mouth.CoreInducedSolidSampleCount;
}
}
}
}
}
}
Mouth.EndpointCount = Mouth.Endpoints.Num();
const FString MouthPath = FPaths::Combine(Arguments.OutDirectory, Mouth.FileName);
if (!FFileHelper::SaveStringToFile(
MouthCsv, *MouthPath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
{
Mouth.Status = TEXT("error");
Mouth.RefusalReason = TEXT("Could not write tunnel_mouth_probe.csv.");
}
else if (Mouth.EndpointCount < 2)
{
Mouth.Status = TEXT("error");
Mouth.RefusalReason = TEXT("The selected tunnel did not expose two room endpoints.");
}
else
{
Mouth.Status = TEXT("ok");
}
Mouth.Seconds = FPlatformTime::Seconds() - MouthStartSeconds;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeTunnelMouthProbe] endpoints=%d room_samples=%lld room_air=%lld "
"core_bottom=%lld final_solid=%lld core_induced_solid=%lld "
"wall_periodic=%.6g_voxels=%.6g_cm floor_periodic=%.6g_voxels=%.6g_cm "
"legacy_floor_periodic=%.6g_voxels=%.6g_cm "
"lateral_window=%.3f step=%.3f file=%s"),
Mouth.EndpointCount,
Mouth.RoomVolumeSampleCount, Mouth.RoomAirSampleCount,
Mouth.CoreBottomSampleCount, Mouth.FinalSolidSampleCount,
Mouth.CoreInducedSolidSampleCount,
Mouth.MaxWallPeriodicAmplitudeVoxels,
Mouth.MaxWallPeriodicAmplitudeVoxels * 25.0f,
Mouth.MaxFloorPeriodicAmplitudeVoxels,
Mouth.MaxFloorPeriodicAmplitudeVoxels * 25.0f,
Mouth.MaxLocalMedianFloorPeriodicAmplitudeVoxels,
Mouth.MaxLocalMedianFloorPeriodicAmplitudeVoxels * 25.0f,
Mouth.LateralWindowVoxels, Mouth.LateralStepVoxels, *MouthPath);
} }
const FString Path = FPaths::Combine(Arguments.OutDirectory, OutOutput.FileName); const FString Path = FPaths::Combine(Arguments.OutDirectory, OutOutput.FileName);
@@ -5718,6 +6206,27 @@ void WriteTunnelCoreProbeAxis(
Writer.WriteObjectEnd(); Writer.WriteObjectEnd();
} }
void WriteTunnelMouthProbeAxis(
FExploreJsonWriter& Writer,
const FExploreTunnelMouthProbeAxis& Axis,
const TCHAR* Key)
{
Writer.WriteObjectStart(Key);
Writer.WriteValue(TEXT("valid_crossings"), Axis.ValidCrossings);
Writer.WriteValue(TEXT("mean_crossing_voxels"), static_cast<double>(Axis.MeanCrossingVoxels));
Writer.WriteValue(TEXT("min_crossing_voxels"), static_cast<double>(Axis.MinCrossingVoxels));
Writer.WriteValue(TEXT("max_crossing_voxels"), static_cast<double>(Axis.MaxCrossingVoxels));
Writer.WriteValue(TEXT("periodic_amplitude_voxels"), static_cast<double>(Axis.PeriodicAmplitudeVoxels));
Writer.WriteValue(TEXT("periodic_amplitude_cm"), static_cast<double>(Axis.PeriodicAmplitudeVoxels) * 25.0);
Writer.WriteValue(TEXT("periodic_period_voxels"), static_cast<double>(Axis.PeriodicPeriodVoxels));
Writer.WriteValue(TEXT("periodic_measured_samples"), Axis.PeriodicMeasuredSamples);
Writer.WriteValue(TEXT("periodic_measured_length_voxels"), static_cast<double>(Axis.PeriodicMeasuredLengthVoxels));
Writer.WriteValue(TEXT("local_median_periodic_amplitude_voxels"), static_cast<double>(Axis.LocalMedianPeriodicAmplitudeVoxels));
Writer.WriteValue(TEXT("local_median_periodic_amplitude_cm"), static_cast<double>(Axis.LocalMedianPeriodicAmplitudeVoxels) * 25.0);
Writer.WriteValue(TEXT("local_median_periodic_period_voxels"), static_cast<double>(Axis.LocalMedianPeriodicPeriodVoxels));
Writer.WriteObjectEnd();
}
void WritePlayerDimensions(FExploreJsonWriter& Writer, const TCHAR* Key) void WritePlayerDimensions(FExploreJsonWriter& Writer, const TCHAR* Key)
{ {
Writer.WriteObjectStart(Key); Writer.WriteObjectStart(Key);
@@ -5884,10 +6393,55 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("final_center_min"), static_cast<double>(Output.TunnelCoreProbe.FinalCenterMin)); Writer->WriteValue(TEXT("final_center_min"), static_cast<double>(Output.TunnelCoreProbe.FinalCenterMin));
Writer->WriteValue(TEXT("final_center_max"), static_cast<double>(Output.TunnelCoreProbe.FinalCenterMax)); Writer->WriteValue(TEXT("final_center_max"), static_cast<double>(Output.TunnelCoreProbe.FinalCenterMax));
Writer->WriteValue(TEXT("final_center_max_abs"), static_cast<double>(Output.TunnelCoreProbe.FinalCenterMaxAbs)); Writer->WriteValue(TEXT("final_center_max_abs"), static_cast<double>(Output.TunnelCoreProbe.FinalCenterMaxAbs));
Writer->WriteValue(TEXT("periodic_method"), TEXT("32-voxel local-median detrend over the longest contiguous >=40-voxel run, single-sided DFT peak in the <=16-voxel band; amplitudes are sinusoid amplitudes")); Writer->WriteValue(TEXT("periodic_method"), TEXT("Tunnel-core axes: 32-voxel local-median detrend. Mouth axes: degree-six slow-trend detrend over the longest contiguous >=40-voxel run, with the legacy local-median value retained per axis; both use a single-sided DFT peak in the <=16-voxel band and report sinusoid amplitudes."));
WriteTunnelCoreProbeAxis(*Writer, Output.TunnelCoreProbe.Floor, TEXT("floor")); WriteTunnelCoreProbeAxis(*Writer, Output.TunnelCoreProbe.Floor, TEXT("floor"));
WriteTunnelCoreProbeAxis(*Writer, Output.TunnelCoreProbe.Wall, TEXT("wall")); WriteTunnelCoreProbeAxis(*Writer, Output.TunnelCoreProbe.Wall, TEXT("wall"));
WriteTunnelCoreProbeAxis(*Writer, Output.TunnelCoreProbe.Ceiling, TEXT("ceiling")); WriteTunnelCoreProbeAxis(*Writer, Output.TunnelCoreProbe.Ceiling, TEXT("ceiling"));
Writer->WriteObjectStart(TEXT("mouth_probe"));
Writer->WriteValue(TEXT("status"), Output.TunnelCoreProbe.Mouth.Status);
Writer->WriteValue(TEXT("file"), Output.TunnelCoreProbe.Mouth.FileName);
Writer->WriteValue(TEXT("refusal_or_error"), Output.TunnelCoreProbe.Mouth.RefusalReason);
Writer->WriteValue(TEXT("endpoint_count"), Output.TunnelCoreProbe.Mouth.EndpointCount);
Writer->WriteValue(TEXT("lateral_sample_count"), Output.TunnelCoreProbe.Mouth.LateralSampleCount);
Writer->WriteValue(TEXT("lateral_step_voxels"), static_cast<double>(Output.TunnelCoreProbe.Mouth.LateralStepVoxels));
Writer->WriteValue(TEXT("lateral_window_voxels"), static_cast<double>(Output.TunnelCoreProbe.Mouth.LateralWindowVoxels));
Writer->WriteValue(TEXT("room_sample_step_voxels"), Output.TunnelCoreProbe.Mouth.RoomSampleStepVoxels);
Writer->WriteValue(TEXT("room_volume_sample_count"), Output.TunnelCoreProbe.Mouth.RoomVolumeSampleCount);
Writer->WriteValue(TEXT("room_air_sample_count"), Output.TunnelCoreProbe.Mouth.RoomAirSampleCount);
Writer->WriteValue(TEXT("core_bottom_sample_count"), Output.TunnelCoreProbe.Mouth.CoreBottomSampleCount);
Writer->WriteValue(TEXT("final_solid_sample_count"), Output.TunnelCoreProbe.Mouth.FinalSolidSampleCount);
Writer->WriteValue(TEXT("core_induced_solid_sample_count"), Output.TunnelCoreProbe.Mouth.CoreInducedSolidSampleCount);
Writer->WriteValue(TEXT("max_wall_periodic_amplitude_voxels"), static_cast<double>(Output.TunnelCoreProbe.Mouth.MaxWallPeriodicAmplitudeVoxels));
Writer->WriteValue(TEXT("max_wall_periodic_amplitude_cm"), static_cast<double>(Output.TunnelCoreProbe.Mouth.MaxWallPeriodicAmplitudeVoxels) * 25.0);
Writer->WriteValue(TEXT("max_floor_periodic_amplitude_voxels"), static_cast<double>(Output.TunnelCoreProbe.Mouth.MaxFloorPeriodicAmplitudeVoxels));
Writer->WriteValue(TEXT("max_floor_periodic_amplitude_cm"), static_cast<double>(Output.TunnelCoreProbe.Mouth.MaxFloorPeriodicAmplitudeVoxels) * 25.0);
Writer->WriteValue(TEXT("max_local_median_wall_periodic_amplitude_voxels"), static_cast<double>(Output.TunnelCoreProbe.Mouth.MaxLocalMedianWallPeriodicAmplitudeVoxels));
Writer->WriteValue(TEXT("max_local_median_wall_periodic_amplitude_cm"), static_cast<double>(Output.TunnelCoreProbe.Mouth.MaxLocalMedianWallPeriodicAmplitudeVoxels) * 25.0);
Writer->WriteValue(TEXT("max_local_median_floor_periodic_amplitude_voxels"), static_cast<double>(Output.TunnelCoreProbe.Mouth.MaxLocalMedianFloorPeriodicAmplitudeVoxels));
Writer->WriteValue(TEXT("max_local_median_floor_periodic_amplitude_cm"), static_cast<double>(Output.TunnelCoreProbe.Mouth.MaxLocalMedianFloorPeriodicAmplitudeVoxels) * 25.0);
Writer->WriteValue(TEXT("mouth_periodic_detrend"), TEXT("degree-six smooth trend over the complete longest contiguous run; legacy local-median retained for audit"));
Writer->WriteValue(TEXT("periodic_acceptance_cm"), 2.0);
Writer->WriteValue(TEXT("periodic_acceptance_pass"),
Output.TunnelCoreProbe.Mouth.MaxWallPeriodicAmplitudeVoxels * 25.0f <= 2.0f
&& Output.TunnelCoreProbe.Mouth.MaxFloorPeriodicAmplitudeVoxels * 25.0f <= 2.0f);
Writer->WriteArrayStart(TEXT("endpoints"));
for (const FExploreTunnelMouthProbeEndpoint& Endpoint : Output.TunnelCoreProbe.Mouth.Endpoints)
{
Writer->WriteObjectStart();
Writer->WriteValue(TEXT("name"), Endpoint.Name);
Writer->WriteValue(TEXT("room_hash"), static_cast<int64>(Endpoint.RoomHash));
WriteJsonVector(*Writer, TEXT("mouth_voxels"), Endpoint.Mouth, 1.0f);
Writer->WriteValue(TEXT("radius_voxels"), static_cast<double>(Endpoint.RadiusVoxels));
Writer->WriteValue(TEXT("tunnel_floor_z"), static_cast<double>(Endpoint.TunnelFloorZ));
Writer->WriteValue(TEXT("room_floor_crossing_z"), static_cast<double>(Endpoint.RoomFloorCrossingZ));
Writer->WriteValue(TEXT("floor_join_delta_voxels"), static_cast<double>(Endpoint.FloorJoinDeltaVoxels));
WriteTunnelMouthProbeAxis(*Writer, Endpoint.Wall, TEXT("wall"));
WriteTunnelMouthProbeAxis(*Writer, Endpoint.Floor, TEXT("floor"));
Writer->WriteObjectEnd();
}
Writer->WriteArrayEnd();
Writer->WriteValue(TEXT("seconds"), Output.TunnelCoreProbe.Mouth.Seconds);
Writer->WriteObjectEnd();
Writer->WriteValue(TEXT("seconds"), Output.TunnelCoreProbe.Seconds); Writer->WriteValue(TEXT("seconds"), Output.TunnelCoreProbe.Seconds);
Writer->WriteObjectEnd(); Writer->WriteObjectEnd();