#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 #include 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 CVarResearchRedefinedDensityField( TEXT("voxel.ResearchRedefinedDensityField"), 0, TEXT("Use the default-off editor-only step 2d redefined density field."), ECVF_Default); TAutoConsoleVariable CVarResearchRedefinedDensityDetailScale( TEXT("voxel.ResearchRedefinedDensityDetailScale"), 1.0f, TEXT("Step 2d research detail amplitude scale; zero is a deliberate smoothing control."), ECVF_Default); TAutoConsoleVariable CVarResearchParameterizedCave( TEXT("voxel.ResearchParameterizedCave"), 0, TEXT("Use the default-off editor-only step 2e parameterized cave field."), ECVF_Default); TAutoConsoleVariable 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 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 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 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 Strates; TArray 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(X) + 0x85EBCA6Bu); H = (H << 13) | (H >> 19); H ^= Mix(static_cast(Y) + 0xC2B2AE35u); H = (H << 11) | (H >> 21); H ^= Mix(static_cast(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(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(IX)); const float AY = Fade(SY - static_cast(IY)); const float AZ = Fade(SZ - static_cast(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 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((Corner & 1) ? End.X : Origin.X); const float Y = static_cast((Corner & 2) ? End.Y : Origin.Y); const float Z = static_cast((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(OutCache.Size.X) * OutCache.Size.Y * OutCache.Size.Z; check(ValueCount > 0 && ValueCount <= MAX_int32); OutCache.Values.SetNumUninitialized(static_cast(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(IX)); const float AY = Fade(SY - static_cast(IY)); const float AZ = Fade(SZ - static_cast(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(FMath::Max(0, Size.X - 1)) * Step > MarginVoxels || static_cast(FMath::Max(0, Size.Y - 1)) * Step > MarginVoxels || static_cast(FMath::Max(0, Size.Z - 1)) * Step > MarginVoxels) { return false; } const auto GetCenteredRegionMin = [](int32 Value) { const float Shifted = static_cast(Value) + static_cast(RegionSpanVoxels / 2); const int32 RegionIndex = FMath::FloorToInt( Shifted / static_cast(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(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(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(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(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& 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(Plan.MinChunkZ * CHUNK_SIZE); const float Top = static_cast(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(Generator.Seed) ^ static_cast(Plan.StrateIndex * 7919 + 104729); Plan.GenerationSeed = StrateSeed ^ 0x2E6F9A31u; Plan.GenerationProfile = ParameterSet >= 0 ? FMath::Clamp(ParameterSet, 0, UE_ARRAY_COUNT(GParameterProfiles) - 1) : static_cast(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(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(Hash3( Plan.StrateIndex, Plan.MinChunkZ, 17, StrateSeed) >> 8) * (Plan.CellSpacing / 16777216.0f); Plan.OffsetY = static_cast(Hash3( Plan.StrateIndex, Plan.MinChunkZ, 31, StrateSeed) >> 8) * (Plan.CellSpacing / 16777216.0f); } } const TArray& 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(Plan.MinChunkZ * CHUNK_SIZE); const float Top = static_cast(Plan.MaxChunkZExclusive * CHUNK_SIZE); Plan.FloorZ = FMath::Clamp( static_cast(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 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(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(FMath::Min(A.CellX, B.CellX)); const float MinCellY = static_cast(FMath::Min(A.CellY, B.CellY)); const int32 Axis = A.CellX != B.CellX ? 0 : 1; const uint32 EdgeHash = Hash3( static_cast(MinCellX), static_cast(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(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((AY + 2) * 5 + AX + 2); const uint8 CellBIndex = static_cast((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(FMath::Min(A.CellX, B.CellX)); const float MinCellY = static_cast(FMath::Min(A.CellY, B.CellY)); const int32 Axis = A.CellX != B.CellX ? 0 : 1; const uint32 EdgeHash = Hash3( static_cast(MinCellX), static_cast(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(Plan.MinChunkZ * CHUNK_SIZE) + VoxelPassageGeometry::MetersToVoxels(2.0f); const float Top = static_cast(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(Plan.MinChunkZ * CHUNK_SIZE); const float StrateTop = static_cast(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(X) - Context.BrickOrigin.X; const int32 LocalY = static_cast(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& 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(UE_ARRAY_COUNT(GParameterProfiles) - 1)), ECVF_SetByCode); } int32 GetParameterSet() { return CVarResearchParameterizedCaveSet.GetValueOnAnyThread(); } int32 GetParameterSetCount() { return static_cast(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(Context->PlanToken) : ParameterizedContext != nullptr && ParameterizedContext->bPlanResolved ? static_cast(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(Generator.Seed) ^ static_cast(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(Plan->MinChunkZ * CHUNK_SIZE); const float StrateTop = static_cast(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(Cell.FeatureFlags), static_cast(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(Generator.Seed) ^ static_cast(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(Context->PlanToken) : nullptr; if (Plan == nullptr) { EnsurePlan(Generator); Plan = FindStrate(Chunk); } const int32 EndChunkZ = FMath::FloorToInt( (WorldZStart + static_cast(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(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(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(OriginVoxels.X) / CHUNK_SIZE), FMath::FloorToInt(static_cast(OriginVoxels.Y) / CHUNK_SIZE), FMath::FloorToInt(static_cast(OriginVoxels.Z) / CHUNK_SIZE)); const FIntVector EndChunk( FMath::FloorToInt(static_cast(End.X) / CHUNK_SIZE), FMath::FloorToInt(static_cast(End.Y) / CHUNK_SIZE), FMath::FloorToInt(static_cast(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(OriginVoxels.X - SampleStep), static_cast(OriginVoxels.Y - SampleStep), static_cast(OriginVoxels.Z - SampleStep)); const FVector Max( static_cast(OriginVoxels.X + Size.X * SampleStep), static_cast(OriginVoxels.Y + Size.Y * SampleStep), static_cast(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(WorldX), static_cast(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& InOutDetail, TArray& InOutColumnValues, TArray& 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(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(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(OriginVoxels.X + X * SampleStep), static_cast(OriginVoxels.Y + Y * SampleStep), static_cast(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& 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(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(OriginVoxels.X) / CHUNK_SIZE), FMath::FloorToInt(static_cast(OriginVoxels.Y) / CHUNK_SIZE), FMath::FloorToInt(static_cast(OriginVoxels.Z) / CHUNK_SIZE)); const FIntVector EndChunk( FMath::FloorToInt(static_cast(OriginVoxels.X + (Size.X - 1) * SampleStep) / CHUNK_SIZE), FMath::FloorToInt(static_cast(OriginVoxels.Y + (Size.Y - 1) * SampleStep) / CHUNK_SIZE), FMath::FloorToInt(static_cast(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(OriginVoxels.X - 1), static_cast(OriginVoxels.Y - 1), static_cast(OriginVoxels.Z - 1)); const FVector Max( static_cast(OriginVoxels.X + Size.X), static_cast(OriginVoxels.Y + Size.Y), static_cast(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(WorldZ) / CHUNK_SIZE)); const FStratePlan* SamplePlan = FindStrate(SampleChunk); if (SamplePlan != nullptr) { const uint32 SampleSeed = static_cast(Generator.Seed) ^ static_cast(SamplePlan->StrateIndex * 7919 + 104729); OutDetail[(Z * Size.Y + Y) * Size.X + X] = DetailAt( static_cast(OriginVoxels.X + X * SampleStep), static_cast(OriginVoxels.Y + Y * SampleStep), static_cast(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(OriginVoxels.X + X * SampleStep); const float WorldY = static_cast(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(Generator.Seed) ^ static_cast(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(OriginVoxels.X + X * SampleStep); const float WorldY = static_cast(OriginVoxels.Y + Y * SampleStep); const float WorldZ = static_cast(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(OriginVoxels.X + X * SampleStep), static_cast(OriginVoxels.Y + Y * SampleStep), static_cast(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* OutBand) { if (OutBand != nullptr) { OutBand->SetNumUninitialized(SampleCount); } const int32 MinCellX = FMath::FloorToInt( static_cast(OriginVoxels.X) / static_cast(CellSize)); const int32 MinCellY = FMath::FloorToInt( static_cast(OriginVoxels.Y) / static_cast(CellSize)); const int32 MinCellZ = FMath::FloorToInt( static_cast(OriginVoxels.Z) / static_cast(CellSize)); const int32 MaxCellX = FMath::FloorToInt( static_cast(OriginVoxels.X + (Size.X - 1) * SampleStep) / static_cast(CellSize)); const int32 MaxCellY = FMath::FloorToInt( static_cast(OriginVoxels.Y + (Size.Y - 1) * SampleStep) / static_cast(CellSize)); const int32 MaxCellZ = FMath::FloorToInt( static_cast(OriginVoxels.Z + (Size.Z - 1) * SampleStep) / static_cast(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(OriginVoxels.X + X * SampleStep) / static_cast(CellSize); const int32 Cell = FMath::FloorToInt(SX); CellX[X] = Cell - MinCellX; FadeX[X] = Fade(SX - static_cast(Cell)); } for (int32 Y = 0; Y < Size.Y; ++Y) { const float SY = static_cast(OriginVoxels.Y + Y * SampleStep) / static_cast(CellSize); const int32 Cell = FMath::FloorToInt(SY); CellY[Y] = Cell - MinCellY; FadeY[Y] = Fade(SY - static_cast(Cell)); } for (int32 Z = 0; Z < Size.Z; ++Z) { const float SZ = static_cast(OriginVoxels.Z + Z * SampleStep) / static_cast(CellSize); const int32 Cell = FMath::FloorToInt(SZ); CellZ[Z] = Cell - MinCellZ; FadeZ[Z] = Fade(SZ - static_cast(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 Broad; TArray Medium; TArray 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(OriginVoxels.X - SampleStep) / CHUNK_SIZE); const int32 MaxChunkX = FMath::FloorToInt( static_cast(OriginVoxels.X + Size.X * SampleStep) / CHUNK_SIZE); const int32 MinChunkY = FMath::FloorToInt( static_cast(OriginVoxels.Y - SampleStep) / CHUNK_SIZE); const int32 MaxChunkY = FMath::FloorToInt( static_cast(OriginVoxels.Y + Size.Y * SampleStep) / CHUNK_SIZE); const int32 MinChunkZ = FMath::FloorToInt( static_cast(OriginVoxels.Z - SampleStep) / CHUNK_SIZE); const int32 MaxChunkZ = FMath::FloorToInt( static_cast(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(OriginVoxels.X - SampleStep), static_cast(OriginVoxels.Y - SampleStep), static_cast(OriginVoxels.Z - SampleStep)); const FVector Max( static_cast(OriginVoxels.X + Size.X * SampleStep), static_cast(OriginVoxels.Y + Size.Y * SampleStep), static_cast(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(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(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