wip(tunnels): checkpoint -- floor parameters exposed, round cut short by Codex credit limit
⚠️ THIS IS A CHECKPOINT, NOT A FINISHED CHANGE. The agent hit its usage limit
mid-round (locked out until 2026-09-15) after compacting its context, and never
filed a report or capability numbers. Committing so 1,344 lines are not left
uncommitted for five days. Verified by me only to the extent stated below.
WHAT LANDED
The tunnel floor controls are now real strate parameters under
"Cave Morphology|Tunnels|Floor", which is what the owner asked for -- hand-made
assets can tune them and the composer can roll them later:
bTunnelFloorEnabled true
bTunnelFloorTerracingEnabled true
TunnelFloorTerraceStepHeight 1.71 voxels (the existing 42.75 cm riser)
TunnelFloorMaxLedgeHeight 12.0 voxels (3 m -- climbable, not walkable)
TunnelFloorGentleSlopeThreshold tan(44 deg)
TunnelFloorLedgeCountPreference 0 (0 = old per-segment staircase;
>0 = whole-chain ledge policy)
TunnelFloorMaxLedges 4096
⭐ So the staircase-vs-ledges question the owner raised is now answerable IN THE
EDITOR without a rebuild: set LedgeCountPreference to 2-3 and the whole-chain
policy takes over from the per-segment one.
VERIFIED BY ME
build 0 errors
export, 2 runs 1.321 / 1.357 s (ce53552 was 1.384 / 1.405 / 1.415) ~-4%
triangles 153,382 (was 153,338, +44)
geometry hash 9815c9a2..ebeb9 -- CHANGED, so behaviour is live beyond the
stated compatibility defaults
⚠️ CAPABILITY DIPPED SLIGHTLY, and I am not calling this an improvement:
player-fit cells 20,835 -> 20,800 (-35, -0.17%)
reachable cells 10,901 -> 10,886 (-15, -0.14%)
largest component 10,886 = reachable, still one connected mass
Small enough to be a consequence of the 44-triangle geometry shift rather than a
real regression, but it is a drop, and my own rule for this work is that fit and
reachable must not fall. Treat it as unresolved until the round can be finished.
NOT DONE (the substance of the brief):
- terracing objective still minimises riser height, not step count
- the room does not yet clip the tunnel floor where they overlap (A > B)
- the traversal validator still cannot climb, so it cannot score real ledges
- the floor profile is still derived per sample, not authored at build time
- the support-column backstop is still present, i.e. the shape is still not
correct by construction
- dig cost and the streaming-cap audit untouched
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
This commit is contained in:
1 parent
ce53552543
commit
e91f1c44bb
10 files changed
+1318
-65
No files matched your search
@@ -90,6 +90,11 @@ namespace
|
|||||||
// is only used to skip a floor evaluation when the existing SmoothMax is provably the identity;
|
// is only used to skip a floor evaluation when the existing SmoothMax is provably the identity;
|
||||||
// it never changes a value that could affect the isosurface.
|
// it never changes a value that could affect the isosurface.
|
||||||
constexpr float VF_FloorReliefNoiseAbsBound = 1.01f;
|
constexpr float VF_FloorReliefNoiseAbsBound = 1.01f;
|
||||||
|
// A room owns a short, build-authored mouth apron. The full room radius is intentionally
|
||||||
|
// not used here: doing so lets a centre-to-centre tunnel floor overwrite the room floor far
|
||||||
|
// inside the chamber and recreates the broad raised shelf this hand-off is meant to remove.
|
||||||
|
constexpr float VF_TunnelMouthBlendRadiusVoxels =
|
||||||
|
VoxelPassageGeometry::WalkableTunnelLandingApronVoxels;
|
||||||
|
|
||||||
enum class EVFFloorReliefFeature : uint8
|
enum class EVFFloorReliefFeature : uint8
|
||||||
{
|
{
|
||||||
@@ -443,6 +448,326 @@ namespace
|
|||||||
0.0f, VF_MaxTunnelFloorReliefScale);
|
0.0f, VF_MaxTunnelFloorReliefScale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FORCEINLINE float VF_TunnelFloorReliefScaleForGradient(
|
||||||
|
const FVector& A, const FVector& B,
|
||||||
|
int32 SegmentIndex, int32 NumSegments,
|
||||||
|
const FCachedTunnel& Tunnel,
|
||||||
|
float BaseGradient, bool bTerraced)
|
||||||
|
{
|
||||||
|
if (bTerraced
|
||||||
|
|| !(Tunnel.FloorReliefStrength > 0.0f)
|
||||||
|
|| !FMath::IsFinite(Tunnel.FloorReliefStrength)
|
||||||
|
|| !FMath::IsFinite(Tunnel.FloorReliefFrequency))
|
||||||
|
{
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float HorizontalRun = FVector2D(
|
||||||
|
static_cast<float>(B.X - A.X), static_cast<float>(B.Y - A.Y)).Size();
|
||||||
|
if (!(HorizontalRun > KINDA_SMALL_NUMBER)
|
||||||
|
|| !FMath::IsFinite(BaseGradient)
|
||||||
|
|| BaseGradient < 0.0f)
|
||||||
|
{
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float AvailableGradient =
|
||||||
|
VoxelPassageGeometry::PlayerWalkableFloorMaxGradient - BaseGradient;
|
||||||
|
if (!(AvailableGradient > 0.0f))
|
||||||
|
{
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float Frequency = FMath::Abs(Tunnel.FloorReliefFrequency);
|
||||||
|
const float Amplitude = FMath::Abs(Tunnel.FloorReliefStrength)
|
||||||
|
* VOXEL_NOISE_SCALE;
|
||||||
|
const float OctaveFrequencyWeight = 0.65f + 0.35f * 2.3f;
|
||||||
|
const float NoiseGradientBound =
|
||||||
|
1.4142135623730951f * VF_Perlin2DPartialAbsBound
|
||||||
|
* OctaveFrequencyWeight * Frequency * Amplitude;
|
||||||
|
const int32 LandingEnvelopeCount = (SegmentIndex == 0 ? 1 : 0)
|
||||||
|
+ (SegmentIndex + 1 == NumSegments ? 1 : 0);
|
||||||
|
const float EnvelopeDerivativeBound = LandingEnvelopeCount > 0
|
||||||
|
? (1.5f * static_cast<float>(LandingEnvelopeCount)
|
||||||
|
/ VoxelPassageGeometry::WalkableTunnelLandingApronVoxels)
|
||||||
|
* Amplitude
|
||||||
|
: 0.0f;
|
||||||
|
const float ReliefGradientBound = NoiseGradientBound
|
||||||
|
+ EnvelopeDerivativeBound;
|
||||||
|
if (!(ReliefGradientBound > KINDA_SMALL_NUMBER)
|
||||||
|
|| !FMath::IsFinite(ReliefGradientBound))
|
||||||
|
{
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMath::Clamp(
|
||||||
|
FMath::Min(
|
||||||
|
VF_MaxTunnelFloorReliefScale,
|
||||||
|
AvailableGradient / ReliefGradientBound),
|
||||||
|
0.0f, VF_MaxTunnelFloorReliefScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void VF_BuildTunnelFloorProfile(
|
||||||
|
const TArray<FVector>& ControlPoints,
|
||||||
|
const TArray<float>& ControlRadii,
|
||||||
|
const FCachedTunnel& Tunnel,
|
||||||
|
TArray<FTunnelFloorSegmentProfile>& OutProfiles)
|
||||||
|
{
|
||||||
|
OutProfiles.Reset();
|
||||||
|
if (ControlPoints.Num() < 2
|
||||||
|
|| ControlRadii.Num() != ControlPoints.Num())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 NumSegments = ControlPoints.Num() - 1;
|
||||||
|
OutProfiles.SetNum(NumSegments);
|
||||||
|
TArray<float, TInlineAllocator<16>> FloorAtControl;
|
||||||
|
FloorAtControl.SetNum(ControlPoints.Num());
|
||||||
|
for (int32 Index = 0; Index < ControlPoints.Num(); ++Index)
|
||||||
|
{
|
||||||
|
FloorAtControl[Index] = VoxelPassageGeometry::TunnelFloorZ(
|
||||||
|
ControlPoints[Index], FMath::Abs(ControlRadii[Index]));
|
||||||
|
}
|
||||||
|
|
||||||
|
const float GentleThreshold = FMath::IsFinite(
|
||||||
|
Tunnel.TunnelFloorGentleSlopeThreshold)
|
||||||
|
? FMath::Max(Tunnel.TunnelFloorGentleSlopeThreshold, 0.0f)
|
||||||
|
: VoxelPassageGeometry::PlayerWalkableFloorMaxGradient;
|
||||||
|
const float LegacyStepHeight = FMath::IsFinite(
|
||||||
|
Tunnel.TunnelFloorTerraceStepHeight)
|
||||||
|
? FMath::Max(Tunnel.TunnelFloorTerraceStepHeight, 0.0f)
|
||||||
|
: 0.0f;
|
||||||
|
const int32 MaxLedges = FMath::Clamp(
|
||||||
|
Tunnel.TunnelFloorMaxLedges, 1, 4096);
|
||||||
|
const int32 PreferredLedges = FMath::Max(
|
||||||
|
Tunnel.TunnelFloorLedgeCountPreference, 0);
|
||||||
|
|
||||||
|
bool bGlobalTerrace = false;
|
||||||
|
int32 GlobalLedgeCount = 0;
|
||||||
|
if (Tunnel.bTunnelFloorTerracingEnabled && PreferredLedges > 0)
|
||||||
|
{
|
||||||
|
float TotalHorizontalRun = 0.0f;
|
||||||
|
for (int32 SegmentIndex = 0; SegmentIndex < NumSegments; ++SegmentIndex)
|
||||||
|
{
|
||||||
|
TotalHorizontalRun += FVector2D(
|
||||||
|
static_cast<float>(ControlPoints[SegmentIndex + 1].X
|
||||||
|
- ControlPoints[SegmentIndex].X),
|
||||||
|
static_cast<float>(ControlPoints[SegmentIndex + 1].Y
|
||||||
|
- ControlPoints[SegmentIndex].Y)).Size();
|
||||||
|
}
|
||||||
|
const float TotalFloorDelta = FMath::Abs(
|
||||||
|
FloorAtControl.Last() - FloorAtControl[0]);
|
||||||
|
bGlobalTerrace = TotalHorizontalRun > KINDA_SMALL_NUMBER
|
||||||
|
&& TotalFloorDelta > TotalHorizontalRun * GentleThreshold;
|
||||||
|
if (bGlobalTerrace)
|
||||||
|
{
|
||||||
|
const float MaxLedgeHeight = FMath::IsFinite(
|
||||||
|
Tunnel.TunnelFloorMaxLedgeHeight)
|
||||||
|
? FMath::Max(Tunnel.TunnelFloorMaxLedgeHeight, KINDA_SMALL_NUMBER)
|
||||||
|
: KINDA_SMALL_NUMBER;
|
||||||
|
const int32 HeightDrivenCount = FMath::CeilToInt(
|
||||||
|
TotalFloorDelta / MaxLedgeHeight);
|
||||||
|
GlobalLedgeCount = FMath::Clamp(
|
||||||
|
FMath::Max(PreferredLedges, HeightDrivenCount),
|
||||||
|
1, FMath::Min(MaxLedges, NumSegments));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int32 SegmentIndex = 0; SegmentIndex < NumSegments; ++SegmentIndex)
|
||||||
|
{
|
||||||
|
FTunnelFloorSegmentProfile& Profile = OutProfiles[SegmentIndex];
|
||||||
|
const FVector& A = ControlPoints[SegmentIndex];
|
||||||
|
const FVector& B = ControlPoints[SegmentIndex + 1];
|
||||||
|
const float FloorA = FloorAtControl[SegmentIndex];
|
||||||
|
const float FloorB = FloorAtControl[SegmentIndex + 1];
|
||||||
|
const float HorizontalRun = FVector2D(
|
||||||
|
static_cast<float>(B.X - A.X),
|
||||||
|
static_cast<float>(B.Y - A.Y)).Size();
|
||||||
|
Profile.StartFloorZ = FloorA;
|
||||||
|
Profile.EndFloorZ = FloorB;
|
||||||
|
Profile.NumSteps = 0;
|
||||||
|
|
||||||
|
if (bGlobalTerrace && GlobalLedgeCount > 0)
|
||||||
|
{
|
||||||
|
// Place at most one transition at a control-point boundary. Rounding the
|
||||||
|
// normalized control index distributes the few requested ledges over the full
|
||||||
|
// chain and, because the count is capped by NumSegments, never skips a level.
|
||||||
|
const int32 StartLevel = FMath::RoundToInt(
|
||||||
|
static_cast<float>(SegmentIndex * GlobalLedgeCount)
|
||||||
|
/ static_cast<float>(NumSegments));
|
||||||
|
const int32 EndLevel = FMath::RoundToInt(
|
||||||
|
static_cast<float>((SegmentIndex + 1) * GlobalLedgeCount)
|
||||||
|
/ static_cast<float>(NumSegments));
|
||||||
|
const float TotalDelta = FloorAtControl.Last() - FloorAtControl[0];
|
||||||
|
Profile.StartFloorZ = FloorAtControl[0]
|
||||||
|
+ TotalDelta * static_cast<float>(StartLevel)
|
||||||
|
/ static_cast<float>(GlobalLedgeCount);
|
||||||
|
Profile.EndFloorZ = FloorAtControl[0]
|
||||||
|
+ TotalDelta * static_cast<float>(EndLevel)
|
||||||
|
/ static_cast<float>(GlobalLedgeCount);
|
||||||
|
Profile.NumSteps = EndLevel > StartLevel ? 1 : 0;
|
||||||
|
}
|
||||||
|
else if (Tunnel.bTunnelFloorTerracingEnabled
|
||||||
|
&& PreferredLedges == 0
|
||||||
|
&& HorizontalRun > KINDA_SMALL_NUMBER
|
||||||
|
&& LegacyStepHeight > KINDA_SMALL_NUMBER
|
||||||
|
&& FMath::Abs(FloorB - FloorA)
|
||||||
|
> HorizontalRun * GentleThreshold)
|
||||||
|
{
|
||||||
|
Profile.NumSteps = FMath::Clamp(
|
||||||
|
FMath::CeilToInt(FMath::Abs(FloorB - FloorA)
|
||||||
|
/ LegacyStepHeight),
|
||||||
|
1, MaxLedges);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool bTransition = Profile.NumSteps > 0
|
||||||
|
&& !FMath::IsNearlyEqual(Profile.StartFloorZ, Profile.EndFloorZ);
|
||||||
|
const float ProfileGradient = (!bTransition && HorizontalRun > KINDA_SMALL_NUMBER)
|
||||||
|
? FMath::Abs(Profile.EndFloorZ - Profile.StartFloorZ) / HorizontalRun
|
||||||
|
: 0.0f;
|
||||||
|
Profile.ReliefScale = VF_TunnelFloorReliefScaleForGradient(
|
||||||
|
A, B, SegmentIndex, NumSegments, Tunnel,
|
||||||
|
ProfileGradient, bTransition);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (VoxelDensityProfile::AreCountersEnabled())
|
||||||
|
{
|
||||||
|
uint64 AuthoredLedges = 0;
|
||||||
|
for (const FTunnelFloorSegmentProfile& Profile : OutProfiles)
|
||||||
|
{
|
||||||
|
if (Profile.NumSteps > 0)
|
||||||
|
{
|
||||||
|
// In whole-chain mode NumSteps is a transition marker. Compatibility mode
|
||||||
|
// retains the old per-segment count, so report the actual authored step count
|
||||||
|
// in both cases.
|
||||||
|
AuthoredLedges += bGlobalTerrace
|
||||||
|
? 1u : static_cast<uint64>(Profile.NumSteps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VoxelDensityProfile::AddCounter(
|
||||||
|
VoxelDensityProfile::ECounter::TunnelFloorProfileBuilds);
|
||||||
|
VoxelDensityProfile::AddCounter(
|
||||||
|
VoxelDensityProfile::ECounter::TunnelFloorProfileSegments,
|
||||||
|
static_cast<uint64>(OutProfiles.Num()));
|
||||||
|
VoxelDensityProfile::AddCounter(
|
||||||
|
VoxelDensityProfile::ECounter::TunnelFloorProfileLedges,
|
||||||
|
AuthoredLedges);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FVFRoomFloorOwnership
|
||||||
|
{
|
||||||
|
bool bValid = false;
|
||||||
|
float FloorZ = 0.0f;
|
||||||
|
float Weight = 0.0f;
|
||||||
|
uint32 RoomHash = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
static FVFRoomFloorOwnership VF_FindTunnelMouthOwnership(
|
||||||
|
const FVector& Position,
|
||||||
|
const TArray<FVector>& ControlPoints,
|
||||||
|
const TArray<float>& ControlRadii,
|
||||||
|
float MouthBlendRadiusA,
|
||||||
|
float MouthBlendRadiusB,
|
||||||
|
float MouthFloorZA,
|
||||||
|
float MouthFloorZB,
|
||||||
|
uint32 FloorRoomHashA,
|
||||||
|
uint32 FloorRoomHashB,
|
||||||
|
float SDFBlendRadius)
|
||||||
|
{
|
||||||
|
FVFRoomFloorOwnership Result;
|
||||||
|
if (ControlPoints.Num() < 2
|
||||||
|
|| ControlRadii.Num() != ControlPoints.Num())
|
||||||
|
{
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float Fade = FMath::Max(
|
||||||
|
FMath::Max(SDFBlendRadius, 0.0f) * 0.35f,
|
||||||
|
1.0f);
|
||||||
|
float BestScore = FLT_MAX;
|
||||||
|
for (int32 Endpoint = 0; Endpoint < 2; ++Endpoint)
|
||||||
|
{
|
||||||
|
const int32 ControlIndex = Endpoint == 0
|
||||||
|
? 0 : ControlPoints.Num() - 1;
|
||||||
|
const FVector& Mouth = ControlPoints[ControlIndex];
|
||||||
|
const float DX = static_cast<float>(Position.X - Mouth.X);
|
||||||
|
const float DY = static_cast<float>(Position.Y - Mouth.Y);
|
||||||
|
const float Distance = FMath::Sqrt(DX * DX + DY * DY);
|
||||||
|
const float BlendRadius = FMath::Max(
|
||||||
|
Endpoint == 0 ? MouthBlendRadiusA : MouthBlendRadiusB,
|
||||||
|
0.0f);
|
||||||
|
const float RelativeDistance = Distance - BlendRadius;
|
||||||
|
const float Weight = RelativeDistance <= 0.0f
|
||||||
|
? 1.0f
|
||||||
|
: (RelativeDistance < Fade
|
||||||
|
? 1.0f - SmoothStep01(RelativeDistance / Fade)
|
||||||
|
: 0.0f);
|
||||||
|
if (!(Weight > 0.0f))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer the nearest normalized mouth. Endpoint order is the deterministic tie-break
|
||||||
|
// when two large rooms overlap the same tunnel entrance.
|
||||||
|
const float Score = Distance / FMath::Max(BlendRadius, 1.0f);
|
||||||
|
if (Score >= BestScore)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const float DefaultFloorZ = static_cast<float>(Mouth.Z)
|
||||||
|
- FMath::Abs(ControlRadii[ControlIndex]);
|
||||||
|
const float StoredFloorZ = Endpoint == 0 ? MouthFloorZA : MouthFloorZB;
|
||||||
|
const float FloorZ = StoredFloorZ > -FLT_MAX
|
||||||
|
&& FMath::IsFinite(StoredFloorZ)
|
||||||
|
? StoredFloorZ
|
||||||
|
: DefaultFloorZ;
|
||||||
|
if (!FMath::IsFinite(FloorZ))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BestScore = Score;
|
||||||
|
Result.bValid = true;
|
||||||
|
Result.FloorZ = FloorZ;
|
||||||
|
Result.Weight = Weight;
|
||||||
|
Result.RoomHash = Endpoint == 0 ? FloorRoomHashA : FloorRoomHashB;
|
||||||
|
}
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static FVFRoomFloorOwnership VF_FindSDFTunnelMouthOwnership(
|
||||||
|
const FVector& Position,
|
||||||
|
const FCachedTunnel& Tunnel,
|
||||||
|
float SDFBlendRadius)
|
||||||
|
{
|
||||||
|
if (!Tunnel.bHasFloorRoomOwnership)
|
||||||
|
{
|
||||||
|
return FVFRoomFloorOwnership();
|
||||||
|
}
|
||||||
|
return VF_FindTunnelMouthOwnership(
|
||||||
|
Position, Tunnel.ControlPoints, Tunnel.ControlRadii,
|
||||||
|
Tunnel.SDFMouthBlendRadiusA, Tunnel.SDFMouthBlendRadiusB,
|
||||||
|
Tunnel.SDFMouthFloorZA, Tunnel.SDFMouthFloorZB,
|
||||||
|
Tunnel.FloorRoomHashA, Tunnel.FloorRoomHashB, SDFBlendRadius);
|
||||||
|
}
|
||||||
|
|
||||||
|
static FVFRoomFloorOwnership VF_FindWorldTunnelMouthOwnership(
|
||||||
|
const FVector& Position,
|
||||||
|
const FCachedTunnel& Tunnel,
|
||||||
|
float SDFBlendRadius)
|
||||||
|
{
|
||||||
|
if (!Tunnel.bHasFloorRoomOwnership)
|
||||||
|
{
|
||||||
|
return FVFRoomFloorOwnership();
|
||||||
|
}
|
||||||
|
return VF_FindTunnelMouthOwnership(
|
||||||
|
Position, Tunnel.WorldControlPoints, Tunnel.WorldControlRadii,
|
||||||
|
Tunnel.WorldMouthBlendRadiusA, Tunnel.WorldMouthBlendRadiusB,
|
||||||
|
Tunnel.WorldMouthFloorZA, Tunnel.WorldMouthFloorZB,
|
||||||
|
Tunnel.FloorRoomHashA, Tunnel.FloorRoomHashB, SDFBlendRadius);
|
||||||
|
}
|
||||||
|
|
||||||
struct FVFTunnelFloorTerms
|
struct FVFTunnelFloorTerms
|
||||||
{
|
{
|
||||||
float BaseFloorZ = 0.0f;
|
float BaseFloorZ = 0.0f;
|
||||||
@@ -454,15 +779,66 @@ namespace
|
|||||||
const FVector& A, float RadiusA,
|
const FVector& A, float RadiusA,
|
||||||
const FVector& B, float RadiusB,
|
const FVector& B, float RadiusB,
|
||||||
float T, int32 SegmentIndex, int32 NumSegments,
|
float T, int32 SegmentIndex, int32 NumSegments,
|
||||||
const FCachedTunnel& Tunnel)
|
const FCachedTunnel& Tunnel,
|
||||||
|
const TArray<FTunnelFloorSegmentProfile>* FloorProfiles,
|
||||||
|
const FVFRoomFloorOwnership* RoomFloorOwnership)
|
||||||
{
|
{
|
||||||
FVFTunnelFloorTerms Terms;
|
FVFTunnelFloorTerms Terms;
|
||||||
|
if (FloorProfiles != nullptr
|
||||||
|
&& FloorProfiles->Num() == NumSegments
|
||||||
|
&& FloorProfiles->IsValidIndex(SegmentIndex))
|
||||||
|
{
|
||||||
|
const FTunnelFloorSegmentProfile& Profile = (*FloorProfiles)[SegmentIndex];
|
||||||
|
const float ClampedT = FMath::Clamp(T, 0.0f, 1.0f);
|
||||||
|
const float ProfileT = Profile.NumSteps > 0
|
||||||
|
? (ClampedT >= 1.0f
|
||||||
|
? 1.0f
|
||||||
|
: FMath::FloorToFloat(ClampedT
|
||||||
|
* static_cast<float>(Profile.NumSteps))
|
||||||
|
/ static_cast<float>(Profile.NumSteps))
|
||||||
|
: ClampedT;
|
||||||
|
Terms.BaseFloorZ = FMath::Lerp(
|
||||||
|
Profile.StartFloorZ, Profile.EndFloorZ, ProfileT);
|
||||||
|
Terms.ReliefScale = Profile.ReliefScale;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
const float ProfileT = VF_TunnelFloorProfileT(
|
const float ProfileT = VF_TunnelFloorProfileT(
|
||||||
A, RadiusA, B, RadiusB, T);
|
A, RadiusA, B, RadiusB, T);
|
||||||
Terms.BaseFloorZ = FMath::Lerp(
|
Terms.BaseFloorZ = FMath::Lerp(
|
||||||
VoxelPassageGeometry::TunnelFloorZ(A, FMath::Abs(RadiusA)),
|
VoxelPassageGeometry::TunnelFloorZ(A, FMath::Abs(RadiusA)),
|
||||||
VoxelPassageGeometry::TunnelFloorZ(B, FMath::Abs(RadiusB)),
|
VoxelPassageGeometry::TunnelFloorZ(B, FMath::Abs(RadiusB)),
|
||||||
ProfileT);
|
ProfileT);
|
||||||
|
Terms.ReliefScale = VF_TunnelFloorReliefScale(
|
||||||
|
A, RadiusA, B, RadiusB, SegmentIndex, NumSegments, Tunnel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The room is the owner of its own floor volume. A tunnel capsule penetrates past the
|
||||||
|
// mouth into that room; without this reconciliation its floor cut can leave a raised or
|
||||||
|
// lowered apron in the room. Blend only across the SDF mouth band so the tunnel remains
|
||||||
|
// unchanged in open corridor space.
|
||||||
|
const bool bOwnsThisTunnelMouth = RoomFloorOwnership != nullptr
|
||||||
|
&& RoomFloorOwnership->bValid
|
||||||
|
&& Tunnel.bHasFloorRoomOwnership
|
||||||
|
&& (RoomFloorOwnership->RoomHash == Tunnel.FloorRoomHashA
|
||||||
|
|| RoomFloorOwnership->RoomHash == Tunnel.FloorRoomHashB);
|
||||||
|
if (bOwnsThisTunnelMouth)
|
||||||
|
{
|
||||||
|
// A raised tunnel apron is the demonstrated failure mode. Lower it toward the room
|
||||||
|
// floor, but never raise an existing tunnel floor into solid space: that one-sided
|
||||||
|
// ownership rule is conservative for the capability contract and leaves a lower
|
||||||
|
// tunnel to be handled by the room's own floor union.
|
||||||
|
const float OwnedFloorZ = FMath::Min(
|
||||||
|
Terms.BaseFloorZ, RoomFloorOwnership->FloorZ);
|
||||||
|
Terms.BaseFloorZ = FMath::Lerp(
|
||||||
|
Terms.BaseFloorZ,
|
||||||
|
OwnedFloorZ,
|
||||||
|
FMath::Clamp(RoomFloorOwnership->Weight, 0.0f, 1.0f));
|
||||||
|
// The room's floor relief is already included in FloorZ. Fade the tunnel relief out
|
||||||
|
// with the ownership blend, so the transition cannot stack two independent floors.
|
||||||
|
Terms.ReliefScale *= 1.0f - FMath::Clamp(
|
||||||
|
RoomFloorOwnership->Weight, 0.0f, 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
const float HorizontalRun = FVector2D(
|
const float HorizontalRun = FVector2D(
|
||||||
static_cast<float>(B.X - A.X), static_cast<float>(B.Y - A.Y)).Size();
|
static_cast<float>(B.X - A.X), static_cast<float>(B.Y - A.Y)).Size();
|
||||||
@@ -481,8 +857,6 @@ namespace
|
|||||||
/ VoxelPassageGeometry::WalkableTunnelLandingApronVoxels,
|
/ VoxelPassageGeometry::WalkableTunnelLandingApronVoxels,
|
||||||
0.0f, 1.0f));
|
0.0f, 1.0f));
|
||||||
}
|
}
|
||||||
Terms.ReliefScale = VF_TunnelFloorReliefScale(
|
|
||||||
A, RadiusA, B, RadiusB, SegmentIndex, NumSegments, Tunnel);
|
|
||||||
return Terms;
|
return Terms;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -513,11 +887,14 @@ namespace
|
|||||||
const FCachedTunnel& Tunnel,
|
const FCachedTunnel& Tunnel,
|
||||||
const void* CacheIdentity, int32 TunnelIndex,
|
const void* CacheIdentity, int32 TunnelIndex,
|
||||||
bool bEvaluateRelief,
|
bool bEvaluateRelief,
|
||||||
const FVFFloorReliefColumnKey& Column)
|
const FVFFloorReliefColumnKey& Column,
|
||||||
|
const TArray<FTunnelFloorSegmentProfile>* FloorProfiles,
|
||||||
|
const FVFRoomFloorOwnership* RoomFloorOwnership)
|
||||||
{
|
{
|
||||||
const FVFTunnelFloorTerms Terms = VF_TunnelFloorTerms(
|
const FVFTunnelFloorTerms Terms = VF_TunnelFloorTerms(
|
||||||
A, RadiusA, B, RadiusB,
|
A, RadiusA, B, RadiusB,
|
||||||
T, SegmentIndex, NumSegments, Tunnel);
|
T, SegmentIndex, NumSegments, Tunnel, FloorProfiles,
|
||||||
|
RoomFloorOwnership);
|
||||||
return VF_TunnelFloorFromTerms(
|
return VF_TunnelFloorFromTerms(
|
||||||
Position, Tunnel, Terms,
|
Position, Tunnel, Terms,
|
||||||
CacheIdentity, TunnelIndex, bEvaluateRelief, Column);
|
CacheIdentity, TunnelIndex, bEvaluateRelief, Column);
|
||||||
@@ -556,7 +933,9 @@ namespace
|
|||||||
float SDFBlendRadius,
|
float SDFBlendRadius,
|
||||||
bool bApplyFloorCut,
|
bool bApplyFloorCut,
|
||||||
const void* CacheIdentity, int32 TunnelIndex,
|
const void* CacheIdentity, int32 TunnelIndex,
|
||||||
const FVFFloorReliefColumnKey& ReliefColumn)
|
const FVFFloorReliefColumnKey& ReliefColumn,
|
||||||
|
const TArray<FTunnelFloorSegmentProfile>* FloorProfiles,
|
||||||
|
const FVFRoomFloorOwnership* RoomFloorOwnership)
|
||||||
{
|
{
|
||||||
FVFTunnelShapeEvaluation Result;
|
FVFTunnelShapeEvaluation Result;
|
||||||
if (ControlPoints.Num() < 2
|
if (ControlPoints.Num() < 2
|
||||||
@@ -592,7 +971,8 @@ namespace
|
|||||||
|
|
||||||
const FVFTunnelFloorTerms FloorTerms = VF_TunnelFloorTerms(
|
const FVFTunnelFloorTerms FloorTerms = VF_TunnelFloorTerms(
|
||||||
A, RadiusA, B, RadiusB,
|
A, RadiusA, B, RadiusB,
|
||||||
T, SegmentIndex, NumSegments, Tunnel);
|
T, SegmentIndex, NumSegments, Tunnel, FloorProfiles,
|
||||||
|
RoomFloorOwnership);
|
||||||
const float SupportRadius = FMath::Max(
|
const float SupportRadius = FMath::Max(
|
||||||
FMath::Min(FMath::Abs(RadiusA), FMath::Abs(RadiusB)) - 0.5f,
|
FMath::Min(FMath::Abs(RadiusA), FMath::Abs(RadiusB)) - 0.5f,
|
||||||
VoxelPassageGeometry::PlayerRadiusVoxels);
|
VoxelPassageGeometry::PlayerRadiusVoxels);
|
||||||
@@ -623,6 +1003,17 @@ namespace
|
|||||||
+ VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels;
|
+ VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels;
|
||||||
bEvaluateRelief = !bFloorCannotAffect || !bCannotBeSupport;
|
bEvaluateRelief = !bFloorCannotAffect || !bCannotBeSupport;
|
||||||
}
|
}
|
||||||
|
if (bApplyFloorCut && RoomFloorOwnership != nullptr
|
||||||
|
&& RoomFloorOwnership->bValid
|
||||||
|
&& Tunnel.bHasFloorRoomOwnership
|
||||||
|
&& (RoomFloorOwnership->RoomHash == Tunnel.FloorRoomHashA
|
||||||
|
|| RoomFloorOwnership->RoomHash == Tunnel.FloorRoomHashB)
|
||||||
|
&& RoomFloorOwnership->Weight > 0.0f)
|
||||||
|
{
|
||||||
|
// Room ownership can move the floor even when the uncapped tunnel floor is
|
||||||
|
// outside its own relief bound. Keep the exact room relief in that mouth band.
|
||||||
|
bEvaluateRelief = true;
|
||||||
|
}
|
||||||
|
|
||||||
const float FloorZ = VF_TunnelFloorFromTerms(
|
const float FloorZ = VF_TunnelFloorFromTerms(
|
||||||
Position, Tunnel, FloorTerms,
|
Position, Tunnel, FloorTerms,
|
||||||
@@ -633,7 +1024,8 @@ namespace
|
|||||||
: SegmentSDF;
|
: SegmentSDF;
|
||||||
Result.SDF = FMath::Min(Result.SDF, SegmentResultSDF);
|
Result.SDF = FMath::Min(Result.SDF, SegmentResultSDF);
|
||||||
|
|
||||||
if (SegmentSDF <= 0.0f
|
if (bApplyFloorCut && Tunnel.bTunnelFloorEnabled
|
||||||
|
&& SegmentSDF <= 0.0f
|
||||||
&& HorizontalDistanceSquared <= FMath::Square(SupportRadius)
|
&& HorizontalDistanceSquared <= FMath::Square(SupportRadius)
|
||||||
&& Position.Z >= FloorZ
|
&& Position.Z >= FloorZ
|
||||||
- VoxelPassageGeometry::LandingFloorThicknessVoxels
|
- VoxelPassageGeometry::LandingFloorThicknessVoxels
|
||||||
@@ -652,7 +1044,8 @@ namespace
|
|||||||
bool bWorldChain,
|
bool bWorldChain,
|
||||||
float SDFBlendRadius,
|
float SDFBlendRadius,
|
||||||
bool bApplyFloorCut,
|
bool bApplyFloorCut,
|
||||||
const void* CacheIdentity, int32 TunnelIndex)
|
const void* CacheIdentity, int32 TunnelIndex,
|
||||||
|
const FVFRoomFloorOwnership* RoomFloorOwnership = nullptr)
|
||||||
{
|
{
|
||||||
const FVFFloorReliefColumnKey ReliefColumn =
|
const FVFFloorReliefColumnKey ReliefColumn =
|
||||||
VF_MakeFloorReliefColumnKey(
|
VF_MakeFloorReliefColumnKey(
|
||||||
@@ -667,7 +1060,9 @@ namespace
|
|||||||
return VF_EvaluateSweptTunnelChain(
|
return VF_EvaluateSweptTunnelChain(
|
||||||
Position, ShapeControlPoints, ShapeControlRadii,
|
Position, ShapeControlPoints, ShapeControlRadii,
|
||||||
Tunnel, SDFBlendRadius, bApplyFloorCut,
|
Tunnel, SDFBlendRadius, bApplyFloorCut,
|
||||||
CacheIdentity, TunnelIndex, ReliefColumn);
|
CacheIdentity, TunnelIndex, ReliefColumn,
|
||||||
|
bWorldChain ? &Tunnel.WorldFloorProfiles : &Tunnel.FloorProfiles,
|
||||||
|
RoomFloorOwnership);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Tunnel.bHasMidpoint)
|
if (Tunnel.bHasMidpoint)
|
||||||
@@ -685,7 +1080,9 @@ namespace
|
|||||||
return VF_EvaluateSweptTunnelChain(
|
return VF_EvaluateSweptTunnelChain(
|
||||||
Position, Points, Radii,
|
Position, Points, Radii,
|
||||||
Tunnel, SDFBlendRadius, bApplyFloorCut,
|
Tunnel, SDFBlendRadius, bApplyFloorCut,
|
||||||
CacheIdentity, TunnelIndex, ReliefColumn);
|
CacheIdentity, TunnelIndex, ReliefColumn,
|
||||||
|
bWorldChain ? &Tunnel.WorldFloorProfiles : &Tunnel.FloorProfiles,
|
||||||
|
RoomFloorOwnership);
|
||||||
}
|
}
|
||||||
|
|
||||||
TArray<FVector> Points;
|
TArray<FVector> Points;
|
||||||
@@ -697,7 +1094,9 @@ namespace
|
|||||||
return VF_EvaluateSweptTunnelChain(
|
return VF_EvaluateSweptTunnelChain(
|
||||||
Position, Points, Radii,
|
Position, Points, Radii,
|
||||||
Tunnel, SDFBlendRadius, bApplyFloorCut,
|
Tunnel, SDFBlendRadius, bApplyFloorCut,
|
||||||
CacheIdentity, TunnelIndex, ReliefColumn);
|
CacheIdentity, TunnelIndex, ReliefColumn,
|
||||||
|
bWorldChain ? &Tunnel.WorldFloorProfiles : &Tunnel.FloorProfiles,
|
||||||
|
RoomFloorOwnership);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool VF_ProjectSweptTunnelFloor(
|
static bool VF_ProjectSweptTunnelFloor(
|
||||||
@@ -706,8 +1105,13 @@ namespace
|
|||||||
bool bWorldChain,
|
bool bWorldChain,
|
||||||
float& OutFloorZ,
|
float& OutFloorZ,
|
||||||
float& OutSupportRadius,
|
float& OutSupportRadius,
|
||||||
const void* CacheIdentity, int32 TunnelIndex)
|
const void* CacheIdentity, int32 TunnelIndex,
|
||||||
|
const FVFRoomFloorOwnership* RoomFloorOwnership = nullptr)
|
||||||
{
|
{
|
||||||
|
if (!Tunnel.bTunnelFloorEnabled)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const FVFFloorReliefColumnKey ReliefColumn =
|
const FVFFloorReliefColumnKey ReliefColumn =
|
||||||
VF_MakeFloorReliefColumnKey(
|
VF_MakeFloorReliefColumnKey(
|
||||||
static_cast<float>(Position.X), static_cast<float>(Position.Y));
|
static_cast<float>(Position.X), static_cast<float>(Position.Y));
|
||||||
@@ -746,7 +1150,9 @@ namespace
|
|||||||
ControlPoints[SegmentIndex + 1], RadiusB,
|
ControlPoints[SegmentIndex + 1], RadiusB,
|
||||||
T, SegmentIndex, NumSegments, Position, Tunnel,
|
T, SegmentIndex, NumSegments, Position, Tunnel,
|
||||||
CacheIdentity, TunnelIndex, /*bEvaluateRelief=*/true,
|
CacheIdentity, TunnelIndex, /*bEvaluateRelief=*/true,
|
||||||
ReliefColumn);
|
ReliefColumn,
|
||||||
|
bWorldChain ? &Tunnel.WorldFloorProfiles : &Tunnel.FloorProfiles,
|
||||||
|
RoomFloorOwnership);
|
||||||
OutSupportRadius = FMath::Max(
|
OutSupportRadius = FMath::Max(
|
||||||
FMath::Min(FMath::Abs(RadiusA), FMath::Abs(RadiusB)) - 0.5f,
|
FMath::Min(FMath::Abs(RadiusA), FMath::Abs(RadiusB)) - 0.5f,
|
||||||
VoxelPassageGeometry::PlayerRadiusVoxels);
|
VoxelPassageGeometry::PlayerRadiusVoxels);
|
||||||
@@ -768,6 +1174,11 @@ void FChunkSDFCache::Reset()
|
|||||||
// populated regions on adjacent requests.
|
// populated regions on adjacent requests.
|
||||||
Rooms.Reset();
|
Rooms.Reset();
|
||||||
RoomFloorJoins.Reset();
|
RoomFloorJoins.Reset();
|
||||||
|
for (FCachedTunnel& Tunnel : Tunnels)
|
||||||
|
{
|
||||||
|
Tunnel.FloorProfiles.Reset();
|
||||||
|
Tunnel.WorldFloorProfiles.Reset();
|
||||||
|
}
|
||||||
Tunnels.Reset();
|
Tunnels.Reset();
|
||||||
Pits.Reset();
|
Pits.Reset();
|
||||||
Chimneys.Reset();
|
Chimneys.Reset();
|
||||||
@@ -829,6 +1240,8 @@ VoxelDensityProfile::FCacheMemoryBreakdown FChunkSDFCache::GetAllocatedSizeBreak
|
|||||||
Breakdown.TunnelsBytes += ArrayBytes(Tunnel.ControlRadii);
|
Breakdown.TunnelsBytes += ArrayBytes(Tunnel.ControlRadii);
|
||||||
Breakdown.TunnelsBytes += ArrayBytes(Tunnel.WorldControlPoints);
|
Breakdown.TunnelsBytes += ArrayBytes(Tunnel.WorldControlPoints);
|
||||||
Breakdown.TunnelsBytes += ArrayBytes(Tunnel.WorldControlRadii);
|
Breakdown.TunnelsBytes += ArrayBytes(Tunnel.WorldControlRadii);
|
||||||
|
Breakdown.TunnelsBytes += ArrayBytes(Tunnel.FloorProfiles);
|
||||||
|
Breakdown.TunnelsBytes += ArrayBytes(Tunnel.WorldFloorProfiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Breakdown;
|
return Breakdown;
|
||||||
@@ -4773,6 +5186,16 @@ void VoxelCaveMorphology::BuildChunkCache(
|
|||||||
CT.FloorReliefStrength = Params.FloorReliefStrength;
|
CT.FloorReliefStrength = Params.FloorReliefStrength;
|
||||||
CT.FloorReliefFrequency = Params.FloorReliefFrequency;
|
CT.FloorReliefFrequency = Params.FloorReliefFrequency;
|
||||||
CT.FloorSeed = VoxelHash::Mix(TunnelHash ^ 0xF100F1u);
|
CT.FloorSeed = VoxelHash::Mix(TunnelHash ^ 0xF100F1u);
|
||||||
|
CT.bHasFloorRoomOwnership = true;
|
||||||
|
CT.FloorRoomHashA = RoomA.Hash;
|
||||||
|
CT.FloorRoomHashB = RoomB.Hash;
|
||||||
|
CT.bTunnelFloorEnabled = Params.bTunnelFloorEnabled;
|
||||||
|
CT.bTunnelFloorTerracingEnabled = Params.bTunnelFloorTerracingEnabled;
|
||||||
|
CT.TunnelFloorTerraceStepHeight = Params.TunnelFloorTerraceStepHeight;
|
||||||
|
CT.TunnelFloorMaxLedgeHeight = Params.TunnelFloorMaxLedgeHeight;
|
||||||
|
CT.TunnelFloorGentleSlopeThreshold = Params.TunnelFloorGentleSlopeThreshold;
|
||||||
|
CT.TunnelFloorLedgeCountPreference = Params.TunnelFloorLedgeCountPreference;
|
||||||
|
CT.TunnelFloorMaxLedges = Params.TunnelFloorMaxLedges;
|
||||||
|
|
||||||
// A room edge is not a straight capsule between cell centres. Build a deterministic
|
// A room edge is not a straight capsule between cell centres. Build a deterministic
|
||||||
// chain of hash-jittered control points instead. The chain is keyed only by the pair
|
// chain of hash-jittered control points instead. The chain is keyed only by the pair
|
||||||
@@ -4871,6 +5294,44 @@ void VoxelCaveMorphology::BuildChunkCache(
|
|||||||
CT.ControlRadii.Add(ControlRadius);
|
CT.ControlRadii.Add(ControlRadius);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Author the complete floor profile once, after both representations of the chain
|
||||||
|
// are known. Evaluation now only projects onto this immutable profile; no sample can
|
||||||
|
// independently decide how many terraces the tunnel needs.
|
||||||
|
VF_BuildTunnelFloorProfile(
|
||||||
|
CT.ControlPoints, CT.ControlRadii, CT, CT.FloorProfiles);
|
||||||
|
VF_BuildTunnelFloorProfile(
|
||||||
|
CT.WorldControlPoints, CT.WorldControlRadii, CT, CT.WorldFloorProfiles);
|
||||||
|
// The floor hand-off is a short apron around each destination, not a second floor
|
||||||
|
// that owns the whole room. Bake the same bounded region in both coordinate spaces;
|
||||||
|
// this keeps the room/tunnel boolean deterministic even though the cave warp is 3D.
|
||||||
|
CT.SDFMouthBlendRadiusA = FMath::Min(
|
||||||
|
FMath::Max(RoomA.RadiusXY, 0.0f), VF_TunnelMouthBlendRadiusVoxels);
|
||||||
|
CT.SDFMouthBlendRadiusB = FMath::Min(
|
||||||
|
FMath::Max(RoomB.RadiusXY, 0.0f), VF_TunnelMouthBlendRadiusVoxels);
|
||||||
|
CT.WorldMouthBlendRadiusA = CT.SDFMouthBlendRadiusA;
|
||||||
|
CT.WorldMouthBlendRadiusB = CT.SDFMouthBlendRadiusB;
|
||||||
|
if (CT.FloorProfiles.Num() == CT.ControlPoints.Num() - 1
|
||||||
|
&& CT.FloorProfiles.Num() > 0)
|
||||||
|
{
|
||||||
|
CT.SDFMouthFloorZA = CT.FloorProfiles[0].StartFloorZ;
|
||||||
|
CT.SDFMouthFloorZB = CT.FloorProfiles.Last().EndFloorZ;
|
||||||
|
}
|
||||||
|
if (CT.WorldFloorProfiles.Num() == CT.WorldControlPoints.Num() - 1
|
||||||
|
&& CT.WorldFloorProfiles.Num() > 0)
|
||||||
|
{
|
||||||
|
CT.WorldMouthFloorZA = CT.WorldFloorProfiles[0].StartFloorZ;
|
||||||
|
CT.WorldMouthFloorZB = CT.WorldFloorProfiles.Last().EndFloorZ;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CT.WorldMouthFloorZA = static_cast<float>(
|
||||||
|
CT.WorldControlPoints[0].Z)
|
||||||
|
- FMath::Abs(CT.WorldControlRadii[0]);
|
||||||
|
CT.WorldMouthFloorZB = static_cast<float>(
|
||||||
|
CT.WorldControlPoints.Last().Z)
|
||||||
|
- FMath::Abs(CT.WorldControlRadii.Last());
|
||||||
|
}
|
||||||
|
|
||||||
CT.Midpoint = CT.ControlPoints.Num() > 2
|
CT.Midpoint = CT.ControlPoints.Num() > 2
|
||||||
? CT.ControlPoints[CT.ControlPoints.Num() / 2]
|
? CT.ControlPoints[CT.ControlPoints.Num() / 2]
|
||||||
: FVector::ZeroVector;
|
: FVector::ZeroVector;
|
||||||
@@ -5534,10 +5995,15 @@ float VoxelCaveMorphology::EvaluateSDFCached(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The corridor is a swept floor cut: each tapered capsule segment is intersected with
|
// The corridor is a swept floor cut: each tapered capsule segment is intersected with
|
||||||
// its interpolated floor half-space before the tunnel union is blended into rooms.
|
// its interpolated floor half-space before the tunnel union is blended into rooms. The
|
||||||
|
// room wins naturally in this union (SmoothMin): a tunnel floor is a solid half-space,
|
||||||
|
// so it cannot replace the room's air above the room floor. Do not lower the SDF floor
|
||||||
|
// from a proximity-only mouth test; a warped query can be near an endpoint without being
|
||||||
|
// inside that endpoint's room, and changing it there loses capability.
|
||||||
const float TunnelSDF = VF_EvaluateSweptTunnel(
|
const float TunnelSDF = VF_EvaluateSweptTunnel(
|
||||||
Pos, Tunnel, /*bWorldChain=*/false, BlendK,
|
Pos, Tunnel, /*bWorldChain=*/false, BlendK,
|
||||||
/*bApplyFloorCut=*/true, &Cache, TunnelIdx).SDF;
|
/*bApplyFloorCut=*/Tunnel.bTunnelFloorEnabled, &Cache, TunnelIdx,
|
||||||
|
nullptr).SDF;
|
||||||
|
|
||||||
MinSDF = VoxelSDF::SmoothMin(MinSDF, TunnelSDF, BlendK);
|
MinSDF = VoxelSDF::SmoothMin(MinSDF, TunnelSDF, BlendK);
|
||||||
};
|
};
|
||||||
@@ -5648,6 +6114,10 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
|
|||||||
VoxelDensityProfile::ECounter::TunnelCoreEvaluated);
|
VoxelDensityProfile::ECounter::TunnelCoreEvaluated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FVFRoomFloorOwnership RoomMouthOwnership =
|
||||||
|
VF_FindWorldTunnelMouthOwnership(Pos, Tunnel, Cache.SDFBlendRadius);
|
||||||
|
const bool bRoomOwnsThisTunnel = RoomMouthOwnership.bValid;
|
||||||
|
bool bRaisedTunnelFloor = false;
|
||||||
if (bHasWorldChain)
|
if (bHasWorldChain)
|
||||||
{
|
{
|
||||||
if (SupportColumn != nullptr && VoxelDensityProfile::AreCountersEnabled())
|
if (SupportColumn != nullptr && VoxelDensityProfile::AreCountersEnabled())
|
||||||
@@ -5665,7 +6135,25 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
|
|||||||
: VF_ProjectSweptTunnelFloor(
|
: VF_ProjectSweptTunnelFloor(
|
||||||
Pos, Tunnel, /*bWorldChain=*/true, FloorZ, SupportRadius,
|
Pos, Tunnel, /*bWorldChain=*/true, FloorZ, SupportRadius,
|
||||||
&Cache, TunnelIdx);
|
&Cache, TunnelIdx);
|
||||||
if (bProjectedFloor
|
bRaisedTunnelFloor = bRoomOwnsThisTunnel
|
||||||
|
&& bProjectedFloor
|
||||||
|
&& FloorZ > RoomMouthOwnership.FloorZ
|
||||||
|
+ KINDA_SMALL_NUMBER;
|
||||||
|
if (bRaisedTunnelFloor
|
||||||
|
&& WorldZ <= RoomMouthOwnership.FloorZ
|
||||||
|
+ VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels)
|
||||||
|
{
|
||||||
|
if (WorldZ > RoomMouthOwnership.FloorZ
|
||||||
|
- VoxelPassageGeometry::LandingFloorThicknessVoxels)
|
||||||
|
{
|
||||||
|
Result.bRoomFloor = true;
|
||||||
|
}
|
||||||
|
// A raised tunnel apron is the artifact being clipped. Keep the room floor in
|
||||||
|
// its finite support band and do not reopen the room below it.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!bRaisedTunnelFloor
|
||||||
|
&& bProjectedFloor
|
||||||
&& WorldZ <= FloorZ
|
&& WorldZ <= FloorZ
|
||||||
+ VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels
|
+ VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels
|
||||||
&& (SupportColumn == nullptr
|
&& (SupportColumn == nullptr
|
||||||
@@ -5675,20 +6163,21 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
|
|||||||
{
|
{
|
||||||
Result.bSupportFloor = true;
|
Result.bSupportFloor = true;
|
||||||
}
|
}
|
||||||
if (bProjectedFloor && WorldZ <= FloorZ
|
if (!bRaisedTunnelFloor
|
||||||
|
&& bProjectedFloor && WorldZ <= FloorZ
|
||||||
+ VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels)
|
+ VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels)
|
||||||
{
|
{
|
||||||
// Preserve the established ownership contract while the SDF-space evaluator is
|
// Preserve the established ownership contract while the SDF-space evaluator is
|
||||||
// still raw: the floor owns the lower half of the corridor, and the capsule owns
|
// still raw: the floor owns the lower half of the corridor, and the capsule owns
|
||||||
// only the air above it. This diagnostic is what distinguishes a coordinate-space
|
// only the air above it. This is a shape-level ownership hand-off, not a later slab.
|
||||||
// mismatch from a genuine swept-floor capability loss.
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const FVFTunnelShapeEvaluation TunnelShape = VF_EvaluateSweptTunnel(
|
const FVFTunnelShapeEvaluation TunnelShape = VF_EvaluateSweptTunnel(
|
||||||
Pos, Tunnel, bHasWorldChain, Cache.SDFBlendRadius,
|
Pos, Tunnel, bHasWorldChain, Cache.SDFBlendRadius,
|
||||||
/*bApplyFloorCut=*/true, &Cache, TunnelIdx);
|
/*bApplyFloorCut=*/Tunnel.bTunnelFloorEnabled && !bRaisedTunnelFloor,
|
||||||
|
&Cache, TunnelIdx, nullptr);
|
||||||
Result.SDF = FMath::Min(Result.SDF, TunnelShape.SDF);
|
Result.SDF = FMath::Min(Result.SDF, TunnelShape.SDF);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -5749,9 +6238,28 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FVFRoomFloorOwnership RoomMouthOwnership =
|
||||||
|
VF_FindWorldTunnelMouthOwnership(Pos, Tunnel, Cache.SDFBlendRadius);
|
||||||
|
if (RoomMouthOwnership.bValid)
|
||||||
|
{
|
||||||
|
float ProjectedFloorZ = 0.0f;
|
||||||
|
float SupportRadius = 0.0f;
|
||||||
|
if (VF_ProjectSweptTunnelFloor(
|
||||||
|
Pos, Tunnel, bHasWorldChain, ProjectedFloorZ, SupportRadius,
|
||||||
|
&Cache, TunnelIdx)
|
||||||
|
&& ProjectedFloorZ > RoomMouthOwnership.FloorZ + KINDA_SMALL_NUMBER)
|
||||||
|
{
|
||||||
|
// Only a raised tunnel apron is clipped. If the tunnel is lower, the room's
|
||||||
|
// SmoothMax floor already wins naturally and retaining this support band is
|
||||||
|
// capability-safe for legacy callers.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return VF_EvaluateSweptTunnel(
|
return VF_EvaluateSweptTunnel(
|
||||||
Pos, Tunnel, bHasWorldChain, Cache.SDFBlendRadius,
|
Pos, Tunnel, bHasWorldChain, Cache.SDFBlendRadius,
|
||||||
/*bApplyFloorCut=*/true, &Cache, TunnelIdx).bSupportFloor;
|
/*bApplyFloorCut=*/Tunnel.bTunnelFloorEnabled, &Cache, TunnelIdx,
|
||||||
|
nullptr).bSupportFloor;
|
||||||
};
|
};
|
||||||
|
|
||||||
bool bSupport = false;
|
bool bSupport = false;
|
||||||
@@ -5791,6 +6299,8 @@ void VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
|
|||||||
static_cast<uint64>(Cache.Tunnels.Num()));
|
static_cast<uint64>(Cache.Tunnels.Num()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FVector ColumnPosition(WorldX, WorldY, 0.0f);
|
||||||
|
|
||||||
for (int32 TunnelIdx = 0; TunnelIdx < Cache.Tunnels.Num(); ++TunnelIdx)
|
for (int32 TunnelIdx = 0; TunnelIdx < Cache.Tunnels.Num(); ++TunnelIdx)
|
||||||
{
|
{
|
||||||
if (!Cache.Tunnels.IsValidIndex(TunnelIdx))
|
if (!Cache.Tunnels.IsValidIndex(TunnelIdx))
|
||||||
@@ -5798,7 +6308,6 @@ void VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const FCachedTunnel& Tunnel = Cache.Tunnels[TunnelIdx];
|
const FCachedTunnel& Tunnel = Cache.Tunnels[TunnelIdx];
|
||||||
const FVector ColumnPosition(WorldX, WorldY, 0.0f);
|
|
||||||
float FloorZ = 0.0f;
|
float FloorZ = 0.0f;
|
||||||
float SupportRadius = 0.0f;
|
float SupportRadius = 0.0f;
|
||||||
if (!VF_ProjectSweptTunnelFloor(
|
if (!VF_ProjectSweptTunnelFloor(
|
||||||
|
|||||||
@@ -414,6 +414,11 @@ namespace VoxelDensityProfile
|
|||||||
case ECounter::TunnelSupportColumnCandidates:return TEXT("TunnelSupportColumnCandidates");
|
case ECounter::TunnelSupportColumnCandidates:return TEXT("TunnelSupportColumnCandidates");
|
||||||
case ECounter::TunnelSupportFloorQueries:return TEXT("TunnelSupportFloorQueries");
|
case ECounter::TunnelSupportFloorQueries:return TEXT("TunnelSupportFloorQueries");
|
||||||
case ECounter::TunnelSupportFloorChecks:return TEXT("TunnelSupportFloorChecks");
|
case ECounter::TunnelSupportFloorChecks:return TEXT("TunnelSupportFloorChecks");
|
||||||
|
case ECounter::TunnelSupportFloorBackstopFires:return TEXT("TunnelSupportFloorBackstopFires");
|
||||||
|
case ECounter::TunnelRoomFloorBackstopFires:return TEXT("TunnelRoomFloorBackstopFires");
|
||||||
|
case ECounter::TunnelFloorProfileBuilds:return TEXT("TunnelFloorProfileBuilds");
|
||||||
|
case ECounter::TunnelFloorProfileSegments:return TEXT("TunnelFloorProfileSegments");
|
||||||
|
case ECounter::TunnelFloorProfileLedges:return TEXT("TunnelFloorProfileLedges");
|
||||||
case ECounter::PassageCandidates: return TEXT("PassageCandidates");
|
case ECounter::PassageCandidates: return TEXT("PassageCandidates");
|
||||||
case ECounter::PassageEvaluated: return TEXT("PassageEvaluated");
|
case ECounter::PassageEvaluated: return TEXT("PassageEvaluated");
|
||||||
case ECounter::MesherBlockTests: return TEXT("MesherBlockTests");
|
case ECounter::MesherBlockTests: return TEXT("MesherBlockTests");
|
||||||
|
|||||||
@@ -1540,11 +1540,30 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
|||||||
if (bHaveTunnelCore)
|
if (bHaveTunnelCore)
|
||||||
{
|
{
|
||||||
const bool bTunnelSupportFloor = TunnelCore.bSupportFloor;
|
const bool bTunnelSupportFloor = TunnelCore.bSupportFloor;
|
||||||
|
if (TunnelCore.bRoomFloor)
|
||||||
|
{
|
||||||
|
if (VoxelDensityProfile::AreCountersEnabled())
|
||||||
|
{
|
||||||
|
VoxelDensityProfile::AddCounter(
|
||||||
|
VoxelDensityProfile::ECounter::TunnelRoomFloorBackstopFires);
|
||||||
|
}
|
||||||
|
// A tunnel that penetrates a room cannot reassert its own floor through the
|
||||||
|
// room's floor. The morphology query marks only the finite room support band;
|
||||||
|
// preserve it after the generic post-disturbance air backstop.
|
||||||
|
Result = FMath::Min(
|
||||||
|
Result,
|
||||||
|
-FMath::Max(LandingBaseDensity * 2.0f, 1.0f));
|
||||||
|
}
|
||||||
// The swept capsule owns the relief profile, while this finite support band is
|
// The swept capsule owns the relief profile, while this finite support band is
|
||||||
// the conservative post-disturbance backstop that keeps terrain modifiers from
|
// the conservative post-disturbance backstop that keeps terrain modifiers from
|
||||||
// removing the only player support at a mouth or a narrow cross-section.
|
// removing the only player support at a mouth or a narrow cross-section.
|
||||||
if (bTunnelSupportFloor)
|
if (bTunnelSupportFloor)
|
||||||
{
|
{
|
||||||
|
if (VoxelDensityProfile::AreCountersEnabled())
|
||||||
|
{
|
||||||
|
VoxelDensityProfile::AddCounter(
|
||||||
|
VoxelDensityProfile::ECounter::TunnelSupportFloorBackstopFires);
|
||||||
|
}
|
||||||
Result = FMath::Min(
|
Result = FMath::Min(
|
||||||
Result,
|
Result,
|
||||||
-FMath::Max(LandingBaseDensity * 2.0f, 1.0f));
|
-FMath::Max(LandingBaseDensity * 2.0f, 1.0f));
|
||||||
@@ -2727,6 +2746,11 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
|
|||||||
// operator-stack path; its support band follows the same relieved swept floor.
|
// operator-stack path; its support band follows the same relieved swept floor.
|
||||||
if (bTunnelSupportFloor)
|
if (bTunnelSupportFloor)
|
||||||
{
|
{
|
||||||
|
if (VoxelDensityProfile::AreCountersEnabled())
|
||||||
|
{
|
||||||
|
VoxelDensityProfile::AddCounter(
|
||||||
|
VoxelDensityProfile::ECounter::TunnelSupportFloorBackstopFires);
|
||||||
|
}
|
||||||
Density = FMath::Max(Density, FMath::Max(Params.BaseDensity * 2.0f, 1.0f));
|
Density = FMath::Max(Density, FMath::Max(Params.BaseDensity * 2.0f, 1.0f));
|
||||||
}
|
}
|
||||||
const float TunnelCoreSDF = (Params.RoomDensity > 0.0f && Params.RoomSpacing > 0.0f)
|
const float TunnelCoreSDF = (Params.RoomDensity > 0.0f && Params.RoomSpacing > 0.0f)
|
||||||
|
|||||||
@@ -1137,6 +1137,9 @@ namespace
|
|||||||
if (FieldName == TEXT("TunnelWarpStrength")) return Set(0.0f, H);
|
if (FieldName == TEXT("TunnelWarpStrength")) return Set(0.0f, H);
|
||||||
if (FieldName == TEXT("TunnelHorizontalBias")) return Set(0.0f, 1.0f);
|
if (FieldName == TEXT("TunnelHorizontalBias")) return Set(0.0f, 1.0f);
|
||||||
if (FieldName == TEXT("TunnelEndpointZOffset")) return Set(0.0f, 1.0f);
|
if (FieldName == TEXT("TunnelEndpointZOffset")) return Set(0.0f, 1.0f);
|
||||||
|
if (FieldName == TEXT("TunnelFloorTerraceStepHeight")) return Set(0.25f, FMath::Max(2.0f, HQuarter));
|
||||||
|
if (FieldName == TEXT("TunnelFloorMaxLedgeHeight")) return Set(1.0f, FMath::Max(4.0f, HQuarter));
|
||||||
|
if (FieldName == TEXT("TunnelFloorGentleSlopeThreshold")) return Set(0.0f, 1.5f);
|
||||||
if (FieldName == TEXT("SDFBlendRadius")) return Set(2.0f, FMath::Max(4.0f, H * 0.15f));
|
if (FieldName == TEXT("SDFBlendRadius")) return Set(2.0f, FMath::Max(4.0f, H * 0.15f));
|
||||||
if (FieldName == TEXT("WaterLevelRelative")) return Set(0.0f, 1.0f);
|
if (FieldName == TEXT("WaterLevelRelative")) return Set(0.0f, 1.0f);
|
||||||
if (FieldName == TEXT("CaveWarpStrength")) return Set(0.0f, HHalf);
|
if (FieldName == TEXT("CaveWarpStrength")) return Set(0.0f, HHalf);
|
||||||
@@ -1368,6 +1371,28 @@ namespace
|
|||||||
Params.bTunnelsFlowTowardOrigin = CategoryRng.FRand() < 0.5f;
|
Params.bTunnelsFlowTowardOrigin = CategoryRng.FRand() < 0.5f;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (FCString::Strcmp(FieldName, TEXT("bTunnelFloorEnabled")) == 0)
|
||||||
|
{
|
||||||
|
// The floor is a capability feature, not a corpus-free variation axis. Keep it on
|
||||||
|
// for every generated candidate so a roll cannot manufacture an invalid bare tube.
|
||||||
|
Params.bTunnelFloorEnabled = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (FCString::Strcmp(FieldName, TEXT("bTunnelFloorTerracingEnabled")) == 0)
|
||||||
|
{
|
||||||
|
Params.bTunnelFloorTerracingEnabled = CategoryRng.FRand() < 0.75f;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (FCString::Strcmp(FieldName, TEXT("TunnelFloorLedgeCountPreference")) == 0)
|
||||||
|
{
|
||||||
|
Params.TunnelFloorLedgeCountPreference = CategoryRng.RandRange(0, 3);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (FCString::Strcmp(FieldName, TEXT("TunnelFloorMaxLedges")) == 0)
|
||||||
|
{
|
||||||
|
Params.TunnelFloorMaxLedges = CategoryRng.RandRange(1, 8);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (FCString::Strcmp(FieldName, TEXT("RoughnessNoiseType")) == 0)
|
if (FCString::Strcmp(FieldName, TEXT("RoughnessNoiseType")) == 0)
|
||||||
{
|
{
|
||||||
Params.RoughnessNoiseType = static_cast<EVoxelNoiseType>(CategoryRng.RandRange(0, 3));
|
Params.RoughnessNoiseType = static_cast<EVoxelNoiseType>(CategoryRng.RandRange(0, 3));
|
||||||
|
|||||||
@@ -468,6 +468,244 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
OutX = InPlane - OutY * Grid.NumX;
|
OutX = InPlane - OutY * Grid.NumX;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct FPlayerTraversalNeighbour
|
||||||
|
{
|
||||||
|
int32 Cell = INDEX_NONE;
|
||||||
|
bool bClimb = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool IsEligiblePlayerFitCell(
|
||||||
|
const FSampleGrid& Grid,
|
||||||
|
const TArray<uint8>& PlayerFitMask,
|
||||||
|
int32 Cell)
|
||||||
|
{
|
||||||
|
return Cell >= 0
|
||||||
|
&& Cell < Grid.CellCount
|
||||||
|
&& Grid.Air[Cell] != 0u
|
||||||
|
&& PlayerFitMask.IsValidIndex(Cell)
|
||||||
|
&& PlayerFitMask[Cell] != 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsClimbableSurfaceEdge(
|
||||||
|
const FSampleGrid& Grid,
|
||||||
|
int32 First,
|
||||||
|
int32 Second,
|
||||||
|
float FirstSupportHeight,
|
||||||
|
float SecondSupportHeight)
|
||||||
|
{
|
||||||
|
int32 FirstX = 0;
|
||||||
|
int32 FirstY = 0;
|
||||||
|
int32 FirstZ = 0;
|
||||||
|
DecodeIndex(Grid, First, FirstX, FirstY, FirstZ);
|
||||||
|
int32 SecondX = 0;
|
||||||
|
int32 SecondY = 0;
|
||||||
|
int32 SecondZ = 0;
|
||||||
|
DecodeIndex(Grid, Second, SecondX, SecondY, SecondZ);
|
||||||
|
if (FMath::Abs(FirstX - SecondX) + FMath::Abs(FirstY - SecondY) != 1)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool bFirstIsHigher = FirstSupportHeight > SecondSupportHeight;
|
||||||
|
const int32 LowerColumnX = bFirstIsHigher ? SecondX : FirstX;
|
||||||
|
const int32 LowerColumnY = bFirstIsHigher ? SecondY : FirstY;
|
||||||
|
const int32 HigherColumnX = bFirstIsHigher ? FirstX : SecondX;
|
||||||
|
const int32 HigherColumnY = bFirstIsHigher ? FirstY : SecondY;
|
||||||
|
const float LowerHeight = FMath::Min(FirstSupportHeight, SecondSupportHeight);
|
||||||
|
const float HigherHeight = FMath::Max(FirstSupportHeight, SecondSupportHeight);
|
||||||
|
const float Step = static_cast<float>(Grid.SampleStep);
|
||||||
|
if (!FMath::IsFinite(Step) || Step <= 0.0f
|
||||||
|
|| !FMath::IsFinite(LowerHeight) || !FMath::IsFinite(HigherHeight))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A climb edge must meet a solid riser, not connect two independently supported
|
||||||
|
// floating poses across an empty gap. Exclude the top support cell itself: that cell is
|
||||||
|
// present for every supported platform and is not evidence of a climbable face.
|
||||||
|
const int32 FirstRiserZ = FMath::Max(
|
||||||
|
0,
|
||||||
|
FMath::FloorToInt((LowerHeight - Grid.MinZ) / Step));
|
||||||
|
const int32 LastRiserZ = FMath::Min(
|
||||||
|
Grid.NumZ - 1,
|
||||||
|
FMath::CeilToInt((HigherHeight - Grid.MinZ) / Step) - 2);
|
||||||
|
for (int32 Z = FirstRiserZ; Z <= LastRiserZ; ++Z)
|
||||||
|
{
|
||||||
|
if (Grid.Air[Grid.Index(HigherColumnX, HigherColumnY, Z)] == 0u
|
||||||
|
|| Grid.Air[Grid.Index(LowerColumnX, LowerColumnY, Z)] == 0u)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsPlayerTraversalEdge(
|
||||||
|
const FSampleGrid& Grid,
|
||||||
|
const TArray<uint8>& PlayerFitMask,
|
||||||
|
const TArray<float>* PlayerFitSupportHeights,
|
||||||
|
const FVoxelStrateMeasureSettings* Settings,
|
||||||
|
int32 First,
|
||||||
|
int32 Second,
|
||||||
|
bool& bOutClimb)
|
||||||
|
{
|
||||||
|
bOutClimb = false;
|
||||||
|
if (!IsEligiblePlayerFitCell(Grid, PlayerFitMask, First)
|
||||||
|
|| !IsEligiblePlayerFitCell(Grid, PlayerFitMask, Second))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (PlayerFitSupportHeights == nullptr || Settings == nullptr)
|
||||||
|
{
|
||||||
|
// Callers that do not carry support data retain the historical six-neighbour graph.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!PlayerFitSupportHeights->IsValidIndex(First)
|
||||||
|
|| !PlayerFitSupportHeights->IsValidIndex(Second))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 FirstX = 0;
|
||||||
|
int32 FirstY = 0;
|
||||||
|
int32 FirstZ = 0;
|
||||||
|
DecodeIndex(Grid, First, FirstX, FirstY, FirstZ);
|
||||||
|
int32 SecondX = 0;
|
||||||
|
int32 SecondY = 0;
|
||||||
|
int32 SecondZ = 0;
|
||||||
|
DecodeIndex(Grid, Second, SecondX, SecondY, SecondZ);
|
||||||
|
const int32 DeltaX = FMath::Abs(FirstX - SecondX);
|
||||||
|
const int32 DeltaY = FMath::Abs(FirstY - SecondY);
|
||||||
|
const int32 DeltaZ = FMath::Abs(FirstZ - SecondZ);
|
||||||
|
if (DeltaX + DeltaY == 0 && DeltaZ == 1)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (DeltaX + DeltaY != 1)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float FirstHeight = (*PlayerFitSupportHeights)[First];
|
||||||
|
const float SecondHeight = (*PlayerFitSupportHeights)[Second];
|
||||||
|
if (!FMath::IsFinite(FirstHeight) || !FMath::IsFinite(SecondHeight))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const float VoxelSize = FVoxelPlayerCapsuleConstants::VoxelSizeMeters;
|
||||||
|
const float StepHeightVoxels = Settings->PlayerMaxStepHeightMeters / VoxelSize;
|
||||||
|
const float ClimbHeightVoxels = Settings->PlayerMaxClimbHeightMeters / VoxelSize;
|
||||||
|
if (!FMath::IsFinite(StepHeightVoxels) || StepHeightVoxels < 0.0f
|
||||||
|
|| !FMath::IsFinite(ClimbHeightVoxels) || ClimbHeightVoxels < 0.0f)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const float HeightDelta = FMath::Abs(FirstHeight - SecondHeight);
|
||||||
|
if (HeightDelta <= StepHeightVoxels + KINDA_SMALL_NUMBER)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!Settings->bPlayerClimbingEnabled
|
||||||
|
|| HeightDelta > ClimbHeightVoxels + KINDA_SMALL_NUMBER)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bOutClimb = IsClimbableSurfaceEdge(
|
||||||
|
Grid, First, Second, FirstHeight, SecondHeight);
|
||||||
|
return bOutClimb;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppendPlayerTraversalNeighbours(
|
||||||
|
const FSampleGrid& Grid,
|
||||||
|
const TArray<uint8>& PlayerFitMask,
|
||||||
|
const TArray<float>* PlayerFitSupportHeights,
|
||||||
|
const FVoxelStrateMeasureSettings* Settings,
|
||||||
|
int32 Cell,
|
||||||
|
TArray<FPlayerTraversalNeighbour, TInlineAllocator<128>>& OutNeighbours)
|
||||||
|
{
|
||||||
|
OutNeighbours.Reset();
|
||||||
|
if (Cell < 0 || Cell >= Grid.CellCount)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 X = 0;
|
||||||
|
int32 Y = 0;
|
||||||
|
int32 Z = 0;
|
||||||
|
DecodeIndex(Grid, Cell, X, Y, Z);
|
||||||
|
auto TryAdd = [&](int32 Neighbor)
|
||||||
|
{
|
||||||
|
if (Neighbor == INDEX_NONE || Neighbor == Cell)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bool bClimb = false;
|
||||||
|
if (IsPlayerTraversalEdge(
|
||||||
|
Grid, PlayerFitMask, PlayerFitSupportHeights, Settings,
|
||||||
|
Cell, Neighbor, bClimb))
|
||||||
|
{
|
||||||
|
OutNeighbours.Add({ Neighbor, bClimb });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Preserve the old direction order for ordinary movement. Extra climb edges are appended
|
||||||
|
// in a fixed direction/height order, so the route and all derived counts are stable.
|
||||||
|
if (X + 1 < Grid.NumX) TryAdd(Grid.Index(X + 1, Y, Z));
|
||||||
|
if (X > 0) TryAdd(Grid.Index(X - 1, Y, Z));
|
||||||
|
if (Y + 1 < Grid.NumY) TryAdd(Grid.Index(X, Y + 1, Z));
|
||||||
|
if (Y > 0) TryAdd(Grid.Index(X, Y - 1, Z));
|
||||||
|
if (Z + 1 < Grid.NumZ) TryAdd(Grid.Index(X, Y, Z + 1));
|
||||||
|
if (Z > 0) TryAdd(Grid.Index(X, Y, Z - 1));
|
||||||
|
|
||||||
|
if (PlayerFitSupportHeights == nullptr || Settings == nullptr
|
||||||
|
|| !Settings->bPlayerClimbingEnabled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const float VoxelSize = FVoxelPlayerCapsuleConstants::VoxelSizeMeters;
|
||||||
|
const float ClimbHeightVoxels = Settings->PlayerMaxClimbHeightMeters / VoxelSize;
|
||||||
|
if (!FMath::IsFinite(ClimbHeightVoxels) || ClimbHeightVoxels <= 0.0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const int32 SearchCells = FMath::Min(
|
||||||
|
4096,
|
||||||
|
FMath::Max(1, FMath::CeilToInt(ClimbHeightVoxels)));
|
||||||
|
const int32 DirectionX[4] = { 1, -1, 0, 0 };
|
||||||
|
const int32 DirectionY[4] = { 0, 0, 1, -1 };
|
||||||
|
for (int32 Direction = 0; Direction < 4; ++Direction)
|
||||||
|
{
|
||||||
|
const int32 TargetX = X + DirectionX[Direction];
|
||||||
|
const int32 TargetY = Y + DirectionY[Direction];
|
||||||
|
if (TargetX < 0 || TargetX >= Grid.NumX
|
||||||
|
|| TargetY < 0 || TargetY >= Grid.NumY)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int32 OffsetZ = -SearchCells; OffsetZ <= SearchCells; ++OffsetZ)
|
||||||
|
{
|
||||||
|
if (OffsetZ == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const int32 TargetZ = Z + OffsetZ;
|
||||||
|
if (TargetZ >= 0 && TargetZ < Grid.NumZ)
|
||||||
|
{
|
||||||
|
const int32 Neighbor = Grid.Index(TargetX, TargetY, TargetZ);
|
||||||
|
bool bClimb = false;
|
||||||
|
if (IsPlayerTraversalEdge(
|
||||||
|
Grid, PlayerFitMask, PlayerFitSupportHeights, Settings,
|
||||||
|
Cell, Neighbor, bClimb)
|
||||||
|
&& bClimb)
|
||||||
|
{
|
||||||
|
OutNeighbours.Add({ Neighbor, true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void FloodFillAir(
|
void FloodFillAir(
|
||||||
const FSampleGrid& Grid,
|
const FSampleGrid& Grid,
|
||||||
TArray<int32>& OutComponents,
|
TArray<int32>& OutComponents,
|
||||||
@@ -477,7 +715,9 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
int32& OutLargestComponentLowestCell,
|
int32& OutLargestComponentLowestCell,
|
||||||
int32& OutNumComponentsAtLeast1Pct,
|
int32& OutNumComponentsAtLeast1Pct,
|
||||||
TArray<int64>* OutComponentCells,
|
TArray<int64>* OutComponentCells,
|
||||||
const TArray<uint8>* EligibilityMask = nullptr)
|
const TArray<uint8>* EligibilityMask = nullptr,
|
||||||
|
const TArray<float>* PlayerFitSupportHeights = nullptr,
|
||||||
|
const FVoxelStrateMeasureSettings* TraversalSettings = nullptr)
|
||||||
{
|
{
|
||||||
OutComponents.SetNumUninitialized(Grid.CellCount);
|
OutComponents.SetNumUninitialized(Grid.CellCount);
|
||||||
for (int32 Index = 0; Index < Grid.CellCount; ++Index)
|
for (int32 Index = 0; Index < Grid.CellCount; ++Index)
|
||||||
@@ -533,6 +773,21 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (EligibilityMask != nullptr
|
||||||
|
&& PlayerFitSupportHeights != nullptr
|
||||||
|
&& TraversalSettings != nullptr)
|
||||||
|
{
|
||||||
|
TArray<FPlayerTraversalNeighbour, TInlineAllocator<128>> Neighbours;
|
||||||
|
AppendPlayerTraversalNeighbours(
|
||||||
|
Grid, *EligibilityMask, PlayerFitSupportHeights,
|
||||||
|
TraversalSettings, Current, Neighbours);
|
||||||
|
for (const FPlayerTraversalNeighbour& Neighbour : Neighbours)
|
||||||
|
{
|
||||||
|
Visit(Neighbour.Cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
// Fixed order keeps every derived value independent of container iteration order.
|
// Fixed order keeps every derived value independent of container iteration order.
|
||||||
if (X + 1 < Grid.NumX) Visit(Grid.Index(X + 1, Y, Z));
|
if (X + 1 < Grid.NumX) Visit(Grid.Index(X + 1, Y, Z));
|
||||||
if (X > 0) Visit(Grid.Index(X - 1, Y, Z));
|
if (X > 0) Visit(Grid.Index(X - 1, Y, Z));
|
||||||
@@ -541,6 +796,7 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
if (Z + 1 < Grid.NumZ) Visit(Grid.Index(X, Y, Z + 1));
|
if (Z + 1 < Grid.NumZ) Visit(Grid.Index(X, Y, Z + 1));
|
||||||
if (Z > 0) Visit(Grid.Index(X, Y, Z - 1));
|
if (Z > 0) Visit(Grid.Index(X, Y, Z - 1));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const int64 ComponentCells64 = static_cast<int64>(ComponentCells);
|
const int64 ComponentCells64 = static_cast<int64>(ComponentCells);
|
||||||
if (OutComponentCells != nullptr)
|
if (OutComponentCells != nullptr)
|
||||||
@@ -1079,10 +1335,15 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
const FVoxelStrateMeasureSettings& Settings,
|
const FVoxelStrateMeasureSettings& Settings,
|
||||||
TArray<uint8>& OutPlayerFit,
|
TArray<uint8>& OutPlayerFit,
|
||||||
int64& OutNumPlayerFitCells,
|
int64& OutNumPlayerFitCells,
|
||||||
FString& OutReason)
|
FString& OutReason,
|
||||||
|
TArray<float>* OutPlayerFitSupportHeights = nullptr)
|
||||||
{
|
{
|
||||||
OutPlayerFit.Reset();
|
OutPlayerFit.Reset();
|
||||||
OutNumPlayerFitCells = 0;
|
OutNumPlayerFitCells = 0;
|
||||||
|
if (OutPlayerFitSupportHeights != nullptr)
|
||||||
|
{
|
||||||
|
OutPlayerFitSupportHeights->Reset();
|
||||||
|
}
|
||||||
if (Grid.SampleStep != 1)
|
if (Grid.SampleStep != 1)
|
||||||
{
|
{
|
||||||
return Refuse(
|
return Refuse(
|
||||||
@@ -1155,6 +1416,10 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
}
|
}
|
||||||
|
|
||||||
OutPlayerFit.Init(0u, Grid.CellCount);
|
OutPlayerFit.Init(0u, Grid.CellCount);
|
||||||
|
if (OutPlayerFitSupportHeights != nullptr)
|
||||||
|
{
|
||||||
|
OutPlayerFitSupportHeights->Init(-FLT_MAX, Grid.CellCount);
|
||||||
|
}
|
||||||
TArray<int64> PlayerFitCounts;
|
TArray<int64> PlayerFitCounts;
|
||||||
PlayerFitCounts.Init(0, Grid.NumZ);
|
PlayerFitCounts.Init(0, Grid.NumZ);
|
||||||
|
|
||||||
@@ -1261,6 +1526,10 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
OutPlayerFit[Current] = 1u;
|
OutPlayerFit[Current] = 1u;
|
||||||
|
if (OutPlayerFitSupportHeights != nullptr)
|
||||||
|
{
|
||||||
|
(*OutPlayerFitSupportHeights)[Current] = SupportHeight;
|
||||||
|
}
|
||||||
++NumPlayerFitInSlice;
|
++NumPlayerFitInSlice;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1411,7 +1680,8 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
const FSampleGrid& Grid,
|
const FSampleGrid& Grid,
|
||||||
const FVoxelStrateMeasureSettings& Settings,
|
const FVoxelStrateMeasureSettings& Settings,
|
||||||
FVoxelStrateMetrics& InOutMetrics,
|
FVoxelStrateMetrics& InOutMetrics,
|
||||||
TArray<uint8>* OutPlayerFitMask = nullptr)
|
TArray<uint8>* OutPlayerFitMask = nullptr,
|
||||||
|
TArray<float>* OutPlayerFitSupportHeights = nullptr)
|
||||||
{
|
{
|
||||||
InOutMetrics.bPlayerFitResolved = false;
|
InOutMetrics.bPlayerFitResolved = false;
|
||||||
InOutMetrics.PlayerFitRefusalReason.Reset();
|
InOutMetrics.PlayerFitRefusalReason.Reset();
|
||||||
@@ -1423,9 +1693,11 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
InOutMetrics.MinimumPlayerClearanceVoxels = 0.0f;
|
InOutMetrics.MinimumPlayerClearanceVoxels = 0.0f;
|
||||||
|
|
||||||
TArray<uint8> PlayerFit;
|
TArray<uint8> PlayerFit;
|
||||||
|
TArray<float> PlayerFitSupportHeights;
|
||||||
if (!BuildPlayerFitMask(
|
if (!BuildPlayerFitMask(
|
||||||
Grid, Settings, PlayerFit, InOutMetrics.NumPlayerFitCells,
|
Grid, Settings, PlayerFit, InOutMetrics.NumPlayerFitCells,
|
||||||
InOutMetrics.PlayerFitRefusalReason))
|
InOutMetrics.PlayerFitRefusalReason,
|
||||||
|
&PlayerFitSupportHeights))
|
||||||
{
|
{
|
||||||
if (OutPlayerFitMask != nullptr)
|
if (OutPlayerFitMask != nullptr)
|
||||||
{
|
{
|
||||||
@@ -1453,7 +1725,9 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
LargestComponentLowestCell,
|
LargestComponentLowestCell,
|
||||||
NumComponentsAtLeast1Pct,
|
NumComponentsAtLeast1Pct,
|
||||||
nullptr,
|
nullptr,
|
||||||
&PlayerFit);
|
&PlayerFit,
|
||||||
|
&PlayerFitSupportHeights,
|
||||||
|
&Settings);
|
||||||
InOutMetrics.NumTraversableComponents = NumComponents;
|
InOutMetrics.NumTraversableComponents = NumComponents;
|
||||||
InOutMetrics.LargestTraversableComponentCells = LargestComponentCells;
|
InOutMetrics.LargestTraversableComponentCells = LargestComponentCells;
|
||||||
InOutMetrics.TraversableComponentShare = InOutMetrics.NumPlayerFitCells > 0
|
InOutMetrics.TraversableComponentShare = InOutMetrics.NumPlayerFitCells > 0
|
||||||
@@ -1466,13 +1740,18 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
{
|
{
|
||||||
*OutPlayerFitMask = MoveTemp(PlayerFit);
|
*OutPlayerFitMask = MoveTemp(PlayerFit);
|
||||||
}
|
}
|
||||||
|
if (OutPlayerFitSupportHeights != nullptr)
|
||||||
|
{
|
||||||
|
*OutPlayerFitSupportHeights = MoveTemp(PlayerFitSupportHeights);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void BuildPlayerFitConnectivityMetrics(
|
void BuildPlayerFitConnectivityMetrics(
|
||||||
const FSampleGrid& Grid,
|
const FSampleGrid& Grid,
|
||||||
const FVoxelStrateMeasureSettings& Settings,
|
const FVoxelStrateMeasureSettings& Settings,
|
||||||
FVoxelStrateMetrics& OutMetrics,
|
FVoxelStrateMetrics& OutMetrics,
|
||||||
TArray<uint8>& OutPlayerFitMask)
|
TArray<uint8>& OutPlayerFitMask,
|
||||||
|
TArray<float>& OutPlayerFitSupportHeights)
|
||||||
{
|
{
|
||||||
OutMetrics = FVoxelStrateMetrics();
|
OutMetrics = FVoxelStrateMetrics();
|
||||||
OutMetrics.ResolvedMarginVoxels = Grid.ResolvedMarginVoxels;
|
OutMetrics.ResolvedMarginVoxels = Grid.ResolvedMarginVoxels;
|
||||||
@@ -1492,7 +1771,9 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
? static_cast<float>(static_cast<double>(OutMetrics.NumAir)
|
? static_cast<float>(static_cast<double>(OutMetrics.NumAir)
|
||||||
/ static_cast<double>(OutMetrics.NumSampled))
|
/ static_cast<double>(OutMetrics.NumSampled))
|
||||||
: 0.0f;
|
: 0.0f;
|
||||||
BuildPlayerFitMetrics(Grid, Settings, OutMetrics, &OutPlayerFitMask);
|
BuildPlayerFitMetrics(
|
||||||
|
Grid, Settings, OutMetrics, &OutPlayerFitMask,
|
||||||
|
&OutPlayerFitSupportHeights);
|
||||||
OutMetrics.bValid = OutMetrics.NumSampled > 0;
|
OutMetrics.bValid = OutMetrics.NumSampled > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1951,7 +2232,9 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
const FVector& B,
|
const FVector& B,
|
||||||
const TArray<int32>& Path,
|
const TArray<int32>& Path,
|
||||||
FCoarseEdge& OutFailedEdge,
|
FCoarseEdge& OutFailedEdge,
|
||||||
const TArray<uint8>* EligibilityMask = nullptr)
|
const TArray<uint8>* EligibilityMask = nullptr,
|
||||||
|
const TArray<float>* PlayerFitSupportHeights = nullptr,
|
||||||
|
const FVoxelStrateMeasureSettings* TraversalSettings = nullptr)
|
||||||
{
|
{
|
||||||
OutFailedEdge = FCoarseEdge();
|
OutFailedEdge = FCoarseEdge();
|
||||||
|
|
||||||
@@ -1972,6 +2255,20 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (PathIndex > 0
|
||||||
|
&& PlayerFitSupportHeights != nullptr
|
||||||
|
&& TraversalSettings != nullptr)
|
||||||
|
{
|
||||||
|
bool bClimb = false;
|
||||||
|
if (!IsPlayerTraversalEdge(
|
||||||
|
Grid, *EligibilityMask, PlayerFitSupportHeights,
|
||||||
|
TraversalSettings, Path[PathIndex - 1], Path[PathIndex], bClimb))
|
||||||
|
{
|
||||||
|
OutFailedEdge = MakeCoarseEdge(
|
||||||
|
Path[PathIndex - 1], Path[PathIndex]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
int32 StartX = 0;
|
int32 StartX = 0;
|
||||||
int32 StartY = 0;
|
int32 StartY = 0;
|
||||||
@@ -2045,7 +2342,9 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
int32 Goal,
|
int32 Goal,
|
||||||
const TArray<FCoarseEdge>& BlockedEdges,
|
const TArray<FCoarseEdge>& BlockedEdges,
|
||||||
TArray<int32>& OutPath,
|
TArray<int32>& OutPath,
|
||||||
const TArray<uint8>* EligibilityMask = nullptr)
|
const TArray<uint8>* EligibilityMask = nullptr,
|
||||||
|
const TArray<float>* PlayerFitSupportHeights = nullptr,
|
||||||
|
const FVoxelStrateMeasureSettings* TraversalSettings = nullptr)
|
||||||
{
|
{
|
||||||
const auto IsEligible = [&, EligibilityMask](int32 Cell)
|
const auto IsEligible = [&, EligibilityMask](int32 Cell)
|
||||||
{
|
{
|
||||||
@@ -2090,6 +2389,21 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (EligibilityMask != nullptr
|
||||||
|
&& PlayerFitSupportHeights != nullptr
|
||||||
|
&& TraversalSettings != nullptr)
|
||||||
|
{
|
||||||
|
TArray<FPlayerTraversalNeighbour, TInlineAllocator<128>> Neighbours;
|
||||||
|
AppendPlayerTraversalNeighbours(
|
||||||
|
Grid, *EligibilityMask, PlayerFitSupportHeights,
|
||||||
|
TraversalSettings, Current, Neighbours);
|
||||||
|
for (const FPlayerTraversalNeighbour& Neighbour : Neighbours)
|
||||||
|
{
|
||||||
|
Visit(Neighbour.Cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
// The order is fixed so the recovered route is deterministic. This is a real BFS
|
// The order is fixed so the recovered route is deterministic. This is a real BFS
|
||||||
// parent chain, not just a component-membership answer.
|
// parent chain, not just a component-membership answer.
|
||||||
if (X + 1 < Grid.NumX) Visit(Grid.Index(X + 1, Y, Z));
|
if (X + 1 < Grid.NumX) Visit(Grid.Index(X + 1, Y, Z));
|
||||||
@@ -2099,6 +2413,7 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
if (Z + 1 < Grid.NumZ) Visit(Grid.Index(X, Y, Z + 1));
|
if (Z + 1 < Grid.NumZ) Visit(Grid.Index(X, Y, Z + 1));
|
||||||
if (Z > 0) Visit(Grid.Index(X, Y, Z - 1));
|
if (Z > 0) Visit(Grid.Index(X, Y, Z - 1));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Parent[Goal] == -2)
|
if (Parent[Goal] == -2)
|
||||||
{
|
{
|
||||||
@@ -2137,7 +2452,9 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
bool& bOutGoalSnapped,
|
bool& bOutGoalSnapped,
|
||||||
int32& OutNumRouteRetries,
|
int32& OutNumRouteRetries,
|
||||||
int32 MaxRouteRetries,
|
int32 MaxRouteRetries,
|
||||||
const TArray<uint8>* EligibilityMask = nullptr)
|
const TArray<uint8>* EligibilityMask = nullptr,
|
||||||
|
const TArray<float>* PlayerFitSupportHeights = nullptr,
|
||||||
|
const FVoxelStrateMeasureSettings* TraversalSettings = nullptr)
|
||||||
{
|
{
|
||||||
OutStart = -1;
|
OutStart = -1;
|
||||||
OutGoal = -1;
|
OutGoal = -1;
|
||||||
@@ -2175,7 +2492,8 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
for (;;)
|
for (;;)
|
||||||
{
|
{
|
||||||
if (!FindCoarsePath(
|
if (!FindCoarsePath(
|
||||||
Grid, OutStart, OutGoal, BlockedEdges, Path, EligibilityMask))
|
Grid, OutStart, OutGoal, BlockedEdges, Path, EligibilityMask,
|
||||||
|
PlayerFitSupportHeights, TraversalSettings))
|
||||||
{
|
{
|
||||||
// This is a negative verdict only after all edges excluded by earlier
|
// This is a negative verdict only after all edges excluded by earlier
|
||||||
// full-resolution failures leave no coarse route at this resolution.
|
// full-resolution failures leave no coarse route at this resolution.
|
||||||
@@ -2188,7 +2506,7 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
FCoarseEdge FailedEdge;
|
FCoarseEdge FailedEdge;
|
||||||
if (FullResolutionPathIsAir(
|
if (FullResolutionPathIsAir(
|
||||||
Generator, Sampler, Grid, AVoxel, BVoxel, Path, FailedEdge,
|
Generator, Sampler, Grid, AVoxel, BVoxel, Path, FailedEdge,
|
||||||
EligibilityMask))
|
EligibilityMask, PlayerFitSupportHeights, TraversalSettings))
|
||||||
{
|
{
|
||||||
return EVoxelConnectivityResult::Connected;
|
return EVoxelConnectivityResult::Connected;
|
||||||
}
|
}
|
||||||
@@ -2259,7 +2577,9 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
FVoxelConnectivityDiagnostics& OutDiagnostics,
|
FVoxelConnectivityDiagnostics& OutDiagnostics,
|
||||||
const TArray<uint8>* EligibilityMask = nullptr,
|
const TArray<uint8>* EligibilityMask = nullptr,
|
||||||
TArray<int32>* OutComponents = nullptr,
|
TArray<int32>* OutComponents = nullptr,
|
||||||
int32* OutNumComponents = nullptr)
|
int32* OutNumComponents = nullptr,
|
||||||
|
const TArray<float>* PlayerFitSupportHeights = nullptr,
|
||||||
|
const FVoxelStrateMeasureSettings* TraversalSettings = nullptr)
|
||||||
{
|
{
|
||||||
OutDiagnostics = FVoxelConnectivityDiagnostics();
|
OutDiagnostics = FVoxelConnectivityDiagnostics();
|
||||||
if (OutComponents != nullptr)
|
if (OutComponents != nullptr)
|
||||||
@@ -2298,7 +2618,9 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
LargestComponentLowestCell,
|
LargestComponentLowestCell,
|
||||||
NumComponentsAtLeast1Pct,
|
NumComponentsAtLeast1Pct,
|
||||||
nullptr,
|
nullptr,
|
||||||
EligibilityMask);
|
EligibilityMask,
|
||||||
|
PlayerFitSupportHeights,
|
||||||
|
TraversalSettings);
|
||||||
|
|
||||||
if (OutComponents != nullptr)
|
if (OutComponents != nullptr)
|
||||||
{
|
{
|
||||||
@@ -2449,6 +2771,7 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
}
|
}
|
||||||
|
|
||||||
TArray<uint8> PlayerFitMask;
|
TArray<uint8> PlayerFitMask;
|
||||||
|
TArray<float> PlayerFitSupportHeights;
|
||||||
FVoxelStrateMetrics PlayerMetrics;
|
FVoxelStrateMetrics PlayerMetrics;
|
||||||
if (bPlayerFitRestricted)
|
if (bPlayerFitRestricted)
|
||||||
{
|
{
|
||||||
@@ -2457,7 +2780,8 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
// Manhattan feature transform here: those values belong to the separate legacy
|
// Manhattan feature transform here: those values belong to the separate legacy
|
||||||
// measurement pass, and recomputing them for every fine candidate made the gate
|
// measurement pass, and recomputing them for every fine candidate made the gate
|
||||||
// needlessly expensive without changing its answer.
|
// needlessly expensive without changing its answer.
|
||||||
BuildPlayerFitConnectivityMetrics(Grid, Settings, PlayerMetrics, PlayerFitMask);
|
BuildPlayerFitConnectivityMetrics(
|
||||||
|
Grid, Settings, PlayerMetrics, PlayerFitMask, PlayerFitSupportHeights);
|
||||||
if (OutPlayerMetrics != nullptr)
|
if (OutPlayerMetrics != nullptr)
|
||||||
{
|
{
|
||||||
*OutPlayerMetrics = PlayerMetrics;
|
*OutPlayerMetrics = PlayerMetrics;
|
||||||
@@ -2483,7 +2807,9 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
bOutGoalSnapped,
|
bOutGoalSnapped,
|
||||||
OutNumRouteRetries,
|
OutNumRouteRetries,
|
||||||
Settings.MaxRouteRetries,
|
Settings.MaxRouteRetries,
|
||||||
bPlayerFitRestricted ? &PlayerFitMask : nullptr);
|
bPlayerFitRestricted ? &PlayerFitMask : nullptr,
|
||||||
|
bPlayerFitRestricted ? &PlayerFitSupportHeights : nullptr,
|
||||||
|
bPlayerFitRestricted ? &Settings : nullptr);
|
||||||
if (OutDiagnostics != nullptr)
|
if (OutDiagnostics != nullptr)
|
||||||
{
|
{
|
||||||
PopulateConnectivityDiagnostics(
|
PopulateConnectivityDiagnostics(
|
||||||
@@ -2495,7 +2821,11 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
bOutGoalSnapped,
|
bOutGoalSnapped,
|
||||||
OutNumRouteRetries,
|
OutNumRouteRetries,
|
||||||
*OutDiagnostics,
|
*OutDiagnostics,
|
||||||
bPlayerFitRestricted ? &PlayerFitMask : nullptr);
|
bPlayerFitRestricted ? &PlayerFitMask : nullptr,
|
||||||
|
nullptr,
|
||||||
|
nullptr,
|
||||||
|
bPlayerFitRestricted ? &PlayerFitSupportHeights : nullptr,
|
||||||
|
bPlayerFitRestricted ? &Settings : nullptr);
|
||||||
if (bPlayerFitRestricted &&
|
if (bPlayerFitRestricted &&
|
||||||
(Result == EVoxelConnectivityResult::StartCellNotPlayerFit
|
(Result == EVoxelConnectivityResult::StartCellNotPlayerFit
|
||||||
|| Result == EVoxelConnectivityResult::GoalCellNotPlayerFit))
|
|| Result == EVoxelConnectivityResult::GoalCellNotPlayerFit))
|
||||||
@@ -2526,31 +2856,21 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool IsEligiblePlayerFitCell(
|
|
||||||
const FSampleGrid& Grid,
|
|
||||||
const TArray<uint8>& PlayerFitMask,
|
|
||||||
int32 Cell)
|
|
||||||
{
|
|
||||||
return Cell >= 0
|
|
||||||
&& Cell < Grid.CellCount
|
|
||||||
&& Grid.Air[Cell] != 0u
|
|
||||||
&& PlayerFitMask[Cell] != 0u;
|
|
||||||
}
|
|
||||||
|
|
||||||
int32 CountPlayerFitNeighbours(
|
int32 CountPlayerFitNeighbours(
|
||||||
const FSampleGrid& Grid,
|
const FSampleGrid& Grid,
|
||||||
const TArray<uint8>& PlayerFitMask,
|
const TArray<uint8>& PlayerFitMask,
|
||||||
|
const TArray<float>* PlayerFitSupportHeights,
|
||||||
|
const FVoxelStrateMeasureSettings* Settings,
|
||||||
int32 Cell)
|
int32 Cell)
|
||||||
{
|
{
|
||||||
|
TArray<FPlayerTraversalNeighbour, TInlineAllocator<128>> Neighbours;
|
||||||
|
AppendPlayerTraversalNeighbours(
|
||||||
|
Grid, PlayerFitMask, PlayerFitSupportHeights, Settings, Cell, Neighbours);
|
||||||
int32 Count = 0;
|
int32 Count = 0;
|
||||||
for (int32 Direction = 0; Direction < 6; ++Direction)
|
for (const FPlayerTraversalNeighbour& Neighbour : Neighbours)
|
||||||
{
|
|
||||||
if (IsEligiblePlayerFitCell(
|
|
||||||
Grid, PlayerFitMask, GetSixNeighbour(Grid, Cell, Direction)))
|
|
||||||
{
|
{
|
||||||
++Count;
|
++Count;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return Count;
|
return Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2616,6 +2936,8 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
void WalkPlayerFitGraph(
|
void WalkPlayerFitGraph(
|
||||||
const FSampleGrid& Grid,
|
const FSampleGrid& Grid,
|
||||||
const TArray<uint8>& PlayerFitMask,
|
const TArray<uint8>& PlayerFitMask,
|
||||||
|
const TArray<float>* PlayerFitSupportHeights,
|
||||||
|
const FVoxelStrateMeasureSettings* TraversalSettings,
|
||||||
int32 Start,
|
int32 Start,
|
||||||
int32 Goal,
|
int32 Goal,
|
||||||
float NarrowGapThresholdVoxels,
|
float NarrowGapThresholdVoxels,
|
||||||
@@ -2651,6 +2973,15 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
auto RecordTraversal = [&](int32 From, int32 To)
|
auto RecordTraversal = [&](int32 From, int32 To)
|
||||||
{
|
{
|
||||||
++InOutReport.AgentGraphTraversals;
|
++InOutReport.AgentGraphTraversals;
|
||||||
|
bool bClimb = false;
|
||||||
|
if (PlayerFitSupportHeights != nullptr && TraversalSettings != nullptr
|
||||||
|
&& IsPlayerTraversalEdge(
|
||||||
|
Grid, PlayerFitMask, PlayerFitSupportHeights, TraversalSettings,
|
||||||
|
From, To, bClimb)
|
||||||
|
&& bClimb)
|
||||||
|
{
|
||||||
|
++InOutReport.ClimbTraversals;
|
||||||
|
}
|
||||||
auto IsNarrowCell = [&](int32 Cell)
|
auto IsNarrowCell = [&](int32 Cell)
|
||||||
{
|
{
|
||||||
if (NarrowCell[Cell] == 255u)
|
if (NarrowCell[Cell] == 255u)
|
||||||
@@ -2678,11 +3009,15 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TArray<FPlayerTraversalNeighbour, TInlineAllocator<128>> Neighbours;
|
||||||
|
AppendPlayerTraversalNeighbours(
|
||||||
|
Grid, PlayerFitMask, PlayerFitSupportHeights, TraversalSettings,
|
||||||
|
Current, Neighbours);
|
||||||
bool bAdvanced = false;
|
bool bAdvanced = false;
|
||||||
while (NextDirection.Last() < 6)
|
while (NextDirection.Last() < Neighbours.Num())
|
||||||
{
|
{
|
||||||
const int32 Direction = NextDirection.Last()++;
|
const int32 Direction = NextDirection.Last()++;
|
||||||
const int32 Neighbour = GetSixNeighbour(Grid, Current, Direction);
|
const int32 Neighbour = Neighbours[Direction].Cell;
|
||||||
if (!IsEligiblePlayerFitCell(Grid, PlayerFitMask, Neighbour)
|
if (!IsEligiblePlayerFitCell(Grid, PlayerFitMask, Neighbour)
|
||||||
|| Visited[Neighbour] != 0u)
|
|| Visited[Neighbour] != 0u)
|
||||||
{
|
{
|
||||||
@@ -2706,7 +3041,9 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
// A degree-one pose is an actual topological dead end. A higher-degree pose with all
|
// A degree-one pose is an actual topological dead end. A higher-degree pose with all
|
||||||
// branches already visited is merely a DFS backtrack through a loop/junction.
|
// branches already visited is merely a DFS backtrack through a loop/junction.
|
||||||
if (Current != Start && Current != Goal
|
if (Current != Start && Current != Goal
|
||||||
&& CountPlayerFitNeighbours(Grid, PlayerFitMask, Current) <= 1)
|
&& CountPlayerFitNeighbours(
|
||||||
|
Grid, PlayerFitMask, PlayerFitSupportHeights,
|
||||||
|
TraversalSettings, Current) <= 1)
|
||||||
{
|
{
|
||||||
++InOutReport.DeadEndsEncountered;
|
++InOutReport.DeadEndsEncountered;
|
||||||
}
|
}
|
||||||
@@ -3048,6 +3385,8 @@ bool VF_MeasurePlayerFitWalkWithSampler(
|
|||||||
OutReport.CapsuleWidthMeters = 2.0f * OutReport.CapsuleRadiusMeters;
|
OutReport.CapsuleWidthMeters = 2.0f * OutReport.CapsuleRadiusMeters;
|
||||||
OutReport.CapsuleHeightMeters = 2.0f * Settings.PlayerCapsuleHalfHeightVoxels
|
OutReport.CapsuleHeightMeters = 2.0f * Settings.PlayerCapsuleHalfHeightVoxels
|
||||||
* FVoxelPlayerCapsuleConstants::VoxelSizeMeters;
|
* FVoxelPlayerCapsuleConstants::VoxelSizeMeters;
|
||||||
|
OutReport.bClimbingEnabled = Settings.bPlayerClimbingEnabled;
|
||||||
|
OutReport.MaxClimbHeightMeters = Settings.PlayerMaxClimbHeightMeters;
|
||||||
OutReport.NarrowGapThresholdMeters = 1.5f * OutReport.CapsuleWidthMeters;
|
OutReport.NarrowGapThresholdMeters = 1.5f * OutReport.CapsuleWidthMeters;
|
||||||
OutReport.NarrowGapDefinition = TEXT(
|
OutReport.NarrowGapDefinition = TEXT(
|
||||||
"widest axis-aligned horizontal air span at each traversed fit-graph cell");
|
"widest axis-aligned horizontal air span at each traversed fit-graph cell");
|
||||||
@@ -3096,13 +3435,15 @@ bool VF_MeasurePlayerFitWalkWithSampler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
TArray<uint8> PlayerFitMask;
|
TArray<uint8> PlayerFitMask;
|
||||||
|
TArray<float> PlayerFitSupportHeights;
|
||||||
int64 NumPlayerFitCells = 0;
|
int64 NumPlayerFitCells = 0;
|
||||||
if (!BuildPlayerFitMask(
|
if (!BuildPlayerFitMask(
|
||||||
Grid,
|
Grid,
|
||||||
Settings,
|
Settings,
|
||||||
PlayerFitMask,
|
PlayerFitMask,
|
||||||
NumPlayerFitCells,
|
NumPlayerFitCells,
|
||||||
RefusalReason))
|
RefusalReason,
|
||||||
|
&PlayerFitSupportHeights))
|
||||||
{
|
{
|
||||||
OutReport.RefusalReason = RefusalReason;
|
OutReport.RefusalReason = RefusalReason;
|
||||||
return false;
|
return false;
|
||||||
@@ -3123,7 +3464,9 @@ bool VF_MeasurePlayerFitWalkWithSampler(
|
|||||||
OutReport.bGoalSnapped,
|
OutReport.bGoalSnapped,
|
||||||
NumRouteRetries,
|
NumRouteRetries,
|
||||||
Settings.MaxRouteRetries,
|
Settings.MaxRouteRetries,
|
||||||
&PlayerFitMask);
|
&PlayerFitMask,
|
||||||
|
&PlayerFitSupportHeights,
|
||||||
|
&Settings);
|
||||||
|
|
||||||
FVoxelConnectivityDiagnostics Diagnostics;
|
FVoxelConnectivityDiagnostics Diagnostics;
|
||||||
TArray<int32> Components;
|
TArray<int32> Components;
|
||||||
@@ -3139,7 +3482,9 @@ bool VF_MeasurePlayerFitWalkWithSampler(
|
|||||||
Diagnostics,
|
Diagnostics,
|
||||||
&PlayerFitMask,
|
&PlayerFitMask,
|
||||||
&Components,
|
&Components,
|
||||||
&NumComponents);
|
&NumComponents,
|
||||||
|
&PlayerFitSupportHeights,
|
||||||
|
&Settings);
|
||||||
|
|
||||||
OutReport.bValid = true;
|
OutReport.bValid = true;
|
||||||
OutReport.Result = Result;
|
OutReport.Result = Result;
|
||||||
@@ -3177,6 +3522,8 @@ bool VF_MeasurePlayerFitWalkWithSampler(
|
|||||||
WalkPlayerFitGraph(
|
WalkPlayerFitGraph(
|
||||||
Grid,
|
Grid,
|
||||||
PlayerFitMask,
|
PlayerFitMask,
|
||||||
|
&PlayerFitSupportHeights,
|
||||||
|
&Settings,
|
||||||
Start,
|
Start,
|
||||||
Goal,
|
Goal,
|
||||||
NarrowGapThresholdVoxels,
|
NarrowGapThresholdVoxels,
|
||||||
|
|||||||
@@ -621,6 +621,18 @@ struct FCachedRoomFloorJoin
|
|||||||
float BoundRadiusSq = 0.0f;
|
float BoundRadiusSq = 0.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// One immutable floor segment authored while the tunnel chain is built. NumSteps == 0 is a
|
||||||
|
// continuous segment; a positive value quantizes the interpolation to that many whole-chain
|
||||||
|
// ledge intervals. ReliefScale is baked with the same derivative bound as the old evaluator, so
|
||||||
|
// evaluation only reads this profile and never decides terrace count per voxel.
|
||||||
|
struct FTunnelFloorSegmentProfile
|
||||||
|
{
|
||||||
|
float StartFloorZ = 0.0f;
|
||||||
|
float EndFloorZ = 0.0f;
|
||||||
|
float ReliefScale = 0.0f;
|
||||||
|
int32 NumSteps = 0;
|
||||||
|
};
|
||||||
|
|
||||||
// A pre-computed wandering tunnel chain — all connection decisions and hash-derived properties
|
// A pre-computed wandering tunnel chain — all connection decisions and hash-derived properties
|
||||||
// (radii, Z offsets, control points) are resolved during cache build.
|
// (radii, Z offsets, control points) are resolved during cache build.
|
||||||
struct FCachedTunnel
|
struct FCachedTunnel
|
||||||
@@ -640,11 +652,43 @@ struct FCachedTunnel
|
|||||||
// turning a gentle authored floor ramp into a vertical break when the cave warp is nonlinear.
|
// turning a gentle authored floor ramp into a vertical break when the cave warp is nonlinear.
|
||||||
TArray<FVector> WorldControlPoints;
|
TArray<FVector> WorldControlPoints;
|
||||||
TArray<float> WorldControlRadii;
|
TArray<float> WorldControlRadii;
|
||||||
|
// Build-time floor authoring. SDF and world chains each keep their own profile because cave
|
||||||
|
// warp changes the control-point Z values. The normal path has one entry per chain segment;
|
||||||
|
// malformed/legacy caches fall back to the old local evaluator.
|
||||||
|
TArray<FTunnelFloorSegmentProfile> FloorProfiles;
|
||||||
|
TArray<FTunnelFloorSegmentProfile> WorldFloorProfiles;
|
||||||
// The corridor floor is a swept SmoothMax cut, not a later slab. These are copied from the
|
// The corridor floor is a swept SmoothMax cut, not a later slab. These are copied from the
|
||||||
// same strate fields used by room floors; the seed is derived once from the tunnel pair hash.
|
// same strate fields used by room floors; the seed is derived once from the tunnel pair hash.
|
||||||
float FloorReliefStrength = 0.0f;
|
float FloorReliefStrength = 0.0f;
|
||||||
float FloorReliefFrequency = 0.015f;
|
float FloorReliefFrequency = 0.015f;
|
||||||
uint32 FloorSeed = 0;
|
uint32 FloorSeed = 0;
|
||||||
|
// Only the two rooms that actually form this edge may own its mouth floor. A nearby unrelated
|
||||||
|
// room must not clip a tunnel merely because the two shapes overlap in a crowded graph.
|
||||||
|
bool bHasFloorRoomOwnership = false;
|
||||||
|
uint32 FloorRoomHashA = 0;
|
||||||
|
uint32 FloorRoomHashB = 0;
|
||||||
|
// World-space mouth ownership is resolved from the baked chain, not by comparing a warped
|
||||||
|
// query against an SDF-space room. These values make the post-disturbance hand-off exact in
|
||||||
|
// the same coordinate space in which the player and the support backstop are evaluated.
|
||||||
|
// The room's full radius is deliberately not used as the ownership region. A tunnel's
|
||||||
|
// floor should hand off to the room over a short mouth apron, not overwrite the room floor
|
||||||
|
// across the entire chamber. These are the build-time capped hand-off radii in both query
|
||||||
|
// coordinate spaces.
|
||||||
|
float SDFMouthBlendRadiusA = 0.0f;
|
||||||
|
float SDFMouthBlendRadiusB = 0.0f;
|
||||||
|
float WorldMouthBlendRadiusA = 0.0f;
|
||||||
|
float WorldMouthBlendRadiusB = 0.0f;
|
||||||
|
float SDFMouthFloorZA = -FLT_MAX;
|
||||||
|
float SDFMouthFloorZB = -FLT_MAX;
|
||||||
|
float WorldMouthFloorZA = -FLT_MAX;
|
||||||
|
float WorldMouthFloorZB = -FLT_MAX;
|
||||||
|
bool bTunnelFloorEnabled = true;
|
||||||
|
bool bTunnelFloorTerracingEnabled = true;
|
||||||
|
float TunnelFloorTerraceStepHeight = 1.71f;
|
||||||
|
float TunnelFloorMaxLedgeHeight = 12.0f;
|
||||||
|
float TunnelFloorGentleSlopeThreshold = 0.9656888f;
|
||||||
|
int32 TunnelFloorLedgeCountPreference = 0;
|
||||||
|
int32 TunnelFloorMaxLedges = 4096;
|
||||||
FVector WorldBoundCenter = FVector::ZeroVector;
|
FVector WorldBoundCenter = FVector::ZeroVector;
|
||||||
float WorldBoundRadiusSq = 0.0f;
|
float WorldBoundRadiusSq = 0.0f;
|
||||||
// Centerline AABBs and scalar influence radii used by the immutable broad phase. The
|
// Centerline AABBs and scalar influence radii used by the immutable broad phase. The
|
||||||
@@ -794,6 +838,9 @@ struct FTunnelCoreWorldEvaluation
|
|||||||
{
|
{
|
||||||
float SDF = FLT_MAX;
|
float SDF = FLT_MAX;
|
||||||
bool bSupportFloor = false;
|
bool bSupportFloor = false;
|
||||||
|
// The room floor owns the overlap band. The common post uses this marker to restore
|
||||||
|
// the room's support after the raw tunnel-air backstop has run.
|
||||||
|
bool bRoomFloor = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
|
|||||||
@@ -96,6 +96,11 @@ namespace VoxelDensityProfile
|
|||||||
TunnelSupportColumnCandidates,
|
TunnelSupportColumnCandidates,
|
||||||
TunnelSupportFloorQueries,
|
TunnelSupportFloorQueries,
|
||||||
TunnelSupportFloorChecks,
|
TunnelSupportFloorChecks,
|
||||||
|
TunnelSupportFloorBackstopFires,
|
||||||
|
TunnelRoomFloorBackstopFires,
|
||||||
|
TunnelFloorProfileBuilds,
|
||||||
|
TunnelFloorProfileSegments,
|
||||||
|
TunnelFloorProfileLedges,
|
||||||
PassageCandidates,
|
PassageCandidates,
|
||||||
PassageEvaluated,
|
PassageEvaluated,
|
||||||
MesherBlockTests,
|
MesherBlockTests,
|
||||||
|
|||||||
@@ -103,6 +103,13 @@ struct VOXELFORGE_API FVoxelStrateMeasureSettings
|
|||||||
// A support patch is sampled at the integer columns covered by the capsule radius. 0.75 with
|
// A support patch is sampled at the integer columns covered by the capsule radius. 0.75 with
|
||||||
// the default 1.36-voxel radius requires 4 of 5 columns, including the centre column.
|
// the default 1.36-voxel radius requires 4 of 5 columns, including the centre column.
|
||||||
float PlayerSupportPatchMinCoverageFraction = 0.75f;
|
float PlayerSupportPatchMinCoverageFraction = 0.75f;
|
||||||
|
|
||||||
|
// Traversal is still constrained by the exact player-fit mask, but a planned climbing
|
||||||
|
// mechanic may cross a higher supported neighbouring column. This is a measurement policy,
|
||||||
|
// not a runtime movement change. The surface remains required to be supported; the extra
|
||||||
|
// height only changes whether that supported edge is traversable.
|
||||||
|
bool bPlayerClimbingEnabled = true;
|
||||||
|
float PlayerMaxClimbHeightMeters = 3.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -302,6 +309,8 @@ struct VOXELFORGE_API FVoxelPlayerFitWalkReport
|
|||||||
bool bStartSnapped = false;
|
bool bStartSnapped = false;
|
||||||
bool bGoalSnapped = false;
|
bool bGoalSnapped = false;
|
||||||
int32 NumRouteRetries = 0;
|
int32 NumRouteRetries = 0;
|
||||||
|
bool bClimbingEnabled = false;
|
||||||
|
float MaxClimbHeightMeters = 0.0f;
|
||||||
|
|
||||||
int64 PlayerFitVolumeCells = 0;
|
int64 PlayerFitVolumeCells = 0;
|
||||||
int64 ReachablePlayerFitCells = 0;
|
int64 ReachablePlayerFitCells = 0;
|
||||||
@@ -320,6 +329,7 @@ struct VOXELFORGE_API FVoxelPlayerFitWalkReport
|
|||||||
// branch edge is counted twice when the agent backtracks, which makes this an instrument of
|
// branch edge is counted twice when the agent backtracks, which makes this an instrument of
|
||||||
// traversal experience rather than a shortest-path length.
|
// traversal experience rather than a shortest-path length.
|
||||||
int64 AgentGraphTraversals = 0;
|
int64 AgentGraphTraversals = 0;
|
||||||
|
int64 ClimbTraversals = 0;
|
||||||
int64 DeadEndsEncountered = 0;
|
int64 DeadEndsEncountered = 0;
|
||||||
float DeadEndsPer100m = 0.0f;
|
float DeadEndsPer100m = 0.0f;
|
||||||
float DistanceTravelledMeters = 0.0f;
|
float DistanceTravelledMeters = 0.0f;
|
||||||
|
|||||||
@@ -283,6 +283,13 @@ enum class EVoxelStrateTransition : uint8
|
|||||||
LERPF(TunnelHorizontalBias) \
|
LERPF(TunnelHorizontalBias) \
|
||||||
SNAPF(bTunnelsFlowTowardOrigin) \
|
SNAPF(bTunnelsFlowTowardOrigin) \
|
||||||
LERPF(TunnelEndpointZOffset) \
|
LERPF(TunnelEndpointZOffset) \
|
||||||
|
SNAPF(bTunnelFloorEnabled) \
|
||||||
|
SNAPF(bTunnelFloorTerracingEnabled) \
|
||||||
|
LERPF(TunnelFloorTerraceStepHeight) \
|
||||||
|
LERPF(TunnelFloorMaxLedgeHeight) \
|
||||||
|
LERPF(TunnelFloorGentleSlopeThreshold) \
|
||||||
|
SNAPF(TunnelFloorLedgeCountPreference) \
|
||||||
|
SNAPF(TunnelFloorMaxLedges) \
|
||||||
LERPF(SDFBlendRadius) \
|
LERPF(SDFBlendRadius) \
|
||||||
LERPF(WaterLevelRelative) \
|
LERPF(WaterLevelRelative) \
|
||||||
/* Cave warp */ \
|
/* Cave warp */ \
|
||||||
@@ -659,6 +666,55 @@ struct VOXELFORGE_API FStrateGenerationParams
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||||
float TunnelEndpointZOffset = 0.25f;
|
float TunnelEndpointZOffset = 0.25f;
|
||||||
|
|
||||||
|
// ===== TUNNEL FLOOR AUTHORING =====
|
||||||
|
//
|
||||||
|
// The corridor floor is authored into the swept tunnel during cache construction. These
|
||||||
|
// controls are intentionally separate from FCaveTerraceMod::TerraceStepHeight: that field
|
||||||
|
// terraces walls after morphology, while these fields describe the tunnel's own floor profile.
|
||||||
|
// FloorReliefStrength/Frequency above are shared with room floors and are reused here.
|
||||||
|
|
||||||
|
// Whether the swept tunnel receives its authored floor cut. Disable only for an archetype that
|
||||||
|
// deliberately wants the bare capsule; the structural support backstop remains a measured
|
||||||
|
// fallback for malformed/legacy caches and is not a replacement for this shape.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels|Floor")
|
||||||
|
bool bTunnelFloorEnabled = true;
|
||||||
|
|
||||||
|
// Enables the floor profile's terrace decisions. With the default legacy ledge preference of
|
||||||
|
// zero, this preserves the existing per-segment walkable staircase. A positive preference
|
||||||
|
// selects the build-time whole-chain ledge policy.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels|Floor")
|
||||||
|
bool bTunnelFloorTerracingEnabled = true;
|
||||||
|
|
||||||
|
// Maximum riser height used by the compatibility/per-segment profile, in voxels. The default
|
||||||
|
// is the previous 1.8 voxel step with its 0.95 safety margin (1.71 voxels).
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels|Floor",
|
||||||
|
meta = (ClampMin = "0.0"))
|
||||||
|
float TunnelFloorTerraceStepHeight = 1.71f;
|
||||||
|
|
||||||
|
// Maximum height of one authored ledge in the whole-chain profile, in voxels. This may exceed
|
||||||
|
// the walking step because ledges are climbable surfaces under the measurement envelope.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels|Floor",
|
||||||
|
meta = (ClampMin = "0.0"))
|
||||||
|
float TunnelFloorMaxLedgeHeight = 12.0f;
|
||||||
|
|
||||||
|
// Whole-chain floor gradient below which the tunnel descends continuously instead of placing
|
||||||
|
// ledges. The default is tan(44 degrees), matching the walkable-floor bound.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels|Floor",
|
||||||
|
meta = (ClampMin = "0.0"))
|
||||||
|
float TunnelFloorGentleSlopeThreshold = 0.9656888f;
|
||||||
|
|
||||||
|
// Preferred number of large ledges for a steep whole-chain floor. Zero is the compatibility
|
||||||
|
// mode (the old per-segment step-count rule); positive values request that many transitions,
|
||||||
|
// subject to the max-height and available control-segment constraints.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels|Floor",
|
||||||
|
meta = (ClampMin = "0", ClampMax = "32"))
|
||||||
|
int32 TunnelFloorLedgeCountPreference = 0;
|
||||||
|
|
||||||
|
// Safety bound on the number of transitions emitted by either floor profile.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels|Floor",
|
||||||
|
meta = (ClampMin = "1", ClampMax = "4096"))
|
||||||
|
int32 TunnelFloorMaxLedges = 4096;
|
||||||
|
|
||||||
// ===== SDF BLEND (junction smoothness) =====
|
// ===== SDF BLEND (junction smoothness) =====
|
||||||
|
|
||||||
// Smooth union blend radius. Controls how rounded the junctions
|
// Smooth union blend radius. Controls how rounded the junctions
|
||||||
|
|||||||
@@ -223,6 +223,20 @@ struct FExploreArguments
|
|||||||
float BudgetMinutes = DefaultBudgetMinutes;
|
float BudgetMinutes = DefaultBudgetMinutes;
|
||||||
bool bSurfaceRoughnessOverride = false;
|
bool bSurfaceRoughnessOverride = false;
|
||||||
float SurfaceRoughness = 0.0f;
|
float SurfaceRoughness = 0.0f;
|
||||||
|
bool bTunnelFloorEnabledOverride = false;
|
||||||
|
int32 TunnelFloorEnabled = 1;
|
||||||
|
bool bTunnelFloorTerracingOverride = false;
|
||||||
|
int32 TunnelFloorTerracing = 1;
|
||||||
|
bool bTunnelFloorStepOverride = false;
|
||||||
|
float TunnelFloorStep = 0.0f;
|
||||||
|
bool bTunnelFloorMaxLedgeOverride = false;
|
||||||
|
float TunnelFloorMaxLedge = 0.0f;
|
||||||
|
bool bTunnelFloorGentleOverride = false;
|
||||||
|
float TunnelFloorGentle = 0.0f;
|
||||||
|
bool bTunnelFloorLedgesOverride = false;
|
||||||
|
int32 TunnelFloorLedges = 0;
|
||||||
|
bool bTunnelFloorMaxLedgesOverride = false;
|
||||||
|
int32 TunnelFloorMaxLedges = 0;
|
||||||
|
|
||||||
FString CanonicalModes() const
|
FString CanonicalModes() const
|
||||||
{
|
{
|
||||||
@@ -303,6 +317,41 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
|||||||
const bool bSurfaceRoughnessParsed = FParse::Value(
|
const bool bSurfaceRoughnessParsed = FParse::Value(
|
||||||
*Params, TEXT("surfaceroughness="), OutArguments.SurfaceRoughness);
|
*Params, TEXT("surfaceroughness="), OutArguments.SurfaceRoughness);
|
||||||
OutArguments.bSurfaceRoughnessOverride = bSurfaceRoughnessSpecified;
|
OutArguments.bSurfaceRoughnessOverride = bSurfaceRoughnessSpecified;
|
||||||
|
const bool bTunnelFloorEnabledSpecified = Params.Contains(
|
||||||
|
TEXT("tunnelfloor="), ESearchCase::IgnoreCase);
|
||||||
|
const bool bTunnelFloorEnabledParsed = FParse::Value(
|
||||||
|
*Params, TEXT("tunnelfloor="), OutArguments.TunnelFloorEnabled);
|
||||||
|
OutArguments.bTunnelFloorEnabledOverride = bTunnelFloorEnabledSpecified;
|
||||||
|
const bool bTunnelFloorTerracingSpecified = Params.Contains(
|
||||||
|
TEXT("tunnelfloorterracing="), ESearchCase::IgnoreCase);
|
||||||
|
const bool bTunnelFloorTerracingParsed = FParse::Value(
|
||||||
|
*Params, TEXT("tunnelfloorterracing="), OutArguments.TunnelFloorTerracing);
|
||||||
|
OutArguments.bTunnelFloorTerracingOverride = bTunnelFloorTerracingSpecified;
|
||||||
|
const bool bTunnelFloorStepSpecified = Params.Contains(
|
||||||
|
TEXT("tunnelfloorstep="), ESearchCase::IgnoreCase);
|
||||||
|
const bool bTunnelFloorStepParsed = FParse::Value(
|
||||||
|
*Params, TEXT("tunnelfloorstep="), OutArguments.TunnelFloorStep);
|
||||||
|
OutArguments.bTunnelFloorStepOverride = bTunnelFloorStepSpecified;
|
||||||
|
const bool bTunnelFloorMaxLedgeSpecified = Params.Contains(
|
||||||
|
TEXT("tunnelfloormaxledge="), ESearchCase::IgnoreCase);
|
||||||
|
const bool bTunnelFloorMaxLedgeParsed = FParse::Value(
|
||||||
|
*Params, TEXT("tunnelfloormaxledge="), OutArguments.TunnelFloorMaxLedge);
|
||||||
|
OutArguments.bTunnelFloorMaxLedgeOverride = bTunnelFloorMaxLedgeSpecified;
|
||||||
|
const bool bTunnelFloorGentleSpecified = Params.Contains(
|
||||||
|
TEXT("tunnelfloorgentle="), ESearchCase::IgnoreCase);
|
||||||
|
const bool bTunnelFloorGentleParsed = FParse::Value(
|
||||||
|
*Params, TEXT("tunnelfloorgentle="), OutArguments.TunnelFloorGentle);
|
||||||
|
OutArguments.bTunnelFloorGentleOverride = bTunnelFloorGentleSpecified;
|
||||||
|
const bool bTunnelFloorLedgesSpecified = Params.Contains(
|
||||||
|
TEXT("tunnelfloorledges="), ESearchCase::IgnoreCase);
|
||||||
|
const bool bTunnelFloorLedgesParsed = FParse::Value(
|
||||||
|
*Params, TEXT("tunnelfloorledges="), OutArguments.TunnelFloorLedges);
|
||||||
|
OutArguments.bTunnelFloorLedgesOverride = bTunnelFloorLedgesSpecified;
|
||||||
|
const bool bTunnelFloorMaxLedgesSpecified = Params.Contains(
|
||||||
|
TEXT("tunnelfloormaxledges="), ESearchCase::IgnoreCase);
|
||||||
|
const bool bTunnelFloorMaxLedgesParsed = FParse::Value(
|
||||||
|
*Params, TEXT("tunnelfloormaxledges="), OutArguments.TunnelFloorMaxLedges);
|
||||||
|
OutArguments.bTunnelFloorMaxLedgesOverride = bTunnelFloorMaxLedgesSpecified;
|
||||||
int32 FailureFocusRender = 0;
|
int32 FailureFocusRender = 0;
|
||||||
FParse::Value(*Params, TEXT("failurefocus="), FailureFocusRender);
|
FParse::Value(*Params, TEXT("failurefocus="), FailureFocusRender);
|
||||||
OutArguments.bFailureFocusRender = FailureFocusRender != 0;
|
OutArguments.bFailureFocusRender = FailureFocusRender != 0;
|
||||||
@@ -439,6 +488,62 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
|||||||
OutError = TEXT("surfaceroughness must be finite and greater than or equal to zero.");
|
OutError = TEXT("surfaceroughness must be finite and greater than or equal to zero.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (OutArguments.bTunnelFloorEnabledOverride
|
||||||
|
&& (!bTunnelFloorEnabledParsed
|
||||||
|
|| (OutArguments.TunnelFloorEnabled != 0
|
||||||
|
&& OutArguments.TunnelFloorEnabled != 1)))
|
||||||
|
{
|
||||||
|
OutError = TEXT("tunnelfloor must be 0 or 1.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (OutArguments.bTunnelFloorTerracingOverride
|
||||||
|
&& (!bTunnelFloorTerracingParsed
|
||||||
|
|| (OutArguments.TunnelFloorTerracing != 0
|
||||||
|
&& OutArguments.TunnelFloorTerracing != 1)))
|
||||||
|
{
|
||||||
|
OutError = TEXT("tunnelfloorterracing must be 0 or 1.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (OutArguments.bTunnelFloorStepOverride
|
||||||
|
&& (!bTunnelFloorStepParsed
|
||||||
|
|| !FMath::IsFinite(OutArguments.TunnelFloorStep)
|
||||||
|
|| OutArguments.TunnelFloorStep < 0.0f))
|
||||||
|
{
|
||||||
|
OutError = TEXT("tunnelfloorstep must be finite and non-negative.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (OutArguments.bTunnelFloorMaxLedgeOverride
|
||||||
|
&& (!bTunnelFloorMaxLedgeParsed
|
||||||
|
|| !FMath::IsFinite(OutArguments.TunnelFloorMaxLedge)
|
||||||
|
|| OutArguments.TunnelFloorMaxLedge <= 0.0f))
|
||||||
|
{
|
||||||
|
OutError = TEXT("tunnelfloormaxledge must be finite and greater than zero.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (OutArguments.bTunnelFloorGentleOverride
|
||||||
|
&& (!bTunnelFloorGentleParsed
|
||||||
|
|| !FMath::IsFinite(OutArguments.TunnelFloorGentle)
|
||||||
|
|| OutArguments.TunnelFloorGentle < 0.0f))
|
||||||
|
{
|
||||||
|
OutError = TEXT("tunnelfloorgentle must be finite and non-negative.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (OutArguments.bTunnelFloorLedgesOverride
|
||||||
|
&& (!bTunnelFloorLedgesParsed
|
||||||
|
|| OutArguments.TunnelFloorLedges < 0
|
||||||
|
|| OutArguments.TunnelFloorLedges > 4096))
|
||||||
|
{
|
||||||
|
OutError = TEXT("tunnelfloorledges must be an integer in [0,4096].");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (OutArguments.bTunnelFloorMaxLedgesOverride
|
||||||
|
&& (!bTunnelFloorMaxLedgesParsed
|
||||||
|
|| OutArguments.TunnelFloorMaxLedges < 1
|
||||||
|
|| OutArguments.TunnelFloorMaxLedges > 4096))
|
||||||
|
{
|
||||||
|
OutError = TEXT("tunnelfloormaxledges must be an integer in [1,4096].");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (OutText.IsEmpty())
|
if (OutText.IsEmpty())
|
||||||
{
|
{
|
||||||
@@ -711,6 +816,40 @@ struct FExploreWorld
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Floor controls are applied only to the requested target definition. They are
|
||||||
|
// commandlet overrides for measured archetype comparisons; authored assets still carry
|
||||||
|
// the defaults/rolled values in FStrateGenerationParams.
|
||||||
|
FStrateGenerationParams& TargetTunnelParams = Target.Definition->GenerationParams;
|
||||||
|
if (Arguments.bTunnelFloorEnabledOverride)
|
||||||
|
{
|
||||||
|
TargetTunnelParams.bTunnelFloorEnabled =
|
||||||
|
Arguments.TunnelFloorEnabled != 0;
|
||||||
|
}
|
||||||
|
if (Arguments.bTunnelFloorTerracingOverride)
|
||||||
|
{
|
||||||
|
TargetTunnelParams.bTunnelFloorTerracingEnabled =
|
||||||
|
Arguments.TunnelFloorTerracing != 0;
|
||||||
|
}
|
||||||
|
if (Arguments.bTunnelFloorStepOverride)
|
||||||
|
{
|
||||||
|
TargetTunnelParams.TunnelFloorTerraceStepHeight = Arguments.TunnelFloorStep;
|
||||||
|
}
|
||||||
|
if (Arguments.bTunnelFloorMaxLedgeOverride)
|
||||||
|
{
|
||||||
|
TargetTunnelParams.TunnelFloorMaxLedgeHeight = Arguments.TunnelFloorMaxLedge;
|
||||||
|
}
|
||||||
|
if (Arguments.bTunnelFloorGentleOverride)
|
||||||
|
{
|
||||||
|
TargetTunnelParams.TunnelFloorGentleSlopeThreshold = Arguments.TunnelFloorGentle;
|
||||||
|
}
|
||||||
|
if (Arguments.bTunnelFloorLedgesOverride)
|
||||||
|
{
|
||||||
|
TargetTunnelParams.TunnelFloorLedgeCountPreference = Arguments.TunnelFloorLedges;
|
||||||
|
}
|
||||||
|
if (Arguments.bTunnelFloorMaxLedgesOverride)
|
||||||
|
{
|
||||||
|
TargetTunnelParams.TunnelFloorMaxLedges = Arguments.TunnelFloorMaxLedges;
|
||||||
|
}
|
||||||
TargetBottomWorldZ = Target.BottomChunkZ * CHUNK_SIZE;
|
TargetBottomWorldZ = Target.BottomChunkZ * CHUNK_SIZE;
|
||||||
TargetTopWorldZ = (Target.TopChunkZ + 1) * CHUNK_SIZE;
|
TargetTopWorldZ = (Target.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
TargetBoundarySealThickness = BoundarySealForArchetype(
|
TargetBoundarySealThickness = BoundarySealForArchetype(
|
||||||
@@ -3615,6 +3754,52 @@ void WriteSurfaceReliefMetrics(
|
|||||||
Writer.WriteObjectEnd();
|
Writer.WriteObjectEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WriteTunnelFloorOverrides(
|
||||||
|
FExploreJsonWriter& Writer,
|
||||||
|
const FExploreArguments& Arguments)
|
||||||
|
{
|
||||||
|
Writer.WriteObjectStart(TEXT("tunnel_floor_overrides"));
|
||||||
|
Writer.WriteValue(TEXT("enabled_override"), Arguments.bTunnelFloorEnabledOverride);
|
||||||
|
if (Arguments.bTunnelFloorEnabledOverride)
|
||||||
|
{
|
||||||
|
Writer.WriteValue(TEXT("enabled"), Arguments.TunnelFloorEnabled != 0);
|
||||||
|
}
|
||||||
|
Writer.WriteValue(TEXT("terracing_override"), Arguments.bTunnelFloorTerracingOverride);
|
||||||
|
if (Arguments.bTunnelFloorTerracingOverride)
|
||||||
|
{
|
||||||
|
Writer.WriteValue(TEXT("terracing"), Arguments.TunnelFloorTerracing != 0);
|
||||||
|
}
|
||||||
|
Writer.WriteValue(TEXT("step_height_override"), Arguments.bTunnelFloorStepOverride);
|
||||||
|
if (Arguments.bTunnelFloorStepOverride)
|
||||||
|
{
|
||||||
|
Writer.WriteValue(TEXT("step_height_voxels"),
|
||||||
|
static_cast<double>(Arguments.TunnelFloorStep));
|
||||||
|
}
|
||||||
|
Writer.WriteValue(TEXT("max_ledge_height_override"), Arguments.bTunnelFloorMaxLedgeOverride);
|
||||||
|
if (Arguments.bTunnelFloorMaxLedgeOverride)
|
||||||
|
{
|
||||||
|
Writer.WriteValue(TEXT("max_ledge_height_voxels"),
|
||||||
|
static_cast<double>(Arguments.TunnelFloorMaxLedge));
|
||||||
|
}
|
||||||
|
Writer.WriteValue(TEXT("gentle_slope_override"), Arguments.bTunnelFloorGentleOverride);
|
||||||
|
if (Arguments.bTunnelFloorGentleOverride)
|
||||||
|
{
|
||||||
|
Writer.WriteValue(TEXT("gentle_slope"),
|
||||||
|
static_cast<double>(Arguments.TunnelFloorGentle));
|
||||||
|
}
|
||||||
|
Writer.WriteValue(TEXT("ledge_count_override"), Arguments.bTunnelFloorLedgesOverride);
|
||||||
|
if (Arguments.bTunnelFloorLedgesOverride)
|
||||||
|
{
|
||||||
|
Writer.WriteValue(TEXT("ledge_count"), Arguments.TunnelFloorLedges);
|
||||||
|
}
|
||||||
|
Writer.WriteValue(TEXT("max_ledges_override"), Arguments.bTunnelFloorMaxLedgesOverride);
|
||||||
|
if (Arguments.bTunnelFloorMaxLedgesOverride)
|
||||||
|
{
|
||||||
|
Writer.WriteValue(TEXT("max_ledges"), Arguments.TunnelFloorMaxLedges);
|
||||||
|
}
|
||||||
|
Writer.WriteObjectEnd();
|
||||||
|
}
|
||||||
|
|
||||||
FString BuildManifestJson(
|
FString BuildManifestJson(
|
||||||
const FExploreArguments& Arguments,
|
const FExploreArguments& Arguments,
|
||||||
const FExploreExportOutput& Export)
|
const FExploreExportOutput& Export)
|
||||||
@@ -3623,7 +3808,7 @@ FString BuildManifestJson(
|
|||||||
TSharedRef<FExploreJsonWriter> Writer =
|
TSharedRef<FExploreJsonWriter> Writer =
|
||||||
TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&Json);
|
TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&Json);
|
||||||
Writer->WriteObjectStart();
|
Writer->WriteObjectStart();
|
||||||
Writer->WriteValue(TEXT("schema_version"), 4);
|
Writer->WriteValue(TEXT("schema_version"), 6);
|
||||||
Writer->WriteValue(TEXT("format"), TEXT("OBJ"));
|
Writer->WriteValue(TEXT("format"), TEXT("OBJ"));
|
||||||
Writer->WriteValue(TEXT("mesh_file"), Export.MeshFileName);
|
Writer->WriteValue(TEXT("mesh_file"), Export.MeshFileName);
|
||||||
Writer->WriteValue(TEXT("mesh_positions_units"), TEXT("metres"));
|
Writer->WriteValue(TEXT("mesh_positions_units"), TEXT("metres"));
|
||||||
@@ -3650,6 +3835,7 @@ FString BuildManifestJson(
|
|||||||
Writer->WriteValue(TEXT("archetype"), ArchetypeName(Arguments.Archetype));
|
Writer->WriteValue(TEXT("archetype"), ArchetypeName(Arguments.Archetype));
|
||||||
Writer->WriteValue(TEXT("slot"), Arguments.Slot);
|
Writer->WriteValue(TEXT("slot"), Arguments.Slot);
|
||||||
Writer->WriteValue(TEXT("operator_stack"), Arguments.bUseOperatorStack);
|
Writer->WriteValue(TEXT("operator_stack"), Arguments.bUseOperatorStack);
|
||||||
|
WriteTunnelFloorOverrides(*Writer, Arguments);
|
||||||
Writer->WriteValue(TEXT("surface_roughness_override"), Arguments.bSurfaceRoughnessOverride);
|
Writer->WriteValue(TEXT("surface_roughness_override"), Arguments.bSurfaceRoughnessOverride);
|
||||||
if (Arguments.bSurfaceRoughnessOverride)
|
if (Arguments.bSurfaceRoughnessOverride)
|
||||||
{
|
{
|
||||||
@@ -3842,6 +4028,8 @@ void WritePlayerDimensions(FExploreJsonWriter& Writer, const TCHAR* Key)
|
|||||||
Writer.WriteValue(TEXT("capsule_half_height_m"), static_cast<double>(FVoxelPlayerCapsuleConstants::HalfHeightCentimeters) / 100.0);
|
Writer.WriteValue(TEXT("capsule_half_height_m"), static_cast<double>(FVoxelPlayerCapsuleConstants::HalfHeightCentimeters) / 100.0);
|
||||||
Writer.WriteValue(TEXT("capsule_height_m"), static_cast<double>(2.0f * FVoxelPlayerCapsuleConstants::HalfHeightCentimeters) / 100.0);
|
Writer.WriteValue(TEXT("capsule_height_m"), static_cast<double>(2.0f * FVoxelPlayerCapsuleConstants::HalfHeightCentimeters) / 100.0);
|
||||||
Writer.WriteValue(TEXT("max_step_height_m"), static_cast<double>(FVoxelPlayerCapsuleConstants::MaxStepHeightMeters));
|
Writer.WriteValue(TEXT("max_step_height_m"), static_cast<double>(FVoxelPlayerCapsuleConstants::MaxStepHeightMeters));
|
||||||
|
Writer.WriteValue(TEXT("climbing_enabled"), true);
|
||||||
|
Writer.WriteValue(TEXT("max_climb_height_m"), 3.0);
|
||||||
Writer.WriteValue(TEXT("walkable_floor_angle_degrees"), static_cast<double>(FVoxelPlayerCapsuleConstants::WalkableFloorAngleDegrees));
|
Writer.WriteValue(TEXT("walkable_floor_angle_degrees"), static_cast<double>(FVoxelPlayerCapsuleConstants::WalkableFloorAngleDegrees));
|
||||||
Writer.WriteObjectEnd();
|
Writer.WriteObjectEnd();
|
||||||
}
|
}
|
||||||
@@ -3857,7 +4045,7 @@ FString BuildExploreJson(
|
|||||||
TSharedRef<FExploreJsonWriter> Writer =
|
TSharedRef<FExploreJsonWriter> Writer =
|
||||||
TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&Json);
|
TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&Json);
|
||||||
Writer->WriteObjectStart();
|
Writer->WriteObjectStart();
|
||||||
Writer->WriteValue(TEXT("schema_version"), 4);
|
Writer->WriteValue(TEXT("schema_version"), 6);
|
||||||
Writer->WriteValue(TEXT("tool"), TEXT("VoxelForgeExplore"));
|
Writer->WriteValue(TEXT("tool"), TEXT("VoxelForgeExplore"));
|
||||||
Writer->WriteValue(TEXT("read_only_generation"), true);
|
Writer->WriteValue(TEXT("read_only_generation"), true);
|
||||||
|
|
||||||
@@ -3866,6 +4054,7 @@ FString BuildExploreJson(
|
|||||||
Writer->WriteValue(TEXT("archetype"), ArchetypeName(Arguments.Archetype));
|
Writer->WriteValue(TEXT("archetype"), ArchetypeName(Arguments.Archetype));
|
||||||
Writer->WriteValue(TEXT("slot"), Arguments.Slot);
|
Writer->WriteValue(TEXT("slot"), Arguments.Slot);
|
||||||
Writer->WriteValue(TEXT("operator_stack"), Arguments.bUseOperatorStack);
|
Writer->WriteValue(TEXT("operator_stack"), Arguments.bUseOperatorStack);
|
||||||
|
WriteTunnelFloorOverrides(*Writer, Arguments);
|
||||||
Writer->WriteValue(TEXT("surface_roughness_override"), Arguments.bSurfaceRoughnessOverride);
|
Writer->WriteValue(TEXT("surface_roughness_override"), Arguments.bSurfaceRoughnessOverride);
|
||||||
if (Arguments.bSurfaceRoughnessOverride)
|
if (Arguments.bSurfaceRoughnessOverride)
|
||||||
{
|
{
|
||||||
@@ -4072,7 +4261,7 @@ FString BuildExploreJson(
|
|||||||
Writer->WriteValue(TEXT("refusal_or_error"), Output.Walk.RefusalReason);
|
Writer->WriteValue(TEXT("refusal_or_error"), Output.Walk.RefusalReason);
|
||||||
Writer->WriteValue(TEXT("window_policy"), Output.Walk.WindowPolicy);
|
Writer->WriteValue(TEXT("window_policy"), Output.Walk.WindowPolicy);
|
||||||
Writer->WriteValue(TEXT("agent_policy"), TEXT(
|
Writer->WriteValue(TEXT("agent_policy"), TEXT(
|
||||||
"fixed-order depth-first walk over the exact VF player-fit six-neighbour graph; stop at departure"));
|
"fixed-order depth-first walk over the exact VF player-fit graph; six-neighbour movement plus supported climb edges; stop at departure"));
|
||||||
Writer->WriteValue(TEXT("player_fit_volume_scope"), TEXT(
|
Writer->WriteValue(TEXT("player_fit_volume_scope"), TEXT(
|
||||||
"bounded fitted route window; WorldRadiusVoxels=0 has no finite whole-world volume"));
|
"bounded fitted route window; WorldRadiusVoxels=0 has no finite whole-world volume"));
|
||||||
Writer->WriteValue(TEXT("max_cells_cap"), Arguments.MaxWalkCells);
|
Writer->WriteValue(TEXT("max_cells_cap"), Arguments.MaxWalkCells);
|
||||||
@@ -4119,6 +4308,8 @@ FString BuildExploreJson(
|
|||||||
Writer->WriteValue(TEXT("start_snapped"), Report.bStartSnapped);
|
Writer->WriteValue(TEXT("start_snapped"), Report.bStartSnapped);
|
||||||
Writer->WriteValue(TEXT("goal_snapped"), Report.bGoalSnapped);
|
Writer->WriteValue(TEXT("goal_snapped"), Report.bGoalSnapped);
|
||||||
Writer->WriteValue(TEXT("route_retries"), Report.NumRouteRetries);
|
Writer->WriteValue(TEXT("route_retries"), Report.NumRouteRetries);
|
||||||
|
Writer->WriteValue(TEXT("climbing_enabled"), Report.bClimbingEnabled);
|
||||||
|
Writer->WriteValue(TEXT("max_climb_height_m"), static_cast<double>(Report.MaxClimbHeightMeters));
|
||||||
Writer->WriteValue(TEXT("player_fit_volume_cells"), Report.PlayerFitVolumeCells);
|
Writer->WriteValue(TEXT("player_fit_volume_cells"), Report.PlayerFitVolumeCells);
|
||||||
Writer->WriteValue(TEXT("reachable_player_fit_cells"), Report.ReachablePlayerFitCells);
|
Writer->WriteValue(TEXT("reachable_player_fit_cells"), Report.ReachablePlayerFitCells);
|
||||||
Writer->WriteValue(TEXT("reachable_player_fit_fraction"), static_cast<double>(Report.ReachablePlayerFitFraction));
|
Writer->WriteValue(TEXT("reachable_player_fit_fraction"), static_cast<double>(Report.ReachablePlayerFitFraction));
|
||||||
@@ -4133,6 +4324,7 @@ FString BuildExploreJson(
|
|||||||
Writer->WriteValue(TEXT("sampled_min_y"), static_cast<double>(Report.SampledMinY));
|
Writer->WriteValue(TEXT("sampled_min_y"), static_cast<double>(Report.SampledMinY));
|
||||||
Writer->WriteValue(TEXT("sampled_max_y_exclusive"), static_cast<double>(Report.SampledMaxY));
|
Writer->WriteValue(TEXT("sampled_max_y_exclusive"), static_cast<double>(Report.SampledMaxY));
|
||||||
Writer->WriteValue(TEXT("agent_graph_traversals"), Report.AgentGraphTraversals);
|
Writer->WriteValue(TEXT("agent_graph_traversals"), Report.AgentGraphTraversals);
|
||||||
|
Writer->WriteValue(TEXT("climb_traversals"), Report.ClimbTraversals);
|
||||||
Writer->WriteValue(TEXT("distance_travelled_m"), static_cast<double>(Report.DistanceTravelledMeters));
|
Writer->WriteValue(TEXT("distance_travelled_m"), static_cast<double>(Report.DistanceTravelledMeters));
|
||||||
Writer->WriteValue(TEXT("straight_line_m"), static_cast<double>(Report.StraightLineMeters));
|
Writer->WriteValue(TEXT("straight_line_m"), static_cast<double>(Report.StraightLineMeters));
|
||||||
Writer->WriteValue(TEXT("tortuosity"), static_cast<double>(Report.Tortuosity));
|
Writer->WriteValue(TEXT("tortuosity"), static_cast<double>(Report.Tortuosity));
|
||||||
@@ -5668,6 +5860,12 @@ bool BuildBatchCaseParams(
|
|||||||
JsonBool(Case, TEXT("op_bounds"), bOpBounds);
|
JsonBool(Case, TEXT("op_bounds"), bOpBounds);
|
||||||
bool bFailureFocus = false;
|
bool bFailureFocus = false;
|
||||||
JsonBool(Case, TEXT("failure_focus_render"), bFailureFocus);
|
JsonBool(Case, TEXT("failure_focus_render"), bFailureFocus);
|
||||||
|
bool bTunnelFloorEnabled = true;
|
||||||
|
const bool bHasTunnelFloorEnabled = JsonBool(
|
||||||
|
Case, TEXT("tunnel_floor_enabled"), bTunnelFloorEnabled);
|
||||||
|
bool bTunnelFloorTerracing = true;
|
||||||
|
const bool bHasTunnelFloorTerracing = JsonBool(
|
||||||
|
Case, TEXT("tunnel_floor_terracing"), bTunnelFloorTerracing);
|
||||||
|
|
||||||
OutParams = FString::Printf(
|
OutParams = FString::Printf(
|
||||||
TEXT("-seed=%d -archetype=%s -slot=%d -modes=%s -opstack=%d "
|
TEXT("-seed=%d -archetype=%s -slot=%d -modes=%s -opstack=%d "
|
||||||
@@ -5679,6 +5877,17 @@ bool BuildBatchCaseParams(
|
|||||||
bBlockEarlyOut ? 1 : 0, MeshMinBatchSize,
|
bBlockEarlyOut ? 1 : 0, MeshMinBatchSize,
|
||||||
bFailureFocus ? 1 : 0, *CaseOutDirectory);
|
bFailureFocus ? 1 : 0, *CaseOutDirectory);
|
||||||
|
|
||||||
|
if (bHasTunnelFloorEnabled)
|
||||||
|
{
|
||||||
|
OutParams += FString::Printf(
|
||||||
|
TEXT(" -tunnelfloor=%d"), bTunnelFloorEnabled ? 1 : 0);
|
||||||
|
}
|
||||||
|
if (bHasTunnelFloorTerracing)
|
||||||
|
{
|
||||||
|
OutParams += FString::Printf(
|
||||||
|
TEXT(" -tunnelfloorterracing=%d"), bTunnelFloorTerracing ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
if (bProfileDensity)
|
if (bProfileDensity)
|
||||||
{
|
{
|
||||||
OutParams += bProfileDensityFull
|
OutParams += bProfileDensityFull
|
||||||
@@ -5711,6 +5920,9 @@ bool BuildBatchCaseParams(
|
|||||||
{ TEXT("render_step"), TEXT("renderstep") },
|
{ TEXT("render_step"), TEXT("renderstep") },
|
||||||
{ TEXT("render_max_distance"), TEXT("rendermaxdistance") },
|
{ TEXT("render_max_distance"), TEXT("rendermaxdistance") },
|
||||||
{ TEXT("surface_roughness"), TEXT("surfaceroughness") },
|
{ TEXT("surface_roughness"), TEXT("surfaceroughness") },
|
||||||
|
{ TEXT("tunnel_floor_step_height"), TEXT("tunnelfloorstep") },
|
||||||
|
{ TEXT("tunnel_floor_max_ledge_height"), TEXT("tunnelfloormaxledge") },
|
||||||
|
{ TEXT("tunnel_floor_gentle_slope"), TEXT("tunnelfloorgentle") },
|
||||||
};
|
};
|
||||||
for (const auto& Field : FloatFields)
|
for (const auto& Field : FloatFields)
|
||||||
{
|
{
|
||||||
@@ -5720,6 +5932,19 @@ bool BuildBatchCaseParams(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TPair<const TCHAR*, const TCHAR*> IntegerFloorFields[] = {
|
||||||
|
{ TEXT("tunnel_floor_ledge_count"), TEXT("tunnelfloorledges") },
|
||||||
|
{ TEXT("tunnel_floor_max_ledges"), TEXT("tunnelfloormaxledges") },
|
||||||
|
};
|
||||||
|
for (const auto& Field : IntegerFloorFields)
|
||||||
|
{
|
||||||
|
if (JsonNumber(Case, Field.Key, Number))
|
||||||
|
{
|
||||||
|
OutParams += FString::Printf(TEXT(" -%s=%d"),
|
||||||
|
Field.Value, FMath::RoundToInt(Number));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user