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:
Fr0zkaandClaude Opus 5 committed 2026-09-08 14:14:29 +02:00
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
+314 -76
View File
@@ -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));
}
}
+198 -52
View File
@@ -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);
+47 -150
View File
@@ -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
+145 -58
View File
@@ -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
+86 -44
View File
@@ -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);
}
}
}
+215 -7
View File
@@ -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;
+4 -3
View File
@@ -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.
+16 -17
View File
@@ -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)
{