feat(passages): §6.5 landings -- you arrive somewhere you can stand

The last unbuilt Tier 1 item. A passage was a bore through rock with no
floor inside the walkable angle, so its mouth was not a standing position
and neither was anything near it -- the departure mouth measured a FOUR-CELL
player-fit component. You arrived and there was nowhere to stand.

Both ends of all 7 passages now open into a landing, sized from the body:
  capsule diameter 2.72 vox      floor >= 12 vox (3 m) to stand and turn
  capsule height 7.04 vox        room >= 12 vox tall, stock mouth gives 18 (4.5 m)
  3-voxel floor slab, tube tangent to it -- no lip to climb out of
  connector 5-vox radius: 2.5 m clear width, 3 m clear height
  ramps over 44 deg get a deterministic level dog-leg

⭐ THE MOUTHS ARE NO LONGER SPECKS:
  archetype        components      largest      arrival-departure
  Maze             309 -> 340      176 -> 3210    43-81  -> 3210-3210
  TunnelNetwork    467 -> 227     1292 -> 3841     2-1   -> 3841-269
  Underwater        86 -> 117       35 -> 3838     3-11  -> 3838-267
  FlatPlain         45 ->  11      259 -> 2130     1-1   ->  268-273
  CrystalChamber   197 ->  90       24 ->  286     1-1   ->  286-274
  FloatingIslands    2 ->   1        3 ->   72     1-1   ->   72-72

Law 1/8 -> 2/7 connected (1 refused on MaxCells), and critically ZERO
connections now run through a four-cell pocket.

Every landing floor was verified against the FINAL density, after all posts:
14/14 pass the 44 deg slope bracket, worst room gradient .059, worst
connector .522. 14/14 deterministic root joins. 0 seal failures.
0 box-verdict violations across 82 proved + 174 mixed, 109,142 voxels.
Op-stack bit-identical, order-independence green over 10 slots / 31
passages / 3 pool orders. Cache 0.665 us rebuild, 0.646 us hot.

Post order: origin spine -> vertical seal -> passage tube + landing floor
-> XY edge seal. Landings clamp 4 voxels inside the vertical seal; the XY
seal stays last and wins at world edges.

⚠️ ARCHITECTURAL NOTE, not a detail: landings join a deterministic annular
ROOT HUB at (0,0), built identically for every endpoint -- no nearest-
component search, no passage ordering. That is what makes the join
guaranteed rather than likely, and it extends the (0,0) spine concept from
a landmark into a structural network. It also means routes tend to pass
through the centre, which is a gameplay consequence worth Jahni's eye.

⚠️ AND IT DOES NOT CLOSE THE LAW. Codex is explicit: the landings guarantee
membership in the new root network, NOT in the archetype's pre-existing fit
component. The whole-strate Maze check still shows a 71.9-voxel gap between
1,235 and 2,938-cell components. So the native corridor networks remain
disconnected from the root network in places. Real, separate, next.

52 build-and-measure cycles, ~9 hours, finding real geometry bugs on the
way (a landing floor overlapping the ramp cells and hiding a floor
discontinuity; the disturbance pass refilling connector headroom).

Build clean, 32/32 success, 0 failed. Showcase regenerated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K8V9my1PL68pmQb2gutZZV
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-09-06 23:55:47 +02:00
1 parent e4e159f0cd
commit 413c15de24
14 files changed
+1943 -187

No files matched your search

@@ -421,13 +421,18 @@ bool FVoxelForgeComposerPromotionTest::RunTest(const FString& Parameters)
if (Passage.LowerStrateIndex == CandidateStrateIndex
&& Passage.UpperStrateIndex + 1 == CandidateStrateIndex)
{
ArrivalPoint = Passage.LowerPoint;
// The promotion simulation intentionally remains the legacy coarse air-law probe
// (SampleStep=8), not the Tier-1 fine player-fit law. A floor standing anchor can lie
// in the solid half of a coarse cell after the landing adds its support slab; the
// deterministic door anchor is the actual air position where the tube meets that
// landing and keeps the strict no-snap assertion meaningful.
ArrivalPoint = Passage.LowerLanding.DoorPoint;
++ArrivalCount;
}
if (Passage.UpperStrateIndex == CandidateStrateIndex
&& Passage.LowerStrateIndex == CandidateStrateIndex + 1)
{
DeparturePoint = Passage.UpperPoint;
DeparturePoint = Passage.UpperLanding.DoorPoint;
++DepartureCount;
}
}
@@ -136,6 +136,28 @@ namespace
&& VoxelForgeTest::BitEqual(A.Z, B.Z);
}
bool SameLanding(const FVoxelPassageLanding& A, const FVoxelPassageLanding& B)
{
return SameVectorBits(A.StandingPoint, B.StandingPoint)
&& SameVectorBits(A.DoorPoint, B.DoorPoint)
&& SameVectorBits(A.DoorDirection, B.DoorDirection)
&& SameVectorBits(A.ConnectorStart, B.ConnectorStart)
&& SameVectorBits(A.ConnectorControl, B.ConnectorControl)
&& SameVectorBits(A.ConnectorEnd, B.ConnectorEnd)
&& VoxelForgeTest::BitEqual(A.FloorZ, B.FloorZ)
&& VoxelForgeTest::BitEqual(A.CeilingZ, B.CeilingZ)
&& VoxelForgeTest::BitEqual(A.HalfWidth, B.HalfWidth)
&& VoxelForgeTest::BitEqual(A.FloorThickness, B.FloorThickness)
&& VoxelForgeTest::BitEqual(A.ConnectorRadius, B.ConnectorRadius)
&& VoxelForgeTest::BitEqual(A.ConnectorCeilingZ, B.ConnectorCeilingZ)
&& VoxelForgeTest::BitEqual(A.RootFloorZ, B.RootFloorZ)
&& VoxelForgeTest::BitEqual(A.RootCeilingZ, B.RootCeilingZ)
&& VoxelForgeTest::BitEqual(A.RootSpineRadius, B.RootSpineRadius)
&& A.bSourcePlayerFit == B.bSourcePlayerFit
&& A.bHasNetworkConnector == B.bHasNetworkConnector
&& A.bHasConnectorBend == B.bHasConnectorBend;
}
bool SamePassage(const FVoxelPassage& A, const FVoxelPassage& B, FString& OutMismatch,
int32 PassageIndex)
{
@@ -155,6 +177,14 @@ namespace
return false;
}
if (!SameLanding(A.UpperLanding, B.UpperLanding)
|| !SameLanding(A.LowerLanding, B.LowerLanding))
{
OutMismatch = FString::Printf(
TEXT("passage %d landing descriptors differ"), PassageIndex);
return false;
}
if (!VoxelForgeTest::BitEqual(A.Radius, B.Radius))
{
OutMismatch = FString::Printf(
@@ -4,6 +4,7 @@
#if WITH_DEV_AUTOMATION_TESTS
#include "HAL/PlatformTime.h"
#include "Misc/AutomationTest.h"
#include "VoxelCaveMorphology.h"
@@ -65,6 +66,29 @@ namespace
return 0.0f;
}
}
float BoundarySealFor(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;
}
}
}
bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
@@ -104,6 +128,27 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
bool bAllAnswerableFootingsAreValid = true;
bool bAllAnswerableEndpointsMatchQuery = true;
int32 NumLandingEnds = 0;
int32 NumLandingFloorsPassed = 0;
int32 NumLandingFloorFailures = 0;
int32 NumLandingNetworkJoins = 0;
int32 NumLandingSealFailures = 0;
int32 NumLandingFloorBoxes = 0;
int32 NumLandingBoxProofs = 0;
int32 NumLandingBoxAllSolid = 0;
int32 NumLandingBoxAllAir = 0;
int32 NumLandingBoxViolations = 0;
int32 NumLandingSourceFit = 0;
int32 NumLandingConnectorSlopesPassed = 0;
int32 NumLandingConnectorSlopeFailures = 0;
float WorstLandingFloorGradient = 0.0f;
float WorstLandingConnectorGradient = 0.0f;
FString FirstLandingFailure;
FString LandingSlopeReport;
constexpr float LandingWalkableAngleDegrees = 44.0f;
const float LandingWalkableSlope = FMath::Tan(
FMath::DegreesToRadians(LandingWalkableAngleDegrees));
for (const FVoxelPassage& Passage : Passages)
{
// The surface entry is a same-strate shaft and is not an inter-strate destination query.
@@ -279,8 +324,9 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
++NumChecked;
AnsweredArchetypes.Add(static_cast<uint8>(Definition->GeneratorType));
// Control-point endpoints are pinned to the complete query result, including any lateral
// snap. A mismatch here means GeneratePassages used stale XY or stale Z downstream.
// Standing endpoints remain pinned to the complete query result, including any lateral
// snap. The tube endpoint is now the landing door, so this intentionally compares the
// public standing anchor rather than the first/last control point.
if (!FMath::IsNearlyEqual(Passage.LowerPoint.X, SuggestedPoint.X, 0.01f)
|| !FMath::IsNearlyEqual(Passage.LowerPoint.Y, SuggestedPoint.Y, 0.01f)
|| !FMath::IsNearlyEqual(Passage.LowerPoint.Z, SuggestedPoint.Z, 0.01f))
@@ -451,6 +497,337 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
}
}
//==========================================================================
// LANDING GEOMETRY — the mouth is now measured as a room, not as a tube.
//==========================================================================
// Use the final production density, including the legacy structural post and any
// disturbance backstop. Five points on every floor give a small numerical walkability
// stencil; the floor writer itself is a plane, but the test must inspect the resulting field.
for (const FVoxelPassage& Passage : Passages)
{
if (Passage.UpperStrateIndex == Passage.LowerStrateIndex) continue;
const FVoxelPassageLanding* Landings[] = {
&Passage.UpperLanding, &Passage.LowerLanding };
const int32 StrateIndices[] = {
Passage.UpperStrateIndex, Passage.LowerStrateIndex };
for (int32 EndIndex = 0; EndIndex < 2; ++EndIndex)
{
++NumLandingEnds;
const FVoxelPassageLanding& Landing = *Landings[EndIndex];
const int32 StrateIndex = StrateIndices[EndIndex];
const UVoxelStrateDefinition* Definition = Layout.IsValidIndex(StrateIndex)
? Layout[StrateIndex].Definition : nullptr;
auto RecordFailure = [&](const FString& Message)
{
if (FirstLandingFailure.IsEmpty()) FirstLandingFailure = Message;
};
if (Definition == nullptr
|| Landing.HalfWidth <= 0.0f
|| Landing.CeilingZ <= Landing.FloorZ
|| !FMath::IsFinite(Landing.FloorZ)
|| !FMath::IsFinite(Landing.CeilingZ)
|| !FMath::IsFinite(Landing.HalfWidth))
{
++NumLandingFloorFailures;
RecordFailure(FString::Printf(
TEXT("invalid landing descriptor at passage strate end %d/%d"),
Passage.UpperStrateIndex, Passage.LowerStrateIndex));
continue;
}
const float StrateTopZ = (float)(Layout[StrateIndex].TopChunkZ + 1) * CHUNK_SIZE;
const float StrateBottomZ = (float)Layout[StrateIndex].BottomChunkZ * CHUNK_SIZE;
const float Seal = BoundarySealFor(*Definition);
if (!(Landing.FloorZ > StrateBottomZ + Seal)
|| !(Landing.CeilingZ < StrateTopZ - Seal))
{
++NumLandingSealFailures;
RecordFailure(FString::Printf(
TEXT("landing at strate %d reaches its seal (floor %.3f, ceiling %.3f, interior %.3f..%.3f)"),
StrateIndex, Landing.FloorZ, Landing.CeilingZ,
StrateBottomZ + Seal, StrateTopZ - Seal));
}
if ((Landing.StandingPoint - (EndIndex == 0
? Passage.UpperPoint : Passage.LowerPoint)).SizeSquared() > 0.0001f)
{
++NumLandingFloorFailures;
RecordFailure(FString::Printf(
TEXT("landing standing anchor does not match passage %s point at strate %d"),
EndIndex == 0 ? TEXT("upper") : TEXT("lower"), StrateIndex));
}
const bool bWidthAndHeadroom = Landing.HalfWidth * 2.0f >= 12.0f - 0.01f
&& Landing.CeilingZ - Landing.FloorZ >= 12.0f - 0.01f;
const bool bDoorTangent = FMath::IsNearlyEqual(
Landing.DoorPoint.Z - Landing.FloorZ,
Passage.Radius >= 0.0f ? FMath::Max(
Passage.ControlRadii.Num() > 0
? Passage.ControlRadii[0] : Passage.Radius,
1.0f) : 1.0f,
0.01f);
if (!bWidthAndHeadroom || !bDoorTangent)
{
++NumLandingFloorFailures;
RecordFailure(FString::Printf(
TEXT("landing at strate %d failed body/door arithmetic (width %.3f, height %.3f, door rise %.3f)"),
StrateIndex, Landing.HalfWidth * 2.0f,
Landing.CeilingZ - Landing.FloorZ,
Landing.DoorPoint.Z - Landing.FloorZ));
}
const FVector FloorSamples[] = {
Landing.StandingPoint,
Landing.StandingPoint + FVector(2.0f, 0.0f, 0.0f),
Landing.StandingPoint + FVector(-2.0f, 0.0f, 0.0f),
Landing.StandingPoint + FVector(0.0f, 2.0f, 0.0f),
Landing.StandingPoint + FVector(0.0f, -2.0f, 0.0f) };
float SurfaceHeights[UE_ARRAY_COUNT(FloorSamples)] = {};
bool bFloorPassed = true;
float LandingGradient = -1.0f;
for (int32 SampleIndex = 0; SampleIndex < UE_ARRAY_COUNT(FloorSamples); ++SampleIndex)
{
const FVector& Sample = FloorSamples[SampleIndex];
const float Below = World.Generator->GetDensityAt(
Sample.X, Sample.Y, Landing.FloorZ - 1.0f);
const float Above = World.Generator->GetDensityAt(
Sample.X, Sample.Y, Landing.FloorZ + 1.0f);
const float Denominator = Above - Below;
if (!FMath::IsFinite(Below) || !FMath::IsFinite(Above)
|| !(Below < 0.0f) || !(Above >= 0.0f)
|| !(Denominator > 0.0f))
{
bFloorPassed = false;
continue;
}
SurfaceHeights[SampleIndex] = Landing.FloorZ - 1.0f
+ FMath::Clamp(-Below / Denominator, 0.0f, 1.0f) * 2.0f;
}
if (bFloorPassed)
{
float MaxGradient = 0.0f;
for (int32 SampleIndex = 1; SampleIndex < UE_ARRAY_COUNT(FloorSamples); ++SampleIndex)
{
MaxGradient = FMath::Max(MaxGradient,
FMath::Abs(SurfaceHeights[SampleIndex] - SurfaceHeights[0]) / 2.0f);
}
LandingGradient = MaxGradient;
WorstLandingFloorGradient = FMath::Max(WorstLandingFloorGradient, MaxGradient);
const float NormalZ = 1.0f / FMath::Sqrt(1.0f + MaxGradient * MaxGradient);
bFloorPassed = NormalZ + KINDA_SMALL_NUMBER >= FMath::Cos(
FMath::DegreesToRadians(LandingWalkableAngleDegrees));
}
float ConnectorGradient = -1.0f;
bool bConnectorSlopePassed = false;
if (Landing.bHasNetworkConnector
&& FMath::IsFinite(Landing.ConnectorStart.X)
&& FMath::IsFinite(Landing.ConnectorStart.Y)
&& FMath::IsFinite(Landing.ConnectorEnd.X)
&& FMath::IsFinite(Landing.ConnectorEnd.Y)
&& (!Landing.bHasConnectorBend
|| (FMath::IsFinite(Landing.ConnectorControl.X)
&& FMath::IsFinite(Landing.ConnectorControl.Y)))
&& FMath::IsFinite(Landing.RootFloorZ)
&& FMath::IsFinite(Landing.FloorZ))
{
const FVector2D ConnectorStart(
Landing.ConnectorStart.X, Landing.ConnectorStart.Y);
const FVector2D ConnectorEnd(
Landing.ConnectorEnd.X, Landing.ConnectorEnd.Y);
const float DirectLength = (ConnectorEnd - ConnectorStart).Size();
if (Landing.bHasConnectorBend)
{
const FVector2D ConnectorControl(
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),
KINDA_SMALL_NUMBER);
ConnectorGradient = FMath::Abs(
Landing.RootFloorZ - Landing.FloorZ) / RampLength;
}
else
{
const float RampLength = FMath::Max(
DirectLength - FMath::Max(Landing.HalfWidth - 1.0f, 0.0f)
- (Landing.RootSpineRadius > 0.0f ? 2.25f : 0.0f),
KINDA_SMALL_NUMBER);
ConnectorGradient = FMath::Abs(
Landing.RootFloorZ - Landing.FloorZ) / RampLength;
}
bConnectorSlopePassed = FMath::IsFinite(ConnectorGradient)
&& ConnectorGradient <= LandingWalkableSlope + KINDA_SMALL_NUMBER;
}
if (bConnectorSlopePassed)
{
++NumLandingConnectorSlopesPassed;
WorstLandingConnectorGradient = FMath::Max(
WorstLandingConnectorGradient, ConnectorGradient);
}
else
{
++NumLandingConnectorSlopeFailures;
RecordFailure(FString::Printf(
TEXT("landing at strate %d has connector slope %.6f above the %.6f walkable limit"),
StrateIndex, ConnectorGradient, LandingWalkableSlope));
}
const bool bLandingSlopePassed = bFloorPassed && bConnectorSlopePassed;
if (bLandingSlopePassed)
{
++NumLandingFloorsPassed;
}
else
{
++NumLandingFloorFailures;
RecordFailure(FString::Printf(
TEXT("landing at strate %d failed its final-density flat-floor slope bracket"),
StrateIndex));
}
if (Landing.bSourcePlayerFit)
{
++NumLandingSourceFit;
}
if (Landing.bHasNetworkConnector)
{
++NumLandingNetworkJoins;
}
else
{
RecordFailure(FString::Printf(
TEXT("landing at strate %d has neither a source fit nor a deterministic network connector"),
StrateIndex));
}
// Exercise the same box shortcut that can otherwise erase a support slab. A uniform
// verdict is allowed only if every exact ClassifyTile lattice sample agrees with the
// final density; a floor-bearing landing should normally force Mixed via the
// bidirectional passage effect. This is one aggregate audit, not per-sample logging.
const FIntVector LandingBoxOrigin(
FMath::FloorToInt(Landing.StandingPoint.X) - 4,
FMath::FloorToInt(Landing.StandingPoint.Y) - 4,
FMath::FloorToInt(Landing.FloorZ) - 3);
constexpr int32 LandingBoxCells = 8;
constexpr int32 LandingBoxStep = 1;
const EVoxelTileClass LandingBoxVerdict = World.Generator->ClassifyTile(
LandingBoxOrigin, LandingBoxStep, LandingBoxCells);
++NumLandingFloorBoxes;
if (LandingBoxVerdict != EVoxelTileClass::Mixed)
{
++NumLandingBoxProofs;
if (LandingBoxVerdict == EVoxelTileClass::AllSolid) ++NumLandingBoxAllSolid;
if (LandingBoxVerdict == EVoxelTileClass::AllAir) ++NumLandingBoxAllAir;
bool bBoxAgrees = true;
const bool bClaimsSolid = LandingBoxVerdict == EVoxelTileClass::AllSolid;
for (int32 GZ = -1; GZ <= LandingBoxCells + 1 && bBoxAgrees; ++GZ)
{
for (int32 GY = -1; GY <= LandingBoxCells + 1 && bBoxAgrees; ++GY)
{
for (int32 GX = -1; GX <= LandingBoxCells + 1; ++GX)
{
const float Density = World.Generator->GetDensityAt(
(float)(LandingBoxOrigin.X + GX * LandingBoxStep),
(float)(LandingBoxOrigin.Y + GY * LandingBoxStep),
(float)(LandingBoxOrigin.Z + GZ * LandingBoxStep));
const bool bMatches = bClaimsSolid
? Density < 0.0f : Density >= 0.0f;
if (!FMath::IsFinite(Density) || !bMatches)
{
bBoxAgrees = false;
break;
}
}
}
}
if (!bBoxAgrees)
{
++NumLandingBoxViolations;
if (NumLandingBoxViolations == 1)
{
AddError(FString::Printf(
TEXT("Landing box verdict %d was not sound at (%d,%d,%d): the uniform %s verdict disagreed with final density."),
NumLandingBoxProofs,
LandingBoxOrigin.X,
LandingBoxOrigin.Y,
LandingBoxOrigin.Z,
bClaimsSolid ? TEXT("AllSolid") : TEXT("AllAir")));
}
}
}
const FString LandingArchetype = ArchetypeName(Definition->GeneratorType);
LandingSlopeReport += FString::Printf(
TEXT("%s[%d:%s]=floor:%s(%.6f),connector:%s(%.6f)%s "),
*LandingArchetype,
StrateIndex,
EndIndex == 0 ? TEXT("upper") : TEXT("lower"),
bFloorPassed ? TEXT("PASS") : TEXT("FAIL"),
LandingGradient,
bConnectorSlopePassed ? TEXT("PASS") : TEXT("FAIL"),
ConnectorGradient,
Landing.bHasConnectorBend ? TEXT("[bend]") : TEXT(""));
}
}
// 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
// contract without adding a log line for any individual sample.
const FVoxelPassage* TimingPassage = nullptr;
for (const FVoxelPassage& Passage : Passages)
{
if (Passage.UpperStrateIndex != Passage.LowerStrateIndex)
{
TimingPassage = &Passage;
break;
}
}
if (TimingPassage != nullptr)
{
const FVector TimingPoint = TimingPassage->UpperLanding.StandingPoint
+ FVector(0.0f, 0.0f, 2.0f);
constexpr int32 HotCalls = 8192;
constexpr int32 RebuildCalls = 96;
volatile float TimingSink = 0.0f;
double StartSeconds = FPlatformTime::Seconds();
for (int32 Call = 0; Call < HotCalls; ++Call)
{
float Density = 8.0f;
World.StrateManager->ApplyPassageModifier(
Density, TimingPoint.X, TimingPoint.Y, TimingPoint.Z, 8.0f, 0.0f);
TimingSink += Density;
}
const double HotSeconds = FPlatformTime::Seconds() - StartSeconds;
StartSeconds = FPlatformTime::Seconds();
for (int32 Call = 0; Call < RebuildCalls; ++Call)
{
const FVector Point = TimingPoint
+ ((Call & 1) != 0 ? FVector((float)CHUNK_SIZE, 0.0f, 0.0f)
: FVector::ZeroVector);
float Density = 8.0f;
World.StrateManager->ApplyPassageModifier(
Density, Point.X, Point.Y, Point.Z, 8.0f, 0.0f);
TimingSink += Density;
}
const double RebuildSeconds = FPlatformTime::Seconds() - StartSeconds;
AddInfo(FString::Printf(
TEXT("Passage TLS cache timing: rebuild %.3f us/call over %d alternating chunks; hot %.3f us/call over %d same-chunk calls (sink %.3f)."),
RebuildSeconds * 1000000.0 / (double)RebuildCalls,
RebuildCalls,
HotSeconds * 1000000.0 / (double)HotCalls,
HotCalls,
(float)TimingSink));
}
AddInfo(FString::Printf(
TEXT("Passage player-fit check: %d inter-strate passages, upper %d checked (%d false), lower %d checked (%d footing checks), %d/%d room/slab ring samples air, lower %d query-false (%d unique archetypes; %d unsupported, %d supported-but-no-point)."),
NumInterStratePassages,
@@ -464,6 +841,36 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
FalseArchetypes.Num(),
NumUnsupported,
NumSupportedWithoutPoint));
AddInfo(FString::Printf(
TEXT("Landing geometry: %d/%d ends passed the final-density floor test (worst room gradient %.6f), connector slopes %d/%d (worst %.6f; walkable limit %.6f), source-fit anchors %d/%d, explicit root-network joins %d/%d, seal violations %d, geometry/floor failures %d."),
NumLandingFloorsPassed,
NumLandingEnds,
WorstLandingFloorGradient,
NumLandingConnectorSlopesPassed,
NumLandingEnds,
WorstLandingConnectorGradient,
LandingWalkableSlope,
NumLandingSourceFit,
NumLandingEnds,
NumLandingNetworkJoins,
NumLandingEnds,
NumLandingSealFailures,
NumLandingFloorFailures));
if (!LandingSlopeReport.IsEmpty())
{
AddInfo(FString::Printf(TEXT("Landing slope tests per end: %s"), *LandingSlopeReport));
}
AddInfo(FString::Printf(
TEXT("Landing floor box verdicts: %d boxes probed, %d uniform proofs (AllSolid %d, AllAir %d), %d violations."),
NumLandingFloorBoxes,
NumLandingBoxProofs,
NumLandingBoxAllSolid,
NumLandingBoxAllAir,
NumLandingBoxViolations));
if (!FirstLandingFailure.IsEmpty())
{
AddError(FString::Printf(TEXT("First landing geometry failure: %s"), *FirstLandingFailure));
}
// 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.
@@ -541,6 +948,14 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
}
return NumInterStratePassages > 0
&& NumLandingEnds == NumInterStratePassages * 2
&& NumLandingFloorsPassed == NumLandingEnds
&& NumLandingConnectorSlopesPassed == NumLandingEnds
&& NumLandingConnectorSlopeFailures == 0
&& NumLandingNetworkJoins == NumLandingEnds
&& NumLandingSealFailures == 0
&& NumLandingFloorFailures == 0
&& NumLandingBoxViolations == 0
&& NumChecked > 0
&& NumUpperChecked > 0
&& NumFootingChecked > 0
@@ -2056,6 +2056,520 @@ namespace
}
}
namespace
{
constexpr float VF_LandingCarveSafetyMargin = 4.0f;
struct FVoxelPassageLandingConnectorProjection
{
FVector2D Closest = FVector2D::ZeroVector;
float DistanceAlong = 0.0f;
float TotalLength = 0.0f;
float SegmentLength = 0.0f;
float SegmentParameter = 0.0f;
int32 SegmentIndex = 0;
};
FVoxelPassageLandingConnectorProjection VF_ProjectPassageLandingConnector(
const FVector& Position,
const FVoxelPassageLanding& Landing)
{
const FVector2D A(Landing.ConnectorStart.X, Landing.ConnectorStart.Y);
const FVector2D Control(Landing.ConnectorControl.X, Landing.ConnectorControl.Y);
const FVector2D B(Landing.ConnectorEnd.X, Landing.ConnectorEnd.Y);
const FVector2D P(Position.X, Position.Y);
const bool bBend = Landing.bHasConnectorBend;
const FVector2D FirstEnd = bBend ? Control : B;
const FVector2D FirstVector = FirstEnd - A;
const FVector2D SecondVector = B - Control;
const float FirstLength = FirstVector.Size();
const float SecondLength = bBend ? SecondVector.Size() : 0.0f;
FVoxelPassageLandingConnectorProjection Result;
Result.TotalLength = FirstLength + SecondLength;
Result.Closest = A;
Result.SegmentLength = FirstLength;
if (Result.TotalLength <= KINDA_SMALL_NUMBER)
{
return Result;
}
float BestDistanceSquared = FLT_MAX;
const auto ProjectSegment = [&](const FVector2D& SegmentStart,
const FVector2D& SegmentEnd,
float SegmentOffset,
int32 SegmentIndex)
{
const FVector2D Segment = SegmentEnd - SegmentStart;
const float LengthSquared = Segment.SizeSquared();
const float Parameter = LengthSquared > KINDA_SMALL_NUMBER
? FMath::Clamp(FVector2D::DotProduct(P - SegmentStart, Segment)
/ LengthSquared, 0.0f, 1.0f)
: 0.0f;
const FVector2D Candidate = SegmentStart + Segment * Parameter;
const float DistanceSquared = (P - Candidate).SizeSquared();
if (DistanceSquared < BestDistanceSquared
|| (DistanceSquared == BestDistanceSquared
&& SegmentIndex < Result.SegmentIndex))
{
BestDistanceSquared = DistanceSquared;
Result.Closest = Candidate;
Result.DistanceAlong = SegmentOffset
+ FMath::Sqrt(FMath::Max(LengthSquared, 0.0f)) * Parameter;
Result.SegmentLength = FMath::Sqrt(FMath::Max(LengthSquared, 0.0f));
Result.SegmentParameter = Parameter;
Result.SegmentIndex = SegmentIndex;
}
};
ProjectSegment(A, FirstEnd, 0.0f, 0);
if (bBend)
{
ProjectSegment(Control, B, FirstLength, 1);
}
return Result;
}
float VF_PassageLandingConnectorFloorParameter(
const FVoxelPassageLandingConnectorProjection& Projection,
const FVoxelPassageLanding& Landing)
{
if (Projection.TotalLength <= KINDA_SMALL_NUMBER)
{
return 0.0f;
}
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.
if (Projection.SegmentIndex == 0)
{
return 0.0f;
}
const float RootFlatLength = Landing.RootSpineRadius > 0.0f ? 2.25f : 0.0f;
const float RampLength = FMath::Max(
Projection.SegmentLength - RootFlatLength, KINDA_SMALL_NUMBER);
return FMath::Clamp(
Projection.SegmentParameter * Projection.SegmentLength / RampLength,
0.0f, 1.0f);
}
// The direct zero-rise case remains a single segment. Keep the old end aprons for
// 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 SourceDistance = FMath::Clamp(
SourceFlatLength, 0.0f, Projection.TotalLength * 0.49f);
const float RootDistance = FMath::Clamp(
Projection.TotalLength - RootFlatLength,
Projection.TotalLength * 0.51f, Projection.TotalLength);
if (RootDistance <= SourceDistance + KINDA_SMALL_NUMBER)
{
return FMath::Clamp(
Projection.DistanceAlong / Projection.TotalLength, 0.0f, 1.0f);
}
return FMath::Clamp(
(Projection.DistanceAlong - SourceDistance)
/ (RootDistance - SourceDistance), 0.0f, 1.0f);
}
}
FVoxelPassageLanding VF_BuildPassageLanding(
const FVector& InStandingPoint,
float MouthRadius,
const FVector& InDoorDirection,
float StrateTopZ,
float StrateBottomZ,
float BoundarySealThickness,
bool bSourcePlayerFit,
bool bHasNetworkConnector,
const FVector& NetworkPoint,
float RootSpineRadius)
{
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.
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 SafeSeal = FMath::Max(BoundarySealThickness, 0.0f);
FVector Direction(InDoorDirection.X, InDoorDirection.Y, 0.0f);
if (!Direction.Normalize())
{
Direction = FVector(1.0f, 0.0f, 0.0f);
}
Landing.StandingPoint = InStandingPoint;
Landing.FloorZ = InStandingPoint.Z - 0.5f;
Landing.HalfWidth = DesiredHalfWidth;
Landing.CeilingZ = Landing.FloorZ + DesiredHeight;
Landing.FloorThickness = 3.0f;
Landing.DoorDirection = Direction;
Landing.RootSpineRadius = FMath::Max(RootSpineRadius, 0.0f);
Landing.bSourcePlayerFit = bSourcePlayerFit;
Landing.bHasNetworkConnector = false;
Landing.RootFloorZ = Landing.FloorZ;
Landing.RootCeilingZ = Landing.CeilingZ;
// Keep the complete room and its floor strictly inside the vertical seal. Normal authored
// strates have far more room than this bound; the clamp is a fail-safe for a source pose near
// a seal and never moves a normal interior pose.
const float InnerBottom = StrateBottomZ + SafeSeal;
const float InnerTop = StrateTopZ - SafeSeal;
if (FMath::IsFinite(InnerBottom) && FMath::IsFinite(InnerTop)
&& InnerTop > InnerBottom)
{
// Passage carving is intentionally ordered after the vertical seal and fades over its
// four-voxel blend radius. Keep both the underside of the support slab and the top of the
// room beyond that radius from the seal, so the landing cannot use the passage exception
// to open a strate boundary.
const float MinFloor = InnerBottom + Landing.FloorThickness
+ VF_LandingCarveSafetyMargin;
const float MaxFloor = InnerTop - DesiredHeight - VF_LandingCarveSafetyMargin;
if (MaxFloor >= MinFloor)
{
Landing.FloorZ = FMath::Clamp(Landing.FloorZ, MinFloor, MaxFloor);
Landing.StandingPoint.Z = Landing.FloorZ + 0.5f;
Landing.CeilingZ = Landing.FloorZ + DesiredHeight;
}
else
{
// A degenerate strate cannot satisfy the full body/headroom contract. Fail closed
// instead of emitting a partial chamber whose blend would approach a seal.
Landing.HalfWidth = 0.0f;
Landing.CeilingZ = Landing.FloorZ;
Landing.bSourcePlayerFit = false;
return Landing;
}
}
// The tube centreline is tangent to the floor: the tube bottom is exactly FloorZ. The door
// sits one voxel inside the room so the room/tube smooth union cannot leave a lip.
Landing.DoorPoint = Landing.StandingPoint
+ Direction * FMath::Max(0.0f, Landing.HalfWidth - 1.0f);
Landing.DoorPoint.Z = Landing.FloorZ + SafeMouthRadius;
if (bHasNetworkConnector
&& FMath::IsFinite(NetworkPoint.X)
&& FMath::IsFinite(NetworkPoint.Y)
&& FMath::IsFinite(NetworkPoint.Z))
{
// The connector is a swept rectangular ramp. Its source end keeps the authored landing
// floor; its root end is the common, deterministic interior floor supplied by the
// manager. This is what joins two mouths in one strate when their source floors sit at
// 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
// 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);
Landing.ConnectorCeilingZ = Landing.FloorZ + ConnectorHeight;
Landing.RootFloorZ = NetworkPoint.Z;
Landing.RootCeilingZ = Landing.RootFloorZ + ConnectorHeight;
// Clamp the supplied common level again at the geometry boundary. This is deliberately
// a clamp, not a per-passage repair: all callers derive NetworkPoint.Z from the strate's
// bounds, and malformed tiny strates remain sealed rather than producing a root breach.
if (FMath::IsFinite(InnerBottom) && FMath::IsFinite(InnerTop)
&& InnerTop > InnerBottom)
{
const float RootMinFloor = InnerBottom + Landing.FloorThickness
+ VF_LandingCarveSafetyMargin;
const float RootMaxFloor = InnerTop - ConnectorHeight
- VF_LandingCarveSafetyMargin;
if (RootMaxFloor >= RootMinFloor)
{
Landing.RootFloorZ = FMath::Clamp(
Landing.RootFloorZ, RootMinFloor, RootMaxFloor);
Landing.RootCeilingZ = Landing.RootFloorZ + ConnectorHeight;
}
else
{
// There is no seal-safe interval for a full landing connector in a malformed
// tiny strate. Fail closed: a partial ramp that reaches a boundary is worse than
// an explicit unanswered landing, and normal authored strates never enter this
// branch.
return Landing;
}
}
else
{
return Landing;
}
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);
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.
// 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
// 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;
const FVector2D SourceXY(Landing.ConnectorStart.X, Landing.ConnectorStart.Y);
const FVector2D RootXY(Landing.ConnectorEnd.X, Landing.ConnectorEnd.Y);
const FVector2D Direct = RootXY - SourceXY;
const float DirectLength = Direct.Size();
const float FloorRise = FMath::Abs(Landing.RootFloorZ - Landing.FloorZ);
const float RequiredRampLength = FloorRise / WalkableSlope;
const float SourceFlatLength = FMath::Max(Landing.HalfWidth - 1.0f, 0.0f);
if (FloorRise > KINDA_SMALL_NUMBER
&& (DirectLength <= KINDA_SMALL_NUMBER
|| DirectLength < RequiredRampLength + RootFlatLength + SourceFlatLength))
{
FVector2D Along = Direct;
if (!Along.Normalize())
{
Along = FVector2D(Direction.X, Direction.Y);
if (!Along.Normalize())
{
Along = FVector2D(1.0f, 0.0f);
}
}
const FVector2D Perpendicular(-Along.Y, Along.X);
const float DogLegLength = RequiredRampLength + RootFlatLength + 1.0f;
const FVector2D ControlXY = RootXY + Perpendicular * DogLegLength;
Landing.ConnectorControl = FVector(ControlXY.X, ControlXY.Y, ConnectorZ);
Landing.bHasConnectorBend = true;
}
else if (FloorRise > KINDA_SMALL_NUMBER)
{
// Keep the same straight geometry when it already satisfies the bound. The explicit
// false flag is part of the descriptor so the evaluator and order-independence test
// cannot infer a different topology from an uninitialised control point.
Landing.ConnectorControl = FVector::ZeroVector;
Landing.bHasConnectorBend = false;
}
else
{
Landing.ConnectorControl = FVector(
(Landing.ConnectorStart.X + Landing.ConnectorEnd.X) * 0.5f,
(Landing.ConnectorStart.Y + Landing.ConnectorEnd.Y) * 0.5f,
ConnectorZ);
Landing.bHasConnectorBend = false;
}
Landing.bHasNetworkConnector = true;
}
return Landing;
}
float VF_EvaluatePassageLandingSDF(
const FVector& Position,
const FVoxelPassageLanding& Landing)
{
if (!FMath::IsFinite(Position.X) || !FMath::IsFinite(Position.Y)
|| !FMath::IsFinite(Position.Z)
|| !FMath::IsFinite(Landing.StandingPoint.X)
|| !FMath::IsFinite(Landing.StandingPoint.Y)
|| !FMath::IsFinite(Landing.FloorZ)
|| !FMath::IsFinite(Landing.CeilingZ)
|| !FMath::IsFinite(Landing.HalfWidth)
|| Landing.HalfWidth <= 0.0f
|| Landing.CeilingZ <= Landing.FloorZ)
{
return FLT_MAX;
}
const FVector RoomCenter(
Landing.StandingPoint.X,
Landing.StandingPoint.Y,
(Landing.FloorZ + Landing.CeilingZ) * 0.5f);
constexpr float RoomRounding = 1.5f;
const FVector RoomHalfExtent(
FMath::Max(Landing.HalfWidth - RoomRounding, 0.25f),
FMath::Max(Landing.HalfWidth - RoomRounding, 0.25f),
FMath::Max((Landing.CeilingZ - Landing.FloorZ) * 0.5f - RoomRounding, 0.25f));
// max(Room, FloorZ-Z) is the important distinction from a bore: below FloorZ is solid,
// while every horizontal section above it has the same flat floor plane.
float LandingSDF = FMath::Max(
VoxelSDF::RoundedBox(Position, RoomCenter, RoomHalfExtent, RoomRounding),
Landing.FloorZ - Position.Z);
if (Landing.bHasNetworkConnector && Landing.ConnectorRadius > 0.0f
&& FMath::IsFinite(Landing.ConnectorCeilingZ)
&& Landing.ConnectorCeilingZ > Landing.FloorZ
&& FMath::IsFinite(Landing.RootFloorZ)
&& FMath::IsFinite(Landing.RootCeilingZ)
&& Landing.RootCeilingZ > Landing.RootFloorZ)
{
const FVoxelPassageLandingConnectorProjection Projection =
VF_ProjectPassageLandingConnector(Position, Landing);
const float FloorT = VF_PassageLandingConnectorFloorParameter(Projection, Landing);
const float HorizontalSDF = (FVector2D(Position.X, Position.Y)
- Projection.Closest).Size()
- Landing.ConnectorRadius;
const float ConnectorFloorZ = FMath::Lerp(
Landing.FloorZ, Landing.RootFloorZ, FloorT);
const float ConnectorCeilingZ = FMath::Lerp(
Landing.ConnectorCeilingZ, Landing.RootCeilingZ, FloorT);
const float VerticalSDF = FMath::Max(
ConnectorFloorZ - Position.Z,
Position.Z - ConnectorCeilingZ);
// A swept box with a flat source leg and a measured-slope final ramp, not a round bore:
// the floor normal is deterministic and bounded, while the fixed twelve-voxel clear height
// follows the route. The source and root ends are exactly the two flat landing floors.
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;
}
bool VF_IsPassageLandingFloor(
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;
}
const auto IsInFloorBand = [](float Z, float FloorZ, float Thickness)
{
// The analytic ramp and the sampled density use the same float interpolation, but a
// caller can arrive at the mathematically identical plane through a different fused
// multiply/add sequence. Include one machine epsilon on the upper side so the support
// 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)
{
const float FloorHalfWidth = FMath::Max(Landing.HalfWidth - 1.0f, 0.0f);
if (FMath::Abs(Position.X - Landing.StandingPoint.X) <= FloorHalfWidth
&& FMath::Abs(Position.Y - Landing.StandingPoint.Y) <= FloorHalfWidth)
{
return true;
}
}
if (Landing.bHasNetworkConnector && Landing.ConnectorRadius > 0.0f
&& FMath::IsFinite(Landing.RootFloorZ)
&& FMath::IsFinite(Landing.RootCeilingZ)
&& Landing.RootCeilingZ > Landing.RootFloorZ)
{
const FVoxelPassageLandingConnectorProjection Projection =
VF_ProjectPassageLandingConnector(Position, Landing);
const FVector2D& Closest = Projection.Closest;
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))
{
// 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
// a solid walking strip without capping a neighbouring shaft at its edge.
const float FloorRadius = FMath::Max(
Landing.ConnectorRadius - 0.5f,
FVoxelPlayerCapsuleConstants::RadiusVoxels);
if ((FVector2D(Position.X, Position.Y) - Closest).SizeSquared()
<= FMath::Square(FloorRadius))
{
return true;
}
}
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;
}
bool VF_SuggestLandingPoint(
ECaveGeneratorType Archetype,
const FStrateGenerationParams& CaveParams,
@@ -1854,8 +1854,7 @@ namespace
void Eval(float X, float Y, float Z, FVoxelOpSample& InOut) const override
{
if (!Manager) { return; }
const float ModSDF = Manager->EvaluateModifierSDF(X, Y, Z);
VF_ApplyPassageCarving(InOut.Density, ModSDF, Base, Seal);
Manager->ApplyPassageModifier(InOut.Density, X, Y, Z, Base, Seal);
}
// ≡ la garde `AnyPassageNearBox` écrite à la main dans ClassifyTile — déjà écrite, ici
@@ -1863,6 +1862,12 @@ namespace
EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext&) const override
{
if (!Manager) { return EVoxelOpEffect::Identity; }
if (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.
return EVoxelOpEffect::Both;
}
return Manager->AnyPassageNearBox(VoxelBox.Min, VoxelBox.Max)
? EVoxelOpEffect::CarveOnly : EVoxelOpEffect::Identity;
}
@@ -5207,10 +5212,10 @@ void FVoxelOpStack::AppendStructuralPost(float StrateTopWorldZ, float StrateBott
const UVoxelStrateManager* StrateManager)
{
// ORDRE NON NÉGOCIABLE : la spine creuse l'intérieur (et ne touche JAMAIS les bandes de seal),
// le seal vertical re-solidifie ses bandes, les passages percent les seals, puis la limite XY
// gagne sur tout ce qui précède. Ainsi, même un passage placé dans la rampe ou au-delà du rayon
// ne peut pas ouvrir la coque extérieure. Les éditions joueur restent le dernier post de
// GetDensityAt, hors de cette pile, comme avant.
// le seal vertical re-solidifie ses bandes, les passages (tube + landing + support) percent
// les seals, puis la limite XY gagne sur tout ce qui précède. Ainsi, même un passage placé
// dans la rampe ou au-delà du rayon ne peut pas ouvrir la coque extérieure. Les éditions joueur
// 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));
+107 -21
View File
@@ -289,12 +289,6 @@ static FORCEINLINE void ApplyBoundarySeal(float& Density, float WorldZ,
VF_ApplyBoundarySeal(Density, WorldZ, StrateTopZ, StrateBottomZ, Thickness, BaseDensity);
}
static FORCEINLINE void ApplyPassageCarving(float& Density, float ModSDF,
float BaseDensity, float SealThickness)
{
VF_ApplyPassageCarving(Density, ModSDF, BaseDensity, SealThickness);
}
static FORCEINLINE void ApplyOriginSpine(float& Density, float WorldX, float WorldY, float WorldZ,
float StrateTopZ, float StrateBottomZ, float SealThickness, float BaseDensity, float Radius)
{
@@ -681,11 +675,12 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
{
// SOLID BEDROCK gap between two strates. The auto-carved passages still tunnel
// through it, but the (0,0) descent stays solid here so the player digs the gap
// to reach the next layer. No caves, no spine, no vertical seal — just rock + passages;
// to reach the next layer. No caves, no spine, no vertical seal — just rock + passage
// tube/landing;
// the global XY edge seal is applied below for bounded worlds.
float Density = 8.0f; // bedrock solidity (positive = solid)
const float ModSDF = StrateManager->EvaluateModifierSDF(WorldX, WorldY, WorldZ);
ApplyPassageCarving(Density, ModSDF, 8.0f, 0.0f);
StrateManager->ApplyPassageModifier(
Density, WorldX, WorldY, WorldZ, 8.0f, 0.0f);
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY, WorldRadiusVoxels, EdgeSealThickness, 8.0f);
Result = -Density;
}
@@ -996,6 +991,81 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
// Disturbance layer (the "wow" post-process) — cached params, MC convention.
ApplyDisturbances(Result, WorldX, WorldY, WorldZ, CP_Dist, (uint32)Seed);
// A disturbance is allowed to add visual detail, but it must not refill the landing's
// measured air volume or turn its support slab back into a hole. Reassert the landing
// air first, then the structural floor, in MC space before the final XY seal. The two
// calls use the same landing geometry already evaluated by the legacy and op-stack post.
float LandingBaseDensity = CP_Dist.BaseDensity;
#if WITH_EDITOR
// A composer parent can deliberately use a different structural base than the authored
// definition. The landing floor was already applied by PassageCarveOp with that parent
// base; use the same value for this post-disturbance MC backstop or live-vs-direct stack
// evaluation would differ exactly on floor voxels.
if (CP_UseComposerRegions && CP_ComposerRegions.bHasGlobalStructuralParams)
{
LandingBaseDensity = CP_ComposerRegions.BaseDensity;
}
else
#endif
if (CP_UseCustomRecipe)
{
switch (CP_CustomRecipe.StructuralParamBlock)
{
case EVoxelStrateParamBlock::TunnelNetwork:
LandingBaseDensity = CP_CustomParams.TunnelNetworkParams.BaseDensity;
break;
case EVoxelStrateParamBlock::Slab:
LandingBaseDensity = CP_CustomParams.SlabParams.BaseDensity;
break;
case EVoxelStrateParamBlock::Maze:
LandingBaseDensity = CP_CustomParams.MazeParams.BaseDensity;
break;
case EVoxelStrateParamBlock::Surface:
LandingBaseDensity = CP_CustomParams.SurfaceParams.BaseDensity;
break;
case EVoxelStrateParamBlock::VerticalShaft:
LandingBaseDensity = CP_CustomParams.VerticalShaftParams.BaseDensity;
break;
case EVoxelStrateParamBlock::FloatingIsland:
LandingBaseDensity = CP_CustomParams.FloatingIslandParams.BaseDensity;
break;
default:
break;
}
}
else
{
switch (CP_GenType)
{
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber:
LandingBaseDensity = CP_Slab.BaseDensity;
break;
case ECaveGeneratorType::Maze:
LandingBaseDensity = CP_Maze.BaseDensity;
break;
case ECaveGeneratorType::SurfaceWorld:
LandingBaseDensity = CP_Surface.BaseDensity;
break;
case ECaveGeneratorType::VerticalShafts:
LandingBaseDensity = CP_Vert.BaseDensity;
break;
case ECaveGeneratorType::FloatingIslands:
LandingBaseDensity = CP_Float.BaseDensity;
break;
case ECaveGeneratorType::TunnelNetwork:
case ECaveGeneratorType::Underwater:
default:
LandingBaseDensity = CP_Tunnel.BaseDensity;
break;
}
}
StrateManager->ApplyPassageLandingAirMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity,
CP_Dist.BoundarySealThickness);
StrateManager->ApplyPassageLandingFloorMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity);
}
else
{
@@ -2151,8 +2221,9 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
//=========================================================================
if (StrateManager)
{
const float ModSDF = StrateManager->EvaluateModifierSDF(WorldX, WorldY, WorldZ);
ApplyPassageCarving(Density, ModSDF, Params.BaseDensity, Params.BoundarySealThickness);
StrateManager->ApplyPassageModifier(
Density, WorldX, WorldY, WorldZ,
Params.BaseDensity, Params.BoundarySealThickness);
}
// Fourth structural post: the XY edge wins over a passage near the rim.
@@ -2373,8 +2444,9 @@ float UVoxelGenerator::GetSlabDensity(float WorldX, float WorldY, float WorldZ,
if (StrateManager)
{
const float ModSDF = StrateManager->EvaluateModifierSDF(WorldX, WorldY, WorldZ);
ApplyPassageCarving(Density, ModSDF, Params.BaseDensity, Params.BoundarySealThickness);
StrateManager->ApplyPassageModifier(
Density, WorldX, WorldY, WorldZ,
Params.BaseDensity, Params.BoundarySealThickness);
}
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY,
@@ -2493,8 +2565,9 @@ float UVoxelGenerator::GetMazeDensity(float WorldX, float WorldY, float WorldZ,
if (StrateManager)
{
const float ModSDF = StrateManager->EvaluateModifierSDF(WorldX, WorldY, WorldZ);
ApplyPassageCarving(Density, ModSDF, Params.BaseDensity, Params.BoundarySealThickness);
StrateManager->ApplyPassageModifier(
Density, WorldX, WorldY, WorldZ,
Params.BaseDensity, Params.BoundarySealThickness);
}
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY,
@@ -2756,8 +2829,9 @@ float UVoxelGenerator::SurfaceDensityFromColumn(float WorldX, float WorldY, floa
if (StrateManager)
{
const float ModSDF = StrateManager->EvaluateModifierSDF(WorldX, WorldY, WorldZ);
ApplyPassageCarving(Density, ModSDF, S.BaseDensity, S.BoundarySealThickness);
StrateManager->ApplyPassageModifier(
Density, WorldX, WorldY, WorldZ,
S.BaseDensity, S.BoundarySealThickness);
}
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY,
@@ -3139,6 +3213,16 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
{
bCanSolid = false;
}
// A landing is bidirectional geometry: its chamber kills the AllSolid hypothesis, while its
// guaranteed support slab kills AllAir. Without this second guard a tile covering only the
// floor could be classified uniformly air and the mesher would skip the solid support that
// keeps the player from falling through it. The operator-stack path returns Both for the
// same box, so the hand-written legacy fold must carry the identical hypothesis kill here.
if (StrateManager->AnyPassageLandingFloorNearBox(
FVector(MinX, MinY, MinZ), FVector(MaxX, MaxY, MaxZ)))
{
bCanAir = false;
}
if (OriginSpineRadius > 0.0f)
{
// Spine (0,0) : colonne XY rayon R + blend 3 (cf. ApplyOriginSpine). Test cercle/boîte XY.
@@ -4468,8 +4552,9 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
if (StrateManager)
{
const float ModSDF = StrateManager->EvaluateModifierSDF(WorldX, WorldY, WorldZ);
ApplyPassageCarving(Density, ModSDF, Params.BaseDensity, Params.BoundarySealThickness);
StrateManager->ApplyPassageModifier(
Density, WorldX, WorldY, WorldZ,
Params.BaseDensity, Params.BoundarySealThickness);
}
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY,
@@ -4628,8 +4713,9 @@ float UVoxelGenerator::GetFloatingIslandDensity(float WorldX, float WorldY, floa
if (StrateManager)
{
const float ModSDF = StrateManager->EvaluateModifierSDF(WorldX, WorldY, WorldZ);
ApplyPassageCarving(Density, ModSDF, Params.BaseDensity, Params.BoundarySealThickness);
StrateManager->ApplyPassageModifier(
Density, WorldX, WorldY, WorldZ,
Params.BaseDensity, Params.BoundarySealThickness);
}
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY,
+446 -55
View File
@@ -7,6 +7,7 @@
#include "VoxelSeasonAsset.h"
#include "VoxelTypes.h" // For CHUNK_SIZE, VOXEL_SIZE, WorldToChunkCoord
#include "VoxelCaveMorphology.h" // For VoxelSDF and VoxelHash
#include "VoxelDensityPrimitives.h" // Shared passage carve polarity/strength
#include "VoxelTerrainOpDefinition.h" // For UVoxelTerrainOpDefinition::ApplyTo
#include "VoxelBiomeDefinition.h" // For UVoxelBiomeDefinition (biome context flatten)
#include "UObject/UObjectGlobals.h"
@@ -60,6 +61,65 @@ static float PassageFBM(float X, float Seed)
return (MaxV > 0.0f) ? (Total / MaxV) : 0.0f;
}
namespace
{
/**
* One shared passage cache for the tube SDF and landing-floor fill.
*
* The source query and all landing dimensions are resolved by GeneratePassages. This cache
* only narrows the immutable passage array once per (manager, version, chunk); neither the
* player-fit stencil nor a topology search can leak into the voxel loop.
*/
struct FPassageEvaluationCache
{
const UVoxelStrateManager* Owner = nullptr;
FIntVector Chunk = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX);
uint32 Version = 0xFFFFFFFFu;
TArray<int32> Nearby;
};
FPassageEvaluationCache& VF_GetPassageEvaluationCache()
{
thread_local FPassageEvaluationCache Cache;
return Cache;
}
const TArray<int32>& VF_GetNearbyPassages(
const UVoxelStrateManager* Manager,
const FIntVector& ChunkCoord)
{
FPassageEvaluationCache& Cache = VF_GetPassageEvaluationCache();
const uint32 Version = Manager ? Manager->GetLayoutVersion() : 0u;
if (Cache.Owner == Manager && Cache.Chunk == ChunkCoord && Cache.Version == Version)
{
return Cache.Nearby;
}
Cache.Owner = Manager;
Cache.Chunk = ChunkCoord;
Cache.Version = Version;
Cache.Nearby.Reset();
if (!Manager) return Cache.Nearby;
const FVector ChunkCenter(
(ChunkCoord.X + 0.5f) * (float)CHUNK_SIZE,
(ChunkCoord.Y + 0.5f) * (float)CHUNK_SIZE,
(ChunkCoord.Z + 0.5f) * (float)CHUNK_SIZE);
const float ChunkRadius = (float)CHUNK_SIZE * 0.8660254f + 3.0f;
const TArray<FVoxelPassage>& Passages = Manager->GetPassages();
for (int32 PassageIndex = 0; PassageIndex < Passages.Num(); ++PassageIndex)
{
const FVoxelPassage& Passage = Passages[PassageIndex];
const float Reach = Passage.BoundRadius + ChunkRadius;
if (FVector::DistSquared(ChunkCenter, Passage.BoundCenter) <= Reach * Reach)
{
Cache.Nearby.Add(PassageIndex);
}
}
return Cache.Nearby;
}
}
bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
{
if (!Settings)
@@ -601,10 +661,14 @@ void UVoxelStrateManager::GeneratePassages()
constexpr uint32 PassageSaltNoiseZ = 0xA1100008u;
constexpr uint32 PassageSaltPhase = 0xA1100009u;
constexpr uint32 PassageSaltBendFreq = 0xA110000Au;
constexpr uint32 PassageSaltUpperDoor = 0xA110000Bu;
constexpr uint32 PassageSaltLowerDoor = 0xA110000Cu;
int32 TotalPassages = 0;
int32 NumAimedAtUpperPlayerFit = 0;
int32 NumAimedAtLowerPlayerFit = 0;
int32 NumUpperRootConnectors = 0;
int32 NumLowerRootConnectors = 0;
TSet<FString> NoQueryArchetypes;
const auto ArchetypeName = [](ECaveGeneratorType Archetype)
@@ -678,6 +742,9 @@ void UVoxelStrateManager::GeneratePassages()
case ECaveGeneratorType::Maze:
return Definition.MazeParams.BoundarySealThickness;
case ECaveGeneratorType::SurfaceWorld:
return Definition.SurfaceParams.BoundarySealThickness;
case ECaveGeneratorType::VerticalShafts:
return Definition.VerticalShaftParams.BoundarySealThickness;
@@ -689,6 +756,30 @@ void UVoxelStrateManager::GeneratePassages()
}
};
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.
const float Seal = FMath::Max(BoundarySealThickness, 0.0f);
const float InnerBottom = StrateBottomZ + Seal;
const float InnerTop = StrateTopZ - Seal;
const float ConnectorHeight = FMath::Max(
12.0f, FVoxelPlayerCapsuleConstants::HeightVoxels + 4.0f);
constexpr float PassageBlendSafety = 4.0f;
const float MinimumFloor = InnerBottom + 3.0f + PassageBlendSafety;
const float MaximumFloor = InnerTop - ConnectorHeight - PassageBlendSafety;
if (FMath::IsFinite(MinimumFloor) && FMath::IsFinite(MaximumFloor)
&& MaximumFloor >= MinimumFloor)
{
return 0.5f * (MinimumFloor + MaximumFloor);
}
return 0.5f * (InnerBottom + InnerTop);
};
//=========================================================================
// INTER-STRATE PASSAGES: tunnels connecting consecutive strates.
// Each passage is randomly assigned one of 5 types, which determines
@@ -983,8 +1074,79 @@ void UVoxelStrateManager::GeneratePassages()
Passage.ControlRadii.Add(RadiusAt(T));
}
Passage.UpperPoint = Passage.ControlPoints[0];
Passage.LowerPoint = Passage.ControlPoints.Last();
// The old control-point endpoints were also the player's standing points. That made
// a sloped/vertical tube open directly under the capsule. Keep the queried points as
// explicit standing anchors, and move only the tube's first/last point to a doorway
// tangent to the landing floor. The endpoint-to-anchor distinction is the geometry
// that turns a mouth into a place.
auto DoorDirectionFrom = [](const FVector& Delta, float Angle) -> FVector
{
FVector Direction(Delta.X, Delta.Y, 0.0f);
if (!Direction.Normalize())
{
Direction = FVector(FMath::Cos(Angle), FMath::Sin(Angle), 0.0f);
if (!Direction.Normalize())
{
Direction = FVector(1.0f, 0.0f, 0.0f);
}
}
return Direction;
};
const FVector UpperDoorDirection = DoorDirectionFrom(
Passage.ControlPoints.Num() > 1
? Passage.ControlPoints[1] - Passage.ControlPoints[0]
: FVector::ZeroVector,
PassageRandom01(PassageSaltUpperDoor) * 2.0f * PI);
const FVector LowerDoorDirection = DoorDirectionFrom(
Passage.ControlPoints.Num() > 1
? Passage.ControlPoints[Passage.ControlPoints.Num() - 2]
- Passage.ControlPoints.Last()
: FVector::ZeroVector,
PassageRandom01(PassageSaltLowerDoor) * 2.0f * PI);
// A source-fit answer proves only a local pose; none of the pure queries performs a
// flood fill over the live strate. Every mouth therefore receives the same explicit
// flat-floor connector to the deterministic root, including answered mouths. This
// is what makes the join guaranteed rather than merely probable.
Passage.UpperLanding = VF_BuildPassageLanding(
FVector(UpperX, UpperY, TopZ),
Cfg.MouthRadius,
UpperDoorDirection,
UpperTopZ,
UpperBottomZ,
UpperDef ? BoundarySealThicknessFor(*UpperDef) : 0.0f,
bAimedUpperAtPlayerFitPoint,
/*bHasNetworkConnector=*/true,
FVector(0.0f, 0.0f,
CommonRootFloorFor(
UpperTopZ, UpperBottomZ,
UpperDef ? BoundarySealThicknessFor(*UpperDef) : 0.0f)),
OriginSpineRadius);
Passage.LowerLanding = VF_BuildPassageLanding(
FVector(LowerX, LowerY, BottomZ),
Cfg.MouthRadius,
LowerDoorDirection,
LowerTopZ,
(float)(Lower.BottomChunkZ) * CHUNK_SIZE,
LowerDef ? BoundarySealThicknessFor(*LowerDef) : 0.0f,
bAimedLowerAtPlayerFitPoint,
/*bHasNetworkConnector=*/true,
FVector(0.0f, 0.0f,
CommonRootFloorFor(
LowerTopZ, (float)(Lower.BottomChunkZ) * CHUNK_SIZE,
LowerDef ? BoundarySealThicknessFor(*LowerDef) : 0.0f)),
OriginSpineRadius);
Passage.UpperPoint = Passage.UpperLanding.StandingPoint;
Passage.LowerPoint = Passage.LowerLanding.StandingPoint;
if (Passage.UpperLanding.bHasNetworkConnector) ++NumUpperRootConnectors;
if (Passage.LowerLanding.bHasNetworkConnector) ++NumLowerRootConnectors;
if (Passage.ControlPoints.Num() > 0)
{
Passage.ControlPoints[0] = Passage.UpperLanding.DoorPoint;
Passage.ControlPoints.Last() = Passage.LowerLanding.DoorPoint;
}
Passage.Radius = 0.0f;
for (const float ControlRadius : Passage.ControlRadii)
{
@@ -992,20 +1154,76 @@ void UVoxelStrateManager::GeneratePassages()
}
// Fallback / bounds if an invalid authored width produced no positive profile.
Passage.Radius = FMath::Max(Passage.Radius, FMath::Max(Cfg.MouthRadius, Cfg.MidRadius));
Passage.Radius = FMath::Max(Passage.Radius,
FMath::Max(Passage.UpperLanding.ConnectorRadius,
Passage.LowerLanding.ConnectorRadius));
// Bounding sphere over all control points (+ widest radius + blend) for culling. This
// remains conservative when either mouth snaps laterally: every slanted segment is
// between two control points, and a segment lies inside the sphere containing both
// endpoints. Under-sizing this sphere would make EvaluateModifierSDF cull a real
// passage and leave a sealed pocket.
// Bounding sphere over the tube, both rooms, both floor slabs, and both optional root
// connectors (+ widest radius + blend) for culling. Under-sizing this sphere would
// cull a real landing and leave a sealed pocket, so the room's full box diagonal is
// included rather than treating the standing anchor as a point.
{
FVector Center = FVector::ZeroVector;
for (const FVector& CP : Passage.ControlPoints) Center += CP;
Center /= (float)Passage.ControlPoints.Num();
float MaxDistSq = 0.0f;
FVector BoundsMin(FLT_MAX, FLT_MAX, FLT_MAX);
FVector BoundsMax(-FLT_MAX, -FLT_MAX, -FLT_MAX);
auto IncludePoint = [&BoundsMin, &BoundsMax](const FVector& Point, float Pad)
{
BoundsMin.X = FMath::Min(BoundsMin.X, Point.X - Pad);
BoundsMin.Y = FMath::Min(BoundsMin.Y, Point.Y - Pad);
BoundsMin.Z = FMath::Min(BoundsMin.Z, Point.Z - Pad);
BoundsMax.X = FMath::Max(BoundsMax.X, Point.X + Pad);
BoundsMax.Y = FMath::Max(BoundsMax.Y, Point.Y + Pad);
BoundsMax.Z = FMath::Max(BoundsMax.Z, Point.Z + Pad);
};
for (const FVector& CP : Passage.ControlPoints)
MaxDistSq = FMath::Max(MaxDistSq, (float)FVector::DistSquared(Center, CP));
const float R = FMath::Sqrt(MaxDistSq) + Passage.Radius + 4.0f;
{
IncludePoint(CP, Passage.Radius + 4.0f);
}
const auto IncludeLanding = [&IncludePoint](const FVoxelPassageLanding& Landing)
{
const float Height = FMath::Max(Landing.CeilingZ - Landing.FloorZ, 0.0f);
const FVector RoomCenter(
Landing.StandingPoint.X,
Landing.StandingPoint.Y,
(Landing.FloorZ + Landing.CeilingZ) * 0.5f);
const float RoomRadius = FMath::Sqrt(
2.0f * FMath::Square(Landing.HalfWidth)
+ 0.25f * FMath::Square(Height))
+ Landing.FloorThickness + 4.0f;
IncludePoint(RoomCenter, RoomRadius);
if (Landing.bHasNetworkConnector)
{
const float HubRadius = Landing.RootSpineRadius
+ Landing.ConnectorRadius;
const float HubHalfHeight = 0.5f * FMath::Max(
Landing.RootCeilingZ - Landing.RootFloorZ, 0.0f);
IncludePoint(FVector(0.0f, 0.0f,
(Landing.RootFloorZ + Landing.RootCeilingZ) * 0.5f),
FMath::Sqrt(FMath::Square(HubRadius)
+ FMath::Square(HubHalfHeight))
+ Landing.FloorThickness + 4.0f);
const float ConnectorHalfHeight = FMath::Max(
0.5f * FMath::Max(
Landing.ConnectorCeilingZ - Landing.FloorZ, 0.0f),
Landing.ConnectorRadius);
IncludePoint(Landing.ConnectorStart,
ConnectorHalfHeight + 4.0f);
if (Landing.bHasConnectorBend)
{
IncludePoint(Landing.ConnectorControl,
ConnectorHalfHeight + 4.0f);
}
IncludePoint(Landing.ConnectorEnd,
ConnectorHalfHeight + 4.0f);
}
};
IncludeLanding(Passage.UpperLanding);
IncludeLanding(Passage.LowerLanding);
const FVector Center = (BoundsMin + BoundsMax) * 0.5f;
float MaxDistSq = 0.0f;
MaxDistSq = FMath::Max(MaxDistSq, (float)FVector::DistSquared(Center, BoundsMin));
MaxDistSq = FMath::Max(MaxDistSq, (float)FVector::DistSquared(Center, BoundsMax));
const float R = FMath::Sqrt(MaxDistSq);
Passage.BoundCenter = Center;
Passage.BoundRadius = R;
Passage.BoundRadiusSq = R * R;
@@ -1028,11 +1246,13 @@ void UVoxelStrateManager::GeneratePassages()
}
UE_LOG(LogTemp, Log,
TEXT("[StrateManager] Passage landing sites: upper %d/%d and lower %d/%d aimed at player-fit source space, %d/%d mouth queries fell back to random reach (archetypes with no query: %s)."),
TEXT("[StrateManager] Passage landings: upper %d/%d and lower %d/%d source-fit; root floor connectors %d/%d; %d/%d mouth queries fell back to random reach (archetypes with no query: %s)."),
NumAimedAtUpperPlayerFit,
TotalPassages,
NumAimedAtLowerPlayerFit,
TotalPassages,
NumUpperRootConnectors,
NumLowerRootConnectors,
(TotalPassages * 2) - NumAimedAtUpperPlayerFit - NumAimedAtLowerPlayerFit,
TotalPassages * 2,
*NoQueryList);
@@ -1080,52 +1300,16 @@ void UVoxelStrateManager::GeneratePassages()
float UVoxelStrateManager::EvaluateModifierSDF(float WorldX, float WorldY, float WorldZ) const
{
//=========================================================================
// PER-CHUNK PASSAGE SHORTLIST
//=========================================================================
// This runs PER VOXEL (35³ per tile). The vast majority of chunks are nowhere near a
// descent passage, yet every voxel still walked the WHOLE Passages array just to reject
// each one on a squared-distance test (Passages.Num() × 35³ rejects per tile, all wasted).
// Cache, per chunk, the shortlist of passages whose bounds actually reach this chunk —
// usually EMPTY → instant FLT_MAX return (no carve). Indices (not pointers) + a version
// stamp keep it safe across a GeneratePassages rebuild. Output is bit-identical: the
// shortlist is a conservative superset (chunk bounding sphere vs each passage bound).
thread_local FIntVector SL_Chunk(INT32_MAX, INT32_MAX, INT32_MAX);
thread_local uint32 SL_Version = 0xFFFFFFFFu;
thread_local TArray<int32> SL_Nearby;
const FIntVector ChunkCoord(
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE));
if (ChunkCoord != SL_Chunk || SL_Version != PassagesVersion)
{
SL_Chunk = ChunkCoord;
SL_Version = PassagesVersion;
SL_Nearby.Reset();
// The shortlist is rebuilt once for a (manager, version, chunk) and is shared by the tube,
// landing, and floor paths. No source-fit stencil or topology search is allowed below it.
const TArray<int32>& Nearby = VF_GetNearbyPassages(this, ChunkCoord);
// Chunk bounding sphere (centre + half-diagonal), padded by the blend radius. A passage
// is kept iff its bounding sphere overlaps the chunk's — i.e. some voxel here could be
// inside its per-voxel reject radius. √3/2 · CHUNK_SIZE ≈ 0.866 · size.
const FVector CCenter(
(ChunkCoord.X + 0.5f) * (float)CHUNK_SIZE,
(ChunkCoord.Y + 0.5f) * (float)CHUNK_SIZE,
(ChunkCoord.Z + 0.5f) * (float)CHUNK_SIZE);
const float ChunkR = (float)CHUNK_SIZE * 0.8660254f + 3.0f; // +BlendK
for (int32 i = 0; i < Passages.Num(); ++i)
{
const FVoxelPassage& P = Passages[i];
const float Reach = P.BoundRadius + ChunkR;
if (FVector::DistSquared(CCenter, P.BoundCenter) <= Reach * Reach)
{
SL_Nearby.Add(i);
}
}
}
if (SL_Nearby.Num() == 0) return FLT_MAX; // no passage near this chunk → no carve
if (Nearby.Num() == 0) return FLT_MAX; // no passage near this chunk → no carve
float MinSDF = FLT_MAX;
const float BlendK = 3.0f; // Smooth blend for passage junctions
@@ -1136,7 +1320,7 @@ float UVoxelStrateManager::EvaluateModifierSDF(float WorldX, float WorldY, float
// entry is a simple straight tube. A bounding-sphere reject skips far passages.
//=========================================================================
const FVector Pos(WorldX, WorldY, WorldZ);
for (int32 PIdx : SL_Nearby)
for (int32 PIdx : Nearby)
{
const FVoxelPassage& P = Passages[PIdx];
// BOUNDING-SPHERE REJECT: skip passages this voxel can't possibly be inside.
@@ -1168,11 +1352,125 @@ float UVoxelStrateManager::EvaluateModifierSDF(float WorldX, float WorldY, float
const float PassageSDF = VoxelSDF::Capsule(Pos, P.UpperPoint, P.LowerPoint, P.Radius);
MinSDF = VoxelSDF::SmoothMin(MinSDF, PassageSDF, BlendK);
}
// A landing is a real room with a hard flat-floor half-space, not a sphere around the
// tube endpoint. The optional connector is the same fixed SDF geometry selected during
// GeneratePassages; it never performs a source query here.
const float UpperLandingSDF = VF_EvaluatePassageLandingSDF(Pos, P.UpperLanding);
const float LowerLandingSDF = VF_EvaluatePassageLandingSDF(Pos, P.LowerLanding);
MinSDF = VoxelSDF::SmoothMin(MinSDF, UpperLandingSDF, BlendK);
MinSDF = VoxelSDF::SmoothMin(MinSDF, LowerLandingSDF, BlendK);
}
return MinSDF;
}
void UVoxelStrateManager::ApplyPassageModifier(
float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const
{
const float ModSDF = EvaluateModifierSDF(WorldX, WorldY, WorldZ);
VF_ApplyPassageCarving(Density, ModSDF, BaseDensity, SealThickness);
ApplyPassageLandingAir(Density, WorldX, WorldY, WorldZ, BaseDensity, SealThickness);
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_IsPassageLandingFloor(Position, Passage.UpperLanding)
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding))
{
// 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
// tunnel through the floor and leave the player over a void.
Density = FMath::Max(Density, BaseDensity);
break;
}
}
}
void UVoxelStrateManager::ApplyPassageLandingAir(
float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) 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);
if (Nearby.Num() == 0) return;
const FVector Position(WorldX, WorldY, WorldZ);
float MinLandingSDF = FLT_MAX;
for (const int32 PassageIndex : Nearby)
{
const FVoxelPassage& Passage = Passages[PassageIndex];
if (FVector::DistSquared(Position, Passage.BoundCenter) > Passage.BoundRadiusSq)
{
continue;
}
const FVoxelPassageLanding* Landings[] = {
&Passage.UpperLanding, &Passage.LowerLanding };
for (const FVoxelPassageLanding* Landing : Landings)
{
const float LandingSDF = VF_EvaluatePassageLandingSDF(Position, *Landing);
if (LandingSDF < MinLandingSDF)
{
MinLandingSDF = LandingSDF;
}
}
}
if (MinLandingSDF == FLT_MAX) return;
// Keep the same smooth interior blend used by the passage op. This landing-specific writer is
// idempotent because the MC-facing post may run after the structural op already saw the same
// voxel. The landing's support floor is restored by the floor writer after this MC-facing pass.
VF_ApplyPassageLandingCarving(Density, MinLandingSDF, BaseDensity, SealThickness);
}
void UVoxelStrateManager::ApplyPassageLandingAirMC(
float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const
{
// ApplyDisturbances is deliberately an MC-space post-process and can add a bridge or ridge
// on top of the structural passage. Reassert only landing air here; the support slab is
// restored by ApplyPassageLandingFloorMC immediately afterwards. This stays on the same
// thread-local passage shortlist as the hot voxel path and performs no source/topology work.
float InternalDensity = -Density;
ApplyPassageLandingAir(
InternalDensity, WorldX, WorldY, WorldZ, BaseDensity, SealThickness);
Density = -InternalDensity;
}
void UVoxelStrateManager::ApplyPassageLandingFloorMC(
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_IsPassageLandingFloor(Position, Passage.UpperLanding)
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding))
{
// Result is in MC convention here (negative = solid). This reassertion is the
// structural floor backstop after the optional MC-space disturbance layer.
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
@@ -1194,6 +1492,99 @@ bool UVoxelStrateManager::AnyPassageNearBox(const FVector& MinVoxel, const FVect
return false;
}
bool UVoxelStrateManager::AnyPassageLandingFloorNearBox(
const FVector& MinVoxel, const FVector& MaxVoxel) const
{
// A conservative AABB test is enough for the floor's only solid write. False positives cost
// a tile; a false negative could classify an air tile uniformly and remove the player's floor.
for (const FVoxelPassage& Passage : Passages)
{
const FVoxelPassageLanding* Landings[] = {
&Passage.UpperLanding, &Passage.LowerLanding };
for (const FVoxelPassageLanding* Landing : Landings)
{
if (Landing->HalfWidth <= 0.0f || Landing->FloorThickness <= 0.0f)
{
continue;
}
const float Pad = 1.0f;
const float FloorMinX = Landing->StandingPoint.X - Landing->HalfWidth - Pad;
const float FloorMaxX = Landing->StandingPoint.X + Landing->HalfWidth + Pad;
const float FloorMinY = Landing->StandingPoint.Y - Landing->HalfWidth - Pad;
const float FloorMaxY = Landing->StandingPoint.Y + Landing->HalfWidth + Pad;
const float FloorMinZ = Landing->FloorZ - Landing->FloorThickness - Pad;
const float FloorMaxZ = Landing->FloorZ + Pad;
if (FloorMaxX >= MinVoxel.X && FloorMinX <= MaxVoxel.X
&& FloorMaxY >= MinVoxel.Y && FloorMinY <= MaxVoxel.Y
&& FloorMaxZ >= MinVoxel.Z && FloorMinZ <= MaxVoxel.Z)
{
return true;
}
// The fallback connector carries the same support slab away from the room. Its
// segment is not covered by the room AABB above; omitting it would let a tile that
// contains only the connector floor prove AllAir and erase the guaranteed join.
if (Landing->bHasNetworkConnector && Landing->ConnectorRadius > 0.0f
&& FMath::IsFinite(Landing->ConnectorStart.X)
&& FMath::IsFinite(Landing->ConnectorStart.Y)
&& FMath::IsFinite(Landing->ConnectorStart.Z)
&& FMath::IsFinite(Landing->ConnectorEnd.X)
&& FMath::IsFinite(Landing->ConnectorEnd.Y)
&& FMath::IsFinite(Landing->ConnectorEnd.Z))
{
const float ConnectorPad = Landing->ConnectorRadius + Pad;
float ConnectorMinX = FMath::Min(
Landing->ConnectorStart.X, Landing->ConnectorEnd.X) - ConnectorPad;
float ConnectorMaxX = FMath::Max(
Landing->ConnectorStart.X, Landing->ConnectorEnd.X) + ConnectorPad;
float ConnectorMinY = FMath::Min(
Landing->ConnectorStart.Y, Landing->ConnectorEnd.Y) - ConnectorPad;
float ConnectorMaxY = FMath::Max(
Landing->ConnectorStart.Y, Landing->ConnectorEnd.Y) + ConnectorPad;
if (Landing->bHasConnectorBend)
{
ConnectorMinX = FMath::Min(
ConnectorMinX, Landing->ConnectorControl.X - ConnectorPad);
ConnectorMaxX = FMath::Max(
ConnectorMaxX, Landing->ConnectorControl.X + ConnectorPad);
ConnectorMinY = FMath::Min(
ConnectorMinY, Landing->ConnectorControl.Y - ConnectorPad);
ConnectorMaxY = FMath::Max(
ConnectorMaxY, Landing->ConnectorControl.Y + ConnectorPad);
}
const float ConnectorMinZ = FMath::Min(
Landing->FloorZ, Landing->RootFloorZ)
- Landing->FloorThickness - Pad;
const float ConnectorMaxZ = FMath::Max(
Landing->FloorZ, Landing->RootFloorZ) + Pad;
if (ConnectorMaxX >= MinVoxel.X && ConnectorMinX <= MaxVoxel.X
&& ConnectorMaxY >= MinVoxel.Y && ConnectorMinY <= MaxVoxel.Y
&& ConnectorMaxZ >= MinVoxel.Z && ConnectorMinZ <= MaxVoxel.Z)
{
return true;
}
if (Landing->RootSpineRadius >= 0.0f)
{
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 false;
}
//=============================================================================
// QUERIES
//=============================================================================
@@ -41,6 +41,48 @@ struct FStrateTerrainOpEntry;
class UVoxelTerrainOpDefinition;
enum class ECaveGeneratorType : uint8;
/**
* The geometry contract for one end of an inter-strate passage.
*
* All values are actor-space voxels. `StandingPoint` is the player-fit anchor: its floor is
* `FloorZ = StandingPoint.Z - 0.5`. The chamber is deliberately separate from the tube endpoint
* so a sloped/vertical tube never presents the player with a hole as its first standing position.
*
* 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.
*/
struct VOXELFORGE_API FVoxelPassageLanding
{
FVector StandingPoint = FVector::ZeroVector;
FVector DoorPoint = FVector::ZeroVector;
FVector DoorDirection = FVector(1.0f, 0.0f, 0.0f);
FVector ConnectorStart = FVector::ZeroVector;
FVector ConnectorControl = FVector::ZeroVector;
FVector ConnectorEnd = FVector::ZeroVector;
float FloorZ = 0.0f;
float CeilingZ = 0.0f;
float HalfWidth = 0.0f;
float FloorThickness = 3.0f;
float ConnectorRadius = 0.0f;
float ConnectorCeilingZ = 0.0f;
float RootFloorZ = 0.0f;
float RootCeilingZ = 0.0f;
float RootSpineRadius = 0.0f;
bool bSourcePlayerFit = false;
bool bHasNetworkConnector = false;
bool bHasConnectorBend = false;
};
//=============================================================================
// SDF PRIMITIVES
//=============================================================================
@@ -153,6 +195,29 @@ namespace VoxelSDF
}
}
/** Build the deterministic, body-sized room geometry at one standing anchor. */
VOXELFORGE_API FVoxelPassageLanding VF_BuildPassageLanding(
const FVector& StandingPoint,
float MouthRadius,
const FVector& DoorDirection,
float StrateTopZ,
float StrateBottomZ,
float BoundarySealThickness,
bool bSourcePlayerFit,
bool bHasNetworkConnector,
const FVector& NetworkPoint,
float RootSpineRadius = 0.0f);
/** Signed distance of the room + optional root connector. Negative means passage air. */
VOXELFORGE_API float VF_EvaluatePassageLandingSDF(
const FVector& Position,
const FVoxelPassageLanding& Landing);
/** True when a point belongs to the guaranteed solid floor slab of the landing/connector. */
VOXELFORGE_API bool VF_IsPassageLandingFloor(
const FVector& Position,
const FVoxelPassageLanding& Landing);
//=============================================================================
// HASH FUNCTIONS
//=============================================================================
@@ -176,7 +176,8 @@ public:
VF_FoldOp(H, *Entry.Op, VoxelBox, Ctx,
(Entry.Writes & VoxelOpChannels::Sdf) != 0);
// Do not early-out on a dead hypothesis: a later forcing structural post may
// deliberately overwrite it (the XY edge seal is appended after passage carving).
// deliberately overwrite it (the XY edge seal is appended after passage tube/landing
// carving and support).
}
return H.Resolve();
}
@@ -87,6 +87,28 @@ FORCEINLINE void VF_ApplyPassageCarving(float& Density, float ModSDF,
Density = FMath::Min(Density, FMath::Lerp(Density, AirTarget, CarveFactor));
}
// Landing air is also reasserted after the MC-space disturbance post. Unlike the legacy tube
// carve above, this threshold is independent of the incoming density: applying it a second time
// is therefore bit-stable. The structural passage op and the post-disturbance backstop can share
// the same operation without turning an otherwise unchanged live-vs-direct op-stack comparison
// into a second, deeper smooth carve. The tube intentionally retains its old current-density
// interpolation because that is part of the legacy structural path's contract.
FORCEINLINE void VF_ApplyPassageLandingCarving(float& Density, float LandingSDF,
float BaseDensity, float SealThickness)
{
constexpr float PASSAGE_BLEND_RADIUS = 4.0f;
if (LandingSDF >= PASSAGE_BLEND_RADIUS) return;
float CarveFactor = FMath::Clamp(
(PASSAGE_BLEND_RADIUS - LandingSDF) / (PASSAGE_BLEND_RADIUS * 2.0f),
0.0f, 1.0f);
CarveFactor = SmoothStep01(CarveFactor);
const float AirTarget = -(BaseDensity * 2.0f + SealThickness + 4.0f);
const float LandingThreshold = FMath::Lerp(BaseDensity, AirTarget, CarveFactor);
Density = FMath::Min(Density, LandingThreshold);
}
//=============================================================================
// SPINE DE DESCENTE (0,0) / (0,0) DESCENT SPINE
//=============================================================================
@@ -21,6 +21,7 @@
#include "VoxelStrateTypes.h"
#include "VoxelStrateDefinition.h"
#include "VoxelSeasonManifest.h"
#include "VoxelCaveMorphology.h"
#include "VoxelStrateManager.generated.h"
class UVoxelSettings;
@@ -84,6 +85,11 @@ struct FVoxelPassage
// ControlPoints, the uniform Radius is used as a fallback.
TArray<float> ControlRadii;
// Body-sized, flat-floored chambers at both tube ends. UpperPoint/LowerPoint remain the
// standing anchors returned by the source query; the tube enters each room at DoorPoint.
FVoxelPassageLanding UpperLanding;
FVoxelPassageLanding LowerLanding;
// 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.
@@ -348,6 +354,22 @@ public:
*/
float EvaluateModifierSDF(float WorldX, float WorldY, float WorldZ) const;
/** Apply the passage tube/landing carve and the guaranteed landing floor in internal density. */
void ApplyPassageModifier(float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const;
/** Reapply the landing-only air carve in internal density (used after visual disturbances). */
void ApplyPassageLandingAir(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 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;
/**
* 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
@@ -355,6 +377,9 @@ public:
*/
bool AnyPassageNearBox(const FVector& MinVoxel, const FVector& MaxVoxel) const;
/** 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;
/** Get all generated passages (for debug display). */
const TArray<FVoxelPassage>& GetPassages() const { return Passages; }