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

+413 -344
View File
@@ -33,26 +33,14 @@
#include "VoxelStrateMeasure.h" #include "VoxelStrateMeasure.h"
#include "VoxelTypes.h" // Pour VOXEL_NOISE_SCALE, SmoothStep01 #include "VoxelTypes.h" // Pour VOXEL_NOISE_SCALE, SmoothStep01
#include "VoxelStrateTypes.h" #include "VoxelStrateTypes.h"
namespace
{
using FVFPlayerDensitySampler = FTunnelSupportFloorDensitySampler;
bool VF_ValidatePlayerFitPose(
const FVoxelStrateMeasureSettings& Settings,
const FVector& CandidateFeetPoint,
float StrateTopZ,
float StrateBottomZ,
float BoundarySealThickness,
FVFPlayerDensitySampler SampleDensity,
FVector& OutPoint);
}
#include "VoxelNoise.h" // Pure FBM used by the slab landing query #include "VoxelNoise.h" // Pure FBM used by the slab landing query
#include "VoxelPassageGeometry.h" #include "VoxelPassageGeometry.h"
#include "VoxelTerrainOpDefinition.h" #include "VoxelTerrainOpDefinition.h"
namespace namespace
{ {
using FVFPlayerDensitySampler = TFunctionRef<float(float, float, float)>;
// Density coordinates and cached cull radii are float-valued. FVector is double-valued in // Density coordinates and cached cull radii are float-valued. FVector is double-valued in
// UE5, so using FVector::DistSquared here needlessly widens every candidate reject. Keep the // UE5, so using FVector::DistSquared here needlessly widens every candidate reject. Keep the
// cull in the same build-fixed float domain as the density query; the exact field output is // cull in the same build-fixed float domain as the density query; the exact field output is
@@ -83,171 +71,394 @@ namespace
return DX * DX + DY * DY + DZ * DZ; return DX * DX + DY * DY + DZ * DZ;
} }
// A graph tunnel's rounded capsule is a candidate natural floor only in the part of the tube struct FVFTunnelShapeEvaluation
// where the player's complete support footprint remains inside a sufficiently wide,
// sufficiently flat cross-section. This analytic prefilter only saves density probes; it is
// never the decision by itself. The exact no-floor field is passed through the same player-fit
// validator used by the capability measurement, and failure retains the old support slab.
bool VF_IsNaturalTunnelFloorWalkable(
const FCachedTunnel& Tunnel,
float WorldX,
float WorldY,
float FloorZ,
float SupportRadius,
int32 SegmentIndex,
const FTunnelSupportFloorDensitySampler* NaturalSampler,
float StrateTopZ,
float StrateBottomZ,
float BoundarySealThickness)
{ {
if (SegmentIndex < 0 float SDF = FLT_MAX;
|| SegmentIndex + 1 >= Tunnel.WorldControlPoints.Num() bool bSupportFloor = false;
|| Tunnel.WorldControlRadii.Num() != Tunnel.WorldControlPoints.Num()) };
// FMath::PerlinNoise2D is the same two-octave field used by room floors. The derivative bound
// below is deliberately loose: each Grad2 component is in [-1, 1], the interpolation
// difference is bounded by four, and SmoothCurve' <= 1.875. Keeping this bound conservative
// is what lets the corridor use the authored relief without turning the walkability promise
// into a sampling accident.
constexpr float VF_Perlin2DPartialAbsBound = 8.5f;
constexpr float VF_MaxTunnelFloorReliefScale = 0.20f;
// Keep corridor relief on the exact two-octave field already used by room floors. The
// endpoint fade is not a second noise system: it is the landing apron that lets the swept
// corridor meet the room's own floor without a seed-dependent vertical lip.
FORCEINLINE float VF_TunnelFloorRelief(
float X, float Y, const FCachedTunnel& Tunnel,
float Envelope, float ReliefScale)
{
if (!(Tunnel.FloorReliefStrength > 0.0f)
|| !FMath::IsFinite(Tunnel.FloorReliefStrength)
|| !FMath::IsFinite(Tunnel.FloorReliefFrequency)
|| !(Envelope > 0.0f)
|| !(ReliefScale > 0.0f))
{ {
return false; return 0.0f;
} }
const FVector& Start = Tunnel.WorldControlPoints[SegmentIndex]; const float RF = Tunnel.FloorReliefFrequency;
const FVector& End = Tunnel.WorldControlPoints[SegmentIndex + 1]; const float SF = static_cast<float>(Tunnel.FloorSeed) * 0.00001f;
const float StartRadius = FMath::Abs(Tunnel.WorldControlRadii[SegmentIndex]); float Noise = FMath::PerlinNoise2D(
const float EndRadius = FMath::Abs(Tunnel.WorldControlRadii[SegmentIndex + 1]); FVector2D(X * RF + SF, Y * RF + SF * 1.7f)) * 0.65f;
const FVector2D Delta(End.X - Start.X, End.Y - Start.Y); Noise += FMath::PerlinNoise2D(
FVector2D(X * RF * 2.3f + SF * 3.1f,
Y * RF * 2.3f + SF * 5.3f)) * 0.35f;
return Noise * VOXEL_NOISE_SCALE * Tunnel.FloorReliefStrength
* Envelope * ReliefScale;
}
FORCEINLINE bool VF_TunnelFloorNeedsTerrace(
const FVector& A, float RadiusA,
const FVector& B, float RadiusB)
{
const float HorizontalRun = FVector2D(
static_cast<float>(B.X - A.X), static_cast<float>(B.Y - A.Y)).Size();
const float FloorDelta = FMath::Abs(
VoxelPassageGeometry::TunnelFloorZ(B, FMath::Abs(RadiusB))
- VoxelPassageGeometry::TunnelFloorZ(A, FMath::Abs(RadiusA)));
return HorizontalRun > KINDA_SMALL_NUMBER
&& FloorDelta > HorizontalRun
* VoxelPassageGeometry::PlayerWalkableFloorMaxGradient;
}
FORCEINLINE float VF_TunnelFloorProfileT(
const FVector& A, float RadiusA,
const FVector& B, float RadiusB,
float T)
{
const float FloorA = VoxelPassageGeometry::TunnelFloorZ(A, FMath::Abs(RadiusA));
const float FloorB = VoxelPassageGeometry::TunnelFloorZ(B, FMath::Abs(RadiusB));
const float HorizontalRun = FVector2D(
static_cast<float>(B.X - A.X), static_cast<float>(B.Y - A.Y)).Size();
const float FloorDelta = FloorB - FloorA;
if (VF_TunnelFloorNeedsTerrace(A, RadiusA, B, RadiusB))
{
// A steep room-to-room change is a staircase, not a steep walking ramp. Every
// vertical riser is at most the authored 45 cm player step; climbing systems may
// still choose to treat those risers as explicit climb surfaces later.
// Leave a small deterministic margin for floating-point evaluation at the riser. The
// terrace has no relief (see VF_TunnelFloorReliefScale), so this is the complete
// adjacent-column step budget rather than a visual tuning constant.
constexpr float TerraceStepSafety = 0.95f;
const float SafeStepHeight =
VoxelPassageGeometry::MaxStepHeightVoxels * TerraceStepSafety;
const int32 NumSteps = FMath::Clamp(
FMath::CeilToInt(FMath::Abs(FloorDelta)
/ SafeStepHeight),
1, 4096);
const float ClampedT = FMath::Clamp(T, 0.0f, 1.0f);
const float StairT = ClampedT >= 1.0f
? 1.0f
: FMath::FloorToFloat(ClampedT * static_cast<float>(NumSteps))
/ static_cast<float>(NumSteps);
return StairT;
}
return FMath::Clamp(T, 0.0f, 1.0f);
}
FORCEINLINE float VF_TunnelFloorReliefScale(
const FVector& A, float RadiusA,
const FVector& B, float RadiusB,
int32 SegmentIndex, int32 NumSegments,
const FCachedTunnel& Tunnel)
{
if (!(Tunnel.FloorReliefStrength > 0.0f)
|| !FMath::IsFinite(Tunnel.FloorReliefStrength)
|| !FMath::IsFinite(Tunnel.FloorReliefFrequency))
{
return 0.0f;
}
// A terrace already consumes the available vertical step at its risers. Keeping the
// terrace segments level and relief-free makes the adjacent-column step bound exact;
// gentle segments retain the bounded authored relief below.
if (VF_TunnelFloorNeedsTerrace(A, RadiusA, B, RadiusB))
{
return 0.0f;
}
const float HorizontalRun = FVector2D(
static_cast<float>(B.X - A.X), static_cast<float>(B.Y - A.Y)).Size();
if (!(HorizontalRun > KINDA_SMALL_NUMBER))
{
return 0.0f;
}
const float FloorDelta = FMath::Abs(
VoxelPassageGeometry::TunnelFloorZ(B, FMath::Abs(RadiusB))
- VoxelPassageGeometry::TunnelFloorZ(A, FMath::Abs(RadiusA)));
const float BaseGradient = VF_TunnelFloorNeedsTerrace(
A, RadiusA, B, RadiusB)
? 0.0f : FloorDelta / HorizontalRun;
const float AvailableGradient =
VoxelPassageGeometry::PlayerWalkableFloorMaxGradient - BaseGradient;
if (!(AvailableGradient > 0.0f))
{
return 0.0f;
}
const float Frequency = FMath::Abs(Tunnel.FloorReliefFrequency);
const float Amplitude = FMath::Abs(Tunnel.FloorReliefStrength)
* VOXEL_NOISE_SCALE;
const float OctaveFrequencyWeight = 0.65f + 0.35f * 2.3f;
const float NoiseGradientBound =
1.4142135623730951f * VF_Perlin2DPartialAbsBound
* OctaveFrequencyWeight * Frequency * Amplitude;
const int32 LandingEnvelopeCount = (SegmentIndex == 0 ? 1 : 0)
+ (SegmentIndex + 1 == NumSegments ? 1 : 0);
const float EnvelopeDerivativeBound = LandingEnvelopeCount > 0
? (1.5f * static_cast<float>(LandingEnvelopeCount)
/ VoxelPassageGeometry::WalkableTunnelLandingApronVoxels)
* Amplitude
: 0.0f;
const float ReliefGradientBound = NoiseGradientBound
+ EnvelopeDerivativeBound;
if (!(ReliefGradientBound > KINDA_SMALL_NUMBER)
|| !FMath::IsFinite(ReliefGradientBound))
{
return 0.0f;
}
return FMath::Clamp(
FMath::Min(
VF_MaxTunnelFloorReliefScale,
AvailableGradient / ReliefGradientBound),
0.0f, VF_MaxTunnelFloorReliefScale);
}
FORCEINLINE float VF_TunnelFloorAtSegment(
const FVector& A, float RadiusA,
const FVector& B, float RadiusB,
float T, int32 SegmentIndex, int32 NumSegments,
const FVector& Position,
const FCachedTunnel& Tunnel)
{
const float ProfileT = VF_TunnelFloorProfileT(
A, RadiusA, B, RadiusB, T);
float FloorZ = FMath::Lerp(
VoxelPassageGeometry::TunnelFloorZ(A, FMath::Abs(RadiusA)),
VoxelPassageGeometry::TunnelFloorZ(B, FMath::Abs(RadiusB)),
ProfileT);
const float HorizontalRun = FVector2D(
static_cast<float>(B.X - A.X), static_cast<float>(B.Y - A.Y)).Size();
float ReliefEnvelope = 1.0f;
if (SegmentIndex == 0)
{
ReliefEnvelope *= SmoothStep01(FMath::Clamp(
(T * HorizontalRun)
/ VoxelPassageGeometry::WalkableTunnelLandingApronVoxels,
0.0f, 1.0f));
}
if (SegmentIndex + 1 == NumSegments)
{
ReliefEnvelope *= SmoothStep01(FMath::Clamp(
((1.0f - T) * HorizontalRun)
/ VoxelPassageGeometry::WalkableTunnelLandingApronVoxels,
0.0f, 1.0f));
}
const float ReliefScale = VF_TunnelFloorReliefScale(
A, RadiusA, B, RadiusB, SegmentIndex, NumSegments, Tunnel);
FloorZ += VF_TunnelFloorRelief(
static_cast<float>(Position.X), static_cast<float>(Position.Y),
Tunnel, ReliefEnvelope, ReliefScale);
return FloorZ;
}
FORCEINLINE bool VF_ProjectTunnelSegmentXY(
const FVector& Position, const FVector& A, const FVector& B,
float& OutT, float& OutDistanceSquared)
{
const FVector2D AXY(static_cast<float>(A.X), static_cast<float>(A.Y));
const FVector2D Delta(
static_cast<float>(B.X - A.X), static_cast<float>(B.Y - A.Y));
const float LengthSquared = Delta.SizeSquared(); const float LengthSquared = Delta.SizeSquared();
if (!(LengthSquared > KINDA_SMALL_NUMBER) if (LengthSquared <= KINDA_SMALL_NUMBER)
|| !FMath::IsFinite(StartRadius)
|| !FMath::IsFinite(EndRadius)
|| !FMath::IsFinite(FloorZ)
|| !FMath::IsFinite(SupportRadius))
{ {
OutT = 0.0f;
OutDistanceSquared = FLT_MAX;
return false; return false;
} }
const FVector2D Query(WorldX, WorldY); const FVector2D QueryXY(
const float T = FMath::Clamp( static_cast<float>(Position.X), static_cast<float>(Position.Y));
FVector2D::DotProduct(Query - FVector2D(Start.X, Start.Y), Delta) OutT = FMath::Clamp(
/ LengthSquared, FVector2D::DotProduct(QueryXY - AXY, Delta) / LengthSquared,
0.0f, 1.0f); 0.0f, 1.0f);
const FVector2D Closest = FVector2D(Start.X, Start.Y) + Delta * T; const FVector2D ClosestXY = AXY + Delta * OutT;
const float CentrelineDistance = (Query - Closest).Size(); OutDistanceSquared = (QueryXY - ClosestXY).SizeSquared();
const float TubeRadius = FMath::Lerp(StartRadius, EndRadius, T); return true;
constexpr float CapsuleSafetyMargin = 0.25f; }
// The first candidate was still too permissive: the analytic tube was walkable, but
// roughness outside the structural core could remove the last supporting voxels. Only static FVFTunnelShapeEvaluation VF_EvaluateSweptTunnelChain(
// the broad, nearly-centreline part of the widest tubes is a certificate strong enough to const FVector& Position,
// replace the authored floor; all marginal geometry keeps the slab. const TArray<FVector>& ControlPoints,
constexpr float NaturalMinimumTubeRadius = 7.5f; const TArray<float>& ControlRadii,
constexpr float NaturalCentrelineTolerance = 0.5f; const FCachedTunnel& Tunnel,
constexpr float NaturalWallMargin = 2.0f; float SDFBlendRadius,
constexpr float NaturalMaximumAxialGradient = 0.12f; bool bApplyFloorCut)
const float PlayerRadius = VoxelPassageGeometry::PlayerRadiusVoxels; {
const float FootprintRadius = PlayerRadius + CapsuleSafetyMargin; FVFTunnelShapeEvaluation Result;
if (TubeRadius < NaturalMinimumTubeRadius if (ControlPoints.Num() < 2
|| CentrelineDistance > NaturalCentrelineTolerance || ControlRadii.Num() != ControlPoints.Num())
|| !(TubeRadius > 0.0f)
|| CentrelineDistance + FootprintRadius
> TubeRadius - NaturalWallMargin
|| SupportRadius + KINDA_SMALL_NUMBER < PlayerRadius)
{ {
return false; return Result;
} }
const float FarthestSupportRadius = CentrelineDistance + FootprintRadius; const int32 NumSegments = ControlPoints.Num() - 1;
const float CrossSectionHalfWidth = FMath::Sqrt(FMath::Max( const float FloorBlend = FMath::Max(SDFBlendRadius, 0.0f) * 0.35f;
TubeRadius * TubeRadius for (int32 SegmentIndex = 0;
- FarthestSupportRadius * FarthestSupportRadius, SegmentIndex < NumSegments;
0.0f)); ++SegmentIndex)
if (2.0f * CrossSectionHalfWidth
< VoxelPassageGeometry::PlayerHeightVoxels
+ 2.0f * CapsuleSafetyMargin)
{ {
return false; const FVector& A = ControlPoints[SegmentIndex];
} const FVector& B = ControlPoints[SegmentIndex + 1];
const float RadiusA = ControlRadii[SegmentIndex];
const float RadiusB = ControlRadii[SegmentIndex + 1];
const float SegmentSDF = VoxelSDF::TaperedCapsule(
Position, A, B, RadiusA, RadiusB);
// The round floor's steepest support-patch slope is attained at its outer edge. Keep it float T = 0.0f;
// no steeper than the authored 15-degree tunnel contract, rather than using a generous float HorizontalDistanceSquared = FLT_MAX;
// engine walkability ceiling merely to make the optimization fire more often. const bool bHasHorizontalProjection = VF_ProjectTunnelSegmentXY(
const float RadialDenominator = FMath::Sqrt(FMath::Max( Position, A, B, T, HorizontalDistanceSquared);
TubeRadius * TubeRadius if (!bHasHorizontalProjection)
- FarthestSupportRadius * FarthestSupportRadius,
KINDA_SMALL_NUMBER));
const float RadialGradient = FarthestSupportRadius / RadialDenominator;
const float AxialGradient = VoxelPassageGeometry::TunnelFloorGradient(
Start, StartRadius, End, EndRadius);
if (!FMath::IsFinite(RadialGradient)
|| !FMath::IsFinite(AxialGradient)
|| RadialGradient > VoxelPassageGeometry::WalkableTunnelMaxGradient
|| AxialGradient > NaturalMaximumAxialGradient)
{
return false;
}
// Reject bends and segment endpoints where the projected support plane changes abruptly.
// The flat-floor path remains responsible for those transition columns.
constexpr float ProbeDistance = 1.0f;
const FVector ProbePositions[] = {
FVector(WorldX - ProbeDistance, WorldY, 0.0f),
FVector(WorldX + ProbeDistance, WorldY, 0.0f),
FVector(WorldX, WorldY - ProbeDistance, 0.0f),
FVector(WorldX, WorldY + ProbeDistance, 0.0f)
};
for (const FVector& Probe : ProbePositions)
{
float ProbeFloorZ = 0.0f;
float ProbeSupportRadius = 0.0f;
int32 ProbeSegmentIndex = INDEX_NONE;
if (!VoxelPassageGeometry::ProjectWalkableTunnelFloor(
Tunnel.WorldControlPoints,
Tunnel.WorldControlRadii,
Probe,
ProbeFloorZ,
ProbeSupportRadius,
&ProbeSegmentIndex)
|| ProbeSegmentIndex != SegmentIndex
|| FMath::Abs(ProbeFloorZ - FloorZ)
> VoxelPassageGeometry::WalkableTunnelMaxGradient
* ProbeDistance + CapsuleSafetyMargin)
{ {
return false; // A vertical/degenerate XY segment has no finite floor projection. Retain the
// capsule for air, but never invent a support surface there.
Result.SDF = FMath::Min(Result.SDF, SegmentSDF);
continue;
}
const float FloorZ = VF_TunnelFloorAtSegment(
A, RadiusA, B, RadiusB,
T, SegmentIndex, NumSegments, Position, Tunnel);
const float SegmentResultSDF = bApplyFloorCut
? VoxelSDF::SmoothMax(
SegmentSDF, FloorZ - static_cast<float>(Position.Z), FloorBlend)
: SegmentSDF;
Result.SDF = FMath::Min(Result.SDF, SegmentResultSDF);
const float SupportRadius = FMath::Max(
FMath::Min(FMath::Abs(RadiusA), FMath::Abs(RadiusB)) - 0.5f,
VoxelPassageGeometry::PlayerRadiusVoxels);
if (SegmentSDF <= 0.0f
&& HorizontalDistanceSquared <= FMath::Square(SupportRadius)
&& Position.Z >= FloorZ
- VoxelPassageGeometry::LandingFloorThicknessVoxels
&& Position.Z <= FloorZ
+ VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels)
{
Result.bSupportFloor = true;
} }
} }
return Result;
}
if (NaturalSampler == nullptr) static FVFTunnelShapeEvaluation VF_EvaluateSweptTunnel(
const FVector& Position,
const FCachedTunnel& Tunnel,
bool bWorldChain,
float SDFBlendRadius,
bool bApplyFloorCut)
{
const TArray<FVector>& ShapeControlPoints = bWorldChain
? Tunnel.WorldControlPoints : Tunnel.ControlPoints;
const TArray<float>& ShapeControlRadii = bWorldChain
? Tunnel.WorldControlRadii : Tunnel.ControlRadii;
if (ShapeControlPoints.Num() >= 2
&& ShapeControlRadii.Num() == ShapeControlPoints.Num())
{
return VF_EvaluateSweptTunnelChain(
Position, ShapeControlPoints, ShapeControlRadii,
Tunnel, SDFBlendRadius, bApplyFloorCut);
}
if (Tunnel.bHasMidpoint)
{
TArray<FVector> Points;
TArray<float> Radii;
Points.Add(Tunnel.EndpointA);
Points.Add(Tunnel.Midpoint);
Points.Add(Tunnel.EndpointB);
Radii.Add(Tunnel.RadiusA);
Radii.Add(Tunnel.RadiusMid);
Radii.Add(Tunnel.RadiusB);
// The fallback descriptor is only retained for cooked/old cache compatibility. The
// temporary arrays stay inline and do not allocate on the normal chain path.
return VF_EvaluateSweptTunnelChain(
Position, Points, Radii,
Tunnel, SDFBlendRadius, bApplyFloorCut);
}
TArray<FVector> Points;
TArray<float> Radii;
Points.Add(Tunnel.EndpointA);
Points.Add(Tunnel.EndpointB);
Radii.Add(Tunnel.RadiusA);
Radii.Add(Tunnel.RadiusB);
return VF_EvaluateSweptTunnelChain(
Position, Points, Radii,
Tunnel, SDFBlendRadius, bApplyFloorCut);
}
static bool VF_ProjectSweptTunnelFloor(
const FVector& Position,
const FCachedTunnel& Tunnel,
bool bWorldChain,
float& OutFloorZ,
float& OutSupportRadius)
{
const TArray<FVector>& ControlPoints = bWorldChain
? Tunnel.WorldControlPoints : Tunnel.ControlPoints;
const TArray<float>& ControlRadii = bWorldChain
? Tunnel.WorldControlRadii : Tunnel.ControlRadii;
if (ControlPoints.Num() < 2
|| ControlRadii.Num() != ControlPoints.Num())
{ {
return false; return false;
} }
FVoxelStrateMeasureSettings FitSettings; const int32 NumSegments = ControlPoints.Num() - 1;
const float MaxStepHeightVoxels = float BestDistanceSquared = FLT_MAX;
FVoxelPlayerCapsuleConstants::MaxStepHeightMeters bool bFound = false;
/ FVoxelPlayerCapsuleConstants::VoxelSizeMeters; for (int32 SegmentIndex = 0;
// The projected graph floor is the centreline's authored support level, but natural rock SegmentIndex < NumSegments;
// may leave a legal footing a little above or below it. Search a small fixed offset set so ++SegmentIndex)
// that the conditional path does not mistake a vertically displaced natural ledge for a
// failed walkability test. The validator still enforces the actual max-step and capsule
// clearance rules for every trial; no geometry is accepted by this search alone.
constexpr int32 FeetOffsetCount = 3;
const float FeetOffsets[FeetOffsetCount] = {
0.0f, -1.0f, 1.0f
};
for (int32 OffsetIndex = 0; OffsetIndex < FeetOffsetCount; ++OffsetIndex)
{ {
const FVector CandidateFeet( float T = 0.0f;
WorldX, WorldY, float DistanceSquared = FLT_MAX;
FloorZ + MaxStepHeightVoxels + 0.5f + FeetOffsets[OffsetIndex]); if (!VF_ProjectTunnelSegmentXY(
FVector FitPoint = FVector::ZeroVector; Position,
if (VF_ValidatePlayerFitPose( ControlPoints[SegmentIndex], ControlPoints[SegmentIndex + 1],
FitSettings, T, DistanceSquared)
CandidateFeet, || DistanceSquared >= BestDistanceSquared)
StrateTopZ,
StrateBottomZ,
BoundarySealThickness,
*NaturalSampler,
FitPoint))
{ {
return true; continue;
} }
const float RadiusA = ControlRadii[SegmentIndex];
const float RadiusB = ControlRadii[SegmentIndex + 1];
OutFloorZ = VF_TunnelFloorAtSegment(
ControlPoints[SegmentIndex], RadiusA,
ControlPoints[SegmentIndex + 1], RadiusB,
T, SegmentIndex, NumSegments, Position, Tunnel);
OutSupportRadius = FMath::Max(
FMath::Min(FMath::Abs(RadiusA), FMath::Abs(RadiusB)) - 0.5f,
VoxelPassageGeometry::PlayerRadiusVoxels);
BestDistanceSquared = DistanceSquared;
bFound = true;
} }
return false;
return bFound
&& FMath::IsFinite(OutFloorZ)
&& FMath::IsFinite(OutSupportRadius)
&& BestDistanceSquared <= FMath::Square(OutSupportRadius);
} }
} }
@@ -268,6 +479,7 @@ void FChunkSDFCache::Reset()
SupportColumnCellsY = 0; SupportColumnCellsY = 0;
SupportColumnEntries.Reset(); SupportColumnEntries.Reset();
SupportColumns.Reset(); SupportColumns.Reset();
SDFBlendRadius = 0.0f;
} }
void FChunkSDFCache::Release() void FChunkSDFCache::Release()
@@ -3679,6 +3891,7 @@ void VoxelCaveMorphology::BuildChunkCache(
{ {
// Clear previous data (arrays keep their allocation for reuse) // Clear previous data (arrays keep their allocation for reuse)
OutCache.Reset(); OutCache.Reset();
OutCache.SDFBlendRadius = Params.SDFBlendRadius;
// Combine world seed with strate index so each strate gets unique caves // Combine world seed with strate index so each strate gets unique caves
const uint32 StrateSeed = VoxelCaveMorphology::MakeStrateSeed(Seed, StrateIndex); const uint32 StrateSeed = VoxelCaveMorphology::MakeStrateSeed(Seed, StrateIndex);
@@ -4235,6 +4448,9 @@ void VoxelCaveMorphology::BuildChunkCache(
CT.EndpointB = EndB; CT.EndpointB = EndB;
CT.RadiusA = RadA; CT.RadiusA = RadA;
CT.RadiusB = RadB; CT.RadiusB = RadB;
CT.FloorReliefStrength = Params.FloorReliefStrength;
CT.FloorReliefFrequency = Params.FloorReliefFrequency;
CT.FloorSeed = VoxelHash::Mix(TunnelHash ^ 0xF100F1u);
// A room edge is not a straight capsule between cell centres. Build a deterministic // A room edge is not a straight capsule between cell centres. Build a deterministic
// chain of hash-jittered control points instead. The chain is keyed only by the pair // chain of hash-jittered control points instead. The chain is keyed only by the pair
@@ -4973,44 +5189,11 @@ float VoxelCaveMorphology::EvaluateSDFCached(
VoxelDensityProfile::ECounter::CaveTunnelEvaluated); VoxelDensityProfile::ECounter::CaveTunnelEvaluated);
} }
float TunnelSDF; // The corridor is a swept floor cut: each tapered capsule segment is intersected with
// its interpolated floor half-space before the tunnel union is blended into rooms.
if (Tunnel.ControlPoints.Num() >= 2 const float TunnelSDF = VF_EvaluateSweptTunnel(
&& Tunnel.ControlRadii.Num() == Tunnel.ControlPoints.Num()) Pos, Tunnel, /*bWorldChain=*/false, BlendK,
{ /*bApplyFloorCut=*/true).SDF;
// A wandering chain is evaluated segment-by-segment. Min keeps the shared control
// point watertight and preserves the old capsule union semantics; the outer
// SmoothMin is still the room/tunnel blend.
TunnelSDF = FLT_MAX;
for (int32 SegmentIndex = 0;
SegmentIndex + 1 < Tunnel.ControlPoints.Num();
++SegmentIndex)
{
TunnelSDF = FMath::Min(
TunnelSDF,
VoxelSDF::TaperedCapsule(
Pos,
Tunnel.ControlPoints[SegmentIndex],
Tunnel.ControlPoints[SegmentIndex + 1],
Tunnel.ControlRadii[SegmentIndex],
Tunnel.ControlRadii[SegmentIndex + 1]));
}
}
else if (Tunnel.bHasMidpoint)
{
// Backward-compatible descriptor path for older cooked caches.
float SegA = VoxelSDF::TaperedCapsule(Pos, Tunnel.EndpointA, Tunnel.Midpoint,
Tunnel.RadiusA, Tunnel.RadiusMid);
float SegB = VoxelSDF::TaperedCapsule(Pos, Tunnel.Midpoint, Tunnel.EndpointB,
Tunnel.RadiusMid, Tunnel.RadiusB);
TunnelSDF = FMath::Min(SegA, SegB);
}
else
{
// Backward-compatible straight descriptor path.
TunnelSDF = VoxelSDF::TaperedCapsule(Pos, Tunnel.EndpointA, Tunnel.EndpointB,
Tunnel.RadiusA, Tunnel.RadiusB);
}
MinSDF = VoxelSDF::SmoothMin(MinSDF, TunnelSDF, BlendK); MinSDF = VoxelSDF::SmoothMin(MinSDF, TunnelSDF, BlendK);
}; };
@@ -5053,44 +5236,11 @@ float VoxelCaveMorphology::EvaluateTunnelCoreSDF(
return; return;
} }
if (Tunnel.ControlPoints.Num() >= 2 MinSDF = FMath::Min(
&& Tunnel.ControlRadii.Num() == Tunnel.ControlPoints.Num()) MinSDF,
{ VF_EvaluateSweptTunnel(
for (int32 SegmentIndex = 0; Pos, Tunnel, /*bWorldChain=*/false, Cache.SDFBlendRadius,
SegmentIndex + 1 < Tunnel.ControlPoints.Num(); /*bApplyFloorCut=*/false).SDF);
++SegmentIndex)
{
MinSDF = FMath::Min(
MinSDF,
VoxelSDF::TaperedCapsule(
Pos,
Tunnel.ControlPoints[SegmentIndex],
Tunnel.ControlPoints[SegmentIndex + 1],
Tunnel.ControlRadii[SegmentIndex],
Tunnel.ControlRadii[SegmentIndex + 1]));
}
}
else if (Tunnel.bHasMidpoint)
{
MinSDF = FMath::Min(
MinSDF,
VoxelSDF::TaperedCapsule(
Pos, Tunnel.EndpointA, Tunnel.Midpoint,
Tunnel.RadiusA, Tunnel.RadiusMid));
MinSDF = FMath::Min(
MinSDF,
VoxelSDF::TaperedCapsule(
Pos, Tunnel.Midpoint, Tunnel.EndpointB,
Tunnel.RadiusMid, Tunnel.RadiusB));
}
else
{
MinSDF = FMath::Min(
MinSDF,
VoxelSDF::TaperedCapsule(
Pos, Tunnel.EndpointA, Tunnel.EndpointB,
Tunnel.RadiusA, Tunnel.RadiusB));
}
}; };
for (int32 TunnelIdx = 0; TunnelIdx < Cache.Tunnels.Num(); ++TunnelIdx) for (int32 TunnelIdx = 0; TunnelIdx < Cache.Tunnels.Num(); ++TunnelIdx)
@@ -5154,10 +5304,6 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
VoxelDensityProfile::ECounter::TunnelCoreEvaluated); VoxelDensityProfile::ECounter::TunnelCoreEvaluated);
} }
const TArray<FVector>& ControlPoints = bHasWorldChain
? Tunnel.WorldControlPoints : Tunnel.ControlPoints;
const TArray<float>& ControlRadii = bHasWorldChain
? Tunnel.WorldControlRadii : Tunnel.ControlRadii;
if (bHasWorldChain) if (bHasWorldChain)
{ {
if (SupportColumn != nullptr && VoxelDensityProfile::AreCountersEnabled()) if (SupportColumn != nullptr && VoxelDensityProfile::AreCountersEnabled())
@@ -5172,12 +5318,8 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
const bool bProjectedFloor = SupportColumn != nullptr const bool bProjectedFloor = SupportColumn != nullptr
? VoxelCaveMorphology::GetTunnelSupportFloorColumnBand( ? VoxelCaveMorphology::GetTunnelSupportFloorColumnBand(
TunnelIdx, *SupportColumn, FloorZ, SupportMinZ, SupportMaxZ) TunnelIdx, *SupportColumn, FloorZ, SupportMinZ, SupportMaxZ)
: VoxelPassageGeometry::ProjectWalkableTunnelFloor( : VF_ProjectSweptTunnelFloor(
ControlPoints, ControlRadii, Pos, FloorZ, SupportRadius); Pos, Tunnel, /*bWorldChain=*/true, FloorZ, SupportRadius);
// The first cull above already used WorldBoundCenter/WorldBoundRadiusSq for a
// world-chain tunnel. Repeating the same squared-distance test here only doubled
// arithmetic on every accepted tunnel; keep the projection and Z checks, which are
// the distinct support-floor predicates.
if (bProjectedFloor if (bProjectedFloor
&& WorldZ <= FloorZ && WorldZ <= FloorZ
+ VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels + VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels
@@ -5191,50 +5333,18 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
if (bProjectedFloor && WorldZ <= FloorZ if (bProjectedFloor && WorldZ <= FloorZ
+ VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels) + VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels)
{ {
// The finite floor slab owns the bottom of this tunnel. Another intersecting // Preserve the established ownership contract while the SDF-space evaluator is
// tunnel may still win below its own floor; the loop continues so that air // still raw: the floor owns the lower half of the corridor, and the capsule owns
// ownership remains the union of all valid tunnel cores. // only the air above it. This diagnostic is what distinguishes a coordinate-space
// mismatch from a genuine swept-floor capability loss.
return; return;
} }
} }
if (ControlPoints.Num() >= 2
&& ControlRadii.Num() == ControlPoints.Num()) const FVFTunnelShapeEvaluation TunnelShape = VF_EvaluateSweptTunnel(
{ Pos, Tunnel, bHasWorldChain, Cache.SDFBlendRadius,
for (int32 SegmentIndex = 0; /*bApplyFloorCut=*/true);
SegmentIndex + 1 < ControlPoints.Num(); Result.SDF = FMath::Min(Result.SDF, TunnelShape.SDF);
++SegmentIndex)
{
Result.SDF = FMath::Min(
Result.SDF,
VoxelSDF::TaperedCapsule(
Pos,
ControlPoints[SegmentIndex],
ControlPoints[SegmentIndex + 1],
ControlRadii[SegmentIndex],
ControlRadii[SegmentIndex + 1]));
}
}
else if (Tunnel.bHasMidpoint)
{
Result.SDF = FMath::Min(
Result.SDF,
VoxelSDF::TaperedCapsule(
Pos, Tunnel.EndpointA, Tunnel.Midpoint,
Tunnel.RadiusA, Tunnel.RadiusMid));
Result.SDF = FMath::Min(
Result.SDF,
VoxelSDF::TaperedCapsule(
Pos, Tunnel.Midpoint, Tunnel.EndpointB,
Tunnel.RadiusMid, Tunnel.RadiusB));
}
else
{
Result.SDF = FMath::Min(
Result.SDF,
VoxelSDF::TaperedCapsule(
Pos, Tunnel.EndpointA, Tunnel.EndpointB,
Tunnel.RadiusA, Tunnel.RadiusB));
}
}; };
for (int32 TunnelIdx = 0; TunnelIdx < Cache.Tunnels.Num(); ++TunnelIdx) for (int32 TunnelIdx = 0; TunnelIdx < Cache.Tunnels.Num(); ++TunnelIdx)
@@ -5268,43 +5378,35 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
auto TestTunnel = [&](int32 TunnelIdx) -> bool auto TestTunnel = [&](int32 TunnelIdx) -> bool
{ {
const FCachedTunnel& Tunnel = Cache.Tunnels[TunnelIdx]; const FCachedTunnel& Tunnel = Cache.Tunnels[TunnelIdx];
if (Tunnel.WorldControlPoints.Num() < 2 const bool bHasWorldChain = Tunnel.WorldControlPoints.Num() >= 2
|| Tunnel.WorldControlRadii.Num() != Tunnel.WorldControlPoints.Num()) && Tunnel.WorldControlRadii.Num() == Tunnel.WorldControlPoints.Num();
{ const FVector& CenterlineMin = bHasWorldChain
return false; ? Tunnel.WorldCenterlineMin : Tunnel.SDFCenterlineMin;
} const FVector& CenterlineMax = bHasWorldChain
? Tunnel.WorldCenterlineMax : Tunnel.SDFCenterlineMax;
if (Tunnel.WorldInfluenceRadius > 0.0f const float InfluenceRadius = bHasWorldChain
? Tunnel.WorldInfluenceRadius : Tunnel.SDFInfluenceRadius;
if (InfluenceRadius > 0.0f
&& VF_DistanceSquaredToAabb( && VF_DistanceSquaredToAabb(
WorldX, WorldY, WorldZ, WorldX, WorldY, WorldZ, CenterlineMin, CenterlineMax)
Tunnel.WorldCenterlineMin, Tunnel.WorldCenterlineMax) > FMath::Square(InfluenceRadius))
> FMath::Square(Tunnel.WorldInfluenceRadius))
{ {
return false; return false;
} }
float FloorZ = 0.0f; const FVector& BoundCenter = bHasWorldChain
float SupportRadius = 0.0f; ? Tunnel.WorldBoundCenter : Tunnel.BoundCenter;
if (!VoxelPassageGeometry::ProjectWalkableTunnelFloor( const float BoundRadiusSq = bHasWorldChain
Tunnel.WorldControlPoints, ? Tunnel.WorldBoundRadiusSq : Tunnel.BoundRadiusSq;
Tunnel.WorldControlRadii, if (BoundRadiusSq > 0.0f
Pos, && VF_FloatDistSquared(WorldX, WorldY, WorldZ, BoundCenter) > BoundRadiusSq)
FloorZ,
SupportRadius,
nullptr)
|| WorldZ > FloorZ + VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels
|| WorldZ < FloorZ - VoxelPassageGeometry::LandingFloorThicknessVoxels)
{ {
return false; return false;
} }
if (Tunnel.WorldBoundRadiusSq <= 0.0f return VF_EvaluateSweptTunnel(
|| VF_FloatDistSquared(WorldX, WorldY, WorldZ, Tunnel.WorldBoundCenter) Pos, Tunnel, bHasWorldChain, Cache.SDFBlendRadius,
<= Tunnel.WorldBoundRadiusSq) /*bApplyFloorCut=*/true).bSupportFloor;
{
return true;
}
return false;
}; };
bool bSupport = false; bool bSupport = false;
@@ -5327,11 +5429,7 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
void VoxelCaveMorphology::BuildTunnelSupportFloorColumn( void VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
float WorldX, float WorldY, float WorldX, float WorldY,
const FChunkSDFCache& Cache, const FChunkSDFCache& Cache,
FTunnelSupportFloorColumn& OutColumn, FTunnelSupportFloorColumn& OutColumn)
const FTunnelSupportFloorDensitySampler* NaturalSampler,
float NaturalStrateTopZ,
float NaturalStrateBottomZ,
float NaturalBoundarySealThickness)
{ {
OutColumn.Reset(); OutColumn.Reset();
@@ -5355,45 +5453,16 @@ void VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
continue; continue;
} }
const FCachedTunnel& Tunnel = Cache.Tunnels[TunnelIdx]; const FCachedTunnel& Tunnel = Cache.Tunnels[TunnelIdx];
if (Tunnel.WorldControlPoints.Num() < 2
|| Tunnel.WorldControlRadii.Num() != Tunnel.WorldControlPoints.Num())
{
continue;
}
const FVector ColumnPosition(WorldX, WorldY, 0.0f); const FVector ColumnPosition(WorldX, WorldY, 0.0f);
float FloorZ = 0.0f; float FloorZ = 0.0f;
float SupportRadius = 0.0f; float SupportRadius = 0.0f;
int32 SegmentIndex = INDEX_NONE; if (!VF_ProjectSweptTunnelFloor(
if (!VoxelPassageGeometry::ProjectWalkableTunnelFloor( ColumnPosition, Tunnel, /*bWorldChain=*/true,
Tunnel.WorldControlPoints, FloorZ, SupportRadius))
Tunnel.WorldControlRadii,
ColumnPosition,
FloorZ,
SupportRadius,
&SegmentIndex))
{ {
continue; continue;
} }
if (NaturalSampler != nullptr && VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelSupportFloorNaturalTests);
}
if (VF_IsNaturalTunnelFloorWalkable(
Tunnel, WorldX, WorldY, FloorZ, SupportRadius, SegmentIndex,
NaturalSampler, NaturalStrateTopZ, NaturalStrateBottomZ,
NaturalBoundarySealThickness))
{
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelSupportFloorNaturalSkips);
}
continue;
}
float MinZ = FloorZ - VoxelPassageGeometry::LandingFloorThicknessVoxels; float MinZ = FloorZ - VoxelPassageGeometry::LandingFloorThicknessVoxels;
float MaxZ = FloorZ + VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels; float MaxZ = FloorZ + VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels;
@@ -3591,10 +3591,8 @@ namespace
{ {
public: public:
FRoomGraphSource(const FStrateGenerationParams& InP, int32 InSeed, FRoomGraphSource(const FStrateGenerationParams& InP, int32 InSeed,
const UVoxelStrateManager* InManager, const UVoxelStrateManager* InManager)
const UVoxelGenerator* InGenerator = nullptr)
: P(InP), Seed(InSeed), SeedU((uint32)InSeed), Manager(InManager) : P(InP), Seed(InSeed), SeedU((uint32)InSeed), Manager(InManager)
, Generator(InGenerator)
, ManagerLifetimeId(InManager ? InManager->GetCacheLifetimeId() : 0) , ManagerLifetimeId(InManager ? InManager->GetCacheLifetimeId() : 0)
, ParamsFingerprint(FCrc::MemCrc32(&InP, sizeof(InP))) , 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. */ * Lu par les arches, les dômes, le pincement et le biais de sol. */
int32 GetNearestRoomIdx() const { return State().NearestRoom; } int32 GetNearestRoomIdx() const { return State().NearestRoom; }
bool HasTunnelAirGeometry() const
{
return P.RoomDensity > 0.0f && P.RoomSpacing > 0.0f;
}
void BuildSupportColumn( void BuildSupportColumn(
float WorldX, float WorldY, float WorldX, float WorldY,
const FChunkSDFCache& Cache, const FChunkSDFCache& Cache,
FTunnelSupportFloorColumn& OutColumn) const 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( VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
WorldX, WorldY, Cache, OutColumn, &NaturalSampler, WorldX, WorldY, Cache, OutColumn);
P.StrateTopWorldZ, P.StrateBottomWorldZ,
P.BoundarySealThickness);
} }
const FTunnelSupportFloorColumn* GetSupportColumn(const FVector& Position) const const FTunnelSupportFloorColumn* GetSupportColumn(const FVector& Position) const
@@ -3857,18 +3828,6 @@ namespace
return &S.FractionalSupportColumn; 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( bool TryGetLastTunnelCoreWorldEvaluation(
FTunnelCoreWorldEvaluation& OutEvaluation) const override FTunnelCoreWorldEvaluation& OutEvaluation) const override
{ {
@@ -4200,7 +4159,8 @@ namespace
} }
float CaveSDF = VoxelCaveMorphology::EvaluateSDFCached( 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 // 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"); } 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: private:
FStrateGenerationParams P; FStrateGenerationParams P;
int32 Seed; int32 Seed;
uint32 SeedU; uint32 SeedU;
TWeakObjectPtr<const UVoxelStrateManager> Manager; TWeakObjectPtr<const UVoxelStrateManager> Manager;
const UVoxelGenerator* Generator = nullptr;
uint64 ManagerLifetimeId = 0; uint64 ManagerLifetimeId = 0;
uint32 ParamsFingerprint; uint32 ParamsFingerprint;
uint32 LayoutVersion = 0; 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 // LE GATE DE BOÎTE DES MODIFICATEURS DE CAVE
//========================================================================= //=========================================================================
@@ -9442,8 +9287,7 @@ namespace VoxelDensityOps
void BuildTunnelNetworkStack(FVoxelOpStack& OutStack, const FStrateGenerationParams& P, void BuildTunnelNetworkStack(FVoxelOpStack& OutStack, const FStrateGenerationParams& P,
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager, int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts, bool bAppendStructuralPosts)
const UVoxelGenerator* Generator)
{ {
// ⚠️ ÉTAPES A + B + C1 — LA PILE EST COMPLÈTE POUR CET ARCHÉTYPE. // ⚠️ É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), // 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 // 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`. // son cache SDF en Z±1. Même motif que `FShaftFieldSource` → `FShaftLedgeMod`.
TUniquePtr<FRoomGraphSource> RoomSource = MakeUnique<FRoomGraphSource>( TUniquePtr<FRoomGraphSource> RoomSource = MakeUnique<FRoomGraphSource>(
P, Seed, StrateManager, Generator); P, Seed, StrateManager);
const FRoomGraphSource* RoomPtr = RoomSource.Get(); const FRoomGraphSource* RoomPtr = RoomSource.Get();
OutStack.Add(MakeConstantRockSource(P.BaseDensity)); OutStack.Add(MakeConstantRockSource(P.BaseDensity));
@@ -414,8 +414,6 @@ namespace VoxelDensityProfile
case ECounter::TunnelSupportColumnCandidates:return TEXT("TunnelSupportColumnCandidates"); case ECounter::TunnelSupportColumnCandidates:return TEXT("TunnelSupportColumnCandidates");
case ECounter::TunnelSupportFloorQueries:return TEXT("TunnelSupportFloorQueries"); case ECounter::TunnelSupportFloorQueries:return TEXT("TunnelSupportFloorQueries");
case ECounter::TunnelSupportFloorChecks:return TEXT("TunnelSupportFloorChecks"); case ECounter::TunnelSupportFloorChecks:return TEXT("TunnelSupportFloorChecks");
case ECounter::TunnelSupportFloorNaturalTests:return TEXT("TunnelSupportFloorNaturalTests");
case ECounter::TunnelSupportFloorNaturalSkips:return TEXT("TunnelSupportFloorNaturalSkips");
case ECounter::PassageCandidates: return TEXT("PassageCandidates"); case ECounter::PassageCandidates: return TEXT("PassageCandidates");
case ECounter::PassageEvaluated: return TEXT("PassageEvaluated"); case ECounter::PassageEvaluated: return TEXT("PassageEvaluated");
case ECounter::MesherBlockTests: return TEXT("MesherBlockTests"); case ECounter::MesherBlockTests: return TEXT("MesherBlockTests");
@@ -467,8 +465,6 @@ namespace VoxelDensityProfile
{ TEXT("OriginSpineOp"), EBucket::OriginSpineOp }, { TEXT("OriginSpineOp"), EBucket::OriginSpineOp },
{ TEXT("BoundarySealOp"), EBucket::BoundarySealOp }, { TEXT("BoundarySealOp"), EBucket::BoundarySealOp },
{ TEXT("PassageCarveOp"), EBucket::PassageCarveOp }, { TEXT("PassageCarveOp"), EBucket::PassageCarveOp },
{ TEXT("CaveTunnelFloorOp"), EBucket::CaveTunnelFloorOp },
{ TEXT("CaveTunnelAirOp"), EBucket::CaveTunnelAirOp },
{ TEXT("XYEdgeSealOp"), EBucket::XYEdgeSealOp }, { TEXT("XYEdgeSealOp"), EBucket::XYEdgeSealOp },
{ TEXT("DensityPrologue"), EBucket::DensityPrologue }, { TEXT("DensityPrologue"), EBucket::DensityPrologue },
{ TEXT("DensityCore"), EBucket::DensityCore }, { TEXT("DensityCore"), EBucket::DensityCore },
@@ -529,8 +525,6 @@ namespace VoxelDensityProfile
case EBucket::OriginSpineOp: return TEXT("OriginSpineOp"); case EBucket::OriginSpineOp: return TEXT("OriginSpineOp");
case EBucket::BoundarySealOp: return TEXT("BoundarySealOp"); case EBucket::BoundarySealOp: return TEXT("BoundarySealOp");
case EBucket::PassageCarveOp: return TEXT("PassageCarveOp"); case EBucket::PassageCarveOp: return TEXT("PassageCarveOp");
case EBucket::CaveTunnelFloorOp:return TEXT("CaveTunnelFloorOp");
case EBucket::CaveTunnelAirOp: return TEXT("CaveTunnelAirOp");
case EBucket::XYEdgeSealOp: return TEXT("XYEdgeSealOp"); case EBucket::XYEdgeSealOp: return TEXT("XYEdgeSealOp");
case EBucket::OtherOp: return TEXT("OtherOp"); case EBucket::OtherOp: return TEXT("OtherOp");
case EBucket::DensityPrologue: return TEXT("DensityPrologue"); case EBucket::DensityPrologue: return TEXT("DensityPrologue");
+22 -51
View File
@@ -507,11 +507,7 @@ namespace
static void BuildTunnelSupportColumnsForChunk( static void BuildTunnelSupportColumnsForChunk(
FChunkSDFCache& Cache, FChunkSDFCache& Cache,
const FIntVector& ChunkCoord, const FIntVector& ChunkCoord);
const UVoxelGenerator* Generator,
const FStrateGenerationParams& Params,
uint32 ParamsFingerprint,
uint32 LayoutVersion);
static const FTunnelSupportFloorColumn* FindTunnelSupportColumn( static const FTunnelSupportFloorColumn* FindTunnelSupportColumn(
const FChunkSDFCache& Cache, const FChunkSDFCache& Cache,
@@ -607,7 +603,6 @@ namespace
} }
void PrepareTunnelCoreCache( void PrepareTunnelCoreCache(
const UVoxelGenerator& Generator,
const UVoxelStrateManager& Manager, const UVoxelStrateManager& Manager,
const FIntVector& ChunkCoord, const FIntVector& ChunkCoord,
const FStrateGenerationParams& Params, const FStrateGenerationParams& Params,
@@ -644,8 +639,7 @@ namespace
Params, static_cast<uint32>(Manager.GetWorldSeed()), Params, static_cast<uint32>(Manager.GetWorldSeed()),
StrateIndex, nullptr); StrateIndex, nullptr);
BuildTunnelSupportColumnsForChunk( BuildTunnelSupportColumnsForChunk(
OutState.Cache, ChunkCoord, &Generator, Params, OutState.Cache, ChunkCoord);
ParamsFingerprint, LayoutVersion);
OutState.OwnerId = OwnerId; OutState.OwnerId = OwnerId;
OutState.ManagerLifetimeId = ManagerLifetimeId; OutState.ManagerLifetimeId = ManagerLifetimeId;
OutState.Chunk = ChunkCoord; OutState.Chunk = ChunkCoord;
@@ -656,11 +650,7 @@ namespace
static void BuildTunnelSupportColumnsForChunk( static void BuildTunnelSupportColumnsForChunk(
FChunkSDFCache& Cache, FChunkSDFCache& Cache,
const FIntVector& ChunkCoord, const FIntVector& ChunkCoord)
const UVoxelGenerator* Generator,
const FStrateGenerationParams& Params,
uint32 ParamsFingerprint,
uint32 LayoutVersion)
{ {
const int32 MinX = ChunkCoord.X * CHUNK_SIZE - 1; const int32 MinX = ChunkCoord.X * CHUNK_SIZE - 1;
const int32 MinY = ChunkCoord.Y * CHUNK_SIZE - 1; const int32 MinY = ChunkCoord.Y * CHUNK_SIZE - 1;
@@ -672,25 +662,6 @@ namespace
Cache.SupportColumnEntries.Init(INDEX_NONE, Cells * Cells); Cache.SupportColumnEntries.Init(INDEX_NONE, Cells * Cells);
Cache.SupportColumns.Reset(); 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 // Empty columns are represented only by INDEX_NONE in the slot map, so
// each stored column retains the inline bands for actual graph tubes. // each stored column retains the inline bands for actual graph tubes.
for (int32 Y = 0; Y < Cells; ++Y) for (int32 Y = 0; Y < Cells; ++Y)
@@ -702,11 +673,7 @@ namespace
static_cast<float>(MinX + X), static_cast<float>(MinX + X),
static_cast<float>(MinY + Y), static_cast<float>(MinY + Y),
Cache, Cache,
Column, Column);
NaturalSampler,
Params.StrateTopWorldZ,
Params.StrateBottomWorldZ,
Params.BoundarySealThickness);
if (Column.Intervals.Num() == 0) if (Column.Intervals.Num() == 0)
{ {
continue; continue;
@@ -770,8 +737,7 @@ namespace
*/ */
bool VF_BuildOpStackForChunk(ECaveGeneratorType Type, FVoxelStackParamRefs& Refs, bool VF_BuildOpStackForChunk(ECaveGeneratorType Type, FVoxelStackParamRefs& Refs,
int32 Seed, float SpineRadius, const UVoxelStrateManager* SM, int32 Seed, float SpineRadius, const UVoxelStrateManager* SM,
FVoxelOpStack& OutStack, FVoxelOpContext& OutCtx, FVoxelOpStack& OutStack, FVoxelOpContext& OutCtx)
const UVoxelGenerator* Generator = nullptr)
{ {
switch (Type) switch (Type)
{ {
@@ -830,7 +796,7 @@ namespace
OutCtx.StrateBottomWorldZ = Refs.Tunnel->StrateBottomWorldZ; OutCtx.StrateBottomWorldZ = Refs.Tunnel->StrateBottomWorldZ;
VoxelDensityOps::BuildTunnelNetworkStack( VoxelDensityOps::BuildTunnelNetworkStack(
OutStack, *Refs.Tunnel, Seed, SpineRadius, SM, OutStack, *Refs.Tunnel, Seed, SpineRadius, SM,
/*bAppendStructuralPosts=*/true, Generator); /*bAppendStructuralPosts=*/true);
return true; return true;
default: default:
@@ -1296,7 +1262,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
CP_UseOpStack = VF_BuildOpStackForChunk( CP_UseOpStack = VF_BuildOpStackForChunk(
CP_GenType, Refs, Seed, OriginSpineRadius, 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 // 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` // 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) if (bCanPrepareNativeTunnelCore)
{ {
PrepareTunnelCoreCache( PrepareTunnelCoreCache(
*this, *StrateManager, ChunkCoord, CP_Tunnel, CP_TunnelFP, *StrateManager, ChunkCoord, CP_Tunnel, CP_TunnelFP,
LayoutVersion, ManagerLifetimeId, DensityCacheOwnerId, GTunnelCoreCache); LayoutVersion, ManagerLifetimeId, DensityCacheOwnerId, GTunnelCoreCache);
} }
else else
@@ -1574,6 +1540,9 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
if (bHaveTunnelCore) if (bHaveTunnelCore)
{ {
const bool bTunnelSupportFloor = TunnelCore.bSupportFloor; 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) if (bTunnelSupportFloor)
{ {
Result = FMath::Min( 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, float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float WorldZ,
const FStrateGenerationParams& Params, const FStrateGenerationParams& Params,
uint32 ParamsFingerprint, uint32 LayoutVersion, uint32 ParamsFingerprint, uint32 LayoutVersion,
bool bApplyLegacyStructuralPosts, bool bApplyLegacyStructuralPosts) const
bool bApplyTunnelSupportFloor) const
{ {
//========================================================================= //=========================================================================
// STRATE DENSITY FUNCTION (Morphology Pipeline) // 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, // 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). // 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_Zp1 = VoxelCaveMorphology::EvaluateSDFCached(
float SDF_Zm1 = VoxelCaveMorphology::EvaluateSDFCached(WorldX, WorldY, WorldZ - 1.0f, SDFCache, Params.SDFBlendRadius); 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; float GZ = (SDF_Zp1 - SDF_Zm1) * 0.5f;
// Normalized vertical component: 1 = perfectly horizontal surface (floor/ceiling) // Normalized vertical component: 1 = perfectly horizontal surface (floor/ceiling)
// 0 = perfectly vertical surface (wall) // 0 = perfectly vertical surface (wall)
@@ -2748,12 +2718,13 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
if (bApplyLegacyStructuralPosts) if (bApplyLegacyStructuralPosts)
{ {
// Terrain operations and passage support floors are allowed to write solid density, but // 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 // 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. // same predicates once in its common MC-space post section below both generation paths.
const bool bTunnelSupportFloor = bApplyTunnelSupportFloor const bool bTunnelSupportFloor = VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
&& VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint( WorldX, WorldY, WorldZ, SDFCache);
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) if (bTunnelSupportFloor)
{ {
Density = FMath::Max(Density, FMath::Max(Params.BaseDensity * 2.0f, 1.0f)); 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; FVoxelOpStack TileStack;
if (!VF_BuildOpStackForChunk(CaveType, Refs, Seed, OriginSpineRadius, 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`, // 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. // donc la pile ne décrit pas ce que le mesher verra. Aucun verdict.
+13 -15
View File
@@ -640,6 +640,11 @@ struct FCachedTunnel
// turning a gentle authored floor ramp into a vertical break when the cave warp is nonlinear. // turning a gentle authored floor ramp into a vertical break when the cave warp is nonlinear.
TArray<FVector> WorldControlPoints; TArray<FVector> WorldControlPoints;
TArray<float> WorldControlRadii; TArray<float> WorldControlRadii;
// The corridor floor is a swept SmoothMax cut, not a later slab. These are copied from the
// same strate fields used by room floors; the seed is derived once from the tunnel pair hash.
float FloorReliefStrength = 0.0f;
float FloorReliefFrequency = 0.015f;
uint32 FloorSeed = 0;
FVector WorldBoundCenter = FVector::ZeroVector; FVector WorldBoundCenter = FVector::ZeroVector;
float WorldBoundRadiusSq = 0.0f; float WorldBoundRadiusSq = 0.0f;
// Centerline AABBs and scalar influence radii used by the immutable broad phase. The // Centerline AABBs and scalar influence radii used by the immutable broad phase. The
@@ -698,12 +703,6 @@ struct FTunnelSupportFloorColumn
} }
}; };
// A non-owning probe used only while a support-column is being prepared. The caller supplies the
// exact MC-facing density field with the graph support slab disabled; the morphology layer then
// runs the same player-fit predicate used by the capability measurement. A missing sampler is a
// deliberate safe fallback: retain the authored support band rather than making an unproven skip.
using FTunnelSupportFloorDensitySampler = TFunctionRef<float(float, float, float)>;
// A pre-baked pit shaft — position and dimensions resolved during BuildChunkCache. // A pre-baked pit shaft — position and dimensions resolved during BuildChunkCache.
// //
// Pits are included in the main SDF evaluation (SmoothMin alongside rooms and // Pits are included in the main SDF evaluation (SmoothMin alongside rooms and
@@ -771,6 +770,10 @@ struct FChunkSDFCache
TArray<int32> SupportColumnEntries; TArray<int32> SupportColumnEntries;
TArray<FTunnelSupportFloorColumn> SupportColumns; TArray<FTunnelSupportFloorColumn> SupportColumns;
// Kept with the cache because the world-space structural evaluator has no params argument.
// A zero value is a valid hard-intersection fallback for legacy/empty caches.
float SDFBlendRadius = 0.0f;
// Allocator-backed bytes owned by this cache, including the nested tunnel chains. The returned // Allocator-backed bytes owned by this cache, including the nested tunnel chains. The returned
// value excludes sizeof(FChunkSDFCache) itself so callers can add the enclosing entry's inline // value excludes sizeof(FChunkSDFCache) itself so callers can add the enclosing entry's inline
// storage exactly once. // storage exactly once.
@@ -909,18 +912,13 @@ namespace VoxelCaveMorphology
const FChunkSDFCache& Cache const FChunkSDFCache& Cache
); );
// Build/query the same support-floor predicate at column granularity. When NaturalSampler is // Build/query the same support-floor predicate at column granularity. The column stores its
// supplied, a projected band is omitted only if the exact field with that graph slab disabled // finite support interval, so every Z sample in a chunk reuses the same deterministic
// accepts a player-fit pose at this column. Without that proof the old interval is retained. // projection and never re-runs a natural-floor validator.
// The sampler is synchronous and non-owning; it is never retained by the cache.
VOXELFORGE_API void BuildTunnelSupportFloorColumn( VOXELFORGE_API void BuildTunnelSupportFloorColumn(
float WorldX, float WorldY, float WorldX, float WorldY,
const FChunkSDFCache& Cache, const FChunkSDFCache& Cache,
FTunnelSupportFloorColumn& OutColumn, FTunnelSupportFloorColumn& OutColumn
const FTunnelSupportFloorDensitySampler* NaturalSampler = nullptr,
float NaturalStrateTopZ = 0.0f,
float NaturalStrateBottomZ = 0.0f,
float NaturalBoundarySealThickness = 0.0f
); );
VOXELFORGE_API bool IsTunnelSupportFloorColumnZ( VOXELFORGE_API bool IsTunnelSupportFloorColumnZ(
@@ -474,8 +474,7 @@ namespace VoxelDensityOps
const FStrateGenerationParams& P, const FStrateGenerationParams& P,
int32 Seed, float SpineRadius, int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager, const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts = true, bool bAppendStructuralPosts = true);
const UVoxelGenerator* Generator = nullptr);
/** /**
* DIAGNOSTIC — la ventilation par CLASSE DE PRIMITIVE de la dernière propagation d'intervalle * DIAGNOSTIC — la ventilation par CLASSE DE PRIMITIVE de la dernière propagation d'intervalle
@@ -54,8 +54,6 @@ namespace VoxelDensityProfile
OriginSpineOp, OriginSpineOp,
BoundarySealOp, BoundarySealOp,
PassageCarveOp, PassageCarveOp,
CaveTunnelFloorOp,
CaveTunnelAirOp,
XYEdgeSealOp, XYEdgeSealOp,
OtherOp, OtherOp,
// Non-overlapping top-level phases. The named operation buckets above are intentionally // Non-overlapping top-level phases. The named operation buckets above are intentionally
@@ -98,8 +96,6 @@ namespace VoxelDensityProfile
TunnelSupportColumnCandidates, TunnelSupportColumnCandidates,
TunnelSupportFloorQueries, TunnelSupportFloorQueries,
TunnelSupportFloorChecks, TunnelSupportFloorChecks,
TunnelSupportFloorNaturalTests,
TunnelSupportFloorNaturalSkips,
PassageCandidates, PassageCandidates,
PassageEvaluated, PassageEvaluated,
MesherBlockTests, MesherBlockTests,
+1 -2
View File
@@ -167,8 +167,7 @@ public:
float GetDensityWithParams(float WorldX, float WorldY, float WorldZ, float GetDensityWithParams(float WorldX, float WorldY, float WorldZ,
const FStrateGenerationParams& Params, const FStrateGenerationParams& Params,
uint32 ParamsFingerprint, uint32 LayoutVersion, uint32 ParamsFingerprint, uint32 LayoutVersion,
bool bApplyLegacyStructuralPosts = true, bool bApplyLegacyStructuralPosts = true) const;
bool bApplyTunnelSupportFloor = true) const;
/** /**
* Densité pour une strate Slab (FlatPlain / CrystalChamber). * Densité pour une strate Slab (FlatPlain / CrystalChamber).
@@ -21,6 +21,7 @@ namespace VoxelPassageGeometry
MinimumTurnFloorWidthVoxels * 0.5f; MinimumTurnFloorWidthVoxels * 0.5f;
constexpr float MinimumRoomHeightVoxels = 12.0f; // 3 m constexpr float MinimumRoomHeightVoxels = 12.0f; // 3 m
constexpr float LandingFloorThicknessVoxels = 3.0f; constexpr float LandingFloorThicknessVoxels = 3.0f;
constexpr float MaxStepHeightVoxels = 1.8f; // 45 cm at 25 cm/voxel
constexpr float SealSafetyMarginVoxels = 4.0f; constexpr float SealSafetyMarginVoxels = 4.0f;
constexpr float RoomRoundingVoxels = 1.5f; constexpr float RoomRoundingVoxels = 1.5f;
constexpr float LandingCarveBlendVoxels = 4.0f; constexpr float LandingCarveBlendVoxels = 4.0f;
@@ -47,6 +48,10 @@ namespace VoxelPassageGeometry
// latter is a scramble limit, not a sensible default for a tunnel the player walks through. // latter is a scramble limit, not a sensible default for a tunnel the player walks through.
constexpr float WalkableTunnelMaxGradientDegrees = 15.0f; constexpr float WalkableTunnelMaxGradientDegrees = 15.0f;
constexpr float WalkableTunnelMaxGradient = 0.2679491924311227f; // tan(15 degrees) constexpr float WalkableTunnelMaxGradient = 0.2679491924311227f; // tan(15 degrees)
// Player-fit measurements use the engine's 44 degree walkability contract. The 15 degree
// value above remains the preferred authored tunnel ramp; this wider bound is only the
// fallback threshold at which a room-to-room floor must become a terrace.
constexpr float PlayerWalkableFloorMaxGradient = 0.9656887748070738f; // tan(44 degrees)
// The walkability contract is about the floor carried by the tube, not only its centreline. // 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 // A tapered tube changes that floor by the radius delta, so the construction and the audit