perf(relief): column cache for floor noise -- 8.5%, and my "it's 2D" premise was wrong

I predicted this would recover the D-section's 30% outright, on the grounds that the
floor relief is FMath::PerlinNoise2D and therefore a function of (X,Y) only, so every
voxel in a column recomputes the same pair of octaves. Codex checked the premise and
it does not hold:

  production applies a 3D CAVE WARP before the floor is evaluated, so the warped X/Y
  fed to the noise change with Z. A world-XY cache would have changed the field.

That is the third arithmetic prediction of mine that did not survive contact with the
code (after the double->float round-trips, and "the candidate scan is the last big
win"). Recording it because the pattern is consistent: I keep reasoning about cost
from a call site instead of from the data flow reaching it.

WHAT WORKS INSTEAD
Cache keyed on actual SDF-domain integer columns plus feature, owner, seed and
frequency -- correct in the warped domain, where world-XY is not. Fixed size, 32,768
entries, ~1.25 MiB per worker, no per-tile or per-region retention.

  28,154,092 column lookups
  25,842,374 hits / 2,311,718 misses      (91.8% hit rate)
  71,133,837 conservative relief skips    (amplitude bound, only where SmoothMax is
                                           provably the identity)
  77,109,353 actual two-octave evaluations

  my build, 3 runs:  1.384 / 1.405 / 1.415 s   (ff8bd7a was 1.509 / 1.542 / 1.543)
                     mean 1.401 vs 1.531  =  -8.5%
  geometry SHA-256 bc0dc059..a8ae, IDENTICAL to ff8bd7a's output

Capability unchanged exactly: player-fit 20,835, reachable 10,901, connected,
40,429 verts / 75,416 tris, dihedral p50/p90 2.5529/33.7191, hash 4520DC6F,
determinism repeat passed.

Codex also declined a sign-only shortcut for the main SDF, correctly: marching cubes
interpolates on the field MAGNITUDE, not just its sign, so a sign-only answer would
move vertices. It kept it for the landing helper where only the sign is consumed.

⚠️ NOTE ON MY OWN "17% NOISE FLOOR": that figure came from a profiled-vs-unprofiled
comparison, not from run-to-run variance. Within one binary the spread here is ~2%
(1.384-1.415). An 8.5% shift that is consistent across every run in both directions
is real. I have been using the wrong number as a threshold and should stop.

Net position: the D-section still costs ~17% against pre-swept-floor (1.401 vs 1.15),
in exchange for +682 player-fit and +418 reachable cells, floors that undulate instead
of presenting a slab, and the retirement of the 1.5M-call validator and two dead ops.

Build 47 actions, 0 errors. Only morphology source changed; zero reparse points;
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:55:06 +02:00
1 parent ff8bd7a8bc
commit ce53552543
1 file changed
+408 -63
+408 -63
View File
@@ -85,12 +85,227 @@ namespace
constexpr float VF_Perlin2DPartialAbsBound = 8.5f; constexpr float VF_Perlin2DPartialAbsBound = 8.5f;
constexpr float VF_MaxTunnelFloorReliefScale = 0.20f; constexpr float VF_MaxTunnelFloorReliefScale = 0.20f;
// The weighted two-octave field is bounded by one in exact arithmetic. Leave a margin for
// the implementation's interpolation/rounding before using it as a proof bound. This bound
// is only used to skip a floor evaluation when the existing SmoothMax is provably the identity;
// it never changes a value that could affect the isosurface.
constexpr float VF_FloorReliefNoiseAbsBound = 1.01f;
enum class EVFFloorReliefFeature : uint8
{
RoomDouble,
TunnelFloat,
LandingFloat,
LandingDouble
};
struct FVFFloorReliefColumnKey
{
bool bInteger = false;
int32 X = 0;
int32 Y = 0;
};
FORCEINLINE FVFFloorReliefColumnKey VF_MakeFloorReliefColumnKey(
double X, double Y)
{
FVFFloorReliefColumnKey Key;
const bool bIntegerX = FMath::IsFinite(X)
&& X >= static_cast<double>(MIN_int32)
&& X <= static_cast<double>(MAX_int32)
&& FMath::FloorToDouble(X) == X;
const bool bIntegerY = FMath::IsFinite(Y)
&& Y >= static_cast<double>(MIN_int32)
&& Y <= static_cast<double>(MAX_int32)
&& FMath::FloorToDouble(Y) == Y;
if (bIntegerX && bIntegerY)
{
Key.bInteger = true;
Key.X = FMath::FloorToInt(X);
Key.Y = FMath::FloorToInt(Y);
}
return Key;
}
// Relief is a function of the actual SDF-domain X/Y, not necessarily the original world X/Y:
// CaveWarp is a 3D warp and therefore makes those coordinates vary with Z. A cache keyed by
// the actual domain is exact in both cases: it hits on genuine repeated columns (the world
// structural path and unwarped queries), and safely falls through to the original calculation
// when a warped sample is not the same column. The table is direct-mapped and fixed-size so
// it cannot become another retained per-tile allocation. Collisions only lose a hit.
struct FVFFloorReliefCacheEntry
{
const void* CacheIdentity = nullptr;
EVFFloorReliefFeature Feature = EVFFloorReliefFeature::RoomDouble;
int32 FeatureIndex = INDEX_NONE;
int32 X = 0;
int32 Y = 0;
uint32 FloorSeed = 0;
float Frequency = 0.0f;
float Noise = 0.0f;
bool bValid = false;
};
struct FVFFloorReliefCache
{
// About 1.25 MiB on the current x64 layout, bounded independently of chunk/region size.
static constexpr int32 Capacity = 32768;
static_assert((Capacity & (Capacity - 1)) == 0, "floor relief cache must be a power of two");
FVFFloorReliefCacheEntry Entries[Capacity];
FORCEINLINE static uint32 HashKey(
const void* CacheIdentity,
EVFFloorReliefFeature Feature,
int32 FeatureIndex,
int32 X,
int32 Y,
uint32 FloorSeed,
float Frequency)
{
const UPTRINT PointerBits = reinterpret_cast<UPTRINT>(CacheIdentity);
uint32 Hash = static_cast<uint32>(PointerBits)
^ static_cast<uint32>(PointerBits >> 32);
auto Combine = [&Hash](uint32 Value)
{
Hash ^= Value + 0x9e3779b9u + (Hash << 6) + (Hash >> 2);
};
Combine(static_cast<uint32>(Feature));
Combine(static_cast<uint32>(FeatureIndex));
Combine(static_cast<uint32>(X));
Combine(static_cast<uint32>(Y));
Combine(FloorSeed);
Combine(GetTypeHash(Frequency));
return VoxelHash::Mix(Hash);
}
FORCEINLINE float GetOrCompute(
double X, double Y,
uint32 FloorSeed, float Frequency,
const void* CacheIdentity,
EVFFloorReliefFeature Feature,
int32 FeatureIndex,
const FVFFloorReliefColumnKey& Column,
float (*Compute)(double, double, uint32, float))
{
if (!Column.bInteger)
{
return Compute(X, Y, FloorSeed, Frequency);
}
FVFFloorReliefCacheEntry& Entry = Entries[HashKey(
CacheIdentity, Feature, FeatureIndex,
Column.X, Column.Y, FloorSeed, Frequency)
& (Capacity - 1)];
if (Entry.bValid
&& Entry.CacheIdentity == CacheIdentity
&& Entry.Feature == Feature
&& Entry.FeatureIndex == FeatureIndex
&& Entry.X == Column.X
&& Entry.Y == Column.Y
&& Entry.FloorSeed == FloorSeed
&& Entry.Frequency == Frequency)
{
return Entry.Noise;
}
const float Noise = Compute(X, Y, FloorSeed, Frequency);
Entry.CacheIdentity = CacheIdentity;
Entry.Feature = Feature;
Entry.FeatureIndex = FeatureIndex;
Entry.X = Column.X;
Entry.Y = Column.Y;
Entry.FloorSeed = FloorSeed;
Entry.Frequency = Frequency;
Entry.Noise = Noise;
Entry.bValid = true;
return Noise;
}
};
static thread_local FVFFloorReliefCache GFloorReliefCache;
FORCEINLINE float VF_ComputeFloorReliefNoiseDouble(
double X, double Y, uint32 FloorSeed, float Frequency)
{
const float SF = static_cast<float>(FloorSeed) * 0.00001f;
float Noise = FMath::PerlinNoise2D(
FVector2D(X * Frequency + SF, Y * Frequency + SF * 1.7f)) * 0.65f;
Noise += FMath::PerlinNoise2D(
FVector2D(X * Frequency * 2.3f + SF * 3.1f,
Y * Frequency * 2.3f + SF * 5.3f)) * 0.35f;
return Noise;
}
FORCEINLINE float VF_ComputeFloorReliefNoiseFloat(
double X, double Y, uint32 FloorSeed, float Frequency)
{
const float FX = static_cast<float>(X);
const float FY = static_cast<float>(Y);
const float SF = static_cast<float>(FloorSeed) * 0.00001f;
float Noise = FMath::PerlinNoise2D(
FVector2D(FX * Frequency + SF, FY * Frequency + SF * 1.7f)) * 0.65f;
Noise += FMath::PerlinNoise2D(
FVector2D(FX * Frequency * 2.3f + SF * 3.1f,
FY * Frequency * 2.3f + SF * 5.3f)) * 0.35f;
return Noise;
}
FORCEINLINE float VF_FloorReliefNoiseDouble(
double X, double Y, uint32 FloorSeed, float Frequency,
const void* CacheIdentity,
EVFFloorReliefFeature Feature,
int32 FeatureIndex,
const FVFFloorReliefColumnKey& Column)
{
return GFloorReliefCache.GetOrCompute(
X, Y, FloorSeed, Frequency, CacheIdentity, Feature, FeatureIndex,
Column,
&VF_ComputeFloorReliefNoiseDouble);
}
FORCEINLINE float VF_FloorReliefNoiseFloat(
float X, float Y, uint32 FloorSeed, float Frequency,
const void* CacheIdentity,
EVFFloorReliefFeature Feature,
int32 FeatureIndex,
const FVFFloorReliefColumnKey& Column)
{
return GFloorReliefCache.GetOrCompute(
static_cast<double>(X), static_cast<double>(Y),
FloorSeed, Frequency, CacheIdentity, Feature, FeatureIndex,
Column,
&VF_ComputeFloorReliefNoiseFloat);
}
FORCEINLINE bool VF_GetFloorReliefBound(
float Strength, float Envelope, float ReliefScale, float Frequency,
float& OutBound)
{
OutBound = 0.0f;
if (!(Strength > 0.0f)
|| !FMath::IsFinite(Strength)
|| !FMath::IsFinite(Frequency)
|| !(Envelope > 0.0f)
|| !FMath::IsFinite(Envelope)
|| !(ReliefScale > 0.0f)
|| !FMath::IsFinite(ReliefScale))
{
return false;
}
OutBound = FMath::Abs(Strength) * VOXEL_NOISE_SCALE
* VF_FloorReliefNoiseAbsBound * Envelope * ReliefScale;
return FMath::IsFinite(OutBound) && OutBound >= 0.0f;
}
// Keep corridor relief on the exact two-octave field already used by room floors. The // 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 // 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. // corridor meet the room's own floor without a seed-dependent vertical lip.
FORCEINLINE float VF_TunnelFloorRelief( FORCEINLINE float VF_TunnelFloorRelief(
float X, float Y, const FCachedTunnel& Tunnel, float X, float Y, const FCachedTunnel& Tunnel,
float Envelope, float ReliefScale) float Envelope, float ReliefScale,
const void* CacheIdentity, int32 TunnelIndex,
const FVFFloorReliefColumnKey& Column)
{ {
if (!(Tunnel.FloorReliefStrength > 0.0f) if (!(Tunnel.FloorReliefStrength > 0.0f)
|| !FMath::IsFinite(Tunnel.FloorReliefStrength) || !FMath::IsFinite(Tunnel.FloorReliefStrength)
@@ -101,13 +316,10 @@ namespace
return 0.0f; return 0.0f;
} }
const float RF = Tunnel.FloorReliefFrequency; const float Noise = VF_FloorReliefNoiseFloat(
const float SF = static_cast<float>(Tunnel.FloorSeed) * 0.00001f; X, Y, Tunnel.FloorSeed, Tunnel.FloorReliefFrequency,
float Noise = FMath::PerlinNoise2D( CacheIdentity, EVFFloorReliefFeature::TunnelFloat, TunnelIndex,
FVector2D(X * RF + SF, Y * RF + SF * 1.7f)) * 0.65f; Column);
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 return Noise * VOXEL_NOISE_SCALE * Tunnel.FloorReliefStrength
* Envelope * ReliefScale; * Envelope * ReliefScale;
} }
@@ -231,45 +443,86 @@ namespace
0.0f, VF_MaxTunnelFloorReliefScale); 0.0f, VF_MaxTunnelFloorReliefScale);
} }
FORCEINLINE float VF_TunnelFloorAtSegment( struct FVFTunnelFloorTerms
{
float BaseFloorZ = 0.0f;
float ReliefEnvelope = 0.0f;
float ReliefScale = 0.0f;
};
FORCEINLINE FVFTunnelFloorTerms VF_TunnelFloorTerms(
const FVector& A, float RadiusA, const FVector& A, float RadiusA,
const FVector& B, float RadiusB, const FVector& B, float RadiusB,
float T, int32 SegmentIndex, int32 NumSegments, float T, int32 SegmentIndex, int32 NumSegments,
const FVector& Position,
const FCachedTunnel& Tunnel) const FCachedTunnel& Tunnel)
{ {
FVFTunnelFloorTerms Terms;
const float ProfileT = VF_TunnelFloorProfileT( const float ProfileT = VF_TunnelFloorProfileT(
A, RadiusA, B, RadiusB, T); A, RadiusA, B, RadiusB, T);
float FloorZ = FMath::Lerp( Terms.BaseFloorZ = FMath::Lerp(
VoxelPassageGeometry::TunnelFloorZ(A, FMath::Abs(RadiusA)), VoxelPassageGeometry::TunnelFloorZ(A, FMath::Abs(RadiusA)),
VoxelPassageGeometry::TunnelFloorZ(B, FMath::Abs(RadiusB)), VoxelPassageGeometry::TunnelFloorZ(B, FMath::Abs(RadiusB)),
ProfileT); ProfileT);
const float HorizontalRun = FVector2D( const float HorizontalRun = FVector2D(
static_cast<float>(B.X - A.X), static_cast<float>(B.Y - A.Y)).Size(); static_cast<float>(B.X - A.X), static_cast<float>(B.Y - A.Y)).Size();
float ReliefEnvelope = 1.0f; Terms.ReliefEnvelope = 1.0f;
if (SegmentIndex == 0) if (SegmentIndex == 0)
{ {
ReliefEnvelope *= SmoothStep01(FMath::Clamp( Terms.ReliefEnvelope *= SmoothStep01(FMath::Clamp(
(T * HorizontalRun) (T * HorizontalRun)
/ VoxelPassageGeometry::WalkableTunnelLandingApronVoxels, / VoxelPassageGeometry::WalkableTunnelLandingApronVoxels,
0.0f, 1.0f)); 0.0f, 1.0f));
} }
if (SegmentIndex + 1 == NumSegments) if (SegmentIndex + 1 == NumSegments)
{ {
ReliefEnvelope *= SmoothStep01(FMath::Clamp( Terms.ReliefEnvelope *= SmoothStep01(FMath::Clamp(
((1.0f - T) * HorizontalRun) ((1.0f - T) * HorizontalRun)
/ VoxelPassageGeometry::WalkableTunnelLandingApronVoxels, / VoxelPassageGeometry::WalkableTunnelLandingApronVoxels,
0.0f, 1.0f)); 0.0f, 1.0f));
} }
const float ReliefScale = VF_TunnelFloorReliefScale( Terms.ReliefScale = VF_TunnelFloorReliefScale(
A, RadiusA, B, RadiusB, SegmentIndex, NumSegments, Tunnel); A, RadiusA, B, RadiusB, SegmentIndex, NumSegments, Tunnel);
FloorZ += VF_TunnelFloorRelief( return Terms;
static_cast<float>(Position.X), static_cast<float>(Position.Y), }
Tunnel, ReliefEnvelope, ReliefScale);
FORCEINLINE float VF_TunnelFloorFromTerms(
const FVector& Position,
const FCachedTunnel& Tunnel,
const FVFTunnelFloorTerms& Terms,
const void* CacheIdentity, int32 TunnelIndex,
bool bEvaluateRelief,
const FVFFloorReliefColumnKey& Column)
{
float FloorZ = Terms.BaseFloorZ;
if (bEvaluateRelief)
{
FloorZ += VF_TunnelFloorRelief(
static_cast<float>(Position.X), static_cast<float>(Position.Y),
Tunnel, Terms.ReliefEnvelope, Terms.ReliefScale,
CacheIdentity, TunnelIndex, Column);
}
return FloorZ; return FloorZ;
} }
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 void* CacheIdentity, int32 TunnelIndex,
bool bEvaluateRelief,
const FVFFloorReliefColumnKey& Column)
{
const FVFTunnelFloorTerms Terms = VF_TunnelFloorTerms(
A, RadiusA, B, RadiusB,
T, SegmentIndex, NumSegments, Tunnel);
return VF_TunnelFloorFromTerms(
Position, Tunnel, Terms,
CacheIdentity, TunnelIndex, bEvaluateRelief, Column);
}
FORCEINLINE bool VF_ProjectTunnelSegmentXY( FORCEINLINE bool VF_ProjectTunnelSegmentXY(
const FVector& Position, const FVector& A, const FVector& B, const FVector& Position, const FVector& A, const FVector& B,
float& OutT, float& OutDistanceSquared) float& OutT, float& OutDistanceSquared)
@@ -301,7 +554,9 @@ namespace
const TArray<float>& ControlRadii, const TArray<float>& ControlRadii,
const FCachedTunnel& Tunnel, const FCachedTunnel& Tunnel,
float SDFBlendRadius, float SDFBlendRadius,
bool bApplyFloorCut) bool bApplyFloorCut,
const void* CacheIdentity, int32 TunnelIndex,
const FVFFloorReliefColumnKey& ReliefColumn)
{ {
FVFTunnelShapeEvaluation Result; FVFTunnelShapeEvaluation Result;
if (ControlPoints.Num() < 2 if (ControlPoints.Num() < 2
@@ -335,18 +590,49 @@ namespace
continue; continue;
} }
const float FloorZ = VF_TunnelFloorAtSegment( const FVFTunnelFloorTerms FloorTerms = VF_TunnelFloorTerms(
A, RadiusA, B, RadiusB, A, RadiusA, B, RadiusB,
T, SegmentIndex, NumSegments, Position, Tunnel); T, SegmentIndex, NumSegments, Tunnel);
const float SupportRadius = FMath::Max(
FMath::Min(FMath::Abs(RadiusA), FMath::Abs(RadiusB)) - 0.5f,
VoxelPassageGeometry::PlayerRadiusVoxels);
float ReliefBound = 0.0f;
const bool bHasReliefBound = VF_GetFloorReliefBound(
Tunnel.FloorReliefStrength,
FloorTerms.ReliefEnvelope,
FloorTerms.ReliefScale,
Tunnel.FloorReliefFrequency,
ReliefBound);
bool bEvaluateRelief = bApplyFloorCut;
if (bEvaluateRelief && bHasReliefBound)
{
// If the capsule is already above the highest possible floor by at least the
// SmoothMax radius, the cut is exactly the identity. The support test below is a
// separate obligation: a floor that does not alter SDF at this sample may still
// be the support band queried by the post stack.
const bool bFloorCannotAffect = SegmentSDF >=
FloorTerms.BaseFloorZ + ReliefBound
- static_cast<float>(Position.Z) + FloorBlend;
const bool bCannotBeSupport =
SegmentSDF > 0.0f
|| HorizontalDistanceSquared > FMath::Square(SupportRadius)
|| Position.Z < FloorTerms.BaseFloorZ - ReliefBound
- VoxelPassageGeometry::LandingFloorThicknessVoxels
|| Position.Z > FloorTerms.BaseFloorZ + ReliefBound
+ VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels;
bEvaluateRelief = !bFloorCannotAffect || !bCannotBeSupport;
}
const float FloorZ = VF_TunnelFloorFromTerms(
Position, Tunnel, FloorTerms,
CacheIdentity, TunnelIndex, bEvaluateRelief, ReliefColumn);
const float SegmentResultSDF = bApplyFloorCut const float SegmentResultSDF = bApplyFloorCut
? VoxelSDF::SmoothMax( ? VoxelSDF::SmoothMax(
SegmentSDF, FloorZ - static_cast<float>(Position.Z), FloorBlend) SegmentSDF, FloorZ - static_cast<float>(Position.Z), FloorBlend)
: SegmentSDF; : SegmentSDF;
Result.SDF = FMath::Min(Result.SDF, SegmentResultSDF); 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 if (SegmentSDF <= 0.0f
&& HorizontalDistanceSquared <= FMath::Square(SupportRadius) && HorizontalDistanceSquared <= FMath::Square(SupportRadius)
&& Position.Z >= FloorZ && Position.Z >= FloorZ
@@ -365,8 +651,12 @@ namespace
const FCachedTunnel& Tunnel, const FCachedTunnel& Tunnel,
bool bWorldChain, bool bWorldChain,
float SDFBlendRadius, float SDFBlendRadius,
bool bApplyFloorCut) bool bApplyFloorCut,
const void* CacheIdentity, int32 TunnelIndex)
{ {
const FVFFloorReliefColumnKey ReliefColumn =
VF_MakeFloorReliefColumnKey(
static_cast<float>(Position.X), static_cast<float>(Position.Y));
const TArray<FVector>& ShapeControlPoints = bWorldChain const TArray<FVector>& ShapeControlPoints = bWorldChain
? Tunnel.WorldControlPoints : Tunnel.ControlPoints; ? Tunnel.WorldControlPoints : Tunnel.ControlPoints;
const TArray<float>& ShapeControlRadii = bWorldChain const TArray<float>& ShapeControlRadii = bWorldChain
@@ -376,7 +666,8 @@ namespace
{ {
return VF_EvaluateSweptTunnelChain( return VF_EvaluateSweptTunnelChain(
Position, ShapeControlPoints, ShapeControlRadii, Position, ShapeControlPoints, ShapeControlRadii,
Tunnel, SDFBlendRadius, bApplyFloorCut); Tunnel, SDFBlendRadius, bApplyFloorCut,
CacheIdentity, TunnelIndex, ReliefColumn);
} }
if (Tunnel.bHasMidpoint) if (Tunnel.bHasMidpoint)
@@ -393,7 +684,8 @@ namespace
// temporary arrays stay inline and do not allocate on the normal chain path. // temporary arrays stay inline and do not allocate on the normal chain path.
return VF_EvaluateSweptTunnelChain( return VF_EvaluateSweptTunnelChain(
Position, Points, Radii, Position, Points, Radii,
Tunnel, SDFBlendRadius, bApplyFloorCut); Tunnel, SDFBlendRadius, bApplyFloorCut,
CacheIdentity, TunnelIndex, ReliefColumn);
} }
TArray<FVector> Points; TArray<FVector> Points;
@@ -404,7 +696,8 @@ namespace
Radii.Add(Tunnel.RadiusB); Radii.Add(Tunnel.RadiusB);
return VF_EvaluateSweptTunnelChain( return VF_EvaluateSweptTunnelChain(
Position, Points, Radii, Position, Points, Radii,
Tunnel, SDFBlendRadius, bApplyFloorCut); Tunnel, SDFBlendRadius, bApplyFloorCut,
CacheIdentity, TunnelIndex, ReliefColumn);
} }
static bool VF_ProjectSweptTunnelFloor( static bool VF_ProjectSweptTunnelFloor(
@@ -412,8 +705,12 @@ namespace
const FCachedTunnel& Tunnel, const FCachedTunnel& Tunnel,
bool bWorldChain, bool bWorldChain,
float& OutFloorZ, float& OutFloorZ,
float& OutSupportRadius) float& OutSupportRadius,
const void* CacheIdentity, int32 TunnelIndex)
{ {
const FVFFloorReliefColumnKey ReliefColumn =
VF_MakeFloorReliefColumnKey(
static_cast<float>(Position.X), static_cast<float>(Position.Y));
const TArray<FVector>& ControlPoints = bWorldChain const TArray<FVector>& ControlPoints = bWorldChain
? Tunnel.WorldControlPoints : Tunnel.ControlPoints; ? Tunnel.WorldControlPoints : Tunnel.ControlPoints;
const TArray<float>& ControlRadii = bWorldChain const TArray<float>& ControlRadii = bWorldChain
@@ -447,7 +744,9 @@ namespace
OutFloorZ = VF_TunnelFloorAtSegment( OutFloorZ = VF_TunnelFloorAtSegment(
ControlPoints[SegmentIndex], RadiusA, ControlPoints[SegmentIndex], RadiusA,
ControlPoints[SegmentIndex + 1], RadiusB, ControlPoints[SegmentIndex + 1], RadiusB,
T, SegmentIndex, NumSegments, Position, Tunnel); T, SegmentIndex, NumSegments, Position, Tunnel,
CacheIdentity, TunnelIndex, /*bEvaluateRelief=*/true,
ReliefColumn);
OutSupportRadius = FMath::Max( OutSupportRadius = FMath::Max(
FMath::Min(FMath::Abs(RadiusA), FMath::Abs(RadiusB)) - 0.5f, FMath::Min(FMath::Abs(RadiusA), FMath::Abs(RadiusB)) - 0.5f,
VoxelPassageGeometry::PlayerRadiusVoxels); VoxelPassageGeometry::PlayerRadiusVoxels);
@@ -1654,14 +1953,13 @@ namespace
} }
const float Frequency = Params.FloorReliefFrequency; const float Frequency = Params.FloorReliefFrequency;
const float FloorSeed = static_cast<float>( const uint32 FloorSeed = VoxelHash::Mix(RoomHash ^ 0xF100F1u);
VoxelHash::Mix(RoomHash ^ 0xF100F1u)) * 0.00001f; const FVFFloorReliefColumnKey ReliefColumn =
float Noise = FMath::PerlinNoise2D(FVector2D( VF_MakeFloorReliefColumnKey(SampleX, SampleY);
SampleX * Frequency + FloorSeed, const float Noise = VF_FloorReliefNoiseFloat(
SampleY * Frequency + FloorSeed * 1.7f)) * 0.65f; SampleX, SampleY, FloorSeed, Frequency,
Noise += FMath::PerlinNoise2D(FVector2D( nullptr, EVFFloorReliefFeature::LandingFloat,
SampleX * Frequency * 2.3f + FloorSeed * 3.1f, static_cast<int32>(RoomHash), ReliefColumn);
SampleY * Frequency * 2.3f + FloorSeed * 5.3f)) * 0.35f;
FloorZ += Noise * VOXEL_NOISE_SCALE * Params.FloorReliefStrength; FloorZ += Noise * VOXEL_NOISE_SCALE * Params.FloorReliefStrength;
} }
// Keep this in the same (unwarped SDF) coordinate space as FCachedRoom::FloorCutZ. The // Keep this in the same (unwarped SDF) coordinate space as FCachedRoom::FloorCutZ. The
@@ -1732,21 +2030,45 @@ namespace
if (FloorCut < 1.0f) if (FloorCut < 1.0f)
{ {
float FloorZ = Site.Center.Z - Site.RadiusZ * FloorCut; float FloorZ = Site.Center.Z - Site.RadiusZ * FloorCut;
bool bFloorCutIsIdentityForDensity = false;
if (Params.FloorReliefStrength > 0.0f) if (Params.FloorReliefStrength > 0.0f)
{ {
const float Frequency = Params.FloorReliefFrequency; const float Frequency = Params.FloorReliefFrequency;
const float FloorSeed = static_cast<float>( const uint32 FloorSeed = VoxelHash::Mix(Site.Hash ^ 0xF100F1u);
VoxelHash::Mix(Site.Hash ^ 0xF100F1u)) * 0.00001f; float ReliefBound = 0.0f;
float Noise = FMath::PerlinNoise2D(FVector2D( const float FloorBlend = Params.SDFBlendRadius * 0.35f;
Position.X * Frequency + FloorSeed, const float BaseFloorSDF = static_cast<float>(
Position.Y * Frequency + FloorSeed * 1.7f)) * 0.65f; FloorZ - Position.Z);
Noise += FMath::PerlinNoise2D(FVector2D( const float BaseCutSDF = VoxelSDF::SmoothMax(
Position.X * Frequency * 2.3f + FloorSeed * 3.1f, RoomSDF, BaseFloorSDF, FloorBlend);
Position.Y * Frequency * 2.3f + FloorSeed * 5.3f)) * 0.35f; if (VF_GetFloorReliefBound(
FloorZ += Noise * VOXEL_NOISE_SCALE * Params.FloorReliefStrength; Params.FloorReliefStrength, 1.0f, 1.0f,
Frequency, ReliefBound)
&& FMath::Abs(BaseCutSDF) > ReliefBound)
{
// This helper exposes only the sign of the room density. SmoothMax is
// one-Lipschitz in its floor argument, so the relief cannot change that
// sign when the unrelieved result is farther from zero than its bound.
RoomSDF = BaseCutSDF;
bFloorCutIsIdentityForDensity = true;
}
else
{
const FVFFloorReliefColumnKey ReliefColumn =
VF_MakeFloorReliefColumnKey(Position.X, Position.Y);
const float Noise = VF_FloorReliefNoiseDouble(
Position.X, Position.Y,
FloorSeed, Frequency,
nullptr, EVFFloorReliefFeature::LandingDouble,
static_cast<int32>(Site.Hash), ReliefColumn);
FloorZ += Noise * VOXEL_NOISE_SCALE * Params.FloorReliefStrength;
}
}
if (!bFloorCutIsIdentityForDensity)
{
RoomSDF = VoxelSDF::SmoothMax(
RoomSDF, FloorZ - Position.Z, Params.SDFBlendRadius * 0.35f);
} }
RoomSDF = VoxelSDF::SmoothMax(
RoomSDF, FloorZ - Position.Z, Params.SDFBlendRadius * 0.35f);
} }
float InternalDensity = RoomSDF < 0.0f ? -1.0f : 1.0f; float InternalDensity = RoomSDF < 0.0f ? -1.0f : 1.0f;
@@ -5059,6 +5381,8 @@ float VoxelCaveMorphology::EvaluateSDFCached(
float MinSDF = FLT_MAX; float MinSDF = FLT_MAX;
const float BlendK = SDFBlendRadius; const float BlendK = SDFBlendRadius;
const FVector Pos(WorldX, WorldY, WorldZ); const FVector Pos(WorldX, WorldY, WorldZ);
const FVFFloorReliefColumnKey ReliefColumn =
VF_MakeFloorReliefColumnKey(Pos.X, Pos.Y);
// Track which room contributes the smallest (most-inside) raw SDF. // Track which room contributes the smallest (most-inside) raw SDF.
// This is used by the terrain ops system to find the "owning" room for // This is used by the terrain ops system to find the "owning" room for
@@ -5103,19 +5427,39 @@ float VoxelCaveMorphology::EvaluateSDFCached(
if (Room.FloorCutZ > -FLT_MAX) if (Room.FloorCutZ > -FLT_MAX)
{ {
float FloorZ = Room.FloorCutZ; float FloorZ = Room.FloorCutZ;
const float FloorBlend = BlendK * 0.35f;
bool bFloorCutIsIdentity = false;
if (Room.FloorReliefStrength > 0.0f) if (Room.FloorReliefStrength > 0.0f)
{ {
const float RF = Room.FloorReliefFrequency; float ReliefBound = 0.0f;
const float SF = (float)Room.FloorSeed * 0.00001f; if (VF_GetFloorReliefBound(
Room.FloorReliefStrength, 1.0f, 1.0f,
Room.FloorReliefFrequency, ReliefBound))
{
// SmoothMax(A, B, K) is exactly A when A >= B + K. Use the highest
// possible relieved floor as B's bound. The extra margin is deliberately
// conservative; an uncertain sample pays for the two noise octaves.
bFloorCutIsIdentity = RoomSDF >=
Room.FloorCutZ + ReliefBound - static_cast<float>(Pos.Z)
+ FMath::Max(FloorBlend, 0.0f);
}
float N = FMath::PerlinNoise2D(FVector2D(Pos.X * RF + SF, Pos.Y * RF + SF * 1.7f)) * 0.65f if (!bFloorCutIsIdentity)
+ FMath::PerlinNoise2D(FVector2D(Pos.X * RF * 2.3f + SF * 3.1f, Pos.Y * RF * 2.3f + SF * 5.3f)) * 0.35f; {
N *= VOXEL_NOISE_SCALE; float N = VF_FloorReliefNoiseDouble(
FloorZ += N * Room.FloorReliefStrength; Pos.X, Pos.Y,
Room.FloorSeed, Room.FloorReliefFrequency,
&Cache, EVFFloorReliefFeature::RoomDouble, RoomIdx,
ReliefColumn);
N *= VOXEL_NOISE_SCALE;
FloorZ += N * Room.FloorReliefStrength;
}
} }
RoomSDF = VoxelSDF::SmoothMax(RoomSDF, FloorZ - Pos.Z, BlendK * 0.35f); if (!bFloorCutIsIdentity)
{
RoomSDF = VoxelSDF::SmoothMax(RoomSDF, FloorZ - Pos.Z, FloorBlend);
}
} }
// Track the room whose SDF is smallest (most inside). // Track the room whose SDF is smallest (most inside).
@@ -5193,7 +5537,7 @@ float VoxelCaveMorphology::EvaluateSDFCached(
// its interpolated floor half-space before the tunnel union is blended into rooms. // its interpolated floor half-space before the tunnel union is blended into rooms.
const float TunnelSDF = VF_EvaluateSweptTunnel( const float TunnelSDF = VF_EvaluateSweptTunnel(
Pos, Tunnel, /*bWorldChain=*/false, BlendK, Pos, Tunnel, /*bWorldChain=*/false, BlendK,
/*bApplyFloorCut=*/true).SDF; /*bApplyFloorCut=*/true, &Cache, TunnelIdx).SDF;
MinSDF = VoxelSDF::SmoothMin(MinSDF, TunnelSDF, BlendK); MinSDF = VoxelSDF::SmoothMin(MinSDF, TunnelSDF, BlendK);
}; };
@@ -5240,7 +5584,7 @@ float VoxelCaveMorphology::EvaluateTunnelCoreSDF(
MinSDF, MinSDF,
VF_EvaluateSweptTunnel( VF_EvaluateSweptTunnel(
Pos, Tunnel, /*bWorldChain=*/false, Cache.SDFBlendRadius, Pos, Tunnel, /*bWorldChain=*/false, Cache.SDFBlendRadius,
/*bApplyFloorCut=*/false).SDF); /*bApplyFloorCut=*/false, &Cache, TunnelIdx).SDF);
}; };
for (int32 TunnelIdx = 0; TunnelIdx < Cache.Tunnels.Num(); ++TunnelIdx) for (int32 TunnelIdx = 0; TunnelIdx < Cache.Tunnels.Num(); ++TunnelIdx)
@@ -5319,7 +5663,8 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
? VoxelCaveMorphology::GetTunnelSupportFloorColumnBand( ? VoxelCaveMorphology::GetTunnelSupportFloorColumnBand(
TunnelIdx, *SupportColumn, FloorZ, SupportMinZ, SupportMaxZ) TunnelIdx, *SupportColumn, FloorZ, SupportMinZ, SupportMaxZ)
: VF_ProjectSweptTunnelFloor( : VF_ProjectSweptTunnelFloor(
Pos, Tunnel, /*bWorldChain=*/true, FloorZ, SupportRadius); Pos, Tunnel, /*bWorldChain=*/true, FloorZ, SupportRadius,
&Cache, TunnelIdx);
if (bProjectedFloor if (bProjectedFloor
&& WorldZ <= FloorZ && WorldZ <= FloorZ
+ VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels + VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels
@@ -5343,7 +5688,7 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
const FVFTunnelShapeEvaluation TunnelShape = VF_EvaluateSweptTunnel( const FVFTunnelShapeEvaluation TunnelShape = VF_EvaluateSweptTunnel(
Pos, Tunnel, bHasWorldChain, Cache.SDFBlendRadius, Pos, Tunnel, bHasWorldChain, Cache.SDFBlendRadius,
/*bApplyFloorCut=*/true); /*bApplyFloorCut=*/true, &Cache, TunnelIdx);
Result.SDF = FMath::Min(Result.SDF, TunnelShape.SDF); Result.SDF = FMath::Min(Result.SDF, TunnelShape.SDF);
}; };
@@ -5406,7 +5751,7 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
return VF_EvaluateSweptTunnel( return VF_EvaluateSweptTunnel(
Pos, Tunnel, bHasWorldChain, Cache.SDFBlendRadius, Pos, Tunnel, bHasWorldChain, Cache.SDFBlendRadius,
/*bApplyFloorCut=*/true).bSupportFloor; /*bApplyFloorCut=*/true, &Cache, TunnelIdx).bSupportFloor;
}; };
bool bSupport = false; bool bSupport = false;
@@ -5458,7 +5803,7 @@ void VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
float SupportRadius = 0.0f; float SupportRadius = 0.0f;
if (!VF_ProjectSweptTunnelFloor( if (!VF_ProjectSweptTunnelFloor(
ColumnPosition, Tunnel, /*bWorldChain=*/true, ColumnPosition, Tunnel, /*bWorldChain=*/true,
FloorZ, SupportRadius)) FloorZ, SupportRadius, &Cache, TunnelIdx))
{ {
continue; continue;
} }