Each symbol below had zero references in Source/ (tests included), Tools/, Config/ and the build files after comment/string stripping. Functions: EvaluateTunnelCoreSDF, EvaluateSDF (uncached wrapper), IsTunnelSupportFloorWorldPoint, BuildTunnelSupportFloorColumn, IsTunnelSupportFloorColumnZ, GetTunnelSupportFloorColumnBand, VF_TunnelFloorAtSegment, VF_ProjectSweptTunnelFloor, FVoxelWormField EvaluateExact, ApplyPassageLandingAirMC, ApplyPassageTunnelAirMC, GetGenerationParamsFingerprint, FVoxelStrateCorpus::LoadFromSettings, GetSkippedDefinitions, VF_RollStrateParams, VF_ArePlayerFitConnectedWithSampler, VF_GetVoxelSeasonSelectionReasonName, UVoxelSeasonAsset::GetManifestContentHash, FVoxelStackSampler::IsRunning, CompareSkippedPostValue, MakeSurfaceColumnSource, WriteObjTile, ExploreEffectName, ExploreTileClassName, and the VoxelTypes.h chunk-local helpers (WorldToLocalCoord, ChunkToWorldPos, LocalToIndex, IndexToLocal, IsValidLocalCoord, GetFaceDirection, GetFaceNormal, CHUNK_SIZE_SQUARED, CHUNK_VOLUME). Members/types: write-only floor flags (FVoxelOpSample:: bProtectedStructuralFloor, StructuralFloorMinimumDensity, bTunnelCoreSupportFloor; bSupportFloor on the tunnel evaluations; FTunnelFloorSegmentProfile::NumSteps), UVoxelStrateManager::BlendChunks, unused FSummary counters in the stack sampler, two unused FVoxelSeasonCompositionSettings aliases, and a few unused locals/constants. No UPROPERTY, UFUNCTION, reflected type or enum was touched. Behaviour is unchanged: removed code was unreachable or only written, never read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01URDGLsu7c4nunqNFPJEFPU
703 lines
31 KiB
C++
703 lines
31 KiB
C++
// Shared dimensions and construction laws for walkable passages and origin landings.
|
|
//
|
|
// This header is deliberately free of UObjects. The density path, the operator stack, the
|
|
// passage generator, and the headless measurements all include the same arithmetic so a geometry
|
|
// change cannot leave one of those paths with a different room or slope contract.
|
|
|
|
#pragma once
|
|
|
|
#include "CoreMinimal.h"
|
|
#include "VoxelTypes.h"
|
|
|
|
namespace VoxelPassageGeometry
|
|
{
|
|
// Authored scale and player body used by the §6.5 landing calculation.
|
|
constexpr float VoxelSizeMeters = 0.25f;
|
|
constexpr float PlayerRadiusVoxels = 34.0f / 25.0f;
|
|
constexpr float PlayerHalfHeightVoxels = 88.0f / 25.0f;
|
|
constexpr float PlayerHeightVoxels = 2.0f * PlayerHalfHeightVoxels;
|
|
constexpr float HeadroomVoxels = 4.0f; // 1 m
|
|
constexpr float MinimumTurnFloorWidthVoxels = 12.0f; // 3 m
|
|
constexpr float MinimumLandingHalfWidthVoxels =
|
|
MinimumTurnFloorWidthVoxels * 0.5f;
|
|
constexpr float MinimumRoomHeightVoxels = 12.0f; // 3 m
|
|
constexpr float LandingFloorThicknessVoxels = 3.0f;
|
|
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 RoomRoundingVoxels = 1.5f;
|
|
constexpr float LandingCarveBlendVoxels = 4.0f;
|
|
// The origin landing is reserved for the same straight spine that the vertical-shaft
|
|
// generator uses: OriginSpineRadius on each side, with a three-voxel / 0.75 m floor ledge
|
|
// around it after the support patch's one-voxel inset. At the default radius (14 voxels) this
|
|
// is a 28-voxel / 7 m future shaft diameter.
|
|
constexpr float OriginShaftFloorClearanceVoxels = 4.0f;
|
|
// Level tunnel apron outside each room-floor edge. It covers the room/tunnel SDF blend and
|
|
// keeps the ramp from starting while the route is still over the room's square support patch.
|
|
constexpr float WalkableTunnelLandingApronVoxels = 6.0f; // 1.5 m
|
|
// A level transition after a bend lets the route turn before its sloped segment begins.
|
|
constexpr float WalkableTunnelTurnTransitionVoxels = 6.0f; // 1.5 m
|
|
// Keep the tunnel-air post a half voxel above the authored support plane. This is a numerical
|
|
// ownership band: the floor writer owns the plane, while the air writer owns the capsule
|
|
// volume above it even when two overlapping passage projections differ by a fraction.
|
|
constexpr float WalkableTunnelFloorAirClearanceVoxels = 0.5f;
|
|
|
|
// A 15 degree ramp is comfortably below the 44 degree engine walkability ceiling. The
|
|
// latter is a scramble limit, not a sensible default for a tunnel the player walks through.
|
|
constexpr float WalkableTunnelMaxGradientDegrees = 15.0f;
|
|
constexpr float WalkableTunnelMaxGradient = 0.2679491924311227f; // tan(15 degrees)
|
|
// Player-fit measurements use the engine's 44 degree walkability contract. The 15 degree
|
|
// value above remains the preferred authored tunnel ramp; this wider bound is only the
|
|
// fallback threshold at which a room-to-room floor must become a terrace.
|
|
constexpr float PlayerWalkableFloorMaxGradient = 0.9656887748070738f; // tan(44 degrees)
|
|
|
|
// TunnelFloorGentleSlopeThreshold is authored as degrees. Zero is still a legal UI value,
|
|
// but the construction path uses this small positive floor so a malformed/overly strict
|
|
// value cannot turn the graph collect window into an unbounded switchback search.
|
|
constexpr float TunnelFloorGentleSlopeMinimumDegrees = 1.0f;
|
|
constexpr float TunnelFloorGentleSlopeMaximumDegrees = 90.0f;
|
|
// The route solver uses at most six half-waves. The canonical tunnel envelope is the default
|
|
// 360-voxel tunnel reach plus its endpoint correction; round that direct-run envelope up to
|
|
// 640 voxels, then derive the total cap from the same wave limit. This leaves room above the
|
|
// proven canonical route while keeping the 0.1-degree collect window finite and bounded.
|
|
constexpr int32 TunnelFloorMaximumWindWaves = 6;
|
|
constexpr float TunnelFloorMaximumSingleWaveRunVoxels = 640.0f;
|
|
constexpr float TunnelFloorMaximumRouteLengthVoxels =
|
|
static_cast<float>(TunnelFloorMaximumWindWaves)
|
|
* TunnelFloorMaximumSingleWaveRunVoxels;
|
|
|
|
FORCEINLINE float EffectiveTunnelFloorGentleSlopeDegrees(float AuthoredDegrees)
|
|
{
|
|
if (!VoxelMath::IsFinite(AuthoredDegrees))
|
|
{
|
|
return 44.0f;
|
|
}
|
|
return FMath::Clamp(
|
|
FMath::Max(AuthoredDegrees, TunnelFloorGentleSlopeMinimumDegrees),
|
|
TunnelFloorGentleSlopeMinimumDegrees,
|
|
TunnelFloorGentleSlopeMaximumDegrees);
|
|
}
|
|
|
|
FORCEINLINE float TunnelFloorGradientFromDegrees(float AuthoredDegrees)
|
|
{
|
|
const float Degrees = EffectiveTunnelFloorGentleSlopeDegrees(AuthoredDegrees);
|
|
float SinValue = 0.0f;
|
|
float CosValue = 0.0f;
|
|
VoxelMath::DetSinCos(
|
|
SinValue, CosValue, Degrees * (PI / 180.0f));
|
|
// At 90 degrees the mathematical tangent is unbounded. The route solver only needs a
|
|
// finite comparison, so preserve the ordering while keeping every downstream bound finite.
|
|
return FMath::Min(
|
|
FMath::Abs(SinValue) / FMath::Max(FMath::Abs(CosValue), 1.0e-3f),
|
|
4096.0f);
|
|
}
|
|
|
|
// The walkability contract is about the floor carried by the tube, not only its centreline.
|
|
// A tapered tube changes that floor by the radius delta, so the construction and the audit
|
|
// use this same conservative floor delta.
|
|
FORCEINLINE float TunnelFloorZ(const FVector& Centreline, float 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
|
|
// 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.
|
|
// This is shaft-network support, not an origin landing connector.
|
|
FORCEINLINE float VerticalShaftConnectorFloorThickness(float Radius)
|
|
{
|
|
return FMath::Min(
|
|
LandingFloorThicknessVoxels,
|
|
FMath::Max(0.5f, FMath::Abs(Radius) * 0.5f));
|
|
}
|
|
|
|
FORCEINLINE float VerticalShaftConnectorFloorZ(
|
|
float StrateBottomZ,
|
|
float InteriorBottomZ,
|
|
float InteriorTopZ,
|
|
float LedgeSpacing,
|
|
float LedgeDepth,
|
|
float ConnectorRadius)
|
|
{
|
|
const float FloorThickness = VerticalShaftConnectorFloorThickness(ConnectorRadius);
|
|
const float MinimumFloor = InteriorBottomZ
|
|
+ FloorThickness + SealSafetyMarginVoxels;
|
|
const float MaximumFloor = InteriorTopZ
|
|
- PlayerHeightVoxels - SealSafetyMarginVoxels;
|
|
if (!VoxelMath::IsFinite(StrateBottomZ)
|
|
|| !VoxelMath::IsFinite(InteriorBottomZ)
|
|
|| !VoxelMath::IsFinite(InteriorTopZ)
|
|
|| !VoxelMath::IsFinite(LedgeSpacing)
|
|
|| !VoxelMath::IsFinite(LedgeDepth)
|
|
|| !VoxelMath::IsFinite(MinimumFloor)
|
|
|| !VoxelMath::IsFinite(MaximumFloor)
|
|
|| !(MaximumFloor > MinimumFloor))
|
|
{
|
|
return 0.5f * (InteriorBottomZ + InteriorTopZ);
|
|
}
|
|
|
|
if (LedgeSpacing > 0.0f && LedgeDepth > 0.0f)
|
|
{
|
|
const float RelativeBottom = InteriorBottomZ - StrateBottomZ;
|
|
const int32 FirstPeriod = FMath::FloorToInt(RelativeBottom / LedgeSpacing);
|
|
// The small fixed bound keeps malformed authored values from turning generation into
|
|
// an unbounded search. Normal shaft strates find the first or second period.
|
|
constexpr int32 MaxPeriods = 64;
|
|
for (int32 PeriodOffset = 0; PeriodOffset < MaxPeriods; ++PeriodOffset)
|
|
{
|
|
const float PeriodStart = static_cast<float>(FirstPeriod + PeriodOffset)
|
|
* LedgeSpacing;
|
|
const float Candidate = StrateBottomZ + PeriodStart + LedgeDepth;
|
|
if (Candidate > MinimumFloor && Candidate < MaximumFloor)
|
|
{
|
|
return Candidate;
|
|
}
|
|
}
|
|
}
|
|
|
|
return FMath::Clamp(
|
|
0.5f * (InteriorBottomZ + InteriorTopZ), MinimumFloor, MaximumFloor);
|
|
}
|
|
|
|
FORCEINLINE float VerticalShaftConnectorCenterZ(
|
|
float StrateBottomZ,
|
|
float InteriorBottomZ,
|
|
float InteriorTopZ,
|
|
float LedgeSpacing,
|
|
float LedgeDepth,
|
|
float ConnectorRadius)
|
|
{
|
|
const float SafeRadius = FMath::Abs(ConnectorRadius);
|
|
const float FloorThickness = VerticalShaftConnectorFloorThickness(SafeRadius);
|
|
return VerticalShaftConnectorFloorZ(
|
|
StrateBottomZ, InteriorBottomZ, InteriorTopZ,
|
|
LedgeSpacing, LedgeDepth, SafeRadius)
|
|
+ SafeRadius - FloorThickness;
|
|
}
|
|
|
|
/** Add a flat, inset support band below one horizontal shaft-tree capsule. */
|
|
FORCEINLINE void VF_ApplyVerticalShaftConnectorFloor(
|
|
float& Density,
|
|
float WorldX,
|
|
float WorldY,
|
|
float WorldZ,
|
|
const FVector& Start,
|
|
const FVector& End,
|
|
float Radius,
|
|
float FloorZ,
|
|
float BaseDensity)
|
|
{
|
|
const float SafeRadius = FMath::Abs(Radius);
|
|
const float FloorThickness = VerticalShaftConnectorFloorThickness(SafeRadius);
|
|
const float FloorRadius = FMath::Max(0.5f, SafeRadius - 0.5f);
|
|
if (!(SafeRadius > 0.0f)
|
|
|| !(BaseDensity > 0.0f)
|
|
|| !VoxelMath::IsFinite(WorldX)
|
|
|| !VoxelMath::IsFinite(WorldY)
|
|
|| !VoxelMath::IsFinite(WorldZ)
|
|
|| !VoxelMath::IsFinite(FloorZ)
|
|
|| !VoxelMath::IsFinite(Start.X) || !VoxelMath::IsFinite(Start.Y)
|
|
|| !VoxelMath::IsFinite(End.X) || !VoxelMath::IsFinite(End.Y))
|
|
{
|
|
return;
|
|
}
|
|
|
|
const FVector2D A(Start.X, Start.Y);
|
|
const FVector2D Delta(End.X - Start.X, End.Y - Start.Y);
|
|
const float LengthSquared = Delta.SizeSquared();
|
|
const float T = LengthSquared > KINDA_SMALL_NUMBER
|
|
? FMath::Clamp(
|
|
FVector2D::DotProduct(FVector2D(WorldX, WorldY) - A, Delta)
|
|
/ LengthSquared,
|
|
0.0f, 1.0f)
|
|
: 0.0f;
|
|
const FVector2D Closest = A + Delta * T;
|
|
if ((FVector2D(WorldX, WorldY) - Closest).SizeSquared()
|
|
> FMath::Square(FloorRadius))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (WorldZ <= FloorZ + KINDA_SMALL_NUMBER
|
|
&& WorldZ > FloorZ - FloorThickness)
|
|
{
|
|
Density = FMath::Max(Density, BaseDensity);
|
|
}
|
|
}
|
|
|
|
// ApplyVerticalShaftConnectorAir runs before the generator's shared disturbance pass. Keep
|
|
// this one-bit, call-local hand-off so a bridge/ridge cannot refill the core it just opened.
|
|
// It is thread-local state, never a per-voxel cache, and is reset around every density query.
|
|
FORCEINLINE bool& VerticalShaftConnectorAirMarker()
|
|
{
|
|
static thread_local bool bMarked = false;
|
|
return bMarked;
|
|
}
|
|
|
|
FORCEINLINE void ResetVerticalShaftConnectorAirMarker()
|
|
{
|
|
VerticalShaftConnectorAirMarker() = false;
|
|
}
|
|
|
|
/** Reassert the walkable air core above one horizontal shaft-tree floor band. */
|
|
FORCEINLINE void VF_ApplyVerticalShaftConnectorAir(
|
|
float& Density,
|
|
float WorldX,
|
|
float WorldY,
|
|
float WorldZ,
|
|
const FVector& Start,
|
|
const FVector& End,
|
|
float Radius,
|
|
float FloorZ,
|
|
float BaseDensity)
|
|
{
|
|
const float SafeRadius = FMath::Abs(Radius);
|
|
// Leave a one-voxel wall band for the roughness/solid shell, while retaining more than
|
|
// enough width for the 1.36-voxel player capsule and its support stencil.
|
|
const float CoreRadius = FMath::Max(0.5f, SafeRadius - 1.0f);
|
|
if (!(SafeRadius > 0.0f)
|
|
|| !(BaseDensity > 0.0f)
|
|
|| !VoxelMath::IsFinite(WorldX)
|
|
|| !VoxelMath::IsFinite(WorldY)
|
|
|| !VoxelMath::IsFinite(WorldZ)
|
|
|| !VoxelMath::IsFinite(FloorZ)
|
|
|| !VoxelMath::IsFinite(Start.X) || !VoxelMath::IsFinite(Start.Y)
|
|
|| !VoxelMath::IsFinite(Start.Z)
|
|
|| !VoxelMath::IsFinite(End.X) || !VoxelMath::IsFinite(End.Y)
|
|
|| !VoxelMath::IsFinite(End.Z))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// The exact floor sample belongs to the air core's lower ownership edge. The floor
|
|
// writer only owns the three voxels below FloorZ, so clearing from the half-voxel band
|
|
// above it cannot remove support.
|
|
if (WorldZ + KINDA_SMALL_NUMBER
|
|
< FloorZ + WalkableTunnelFloorAirClearanceVoxels)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const FVector Delta = End - Start;
|
|
const float LengthSquared = Delta.SizeSquared();
|
|
const float T = LengthSquared > KINDA_SMALL_NUMBER
|
|
? FMath::Clamp(FVector::DotProduct(FVector(WorldX, WorldY, WorldZ) - Start, Delta)
|
|
/ LengthSquared, 0.0f, 1.0f)
|
|
: 0.0f;
|
|
const FVector Closest = Start + Delta * T;
|
|
if ((FVector(WorldX, WorldY, WorldZ) - Closest).SizeSquared()
|
|
> FMath::Square(CoreRadius))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Internal convention: negative is air, positive is solid. This is intentionally a
|
|
// post-ledge write; a partial shaft shelf must not cap the tree route's player-height
|
|
// envelope after the capsule has already carved it.
|
|
VerticalShaftConnectorAirMarker() = true;
|
|
Density = FMath::Min(Density, -BaseDensity);
|
|
}
|
|
|
|
FORCEINLINE float TunnelFloorGradient(
|
|
const FVector& Start, float StartRadius,
|
|
const FVector& End, float EndRadius)
|
|
{
|
|
const float HorizontalRun = FVector2D(
|
|
End.X - Start.X, End.Y - Start.Y).Size();
|
|
if (HorizontalRun <= KINDA_SMALL_NUMBER)
|
|
{
|
|
return FMath::Abs(TunnelFloorZ(End, EndRadius)
|
|
- TunnelFloorZ(Start, StartRadius)) > KINDA_SMALL_NUMBER
|
|
? FLT_MAX : 0.0f;
|
|
}
|
|
return FMath::Abs(TunnelFloorZ(End, EndRadius)
|
|
- TunnelFloorZ(Start, StartRadius)) / HorizontalRun;
|
|
}
|
|
|
|
FORCEINLINE float RequiredHorizontalRunForFloorDrop(
|
|
float FirstFloorDrop, float SecondFloorDrop)
|
|
{
|
|
return 2.0f * FMath::Max(
|
|
FMath::Abs(FirstFloorDrop), FMath::Abs(SecondFloorDrop))
|
|
/ WalkableTunnelMaxGradient;
|
|
}
|
|
|
|
/** Project a query into the nearest horizontal control segment of a walkable tunnel. */
|
|
FORCEINLINE bool ProjectWalkableTunnelFloor(
|
|
const TArray<FVector>& ControlPoints,
|
|
const TArray<float>& ControlRadii,
|
|
const FVector& Position,
|
|
float& OutFloorZ,
|
|
float& OutSupportRadius,
|
|
int32* OutSegmentIndex = nullptr)
|
|
{
|
|
if (OutSegmentIndex != nullptr)
|
|
{
|
|
*OutSegmentIndex = INDEX_NONE;
|
|
}
|
|
if (ControlPoints.Num() < 2
|
|
|| ControlRadii.Num() != ControlPoints.Num())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const double QueryX = Position.X;
|
|
const double QueryY = Position.Y;
|
|
const int32 ControlPointCount = ControlPoints.Num();
|
|
const FVector* ControlPointData = ControlPoints.GetData();
|
|
const float* ControlRadiusData = ControlRadii.GetData();
|
|
float BestDistanceSquared = FLT_MAX;
|
|
float BestHorizontalDistanceSquared = FLT_MAX;
|
|
bool bFoundSegment = false;
|
|
for (int32 SegmentIndex = 0;
|
|
SegmentIndex + 1 < ControlPointCount;
|
|
++SegmentIndex)
|
|
{
|
|
const FVector& A = ControlPointData[SegmentIndex];
|
|
const FVector& B = ControlPointData[SegmentIndex + 1];
|
|
const double AXY_X = A.X;
|
|
const double AXY_Y = A.Y;
|
|
const double DeltaX = B.X - AXY_X;
|
|
const double DeltaY = B.Y - AXY_Y;
|
|
const float LengthSquared = static_cast<float>(
|
|
DeltaX * DeltaX + DeltaY * DeltaY);
|
|
if (LengthSquared <= KINDA_SMALL_NUMBER)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const double HorizontalDot = (QueryX - AXY_X) * DeltaX
|
|
+ (QueryY - AXY_Y) * DeltaY;
|
|
const float HorizontalT = FMath::Clamp(
|
|
HorizontalDot / LengthSquared,
|
|
0.0f, 1.0f);
|
|
const double ClosestX = AXY_X + DeltaX * HorizontalT;
|
|
const double ClosestY = AXY_Y + DeltaY * HorizontalT;
|
|
const double DistanceX = QueryX - ClosestX;
|
|
const double DistanceY = QueryY - ClosestY;
|
|
const float HorizontalDistanceSquared = static_cast<float>(
|
|
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)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
OutFloorZ = FMath::Lerp(
|
|
StartFloorZ,
|
|
EndFloorZ,
|
|
HorizontalT);
|
|
OutSupportRadius = FMath::Max(
|
|
FMath::Min(StartRadius, EndRadius) - 0.5f,
|
|
PlayerRadiusVoxels);
|
|
if (OutSegmentIndex != nullptr)
|
|
{
|
|
*OutSegmentIndex = SegmentIndex;
|
|
}
|
|
BestDistanceSquared = DistanceSquared;
|
|
BestHorizontalDistanceSquared = HorizontalDistanceSquared;
|
|
bFoundSegment = true;
|
|
}
|
|
|
|
return bFoundSegment
|
|
&& VoxelMath::IsFinite(OutFloorZ)
|
|
&& VoxelMath::IsFinite(OutSupportRadius)
|
|
&& OutSupportRadius > 0.0f
|
|
&& BestHorizontalDistanceSquared <= FMath::Square(OutSupportRadius);
|
|
}
|
|
|
|
struct FOriginLandingGeometry
|
|
{
|
|
bool bValid = false;
|
|
float FloorZ = 0.0f;
|
|
float CeilingZ = 0.0f;
|
|
float HalfWidth = 0.0f;
|
|
float FloorThickness = LandingFloorThicknessVoxels;
|
|
};
|
|
|
|
FORCEINLINE float LandingHalfWidthForRadius(float Radius)
|
|
{
|
|
const float SafeRadius = FMath::Max(FMath::Abs(Radius), 1.0f);
|
|
return FMath::Max(MinimumLandingHalfWidthVoxels, SafeRadius + 2.0f);
|
|
}
|
|
|
|
FORCEINLINE float LandingHeightForRadius(float Radius)
|
|
{
|
|
const float SafeRadius = FMath::Max(FMath::Abs(Radius), 1.0f);
|
|
return FMath::Max(
|
|
FMath::Max(MinimumRoomHeightVoxels, PlayerHeightVoxels + HeadroomVoxels),
|
|
2.0f * SafeRadius + 2.0f);
|
|
}
|
|
|
|
/**
|
|
* Build the one origin landing in a strate.
|
|
*
|
|
* `OriginRadius` is the old spine setting and becomes the room's authored mouth radius. The
|
|
* room dimensions therefore reuse the §6.5 formula exactly:
|
|
* half-width = max(7, radius + 2),
|
|
* height = max(12, capsule height + 4, 2 * radius + 2).
|
|
* The floor is placed against the top of the safe interior interval. This makes the first
|
|
* landing the place reached from above, while every lower landing remains sealed above until
|
|
* progression opens its inter-strate passage.
|
|
*/
|
|
FORCEINLINE FOriginLandingGeometry BuildOriginLandingGeometry(
|
|
float StrateTopZ, float StrateBottomZ, float SealThickness, float OriginRadius)
|
|
{
|
|
FOriginLandingGeometry Result;
|
|
if (!(OriginRadius > 0.0f)
|
|
|| !VoxelMath::IsFinite(StrateTopZ)
|
|
|| !VoxelMath::IsFinite(StrateBottomZ)
|
|
|| !VoxelMath::IsFinite(SealThickness)
|
|
|| !VoxelMath::IsFinite(OriginRadius))
|
|
{
|
|
return Result;
|
|
}
|
|
|
|
// Keep a solid walking ledge around the future shaft, not merely a room whose wall is
|
|
// tangent to it. The shaft diameter is 2 * OriginRadius; the extra four voxels leave a
|
|
// three-voxel / 0.75 m floor margin after the support patch's one-voxel inset at the
|
|
// authored 25 cm scale.
|
|
Result.HalfWidth = FMath::Max(
|
|
LandingHalfWidthForRadius(OriginRadius),
|
|
FMath::Max(FMath::Abs(OriginRadius), 1.0f)
|
|
+ OriginShaftFloorClearanceVoxels);
|
|
const float DesiredHeight = LandingHeightForRadius(OriginRadius);
|
|
const float SafeSeal = FMath::Max(SealThickness, 0.0f);
|
|
const float InnerBottom = StrateBottomZ + SafeSeal;
|
|
const float InnerTop = StrateTopZ - SafeSeal;
|
|
const float MinimumFloor = InnerBottom
|
|
+ Result.FloorThickness + SealSafetyMarginVoxels;
|
|
const float MaximumFloor = InnerTop
|
|
- DesiredHeight - SealSafetyMarginVoxels;
|
|
if (!VoxelMath::IsFinite(InnerBottom) || !VoxelMath::IsFinite(InnerTop)
|
|
|| !(InnerTop > InnerBottom)
|
|
|| !VoxelMath::IsFinite(MinimumFloor)
|
|
|| !VoxelMath::IsFinite(MaximumFloor)
|
|
|| MaximumFloor < MinimumFloor)
|
|
{
|
|
Result.HalfWidth = 0.0f;
|
|
return Result;
|
|
}
|
|
|
|
Result.FloorZ = MaximumFloor;
|
|
Result.CeilingZ = Result.FloorZ + DesiredHeight;
|
|
Result.bValid = true;
|
|
return Result;
|
|
}
|
|
|
|
FORCEINLINE bool IntersectsClosedInterval(float MinA, float MaxA,
|
|
float MinB, float MaxB)
|
|
{
|
|
return MaxA >= MinB && MinA <= MaxB;
|
|
}
|
|
|
|
/** True when the exact MC lattice has at least one sample in a closed interval. */
|
|
FORCEINLINE bool LatticeAxisHasSampleInInterval(
|
|
float Min, float Max, float Origin, int32 Step)
|
|
{
|
|
if (Step <= 0 || !VoxelMath::IsFinite(Min) || !VoxelMath::IsFinite(Max)
|
|
|| !VoxelMath::IsFinite(Origin) || Min > Max)
|
|
{
|
|
return false;
|
|
}
|
|
const float InvStep = 1.0f / static_cast<float>(Step);
|
|
const int32 First = FMath::CeilToInt(
|
|
(Min - Origin) * InvStep - 1.0e-4f);
|
|
const int32 Last = FMath::FloorToInt(
|
|
(Max - Origin) * InvStep + 1.0e-4f);
|
|
return First <= Last;
|
|
}
|
|
|
|
FORCEINLINE bool OriginLandingRoomTouchesLattice(
|
|
const FBox& VoxelBox, const FIntVector& LatticeOrigin, int32 Step,
|
|
float StrateTopZ, float StrateBottomZ, float SealThickness, float OriginRadius,
|
|
float ReachScale = 1.0f)
|
|
{
|
|
if (!VoxelBox.IsValid) return false;
|
|
if (!VoxelMath::IsFinite(ReachScale) || ReachScale <= 0.0f)
|
|
{
|
|
// This is an invalid proof request, not an empty room. The manager treats a
|
|
// malformed scale as conservative/unknown before calling this helper.
|
|
return true;
|
|
}
|
|
const FOriginLandingGeometry Geometry = BuildOriginLandingGeometry(
|
|
StrateTopZ, StrateBottomZ, SealThickness, OriginRadius);
|
|
if (!Geometry.bValid) return false;
|
|
const float Pad = LandingCarveBlendVoxels * ReachScale;
|
|
return LatticeAxisHasSampleInInterval(
|
|
FMath::Max((float)VoxelBox.Min.X, -Geometry.HalfWidth - Pad),
|
|
FMath::Min((float)VoxelBox.Max.X, Geometry.HalfWidth + Pad),
|
|
(float)LatticeOrigin.X, Step)
|
|
&& LatticeAxisHasSampleInInterval(
|
|
FMath::Max((float)VoxelBox.Min.Y, -Geometry.HalfWidth - Pad),
|
|
FMath::Min((float)VoxelBox.Max.Y, Geometry.HalfWidth + Pad),
|
|
(float)LatticeOrigin.Y, Step)
|
|
&& LatticeAxisHasSampleInInterval(
|
|
FMath::Max((float)VoxelBox.Min.Z, Geometry.FloorZ - Pad),
|
|
FMath::Min((float)VoxelBox.Max.Z, Geometry.CeilingZ + Pad),
|
|
(float)LatticeOrigin.Z, Step);
|
|
}
|
|
|
|
FORCEINLINE bool OriginLandingFloorTouchesLattice(
|
|
const FBox& VoxelBox, const FIntVector& LatticeOrigin, int32 Step,
|
|
float StrateTopZ, float StrateBottomZ, float SealThickness, float OriginRadius)
|
|
{
|
|
if (!VoxelBox.IsValid) return false;
|
|
const FOriginLandingGeometry Geometry = BuildOriginLandingGeometry(
|
|
StrateTopZ, StrateBottomZ, SealThickness, OriginRadius);
|
|
if (!Geometry.bValid) return false;
|
|
|
|
constexpr float ProofPad = 1.0f;
|
|
const float FloorHalfWidth = FMath::Max(
|
|
Geometry.HalfWidth - 1.0f, 0.0f);
|
|
return LatticeAxisHasSampleInInterval(
|
|
FMath::Max((float)VoxelBox.Min.X, -FloorHalfWidth - ProofPad),
|
|
FMath::Min((float)VoxelBox.Max.X, FloorHalfWidth + ProofPad),
|
|
(float)LatticeOrigin.X, Step)
|
|
&& LatticeAxisHasSampleInInterval(
|
|
FMath::Max((float)VoxelBox.Min.Y, -FloorHalfWidth - ProofPad),
|
|
FMath::Min((float)VoxelBox.Max.Y, FloorHalfWidth + ProofPad),
|
|
(float)LatticeOrigin.Y, Step)
|
|
&& LatticeAxisHasSampleInInterval(
|
|
FMath::Max((float)VoxelBox.Min.Z,
|
|
Geometry.FloorZ - Geometry.FloorThickness - ProofPad),
|
|
FMath::Min((float)VoxelBox.Max.Z, Geometry.FloorZ + ProofPad),
|
|
(float)LatticeOrigin.Z, Step);
|
|
}
|
|
|
|
/** Conservative influence box for the origin room's air carve. */
|
|
FORCEINLINE bool OriginLandingRoomTouchesBox(
|
|
const FBox& VoxelBox,
|
|
float StrateTopZ, float StrateBottomZ, float SealThickness, float OriginRadius)
|
|
{
|
|
if (!VoxelBox.IsValid) return false;
|
|
const FOriginLandingGeometry Geometry = BuildOriginLandingGeometry(
|
|
StrateTopZ, StrateBottomZ, SealThickness, OriginRadius);
|
|
if (!Geometry.bValid) return false;
|
|
const float Pad = LandingCarveBlendVoxels;
|
|
return IntersectsClosedInterval(
|
|
VoxelBox.Min.X, VoxelBox.Max.X,
|
|
-Geometry.HalfWidth - Pad, Geometry.HalfWidth + Pad)
|
|
&& IntersectsClosedInterval(
|
|
VoxelBox.Min.Y, VoxelBox.Max.Y,
|
|
-Geometry.HalfWidth - Pad, Geometry.HalfWidth + Pad)
|
|
&& IntersectsClosedInterval(
|
|
VoxelBox.Min.Z, VoxelBox.Max.Z,
|
|
Geometry.FloorZ - Pad, Geometry.CeilingZ + Pad);
|
|
}
|
|
|
|
/** Conservative influence box for the origin room's guaranteed solid floor. */
|
|
FORCEINLINE bool OriginLandingFloorTouchesBox(
|
|
const FBox& VoxelBox,
|
|
float StrateTopZ, float StrateBottomZ, float SealThickness, float OriginRadius)
|
|
{
|
|
if (!VoxelBox.IsValid) return false;
|
|
const FOriginLandingGeometry Geometry = BuildOriginLandingGeometry(
|
|
StrateTopZ, StrateBottomZ, SealThickness, OriginRadius);
|
|
if (!Geometry.bValid) return false;
|
|
|
|
// One voxel is a proof margin, not a write margin. It keeps ClassifyTile conservative at
|
|
// an exact lattice edge and mirrors the existing passage-floor guard.
|
|
constexpr float ProofPad = 1.0f;
|
|
const float FloorHalfWidth = FMath::Max(
|
|
Geometry.HalfWidth - 1.0f, 0.0f);
|
|
return IntersectsClosedInterval(
|
|
VoxelBox.Min.X, VoxelBox.Max.X,
|
|
-FloorHalfWidth - ProofPad, FloorHalfWidth + ProofPad)
|
|
&& IntersectsClosedInterval(
|
|
VoxelBox.Min.Y, VoxelBox.Max.Y,
|
|
-FloorHalfWidth - ProofPad, FloorHalfWidth + ProofPad)
|
|
&& IntersectsClosedInterval(
|
|
VoxelBox.Min.Z, VoxelBox.Max.Z,
|
|
Geometry.FloorZ - Geometry.FloorThickness - ProofPad,
|
|
Geometry.FloorZ + ProofPad);
|
|
}
|
|
|
|
/** Signed distance for a rounded origin room with a hard flat floor. */
|
|
FORCEINLINE float OriginLandingRoomSDF(
|
|
const FVector& Position, const FOriginLandingGeometry& Geometry)
|
|
{
|
|
if (!Geometry.bValid || Geometry.CeilingZ <= Geometry.FloorZ)
|
|
{
|
|
return FLT_MAX;
|
|
}
|
|
|
|
const FVector Center(0.0f, 0.0f,
|
|
(Geometry.FloorZ + Geometry.CeilingZ) * 0.5f);
|
|
const float Rounding = RoomRoundingVoxels;
|
|
const float HalfZ = (Geometry.CeilingZ - Geometry.FloorZ) * 0.5f;
|
|
const FVector HalfExtent(
|
|
FMath::Max(Geometry.HalfWidth - Rounding, 0.25f),
|
|
FMath::Max(Geometry.HalfWidth - Rounding, 0.25f),
|
|
FMath::Max(HalfZ - Rounding, 0.25f));
|
|
const FVector D(
|
|
FMath::Abs(Position.X - Center.X) - HalfExtent.X,
|
|
FMath::Abs(Position.Y - Center.Y) - HalfExtent.Y,
|
|
FMath::Abs(Position.Z - Center.Z) - HalfExtent.Z);
|
|
const FVector Outside(
|
|
FMath::Max(D.X, 0.0f),
|
|
FMath::Max(D.Y, 0.0f),
|
|
FMath::Max(D.Z, 0.0f));
|
|
const float Inside = FMath::Min(
|
|
FMath::Max(D.X, FMath::Max(D.Y, D.Z)), 0.0f);
|
|
const float RoundedBoxSDF = Outside.Size() + Inside - Rounding;
|
|
|
|
// Below the floor is rock. Above it the rounded-box air volume is authoritative.
|
|
return FMath::Max(RoundedBoxSDF, Geometry.FloorZ - Position.Z);
|
|
}
|
|
}
|