wip(step2e): parameterised chamber-graph model -- compiles, traversal test PASS; benchmark not run

Stopped by low memory (another app held 12.5 GB; processes failed with 0xC0000142).
Editor-only, default off: a seeded connected room graph (varied chambers on a spanning network,
optional loops, shafts, rock features, ledges, basin floors), eight parameter profiles, the
step-2d brick driver and detail bands, a new test VoxelForge.ParameterizedGeneration.TraversalSeeds
(PASS in step2e_functional_01, which also proves the tree compiles). Existing live tests untouched.
Not yet run: the eight-profile ring benchmark, variety metrics, slices, detail/artefact/determinism
checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-09-30 11:45:45 +02:00
1 parent 78641da0bb
commit f8a46ecf8d
9 files changed
+1585 -51

No files matched your search

@@ -0,0 +1,151 @@
#if WITH_DEV_AUTOMATION_TESTS
#include "Misc/AutomationTest.h"
#include "VoxelForgeTestFixture.h"
#include "VoxelRedefinedDensityField.h"
#include "VoxelStrateMeasure.h"
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FVoxelForgeParameterizedGenerationTraversalSeedsTest,
"VoxelForge.ParameterizedGeneration.TraversalSeeds",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FVoxelForgeParameterizedGenerationTraversalSeedsTest::RunTest(const FString& Parameters)
{
(void)Parameters;
struct FRestoreResearchFields
{
bool bWasParameterizedEnabled = VoxelForgeRedefinedDensityField::IsParameterizedEnabled();
bool bWasRedefinedEnabled = VoxelForgeRedefinedDensityField::IsEnabled();
int32 PreviousSet = VoxelForgeRedefinedDensityField::GetParameterSet();
float PreviousScale = VoxelForgeRedefinedDensityField::GetDetailScale();
~FRestoreResearchFields()
{
VoxelForgeRedefinedDensityField::SetDetailScale(PreviousScale);
VoxelForgeRedefinedDensityField::SetParameterSet(PreviousSet);
VoxelForgeRedefinedDensityField::SetEnabled(bWasRedefinedEnabled);
VoxelForgeRedefinedDensityField::SetParameterizedEnabled(bWasParameterizedEnabled);
}
} Restore;
VoxelForgeRedefinedDensityField::SetEnabled(false);
VoxelForgeRedefinedDensityField::SetParameterizedEnabled(true);
VoxelForgeRedefinedDensityField::SetParameterSet(-1);
VoxelForgeRedefinedDensityField::SetDetailScale(1.0f);
constexpr int32 Seeds[] = {1337, 12345, 20260930};
int32 CheckedLandings = 0;
int32 CheckedRoutes = 0;
int32 RoutesWithResolvedEndpoints = 0;
for (const int32 Seed : Seeds)
{
VoxelForgeTest::FTestWorld World;
World.Build(Seed, /*InGapChunks=*/2, /*InStrateHeightInChunks=*/4);
if (!World.IsValid())
{
AddError(FString::Printf(
TEXT("Seed %d: test world did not build: %s"), Seed, *World.WhyInvalid()));
return false;
}
const TArray<FStrateSlot>& Layout = World.StrateManager->GetLayout();
const TArray<FVoxelPassage>& Passages = World.StrateManager->GetPassages();
for (const FVoxelPassage& Passage : Passages)
{
if (Passage.UpperStrateIndex == Passage.LowerStrateIndex)
{
continue;
}
const FVector UpperAir = Passage.UpperLanding.StandingPoint;
const FVector LowerAir = Passage.LowerLanding.StandingPoint;
const float UpperDensity = World.Generator->GetDensityAt(
UpperAir.X, UpperAir.Y, UpperAir.Z);
const float UpperFooting = World.Generator->GetDensityAt(
UpperAir.X, UpperAir.Y, Passage.UpperLanding.FloorZ - 0.75f);
const float LowerDensity = World.Generator->GetDensityAt(
LowerAir.X, LowerAir.Y, LowerAir.Z);
const float LowerFooting = World.Generator->GetDensityAt(
LowerAir.X, LowerAir.Y, Passage.LowerLanding.FloorZ - 0.75f);
++CheckedLandings;
if (!FMath::IsFinite(UpperDensity) || !FMath::IsFinite(UpperFooting)
|| !FMath::IsFinite(LowerDensity) || !FMath::IsFinite(LowerFooting)
|| UpperDensity <= 0.0f || LowerDensity <= 0.0f
|| UpperFooting >= 0.0f || LowerFooting >= 0.0f)
{
AddError(FString::Printf(
TEXT("Parameterized model seed %d: passage %d landing failed the final-field "
"air/floor bracket (upper %.4f/%.4f, lower %.4f/%.4f)."),
Seed, CheckedLandings - 1, UpperDensity, UpperFooting,
LowerDensity, LowerFooting));
}
}
for (int32 StrateIndex = 0; StrateIndex < Layout.Num(); ++StrateIndex)
{
FVector Arrival = FVector::ZeroVector;
FVector Departure = FVector::ZeroVector;
bool bHasArrival = false;
bool bHasDeparture = false;
for (const FVoxelPassage& Passage : Passages)
{
if (Passage.UpperStrateIndex == Passage.LowerStrateIndex)
{
continue;
}
if (Passage.LowerStrateIndex == StrateIndex && !bHasArrival)
{
Arrival = Passage.LowerLanding.StandingPoint;
bHasArrival = true;
}
if (Passage.UpperStrateIndex == StrateIndex && !bHasDeparture)
{
Departure = Passage.UpperLanding.StandingPoint;
bHasDeparture = true;
}
}
if (!bHasArrival || !bHasDeparture)
{
continue;
}
FVoxelStrateMeasureSettings Settings;
Settings.SampleStep = 4;
Settings.RadiusInVoxels = 192;
Settings.CenterXY = FVector2D(
(Arrival.X + Departure.X) * 0.5f,
(Arrival.Y + Departure.Y) * 0.5f);
Settings.MaxCells = 4000000;
Settings.HeadroomCells = 2;
const FVoxelConnectivityDiagnostics Route = VF_DiagnoseConnectivity(
*World.Generator, *World.StrateManager, StrateIndex,
Arrival, Departure, Settings);
++CheckedRoutes;
RoutesWithResolvedEndpoints += Route.bStartSnapped || Route.bGoalSnapped;
if (!Route.bValid || Route.Result != EVoxelConnectivityResult::Connected)
{
AddError(FString::Printf(
TEXT("Parameterized model seed %d strate %d (%s): arrival-to-exit walking "
"route was not connected (result=%d, valid=%s, start resolved=%s, "
"goal resolved=%s)."),
Seed, StrateIndex,
Layout[StrateIndex].Definition
? *Layout[StrateIndex].Definition->GetName() : TEXT("no definition"),
static_cast<int32>(Route.Result),
Route.bValid ? TEXT("yes") : TEXT("no"),
Route.bStartSnapped ? TEXT("yes") : TEXT("no"),
Route.bGoalSnapped ? TEXT("yes") : TEXT("no")));
}
}
}
TestTrue(TEXT("Several seeds exercised parameterized passage landings"), CheckedLandings >= 6);
TestTrue(TEXT("Several seeds exercised parameterized arrival-to-exit walking"), CheckedRoutes >= 6);
AddInfo(FString::Printf(
TEXT("Parameterized generation functional sweep: %d inter-strate passage landings "
"bracketed open air over %d seeds; %d arrival-to-exit walking routes checked; "
"%d used a resolved coarse endpoint after the full-density segment check."),
CheckedLandings, UE_ARRAY_COUNT(Seeds), CheckedRoutes, RoutesWithResolvedEndpoints));
return CheckedLandings >= 6 && CheckedRoutes >= 6;
}
#endif // WITH_DEV_AUTOMATION_TESTS
+11 -2
View File
@@ -2817,8 +2817,17 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ,
float* OutPreEditDensity) const
{
#if WITH_EDITOR
// Step 2d is an editor-only, default-off side path. It does not alter the live evaluator's
// state or caches; the field is selected only for an explicit research/test run.
// Step 2e and step 2d are separate editor-only, default-off side paths. The owner field stays
// authoritative unless a research commandlet or automation test explicitly selects one.
if (VoxelForgeRedefinedDensityField::IsParameterizedEnabled())
{
VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker();
const float ResearchDensity = VoxelForgeRedefinedDensityField::EvaluateParameterized(
*this, WorldX, WorldY, WorldZ, OutPreEditDensity,
VoxelForgeRedefinedDensityField::GetDetailScale());
VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker();
return ResearchDensity;
}
if (VoxelForgeRedefinedDensityField::IsEnabled())
{
VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker();
@@ -20,6 +20,7 @@ namespace
constexpr float DetailAmplitude = 1.4f + 0.9f + 0.45f + 2.5f;
constexpr float DetailBoundNumericalMargin = 1.0e-3f;
constexpr float BaseFieldLipschitzBound = 1.25f;
constexpr float ParameterizedFieldLipschitzBound = 1.60f;
constexpr float QuinticFadeSlopeBound = 1.875f;
constexpr float MinimumCaveHeight = 48.0f;
constexpr float LandingFloorBlendVoxels = 96.0f;
@@ -38,6 +39,39 @@ namespace
TEXT("voxel.ResearchRedefinedDensityDetailScale"), 1.0f,
TEXT("Step 2d research detail amplitude scale; zero is a deliberate smoothing control."),
ECVF_Default);
TAutoConsoleVariable<int32> CVarResearchParameterizedCave(
TEXT("voxel.ResearchParameterizedCave"), 0,
TEXT("Use the default-off editor-only step 2e parameterized cave field."),
ECVF_Default);
TAutoConsoleVariable<int32> CVarResearchParameterizedCaveSet(
TEXT("voxel.ResearchParameterizedCaveSet"), -1,
TEXT("Step 2e parameter profile 0-7; -1 rolls a profile deterministically per strate."),
ECVF_Default);
struct FParameterProfile
{
const TCHAR* Name;
FGenerationParameters Parameters;
};
const FParameterProfile GParameterProfiles[] = {
{ TEXT("OpenVaults"), { 176.0f, 0.34f, 0.45f, 340.0f, 12.0f, 40.0f, 36.0f,
0.35f, 0.14f, 22.0f, 0.04f, 0.12f, 0.08f, 0.15f, 0.00f, 0.0f, 0.50f } },
{ TEXT("TightNetwork"), { 104.0f, 0.28f, 0.42f, 112.0f, 8.0f, 20.0f, 22.0f,
0.12f, 0.03f, 14.0f, 0.06f, 0.08f, 0.02f, 0.12f, 0.00f, 0.0f, 0.50f } },
{ TEXT("ChamberHalls"), { 144.0f, 0.38f, 0.45f, 320.0f, 18.0f, 34.0f, 32.0f,
0.28f, 0.18f, 22.0f, 0.08f, 0.22f, 0.10f, 0.22f, 0.05f, 20.0f, 0.40f } },
{ TEXT("ShaftForest"), { 128.0f, 0.32f, 0.43f, 176.0f, 12.0f, 26.0f, 28.0f,
0.12f, 0.55f, 20.0f, 0.05f, 0.18f, 0.04f, 0.20f, 0.00f, 0.0f, 0.50f } },
{ TEXT("FloatingArchipelago"), { 136.0f, 0.37f, 0.45f, 192.0f, 10.0f, 28.0f, 28.0f,
0.15f, 0.10f, 16.0f, 0.05f, 0.10f, 0.72f, 0.28f, 0.00f, 0.0f, 0.50f } },
{ TEXT("PillaredCathedral"), { 160.0f, 0.38f, 0.45f, 248.0f, 12.0f, 32.0f, 34.0f,
0.25f, 0.08f, 18.0f, 0.72f, 0.30f, 0.08f, 0.45f, 0.00f, 0.0f, 0.50f } },
{ TEXT("TerracedBasin"), { 136.0f, 0.34f, 0.45f, 216.0f, 14.0f, 28.0f, 30.0f,
0.20f, 0.10f, 18.0f, 0.10f, 0.84f, 0.05f, 0.25f, 0.85f, 48.0f, 0.30f } },
{ TEXT("HyperComplex"), { 96.0f, 0.30f, 0.45f, 192.0f, 20.0f, 30.0f, 28.0f,
0.40f, 0.55f, 20.0f, 0.45f, 0.68f, 0.50f, 0.75f, 0.42f, 28.0f, 0.40f } },
};
struct FLandingSite
{
@@ -100,6 +134,14 @@ namespace
float OffsetX = 0.0f;
float OffsetY = 0.0f;
float CorridorHalfWidth = 40.0f;
FGenerationParameters Generation;
int32 GenerationProfile = 0;
uint32 GenerationSeed = 0;
float GenerationFloorZ = 0.0f;
float GridCos = 1.0f;
float GridSin = 0.0f;
float GridOffsetX = 0.0f;
float GridOffsetY = 0.0f;
FVerticalShaftParams VerticalShaftParams;
int32 LandingHubIndex = INDEX_NONE;
TArray<FLandingSite> Landings;
@@ -111,6 +153,7 @@ namespace
uint64 ManagerLifetimeId = 0;
uint32 LayoutVersion = 0xFFFFFFFFu;
int32 Seed = INT32_MIN;
int32 ParameterSet = INT32_MIN;
TArray<FStratePlan> Strates;
TArray<FPassageBound> PassageBounds;
};
@@ -438,10 +481,12 @@ namespace
}
const uint64 Lifetime = Manager->GetCacheLifetimeId();
const uint32 Version = Manager->GetLayoutVersion();
const int32 ParameterSet = CVarResearchParameterizedCaveSet.GetValueOnAnyThread();
if (GPlanCache.Manager == Manager
&& GPlanCache.ManagerLifetimeId == Lifetime
&& GPlanCache.LayoutVersion == Version
&& GPlanCache.Seed == Generator.Seed)
&& GPlanCache.Seed == Generator.Seed
&& GPlanCache.ParameterSet == ParameterSet)
{
return;
}
@@ -451,6 +496,7 @@ namespace
GPlanCache.ManagerLifetimeId = Lifetime;
GPlanCache.LayoutVersion = Version;
GPlanCache.Seed = Generator.Seed;
GPlanCache.ParameterSet = ParameterSet;
const TArray<FStrateSlot>& Layout = Manager->GetLayout();
GPlanCache.Strates.Reserve(Layout.Num());
for (const FStrateSlot& Slot : Layout)
@@ -506,6 +552,58 @@ namespace
}
const uint32 StrateSeed = static_cast<uint32>(Generator.Seed)
^ static_cast<uint32>(Plan.StrateIndex * 7919 + 104729);
Plan.GenerationSeed = StrateSeed ^ 0x2E6F9A31u;
Plan.GenerationProfile = ParameterSet >= 0
? FMath::Clamp(ParameterSet, 0, UE_ARRAY_COUNT(GParameterProfiles) - 1)
: static_cast<int32>(Hash3(
Plan.StrateIndex, Plan.MinChunkZ, 0x2E, StrateSeed)
% UE_ARRAY_COUNT(GParameterProfiles));
Plan.Generation = GParameterProfiles[Plan.GenerationProfile].Parameters;
const float StrateHeight = FMath::Max(
48.0f, Top - Bottom - 8.0f);
Plan.Generation.RoomHeightVoxels = FMath::Clamp(
Plan.Generation.RoomHeightVoxels, 48.0f, StrateHeight);
Plan.Generation.RoomPitchVoxels = FMath::Max(
Plan.Generation.RoomPitchVoxels, 80.0f);
Plan.Generation.MinimumRoomRadiusFraction = FMath::Clamp(
Plan.Generation.MinimumRoomRadiusFraction, 0.20f, 0.41f);
Plan.Generation.MaximumRoomRadiusFraction = FMath::Clamp(
Plan.Generation.MaximumRoomRadiusFraction,
Plan.Generation.MinimumRoomRadiusFraction, 0.45f);
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, 48.0f);
Plan.Generation.WaterLevelFraction = FMath::Clamp(
Plan.Generation.WaterLevelFraction, 0.0f, 1.0f);
Plan.GenerationFloorZ = (Bottom + Top - Plan.Generation.RoomHeightVoxels) * 0.5f;
static constexpr float GridRotations[][2] = {
{1.0f, 0.0f}, {0.923879533f, 0.382683432f},
{0.707106781f, 0.707106781f}, {0.382683432f, 0.923879533f},
{0.0f, 1.0f}, {-0.382683432f, 0.923879533f},
{-0.707106781f, 0.707106781f}, {-0.923879533f, 0.382683432f},
};
const uint32 RotationHash = Hash3(
Plan.StrateIndex, Plan.MinChunkZ, 0x8D, StrateSeed);
const int32 RotationIndex = static_cast<int32>(RotationHash % UE_ARRAY_COUNT(GridRotations));
Plan.GridCos = GridRotations[RotationIndex][0];
Plan.GridSin = GridRotations[RotationIndex][1];
Plan.GridOffsetX = VoxelHash::ToFloat01(VoxelHash::Mix(RotationHash ^ 0x1975u))
* Plan.Generation.RoomPitchVoxels;
Plan.GridOffsetY = VoxelHash::ToFloat01(VoxelHash::Mix(RotationHash ^ 0x67A3u))
* Plan.Generation.RoomPitchVoxels;
if (Plan.Archetype == ECaveGeneratorType::VerticalShafts)
{
// Keep the archetype's central origin spine open for its authored route probe.
@@ -620,6 +718,7 @@ namespace
Plan.FloorZ = FMath::Clamp(
static_cast<float>(FloorSum / FloorCount), Bottom + 16.0f, Top - 72.0f);
Plan.CeilingZ = Plan.FloorZ + MinimumCaveHeight;
Plan.GenerationFloorZ = Plan.FloorZ;
}
}
for (FLandingSite& Site : Plan.Landings)
@@ -668,6 +767,409 @@ namespace
}
}
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 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 SpireLength = 0.0f;
float SpireRadius = 0.0f;
uint8 FeatureFlags = 0;
};
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;
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;
}
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 float JitterX = (ParameterRoll(Hash, 0x91A2u) - 0.5f) * 0.08f;
const float JitterY = (ParameterRoll(Hash, 0xB4C3u) - 0.5f) * 0.08f;
const float LocalX = Plan.GridOffsetX + (CellX + 0.5f + JitterX) * Pitch;
const float LocalY = Plan.GridOffsetY + (CellY + 0.5f + JitterY) * Pitch;
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;
Cell.RadiusY = Cell.RadiusX * FMath::Lerp(
0.82f, 0.98f, ParameterRoll(Hash, 0x27C5u));
Cell.FloorZ = Plan.GenerationFloorZ
+ (ParameterRoll(Hash, 0xF103u) * 2.0f - 1.0f) * P.FloorReliefVoxels;
Cell.CeilingZ = Cell.FloorZ + P.RoomHeightVoxels;
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(14.0f, 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;
for (int32 Blob = 0; Blob < 2; ++Blob)
{
const uint32 BlobSalt = 0x1567u + static_cast<uint32>(Blob) * 0x101u;
const float OffsetX = (ParameterRoll(Hash, BlobSalt) - 0.5f) * Cell.RadiusX * 0.60f;
const float OffsetY = (ParameterRoll(Hash, BlobSalt + 1u) - 0.5f) * Cell.RadiusY * 0.60f;
Cell.FloatingX[Blob] = Cell.CenterX + OffsetX;
Cell.FloatingY[Blob] = Cell.CenterY + OffsetY;
Cell.FloatingZ[Blob] = FMath::Lerp(
Cell.FloorZ + P.RoomHeightVoxels * 0.28f,
Cell.CeilingZ - P.RoomHeightVoxels * 0.28f,
ParameterRoll(Hash, BlobSalt + 2u));
Cell.FloatingRadius[Blob] = Cell.RadiusX * FMath::Lerp(
0.07f, 0.15f, ParameterRoll(Hash, BlobSalt + 3u));
}
}
if (ParameterRoll(Hash, 0xC6A9u) < P.SpireProbability)
{
Cell.FeatureFlags |= HasSpireFlag;
Cell.SpireLength = FMath::Min(56.0f, P.RoomHeightVoxels * 0.30f);
Cell.SpireRadius = FMath::Min(14.0f, Cell.RadiusX * 0.22f);
}
if (ParameterRoll(Hash, 0x4537u) < P.BasinProbability)
{
Cell.FeatureFlags |= HasBasinFlag;
}
return Cell;
}
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 FStratePlan& Plan, const FGeneratedCaveCell& Cell,
float X, float Y, float Z)
{
const float DX = X - Cell.CenterX;
const float DY = Y - Cell.CenterY;
const float Radius = FMath::Sqrt(DX * DX + DY * DY);
const float Horizontal = Cell.RadiusX - Radius;
float FloorZ = Cell.FloorZ;
if ((Cell.FeatureFlags & HasBasinFlag) != 0)
{
FloorZ += Plan.Generation.BasinDepthVoxels
* FMath::Clamp(Radius / FMath::Max(Cell.RadiusX, 1.0f), 0.0f, 1.0f);
}
const float Vertical = FMath::Min(Z - FloorZ, Cell.CeilingZ - Z);
float Room = SmoothMinimum(Horizontal, Vertical, 8.0f);
if (Room > 0.0f && (Cell.FeatureFlags & HasPillarFlag) != 0)
{
const float PX = X - Cell.PillarX;
const float PY = Y - Cell.PillarY;
const float Pillar = FMath::Max3(
FMath::Sqrt(PX * PX + PY * PY) - Cell.PillarRadius,
Cell.FloorZ - Z, Z - Cell.CeilingZ);
Room = FMath::Min(Room, Pillar);
}
if ((Cell.FeatureFlags & HasLedgeFlag) != 0
&& Horizontal >= -2.0f && FMath::Abs(Z - Cell.LedgeZ) <= 3.0f)
{
const float Inner = Cell.RadiusX * 0.58f;
const float Outer = Cell.RadiusX + 1.0f;
const float Ledge = FMath::Max3(
Inner - Radius, Radius - Outer, FMath::Abs(Z - Cell.LedgeZ) - 2.5f);
Room = FMath::Min(Room, Ledge);
}
if (Room > 0.0f && (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];
const float BZ = Z - Cell.FloatingZ[Blob];
const float BlobSolid = FMath::Sqrt(BX * BX + BY * BY + BZ * BZ)
- Cell.FloatingRadius[Blob];
Room = FMath::Min(Room, BlobSolid);
}
}
if (Room > 0.0f && (Cell.FeatureFlags & HasSpireFlag) != 0)
{
const float BaseZ = Cell.FloorZ + 2.0f;
const float TipZ = FMath::Min(Cell.CeilingZ - 2.0f, BaseZ + Cell.SpireLength);
if (Z >= BaseZ && Z <= TipZ)
{
const float T = (Z - BaseZ) / FMath::Max(TipZ - BaseZ, 1.0f);
const float RadiusAtZ = FMath::Lerp(Cell.SpireRadius, 1.25f, T);
const float Spire = FMath::Max3(
Radius - RadiusAtZ, BaseZ - Z, Z - TipZ);
Room = FMath::Min(Room, Spire);
}
const float BaseTop = Cell.CeilingZ - 2.0f;
const float TipBottom = FMath::Max(Cell.FloorZ + 2.0f, BaseTop - Cell.SpireLength);
if (Z >= TipBottom && Z <= BaseTop)
{
const float T = (BaseTop - Z) / FMath::Max(BaseTop - TipBottom, 1.0f);
const float RadiusAtZ = FMath::Lerp(Cell.SpireRadius, 1.25f, T);
const float Stalactite = FMath::Max3(
Radius - RadiusAtZ, Z - BaseTop, TipBottom - Z);
Room = FMath::Min(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 X, float Y, float Z)
{
if (!IsGeneratedEdgeOpen(Plan, A.CellX, A.CellY, B.CellX, B.CellY))
{
return -FLT_MAX;
}
const float T = SegmentParameterXY(X, Y,
FVector(A.CenterX, A.CenterY, 0.0f), FVector(B.CenterX, B.CenterY, 0.0f));
const float Horizontal = Plan.Generation.TunnelRadiusVoxels
- FMath::Sqrt(DistanceSquaredToSegmentXY(
X, Y, FVector(A.CenterX, A.CenterY, 0.0f),
FVector(B.CenterX, B.CenterY, 0.0f)));
const float FloorZ = FMath::Lerp(A.FloorZ, B.FloorZ, T);
const float Vertical = FMath::Min(
Z - FloorZ,
FloorZ + 2.0f * Plan.Generation.TunnelHalfHeightVoxels - Z);
return SmoothMinimum(Horizontal, Vertical, 5.0f);
}
float EvaluateParameterizedMacro(
const FStratePlan& Plan, float X, float Y, float Z, float* OutDetailScale)
{
const FIntPoint BaseCell = GenerationCellAt(Plan, X, Y);
FGeneratedCaveCell Cells[9];
for (int32 CellY = -1; CellY <= 1; ++CellY)
for (int32 CellX = -1; CellX <= 1; ++CellX)
{
const int32 Index = (CellY + 1) * 3 + CellX + 1;
Cells[Index] = BuildGeneratedCell(
Plan, BaseCell.X + CellX, BaseCell.Y + CellY);
}
float Macro = -FLT_MAX;
for (const FGeneratedCaveCell& Cell : Cells)
{
const float DX = X - Cell.CenterX;
const float DY = Y - Cell.CenterY;
const float Horizontal = Cell.RadiusX - FMath::Sqrt(DX * DX + DY * DY);
if (Macro > -FLT_MAX * 0.5f && Horizontal < Macro - 6.0f)
{
continue;
}
Macro = SmoothMaximum(Macro,
GeneratedRoomCell(Plan, Cell, X, Y, Z), 6.0f);
}
const FGeneratedCaveCell& Centre = Cells[4];
const int32 EdgeIndices[] = {1, 7, 3, 5};
for (const int32 EdgeIndex : EdgeIndices)
{
Macro = SmoothMaximum(Macro,
GeneratedTunnelEdge(Plan, Centre, Cells[EdgeIndex], X, Y, Z), 5.0f);
}
const float Bottom = static_cast<float>(Plan.MinChunkZ * CHUNK_SIZE) + 8.0f;
const float Top = static_cast<float>(Plan.MaxChunkZExclusive * CHUNK_SIZE) - 8.0f;
for (const FGeneratedCaveCell& Cell : Cells)
{
const uint32 ShaftHash = Hash3(
Cell.CellX, Cell.CellY, 0x53, Plan.GenerationSeed);
if (VoxelHash::ToFloat01(ShaftHash) >= Plan.Generation.VerticalShaftProbability)
{
continue;
}
const float DX = X - Cell.CenterX;
const float DY = Y - Cell.CenterY;
const float Horizontal = Plan.Generation.ShaftRadiusVoxels
- FMath::Sqrt(DX * DX + DY * DY);
const float Vertical = FMath::Min(Z - Bottom, Top - Z);
Macro = SmoothMaximum(Macro,
SmoothMinimum(Horizontal, Vertical, 5.0f), 5.0f);
}
float DetailFade = 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 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, 6.0f);
if (Distance < FMath::Max(0.0f, Site.PadRadius - 3.0f))
{
DetailFade = 0.0f;
}
const float AnchorReach = Plan.Generation.RoomPitchVoxels * 1.25f
+ FMath::Max(32.0f, Plan.Generation.TunnelRadiusVoxels);
if (DistanceSquared > FMath::Square(AnchorReach))
{
continue;
}
const FIntPoint LandingCell = GenerationCellAt(Plan, Site.Position.X, Site.Position.Y);
FGeneratedCaveCell NearestCell;
float BestDistanceSq = 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 CandidateDistanceSq = FMath::Square(Candidate.CenterX - Site.Position.X)
+ FMath::Square(Candidate.CenterY - Site.Position.Y);
if (CandidateDistanceSq < BestDistanceSq)
{
BestDistanceSq = CandidateDistanceSq;
NearestCell = Candidate;
}
}
const FVector AnchorA(Site.Position.X, Site.Position.Y, Site.FloorZ + 24.0f);
const FVector AnchorB(NearestCell.CenterX, NearestCell.CenterY,
NearestCell.FloorZ + Plan.Generation.TunnelHalfHeightVoxels);
const FVector Segment = AnchorB - AnchorA;
const float SegmentLengthSq = Segment.SizeSquared();
const FVector Point(X, Y, Z);
const float T = SegmentLengthSq > 1.0e-8f
? FMath::Clamp(FVector::DotProduct(Point - AnchorA, Segment) / SegmentLengthSq,
0.0f, 1.0f) : 0.0f;
const float Anchor = FMath::Max(32.0f, Plan.Generation.TunnelRadiusVoxels)
- (Point - (AnchorA + Segment * T)).Size();
Macro = SmoothMaximum(Macro, Anchor, 5.0f);
}
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 = LandingBridgeRadius - FMath::Sqrt(
DistanceSquaredToSegmentXY(X, Y, Site.Position, Hub.Position));
const float T = 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, 5.0f), 5.0f);
}
}
if (OutDetailScale != nullptr)
{
*OutDetailScale = DetailFade;
}
return Macro;
}
const FStratePlan* FindStrate(const FIntVector& Chunk)
{
for (const FStratePlan& Plan : GPlanCache.Strates)
@@ -910,6 +1412,45 @@ 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 SetParameterSet(int32 ParameterSet)
{
CVarResearchParameterizedCaveSet->Set(
ParameterSet < 0 ? -1 : FMath::Clamp(ParameterSet, 0,
static_cast<int32>(UE_ARRAY_COUNT(GParameterProfiles) - 1)), ECVF_SetByCode);
}
int32 GetParameterSet()
{
return CVarResearchParameterizedCaveSet.GetValueOnAnyThread();
}
int32 GetParameterSetCount()
{
return static_cast<int32>(UE_ARRAY_COUNT(GParameterProfiles));
}
bool GetParameterSetInfo(int32 ParameterSet, const TCHAR*& OutName,
FGenerationParameters& OutParameters)
{
if (ParameterSet < 0 || ParameterSet >= GetParameterSetCount())
{
return false;
}
OutName = GParameterProfiles[ParameterSet].Name;
OutParameters = GParameterProfiles[ParameterSet].Parameters;
return true;
}
void ResetThreadCache()
{
GPlanCache = FThreadPlanCache();
@@ -919,20 +1460,28 @@ void ResetThreadCache()
static float EvaluateInternal(const UVoxelGenerator& Generator,
float WorldX, float WorldY, float WorldZ, float DetailValue, bool bHasDetailValue,
float* OutPreEditDensity, float DetailScale,
const FBrickEvaluationContext* Context = nullptr)
const FBrickEvaluationContext* Context = nullptr,
bool bParameterized = false,
const FParameterizedBrickContext* ParameterizedContext = nullptr)
{
if (Context == nullptr)
if (Context == nullptr && ParameterizedContext == nullptr)
{
EnsurePlan(Generator);
}
const bool bChunkResolved = Context != nullptr && Context->bChunkResolved
|| ParameterizedContext != nullptr && ParameterizedContext->bChunkResolved;
const FIntVector Chunk = Context != nullptr && Context->bChunkResolved
? Context->Chunk
: ParameterizedContext != nullptr && ParameterizedContext->bChunkResolved
? ParameterizedContext->Chunk
: FIntVector(
FMath::FloorToInt(WorldX / CHUNK_SIZE),
FMath::FloorToInt(WorldY / CHUNK_SIZE),
FMath::FloorToInt(WorldZ / CHUNK_SIZE));
const FStratePlan* Plan = Context != nullptr && Context->bPlanResolved
? static_cast<const FStratePlan*>(Context->PlanToken)
: ParameterizedContext != nullptr && ParameterizedContext->bPlanResolved
? static_cast<const FStratePlan*>(ParameterizedContext->PlanToken)
: FindStrate(Chunk);
float Result = 0.0f;
const UVoxelStrateManager* Manager = Generator.StrateManager;
@@ -940,8 +1489,10 @@ static float EvaluateInternal(const UVoxelGenerator& Generator,
if (Plan == nullptr)
{
const bool bIsGapChunk = Manager != nullptr
&& (Context != nullptr && Context->bChunkResolved
? Context->bIsGapChunk : Manager->IsGapChunk(Chunk));
&& (bChunkResolved
? (Context != nullptr && Context->bChunkResolved
? Context->bIsGapChunk : ParameterizedContext->bIsGapChunk)
: Manager->IsGapChunk(Chunk));
if (bIsGapChunk)
{
float Internal = 8.0f;
@@ -957,22 +1508,27 @@ static float EvaluateInternal(const UVoxelGenerator& Generator,
else
{
float DetailFade = 1.0f;
const float Macro = Context != nullptr && Context->bMacroXYPrecomputed
? EvaluateMacroFromBrick(*Plan, *Context, WorldX, WorldY, WorldZ, &DetailFade)
: EvaluateMacro(*Plan, WorldX, WorldY, WorldZ, &DetailFade);
const float Macro = bParameterized
? EvaluateParameterizedMacro(*Plan, WorldX, WorldY, WorldZ, &DetailFade)
: (Context != nullptr && Context->bMacroXYPrecomputed
? EvaluateMacroFromBrick(*Plan, *Context, WorldX, WorldY, WorldZ, &DetailFade)
: EvaluateMacro(*Plan, WorldX, WorldY, WorldZ, &DetailFade));
const uint32 DetailSeed = static_cast<uint32>(Generator.Seed)
^ static_cast<uint32>(Plan->StrateIndex * 7919 + 104729);
const float FinalDetail = bHasDetailValue
? DetailValue : DetailAt(WorldX, WorldY, WorldZ, DetailSeed);
Result = Macro + FinalDetail * DetailFade * DetailScale;
if (Manager != nullptr && (Context == nullptr || Context->bMayContainPassage))
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)
if (Context != nullptr && !bParameterized)
{
for (const int32 BoundsIndex : Context->PassageCandidates)
{
@@ -983,6 +1539,17 @@ static float EvaluateInternal(const UVoxelGenerator& Generator,
}
}
}
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)
@@ -1025,7 +1592,11 @@ static float EvaluateInternal(const UVoxelGenerator& Generator,
*OutPreEditDensity = Result;
}
if (Generator.DiffLayer != nullptr
&& (Context != nullptr ? Context->bHasDiffMods : Generator.DiffLayer->HasAnyMods()))
&& (bParameterized && ParameterizedContext != nullptr
? ParameterizedContext->bHasDiffMods
: Context != nullptr && !bParameterized
? Context->bHasDiffMods
: Generator.DiffLayer->HasAnyMods()))
{
Result -= Generator.DiffLayer->GetDensityOffset(Chunk, WorldX, WorldY, WorldZ);
}
@@ -1048,6 +1619,63 @@ float EvaluateWithDetailValue(const UVoxelGenerator& Generator,
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 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 BuildParameterizedBrickContext(
const UVoxelGenerator& Generator, FIntVector OriginVoxels, FIntVector Size,
FParameterizedBrickContext& OutContext)
{
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));
const FIntVector Chunk(
FMath::FloorToInt(static_cast<float>(OriginVoxels.X) / CHUNK_SIZE),
FMath::FloorToInt(static_cast<float>(OriginVoxels.Y) / CHUNK_SIZE),
FMath::FloorToInt(static_cast<float>(OriginVoxels.Z) / CHUNK_SIZE));
const FIntVector EndChunk(
FMath::FloorToInt(static_cast<float>(End.X) / CHUNK_SIZE),
FMath::FloorToInt(static_cast<float>(End.Y) / CHUNK_SIZE),
FMath::FloorToInt(static_cast<float>(End.Z) / CHUNK_SIZE));
const bool bStableStrate = Chunk.Z == EndChunk.Z;
const FStratePlan* Plan = bStableStrate ? FindStrate(Chunk) : nullptr;
OutContext.BrickOrigin = OriginVoxels;
OutContext.BrickSize = Size;
OutContext.Chunk = Chunk;
OutContext.bChunkResolved = Chunk == EndChunk;
OutContext.bPlanResolved = bStableStrate;
OutContext.PlanToken = bStableStrate ? Plan : nullptr;
OutContext.bIsGapChunk = OutContext.bChunkResolved
&& Generator.StrateManager != nullptr
&& Generator.StrateManager->IsGapChunk(Chunk);
OutContext.bHasDiffMods = Generator.DiffLayer != nullptr
&& Generator.DiffLayer->HasAnyMods();
const FVector Min(
static_cast<float>(OriginVoxels.X - 1),
static_cast<float>(OriginVoxels.Y - 1),
static_cast<float>(OriginVoxels.Z - 1));
const FVector Max(
static_cast<float>(OriginVoxels.X + Size.X),
static_cast<float>(OriginVoxels.Y + Size.Y),
static_cast<float>(OriginVoxels.Z + Size.Z));
FindPassageCandidates(FBox(Min, Max), OutContext.PassageCandidates);
OutContext.bMayContainPassage = !OutContext.PassageCandidates.IsEmpty();
}
void BuildDetailBrick(const UVoxelGenerator& Generator, FIntVector OriginVoxels,
FIntVector Size, float DetailScale, TArray<float>& OutDetail,
FBrickEvaluationContext* OutContext)
@@ -1289,10 +1917,12 @@ void BuildDetailBrick(const UVoxelGenerator& Generator, FIntVector OriginVoxels,
AddNoiseBand(1, Seed ^ 0xA511E9B3u, 2.5f);
}
bool TryClassifyBrick(const UVoxelGenerator& Generator, FIntVector OriginVoxels,
static bool TryClassifyBrickInternal(
const UVoxelGenerator& Generator, FIntVector OriginVoxels,
FIntVector Size, bool& bOutAir, float DetailScale,
bool* bOutEvaluatedMacroCentre,
EBrickFallbackReason* OutFallbackReason)
EBrickFallbackReason* OutFallbackReason,
bool bParameterized)
{
if (OutFallbackReason != nullptr)
{
@@ -1444,7 +2074,7 @@ bool TryClassifyBrick(const UVoxelGenerator& Generator, FIntVector OriginVoxels,
FMath::Max(SumAbsDeltaSlope, 2.0 * MaxAbsFloorDelta * SumInverseRadiusSlope),
LandingBridgeSlopeBound);
const float MacroLipschitzBound = FMath::Max(
BaseFieldLipschitzBound,
bParameterized ? ParameterizedFieldLipschitzBound : BaseFieldLipschitzBound,
FMath::Sqrt(1.0f + FMath::Square(static_cast<float>(FloorSlopeBound))) + 0.25f);
const FVector Centre = (Min + Max) * 0.5f;
@@ -1454,7 +2084,9 @@ bool TryClassifyBrick(const UVoxelGenerator& Generator, FIntVector OriginVoxels,
{
*bOutEvaluatedMacroCentre = true;
}
const float Macro = EvaluateMacro(*Plan, Centre.X, Centre.Y, Centre.Z, nullptr);
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
+ DetailAmplitude * FMath::Abs(DetailScale)
+ DetailBoundNumericalMargin * FMath::Max(1.0f, FMath::Abs(DetailScale));
@@ -1482,6 +2114,25 @@ bool TryClassifyBrick(const UVoxelGenerator& Generator, FIntVector OriginVoxels,
}
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);
}
bool TryClassifyParameterizedBrick(const UVoxelGenerator& Generator,
FIntVector OriginVoxels, FIntVector Size,
bool& bOutAir, float DetailScale,
bool* bOutEvaluatedMacroCentre,
EBrickFallbackReason* OutFallbackReason)
{
return TryClassifyBrickInternal(Generator, OriginVoxels, Size, bOutAir,
DetailScale, bOutEvaluatedMacroCentre, OutFallbackReason, true);
}
}
#endif // WITH_EDITOR
@@ -40,6 +40,43 @@ namespace VoxelForgeRedefinedDensityField
TArray<int32> PassageCandidates;
};
/** Cell-level controls for the separate step 2e procedural room-network field. */
struct FGenerationParameters
{
float RoomPitchVoxels = 144.0f;
float MinimumRoomRadiusFraction = 0.30f;
float MaximumRoomRadiusFraction = 0.45f;
float RoomHeightVoxels = 192.0f;
float FloorReliefVoxels = 8.0f;
float TunnelRadiusVoxels = 28.0f;
float TunnelHalfHeightVoxels = 26.0f;
float LoopProbability = 0.20f;
float VerticalShaftProbability = 0.10f;
float ShaftRadiusVoxels = 18.0f;
float PillarProbability = 0.10f;
float LedgeProbability = 0.20f;
float FloatingMassProbability = 0.10f;
float SpireProbability = 0.25f;
float BasinProbability = 0.0f;
float BasinDepthVoxels = 0.0f;
float WaterLevelFraction = 0.5f;
};
/** Brick-local immutable room cells used by step 2e evaluation and its final-field proof. */
struct FParameterizedBrickContext
{
const void* PlanToken = nullptr;
bool bPlanResolved = false;
bool bChunkResolved = false;
bool bIsGapChunk = false;
bool bHasDiffMods = false;
bool bMayContainPassage = true;
FIntVector BrickOrigin = FIntVector::ZeroValue;
FIntVector BrickSize = FIntVector::ZeroValue;
FIntVector Chunk = FIntVector::ZeroValue;
TArray<int32> PassageCandidates;
};
enum class EBrickFallbackReason : uint8
{
None,
@@ -61,6 +98,35 @@ namespace VoxelForgeRedefinedDensityField
VOXELFORGE_API float GetDetailScale();
VOXELFORGE_API void SetDetailScale(float Scale);
/** Step 2e is editor-only and default-off. -1 rolls one of eight profiles per strate. */
VOXELFORGE_API bool IsParameterizedEnabled();
VOXELFORGE_API void SetParameterizedEnabled(bool bEnabled);
VOXELFORGE_API void SetParameterSet(int32 ParameterSet);
VOXELFORGE_API int32 GetParameterSet();
VOXELFORGE_API int32 GetParameterSetCount();
VOXELFORGE_API bool GetParameterSetInfo(
int32 ParameterSet, const TCHAR*& OutName, FGenerationParameters& OutParameters);
/** Evaluate the separate parameterized room / tunnel network, including edits/passages. */
VOXELFORGE_API float EvaluateParameterized(
const UVoxelGenerator& Generator, float WorldX, float WorldY, float WorldZ,
float* OutPreEditDensity = nullptr, float DetailScale = 1.0f);
VOXELFORGE_API float EvaluateParameterizedWithDetailValue(
const UVoxelGenerator& Generator, float WorldX, float WorldY, float WorldZ,
float DetailValue, float* OutPreEditDensity = nullptr, float DetailScale = 1.0f,
const FParameterizedBrickContext* Context = nullptr);
VOXELFORGE_API bool TryClassifyParameterizedBrick(
const UVoxelGenerator& Generator, FIntVector OriginVoxels, FIntVector Size,
bool& bOutAir, float DetailScale = 1.0f,
bool* bOutEvaluatedMacroCentre = nullptr,
EBrickFallbackReason* OutFallbackReason = nullptr);
VOXELFORGE_API void BuildParameterizedBrickContext(
const UVoxelGenerator& Generator, FIntVector OriginVoxels, FIntVector Size,
FParameterizedBrickContext& OutContext);
/** Evaluate the new field, including existing passage carving and player diffs. */
VOXELFORGE_API float Evaluate(const UVoxelGenerator& Generator, float WorldX, float WorldY,
float WorldZ, float* OutPreEditDensity = nullptr,