feat(explore): a commandlet -- 91x faster, and the first real PICTURE
Every experiment in this project cost a build + full-suite cycle. The
landing task took 52 of them over ~9 hours. And nobody could SEE the world:
every visual judgement all session was made on density SLICES -- diagrams,
not pictures. That is how a floor heaving 1.4 m every two metres got
described as "roughness visible but modest" while Jahni spotted it in
thirty seconds by walking it.
-run=VoxelForgeExplore, editor-only module, three modes, one build many runs:
render raymarched PNGs, fixed 0.25-voxel step + 10 bisection iterations
(NOT sphere tracing -- the op stack yields density, not a signed
distance, so a long step tunnels through walls). Three views, each
with a 1.76 m human marker in frame, because every scale mistake
here came from numbers with no body beside them.
walk an agent using the EXISTING VF player-fit stencil, never its own
movement model -- two definitions of "can a player stand here"
would drift and we could not tell which was lying.
export OBJ through the canonical UVoxelMarchingCubesMesher. No second
mesher; a forked meshing invariant is the bug this project keeps
paying for. 23.2 MB, 106,762 verts, 200,842 tris for 128^3.
⭐ THE NUMBER THAT JUSTIFIES IT: the full automation run takes 4,396 s
(73m16s). The commandlet does all three modes in 48.1 s wall, 5.9 s of
which is engine startup. ~91x faster. And I had guessed the suite cost
~90 s -- it is 73 MINUTES. The iteration loop was far more expensive than
I knew.
First real walk metrics on the Maze (seed 0, slot 4):
distance 214 m · tortuosity 4.98 · dead ends 0.935/100 m
narrow-gap events 0 · reachable fit fraction 3.01%
result NotConnectedAtThisResolution
Tortuosity ~5 is a genuinely winding route, which is right for a maze.
3% reachable is not.
Editor Development build clean. SHIPPING build clean with the receipt
containing VoxelForge and NOT VoxelForgeEditor -- the instrument cannot
reach a packaged game. Automation 32/32, 0 failed.
⚠️ Codex initially shipped this UNBUILT, and correctly: I omitted the
## Validation section from that spec, so it fell back to CLAUDE.md's
"NEVER build or compile". 100+ compile errors from three causes -- bare
TCondensedJsonPrintPolicy and TJsonWriter needing their template
arguments, and a missing UObject/Package.h. That include is the SECOND
miss this session, so CODEMAP §6 now carries the gotcha.
⚠️ Unrelated, found in passing: VoxelForge.uplugin does not declare
RealtimeMeshComponent although the runtime module uses it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K8V9my1PL68pmQb2gutZZV
This commit is contained in:
1 parent
413c15de24
commit
bd547c35c4
9 files changed
+2327
-24
No files matched your search
@@ -1338,6 +1338,37 @@ namespace VoxelStrateMeasurePrivate
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -1948,7 +1979,6 @@ namespace VoxelStrateMeasurePrivate
|
||||
bOutStartSnapped = false;
|
||||
bOutGoalSnapped = false;
|
||||
OutNumRouteRetries = 0;
|
||||
|
||||
const EEndpointCellResult StartCell = ResolveEndpointCell(
|
||||
Grid, AVoxel, OutStart, bOutStartSnapped, EligibilityMask);
|
||||
const EEndpointCellResult GoalCell = ResolveEndpointCell(
|
||||
@@ -2243,28 +2273,7 @@ namespace VoxelStrateMeasurePrivate
|
||||
// Manhattan feature transform here: those values belong to the separate legacy
|
||||
// measurement pass, and recomputing them for every fine candidate made the gate
|
||||
// needlessly expensive without changing its answer.
|
||||
PlayerMetrics.ResolvedMarginVoxels = Grid.ResolvedMarginVoxels;
|
||||
PlayerMetrics.SampledMinZ = Grid.SampledMinZ;
|
||||
PlayerMetrics.SampledMaxZ = Grid.SampledMaxZ;
|
||||
PlayerMetrics.SampledNumX = Grid.NumX;
|
||||
PlayerMetrics.SampledNumY = Grid.NumY;
|
||||
PlayerMetrics.SampledNumZ = Grid.NumZ;
|
||||
PlayerMetrics.SampledMinX = Grid.MinX;
|
||||
PlayerMetrics.SampledMaxX = Grid.MaxX;
|
||||
PlayerMetrics.SampledMinY = Grid.MinY;
|
||||
PlayerMetrics.SampledMaxY = Grid.MaxY;
|
||||
PlayerMetrics.NumSampled = Grid.CellCount;
|
||||
for (const uint8 bAir : Grid.Air)
|
||||
{
|
||||
PlayerMetrics.NumAir += bAir != 0u ? 1 : 0;
|
||||
}
|
||||
PlayerMetrics.NumSolid = static_cast<int64>(Grid.CellCount) - PlayerMetrics.NumAir;
|
||||
PlayerMetrics.AirFraction = PlayerMetrics.NumSampled > 0
|
||||
? static_cast<float>(static_cast<double>(PlayerMetrics.NumAir)
|
||||
/ static_cast<double>(PlayerMetrics.NumSampled))
|
||||
: 0.0f;
|
||||
BuildPlayerFitMetrics(Grid, Settings, PlayerMetrics, &PlayerFitMask);
|
||||
PlayerMetrics.bValid = PlayerMetrics.NumSampled > 0;
|
||||
BuildPlayerFitConnectivityMetrics(Grid, Settings, PlayerMetrics, PlayerFitMask);
|
||||
if (OutPlayerMetrics != nullptr)
|
||||
{
|
||||
*OutPlayerMetrics = PlayerMetrics;
|
||||
@@ -2314,6 +2323,227 @@ namespace VoxelStrateMeasurePrivate
|
||||
}
|
||||
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)
|
||||
{
|
||||
if (!IsEligiblePlayerFitCell(Grid, PlayerFitMask, Start)
|
||||
|| !IsEligiblePlayerFitCell(Grid, PlayerFitMask, Goal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
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);
|
||||
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(
|
||||
@@ -2587,4 +2817,182 @@ FVoxelConnectivityDiagnostics VF_DiagnosePlayerFitConnectivityWithSampler(
|
||||
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;
|
||||
PopulateConnectivityDiagnostics(
|
||||
Grid,
|
||||
Start,
|
||||
Goal,
|
||||
Result,
|
||||
OutReport.bStartSnapped,
|
||||
OutReport.bGoalSnapped,
|
||||
NumRouteRetries,
|
||||
Diagnostics,
|
||||
&PlayerFitMask);
|
||||
|
||||
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.StraightLineMeters = FVector::Dist(AVoxel, BVoxel)
|
||||
* FVoxelPlayerCapsuleConstants::VoxelSizeMeters;
|
||||
|
||||
if (Start >= 0 && Goal >= 0)
|
||||
{
|
||||
const float NarrowGapThresholdVoxels = OutReport.NarrowGapThresholdMeters
|
||||
/ FVoxelPlayerCapsuleConstants::VoxelSizeMeters;
|
||||
WalkPlayerFitGraph(
|
||||
Grid,
|
||||
PlayerFitMask,
|
||||
Start,
|
||||
Goal,
|
||||
NarrowGapThresholdVoxels,
|
||||
OutReport);
|
||||
}
|
||||
|
||||
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
|
||||
@@ -255,6 +255,61 @@ struct VOXELFORGE_API FVoxelConnectivityDiagnostics
|
||||
float GoalToStartComponentDistanceCells = -1.0f;
|
||||
};
|
||||
|
||||
/**
|
||||
* Experiential traversal facts over the exact VF player-fit mask.
|
||||
*
|
||||
* This is intentionally a report, not a new movement implementation. The mask is built by the
|
||||
* same direct capsule stencil used by VF_DiagnosePlayerFitConnectivityWithSampler; the explorer's
|
||||
* deterministic graph walk only instruments that already-resolved six-neighbour pose graph.
|
||||
* Distances are metres so the result can be read beside a human-scale screenshot.
|
||||
*/
|
||||
struct VOXELFORGE_API FVoxelPlayerFitWalkReport
|
||||
{
|
||||
bool bValid = false;
|
||||
FString RefusalReason;
|
||||
EVoxelConnectivityResult Result = EVoxelConnectivityResult::OutOfWindow;
|
||||
bool bCanReachDeparture = false;
|
||||
bool bStartSnapped = false;
|
||||
bool bGoalSnapped = false;
|
||||
int32 NumRouteRetries = 0;
|
||||
|
||||
int64 PlayerFitVolumeCells = 0;
|
||||
int64 ReachablePlayerFitCells = 0;
|
||||
float ReachablePlayerFitFraction = 0.0f;
|
||||
int32 SampledNumX = 0;
|
||||
int32 SampledNumY = 0;
|
||||
int32 SampledNumZ = 0;
|
||||
int32 SampledMinZ = 0;
|
||||
int32 SampledMaxZ = 0;
|
||||
float SampledMinX = 0.0f;
|
||||
float SampledMaxX = 0.0f;
|
||||
float SampledMinY = 0.0f;
|
||||
float SampledMaxY = 0.0f;
|
||||
|
||||
// The agent follows a fixed-order depth-first walk over the already-resolved fit graph. A
|
||||
// branch edge is counted twice when the agent backtracks, which makes this an instrument of
|
||||
// traversal experience rather than a shortest-path length.
|
||||
int64 AgentGraphTraversals = 0;
|
||||
int64 DeadEndsEncountered = 0;
|
||||
float DeadEndsPer100m = 0.0f;
|
||||
float DistanceTravelledMeters = 0.0f;
|
||||
float StraightLineMeters = 0.0f;
|
||||
float Tortuosity = 0.0f;
|
||||
|
||||
// A narrow-gap event is a traversed graph edge touching a route cell whose widest
|
||||
// axis-aligned horizontal air span is below the threshold. This deliberately reports the
|
||||
// operational proxy instead of pretending the voxel mask contains an oriented corridor width.
|
||||
int64 NarrowGapTraversals = 0;
|
||||
float NarrowGapFraction = 0.0f;
|
||||
float NarrowGapThresholdMeters = 0.0f;
|
||||
float NarrowGapEventsPer100m = 0.0f;
|
||||
|
||||
float CapsuleRadiusMeters = 0.0f;
|
||||
float CapsuleWidthMeters = 0.0f;
|
||||
float CapsuleHeightMeters = 0.0f;
|
||||
FString NarrowGapDefinition;
|
||||
};
|
||||
|
||||
/**
|
||||
* Measure one strate in actor-space voxel coordinates.
|
||||
*
|
||||
@@ -395,3 +450,18 @@ VOXELFORGE_API FVoxelConnectivityDiagnostics VF_DiagnosePlayerFitConnectivityWit
|
||||
const FVector& BVoxel,
|
||||
const FVoxelStrateMeasureSettings& Settings,
|
||||
FVoxelStrateMetrics* OutPlayerMetrics = nullptr);
|
||||
|
||||
/**
|
||||
* Instrument a deterministic agent over the same exact player-fit graph used by the route gate.
|
||||
* The return value is false only when the bounded fine grid/stencil could not be built; a valid
|
||||
* report may still say NotConnectedAtThisResolution or another explicit connectivity result.
|
||||
*/
|
||||
VOXELFORGE_API bool VF_MeasurePlayerFitWalkWithSampler(
|
||||
const IVoxelStrateDensitySampler& Sampler,
|
||||
int32 StrateBottomWorldZ,
|
||||
int32 StrateTopWorldZ,
|
||||
float BoundarySealThickness,
|
||||
const FVector& AVoxel,
|
||||
const FVector& BVoxel,
|
||||
const FVoxelStrateMeasureSettings& Settings,
|
||||
FVoxelPlayerFitWalkReport& OutReport);
|
||||
@@ -0,0 +1,11 @@
|
||||
#include "CoreMinimal.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
class FVoxelForgeEditorModule final : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
virtual void StartupModule() override {}
|
||||
virtual void ShutdownModule() override {}
|
||||
};
|
||||
|
||||
IMPLEMENT_MODULE(FVoxelForgeEditorModule, VoxelForgeEditor)
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "Commandlets/Commandlet.h"
|
||||
#include "VoxelForgeExploreCommandlet.generated.h"
|
||||
|
||||
/** Headless, editor-only explorer for authoritative density, traversal, and mesh output. */
|
||||
UCLASS()
|
||||
class VOXELFORGEEDITOR_API UVoxelForgeExploreCommandlet : public UCommandlet
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UVoxelForgeExploreCommandlet();
|
||||
|
||||
virtual int32 Main(const FString& Params) override;
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
// Editor-only commandlets and offline explorers. This module is not linked by Game/Shipping.
|
||||
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class VoxelForgeEditor : ModuleRules
|
||||
{
|
||||
public VoxelForgeEditor(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
// The commandlet's PNG shading, metre conversions, and JSON fields are part of its
|
||||
// deterministic artifact contract, so keep the editor-side arithmetic on the same precise
|
||||
// floating-point model as the authoritative Runtime module.
|
||||
FPSemantics = FPSemanticsMode.Precise;
|
||||
|
||||
PublicDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"Core",
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
});
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"Json",
|
||||
"ImageWrapper",
|
||||
"VoxelForge",
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user