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

@@ -3591,10 +3591,8 @@ namespace
{
public:
FRoomGraphSource(const FStrateGenerationParams& InP, int32 InSeed,
const UVoxelStrateManager* InManager,
const UVoxelGenerator* InGenerator = nullptr)
const UVoxelStrateManager* InManager)
: P(InP), Seed(InSeed), SeedU((uint32)InSeed), Manager(InManager)
, Generator(InGenerator)
, ManagerLifetimeId(InManager ? InManager->GetCacheLifetimeId() : 0)
, ParamsFingerprint(FCrc::MemCrc32(&InP, sizeof(InP)))
{}
@@ -3788,40 +3786,13 @@ 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;
}
void BuildSupportColumn(
float WorldX, float WorldY,
const FChunkSDFCache& Cache,
FTunnelSupportFloorColumn& OutColumn) const
{
if (Generator == nullptr)
{
VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
WorldX, WorldY, Cache, OutColumn);
return;
}
// This is a synchronous, non-owning view of the exact field with only the graph
// support slab disabled. BuildTunnelSupportFloorColumn consumes it immediately and
// retains only the proven interval decision in the worker-local column cache.
const auto SampleWithoutTunnelSupportFloor =
[this](float X, float Y, float Z) -> float
{
return Generator->GetDensityWithParams(
X, Y, Z, P, ParamsFingerprint, LayoutVersion,
/*bApplyLegacyStructuralPosts=*/true,
/*bApplyTunnelSupportFloor=*/false);
};
FTunnelSupportFloorDensitySampler NaturalSampler =
SampleWithoutTunnelSupportFloor;
VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
WorldX, WorldY, Cache, OutColumn, &NaturalSampler,
P.StrateTopWorldZ, P.StrateBottomWorldZ,
P.BoundarySealThickness);
WorldX, WorldY, Cache, OutColumn);
}
const FTunnelSupportFloorColumn* GetSupportColumn(const FVector& Position) const
@@ -3857,18 +3828,6 @@ namespace
return &S.FractionalSupportColumn;
}
float GetTunnelCoreSDF() const
{
FState& S = State();
const FVector& Position = S.LastWorldPosition;
const FTunnelSupportFloorColumn* SupportColumn = GetSupportColumn(Position);
S.LastTunnelCoreWorldEvaluation = VoxelCaveMorphology::EvaluateTunnelCoreWorld(
Position.X, Position.Y, Position.Z, GetCache(), SupportColumn);
S.LastTunnelCoreEvaluationPosition = Position;
S.bLastTunnelCoreWorldEvaluationValid = true;
return S.LastTunnelCoreWorldEvaluation.SDF;
}
bool TryGetLastTunnelCoreWorldEvaluation(
FTunnelCoreWorldEvaluation& OutEvaluation) const override
{
@@ -4200,7 +4159,8 @@ namespace
}
float CaveSDF = VoxelCaveMorphology::EvaluateSDFCached(
WarpedX, WarpedY, WarpedZ, GetCache(), P.SDFBlendRadius, &S.NearestRoom);
WarpedX, WarpedY, WarpedZ, GetCache(), P.SDFBlendRadius,
&S.NearestRoom);
//---------------------------------------------------------------
// PITS & CHEMINÉES — coordonnées RÉELLES, SmoothMin dans le même canal SDF
@@ -6640,131 +6600,16 @@ namespace
const TCHAR* DebugName() const override { return TEXT("RoomGraphSource"); }
bool IsTunnelSupportFloor() const
{
FState& S = State();
const FVector& Position = S.LastWorldPosition;
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelSupportFloorQueries);
}
const FTunnelSupportFloorColumn* Column = GetSupportColumn(Position);
const bool bSupport = Column != nullptr
&& VoxelCaveMorphology::IsTunnelSupportFloorColumnZ(
static_cast<float>(Position.Z), *Column);
S.LastTunnelCoreWorldEvaluation = FTunnelCoreWorldEvaluation();
S.LastTunnelCoreWorldEvaluation.bSupportFloor = bSupport;
S.LastTunnelCoreEvaluationPosition = Position;
S.bLastTunnelCoreWorldEvaluationValid = true;
return bSupport;
}
private:
FStrateGenerationParams P;
int32 Seed;
uint32 SeedU;
TWeakObjectPtr<const UVoxelStrateManager> Manager;
const UVoxelGenerator* Generator = nullptr;
uint64 ManagerLifetimeId = 0;
uint32 ParamsFingerprint;
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
//=========================================================================
@@ -9442,8 +9287,7 @@ namespace VoxelDensityOps
void BuildTunnelNetworkStack(FVoxelOpStack& OutStack, const FStrateGenerationParams& P,
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts,
const UVoxelGenerator* Generator)
bool bAppendStructuralPosts)
{
// ⚠️ ÉTAPES A + B + C1 — LA PILE EST COMPLÈTE POUR CET ARCHÉTYPE.
// Portés : échelle verticale, roc de base, warp, graphe de salles (+ pits + cheminées),
@@ -9488,7 +9332,7 @@ namespace VoxelDensityOps
// La source de salles est retenue par pointeur non possédant : les terrasses re-interrogent
// son cache SDF en Z±1. Même motif que `FShaftFieldSource` → `FShaftLedgeMod`.
TUniquePtr<FRoomGraphSource> RoomSource = MakeUnique<FRoomGraphSource>(
P, Seed, StrateManager, Generator);
P, Seed, StrateManager);
const FRoomGraphSource* RoomPtr = RoomSource.Get();
OutStack.Add(MakeConstantRockSource(P.BaseDensity));