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:
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
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user