fix(passages): skip proofs cover the new descent geometry -- soundness PASS; passage tiles +28% cost

- Passage proof bounds now enclose everything the passage code can write: separated switchback
  legs, outward door approaches, native floor slabs with a derived relief supremum, and the final
  body-envelope carve (segment XY extent + half-width, floor range + player height + step).
- ClassifyTile: the cave-only AllSolid shortcut could restore a solid verdict after a passage
  carve candidate was found; it now keeps the tile mixed.
- Descent corridor test asserts no corridor stencil tile gets a uniform verdict.

Evidence: SealedSolidProofSoundness and ClassifyTileSoundness pass; 64 owner seeds, 0 body
blockers, 0 uniform stencil tiles, CRCs match. Negative control (passage reach shrunk to 4 voxels):
7 sealed-solid proofs among 620 corridor tiles, i.e. the check bites.

KNOWN COST, not fixed: tile profile, owner world, seed 0, 15 s window: passage-candidate tiles
83.6 ms (de05e29) -> 107.2 ms (+28%), all tiles +5.4%. Two segment-rejection filters did not help
and were removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-09-28 10:03:34 +02:00
1 parent fb052716c6
commit b4ac9969f5
3 files changed
+444 -43

No files matched your search

@@ -99,6 +99,11 @@ namespace
TArray<int32> SampleStations; TArray<int32> SampleStations;
TArray<int32> SampleLaterals; TArray<int32> SampleLaterals;
TArray<int32> SampleHeights; TArray<int32> SampleHeights;
TSet<FIntVector> ProofStencilTileOrigins;
int32 ProofStencilUniformTiles = 0;
int32 ProofStencilSealedTiles = 0;
int32 ProofStencilOutOfLayoutAirTiles = 0;
int32 ProofStencilClassifyTiles = 0;
int32 SolidSamples = 0; int32 SolidSamples = 0;
int32 BlockingSolidSamples = 0; int32 BlockingSolidSamples = 0;
int32 SolidStations = 0; int32 SolidStations = 0;
@@ -214,6 +219,41 @@ namespace
(2.0f * FloorProbeHalfRange) / static_cast<float>(FloorProbeIntervals); (2.0f * FloorProbeHalfRange) / static_cast<float>(FloorProbeIntervals);
constexpr float FirstBodySampleZ = 0.25f; constexpr float FirstBodySampleZ = 0.25f;
constexpr float BodySampleSpacing = 0.5f; constexpr float BodySampleSpacing = 0.5f;
OutSamples.ProofStencilTileOrigins.Reset();
OutSamples.ProofStencilUniformTiles = 0;
OutSamples.ProofStencilSealedTiles = 0;
OutSamples.ProofStencilOutOfLayoutAirTiles = 0;
OutSamples.ProofStencilClassifyTiles = 0;
const auto AddStencilTilesAtPosition = [&OutSamples](const FVector& Position)
{
int32 AxisStarts[3][2] = {};
int32 AxisCounts[3] = {};
const double Coordinates[3] = { Position.X, Position.Y, Position.Z };
for (int32 Axis = 0; Axis < 3; ++Axis)
{
const int32 TileIndex = FMath::FloorToInt(
Coordinates[Axis] / static_cast<double>(CHUNK_SIZE));
AxisStarts[Axis][0] = TileIndex * CHUNK_SIZE;
AxisCounts[Axis] = 1;
// MC tiles share their high-face samples. Include both tiles when a stencil
// position lies exactly on a 32-voxel boundary.
if (Coordinates[Axis]
== static_cast<double>(AxisStarts[Axis][0]))
{
AxisStarts[Axis][1] = AxisStarts[Axis][0] - CHUNK_SIZE;
AxisCounts[Axis] = 2;
}
}
for (int32 XIndex = 0; XIndex < AxisCounts[0]; ++XIndex)
for (int32 YIndex = 0; YIndex < AxisCounts[1]; ++YIndex)
for (int32 ZIndex = 0; ZIndex < AxisCounts[2]; ++ZIndex)
{
OutSamples.ProofStencilTileOrigins.Add(FIntVector(
AxisStarts[0][XIndex],
AxisStarts[1][YIndex],
AxisStarts[2][ZIndex]));
}
};
TArray<FVector> FeetStations; TArray<FVector> FeetStations;
TArray<FVector> LateralAxes; TArray<FVector> LateralAxes;
TArray<float> ExpectedFloorZ; TArray<float> ExpectedFloorZ;
@@ -344,6 +384,13 @@ namespace
} }
MeasuredFloorZ[FloorIndex] = BestFloor; MeasuredFloorZ[FloorIndex] = BestFloor;
FVector FloorStencilPosition = FeetStations[StationIndex]
+ LateralAxes[StationIndex]
* (-PlayerHalfWidthVoxels
+ static_cast<float>(LateralIndex)
* LateralSampleSpacing);
FloorStencilPosition.Z = BestFloor;
AddStencilTilesAtPosition(FloorStencilPosition);
if (BestDistance != FLT_MAX) if (BestDistance != FLT_MAX)
{ {
bHasMeasuredFloor[FloorIndex] = 1; bHasMeasuredFloor[FloorIndex] = 1;
@@ -515,8 +562,10 @@ namespace
0.0f)); 0.0f));
const float BodySide = SliceRadius const float BodySide = SliceRadius
* (static_cast<float>(LateralIndex) / 2.0f - 1.0f); * (static_cast<float>(LateralIndex) / 2.0f - 1.0f);
OutSamples.Positions.Add( const FVector BodyPosition =
Feet + Lateral * BodySide + FVector(0.0f, 0.0f, Height)); Feet + Lateral * BodySide + FVector(0.0f, 0.0f, Height);
OutSamples.Positions.Add(BodyPosition);
AddStencilTilesAtPosition(BodyPosition);
OutSamples.SampleStations.Add(StationIndex); OutSamples.SampleStations.Add(StationIndex);
OutSamples.SampleLaterals.Add(LateralIndex); OutSamples.SampleLaterals.Add(LateralIndex);
OutSamples.SampleHeights.Add(VerticalIndex); OutSamples.SampleHeights.Add(VerticalIndex);
@@ -631,7 +680,8 @@ namespace
bool SampleOwnerSeed( bool SampleOwnerSeed(
int32 Seed, int32 Seed,
FWalkCorridorSamples& OutSamples, FWalkCorridorSamples& OutSamples,
FString& OutError) FString& OutError,
bool bCheckProofStencil = false)
{ {
FOwnerPassageWorld World; FOwnerPassageWorld World;
if (!World.Build(Seed, OutError)) if (!World.Build(Seed, OutError))
@@ -642,6 +692,24 @@ namespace
{ {
return false; return false;
} }
if (bCheckProofStencil)
{
for (const FIntVector& Origin : OutSamples.ProofStencilTileOrigins)
{
const bool bSealedSolid = World.Generator->TryProveSealedSolidTile(
Origin, 1, CHUNK_SIZE);
const bool bOutOfLayoutAir = World.Generator->TryProveOutOfLayoutAirTile(
Origin, 1, CHUNK_SIZE);
const EVoxelTileClass Verdict = World.Generator->ClassifyTile(
Origin, 1, CHUNK_SIZE);
const bool bUniformClassify = Verdict != EVoxelTileClass::Mixed;
OutSamples.ProofStencilSealedTiles += bSealedSolid ? 1 : 0;
OutSamples.ProofStencilOutOfLayoutAirTiles += bOutOfLayoutAir ? 1 : 0;
OutSamples.ProofStencilClassifyTiles += bUniformClassify ? 1 : 0;
OutSamples.ProofStencilUniformTiles +=
(bSealedSolid || bOutOfLayoutAir || bUniformClassify) ? 1 : 0;
}
}
if (Seed == 0 && OutSamples.SolidSamples > 0) if (Seed == 0 && OutSamples.SolidSamples > 0)
{ {
const FIntVector ChunkCoord( const FIntVector ChunkCoord(
@@ -1093,6 +1161,11 @@ bool FVoxelForgeDescentCorridorSeedSweepTest::RunTest(const FString& Parameters)
int64 TotalBlockingSolidSamples = 0; int64 TotalBlockingSolidSamples = 0;
int64 TotalWidthFloorGaps = 0; int64 TotalWidthFloorGaps = 0;
int64 TotalSamples = 0; int64 TotalSamples = 0;
int64 TotalProofStencilTiles = 0;
int64 TotalProofStencilUniformTiles = 0;
int64 TotalProofStencilSealedTiles = 0;
int64 TotalProofStencilOutOfLayoutAirTiles = 0;
int64 TotalProofStencilClassifyTiles = 0;
int32 FirstFailingSeed = INDEX_NONE; int32 FirstFailingSeed = INDEX_NONE;
FWalkCorridorSamples Worst; FWalkCorridorSamples Worst;
FString WorstError; FString WorstError;
@@ -1101,13 +1174,18 @@ bool FVoxelForgeDescentCorridorSeedSweepTest::RunTest(const FString& Parameters)
{ {
FWalkCorridorSamples Samples; FWalkCorridorSamples Samples;
FString Error; FString Error;
if (!SampleOwnerSeed(Seed, Samples, Error)) if (!SampleOwnerSeed(Seed, Samples, Error, /*bCheckProofStencil=*/true))
{ {
AddError(Error); AddError(Error);
return false; return false;
} }
LogCorridorResult(*this, Seed, Samples); LogCorridorResult(*this, Seed, Samples);
TotalSamples += Samples.Densities.Num(); TotalSamples += Samples.Densities.Num();
TotalProofStencilTiles += Samples.ProofStencilTileOrigins.Num();
TotalProofStencilUniformTiles += Samples.ProofStencilUniformTiles;
TotalProofStencilSealedTiles += Samples.ProofStencilSealedTiles;
TotalProofStencilOutOfLayoutAirTiles += Samples.ProofStencilOutOfLayoutAirTiles;
TotalProofStencilClassifyTiles += Samples.ProofStencilClassifyTiles;
TotalSolidSamples += Samples.SolidSamples; TotalSolidSamples += Samples.SolidSamples;
TotalBlockingSolidSamples += Samples.BlockingSolidSamples; TotalBlockingSolidSamples += Samples.BlockingSolidSamples;
TotalWidthFloorGaps += Samples.WidthFloorGaps; TotalWidthFloorGaps += Samples.WidthFloorGaps;
@@ -1169,9 +1247,19 @@ bool FVoxelForgeDescentCorridorSeedSweepTest::RunTest(const FString& Parameters)
static_cast<long long>(TotalWidthFloorGaps), static_cast<long long>(TotalSamples), static_cast<long long>(TotalWidthFloorGaps), static_cast<long long>(TotalSamples),
FirstFailingSeed, WorstError.IsEmpty() ? TEXT("none") : *WorstError, FirstFailingSeed, WorstError.IsEmpty() ? TEXT("none") : *WorstError,
Worst.BlockingSolidSamples); Worst.BlockingSolidSamples);
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeDescentProofStencilSweep] seeds=%d tiles_checked=%lld "
"uniform_tiles=%lld sealed_solid=%lld out_of_layout_air=%lld classify_uniform=%lld"),
SeedCount, static_cast<long long>(TotalProofStencilTiles),
static_cast<long long>(TotalProofStencilUniformTiles),
static_cast<long long>(TotalProofStencilSealedTiles),
static_cast<long long>(TotalProofStencilOutOfLayoutAirTiles),
static_cast<long long>(TotalProofStencilClassifyTiles));
TestEqual(TEXT("All 64 owner seeds have a clear, walkable A-to-B body corridor"), TestEqual(TEXT("All 64 owner seeds have a clear, walkable A-to-B body corridor"),
SeedsWithWalkFailures, 0); SeedsWithWalkFailures, 0);
return SeedsWithWalkFailures == 0; TestEqual(TEXT("No corridor stencil tile is proven uniform by either proof or ClassifyTile"),
TotalProofStencilUniformTiles, static_cast<int64>(0));
return SeedsWithWalkFailures == 0 && TotalProofStencilUniformTiles == 0;
} }
#endif // WITH_DEV_AUTOMATION_TESTS #endif // WITH_DEV_AUTOMATION_TESTS
+8 -6
View File
@@ -7107,8 +7107,10 @@ EVoxelTileClass UVoxelGenerator::ClassifyTileUncached(
// ── Structural passage/origin guards for non-open-air categories. ── // ── Structural passage/origin guards for non-open-air categories. ──
// Carvers kill AllSolid but never add rock; landing and origin support floors kill AllAir. // Carvers kill AllSolid but never add rock; landing and origin support floors kill AllAir.
if (StrateManager->AnyPassageNearLattice( const bool bHasPassageCarvingCandidate =
TileVoxelBox, OriginVoxels, Step)) StrateManager->AnyPassageNearLattice(
TileVoxelBox, OriginVoxels, Step);
if (bHasPassageCarvingCandidate)
{ {
bCanSolid = false; bCanSolid = false;
} }
@@ -7355,10 +7357,10 @@ EVoxelTileClass UVoxelGenerator::ClassifyTileUncached(
&& CaveMinZ == MinZ && CaveMaxZ == MaxZ; && CaveMinZ == MinZ && CaveMaxZ == MaxZ;
if (bCaveOnlyTile && StackVerdict == EVoxelTileClass::AllSolid) if (bCaveOnlyTile && StackVerdict == EVoxelTileClass::AllSolid)
{ {
// The stack verdict covers the complete cave-only MC lattice. Its exact structural // The operator stack does not own the final passage carve. Keep
// post path was supplied above, so a broad conservative passage guard must not force // its AllSolid shortcut only when the exact-lattice passage guard
// this already-certified solid tile through GenerateMesh. // found no modifier or body-envelope write in this tile.
bCanSolid = true; bCanSolid = !bHasPassageCarvingCandidate;
bCanAir = false; bCanAir = false;
} }
else if (bCaveOnlyTile && StackVerdict == EVoxelTileClass::AllAir) else if (bCaveOnlyTile && StackVerdict == EVoxelTileClass::AllAir)
+343 -32
View File
@@ -687,6 +687,74 @@ namespace
&& OutSupportRadius > 0.0f; && OutSupportRadius > 0.0f;
} }
// FMath::PerlinNoise2D is in (-1, 1); the two positive octave weights sum to one,
// SmoothStep01 keeps the apron envelope in [0, 1], and each segment stores the scale
// actually consumed by VF_ProjectNativePassageFloorSegment. This is the write supremum,
// including a small float-rounding allowance at the authored floor's world Z.
static bool VF_GetNativePassageFloorReliefBound(
const FVoxelPassage& Passage, float& OutReliefBound)
{
OutReliefBound = 0.0f;
if (!Passage.bNativeFloorEnabled)
{
return true;
}
if (Passage.ControlPoints.Num() < 2
|| Passage.ControlRadii.Num() != Passage.ControlPoints.Num()
|| Passage.NativeFloorProfileZ.Num() != Passage.ControlPoints.Num()
|| Passage.ControlPoints.Num() > 4096)
{
return false;
}
float MaxAbsFloorZ = 0.0f;
for (int32 Index = 0; Index < Passage.ControlPoints.Num(); ++Index)
{
if (!VoxelMath::IsFinite(Passage.NativeFloorProfileZ[Index]))
{
return false;
}
MaxAbsFloorZ = FMath::Max(
MaxAbsFloorZ, FMath::Abs(Passage.NativeFloorProfileZ[Index]));
}
// The relief helper returns exactly zero for these inputs.
if (!(Passage.NativeFloorReliefStrength > 0.0f)
|| !VoxelMath::IsFinite(Passage.NativeFloorReliefStrength)
|| !VoxelMath::IsFinite(Passage.NativeFloorReliefFrequency)
|| !(FMath::Abs(Passage.NativeFloorReliefFrequency) > KINDA_SMALL_NUMBER))
{
return true;
}
float MaxAmplitude = 0.0f;
for (int32 SegmentIndex = 0;
SegmentIndex + 1 < Passage.ControlPoints.Num(); ++SegmentIndex)
{
const float ReliefScale =
Passage.NativeFloorReliefScales.IsValidIndex(SegmentIndex)
? Passage.NativeFloorReliefScales[SegmentIndex] : 0.0f;
if (!VoxelMath::IsFinite(ReliefScale))
{
return false;
}
const float Amplitude =
FMath::Abs(Passage.NativeFloorReliefStrength)
* VOXEL_NOISE_SCALE * FMath::Abs(ReliefScale);
if (!VoxelMath::IsFinite(Amplitude))
{
return false;
}
MaxAmplitude = FMath::Max(MaxAmplitude, Amplitude);
}
const float RoundingAllowance =
8.0f * FLT_EPSILON * FMath::Max(1.0f, MaxAbsFloorZ + MaxAmplitude)
+ KINDA_SMALL_NUMBER;
OutReliefBound = MaxAmplitude + RoundingAllowance;
return VoxelMath::IsFinite(OutReliefBound);
}
static bool VF_ProjectNativePassageFloorUncached( static bool VF_ProjectNativePassageFloorUncached(
const FVoxelPassage& Passage, const FVector& Position, const FVoxelPassage& Passage, const FVector& Position,
float& OutFloorZ, float& OutSupportRadius, float& OutFloorZ, float& OutSupportRadius,
@@ -2410,6 +2478,16 @@ void UVoxelStrateManager::GeneratePassages()
BoundsMax.Y = FMath::Max(BoundsMax.Y, Point.Y + Pad); BoundsMax.Y = FMath::Max(BoundsMax.Y, Point.Y + Pad);
BoundsMax.Z = FMath::Max(BoundsMax.Z, Point.Z + Pad); BoundsMax.Z = FMath::Max(BoundsMax.Z, Point.Z + Pad);
}; };
auto IncludeBounds = [&BoundsMin, &BoundsMax](
const FVector& Min, const FVector& Max)
{
BoundsMin.X = FMath::Min(BoundsMin.X, Min.X);
BoundsMin.Y = FMath::Min(BoundsMin.Y, Min.Y);
BoundsMin.Z = FMath::Min(BoundsMin.Z, Min.Z);
BoundsMax.X = FMath::Max(BoundsMax.X, Max.X);
BoundsMax.Y = FMath::Max(BoundsMax.Y, Max.Y);
BoundsMax.Z = FMath::Max(BoundsMax.Z, Max.Z);
};
for (const FVector& CP : Passage.ControlPoints) for (const FVector& CP : Passage.ControlPoints)
{ {
IncludePoint(CP, Passage.Radius + 4.0f); IncludePoint(CP, Passage.Radius + 4.0f);
@@ -2430,6 +2508,109 @@ void UVoxelStrateManager::GeneratePassages()
IncludeLanding(Passage.UpperLanding); IncludeLanding(Passage.UpperLanding);
IncludeLanding(Passage.LowerLanding); IncludeLanding(Passage.LowerLanding);
// These two MC writers run after the tunnel SDF: the native support slab uses
// the relieved floor profile, and the final corridor air envelope uses each
// segment's own relief branch plus the door-to-standing approaches. Include the
// actual write suprema so the chunk shortlist and all sphere-based proofs remain
// conservative even when the authored relief exceeds the old one-voxel reserve.
if (Passage.bWalkableTunnelContract && Passage.bNativeFloorEnabled)
{
constexpr float CorridorHalfWidth =
0.5f * VoxelPassageGeometry::WalkCorridorPlayerWidthVoxels
+ VoxelPassageGeometry::WalkCorridorLatticeMarginVoxels;
constexpr float CorridorBottom =
VoxelPassageGeometry::MaxStepHeightVoxels;
constexpr float CorridorTop = CorridorBottom
+ VoxelPassageGeometry::WalkCorridorPlayerHeightVoxels;
float ReliefBound = 0.0f;
if (VF_GetNativePassageFloorReliefBound(Passage, ReliefBound)
&& Passage.ControlPoints.Num() >= 2
&& Passage.ControlRadii.Num() == Passage.ControlPoints.Num())
{
for (int32 SegmentIndex = 0;
SegmentIndex + 1 < Passage.ControlPoints.Num();
++SegmentIndex)
{
const FVector& A = Passage.ControlPoints[SegmentIndex];
const FVector& B = Passage.ControlPoints[SegmentIndex + 1];
const float RadiusA =
FMath::Abs(Passage.ControlRadii[SegmentIndex]);
const float RadiusB =
FMath::Abs(Passage.ControlRadii[SegmentIndex + 1]);
const float FloorA =
Passage.NativeFloorProfileZ[SegmentIndex];
const float FloorB =
Passage.NativeFloorProfileZ[SegmentIndex + 1];
if (!VoxelMath::IsFinite(A.X) || !VoxelMath::IsFinite(A.Y)
|| !VoxelMath::IsFinite(B.X) || !VoxelMath::IsFinite(B.Y)
|| !VoxelMath::IsFinite(FloorA)
|| !VoxelMath::IsFinite(FloorB))
{
continue;
}
const float SupportRadius =
VoxelPassageGeometry::NativePassageFloorSupportRadius(
RadiusA, RadiusB);
IncludeBounds(
FVector(
FMath::Min(A.X, B.X) - SupportRadius,
FMath::Min(A.Y, B.Y) - SupportRadius,
FMath::Min(FloorA, FloorB) - ReliefBound
- VoxelPassageGeometry::LandingFloorThicknessVoxels),
FVector(
FMath::Max(A.X, B.X) + SupportRadius,
FMath::Max(A.Y, B.Y) + SupportRadius,
FMath::Max(FloorA, FloorB) + ReliefBound
+ KINDA_SMALL_NUMBER));
IncludeBounds(
FVector(
FMath::Min(A.X, B.X) - CorridorHalfWidth,
FMath::Min(A.Y, B.Y) - CorridorHalfWidth,
FMath::Min(FloorA, FloorB) + CorridorBottom
- ReliefBound),
FVector(
FMath::Max(A.X, B.X) + CorridorHalfWidth,
FMath::Max(A.Y, B.Y) + CorridorHalfWidth,
FMath::Max(FloorA, FloorB) + CorridorTop
+ ReliefBound));
}
}
for (const FVoxelPassageLanding* Landing : {
&Passage.UpperLanding, &Passage.LowerLanding })
{
if (!VoxelMath::IsFinite(Landing->DoorPoint.X)
|| !VoxelMath::IsFinite(Landing->DoorPoint.Y)
|| !VoxelMath::IsFinite(Landing->StandingPoint.X)
|| !VoxelMath::IsFinite(Landing->StandingPoint.Y)
|| !VoxelMath::IsFinite(Landing->FloorZ))
{
continue;
}
const float LengthSquared = FVector2D::DistSquared(
FVector2D(Landing->DoorPoint.X, Landing->DoorPoint.Y),
FVector2D(Landing->StandingPoint.X, Landing->StandingPoint.Y));
if (!(LengthSquared > KINDA_SMALL_NUMBER)
|| !VoxelMath::IsFinite(LengthSquared))
{
continue;
}
IncludeBounds(
FVector(
FMath::Min(Landing->DoorPoint.X, Landing->StandingPoint.X)
- CorridorHalfWidth,
FMath::Min(Landing->DoorPoint.Y, Landing->StandingPoint.Y)
- CorridorHalfWidth,
Landing->FloorZ + CorridorBottom),
FVector(
FMath::Max(Landing->DoorPoint.X, Landing->StandingPoint.X)
+ CorridorHalfWidth,
FMath::Max(Landing->DoorPoint.Y, Landing->StandingPoint.Y)
+ CorridorHalfWidth,
Landing->FloorZ + CorridorTop));
}
}
const FVector Center = (BoundsMin + BoundsMax) * 0.5f; const FVector Center = (BoundsMin + BoundsMax) * 0.5f;
float MaxDistSq = 0.0f; float MaxDistSq = 0.0f;
MaxDistSq = FMath::Max(MaxDistSq, (float)FVector::DistSquared(Center, BoundsMin)); MaxDistSq = FMath::Max(MaxDistSq, (float)FVector::DistSquared(Center, BoundsMin));
@@ -3399,6 +3580,11 @@ bool UVoxelStrateManager::AnyPassageLandingFloorNearBox(
// a tile; a false negative could classify an air tile uniformly and remove the player's floor. // a tile; a false negative could classify an air tile uniformly and remove the player's floor.
for (const FVoxelPassage& Passage : Passages) for (const FVoxelPassage& Passage : Passages)
{ {
float ReliefBound = 0.0f;
if (!VF_GetNativePassageFloorReliefBound(Passage, ReliefBound))
{
return true;
}
const FVoxelPassageLanding* Landings[] = { const FVoxelPassageLanding* Landings[] = {
&Passage.UpperLanding, &Passage.LowerLanding }; &Passage.UpperLanding, &Passage.LowerLanding };
for (const FVoxelPassageLanding* Landing : Landings) for (const FVoxelPassageLanding* Landing : Landings)
@@ -3426,10 +3612,13 @@ bool UVoxelStrateManager::AnyPassageLandingFloorNearBox(
// The default tube also owns a three-voxel support band. Its conservative segment AABB // The default tube also owns a three-voxel support band. Its conservative segment AABB
// keeps a classifier from proving AllAir over the floor and then dropping that support // keeps a classifier from proving AllAir over the floor and then dropping that support
// during meshing. False positives are intentional; false negatives would be a hole. // during meshing. False positives are intentional; false negatives would be a hole.
if (Passage.bWalkableTunnelContract if (Passage.bWalkableTunnelContract)
&& Passage.ControlPoints.Num() >= 2
&& Passage.ControlRadii.Num() == Passage.ControlPoints.Num())
{ {
if (Passage.ControlPoints.Num() < 2
|| Passage.ControlRadii.Num() != Passage.ControlPoints.Num())
{
return true;
}
constexpr float Pad = 1.0f; constexpr float Pad = 1.0f;
for (int32 SegmentIndex = 0; for (int32 SegmentIndex = 0;
SegmentIndex + 1 < Passage.ControlPoints.Num(); SegmentIndex + 1 < Passage.ControlPoints.Num();
@@ -3437,22 +3626,29 @@ bool UVoxelStrateManager::AnyPassageLandingFloorNearBox(
{ {
const FVector& A = Passage.ControlPoints[SegmentIndex]; const FVector& A = Passage.ControlPoints[SegmentIndex];
const FVector& B = Passage.ControlPoints[SegmentIndex + 1]; const FVector& B = Passage.ControlPoints[SegmentIndex + 1];
const float SupportRadius = FMath::Max( if (!VoxelMath::IsFinite(A.X) || !VoxelMath::IsFinite(A.Y)
FMath::Min( || !VoxelMath::IsFinite(A.Z) || !VoxelMath::IsFinite(B.X)
FMath::Abs(Passage.ControlRadii[SegmentIndex]), || !VoxelMath::IsFinite(B.Y) || !VoxelMath::IsFinite(B.Z)
FMath::Abs(Passage.ControlRadii[SegmentIndex + 1])) - 0.5f, || !VoxelMath::IsFinite(Passage.ControlRadii[SegmentIndex])
VoxelPassageGeometry::PlayerRadiusVoxels); || !VoxelMath::IsFinite(Passage.ControlRadii[SegmentIndex + 1]))
const float FloorMinZ = FMath::Min( {
VoxelPassageGeometry::TunnelFloorZ( return true;
A, Passage.ControlRadii[SegmentIndex]), }
VoxelPassageGeometry::TunnelFloorZ( const float RadiusA = FMath::Abs(Passage.ControlRadii[SegmentIndex]);
B, Passage.ControlRadii[SegmentIndex + 1])) const float RadiusB = FMath::Abs(Passage.ControlRadii[SegmentIndex + 1]);
- VoxelPassageGeometry::LandingFloorThicknessVoxels - Pad; const float SupportRadius =
const float FloorMaxZ = FMath::Max( VoxelPassageGeometry::NativePassageFloorSupportRadius(RadiusA, RadiusB);
VoxelPassageGeometry::TunnelFloorZ( const float FloorA = Passage.bNativeFloorEnabled
A, Passage.ControlRadii[SegmentIndex]), ? Passage.NativeFloorProfileZ[SegmentIndex]
VoxelPassageGeometry::TunnelFloorZ( : VoxelPassageGeometry::TunnelFloorZ(A, RadiusA);
B, Passage.ControlRadii[SegmentIndex + 1])) + Pad; const float FloorB = Passage.bNativeFloorEnabled
? Passage.NativeFloorProfileZ[SegmentIndex + 1]
: VoxelPassageGeometry::TunnelFloorZ(B, RadiusB);
const float FloorMinZ = FMath::Min(FloorA, FloorB)
- VoxelPassageGeometry::LandingFloorThicknessVoxels
- Pad - ReliefBound;
const float FloorMaxZ = FMath::Max(FloorA, FloorB)
+ Pad + ReliefBound;
const float FloorMinX = FMath::Min(A.X, B.X) - SupportRadius - Pad; const float FloorMinX = FMath::Min(A.X, B.X) - SupportRadius - Pad;
const float FloorMaxX = FMath::Max(A.X, B.X) + SupportRadius + Pad; const float FloorMaxX = FMath::Max(A.X, B.X) + SupportRadius + Pad;
const float FloorMinY = FMath::Min(A.Y, B.Y) - SupportRadius - Pad; const float FloorMinY = FMath::Min(A.Y, B.Y) - SupportRadius - Pad;
@@ -4255,6 +4451,106 @@ namespace
FMath::Clamp(Center.Z, (float)Box.Min.Z, (float)Box.Max.Z)); FMath::Clamp(Center.Z, (float)Box.Min.Z, (float)Box.Max.Z));
return FVector::DistSquared(Closest, Center) <= Radius * Radius; return FVector::DistSquared(Closest, Center) <= Radius * Radius;
} }
static bool VF_WalkCorridorBodyCarveTouchesLattice(
const TArray<FVoxelPassage>& Passages,
const FBox& VoxelBox, const FIntVector& LatticeOrigin, int32 Step)
{
if (Step <= 0 || !VoxelBox.IsValid)
{
return true;
}
constexpr float CorridorHalfWidth =
0.5f * VoxelPassageGeometry::WalkCorridorPlayerWidthVoxels
+ VoxelPassageGeometry::WalkCorridorLatticeMarginVoxels;
constexpr float CorridorBottom = VoxelPassageGeometry::MaxStepHeightVoxels;
constexpr float CorridorTop = CorridorBottom
+ VoxelPassageGeometry::WalkCorridorPlayerHeightVoxels;
for (const FVoxelPassage& Passage : Passages)
{
if (!Passage.bWalkableTunnelContract || !Passage.bNativeFloorEnabled)
{
continue;
}
float ReliefBound = 0.0f;
if (!VF_GetNativePassageFloorReliefBound(Passage, ReliefBound))
{
return true;
}
if (Passage.ControlPoints.Num() < 2
|| Passage.ControlRadii.Num() != Passage.ControlPoints.Num())
{
return true;
}
for (int32 SegmentIndex = 0;
SegmentIndex + 1 < Passage.ControlPoints.Num(); ++SegmentIndex)
{
const FVector& A = Passage.ControlPoints[SegmentIndex];
const FVector& B = Passage.ControlPoints[SegmentIndex + 1];
const float FloorA = Passage.NativeFloorProfileZ[SegmentIndex];
const float FloorB = Passage.NativeFloorProfileZ[SegmentIndex + 1];
if (!VoxelMath::IsFinite(A.X) || !VoxelMath::IsFinite(A.Y)
|| !VoxelMath::IsFinite(B.X) || !VoxelMath::IsFinite(B.Y)
|| !VoxelMath::IsFinite(FloorA) || !VoxelMath::IsFinite(FloorB))
{
return true;
}
if (VF_LatticeBoxTouchesAABB(
VoxelBox, LatticeOrigin, Step,
FMath::Min(A.X, B.X) - CorridorHalfWidth,
FMath::Max(A.X, B.X) + CorridorHalfWidth,
FMath::Min(A.Y, B.Y) - CorridorHalfWidth,
FMath::Max(A.Y, B.Y) + CorridorHalfWidth,
FMath::Min(FloorA, FloorB) + CorridorBottom - ReliefBound,
FMath::Max(FloorA, FloorB) + CorridorTop + ReliefBound))
{
return true;
}
}
for (const FVoxelPassageLanding* Landing : {
&Passage.UpperLanding, &Passage.LowerLanding })
{
if (!VoxelMath::IsFinite(Landing->DoorPoint.X)
|| !VoxelMath::IsFinite(Landing->DoorPoint.Y)
|| !VoxelMath::IsFinite(Landing->StandingPoint.X)
|| !VoxelMath::IsFinite(Landing->StandingPoint.Y)
|| !VoxelMath::IsFinite(Landing->FloorZ))
{
return true;
}
const float LengthSquared = FVector2D::DistSquared(
FVector2D(Landing->DoorPoint.X, Landing->DoorPoint.Y),
FVector2D(Landing->StandingPoint.X, Landing->StandingPoint.Y));
if (!VoxelMath::IsFinite(LengthSquared))
{
return true;
}
if (LengthSquared <= KINDA_SMALL_NUMBER)
{
continue;
}
if (VF_LatticeBoxTouchesAABB(
VoxelBox, LatticeOrigin, Step,
FMath::Min(Landing->DoorPoint.X, Landing->StandingPoint.X)
- CorridorHalfWidth,
FMath::Max(Landing->DoorPoint.X, Landing->StandingPoint.X)
+ CorridorHalfWidth,
FMath::Min(Landing->DoorPoint.Y, Landing->StandingPoint.Y)
- CorridorHalfWidth,
FMath::Max(Landing->DoorPoint.Y, Landing->StandingPoint.Y)
+ CorridorHalfWidth,
Landing->FloorZ + CorridorBottom,
Landing->FloorZ + CorridorTop))
{
return true;
}
}
}
return false;
}
} }
bool UVoxelStrateManager::AnyPassageNearLattice( bool UVoxelStrateManager::AnyPassageNearLattice(
@@ -4265,17 +4561,20 @@ bool UVoxelStrateManager::AnyPassageNearLattice(
// zero carve factor. That was the LOD0 false-Mixed path. Keep the cheap domain test as the // zero carve factor. That was the LOD0 false-Mixed path. Keep the cheap domain test as the
// common reject, then use the same exact lattice factor that MaxCarveOverBox consumes. The // common reject, then use the same exact lattice factor that MaxCarveOverBox consumes. The
// manager cache makes the second call in the fold free for the same box. // manager cache makes the second call in the fold free for the same box.
const bool bBodyCarveCandidate = VF_WalkCorridorBodyCarveTouchesLattice(
Passages, VoxelBox, LatticeOrigin, Step);
if (!VF_AnyPassageModifierDomainTouchesLattice( if (!VF_AnyPassageModifierDomainTouchesLattice(
Passages, VoxelBox, LatticeOrigin, Step)) Passages, VoxelBox, LatticeOrigin, Step))
{ {
return false; return bBodyCarveCandidate;
} }
const float MaxFactor = MaxPassageCarveFactorNearLattice( const float MaxFactor = MaxPassageCarveFactorNearLattice(
VoxelBox, LatticeOrigin, Step); VoxelBox, LatticeOrigin, Step);
// Invalid input deliberately returns 1.0 from the exact helper. A non-finite result is also // Invalid input deliberately returns 1.0 from the exact helper. A non-finite result is also
// retained as a candidate rather than becoming an identity proof. // retained as a candidate rather than becoming an identity proof.
return !VoxelMath::IsFinite(MaxFactor) || MaxFactor > 0.0f; return !VoxelMath::IsFinite(MaxFactor) || MaxFactor > 0.0f
|| bBodyCarveCandidate;
} }
bool UVoxelStrateManager::AnyPassageAirPostNearLattice( bool UVoxelStrateManager::AnyPassageAirPostNearLattice(
@@ -4296,6 +4595,11 @@ bool UVoxelStrateManager::AnyPassageAirPostNearLattice(
{ {
return true; return true;
} }
if (VF_WalkCorridorBodyCarveTouchesLattice(
Passages, VoxelBox, LatticeOrigin, Step))
{
return true;
}
int32 FirstX = 0, LastX = -1; int32 FirstX = 0, LastX = -1;
int32 FirstY = 0, LastY = -1; int32 FirstY = 0, LastY = -1;
@@ -5033,6 +5337,11 @@ bool UVoxelStrateManager::AnyPassageLandingFloorNearLattice(
} }
for (const FVoxelPassage& Passage : Passages) for (const FVoxelPassage& Passage : Passages)
{ {
float ReliefBound = 0.0f;
if (!VF_GetNativePassageFloorReliefBound(Passage, ReliefBound))
{
return true;
}
const FVoxelPassageLanding* Landings[] = { const FVoxelPassageLanding* Landings[] = {
&Passage.UpperLanding, &Passage.LowerLanding }; &Passage.UpperLanding, &Passage.LowerLanding };
for (const FVoxelPassageLanding* Landing : Landings) for (const FVoxelPassageLanding* Landing : Landings)
@@ -5084,15 +5393,16 @@ bool UVoxelStrateManager::AnyPassageLandingFloorNearLattice(
{ {
return true; return true;
} }
const float SupportRadius = FMath::Max( const float RadiusA = FMath::Abs(Passage.ControlRadii[SegmentIndex]);
FMath::Min( const float RadiusB = FMath::Abs(Passage.ControlRadii[SegmentIndex + 1]);
FMath::Abs(Passage.ControlRadii[SegmentIndex]), const float SupportRadius =
FMath::Abs(Passage.ControlRadii[SegmentIndex + 1])) - 0.5f, VoxelPassageGeometry::NativePassageFloorSupportRadius(RadiusA, RadiusB);
VoxelPassageGeometry::PlayerRadiusVoxels); const float FloorA = Passage.bNativeFloorEnabled
const float FloorA = VoxelPassageGeometry::TunnelFloorZ( ? Passage.NativeFloorProfileZ[SegmentIndex]
A, Passage.ControlRadii[SegmentIndex]); : VoxelPassageGeometry::TunnelFloorZ(A, RadiusA);
const float FloorB = VoxelPassageGeometry::TunnelFloorZ( const float FloorB = Passage.bNativeFloorEnabled
B, Passage.ControlRadii[SegmentIndex + 1]); ? Passage.NativeFloorProfileZ[SegmentIndex + 1]
: VoxelPassageGeometry::TunnelFloorZ(B, RadiusB);
if (VF_LatticeBoxTouchesAABB( if (VF_LatticeBoxTouchesAABB(
VoxelBox, LatticeOrigin, Step, VoxelBox, LatticeOrigin, Step,
FMath::Min(A.X, B.X) - SupportRadius - Pad, FMath::Min(A.X, B.X) - SupportRadius - Pad,
@@ -5100,8 +5410,9 @@ bool UVoxelStrateManager::AnyPassageLandingFloorNearLattice(
FMath::Min(A.Y, B.Y) - SupportRadius - Pad, FMath::Min(A.Y, B.Y) - SupportRadius - Pad,
FMath::Max(A.Y, B.Y) + SupportRadius + Pad, FMath::Max(A.Y, B.Y) + SupportRadius + Pad,
FMath::Min(FloorA, FloorB) FMath::Min(FloorA, FloorB)
- VoxelPassageGeometry::LandingFloorThicknessVoxels - Pad, - VoxelPassageGeometry::LandingFloorThicknessVoxels
FMath::Max(FloorA, FloorB) + Pad)) - Pad - ReliefBound,
FMath::Max(FloorA, FloorB) + Pad + ReliefBound))
{ {
return true; return true;
} }