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

+5 -4
View File
@@ -117,11 +117,12 @@ remains `0`, and lateral regions remain gated off.
|------|------|
| `../../VoxelForge.uplugin` | Plugin manifest. Runtime module `VoxelForge` plus the Editor-only `VoxelForgeEditor` commandlet module. Beta. |
| `VoxelForge.Build.cs` | Deps: Core, CoreUObject, Engine, **GameplayTags**, **RealtimeMeshComponent**. |
| `Tools/VoxelForgeTest.ps1` | The single guarded test entry point. `-Scenario canonical|owner|probe|perf|tests|surface-fall|crossing|gate-stress|visual-edit|visual-lod|visual-gap-walk|visual-gap-inject|dig-motion|parity|horizon|horizon-render|determinism|region-eval|step2d`, optional `-Build`, `-Assets owner|default`, `-Cvars`, absolute `-Out`, `-Label`, `-HorizonLevel`, `-HorizonRenderDistanceChunks`, `-ParityExportSize=32|64|128`, `-ParityExportTile=x,y,z`, `-SharedDDC`, `-SampleDensityStacks`, `-StackSampleIntervalUs`, and `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH`; stages the host/assets, isolates user/DDC/config state, serializes one Unreal launch at a time, enforces the 8 GiB/30-minute guards, waits for 6 GiB free RAM before launches, and emits compact `result.json`/`summary.txt` plus full logs. Parity can render its game leg with `-EnableRender` and compares the selected export region against the exact streamed tile hashes. `visual-gap-walk` records camera-frustum surface-point coverage at 6000 cm/s; `visual-gap-inject` proves an early component hide is detected; `dig-motion` logs per-frame vertical velocity, gate, support readiness, and movement mode. `-Cvars @{ 'voxel.TestGapAuditLegacyHandoff'=1 }` replays the old visibility handoff for A/B audit baselines. Horizon traces report desired/loaded tiles, resident stream geometry, worker seconds, steady wall time, and peak private memory; `horizon-render` captures the owner view; `determinism` compares per-tile hashes across the default CRT path and `-voxel.CrtFma3=0`; `region-eval` compares the live path with the default-off editor chunk program on the 259³ lattice and five-seed probe set; `step2d` measures the independent full-detail research field, sound 8³ brick proofs, detail/gradient controls, slices, and worker hashes. `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH` checks a full-ring hash at another partition count. `-SampleDensityStacks` captures profiles, and `-StackSampleIntervalUs` sets the sampling interval.
| `Tools/VoxelForgeTest.ps1` | The single guarded test entry point. `-Scenario canonical|owner|probe|perf|tests|surface-fall|crossing|gate-stress|visual-edit|visual-lod|visual-gap-walk|visual-gap-inject|dig-motion|parity|horizon|horizon-render|determinism|region-eval|step2d|step2e`, optional `-Build`, `-Assets owner|default`, `-Cvars`, absolute `-Out`, `-Label`, `-HorizonLevel`, `-HorizonRenderDistanceChunks`, `-ParityExportSize=32|64|128`, `-ParityExportTile=x,y,z`, `-SharedDDC`, `-SampleDensityStacks`, `-StackSampleIntervalUs`, and `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH`; stages the host/assets, isolates user/DDC/config state, serializes one Unreal launch at a time, enforces the 8 GiB/30-minute guards, waits for 6 GiB free RAM before launches, and emits compact `result.json`/`summary.txt` plus full logs. Parity can render its game leg with `-EnableRender` and compares the selected export region against the exact streamed tile hashes. `visual-gap-walk` records camera-frustum surface-point coverage at 6000 cm/s; `visual-gap-inject` proves an early component hide is detected; `dig-motion` logs per-frame vertical velocity, gate, support readiness, and movement mode. `-Cvars @{ 'voxel.TestGapAuditLegacyHandoff'=1 }` replays the old visibility handoff for A/B audit baselines. Horizon traces report desired/loaded tiles, resident stream geometry, worker seconds, steady wall time, and peak private memory; `horizon-render` captures the owner view; `determinism` compares per-tile hashes across the default CRT path and `-voxel.CrtFma3=0`; `region-eval` compares the live path with the default-off editor chunk program on the 259³ lattice and five-seed probe set; `step2d` retains the earlier independent field; `step2e` measures eight room-network profiles on the 259³ ring, emits variety metrics and live/new slices, and checks detail, boundaries, bounds, and 1-vs-6-worker hashes. `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH` checks a full-ring hash at another partition count. `-SampleDensityStacks` captures profiles, and `-StackSampleIntervalUs` sets the sampling interval.
| `Public/VoxelForgeModule.h` / `Private/VoxelForgeModule.cpp` | `FVoxelForgeModule` boilerplate (Startup/Shutdown just log). |
| `Source/VoxelForgeEditor/VoxelForgeEditor.Build.cs` / `Private/VoxelForgeExploreCommandlet.*` | Editor/commandlet-only `-run=VoxelForgeExplore`; owns bounded render/`VF_` walk/OBJ export orchestration and is not linked by Game/Shipping. Optional `-exporttilex/y/z` sets an exact L0 tile origin for controlled export/game parity; omitting all three preserves the existing center-derived origin. `-regioneval=1` retains the exact-field step-2c research harness. `-step2d` runs the independent field benchmark without old-world equality gates and writes timing, detail, boundary, determinism, triangle, and side-by-side slice evidence under `Saved/Research/step2d`. |
| `Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.*` | Six-worker lattice drivers for step 2c and step 2d. The original path still compares live `GetDensityAt` with the editor chunk program. Step 2d evaluates the independent field and assigns contiguous batches of 8³ bricks dynamically so surface-heavy work balances across workers. It first proves bricks uniform using a macro Lipschitz bound plus the full detail-amplitude bound; uncertain bricks retain all 25 cm detail samples. Timings split classification, point evaluation, and uniform fill; hashes compare workers 1 and 6. |
| `Public/VoxelRedefinedDensityField.h` / `Private/VoxelRedefinedDensityField.cpp` | Default-off `WITH_EDITOR` research field, selected by `voxel.ResearchRedefinedDensityField`. A seeded connected room/corridor lattice joins authored flat passage landings; four bounded world-space value-noise bands, including a one-sample micro band, are evaluated at full resolution. Skip proofs refuse edits, passage bounds, strate boundaries, and uncertain bricks; a local derivative bound permits sound proofs through landing-floor blends. The public API adds no reflected class layout. |
| `Source/VoxelForgeEditor/VoxelForgeEditor.Build.cs` / `Private/VoxelForgeExploreCommandlet.*` | Editor/commandlet-only `-run=VoxelForgeExplore`; owns bounded render/`VF_` walk/OBJ export orchestration and is not linked by Game/Shipping. Optional `-exporttilex/y/z` sets an exact L0 tile origin for controlled export/game parity; omitting all three preserves the existing center-derived origin. `-regioneval=1` retains the exact-field step-2c research harness. `-step2d` retains the earlier independent field benchmark; `-step2e` measures eight room-network profiles and writes timing, variety, and side-by-side slice evidence under `Saved/Research/step2e`. |
| `Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.*` | Six-worker lattice drivers for step 2c, step 2d, and step 2e. The original path still compares live `GetDensityAt` with the editor chunk program. Research modes assign contiguous batches of 8³ bricks dynamically so surface-heavy work balances across workers. Each skip requires a macro Lipschitz bound plus the complete detail-amplitude supremum; uncertain bricks retain all 25 cm detail samples. Timings split classification, point evaluation, and uniform fill; step 2e hashes compare workers 1 and 6. |
| `Public/VoxelRedefinedDensityField.h` / `Private/VoxelRedefinedDensityField.cpp` | Two default-off `WITH_EDITOR` research fields, selected by `voxel.ResearchRedefinedDensityField` (step 2d) and `voxel.ResearchParameterizedCave` (step 2e). Step 2e deterministically assigns one of eight parameter profiles per strate, then builds jittered rooms and a root-connected tunnel graph with optional loops and shafts, pillars, ledges, floating masses, stalactites/stalagmites, and basin floors; four bounded world-space value-noise bands, including a one-sample micro band, are evaluated at full resolution. Step 2e proofs include the model Lipschitz bound, passage/refinement fallback, and full detail supremum; a local derivative bound permits sound proofs through landing-floor blends. The public API adds no reflected class layout. |
| `Private/Tests/VoxelForgeParameterizedGenerationTest.cpp` | Separate three-seed functional sweep for the step 2e field; passage landing air/floor brackets and arrival-to-exit walking routes are checked without changing the live-world tests. |
| `Private/Tests/VoxelForgeRedefinedDensityFieldTest.cpp` | Three-seed functional sweep for the research field: authored passage endpoints must bracket open air and solid floor, and each tested strate must have a walkable arrival-to-exit route. |
| `Public/VoxelGeometryHash.h` | Header-only geometry digest shared by commandlet export and runtime tile-hash dump; hashes mesh counts and the vertex/normal/UV/color/triangle arrays with the historical CRC order. |
| `Public/VoxelStats.h` / `Private/VoxelStats.cpp` | `stat VoxelForge` DWORD counters for tile classification, skipping, meshing, operator-stack verdicts, and cave-bail diagnosis. Each `Cave Bail *` counter names one guard; `Cave Bail No Stack` counts slots with no buildable stack (no definition, or a degenerate strate). |
@@ -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,
@@ -2552,7 +2552,8 @@ struct FStep2dBrickProofAudit
FStep2dBrickProofAudit AuditStep2dBrickProofs(
const UVoxelGenerator& Generator,
FIntVector Origin,
int32 Dimension)
int32 Dimension,
bool bParameterized = false)
{
FStep2dBrickProofAudit Result;
const FIntVector Last = Origin + FIntVector(Dimension - 1, Dimension - 1, Dimension - 1);
@@ -2580,8 +2581,12 @@ FStep2dBrickProofAudit AuditStep2dBrickProofs(
FMath::Min(BrickWorldMin.Z + 8, Origin.Z + Dimension));
const FIntVector Size = End - BrickOrigin;
bool bProvenAir = false;
if (VoxelForgeRedefinedDensityField::TryClassifyBrick(
Generator, BrickOrigin, Size, bProvenAir, 1.0f))
const bool bSoundClaim = bParameterized
? VoxelForgeRedefinedDensityField::TryClassifyParameterizedBrick(
Generator, BrickOrigin, Size, bProvenAir, 1.0f)
: VoxelForgeRedefinedDensityField::TryClassifyBrick(
Generator, BrickOrigin, Size, bProvenAir, 1.0f);
if (bSoundClaim)
{
const bool bAuditThisSkip = (CorrectSkipOrdinal++ % 128u) == 0u;
if (bAuditThisSkip)
@@ -2591,11 +2596,15 @@ FStep2dBrickProofAudit AuditStep2dBrickProofs(
for (int32 Y = -1; Y <= Size.Y; ++Y)
for (int32 X = -1; X <= Size.X; ++X)
{
const float Density = VoxelForgeRedefinedDensityField::Evaluate(
Generator,
static_cast<float>(BrickOrigin.X + X),
static_cast<float>(BrickOrigin.Y + Y),
static_cast<float>(BrickOrigin.Z + Z), nullptr, 1.0f);
const float Density = bParameterized
? VoxelForgeRedefinedDensityField::EvaluateParameterized(
Generator, static_cast<float>(BrickOrigin.X + X),
static_cast<float>(BrickOrigin.Y + Y),
static_cast<float>(BrickOrigin.Z + Z), nullptr, 1.0f)
: VoxelForgeRedefinedDensityField::Evaluate(
Generator, static_cast<float>(BrickOrigin.X + X),
static_cast<float>(BrickOrigin.Y + Y),
static_cast<float>(BrickOrigin.Z + Z), nullptr, 1.0f);
++Result.CorrectProofSamplesChecked;
if ((Density > 0.0f) != bProvenAir)
{
@@ -2609,21 +2618,27 @@ FStep2dBrickProofAudit AuditStep2dBrickProofs(
// maximum detail amplitude. A valid supremum must reject at least some of these
// sign claims; otherwise this audit would not demonstrate that it can catch a bad bound.
bool bDeliberatelyClaimedAir = false;
if (VoxelForgeRedefinedDensityField::TryClassifyBrick(
Generator, BrickOrigin, Size, bDeliberatelyClaimedAir, 0.0f))
const bool bUndersizedClaim = bParameterized
? VoxelForgeRedefinedDensityField::TryClassifyParameterizedBrick(
Generator, BrickOrigin, Size, bDeliberatelyClaimedAir, 0.0f)
: VoxelForgeRedefinedDensityField::TryClassifyBrick(
Generator, BrickOrigin, Size, bDeliberatelyClaimedAir, 0.0f);
if (bUndersizedClaim)
{
++Result.DeliberatelyUndersizedBoundClaimsChecked;
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 MaximumDetailDensity = VoxelForgeRedefinedDensityField::Evaluate(
Generator,
static_cast<float>(BrickOrigin.X + X),
static_cast<float>(BrickOrigin.Y + Y),
static_cast<float>(BrickOrigin.Z + Z),
nullptr,
4.0f);
const float MaximumDetailDensity = bParameterized
? VoxelForgeRedefinedDensityField::EvaluateParameterized(
Generator, static_cast<float>(BrickOrigin.X + X),
static_cast<float>(BrickOrigin.Y + Y),
static_cast<float>(BrickOrigin.Z + Z), nullptr, 4.0f)
: VoxelForgeRedefinedDensityField::Evaluate(
Generator, static_cast<float>(BrickOrigin.X + X),
static_cast<float>(BrickOrigin.Y + Y),
static_cast<float>(BrickOrigin.Z + Z), nullptr, 4.0f);
if ((MaximumDetailDensity > 0.0f) != bDeliberatelyClaimedAir)
{
++Result.DeliberateBoundDisagreements;
@@ -2847,6 +2862,200 @@ void SaveStep2dSideBySideSlice(
SavePng(Path, Pixels, Width, Height, OutError);
}
struct FStep2eVarietyMetrics
{
double OpenFraction = 0.0;
double SurfaceAreaSquareMeters = 0.0;
double SurfaceAreaPerVolume = 0.0;
double VerticalAdjacencyShare = 0.0;
double TunnelNeckShare = 0.0;
int32 ConnectedComponentsAtOneMeter = 0;
int64 LargestComponentCubicMeters = 0;
int64 SmallComponentsUpToEightCubicMeters = 0;
int64 MediumComponentsNineToSixtyFourCubicMeters = 0;
int64 LargeComponentsSixtyFiveToFiveHundredTwelveCubicMeters = 0;
int64 VeryLargeComponentsOverFiveHundredTwelveCubicMeters = 0;
};
FStep2eVarietyMetrics ComputeStep2eVarietyMetrics(
const TArray<float>& Density, int32 Dimension)
{
FStep2eVarietyMetrics Result;
const int64 Total = static_cast<int64>(Dimension) * Dimension * Dimension;
int64 AirCount = 0;
uint64 SurfaceEdgeCount = 0;
uint64 HorizontalAdjacencies = 0;
uint64 VerticalAdjacencies = 0;
for (int32 Z = 0; Z < Dimension; ++Z)
for (int32 Y = 0; Y < Dimension; ++Y)
for (int32 X = 0; X < Dimension; ++X)
{
const int32 Index = (Z * Dimension + Y) * Dimension + X;
const bool bAir = Density[Index] > 0.0f;
AirCount += bAir ? 1 : 0;
if (X + 1 < Dimension)
{
const bool bNextAir = Density[Index + 1] > 0.0f;
SurfaceEdgeCount += bAir != bNextAir ? 1u : 0u;
HorizontalAdjacencies += bAir && bNextAir ? 1u : 0u;
}
if (Y + 1 < Dimension)
{
const bool bNextAir = Density[Index + Dimension] > 0.0f;
SurfaceEdgeCount += bAir != bNextAir ? 1u : 0u;
HorizontalAdjacencies += bAir && bNextAir ? 1u : 0u;
}
if (Z + 1 < Dimension)
{
const bool bNextAir = Density[Index + Dimension * Dimension] > 0.0f;
SurfaceEdgeCount += bAir != bNextAir ? 1u : 0u;
VerticalAdjacencies += bAir && bNextAir ? 1u : 0u;
}
}
Result.OpenFraction = Total > 0 ? static_cast<double>(AirCount) / Total : 0.0;
// Each sign-changing lattice edge estimates one quarter-meter-squared patch. This is a
// consistent edge-count estimate, not a marching-cubes triangle integral.
Result.SurfaceAreaSquareMeters = static_cast<double>(SurfaceEdgeCount) * 0.25 * 0.25;
const double VolumeCubicMeters = static_cast<double>(Total) * 0.25 * 0.25 * 0.25;
Result.SurfaceAreaPerVolume = VolumeCubicMeters > 0.0
? Result.SurfaceAreaSquareMeters / VolumeCubicMeters : 0.0;
const uint64 AllAdjacencies = HorizontalAdjacencies + VerticalAdjacencies;
Result.VerticalAdjacencyShare = AllAdjacencies > 0
? static_cast<double>(VerticalAdjacencies) / AllAdjacencies : 0.0;
constexpr int32 CoarseStep = 4;
const int32 CoarseDimension = (Dimension - 1) / CoarseStep + 1;
const int32 CoarseCount = CoarseDimension * CoarseDimension * CoarseDimension;
TArray<int32> Parent;
Parent.Init(INDEX_NONE, CoarseCount);
const auto CoarseIndex = [CoarseDimension](int32 X, int32 Y, int32 Z)
{
return (Z * CoarseDimension + Y) * CoarseDimension + X;
};
int64 CoarseAirCount = 0;
int64 TunnelNeckCount = 0;
for (int32 Z = 0; Z < CoarseDimension; ++Z)
for (int32 Y = 0; Y < CoarseDimension; ++Y)
for (int32 X = 0; X < CoarseDimension; ++X)
{
const int32 SourceIndex = ((Z * CoarseStep) * Dimension + Y * CoarseStep)
* Dimension + X * CoarseStep;
if (Density[SourceIndex] <= 0.0f)
{
continue;
}
const int32 Index = CoarseIndex(X, Y, Z);
Parent[Index] = Index;
++CoarseAirCount;
int32 OpenNeighbors = 0;
const int32 Delta[6][3] = {
{-1, 0, 0}, {1, 0, 0}, {0, -1, 0}, {0, 1, 0}, {0, 0, -1}, {0, 0, 1}};
for (const auto& D : Delta)
{
const int32 NX = X + D[0];
const int32 NY = Y + D[1];
const int32 NZ = Z + D[2];
if (NX < 0 || NY < 0 || NZ < 0
|| NX >= CoarseDimension || NY >= CoarseDimension || NZ >= CoarseDimension)
{
continue;
}
const int32 NSource = ((NZ * CoarseStep) * Dimension + NY * CoarseStep)
* Dimension + NX * CoarseStep;
OpenNeighbors += Density[NSource] > 0.0f ? 1 : 0;
}
TunnelNeckCount += OpenNeighbors <= 4 ? 1 : 0;
}
Result.TunnelNeckShare = CoarseAirCount > 0
? static_cast<double>(TunnelNeckCount) / CoarseAirCount : 0.0;
const auto FindRoot = [&Parent](int32 Index)
{
int32 Root = Index;
while (Parent[Root] != Root)
{
Root = Parent[Root];
}
while (Parent[Index] != Index)
{
const int32 Next = Parent[Index];
Parent[Index] = Root;
Index = Next;
}
return Root;
};
for (int32 Z = 0; Z < CoarseDimension; ++Z)
for (int32 Y = 0; Y < CoarseDimension; ++Y)
for (int32 X = 0; X < CoarseDimension; ++X)
{
const int32 Index = CoarseIndex(X, Y, Z);
if (Parent[Index] == INDEX_NONE)
{
continue;
}
const int32 Neighbors[3] = {
X > 0 ? CoarseIndex(X - 1, Y, Z) : INDEX_NONE,
Y > 0 ? CoarseIndex(X, Y - 1, Z) : INDEX_NONE,
Z > 0 ? CoarseIndex(X, Y, Z - 1) : INDEX_NONE};
for (const int32 Neighbor : Neighbors)
{
if (Neighbor == INDEX_NONE || Parent[Neighbor] == INDEX_NONE)
{
continue;
}
const int32 RootA = FindRoot(Index);
const int32 RootB = FindRoot(Neighbor);
if (RootA != RootB)
{
Parent[RootB] = RootA;
}
}
}
TMap<int32, int64> ComponentSizes;
for (int32 Index = 0; Index < CoarseCount; ++Index)
{
if (Parent[Index] != INDEX_NONE)
{
++ComponentSizes.FindOrAdd(FindRoot(Index));
}
}
Result.ConnectedComponentsAtOneMeter = ComponentSizes.Num();
for (const TPair<int32, int64>& Pair : ComponentSizes)
{
const int64 Size = Pair.Value;
Result.LargestComponentCubicMeters = FMath::Max(Result.LargestComponentCubicMeters, Size);
if (Size <= 8) ++Result.SmallComponentsUpToEightCubicMeters;
else if (Size <= 64) ++Result.MediumComponentsNineToSixtyFourCubicMeters;
else if (Size <= 512) ++Result.LargeComponentsSixtyFiveToFiveHundredTwelveCubicMeters;
else ++Result.VeryLargeComponentsOverFiveHundredTwelveCubicMeters;
}
return Result;
}
TSharedRef<FJsonObject> Step2eParametersJson(
const VoxelForgeRedefinedDensityField::FGenerationParameters& P)
{
TSharedRef<FJsonObject> Json = MakeShared<FJsonObject>();
Json->SetNumberField(TEXT("room_pitch_voxels"), P.RoomPitchVoxels);
Json->SetNumberField(TEXT("room_radius_fraction_min"), P.MinimumRoomRadiusFraction);
Json->SetNumberField(TEXT("room_radius_fraction_max"), P.MaximumRoomRadiusFraction);
Json->SetNumberField(TEXT("room_height_voxels"), P.RoomHeightVoxels);
Json->SetNumberField(TEXT("floor_relief_voxels"), P.FloorReliefVoxels);
Json->SetNumberField(TEXT("tunnel_radius_voxels"), P.TunnelRadiusVoxels);
Json->SetNumberField(TEXT("tunnel_half_height_voxels"), P.TunnelHalfHeightVoxels);
Json->SetNumberField(TEXT("loop_probability"), P.LoopProbability);
Json->SetNumberField(TEXT("vertical_shaft_probability"), P.VerticalShaftProbability);
Json->SetNumberField(TEXT("shaft_radius_voxels"), P.ShaftRadiusVoxels);
Json->SetNumberField(TEXT("pillar_probability"), P.PillarProbability);
Json->SetNumberField(TEXT("ledge_probability"), P.LedgeProbability);
Json->SetNumberField(TEXT("floating_mass_probability"), P.FloatingMassProbability);
Json->SetNumberField(TEXT("stalactite_stalagmite_probability"), P.SpireProbability);
Json->SetNumberField(TEXT("basin_probability"), P.BasinProbability);
Json->SetNumberField(TEXT("basin_depth_voxels"), P.BasinDepthVoxels);
Json->SetNumberField(TEXT("water_level_fraction"), P.WaterLevelFraction);
return Json;
}
TSharedRef<FJsonObject> Step2dTimingJson(const VoxelForgeRegionEval::FTiming& Timing)
{
TSharedRef<FJsonObject> Json = MakeShared<FJsonObject>();
@@ -3338,6 +3547,405 @@ int32 RunStep2d(const FString& Params)
return bPassed ? 0 : 2;
}
int32 RunStep2e(const FString& Params)
{
FExploreArguments Arguments;
FString Error;
if (!ParseArguments(Params, Arguments, Error))
{
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep2e] argument error: %s"), *Error);
return 2;
}
int32 Seed = 1337;
FParse::Value(*Params, TEXT("seed="), Seed);
int32 Workers = VoxelForgeRegionEval::WorkerCount;
FParse::Value(*Params, TEXT("regionworkers="), Workers);
Workers = FMath::Clamp(Workers, 1, 32);
FString ResearchOut;
FParse::Value(*Params, TEXT("researchout="), ResearchOut);
if (ResearchOut.IsEmpty())
{
ResearchOut = FPaths::Combine(Arguments.OutDirectory, TEXT("Research/step2e"));
}
ResearchOut = FPaths::ConvertRelativePathToFull(ResearchOut);
IFileManager::Get().MakeDirectory(*ResearchOut, true);
IFileManager::Get().MakeDirectory(*Arguments.OutDirectory, true);
FExploreWorld World;
int32 RecipeIndex = INDEX_NONE;
uint32 RecipeHash = 0;
int32 EditChunkCount = 0;
const double PrepareStart = FPlatformTime::Seconds();
if (!PrepareRegionEvalWorld(World, Arguments, Seed,
RecipeIndex, RecipeHash, EditChunkCount, Error))
{
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep2e] world preparation failed: %s"), *Error);
return 2;
}
const double WorldPrepareSeconds = FPlatformTime::Seconds() - PrepareStart;
if (World.DiffLayer != nullptr)
{
World.DiffLayer->Clear();
}
const int32 Dimension = VoxelForgeRegionEval::RingDimension;
const int64 TotalSamples = static_cast<int64>(Dimension) * Dimension * Dimension;
const int32 CenterZ = (World.TargetTopWorldZ + World.TargetBottomWorldZ) / 2;
const FIntVector Origin(-129, -129, CenterZ - 129);
VoxelForgeRedefinedDensityField::SetEnabled(false);
VoxelForgeRedefinedDensityField::SetParameterizedEnabled(false);
VoxelForgeRedefinedDensityField::SetParameterSet(-1);
VoxelForgeRedefinedDensityField::SetDetailScale(1.0f);
TArray<float> OldDensities;
VoxelForgeRegionEval::FTiming OldTiming;
if (!VoxelForgeRegionEval::Evaluate(*World.Generator, Origin, Dimension,
nullptr, true, false, 1.0f, &OldDensities, OldTiming, false, Workers, false))
{
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep2e] live comparison ring failed."));
return 2;
}
VoxelForgeRedefinedDensityField::SetParameterizedEnabled(true);
TArray<TSharedPtr<FJsonValue>> ProfileRows;
TArray<TSharedPtr<FJsonValue>> SliceRows;
TArray<double> ProfileMedianMs;
bool bProfilePasses = true;
VoxelForgeRedefinedDensityField::FGenerationParameters HardestParameters;
const TCHAR* HardestName = TEXT("HyperComplex");
for (int32 Profile = 0;
Profile < VoxelForgeRedefinedDensityField::GetParameterSetCount(); ++Profile)
{
const TCHAR* ProfileName = nullptr;
VoxelForgeRedefinedDensityField::FGenerationParameters ProfileParameters;
if (!VoxelForgeRedefinedDensityField::GetParameterSetInfo(
Profile, ProfileName, ProfileParameters))
{
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep2e] missing profile %d."), Profile);
return 2;
}
VoxelForgeRedefinedDensityField::SetParameterSet(Profile);
TArray<double> Times;
TArray<float> ProfileDensities;
VoxelForgeRegionEval::FTiming LastTiming;
for (int32 Trial = 0; Trial < 3; ++Trial)
{
if (!VoxelForgeRegionEval::EvaluateRedefined(
*World.Generator, Origin, Dimension, Workers, 1.0f,
ProfileDensities, LastTiming, true, true))
{
UE_LOG(LogTemp, Error,
TEXT("[VoxelForgeStep2e] profile %d six-worker ring trial %d failed."),
Profile, Trial + 1);
return 2;
}
Times.Add(LastTiming.WallSeconds * 1000.0);
}
Times.Sort();
const double MedianMs = Times[Times.Num() / 2];
ProfileMedianMs.Add(MedianMs);
bProfilePasses &= MedianMs <= 600.0;
const FStep2eVarietyMetrics Variety = ComputeStep2eVarietyMetrics(
ProfileDensities, Dimension);
TSharedRef<FJsonObject> Row = MakeShared<FJsonObject>();
Row->SetNumberField(TEXT("profile_index"), Profile);
Row->SetStringField(TEXT("name"), ProfileName);
Row->SetObjectField(TEXT("parameters"), Step2eParametersJson(ProfileParameters));
Row->SetNumberField(TEXT("six_worker_ring_median_ms"), MedianMs);
TArray<TSharedPtr<FJsonValue>> TrialValues;
TrialValues.Add(MakeShared<FJsonValueNumber>(Times[0]));
TrialValues.Add(MakeShared<FJsonValueNumber>(Times[1]));
TrialValues.Add(MakeShared<FJsonValueNumber>(Times[2]));
Row->SetArrayField(TEXT("six_worker_ring_trials_ms"), TrialValues);
Row->SetNumberField(TEXT("open_air_fraction"), Variety.OpenFraction);
Row->SetNumberField(TEXT("estimated_surface_area_m2"), Variety.SurfaceAreaSquareMeters);
Row->SetNumberField(TEXT("estimated_surface_area_per_volume_m_inv"),
Variety.SurfaceAreaPerVolume);
Row->SetNumberField(TEXT("vertical_air_adjacency_share"), Variety.VerticalAdjacencyShare);
Row->SetNumberField(TEXT("one_meter_tunnel_neck_share"), Variety.TunnelNeckShare);
Row->SetNumberField(TEXT("one_meter_connected_components"),
Variety.ConnectedComponentsAtOneMeter);
Row->SetNumberField(TEXT("largest_component_coarse_m3"), Variety.LargestComponentCubicMeters);
Row->SetNumberField(TEXT("components_1_to_8_coarse_m3"), Variety.SmallComponentsUpToEightCubicMeters);
Row->SetNumberField(TEXT("components_9_to_64_coarse_m3"), Variety.MediumComponentsNineToSixtyFourCubicMeters);
Row->SetNumberField(TEXT("components_65_to_512_coarse_m3"), Variety.LargeComponentsSixtyFiveToFiveHundredTwelveCubicMeters);
Row->SetNumberField(TEXT("components_over_512_coarse_m3"), Variety.VeryLargeComponentsOverFiveHundredTwelveCubicMeters);
Row->SetNumberField(TEXT("skipped_bricks"), static_cast<double>(LastTiming.SkippedBricks));
Row->SetNumberField(TEXT("skipped_sample_fraction"), LastTiming.SampleCount > 0
? static_cast<double>(LastTiming.SkippedSamples) / LastTiming.SampleCount : 0.0);
FString SafeName(ProfileName);
SafeName.ToLowerInline();
const FString SetName = FString::Printf(TEXT("set_%02d_%s"), Profile, *SafeName);
const FString SetDirectory = FPaths::Combine(ResearchOut, SetName);
IFileManager::Get().MakeDirectory(*SetDirectory, true);
const FString ImageNames[] = {
TEXT("slice_xy_center.png"), TEXT("slice_xz_y0.png"),
TEXT("slice_yz_x0.png"), TEXT("slice_xz_passage.png")};
FString SliceError;
SaveStep2dSideBySideSlice(FPaths::Combine(SetDirectory, ImageNames[0]),
OldDensities, ProfileDensities, Dimension, TEXT('h'), Dimension / 2, SliceError);
SaveStep2dSideBySideSlice(FPaths::Combine(SetDirectory, ImageNames[1]),
OldDensities, ProfileDensities, Dimension, TEXT('x'), Dimension / 2, SliceError);
SaveStep2dSideBySideSlice(FPaths::Combine(SetDirectory, ImageNames[2]),
OldDensities, ProfileDensities, Dimension, TEXT('y'), Dimension / 2, SliceError);
int32 PassageIndex = Dimension / 2;
if (World.Manager != nullptr && !World.Manager->GetPassages().IsEmpty())
{
PassageIndex = FMath::Clamp(FMath::RoundToInt(
World.Manager->GetPassages()[0].LowerLanding.StandingPoint.Y) - Origin.Y,
0, Dimension - 1);
}
SaveStep2dSideBySideSlice(FPaths::Combine(SetDirectory, ImageNames[3]),
OldDensities, ProfileDensities, Dimension, TEXT('x'), PassageIndex, SliceError);
if (!SliceError.IsEmpty())
{
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep2e] slice write failed for %s: %s"),
ProfileName, *SliceError);
return 2;
}
Row->SetStringField(TEXT("slice_directory"), SetDirectory);
ProfileRows.Add(MakeShared<FJsonValueObject>(Row));
for (const FString& ImageName : ImageNames)
{
TSharedRef<FJsonObject> Slice = MakeShared<FJsonObject>();
Slice->SetStringField(TEXT("profile"), ProfileName);
Slice->SetStringField(TEXT("file"), FPaths::Combine(SetDirectory, ImageName));
Slice->SetStringField(TEXT("layout"),
TEXT("today's live field on left; step 2e parameterized field on right; light=air, dark=solid, cyan=near zero"));
SliceRows.Add(MakeShared<FJsonValueObject>(Slice));
}
if (Profile == VoxelForgeRedefinedDensityField::GetParameterSetCount() - 1)
{
HardestParameters = ProfileParameters;
HardestName = ProfileName;
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeStep2e] profile=%d/%s ring_ms=%.3f open=%.3f area=%.1f m2 components=%d"),
Profile, ProfileName, MedianMs, Variety.OpenFraction,
Variety.SurfaceAreaSquareMeters, Variety.ConnectedComponentsAtOneMeter);
}
const int32 HardestProfile = VoxelForgeRedefinedDensityField::GetParameterSetCount() - 1;
VoxelForgeRedefinedDensityField::SetParameterSet(HardestProfile);
TArray<float> HardestFullDensity;
TArray<float> RepeatFullDensity;
TArray<float> OneWorkerDensity;
TArray<float> SmoothDensity;
VoxelForgeRegionEval::FTiming HardestFullTiming;
VoxelForgeRegionEval::FTiming RepeatTiming;
VoxelForgeRegionEval::FTiming OneWorkerTiming;
VoxelForgeRegionEval::FTiming SmoothTiming;
const bool bHardestFullOk = VoxelForgeRegionEval::EvaluateRedefined(
*World.Generator, Origin, Dimension, Workers, 1.0f,
HardestFullDensity, HardestFullTiming, false, true);
const bool bRepeatOk = bHardestFullOk && VoxelForgeRegionEval::EvaluateRedefined(
*World.Generator, Origin, Dimension, Workers, 1.0f,
RepeatFullDensity, RepeatTiming, false, true);
const bool bOneWorkerOk = bHardestFullOk && VoxelForgeRegionEval::EvaluateRedefined(
*World.Generator, Origin, Dimension, 1, 1.0f,
OneWorkerDensity, OneWorkerTiming, false, true);
const bool bSmoothOk = bHardestFullOk && VoxelForgeRegionEval::EvaluateRedefined(
*World.Generator, Origin, Dimension, Workers, 0.0f,
SmoothDensity, SmoothTiming, false, true);
if (!bHardestFullOk || !bRepeatOk || !bOneWorkerOk || !bSmoothOk)
{
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep2e] hardest-set control pass failed."));
return 2;
}
int64 DetailParitySamples = 0;
double DetailParityMaxError = 0.0;
for (int64 Index = 0; Index < TotalSamples; Index += 2047)
{
const int32 X = static_cast<int32>(Index % Dimension);
const int32 Y = static_cast<int32>((Index / Dimension) % Dimension);
const int32 Z = static_cast<int32>(Index
/ (static_cast<int64>(Dimension) * Dimension));
const float Direct = VoxelForgeRedefinedDensityField::EvaluateParameterized(
*World.Generator, static_cast<float>(Origin.X + X),
static_cast<float>(Origin.Y + Y), static_cast<float>(Origin.Z + Z), nullptr, 1.0f);
DetailParityMaxError = FMath::Max(DetailParityMaxError,
static_cast<double>(FMath::Abs(Direct - HardestFullDensity[static_cast<int32>(Index)])));
++DetailParitySamples;
}
const bool bDetailParity = DetailParityMaxError <= 1.0e-5;
const FStep2dSurfaceSpectrum OldSpectrum = ComputeStep2dSpectrum(OldDensities, Dimension);
const FStep2dSurfaceSpectrum NewSpectrum = ComputeStep2dSpectrum(HardestFullDensity, Dimension);
const FStep2dSurfaceSpectrum SmoothSpectrum = ComputeStep2dSpectrum(SmoothDensity, Dimension);
const FStep2dBoundaryStats BoundaryStats = ComputeStep2dBoundaryStats(
HardestFullDensity, Dimension, Origin,
FMath::RoundToInt(HardestParameters.RoomPitchVoxels));
const FStep2dBrickProofAudit ProofAudit = AuditStep2dBrickProofs(
*World.Generator, Origin, Dimension, true);
const uint64 HardestHash = VoxelForgeRegionEval::HashQuantized(HardestFullDensity);
const uint64 RepeatHash = VoxelForgeRegionEval::HashQuantized(RepeatFullDensity);
const uint64 OneWorkerHash = VoxelForgeRegionEval::HashQuantized(OneWorkerDensity);
const bool bDeterminism = HardestHash == RepeatHash && HardestHash == OneWorkerHash
&& HardestFullTiming.RawFloatHash == RepeatTiming.RawFloatHash
&& HardestFullTiming.RawFloatHash == OneWorkerTiming.RawFloatHash;
const bool bDetailAtLeastLive = NewSpectrum.RMSLag1 >= OldSpectrum.RMSLag1
&& NewSpectrum.RMSLag2 >= OldSpectrum.RMSLag2;
const bool bSmoothingControlCaught = SmoothSpectrum.RMSLag1 < NewSpectrum.RMSLag1 * 0.80
|| SmoothSpectrum.RMSLag2 < NewSpectrum.RMSLag2 * 0.80;
const bool bBoundaryPass = BoundaryStats.BrickBoundarySamples > 0
&& BoundaryStats.InteriorSamples > 0
&& BoundaryStats.ChunkBoundarySamples > 0
&& BoundaryStats.FeatureCellBoundarySamples > 0
&& BoundaryStats.BrickJumpRms <= BoundaryStats.InteriorJumpRms * 2.0 + 0.25
&& BoundaryStats.ChunkBoundaryJumpRms <= BoundaryStats.InteriorJumpRms * 2.0 + 0.25
&& BoundaryStats.FeatureCellJumpRms
<= BoundaryStats.FeatureCellInteriorJumpRms * 2.0 + 0.25;
const bool bBoundAudit = ProofAudit.CorrectlySkippedBricksAudited > 0
&& ProofAudit.CorrectProofSamplesChecked > 0
&& ProofAudit.CorrectProofViolations == 0;
const bool bUndersizedBoundCaught = ProofAudit.DeliberateBoundDisagreements > 0;
double WorstMedianMs = 0.0;
for (const double MedianMs : ProfileMedianMs)
{
WorstMedianMs = FMath::Max(WorstMedianMs, MedianMs);
}
const bool bSpeedPass = Workers == 6 && bProfilePasses && WorstMedianMs <= 600.0;
const bool bPassed = bSpeedPass && bDeterminism && bDetailAtLeastLive
&& bSmoothingControlCaught && bDetailParity && bBoundaryPass
&& bBoundAudit && bUndersizedBoundCaught
&& HardestFullTiming.NonFiniteSamples == 0;
TSharedRef<FJsonObject> Report = MakeShared<FJsonObject>();
Report->SetStringField(TEXT("status"), bPassed ? TEXT("pass") : TEXT("fail"));
Report->SetBoolField(TEXT("pass"), bPassed);
Report->SetBoolField(TEXT("worst_profile_meets_600ms"), bSpeedPass);
Report->SetNumberField(TEXT("seed"), Seed);
Report->SetNumberField(TEXT("ring_dimension"), Dimension);
Report->SetNumberField(TEXT("ring_samples"), static_cast<double>(TotalSamples));
Report->SetNumberField(TEXT("workers_requested"), Workers);
Report->SetNumberField(TEXT("world_prepare_ms_excluded"), WorldPrepareSeconds * 1000.0);
Report->SetNumberField(TEXT("old_live_ring_ms"), OldTiming.WallSeconds * 1000.0);
Report->SetStringField(TEXT("old_live_ring_raw_float_hash"), FString::Printf(
TEXT("%016llx"), static_cast<unsigned long long>(OldTiming.RawFloatHash)));
Report->SetStringField(TEXT("field_definition"),
TEXT("Seeded jittered XY chamber cells form a deterministic origin-rooted spanning tree; "
"selected loops, horizontal tunnels and full-strate shafts open the network. Each "
"chamber can add pillars, annular ledges, paired stalactites/stalagmites, floating "
"rock masses and a bowl floor with an explicit water level. Passage pads and anchor "
"tunnels join the authored inter-strate route to the generated network."));
Report->SetStringField(TEXT("parameter_space"),
TEXT("Eight named profiles can be assigned per strate from a seed hash (-1 mode), or "
"forced for measurement. Each profile rolls cell placement and feature choices from "
"the world seed and strate index; see parameters and documented ranges in the report."));
Report->SetStringField(TEXT("resolution_contract"),
TEXT("One exact field sample per 25 cm lattice point. The four existing 8/4/2/1-sample "
"detail bands are evaluated for every potentially surface-affecting point; no coarse "
"field is interpolated onto the lattice. 8-cubed skips require the final macro field "
"Lipschitz interval plus the full 5.25-unit detail supremum and marching-cubes halo."));
Report->SetStringField(TEXT("finer_lattice_scaling"),
TEXT("For 12.5 cm voxels, double room pitch, room height, tunnel/feature dimensions and "
"the broad-to-medium noise periods in sample units to preserve meter-scale shapes. "
"Keep the 1-sample detail band or add a finer octave. The lattice grows 8x for a fixed "
"world volume; surface bricks grow roughly with area while certified interior bricks "
"remain cheap. This scale has not been timed."));
Report->SetStringField(TEXT("variety_metric_method"),
TEXT("Open fraction uses every ring sample. Surface area estimates sign-changing lattice "
"edges times 0.25 m squared. Chamber-size bins, connected components and tunnel-neck "
"share use a 1 m downsampled six-neighbor occupancy grid; verticality is the share of "
"open 1-cell adjacencies that point vertically."));
Report->SetStringField(TEXT("water_geometry_limit"),
TEXT("Basin profiles generate the bowl floor and water-level parameter; this density field "
"does not render or simulate water."));
Report->SetNumberField(TEXT("worst_six_worker_ring_median_ms"), WorstMedianMs);
Report->SetNumberField(TEXT("target_aim_ms"), 350.0);
Report->SetStringField(TEXT("hardest_profile"), HardestName);
Report->SetArrayField(TEXT("profiles"), ProfileRows);
Report->SetArrayField(TEXT("slices"), SliceRows);
Report->SetStringField(TEXT("slice_directory"), ResearchOut);
TSharedRef<FJsonObject> Detail = MakeShared<FJsonObject>();
Detail->SetNumberField(TEXT("live_surface_band_samples"), static_cast<double>(OldSpectrum.SurfaceBandSamples));
Detail->SetNumberField(TEXT("new_surface_band_samples"), static_cast<double>(NewSpectrum.SurfaceBandSamples));
Detail->SetNumberField(TEXT("live_curvature_rms_lag1"), OldSpectrum.RMSLag1);
Detail->SetNumberField(TEXT("new_curvature_rms_lag1"), NewSpectrum.RMSLag1);
Detail->SetNumberField(TEXT("smooth_control_curvature_rms_lag1"), SmoothSpectrum.RMSLag1);
Detail->SetNumberField(TEXT("live_curvature_rms_lag2"), OldSpectrum.RMSLag2);
Detail->SetNumberField(TEXT("new_curvature_rms_lag2"), NewSpectrum.RMSLag2);
Detail->SetNumberField(TEXT("smooth_control_curvature_rms_lag2"), SmoothSpectrum.RMSLag2);
Detail->SetNumberField(TEXT("detail_raw_parity_samples"), DetailParitySamples);
Detail->SetNumberField(TEXT("detail_raw_parity_max_error"), DetailParityMaxError);
Detail->SetBoolField(TEXT("detail_at_least_live_spectrum"), bDetailAtLeastLive);
Detail->SetBoolField(TEXT("deliberate_zero_detail_smoothing_detected"), bSmoothingControlCaught);
Detail->SetBoolField(TEXT("direct_vs_brick_detail_parity"), bDetailParity);
Report->SetObjectField(TEXT("detail_check"), Detail);
TSharedRef<FJsonObject> Boundary = MakeShared<FJsonObject>();
Boundary->SetNumberField(TEXT("brick_boundary_gradient_jump_rms"), BoundaryStats.BrickJumpRms);
Boundary->SetNumberField(TEXT("interior_gradient_jump_rms"), BoundaryStats.InteriorJumpRms);
Boundary->SetNumberField(TEXT("chunk_boundary_gradient_jump_rms"), BoundaryStats.ChunkBoundaryJumpRms);
Boundary->SetNumberField(TEXT("feature_cell_boundary_gradient_jump_rms"), BoundaryStats.FeatureCellJumpRms);
Boundary->SetNumberField(TEXT("feature_cell_interior_gradient_jump_rms"), BoundaryStats.FeatureCellInteriorJumpRms);
Boundary->SetBoolField(TEXT("boundary_artifact_check_pass"), bBoundaryPass);
Report->SetObjectField(TEXT("boundary_artifact_check"), Boundary);
TSharedRef<FJsonObject> Proofs = MakeShared<FJsonObject>();
Proofs->SetNumberField(TEXT("correctly_skipped_bricks_audited"),
static_cast<double>(ProofAudit.CorrectlySkippedBricksAudited));
Proofs->SetNumberField(TEXT("final_field_samples_checked_including_halo"),
static_cast<double>(ProofAudit.CorrectProofSamplesChecked));
Proofs->SetNumberField(TEXT("correct_proof_sign_violations"),
static_cast<double>(ProofAudit.CorrectProofViolations));
Proofs->SetNumberField(TEXT("undersized_bound_claims_checked"),
static_cast<double>(ProofAudit.DeliberatelyUndersizedBoundClaimsChecked));
Proofs->SetNumberField(TEXT("wrong_sign_samples_caught_by_undersized_bound"),
static_cast<double>(ProofAudit.DeliberateBoundDisagreements));
Proofs->SetBoolField(TEXT("sound_supremum_audit_pass"), bBoundAudit);
Proofs->SetBoolField(TEXT("deliberate_undersized_bound_failure_caught"), bUndersizedBoundCaught);
Report->SetObjectField(TEXT("brick_bound_audit"), Proofs);
TSharedRef<FJsonObject> Determinism = MakeShared<FJsonObject>();
Determinism->SetStringField(TEXT("six_worker_run_1_quantized"), FString::Printf(
TEXT("%016llx"), static_cast<unsigned long long>(HardestHash)));
Determinism->SetStringField(TEXT("six_worker_run_2_quantized"), FString::Printf(
TEXT("%016llx"), static_cast<unsigned long long>(RepeatHash)));
Determinism->SetStringField(TEXT("one_worker_quantized"), FString::Printf(
TEXT("%016llx"), static_cast<unsigned long long>(OneWorkerHash)));
Determinism->SetStringField(TEXT("six_worker_run_1_raw_float"), FString::Printf(
TEXT("%016llx"), static_cast<unsigned long long>(HardestFullTiming.RawFloatHash)));
Determinism->SetStringField(TEXT("six_worker_run_2_raw_float"), FString::Printf(
TEXT("%016llx"), static_cast<unsigned long long>(RepeatTiming.RawFloatHash)));
Determinism->SetStringField(TEXT("one_worker_raw_float"), FString::Printf(
TEXT("%016llx"), static_cast<unsigned long long>(OneWorkerTiming.RawFloatHash)));
Determinism->SetBoolField(TEXT("same_across_two_runs_and_1_vs_6_workers"), bDeterminism);
Report->SetObjectField(TEXT("determinism"), Determinism);
Report->SetBoolField(TEXT("entrance_exit_walkability_test_added"), true);
Report->SetBoolField(TEXT("inter_strate_passage_open_space_test_added"), true);
Report->SetStringField(TEXT("functional_test_filter"),
TEXT("VoxelForge.ParameterizedGeneration.TraversalSeeds"));
FString JsonText;
const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&JsonText);
const FString ReportPath = FPaths::Combine(ResearchOut, TEXT("step2e_report.json"));
const FString LaunchReportPath = FPaths::Combine(Arguments.OutDirectory, TEXT("step2e_report.json"));
if (!FJsonSerializer::Serialize(Report, Writer)
|| !FFileHelper::SaveStringToFile(JsonText, *ReportPath,
FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)
|| !FFileHelper::SaveStringToFile(JsonText, *LaunchReportPath,
FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
{
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep2e] report write failed: %s"), *ReportPath);
return 2;
}
VoxelForgeRedefinedDensityField::SetParameterizedEnabled(false);
VoxelForgeRedefinedDensityField::SetDetailScale(1.0f);
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeStep2e] pass=%s speed600=%s worst_ms=%.3f detail=%s boundary=%s "
"bound=%s deterministic=%s report=%s"),
bPassed ? TEXT("yes") : TEXT("no"), bSpeedPass ? TEXT("yes") : TEXT("no"),
WorstMedianMs, bDetailAtLeastLive ? TEXT("pass") : TEXT("fail"),
bBoundaryPass ? TEXT("pass") : TEXT("fail"), bBoundAudit ? TEXT("pass") : TEXT("fail"),
bDeterminism ? TEXT("pass") : TEXT("fail"), *ReportPath);
return bPassed ? 0 : 2;
}
class FGeneratorDensitySampler final : public IVoxelStrateDensitySampler
{
public:
@@ -10475,6 +11083,10 @@ int32 RunBatch(const FString& Params)
int32 UVoxelForgeExploreCommandlet::Main(const FString& Params)
{
if (FParse::Param(*Params, TEXT("step2e")))
{
return RunStep2e(Params);
}
if (FParse::Param(*Params, TEXT("step2d")))
{
return RunStep2d(Params);
@@ -157,7 +157,8 @@ namespace VoxelForgeRegionEval
float DetailScale,
TArray<float>& OutDensities,
FTiming& OutTiming,
bool bEnableBrickSkips)
bool bEnableBrickSkips,
bool bParameterized)
{
OutTiming = FTiming();
OutDensities.Reset();
@@ -250,6 +251,7 @@ namespace VoxelForgeRegionEval
const bool bHaveCpuStart = GetCurrentThreadCpuSeconds(CpuStart);
TArray<float> BrickDetail;
VoxelForgeRedefinedDensityField::FBrickEvaluationContext BrickContext;
VoxelForgeRedefinedDensityField::FParameterizedBrickContext ParameterizedBrickContext;
for (;;)
{
const int32 BatchBegin = NextBrickIndex.fetch_add(
@@ -268,10 +270,13 @@ namespace VoxelForgeRegionEval
VoxelForgeRedefinedDensityField::EBrickFallbackReason::UnsupportedState;
++WorkerMacroChecks[WorkerIndex];
const double ClassifyStart = FPlatformTime::Seconds();
const bool bCanSkip = bEnableBrickSkips
&& VoxelForgeRedefinedDensityField::TryClassifyBrick(
const bool bCanSkip = bEnableBrickSkips && (bParameterized
? VoxelForgeRedefinedDensityField::TryClassifyParameterizedBrick(
Generator, Brick.Origin, Brick.Size, bUniformAir, DetailScale,
&bEvaluatedMacroCentre, &FallbackReason);
&bEvaluatedMacroCentre, &FallbackReason)
: VoxelForgeRedefinedDensityField::TryClassifyBrick(
Generator, Brick.Origin, Brick.Size, bUniformAir, DetailScale,
&bEvaluatedMacroCentre, &FallbackReason));
WorkerClassify[WorkerIndex] += FPlatformTime::Seconds() - ClassifyStart;
WorkerMacroCentreEvaluations[WorkerIndex] += bEvaluatedMacroCentre ? 1u : 0u;
if (bEnableBrickSkips && !bCanSkip)
@@ -304,7 +309,12 @@ namespace VoxelForgeRegionEval
const double EvaluationStart = FPlatformTime::Seconds();
VoxelForgeRedefinedDensityField::BuildDetailBrick(
Generator, Brick.Origin, Brick.Size, DetailScale, BrickDetail,
&BrickContext);
bParameterized ? nullptr : &BrickContext);
if (bParameterized)
{
VoxelForgeRedefinedDensityField::BuildParameterizedBrickContext(
Generator, Brick.Origin, Brick.Size, ParameterizedBrickContext);
}
for (int32 Z = 0; Z < Brick.Size.Z; ++Z)
for (int32 Y = 0; Y < Brick.Size.Y; ++Y)
for (int32 X = 0; X < Brick.Size.X; ++X)
@@ -315,11 +325,16 @@ namespace VoxelForgeRegionEval
const int64 LatticeIndex = LinearIndex(
Brick.Lattice.X + X, Brick.Lattice.Y + Y,
Brick.Lattice.Z + Z, Dimension);
OutDensities[static_cast<int32>(LatticeIndex)] =
VoxelForgeRedefinedDensityField::EvaluateWithDetailValue(
const float DetailValue = BrickDetail[
(Z * Brick.Size.Y + Y) * Brick.Size.X + X];
OutDensities[static_cast<int32>(LatticeIndex)] = bParameterized
? VoxelForgeRedefinedDensityField::EvaluateParameterizedWithDetailValue(
Generator, static_cast<float>(WorldX), static_cast<float>(WorldY),
static_cast<float>(WorldZ),
BrickDetail[(Z * Brick.Size.Y + Y) * Brick.Size.X + X],
static_cast<float>(WorldZ), DetailValue, nullptr, DetailScale,
&ParameterizedBrickContext)
: VoxelForgeRedefinedDensityField::EvaluateWithDetailValue(
Generator, static_cast<float>(WorldX), static_cast<float>(WorldY),
static_cast<float>(WorldZ), DetailValue,
nullptr, DetailScale, &BrickContext);
++WorkerEvaluated[WorkerIndex];
}
@@ -144,7 +144,8 @@ namespace VoxelForgeRegionEval
float DetailScale,
TArray<float>& OutDensities,
FTiming& OutTiming,
bool bEnableBrickSkips = true);
bool bEnableBrickSkips = true,
bool bParameterized = false);
/** Count exact room/tunnel/core/passage candidate visits with instrumentation enabled. */
FCandidateVisits CountCandidates(
+31 -3
View File
@@ -1,6 +1,6 @@
[CmdletBinding()]
param(
[ValidateSet('canonical', 'owner', 'probe', 'perf', 'visual-edit', 'visual-lod', 'visual-gap-walk', 'visual-gap-inject', 'dig-motion', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'parity', 'horizon', 'horizon-render', 'determinism', 'symbolize-stacks', 'region-eval', 'step2d')]
[ValidateSet('canonical', 'owner', 'probe', 'perf', 'visual-edit', 'visual-lod', 'visual-gap-walk', 'visual-gap-inject', 'dig-motion', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'parity', 'horizon', 'horizon-render', 'determinism', 'symbolize-stacks', 'region-eval', 'step2d', 'step2e')]
[string]$Scenario = 'canonical',
[switch]$Build,
[switch]$EnableRender,
@@ -575,7 +575,7 @@ function Sync-OwnerAssets {
$null = Copy-Item -LiteralPath $source -Destination $target -Force -PassThru
[void]$rows.Add([ordered]@{
name = $assetName
used_by_scenario = $Scenario -in @('owner', 'probe', 'parity', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'visual-edit', 'visual-lod', 'visual-gap-walk', 'visual-gap-inject', 'dig-motion', 'horizon', 'horizon-render', 'determinism', 'region-eval', 'step2d')
used_by_scenario = $Scenario -in @('owner', 'probe', 'parity', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'visual-edit', 'visual-lod', 'visual-gap-walk', 'visual-gap-inject', 'dig-motion', 'horizon', 'horizon-render', 'determinism', 'region-eval', 'step2d', 'step2e')
source_path = $source
source_sha256 = Get-Sha256 $source
staged_path = $target
@@ -623,7 +623,7 @@ function Get-AssetEvidence {
$target = Join-Path $HostRoot "Content\VoxelForge\$assetName.uasset"
[void]$rows.Add([ordered]@{
name = $assetName
used_by_scenario = $Scenario -in @('owner', 'probe', 'parity', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'visual-edit', 'visual-lod', 'visual-gap-walk', 'visual-gap-inject', 'dig-motion', 'horizon', 'horizon-render', 'determinism', 'region-eval', 'step2d')
used_by_scenario = $Scenario -in @('owner', 'probe', 'parity', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'visual-edit', 'visual-lod', 'visual-gap-walk', 'visual-gap-inject', 'dig-motion', 'horizon', 'horizon-render', 'determinism', 'region-eval', 'step2d', 'step2e')
source_path = $source
source_sha256 = Get-Sha256 $source
staged_path = $target
@@ -1499,6 +1499,33 @@ try {
}
}
}
'step2e' {
$step2eLog = Join-Path $RunRoot 'Logs\step2e.log'
$args = New-CommonArguments 'commandlet' $step2eLog (Merge-Cvars $baseCvars)
$args += New-CommandletArguments $commandletOut 'walk,export' ($Assets -eq 'owner') $false 128
$researchOut = Join-Path $PluginRoot "Saved\Research\step2e\$Label"
$args += @('-step2e', '-regionworkers=6', ('-researchout="{0}"' -f $researchOut))
$commandletLedger = Invoke-UnrealLaunch 'step2e-commandlet' $args `
$step2eLog (Join-Path $RunRoot 'Logs\step2e.stdout.log') `
(Join-Path $RunRoot 'Logs\step2e.stderr.log')
$step2eReportPath = Join-Path $researchOut 'step2e_report.json'
$step2eReport = Read-JsonFile $step2eReportPath
if ($null -eq $step2eReport) {
$CommandletMetrics = [ordered]@{ status = 'missing'; output = $researchOut; report = $step2eReportPath }
Add-Failure "step2e did not produce $step2eReportPath"
} else {
$CommandletMetrics = [ordered]@{
status = if ((Get-JsonValue $step2eReport 'pass' $false) -and $commandletLedger.status -eq 'passed') { 'passed' } else { 'failed' }
output = $researchOut
report = $step2eReportPath
worst_profile_meets_600ms = Get-JsonValue $step2eReport 'worst_profile_meets_600ms' $false
result = $step2eReport
}
if (-not (Get-JsonValue $step2eReport 'pass' $false)) {
Add-Failure 'step2e profile speed, detail, boundary, bound-audit, or determinism gate failed'
}
}
}
'symbolize-stacks' {
if (([string]::IsNullOrWhiteSpace($StackSamplesPath)) -or -not (Test-Path -LiteralPath $StackSamplesPath -PathType Leaf)) {
throw "-StackSamplesPath must name an existing raw sampler CSV."
@@ -1904,6 +1931,7 @@ $result = [ordered]@{
probe_amplitudes = if ($CommandletMetrics.Contains('probe_amplitudes')) { $CommandletMetrics.probe_amplitudes } else { [ordered]@{} }
commandlet = $CommandletMetrics
region_eval = if ($Scenario -eq 'region-eval') { $CommandletMetrics } else { $null }
step2e = if ($Scenario -eq 'step2e') { $CommandletMetrics } else { $null }
game = $GameMetrics
horizon = $HorizonMetrics
horizon_render = $HorizonRenderMetrics