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