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:
Fr0zkaandClaude Opus 5 committed 2026-09-10 22:01:32 +02:00
1 parent ce53552543
commit e91f1c44bb
10 files changed
+1318 -65

No files matched your search

+532 -23
View File
@@ -90,6 +90,11 @@ namespace
// 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.
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
{
@@ -443,6 +448,326 @@ namespace
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
{
float BaseFloorZ = 0.0f;
@@ -454,15 +779,66 @@ namespace
const FVector& A, float RadiusA,
const FVector& B, float RadiusB,
float T, int32 SegmentIndex, int32 NumSegments,
const FCachedTunnel& Tunnel)
const FCachedTunnel& Tunnel,
const TArray<FTunnelFloorSegmentProfile>* FloorProfiles,
const FVFRoomFloorOwnership* RoomFloorOwnership)
{
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(
A, RadiusA, B, RadiusB, T);
Terms.BaseFloorZ = FMath::Lerp(
VoxelPassageGeometry::TunnelFloorZ(A, FMath::Abs(RadiusA)),
VoxelPassageGeometry::TunnelFloorZ(B, FMath::Abs(RadiusB)),
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(
static_cast<float>(B.X - A.X), static_cast<float>(B.Y - A.Y)).Size();
@@ -481,8 +857,6 @@ namespace
/ VoxelPassageGeometry::WalkableTunnelLandingApronVoxels,
0.0f, 1.0f));
}
Terms.ReliefScale = VF_TunnelFloorReliefScale(
A, RadiusA, B, RadiusB, SegmentIndex, NumSegments, Tunnel);
return Terms;
}
@@ -513,11 +887,14 @@ namespace
const FCachedTunnel& Tunnel,
const void* CacheIdentity, int32 TunnelIndex,
bool bEvaluateRelief,
const FVFFloorReliefColumnKey& Column)
const FVFFloorReliefColumnKey& Column,
const TArray<FTunnelFloorSegmentProfile>* FloorProfiles,
const FVFRoomFloorOwnership* RoomFloorOwnership)
{
const FVFTunnelFloorTerms Terms = VF_TunnelFloorTerms(
A, RadiusA, B, RadiusB,
T, SegmentIndex, NumSegments, Tunnel);
T, SegmentIndex, NumSegments, Tunnel, FloorProfiles,
RoomFloorOwnership);
return VF_TunnelFloorFromTerms(
Position, Tunnel, Terms,
CacheIdentity, TunnelIndex, bEvaluateRelief, Column);
@@ -556,7 +933,9 @@ namespace
float SDFBlendRadius,
bool bApplyFloorCut,
const void* CacheIdentity, int32 TunnelIndex,
const FVFFloorReliefColumnKey& ReliefColumn)
const FVFFloorReliefColumnKey& ReliefColumn,
const TArray<FTunnelFloorSegmentProfile>* FloorProfiles,
const FVFRoomFloorOwnership* RoomFloorOwnership)
{
FVFTunnelShapeEvaluation Result;
if (ControlPoints.Num() < 2
@@ -592,7 +971,8 @@ namespace
const FVFTunnelFloorTerms FloorTerms = VF_TunnelFloorTerms(
A, RadiusA, B, RadiusB,
T, SegmentIndex, NumSegments, Tunnel);
T, SegmentIndex, NumSegments, Tunnel, FloorProfiles,
RoomFloorOwnership);
const float SupportRadius = FMath::Max(
FMath::Min(FMath::Abs(RadiusA), FMath::Abs(RadiusB)) - 0.5f,
VoxelPassageGeometry::PlayerRadiusVoxels);
@@ -623,6 +1003,17 @@ namespace
+ VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels;
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(
Position, Tunnel, FloorTerms,
@@ -633,7 +1024,8 @@ namespace
: SegmentSDF;
Result.SDF = FMath::Min(Result.SDF, SegmentResultSDF);
if (SegmentSDF <= 0.0f
if (bApplyFloorCut && Tunnel.bTunnelFloorEnabled
&& SegmentSDF <= 0.0f
&& HorizontalDistanceSquared <= FMath::Square(SupportRadius)
&& Position.Z >= FloorZ
- VoxelPassageGeometry::LandingFloorThicknessVoxels
@@ -652,7 +1044,8 @@ namespace
bool bWorldChain,
float SDFBlendRadius,
bool bApplyFloorCut,
const void* CacheIdentity, int32 TunnelIndex)
const void* CacheIdentity, int32 TunnelIndex,
const FVFRoomFloorOwnership* RoomFloorOwnership = nullptr)
{
const FVFFloorReliefColumnKey ReliefColumn =
VF_MakeFloorReliefColumnKey(
@@ -667,7 +1060,9 @@ namespace
return VF_EvaluateSweptTunnelChain(
Position, ShapeControlPoints, ShapeControlRadii,
Tunnel, SDFBlendRadius, bApplyFloorCut,
CacheIdentity, TunnelIndex, ReliefColumn);
CacheIdentity, TunnelIndex, ReliefColumn,
bWorldChain ? &Tunnel.WorldFloorProfiles : &Tunnel.FloorProfiles,
RoomFloorOwnership);
}
if (Tunnel.bHasMidpoint)
@@ -685,7 +1080,9 @@ namespace
return VF_EvaluateSweptTunnelChain(
Position, Points, Radii,
Tunnel, SDFBlendRadius, bApplyFloorCut,
CacheIdentity, TunnelIndex, ReliefColumn);
CacheIdentity, TunnelIndex, ReliefColumn,
bWorldChain ? &Tunnel.WorldFloorProfiles : &Tunnel.FloorProfiles,
RoomFloorOwnership);
}
TArray<FVector> Points;
@@ -697,7 +1094,9 @@ namespace
return VF_EvaluateSweptTunnelChain(
Position, Points, Radii,
Tunnel, SDFBlendRadius, bApplyFloorCut,
CacheIdentity, TunnelIndex, ReliefColumn);
CacheIdentity, TunnelIndex, ReliefColumn,
bWorldChain ? &Tunnel.WorldFloorProfiles : &Tunnel.FloorProfiles,
RoomFloorOwnership);
}
static bool VF_ProjectSweptTunnelFloor(
@@ -706,8 +1105,13 @@ namespace
bool bWorldChain,
float& OutFloorZ,
float& OutSupportRadius,
const void* CacheIdentity, int32 TunnelIndex)
const void* CacheIdentity, int32 TunnelIndex,
const FVFRoomFloorOwnership* RoomFloorOwnership = nullptr)
{
if (!Tunnel.bTunnelFloorEnabled)
{
return false;
}
const FVFFloorReliefColumnKey ReliefColumn =
VF_MakeFloorReliefColumnKey(
static_cast<float>(Position.X), static_cast<float>(Position.Y));
@@ -746,7 +1150,9 @@ namespace
ControlPoints[SegmentIndex + 1], RadiusB,
T, SegmentIndex, NumSegments, Position, Tunnel,
CacheIdentity, TunnelIndex, /*bEvaluateRelief=*/true,
ReliefColumn);
ReliefColumn,
bWorldChain ? &Tunnel.WorldFloorProfiles : &Tunnel.FloorProfiles,
RoomFloorOwnership);
OutSupportRadius = FMath::Max(
FMath::Min(FMath::Abs(RadiusA), FMath::Abs(RadiusB)) - 0.5f,
VoxelPassageGeometry::PlayerRadiusVoxels);
@@ -768,6 +1174,11 @@ void FChunkSDFCache::Reset()
// populated regions on adjacent requests.
Rooms.Reset();
RoomFloorJoins.Reset();
for (FCachedTunnel& Tunnel : Tunnels)
{
Tunnel.FloorProfiles.Reset();
Tunnel.WorldFloorProfiles.Reset();
}
Tunnels.Reset();
Pits.Reset();
Chimneys.Reset();
@@ -829,6 +1240,8 @@ VoxelDensityProfile::FCacheMemoryBreakdown FChunkSDFCache::GetAllocatedSizeBreak
Breakdown.TunnelsBytes += ArrayBytes(Tunnel.ControlRadii);
Breakdown.TunnelsBytes += ArrayBytes(Tunnel.WorldControlPoints);
Breakdown.TunnelsBytes += ArrayBytes(Tunnel.WorldControlRadii);
Breakdown.TunnelsBytes += ArrayBytes(Tunnel.FloorProfiles);
Breakdown.TunnelsBytes += ArrayBytes(Tunnel.WorldFloorProfiles);
}
return Breakdown;
@@ -4773,6 +5186,16 @@ void VoxelCaveMorphology::BuildChunkCache(
CT.FloorReliefStrength = Params.FloorReliefStrength;
CT.FloorReliefFrequency = Params.FloorReliefFrequency;
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
// 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);
}
// 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.ControlPoints[CT.ControlPoints.Num() / 2]
: FVector::ZeroVector;
@@ -5534,10 +5995,15 @@ float VoxelCaveMorphology::EvaluateSDFCached(
}
// 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(
Pos, Tunnel, /*bWorldChain=*/false, BlendK,
/*bApplyFloorCut=*/true, &Cache, TunnelIdx).SDF;
/*bApplyFloorCut=*/Tunnel.bTunnelFloorEnabled, &Cache, TunnelIdx,
nullptr).SDF;
MinSDF = VoxelSDF::SmoothMin(MinSDF, TunnelSDF, BlendK);
};
@@ -5648,6 +6114,10 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
VoxelDensityProfile::ECounter::TunnelCoreEvaluated);
}
const FVFRoomFloorOwnership RoomMouthOwnership =
VF_FindWorldTunnelMouthOwnership(Pos, Tunnel, Cache.SDFBlendRadius);
const bool bRoomOwnsThisTunnel = RoomMouthOwnership.bValid;
bool bRaisedTunnelFloor = false;
if (bHasWorldChain)
{
if (SupportColumn != nullptr && VoxelDensityProfile::AreCountersEnabled())
@@ -5665,7 +6135,25 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
: VF_ProjectSweptTunnelFloor(
Pos, Tunnel, /*bWorldChain=*/true, FloorZ, SupportRadius,
&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
+ VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels
&& (SupportColumn == nullptr
@@ -5675,20 +6163,21 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
{
Result.bSupportFloor = true;
}
if (bProjectedFloor && WorldZ <= FloorZ
if (!bRaisedTunnelFloor
&& bProjectedFloor && WorldZ <= FloorZ
+ VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels)
{
// 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
// only the air above it. This diagnostic is what distinguishes a coordinate-space
// mismatch from a genuine swept-floor capability loss.
// only the air above it. This is a shape-level ownership hand-off, not a later slab.
return;
}
}
const FVFTunnelShapeEvaluation TunnelShape = VF_EvaluateSweptTunnel(
Pos, Tunnel, bHasWorldChain, Cache.SDFBlendRadius,
/*bApplyFloorCut=*/true, &Cache, TunnelIdx);
/*bApplyFloorCut=*/Tunnel.bTunnelFloorEnabled && !bRaisedTunnelFloor,
&Cache, TunnelIdx, nullptr);
Result.SDF = FMath::Min(Result.SDF, TunnelShape.SDF);
};
@@ -5749,9 +6238,28 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
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(
Pos, Tunnel, bHasWorldChain, Cache.SDFBlendRadius,
/*bApplyFloorCut=*/true, &Cache, TunnelIdx).bSupportFloor;
/*bApplyFloorCut=*/Tunnel.bTunnelFloorEnabled, &Cache, TunnelIdx,
nullptr).bSupportFloor;
};
bool bSupport = false;
@@ -5791,6 +6299,8 @@ void VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
static_cast<uint64>(Cache.Tunnels.Num()));
}
const FVector ColumnPosition(WorldX, WorldY, 0.0f);
for (int32 TunnelIdx = 0; TunnelIdx < Cache.Tunnels.Num(); ++TunnelIdx)
{
if (!Cache.Tunnels.IsValidIndex(TunnelIdx))
@@ -5798,7 +6308,6 @@ void VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
continue;
}
const FCachedTunnel& Tunnel = Cache.Tunnels[TunnelIdx];
const FVector ColumnPosition(WorldX, WorldY, 0.0f);
float FloorZ = 0.0f;
float SupportRadius = 0.0f;
if (!VF_ProjectSweptTunnelFloor(
@@ -414,6 +414,11 @@ namespace VoxelDensityProfile
case ECounter::TunnelSupportColumnCandidates:return TEXT("TunnelSupportColumnCandidates");
case ECounter::TunnelSupportFloorQueries:return TEXT("TunnelSupportFloorQueries");
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::PassageEvaluated: return TEXT("PassageEvaluated");
case ECounter::MesherBlockTests: return TEXT("MesherBlockTests");
@@ -1540,11 +1540,30 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
if (bHaveTunnelCore)
{
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 conservative post-disturbance backstop that keeps terrain modifiers from
// removing the only player support at a mouth or a narrow cross-section.
if (bTunnelSupportFloor)
{
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelSupportFloorBackstopFires);
}
Result = FMath::Min(
Result,
-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.
if (bTunnelSupportFloor)
{
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelSupportFloorBackstopFires);
}
Density = FMath::Max(Density, FMath::Max(Params.BaseDensity * 2.0f, 1.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("TunnelHorizontalBias")) 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("WaterLevelRelative")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("CaveWarpStrength")) return Set(0.0f, HHalf);
@@ -1368,6 +1371,28 @@ namespace
Params.bTunnelsFlowTowardOrigin = CategoryRng.FRand() < 0.5f;
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)
{
Params.RoughnessNoiseType = static_cast<EVoxelNoiseType>(CategoryRng.RandRange(0, 3));
+386 -39
View File
@@ -468,6 +468,244 @@ namespace VoxelStrateMeasurePrivate
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(
const FSampleGrid& Grid,
TArray<int32>& OutComponents,
@@ -477,7 +715,9 @@ namespace VoxelStrateMeasurePrivate
int32& OutLargestComponentLowestCell,
int32& OutNumComponentsAtLeast1Pct,
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);
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.
if (X + 1 < Grid.NumX) 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 > 0) Visit(Grid.Index(X, Y, Z - 1));
}
}
const int64 ComponentCells64 = static_cast<int64>(ComponentCells);
if (OutComponentCells != nullptr)
@@ -1079,10 +1335,15 @@ namespace VoxelStrateMeasurePrivate
const FVoxelStrateMeasureSettings& Settings,
TArray<uint8>& OutPlayerFit,
int64& OutNumPlayerFitCells,
FString& OutReason)
FString& OutReason,
TArray<float>* OutPlayerFitSupportHeights = nullptr)
{
OutPlayerFit.Reset();
OutNumPlayerFitCells = 0;
if (OutPlayerFitSupportHeights != nullptr)
{
OutPlayerFitSupportHeights->Reset();
}
if (Grid.SampleStep != 1)
{
return Refuse(
@@ -1155,6 +1416,10 @@ namespace VoxelStrateMeasurePrivate
}
OutPlayerFit.Init(0u, Grid.CellCount);
if (OutPlayerFitSupportHeights != nullptr)
{
OutPlayerFitSupportHeights->Init(-FLT_MAX, Grid.CellCount);
}
TArray<int64> PlayerFitCounts;
PlayerFitCounts.Init(0, Grid.NumZ);
@@ -1261,6 +1526,10 @@ namespace VoxelStrateMeasurePrivate
continue;
}
OutPlayerFit[Current] = 1u;
if (OutPlayerFitSupportHeights != nullptr)
{
(*OutPlayerFitSupportHeights)[Current] = SupportHeight;
}
++NumPlayerFitInSlice;
}
}
@@ -1411,7 +1680,8 @@ namespace VoxelStrateMeasurePrivate
const FSampleGrid& Grid,
const FVoxelStrateMeasureSettings& Settings,
FVoxelStrateMetrics& InOutMetrics,
TArray<uint8>* OutPlayerFitMask = nullptr)
TArray<uint8>* OutPlayerFitMask = nullptr,
TArray<float>* OutPlayerFitSupportHeights = nullptr)
{
InOutMetrics.bPlayerFitResolved = false;
InOutMetrics.PlayerFitRefusalReason.Reset();
@@ -1423,9 +1693,11 @@ namespace VoxelStrateMeasurePrivate
InOutMetrics.MinimumPlayerClearanceVoxels = 0.0f;
TArray<uint8> PlayerFit;
TArray<float> PlayerFitSupportHeights;
if (!BuildPlayerFitMask(
Grid, Settings, PlayerFit, InOutMetrics.NumPlayerFitCells,
InOutMetrics.PlayerFitRefusalReason))
InOutMetrics.PlayerFitRefusalReason,
&PlayerFitSupportHeights))
{
if (OutPlayerFitMask != nullptr)
{
@@ -1453,7 +1725,9 @@ namespace VoxelStrateMeasurePrivate
LargestComponentLowestCell,
NumComponentsAtLeast1Pct,
nullptr,
&PlayerFit);
&PlayerFit,
&PlayerFitSupportHeights,
&Settings);
InOutMetrics.NumTraversableComponents = NumComponents;
InOutMetrics.LargestTraversableComponentCells = LargestComponentCells;
InOutMetrics.TraversableComponentShare = InOutMetrics.NumPlayerFitCells > 0
@@ -1466,13 +1740,18 @@ namespace VoxelStrateMeasurePrivate
{
*OutPlayerFitMask = MoveTemp(PlayerFit);
}
if (OutPlayerFitSupportHeights != nullptr)
{
*OutPlayerFitSupportHeights = MoveTemp(PlayerFitSupportHeights);
}
}
void BuildPlayerFitConnectivityMetrics(
const FSampleGrid& Grid,
const FVoxelStrateMeasureSettings& Settings,
FVoxelStrateMetrics& OutMetrics,
TArray<uint8>& OutPlayerFitMask)
TArray<uint8>& OutPlayerFitMask,
TArray<float>& OutPlayerFitSupportHeights)
{
OutMetrics = FVoxelStrateMetrics();
OutMetrics.ResolvedMarginVoxels = Grid.ResolvedMarginVoxels;
@@ -1492,7 +1771,9 @@ namespace VoxelStrateMeasurePrivate
? static_cast<float>(static_cast<double>(OutMetrics.NumAir)
/ static_cast<double>(OutMetrics.NumSampled))
: 0.0f;
BuildPlayerFitMetrics(Grid, Settings, OutMetrics, &OutPlayerFitMask);
BuildPlayerFitMetrics(
Grid, Settings, OutMetrics, &OutPlayerFitMask,
&OutPlayerFitSupportHeights);
OutMetrics.bValid = OutMetrics.NumSampled > 0;
}
@@ -1951,7 +2232,9 @@ namespace VoxelStrateMeasurePrivate
const FVector& B,
const TArray<int32>& Path,
FCoarseEdge& OutFailedEdge,
const TArray<uint8>* EligibilityMask = nullptr)
const TArray<uint8>* EligibilityMask = nullptr,
const TArray<float>* PlayerFitSupportHeights = nullptr,
const FVoxelStrateMeasureSettings* TraversalSettings = nullptr)
{
OutFailedEdge = FCoarseEdge();
@@ -1972,6 +2255,20 @@ namespace VoxelStrateMeasurePrivate
}
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 StartY = 0;
@@ -2045,7 +2342,9 @@ namespace VoxelStrateMeasurePrivate
int32 Goal,
const TArray<FCoarseEdge>& BlockedEdges,
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)
{
@@ -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
// parent chain, not just a component-membership answer.
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 > 0) Visit(Grid.Index(X, Y, Z - 1));
}
}
if (Parent[Goal] == -2)
{
@@ -2137,7 +2452,9 @@ namespace VoxelStrateMeasurePrivate
bool& bOutGoalSnapped,
int32& OutNumRouteRetries,
int32 MaxRouteRetries,
const TArray<uint8>* EligibilityMask = nullptr)
const TArray<uint8>* EligibilityMask = nullptr,
const TArray<float>* PlayerFitSupportHeights = nullptr,
const FVoxelStrateMeasureSettings* TraversalSettings = nullptr)
{
OutStart = -1;
OutGoal = -1;
@@ -2175,7 +2492,8 @@ namespace VoxelStrateMeasurePrivate
for (;;)
{
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
// full-resolution failures leave no coarse route at this resolution.
@@ -2188,7 +2506,7 @@ namespace VoxelStrateMeasurePrivate
FCoarseEdge FailedEdge;
if (FullResolutionPathIsAir(
Generator, Sampler, Grid, AVoxel, BVoxel, Path, FailedEdge,
EligibilityMask))
EligibilityMask, PlayerFitSupportHeights, TraversalSettings))
{
return EVoxelConnectivityResult::Connected;
}
@@ -2259,7 +2577,9 @@ namespace VoxelStrateMeasurePrivate
FVoxelConnectivityDiagnostics& OutDiagnostics,
const TArray<uint8>* EligibilityMask = nullptr,
TArray<int32>* OutComponents = nullptr,
int32* OutNumComponents = nullptr)
int32* OutNumComponents = nullptr,
const TArray<float>* PlayerFitSupportHeights = nullptr,
const FVoxelStrateMeasureSettings* TraversalSettings = nullptr)
{
OutDiagnostics = FVoxelConnectivityDiagnostics();
if (OutComponents != nullptr)
@@ -2298,7 +2618,9 @@ namespace VoxelStrateMeasurePrivate
LargestComponentLowestCell,
NumComponentsAtLeast1Pct,
nullptr,
EligibilityMask);
EligibilityMask,
PlayerFitSupportHeights,
TraversalSettings);
if (OutComponents != nullptr)
{
@@ -2449,6 +2771,7 @@ namespace VoxelStrateMeasurePrivate
}
TArray<uint8> PlayerFitMask;
TArray<float> PlayerFitSupportHeights;
FVoxelStrateMetrics PlayerMetrics;
if (bPlayerFitRestricted)
{
@@ -2457,7 +2780,8 @@ namespace VoxelStrateMeasurePrivate
// Manhattan feature transform here: those values belong to the separate legacy
// measurement pass, and recomputing them for every fine candidate made the gate
// needlessly expensive without changing its answer.
BuildPlayerFitConnectivityMetrics(Grid, Settings, PlayerMetrics, PlayerFitMask);
BuildPlayerFitConnectivityMetrics(
Grid, Settings, PlayerMetrics, PlayerFitMask, PlayerFitSupportHeights);
if (OutPlayerMetrics != nullptr)
{
*OutPlayerMetrics = PlayerMetrics;
@@ -2483,7 +2807,9 @@ namespace VoxelStrateMeasurePrivate
bOutGoalSnapped,
OutNumRouteRetries,
Settings.MaxRouteRetries,
bPlayerFitRestricted ? &PlayerFitMask : nullptr);
bPlayerFitRestricted ? &PlayerFitMask : nullptr,
bPlayerFitRestricted ? &PlayerFitSupportHeights : nullptr,
bPlayerFitRestricted ? &Settings : nullptr);
if (OutDiagnostics != nullptr)
{
PopulateConnectivityDiagnostics(
@@ -2495,7 +2821,11 @@ namespace VoxelStrateMeasurePrivate
bOutGoalSnapped,
OutNumRouteRetries,
*OutDiagnostics,
bPlayerFitRestricted ? &PlayerFitMask : nullptr);
bPlayerFitRestricted ? &PlayerFitMask : nullptr,
nullptr,
nullptr,
bPlayerFitRestricted ? &PlayerFitSupportHeights : nullptr,
bPlayerFitRestricted ? &Settings : nullptr);
if (bPlayerFitRestricted &&
(Result == EVoxelConnectivityResult::StartCellNotPlayerFit
|| 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(
const FSampleGrid& Grid,
const TArray<uint8>& PlayerFitMask,
const TArray<float>* PlayerFitSupportHeights,
const FVoxelStrateMeasureSettings* Settings,
int32 Cell)
{
TArray<FPlayerTraversalNeighbour, TInlineAllocator<128>> Neighbours;
AppendPlayerTraversalNeighbours(
Grid, PlayerFitMask, PlayerFitSupportHeights, Settings, Cell, Neighbours);
int32 Count = 0;
for (int32 Direction = 0; Direction < 6; ++Direction)
{
if (IsEligiblePlayerFitCell(
Grid, PlayerFitMask, GetSixNeighbour(Grid, Cell, Direction)))
for (const FPlayerTraversalNeighbour& Neighbour : Neighbours)
{
++Count;
}
}
return Count;
}
@@ -2616,6 +2936,8 @@ namespace VoxelStrateMeasurePrivate
void WalkPlayerFitGraph(
const FSampleGrid& Grid,
const TArray<uint8>& PlayerFitMask,
const TArray<float>* PlayerFitSupportHeights,
const FVoxelStrateMeasureSettings* TraversalSettings,
int32 Start,
int32 Goal,
float NarrowGapThresholdVoxels,
@@ -2651,6 +2973,15 @@ namespace VoxelStrateMeasurePrivate
auto RecordTraversal = [&](int32 From, int32 To)
{
++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)
{
if (NarrowCell[Cell] == 255u)
@@ -2678,11 +3009,15 @@ namespace VoxelStrateMeasurePrivate
break;
}
TArray<FPlayerTraversalNeighbour, TInlineAllocator<128>> Neighbours;
AppendPlayerTraversalNeighbours(
Grid, PlayerFitMask, PlayerFitSupportHeights, TraversalSettings,
Current, Neighbours);
bool bAdvanced = false;
while (NextDirection.Last() < 6)
while (NextDirection.Last() < Neighbours.Num())
{
const int32 Direction = NextDirection.Last()++;
const int32 Neighbour = GetSixNeighbour(Grid, Current, Direction);
const int32 Neighbour = Neighbours[Direction].Cell;
if (!IsEligiblePlayerFitCell(Grid, PlayerFitMask, Neighbour)
|| 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
// branches already visited is merely a DFS backtrack through a loop/junction.
if (Current != Start && Current != Goal
&& CountPlayerFitNeighbours(Grid, PlayerFitMask, Current) <= 1)
&& CountPlayerFitNeighbours(
Grid, PlayerFitMask, PlayerFitSupportHeights,
TraversalSettings, Current) <= 1)
{
++InOutReport.DeadEndsEncountered;
}
@@ -3048,6 +3385,8 @@ bool VF_MeasurePlayerFitWalkWithSampler(
OutReport.CapsuleWidthMeters = 2.0f * OutReport.CapsuleRadiusMeters;
OutReport.CapsuleHeightMeters = 2.0f * Settings.PlayerCapsuleHalfHeightVoxels
* FVoxelPlayerCapsuleConstants::VoxelSizeMeters;
OutReport.bClimbingEnabled = Settings.bPlayerClimbingEnabled;
OutReport.MaxClimbHeightMeters = Settings.PlayerMaxClimbHeightMeters;
OutReport.NarrowGapThresholdMeters = 1.5f * OutReport.CapsuleWidthMeters;
OutReport.NarrowGapDefinition = TEXT(
"widest axis-aligned horizontal air span at each traversed fit-graph cell");
@@ -3096,13 +3435,15 @@ bool VF_MeasurePlayerFitWalkWithSampler(
}
TArray<uint8> PlayerFitMask;
TArray<float> PlayerFitSupportHeights;
int64 NumPlayerFitCells = 0;
if (!BuildPlayerFitMask(
Grid,
Settings,
PlayerFitMask,
NumPlayerFitCells,
RefusalReason))
RefusalReason,
&PlayerFitSupportHeights))
{
OutReport.RefusalReason = RefusalReason;
return false;
@@ -3123,7 +3464,9 @@ bool VF_MeasurePlayerFitWalkWithSampler(
OutReport.bGoalSnapped,
NumRouteRetries,
Settings.MaxRouteRetries,
&PlayerFitMask);
&PlayerFitMask,
&PlayerFitSupportHeights,
&Settings);
FVoxelConnectivityDiagnostics Diagnostics;
TArray<int32> Components;
@@ -3139,7 +3482,9 @@ bool VF_MeasurePlayerFitWalkWithSampler(
Diagnostics,
&PlayerFitMask,
&Components,
&NumComponents);
&NumComponents,
&PlayerFitSupportHeights,
&Settings);
OutReport.bValid = true;
OutReport.Result = Result;
@@ -3177,6 +3522,8 @@ bool VF_MeasurePlayerFitWalkWithSampler(
WalkPlayerFitGraph(
Grid,
PlayerFitMask,
&PlayerFitSupportHeights,
&Settings,
Start,
Goal,
NarrowGapThresholdVoxels,
@@ -621,6 +621,18 @@ struct FCachedRoomFloorJoin
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
// (radii, Z offsets, control points) are resolved during cache build.
struct FCachedTunnel
@@ -640,11 +652,43 @@ struct FCachedTunnel
// turning a gentle authored floor ramp into a vertical break when the cave warp is nonlinear.
TArray<FVector> WorldControlPoints;
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
// same strate fields used by room floors; the seed is derived once from the tunnel pair hash.
float FloorReliefStrength = 0.0f;
float FloorReliefFrequency = 0.015f;
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;
float WorldBoundRadiusSq = 0.0f;
// Centerline AABBs and scalar influence radii used by the immutable broad phase. The
@@ -794,6 +838,9 @@ struct FTunnelCoreWorldEvaluation
{
float SDF = FLT_MAX;
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,
TunnelSupportFloorQueries,
TunnelSupportFloorChecks,
TunnelSupportFloorBackstopFires,
TunnelRoomFloorBackstopFires,
TunnelFloorProfileBuilds,
TunnelFloorProfileSegments,
TunnelFloorProfileLedges,
PassageCandidates,
PassageEvaluated,
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
// the default 1.36-voxel radius requires 4 of 5 columns, including the centre column.
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 bGoalSnapped = false;
int32 NumRouteRetries = 0;
bool bClimbingEnabled = false;
float MaxClimbHeightMeters = 0.0f;
int64 PlayerFitVolumeCells = 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
// traversal experience rather than a shortest-path length.
int64 AgentGraphTraversals = 0;
int64 ClimbTraversals = 0;
int64 DeadEndsEncountered = 0;
float DeadEndsPer100m = 0.0f;
float DistanceTravelledMeters = 0.0f;
@@ -283,6 +283,13 @@ enum class EVoxelStrateTransition : uint8
LERPF(TunnelHorizontalBias) \
SNAPF(bTunnelsFlowTowardOrigin) \
LERPF(TunnelEndpointZOffset) \
SNAPF(bTunnelFloorEnabled) \
SNAPF(bTunnelFloorTerracingEnabled) \
LERPF(TunnelFloorTerraceStepHeight) \
LERPF(TunnelFloorMaxLedgeHeight) \
LERPF(TunnelFloorGentleSlopeThreshold) \
SNAPF(TunnelFloorLedgeCountPreference) \
SNAPF(TunnelFloorMaxLedges) \
LERPF(SDFBlendRadius) \
LERPF(WaterLevelRelative) \
/* Cave warp */ \
@@ -659,6 +666,55 @@ struct VOXELFORGE_API FStrateGenerationParams
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels", meta = (ClampMin = "0.0", ClampMax = "1.0"))
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) =====
// Smooth union blend radius. Controls how rounded the junctions
@@ -223,6 +223,20 @@ struct FExploreArguments
float BudgetMinutes = DefaultBudgetMinutes;
bool bSurfaceRoughnessOverride = false;
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
{
@@ -303,6 +317,41 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
const bool bSurfaceRoughnessParsed = FParse::Value(
*Params, TEXT("surfaceroughness="), OutArguments.SurfaceRoughness);
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;
FParse::Value(*Params, TEXT("failurefocus="), FailureFocusRender);
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.");
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())
{
@@ -711,6 +816,40 @@ struct FExploreWorld
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;
TargetTopWorldZ = (Target.TopChunkZ + 1) * CHUNK_SIZE;
TargetBoundarySealThickness = BoundarySealForArchetype(
@@ -3615,6 +3754,52 @@ void WriteSurfaceReliefMetrics(
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(
const FExploreArguments& Arguments,
const FExploreExportOutput& Export)
@@ -3623,7 +3808,7 @@ FString BuildManifestJson(
TSharedRef<FExploreJsonWriter> Writer =
TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&Json);
Writer->WriteObjectStart();
Writer->WriteValue(TEXT("schema_version"), 4);
Writer->WriteValue(TEXT("schema_version"), 6);
Writer->WriteValue(TEXT("format"), TEXT("OBJ"));
Writer->WriteValue(TEXT("mesh_file"), Export.MeshFileName);
Writer->WriteValue(TEXT("mesh_positions_units"), TEXT("metres"));
@@ -3650,6 +3835,7 @@ FString BuildManifestJson(
Writer->WriteValue(TEXT("archetype"), ArchetypeName(Arguments.Archetype));
Writer->WriteValue(TEXT("slot"), Arguments.Slot);
Writer->WriteValue(TEXT("operator_stack"), Arguments.bUseOperatorStack);
WriteTunnelFloorOverrides(*Writer, Arguments);
Writer->WriteValue(TEXT("surface_roughness_override"), 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_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("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.WriteObjectEnd();
}
@@ -3857,7 +4045,7 @@ FString BuildExploreJson(
TSharedRef<FExploreJsonWriter> Writer =
TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&Json);
Writer->WriteObjectStart();
Writer->WriteValue(TEXT("schema_version"), 4);
Writer->WriteValue(TEXT("schema_version"), 6);
Writer->WriteValue(TEXT("tool"), TEXT("VoxelForgeExplore"));
Writer->WriteValue(TEXT("read_only_generation"), true);
@@ -3866,6 +4054,7 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("archetype"), ArchetypeName(Arguments.Archetype));
Writer->WriteValue(TEXT("slot"), Arguments.Slot);
Writer->WriteValue(TEXT("operator_stack"), Arguments.bUseOperatorStack);
WriteTunnelFloorOverrides(*Writer, Arguments);
Writer->WriteValue(TEXT("surface_roughness_override"), Arguments.bSurfaceRoughnessOverride);
if (Arguments.bSurfaceRoughnessOverride)
{
@@ -4072,7 +4261,7 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("refusal_or_error"), Output.Walk.RefusalReason);
Writer->WriteValue(TEXT("window_policy"), Output.Walk.WindowPolicy);
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(
"bounded fitted route window; WorldRadiusVoxels=0 has no finite whole-world volume"));
Writer->WriteValue(TEXT("max_cells_cap"), Arguments.MaxWalkCells);
@@ -4119,6 +4308,8 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("start_snapped"), Report.bStartSnapped);
Writer->WriteValue(TEXT("goal_snapped"), Report.bGoalSnapped);
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("reachable_player_fit_cells"), Report.ReachablePlayerFitCells);
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_max_y_exclusive"), static_cast<double>(Report.SampledMaxY));
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("straight_line_m"), static_cast<double>(Report.StraightLineMeters));
Writer->WriteValue(TEXT("tortuosity"), static_cast<double>(Report.Tortuosity));
@@ -5668,6 +5860,12 @@ bool BuildBatchCaseParams(
JsonBool(Case, TEXT("op_bounds"), bOpBounds);
bool bFailureFocus = false;
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(
TEXT("-seed=%d -archetype=%s -slot=%d -modes=%s -opstack=%d "
@@ -5679,6 +5877,17 @@ bool BuildBatchCaseParams(
bBlockEarlyOut ? 1 : 0, MeshMinBatchSize,
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)
{
OutParams += bProfileDensityFull
@@ -5711,6 +5920,9 @@ bool BuildBatchCaseParams(
{ TEXT("render_step"), TEXT("renderstep") },
{ TEXT("render_max_distance"), TEXT("rendermaxdistance") },
{ 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)
{
@@ -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;
}