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