Files
VoxelForge/Source/VoxelForge/Private/VoxelStrateMeasure.cpp
T
Fr0zkaandClaude Opus 5 8e42d6d807 feat(tunnels): floor authored at BUILD time, room clips tunnel, climb tier added
The round the owner asked for, with three corrections from Codex that are worth more
than the features.

WHAT LANDED
- The complete tunnel floor profile is authored into FCachedTunnel during
  BuildChunkCache, not derived per sample. This was his point: "it should be done at
  creation time, since it knows its destination."
- Room floor ownership blends/clips the tunnel floor at mouths -- his A > B boolean.
- The traversal validator gained a climb tier, structured exactly as agreed:
    bWalkOnlyCanReachDeparture / bClimbCanReachDeparture kept SEPARATE, with
    WalkOnly / Climb / MineRequired cell counts reported alongside.
  The comment in the header states the contract: "The primordial capability contract
  is walk-only... climb is reported as a separate tier and never makes the hard
  connectivity verdict pass." That is the distinction we wanted: reporting tiers, not
  permission.
- Floor controls exposed under Cave Morphology|Tunnels|Floor.

⚠️ CORRECTION 1 -- LARGE LEDGES ARE NOT CAPABILITY-SAFE YET, so they stay opt-in.
    default (preference 0)   fit 20,804   walk reach 10,884   0 ledges
    preference 2             fit 20,765   walk reach 10,712   360 ledges
  Ledges cost 172 walk-reachable cells. Codex kept preference 0 (the old staircase) as
  the default rather than shipping the owner's preferred look at the price of walkable
  space, and led its report with that. Correct call.

⚠️ CORRECTION 2 -- THE BACKSTOP STILL CANNOT GO. Support/room fallback counters were
  zero, but the structural protection clamp fired on 31,017 of 31,017 authored floor
  samples -- 100%. So the shape is still not correct by construction; the clamp is
  doing the work. Instrumenting it was the right move: we now have a number instead of
  an argument.

⚠️ CORRECTION 3 -- THE "X + 2A" CLEARANCE BUDGET IS NOT IMPLEMENTABLE AS I SPECIFIED.
  Interval propagation bounds FIELD VALUES, not SPATIAL DISPLACEMENT. Deriving A needs
  a proven Lipschitz/influence bound for every post-operation, which we do not have.
  Both the owner and I assumed the interval work already gave us this; it does not.
  The idea is still right, the prerequisite is a displacement bound.

MEASURED BY ME: build 0 errors; export 1.365 / 1.357 / 1.420 s (checkpoint was
1.321 / 1.357), flat within noise; 153,346 triangles.

ALSO MEASURED THIS ROUND
- Edit cost: 8 affected chunks, 195.5 ms total -- generation 195.471 ms, apply 0.059,
  diff 0.020, queue 0.001. It is full level-0 regeneration; a dirty-mesh path needs
  partial section support that is unavailable without modifying RMC.
- Streaming caps: effective generation cap 14, pending max 14, CAP HIT. Mesh applies
  per frame 4 (not hit), 2 ms apply budget (not hit), unloads 6 (not hit).
  ⇒ The generation core cap is what binds during startup fill.
- Peak export RSS 1.70 GB (was 19.5 GB before the memory work).
- LOD4 empty outliers gone. LOD0 not solved: 4,600 empty tiles at 3 us median, but 11
  mixed-classifier fallbacks over 100 ms, max 554 ms -- verdict=Mixed, then the full
  mesher, producing no triangles.

⚠️ UNVERIFIED AND CONCERNING: the reported in-game meshed times are far worse than
  412a7ad's -- p50 498.8 ms vs 115 ms, p95 1.227 s vs 410 ms, max 1.974 s vs 844 ms.
  The samples differ (9,336 ready events vs 841 requests, different traversal, possibly
  queue wait included), and the export path is flat, so I do not know whether this is a
  real regression or a different measurement. I have NOT reproduced it myself. It is
  the first thing the next round must settle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
2026-09-11 04:25:45 +02:00

3881 lines
149 KiB
C++

// Read-only, deterministic strate measurement and connectivity checks.
#include "VoxelStrateMeasure.h"
#if WITH_EDITOR
#include "VoxelGenerator.h"
#include "VoxelStrateManager.h"
#include "VoxelTypes.h"
#include "Async/ParallelFor.h"
namespace VoxelStrateMeasurePrivate
{
struct FSampleGrid
{
int32 NumX = 0;
int32 NumY = 0;
int32 NumZ = 0;
int32 SampleStep = 1;
int32 CellCount = 0;
float MinX = 0.0f;
float MinY = 0.0f;
float MinZ = 0.0f;
float MaxX = 0.0f;
float MaxY = 0.0f;
float MaxZ = 0.0f;
float ExtentX = 0.0f;
float ExtentY = 0.0f;
float ExtentZ = 0.0f;
int32 ResolvedMarginVoxels = 0;
int32 SampledMinZ = 0;
int32 SampledMaxZ = 0;
// 1 = air, 0 = solid. The polarity is deliberately explicit at the sampling site below.
TArray<uint8> Air;
// Exact scalar samples retained when the player-fit stencil needs a bounded local
// surface/clearance interpolation, or when an explicit preview capture requests them.
TArray<float> Density;
FORCEINLINE int32 Index(int32 X, int32 Y, int32 Z) const
{
return static_cast<int32>(
static_cast<int64>(X)
+ static_cast<int64>(NumX) * (static_cast<int64>(Y)
+ static_cast<int64>(NumY) * static_cast<int64>(Z)));
}
FVector CellCenter(int32 X, int32 Y, int32 Z) const
{
const float Step = static_cast<float>(SampleStep);
const float XOffset = FMath::Min(
static_cast<float>(X) * Step + 0.5f * Step, ExtentX - 0.5f);
const float YOffset = FMath::Min(
static_cast<float>(Y) * Step + 0.5f * Step, ExtentY - 0.5f);
const float ZOffset = FMath::Min(
static_cast<float>(Z) * Step + 0.5f * Step, ExtentZ - 0.5f);
return FVector(MinX + XOffset, MinY + YOffset, MinZ + ZOffset);
}
bool ContainsPoint(const FVector& Point) const
{
// The upper bounds are exclusive: a point on the box edge belongs to the next box,
// not to the last coarse cell in this measurement.
return Point.X >= MinX && Point.X < MaxX
&& Point.Y >= MinY && Point.Y < MaxY
&& Point.Z >= MinZ && Point.Z < MaxZ;
}
};
int64 CeilDivPositive(int64 Numerator, int64 Denominator)
{
return (Numerator + Denominator - 1) / Denominator;
}
bool Refuse(FString& OutReason, const TCHAR* Reason)
{
OutReason = Reason;
return false;
}
bool BuildSampleGrid(
const UVoxelGenerator* Generator,
const UVoxelStrateManager* Manager,
const IVoxelStrateDensitySampler* Sampler,
int32 StrateIndex,
int32 ExplicitBottomWorldZ,
int32 ExplicitTopWorldZ,
float ExplicitBoundarySealThickness,
bool bUseExplicitBounds,
const FVoxelStrateMeasureSettings& Settings,
bool bCaptureDensity,
FSampleGrid& OutGrid,
FString& OutReason)
{
if (bUseExplicitBounds)
{
if (Sampler == nullptr)
{
return Refuse(OutReason, TEXT("An explicit measurement window requires a density sampler."));
}
}
else if (Generator == nullptr || Manager == nullptr)
{
return Refuse(OutReason, TEXT("A generator and manager are required for a layout measurement."));
}
if (Settings.SampleStep <= 0)
{
return Refuse(OutReason, TEXT("SampleStep must be greater than zero."));
}
const bool bHasCoverPointA = Settings.CoverPointA.IsSet();
const bool bHasCoverPointB = Settings.CoverPointB.IsSet();
if (bHasCoverPointA != bHasCoverPointB)
{
return Refuse(OutReason, TEXT("CoverPointA and CoverPointB must be set together."));
}
const bool bUseFittedWindow = bHasCoverPointA && bHasCoverPointB;
if (!bUseFittedWindow && Settings.RadiusInVoxels <= 0)
{
return Refuse(OutReason, TEXT("RadiusInVoxels must be greater than zero."));
}
if (Settings.MaxCells <= 0)
{
return Refuse(OutReason, TEXT("MaxCells must be greater than zero."));
}
if (Settings.HeadroomCells < 0)
{
return Refuse(OutReason, TEXT("HeadroomCells cannot be negative."));
}
if (!bUseFittedWindow
&& (!FMath::IsFinite(Settings.CenterXY.X) || !FMath::IsFinite(Settings.CenterXY.Y)))
{
return Refuse(OutReason, TEXT("CenterXY must contain finite voxel coordinates."));
}
if (bUseFittedWindow)
{
const FVector2D& PointA = Settings.CoverPointA.GetValue();
const FVector2D& PointB = Settings.CoverPointB.GetValue();
if (!FMath::IsFinite(PointA.X) || !FMath::IsFinite(PointA.Y)
|| !FMath::IsFinite(PointB.X) || !FMath::IsFinite(PointB.Y))
{
return Refuse(OutReason, TEXT("Cover points must contain finite voxel coordinates."));
}
if (!FMath::IsFinite(Settings.CoverMarginVoxels)
|| Settings.CoverMarginVoxels < 0.0f)
{
return Refuse(OutReason, TEXT("CoverMarginVoxels must be finite and non-negative."));
}
}
int64 StrateBottomZ = 0;
int64 StrateTopZ = 0;
if (bUseExplicitBounds)
{
StrateBottomZ = static_cast<int64>(ExplicitBottomWorldZ);
StrateTopZ = static_cast<int64>(ExplicitTopWorldZ);
if (StrateTopZ <= StrateBottomZ)
{
return Refuse(OutReason, TEXT("The explicit strate has no positive vertical extent."));
}
}
else
{
const TArray<FStrateSlot>& Layout = Manager->GetLayout();
if (!Layout.IsValidIndex(StrateIndex))
{
return Refuse(OutReason, TEXT("StrateIndex is outside the manager layout."));
}
const FStrateSlot& Slot = Layout[StrateIndex];
if (Slot.Definition == nullptr)
{
return Refuse(OutReason, TEXT("The requested strate has no definition."));
}
if (Slot.TopChunkZ <= Slot.BottomChunkZ)
{
return Refuse(OutReason, TEXT("The requested strate has no positive vertical extent."));
}
StrateBottomZ = static_cast<int64>(Slot.BottomChunkZ) * CHUNK_SIZE;
StrateTopZ = (static_cast<int64>(Slot.TopChunkZ) + 1) * CHUNK_SIZE;
}
const int64 StrateHeight = StrateTopZ - StrateBottomZ;
if (StrateHeight <= 0)
{
return Refuse(OutReason, TEXT("The requested strate has no positive voxel height."));
}
// Resolve the margin from the same blended parameter query used by generation. The
// representative chunk is deliberately in the middle of this slot and uses the sample
// window centre's XY chunk, so a future non-Hard transition resolves through the manager
// exactly as generation does at that representative location.
FVector2D WindowCenter = Settings.CenterXY;
double MinX64 = 0.0;
double MaxX64 = 0.0;
double MinY64 = 0.0;
double MaxY64 = 0.0;
if (bUseFittedWindow)
{
const FVector2D& PointA = Settings.CoverPointA.GetValue();
const FVector2D& PointB = Settings.CoverPointB.GetValue();
const double Margin = static_cast<double>(Settings.CoverMarginVoxels);
MinX64 = FMath::Min(static_cast<double>(PointA.X), static_cast<double>(PointB.X)) - Margin;
MaxX64 = FMath::Max(static_cast<double>(PointA.X), static_cast<double>(PointB.X)) + Margin;
MinY64 = FMath::Min(static_cast<double>(PointA.Y), static_cast<double>(PointB.Y)) - Margin;
MaxY64 = FMath::Max(static_cast<double>(PointA.Y), static_cast<double>(PointB.Y)) + Margin;
if (Settings.bIncludeOriginInCoverWindow)
{
// The fitted box is a route-coverage contract, not merely an endpoint box. An
// origin-rooted graph may have its only mouth-to-mouth path through this spine.
// Expand the requested bounds before the MaxCells check below; never crop the
// route after discovering that the full box is too large.
MinX64 = FMath::Min(MinX64, 0.0);
MaxX64 = FMath::Max(MaxX64, 0.0);
MinY64 = FMath::Min(MinY64, 0.0);
MaxY64 = FMath::Max(MaxY64, 0.0);
}
if (Settings.bForceOriginColumnInCoverWindow)
{
// Upper bounds are exclusive. The ordinary origin policy can therefore stop at
// exactly X=0/Y=0 when both mouths are on one side; a diagnostic column probe
// needs one real sampled column straddling the origin instead.
MinX64 = FMath::Min(MinX64, -1.0);
MaxX64 = FMath::Max(MaxX64, 1.0);
MinY64 = FMath::Min(MinY64, -1.0);
MaxY64 = FMath::Max(MaxY64, 1.0);
}
WindowCenter = FVector2D(
static_cast<float>(0.5 * (MinX64 + MaxX64)),
static_cast<float>(0.5 * (MinY64 + MaxY64)));
}
else
{
MinX64 = static_cast<double>(Settings.CenterXY.X)
- static_cast<double>(Settings.RadiusInVoxels);
MaxX64 = static_cast<double>(Settings.CenterXY.X)
+ static_cast<double>(Settings.RadiusInVoxels);
MinY64 = static_cast<double>(Settings.CenterXY.Y)
- static_cast<double>(Settings.RadiusInVoxels);
MaxY64 = static_cast<double>(Settings.CenterXY.Y)
+ static_cast<double>(Settings.RadiusInVoxels);
}
const double XYExtentX = MaxX64 - MinX64;
const double XYExtentY = MaxY64 - MinY64;
if (!FMath::IsFinite(MinX64) || !FMath::IsFinite(MaxX64)
|| !FMath::IsFinite(MinY64) || !FMath::IsFinite(MaxY64)
|| !FMath::IsFinite(XYExtentX) || !FMath::IsFinite(XYExtentY)
|| XYExtentX < 1.0 || XYExtentY < 1.0)
{
return Refuse(OutReason, TEXT("The requested XY window is not a finite positive voxel box."));
}
FStrateGenerationParams RepresentativeParams;
if (bUseExplicitBounds)
{
RepresentativeParams.BoundarySealThickness = ExplicitBoundarySealThickness;
}
else
{
const TArray<FStrateSlot>& Layout = Manager->GetLayout();
const FStrateSlot& Slot = Layout[StrateIndex];
const int32 MidChunkZ = Slot.BottomChunkZ
+ (Slot.TopChunkZ - Slot.BottomChunkZ) / 2;
const FIntVector RepresentativeChunk(
FMath::FloorToInt(WindowCenter.X / static_cast<float>(CHUNK_SIZE)),
FMath::FloorToInt(WindowCenter.Y / static_cast<float>(CHUNK_SIZE)),
MidChunkZ);
RepresentativeParams = Manager->GetGenerationParams(RepresentativeChunk);
}
const int64 MaxMargin64 = FMath::Max<int64>(1, StrateHeight / 4);
const int32 MaxMargin = static_cast<int32>(
FMath::Min<int64>(MaxMargin64, static_cast<int64>(INT32_MAX)));
int32 RequestedMargin = Settings.InteriorMarginVoxels;
if (RequestedMargin < 0)
{
const float DoubleSealThickness = 2.0f * RepresentativeParams.BoundarySealThickness;
if (FMath::IsNaN(RepresentativeParams.BoundarySealThickness))
{
return Refuse(OutReason, TEXT("BoundarySealThickness is NaN; the interior margin cannot be derived."));
}
// Avoid converting an infinite value to int. A positive overflow is still an
// intentionally absurd seal request and therefore resolves to the safe maximum;
// negative infinity resolves to the minimum.
if (!FMath::IsFinite(DoubleSealThickness))
{
RequestedMargin = DoubleSealThickness > 0.0f ? MaxMargin : 1;
}
else if (DoubleSealThickness >= static_cast<float>(INT32_MAX))
{
RequestedMargin = MaxMargin;
}
else if (DoubleSealThickness <= static_cast<float>(INT32_MIN))
{
RequestedMargin = 0;
}
else
{
RequestedMargin = FMath::CeilToInt(DoubleSealThickness);
}
}
const int32 ResolvedMargin = FMath::Clamp(RequestedMargin, 0, MaxMargin);
const int64 InteriorMinZ = StrateBottomZ + static_cast<int64>(ResolvedMargin);
const int64 InteriorMaxZ = StrateTopZ - static_cast<int64>(ResolvedMargin);
const int64 InteriorHeight = InteriorMaxZ - InteriorMinZ;
if (InteriorHeight <= 0)
{
return Refuse(OutReason, TEXT("The strate has no measurable height after applying its interior margin."));
}
if (InteriorMinZ < static_cast<int64>(INT32_MIN)
|| InteriorMinZ > static_cast<int64>(INT32_MAX)
|| InteriorMaxZ < static_cast<int64>(INT32_MIN)
|| InteriorMaxZ > static_cast<int64>(INT32_MAX))
{
return Refuse(OutReason, TEXT("The sampled Z window is outside the metrics struct's int32 range."));
}
const int64 Step = static_cast<int64>(Settings.SampleStep);
const double NumXReal = XYExtentX / static_cast<double>(Step);
const double NumYReal = XYExtentY / static_cast<double>(Step);
if (!FMath::IsFinite(NumXReal) || !FMath::IsFinite(NumYReal)
|| NumXReal <= 0.0 || NumYReal <= 0.0
|| NumXReal > static_cast<double>(INT32_MAX)
|| NumYReal > static_cast<double>(INT32_MAX))
{
return Refuse(OutReason, TEXT("A sample-grid dimension exceeds the supported array size."));
}
const int64 NumX64 = FMath::CeilToInt64(NumXReal);
const int64 NumY64 = FMath::CeilToInt64(NumYReal);
const int64 NumZ64 = CeilDivPositive(InteriorHeight, Step);
const int64 MaxCells = static_cast<int64>(Settings.MaxCells);
if (NumX64 > INT32_MAX || NumY64 > INT32_MAX || NumZ64 > INT32_MAX)
{
return Refuse(OutReason, TEXT("A sample-grid dimension exceeds the supported array size."));
}
if (NumX64 > MaxCells || NumY64 > MaxCells || NumZ64 > MaxCells
|| NumX64 > MaxCells / NumY64)
{
return Refuse(OutReason, TEXT("The requested sample grid exceeds MaxCells; measurement refused."));
}
const int64 XYCells = NumX64 * NumY64;
if (XYCells > MaxCells / NumZ64)
{
return Refuse(OutReason, TEXT("The requested sample grid exceeds MaxCells; measurement refused."));
}
const int64 CellCount64 = XYCells * NumZ64;
if (CellCount64 <= 0 || CellCount64 > MaxCells || CellCount64 > INT32_MAX)
{
return Refuse(OutReason, TEXT("The requested sample grid exceeds the bounded cell count."));
}
const float MinX = static_cast<float>(MinX64);
const float MaxX = static_cast<float>(MaxX64);
const float MinY = static_cast<float>(MinY64);
const float MaxY = static_cast<float>(MaxY64);
const float MinZ = static_cast<float>(InteriorMinZ);
const float MaxZ = static_cast<float>(InteriorMaxZ);
if (!FMath::IsFinite(MinX) || !FMath::IsFinite(MaxX)
|| !FMath::IsFinite(MinY) || !FMath::IsFinite(MaxY)
|| !FMath::IsFinite(MinZ) || !FMath::IsFinite(MaxZ)
|| MaxX <= MinX || MaxY <= MinY || MaxZ <= MinZ)
{
return Refuse(OutReason, TEXT("The requested measurement bounds are not representable as finite voxel coordinates."));
}
OutGrid.NumX = static_cast<int32>(NumX64);
OutGrid.NumY = static_cast<int32>(NumY64);
OutGrid.NumZ = static_cast<int32>(NumZ64);
OutGrid.SampleStep = Settings.SampleStep;
OutGrid.CellCount = static_cast<int32>(CellCount64);
OutGrid.MinX = MinX;
OutGrid.MinY = MinY;
OutGrid.MinZ = MinZ;
OutGrid.MaxX = MaxX;
OutGrid.MaxY = MaxY;
OutGrid.MaxZ = MaxZ;
OutGrid.ExtentX = MaxX - MinX;
OutGrid.ExtentY = MaxY - MinY;
OutGrid.ExtentZ = MaxZ - MinZ;
OutGrid.ResolvedMarginVoxels = ResolvedMargin;
OutGrid.SampledMinZ = static_cast<int32>(InteriorMinZ);
OutGrid.SampledMaxZ = static_cast<int32>(InteriorMaxZ);
OutGrid.Air.SetNumUninitialized(OutGrid.CellCount);
if (bCaptureDensity)
{
OutGrid.Density.SetNumUninitialized(OutGrid.CellCount);
}
// Each Z slab writes a disjoint contiguous region. GetDensityAt is a const, read-only
// query used concurrently by the mesher, and custom samplers are required to provide
// the same const/thread-safe contract. The non-finite flag is also per-cell so the
// diagnostic reduction below never depends on task completion order.
TArray<uint8> NonFiniteFlags;
NonFiniteFlags.Init(0u, OutGrid.CellCount);
ParallelFor(OutGrid.NumZ, [&](int32 Z)
{
for (int32 Y = 0; Y < OutGrid.NumY; ++Y)
{
for (int32 X = 0; X < OutGrid.NumX; ++X)
{
const int32 CellIndex = OutGrid.Index(X, Y, Z);
const FVector Sample = OutGrid.CellCenter(X, Y, Z);
const float Density = Sampler != nullptr
? Sampler->SampleDensity(Sample.X, Sample.Y, Sample.Z)
: Generator->GetDensityAt(Sample.X, Sample.Y, Sample.Z);
if (!FMath::IsFinite(Density))
{
NonFiniteFlags[CellIndex] = 1u;
}
// MC convention in this codebase: negative is solid, positive is air.
if (bCaptureDensity)
{
OutGrid.Density[CellIndex] = Density;
}
OutGrid.Air[CellIndex] = Density > 0.0f ? 1u : 0u;
}
}
});
bool bSawNonFiniteDensity = false;
for (int32 CellIndex = 0; CellIndex < OutGrid.CellCount; ++CellIndex)
{
bSawNonFiniteDensity |= NonFiniteFlags[CellIndex] != 0u;
}
if (bSawNonFiniteDensity)
{
return Refuse(OutReason, TEXT("At least one density sample was non-finite."));
}
return true;
}
void ExportSampleGrid(FSampleGrid& Source, FVoxelStrateSampleGrid& Destination)
{
Destination = FVoxelStrateSampleGrid();
Destination.bValid = true;
Destination.NumX = Source.NumX;
Destination.NumY = Source.NumY;
Destination.NumZ = Source.NumZ;
Destination.SampleStep = Source.SampleStep;
Destination.CellCount = Source.CellCount;
Destination.MinX = Source.MinX;
Destination.MinY = Source.MinY;
Destination.MinZ = Source.MinZ;
Destination.MaxX = Source.MaxX;
Destination.MaxY = Source.MaxY;
Destination.MaxZ = Source.MaxZ;
Destination.ResolvedMarginVoxels = Source.ResolvedMarginVoxels;
Destination.SampledMinZ = Source.SampledMinZ;
Destination.SampledMaxZ = Source.SampledMaxZ;
// Move, rather than copy, the already-built polarity grid. The caller owns this buffer
// only when it explicitly requested a capture.
Destination.Air = MoveTemp(Source.Air);
Destination.Density = MoveTemp(Source.Density);
Destination.PlayerFitMask.Reset();
Destination.PlayerFitCellCount = 0;
}
void DecodeIndex(const FSampleGrid& Grid, int32 Index, int32& OutX, int32& OutY, int32& OutZ)
{
const int32 Plane = Grid.NumX * Grid.NumY;
OutZ = Index / Plane;
const int32 InPlane = Index - OutZ * Plane;
OutY = InPlane / Grid.NumX;
OutX = InPlane - OutY * Grid.NumX;
}
struct FPlayerTraversalNeighbour
{
int32 Cell = INDEX_NONE;
bool bClimb = false;
};
// Walk is the hard capability contract. The climb policy is used only for the diagnostic
// tier; keeping it explicit at every graph operation prevents a future caller from silently
// turning a climbable ledge into a walk-only connectivity pass.
enum class EPlayerTraversalPolicy : uint8
{
WalkOnly,
WalkAndClimb,
};
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 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. Test the actual vertical interval between the two
// support surfaces in the higher column. The old floor/ceil/-2 indexing skipped the only
// solid sample for short quantised ledges and was especially brittle when the support
// heights came from scalar interpolation rather than an integer cell boundary.
for (int32 Z = 0; Z < Grid.NumZ; ++Z)
{
const float SampleZ = Grid.MinZ + (static_cast<float>(Z) + 0.5f) * Step;
if (SampleZ + KINDA_SMALL_NUMBER < LowerHeight
|| SampleZ >= HigherHeight - KINDA_SMALL_NUMBER)
{
continue;
}
if (Grid.Air[Grid.Index(HigherColumnX, HigherColumnY, 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,
EPlayerTraversalPolicy Policy = EPlayerTraversalPolicy::WalkAndClimb)
{
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 (Policy == EPlayerTraversalPolicy::WalkOnly
|| !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,
EPlayerTraversalPolicy Policy = EPlayerTraversalPolicy::WalkAndClimb)
{
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, Policy))
{
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 (Policy == EPlayerTraversalPolicy::WalkOnly
|| 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, Policy)
&& bClimb)
{
OutNeighbours.Add({ Neighbor, true });
}
}
}
}
}
void FloodFillAir(
const FSampleGrid& Grid,
TArray<int32>& OutComponents,
int32& OutNumComponents,
int64 NumAir,
int64& OutLargestComponentCells,
int32& OutLargestComponentLowestCell,
int32& OutNumComponentsAtLeast1Pct,
TArray<int64>* OutComponentCells,
const TArray<uint8>* EligibilityMask = nullptr,
const TArray<float>* PlayerFitSupportHeights = nullptr,
const FVoxelStrateMeasureSettings* TraversalSettings = nullptr,
EPlayerTraversalPolicy TraversalPolicy = EPlayerTraversalPolicy::WalkAndClimb)
{
OutComponents.SetNumUninitialized(Grid.CellCount);
for (int32 Index = 0; Index < Grid.CellCount; ++Index)
{
OutComponents[Index] = -1;
}
TArray<int32> Queue;
Queue.SetNumUninitialized(Grid.CellCount);
OutNumComponents = 0;
OutLargestComponentCells = 0;
OutLargestComponentLowestCell = INDEX_NONE;
OutNumComponentsAtLeast1Pct = 0;
if (OutComponentCells != nullptr)
{
OutComponentCells->Reset();
}
for (int32 Start = 0; Start < Grid.CellCount; ++Start)
{
if (Grid.Air[Start] == 0u
|| (EligibilityMask != nullptr && (*EligibilityMask)[Start] == 0u)
|| OutComponents[Start] >= 0)
{
continue;
}
const int32 ComponentId = OutNumComponents++;
int32 Head = 0;
int32 Tail = 0;
int32 ComponentCells = 0;
OutComponents[Start] = ComponentId;
Queue[Tail++] = Start;
while (Head < Tail)
{
const int32 Current = Queue[Head++];
++ComponentCells;
int32 X = 0;
int32 Y = 0;
int32 Z = 0;
DecodeIndex(Grid, Current, X, Y, Z);
auto Visit = [&](int32 Neighbor)
{
if (Grid.Air[Neighbor] != 0u
&& (EligibilityMask == nullptr || (*EligibilityMask)[Neighbor] != 0u)
&& OutComponents[Neighbor] < 0)
{
OutComponents[Neighbor] = ComponentId;
Queue[Tail++] = Neighbor;
}
};
if (EligibilityMask != nullptr
&& PlayerFitSupportHeights != nullptr
&& TraversalSettings != nullptr)
{
TArray<FPlayerTraversalNeighbour, TInlineAllocator<128>> Neighbours;
AppendPlayerTraversalNeighbours(
Grid, *EligibilityMask, PlayerFitSupportHeights,
TraversalSettings, Current, Neighbours, TraversalPolicy);
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));
if (Y + 1 < Grid.NumY) Visit(Grid.Index(X, Y + 1, Z));
if (Y > 0) Visit(Grid.Index(X, Y - 1, Z));
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)
{
OutComponentCells->Add(ComponentCells64);
}
if (NumAir > 0 && ComponentCells64 * 100 >= NumAir)
{
++OutNumComponentsAtLeast1Pct;
}
if (ComponentCells64 > OutLargestComponentCells
|| (ComponentCells64 == OutLargestComponentCells
&& (OutLargestComponentLowestCell == INDEX_NONE
|| Start < OutLargestComponentLowestCell)))
{
OutLargestComponentCells = ComponentCells64;
OutLargestComponentLowestCell = Start;
}
}
}
void BuildManhattanDistanceToSolid(const FSampleGrid& Grid, TArray<int32>& OutDistances)
{
// Two wavefront passes with 6-neighbour Manhattan relaxation. A cell depends only on the
// immediately preceding diagonal in the forward pass (or following diagonal in the
// reverse pass), so each diagonal is safe to process in parallel while the diagonal
// barriers preserve the exact raster result. Each coarse grid unit is SampleStep VOXELS.
constexpr int32 Infinity = INT32_MAX;
OutDistances.SetNumUninitialized(Grid.CellCount);
ParallelFor(Grid.CellCount, [&](int32 Index)
{
OutDistances[Index] = Grid.Air[Index] == 0u ? 0 : Infinity;
});
const int32 LastDiagonal = Grid.NumX + Grid.NumY + Grid.NumZ - 3;
const auto ProcessDiagonal = [&](int32 Diagonal, bool bForward)
{
const int32 MinZ = FMath::Max(
0, Diagonal - (Grid.NumX - 1) - (Grid.NumY - 1));
const int32 MaxZ = FMath::Min(Grid.NumZ - 1, Diagonal);
if (MinZ > MaxZ)
{
return;
}
int32 DiagonalCellCount = 0;
for (int32 Z = MinZ; Z <= MaxZ; ++Z)
{
const int32 MinY = FMath::Max(
0, Diagonal - Z - (Grid.NumX - 1));
const int32 MaxY = FMath::Min(Grid.NumY - 1, Diagonal - Z);
DiagonalCellCount += FMath::Max(0, MaxY - MinY + 1);
}
const auto ProcessZ = [&](int32 Z)
{
const int32 MinY = FMath::Max(
0, Diagonal - Z - (Grid.NumX - 1));
const int32 MaxY = FMath::Min(Grid.NumY - 1, Diagonal - Z);
for (int32 Y = MinY; Y <= MaxY; ++Y)
{
const int32 X = Diagonal - Z - Y;
const int32 Current = Grid.Index(X, Y, Z);
if (Grid.Air[Current] == 0u)
{
continue;
}
const auto Relax = [&](int32 Neighbor)
{
if (OutDistances[Neighbor] == Infinity)
{
return;
}
const int32 Candidate = OutDistances[Neighbor] == INT32_MAX - 1
? Infinity : OutDistances[Neighbor] + 1;
OutDistances[Current] = FMath::Min(
OutDistances[Current], Candidate);
};
if (bForward)
{
if (X > 0) Relax(Grid.Index(X - 1, Y, Z));
if (Y > 0) Relax(Grid.Index(X, Y - 1, Z));
if (Z > 0) Relax(Grid.Index(X, Y, Z - 1));
}
else
{
if (X + 1 < Grid.NumX) Relax(Grid.Index(X + 1, Y, Z));
if (Y + 1 < Grid.NumY) Relax(Grid.Index(X, Y + 1, Z));
if (Z + 1 < Grid.NumZ) Relax(Grid.Index(X, Y, Z + 1));
}
}
};
// Tiny edge diagonals are cheaper to execute directly; substantial diagonals get
// one task per Z row, with every row writing a disjoint set of cells.
const int32 ZCount = MaxZ - MinZ + 1;
if (ZCount > 1 && DiagonalCellCount >= 256)
{
ParallelFor(ZCount, [&](int32 ZOffset)
{
ProcessZ(MinZ + ZOffset);
});
}
else
{
for (int32 Z = MinZ; Z <= MaxZ; ++Z)
{
ProcessZ(Z);
}
}
};
for (int32 Diagonal = 0; Diagonal <= LastDiagonal; ++Diagonal)
{
ProcessDiagonal(Diagonal, true);
}
for (int32 Diagonal = LastDiagonal; Diagonal >= 0; --Diagonal)
{
ProcessDiagonal(Diagonal, false);
}
}
struct FPlayerCapsuleStencilRow
{
int32 RelativeZ = 0;
TArray<FIntPoint> HorizontalOffsets;
};
bool HasScalarDensity(const FSampleGrid& Grid)
{
return Grid.Density.Num() == Grid.CellCount;
}
float CellDensity(const FSampleGrid& Grid, int32 Cell)
{
return HasScalarDensity(Grid)
? Grid.Density[Cell]
: (Grid.Air[Cell] != 0u ? 1.0f : -1.0f);
}
bool SampleGridDensity(
const FSampleGrid& Grid,
const FVector& Point,
float& OutDensity)
{
if (!Grid.ContainsPoint(Point))
{
return false;
}
const float Step = static_cast<float>(Grid.SampleStep);
const float LocalX = (Point.X - Grid.MinX) / Step - 0.5f;
const float LocalY = (Point.Y - Grid.MinY) / Step - 0.5f;
const float LocalZ = (Point.Z - Grid.MinZ) / Step - 0.5f;
if (!HasScalarDensity(Grid))
{
const int32 X = FMath::Clamp(
FMath::FloorToInt((Point.X - Grid.MinX) / Step), 0, Grid.NumX - 1);
const int32 Y = FMath::Clamp(
FMath::FloorToInt((Point.Y - Grid.MinY) / Step), 0, Grid.NumY - 1);
const int32 Z = FMath::Clamp(
FMath::FloorToInt((Point.Z - Grid.MinZ) / Step), 0, Grid.NumZ - 1);
OutDensity = CellDensity(Grid, Grid.Index(X, Y, Z));
return FMath::IsFinite(OutDensity);
}
const int32 X0 = FMath::Clamp(FMath::FloorToInt(LocalX), 0, Grid.NumX - 1);
const int32 Y0 = FMath::Clamp(FMath::FloorToInt(LocalY), 0, Grid.NumY - 1);
const int32 Z0 = FMath::Clamp(FMath::FloorToInt(LocalZ), 0, Grid.NumZ - 1);
const int32 X1 = FMath::Min(X0 + 1, Grid.NumX - 1);
const int32 Y1 = FMath::Min(Y0 + 1, Grid.NumY - 1);
const int32 Z1 = FMath::Min(Z0 + 1, Grid.NumZ - 1);
const float AlphaX = FMath::Clamp(LocalX - static_cast<float>(X0), 0.0f, 1.0f);
const float AlphaY = FMath::Clamp(LocalY - static_cast<float>(Y0), 0.0f, 1.0f);
const float AlphaZ = FMath::Clamp(LocalZ - static_cast<float>(Z0), 0.0f, 1.0f);
const float D000 = CellDensity(Grid, Grid.Index(X0, Y0, Z0));
const float D100 = CellDensity(Grid, Grid.Index(X1, Y0, Z0));
const float D010 = CellDensity(Grid, Grid.Index(X0, Y1, Z0));
const float D110 = CellDensity(Grid, Grid.Index(X1, Y1, Z0));
const float D001 = CellDensity(Grid, Grid.Index(X0, Y0, Z1));
const float D101 = CellDensity(Grid, Grid.Index(X1, Y0, Z1));
const float D011 = CellDensity(Grid, Grid.Index(X0, Y1, Z1));
const float D111 = CellDensity(Grid, Grid.Index(X1, Y1, Z1));
const float D00 = FMath::Lerp(D000, D100, AlphaX);
const float D10 = FMath::Lerp(D010, D110, AlphaX);
const float D01 = FMath::Lerp(D001, D101, AlphaX);
const float D11 = FMath::Lerp(D011, D111, AlphaX);
const float D0 = FMath::Lerp(D00, D10, AlphaY);
const float D1 = FMath::Lerp(D01, D11, AlphaY);
OutDensity = FMath::Lerp(D0, D1, AlphaZ);
return FMath::IsFinite(OutDensity);
}
bool FindSupportSurfaceBelow(
const FSampleGrid& Grid,
int32 X,
int32 Y,
int32 CandidateAnchorZ,
int32 MaxDownwardSearchCells,
float MaxStepHeightVoxels,
float& OutSurfaceHeight,
int32& OutSurfaceAnchorZ)
{
const float Step = static_cast<float>(Grid.SampleStep);
const float CandidateBottom = Grid.MinZ
+ static_cast<float>(CandidateAnchorZ) * Step;
const float LowestAllowedSurface = CandidateBottom - MaxStepHeightVoxels;
const int32 LowestAnchorZ = FMath::Max(
1, CandidateAnchorZ - MaxDownwardSearchCells);
// Search from the free pose downwards. The first transition in a column is the nearest
// supporting surface; lower transitions are relevant only after the candidate has been
// lifted by the bounded step-height tolerance.
for (int32 SurfaceAnchorZ = CandidateAnchorZ;
SurfaceAnchorZ >= LowestAnchorZ;
--SurfaceAnchorZ)
{
const int32 SolidZ = SurfaceAnchorZ - 1;
if (SolidZ < 0 || SurfaceAnchorZ >= Grid.NumZ)
{
continue;
}
const int32 SolidCell = Grid.Index(X, Y, SolidZ);
const int32 AirCell = Grid.Index(X, Y, SurfaceAnchorZ);
if (Grid.Air[SolidCell] != 0u || Grid.Air[AirCell] == 0u)
{
continue;
}
float SurfaceHeight = Grid.MinZ + static_cast<float>(SurfaceAnchorZ) * Step;
if (HasScalarDensity(Grid))
{
const float LowerDensity = CellDensity(Grid, SolidCell);
const float UpperDensity = CellDensity(Grid, AirCell);
const float Denominator = UpperDensity - LowerDensity;
if (!FMath::IsFinite(LowerDensity) || !FMath::IsFinite(UpperDensity)
|| !FMath::IsFinite(Denominator) || Denominator <= 0.0f)
{
continue;
}
const float Fraction = FMath::Clamp(
-LowerDensity / Denominator, 0.0f, 1.0f);
SurfaceHeight = Grid.CellCenter(X, Y, SolidZ).Z + Fraction * Step;
}
if (FMath::IsFinite(SurfaceHeight)
&& SurfaceHeight <= CandidateBottom + KINDA_SMALL_NUMBER
&& SurfaceHeight >= LowestAllowedSurface - KINDA_SMALL_NUMBER)
{
OutSurfaceHeight = SurfaceHeight;
OutSurfaceAnchorZ = SurfaceAnchorZ;
return true;
}
}
return false;
}
bool BuildPlayerSupportPatchOffsets(
const FVoxelStrateMeasureSettings& Settings,
TArray<FIntPoint>& OutOffsets,
int32& OutCentreOffsetIndex,
FString& OutReason)
{
OutOffsets.Reset();
OutCentreOffsetIndex = INDEX_NONE;
const double Radius = static_cast<double>(Settings.PlayerCapsuleRadiusVoxels);
if (!FMath::IsFinite(Radius) || Radius <= 0.0)
{
return Refuse(OutReason,
TEXT("Player capsule radius must be finite and greater than zero."));
}
const double MaxOffsetReal = FMath::CeilToDouble(Radius);
constexpr double MaxSupportPatchOffsets = static_cast<double>(1ll << 20);
if (!FMath::IsFinite(MaxOffsetReal) || MaxOffsetReal > 4096.0)
{
return Refuse(OutReason,
TEXT("The player support patch radius exceeds the bounded fit stencil."));
}
const int32 MaxOffset = static_cast<int32>(MaxOffsetReal);
int64 NumOffsets = 0;
for (int32 OffsetY = -MaxOffset; OffsetY <= MaxOffset; ++OffsetY)
{
for (int32 OffsetX = -MaxOffset; OffsetX <= MaxOffset; ++OffsetX)
{
if (static_cast<double>(OffsetX * OffsetX + OffsetY * OffsetY)
> Radius * Radius + KINDA_SMALL_NUMBER)
{
continue;
}
if (NumOffsets >= static_cast<int64>(MaxSupportPatchOffsets))
{
return Refuse(
OutReason,
TEXT("The player support patch exceeds its bounded work limit."));
}
if (OffsetX == 0 && OffsetY == 0)
{
OutCentreOffsetIndex = OutOffsets.Num();
}
OutOffsets.Add(FIntPoint(OffsetX, OffsetY));
++NumOffsets;
}
}
if (OutCentreOffsetIndex == INDEX_NONE || OutOffsets.IsEmpty())
{
return Refuse(OutReason, TEXT("The player support patch has no footprint samples."));
}
return true;
}
bool IsWalkableSupportPatch(
const TArray<FIntPoint>& SupportOffsets,
const TArray<float>& SupportHeights,
const TArray<uint8>& bSupported,
int32 CentreOffsetIndex,
int32 RequiredSupportCount,
float MinimumWalkableNormalZ,
int32& OutSupportCount,
float& OutSupportHeight,
int32& OutSupportAnchorZ,
const TArray<int32>& SupportAnchorZs)
{
OutSupportCount = 0;
OutSupportHeight = -FLT_MAX;
OutSupportAnchorZ = INDEX_NONE;
if (!bSupported.IsValidIndex(CentreOffsetIndex)
|| bSupported[CentreOffsetIndex] == 0u)
{
return false;
}
for (int32 Index = 0; Index < SupportOffsets.Num(); ++Index)
{
if (bSupported[Index] == 0u)
{
continue;
}
++OutSupportCount;
if (SupportHeights[Index] > OutSupportHeight
|| (SupportHeights[Index] == OutSupportHeight
&& (OutSupportAnchorZ == INDEX_NONE
|| SupportAnchorZs[Index] > OutSupportAnchorZ)))
{
OutSupportHeight = SupportHeights[Index];
OutSupportAnchorZ = SupportAnchorZs[Index];
}
}
if (OutSupportCount < RequiredSupportCount || OutSupportAnchorZ == INDEX_NONE)
{
return false;
}
// The worst pairwise secant is intentionally conservative: an isolated height jump in
// the footprint cannot hide behind an average plane normal. For the default 1.36-voxel
// capsule this checks the centre and the four cardinal support samples.
float MaximumGradient = 0.0f;
for (int32 First = 0; First < SupportOffsets.Num(); ++First)
{
if (bSupported[First] == 0u)
{
continue;
}
for (int32 Second = First + 1; Second < SupportOffsets.Num(); ++Second)
{
if (bSupported[Second] == 0u)
{
continue;
}
const float DX = static_cast<float>(
SupportOffsets[Second].X - SupportOffsets[First].X);
const float DY = static_cast<float>(
SupportOffsets[Second].Y - SupportOffsets[First].Y);
const float HorizontalDistance = FMath::Sqrt(DX * DX + DY * DY);
if (HorizontalDistance <= KINDA_SMALL_NUMBER)
{
continue;
}
MaximumGradient = FMath::Max(
MaximumGradient,
FMath::Abs(SupportHeights[Second] - SupportHeights[First])
/ HorizontalDistance);
}
}
const float SupportNormalZ = 1.0f / FMath::Sqrt(
1.0f + MaximumGradient * MaximumGradient);
return FMath::IsFinite(SupportNormalZ)
&& SupportNormalZ + KINDA_SMALL_NUMBER >= MinimumWalkableNormalZ;
}
bool CapsuleClearAtSupportHeight(
const FSampleGrid& Grid,
const TArray<FPlayerCapsuleStencilRow>& StencilRows,
int32 X,
int32 Y,
float SupportHeight)
{
const float Step = static_cast<float>(Grid.SampleStep);
const float BaseX = Grid.MinX + (static_cast<float>(X) + 0.5f) * Step;
const float BaseY = Grid.MinY + (static_cast<float>(Y) + 0.5f) * Step;
for (const FPlayerCapsuleStencilRow& Row : StencilRows)
{
const float SampleZ = SupportHeight + static_cast<float>(Row.RelativeZ) + 0.5f * Step;
for (const FIntPoint& Offset : Row.HorizontalOffsets)
{
float Density = 0.0f;
if (!SampleGridDensity(
Grid,
FVector(
BaseX + static_cast<float>(Offset.X) * Step,
BaseY + static_cast<float>(Offset.Y) * Step,
SampleZ),
Density)
|| !(Density > 0.0f))
{
return false;
}
}
}
return true;
}
bool IsPointInsideCapsule(
float RelativeZ,
int32 OffsetX,
int32 OffsetY,
float Radius,
float HalfHeight)
{
// The capsule is upright and this row is evaluated relative to the resolved support
// surface. This is a direct capsule test in the fine voxel lattice; it deliberately does
// not infer occupancy from Manhattan distance.
const float AxisHalfLength = FMath::Max(0.0f, HalfHeight - Radius);
const float DistanceToAxis = FMath::Max(
FMath::Abs(RelativeZ) - AxisHalfLength, 0.0f);
const float DistanceSquared = static_cast<float>(OffsetX * OffsetX + OffsetY * OffsetY)
+ DistanceToAxis * DistanceToAxis;
return DistanceSquared <= Radius * Radius + KINDA_SMALL_NUMBER;
}
bool BuildPlayerCapsuleStencil(
const FVoxelStrateMeasureSettings& Settings,
TArray<FPlayerCapsuleStencilRow>& OutRows,
FString& OutReason)
{
OutRows.Reset();
if (!FMath::IsFinite(Settings.PlayerCapsuleRadiusVoxels)
|| Settings.PlayerCapsuleRadiusVoxels <= 0.0f
|| !FMath::IsFinite(Settings.PlayerCapsuleHalfHeightVoxels)
|| Settings.PlayerCapsuleHalfHeightVoxels <= 0.0f)
{
return Refuse(OutReason,
TEXT("Player capsule radius and half-height must be finite and greater than zero."));
}
const double HeightCellsReal = FMath::CeilToDouble(
2.0 * static_cast<double>(Settings.PlayerCapsuleHalfHeightVoxels));
// This is a stencil-size guard, not a gameplay threshold. It prevents a malformed
// caller-supplied capsule from allocating an unbounded per-anchor loop.
constexpr double MaxStencilHeightCells = 4096.0;
if (!FMath::IsFinite(HeightCellsReal) || HeightCellsReal <= 0.0
|| HeightCellsReal > MaxStencilHeightCells)
{
return Refuse(OutReason,
TEXT("The player capsule height exceeds the bounded fit stencil."));
}
const double MaxHorizontalOffsetReal = FMath::CeilToDouble(
static_cast<double>(Settings.PlayerCapsuleRadiusVoxels));
constexpr double MaxStencilHorizontalRadiusCells = 4096.0;
if (!FMath::IsFinite(MaxHorizontalOffsetReal)
|| MaxHorizontalOffsetReal < 0.0
|| MaxHorizontalOffsetReal > MaxStencilHorizontalRadiusCells)
{
return Refuse(OutReason,
TEXT("The player capsule radius exceeds the bounded fit stencil."));
}
const int32 HeightCells = static_cast<int32>(HeightCellsReal);
const int32 MaxHorizontalOffset = static_cast<int32>(MaxHorizontalOffsetReal);
// This bounds both the retained stencil and the per-anchor occupancy work. It is
// deliberately far above the confirmed capsule's handful of offsets, but prevents a
// malformed caller from turning a valid-looking finite radius into an enormous array.
constexpr int64 MaxStencilOffsets = 1ll << 20;
int64 NumStencilOffsets = 0;
OutRows.Reserve(HeightCells);
for (int32 RelativeZ = 0; RelativeZ < HeightCells; ++RelativeZ)
{
FPlayerCapsuleStencilRow& Row = OutRows.AddDefaulted_GetRef();
Row.RelativeZ = RelativeZ;
const float CapsuleRelativeZ = static_cast<float>(RelativeZ) + 0.5f
- Settings.PlayerCapsuleHalfHeightVoxels;
for (int32 OffsetY = -MaxHorizontalOffset;
OffsetY <= MaxHorizontalOffset;
++OffsetY)
{
for (int32 OffsetX = -MaxHorizontalOffset;
OffsetX <= MaxHorizontalOffset;
++OffsetX)
{
if (IsPointInsideCapsule(
CapsuleRelativeZ, OffsetX, OffsetY,
Settings.PlayerCapsuleRadiusVoxels,
Settings.PlayerCapsuleHalfHeightVoxels))
{
if (NumStencilOffsets >= MaxStencilOffsets)
{
return Refuse(
OutReason,
TEXT("The player capsule occupancy stencil exceeds its bounded work limit."));
}
Row.HorizontalOffsets.Add(FIntPoint(OffsetX, OffsetY));
++NumStencilOffsets;
}
}
}
// Keep a centre sample for every row. This matters for the top partial row of a
// capsule whose height is not an integer number of voxels: the full-height clearance
// requirement must not silently become a seven-centre-point approximation.
if (Row.HorizontalOffsets.IsEmpty())
{
Row.HorizontalOffsets.Add(FIntPoint::ZeroValue);
}
}
return true;
}
bool BuildPlayerFitMask(
const FSampleGrid& Grid,
const FVoxelStrateMeasureSettings& Settings,
TArray<uint8>& OutPlayerFit,
int64& OutNumPlayerFitCells,
FString& OutReason,
TArray<float>* OutPlayerFitSupportHeights = nullptr)
{
OutPlayerFit.Reset();
OutNumPlayerFitCells = 0;
if (OutPlayerFitSupportHeights != nullptr)
{
OutPlayerFitSupportHeights->Reset();
}
if (Grid.SampleStep != 1)
{
return Refuse(
OutReason,
*FString::Printf(
TEXT("Player-fit metrics refused: SampleStep=%d; exact capsule fit requires SampleStep=1."),
Grid.SampleStep));
}
if (!FMath::IsFinite(Settings.PlayerMaxStepHeightMeters)
|| Settings.PlayerMaxStepHeightMeters < 0.0f)
{
return Refuse(
OutReason,
TEXT("PlayerMaxStepHeightMeters must be finite and non-negative."));
}
if (!FMath::IsFinite(Settings.PlayerWalkableFloorAngleDegrees)
|| Settings.PlayerWalkableFloorAngleDegrees < 0.0f
|| Settings.PlayerWalkableFloorAngleDegrees > 90.0f)
{
return Refuse(
OutReason,
TEXT("PlayerWalkableFloorAngleDegrees must be finite and within [0,90]."));
}
if (!FMath::IsFinite(Settings.PlayerSupportPatchMinCoverageFraction)
|| Settings.PlayerSupportPatchMinCoverageFraction <= 0.0f
|| Settings.PlayerSupportPatchMinCoverageFraction > 1.0f)
{
return Refuse(
OutReason,
TEXT("PlayerSupportPatchMinCoverageFraction must be within (0,1]."));
}
const double MaxStepHeightVoxelsReal =
static_cast<double>(Settings.PlayerMaxStepHeightMeters)
/ static_cast<double>(FVoxelPlayerCapsuleConstants::VoxelSizeMeters);
constexpr double MaxSupportSearchHeightVoxels = 4096.0;
if (!FMath::IsFinite(MaxStepHeightVoxelsReal)
|| MaxStepHeightVoxelsReal < 0.0
|| MaxStepHeightVoxelsReal > MaxSupportSearchHeightVoxels)
{
return Refuse(
OutReason,
TEXT("PlayerMaxStepHeightMeters exceeds the bounded support search."));
}
const int32 MaxDownwardSearchCells = static_cast<int32>(
FMath::CeilToDouble(MaxStepHeightVoxelsReal));
const float MaxStepHeightVoxels = static_cast<float>(MaxStepHeightVoxelsReal);
const float MinimumWalkableNormalZ = FMath::Cos(FMath::DegreesToRadians(
Settings.PlayerWalkableFloorAngleDegrees));
TArray<FIntPoint> SupportOffsets;
int32 CentreOffsetIndex = INDEX_NONE;
if (!BuildPlayerSupportPatchOffsets(
Settings, SupportOffsets, CentreOffsetIndex, OutReason))
{
return false;
}
const int32 RequiredSupportCount = FMath::Clamp(
FMath::CeilToInt(
Settings.PlayerSupportPatchMinCoverageFraction
* static_cast<float>(SupportOffsets.Num())),
1,
SupportOffsets.Num());
TArray<FPlayerCapsuleStencilRow> StencilRows;
if (!BuildPlayerCapsuleStencil(Settings, StencilRows, OutReason))
{
return false;
}
OutPlayerFit.Init(0u, Grid.CellCount);
if (OutPlayerFitSupportHeights != nullptr)
{
OutPlayerFitSupportHeights->Init(-FLT_MAX, Grid.CellCount);
}
TArray<int64> PlayerFitCounts;
PlayerFitCounts.Init(0, Grid.NumZ);
// A candidate pose only reads the immutable sampled grid and writes its own mask cell.
// Give each Z slab private stencil scratch, then reduce the per-slab counts in Z order
// below. This removes the old shared scratch/count race without changing any predicate.
ParallelFor(FMath::Max(Grid.NumZ - 1, 0), [&](int32 SliceIndex)
{
const int32 Z = SliceIndex + 1;
TArray<float> SupportHeights;
SupportHeights.SetNumUninitialized(SupportOffsets.Num());
TArray<int32> SupportAnchorZs;
SupportAnchorZs.SetNumUninitialized(SupportOffsets.Num());
TArray<uint8> bSupported;
bSupported.Init(0u, SupportOffsets.Num());
int64 NumPlayerFitInSlice = 0;
for (int32 Y = 0; Y < Grid.NumY; ++Y)
{
for (int32 X = 0; X < Grid.NumX; ++X)
{
const int32 Current = Grid.Index(X, Y, Z);
// This is the free-pose candidate. Its support is resolved independently
// below, so an air cell one or two voxels above a surface can be lowered to
// that surface when the character's named step height permits it.
if (Grid.Air[Current] == 0u)
{
continue;
}
int32 NumSupported = 0;
for (int32 SupportIndex = 0;
SupportIndex < SupportOffsets.Num();
++SupportIndex)
{
const FIntPoint& Offset = SupportOffsets[SupportIndex];
const int32 SupportX = X + Offset.X;
const int32 SupportY = Y + Offset.Y;
bSupported[SupportIndex] = 0u;
if (SupportX < 0 || SupportX >= Grid.NumX
|| SupportY < 0 || SupportY >= Grid.NumY)
{
continue;
}
float SurfaceHeight = 0.0f;
int32 SurfaceAnchorZ = INDEX_NONE;
if (FindSupportSurfaceBelow(
Grid,
SupportX,
SupportY,
Z,
MaxDownwardSearchCells,
MaxStepHeightVoxels,
SurfaceHeight,
SurfaceAnchorZ))
{
bSupported[SupportIndex] = 1u;
SupportHeights[SupportIndex] = SurfaceHeight;
SupportAnchorZs[SupportIndex] = SurfaceAnchorZ;
++NumSupported;
}
}
if (NumSupported < RequiredSupportCount
|| bSupported[CentreOffsetIndex] == 0u)
{
continue;
}
int32 SupportCount = 0;
float SupportHeight = 0.0f;
int32 SupportAnchorZ = INDEX_NONE;
if (!IsWalkableSupportPatch(
SupportOffsets,
SupportHeights,
bSupported,
CentreOffsetIndex,
RequiredSupportCount,
MinimumWalkableNormalZ,
SupportCount,
SupportHeight,
SupportAnchorZ,
SupportAnchorZs))
{
continue;
}
// Resolve the pose to the highest coherent support in the patch and run the
// full capsule stencil again there. The support search alone is not allowed
// to turn a ledge or an overhang into a fit result. Keep the original free
// pose cell in the mask: every accepted pose is a valid point in the
// existing 6-neighbour traversal graph, and retaining the bounded vertical
// band is what lets adjacent gently sloped surfaces overlap instead of
// becoming diagonal, disconnected floor anchors.
if (!CapsuleClearAtSupportHeight(
Grid, StencilRows, X, Y, SupportHeight))
{
continue;
}
if (SupportAnchorZ < 1 || SupportAnchorZ >= Grid.NumZ)
{
continue;
}
OutPlayerFit[Current] = 1u;
if (OutPlayerFitSupportHeights != nullptr)
{
(*OutPlayerFitSupportHeights)[Current] = SupportHeight;
}
++NumPlayerFitInSlice;
}
}
PlayerFitCounts[Z] = NumPlayerFitInSlice;
});
// Fixed-index reduction is intentional: even though the terms are integral, keeping the
// reduction serial documents the determinism rule shared by the floating metrics below.
for (int32 Z = 1; Z < Grid.NumZ; ++Z)
{
OutNumPlayerFitCells += PlayerFitCounts[Z];
}
return true;
}
float MinimumPlayerClearanceChebyshev(
const FSampleGrid& Grid,
const TArray<uint8>& PlayerFit)
{
if (PlayerFit.Num() != Grid.CellCount)
{
return 0.0f;
}
const int32 PlaneCells = Grid.NumX * Grid.NumY;
constexpr int32 Infinity = INT32_MAX / 4;
TArray<int32> PerSliceMinimum;
PerSliceMinimum.Init(Infinity, Grid.NumZ);
// Each horizontal distance transform is independent. Keep its mutable raster in the
// worker and publish one integer result per Z slice; the final minimum is reduced in
// ascending Z order for an explicit deterministic reduction.
ParallelFor(Grid.NumZ, [&](int32 Z)
{
TArray<int32> Distances;
Distances.SetNumUninitialized(PlaneCells);
for (int32 Y = 0; Y < Grid.NumY; ++Y)
{
for (int32 X = 0; X < Grid.NumX; ++X)
{
const int32 PlaneIndex = X + Grid.NumX * Y;
Distances[PlaneIndex] = Grid.Air[Grid.Index(X, Y, Z)] == 0u
? 0 : Infinity;
}
}
auto Relax = [&](int32 Current, int32 Neighbor)
{
if (Distances[Neighbor] < Infinity)
{
Distances[Current] = FMath::Min(
Distances[Current], Distances[Neighbor] + 1);
}
};
// Two raster passes with the complete 8-neighbour stencil give a true 2D
// Chebyshev distance to solid in this horizontal slice. It is used only for the
// reported minimum; capsule occupancy above is the direct stencil check.
for (int32 Y = 0; Y < Grid.NumY; ++Y)
{
for (int32 X = 0; X < Grid.NumX; ++X)
{
const int32 Current = X + Grid.NumX * Y;
if (Grid.Air[Grid.Index(X, Y, Z)] == 0u) continue;
if (X > 0) Relax(Current, Current - 1);
if (Y > 0)
{
Relax(Current, Current - Grid.NumX);
if (X > 0) Relax(Current, Current - Grid.NumX - 1);
if (X + 1 < Grid.NumX) Relax(Current, Current - Grid.NumX + 1);
}
}
}
for (int32 Y = Grid.NumY - 1; Y >= 0; --Y)
{
for (int32 X = Grid.NumX - 1; X >= 0; --X)
{
const int32 Current = X + Grid.NumX * Y;
if (Grid.Air[Grid.Index(X, Y, Z)] == 0u) continue;
if (X + 1 < Grid.NumX) Relax(Current, Current + 1);
if (Y + 1 < Grid.NumY)
{
Relax(Current, Current + Grid.NumX);
if (X > 0) Relax(Current, Current + Grid.NumX - 1);
if (X + 1 < Grid.NumX) Relax(Current, Current + Grid.NumX + 1);
}
}
}
int32 SliceMinimum = Infinity;
for (int32 Y = 0; Y < Grid.NumY; ++Y)
{
for (int32 X = 0; X < Grid.NumX; ++X)
{
const int32 Cell = Grid.Index(X, Y, Z);
if (PlayerFit[Cell] != 0u)
{
SliceMinimum = FMath::Min(
SliceMinimum, Distances[X + Grid.NumX * Y]);
}
}
}
PerSliceMinimum[Z] = SliceMinimum;
});
int32 MinimumDistance = Infinity;
for (int32 Z = 0; Z < Grid.NumZ; ++Z)
{
MinimumDistance = FMath::Min(MinimumDistance, PerSliceMinimum[Z]);
}
return MinimumDistance == Infinity
? 0.0f : static_cast<float>(MinimumDistance);
}
int64 CountAirCells(
const FSampleGrid& Grid,
const TArray<uint8>* EligibilityMask = nullptr)
{
TArray<int64> AirCountsByZ;
AirCountsByZ.Init(0, Grid.NumZ);
ParallelFor(Grid.NumZ, [&](int32 Z)
{
int64 Count = 0;
for (int32 Y = 0; Y < Grid.NumY; ++Y)
{
for (int32 X = 0; X < Grid.NumX; ++X)
{
const int32 Cell = Grid.Index(X, Y, Z);
Count += Grid.Air[Cell] != 0u
&& (EligibilityMask == nullptr || (*EligibilityMask)[Cell] != 0u)
? 1
: 0;
}
}
AirCountsByZ[Z] = Count;
});
int64 Total = 0;
for (int32 Z = 0; Z < Grid.NumZ; ++Z)
{
Total += AirCountsByZ[Z];
}
return Total;
}
void BuildPlayerFitMetrics(
const FSampleGrid& Grid,
const FVoxelStrateMeasureSettings& Settings,
FVoxelStrateMetrics& InOutMetrics,
TArray<uint8>* OutPlayerFitMask = nullptr,
TArray<float>* OutPlayerFitSupportHeights = nullptr)
{
InOutMetrics.bPlayerFitResolved = false;
InOutMetrics.PlayerFitRefusalReason.Reset();
InOutMetrics.NumPlayerFitCells = 0;
InOutMetrics.PlayerFitFraction = 0.0f;
InOutMetrics.NumTraversableComponents = 0;
InOutMetrics.LargestTraversableComponentCells = 0;
InOutMetrics.TraversableComponentShare = 0.0f;
InOutMetrics.MinimumPlayerClearanceVoxels = 0.0f;
TArray<uint8> PlayerFit;
TArray<float> PlayerFitSupportHeights;
if (!BuildPlayerFitMask(
Grid, Settings, PlayerFit, InOutMetrics.NumPlayerFitCells,
InOutMetrics.PlayerFitRefusalReason,
&PlayerFitSupportHeights))
{
if (OutPlayerFitMask != nullptr)
{
OutPlayerFitMask->Reset();
}
return;
}
InOutMetrics.bPlayerFitResolved = true;
InOutMetrics.PlayerFitFraction = InOutMetrics.NumAir > 0
? static_cast<float>(static_cast<double>(InOutMetrics.NumPlayerFitCells)
/ static_cast<double>(InOutMetrics.NumAir))
: 0.0f;
TArray<int32> Components;
int32 NumComponents = 0;
int64 LargestComponentCells = 0;
int32 LargestComponentLowestCell = INDEX_NONE;
int32 NumComponentsAtLeast1Pct = 0;
FloodFillAir(
Grid,
Components,
NumComponents,
InOutMetrics.NumPlayerFitCells,
LargestComponentCells,
LargestComponentLowestCell,
NumComponentsAtLeast1Pct,
nullptr,
&PlayerFit,
&PlayerFitSupportHeights,
&Settings,
EPlayerTraversalPolicy::WalkOnly);
InOutMetrics.NumTraversableComponents = NumComponents;
InOutMetrics.LargestTraversableComponentCells = LargestComponentCells;
InOutMetrics.TraversableComponentShare = InOutMetrics.NumPlayerFitCells > 0
? static_cast<float>(static_cast<double>(LargestComponentCells)
/ static_cast<double>(InOutMetrics.NumPlayerFitCells))
: 0.0f;
InOutMetrics.MinimumPlayerClearanceVoxels = MinimumPlayerClearanceChebyshev(
Grid, PlayerFit);
if (OutPlayerFitMask != nullptr)
{
*OutPlayerFitMask = MoveTemp(PlayerFit);
}
if (OutPlayerFitSupportHeights != nullptr)
{
*OutPlayerFitSupportHeights = MoveTemp(PlayerFitSupportHeights);
}
}
void BuildPlayerFitConnectivityMetrics(
const FSampleGrid& Grid,
const FVoxelStrateMeasureSettings& Settings,
FVoxelStrateMetrics& OutMetrics,
TArray<uint8>& OutPlayerFitMask,
TArray<float>& OutPlayerFitSupportHeights)
{
OutMetrics = FVoxelStrateMetrics();
OutMetrics.ResolvedMarginVoxels = Grid.ResolvedMarginVoxels;
OutMetrics.SampledMinZ = Grid.SampledMinZ;
OutMetrics.SampledMaxZ = Grid.SampledMaxZ;
OutMetrics.SampledNumX = Grid.NumX;
OutMetrics.SampledNumY = Grid.NumY;
OutMetrics.SampledNumZ = Grid.NumZ;
OutMetrics.SampledMinX = Grid.MinX;
OutMetrics.SampledMaxX = Grid.MaxX;
OutMetrics.SampledMinY = Grid.MinY;
OutMetrics.SampledMaxY = Grid.MaxY;
OutMetrics.NumSampled = Grid.CellCount;
OutMetrics.NumAir = CountAirCells(Grid);
OutMetrics.NumSolid = static_cast<int64>(Grid.CellCount) - OutMetrics.NumAir;
OutMetrics.AirFraction = OutMetrics.NumSampled > 0
? static_cast<float>(static_cast<double>(OutMetrics.NumAir)
/ static_cast<double>(OutMetrics.NumSampled))
: 0.0f;
BuildPlayerFitMetrics(
Grid, Settings, OutMetrics, &OutPlayerFitMask,
&OutPlayerFitSupportHeights);
OutMetrics.bValid = OutMetrics.NumSampled > 0;
}
float MedianFloat(TArray<float>& Values)
{
if (Values.Num() == 0) return 0.0f;
Values.Sort();
const int32 Middle = Values.Num() / 2;
if ((Values.Num() & 1) != 0)
{
return Values[Middle];
}
return (Values[Middle - 1] + Values[Middle]) * 0.5f;
}
void BuildMetricsFromGrid(
const FSampleGrid& Grid,
const FVoxelStrateMeasureSettings& Settings,
FVoxelStrateMetrics& InOutMetrics,
TArray<uint8>* OutPlayerFitMask = nullptr)
{
TArray<int32> Components;
int32 NumComponents = 0;
const int64 NumAir = CountAirCells(Grid);
int64 LargestComponentCells = 0;
int32 LargestComponentLowestCell = INDEX_NONE;
int32 NumComponentsAtLeast1Pct = 0;
TArray<int64> ComponentCells;
FloodFillAir(
Grid,
Components,
NumComponents,
NumAir,
LargestComponentCells,
LargestComponentLowestCell,
NumComponentsAtLeast1Pct,
&ComponentCells);
InOutMetrics.NumSampled = Grid.CellCount;
InOutMetrics.NumAir = NumAir;
InOutMetrics.NumSolid = static_cast<int64>(Grid.CellCount) - NumAir;
InOutMetrics.AirFraction = static_cast<float>(
static_cast<double>(NumAir) / static_cast<double>(Grid.CellCount));
InOutMetrics.NumAirComponents = NumComponents;
InOutMetrics.AirComponentCells = MoveTemp(ComponentCells);
InOutMetrics.LargestComponentCells = LargestComponentCells;
InOutMetrics.NumComponentsAtLeast1Pct = NumComponentsAtLeast1Pct;
InOutMetrics.LargestComponentShare = NumAir > 0
? static_cast<float>(static_cast<double>(LargestComponentCells)
/ static_cast<double>(NumAir))
: 0.0f;
if (NumAir > 0 && LargestComponentLowestCell != INDEX_NONE)
{
int32 LargestX = 0;
int32 LargestY = 0;
int32 LargestZ = 0;
DecodeIndex(Grid, LargestComponentLowestCell, LargestX, LargestY, LargestZ);
InOutMetrics.LargestComponentPoint = Grid.CellCenter(LargestX, LargestY, LargestZ);
}
TArray<int32> ClearanceValues;
ClearanceValues.Reserve(static_cast<int32>(NumAir));
int64 NumWalkable = 0;
const int64 XYColumnCount64 = static_cast<int64>(Grid.NumX)
* static_cast<int64>(Grid.NumY);
TArray<uint8> WalkableColumns;
WalkableColumns.Init(0u, static_cast<int32>(XYColumnCount64));
// Candidate poses read only the immutable sampled field. Keep every worker's clearance
// values and compact column flags private, then merge by fixed Z order so the median and
// projected surface graph remain deterministic.
TArray<int64> WalkableCountsByZ;
WalkableCountsByZ.Init(0, Grid.NumZ);
TArray<TArray<int32>> ClearanceValuesByZ;
ClearanceValuesByZ.SetNum(Grid.NumZ);
TArray<TArray<uint8>> WalkableColumnsByZ;
WalkableColumnsByZ.SetNum(Grid.NumZ);
ParallelFor(Grid.NumZ, [&](int32 Z)
{
TArray<int32>& SliceClearanceValues = ClearanceValuesByZ[Z];
TArray<uint8>& SliceWalkableColumns = WalkableColumnsByZ[Z];
SliceWalkableColumns.Init(0u, static_cast<int32>(XYColumnCount64));
int64 SliceWalkableCount = 0;
for (int32 Y = 0; Y < Grid.NumY; ++Y)
{
for (int32 X = 0; X < Grid.NumX; ++X)
{
const int32 Current = Grid.Index(X, Y, Z);
if (Grid.Air[Current] == 0u || Z == 0
|| Grid.Air[Grid.Index(X, Y, Z - 1)] != 0u)
{
continue;
}
bool bHasHeadroom = true;
for (int32 H = 0; H < Settings.HeadroomCells; ++H)
{
const int64 HeadroomZ = static_cast<int64>(Z) + H;
if (HeadroomZ >= Grid.NumZ
|| Grid.Air[Grid.Index(X, Y, static_cast<int32>(HeadroomZ))] == 0u)
{
bHasHeadroom = false;
break;
}
}
if (!bHasHeadroom) continue;
++SliceWalkableCount;
SliceWalkableColumns[X + Grid.NumX * Y] = 1u;
int32 ClearanceCells = 0;
while (static_cast<int64>(Z) + ClearanceCells < Grid.NumZ
&& Grid.Air[Grid.Index(X, Y, Z + ClearanceCells)] != 0u)
{
++ClearanceCells;
}
const int64 ClearanceVoxels = static_cast<int64>(ClearanceCells)
* Settings.SampleStep;
SliceClearanceValues.Add(ClearanceVoxels > INT32_MAX
? INT32_MAX : static_cast<int32>(ClearanceVoxels));
}
}
WalkableCountsByZ[Z] = SliceWalkableCount;
});
for (int32 Z = 0; Z < Grid.NumZ; ++Z)
{
NumWalkable += WalkableCountsByZ[Z];
const TArray<uint8>& SliceWalkableColumns = WalkableColumnsByZ[Z];
for (int32 Column = 0; Column < SliceWalkableColumns.Num(); ++Column)
{
if (SliceWalkableColumns[Column] != 0u)
{
WalkableColumns[Column] = 1u;
}
}
ClearanceValues.Append(ClearanceValuesByZ[Z]);
}
InOutMetrics.WalkableFraction = NumAir > 0
? static_cast<float>(static_cast<double>(NumWalkable)
/ static_cast<double>(NumAir))
: 0.0f;
for (const uint8 bWalkable : WalkableColumns)
{
InOutMetrics.WalkableFloorColumns += bWalkable != 0u ? 1 : 0;
}
InOutMetrics.WalkableFloorAreaFraction = XYColumnCount64 > 0
? static_cast<float>(static_cast<double>(InOutMetrics.WalkableFloorColumns)
/ static_cast<double>(XYColumnCount64))
: 0.0f;
// The area graph is intentionally projected to XY: each column contributes one floor
// footprint even when several sampled Z cells in it meet the walkability predicate.
// Keep the flood fill deterministic and bounded by the already bounded XY grid.
TArray<uint8> VisitedWalkableColumns;
VisitedWalkableColumns.Init(0u, static_cast<int32>(XYColumnCount64));
TArray<int32> SurfaceQueue;
SurfaceQueue.Reserve(static_cast<int32>(XYColumnCount64));
for (int32 Column = 0; Column < WalkableColumns.Num(); ++Column)
{
if (WalkableColumns[Column] == 0u || VisitedWalkableColumns[Column] != 0u)
{
continue;
}
++InOutMetrics.NumWalkableSurfaceComponents;
VisitedWalkableColumns[Column] = 1u;
SurfaceQueue.Reset();
SurfaceQueue.Add(Column);
int64 ComponentColumns = 0;
for (int32 QueueIndex = 0; QueueIndex < SurfaceQueue.Num(); ++QueueIndex)
{
const int32 CurrentColumn = SurfaceQueue[QueueIndex];
++ComponentColumns;
const int32 CurrentX = CurrentColumn % Grid.NumX;
const int32 CurrentY = CurrentColumn / Grid.NumX;
const int32 NeighbourColumns[4] = {
CurrentX > 0 ? CurrentColumn - 1 : INDEX_NONE,
CurrentX + 1 < Grid.NumX ? CurrentColumn + 1 : INDEX_NONE,
CurrentY > 0 ? CurrentColumn - Grid.NumX : INDEX_NONE,
CurrentY + 1 < Grid.NumY ? CurrentColumn + Grid.NumX : INDEX_NONE
};
for (const int32 Neighbour : NeighbourColumns)
{
if (Neighbour != INDEX_NONE
&& WalkableColumns[Neighbour] != 0u
&& VisitedWalkableColumns[Neighbour] == 0u)
{
VisitedWalkableColumns[Neighbour] = 1u;
SurfaceQueue.Add(Neighbour);
}
}
}
InOutMetrics.LargestWalkableSurfaceColumns = FMath::Max(
InOutMetrics.LargestWalkableSurfaceColumns, ComponentColumns);
}
InOutMetrics.LargestWalkableSurfaceShare = InOutMetrics.WalkableFloorColumns > 0
? static_cast<float>(static_cast<double>(InOutMetrics.LargestWalkableSurfaceColumns)
/ static_cast<double>(InOutMetrics.WalkableFloorColumns))
: 0.0f;
if (ClearanceValues.Num() > 0)
{
ClearanceValues.Sort();
// The public metric is integral, so use the lower median for an even sample count.
InOutMetrics.MedianVerticalClearance = ClearanceValues[(ClearanceValues.Num() - 1) / 2];
}
if (InOutMetrics.NumSolid > 0 && NumAir > 0)
{
TArray<int32> Distances;
BuildManhattanDistanceToSolid(Grid, Distances);
TArray<TArray<float>> FeatureScalesByZ;
FeatureScalesByZ.SetNum(Grid.NumZ);
ParallelFor(Grid.NumZ, [&](int32 Z)
{
TArray<float>& SliceFeatureScales = FeatureScalesByZ[Z];
for (int32 Y = 0; Y < Grid.NumY; ++Y)
{
for (int32 X = 0; X < Grid.NumX; ++X)
{
const int32 Index = Grid.Index(X, Y, Z);
if (Grid.Air[Index] != 0u)
{
SliceFeatureScales.Add(static_cast<float>(Distances[Index])
* static_cast<float>(Settings.SampleStep));
}
}
}
});
TArray<float> FeatureScales;
FeatureScales.Reserve(static_cast<int32>(NumAir));
for (int32 Z = 0; Z < Grid.NumZ; ++Z)
{
FeatureScales.Append(FeatureScalesByZ[Z]);
}
InOutMetrics.MedianFeatureScale = MedianFloat(FeatureScales);
}
// This is intentionally a separate fine-resolution pass. The legacy metrics above may
// be useful at SampleStep=4/8, but they cannot answer whether the confirmed player capsule
// fits. A coarse grid therefore receives an explicit refusal instead of an estimate.
BuildPlayerFitMetrics(Grid, Settings, InOutMetrics, OutPlayerFitMask);
}
bool FindCellForPoint(const FSampleGrid& Grid, const FVector& Point, int32& OutCell)
{
if (!Grid.ContainsPoint(Point)) return false;
const float Step = static_cast<float>(Grid.SampleStep);
const int32 X = FMath::Clamp(FMath::FloorToInt((Point.X - Grid.MinX) / Step), 0, Grid.NumX - 1);
const int32 Y = FMath::Clamp(FMath::FloorToInt((Point.Y - Grid.MinY) / Step), 0, Grid.NumY - 1);
const int32 Z = FMath::Clamp(FMath::FloorToInt((Point.Z - Grid.MinZ) / Step), 0, Grid.NumZ - 1);
OutCell = Grid.Index(X, Y, Z);
return true;
}
enum class EEndpointCellResult : uint8
{
OutOfWindow,
Air,
Solid,
NotPlayerFit
};
EEndpointCellResult ResolveEndpointCell(
const FSampleGrid& Grid,
const FVector& Point,
int32& OutCell,
bool& bOutSnapped,
const TArray<uint8>* EligibilityMask = nullptr)
{
bOutSnapped = false;
int32 OriginalCell = -1;
if (!FindCellForPoint(Grid, Point, OriginalCell))
{
return EEndpointCellResult::OutOfWindow;
}
const auto IsEligible = [&, EligibilityMask](int32 Cell)
{
return Grid.Air[Cell] != 0u
&& (EligibilityMask == nullptr || (*EligibilityMask)[Cell] != 0u);
};
if (IsEligible(OriginalCell))
{
OutCell = OriginalCell;
return EEndpointCellResult::Air;
}
int32 OriginalX = 0;
int32 OriginalY = 0;
int32 OriginalZ = 0;
DecodeIndex(Grid, OriginalCell, OriginalX, OriginalY, OriginalZ);
int32 BestCell = INDEX_NONE;
float BestDistanceSquared = FLT_MAX;
const int32 MaxX = Grid.NumX - 1;
const int32 MaxY = Grid.NumY - 1;
const int32 MaxZ = Grid.NumZ - 1;
const int32 MinX = OriginalX > 0 ? OriginalX - 1 : 0;
const int32 MinY = OriginalY > 0 ? OriginalY - 1 : 0;
const int32 MinZ = OriginalZ > 0 ? OriginalZ - 1 : 0;
const int32 CandidateMaxX = OriginalX < MaxX ? OriginalX + 1 : MaxX;
const int32 CandidateMaxY = OriginalY < MaxY ? OriginalY + 1 : MaxY;
const int32 CandidateMaxZ = OriginalZ < MaxZ ? OriginalZ + 1 : MaxZ;
for (int32 Z = MinZ;
Z <= CandidateMaxZ;
++Z)
{
for (int32 Y = MinY;
Y <= CandidateMaxY;
++Y)
{
for (int32 X = MinX;
X <= CandidateMaxX;
++X)
{
if (X == OriginalX && Y == OriginalY && Z == OriginalZ)
{
continue;
}
const int32 Candidate = Grid.Index(X, Y, Z);
if (!IsEligible(Candidate))
{
continue;
}
const float DistanceSquared = FVector::DistSquared(
Point, Grid.CellCenter(X, Y, Z));
if (DistanceSquared < BestDistanceSquared
|| (DistanceSquared == BestDistanceSquared
&& (BestCell == INDEX_NONE || Candidate < BestCell)))
{
BestDistanceSquared = DistanceSquared;
BestCell = Candidate;
}
}
}
}
if (BestCell == INDEX_NONE)
{
if (EligibilityMask != nullptr)
{
// Passage mouth points are authored on the centreline of a carved connector, not
// necessarily on the floor-anchor lattice used by PlayerFit. Resolve them to the
// nearest player-fitting floor cell over the bounded fine grid, rather than
// declaring an in-air mouth unreachable just because its floor is several cells
// below. The endpoint is reported as snapped, and the route re-check below still
// validates the endpoint-to-anchor segment against density.
for (int32 Candidate = 0; Candidate < Grid.CellCount; ++Candidate)
{
if ((*EligibilityMask)[Candidate] == 0u)
{
continue;
}
int32 CandidateX = 0;
int32 CandidateY = 0;
int32 CandidateZ = 0;
DecodeIndex(Grid, Candidate, CandidateX, CandidateY, CandidateZ);
const float DistanceSquared = FVector::DistSquared(
Point, Grid.CellCenter(CandidateX, CandidateY, CandidateZ));
if (DistanceSquared < BestDistanceSquared
|| (DistanceSquared == BestDistanceSquared
&& (BestCell == INDEX_NONE || Candidate < BestCell)))
{
BestDistanceSquared = DistanceSquared;
BestCell = Candidate;
}
}
}
}
if (BestCell == INDEX_NONE)
{
return EligibilityMask != nullptr
? EEndpointCellResult::NotPlayerFit
: EEndpointCellResult::Solid;
}
OutCell = BestCell;
bOutSnapped = true;
return EEndpointCellResult::Air;
}
struct FCoarseEdge
{
int32 First = INDEX_NONE;
int32 Second = INDEX_NONE;
bool IsValid() const
{
return First >= 0 && Second >= 0 && First != Second;
}
};
FCoarseEdge MakeCoarseEdge(int32 A, int32 B)
{
FCoarseEdge Edge;
Edge.First = FMath::Min(A, B);
Edge.Second = FMath::Max(A, B);
return Edge;
}
bool IsCoarseEdgeBlocked(
const TArray<FCoarseEdge>& BlockedEdges,
int32 A,
int32 B)
{
const FCoarseEdge Candidate = MakeCoarseEdge(A, B);
for (const FCoarseEdge& Blocked : BlockedEdges)
{
if (Blocked.First == Candidate.First && Blocked.Second == Candidate.Second)
{
return true;
}
}
return false;
}
bool FullResolutionAirOnSegment(
const UVoxelGenerator* Generator,
const IVoxelStrateDensitySampler* Sampler,
const FVector& Start,
const FVector& End)
{
const FVector Delta = End - Start;
const float MaxDelta = FMath::Max3(
FMath::Abs(Delta.X), FMath::Abs(Delta.Y), FMath::Abs(Delta.Z));
const int32 NumSteps = FMath::Max(1, FMath::CeilToInt(MaxDelta));
for (int32 Step = 0; Step <= NumSteps; ++Step)
{
const float Alpha = static_cast<float>(Step) / static_cast<float>(NumSteps);
const FVector Sample = Start + Delta * Alpha;
const float Density = Sampler != nullptr
? Sampler->SampleDensity(Sample.X, Sample.Y, Sample.Z)
: Generator->GetDensityAt(Sample.X, Sample.Y, Sample.Z);
if (!FMath::IsFinite(Density) || !(Density > 0.0f))
{
return false;
}
}
return true;
}
bool FullResolutionPathIsAir(
const UVoxelGenerator* Generator,
const IVoxelStrateDensitySampler* Sampler,
const FSampleGrid& Grid,
const FVector& A,
const FVector& B,
const TArray<int32>& Path,
FCoarseEdge& OutFailedEdge,
const TArray<uint8>* EligibilityMask = nullptr,
const TArray<float>* PlayerFitSupportHeights = nullptr,
const FVoxelStrateMeasureSettings* TraversalSettings = nullptr,
EPlayerTraversalPolicy TraversalPolicy = EPlayerTraversalPolicy::WalkAndClimb)
{
OutFailedEdge = FCoarseEdge();
if (EligibilityMask != nullptr)
{
// A player-fit route is deliberately a graph over supported free poses, not a second
// air route with a coarse-cell illusion. The mask was built at SampleStep=1 and every
// edge below is between adjacent player-fitting cells.
for (int32 PathIndex = 0; PathIndex < Path.Num(); ++PathIndex)
{
if (!EligibilityMask->IsValidIndex(Path[PathIndex])
|| (*EligibilityMask)[Path[PathIndex]] == 0u)
{
if (PathIndex > 0)
{
OutFailedEdge = MakeCoarseEdge(
Path[PathIndex - 1], Path[PathIndex]);
}
return false;
}
if (PathIndex > 0
&& PlayerFitSupportHeights != nullptr
&& TraversalSettings != nullptr)
{
bool bClimb = false;
if (!IsPlayerTraversalEdge(
Grid, *EligibilityMask, PlayerFitSupportHeights,
TraversalSettings, Path[PathIndex - 1], Path[PathIndex], bClimb,
TraversalPolicy))
{
OutFailedEdge = MakeCoarseEdge(
Path[PathIndex - 1], Path[PathIndex]);
return false;
}
}
}
int32 StartX = 0;
int32 StartY = 0;
int32 StartZ = 0;
DecodeIndex(Grid, Path[0], StartX, StartY, StartZ);
int32 EndX = 0;
int32 EndY = 0;
int32 EndZ = 0;
DecodeIndex(Grid, Path.Last(), EndX, EndY, EndZ);
if (!FullResolutionAirOnSegment(
Generator, Sampler, A, Grid.CellCenter(StartX, StartY, StartZ))
|| !FullResolutionAirOnSegment(
Generator, Sampler, Grid.CellCenter(EndX, EndY, EndZ), B))
{
// Endpoint approaches have no graph edge to exclude. The player-fit route is
// therefore unknown if either mouth-to-anchor segment contains solid density.
return false;
}
return true;
}
int32 StartX = 0;
int32 StartY = 0;
int32 StartZ = 0;
DecodeIndex(Grid, Path[0], StartX, StartY, StartZ);
int32 EndX = 0;
int32 EndY = 0;
int32 EndZ = 0;
DecodeIndex(Grid, Path.Last(), EndX, EndY, EndZ);
if (!FullResolutionAirOnSegment(Generator, Sampler, A, Grid.CellCenter(StartX, StartY, StartZ)))
{
// The endpoint-to-cell-center segment has no coarse cell pair to exclude. It is
// common to every route from this endpoint, so report unknown rather than claiming
// that the pair is disconnected at this resolution.
return false;
}
for (int32 PathIndex = 1; PathIndex < Path.Num(); ++PathIndex)
{
int32 PreviousX = 0;
int32 PreviousY = 0;
int32 PreviousZ = 0;
DecodeIndex(Grid, Path[PathIndex - 1], PreviousX, PreviousY, PreviousZ);
int32 CurrentX = 0;
int32 CurrentY = 0;
int32 CurrentZ = 0;
DecodeIndex(Grid, Path[PathIndex], CurrentX, CurrentY, CurrentZ);
if (!FullResolutionAirOnSegment(
Generator, Sampler,
Grid.CellCenter(PreviousX, PreviousY, PreviousZ),
Grid.CellCenter(CurrentX, CurrentY, CurrentZ)))
{
OutFailedEdge = MakeCoarseEdge(Path[PathIndex - 1], Path[PathIndex]);
return false;
}
}
if (!FullResolutionAirOnSegment(Generator, Sampler, Grid.CellCenter(EndX, EndY, EndZ), B))
{
// As above, the final endpoint segment is shared by every route that reaches the
// goal cell and therefore has no precise coarse edge to exclude.
return false;
}
return true;
}
bool FindCoarsePath(
const FSampleGrid& Grid,
int32 Start,
int32 Goal,
const TArray<FCoarseEdge>& BlockedEdges,
TArray<int32>& OutPath,
const TArray<uint8>* EligibilityMask = nullptr,
const TArray<float>* PlayerFitSupportHeights = nullptr,
const FVoxelStrateMeasureSettings* TraversalSettings = nullptr,
EPlayerTraversalPolicy TraversalPolicy = EPlayerTraversalPolicy::WalkAndClimb)
{
const auto IsEligible = [&, EligibilityMask](int32 Cell)
{
return Grid.Air[Cell] != 0u
&& (EligibilityMask == nullptr || (*EligibilityMask)[Cell] != 0u);
};
if (!IsEligible(Start) || !IsEligible(Goal))
{
return false;
}
TArray<int32> Parent;
Parent.SetNumUninitialized(Grid.CellCount);
for (int32 Index = 0; Index < Grid.CellCount; ++Index)
{
Parent[Index] = -2; // unvisited; -1 is the root's parent
}
TArray<int32> Queue;
Queue.SetNumUninitialized(Grid.CellCount);
int32 Head = 0;
int32 Tail = 0;
Parent[Start] = -1;
Queue[Tail++] = Start;
while (Head < Tail && Parent[Goal] == -2)
{
const int32 Current = Queue[Head++];
int32 X = 0;
int32 Y = 0;
int32 Z = 0;
DecodeIndex(Grid, Current, X, Y, Z);
auto Visit = [&](int32 Neighbor)
{
if (IsEligible(Neighbor)
&& Parent[Neighbor] == -2
&& !IsCoarseEdgeBlocked(BlockedEdges, Current, Neighbor))
{
Parent[Neighbor] = Current;
Queue[Tail++] = Neighbor;
}
};
if (EligibilityMask != nullptr
&& PlayerFitSupportHeights != nullptr
&& TraversalSettings != nullptr)
{
TArray<FPlayerTraversalNeighbour, TInlineAllocator<128>> Neighbours;
AppendPlayerTraversalNeighbours(
Grid, *EligibilityMask, PlayerFitSupportHeights,
TraversalSettings, Current, Neighbours, TraversalPolicy);
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));
if (X > 0) Visit(Grid.Index(X - 1, Y, Z));
if (Y + 1 < Grid.NumY) Visit(Grid.Index(X, Y + 1, Z));
if (Y > 0) Visit(Grid.Index(X, Y - 1, Z));
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)
{
return false;
}
OutPath.Reset();
OutPath.Reserve(Tail);
for (int32 Current = Goal; Current >= 0; Current = Parent[Current])
{
OutPath.Add(Current);
if (Current == Start) break;
}
if (OutPath.Last() != Start)
{
OutPath.Reset();
return false;
}
for (int32 Left = 0, Right = OutPath.Num() - 1; Left < Right; ++Left, --Right)
{
Swap(OutPath[Left], OutPath[Right]);
}
return true;
}
EVoxelConnectivityResult EvaluateConnectivityOnGrid(
const UVoxelGenerator* Generator,
const IVoxelStrateDensitySampler* Sampler,
const FSampleGrid& Grid,
const FVector& AVoxel,
const FVector& BVoxel,
int32& OutStart,
int32& OutGoal,
bool& bOutStartSnapped,
bool& bOutGoalSnapped,
int32& OutNumRouteRetries,
int32 MaxRouteRetries,
const TArray<uint8>* EligibilityMask = nullptr,
const TArray<float>* PlayerFitSupportHeights = nullptr,
const FVoxelStrateMeasureSettings* TraversalSettings = nullptr,
EPlayerTraversalPolicy TraversalPolicy = EPlayerTraversalPolicy::WalkAndClimb)
{
OutStart = -1;
OutGoal = -1;
bOutStartSnapped = false;
bOutGoalSnapped = false;
OutNumRouteRetries = 0;
const EEndpointCellResult StartCell = ResolveEndpointCell(
Grid, AVoxel, OutStart, bOutStartSnapped, EligibilityMask);
const EEndpointCellResult GoalCell = ResolveEndpointCell(
Grid, BVoxel, OutGoal, bOutGoalSnapped, EligibilityMask);
if (StartCell == EEndpointCellResult::OutOfWindow
|| GoalCell == EEndpointCellResult::OutOfWindow)
{
return EVoxelConnectivityResult::OutOfWindow;
}
if (StartCell == EEndpointCellResult::Solid)
{
return EVoxelConnectivityResult::StartCellSolid;
}
if (StartCell == EEndpointCellResult::NotPlayerFit)
{
return EVoxelConnectivityResult::StartCellNotPlayerFit;
}
if (GoalCell == EEndpointCellResult::Solid)
{
return EVoxelConnectivityResult::GoalCellSolid;
}
if (GoalCell == EEndpointCellResult::NotPlayerFit)
{
return EVoxelConnectivityResult::GoalCellNotPlayerFit;
}
TArray<FCoarseEdge> BlockedEdges;
TArray<int32> Path;
for (;;)
{
if (!FindCoarsePath(
Grid, OutStart, OutGoal, BlockedEdges, Path, EligibilityMask,
PlayerFitSupportHeights, TraversalSettings, TraversalPolicy))
{
// This is a negative verdict only after all edges excluded by earlier
// full-resolution failures leave no coarse route at this resolution.
return EVoxelConnectivityResult::NotConnectedAtThisResolution;
}
// The coarse route is evidence, not a pass. Re-check every candidate at one-voxel
// spacing in the original voxel field. A single solid sample refutes only this
// route, so exclude its precise coarse edge and search again.
FCoarseEdge FailedEdge;
if (FullResolutionPathIsAir(
Generator, Sampler, Grid, AVoxel, BVoxel, Path, FailedEdge,
EligibilityMask, PlayerFitSupportHeights, TraversalSettings, TraversalPolicy))
{
return EVoxelConnectivityResult::Connected;
}
if (OutNumRouteRetries >= MaxRouteRetries || !FailedEdge.IsValid())
{
// A route can be refuted without a retryable pair when the failure is in an
// endpoint-to-cell segment (or when the explicit retry budget is spent). Both
// cases are unknown, never evidence of disconnection.
return EVoxelConnectivityResult::CoarseLiedBudgetExhausted;
}
check(FailedEdge.First != FailedEdge.Second);
check(!IsCoarseEdgeBlocked(
BlockedEdges, FailedEdge.First, FailedEdge.Second));
BlockedEdges.Add(FailedEdge);
++OutNumRouteRetries;
}
}
float DistanceToComponent(
const FSampleGrid& Grid,
int32 StartCell,
int32 TargetComponent,
const TArray<int32>& Components)
{
if (Components[StartCell] == TargetComponent)
{
return 0.0f;
}
int32 StartX = 0;
int32 StartY = 0;
int32 StartZ = 0;
DecodeIndex(Grid, StartCell, StartX, StartY, StartZ);
int64 BestDistanceSquared = MAX_int64;
for (int32 Index = 0; Index < Grid.CellCount; ++Index)
{
if (Components[Index] != TargetComponent)
{
continue;
}
int32 X = 0;
int32 Y = 0;
int32 Z = 0;
DecodeIndex(Grid, Index, X, Y, Z);
const int64 DX = static_cast<int64>(X) - static_cast<int64>(StartX);
const int64 DY = static_cast<int64>(Y) - static_cast<int64>(StartY);
const int64 DZ = static_cast<int64>(Z) - static_cast<int64>(StartZ);
const int64 DistanceSquared = DX * DX + DY * DY + DZ * DZ;
BestDistanceSquared = FMath::Min(BestDistanceSquared, DistanceSquared);
}
return BestDistanceSquared == MAX_int64
? -1.0f
: FMath::Sqrt(static_cast<float>(BestDistanceSquared));
}
void PopulateConnectivityDiagnostics(
const FSampleGrid& Grid,
int32 Start,
int32 Goal,
EVoxelConnectivityResult ConnectivityResult,
bool bStartSnapped,
bool bGoalSnapped,
int32 NumRouteRetries,
FVoxelConnectivityDiagnostics& OutDiagnostics,
const TArray<uint8>* EligibilityMask = nullptr,
TArray<int32>* OutComponents = nullptr,
int32* OutNumComponents = nullptr,
const TArray<float>* PlayerFitSupportHeights = nullptr,
const FVoxelStrateMeasureSettings* TraversalSettings = nullptr,
EPlayerTraversalPolicy TraversalPolicy = EPlayerTraversalPolicy::WalkAndClimb)
{
OutDiagnostics = FVoxelConnectivityDiagnostics();
if (OutComponents != nullptr)
{
OutComponents->Reset();
}
if (OutNumComponents != nullptr)
{
*OutNumComponents = 0;
}
OutDiagnostics.Result = ConnectivityResult;
OutDiagnostics.bPlayerFitRestricted = EligibilityMask != nullptr;
OutDiagnostics.bPlayerFitResolved = EligibilityMask != nullptr;
OutDiagnostics.bStartSnapped = bStartSnapped;
OutDiagnostics.bGoalSnapped = bGoalSnapped;
OutDiagnostics.NumRouteRetries = NumRouteRetries;
OutDiagnostics.bValid = ConnectivityResult != EVoxelConnectivityResult::OutOfWindow
|| (Start >= 0 && Goal >= 0);
if (Start < 0 || Goal < 0)
{
return;
}
TArray<int32> Components;
int32 NumComponents = 0;
int64 LargestComponentCells = 0;
int32 LargestComponentLowestCell = INDEX_NONE;
int32 NumComponentsAtLeast1Pct = 0;
const int64 NumAir = CountAirCells(Grid, EligibilityMask);
FloodFillAir(
Grid,
Components,
NumComponents,
NumAir,
LargestComponentCells,
LargestComponentLowestCell,
NumComponentsAtLeast1Pct,
nullptr,
EligibilityMask,
PlayerFitSupportHeights,
TraversalSettings,
TraversalPolicy);
if (OutComponents != nullptr)
{
*OutComponents = Components;
}
if (OutNumComponents != nullptr)
{
*OutNumComponents = NumComponents;
}
const int32 StartComponent = Components[Start];
const int32 GoalComponent = Components[Goal];
if (StartComponent < 0 || GoalComponent < 0)
{
return;
}
TArray<int32> ComponentCells;
ComponentCells.Init(0, NumComponents);
for (const int32 Component : Components)
{
if (Component >= 0)
{
++ComponentCells[Component];
}
}
OutDiagnostics.NumAirCells = NumAir;
OutDiagnostics.StartComponentCells = ComponentCells[StartComponent];
OutDiagnostics.GoalComponentCells = ComponentCells[GoalComponent];
OutDiagnostics.LargestComponentCells = LargestComponentCells;
if (NumAir > 0)
{
OutDiagnostics.StartComponentShare = static_cast<float>(
static_cast<double>(OutDiagnostics.StartComponentCells)
/ static_cast<double>(NumAir));
OutDiagnostics.GoalComponentShare = static_cast<float>(
static_cast<double>(OutDiagnostics.GoalComponentCells)
/ static_cast<double>(NumAir));
OutDiagnostics.LargestComponentShare = static_cast<float>(
static_cast<double>(LargestComponentCells)
/ static_cast<double>(NumAir));
}
OutDiagnostics.bStartComponentIsLargest =
OutDiagnostics.StartComponentCells == LargestComponentCells;
OutDiagnostics.bGoalComponentIsLargest =
OutDiagnostics.GoalComponentCells == LargestComponentCells;
TArray<float> ComponentDistances;
ComponentDistances.Init(0.0f, 2);
ParallelFor(2, [&](int32 DistanceIndex)
{
ComponentDistances[DistanceIndex] = DistanceIndex == 0
? DistanceToComponent(Grid, Start, GoalComponent, Components)
: DistanceToComponent(Grid, Goal, StartComponent, Components);
});
OutDiagnostics.StartToGoalComponentDistanceCells = ComponentDistances[0];
OutDiagnostics.GoalToStartComponentDistanceCells = ComponentDistances[1];
}
// The hard capability graph is deliberately flooded with walking edges only. This second
// pass contracts each walk-only component to one node and adds only the explicitly supported
// climb edges between nodes. It gives the reporting tiers without allowing a climb to turn a
// failed primordial walk-only verdict into a pass.
void BuildClimbTierFromWalkComponents(
const FSampleGrid& Grid,
const TArray<uint8>& PlayerFitMask,
const TArray<float>& PlayerFitSupportHeights,
const FVoxelStrateMeasureSettings& Settings,
const TArray<int32>& Components,
int32 NumComponents,
int32 StartCell,
int32 GoalCell,
int64 PlayerFitCells,
int64& OutWalkOnlyReachableCells,
int64& OutClimbReachableCells,
int64& OutMineRequiredCells,
bool& bOutClimbCanReach,
int64& OutClimbEdges)
{
OutWalkOnlyReachableCells = 0;
OutClimbReachableCells = 0;
OutMineRequiredCells = 0;
bOutClimbCanReach = false;
OutClimbEdges = 0;
if (NumComponents <= 0
|| Components.Num() != Grid.CellCount
|| !Components.IsValidIndex(StartCell)
|| !Components.IsValidIndex(GoalCell))
{
return;
}
const int32 StartComponent = Components[StartCell];
const int32 GoalComponent = Components[GoalCell];
if (StartComponent < 0 || StartComponent >= NumComponents
|| GoalComponent < 0 || GoalComponent >= NumComponents)
{
return;
}
TArray<TArray<int32>> CellsByComponent;
CellsByComponent.SetNum(NumComponents);
for (int32 Cell = 0; Cell < Grid.CellCount; ++Cell)
{
const int32 Component = Components[Cell];
if (Component >= 0 && Component < NumComponents
&& IsEligiblePlayerFitCell(Grid, PlayerFitMask, Cell))
{
CellsByComponent[Component].Add(Cell);
}
}
OutWalkOnlyReachableCells = CellsByComponent[StartComponent].Num();
TArray<uint8> ReachedComponents;
ReachedComponents.Init(0u, NumComponents);
TArray<int32> Queue;
Queue.Reserve(NumComponents);
ReachedComponents[StartComponent] = 1u;
Queue.Add(StartComponent);
for (int32 QueueIndex = 0; QueueIndex < Queue.Num(); ++QueueIndex)
{
const int32 Component = Queue[QueueIndex];
for (const int32 Cell : CellsByComponent[Component])
{
TArray<FPlayerTraversalNeighbour, TInlineAllocator<128>> Neighbours;
AppendPlayerTraversalNeighbours(
Grid,
PlayerFitMask,
&PlayerFitSupportHeights,
&Settings,
Cell,
Neighbours,
EPlayerTraversalPolicy::WalkAndClimb);
for (const FPlayerTraversalNeighbour& Neighbour : Neighbours)
{
if (!Neighbour.bClimb || !Components.IsValidIndex(Neighbour.Cell))
{
continue;
}
++OutClimbEdges;
const int32 NeighbourComponent = Components[Neighbour.Cell];
if (NeighbourComponent >= 0 && NeighbourComponent < NumComponents
&& NeighbourComponent != Component
&& ReachedComponents[NeighbourComponent] == 0u)
{
ReachedComponents[NeighbourComponent] = 1u;
Queue.Add(NeighbourComponent);
}
}
}
}
for (int32 Component = 0; Component < NumComponents; ++Component)
{
if (ReachedComponents[Component] != 0u)
{
OutClimbReachableCells += CellsByComponent[Component].Num();
}
}
bOutClimbCanReach = ReachedComponents[GoalComponent] != 0u;
OutMineRequiredCells = FMath::Max<int64>(
0,
PlayerFitCells - OutClimbReachableCells);
}
EVoxelConnectivityResult QueryConnectivity(
const UVoxelGenerator* Generator,
const UVoxelStrateManager* Manager,
const IVoxelStrateDensitySampler* Sampler,
int32 StrateIndex,
int32 ExplicitBottomWorldZ,
int32 ExplicitTopWorldZ,
float ExplicitBoundarySealThickness,
bool bUseExplicitBounds,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
bool& bOutStartSnapped,
bool& bOutGoalSnapped,
int32& OutNumRouteRetries,
FVoxelConnectivityDiagnostics* OutDiagnostics,
bool bPlayerFitRestricted = false,
FVoxelStrateMetrics* OutPlayerMetrics = nullptr)
{
bOutStartSnapped = false;
bOutGoalSnapped = false;
OutNumRouteRetries = 0;
if (OutPlayerMetrics != nullptr)
{
*OutPlayerMetrics = FVoxelStrateMetrics();
}
auto RefusePlayerFit = [&](const FString& Reason)
{
if (OutDiagnostics != nullptr)
{
*OutDiagnostics = FVoxelConnectivityDiagnostics();
OutDiagnostics->bPlayerFitRestricted = bPlayerFitRestricted;
OutDiagnostics->RefusalReason = Reason;
}
if (OutPlayerMetrics != nullptr)
{
OutPlayerMetrics->RefusalReason = Reason;
OutPlayerMetrics->PlayerFitRefusalReason = Reason;
}
};
if (!FMath::IsFinite(AVoxel.X) || !FMath::IsFinite(AVoxel.Y)
|| !FMath::IsFinite(AVoxel.Z) || !FMath::IsFinite(BVoxel.X)
|| !FMath::IsFinite(BVoxel.Y) || !FMath::IsFinite(BVoxel.Z))
{
if (OutDiagnostics != nullptr)
{
*OutDiagnostics = FVoxelConnectivityDiagnostics();
OutDiagnostics->bPlayerFitRestricted = bPlayerFitRestricted;
}
return EVoxelConnectivityResult::OutOfWindow;
}
if (Settings.MaxRouteRetries < 0)
{
if (OutDiagnostics != nullptr)
{
*OutDiagnostics = FVoxelConnectivityDiagnostics();
OutDiagnostics->bPlayerFitRestricted = bPlayerFitRestricted;
}
return EVoxelConnectivityResult::OutOfWindow;
}
if (bPlayerFitRestricted && Settings.SampleStep != 1)
{
RefusePlayerFit(FString::Printf(
TEXT("Player-fit connectivity refused: SampleStep=%d; exact capsule fit requires SampleStep=1."),
Settings.SampleStep));
return EVoxelConnectivityResult::OutOfWindow;
}
FSampleGrid Grid;
FString RefusalReason;
if (!BuildSampleGrid(Generator, Manager, Sampler, StrateIndex,
ExplicitBottomWorldZ, ExplicitTopWorldZ,
ExplicitBoundarySealThickness, bUseExplicitBounds,
Settings, bPlayerFitRestricted, Grid, RefusalReason))
{
if (OutDiagnostics != nullptr)
{
*OutDiagnostics = FVoxelConnectivityDiagnostics();
OutDiagnostics->bPlayerFitRestricted = bPlayerFitRestricted;
OutDiagnostics->RefusalReason = RefusalReason;
}
if (OutPlayerMetrics != nullptr)
{
OutPlayerMetrics->RefusalReason = RefusalReason;
OutPlayerMetrics->PlayerFitRefusalReason = RefusalReason;
}
return EVoxelConnectivityResult::OutOfWindow;
}
TArray<uint8> PlayerFitMask;
TArray<float> PlayerFitSupportHeights;
FVoxelStrateMetrics PlayerMetrics;
if (bPlayerFitRestricted)
{
// A player-fit query needs only the fine-grid air count plus the direct capsule
// stencil. Do not pay for the legacy all-air flood fill, walkable-column scan, or
// 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, PlayerFitSupportHeights);
if (OutPlayerMetrics != nullptr)
{
*OutPlayerMetrics = PlayerMetrics;
}
if (!PlayerMetrics.bPlayerFitResolved)
{
RefusePlayerFit(PlayerMetrics.PlayerFitRefusalReason);
return EVoxelConnectivityResult::OutOfWindow;
}
}
int32 Start = -1;
int32 Goal = -1;
const EVoxelConnectivityResult Result = EvaluateConnectivityOnGrid(
Generator,
Sampler,
Grid,
AVoxel,
BVoxel,
Start,
Goal,
bOutStartSnapped,
bOutGoalSnapped,
OutNumRouteRetries,
Settings.MaxRouteRetries,
bPlayerFitRestricted ? &PlayerFitMask : nullptr,
bPlayerFitRestricted ? &PlayerFitSupportHeights : nullptr,
bPlayerFitRestricted ? &Settings : nullptr,
bPlayerFitRestricted
? EPlayerTraversalPolicy::WalkOnly
: EPlayerTraversalPolicy::WalkAndClimb);
if (OutDiagnostics != nullptr)
{
PopulateConnectivityDiagnostics(
Grid,
Start,
Goal,
Result,
bOutStartSnapped,
bOutGoalSnapped,
OutNumRouteRetries,
*OutDiagnostics,
bPlayerFitRestricted ? &PlayerFitMask : nullptr,
nullptr,
nullptr,
bPlayerFitRestricted ? &PlayerFitSupportHeights : nullptr,
bPlayerFitRestricted ? &Settings : nullptr,
bPlayerFitRestricted
? EPlayerTraversalPolicy::WalkOnly
: EPlayerTraversalPolicy::WalkAndClimb);
if (bPlayerFitRestricted &&
(Result == EVoxelConnectivityResult::StartCellNotPlayerFit
|| Result == EVoxelConnectivityResult::GoalCellNotPlayerFit))
{
OutDiagnostics->RefusalReason = Result == EVoxelConnectivityResult::StartCellNotPlayerFit
? TEXT("arrival endpoint is not a player-fitting floor cell")
: TEXT("departure endpoint is not a player-fitting floor cell");
}
}
return Result;
}
int32 GetSixNeighbour(const FSampleGrid& Grid, int32 Cell, int32 Direction)
{
int32 X = 0;
int32 Y = 0;
int32 Z = 0;
DecodeIndex(Grid, Cell, X, Y, Z);
switch (Direction)
{
case 0: return X + 1 < Grid.NumX ? Grid.Index(X + 1, Y, Z) : INDEX_NONE;
case 1: return X > 0 ? Grid.Index(X - 1, Y, Z) : INDEX_NONE;
case 2: return Y + 1 < Grid.NumY ? Grid.Index(X, Y + 1, Z) : INDEX_NONE;
case 3: return Y > 0 ? Grid.Index(X, Y - 1, Z) : INDEX_NONE;
case 4: return Z + 1 < Grid.NumZ ? Grid.Index(X, Y, Z + 1) : INDEX_NONE;
case 5: return Z > 0 ? Grid.Index(X, Y, Z - 1) : INDEX_NONE;
default: return INDEX_NONE;
}
}
int32 CountPlayerFitNeighbours(
const FSampleGrid& Grid,
const TArray<uint8>& PlayerFitMask,
const TArray<float>* PlayerFitSupportHeights,
const FVoxelStrateMeasureSettings* Settings,
int32 Cell,
EPlayerTraversalPolicy Policy = EPlayerTraversalPolicy::WalkAndClimb)
{
TArray<FPlayerTraversalNeighbour, TInlineAllocator<128>> Neighbours;
AppendPlayerTraversalNeighbours(
Grid, PlayerFitMask, PlayerFitSupportHeights, Settings, Cell, Neighbours, Policy);
int32 Count = 0;
for (const FPlayerTraversalNeighbour& Neighbour : Neighbours)
{
++Count;
}
return Count;
}
int32 LocalAirSpanVoxels(
const FSampleGrid& Grid,
int32 Cell,
int32 Axis,
int32 MaxRelevantSpanVoxels)
{
if (Cell < 0 || Cell >= Grid.CellCount || Grid.Air[Cell] == 0u)
{
return 0;
}
int32 CentreX = 0;
int32 CentreY = 0;
int32 CentreZ = 0;
DecodeIndex(Grid, Cell, CentreX, CentreY, CentreZ);
int32 Span = 1;
bool bTouchesWindowBoundary = false;
for (const int32 Sign : { -1, 1 })
{
int32 Offset = 1;
for (;; ++Offset)
{
if (Span >= MaxRelevantSpanVoxels)
{
return Span;
}
const int32 X = Axis == 0 ? CentreX + Sign * Offset : CentreX;
const int32 Y = Axis == 1 ? CentreY + Sign * Offset : CentreY;
const int32 Z = CentreZ;
if (X < 0 || X >= Grid.NumX || Y < 0 || Y >= Grid.NumY)
{
bTouchesWindowBoundary = true;
break;
}
if (Grid.Air[Grid.Index(X, Y, Z)] == 0u)
{
break;
}
++Span;
}
}
// A window edge is a lower bound, not a solid wall. Do not label a corridor narrow merely
// because the fitted diagnostic box clipped the air span before reaching rock.
return bTouchesWindowBoundary ? MAX_int32 : Span * Grid.SampleStep;
}
float LocalWidestHorizontalAirSpanVoxels(
const FSampleGrid& Grid,
int32 Cell,
float NarrowGapThresholdVoxels)
{
const int32 MaxRelevantSpanVoxels = FMath::Max(
1,
FMath::CeilToInt(NarrowGapThresholdVoxels));
return static_cast<float>(FMath::Max(
LocalAirSpanVoxels(Grid, Cell, 0, MaxRelevantSpanVoxels),
LocalAirSpanVoxels(Grid, Cell, 1, MaxRelevantSpanVoxels)));
}
void WalkPlayerFitGraph(
const FSampleGrid& Grid,
const TArray<uint8>& PlayerFitMask,
const TArray<float>* PlayerFitSupportHeights,
const FVoxelStrateMeasureSettings* TraversalSettings,
EPlayerTraversalPolicy TraversalPolicy,
int32 Start,
int32 Goal,
float NarrowGapThresholdVoxels,
FVoxelPlayerFitWalkReport& InOutReport,
int32& OutFinalCell,
int32& OutLastReachedCell)
{
OutFinalCell = INDEX_NONE;
OutLastReachedCell = INDEX_NONE;
if (!IsEligiblePlayerFitCell(Grid, PlayerFitMask, Start)
|| !IsEligiblePlayerFitCell(Grid, PlayerFitMask, Goal))
{
return;
}
OutFinalCell = Start;
OutLastReachedCell = Start;
TArray<uint8> Visited;
Visited.Init(0u, Grid.CellCount);
TArray<uint8> NarrowCell;
NarrowCell.Init(255u, Grid.CellCount);
TArray<int32> Stack;
TArray<int32> NextDirection;
// Reserve the full bounded capacity up front. TArray growth slack is otherwise allowed
// to exceed the walker's stated memory budget while the DFS discovers a large component.
Stack.Reserve(Grid.CellCount);
NextDirection.Reserve(Grid.CellCount);
Stack.Add(Start);
NextDirection.Add(0);
Visited[Start] = 1u;
auto RecordTraversal = [&](int32 From, int32 To)
{
++InOutReport.AgentGraphTraversals;
bool bClimb = false;
if (PlayerFitSupportHeights != nullptr && TraversalSettings != nullptr
&& IsPlayerTraversalEdge(
Grid, PlayerFitMask, PlayerFitSupportHeights, TraversalSettings,
From, To, bClimb, TraversalPolicy)
&& bClimb)
{
++InOutReport.ClimbTraversals;
}
auto IsNarrowCell = [&](int32 Cell)
{
if (NarrowCell[Cell] == 255u)
{
NarrowCell[Cell] = LocalWidestHorizontalAirSpanVoxels(
Grid, Cell, NarrowGapThresholdVoxels)
< NarrowGapThresholdVoxels ? 1u : 0u;
}
return NarrowCell[Cell] != 0u;
};
const bool bNarrow = IsNarrowCell(From) || IsNarrowCell(To);
if (bNarrow)
{
++InOutReport.NarrowGapTraversals;
}
};
while (Stack.Num() > 0)
{
const int32 Current = Stack.Last();
OutFinalCell = Current;
if (Current == Goal)
{
InOutReport.bCanReachDeparture = true;
break;
}
TArray<FPlayerTraversalNeighbour, TInlineAllocator<128>> Neighbours;
AppendPlayerTraversalNeighbours(
Grid, PlayerFitMask, PlayerFitSupportHeights, TraversalSettings,
Current, Neighbours, TraversalPolicy);
bool bAdvanced = false;
while (NextDirection.Last() < Neighbours.Num())
{
const int32 Direction = NextDirection.Last()++;
const int32 Neighbour = Neighbours[Direction].Cell;
if (!IsEligiblePlayerFitCell(Grid, PlayerFitMask, Neighbour)
|| Visited[Neighbour] != 0u)
{
continue;
}
Visited[Neighbour] = 1u;
RecordTraversal(Current, Neighbour);
Stack.Add(Neighbour);
NextDirection.Add(0);
OutLastReachedCell = Neighbour;
bAdvanced = true;
break;
}
if (bAdvanced)
{
continue;
}
// 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, PlayerFitSupportHeights,
TraversalSettings, Current, TraversalPolicy) <= 1)
{
++InOutReport.DeadEndsEncountered;
}
Stack.Pop();
NextDirection.Pop();
if (Stack.Num() > 0)
{
RecordTraversal(Current, Stack.Last());
}
}
const float DistanceMeters = static_cast<float>(InOutReport.AgentGraphTraversals)
* FVoxelPlayerCapsuleConstants::VoxelSizeMeters;
InOutReport.DistanceTravelledMeters = DistanceMeters;
InOutReport.NarrowGapFraction = InOutReport.AgentGraphTraversals > 0
? static_cast<float>(static_cast<double>(InOutReport.NarrowGapTraversals)
/ static_cast<double>(InOutReport.AgentGraphTraversals))
: 0.0f;
InOutReport.NarrowGapEventsPer100m = DistanceMeters > KINDA_SMALL_NUMBER
? static_cast<float>(static_cast<double>(InOutReport.NarrowGapTraversals)
* 100.0 / static_cast<double>(DistanceMeters))
: 0.0f;
InOutReport.DeadEndsPer100m = DistanceMeters > KINDA_SMALL_NUMBER
? static_cast<float>(static_cast<double>(InOutReport.DeadEndsEncountered)
* 100.0 / static_cast<double>(DistanceMeters))
: 0.0f;
InOutReport.DeadEndsEncountered = FMath::Max<int64>(
InOutReport.DeadEndsEncountered, 0);
}
}
FVoxelStrateMetrics VF_MeasureStrate(
const UVoxelGenerator& Generator,
const UVoxelStrateManager& Manager,
int32 StrateIndex,
const FVoxelStrateMeasureSettings& Settings,
FVoxelStrateSampleGrid* OutSampleGrid)
{
FVoxelStrateMetrics Result;
if (OutSampleGrid != nullptr)
{
*OutSampleGrid = FVoxelStrateSampleGrid();
}
VoxelStrateMeasurePrivate::FSampleGrid Grid;
if (!VoxelStrateMeasurePrivate::BuildSampleGrid(
&Generator, &Manager, nullptr, StrateIndex, 0, 0, 0.0f, false,
Settings, OutSampleGrid != nullptr || Settings.SampleStep == 1,
Grid, Result.RefusalReason))
{
return Result;
}
Result.ResolvedMarginVoxels = Grid.ResolvedMarginVoxels;
Result.SampledMinZ = Grid.SampledMinZ;
Result.SampledMaxZ = Grid.SampledMaxZ;
Result.SampledNumX = Grid.NumX;
Result.SampledNumY = Grid.NumY;
Result.SampledNumZ = Grid.NumZ;
Result.SampledMinX = Grid.MinX;
Result.SampledMaxX = Grid.MaxX;
Result.SampledMinY = Grid.MinY;
Result.SampledMaxY = Grid.MaxY;
TArray<uint8> PlayerFitMask;
VoxelStrateMeasurePrivate::BuildMetricsFromGrid(
Grid,
Settings,
Result,
OutSampleGrid != nullptr ? &PlayerFitMask : nullptr);
Result.bValid = Result.NumSampled > 0;
if (!Result.bValid)
{
Result.RefusalReason = TEXT("The measurement grid was empty.");
}
if (OutSampleGrid != nullptr && Result.bValid)
{
VoxelStrateMeasurePrivate::ExportSampleGrid(Grid, *OutSampleGrid);
OutSampleGrid->PlayerFitMask = MoveTemp(PlayerFitMask);
OutSampleGrid->PlayerFitCellCount = Result.NumPlayerFitCells;
}
return Result;
}
FVoxelStrateMetrics VF_MeasureStrateWithSampler(
const IVoxelStrateDensitySampler& Sampler,
int32 StrateBottomWorldZ,
int32 StrateTopWorldZ,
float BoundarySealThickness,
const FVoxelStrateMeasureSettings& Settings,
FVoxelStrateSampleGrid* OutSampleGrid)
{
FVoxelStrateMetrics Result;
if (OutSampleGrid != nullptr)
{
*OutSampleGrid = FVoxelStrateSampleGrid();
}
VoxelStrateMeasurePrivate::FSampleGrid Grid;
if (!VoxelStrateMeasurePrivate::BuildSampleGrid(
nullptr, nullptr, &Sampler, INDEX_NONE, StrateBottomWorldZ, StrateTopWorldZ,
BoundarySealThickness, true, Settings,
OutSampleGrid != nullptr || Settings.SampleStep == 1,
Grid, Result.RefusalReason))
{
return Result;
}
Result.ResolvedMarginVoxels = Grid.ResolvedMarginVoxels;
Result.SampledMinZ = Grid.SampledMinZ;
Result.SampledMaxZ = Grid.SampledMaxZ;
Result.SampledNumX = Grid.NumX;
Result.SampledNumY = Grid.NumY;
Result.SampledNumZ = Grid.NumZ;
Result.SampledMinX = Grid.MinX;
Result.SampledMaxX = Grid.MaxX;
Result.SampledMinY = Grid.MinY;
Result.SampledMaxY = Grid.MaxY;
TArray<uint8> PlayerFitMask;
VoxelStrateMeasurePrivate::BuildMetricsFromGrid(
Grid,
Settings,
Result,
OutSampleGrid != nullptr ? &PlayerFitMask : nullptr);
Result.bValid = Result.NumSampled > 0;
if (!Result.bValid)
{
Result.RefusalReason = TEXT("The measurement grid was empty.");
}
if (OutSampleGrid != nullptr && Result.bValid)
{
VoxelStrateMeasurePrivate::ExportSampleGrid(Grid, *OutSampleGrid);
OutSampleGrid->PlayerFitMask = MoveTemp(PlayerFitMask);
OutSampleGrid->PlayerFitCellCount = Result.NumPlayerFitCells;
}
return Result;
}
EVoxelConnectivityResult VF_AreConnected(
const UVoxelGenerator& Generator,
const UVoxelStrateManager& Manager,
int32 StrateIndex,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
bool& bOutStartSnapped,
bool& bOutGoalSnapped)
{
int32 NumRouteRetries = 0;
return VoxelStrateMeasurePrivate::QueryConnectivity(
&Generator,
&Manager,
nullptr,
StrateIndex,
0, 0, 0.0f, false,
AVoxel,
BVoxel,
Settings,
bOutStartSnapped,
bOutGoalSnapped,
NumRouteRetries,
nullptr);
}
EVoxelConnectivityResult VF_AreConnected(
const UVoxelGenerator& Generator,
const UVoxelStrateManager& Manager,
int32 StrateIndex,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
bool& bOutStartSnapped,
bool& bOutGoalSnapped,
int32& OutNumRouteRetries)
{
return VoxelStrateMeasurePrivate::QueryConnectivity(
&Generator,
&Manager,
nullptr,
StrateIndex,
0, 0, 0.0f, false,
AVoxel,
BVoxel,
Settings,
bOutStartSnapped,
bOutGoalSnapped,
OutNumRouteRetries,
nullptr);
}
EVoxelConnectivityResult VF_AreConnected(
const UVoxelGenerator& Generator,
const UVoxelStrateManager& Manager,
int32 StrateIndex,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
bool& bOutStartSnapped,
bool& bOutGoalSnapped,
FVoxelConnectivityDiagnostics& OutDiagnostics)
{
int32 NumRouteRetries = 0;
return VoxelStrateMeasurePrivate::QueryConnectivity(
&Generator,
&Manager,
nullptr,
StrateIndex,
0, 0, 0.0f, false,
AVoxel,
BVoxel,
Settings,
bOutStartSnapped,
bOutGoalSnapped,
NumRouteRetries,
&OutDiagnostics);
}
FVoxelConnectivityDiagnostics VF_DiagnoseConnectivity(
const UVoxelGenerator& Generator,
const UVoxelStrateManager& Manager,
int32 StrateIndex,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings)
{
FVoxelConnectivityDiagnostics Result;
int32 NumRouteRetries = 0;
VoxelStrateMeasurePrivate::QueryConnectivity(
&Generator,
&Manager,
nullptr,
StrateIndex,
0, 0, 0.0f, false,
AVoxel,
BVoxel,
Settings,
Result.bStartSnapped,
Result.bGoalSnapped,
NumRouteRetries,
&Result);
return Result;
}
EVoxelConnectivityResult VF_AreConnectedWithSampler(
const IVoxelStrateDensitySampler& Sampler,
int32 StrateBottomWorldZ,
int32 StrateTopWorldZ,
float BoundarySealThickness,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
bool& bOutStartSnapped,
bool& bOutGoalSnapped,
FVoxelConnectivityDiagnostics* OutDiagnostics)
{
int32 NumRouteRetries = 0;
return VoxelStrateMeasurePrivate::QueryConnectivity(
nullptr, nullptr, &Sampler, INDEX_NONE,
StrateBottomWorldZ, StrateTopWorldZ, BoundarySealThickness, true,
AVoxel, BVoxel, Settings, bOutStartSnapped, bOutGoalSnapped,
NumRouteRetries, OutDiagnostics);
}
FVoxelConnectivityDiagnostics VF_DiagnoseConnectivityWithSampler(
const IVoxelStrateDensitySampler& Sampler,
int32 StrateBottomWorldZ,
int32 StrateTopWorldZ,
float BoundarySealThickness,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings)
{
FVoxelConnectivityDiagnostics Result;
VF_AreConnectedWithSampler(Sampler, StrateBottomWorldZ, StrateTopWorldZ,
BoundarySealThickness, AVoxel, BVoxel, Settings,
Result.bStartSnapped, Result.bGoalSnapped, &Result);
return Result;
}
EVoxelConnectivityResult VF_ArePlayerFitConnectedWithSampler(
const IVoxelStrateDensitySampler& Sampler,
int32 StrateBottomWorldZ,
int32 StrateTopWorldZ,
float BoundarySealThickness,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
bool& bOutStartSnapped,
bool& bOutGoalSnapped,
FVoxelConnectivityDiagnostics* OutDiagnostics)
{
int32 NumRouteRetries = 0;
return VoxelStrateMeasurePrivate::QueryConnectivity(
nullptr, nullptr, &Sampler, INDEX_NONE,
StrateBottomWorldZ, StrateTopWorldZ, BoundarySealThickness, true,
AVoxel, BVoxel, Settings, bOutStartSnapped, bOutGoalSnapped,
NumRouteRetries, OutDiagnostics, true, nullptr);
}
FVoxelConnectivityDiagnostics VF_DiagnosePlayerFitConnectivityWithSampler(
const IVoxelStrateDensitySampler& Sampler,
int32 StrateBottomWorldZ,
int32 StrateTopWorldZ,
float BoundarySealThickness,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
FVoxelStrateMetrics* OutPlayerMetrics)
{
FVoxelConnectivityDiagnostics Result;
int32 NumRouteRetries = 0;
VoxelStrateMeasurePrivate::QueryConnectivity(
nullptr, nullptr, &Sampler, INDEX_NONE,
StrateBottomWorldZ, StrateTopWorldZ, BoundarySealThickness, true,
AVoxel, BVoxel, Settings, Result.bStartSnapped, Result.bGoalSnapped,
NumRouteRetries, &Result, true, OutPlayerMetrics);
return Result;
}
bool VF_MeasurePlayerFitWalkWithSampler(
const IVoxelStrateDensitySampler& Sampler,
int32 StrateBottomWorldZ,
int32 StrateTopWorldZ,
float BoundarySealThickness,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
FVoxelPlayerFitWalkReport& OutReport,
const TArray<FVector>* ComponentProbePoints,
FVoxelStrateSampleGrid* OutSampleGrid)
{
using namespace VoxelStrateMeasurePrivate;
OutReport = FVoxelPlayerFitWalkReport();
if (OutSampleGrid != nullptr)
{
*OutSampleGrid = FVoxelStrateSampleGrid();
}
OutReport.CapsuleRadiusMeters = Settings.PlayerCapsuleRadiusVoxels
* FVoxelPlayerCapsuleConstants::VoxelSizeMeters;
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");
const bool bFiniteEndpoints = FMath::IsFinite(AVoxel.X)
&& FMath::IsFinite(AVoxel.Y)
&& FMath::IsFinite(AVoxel.Z)
&& FMath::IsFinite(BVoxel.X)
&& FMath::IsFinite(BVoxel.Y)
&& FMath::IsFinite(BVoxel.Z);
if (!bFiniteEndpoints)
{
OutReport.RefusalReason = TEXT("Arrival and departure points must be finite.");
return false;
}
if (Settings.MaxRouteRetries < 0)
{
OutReport.RefusalReason = TEXT("MaxRouteRetries cannot be negative.");
return false;
}
if (Settings.SampleStep != 1)
{
OutReport.RefusalReason = TEXT(
"Player-fit walk refused: exact capsule fit requires SampleStep=1.");
return false;
}
FSampleGrid Grid;
FString RefusalReason;
if (!BuildSampleGrid(
nullptr,
nullptr,
&Sampler,
INDEX_NONE,
StrateBottomWorldZ,
StrateTopWorldZ,
BoundarySealThickness,
true,
Settings,
true,
Grid,
RefusalReason))
{
OutReport.RefusalReason = RefusalReason;
return false;
}
TArray<uint8> PlayerFitMask;
TArray<float> PlayerFitSupportHeights;
int64 NumPlayerFitCells = 0;
if (!BuildPlayerFitMask(
Grid,
Settings,
PlayerFitMask,
NumPlayerFitCells,
RefusalReason,
&PlayerFitSupportHeights))
{
OutReport.RefusalReason = RefusalReason;
return false;
}
int32 Start = INDEX_NONE;
int32 Goal = INDEX_NONE;
int32 NumRouteRetries = 0;
const EVoxelConnectivityResult Result = EvaluateConnectivityOnGrid(
nullptr,
&Sampler,
Grid,
AVoxel,
BVoxel,
Start,
Goal,
OutReport.bStartSnapped,
OutReport.bGoalSnapped,
NumRouteRetries,
Settings.MaxRouteRetries,
&PlayerFitMask,
&PlayerFitSupportHeights,
&Settings,
EPlayerTraversalPolicy::WalkOnly);
FVoxelConnectivityDiagnostics Diagnostics;
TArray<int32> Components;
int32 NumComponents = 0;
PopulateConnectivityDiagnostics(
Grid,
Start,
Goal,
Result,
OutReport.bStartSnapped,
OutReport.bGoalSnapped,
NumRouteRetries,
Diagnostics,
&PlayerFitMask,
&Components,
&NumComponents,
&PlayerFitSupportHeights,
&Settings,
EPlayerTraversalPolicy::WalkOnly);
OutReport.bValid = true;
OutReport.Result = Result;
OutReport.NumRouteRetries = NumRouteRetries;
OutReport.SampledNumX = Grid.NumX;
OutReport.SampledNumY = Grid.NumY;
OutReport.SampledNumZ = Grid.NumZ;
OutReport.SampledMinZ = Grid.SampledMinZ;
OutReport.SampledMaxZ = Grid.SampledMaxZ;
OutReport.SampledMinX = Grid.MinX;
OutReport.SampledMaxX = Grid.MaxX;
OutReport.SampledMinY = Grid.MinY;
OutReport.SampledMaxY = Grid.MaxY;
OutReport.PlayerFitVolumeCells = NumPlayerFitCells;
OutReport.WalkOnlyReachablePlayerFitCells = FMath::Max<int64>(
Diagnostics.StartComponentCells, 0);
OutReport.ReachablePlayerFitCells = OutReport.WalkOnlyReachablePlayerFitCells;
OutReport.bWalkOnlyCanReachDeparture = Result == EVoxelConnectivityResult::Connected;
OutReport.bCanReachDeparture = OutReport.bWalkOnlyCanReachDeparture;
BuildClimbTierFromWalkComponents(
Grid,
PlayerFitMask,
PlayerFitSupportHeights,
Settings,
Components,
NumComponents,
Start,
Goal,
NumPlayerFitCells,
OutReport.WalkOnlyReachablePlayerFitCells,
OutReport.ClimbReachablePlayerFitCells,
OutReport.MineRequiredPlayerFitCells,
OutReport.bClimbCanReachDeparture,
OutReport.ClimbEdges);
OutReport.ReachablePlayerFitFraction = OutReport.PlayerFitVolumeCells > 0
? static_cast<float>(static_cast<double>(OutReport.ReachablePlayerFitCells)
/ static_cast<double>(OutReport.PlayerFitVolumeCells))
: 0.0f;
OutReport.ClimbReachablePlayerFitFraction = OutReport.PlayerFitVolumeCells > 0
? static_cast<float>(static_cast<double>(OutReport.ClimbReachablePlayerFitCells)
/ static_cast<double>(OutReport.PlayerFitVolumeCells))
: 0.0f;
OutReport.PlayerFitComponents = NumComponents;
OutReport.LargestPlayerFitComponentCells = Diagnostics.LargestComponentCells;
OutReport.ArrivalComponentCells = Diagnostics.StartComponentCells;
OutReport.DepartureComponentCells = Diagnostics.GoalComponentCells;
OutReport.MouthComponentGapVoxels = Diagnostics.StartToGoalComponentDistanceCells;
OutReport.StraightLineMeters = FVector::Dist(AVoxel, BVoxel)
* FVoxelPlayerCapsuleConstants::VoxelSizeMeters;
int32 AgentFinalCell = INDEX_NONE;
int32 LastReachedCell = INDEX_NONE;
if (Start >= 0 && Goal >= 0)
{
const float NarrowGapThresholdVoxels = OutReport.NarrowGapThresholdMeters
/ FVoxelPlayerCapsuleConstants::VoxelSizeMeters;
WalkPlayerFitGraph(
Grid,
PlayerFitMask,
&PlayerFitSupportHeights,
&Settings,
EPlayerTraversalPolicy::WalkOnly,
Start,
Goal,
NarrowGapThresholdVoxels,
OutReport,
AgentFinalCell,
LastReachedCell);
}
auto RecordCellPosition = [&Grid](
int32 Cell,
bool& bOutHasPosition,
FVector& OutPosition)
{
if (Cell >= 0 && Cell < Grid.CellCount)
{
int32 X = 0;
int32 Y = 0;
int32 Z = 0;
DecodeIndex(Grid, Cell, X, Y, Z);
bOutHasPosition = true;
OutPosition = Grid.CellCenter(X, Y, Z);
}
};
RecordCellPosition(
AgentFinalCell,
OutReport.bHasAgentFinalPosition,
OutReport.AgentFinalVoxels);
RecordCellPosition(
LastReachedCell,
OutReport.bHasLastReachedPosition,
OutReport.LastReachedVoxels);
const int32 StartComponent = Start >= 0 && Components.IsValidIndex(Start)
? Components[Start] : INDEX_NONE;
const int32 GoalComponent = Goal >= 0 && Components.IsValidIndex(Goal)
? Components[Goal] : INDEX_NONE;
if (ComponentProbePoints != nullptr)
{
OutReport.ComponentProbePlayerFit.Init(0u, ComponentProbePoints->Num());
OutReport.ComponentProbeInStartComponent.Init(0u, ComponentProbePoints->Num());
for (int32 ProbeIndex = 0; ProbeIndex < ComponentProbePoints->Num(); ++ProbeIndex)
{
int32 ProbeCell = INDEX_NONE;
const FVector& ProbePoint = (*ComponentProbePoints)[ProbeIndex];
if (!FindCellForPoint(Grid, ProbePoint, ProbeCell)
|| !IsEligiblePlayerFitCell(Grid, PlayerFitMask, ProbeCell))
{
continue;
}
OutReport.ComponentProbePlayerFit[ProbeIndex] = 1u;
if (StartComponent >= 0 && Components.IsValidIndex(ProbeCell)
&& Components[ProbeCell] == StartComponent)
{
OutReport.ComponentProbeInStartComponent[ProbeIndex] = 1u;
}
}
}
if (StartComponent >= 0)
{
float BestOriginDistanceSquared = FLT_MAX;
for (int32 Cell = 0; Cell < Grid.CellCount; ++Cell)
{
if (!Components.IsValidIndex(Cell) || Components[Cell] != StartComponent)
{
continue;
}
int32 X = 0;
int32 Y = 0;
int32 Z = 0;
DecodeIndex(Grid, Cell, X, Y, Z);
const FVector Position = Grid.CellCenter(X, Y, Z);
BestOriginDistanceSquared = FMath::Min(
BestOriginDistanceSquared,
Position.X * Position.X + Position.Y * Position.Y);
}
if (BestOriginDistanceSquared < FLT_MAX)
{
OutReport.ReachableSetToOriginColumnVoxels = FMath::Sqrt(
BestOriginDistanceSquared);
}
}
const bool bOriginPointInWindow = 0.0f >= Grid.MinX && 0.0f < Grid.MaxX
&& 0.0f >= Grid.MinY && 0.0f < Grid.MaxY;
if (bOriginPointInWindow)
{
const int32 OriginX = FMath::Clamp(
FMath::FloorToInt((0.0f - Grid.MinX) / static_cast<float>(Grid.SampleStep)),
0,
Grid.NumX - 1);
const int32 OriginY = FMath::Clamp(
FMath::FloorToInt((0.0f - Grid.MinY) / static_cast<float>(Grid.SampleStep)),
0,
Grid.NumY - 1);
OutReport.bOriginColumnInSampledWindow = true;
for (int32 Z = 0; Z < Grid.NumZ; ++Z)
{
const int32 Cell = Grid.Index(OriginX, OriginY, Z);
if (PlayerFitMask[Cell] == 0u)
{
continue;
}
++OutReport.OriginColumnPlayerFitCells;
OutReport.bOriginColumnHasPlayerFit = true;
if (Components.IsValidIndex(Cell) && Components[Cell] == StartComponent)
{
OutReport.bOriginColumnReachable = true;
}
}
}
if (AgentFinalCell >= 0 && AgentFinalCell < Grid.CellCount && GoalComponent >= 0)
{
const FVector AgentPosition = OutReport.AgentFinalVoxels;
float BestTargetDistanceSquared = FLT_MAX;
int32 BestTargetCell = INDEX_NONE;
for (int32 Cell = 0; Cell < Grid.CellCount; ++Cell)
{
if (!Components.IsValidIndex(Cell) || Components[Cell] != GoalComponent)
{
continue;
}
int32 X = 0;
int32 Y = 0;
int32 Z = 0;
DecodeIndex(Grid, Cell, X, Y, Z);
const float DistanceSquared = FVector::DistSquared(
AgentPosition, Grid.CellCenter(X, Y, Z));
if (DistanceSquared < BestTargetDistanceSquared
|| (DistanceSquared == BestTargetDistanceSquared
&& (BestTargetCell == INDEX_NONE || Cell < BestTargetCell)))
{
BestTargetDistanceSquared = DistanceSquared;
BestTargetCell = Cell;
}
}
if (BestTargetCell >= 0)
{
RecordCellPosition(
BestTargetCell,
OutReport.bHasTargetComponentNearestCell,
OutReport.TargetComponentNearestVoxels);
OutReport.AgentToTargetComponentGapVoxels = FMath::Sqrt(
BestTargetDistanceSquared);
}
}
OutReport.bCanReachDeparture = Result == EVoxelConnectivityResult::Connected;
OutReport.Tortuosity = OutReport.StraightLineMeters > KINDA_SMALL_NUMBER
? OutReport.DistanceTravelledMeters / OutReport.StraightLineMeters
: 0.0f;
OutReport.DeadEndsEncountered = FMath::Max<int64>(
OutReport.DeadEndsEncountered, 0);
OutReport.NarrowGapEventsPer100m = OutReport.DistanceTravelledMeters
> KINDA_SMALL_NUMBER
? static_cast<float>(static_cast<double>(OutReport.NarrowGapTraversals)
* 100.0 / static_cast<double>(OutReport.DistanceTravelledMeters))
: 0.0f;
OutReport.DeadEndsPer100m = OutReport.DistanceTravelledMeters
> KINDA_SMALL_NUMBER
? static_cast<float>(static_cast<double>(OutReport.DeadEndsEncountered)
* 100.0 / static_cast<double>(OutReport.DistanceTravelledMeters))
: 0.0f;
if (Result == EVoxelConnectivityResult::StartCellNotPlayerFit)
{
OutReport.RefusalReason = TEXT("arrival endpoint is not a player-fitting floor cell");
}
else if (Result == EVoxelConnectivityResult::GoalCellNotPlayerFit)
{
OutReport.RefusalReason = TEXT("departure endpoint is not a player-fitting floor cell");
}
else if (Result == EVoxelConnectivityResult::OutOfWindow)
{
OutReport.RefusalReason = TEXT("arrival or departure point is outside the fitted walk window");
}
else
{
OutReport.RefusalReason = Diagnostics.RefusalReason;
}
if (OutSampleGrid != nullptr)
{
ExportSampleGrid(Grid, *OutSampleGrid);
OutSampleGrid->PlayerFitMask = MoveTemp(PlayerFitMask);
OutSampleGrid->PlayerFitCellCount = NumPlayerFitCells;
}
return true;
}
#endif // WITH_EDITOR — measurement never ships