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:
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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user