feat(gen): Jahni's playtest corrections -- rooms flattened, roads deleted
From walking a TunnelNetwork preset at (0,0). Four corrections, and two of
them were the same mistake.
2a. The (0,0) landing room is sized to host a FUTURE straight vertical
shaft from the surface, with flooring around the origin itself. The
room is the landing for a shaft the player digs later.
2b. The inter-strate descent no longer starts at the spine -- placed by
deterministic hash within a radius around (0,0). The spine room is for
the surface shaft; the descent is elsewhere.
2c. ⛔ DELETED the landing->hub connector roads from 413c15d. Jahni:
"its straight up making other tunnels straightly through everything...
they have nothing to do here." Zero references remain.
2d. ⭐ FLATTEN THE ROOM JOINS instead. Where two spherical rooms intersect
and leave a lip above head height, the join is brought down to a
walkable level.
⭐ 2c AND 2d ARE ONE CORRECTION. I solved "rooms do not connect" by
TUNNELLING BETWEEN THEM when the answer was MAKING THEIR FLOORS MEET. The
81-run sweep had already said so -- zero narrow-gap events, every failure a
vertical severance, renders showing "a sharp diagonal ledge descending into
black open space". A radial corridor to the origin could never have fixed
that; flattening the joins is aimed at exactly it.
2e. Descent tunnel 1.5x wider, per "1.5x bigger wouldn't be too much".
2f. Corridors WANDER. Jahni, from the renders: "its not a maze, its still a
latice made straight paths." The spanning tree changed WHICH edges
exist, not what an edge looks like -- every corridor was a straight
axis-aligned capsule, every junction 90 degrees. Now hash-jittered
chains, seam-safe like the tree.
RENDERER: 192x128 -> 512x288, and cameras are now seeded from the
player-fit set -- "a density-checked pose from the exact player-fit walk
mask". Previously they sat INSIDE ROCK looking at back-faces, which Jahni
spotted instantly and no metric could. First genuinely legible pictures of
this world.
ParallelFor landed at 6 sites in VoxelStrateMeasure before I stopped the
run; the rest of that work continues next.
⚠️ AND THE RENDERER IS ARCHITECTURALLY WRONG, per Jahni: "why, in engine,
does it take a minute to have clear view of thousands of angles, but in our
renderer it takes ages?" Because the engine MESHES ONCE and rasterises --
~32k density calls per chunk, then every angle is free. I RAYMARCH: tens of
millions of density calls PER IMAGE, nothing reused between frames. We
already have the mesher and an export mode. Next task renders from the
mesh.
Also from him: runs must stay under 30 minutes. This one hit 33 and
produced 2 images.
Build clean. Committed as a checkpoint -- the task was stopped mid-Part-3,
not finished.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
This commit is contained in:
1 parent
55bf807f60
commit
d97373c5a8
18 files changed
+2433
-1021
No files matched your search
@@ -141,21 +141,11 @@ namespace
|
||||
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;
|
||||
&& A.bSourcePlayerFit == B.bSourcePlayerFit;
|
||||
}
|
||||
|
||||
bool SamePassage(const FVoxelPassage& A, const FVoxelPassage& B, FString& OutMismatch,
|
||||
|
||||
@@ -492,7 +492,6 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
|
||||
int32 NumLandingEnds = 0;
|
||||
int32 NumLandingFloorsPassed = 0;
|
||||
int32 NumLandingFloorFailures = 0;
|
||||
int32 NumLandingNetworkJoins = 0;
|
||||
int32 NumLandingSealFailures = 0;
|
||||
int32 NumLandingFloorBoxes = 0;
|
||||
int32 NumLandingBoxProofs = 0;
|
||||
@@ -500,17 +499,12 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
|
||||
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;
|
||||
FWalkableTunnelAudit TunnelAudit;
|
||||
constexpr float LandingWalkableAngleDegrees =
|
||||
VoxelPassageGeometry::WalkableTunnelMaxGradientDegrees;
|
||||
constexpr float LandingWalkableSlope =
|
||||
VoxelPassageGeometry::WalkableTunnelMaxGradient;
|
||||
|
||||
for (const FVoxelPassage& Passage : Passages)
|
||||
{
|
||||
@@ -985,65 +979,10 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
|
||||
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
|
||||
? VoxelPassageGeometry::RootOverlapVoxels : 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
|
||||
? VoxelPassageGeometry::RootOverlapVoxels : 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;
|
||||
// Landing rooms are intentionally local now. Their flat floors are the only landing
|
||||
// contract; the removed radial root connectors were the source of the straight roads
|
||||
// through unrelated rooms.
|
||||
const bool bLandingSlopePassed = bFloorPassed;
|
||||
if (bLandingSlopePassed)
|
||||
{
|
||||
++NumLandingFloorsPassed;
|
||||
@@ -1076,16 +1015,6 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
++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
|
||||
@@ -1146,15 +1075,12 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
|
||||
|
||||
const FString LandingArchetype = ArchetypeName(Definition->GeneratorType);
|
||||
LandingSlopeReport += FString::Printf(
|
||||
TEXT("%s[%d:%s]=floor:%s(%.6f),connector:%s(%.6f)%s "),
|
||||
TEXT("%s[%d:%s]=flat-floor:%s(%.6f) "),
|
||||
*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(""));
|
||||
LandingGradient);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1237,18 +1163,12 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
|
||||
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."),
|
||||
TEXT("Landing geometry: %d/%d ends passed the final-density flat-floor test (worst gradient %.6f), source-fit anchors %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())
|
||||
@@ -1372,9 +1292,6 @@ 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
|
||||
|
||||
@@ -420,16 +420,23 @@ namespace
|
||||
for (int32 TunnelIndex = 0; TunnelIndex < Cache.Tunnels.Num(); ++TunnelIndex)
|
||||
{
|
||||
const FCachedTunnel& Tunnel = Cache.Tunnels[TunnelIndex];
|
||||
if (Tunnel.bHasMidpoint)
|
||||
if (Tunnel.ControlPoints.Num() >= 2
|
||||
&& Tunnel.ControlRadii.Num() == Tunnel.ControlPoints.Num())
|
||||
{
|
||||
VF_ConsiderTunnelSegment(
|
||||
World, Params, Core, Tunnel.EndpointA, Tunnel.Midpoint,
|
||||
Tunnel.RadiusA, Tunnel.RadiusMid,
|
||||
FString::Printf(TEXT("tunnel[%d].A-mid"), TunnelIndex), OutBest);
|
||||
VF_ConsiderTunnelSegment(
|
||||
World, Params, Core, Tunnel.Midpoint, Tunnel.EndpointB,
|
||||
Tunnel.RadiusMid, Tunnel.RadiusB,
|
||||
FString::Printf(TEXT("tunnel[%d].mid-B"), TunnelIndex), OutBest);
|
||||
for (int32 SegmentIndex = 0;
|
||||
SegmentIndex + 1 < Tunnel.ControlPoints.Num();
|
||||
++SegmentIndex)
|
||||
{
|
||||
VF_ConsiderTunnelSegment(
|
||||
World, Params, Core,
|
||||
Tunnel.ControlPoints[SegmentIndex],
|
||||
Tunnel.ControlPoints[SegmentIndex + 1],
|
||||
Tunnel.ControlRadii[SegmentIndex],
|
||||
Tunnel.ControlRadii[SegmentIndex + 1],
|
||||
FString::Printf(TEXT("tunnel[%d].segment%d"),
|
||||
TunnelIndex, SegmentIndex),
|
||||
OutBest);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -196,42 +196,6 @@ namespace
|
||||
return A + B;
|
||||
}
|
||||
|
||||
// Structural tree capsules must meet shaft axes without landing inside a ledge band. This is
|
||||
// the same pure helper as the generator path: one deterministic safe interval is shared by the
|
||||
// whole strate, so a link cannot be blocked when it crosses an unrelated shaft's ledge.
|
||||
FORCEINLINE float VF_SelectVerticalTreeConnectorZ(
|
||||
const FVerticalShaftParams& Params,
|
||||
float BottomZ,
|
||||
float TopZ,
|
||||
uint32 LinkHash)
|
||||
{
|
||||
if (Params.LedgeSpacing > 0.0f && Params.LedgeDepth > 0.0f)
|
||||
{
|
||||
const float Period = Params.LedgeSpacing;
|
||||
const float RelativeBottom = BottomZ - Params.StrateBottomWorldZ;
|
||||
const float RelativeTop = TopZ - Params.StrateBottomWorldZ;
|
||||
const int32 FirstPeriod = FMath::FloorToInt(RelativeBottom / Period);
|
||||
const float Random01 = VoxelHash::ToFloat01(VoxelHash::Mix(LinkHash));
|
||||
|
||||
for (int32 PeriodOffset = 0; PeriodOffset <= 1; ++PeriodOffset)
|
||||
{
|
||||
const float PeriodStart = static_cast<float>(FirstPeriod + PeriodOffset) * Period;
|
||||
const float SafeStart = FMath::Max(
|
||||
RelativeBottom, PeriodStart + Params.LedgeDepth + 0.01f);
|
||||
const float SafeEnd = FMath::Min(
|
||||
RelativeTop, PeriodStart + Period - Params.LedgeDepth - 0.01f);
|
||||
if (SafeEnd > SafeStart)
|
||||
{
|
||||
return Params.StrateBottomWorldZ
|
||||
+ FMath::Lerp(SafeStart, SafeEnd, Random01);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return FMath::Lerp(BottomZ, TopZ,
|
||||
VoxelHash::ToFloat01(VoxelHash::Mix(LinkHash)));
|
||||
}
|
||||
|
||||
//=========================================================================
|
||||
// RÔLE 1 — SOURCE : CHAMP CONSTANT / CONSTANT FIELD (roc ET vide)
|
||||
//=========================================================================
|
||||
@@ -1856,7 +1820,7 @@ namespace
|
||||
}
|
||||
// Keep the source and op-stack operation order identical: the origin room is carved
|
||||
// before the boundary seal, then its support floor is reasserted after the passage
|
||||
// post so no connector can remove the standing surface.
|
||||
// post so no passage-air post can remove the standing surface.
|
||||
VF_ApplyOriginLandingFloor(
|
||||
InOut.Density, X, Y, Z, TopZ, BotZ, Seal, Base, SpineRadius);
|
||||
}
|
||||
@@ -1925,7 +1889,13 @@ namespace
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
struct FShaft { float X, Y, R; int32 CellX, CellY; bool bOriginSpine; };
|
||||
struct FConn { FVector A, B; float Radius; };
|
||||
struct FConn
|
||||
{
|
||||
FVector A, B;
|
||||
float Radius = 0.0f;
|
||||
float FloorZ = 0.0f;
|
||||
bool bWalkableFloor = false;
|
||||
};
|
||||
|
||||
// ⚠️ DÉCLARÉE ICI, avant toute fonction qui la renvoie. Un type imbriqué doit exister au
|
||||
// moment où le COMPILATEUR lit la SIGNATURE — les corps de méthodes sont différés, pas les
|
||||
@@ -2178,17 +2148,26 @@ namespace
|
||||
|
||||
auto EmitTreeConnector = [&](const FShaft& Child, const FShaft* Parent)
|
||||
{
|
||||
const int32 ParentCellX = Parent != nullptr ? Parent->CellX : 0;
|
||||
const int32 ParentCellY = Parent != nullptr ? Parent->CellY : 0;
|
||||
const uint32 LinkHash = VoxelHash::Pair(
|
||||
Child.CellX, Child.CellY, ParentCellX, ParentCellY, Salt ^ TreeSalt);
|
||||
const float Zc = VF_SelectVerticalTreeConnectorZ(
|
||||
P, BottomZ, TopZ, LinkHash);
|
||||
const float Zc = VoxelPassageGeometry::VerticalShaftConnectorCenterZ(
|
||||
P.StrateBottomWorldZ,
|
||||
BottomZ,
|
||||
TopZ,
|
||||
P.LedgeSpacing,
|
||||
P.LedgeDepth,
|
||||
TreeRadius);
|
||||
const float FloorZ = VoxelPassageGeometry::VerticalShaftConnectorFloorZ(
|
||||
P.StrateBottomWorldZ,
|
||||
BottomZ,
|
||||
TopZ,
|
||||
P.LedgeSpacing,
|
||||
P.LedgeDepth,
|
||||
TreeRadius);
|
||||
const FVector ParentPoint = Parent != nullptr
|
||||
? FVector(Parent->X, Parent->Y, Zc)
|
||||
: FVector(0.0f, 0.0f, Zc);
|
||||
const FConn Conn{
|
||||
FVector(Child.X, Child.Y, Zc), ParentPoint, TreeRadius };
|
||||
FVector(Child.X, Child.Y, Zc), ParentPoint, TreeRadius,
|
||||
FloorZ, true };
|
||||
if (ConnectorMayReachCell(Conn))
|
||||
{
|
||||
Cache.Conns.Add(Conn);
|
||||
@@ -2310,7 +2289,8 @@ namespace
|
||||
const float ConnectorR = bSpineConnector
|
||||
? SpineConnectorRadius() : P.ConnectorRadius;
|
||||
const FConn Conn{
|
||||
FVector(A.X, A.Y, Zc), FVector(B.X, B.Y, Zc), ConnectorR };
|
||||
FVector(A.X, A.Y, Zc), FVector(B.X, B.Y, Zc), ConnectorR,
|
||||
0.0f, false };
|
||||
if (ConnectorMayReachCell(Conn))
|
||||
{
|
||||
Cache.Conns.Add(Conn);
|
||||
@@ -2404,6 +2384,109 @@ namespace
|
||||
const FShaftFieldSource* Field; // NON possédant : la pile possède la source
|
||||
};
|
||||
|
||||
// The deterministic shaft tree is a walkable network as well as an air topology. Reassert
|
||||
// its air core after partial shaft ledges, then add the same inset floor band as the native
|
||||
// path at the common first ledge level. This lets a player-fit route cross between shafts
|
||||
// without restoring the removed landing-to-origin roads.
|
||||
class FShaftConnectorAirMod final : public IVoxelDensityOp
|
||||
{
|
||||
public:
|
||||
explicit FShaftConnectorAirMod(const FShaftFieldSource* InField, float InBaseDensity)
|
||||
: Field(InField), BaseDensity(InBaseDensity) {}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; }
|
||||
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density; }
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::ShaftGeometry; }
|
||||
bool IsAdditive() const override { return false; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
void Eval(float WorldX, float WorldY, float WorldZ,
|
||||
FVoxelOpSample& InOut) const override
|
||||
{
|
||||
if (Field == nullptr) return;
|
||||
const FShaftFieldSource::FCells& Cells = Field->GetCellsAt(WorldX, WorldY);
|
||||
static thread_local bool bLoggedTemporaryShaftConnectors = false;
|
||||
if (!bLoggedTemporaryShaftConnectors
|
||||
&& FMath::IsNearlyEqual(WorldZ, -700.0f)
|
||||
&& WorldX > -180.0f && WorldX < -170.0f
|
||||
&& WorldY > -48.0f && WorldY < -38.0f)
|
||||
{
|
||||
bLoggedTemporaryShaftConnectors = true;
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeExplore][TemporaryShaftConnectors] query=(%.3f,%.3f,%.3f) count=%d"),
|
||||
WorldX, WorldY, WorldZ, Cells.Conns.Num());
|
||||
for (const FShaftFieldSource::FConn& DebugConn : Cells.Conns)
|
||||
{
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeExplore][TemporaryShaftConnectors] A=(%.3f,%.3f,%.3f) "
|
||||
"B=(%.3f,%.3f,%.3f) radius=%.3f floor=%.3f walk=%d"),
|
||||
DebugConn.A.X, DebugConn.A.Y, DebugConn.A.Z,
|
||||
DebugConn.B.X, DebugConn.B.Y, DebugConn.B.Z,
|
||||
DebugConn.Radius, DebugConn.FloorZ,
|
||||
DebugConn.bWalkableFloor ? 1 : 0);
|
||||
}
|
||||
}
|
||||
for (const FShaftFieldSource::FConn& Conn : Cells.Conns)
|
||||
{
|
||||
if (!Conn.bWalkableFloor) continue;
|
||||
VoxelPassageGeometry::VF_ApplyVerticalShaftConnectorAir(
|
||||
InOut.Density, WorldX, WorldY, WorldZ,
|
||||
Conn.A, Conn.B, Conn.Radius, Conn.FloorZ, BaseDensity);
|
||||
}
|
||||
}
|
||||
|
||||
EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override
|
||||
{
|
||||
return Field != nullptr ? EVoxelOpEffect::Both : EVoxelOpEffect::Identity;
|
||||
}
|
||||
|
||||
const TCHAR* DebugName() const override { return TEXT("ShaftConnectorAirOp"); }
|
||||
|
||||
private:
|
||||
const FShaftFieldSource* Field = nullptr; // NON possédant : la pile possède la source
|
||||
float BaseDensity = 8.0f;
|
||||
};
|
||||
|
||||
class FShaftConnectorFloorMod final : public IVoxelDensityOp
|
||||
{
|
||||
public:
|
||||
FShaftConnectorFloorMod(const FShaftFieldSource* InField, float InBaseDensity)
|
||||
: Field(InField), BaseDensity(InBaseDensity) {}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; }
|
||||
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density; }
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::ShaftGeometry; }
|
||||
bool IsAdditive() const override { return false; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
void Eval(float WorldX, float WorldY, float WorldZ,
|
||||
FVoxelOpSample& InOut) const override
|
||||
{
|
||||
if (Field == nullptr) return;
|
||||
const FShaftFieldSource::FCells& Cells = Field->GetCellsAt(WorldX, WorldY);
|
||||
for (const FShaftFieldSource::FConn& Conn : Cells.Conns)
|
||||
{
|
||||
if (!Conn.bWalkableFloor) continue;
|
||||
VoxelPassageGeometry::VF_ApplyVerticalShaftConnectorFloor(
|
||||
InOut.Density, WorldX, WorldY, WorldZ,
|
||||
Conn.A, Conn.B, Conn.Radius, Conn.FloorZ, BaseDensity);
|
||||
}
|
||||
}
|
||||
|
||||
EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override
|
||||
{
|
||||
return Field != nullptr ? EVoxelOpEffect::FillOnly : EVoxelOpEffect::Identity;
|
||||
}
|
||||
|
||||
const TCHAR* DebugName() const override { return TEXT("ShaftConnectorFloorOp"); }
|
||||
|
||||
private:
|
||||
const FShaftFieldSource* Field = nullptr; // NON possédant : la pile possède la source
|
||||
float BaseDensity = 8.0f;
|
||||
};
|
||||
|
||||
//=========================================================================
|
||||
// RÔLE 1 — SOURCE : ÎLES FLOTTANTES / FLOATING ISLAND BLOBS
|
||||
//=========================================================================
|
||||
@@ -2774,6 +2857,12 @@ namespace
|
||||
/** La salle de SDF minimal pour le dernier voxel évalué. -1 = aucune. */
|
||||
int32 NearestRoom = -1;
|
||||
|
||||
/** The warped SDF-space position belonging to the current voxel. */
|
||||
FVector LastWarpedPosition = FVector::ZeroVector;
|
||||
|
||||
/** The unwarped position belonging to the current voxel. */
|
||||
FVector LastWorldPosition = FVector::ZeroVector;
|
||||
|
||||
/** ÉTAPE C1 — les params de la strate avec l'op de CETTE salle appliqué par-dessus.
|
||||
* Mémo par voxel : invalidé au début de chaque `Eval`, calculé au PREMIER modificateur
|
||||
* qui le demande. C'est ce qui reproduit le coût de l'original (une copie de struct par
|
||||
@@ -2795,6 +2884,18 @@ namespace
|
||||
* Lu par les arches, les dômes, le pincement et le biais de sol. */
|
||||
int32 GetNearestRoomIdx() const { return State().NearestRoom; }
|
||||
|
||||
bool HasTunnelAirGeometry() const
|
||||
{
|
||||
return P.RoomDensity > 0.0f && P.RoomSpacing > 0.0f;
|
||||
}
|
||||
|
||||
float GetTunnelCoreSDF() const
|
||||
{
|
||||
const FVector& Position = State().LastWorldPosition;
|
||||
return VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF(
|
||||
Position.X, Position.Y, Position.Z, State().Cache);
|
||||
}
|
||||
|
||||
/**
|
||||
* ÉTAPE C1 — L'OVERRIDE D'OP PAR SALLE. Les params de la strate avec l'op de terrain tiré
|
||||
* pour la salle la plus proche appliqué par-dessus. **ONZE des douze modificateurs de détail
|
||||
@@ -2875,6 +2976,7 @@ namespace
|
||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
{
|
||||
FState& S = State();
|
||||
S.LastWorldPosition = FVector(WorldX, WorldY, WorldZ);
|
||||
|
||||
// ⚠️ REMIS À -1 INCONDITIONNELLEMENT, ce que l'original ne fait pas : chez lui
|
||||
// `NearestRoomIdx` est un `thread_local` qui, quand `RoomDensity <= 0`, garde la valeur
|
||||
@@ -2919,6 +3021,7 @@ namespace
|
||||
WorldY * WF + 9.7f,
|
||||
EffectiveZ * WF + VoxelHash::SeedOffset(SeedU, 0.41f))) * VOXEL_NOISE_SCALE * WS;
|
||||
}
|
||||
S.LastWarpedPosition = FVector(WarpedX, WarpedY, WarpedZ);
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// LE CACHE PAR BOÎTE DE RECHERCHE
|
||||
@@ -3388,37 +3491,61 @@ namespace
|
||||
|
||||
for (const FCachedTunnel& Tunnel : B.Cache.Tunnels)
|
||||
{
|
||||
const float MaxRadius = FMath::Max3(
|
||||
FMath::Abs(Tunnel.RadiusA), FMath::Abs(Tunnel.RadiusB),
|
||||
Tunnel.bHasMidpoint ? FMath::Abs(Tunnel.RadiusMid) : 0.0f);
|
||||
const FVector SegmentAMin(
|
||||
FMath::Min(Tunnel.EndpointA.X, Tunnel.bHasMidpoint ? Tunnel.Midpoint.X : Tunnel.EndpointB.X),
|
||||
FMath::Min(Tunnel.EndpointA.Y, Tunnel.bHasMidpoint ? Tunnel.Midpoint.Y : Tunnel.EndpointB.Y),
|
||||
FMath::Min(Tunnel.EndpointA.Z, Tunnel.bHasMidpoint ? Tunnel.Midpoint.Z : Tunnel.EndpointB.Z));
|
||||
const FVector SegmentAMax(
|
||||
FMath::Max(Tunnel.EndpointA.X, Tunnel.bHasMidpoint ? Tunnel.Midpoint.X : Tunnel.EndpointB.X),
|
||||
FMath::Max(Tunnel.EndpointA.Y, Tunnel.bHasMidpoint ? Tunnel.Midpoint.Y : Tunnel.EndpointB.Y),
|
||||
FMath::Max(Tunnel.EndpointA.Z, Tunnel.bHasMidpoint ? Tunnel.Midpoint.Z : Tunnel.EndpointB.Z));
|
||||
|
||||
float TunnelLower = VF_DistanceBetweenBoxes(
|
||||
QMin, QMax, SegmentAMin, SegmentAMax) - MaxRadius;
|
||||
if (Tunnel.bHasMidpoint)
|
||||
float MaxRadius = FMath::Max(
|
||||
FMath::Abs(Tunnel.RadiusA), FMath::Abs(Tunnel.RadiusB));
|
||||
float TunnelLower = FLT_MAX;
|
||||
if (Tunnel.ControlPoints.Num() >= 2
|
||||
&& Tunnel.ControlRadii.Num() == Tunnel.ControlPoints.Num())
|
||||
{
|
||||
const FVector SegmentBMin(
|
||||
FMath::Min(Tunnel.Midpoint.X, Tunnel.EndpointB.X),
|
||||
FMath::Min(Tunnel.Midpoint.Y, Tunnel.EndpointB.Y),
|
||||
FMath::Min(Tunnel.Midpoint.Z, Tunnel.EndpointB.Z));
|
||||
const FVector SegmentBMax(
|
||||
FMath::Max(Tunnel.Midpoint.X, Tunnel.EndpointB.X),
|
||||
FMath::Max(Tunnel.Midpoint.Y, Tunnel.EndpointB.Y),
|
||||
FMath::Max(Tunnel.Midpoint.Z, Tunnel.EndpointB.Z));
|
||||
TunnelLower = FMath::Min(
|
||||
TunnelLower,
|
||||
VF_DistanceBetweenBoxes(QMin, QMax, SegmentBMin, SegmentBMax) - MaxRadius);
|
||||
for (int32 SegmentIndex = 0;
|
||||
SegmentIndex + 1 < Tunnel.ControlPoints.Num();
|
||||
++SegmentIndex)
|
||||
{
|
||||
const FVector& A = Tunnel.ControlPoints[SegmentIndex];
|
||||
const FVector& BPoint = Tunnel.ControlPoints[SegmentIndex + 1];
|
||||
MaxRadius = FMath::Max(
|
||||
MaxRadius,
|
||||
FMath::Max(FMath::Abs(Tunnel.ControlRadii[SegmentIndex]),
|
||||
FMath::Abs(Tunnel.ControlRadii[SegmentIndex + 1])));
|
||||
const FVector SegmentMin(
|
||||
FMath::Min(A.X, BPoint.X),
|
||||
FMath::Min(A.Y, BPoint.Y),
|
||||
FMath::Min(A.Z, BPoint.Z));
|
||||
const FVector SegmentMax(
|
||||
FMath::Max(A.X, BPoint.X),
|
||||
FMath::Max(A.Y, BPoint.Y),
|
||||
FMath::Max(A.Z, BPoint.Z));
|
||||
TunnelLower = FMath::Min(
|
||||
TunnelLower,
|
||||
VF_DistanceBetweenBoxes(QMin, QMax, SegmentMin, SegmentMax));
|
||||
}
|
||||
TunnelLower -= MaxRadius;
|
||||
}
|
||||
else
|
||||
{
|
||||
const FVector SegmentMin(
|
||||
FMath::Min(Tunnel.EndpointA.X, Tunnel.EndpointB.X),
|
||||
FMath::Min(Tunnel.EndpointA.Y, Tunnel.EndpointB.Y),
|
||||
FMath::Min(Tunnel.EndpointA.Z, Tunnel.EndpointB.Z));
|
||||
const FVector SegmentMax(
|
||||
FMath::Max(Tunnel.EndpointA.X, Tunnel.EndpointB.X),
|
||||
FMath::Max(Tunnel.EndpointA.Y, Tunnel.EndpointB.Y),
|
||||
FMath::Max(Tunnel.EndpointA.Z, Tunnel.EndpointB.Z));
|
||||
TunnelLower = VF_DistanceBetweenBoxes(
|
||||
QMin, QMax, SegmentMin, SegmentMax) - MaxRadius;
|
||||
}
|
||||
|
||||
const FVector NoWarpSegmentMin(
|
||||
FMath::Min(Tunnel.EndpointA.X, Tunnel.EndpointB.X),
|
||||
FMath::Min(Tunnel.EndpointA.Y, Tunnel.EndpointB.Y),
|
||||
FMath::Min(Tunnel.EndpointA.Z, Tunnel.EndpointB.Z));
|
||||
const FVector NoWarpSegmentMax(
|
||||
FMath::Max(Tunnel.EndpointA.X, Tunnel.EndpointB.X),
|
||||
FMath::Max(Tunnel.EndpointA.Y, Tunnel.EndpointB.Y),
|
||||
FMath::Max(Tunnel.EndpointA.Z, Tunnel.EndpointB.Z));
|
||||
const float TunnelLowerNoWarp = VF_DistanceBetweenBoxes(
|
||||
NoWarpMin, NoWarpMax, SegmentAMin, SegmentAMax) - MaxRadius;
|
||||
NoWarpMin, NoWarpMax, NoWarpSegmentMin, NoWarpSegmentMax)
|
||||
- MaxRadius;
|
||||
Consider(TunnelLower);
|
||||
CountThreshold(TunnelLower, B.HitTunnels);
|
||||
CountThreshold(TunnelLowerNoWarp, B.HitTunnelsNoWarp);
|
||||
@@ -3482,6 +3609,13 @@ namespace
|
||||
|
||||
const TCHAR* DebugName() const override { return TEXT("RoomGraphSource"); }
|
||||
|
||||
bool IsTunnelSupportFloor() const
|
||||
{
|
||||
const FVector& Position = State().LastWorldPosition;
|
||||
return VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
|
||||
Position.X, Position.Y, Position.Z, State().Cache);
|
||||
}
|
||||
|
||||
private:
|
||||
FStrateGenerationParams P;
|
||||
int32 Seed;
|
||||
@@ -3491,6 +3625,99 @@ namespace
|
||||
uint32 LayoutVersion = 0;
|
||||
};
|
||||
|
||||
// The graph tunnel floor closes the rounded bottom of the core into a finite, walkable support
|
||||
// slab. It is separate from the inter-strate passage floor: this owns only the wandering
|
||||
// intra-strate graph edges and does not create any new corridor.
|
||||
class FCaveTunnelFloorOp final : public IVoxelDensityOp
|
||||
{
|
||||
public:
|
||||
FCaveTunnelFloorOp(const FRoomGraphSource* InRooms, float InBase)
|
||||
: Rooms(InRooms), Base(InBase)
|
||||
{}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::StructuralPost; }
|
||||
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::None; }
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||
EVoxelOpResourceMask RequiredResources() const override
|
||||
{
|
||||
return VoxelOpResources::RoomGeometry;
|
||||
}
|
||||
bool IsAdditive() const override { return false; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
void Eval(float, float, float, FVoxelOpSample& InOut) const override
|
||||
{
|
||||
if (Rooms != nullptr && Rooms->IsTunnelSupportFloor())
|
||||
{
|
||||
InOut.Density = FMath::Max(
|
||||
InOut.Density,
|
||||
FMath::Max(Base * 2.0f, 1.0f));
|
||||
}
|
||||
}
|
||||
|
||||
EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override
|
||||
{
|
||||
return Rooms != nullptr && Rooms->HasTunnelAirGeometry()
|
||||
? EVoxelOpEffect::Both
|
||||
: EVoxelOpEffect::Identity;
|
||||
}
|
||||
|
||||
const TCHAR* DebugName() const override { return TEXT("CaveTunnelFloorOp"); }
|
||||
|
||||
private:
|
||||
const FRoomGraphSource* Rooms = nullptr;
|
||||
float Base = 8.0f;
|
||||
};
|
||||
|
||||
// A graph tunnel's raw SDF is the structural air contract for the wandering chain. It runs
|
||||
// after the passage post because a different passage can project a solid support floor onto
|
||||
// the same voxel; the core keeps its own half-voxel support band and restores the route above
|
||||
// it. FRoomGraphSource evaluates this post against the cached world-space chain, so the cave
|
||||
// warp cannot turn a walkable floor ramp into a vertical break.
|
||||
class FCaveTunnelAirOp final : public IVoxelDensityOp
|
||||
{
|
||||
public:
|
||||
FCaveTunnelAirOp(const FRoomGraphSource* InRooms, float InBase, float InInset)
|
||||
: Rooms(InRooms), Base(InBase), Inset(InInset)
|
||||
{}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::StructuralPost; }
|
||||
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density; }
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||
EVoxelOpResourceMask RequiredResources() const override
|
||||
{
|
||||
return VoxelOpResources::RoomGeometry;
|
||||
}
|
||||
bool IsAdditive() const override { return false; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
void Eval(float, float, float, FVoxelOpSample& InOut) const override
|
||||
{
|
||||
if (Rooms != nullptr
|
||||
&& !Rooms->IsTunnelSupportFloor()
|
||||
&& Rooms->GetTunnelCoreSDF() < -Inset)
|
||||
{
|
||||
InOut.Density = FMath::Min(
|
||||
InOut.Density,
|
||||
-FMath::Max(Base * 2.0f, 1.0f));
|
||||
}
|
||||
}
|
||||
|
||||
EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override
|
||||
{
|
||||
return Rooms != nullptr && Rooms->HasTunnelAirGeometry()
|
||||
? EVoxelOpEffect::CarveOnly
|
||||
: EVoxelOpEffect::Identity;
|
||||
}
|
||||
|
||||
const TCHAR* DebugName() const override { return TEXT("CaveTunnelAirOp"); }
|
||||
|
||||
private:
|
||||
const FRoomGraphSource* Rooms = nullptr;
|
||||
float Base = 8.0f;
|
||||
float Inset = 2.0f;
|
||||
};
|
||||
|
||||
//=========================================================================
|
||||
// LE GATE DE BOÎTE DES MODIFICATEURS DE CAVE
|
||||
//=========================================================================
|
||||
@@ -5215,7 +5442,8 @@ bool FVoxelOpStack::ValidateChannelOrder(FString* OutError) const
|
||||
|
||||
void FVoxelOpStack::AppendStructuralPost(float StrateTopWorldZ, float StrateBottomWorldZ,
|
||||
float SealThickness, float BaseDensity, float SpineRadius,
|
||||
const UVoxelStrateManager* StrateManager)
|
||||
const UVoxelStrateManager* StrateManager,
|
||||
bool bAppendEdgeSeal)
|
||||
{
|
||||
// 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 (tube + landing + support) percent
|
||||
@@ -5226,7 +5454,10 @@ void FVoxelOpStack::AppendStructuralPost(float StrateTopWorldZ, float StrateBott
|
||||
Add(MakeUnique<FBoundarySealOp>(StrateTopWorldZ, StrateBottomWorldZ, SealThickness, BaseDensity));
|
||||
Add(MakeUnique<FPassageCarveOp>(StrateManager, BaseDensity, SealThickness,
|
||||
StrateTopWorldZ, StrateBottomWorldZ, SpineRadius));
|
||||
Add(MakeUnique<FXYEdgeSealOp>(BaseDensity));
|
||||
if (bAppendEdgeSeal)
|
||||
{
|
||||
Add(MakeUnique<FXYEdgeSealOp>(BaseDensity));
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
@@ -5466,6 +5697,8 @@ namespace VoxelDensityOps
|
||||
OutStack.Add(MakeSdfRoughnessMod(P.SurfaceRoughness, 0.1f, 3, P.SurfaceRoughness + 4.0f));
|
||||
OutStack.Add(MakeSdfCarve(CarveBlend, P.BaseDensity));
|
||||
OutStack.Add(MakeUnique<FShaftLedgeMod>(P, ShaftPtr));
|
||||
OutStack.Add(MakeUnique<FShaftConnectorAirMod>(ShaftPtr, P.BaseDensity));
|
||||
OutStack.Add(MakeUnique<FShaftConnectorFloorMod>(ShaftPtr, P.BaseDensity));
|
||||
|
||||
if (bAppendStructuralPosts)
|
||||
{
|
||||
@@ -5552,7 +5785,12 @@ namespace VoxelDensityOps
|
||||
{
|
||||
OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ,
|
||||
P.BoundarySealThickness, P.BaseDensity,
|
||||
SpineRadius, StrateManager);
|
||||
SpineRadius, StrateManager, false);
|
||||
OutStack.Add(MakeUnique<FCaveTunnelFloorOp>(RoomPtr, P.BaseDensity));
|
||||
OutStack.Add(MakeUnique<FCaveTunnelAirOp>(
|
||||
RoomPtr, P.BaseDensity,
|
||||
VoxelPassageGeometry::CaveTunnelAirCoreInsetVoxels));
|
||||
OutStack.Add(MakeUnique<FXYEdgeSealOp>(P.BaseDensity));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -230,44 +230,6 @@ static float CellularNoise3D(const FVector& Position)
|
||||
return VoxelNoise::Cellular3D(Position);
|
||||
}
|
||||
|
||||
// Structural tree capsules must meet shaft axes without landing inside a ledge band. A single
|
||||
// deterministic safe interval is enough for the whole tree: every shaft in one strate shares the
|
||||
// same ledge phase, and keeping every tree link in that interval also keeps crossings of unrelated
|
||||
// shafts out of their additive ledges. The fallback is only for malformed settings with no safe
|
||||
// interval; the stock params always take the proven branch.
|
||||
static float VF_SelectVerticalTreeConnectorZ(
|
||||
const FVerticalShaftParams& Params,
|
||||
float BottomZ,
|
||||
float TopZ,
|
||||
uint32 LinkHash)
|
||||
{
|
||||
if (Params.LedgeSpacing > 0.0f && Params.LedgeDepth > 0.0f)
|
||||
{
|
||||
const float Period = Params.LedgeSpacing;
|
||||
const float RelativeBottom = BottomZ - Params.StrateBottomWorldZ;
|
||||
const float RelativeTop = TopZ - Params.StrateBottomWorldZ;
|
||||
const int32 FirstPeriod = FMath::FloorToInt(RelativeBottom / Period);
|
||||
const float Random01 = VoxelHash::ToFloat01(VoxelHash::Mix(LinkHash));
|
||||
|
||||
for (int32 PeriodOffset = 0; PeriodOffset <= 1; ++PeriodOffset)
|
||||
{
|
||||
const float PeriodStart = static_cast<float>(FirstPeriod + PeriodOffset) * Period;
|
||||
const float SafeStart = FMath::Max(
|
||||
RelativeBottom, PeriodStart + Params.LedgeDepth + 0.01f);
|
||||
const float SafeEnd = FMath::Min(
|
||||
RelativeTop, PeriodStart + Period - Params.LedgeDepth - 0.01f);
|
||||
if (SafeEnd > SafeStart)
|
||||
{
|
||||
return Params.StrateBottomWorldZ
|
||||
+ FMath::Lerp(SafeStart, SafeEnd, Random01);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return FMath::Lerp(BottomZ, TopZ,
|
||||
VoxelHash::ToFloat01(VoxelHash::Mix(LinkHash)));
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// DENSITY PIPELINE HELPERS (partagés entre TunnelNetwork et Slab)
|
||||
//=============================================================================
|
||||
@@ -309,7 +271,7 @@ static FORCEINLINE void ApplyOriginLandingFloor(float& Density,
|
||||
// it works uniformly for every generator type. Stays inside the seal bands so it can
|
||||
// never breach a strate boundary. All features are hash-placed and deterministic.
|
||||
static void ApplyDisturbances(float& MC, float X, float Y, float Z,
|
||||
const FStrateDisturbanceParams& D, uint32 Seed)
|
||||
const FStrateDisturbanceParams& D, uint32 Seed, bool bProtectVerticalShaftAir)
|
||||
{
|
||||
const float InnerTop = D.StrateTopWorldZ - D.BoundarySealThickness;
|
||||
const float InnerBot = D.StrateBottomWorldZ + D.BoundarySealThickness;
|
||||
@@ -418,7 +380,10 @@ static void ApplyDisturbances(float& MC, float X, float Y, float Z,
|
||||
{
|
||||
float f = FMath::Clamp((Blend - sdf) / (Blend * 2.0f), 0.0f, 1.0f);
|
||||
f = SmoothStep01(f);
|
||||
MC = FMath::Min(MC, -f * Solid); // force solid
|
||||
if (!bProtectVerticalShaftAir)
|
||||
{
|
||||
MC = FMath::Min(MC, -f * Solid); // force solid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -465,7 +430,10 @@ static void ApplyDisturbances(float& MC, float X, float Y, float Z,
|
||||
f = SmoothStep01(f) * zFade;
|
||||
best = FMath::Max(best, f);
|
||||
}
|
||||
if (best > 0.0f) MC = FMath::Min(MC, -best * Solid); // force solid
|
||||
if (best > 0.0f && !bProtectVerticalShaftAir)
|
||||
{
|
||||
MC = FMath::Min(MC, -best * Solid); // force solid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -498,6 +466,57 @@ namespace
|
||||
// address. Relaxed ordering is sufficient: this allocates uniqueness, it publishes no data.
|
||||
std::atomic<uint64> GNextDensityCacheOwnerId { 0 };
|
||||
|
||||
struct FTunnelCoreCacheState
|
||||
{
|
||||
FChunkSDFCache Cache;
|
||||
uint64 OwnerId = 0;
|
||||
FIntVector Chunk = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX);
|
||||
uint32 ParamsFingerprint = 0xFFFFFFFFu;
|
||||
uint32 LayoutVersion = 0xFFFFFFFFu;
|
||||
bool bValid = false;
|
||||
};
|
||||
|
||||
static thread_local FTunnelCoreCacheState GTunnelCoreCache;
|
||||
void PrepareTunnelCoreCache(
|
||||
const UVoxelStrateManager& Manager,
|
||||
const FIntVector& ChunkCoord,
|
||||
const FStrateGenerationParams& Params,
|
||||
uint32 ParamsFingerprint,
|
||||
uint32 LayoutVersion,
|
||||
uint64 OwnerId,
|
||||
FTunnelCoreCacheState& OutState)
|
||||
{
|
||||
if (OutState.bValid
|
||||
&& OutState.OwnerId == OwnerId
|
||||
&& OutState.Chunk == ChunkCoord
|
||||
&& OutState.ParamsFingerprint == ParamsFingerprint
|
||||
&& OutState.LayoutVersion == LayoutVersion)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const float ChunkMinX = static_cast<float>(ChunkCoord.X * CHUNK_SIZE);
|
||||
const float ChunkMinY = static_cast<float>(ChunkCoord.Y * CHUNK_SIZE);
|
||||
const float ChunkMaxX = ChunkMinX + static_cast<float>(CHUNK_SIZE);
|
||||
const float ChunkMaxY = ChunkMinY + static_cast<float>(CHUNK_SIZE);
|
||||
const float Expansion = FMath::Max(Params.CaveWarpStrength, 0.0f) + 2.0f;
|
||||
const int32 StrateIndex = Manager.GetStrateIndex(
|
||||
(static_cast<float>(ChunkCoord.Z) + 0.5f)
|
||||
* static_cast<float>(CHUNK_SIZE) * VOXEL_SIZE);
|
||||
|
||||
VoxelCaveMorphology::BuildChunkCache(
|
||||
OutState.Cache,
|
||||
ChunkMinX - Expansion, ChunkMinY - Expansion,
|
||||
ChunkMaxX + Expansion, ChunkMaxY + Expansion,
|
||||
Params, static_cast<uint32>(Manager.GetWorldSeed()),
|
||||
StrateIndex, nullptr);
|
||||
OutState.OwnerId = OwnerId;
|
||||
OutState.Chunk = ChunkCoord;
|
||||
OutState.ParamsFingerprint = ParamsFingerprint;
|
||||
OutState.LayoutVersion = LayoutVersion;
|
||||
OutState.bValid = true;
|
||||
}
|
||||
|
||||
struct FVoxelStackParamRefs
|
||||
{
|
||||
const FSlabGenerationParams* Slab = nullptr;
|
||||
@@ -673,6 +692,10 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
// Query per-chunk params from the manager so each strate has different caves.
|
||||
float Result;
|
||||
|
||||
// A vertical tree connector may need to stay open through the shared disturbance and
|
||||
// structural-post tail below. This is one bit for this density call, not cached voxel data.
|
||||
VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker();
|
||||
|
||||
FIntVector ChunkCoord(
|
||||
FMath::FloorToInt(WorldX / CHUNK_SIZE),
|
||||
FMath::FloorToInt(WorldY / CHUNK_SIZE),
|
||||
@@ -831,6 +854,23 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
// silently fall back to the old one-archetype switch.
|
||||
CP_UseOpStack = CP_UseOpStack || CP_UseComposerRegions;
|
||||
#endif
|
||||
bool bUseNativeTunnelCore = !CP_UseCustomRecipe
|
||||
&& (CP_GenType == ECaveGeneratorType::TunnelNetwork
|
||||
|| CP_GenType == ECaveGeneratorType::Underwater);
|
||||
#if WITH_EDITOR
|
||||
bUseNativeTunnelCore = bUseNativeTunnelCore && !CP_UseComposerRegions;
|
||||
#endif
|
||||
if (bUseNativeTunnelCore)
|
||||
{
|
||||
PrepareTunnelCoreCache(
|
||||
*StrateManager, ChunkCoord, CP_Tunnel, CP_TunnelFP,
|
||||
LayoutVersion, DensityCacheOwnerId, GTunnelCoreCache);
|
||||
}
|
||||
else
|
||||
{
|
||||
GTunnelCoreCache.bValid = false;
|
||||
}
|
||||
|
||||
if (CP_UseOpStack)
|
||||
{
|
||||
CP_OpStack = FVoxelOpStack(); // move-assign : libère l'ancienne pile
|
||||
@@ -997,8 +1037,13 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
CP_TunnelFP, LayoutVersion); break;
|
||||
}
|
||||
|
||||
// Disturbance layer (the "wow" post-process) — cached params, MC convention.
|
||||
ApplyDisturbances(Result, WorldX, WorldY, WorldZ, CP_Dist, (uint32)Seed);
|
||||
// Disturbance layer (the "wow" post-process) — cached params, MC convention. The
|
||||
// vertical shaft tree opens a walkable route before this shared pass, so a bridge/ridge
|
||||
// must not refill it. Both native and operator-stack paths use the same marker helper.
|
||||
const bool bProtectVerticalShaftAir =
|
||||
VoxelPassageGeometry::VerticalShaftConnectorAirMarker();
|
||||
ApplyDisturbances(Result, WorldX, WorldY, WorldZ, CP_Dist, (uint32)Seed,
|
||||
bProtectVerticalShaftAir);
|
||||
|
||||
// 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
|
||||
@@ -1103,6 +1148,45 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
|
||||
StrateManager->ApplyPassageLandingRoomFloorMC(
|
||||
Result, WorldX, WorldY, WorldZ, LandingBaseDensity);
|
||||
|
||||
// Disturbance features are authored as a generic MC-space post and may add a ridge or
|
||||
// bridge over a graph tunnel. Reassert the native cached tunnel core here, after every
|
||||
// solid floor writer but before the global XY seal. This cache is built once per chunk,
|
||||
// never once per voxel.
|
||||
if (GTunnelCoreCache.bValid)
|
||||
{
|
||||
const bool bTunnelSupportFloor =
|
||||
VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
|
||||
WorldX, WorldY, WorldZ, GTunnelCoreCache.Cache);
|
||||
if (bTunnelSupportFloor)
|
||||
{
|
||||
Result = FMath::Min(
|
||||
Result,
|
||||
-FMath::Max(LandingBaseDensity * 2.0f, 1.0f));
|
||||
}
|
||||
const float CoreSDF = VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF(
|
||||
WorldX, WorldY, WorldZ, GTunnelCoreCache.Cache);
|
||||
if (!bTunnelSupportFloor
|
||||
&& CoreSDF < -VoxelPassageGeometry::CaveTunnelAirCoreInsetVoxels)
|
||||
{
|
||||
Result = FMath::Max(
|
||||
Result,
|
||||
FMath::Max(LandingBaseDensity * 2.0f, 1.0f));
|
||||
}
|
||||
|
||||
// The graph tunnel can overlap an inter-strate landing at a room mouth. Its air
|
||||
// backstop is allowed to reopen the tunnel, but the landing's proved support floor
|
||||
// must own the final floor band; otherwise the graph post can erase the only support
|
||||
// surface at the mouth and leave the player-fit graph with a disconnected pocket.
|
||||
VF_ApplyOriginLandingFloorMC(
|
||||
Result, WorldX, WorldY, WorldZ,
|
||||
CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ,
|
||||
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
|
||||
StrateManager->ApplyPassageLandingFloorMC(
|
||||
Result, WorldX, WorldY, WorldZ, LandingBaseDensity);
|
||||
StrateManager->ApplyPassageLandingRoomFloorMC(
|
||||
Result, WorldX, WorldY, WorldZ, LandingBaseDensity);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1164,6 +1248,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
}
|
||||
}
|
||||
|
||||
VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker();
|
||||
return Result;
|
||||
}
|
||||
|
||||
@@ -2262,6 +2347,28 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
|
||||
Density, WorldX, WorldY, WorldZ,
|
||||
Params.BaseDensity, Params.BoundarySealThickness);
|
||||
}
|
||||
|
||||
// Terrain operations and passage support floors are allowed to write solid density, but the
|
||||
// graph's own tunnel floor/core are structural route geometry. Close the rounded bottom first,
|
||||
// then reopen only the core above its half-voxel air clearance. Both predicates are shared
|
||||
// with the operator stack and are evaluated from the per-chunk cache, never per-voxel rebuilt.
|
||||
const bool bTunnelSupportFloor =
|
||||
VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
|
||||
WorldX, WorldY, WorldZ, SDFCache);
|
||||
if (bTunnelSupportFloor)
|
||||
{
|
||||
Density = FMath::Max(Density, FMath::Max(Params.BaseDensity * 2.0f, 1.0f));
|
||||
}
|
||||
const float TunnelCoreSDF = (Params.RoomDensity > 0.0f && Params.RoomSpacing > 0.0f)
|
||||
? VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF(WorldX, WorldY, WorldZ, SDFCache)
|
||||
: FLT_MAX;
|
||||
if (!bTunnelSupportFloor
|
||||
&& TunnelCoreSDF < -VoxelPassageGeometry::CaveTunnelAirCoreInsetVoxels)
|
||||
{
|
||||
Density = FMath::Min(
|
||||
Density,
|
||||
-FMath::Max(Params.BaseDensity * 2.0f, 1.0f));
|
||||
}
|
||||
ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ,
|
||||
Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
|
||||
Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius);
|
||||
@@ -4271,7 +4378,13 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
|
||||
// them. That matters: a capsule from a child to a parent two or three cells away must be
|
||||
// present in the cells along its segment, not only in the cache centred on the child.
|
||||
struct FLocalShaft { float X, Y, R; int32 CellX, CellY; bool bOriginSpine; };
|
||||
struct FLocalConn { FVector A, B; float Radius; };
|
||||
struct FLocalConn
|
||||
{
|
||||
FVector A, B;
|
||||
float Radius = 0.0f;
|
||||
float FloorZ = 0.0f;
|
||||
bool bWalkableFloor = false;
|
||||
};
|
||||
thread_local TArray<FLocalShaft, TInlineAllocator<10>> Shafts;
|
||||
thread_local TArray<FLocalConn, TInlineAllocator<32>> Conns;
|
||||
thread_local TArray<FLocalShaft, TInlineAllocator<81>> TreeEmitShafts;
|
||||
@@ -4396,17 +4509,26 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
|
||||
|
||||
auto EmitTreeConnector = [&](const FLocalShaft& Child, const FLocalShaft* Parent)
|
||||
{
|
||||
const int32 ParentCellX = Parent != nullptr ? Parent->CellX : 0;
|
||||
const int32 ParentCellY = Parent != nullptr ? Parent->CellY : 0;
|
||||
const uint32 LinkHash = VoxelHash::Pair(
|
||||
Child.CellX, Child.CellY, ParentCellX, ParentCellY, S ^ TreeSalt);
|
||||
const float Zc = VF_SelectVerticalTreeConnectorZ(
|
||||
Params, BottomZ, TopZ, LinkHash);
|
||||
const float Zc = VoxelPassageGeometry::VerticalShaftConnectorCenterZ(
|
||||
Params.StrateBottomWorldZ,
|
||||
BottomZ,
|
||||
TopZ,
|
||||
Params.LedgeSpacing,
|
||||
Params.LedgeDepth,
|
||||
TreeConnectorRadius);
|
||||
const float FloorZ = VoxelPassageGeometry::VerticalShaftConnectorFloorZ(
|
||||
Params.StrateBottomWorldZ,
|
||||
BottomZ,
|
||||
TopZ,
|
||||
Params.LedgeSpacing,
|
||||
Params.LedgeDepth,
|
||||
TreeConnectorRadius);
|
||||
const FVector ParentPoint = Parent != nullptr
|
||||
? FVector(Parent->X, Parent->Y, Zc)
|
||||
: FVector(0.0f, 0.0f, Zc);
|
||||
const FLocalConn Conn{
|
||||
FVector(Child.X, Child.Y, Zc), ParentPoint, TreeConnectorRadius };
|
||||
FVector(Child.X, Child.Y, Zc), ParentPoint, TreeConnectorRadius,
|
||||
FloorZ, true };
|
||||
if (ConnectorMayReachCell(Conn))
|
||||
{
|
||||
Conns.Add(Conn);
|
||||
@@ -4531,7 +4653,8 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
|
||||
RoughnessReach + 1.0f)
|
||||
: Params.ConnectorRadius;
|
||||
const FLocalConn Conn{
|
||||
FVector(A.X, A.Y, Zc), FVector(B.X, B.Y, Zc), ConnectorR };
|
||||
FVector(A.X, A.Y, Zc), FVector(B.X, B.Y, Zc), ConnectorR,
|
||||
0.0f, false };
|
||||
if (ConnectorMayReachCell(Conn))
|
||||
{
|
||||
Conns.Add(Conn);
|
||||
@@ -4598,6 +4721,29 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
|
||||
}
|
||||
}
|
||||
|
||||
// The drainage tree is a walkable shaft network, not only a connected air SDF. Reassert its
|
||||
// air core after partial shaft ledges, then emit support bands at the common first ledge level
|
||||
// so every parent edge can be traversed by the player-fit stencil. This remains local to
|
||||
// shaft edges; no landing-to-origin road is added.
|
||||
for (const FLocalConn& C : Conns)
|
||||
{
|
||||
if (C.bWalkableFloor)
|
||||
{
|
||||
VoxelPassageGeometry::VF_ApplyVerticalShaftConnectorAir(
|
||||
Density, WorldX, WorldY, WorldZ,
|
||||
C.A, C.B, C.Radius, C.FloorZ, Params.BaseDensity);
|
||||
}
|
||||
}
|
||||
for (const FLocalConn& C : Conns)
|
||||
{
|
||||
if (C.bWalkableFloor)
|
||||
{
|
||||
VoxelPassageGeometry::VF_ApplyVerticalShaftConnectorFloor(
|
||||
Density, WorldX, WorldY, WorldZ,
|
||||
C.A, C.B, C.Radius, C.FloorZ, Params.BaseDensity);
|
||||
}
|
||||
}
|
||||
|
||||
ApplyOriginSpine(Density, WorldX, WorldY, WorldZ,
|
||||
Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
|
||||
Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius);
|
||||
|
||||
@@ -269,9 +269,10 @@ static bool VF_IsAnyPassageFloorAt(
|
||||
continue;
|
||||
}
|
||||
const FVoxelPassage& Passage = Passages[PassageIndex];
|
||||
if (VF_IsPassageLandingFloor(Position, Passage.UpperLanding)
|
||||
if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker()
|
||||
&& (VF_IsPassageLandingFloor(Position, Passage.UpperLanding)
|
||||
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding)
|
||||
|| VF_IsWalkableTunnelFloor(Passage, Position))
|
||||
|| VF_IsWalkableTunnelFloor(Passage, Position)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -826,8 +827,6 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
int32 TotalPassages = 0;
|
||||
int32 NumAimedAtUpperPlayerFit = 0;
|
||||
int32 NumAimedAtLowerPlayerFit = 0;
|
||||
int32 NumUpperRootConnectors = 0;
|
||||
int32 NumLowerRootConnectors = 0;
|
||||
TSet<FString> NoQueryArchetypes;
|
||||
|
||||
const auto ArchetypeName = [](ECaveGeneratorType Archetype)
|
||||
@@ -915,39 +914,6 @@ 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. The
|
||||
// root is the origin landing room's floor, not a hidden continuous shaft and not a
|
||||
// passage-order-dependent midpoint. Reusing the same top-anchored geometry here makes
|
||||
// every connector meet the (0,0) room at its actual support level.
|
||||
const VoxelPassageGeometry::FOriginLandingGeometry OriginLanding =
|
||||
VoxelPassageGeometry::BuildOriginLandingGeometry(
|
||||
StrateTopZ, StrateBottomZ, BoundarySealThickness, OriginSpineRadius);
|
||||
if (OriginLanding.bValid)
|
||||
{
|
||||
return OriginLanding.FloorZ;
|
||||
}
|
||||
|
||||
// Malformed tiny strates still fail closed. This fallback is only for the descriptor
|
||||
// builder's diagnostic path; normal authored strates always satisfy the room interval.
|
||||
const float Seal = FMath::Max(BoundarySealThickness, 0.0f);
|
||||
const float InnerBottom = StrateBottomZ + Seal;
|
||||
const float InnerTop = StrateTopZ - Seal;
|
||||
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
|
||||
@@ -974,6 +940,17 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
|
||||
const float DistLo = FMath::Min(Cfg.DistanceMin, Cfg.DistanceMax);
|
||||
const float DistHi = FMath::Max(Cfg.DistanceMin, Cfg.DistanceMax);
|
||||
// The origin room owns the future straight shaft. Keep the progression mouth outside its
|
||||
// carve/blend envelope, then choose a deterministic point in the authored annulus. If an
|
||||
// asset has an invalid inner radius, repair only that radius; the outer authored radius is
|
||||
// preserved unless it is too small to contain the required inner edge.
|
||||
const float OriginLandingHalfWidth =
|
||||
VoxelPassageGeometry::LandingHalfWidthForRadius(OriginSpineRadius);
|
||||
const float MinimumDescentDistance = FMath::Max(
|
||||
OriginLandingHalfWidth + VoxelPassageGeometry::LandingCarveBlendVoxels,
|
||||
VoxelPassageGeometry::MinimumTurnFloorWidthVoxels);
|
||||
const float PlacementLo = FMath::Max(DistLo, MinimumDescentDistance);
|
||||
const float PlacementHi = FMath::Max(DistHi, PlacementLo);
|
||||
|
||||
const int32 Conns = FMath::Max(0, Cfg.Connections);
|
||||
for (int32 c = 0; c < Conns; c++)
|
||||
@@ -1010,9 +987,11 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
break;
|
||||
}
|
||||
|
||||
// PLACEMENT: random angle, distance from the (0,0) spine within config range.
|
||||
// PLACEMENT: random angle, distance from the (0,0) spine within a deterministic
|
||||
// annulus. It is deliberately not the spine: that room is reserved for the future
|
||||
// surface shaft, while this passage opens a separate room in the strate network.
|
||||
const float Angle = PassageRandom01(PassageSaltAngle) * (2.0f * PI);
|
||||
const float Distance = PassageRandomRange(DistLo, DistHi, PassageSaltDistance);
|
||||
const float Distance = PassageRandomRange(PlacementLo, PlacementHi, PassageSaltDistance);
|
||||
const float PX = FMath::Cos(Angle) * Distance;
|
||||
const float PY = FMath::Sin(Angle) * Distance;
|
||||
|
||||
@@ -1049,7 +1028,8 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
PX,
|
||||
PY,
|
||||
MaxLateralSnapFor(*UpperDef),
|
||||
SuggestedUpperPoint);
|
||||
SuggestedUpperPoint,
|
||||
CachedSeed);
|
||||
|
||||
if (bAimedUpperAtPlayerFitPoint)
|
||||
{
|
||||
@@ -1088,7 +1068,8 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
PX,
|
||||
PY,
|
||||
MaxLateralSnap,
|
||||
SuggestedLowerPoint);
|
||||
SuggestedLowerPoint,
|
||||
CachedSeed);
|
||||
|
||||
if (bAimedLowerAtPlayerFitPoint)
|
||||
{
|
||||
@@ -1289,10 +1270,9 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
: 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.
|
||||
// A source-fit answer is the landing itself. Keep it local to the requested room: the
|
||||
// (0,0) landing is reserved for the future straight shaft, so no radial connector is
|
||||
// synthesized here.
|
||||
Passage.UpperLanding = VF_BuildPassageLanding(
|
||||
FVector(UpperX, UpperY, TopZ),
|
||||
Cfg.MouthRadius,
|
||||
@@ -1300,13 +1280,7 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
UpperTopZ,
|
||||
UpperBottomZ,
|
||||
UpperDef ? BoundarySealThicknessFor(*UpperDef) : 0.0f,
|
||||
bAimedUpperAtPlayerFitPoint,
|
||||
/*bHasNetworkConnector=*/true,
|
||||
FVector(0.0f, 0.0f,
|
||||
CommonRootFloorFor(
|
||||
UpperTopZ, UpperBottomZ,
|
||||
UpperDef ? BoundarySealThicknessFor(*UpperDef) : 0.0f)),
|
||||
OriginSpineRadius);
|
||||
bAimedUpperAtPlayerFitPoint);
|
||||
Passage.LowerLanding = VF_BuildPassageLanding(
|
||||
FVector(LowerX, LowerY, BottomZ),
|
||||
Cfg.MouthRadius,
|
||||
@@ -1314,17 +1288,9 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
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);
|
||||
bAimedLowerAtPlayerFitPoint);
|
||||
Passage.UpperPoint = Passage.UpperLanding.StandingPoint;
|
||||
Passage.LowerPoint = Passage.LowerLanding.StandingPoint;
|
||||
if (Passage.UpperLanding.bHasNetworkConnector) ++NumUpperRootConnectors;
|
||||
if (Passage.LowerLanding.bHasNetworkConnector) ++NumLowerRootConnectors;
|
||||
|
||||
if (Cfg.Style == EVoxelPassageStyle::Straight
|
||||
&& Passage.UpperLanding.HalfWidth > 0.0f
|
||||
@@ -1554,14 +1520,11 @@ 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 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.
|
||||
// Bounding sphere over the tube, both rooms, and both floor slabs (+ 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 BoundsMin(FLT_MAX, FLT_MAX, FLT_MAX);
|
||||
FVector BoundsMax(-FLT_MAX, -FLT_MAX, -FLT_MAX);
|
||||
@@ -1590,31 +1553,6 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
+ 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);
|
||||
@@ -1646,13 +1584,11 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Log,
|
||||
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)."),
|
||||
TEXT("[StrateManager] Passage landings: upper %d/%d and lower %d/%d source-fit; %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);
|
||||
@@ -1686,7 +1622,9 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
// Keep the legacy type: this is the explicitly opted-in above-ground vertical opening,
|
||||
// not the default inter-strate descent style.
|
||||
Entry.PassageType = EVoxelPassageType::VerticalShaft;
|
||||
Entry.Radius = FMath::Max(OriginSpineRadius * 0.7f, 4.0f);
|
||||
// Match the reserved future shaft envelope exactly. The entry is only opened through
|
||||
// the top seal into the finite landing room; lower strate seals remain untouched.
|
||||
Entry.Radius = FMath::Max(OriginSpineRadius, 1.0f);
|
||||
Entry.UpperPoint = FVector(0.0f, 0.0f, TopZ + CHUNK_SIZE);
|
||||
// The capsule's lower tangent meets the room ceiling; it never bores through the
|
||||
// room's floor or creates an origin column in strate 0.
|
||||
@@ -1779,8 +1717,8 @@ float UVoxelStrateManager::EvaluateModifierSDF(float WorldX, float WorldY, float
|
||||
}
|
||||
|
||||
// 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.
|
||||
// tube endpoint. It is the same fixed SDF geometry selected during GeneratePassages; it
|
||||
// never performs a source query here and does not synthesize a radial root connector.
|
||||
const float UpperLandingSDF = VF_EvaluatePassageLandingSDF(Pos, P.UpperLanding);
|
||||
const float LowerLandingSDF = VF_EvaluatePassageLandingSDF(Pos, P.LowerLanding);
|
||||
MinSDF = VoxelSDF::SmoothMin(MinSDF, UpperLandingSDF, BlendK);
|
||||
@@ -1807,9 +1745,10 @@ void UVoxelStrateManager::ApplyPassageModifier(
|
||||
for (const int32 PassageIndex : Nearby)
|
||||
{
|
||||
const FVoxelPassage& Passage = Passages[PassageIndex];
|
||||
if (VF_IsPassageLandingFloor(Position, Passage.UpperLanding)
|
||||
if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker()
|
||||
&& (VF_IsPassageLandingFloor(Position, Passage.UpperLanding)
|
||||
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding)
|
||||
|| VF_IsWalkableTunnelFloor(Passage, Position))
|
||||
|| VF_IsWalkableTunnelFloor(Passage, Position)))
|
||||
{
|
||||
// This is the one bidirectional part of PassageCarveOp: a floor is a proved solid
|
||||
// support slab. It is deliberately applied after the air carve so a tube can never
|
||||
@@ -1928,9 +1867,10 @@ void UVoxelStrateManager::ApplyPassageLandingFloorMC(
|
||||
for (const int32 PassageIndex : Nearby)
|
||||
{
|
||||
const FVoxelPassage& Passage = Passages[PassageIndex];
|
||||
if (VF_IsPassageLandingFloor(Position, Passage.UpperLanding)
|
||||
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding)
|
||||
|| VF_IsWalkableTunnelFloor(Passage, Position))
|
||||
if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker()
|
||||
&& (VF_IsPassageLandingFloor(Position, Passage.UpperLanding)
|
||||
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding)
|
||||
|| VF_IsWalkableTunnelFloor(Passage, Position)))
|
||||
{
|
||||
// Result is in MC convention here (negative = solid). This reassertion is the
|
||||
// structural floor backstop after the optional MC-space disturbance layer.
|
||||
@@ -1971,8 +1911,9 @@ void UVoxelStrateManager::ApplyPassageLandingRoomFloorMC(
|
||||
for (const int32 PassageIndex : Nearby)
|
||||
{
|
||||
const FVoxelPassage& Passage = Passages[PassageIndex];
|
||||
if (VF_IsPassageRoomFloor(Position, Passage.UpperLanding)
|
||||
|| VF_IsPassageRoomFloor(Position, Passage.LowerLanding))
|
||||
if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker()
|
||||
&& (VF_IsPassageRoomFloor(Position, Passage.UpperLanding)
|
||||
|| VF_IsPassageRoomFloor(Position, Passage.LowerLanding)))
|
||||
{
|
||||
Density = FMath::Min(Density, -BaseDensity);
|
||||
break;
|
||||
@@ -2030,50 +1971,6 @@ bool UVoxelStrateManager::AnyPassageLandingFloorNearBox(
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// The default tube also owns a three-voxel support band. Its conservative segment AABB
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "VoxelGenerator.h"
|
||||
#include "VoxelStrateManager.h"
|
||||
#include "VoxelTypes.h"
|
||||
#include "Async/ParallelFor.h"
|
||||
|
||||
namespace VoxelStrateMeasurePrivate
|
||||
{
|
||||
@@ -388,21 +389,26 @@ namespace VoxelStrateMeasurePrivate
|
||||
OutGrid.Density.SetNumUninitialized(OutGrid.CellCount);
|
||||
}
|
||||
|
||||
bool bSawNonFiniteDensity = false;
|
||||
int32 CellIndex = 0;
|
||||
for (int32 Z = 0; Z < OutGrid.NumZ; ++Z)
|
||||
// Each Z slab writes a disjoint contiguous region. GetDensityAt is a const, read-only
|
||||
// query used concurrently by the mesher, and custom samplers are required to provide
|
||||
// the same const/thread-safe contract. The non-finite flag is also per-cell so the
|
||||
// diagnostic reduction below never depends on task completion order.
|
||||
TArray<uint8> NonFiniteFlags;
|
||||
NonFiniteFlags.Init(0u, OutGrid.CellCount);
|
||||
ParallelFor(OutGrid.NumZ, [&](int32 Z)
|
||||
{
|
||||
for (int32 Y = 0; Y < OutGrid.NumY; ++Y)
|
||||
{
|
||||
for (int32 X = 0; X < OutGrid.NumX; ++X)
|
||||
{
|
||||
const int32 CellIndex = OutGrid.Index(X, Y, Z);
|
||||
const FVector Sample = OutGrid.CellCenter(X, Y, Z);
|
||||
const float Density = Sampler != nullptr
|
||||
? Sampler->SampleDensity(Sample.X, Sample.Y, Sample.Z)
|
||||
: Generator->GetDensityAt(Sample.X, Sample.Y, Sample.Z);
|
||||
if (!FMath::IsFinite(Density))
|
||||
{
|
||||
bSawNonFiniteDensity = true;
|
||||
NonFiniteFlags[CellIndex] = 1u;
|
||||
}
|
||||
|
||||
// MC convention in this codebase: negative is solid, positive is air.
|
||||
@@ -410,11 +416,16 @@ namespace VoxelStrateMeasurePrivate
|
||||
{
|
||||
OutGrid.Density[CellIndex] = Density;
|
||||
}
|
||||
OutGrid.Air[CellIndex++] = Density > 0.0f ? 1u : 0u;
|
||||
OutGrid.Air[CellIndex] = Density > 0.0f ? 1u : 0u;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
bool bSawNonFiniteDensity = false;
|
||||
for (int32 CellIndex = 0; CellIndex < OutGrid.CellCount; ++CellIndex)
|
||||
{
|
||||
bSawNonFiniteDensity |= NonFiniteFlags[CellIndex] != 0u;
|
||||
}
|
||||
if (bSawNonFiniteDensity)
|
||||
{
|
||||
return Refuse(OutReason, TEXT("At least one density sample was non-finite."));
|
||||
@@ -552,54 +563,104 @@ namespace VoxelStrateMeasurePrivate
|
||||
|
||||
void BuildManhattanDistanceToSolid(const FSampleGrid& Grid, TArray<int32>& OutDistances)
|
||||
{
|
||||
// Two raster passes with 6-neighbour Manhattan relaxation. The result is an approximation
|
||||
// of distance-to-solid in the original field: each coarse grid unit is SampleStep VOXELS.
|
||||
// Two wavefront passes with 6-neighbour Manhattan relaxation. A cell depends only on the
|
||||
// immediately preceding diagonal in the forward pass (or following diagonal in the
|
||||
// reverse pass), so each diagonal is safe to process in parallel while the diagonal
|
||||
// barriers preserve the exact raster result. Each coarse grid unit is SampleStep VOXELS.
|
||||
constexpr int32 Infinity = INT32_MAX;
|
||||
OutDistances.SetNumUninitialized(Grid.CellCount);
|
||||
for (int32 Index = 0; Index < Grid.CellCount; ++Index)
|
||||
ParallelFor(Grid.CellCount, [&](int32 Index)
|
||||
{
|
||||
OutDistances[Index] = Grid.Air[Index] == 0u ? 0 : Infinity;
|
||||
}
|
||||
});
|
||||
|
||||
auto Relax = [&](int32 Current, int32 Neighbor)
|
||||
const int32 LastDiagonal = Grid.NumX + Grid.NumY + Grid.NumZ - 3;
|
||||
const auto ProcessDiagonal = [&](int32 Diagonal, bool bForward)
|
||||
{
|
||||
if (OutDistances[Neighbor] == Infinity)
|
||||
const int32 MinZ = FMath::Max(
|
||||
0, Diagonal - (Grid.NumX - 1) - (Grid.NumY - 1));
|
||||
const int32 MaxZ = FMath::Min(Grid.NumZ - 1, Diagonal);
|
||||
if (MinZ > MaxZ)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const int32 Candidate = OutDistances[Neighbor] == INT32_MAX - 1
|
||||
? Infinity : OutDistances[Neighbor] + 1;
|
||||
OutDistances[Current] = FMath::Min(OutDistances[Current], Candidate);
|
||||
|
||||
int32 DiagonalCellCount = 0;
|
||||
for (int32 Z = MinZ; Z <= MaxZ; ++Z)
|
||||
{
|
||||
const int32 MinY = FMath::Max(
|
||||
0, Diagonal - Z - (Grid.NumX - 1));
|
||||
const int32 MaxY = FMath::Min(Grid.NumY - 1, Diagonal - Z);
|
||||
DiagonalCellCount += FMath::Max(0, MaxY - MinY + 1);
|
||||
}
|
||||
|
||||
const auto ProcessZ = [&](int32 Z)
|
||||
{
|
||||
const int32 MinY = FMath::Max(
|
||||
0, Diagonal - Z - (Grid.NumX - 1));
|
||||
const int32 MaxY = FMath::Min(Grid.NumY - 1, Diagonal - Z);
|
||||
for (int32 Y = MinY; Y <= MaxY; ++Y)
|
||||
{
|
||||
const int32 X = Diagonal - Z - Y;
|
||||
const int32 Current = Grid.Index(X, Y, Z);
|
||||
if (Grid.Air[Current] == 0u)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto Relax = [&](int32 Neighbor)
|
||||
{
|
||||
if (OutDistances[Neighbor] == Infinity)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const int32 Candidate = OutDistances[Neighbor] == INT32_MAX - 1
|
||||
? Infinity : OutDistances[Neighbor] + 1;
|
||||
OutDistances[Current] = FMath::Min(
|
||||
OutDistances[Current], Candidate);
|
||||
};
|
||||
|
||||
if (bForward)
|
||||
{
|
||||
if (X > 0) Relax(Grid.Index(X - 1, Y, Z));
|
||||
if (Y > 0) Relax(Grid.Index(X, Y - 1, Z));
|
||||
if (Z > 0) Relax(Grid.Index(X, Y, Z - 1));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (X + 1 < Grid.NumX) Relax(Grid.Index(X + 1, Y, Z));
|
||||
if (Y + 1 < Grid.NumY) Relax(Grid.Index(X, Y + 1, Z));
|
||||
if (Z + 1 < Grid.NumZ) Relax(Grid.Index(X, Y, Z + 1));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Tiny edge diagonals are cheaper to execute directly; substantial diagonals get
|
||||
// one task per Z row, with every row writing a disjoint set of cells.
|
||||
const int32 ZCount = MaxZ - MinZ + 1;
|
||||
if (ZCount > 1 && DiagonalCellCount >= 256)
|
||||
{
|
||||
ParallelFor(ZCount, [&](int32 ZOffset)
|
||||
{
|
||||
ProcessZ(MinZ + ZOffset);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int32 Z = MinZ; Z <= MaxZ; ++Z)
|
||||
{
|
||||
ProcessZ(Z);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (int32 Z = 0; Z < Grid.NumZ; ++Z)
|
||||
for (int32 Diagonal = 0; Diagonal <= LastDiagonal; ++Diagonal)
|
||||
{
|
||||
for (int32 Y = 0; Y < Grid.NumY; ++Y)
|
||||
{
|
||||
for (int32 X = 0; X < Grid.NumX; ++X)
|
||||
{
|
||||
const int32 Current = Grid.Index(X, Y, Z);
|
||||
if (Grid.Air[Current] == 0u) continue;
|
||||
if (X > 0) Relax(Current, Grid.Index(X - 1, Y, Z));
|
||||
if (Y > 0) Relax(Current, Grid.Index(X, Y - 1, Z));
|
||||
if (Z > 0) Relax(Current, Grid.Index(X, Y, Z - 1));
|
||||
}
|
||||
}
|
||||
ProcessDiagonal(Diagonal, true);
|
||||
}
|
||||
|
||||
for (int32 Z = Grid.NumZ - 1; Z >= 0; --Z)
|
||||
for (int32 Diagonal = LastDiagonal; Diagonal >= 0; --Diagonal)
|
||||
{
|
||||
for (int32 Y = Grid.NumY - 1; Y >= 0; --Y)
|
||||
{
|
||||
for (int32 X = Grid.NumX - 1; X >= 0; --X)
|
||||
{
|
||||
const int32 Current = Grid.Index(X, Y, Z);
|
||||
if (Grid.Air[Current] == 0u) continue;
|
||||
if (X + 1 < Grid.NumX) Relax(Current, Grid.Index(X + 1, Y, Z));
|
||||
if (Y + 1 < Grid.NumY) Relax(Current, Grid.Index(X, Y + 1, Z));
|
||||
if (Z + 1 < Grid.NumZ) Relax(Current, Grid.Index(X, Y, Z + 1));
|
||||
}
|
||||
}
|
||||
ProcessDiagonal(Diagonal, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1092,14 +1153,23 @@ namespace VoxelStrateMeasurePrivate
|
||||
}
|
||||
|
||||
OutPlayerFit.Init(0u, Grid.CellCount);
|
||||
TArray<float> SupportHeights;
|
||||
SupportHeights.SetNumUninitialized(SupportOffsets.Num());
|
||||
TArray<int32> SupportAnchorZs;
|
||||
SupportAnchorZs.SetNumUninitialized(SupportOffsets.Num());
|
||||
TArray<uint8> bSupported;
|
||||
bSupported.Init(0u, SupportOffsets.Num());
|
||||
for (int32 Z = 1; Z < Grid.NumZ; ++Z)
|
||||
TArray<int64> PlayerFitCounts;
|
||||
PlayerFitCounts.Init(0, Grid.NumZ);
|
||||
|
||||
// A candidate pose only reads the immutable sampled grid and writes its own mask cell.
|
||||
// Give each Z slab private stencil scratch, then reduce the per-slab counts in Z order
|
||||
// below. This removes the old shared scratch/count race without changing any predicate.
|
||||
ParallelFor(FMath::Max(Grid.NumZ - 1, 0), [&](int32 SliceIndex)
|
||||
{
|
||||
const int32 Z = SliceIndex + 1;
|
||||
TArray<float> SupportHeights;
|
||||
SupportHeights.SetNumUninitialized(SupportOffsets.Num());
|
||||
TArray<int32> SupportAnchorZs;
|
||||
SupportAnchorZs.SetNumUninitialized(SupportOffsets.Num());
|
||||
TArray<uint8> bSupported;
|
||||
bSupported.Init(0u, SupportOffsets.Num());
|
||||
int64 NumPlayerFitInSlice = 0;
|
||||
|
||||
for (int32 Y = 0; Y < Grid.NumY; ++Y)
|
||||
{
|
||||
for (int32 X = 0; X < Grid.NumX; ++X)
|
||||
@@ -1188,14 +1258,18 @@ namespace VoxelStrateMeasurePrivate
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (OutPlayerFit[Current] != 0u)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
OutPlayerFit[Current] = 1u;
|
||||
++OutNumPlayerFitCells;
|
||||
++NumPlayerFitInSlice;
|
||||
}
|
||||
}
|
||||
PlayerFitCounts[Z] = NumPlayerFitInSlice;
|
||||
});
|
||||
|
||||
// Fixed-index reduction is intentional: even though the terms are integral, keeping the
|
||||
// reduction serial documents the determinism rule shared by the floating metrics below.
|
||||
for (int32 Z = 1; Z < Grid.NumZ; ++Z)
|
||||
{
|
||||
OutNumPlayerFitCells += PlayerFitCounts[Z];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1211,11 +1285,16 @@ namespace VoxelStrateMeasurePrivate
|
||||
|
||||
const int32 PlaneCells = Grid.NumX * Grid.NumY;
|
||||
constexpr int32 Infinity = INT32_MAX / 4;
|
||||
TArray<int32> Distances;
|
||||
Distances.SetNumUninitialized(PlaneCells);
|
||||
int32 MinimumDistance = Infinity;
|
||||
for (int32 Z = 0; Z < Grid.NumZ; ++Z)
|
||||
TArray<int32> PerSliceMinimum;
|
||||
PerSliceMinimum.Init(Infinity, Grid.NumZ);
|
||||
|
||||
// Each horizontal distance transform is independent. Keep its mutable raster in the
|
||||
// worker and publish one integer result per Z slice; the final minimum is reduced in
|
||||
// ascending Z order for an explicit deterministic reduction.
|
||||
ParallelFor(Grid.NumZ, [&](int32 Z)
|
||||
{
|
||||
TArray<int32> Distances;
|
||||
Distances.SetNumUninitialized(PlaneCells);
|
||||
for (int32 Y = 0; Y < Grid.NumY; ++Y)
|
||||
{
|
||||
for (int32 X = 0; X < Grid.NumX; ++X)
|
||||
@@ -1269,6 +1348,7 @@ namespace VoxelStrateMeasurePrivate
|
||||
}
|
||||
}
|
||||
|
||||
int32 SliceMinimum = Infinity;
|
||||
for (int32 Y = 0; Y < Grid.NumY; ++Y)
|
||||
{
|
||||
for (int32 X = 0; X < Grid.NumX; ++X)
|
||||
@@ -1276,11 +1356,18 @@ namespace VoxelStrateMeasurePrivate
|
||||
const int32 Cell = Grid.Index(X, Y, Z);
|
||||
if (PlayerFit[Cell] != 0u)
|
||||
{
|
||||
MinimumDistance = FMath::Min(
|
||||
MinimumDistance, Distances[X + Grid.NumX * Y]);
|
||||
SliceMinimum = FMath::Min(
|
||||
SliceMinimum, Distances[X + Grid.NumX * Y]);
|
||||
}
|
||||
}
|
||||
}
|
||||
PerSliceMinimum[Z] = SliceMinimum;
|
||||
});
|
||||
|
||||
int32 MinimumDistance = Infinity;
|
||||
for (int32 Z = 0; Z < Grid.NumZ; ++Z)
|
||||
{
|
||||
MinimumDistance = FMath::Min(MinimumDistance, PerSliceMinimum[Z]);
|
||||
}
|
||||
|
||||
return MinimumDistance == Infinity
|
||||
|
||||
Reference in new issue
Block a user