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

@@ -1608,6 +1608,61 @@ struct FExploreTunnelCoreProbeAxis
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
{
FString Status;
@@ -1634,6 +1689,7 @@ struct FExploreTunnelCoreProbeOutput
FExploreTunnelCoreProbeAxis Floor;
FExploreTunnelCoreProbeAxis Wall;
FExploreTunnelCoreProbeAxis Ceiling;
FExploreTunnelMouthProbeOutput Mouth;
double Seconds = 0.0;
};
@@ -4869,7 +4925,8 @@ void ComputeTunnelCoreProbeMetrics(
float& OutPeriodicAmplitudeVoxels,
float& OutPeriodicPeriodVoxels,
int32& OutPeriodicMeasuredSamples,
float& OutPeriodicMeasuredLengthVoxels)
float& OutPeriodicMeasuredLengthVoxels,
bool bPolynomialTrend = false)
{
OutValidCount = 0;
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
// the 1..16 voxel rib band, so a genuine rib train survives while a single mouth transition
// or route slope follows the local baseline. Discarding the half-window at each edge keeps
// the asymmetric endpoint windows out of the spectral result.
// the 1..16 voxel rib band, so a genuine rib train survives while a single tunnel transition
// or route slope follows the local baseline. The mouth probe uses a degree-six fit instead: the
// 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;
const int32 HalfWindowSamples = FMath::Max(
1, FMath::RoundToInt(0.5f * TrendWindowVoxels / StepVoxels));
const int32 AnalysisStart = BestRunStart + HalfWindowSamples;
const int32 AnalysisEnd = BestRunEnd - HalfWindowSamples;
const int32 AnalysisStart = bPolynomialTrend
? BestRunStart : BestRunStart + HalfWindowSamples;
const int32 AnalysisEnd = bPolynomialTrend
? BestRunEnd : BestRunEnd - HalfWindowSamples;
const int32 AnalysisCount = AnalysisEnd - AnalysisStart + 1;
if (AnalysisCount < 4)
{
@@ -4968,6 +5028,110 @@ void ComputeTunnelCoreProbeMetrics(
OutPeriodicMeasuredSamples = AnalysisCount;
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;
const int32 MinRibFrequency = FMath::Max(
1, FMath::CeilToInt(StepVoxels * static_cast<float>(AnalysisCount)
@@ -4981,22 +5145,27 @@ void ComputeTunnelCoreProbeMetrics(
double Imaginary = 0.0;
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;
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)
* static_cast<double>(AnalysisIndex) / static_cast<double>(AnalysisCount);
Real += Residual * FMath::Cos(static_cast<float>(Angle));
@@ -5017,6 +5186,31 @@ void ComputeTunnelCoreProbeMetrics(
: 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(
const FExploreArguments& Arguments,
FExploreWorld& World,
@@ -5661,7 +5855,301 @@ bool RunTunnelCoreProbe(
Axis->ValidCoreCrossings, Axis->CorePeriodicAmplitudeVoxels,
Axis->CorePeriodicAmplitudeVoxels * 25.0f,
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);
@@ -5718,6 +6206,27 @@ void WriteTunnelCoreProbeAxis(
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)
{
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_max"), static_cast<double>(Output.TunnelCoreProbe.FinalCenterMax));
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.Wall, TEXT("wall"));
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->WriteObjectEnd();