fix(measure): the fragmentation was the instrument -- 309 pockets -> 96

Jahni walked the maze end to end while the metric called it 309
disconnected pockets whose largest held 176 cells, with the arrival mouth
in a THREE-CELL pocket. Direct observation beat the instrument, again.

Cause, found exactly: the stencil demanded a solid cell at precisely
(X,Y,Z-1). No supporting-surface search, no slope tolerance, no step
height. At step 1 a floor that slopes even slightly loses its solid cell
under most of its air, so a walkable corridor dissolved into specks.

Now: a bounded downward search for a COHERENT SUPPORTING SURFACE within a
step-height tolerance (MaxStepHeight 45 cm = 1.8 voxels), a walkable-slope
normal test (WalkableFloorAngle 44 deg), then the full capsule re-tested at
the resolved support height. Deliberately NOT "any solid within N cells",
which would accept ledges and isolated pixels.

⭐ ON THE WALKED MAZE (seed 0, index 50, slot 4):
  components   309 -> 96          largest       176 -> 1401
  fit cells   2801 -> 6668        mouth comps  43/81 -> 147/1401
The 300-pocket world was a measurement artifact. Fifth time this session
the instrument was the suspect; fourth time it was the culprit.

⚠️ AND IT IS NOT SELF-FLATTERING -- a metric change that only ever makes
numbers rise is worthless, so it ships with controls that must REJECT:
  vertical shaft wall      0 fit cells
  isolated 1-voxel ledge   0 fit cells
  overhang underside       0 fit cells
  ramp steeper than 44 deg 0 fit cells
  coarse step 4            refused, not guessed
Open flat-floor control: 6.13% of air cells fit, ONE component of 12,168 --
sensible, since only near-floor poses qualify in a tall room.

⚠️ BUT THE ROUTE STILL DOES NOT EXIST, and that part of H3 survives. The law
is 1/8 before and after -- what changed is its honesty: the one connection
used to be FloatingIslands through FOUR cells, and is now FlatPlain through
2,221 with a zero gap. Seven archetypes still have both mouths in separate
player-fit components 56-169 voxels (14-42 m) apart, and those components
are now large and real (663-2902 cells), not specks.

⇒ So the world HAS substantial connected player-sized space. The passage
MOUTHS are not landing in it. That is the Tier 1 landing problem again,
one level down: VF_SuggestLandingPoint aims at open AIR, and air is not
where a body fits. Next task.

Codex found three errors in my baseline quoting, all mine: I wrote "0/8
connected" while also saying one connects through four cells (it was 1/8),
misquoted SurfaceWorld's gap as 1101 when the saved value is 110.648, and
mixed provenance on the VerticalShafts row.

Build clean, 31 succeeded / 0 failed. The 2 warnings are both deliberate:
the lateral-regions gate, and a test reporting that its own sample "barely
asked its question".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K8V9my1PL68pmQb2gutZZV
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-09-06 04:46:01 +02:00
1 parent cf5f83e9f0
commit 110ae9d0ed
4 files changed
+789 -52

No files matched your search

@@ -4,6 +4,10 @@
#include "Misc/AutomationTest.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/Character.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "VoxelCaveMorphology.h"
#include "VoxelDensityOpStack.h"
#include "VoxelForgeTestFixture.h"
@@ -557,6 +561,126 @@ namespace
const UVoxelGenerator& Generator;
};
class FVerticalWallSampler final : public IVoxelStrateDensitySampler
{
public:
float SampleDensity(float WorldX, float, float) const override
{
// A wall that spans the whole measured height has no lower supporting surface.
// It catches a stencil that treats a nearby solid column as a floor pixel.
return FMath::Abs(WorldX) < 1.0f ? -1.0f : 1.0f;
}
};
class FIsolatedLedgeSampler final : public IVoxelStrateDensitySampler
{
public:
float SampleDensity(float WorldX, float WorldY, float WorldZ) const override
{
// With one-voxel sampling and bounds [-8,8), this is exactly one solid floor cell
// at the (0.5,0.5,0.5) sample. It must fail the 4-of-5 support patch.
const bool bSingleCell = WorldZ < 1.0f
&& FMath::Abs(WorldX) < 1.0f
&& FMath::Abs(WorldY) < 1.0f;
return bSingleCell ? -1.0f : 1.0f;
}
};
class FOverhangSampler final : public IVoxelStrateDensitySampler
{
public:
float SampleDensity(float, float, float WorldZ) const override
{
// Air below an overhang/ceiling, with solid continuing above the measured window.
// A downward-only support search must not use that ceiling as a floor.
return WorldZ >= 4.0f ? -1.0f : 1.0f;
}
};
class FSteepRampSampler final : public IVoxelStrateDensitySampler
{
public:
float SampleDensity(float WorldX, float, float WorldZ) const override
{
// One voxel of rise per one voxel of run is 45 degrees. The project CDO's 44.8
// degree limit must reject the support patch instead of averaging this into floor.
const float FloorHeight = 8.0f + WorldX;
return WorldZ < FloorHeight ? -1.0f : 1.0f;
}
};
bool VF_ApplyProjectCharacterFitDefaults(
FAutomationTestBase& Test,
FVoxelStrateMeasureSettings& InOutSettings)
{
constexpr const TCHAR* CharacterCDOPath =
TEXT("/Game/FirstPerson/Blueprints/BP_FirstPersonCharacter.BP_FirstPersonCharacter_C");
UClass* CharacterClass = LoadObject<UClass>(nullptr, CharacterCDOPath);
if (CharacterClass == nullptr || !CharacterClass->IsChildOf(ACharacter::StaticClass()))
{
Test.AddError(FString::Printf(
TEXT("Could not load the project character CDO at %s."), CharacterCDOPath));
return false;
}
const ACharacter* CharacterCDO = CharacterClass->GetDefaultObject<ACharacter>();
const UCapsuleComponent* Capsule = CharacterCDO != nullptr
? CharacterCDO->GetCapsuleComponent() : nullptr;
const UCharacterMovementComponent* Movement = CharacterCDO != nullptr
? CharacterCDO->GetCharacterMovement() : nullptr;
if (Capsule == nullptr || Movement == nullptr)
{
Test.AddError(TEXT("The project character CDO has no capsule or movement component."));
return false;
}
const float RadiusCentimeters = Capsule->GetUnscaledCapsuleRadius();
const float HalfHeightCentimeters = Capsule->GetUnscaledCapsuleHalfHeight();
const float MaxStepHeightMeters = Movement->MaxStepHeight / 100.0f;
const float WalkableFloorAngleDegrees = Movement->GetWalkableFloorAngle();
if (!FMath::IsFinite(RadiusCentimeters) || RadiusCentimeters <= 0.0f
|| !FMath::IsFinite(HalfHeightCentimeters) || HalfHeightCentimeters <= 0.0f
|| !FMath::IsFinite(MaxStepHeightMeters) || MaxStepHeightMeters < 0.0f
|| !FMath::IsFinite(WalkableFloorAngleDegrees)
|| WalkableFloorAngleDegrees < 0.0f || WalkableFloorAngleDegrees > 90.0f)
{
Test.AddError(TEXT("The project character CDO has invalid fit parameters."));
return false;
}
// Keep the existing 34 cm / 88 cm metric capsule fixed for before/after comparability.
// The task-specific movement limits below are read from the project character CDO.
Test.AddInfo(FString::Printf(
TEXT("PLAYER_CHARACTER capsule CDO reports radius=%.1f cm, half_height=%.1f cm; "
"measurement dimensions remain radius=%.1f cm, half_height=%.1f cm "
"to isolate the stencil change"),
RadiusCentimeters,
HalfHeightCentimeters,
FVoxelPlayerCapsuleConstants::RadiusCentimeters,
FVoxelPlayerCapsuleConstants::HalfHeightCentimeters));
InOutSettings.PlayerMaxStepHeightMeters = MaxStepHeightMeters;
InOutSettings.PlayerWalkableFloorAngleDegrees = WalkableFloorAngleDegrees;
return true;
}
int32 VF_CountSupportFootprintColumns(float RadiusVoxels)
{
const int32 MaxOffset = FMath::CeilToInt(RadiusVoxels);
int32 Count = 0;
for (int32 Y = -MaxOffset; Y <= MaxOffset; ++Y)
{
for (int32 X = -MaxOffset; X <= MaxOffset; ++X)
{
if (static_cast<float>(X * X + Y * Y)
<= RadiusVoxels * RadiusVoxels + KINDA_SMALL_NUMBER)
{
++Count;
}
}
}
return Count;
}
float VF_BoundarySealForCandidate(const FVoxelStrateComposerCandidate& Candidate)
{
switch (Candidate.Archetype)
@@ -639,6 +763,11 @@ bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters)
AddError(World.WhyInvalid());
return false;
}
TestTrue(TEXT("scale diagnosis keeps WorldRadiusVoxels at zero"),
World.Settings->WorldRadiusVoxels == 0.0f
&& World.Generator->WorldRadiusVoxels == 0.0f);
TestFalse(TEXT("scale diagnosis keeps lateral regions gated off"),
VF_LateralRegionsAreShippable());
const FVoxelStrateComposerCandidate MazeCandidate = VF_RollStrateCandidate(
Corpus, DiagnosisSeed, MazeCandidateIndex, false);
@@ -757,9 +886,42 @@ bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters)
ShaftCore.Negative.CrossingRadiusVoxels * 0.25f));
}
FVoxelStrateMeasureSettings CharacterFitSettings;
if (!VF_ApplyProjectCharacterFitDefaults(*this, CharacterFitSettings))
{
return false;
}
const float MinimumWalkableNormalZ = FMath::Cos(FMath::DegreesToRadians(
CharacterFitSettings.PlayerWalkableFloorAngleDegrees));
const int32 SupportFootprintColumns = VF_CountSupportFootprintColumns(
CharacterFitSettings.PlayerCapsuleRadiusVoxels);
const int32 RequiredSupportColumns = FMath::Clamp(
FMath::CeilToInt(CharacterFitSettings.PlayerSupportPatchMinCoverageFraction
* static_cast<float>(SupportFootprintColumns)),
1, SupportFootprintColumns);
const int32 MaxDownwardSearchCells = FMath::CeilToInt(
CharacterFitSettings.PlayerMaxStepHeightMeters
/ FVoxelPlayerCapsuleConstants::VoxelSizeMeters);
AddInfo(FString::Printf(
TEXT("PLAYER_CHARACTER movement source=%s: MaxStepHeight=%.3f m "
"(%.3f vox; search<=%d cells), WalkableFloorAngle=%.1f deg "
"(normal_z>=%.6f); support_patch=%.1f%% requires %d/%d integer footprint columns "
"and always requires the centre"),
TEXT("/Game/FirstPerson/Blueprints/BP_FirstPersonCharacter CDO"),
CharacterFitSettings.PlayerMaxStepHeightMeters,
CharacterFitSettings.PlayerMaxStepHeightMeters
/ FVoxelPlayerCapsuleConstants::VoxelSizeMeters,
MaxDownwardSearchCells,
CharacterFitSettings.PlayerWalkableFloorAngleDegrees,
MinimumWalkableNormalZ,
CharacterFitSettings.PlayerSupportPatchMinCoverageFraction * 100.0f,
RequiredSupportColumns,
SupportFootprintColumns));
// H2 — a known-open control with a solid floor. This must produce non-zero fit cells.
FEmptyRoomSampler EmptyRoom;
FVoxelStrateMeasureSettings ControlSettings;
FVoxelStrateMeasureSettings ControlSettings = CharacterFitSettings;
ControlSettings.SampleStep = 1;
ControlSettings.RadiusInVoxels = 40; // 80 voxels = 20 m across.
ControlSettings.CenterXY = FVector2D::ZeroVector;
@@ -781,12 +943,65 @@ bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters)
ControlMetrics.NumTraversableComponents,
static_cast<long long>(ControlMetrics.LargestTraversableComponentCells),
ControlMetrics.TraversableComponentShare));
FVoxelStrateMeasureSettings CoarseControlSettings = ControlSettings;
CoarseControlSettings.SampleStep = 4;
const FVoxelStrateMetrics CoarseControlMetrics = VF_MeasureStrateWithSampler(
EmptyRoom, 0, 32, 0.0f, CoarseControlSettings, nullptr);
const bool bCoarseRefused = CoarseControlMetrics.bValid
&& !CoarseControlMetrics.bPlayerFitResolved
&& CoarseControlMetrics.PlayerFitRefusalReason.Contains(TEXT("SampleStep=4"));
TestTrue(TEXT("coarse player-fit control refuses unresolved capsule fit"), bCoarseRefused);
AddInfo(FString::Printf(
TEXT("CONTROL coarse_step4_player_fit: refused=%s reason=%s"),
bCoarseRefused ? TEXT("true") : TEXT("false"),
CoarseControlMetrics.PlayerFitRefusalReason.IsEmpty()
? TEXT("none") : *CoarseControlMetrics.PlayerFitRefusalReason));
// A permissive stencil must fail these deliberately pathological supports. The wall has no
// downward transition, the ledge covers only 1/5 of the footprint, and the overhang has
// solid above rather than below the candidate. All controls use the same character-derived
// settings and a bounded 16x16x12-voxel window.
FVoxelStrateMeasureSettings RejectionSettings = CharacterFitSettings;
RejectionSettings.SampleStep = 1;
RejectionSettings.RadiusInVoxels = 8;
RejectionSettings.CenterXY = FVector2D::ZeroVector;
RejectionSettings.MaxCells = 100000;
RejectionSettings.InteriorMarginVoxels = 0;
auto RunStencilRejectionControl = [this, &RejectionSettings](
const TCHAR* Label, const IVoxelStrateDensitySampler& Sampler)
{
const FVoxelStrateMetrics Metrics = VF_MeasureStrateWithSampler(
Sampler, 0, 12, 0.0f, RejectionSettings, nullptr);
const bool bRejected = Metrics.bValid && Metrics.bPlayerFitResolved
&& Metrics.NumPlayerFitCells == 0;
TestTrue(FString::Printf(TEXT("stencil rejects %s"), Label), bRejected);
AddInfo(FString::Printf(
TEXT("CONTROL stencil_rejection %s: rejected=%s fit_cells=%lld "
"resolved=%s reason=%s"),
Label,
bRejected ? TEXT("true") : TEXT("false"),
static_cast<long long>(Metrics.NumPlayerFitCells),
Metrics.bPlayerFitResolved ? TEXT("true") : TEXT("false"),
Metrics.PlayerFitRefusalReason.IsEmpty()
? TEXT("none") : *Metrics.PlayerFitRefusalReason));
};
FVerticalWallSampler VerticalWall;
FIsolatedLedgeSampler IsolatedLedge;
FOverhangSampler Overhang;
FSteepRampSampler SteepRamp;
RunStencilRejectionControl(TEXT("vertical_shaft_wall"), VerticalWall);
RunStencilRejectionControl(TEXT("isolated_1_voxel_ledge"), IsolatedLedge);
RunStencilRejectionControl(TEXT("overhang_underside"), Overhang);
RunStencilRejectionControl(TEXT("over_44_degree_ramp"), SteepRamp);
AddInfo(FString::Printf(
TEXT("PLAYER_STENCIL radius=%.3f voxels (%.2f m), half_height=%.3f voxels "
"(%.2f m), height=%.3f voxels (%.2f m); rows=ceil(height)=%d "
"relative rows 0..%d; floor=air anchor with exactly one solid cell at Z-1; "
"no floor tolerance; horizontal integer offsets are center plus four cardinals "
"in rows 0..6 and center-only forced in row 7; HeadroomCells=%d is legacy-only"),
"relative rows 0..%d; support=nearest scalar solid-to-air surface searched "
"down <=%.3f m/%d cells; support patch=%d/%d columns with centre required; "
"walkable slope<=%.1f deg; capsule clearance is re-tested at resolved support; "
"HeadroomCells=%d is legacy-only"),
ControlSettings.PlayerCapsuleRadiusVoxels,
ControlSettings.PlayerCapsuleRadiusVoxels * 0.25f,
ControlSettings.PlayerCapsuleHalfHeightVoxels,
@@ -795,6 +1010,11 @@ bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters)
FVoxelPlayerCapsuleConstants::HeightVoxels * 0.25f,
FMath::CeilToInt(2.0f * ControlSettings.PlayerCapsuleHalfHeightVoxels),
FMath::CeilToInt(2.0f * ControlSettings.PlayerCapsuleHalfHeightVoxels) - 1,
ControlSettings.PlayerMaxStepHeightMeters,
MaxDownwardSearchCells,
RequiredSupportColumns,
SupportFootprintColumns,
ControlSettings.PlayerWalkableFloorAngleDegrees,
ControlSettings.HeadroomCells));
// H3 — the eight fixed showcase identities, measured at step 1 on a mouth-fitted window.
@@ -818,13 +1038,44 @@ bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters)
{ ECaveGeneratorType::VerticalShafts, 0, 34, FTestWorld::SlotFlatPlain, TEXT("VerticalShafts") },
};
// Baseline rows from the pre-fix fitted-window report. The supplied VerticalShafts row is
// retained verbatim (it came from the owner's scale report); it is deliberately labelled as
// a comparison row rather than recomputed by this test.
struct FShowcaseBaseline
{
int64 FitCells;
int32 Components;
int64 LargestComponent;
int64 ArrivalComponent;
int64 DepartureComponent;
float MouthGap;
const TCHAR* Law;
};
const FShowcaseBaseline Baselines[] = {
{655, 197, 24, 1, 1, 47.529f, TEXT("NotConnectedAtThisResolution")},
{1116, 45, 259, 1, 1, 48.104f, TEXT("NotConnectedAtThisResolution")},
{4, 2, 3, 1, 1, 0.000f, TEXT("Connected")},
{2801, 309, 176, 43, 81, 206.630f, TEXT("NotConnectedAtThisResolution")},
{713, 520, 6, 1, 1, 110.648f, TEXT("NotConnectedAtThisResolution")},
{2748, 467, 1292, 2, 1, 164.405f, TEXT("NotConnectedAtThisResolution")},
{337, 86, 35, 3, 11, 142.499f, TEXT("NotConnectedAtThisResolution")},
{1156, 112, 96, 3, 36, 94.000f, TEXT("NotConnectedAtThisResolution")},
};
static_assert(UE_ARRAY_COUNT(Selections) == UE_ARRAY_COUNT(Baselines),
"H3 before/after rows must remain paired.");
int32 NumH3Connected = 0;
int32 NumH3DegenerateConnected = 0;
int32 NumH3Measured = 0;
for (const FShowcaseSelection& Selection : Selections)
{
const FVoxelStrateComposerCandidate Candidate = VF_RollStrateCandidate(
Corpus, Selection.Seed, Selection.CandidateIndex, false);
TestTrue(FString::Printf(TEXT("H3 %s candidate identity is valid"), Selection.Label),
Candidate.bValid && Candidate.Archetype == Selection.Archetype);
if (!Candidate.bValid || Candidate.Archetype != Selection.Archetype)
Candidate.bValid && Candidate.Archetype == Selection.Archetype
&& Candidate.Regions.IsSingleRegion());
if (!Candidate.bValid || Candidate.Archetype != Selection.Archetype
|| !Candidate.Regions.IsSingleRegion())
{
continue;
}
@@ -856,7 +1107,7 @@ bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters)
continue;
}
FVoxelStrateMeasureSettings Settings;
FVoxelStrateMeasureSettings Settings = CharacterFitSettings;
Settings.SampleStep = 1;
Settings.RadiusInVoxels = 64;
Settings.CenterXY = FVector2D(
@@ -887,7 +1138,56 @@ bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters)
VF_BoundarySealForCandidate(Candidate), ArrivalPoint, DeparturePoint,
Settings, &Metrics);
VF_ReportPlayerFitRow(*this, Selection.Label, Metrics, Diagnostics);
++NumH3Measured;
if (Diagnostics.Result == EVoxelConnectivityResult::Connected)
{
++NumH3Connected;
if (Metrics.NumPlayerFitCells <= 4)
{
++NumH3DegenerateConnected;
}
}
const FShowcaseBaseline& Before = Baselines[&Selection - Selections];
FString DeltaReport = TEXT("H3_DELTA step1 ");
DeltaReport += Selection.Label;
DeltaReport += FString::Printf(
TEXT(": before=%lld/%d/%lld/%lld/%lld/%.3f/"),
static_cast<long long>(Before.FitCells), Before.Components,
static_cast<long long>(Before.LargestComponent),
static_cast<long long>(Before.ArrivalComponent),
static_cast<long long>(Before.DepartureComponent), Before.MouthGap);
DeltaReport += Before.Law;
DeltaReport += FString::Printf(
TEXT("; after=%lld/%d/%lld/%lld/%lld/%.3f/"),
static_cast<long long>(Metrics.NumPlayerFitCells), Metrics.NumTraversableComponents,
static_cast<long long>(Metrics.LargestTraversableComponentCells),
static_cast<long long>(Diagnostics.StartComponentCells),
static_cast<long long>(Diagnostics.GoalComponentCells),
Diagnostics.StartToGoalComponentDistanceCells);
DeltaReport += VF_DiagnosisConnectivityName(Diagnostics.Result);
DeltaReport += FString::Printf(
TEXT("; delta_fit=%lld delta_components=%d delta_largest=%lld "
"delta_arrival=%lld delta_departure=%lld delta_gap=%.3f"),
static_cast<long long>(Metrics.NumPlayerFitCells - Before.FitCells),
Metrics.NumTraversableComponents - Before.Components,
static_cast<long long>(Metrics.LargestTraversableComponentCells - Before.LargestComponent),
static_cast<long long>(Diagnostics.StartComponentCells - Before.ArrivalComponent),
static_cast<long long>(Diagnostics.GoalComponentCells - Before.DepartureComponent),
Diagnostics.StartToGoalComponentDistanceCells - Before.MouthGap);
AddInfo(DeltaReport);
}
// The old tally's one Connected row was the four-cell FloatingIslands pocket. Keep that
// degenerate fact visible beside the new tally so a tiny connected island is not mistaken for
// an instrument-wide success.
AddInfo(FString::Printf(
TEXT("LAW_TALLY restricted step1: before_connected=1/8 "
"before_connected_through_4_cells=1; after_connected=%d/%d "
"after_connected_through_4_cells=%d measured=%d/8"),
NumH3Connected,
UE_ARRAY_COUNT(Selections),
NumH3DegenerateConnected,
NumH3Measured));
return true;
}
@@ -1957,7 +1957,15 @@ namespace VoxelSeasonCompositionPrivate
&& FMath::IsFinite(Settings.PlayerCapsuleRadiusVoxels)
&& Settings.PlayerCapsuleRadiusVoxels > 0.0f
&& FMath::IsFinite(Settings.PlayerCapsuleHalfHeightVoxels)
&& Settings.PlayerCapsuleHalfHeightVoxels > 0.0f;
&& Settings.PlayerCapsuleHalfHeightVoxels > 0.0f
&& FMath::IsFinite(Settings.PlayerMaxStepHeightMeters)
&& Settings.PlayerMaxStepHeightMeters >= 0.0f
&& FMath::IsFinite(Settings.PlayerWalkableFloorAngleDegrees)
&& Settings.PlayerWalkableFloorAngleDegrees >= 0.0f
&& Settings.PlayerWalkableFloorAngleDegrees <= 90.0f
&& FMath::IsFinite(Settings.PlayerSupportPatchMinCoverageFraction)
&& Settings.PlayerSupportPatchMinCoverageFraction > 0.0f
&& Settings.PlayerSupportPatchMinCoverageFraction <= 1.0f;
}
bool IsBossSlot(int32 DepthIndex, int32 TotalStrates, int32 Interval)
+448 -37
View File
@@ -34,7 +34,8 @@ namespace VoxelStrateMeasurePrivate
// 1 = air, 0 = solid. The polarity is deliberately explicit at the sampling site below.
TArray<uint8> Air;
// Optional exact scalar samples retained only for an explicit preview capture.
// 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
@@ -587,6 +588,302 @@ namespace VoxelStrateMeasurePrivate
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,
@@ -594,9 +891,9 @@ namespace VoxelStrateMeasurePrivate
float Radius,
float HalfHeight)
{
// The capsule is upright and its centre is HalfHeight above the top face of the solid
// floor cell. This is a direct capsule test in the fine voxel lattice; it deliberately
// does not infer occupancy from Manhattan distance.
// 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);
@@ -682,10 +979,9 @@ namespace VoxelStrateMeasurePrivate
}
}
// The full-height floor rule still requires one clear voxel in rows whose voxel
// centre lies outside the rounded cap (normally the top partial voxel of a 7.04
// voxel player). This is what makes 176 cm a full-height requirement rather than a
// 7-centre-point approximation.
// 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);
@@ -712,6 +1008,62 @@ namespace VoxelStrateMeasurePrivate
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))
{
@@ -719,6 +1071,12 @@ namespace VoxelStrateMeasurePrivate
}
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)
@@ -726,47 +1084,98 @@ namespace VoxelStrateMeasurePrivate
for (int32 X = 0; X < Grid.NumX; ++X)
{
const int32 Current = Grid.Index(X, Y, Z);
if (Grid.Air[Current] == 0u
|| Grid.Air[Grid.Index(X, Y, Z - 1)] != 0u)
// 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;
}
bool bFits = true;
for (const FPlayerCapsuleStencilRow& Row : StencilRows)
int32 NumSupported = 0;
for (int32 SupportIndex = 0;
SupportIndex < SupportOffsets.Num();
++SupportIndex)
{
const int32 TargetZ = Z + Row.RelativeZ;
if (TargetZ < 0 || TargetZ >= Grid.NumZ)
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)
{
bFits = false;
break;
continue;
}
for (const FIntPoint& Offset : Row.HorizontalOffsets)
float SurfaceHeight = 0.0f;
int32 SurfaceAnchorZ = INDEX_NONE;
if (FindSupportSurfaceBelow(
Grid,
SupportX,
SupportY,
Z,
MaxDownwardSearchCells,
MaxStepHeightVoxels,
SurfaceHeight,
SurfaceAnchorZ))
{
const int32 TargetX = X + Offset.X;
const int32 TargetY = Y + Offset.Y;
if (TargetX < 0 || TargetX >= Grid.NumX
|| TargetY < 0 || TargetY >= Grid.NumY
|| Grid.Air[Grid.Index(TargetX, TargetY, TargetZ)] == 0u)
{
bFits = false;
break;
}
}
if (!bFits)
{
break;
bSupported[SupportIndex] = 1u;
SupportHeights[SupportIndex] = SurfaceHeight;
SupportAnchorZs[SupportIndex] = SurfaceAnchorZ;
++NumSupported;
}
}
if (bFits)
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;
}
@@ -1341,8 +1750,8 @@ namespace VoxelStrateMeasurePrivate
if (EligibilityMask != nullptr)
{
// A player-fit route is deliberately a graph over floor anchors, not a second air
// route with a coarse-cell illusion. The mask was built at SampleStep=1 and every
// 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)
{
@@ -1797,7 +2206,7 @@ namespace VoxelStrateMeasurePrivate
if (!BuildSampleGrid(Generator, Manager, Sampler, StrateIndex,
ExplicitBottomWorldZ, ExplicitTopWorldZ,
ExplicitBoundarySealThickness, bUseExplicitBounds,
Settings, false, Grid, RefusalReason))
Settings, bPlayerFitRestricted, Grid, RefusalReason))
{
if (OutDiagnostics != nullptr)
{
@@ -1909,7 +2318,8 @@ FVoxelStrateMetrics VF_MeasureStrate(
VoxelStrateMeasurePrivate::FSampleGrid Grid;
if (!VoxelStrateMeasurePrivate::BuildSampleGrid(
&Generator, &Manager, nullptr, StrateIndex, 0, 0, 0.0f, false,
Settings, OutSampleGrid != nullptr, Grid, Result.RefusalReason))
Settings, OutSampleGrid != nullptr || Settings.SampleStep == 1,
Grid, Result.RefusalReason))
{
return Result;
}
@@ -1953,7 +2363,8 @@ FVoxelStrateMetrics VF_MeasureStrateWithSampler(
VoxelStrateMeasurePrivate::FSampleGrid Grid;
if (!VoxelStrateMeasurePrivate::BuildSampleGrid(
nullptr, nullptr, &Sampler, INDEX_NONE, StrateBottomWorldZ, StrateTopWorldZ,
BoundarySealThickness, true, Settings, OutSampleGrid != nullptr,
BoundarySealThickness, true, Settings,
OutSampleGrid != nullptr || Settings.SampleStep == 1,
Grid, Result.RefusalReason))
{
return Result;
+25 -7
View File
@@ -8,18 +8,27 @@ class UVoxelGenerator;
class UVoxelStrateManager;
/**
* The confirmed base ACharacter capsule in voxel-space defaults.
* The pinned metric capsule dimensions in voxel-space.
*
* 25 cm/voxel is the authored world scale: radius = 34 cm / 25 = 1.36 voxels and
* half-height = 88 cm / 25 = 3.52 voxels, hence a 176 cm / 25 = 7.04 voxel total height.
* The measure settings copy these values so a future character can change the query without
* changing generation.
* changing generation. The diagnosis test separately reads movement limits from the project's
* character CDO.
*/
struct VOXELFORGE_API FVoxelPlayerCapsuleConstants
{
static constexpr float VoxelSizeCentimeters = 25.0f;
static constexpr float VoxelSizeMeters = VoxelSizeCentimeters / 100.0f;
static constexpr float RadiusCentimeters = 34.0f;
static constexpr float HalfHeightCentimeters = 88.0f;
// UCharacterMovementComponent's UE 5.7 default. Callers that have a character CDO should
// copy its authored value into PlayerMaxStepHeightMeters.
static constexpr float MaxStepHeightCentimeters = 45.0f;
static constexpr float MaxStepHeightMeters = MaxStepHeightCentimeters / 100.0f;
// UE 5.7's WalkableFloorZ=0.71 resolves to about 44.8 degrees; a character CDO may expose
// the precise angle. The diagnosis test copies that precise project value when available.
static constexpr float WalkableFloorAngleDegrees = 44.8f;
static constexpr float RadiusVoxels = RadiusCentimeters / VoxelSizeCentimeters;
static constexpr float HalfHeightVoxels = HalfHeightCentimeters / VoxelSizeCentimeters;
static constexpr float HeightVoxels = 2.0f * HalfHeightVoxels;
@@ -61,7 +70,7 @@ struct VOXELFORGE_API FVoxelStrateMeasureSettings
float CoverMarginVoxels = 48.0f;
int32 MaxCells = 8000000; // Refuse a grid larger than this many cells.
int32 MaxRouteRetries = 16; // Alternate coarse routes to full-resolution-check after the first route.
int32 HeadroomCells = 2; // Air cells above a floor cell required for walkable.
int32 HeadroomCells = 2; // Legacy walkable-column metric only.
int32 InteriorMarginVoxels = -1; // <0 = derive from BoundarySealThickness (2x, clamped); explicit 0 includes the seal.
// Base ACharacter defaults at 25 cm/voxel: 34 cm radius / 25 = 1.36 voxels and
@@ -69,6 +78,15 @@ struct VOXELFORGE_API FVoxelStrateMeasureSettings
// generation values, so another playable capsule can be measured without a code change.
float PlayerCapsuleRadiusVoxels = FVoxelPlayerCapsuleConstants::RadiusVoxels;
float PlayerCapsuleHalfHeightVoxels = FVoxelPlayerCapsuleConstants::HalfHeightVoxels;
// Character-derived fit parameters. MaxStepHeight is deliberately named and stored in
// metres, matching UCharacterMovementComponent's authored unit. The measurement converts it
// to voxels only for the bounded downward support search.
float PlayerMaxStepHeightMeters = FVoxelPlayerCapsuleConstants::MaxStepHeightMeters;
float PlayerWalkableFloorAngleDegrees = FVoxelPlayerCapsuleConstants::WalkableFloorAngleDegrees;
// A support patch is sampled at the integer columns covered by the capsule radius. 0.75 with
// the default 1.36-voxel radius requires 4 of 5 columns, including the centre column.
float PlayerSupportPatchMinCoverageFraction = 0.75f;
};
/**
@@ -163,16 +181,16 @@ struct VOXELFORGE_API FVoxelStrateMetrics
int32 MedianVerticalClearance = 0;
// Player-fit metrics are answered only by a one-voxel grid. A coarse grid refuses to infer
// clearance because SampleStep=4 cannot resolve a 1.36-voxel radius capsule.
// support or clearance because SampleStep=4 cannot resolve a 1.36-voxel radius capsule.
bool bPlayerFitResolved = false;
FString PlayerFitRefusalReason;
int64 NumPlayerFitCells = 0;
float PlayerFitFraction = 0.0f; // Player-fitting floor anchors / all air cells.
float PlayerFitFraction = 0.0f; // Accepted free supported poses / all air cells.
int32 NumTraversableComponents = 0;
int64 LargestTraversableComponentCells = 0;
float TraversableComponentShare = 0.0f; // Largest player-fit component / player-fit cells.
// Minimum horizontal Chebyshev clearance, in voxels, among player-fit floor anchors. This
// is a diagnostic only; player occupancy itself uses a direct capsule stencil, never the old
// Minimum horizontal Chebyshev clearance, in voxels, among accepted supported poses. This is
// a diagnostic only; player occupancy itself uses a direct capsule stencil, never the old
// Manhattan distance transform.
float MinimumPlayerClearanceVoxels = 0.0f;