feat(tunnels): tunnels get the floor rooms always had -- capability up, 30% slower (fix known)

The owner asked the question that unlocked this: "isn't the floor cut a modification
made to rooms afterward, instead of having them be naturally like so?"

Reading the code answered it, and the answer was that rooms and tunnels were built on
OPPOSITE principles:
  room   = SmoothMax(sphereSDF, floorHalfSpace), floor part of the SHAPE, evaluated
           in one pass, with two octaves of Perlin relief already on the floor height
           (FloorReliefStrength / RoomFloorCutMin=0.70, hash-rolled per room)
  tunnel = a bare capsule with NO floor, then CaveTunnelFloorOp + support columns +
           IsTunnelSupportFloorWorldPoint asserting a slab in afterwards

That is why tunnels read as "forced flat" and rooms never did, and why the previous
conditional-floor round could only find 1,539 skips in 1.5M tests: it was optimising
a correction instead of removing the need for one.

WHAT LANDED
Corridors now use a swept SmoothMax(capsule, floor): endpoint floor heights derived
from the rooms they join, deterministically interpolated along the control chain, the
existing two-octave relief reused (not reimplemented) and faded at mouths, and
TERRACES where a room-to-room transition is too steep -- risers capped at 1.71 voxels
(42.75 cm), with relief disabled on risers so the step bound holds.

  player-fit cells   20,153 -> 20,835   (+682)
  reachable cells    10,483 -> 10,901   (+418)
  connectivity       connected, both;  mouth gap 0
  vertices/tris      40,623/75,776 -> 40,429/75,416
  dihedral p50/p90   2.5429/33.8499 -> 2.5529/33.7191 deg

Capability went UP, which is the point: the shape now provides what the correction
was faking, and it reaches places the slab did not.

RETIRED: the 1.5M-call natural-floor validator and its counters, plus the now-dead
CaveTunnelFloorOp / CaveTunnelAirOp implementations and profiler labels.

NOT retired: the finite support-column backstop. Removing it failed hard --
19,097 fit, 396 reachable, DISCONNECTED. It stays, and Codex was right not to force it.

⚠️ THE HONEST COST: 30% SLOWER
  my own build, 3 runs:  1.509 / 1.542 / 1.543 s   (was 1.118 / 1.131 / 1.197 s)
Well above the 17% noise floor. The relief evaluation is what costs.

⭐ And the fix is already identified: the relief is `FMath::PerlinNoise2D` -- a
function of (X,Y) ONLY -- yet it is evaluated per 3D sample. That is exactly the bug
pattern the support floor had, where making the decision per-column took it from
2.29 to 0.138 us/sample (16x). Caching relief per XY column should recover most or
all of the 30%. Next round.

Renders confirm the floors now undulate gently instead of presenting a flat plane;
the global relief metric is wall-dominated and inconclusive, and Codex correctly
declined to claim the visual complaint solved on that basis.

Determinism holds: both final runs hash 4520DC6F with canonical repeat equality.
Analytically the authored floor profile is bounded by tan(44 deg) including the
Perlin derivative and mouth envelope; the full post-modifier SDF still is not
provable, so the player-fit audit stays.

Build 0 errors. Nothing outside Plugins/VoxelForge; RealtimeMeshComponent untouched.

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-10 18:00:11 +02:00
1 parent 383ec86c37
commit ff8bd7a8bc
9 files changed
+461 -586

No files matched your search

+22 -51
View File
@@ -507,11 +507,7 @@ namespace
static void BuildTunnelSupportColumnsForChunk(
FChunkSDFCache& Cache,
const FIntVector& ChunkCoord,
const UVoxelGenerator* Generator,
const FStrateGenerationParams& Params,
uint32 ParamsFingerprint,
uint32 LayoutVersion);
const FIntVector& ChunkCoord);
static const FTunnelSupportFloorColumn* FindTunnelSupportColumn(
const FChunkSDFCache& Cache,
@@ -607,7 +603,6 @@ namespace
}
void PrepareTunnelCoreCache(
const UVoxelGenerator& Generator,
const UVoxelStrateManager& Manager,
const FIntVector& ChunkCoord,
const FStrateGenerationParams& Params,
@@ -644,8 +639,7 @@ namespace
Params, static_cast<uint32>(Manager.GetWorldSeed()),
StrateIndex, nullptr);
BuildTunnelSupportColumnsForChunk(
OutState.Cache, ChunkCoord, &Generator, Params,
ParamsFingerprint, LayoutVersion);
OutState.Cache, ChunkCoord);
OutState.OwnerId = OwnerId;
OutState.ManagerLifetimeId = ManagerLifetimeId;
OutState.Chunk = ChunkCoord;
@@ -656,11 +650,7 @@ namespace
static void BuildTunnelSupportColumnsForChunk(
FChunkSDFCache& Cache,
const FIntVector& ChunkCoord,
const UVoxelGenerator* Generator,
const FStrateGenerationParams& Params,
uint32 ParamsFingerprint,
uint32 LayoutVersion)
const FIntVector& ChunkCoord)
{
const int32 MinX = ChunkCoord.X * CHUNK_SIZE - 1;
const int32 MinY = ChunkCoord.Y * CHUNK_SIZE - 1;
@@ -672,25 +662,6 @@ namespace
Cache.SupportColumnEntries.Init(INDEX_NONE, Cells * Cells);
Cache.SupportColumns.Reset();
// The conditional floor is tested against the exact density pipeline with only the
// graph support slab disabled. The sampler is synchronous and lives only for this cache
// build; the immutable cache stores the resulting interval decision, never the callback.
const auto SampleWithoutTunnelSupportFloor =
[Generator, &Params, ParamsFingerprint, LayoutVersion](
float X, float Y, float Z) -> float
{
return Generator != nullptr
? Generator->GetDensityWithParams(
X, Y, Z, Params, ParamsFingerprint, LayoutVersion,
/*bApplyLegacyStructuralPosts=*/true,
/*bApplyTunnelSupportFloor=*/false)
: 0.0f;
};
FTunnelSupportFloorDensitySampler NaturalSamplerRef =
SampleWithoutTunnelSupportFloor;
const FTunnelSupportFloorDensitySampler* NaturalSampler =
Generator != nullptr ? &NaturalSamplerRef : nullptr;
// Empty columns are represented only by INDEX_NONE in the slot map, so
// each stored column retains the inline bands for actual graph tubes.
for (int32 Y = 0; Y < Cells; ++Y)
@@ -702,11 +673,7 @@ namespace
static_cast<float>(MinX + X),
static_cast<float>(MinY + Y),
Cache,
Column,
NaturalSampler,
Params.StrateTopWorldZ,
Params.StrateBottomWorldZ,
Params.BoundarySealThickness);
Column);
if (Column.Intervals.Num() == 0)
{
continue;
@@ -770,8 +737,7 @@ namespace
*/
bool VF_BuildOpStackForChunk(ECaveGeneratorType Type, FVoxelStackParamRefs& Refs,
int32 Seed, float SpineRadius, const UVoxelStrateManager* SM,
FVoxelOpStack& OutStack, FVoxelOpContext& OutCtx,
const UVoxelGenerator* Generator = nullptr)
FVoxelOpStack& OutStack, FVoxelOpContext& OutCtx)
{
switch (Type)
{
@@ -830,7 +796,7 @@ namespace
OutCtx.StrateBottomWorldZ = Refs.Tunnel->StrateBottomWorldZ;
VoxelDensityOps::BuildTunnelNetworkStack(
OutStack, *Refs.Tunnel, Seed, SpineRadius, SM,
/*bAppendStructuralPosts=*/true, Generator);
/*bAppendStructuralPosts=*/true);
return true;
default:
@@ -1296,7 +1262,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
CP_UseOpStack = VF_BuildOpStackForChunk(
CP_GenType, Refs, Seed, OriginSpineRadius,
StrateManager, CP_OpStack, OpCtx, this);
StrateManager, CP_OpStack, OpCtx);
// Le test appelle PrepareChunk, pas la production : c'est exactement la divergence
// qui rend un opérateur vert en test et faux en jeu. Les sept `PrepareChunk`
@@ -1330,7 +1296,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
if (bCanPrepareNativeTunnelCore)
{
PrepareTunnelCoreCache(
*this, *StrateManager, ChunkCoord, CP_Tunnel, CP_TunnelFP,
*StrateManager, ChunkCoord, CP_Tunnel, CP_TunnelFP,
LayoutVersion, ManagerLifetimeId, DensityCacheOwnerId, GTunnelCoreCache);
}
else
@@ -1574,6 +1540,9 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
if (bHaveTunnelCore)
{
const bool bTunnelSupportFloor = TunnelCore.bSupportFloor;
// The swept capsule owns the relief profile, while this finite support band is
// the conservative post-disturbance backstop that keeps terrain modifiers from
// removing the only player support at a mouth or a narrow cross-section.
if (bTunnelSupportFloor)
{
Result = FMath::Min(
@@ -1684,8 +1653,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float WorldZ,
const FStrateGenerationParams& Params,
uint32 ParamsFingerprint, uint32 LayoutVersion,
bool bApplyLegacyStructuralPosts,
bool bApplyTunnelSupportFloor) const
bool bApplyLegacyStructuralPosts) const
{
//=========================================================================
// STRATE DENSITY FUNCTION (Morphology Pipeline)
@@ -2222,8 +2190,10 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
//
// We use the cached SDF so this costs two extra SDF evaluations per voxel,
// only when near a surface — the common case is cheap (DistFromSurface > TerraceRange).
float SDF_Zp1 = VoxelCaveMorphology::EvaluateSDFCached(WorldX, WorldY, WorldZ + 1.0f, SDFCache, Params.SDFBlendRadius);
float SDF_Zm1 = VoxelCaveMorphology::EvaluateSDFCached(WorldX, WorldY, WorldZ - 1.0f, SDFCache, Params.SDFBlendRadius);
float SDF_Zp1 = VoxelCaveMorphology::EvaluateSDFCached(
WorldX, WorldY, WorldZ + 1.0f, SDFCache, Params.SDFBlendRadius);
float SDF_Zm1 = VoxelCaveMorphology::EvaluateSDFCached(
WorldX, WorldY, WorldZ - 1.0f, SDFCache, Params.SDFBlendRadius);
float GZ = (SDF_Zp1 - SDF_Zm1) * 0.5f;
// Normalized vertical component: 1 = perfectly horizontal surface (floor/ceiling)
// 0 = perfectly vertical surface (wall)
@@ -2748,12 +2718,13 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
if (bApplyLegacyStructuralPosts)
{
// Terrain operations and passage support floors are allowed to write solid density, but
// the graph's own tunnel floor/core are structural route geometry. Direct callers of
// the graph's own tunnel floor/core are structural route geometry. Direct callers of
// GetDensityWithParams retain this historical tail; GetDensityAt skips it and applies the
// same predicates once in its common MC-space post section below both generation paths.
const bool bTunnelSupportFloor = bApplyTunnelSupportFloor
&& VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
WorldX, WorldY, WorldZ, SDFCache);
const bool bTunnelSupportFloor = VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
WorldX, WorldY, WorldZ, SDFCache);
// Keep the direct legacy entry point capability-safe for callers outside the canonical
// operator-stack path; its support band follows the same relieved swept floor.
if (bTunnelSupportFloor)
{
Density = FMath::Max(Density, FMath::Max(Params.BaseDensity * 2.0f, 1.0f));
@@ -4791,7 +4762,7 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
FVoxelOpStack TileStack;
if (!VF_BuildOpStackForChunk(CaveType, Refs, Seed, OriginSpineRadius,
StrateManager, TileStack, OpCtx, this))
StrateManager, TileStack, OpCtx))
{
// Strate dégénérée ou archétype non porté : `GetDensityAt` retomberait sur le `switch`,
// donc la pile ne décrit pas ce que le mesher verra. Aucun verdict.