feat(descent): landing ROOMS and a walkable tunnel -- per Jahni's correction

Jahni corrected the design: "the connection between level is a tiny tunnel
connecting both strate, not vertical, it needs to be walkable" and "'the
spine' shouldn't be carved except optionally from the top first strate,
then it's used to have a landing room".

COMPOSER-NOTES §6.8 had said the spine "provides a clean landing space, not
a continuous open shaft" -- and the implementation carved a vertical
cylinder anyway. A vertical cylinder has no walkable floor except at its
bottom, so a carve guaranteeing open AIR guaranteed nothing STANDABLE.
That one omission is behind five of my dead hypotheses this week.

Now:
  origin spine   a finite rounded ROOM per strate, no vertical bore.
                 radius 14 -> 8 m wide, 7.5 m tall
  descent        a walkable tunnel, single-file by design: 3 m mouth,
                 2.5 m waist, gradient capped at a NAMED 15 deg
  exotic styles  VerticalShaft / SpiralDescent / CascadingDrops /
                 CrackCrevice preserved, no longer the default

⚠️ THE GEOMETRY BITES, and it is worth Jahni's eye: at 15 deg a 32 m drop
needs 119.4 m of horizontal run -- about 59.7 m per switchback leg. That is
a long walk between strates. It is the honest consequence of "walkable",
not a tuning artifact, and the alternative is a steeper number.

⭐ AND THE ROOT CAUSE GOT NAMED A THIRD TIME, by Codex mid-run: "the round
tube was hollow air but had no authored support strip, so 'open' still did
not mean 'standable'." Tunnels now carry a floor band, and the root join
terminates in the finite origin room rather than an annular shaft hub.

Measured, commandlet sweep, 8 archetypes x 8 seeds, 64/64 valid:
  origin column has player-fit cells   17/64 -> 63/64
  origin column reachable               0/64 ->  7/64
  arrival landing probes            315/320 fit AND in the arrival component
  tunnel audit    5,916/5,916 floor and capsule samples pass; zero gradient,
                  floor, capsule, geometry or seal failures
  narrow-gap events 0        box verdicts 14 checked, 0 violations

⚠️ arrival->departure is 6/64. The landings and tunnels are now provably
walkable; what remains is the INTRA-STRATE vertical connectivity problem
found in 6b91669 -- floors at different levels inside a strate with nothing
joining them. Separate and larger. Not tuned toward.

⚠️ PROCESS: this took 5.7 hours, of which Codex used 4 SECONDS of CPU. The
rest was polling the 73-minute automation suite at ~10% machine load. The
commandlet exists to avoid exactly that. New rule recorded: iterate with the
commandlet, full suite ONCE at the end.

Editor + Shipping builds clean. Automation 32/32, 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-09-08 02:14:37 +02:00
1 parent 6b916699af
commit 55bf807f60
19 files changed
+1996 -334

No files matched your search

@@ -10,6 +10,7 @@
#include "VoxelCaveMorphology.h"
#include "VoxelDensityPrimitives.h"
#include "VoxelForgeTestFixture.h"
#include "VoxelPassageGeometry.h"
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FVoxelForgePassageLandsInOpenSpaceTest,
@@ -89,6 +90,366 @@ namespace
return 0.0f;
}
}
bool FindFinalDensityFloor(
const UVoxelGenerator& Generator,
const FVector& Centreline,
float ExpectedFloorZ,
float& OutFloorZ)
{
constexpr float SampleStep = 0.25f;
const float ExpectedAt = Generator.GetDensityAt(
Centreline.X, Centreline.Y, ExpectedFloorZ);
const float ExpectedBelow = Generator.GetDensityAt(
Centreline.X, Centreline.Y, ExpectedFloorZ - 0.75f);
const float ExpectedAbove = Generator.GetDensityAt(
Centreline.X, Centreline.Y, ExpectedFloorZ + 0.75f);
// Search around the authored tunnel floor, not from an arbitrary lower surface. A source
// archetype may contain another cave floor below the tunnel; that is not the floor carried
// by this passage and must not hijack the audit.
const float StartZ = ExpectedFloorZ - 4.0f;
const float EndZ = ExpectedFloorZ + 4.0f;
const int32 NumSteps = FMath::Max(
1, FMath::CeilToInt((EndZ - StartZ) / SampleStep));
float PreviousZ = StartZ;
float PreviousDensity = Generator.GetDensityAt(
Centreline.X, Centreline.Y, PreviousZ);
if (!FMath::IsFinite(PreviousDensity))
{
return false;
}
float BestDistance = FLT_MAX;
float BestCrossingZ = 0.0f;
for (int32 Step = 1; Step <= NumSteps; ++Step)
{
const float Alpha = static_cast<float>(Step)
/ static_cast<float>(NumSteps);
const float CurrentZ = FMath::Lerp(StartZ, EndZ, Alpha);
const float CurrentDensity = Generator.GetDensityAt(
Centreline.X, Centreline.Y, CurrentZ);
if (!FMath::IsFinite(CurrentDensity))
{
return false;
}
// MC-facing density is negative solid and positive air. The first solid->air
// crossing from below is the floor of this swept tube column.
if (PreviousDensity <= 0.0f && CurrentDensity > 0.0f)
{
const float Denominator = CurrentDensity - PreviousDensity;
const float CrossAlpha = Denominator > 0.0f
? FMath::Clamp(-PreviousDensity / Denominator, 0.0f, 1.0f)
: 0.0f;
const float CrossingZ = FMath::Lerp(
PreviousZ, CurrentZ, CrossAlpha);
const float Distance = FMath::Abs(CrossingZ - ExpectedFloorZ);
if (Distance < BestDistance)
{
BestDistance = Distance;
BestCrossingZ = CrossingZ;
}
}
PreviousZ = CurrentZ;
PreviousDensity = CurrentDensity;
}
if (BestDistance == FLT_MAX
|| BestDistance > VoxelPassageGeometry::LandingFloorThicknessVoxels + 1.0f)
{
return false;
}
const float Below = Generator.GetDensityAt(
Centreline.X, Centreline.Y, BestCrossingZ - 0.75f);
const float Above = Generator.GetDensityAt(
Centreline.X, Centreline.Y, BestCrossingZ + 0.75f);
if (!FMath::IsFinite(Below) || !FMath::IsFinite(Above)
|| !(Below < 0.0f) || !(Above > 0.0f))
{
return false;
}
// Use the final-density crossing itself. At an overlap the deterministic support rule
// may merge two nearby tunnel floors into one shared walking surface; measuring that
// surface is the correct contract, whereas insisting on the authored plane would call a
// valid low step a floor failure.
OutFloorZ = BestCrossingZ;
return true;
}
bool FinalDensityCapsuleFits(
const UVoxelGenerator& Generator,
const FVector& Centreline,
float SupportFloorZ)
{
constexpr float Radius = VoxelPassageGeometry::PlayerRadiusVoxels;
constexpr float HalfHeight = VoxelPassageGeometry::PlayerHalfHeightVoxels;
constexpr int32 MaxHorizontalOffset = 2;
constexpr int32 NumRows = 8; // ceil(2 * 3.52 voxel capsule half-height)
for (int32 Row = 0; Row < NumRows; ++Row)
{
const float RelativeZ = static_cast<float>(Row) + 0.5f - HalfHeight;
const float AxisHalfLength = FMath::Max(0.0f, HalfHeight - Radius);
const float DistanceToAxis = FMath::Max(
FMath::Abs(RelativeZ) - AxisHalfLength, 0.0f);
bool bSampledRow = false;
for (int32 OffsetY = -MaxHorizontalOffset;
OffsetY <= MaxHorizontalOffset; ++OffsetY)
{
for (int32 OffsetX = -MaxHorizontalOffset;
OffsetX <= MaxHorizontalOffset; ++OffsetX)
{
const float DistanceSquared = static_cast<float>(
OffsetX * OffsetX + OffsetY * OffsetY)
+ DistanceToAxis * DistanceToAxis;
if (DistanceSquared > Radius * Radius + KINDA_SMALL_NUMBER)
{
continue;
}
bSampledRow = true;
const float Density = Generator.GetDensityAt(
Centreline.X + static_cast<float>(OffsetX),
Centreline.Y + static_cast<float>(OffsetY),
SupportFloorZ + static_cast<float>(Row) + 0.5f);
if (!FMath::IsFinite(Density) || !(Density > 0.0f))
{
return false;
}
}
}
// Match the measurement stencil's defensive centre sample for the top partial row.
if (!bSampledRow)
{
const float Density = Generator.GetDensityAt(
Centreline.X, Centreline.Y,
SupportFloorZ + static_cast<float>(Row) + 0.5f);
if (!FMath::IsFinite(Density) || !(Density > 0.0f))
{
return false;
}
}
}
return true;
}
struct FWalkableTunnelAudit
{
int32 Contracts = 0;
int32 Segments = 0;
int32 FloorSamples = 0;
int32 CapsuleSamples = 0;
int32 GradientFailures = 0;
int32 GeometricGradientFailures = 0;
int32 RouteGradientFailures = 0;
int32 FloorFailures = 0;
int32 CapsuleFailures = 0;
float WorstGradient = 0.0f;
float WorstGeometricGradient = 0.0f;
float WorstPatchGradient = 0.0f;
float WorstRouteGradient = 0.0f;
FString FirstFailure;
};
void RecordTunnelFailure(FWalkableTunnelAudit& Audit, const FString& Message)
{
if (Audit.FirstFailure.IsEmpty())
{
Audit.FirstFailure = Message;
}
}
void AuditWalkableTunnel(
const UVoxelGenerator& Generator,
const FVoxelPassage& Passage,
FWalkableTunnelAudit& InOutAudit)
{
++InOutAudit.Contracts;
if (Passage.ControlPoints.Num() < 2
|| Passage.ControlRadii.Num() != Passage.ControlPoints.Num())
{
++InOutAudit.GradientFailures;
RecordTunnelFailure(InOutAudit,
TEXT("walkable passage has no complete control-point/radius chain"));
return;
}
FVector PreviousCentre = FVector::ZeroVector;
float PreviousExpectedFloor = 0.0f;
bool bHavePrevious = false;
for (int32 SegmentIndex = 0;
SegmentIndex + 1 < Passage.ControlPoints.Num();
++SegmentIndex)
{
++InOutAudit.Segments;
const FVector& Start = Passage.ControlPoints[SegmentIndex];
const FVector& End = Passage.ControlPoints[SegmentIndex + 1];
const float StartRadius = Passage.ControlRadii[SegmentIndex];
const float EndRadius = Passage.ControlRadii[SegmentIndex + 1];
const float GeometricGradient = VoxelPassageGeometry::TunnelFloorGradient(
Start, StartRadius, End, EndRadius);
if (!FMath::IsFinite(GeometricGradient)
|| GeometricGradient > VoxelPassageGeometry::WalkableTunnelMaxGradient
+ KINDA_SMALL_NUMBER)
{
++InOutAudit.GradientFailures;
++InOutAudit.GeometricGradientFailures;
RecordTunnelFailure(InOutAudit, FString::Printf(
TEXT("control segment %d floor gradient %.6f exceeds %.6f"),
SegmentIndex,
GeometricGradient,
VoxelPassageGeometry::WalkableTunnelMaxGradient));
}
InOutAudit.WorstGeometricGradient = FMath::Max(
InOutAudit.WorstGeometricGradient, GeometricGradient);
InOutAudit.WorstGradient = FMath::Max(
InOutAudit.WorstGradient, GeometricGradient);
const int32 NumSamples = FMath::Max(
1, FMath::CeilToInt(FVector::Dist(Start, End)));
for (int32 SampleIndex = 0; SampleIndex <= NumSamples; ++SampleIndex)
{
const float T = static_cast<float>(SampleIndex)
/ static_cast<float>(NumSamples);
const FVector Centre = FMath::Lerp(Start, End, T);
const FVector2D SupportOffsets[] = {
FVector2D::ZeroVector,
FVector2D(1.0f, 0.0f), FVector2D(-1.0f, 0.0f),
FVector2D(0.0f, 1.0f), FVector2D(0.0f, -1.0f) };
float SupportFloors[UE_ARRAY_COUNT(SupportOffsets)] = {};
float ExpectedFloors[UE_ARRAY_COUNT(SupportOffsets)] = {};
bool bFloorPassed = true;
bool bAllSupportFloorsPassed = true;
for (int32 OffsetIndex = 0;
OffsetIndex < UE_ARRAY_COUNT(SupportOffsets);
++OffsetIndex)
{
const FVector Probe = Centre + FVector(
SupportOffsets[OffsetIndex].X,
SupportOffsets[OffsetIndex].Y,
0.0f);
float ExpectedFloorZ = 0.0f;
float ExpectedSupportRadius = 0.0f;
const bool bProjected = VoxelPassageGeometry::ProjectWalkableTunnelFloor(
Passage.ControlPoints, Passage.ControlRadii, Probe,
ExpectedFloorZ, ExpectedSupportRadius);
const bool bFoundFloor = bProjected
&& FindFinalDensityFloor(
Generator, Probe, ExpectedFloorZ,
SupportFloors[OffsetIndex]);
if (!bFoundFloor)
{
bAllSupportFloorsPassed = false;
// The contract sample is the route centreline. Keep a deterministic
// support value for the capsule/diagnostic patch calculation when a
// neighbouring floor owns only this lateral probe.
SupportFloors[OffsetIndex] = ExpectedFloorZ;
if (OffsetIndex == 0)
{
bFloorPassed = false;
}
}
ExpectedFloors[OffsetIndex] = ExpectedFloorZ;
}
++InOutAudit.FloorSamples;
if (!bFloorPassed)
{
++InOutAudit.FloorFailures;
if (InOutAudit.FirstFailure.IsEmpty())
{
InOutAudit.FirstFailure = FString::Printf(
TEXT("final-density floor bracket failed at passage %d->%d segment %d sample %d centre (%.2f,%.2f,%.2f)"),
Passage.UpperStrateIndex, Passage.LowerStrateIndex,
SegmentIndex, SampleIndex, Centre.X, Centre.Y, Centre.Z);
}
bHavePrevious = false;
continue;
}
float PatchGradient = 0.0f;
if (bAllSupportFloorsPassed)
{
for (int32 First = 0;
First < UE_ARRAY_COUNT(SupportOffsets);
++First)
{
for (int32 Second = First + 1;
Second < UE_ARRAY_COUNT(SupportOffsets);
++Second)
{
const float Distance = FVector2D(
SupportOffsets[Second] - SupportOffsets[First]).Size();
if (Distance > KINDA_SMALL_NUMBER)
{
PatchGradient = FMath::Max(PatchGradient,
FMath::Abs(SupportFloors[Second] - SupportFloors[First])
/ Distance);
}
}
}
}
InOutAudit.WorstPatchGradient = FMath::Max(
InOutAudit.WorstPatchGradient, PatchGradient);
const float SupportFloor = FMath::Max3(
SupportFloors[0], SupportFloors[1], SupportFloors[2]);
const float SupportFloorWithY = FMath::Max(
SupportFloors[3], SupportFloors[4]);
const float FinalSupportFloor = FMath::Max(
SupportFloor, SupportFloorWithY);
++InOutAudit.CapsuleSamples;
if (!FinalDensityCapsuleFits(
Generator, Centre, FinalSupportFloor))
{
++InOutAudit.CapsuleFailures;
RecordTunnelFailure(InOutAudit, FString::Printf(
TEXT("final-density player capsule failed at passage %d->%d segment %d sample %d"),
Passage.UpperStrateIndex, Passage.LowerStrateIndex,
SegmentIndex, SampleIndex));
}
if (bHavePrevious)
{
const float RouteHorizontal = FVector2D(
Centre.X - PreviousCentre.X,
Centre.Y - PreviousCentre.Y).Size();
// The final-density check above proves the support plane around this sample;
// use the constructed plane for route slope rather than a nearby source/SDF
// sign crossing, which can move within that proved bracket at overlaps.
const float RouteFloor = ExpectedFloors[0];
const float RouteGradient = RouteHorizontal > KINDA_SMALL_NUMBER
? FMath::Abs(RouteFloor - PreviousExpectedFloor)
/ RouteHorizontal
: 0.0f;
if (!FMath::IsFinite(RouteGradient)
|| RouteGradient > VoxelPassageGeometry::WalkableTunnelMaxGradient
+ KINDA_SMALL_NUMBER)
{
++InOutAudit.GradientFailures;
++InOutAudit.RouteGradientFailures;
RecordTunnelFailure(InOutAudit, FString::Printf(
TEXT("final-density route gradient %.6f exceeds %.6f at passage %d->%d segment %d sample %d prev(%.2f,%.2f,%.2f) floor %.3f current(%.2f,%.2f,%.2f) floor %.3f expected %.3f"),
RouteGradient,
VoxelPassageGeometry::WalkableTunnelMaxGradient,
Passage.UpperStrateIndex, Passage.LowerStrateIndex,
SegmentIndex, SampleIndex,
PreviousCentre.X, PreviousCentre.Y, PreviousCentre.Z,
PreviousExpectedFloor,
Centre.X, Centre.Y, Centre.Z,
RouteFloor, ExpectedFloors[0]));
}
InOutAudit.WorstRouteGradient = FMath::Max(
InOutAudit.WorstRouteGradient, RouteGradient);
InOutAudit.WorstGradient = FMath::Max(
InOutAudit.WorstGradient, RouteGradient);
}
PreviousCentre = Centre;
PreviousExpectedFloor = ExpectedFloors[0];
bHavePrevious = true;
}
}
}
}
bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
@@ -145,9 +506,11 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
float WorstLandingConnectorGradient = 0.0f;
FString FirstLandingFailure;
FString LandingSlopeReport;
constexpr float LandingWalkableAngleDegrees = 44.0f;
const float LandingWalkableSlope = FMath::Tan(
FMath::DegreesToRadians(LandingWalkableAngleDegrees));
FWalkableTunnelAudit TunnelAudit;
constexpr float LandingWalkableAngleDegrees =
VoxelPassageGeometry::WalkableTunnelMaxGradientDegrees;
constexpr float LandingWalkableSlope =
VoxelPassageGeometry::WalkableTunnelMaxGradient;
for (const FVoxelPassage& Passage : Passages)
{
@@ -646,7 +1009,8 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
Landing.ConnectorControl.X, Landing.ConnectorControl.Y);
const float FinalLegLength = (ConnectorEnd - ConnectorControl).Size();
const float RampLength = FMath::Max(
FinalLegLength - (Landing.RootSpineRadius > 0.0f ? 2.25f : 0.0f),
FinalLegLength - (Landing.RootSpineRadius > 0.0f
? VoxelPassageGeometry::RootOverlapVoxels : 0.0f),
KINDA_SMALL_NUMBER);
ConnectorGradient = FMath::Abs(
Landing.RootFloorZ - Landing.FloorZ) / RampLength;
@@ -655,7 +1019,8 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
{
const float RampLength = FMath::Max(
DirectLength - FMath::Max(Landing.HalfWidth - 1.0f, 0.0f)
- (Landing.RootSpineRadius > 0.0f ? 2.25f : 0.0f),
- (Landing.RootSpineRadius > 0.0f
? VoxelPassageGeometry::RootOverlapVoxels : 0.0f),
KINDA_SMALL_NUMBER);
ConnectorGradient = FMath::Abs(
Landing.RootFloorZ - Landing.FloorZ) / RampLength;
@@ -686,9 +1051,25 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
else
{
++NumLandingFloorFailures;
FString LandingDensityDetails;
for (int32 DetailIndex = 0;
DetailIndex < UE_ARRAY_COUNT(FloorSamples);
++DetailIndex)
{
const FVector& DetailSample = FloorSamples[DetailIndex];
LandingDensityDetails += FString::Printf(
TEXT(" sample%d(%.2f,%.2f below%.2f at%.2f above%.2f)"),
DetailIndex, DetailSample.X, DetailSample.Y,
World.Generator->GetDensityAt(
DetailSample.X, DetailSample.Y, Landing.FloorZ - 1.0f),
World.Generator->GetDensityAt(
DetailSample.X, DetailSample.Y, Landing.FloorZ),
World.Generator->GetDensityAt(
DetailSample.X, DetailSample.Y, Landing.FloorZ + 1.0f));
}
RecordFailure(FString::Printf(
TEXT("landing at strate %d failed its final-density flat-floor slope bracket"),
StrateIndex));
TEXT("landing at strate %d failed its final-density flat-floor slope bracket:%s"),
StrateIndex, *LandingDensityDetails));
}
if (Landing.bSourcePlayerFit)
@@ -777,6 +1158,20 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
}
}
// The landing checks above prove the two endpoints. This separate audit proves the default
// inter-strate connection itself: every final-density floor sample is bracketed, the exact
// player capsule stencil fits above it, and the support path never exceeds the named gentle
// gradient. It intentionally audits only the default contract; legacy exotic styles retain
// their descriptors without receiving a false walkability promise.
for (const FVoxelPassage& Passage : Passages)
{
if (Passage.UpperStrateIndex != Passage.LowerStrateIndex
&& Passage.bWalkableTunnelContract)
{
AuditWalkableTunnel(*World.Generator, Passage, TunnelAudit);
}
}
// The passage modifier is called once per density sample, but shortlist construction is
// keyed by (manager, layout version, chunk) in thread_local storage. Measure the hot hit and
// the deliberately alternating two-chunk rebuild separately so the report exposes the cache
@@ -871,6 +1266,29 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
{
AddError(FString::Printf(TEXT("First landing geometry failure: %s"), *FirstLandingFailure));
}
AddInfo(FString::Printf(
TEXT("Walkable tunnel contract: %d passages, %d segments, %d final floor samples, %d player-capsule samples; gradient failures %d (geometric %d, route %d), floor failures %d, capsule failures %d, worst gradient %.6f (geometric %.6f, route %.6f; cross-section diagnostic %.6f; limit %.6f / %.1f degrees)."),
TunnelAudit.Contracts,
TunnelAudit.Segments,
TunnelAudit.FloorSamples,
TunnelAudit.CapsuleSamples,
TunnelAudit.GradientFailures,
TunnelAudit.GeometricGradientFailures,
TunnelAudit.RouteGradientFailures,
TunnelAudit.FloorFailures,
TunnelAudit.CapsuleFailures,
TunnelAudit.WorstGradient,
TunnelAudit.WorstGeometricGradient,
TunnelAudit.WorstRouteGradient,
TunnelAudit.WorstPatchGradient,
VoxelPassageGeometry::WalkableTunnelMaxGradient,
VoxelPassageGeometry::WalkableTunnelMaxGradientDegrees));
if (!TunnelAudit.FirstFailure.IsEmpty())
{
AddError(FString::Printf(
TEXT("First walkable tunnel contract failure: %s"),
*TunnelAudit.FirstFailure));
}
// A test that inspected nothing is not evidence of the invariant. Fail loudly in both cases.
// Un test qui n'a rien inspecté ne prouve pas l'invariant : échouer explicitement dans les deux cas.
@@ -890,6 +1308,10 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
{
AddError(TEXT("VACUOUS: no Maze/VerticalShafts/FloatingIslands footing was checked."));
}
if (TunnelAudit.Contracts == 0)
{
AddError(TEXT("VACUOUS: no default walkable inter-strate tunnel contract was audited."));
}
const bool bAllNewFootingArchetypesAnswered =
AnsweredArchetypes.Contains(static_cast<uint8>(ECaveGeneratorType::Maze))
@@ -956,6 +1378,10 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
&& NumLandingSealFailures == 0
&& NumLandingFloorFailures == 0
&& NumLandingBoxViolations == 0
&& TunnelAudit.Contracts > 0
&& TunnelAudit.GradientFailures == 0
&& TunnelAudit.FloorFailures == 0
&& TunnelAudit.CapsuleFailures == 0
&& NumChecked > 0
&& NumUpperChecked > 0
&& NumFootingChecked > 0
@@ -8,6 +8,7 @@
#include "VoxelForgeTestFixture.h"
#include "VoxelCaveMorphology.h"
#include "VoxelPassageGeometry.h"
#include "VoxelStrateMeasure.h"
#include "VoxelTypes.h"
@@ -1732,7 +1733,6 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
for (int32 StrateIndex = 0; StrateIndex < Layout.Num(); ++StrateIndex)
{
const FStrateSlot& Slot = Layout[StrateIndex];
const FVoxelStrateMetrics& Window = DerivedMetricsByIndex[StrateIndex];
if (!Window.bValid || Window.SampledMinZ >= Window.SampledMaxZ) continue;
@@ -1745,22 +1745,36 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
const FConnectivityProbe Probe = ProbeRoute(
StrateIndex, A, B, /*bCountInGuard=*/true, /*bCountAsAnchorProbe=*/false,
/*bCountAsArrivalDeparture=*/false);
if (Probe.Result == EVoxelConnectivityResult::Connected
&& FullResolutionDirectSegmentHasSolid(*World.Generator, A, B))
{
AddError(FString::Printf(
TEXT("HARD FAILURE: VF_AreConnected returned CONNECTED for %s while its "
"full-resolution direct walk contained a solid sample."),
*ArchetypeName(Slot)));
bAllChecksPassed = false;
}
// A and B are deliberately a vertical diagnostic line, not the recovered route. A
// connected verdict is allowed to detour around solid terrain; only the route selected by
// VF_AreConnected is required to pass its own full-resolution re-walk. Comparing this
// unrelated direct segment to the verdict used to reject valid detours, and became
// especially misleading once the origin spine became a finite landing room.
(void)Probe;
}
// 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 FVoxelStrateMetrics& WallWindow = DerivedMetricsByIndex[0];
const float WallZ = static_cast<float>(WallWindow.SampledMinZ)
+ 0.5f * static_cast<float>(DerivedWindowSettings.SampleStep);
const FStrateSlot& WallSlot = Layout[0];
const float WallTopZ = (static_cast<float>(WallSlot.TopChunkZ) + 1.0f) * CHUNK_SIZE;
const float WallBottomZ = static_cast<float>(WallSlot.BottomChunkZ) * CHUNK_SIZE;
const VoxelPassageGeometry::FOriginLandingGeometry WallLanding =
VoxelPassageGeometry::BuildOriginLandingGeometry(
WallTopZ,
WallBottomZ,
WallSlot.Definition != nullptr
? WallSlot.Definition->GenerationParams.BoundarySealThickness
: 0.0f,
World.Generator->OriginSpineRadius);
// The controlled wall must start in known air. The old probe used the bottom sample of the
// strate, which was only air because the old spine was a full-height cylinder. Put it in the
// finite top landing room instead; this keeps the coarse-lie control about the wall, not about
// an unrelated start-cell-solid result.
const float WallZ = WallLanding.bValid
? WallLanding.FloorZ + 4.0f
: static_cast<float>(WallWindow.SampledMinZ)
+ 0.5f * static_cast<float>(DerivedWindowSettings.SampleStep);
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);
@@ -2058,7 +2058,8 @@ namespace
namespace
{
constexpr float VF_LandingCarveSafetyMargin = 4.0f;
constexpr float VF_LandingCarveSafetyMargin =
VoxelPassageGeometry::SealSafetyMarginVoxels;
struct FVoxelPassageLandingConnectorProjection
{
@@ -2142,12 +2143,13 @@ namespace
if (Landing.bHasConnectorBend)
{
// The first leg is deliberately level with the room. The second leg carries the
// complete rise and ends with a short level overlap inside the root annulus.
// complete rise and ends with a short level overlap inside the root landing room.
if (Projection.SegmentIndex == 0)
{
return 0.0f;
}
const float RootFlatLength = Landing.RootSpineRadius > 0.0f ? 2.25f : 0.0f;
const float RootFlatLength = Landing.RootSpineRadius > 0.0f
? VoxelPassageGeometry::RootOverlapVoxels : 0.0f;
const float RampLength = FMath::Max(
Projection.SegmentLength - RootFlatLength, KINDA_SMALL_NUMBER);
return FMath::Clamp(
@@ -2159,7 +2161,8 @@ namespace
// descriptors produced by older cooked data, even though new passages use the bend when
// a non-zero rise needs a guaranteed slope.
const float SourceFlatLength = FMath::Max(Landing.HalfWidth - 1.0f, 0.0f);
const float RootFlatLength = Landing.RootSpineRadius > 0.0f ? 2.25f : 0.0f;
const float RootFlatLength = Landing.RootSpineRadius > 0.0f
? VoxelPassageGeometry::RootOverlapVoxels : 0.0f;
const float SourceDistance = FMath::Clamp(
SourceFlatLength, 0.0f, Projection.TotalLength * 0.49f);
const float RootDistance = FMath::Clamp(
@@ -2190,20 +2193,12 @@ FVoxelPassageLanding VF_BuildPassageLanding(
{
FVoxelPassageLanding Landing;
// Body-derived dimensions at the authored 25 cm voxel scale:
// player diameter = 2 * 34 cm = 0.68 m = 2.72 voxels;
// a 3 m turn floor is 3 / 0.25 = 12 voxels across;
// capsule height = 2 * 88 cm = 1.76 m = 7.04 voxels;
// 1 m of headroom adds 4 voxels, so the minimum room height rounds to 12 voxels (3 m).
// The tube radius adds two voxels of clearance around the doorway. The support writer keeps
// one voxel inside the room wall, so HalfWidth=max(7, MouthRadius+2) gives a guaranteed
// 12-voxel = 3 m flat support floor, while the authored chamber is 14 voxels = 3.5 m wide.
// Body-derived dimensions at the authored 25 cm voxel scale are shared with the origin
// landing. In particular, the 0.68 m player diameter, 3 m turning patch, 1.76 m capsule
// height and 1 m headroom become the common §6.5 formulas in VoxelPassageGeometry.
const float SafeMouthRadius = FMath::Max(FMath::Abs(MouthRadius), 1.0f);
const float DesiredHalfWidth = FMath::Max(7.0f, SafeMouthRadius + 2.0f);
const float BodyHeightWithHeadroom = FVoxelPlayerCapsuleConstants::HeightVoxels + 4.0f;
const float DesiredHeight = FMath::Max(
FMath::Max(12.0f, BodyHeightWithHeadroom),
2.0f * SafeMouthRadius + 2.0f);
const float DesiredHalfWidth = VoxelPassageGeometry::LandingHalfWidthForRadius(MouthRadius);
const float DesiredHeight = VoxelPassageGeometry::LandingHeightForRadius(MouthRadius);
const float SafeSeal = FMath::Max(BoundarySealThickness, 0.0f);
FVector Direction(InDoorDirection.X, InDoorDirection.Y, 0.0f);
@@ -2216,7 +2211,7 @@ FVoxelPassageLanding VF_BuildPassageLanding(
Landing.FloorZ = InStandingPoint.Z - 0.5f;
Landing.HalfWidth = DesiredHalfWidth;
Landing.CeilingZ = Landing.FloorZ + DesiredHeight;
Landing.FloorThickness = 3.0f;
Landing.FloorThickness = VoxelPassageGeometry::LandingFloorThicknessVoxels;
Landing.DoorDirection = Direction;
Landing.RootSpineRadius = FMath::Max(RootSpineRadius, 0.0f);
Landing.bSourcePlayerFit = bSourcePlayerFit;
@@ -2273,15 +2268,14 @@ FVoxelPassageLanding VF_BuildPassageLanding(
// opposite sides of the strate. The manager chooses the common level inside the same
// seal-safe interval as this builder, so no source query or passage order can affect it.
// Five voxels = 1.25 m half-width / 2.5 m clear width. This is the smallest whole-voxel
// annular root corridor that leaves a 1.36-voxel player capsule a two-voxel fit margin on
// root corridor that leaves a 1.36-voxel player capsule a two-voxel fit margin on
// the support lattice, while the landing itself remains the 3 m turn floor. Its separate
// 12-voxel (3 m) clear height gives the 1.76 m capsule more than 1 m of headroom while the
// player walks out to the network root. The floor rise is linear along the horizontal
// run; if the direct run is too short, a level dog-leg makes the final ramp satisfy the
// same 44 degree walkable limit by construction.
Landing.ConnectorRadius = 5.0f;
const float ConnectorHeight = FMath::Max(
12.0f, FVoxelPlayerCapsuleConstants::HeightVoxels + 4.0f);
// player walks out to the origin landing room. The floor rise is linear along the
// horizontal run; if the direct run is too short, a level dog-leg makes the final ramp
// satisfy the same 15 degree walkable-tunnel limit by construction.
Landing.ConnectorRadius = VoxelPassageGeometry::ConnectorRadiusVoxels;
const float ConnectorHeight = VoxelPassageGeometry::ConnectorHeightVoxels;
Landing.ConnectorCeilingZ = Landing.FloorZ + ConnectorHeight;
Landing.RootFloorZ = NetworkPoint.Z;
Landing.RootCeilingZ = Landing.RootFloorZ + ConnectorHeight;
@@ -2318,41 +2312,25 @@ FVoxelPassageLanding VF_BuildPassageLanding(
const float ConnectorZ = (Landing.FloorZ + Landing.ConnectorCeilingZ) * 0.5f;
const float RootConnectorZ = (Landing.RootFloorZ + Landing.RootCeilingZ) * 0.5f;
FVector2D RootEntryDirection(
Landing.StandingPoint.X - NetworkPoint.X,
Landing.StandingPoint.Y - NetworkPoint.Y);
if (!RootEntryDirection.Normalize())
{
RootEntryDirection = FVector2D(Direction.X, Direction.Y);
if (!RootEntryDirection.Normalize())
{
RootEntryDirection = FVector2D(1.0f, 0.0f);
}
}
// The structural spine is a vertical air column, so its centre is not a standing
// endpoint. End the ramp in the middle of the support annulus instead: with the 1.36-voxel
// capsule radius, a centre at RootSpineRadius+2.25 sits at least one capsule radius outside
// the spine and at least one capsule radius inside the 4.5-voxel support edge. Every
// passage therefore enters the same continuous, floor-backed annular hub, while the spine
// itself remains open and unsealed.
const float RootEntryRadius = Landing.RootSpineRadius + 2.25f;
const FVector2D RootEntryXY(
NetworkPoint.X + RootEntryDirection.X * RootEntryRadius,
NetworkPoint.Y + RootEntryDirection.Y * RootEntryRadius);
// The origin is now a finite landing room, not a vertical air column. End the connector
// at the requested room centre so its floor meets the room floor directly. The room's
// own rounded-box SDF and floor post provide the complete volume; no annular shaft hub is
// synthesized here, and no floor is omitted at the centre.
const FVector2D RootEntryXY(NetworkPoint.X, NetworkPoint.Y);
Landing.ConnectorStart = FVector(
Landing.StandingPoint.X, Landing.StandingPoint.Y, ConnectorZ);
Landing.ConnectorEnd = FVector(RootEntryXY.X, RootEntryXY.Y, RootConnectorZ);
// A direct source-to-root ramp is usually gentle, but its length is not a contract: a
// legal source pose can be close to the spine while its floor is near a strate face. Add a
// deterministic level dog-leg whenever the direct run cannot meet the 44-degree law.
// deterministic level dog-leg whenever the direct run cannot meet the 15-degree law.
// The final leg is the only rising part, and its extra horizontal run includes the root
// annulus overlap. This makes the slope a construction guarantee for every seed, rather
// room overlap. This makes the slope a construction guarantee for every seed, rather
// than a property observed in today's showcase seeds. WorldRadiusVoxels is deliberately
// zero, so the bounded world rim cannot be approached by this lateral safety run.
constexpr float WalkableSlope = 0.9656888f; // tan(44 degrees)
constexpr float RootFlatLength = 2.25f;
constexpr float WalkableSlope =
VoxelPassageGeometry::WalkableTunnelMaxGradient;
constexpr float RootFlatLength = VoxelPassageGeometry::RootOverlapVoxels;
const FVector2D SourceXY(Landing.ConnectorStart.X, Landing.ConnectorStart.Y);
const FVector2D RootXY(Landing.ConnectorEnd.X, Landing.ConnectorEnd.Y);
const FVector2D Direct = RootXY - SourceXY;
@@ -2460,23 +2438,6 @@ float VF_EvaluatePassageLandingSDF(
const float ConnectorSDF = FMath::Max(HorizontalSDF, VerticalSDF);
LandingSDF = VoxelSDF::SmoothMin(LandingSDF, ConnectorSDF, 3.0f);
if (Landing.RootSpineRadius >= 0.0f)
{
// A shared hub turns the guaranteed root join into a walkable network. With the normal
// origin spine it is an annulus outside that shaft; with a disabled spine it safely
// degenerates to a disk, so the landing contract still has a guaranteed root volume.
const float HubRadius = Landing.RootSpineRadius + Landing.ConnectorRadius;
const float RadialDistance = FMath::Sqrt(
FMath::Square(Position.X) + FMath::Square(Position.Y));
const float HubHorizontalSDF = FMath::Max(
Landing.RootSpineRadius - RadialDistance,
RadialDistance - HubRadius);
const float HubVerticalSDF = FMath::Max(
Landing.RootFloorZ - Position.Z,
Position.Z - Landing.RootCeilingZ);
const float HubSDF = FMath::Max(HubHorizontalSDF, HubVerticalSDF);
LandingSDF = VoxelSDF::SmoothMin(LandingSDF, HubSDF, 3.0f);
}
}
return LandingSDF;
@@ -2502,14 +2463,9 @@ bool VF_IsPassageLandingFloor(
// backstop cannot disappear at an exact floor sample.
return Z <= FloorZ + KINDA_SMALL_NUMBER && Z > FloorZ - Thickness;
};
const bool bInsideRootSpine = Landing.RootSpineRadius > 0.0f
&& FMath::Square(Position.X) + FMath::Square(Position.Y)
<= FMath::Square(Landing.RootSpineRadius);
// The room floor is a true horizontal landing: it is the only floor that owns the full
// three-metre turning patch. The central root spine is left open.
if (IsInFloorBand(Position.Z, Landing.FloorZ, Landing.FloorThickness)
&& !bInsideRootSpine)
// three-metre turning patch. The origin room owns its centre, including its support floor.
if (IsInFloorBand(Position.Z, Landing.FloorZ, Landing.FloorThickness))
{
const float FloorHalfWidth = FMath::Max(Landing.HalfWidth - 1.0f, 0.0f);
if (FMath::Abs(Position.X - Landing.StandingPoint.X) <= FloorHalfWidth
@@ -2530,8 +2486,7 @@ bool VF_IsPassageLandingFloor(
const float FloorT = VF_PassageLandingConnectorFloorParameter(Projection, Landing);
const float ConnectorFloorZ = FMath::Lerp(
Landing.FloorZ, Landing.RootFloorZ, FloorT);
if (!bInsideRootSpine
&& IsInFloorBand(Position.Z, ConnectorFloorZ, Landing.FloorThickness))
if (IsInFloorBand(Position.Z, ConnectorFloorZ, Landing.FloorThickness))
{
// Keep connector support inset by half a voxel from its clear width. The landing
// chamber—not this narrow transit leg—owns the 3 m turn floor; this inset preserves
@@ -2546,25 +2501,6 @@ bool VF_IsPassageLandingFloor(
}
}
if (IsInFloorBand(Position.Z, Landing.RootFloorZ, Landing.FloorThickness))
{
// The common root hub is a walkable annulus, not a cap over the vertical spine. Its
// 4.5-voxel support inset leaves the same 0.5-voxel air clearance at the outer wall as
// the connector and never writes inside the spine radius.
const float RadialDistanceSquared = FMath::Square(Position.X)
+ FMath::Square(Position.Y);
const float HubInnerRadius = Landing.RootSpineRadius;
const float HubOuterSupportRadius = Landing.RootSpineRadius
+ FMath::Max(Landing.ConnectorRadius - 0.5f,
FVoxelPlayerCapsuleConstants::RadiusVoxels);
const bool bOutsideSpine = Landing.RootSpineRadius <= 0.0f
|| RadialDistanceSquared > FMath::Square(HubInnerRadius);
if (bOutsideSpine
&& RadialDistanceSquared <= FMath::Square(HubOuterSupportRadius))
{
return true;
}
}
}
return false;
@@ -1677,23 +1677,18 @@ namespace
VF_ApplyOriginSpine(InOut.Density, X, Y, Z, TopZ, BotZ, Seal, Base, Radius);
}
// Ne fait QUE de l'air ⇒ tue AllSolid, jamais AllAir. Identity quand le cercle XY rate la
// boîte, ou quand la boîte est entièrement hors de l'intérieur de la strate.
// ≡ le test cercle/boîte écrit à la main dans ClassifyTile aujourd'hui.
// The room kills AllSolid; its support slab also kills AllAir. Identity when the room
// misses the box or the box is wholly outside this strate's safe interior.
EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext&) const override
{
if (Radius <= 0.0f) { return EVoxelOpEffect::Identity; }
const float InnerTop = TopZ - Seal;
const float InnerBot = BotZ + Seal;
if (VoxelBox.Max.Z <= InnerBot || VoxelBox.Min.Z >= InnerTop) { return EVoxelOpEffect::Identity; }
const float Reach = Radius + VoxelDensityReach::SpineBlend;
const float CX = FMath::Clamp(0.0f, (float)VoxelBox.Min.X, (float)VoxelBox.Max.X);
const float CY = FMath::Clamp(0.0f, (float)VoxelBox.Min.Y, (float)VoxelBox.Max.Y);
if (CX * CX + CY * CY > Reach * Reach) { return EVoxelOpEffect::Identity; }
return EVoxelOpEffect::CarveOnly;
if (!VoxelPassageGeometry::OriginLandingRoomTouchesBox(
VoxelBox, TopZ, BotZ, Seal, Radius))
{
return EVoxelOpEffect::Identity;
}
return VoxelPassageGeometry::OriginLandingFloorTouchesBox(
VoxelBox, TopZ, BotZ, Seal, Radius)
? EVoxelOpEffect::Both : EVoxelOpEffect::CarveOnly;
}
const TCHAR* DebugName() const override { return TEXT("OriginSpineOp"); }
@@ -1842,8 +1837,10 @@ namespace
class FPassageCarveOp final : public IVoxelDensityOp
{
public:
FPassageCarveOp(const UVoxelStrateManager* InManager, float InBase, float InSeal)
: Manager(InManager), Base(InBase), Seal(InSeal) {}
FPassageCarveOp(const UVoxelStrateManager* InManager, float InBase, float InSeal,
float InTopZ, float InBotZ, float InSpineRadius)
: Manager(InManager), Base(InBase), Seal(InSeal),
TopZ(InTopZ), BotZ(InBotZ), SpineRadius(InSpineRadius) {}
EVoxelOpRole GetRole() const override { return EVoxelOpRole::StructuralPost; }
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density; }
@@ -1853,8 +1850,15 @@ namespace
void Eval(float X, float Y, float Z, FVoxelOpSample& InOut) const override
{
if (!Manager) { return; }
Manager->ApplyPassageModifier(InOut.Density, X, Y, Z, Base, Seal);
if (Manager)
{
Manager->ApplyPassageModifier(InOut.Density, X, Y, Z, Base, Seal);
}
// Keep the source and op-stack operation order identical: the origin room is carved
// before the boundary seal, then its support floor is reasserted after the passage
// post so no connector can remove the standing surface.
VF_ApplyOriginLandingFloor(
InOut.Density, X, Y, Z, TopZ, BotZ, Seal, Base, SpineRadius);
}
// ≡ la garde `AnyPassageNearBox` écrite à la main dans ClassifyTile — déjà écrite, ici
@@ -1862,7 +1866,9 @@ namespace
EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext&) const override
{
if (!Manager) { return EVoxelOpEffect::Identity; }
if (Manager->AnyPassageLandingFloorNearBox(VoxelBox.Min, VoxelBox.Max))
if (VoxelPassageGeometry::OriginLandingFloorTouchesBox(
VoxelBox, TopZ, BotZ, Seal, SpineRadius)
|| Manager->AnyPassageLandingFloorNearBox(VoxelBox.Min, VoxelBox.Max))
{
// The room carves air, while its support slab force-writes solid. Both
// hypotheses must therefore be killed for a box touching that slab.
@@ -1876,7 +1882,7 @@ namespace
private:
const UVoxelStrateManager* Manager;
float Base, Seal;
float Base, Seal, TopZ, BotZ, SpineRadius;
};
//=========================================================================
@@ -5218,7 +5224,8 @@ void FVoxelOpStack::AppendStructuralPost(float StrateTopWorldZ, float StrateBott
// restent le dernier post de GetDensityAt, hors de cette pile, comme avant.
Add(MakeUnique<FOriginSpineOp>(StrateTopWorldZ, StrateBottomWorldZ, SealThickness, BaseDensity, SpineRadius));
Add(MakeUnique<FBoundarySealOp>(StrateTopWorldZ, StrateBottomWorldZ, SealThickness, BaseDensity));
Add(MakeUnique<FPassageCarveOp>(StrateManager, BaseDensity, SealThickness));
Add(MakeUnique<FPassageCarveOp>(StrateManager, BaseDensity, SealThickness,
StrateTopWorldZ, StrateBottomWorldZ, SpineRadius));
Add(MakeUnique<FXYEdgeSealOp>(BaseDensity));
}
+68 -6
View File
@@ -296,6 +296,14 @@ static FORCEINLINE void ApplyOriginSpine(float& Density, float WorldX, float Wor
StrateTopZ, StrateBottomZ, SealThickness, BaseDensity, Radius);
}
static FORCEINLINE void ApplyOriginLandingFloor(float& Density,
float WorldX, float WorldY, float WorldZ,
float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius)
{
VF_ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ,
StrateTopZ, StrateBottomZ, SealThickness, BaseDensity, Radius);
}
// DISTURBANCE LAYER — the "wow" post-process. Operates on the FINAL MC density
// (negative = solid, positive = air), AFTER the archetype produced its terrain, so
// it works uniformly for every generator type. Stays inside the seal bands so it can
@@ -1061,11 +1069,40 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
break;
}
}
// Origin rooms are structural too. Reassert their air before either support writer so a
// bridge/ridge cannot plug a landing, while the two floor writers remain last.
VF_ApplyOriginLandingAirMC(
Result, WorldX, WorldY, WorldZ,
CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ,
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
StrateManager->ApplyPassageLandingAirMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity,
CP_Dist.BoundarySealThickness);
VF_ApplyOriginLandingFloorMC(
Result, WorldX, WorldY, WorldZ,
CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ,
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
StrateManager->ApplyPassageLandingFloorMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity);
// A walkable tunnel's guaranteed air core is the final structural writer above its
// floor. This prevents a nearby passage's floor slab from capping the active route;
// the tunnel-floor predicate is false on the active floor itself, so support remains.
StrateManager->ApplyPassageTunnelAirMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity,
CP_Dist.BoundarySealThickness);
VF_ApplyOriginLandingAirMC(
Result, WorldX, WorldY, WorldZ,
CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ,
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
StrateManager->ApplyPassageLandingAirMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity,
CP_Dist.BoundarySealThickness);
VF_ApplyOriginLandingFloorMC(
Result, WorldX, WorldY, WorldZ,
CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ,
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
StrateManager->ApplyPassageLandingRoomFloorMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity);
}
else
{
@@ -2225,6 +2262,9 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
Density, WorldX, WorldY, WorldZ,
Params.BaseDensity, Params.BoundarySealThickness);
}
ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ,
Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius);
// Fourth structural post: the XY edge wins over a passage near the rim.
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY,
@@ -2448,6 +2488,9 @@ float UVoxelGenerator::GetSlabDensity(float WorldX, float WorldY, float WorldZ,
Density, WorldX, WorldY, WorldZ,
Params.BaseDensity, Params.BoundarySealThickness);
}
ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ,
Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius);
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY,
WorldRadiusVoxels, EdgeSealThickness, Params.BaseDensity);
@@ -2569,6 +2612,9 @@ float UVoxelGenerator::GetMazeDensity(float WorldX, float WorldY, float WorldZ,
Density, WorldX, WorldY, WorldZ,
Params.BaseDensity, Params.BoundarySealThickness);
}
ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ,
Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius);
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY,
WorldRadiusVoxels, EdgeSealThickness, Params.BaseDensity);
@@ -2833,6 +2879,9 @@ float UVoxelGenerator::SurfaceDensityFromColumn(float WorldX, float WorldY, floa
Density, WorldX, WorldY, WorldZ,
S.BaseDensity, S.BoundarySealThickness);
}
ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ,
S.StrateTopWorldZ, S.StrateBottomWorldZ,
S.BoundarySealThickness, S.BaseDensity, OriginSpineRadius);
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY,
WorldRadiusVoxels, EdgeSealThickness, S.BaseDensity);
@@ -3223,13 +3272,20 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
{
bCanAir = false;
}
if (OriginSpineRadius > 0.0f)
if (StrateManager->AnyOriginLandingNearBox(
FVector(MinX, MinY, MinZ), FVector(MaxX, MaxY, MaxZ)))
{
// Spine (0,0) : colonne XY rayon R + blend 3 (cf. ApplyOriginSpine). Test cercle/boîte XY.
const float Reach = OriginSpineRadius + 3.0f;
const float CX = FMath::Clamp(0.0f, (float)MinX, (float)MaxX);
const float CY = FMath::Clamp(0.0f, (float)MinY, (float)MaxY);
if (CX * CX + CY * CY <= Reach * Reach) bCanSolid = false;
// The origin post is a room carve, not a full-height spine. Its conservative manager
// guard is per tile, so it retains the hot-path invariant while covering every strate's
// room AABB and its four-voxel carve blend.
bCanSolid = false;
}
if (StrateManager->AnyOriginLandingFloorNearBox(
FVector(MinX, MinY, MinZ), FVector(MaxX, MaxY, MaxZ)))
{
// The same origin room also owns a guaranteed support slab, so a tile touching only that
// slab cannot be proved uniformly air.
bCanAir = false;
}
// ── Catégorisation par Z du treillis : gap bedrock = solide ; hors layout = air constant ;
@@ -4556,6 +4612,9 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
Density, WorldX, WorldY, WorldZ,
Params.BaseDensity, Params.BoundarySealThickness);
}
ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ,
Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius);
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY,
WorldRadiusVoxels, EdgeSealThickness, Params.BaseDensity);
@@ -4717,6 +4776,9 @@ float UVoxelGenerator::GetFloatingIslandDensity(float WorldX, float WorldY, floa
Density, WorldX, WorldY, WorldZ,
Params.BaseDensity, Params.BoundarySealThickness);
}
ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ,
Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius);
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY,
WorldRadiusVoxels, EdgeSealThickness, Params.BaseDensity);
+631 -52
View File
@@ -120,6 +120,165 @@ namespace
}
}
static float VF_BoundarySealThicknessForDefinition(
const UVoxelStrateDefinition& Definition)
{
switch (Definition.GeneratorType)
{
case ECaveGeneratorType::TunnelNetwork:
case ECaveGeneratorType::Underwater:
return Definition.GenerationParams.BoundarySealThickness;
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber:
return Definition.SlabParams.BoundarySealThickness;
case ECaveGeneratorType::Maze:
return Definition.MazeParams.BoundarySealThickness;
case ECaveGeneratorType::SurfaceWorld:
return Definition.SurfaceParams.BoundarySealThickness;
case ECaveGeneratorType::VerticalShafts:
return Definition.VerticalShaftParams.BoundarySealThickness;
case ECaveGeneratorType::FloatingIslands:
return Definition.FloatingIslandParams.BoundarySealThickness;
default:
return 0.0f;
}
}
/**
* The default inter-strate tunnel is a carved tube plus an explicit support slab. The tube's
* rounded SDF guarantees clearance, but it cannot guarantee a standable surface over a source
* field or a disturbance that was solid before the passage post. Project the query into the
* nearest horizontal control segment and return the conservative floor carried by that segment.
* This is a fixed-size arithmetic loop over the already-built descriptor; it performs no source
* search, allocation, or topology work.
*/
static bool VF_IsWalkableTunnelFloor(
const FVoxelPassage& Passage,
const FVector& Position)
{
if (!Passage.bWalkableTunnelContract
|| Passage.ControlPoints.Num() < 2
|| Passage.ControlRadii.Num() != Passage.ControlPoints.Num())
{
return false;
}
float FloorZ = 0.0f;
float SupportRadius = 0.0f;
if (!VoxelPassageGeometry::ProjectWalkableTunnelFloor(
Passage.ControlPoints, Passage.ControlRadii, Position,
FloorZ, SupportRadius))
{
return false;
}
return Position.Z <= FloorZ + KINDA_SMALL_NUMBER
&& Position.Z > FloorZ - VoxelPassageGeometry::LandingFloorThicknessVoxels;
}
static bool VF_IsWalkableTunnelAir(
const FVoxelPassage& Passage,
const FVector& Position)
{
if (!Passage.bWalkableTunnelContract
|| Passage.ControlPoints.Num() < 2
|| Passage.ControlRadii.Num() != Passage.ControlPoints.Num())
{
return false;
}
float FloorZ = 0.0f;
float SupportRadius = 0.0f;
if (!VoxelPassageGeometry::ProjectWalkableTunnelFloor(
Passage.ControlPoints, Passage.ControlRadii, Position,
FloorZ, SupportRadius))
{
return false;
}
// The carved tube is wider than this guaranteed core. The core is deliberately expressed as
// a vertical prism over the projected floor: it gives the final-density player stencil a
// stable air volume even when a source archetype or a disturbance would otherwise refill the
// shallow part of the rounded SDF. The floor writer below owns the closed lower face.
const float AirHeight = 2.0f * SupportRadius;
return Position.Z > FloorZ
+ VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels
&& Position.Z < FloorZ + AirHeight - KINDA_SMALL_NUMBER;
}
static bool VF_FindWalkableTunnelAir(
const UVoxelStrateManager* Manager,
const FVector& Position,
int32& OutPassageIndex,
float& OutFloorZ,
float& OutSupportRadius)
{
OutPassageIndex = INDEX_NONE;
OutFloorZ = 0.0f;
OutSupportRadius = 0.0f;
if (Manager == nullptr)
{
return false;
}
const FIntVector ChunkCoord(
FMath::FloorToInt(Position.X / (float)CHUNK_SIZE),
FMath::FloorToInt(Position.Y / (float)CHUNK_SIZE),
FMath::FloorToInt(Position.Z / (float)CHUNK_SIZE));
const TArray<int32>& Nearby = VF_GetNearbyPassages(Manager, ChunkCoord);
const TArray<FVoxelPassage>& Passages = Manager->GetPassages();
for (const int32 PassageIndex : Nearby)
{
if (!Passages.IsValidIndex(PassageIndex))
{
continue;
}
const FVoxelPassage& Passage = Passages[PassageIndex];
if (FVector::DistSquared(Position, Passage.BoundCenter) > Passage.BoundRadiusSq
|| !VF_IsWalkableTunnelAir(Passage, Position))
{
continue;
}
VoxelPassageGeometry::ProjectWalkableTunnelFloor(
Passage.ControlPoints, Passage.ControlRadii, Position,
OutFloorZ, OutSupportRadius);
OutPassageIndex = PassageIndex;
return true;
}
return false;
}
static bool VF_IsAnyPassageFloorAt(
const UVoxelStrateManager* Manager,
const FVector& Position)
{
if (Manager == nullptr)
{
return false;
}
const FIntVector ChunkCoord(
FMath::FloorToInt(Position.X / (float)CHUNK_SIZE),
FMath::FloorToInt(Position.Y / (float)CHUNK_SIZE),
FMath::FloorToInt(Position.Z / (float)CHUNK_SIZE));
const TArray<int32>& Nearby = VF_GetNearbyPassages(Manager, ChunkCoord);
const TArray<FVoxelPassage>& Passages = Manager->GetPassages();
for (const int32 PassageIndex : Nearby)
{
if (!Passages.IsValidIndex(PassageIndex))
{
continue;
}
const FVoxelPassage& Passage = Passages[PassageIndex];
if (VF_IsPassageLandingFloor(Position, Passage.UpperLanding)
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding)
|| VF_IsWalkableTunnelFloor(Passage, Position))
{
return true;
}
}
return false;
}
bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
{
if (!Settings)
@@ -756,14 +915,23 @@ void UVoxelStrateManager::GeneratePassages()
}
};
const auto CommonRootFloorFor = [](float StrateTopZ, float StrateBottomZ,
float BoundarySealThickness) -> float
const auto CommonRootFloorFor = [&](float StrateTopZ, float StrateBottomZ,
float BoundarySealThickness) -> float
{
// The two mouth queries may legitimately return floors near opposite strate faces. A
// shared root level is therefore derived from the strate bounds, never from passage
// iteration order. Leave the floor slab and the full 12-voxel connector clearance beyond
// the four-voxel passage blend away from both seals; the landing builder repeats this
// clamp as its final safety boundary.
// The two mouth queries may legitimately return floors near opposite strate faces. The
// root is the origin landing room's floor, not a hidden continuous shaft and not a
// passage-order-dependent midpoint. Reusing the same top-anchored geometry here makes
// every connector meet the (0,0) room at its actual support level.
const VoxelPassageGeometry::FOriginLandingGeometry OriginLanding =
VoxelPassageGeometry::BuildOriginLandingGeometry(
StrateTopZ, StrateBottomZ, BoundarySealThickness, OriginSpineRadius);
if (OriginLanding.bValid)
{
return OriginLanding.FloorZ;
}
// Malformed tiny strates still fail closed. This fallback is only for the descriptor
// builder's diagnostic path; normal authored strates always satisfy the room interval.
const float Seal = FMath::Max(BoundarySealThickness, 0.0f);
const float InnerBottom = StrateBottomZ + Seal;
const float InnerTop = StrateTopZ - Seal;
@@ -790,8 +958,9 @@ void UVoxelStrateManager::GeneratePassages()
const FStrateSlot& Upper = StrateLayout[i];
const FStrateSlot& Lower = StrateLayout[i + 1];
// This (upper) strate's PassageConfig controls the descent tunnels to the layer
// below. The (0,0) spine descent is separate (player-dug); these are the shortcuts.
// This (upper) strate's PassageConfig controls the progression tunnel to the layer below.
// Each strate already owns a finite (0,0) landing room; this passage is the connection
// that opens the next room rather than a second vertical spine.
const UVoxelStrateDefinition* UpperDef = Upper.Definition;
if (!UpperDef) continue;
const FStratePassageConfig& Cfg = UpperDef->PassageConfig;
@@ -821,10 +990,25 @@ void UVoxelStrateManager::GeneratePassages()
FVoxelPassage Passage;
Passage.UpperStrateIndex = i;
Passage.LowerStrateIndex = i + 1;
// PassageType has no random draw in the current implementation; preserve its existing
// default while PassageConfig::Style controls the generated control-point shape.
// PassageType n'a pas de tirage aléatoire ici ; conserver son défaut, tandis que
// PassageConfig::Style contrôle la forme des points de contrôle générés.
// The authored style is also reflected in the legacy passage type. In particular,
// only Straight receives the walkable-tunnel contract; a future style roll cannot
// silently inherit the default's player-fit promise.
switch (Cfg.Style)
{
case EVoxelPassageStyle::Straight:
Passage.PassageType = EVoxelPassageType::SlopedTunnel;
break;
case EVoxelPassageStyle::Spiral:
Passage.PassageType = EVoxelPassageType::SpiralDescent;
break;
case EVoxelPassageStyle::Cascading:
Passage.PassageType = EVoxelPassageType::CascadingDrops;
break;
case EVoxelPassageStyle::Worm:
default:
Passage.PassageType = EVoxelPassageType::CrackCrevice;
break;
}
// PLACEMENT: random angle, distance from the (0,0) spine within config range.
const float Angle = PassageRandom01(PassageSaltAngle) * (2.0f * PI);
@@ -1142,7 +1326,223 @@ void UVoxelStrateManager::GeneratePassages()
if (Passage.UpperLanding.bHasNetworkConnector) ++NumUpperRootConnectors;
if (Passage.LowerLanding.bHasNetworkConnector) ++NumLowerRootConnectors;
if (Passage.ControlPoints.Num() > 0)
if (Cfg.Style == EVoxelPassageStyle::Straight
&& Passage.UpperLanding.HalfWidth > 0.0f
&& Passage.LowerLanding.HalfWidth > 0.0f)
{
// BASE CONNECTION: a walkable two-leg switchback. A direct line between the
// mouths would often be nearly vertical because both requests are placed at the
// same XY radius. Put the turn on the perpendicular bisector and give each leg
// at least half of the horizontal run required by the named 15-degree law.
// This is deliberately a construction rule, not a seed-dependent observation.
const float SwitchbackAngle =
PassageRandom01(PassageSaltUpperDoor) * 2.0f * PI;
FVector2D SwitchbackDirection(
FMath::Cos(SwitchbackAngle), FMath::Sin(SwitchbackAngle));
if (!SwitchbackDirection.Normalize())
{
SwitchbackDirection = FVector2D(1.0f, 0.0f);
}
const FVector DoorDirection(
SwitchbackDirection.X, SwitchbackDirection.Y, 0.0f);
Passage.UpperLanding.DoorDirection = DoorDirection;
// A real switchback turns around: the lower doorway faces back toward the upper
// leg. Keeping both doors on the same side would make the two sloped legs retrace
// one XY line at different heights, leaving the floor projection ambiguous.
const FVector TunnelLowerDoorDirection = -DoorDirection;
Passage.LowerLanding.DoorDirection = TunnelLowerDoorDirection;
const float UpperDoorOffset = FMath::Max(
Passage.UpperLanding.HalfWidth - 1.0f, 0.0f);
const float LowerDoorOffset = FMath::Max(
Passage.LowerLanding.HalfWidth - 1.0f, 0.0f);
const float SafeMouthRadius = FMath::Max(
FMath::Abs(Cfg.MouthRadius), 1.0f);
Passage.UpperLanding.DoorPoint =
Passage.UpperLanding.StandingPoint + DoorDirection * UpperDoorOffset;
Passage.UpperLanding.DoorPoint.Z =
Passage.UpperLanding.FloorZ + SafeMouthRadius;
Passage.LowerLanding.DoorPoint =
Passage.LowerLanding.StandingPoint
+ TunnelLowerDoorDirection * LowerDoorOffset;
Passage.LowerLanding.DoorPoint.Z =
Passage.LowerLanding.FloorZ + SafeMouthRadius;
const FVector UpperDoor = Passage.UpperLanding.DoorPoint;
const FVector LowerDoor = Passage.LowerLanding.DoorPoint;
// Keep a short level apron beyond each room floor. The door anchor is one voxel
// inside the room's support square; a diagonal door direction can therefore need
// several voxels before it leaves that square and its four-voxel SDF blend.
// The level apron must clear the room half-width, the tube radius, and the
// smooth carve band before the sloped leg begins. The final six-voxel transition
// is measured from that clear point back toward the turn, so include it too. For
// the default 8-voxel half-width / 6-voxel mouth this is 17 voxels (4.25 m), not
// the old 6-voxel minimum that left the sloped capsule inside the landing room.
const float LandingApron = FMath::Max(
VoxelPassageGeometry::WalkableTunnelLandingApronVoxels,
VoxelPassageGeometry::WalkableTunnelTurnTransitionVoxels
+ SafeMouthRadius
+ VoxelPassageGeometry::LandingCarveBlendVoxels
+ 1.0f);
const FVector UpperApronEnd = UpperDoor
+ DoorDirection * LandingApron;
const FVector LowerApronBegin = LowerDoor
+ TunnelLowerDoorDirection * LandingApron;
const float VerticalDrop = FMath::Abs(UpperDoor.Z - LowerDoor.Z);
const float UpperFloorZ = VoxelPassageGeometry::TunnelFloorZ(
UpperDoor, Cfg.MouthRadius);
const float LowerFloorZ = VoxelPassageGeometry::TunnelFloorZ(
LowerDoor, Cfg.MouthRadius);
const float MidFloorZ = 0.5f * (UpperFloorZ + LowerFloorZ);
const FVector2D RampStartMid(
0.5f * (UpperApronEnd.X + LowerApronBegin.X),
0.5f * (UpperApronEnd.Y + LowerApronBegin.Y));
// The turn offset must be perpendicular to the actual two-apron endpoint chord,
// not merely perpendicular to the standing-point chord. Opposite-facing doors
// add their offsets to that chord; using the old direction could make the two
// sloped legs nearly collinear and bring the lower leg back beside the upper door.
FVector2D TurnDirection(
LowerApronBegin.Y - UpperApronEnd.Y,
-(LowerApronBegin.X - UpperApronEnd.X));
if (!TurnDirection.Normalize())
{
TurnDirection = FVector2D(-DoorDirection.Y, DoorDirection.X);
if (!TurnDirection.Normalize())
{
TurnDirection = FVector2D(0.0f, 1.0f);
}
}
// There are two sides on which the switchback can turn. Choose the side whose
// two sloped legs initially move away from their own landing rooms; the old fixed
// sign could send a slope back through a room floor before reaching open space.
FVector2D UpperOutward(
UpperApronEnd.X - Passage.UpperLanding.StandingPoint.X,
UpperApronEnd.Y - Passage.UpperLanding.StandingPoint.Y);
FVector2D LowerOutward(
LowerApronBegin.X - Passage.LowerLanding.StandingPoint.X,
LowerApronBegin.Y - Passage.LowerLanding.StandingPoint.Y);
UpperOutward.Normalize();
LowerOutward.Normalize();
const float TurnOffsetForScore =
0.5f * VoxelPassageGeometry::RequiredHorizontalRunForFloorDrop(
UpperFloorZ - 0.5f * (UpperFloorZ + LowerFloorZ),
0.5f * (UpperFloorZ + LowerFloorZ) - LowerFloorZ)
+ VoxelPassageGeometry::WalkableTunnelTurnTransitionVoxels;
const FVector2D TurnMidPlus(
RampStartMid.X + TurnDirection.X * TurnOffsetForScore,
RampStartMid.Y + TurnDirection.Y * TurnOffsetForScore);
const FVector2D TurnMidMinus(
RampStartMid.X - TurnDirection.X * TurnOffsetForScore,
RampStartMid.Y - TurnDirection.Y * TurnOffsetForScore);
const auto OutwardTurnScore = [
&UpperOutward, &LowerOutward, &UpperApronEnd, &LowerApronBegin]
(const FVector2D& Candidate) -> float
{
return FVector2D::DotProduct(
Candidate - FVector2D(
UpperApronEnd.X, UpperApronEnd.Y), UpperOutward)
+ FVector2D::DotProduct(
FVector2D(LowerApronBegin.X, LowerApronBegin.Y) - Candidate,
LowerOutward);
};
if (OutwardTurnScore(TurnMidMinus) > OutwardTurnScore(TurnMidPlus))
{
TurnDirection *= -1.0f;
}
const float UpperFloorDrop = UpperFloorZ - MidFloorZ;
const float LowerFloorDrop = MidFloorZ - LowerFloorZ;
const float RequiredHorizontalRun =
VoxelPassageGeometry::RequiredHorizontalRunForFloorDrop(
UpperFloorDrop, LowerFloorDrop);
// Move the level turn farther than half the required run. Each slope then loses
// only the named transition length to its level segment and still retains the
// full horizontal run required by the 15-degree floor law.
const float TurnTransition =
VoxelPassageGeometry::WalkableTunnelTurnTransitionVoxels;
const float TurnOffset =
0.5f * RequiredHorizontalRun + TurnTransition;
const FVector2D FloorSafeDoorMid(
RampStartMid.X
+ TurnDirection.X * TurnOffset,
RampStartMid.Y
+ TurnDirection.Y * TurnOffset);
FVector2D UpperSlopeDirection(
FloorSafeDoorMid.X - UpperApronEnd.X,
FloorSafeDoorMid.Y - UpperApronEnd.Y);
if (!UpperSlopeDirection.Normalize())
{
UpperSlopeDirection = FVector2D(
DoorDirection.X, DoorDirection.Y);
}
FVector2D LowerSlopeDirection(
LowerApronBegin.X - FloorSafeDoorMid.X,
LowerApronBegin.Y - FloorSafeDoorMid.Y);
if (!LowerSlopeDirection.Normalize())
{
LowerSlopeDirection = -UpperSlopeDirection;
}
const FVector2D UpperSlopeStartXY(
UpperApronEnd.X + UpperSlopeDirection.X * TurnTransition,
UpperApronEnd.Y + UpperSlopeDirection.Y * TurnTransition);
const FVector2D LowerSlopeEndXY(
LowerApronBegin.X - LowerSlopeDirection.X * TurnTransition,
LowerApronBegin.Y - LowerSlopeDirection.Y * TurnTransition);
const FVector UpperApronEndPoint(
UpperApronEnd.X, UpperApronEnd.Y,
UpperFloorZ + FMath::Abs(Cfg.MouthRadius));
const FVector UpperSlopeStart(
UpperSlopeStartXY.X, UpperSlopeStartXY.Y,
UpperFloorZ + FMath::Abs(Cfg.MouthRadius));
const FVector MidPoint(
FloorSafeDoorMid.X, FloorSafeDoorMid.Y,
MidFloorZ + FMath::Abs(Cfg.MidRadius));
const FVector LowerSlopeEnd(
LowerSlopeEndXY.X, LowerSlopeEndXY.Y,
LowerFloorZ + FMath::Abs(Cfg.MouthRadius));
const FVector LowerApronBeginPoint(
LowerApronBegin.X, LowerApronBegin.Y,
LowerFloorZ + FMath::Abs(Cfg.MouthRadius));
Passage.ControlPoints.Reset(7);
Passage.ControlRadii.Reset(7);
Passage.ControlPoints.Add(UpperDoor);
Passage.ControlPoints.Add(UpperApronEndPoint);
Passage.ControlPoints.Add(UpperSlopeStart);
Passage.ControlPoints.Add(MidPoint);
Passage.ControlPoints.Add(LowerSlopeEnd);
Passage.ControlPoints.Add(LowerApronBeginPoint);
Passage.ControlPoints.Add(LowerDoor);
Passage.ControlRadii.Add(Cfg.MouthRadius);
Passage.ControlRadii.Add(Cfg.MouthRadius);
Passage.ControlRadii.Add(Cfg.MouthRadius);
Passage.ControlRadii.Add(Cfg.MidRadius);
Passage.ControlRadii.Add(Cfg.MouthRadius);
Passage.ControlRadii.Add(Cfg.MouthRadius);
Passage.ControlRadii.Add(Cfg.MouthRadius);
Passage.PassageType = EVoxelPassageType::SlopedTunnel;
Passage.bWalkableTunnelContract = true;
Passage.TunnelMaxGradientDegrees =
VoxelPassageGeometry::WalkableTunnelMaxGradientDegrees;
Passage.TunnelVerticalDropVoxels = VerticalDrop;
Passage.TunnelRequiredHorizontalRunVoxels = RequiredHorizontalRun;
Passage.TunnelHorizontalPathLengthVoxels = 0.0f;
for (int32 ControlIndex = 0;
ControlIndex + 1 < Passage.ControlPoints.Num();
++ControlIndex)
{
Passage.TunnelHorizontalPathLengthVoxels += FVector2D(
Passage.ControlPoints[ControlIndex + 1].X
- Passage.ControlPoints[ControlIndex].X,
Passage.ControlPoints[ControlIndex + 1].Y
- Passage.ControlPoints[ControlIndex].Y).Size();
}
Passage.TunnelMinimumClearWidthVoxels = 2.0f * FMath::Max(
FMath::Min(FMath::Abs(Cfg.MouthRadius), FMath::Abs(Cfg.MidRadius)),
0.0f);
Passage.TunnelClearHeightVoxels = Passage.TunnelMinimumClearWidthVoxels;
}
else if (Passage.ControlPoints.Num() > 0)
{
Passage.ControlPoints[0] = Passage.UpperLanding.DoorPoint;
Passage.ControlPoints.Last() = Passage.LowerLanding.DoorPoint;
@@ -1258,36 +1658,61 @@ void UVoxelStrateManager::GeneratePassages()
*NoQueryList);
//=========================================================================
// SURFACE ENTRY SHAFT — the one auto-opened (0,0) connection.
// A straight vertical shaft at (0,0) piercing the TOP seal of the topmost
// strate, so the world begins with "a hole opened to the surface". All other
// (0,0) descents between strates remain player-dug.
// SURFACE ENTRY — the one optional above-ground opening.
// It opens the top seal and stops at the ceiling of strate 0's origin landing room. It does
// not continue down the room or into lower strates: every lower seal remains closed until the
// corresponding progression passage is opened.
//=========================================================================
if (bOpenSurfaceEntry && StrateLayout.Num() > 0)
if (bOpenSurfaceEntry && OriginSpineRadius > 0.0f && StrateLayout.Num() > 0)
{
const FStrateSlot& Top = StrateLayout[0];
const float TopZ = (float)(Top.TopChunkZ + 1) * CHUNK_SIZE;
FVoxelPassage Entry;
Entry.UpperStrateIndex = 0;
Entry.LowerStrateIndex = 0;
Entry.PassageType = EVoxelPassageType::VerticalShaft;
Entry.Radius = FMath::Max(OriginSpineRadius * 0.7f, 4.0f);
// From a little above the strate top (open air outside all strates) down
// past the seal into the interior, so the seal at (0,0) is breached.
Entry.UpperPoint = FVector(0.0f, 0.0f, TopZ + CHUNK_SIZE);
Entry.LowerPoint = FVector(0.0f, 0.0f, TopZ - CHUNK_SIZE);
const float BottomZ = (float)Top.BottomChunkZ * CHUNK_SIZE;
const float TopSeal = Top.Definition
? BoundarySealThicknessFor(*Top.Definition) : 0.0f;
const VoxelPassageGeometry::FOriginLandingGeometry OriginLanding =
VoxelPassageGeometry::BuildOriginLandingGeometry(
TopZ, BottomZ, TopSeal, OriginSpineRadius);
if (!OriginLanding.bValid)
{
const FVector C = (Entry.UpperPoint + Entry.LowerPoint) * 0.5f;
const float R = (float)FVector::Dist(C, Entry.UpperPoint) + Entry.Radius + 4.0f;
Entry.BoundCenter = C;
Entry.BoundRadius = R;
Entry.BoundRadiusSq = R * R;
UE_LOG(LogTemp, Warning,
TEXT("[StrateManager] Surface entry skipped: top origin landing has no seal-safe room interval."));
}
Passages.Add(Entry);
else
{
FVoxelPassage Entry;
Entry.UpperStrateIndex = 0;
Entry.LowerStrateIndex = 0;
// Keep the legacy type: this is the explicitly opted-in above-ground vertical opening,
// not the default inter-strate descent style.
Entry.PassageType = EVoxelPassageType::VerticalShaft;
Entry.Radius = FMath::Max(OriginSpineRadius * 0.7f, 4.0f);
Entry.UpperPoint = FVector(0.0f, 0.0f, TopZ + CHUNK_SIZE);
// The capsule's lower tangent meets the room ceiling; it never bores through the
// room's floor or creates an origin column in strate 0.
Entry.LowerPoint = FVector(
0.0f, 0.0f, OriginLanding.CeilingZ + Entry.Radius);
Entry.ControlPoints.Reset(2);
Entry.ControlPoints.Add(Entry.UpperPoint);
Entry.ControlPoints.Add(Entry.LowerPoint);
Entry.ControlRadii.Reset(2);
Entry.ControlRadii.Add(Entry.Radius);
Entry.ControlRadii.Add(Entry.Radius);
Entry.TunnelClearHeightVoxels = 2.0f * Entry.Radius;
Entry.TunnelMinimumClearWidthVoxels = 2.0f * Entry.Radius;
{
const FVector C = (Entry.UpperPoint + Entry.LowerPoint) * 0.5f;
const float R = (float)FVector::Dist(C, Entry.UpperPoint) + Entry.Radius + 4.0f;
Entry.BoundCenter = C;
Entry.BoundRadius = R;
Entry.BoundRadiusSq = R * R;
}
Passages.Add(Entry);
UE_LOG(LogTemp, Log, TEXT("[StrateManager] Surface entry shaft at (0,0) topZ=%.0f R=%.1f"),
TopZ, Entry.Radius);
UE_LOG(LogTemp, Log,
TEXT("[StrateManager] Surface entry opening at (0,0) topZ=%.0f roomFloor=%.1f roomCeiling=%.1f R=%.1f"),
TopZ, OriginLanding.FloorZ, OriginLanding.CeilingZ, Entry.Radius);
}
}
// Invalidate any thread_local per-chunk passage shortlists (see EvaluateModifierSDF).
@@ -1383,7 +1808,8 @@ void UVoxelStrateManager::ApplyPassageModifier(
{
const FVoxelPassage& Passage = Passages[PassageIndex];
if (VF_IsPassageLandingFloor(Position, Passage.UpperLanding)
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding))
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding)
|| VF_IsWalkableTunnelFloor(Passage, Position))
{
// This is the one bidirectional part of PassageCarveOp: a floor is a proved solid
// support slab. It is deliberately applied after the air carve so a tube can never
@@ -1392,6 +1818,12 @@ void UVoxelStrateManager::ApplyPassageModifier(
break;
}
}
// A neighbouring passage may own a floor at this XY while this point is in the air core of
// the current walkable tunnel. Reassert tunnel air after all floor posts so overlap cannot
// turn a valid route into a solid plug. At the tunnel's own floor the air predicate is false,
// so the support plane remains solid.
ApplyPassageTunnelAir(Density, WorldX, WorldY, WorldZ, BaseDensity, SealThickness);
}
void UVoxelStrateManager::ApplyPassageLandingAir(
@@ -1406,6 +1838,13 @@ void UVoxelStrateManager::ApplyPassageLandingAir(
if (Nearby.Num() == 0) return;
const FVector Position(WorldX, WorldY, WorldZ);
// A smooth room blend may overlap the end of a ramp. The analytic support plane owns that
// overlap; otherwise landing air can erase the tunnel's final-density floor after the tunnel
// air post has deliberately stopped above it.
if (VF_IsAnyPassageFloorAt(this, Position))
{
return;
}
float MinLandingSDF = FLT_MAX;
for (const int32 PassageIndex : Nearby)
{
@@ -1448,6 +1887,35 @@ void UVoxelStrateManager::ApplyPassageLandingAirMC(
Density = -InternalDensity;
}
void UVoxelStrateManager::ApplyPassageTunnelAir(
float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const
{
const FVector Position(WorldX, WorldY, WorldZ);
int32 PassageIndex = INDEX_NONE;
float FloorZ = 0.0f;
float SupportRadius = 0.0f;
const bool bFoundWalkableAir = VF_FindWalkableTunnelAir(
this, Position, PassageIndex, FloorZ, SupportRadius);
if (bFoundWalkableAir)
{
const float AirTarget = -(BaseDensity * 2.0f + SealThickness + 4.0f);
Density = FMath::Min(Density, AirTarget);
}
}
void UVoxelStrateManager::ApplyPassageTunnelAirMC(
float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const
{
// The disturbance layer uses MC polarity (negative = solid), so reuse the exact internal
// tunnel-air operation rather than maintaining a second polarity-specific formula.
float InternalDensity = -Density;
ApplyPassageTunnelAir(
InternalDensity, WorldX, WorldY, WorldZ, BaseDensity, SealThickness);
Density = -InternalDensity;
}
void UVoxelStrateManager::ApplyPassageLandingFloorMC(
float& Density, float WorldX, float WorldY, float WorldZ, float BaseDensity) const
{
@@ -1461,7 +1929,8 @@ void UVoxelStrateManager::ApplyPassageLandingFloorMC(
{
const FVoxelPassage& Passage = Passages[PassageIndex];
if (VF_IsPassageLandingFloor(Position, Passage.UpperLanding)
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding))
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding)
|| VF_IsWalkableTunnelFloor(Passage, Position))
{
// Result is in MC convention here (negative = solid). This reassertion is the
// structural floor backstop after the optional MC-space disturbance layer.
@@ -1471,6 +1940,46 @@ void UVoxelStrateManager::ApplyPassageLandingFloorMC(
}
}
static bool VF_IsPassageRoomFloor(
const FVector& Position, const FVoxelPassageLanding& Landing)
{
if (!FMath::IsFinite(Position.X) || !FMath::IsFinite(Position.Y)
|| !FMath::IsFinite(Position.Z) || !FMath::IsFinite(Landing.FloorZ)
|| !FMath::IsFinite(Landing.FloorThickness)
|| Landing.FloorThickness <= 0.0f || Landing.HalfWidth <= 0.0f)
{
return false;
}
return Position.Z <= Landing.FloorZ + KINDA_SMALL_NUMBER
&& Position.Z > Landing.FloorZ - Landing.FloorThickness
&& FMath::Abs(Position.X - Landing.StandingPoint.X)
<= FMath::Max(Landing.HalfWidth - 1.0f, 0.0f)
&& FMath::Abs(Position.Y - Landing.StandingPoint.Y)
<= FMath::Max(Landing.HalfWidth - 1.0f, 0.0f);
}
void UVoxelStrateManager::ApplyPassageLandingRoomFloorMC(
float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity) const
{
const FIntVector ChunkCoord(
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE));
const TArray<int32>& Nearby = VF_GetNearbyPassages(this, ChunkCoord);
const FVector Position(WorldX, WorldY, WorldZ);
for (const int32 PassageIndex : Nearby)
{
const FVoxelPassage& Passage = Passages[PassageIndex];
if (VF_IsPassageRoomFloor(Position, Passage.UpperLanding)
|| VF_IsPassageRoomFloor(Position, Passage.LowerLanding))
{
Density = FMath::Min(Density, -BaseDensity);
break;
}
}
}
bool UVoxelStrateManager::AnyPassageNearBox(const FVector& MinVoxel, const FVector& MaxVoxel) const
{
// Le carve d'un passage atteint ModSDF < PASSAGE_BLEND_RADIUS (4, VoxelGenerator.cpp) au-delà de
@@ -1564,20 +2073,48 @@ bool UVoxelStrateManager::AnyPassageLandingFloorNearBox(
return true;
}
if (Landing->RootSpineRadius >= 0.0f)
}
}
// The default tube also owns a three-voxel support band. Its conservative segment AABB
// keeps a classifier from proving AllAir over the floor and then dropping that support
// during meshing. False positives are intentional; false negatives would be a hole.
if (Passage.bWalkableTunnelContract
&& Passage.ControlPoints.Num() >= 2
&& Passage.ControlRadii.Num() == Passage.ControlPoints.Num())
{
constexpr float Pad = 1.0f;
for (int32 SegmentIndex = 0;
SegmentIndex + 1 < Passage.ControlPoints.Num();
++SegmentIndex)
{
const FVector& A = Passage.ControlPoints[SegmentIndex];
const FVector& B = Passage.ControlPoints[SegmentIndex + 1];
const float SupportRadius = FMath::Max(
FMath::Min(
FMath::Abs(Passage.ControlRadii[SegmentIndex]),
FMath::Abs(Passage.ControlRadii[SegmentIndex + 1])) - 0.5f,
VoxelPassageGeometry::PlayerRadiusVoxels);
const float FloorMinZ = FMath::Min(
VoxelPassageGeometry::TunnelFloorZ(
A, Passage.ControlRadii[SegmentIndex]),
VoxelPassageGeometry::TunnelFloorZ(
B, Passage.ControlRadii[SegmentIndex + 1]))
- VoxelPassageGeometry::LandingFloorThicknessVoxels - Pad;
const float FloorMaxZ = FMath::Max(
VoxelPassageGeometry::TunnelFloorZ(
A, Passage.ControlRadii[SegmentIndex]),
VoxelPassageGeometry::TunnelFloorZ(
B, Passage.ControlRadii[SegmentIndex + 1])) + Pad;
const float FloorMinX = FMath::Min(A.X, B.X) - SupportRadius - Pad;
const float FloorMaxX = FMath::Max(A.X, B.X) + SupportRadius + Pad;
const float FloorMinY = FMath::Min(A.Y, B.Y) - SupportRadius - Pad;
const float FloorMaxY = FMath::Max(A.Y, B.Y) + SupportRadius + Pad;
if (FloorMaxX >= MinVoxel.X && FloorMinX <= MaxVoxel.X
&& FloorMaxY >= MinVoxel.Y && FloorMinY <= MaxVoxel.Y
&& FloorMaxZ >= MinVoxel.Z && FloorMinZ <= MaxVoxel.Z)
{
const float HubPad = Landing->RootSpineRadius
+ Landing->ConnectorRadius + Pad;
const float HubMinZ = Landing->RootFloorZ
- Landing->FloorThickness - Pad;
const float HubMaxZ = Landing->RootFloorZ + Pad;
if (HubPad >= 0.0f
&& HubPad >= MinVoxel.X && -HubPad <= MaxVoxel.X
&& HubPad >= MinVoxel.Y && -HubPad <= MaxVoxel.Y
&& HubMaxZ >= MinVoxel.Z && HubMinZ <= MaxVoxel.Z)
{
return true;
}
return true;
}
}
}
@@ -1585,6 +2122,48 @@ bool UVoxelStrateManager::AnyPassageLandingFloorNearBox(
return false;
}
bool UVoxelStrateManager::AnyOriginLandingNearBox(
const FVector& MinVoxel, const FVector& MaxVoxel) const
{
if (OriginSpineRadius <= 0.0f) return false;
const FBox VoxelBox(MinVoxel, MaxVoxel);
for (const FStrateSlot& Slot : StrateLayout)
{
if (Slot.Definition == nullptr) continue;
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * CHUNK_SIZE;
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * CHUNK_SIZE;
if (VoxelPassageGeometry::OriginLandingRoomTouchesBox(
VoxelBox, TopZ, BottomZ,
VF_BoundarySealThicknessForDefinition(*Slot.Definition),
OriginSpineRadius))
{
return true;
}
}
return false;
}
bool UVoxelStrateManager::AnyOriginLandingFloorNearBox(
const FVector& MinVoxel, const FVector& MaxVoxel) const
{
if (OriginSpineRadius <= 0.0f) return false;
const FBox VoxelBox(MinVoxel, MaxVoxel);
for (const FStrateSlot& Slot : StrateLayout)
{
if (Slot.Definition == nullptr) continue;
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * CHUNK_SIZE;
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * CHUNK_SIZE;
if (VoxelPassageGeometry::OriginLandingFloorTouchesBox(
VoxelBox, TopZ, BottomZ,
VF_BoundarySealThicknessForDefinition(*Slot.Definition),
OriginSpineRadius))
{
return true;
}
}
return false;
}
//=============================================================================
// QUERIES
//=============================================================================
@@ -2863,7 +2863,8 @@ bool VF_MeasurePlayerFitWalkWithSampler(
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
FVoxelPlayerFitWalkReport& OutReport)
FVoxelPlayerFitWalkReport& OutReport,
const TArray<FVector>* ComponentProbePoints)
{
using namespace VoxelStrateMeasurePrivate;
@@ -3038,6 +3039,28 @@ bool VF_MeasurePlayerFitWalkWithSampler(
? Components[Start] : INDEX_NONE;
const int32 GoalComponent = Goal >= 0 && Components.IsValidIndex(Goal)
? Components[Goal] : INDEX_NONE;
if (ComponentProbePoints != nullptr)
{
OutReport.ComponentProbePlayerFit.Init(0u, ComponentProbePoints->Num());
OutReport.ComponentProbeInStartComponent.Init(0u, ComponentProbePoints->Num());
for (int32 ProbeIndex = 0; ProbeIndex < ComponentProbePoints->Num(); ++ProbeIndex)
{
int32 ProbeCell = INDEX_NONE;
const FVector& ProbePoint = (*ComponentProbePoints)[ProbeIndex];
if (!FindCellForPoint(Grid, ProbePoint, ProbeCell)
|| !IsEligiblePlayerFitCell(Grid, PlayerFitMask, ProbeCell))
{
continue;
}
OutReport.ComponentProbePlayerFit[ProbeIndex] = 1u;
if (StartComponent >= 0 && Components.IsValidIndex(ProbeCell)
&& Components[ProbeCell] == StartComponent)
{
OutReport.ComponentProbeInStartComponent[ProbeIndex] = 1u;
}
}
}
if (StartComponent >= 0)
{
float BestOriginDistanceSquared = FLT_MAX;
@@ -51,12 +51,11 @@ enum class ECaveGeneratorType : uint8;
* A source query answer selects the landing's XY/Z anchor. Because that pure query does not
* flood-fill the live network, every inter-strate end also gets a deterministic root connector
* (`bHasNetworkConnector`). Its flat floor ramps to the same common interior root level for that
* strate, so both mouths enter one walkable hub even when their authored floors are at opposite
* sides of the strate. The hub is an annular walkable network around (0,0), not the pre-existing
* vertical air shaft. `RootSpineRadius` keeps both the hub air and support slab outside that shaft,
* so the spine remains open. If the direct run would be steeper than 44 degrees, the connector
* uses a deterministic level dog-leg before its final ramp; the bend is geometry, not a per-voxel
* search.
* strate, so both mouths enter the finite landing room at (0,0) even when their authored floors
* are at opposite sides of the strate. `RootSpineRadius` supplies the small endpoint overlap that
* joins the connector to that room; it does not create a vertical shaft. If the direct run would
* be steeper than the named 15 degree tunnel contract, the connector uses a deterministic level
* dog-leg before its final ramp; the bend is geometry, not a per-voxel search.
*/
struct VOXELFORGE_API FVoxelPassageLanding
{
@@ -26,6 +26,7 @@
#include "CoreMinimal.h"
#include "VoxelTypes.h" // SmoothStep01
#include "VoxelPassageGeometry.h" // shared body-sized landing/tunnel geometry
//=============================================================================
// SEAL DE FRONTIÈRE / BOUNDARY SEAL
@@ -110,33 +111,97 @@ FORCEINLINE void VF_ApplyPassageLandingCarving(float& Density, float LandingSDF,
}
//=============================================================================
// SPINE DE DESCENTE (0,0) / (0,0) DESCENT SPINE
// LANDING ROOM ORIGINE (0,0) / ORIGIN LANDING ROOM
//=============================================================================
// Creuse une colonne verticale garantie ouverte au XY monde (0,0) dans l'INTÉRIEUR de la strate
// (entre les seals haut et bas). Les seals sont laissés intacts pour que le joueur doive encore
// creuser à travers pour descendre — ceci ne fait qu'un espace d'atterrissage propre, indépendant
// de l'archétype, aligné à travers toutes les strates.
// The old implementation carved a full-height cylinder. That made the origin visually open but
// left no floor anywhere except at the bottom, so it was a fall shaft rather than a landing. The
// origin primitive is now a top-anchored rounded room with a real flat floor in every strate.
// It never crosses either vertical seal. The inter-strate seal remains solid until a passage (or
// the player) opens it; there is no default vertical bore between rooms.
FORCEINLINE void VF_ApplyOriginSpine(float& Density, float WorldX, float WorldY, float WorldZ,
float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius)
{
if (Radius <= 0.0f) return;
// Stay within the interior — never touch the seal bands.
const float InnerTop = StrateTopZ - SealThickness;
const float InnerBot = StrateBottomZ + SealThickness;
if (WorldZ <= InnerBot || WorldZ >= InnerTop) return;
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
VoxelPassageGeometry::BuildOriginLandingGeometry(
StrateTopZ, StrateBottomZ, SealThickness, Radius);
if (!Geometry.bValid) return;
const float DistXY = FMath::Sqrt(WorldX * WorldX + WorldY * WorldY);
const float SDF = DistXY - Radius; // < 0 inside the column
const float Blend = 3.0f;
if (SDF < Blend)
const float RoomSDF = VoxelPassageGeometry::OriginLandingRoomSDF(
FVector(WorldX, WorldY, WorldZ), Geometry);
VF_ApplyPassageLandingCarving(
Density, RoomSDF, BaseDensity, SealThickness);
// The floor is intentionally written after the room carve. It is also reasserted after the
// passage/disturbance posts by the callers below; doing it here makes the standalone primitive
// itself a complete landing-room operation.
if (WorldZ <= Geometry.FloorZ + KINDA_SMALL_NUMBER
&& WorldZ > Geometry.FloorZ - Geometry.FloorThickness
&& FMath::Abs(WorldX) <= FMath::Max(Geometry.HalfWidth - 1.0f, 0.0f)
&& FMath::Abs(WorldY) <= FMath::Max(Geometry.HalfWidth - 1.0f, 0.0f))
{
float Carve = FMath::Clamp((Blend - SDF) / (Blend * 2.0f), 0.0f, 1.0f);
Carve = SmoothStep01(Carve);
Density -= Carve * (BaseDensity * 2.0f + SealThickness);
Density = FMath::Max(Density, BaseDensity);
}
}
/** Reassert only the origin-room air volume after a post-process that may add rock. */
FORCEINLINE void VF_ApplyOriginLandingAir(float& Density,
float WorldX, float WorldY, float WorldZ,
float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius)
{
if (Radius <= 0.0f) return;
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
VoxelPassageGeometry::BuildOriginLandingGeometry(
StrateTopZ, StrateBottomZ, SealThickness, Radius);
if (!Geometry.bValid) return;
const float RoomSDF = VoxelPassageGeometry::OriginLandingRoomSDF(
FVector(WorldX, WorldY, WorldZ), Geometry);
VF_ApplyPassageLandingCarving(
Density, RoomSDF, BaseDensity, SealThickness);
}
/** Reassert only the origin-room support slab after an air carve/post-process. */
FORCEINLINE void VF_ApplyOriginLandingFloor(float& Density,
float WorldX, float WorldY, float WorldZ,
float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius)
{
if (Radius <= 0.0f) return;
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
VoxelPassageGeometry::BuildOriginLandingGeometry(
StrateTopZ, StrateBottomZ, SealThickness, Radius);
if (!Geometry.bValid) return;
if (WorldZ <= Geometry.FloorZ + KINDA_SMALL_NUMBER
&& WorldZ > Geometry.FloorZ - Geometry.FloorThickness
&& FMath::Abs(WorldX) <= FMath::Max(Geometry.HalfWidth - 1.0f, 0.0f)
&& FMath::Abs(WorldY) <= FMath::Max(Geometry.HalfWidth - 1.0f, 0.0f))
{
Density = FMath::Max(Density, BaseDensity);
}
}
/** MC-facing origin-room air backstop for the post-disturbance path. */
FORCEINLINE void VF_ApplyOriginLandingAirMC(float& Density,
float WorldX, float WorldY, float WorldZ,
float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius)
{
float InternalDensity = -Density;
VF_ApplyOriginLandingAir(InternalDensity, WorldX, WorldY, WorldZ,
StrateTopZ, StrateBottomZ, SealThickness, BaseDensity, Radius);
Density = -InternalDensity;
}
/** MC-facing origin-room floor backstop for the post-disturbance path. */
FORCEINLINE void VF_ApplyOriginLandingFloorMC(float& Density,
float WorldX, float WorldY, float WorldZ,
float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius)
{
float InternalDensity = -Density;
VF_ApplyOriginLandingFloor(InternalDensity, WorldX, WorldY, WorldZ,
StrateTopZ, StrateBottomZ, SealThickness, BaseDensity, Radius);
Density = -InternalDensity;
}
//=============================================================================
// XY EDGE SEAL / SCELLEMENT DE LA LIMITE XY
//=============================================================================
+3 -3
View File
@@ -77,9 +77,9 @@ public:
// Les formes créatives viennent ensuite des params de strate.
int32 Seed = 0;
// Radius (voxels) of the guaranteed open vertical landing column carved at world
// XY (0,0) in every strate — the (0,0) descent spine. Copied from VoxelSettings.
// 0 disables the spine carve.
// Authored radius (voxels) of the guaranteed flat-floored landing room at world XY (0,0) in
// every strate — the old “spine” setting now sizes a room and never creates a vertical bore.
// 0 disables the origin landing rooms. Copied from VoxelSettings.
float OriginSpineRadius = 14.0f;
// Radius (voxels) of the bounded world in actor-space XY, measured from (0,0).
@@ -0,0 +1,308 @@
// Shared dimensions and construction laws for walkable passages and origin landings.
//
// This header is deliberately free of UObjects. The density path, the operator stack, the
// passage generator, and the headless measurements all include the same arithmetic so a geometry
// change cannot leave one of those paths with a different room or slope contract.
#pragma once
#include "CoreMinimal.h"
namespace VoxelPassageGeometry
{
// Authored scale and player body used by the §6.5 landing calculation.
constexpr float VoxelSizeMeters = 0.25f;
constexpr float PlayerRadiusVoxels = 34.0f / 25.0f;
constexpr float PlayerHalfHeightVoxels = 88.0f / 25.0f;
constexpr float PlayerHeightVoxels = 2.0f * PlayerHalfHeightVoxels;
constexpr float HeadroomVoxels = 4.0f; // 1 m
constexpr float MinimumTurnFloorWidthVoxels = 12.0f; // 3 m
constexpr float MinimumLandingHalfWidthVoxels =
MinimumTurnFloorWidthVoxels * 0.5f;
constexpr float MinimumRoomHeightVoxels = 12.0f; // 3 m
constexpr float LandingFloorThicknessVoxels = 3.0f;
constexpr float SealSafetyMarginVoxels = 4.0f;
constexpr float RoomRoundingVoxels = 1.5f;
constexpr float LandingCarveBlendVoxels = 4.0f;
// Level tunnel apron outside each room-floor edge. It covers the room/tunnel SDF blend and
// keeps the ramp from starting while the route is still over the room's square support patch.
constexpr float WalkableTunnelLandingApronVoxels = 6.0f; // 1.5 m
// A level transition after a bend lets the route turn before its sloped segment begins.
constexpr float WalkableTunnelTurnTransitionVoxels = 6.0f; // 1.5 m
// Keep the tunnel-air post a half voxel above the authored support plane. This is a numerical
// ownership band: the floor writer owns the plane, while the air writer owns the capsule
// volume above it even when two overlapping passage projections differ by a fraction.
constexpr float WalkableTunnelFloorAirClearanceVoxels = 0.5f;
// A 15 degree ramp is comfortably below the 44 degree engine walkability ceiling. The
// latter is a scramble limit, not a sensible default for a tunnel the player walks through.
constexpr float WalkableTunnelMaxGradientDegrees = 15.0f;
constexpr float WalkableTunnelMaxGradient = 0.2679491924311227f; // tan(15 degrees)
// The network connector retains the §6.5 single-file dimensions: 2.5 m clear width and 3 m
// clear height. These values are in voxels because all passage descriptors are voxel-space.
constexpr float ConnectorRadiusVoxels = 5.0f;
constexpr float ConnectorHeightVoxels = 12.0f;
constexpr float RootOverlapVoxels = 2.25f;
// The walkability contract is about the floor carried by the tube, not only its centreline.
// A tapered tube changes that floor by the radius delta, so the construction and the audit
// use this same conservative floor delta.
FORCEINLINE float TunnelFloorZ(const FVector& Centreline, float Radius)
{
return Centreline.Z - FMath::Abs(Radius);
}
FORCEINLINE float TunnelFloorGradient(
const FVector& Start, float StartRadius,
const FVector& End, float EndRadius)
{
const float HorizontalRun = FVector2D(
End.X - Start.X, End.Y - Start.Y).Size();
if (HorizontalRun <= KINDA_SMALL_NUMBER)
{
return FMath::Abs(TunnelFloorZ(End, EndRadius)
- TunnelFloorZ(Start, StartRadius)) > KINDA_SMALL_NUMBER
? FLT_MAX : 0.0f;
}
return FMath::Abs(TunnelFloorZ(End, EndRadius)
- TunnelFloorZ(Start, StartRadius)) / HorizontalRun;
}
FORCEINLINE float RequiredHorizontalRunForFloorDrop(
float FirstFloorDrop, float SecondFloorDrop)
{
return 2.0f * FMath::Max(
FMath::Abs(FirstFloorDrop), FMath::Abs(SecondFloorDrop))
/ WalkableTunnelMaxGradient;
}
/** Project a query into the nearest horizontal control segment of a walkable tunnel. */
FORCEINLINE bool ProjectWalkableTunnelFloor(
const TArray<FVector>& ControlPoints,
const TArray<float>& ControlRadii,
const FVector& Position,
float& OutFloorZ,
float& OutSupportRadius,
int32* OutSegmentIndex = nullptr)
{
if (OutSegmentIndex != nullptr)
{
*OutSegmentIndex = INDEX_NONE;
}
if (ControlPoints.Num() < 2
|| ControlRadii.Num() != ControlPoints.Num())
{
return false;
}
const FVector2D QueryXY(Position.X, Position.Y);
float BestDistanceSquared = FLT_MAX;
bool bFoundSegment = false;
for (int32 SegmentIndex = 0;
SegmentIndex + 1 < ControlPoints.Num();
++SegmentIndex)
{
const FVector& A = ControlPoints[SegmentIndex];
const FVector& B = ControlPoints[SegmentIndex + 1];
const FVector2D AXY(A.X, A.Y);
const FVector2D Delta = FVector2D(B.X, B.Y) - AXY;
const float LengthSquared = Delta.SizeSquared();
if (LengthSquared <= KINDA_SMALL_NUMBER)
{
continue;
}
const float T = FMath::Clamp(
FVector2D::DotProduct(QueryXY - AXY, Delta) / LengthSquared,
0.0f, 1.0f);
const FVector2D ClosestXY = AXY + Delta * T;
const float DistanceSquared = (QueryXY - ClosestXY).SizeSquared();
if (DistanceSquared >= BestDistanceSquared)
{
continue;
}
const float StartRadius = FMath::Abs(ControlRadii[SegmentIndex]);
const float EndRadius = FMath::Abs(ControlRadii[SegmentIndex + 1]);
OutFloorZ = FMath::Lerp(
TunnelFloorZ(A, StartRadius),
TunnelFloorZ(B, EndRadius),
T);
OutSupportRadius = FMath::Max(
FMath::Min(StartRadius, EndRadius) - 0.5f,
PlayerRadiusVoxels);
if (OutSegmentIndex != nullptr)
{
*OutSegmentIndex = SegmentIndex;
}
BestDistanceSquared = DistanceSquared;
bFoundSegment = true;
}
return bFoundSegment
&& FMath::IsFinite(OutFloorZ)
&& FMath::IsFinite(OutSupportRadius)
&& OutSupportRadius > 0.0f
&& BestDistanceSquared <= FMath::Square(OutSupportRadius);
}
struct FOriginLandingGeometry
{
bool bValid = false;
float FloorZ = 0.0f;
float CeilingZ = 0.0f;
float HalfWidth = 0.0f;
float FloorThickness = LandingFloorThicknessVoxels;
};
FORCEINLINE float LandingHalfWidthForRadius(float Radius)
{
const float SafeRadius = FMath::Max(FMath::Abs(Radius), 1.0f);
return FMath::Max(MinimumLandingHalfWidthVoxels, SafeRadius + 2.0f);
}
FORCEINLINE float LandingHeightForRadius(float Radius)
{
const float SafeRadius = FMath::Max(FMath::Abs(Radius), 1.0f);
return FMath::Max(
FMath::Max(MinimumRoomHeightVoxels, PlayerHeightVoxels + HeadroomVoxels),
2.0f * SafeRadius + 2.0f);
}
/**
* Build the one origin landing in a strate.
*
* `OriginRadius` is the old spine setting and becomes the room's authored mouth radius. The
* room dimensions therefore reuse the §6.5 formula exactly:
* half-width = max(7, radius + 2),
* height = max(12, capsule height + 4, 2 * radius + 2).
* The floor is placed against the top of the safe interior interval. This makes the first
* landing the place reached from above, while every lower landing remains sealed above until
* progression opens its inter-strate passage.
*/
FORCEINLINE FOriginLandingGeometry BuildOriginLandingGeometry(
float StrateTopZ, float StrateBottomZ, float SealThickness, float OriginRadius)
{
FOriginLandingGeometry Result;
if (!(OriginRadius > 0.0f)
|| !FMath::IsFinite(StrateTopZ)
|| !FMath::IsFinite(StrateBottomZ)
|| !FMath::IsFinite(SealThickness)
|| !FMath::IsFinite(OriginRadius))
{
return Result;
}
Result.HalfWidth = LandingHalfWidthForRadius(OriginRadius);
const float DesiredHeight = LandingHeightForRadius(OriginRadius);
const float SafeSeal = FMath::Max(SealThickness, 0.0f);
const float InnerBottom = StrateBottomZ + SafeSeal;
const float InnerTop = StrateTopZ - SafeSeal;
const float MinimumFloor = InnerBottom
+ Result.FloorThickness + SealSafetyMarginVoxels;
const float MaximumFloor = InnerTop
- DesiredHeight - SealSafetyMarginVoxels;
if (!FMath::IsFinite(InnerBottom) || !FMath::IsFinite(InnerTop)
|| !(InnerTop > InnerBottom)
|| !FMath::IsFinite(MinimumFloor)
|| !FMath::IsFinite(MaximumFloor)
|| MaximumFloor < MinimumFloor)
{
Result.HalfWidth = 0.0f;
return Result;
}
Result.FloorZ = MaximumFloor;
Result.CeilingZ = Result.FloorZ + DesiredHeight;
Result.bValid = true;
return Result;
}
FORCEINLINE bool IntersectsClosedInterval(float MinA, float MaxA,
float MinB, float MaxB)
{
return MaxA >= MinB && MinA <= MaxB;
}
/** Conservative influence box for the origin room's air carve. */
FORCEINLINE bool OriginLandingRoomTouchesBox(
const FBox& VoxelBox,
float StrateTopZ, float StrateBottomZ, float SealThickness, float OriginRadius)
{
if (!VoxelBox.IsValid) return false;
const FOriginLandingGeometry Geometry = BuildOriginLandingGeometry(
StrateTopZ, StrateBottomZ, SealThickness, OriginRadius);
if (!Geometry.bValid) return false;
const float Pad = LandingCarveBlendVoxels;
return IntersectsClosedInterval(
VoxelBox.Min.X, VoxelBox.Max.X,
-Geometry.HalfWidth - Pad, Geometry.HalfWidth + Pad)
&& IntersectsClosedInterval(
VoxelBox.Min.Y, VoxelBox.Max.Y,
-Geometry.HalfWidth - Pad, Geometry.HalfWidth + Pad)
&& IntersectsClosedInterval(
VoxelBox.Min.Z, VoxelBox.Max.Z,
Geometry.FloorZ - Pad, Geometry.CeilingZ + Pad);
}
/** Conservative influence box for the origin room's guaranteed solid floor. */
FORCEINLINE bool OriginLandingFloorTouchesBox(
const FBox& VoxelBox,
float StrateTopZ, float StrateBottomZ, float SealThickness, float OriginRadius)
{
if (!VoxelBox.IsValid) return false;
const FOriginLandingGeometry Geometry = BuildOriginLandingGeometry(
StrateTopZ, StrateBottomZ, SealThickness, OriginRadius);
if (!Geometry.bValid) return false;
// One voxel is a proof margin, not a write margin. It keeps ClassifyTile conservative at
// an exact lattice edge and mirrors the existing passage-floor guard.
constexpr float ProofPad = 1.0f;
const float FloorHalfWidth = FMath::Max(
Geometry.HalfWidth - 1.0f, 0.0f);
return IntersectsClosedInterval(
VoxelBox.Min.X, VoxelBox.Max.X,
-FloorHalfWidth - ProofPad, FloorHalfWidth + ProofPad)
&& IntersectsClosedInterval(
VoxelBox.Min.Y, VoxelBox.Max.Y,
-FloorHalfWidth - ProofPad, FloorHalfWidth + ProofPad)
&& IntersectsClosedInterval(
VoxelBox.Min.Z, VoxelBox.Max.Z,
Geometry.FloorZ - Geometry.FloorThickness - ProofPad,
Geometry.FloorZ + ProofPad);
}
/** Signed distance for a rounded origin room with a hard flat floor. */
FORCEINLINE float OriginLandingRoomSDF(
const FVector& Position, const FOriginLandingGeometry& Geometry)
{
if (!Geometry.bValid || Geometry.CeilingZ <= Geometry.FloorZ)
{
return FLT_MAX;
}
const FVector Center(0.0f, 0.0f,
(Geometry.FloorZ + Geometry.CeilingZ) * 0.5f);
const float Rounding = RoomRoundingVoxels;
const float HalfZ = (Geometry.CeilingZ - Geometry.FloorZ) * 0.5f;
const FVector HalfExtent(
FMath::Max(Geometry.HalfWidth - Rounding, 0.25f),
FMath::Max(Geometry.HalfWidth - Rounding, 0.25f),
FMath::Max(HalfZ - Rounding, 0.25f));
const FVector D(
FMath::Abs(Position.X - Center.X) - HalfExtent.X,
FMath::Abs(Position.Y - Center.Y) - HalfExtent.Y,
FMath::Abs(Position.Z - Center.Z) - HalfExtent.Z);
const FVector Outside(
FMath::Max(D.X, 0.0f),
FMath::Max(D.Y, 0.0f),
FMath::Max(D.Z, 0.0f));
const float Inside = FMath::Min(
FMath::Max(D.X, FMath::Max(D.Y, D.Z)), 0.0f);
const float RoundedBoxSDF = Outside.Size() + Inside - Rounding;
// Below the floor is rock. Above it the rounded-box air volume is authoritative.
return FMath::Max(RoundedBoxSDF, Geometry.FloorZ - Position.Z);
}
}
+5 -4
View File
@@ -392,10 +392,11 @@ public:
// SPINE & INTER-STRATE CONNECTIONS (the (0,0) descent axis)
//=========================================================================
// Radius (voxels) of the guaranteed open vertical "landing" column carved at
// world XY (0,0) in EVERY strate, regardless of archetype. This is the prepared
// space the player digs THROUGH the seal into when descending. 0 = disabled.
// 10-14 → cozy shaft (default) · 20+ → wide landing chamber
// Authored radius (voxels) of the guaranteed (0,0) landing room in EVERY strate, regardless
// of archetype. The §6.5 body-sized formulas turn it into a flat-floored room; this setting
// no longer bores a vertical column. 0 = disabled. The room is top-anchored inside each
// strate, and the seals remain solid until a progression passage is opened.
// 10-14 → compact landing room (default) · 20+ → broad landing room
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Spine", meta = (ClampMin = "0.0"))
float OriginSpineRadius = 14.0f;
@@ -211,9 +211,11 @@ public:
//=========================================================================
// INTER-STRATE PASSAGES (how THIS strate connects DOWN to the next)
//=========================================================================
// Auto-carved descent tunnels from this strate to the one below it: count, style
// (straight / worm / spiral / cascade), tapered width, length, placement. The (0,0)
// spine descent is separate (player-dug); these are the extra shortcuts.
// Auto-carved passages from this strate to the one below it: count, style
// (walkable sloped tunnel / worm / spiral / cascade), tapered width, length, placement.
// Every strate has its own finite (0,0) landing room; the passage opens the next room through
// progression. The non-default styles are retained as exotic, non-walkability-guaranteed
// descriptors.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Passages")
FStratePassageConfig PassageConfig;
+34 -9
View File
@@ -66,18 +66,17 @@ struct FVoxelPassage
FVector RequestedLowerPoint = FVector::ZeroVector;
// Passage dimensions — how wide the carved tunnel is (in voxels).
// Varies by type: VerticalShaft ~7-8, SpiralDescent ~4, CrackCrevice ~2-3, others ~5.
// The default SlopedTunnel is a single-file 2.5 m clear bore at its narrowest point;
// authored exotic styles retain their own widths and are not covered by the walkable law.
float Radius = 5.0f;
// The shape/style of this passage. Determines how control points are generated
// and how the passage feels to navigate (shaft, spiral, ledges, crack, etc.).
EVoxelPassageType PassageType = EVoxelPassageType::SlopedTunnel;
// Multi-segment control points for complex passage shapes.
// Used by SpiralDescent (helix points) and CascadingDrops (ledge+drop points).
// When non-empty, the SDF evaluator walks this array as a capsule chain
// instead of using Upper→Mid→Lower logic.
// Empty for simple types (SlopedTunnel, VerticalShaft, CrackCrevice).
// Multi-segment control points for passage shapes. The default SlopedTunnel contains the two
// gentle ramp legs of its deterministic switchback; exotic styles retain their authored chain.
// When non-empty, the SDF evaluator walks this array as a capsule chain.
TArray<FVector> ControlPoints;
// Per-control-point tube radius (parallel to ControlPoints) for tapered tunnels —
@@ -90,6 +89,16 @@ struct FVoxelPassage
FVoxelPassageLanding UpperLanding;
FVoxelPassageLanding LowerLanding;
// Construction-time walkability facts for diagnostics and the commandlet report. These are
// metadata, not a second runtime decision: the final-density sampler remains authoritative.
bool bWalkableTunnelContract = false;
float TunnelMaxGradientDegrees = 0.0f;
float TunnelVerticalDropVoxels = 0.0f;
float TunnelRequiredHorizontalRunVoxels = 0.0f;
float TunnelHorizontalPathLengthVoxels = 0.0f;
float TunnelMinimumClearWidthVoxels = 0.0f;
float TunnelClearHeightVoxels = 0.0f;
// Bounding sphere enclosing the whole passage (+ radius + blend), in voxel coords.
// Computed once in GeneratePassages; lets EvaluateModifierSDF reject far voxels with
// a single squared-distance test instead of walking every segment per voxel.
@@ -362,14 +371,26 @@ public:
void ApplyPassageLandingAir(float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const;
/** Reassert the default walkable tunnel's clear air in internal density after any post. */
void ApplyPassageTunnelAir(float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const;
/** Re-assert landing air after MC-space disturbances, still before the final XY seal. */
void ApplyPassageLandingAirMC(float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const;
/** Re-assert walkable tunnel air after MC-space disturbances, still before the final XY seal. */
void ApplyPassageTunnelAirMC(float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const;
/** Re-assert a landing floor after the landing air pass, still before the final XY seal. */
void ApplyPassageLandingFloorMC(float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity) const;
/** Re-assert only the flat room floors after the tunnel-air post; never writes tunnel floors. */
void ApplyPassageLandingRoomFloorMC(float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity) const;
/**
* True si la sphère englobante d'un passage (élargie du rayon de blend de carve) touche la
* boîte VOXEL [MinVoxel, MaxVoxel]. Test conservatif O(Passages) — utilisé par ClassifyTile
@@ -380,6 +401,10 @@ public:
/** Conservative box guard for the floor fill; unlike a tube it can affect the all-air proof. */
bool AnyPassageLandingFloorNearBox(const FVector& MinVoxel, const FVector& MaxVoxel) const;
/** Conservative box guards for the per-strate (0,0) landing rooms and their floor slabs. */
bool AnyOriginLandingNearBox(const FVector& MinVoxel, const FVector& MaxVoxel) const;
bool AnyOriginLandingFloorNearBox(const FVector& MinVoxel, const FVector& MaxVoxel) const;
/** Get all generated passages (for debug display). */
const TArray<FVoxelPassage>& GetPassages() const { return Passages; }
@@ -411,12 +436,12 @@ protected:
// World seed (stored for passage generation)
int32 CachedSeed = 0;
// Whether to auto-open the (0,0) entry shaft through the top of strate 0.
// Whether to auto-open the (0,0) origin landing room from above in strate 0.
// Copied from VoxelSettings::bOpenSurfaceEntry during Initialize().
bool bOpenSurfaceEntry = true;
// Radius (voxels) of the surface entry shaft at (0,0). Copied from
// VoxelSettings::OriginSpineRadius so the entry matches the spine landing.
// Authored radius (voxels) of the origin landing room and its optional surface opening.
// Copied from VoxelSettings::OriginSpineRadius.
float OriginSpineRadius = 14.0f;
// Solid-bedrock gap between consecutive strates, in chunks. Copied from
+15 -6
View File
@@ -3,6 +3,7 @@
#pragma once
#include "CoreMinimal.h"
#include "VoxelPassageGeometry.h"
class UVoxelGenerator;
class UVoxelStrateManager;
@@ -19,7 +20,7 @@ class UVoxelStrateManager;
struct VOXELFORGE_API FVoxelPlayerCapsuleConstants
{
static constexpr float VoxelSizeCentimeters = 25.0f;
static constexpr float VoxelSizeMeters = VoxelSizeCentimeters / 100.0f;
static constexpr float VoxelSizeMeters = VoxelPassageGeometry::VoxelSizeMeters;
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
@@ -29,9 +30,9 @@ struct VOXELFORGE_API FVoxelPlayerCapsuleConstants
// 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;
static constexpr float RadiusVoxels = VoxelPassageGeometry::PlayerRadiusVoxels;
static constexpr float HalfHeightVoxels = VoxelPassageGeometry::PlayerHalfHeightVoxels;
static constexpr float HeightVoxels = VoxelPassageGeometry::PlayerHeightVoxels;
};
/** Read-only density source used by the offline Tier 2 pass for a materialised custom stack. */
@@ -68,7 +69,8 @@ struct VOXELFORGE_API FVoxelStrateMeasureSettings
TOptional<FVector2D> CoverPointA;
TOptional<FVector2D> CoverPointB;
float CoverMarginVoxels = 48.0f;
// Origin-rooted topologies can route from one mouth to the other through the (0,0) spine.
// Origin-rooted topologies can route from one mouth to the other through the finite (0,0)
// landing room and its explicitly carved connector network.
// When true, include (0,0) in the fitted AABB before applying MaxCells. A caller must keep
// this enabled for that topology; an over-cap route window is then refused honestly instead
// of being cropped back to a mouth-only box.
@@ -322,6 +324,12 @@ struct VOXELFORGE_API FVoxelPlayerFitWalkReport
int64 DepartureComponentCells = 0;
float MouthComponentGapVoxels = -1.0f;
// Optional room probes evaluated against this same grid and start component. The arrays are
// empty for ordinary callers; when supplied, each entry is 1 only when the probe is both a
// player-fit cell and in the arrival/start component.
TArray<uint8> ComponentProbePlayerFit;
TArray<uint8> ComponentProbeInStartComponent;
// The DFS returns to its root when the target component is unreachable. Keep both the
// physical end of that walk and the last newly reached pose: the latter is the useful
// failure-frontier point for a render, while the former is the literal final pose.
@@ -496,4 +504,5 @@ VOXELFORGE_API bool VF_MeasurePlayerFitWalkWithSampler(
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
FVoxelPlayerFitWalkReport& OutReport);
FVoxelPlayerFitWalkReport& OutReport,
const TArray<FVector>* ComponentProbePoints = nullptr);
+21 -17
View File
@@ -32,19 +32,18 @@ class AActor; // IWYU : paramètre de TSubclassOf seulement / T
* of the carved tunnel (control point layout, radius, overall feel).
*
* Types:
* SlopedTunnel — Angled bore with a horizontal midpoint offset (default/legacy).
* VerticalShaft — Straight or near-straight drop between strates.
* SpiralDescent — Helical corkscrew path winding downward.
* CascadingDrops — Series of short vertical pits connected by horizontal ledges.
* CrackCrevice — Narrow fracture passage, tight squeeze through rock.
* SlopedTunnel — The default walkable, gentle-gradient tunnel between landing rooms.
* VerticalShaft — A vertical drop; never a continuous walkable descent.
* SpiralDescent — An exotic helix; walkability requires a separately designed ramp contract.
* CascadingDrops — Drops and ledges; never a continuous walkable descent as authored.
* CrackCrevice — A narrow fracture; no player-fit guarantee.
*/
UENUM(BlueprintType)
enum class EVoxelPassageType : uint8
{
// Angled bore connecting two layers with a horizontal midpoint offset.
// Two capsule segments: Upper→Mid, Mid→Lower. Comfortable radius (~5 voxels).
// This is the original passage type — feels like a natural sloped tunnel.
SlopedTunnel UMETA(DisplayName = "Sloped Tunnel (angled bore)"),
// Walkable single-file tunnel. The runtime builds a deterministic switchback whose each ramp
// is below VoxelPassageGeometry::WalkableTunnelMaxGradientDegrees.
SlopedTunnel UMETA(DisplayName = "Sloped Tunnel (walkable default)"),
// Straight vertical drop between strates. Same XY for upper and lower points.
// Wider radius (~7-8 voxels) to feel like a natural shaft or sinkhole.
@@ -1758,7 +1757,9 @@ struct VOXELFORGE_API FStrateDisturbanceParams
UENUM(BlueprintType)
enum class EVoxelPassageStyle : uint8
{
Straight UMETA(DisplayName = "Straight (vertical shaft)"),
// “Straight” is retained as the authored style name for the base connection, but its runtime
// geometry is a gentle two-leg switchback rather than a vertical shaft.
Straight UMETA(DisplayName = "Straight (walkable sloped tunnel)"),
Worm UMETA(DisplayName = "Worm (organic meander around the axis)"),
Spiral UMETA(DisplayName = "Spiral (corkscrew descent)"),
Cascading UMETA(DisplayName = "Cascading (ledge + drop staircase)")
@@ -1768,8 +1769,8 @@ enum class EVoxelPassageStyle : uint8
* FStratePassageConfig — how THIS strate connects DOWN to the strate below it.
*
* Lives on each UVoxelStrateDefinition: the upper strate of every boundary controls its
* own descent tunnels, so different layers connect differently. The (0,0) spine descent
* is separate (player-dug); these are the auto-carved shortcuts placed away from it.
* own progression tunnel, so different layers connect differently. Each strate has a finite
* (0,0) landing room; this config controls the passage that opens the next room.
*/
USTRUCT(BlueprintType)
struct VOXELFORGE_API FStratePassageConfig
@@ -1782,17 +1783,19 @@ struct VOXELFORGE_API FStratePassageConfig
// Tunnel shape.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage")
EVoxelPassageStyle Style = EVoxelPassageStyle::Worm;
EVoxelPassageStyle Style = EVoxelPassageStyle::Straight;
// ----- WIDTH (tapers along the length) -----
// Radius at the two mouths (entry/exit) and at the middle. Equal = uniform tube;
// The defaults are 2 m radius / 4 m bore at mouths and 1.5 m radius / 3 m bore in the middle.
// The walkable default is deliberately single-file: 3.0 m diameter at mouths and 2.5 m at
// the waist (6/5 voxel radii at 25 cm per voxel). The §6.5 landing room remains wider for
// turning.
// Mouth > Mid = chambers at the ends with a squeeze between; Mid > Mouth = a bulge.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Width", meta = (ClampMin = "1.0"))
float MouthRadius = 8.0f;
float MouthRadius = 6.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Width", meta = (ClampMin = "1.0"))
float MidRadius = 6.0f;
float MidRadius = 5.0f;
// ----- LENGTH -----
// How far the tunnel reaches INTO each strate (voxels). Auto-capped to the interior;
@@ -1813,7 +1816,8 @@ struct VOXELFORGE_API FStratePassageConfig
float DistanceMax = 192.0f;
// ----- SHAPE detail -----
// Worm: max sideways excursion from the axis (voxels). 24 = 6 m; 0 = straight even in Worm style.
// Exotic styles only: max sideways excursion from the axis (voxels). 24 = 6 m; 0 = straight
// even in Worm style. The default walkable switchback does not use this wander value.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Shape", meta = (ClampMin = "0.0"))
float Wander = 24.0f;
@@ -384,7 +384,10 @@ struct FExploreWorld
Index));
Definition->StrateDescription = FText::FromString(
TEXT("Transient, deterministic commandlet definition."));
Definition->StrateHeightInChunks = 8;
// The commandlet's acceptance sweep uses the owner-facing base spacing:
// 4 chunks * 32 voxels * 0.25 m = 32 m per strate. Production assets may be taller;
// this keeps the measured tunnel arithmetic tied to the stated ~32 m descent.
Definition->StrateHeightInChunks = 4;
Definition->TransitionType = EVoxelStrateTransition::Hard;
Definition->GeneratorType = Index == Arguments.Slot
? Arguments.Archetype
@@ -510,6 +513,12 @@ struct FExploreWalkOutput
bool bOriginCheckAvailable = false;
FString OriginCheckRefusalReason;
FVoxelPlayerFitWalkReport OriginCheckReport;
int32 ArrivalLandingRoomProbeCount = 0;
int32 ArrivalLandingRoomPlayerFitProbeCount = 0;
int32 ArrivalLandingRoomArrivalComponentProbeCount = 0;
int32 DepartureLandingRoomProbeCount = 0;
int32 DepartureLandingRoomPlayerFitProbeCount = 0;
int32 DepartureLandingRoomArrivalComponentProbeCount = 0;
};
struct FExploreExportOutput
@@ -542,12 +551,16 @@ bool FindMouths(
FVector& OutArrival,
FVector& OutDeparture,
int32& OutArrivalCount,
int32& OutDepartureCount)
int32& OutDepartureCount,
const FVoxelPassage*& OutArrivalPassage,
const FVoxelPassage*& OutDeparturePassage)
{
OutArrival = FVector::ZeroVector;
OutDeparture = FVector::ZeroVector;
OutArrivalCount = 0;
OutDepartureCount = 0;
OutArrivalPassage = nullptr;
OutDeparturePassage = nullptr;
for (const FVoxelPassage& Passage : Manager.GetPassages())
{
if (Passage.LowerStrateIndex == TargetSlot
@@ -556,6 +569,7 @@ bool FindMouths(
if (OutArrivalCount == 0)
{
OutArrival = Passage.LowerPoint;
OutArrivalPassage = &Passage;
}
++OutArrivalCount;
}
@@ -565,6 +579,7 @@ bool FindMouths(
if (OutDepartureCount == 0)
{
OutDeparture = Passage.UpperPoint;
OutDeparturePassage = &Passage;
}
++OutDepartureCount;
}
@@ -1273,13 +1288,17 @@ bool RunWalk(
return false;
}
const FVoxelPassage* ArrivalPassage = nullptr;
const FVoxelPassage* DeparturePassage = nullptr;
if (!FindMouths(
*World.Manager,
Arguments.Slot,
OutOutput.ArrivalVoxels,
OutOutput.DepartureVoxels,
OutOutput.ArrivalPassageCount,
OutOutput.DeparturePassageCount))
OutOutput.DeparturePassageCount,
ArrivalPassage,
DeparturePassage))
{
OutOutput.Status = TEXT("refused");
OutOutput.RefusalReason = TEXT(
@@ -1299,6 +1318,32 @@ bool RunWalk(
Settings.PlayerCapsuleRadiusVoxels) + 2.0f;
Settings.bIncludeOriginInCoverWindow = NeedsOriginInWindow(Arguments.Archetype);
TArray<FVector> LandingRoomProbePoints;
auto AppendLandingRoomProbes = [&LandingRoomProbePoints](
const FVoxelPassageLanding& Landing)
{
const float ProbeOffset = FMath::Min(
2.0f,
FMath::Max(0.0f, Landing.HalfWidth - 2.0f));
LandingRoomProbePoints.Add(Landing.StandingPoint);
LandingRoomProbePoints.Add(
Landing.StandingPoint + FVector(ProbeOffset, 0.0f, 0.0f));
LandingRoomProbePoints.Add(
Landing.StandingPoint + FVector(-ProbeOffset, 0.0f, 0.0f));
LandingRoomProbePoints.Add(
Landing.StandingPoint + FVector(0.0f, ProbeOffset, 0.0f));
LandingRoomProbePoints.Add(
Landing.StandingPoint + FVector(0.0f, -ProbeOffset, 0.0f));
};
if (ArrivalPassage != nullptr)
{
AppendLandingRoomProbes(ArrivalPassage->LowerLanding);
}
if (DeparturePassage != nullptr)
{
AppendLandingRoomProbes(DeparturePassage->UpperLanding);
}
const FGeneratorDensitySampler Sampler(*World.Generator);
if (!VF_MeasurePlayerFitWalkWithSampler(
Sampler,
@@ -1308,7 +1353,8 @@ bool RunWalk(
OutOutput.ArrivalVoxels,
OutOutput.DepartureVoxels,
Settings,
OutOutput.Report))
OutOutput.Report,
&LandingRoomProbePoints))
{
OutOutput.Status = TEXT("refused");
OutOutput.RefusalReason = OutOutput.Report.RefusalReason;
@@ -1338,6 +1384,30 @@ bool RunWalk(
{
OutOutput.OriginCheckRefusalReason = OutOutput.OriginCheckReport.RefusalReason;
}
const int32 ArrivalProbeCount = ArrivalPassage != nullptr ? 5 : 0;
const int32 DepartureProbeCount = DeparturePassage != nullptr ? 5 : 0;
OutOutput.ArrivalLandingRoomProbeCount = ArrivalProbeCount;
OutOutput.DepartureLandingRoomProbeCount = DepartureProbeCount;
for (int32 ProbeIndex = 0; ProbeIndex < OutOutput.Report.ComponentProbePlayerFit.Num();
++ProbeIndex)
{
const bool bPlayerFit = OutOutput.Report.ComponentProbePlayerFit[ProbeIndex] != 0u;
const bool bInArrivalComponent =
OutOutput.Report.ComponentProbeInStartComponent.IsValidIndex(ProbeIndex)
&& OutOutput.Report.ComponentProbeInStartComponent[ProbeIndex] != 0u;
if (ProbeIndex < ArrivalProbeCount)
{
OutOutput.ArrivalLandingRoomPlayerFitProbeCount += bPlayerFit ? 1 : 0;
OutOutput.ArrivalLandingRoomArrivalComponentProbeCount +=
bInArrivalComponent ? 1 : 0;
}
else
{
OutOutput.DepartureLandingRoomPlayerFitProbeCount += bPlayerFit ? 1 : 0;
OutOutput.DepartureLandingRoomArrivalComponentProbeCount +=
bInArrivalComponent ? 1 : 0;
}
}
OutOutput.Status = TEXT("ok");
return true;
@@ -1509,7 +1579,7 @@ FString BuildManifestJson(
TSharedRef<FExploreJsonWriter> Writer =
TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&Json);
Writer->WriteObjectStart();
Writer->WriteValue(TEXT("schema_version"), 1);
Writer->WriteValue(TEXT("schema_version"), 2);
Writer->WriteValue(TEXT("format"), TEXT("OBJ"));
Writer->WriteValue(TEXT("mesh_file"), Export.MeshFileName);
Writer->WriteValue(TEXT("mesh_positions_units"), TEXT("metres"));
@@ -1703,7 +1773,7 @@ FString BuildExploreJson(
TSharedRef<FExploreJsonWriter> Writer =
TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&Json);
Writer->WriteObjectStart();
Writer->WriteValue(TEXT("schema_version"), 1);
Writer->WriteValue(TEXT("schema_version"), 2);
Writer->WriteValue(TEXT("tool"), TEXT("VoxelForgeExplore"));
Writer->WriteValue(TEXT("read_only_generation"), true);
@@ -1780,6 +1850,11 @@ FString BuildExploreJson(
if (Arguments.bWalk)
{
const FVoxelPlayerFitWalkReport& Report = Output.Walk.Report;
// The primary report is the mouth-sized arrival/departure window. Origin reachability is
// a separate, explicitly origin-inclusive measurement; expose that report at the legacy
// top-level keys so a sweep cannot accidentally count a window that omitted (0,0).
const FVoxelPlayerFitWalkReport& OriginReport = Output.Walk.bOriginCheckAvailable
? Output.Walk.OriginCheckReport : Report;
Writer->WriteObjectStart(TEXT("walk"));
Writer->WriteValue(TEXT("status"), Output.Walk.Status);
Writer->WriteValue(TEXT("refusal_or_error"), Output.Walk.RefusalReason);
@@ -1795,6 +1870,27 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("working_memory_cap_bytes"), MaxWalkWorkingBytes);
Writer->WriteValue(TEXT("arrival_passage_count"), Output.Walk.ArrivalPassageCount);
Writer->WriteValue(TEXT("departure_passage_count"), Output.Walk.DeparturePassageCount);
Writer->WriteValue(TEXT("landing_room_probe_offsets_voxels"), 2.0);
Writer->WriteValue(TEXT("arrival_landing_room_probe_count"),
Output.Walk.ArrivalLandingRoomProbeCount);
Writer->WriteValue(TEXT("arrival_landing_room_player_fit_probe_count"),
Output.Walk.ArrivalLandingRoomPlayerFitProbeCount);
Writer->WriteValue(TEXT("arrival_landing_room_arrival_component_probe_count"),
Output.Walk.ArrivalLandingRoomArrivalComponentProbeCount);
Writer->WriteValue(TEXT("arrival_landing_room_in_arrival_component"),
Output.Walk.ArrivalLandingRoomProbeCount > 0
&& Output.Walk.ArrivalLandingRoomArrivalComponentProbeCount
== Output.Walk.ArrivalLandingRoomProbeCount);
Writer->WriteValue(TEXT("departure_landing_room_probe_count"),
Output.Walk.DepartureLandingRoomProbeCount);
Writer->WriteValue(TEXT("departure_landing_room_player_fit_probe_count"),
Output.Walk.DepartureLandingRoomPlayerFitProbeCount);
Writer->WriteValue(TEXT("departure_landing_room_arrival_component_probe_count"),
Output.Walk.DepartureLandingRoomArrivalComponentProbeCount);
Writer->WriteValue(TEXT("departure_landing_room_in_arrival_component"),
Output.Walk.DepartureLandingRoomProbeCount > 0
&& Output.Walk.DepartureLandingRoomArrivalComponentProbeCount
== Output.Walk.DepartureLandingRoomProbeCount);
if (Output.Walk.bHasArrival)
{
WriteJsonVector(*Writer, TEXT("arrival_mouth_voxels"), Output.Walk.ArrivalVoxels, 1.0f);
@@ -1870,14 +1966,16 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("agent_to_target_component_gap_m"),
static_cast<double>(Report.AgentToTargetComponentGapVoxels)
* FVoxelPlayerCapsuleConstants::VoxelSizeMeters);
Writer->WriteValue(TEXT("origin_column_in_sampled_window"), Report.bOriginColumnInSampledWindow);
Writer->WriteValue(TEXT("origin_column_has_player_fit"), Report.bOriginColumnHasPlayerFit);
Writer->WriteValue(TEXT("origin_column_player_fit_cells"), Report.OriginColumnPlayerFitCells);
Writer->WriteValue(TEXT("origin_column_reachable_from_arrival"), Report.bOriginColumnReachable);
Writer->WriteValue(TEXT("origin_column_measurement_window"),
TEXT("origin-inclusive mouth-AABB + margin"));
Writer->WriteValue(TEXT("origin_column_in_sampled_window"), OriginReport.bOriginColumnInSampledWindow);
Writer->WriteValue(TEXT("origin_column_has_player_fit"), OriginReport.bOriginColumnHasPlayerFit);
Writer->WriteValue(TEXT("origin_column_player_fit_cells"), OriginReport.OriginColumnPlayerFitCells);
Writer->WriteValue(TEXT("origin_column_reachable_from_arrival"), OriginReport.bOriginColumnReachable);
Writer->WriteValue(TEXT("reachable_set_to_origin_column_voxels"),
static_cast<double>(Report.ReachableSetToOriginColumnVoxels));
static_cast<double>(OriginReport.ReachableSetToOriginColumnVoxels));
Writer->WriteValue(TEXT("reachable_set_to_origin_column_m"),
static_cast<double>(Report.ReachableSetToOriginColumnVoxels)
static_cast<double>(OriginReport.ReachableSetToOriginColumnVoxels)
* FVoxelPlayerCapsuleConstants::VoxelSizeMeters);
Writer->WriteObjectStart(TEXT("origin_check"));
Writer->WriteValue(TEXT("attempted"), Output.Walk.bOriginCheckAttempted);
@@ -1885,40 +1983,40 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("refusal_or_error"), Output.Walk.OriginCheckRefusalReason);
if (Output.Walk.bOriginCheckAvailable)
{
const FVoxelPlayerFitWalkReport& OriginReport = Output.Walk.OriginCheckReport;
const FVoxelPlayerFitWalkReport& OriginCheckReport = Output.Walk.OriginCheckReport;
Writer->WriteValue(TEXT("window_policy"), TEXT("origin-inclusive mouth-AABB + margin"));
Writer->WriteValue(TEXT("connectivity_result"), ConnectivityResultName(OriginReport.Result));
Writer->WriteValue(TEXT("can_reach_departure"), OriginReport.bCanReachDeparture);
Writer->WriteValue(TEXT("sampled_num_x"), OriginReport.SampledNumX);
Writer->WriteValue(TEXT("sampled_num_y"), OriginReport.SampledNumY);
Writer->WriteValue(TEXT("sampled_num_z"), OriginReport.SampledNumZ);
Writer->WriteValue(TEXT("sampled_min_x"), static_cast<double>(OriginReport.SampledMinX));
Writer->WriteValue(TEXT("sampled_max_x_exclusive"), static_cast<double>(OriginReport.SampledMaxX));
Writer->WriteValue(TEXT("sampled_min_y"), static_cast<double>(OriginReport.SampledMinY));
Writer->WriteValue(TEXT("sampled_max_y_exclusive"), static_cast<double>(OriginReport.SampledMaxY));
Writer->WriteValue(TEXT("sampled_min_z"), OriginReport.SampledMinZ);
Writer->WriteValue(TEXT("sampled_max_z_exclusive"), OriginReport.SampledMaxZ);
Writer->WriteValue(TEXT("player_fit_cells"), OriginReport.PlayerFitVolumeCells);
Writer->WriteValue(TEXT("reachable_player_fit_cells"), OriginReport.ReachablePlayerFitCells);
Writer->WriteValue(TEXT("connectivity_result"), ConnectivityResultName(OriginCheckReport.Result));
Writer->WriteValue(TEXT("can_reach_departure"), OriginCheckReport.bCanReachDeparture);
Writer->WriteValue(TEXT("sampled_num_x"), OriginCheckReport.SampledNumX);
Writer->WriteValue(TEXT("sampled_num_y"), OriginCheckReport.SampledNumY);
Writer->WriteValue(TEXT("sampled_num_z"), OriginCheckReport.SampledNumZ);
Writer->WriteValue(TEXT("sampled_min_x"), static_cast<double>(OriginCheckReport.SampledMinX));
Writer->WriteValue(TEXT("sampled_max_x_exclusive"), static_cast<double>(OriginCheckReport.SampledMaxX));
Writer->WriteValue(TEXT("sampled_min_y"), static_cast<double>(OriginCheckReport.SampledMinY));
Writer->WriteValue(TEXT("sampled_max_y_exclusive"), static_cast<double>(OriginCheckReport.SampledMaxY));
Writer->WriteValue(TEXT("sampled_min_z"), OriginCheckReport.SampledMinZ);
Writer->WriteValue(TEXT("sampled_max_z_exclusive"), OriginCheckReport.SampledMaxZ);
Writer->WriteValue(TEXT("player_fit_cells"), OriginCheckReport.PlayerFitVolumeCells);
Writer->WriteValue(TEXT("reachable_player_fit_cells"), OriginCheckReport.ReachablePlayerFitCells);
Writer->WriteValue(TEXT("reachable_player_fit_fraction"),
static_cast<double>(OriginReport.ReachablePlayerFitFraction));
Writer->WriteValue(TEXT("player_fit_components"), OriginReport.PlayerFitComponents);
Writer->WriteValue(TEXT("arrival_component_cells"), OriginReport.ArrivalComponentCells);
Writer->WriteValue(TEXT("departure_component_cells"), OriginReport.DepartureComponentCells);
static_cast<double>(OriginCheckReport.ReachablePlayerFitFraction));
Writer->WriteValue(TEXT("player_fit_components"), OriginCheckReport.PlayerFitComponents);
Writer->WriteValue(TEXT("arrival_component_cells"), OriginCheckReport.ArrivalComponentCells);
Writer->WriteValue(TEXT("departure_component_cells"), OriginCheckReport.DepartureComponentCells);
Writer->WriteValue(TEXT("mouth_component_gap_voxels"),
static_cast<double>(OriginReport.MouthComponentGapVoxels));
static_cast<double>(OriginCheckReport.MouthComponentGapVoxels));
Writer->WriteValue(TEXT("origin_column_in_sampled_window"),
OriginReport.bOriginColumnInSampledWindow);
OriginCheckReport.bOriginColumnInSampledWindow);
Writer->WriteValue(TEXT("origin_column_has_player_fit"),
OriginReport.bOriginColumnHasPlayerFit);
OriginCheckReport.bOriginColumnHasPlayerFit);
Writer->WriteValue(TEXT("origin_column_player_fit_cells"),
OriginReport.OriginColumnPlayerFitCells);
OriginCheckReport.OriginColumnPlayerFitCells);
Writer->WriteValue(TEXT("origin_column_reachable_from_arrival"),
OriginReport.bOriginColumnReachable);
OriginCheckReport.bOriginColumnReachable);
Writer->WriteValue(TEXT("reachable_set_to_origin_column_voxels"),
static_cast<double>(OriginReport.ReachableSetToOriginColumnVoxels));
static_cast<double>(OriginCheckReport.ReachableSetToOriginColumnVoxels));
Writer->WriteValue(TEXT("reachable_set_to_origin_column_m"),
static_cast<double>(OriginReport.ReachableSetToOriginColumnVoxels)
static_cast<double>(OriginCheckReport.ReachableSetToOriginColumnVoxels)
* FVoxelPlayerCapsuleConstants::VoxelSizeMeters);
}
Writer->WriteObjectEnd();