Files
VoxelForge/Source/VoxelForge/Private/VoxelStrateMeasure.cpp
T
Fr0zkaandClaude Opus 5 6b916699af test(explore): 81 runs in an afternoon -- every failure is a DROP, not a squeeze
First real use of the commandlet, and it paid for itself immediately: 64
sweep runs + failure renders + origin probes, which at the old 73-minutes-
per-automation-run would have been days.

⭐ FINDING 1: THE WORLD IS FRAGMENTED VERTICALLY, NOT PINCHED.
Across all 64 runs: ZERO narrow-gap events. Every failed run has separate
arrival/departure fit components with a positive gap, 12.75 m to 72.98 m.
So corridors are not too tight anywhere -- the space is severed.

And for the first time in this project we LOOKED at a failure instead of
inferring from aggregates:
  FlatPlain/42   "a broad flat floor sheet with a sharp diagonal ledge
                  descending into black open space" -- a drop, gap 63.9 m
  SurfaceWorld/7 "terrain shelves and a large black void between them.
                  There is no connecting floor" -- gap 51.8 m
  Maze/42        "a massive solid rock wall/overhang fills the separation"
                  -- gap 27.7 m
I checked the images myself. FlatPlain shows two floor LEVELS 27 m apart
in Z with nothing joining them.

⇒ The missing thing is VERTICAL CONNECTION between floor levels. Ramps,
slopes, ledges, stairs. Every archetype builds floors; nothing builds a way
between them.

⭐ FINDING 2: THE ORIGIN HUB IS NOT REACHABLE, which contradicts 413c15d.
  agent's reachable set included the origin column:  0 / 64 runs
  origin column had ANY player-fit cell:            17 / 64 runs
  Maze slot 4, all 8 seeds:                          0 fit cells, unreachable
Even in the 17, the origin cells were not in the arrival component.

The reason is structural and obvious in hindsight: the origin spine is a
VERTICAL CYLINDER. A vertical shaft has no walkable floor except at its
bottom, so a scalar-density carve that guarantees open air guarantees
nothing standable. The landings join a hub you cannot stand in.

That is a finding about the §6.5 hub join as exposed to the player-fit
layer -- not walk tuning. Recorded rather than patched.

Also from Jahni, looking at the renders -- two things numbers missed:
- The render cameras sit INSIDE ROCK, viewing tunnel surfaces from the
  solid side. Cameras must be seeded from the player-fit set.
- ⚠️ THE MAZE IS STILL A LATTICE OF STRAIGHT PATHS. The spanning tree
  changed WHICH edges exist, not what an edge LOOKS like: every corridor is
  still a straight axis-aligned capsule between cell centres, every junction
  90 deg. So 15.9% dead ends and mean degree 2.504 measured maze-ness of the
  TOPOLOGY while the eye sees a sparser grid. Sixth time a metric answered a
  question adjacent to the one that mattered.

Two tool issues found: PowerShell needs -modes="render,walk" quoted or it
parses as render-only; and "showcase slot" is ambiguous (cards use 1/6, the
Maze diagnosis uses 4).

Determinism spot-checked: repeated arguments give identical SHA-256 JSON.
Measurement-only changes; no generation, parameter or threshold edits.
Editor build succeeded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
2026-09-07 18:33:15 +02:00

3168 lines
120 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"
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);
}
bool bSawNonFiniteDensity = false;
int32 CellIndex = 0;
for (int32 Z = 0; Z < OutGrid.NumZ; ++Z)
{
for (int32 Y = 0; Y < OutGrid.NumY; ++Y)
{
for (int32 X = 0; X < OutGrid.NumX; ++X)
{
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))
{
bSawNonFiniteDensity = true;
}
// 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;
}
}
}
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);
}
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;
}
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)
{
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;
}
};
// 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 raster passes with 6-neighbour Manhattan relaxation. The result is an approximation
// of distance-to-solid in the original field: each coarse grid unit is SampleStep VOXELS.
constexpr int32 Infinity = INT32_MAX;
OutDistances.SetNumUninitialized(Grid.CellCount);
for (int32 Index = 0; Index < Grid.CellCount; ++Index)
{
OutDistances[Index] = Grid.Air[Index] == 0u ? 0 : Infinity;
}
auto Relax = [&](int32 Current, 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);
};
for (int32 Z = 0; Z < Grid.NumZ; ++Z)
{
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) continue;
if (X > 0) Relax(Current, Grid.Index(X - 1, Y, Z));
if (Y > 0) Relax(Current, Grid.Index(X, Y - 1, Z));
if (Z > 0) Relax(Current, Grid.Index(X, Y, Z - 1));
}
}
}
for (int32 Z = Grid.NumZ - 1; Z >= 0; --Z)
{
for (int32 Y = Grid.NumY - 1; Y >= 0; --Y)
{
for (int32 X = Grid.NumX - 1; X >= 0; --X)
{
const int32 Current = Grid.Index(X, Y, Z);
if (Grid.Air[Current] == 0u) continue;
if (X + 1 < Grid.NumX) Relax(Current, Grid.Index(X + 1, Y, Z));
if (Y + 1 < Grid.NumY) Relax(Current, Grid.Index(X, Y + 1, Z));
if (Z + 1 < Grid.NumZ) Relax(Current, Grid.Index(X, Y, Z + 1));
}
}
}
}
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)
{
OutPlayerFit.Reset();
OutNumPlayerFitCells = 0;
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);
TArray<float> SupportHeights;
SupportHeights.SetNumUninitialized(SupportOffsets.Num());
TArray<int32> SupportAnchorZs;
SupportAnchorZs.SetNumUninitialized(SupportOffsets.Num());
TArray<uint8> bSupported;
bSupported.Init(0u, SupportOffsets.Num());
for (int32 Z = 1; Z < Grid.NumZ; ++Z)
{
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;
}
if (OutPlayerFit[Current] != 0u)
{
continue;
}
OutPlayerFit[Current] = 1u;
++OutNumPlayerFitCells;
}
}
}
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> Distances;
Distances.SetNumUninitialized(PlaneCells);
int32 MinimumDistance = Infinity;
for (int32 Z = 0; Z < Grid.NumZ; ++Z)
{
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);
}
}
}
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)
{
MinimumDistance = FMath::Min(
MinimumDistance, Distances[X + Grid.NumX * Y]);
}
}
}
}
return MinimumDistance == Infinity
? 0.0f : static_cast<float>(MinimumDistance);
}
void BuildPlayerFitMetrics(
const FSampleGrid& Grid,
const FVoxelStrateMeasureSettings& Settings,
FVoxelStrateMetrics& InOutMetrics,
TArray<uint8>* OutPlayerFitMask = 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;
if (!BuildPlayerFitMask(
Grid, Settings, PlayerFit, InOutMetrics.NumPlayerFitCells,
InOutMetrics.PlayerFitRefusalReason))
{
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);
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);
}
}
void BuildPlayerFitConnectivityMetrics(
const FSampleGrid& Grid,
const FVoxelStrateMeasureSettings& Settings,
FVoxelStrateMetrics& OutMetrics,
TArray<uint8>& OutPlayerFitMask)
{
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;
for (const uint8 bAir : Grid.Air)
{
OutMetrics.NumAir += bAir != 0u ? 1 : 0;
}
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);
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;
int64 NumAir = 0;
for (const uint8 bAir : Grid.Air)
{
NumAir += bAir != 0u ? 1 : 0;
}
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));
for (int32 Z = 0; Z < Grid.NumZ; ++Z)
{
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;
++NumWalkable;
WalkableColumns[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;
ClearanceValues.Add(ClearanceVoxels > INT32_MAX
? INT32_MAX : static_cast<int32>(ClearanceVoxels));
}
}
}
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<float> FeatureScales;
FeatureScales.Reserve(static_cast<int32>(NumAir));
for (int32 Index = 0; Index < Grid.CellCount; ++Index)
{
if (Grid.Air[Index] != 0u)
{
FeatureScales.Add(static_cast<float>(Distances[Index])
* static_cast<float>(Settings.SampleStep));
}
}
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)
{
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;
}
}
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 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;
}
};
// 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)
{
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))
{
// 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))
{
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)
{
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;
int64 NumAir = 0;
for (int32 Index = 0; Index < Grid.CellCount; ++Index)
{
NumAir += Grid.Air[Index] != 0u
&& (EligibilityMask == nullptr || (*EligibilityMask)[Index] != 0u)
? 1 : 0;
}
FloodFillAir(
Grid,
Components,
NumComponents,
NumAir,
LargestComponentCells,
LargestComponentLowestCell,
NumComponentsAtLeast1Pct,
nullptr,
EligibilityMask);
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;
OutDiagnostics.StartToGoalComponentDistanceCells = DistanceToComponent(
Grid, Start, GoalComponent, Components);
OutDiagnostics.GoalToStartComponentDistanceCells = DistanceToComponent(
Grid, Goal, StartComponent, Components);
}
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;
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);
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);
if (OutDiagnostics != nullptr)
{
PopulateConnectivityDiagnostics(
Grid,
Start,
Goal,
Result,
bOutStartSnapped,
bOutGoalSnapped,
OutNumRouteRetries,
*OutDiagnostics,
bPlayerFitRestricted ? &PlayerFitMask : nullptr);
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;
}
}
bool IsEligiblePlayerFitCell(
const FSampleGrid& Grid,
const TArray<uint8>& PlayerFitMask,
int32 Cell)
{
return Cell >= 0
&& Cell < Grid.CellCount
&& Grid.Air[Cell] != 0u
&& PlayerFitMask[Cell] != 0u;
}
int32 CountPlayerFitNeighbours(
const FSampleGrid& Grid,
const TArray<uint8>& PlayerFitMask,
int32 Cell)
{
int32 Count = 0;
for (int32 Direction = 0; Direction < 6; ++Direction)
{
if (IsEligiblePlayerFitCell(
Grid, PlayerFitMask, GetSixNeighbour(Grid, Cell, Direction)))
{
++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,
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;
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;
}
bool bAdvanced = false;
while (NextDirection.Last() < 6)
{
const int32 Direction = NextDirection.Last()++;
const int32 Neighbour = GetSixNeighbour(Grid, Current, Direction);
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, Current) <= 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;
VoxelStrateMeasurePrivate::BuildMetricsFromGrid(Grid, Settings, Result);
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);
}
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;
VoxelStrateMeasurePrivate::BuildMetricsFromGrid(Grid, Settings, Result);
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);
}
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)
{
using namespace VoxelStrateMeasurePrivate;
OutReport = FVoxelPlayerFitWalkReport();
OutReport.CapsuleRadiusMeters = Settings.PlayerCapsuleRadiusVoxels
* FVoxelPlayerCapsuleConstants::VoxelSizeMeters;
OutReport.CapsuleWidthMeters = 2.0f * OutReport.CapsuleRadiusMeters;
OutReport.CapsuleHeightMeters = 2.0f * Settings.PlayerCapsuleHalfHeightVoxels
* FVoxelPlayerCapsuleConstants::VoxelSizeMeters;
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;
int64 NumPlayerFitCells = 0;
if (!BuildPlayerFitMask(
Grid,
Settings,
PlayerFitMask,
NumPlayerFitCells,
RefusalReason))
{
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);
FVoxelConnectivityDiagnostics Diagnostics;
TArray<int32> Components;
int32 NumComponents = 0;
PopulateConnectivityDiagnostics(
Grid,
Start,
Goal,
Result,
OutReport.bStartSnapped,
OutReport.bGoalSnapped,
NumRouteRetries,
Diagnostics,
&PlayerFitMask,
&Components,
&NumComponents);
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.ReachablePlayerFitCells = FMath::Max<int64>(
Diagnostics.StartComponentCells, 0);
OutReport.ReachablePlayerFitFraction = OutReport.PlayerFitVolumeCells > 0
? static_cast<float>(static_cast<double>(OutReport.ReachablePlayerFitCells)
/ 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,
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 (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;
}
return true;
}
#endif // WITH_EDITOR — measurement never ships