feat(scale): size the world to a human body -- and it is NOT enough
Jahni: "can't you try and make the world to scale?" Every length parameter is now derived from the body and annotated with its metre value, so nobody has to do the arithmetic to notice something is absurd. CorridorRadius 4 -> 8 1 m -> 2 m radius (4 m bore) CellSize 40 -> 64 keeps a real wall between wider corridors TunnelMinRadius 3 -> 6 0.75 m -> 1.5 m ShaftMinRadius 5 -> 8 1.25 m -> 2 m IslandMinRadius 18 -> 24 4.5 m -> 6 m MouthRadius 6 -> 8 passage mouths 4 m plus passage reach, spiral, cascade, bridges, ridges, chasms The reasoning that motivated it still looks right: a 2 m circular bore for a 1.76 m player clears the head only along the exact centreline, so almost no cell has a floor with 7.04 voxels of clear air above it. A 4 m bore is genuinely walkable. ⚠️ BUT IT DID NOT FIX TRAVERSABILITY, and this time it is not the instrument. The player-fit law is measured at STEP 1 on a mouth-fitted window -- full resolution, both mouths inside the box -- and across the eight showcase archetypes it reports: 6 NotConnectedAtThisResolution (real negatives at full resolution) 2 OutOfWindow (unmeasurable, not passing) 1 StartCellNotPlayerFit (the arrival mouth itself is too small) 0 CONNECTED And every archetype reports hard-gate survivors=0, so the showcase is displaying the least-bad of an entirely failing set. ⛔ DO NOT READ player_fit_fraction / player_clearance FROM THE SHOWCASE LINE. Those come from the COARSE step-4 measurement in the same log line, where the 1.36-voxel player is sub-cell and fit is unmeasurable -- player_clearance reads a uniform "1.00 voxels" for all eight archetypes, which is one cell, not one voxel. Only restricted_law is at step 1 and only it is trustworthy. Fixing that mixed-resolution reporting is next. So scale was necessary and insufficient. My hypothesis was that bore diameter versus body height was the binding constraint; doubling it moved nothing measurable. Something else is preventing player-sized volume from existing at all -- worth finding before tuning any further. Codex applied these edits and then died mid-poll without validating them (third time a killed agent left a clean-looking unbuilt diff). Build and suite run by me: build clean, 30 succeeded / 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K8V9my1PL68pmQb2gutZZV
This commit is contained in:
1 parent
eb47efdec9
commit
fc43a86709
17 files changed
+1298
-349
No files matched your search
@@ -656,12 +656,12 @@ bool FVoxelForgeComposerCorpusFreeTest::RunTest(const FString& Parameters)
|
||||
const double MeasurementSeconds = FPlatformTime::Seconds() - MeasurementStartSeconds;
|
||||
|
||||
// Audit the same declared relations against the 12 current authored corpus vectors. Project
|
||||
// assets carry their own height; the eight C++ defaults use the fixture's 4-chunk height.
|
||||
// assets carry their own height; the eight C++ defaults use the fixture's 8-chunk height.
|
||||
int32 AuthoredConstraintViolations = 0;
|
||||
FString AuthoredViolationList;
|
||||
for (const FVoxelStrateCorpusEntry& Entry : Corpus.GetEntries())
|
||||
{
|
||||
float EntryHeight = 4.0f * (float)CHUNK_SIZE;
|
||||
float EntryHeight = 8.0f * (float)CHUNK_SIZE;
|
||||
if (!Entry.SourcePath.Contains(TEXT("/VoxelForge/ComposerDefaults/")))
|
||||
{
|
||||
UVoxelStrateDefinition* Definition = LoadObject<UVoxelStrateDefinition>(
|
||||
|
||||
@@ -49,7 +49,7 @@ bool FVoxelForgeComposerSeasonTest::RunTest(const FString& Parameters)
|
||||
Settings.Corpus = &Corpus;
|
||||
Settings.CandidateCount = 24;
|
||||
Settings.SelectedStrateCount = 6;
|
||||
Settings.StrateHeightInChunks = 4;
|
||||
Settings.StrateHeightInChunks = 8;
|
||||
Settings.InterStrateGapChunks = 0;
|
||||
Settings.OriginSpineRadius = 14.0f;
|
||||
Settings.WorldRadiusVoxels = 0.0f;
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace
|
||||
constexpr float ClearanceNormalizationVoxels = 64.0f;
|
||||
constexpr int32 FinePreviewSampleStep = 1;
|
||||
constexpr int32 FinePreviewRadiusVoxels = 64;
|
||||
constexpr int32 FinePreviewMaxCells = 2000000;
|
||||
constexpr int32 FinePreviewMaxCells = 4000000;
|
||||
|
||||
const ECaveGeneratorType GShowcaseArchetypes[] = {
|
||||
ECaveGeneratorType::CrystalChamber,
|
||||
@@ -125,23 +125,48 @@ namespace
|
||||
int32 EvaluatedCandidates = 0;
|
||||
TArray<FShowcaseSample> Survivors;
|
||||
TOptional<FShowcaseSample> Best;
|
||||
// Best coarse-measured candidate, retained even when a hard gate rejects it. This keeps
|
||||
// the scale audit useful when the player-fit law correctly finds no survivor.
|
||||
TOptional<FShowcaseSample> BestMeasured;
|
||||
int32 MeasuredCandidates = 0;
|
||||
FString LastFailure;
|
||||
};
|
||||
|
||||
bool VF_IsBetterShowcaseSample(const FShowcaseSample& Sample,
|
||||
const TOptional<FShowcaseSample>& Current)
|
||||
{
|
||||
return !Current.IsSet()
|
||||
|| Sample.SelectionScore > Current->SelectionScore
|
||||
|| (Sample.SelectionScore == Current->SelectionScore
|
||||
&& (Sample.Candidate.Seed < Current->Candidate.Seed
|
||||
|| (Sample.Candidate.Seed == Current->Candidate.Seed
|
||||
&& Sample.Candidate.Index < Current->Candidate.Index)));
|
||||
}
|
||||
|
||||
void VF_AddMetricSample(FShowcaseStats& Stats, const FShowcaseSample& Sample)
|
||||
{
|
||||
Stats.Survivors.Add(Sample);
|
||||
if (!Stats.Best.IsSet()
|
||||
|| Sample.SelectionScore > Stats.Best->SelectionScore
|
||||
|| (Sample.SelectionScore == Stats.Best->SelectionScore
|
||||
&& (Sample.Candidate.Seed < Stats.Best->Candidate.Seed
|
||||
|| (Sample.Candidate.Seed == Stats.Best->Candidate.Seed
|
||||
&& Sample.Candidate.Index < Stats.Best->Candidate.Index))))
|
||||
if (VF_IsBetterShowcaseSample(Sample, Stats.Best))
|
||||
{
|
||||
Stats.Best = Sample;
|
||||
}
|
||||
}
|
||||
|
||||
void VF_AddMeasuredSample(FShowcaseStats& Stats, const FShowcaseSample& Sample)
|
||||
{
|
||||
// NumSampled is the direct evidence that the coarse grid was built. Keep this diagnostic
|
||||
// path independent of the legacy gate's bool return and of any future metric-validity
|
||||
// policy changes: a rejected candidate can still be the most useful scale evidence.
|
||||
if (Sample.Candidate.bValid && Sample.Metrics.NumSampled > 0)
|
||||
{
|
||||
++Stats.MeasuredCandidates;
|
||||
if (VF_IsBetterShowcaseSample(Sample, Stats.BestMeasured))
|
||||
{
|
||||
Stats.BestMeasured = Sample;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool VF_BuildShowcaseStack(
|
||||
const VoxelForgeTest::FTestWorld& World,
|
||||
const FVoxelStrateComposerCandidate& Candidate,
|
||||
@@ -569,6 +594,396 @@ namespace
|
||||
return Report;
|
||||
}
|
||||
|
||||
FString VF_FormatWorldScaleAudit()
|
||||
{
|
||||
constexpr float VoxelMetres = 0.25f;
|
||||
constexpr float StrateHeightVoxels = 8.0f * static_cast<float>(CHUNK_SIZE);
|
||||
FString Report = TEXT(
|
||||
"WORLD SCALE AUDIT (all distances are authored in voxels; METRES = voxels * 0.25; "
|
||||
"player radius=1.36 vox/0.34 m, height=7.04 vox/1.76 m; frequency rows show nominal "
|
||||
"period=1/frequency)\n"
|
||||
"archetype | name | voxels | METRES | body/level meaning\n");
|
||||
auto Add = [&Report](const TCHAR* Archetype, const TCHAR* Name,
|
||||
float Voxels, const TCHAR* Meaning)
|
||||
{
|
||||
Report += FString::Printf(
|
||||
TEXT("%s | %s | %.2f | %.2f | %s\n"), Archetype, Name,
|
||||
Voxels, Voxels * 0.25f, Meaning);
|
||||
};
|
||||
auto AddRange = [&Report](const TCHAR* Archetype, const TCHAR* Name,
|
||||
float MinVoxels, float MaxVoxels,
|
||||
const TCHAR* Meaning)
|
||||
{
|
||||
Report += FString::Printf(
|
||||
TEXT("%s | %s | %.2f..%.2f | %.2f..%.2f | %s\n"), Archetype, Name,
|
||||
MinVoxels, MaxVoxels, MinVoxels * 0.25f, MaxVoxels * 0.25f, Meaning);
|
||||
};
|
||||
const TCHAR* TunnelNames[] = { TEXT("TunnelNetwork"), TEXT("Underwater") };
|
||||
for (const TCHAR* Archetype : TunnelNames)
|
||||
{
|
||||
Add(Archetype, TEXT("StrateHeight"), StrateHeightVoxels,
|
||||
TEXT("8 chunks: a 64 m vertical envelope for tall rooms and passages"));
|
||||
Add(Archetype, TEXT("BoundarySealThickness"), 4.0f,
|
||||
TEXT("1 m solid geological boundary; not player space"));
|
||||
Add(Archetype, TEXT("WormNetworkRange"), 64.0f,
|
||||
TEXT("16 m side-passage reach around the room/tunnel skeleton"));
|
||||
Add(Archetype, TEXT("WormFrequency (nominal period)"), 1.0f / 0.015f,
|
||||
TEXT("16.7 m noise period: about two chunks for broad bends"));
|
||||
Add(Archetype, TEXT("RoomSpacing"), 128.0f,
|
||||
TEXT("32 m room-cell spacing; leaves a 6 m radial margin around a 20 m room"));
|
||||
AddRange(Archetype, TEXT("RoomRadius"), 16.0f, 40.0f,
|
||||
TEXT("8 m minimum room to 20 m large chamber diameter"));
|
||||
Add(Archetype, TEXT("OriginRoomRadius"), 48.0f,
|
||||
TEXT("24 m hub diameter: a three-chunk boss-space anchor"));
|
||||
AddRange(Archetype, TEXT("TunnelRadius"), 6.0f, 8.0f,
|
||||
TEXT("3-4 m bores: body height plus generous headroom and fight width"));
|
||||
Add(Archetype, TEXT("MaxTunnelLength"), 360.0f,
|
||||
TEXT("90 m: reaches the worst 32 m cell-jitter neighbour with margin"));
|
||||
Add(Archetype, TEXT("TunnelWarpStrength"), 24.0f,
|
||||
TEXT("6 m lateral bend, visibly organic but smaller than the 90 m reach"));
|
||||
Add(Archetype, TEXT("SDFBlendRadius"), 4.0f,
|
||||
TEXT("1 m junction rounding, below the 1.5 m minimum tunnel radius"));
|
||||
Add(Archetype, TEXT("CaveWarpStrength"), 16.0f,
|
||||
TEXT("4 m skeleton distortion"));
|
||||
Add(Archetype, TEXT("SurfaceRoughness"), 2.0f,
|
||||
TEXT("0.5 m wall texture; bounded reach is 0.94 m"));
|
||||
Add(Archetype, TEXT("FloorReliefStrength"), 4.0f,
|
||||
TEXT("1 m floor undulation, small relative to a 3 m bore"));
|
||||
Add(Archetype, TEXT("FloorReliefFrequency (nominal period)"), 1.0f / 0.015f,
|
||||
TEXT("16.7 m floor-undulation period: broad enough to walk as a slope"));
|
||||
Add(Archetype, TEXT("RoughnessFrequency (nominal period)"), 1.0f / 0.1f,
|
||||
TEXT("2.5 m wall-detail period: a few body lengths, not voxel-grain tunnel"));
|
||||
Add(Archetype, TEXT("CaveWarpFrequency (nominal period)"), 1.0f / 0.015f,
|
||||
TEXT("16.7 m skeleton-warp period: broad landform-scale bend"));
|
||||
Add(Archetype, TEXT("DomainWarpFrequency (nominal period)"), 1.0f / 0.03f,
|
||||
TEXT("8.3 m optional roughness-warp period"));
|
||||
Add(Archetype, TEXT("CliffSampleDist (transport)"), 2.0f,
|
||||
TEXT("0.5 m optional local slope sample distance"));
|
||||
Add(Archetype, TEXT("FloorBias"), 4.0f,
|
||||
TEXT("1 m floor stabilisation applied below room centre"));
|
||||
Add(Archetype, TEXT("DomainWarpStrength"), 0.0f,
|
||||
TEXT("0 m optional roughness-coordinate warp; disabled by default"));
|
||||
Add(Archetype, TEXT("OverhangFrequency (nominal period)"), 1.0f / 0.06f,
|
||||
TEXT("4.2 m optional shelf-shape period"));
|
||||
Add(Archetype, TEXT("ScallopFrequency (nominal period)"), 1.0f / 0.1f,
|
||||
TEXT("2.5 m optional erosion-bowl period"));
|
||||
Add(Archetype, TEXT("TerraceStepHeight (transport)"), 0.0f,
|
||||
TEXT("0 m optional step; disabled by default"));
|
||||
Add(Archetype, TEXT("LayerLineSpacing (transport)"), 0.0f,
|
||||
TEXT("0 m optional geological band period; disabled by default"));
|
||||
Add(Archetype, TEXT("LayerLineDepth (transport)"), 0.3f,
|
||||
TEXT("0.075 m optional groove depth; inactive while spacing is zero"));
|
||||
Add(Archetype, TEXT("RibbingSpacing (transport)"), 0.0f,
|
||||
TEXT("0 m optional rib period; disabled by default"));
|
||||
Add(Archetype, TEXT("RibbingDepth (transport)"), 0.4f,
|
||||
TEXT("0.1 m optional rib protrusion; inactive while spacing is zero"));
|
||||
Add(Archetype, TEXT("OverhangDepth (transport)"), 8.0f,
|
||||
TEXT("2 m optional shelf; terrain-op transport is reset when disabled"));
|
||||
AddRange(Archetype, TEXT("ArchRadius (transport)"), 6.0f, 12.0f,
|
||||
TEXT("1.5-3 m optional arch feature"));
|
||||
AddRange(Archetype, TEXT("ColumnRadius (transport)"), 4.0f, 8.0f,
|
||||
TEXT("1-2 m optional pillar radius"));
|
||||
AddRange(Archetype, TEXT("PitRadius (transport)"), 8.0f, 16.0f,
|
||||
TEXT("2-4 m optional vertical opening radius"));
|
||||
Add(Archetype, TEXT("PitDepth (transport)"), 32.0f,
|
||||
TEXT("8 m optional vertical drop"));
|
||||
AddRange(Archetype, TEXT("ChimneyRadius (transport)"), 4.0f, 8.0f,
|
||||
TEXT("1-2 m optional upward shaft radius"));
|
||||
Add(Archetype, TEXT("ChimneyHeight (transport)"), 32.0f,
|
||||
TEXT("8 m optional upward shaft height"));
|
||||
AddRange(Archetype, TEXT("DomeRadius (transport)"), 16.0f, 32.0f,
|
||||
TEXT("4-8 m optional ceiling dome radius"));
|
||||
Add(Archetype, TEXT("PinchLength (transport)"), 24.0f,
|
||||
TEXT("6 m optional bottleneck length; default operation is disabled"));
|
||||
}
|
||||
|
||||
for (const TCHAR* Archetype : { TEXT("FlatPlain"), TEXT("CrystalChamber") })
|
||||
{
|
||||
Add(Archetype, TEXT("StrateHeight"), StrateHeightVoxels,
|
||||
TEXT("8 chunks / 64 m: open vertical chamber envelope"));
|
||||
Add(Archetype, TEXT("Floor height"), StrateHeightVoxels * 0.25f,
|
||||
TEXT("16 m above the strate bottom"));
|
||||
Add(Archetype, TEXT("Ceiling height"), StrateHeightVoxels * 0.60f,
|
||||
TEXT("38.4 m above the bottom; 22.4 m clear span before roughness"));
|
||||
Add(Archetype, TEXT("FloorRoughness"), 4.0f,
|
||||
TEXT("1 m rolling floor displacement"));
|
||||
Add(Archetype, TEXT("CeilingRoughness"), 6.0f,
|
||||
TEXT("1.5 m downward formation reach"));
|
||||
Add(Archetype, TEXT("FloorRoughnessFrequency (nominal period)"), 1.0f / 0.04f,
|
||||
TEXT("6.25 m floor-detail period: broad rolling ground"));
|
||||
Add(Archetype, TEXT("CeilingRoughnessFrequency (nominal period)"), 1.0f / 0.04f,
|
||||
TEXT("6.25 m ceiling-formation period"));
|
||||
AddRange(Archetype, TEXT("ColumnRadius"), 8.0f, 16.0f,
|
||||
TEXT("4-8 m pillar diameters; large enough to read without erasing a fight lane"));
|
||||
Add(Archetype, TEXT("ColumnSpacing"), 96.0f,
|
||||
TEXT("24 m pillar-cell spacing"));
|
||||
Add(Archetype, TEXT("BoundarySealThickness"), 4.0f,
|
||||
TEXT("1 m solid boundary"));
|
||||
}
|
||||
|
||||
Add(TEXT("Common layout"), TEXT("OriginSpineRadius"), 14.0f,
|
||||
TEXT("3.5 m radius / 7 m diameter central descent spine"));
|
||||
Add(TEXT("Common layout"), TEXT("InterStrateGap"), 0.0f,
|
||||
TEXT("0 m bedrock gap by default; configured as whole 8 m chunks"));
|
||||
Add(TEXT("Common layout"), TEXT("WorldRadiusVoxels"), 0.0f,
|
||||
TEXT("0 m lateral bound: radial regions remain gated off"));
|
||||
|
||||
Add(TEXT("Maze"), TEXT("StrateHeight"), StrateHeightVoxels,
|
||||
TEXT("8 chunks / 64 m: supports several 16 m maze levels"));
|
||||
Add(TEXT("Maze"), TEXT("CellSize"), 64.0f,
|
||||
TEXT("16 m lattice cells, matching ordinary fight-space scale"));
|
||||
Add(TEXT("Maze"), TEXT("CorridorRadius"), 8.0f,
|
||||
TEXT("2 m radius / 4 m circular bore: upper fight-space target"));
|
||||
Add(TEXT("Maze"), TEXT("SurfaceRoughness"), 2.0f,
|
||||
TEXT("0.5 m texture; bounded reach is 0.94 m, below the 2 m radius"));
|
||||
Add(TEXT("Maze"), TEXT("BoundarySealThickness"), 4.0f,
|
||||
TEXT("1 m solid boundary"));
|
||||
|
||||
Add(TEXT("SurfaceWorld"), TEXT("StrateHeight"), StrateHeightVoxels,
|
||||
TEXT("8 chunks / 64 m: high sky-cap envelope"));
|
||||
Add(TEXT("SurfaceWorld"), TEXT("ElevationRange"), 80.0f,
|
||||
TEXT("20 m terrain relief: human-scale hills, not a miniature room"));
|
||||
Add(TEXT("SurfaceWorld"), TEXT("Base ground height"), StrateHeightVoxels * 0.25f,
|
||||
TEXT("16 m mean ground elevation above the strate bottom"));
|
||||
Add(TEXT("SurfaceWorld"), TEXT("HeightWarpStrength"), 48.0f,
|
||||
TEXT("12 m horizontal landform distortion"));
|
||||
Add(TEXT("SurfaceWorld"), TEXT("SurfaceRoughness"), 2.0f,
|
||||
TEXT("0.5 m ground detail; bounded reach is 0.94 m"));
|
||||
Add(TEXT("SurfaceWorld"), TEXT("ContinentFrequency (nominal period)"), 1.0f / 0.006f,
|
||||
TEXT("41.7 m broad landmass period"));
|
||||
Add(TEXT("SurfaceWorld"), TEXT("MountainFrequency (nominal period)"), 1.0f / 0.012f,
|
||||
TEXT("20.8 m ridge period"));
|
||||
Add(TEXT("SurfaceWorld"), TEXT("DetailFrequency (nominal period)"), 1.0f / 0.04f,
|
||||
TEXT("6.25 m fine terrain period"));
|
||||
Add(TEXT("SurfaceWorld"), TEXT("TerraceHeight"), 8.0f,
|
||||
TEXT("2 m optional step; operation remains disabled by strength 0"));
|
||||
Add(TEXT("SurfaceWorld"), TEXT("LayerLineSpacing"), 8.0f,
|
||||
TEXT("2 m optional geological band spacing"));
|
||||
Add(TEXT("SurfaceWorld"), TEXT("LayerLineDepth"), 0.0f,
|
||||
TEXT("0 m default band displacement: optional operation disabled"));
|
||||
Add(TEXT("SurfaceWorld"), TEXT("CliffSampleDist"), 2.0f,
|
||||
TEXT("0.5 m slope sample scale for optional cliff operation"));
|
||||
Add(TEXT("SurfaceWorld"), TEXT("OverhangReach"), 16.0f,
|
||||
TEXT("4 m optional shelf reach"));
|
||||
Add(TEXT("SurfaceWorld"), TEXT("OverhangHeight"), 24.0f,
|
||||
TEXT("6 m optional shelf zone"));
|
||||
Add(TEXT("SurfaceWorld"), TEXT("BeachWidth"), 12.0f,
|
||||
TEXT("3 m shore band"));
|
||||
Add(TEXT("SurfaceWorld"), TEXT("Sky cap height"), StrateHeightVoxels * 0.95f,
|
||||
TEXT("60.8 m above the strate bottom; leaves a high open sky"));
|
||||
Add(TEXT("SurfaceWorld"), TEXT("CeilingRoughness"), 6.0f,
|
||||
TEXT("1.5 m optional downward cap detail"));
|
||||
Add(TEXT("SurfaceWorld"), TEXT("CeilingUndulation"), 0.0f,
|
||||
TEXT("0 m broad cap swell; disabled by default"));
|
||||
Add(TEXT("SurfaceWorld"), TEXT("CeilingRidgeStrength"), 0.0f,
|
||||
TEXT("0 m hanging ridge height; disabled by default"));
|
||||
Add(TEXT("SurfaceWorld"), TEXT("CeilingWarpStrength"), 0.0f,
|
||||
TEXT("0 m cap-coordinate warp; disabled by default"));
|
||||
Add(TEXT("SurfaceWorld"), TEXT("BoundarySealThickness"), 4.0f,
|
||||
TEXT("1 m solid boundary"));
|
||||
|
||||
Add(TEXT("SurfaceWorld"), TEXT("HeightWarpFrequency (nominal period)"), 1.0f / 0.008f,
|
||||
TEXT("31.25 m landform-warp period"));
|
||||
Add(TEXT("SurfaceWorld"), TEXT("ReliefFrequency (nominal period)"), 1.0f / 0.0015f,
|
||||
TEXT("166.7 m macro-region period"));
|
||||
Add(TEXT("SurfaceWorld"), TEXT("OverhangFrequency (nominal period)"), 1.0f / 0.02f,
|
||||
TEXT("12.5 m optional shelf-shape period"));
|
||||
Add(TEXT("SurfaceWorld"), TEXT("CeilingRoughnessFrequency (nominal period)"), 1.0f / 0.04f,
|
||||
TEXT("6.25 m optional cap-detail period"));
|
||||
Add(TEXT("SurfaceWorld"), TEXT("CeilingUndulationFrequency (nominal period)"), 1.0f / 0.004f,
|
||||
TEXT("62.5 m optional cap-swell period"));
|
||||
Add(TEXT("SurfaceWorld"), TEXT("CeilingRidgeFrequency (nominal period)"), 1.0f / 0.02f,
|
||||
TEXT("12.5 m optional hanging-ridge period"));
|
||||
Add(TEXT("SurfaceWorld"), TEXT("CeilingWarpFrequency (nominal period)"), 1.0f / 0.01f,
|
||||
TEXT("25 m optional cap-warp period"));
|
||||
|
||||
Add(TEXT("VerticalShafts"), TEXT("StrateHeight"), StrateHeightVoxels,
|
||||
TEXT("8 chunks / 64 m: a full vertical traversal envelope"));
|
||||
Add(TEXT("VerticalShafts"), TEXT("ShaftSpacing"), 80.0f,
|
||||
TEXT("20 m shaft-cell spacing"));
|
||||
AddRange(TEXT("VerticalShafts"), TEXT("ShaftRadius"), 8.0f, 14.0f,
|
||||
TEXT("4-7 m shaft diameters"));
|
||||
Add(TEXT("VerticalShafts"), TEXT("ConnectorRadius"), 6.0f,
|
||||
TEXT("1.5 m radius / 3 m horizontal connector bore"));
|
||||
Add(TEXT("VerticalShafts"), TEXT("LedgeSpacing"), 32.0f,
|
||||
TEXT("8 m vertical landing interval"));
|
||||
Add(TEXT("VerticalShafts"), TEXT("LedgeDepth"), 4.0f,
|
||||
TEXT("1 m landing intrusion"));
|
||||
Add(TEXT("VerticalShafts"), TEXT("SurfaceRoughness"), 2.0f,
|
||||
TEXT("0.5 m wall texture; bounded reach is 0.94 m"));
|
||||
Add(TEXT("VerticalShafts"), TEXT("BoundarySealThickness"), 4.0f,
|
||||
TEXT("1 m solid boundary"));
|
||||
|
||||
Add(TEXT("FloatingIslands"), TEXT("StrateHeight"), StrateHeightVoxels,
|
||||
TEXT("8 chunks / 64 m: vertical sky volume"));
|
||||
Add(TEXT("FloatingIslands"), TEXT("IslandSpacing"), 112.0f,
|
||||
TEXT("28 m island-cell spacing; max 24 m diameter leaves a 2 m centre-to-edge gap"));
|
||||
AddRange(TEXT("FloatingIslands"), TEXT("IslandRadius"), 24.0f, 48.0f,
|
||||
TEXT("12-24 m landmass diameters: room to cathedral scale"));
|
||||
Add(TEXT("FloatingIslands"), TEXT("Underside half-depth"), 48.0f * 0.60f,
|
||||
TEXT("7.2 m below the centre for the largest island; top half is 2.4 m"));
|
||||
Add(TEXT("FloatingIslands"), TEXT("SurfaceRoughness"), 3.0f,
|
||||
TEXT("0.75 m island texture; bounded reach is 1.41 m"));
|
||||
Add(TEXT("FloatingIslands"), TEXT("SDFBlendRadius"), 6.0f,
|
||||
TEXT("1.5 m island edge blend"));
|
||||
Add(TEXT("FloatingIslands"), TEXT("BoundarySealThickness"), 4.0f,
|
||||
TEXT("1 m solid boundary"));
|
||||
|
||||
Add(TEXT("Common passage"), TEXT("MouthRadius"), 8.0f,
|
||||
TEXT("2 m radius / 4 m bore at each strate mouth"));
|
||||
Add(TEXT("Common passage"), TEXT("MidRadius"), 6.0f,
|
||||
TEXT("1.5 m radius / 3 m minimum bore between mouths"));
|
||||
AddRange(TEXT("Common passage"), TEXT("Reach"), 32.0f, 96.0f,
|
||||
TEXT("8-24 m reach into each strate"));
|
||||
AddRange(TEXT("Common passage"), TEXT("Distance from spine"), 64.0f, 192.0f,
|
||||
TEXT("16-48 m placement range"));
|
||||
Add(TEXT("Common passage"), TEXT("Wander"), 24.0f,
|
||||
TEXT("6 m sideways meander"));
|
||||
Add(TEXT("Common passage"), TEXT("VerticalWobble"), 0.0f,
|
||||
TEXT("0 m vertical wobble by default; descent remains controlled"));
|
||||
Add(TEXT("Common passage"), TEXT("SpiralRadius"), 24.0f,
|
||||
TEXT("6 m corkscrew radius"));
|
||||
Add(TEXT("Common passage"), TEXT("CascadeLedge"), 8.0f,
|
||||
TEXT("2 m ledge run per cascade step"));
|
||||
|
||||
Add(TEXT("Common disturbance"), TEXT("ChasmSpacing"), 192.0f,
|
||||
TEXT("48 m chasm-cell spacing"));
|
||||
Add(TEXT("Common disturbance"), TEXT("ChasmRadius"), 24.0f,
|
||||
TEXT("6 m radius / 12 m opening when enabled"));
|
||||
Add(TEXT("Common disturbance"), TEXT("BridgeSpacing"), 128.0f,
|
||||
TEXT("32 m bridge-cell spacing"));
|
||||
Add(TEXT("Common disturbance"), TEXT("BridgeRadius"), 8.0f,
|
||||
TEXT("2 m radius / 4 m bridge width"));
|
||||
Add(TEXT("Common disturbance"), TEXT("RidgeSpacing"), 128.0f,
|
||||
TEXT("32 m ridge-cell spacing"));
|
||||
Add(TEXT("Common disturbance"), TEXT("RidgeHeight"), 32.0f,
|
||||
TEXT("8 m optional blade height"));
|
||||
Add(TEXT("Common disturbance"), TEXT("RidgeThickness"), 8.0f,
|
||||
TEXT("2 m optional blade half-width"));
|
||||
Add(TEXT("Common disturbance"), TEXT("BoundarySealThickness"), 4.0f,
|
||||
TEXT("1 m solid boundary shared with the active archetype"));
|
||||
|
||||
Report += FString::Printf(
|
||||
TEXT("Audit anchors: 1 voxel=%.2f m; 1 chunk=8.00 m; 2.5 m walk corridor=10 vox; "
|
||||
"3-4 m fight corridor=12-16 vox; 8-15 m room=32-60 vox; 20 m cathedral=80 vox.\n"),
|
||||
VoxelMetres);
|
||||
return Report;
|
||||
}
|
||||
|
||||
FString VF_FormatDefaultRoughnessRatios()
|
||||
{
|
||||
const float TunnelReach = VF_RoughnessReach(2.0f);
|
||||
const float SlabFloorReach = VF_RoughnessReach(4.0f);
|
||||
const float SlabCeilingReach = VF_RoughnessReach(6.0f);
|
||||
const float MazeReach = VF_RoughnessReach(2.0f);
|
||||
const float ShaftReach = VF_RoughnessReach(2.0f);
|
||||
const float IslandReach = VF_RoughnessReach(3.0f);
|
||||
return FString::Printf(
|
||||
TEXT("Default roughness-to-feature ratios (reach = roughness * 1.25 * 1.5; "
|
||||
"all values are dimensionless): TunnelNetwork=%.3f reach/%.1f-voxel "
|
||||
"minimum tunnel radius (room mean %.3f); Underwater=same; FlatPlain="
|
||||
"%.3f floor reach/mean column radius and %.3f ceiling reach/mean column radius; "
|
||||
"CrystalChamber=same slab engine; Maze=%.3f reach/%.1f-voxel corridor radius; "
|
||||
"SurfaceWorld=%.3f reach/80-voxel terrain relief; VerticalShafts=%.3f shaft-min "
|
||||
"and %.3f connector; FloatingIslands=%.3f reach/min island radius "
|
||||
"(%.3f including 6-voxel blend).\n"),
|
||||
TunnelReach / 6.0f, 6.0f, TunnelReach / 28.0f,
|
||||
SlabFloorReach / 12.0f, SlabCeilingReach / 12.0f,
|
||||
MazeReach / 8.0f, 8.0f, TunnelReach / 80.0f,
|
||||
ShaftReach / 8.0f, ShaftReach / 6.0f,
|
||||
IslandReach / 24.0f, (IslandReach + 6.0f) / 24.0f);
|
||||
}
|
||||
|
||||
float VF_ShowcaseRoughnessFeatureRatio(
|
||||
const FVoxelStrateComposerCandidate& Candidate)
|
||||
{
|
||||
switch (Candidate.Archetype)
|
||||
{
|
||||
case ECaveGeneratorType::TunnelNetwork:
|
||||
case ECaveGeneratorType::Underwater:
|
||||
return VF_RoughnessReach(
|
||||
Candidate.ArchetypeParams.TunnelNetworkParams.SurfaceRoughness)
|
||||
/ FMath::Max(
|
||||
Candidate.ArchetypeParams.TunnelNetworkParams.TunnelMinRadius,
|
||||
KINDA_SMALL_NUMBER);
|
||||
case ECaveGeneratorType::FlatPlain:
|
||||
case ECaveGeneratorType::CrystalChamber:
|
||||
{
|
||||
const FSlabGenerationParams& P = Candidate.ArchetypeParams.SlabParams;
|
||||
const float ColumnRadius = 0.5f * (P.ColumnMinRadius + P.ColumnMaxRadius);
|
||||
return FMath::Max(
|
||||
VF_RoughnessReach(P.FloorRoughness),
|
||||
VF_RoughnessReach(P.CeilingRoughness))
|
||||
/ FMath::Max(ColumnRadius, KINDA_SMALL_NUMBER);
|
||||
}
|
||||
case ECaveGeneratorType::Maze:
|
||||
return VF_RoughnessReach(Candidate.ArchetypeParams.MazeParams.SurfaceRoughness)
|
||||
/ FMath::Max(Candidate.ArchetypeParams.MazeParams.CorridorRadius,
|
||||
KINDA_SMALL_NUMBER);
|
||||
case ECaveGeneratorType::SurfaceWorld:
|
||||
return VF_RoughnessReach(Candidate.ArchetypeParams.SurfaceParams.SurfaceRoughness)
|
||||
/ FMath::Max(Candidate.ArchetypeParams.SurfaceParams.ElevationRange,
|
||||
KINDA_SMALL_NUMBER);
|
||||
case ECaveGeneratorType::VerticalShafts:
|
||||
return VF_RoughnessReach(
|
||||
Candidate.ArchetypeParams.VerticalShaftParams.SurfaceRoughness)
|
||||
/ FMath::Max(
|
||||
Candidate.ArchetypeParams.VerticalShaftParams.ShaftMinRadius,
|
||||
KINDA_SMALL_NUMBER);
|
||||
case ECaveGeneratorType::FloatingIslands:
|
||||
{
|
||||
const FFloatingIslandParams& P = Candidate.ArchetypeParams.FloatingIslandParams;
|
||||
return (VF_RoughnessReach(P.SurfaceRoughness) + P.SDFBlendRadius)
|
||||
/ FMath::Max(P.IslandMinRadius, KINDA_SMALL_NUMBER);
|
||||
}
|
||||
default:
|
||||
return 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
FString VF_FormatPlayerFitSummary(
|
||||
const TArray<FShowcaseSample>& Samples,
|
||||
const TCHAR* Label)
|
||||
{
|
||||
FString Report = FString::Printf(
|
||||
TEXT("%s (walkable floor area is the coarse 128 m x 128 m footprint proxy)\n"
|
||||
"archetype | PlayerFitFraction | TraversableComponentShare | "
|
||||
"player-fit arrival->departure | walkable floor area m2 | "
|
||||
"median vertical clearance m | roughness-to-feature ratio\n"), Label);
|
||||
for (const ECaveGeneratorType Archetype : GShowcaseArchetypes)
|
||||
{
|
||||
const FShowcaseSample* Found = Samples.FindByPredicate(
|
||||
[Archetype](const FShowcaseSample& Sample)
|
||||
{
|
||||
return Sample.Candidate.Archetype == Archetype;
|
||||
});
|
||||
if (Found == nullptr)
|
||||
{
|
||||
Report += FString::Printf(
|
||||
TEXT("%s | no measured sample | no measured sample | no measured sample | "
|
||||
"n/a | n/a | n/a\n"),
|
||||
VF_GetStrateArchetypeName(Archetype));
|
||||
continue;
|
||||
}
|
||||
const FVoxelStrateMetrics& M = Found->FineMetrics;
|
||||
const FString Law = M.bPlayerFitResolved
|
||||
? VF_ShowcaseConnectivityName(Found->PlayerFitLaw.Result)
|
||||
: TEXT("unresolved");
|
||||
const float FloorAreaM2 = Found->Metrics.WalkableFloorAreaFraction
|
||||
* 128.0f * 128.0f;
|
||||
Report += FString::Printf(
|
||||
TEXT("%s | %.6f | %.6f | %s | %.1f (%.6f footprint) | %.2f | %.3f\n"),
|
||||
VF_GetStrateArchetypeName(Archetype),
|
||||
M.bPlayerFitResolved ? M.PlayerFitFraction : 0.0f,
|
||||
M.bPlayerFitResolved ? M.TraversableComponentShare : 0.0f,
|
||||
*Law, FloorAreaM2,
|
||||
Found->Metrics.WalkableFloorAreaFraction,
|
||||
Found->Metrics.MedianVerticalClearance * 0.25f,
|
||||
VF_ShowcaseRoughnessFeatureRatio(Found->Candidate));
|
||||
}
|
||||
return Report;
|
||||
}
|
||||
|
||||
bool VF_RunMazeRoughnessExperiment(
|
||||
const VoxelForgeTest::FTestWorld& World,
|
||||
const FShowcaseSample& MazeSample,
|
||||
@@ -636,6 +1051,80 @@ namespace
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VF_RunMazeCorridorRadiusSweep(
|
||||
const VoxelForgeTest::FTestWorld& World,
|
||||
const FShowcaseSample& MazeSample,
|
||||
const FVoxelStrateMeasureSettings& BaseSettings,
|
||||
const FVoxelStrateFinePreviewSettings& FinePreviewSettings,
|
||||
FString& OutReport)
|
||||
{
|
||||
if (MazeSample.Candidate.Archetype != ECaveGeneratorType::Maze)
|
||||
{
|
||||
OutReport = TEXT("Part A Maze corridor-radius sweep unavailable: previous Maze roll was not found.");
|
||||
return false;
|
||||
}
|
||||
|
||||
FVector ArrivalPoint = FVector::ZeroVector;
|
||||
FVector DeparturePoint = FVector::ZeroVector;
|
||||
if (!VF_GetShowcaseMouthPair(
|
||||
World, MazeSample.TargetStrateIndex, ArrivalPoint, DeparturePoint))
|
||||
{
|
||||
OutReport = TEXT("Part A Maze corridor-radius sweep unavailable: mouth pair was not found.");
|
||||
return false;
|
||||
}
|
||||
|
||||
int32 TopVoxelZ = 0;
|
||||
int32 BottomVoxelZ = 0;
|
||||
if (!World.GetSlotVoxelZRange(MazeSample.TargetStrateIndex, TopVoxelZ, BottomVoxelZ))
|
||||
{
|
||||
OutReport = TEXT("Part A Maze corridor-radius sweep unavailable: Z range was not found.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const FVoxelStrateMeasureSettings FineSettings = VF_MakeFinePlayerFitSettings(
|
||||
BaseSettings, FinePreviewSettings, ArrivalPoint, DeparturePoint);
|
||||
const float RadiusSweep[] = { 4.0f, 6.0f, 8.0f, 10.0f, 12.0f };
|
||||
OutReport = TEXT(
|
||||
"Part A Maze corridor-radius sweep (same seed/index, fitted step-1 ROI; "
|
||||
"radius voxels -> bore metres):\n");
|
||||
for (const float CorridorRadius : RadiusSweep)
|
||||
{
|
||||
FVoxelStrateComposerCandidate Candidate = MazeSample.Candidate;
|
||||
Candidate.ArchetypeParams.MazeParams.CorridorRadius = CorridorRadius;
|
||||
|
||||
FVoxelOpStack Stack;
|
||||
FVoxelOpContext Context;
|
||||
FString BuildError;
|
||||
if (!VF_BuildShowcaseStack(
|
||||
World, Candidate, TopVoxelZ, BottomVoxelZ, Stack, Context, BuildError))
|
||||
{
|
||||
OutReport += FString::Printf(
|
||||
TEXT(" radius=%.0f bore=%.2f m: build failed (%s)\n"),
|
||||
CorridorRadius, CorridorRadius * 2.0f * VOXEL_SIZE / 100.0f, *BuildError);
|
||||
continue;
|
||||
}
|
||||
|
||||
Stack.PrepareChunk(Context);
|
||||
FShowcaseStackSampler Sampler(Stack);
|
||||
FVoxelStrateMetrics Metrics;
|
||||
const FVoxelConnectivityDiagnostics Law =
|
||||
VF_DiagnosePlayerFitConnectivityWithSampler(
|
||||
Sampler, BottomVoxelZ, TopVoxelZ + 1, Context.EdgeSealThickness,
|
||||
ArrivalPoint, DeparturePoint, FineSettings, &Metrics);
|
||||
OutReport += FString::Printf(
|
||||
TEXT(" radius=%.0f (%.2f m) bore=%.2f m: fit=%.6f traversable=%.6f "
|
||||
"fit_cells=%lld min_clearance=%.2f voxels (%.2f m) law=%s\n"),
|
||||
CorridorRadius, CorridorRadius * VOXEL_SIZE / 100.0f,
|
||||
CorridorRadius * 2.0f * VOXEL_SIZE / 100.0f,
|
||||
Metrics.PlayerFitFraction, Metrics.TraversableComponentShare,
|
||||
static_cast<long long>(Metrics.NumPlayerFitCells),
|
||||
Metrics.MinimumPlayerClearanceVoxels,
|
||||
Metrics.MinimumPlayerClearanceVoxels * VOXEL_SIZE / 100.0f,
|
||||
VF_ShowcaseConnectivityName(Law.Result));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
struct FIslandVerticalAudit
|
||||
{
|
||||
int32 NumIslands = 0;
|
||||
@@ -1073,7 +1562,9 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
|
||||
}
|
||||
|
||||
FTestWorld World;
|
||||
World.Build(BaseComposerSeed, 2, true);
|
||||
// The shared fixture defaults to a bounded 4-chunk volume for fast density tests. The
|
||||
// owner-facing scale audit must exercise the production/default 8-chunk envelope.
|
||||
World.Build(BaseComposerSeed, 2, true, 8);
|
||||
if (!World.IsValid())
|
||||
{
|
||||
AddError(World.WhyInvalid());
|
||||
@@ -1188,6 +1679,7 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
Stats[ArchetypeIndex].LastFailure = MoveTemp(FailureReason);
|
||||
}
|
||||
VF_AddMeasuredSample(Stats[ArchetypeIndex], Sample);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1248,6 +1740,7 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
Stats[ArchetypeIndex].LastFailure = MoveTemp(FailureReason);
|
||||
}
|
||||
VF_AddMeasuredSample(Stats[ArchetypeIndex], Sample);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1270,13 +1763,76 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
|
||||
++ArchetypeIndex)
|
||||
{
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Showcase search detail %s: eligible=%d hard_gate_survivors=%d last_failure=%s"),
|
||||
TEXT("Showcase search detail %s: eligible=%d measured=%d hard_gate_survivors=%d last_failure=%s"),
|
||||
VF_GetStrateArchetypeName(GShowcaseArchetypes[ArchetypeIndex]),
|
||||
Stats[ArchetypeIndex].EvaluatedCandidates,
|
||||
Stats[ArchetypeIndex].MeasuredCandidates,
|
||||
Stats[ArchetypeIndex].Survivors.Num(),
|
||||
Stats[ArchetypeIndex].LastFailure.IsEmpty()
|
||||
Stats[ArchetypeIndex].LastFailure.IsEmpty()
|
||||
? TEXT("none") : *Stats[ArchetypeIndex].LastFailure));
|
||||
}
|
||||
TArray<FShowcaseSample> FreshSurvivorSamples;
|
||||
for (const FShowcaseStats& ArchetypeStats : Stats)
|
||||
{
|
||||
if (ArchetypeStats.Best.IsSet())
|
||||
{
|
||||
FreshSurvivorSamples.Add(ArchetypeStats.Best.GetValue());
|
||||
}
|
||||
}
|
||||
AddInfo(VF_FormatPlayerFitSummary(
|
||||
FreshSurvivorSamples, TEXT("AFTER defaults: fresh hard-gate survivors")));
|
||||
|
||||
TArray<FShowcaseSample> FreshDiagnosticSamples;
|
||||
FreshDiagnosticSamples.Reserve(UE_ARRAY_COUNT(GShowcaseArchetypes));
|
||||
for (const FShowcaseStats& ArchetypeStats : Stats)
|
||||
{
|
||||
if (!ArchetypeStats.BestMeasured.IsSet())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const FShowcaseSample Measured = ArchetypeStats.BestMeasured.GetValue();
|
||||
const FVoxelStrateComposerCandidate DiagnosticCandidate = Measured.Candidate;
|
||||
FShowcaseSample Diagnostic;
|
||||
FString DiagnosticFailure;
|
||||
// Re-run the selected coarse measurement with the player-fit pass enabled even when a
|
||||
// legacy gate fails. This is diagnostic only: it never enters Survivors or changes card
|
||||
// selection, but it gives every archetype an honest before/after scale row.
|
||||
VF_EvaluateShowcaseCandidate(
|
||||
World, DiagnosticCandidate, Measured.TargetStrateIndex, MeasureSettings,
|
||||
Diagnostic, &DiagnosticFailure, true);
|
||||
if (Diagnostic.Candidate.bValid && Diagnostic.Metrics.NumSampled > 0)
|
||||
{
|
||||
FreshDiagnosticSamples.Add(MoveTemp(Diagnostic));
|
||||
}
|
||||
}
|
||||
AddInfo(VF_FormatPlayerFitSummary(
|
||||
FreshDiagnosticSamples,
|
||||
TEXT("AFTER defaults: best measured candidate per archetype (diagnostic; hard gates separate)")));
|
||||
|
||||
TArray<FShowcaseSample> PreviewSelections;
|
||||
PreviewSelections.Reserve(UE_ARRAY_COUNT(GShowcaseArchetypes));
|
||||
bool bHaveAllPreviewSelections = true;
|
||||
for (int32 ArchetypeIndex = 0;
|
||||
ArchetypeIndex < UE_ARRAY_COUNT(GShowcaseArchetypes);
|
||||
++ArchetypeIndex)
|
||||
{
|
||||
if (Stats[ArchetypeIndex].Best.IsSet())
|
||||
{
|
||||
PreviewSelections.Add(Stats[ArchetypeIndex].Best.GetValue());
|
||||
continue;
|
||||
}
|
||||
const FShowcaseSample* Diagnostic = FreshDiagnosticSamples.FindByPredicate(
|
||||
[Archetype = GShowcaseArchetypes[ArchetypeIndex]](const FShowcaseSample& Sample)
|
||||
{
|
||||
return Sample.Candidate.Archetype == Archetype;
|
||||
});
|
||||
if (Diagnostic == nullptr)
|
||||
{
|
||||
bHaveAllPreviewSelections = false;
|
||||
break;
|
||||
}
|
||||
PreviewSelections.Add(*Diagnostic);
|
||||
}
|
||||
|
||||
struct FPreviousShowcaseSelection
|
||||
{
|
||||
@@ -1358,6 +1914,10 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Previous eight showcase candidates rejected by the new player-fit gate: %d/8."),
|
||||
NumPreviousShowcaseRejected));
|
||||
AddInfo(VF_FormatPlayerFitSummary(
|
||||
PreviousSamples, TEXT("AFTER defaults: historical candidate re-measurement")));
|
||||
AddInfo(VF_FormatWorldScaleAudit());
|
||||
AddInfo(VF_FormatDefaultRoughnessRatios());
|
||||
AddInfo(VF_FormatRoughnessRadiusReport(PreviousSamples));
|
||||
FString MazeRoughnessReport;
|
||||
if (const FShowcaseSample* MazeSample = PreviousSamples.FindByPredicate(
|
||||
@@ -1368,6 +1928,10 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
VF_RunMazeRoughnessExperiment(
|
||||
World, *MazeSample, MeasureSettings, FinePreviewSettings, MazeRoughnessReport);
|
||||
FString MazeRadiusReport;
|
||||
VF_RunMazeCorridorRadiusSweep(
|
||||
World, *MazeSample, MeasureSettings, FinePreviewSettings, MazeRadiusReport);
|
||||
AddInfo(MazeRadiusReport);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1392,8 +1956,13 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
|
||||
}
|
||||
if (!bHaveAllArchetypes)
|
||||
{
|
||||
AddInfo(TEXT("Showcase cards are not written because the new hard gate found no complete "
|
||||
"eight-archetype survivor set; the diagnostic report above is the result."));
|
||||
AddInfo(TEXT("No complete eight-archetype hard-gate survivor set was found; the fresh "
|
||||
"preview cards below are rejected diagnostic candidates for scale review."));
|
||||
}
|
||||
if (!bHaveAllPreviewSelections)
|
||||
{
|
||||
AddInfo(TEXT("Fresh diagnostic previews are incomplete because at least one archetype had "
|
||||
"no measurable candidate."));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1429,8 +1998,9 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
|
||||
ArchetypeIndex < UE_ARRAY_COUNT(GShowcaseArchetypes);
|
||||
++ArchetypeIndex)
|
||||
{
|
||||
const FShowcaseSample& Selected = Stats[ArchetypeIndex].Best.GetValue();
|
||||
const FShowcaseSample& Selected = PreviewSelections[ArchetypeIndex];
|
||||
const FVoxelStrateComposerCandidate& Candidate = Selected.Candidate;
|
||||
const bool bDiagnosticCard = !bHaveAllArchetypes;
|
||||
|
||||
// The output filename namespace is local to this page. Keep it distinct even if a future
|
||||
// search selects equal candidate indices from two different composer seeds; the PIE fields
|
||||
@@ -1484,7 +2054,11 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
|
||||
ShowcaseDirectory, RenderIndex,
|
||||
VF_FormatNativeParameterRecipe(Candidate),
|
||||
SampleGrid, Metrics, MeasureSettings.HeadroomCells,
|
||||
Selected.SelectionScore, Selected.LawText, false, TEXT(""),
|
||||
Selected.SelectionScore, Selected.LawText, bDiagnosticCard,
|
||||
bDiagnosticCard
|
||||
? TEXT("Diagnostic only: no complete hard-gate survivor set; this card is retained "
|
||||
"to inspect the generated scale and exact player-fit law.")
|
||||
: TEXT(""),
|
||||
PreviewCandidate, PreviewError);
|
||||
TestTrue(FString::Printf(TEXT("selected %s coarse preview writes"),
|
||||
VF_GetStrateArchetypeName(Candidate.Archetype)),
|
||||
@@ -1516,6 +2090,11 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
|
||||
const FVoxelStrateMetrics FineMetrics = VF_MeasureStrateWithSampler(
|
||||
Sampler, Selected.BottomVoxelZ, Selected.TopVoxelZ + 1, Context.EdgeSealThickness,
|
||||
FineMeasureSettings, &FineGrid);
|
||||
FVoxelStrateMetrics FinePlayerMetrics;
|
||||
VF_DiagnosePlayerFitConnectivityWithSampler(
|
||||
Sampler, Selected.BottomVoxelZ, Selected.TopVoxelZ + 1,
|
||||
Context.EdgeSealThickness, FineArrivalPoint, FineDeparturePoint,
|
||||
FineMeasureSettings, &FinePlayerMetrics);
|
||||
const auto IsInsideFineXY = [&FineGrid](const FVector& Point)
|
||||
{
|
||||
return Point.X >= FineGrid.MinX && Point.X < FineGrid.MaxX
|
||||
@@ -1531,7 +2110,7 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
|
||||
!FineMetrics.bValid || FineGrid.CellCount <= FinePreviewMaxCells);
|
||||
TestTrue(FString::Printf(TEXT("selected %s fine player-fit metrics rerun identically"),
|
||||
VF_GetStrateArchetypeName(Candidate.Archetype)),
|
||||
VF_ShowcaseMetricsEqual(Selected.FineMetrics, FineMetrics));
|
||||
VF_ShowcaseMetricsEqual(Selected.FineMetrics, FinePlayerMetrics));
|
||||
const bool bFineWritten = VF_WriteStratePreviewFineCandidate(
|
||||
ShowcaseDirectory, RenderIndex, FineGrid,
|
||||
FineMeasureSettings.HeadroomCells,
|
||||
@@ -1546,17 +2125,17 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
|
||||
VF_GetStrateArchetypeName(Candidate.Archetype), *PreviewError));
|
||||
return false;
|
||||
}
|
||||
PreviewCandidate.bPlayerFitResolved = FineMetrics.bPlayerFitResolved;
|
||||
PreviewCandidate.PlayerFitRefusalReason = FineMetrics.PlayerFitRefusalReason;
|
||||
PreviewCandidate.NumPlayerFitCells = FineMetrics.NumPlayerFitCells;
|
||||
PreviewCandidate.PlayerFitFraction = FineMetrics.PlayerFitFraction;
|
||||
PreviewCandidate.NumTraversableComponents = FineMetrics.NumTraversableComponents;
|
||||
PreviewCandidate.bPlayerFitResolved = FinePlayerMetrics.bPlayerFitResolved;
|
||||
PreviewCandidate.PlayerFitRefusalReason = FinePlayerMetrics.PlayerFitRefusalReason;
|
||||
PreviewCandidate.NumPlayerFitCells = FinePlayerMetrics.NumPlayerFitCells;
|
||||
PreviewCandidate.PlayerFitFraction = FinePlayerMetrics.PlayerFitFraction;
|
||||
PreviewCandidate.NumTraversableComponents = FinePlayerMetrics.NumTraversableComponents;
|
||||
PreviewCandidate.LargestTraversableComponentCells =
|
||||
FineMetrics.LargestTraversableComponentCells;
|
||||
PreviewCandidate.TraversableComponentShare = FineMetrics.TraversableComponentShare;
|
||||
FinePlayerMetrics.LargestTraversableComponentCells;
|
||||
PreviewCandidate.TraversableComponentShare = FinePlayerMetrics.TraversableComponentShare;
|
||||
PreviewCandidate.MinimumPlayerClearanceVoxels =
|
||||
FineMetrics.MinimumPlayerClearanceVoxels;
|
||||
if (FineMetrics.bValid)
|
||||
FinePlayerMetrics.MinimumPlayerClearanceVoxels;
|
||||
if (FinePlayerMetrics.bValid)
|
||||
{
|
||||
TestTrue(FString::Printf(TEXT("selected %s fine preview has filled and contour views"),
|
||||
VF_GetStrateArchetypeName(Candidate.Archetype)),
|
||||
@@ -1581,17 +2160,28 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
|
||||
PreviewCandidate.ComposerSeed = Candidate.Seed;
|
||||
PreviewCandidate.ComposerTargetStrateIndex = Selected.TargetStrateIndex;
|
||||
PreviewCandidate.bComposerRollStructure = false;
|
||||
PreviewCandidate.SelectionReason = FString::Printf(
|
||||
TEXT("Hard gates passed: non-vacuous, largest air component >= %.2f, and "
|
||||
"player-fit arrival → departure connectivity (fine step=1; "
|
||||
"fit cells=%lld). Selected by legacy score "
|
||||
"floor_area + 0.25*clamp(clearance/%d,0,1) + 0.10*largest_surface = %.6f; "
|
||||
"floor area is the primary walking signal. Applied to existing target slot %d."),
|
||||
LargestComponentSurvivalThreshold,
|
||||
static_cast<long long>(FineMetrics.NumPlayerFitCells),
|
||||
static_cast<int32>(ClearanceNormalizationVoxels),
|
||||
Selected.SelectionScore,
|
||||
Selected.TargetStrateIndex);
|
||||
PreviewCandidate.SelectionReason = bDiagnosticCard
|
||||
? FString::Printf(
|
||||
TEXT("Diagnostic only: the complete hard-gate set was not found. Legacy law=%s; "
|
||||
"player-fit law=%s; selected by the same score "
|
||||
"floor_area + 0.25*clamp(clearance/%d,0,1) + 0.10*largest_surface = %.6f; "
|
||||
"applied to existing target slot %d."),
|
||||
VF_ShowcaseConnectivityName(Selected.LegacyLaw.Result),
|
||||
VF_ShowcaseConnectivityName(Selected.PlayerFitLaw.Result),
|
||||
static_cast<int32>(ClearanceNormalizationVoxels),
|
||||
Selected.SelectionScore,
|
||||
Selected.TargetStrateIndex)
|
||||
: FString::Printf(
|
||||
TEXT("Hard gates passed: non-vacuous, largest air component >= %.2f, and "
|
||||
"player-fit arrival → departure connectivity (fine step=1; "
|
||||
"fit cells=%lld). Selected by legacy score "
|
||||
"floor_area + 0.25*clamp(clearance/%d,0,1) + 0.10*largest_surface = %.6f; "
|
||||
"floor area is the primary walking signal. Applied to existing target slot %d."),
|
||||
LargestComponentSurvivalThreshold,
|
||||
static_cast<long long>(FineMetrics.NumPlayerFitCells),
|
||||
static_cast<int32>(ClearanceNormalizationVoxels),
|
||||
Selected.SelectionScore,
|
||||
Selected.TargetStrateIndex);
|
||||
PreviewCandidates.Add(MoveTemp(PreviewCandidate));
|
||||
|
||||
TestTrue(FString::Printf(TEXT("selected %s retains a valid exact coarse window"),
|
||||
@@ -1603,7 +2193,7 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
|
||||
ArchetypeIndex < UE_ARRAY_COUNT(GShowcaseArchetypes);
|
||||
++ArchetypeIndex)
|
||||
{
|
||||
const FShowcaseSample& Selected = Stats[ArchetypeIndex].Best.GetValue();
|
||||
const FShowcaseSample& Selected = PreviewSelections[ArchetypeIndex];
|
||||
Summaries[ArchetypeIndex].Window = PreviewCandidates[ArchetypeIndex].Window;
|
||||
Summaries[ArchetypeIndex].WindowSummary = VF_FormatShowcaseSummaryWindow(
|
||||
Stats[ArchetypeIndex]);
|
||||
|
||||
@@ -104,7 +104,7 @@ namespace
|
||||
// retourne ce garde-fou : il EXIGE maintenant des colonnes.
|
||||
|
||||
/**
|
||||
* ⚠️ DENSIFIÉ après le premier run vert. Aux défauts (`RoomSpacing = 80`, `RoomDensity = 0.35`)
|
||||
* ⚠️ DENSIFIÉ après le premier run vert. Aux anciens défauts (`RoomSpacing = 80`, `RoomDensity = 0.35`)
|
||||
* le premier passage a rendu **65 échantillons en grotte sur 6000, soit 1,1 %** : bit-identique,
|
||||
* oui, mais en comparant surtout du roc plein à du roc plein, là où le carve et les vers ne
|
||||
* s'exécutent même pas. Le compteur avait été écrit exactement pour dire ça, et il l'a dit ;
|
||||
@@ -125,14 +125,14 @@ namespace
|
||||
*/
|
||||
void EnableTunnelFeatures(FStrateGenerationParams& P)
|
||||
{
|
||||
P.RoomSpacing = 42.0f; // 80 → 42 : des salles à portée de chaque chunk échantillonné
|
||||
P.RoomDensity = 0.85f; // 0.35 → 0.85
|
||||
P.RoomSpacing = 42.0f; // current production scale → 42: rooms reach sampled chunks
|
||||
P.RoomDensity = 0.85f; // production .35 → .85
|
||||
P.VerticalScale = 1.35f; // ≠ 1 ⇒ le Z « effectif » diverge du Z monde partout
|
||||
|
||||
// ── ÉTAPE B1 : rugosité de paroi (4b) ────────────────────────────────────────────────
|
||||
// Écrits EXPLICITEMENT, pas laissés au défaut : un test qui dépend d'un défaut se casse en
|
||||
// silence le jour où le défaut change. `SurfaceRoughness` était le seul de ces champs non nul
|
||||
// par défaut (5.0), et l'étape A le remettait à zéro — c'est ce zéro qui disparaît ici.
|
||||
// silence le jour où le défaut change. The production default is now 2.0, but this fixture
|
||||
// deliberately uses 5.0 so the full roughness branch is exercised.
|
||||
P.SurfaceRoughness = 5.0f;
|
||||
P.RoughnessFrequency = 0.1f;
|
||||
P.RoughnessNoiseType = EVoxelNoiseType::FBM; // les 4 types sont balayés au contrôle 1c
|
||||
@@ -450,7 +450,9 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
|
||||
}
|
||||
|
||||
const int32 MidChunkZ = ((TopVoxelZ + BottomVoxelZ) / 2) / CHUNK_SIZE;
|
||||
FStrateGenerationParams P = World.StrateManager->GetGenerationParams(FIntVector(0, 0, MidChunkZ));
|
||||
const FStrateGenerationParams ProductionP =
|
||||
World.StrateManager->GetGenerationParams(FIntVector(0, 0, MidChunkZ));
|
||||
FStrateGenerationParams P = ProductionP;
|
||||
|
||||
if (P.StrateTopWorldZ - P.StrateBottomWorldZ <= 0.0f)
|
||||
{
|
||||
@@ -1115,8 +1117,8 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
|
||||
float WorstViolation = 0.0f;
|
||||
TMap<FString, int32> SolidKillerCounts;
|
||||
int32 NumRoomKilled = 0, NumTilesAwayFromSpine = 0;
|
||||
// ±320 voxels = 4 x RoomSpacing. Hors de la boucle : la ligne de rapport en a besoin, et
|
||||
// une étendue d'échantillonnage qu'on ne peut pas citer est une étendue qu'on ne surveille pas.
|
||||
// Fixed ±320-voxel probe envelope. The report computes its ratio to the active
|
||||
// RoomSpacing; keeping the envelope fixed makes before/after tile counts comparable.
|
||||
const int32 SpanCells = 40;
|
||||
const int32 SpanVoxelsReported = SpanCells * 8; // Extent = Step * Cells = 1 * 8
|
||||
int32 TilesHitByRooms = 0, TilesHitByTunnels = 0, TilesHitByPits = 0, TilesHitByChimneys = 0;
|
||||
@@ -1133,9 +1135,9 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
|
||||
// ⚠️ L'ÉTENDUE XY ÉTAIT ±32 VOXELS, ET C'EST CE QUI RENDAIT CE BLOC INEXPLOITABLE.
|
||||
// `RandRange(-4, 4) * 8` échantillonnait 40 tuiles dans un cube de ±32 voxels autour de
|
||||
// (0,0) — c'est-à-dire l'endroit le PLUS creusé du monde entier, et de loin :
|
||||
// • `RoomSpacing = 80`, donc ±32 ne couvre même pas la moitié d'UNE cellule de salle ;
|
||||
// • `OriginRoomRadius = 20` garantit une grosse salle exactement à (0,0), de rayon de
|
||||
// cull `max(20·1.5, 8) + 3·4 = 42` — qui avale la quasi-totalité de la fenêtre ;
|
||||
// • the old `RoomSpacing = 80` meant ±32 did not cover half of ONE room cell ;
|
||||
// • the old `OriginRoomRadius = 20` guaranteed a large room at (0,0), whose cull
|
||||
// radius `max(20·1.5, 8) + 3·4 = 42` swallowed almost the whole window ;
|
||||
// • la spine (0,0) descend précisément là.
|
||||
// La mesure « 4.9 salles sur 7.2 atteignent la boîte » ne décrivait donc pas la densité
|
||||
// de grottes du monde, elle décrivait le hub de la spine. Aucune conclusion sur la
|
||||
@@ -1146,9 +1148,8 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
|
||||
// produirait un verdict FAUX échoue exactement comme avant. On corrige ce que la mesure
|
||||
// REGARDE, pas ce qu'elle exige.
|
||||
//
|
||||
// The XY extent was ±32 voxels around (0,0) -- with RoomSpacing = 80 and a guaranteed
|
||||
// OriginRoomRadius = 20 room at the origin, that samples the single most cave-dense spot
|
||||
// in the world and says nothing about deep rock. Widening changes what the measurement
|
||||
// The old XY extent was ±32 voxels around (0,0) -- inside the guaranteed origin room.
|
||||
// Widening changes what the measurement
|
||||
// LOOKS AT, not what it demands: every verdict is still brute-forced below.
|
||||
const FIntVector Origin(
|
||||
Rng.RandRange(-SpanCells, SpanCells) * Extent,
|
||||
@@ -1300,7 +1301,7 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
|
||||
// A warning that fires every run and always means "this is fine" is noise that
|
||||
// trains the reader to ignore warnings. Zero proved is the only possible answer on
|
||||
// the dense fixture (info); on production defaults it would be a real regression
|
||||
// from 11 (warning).
|
||||
// from the previous measured count (warning).
|
||||
const FString ZeroMsg = FString::Printf(
|
||||
TEXT("[%s] No tile was proved, so the brute force verified nothing -- it has no ")
|
||||
TEXT("verdict to contradict. Do NOT re-derive the cause: read the two lines above, ")
|
||||
@@ -1326,15 +1327,14 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
|
||||
// LES DEUX MONDES, ET POURQUOI IL EN FAUT DEUX
|
||||
//-------------------------------------------------------------------------
|
||||
// ⚠️ LA FIXTURE REND CE VERDICT STRUCTURELLEMENT IMPOSSIBLE, ET CE N'EST PAS UN DÉFAUT DE LA
|
||||
// FIXTURE. `EnableTunnelFeatures` densifie délibérément (`RoomSpacing` 80 → 42,
|
||||
// `RoomDensity` 0.35 → 0.85) parce qu'aux défauts le premier run n'avait que 1,1 % des
|
||||
// FIXTURE. `EnableTunnelFeatures` densifie délibérément (production spacing → 42,
|
||||
// `RoomDensity` 0.35 → 0.85) because the first run had only 1.1% of samples in open cave,
|
||||
// échantillons en grotte — l'équivalence comparait du roc plein à du roc plein. Cette
|
||||
// densification est ce qui rend le contrôle 1 SIGNIFIANT.
|
||||
//
|
||||
// Mais elle est exactement ANTAGONISTE de la prouvabilité, et l'arithmétique le dit sans
|
||||
// ambiguïté : le rayon de cull d'une salle vaut `max(R·1.5, R·HeightRatio) + 3·SDFBlendRadius`,
|
||||
// soit `1.5R + 12` ⇒ entre 27 et 57 pour `R ∈ [10, 30]`, moyenne ≈ 42 — c'est-à-dire
|
||||
// **exactement le pas du réseau**, à 85 % d'occupation. Des sphères de cull de rayon égal au pas
|
||||
// soit `1.5R + 12`; at the dense fixture's 85% occupancy those cull spheres overlap the lattice
|
||||
// du réseau recouvrent l'espace ~3,6 fois. **Aucune boîte de ce monde ne peut être hors de
|
||||
// toutes les sphères de cull.** Le « 6.3 salles sur 8.3 atteignent la boîte » mesuré est
|
||||
// exactement ça, et élargir l'échantillonneur n'y a rien changé (39 tuiles sur 40 étaient déjà
|
||||
@@ -1353,9 +1353,7 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
|
||||
RunTileScan(Stack, P, Ctx, TEXT("dense fixture"), /*bZeroProvedIsExpected*/ true);
|
||||
|
||||
{
|
||||
FStrateGenerationParams SparseP = P;
|
||||
SparseP.RoomSpacing = 80.0f; // le défaut d'`UVoxelStrateDefinition`
|
||||
SparseP.RoomDensity = 0.35f; // idem — voir `EnableTunnelFeatures`
|
||||
FStrateGenerationParams SparseP = ProductionP;
|
||||
|
||||
FVoxelOpStack SparseStack;
|
||||
VoxelDensityOps::BuildTunnelNetworkStack(SparseStack, SparseP, World.Settings->Seed,
|
||||
|
||||
@@ -52,7 +52,10 @@ namespace
|
||||
case ECaveGeneratorType::Maze:
|
||||
return FMath::Max(Definition.MazeParams.CellSize, 1.0f);
|
||||
case ECaveGeneratorType::VerticalShafts:
|
||||
return FMath::Max(Definition.VerticalShaftParams.ShaftSpacing, 1.0f);
|
||||
// A 0.6 shaft density does not guarantee an occupied immediate cell. The production
|
||||
// query therefore permits a bounded two-cell local search and still returns the
|
||||
// selected site's exact axis.
|
||||
return FMath::Max(2.0f * Definition.VerticalShaftParams.ShaftSpacing, 1.0f);
|
||||
case ECaveGeneratorType::FloatingIslands:
|
||||
return FMath::Max(Definition.FloatingIslandParams.IslandSpacing, 1.0f);
|
||||
default:
|
||||
|
||||
@@ -2107,7 +2107,9 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
|
||||
|
||||
FVoxelStrateMeasureSettings BaseSettings;
|
||||
BaseSettings.CenterXY = FVector2D::ZeroVector;
|
||||
BaseSettings.MaxCells = 8000000;
|
||||
// The centered control stays bounded after the 4 m vertical-volume fixture is retained;
|
||||
// 12 M also leaves room for the scale-aware 320-voxel control below.
|
||||
BaseSettings.MaxCells = 12000000;
|
||||
BaseSettings.HeadroomCells = 2;
|
||||
BaseSettings.InteriorMarginVoxels = -1;
|
||||
|
||||
@@ -2121,8 +2123,8 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
|
||||
{4, 256},
|
||||
{4, 192},
|
||||
{2, 192},
|
||||
{4, 128},
|
||||
{1, 128},
|
||||
{4, 160},
|
||||
{1, 160},
|
||||
};
|
||||
|
||||
bool bAllChecksPassed = true;
|
||||
@@ -2458,7 +2460,7 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
|
||||
}
|
||||
Summary += TEXT(
|
||||
"Controlled comparisons hold radius constant: (step 4, radius 192) vs (step 2, "
|
||||
"radius 192), and (step 4, radius 128) vs (step 1, radius 128).\n");
|
||||
"radius 192), and (step 4, radius 160) vs (step 1, radius 160).\n");
|
||||
Summary += FString::Printf(
|
||||
TEXT("VerticalShafts refinement sweep wall-clock: %.3f seconds.\n"),
|
||||
SweepSeconds);
|
||||
@@ -3236,10 +3238,17 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
|
||||
// PART A/B — the production acceptance window follows the actual two mouths. The older
|
||||
// centered-window diagnostics above remain useful for the origin-spine invariant; they are
|
||||
// deliberately not reused here because a mouth-fitted box is not expected to contain (0,0).
|
||||
constexpr int32 FittedMaxCells = 20000000;
|
||||
// The tree's prescribed neighbour fallback reaches three shaft-grid cells. A mouth window
|
||||
// must therefore cover three spacings beyond the endpoints before it can be called a fitted
|
||||
// diagnostic box. This grows with the body-scale shaft spacing rather than preserving the
|
||||
// old 48-voxel window by habit.
|
||||
const FVerticalShaftParams& FittedShaftParams =
|
||||
Layout[VerticalShaftsIndex].Definition->VerticalShaftParams;
|
||||
const float FittedMargin = FMath::Max(
|
||||
48.0f, 3.0f * FMath::Max(FittedShaftParams.ShaftSpacing, 1.0f));
|
||||
const float DoubledFittedMargin = 2.0f * FittedMargin;
|
||||
constexpr int32 FittedMaxCells = 120000000;
|
||||
constexpr int32 BeforeChangeMaxCells = 12000000;
|
||||
constexpr float FittedMargin = 48.0f;
|
||||
constexpr float DoubledFittedMargin = 96.0f;
|
||||
|
||||
auto MakeFittedSettings = [&](int32 SampleStep,
|
||||
const FVector& A,
|
||||
@@ -3297,8 +3306,8 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
|
||||
};
|
||||
|
||||
Summary += TEXT(
|
||||
"PART A — fitted VerticalShafts mouth window (AABB of the two XY mouths + 48 voxels; "
|
||||
"step 1/2; fitted MaxCells=20000000):\n");
|
||||
"PART A — fitted VerticalShafts mouth window (AABB of the two XY mouths + three shaft "
|
||||
"spacings; step 1/2; doubled-margin control; fitted MaxCells=120000000):\n");
|
||||
Summary += TEXT(
|
||||
" seed | step | default margin verdict | fitted dimensions/cells | doubled-margin check | "
|
||||
"interpretation | arrival component share | departure component share\n");
|
||||
@@ -3341,12 +3350,21 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
|
||||
{
|
||||
AddError(FString::Printf(
|
||||
TEXT("HARD FAILURE: fitted-window seed %d has a VerticalShafts mouth not on a "
|
||||
"shaft axis (arrival axis=%s, departure axis=%s)."),
|
||||
"shaft axis (arrival axis=%s, departure axis=%s; arrival=(%.3f,%.3f,%.3f); "
|
||||
"departure=(%.3f,%.3f,%.3f); spacing=%.3f; density=%.3f)."),
|
||||
Seed,
|
||||
IsVerticalShaftAxis(ShaftParams, Seed, ResolutionArrivalPoint)
|
||||
? TEXT("yes") : TEXT("no"),
|
||||
IsVerticalShaftAxis(ShaftParams, Seed, ResolutionDeparturePoint)
|
||||
? TEXT("yes") : TEXT("no")));
|
||||
? TEXT("yes") : TEXT("no"),
|
||||
ResolutionArrivalPoint.X,
|
||||
ResolutionArrivalPoint.Y,
|
||||
ResolutionArrivalPoint.Z,
|
||||
ResolutionDeparturePoint.X,
|
||||
ResolutionDeparturePoint.Y,
|
||||
ResolutionDeparturePoint.Z,
|
||||
ShaftParams.ShaftSpacing,
|
||||
ShaftParams.ShaftDensity));
|
||||
bPartAFittedChecksPassed = false;
|
||||
}
|
||||
|
||||
@@ -3491,13 +3509,13 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
|
||||
}
|
||||
|
||||
Summary += TEXT(
|
||||
"PART B — fitted 16-seed audit (step 2, margin 48): strict-local-minimum shafts are the "
|
||||
"PART B — fitted 16-seed audit (step 2, margin=three shaft spacings): strict-local-minimum shafts are the "
|
||||
"pre-fix orphan candidates; post-fix parent paths are checked with the same fixed ±2/±3 "
|
||||
"resolver over a padded [-20,20] cell audit domain. Small-component threshold is one "
|
||||
"mean-radius shaft volume.\n");
|
||||
Summary += TEXT(
|
||||
" seed | fitted verdict | cells | components | small non-largest/total | strict minima/shafts "
|
||||
"| neighbour fallback | path orphans | axis mouths | doubled margin 96 verdict/cells | "
|
||||
"| neighbour fallback | path orphans | axis mouths | doubled fitted-margin verdict/cells | "
|
||||
"tree path XY bounds\n");
|
||||
|
||||
static constexpr int32 FittedSeedCases[] = {
|
||||
@@ -3859,7 +3877,7 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
|
||||
SmallComponentThresholdMin == INT32_MAX ? 0 : SmallComponentThresholdMin,
|
||||
SmallComponentThresholdMax);
|
||||
Summary += FString::Printf(
|
||||
TEXT(" fitted acceptance arrival->departure: %s (%d/%d effective pass; direct margin48="
|
||||
TEXT(" fitted acceptance arrival->departure: %s (%d/%d effective pass; direct fitted margin="
|
||||
"%d/%d, %d fail; binding negatives=%d, trusted negatives=%d, measurement limits=%d); "
|
||||
"sweep wall-clock=%.3f seconds.\n"),
|
||||
SweepPasses == UE_ARRAY_COUNT(FittedSeedCases) && SweepFailures == 0
|
||||
|
||||
@@ -73,7 +73,8 @@ namespace VoxelForgeTest
|
||||
* dangerous — and `VoxelForge.Determinism.LargeSeedSurvives` deliberately passes big ones
|
||||
* (up to 2e9) to prove it stays that way.
|
||||
*/
|
||||
void Build(int32 InSeed = 1337, int32 InGapChunks = 2, bool bUseOperatorStack = false)
|
||||
void Build(int32 InSeed = 1337, int32 InGapChunks = 2, bool bUseOperatorStack = false,
|
||||
int32 InStrateHeightInChunks = 4)
|
||||
{
|
||||
Settings = TStrongObjectPtr<UVoxelSettings>(
|
||||
NewObject<UVoxelSettings>(GetTransientPackage(), NAME_None, RF_Transient));
|
||||
@@ -105,7 +106,11 @@ namespace VoxelForgeTest
|
||||
UVoxelStrateDefinition* Def = NewObject<UVoxelStrateDefinition>(
|
||||
GetTransientPackage(), NAME_None, RF_Transient);
|
||||
Def->GeneratorType = Archetypes[i];
|
||||
Def->StrateHeightInChunks = 4;
|
||||
// Keep the shared density fixture at the historical 4-chunk test volume unless
|
||||
// a caller explicitly asks for the production/default 8-chunk envelope. The
|
||||
// refinement and roll tests use the smaller synthetic volume as a bounded memory
|
||||
// control; the owner-facing showcase opts into 8 chunks below.
|
||||
Def->StrateHeightInChunks = FMath::Max(1, InStrateHeightInChunks);
|
||||
// L'OPT-IN de la pile d'opérateurs. Faux par défaut : les treize tests existants
|
||||
// doivent continuer à exercer le `switch`, qui reste le comportement de référence.
|
||||
// Seul le test de solidité de ClassifyTie côté pile le passe à vrai.
|
||||
|
||||
@@ -634,6 +634,13 @@ namespace
|
||||
// than a point on the smoothed ledge edge.
|
||||
float LandingFromBottom = FMath::Max(
|
||||
Params.BoundarySealThickness, Params.LedgeDepth) + FloorClearance;
|
||||
// The measurement API derives its default interior margin as two seal thicknesses.
|
||||
// Keep a source-level landing strictly inside that same measured window; otherwise a
|
||||
// lower-roughness default can produce a perfectly valid shaft landing just below the
|
||||
// diagnostic grid's lower edge and turn a real mouth query into OUT_OF_WINDOW.
|
||||
LandingFromBottom = FMath::Max(
|
||||
LandingFromBottom,
|
||||
2.0f * Params.BoundarySealThickness + 0.01f);
|
||||
if (Params.LedgeSpacing > 0.0f && Params.LedgeDepth > 0.0f)
|
||||
{
|
||||
const float SafeBand = Params.LedgeDepth + FloorClearance;
|
||||
|
||||
@@ -1119,34 +1119,34 @@ namespace
|
||||
if (FieldName == TEXT("WormThreshold")) return Set(0.01f, 0.50f);
|
||||
if (FieldName == TEXT("WormStrength")) return Set(1.0f, 64.0f);
|
||||
if (FieldName == TEXT("WormNetworkRange")) return Set(0.0f, H * 1.5f);
|
||||
if (FieldName == TEXT("RoomSpacing")) return Set(16.0f, H * 2.5f);
|
||||
if (FieldName == TEXT("RoomSpacing")) return Set(64.0f, FMath::Max(96.0f, H * 0.75f));
|
||||
if (FieldName == TEXT("RoomDensity")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("MinRoomRadius")) return Set(1.0f, HHalf);
|
||||
if (FieldName == TEXT("MaxRoomRadius")) return Set(1.0f, H);
|
||||
if (FieldName == TEXT("MinRoomRadius")) return Set(16.0f, FMath::Max(32.0f, HHalf));
|
||||
if (FieldName == TEXT("MaxRoomRadius")) return Set(16.0f, FMath::Max(48.0f, H * 0.75f));
|
||||
if (FieldName == TEXT("RoomHeightRatio")) return Set(0.10f, 1.0f);
|
||||
if (FieldName == TEXT("RoomShapeVariety")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("RoomFloorCutMin")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("RoomFloorCutMax")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("FloorReliefStrength")) return Set(0.0f, HQuarter);
|
||||
if (FieldName == TEXT("FloorReliefFrequency")) return Set(0.001f, 0.10f);
|
||||
if (FieldName == TEXT("OriginRoomRadius")) return Set(0.0f, H * 0.75f);
|
||||
if (FieldName == TEXT("TunnelMinRadius")) return Set(1.0f, HQuarter);
|
||||
if (FieldName == TEXT("TunnelMaxRadius")) return Set(1.0f, HHalf);
|
||||
if (FieldName == TEXT("OriginRoomRadius")) return Set(24.0f, FMath::Max(64.0f, H * 0.75f));
|
||||
if (FieldName == TEXT("TunnelMinRadius")) return Set(6.0f, FMath::Max(8.0f, HQuarter));
|
||||
if (FieldName == TEXT("TunnelMaxRadius")) return Set(8.0f, FMath::Max(16.0f, H * 0.25f));
|
||||
if (FieldName == TEXT("TunnelDensity")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("MaxTunnelLength")) return Set(1.0f, H * 4.0f);
|
||||
if (FieldName == TEXT("MaxTunnelLength")) return Set(64.0f, H * 4.0f);
|
||||
if (FieldName == TEXT("TunnelWarpStrength")) return Set(0.0f, H);
|
||||
if (FieldName == TEXT("TunnelHorizontalBias")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("TunnelEndpointZOffset")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("SDFBlendRadius")) return Set(0.0f, H * 0.15f);
|
||||
if (FieldName == TEXT("SDFBlendRadius")) return Set(2.0f, FMath::Max(4.0f, H * 0.15f));
|
||||
if (FieldName == TEXT("WaterLevelRelative")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("CaveWarpStrength")) return Set(0.0f, HHalf);
|
||||
if (FieldName == TEXT("CaveWarpFrequency")) return Set(0.001f, 0.10f);
|
||||
if (FieldName == TEXT("SurfaceRoughness")) return Set(0.0f, HQuarter);
|
||||
if (FieldName == TEXT("SurfaceRoughness")) return Set(0.5f, FMath::Max(4.0f, H * 0.04f));
|
||||
if (FieldName == TEXT("RoughnessFrequency")) return Set(0.001f, 0.20f);
|
||||
if (FieldName == TEXT("BoundarySealThickness")) return Set(0.0f, HQuarter);
|
||||
if (FieldName == TEXT("DomainWarpStrength")) return Set(0.0f, HHalf);
|
||||
if (FieldName == TEXT("DomainWarpFrequency")) return Set(0.001f, 0.20f);
|
||||
if (FieldName == TEXT("FloorBias")) return Set(0.0f, HQuarter);
|
||||
if (FieldName == TEXT("FloorBias")) return Set(0.0f, FMath::Max(4.0f, H * 0.10f));
|
||||
if (FieldName == TEXT("TerraceStepHeight")) return Set(0.0f, HQuarter);
|
||||
if (FieldName == TEXT("TerraceHardness")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("TerraceNoiseDisplacement")) return Set(0.0f, 1.0f);
|
||||
@@ -1161,22 +1161,22 @@ namespace
|
||||
if (FieldName == TEXT("ScallopStrength")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("ScallopFrequency")) return Set(0.001f, 0.20f);
|
||||
if (FieldName == TEXT("ArchDensity")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("ArchMinRadius")) return Set(1.0f, H * 0.20f);
|
||||
if (FieldName == TEXT("ArchMaxRadius")) return Set(1.0f, H * 0.20f);
|
||||
if (FieldName == TEXT("ArchMinRadius")) return Set(6.0f, FMath::Max(12.0f, H * 0.20f));
|
||||
if (FieldName == TEXT("ArchMaxRadius")) return Set(12.0f, FMath::Max(24.0f, H * 0.30f));
|
||||
if (FieldName == TEXT("ColumnDensity")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("ColumnMinRadius")) return Set(1.0f, H * 0.20f);
|
||||
if (FieldName == TEXT("ColumnMaxRadius")) return Set(1.0f, H * 0.40f);
|
||||
if (FieldName == TEXT("ColumnMinRadius")) return Set(4.0f, FMath::Max(8.0f, H * 0.20f));
|
||||
if (FieldName == TEXT("ColumnMaxRadius")) return Set(8.0f, FMath::Max(16.0f, H * 0.40f));
|
||||
if (FieldName == TEXT("PitDensity")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("PitMinRadius")) return Set(1.0f, H * 0.20f);
|
||||
if (FieldName == TEXT("PitMaxRadius")) return Set(1.0f, H * 0.40f);
|
||||
if (FieldName == TEXT("PitMinRadius")) return Set(8.0f, FMath::Max(16.0f, H * 0.20f));
|
||||
if (FieldName == TEXT("PitMaxRadius")) return Set(16.0f, FMath::Max(32.0f, H * 0.40f));
|
||||
if (FieldName == TEXT("PitDepth")) return Set(0.0f, HHalf);
|
||||
if (FieldName == TEXT("ChimneyDensity")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("ChimneyMinRadius")) return Set(1.0f, H * 0.20f);
|
||||
if (FieldName == TEXT("ChimneyMaxRadius")) return Set(1.0f, H * 0.40f);
|
||||
if (FieldName == TEXT("ChimneyMinRadius")) return Set(4.0f, FMath::Max(8.0f, H * 0.20f));
|
||||
if (FieldName == TEXT("ChimneyMaxRadius")) return Set(8.0f, FMath::Max(16.0f, H * 0.40f));
|
||||
if (FieldName == TEXT("ChimneyHeight")) return Set(0.0f, HHalf);
|
||||
if (FieldName == TEXT("DomeDensity")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("DomeMinRadius")) return Set(1.0f, H * 0.30f);
|
||||
if (FieldName == TEXT("DomeMaxRadius")) return Set(1.0f, H * 0.50f);
|
||||
if (FieldName == TEXT("DomeMinRadius")) return Set(16.0f, FMath::Max(32.0f, H * 0.30f));
|
||||
if (FieldName == TEXT("DomeMaxRadius")) return Set(32.0f, FMath::Max(48.0f, H * 0.50f));
|
||||
if (FieldName == TEXT("DomeHeightRatio")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("PinchDensity")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("PinchStrength")) return Set(0.0f, H);
|
||||
@@ -1190,58 +1190,58 @@ namespace
|
||||
case ECaveGeneratorType::CrystalChamber:
|
||||
if (FieldName == TEXT("FloorRelativeHeight")) return Set(0.0f, 0.95f);
|
||||
if (FieldName == TEXT("CeilingRelativeHeight")) return Set(0.05f, 1.0f);
|
||||
if (FieldName == TEXT("FloorRoughness")) return Set(0.0f, HQuarter);
|
||||
if (FieldName == TEXT("FloorRoughness")) return Set(0.5f, FMath::Max(4.0f, H * 0.04f));
|
||||
if (FieldName == TEXT("FloorRoughnessFrequency")) return Set(0.001f, 0.15f);
|
||||
if (FieldName == TEXT("CeilingRoughness")) return Set(0.0f, HHalf);
|
||||
if (FieldName == TEXT("CeilingRoughness")) return Set(1.0f, FMath::Max(16.0f, H * 0.12f));
|
||||
if (FieldName == TEXT("CeilingRoughnessFrequency")) return Set(0.001f, 0.15f);
|
||||
if (FieldName == TEXT("ColumnDensity")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("ColumnMinRadius")) return Set(1.0f, H * 0.20f);
|
||||
if (FieldName == TEXT("ColumnMaxRadius")) return Set(1.0f, H * 0.40f);
|
||||
if (FieldName == TEXT("ColumnSpacing")) return Set(10.0f, H * 1.5f);
|
||||
if (FieldName == TEXT("ColumnMinRadius")) return Set(8.0f, FMath::Max(12.0f, H * 0.20f));
|
||||
if (FieldName == TEXT("ColumnMaxRadius")) return Set(16.0f, FMath::Max(24.0f, H * 0.40f));
|
||||
if (FieldName == TEXT("ColumnSpacing")) return Set(64.0f, FMath::Max(96.0f, H * 0.75f));
|
||||
if (FieldName == TEXT("BoundarySealThickness")) return Set(0.0f, HQuarter);
|
||||
if (FieldName == TEXT("BaseDensity")) return Set(1.0f, 32.0f);
|
||||
return false;
|
||||
|
||||
case ECaveGeneratorType::Maze:
|
||||
if (FieldName == TEXT("CellSize")) return Set(8.0f, H * 2.0f);
|
||||
if (FieldName == TEXT("CorridorRadius")) return Set(1.0f, HQuarter);
|
||||
if (FieldName == TEXT("CellSize")) return Set(48.0f, FMath::Max(64.0f, FMath::Min(96.0f, H * 0.75f)));
|
||||
if (FieldName == TEXT("CorridorRadius")) return Set(6.0f, 8.0f);
|
||||
if (FieldName == TEXT("BranchProbability")) return Set(0.05f, 1.0f);
|
||||
if (FieldName == TEXT("Verticality")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("SurfaceRoughness")) return Set(0.0f, HQuarter);
|
||||
if (FieldName == TEXT("SurfaceRoughness")) return Set(0.5f, FMath::Max(3.0f, H * 0.02f));
|
||||
if (FieldName == TEXT("BoundarySealThickness")) return Set(0.0f, HQuarter);
|
||||
if (FieldName == TEXT("BaseDensity")) return Set(1.0f, 32.0f);
|
||||
return false;
|
||||
|
||||
case ECaveGeneratorType::SurfaceWorld:
|
||||
if (FieldName == TEXT("BaseGroundRelative")) return Set(0.05f, 0.90f);
|
||||
if (FieldName == TEXT("ElevationRange")) return Set(0.0f, H * 2.0f);
|
||||
if (FieldName == TEXT("ElevationRange")) return Set(24.0f, FMath::Max(80.0f, H * 0.50f));
|
||||
if (FieldName == TEXT("ContinentFrequency")) return Set(0.0005f, 0.10f);
|
||||
if (FieldName == TEXT("MountainStrength")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("MountainFrequency")) return Set(0.0005f, 0.10f);
|
||||
if (FieldName == TEXT("DetailFrequency")) return Set(0.0005f, 0.20f);
|
||||
if (FieldName == TEXT("SurfaceRoughness")) return Set(0.0f, HQuarter);
|
||||
if (FieldName == TEXT("HeightWarpStrength")) return Set(0.0f, H);
|
||||
if (FieldName == TEXT("SurfaceRoughness")) return Set(1.0f, FMath::Max(3.0f, H * 0.02f));
|
||||
if (FieldName == TEXT("HeightWarpStrength")) return Set(0.0f, FMath::Max(48.0f, H * 0.25f));
|
||||
if (FieldName == TEXT("HeightWarpFrequency")) return Set(0.0005f, 0.10f);
|
||||
if (FieldName == TEXT("ReliefFrequency")) return Set(0.0002f, 0.02f);
|
||||
if (FieldName == TEXT("ReliefStrength")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("ReliefContrast")) return Set(0.25f, 4.0f);
|
||||
if (FieldName == TEXT("TerraceStrength")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("TerraceHeight")) return Set(1.0f, HHalf);
|
||||
if (FieldName == TEXT("TerraceHeight")) return Set(2.0f, FMath::Max(8.0f, H * 0.08f));
|
||||
if (FieldName == TEXT("TerraceHardness")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("LayerLineDepth")) return Set(0.0f, HQuarter);
|
||||
if (FieldName == TEXT("LayerLineSpacing")) return Set(1.0f, H);
|
||||
if (FieldName == TEXT("LayerLineSpacing")) return Set(2.0f, FMath::Max(8.0f, H * 0.25f));
|
||||
if (FieldName == TEXT("CliffStrength")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("CliffSlopeThreshold")) return Set(0.05f, 2.0f);
|
||||
if (FieldName == TEXT("CliffSharpness")) return Set(0.0f, 4.0f);
|
||||
if (FieldName == TEXT("CliffSampleDist")) return Set(0.5f, HQuarter);
|
||||
if (FieldName == TEXT("OverhangStrength")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("OverhangReach")) return Set(0.0f, HHalf);
|
||||
if (FieldName == TEXT("OverhangHeight")) return Set(0.0f, HHalf);
|
||||
if (FieldName == TEXT("OverhangReach")) return Set(4.0f, FMath::Max(16.0f, H * 0.15f));
|
||||
if (FieldName == TEXT("OverhangHeight")) return Set(4.0f, FMath::Max(24.0f, H * 0.25f));
|
||||
if (FieldName == TEXT("OverhangFrequency")) return Set(0.0005f, 0.20f);
|
||||
if (FieldName == TEXT("OverhangZScale")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("OverhangSlopeThreshold")) return Set(0.05f, 2.0f);
|
||||
if (FieldName == TEXT("WaterLevelRelative")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("BeachWidth")) return Set(0.0f, HQuarter);
|
||||
if (FieldName == TEXT("BeachWidth")) return Set(4.0f, FMath::Max(12.0f, H * 0.10f));
|
||||
if (FieldName == TEXT("CeilingRelative")) return Set(0.30f, 1.0f);
|
||||
if (FieldName == TEXT("CeilingRoughness")) return Set(0.0f, HHalf);
|
||||
if (FieldName == TEXT("CeilingRoughnessFrequency")) return Set(0.0005f, 0.20f);
|
||||
@@ -1256,29 +1256,29 @@ namespace
|
||||
return false;
|
||||
|
||||
case ECaveGeneratorType::VerticalShafts:
|
||||
if (FieldName == TEXT("ShaftSpacing")) return Set(10.0f, H * 2.0f);
|
||||
if (FieldName == TEXT("ShaftSpacing")) return Set(64.0f, FMath::Max(80.0f, FMath::Min(112.0f, H * 0.75f)));
|
||||
if (FieldName == TEXT("ShaftDensity")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("ShaftMinRadius")) return Set(1.0f, H * 0.20f);
|
||||
if (FieldName == TEXT("ShaftMaxRadius")) return Set(1.0f, H * 0.40f);
|
||||
if (FieldName == TEXT("ShaftMinRadius")) return Set(8.0f, 16.0f);
|
||||
if (FieldName == TEXT("ShaftMaxRadius")) return Set(12.0f, 24.0f);
|
||||
if (FieldName == TEXT("CrossConnectChance")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("ConnectorRadius")) return Set(1.0f, H * 0.20f);
|
||||
if (FieldName == TEXT("LedgeSpacing")) return Set(0.0f, H);
|
||||
if (FieldName == TEXT("LedgeDepth")) return Set(0.0f, HQuarter);
|
||||
if (FieldName == TEXT("SurfaceRoughness")) return Set(0.0f, HQuarter);
|
||||
if (FieldName == TEXT("ConnectorRadius")) return Set(6.0f, 12.0f);
|
||||
if (FieldName == TEXT("LedgeSpacing")) return Set(8.0f, FMath::Max(32.0f, H * 0.25f));
|
||||
if (FieldName == TEXT("LedgeDepth")) return Set(2.0f, FMath::Max(4.0f, H * 0.08f));
|
||||
if (FieldName == TEXT("SurfaceRoughness")) return Set(0.5f, FMath::Max(3.0f, H * 0.02f));
|
||||
if (FieldName == TEXT("BoundarySealThickness")) return Set(0.0f, HQuarter);
|
||||
if (FieldName == TEXT("BaseDensity")) return Set(1.0f, 32.0f);
|
||||
return false;
|
||||
|
||||
case ECaveGeneratorType::FloatingIslands:
|
||||
if (FieldName == TEXT("IslandSpacing")) return Set(20.0f, H * 3.0f);
|
||||
if (FieldName == TEXT("IslandSpacing")) return Set(96.0f, FMath::Max(112.0f, FMath::Min(160.0f, H * 0.75f)));
|
||||
if (FieldName == TEXT("IslandDensity")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("IslandMinRadius")) return Set(2.0f, H * 0.30f);
|
||||
if (FieldName == TEXT("IslandMaxRadius")) return Set(2.0f, H * 0.75f);
|
||||
if (FieldName == TEXT("IslandMinRadius")) return Set(24.0f, FMath::Max(32.0f, H * 0.30f));
|
||||
if (FieldName == TEXT("IslandMaxRadius")) return Set(48.0f, FMath::Max(64.0f, H * 0.50f));
|
||||
if (FieldName == TEXT("ThicknessRatio")) return Set(0.10f, 1.5f);
|
||||
if (FieldName == TEXT("VerticalJitter")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("TopFlatten")) return Set(0.0f, 1.0f);
|
||||
if (FieldName == TEXT("SurfaceRoughness")) return Set(0.0f, HQuarter);
|
||||
if (FieldName == TEXT("SDFBlendRadius")) return Set(0.0f, HQuarter);
|
||||
if (FieldName == TEXT("SurfaceRoughness")) return Set(0.5f, FMath::Max(3.0f, H * 0.02f));
|
||||
if (FieldName == TEXT("SDFBlendRadius")) return Set(2.0f, FMath::Max(6.0f, H * 0.10f));
|
||||
if (FieldName == TEXT("BoundarySealThickness")) return Set(0.0f, HQuarter);
|
||||
if (FieldName == TEXT("BaseDensity")) return Set(1.0f, 32.0f);
|
||||
return false;
|
||||
@@ -1533,35 +1533,47 @@ namespace
|
||||
float StrateHeightInVoxels)
|
||||
{
|
||||
const float H = FMath::Max(StrateHeightInVoxels, static_cast<float>(CHUNK_SIZE));
|
||||
const float SpacingMin = FMath::Min(48.0f, H * 0.35f);
|
||||
const float SpacingMax = FMath::Max(SpacingMin + 8.0f, FMath::Min(160.0f, H * 0.94f));
|
||||
// Human-scale derivation: an 8 m room is the smallest room-like volume (radius 16),
|
||||
// a 20 m chamber is the large-room target (radius 40), and a 24 m hub is the three-
|
||||
// chunk boss-space target. Keep the room-cell margin independent of those radii.
|
||||
const float SpacingMin = FMath::Max(96.0f, H * 0.35f);
|
||||
const float SpacingMax = FMath::Max(SpacingMin + 32.0f, FMath::Min(160.0f, H * 0.94f));
|
||||
const float Coverage = VF_CorpusFreeU(Rng, 0.08f, 0.18f);
|
||||
|
||||
P.BoundarySealThickness = VF_CorpusFreeU(
|
||||
Rng, FMath::Min(3.0f, H * 0.02f), FMath::Min(8.0f, H * 0.08f));
|
||||
P.RoomSpacing = VF_CorpusFreeU(Rng, SpacingMin, SpacingMax);
|
||||
P.RoomDensity = VF_CorpusFreeU(Rng, 0.25f, 0.75f);
|
||||
P.RoomDensity = VF_CorpusFreeU(Rng, 0.20f, 0.45f);
|
||||
const float RadiusRatio = FMath::Sqrt(
|
||||
Coverage / (PI * FMath::Max(P.RoomDensity, 0.001f)));
|
||||
P.MaxRoomRadius = P.RoomSpacing * RadiusRatio;
|
||||
P.MinRoomRadius = P.MaxRoomRadius * VF_CorpusFreeU(Rng, 0.35f, 0.75f);
|
||||
P.RoomHeightRatio = VF_CorpusFreeU(Rng, 0.25f, 0.85f);
|
||||
P.OriginRoomRadius = P.MaxRoomRadius * VF_CorpusFreeU(Rng, 0.60f, 0.95f);
|
||||
P.MaxRoomRadius = FMath::Min(
|
||||
FMath::Max(40.0f, P.RoomSpacing * RadiusRatio),
|
||||
FMath::Min(P.RoomSpacing * 0.45f, H * 0.30f));
|
||||
P.MinRoomRadius = FMath::Max(
|
||||
16.0f, P.MaxRoomRadius * VF_CorpusFreeU(Rng, 0.45f, 0.70f));
|
||||
P.MinRoomRadius = FMath::Min(P.MinRoomRadius, P.MaxRoomRadius);
|
||||
P.RoomHeightRatio = VF_CorpusFreeU(Rng, 0.70f, 0.95f);
|
||||
const float MaxRoomVerticalRadius = FMath::Max(
|
||||
8.0f, H * 0.50f - P.BoundarySealThickness - 1.0f);
|
||||
P.OriginRoomRadius = FMath::Min(
|
||||
P.MaxRoomRadius * VF_CorpusFreeU(Rng, 0.90f, 1.15f),
|
||||
MaxRoomVerticalRadius / FMath::Max(P.RoomHeightRatio, 0.1f));
|
||||
P.OriginRoomRadius = FMath::Max(P.OriginRoomRadius, P.MinRoomRadius);
|
||||
|
||||
P.RoomFloorCutMin = VF_CorpusFreeU(Rng, 0.35f, 0.85f);
|
||||
P.RoomFloorCutMin = VF_CorpusFreeU(Rng, 0.55f, 0.75f);
|
||||
P.RoomFloorCutMax = P.RoomFloorCutMin + VF_CorpusFreeU(
|
||||
Rng, 0.0f, 1.0f - P.RoomFloorCutMin);
|
||||
Rng, 0.0f, FMath::Min(0.25f, 1.0f - P.RoomFloorCutMin));
|
||||
P.FloorReliefStrength = VF_CorpusFreeU(
|
||||
Rng, 0.0f,
|
||||
FMath::Min(12.0f, P.MaxRoomRadius * P.RoomHeightRatio * 0.25f));
|
||||
|
||||
const float TunnelMinCap = FMath::Max(1.5f, P.MinRoomRadius * 0.45f);
|
||||
const float TunnelMinCap = FMath::Max(6.0f, FMath::Min(8.0f, P.MinRoomRadius * 0.45f));
|
||||
P.TunnelMinRadius = FMath::Max(
|
||||
1.5f, P.MinRoomRadius * VF_CorpusFreeU(Rng, 0.25f, 0.45f));
|
||||
6.0f, P.MinRoomRadius * VF_CorpusFreeU(Rng, 0.25f, 0.40f));
|
||||
P.TunnelMinRadius = FMath::Min(P.TunnelMinRadius, TunnelMinCap);
|
||||
P.TunnelMaxRadius = FMath::Max(
|
||||
P.TunnelMinRadius,
|
||||
P.MinRoomRadius * VF_CorpusFreeU(Rng, 0.55f, 0.90f));
|
||||
FMath::Min(8.0f, P.MinRoomRadius * VF_CorpusFreeU(Rng, 0.50f, 0.75f)));
|
||||
|
||||
// Adjacent cells can differ in both X and Y. The placement jitter is [0.15, 0.85]
|
||||
// cell, so the worst local diagonal separation is 1.7 * S * sqrt(2), not the
|
||||
@@ -1570,17 +1582,17 @@ namespace
|
||||
const float VerticalReach = 2.0f * P.MaxRoomRadius * P.RoomHeightRatio;
|
||||
const float NeighborReach = FMath::Sqrt(
|
||||
HorizontalReach * HorizontalReach + VerticalReach * VerticalReach);
|
||||
P.MaxTunnelLength = NeighborReach * VF_CorpusFreeU(Rng, 1.0f, 1.40f);
|
||||
P.TunnelWarpStrength = P.MaxTunnelLength * VF_CorpusFreeU(Rng, 0.02f, 0.12f);
|
||||
P.WormStrength = P.BaseDensity * VF_CorpusFreeU(Rng, 1.10f, 1.80f);
|
||||
P.WormNetworkRange = P.RoomSpacing * VF_CorpusFreeU(Rng, 0.20f, 0.60f);
|
||||
P.CaveWarpStrength = P.RoomSpacing * VF_CorpusFreeU(Rng, 0.02f, 0.12f);
|
||||
P.MaxTunnelLength = NeighborReach * VF_CorpusFreeU(Rng, 1.05f, 1.30f);
|
||||
P.TunnelWarpStrength = P.MaxTunnelLength * VF_CorpusFreeU(Rng, 0.03f, 0.08f);
|
||||
P.WormStrength = P.BaseDensity * VF_CorpusFreeU(Rng, 1.35f, 1.80f);
|
||||
P.WormNetworkRange = P.RoomSpacing * VF_CorpusFreeU(Rng, 0.25f, 0.50f);
|
||||
P.CaveWarpStrength = P.RoomSpacing * VF_CorpusFreeU(Rng, 0.03f, 0.10f);
|
||||
|
||||
P.SDFBlendRadius = VF_CorpusFreeU(
|
||||
Rng, 0.5f, FMath::Max(0.5f, FMath::Min(4.0f, P.TunnelMinRadius * 0.35f)));
|
||||
Rng, 2.0f, FMath::Max(2.0f, FMath::Min(4.0f, P.TunnelMinRadius * 0.50f)));
|
||||
const float RoughnessMax = FMath::Max(
|
||||
0.0f, (P.TunnelMinRadius - P.SDFBlendRadius - 0.25f) / VOXEL_NOISE_SCALE);
|
||||
P.SurfaceRoughness = VF_CorpusFreeU(Rng, 0.0f, RoughnessMax * 0.70f);
|
||||
P.SurfaceRoughness = VF_CorpusFreeU(Rng, 0.5f, RoughnessMax * 0.60f);
|
||||
P.FloorBias = VF_CorpusFreeU(
|
||||
Rng, 0.0f, P.MaxRoomRadius * P.RoomHeightRatio * 0.25f);
|
||||
|
||||
@@ -1597,7 +1609,12 @@ namespace
|
||||
P.ArchMaxRadius = FMath::Max(
|
||||
P.ArchMinRadius,
|
||||
P.MinRoomRadius * VF_CorpusFreeU(Rng, 0.20f, 0.45f));
|
||||
P.DomeMaxRadius = P.MaxRoomRadius * VF_CorpusFreeU(Rng, 0.30f, 0.60f);
|
||||
// The public envelope has a 32-voxel minimum dome radius. Apply that envelope before
|
||||
// deriving the height ratio; otherwise a small sampled dome would be clamped larger
|
||||
// after this equation and could violate the room-height relation.
|
||||
P.DomeMaxRadius = FMath::Min(
|
||||
P.MaxRoomRadius,
|
||||
FMath::Max(32.0f, P.MaxRoomRadius * VF_CorpusFreeU(Rng, 0.30f, 0.60f)));
|
||||
P.DomeMinRadius = P.DomeMaxRadius * VF_CorpusFreeU(Rng, 0.35f, 0.75f);
|
||||
P.DomeHeightRatio = FMath::Min(
|
||||
1.0f,
|
||||
@@ -1615,23 +1632,27 @@ namespace
|
||||
const float H = FMath::Max(StrateHeightInVoxels, static_cast<float>(CHUNK_SIZE));
|
||||
P.BoundarySealThickness = VF_CorpusFreeU(
|
||||
Rng, FMath::Min(3.0f, H * 0.02f), FMath::Min(8.0f, H * 0.08f));
|
||||
P.FloorRoughness = VF_CorpusFreeU(Rng, 0.0f, H * 0.04f);
|
||||
P.CeilingRoughness = VF_CorpusFreeU(Rng, 0.0f, H * 0.08f);
|
||||
// A slab is an open fight/chamber volume, not a one-voxel shell. At the new
|
||||
// eight-chunk default this leaves roughly 20-50 m between the two surfaces after
|
||||
// reserving the 2.5-4 m feature envelopes below.
|
||||
P.FloorRoughness = VF_CorpusFreeU(Rng, 1.0f, FMath::Max(4.0f, H * 0.04f));
|
||||
P.CeilingRoughness = VF_CorpusFreeU(Rng, 2.0f, FMath::Max(8.0f, H * 0.06f));
|
||||
|
||||
const float FloorNoise = VOXEL_NOISE_SCALE * P.FloorRoughness;
|
||||
const float CeilingNoise = VOXEL_NOISE_SCALE * P.CeilingRoughness;
|
||||
const float FloorClearance = VF_CorpusFreeU(Rng, H * 0.08f, H * 0.16f);
|
||||
const float CeilingClearance = VF_CorpusFreeU(Rng, H * 0.08f, H * 0.16f);
|
||||
const float FloorClearance = VF_CorpusFreeU(Rng, H * 0.08f, H * 0.12f);
|
||||
const float CeilingClearance = VF_CorpusFreeU(Rng, H * 0.08f, H * 0.12f);
|
||||
P.FloorRelativeHeight = (P.BoundarySealThickness + FloorNoise + FloorClearance) / H;
|
||||
P.CeilingRelativeHeight = 1.0f
|
||||
- (P.BoundarySealThickness + CeilingNoise + CeilingClearance) / H;
|
||||
|
||||
P.ColumnSpacing = VF_CorpusFreeU(
|
||||
Rng, FMath::Max(24.0f, H * 0.30f), FMath::Max(32.0f, H * 0.75f));
|
||||
P.ColumnMinRadius = VF_CorpusFreeU(Rng, 1.0f, P.ColumnSpacing * 0.08f);
|
||||
Rng, FMath::Max(96.0f, H * 0.375f),
|
||||
FMath::Max(128.0f, FMath::Min(160.0f, H * 0.75f)));
|
||||
P.ColumnMinRadius = VF_CorpusFreeU(Rng, 8.0f, 12.0f);
|
||||
P.ColumnMaxRadius = FMath::Max(
|
||||
P.ColumnMinRadius,
|
||||
FMath::Min(P.ColumnSpacing * 0.40f, H * 0.38f));
|
||||
FMath::Min(P.ColumnSpacing * 0.40f, P.ColumnMinRadius + 8.0f));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1640,12 +1661,16 @@ namespace
|
||||
float StrateHeightInVoxels)
|
||||
{
|
||||
const float H = FMath::Max(StrateHeightInVoxels, static_cast<float>(CHUNK_SIZE));
|
||||
// Body-derived lattice: the fight corridor is 3-4 m wide (radius 6-8 voxels),
|
||||
// while the lattice cell is 12-16 m (48-64 voxels). Thus a corridor radius is
|
||||
// about one eighth of a cell, far below CellSize/2, leaving a real rock wall.
|
||||
P.CellSize = VF_CorpusFreeU(
|
||||
Rng, FMath::Max(24.0f, H * 0.25f), FMath::Min(96.0f, H * 0.75f));
|
||||
P.CorridorRadius = P.CellSize * VF_CorpusFreeU(Rng, 0.12f, 0.24f);
|
||||
Rng, 48.0f, FMath::Max(64.0f, FMath::Min(96.0f, H * 0.75f)));
|
||||
P.CorridorRadius = VF_CorpusFreeU(
|
||||
Rng, 6.0f, FMath::Min(8.0f, P.CellSize * 0.20f));
|
||||
const float RoughnessCap = FMath::Max(
|
||||
0.0f, (P.CorridorRadius + 2.0f) / (VOXEL_NOISE_SCALE * 1.25f));
|
||||
P.SurfaceRoughness = VF_CorpusFreeU(Rng, 0.0f, RoughnessCap * 0.70f);
|
||||
P.SurfaceRoughness = VF_CorpusFreeU(Rng, 0.5f, RoughnessCap * 0.60f);
|
||||
P.BoundarySealThickness = VF_CorpusFreeU(
|
||||
Rng, FMath::Min(3.0f, H * 0.02f), FMath::Min(8.0f, H * 0.08f));
|
||||
return true;
|
||||
@@ -1658,9 +1683,11 @@ namespace
|
||||
const float H = FMath::Max(StrateHeightInVoxels, static_cast<float>(CHUNK_SIZE));
|
||||
P.BoundarySealThickness = VF_CorpusFreeU(
|
||||
Rng, FMath::Min(3.0f, H * 0.02f), FMath::Min(8.0f, H * 0.08f));
|
||||
P.ElevationRange = VF_CorpusFreeU(Rng, H * 0.06f, H * 0.16f);
|
||||
// Outdoor relief uses 6-20 m as its authored human-scale band; the absolute
|
||||
// height is still tied to H so the sky cap has room above the terrain.
|
||||
P.ElevationRange = VF_CorpusFreeU(Rng, H * 0.10f, H * 0.25f);
|
||||
P.MountainStrength = VF_CorpusFreeU(Rng, 0.20f, 0.80f);
|
||||
P.SurfaceRoughness = VF_CorpusFreeU(Rng, 0.0f, H * 0.02f);
|
||||
P.SurfaceRoughness = VF_CorpusFreeU(Rng, 1.0f, FMath::Max(3.0f, H * 0.02f));
|
||||
|
||||
const float GroundLowerDeviation = P.ElevationRange * 0.50f
|
||||
+ P.SurfaceRoughness * VOXEL_NOISE_SCALE;
|
||||
@@ -1743,18 +1770,21 @@ namespace
|
||||
const float H = FMath::Max(StrateHeightInVoxels, static_cast<float>(CHUNK_SIZE));
|
||||
P.BoundarySealThickness = VF_CorpusFreeU(
|
||||
Rng, FMath::Min(3.0f, H * 0.02f), FMath::Min(8.0f, H * 0.08f));
|
||||
// A shaft is a 4-7 m diameter vertical room; connectors are 3-6 m bores.
|
||||
// The 20-28 m site spacing leaves a substantial rock web between shafts.
|
||||
P.ShaftSpacing = VF_CorpusFreeU(
|
||||
Rng, FMath::Max(32.0f, H * 0.30f),
|
||||
FMath::Max(32.0f, FMath::Min(112.0f, H * 0.90f)));
|
||||
P.SurfaceRoughness = VF_CorpusFreeU(Rng, 0.0f, H * 0.04f);
|
||||
Rng, 80.0f, FMath::Max(96.0f, FMath::Min(112.0f, H * 0.90f)));
|
||||
P.SurfaceRoughness = VF_CorpusFreeU(Rng, 1.0f, FMath::Max(3.0f, H * 0.02f));
|
||||
const float RoughnessReach = P.SurfaceRoughness * VOXEL_NOISE_SCALE * 1.5f;
|
||||
P.ShaftMinRadius = RoughnessReach + 0.5f + VF_CorpusFreeU(Rng, 1.0f, 4.0f);
|
||||
P.ShaftMinRadius = FMath::Max(
|
||||
8.0f, RoughnessReach + 0.5f + VF_CorpusFreeU(Rng, 1.0f, 3.0f));
|
||||
P.ShaftMaxRadius = FMath::Max(
|
||||
P.ShaftMinRadius + 0.5f,
|
||||
FMath::Min(P.ShaftSpacing * 0.40f, P.ShaftMinRadius + 16.0f));
|
||||
P.ConnectorRadius = RoughnessReach + 1.0f + VF_CorpusFreeU(Rng, 0.0f, 3.0f);
|
||||
P.ShaftMinRadius + 4.0f,
|
||||
FMath::Min(P.ShaftSpacing * 0.40f, P.ShaftMinRadius + 8.0f));
|
||||
P.ConnectorRadius = FMath::Max(
|
||||
6.0f, RoughnessReach + 1.0f + VF_CorpusFreeU(Rng, 0.0f, 2.0f));
|
||||
P.LedgeDepth = VF_CorpusFreeU(
|
||||
Rng, 0.5f, FMath::Max(0.5f, FMath::Min(4.0f, P.ShaftSpacing * 0.08f)));
|
||||
Rng, 2.0f, FMath::Max(4.0f, FMath::Min(6.0f, P.ShaftSpacing * 0.08f)));
|
||||
const float FloorClearance = FMath::Max(
|
||||
1.0f, P.SurfaceRoughness * VOXEL_NOISE_SCALE + 1.0f);
|
||||
P.LedgeSpacing = 2.0f * (P.LedgeDepth + FloorClearance)
|
||||
@@ -1770,21 +1800,23 @@ namespace
|
||||
const float H = FMath::Max(StrateHeightInVoxels, static_cast<float>(CHUNK_SIZE));
|
||||
P.BoundarySealThickness = VF_CorpusFreeU(
|
||||
Rng, FMath::Min(3.0f, H * 0.02f), FMath::Min(8.0f, H * 0.08f));
|
||||
// Island radii are deliberately larger than the roughness envelope: 12-24 m
|
||||
// diameters read as land masses, while 28-40 m site spacing prevents mush.
|
||||
P.IslandSpacing = VF_CorpusFreeU(
|
||||
Rng, FMath::Max(40.0f, H * 0.30f),
|
||||
FMath::Max(40.0f, FMath::Min(120.0f, H * 0.75f)));
|
||||
P.ThicknessRatio = VF_CorpusFreeU(Rng, 0.30f, 0.90f);
|
||||
Rng, 112.0f, FMath::Max(112.0f, FMath::Min(160.0f, H * 0.75f)));
|
||||
P.ThicknessRatio = VF_CorpusFreeU(Rng, 0.45f, 0.75f);
|
||||
const float MaxByHeight = (H * 0.50f - P.BoundarySealThickness)
|
||||
/ FMath::Max(P.ThicknessRatio, 0.25f);
|
||||
const float MaxRadius = FMath::Max(
|
||||
2.0f, FMath::Min(P.IslandSpacing * 0.45f, MaxByHeight * 0.85f));
|
||||
48.0f, FMath::Min(P.IslandSpacing * 0.45f, MaxByHeight * 0.85f));
|
||||
P.IslandMaxRadius = MaxRadius;
|
||||
P.IslandMinRadius = P.IslandMaxRadius * VF_CorpusFreeU(Rng, 0.45f, 0.80f);
|
||||
P.IslandMinRadius = FMath::Max(
|
||||
24.0f, P.IslandMaxRadius * VF_CorpusFreeU(Rng, 0.50f, 0.80f));
|
||||
P.SDFBlendRadius = VF_CorpusFreeU(
|
||||
Rng, 0.5f, FMath::Min(6.0f, P.IslandMinRadius * 0.20f));
|
||||
Rng, 2.0f, FMath::Min(6.0f, P.IslandMinRadius * 0.20f));
|
||||
const float RoughnessMax = FMath::Max(
|
||||
0.0f, (P.IslandMinRadius - P.SDFBlendRadius) / VOXEL_NOISE_SCALE);
|
||||
P.SurfaceRoughness = VF_CorpusFreeU(Rng, 0.0f, RoughnessMax * 0.70f);
|
||||
P.SurfaceRoughness = VF_CorpusFreeU(Rng, 0.5f, RoughnessMax * 0.60f);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1860,8 +1892,39 @@ namespace
|
||||
}
|
||||
if (bValid && bConstraintSampled)
|
||||
{
|
||||
bValid = VF_ApplyCorpusFreeConstraints(Archetype, Params, ScalarRng,
|
||||
StrateHeightInVoxels);
|
||||
// Dependent constraints are authored in terms of the final vector, while the
|
||||
// reflected envelope clamp below can move an independently sampled field (for
|
||||
// example SurfaceWorld elevation) after that derivation. Treat the fully clamped
|
||||
// vector as the candidate and deterministically resample until the declared
|
||||
// relations hold; never return a vector whose coupling was only true pre-clamp.
|
||||
constexpr int32 MaxConstraintAttempts = 64;
|
||||
FString ConstraintViolation;
|
||||
bool bConstraintValid = false;
|
||||
for (int32 Attempt = 0; Attempt < MaxConstraintAttempts; ++Attempt)
|
||||
{
|
||||
bConstraintValid = VF_ApplyCorpusFreeConstraints(
|
||||
Archetype, Params, ScalarRng, StrateHeightInVoxels);
|
||||
if (!bConstraintValid)
|
||||
{
|
||||
break;
|
||||
}
|
||||
VF_ClampCorpusFreeFields(Params, Archetype, StrateHeightInVoxels);
|
||||
VF_ResetExcludedFields(Params, Archetype);
|
||||
if (VF_ValidateStrateCorpusFreeConstraints(
|
||||
Archetype, Params, StrateHeightInVoxels, ConstraintViolation))
|
||||
{
|
||||
break;
|
||||
}
|
||||
bConstraintValid = false;
|
||||
}
|
||||
if (!bConstraintValid)
|
||||
{
|
||||
Result.FailureReason = ConstraintViolation.IsEmpty()
|
||||
? TEXT("Constraint sampler could not produce a clamped legal vector.")
|
||||
: FString::Printf(TEXT("Constraint sampler exhausted legal retries: %s"),
|
||||
*ConstraintViolation);
|
||||
return Result;
|
||||
}
|
||||
}
|
||||
if (!bValid)
|
||||
{
|
||||
@@ -1869,10 +1932,12 @@ namespace
|
||||
return Result;
|
||||
}
|
||||
|
||||
// Last safety net only: all dependent values above are derived before this envelope
|
||||
// clamp, and no corpus/default value is consulted.
|
||||
VF_ClampCorpusFreeFields(Params, Archetype, StrateHeightInVoxels);
|
||||
VF_ResetExcludedFields(Params, Archetype);
|
||||
if (!bConstraintSampled)
|
||||
{
|
||||
// Last safety net for the independent arm; no corpus/default value is consulted.
|
||||
VF_ClampCorpusFreeFields(Params, Archetype, StrateHeightInVoxels);
|
||||
VF_ResetExcludedFields(Params, Archetype);
|
||||
}
|
||||
Result.ArchetypeParams = Params;
|
||||
Result.Params = Params.TunnelNetworkParams;
|
||||
Result.bValid = true;
|
||||
|
||||
@@ -627,9 +627,11 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
return FMath::Max(Definition.MazeParams.CellSize, 1.0f);
|
||||
|
||||
case ECaveGeneratorType::VerticalShafts:
|
||||
// One shaft grid spacing is the natural nearest-site budget. A sparser layout may
|
||||
// legitimately decline instead of pulling the passage away from the spine.
|
||||
return FMath::Max(Definition.VerticalShaftParams.ShaftSpacing, 1.0f);
|
||||
// A density of 0.6 does not guarantee an occupied cell in the immediate grid
|
||||
// neighbourhood. Allow the nearest occupied shaft to be two grid cells away while
|
||||
// remaining a bounded local query; the landing query still returns that shaft's
|
||||
// exact axis, so this cannot create a non-topological mouth.
|
||||
return FMath::Max(2.0f * Definition.VerticalShaftParams.ShaftSpacing, 1.0f);
|
||||
|
||||
case ECaveGeneratorType::FloatingIslands:
|
||||
// One island grid spacing protects the intentional radial placement while still
|
||||
|
||||
Reference in new issue
Block a user