Files
VoxelForge/Source/VoxelForge/Private/VoxelRedefinedDensityField.cpp
T
Fr0zkaandClaude Opus 5.5 bc0f9ee791 wip(step3): 12.5 cm voxels, fall-through fixed, brick evaluator in game, organic tunnels -- speed gate FAILS
Stopped by the owner's decision (in-game lit captures were eating the round); he judges the look in
game instead.
1. Fall-through with the research field: fixed. Deep drop, gate on: 2 holds (0.50 s), min foot
   clearance +43 cm; gate off: penetrates (-278 cm). Stress fixture offsets converted to metres.
2. VOXEL_SIZE 25 -> 12.5 cm. Physical sizes kept in metres (player, steps, gate, insets, research
   field sizes, dig radii, test thresholds); class defaults that count chunks/voxels doubled
   (ClipRadius 6, RenderDistanceChunks 1536, ViewDistance*, StrateViewMargin 6,
   PawnCollisionRadius 2, SkirtCells 4, EdgeSealThickness 128, decoration spacings, density volume
   res 256). Asset values override these defaults: the owner must double his own.
   Ring scale, six workers, idle, step-2f field before the look changes: 64 m cube (519^3) 4.55 s;
   32 m cube (259^3) 0.95 s.
3. In game: tiles go through the brick evaluator (proved by a log marker); profile switch rebuilds
   the loaded world (~5,800 tiles); full-detail fill 12.4 s after spawn in the visible-window test.
4. Look: round winding tunnels, per-column ellipsoid/lattice caches. All step-2f geometry gates pass
   at 12.5 cm (0 crumbs, 1 solid component per profile, solid share 69-87%). 259^3 ring, six workers:
   1.08-2.02 s (worst HyperComplex) -- the 600 ms gate FAILS. Lit captures failed (hidden/off-screen
   window, then blank frames); not pursued.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
2026-10-01 21:21:45 +02:00

3619 lines
166 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.
#include "VoxelRedefinedDensityField.h"
#if WITH_EDITOR
#include "HAL/IConsoleManager.h"
#include "VoxelCaveMorphology.h"
#include "VoxelDiffLayer.h"
#include "VoxelGenerator.h"
#include "VoxelPassageGeometry.h"
#include "VoxelStrateManager.h"
#include "VoxelStrateTypes.h"
#include "VoxelTypes.h"
#include <algorithm>
#include <atomic>
namespace VoxelForgeRedefinedDensityField
{
namespace
{
constexpr float DensityUnitsPerLegacyVoxel = 25.0f / VOXEL_SIZE;
constexpr float LegacyDetailAmplitude =
(1.4f + 0.9f + 0.45f + 2.5f) * DensityUnitsPerLegacyVoxel;
constexpr float CoherentDetailAmplitude =
(1.82f + 1.092f + 0.546f + 0.364f) * DensityUnitsPerLegacyVoxel;
constexpr float CoherentRockDensityBias =
VoxelPassageGeometry::MetersToVoxels(0.025f);
constexpr float DetailBoundNumericalMargin =
VoxelPassageGeometry::MetersToVoxels(0.00025f);
constexpr float BaseFieldLipschitzBound = 1.25f;
constexpr float ParameterizedFieldLipschitzBound = 1.60f;
constexpr float QuinticFadeSlopeBound = 1.875f;
constexpr float MinimumCaveHeight = VoxelPassageGeometry::MetersToVoxels(12.0f);
constexpr float LandingFloorBlendVoxels = VoxelPassageGeometry::MetersToVoxels(24.0f);
constexpr float LandingBridgeRadius = VoxelPassageGeometry::MetersToVoxels(12.0f);
constexpr float PassageBoundPadding = VoxelPassageGeometry::MetersToVoxels(4.0f);
constexpr float VerticalShaftSpineRadius = VoxelPassageGeometry::MetersToVoxels(6.0f);
constexpr int32 VerticalShaftConnectorChildReach = 4;
constexpr float LandingApproachRadius = VoxelPassageGeometry::MetersToVoxels(8.0f);
constexpr uint32 VerticalShaftTreeSalt = 0x7A11u;
TAutoConsoleVariable<int32> CVarResearchRedefinedDensityField(
TEXT("voxel.ResearchRedefinedDensityField"), 0,
TEXT("Use the default-off editor-only step 2d redefined density field."),
ECVF_Default);
TAutoConsoleVariable<float> CVarResearchRedefinedDensityDetailScale(
TEXT("voxel.ResearchRedefinedDensityDetailScale"), 1.0f,
TEXT("Step 2d research detail amplitude scale; zero is a deliberate smoothing control."),
ECVF_Default);
TAutoConsoleVariable<int32> CVarResearchParameterizedCave(
TEXT("voxel.ResearchParameterizedCave"), 0,
TEXT("Use the default-off editor-only step 2e parameterized cave field."),
ECVF_Default);
TAutoConsoleVariable<int32> CVarResearchParameterizedCaveSet(
TEXT("voxel.ResearchParameterizedCaveSet"), -1,
TEXT("Step 2e parameter profile 0-7; -1 rolls a profile deterministically per strate."),
ECVF_Default);
struct FParameterProfile
{
const TCHAR* Name;
FGenerationParameters Parameters;
};
// Frozen step 2e profiles are retained only for the step 2f before/after audit.
const FParameterProfile GLegacyStep2eProfiles[] = {
{ TEXT("OpenVaults"), { VoxelPassageGeometry::MetersToVoxels(44.0f), 0.62f, 0.76f,
VoxelPassageGeometry::MetersToVoxels(85.0f), VoxelPassageGeometry::MetersToVoxels(3.0f),
VoxelPassageGeometry::MetersToVoxels(10.0f), VoxelPassageGeometry::MetersToVoxels(9.0f),
0.35f, 0.14f, VoxelPassageGeometry::MetersToVoxels(5.5f),
0.04f, 0.12f, 0.08f, 0.15f, 0.00f, 0.0f, 0.50f } },
{ TEXT("TightNetwork"), { VoxelPassageGeometry::MetersToVoxels(26.0f), 0.52f, 0.64f,
VoxelPassageGeometry::MetersToVoxels(64.0f), VoxelPassageGeometry::MetersToVoxels(2.0f),
VoxelPassageGeometry::MetersToVoxels(5.0f), VoxelPassageGeometry::MetersToVoxels(5.5f),
0.12f, 0.03f, VoxelPassageGeometry::MetersToVoxels(3.5f),
0.06f, 0.08f, 0.02f, 0.12f, 0.00f, 0.0f, 0.50f } },
{ TEXT("ChamberHalls"), { VoxelPassageGeometry::MetersToVoxels(36.0f), 0.55f, 0.72f,
VoxelPassageGeometry::MetersToVoxels(80.0f), VoxelPassageGeometry::MetersToVoxels(4.5f),
VoxelPassageGeometry::MetersToVoxels(8.5f), VoxelPassageGeometry::MetersToVoxels(8.0f),
0.28f, 0.18f, VoxelPassageGeometry::MetersToVoxels(5.5f),
0.08f, 0.22f, 0.10f, 0.22f, 0.05f,
VoxelPassageGeometry::MetersToVoxels(5.0f), 0.40f } },
{ TEXT("ShaftForest"), { VoxelPassageGeometry::MetersToVoxels(32.0f), 0.50f, 0.62f,
VoxelPassageGeometry::MetersToVoxels(68.0f), VoxelPassageGeometry::MetersToVoxels(3.0f),
VoxelPassageGeometry::MetersToVoxels(6.5f), VoxelPassageGeometry::MetersToVoxels(7.0f),
0.12f, 0.92f, VoxelPassageGeometry::MetersToVoxels(7.0f),
0.05f, 0.18f, 0.04f, 0.20f, 0.00f, 0.0f, 0.50f } },
{ TEXT("FloatingArchipelago"), { VoxelPassageGeometry::MetersToVoxels(34.0f), 0.55f, 0.68f,
VoxelPassageGeometry::MetersToVoxels(54.0f), VoxelPassageGeometry::MetersToVoxels(2.5f),
VoxelPassageGeometry::MetersToVoxels(7.0f), VoxelPassageGeometry::MetersToVoxels(7.0f),
0.15f, 0.10f, VoxelPassageGeometry::MetersToVoxels(4.0f),
0.05f, 0.10f, 0.72f, 0.28f, 0.00f, 0.0f, 0.50f } },
{ TEXT("PillaredCathedral"), { VoxelPassageGeometry::MetersToVoxels(40.0f), 0.55f, 0.72f,
VoxelPassageGeometry::MetersToVoxels(62.0f), VoxelPassageGeometry::MetersToVoxels(3.0f),
VoxelPassageGeometry::MetersToVoxels(8.0f), VoxelPassageGeometry::MetersToVoxels(8.5f),
0.25f, 0.08f, VoxelPassageGeometry::MetersToVoxels(4.5f),
0.72f, 0.30f, 0.08f, 0.45f, 0.00f, 0.0f, 0.50f } },
{ TEXT("TerracedBasin"), { VoxelPassageGeometry::MetersToVoxels(34.0f), 0.52f, 0.65f,
VoxelPassageGeometry::MetersToVoxels(66.0f), VoxelPassageGeometry::MetersToVoxels(3.5f),
VoxelPassageGeometry::MetersToVoxels(7.0f), VoxelPassageGeometry::MetersToVoxels(7.5f),
0.20f, 0.10f, VoxelPassageGeometry::MetersToVoxels(4.5f),
0.10f, 0.84f, 0.05f, 0.25f, 0.85f,
VoxelPassageGeometry::MetersToVoxels(12.0f), 0.30f } },
{ TEXT("HyperComplex"), { VoxelPassageGeometry::MetersToVoxels(24.0f), 0.60f, 0.74f,
VoxelPassageGeometry::MetersToVoxels(70.0f), VoxelPassageGeometry::MetersToVoxels(5.0f),
VoxelPassageGeometry::MetersToVoxels(7.5f), VoxelPassageGeometry::MetersToVoxels(7.0f),
0.40f, 0.55f, VoxelPassageGeometry::MetersToVoxels(5.0f),
0.45f, 0.68f, 0.50f, 0.75f, 0.42f,
VoxelPassageGeometry::MetersToVoxels(7.0f), 0.40f } },
};
const FParameterProfile GParameterProfiles[] = {
// Geometry is specified in metres, then converted to voxel-space for field math.
{ TEXT("OpenVaults"), { VoxelPassageGeometry::MetersToVoxels(44.0f), 0.31f, 0.41f,
VoxelPassageGeometry::MetersToVoxels(33.0f), VoxelPassageGeometry::MetersToVoxels(2.0f),
VoxelPassageGeometry::MetersToVoxels(5.5f), VoxelPassageGeometry::MetersToVoxels(4.5f),
0.12f, 0.08f, VoxelPassageGeometry::MetersToVoxels(4.0f),
0.02f, 0.06f, 0.04f, 0.22f, 0.00f, 0.0f, 0.50f } },
{ TEXT("TightNetwork"), { VoxelPassageGeometry::MetersToVoxels(26.0f), 0.27f, 0.35f,
VoxelPassageGeometry::MetersToVoxels(26.0f), VoxelPassageGeometry::MetersToVoxels(1.25f),
VoxelPassageGeometry::MetersToVoxels(4.5f), VoxelPassageGeometry::MetersToVoxels(3.5f),
0.28f, 0.12f, VoxelPassageGeometry::MetersToVoxels(3.75f),
0.02f, 0.06f, 0.02f, 0.14f, 0.00f, 0.0f, 0.50f } },
{ TEXT("ChamberHalls"), { VoxelPassageGeometry::MetersToVoxels(36.0f), 0.32f, 0.41f,
VoxelPassageGeometry::MetersToVoxels(34.0f), VoxelPassageGeometry::MetersToVoxels(2.5f),
VoxelPassageGeometry::MetersToVoxels(5.25f), VoxelPassageGeometry::MetersToVoxels(4.25f),
0.18f, 0.10f, VoxelPassageGeometry::MetersToVoxels(4.0f),
0.03f, 0.06f, 0.06f, 0.20f, 0.05f,
VoxelPassageGeometry::MetersToVoxels(4.0f), 0.40f } },
{ TEXT("ShaftForest"), { VoxelPassageGeometry::MetersToVoxels(32.0f), 0.28f, 0.36f,
VoxelPassageGeometry::MetersToVoxels(30.0f), VoxelPassageGeometry::MetersToVoxels(1.75f),
VoxelPassageGeometry::MetersToVoxels(4.75f), VoxelPassageGeometry::MetersToVoxels(3.75f),
0.16f, 0.42f, VoxelPassageGeometry::MetersToVoxels(5.0f),
0.03f, 0.06f, 0.03f, 0.22f, 0.00f, 0.0f, 0.50f } },
{ TEXT("FloatingArchipelago"), { VoxelPassageGeometry::MetersToVoxels(34.0f), 0.29f, 0.37f,
VoxelPassageGeometry::MetersToVoxels(29.0f), VoxelPassageGeometry::MetersToVoxels(1.5f),
VoxelPassageGeometry::MetersToVoxels(4.75f), VoxelPassageGeometry::MetersToVoxels(3.75f),
0.12f, 0.08f, VoxelPassageGeometry::MetersToVoxels(3.75f),
0.02f, 0.06f, 0.08f, 0.20f, 0.00f, 0.0f, 0.50f } },
{ TEXT("PillaredCathedral"), { VoxelPassageGeometry::MetersToVoxels(40.0f), 0.31f, 0.40f,
VoxelPassageGeometry::MetersToVoxels(33.0f), VoxelPassageGeometry::MetersToVoxels(2.0f),
VoxelPassageGeometry::MetersToVoxels(5.5f), VoxelPassageGeometry::MetersToVoxels(4.25f),
0.20f, 0.08f, VoxelPassageGeometry::MetersToVoxels(4.0f),
0.16f, 0.06f, 0.04f, 0.22f, 0.00f, 0.0f, 0.50f } },
{ TEXT("TerracedBasin"), { VoxelPassageGeometry::MetersToVoxels(34.0f), 0.30f, 0.38f,
VoxelPassageGeometry::MetersToVoxels(31.0f), VoxelPassageGeometry::MetersToVoxels(2.0f),
VoxelPassageGeometry::MetersToVoxels(5.0f), VoxelPassageGeometry::MetersToVoxels(4.0f),
0.14f, 0.10f, VoxelPassageGeometry::MetersToVoxels(4.0f),
0.04f, 0.06f, 0.04f, 0.18f, 0.46f,
VoxelPassageGeometry::MetersToVoxels(6.0f), 0.30f } },
{ TEXT("HyperComplex"), { VoxelPassageGeometry::MetersToVoxels(24.0f), 0.28f, 0.37f,
VoxelPassageGeometry::MetersToVoxels(26.0f), VoxelPassageGeometry::MetersToVoxels(2.25f),
VoxelPassageGeometry::MetersToVoxels(4.5f), VoxelPassageGeometry::MetersToVoxels(3.5f),
0.26f, 0.32f, VoxelPassageGeometry::MetersToVoxels(4.25f),
0.08f, 0.06f, 0.05f, 0.18f, 0.12f,
VoxelPassageGeometry::MetersToVoxels(3.0f), 0.40f } },
};
std::atomic<bool> GUseLegacyStep2eAuditMode{false};
FORCEINLINE bool IsLegacyStep2eAuditMode()
{
return GUseLegacyStep2eAuditMode.load(std::memory_order_relaxed);
}
FORCEINLINE float CurrentDetailAmplitude(bool bParameterized)
{
return bParameterized && !IsLegacyStep2eAuditMode()
? CoherentDetailAmplitude : LegacyDetailAmplitude;
}
struct FLandingSite
{
FVector Position = FVector::ZeroVector;
float FloorZ = 0.0f;
float HalfWidth = 0.0f;
float CeilingZ = 0.0f;
float PadRadius = 0.0f;
float BlendRadius = LandingFloorBlendVoxels;
FVector ApproachEnd = FVector::ZeroVector;
bool bGeneratedAnchorPrepared = false;
FVector GeneratedAnchorStart = FVector::ZeroVector;
FVector GeneratedAnchorDelta = FVector::ZeroVector;
float GeneratedAnchorLengthSquared = 0.0f;
float GeneratedAnchorReach = 0.0f;
};
struct FVerticalShaftNode
{
int32 CellX = 0;
int32 CellY = 0;
float X = 0.0f;
float Y = 0.0f;
float Radius = 0.0f;
};
struct FVerticalShaftEdge
{
FVector A = FVector::ZeroVector;
FVector B = FVector::ZeroVector;
float Radius = 0.0f;
};
struct FThreadShaftEdgeCache
{
const void* Plan = nullptr;
const UVoxelStrateManager* Manager = nullptr;
uint64 ManagerLifetimeId = 0;
uint32 LayoutVersion = 0;
int32 Seed = INT32_MIN;
int32 CellX = INT32_MIN;
int32 CellY = INT32_MIN;
TArray<FVerticalShaftEdge> Edges;
};
struct FPassageBound
{
FVector Min = FVector(FLT_MAX, FLT_MAX, FLT_MAX);
FVector Max = FVector(-FLT_MAX, -FLT_MAX, -FLT_MAX);
FVector SegmentA = FVector::ZeroVector;
FVector SegmentB = FVector::ZeroVector;
float SupportRadius = 0.0f;
bool bIsSegment = false;
};
struct FStratePlan
{
int32 StrateIndex = INDEX_NONE;
int32 MinChunkZ = 0;
int32 MaxChunkZExclusive = 0;
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
float FloorZ = 0.0f;
float CeilingZ = 0.0f;
float CellSpacing = VoxelPassageGeometry::MetersToVoxels(32.0f);
float OffsetX = 0.0f;
float OffsetY = 0.0f;
float CorridorHalfWidth = VoxelPassageGeometry::MetersToVoxels(10.0f);
FGenerationParameters Generation;
int32 GenerationProfile = 0;
uint32 GenerationSeed = 0;
float GenerationFloorZ = 0.0f;
float GridCos = 1.0f;
float GridSin = 0.0f;
float GridOffsetX = 0.0f;
float GridOffsetY = 0.0f;
FVerticalShaftParams VerticalShaftParams;
int32 LandingHubIndex = INDEX_NONE;
TArray<FLandingSite> Landings;
};
void PrepareParameterizedAnchors(FStratePlan& Plan);
struct FThreadPlanCache
{
const UVoxelStrateManager* Manager = nullptr;
uint64 ManagerLifetimeId = 0;
uint32 LayoutVersion = 0xFFFFFFFFu;
int32 Seed = INT32_MIN;
int32 ParameterSet = INT32_MIN;
bool bLegacyStep2eAuditMode = false;
TArray<FStratePlan> Strates;
TArray<FPassageBound> PassageBounds;
};
thread_local FThreadPlanCache GPlanCache;
thread_local const FStratePlan* GParameterizedRoomCellCachePlan = nullptr;
thread_local FThreadShaftEdgeCache GShaftEdgeCache;
FORCEINLINE uint32 Mix(uint32 Value)
{
Value ^= Value >> 16;
Value *= 0x7FEB352Du;
Value ^= Value >> 15;
Value *= 0x846CA68Bu;
Value ^= Value >> 16;
return Value;
}
FORCEINLINE uint32 Hash3(int32 X, int32 Y, int32 Z, uint32 Seed)
{
uint32 H = Seed ^ 0x9E3779B9u;
H ^= Mix(static_cast<uint32>(X) + 0x85EBCA6Bu);
H = (H << 13) | (H >> 19);
H ^= Mix(static_cast<uint32>(Y) + 0xC2B2AE35u);
H = (H << 11) | (H >> 21);
H ^= Mix(static_cast<uint32>(Z) + 0x27D4EB2Fu);
return Mix(H);
}
FORCEINLINE float HashValue(int32 X, int32 Y, int32 Z, uint32 Seed)
{
const uint32 H = Hash3(X, Y, Z, Seed);
return static_cast<float>(H >> 8) * (2.0f / 16777215.0f) - 1.0f;
}
FORCEINLINE float Fade(float T)
{
return T * T * T * (T * (T * 6.0f - 15.0f) + 10.0f);
}
FORCEINLINE float Noise3D(float X, float Y, float Z, float CellSize, uint32 Seed)
{
const float Inv = 1.0f / CellSize;
const float SX = X * Inv;
const float SY = Y * Inv;
const float SZ = Z * Inv;
const int32 IX = FMath::FloorToInt(SX);
const int32 IY = FMath::FloorToInt(SY);
const int32 IZ = FMath::FloorToInt(SZ);
const float AX = Fade(SX - static_cast<float>(IX));
const float AY = Fade(SY - static_cast<float>(IY));
const float AZ = Fade(SZ - static_cast<float>(IZ));
const float Z0Y0 = FMath::Lerp(
FMath::Lerp(HashValue(IX, IY, IZ, Seed), HashValue(IX + 1, IY, IZ, Seed), AX),
FMath::Lerp(HashValue(IX, IY + 1, IZ, Seed), HashValue(IX + 1, IY + 1, IZ, Seed), AX), AY);
const float Z0Y1 = FMath::Lerp(
FMath::Lerp(HashValue(IX, IY, IZ + 1, Seed), HashValue(IX + 1, IY, IZ + 1, Seed), AX),
FMath::Lerp(HashValue(IX, IY + 1, IZ + 1, Seed), HashValue(IX + 1, IY + 1, IZ + 1, Seed), AX), AY);
return FMath::Lerp(Z0Y0, Z0Y1, AZ);
}
struct FNoisePoint3
{
float X = 0.0f;
float Y = 0.0f;
float Z = 0.0f;
};
struct FNoiseLatticeCache
{
FIntVector MinCell = FIntVector::ZeroValue;
FIntVector Size = FIntVector::ZeroValue;
TArray<float> Values;
FORCEINLINE float Get(int32 X, int32 Y, int32 Z) const
{
const int32 LocalX = X - MinCell.X;
const int32 LocalY = Y - MinCell.Y;
const int32 LocalZ = Z - MinCell.Z;
return Values[(LocalZ * Size.Y + LocalY) * Size.X + LocalX];
}
};
constexpr float BroadDetailTransform[9] = {
0.838671f, -0.493587f, 0.230129f,
0.544639f, 0.760095f, -0.354003f,
0.0f, 0.422618f, 0.906308f};
constexpr float MediumDetailTransform[9] = {
0.956305f, 0.220661f, 0.191825f,
-0.292372f, 0.721484f, 0.627548f,
0.0f, 0.656059f, 0.754710f};
constexpr float FineDetailTransform[9] = {
0.325568f, -0.834725f, -0.443914f,
0.945519f, 0.287381f, 0.152824f,
0.0f, -0.469472f, 0.882948f};
FORCEINLINE FNoisePoint3 TransformNoisePoint(
const float* Matrix, float X, float Y, float Z)
{
return FNoisePoint3{
Matrix[0] * X + Matrix[1] * Y + Matrix[2] * Z,
Matrix[3] * X + Matrix[4] * Y + Matrix[5] * Z,
Matrix[6] * X + Matrix[7] * Y + Matrix[8] * Z};
}
void BuildNoiseLatticeCache(const FIntVector& Origin, const FIntVector& Size,
int32 SampleStep, float CellSize, uint32 Seed, const float* Transform,
FNoiseLatticeCache& OutCache)
{
const float InvCellSize = 1.0f / CellSize;
const FIntVector End = Origin + FIntVector(
FMath::Max(Size.X - 1, 0), FMath::Max(Size.Y - 1, 0),
FMath::Max(Size.Z - 1, 0)) * SampleStep;
FIntVector MinCell(MAX_int32, MAX_int32, MAX_int32);
FIntVector MaxCell(MIN_int32, MIN_int32, MIN_int32);
for (int32 Corner = 0; Corner < 8; ++Corner)
{
const float X = static_cast<float>((Corner & 1) ? End.X : Origin.X);
const float Y = static_cast<float>((Corner & 2) ? End.Y : Origin.Y);
const float Z = static_cast<float>((Corner & 4) ? End.Z : Origin.Z);
const FNoisePoint3 Point = TransformNoisePoint(Transform, X, Y, Z);
const FIntVector Cell(
FMath::FloorToInt(Point.X * InvCellSize),
FMath::FloorToInt(Point.Y * InvCellSize),
FMath::FloorToInt(Point.Z * InvCellSize));
MinCell.X = FMath::Min(MinCell.X, Cell.X);
MinCell.Y = FMath::Min(MinCell.Y, Cell.Y);
MinCell.Z = FMath::Min(MinCell.Z, Cell.Z);
MaxCell.X = FMath::Max(MaxCell.X, Cell.X);
MaxCell.Y = FMath::Max(MaxCell.Y, Cell.Y);
MaxCell.Z = FMath::Max(MaxCell.Z, Cell.Z);
}
// Include a spare lattice layer on either side so floating point rounding at a rotated
// brick edge cannot make a later sample address outside the exact cached hash range.
OutCache.MinCell = MinCell - FIntVector(2, 2, 2);
const FIntVector MaxWithMargin = MaxCell + FIntVector(2, 2, 2);
OutCache.Size = MaxWithMargin - OutCache.MinCell + FIntVector(1, 1, 1);
const int64 ValueCount = static_cast<int64>(OutCache.Size.X)
* OutCache.Size.Y * OutCache.Size.Z;
check(ValueCount > 0 && ValueCount <= MAX_int32);
OutCache.Values.SetNumUninitialized(static_cast<int32>(ValueCount));
for (int32 Z = 0; Z < OutCache.Size.Z; ++Z)
for (int32 Y = 0; Y < OutCache.Size.Y; ++Y)
for (int32 X = 0; X < OutCache.Size.X; ++X)
{
OutCache.Values[(Z * OutCache.Size.Y + Y) * OutCache.Size.X + X] = HashValue(
OutCache.MinCell.X + X, OutCache.MinCell.Y + Y,
OutCache.MinCell.Z + Z, Seed);
}
}
FORCEINLINE float Noise3DWithLatticeCache(float X, float Y, float Z,
float CellSize, const FNoiseLatticeCache& Cache)
{
const float Inv = 1.0f / CellSize;
const float SX = X * Inv;
const float SY = Y * Inv;
const float SZ = Z * Inv;
const int32 IX = FMath::FloorToInt(SX);
const int32 IY = FMath::FloorToInt(SY);
const int32 IZ = FMath::FloorToInt(SZ);
check(IX >= Cache.MinCell.X && IX + 1 < Cache.MinCell.X + Cache.Size.X
&& IY >= Cache.MinCell.Y && IY + 1 < Cache.MinCell.Y + Cache.Size.Y
&& IZ >= Cache.MinCell.Z && IZ + 1 < Cache.MinCell.Z + Cache.Size.Z);
const float AX = Fade(SX - static_cast<float>(IX));
const float AY = Fade(SY - static_cast<float>(IY));
const float AZ = Fade(SZ - static_cast<float>(IZ));
// All eight lattice values are adjacent in the cache. Resolve the sample base once
// instead of recalculating three local coordinates and a 3D index for each corner.
const int32 LocalX = IX - Cache.MinCell.X;
const int32 LocalY = IY - Cache.MinCell.Y;
const int32 LocalZ = IZ - Cache.MinCell.Z;
const int32 RowStride = Cache.Size.X;
const int32 PlaneStride = RowStride * Cache.Size.Y;
const float* const Lattice = Cache.Values.GetData()
+ (LocalZ * Cache.Size.Y + LocalY) * RowStride + LocalX;
const float Z0Y0 = FMath::Lerp(
FMath::Lerp(Lattice[0], Lattice[1], AX),
FMath::Lerp(Lattice[RowStride], Lattice[RowStride + 1], AX), AY);
const float Z0Y1 = FMath::Lerp(
FMath::Lerp(Lattice[PlaneStride], Lattice[PlaneStride + 1], AX),
FMath::Lerp(Lattice[PlaneStride + RowStride],
Lattice[PlaneStride + RowStride + 1], AX), AY);
return FMath::Lerp(Z0Y0, Z0Y1, AZ);
}
FORCEINLINE float DetailAtWithLatticeCaches(float X, float Y, float Z, uint32 Seed,
const FNoiseLatticeCache& BroadCache, const FNoiseLatticeCache& MediumCache,
const FNoiseLatticeCache& FineCache)
{
const FNoisePoint3 BroadPoint = TransformNoisePoint(BroadDetailTransform, X, Y, Z);
const FNoisePoint3 MediumPoint = TransformNoisePoint(MediumDetailTransform, X, Y, Z);
const FNoisePoint3 FinePoint = TransformNoisePoint(FineDetailTransform, X, Y, Z);
const float Broad = Noise3DWithLatticeCache(BroadPoint.X, BroadPoint.Y, BroadPoint.Z,
VoxelPassageGeometry::MetersToVoxels(12.0f), BroadCache);
const float Medium = Noise3DWithLatticeCache(MediumPoint.X, MediumPoint.Y, MediumPoint.Z,
VoxelPassageGeometry::MetersToVoxels(6.0f), MediumCache);
const float Fine = Noise3DWithLatticeCache(FinePoint.X, FinePoint.Y, FinePoint.Z,
VoxelPassageGeometry::MetersToVoxels(3.0f), FineCache);
const float SmoothRidge = FMath::Sqrt(Fine * Fine + 0.0625f) - 0.25f;
return (Broad * 1.82f + Medium * 1.092f + Fine * 0.546f
+ SmoothRidge * 0.364f) * DensityUnitsPerLegacyVoxel;
}
struct FThreadParameterizedDetailCache
{
static constexpr int32 RegionSpanVoxels = 512;
static constexpr int32 MarginVoxels = 64;
bool bReady = false;
uint32 Seed = 0;
FIntVector RegionMin = FIntVector::ZeroValue;
FNoiseLatticeCache Broad;
FNoiseLatticeCache Medium;
FNoiseLatticeCache Fine;
bool Prepare(FIntVector Origin, FIntVector Size, int32 SampleStep, uint32 InSeed)
{
const int32 Step = FMath::Max(1, SampleStep);
if (static_cast<int64>(FMath::Max(0, Size.X - 1)) * Step > MarginVoxels
|| static_cast<int64>(FMath::Max(0, Size.Y - 1)) * Step > MarginVoxels
|| static_cast<int64>(FMath::Max(0, Size.Z - 1)) * Step > MarginVoxels)
{
return false;
}
const auto GetCenteredRegionMin = [](int32 Value)
{
const float Shifted = static_cast<float>(Value)
+ static_cast<float>(RegionSpanVoxels / 2);
const int32 RegionIndex = FMath::FloorToInt(
Shifted / static_cast<float>(RegionSpanVoxels));
return RegionIndex * RegionSpanVoxels - RegionSpanVoxels / 2;
};
const FIntVector RequestedRegion(
GetCenteredRegionMin(Origin.X),
GetCenteredRegionMin(Origin.Y),
GetCenteredRegionMin(Origin.Z));
if (bReady && Seed == InSeed && RegionMin == RequestedRegion)
{
return true;
}
Seed = InSeed;
RegionMin = RequestedRegion;
const FIntVector CacheOrigin = RegionMin - FIntVector(
MarginVoxels, MarginVoxels, MarginVoxels);
const int32 CacheSpan = RegionSpanVoxels + MarginVoxels * 2;
const FIntVector CacheSize(CacheSpan, CacheSpan, CacheSpan);
BuildNoiseLatticeCache(CacheOrigin, CacheSize, 1,
VoxelPassageGeometry::MetersToVoxels(12.0f),
Seed ^ 0x68BC21EBu, BroadDetailTransform, Broad);
BuildNoiseLatticeCache(CacheOrigin, CacheSize, 1,
VoxelPassageGeometry::MetersToVoxels(6.0f),
Seed ^ 0x02E5BE93u, MediumDetailTransform, Medium);
BuildNoiseLatticeCache(CacheOrigin, CacheSize, 1,
VoxelPassageGeometry::MetersToVoxels(3.0f),
Seed ^ 0x967A889Bu, FineDetailTransform, Fine);
bReady = true;
return true;
}
};
thread_local FThreadParameterizedDetailCache GParameterizedDetailCache;
FORCEINLINE float DetailAt(
float X, float Y, float Z, uint32 Seed, bool bCoherentStep2f)
{
if (!bCoherentStep2f || IsLegacyStep2eAuditMode())
{
const float Broad = Noise3D(X, Y, Z,
VoxelPassageGeometry::MetersToVoxels(2.0f), Seed ^ 0x68BC21EBu);
const float Medium = Noise3D(X, Y, Z,
VoxelPassageGeometry::MetersToVoxels(1.0f), Seed ^ 0x02E5BE93u);
const float Fine = Noise3D(X, Y, Z,
VoxelPassageGeometry::MetersToVoxels(0.5f), Seed ^ 0x967A889Bu);
const float Micro = Noise3D(X, Y, Z,
VoxelPassageGeometry::MetersToVoxels(0.25f), Seed ^ 0xA511E9B3u);
return (Broad * 1.4f + Medium * 0.9f + Fine * 0.45f + Micro * 2.5f)
* DensityUnitsPerLegacyVoxel;
}
// The shortest wavelength is three metres: 24 lattice samples at 12.5 cm/voxel. Three
// independently rotated, interpolated scales and the smooth ridge response keep broad folds
// while excluding the lattice-scale noise used by the frozen 2e field.
// Independent orthonormal frames keep the value-noise bands smooth and bounded without
// exposing their axis-aligned interpolation cells as repeated square relief on the rock.
const float BroadX = 0.838671f * X - 0.493587f * Y + 0.230129f * Z;
const float BroadY = 0.544639f * X + 0.760095f * Y - 0.354003f * Z;
const float BroadZ = 0.422618f * Y + 0.906308f * Z;
const float MediumX = 0.956305f * X + 0.220661f * Y + 0.191825f * Z;
const float MediumY = -0.292372f * X + 0.721484f * Y + 0.627548f * Z;
const float MediumZ = 0.656059f * Y + 0.754710f * Z;
const float FineX = 0.325568f * X - 0.834725f * Y - 0.443914f * Z;
const float FineY = 0.945519f * X + 0.287381f * Y + 0.152824f * Z;
const float FineZ = -0.469472f * Y + 0.882948f * Z;
const float Broad = Noise3D(BroadX, BroadY, BroadZ,
VoxelPassageGeometry::MetersToVoxels(12.0f), Seed ^ 0x68BC21EBu);
const float Medium = Noise3D(MediumX, MediumY, MediumZ,
VoxelPassageGeometry::MetersToVoxels(6.0f), Seed ^ 0x02E5BE93u);
const float Fine = Noise3D(FineX, FineY, FineZ,
VoxelPassageGeometry::MetersToVoxels(3.0f), Seed ^ 0x967A889Bu);
const float SmoothRidge = FMath::Sqrt(Fine * Fine + 0.0625f) - 0.25f;
return (Broad * 1.82f + Medium * 1.092f + Fine * 0.546f
+ SmoothRidge * 0.364f) * DensityUnitsPerLegacyVoxel;
}
FORCEINLINE float DistanceToPeriodicLine(float Position, float Spacing, float Offset)
{
const float Cell = (Position - Offset) / Spacing;
const float Fraction = Cell - static_cast<float>(FMath::FloorToInt(Cell + 0.5f));
return FMath::Abs(Fraction) * Spacing;
}
FORCEINLINE float DistanceSquaredToSegmentXY(
float X, float Y, const FVector& A, const FVector& B)
{
const float ABX = B.X - A.X;
const float ABY = B.Y - A.Y;
const float LengthSquared = ABX * ABX + ABY * ABY;
const float T = LengthSquared > 1.0e-8f
? FMath::Clamp(((X - A.X) * ABX + (Y - A.Y) * ABY) / LengthSquared, 0.0f, 1.0f)
: 0.0f;
const float DX = X - (A.X + T * ABX);
const float DY = Y - (A.Y + T * ABY);
return DX * DX + DY * DY;
}
FORCEINLINE float SegmentParameterXY(
float X, float Y, const FVector& A, const FVector& B)
{
const float ABX = B.X - A.X;
const float ABY = B.Y - A.Y;
const float LengthSquared = ABX * ABX + ABY * ABY;
return LengthSquared > 1.0e-8f
? FMath::Clamp(((X - A.X) * ABX + (Y - A.Y) * ABY) / LengthSquared, 0.0f, 1.0f)
: 0.0f;
}
FORCEINLINE bool RollVerticalShaft(
const FVerticalShaftParams& Params, uint32 ShaftSeed,
int32 CellX, int32 CellY, FVerticalShaftNode& OutNode)
{
const float Spacing = FMath::Max(Params.ShaftSpacing, 1.0f);
const uint32 Hash = VoxelHash::Cell(CellX, CellY, ShaftSeed);
if (VoxelHash::ToFloat01(Hash) > Params.ShaftDensity)
{
return false;
}
OutNode.CellX = CellX;
OutNode.CellY = CellY;
OutNode.X = (CellX + 0.15f
+ VoxelHash::ToFloat01(VoxelHash::Mix(Hash ^ 0x12345678u)) * 0.7f) * Spacing;
OutNode.Y = (CellY + 0.15f
+ VoxelHash::ToFloat01(VoxelHash::Mix(Hash ^ 0x9ABCDEF0u)) * 0.7f) * Spacing;
OutNode.Radius = FMath::Lerp(Params.ShaftMinRadius, Params.ShaftMaxRadius,
VoxelHash::ToFloat01(VoxelHash::Mix(Hash ^ 0xBEEFu)));
return true;
}
bool ResolveVerticalShaftParent(
const FVerticalShaftParams& Params, uint32 ShaftSeed,
const FVerticalShaftNode& Child, FVerticalShaftNode& OutParent)
{
const float ChildOriginSq = FMath::Square(Child.X) + FMath::Square(Child.Y);
bool bFoundParent = false;
float BestDistanceSq = FLT_MAX;
const auto Search = [&](int32 Radius)
{
for (int32 DY = -Radius; DY <= Radius; ++DY)
for (int32 DX = -Radius; DX <= Radius; ++DX)
{
FVerticalShaftNode Candidate;
if ((DX == 0 && DY == 0)
|| !RollVerticalShaft(Params, ShaftSeed,
Child.CellX + DX, Child.CellY + DY, Candidate))
{
continue;
}
const float CandidateOriginSq = FMath::Square(Candidate.X)
+ FMath::Square(Candidate.Y);
if (!(CandidateOriginSq < ChildOriginSq))
{
continue;
}
const float DistanceSq = FMath::Square(Child.X - Candidate.X)
+ FMath::Square(Child.Y - Candidate.Y);
const bool bLowerCell = !bFoundParent
|| Candidate.CellY < OutParent.CellY
|| (Candidate.CellY == OutParent.CellY
&& Candidate.CellX < OutParent.CellX);
if (DistanceSq < BestDistanceSq
|| (DistanceSq == BestDistanceSq && bLowerCell))
{
BestDistanceSq = DistanceSq;
OutParent = Candidate;
bFoundParent = true;
}
}
};
Search(2);
if (bFoundParent)
{
return true;
}
if (FMath::Abs(Child.CellX) <= 2 && FMath::Abs(Child.CellY) <= 2)
{
return false;
}
Search(3);
return bFoundParent;
}
float ResolveVerticalShaftConnectorZ(
const FVerticalShaftParams& Params, uint32 LinkHash)
{
const float BottomZ = Params.StrateBottomWorldZ + Params.BoundarySealThickness;
const float TopZ = Params.StrateTopWorldZ - Params.BoundarySealThickness;
if (Params.LedgeSpacing > 0.0f && Params.LedgeDepth > 0.0f)
{
const float RelativeBottom = BottomZ - Params.StrateBottomWorldZ;
const float RelativeTop = TopZ - Params.StrateBottomWorldZ;
const int32 FirstPeriod = FMath::FloorToInt(
RelativeBottom / Params.LedgeSpacing);
const float Random01 = VoxelHash::ToFloat01(VoxelHash::Mix(LinkHash));
for (int32 PeriodOffset = 0; PeriodOffset <= 1; ++PeriodOffset)
{
const float PeriodStart = static_cast<float>(FirstPeriod + PeriodOffset)
* Params.LedgeSpacing;
const float SafeStart = FMath::Max(
RelativeBottom, PeriodStart + Params.LedgeDepth + 0.01f);
const float SafeEnd = FMath::Min(
RelativeTop, PeriodStart + Params.LedgeSpacing
- Params.LedgeDepth - 0.01f);
if (SafeEnd > SafeStart)
{
return Params.StrateBottomWorldZ
+ FMath::Lerp(SafeStart, SafeEnd, Random01);
}
}
}
return FMath::Lerp(BottomZ, TopZ, VoxelHash::ToFloat01(VoxelHash::Mix(LinkHash)));
}
void RebuildVerticalShaftEdgeCache(const FStratePlan& Plan, float X, float Y)
{
const FVerticalShaftParams& Params = Plan.VerticalShaftParams;
const float Spacing = FMath::Max(Params.ShaftSpacing, 1.0f);
const int32 CellX = FMath::FloorToInt(X / Spacing);
const int32 CellY = FMath::FloorToInt(Y / Spacing);
if (GShaftEdgeCache.Plan == &Plan
&& GShaftEdgeCache.Manager == GPlanCache.Manager
&& GShaftEdgeCache.ManagerLifetimeId == GPlanCache.ManagerLifetimeId
&& GShaftEdgeCache.LayoutVersion == GPlanCache.LayoutVersion
&& GShaftEdgeCache.Seed == GPlanCache.Seed
&& GShaftEdgeCache.CellX == CellX && GShaftEdgeCache.CellY == CellY)
{
return;
}
GShaftEdgeCache.Plan = &Plan;
GShaftEdgeCache.Manager = GPlanCache.Manager;
GShaftEdgeCache.ManagerLifetimeId = GPlanCache.ManagerLifetimeId;
GShaftEdgeCache.LayoutVersion = GPlanCache.LayoutVersion;
GShaftEdgeCache.Seed = GPlanCache.Seed;
GShaftEdgeCache.CellX = CellX;
GShaftEdgeCache.CellY = CellY;
GShaftEdgeCache.Edges.Reset();
const uint32 ShaftSeed = static_cast<uint32>(GPlanCache.Seed) ^ 0x53686674u;
// Keep at least a 9 m diameter for the deterministic route tree, and retain room for
// the full wall-detail envelope at the current voxel scale.
const float VerticalShaftDetailPad = LegacyDetailAmplitude
* VoxelPassageGeometry::VoxelSizeMeters
+ VoxelPassageGeometry::MetersToVoxels(0.25f);
const float ConnectorRadius = FMath::Max3(
Params.ConnectorRadius, VerticalShaftDetailPad,
VoxelPassageGeometry::MetersToVoxels(4.5f));
const float CellMinX = CellX * Spacing;
const float CellMaxX = CellMinX + Spacing;
const float CellMinY = CellY * Spacing;
const float CellMaxY = CellMinY + Spacing;
for (int32 ChildCellY = CellY - VerticalShaftConnectorChildReach;
ChildCellY <= CellY + VerticalShaftConnectorChildReach; ++ChildCellY)
for (int32 ChildCellX = CellX - VerticalShaftConnectorChildReach;
ChildCellX <= CellX + VerticalShaftConnectorChildReach; ++ChildCellX)
{
FVerticalShaftNode Child;
if (!RollVerticalShaft(Params, ShaftSeed, ChildCellX, ChildCellY, Child))
{
continue;
}
FVerticalShaftNode Parent;
const bool bHasParent = ResolveVerticalShaftParent(
Params, ShaftSeed, Child, Parent);
const FVector ParentXY = bHasParent
? FVector(Parent.X, Parent.Y, 0.0f) : FVector::ZeroVector;
const bool bMayReachCell = FMath::Max(Child.X, ParentXY.X) + ConnectorRadius >= CellMinX
&& FMath::Min(Child.X, ParentXY.X) - ConnectorRadius <= CellMaxX
&& FMath::Max(Child.Y, ParentXY.Y) + ConnectorRadius >= CellMinY
&& FMath::Min(Child.Y, ParentXY.Y) - ConnectorRadius <= CellMaxY;
if (!bMayReachCell)
{
continue;
}
const uint32 LinkHash = VoxelHash::Pair(
Child.CellX, Child.CellY,
bHasParent ? Parent.CellX : 0,
bHasParent ? Parent.CellY : 0,
ShaftSeed ^ VerticalShaftTreeSalt);
const float ConnectorZ = ResolveVerticalShaftConnectorZ(Params, LinkHash);
FVerticalShaftEdge& Edge = GShaftEdgeCache.Edges.AddDefaulted_GetRef();
Edge.A = FVector(Child.X, Child.Y, ConnectorZ);
Edge.B = FVector(ParentXY.X, ParentXY.Y, ConnectorZ);
Edge.Radius = ConnectorRadius;
}
}
float EvaluateVerticalShaftMacro(
const FStratePlan& Plan, float X, float Y, float Z)
{
const FVerticalShaftParams& Params = Plan.VerticalShaftParams;
const float Spacing = FMath::Max(Params.ShaftSpacing, 1.0f);
const int32 BaseCellX = FMath::FloorToInt(X / Spacing);
const int32 BaseCellY = FMath::FloorToInt(Y / Spacing);
const float VerticalPad = LegacyDetailAmplitude
* VoxelPassageGeometry::VoxelSizeMeters
+ VoxelPassageGeometry::MetersToVoxels(0.25f);
const float BottomZ = Params.StrateBottomWorldZ + Params.BoundarySealThickness;
const float TopZ = Params.StrateTopWorldZ - Params.BoundarySealThickness;
const float ShaftVertical = FMath::Min(Z - BottomZ, TopZ - Z);
float Macro = FMath::Min(
VerticalShaftSpineRadius + VerticalPad - FMath::Sqrt(X * X + Y * Y),
ShaftVertical);
const uint32 ShaftSeed = static_cast<uint32>(GPlanCache.Seed) ^ 0x53686674u;
for (int32 CellY = BaseCellY - 1; CellY <= BaseCellY + 1; ++CellY)
for (int32 CellX = BaseCellX - 1; CellX <= BaseCellX + 1; ++CellX)
{
FVerticalShaftNode Shaft;
if (!RollVerticalShaft(Params, ShaftSeed, CellX, CellY, Shaft))
{
continue;
}
const float ShaftHorizontal = Shaft.Radius + VerticalPad
- FMath::Sqrt(FMath::Square(X - Shaft.X) + FMath::Square(Y - Shaft.Y));
Macro = FMath::Max(Macro, FMath::Min(ShaftHorizontal, ShaftVertical));
}
RebuildVerticalShaftEdgeCache(Plan, X, Y);
for (const FVerticalShaftEdge& Edge : GShaftEdgeCache.Edges)
{
const float HorizontalSq = DistanceSquaredToSegmentXY(X, Y, Edge.A, Edge.B);
const float DistSq = HorizontalSq + FMath::Square(Z - Edge.A.Z);
Macro = FMath::Max(Macro, Edge.Radius - FMath::Sqrt(DistSq));
}
return Macro;
}
void EnsurePlan(const UVoxelGenerator& Generator)
{
const UVoxelStrateManager* Manager = Generator.StrateManager;
if (Manager == nullptr)
{
GPlanCache = FThreadPlanCache();
GParameterizedRoomCellCachePlan = nullptr;
return;
}
const uint64 Lifetime = Manager->GetCacheLifetimeId();
const uint32 Version = Manager->GetLayoutVersion();
const int32 ParameterSet = CVarResearchParameterizedCaveSet.GetValueOnAnyThread();
const bool bLegacyStep2eAuditMode = IsLegacyStep2eAuditMode();
if (GPlanCache.Manager == Manager
&& GPlanCache.ManagerLifetimeId == Lifetime
&& GPlanCache.LayoutVersion == Version
&& GPlanCache.Seed == Generator.Seed
&& GPlanCache.ParameterSet == ParameterSet
&& GPlanCache.bLegacyStep2eAuditMode == bLegacyStep2eAuditMode)
{
return;
}
GPlanCache = FThreadPlanCache();
GParameterizedRoomCellCachePlan = nullptr;
GPlanCache.Manager = Manager;
GPlanCache.ManagerLifetimeId = Lifetime;
GPlanCache.LayoutVersion = Version;
GPlanCache.Seed = Generator.Seed;
GPlanCache.ParameterSet = ParameterSet;
GPlanCache.bLegacyStep2eAuditMode = bLegacyStep2eAuditMode;
const TArray<FStrateSlot>& Layout = Manager->GetLayout();
GPlanCache.Strates.Reserve(Layout.Num());
for (const FStrateSlot& Slot : Layout)
{
if (Slot.Definition == nullptr)
{
continue;
}
FStratePlan& Plan = GPlanCache.Strates.AddDefaulted_GetRef();
Plan.StrateIndex = Slot.StrateIndex;
Plan.MinChunkZ = FMath::Min(Slot.BottomChunkZ, Slot.TopChunkZ);
Plan.MaxChunkZExclusive = FMath::Max(Slot.BottomChunkZ, Slot.TopChunkZ) + 1;
Plan.Archetype = Slot.Definition->GeneratorType;
if (Plan.Archetype == ECaveGeneratorType::VerticalShafts)
{
Plan.VerticalShaftParams = Manager->GetVerticalShaftParamsForChunk(
FIntVector(0, 0, Plan.MinChunkZ));
}
const float Bottom = static_cast<float>(Plan.MinChunkZ * CHUNK_SIZE);
const float Top = static_cast<float>(Plan.MaxChunkZExclusive * CHUNK_SIZE);
Plan.FloorZ = (Bottom + Top) * 0.5f
- VoxelPassageGeometry::MetersToVoxels(6.0f);
Plan.CeilingZ = Plan.FloorZ + MinimumCaveHeight;
switch (Plan.Archetype)
{
case ECaveGeneratorType::FlatPlain:
Plan.CellSpacing = VoxelPassageGeometry::MetersToVoxels(40.0f);
Plan.CorridorHalfWidth = VoxelPassageGeometry::MetersToVoxels(13.5f);
break;
case ECaveGeneratorType::CrystalChamber:
Plan.CellSpacing = VoxelPassageGeometry::MetersToVoxels(30.0f);
Plan.CorridorHalfWidth = VoxelPassageGeometry::MetersToVoxels(10.5f);
break;
case ECaveGeneratorType::Maze:
Plan.CellSpacing = VoxelPassageGeometry::MetersToVoxels(24.0f);
Plan.CorridorHalfWidth = VoxelPassageGeometry::MetersToVoxels(7.5f);
break;
case ECaveGeneratorType::VerticalShafts:
Plan.CellSpacing = VoxelPassageGeometry::MetersToVoxels(28.0f);
Plan.CorridorHalfWidth = VoxelPassageGeometry::MetersToVoxels(9.0f);
break;
case ECaveGeneratorType::FloatingIslands:
Plan.CellSpacing = VoxelPassageGeometry::MetersToVoxels(28.0f);
Plan.CorridorHalfWidth = VoxelPassageGeometry::MetersToVoxels(8.5f);
break;
case ECaveGeneratorType::SurfaceWorld:
Plan.CellSpacing = VoxelPassageGeometry::MetersToVoxels(36.0f);
Plan.CorridorHalfWidth = VoxelPassageGeometry::MetersToVoxels(11.5f);
break;
default:
Plan.CellSpacing = VoxelPassageGeometry::MetersToVoxels(32.0f);
Plan.CorridorHalfWidth = VoxelPassageGeometry::MetersToVoxels(10.0f);
break;
}
const uint32 StrateSeed = static_cast<uint32>(Generator.Seed)
^ static_cast<uint32>(Plan.StrateIndex * 7919 + 104729);
Plan.GenerationSeed = StrateSeed ^ 0x2E6F9A31u;
Plan.GenerationProfile = ParameterSet >= 0
? FMath::Clamp(ParameterSet, 0, UE_ARRAY_COUNT(GParameterProfiles) - 1)
: static_cast<int32>(Hash3(
Plan.StrateIndex, Plan.MinChunkZ, 0x2E, StrateSeed)
% UE_ARRAY_COUNT(GParameterProfiles));
Plan.Generation = (bLegacyStep2eAuditMode
? GLegacyStep2eProfiles[Plan.GenerationProfile]
: GParameterProfiles[Plan.GenerationProfile]).Parameters;
const float StrateHeight = bLegacyStep2eAuditMode
? FMath::Max(MinimumCaveHeight,
Top - Bottom - VoxelPassageGeometry::MetersToVoxels(2.0f))
: FMath::Max(MinimumCaveHeight,
Top - Bottom - VoxelPassageGeometry::MetersToVoxels(16.0f));
Plan.Generation.RoomHeightVoxels = FMath::Clamp(
Plan.Generation.RoomHeightVoxels,
MinimumCaveHeight, StrateHeight);
Plan.Generation.RoomPitchVoxels = FMath::Max(
Plan.Generation.RoomPitchVoxels,
VoxelPassageGeometry::MetersToVoxels(20.0f));
Plan.Generation.MinimumRoomRadiusFraction = FMath::Clamp(
Plan.Generation.MinimumRoomRadiusFraction, 0.20f, 0.72f);
Plan.Generation.MaximumRoomRadiusFraction = FMath::Clamp(
Plan.Generation.MaximumRoomRadiusFraction,
Plan.Generation.MinimumRoomRadiusFraction, 0.80f);
Plan.Generation.LoopProbability = FMath::Clamp(
Plan.Generation.LoopProbability, 0.0f, 0.8f);
Plan.Generation.VerticalShaftProbability = FMath::Clamp(
Plan.Generation.VerticalShaftProbability, 0.0f, 1.0f);
Plan.Generation.PillarProbability = FMath::Clamp(
Plan.Generation.PillarProbability, 0.0f, 1.0f);
Plan.Generation.LedgeProbability = FMath::Clamp(
Plan.Generation.LedgeProbability, 0.0f, 1.0f);
Plan.Generation.FloatingMassProbability = FMath::Clamp(
Plan.Generation.FloatingMassProbability, 0.0f, 1.0f);
Plan.Generation.SpireProbability = FMath::Clamp(
Plan.Generation.SpireProbability, 0.0f, 1.0f);
Plan.Generation.BasinProbability = FMath::Clamp(
Plan.Generation.BasinProbability, 0.0f, 1.0f);
Plan.Generation.BasinDepthVoxels = FMath::Clamp(
Plan.Generation.BasinDepthVoxels, 0.0f,
VoxelPassageGeometry::MetersToVoxels(12.0f));
Plan.Generation.WaterLevelFraction = FMath::Clamp(
Plan.Generation.WaterLevelFraction, 0.0f, 1.0f);
Plan.GenerationFloorZ = (Bottom + Top - Plan.Generation.RoomHeightVoxels) * 0.5f;
static constexpr float GridRotations[][2] = {
{1.0f, 0.0f}, {0.923879533f, 0.382683432f},
{0.707106781f, 0.707106781f}, {0.382683432f, 0.923879533f},
{0.0f, 1.0f}, {-0.382683432f, 0.923879533f},
{-0.707106781f, 0.707106781f}, {-0.923879533f, 0.382683432f},
};
const uint32 RotationHash = Hash3(
Plan.StrateIndex, Plan.MinChunkZ, 0x8D, StrateSeed);
const int32 RotationIndex = static_cast<int32>(RotationHash % UE_ARRAY_COUNT(GridRotations));
Plan.GridCos = GridRotations[RotationIndex][0];
Plan.GridSin = GridRotations[RotationIndex][1];
Plan.GridOffsetX = VoxelHash::ToFloat01(VoxelHash::Mix(RotationHash ^ 0x1975u))
* Plan.Generation.RoomPitchVoxels;
Plan.GridOffsetY = VoxelHash::ToFloat01(VoxelHash::Mix(RotationHash ^ 0x67A3u))
* Plan.Generation.RoomPitchVoxels;
if (Plan.Archetype == ECaveGeneratorType::VerticalShafts)
{
// Keep the archetype's central origin spine open for its authored route probe.
Plan.OffsetX = 0.0f;
Plan.OffsetY = 0.0f;
}
else
{
Plan.OffsetX = static_cast<float>(Hash3(
Plan.StrateIndex, Plan.MinChunkZ, 17, StrateSeed) >> 8)
* (Plan.CellSpacing / 16777216.0f);
Plan.OffsetY = static_cast<float>(Hash3(
Plan.StrateIndex, Plan.MinChunkZ, 31, StrateSeed) >> 8)
* (Plan.CellSpacing / 16777216.0f);
}
}
const TArray<FVoxelPassage>& Passages = Manager->GetPassages();
for (const FVoxelPassage& Passage : Passages)
{
const auto AddSegmentBounds = [](
const FVector& A, const FVector& B, float Radius)
{
FPassageBound& Bounds = GPlanCache.PassageBounds.AddDefaulted_GetRef();
const float R = FMath::Max(Radius,
VoxelPassageGeometry::MetersToVoxels(2.0f)) + PassageBoundPadding;
Bounds.SegmentA = A;
Bounds.SegmentB = B;
Bounds.SupportRadius = R;
Bounds.bIsSegment = true;
Bounds.Min = FVector(
FMath::Min(A.X, B.X) - R,
FMath::Min(A.Y, B.Y) - R,
FMath::Min(A.Z, B.Z) - R);
Bounds.Max = FVector(
FMath::Max(A.X, B.X) + R,
FMath::Max(A.Y, B.Y) + R,
FMath::Max(A.Z, B.Z) + R);
};
if (Passage.ControlPoints.Num() >= 2
&& Passage.ControlRadii.Num() == Passage.ControlPoints.Num())
{
for (int32 Segment = 0; Segment + 1 < Passage.ControlPoints.Num(); ++Segment)
{
AddSegmentBounds(
Passage.ControlPoints[Segment], Passage.ControlPoints[Segment + 1],
FMath::Max(Passage.Radius,
FMath::Max(FMath::Abs(Passage.ControlRadii[Segment]),
FMath::Abs(Passage.ControlRadii[Segment + 1]))));
}
}
else
{
AddSegmentBounds(Passage.UpperPoint, Passage.LowerPoint, Passage.Radius);
}
const auto AddLandingBounds = [](const FVoxelPassageLanding& Landing)
{
const float XYExtent = Landing.HalfWidth + PassageBoundPadding;
const float ZExtent = FMath::Max(
Landing.HalfWidth, Landing.CeilingZ - Landing.FloorZ)
+ PassageBoundPadding;
FPassageBound& Bounds = GPlanCache.PassageBounds.AddDefaulted_GetRef();
const FVector Pad(XYExtent, XYExtent, ZExtent);
const FVector& P = Landing.StandingPoint;
Bounds.Min = P - Pad;
Bounds.Max = P + Pad;
};
AddLandingBounds(Passage.UpperLanding);
AddLandingBounds(Passage.LowerLanding);
for (FStratePlan& Plan : GPlanCache.Strates)
{
if (Plan.StrateIndex == Passage.UpperStrateIndex)
{
FLandingSite& Site = Plan.Landings.AddDefaulted_GetRef();
Site.Position = Passage.UpperLanding.StandingPoint;
Site.FloorZ = Passage.UpperLanding.FloorZ;
Site.HalfWidth = Passage.UpperLanding.HalfWidth;
Site.CeilingZ = Passage.UpperLanding.CeilingZ;
}
if (Plan.StrateIndex == Passage.LowerStrateIndex)
{
FLandingSite& Site = Plan.Landings.AddDefaulted_GetRef();
Site.Position = Passage.LowerLanding.StandingPoint;
Site.FloorZ = Passage.LowerLanding.FloorZ;
Site.HalfWidth = Passage.LowerLanding.HalfWidth;
Site.CeilingZ = Passage.LowerLanding.CeilingZ;
}
}
}
for (FStratePlan& Plan : GPlanCache.Strates)
{
if (!Plan.Landings.IsEmpty())
{
double FloorSum = 0.0;
int32 FloorCount = 0;
for (FLandingSite& Site : Plan.Landings)
{
if (!FMath::IsFinite(Site.FloorZ))
{
continue;
}
FloorSum += Site.FloorZ;
++FloorCount;
Site.PadRadius = FMath::Max(
Site.HalfWidth + VoxelPassageGeometry::MetersToVoxels(6.0f),
VoxelPassageGeometry::MetersToVoxels(10.0f));
}
if (FloorCount > 0)
{
const float Bottom = static_cast<float>(Plan.MinChunkZ * CHUNK_SIZE);
const float Top = static_cast<float>(Plan.MaxChunkZExclusive * CHUNK_SIZE);
Plan.FloorZ = FMath::Clamp(
static_cast<float>(FloorSum / FloorCount),
Bottom + VoxelPassageGeometry::MetersToVoxels(4.0f),
Top - VoxelPassageGeometry::MetersToVoxels(18.0f));
Plan.CeilingZ = Plan.FloorZ + MinimumCaveHeight;
// Parameterized chambers keep their profile-defined vertical span. The
// passage landing is connected into that graph by its generated anchor.
}
}
for (FLandingSite& Site : Plan.Landings)
{
const int32 XLaneIndex = FMath::RoundToInt(
(Site.Position.X - Plan.OffsetX) / Plan.CellSpacing);
const int32 YLaneIndex = FMath::RoundToInt(
(Site.Position.Y - Plan.OffsetY) / Plan.CellSpacing);
const float XLaneCoordinate = Plan.OffsetX + XLaneIndex * Plan.CellSpacing;
const float YLaneCoordinate = Plan.OffsetY + YLaneIndex * Plan.CellSpacing;
const float XLaneDistance = FMath::Abs(Site.Position.X - XLaneCoordinate);
const float YLaneDistance = FMath::Abs(Site.Position.Y - YLaneCoordinate);
Site.ApproachEnd = Site.Position;
if (XLaneDistance <= YLaneDistance)
{
Site.ApproachEnd.X = XLaneCoordinate;
}
else
{
Site.ApproachEnd.Y = YLaneCoordinate;
}
const float FloorDelta = FMath::Abs(Site.FloorZ - Plan.FloorZ);
Site.BlendRadius = FMath::Max(
LandingFloorBlendVoxels, Site.PadRadius + FloorDelta * 2.0f
+ VoxelPassageGeometry::MetersToVoxels(2.0f));
}
Plan.LandingHubIndex = INDEX_NONE;
for (int32 SiteIndex = 0; SiteIndex < Plan.Landings.Num(); ++SiteIndex)
{
const FLandingSite& Candidate = Plan.Landings[SiteIndex];
if (Plan.LandingHubIndex == INDEX_NONE)
{
Plan.LandingHubIndex = SiteIndex;
continue;
}
const FLandingSite& Current = Plan.Landings[Plan.LandingHubIndex];
if (Candidate.Position.X < Current.Position.X
|| (Candidate.Position.X == Current.Position.X
&& Candidate.Position.Y < Current.Position.Y)
|| (Candidate.Position.X == Current.Position.X
&& Candidate.Position.Y == Current.Position.Y
&& Candidate.FloorZ < Current.FloorZ))
{
Plan.LandingHubIndex = SiteIndex;
}
}
}
for (FStratePlan& Plan : GPlanCache.Strates)
{
PrepareParameterizedAnchors(Plan);
}
}
struct FGeneratedCaveCell
{
int32 CellX = 0;
int32 CellY = 0;
float CenterX = 0.0f;
float CenterY = 0.0f;
float FloorZ = 0.0f;
float CeilingZ = 0.0f;
float RadiusX = 0.0f;
float RadiusY = 0.0f;
float RoomCos = 1.0f;
float RoomSin = 0.0f;
float PillarX = 0.0f;
float PillarY = 0.0f;
float PillarRadius = 0.0f;
float LedgeZ = 0.0f;
float FloatingX[2] = {};
float FloatingY[2] = {};
float FloatingZ[2] = {};
float FloatingRadius[2] = {};
float FloatingRadiusY[2] = {};
float FloatingRadiusZ[2] = {};
float SpireLength = 0.0f;
float SpireRadius = 0.0f;
uint8 FeatureFlags = 0;
uint8 OpenTunnelEdges = 0;
};
struct FGeneratedTunnelColumnProfile
{
float FloorZ = 0.0f;
float Radius = 0.0f;
float HalfHeight = 0.0f;
float VerticalWave = 0.0f;
};
struct FParameterizedColumnContext
{
FGeneratedCaveCell GeneratedCells[9];
const FGeneratedCaveCell* Cells[9] = {};
const FGeneratedCaveCell* TunnelCells[25] = {};
uint8 TunnelEdgeCellA[24] = {};
uint8 TunnelEdgeCellB[24] = {};
int32 TunnelEdgeCount = 0;
float RoomDistanceSquared[9] = {};
float RoomRadii[9] = {};
float RoomHorizontals[9] = {};
float RoomFloors[9] = {};
float PillarHorizontals[9] = {};
float FloatingXYDistanceSquared[9][2] = {};
float FloatingScaledLengthXY[9][2] = {};
float FloatingScaledGradientXY[9][2] = {};
float SpireBaseZ[9] = {};
float SpireTipZ[9] = {};
float SpireBaseTopZ[9] = {};
float SpireTipBottomZ[9] = {};
float TunnelT[24] = {};
float TunnelHorizontal[24] = {};
FGeneratedTunnelColumnProfile TunnelProfiles[24] = {};
float LandingDistanceSquared[8] = {};
float LandingDistance[8] = {};
float LandingBridgeT[8] = {};
float LandingBridgeHorizontal[8] = {};
bool bHasLandingXYCache = false;
};
constexpr uint8 HasPillarFlag = 1u << 0;
constexpr uint8 HasLedgeFlag = 1u << 1;
constexpr uint8 HasFloatingFlag = 1u << 2;
constexpr uint8 HasSpireFlag = 1u << 3;
constexpr uint8 HasBasinFlag = 1u << 4;
constexpr uint8 HasVerticalShaftFlag = 1u << 5;
constexpr uint8 TunnelToNegativeY = 1u << 0;
constexpr uint8 TunnelToPositiveY = 1u << 1;
constexpr uint8 TunnelToNegativeX = 1u << 2;
constexpr uint8 TunnelToPositiveX = 1u << 3;
thread_local TArray<FGeneratedCaveCell> GParameterizedRoomCellCache;
thread_local FIntPoint GParameterizedRoomCellCacheMin = FIntPoint::ZeroValue;
thread_local FIntPoint GParameterizedRoomCellCacheSize = FIntPoint::ZeroValue;
FIntPoint GeneratedParent(const FStratePlan& Plan, int32 CellX, int32 CellY);
bool IsGeneratedEdgeOpen(
const FStratePlan& Plan, int32 AX, int32 AY, int32 BX, int32 BY);
FORCEINLINE FIntPoint GenerationCellAt(const FStratePlan& Plan, float X, float Y);
FORCEINLINE float ParameterRoll(uint32 Hash, uint32 Salt)
{
return VoxelHash::ToFloat01(VoxelHash::Mix(Hash ^ Salt));
}
FORCEINLINE void WorldToGenerationGrid(
const FStratePlan& Plan, float X, float Y, float& OutX, float& OutY)
{
OutX = Plan.GridCos * X + Plan.GridSin * Y;
OutY = -Plan.GridSin * X + Plan.GridCos * Y;
}
FORCEINLINE void GeneratedCellGridCenter(
const FStratePlan& Plan, int32 CellX, int32 CellY,
float& OutX, float& OutY, bool bLegacyStep2eAuditMode)
{
const uint32 Hash = Hash3(CellX, CellY, Plan.StrateIndex, Plan.GenerationSeed);
const float JitterScale = bLegacyStep2eAuditMode ? 0.08f : 1.00f;
const float JitterX = (ParameterRoll(Hash, 0x91A2u) - 0.5f) * JitterScale;
const float JitterY = (ParameterRoll(Hash, 0xB4C3u) - 0.5f) * JitterScale;
const float Pitch = Plan.Generation.RoomPitchVoxels;
OutX = Plan.GridOffsetX + (CellX + 0.5f + JitterX) * Pitch;
OutY = Plan.GridOffsetY + (CellY + 0.5f + JitterY) * Pitch;
}
FGeneratedCaveCell BuildGeneratedCell(
const FStratePlan& Plan, int32 CellX, int32 CellY)
{
FGeneratedCaveCell Cell;
Cell.CellX = CellX;
Cell.CellY = CellY;
const FGenerationParameters& P = Plan.Generation;
const float Pitch = P.RoomPitchVoxels;
const uint32 Hash = Hash3(CellX, CellY, Plan.StrateIndex, Plan.GenerationSeed);
const bool bLegacyStep2eAuditMode = IsLegacyStep2eAuditMode();
float LocalX = 0.0f;
float LocalY = 0.0f;
GeneratedCellGridCenter(
Plan, CellX, CellY, LocalX, LocalY, bLegacyStep2eAuditMode);
Cell.CenterX = Plan.GridCos * LocalX - Plan.GridSin * LocalY;
Cell.CenterY = Plan.GridSin * LocalX + Plan.GridCos * LocalY;
const float RadiusFraction = FMath::Lerp(
P.MinimumRoomRadiusFraction, P.MaximumRoomRadiusFraction,
ParameterRoll(Hash, 0xA61Bu));
Cell.RadiusX = Pitch * RadiusFraction;
if (!bLegacyStep2eAuditMode)
{
float NearestCenterSpacing = FLT_MAX;
for (int32 NeighborY = -1; NeighborY <= 1; ++NeighborY)
for (int32 NeighborX = -1; NeighborX <= 1; ++NeighborX)
{
if (NeighborX == 0 && NeighborY == 0)
{
continue;
}
float NeighborLocalX = 0.0f;
float NeighborLocalY = 0.0f;
GeneratedCellGridCenter(Plan, CellX + NeighborX, CellY + NeighborY,
NeighborLocalX, NeighborLocalY, false);
NearestCenterSpacing = FMath::Min(NearestCenterSpacing,
FMath::Sqrt(FMath::Square(NeighborLocalX - LocalX)
+ FMath::Square(NeighborLocalY - LocalY)));
}
// Keep a measurable rock web between chambers. Their graph edges open only narrow
// connecting tunnels, so the room lattice no longer merges into a forest of pillars.
Cell.RadiusX = FMath::Min(Cell.RadiusX, NearestCenterSpacing * 0.47f);
const float Angle = ParameterRoll(Hash, 0xE17Du) * 6.283185307179586f;
VoxelMath::DetSinCos(Cell.RoomSin, Cell.RoomCos, Angle);
Cell.RadiusY = Cell.RadiusX * FMath::Lerp(
0.64f, 0.94f, ParameterRoll(Hash, 0x27C5u));
}
else
{
Cell.RadiusY = Cell.RadiusX * FMath::Lerp(
0.82f, 0.98f, ParameterRoll(Hash, 0x27C5u));
}
if (VoxelHash::ToFloat01(Hash3(CellX, CellY, 0x53, Plan.GenerationSeed))
< P.VerticalShaftProbability)
{
Cell.FeatureFlags |= HasVerticalShaftFlag;
}
if (IsGeneratedEdgeOpen(Plan, CellX, CellY, CellX, CellY - 1))
{
Cell.OpenTunnelEdges |= TunnelToNegativeY;
}
if (IsGeneratedEdgeOpen(Plan, CellX, CellY, CellX, CellY + 1))
{
Cell.OpenTunnelEdges |= TunnelToPositiveY;
}
if (IsGeneratedEdgeOpen(Plan, CellX, CellY, CellX - 1, CellY))
{
Cell.OpenTunnelEdges |= TunnelToNegativeX;
}
if (IsGeneratedEdgeOpen(Plan, CellX, CellY, CellX + 1, CellY))
{
Cell.OpenTunnelEdges |= TunnelToPositiveX;
}
Cell.FloorZ = Plan.GenerationFloorZ
+ (ParameterRoll(Hash, 0xF103u) * 2.0f - 1.0f) * P.FloorReliefVoxels;
const float RoomHeight = bLegacyStep2eAuditMode
? P.RoomHeightVoxels
: P.RoomHeightVoxels * FMath::Lerp(
0.82f, 1.12f, ParameterRoll(Hash, 0x55A9u));
Cell.CeilingZ = Cell.FloorZ + RoomHeight;
if (ParameterRoll(Hash, 0x8811u) < P.PillarProbability)
{
Cell.FeatureFlags |= HasPillarFlag;
Cell.PillarX = Cell.CenterX + (ParameterRoll(Hash, 0x1E09u) - 0.5f)
* Cell.RadiusX * 0.30f;
Cell.PillarY = Cell.CenterY + (ParameterRoll(Hash, 0x2F13u) - 0.5f)
* Cell.RadiusY * 0.30f;
Cell.PillarRadius = FMath::Min(
VoxelPassageGeometry::MetersToVoxels(3.5f), Cell.RadiusX * FMath::Lerp(
0.10f, 0.22f, ParameterRoll(Hash, 0x3467u)));
}
if (ParameterRoll(Hash, 0x73BDu) < P.LedgeProbability)
{
Cell.FeatureFlags |= HasLedgeFlag;
Cell.LedgeZ = FMath::Lerp(Cell.FloorZ, Cell.CeilingZ,
FMath::Lerp(0.48f, 0.72f, ParameterRoll(Hash, 0xC579u)));
}
if (ParameterRoll(Hash, 0xE25Fu) < P.FloatingMassProbability)
{
Cell.FeatureFlags |= HasFloatingFlag;
const int32 BlobCount = bLegacyStep2eAuditMode ? 2 : 1;
for (int32 Blob = 0; Blob < BlobCount; ++Blob)
{
const uint32 BlobSalt = 0x1567u + static_cast<uint32>(Blob) * 0x101u;
const float OffsetX = (ParameterRoll(Hash, BlobSalt) - 0.5f) * Cell.RadiusX * 0.60f;
const float OffsetY = (ParameterRoll(Hash, BlobSalt + 1u) - 0.5f) * Cell.RadiusY * 0.60f;
Cell.FloatingX[Blob] = Cell.CenterX + OffsetX;
Cell.FloatingY[Blob] = Cell.CenterY + OffsetY;
Cell.FloatingZ[Blob] = FMath::Lerp(
Cell.FloorZ + P.RoomHeightVoxels * 0.28f,
Cell.CeilingZ - P.RoomHeightVoxels * 0.28f,
ParameterRoll(Hash, BlobSalt + 2u));
Cell.FloatingRadius[Blob] = bLegacyStep2eAuditMode
? Cell.RadiusX * FMath::Lerp(
0.07f, 0.15f, ParameterRoll(Hash, BlobSalt + 3u))
: FMath::Clamp(Cell.RadiusX * FMath::Lerp(
0.16f, 0.28f, ParameterRoll(Hash, BlobSalt + 3u)),
VoxelPassageGeometry::MetersToVoxels(2.0f),
VoxelPassageGeometry::MetersToVoxels(4.5f));
Cell.FloatingRadiusY[Blob] = bLegacyStep2eAuditMode
? Cell.FloatingRadius[Blob]
: Cell.FloatingRadius[Blob] * FMath::Lerp(
0.62f, 1.16f, ParameterRoll(Hash, BlobSalt + 4u));
Cell.FloatingRadiusZ[Blob] = bLegacyStep2eAuditMode
? Cell.FloatingRadius[Blob]
: Cell.FloatingRadius[Blob] * FMath::Lerp(
0.58f, 1.62f, ParameterRoll(Hash, BlobSalt + 5u));
}
}
if (ParameterRoll(Hash, 0xC6A9u) < P.SpireProbability)
{
Cell.FeatureFlags |= HasSpireFlag;
Cell.SpireLength = FMath::Min(
VoxelPassageGeometry::MetersToVoxels(14.0f),
P.RoomHeightVoxels * 0.30f);
Cell.SpireRadius = FMath::Min(
VoxelPassageGeometry::MetersToVoxels(3.5f), Cell.RadiusX * 0.22f);
}
if (ParameterRoll(Hash, 0x4537u) < P.BasinProbability)
{
Cell.FeatureFlags |= HasBasinFlag;
}
return Cell;
}
void PrepareParameterizedAnchors(FStratePlan& Plan)
{
const float AnchorReach = Plan.Generation.RoomPitchVoxels * 1.25f
+ FMath::Max(VoxelPassageGeometry::MetersToVoxels(8.0f),
Plan.Generation.TunnelRadiusVoxels);
for (FLandingSite& Site : Plan.Landings)
{
const FIntPoint LandingCell = GenerationCellAt(
Plan, Site.Position.X, Site.Position.Y);
FGeneratedCaveCell NearestCell;
float BestDistanceSquared = FLT_MAX;
for (int32 CellY = -1; CellY <= 1; ++CellY)
for (int32 CellX = -1; CellX <= 1; ++CellX)
{
const FGeneratedCaveCell Candidate = BuildGeneratedCell(
Plan, LandingCell.X + CellX, LandingCell.Y + CellY);
const float DX = Candidate.CenterX - Site.Position.X;
const float DY = Candidate.CenterY - Site.Position.Y;
const float DistanceSquared = DX * DX + DY * DY;
if (DistanceSquared < BestDistanceSquared)
{
BestDistanceSquared = DistanceSquared;
NearestCell = Candidate;
}
}
Site.GeneratedAnchorStart = FVector(
Site.Position.X, Site.Position.Y,
Site.FloorZ + VoxelPassageGeometry::MetersToVoxels(6.0f));
const FVector AnchorEnd(
NearestCell.CenterX, NearestCell.CenterY,
NearestCell.FloorZ + Plan.Generation.TunnelHalfHeightVoxels);
Site.GeneratedAnchorDelta = AnchorEnd - Site.GeneratedAnchorStart;
Site.GeneratedAnchorLengthSquared = Site.GeneratedAnchorDelta.SizeSquared();
Site.GeneratedAnchorReach = AnchorReach;
Site.bGeneratedAnchorPrepared = true;
}
}
FORCEINLINE float SmoothMinimum(float A, float B, float K)
{
const float H = FMath::Max(K - FMath::Abs(A - B), 0.0f) / K;
return FMath::Min(A, B) - H * H * K * 0.25f;
}
FORCEINLINE float SmoothMaximum(float A, float B, float K)
{
return -SmoothMinimum(-A, -B, K);
}
FORCEINLINE FIntPoint GenerationCellAt(
const FStratePlan& Plan, float X, float Y)
{
float GridX = 0.0f;
float GridY = 0.0f;
WorldToGenerationGrid(Plan, X, Y, GridX, GridY);
return FIntPoint(
FMath::FloorToInt((GridX - Plan.GridOffsetX) / Plan.Generation.RoomPitchVoxels),
FMath::FloorToInt((GridY - Plan.GridOffsetY) / Plan.Generation.RoomPitchVoxels));
}
FORCEINLINE float GeneratedRoomCell(
const FGeneratedCaveCell& Cell,
float X, float Y, float Z, float Radius, float Horizontal,
const FParameterizedColumnContext& Column, int32 CellIndex)
{
const float FloorZ = Column.RoomFloors[CellIndex];
const float Vertical = FMath::Min(Z - FloorZ, Cell.CeilingZ - Z);
const bool bLegacyStep2eAuditMode = IsLegacyStep2eAuditMode();
float Room = SmoothMinimum(Horizontal, Vertical,
VoxelPassageGeometry::MetersToVoxels(2.0f));
const float FeatureWakeDistance = VoxelPassageGeometry::MetersToVoxels(3.5f);
if ((Cell.FeatureFlags & HasPillarFlag) != 0 && Room > -FeatureWakeDistance)
{
const float Pillar = FMath::Max3(
Column.PillarHorizontals[CellIndex],
Cell.FloorZ - Z, Z - Cell.CeilingZ);
if (Pillar < Room)
{
Room = Pillar;
}
}
if ((Cell.FeatureFlags & HasLedgeFlag) != 0)
{
const float Inner = Cell.RadiusX * 0.58f;
const float Outer = Cell.RadiusX
+ VoxelPassageGeometry::MetersToVoxels(0.25f);
const float Ledge = FMath::Max3(
Inner - Radius, Radius - Outer,
FMath::Abs(Z - Cell.LedgeZ)
- VoxelPassageGeometry::MetersToVoxels(0.625f));
if (Ledge < Room)
{
Room = Ledge;
}
}
if ((Cell.FeatureFlags & HasFloatingFlag) != 0)
{
const int32 BlobCount = IsLegacyStep2eAuditMode() ? 2 : 1;
for (int32 Blob = 0; Blob < BlobCount; ++Blob)
{
const float FloatingSupportRadius = IsLegacyStep2eAuditMode()
? Cell.FloatingRadius[Blob]
: FMath::Max3(Cell.FloatingRadius[Blob],
Cell.FloatingRadiusY[Blob], Cell.FloatingRadiusZ[Blob]);
if (Room <= -FloatingSupportRadius)
{
continue;
}
const float BZ = Z - Cell.FloatingZ[Blob];
float BlobSolid = 0.0f;
if (IsLegacyStep2eAuditMode())
{
BlobSolid = FMath::Sqrt(
Column.FloatingXYDistanceSquared[CellIndex][Blob] + BZ * BZ)
- Cell.FloatingRadius[Blob];
}
else
{
const float RadiusX = FMath::Max(Cell.FloatingRadius[Blob],
VoxelPassageGeometry::MetersToVoxels(0.25f));
const float RadiusY = FMath::Max(Cell.FloatingRadiusY[Blob],
VoxelPassageGeometry::MetersToVoxels(0.25f));
const float RadiusZ = FMath::Max(Cell.FloatingRadiusZ[Blob],
VoxelPassageGeometry::MetersToVoxels(0.25f));
const float ScaledLength = FMath::Sqrt(
Column.FloatingScaledLengthXY[CellIndex][Blob]
+ FMath::Square(BZ / RadiusZ));
const float ScaledGradient = FMath::Sqrt(
Column.FloatingScaledGradientXY[CellIndex][Blob]
+ FMath::Square(BZ / FMath::Square(RadiusZ)));
BlobSolid = ScaledGradient > 1.0e-3f
? ScaledLength * (ScaledLength - 1.0f) / ScaledGradient
: -FMath::Min3(RadiusX, RadiusY, RadiusZ);
}
if (BlobSolid < Room)
{
Room = BlobSolid;
}
}
}
if ((Cell.FeatureFlags & HasSpireFlag) != 0 && Room > -FeatureWakeDistance)
{
const float BaseZ = Column.SpireBaseZ[CellIndex];
const float TipZ = Column.SpireTipZ[CellIndex];
if (Z >= BaseZ || BaseZ - Z < Room)
{
const float T = FMath::Clamp(
(Z - BaseZ) / FMath::Max(TipZ - BaseZ, 1.0f), 0.0f, 1.0f);
const float RadiusAtZ = FMath::Lerp(Cell.SpireRadius,
VoxelPassageGeometry::MetersToVoxels(0.3125f), T);
const float Spire = FMath::Max3(
Radius - RadiusAtZ, BaseZ - Z, Z - TipZ);
if (Spire < Room)
{
Room = Spire;
}
}
const float BaseTop = Column.SpireBaseTopZ[CellIndex];
const float TipBottom = Column.SpireTipBottomZ[CellIndex];
if (Z <= BaseTop || Z - BaseTop < Room)
{
const float T = FMath::Clamp(
(BaseTop - Z) / FMath::Max(BaseTop - TipBottom, 1.0f), 0.0f, 1.0f);
const float RadiusAtZ = FMath::Lerp(Cell.SpireRadius,
VoxelPassageGeometry::MetersToVoxels(0.3125f), T);
const float Stalactite = FMath::Max3(
Radius - RadiusAtZ, Z - BaseTop, TipBottom - Z);
if (Stalactite < Room)
{
Room = Stalactite;
}
}
}
return Room;
}
FORCEINLINE FIntPoint GeneratedParent(
const FStratePlan& Plan, int32 CellX, int32 CellY)
{
const int32 AbsX = FMath::Abs(CellX);
const int32 AbsY = FMath::Abs(CellY);
if (AbsX == 0 && AbsY == 0)
{
return FIntPoint(INDEX_NONE, INDEX_NONE);
}
if (AbsX > 0 && AbsY > 0)
{
const uint32 Hash = Hash3(CellX, CellY, 0x7E, Plan.GenerationSeed);
if ((Hash & 1u) != 0)
{
return FIntPoint(CellX - (CellX > 0 ? 1 : -1), CellY);
}
}
if (AbsX > 0)
{
return FIntPoint(CellX - (CellX > 0 ? 1 : -1), CellY);
}
return FIntPoint(CellX, CellY - (CellY > 0 ? 1 : -1));
}
bool IsGeneratedEdgeOpen(
const FStratePlan& Plan, int32 AX, int32 AY, int32 BX, int32 BY)
{
const FIntPoint ParentA = GeneratedParent(Plan, AX, AY);
const FIntPoint ParentB = GeneratedParent(Plan, BX, BY);
if ((ParentA.X == BX && ParentA.Y == BY)
|| (ParentB.X == AX && ParentB.Y == AY))
{
return true;
}
const int32 MinX = FMath::Min(AX, BX);
const int32 MinY = FMath::Min(AY, BY);
const int32 Axis = AX != BX ? 0 : 1;
const uint32 EdgeHash = Hash3(MinX, MinY, Axis ^ 0x2D, Plan.GenerationSeed);
return VoxelHash::ToFloat01(EdgeHash) < Plan.Generation.LoopProbability;
}
FORCEINLINE float GeneratedTunnelEdge(
const FStratePlan& Plan,
const FGeneratedCaveCell& A, const FGeneratedCaveCell& B,
float Z, float T, float Horizontal)
{
const float FloorZ = FMath::Lerp(A.FloorZ, B.FloorZ, T);
if (!IsLegacyStep2eAuditMode())
{
const float MinCellX = static_cast<float>(FMath::Min(A.CellX, B.CellX));
const float MinCellY = static_cast<float>(FMath::Min(A.CellY, B.CellY));
const int32 Axis = A.CellX != B.CellX ? 0 : 1;
const uint32 EdgeHash = Hash3(
static_cast<int32>(MinCellX), static_cast<int32>(MinCellY),
Axis ^ 0x2Du, Plan.GenerationSeed);
const float RadiusSeed = VoxelHash::ToFloat01(EdgeHash ^ 0xA511E9B3u);
const float Phase = VoxelHash::ToFloat01(EdgeHash ^ 0x63D83595u)
* 6.28318530717958647692f;
const float Radius = Plan.Generation.TunnelRadiusVoxels
* (0.82f + 0.36f * RadiusSeed)
* (1.0f + 0.22f * FMath::Sin(6.28318530717958647692f * T + Phase));
const float HalfHeight = Plan.Generation.TunnelHalfHeightVoxels;
const float VerticalWave = VoxelPassageGeometry::MetersToVoxels(1.0f)
* FMath::Sin(3.14159265358979323846f * T)
* FMath::Sin(6.28318530717958647692f * T + Phase);
const float VerticalOffset = Z - (FloorZ + HalfHeight + VerticalWave);
const float HorizontalDistance = FMath::Max(0.0f, Radius - Horizontal);
const float NormalizedDistance = FMath::Sqrt(
FMath::Square(HorizontalDistance / FMath::Max(Radius, 1.0f))
+ FMath::Square(VerticalOffset / FMath::Max(HalfHeight, 1.0f)));
return (1.0f - NormalizedDistance) * FMath::Min(Radius, HalfHeight);
}
const float Vertical = FMath::Min(
Z - FloorZ,
FloorZ + 2.0f * Plan.Generation.TunnelHalfHeightVoxels - Z);
return SmoothMinimum(Horizontal, Vertical,
VoxelPassageGeometry::MetersToVoxels(1.25f));
}
FORCEINLINE float GeneratedTunnelEdgeFromColumnProfile(
float Z, float Horizontal, const FGeneratedTunnelColumnProfile& Profile)
{
const float VerticalOffset = Z - (Profile.FloorZ
+ Profile.HalfHeight + Profile.VerticalWave);
const float HorizontalDistance = FMath::Max(0.0f, Profile.Radius - Horizontal);
const float NormalizedDistance = FMath::Sqrt(
FMath::Square(HorizontalDistance / FMath::Max(Profile.Radius, 1.0f))
+ FMath::Square(VerticalOffset / FMath::Max(Profile.HalfHeight, 1.0f)));
return (1.0f - NormalizedDistance)
* FMath::Min(Profile.Radius, Profile.HalfHeight);
}
const FGeneratedCaveCell* FindParameterizedBrickRoomCell(
const FStratePlan& Plan, const FParameterizedBrickContext* Context,
int32 CellX, int32 CellY)
{
if (Context == nullptr || Context->PlanToken != &Plan
|| Context->RoomCellCacheToken != GParameterizedRoomCellCache.GetData()
|| GParameterizedRoomCellCachePlan != &Plan
|| CellX < Context->RoomCellCacheMin.X
|| CellY < Context->RoomCellCacheMin.Y
|| CellX >= Context->RoomCellCacheMin.X + Context->RoomCellCacheSize.X
|| CellY >= Context->RoomCellCacheMin.Y + Context->RoomCellCacheSize.Y)
{
return nullptr;
}
const int32 Index = (CellY - Context->RoomCellCacheMin.Y)
* Context->RoomCellCacheSize.X + CellX - Context->RoomCellCacheMin.X;
return GParameterizedRoomCellCache.IsValidIndex(Index)
? &GParameterizedRoomCellCache[Index] : nullptr;
}
void BuildParameterizedColumnContext(
const FStratePlan& Plan, float X, float Y,
const FParameterizedBrickContext* Context,
FParameterizedColumnContext& OutColumn)
{
const FIntPoint BaseCell = GenerationCellAt(Plan, X, Y);
const bool bHasRoomCellCache = Context != nullptr
&& Context->PlanToken == &Plan
&& Context->RoomCellCacheToken == GParameterizedRoomCellCache.GetData()
&& GParameterizedRoomCellCachePlan == &Plan;
for (int32 CellY = -1; CellY <= 1; ++CellY)
for (int32 CellX = -1; CellX <= 1; ++CellX)
{
const int32 Index = (CellY + 1) * 3 + CellX + 1;
const int32 GeneratedX = BaseCell.X + CellX;
const int32 GeneratedY = BaseCell.Y + CellY;
if (bHasRoomCellCache)
{
const int32 CacheX = GeneratedX - Context->RoomCellCacheMin.X;
const int32 CacheY = GeneratedY - Context->RoomCellCacheMin.Y;
if (CacheX >= 0 && CacheY >= 0
&& CacheX < Context->RoomCellCacheSize.X
&& CacheY < Context->RoomCellCacheSize.Y)
{
const int32 CacheIndex = CacheY * Context->RoomCellCacheSize.X + CacheX;
if (GParameterizedRoomCellCache.IsValidIndex(CacheIndex))
{
OutColumn.Cells[Index] = &GParameterizedRoomCellCache[CacheIndex];
}
}
}
else
{
OutColumn.Cells[Index] = FindParameterizedBrickRoomCell(
Plan, Context, GeneratedX, GeneratedY);
}
if (OutColumn.Cells[Index] == nullptr)
{
OutColumn.GeneratedCells[Index] = BuildGeneratedCell(
Plan, GeneratedX, GeneratedY);
OutColumn.Cells[Index] = &OutColumn.GeneratedCells[Index];
}
const float DX = X - OutColumn.Cells[Index]->CenterX;
const float DY = Y - OutColumn.Cells[Index]->CenterY;
const float DistanceSquared = DX * DX + DY * DY;
OutColumn.RoomDistanceSquared[Index] = DistanceSquared;
if (IsLegacyStep2eAuditMode())
{
OutColumn.RoomRadii[Index] = FMath::Sqrt(DistanceSquared);
}
else
{
const FGeneratedCaveCell& Cell = *OutColumn.Cells[Index];
const float LocalX = Cell.RoomCos * DX + Cell.RoomSin * DY;
const float LocalY = -Cell.RoomSin * DX + Cell.RoomCos * DY;
const float ScaledY = LocalY * Cell.RadiusX
/ FMath::Max(Cell.RadiusY, 1.0f);
OutColumn.RoomRadii[Index] = FMath::Sqrt(
LocalX * LocalX + ScaledY * ScaledY);
}
OutColumn.RoomHorizontals[Index] = OutColumn.Cells[Index]->RadiusX
- OutColumn.RoomRadii[Index];
const FGeneratedCaveCell& Cell = *OutColumn.Cells[Index];
OutColumn.RoomFloors[Index] = Cell.FloorZ;
if ((Cell.FeatureFlags & HasBasinFlag) != 0)
{
OutColumn.RoomFloors[Index] += Plan.Generation.BasinDepthVoxels
* FMath::Clamp(OutColumn.RoomRadii[Index]
/ FMath::Max(Cell.RadiusX, 1.0f), 0.0f, 1.0f);
}
if ((Cell.FeatureFlags & HasPillarFlag) != 0)
{
const float PX = X - Cell.PillarX;
const float PY = Y - Cell.PillarY;
OutColumn.PillarHorizontals[Index] = FMath::Sqrt(PX * PX + PY * PY)
- Cell.PillarRadius;
}
if ((Cell.FeatureFlags & HasFloatingFlag) != 0)
{
for (int32 Blob = 0; Blob < 2; ++Blob)
{
const float BX = X - Cell.FloatingX[Blob];
const float BY = Y - Cell.FloatingY[Blob];
OutColumn.FloatingXYDistanceSquared[Index][Blob] = BX * BX + BY * BY;
if (!IsLegacyStep2eAuditMode())
{
const float RadiusX = FMath::Max(Cell.FloatingRadius[Blob],
VoxelPassageGeometry::MetersToVoxels(0.25f));
const float RadiusY = FMath::Max(Cell.FloatingRadiusY[Blob],
VoxelPassageGeometry::MetersToVoxels(0.25f));
const float ScaledX = BX / RadiusX;
const float ScaledY = BY / RadiusY;
const float RadiusX2 = FMath::Square(RadiusX);
const float RadiusY2 = FMath::Square(RadiusY);
const float GradientX = BX / RadiusX2;
const float GradientY = BY / RadiusY2;
OutColumn.FloatingScaledLengthXY[Index][Blob] =
FMath::Square(ScaledX) + FMath::Square(ScaledY);
OutColumn.FloatingScaledGradientXY[Index][Blob] =
FMath::Square(GradientX) + FMath::Square(GradientY);
}
}
}
if ((Cell.FeatureFlags & HasSpireFlag) != 0)
{
OutColumn.SpireBaseZ[Index] = Cell.FloorZ
- VoxelPassageGeometry::MetersToVoxels(0.5f);
OutColumn.SpireTipZ[Index] = FMath::Min(
Cell.CeilingZ - VoxelPassageGeometry::MetersToVoxels(0.5f),
OutColumn.SpireBaseZ[Index] + Cell.SpireLength);
OutColumn.SpireBaseTopZ[Index] = Cell.CeilingZ
+ VoxelPassageGeometry::MetersToVoxels(0.5f);
OutColumn.SpireTipBottomZ[Index] = FMath::Max(
Cell.FloorZ - VoxelPassageGeometry::MetersToVoxels(0.5f),
OutColumn.SpireBaseTopZ[Index] - Cell.SpireLength);
}
}
for (int32 CellY = -1; CellY <= 1; ++CellY)
for (int32 CellX = -1; CellX <= 1; ++CellX)
{
const int32 TunnelIndex = (CellY + 2) * 5 + CellX + 2;
const int32 RoomIndex = (CellY + 1) * 3 + CellX + 1;
OutColumn.TunnelCells[TunnelIndex] = OutColumn.Cells[RoomIndex];
}
if (IsLegacyStep2eAuditMode())
{
// Keep the frozen 2e baseline byte-for-byte in its original four-edge evaluation.
const FGeneratedCaveCell& Centre = *OutColumn.Cells[4];
const int32 EdgeIndices[] = {1, 7, 3, 5};
const int32 TunnelCellIndices[] = {7, 17, 11, 13};
const uint8 EdgeFlags[] = {
TunnelToNegativeY, TunnelToPositiveY,
TunnelToNegativeX, TunnelToPositiveX};
for (int32 EdgeIndex = 0; EdgeIndex < UE_ARRAY_COUNT(EdgeIndices); ++EdgeIndex)
{
if ((Centre.OpenTunnelEdges & EdgeFlags[EdgeIndex]) == 0)
{
continue;
}
const FGeneratedCaveCell& Next = *OutColumn.Cells[EdgeIndices[EdgeIndex]];
const float ABX = Next.CenterX - Centre.CenterX;
const float ABY = Next.CenterY - Centre.CenterY;
const float LengthSquared = ABX * ABX + ABY * ABY;
const float T = LengthSquared > 1.0e-8f
? FMath::Clamp(((X - Centre.CenterX) * ABX
+ (Y - Centre.CenterY) * ABY) / LengthSquared, 0.0f, 1.0f)
: 0.0f;
const float DX = X - (Centre.CenterX + T * ABX);
const float DY = Y - (Centre.CenterY + T * ABY);
OutColumn.TunnelEdgeCellA[OutColumn.TunnelEdgeCount] = 12;
OutColumn.TunnelEdgeCellB[OutColumn.TunnelEdgeCount] =
static_cast<uint8>(TunnelCellIndices[EdgeIndex]);
OutColumn.TunnelT[OutColumn.TunnelEdgeCount] = T;
OutColumn.TunnelHorizontal[OutColumn.TunnelEdgeCount] =
Plan.Generation.TunnelRadiusVoxels - FMath::Sqrt(DX * DX + DY * DY);
++OutColumn.TunnelEdgeCount;
}
}
else
{
const auto CacheTunnelEdge = [&](int32 AX, int32 AY, int32 BX, int32 BY)
{
const uint8 CellAIndex = static_cast<uint8>((AY + 2) * 5 + AX + 2);
const uint8 CellBIndex = static_cast<uint8>((BY + 2) * 5 + BX + 2);
const FGeneratedCaveCell& A = *OutColumn.TunnelCells[CellAIndex];
const FGeneratedCaveCell& B = *OutColumn.TunnelCells[CellBIndex];
const uint8 OpenEdgeFlag = AX != BX
? TunnelToPositiveX : TunnelToPositiveY;
if ((A.OpenTunnelEdges & OpenEdgeFlag) == 0)
{
return;
}
// Beyond this horizontal reach, even the largest supported detail and tunnel
// smooth-union rise cannot turn rock into air. Reject before calculating the
// point-to-segment distance.
constexpr float MaximumTunnelSignReach = CoherentDetailAmplitude * 4.0f
+ VoxelPassageGeometry::MetersToVoxels(0.3125f)
+ VoxelPassageGeometry::MetersToVoxels(0.5f);
const float Reach = Plan.Generation.TunnelRadiusVoxels
* 1.45f + Plan.Generation.RoomPitchVoxels * 0.16f
+ MaximumTunnelSignReach;
const float MinX = FMath::Min(A.CenterX, B.CenterX);
const float MaxX = FMath::Max(A.CenterX, B.CenterX);
const float MinY = FMath::Min(A.CenterY, B.CenterY);
const float MaxY = FMath::Max(A.CenterY, B.CenterY);
const float GapX = X < MinX ? MinX - X : (X > MaxX ? X - MaxX : 0.0f);
const float GapY = Y < MinY ? MinY - Y : (Y > MaxY ? Y - MaxY : 0.0f);
if (GapX * GapX + GapY * GapY >= Reach * Reach)
{
return;
}
check(OutColumn.TunnelEdgeCount < UE_ARRAY_COUNT(OutColumn.TunnelT));
const float ABX = B.CenterX - A.CenterX;
const float ABY = B.CenterY - A.CenterY;
const float LengthSquared = ABX * ABX + ABY * ABY;
float T = LengthSquared > 1.0e-8f
? FMath::Clamp(((X - A.CenterX) * ABX
+ (Y - A.CenterY) * ABY) / LengthSquared, 0.0f, 1.0f)
: 0.0f;
float Horizontal = 0.0f;
FGeneratedTunnelColumnProfile TunnelProfile;
if (IsLegacyStep2eAuditMode())
{
const float DX = X - (A.CenterX + T * ABX);
const float DY = Y - (A.CenterY + T * ABY);
Horizontal = Plan.Generation.TunnelRadiusVoxels
- FMath::Sqrt(DX * DX + DY * DY);
}
else
{
const float MinCellX = static_cast<float>(FMath::Min(A.CellX, B.CellX));
const float MinCellY = static_cast<float>(FMath::Min(A.CellY, B.CellY));
const int32 Axis = A.CellX != B.CellX ? 0 : 1;
const uint32 EdgeHash = Hash3(
static_cast<int32>(MinCellX), static_cast<int32>(MinCellY),
Axis ^ 0x2Du, Plan.GenerationSeed);
const float RadiusSeed = VoxelHash::ToFloat01(EdgeHash ^ 0xA511E9B3u);
const float Phase = VoxelHash::ToFloat01(EdgeHash ^ 0x63D83595u)
* 6.28318530717958647692f;
const float BendA = (VoxelHash::ToFloat01(EdgeHash ^ 0xB5297A4Du)
* 2.0f - 1.0f) * Plan.Generation.RoomPitchVoxels * 0.16f;
const float BendB = (VoxelHash::ToFloat01(EdgeHash ^ 0x68E31DA4u)
* 2.0f - 1.0f) * Plan.Generation.RoomPitchVoxels * 0.16f;
const float Length = FMath::Sqrt(LengthSquared);
const float NX = Length > 1.0e-4f ? -ABY / Length : 0.0f;
const float NY = Length > 1.0e-4f ? ABX / Length : 0.0f;
const float C1X = A.CenterX + ABX / 3.0f + NX * BendA;
const float C1Y = A.CenterY + ABY / 3.0f + NY * BendA;
const float C2X = A.CenterX + ABX * (2.0f / 3.0f) + NX * BendB;
const float C2Y = A.CenterY + ABY * (2.0f / 3.0f) + NY * BendB;
// Refine the straight-edge projection against the cubic centerline. Four
// deterministic Newton steps are sufficient at the authored bend limit.
for (int32 Iteration = 0; Iteration < 4; ++Iteration)
{
const float U = T;
const float OneMinusU = 1.0f - U;
const float PX = OneMinusU * OneMinusU * OneMinusU * A.CenterX
+ 3.0f * OneMinusU * OneMinusU * U * C1X
+ 3.0f * OneMinusU * U * U * C2X + U * U * U * B.CenterX;
const float PY = OneMinusU * OneMinusU * OneMinusU * A.CenterY
+ 3.0f * OneMinusU * OneMinusU * U * C1Y
+ 3.0f * OneMinusU * U * U * C2Y + U * U * U * B.CenterY;
const float DX = 3.0f * OneMinusU * OneMinusU * (C1X - A.CenterX)
+ 6.0f * OneMinusU * U * (C2X - C1X)
+ 3.0f * U * U * (B.CenterX - C2X);
const float DY = 3.0f * OneMinusU * OneMinusU * (C1Y - A.CenterY)
+ 6.0f * OneMinusU * U * (C2Y - C1Y)
+ 3.0f * U * U * (B.CenterY - C2Y);
const float DDX = 6.0f * OneMinusU * (C2X - 2.0f * C1X + A.CenterX)
+ 6.0f * U * (B.CenterX - 2.0f * C2X + C1X);
const float DDY = 6.0f * OneMinusU * (C2Y - 2.0f * C1Y + A.CenterY)
+ 6.0f * U * (B.CenterY - 2.0f * C2Y + C1Y);
const float Denominator = DX * DX + DY * DY
+ (PX - X) * DDX + (PY - Y) * DDY;
if (FMath::Abs(Denominator) > 1.0e-4f)
{
T = FMath::Clamp(U - ((PX - X) * DX + (PY - Y) * DY)
/ Denominator, 0.0f, 1.0f);
}
}
const float OneMinusT = 1.0f - T;
const float PX = OneMinusT * OneMinusT * OneMinusT * A.CenterX
+ 3.0f * OneMinusT * OneMinusT * T * C1X
+ 3.0f * OneMinusT * T * T * C2X + T * T * T * B.CenterX;
const float PY = OneMinusT * OneMinusT * OneMinusT * A.CenterY
+ 3.0f * OneMinusT * OneMinusT * T * C1Y
+ 3.0f * OneMinusT * T * T * C2Y + T * T * T * B.CenterY;
const float DX = X - PX;
const float DY = Y - PY;
const float Distance = FMath::Sqrt(DX * DX + DY * DY);
const float Radius = Plan.Generation.TunnelRadiusVoxels
* (0.82f + 0.36f * RadiusSeed)
* (1.0f + 0.22f * FMath::Sin(6.28318530717958647692f * T + Phase));
Horizontal = Radius - Distance;
TunnelProfile.FloorZ = FMath::Lerp(A.FloorZ, B.FloorZ, T);
TunnelProfile.Radius = Radius;
TunnelProfile.HalfHeight = Plan.Generation.TunnelHalfHeightVoxels;
TunnelProfile.VerticalWave = VoxelPassageGeometry::MetersToVoxels(1.0f)
* FMath::Sin(3.14159265358979323846f * T)
* FMath::Sin(6.28318530717958647692f * T + Phase);
}
int32 InsertAt = OutColumn.TunnelEdgeCount;
while (InsertAt > 0
&& OutColumn.TunnelHorizontal[InsertAt - 1] < Horizontal)
{
OutColumn.TunnelEdgeCellA[InsertAt] =
OutColumn.TunnelEdgeCellA[InsertAt - 1];
OutColumn.TunnelEdgeCellB[InsertAt] =
OutColumn.TunnelEdgeCellB[InsertAt - 1];
OutColumn.TunnelT[InsertAt] = OutColumn.TunnelT[InsertAt - 1];
OutColumn.TunnelHorizontal[InsertAt] =
OutColumn.TunnelHorizontal[InsertAt - 1];
OutColumn.TunnelProfiles[InsertAt] =
OutColumn.TunnelProfiles[InsertAt - 1];
--InsertAt;
}
OutColumn.TunnelEdgeCellA[InsertAt] = CellAIndex;
OutColumn.TunnelEdgeCellB[InsertAt] = CellBIndex;
OutColumn.TunnelT[InsertAt] = T;
OutColumn.TunnelHorizontal[InsertAt] = Horizontal;
OutColumn.TunnelProfiles[InsertAt] = TunnelProfile;
++OutColumn.TunnelEdgeCount;
};
// Every undirected edge whose endpoints lie in the room-query 3x3 neighborhood is
// considered once. This retains the old base cell's edges when the query base shifts.
for (int32 CellY = -1; CellY <= 1; ++CellY)
for (int32 CellX = -1; CellX < 1; ++CellX)
{
CacheTunnelEdge(CellX, CellY, CellX + 1, CellY);
}
for (int32 CellX = -1; CellX <= 1; ++CellX)
for (int32 CellY = -1; CellY < 1; ++CellY)
{
CacheTunnelEdge(CellX, CellY, CellX, CellY + 1);
}
}
OutColumn.bHasLandingXYCache = Plan.Landings.Num() <= UE_ARRAY_COUNT(
OutColumn.LandingDistanceSquared);
if (OutColumn.bHasLandingXYCache)
{
const FLandingSite* Hub = Plan.LandingHubIndex != INDEX_NONE
? &Plan.Landings[Plan.LandingHubIndex] : nullptr;
for (int32 SiteIndex = 0; SiteIndex < Plan.Landings.Num(); ++SiteIndex)
{
const FLandingSite& Site = Plan.Landings[SiteIndex];
const float DX = X - Site.Position.X;
const float DY = Y - Site.Position.Y;
const float DistanceSquared = DX * DX + DY * DY;
OutColumn.LandingDistanceSquared[SiteIndex] = DistanceSquared;
OutColumn.LandingDistance[SiteIndex] = FMath::Sqrt(DistanceSquared);
OutColumn.LandingBridgeT[SiteIndex] = 0.0f;
OutColumn.LandingBridgeHorizontal[SiteIndex] = -FLT_MAX;
if (Hub != nullptr && Plan.Landings.Num() > 1
&& SiteIndex != Plan.LandingHubIndex)
{
OutColumn.LandingBridgeT[SiteIndex] = SegmentParameterXY(
X, Y, Site.Position, Hub->Position);
OutColumn.LandingBridgeHorizontal[SiteIndex] = LandingBridgeRadius
- FMath::Sqrt(DistanceSquaredToSegmentXY(
X, Y, Site.Position, Hub->Position));
}
}
}
}
float EvaluateParameterizedMacro(
const FStratePlan& Plan, float X, float Y, float Z, float* OutDetailScale,
const FParameterizedBrickContext* Context = nullptr,
const FParameterizedColumnContext* ColumnContext = nullptr)
{
FParameterizedColumnContext LocalColumn;
if (ColumnContext == nullptr)
{
BuildParameterizedColumnContext(Plan, X, Y, Context, LocalColumn);
ColumnContext = &LocalColumn;
}
float Macro = -FLT_MAX;
const float Bottom = static_cast<float>(Plan.MinChunkZ * CHUNK_SIZE)
+ VoxelPassageGeometry::MetersToVoxels(2.0f);
const float Top = static_cast<float>(Plan.MaxChunkZExclusive * CHUNK_SIZE)
- VoxelPassageGeometry::MetersToVoxels(2.0f);
static constexpr int32 CellEvaluationOrder[] = {4, 1, 3, 5, 7, 0, 2, 6, 8};
for (int32 OrderIndex = 0;
OrderIndex < UE_ARRAY_COUNT(CellEvaluationOrder); ++OrderIndex)
{
const int32 Index = CellEvaluationOrder[OrderIndex];
const FGeneratedCaveCell& Cell = *ColumnContext->Cells[Index];
const float DistanceSquared = ColumnContext->RoomDistanceSquared[Index];
if (Macro > -FLT_MAX * 0.5f)
{
const float MaximumDistance = Cell.RadiusX - Macro
+ VoxelPassageGeometry::MetersToVoxels(1.5f);
if (MaximumDistance < 0.0f
|| DistanceSquared > FMath::Square(MaximumDistance + 1.0e-3f))
{
continue;
}
const float VerticalUpperBound = FMath::Min(
Z - ColumnContext->RoomFloors[Index], Cell.CeilingZ - Z);
if (VerticalUpperBound <= Macro
- VoxelPassageGeometry::MetersToVoxels(1.5f))
{
continue;
}
}
Macro = SmoothMaximum(Macro,
GeneratedRoomCell(Cell, X, Y, Z,
ColumnContext->RoomRadii[Index],
ColumnContext->RoomHorizontals[Index],
*ColumnContext, Index), VoxelPassageGeometry::MetersToVoxels(1.5f));
}
const bool bLegacyStep2eAuditMode = IsLegacyStep2eAuditMode();
if (bLegacyStep2eAuditMode)
{
for (int32 EdgeIndex = 0; EdgeIndex < ColumnContext->TunnelEdgeCount; ++EdgeIndex)
{
const FGeneratedCaveCell& A = *ColumnContext->TunnelCells[
ColumnContext->TunnelEdgeCellA[EdgeIndex]];
const FGeneratedCaveCell& B = *ColumnContext->TunnelCells[
ColumnContext->TunnelEdgeCellB[EdgeIndex]];
Macro = SmoothMaximum(Macro, GeneratedTunnelEdge(
Plan, A, B, Z, ColumnContext->TunnelT[EdgeIndex],
ColumnContext->TunnelHorizontal[EdgeIndex]),
VoxelPassageGeometry::MetersToVoxels(1.25f));
}
}
else
{
// Far inside solid rock, density magnitude is only a proof margin. Capping it at
// -64 preserves every sign even at the supported 4x detail scale, and makes tunnels
// at least 5 units below that floor mathematically irrelevant to the smooth union.
Macro = FMath::Max(Macro, -VoxelPassageGeometry::MetersToVoxels(16.0f));
const float MinimumTunnelFloor =
Plan.GenerationFloorZ - Plan.Generation.FloorReliefVoxels;
const float MaximumTunnelFloor =
Plan.GenerationFloorZ + Plan.Generation.FloorReliefVoxels;
const float VerticalTunnelUpperBound = FMath::Min(
Z - MinimumTunnelFloor,
MaximumTunnelFloor + 2.0f * Plan.Generation.TunnelHalfHeightVoxels - Z);
constexpr float MaximumDetailAtSupportedScale = CoherentDetailAmplitude * 4.0f;
constexpr float MaximumSmoothUnionRise =
VoxelPassageGeometry::MetersToVoxels(0.3125f);
constexpr float SolidTunnelMargin =
MaximumDetailAtSupportedScale + MaximumSmoothUnionRise
+ VoxelPassageGeometry::MetersToVoxels(0.5f);
const float TunnelSmoothRadius = VoxelPassageGeometry::MetersToVoxels(1.25f);
const bool bNoTunnelCanChangeMacro =
VerticalTunnelUpperBound <= Macro - TunnelSmoothRadius;
const bool bNoTunnelCanChangeSign = Macro <= -SolidTunnelMargin
&& VerticalTunnelUpperBound <= -SolidTunnelMargin;
float TunnelMacro = -FLT_MAX;
if (!bNoTunnelCanChangeMacro && !bNoTunnelCanChangeSign)
{
for (int32 EdgeIndex = 0;
EdgeIndex < ColumnContext->TunnelEdgeCount; ++EdgeIndex)
{
const float Horizontal = ColumnContext->TunnelHorizontal[EdgeIndex];
if (Horizontal <= Macro - TunnelSmoothRadius
|| Horizontal <= TunnelMacro)
{
// Remaining tunnel SDFs cannot change either the selected maximum or the
// room/tunnel smooth union; edges are ordered by descending horizontal bound.
break;
}
const FGeneratedCaveCell& A = *ColumnContext->TunnelCells[
ColumnContext->TunnelEdgeCellA[EdgeIndex]];
const FGeneratedCaveCell& B = *ColumnContext->TunnelCells[
ColumnContext->TunnelEdgeCellB[EdgeIndex]];
const float EdgeDensity = bLegacyStep2eAuditMode
? GeneratedTunnelEdge(Plan, A, B, Z,
ColumnContext->TunnelT[EdgeIndex], Horizontal)
: GeneratedTunnelEdgeFromColumnProfile(
Z, Horizontal, ColumnContext->TunnelProfiles[EdgeIndex]);
TunnelMacro = FMath::Max(TunnelMacro, EdgeDensity);
}
}
if (TunnelMacro > -FLT_MAX * 0.5f)
{
Macro = SmoothMaximum(Macro, TunnelMacro, TunnelSmoothRadius);
}
}
for (int32 Index = 0; Index < UE_ARRAY_COUNT(ColumnContext->Cells); ++Index)
{
const FGeneratedCaveCell& Cell = *ColumnContext->Cells[Index];
if ((Cell.FeatureFlags & HasVerticalShaftFlag) == 0)
{
continue;
}
const float DistanceSquared = ColumnContext->RoomDistanceSquared[Index];
const float ShaftSmoothRadius = VoxelPassageGeometry::MetersToVoxels(1.25f);
const float MaximumDistance =
Plan.Generation.ShaftRadiusVoxels - Macro + ShaftSmoothRadius;
if (MaximumDistance < 0.0f
|| DistanceSquared > FMath::Square(MaximumDistance + 1.0e-3f))
{
continue;
}
const float Horizontal = Plan.Generation.ShaftRadiusVoxels
- ColumnContext->RoomRadii[Index];
const float ShaftInset = IsLegacyStep2eAuditMode()
? 0.0f : FMath::Min(
VoxelPassageGeometry::MetersToVoxels(12.0f),
(Top - Bottom) * 0.20f);
const float Vertical = FMath::Min(
Z - (Bottom + ShaftInset), (Top - ShaftInset) - Z);
Macro = SmoothMaximum(Macro,
SmoothMinimum(Horizontal, Vertical, ShaftSmoothRadius), ShaftSmoothRadius);
}
float DetailFade = 1.0f;
for (int32 SiteIndex = 0; SiteIndex < Plan.Landings.Num(); ++SiteIndex)
{
const FLandingSite& Site = Plan.Landings[SiteIndex];
float DistanceSquared = 0.0f;
float Distance = 0.0f;
if (ColumnContext->bHasLandingXYCache)
{
DistanceSquared = ColumnContext->LandingDistanceSquared[SiteIndex];
Distance = ColumnContext->LandingDistance[SiteIndex];
}
else
{
const float DX = X - Site.Position.X;
const float DY = Y - Site.Position.Y;
DistanceSquared = DX * DX + DY * DY;
Distance = FMath::Sqrt(DistanceSquared);
}
const float RoomAir = FMath::Min(
Site.PadRadius - Distance,
FMath::Min(Z - Site.FloorZ, Site.FloorZ + MinimumCaveHeight - Z));
Macro = SmoothMaximum(Macro, RoomAir,
VoxelPassageGeometry::MetersToVoxels(1.5f));
if (Distance < FMath::Max(0.0f, Site.PadRadius
- VoxelPassageGeometry::MetersToVoxels(0.75f)))
{
DetailFade = 0.0f;
}
if (!Site.bGeneratedAnchorPrepared
|| DistanceSquared > FMath::Square(Site.GeneratedAnchorReach))
{
continue;
}
const FVector Point(X, Y, Z);
const float T = Site.GeneratedAnchorLengthSquared > 1.0e-8f
? FMath::Clamp(FVector::DotProduct(
Point - Site.GeneratedAnchorStart, Site.GeneratedAnchorDelta)
/ Site.GeneratedAnchorLengthSquared, 0.0f, 1.0f) : 0.0f;
const FVector AnchorDelta = Point
- (Site.GeneratedAnchorStart + Site.GeneratedAnchorDelta * T);
const float Anchor = FMath::Max(
VoxelPassageGeometry::MetersToVoxels(8.0f),
Plan.Generation.TunnelRadiusVoxels)
- AnchorDelta.Size();
Macro = SmoothMaximum(Macro, Anchor,
VoxelPassageGeometry::MetersToVoxels(1.25f));
}
if (Plan.LandingHubIndex != INDEX_NONE && Plan.Landings.Num() > 1)
{
const FLandingSite& Hub = Plan.Landings[Plan.LandingHubIndex];
for (int32 SiteIndex = 0; SiteIndex < Plan.Landings.Num(); ++SiteIndex)
{
if (SiteIndex == Plan.LandingHubIndex)
{
continue;
}
const FLandingSite& Site = Plan.Landings[SiteIndex];
const float Horizontal = ColumnContext->bHasLandingXYCache
? ColumnContext->LandingBridgeHorizontal[SiteIndex]
: LandingBridgeRadius - FMath::Sqrt(
DistanceSquaredToSegmentXY(X, Y, Site.Position, Hub.Position));
const float T = ColumnContext->bHasLandingXYCache
? ColumnContext->LandingBridgeT[SiteIndex]
: SegmentParameterXY(X, Y, Site.Position, Hub.Position);
const float FloorZ = FMath::Lerp(Site.FloorZ, Hub.FloorZ, T);
const float Vertical = FMath::Min(
Z - FloorZ, FloorZ + MinimumCaveHeight - Z);
Macro = SmoothMaximum(Macro,
SmoothMinimum(Horizontal, Vertical,
VoxelPassageGeometry::MetersToVoxels(1.25f)),
VoxelPassageGeometry::MetersToVoxels(1.25f));
}
}
if (!IsLegacyStep2eAuditMode())
{
// Keep a four-meter roof and floor through rooms, shafts, and authored passage
// modifiers. The same cap is applied again after passage edits in EvaluateInternal.
constexpr float StrateSealThickness =
VoxelPassageGeometry::MetersToVoxels(4.0f);
const float StrateBottom = static_cast<float>(Plan.MinChunkZ * CHUNK_SIZE);
const float StrateTop = static_cast<float>(Plan.MaxChunkZExclusive * CHUNK_SIZE);
const float BoundaryLimit = FMath::Min(
Z - StrateBottom - StrateSealThickness,
StrateTop - StrateSealThickness - Z);
Macro = FMath::Min(Macro, BoundaryLimit);
// A 2.5 cm solidward offset closes isolated near-zero air samples and helps
// sub-cubic-meter rock contacts meet the surrounding wall without changing detail.
Macro -= CoherentRockDensityBias;
}
if (OutDetailScale != nullptr)
{
*OutDetailScale = DetailFade;
}
return Macro;
}
const FStratePlan* FindStrate(const FIntVector& Chunk)
{
for (const FStratePlan& Plan : GPlanCache.Strates)
{
if (Chunk.Z >= Plan.MinChunkZ && Chunk.Z < Plan.MaxChunkZExclusive)
{
return &Plan;
}
}
return nullptr;
}
float EvaluateMacro(const FStratePlan& Plan, float X, float Y, float Z,
float* OutDetailScale)
{
float WeightedDelta = 0.0f;
float TotalWeight = 0.0f;
float LandingMacro = -FLT_MAX;
float Scale = 1.0f;
for (const FLandingSite& Site : Plan.Landings)
{
const float DX = X - Site.Position.X;
const float DY = Y - Site.Position.Y;
const float DistanceSquared = DX * DX + DY * DY;
const float HorizontalDistance = FMath::Sqrt(DistanceSquared);
if (DistanceSquared < Site.BlendRadius * Site.BlendRadius)
{
const float Weight = Fade(1.0f - HorizontalDistance / Site.BlendRadius);
WeightedDelta += Weight * (Site.FloorZ - Plan.FloorZ);
TotalWeight += Weight;
}
const float RoomVertical = FMath::Min(
Z - Site.FloorZ, Site.FloorZ + MinimumCaveHeight - Z);
const float RoomAir = FMath::Min(Site.PadRadius - HorizontalDistance, RoomVertical);
LandingMacro = FMath::Max(LandingMacro, RoomAir);
if (OutDetailScale != nullptr)
{
const float FlatRadius = FMath::Max(0.0f, Site.PadRadius
- VoxelPassageGeometry::MetersToVoxels(0.75f));
if (DistanceSquared < FlatRadius * FlatRadius)
{
Scale = 0.0f;
}
}
}
const float Floor = Plan.FloorZ + WeightedDelta / FMath::Max(1.0f, TotalWeight);
const float Ceiling = Floor + (Plan.CeilingZ - Plan.FloorZ);
const float VerticalAir = FMath::Min(Z - Floor, Ceiling - Z);
const float XLaneDistance = DistanceToPeriodicLine(
X, Plan.CellSpacing, Plan.OffsetX);
const float YLaneDistance = DistanceToPeriodicLine(
Y, Plan.CellSpacing, Plan.OffsetY);
const float LaneDistance = FMath::Min(XLaneDistance, YLaneDistance);
const float GridAir = Plan.CorridorHalfWidth - LaneDistance;
float Macro = FMath::Max(
FMath::Min(VerticalAir, GridAir), LandingMacro);
if (Plan.Archetype == ECaveGeneratorType::VerticalShafts)
{
Macro = FMath::Max(Macro, VerticalAir);
Macro = FMath::Max(Macro, EvaluateVerticalShaftMacro(Plan, X, Y, Z));
}
for (const FLandingSite& Site : Plan.Landings)
{
const float ApproachHorizontal = LandingApproachRadius - FMath::Sqrt(
DistanceSquaredToSegmentXY(X, Y, Site.Position, Site.ApproachEnd));
Macro = FMath::Max(Macro, FMath::Min(ApproachHorizontal, VerticalAir));
}
if (Plan.LandingHubIndex != INDEX_NONE && Plan.Landings.Num() > 1)
{
const FLandingSite& Hub = Plan.Landings[Plan.LandingHubIndex];
for (int32 SiteIndex = 0; SiteIndex < Plan.Landings.Num(); ++SiteIndex)
{
if (SiteIndex == Plan.LandingHubIndex)
{
continue;
}
const FLandingSite& Site = Plan.Landings[SiteIndex];
const float DistanceSquared = DistanceSquaredToSegmentXY(
X, Y, Site.Position, Hub.Position);
const float Horizontal = LandingBridgeRadius - FMath::Sqrt(DistanceSquared);
const float T = SegmentParameterXY(X, Y, Site.Position, Hub.Position);
const float BridgeFloor = FMath::Lerp(Site.FloorZ, Hub.FloorZ, T);
Macro = FMath::Max(Macro, FMath::Min(
Horizontal,
FMath::Min(Z - BridgeFloor, BridgeFloor + MinimumCaveHeight - Z)));
}
}
if (OutDetailScale != nullptr)
{
*OutDetailScale = Scale;
}
return Macro;
}
FORCEINLINE float EvaluateMacroFromBrick(
const FStratePlan& Plan, const FBrickEvaluationContext& Context,
float X, float Y, float Z, float* OutDetailScale)
{
const int32 LocalX = static_cast<int32>(X) - Context.BrickOrigin.X;
const int32 LocalY = static_cast<int32>(Y) - Context.BrickOrigin.Y;
const int32 XYIndex = LocalY * Context.BrickSize.X + LocalX;
const float Floor = Context.FloorByXY[XYIndex];
const float Ceiling = Floor + (Plan.CeilingZ - Plan.FloorZ);
const float VerticalAir = FMath::Min(Z - Floor, Ceiling - Z);
float Macro = FMath::Min(VerticalAir, Context.GridAirByXY[XYIndex]);
for (int32 SiteIndex = 0; SiteIndex < Plan.Landings.Num(); ++SiteIndex)
{
const FLandingSite& Site = Plan.Landings[SiteIndex];
const float HorizontalRoom = Context.LandingHorizontalByXY[
SiteIndex * Context.BrickSize.X * Context.BrickSize.Y + XYIndex];
const float RoomVertical = FMath::Min(
Z - Site.FloorZ, Site.FloorZ + MinimumCaveHeight - Z);
Macro = FMath::Max(Macro, FMath::Min(HorizontalRoom, RoomVertical));
}
const int32 XYCount = Context.BrickSize.X * Context.BrickSize.Y;
for (int32 SiteIndex = 0; SiteIndex < Plan.Landings.Num(); ++SiteIndex)
{
const float ApproachHorizontal = Context.LandingApproachByXY[
SiteIndex * XYCount + XYIndex];
Macro = FMath::Max(Macro, FMath::Min(ApproachHorizontal, VerticalAir));
}
if (Plan.LandingHubIndex != INDEX_NONE && Plan.Landings.Num() > 1)
{
for (int32 SiteIndex = 0; SiteIndex < Plan.Landings.Num(); ++SiteIndex)
{
if (SiteIndex == Plan.LandingHubIndex)
{
continue;
}
const int32 BridgeIndex = SiteIndex * XYCount + XYIndex;
const float BridgeFloor = Context.LandingBridgeFloorByXY[BridgeIndex];
Macro = FMath::Max(Macro, FMath::Min(
Context.LandingBridgeHorizontalByXY[BridgeIndex],
FMath::Min(Z - BridgeFloor,
BridgeFloor + MinimumCaveHeight - Z)));
}
}
if (Plan.Archetype == ECaveGeneratorType::VerticalShafts)
{
Macro = FMath::Max(Macro, VerticalAir);
Macro = FMath::Max(Macro, EvaluateVerticalShaftMacro(Plan, X, Y, Z));
}
if (OutDetailScale != nullptr)
{
*OutDetailScale = Context.DetailFadeByXY[XYIndex];
}
return Macro;
}
FORCEINLINE float DistanceSquaredToSegment(
const FVector& Point, const FVector& A, const FVector& B)
{
const FVector AB = B - A;
const float LengthSquared = AB.SizeSquared();
const float T = LengthSquared > SMALL_NUMBER
? FMath::Clamp(FVector::DotProduct(Point - A, AB) / LengthSquared, 0.0f, 1.0f)
: 0.0f;
return FVector::DistSquared(Point, A + AB * T);
}
FORCEINLINE bool BoxMayIntersectPassageBound(const FBox& Box, const FPassageBound& Bounds)
{
if (!(Bounds.Min.X <= Box.Max.X && Bounds.Max.X >= Box.Min.X
&& Bounds.Min.Y <= Box.Max.Y && Bounds.Max.Y >= Box.Min.Y
&& Bounds.Min.Z <= Box.Max.Z && Bounds.Max.Z >= Box.Min.Z))
{
return false;
}
if (!Bounds.bIsSegment)
{
return true;
}
const float Reach = Bounds.SupportRadius + Box.GetExtent().Size();
return DistanceSquaredToSegment(Box.GetCenter(), Bounds.SegmentA, Bounds.SegmentB)
<= Reach * Reach;
}
FORCEINLINE bool PointMayIntersectPassageBound(
const FVector& Point, const FPassageBound& Bounds)
{
if (Point.X < Bounds.Min.X || Point.X > Bounds.Max.X
|| Point.Y < Bounds.Min.Y || Point.Y > Bounds.Max.Y
|| Point.Z < Bounds.Min.Z || Point.Z > Bounds.Max.Z)
{
return false;
}
return !Bounds.bIsSegment
|| DistanceSquaredToSegment(Point, Bounds.SegmentA, Bounds.SegmentB)
<= FMath::Square(Bounds.SupportRadius);
}
bool IntersectsPassageBounds(const FBox& Box)
{
for (const FPassageBound& Bounds : GPlanCache.PassageBounds)
{
if (BoxMayIntersectPassageBound(Box, Bounds))
{
return true;
}
}
return false;
}
void FindPassageCandidates(const FBox& Box, TArray<int32>& OutCandidates)
{
OutCandidates.Reset();
for (int32 Index = 0; Index < GPlanCache.PassageBounds.Num(); ++Index)
{
const FPassageBound& Bounds = GPlanCache.PassageBounds[Index];
if (BoxMayIntersectPassageBound(Box, Bounds))
{
OutCandidates.Add(Index);
}
}
}
}
bool IsEnabled()
{
return CVarResearchRedefinedDensityField.GetValueOnAnyThread() != 0;
}
void SetEnabled(bool bEnabled)
{
CVarResearchRedefinedDensityField->Set(bEnabled ? 1 : 0, ECVF_SetByCode);
}
float GetDetailScale()
{
return FMath::Clamp(CVarResearchRedefinedDensityDetailScale.GetValueOnAnyThread(), 0.0f, 4.0f);
}
void SetDetailScale(float Scale)
{
CVarResearchRedefinedDensityDetailScale->Set(FMath::Clamp(Scale, 0.0f, 4.0f), ECVF_SetByCode);
}
bool IsParameterizedEnabled()
{
return CVarResearchParameterizedCave.GetValueOnAnyThread() != 0;
}
void SetParameterizedEnabled(bool bEnabled)
{
CVarResearchParameterizedCave->Set(bEnabled ? 1 : 0, ECVF_SetByCode);
}
void SetLegacyStep2eAuditMode(bool bEnabled)
{
GUseLegacyStep2eAuditMode.store(bEnabled, std::memory_order_relaxed);
}
void SetParameterSet(int32 ParameterSet)
{
CVarResearchParameterizedCaveSet->Set(
ParameterSet < 0 ? -1 : FMath::Clamp(ParameterSet, 0,
static_cast<int32>(UE_ARRAY_COUNT(GParameterProfiles) - 1)), ECVF_SetByCode);
}
int32 GetParameterSet()
{
return CVarResearchParameterizedCaveSet.GetValueOnAnyThread();
}
int32 GetParameterSetCount()
{
return static_cast<int32>(UE_ARRAY_COUNT(GParameterProfiles));
}
bool GetParameterSetInfo(int32 ParameterSet, const TCHAR*& OutName,
FGenerationParameters& OutParameters)
{
if (ParameterSet < 0 || ParameterSet >= GetParameterSetCount())
{
return false;
}
const FParameterProfile& Profile = IsLegacyStep2eAuditMode()
? GLegacyStep2eProfiles[ParameterSet] : GParameterProfiles[ParameterSet];
OutName = Profile.Name;
OutParameters = Profile.Parameters;
return true;
}
void ResetThreadCache()
{
GPlanCache = FThreadPlanCache();
GShaftEdgeCache = FThreadShaftEdgeCache();
GParameterizedRoomCellCache.Reset();
GParameterizedRoomCellCacheMin = FIntPoint::ZeroValue;
GParameterizedRoomCellCacheSize = FIntPoint::ZeroValue;
GParameterizedRoomCellCachePlan = nullptr;
}
static float EvaluateInternal(const UVoxelGenerator& Generator,
float WorldX, float WorldY, float WorldZ, float DetailValue, bool bHasDetailValue,
float* OutPreEditDensity, float DetailScale,
const FBrickEvaluationContext* Context = nullptr,
bool bParameterized = false,
const FParameterizedBrickContext* ParameterizedContext = nullptr,
const FParameterizedColumnContext* ParameterizedColumnContext = nullptr)
{
VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker();
if (Context == nullptr && ParameterizedContext == nullptr)
{
EnsurePlan(Generator);
}
const bool bChunkResolved = Context != nullptr && Context->bChunkResolved
|| ParameterizedContext != nullptr && ParameterizedContext->bChunkResolved;
const FIntVector Chunk = Context != nullptr && Context->bChunkResolved
? Context->Chunk
: ParameterizedContext != nullptr && ParameterizedContext->bChunkResolved
? ParameterizedContext->Chunk
: FIntVector(
FMath::FloorToInt(WorldX / CHUNK_SIZE),
FMath::FloorToInt(WorldY / CHUNK_SIZE),
FMath::FloorToInt(WorldZ / CHUNK_SIZE));
const FStratePlan* Plan = Context != nullptr && Context->bPlanResolved
? static_cast<const FStratePlan*>(Context->PlanToken)
: ParameterizedContext != nullptr && ParameterizedContext->bPlanResolved
? static_cast<const FStratePlan*>(ParameterizedContext->PlanToken)
: FindStrate(Chunk);
float Result = 0.0f;
const UVoxelStrateManager* Manager = Generator.StrateManager;
if (Plan == nullptr)
{
const bool bIsGapChunk = Manager != nullptr
&& (bChunkResolved
? (Context != nullptr && Context->bChunkResolved
? Context->bIsGapChunk : ParameterizedContext->bIsGapChunk)
: Manager->IsGapChunk(Chunk));
if (bIsGapChunk)
{
float Internal = 8.0f * DensityUnitsPerLegacyVoxel;
Manager->ApplyPassageModifier(Internal, WorldX, WorldY, WorldZ,
8.0f * DensityUnitsPerLegacyVoxel, 0.0f);
Result = -Internal;
Manager->ApplyPassageNativeFloorMC(Result, WorldX, WorldY, WorldZ,
8.0f * DensityUnitsPerLegacyVoxel);
}
else
{
Result = 8.0f * DensityUnitsPerLegacyVoxel;
}
}
else
{
float DetailFade = 1.0f;
const float Macro = bParameterized
? EvaluateParameterizedMacro(*Plan, WorldX, WorldY, WorldZ, &DetailFade,
ParameterizedContext, ParameterizedColumnContext)
: (Context != nullptr && Context->bMacroXYPrecomputed
? EvaluateMacroFromBrick(*Plan, *Context, WorldX, WorldY, WorldZ, &DetailFade)
: EvaluateMacro(*Plan, WorldX, WorldY, WorldZ, &DetailFade));
const uint32 DetailSeed = static_cast<uint32>(Generator.Seed)
^ static_cast<uint32>(Plan->StrateIndex * 7919 + 104729);
const float FinalDetail = bHasDetailValue
? DetailValue : DetailAt(WorldX, WorldY, WorldZ, DetailSeed, bParameterized);
Result = Macro + FinalDetail * DetailFade * DetailScale;
const bool bMayContainPassage = bParameterized
? (ParameterizedContext == nullptr || ParameterizedContext->bMayContainPassage)
: (Context == nullptr || Context->bMayContainPassage);
if (Manager != nullptr && bMayContainPassage)
{
const FVector Point(WorldX, WorldY, WorldZ);
bool bNearPassage = false;
const auto IsInsideBound = [&Point](const FPassageBound& Bounds)
{ return PointMayIntersectPassageBound(Point, Bounds); };
if (Context != nullptr && !bParameterized)
{
for (const int32 BoundsIndex : Context->PassageCandidates)
{
if (IsInsideBound(GPlanCache.PassageBounds[BoundsIndex]))
{
bNearPassage = true;
break;
}
}
}
else if (ParameterizedContext != nullptr)
{
for (const int32 BoundsIndex : ParameterizedContext->PassageCandidates)
{
if (IsInsideBound(GPlanCache.PassageBounds[BoundsIndex]))
{
bNearPassage = true;
break;
}
}
}
else
{
for (const FPassageBound& Bounds : GPlanCache.PassageBounds)
{
if (IsInsideBound(Bounds))
{
bNearPassage = true;
break;
}
}
}
if (bNearPassage)
{
float Internal = -Result;
Manager->ApplyPassageModifier(Internal, WorldX, WorldY, WorldZ,
8.0f * DensityUnitsPerLegacyVoxel, 0.0f);
Result = -Internal;
Manager->ApplyPassageNativeFloorMC(Result, WorldX, WorldY, WorldZ,
8.0f * DensityUnitsPerLegacyVoxel);
}
}
}
if (Generator.WorldRadiusVoxels > 0.0f)
{
const float Radial = FMath::Sqrt(WorldX * WorldX + WorldY * WorldY);
if (Radial > Generator.WorldRadiusVoxels)
{
const float EdgeDistance = Radial - Generator.WorldRadiusVoxels;
Result = FMath::Min(Result, -FMath::Min(EdgeDistance, Generator.EdgeSealThickness));
}
else if (Generator.EdgeSealThickness > 0.0f
&& Radial > Generator.WorldRadiusVoxels - Generator.EdgeSealThickness)
{
const float Alpha = (Generator.WorldRadiusVoxels - Radial) / Generator.EdgeSealThickness;
Result = FMath::Min(Result,
(Alpha * 8.0f - 8.0f) * DensityUnitsPerLegacyVoxel);
}
}
if (bParameterized && Plan != nullptr && !IsLegacyStep2eAuditMode())
{
constexpr float StrateSealThickness =
VoxelPassageGeometry::MetersToVoxels(4.0f);
const float StrateBottom = static_cast<float>(Plan->MinChunkZ * CHUNK_SIZE);
const float StrateTop = static_cast<float>(Plan->MaxChunkZExclusive * CHUNK_SIZE);
const float BoundaryLimit = FMath::Min(
WorldZ - StrateBottom - StrateSealThickness,
StrateTop - StrateSealThickness - WorldZ);
Result = FMath::Min(Result, BoundaryLimit);
}
if (OutPreEditDensity != nullptr)
{
*OutPreEditDensity = Result;
}
if (Generator.DiffLayer != nullptr
&& (bParameterized && ParameterizedContext != nullptr
? ParameterizedContext->bHasDiffMods
: Context != nullptr && !bParameterized
? Context->bHasDiffMods
: Generator.DiffLayer->HasAnyMods()))
{
Result -= Generator.DiffLayer->GetDensityOffset(Chunk, WorldX, WorldY, WorldZ);
}
VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker();
return Result;
}
float Evaluate(const UVoxelGenerator& Generator, float WorldX, float WorldY, float WorldZ,
float* OutPreEditDensity, float DetailScale)
{
return EvaluateInternal(Generator, WorldX, WorldY, WorldZ, 0.0f, false,
OutPreEditDensity, DetailScale);
}
float EvaluateWithDetailValue(const UVoxelGenerator& Generator,
float WorldX, float WorldY, float WorldZ, float DetailValue,
float* OutPreEditDensity, float DetailScale,
const FBrickEvaluationContext* Context)
{
return EvaluateInternal(Generator, WorldX, WorldY, WorldZ, DetailValue, true,
OutPreEditDensity, DetailScale, Context);
}
float EvaluateParameterized(const UVoxelGenerator& Generator,
float WorldX, float WorldY, float WorldZ, float* OutPreEditDensity, float DetailScale)
{
return EvaluateInternal(Generator, WorldX, WorldY, WorldZ, 0.0f, false,
OutPreEditDensity, DetailScale, nullptr, true, nullptr);
}
float EvaluateParameterizedMacroOnly(
const UVoxelGenerator& Generator, float WorldX, float WorldY, float WorldZ)
{
EnsurePlan(Generator);
const FIntVector Chunk(
FMath::FloorToInt(WorldX / CHUNK_SIZE),
FMath::FloorToInt(WorldY / CHUNK_SIZE),
FMath::FloorToInt(WorldZ / CHUNK_SIZE));
const FStratePlan* Plan = FindStrate(Chunk);
return Plan != nullptr
? EvaluateParameterizedMacro(*Plan, WorldX, WorldY, WorldZ, nullptr)
: 0.0f;
}
FString DescribeParameterizedMacroAt(
const UVoxelGenerator& Generator, float WorldX, float WorldY, float WorldZ)
{
EnsurePlan(Generator);
const FIntVector Chunk(
FMath::FloorToInt(WorldX / CHUNK_SIZE),
FMath::FloorToInt(WorldY / CHUNK_SIZE),
FMath::FloorToInt(WorldZ / CHUNK_SIZE));
const FStratePlan* Plan = FindStrate(Chunk);
if (Plan == nullptr)
{
return TEXT("no-plan");
}
FParameterizedColumnContext Column;
BuildParameterizedColumnContext(*Plan, WorldX, WorldY, nullptr, Column);
const FIntPoint BaseCell = GenerationCellAt(*Plan, WorldX, WorldY);
FString Description = FString::Printf(
TEXT("base=(%d,%d); cells="), BaseCell.X, BaseCell.Y);
for (int32 Index = 0; Index < UE_ARRAY_COUNT(Column.Cells); ++Index)
{
const FGeneratedCaveCell& Cell = *Column.Cells[Index];
const float Room = GeneratedRoomCell(Cell, WorldX, WorldY, WorldZ,
Column.RoomRadii[Index], Column.RoomHorizontals[Index], Column, Index);
Description += FString::Printf(
TEXT("(%d,%d,r=%.2f,h=%.2f,v=%.2f,room=%.2f,flags=%u,edges=%u)"),
Cell.CellX, Cell.CellY, Cell.RadiusX, Column.RoomHorizontals[Index],
FMath::Min(WorldZ - Column.RoomFloors[Index], Cell.CeilingZ - WorldZ),
Room, static_cast<uint32>(Cell.FeatureFlags),
static_cast<uint32>(Cell.OpenTunnelEdges));
if (Index + 1 < UE_ARRAY_COUNT(Column.Cells))
{
Description += TEXT(",");
}
}
Description += FString::Printf(TEXT(";macro=%.3f"),
EvaluateParameterizedMacro(*Plan, WorldX, WorldY, WorldZ, nullptr));
return Description;
}
float EvaluateParameterizedDetail(
const UVoxelGenerator& Generator, float WorldX, float WorldY, float WorldZ)
{
EnsurePlan(Generator);
const FIntVector Chunk(
FMath::FloorToInt(WorldX / CHUNK_SIZE),
FMath::FloorToInt(WorldY / CHUNK_SIZE),
FMath::FloorToInt(WorldZ / CHUNK_SIZE));
const FStratePlan* Plan = FindStrate(Chunk);
if (Plan == nullptr)
{
return 0.0f;
}
const uint32 DetailSeed = static_cast<uint32>(Generator.Seed)
^ static_cast<uint32>(Plan->StrateIndex * 7919 + 104729);
return DetailAt(WorldX, WorldY, WorldZ, DetailSeed, true);
}
float EvaluateParameterizedWithDetailValue(const UVoxelGenerator& Generator,
float WorldX, float WorldY, float WorldZ, float DetailValue,
float* OutPreEditDensity, float DetailScale,
const FParameterizedBrickContext* Context)
{
return EvaluateInternal(Generator, WorldX, WorldY, WorldZ, DetailValue, true,
OutPreEditDensity, DetailScale, nullptr, true, Context);
}
void EvaluateParameterizedColumnWithDetailValues(
const UVoxelGenerator& Generator, float WorldX, float WorldY, float WorldZStart,
const float* DetailValues, int32 DetailStride, int32 Count, float* OutValues,
const FParameterizedBrickContext* Context, float DetailScale, int32 SampleStep)
{
SampleStep = FMath::Max(1, SampleStep);
if (DetailValues == nullptr || OutValues == nullptr || Count <= 0 || DetailStride <= 0)
{
return;
}
const FIntVector Chunk(
FMath::FloorToInt(WorldX / CHUNK_SIZE),
FMath::FloorToInt(WorldY / CHUNK_SIZE),
FMath::FloorToInt(WorldZStart / CHUNK_SIZE));
const FStratePlan* Plan = Context != nullptr && Context->bPlanResolved
? static_cast<const FStratePlan*>(Context->PlanToken) : nullptr;
if (Plan == nullptr)
{
EnsurePlan(Generator);
Plan = FindStrate(Chunk);
}
const int32 EndChunkZ = FMath::FloorToInt(
(WorldZStart + static_cast<float>(Count - 1) * SampleStep) / CHUNK_SIZE);
if (Plan == nullptr || Chunk.Z != EndChunkZ)
{
for (int32 Z = 0; Z < Count; ++Z)
{
OutValues[Z] = EvaluateInternal(
Generator, WorldX, WorldY,
WorldZStart + static_cast<float>(Z) * SampleStep,
DetailValues[Z * DetailStride], true, nullptr, DetailScale,
nullptr, true, Context);
}
return;
}
FParameterizedColumnContext ColumnContext;
BuildParameterizedColumnContext(*Plan, WorldX, WorldY, Context, ColumnContext);
for (int32 Z = 0; Z < Count; ++Z)
{
OutValues[Z] = EvaluateInternal(
Generator, WorldX, WorldY,
WorldZStart + static_cast<float>(Z) * SampleStep,
DetailValues[Z * DetailStride], true, nullptr, DetailScale,
nullptr, true, Context, &ColumnContext);
}
}
void BuildParameterizedBrickContext(
const UVoxelGenerator& Generator, FIntVector OriginVoxels, FIntVector Size,
FParameterizedBrickContext& OutContext, int32 SampleStep)
{
SampleStep = FMath::Max(1, SampleStep);
OutContext = FParameterizedBrickContext();
EnsurePlan(Generator);
const FIntVector End = OriginVoxels + FIntVector(
FMath::Max(Size.X - 1, 0), FMath::Max(Size.Y - 1, 0),
FMath::Max(Size.Z - 1, 0)) * SampleStep;
const FIntVector Chunk(
FMath::FloorToInt(static_cast<float>(OriginVoxels.X) / CHUNK_SIZE),
FMath::FloorToInt(static_cast<float>(OriginVoxels.Y) / CHUNK_SIZE),
FMath::FloorToInt(static_cast<float>(OriginVoxels.Z) / CHUNK_SIZE));
const FIntVector EndChunk(
FMath::FloorToInt(static_cast<float>(End.X) / CHUNK_SIZE),
FMath::FloorToInt(static_cast<float>(End.Y) / CHUNK_SIZE),
FMath::FloorToInt(static_cast<float>(End.Z) / CHUNK_SIZE));
const bool bStableStrate = Chunk.Z == EndChunk.Z;
const FStratePlan* Plan = bStableStrate ? FindStrate(Chunk) : nullptr;
OutContext.BrickOrigin = OriginVoxels;
OutContext.BrickSize = Size;
OutContext.Chunk = Chunk;
OutContext.bChunkResolved = Chunk == EndChunk;
OutContext.bPlanResolved = bStableStrate;
OutContext.PlanToken = bStableStrate ? Plan : nullptr;
OutContext.bIsGapChunk = OutContext.bChunkResolved
&& Generator.StrateManager != nullptr
&& Generator.StrateManager->IsGapChunk(Chunk);
OutContext.bHasDiffMods = Generator.DiffLayer != nullptr
&& Generator.DiffLayer->HasAnyMods();
const FVector Min(
static_cast<float>(OriginVoxels.X - SampleStep),
static_cast<float>(OriginVoxels.Y - SampleStep),
static_cast<float>(OriginVoxels.Z - SampleStep));
const FVector Max(
static_cast<float>(OriginVoxels.X + Size.X * SampleStep),
static_cast<float>(OriginVoxels.Y + Size.Y * SampleStep),
static_cast<float>(OriginVoxels.Z + Size.Z * SampleStep));
FindPassageCandidates(FBox(Min, Max), OutContext.PassageCandidates);
OutContext.bMayContainPassage = !OutContext.PassageCandidates.IsEmpty();
if (Plan != nullptr && bStableStrate && !OutContext.bIsGapChunk
&& Size.X > 0 && Size.Y > 0)
{
FIntPoint MinimumBaseCell(MAX_int32, MAX_int32);
FIntPoint MaximumBaseCell(MIN_int32, MIN_int32);
const int32 CornerX[] = {OriginVoxels.X, End.X};
const int32 CornerY[] = {OriginVoxels.Y, End.Y};
for (const int32 WorldY : CornerY)
for (const int32 WorldX : CornerX)
{
const FIntPoint Cell = GenerationCellAt(
*Plan, static_cast<float>(WorldX), static_cast<float>(WorldY));
MinimumBaseCell.X = FMath::Min(MinimumBaseCell.X, Cell.X);
MinimumBaseCell.Y = FMath::Min(MinimumBaseCell.Y, Cell.Y);
MaximumBaseCell.X = FMath::Max(MaximumBaseCell.X, Cell.X);
MaximumBaseCell.Y = FMath::Max(MaximumBaseCell.Y, Cell.Y);
}
const FIntPoint NeededMin = MinimumBaseCell - FIntPoint(1, 1);
const FIntPoint NeededMax = MaximumBaseCell + FIntPoint(1, 1);
const bool bCacheContainsNeededCells = GParameterizedRoomCellCachePlan == Plan
&& GParameterizedRoomCellCacheMin.X <= NeededMin.X
&& GParameterizedRoomCellCacheMin.Y <= NeededMin.Y
&& GParameterizedRoomCellCacheMin.X + GParameterizedRoomCellCacheSize.X - 1
>= NeededMax.X
&& GParameterizedRoomCellCacheMin.Y + GParameterizedRoomCellCacheSize.Y - 1
>= NeededMax.Y;
if (!bCacheContainsNeededCells)
{
// Keep one extra generated-cell layer beyond the 3x3 query halo so adjacent
// proof bricks reuse their deterministic room and edge rolls.
GParameterizedRoomCellCacheMin = MinimumBaseCell - FIntPoint(2, 2);
GParameterizedRoomCellCacheSize = MaximumBaseCell - MinimumBaseCell
+ FIntPoint(5, 5);
const int32 RoomCellCount = GParameterizedRoomCellCacheSize.X
* GParameterizedRoomCellCacheSize.Y;
GParameterizedRoomCellCache.SetNumUninitialized(RoomCellCount);
int32 RoomCellIndex = 0;
for (int32 CellY = 0; CellY < GParameterizedRoomCellCacheSize.Y; ++CellY)
for (int32 CellX = 0; CellX < GParameterizedRoomCellCacheSize.X; ++CellX)
{
GParameterizedRoomCellCache[RoomCellIndex++] = BuildGeneratedCell(
*Plan, GParameterizedRoomCellCacheMin.X + CellX,
GParameterizedRoomCellCacheMin.Y + CellY);
}
GParameterizedRoomCellCachePlan = Plan;
}
OutContext.RoomCellCacheMin = GParameterizedRoomCellCacheMin;
OutContext.RoomCellCacheSize = GParameterizedRoomCellCacheSize;
OutContext.RoomCellCacheToken = GParameterizedRoomCellCache.GetData();
}
}
void EvaluateParameterizedDensityBrick(
const UVoxelGenerator& Generator, FIntVector OriginVoxels, FIntVector Size,
float DetailScale, FParameterizedBrickContext& InOutContext,
TArray<float>& InOutDetail, TArray<float>& InOutColumnValues,
TArray<float>& OutValues, int32 SampleStep)
{
SampleStep = FMath::Max(1, SampleStep);
if (Size.X <= 0 || Size.Y <= 0 || Size.Z <= 0)
{
OutValues.Reset();
return;
}
const int64 SampleCount = static_cast<int64>(Size.X) * Size.Y * Size.Z;
if (SampleCount > MAX_int32)
{
OutValues.Reset();
return;
}
BuildDetailBrick(Generator, OriginVoxels, Size, DetailScale,
InOutDetail, nullptr, /*bParameterized=*/true, SampleStep);
BuildParameterizedBrickContext(
Generator, OriginVoxels, Size, InOutContext, SampleStep);
InOutColumnValues.SetNumUninitialized(Size.Z);
OutValues.SetNumUninitialized(static_cast<int32>(SampleCount));
const int32 DetailStride = Size.X * Size.Y;
for (int32 Y = 0; Y < Size.Y; ++Y)
for (int32 X = 0; X < Size.X; ++X)
{
const int32 DetailBase = Y * Size.X + X;
EvaluateParameterizedColumnWithDetailValues(
Generator,
static_cast<float>(OriginVoxels.X + X * SampleStep),
static_cast<float>(OriginVoxels.Y + Y * SampleStep),
static_cast<float>(OriginVoxels.Z),
InOutDetail.GetData() + DetailBase, DetailStride, Size.Z,
InOutColumnValues.GetData(), &InOutContext, DetailScale, SampleStep);
for (int32 Z = 0; Z < Size.Z; ++Z)
{
OutValues[(Z * Size.Y + Y) * Size.X + X] = InOutColumnValues[Z];
}
}
}
void BuildDetailBrick(const UVoxelGenerator& Generator, FIntVector OriginVoxels,
FIntVector Size, float DetailScale, TArray<float>& OutDetail,
FBrickEvaluationContext* OutContext, bool bParameterized, int32 SampleStep)
{
(void)DetailScale;
SampleStep = FMath::Max(1, SampleStep);
const int32 SampleCount = Size.X * Size.Y * Size.Z;
OutDetail.SetNumUninitialized(SampleCount);
FMemory::Memzero(OutDetail.GetData(), static_cast<SIZE_T>(SampleCount) * sizeof(float));
if (OutContext != nullptr)
{
OutContext->PlanToken = nullptr;
OutContext->bPlanResolved = false;
OutContext->bMayContainPassage = true;
OutContext->bChunkResolved = false;
OutContext->bIsGapChunk = false;
OutContext->bHasDiffMods = false;
OutContext->bMacroXYPrecomputed = false;
OutContext->FloorByXY.Reset();
OutContext->GridAirByXY.Reset();
OutContext->DetailFadeByXY.Reset();
OutContext->LandingHorizontalByXY.Reset();
OutContext->LandingApproachByXY.Reset();
OutContext->LandingBridgeHorizontalByXY.Reset();
OutContext->LandingBridgeFloorByXY.Reset();
OutContext->PassageCandidates.Reset();
}
if (Size.X <= 0 || Size.Y <= 0 || Size.Z <= 0
|| (!bParameterized && (Size.X > 8 || Size.Y > 8 || Size.Z > 8))
|| Generator.StrateManager == nullptr)
{
return;
}
EnsurePlan(Generator);
const FIntVector Chunk(
FMath::FloorToInt(static_cast<float>(OriginVoxels.X) / CHUNK_SIZE),
FMath::FloorToInt(static_cast<float>(OriginVoxels.Y) / CHUNK_SIZE),
FMath::FloorToInt(static_cast<float>(OriginVoxels.Z) / CHUNK_SIZE));
const FIntVector EndChunk(
FMath::FloorToInt(static_cast<float>(OriginVoxels.X + (Size.X - 1) * SampleStep) / CHUNK_SIZE),
FMath::FloorToInt(static_cast<float>(OriginVoxels.Y + (Size.Y - 1) * SampleStep) / CHUNK_SIZE),
FMath::FloorToInt(static_cast<float>(OriginVoxels.Z + (Size.Z - 1) * SampleStep) / CHUNK_SIZE));
const bool bStableStrate = Chunk.Z == EndChunk.Z;
const FStratePlan* Plan = FindStrate(Chunk);
if (OutContext != nullptr)
{
OutContext->BrickOrigin = OriginVoxels;
OutContext->BrickSize = Size;
OutContext->bPlanResolved = bStableStrate;
OutContext->PlanToken = bStableStrate ? Plan : nullptr;
OutContext->Chunk = Chunk;
OutContext->bChunkResolved = Chunk == EndChunk;
OutContext->bIsGapChunk = OutContext->bChunkResolved
&& Generator.StrateManager->IsGapChunk(Chunk);
OutContext->bHasDiffMods = Generator.DiffLayer != nullptr
&& Generator.DiffLayer->HasAnyMods();
const FVector Min(
static_cast<float>(OriginVoxels.X - 1),
static_cast<float>(OriginVoxels.Y - 1),
static_cast<float>(OriginVoxels.Z - 1));
const FVector Max(
static_cast<float>(OriginVoxels.X + Size.X),
static_cast<float>(OriginVoxels.Y + Size.Y),
static_cast<float>(OriginVoxels.Z + Size.Z));
FindPassageCandidates(FBox(Min, Max), OutContext->PassageCandidates);
OutContext->bMayContainPassage = !OutContext->PassageCandidates.IsEmpty();
}
if (!bStableStrate)
{
// A clipped ring-edge brick can span two strate plans. Keep its detail field exact by
// using the direct evaluator there; this is a tiny boundary-only fallback.
for (int32 Z = 0; Z < Size.Z; ++Z)
for (int32 Y = 0; Y < Size.Y; ++Y)
for (int32 X = 0; X < Size.X; ++X)
{
const int32 WorldZ = OriginVoxels.Z + Z * SampleStep;
const FIntVector SampleChunk(0, 0,
FMath::FloorToInt(static_cast<float>(WorldZ) / CHUNK_SIZE));
const FStratePlan* SamplePlan = FindStrate(SampleChunk);
if (SamplePlan != nullptr)
{
const uint32 SampleSeed = static_cast<uint32>(Generator.Seed)
^ static_cast<uint32>(SamplePlan->StrateIndex * 7919 + 104729);
OutDetail[(Z * Size.Y + Y) * Size.X + X] = DetailAt(
static_cast<float>(OriginVoxels.X + X * SampleStep),
static_cast<float>(OriginVoxels.Y + Y * SampleStep),
static_cast<float>(WorldZ), SampleSeed, bParameterized);
}
}
return;
}
if (Plan == nullptr)
{
return;
}
if (OutContext != nullptr)
{
const int32 XYCount = Size.X * Size.Y;
OutContext->FloorByXY.SetNumUninitialized(XYCount);
OutContext->GridAirByXY.SetNumUninitialized(XYCount);
OutContext->DetailFadeByXY.SetNumUninitialized(XYCount);
OutContext->LandingHorizontalByXY.SetNumUninitialized(XYCount * Plan->Landings.Num());
OutContext->LandingApproachByXY.SetNumUninitialized(XYCount * Plan->Landings.Num());
OutContext->LandingBridgeHorizontalByXY.SetNumUninitialized(XYCount * Plan->Landings.Num());
OutContext->LandingBridgeFloorByXY.SetNumUninitialized(XYCount * Plan->Landings.Num());
for (int32 Y = 0; Y < Size.Y; ++Y)
for (int32 X = 0; X < Size.X; ++X)
{
const int32 XYIndex = Y * Size.X + X;
const float WorldX = static_cast<float>(OriginVoxels.X + X * SampleStep);
const float WorldY = static_cast<float>(OriginVoxels.Y + Y * SampleStep);
const float XLane = DistanceToPeriodicLine(
WorldX, Plan->CellSpacing, Plan->OffsetX);
const float YLane = DistanceToPeriodicLine(
WorldY, Plan->CellSpacing, Plan->OffsetY);
OutContext->GridAirByXY[XYIndex] = Plan->CorridorHalfWidth
- FMath::Min(XLane, YLane);
float WeightedDelta = 0.0f;
float TotalWeight = 0.0f;
float DetailFade = 1.0f;
for (int32 SiteIndex = 0; SiteIndex < Plan->Landings.Num(); ++SiteIndex)
{
const FLandingSite& Site = Plan->Landings[SiteIndex];
const float DX = WorldX - Site.Position.X;
const float DY = WorldY - Site.Position.Y;
const float DistanceSquared = DX * DX + DY * DY;
const float Distance = FMath::Sqrt(DistanceSquared);
if (DistanceSquared < Site.BlendRadius * Site.BlendRadius)
{
const float Weight = Fade(1.0f - Distance / Site.BlendRadius);
WeightedDelta += Weight * (Site.FloorZ - Plan->FloorZ);
TotalWeight += Weight;
}
OutContext->LandingHorizontalByXY[SiteIndex * XYCount + XYIndex] =
Site.PadRadius - Distance;
OutContext->LandingApproachByXY[SiteIndex * XYCount + XYIndex] =
LandingApproachRadius - FMath::Sqrt(DistanceSquaredToSegmentXY(
WorldX, WorldY, Site.Position, Site.ApproachEnd));
float BridgeHorizontal = -FLT_MAX;
float BridgeFloor = Plan->FloorZ;
if (Plan->LandingHubIndex != INDEX_NONE
&& Plan->Landings.Num() > 1
&& SiteIndex != Plan->LandingHubIndex)
{
const FLandingSite& Hub = Plan->Landings[Plan->LandingHubIndex];
const float T = SegmentParameterXY(
WorldX, WorldY, Site.Position, Hub.Position);
BridgeHorizontal = LandingBridgeRadius - FMath::Sqrt(
DistanceSquaredToSegmentXY(WorldX, WorldY, Site.Position, Hub.Position));
BridgeFloor = FMath::Lerp(Site.FloorZ, Hub.FloorZ, T);
}
OutContext->LandingBridgeHorizontalByXY[SiteIndex * XYCount + XYIndex] =
BridgeHorizontal;
OutContext->LandingBridgeFloorByXY[SiteIndex * XYCount + XYIndex] = BridgeFloor;
const float FlatRadius = FMath::Max(0.0f, Site.PadRadius
- VoxelPassageGeometry::MetersToVoxels(0.75f));
if (DistanceSquared < FlatRadius * FlatRadius)
{
DetailFade = 0.0f;
}
}
OutContext->FloorByXY[XYIndex] = Plan->FloorZ
+ WeightedDelta / FMath::Max(1.0f, TotalWeight);
OutContext->DetailFadeByXY[XYIndex] = DetailFade;
}
OutContext->bMacroXYPrecomputed = true;
}
const uint32 Seed = static_cast<uint32>(Generator.Seed)
^ static_cast<uint32>(Plan->StrateIndex * 7919 + 104729);
if (bParameterized)
{
if (SampleCount < 4096)
{
// Small mixed proof bricks do not repay their own three lookup grids, but adjacent
// bricks reuse a worker-local cache spanning the surrounding 64 m region. Keep the
// scalar interpolation unchanged while hashing each lattice corner only once there.
const bool bUseSharedDetailCache = bParameterized
&& !IsLegacyStep2eAuditMode()
&& GParameterizedDetailCache.Prepare(
OriginVoxels, Size, SampleStep, Seed);
for (int32 Z = 0; Z < Size.Z; ++Z)
for (int32 Y = 0; Y < Size.Y; ++Y)
for (int32 X = 0; X < Size.X; ++X)
{
const float WorldX = static_cast<float>(OriginVoxels.X + X * SampleStep);
const float WorldY = static_cast<float>(OriginVoxels.Y + Y * SampleStep);
const float WorldZ = static_cast<float>(OriginVoxels.Z + Z * SampleStep);
OutDetail[(Z * Size.Y + Y) * Size.X + X] = bUseSharedDetailCache
? DetailAtWithLatticeCaches(WorldX, WorldY, WorldZ, Seed,
GParameterizedDetailCache.Broad,
GParameterizedDetailCache.Medium,
GParameterizedDetailCache.Fine)
: DetailAt(WorldX, WorldY, WorldZ, Seed, true);
}
return;
}
// Cache each rotated band’s exact integer lattice hashes once for this brick. Every
// retained sample still uses the same point interpolation and arithmetic as DetailAt;
// adjacent samples simply reuse the same deterministic corner values.
FNoiseLatticeCache BroadCache;
FNoiseLatticeCache MediumCache;
FNoiseLatticeCache FineCache;
BuildNoiseLatticeCache(OriginVoxels, Size, SampleStep,
VoxelPassageGeometry::MetersToVoxels(12.0f),
Seed ^ 0x68BC21EBu, BroadDetailTransform, BroadCache);
BuildNoiseLatticeCache(OriginVoxels, Size, SampleStep,
VoxelPassageGeometry::MetersToVoxels(6.0f),
Seed ^ 0x02E5BE93u, MediumDetailTransform, MediumCache);
BuildNoiseLatticeCache(OriginVoxels, Size, SampleStep,
VoxelPassageGeometry::MetersToVoxels(3.0f),
Seed ^ 0x967A889Bu, FineDetailTransform, FineCache);
for (int32 Z = 0; Z < Size.Z; ++Z)
for (int32 Y = 0; Y < Size.Y; ++Y)
for (int32 X = 0; X < Size.X; ++X)
{
OutDetail[(Z * Size.Y + Y) * Size.X + X] = DetailAtWithLatticeCaches(
static_cast<float>(OriginVoxels.X + X * SampleStep),
static_cast<float>(OriginVoxels.Y + Y * SampleStep),
static_cast<float>(OriginVoxels.Z + Z * SampleStep), Seed,
BroadCache, MediumCache, FineCache);
}
return;
}
const int32 CountX = Size.X;
const int32 CountY = Size.Y;
const auto SampleIndex = [CountX, CountY](int32 X, int32 Y, int32 Z)
{
return (Z * CountY + Y) * CountX + X;
};
const auto AddNoiseBand = [&](int32 CellSize, uint32 BandSeed, float Amplitude,
TArray<float>* OutBand)
{
if (OutBand != nullptr)
{
OutBand->SetNumUninitialized(SampleCount);
}
const int32 MinCellX = FMath::FloorToInt(
static_cast<float>(OriginVoxels.X) / static_cast<float>(CellSize));
const int32 MinCellY = FMath::FloorToInt(
static_cast<float>(OriginVoxels.Y) / static_cast<float>(CellSize));
const int32 MinCellZ = FMath::FloorToInt(
static_cast<float>(OriginVoxels.Z) / static_cast<float>(CellSize));
const int32 MaxCellX = FMath::FloorToInt(
static_cast<float>(OriginVoxels.X + (Size.X - 1) * SampleStep) / static_cast<float>(CellSize));
const int32 MaxCellY = FMath::FloorToInt(
static_cast<float>(OriginVoxels.Y + (Size.Y - 1) * SampleStep) / static_cast<float>(CellSize));
const int32 MaxCellZ = FMath::FloorToInt(
static_cast<float>(OriginVoxels.Z + (Size.Z - 1) * SampleStep) / static_cast<float>(CellSize));
const int32 GridX = MaxCellX - MinCellX + 2;
const int32 GridY = MaxCellY - MinCellY + 2;
const int32 GridZ = MaxCellZ - MinCellZ + 2;
float Grid[1000];
check(GridX * GridY * GridZ <= UE_ARRAY_COUNT(Grid));
for (int32 Z = 0; Z < GridZ; ++Z)
for (int32 Y = 0; Y < GridY; ++Y)
for (int32 X = 0; X < GridX; ++X)
{
Grid[(Z * GridY + Y) * GridX + X] = HashValue(
MinCellX + X, MinCellY + Y, MinCellZ + Z, BandSeed);
}
int32 CellX[8];
int32 CellY[8];
int32 CellZ[8];
float FadeX[8];
float FadeY[8];
float FadeZ[8];
for (int32 X = 0; X < Size.X; ++X)
{
const float SX = static_cast<float>(OriginVoxels.X + X * SampleStep)
/ static_cast<float>(CellSize);
const int32 Cell = FMath::FloorToInt(SX);
CellX[X] = Cell - MinCellX;
FadeX[X] = Fade(SX - static_cast<float>(Cell));
}
for (int32 Y = 0; Y < Size.Y; ++Y)
{
const float SY = static_cast<float>(OriginVoxels.Y + Y * SampleStep)
/ static_cast<float>(CellSize);
const int32 Cell = FMath::FloorToInt(SY);
CellY[Y] = Cell - MinCellY;
FadeY[Y] = Fade(SY - static_cast<float>(Cell));
}
for (int32 Z = 0; Z < Size.Z; ++Z)
{
const float SZ = static_cast<float>(OriginVoxels.Z + Z * SampleStep)
/ static_cast<float>(CellSize);
const int32 Cell = FMath::FloorToInt(SZ);
CellZ[Z] = Cell - MinCellZ;
FadeZ[Z] = Fade(SZ - static_cast<float>(Cell));
}
for (int32 Z = 0; Z < Size.Z; ++Z)
for (int32 Y = 0; Y < Size.Y; ++Y)
for (int32 X = 0; X < Size.X; ++X)
{
const int32 IX = CellX[X];
const int32 IY = CellY[Y];
const int32 IZ = CellZ[Z];
const float AX = FadeX[X];
const float AY = FadeY[Y];
const float AZ = FadeZ[Z];
const int32 Index = (IZ * GridY + IY) * GridX + IX;
const float Value = CellSize == 1
? Grid[Index]
: FMath::Lerp(
FMath::Lerp(
FMath::Lerp(Grid[Index], Grid[Index + 1], AX),
FMath::Lerp(Grid[Index + GridX], Grid[Index + GridX + 1], AX), AY),
FMath::Lerp(
FMath::Lerp(Grid[Index + GridX * GridY],
Grid[Index + GridX * GridY + 1], AX),
FMath::Lerp(Grid[Index + GridX * GridY + GridX],
Grid[Index + GridX * GridY + GridX + 1], AX), AY), AZ);
const int32 OutputIndex = SampleIndex(X, Y, Z);
if (OutBand != nullptr)
{
(*OutBand)[OutputIndex] = Value;
}
else
{
OutDetail[OutputIndex] += Value * Amplitude;
}
}
};
if (bParameterized && !IsLegacyStep2eAuditMode())
{
TArray<float> Broad;
TArray<float> Medium;
TArray<float> Fine;
AddNoiseBand(VoxelPassageGeometry::MetersToVoxels(12.0f),
Seed ^ 0x68BC21EBu, 1.0f, &Broad);
AddNoiseBand(VoxelPassageGeometry::MetersToVoxels(6.0f),
Seed ^ 0x02E5BE93u, 1.0f, &Medium);
AddNoiseBand(VoxelPassageGeometry::MetersToVoxels(3.0f),
Seed ^ 0x967A889Bu, 1.0f, &Fine);
for (int32 Index = 0; Index < SampleCount; ++Index)
{
const float Ridge = FMath::Sqrt(Fine[Index] * Fine[Index] + 0.0625f) - 0.25f;
OutDetail[Index] = Broad[Index] * 2.0f + Medium[Index] * 1.2f
+ Fine[Index] * 0.6f + Ridge * 0.4f;
}
return;
}
AddNoiseBand(VoxelPassageGeometry::MetersToVoxels(2.0f),
Seed ^ 0x68BC21EBu, 1.4f * DensityUnitsPerLegacyVoxel, nullptr);
AddNoiseBand(VoxelPassageGeometry::MetersToVoxels(1.0f),
Seed ^ 0x02E5BE93u, 0.9f * DensityUnitsPerLegacyVoxel, nullptr);
AddNoiseBand(VoxelPassageGeometry::MetersToVoxels(0.5f),
Seed ^ 0x967A889Bu, 0.45f * DensityUnitsPerLegacyVoxel, nullptr);
AddNoiseBand(VoxelPassageGeometry::MetersToVoxels(0.25f),
Seed ^ 0xA511E9B3u, 2.5f * DensityUnitsPerLegacyVoxel, nullptr);
}
static bool TryClassifyBrickInternal(
const UVoxelGenerator& Generator, FIntVector OriginVoxels,
FIntVector Size, bool& bOutAir, float DetailScale,
bool* bOutEvaluatedMacroCentre,
EBrickFallbackReason* OutFallbackReason,
bool bParameterized, int32 SampleStep)
{
SampleStep = FMath::Max(1, SampleStep);
if (OutFallbackReason != nullptr)
{
*OutFallbackReason = EBrickFallbackReason::UnsupportedState;
}
if (bOutEvaluatedMacroCentre != nullptr)
{
*bOutEvaluatedMacroCentre = false;
}
if (Size.X <= 0 || Size.Y <= 0 || Size.Z <= 0
|| Size.X > 8 || Size.Y > 8 || Size.Z > 8
|| Generator.StrateManager == nullptr
|| Generator.WorldRadiusVoxels != 0.0f
|| (Generator.DiffLayer != nullptr && Generator.DiffLayer->HasAnyMods()))
{
return false;
}
EnsurePlan(Generator);
const int32 MinChunkX = FMath::FloorToInt(
static_cast<float>(OriginVoxels.X - SampleStep) / CHUNK_SIZE);
const int32 MaxChunkX = FMath::FloorToInt(
static_cast<float>(OriginVoxels.X + Size.X * SampleStep) / CHUNK_SIZE);
const int32 MinChunkY = FMath::FloorToInt(
static_cast<float>(OriginVoxels.Y - SampleStep) / CHUNK_SIZE);
const int32 MaxChunkY = FMath::FloorToInt(
static_cast<float>(OriginVoxels.Y + Size.Y * SampleStep) / CHUNK_SIZE);
const int32 MinChunkZ = FMath::FloorToInt(
static_cast<float>(OriginVoxels.Z - SampleStep) / CHUNK_SIZE);
const int32 MaxChunkZ = FMath::FloorToInt(
static_cast<float>(OriginVoxels.Z + Size.Z * SampleStep) / CHUNK_SIZE);
const FIntVector Chunk(
MinChunkX, MinChunkY, MinChunkZ);
const FStratePlan* Plan = FindStrate(Chunk);
for (int32 ChunkZ = MinChunkZ; ChunkZ <= MaxChunkZ; ++ChunkZ)
{
if (FindStrate(FIntVector(MinChunkX, MinChunkY, ChunkZ)) != Plan)
{
if (OutFallbackReason != nullptr)
{
*OutFallbackReason = EBrickFallbackReason::ChunkOrStrateBoundary;
}
return false;
}
}
const FVector Min(
static_cast<float>(OriginVoxels.X - SampleStep),
static_cast<float>(OriginVoxels.Y - SampleStep),
static_cast<float>(OriginVoxels.Z - SampleStep));
const FVector Max(
static_cast<float>(OriginVoxels.X + Size.X * SampleStep),
static_cast<float>(OriginVoxels.Y + Size.Y * SampleStep),
static_cast<float>(OriginVoxels.Z + Size.Z * SampleStep));
const FBox Expanded(Min, Max);
if (IntersectsPassageBounds(Expanded))
{
if (OutFallbackReason != nullptr)
{
*OutFallbackReason = EBrickFallbackReason::PassageBounds;
}
return false;
}
if (Plan == nullptr)
{
// Outside a strate there is no added detail: gaps are bedrock and out-of-layout chunks
// are air. Passage bounds above keep all carved tubes, landings, and support writes mixed.
const bool bGap = Generator.StrateManager->IsGapChunk(Chunk);
for (int32 ChunkZ = MinChunkZ; ChunkZ <= MaxChunkZ; ++ChunkZ)
for (int32 ChunkY = MinChunkY; ChunkY <= MaxChunkY; ++ChunkY)
for (int32 ChunkX = MinChunkX; ChunkX <= MaxChunkX; ++ChunkX)
{
if (Generator.StrateManager->IsGapChunk(FIntVector(ChunkX, ChunkY, ChunkZ)) != bGap)
{
if (OutFallbackReason != nullptr)
{
*OutFallbackReason = EBrickFallbackReason::ChunkOrStrateBoundary;
}
return false;
}
}
bOutAir = !bGap;
if (OutFallbackReason != nullptr)
{
*OutFallbackReason = EBrickFallbackReason::None;
}
return true;
}
if (Plan->StrateIndex < 0
|| MinChunkZ < Plan->MinChunkZ || MaxChunkZ >= Plan->MaxChunkZExclusive)
{
if (OutFallbackReason != nullptr)
{
*OutFallbackReason = EBrickFallbackReason::ChunkOrStrateBoundary;
}
return false;
}
// Bound the spatial landing-floor blend over this brick instead of disabling proofs near
// every landing. The floor is a weighted average while the total weight is >= 1, and an
// unnormalised sum below that threshold; these two derivative bounds cover both branches.
double SumAbsDeltaSlope = 0.0;
double SumInverseRadiusSlope = 0.0;
double LandingBridgeSlopeBound = 0.0;
float MaxAbsFloorDelta = 0.0f;
for (const FLandingSite& Site : Plan->Landings)
{
const float ClosestX = FMath::Clamp(Site.Position.X, Min.X, Max.X);
const float ClosestY = FMath::Clamp(Site.Position.Y, Min.Y, Max.Y);
const float DX = ClosestX - Site.Position.X;
const float DY = ClosestY - Site.Position.Y;
if (DX * DX + DY * DY > Site.BlendRadius * Site.BlendRadius)
{
continue;
}
const float AbsDelta = FMath::Abs(Site.FloorZ - Plan->FloorZ);
MaxAbsFloorDelta = FMath::Max(MaxAbsFloorDelta, AbsDelta);
SumAbsDeltaSlope += QuinticFadeSlopeBound * AbsDelta / Site.BlendRadius;
SumInverseRadiusSlope += QuinticFadeSlopeBound / Site.BlendRadius;
}
if (Plan->LandingHubIndex != INDEX_NONE && Plan->Landings.Num() > 1)
{
const FLandingSite& Hub = Plan->Landings[Plan->LandingHubIndex];
for (int32 SiteIndex = 0; SiteIndex < Plan->Landings.Num(); ++SiteIndex)
{
if (SiteIndex == Plan->LandingHubIndex)
{
continue;
}
const FLandingSite& Site = Plan->Landings[SiteIndex];
const float SegmentLength = FMath::Sqrt(FMath::Square(Hub.Position.X - Site.Position.X)
+ FMath::Square(Hub.Position.Y - Site.Position.Y));
if (SegmentLength <= KINDA_SMALL_NUMBER)
{
continue;
}
const float MinX = FMath::Min(Hub.Position.X, Site.Position.X) - LandingBridgeRadius;
const float MaxX = FMath::Max(Hub.Position.X, Site.Position.X) + LandingBridgeRadius;
const float MinY = FMath::Min(Hub.Position.Y, Site.Position.Y) - LandingBridgeRadius;
const float MaxY = FMath::Max(Hub.Position.Y, Site.Position.Y) + LandingBridgeRadius;
if (MaxX < Min.X || MinX > Max.X || MaxY < Min.Y || MinY > Max.Y)
{
continue;
}
LandingBridgeSlopeBound = FMath::Max(
LandingBridgeSlopeBound,
static_cast<double>(FMath::Abs(Hub.FloorZ - Site.FloorZ) / SegmentLength));
}
}
const double FloorSlopeBound = FMath::Max(
FMath::Max(SumAbsDeltaSlope, 2.0 * MaxAbsFloorDelta * SumInverseRadiusSlope),
LandingBridgeSlopeBound);
const float MacroLipschitzBound = FMath::Max(
bParameterized ? ParameterizedFieldLipschitzBound : BaseFieldLipschitzBound,
FMath::Sqrt(1.0f + FMath::Square(static_cast<float>(FloorSlopeBound))) + 0.25f);
const FVector Centre = (Min + Max) * 0.5f;
const FVector HalfExtent = (Max - Min) * 0.5f;
const float Radius = HalfExtent.Size();
if (bOutEvaluatedMacroCentre != nullptr)
{
*bOutEvaluatedMacroCentre = true;
}
const float Macro = bParameterized
? EvaluateParameterizedMacro(*Plan, Centre.X, Centre.Y, Centre.Z, nullptr)
: EvaluateMacro(*Plan, Centre.X, Centre.Y, Centre.Z, nullptr);
const float FinalBound = MacroLipschitzBound * Radius
+ CurrentDetailAmplitude(bParameterized) * FMath::Abs(DetailScale)
+ DetailBoundNumericalMargin * FMath::Max(1.0f, FMath::Abs(DetailScale));
if (Macro > FinalBound)
{
bOutAir = true;
if (OutFallbackReason != nullptr)
{
*OutFallbackReason = EBrickFallbackReason::None;
}
return true;
}
if (Macro < -FinalBound)
{
bOutAir = false;
if (OutFallbackReason != nullptr)
{
*OutFallbackReason = EBrickFallbackReason::None;
}
return true;
}
if (OutFallbackReason != nullptr)
{
*OutFallbackReason = EBrickFallbackReason::MixedMacroInterval;
}
return false;
}
bool TryClassifyBrick(const UVoxelGenerator& Generator, FIntVector OriginVoxels,
FIntVector Size, bool& bOutAir, float DetailScale,
bool* bOutEvaluatedMacroCentre,
EBrickFallbackReason* OutFallbackReason)
{
return TryClassifyBrickInternal(Generator, OriginVoxels, Size, bOutAir,
DetailScale, bOutEvaluatedMacroCentre, OutFallbackReason, false, 1);
}
bool TryClassifyParameterizedBrick(const UVoxelGenerator& Generator,
FIntVector OriginVoxels, FIntVector Size,
bool& bOutAir, float DetailScale,
bool* bOutEvaluatedMacroCentre,
EBrickFallbackReason* OutFallbackReason,
int32 SampleStep)
{
return TryClassifyBrickInternal(Generator, OriginVoxels, Size, bOutAir,
DetailScale, bOutEvaluatedMacroCentre, OutFallbackReason, true, SampleStep);
}
}
#endif // WITH_EDITOR