From b4ac9969f58b8ed85f637e5c9199b53ad9dfedcc Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Mon, 28 Sep 2026 10:03:34 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ --- .../Tests/VoxelForgeDescentCorridorTest.cpp | 98 ++++- Source/VoxelForge/Private/VoxelGenerator.cpp | 14 +- .../VoxelForge/Private/VoxelStrateManager.cpp | 375 ++++++++++++++++-- 3 files changed, 444 insertions(+), 43 deletions(-) diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeDescentCorridorTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeDescentCorridorTest.cpp index 511b974..380c252 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeDescentCorridorTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeDescentCorridorTest.cpp @@ -99,6 +99,11 @@ namespace TArray SampleStations; TArray SampleLaterals; TArray SampleHeights; + TSet ProofStencilTileOrigins; + int32 ProofStencilUniformTiles = 0; + int32 ProofStencilSealedTiles = 0; + int32 ProofStencilOutOfLayoutAirTiles = 0; + int32 ProofStencilClassifyTiles = 0; int32 SolidSamples = 0; int32 BlockingSolidSamples = 0; int32 SolidStations = 0; @@ -214,6 +219,41 @@ namespace (2.0f * FloorProbeHalfRange) / static_cast(FloorProbeIntervals); constexpr float FirstBodySampleZ = 0.25f; 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(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(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 FeetStations; TArray LateralAxes; TArray ExpectedFloorZ; @@ -344,6 +384,13 @@ namespace } MeasuredFloorZ[FloorIndex] = BestFloor; + FVector FloorStencilPosition = FeetStations[StationIndex] + + LateralAxes[StationIndex] + * (-PlayerHalfWidthVoxels + + static_cast(LateralIndex) + * LateralSampleSpacing); + FloorStencilPosition.Z = BestFloor; + AddStencilTilesAtPosition(FloorStencilPosition); if (BestDistance != FLT_MAX) { bHasMeasuredFloor[FloorIndex] = 1; @@ -515,8 +562,10 @@ namespace 0.0f)); const float BodySide = SliceRadius * (static_cast(LateralIndex) / 2.0f - 1.0f); - OutSamples.Positions.Add( - Feet + Lateral * BodySide + FVector(0.0f, 0.0f, Height)); + const FVector BodyPosition = + Feet + Lateral * BodySide + FVector(0.0f, 0.0f, Height); + OutSamples.Positions.Add(BodyPosition); + AddStencilTilesAtPosition(BodyPosition); OutSamples.SampleStations.Add(StationIndex); OutSamples.SampleLaterals.Add(LateralIndex); OutSamples.SampleHeights.Add(VerticalIndex); @@ -631,7 +680,8 @@ namespace bool SampleOwnerSeed( int32 Seed, FWalkCorridorSamples& OutSamples, - FString& OutError) + FString& OutError, + bool bCheckProofStencil = false) { FOwnerPassageWorld World; if (!World.Build(Seed, OutError)) @@ -642,6 +692,24 @@ namespace { 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) { const FIntVector ChunkCoord( @@ -1093,6 +1161,11 @@ bool FVoxelForgeDescentCorridorSeedSweepTest::RunTest(const FString& Parameters) int64 TotalBlockingSolidSamples = 0; int64 TotalWidthFloorGaps = 0; int64 TotalSamples = 0; + int64 TotalProofStencilTiles = 0; + int64 TotalProofStencilUniformTiles = 0; + int64 TotalProofStencilSealedTiles = 0; + int64 TotalProofStencilOutOfLayoutAirTiles = 0; + int64 TotalProofStencilClassifyTiles = 0; int32 FirstFailingSeed = INDEX_NONE; FWalkCorridorSamples Worst; FString WorstError; @@ -1101,13 +1174,18 @@ bool FVoxelForgeDescentCorridorSeedSweepTest::RunTest(const FString& Parameters) { FWalkCorridorSamples Samples; FString Error; - if (!SampleOwnerSeed(Seed, Samples, Error)) + if (!SampleOwnerSeed(Seed, Samples, Error, /*bCheckProofStencil=*/true)) { AddError(Error); return false; } LogCorridorResult(*this, Seed, Samples); TotalSamples += Samples.Densities.Num(); + TotalProofStencilTiles += Samples.ProofStencilTileOrigins.Num(); + TotalProofStencilUniformTiles += Samples.ProofStencilUniformTiles; + TotalProofStencilSealedTiles += Samples.ProofStencilSealedTiles; + TotalProofStencilOutOfLayoutAirTiles += Samples.ProofStencilOutOfLayoutAirTiles; + TotalProofStencilClassifyTiles += Samples.ProofStencilClassifyTiles; TotalSolidSamples += Samples.SolidSamples; TotalBlockingSolidSamples += Samples.BlockingSolidSamples; TotalWidthFloorGaps += Samples.WidthFloorGaps; @@ -1169,9 +1247,19 @@ bool FVoxelForgeDescentCorridorSeedSweepTest::RunTest(const FString& Parameters) static_cast(TotalWidthFloorGaps), static_cast(TotalSamples), FirstFailingSeed, WorstError.IsEmpty() ? TEXT("none") : *WorstError, 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(TotalProofStencilTiles), + static_cast(TotalProofStencilUniformTiles), + static_cast(TotalProofStencilSealedTiles), + static_cast(TotalProofStencilOutOfLayoutAirTiles), + static_cast(TotalProofStencilClassifyTiles)); TestEqual(TEXT("All 64 owner seeds have a clear, walkable A-to-B body corridor"), SeedsWithWalkFailures, 0); - return SeedsWithWalkFailures == 0; + TestEqual(TEXT("No corridor stencil tile is proven uniform by either proof or ClassifyTile"), + TotalProofStencilUniformTiles, static_cast(0)); + return SeedsWithWalkFailures == 0 && TotalProofStencilUniformTiles == 0; } #endif // WITH_DEV_AUTOMATION_TESTS diff --git a/Source/VoxelForge/Private/VoxelGenerator.cpp b/Source/VoxelForge/Private/VoxelGenerator.cpp index 05485ea..adcda2c 100644 --- a/Source/VoxelForge/Private/VoxelGenerator.cpp +++ b/Source/VoxelForge/Private/VoxelGenerator.cpp @@ -7107,8 +7107,10 @@ EVoxelTileClass UVoxelGenerator::ClassifyTileUncached( // ── Structural passage/origin guards for non-open-air categories. ── // Carvers kill AllSolid but never add rock; landing and origin support floors kill AllAir. - if (StrateManager->AnyPassageNearLattice( - TileVoxelBox, OriginVoxels, Step)) + const bool bHasPassageCarvingCandidate = + StrateManager->AnyPassageNearLattice( + TileVoxelBox, OriginVoxels, Step); + if (bHasPassageCarvingCandidate) { bCanSolid = false; } @@ -7355,10 +7357,10 @@ EVoxelTileClass UVoxelGenerator::ClassifyTileUncached( && CaveMinZ == MinZ && CaveMaxZ == MaxZ; if (bCaveOnlyTile && StackVerdict == EVoxelTileClass::AllSolid) { - // The stack verdict covers the complete cave-only MC lattice. Its exact structural - // post path was supplied above, so a broad conservative passage guard must not force - // this already-certified solid tile through GenerateMesh. - bCanSolid = true; + // The operator stack does not own the final passage carve. Keep + // its AllSolid shortcut only when the exact-lattice passage guard + // found no modifier or body-envelope write in this tile. + bCanSolid = !bHasPassageCarvingCandidate; bCanAir = false; } else if (bCaveOnlyTile && StackVerdict == EVoxelTileClass::AllAir) diff --git a/Source/VoxelForge/Private/VoxelStrateManager.cpp b/Source/VoxelForge/Private/VoxelStrateManager.cpp index 26d939e..08fbc52 100644 --- a/Source/VoxelForge/Private/VoxelStrateManager.cpp +++ b/Source/VoxelForge/Private/VoxelStrateManager.cpp @@ -687,6 +687,74 @@ namespace && 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( const FVoxelPassage& Passage, const FVector& Position, float& OutFloorZ, float& OutSupportRadius, @@ -2410,6 +2478,16 @@ void UVoxelStrateManager::GeneratePassages() BoundsMax.Y = FMath::Max(BoundsMax.Y, Point.Y + 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) { IncludePoint(CP, Passage.Radius + 4.0f); @@ -2430,6 +2508,109 @@ void UVoxelStrateManager::GeneratePassages() IncludeLanding(Passage.UpperLanding); 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; float MaxDistSq = 0.0f; 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. for (const FVoxelPassage& Passage : Passages) { + float ReliefBound = 0.0f; + if (!VF_GetNativePassageFloorReliefBound(Passage, ReliefBound)) + { + return true; + } const FVoxelPassageLanding* Landings[] = { &Passage.UpperLanding, &Passage.LowerLanding }; 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 // 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. - if (Passage.bWalkableTunnelContract - && Passage.ControlPoints.Num() >= 2 - && Passage.ControlRadii.Num() == Passage.ControlPoints.Num()) + if (Passage.bWalkableTunnelContract) { + if (Passage.ControlPoints.Num() < 2 + || Passage.ControlRadii.Num() != Passage.ControlPoints.Num()) + { + return true; + } constexpr float Pad = 1.0f; for (int32 SegmentIndex = 0; SegmentIndex + 1 < Passage.ControlPoints.Num(); @@ -3437,22 +3626,29 @@ bool UVoxelStrateManager::AnyPassageLandingFloorNearBox( { const FVector& A = Passage.ControlPoints[SegmentIndex]; const FVector& B = Passage.ControlPoints[SegmentIndex + 1]; - const float SupportRadius = FMath::Max( - FMath::Min( - FMath::Abs(Passage.ControlRadii[SegmentIndex]), - FMath::Abs(Passage.ControlRadii[SegmentIndex + 1])) - 0.5f, - VoxelPassageGeometry::PlayerRadiusVoxels); - const float FloorMinZ = FMath::Min( - VoxelPassageGeometry::TunnelFloorZ( - A, Passage.ControlRadii[SegmentIndex]), - VoxelPassageGeometry::TunnelFloorZ( - B, Passage.ControlRadii[SegmentIndex + 1])) - - VoxelPassageGeometry::LandingFloorThicknessVoxels - Pad; - const float FloorMaxZ = FMath::Max( - VoxelPassageGeometry::TunnelFloorZ( - A, Passage.ControlRadii[SegmentIndex]), - VoxelPassageGeometry::TunnelFloorZ( - B, Passage.ControlRadii[SegmentIndex + 1])) + Pad; + if (!VoxelMath::IsFinite(A.X) || !VoxelMath::IsFinite(A.Y) + || !VoxelMath::IsFinite(A.Z) || !VoxelMath::IsFinite(B.X) + || !VoxelMath::IsFinite(B.Y) || !VoxelMath::IsFinite(B.Z) + || !VoxelMath::IsFinite(Passage.ControlRadii[SegmentIndex]) + || !VoxelMath::IsFinite(Passage.ControlRadii[SegmentIndex + 1])) + { + return true; + } + const float RadiusA = FMath::Abs(Passage.ControlRadii[SegmentIndex]); + const float RadiusB = FMath::Abs(Passage.ControlRadii[SegmentIndex + 1]); + const float SupportRadius = + VoxelPassageGeometry::NativePassageFloorSupportRadius(RadiusA, RadiusB); + const float FloorA = Passage.bNativeFloorEnabled + ? Passage.NativeFloorProfileZ[SegmentIndex] + : VoxelPassageGeometry::TunnelFloorZ(A, RadiusA); + 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 FloorMaxX = FMath::Max(A.X, B.X) + 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)); return FVector::DistSquared(Closest, Center) <= Radius * Radius; } + + static bool VF_WalkCorridorBodyCarveTouchesLattice( + const TArray& 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( @@ -4265,17 +4561,20 @@ bool UVoxelStrateManager::AnyPassageNearLattice( // 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 // 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( Passages, VoxelBox, LatticeOrigin, Step)) { - return false; + return bBodyCarveCandidate; } const float MaxFactor = MaxPassageCarveFactorNearLattice( VoxelBox, LatticeOrigin, Step); // 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. - return !VoxelMath::IsFinite(MaxFactor) || MaxFactor > 0.0f; + return !VoxelMath::IsFinite(MaxFactor) || MaxFactor > 0.0f + || bBodyCarveCandidate; } bool UVoxelStrateManager::AnyPassageAirPostNearLattice( @@ -4296,6 +4595,11 @@ bool UVoxelStrateManager::AnyPassageAirPostNearLattice( { return true; } + if (VF_WalkCorridorBodyCarveTouchesLattice( + Passages, VoxelBox, LatticeOrigin, Step)) + { + return true; + } int32 FirstX = 0, LastX = -1; int32 FirstY = 0, LastY = -1; @@ -5033,6 +5337,11 @@ bool UVoxelStrateManager::AnyPassageLandingFloorNearLattice( } for (const FVoxelPassage& Passage : Passages) { + float ReliefBound = 0.0f; + if (!VF_GetNativePassageFloorReliefBound(Passage, ReliefBound)) + { + return true; + } const FVoxelPassageLanding* Landings[] = { &Passage.UpperLanding, &Passage.LowerLanding }; for (const FVoxelPassageLanding* Landing : Landings) @@ -5084,15 +5393,16 @@ bool UVoxelStrateManager::AnyPassageLandingFloorNearLattice( { return true; } - const float SupportRadius = FMath::Max( - FMath::Min( - FMath::Abs(Passage.ControlRadii[SegmentIndex]), - FMath::Abs(Passage.ControlRadii[SegmentIndex + 1])) - 0.5f, - VoxelPassageGeometry::PlayerRadiusVoxels); - const float FloorA = VoxelPassageGeometry::TunnelFloorZ( - A, Passage.ControlRadii[SegmentIndex]); - const float FloorB = VoxelPassageGeometry::TunnelFloorZ( - B, Passage.ControlRadii[SegmentIndex + 1]); + const float RadiusA = FMath::Abs(Passage.ControlRadii[SegmentIndex]); + const float RadiusB = FMath::Abs(Passage.ControlRadii[SegmentIndex + 1]); + const float SupportRadius = + VoxelPassageGeometry::NativePassageFloorSupportRadius(RadiusA, RadiusB); + const float FloorA = Passage.bNativeFloorEnabled + ? Passage.NativeFloorProfileZ[SegmentIndex] + : VoxelPassageGeometry::TunnelFloorZ(A, RadiusA); + const float FloorB = Passage.bNativeFloorEnabled + ? Passage.NativeFloorProfileZ[SegmentIndex + 1] + : VoxelPassageGeometry::TunnelFloorZ(B, RadiusB); if (VF_LatticeBoxTouchesAABB( VoxelBox, LatticeOrigin, Step, FMath::Min(A.X, B.X) - SupportRadius - Pad, @@ -5100,8 +5410,9 @@ bool UVoxelStrateManager::AnyPassageLandingFloorNearLattice( FMath::Min(A.Y, B.Y) - SupportRadius - Pad, FMath::Max(A.Y, B.Y) + SupportRadius + Pad, FMath::Min(FloorA, FloorB) - - VoxelPassageGeometry::LandingFloorThicknessVoxels - Pad, - FMath::Max(FloorA, FloorB) + Pad)) + - VoxelPassageGeometry::LandingFloorThicknessVoxels + - Pad - ReliefBound, + FMath::Max(FloorA, FloorB) + Pad + ReliefBound)) { return true; }