feat(measure): one grid, one flood fill -- the strate measurement pass
Tier 2 slice 1 from COMPOSER-NOTES.md 3.4. A read-only, deterministic, headless measurement of a built strate: sample a coarse grid once, flood-fill the air once, derive every metric from that same grid. What it answers today: air fraction, air component count and largest component share (the swiss-cheese signal), walkable fraction, median feature scale, median vertical clearance -- and whether two points are actually connected. The connectivity verdict is the point. Coarse sampling LIES about it in the dangerous direction: a wall thinner than the sample step vanishes and two sealed caverns read as joined. So VF_AreConnected recovers the actual BFS route and re-walks that one corridor at one-voxel spacing; a true verdict is never returned on coarse evidence alone. It fired: 2 of 21 routes were coarse false positives. The test carries a negative control, because a green suite that cannot fail is what hid the last defect. Two of them, in fact: a direct probe of the known-solid inter-strate gap (AirFraction 0.00042), and -- Codex's improvement on the spec -- a diff-layer box filled solid and measured through VF_MeasureStrate itself, so an inverted sign is caught end to end rather than around the function under test. Build clean, 18 succeeded / 0 warnings / 0 failed. KNOWN, fix in flight: the interior window excludes a whole chunk (32 voxels) at each end to avoid a 4-voxel seal band -- 8x too much. On the fixture's 4-chunk strates that discards the floor, so the reported FlatPlain/CrystalChamber air fractions of 0.999 describe the open band above the floor, not the strate. The engine is right; the window is not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bp7FxoQcSoiuhxjuLr6aBM
This commit is contained in:
1 parent
8cd5face9b
commit
6988e5ace1
4 files changed
+1268
No files matched your search
+13
@@ -302,6 +302,19 @@ This is **where terrain shape lives.**
|
|||||||
> tile by the mesher drops tail octaves on coarse tiles (opt-in `LODOctaveDrop`, default 0 = off);
|
> tile by the mesher drops tail octaves on coarse tiles (opt-in `LODOctaveDrop`, default 0 = off);
|
||||||
> XY-field noise (heightfield/ceiling/relief/moisture) deliberately stays un-biased. §8.10.
|
> XY-field noise (heightfield/ceiling/relief/moisture) deliberately stays un-biased. §8.10.
|
||||||
|
|
||||||
|
### 3.6b Strate measurement — `Public/VoxelStrateMeasure.h` + `Private/VoxelStrateMeasure.cpp`
|
||||||
|
Headless, read-only Tier 2 measurement pass. `VF_MeasureStrate` samples one strate's strict
|
||||||
|
interior into a bounded coarse grid, performs one deterministic 6-connected air flood fill, and
|
||||||
|
derives fractions, components, walkability, feature scale, and clearance from that grid.
|
||||||
|
`VF_AreConnected` recovers a deterministic BFS parent path from the same kind of grid and checks
|
||||||
|
only that route at full voxel resolution before returning true; a coarse false positive sets
|
||||||
|
`bOutCoarseLied`. No UObject state, cache, actor, world, or PIE is required.
|
||||||
|
| Symbol | Role |
|
||||||
|
|--------|------|
|
||||||
|
| `FVoxelStrateMeasureSettings` / `FVoxelStrateMetrics` | Plain settings/result structs for bounded strate sampling and derived measurements. |
|
||||||
|
| `VF_MeasureStrate` | One-grid/one-flood-fill strate metrics; refuses invalid bounds or a grid over `MaxCells`. |
|
||||||
|
| `VF_AreConnected` | Coarse 6-connected BFS plus one full-resolution recheck of its recovered route. |
|
||||||
|
|
||||||
### 3.7 Cave morphology (SDF rooms/tunnels) — `Public/VoxelCaveMorphology.h` + `.cpp`
|
### 3.7 Cave morphology (SDF rooms/tunnels) — `Public/VoxelCaveMorphology.h` + `.cpp`
|
||||||
Header is rich with inline docs. Two namespaces + a per-chunk cache system.
|
Header is rich with inline docs. Two namespaces + a per-chunk cache system.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,506 @@
|
|||||||
|
// VoxelForgeStrateConnectivityTest.cpp
|
||||||
|
// Tier 2: the measurement pass and its coarse-connectivity false-positive guard.
|
||||||
|
|
||||||
|
#if WITH_DEV_AUTOMATION_TESTS
|
||||||
|
|
||||||
|
#include "Misc/AutomationTest.h"
|
||||||
|
|
||||||
|
#include "VoxelForgeTestFixture.h"
|
||||||
|
#include "VoxelStrateMeasure.h"
|
||||||
|
|
||||||
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||||
|
FVoxelForgeStrateConnectivityTest,
|
||||||
|
"VoxelForge.Generation.StrateConnectivity",
|
||||||
|
EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
struct FArchetypeReport
|
||||||
|
{
|
||||||
|
FString Name;
|
||||||
|
int32 Index = INDEX_NONE;
|
||||||
|
FVoxelStrateMetrics First;
|
||||||
|
FVoxelStrateMetrics Second;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FPassageReport
|
||||||
|
{
|
||||||
|
int32 Index = INDEX_NONE;
|
||||||
|
FString Name;
|
||||||
|
FString UpperResult;
|
||||||
|
FString LowerResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
FString ArchetypeName(const FStrateSlot& Slot)
|
||||||
|
{
|
||||||
|
if (Slot.Definition != nullptr)
|
||||||
|
{
|
||||||
|
if (const UEnum* ArchetypeEnum = StaticEnum<ECaveGeneratorType>())
|
||||||
|
{
|
||||||
|
return ArchetypeEnum->GetNameStringByValue(
|
||||||
|
static_cast<int64>(Slot.Definition->GeneratorType));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FString::Printf(TEXT("Strate_%d"), Slot.StrateIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SameFloatBits(float A, float B)
|
||||||
|
{
|
||||||
|
return FMemory::Memcmp(&A, &B, sizeof(float)) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MetricsAreBitIdentical(const FVoxelStrateMetrics& A, const FVoxelStrateMetrics& B)
|
||||||
|
{
|
||||||
|
return A.bValid == B.bValid
|
||||||
|
&& A.RefusalReason == B.RefusalReason
|
||||||
|
&& A.NumSampled == B.NumSampled
|
||||||
|
&& A.NumAir == B.NumAir
|
||||||
|
&& A.NumSolid == B.NumSolid
|
||||||
|
&& SameFloatBits(A.AirFraction, B.AirFraction)
|
||||||
|
&& A.NumAirComponents == B.NumAirComponents
|
||||||
|
&& SameFloatBits(A.LargestComponentShare, B.LargestComponentShare)
|
||||||
|
&& SameFloatBits(A.WalkableFraction, B.WalkableFraction)
|
||||||
|
&& SameFloatBits(A.MedianFeatureScale, B.MedianFeatureScale)
|
||||||
|
&& A.MedianVerticalClearance == B.MedianVerticalClearance;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsStrictInteriorPoint(const FStrateSlot& Slot, const FVector& Point)
|
||||||
|
{
|
||||||
|
const float MinZ = (static_cast<float>(Slot.BottomChunkZ) + 1.0f) * CHUNK_SIZE;
|
||||||
|
const float MaxZ = static_cast<float>(Slot.TopChunkZ) * CHUNK_SIZE;
|
||||||
|
return Point.Z >= MinZ && Point.Z < MaxZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
FVector LargestComponentAnchor(const FStrateSlot& Slot, const FVector2D& CenterXY)
|
||||||
|
{
|
||||||
|
// The fixture's origin spine is the deterministic hub/void anchor. The measurement API
|
||||||
|
// intentionally returns metrics rather than a component coordinate, so this report probes
|
||||||
|
// the known spine point that represents the largest reachable component in this fixture.
|
||||||
|
const float AnchorZ = (static_cast<float>(Slot.BottomChunkZ) + 1.0f) * CHUNK_SIZE + 2.0f;
|
||||||
|
return FVector(CenterXY.X + 2.0f, CenterXY.Y + 2.0f, AnchorZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FullResolutionDirectSegmentHasSolid(
|
||||||
|
const UVoxelGenerator& Generator,
|
||||||
|
const FVector& A,
|
||||||
|
const FVector& B)
|
||||||
|
{
|
||||||
|
const FVector Delta = B - A;
|
||||||
|
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 = A + Delta * Alpha;
|
||||||
|
const float Density = Generator.GetDensityAt(Sample.X, Sample.Y, Sample.Z);
|
||||||
|
if (!FMath::IsFinite(Density) || !(Density > 0.0f))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FindFirstGap(
|
||||||
|
const TArray<FStrateSlot>& Layout,
|
||||||
|
int32& OutGapTopChunkZ,
|
||||||
|
int32& OutGapBottomChunkZ)
|
||||||
|
{
|
||||||
|
for (int32 Index = 0; Index + 1 < Layout.Num(); ++Index)
|
||||||
|
{
|
||||||
|
const int32 GapTop = Layout[Index].BottomChunkZ - 1;
|
||||||
|
const int32 GapBottom = Layout[Index + 1].TopChunkZ + 1;
|
||||||
|
if (GapTop >= GapBottom)
|
||||||
|
{
|
||||||
|
OutGapTopChunkZ = GapTop;
|
||||||
|
OutGapBottomChunkZ = GapBottom;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
float MeasureGapAirFraction(
|
||||||
|
const UVoxelGenerator& Generator,
|
||||||
|
const UVoxelStrateManager& Manager,
|
||||||
|
int32 GapTopChunkZ,
|
||||||
|
int32 GapBottomChunkZ,
|
||||||
|
const FVoxelStrateMeasureSettings& Settings,
|
||||||
|
int64& OutSampleCount,
|
||||||
|
int64& OutAirCount)
|
||||||
|
{
|
||||||
|
OutSampleCount = 0;
|
||||||
|
OutAirCount = 0;
|
||||||
|
|
||||||
|
const float MinX = Settings.CenterXY.X - static_cast<float>(Settings.RadiusInVoxels);
|
||||||
|
const float MinY = Settings.CenterXY.Y - static_cast<float>(Settings.RadiusInVoxels);
|
||||||
|
const float Step = static_cast<float>(Settings.SampleStep);
|
||||||
|
const int32 NumXY = FMath::CeilToInt(
|
||||||
|
(2.0f * static_cast<float>(Settings.RadiusInVoxels)) / Step);
|
||||||
|
const int32 MinGapChunkZ = GapBottomChunkZ;
|
||||||
|
const int32 MaxGapChunkZ = GapTopChunkZ;
|
||||||
|
|
||||||
|
for (int32 Z = MinGapChunkZ * CHUNK_SIZE + Settings.SampleStep / 2;
|
||||||
|
Z < (MaxGapChunkZ + 1) * CHUNK_SIZE;
|
||||||
|
Z += Settings.SampleStep)
|
||||||
|
{
|
||||||
|
for (int32 YIndex = 0; YIndex < NumXY; ++YIndex)
|
||||||
|
{
|
||||||
|
const float Y = FMath::Min(
|
||||||
|
MinY + (static_cast<float>(YIndex) + 0.5f) * Step,
|
||||||
|
Settings.CenterXY.Y + static_cast<float>(Settings.RadiusInVoxels) - 0.5f);
|
||||||
|
for (int32 XIndex = 0; XIndex < NumXY; ++XIndex)
|
||||||
|
{
|
||||||
|
const float X = FMath::Min(
|
||||||
|
MinX + (static_cast<float>(XIndex) + 0.5f) * Step,
|
||||||
|
Settings.CenterXY.X + static_cast<float>(Settings.RadiusInVoxels) - 0.5f);
|
||||||
|
const int32 ChunkZ = FMath::FloorToInt(static_cast<float>(Z) / CHUNK_SIZE);
|
||||||
|
if (!Manager.IsGapChunk(FIntVector(
|
||||||
|
FMath::FloorToInt(X / CHUNK_SIZE),
|
||||||
|
FMath::FloorToInt(Y / CHUNK_SIZE),
|
||||||
|
ChunkZ)))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float Density = Generator.GetDensityAt(X, Y, static_cast<float>(Z));
|
||||||
|
++OutSampleCount;
|
||||||
|
if (FMath::IsFinite(Density) && Density > 0.0f)
|
||||||
|
{
|
||||||
|
++OutAirCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return OutSampleCount > 0
|
||||||
|
? static_cast<float>(static_cast<double>(OutAirCount)
|
||||||
|
/ static_cast<double>(OutSampleCount))
|
||||||
|
: 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
|
||||||
|
{
|
||||||
|
using namespace VoxelForgeTest;
|
||||||
|
|
||||||
|
FTestWorld World;
|
||||||
|
World.Build(/*InSeed=*/1337, /*InGapChunks=*/2);
|
||||||
|
if (!World.IsValid())
|
||||||
|
{
|
||||||
|
AddError(World.WhyInvalid());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FVoxelStrateMeasureSettings Settings;
|
||||||
|
Settings.SampleStep = 4;
|
||||||
|
Settings.RadiusInVoxels = 256;
|
||||||
|
Settings.CenterXY = FVector2D::ZeroVector;
|
||||||
|
Settings.MaxCells = 8000000;
|
||||||
|
Settings.HeadroomCells = 2;
|
||||||
|
|
||||||
|
bool bAllChecksPassed = true;
|
||||||
|
FString Summary = TEXT("VoxelForge strate measurement summary (seed 1337, step 4, radius 256):\n");
|
||||||
|
|
||||||
|
/*
|
||||||
|
* HARD FAILURES — these are the reasons this test exists:
|
||||||
|
*
|
||||||
|
* 1. Negative control: the inter-strate gap is bedrock. If it reports mostly air, the density
|
||||||
|
* sign is inverted and every other number in this file is meaningless.
|
||||||
|
* 2. Vacuity: a main measurement with no samples, no air, or no solid measures nothing.
|
||||||
|
* 3. Range sanity: fractions/components must describe an actual air field.
|
||||||
|
* 4. Determinism: identical inputs must produce bit-identical metrics.
|
||||||
|
* 5. Coarse-lie guard: a coarse route must never be reported connected after a full-resolution
|
||||||
|
* solid sample; the controlled 3-voxel wall below must make bOutCoarseLied fire.
|
||||||
|
*/
|
||||||
|
|
||||||
|
int32 GapTopChunkZ = 0;
|
||||||
|
int32 GapBottomChunkZ = 0;
|
||||||
|
int64 GapSamples = 0;
|
||||||
|
int64 GapAirSamples = 0;
|
||||||
|
const bool bHaveGap = FindFirstGap(
|
||||||
|
World.StrateManager->GetLayout(), GapTopChunkZ, GapBottomChunkZ);
|
||||||
|
float GapAirFraction = 0.0f;
|
||||||
|
if (!bHaveGap)
|
||||||
|
{
|
||||||
|
AddError(TEXT("HARD FAILURE: the fixture has no inter-strate gap for the density-sign negative control."));
|
||||||
|
bAllChecksPassed = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GapAirFraction = MeasureGapAirFraction(
|
||||||
|
*World.Generator,
|
||||||
|
*World.StrateManager,
|
||||||
|
GapTopChunkZ,
|
||||||
|
GapBottomChunkZ,
|
||||||
|
Settings,
|
||||||
|
GapSamples,
|
||||||
|
GapAirSamples);
|
||||||
|
if (GapSamples == 0)
|
||||||
|
{
|
||||||
|
AddError(TEXT("HARD FAILURE: the inter-strate gap negative control sampled zero cells."));
|
||||||
|
bAllChecksPassed = false;
|
||||||
|
}
|
||||||
|
if (GapAirFraction > 0.05f)
|
||||||
|
{
|
||||||
|
AddError(FString::Printf(
|
||||||
|
TEXT("HARD FAILURE: density sign is inverted — the known-solid inter-strate gap "
|
||||||
|
"reported AirFraction %.9g (mostly air), and every other number in this file "
|
||||||
|
"is meaningless."),
|
||||||
|
GapAirFraction));
|
||||||
|
bAllChecksPassed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This second control runs through VF_MeasureStrate itself. It fills the complete sampled
|
||||||
|
// box of the first strate, then requires that the measurement reports almost no air. It is
|
||||||
|
// deliberately separate from the gap probe above: this catches a sign inversion in the new
|
||||||
|
// measurement pass even though the generator-level gap probe uses the correct MC predicate.
|
||||||
|
const FStrateSlot& SolidControlSlot = World.StrateManager->GetLayout()[0];
|
||||||
|
const int32 SolidControlMinZ = (SolidControlSlot.BottomChunkZ + 1) * CHUNK_SIZE;
|
||||||
|
const int32 SolidControlMaxZ = SolidControlSlot.TopChunkZ * CHUNK_SIZE;
|
||||||
|
FVoxelModification SolidControl;
|
||||||
|
SolidControl.Shape = EVoxelBrushShape::Box;
|
||||||
|
SolidControl.Center = FVector(
|
||||||
|
Settings.CenterXY.X,
|
||||||
|
Settings.CenterXY.Y,
|
||||||
|
static_cast<float>(SolidControlMinZ + SolidControlMaxZ) * 0.5f);
|
||||||
|
SolidControl.BoxExtent = FVector(
|
||||||
|
static_cast<float>(Settings.RadiusInVoxels),
|
||||||
|
static_cast<float>(Settings.RadiusInVoxels),
|
||||||
|
static_cast<float>(Settings.RadiusInVoxels));
|
||||||
|
SolidControl.Radius = 1.0f;
|
||||||
|
SolidControl.Falloff = 0.1f;
|
||||||
|
SolidControl.Strength = 100.0f;
|
||||||
|
const TArray<FIntVector> SolidControlChunks = World.DiffLayer->ApplyModification(SolidControl);
|
||||||
|
const FVoxelStrateMetrics SolidControlMetrics = VF_MeasureStrate(
|
||||||
|
*World.Generator, *World.StrateManager, 0, Settings);
|
||||||
|
if (SolidControlChunks.Num() == 0 || !SolidControlMetrics.bValid
|
||||||
|
|| SolidControlMetrics.AirFraction > 0.05f)
|
||||||
|
{
|
||||||
|
AddError(FString::Printf(
|
||||||
|
TEXT("HARD FAILURE: VF_MeasureStrate classified the known-solid control as air "
|
||||||
|
"(AirFraction %.9g); the density sign is inverted and every other number in "
|
||||||
|
"this file is meaningless."),
|
||||||
|
SolidControlMetrics.AirFraction));
|
||||||
|
bAllChecksPassed = false;
|
||||||
|
}
|
||||||
|
World.DiffLayer->Clear();
|
||||||
|
|
||||||
|
TArray<FArchetypeReport> ArchetypeReports;
|
||||||
|
const TArray<FStrateSlot>& Layout = World.StrateManager->GetLayout();
|
||||||
|
ArchetypeReports.Reserve(Layout.Num());
|
||||||
|
for (int32 StrateIndex = 0; StrateIndex < Layout.Num(); ++StrateIndex)
|
||||||
|
{
|
||||||
|
FArchetypeReport& Report = ArchetypeReports.AddDefaulted_GetRef();
|
||||||
|
Report.Index = StrateIndex;
|
||||||
|
Report.Name = ArchetypeName(Layout[StrateIndex]);
|
||||||
|
Report.First = VF_MeasureStrate(
|
||||||
|
*World.Generator, *World.StrateManager, StrateIndex, Settings);
|
||||||
|
Report.Second = VF_MeasureStrate(
|
||||||
|
*World.Generator, *World.StrateManager, StrateIndex, Settings);
|
||||||
|
|
||||||
|
const FVoxelStrateMetrics& Metrics = Report.First;
|
||||||
|
if (!Metrics.bValid)
|
||||||
|
{
|
||||||
|
AddError(FString::Printf(
|
||||||
|
TEXT("HARD FAILURE: %s measurement refused: %s."),
|
||||||
|
*Report.Name,
|
||||||
|
*Metrics.RefusalReason));
|
||||||
|
bAllChecksPassed = false;
|
||||||
|
}
|
||||||
|
if (Metrics.NumSampled == 0 || Metrics.NumAir == 0 || Metrics.NumSolid == 0)
|
||||||
|
{
|
||||||
|
AddError(FString::Printf(
|
||||||
|
TEXT("HARD FAILURE: vacuous %s measurement — sampled=%lld, air=%lld, solid=%lld."),
|
||||||
|
*Report.Name,
|
||||||
|
Metrics.NumSampled,
|
||||||
|
Metrics.NumAir,
|
||||||
|
Metrics.NumSolid));
|
||||||
|
bAllChecksPassed = false;
|
||||||
|
}
|
||||||
|
if (!FMath::IsFinite(Metrics.AirFraction)
|
||||||
|
|| Metrics.AirFraction < 0.0f || Metrics.AirFraction > 1.0f
|
||||||
|
|| !FMath::IsFinite(Metrics.LargestComponentShare)
|
||||||
|
|| Metrics.LargestComponentShare < 0.0f || Metrics.LargestComponentShare > 1.0f
|
||||||
|
|| !FMath::IsFinite(Metrics.WalkableFraction)
|
||||||
|
|| Metrics.WalkableFraction < 0.0f || Metrics.WalkableFraction > 1.0f
|
||||||
|
|| (Metrics.NumAir > 0 && Metrics.NumAirComponents == 0)
|
||||||
|
|| (Metrics.LargestComponentShare == 0.0f && Metrics.AirFraction > 0.05f))
|
||||||
|
{
|
||||||
|
AddError(FString::Printf(
|
||||||
|
TEXT("HARD FAILURE: range/component sanity failed for %s."), *Report.Name));
|
||||||
|
bAllChecksPassed = false;
|
||||||
|
}
|
||||||
|
if (!MetricsAreBitIdentical(Report.First, Report.Second))
|
||||||
|
{
|
||||||
|
AddError(FString::Printf(
|
||||||
|
TEXT("HARD FAILURE: %s measurement was not bit-identical on the second call."),
|
||||||
|
*Report.Name));
|
||||||
|
bAllChecksPassed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ArchetypeReports.Sort([](const FArchetypeReport& A, const FArchetypeReport& B)
|
||||||
|
{
|
||||||
|
if (A.Name == B.Name) return A.Index < B.Index;
|
||||||
|
return A.Name < B.Name;
|
||||||
|
});
|
||||||
|
|
||||||
|
Summary += FString::Printf(
|
||||||
|
TEXT("Negative control: gap chunks [%d..%d], AirFraction=%.9g (%lld/%lld samples air).\n"),
|
||||||
|
GapTopChunkZ,
|
||||||
|
GapBottomChunkZ,
|
||||||
|
GapAirFraction,
|
||||||
|
GapAirSamples,
|
||||||
|
GapSamples);
|
||||||
|
Summary += TEXT("Archetypes (sorted):\n");
|
||||||
|
for (const FArchetypeReport& Report : ArchetypeReports)
|
||||||
|
{
|
||||||
|
const FVoxelStrateMetrics& Metrics = Report.First;
|
||||||
|
Summary += FString::Printf(
|
||||||
|
TEXT(" %s: Air=%.9g, Largest=%.9g, Walkable=%.9g, FeatureScale=%.9g vox, "
|
||||||
|
"Clearance=%d vox, Components=%d, Samples=%lld, Solid=%lld\n"),
|
||||||
|
*Report.Name,
|
||||||
|
Metrics.AirFraction,
|
||||||
|
Metrics.LargestComponentShare,
|
||||||
|
Metrics.WalkableFraction,
|
||||||
|
Metrics.MedianFeatureScale,
|
||||||
|
Metrics.MedianVerticalClearance,
|
||||||
|
Metrics.NumAirComponents,
|
||||||
|
Metrics.NumSampled,
|
||||||
|
Metrics.NumSolid);
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 NumRoutesChecked = 0;
|
||||||
|
int32 NumRoutesRefuted = 0;
|
||||||
|
for (int32 StrateIndex = 0; StrateIndex < Layout.Num(); ++StrateIndex)
|
||||||
|
{
|
||||||
|
const FStrateSlot& Slot = Layout[StrateIndex];
|
||||||
|
const int32 InteriorMinZ = (Slot.BottomChunkZ + 1) * CHUNK_SIZE;
|
||||||
|
const int32 InteriorMaxZ = Slot.TopChunkZ * CHUNK_SIZE;
|
||||||
|
if (InteriorMinZ >= InteriorMaxZ) continue;
|
||||||
|
|
||||||
|
const FVector A(Settings.CenterXY.X + 2.0f, Settings.CenterXY.Y + 2.0f,
|
||||||
|
static_cast<float>(InteriorMinZ + 2));
|
||||||
|
const FVector B(Settings.CenterXY.X + 2.0f, Settings.CenterXY.Y + 2.0f,
|
||||||
|
static_cast<float>(InteriorMaxZ - 2));
|
||||||
|
bool bCoarseLied = false;
|
||||||
|
const bool bConnected = VF_AreConnected(
|
||||||
|
*World.Generator, *World.StrateManager, StrateIndex, A, B, Settings, bCoarseLied);
|
||||||
|
++NumRoutesChecked;
|
||||||
|
if (bCoarseLied) ++NumRoutesRefuted;
|
||||||
|
if (bConnected && FullResolutionDirectSegmentHasSolid(*World.Generator, A, B))
|
||||||
|
{
|
||||||
|
AddError(FString::Printf(
|
||||||
|
TEXT("HARD FAILURE: VF_AreConnected returned true for %s while its full-resolution "
|
||||||
|
"direct walk contained a solid sample."),
|
||||||
|
*ArchetypeName(Slot)));
|
||||||
|
bAllChecksPassed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Controlled negative route: at SampleStep 4, the two coarse centres at X=-2 and X=+2
|
||||||
|
// remain air while the three full-resolution voxels at X=-1,0,+1 are filled solid.
|
||||||
|
const FStrateSlot& WallSlot = Layout[0];
|
||||||
|
const int32 WallInteriorMinZ = (WallSlot.BottomChunkZ + 1) * CHUNK_SIZE;
|
||||||
|
const float WallZ = static_cast<float>(WallInteriorMinZ + 2);
|
||||||
|
const FVector WallA(Settings.CenterXY.X - 2.0f, Settings.CenterXY.Y + 2.0f, WallZ);
|
||||||
|
const FVector WallB(Settings.CenterXY.X + 2.0f, Settings.CenterXY.Y + 2.0f, WallZ);
|
||||||
|
|
||||||
|
FVoxelModification Wall;
|
||||||
|
Wall.Shape = EVoxelBrushShape::Box;
|
||||||
|
Wall.Center = FVector(Settings.CenterXY.X, Settings.CenterXY.Y + 2.0f, WallZ);
|
||||||
|
Wall.BoxExtent = FVector(1.5f, 256.0f, 256.0f);
|
||||||
|
Wall.Radius = 1.0f;
|
||||||
|
Wall.Falloff = 0.1f;
|
||||||
|
Wall.Strength = 100.0f;
|
||||||
|
const TArray<FIntVector> WallChunks = World.DiffLayer->ApplyModification(Wall);
|
||||||
|
if (WallChunks.Num() == 0)
|
||||||
|
{
|
||||||
|
AddError(TEXT("HARD FAILURE: the controlled 3-voxel wall could not be installed."));
|
||||||
|
bAllChecksPassed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bWallCoarseLied = false;
|
||||||
|
const bool bWallConnected = VF_AreConnected(
|
||||||
|
*World.Generator,
|
||||||
|
*World.StrateManager,
|
||||||
|
0,
|
||||||
|
WallA,
|
||||||
|
WallB,
|
||||||
|
Settings,
|
||||||
|
bWallCoarseLied);
|
||||||
|
++NumRoutesChecked;
|
||||||
|
if (bWallCoarseLied) ++NumRoutesRefuted;
|
||||||
|
if (bWallConnected)
|
||||||
|
{
|
||||||
|
AddError(TEXT("HARD FAILURE: VF_AreConnected reported true across a full-resolution solid 3-voxel wall."));
|
||||||
|
bAllChecksPassed = false;
|
||||||
|
}
|
||||||
|
if (!bWallCoarseLied)
|
||||||
|
{
|
||||||
|
AddError(TEXT("HARD FAILURE: the controlled coarse-connectivity lie did not set bOutCoarseLied."));
|
||||||
|
bAllChecksPassed = false;
|
||||||
|
}
|
||||||
|
World.DiffLayer->Clear();
|
||||||
|
|
||||||
|
TArray<FPassageReport> PassageReports;
|
||||||
|
const TArray<FVoxelPassage>& Passages = World.StrateManager->GetPassages();
|
||||||
|
PassageReports.Reserve(Passages.Num());
|
||||||
|
for (int32 PassageIndex = 0; PassageIndex < Passages.Num(); ++PassageIndex)
|
||||||
|
{
|
||||||
|
const FVoxelPassage& Passage = Passages[PassageIndex];
|
||||||
|
FPassageReport& Report = PassageReports.AddDefaulted_GetRef();
|
||||||
|
Report.Index = PassageIndex;
|
||||||
|
Report.Name = FString::Printf(TEXT("%03d"), PassageIndex);
|
||||||
|
|
||||||
|
auto CheckEndpoint = [&](int32 EndpointStrateIndex, const FVector& Endpoint) -> FString
|
||||||
|
{
|
||||||
|
if (!Layout.IsValidIndex(EndpointStrateIndex)) return TEXT("invalid-strate");
|
||||||
|
const FStrateSlot& Slot = Layout[EndpointStrateIndex];
|
||||||
|
if (!IsStrictInteriorPoint(Slot, Endpoint)) return TEXT("out-of-band");
|
||||||
|
|
||||||
|
const FVector Anchor = LargestComponentAnchor(Slot, Settings.CenterXY);
|
||||||
|
bool bCoarseLied = false;
|
||||||
|
const bool bConnected = VF_AreConnected(
|
||||||
|
*World.Generator,
|
||||||
|
*World.StrateManager,
|
||||||
|
EndpointStrateIndex,
|
||||||
|
Endpoint,
|
||||||
|
Anchor,
|
||||||
|
Settings,
|
||||||
|
bCoarseLied);
|
||||||
|
++NumRoutesChecked;
|
||||||
|
if (bCoarseLied) ++NumRoutesRefuted;
|
||||||
|
return bConnected ? TEXT("connected") : TEXT("not-connected");
|
||||||
|
};
|
||||||
|
|
||||||
|
Report.UpperResult = CheckEndpoint(Passage.UpperStrateIndex, Passage.UpperPoint);
|
||||||
|
Report.LowerResult = CheckEndpoint(Passage.LowerStrateIndex, Passage.LowerPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
PassageReports.Sort([](const FPassageReport& A, const FPassageReport& B)
|
||||||
|
{
|
||||||
|
return A.Index < B.Index;
|
||||||
|
});
|
||||||
|
Summary += FString::Printf(
|
||||||
|
TEXT("Connectivity guard: %d routes checked, %d coarse routes refuted at full resolution.\n"),
|
||||||
|
NumRoutesChecked,
|
||||||
|
NumRoutesRefuted);
|
||||||
|
Summary += TEXT("Passages (endpoint -> largest-component spine anchor, sorted):\n");
|
||||||
|
for (const FPassageReport& Report : PassageReports)
|
||||||
|
{
|
||||||
|
Summary += FString::Printf(
|
||||||
|
TEXT(" passage %s: upper=%s, lower=%s\n"),
|
||||||
|
*Report.Name,
|
||||||
|
*Report.UpperResult,
|
||||||
|
*Report.LowerResult);
|
||||||
|
}
|
||||||
|
AddInfo(Summary);
|
||||||
|
|
||||||
|
return bAllChecksPassed;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // WITH_DEV_AUTOMATION_TESTS
|
||||||
@@ -0,0 +1,686 @@
|
|||||||
|
// Read-only, deterministic strate measurement and connectivity checks.
|
||||||
|
|
||||||
|
#include "VoxelStrateMeasure.h"
|
||||||
|
|
||||||
|
#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;
|
||||||
|
|
||||||
|
// 1 = air, 0 = solid. The polarity is deliberately explicit at the sampling site below.
|
||||||
|
TArray<uint8> Air;
|
||||||
|
|
||||||
|
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,
|
||||||
|
int32 StrateIndex,
|
||||||
|
const FVoxelStrateMeasureSettings& Settings,
|
||||||
|
FSampleGrid& OutGrid,
|
||||||
|
FString& OutReason)
|
||||||
|
{
|
||||||
|
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 (Settings.SampleStep <= 0)
|
||||||
|
{
|
||||||
|
return Refuse(OutReason, TEXT("SampleStep must be greater than zero."));
|
||||||
|
}
|
||||||
|
if (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 (!FMath::IsFinite(Settings.CenterXY.X) || !FMath::IsFinite(Settings.CenterXY.Y))
|
||||||
|
{
|
||||||
|
return Refuse(OutReason, TEXT("CenterXY must contain finite voxel coordinates."));
|
||||||
|
}
|
||||||
|
if (Slot.TopChunkZ <= Slot.BottomChunkZ)
|
||||||
|
{
|
||||||
|
return Refuse(OutReason, TEXT("The requested strate has no positive vertical extent."));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude the complete top and bottom chunks. Those outer chunks contain the seal bands;
|
||||||
|
// the measurement is about the strate interior, not the boundary bedrock.
|
||||||
|
const int64 InteriorMinZ = (static_cast<int64>(Slot.BottomChunkZ) + 1) * CHUNK_SIZE;
|
||||||
|
const int64 InteriorMaxZ = static_cast<int64>(Slot.TopChunkZ) * CHUNK_SIZE;
|
||||||
|
const int64 InteriorHeight = InteriorMaxZ - InteriorMinZ;
|
||||||
|
if (InteriorHeight <= 0)
|
||||||
|
{
|
||||||
|
return Refuse(OutReason, TEXT("The strate is too short after excluding its top and bottom chunks."));
|
||||||
|
}
|
||||||
|
|
||||||
|
const int64 XYExtent = static_cast<int64>(Settings.RadiusInVoxels) * 2;
|
||||||
|
const int64 Step = static_cast<int64>(Settings.SampleStep);
|
||||||
|
const int64 NumX64 = CeilDivPositive(XYExtent, Step);
|
||||||
|
const int64 NumY64 = NumX64;
|
||||||
|
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 double MinX64 = static_cast<double>(Settings.CenterXY.X)
|
||||||
|
- static_cast<double>(Settings.RadiusInVoxels);
|
||||||
|
const double MaxX64 = static_cast<double>(Settings.CenterXY.X)
|
||||||
|
+ static_cast<double>(Settings.RadiusInVoxels);
|
||||||
|
const double MinY64 = static_cast<double>(Settings.CenterXY.Y)
|
||||||
|
- static_cast<double>(Settings.RadiusInVoxels);
|
||||||
|
const double MaxY64 = static_cast<double>(Settings.CenterXY.Y)
|
||||||
|
+ static_cast<double>(Settings.RadiusInVoxels);
|
||||||
|
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.Air.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 = 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.
|
||||||
|
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 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,
|
||||||
|
int32& OutLargestComponentCells)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
|
||||||
|
for (int32 Start = 0; Start < Grid.CellCount; ++Start)
|
||||||
|
{
|
||||||
|
if (Grid.Air[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 && 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
OutLargestComponentCells = FMath::Max(OutLargestComponentCells, ComponentCells);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<int32> Components;
|
||||||
|
int32 NumComponents = 0;
|
||||||
|
int32 LargestComponentCells = 0;
|
||||||
|
FloodFillAir(Grid, Components, NumComponents, LargestComponentCells);
|
||||||
|
|
||||||
|
int64 NumAir = 0;
|
||||||
|
for (const uint8 bAir : Grid.Air)
|
||||||
|
{
|
||||||
|
NumAir += bAir != 0u ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.LargestComponentShare = NumAir > 0
|
||||||
|
? static_cast<float>(static_cast<double>(LargestComponentCells)
|
||||||
|
/ static_cast<double>(NumAir))
|
||||||
|
: 0.0f;
|
||||||
|
|
||||||
|
TArray<int32> ClearanceValues;
|
||||||
|
ClearanceValues.Reserve(static_cast<int32>(NumAir));
|
||||||
|
int64 NumWalkable = 0;
|
||||||
|
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FullResolutionAirOnSegment(
|
||||||
|
const UVoxelGenerator& Generator,
|
||||||
|
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 = 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 FSampleGrid& Grid,
|
||||||
|
const FVector& A,
|
||||||
|
const FVector& B,
|
||||||
|
const TArray<int32>& Path)
|
||||||
|
{
|
||||||
|
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, A, Grid.CellCenter(StartX, StartY, StartZ)))
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
Grid.CellCenter(PreviousX, PreviousY, PreviousZ),
|
||||||
|
Grid.CellCenter(CurrentX, CurrentY, CurrentZ)))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return FullResolutionAirOnSegment(Generator, Grid.CellCenter(EndX, EndY, EndZ), B);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FindCoarsePath(
|
||||||
|
const FSampleGrid& Grid,
|
||||||
|
int32 Start,
|
||||||
|
int32 Goal,
|
||||||
|
TArray<int32>& OutPath)
|
||||||
|
{
|
||||||
|
if (Grid.Air[Start] == 0u || Grid.Air[Goal] == 0u)
|
||||||
|
{
|
||||||
|
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 (Grid.Air[Neighbor] != 0u && Parent[Neighbor] == -2)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FVoxelStrateMetrics VF_MeasureStrate(
|
||||||
|
const UVoxelGenerator& Generator,
|
||||||
|
const UVoxelStrateManager& Manager,
|
||||||
|
int32 StrateIndex,
|
||||||
|
const FVoxelStrateMeasureSettings& Settings)
|
||||||
|
{
|
||||||
|
FVoxelStrateMetrics Result;
|
||||||
|
VoxelStrateMeasurePrivate::FSampleGrid Grid;
|
||||||
|
if (!VoxelStrateMeasurePrivate::BuildSampleGrid(
|
||||||
|
Generator, Manager, StrateIndex, Settings, Grid, Result.RefusalReason))
|
||||||
|
{
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
VoxelStrateMeasurePrivate::BuildMetricsFromGrid(Grid, Settings, Result);
|
||||||
|
Result.bValid = Result.NumSampled > 0;
|
||||||
|
if (!Result.bValid)
|
||||||
|
{
|
||||||
|
Result.RefusalReason = TEXT("The measurement grid was empty.");
|
||||||
|
}
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VF_AreConnected(
|
||||||
|
const UVoxelGenerator& Generator,
|
||||||
|
const UVoxelStrateManager& Manager,
|
||||||
|
int32 StrateIndex,
|
||||||
|
const FVector& AVoxel,
|
||||||
|
const FVector& BVoxel,
|
||||||
|
const FVoxelStrateMeasureSettings& Settings,
|
||||||
|
bool& bOutCoarseLied)
|
||||||
|
{
|
||||||
|
bOutCoarseLied = false;
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
VoxelStrateMeasurePrivate::FSampleGrid Grid;
|
||||||
|
FString RefusalReason;
|
||||||
|
if (!VoxelStrateMeasurePrivate::BuildSampleGrid(
|
||||||
|
Generator, Manager, StrateIndex, Settings, Grid, RefusalReason))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 Start = -1;
|
||||||
|
int32 Goal = -1;
|
||||||
|
if (!VoxelStrateMeasurePrivate::FindCellForPoint(Grid, AVoxel, Start)
|
||||||
|
|| !VoxelStrateMeasurePrivate::FindCellForPoint(Grid, BVoxel, Goal))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<int32> Path;
|
||||||
|
if (!VoxelStrateMeasurePrivate::FindCoarsePath(Grid, Start, Goal, Path))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The coarse route is evidence, not a pass. Re-check this one BFS route at one-voxel spacing
|
||||||
|
// in the original voxel field. A single solid sample is the dangerous coarse false positive.
|
||||||
|
if (!VoxelStrateMeasurePrivate::FullResolutionPathIsAir(
|
||||||
|
Generator, Grid, AVoxel, BVoxel, Path))
|
||||||
|
{
|
||||||
|
bOutCoarseLied = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
// Read-only, headless measurements over a coarse sample of one strate.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
|
||||||
|
class UVoxelGenerator;
|
||||||
|
class UVoxelStrateManager;
|
||||||
|
|
||||||
|
/** Settings for the deterministic coarse grid used by the strate measurement pass. */
|
||||||
|
struct VOXELFORGE_API FVoxelStrateMeasureSettings
|
||||||
|
{
|
||||||
|
int32 SampleStep = 4; // Voxels between samples.
|
||||||
|
int32 RadiusInVoxels = 256; // Half-extent in X and Y around CenterXY.
|
||||||
|
FVector2D CenterXY = FVector2D::ZeroVector; // Actor-space voxel coordinates.
|
||||||
|
int32 MaxCells = 8000000; // Refuse a grid larger than this many cells.
|
||||||
|
int32 HeadroomCells = 2; // Air cells above a floor cell required for walkable.
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Metrics derived from one coarse grid and its single air flood fill. */
|
||||||
|
struct VOXELFORGE_API FVoxelStrateMetrics
|
||||||
|
{
|
||||||
|
bool bValid = false;
|
||||||
|
FString RefusalReason;
|
||||||
|
|
||||||
|
int64 NumSampled = 0;
|
||||||
|
int64 NumAir = 0;
|
||||||
|
int64 NumSolid = 0;
|
||||||
|
|
||||||
|
float AirFraction = 0.f;
|
||||||
|
int32 NumAirComponents = 0;
|
||||||
|
float LargestComponentShare = 0.f;
|
||||||
|
float WalkableFraction = 0.f;
|
||||||
|
float MedianFeatureScale = 0.f;
|
||||||
|
int32 MedianVerticalClearance = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measure one strate in actor-space voxel coordinates.
|
||||||
|
*
|
||||||
|
* The pass samples only the interior chunks of the requested strate, flood-fills its air once,
|
||||||
|
* and derives all returned metrics from that same grid. It never mutates either input object.
|
||||||
|
*/
|
||||||
|
VOXELFORGE_API FVoxelStrateMetrics VF_MeasureStrate(
|
||||||
|
const UVoxelGenerator& Generator,
|
||||||
|
const UVoxelStrateManager& Manager,
|
||||||
|
int32 StrateIndex,
|
||||||
|
const FVoxelStrateMeasureSettings& Settings);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test coarse connectivity and then re-check the one recovered coarse route at full resolution.
|
||||||
|
*
|
||||||
|
* bOutCoarseLied is true only when the coarse route existed but a full-resolution sample on that
|
||||||
|
* route was solid (or non-finite), in which case this function always returns false.
|
||||||
|
*/
|
||||||
|
VOXELFORGE_API bool VF_AreConnected(
|
||||||
|
const UVoxelGenerator& Generator,
|
||||||
|
const UVoxelStrateManager& Manager,
|
||||||
|
int32 StrateIndex,
|
||||||
|
const FVector& AVoxel,
|
||||||
|
const FVector& BVoxel,
|
||||||
|
const FVoxelStrateMeasureSettings& Settings,
|
||||||
|
bool& bOutCoarseLied);
|
||||||
Reference in new issue
Block a user