feat(gen): Jahni's playtest corrections -- rooms flattened, roads deleted
From walking a TunnelNetwork preset at (0,0). Four corrections, and two of
them were the same mistake.
2a. The (0,0) landing room is sized to host a FUTURE straight vertical
shaft from the surface, with flooring around the origin itself. The
room is the landing for a shaft the player digs later.
2b. The inter-strate descent no longer starts at the spine -- placed by
deterministic hash within a radius around (0,0). The spine room is for
the surface shaft; the descent is elsewhere.
2c. ⛔ DELETED the landing->hub connector roads from 413c15d. Jahni:
"its straight up making other tunnels straightly through everything...
they have nothing to do here." Zero references remain.
2d. ⭐ FLATTEN THE ROOM JOINS instead. Where two spherical rooms intersect
and leave a lip above head height, the join is brought down to a
walkable level.
⭐ 2c AND 2d ARE ONE CORRECTION. I solved "rooms do not connect" by
TUNNELLING BETWEEN THEM when the answer was MAKING THEIR FLOORS MEET. The
81-run sweep had already said so -- zero narrow-gap events, every failure a
vertical severance, renders showing "a sharp diagonal ledge descending into
black open space". A radial corridor to the origin could never have fixed
that; flattening the joins is aimed at exactly it.
2e. Descent tunnel 1.5x wider, per "1.5x bigger wouldn't be too much".
2f. Corridors WANDER. Jahni, from the renders: "its not a maze, its still a
latice made straight paths." The spanning tree changed WHICH edges
exist, not what an edge looks like -- every corridor was a straight
axis-aligned capsule, every junction 90 degrees. Now hash-jittered
chains, seam-safe like the tree.
RENDERER: 192x128 -> 512x288, and cameras are now seeded from the
player-fit set -- "a density-checked pose from the exact player-fit walk
mask". Previously they sat INSIDE ROCK looking at back-faces, which Jahni
spotted instantly and no metric could. First genuinely legible pictures of
this world.
ParallelFor landed at 6 sites in VoxelStrateMeasure before I stopped the
run; the rest of that work continues next.
⚠️ AND THE RENDERER IS ARCHITECTURALLY WRONG, per Jahni: "why, in engine,
does it take a minute to have clear view of thousands of angles, but in our
renderer it takes ages?" Because the engine MESHES ONCE and rasterises --
~32k density calls per chunk, then every angle is free. I RAYMARCH: tens of
millions of density calls PER IMAGE, nothing reused between frames. We
already have the mesher and an export mode. Next task renders from the
mesh.
Also from him: runs must stay under 30 minutes. This one hit 33 and
produced 2 images.
Build clean. Committed as a checkpoint -- the task was stopped mid-Part-3,
not finished.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
This commit is contained in:
1 parent
55bf807f60
commit
d97373c5a8
18 files changed
+2433
-1021
No files matched your search
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user