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
3619 lines
166 KiB
C++
3619 lines
166 KiB
C++
#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
|