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
|
||||
|
||||
@@ -10,17 +10,17 @@
|
||||
// infinite worlds without storing anything.
|
||||
//
|
||||
// ROOMS:
|
||||
// - Origin room: guaranteed large room at (0,0) per strate — the hub
|
||||
// - Origin room: guaranteed large room at (0,0) per strate — the future shaft landing
|
||||
// - Hash rooms: ellipsoids, rounded boxes, or elongated capsules
|
||||
// - All blended with smooth-min for organic junctions
|
||||
//
|
||||
// TUNNELS:
|
||||
// - Connect pairs of rooms based on distance and probability
|
||||
// - Tapered (variable min/max radius at each endpoint)
|
||||
// - Curved (midpoint displaced perpendicular to tunnel direction)
|
||||
// - Curved wandering chains with deterministic perpendicular control-point jitter
|
||||
// - Horizontal bias (vertical connections penalized)
|
||||
// - Endpoint Z offset (tunnels enter rooms at different heights)
|
||||
// - Origin room forces connections to all nearby rooms
|
||||
// - Floor-aligned endpoints and variable radii preserve walkable room joins
|
||||
// - The origin room is not joined to landings by radial connector roads
|
||||
//
|
||||
// PIPELINE POSITION:
|
||||
// Step 3 (cave warp) bends the SDF query coordinates before we evaluate.
|
||||
@@ -48,14 +48,10 @@ enum class ECaveGeneratorType : uint8;
|
||||
* `FloorZ = StandingPoint.Z - 0.5`. The chamber is deliberately separate from the tube endpoint
|
||||
* so a sloped/vertical tube never presents the player with a hole as its first standing position.
|
||||
*
|
||||
* A source query answer selects the landing's XY/Z anchor. Because that pure query does not
|
||||
* flood-fill the live network, every inter-strate end also gets a deterministic root connector
|
||||
* (`bHasNetworkConnector`). Its flat floor ramps to the same common interior root level for that
|
||||
* strate, so both mouths enter the finite landing room at (0,0) even when their authored floors
|
||||
* are at opposite sides of the strate. `RootSpineRadius` supplies the small endpoint overlap that
|
||||
* joins the connector to that room; it does not create a vertical shaft. If the direct run would
|
||||
* be steeper than the named 15 degree tunnel contract, the connector uses a deterministic level
|
||||
* dog-leg before its final ramp; the bend is geometry, not a per-voxel search.
|
||||
* A source query answer selects the landing's XY/Z anchor. The landing is deliberately local to
|
||||
* that answer: it is a flat-floored room where the inter-strate tube meets a legal player-fit
|
||||
* pose. The (0,0) landing is the only place reserved for the future straight surface shaft;
|
||||
* progression mouths do not receive hidden radial roads to that spine room.
|
||||
*/
|
||||
struct VOXELFORGE_API FVoxelPassageLanding
|
||||
{
|
||||
@@ -63,23 +59,12 @@ struct VOXELFORGE_API FVoxelPassageLanding
|
||||
FVector DoorPoint = FVector::ZeroVector;
|
||||
FVector DoorDirection = FVector(1.0f, 0.0f, 0.0f);
|
||||
|
||||
FVector ConnectorStart = FVector::ZeroVector;
|
||||
FVector ConnectorControl = FVector::ZeroVector;
|
||||
FVector ConnectorEnd = FVector::ZeroVector;
|
||||
|
||||
float FloorZ = 0.0f;
|
||||
float CeilingZ = 0.0f;
|
||||
float HalfWidth = 0.0f;
|
||||
float FloorThickness = 3.0f;
|
||||
float ConnectorRadius = 0.0f;
|
||||
float ConnectorCeilingZ = 0.0f;
|
||||
float RootFloorZ = 0.0f;
|
||||
float RootCeilingZ = 0.0f;
|
||||
float RootSpineRadius = 0.0f;
|
||||
|
||||
bool bSourcePlayerFit = false;
|
||||
bool bHasNetworkConnector = false;
|
||||
bool bHasConnectorBend = false;
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
@@ -202,17 +187,14 @@ VOXELFORGE_API FVoxelPassageLanding VF_BuildPassageLanding(
|
||||
float StrateTopZ,
|
||||
float StrateBottomZ,
|
||||
float BoundarySealThickness,
|
||||
bool bSourcePlayerFit,
|
||||
bool bHasNetworkConnector,
|
||||
const FVector& NetworkPoint,
|
||||
float RootSpineRadius = 0.0f);
|
||||
bool bSourcePlayerFit);
|
||||
|
||||
/** Signed distance of the room + optional root connector. Negative means passage air. */
|
||||
/** Signed distance of the local landing room. Negative means passage air. */
|
||||
VOXELFORGE_API float VF_EvaluatePassageLandingSDF(
|
||||
const FVector& Position,
|
||||
const FVoxelPassageLanding& Landing);
|
||||
|
||||
/** True when a point belongs to the guaranteed solid floor slab of the landing/connector. */
|
||||
/** True when a point belongs to the guaranteed solid floor slab of the landing room. */
|
||||
VOXELFORGE_API bool VF_IsPassageLandingFloor(
|
||||
const FVector& Position,
|
||||
const FVoxelPassageLanding& Landing);
|
||||
@@ -537,7 +519,7 @@ namespace VoxelNoise
|
||||
// - Determine room existence, position, size
|
||||
// - Run O(N²) nearest-neighbor backbone
|
||||
// - Decide O(N²) tunnel connections with hash lookups
|
||||
// - Compute tunnel radii, Z offsets, midpoint warping
|
||||
// - Compute tunnel radii, floor-aligned endpoints, and wandering control chains
|
||||
//
|
||||
// With caching: build once, evaluate 32K times with just SDF math.
|
||||
// This is the single biggest CPU performance win for cave generation.
|
||||
@@ -588,19 +570,41 @@ struct FCachedRoom
|
||||
float ShapeR = 0.0f;
|
||||
};
|
||||
|
||||
// A pre-computed tunnel segment — all connection decisions and hash-derived
|
||||
// properties (radius, Z offset, midpoint warp) are resolved during cache build.
|
||||
// A local flat-floor bridge over the overlap of two room bodies. It is not a new graph edge: it
|
||||
// only gives an existing spherical-room intersection one shared walkable floor level, removing
|
||||
// the high lip that otherwise appears when the two room floor cuts disagree.
|
||||
struct FCachedRoomFloorJoin
|
||||
{
|
||||
FVector Start;
|
||||
FVector End;
|
||||
float Radius = 0.0f;
|
||||
float FloorZ = 0.0f;
|
||||
float CeilingZ = 0.0f;
|
||||
FVector BoundCenter;
|
||||
float BoundRadiusSq = 0.0f;
|
||||
};
|
||||
|
||||
// A pre-computed wandering tunnel chain — all connection decisions and hash-derived properties
|
||||
// (radii, Z offsets, control points) are resolved during cache build.
|
||||
struct FCachedTunnel
|
||||
{
|
||||
FVector EndpointA; // First room's connection point (with Z offset)
|
||||
FVector EndpointB; // Second room's connection point (with Z offset)
|
||||
FVector EndpointA; // First room's floor-tangent connection point in SDF coordinates
|
||||
FVector EndpointB; // Second room's floor-tangent connection point in SDF coordinates
|
||||
float RadiusA; // Tube radius at endpoint A
|
||||
float RadiusB; // Tube radius at endpoint B
|
||||
// Warped midpoint — only used if bHasMidpoint is true.
|
||||
// Creates a two-segment curved path instead of a straight tube.
|
||||
// Compatibility summary of the chain's first/middle/last points. These fields are retained
|
||||
// for diagnostics and old cache consumers; EvaluateSDFCached uses ControlPoints when present.
|
||||
FVector Midpoint;
|
||||
float RadiusMid; // Radius at the midpoint (average of A and B)
|
||||
bool bHasMidpoint; // True when TunnelWarpStrength > 0 and tunnel is long enough
|
||||
float RadiusMid;
|
||||
bool bHasMidpoint;
|
||||
TArray<FVector> ControlPoints;
|
||||
TArray<float> ControlRadii;
|
||||
// World-space copy used by the final structural air backstop. Evaluating this copy avoids
|
||||
// turning a gentle authored floor ramp into a vertical break when the cave warp is nonlinear.
|
||||
TArray<FVector> WorldControlPoints;
|
||||
TArray<float> WorldControlRadii;
|
||||
FVector WorldBoundCenter = FVector::ZeroVector;
|
||||
float WorldBoundRadiusSq = 0.0f;
|
||||
// Bounding sphere for quick per-voxel rejection
|
||||
FVector BoundCenter; // Center of the bounding sphere
|
||||
float BoundRadiusSq; // Squared radius — if voxel is further, skip this tunnel
|
||||
@@ -657,7 +661,8 @@ struct FCachedColumn
|
||||
// for every voxel in the chunk. Typically contains 5-15 rooms and 10-30 tunnels.
|
||||
struct FChunkSDFCache
|
||||
{
|
||||
TArray<FCachedRoom> Rooms;
|
||||
TArray<FCachedRoom> Rooms;
|
||||
TArray<FCachedRoomFloorJoin> RoomFloorJoins;
|
||||
TArray<FCachedTunnel> Tunnels;
|
||||
TArray<FCachedPit> Pits;
|
||||
TArray<FCachedChimney> Chimneys;
|
||||
@@ -681,6 +686,15 @@ namespace VoxelCaveMorphology
|
||||
return VoxelHash::Mix(WorldSeed ^ (uint32)(StrateIndex * 7919 + 104729));
|
||||
}
|
||||
|
||||
// Map a world-space query to the SDF coordinate space used by the room/tunnel cache. Keeping
|
||||
// this transform in the morphology module lets the generator's post-disturbance structural
|
||||
// check share the exact same warp as the source and the landing query.
|
||||
VOXELFORGE_API FVector ApplyCaveWarp(
|
||||
const FVector& WorldPoint,
|
||||
const FStrateGenerationParams& Params,
|
||||
uint32 Seed
|
||||
);
|
||||
|
||||
// PHASE 1: Build the SDF cache for a rectangular region.
|
||||
// Collects all rooms, computes nearest-neighbor backbone, decides tunnel
|
||||
// connections, and pre-computes all tunnel geometry (radii, offsets, midpoints).
|
||||
@@ -697,7 +711,7 @@ namespace VoxelCaveMorphology
|
||||
// @param Seed — world seed
|
||||
// @param StrateIndex — which strate (offsets hash for variety)
|
||||
// @param TerrainOps — optional probability pool; null = no per-room ops
|
||||
void BuildChunkCache(
|
||||
VOXELFORGE_API void BuildChunkCache(
|
||||
FChunkSDFCache& OutCache,
|
||||
float SearchMinX, float SearchMinY,
|
||||
float SearchMaxX, float SearchMaxY,
|
||||
@@ -720,13 +734,37 @@ namespace VoxelCaveMorphology
|
||||
// per-room terrain op assigned to this voxel's room.
|
||||
// (Room shape variety is baked into FCachedRoom by BuildChunkCache — no per-voxel roll.)
|
||||
// @return negative = inside cave, positive = solid rock
|
||||
float EvaluateSDFCached(
|
||||
VOXELFORGE_API float EvaluateSDFCached(
|
||||
float WorldX, float WorldY, float WorldZ,
|
||||
const FChunkSDFCache& Cache,
|
||||
float SDFBlendRadius,
|
||||
int32* OutNearestRoomIdx = nullptr
|
||||
);
|
||||
|
||||
// Evaluate the raw union of cached graph-tunnel capsules. This is intentionally separate from
|
||||
// the SmoothMin morphology: the generator uses it after terrain/passage posts to reassert the
|
||||
// tunnel's walkable air core without reintroducing a connector or changing room ownership.
|
||||
VOXELFORGE_API float EvaluateTunnelCoreSDF(
|
||||
float WorldX, float WorldY, float WorldZ,
|
||||
const FChunkSDFCache& Cache
|
||||
);
|
||||
|
||||
// Evaluate the final structural air contract against the world-space wandering chain. Room
|
||||
// SDFs remain evaluated in warped coordinates; only this post uses world coordinates so its
|
||||
// walkable floor follows the authored chain exactly.
|
||||
VOXELFORGE_API float EvaluateTunnelCoreWorldSDF(
|
||||
float WorldX, float WorldY, float WorldZ,
|
||||
const FChunkSDFCache& Cache
|
||||
);
|
||||
|
||||
// True for the finite support slab beneath a world-space graph tunnel. The native density
|
||||
// path and the operator stack use this same predicate before reopening the air core, keeping
|
||||
// the rounded capsule's bottom from becoming an unsupported point.
|
||||
VOXELFORGE_API bool IsTunnelSupportFloorWorldPoint(
|
||||
float WorldX, float WorldY, float WorldZ,
|
||||
const FChunkSDFCache& Cache
|
||||
);
|
||||
|
||||
// CONVENIENCE WRAPPER: builds a temporary cache and evaluates in one call.
|
||||
// Use this for one-off queries (debug visualization, single-point sampling).
|
||||
// For chunk generation, use BuildChunkCache + EvaluateSDFCached instead.
|
||||
@@ -748,7 +786,9 @@ namespace VoxelCaveMorphology
|
||||
* clear capsule volume. The local source stencil deliberately does not claim flood-fill
|
||||
* connectivity or a measured component size; it selects a topology-backed feature core instead.
|
||||
* For TunnelNetwork/Underwater, Seed must be the strate-specific room seed returned by
|
||||
* VoxelCaveMorphology::MakeStrateSeed; for slab, maze, shaft, and island archetypes, Seed is the
|
||||
* VoxelCaveMorphology::MakeStrateSeed; the optional WorldSeed selects the production cave-warp
|
||||
* field and should be the generator world seed. If omitted, the room seed is used for both for
|
||||
* backward-compatible standalone queries. For slab, maze, shaft, and island archetypes, Seed is the
|
||||
* generator world seed consumed by their source. SurfaceWorld deliberately has no answer here:
|
||||
* its production height can be selected by manager-owned biome context and per-chunk state,
|
||||
* which this re-entrant query cannot inspect.
|
||||
@@ -772,7 +812,8 @@ VOXELFORGE_API bool VF_SuggestLandingPoint(
|
||||
float DesiredX,
|
||||
float DesiredY,
|
||||
float MaxLateralSnap,
|
||||
FVector& OutPoint
|
||||
FVector& OutPoint,
|
||||
int32 WorldSeed = MIN_int32
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -797,5 +838,6 @@ VOXELFORGE_API bool VF_SuggestLandingPoint(
|
||||
float DesiredX,
|
||||
float DesiredY,
|
||||
float MaxLateralSnap,
|
||||
FVector& OutPoint
|
||||
FVector& OutPoint,
|
||||
int32 WorldSeed = MIN_int32
|
||||
);
|
||||
@@ -250,7 +250,7 @@ public:
|
||||
|
||||
/**
|
||||
* RÔLE 4 — ajoute les invariants de monde, dans l'ordre fixe, à la fin de la pile.
|
||||
* spine (0,0) → seal de frontière → carve de passage → seal de limite XY.
|
||||
* spine (0,0) → seal de frontière → carve de passage → [posts spécifiques] → seal de limite XY.
|
||||
*
|
||||
* ⚠️ La couche de diff (édits joueur) n'est PAS ici : elle vit dans `GetDensityAt`, APRÈS la
|
||||
* négation MC, avec les disturbances. Elle rejoindra la pile quand les disturbances seront
|
||||
@@ -266,7 +266,8 @@ public:
|
||||
*/
|
||||
VOXELFORGE_API void AppendStructuralPost(float StrateTopWorldZ, float StrateBottomWorldZ,
|
||||
float SealThickness, float BaseDensity, float SpineRadius,
|
||||
const UVoxelStrateManager* StrateManager);
|
||||
const UVoxelStrateManager* StrateManager,
|
||||
bool bAppendEdgeSeal = true);
|
||||
|
||||
private:
|
||||
TArray<FOpEntry> Ops;
|
||||
@@ -387,10 +388,10 @@ namespace VoxelDensityOps
|
||||
bool bAppendStructuralPosts = true);
|
||||
|
||||
/**
|
||||
* TunnelNetwork — **COMPLET, 20 ops** :
|
||||
* TunnelNetwork — **COMPLET** :
|
||||
* ConstantRock → RoomGraph(warp + pits + cheminées) → SdfCarve → CaveRoughness(4b)
|
||||
* → Terrace → LayerLines → Ribbing → Overhang → Cliff → Scallop → Arch → RoomColumn(4d)
|
||||
* → Dome(4g) → Pinch(4h) → FloorBias → Worms → [structural ×4]
|
||||
* → Dome(4g) → Pinch(4h) → FloorBias → Worms → [spine, boundary, passage, graph-air, XY seal]
|
||||
*
|
||||
* L'override d'op PAR SALLE (étape C1) n'ajoute aucun opérateur : `FRoomGraphSource` publie
|
||||
* `LocalParams()` — les params de la strate avec l'op de la salle la plus proche appliqué — et
|
||||
|
||||
@@ -141,7 +141,10 @@ FORCEINLINE void VF_ApplyOriginSpine(float& Density, float WorldX, float WorldY,
|
||||
&& FMath::Abs(WorldX) <= FMath::Max(Geometry.HalfWidth - 1.0f, 0.0f)
|
||||
&& FMath::Abs(WorldY) <= FMath::Max(Geometry.HalfWidth - 1.0f, 0.0f))
|
||||
{
|
||||
Density = FMath::Max(Density, BaseDensity);
|
||||
if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker())
|
||||
{
|
||||
Density = FMath::Max(Density, BaseDensity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +179,10 @@ FORCEINLINE void VF_ApplyOriginLandingFloor(float& Density,
|
||||
&& FMath::Abs(WorldX) <= FMath::Max(Geometry.HalfWidth - 1.0f, 0.0f)
|
||||
&& FMath::Abs(WorldY) <= FMath::Max(Geometry.HalfWidth - 1.0f, 0.0f))
|
||||
{
|
||||
Density = FMath::Max(Density, BaseDensity);
|
||||
if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker())
|
||||
{
|
||||
Density = FMath::Max(Density, BaseDensity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,11 @@ namespace VoxelPassageGeometry
|
||||
constexpr float SealSafetyMarginVoxels = 4.0f;
|
||||
constexpr float RoomRoundingVoxels = 1.5f;
|
||||
constexpr float LandingCarveBlendVoxels = 4.0f;
|
||||
// The origin landing is reserved for the same straight spine that the vertical-shaft
|
||||
// generator uses: OriginSpineRadius on each side, with a three-voxel / 0.75 m floor ledge
|
||||
// around it after the support patch's one-voxel inset. At the default radius (14 voxels) this
|
||||
// is a 28-voxel / 7 m future shaft diameter.
|
||||
constexpr float OriginShaftFloorClearanceVoxels = 4.0f;
|
||||
// Level tunnel apron outside each room-floor edge. It covers the room/tunnel SDF blend and
|
||||
// keeps the ramp from starting while the route is still over the room's square support patch.
|
||||
constexpr float WalkableTunnelLandingApronVoxels = 6.0f; // 1.5 m
|
||||
@@ -33,18 +38,16 @@ namespace VoxelPassageGeometry
|
||||
// ownership band: the floor writer owns the plane, while the air writer owns the capsule
|
||||
// volume above it even when two overlapping passage projections differ by a fraction.
|
||||
constexpr float WalkableTunnelFloorAirClearanceVoxels = 0.5f;
|
||||
// The graph-tunnel post owns the interior above its floor after terrain and passage-floor
|
||||
// writers have run. The support-floor predicate is also an exclusion mask for the air post,
|
||||
// so overlapping graph tubes cannot erase one another's walkable support band.
|
||||
constexpr float CaveTunnelAirCoreInsetVoxels = 0.5f;
|
||||
|
||||
// A 15 degree ramp is comfortably below the 44 degree engine walkability ceiling. The
|
||||
// latter is a scramble limit, not a sensible default for a tunnel the player walks through.
|
||||
constexpr float WalkableTunnelMaxGradientDegrees = 15.0f;
|
||||
constexpr float WalkableTunnelMaxGradient = 0.2679491924311227f; // tan(15 degrees)
|
||||
|
||||
// The network connector retains the §6.5 single-file dimensions: 2.5 m clear width and 3 m
|
||||
// clear height. These values are in voxels because all passage descriptors are voxel-space.
|
||||
constexpr float ConnectorRadiusVoxels = 5.0f;
|
||||
constexpr float ConnectorHeightVoxels = 12.0f;
|
||||
constexpr float RootOverlapVoxels = 2.25f;
|
||||
|
||||
// The walkability contract is about the floor carried by the tube, not only its centreline.
|
||||
// A tapered tube changes that floor by the radius delta, so the construction and the audit
|
||||
// use this same conservative floor delta.
|
||||
@@ -53,6 +56,204 @@ namespace VoxelPassageGeometry
|
||||
return Centreline.Z - FMath::Abs(Radius);
|
||||
}
|
||||
|
||||
// Vertical-shaft tree links are round air capsules, but the player-fit graph needs a
|
||||
// deterministic support plane to walk between shafts. Keep that plane on the first
|
||||
// seal-safe ledge shared by the whole strate, so every tree edge has the same walk level.
|
||||
// This is shaft-network support, not an origin landing connector.
|
||||
FORCEINLINE float VerticalShaftConnectorFloorThickness(float Radius)
|
||||
{
|
||||
return FMath::Min(
|
||||
LandingFloorThicknessVoxels,
|
||||
FMath::Max(0.5f, FMath::Abs(Radius) * 0.5f));
|
||||
}
|
||||
|
||||
FORCEINLINE float VerticalShaftConnectorFloorZ(
|
||||
float StrateBottomZ,
|
||||
float InteriorBottomZ,
|
||||
float InteriorTopZ,
|
||||
float LedgeSpacing,
|
||||
float LedgeDepth,
|
||||
float ConnectorRadius)
|
||||
{
|
||||
const float FloorThickness = VerticalShaftConnectorFloorThickness(ConnectorRadius);
|
||||
const float MinimumFloor = InteriorBottomZ
|
||||
+ FloorThickness + SealSafetyMarginVoxels;
|
||||
const float MaximumFloor = InteriorTopZ
|
||||
- PlayerHeightVoxels - SealSafetyMarginVoxels;
|
||||
if (!FMath::IsFinite(StrateBottomZ)
|
||||
|| !FMath::IsFinite(InteriorBottomZ)
|
||||
|| !FMath::IsFinite(InteriorTopZ)
|
||||
|| !FMath::IsFinite(LedgeSpacing)
|
||||
|| !FMath::IsFinite(LedgeDepth)
|
||||
|| !FMath::IsFinite(MinimumFloor)
|
||||
|| !FMath::IsFinite(MaximumFloor)
|
||||
|| !(MaximumFloor > MinimumFloor))
|
||||
{
|
||||
return 0.5f * (InteriorBottomZ + InteriorTopZ);
|
||||
}
|
||||
|
||||
if (LedgeSpacing > 0.0f && LedgeDepth > 0.0f)
|
||||
{
|
||||
const float RelativeBottom = InteriorBottomZ - StrateBottomZ;
|
||||
const int32 FirstPeriod = FMath::FloorToInt(RelativeBottom / LedgeSpacing);
|
||||
// The small fixed bound keeps malformed authored values from turning generation into
|
||||
// an unbounded search. Normal shaft strates find the first or second period.
|
||||
constexpr int32 MaxPeriods = 64;
|
||||
for (int32 PeriodOffset = 0; PeriodOffset < MaxPeriods; ++PeriodOffset)
|
||||
{
|
||||
const float PeriodStart = static_cast<float>(FirstPeriod + PeriodOffset)
|
||||
* LedgeSpacing;
|
||||
const float Candidate = StrateBottomZ + PeriodStart + LedgeDepth;
|
||||
if (Candidate > MinimumFloor && Candidate < MaximumFloor)
|
||||
{
|
||||
return Candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return FMath::Clamp(
|
||||
0.5f * (InteriorBottomZ + InteriorTopZ), MinimumFloor, MaximumFloor);
|
||||
}
|
||||
|
||||
FORCEINLINE float VerticalShaftConnectorCenterZ(
|
||||
float StrateBottomZ,
|
||||
float InteriorBottomZ,
|
||||
float InteriorTopZ,
|
||||
float LedgeSpacing,
|
||||
float LedgeDepth,
|
||||
float ConnectorRadius)
|
||||
{
|
||||
const float SafeRadius = FMath::Abs(ConnectorRadius);
|
||||
const float FloorThickness = VerticalShaftConnectorFloorThickness(SafeRadius);
|
||||
return VerticalShaftConnectorFloorZ(
|
||||
StrateBottomZ, InteriorBottomZ, InteriorTopZ,
|
||||
LedgeSpacing, LedgeDepth, SafeRadius)
|
||||
+ SafeRadius - FloorThickness;
|
||||
}
|
||||
|
||||
/** Add a flat, inset support band below one horizontal shaft-tree capsule. */
|
||||
FORCEINLINE void VF_ApplyVerticalShaftConnectorFloor(
|
||||
float& Density,
|
||||
float WorldX,
|
||||
float WorldY,
|
||||
float WorldZ,
|
||||
const FVector& Start,
|
||||
const FVector& End,
|
||||
float Radius,
|
||||
float FloorZ,
|
||||
float BaseDensity)
|
||||
{
|
||||
const float SafeRadius = FMath::Abs(Radius);
|
||||
const float FloorThickness = VerticalShaftConnectorFloorThickness(SafeRadius);
|
||||
const float FloorRadius = FMath::Max(0.5f, SafeRadius - 0.5f);
|
||||
if (!(SafeRadius > 0.0f)
|
||||
|| !(BaseDensity > 0.0f)
|
||||
|| !FMath::IsFinite(WorldX)
|
||||
|| !FMath::IsFinite(WorldY)
|
||||
|| !FMath::IsFinite(WorldZ)
|
||||
|| !FMath::IsFinite(FloorZ)
|
||||
|| !FMath::IsFinite(Start.X) || !FMath::IsFinite(Start.Y)
|
||||
|| !FMath::IsFinite(End.X) || !FMath::IsFinite(End.Y))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const FVector2D A(Start.X, Start.Y);
|
||||
const FVector2D Delta(End.X - Start.X, End.Y - Start.Y);
|
||||
const float LengthSquared = Delta.SizeSquared();
|
||||
const float T = LengthSquared > KINDA_SMALL_NUMBER
|
||||
? FMath::Clamp(
|
||||
FVector2D::DotProduct(FVector2D(WorldX, WorldY) - A, Delta)
|
||||
/ LengthSquared,
|
||||
0.0f, 1.0f)
|
||||
: 0.0f;
|
||||
const FVector2D Closest = A + Delta * T;
|
||||
if ((FVector2D(WorldX, WorldY) - Closest).SizeSquared()
|
||||
> FMath::Square(FloorRadius))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (WorldZ <= FloorZ + KINDA_SMALL_NUMBER
|
||||
&& WorldZ > FloorZ - FloorThickness)
|
||||
{
|
||||
Density = FMath::Max(Density, BaseDensity);
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyVerticalShaftConnectorAir runs before the generator's shared disturbance pass. Keep
|
||||
// this one-bit, call-local hand-off so a bridge/ridge cannot refill the core it just opened.
|
||||
// It is thread-local state, never a per-voxel cache, and is reset around every density query.
|
||||
FORCEINLINE bool& VerticalShaftConnectorAirMarker()
|
||||
{
|
||||
static thread_local bool bMarked = false;
|
||||
return bMarked;
|
||||
}
|
||||
|
||||
FORCEINLINE void ResetVerticalShaftConnectorAirMarker()
|
||||
{
|
||||
VerticalShaftConnectorAirMarker() = false;
|
||||
}
|
||||
|
||||
/** Reassert the walkable air core above one horizontal shaft-tree floor band. */
|
||||
FORCEINLINE void VF_ApplyVerticalShaftConnectorAir(
|
||||
float& Density,
|
||||
float WorldX,
|
||||
float WorldY,
|
||||
float WorldZ,
|
||||
const FVector& Start,
|
||||
const FVector& End,
|
||||
float Radius,
|
||||
float FloorZ,
|
||||
float BaseDensity)
|
||||
{
|
||||
const float SafeRadius = FMath::Abs(Radius);
|
||||
// Leave a one-voxel wall band for the roughness/solid shell, while retaining more than
|
||||
// enough width for the 1.36-voxel player capsule and its support stencil.
|
||||
const float CoreRadius = FMath::Max(0.5f, SafeRadius - 1.0f);
|
||||
if (!(SafeRadius > 0.0f)
|
||||
|| !(BaseDensity > 0.0f)
|
||||
|| !FMath::IsFinite(WorldX)
|
||||
|| !FMath::IsFinite(WorldY)
|
||||
|| !FMath::IsFinite(WorldZ)
|
||||
|| !FMath::IsFinite(FloorZ)
|
||||
|| !FMath::IsFinite(Start.X) || !FMath::IsFinite(Start.Y)
|
||||
|| !FMath::IsFinite(Start.Z)
|
||||
|| !FMath::IsFinite(End.X) || !FMath::IsFinite(End.Y)
|
||||
|| !FMath::IsFinite(End.Z))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// The exact floor sample belongs to the air core's lower ownership edge. The floor
|
||||
// writer only owns the three voxels below FloorZ, so clearing from the half-voxel band
|
||||
// above it cannot remove support.
|
||||
if (WorldZ + KINDA_SMALL_NUMBER
|
||||
< FloorZ + WalkableTunnelFloorAirClearanceVoxels)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const FVector Delta = End - Start;
|
||||
const float LengthSquared = Delta.SizeSquared();
|
||||
const float T = LengthSquared > KINDA_SMALL_NUMBER
|
||||
? FMath::Clamp(FVector::DotProduct(FVector(WorldX, WorldY, WorldZ) - Start, Delta)
|
||||
/ LengthSquared, 0.0f, 1.0f)
|
||||
: 0.0f;
|
||||
const FVector Closest = Start + Delta * T;
|
||||
if ((FVector(WorldX, WorldY, WorldZ) - Closest).SizeSquared()
|
||||
> FMath::Square(CoreRadius))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Internal convention: negative is air, positive is solid. This is intentionally a
|
||||
// post-ledge write; a partial shaft shelf must not cap the tree route's player-height
|
||||
// envelope after the capsule has already carved it.
|
||||
VerticalShaftConnectorAirMarker() = true;
|
||||
Density = FMath::Min(Density, -BaseDensity);
|
||||
}
|
||||
|
||||
FORCEINLINE float TunnelFloorGradient(
|
||||
const FVector& Start, float StartRadius,
|
||||
const FVector& End, float EndRadius)
|
||||
@@ -194,7 +395,14 @@ namespace VoxelPassageGeometry
|
||||
return Result;
|
||||
}
|
||||
|
||||
Result.HalfWidth = LandingHalfWidthForRadius(OriginRadius);
|
||||
// Keep a solid walking ledge around the future shaft, not merely a room whose wall is
|
||||
// tangent to it. The shaft diameter is 2 * OriginRadius; the extra four voxels leave a
|
||||
// three-voxel / 0.75 m floor margin after the support patch's one-voxel inset at the
|
||||
// authored 25 cm scale.
|
||||
Result.HalfWidth = FMath::Max(
|
||||
LandingHalfWidthForRadius(OriginRadius),
|
||||
FMath::Max(FMath::Abs(OriginRadius), 1.0f)
|
||||
+ OriginShaftFloorClearanceVoxels);
|
||||
const float DesiredHeight = LandingHeightForRadius(OriginRadius);
|
||||
const float SafeSeal = FMath::Max(SealThickness, 0.0f);
|
||||
const float InnerBottom = StrateBottomZ + SafeSeal;
|
||||
|
||||
@@ -393,9 +393,10 @@ public:
|
||||
//=========================================================================
|
||||
|
||||
// Authored radius (voxels) of the guaranteed (0,0) landing room in EVERY strate, regardless
|
||||
// of archetype. The §6.5 body-sized formulas turn it into a flat-floored room; this setting
|
||||
// no longer bores a vertical column. 0 = disabled. The room is top-anchored inside each
|
||||
// strate, and the seals remain solid until a progression passage is opened.
|
||||
// of archetype. The §6.5 body-sized formulas turn it into a flat-floored room reserved for a
|
||||
// future straight shaft of diameter 2 * this radius. At the default 14-voxel radius that is
|
||||
// 28 voxels / 7 m, with a three-voxel / 0.75 m floor ledge around it. This setting does not bore the
|
||||
// shaft today: lower seals remain solid until a progression passage is opened.
|
||||
// 10-14 → compact landing room (default) · 20+ → broad landing room
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Spine", meta = (ClampMin = "0.0"))
|
||||
float OriginSpineRadius = 14.0f;
|
||||
|
||||
@@ -35,7 +35,12 @@ struct VOXELFORGE_API FVoxelPlayerCapsuleConstants
|
||||
static constexpr float HeightVoxels = VoxelPassageGeometry::PlayerHeightVoxels;
|
||||
};
|
||||
|
||||
/** Read-only density source used by the offline Tier 2 pass for a materialised custom stack. */
|
||||
/**
|
||||
* Read-only density source used by the offline Tier 2 pass for a materialised custom stack.
|
||||
*
|
||||
* SampleDensity is called concurrently during grid sampling; implementations must be const,
|
||||
* re-entrant, and safe for concurrent read-only calls, just like UVoxelGenerator::GetDensityAt.
|
||||
*/
|
||||
class VOXELFORGE_API IVoxelStrateDensitySampler
|
||||
{
|
||||
public:
|
||||
@@ -70,7 +75,7 @@ struct VOXELFORGE_API FVoxelStrateMeasureSettings
|
||||
TOptional<FVector2D> CoverPointB;
|
||||
float CoverMarginVoxels = 48.0f;
|
||||
// Origin-rooted topologies can route from one mouth to the other through the finite (0,0)
|
||||
// landing room and its explicitly carved connector network.
|
||||
// landing room and its local room/join geometry. There is no hidden radial connector network.
|
||||
// When true, include (0,0) in the fitted AABB before applying MaxCells. A caller must keep
|
||||
// this enabled for that topology; an over-cap route window is then refused honestly instead
|
||||
// of being cropped back to a mouth-only box.
|
||||
|
||||
@@ -620,8 +620,8 @@ struct VOXELFORGE_API FStrateGenerationParams
|
||||
|
||||
// How much tunnel paths curve (in voxels of sideways displacement).
|
||||
// Without this, tunnels are straight lines between rooms.
|
||||
// This adds a hash-derived midpoint offset to each tunnel, creating
|
||||
// unique curves — some bend left, some right, some are nearly straight.
|
||||
// This adds deterministic hash-jittered control points along each tunnel, creating
|
||||
// unique wandering chains — some bend left, some right, some are nearly straight.
|
||||
// Capped at 25% of tunnel length to prevent kinky short tunnels.
|
||||
// 0 → perfectly straight tunnels (artificial look)
|
||||
// 8-12→ gentle natural curves
|
||||
@@ -642,21 +642,20 @@ struct VOXELFORGE_API FStrateGenerationParams
|
||||
// Topology of the guaranteed tunnel network.
|
||||
// true → each room links to its best candidate among rooms CLOSER to (0,0): the whole
|
||||
// network becomes a tree rooted at the origin room — every cave is reachable
|
||||
// from the spine hub, tunnels flow inward like tributaries (intentional descent
|
||||
// structure). TunnelDensity still adds loops on top.
|
||||
// from the graph root, tunnels flow inward like tributaries (intentional descent
|
||||
// structure). This graph root is not a radial landing-to-hub road. TunnelDensity
|
||||
// still adds loops on top.
|
||||
// false → legacy nearest-neighbor pairing: organic scattered clusters, but connectivity
|
||||
// between clusters is NOT guaranteed (isolated pockets are common).
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels")
|
||||
bool bTunnelsFlowTowardOrigin = true;
|
||||
|
||||
// How much tunnel endpoints shift up/down within rooms (0-1).
|
||||
// Fraction of the room's vertical radius. Each tunnel endpoint gets
|
||||
// a hash-derived Z offset, so tunnels enter rooms at different heights.
|
||||
// Combined with terrain ops (cliffs, terraces), this creates multi-level rooms
|
||||
// where one tunnel exits at the upper ledge and another at the lower floor.
|
||||
// 0.0 → all tunnels connect at room center height
|
||||
// 0.25 → endpoints use a restrained quarter-height offset (default)
|
||||
// 1.0 → full range — tunnels can enter near floor or ceiling
|
||||
// Vertical wander amount for tunnel control points (0-1). Endpoints are always tangent to
|
||||
// their room's deterministic floor so a room join cannot be vertically severed; this value
|
||||
// controls only the interior chain's bounded up/down variation.
|
||||
// 0.0 → level floor-to-floor chain
|
||||
// 0.25 → restrained vertical wander (default)
|
||||
// 1.0 → strongest interior vertical variation
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||
float TunnelEndpointZOffset = 0.25f;
|
||||
|
||||
@@ -1787,15 +1786,15 @@ struct VOXELFORGE_API FStratePassageConfig
|
||||
|
||||
// ----- WIDTH (tapers along the length) -----
|
||||
// Radius at the two mouths (entry/exit) and at the middle. Equal = uniform tube;
|
||||
// The walkable default is deliberately single-file: 3.0 m diameter at mouths and 2.5 m at
|
||||
// the waist (6/5 voxel radii at 25 cm per voxel). The §6.5 landing room remains wider for
|
||||
// turning.
|
||||
// The walkable default is 1.5x the original comfortable profile: 4.5 m diameter at mouths
|
||||
// and 3.75 m at the waist (9/7.5 voxel radii at 25 cm per voxel). The §6.5 landing room
|
||||
// remains wider for turning.
|
||||
// Mouth > Mid = chambers at the ends with a squeeze between; Mid > Mouth = a bulge.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Width", meta = (ClampMin = "1.0"))
|
||||
float MouthRadius = 6.0f;
|
||||
float MouthRadius = 9.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Width", meta = (ClampMin = "1.0"))
|
||||
float MidRadius = 5.0f;
|
||||
float MidRadius = 7.5f;
|
||||
|
||||
// ----- LENGTH -----
|
||||
// How far the tunnel reaches INTO each strate (voxels). Auto-capped to the interior;
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "VoxelDiffLayer.h"
|
||||
#include "VoxelGenerator.h"
|
||||
#include "VoxelMarchingCubesMesher.h"
|
||||
#include "VoxelNoise.h"
|
||||
#include "VoxelSettings.h"
|
||||
#include "VoxelStrateDefinition.h"
|
||||
#include "VoxelStrateManager.h"
|
||||
@@ -32,21 +33,21 @@ namespace
|
||||
{
|
||||
using FExploreJsonWriter = TJsonWriter<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>;
|
||||
|
||||
constexpr int32 DefaultRenderWidth = 192;
|
||||
constexpr int32 DefaultRenderHeight = 128;
|
||||
constexpr int32 DefaultRenderWidth = 512;
|
||||
constexpr int32 DefaultRenderHeight = 288;
|
||||
constexpr float DefaultRenderStepVoxels = 0.25f;
|
||||
constexpr float DefaultRenderMaxDistanceVoxels = 256.0f;
|
||||
constexpr float DefaultRenderMaxDistanceVoxels = 160.0f;
|
||||
constexpr int32 DefaultExportSize = 128;
|
||||
constexpr int32 DefaultMaxWalkCells = 32000000;
|
||||
constexpr int32 MaxSyntheticStrates = 64;
|
||||
constexpr int32 MaxRenderPixels = 1048576;
|
||||
constexpr int64 MaxRenderDensitySamples = 200000000ll;
|
||||
constexpr int64 MaxRenderDensitySamples = 400000000ll;
|
||||
constexpr int32 MaxExportSize = 128;
|
||||
constexpr int64 MaxWalkWorkingBytes = 768ll * 1024ll * 1024ll;
|
||||
// The exact fine-grid walk retains Air + Density + player-fit bits, and can transiently hold
|
||||
// Parent + Queue + Path or Visited + NarrowCell + DFS stacks. 20 bytes/cell is a conservative
|
||||
// preflight figure for those arrays, including allocator slack/headroom.
|
||||
constexpr int64 EstimatedWalkBytesPerCell = 20ll;
|
||||
// The exact fine-grid walk retains Air + Density + player-fit bits + non-finite flags, and can
|
||||
// transiently hold Parent + Queue + Path or Visited + NarrowCell + DFS stacks. 21 bytes/cell is a
|
||||
// conservative preflight figure for those arrays, including allocator slack/headroom.
|
||||
constexpr int64 EstimatedWalkBytesPerCell = 21ll;
|
||||
constexpr int64 MaxExportWorkingBytes = 1024ll * 1024ll * 1024ll;
|
||||
|
||||
const TCHAR* ArchetypeName(ECaveGeneratorType Archetype)
|
||||
@@ -475,6 +476,8 @@ struct FExploreRenderFrame
|
||||
{
|
||||
FString FileName;
|
||||
FString Purpose = TEXT("overview");
|
||||
FString CameraSeedSource = TEXT("player_fit");
|
||||
FVector CameraSeedPoseVoxels = FVector::ZeroVector;
|
||||
FVector CameraVoxels = FVector::ZeroVector;
|
||||
FVector TargetVoxels = FVector::ZeroVector;
|
||||
int32 HitPixels = 0;
|
||||
@@ -494,6 +497,9 @@ struct FExploreRenderOutput
|
||||
float MaxDistanceVoxels = 0.0f;
|
||||
int64 EstimatedFixedDensitySamples = 0;
|
||||
FString StepRationale;
|
||||
FString CameraSeedPolicy;
|
||||
int32 CameraSeedCount = 0;
|
||||
bool bAllCamerasPlayerFit = false;
|
||||
TArray<FExploreRenderFrame> Frames;
|
||||
};
|
||||
|
||||
@@ -888,6 +894,7 @@ bool SavePng(
|
||||
bool RunRender(
|
||||
const FExploreArguments& Arguments,
|
||||
const FExploreWorld& World,
|
||||
const FExploreWalkOutput& CameraSeedWalk,
|
||||
FExploreRenderOutput& OutOutput)
|
||||
{
|
||||
OutOutput = FExploreRenderOutput();
|
||||
@@ -910,17 +917,120 @@ bool RunRender(
|
||||
"fixed 0.25-voxel default step (override with -renderstep), detects density sign crossings, "
|
||||
"and refines each crossing with 10 bisection iterations; it never sphere-traces.");
|
||||
|
||||
const float MiddleZ = 0.5f * static_cast<float>(World.TargetBottomWorldZ + World.TargetTopWorldZ);
|
||||
const FVector Target(0.0f, 0.0f, MiddleZ);
|
||||
const FVector CameraPositions[] = {
|
||||
FVector(0.0f, -192.0f, MiddleZ + 32.0f),
|
||||
FVector(192.0f, -160.0f, MiddleZ + 64.0f),
|
||||
FVector(-160.0f, 128.0f, MiddleZ + 96.0f),
|
||||
OutOutput.CameraSeedPolicy = TEXT(
|
||||
"Every camera origin is a density-checked pose from the exact player-fit walk mask: "
|
||||
"arrival, departure, last reached, and agent-final poses are considered in fixed order; "
|
||||
"duplicate poses are removed. The target is offset toward the fitted route so the view is "
|
||||
"from inside the walkable space.");
|
||||
|
||||
TArray<TPair<FVector, FString>, TInlineAllocator<4>> CameraSeeds;
|
||||
const auto AddCameraSeed = [&](bool bAvailable, const FVector& Position, const TCHAR* Source)
|
||||
{
|
||||
if (!bAvailable
|
||||
|| !FMath::IsFinite(Position.X)
|
||||
|| !FMath::IsFinite(Position.Y)
|
||||
|| !FMath::IsFinite(Position.Z))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// The walk mask is authoritative, but this guard catches stale/corrupt report data before
|
||||
// a render can silently start inside rock.
|
||||
const float Density = World.Generator->GetDensityAt(Position.X, Position.Y, Position.Z);
|
||||
if (!FMath::IsFinite(Density) || !(Density > 0.0f))
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (const TPair<FVector, FString>& Existing : CameraSeeds)
|
||||
{
|
||||
if (Existing.Key.Equals(Position, 0.001f))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
CameraSeeds.Emplace(Position, FString(Source));
|
||||
};
|
||||
|
||||
for (int32 ViewIndex = 0; ViewIndex < UE_ARRAY_COUNT(CameraPositions); ++ViewIndex)
|
||||
AddCameraSeed(
|
||||
CameraSeedWalk.bHasArrival,
|
||||
CameraSeedWalk.ArrivalVoxels,
|
||||
TEXT("arrival_player_fit"));
|
||||
AddCameraSeed(
|
||||
CameraSeedWalk.bHasDeparture,
|
||||
CameraSeedWalk.DepartureVoxels,
|
||||
TEXT("departure_player_fit"));
|
||||
AddCameraSeed(
|
||||
CameraSeedWalk.Report.bHasLastReachedPosition,
|
||||
CameraSeedWalk.Report.LastReachedVoxels,
|
||||
TEXT("last_reached_player_fit"));
|
||||
AddCameraSeed(
|
||||
CameraSeedWalk.Report.bHasAgentFinalPosition,
|
||||
CameraSeedWalk.Report.AgentFinalVoxels,
|
||||
TEXT("agent_final_player_fit"));
|
||||
|
||||
if (CameraSeeds.Num() == 0)
|
||||
{
|
||||
const FVector CameraPosition = CameraPositions[ViewIndex];
|
||||
OutOutput.Status = TEXT("refused");
|
||||
OutOutput.RefusalReason = TEXT(
|
||||
"The player-fit seed pass produced no density-positive pose; refusing an inside-rock camera.");
|
||||
return false;
|
||||
}
|
||||
|
||||
OutOutput.CameraSeedCount = CameraSeeds.Num();
|
||||
OutOutput.bAllCamerasPlayerFit = true;
|
||||
FVector RouteFocus = FVector::ZeroVector;
|
||||
int32 RouteFocusCount = 0;
|
||||
if (CameraSeedWalk.bHasArrival)
|
||||
{
|
||||
RouteFocus += CameraSeedWalk.ArrivalVoxels;
|
||||
++RouteFocusCount;
|
||||
}
|
||||
if (CameraSeedWalk.bHasDeparture)
|
||||
{
|
||||
RouteFocus += CameraSeedWalk.DepartureVoxels;
|
||||
++RouteFocusCount;
|
||||
}
|
||||
if (RouteFocusCount > 0)
|
||||
{
|
||||
RouteFocus /= static_cast<float>(RouteFocusCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
RouteFocus = CameraSeeds[0].Key;
|
||||
}
|
||||
|
||||
const FVector KeyLightDirection = FVector(-0.35f, -0.45f, 0.82f).GetSafeNormal();
|
||||
const FVector FillLightDirection = FVector(0.65f, 0.30f, 0.70f).GetSafeNormal();
|
||||
constexpr float AmbientFill = 0.62f;
|
||||
constexpr float KeyLightStrength = 0.50f;
|
||||
constexpr float FillLightStrength = 0.24f;
|
||||
|
||||
for (int32 ViewIndex = 0; ViewIndex < CameraSeeds.Num(); ++ViewIndex)
|
||||
{
|
||||
const FVector CameraSeedPose = CameraSeeds[ViewIndex].Key;
|
||||
const FVector CameraPosition = CameraSeedPose
|
||||
+ FVector::UpVector * (FVoxelPlayerCapsuleConstants::HalfHeightVoxels - 0.5f);
|
||||
const float CameraDensity = World.Generator->GetDensityAt(
|
||||
CameraPosition.X, CameraPosition.Y, CameraPosition.Z);
|
||||
if (!FMath::IsFinite(CameraDensity) || !(CameraDensity > 0.0f))
|
||||
{
|
||||
OutOutput.Status = TEXT("refused");
|
||||
OutOutput.RefusalReason = TEXT(
|
||||
"The eye-height offset of a player-fit camera entered solid density.");
|
||||
return false;
|
||||
}
|
||||
FVector LookDirection = RouteFocus - CameraPosition;
|
||||
LookDirection.Z = 0.0f;
|
||||
LookDirection.Normalize();
|
||||
if (LookDirection.IsNearlyZero())
|
||||
{
|
||||
LookDirection = FVector(
|
||||
ViewIndex == 1 ? -0.6f : 0.8f,
|
||||
ViewIndex == 2 ? 0.7f : -0.4f,
|
||||
0.18f).GetSafeNormal();
|
||||
}
|
||||
// Keep the camera at the fitted pose, while aiming at roughly eye level in the route.
|
||||
const FVector Target = CameraPosition + LookDirection * 96.0f + FVector::UpVector * 8.0f;
|
||||
FExploreCameraBasis Basis;
|
||||
if (!BuildCameraBasis(
|
||||
CameraPosition,
|
||||
@@ -936,9 +1046,8 @@ bool RunRender(
|
||||
}
|
||||
|
||||
TArray<FColor> Pixels;
|
||||
Pixels.Init(FColor(7, 11, 18, 255), Arguments.RenderWidth * Arguments.RenderHeight);
|
||||
Pixels.Init(FColor(12, 18, 28, 255), Arguments.RenderWidth * Arguments.RenderHeight);
|
||||
int32 HitPixels = 0;
|
||||
const FVector LightDirection = FVector(-0.35f, -0.45f, 0.82f).GetSafeNormal();
|
||||
for (int32 PixelY = 0; PixelY < Arguments.RenderHeight; ++PixelY)
|
||||
{
|
||||
for (int32 PixelX = 0; PixelX < Arguments.RenderWidth; ++PixelX)
|
||||
@@ -965,11 +1074,16 @@ bool RunRender(
|
||||
}
|
||||
|
||||
++HitPixels;
|
||||
const float Diffuse = FMath::Max(0.0f, FVector::DotProduct(Normal, LightDirection));
|
||||
const float Brightness = 0.18f + 0.82f * Diffuse;
|
||||
const uint8 RockR = static_cast<uint8>(FMath::Clamp(54.0f * Brightness, 0.0f, 255.0f));
|
||||
const uint8 RockG = static_cast<uint8>(FMath::Clamp(72.0f * Brightness, 0.0f, 255.0f));
|
||||
const uint8 RockB = static_cast<uint8>(FMath::Clamp(92.0f * Brightness, 0.0f, 255.0f));
|
||||
const float Key = FMath::Max(
|
||||
0.0f, FVector::DotProduct(Normal, KeyLightDirection));
|
||||
const float Fill = FMath::Max(
|
||||
0.0f, FVector::DotProduct(Normal, FillLightDirection));
|
||||
const float Brightness = FMath::Clamp(
|
||||
AmbientFill + KeyLightStrength * Key + FillLightStrength * Fill,
|
||||
0.0f, 1.25f);
|
||||
const uint8 RockR = static_cast<uint8>(FMath::Clamp(100.0f * Brightness, 0.0f, 255.0f));
|
||||
const uint8 RockG = static_cast<uint8>(FMath::Clamp(128.0f * Brightness, 0.0f, 255.0f));
|
||||
const uint8 RockB = static_cast<uint8>(FMath::Clamp(160.0f * Brightness, 0.0f, 255.0f));
|
||||
PutPixel(
|
||||
Pixels,
|
||||
Arguments.RenderWidth,
|
||||
@@ -992,6 +1106,9 @@ bool RunRender(
|
||||
|
||||
FExploreRenderFrame& Frame = OutOutput.Frames.AddDefaulted_GetRef();
|
||||
Frame.FileName = FString::Printf(TEXT("render_%02d.png"), ViewIndex);
|
||||
Frame.Purpose = CameraSeeds[ViewIndex].Value;
|
||||
Frame.CameraSeedSource = CameraSeeds[ViewIndex].Value;
|
||||
Frame.CameraSeedPoseVoxels = CameraSeedPose;
|
||||
Frame.CameraVoxels = CameraPosition;
|
||||
Frame.TargetVoxels = Target;
|
||||
Frame.HitPixels = HitPixels;
|
||||
@@ -1086,17 +1203,12 @@ bool RunFailureBoundaryRender(
|
||||
}
|
||||
const float SegmentLength = Segment.Size();
|
||||
const float ViewSideOffset = FMath::Min(64.0f, FMath::Max(16.0f, 0.25f * SegmentLength));
|
||||
const FVector CameraCandidates[][3] = {
|
||||
{
|
||||
AgentFinal - SegmentDirection * 4.0f,
|
||||
bHaveLastReached ? LastReached : AgentFinal,
|
||||
AgentFinal,
|
||||
},
|
||||
{
|
||||
TargetComponentCell + SegmentDirection * 4.0f,
|
||||
TargetComponentCell,
|
||||
TargetComponentCell,
|
||||
},
|
||||
// Failure-focus views are still required to originate from the fitted graph. The old
|
||||
// implementation nudged the camera four voxels toward/away from the boundary and could put
|
||||
// the viewpoint in the very rock that this diagnostic is meant to explain.
|
||||
const FVector CameraCandidates[] = {
|
||||
AgentFinal,
|
||||
bHaveLastReached ? LastReached : AgentFinal,
|
||||
};
|
||||
const FVector ViewTargets[] = {
|
||||
Focus + ViewSide * ViewSideOffset,
|
||||
@@ -1105,16 +1217,17 @@ bool RunFailureBoundaryRender(
|
||||
|
||||
for (int32 ViewIndex = 0; ViewIndex < UE_ARRAY_COUNT(ViewTargets); ++ViewIndex)
|
||||
{
|
||||
FVector CameraPosition = CameraCandidates[ViewIndex][2];
|
||||
for (int32 CandidateIndex = 0; CandidateIndex < 3; ++CandidateIndex)
|
||||
const FVector CameraSeedPose = CameraCandidates[ViewIndex];
|
||||
const FVector CameraPosition = CameraSeedPose
|
||||
+ FVector::UpVector * (FVoxelPlayerCapsuleConstants::HalfHeightVoxels - 0.5f);
|
||||
const float CameraDensity = World.Generator->GetDensityAt(
|
||||
CameraPosition.X, CameraPosition.Y, CameraPosition.Z);
|
||||
if (!FMath::IsFinite(CameraDensity) || !(CameraDensity > 0.0f))
|
||||
{
|
||||
const FVector Candidate = CameraCandidates[ViewIndex][CandidateIndex];
|
||||
const float Density = World.Generator->GetDensityAt(Candidate.X, Candidate.Y, Candidate.Z);
|
||||
if (FMath::IsFinite(Density) && Density > 0.0f)
|
||||
{
|
||||
CameraPosition = Candidate;
|
||||
break;
|
||||
}
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("[VoxelForgeExplore] refusing failure-focus camera %d: fitted pose is not air."),
|
||||
ViewIndex);
|
||||
return false;
|
||||
}
|
||||
const float FailureMaxDistanceVoxels = FMath::Max(
|
||||
Arguments.RenderMaxDistanceVoxels,
|
||||
@@ -1134,9 +1247,13 @@ bool RunFailureBoundaryRender(
|
||||
}
|
||||
|
||||
TArray<FColor> Pixels;
|
||||
Pixels.Init(FColor(7, 11, 18, 255), Arguments.RenderWidth * Arguments.RenderHeight);
|
||||
Pixels.Init(FColor(12, 18, 28, 255), Arguments.RenderWidth * Arguments.RenderHeight);
|
||||
int32 HitPixels = 0;
|
||||
const FVector LightDirection = FVector(-0.35f, -0.45f, 0.82f).GetSafeNormal();
|
||||
const FVector KeyLightDirection = FVector(-0.35f, -0.45f, 0.82f).GetSafeNormal();
|
||||
const FVector FillLightDirection = FVector(0.65f, 0.30f, 0.70f).GetSafeNormal();
|
||||
constexpr float AmbientFill = 0.62f;
|
||||
constexpr float KeyLightStrength = 0.50f;
|
||||
constexpr float FillLightStrength = 0.24f;
|
||||
for (int32 PixelY = 0; PixelY < Arguments.RenderHeight; ++PixelY)
|
||||
{
|
||||
for (int32 PixelX = 0; PixelX < Arguments.RenderWidth; ++PixelX)
|
||||
@@ -1163,11 +1280,16 @@ bool RunFailureBoundaryRender(
|
||||
}
|
||||
|
||||
++HitPixels;
|
||||
const float Diffuse = FMath::Max(0.0f, FVector::DotProduct(Normal, LightDirection));
|
||||
const float Brightness = 0.18f + 0.82f * Diffuse;
|
||||
const uint8 RockR = static_cast<uint8>(FMath::Clamp(54.0f * Brightness, 0.0f, 255.0f));
|
||||
const uint8 RockG = static_cast<uint8>(FMath::Clamp(72.0f * Brightness, 0.0f, 255.0f));
|
||||
const uint8 RockB = static_cast<uint8>(FMath::Clamp(92.0f * Brightness, 0.0f, 255.0f));
|
||||
const float Key = FMath::Max(
|
||||
0.0f, FVector::DotProduct(Normal, KeyLightDirection));
|
||||
const float Fill = FMath::Max(
|
||||
0.0f, FVector::DotProduct(Normal, FillLightDirection));
|
||||
const float Brightness = FMath::Clamp(
|
||||
AmbientFill + KeyLightStrength * Key + FillLightStrength * Fill,
|
||||
0.0f, 1.25f);
|
||||
const uint8 RockR = static_cast<uint8>(FMath::Clamp(100.0f * Brightness, 0.0f, 255.0f));
|
||||
const uint8 RockG = static_cast<uint8>(FMath::Clamp(128.0f * Brightness, 0.0f, 255.0f));
|
||||
const uint8 RockB = static_cast<uint8>(FMath::Clamp(160.0f * Brightness, 0.0f, 255.0f));
|
||||
PutPixel(
|
||||
Pixels,
|
||||
Arguments.RenderWidth,
|
||||
@@ -1243,6 +1365,10 @@ bool RunFailureBoundaryRender(
|
||||
FExploreRenderFrame& Frame = InOutOutput.Frames.AddDefaulted_GetRef();
|
||||
Frame.FileName = FString::Printf(TEXT("failure_boundary_%02d.png"), ViewIndex);
|
||||
Frame.Purpose = TEXT("agent_final_red_target_component_nearest_green_last_reached_orange");
|
||||
Frame.CameraSeedSource = bHaveLastReached && ViewIndex == 1
|
||||
? TEXT("last_reached_player_fit")
|
||||
: TEXT("agent_final_player_fit");
|
||||
Frame.CameraSeedPoseVoxels = CameraSeedPose;
|
||||
Frame.CameraVoxels = CameraPosition;
|
||||
Frame.TargetVoxels = ViewTargets[ViewIndex];
|
||||
Frame.HitPixels = HitPixels;
|
||||
@@ -1273,8 +1399,8 @@ bool RunWalk(
|
||||
{
|
||||
OutOutput = FExploreWalkOutput();
|
||||
OutOutput.WindowPolicy = NeedsOriginInWindow(Arguments.Archetype)
|
||||
? TEXT("origin-inclusive mouth-AABB + margin")
|
||||
: TEXT("mouth-AABB + margin");
|
||||
? TEXT("origin-inclusive target-strate passage-envelope AABB + margin")
|
||||
: TEXT("target-strate passage-envelope AABB + margin");
|
||||
|
||||
const int64 EstimatedWorkingBytes = static_cast<int64>(Arguments.MaxWalkCells)
|
||||
* EstimatedWalkBytesPerCell;
|
||||
@@ -1308,6 +1434,57 @@ bool RunWalk(
|
||||
OutOutput.bHasArrival = true;
|
||||
OutOutput.bHasDeparture = true;
|
||||
|
||||
// Temporary seed-14 probe while diagnosing the fitted VerticalShafts junction. This is
|
||||
// intentionally narrow and will be removed after the floor/tree seam is corrected.
|
||||
if (Arguments.Archetype == ECaveGeneratorType::VerticalShafts && Arguments.Seed == 14)
|
||||
{
|
||||
const FVector ProbeA = OutOutput.ArrivalVoxels;
|
||||
const FVector ProbeB(-100.8f, -26.9f, ProbeA.Z);
|
||||
for (int32 Index = 0; Index <= 10; ++Index)
|
||||
{
|
||||
const float T = static_cast<float>(Index) / 10.0f;
|
||||
const FVector P = FMath::Lerp(ProbeA, ProbeB, T);
|
||||
const float Dm704 = World.Generator->GetDensityAt(P.X, P.Y, -704.0f);
|
||||
const float Dm702 = World.Generator->GetDensityAt(P.X, P.Y, -702.0f);
|
||||
const float Dm7005 = World.Generator->GetDensityAt(P.X, P.Y, -700.5f);
|
||||
const float Dm700 = World.Generator->GetDensityAt(P.X, P.Y, -700.0f);
|
||||
const float Dm6995 = World.Generator->GetDensityAt(P.X, P.Y, -699.5f);
|
||||
const float Dm697 = World.Generator->GetDensityAt(P.X, P.Y, -697.0f);
|
||||
float MinBody = FLT_MAX;
|
||||
int32 NumSolidBody = 0;
|
||||
int32 SolidOffsetX = 0;
|
||||
int32 SolidOffsetY = 0;
|
||||
int32 SolidBodyRow = -1;
|
||||
for (int32 OffsetY = -1; OffsetY <= 1; ++OffsetY)
|
||||
{
|
||||
for (int32 OffsetX = -1; OffsetX <= 1; ++OffsetX)
|
||||
{
|
||||
for (int32 BodyRow = 0; BodyRow < 8; ++BodyRow)
|
||||
{
|
||||
const float BodyDensity = World.Generator->GetDensityAt(
|
||||
P.X + static_cast<float>(OffsetX),
|
||||
P.Y + static_cast<float>(OffsetY),
|
||||
-699.5f + static_cast<float>(BodyRow));
|
||||
if (BodyDensity < MinBody)
|
||||
{
|
||||
MinBody = BodyDensity;
|
||||
SolidOffsetX = OffsetX;
|
||||
SolidOffsetY = OffsetY;
|
||||
SolidBodyRow = BodyRow;
|
||||
}
|
||||
NumSolidBody += BodyDensity <= 0.0f ? 1 : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeExplore][TemporaryShaftProbe] t=%.2f xy=(%.2f,%.2f) "
|
||||
"d[-704,-702,-700.5,-700,-699.5,-697]=[%.3f,%.3f,%.3f,%.3f,%.3f,%.3f] "
|
||||
"bodyMin=%.3f bodySolid=%d/72 firstMinOffset=(%d,%d) row=%d"),
|
||||
T, P.X, P.Y, Dm704, Dm702, Dm7005, Dm700, Dm6995, Dm697,
|
||||
MinBody, NumSolidBody, SolidOffsetX, SolidOffsetY, SolidBodyRow);
|
||||
}
|
||||
}
|
||||
|
||||
FVoxelStrateMeasureSettings Settings;
|
||||
Settings.SampleStep = 1;
|
||||
Settings.MaxCells = Arguments.MaxWalkCells;
|
||||
@@ -1316,9 +1493,86 @@ bool RunWalk(
|
||||
Settings.CoverPointB = FVector2D(OutOutput.DepartureVoxels.X, OutOutput.DepartureVoxels.Y);
|
||||
Settings.CoverMarginVoxels = FMath::CeilToFloat(
|
||||
Settings.PlayerCapsuleRadiusVoxels) + 2.0f;
|
||||
|
||||
// A mouth-AABB is not a landing window: the landing door and its local apron can legitimately
|
||||
// leave that box before the player reaches the next room. Expand the same two-point box just
|
||||
// enough to contain the selected production mouth envelope. This is still a bounded,
|
||||
// deterministic measurement window; the central inter-strate descent belongs to the adjacent
|
||||
// strata and is not folded into this intra-strate walk question.
|
||||
const float WindowMinX = FMath::Min(OutOutput.ArrivalVoxels.X, OutOutput.DepartureVoxels.X);
|
||||
const float WindowMaxX = FMath::Max(OutOutput.ArrivalVoxels.X, OutOutput.DepartureVoxels.X);
|
||||
const float WindowMinY = FMath::Min(OutOutput.ArrivalVoxels.Y, OutOutput.DepartureVoxels.Y);
|
||||
const float WindowMaxY = FMath::Max(OutOutput.ArrivalVoxels.Y, OutOutput.DepartureVoxels.Y);
|
||||
const auto IncludeTargetPassageEnvelope = [&](
|
||||
const FVoxelPassage* Passage,
|
||||
const FVoxelPassageLanding& Landing)
|
||||
{
|
||||
if (Passage == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const auto IncludeXY = [&](const FVector& Point, float Radius)
|
||||
{
|
||||
if (!FMath::IsFinite(Point.X) || !FMath::IsFinite(Point.Y)
|
||||
|| !FMath::IsFinite(Point.Z) || !FMath::IsFinite(Radius))
|
||||
{
|
||||
return;
|
||||
}
|
||||
const float AbsRadius = FMath::Abs(Radius);
|
||||
// Include control points whose centreline belongs to the measured strate. A control
|
||||
// point just outside the boundary belongs to the adjacent strate's descent; pulling
|
||||
// that whole switchback into this bounded walk box can multiply the sampled volume
|
||||
// without improving the intra-strate mouth-to-mouth question.
|
||||
if (Point.Z < static_cast<float>(World.TargetBottomWorldZ)
|
||||
|| Point.Z > static_cast<float>(World.TargetTopWorldZ))
|
||||
{
|
||||
return;
|
||||
}
|
||||
const float ExcessX = FMath::Max(
|
||||
0.0f,
|
||||
FMath::Max(
|
||||
WindowMinX - (Point.X + AbsRadius),
|
||||
(Point.X - AbsRadius) - WindowMaxX));
|
||||
const float ExcessY = FMath::Max(
|
||||
0.0f,
|
||||
FMath::Max(
|
||||
WindowMinY - (Point.Y + AbsRadius),
|
||||
(Point.Y - AbsRadius) - WindowMaxY));
|
||||
Settings.CoverMarginVoxels = FMath::Max(
|
||||
Settings.CoverMarginVoxels,
|
||||
FMath::Max(ExcessX, ExcessY));
|
||||
};
|
||||
|
||||
IncludeXY(
|
||||
Landing.StandingPoint,
|
||||
Landing.HalfWidth + VoxelPassageGeometry::LandingCarveBlendVoxels);
|
||||
// The first/last three points are the production mouth, apron, and first slope section.
|
||||
// Do not pull the whole inter-strate switchback into the bounded target-strate test.
|
||||
const int32 NumLocalPoints = FMath::Min(3, Passage->ControlPoints.Num());
|
||||
const bool bUseUpperEnd = &Landing == &Passage->UpperLanding;
|
||||
const int32 FirstPoint = bUseUpperEnd
|
||||
? 0 : FMath::Max(0, Passage->ControlPoints.Num() - NumLocalPoints);
|
||||
const int32 LastPoint = bUseUpperEnd
|
||||
? NumLocalPoints : Passage->ControlPoints.Num();
|
||||
for (int32 PointIndex = FirstPoint; PointIndex < LastPoint; ++PointIndex)
|
||||
{
|
||||
const float Radius = Passage->ControlRadii.IsValidIndex(PointIndex)
|
||||
? Passage->ControlRadii[PointIndex] : Passage->Radius;
|
||||
IncludeXY(Passage->ControlPoints[PointIndex], Radius
|
||||
+ VoxelPassageGeometry::LandingCarveBlendVoxels);
|
||||
}
|
||||
};
|
||||
IncludeTargetPassageEnvelope(
|
||||
ArrivalPassage,
|
||||
ArrivalPassage != nullptr ? ArrivalPassage->LowerLanding : FVoxelPassageLanding());
|
||||
IncludeTargetPassageEnvelope(
|
||||
DeparturePassage,
|
||||
DeparturePassage != nullptr ? DeparturePassage->UpperLanding : FVoxelPassageLanding());
|
||||
Settings.bIncludeOriginInCoverWindow = NeedsOriginInWindow(Arguments.Archetype);
|
||||
|
||||
TArray<FVector> LandingRoomProbePoints;
|
||||
|
||||
auto AppendLandingRoomProbes = [&LandingRoomProbePoints](
|
||||
const FVoxelPassageLanding& Landing)
|
||||
{
|
||||
@@ -1386,9 +1640,12 @@ bool RunWalk(
|
||||
}
|
||||
const int32 ArrivalProbeCount = ArrivalPassage != nullptr ? 5 : 0;
|
||||
const int32 DepartureProbeCount = DeparturePassage != nullptr ? 5 : 0;
|
||||
const int32 LandingProbeCount = ArrivalProbeCount + DepartureProbeCount;
|
||||
OutOutput.ArrivalLandingRoomProbeCount = ArrivalProbeCount;
|
||||
OutOutput.DepartureLandingRoomProbeCount = DepartureProbeCount;
|
||||
for (int32 ProbeIndex = 0; ProbeIndex < OutOutput.Report.ComponentProbePlayerFit.Num();
|
||||
for (int32 ProbeIndex = 0;
|
||||
ProbeIndex < FMath::Min(LandingProbeCount,
|
||||
OutOutput.Report.ComponentProbePlayerFit.Num());
|
||||
++ProbeIndex)
|
||||
{
|
||||
const bool bPlayerFit = OutOutput.Report.ComponentProbePlayerFit[ProbeIndex] != 0u;
|
||||
@@ -1408,7 +1665,6 @@ bool RunWalk(
|
||||
bInArrivalComponent ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
OutOutput.Status = TEXT("ok");
|
||||
return true;
|
||||
}
|
||||
@@ -1829,11 +2085,15 @@ FString BuildExploreJson(
|
||||
Writer->WriteValue(TEXT("bisection_iterations"), Output.Render.BisectionIterations);
|
||||
Writer->WriteValue(TEXT("human_marker_height_m"), 1.76);
|
||||
Writer->WriteValue(TEXT("step_rationale"), Output.Render.StepRationale);
|
||||
Writer->WriteValue(TEXT("camera_seed_policy"), Output.Render.CameraSeedPolicy);
|
||||
Writer->WriteValue(TEXT("camera_seed_count"), Output.Render.CameraSeedCount);
|
||||
Writer->WriteValue(TEXT("all_cameras_player_fit"), Output.Render.bAllCamerasPlayerFit);
|
||||
Writer->WriteArrayStart(TEXT("images"));
|
||||
for (const FExploreRenderFrame& Frame : Output.Render.Frames)
|
||||
{
|
||||
Writer->WriteObjectStart();
|
||||
Writer->WriteValue(TEXT("purpose"), Frame.Purpose);
|
||||
Writer->WriteValue(TEXT("camera_seed_source"), Frame.CameraSeedSource);
|
||||
Writer->WriteValue(TEXT("file"), Frame.FileName);
|
||||
WriteJsonVector(*Writer, TEXT("camera_voxels"), Frame.CameraVoxels, 1.0f);
|
||||
WriteJsonVector(*Writer, TEXT("target_voxels"), Frame.TargetVoxels, 1.0f);
|
||||
@@ -2102,16 +2362,8 @@ int32 UVoxelForgeExploreCommandlet::Main(const FString& Params)
|
||||
|
||||
FExploreRunOutput Output;
|
||||
bool bRequestedModeFailed = false;
|
||||
if (Arguments.bRender)
|
||||
{
|
||||
const double Start = FPlatformTime::Seconds();
|
||||
if (!RunRender(Arguments, World, Output.Render))
|
||||
{
|
||||
bRequestedModeFailed = true;
|
||||
}
|
||||
UE_LOG(LogTemp, Display, TEXT("[VoxelForgeExplore] render %.3fs (%s)"),
|
||||
FPlatformTime::Seconds() - Start, *Output.Render.Status);
|
||||
}
|
||||
FExploreWalkOutput RenderSeedWalk;
|
||||
const FExploreWalkOutput* CameraSeedWalk = nullptr;
|
||||
if (Arguments.bWalk)
|
||||
{
|
||||
const double Start = FPlatformTime::Seconds();
|
||||
@@ -2121,6 +2373,31 @@ int32 UVoxelForgeExploreCommandlet::Main(const FString& Params)
|
||||
}
|
||||
UE_LOG(LogTemp, Display, TEXT("[VoxelForgeExplore] walk %.3fs (%s)"),
|
||||
FPlatformTime::Seconds() - Start, *Output.Walk.Status);
|
||||
CameraSeedWalk = &Output.Walk;
|
||||
}
|
||||
else if (Arguments.bRender)
|
||||
{
|
||||
// A render-only invocation still has to obey the inside-walkable-space contract. Run the
|
||||
// same focused walk as a private seed pass; it is intentionally omitted from the render-only
|
||||
// JSON so the selected mode remains render.
|
||||
const double Start = FPlatformTime::Seconds();
|
||||
if (!RunWalk(Arguments, World, RenderSeedWalk))
|
||||
{
|
||||
bRequestedModeFailed = true;
|
||||
}
|
||||
UE_LOG(LogTemp, Display, TEXT("[VoxelForgeExplore] render camera seed walk %.3fs (%s)"),
|
||||
FPlatformTime::Seconds() - Start, *RenderSeedWalk.Status);
|
||||
CameraSeedWalk = &RenderSeedWalk;
|
||||
}
|
||||
if (Arguments.bRender)
|
||||
{
|
||||
const double Start = FPlatformTime::Seconds();
|
||||
if (CameraSeedWalk == nullptr || !RunRender(Arguments, World, *CameraSeedWalk, Output.Render))
|
||||
{
|
||||
bRequestedModeFailed = true;
|
||||
}
|
||||
UE_LOG(LogTemp, Display, TEXT("[VoxelForgeExplore] render %.3fs (%s)"),
|
||||
FPlatformTime::Seconds() - Start, *Output.Render.Status);
|
||||
}
|
||||
if (Arguments.bFailureFocusRender && Arguments.bRender && Arguments.bWalk)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user