Files
VoxelForge/Standalone/Source/VoxelForge/Public/VoxelCaveMorphology.h
T
Fr0zkaandClaude Opus 5.5 c0c204ba50 fix: signed-overflow UB in VoxelHash made cave placement compiler-dependent; all generators LOD0-8; speed matrix (L60)
VoxelHash::Cell/Pair, legacy Cellular3D and the shared Core cellular hash added 0x7FFFFFFF to a signed
int32 before casting to uint32; overflow is undefined, so Clang and GCC placed cave passage 1 differently
(first differing sample at LOD1 tile (0,64,-512)). The bias is now done in unsigned arithmetic, in both the
standalone copy and the Unreal plugin header (Source/VoxelForge/Public/VoxelCaveMorphology.h). Native
caves, motifs and lattice now stream LOD0-8 with skirts and cross-level transitions. New verify.py
--speed-matrix runs every generator on one asserted-identical view (5 interleaved runs): motifs 1.33,
profiles 1.30-1.32, caves 1.41, natural SW 1.67, lattice 1.70, FI 4.93 s whole view. fast/medium/full PASS,
Clang/GCC peer hashes equal. Research R29 (open-edge ledge) and R30 (compiler-divergence suspects).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-26 01:50:15 +01:00

1208 lines
54 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// VoxelCaveMorphology.h
// Hash-based cave room/tunnel generation and SDF evaluation.
//
// HOW IT WORKS:
// =============
// The world is divided into a coarse 2D grid (cell size = RoomSpacing).
// Each cell deterministically either contains a room or doesn't, based on
// a hash of (CellX, CellY, Seed, StrateIndex). No pre-computation needed —
// room positions are derived on-the-fly from the hash, so this works for
// infinite worlds without storing anything.
//
// ROOMS:
// - Origin room: guaranteed large room at (0,0) per strate — the future shaft landing
// - Hash rooms: ellipsoids, rounded boxes, or elongated capsules
// - All blended with smooth-min for organic junctions
//
// TUNNELS:
// - Connect pairs of rooms based on distance and probability
// - Tapered (variable min/max radius at each endpoint)
// - Curved wandering chains with deterministic perpendicular control-point jitter
// - Horizontal bias (vertical connections penalized)
// - Floor-aligned endpoints and variable radii preserve walkable room joins
// - The origin room is not joined to landings by radial connector roads
//
// PIPELINE POSITION:
// Step 3 (cave warp) bends the SDF query coordinates before we evaluate.
// This step (Step 4) builds the SDF skeleton of rooms + tunnels.
// Surface roughness (Step 4b), terrain ops, and worm tunnels layer on top.
#pragma once
#include "CoreMinimal.h"
#include "VoxelDensityProfile.h"
// Forward declarations
struct FStrateGenerationParams;
struct FSlabGenerationParams;
struct FMazeGenerationParams;
struct FVerticalShaftParams;
struct FFloatingIslandParams;
struct FStrateTerrainOpEntry;
class UVoxelTerrainOpDefinition;
enum class ECaveGeneratorType : uint8;
/**
* The geometry contract for one end of an inter-strate passage.
*
* All values are actor-space voxels. `StandingPoint` is the player-fit anchor: its floor is
* `FloorZ = StandingPoint.Z - 0.5`. The chamber is deliberately separate from the tube endpoint
* so a sloped/vertical tube never presents the player with a hole as its first standing position.
*
* A source query answer selects the landing's XY/Z anchor. The landing is deliberately local to
* that answer: it is a flat-floored room where the inter-strate tube meets a legal player-fit
* pose. The (0,0) landing is the only place reserved for the future straight surface shaft;
* progression mouths do not receive hidden radial roads to that spine room.
*/
struct VOXELFORGE_API FVoxelPassageLanding
{
FVector StandingPoint = FVector::ZeroVector;
FVector DoorPoint = FVector::ZeroVector;
FVector DoorDirection = FVector(1.0f, 0.0f, 0.0f);
float FloorZ = 0.0f;
float CeilingZ = 0.0f;
float HalfWidth = 0.0f;
float FloorThickness = 3.0f;
bool bSourcePlayerFit = false;
};
//=============================================================================
// SDF PRIMITIVES
//=============================================================================
// Signed Distance Field functions.
// Return value: negative = inside the shape, positive = outside.
// The "distance" is how far from the surface — 0.0 = exactly on the surface.
namespace VoxelSDF
{
// Distance from point P to the surface of a sphere at Center with Radius.
// Negative when P is inside the sphere.
FORCEINLINE float Sphere(const FVector& P, const FVector& Center, float Radius)
{
return FVector::Dist(P, Center) - Radius;
}
// Distance from P to an ellipsoid (squished sphere).
// Radii = (RadiusX, RadiusY, RadiusZ) — different size per axis.
// We scale the point into a unit sphere, compute distance, then scale back.
// This isn't an exact SDF (distances are approximate near the surface),
// but it's good enough for marching cubes density evaluation.
FORCEINLINE float Ellipsoid(const FVector& P, const FVector& Center, const FVector& Radii)
{
// Transform to unit sphere space
FVector Scaled = (P - Center) / Radii;
float ScaledDist = Scaled.Size();
// Approximate: scale the distance back by the average radius
float AvgRadius = (Radii.X + Radii.Y + Radii.Z) / 3.0f;
return (ScaledDist - 1.0f) * AvgRadius;
}
// Distance from P to a capsule (line segment with thickness).
// A and B are the endpoints, Radius is the tube thickness.
// This is an EXACT SDF — used for tunnels.
FORCEINLINE float Capsule(const FVector& P, const FVector& A, const FVector& B, float Radius)
{
FVector AB = B - A;
FVector AP = P - A;
// Project P onto line AB, clamped to [0,1] (stays within segment)
float T = FMath::Clamp(FVector::DotProduct(AP, AB) / FMath::Max(FVector::DotProduct(AB, AB), KINDA_SMALL_NUMBER), 0.0f, 1.0f);
// Closest point on the segment
FVector Closest = A + AB * T;
return FVector::Dist(P, Closest) - Radius;
}
// Distance from P to a rounded box (box with rounded edges).
// Center = box center, HalfExtent = half-size per axis, Rounding = edge radius.
// This creates angular chambers (rectangular rooms with smooth corners).
// Without rounding (Rounding=0), it's a sharp box.
// With rounding, edges and corners are smoothed — essential for MC quality.
FORCEINLINE float RoundedBox(const FVector& P, const FVector& Center, const FVector& HalfExtent, float Rounding)
{
// Vector from center to P, take absolute value (box symmetry)
FVector D = FVector(
FMath::Abs(P.X - Center.X),
FMath::Abs(P.Y - Center.Y),
FMath::Abs(P.Z - Center.Z)
) - HalfExtent;
// Distance outside the box (positive when outside)
float Outside = FVector(
FMath::Max(D.X, 0.0f),
FMath::Max(D.Y, 0.0f),
FMath::Max(D.Z, 0.0f)
).Size();
// Distance inside the box (negative when inside)
float Inside = FMath::Min(FMath::Max(D.X, FMath::Max(D.Y, D.Z)), 0.0f);
return Outside + Inside - Rounding;
}
// Distance from P to a tapered capsule (cone-like tube with spherical caps).
// A and B are endpoints, RadiusA and RadiusB are the tube radius at each end.
// When RadiusA != RadiusB, the tube narrows/widens along its length.
// This creates natural-looking corridors that aren't perfectly uniform.
FORCEINLINE float TaperedCapsule(const FVector& P, const FVector& A, const FVector& B, float RadiusA, float RadiusB)
{
FVector AB = B - A;
FVector AP = P - A;
float LenSq = FVector::DotProduct(AB, AB);
// T = how far along AB the closest point is (0 = at A, 1 = at B)
float T = (LenSq > KINDA_SMALL_NUMBER)
? FMath::Clamp(FVector::DotProduct(AP, AB) / LenSq, 0.0f, 1.0f)
: 0.0f;
FVector Closest = A + AB * T;
// Radius interpolates from RadiusA at A to RadiusB at B
float R = FMath::Lerp(RadiusA, RadiusB, T);
return FVector::Dist(P, Closest) - R;
}
// Polynomial smooth minimum — blends two SDF shapes together.
// K controls the blend radius: higher K = rounder, smoother junctions.
// With K=0, this is just FMath::Min(A, B) (hard intersection).
// With K=4, room/tunnel junctions look organic and natural.
FORCEINLINE float SmoothMin(float A, float B, float K)
{
if (K <= 0.0f) return FMath::Min(A, B);
float H = FMath::Max(K - FMath::Abs(A - B), 0.0f) / K;
return FMath::Min(A, B) - H * H * H * K * (1.0f / 6.0f);
}
// Polynomial smooth maximum — dual of SmoothMin, enforces the larger value softly.
// Used for soft SDF intersections (e.g., floor cut planes) so the boundary
// rounds off near tunnel/pit openings instead of creating a hard lip.
// With K=0, this is just FMath::Max(A, B).
FORCEINLINE float SmoothMax(float A, float B, float K)
{
return -SmoothMin(-A, -B, K);
}
}
/** Build the deterministic, body-sized room geometry at one standing anchor. */
VOXELFORGE_API FVoxelPassageLanding VF_BuildPassageLanding(
const FVector& StandingPoint,
float MouthRadius,
const FVector& DoorDirection,
float StrateTopZ,
float StrateBottomZ,
float BoundarySealThickness,
bool bSourcePlayerFit);
/** Signed distance of the local landing room. Negative means passage air. */
VOXELFORGE_API float VF_EvaluatePassageLandingSDF(
const FVector& Position,
const FVoxelPassageLanding& Landing);
/** True when a point belongs to the guaranteed solid floor slab of the landing room. */
VOXELFORGE_API bool VF_IsPassageLandingFloor(
const FVector& Position,
const FVoxelPassageLanding& Landing);
//=============================================================================
// HASH FUNCTIONS
//=============================================================================
// Deterministic integer hashing for room placement.
// Given (CellX, CellY, Seed), always returns the same result.
// No randomness — fully reproducible across sessions and clients.
namespace VoxelHash
{
// Mix a single uint32 — scrambles bits to reduce patterns
FORCEINLINE uint32 Mix(uint32 X)
{
X ^= X >> 16;
X *= 0x45d9f3bu;
X ^= X >> 16;
X *= 0x45d9f3bu;
X ^= X >> 16;
return X;
}
/**
* AUDIT §C1 — décalage de bruit BORNÉ et salé par site. Remplace le motif `SeedF * K`.
*
* LE BUG QUE ÇA CORRIGE : les sites de bruit s'écrivaient
* `WorldX * Freq + (float)Seed * 97.7f`. Le float a 24 bits de mantisse, donc à magnitude `V`
* l'ULP vaut `V · 2⁻²³`. Avec `Seed = 10⁷` le terme atteint 10⁹, où l'ULP vaut **117** — la
* coordonnée du voxel (qui avance de ~0.02 par voxel) est **entièrement absorbée** et le champ
* de bruit devient CONSTANT. Terrain plat. `ChangeSeed` est `BlueprintCallable`, donc un
* `FMath::Rand()` suffit à déclencher ça. Ça ne marchait que parce que les seeds restaient petits.
*
* ⚠️ LE CORRECTIF ÉVIDENT EST FAUX. Borner `SeedF` à 16383 en gardant le `· 97.7` laisse le
* terme atteindre 1.6e6, où l'ULP vaut 0.19 — **9.5× le pas par voxel**. Ça rend le bug moins
* spectaculaire tout en le laissant vivant, et referme le ticket. C'est le multiplicateur qu'il
* faut supprimer, pas le seed qu'il faut réduire.
*
* CE QUE FAIT CETTE FONCTION : le multiplicateur ne SERT plus à décorréler par amplification —
* il IDENTIFIE le site, et c'est le hash qui décorrèle. La sortie est déjà dans les unités
* finales, bornée à [0, 16383] : l'ULP y vaut 0.002, soit 10 % d'un pas de voxel.
*
* ET C'EST PLUS SÛR QU'UN SEEDF BORNÉ PARTAGÉ : avec un offset unique par monde, deux seeds qui
* collident donneraient un bruit identique PARTOUT. Salé par site, il faudrait qu'ils
* collident sur les ~50 sites à la fois — c'est-à-dire jamais.
*
* The multiplier does not decorrelate by amplifying — it IDENTIFIES the site, and the hash
* decorrelates. Output is already in final units and bounded, so the ULP is 10% of a voxel step.
*
* @param SiteKey la constante littérale d'origine (`7.3f`, `97.7f`, …). Gardée VISIBLE au site
* d'appel pour que la correspondance avec le code d'avant reste vérifiable à l'œil.
*/
FORCEINLINE float SeedOffset(uint32 Seed, float SiteKey)
{
// ×100 puis arrondi : les constantes ont au plus 2 décimales, donc `0.31f` → 31 et
// `3.1f` → 310 restent distincts. Le site est une identité entière, pas un flottant.
const uint32 Site = (uint32)(SiteKey * 100.0f + 0.5f);
return (float)(Mix(Seed ^ (Site * 2654435761u)) & 0x3FFFu); // [0, 16383]
}
// Hash a 2D cell coordinate with a seed → deterministic uint32
FORCEINLINE uint32 Cell(int32 CellX, int32 CellY, uint32 Seed)
{
uint32 H = Seed;
// Bias in unsigned arithmetic. Adding the signed cell coordinate first overflows for
// positive cells and makes the world hash compiler-dependent.
const uint32 BiasedX = static_cast<uint32>(CellX) + 0x7FFFFFFFu;
const uint32 BiasedY = static_cast<uint32>(CellY) + 0x7FFFFFFFu;
H ^= Mix(BiasedX);
H ^= Mix(BiasedY * 2654435761u);
return Mix(H);
}
// Hash a pair of cells (for tunnel connectivity decisions).
// Order-independent: Cell(A,B) == Cell(B,A) so each tunnel is evaluated once.
FORCEINLINE uint32 Pair(int32 AX, int32 AY, int32 BX, int32 BY, uint32 Seed)
{
// Sort the pair so (A,B) and (B,A) give the same hash
int32 MinX = FMath::Min(AX, BX), MinY = FMath::Min(AY, BY);
int32 MaxX = FMath::Max(AX, BX), MaxY = FMath::Max(AY, BY);
uint32 H = Seed ^ 0xDEADBEEF;
const uint32 BiasedMinX = static_cast<uint32>(MinX) + 0x7FFFFFFFu;
const uint32 BiasedMinY = static_cast<uint32>(MinY) + 0x7FFFFFFFu;
const uint32 BiasedMaxX = static_cast<uint32>(MaxX) + 0x7FFFFFFFu;
const uint32 BiasedMaxY = static_cast<uint32>(MaxY) + 0x7FFFFFFFu;
H ^= Mix(BiasedMinX);
H ^= Mix(BiasedMinY * 2654435761u);
H ^= Mix(BiasedMaxX * 374761393u);
H ^= Mix(BiasedMaxY * 668265263u);
return Mix(H);
}
// Convert a hash to a float in [0, 1) — uniform distribution
FORCEINLINE float ToFloat01(uint32 Hash)
{
return (float)(Hash & 0xFFFFFF) / (float)0x1000000;
}
// Convert a hash to a float in [-1, 1) — for centering
FORCEINLINE float ToFloatSigned(uint32 Hash)
{
return ToFloat01(Hash) * 2.0f - 1.0f;
}
}
//=============================================================================
// MAZE TOPOLOGY — ORIGIN-DIRECTED SPANNING TREE
//=============================================================================
// A Maze node never waits for a probabilistic edge to connect it to the network. Every node
// except the origin chooses exactly one parent among the coordinate axes that point toward the
// origin. The chosen parent lowers |X|+|Y|+|Z| by one, so following parents terminates at (0,0,0)
// and the undirected parent edges are a spanning tree of the infinite lattice.
//
// The optional loop rolls below are deliberately capped and are never used for the parent edge.
// They add alternate routes and visual irregularity, but deleting every loop still leaves the
// complete tree. This is the locality contract: the parent is a function of the child node and
// seed only; an emitted voxel cell needs the eight child nodes in its one-cell {-1,0} halo.
namespace VoxelMazeTopology
{
enum class EAxis : uint8
{
X = 0,
Y = 1,
Z = 2,
};
// These are authoring knobs for optional loops, not connectivity probabilities. The scales
// keep old authored BranchProbability/Verticality values useful without allowing a lattice
// full of redundant parallel edges to turn the tree back into a grid.
constexpr float HorizontalLoopScale = 0.18f;
constexpr float VerticalLoopScale = 0.10f;
FORCEINLINE uint32 NodeHash(int32 X, int32 Y, int32 Z, uint32 Salt, uint32 DomainSalt)
{
uint32 H = VoxelHash::Cell(X, Y, Salt ^ DomainSalt);
H ^= VoxelHash::Mix(static_cast<uint32>(Z) * 73856093u ^ DomainSalt);
return VoxelHash::Mix(H);
}
FORCEINLINE uint32 AxisSalt(EAxis Axis)
{
switch (Axis)
{
case EAxis::X: return 0xA1u;
case EAxis::Y: return 0xB2u;
case EAxis::Z: return 0xC3u;
default: return 0xA1u;
}
}
FORCEINLINE bool TryGetParent(int32 X, int32 Y, int32 Z, uint32 Salt,
FIntVector& OutParent)
{
if (X == 0 && Y == 0 && Z == 0)
{
return false;
}
EAxis Available[3];
int32 Steps[3];
int32 NumAvailable = 0;
if (X != 0)
{
Available[NumAvailable] = EAxis::X;
Steps[NumAvailable++] = X > 0 ? -1 : 1;
}
if (Y != 0)
{
Available[NumAvailable] = EAxis::Y;
Steps[NumAvailable++] = Y > 0 ? -1 : 1;
}
if (Z != 0)
{
Available[NumAvailable] = EAxis::Z;
Steps[NumAvailable++] = Z > 0 ? -1 : 1;
}
const uint32 H = NodeHash(X, Y, Z, Salt, 0xD4E1A5E1u);
const int32 ParentIndex = static_cast<int32>(H % static_cast<uint32>(NumAvailable));
const EAxis ParentAxis = Available[ParentIndex];
const int32 ParentStep = Steps[ParentIndex];
OutParent = FIntVector(X, Y, Z);
switch (ParentAxis)
{
case EAxis::X: OutParent.X += ParentStep; break;
case EAxis::Y: OutParent.Y += ParentStep; break;
case EAxis::Z: OutParent.Z += ParentStep; break;
default: break;
}
return true;
}
FORCEINLINE FIntVector AxisNeighbour(int32 X, int32 Y, int32 Z, EAxis Axis)
{
switch (Axis)
{
case EAxis::X: return FIntVector(X + 1, Y, Z);
case EAxis::Y: return FIntVector(X, Y + 1, Z);
case EAxis::Z: return FIntVector(X, Y, Z + 1);
default: return FIntVector(X, Y, Z);
}
}
FORCEINLINE bool IsTreeEdge(int32 X, int32 Y, int32 Z, EAxis Axis, uint32 Salt)
{
const FIntVector A(X, Y, Z);
const FIntVector B = AxisNeighbour(X, Y, Z, Axis);
FIntVector Parent;
return (TryGetParent(A.X, A.Y, A.Z, Salt, Parent) && Parent == B)
|| (TryGetParent(B.X, B.Y, B.Z, Salt, Parent) && Parent == A);
}
FORCEINLINE float LoopProbability(EAxis Axis, float BranchProbability, float Verticality)
{
return Axis == EAxis::Z
? FMath::Clamp(Verticality, 0.0f, 1.0f) * VerticalLoopScale
: FMath::Clamp(BranchProbability, 0.0f, 1.0f) * HorizontalLoopScale;
}
FORCEINLINE bool IsLoopEdgeOpen(int32 X, int32 Y, int32 Z, EAxis Axis, uint32 Salt,
float BranchProbability, float Verticality)
{
if (IsTreeEdge(X, Y, Z, Axis, Salt))
{
return false;
}
const float Probability = LoopProbability(Axis, BranchProbability, Verticality);
return Probability > 0.0f
&& VoxelHash::ToFloat01(NodeHash(X, Y, Z, Salt, AxisSalt(Axis))) < Probability;
}
FORCEINLINE bool IsOpenEdge(int32 X, int32 Y, int32 Z, EAxis Axis, uint32 Salt,
float BranchProbability, float Verticality)
{
return IsTreeEdge(X, Y, Z, Axis, Salt)
|| IsLoopEdgeOpen(X, Y, Z, Axis, Salt, BranchProbability, Verticality);
}
}
//=============================================================================
// BRUIT CELLULAIRE / CELLULAR (WORLEY) NOISE — 3D
//=============================================================================
// ⚠️ POURQUOI CE CORPS VIT ICI ET NON DANS VoxelNoise.h.
// Il a besoin de `VoxelHash::Mix` / `ToFloat01`, qui vivent dans CE fichier. Faire dépendre
// VoxelNoise.h (le socle bas niveau, inclus partout) du header de morphologie de grotte serait une
// inversion de dépendance ; dupliquer les 50 lignes serait un FORK d'une fonction pure — exactement
// le motif qui a produit `AUDIT §C1` (un correctif appliqué à une copie sur deux). Il monte donc au
// point le plus bas qui voit déjà le hash, et le générateur comme la pile d'opérateurs l'appellent.
//
// Il vit ici pour que le générateur et la pile d'opérateurs partagent la même implémentation.
// The hot overload uses FVector3f: noise coordinates are floats, so no double round-trip is paid
// before the hash/lattice arithmetic. Output compatibility with a double-precision evaluation is
// not a contract.
//
// Algorithme : distance au point-feature le plus proche dans une grille hachée.
// 1. cellule entière du point 2. voisinage 3×3×3 3. rendre (F2 − F1), normalisé ~[-1, 1]
// F2−F1 donne des frontières de cellules lisses avec des arêtes entre elles.
namespace VoxelNoise
{
FORCEINLINE float Cellular3D(const FVector3f& Position)
{
// Integer cell coordinates
int32 CellX = FMath::FloorToInt(Position.X);
int32 CellY = FMath::FloorToInt(Position.Y);
int32 CellZ = FMath::FloorToInt(Position.Z);
// Fractional position within cell
float FracX = Position.X - CellX;
float FracY = Position.Y - CellY;
float FracZ = Position.Z - CellZ;
float F1 = FLT_MAX; // Distance to nearest feature point
float F2 = FLT_MAX; // Distance to 2nd nearest
// Search 3x3x3 neighborhood
for (int32 DZ = -1; DZ <= 1; DZ++)
{
for (int32 DY = -1; DY <= 1; DY++)
{
for (int32 DX = -1; DX <= 1; DX++)
{
int32 NX = CellX + DX;
int32 NY = CellY + DY;
int32 NZ = CellZ + DZ;
// Hash the neighbor cell to get a feature point position [0,1)
// Using three different hash mixes for X, Y, Z offsets
const uint32 BiasedX = static_cast<uint32>(NX) + 0x7FFFFFFFu;
const uint32 BiasedY = static_cast<uint32>(NY) + 0x7FFFFFFFu;
const uint32 BiasedZ = static_cast<uint32>(NZ) + 0x7FFFFFFFu;
uint32 H = VoxelHash::Mix(
BiasedX
^ VoxelHash::Mix(BiasedY * 2654435761u)
^ VoxelHash::Mix(BiasedZ * 374761393u)
);
float FPX = (float)DX + VoxelHash::ToFloat01(H) - FracX;
float FPY = (float)DY + VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x12345678u)) - FracY;
float FPZ = (float)DZ + VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x9ABCDEF0u)) - FracZ;
float DistSq = FPX * FPX + FPY * FPY + FPZ * FPZ;
// Track closest two distances
if (DistSq < F1)
{
F2 = F1;
F1 = DistSq;
}
else if (DistSq < F2)
{
F2 = DistSq;
}
}
}
}
// F2 - F1: smooth cell boundaries with ridges between cells
// Sqrt for actual distance, then normalize to ~[-1, 1]
float Result = FMath::Sqrt(F2) - FMath::Sqrt(F1);
// Result is in [0, ~1.0]. Map to [-1, 1] for compatibility with other noise types.
return Result * 2.0f - 1.0f;
}
FORCEINLINE float Cellular3D(const FVector& Position)
{
return Cellular3D(FVector3f((float)Position.X, (float)Position.Y, (float)Position.Z));
}
}
//=============================================================================
// PER-CHUNK SDF CACHE
//=============================================================================
// The room list and tunnel connections are IDENTICAL for all voxels in a chunk.
// Without caching, a per-voxel evaluation would rebuild these 32,768 times per 32³ chunk:
// - Hash every grid cell in the search area
// - Determine room existence, position, size
// - Run O(N²) nearest-neighbor backbone
// - Decide O(N²) tunnel connections with hash lookups
// - Compute tunnel radii, floor-aligned endpoints, and wandering control chains
//
// With caching: build once, evaluate 32K times with just SDF math.
// This is the single biggest CPU performance win for cave generation.
// Room-relative detail geometry whose hash/trigonometry is constant for a cached room.
// These are built once with the room's terrain-op parameters and then only evaluated per voxel.
struct FCachedArch
{
bool bActive = false;
FVector EndpointA = FVector::ZeroVector;
FVector EndpointB = FVector::ZeroVector;
float Radius = 0.0f;
float BaseDensity = 0.0f;
};
struct FCachedPinch
{
bool bActive = false;
float CenterX = 0.0f;
float CenterY = 0.0f;
float CenterZ = 0.0f;
float CosAngle = 1.0f;
float SinAngle = 0.0f;
float MaxExtent = 0.0f;
float HalfLength = 0.0f;
float HalfNarrow = 0.0f;
float HalfVertical = 0.0f;
float BaseDensity = 0.0f;
};
// A build-time room-floor bench authored from one tunnel mouth. The room evaluator only reads
// these immutable descriptors; it never searches the graph or decides a correction per sample.
struct FCachedRoomMouthRise
{
FVector Mouth = FVector::ZeroVector;
float TargetFloorZ = 0.0f;
float BlendRadius = 8.0f;
float Strength = 0.0f;
};
// A room in the cache — everything needed for per-voxel SDF evaluation.
// Internal details (CellX, CellY) used during cache building are NOT stored here.
struct FCachedRoom
{
FVector Center; // World position of room center
float RadiusXY; // Horizontal radius (used for SDF + shape selection)
float RadiusZ; // Vertical radius (usually squished: RadiusXY * HeightRatio)
uint32 Hash; // Cell hash — used for deterministic shape selection per voxel
bool bIsOrigin; // True = origin room at (0,0), always uses ellipsoid shape
float CullRadiusSq; // Squared distance beyond which this room can't affect a voxel
// Flat floor cut: base world Z of the soft floor plane.
// Applied via SmoothMax so tunnels/pits pass through without a hard lip seam.
// Set to -FLT_MAX when the hash-rolled floor cut = 1.0 (full bubble, no cut).
float FloorCutZ;
// Floor relief: noise amplitude (voxels) and frequency for floor undulation.
// 0 strength = perfectly flat floor (just the cut plane, no variation).
// Stored per-room so EvaluateSDFCached can apply it without a params lookup.
float FloorReliefStrength;
float FloorReliefFrequency;
uint32 FloorSeed; // Deterministic seed offset for this room's floor noise
// Optional build-time floor benches leading to higher tunnel mouths. Empty in the default
// compatibility profile, so the baseline field is unchanged until an archetype opts in.
TArray<FCachedRoomMouthRise> MouthRises;
// Per-room terrain operation, hash-rolled during BuildChunkCache from the
// strate's probability pool (FStrateTerrainOpEntry::Probability).
// null = this room has no terrain op (the "no op" slot in the probability pool).
// Raw pointer is safe — assets stay loaded for the entire generation session.
const UVoxelTerrainOpDefinition* RoomOp = nullptr;
// Intensity scale for this room's op (from FStrateTerrainOpEntry::Weight).
// 1.0 = use op as configured, 0.5 = half intensity, 2.0 = double.
float RoomOpWeight = 1.0f;
// PRE-BAKED SHAPE (BuildChunkCache). The shape roll, variety thresholds and the capsule's
// Cos/Sin direction are constants of the room: re-deriving them PER VOXEL inside
// EvaluateSDFCached would put hash mixes + trig in the hottest loop of the plugin.
// Bit-identical to a per-voxel roll (same hashes, same math, done once per chunk).
// 0 = ellipsoid → ShapeA = radii (x=y=RadiusXY, z=RadiusZ)
// 1 = rounded box → ShapeA = half-extents, ShapeR = corner rounding
// 2 = capsule → ShapeA/ShapeB = world endpoints, ShapeR = tube radius
uint8 ShapeType = 0;
FVector ShapeA = FVector::ZeroVector;
FVector ShapeB = FVector::ZeroVector;
float ShapeR = 0.0f;
// Hash-derived room details. The old evaluator rebuilt these hashes and angles for every
// surface sample; the cache build is the only place where their geometry can change.
FCachedArch Arches[3];
FCachedPinch Pinches[3];
};
// A local flat-floor bridge over the overlap of two room bodies. It is not a new graph edge: it
// only gives an existing spherical-room intersection one shared walkable floor level, removing
// the high lip that otherwise appears when the two room floor cuts disagree.
struct FCachedRoomFloorJoin
{
FVector Start;
FVector End;
float Radius = 0.0f;
float FloorZ = 0.0f;
float CeilingZ = 0.0f;
FVector BoundCenter;
float BoundRadiusSq = 0.0f;
};
// One immutable floor segment authored while the tunnel chain is built. The floor is continuous
// unless bLedgeTransition is set; that marker means the segment terminates at a deliberate
// dramatic ledge and the vertical riser is placed at the next control-point boundary. A steep
// floor is never quantised into a staircase. ReliefScale is baked with a derivative bound,
// so evaluation only reads this profile and never decides a floor shape per voxel.
struct FTunnelFloorSegmentProfile
{
float StartFloorZ = 0.0f;
float EndFloorZ = 0.0f;
// The unmodified tapered-capsule bottom at the same two control points. Keeping these
// alongside the authored floor lets evaluation translate the arch to the baked floor without
// re-deriving a second profile (and makes the build-time floor/arch relationship explicit).
float NaturalStartFloorZ = 0.0f;
float NaturalEndFloorZ = 0.0f;
float ReliefScale = 0.0f;
bool bLedgeTransition = false;
};
// A pre-computed wandering tunnel chain — all connection decisions and hash-derived properties
// (radii, Z offsets, control points) are resolved during cache build.
struct FCachedTunnel
{
FVector EndpointA; // First room's floor-tangent connection point in SDF coordinates
FVector EndpointB; // Second room's floor-tangent connection point in SDF coordinates
float RadiusA; // Tube radius at endpoint A
float RadiusB; // Tube radius at endpoint B
// Compatibility summary of the chain's first/middle/last points. These fields are retained
// for diagnostics and old cache consumers; EvaluateSDFCached uses ControlPoints when present.
FVector Midpoint;
float RadiusMid;
bool bHasMidpoint;
TArray<FVector> ControlPoints;
TArray<float> ControlRadii;
// World-space copy used by the final structural air backstop. Evaluating this copy avoids
// turning a gentle authored floor ramp into a vertical break when the cave warp is nonlinear.
TArray<FVector> WorldControlPoints;
TArray<float> WorldControlRadii;
// Build-time floor authoring. SDF and world chains each keep their own profile because cave
// warp changes the control-point Z values. The normal path has one entry per chain segment;
// malformed/legacy caches fall back to the old local evaluator.
TArray<FTunnelFloorSegmentProfile> FloorProfiles;
TArray<FTunnelFloorSegmentProfile> WorldFloorProfiles;
// For a dramatic ledge, these monotonic levels identify the control-point boundaries at which
// the few large drops occur. Both SDF and world profiles share the boundaries, while each
// coordinate space supplies its own endpoint heights.
TArray<int32> FloorLedgeLevels;
bool bHasCompleteFloorProfile = false;
bool bHasCompleteWorldFloorProfile = false;
// 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;
// Only the two rooms that actually form this edge may own its mouth floor. A nearby unrelated
// room must not clip a tunnel merely because the two shapes overlap in a crowded graph.
bool bHasFloorRoomOwnership = false;
uint32 FloorRoomHashA = 0;
uint32 FloorRoomHashB = 0;
// World-space mouth ownership is resolved from the baked chain, not by comparing a warped
// query against an SDF-space room. These values make the post-disturbance hand-off exact in
// the same coordinate space in which the player and the support backstop are evaluated.
// The room's full radius is deliberately not used as the ownership region. A tunnel's
// floor should hand off to the room over a short mouth apron, not overwrite the room floor
// across the entire chamber. These are the build-time capped hand-off radii in both query
// coordinate spaces.
float SDFMouthBlendRadiusA = 0.0f;
float SDFMouthBlendRadiusB = 0.0f;
float WorldMouthBlendRadiusA = 0.0f;
float WorldMouthBlendRadiusB = 0.0f;
float SDFMouthFloorZA = -FLT_MAX;
float SDFMouthFloorZB = -FLT_MAX;
float WorldMouthFloorZA = -FLT_MAX;
float WorldMouthFloorZB = -FLT_MAX;
bool bTunnelFloorTerracingEnabled = true;
float TunnelFloorTerraceStepHeight = 1.71f;
float TunnelFloorMaxLedgeHeight = 12.0f;
// Cached internal gradient derived from the authored degree value. This is not serialized.
float TunnelFloorGentleSlopeGradient = 0.9656888f;
int32 TunnelFloorLedgeCountPreference = 0;
int32 TunnelFloorMaxLedges = 4096;
bool bDramaticLedge = false;
bool bLedgeGraphEligible = false;
bool bFloorRouteWasWound = false;
int32 TunnelFloorAuthoredLedgeCount = 0;
FVector WorldBoundCenter = FVector::ZeroVector;
float WorldBoundRadiusSq = 0.0f;
// Centerline AABBs and scalar influence radii used by the immutable broad phase. The
// AABB test is a cheap lower bound on distance to the whole chain; the existing squared
// sphere test remains the exact conservative second-stage reject.
FVector SDFCenterlineMin = FVector::ZeroVector;
FVector SDFCenterlineMax = FVector::ZeroVector;
float SDFInfluenceRadius = 0.0f;
FVector WorldCenterlineMin = FVector::ZeroVector;
FVector WorldCenterlineMax = FVector::ZeroVector;
float WorldInfluenceRadius = 0.0f;
// Bounding sphere for quick per-voxel rejection
FVector BoundCenter; // Center of the bounding sphere
float BoundRadiusSq; // Squared radius — if voxel is further, skip this tunnel
};
struct FTunnelSupportFloorInterval
{
int32 TunnelIndex = INDEX_NONE;
float FloorZ = 0.0f;
float MinZ = 0.0f;
float MaxZ = 0.0f;
};
struct FTunnelSupportFloorColumn
{
TArray<FTunnelSupportFloorInterval, TInlineAllocator<16>> Intervals;
FTunnelSupportFloorColumn() = default;
FTunnelSupportFloorColumn(const FTunnelSupportFloorColumn& Other)
: Intervals(Other.Intervals)
{
}
FTunnelSupportFloorColumn& operator=(const FTunnelSupportFloorColumn& Other)
{
Intervals = Other.Intervals;
return *this;
}
FTunnelSupportFloorColumn(FTunnelSupportFloorColumn&& Other)
: Intervals(MoveTemp(Other.Intervals))
{
}
FTunnelSupportFloorColumn& operator=(FTunnelSupportFloorColumn&& Other)
{
Intervals = MoveTemp(Other.Intervals);
return *this;
}
void Reset()
{
Intervals.Reset();
}
};
// A pre-baked pit shaft — position and dimensions resolved during BuildChunkCache.
//
// Pits are included in the main SDF evaluation (SmoothMin alongside rooms and
// tunnels) rather than as a separate density subtraction. This lets SmoothMin
// handle the pit-to-room junction organically — same mechanism as tunnel junctions.
// No more hard seam at PitTopZ. BlendK drives the junction roundness and comes
// from the strate's SDFBlendRadius, making it tweakable in the data asset.
struct FCachedPit
{
float CenterX, CenterY; // XY center in world coords (unwarped)
float TopZ; // World Z where the pit starts (anchored inside room air)
float Radius; // Shaft cylinder radius
float Depth; // Max depth the pit reaches below TopZ
float FlareDist; // Over how many voxels the opening flares (Radius * 2)
float FlareExtra; // Extra radius at the lip (Radius * 1)
float BaseDensity; // Carving strength — matches the strate's BaseDensity
float BlendK; // SmoothMin blend radius — set from Params.SDFBlendRadius
float BoundXYRadiusSq; // XY rejection: skip if (dx^2+dy^2) > this
};
// A pre-baked chimney shaft — inverse of a pit (carves upward from room ceiling).
struct FCachedChimney
{
float CenterX, CenterY;
float BottomZ; // World Z where the chimney starts (anchored inside room air)
float Radius;
float Height; // How far the chimney reaches above BottomZ
float FlareDist;
float FlareExtra;
float BaseDensity;
float BlendK; // SmoothMin blend radius — set from Params.SDFBlendRadius
float BoundXYRadiusSq;
};
// A pre-baked column — vertical solid cylinder inside a room.
// Pre-baking fixes the NearestRoomIdx ownership issue for columns too:
// a column's XY position is fixed, but which room "owns" the voxel can change
// with depth, which would cause columns to appear/disappear mid-height.
struct FCachedColumn
{
float CenterX, CenterY;
float Radius;
float BaseDensity;
float BoundXYRadiusSq;
};
// Build-site attribution is diagnostic only. It is deliberately passed to BuildChunkCache
// rather than inferred from the caller's stack so the game report can separate the generator,
// ordinary op-source, shared op-source, and classifier fallback windows without changing the
// cache key or the generated field.
enum class ERoomGraphBuildSite : uint8
{
Unknown,
GeneratorTile,
GeneratorTunnelCore,
OpShared,
OpLocal,
ClassifierShared,
ClassifierLocal,
};
// Immutable XY broad phase for one cached primitive array. Each bucket stores source-array
// indices in ascending order, so switching from a full scan to a bucket scan cannot change the
// order of SmoothMin/Min reductions or the selected nearest-room owner. Invalid/oversized
// bounds leave bValid false and callers deliberately fall back to the exact full scan.
struct FChunkSDFSpatialIndex
{
int32 MinChunkX = 0;
int32 MinChunkY = 0;
int32 NumCellsX = 0;
int32 NumCellsY = 0;
bool bValid = false;
TArray<int32> CellOffsets;
TArray<int32> ItemIndices;
void Reset();
void Release();
SIZE_T GetAllocatedSize() const;
bool GetRange(float WorldX, float WorldY, int32& OutBegin, int32& OutEnd) const;
};
// Research-only 3D broad phase used by the step-2 region evaluator. It uses 8-voxel bins and
// keeps source indices in ascending order so SmoothMin / ownership reductions retain the live
// evaluator's order. Invalid or oversized grids fall back to the existing XY index.
struct FChunkSDFBrickIndex
{
int32 MinBrickX = 0;
int32 MinBrickY = 0;
int32 MinBrickZ = 0;
int32 NumBricksX = 0;
int32 NumBricksY = 0;
int32 NumBricksZ = 0;
bool bValid = false;
TArray<int32> CellOffsets;
TArray<int32> ItemIndices;
void Reset();
void Release();
SIZE_T GetAllocatedSize() const;
bool GetRange(float WorldX, float WorldY, float WorldZ,
int32& OutBegin, int32& OutEnd) const;
};
template <typename FVisit>
FORCEINLINE int32 VF_ForEachChunkSDFSpatialCandidate(
const FChunkSDFSpatialIndex& Index, int32 ItemCount,
float WorldX, float WorldY, FVisit&& Visit,
bool bUseSpatialIndex = true)
{
int32 Begin = 0;
int32 End = 0;
int32 CandidateCount = 0;
if (bUseSpatialIndex && Index.GetRange(WorldX, WorldY, Begin, End))
{
for (int32 Cursor = Begin; Cursor < End; ++Cursor)
{
const int32 ItemIndex = Index.ItemIndices[Cursor];
if (ItemIndex >= 0 && ItemIndex < ItemCount)
{
++CandidateCount;
Visit(ItemIndex);
}
}
return CandidateCount;
}
for (int32 ItemIndex = 0; ItemIndex < ItemCount; ++ItemIndex)
{
++CandidateCount;
Visit(ItemIndex);
}
return CandidateCount;
}
// The complete SDF cache for a chunk region.
// Built once per chunk by BuildChunkCache(), then passed to EvaluateSDFCached()
// for every voxel in the chunk. Typically contains 5-15 rooms and 10-30 tunnels.
struct FChunkSDFCache
{
TArray<FCachedRoom> Rooms;
TArray<FCachedRoomFloorJoin> RoomFloorJoins;
TArray<FCachedTunnel> Tunnels;
TArray<FCachedPit> Pits;
TArray<FCachedChimney> Chimneys;
TArray<FCachedColumn> Columns;
// Per-primitive XY filters. The bounds are conservative unions of every exact evaluator
// broad phase, so a failed index build is a performance fallback, never a density fallback.
FChunkSDFSpatialIndex RoomSpatialIndex;
FChunkSDFSpatialIndex RoomFloorJoinSpatialIndex;
FChunkSDFSpatialIndex TunnelSpatialIndex;
FChunkSDFSpatialIndex PitSpatialIndex;
FChunkSDFSpatialIndex ChimneySpatialIndex;
FChunkSDFSpatialIndex ColumnSpatialIndex;
#if WITH_EDITOR
FChunkSDFBrickIndex RoomBrickIndex;
FChunkSDFBrickIndex RoomFloorJoinBrickIndex;
FChunkSDFBrickIndex TunnelBrickIndex;
#endif
// Kept with the cache because the world-space structural evaluator has no params argument.
// A zero value is a valid fallback for legacy/empty caches.
float SDFBlendRadius = 0.0f;
// 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
// storage exactly once.
// Reset keeps the allocated backing storage and clears the immutable contents.
// It is used when a classifier proves that the current search window has no
// possible room/tunnel feature.
void Reset();
// Release all backing storage. Used when a worker has moved from a populated
// classifier window to a feature-free one and the retained capacity is no
// longer useful; this is intentionally separate from hot-path Reset().
void Release();
SIZE_T GetAllocatedSize() const;
VoxelDensityProfile::FCacheMemoryBreakdown GetAllocatedSizeBreakdown() const;
};
/** World-space tunnel shape query. */
struct FTunnelCoreWorldEvaluation
{
float SDF = FLT_MAX;
// Room geometry owns the lower side while the world-space tunnel is inside a room. The
// tunnel SDF may still reopen air there, but its swept bottom must not turn that room air back
// into a shelf or wall.
bool bRoomFloor = false;
// Shape-level floor ownership. This is not an authored slab: it identifies the bottom side
// of the swept tunnel SDF so a room that is already air cannot erase the tunnel's own floor.
bool bHasSweptFloor = false;
float SweptFloorZ = -FLT_MAX;
float SweptFloorRadius = 0.0f;
};
//=============================================================================
// CAVE MORPHOLOGY EVALUATOR
//=============================================================================
// Two-phase evaluation: BuildChunkCache (once per chunk) + EvaluateSDFCached (per voxel).
namespace VoxelCaveMorphology
{
#if WITH_EDITOR
/**
* Enable the isolated step-2 8^3-cell SDF binning path on the current worker thread. The
* default is off and gameplay callers never construct or select these indices.
*/
VOXELFORGE_API void SetResearchBrickBinningEnabled(bool bEnabled);
VOXELFORGE_API bool IsResearchBrickBinningEnabled();
VOXELFORGE_API void SetResearchBrickRoomReachScale(float Scale);
#endif
// The player-fit memo is process-wide and uses a fixed-size table. These controls are kept
// beside the cave source so the command-line value is applied before generation workers start,
// while the CVar remains available for an in-process A/B switch.
VOXELFORGE_API void ConfigurePlayerFitMemoFromCommandLine();
VOXELFORGE_API void ResetPlayerFitMemoStats();
VOXELFORGE_API void LogPlayerFitMemoStats();
// The room graph's seed is the world seed salted by the strate index. Keep this formula
// shared by BuildChunkCache and landing-site queries so a query cannot inspect another
// strate's room layout. / La seed du graphe des salles est la seed monde salée par l'index
// de strate. La formule est partagée entre BuildChunkCache et les requêtes d'atterrissage.
FORCEINLINE uint32 MakeStrateSeed(uint32 WorldSeed, int32 StrateIndex)
{
return VoxelHash::Mix(WorldSeed ^ (uint32)(StrateIndex * 7919 + 104729));
}
// Map a world-space query to the SDF coordinate space used by the room/tunnel cache. Keeping
// this transform in the morphology module lets the generator's post-disturbance structural
// check share the exact same warp as the source and the landing query.
VOXELFORGE_API FVector ApplyCaveWarp(
const FVector& WorldPoint,
const FStrateGenerationParams& Params,
uint32 Seed
);
// PHASE 1: Build the SDF cache for a rectangular region.
// Collects all rooms, computes nearest-neighbor backbone, decides tunnel
// connections, and pre-computes all tunnel geometry (radii, offsets, midpoints).
// Also hash-rolls a terrain op per room from the optional probability pool.
//
// Call once per chunk before the voxel loop. The region bounds should cover
// the chunk's XY extent PLUS CaveWarpStrength (warped queries can shift)
// PLUS a small margin for gradient normal sampling (~2 voxels).
// MaxInfluence (room/tunnel reach) is computed internally from Params.
//
// @param OutCache — filled with rooms and tunnels for this region
// @param SearchMinX/Y, SearchMaxX/Y — XY world bounds to search (expanded by caller)
// @param Params — strate generation parameters
// @param Seed — world seed
// @param StrateIndex — which strate (offsets hash for variety)
// @param TerrainOps — optional probability pool; null = no per-room ops
VOXELFORGE_API void BuildChunkCache(
FChunkSDFCache& OutCache,
float SearchMinX, float SearchMinY,
float SearchMaxX, float SearchMaxY,
const FStrateGenerationParams& Params,
uint32 Seed, int32 StrateIndex,
const TArray<FStrateTerrainOpEntry>* TerrainOps = nullptr,
ERoomGraphBuildSite BuildSite = ERoomGraphBuildSite::Unknown
);
// Conservative, geometry-free preflight for BuildChunkCache. It recreates
// the deterministic room candidates and edge decisions, but does not build
// player-fit points or tunnel control chains. False means that the same
// cache window is guaranteed to contain no room, join, decoration, or
// connected tunnel. Any malformed or unbounded input returns true.
VOXELFORGE_API bool MayHaveFeatureInSearchBox(
float SearchMinX, float SearchMinY,
float SearchMaxX, float SearchMaxY,
const FStrateGenerationParams& Params,
uint32 Seed, int32 StrateIndex,
bool bUseZ = false,
float SearchMinZ = 0.0f, float SearchMaxZ = 0.0f,
const TArray<FStrateTerrainOpEntry>* TerrainOps = nullptr
);
// True only for the finite parameter envelope for which BuildChunkCache's fixed collect
// margin proves window invariance. Callers may widen the XY store window to a whole tile only
// when this returns true; malformed or unbounded authored input must retain the legacy path.
VOXELFORGE_API bool IsRoomGraphWindowInvariant(
const FStrateGenerationParams& Params,
const TArray<FStrateTerrainOpEntry>* TerrainOps = nullptr
);
// PHASE 2: Evaluate the SDF at a single world position using cached data.
// Loops through cached rooms and tunnels, computes SDF primitives, applies
// SmoothMin. Includes per-room/tunnel distance culling to skip primitives
// that are too far to contribute.
//
// @param WorldX, WorldY, WorldZ — room/query position in voxel coordinates (may be warped)
// @param Cache — pre-built cache from BuildChunkCache
// @param SDFBlendRadius — SmoothMin blend radius (from Params.SDFBlendRadius)
// @param OutNearestRoomIdx — optional out: index of the room with minimum SDF
// contribution. -1 if no room passed the cull test.
// Used by the terrain ops system to look up the
// per-room terrain op assigned to this voxel's room.
// @param WorldTunnelPosition — optional unwarped position for the tunnel primitive. When
// supplied, rooms remain at the query position but the tunnel
// uses its authored world chain. Null preserves the legacy
// all-query behavior.
// @param OutRoomOwnsBottom — optional out: true when the query position is inside any
// cached room's raw authored shape. When the caller evaluates
// the tunnel core for this same sample, pass this result back
// to EvaluateTunnelCoreWorld to avoid a second room query.
// (Room shape variety is baked into FCachedRoom by BuildChunkCache — no per-voxel roll.)
// @return negative = inside cave, positive = solid rock
VOXELFORGE_API float EvaluateSDFCached(
float WorldX, float WorldY, float WorldZ,
const FChunkSDFCache& Cache,
float SDFBlendRadius,
int32* OutNearestRoomIdx = nullptr,
bool bUseSpatialIndex = true,
const FVector* WorldTunnelPosition = nullptr,
bool* OutRoomOwnsBottom = nullptr
);
// Evaluate the final structural air contract against the world-space wandering chain. Room
// SDFs remain evaluated in warped coordinates; only this post uses world coordinates so its
// walkable floor follows the authored chain exactly.
VOXELFORGE_API float EvaluateTunnelCoreWorldSDF(
float WorldX, float WorldY, float WorldZ,
const FChunkSDFCache& Cache,
bool bUseSpatialIndex = true
);
VOXELFORGE_API FTunnelCoreWorldEvaluation EvaluateTunnelCoreWorld(
float WorldX, float WorldY, float WorldZ,
const FChunkSDFCache& Cache,
// Deprecated legacy argument; graph-tunnel evaluation ignores the old support column.
const FTunnelSupportFloorColumn* SupportColumn = nullptr,
bool bUseSpatialIndex = true,
// The room graph is evaluated in warped coordinates while the tunnel core is evaluated in
// the authored world frame. Pass the former when available so room ownership is tested in
// the same domain as the generic room field.
const FVector* RoomQueryPosition = nullptr,
// Optional result from EvaluateSDFCached for this exact sample. When supplied, it avoids
// repeating the room-shape containment query; null preserves standalone-call behavior.
const bool* CachedRoomOwnsBottom = nullptr
);
// Per-tile broad reach for the world-space core/floor post. The bound includes the swept
// chain, floor band, relief envelope, and the finite room-mouth ownership apron.
VOXELFORGE_API bool AnyTunnelCoreWorldNearLattice(
const FChunkSDFCache& Cache,
const FBox& VoxelBox,
float ReachScale = 1.0f
);
}
/**
* Suggest a deterministic player-fit landing point for an inter-strate passage mouth.
*
* This is deliberately a pure source query: it does not touch a generator, an operator stack,
* a strate manager, a cache, or mutable state. DesiredX/DesiredY is the passage's deliberate
* placement around the (0,0) spine. Sparse sources may move laterally, but never farther than
* MaxLateralSnap; false means no confident player-fit pose exists inside that bound. A successful
* point has a locally supported floor within player step height, a walkable support patch, and a
* clear capsule volume. The local source stencil deliberately does not claim flood-fill
* connectivity or a measured component size; it selects a topology-backed feature core instead.
* For TunnelNetwork/Underwater, Seed must be the strate-specific room seed returned by
* VoxelCaveMorphology::MakeStrateSeed; the optional WorldSeed selects the production cave-warp
* field and should be the generator world seed. If omitted, the room seed is used for both for
* backward-compatible standalone queries. For slab, maze, shaft, and island archetypes, Seed is the
* generator world seed consumed by their source. SurfaceWorld deliberately has no answer here:
* its production height can be selected by manager-owned biome context and per-chunk state,
* which this re-entrant query cannot inspect.
*
* Propose un point d'atterrissage déterministe où la capsule du joueur tient réellement pour la
* bouche d'un passage inter-strates. Cette requête reste pure et le déplacement latéral est
* strictement borné par MaxLateralSnap; elle ne prétend pas prouver une composante connectée.
* Pour VerticalShafts, la réponse s'appuie sur l'axe d'un puits sélectionné et peut se déplacer
* vers une banquette déterministe (+X/+Y) pour obtenir un support; ce n'est pas un point ouvert
* arbitraire à l'intérieur de son disque.
*
* @return true when the archetype can provide a confident point; false when it cannot.
*/
VOXELFORGE_API bool VF_SuggestLandingPoint(
ECaveGeneratorType Archetype,
const FStrateGenerationParams& CaveParams,
const FSlabGenerationParams& SlabParams,
int32 Seed,
float StrateTopZ,
float StrateBottomZ,
float DesiredX,
float DesiredY,
float MaxLateralSnap,
FVector& OutPoint,
int32 WorldSeed = MIN_int32
);
/**
* Complete player-fit source-query overload for archetypes whose placement parameters are not part of
* FStrateGenerationParams/FSlabGenerationParams. The extra structs are read-only inputs only;
* the function remains pure and SurfaceWorld still returns false for the reason above. The
* returned point is either at the requested XY or within MaxLateralSnap of it, and is accepted
* only after the local player capsule/support stencil passes. VerticalShafts uses a selected
* shaft's deterministic feature core and ledge-side pose; the axis identifies the topology but
* is not itself claimed to be a standable point.
*/
VOXELFORGE_API bool VF_SuggestLandingPoint(
ECaveGeneratorType Archetype,
const FStrateGenerationParams& CaveParams,
const FSlabGenerationParams& SlabParams,
const FMazeGenerationParams& MazeParams,
const FVerticalShaftParams& VerticalShaftParams,
const FFloatingIslandParams& FloatingIslandParams,
int32 Seed,
float StrateTopZ,
float StrateBottomZ,
float DesiredX,
float DesiredY,
float MaxLateralSnap,
FVector& OutPoint,
int32 WorldSeed = MIN_int32
);