wip(passages): A->B descent corridor clear -- 60/64 seeds blocked -> 0/64; soundness + cost not yet checked
What the pillars were: at switchbacks the native-floor D-section clip in UVoxelStrateManager::EvaluateModifierSDF used ONE floor branch (the nearest by projection) for neighbouring capsule segments, so one leg's floor clipped the other leg's carve and left rock standing in the corridor (seed 0 witness: density -29.45, carve SDF +3.28; with voxel.TunnelAblateNativeFloor=1 the same point is air). Separately, the lower ramp was routed through the lower landing room (sign error in the lower "outward" score), where room ownership correctly erased the tunnel floor: up to 12.5-voxel drops. Fix: each segment clips with its own floor branch before the union; switchback legs separated and door approaches moved outward so ramps leave their landing rooms; a final carve clears the player body envelope (1.36 + 2x1 voxel wide, 1.8..8.8 voxels above the route floor) after all writers, with a room-SDF guard. No floor slab, no fill, no blockade on the through edge. Owner world, 64-seed sweep, stations every 0.5 voxel x 5 lateral x 14 heights: before 89,603 body blockers in 60/64 seeds; after 0 in 64/64, 0 floor/width gaps, worst step 0.958 voxels (limit 1.8). Seed 0 built twice: identical density CRC. Crossing at default speed still stops on capsule_overlaps_pending_floor (also before); 500 cm/s reaches B. NOT YET CHECKED: sealed-solid / classifier soundness against the moved passage geometry, and the generation cost of the per-segment projection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
1 parent
de05e293aa
commit
fb052716c6
3 files changed
+1549
-171
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -571,111 +571,107 @@ namespace
|
|||||||
0.0f, VF_PassageMaxReliefScale);
|
0.0f, VF_PassageMaxReliefScale);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool VF_ProjectNativePassageFloorUncached(
|
static bool VF_ProjectNativePassageFloorSegment(
|
||||||
const FVoxelPassage& Passage, const FVector& Position,
|
const FVoxelPassage& Passage, int32 SegmentIndex,
|
||||||
float& OutFloorZ, float& OutSupportRadius,
|
const FVector& Position, float& OutFloorZ, float& OutSupportRadius,
|
||||||
|
float& OutDistanceSquared, float& OutHorizontalDistanceSquared,
|
||||||
const UVoxelStrateManager* Manager = nullptr,
|
const UVoxelStrateManager* Manager = nullptr,
|
||||||
int32 PassageIndex = INDEX_NONE)
|
int32 PassageIndex = INDEX_NONE)
|
||||||
{
|
{
|
||||||
const int32 ControlPointCount = Passage.ControlPoints.Num();
|
const int32 ControlPointCount = Passage.ControlPoints.Num();
|
||||||
if (!Passage.bNativeFloorEnabled
|
if (!Passage.bNativeFloorEnabled
|
||||||
|| ControlPointCount < 2
|
|| SegmentIndex < 0
|
||||||
|
|| SegmentIndex + 1 >= ControlPointCount
|
||||||
|| Passage.ControlRadii.Num() != ControlPointCount
|
|| Passage.ControlRadii.Num() != ControlPointCount
|
||||||
|| Passage.NativeFloorProfileZ.Num() != ControlPointCount)
|
|| Passage.NativeFloorProfileZ.Num() != ControlPointCount)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FVector* ControlPointData = Passage.ControlPoints.GetData();
|
const FVector& A = Passage.ControlPoints[SegmentIndex];
|
||||||
const float* ControlRadiusData = Passage.ControlRadii.GetData();
|
const FVector& B = Passage.ControlPoints[SegmentIndex + 1];
|
||||||
const float* NativeFloorProfileData = Passage.NativeFloorProfileZ.GetData();
|
const float StartRadius = FMath::Abs(Passage.ControlRadii[SegmentIndex]);
|
||||||
|
const float EndRadius = FMath::Abs(Passage.ControlRadii[SegmentIndex + 1]);
|
||||||
const double QueryX = static_cast<double>(static_cast<float>(Position.X));
|
const double QueryX = static_cast<double>(static_cast<float>(Position.X));
|
||||||
const double QueryY = static_cast<double>(static_cast<float>(Position.Y));
|
const double QueryY = static_cast<double>(static_cast<float>(Position.Y));
|
||||||
float BestDistanceSquared = FLT_MAX;
|
|
||||||
int32 BestSegment = INDEX_NONE;
|
|
||||||
float BestT = 0.0f;
|
|
||||||
float BestSupportRadius = 0.0f;
|
|
||||||
for (int32 SegmentIndex = 0;
|
|
||||||
SegmentIndex + 1 < ControlPointCount;
|
|
||||||
++SegmentIndex)
|
|
||||||
{
|
|
||||||
const FVector& A = ControlPointData[SegmentIndex];
|
|
||||||
const FVector& B = ControlPointData[SegmentIndex + 1];
|
|
||||||
const double AXY_X = static_cast<double>(static_cast<float>(A.X));
|
const double AXY_X = static_cast<double>(static_cast<float>(A.X));
|
||||||
const double AXY_Y = static_cast<double>(static_cast<float>(A.Y));
|
const double AXY_Y = static_cast<double>(static_cast<float>(A.Y));
|
||||||
const double DeltaX = static_cast<double>(static_cast<float>(B.X - A.X));
|
const double DeltaX = static_cast<double>(static_cast<float>(B.X - A.X));
|
||||||
const double DeltaY = static_cast<double>(static_cast<float>(B.Y - A.Y));
|
const double DeltaY = static_cast<double>(static_cast<float>(B.Y - A.Y));
|
||||||
const float LengthSquared = static_cast<float>(
|
const float HorizontalLengthSquared = static_cast<float>(
|
||||||
DeltaX * DeltaX + DeltaY * DeltaY);
|
DeltaX * DeltaX + DeltaY * DeltaY);
|
||||||
if (!(LengthSquared > KINDA_SMALL_NUMBER))
|
if (!(HorizontalLengthSquared > KINDA_SMALL_NUMBER))
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const double Dot = (QueryX - AXY_X) * DeltaX
|
|
||||||
+ (QueryY - AXY_Y) * DeltaY;
|
|
||||||
const float T = FMath::Clamp(
|
|
||||||
Dot / LengthSquared,
|
|
||||||
0.0f, 1.0f);
|
|
||||||
const double ClosestX = AXY_X + DeltaX * T;
|
|
||||||
const double ClosestY = AXY_Y + DeltaY * T;
|
|
||||||
const double DistanceX = QueryX - ClosestX;
|
|
||||||
const double DistanceY = QueryY - ClosestY;
|
|
||||||
const float DistanceSquared = static_cast<float>(
|
|
||||||
DistanceX * DistanceX + DistanceY * DistanceY);
|
|
||||||
if (DistanceSquared >= BestDistanceSquared)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
BestDistanceSquared = DistanceSquared;
|
|
||||||
BestSegment = SegmentIndex;
|
|
||||||
BestT = T;
|
|
||||||
BestSupportRadius = FMath::Max(
|
|
||||||
FMath::Min(
|
|
||||||
FMath::Abs(ControlRadiusData[SegmentIndex]),
|
|
||||||
FMath::Abs(ControlRadiusData[SegmentIndex + 1])) - 0.5f,
|
|
||||||
VoxelPassageGeometry::PlayerRadiusVoxels);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (BestSegment == INDEX_NONE
|
|
||||||
|| BestDistanceSquared > FMath::Square(BestSupportRadius))
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const float ClampedT = FMath::Clamp(BestT, 0.0f, 1.0f);
|
const double HorizontalDot = (QueryX - AXY_X) * DeltaX
|
||||||
OutFloorZ = FMath::Lerp(
|
+ (QueryY - AXY_Y) * DeltaY;
|
||||||
NativeFloorProfileData[BestSegment],
|
const float HorizontalT = FMath::Clamp(
|
||||||
NativeFloorProfileData[BestSegment + 1],
|
HorizontalDot / HorizontalLengthSquared,
|
||||||
ClampedT);
|
0.0f, 1.0f);
|
||||||
|
const double HorizontalClosestX = AXY_X + DeltaX * HorizontalT;
|
||||||
float ReliefScale = 0.0f;
|
const double HorizontalClosestY = AXY_Y + DeltaY * HorizontalT;
|
||||||
const int32 ReliefScaleCount = Passage.NativeFloorReliefScales.Num();
|
const double HorizontalDistanceX = QueryX - HorizontalClosestX;
|
||||||
if (BestSegment >= 0 && BestSegment < ReliefScaleCount)
|
const double HorizontalDistanceY = QueryY - HorizontalClosestY;
|
||||||
|
const float HorizontalDistanceSquared = static_cast<float>(
|
||||||
|
HorizontalDistanceX * HorizontalDistanceX
|
||||||
|
+ HorizontalDistanceY * HorizontalDistanceY);
|
||||||
|
OutHorizontalDistanceSquared = HorizontalDistanceSquared;
|
||||||
|
const float SegmentSupportRadius =
|
||||||
|
VoxelPassageGeometry::NativePassageFloorSupportRadius(
|
||||||
|
StartRadius, EndRadius);
|
||||||
|
if (HorizontalDistanceSquared > FMath::Square(SegmentSupportRadius))
|
||||||
{
|
{
|
||||||
ReliefScale = Passage.NativeFloorReliefScales.GetData()[BestSegment];
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float FloorA = Passage.NativeFloorProfileZ[SegmentIndex];
|
||||||
|
const float FloorB = Passage.NativeFloorProfileZ[SegmentIndex + 1];
|
||||||
|
const float FloorDeltaZ = FloorB - FloorA;
|
||||||
|
const float FullLengthSquared = HorizontalLengthSquared
|
||||||
|
+ FMath::Square(FloorDeltaZ);
|
||||||
|
if (!(FullLengthSquared > KINDA_SMALL_NUMBER))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const double FullDot = HorizontalDot
|
||||||
|
+ (static_cast<double>(static_cast<float>(Position.Z)) - FloorA)
|
||||||
|
* FloorDeltaZ;
|
||||||
|
const float SpatialT = FMath::Clamp(
|
||||||
|
FullDot / FullLengthSquared,
|
||||||
|
0.0f, 1.0f);
|
||||||
|
const double SpatialX = AXY_X + DeltaX * SpatialT;
|
||||||
|
const double SpatialY = AXY_Y + DeltaY * SpatialT;
|
||||||
|
const double SpatialZ = FloorA + FloorDeltaZ * SpatialT;
|
||||||
|
const double SpatialDistanceX = QueryX - SpatialX;
|
||||||
|
const double SpatialDistanceY = QueryY - SpatialY;
|
||||||
|
const double SpatialDistanceZ =
|
||||||
|
static_cast<double>(static_cast<float>(Position.Z)) - SpatialZ;
|
||||||
|
OutDistanceSquared = static_cast<float>(
|
||||||
|
SpatialDistanceX * SpatialDistanceX
|
||||||
|
+ SpatialDistanceY * SpatialDistanceY
|
||||||
|
+ SpatialDistanceZ * SpatialDistanceZ);
|
||||||
|
|
||||||
|
OutFloorZ = FMath::Lerp(FloorA, FloorB, HorizontalT);
|
||||||
|
float ReliefScale = 0.0f;
|
||||||
|
if (Passage.NativeFloorReliefScales.IsValidIndex(SegmentIndex))
|
||||||
|
{
|
||||||
|
ReliefScale = Passage.NativeFloorReliefScales[SegmentIndex];
|
||||||
}
|
}
|
||||||
float Envelope = 1.0f;
|
float Envelope = 1.0f;
|
||||||
const FVector& BestA = ControlPointData[BestSegment];
|
const float HorizontalRun = FMath::Sqrt(HorizontalLengthSquared);
|
||||||
const FVector& BestB = ControlPointData[BestSegment + 1];
|
if (SegmentIndex == 0)
|
||||||
const double BestDeltaX = static_cast<double>(static_cast<float>(
|
|
||||||
BestB.X - BestA.X));
|
|
||||||
const double BestDeltaY = static_cast<double>(static_cast<float>(
|
|
||||||
BestB.Y - BestA.Y));
|
|
||||||
const float HorizontalRun = static_cast<float>(FMath::Sqrt(
|
|
||||||
BestDeltaX * BestDeltaX + BestDeltaY * BestDeltaY));
|
|
||||||
if (BestSegment == 0)
|
|
||||||
{
|
{
|
||||||
Envelope *= SmoothStep01(FMath::Clamp(
|
Envelope *= SmoothStep01(FMath::Clamp(
|
||||||
(ClampedT * HorizontalRun)
|
(HorizontalT * HorizontalRun)
|
||||||
/ VoxelPassageGeometry::WalkableTunnelLandingApronVoxels,
|
/ VoxelPassageGeometry::WalkableTunnelLandingApronVoxels,
|
||||||
0.0f, 1.0f));
|
0.0f, 1.0f));
|
||||||
}
|
}
|
||||||
if (BestSegment + 1 == ControlPointCount - 1)
|
if (SegmentIndex + 1 == ControlPointCount - 1)
|
||||||
{
|
{
|
||||||
Envelope *= SmoothStep01(FMath::Clamp(
|
Envelope *= SmoothStep01(FMath::Clamp(
|
||||||
((1.0f - ClampedT) * HorizontalRun)
|
((1.0f - HorizontalT) * HorizontalRun)
|
||||||
/ VoxelPassageGeometry::WalkableTunnelLandingApronVoxels,
|
/ VoxelPassageGeometry::WalkableTunnelLandingApronVoxels,
|
||||||
0.0f, 1.0f));
|
0.0f, 1.0f));
|
||||||
}
|
}
|
||||||
@@ -684,12 +680,53 @@ namespace
|
|||||||
static_cast<double>(Position.X), static_cast<double>(Position.Y))
|
static_cast<double>(Position.X), static_cast<double>(Position.Y))
|
||||||
* VOXEL_NOISE_SCALE * Passage.NativeFloorReliefStrength
|
* VOXEL_NOISE_SCALE * Passage.NativeFloorReliefStrength
|
||||||
* Envelope * ReliefScale;
|
* Envelope * ReliefScale;
|
||||||
OutSupportRadius = BestSupportRadius;
|
OutSupportRadius = SegmentSupportRadius;
|
||||||
return VoxelMath::IsFinite(OutFloorZ)
|
return VoxelMath::IsFinite(OutFloorZ)
|
||||||
&& VoxelMath::IsFinite(OutSupportRadius)
|
&& VoxelMath::IsFinite(OutSupportRadius)
|
||||||
|
&& VoxelMath::IsFinite(OutDistanceSquared)
|
||||||
&& OutSupportRadius > 0.0f;
|
&& OutSupportRadius > 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool VF_ProjectNativePassageFloorUncached(
|
||||||
|
const FVoxelPassage& Passage, const FVector& Position,
|
||||||
|
float& OutFloorZ, float& OutSupportRadius,
|
||||||
|
const UVoxelStrateManager* Manager = nullptr,
|
||||||
|
int32 PassageIndex = INDEX_NONE)
|
||||||
|
{
|
||||||
|
const int32 SegmentCount = Passage.ControlPoints.Num() - 1;
|
||||||
|
float BestDistanceSquared = FLT_MAX;
|
||||||
|
float BestFloorZ = 0.0f;
|
||||||
|
float BestSupportRadius = 0.0f;
|
||||||
|
bool bFoundSegment = false;
|
||||||
|
for (int32 SegmentIndex = 0; SegmentIndex < SegmentCount; ++SegmentIndex)
|
||||||
|
{
|
||||||
|
float FloorZ = 0.0f;
|
||||||
|
float SupportRadius = 0.0f;
|
||||||
|
float DistanceSquared = FLT_MAX;
|
||||||
|
float HorizontalDistanceSquared = FLT_MAX;
|
||||||
|
if (!VF_ProjectNativePassageFloorSegment(
|
||||||
|
Passage, SegmentIndex, Position,
|
||||||
|
FloorZ, SupportRadius, DistanceSquared,
|
||||||
|
HorizontalDistanceSquared,
|
||||||
|
Manager, PassageIndex)
|
||||||
|
|| DistanceSquared >= BestDistanceSquared)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BestDistanceSquared = DistanceSquared;
|
||||||
|
BestFloorZ = FloorZ;
|
||||||
|
BestSupportRadius = SupportRadius;
|
||||||
|
bFoundSegment = true;
|
||||||
|
}
|
||||||
|
if (!bFoundSegment)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
OutFloorZ = BestFloorZ;
|
||||||
|
OutSupportRadius = BestSupportRadius;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static bool VF_ProjectNativePassageFloor(
|
static bool VF_ProjectNativePassageFloor(
|
||||||
const FVoxelPassage& Passage, const FVector& Position,
|
const FVoxelPassage& Passage, const FVector& Position,
|
||||||
float& OutFloorZ, float& OutSupportRadius,
|
float& OutFloorZ, float& OutSupportRadius,
|
||||||
@@ -2157,10 +2194,10 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
const FVector DoorDirection(
|
const FVector DoorDirection(
|
||||||
SwitchbackDirection.X, SwitchbackDirection.Y, 0.0f);
|
SwitchbackDirection.X, SwitchbackDirection.Y, 0.0f);
|
||||||
Passage.UpperLanding.DoorDirection = DoorDirection;
|
Passage.UpperLanding.DoorDirection = DoorDirection;
|
||||||
// A real switchback turns around: the lower doorway faces back toward the upper
|
// Keep both door approaches on the same outward side so each sloped leg reaches
|
||||||
// leg. Keeping both doors on the same side would make the two sloped legs retrace
|
// its own apron before entering the room. Separate their turn-side floor branches
|
||||||
// one XY line at different heights, leaving the floor projection ambiguous.
|
// below so the two projections do not retrace one XY line at different heights.
|
||||||
const FVector TunnelLowerDoorDirection = -DoorDirection;
|
const FVector TunnelLowerDoorDirection = DoorDirection;
|
||||||
Passage.LowerLanding.DoorDirection = TunnelLowerDoorDirection;
|
Passage.LowerLanding.DoorDirection = TunnelLowerDoorDirection;
|
||||||
const float UpperDoorOffset = FMath::Max(
|
const float UpperDoorOffset = FMath::Max(
|
||||||
Passage.UpperLanding.HalfWidth - 1.0f, 0.0f);
|
Passage.UpperLanding.HalfWidth - 1.0f, 0.0f);
|
||||||
@@ -2204,27 +2241,9 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
const float LowerFloorZ = VoxelPassageGeometry::TunnelFloorZ(
|
const float LowerFloorZ = VoxelPassageGeometry::TunnelFloorZ(
|
||||||
LowerDoor, Cfg.MouthRadius);
|
LowerDoor, Cfg.MouthRadius);
|
||||||
const float MidFloorZ = 0.5f * (UpperFloorZ + LowerFloorZ);
|
const float MidFloorZ = 0.5f * (UpperFloorZ + LowerFloorZ);
|
||||||
const FVector2D RampStartMid(
|
// Each ramp must leave its own landing on the outward side. The two doors face
|
||||||
0.5f * (UpperApronEnd.X + LowerApronBegin.X),
|
// that same direction; orient the shared level turn along it, then split the lower
|
||||||
0.5f * (UpperApronEnd.Y + LowerApronBegin.Y));
|
// floor branch laterally so the ramp projections remain unambiguous.
|
||||||
// The turn offset must be perpendicular to the actual two-apron endpoint chord,
|
|
||||||
// not merely perpendicular to the standing-point chord. Opposite-facing doors
|
|
||||||
// add their offsets to that chord; using the old direction could make the two
|
|
||||||
// sloped legs nearly collinear and bring the lower leg back beside the upper door.
|
|
||||||
FVector2D TurnDirection(
|
|
||||||
LowerApronBegin.Y - UpperApronEnd.Y,
|
|
||||||
-(LowerApronBegin.X - UpperApronEnd.X));
|
|
||||||
if (!TurnDirection.Normalize())
|
|
||||||
{
|
|
||||||
TurnDirection = FVector2D(-DoorDirection.Y, DoorDirection.X);
|
|
||||||
if (!TurnDirection.Normalize())
|
|
||||||
{
|
|
||||||
TurnDirection = FVector2D(0.0f, 1.0f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// There are two sides on which the switchback can turn. Choose the side whose
|
|
||||||
// two sloped legs initially move away from their own landing rooms; the old fixed
|
|
||||||
// sign could send a slope back through a room floor before reaching open space.
|
|
||||||
FVector2D UpperOutward(
|
FVector2D UpperOutward(
|
||||||
UpperApronEnd.X - Passage.UpperLanding.StandingPoint.X,
|
UpperApronEnd.X - Passage.UpperLanding.StandingPoint.X,
|
||||||
UpperApronEnd.Y - Passage.UpperLanding.StandingPoint.Y);
|
UpperApronEnd.Y - Passage.UpperLanding.StandingPoint.Y);
|
||||||
@@ -2233,32 +2252,6 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
LowerApronBegin.Y - Passage.LowerLanding.StandingPoint.Y);
|
LowerApronBegin.Y - Passage.LowerLanding.StandingPoint.Y);
|
||||||
UpperOutward.Normalize();
|
UpperOutward.Normalize();
|
||||||
LowerOutward.Normalize();
|
LowerOutward.Normalize();
|
||||||
const float TurnOffsetForScore =
|
|
||||||
0.5f * VoxelPassageGeometry::RequiredHorizontalRunForFloorDrop(
|
|
||||||
UpperFloorZ - 0.5f * (UpperFloorZ + LowerFloorZ),
|
|
||||||
0.5f * (UpperFloorZ + LowerFloorZ) - LowerFloorZ)
|
|
||||||
+ VoxelPassageGeometry::WalkableTunnelTurnTransitionVoxels;
|
|
||||||
const FVector2D TurnMidPlus(
|
|
||||||
RampStartMid.X + TurnDirection.X * TurnOffsetForScore,
|
|
||||||
RampStartMid.Y + TurnDirection.Y * TurnOffsetForScore);
|
|
||||||
const FVector2D TurnMidMinus(
|
|
||||||
RampStartMid.X - TurnDirection.X * TurnOffsetForScore,
|
|
||||||
RampStartMid.Y - TurnDirection.Y * TurnOffsetForScore);
|
|
||||||
const auto OutwardTurnScore = [
|
|
||||||
&UpperOutward, &LowerOutward, &UpperApronEnd, &LowerApronBegin]
|
|
||||||
(const FVector2D& Candidate) -> float
|
|
||||||
{
|
|
||||||
return FVector2D::DotProduct(
|
|
||||||
Candidate - FVector2D(
|
|
||||||
UpperApronEnd.X, UpperApronEnd.Y), UpperOutward)
|
|
||||||
+ FVector2D::DotProduct(
|
|
||||||
FVector2D(LowerApronBegin.X, LowerApronBegin.Y) - Candidate,
|
|
||||||
LowerOutward);
|
|
||||||
};
|
|
||||||
if (OutwardTurnScore(TurnMidMinus) > OutwardTurnScore(TurnMidPlus))
|
|
||||||
{
|
|
||||||
TurnDirection *= -1.0f;
|
|
||||||
}
|
|
||||||
const float UpperFloorDrop = UpperFloorZ - MidFloorZ;
|
const float UpperFloorDrop = UpperFloorZ - MidFloorZ;
|
||||||
const float LowerFloorDrop = MidFloorZ - LowerFloorZ;
|
const float LowerFloorDrop = MidFloorZ - LowerFloorZ;
|
||||||
const float RequiredHorizontalRun =
|
const float RequiredHorizontalRun =
|
||||||
@@ -2271,22 +2264,46 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
VoxelPassageGeometry::WalkableTunnelTurnTransitionVoxels;
|
VoxelPassageGeometry::WalkableTunnelTurnTransitionVoxels;
|
||||||
const float TurnOffset =
|
const float TurnOffset =
|
||||||
0.5f * RequiredHorizontalRun + TurnTransition;
|
0.5f * RequiredHorizontalRun + TurnTransition;
|
||||||
const FVector2D FloorSafeDoorMid(
|
FVector2D TurnDirection(
|
||||||
RampStartMid.X
|
UpperOutward.X + LowerOutward.X,
|
||||||
+ TurnDirection.X * TurnOffset,
|
UpperOutward.Y + LowerOutward.Y);
|
||||||
RampStartMid.Y
|
if (!TurnDirection.Normalize())
|
||||||
+ TurnDirection.Y * TurnOffset);
|
{
|
||||||
|
TurnDirection = UpperOutward;
|
||||||
|
}
|
||||||
|
const FVector2D ApronChord(
|
||||||
|
LowerApronBegin.X - UpperApronEnd.X,
|
||||||
|
LowerApronBegin.Y - UpperApronEnd.Y);
|
||||||
|
const float LowerApronAlong = FVector2D::DotProduct(
|
||||||
|
ApronChord, TurnDirection);
|
||||||
|
const float SharedTurnOffset = TurnOffset
|
||||||
|
+ FMath::Max(LowerApronAlong, 0.0f);
|
||||||
|
const FVector2D SharedTurnMid(
|
||||||
|
UpperApronEnd.X + TurnDirection.X * SharedTurnOffset,
|
||||||
|
UpperApronEnd.Y + TurnDirection.Y * SharedTurnOffset);
|
||||||
|
const float TurnLegSeparation =
|
||||||
|
VoxelPassageGeometry::WalkableTunnelTurnLegSeparation(
|
||||||
|
Cfg.MidRadius, Cfg.MouthRadius);
|
||||||
|
FVector2D TurnLegSideDirection(-TurnDirection.Y, TurnDirection.X);
|
||||||
|
if (FVector2D::DotProduct(ApronChord, TurnLegSideDirection) < 0.0f)
|
||||||
|
{
|
||||||
|
TurnLegSideDirection *= -1.0f;
|
||||||
|
}
|
||||||
|
const FVector2D UpperTurnMid = SharedTurnMid;
|
||||||
|
const FVector2D LowerTurnMid(
|
||||||
|
SharedTurnMid.X + TurnLegSideDirection.X * TurnLegSeparation,
|
||||||
|
SharedTurnMid.Y + TurnLegSideDirection.Y * TurnLegSeparation);
|
||||||
FVector2D UpperSlopeDirection(
|
FVector2D UpperSlopeDirection(
|
||||||
FloorSafeDoorMid.X - UpperApronEnd.X,
|
UpperTurnMid.X - UpperApronEnd.X,
|
||||||
FloorSafeDoorMid.Y - UpperApronEnd.Y);
|
UpperTurnMid.Y - UpperApronEnd.Y);
|
||||||
if (!UpperSlopeDirection.Normalize())
|
if (!UpperSlopeDirection.Normalize())
|
||||||
{
|
{
|
||||||
UpperSlopeDirection = FVector2D(
|
UpperSlopeDirection = FVector2D(
|
||||||
DoorDirection.X, DoorDirection.Y);
|
DoorDirection.X, DoorDirection.Y);
|
||||||
}
|
}
|
||||||
FVector2D LowerSlopeDirection(
|
FVector2D LowerSlopeDirection(
|
||||||
LowerApronBegin.X - FloorSafeDoorMid.X,
|
LowerApronBegin.X - LowerTurnMid.X,
|
||||||
LowerApronBegin.Y - FloorSafeDoorMid.Y);
|
LowerApronBegin.Y - LowerTurnMid.Y);
|
||||||
if (!LowerSlopeDirection.Normalize())
|
if (!LowerSlopeDirection.Normalize())
|
||||||
{
|
{
|
||||||
LowerSlopeDirection = -UpperSlopeDirection;
|
LowerSlopeDirection = -UpperSlopeDirection;
|
||||||
@@ -2304,7 +2321,10 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
UpperSlopeStartXY.X, UpperSlopeStartXY.Y,
|
UpperSlopeStartXY.X, UpperSlopeStartXY.Y,
|
||||||
UpperFloorZ + FMath::Abs(Cfg.MouthRadius));
|
UpperFloorZ + FMath::Abs(Cfg.MouthRadius));
|
||||||
const FVector MidPoint(
|
const FVector MidPoint(
|
||||||
FloorSafeDoorMid.X, FloorSafeDoorMid.Y,
|
UpperTurnMid.X, UpperTurnMid.Y,
|
||||||
|
MidFloorZ + FMath::Abs(Cfg.MidRadius));
|
||||||
|
const FVector LowerTurnPoint(
|
||||||
|
LowerTurnMid.X, LowerTurnMid.Y,
|
||||||
MidFloorZ + FMath::Abs(Cfg.MidRadius));
|
MidFloorZ + FMath::Abs(Cfg.MidRadius));
|
||||||
const FVector LowerSlopeEnd(
|
const FVector LowerSlopeEnd(
|
||||||
LowerSlopeEndXY.X, LowerSlopeEndXY.Y,
|
LowerSlopeEndXY.X, LowerSlopeEndXY.Y,
|
||||||
@@ -2313,12 +2333,13 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
LowerApronBegin.X, LowerApronBegin.Y,
|
LowerApronBegin.X, LowerApronBegin.Y,
|
||||||
LowerFloorZ + FMath::Abs(Cfg.MouthRadius));
|
LowerFloorZ + FMath::Abs(Cfg.MouthRadius));
|
||||||
|
|
||||||
Passage.ControlPoints.Reset(7);
|
Passage.ControlPoints.Reset(8);
|
||||||
Passage.ControlRadii.Reset(7);
|
Passage.ControlRadii.Reset(8);
|
||||||
Passage.ControlPoints.Add(UpperDoor);
|
Passage.ControlPoints.Add(UpperDoor);
|
||||||
Passage.ControlPoints.Add(UpperApronEndPoint);
|
Passage.ControlPoints.Add(UpperApronEndPoint);
|
||||||
Passage.ControlPoints.Add(UpperSlopeStart);
|
Passage.ControlPoints.Add(UpperSlopeStart);
|
||||||
Passage.ControlPoints.Add(MidPoint);
|
Passage.ControlPoints.Add(MidPoint);
|
||||||
|
Passage.ControlPoints.Add(LowerTurnPoint);
|
||||||
Passage.ControlPoints.Add(LowerSlopeEnd);
|
Passage.ControlPoints.Add(LowerSlopeEnd);
|
||||||
Passage.ControlPoints.Add(LowerApronBeginPoint);
|
Passage.ControlPoints.Add(LowerApronBeginPoint);
|
||||||
Passage.ControlPoints.Add(LowerDoor);
|
Passage.ControlPoints.Add(LowerDoor);
|
||||||
@@ -2326,6 +2347,7 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
Passage.ControlRadii.Add(Cfg.MouthRadius);
|
Passage.ControlRadii.Add(Cfg.MouthRadius);
|
||||||
Passage.ControlRadii.Add(Cfg.MouthRadius);
|
Passage.ControlRadii.Add(Cfg.MouthRadius);
|
||||||
Passage.ControlRadii.Add(Cfg.MidRadius);
|
Passage.ControlRadii.Add(Cfg.MidRadius);
|
||||||
|
Passage.ControlRadii.Add(Cfg.MidRadius);
|
||||||
Passage.ControlRadii.Add(Cfg.MouthRadius);
|
Passage.ControlRadii.Add(Cfg.MouthRadius);
|
||||||
Passage.ControlRadii.Add(Cfg.MouthRadius);
|
Passage.ControlRadii.Add(Cfg.MouthRadius);
|
||||||
Passage.ControlRadii.Add(Cfg.MouthRadius);
|
Passage.ControlRadii.Add(Cfg.MouthRadius);
|
||||||
@@ -2592,30 +2614,32 @@ float UVoxelStrateManager::EvaluateModifierSDF(float WorldX, float WorldY, float
|
|||||||
{
|
{
|
||||||
const float rA = bTaper ? ControlRadiusData[j] : P.Radius;
|
const float rA = bTaper ? ControlRadiusData[j] : P.Radius;
|
||||||
const float rB = bTaper ? ControlRadiusData[j + 1] : P.Radius;
|
const float rB = bTaper ? ControlRadiusData[j + 1] : P.Radius;
|
||||||
const float SegSDF = VoxelSDF::TaperedCapsule(
|
float SegmentSDF = VoxelSDF::TaperedCapsule(
|
||||||
Pos, ControlPointData[j], ControlPointData[j + 1], rA, rB);
|
Pos, ControlPointData[j], ControlPointData[j + 1], rA, rB);
|
||||||
PassageSDF = VoxelSDF::SmoothMin(PassageSDF, SegSDF, BlendK);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A walkable inter-strate passage is a D-shaped primitive, not a capsule followed
|
// Clip each capsule against its own floor profile before the route segments are
|
||||||
// by a slab. The profile and its bounded relief were authored in GeneratePassages;
|
// unioned. A single nearest-floor projection after the union can select the
|
||||||
// projecting once here selects the nearest immutable floor segment for this sample.
|
// neighboring leg of a switchback and leave a solid post in this segment.
|
||||||
// Unknown/malformed descriptors deliberately retain the bare capsule and the legacy
|
|
||||||
// diagnostic backstop remains available.
|
|
||||||
if (P.bNativeFloorEnabled && !VoxelDensityAblation::IsNativeFloorOff())
|
if (P.bNativeFloorEnabled && !VoxelDensityAblation::IsNativeFloorOff())
|
||||||
{
|
{
|
||||||
float NativeFloorZ = 0.0f;
|
float NativeFloorZ = 0.0f;
|
||||||
float NativeSupportRadius = 0.0f;
|
float NativeSupportRadius = 0.0f;
|
||||||
if (VF_ProjectNativePassageFloor(
|
float FloorDistanceSquared = FLT_MAX;
|
||||||
P, Pos, NativeFloorZ, NativeSupportRadius,
|
float FloorHorizontalDistanceSquared = FLT_MAX;
|
||||||
|
if (VF_ProjectNativePassageFloorSegment(
|
||||||
|
P, j, Pos, NativeFloorZ, NativeSupportRadius,
|
||||||
|
FloorDistanceSquared, FloorHorizontalDistanceSquared,
|
||||||
this, PIdx))
|
this, PIdx))
|
||||||
{
|
{
|
||||||
PassageSDF = VoxelSDF::SmoothMax(
|
SegmentSDF = VoxelSDF::SmoothMax(
|
||||||
PassageSDF,
|
SegmentSDF,
|
||||||
NativeFloorZ - static_cast<float>(Pos.Z),
|
NativeFloorZ - static_cast<float>(Pos.Z),
|
||||||
BlendK * 0.35f);
|
BlendK * 0.35f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PassageSDF = VoxelSDF::SmoothMin(PassageSDF, SegmentSDF, BlendK);
|
||||||
|
}
|
||||||
MinSDF = VoxelSDF::SmoothMin(MinSDF, PassageSDF, BlendK);
|
MinSDF = VoxelSDF::SmoothMin(MinSDF, PassageSDF, BlendK);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -3145,6 +3169,30 @@ void UVoxelStrateManager::ApplyPassageNativeFloorMC(
|
|||||||
|
|
||||||
const FVoxelPassage* PassageData = Passages.GetData();
|
const FVoxelPassage* PassageData = Passages.GetData();
|
||||||
const FVector Position(WorldX, WorldY, WorldZ);
|
const FVector Position(WorldX, WorldY, WorldZ);
|
||||||
|
bool bRoomOwnershipChecked = false;
|
||||||
|
bool bInsidePassageRoom = false;
|
||||||
|
const auto IsInsidePassageRoom = [&]()
|
||||||
|
{
|
||||||
|
if (!bRoomOwnershipChecked)
|
||||||
|
{
|
||||||
|
bRoomOwnershipChecked = true;
|
||||||
|
for (const int32 PassageIndex : Nearby)
|
||||||
|
{
|
||||||
|
if (!Passages.IsValidIndex(PassageIndex))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const FVoxelPassage& Passage = PassageData[PassageIndex];
|
||||||
|
if (VF_EvaluatePassageLandingSDF(Position, Passage.UpperLanding) <= 0.0f
|
||||||
|
|| VF_EvaluatePassageLandingSDF(Position, Passage.LowerLanding) <= 0.0f)
|
||||||
|
{
|
||||||
|
bInsidePassageRoom = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bInsidePassageRoom;
|
||||||
|
};
|
||||||
for (const int32 PassageIndex : Nearby)
|
for (const int32 PassageIndex : Nearby)
|
||||||
{
|
{
|
||||||
if (!Passages.IsValidIndex(PassageIndex))
|
if (!Passages.IsValidIndex(PassageIndex))
|
||||||
@@ -3159,27 +3207,124 @@ void UVoxelStrateManager::ApplyPassageNativeFloorMC(
|
|||||||
|
|
||||||
float FloorZ = 0.0f;
|
float FloorZ = 0.0f;
|
||||||
float SupportRadius = 0.0f;
|
float SupportRadius = 0.0f;
|
||||||
if (!VF_ProjectNativePassageFloor(
|
if (VF_ProjectNativePassageFloor(
|
||||||
Passage, Position, FloorZ, SupportRadius,
|
Passage, Position, FloorZ, SupportRadius, this, PassageIndex)
|
||||||
this, PassageIndex))
|
&& Position.Z <= FloorZ + KINDA_SMALL_NUMBER
|
||||||
|
&& Position.Z > FloorZ
|
||||||
|
- VoxelPassageGeometry::LandingFloorThicknessVoxels)
|
||||||
{
|
{
|
||||||
continue;
|
// Room geometry is strate A and owns every overlap. A tunnel's native post must not
|
||||||
}
|
// put a floor patch through a landing room; its own room-floor writer owns support.
|
||||||
if (Position.Z <= FloorZ + KINDA_SMALL_NUMBER
|
if (!IsInsidePassageRoom())
|
||||||
&& Position.Z > FloorZ - VoxelPassageGeometry::LandingFloorThicknessVoxels)
|
|
||||||
{
|
{
|
||||||
// The old support slab is now disabled in the A/B run. This final write is the
|
// Keep the existing narrow floor surface on the segment selected by the passage's
|
||||||
// D-shaped primitive's floor ownership, derived from its build-time profile; it is
|
// full 3D centerline, so a switchback's XY-near neighbor cannot add a second post.
|
||||||
// intentionally not controlled by bPassageSupportFloorWritesEnabled.
|
|
||||||
Density = FMath::Min(Density, -FMath::Max(BaseDensity, 1.0f));
|
Density = FMath::Min(Density, -FMath::Max(BaseDensity, 1.0f));
|
||||||
if (VoxelDensityProfile::AreCountersEnabled())
|
if (VoxelDensityProfile::AreCountersEnabled())
|
||||||
{
|
{
|
||||||
VoxelDensityProfile::AddCounter(
|
VoxelDensityProfile::AddCounter(
|
||||||
VoxelDensityProfile::ECounter::PassageNativeFloorCompositions);
|
VoxelDensityProfile::ECounter::PassageNativeFloorCompositions);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Eval, terrain operations, and disturbance writers have all run before this MC-facing
|
||||||
|
// composition point. Reassert the walkable passage's swept body volume here so their solid
|
||||||
|
// features cannot put a rib or a neighboring floor post back through the through-route.
|
||||||
|
// The profile is evaluated per route segment: switchback legs keep their own floor branch.
|
||||||
|
// Room interiors stay owned by the room SDF, and the floor band remains untouched below the
|
||||||
|
// maximum legal step. This is an air carve over the tunnel's own shape, not a support slab.
|
||||||
|
const float CorridorHalfWidth =
|
||||||
|
0.5f * VoxelPassageGeometry::WalkCorridorPlayerWidthVoxels
|
||||||
|
+ VoxelPassageGeometry::WalkCorridorLatticeMarginVoxels;
|
||||||
|
const float CorridorBottom = VoxelPassageGeometry::MaxStepHeightVoxels;
|
||||||
|
const float CorridorTop = CorridorBottom
|
||||||
|
+ VoxelPassageGeometry::WalkCorridorPlayerHeightVoxels;
|
||||||
|
bool bInsideWalkCorridor = false;
|
||||||
|
for (const int32 PassageIndex : Nearby)
|
||||||
|
{
|
||||||
|
if (!Passages.IsValidIndex(PassageIndex))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const FVoxelPassage& Passage = PassageData[PassageIndex];
|
||||||
|
if (!Passage.bWalkableTunnelContract || !Passage.bNativeFloorEnabled)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 NumSegments = Passage.ControlPoints.Num() - 1;
|
||||||
|
for (int32 SegmentIndex = 0; SegmentIndex < NumSegments; ++SegmentIndex)
|
||||||
|
{
|
||||||
|
float FloorZ = 0.0f;
|
||||||
|
float SupportRadius = 0.0f;
|
||||||
|
float DistanceSquared = FLT_MAX;
|
||||||
|
float HorizontalDistanceSquared = FLT_MAX;
|
||||||
|
if (!VF_ProjectNativePassageFloorSegment(
|
||||||
|
Passage, SegmentIndex, Position,
|
||||||
|
FloorZ, SupportRadius, DistanceSquared,
|
||||||
|
HorizontalDistanceSquared, this, PassageIndex)
|
||||||
|
|| HorizontalDistanceSquared > FMath::Square(CorridorHalfWidth))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float HeightAboveFloor = Position.Z - FloorZ;
|
||||||
|
if (HeightAboveFloor >= CorridorBottom
|
||||||
|
&& HeightAboveFloor <= CorridorTop)
|
||||||
|
{
|
||||||
|
bInsideWalkCorridor = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The audited A-to-B path continues from each mouth to the landing's standing point.
|
||||||
|
// Cover those short approach lines too, while letting the actual landing room retain A
|
||||||
|
// ownership wherever its room SDF contains the query.
|
||||||
|
const auto IsInsideLandingApproach = [&](const FVoxelPassageLanding& Landing)
|
||||||
|
{
|
||||||
|
const FVector2D Start(Landing.DoorPoint.X, Landing.DoorPoint.Y);
|
||||||
|
const FVector2D End(Landing.StandingPoint.X, Landing.StandingPoint.Y);
|
||||||
|
const FVector2D Delta = End - Start;
|
||||||
|
const float LengthSquared = Delta.SizeSquared();
|
||||||
|
if (!(LengthSquared > KINDA_SMALL_NUMBER))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const FVector2D Query(Position.X, Position.Y);
|
||||||
|
const float T = FMath::Clamp(
|
||||||
|
FVector2D::DotProduct(Query - Start, Delta) / LengthSquared,
|
||||||
|
0.0f, 1.0f);
|
||||||
|
const FVector2D Closest = Start + Delta * T;
|
||||||
|
if (FVector2D::DistSquared(Query, Closest)
|
||||||
|
> FMath::Square(CorridorHalfWidth))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const float HeightAboveFloor = Position.Z - Landing.FloorZ;
|
||||||
|
return HeightAboveFloor >= CorridorBottom
|
||||||
|
&& HeightAboveFloor <= CorridorTop;
|
||||||
|
};
|
||||||
|
if (!bInsideWalkCorridor
|
||||||
|
&& (IsInsideLandingApproach(Passage.UpperLanding)
|
||||||
|
|| IsInsideLandingApproach(Passage.LowerLanding)))
|
||||||
|
{
|
||||||
|
bInsideWalkCorridor = true;
|
||||||
|
}
|
||||||
|
if (bInsideWalkCorridor)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bInsideWalkCorridor)
|
||||||
|
{
|
||||||
|
if (!IsInsidePassageRoom())
|
||||||
|
{
|
||||||
|
Density = FMath::Max(Density, FMath::Max(BaseDensity, 1.0f));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void UVoxelStrateManager::ApplyPassageLandingRoomFloorMC(
|
void UVoxelStrateManager::ApplyPassageLandingRoomFloorMC(
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ namespace VoxelPassageGeometry
|
|||||||
constexpr float MinimumRoomHeightVoxels = 12.0f; // 3 m
|
constexpr float MinimumRoomHeightVoxels = 12.0f; // 3 m
|
||||||
constexpr float LandingFloorThicknessVoxels = 3.0f;
|
constexpr float LandingFloorThicknessVoxels = 3.0f;
|
||||||
constexpr float MaxStepHeightVoxels = 1.8f; // 45 cm at 25 cm/voxel
|
constexpr float MaxStepHeightVoxels = 1.8f; // 45 cm at 25 cm/voxel
|
||||||
|
// The A-to-B walk corridor is budgeted as a full player-width rectangle. Its vertical
|
||||||
|
// envelope starts above the legal step and extends one player height above that raised floor.
|
||||||
|
constexpr float WalkCorridorPlayerWidthVoxels = 1.36f;
|
||||||
|
constexpr float WalkCorridorPlayerHeightVoxels = 7.0f;
|
||||||
|
constexpr float WalkCorridorLatticeMarginVoxels = 1.0f;
|
||||||
constexpr float SealSafetyMarginVoxels = 4.0f;
|
constexpr float SealSafetyMarginVoxels = 4.0f;
|
||||||
constexpr float RoomRoundingVoxels = 1.5f;
|
constexpr float RoomRoundingVoxels = 1.5f;
|
||||||
constexpr float LandingCarveBlendVoxels = 4.0f;
|
constexpr float LandingCarveBlendVoxels = 4.0f;
|
||||||
@@ -103,6 +108,25 @@ namespace VoxelPassageGeometry
|
|||||||
return Centreline.Z - FMath::Abs(Radius);
|
return Centreline.Z - FMath::Abs(Radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FORCEINLINE float NativePassageFloorSupportRadius(float StartRadius, float EndRadius)
|
||||||
|
{
|
||||||
|
return FMath::Max(
|
||||||
|
FMath::Min(FMath::Abs(StartRadius), FMath::Abs(EndRadius)) - 0.5f,
|
||||||
|
PlayerRadiusVoxels);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep two switchback floor branches apart far enough that the upper branch's final floor
|
||||||
|
// post cannot enter the lower player's full-width walk corridor. The one-voxel margin covers
|
||||||
|
// the scalar lattice/interpolation boundary; Eval displacement is already followed by the
|
||||||
|
// MC-facing tunnel and native-floor stages before collision density is returned.
|
||||||
|
FORCEINLINE float WalkableTunnelTurnLegSeparation(
|
||||||
|
float StartRadius, float EndRadius)
|
||||||
|
{
|
||||||
|
return NativePassageFloorSupportRadius(StartRadius, EndRadius)
|
||||||
|
+ 0.5f * WalkCorridorPlayerWidthVoxels
|
||||||
|
+ WalkCorridorLatticeMarginVoxels;
|
||||||
|
}
|
||||||
|
|
||||||
// Vertical-shaft tree links are round air capsules, but the player-fit graph needs a
|
// Vertical-shaft tree links are round air capsules, but the player-fit graph needs a
|
||||||
// deterministic support plane to walk between shafts. Keep that plane on the first
|
// deterministic support plane to walk between shafts. Keep that plane on the first
|
||||||
// seal-safe ledge shared by the whole strate, so every tree edge has the same walk level.
|
// seal-safe ledge shared by the whole strate, so every tree edge has the same walk level.
|
||||||
@@ -350,6 +374,7 @@ namespace VoxelPassageGeometry
|
|||||||
const FVector* ControlPointData = ControlPoints.GetData();
|
const FVector* ControlPointData = ControlPoints.GetData();
|
||||||
const float* ControlRadiusData = ControlRadii.GetData();
|
const float* ControlRadiusData = ControlRadii.GetData();
|
||||||
float BestDistanceSquared = FLT_MAX;
|
float BestDistanceSquared = FLT_MAX;
|
||||||
|
float BestHorizontalDistanceSquared = FLT_MAX;
|
||||||
bool bFoundSegment = false;
|
bool bFoundSegment = false;
|
||||||
for (int32 SegmentIndex = 0;
|
for (int32 SegmentIndex = 0;
|
||||||
SegmentIndex + 1 < ControlPointCount;
|
SegmentIndex + 1 < ControlPointCount;
|
||||||
@@ -368,28 +393,58 @@ namespace VoxelPassageGeometry
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const double Dot = (QueryX - AXY_X) * DeltaX
|
const double HorizontalDot = (QueryX - AXY_X) * DeltaX
|
||||||
+ (QueryY - AXY_Y) * DeltaY;
|
+ (QueryY - AXY_Y) * DeltaY;
|
||||||
const float T = FMath::Clamp(
|
const float HorizontalT = FMath::Clamp(
|
||||||
Dot / LengthSquared,
|
HorizontalDot / LengthSquared,
|
||||||
0.0f, 1.0f);
|
0.0f, 1.0f);
|
||||||
const double ClosestX = AXY_X + DeltaX * T;
|
const double ClosestX = AXY_X + DeltaX * HorizontalT;
|
||||||
const double ClosestY = AXY_Y + DeltaY * T;
|
const double ClosestY = AXY_Y + DeltaY * HorizontalT;
|
||||||
const double DistanceX = QueryX - ClosestX;
|
const double DistanceX = QueryX - ClosestX;
|
||||||
const double DistanceY = QueryY - ClosestY;
|
const double DistanceY = QueryY - ClosestY;
|
||||||
const float DistanceSquared = static_cast<float>(
|
const float HorizontalDistanceSquared = static_cast<float>(
|
||||||
DistanceX * DistanceX + DistanceY * DistanceY);
|
DistanceX * DistanceX + DistanceY * DistanceY);
|
||||||
|
|
||||||
|
const float StartRadius = FMath::Abs(ControlRadiusData[SegmentIndex]);
|
||||||
|
const float EndRadius = FMath::Abs(ControlRadiusData[SegmentIndex + 1]);
|
||||||
|
const float StartFloorZ = TunnelFloorZ(A, StartRadius);
|
||||||
|
const float EndFloorZ = TunnelFloorZ(B, EndRadius);
|
||||||
|
const double FloorDeltaZ = static_cast<double>(EndFloorZ - StartFloorZ);
|
||||||
|
const double FullLengthSquared =
|
||||||
|
LengthSquared + FloorDeltaZ * FloorDeltaZ;
|
||||||
|
if (!(FullLengthSquared > KINDA_SMALL_NUMBER))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A switchback may place two different floors at the same XY. Select the segment
|
||||||
|
// nearest to the complete query point on the floor curve, so a lower passage leg
|
||||||
|
// cannot inherit the floor height of a vertically separated upper leg. Keep floor
|
||||||
|
// interpolation horizontal after choosing the branch: FloorZ remains a function of XY.
|
||||||
|
const double FullDot = HorizontalDot
|
||||||
|
+ (static_cast<double>(Position.Z) - StartFloorZ) * FloorDeltaZ;
|
||||||
|
const float SpatialT = FMath::Clamp(
|
||||||
|
FullDot / FullLengthSquared,
|
||||||
|
0.0f, 1.0f);
|
||||||
|
const double SpatialX = AXY_X + DeltaX * SpatialT;
|
||||||
|
const double SpatialY = AXY_Y + DeltaY * SpatialT;
|
||||||
|
const double SpatialZ = StartFloorZ + FloorDeltaZ * SpatialT;
|
||||||
|
const double SpatialDistanceX = QueryX - SpatialX;
|
||||||
|
const double SpatialDistanceY = QueryY - SpatialY;
|
||||||
|
const double SpatialDistanceZ = Position.Z - SpatialZ;
|
||||||
|
const float DistanceSquared = static_cast<float>(
|
||||||
|
SpatialDistanceX * SpatialDistanceX
|
||||||
|
+ SpatialDistanceY * SpatialDistanceY
|
||||||
|
+ SpatialDistanceZ * SpatialDistanceZ);
|
||||||
if (DistanceSquared >= BestDistanceSquared)
|
if (DistanceSquared >= BestDistanceSquared)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const float StartRadius = FMath::Abs(ControlRadiusData[SegmentIndex]);
|
|
||||||
const float EndRadius = FMath::Abs(ControlRadiusData[SegmentIndex + 1]);
|
|
||||||
OutFloorZ = FMath::Lerp(
|
OutFloorZ = FMath::Lerp(
|
||||||
TunnelFloorZ(A, StartRadius),
|
StartFloorZ,
|
||||||
TunnelFloorZ(B, EndRadius),
|
EndFloorZ,
|
||||||
T);
|
HorizontalT);
|
||||||
OutSupportRadius = FMath::Max(
|
OutSupportRadius = FMath::Max(
|
||||||
FMath::Min(StartRadius, EndRadius) - 0.5f,
|
FMath::Min(StartRadius, EndRadius) - 0.5f,
|
||||||
PlayerRadiusVoxels);
|
PlayerRadiusVoxels);
|
||||||
@@ -398,6 +453,7 @@ namespace VoxelPassageGeometry
|
|||||||
*OutSegmentIndex = SegmentIndex;
|
*OutSegmentIndex = SegmentIndex;
|
||||||
}
|
}
|
||||||
BestDistanceSquared = DistanceSquared;
|
BestDistanceSquared = DistanceSquared;
|
||||||
|
BestHorizontalDistanceSquared = HorizontalDistanceSquared;
|
||||||
bFoundSegment = true;
|
bFoundSegment = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,7 +461,7 @@ namespace VoxelPassageGeometry
|
|||||||
&& VoxelMath::IsFinite(OutFloorZ)
|
&& VoxelMath::IsFinite(OutFloorZ)
|
||||||
&& VoxelMath::IsFinite(OutSupportRadius)
|
&& VoxelMath::IsFinite(OutSupportRadius)
|
||||||
&& OutSupportRadius > 0.0f
|
&& OutSupportRadius > 0.0f
|
||||||
&& BestDistanceSquared <= FMath::Square(OutSupportRadius);
|
&& BestHorizontalDistanceSquared <= FMath::Square(OutSupportRadius);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct FOriginLandingGeometry
|
struct FOriginLandingGeometry
|
||||||
|
|||||||
Reference in new issue
Block a user