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
@@ -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
|
||||
//=============================================================================
|
||||
|
||||
Reference in new issue
Block a user