feat(composer): the structure roll -- 64 distinct stacks nobody wrote

Tier 4b. Until now the composer varied PARAMETERS within an archetype:
differently-sized caves. This rolls STRUCTURE, which is what makes a strate
a different KIND of place.

Per COMPOSER-NOTES §3.2: roll root polarity (rock+carve or void+fill) ->
a shape source legal for that polarity -> the conversion FOLLOWS from
polarity rather than being rolled -> k modifiers drawn from what the
CHANNEL DECLARATIONS say is legal, not from a hand-written list ->
structural posts appended automatically and not omittable.

  64 distinct recipes of 64          (perfect structural diversity)
  invalid recipes emitted: 0         (the roller cannot produce an
                                      order the DAG validator rejects)
  survival 46/64 (71.9%)             non-vacuous 62, largest-share 60,
                                      law pass 48

⭐ AND THEY ARE REAL PLACES NOBODY DESIGNED. Root polarity as a one-bit
identity lever, exactly as §3.2 predicted:

  VF|r=vd/T|s=rg/T|x=fl/T   TunnelNetwork's ROOM GRAPH rooted in void with
                            a FILL -- rooms become solid blobs hanging in
                            open space. air .978, feature scale 64.
  VF|r=vd/M|s=lt/M|x=fl/M   Maze's lattice corridors filled instead of
                            carved. air .938, feature scale 16.
  RC|r=rk/V|s=sh/V|x=cv/V   shafts carved from rock with a Maze modifier
                            set they never ship with.

Same parts, different worlds. That is the whole thesis of the op stack,
finally exercised.

⭐⭐ AND THE VERDICTS HOLD ON STACKS NOBODY VERIFIED:
  489 proved (213 AllSolid, 276 AllAir), 2,071 mixed
  650,859 voxels brute-forced
  0 VIOLATIONS

This is what 8e346a3 was for. A false uniform verdict on an invented stack
means no geometry, no collision, and a player falls through the floor --
and it is the failure that cannot be seen without brute force. Zero.

Survival is 71.9% against 92.2% for the parameter roll, and that is the
EXPECTED direction, not a regression. Rolling structure aggressively is
supposed to fail sometimes; §3.5's reject-and-resample is what that is for.
A composer that only emits safe stacks is not exploring. Deliberately not
tuned.

Recipes are serialisable (op ids + param-block bindings), so a season
manifest can ship one -- which is also what will let an archetype become a
spatial REGION inside a strate rather than a label on it.

Build clean, 24 succeeded / 0 failed, verified by me in VoxelM.log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-09-04 15:21:27 +02:00
1 parent 190ff381c0
commit 859a9b25c1
10 files changed
+2194 -55

No files matched your search

@@ -0,0 +1,564 @@
// Tier 4b — deterministic structure recipes, dependency-safe materialisation, and measurement.
#if WITH_DEV_AUTOMATION_TESTS
#include "Misc/AutomationTest.h"
#include "HAL/PlatformTime.h"
#include "Math/RandomStream.h"
#include "VoxelForgeTestFixture.h"
#include "VoxelDensityOpStack.h"
#include "VoxelSettings.h"
#include "VoxelStrateComposer.h"
#include "VoxelStrateMeasure.h"
namespace
{
constexpr int32 NumCandidates = 64;
constexpr float LargestComponentSurvivalThreshold = 0.50f;
constexpr int32 BoxChecksPerCandidate = 40;
constexpr int32 BoxStep = 1;
constexpr int32 BoxCells = 8;
const TCHAR* VF_ConnectivityName(EVoxelConnectivityResult Result)
{
switch (Result)
{
case EVoxelConnectivityResult::Connected: return TEXT("Connected");
case EVoxelConnectivityResult::NotConnectedAtThisResolution: return TEXT("NotConnected");
case EVoxelConnectivityResult::StartCellSolid: return TEXT("StartCellSolid");
case EVoxelConnectivityResult::GoalCellSolid: return TEXT("GoalCellSolid");
case EVoxelConnectivityResult::OutOfWindow: return TEXT("OutOfWindow");
case EVoxelConnectivityResult::CoarseLiedBudgetExhausted: return TEXT("CoarseLiedBudget");
}
return TEXT("Unknown");
}
const TCHAR* VF_ShapeName(EVoxelStrateOpClass OpClass)
{
switch (OpClass)
{
case EVoxelStrateOpClass::RoomGraphSource: return TEXT("RoomGraph");
case EVoxelStrateOpClass::LatticeCorridorSource:return TEXT("Lattice");
case EVoxelStrateOpClass::ShaftFieldSource: return TEXT("Shaft");
case EVoxelStrateOpClass::IslandBlobSource: return TEXT("Island");
case EVoxelStrateOpClass::NoiseRibbonSource: return TEXT("Noise");
default: return TEXT("invalid");
}
}
int32 VF_FloorDiv(int32 A, int32 B)
{
const int32 Q = A / B;
const int32 R = A % B;
return (R != 0 && (R < 0) != (B < 0)) ? Q - 1 : Q;
}
bool VF_BitEqual(float A, float B)
{
uint32 ABits = 0;
uint32 BBits = 0;
FMemory::Memcpy(&ABits, &A, sizeof(ABits));
FMemory::Memcpy(&BBits, &B, sizeof(BBits));
return ABits == BBits;
}
struct FBlockSpec
{
EVoxelStrateParamBlock Block;
ECaveGeneratorType Archetype;
uint32 SeedSalt;
};
static const FBlockSpec GBlockSpecs[] =
{
{ EVoxelStrateParamBlock::TunnelNetwork, ECaveGeneratorType::TunnelNetwork, 0x1001u },
{ EVoxelStrateParamBlock::Slab, ECaveGeneratorType::FlatPlain, 0x1003u },
{ EVoxelStrateParamBlock::Maze, ECaveGeneratorType::Maze, 0x1005u },
{ EVoxelStrateParamBlock::Surface, ECaveGeneratorType::SurfaceWorld, 0x1007u },
{ EVoxelStrateParamBlock::VerticalShaft, ECaveGeneratorType::VerticalShafts, 0x1009u },
{ EVoxelStrateParamBlock::FloatingIsland, ECaveGeneratorType::FloatingIslands, 0x100Bu },
};
void VF_CopyRollBlock(FVoxelStrateArchetypeParams& OutParams,
const FVoxelStrateRollInfo& Roll,
ECaveGeneratorType Archetype)
{
switch (Archetype)
{
case ECaveGeneratorType::TunnelNetwork:
OutParams.TunnelNetworkParams = Roll.ArchetypeParams.TunnelNetworkParams;
break;
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber:
OutParams.SlabParams = Roll.ArchetypeParams.SlabParams;
break;
case ECaveGeneratorType::Maze:
OutParams.MazeParams = Roll.ArchetypeParams.MazeParams;
break;
case ECaveGeneratorType::SurfaceWorld:
OutParams.SurfaceParams = Roll.ArchetypeParams.SurfaceParams;
break;
case ECaveGeneratorType::VerticalShafts:
OutParams.VerticalShaftParams = Roll.ArchetypeParams.VerticalShaftParams;
break;
case ECaveGeneratorType::FloatingIslands:
OutParams.FloatingIslandParams = Roll.ArchetypeParams.FloatingIslandParams;
break;
default:
break;
}
}
void VF_SetAllRuntimeBounds(FVoxelStrateArchetypeParams& Params,
float TopWorldZ, float BottomWorldZ)
{
Params.TunnelNetworkParams.StrateTopWorldZ = TopWorldZ;
Params.TunnelNetworkParams.StrateBottomWorldZ = BottomWorldZ;
Params.SlabParams.StrateTopWorldZ = TopWorldZ;
Params.SlabParams.StrateBottomWorldZ = BottomWorldZ;
Params.MazeParams.StrateTopWorldZ = TopWorldZ;
Params.MazeParams.StrateBottomWorldZ = BottomWorldZ;
Params.SurfaceParams.StrateTopWorldZ = TopWorldZ;
Params.SurfaceParams.StrateBottomWorldZ = BottomWorldZ;
Params.VerticalShaftParams.StrateTopWorldZ = TopWorldZ;
Params.VerticalShaftParams.StrateBottomWorldZ = BottomWorldZ;
Params.FloatingIslandParams.StrateTopWorldZ = TopWorldZ;
Params.FloatingIslandParams.StrateBottomWorldZ = BottomWorldZ;
}
class FStackDensitySampler final : public IVoxelStrateDensitySampler
{
public:
explicit FStackDensitySampler(const FVoxelOpStack& InStack) : Stack(InStack) {}
float SampleDensity(float WorldX, float WorldY, float WorldZ) const override
{
return Stack.EvalMC(WorldX, WorldY, WorldZ);
}
private:
const FVoxelOpStack& Stack;
};
struct FBoxVerdictReport
{
int32 Mixed = 0;
int32 AllSolid = 0;
int32 AllAir = 0;
int32 Proved = 0;
int64 CheckedVoxels = 0;
int32 Violations = 0;
FString FirstViolation;
};
FBoxVerdictReport VF_CheckStackBoxVerdicts(const FVoxelOpStack& Stack,
const FVoxelOpContext& Context,
int32 CandidateIndex,
int32 BottomVoxelZ,
int32 TopVoxelZ)
{
FBoxVerdictReport Report;
constexpr int32 Extent = BoxStep * BoxCells;
constexpr int32 SpanCells = 40;
const int32 LoTile = VF_FloorDiv(BottomVoxelZ, Extent);
const int32 HiTile = FMath::Max(LoTile, VF_FloorDiv(TopVoxelZ, Extent));
FRandomStream Rng(0x6C617731 ^ CandidateIndex);
for (int32 BoxIndex = 0; BoxIndex < BoxChecksPerCandidate; ++BoxIndex)
{
const FIntVector Origin(
Rng.RandRange(-SpanCells, SpanCells) * Extent,
Rng.RandRange(-SpanCells, SpanCells) * Extent,
Rng.RandRange(LoTile, HiTile) * Extent);
const FBox Box(
FVector((float)(Origin.X - BoxStep), (float)(Origin.Y - BoxStep),
(float)(Origin.Z - BoxStep)),
FVector((float)(Origin.X + (BoxCells + 1) * BoxStep),
(float)(Origin.Y + (BoxCells + 1) * BoxStep),
(float)(Origin.Z + (BoxCells + 1) * BoxStep)));
const EVoxelTileClass Verdict = Stack.ClassifyBox(Box, Context);
if (Verdict == EVoxelTileClass::Mixed)
{
++Report.Mixed;
continue;
}
if (Verdict == EVoxelTileClass::AllSolid)
{
++Report.AllSolid;
}
else
{
++Report.AllAir;
}
++Report.Proved;
const int32 GridDim = BoxCells + 1;
const bool bClaimsSolid = Verdict == EVoxelTileClass::AllSolid;
for (int32 GZ = -1; GZ <= GridDim; ++GZ)
for (int32 GY = -1; GY <= GridDim; ++GY)
for (int32 GX = -1; GX <= GridDim; ++GX)
{
const float X = (float)(Origin.X + GX * BoxStep);
const float Y = (float)(Origin.Y + GY * BoxStep);
const float Z = (float)(Origin.Z + GZ * BoxStep);
const float Density = Stack.EvalMC(X, Y, Z);
++Report.CheckedVoxels;
const bool bAgrees = bClaimsSolid ? (Density < 0.0f) : (Density >= 0.0f);
if (!bAgrees)
{
++Report.Violations;
if (Report.FirstViolation.IsEmpty())
{
Report.FirstViolation = FString::Printf(
TEXT("candidate %d box %d origin (%d,%d,%d) said %s but MC density "
"at (%.0f,%.0f,%.0f) was %.9g"),
CandidateIndex, BoxIndex, Origin.X, Origin.Y, Origin.Z,
bClaimsSolid ? TEXT("AllSolid") : TEXT("AllAir"),
X, Y, Z, Density);
}
}
}
}
return Report;
}
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FVoxelForgeComposerStructureRollTest,
"VoxelForge.Composer.StructureRoll",
EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter)
bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
{
using namespace VoxelForgeTest;
(void)Parameters;
const double TestStartSeconds = FPlatformTime::Seconds();
const FString SettingsPath = TEXT("/Game/VoxelForge/DA_Settings.DA_Settings");
UVoxelSettings* AuthoredSettings = LoadObject<UVoxelSettings>(nullptr, *SettingsPath);
if (AuthoredSettings == nullptr)
{
AddError(FString::Printf(TEXT("Could not load %s; structure roll did not run."), *SettingsPath));
return false;
}
FVoxelStrateCorpus Corpus;
FString CorpusReport;
const bool bCorpusLoaded = Corpus.LoadFromAssetRegistry(CorpusReport);
AddInfo(CorpusReport);
TestTrue(TEXT("structure roll has a usable corpus for parameter blocks"),
bCorpusLoaded && Corpus.IsValid());
if (!bCorpusLoaded || !Corpus.IsValid())
{
return false;
}
FTestWorld World;
World.Build(1337, 2, true);
if (!World.IsValid())
{
AddError(World.WhyInvalid());
return false;
}
TestTrue(TEXT("structure-roll fixture keeps WorldRadiusVoxels at 0"),
World.Settings->WorldRadiusVoxels == 0.0f);
constexpr int32 CandidateStrateIndex = FTestWorld::SlotFlatPlain;
int32 TopVoxelZ = 0;
int32 BottomVoxelZ = 0;
if (!World.GetSlotVoxelZRange(CandidateStrateIndex, TopVoxelZ, BottomVoxelZ))
{
AddError(TEXT("structure-roll fixture has no candidate Z range"));
return false;
}
const float TopWorldZ = (float)TopVoxelZ + 1.0f;
const float BottomWorldZ = (float)BottomVoxelZ;
FVector ArrivalPoint = FVector::ZeroVector;
FVector DeparturePoint = FVector::ZeroVector;
int32 ArrivalCount = 0;
int32 DepartureCount = 0;
for (const FVoxelPassage& Passage : World.StrateManager->GetPassages())
{
if (Passage.LowerStrateIndex == CandidateStrateIndex
&& Passage.UpperStrateIndex + 1 == CandidateStrateIndex)
{
ArrivalPoint = Passage.LowerPoint;
++ArrivalCount;
}
if (Passage.UpperStrateIndex == CandidateStrateIndex
&& Passage.LowerStrateIndex == CandidateStrateIndex + 1)
{
DeparturePoint = Passage.UpperPoint;
++DepartureCount;
}
}
TestEqual(TEXT("candidate has exactly one arrival mouth"), ArrivalCount, 1);
TestEqual(TEXT("candidate has exactly one departure mouth"), DepartureCount, 1);
FVoxelStrateMeasureSettings MeasureSettings;
MeasureSettings.SampleStep = 4;
MeasureSettings.RadiusInVoxels = 256;
MeasureSettings.CenterXY = FVector2D::ZeroVector;
MeasureSettings.MaxCells = 8000000;
MeasureSettings.MaxRouteRetries = 16;
MeasureSettings.HeadroomCells = 2;
MeasureSettings.InteriorMarginVoxels = -1;
FString CandidateTable = TEXT(
"candidate | recipe (compact) | air | largest share | walkable | feature scale | arrival->departure\n");
int32 NumSurvivors = 0;
int32 NumNonVacuous = 0;
int32 NumLargestEnough = 0;
int32 NumLawPasses = 0;
int32 NumInvalidRecipes = 0;
int32 NumParameterRollFailures = 0;
int32 TotalBoxMixed = 0;
int32 TotalBoxAllSolid = 0;
int32 TotalBoxAllAir = 0;
int32 TotalBoxProved = 0;
int64 TotalBoxCheckedVoxels = 0;
int32 TotalBoxViolations = 0;
FString FirstBoxViolation;
TArray<int32> ShapeCounts;
ShapeCounts.Init(0, 5);
int32 KCounts[9] = { 0 };
TArray<FVoxelOpStackRecipe> DistinctRecipes;
DistinctRecipes.Reserve(NumCandidates);
const double RollAndMeasureStartSeconds = FPlatformTime::Seconds();
for (int32 CandidateIndex = 0; CandidateIndex < NumCandidates; ++CandidateIndex)
{
const FVoxelOpStackRecipe Recipe = VF_RollStrateStructure(
AuthoredSettings->Seed, CandidateIndex);
const FVoxelOpStackRecipe RepeatRecipe = VF_RollStrateStructure(
AuthoredSettings->Seed, CandidateIndex);
TestTrue(FString::Printf(TEXT("candidate %d recipe reroll is identical"), CandidateIndex),
VF_AreStrateStructureRecipesIdentical(Recipe, RepeatRecipe));
const uint32 RecipeHash = VF_HashStrateStructureRecipe(Recipe);
bool bSeenRecipe = false;
for (const FVoxelOpStackRecipe& Existing : DistinctRecipes)
{
if (VF_HashStrateStructureRecipe(Existing) == RecipeHash
&& VF_AreStrateStructureRecipesIdentical(Existing, Recipe))
{
bSeenRecipe = true;
break;
}
}
if (!bSeenRecipe) { DistinctRecipes.Add(Recipe); }
switch (Recipe.ShapeSource.OpClass)
{
case EVoxelStrateOpClass::RoomGraphSource: ++ShapeCounts[0]; break;
case EVoxelStrateOpClass::LatticeCorridorSource:++ShapeCounts[1]; break;
case EVoxelStrateOpClass::ShaftFieldSource: ++ShapeCounts[2]; break;
case EVoxelStrateOpClass::IslandBlobSource: ++ShapeCounts[3]; break;
case EVoxelStrateOpClass::NoiseRibbonSource: ++ShapeCounts[4]; break;
default: break;
}
if (Recipe.Modifiers.IsValidIndex(0))
{
++KCounts[Recipe.Modifiers.Num()];
}
FVoxelStrateArchetypeParams CandidateParams;
bool bParameterRollsValid = true;
for (const FBlockSpec& Spec : GBlockSpecs)
{
const FVoxelStrateRollInfo BlockRoll = VF_RollStrateParamsDetailedForArchetype(
Corpus, Spec.Archetype,
AuthoredSettings->Seed ^ static_cast<int32>(Spec.SeedSalt), CandidateIndex);
if (!BlockRoll.bValid)
{
bParameterRollsValid = false;
++NumParameterRollFailures;
AddError(FString::Printf(TEXT("candidate %d parameter block %d failed: %s"),
CandidateIndex, static_cast<int32>(Spec.Block),
*BlockRoll.FailureReason));
continue;
}
VF_CopyRollBlock(CandidateParams, BlockRoll, Spec.Archetype);
}
VF_SetAllRuntimeBounds(CandidateParams, TopWorldZ, BottomWorldZ);
FString AirText = TEXT("invalid");
FString LargestText = TEXT("invalid");
FString WalkableText = TEXT("invalid");
FString FeatureText = TEXT("invalid");
FString LawText = TEXT("not measured");
FVoxelOpStack Stack;
FVoxelOpContext Context;
FString BuildError;
const bool bBuilt = bParameterRollsValid && VF_BuildStackFromRecipe(
Recipe, CandidateParams, AuthoredSettings->Seed, 14.0f,
World.StrateManager.Get(), Stack, Context, &BuildError);
if (!bBuilt)
{
++NumInvalidRecipes;
CandidateTable += FString::Printf(
TEXT("%d | %s | invalid | invalid | invalid | invalid | invalid | build=%s\n"),
CandidateIndex, *VF_FormatStrateStructureRecipe(Recipe),
bParameterRollsValid ? *BuildError : TEXT("parameter block roll failed"));
TestTrue(FString::Printf(TEXT("candidate %d recipe materialises"), CandidateIndex), false);
continue;
}
TestEqual(FString::Printf(TEXT("candidate %d has four mandatory structural posts"), CandidateIndex),
Stack.Num(), Recipe.Modifiers.Num() + 7);
FString ValidationError;
TestTrue(FString::Printf(TEXT("candidate %d passes ValidateChannelOrder"), CandidateIndex),
Stack.ValidateChannelOrder(&ValidationError));
TestTrue(FString::Printf(TEXT("candidate %d context keeps WorldRadiusVoxels at 0"), CandidateIndex),
Context.WorldRadiusVoxels == 0.0f);
Stack.PrepareChunk(Context);
FStackDensitySampler Sampler(Stack);
const FVoxelStrateMetrics Metrics = VF_MeasureStrateWithSampler(
Sampler, BottomVoxelZ, TopVoxelZ + 1,
Context.EdgeSealThickness, MeasureSettings);
FVoxelConnectivityDiagnostics Law;
if (ArrivalCount == 1 && DepartureCount == 1)
{
Law = VF_DiagnoseConnectivityWithSampler(
Sampler, BottomVoxelZ, TopVoxelZ + 1,
Context.EdgeSealThickness, ArrivalPoint, DeparturePoint, MeasureSettings);
LawText = VF_ConnectivityName(Law.Result);
if (Law.bStartSnapped || Law.bGoalSnapped) { LawText += TEXT(" (snapped)"); }
}
else
{
LawText = FString::Printf(TEXT("mouths=%d/%d"), ArrivalCount, DepartureCount);
}
const bool bNonVacuous = Metrics.bValid
&& Metrics.NumSampled > 0 && Metrics.NumAir > 0 && Metrics.NumSolid > 0;
const bool bLargestEnough = bNonVacuous
&& Metrics.LargestComponentShare >= LargestComponentSurvivalThreshold;
const bool bLawPass = ArrivalCount == 1 && DepartureCount == 1
&& Law.bValid && Law.Result == EVoxelConnectivityResult::Connected
&& !Law.bStartSnapped && !Law.bGoalSnapped;
if (bNonVacuous) { ++NumNonVacuous; }
if (bLargestEnough) { ++NumLargestEnough; }
if (bLawPass) { ++NumLawPasses; }
if (bNonVacuous && bLargestEnough && bLawPass) { ++NumSurvivors; }
if (Metrics.bValid)
{
AirText = FString::Printf(TEXT("%.6f"), Metrics.AirFraction);
LargestText = FString::Printf(TEXT("%.6f"), Metrics.LargestComponentShare);
WalkableText = FString::Printf(TEXT("%.6f"), Metrics.WalkableFraction);
FeatureText = FString::Printf(TEXT("%.6f"), Metrics.MedianFeatureScale);
}
const FBoxVerdictReport BoxReport = VF_CheckStackBoxVerdicts(
Stack, Context, CandidateIndex, BottomVoxelZ, TopVoxelZ);
TotalBoxMixed += BoxReport.Mixed;
TotalBoxAllSolid += BoxReport.AllSolid;
TotalBoxAllAir += BoxReport.AllAir;
TotalBoxProved += BoxReport.Proved;
TotalBoxCheckedVoxels += BoxReport.CheckedVoxels;
TotalBoxViolations += BoxReport.Violations;
if (FirstBoxViolation.IsEmpty() && !BoxReport.FirstViolation.IsEmpty())
{
FirstBoxViolation = BoxReport.FirstViolation;
}
// Same recipe + same block vectors + same seed must yield the same world values. This is
// checked on a fixed point array for every candidate, including cache-backed sources.
FVoxelOpStack RepeatStack;
FVoxelOpContext RepeatContext;
FString RepeatBuildError;
const bool bRepeatBuilt = VF_BuildStackFromRecipe(
Recipe, CandidateParams, AuthoredSettings->Seed, 14.0f,
World.StrateManager.Get(), RepeatStack, RepeatContext, &RepeatBuildError);
TestTrue(FString::Printf(TEXT("candidate %d same recipe rebuild succeeds"), CandidateIndex),
bRepeatBuilt);
if (bRepeatBuilt)
{
RepeatStack.PrepareChunk(RepeatContext);
const FVector ProbePoints[] = {
FVector(-91.0f, -37.0f, BottomWorldZ + 17.0f),
FVector(0.0f, 0.0f, (BottomWorldZ + TopWorldZ) * 0.5f),
FVector(73.0f, 119.0f, TopWorldZ - 19.0f),
FVector(-256.0f, 256.0f, BottomWorldZ + 61.0f),
};
bool bSameWorld = true;
for (const FVector& Point : ProbePoints)
{
if (!VF_BitEqual(Stack.EvalMC(Point.X, Point.Y, Point.Z),
RepeatStack.EvalMC(Point.X, Point.Y, Point.Z)))
{
bSameWorld = false;
break;
}
}
TestTrue(FString::Printf(TEXT("candidate %d same seed yields same world"), CandidateIndex),
bSameWorld);
}
CandidateTable += FString::Printf(
TEXT("%d | %s | %s | %s | %s | %s | %s\n"),
CandidateIndex,
*VF_FormatStrateStructureRecipe(Recipe),
*AirText, *LargestText, *WalkableText, *FeatureText, *LawText);
}
const double RollAndMeasureSeconds = FPlatformTime::Seconds() - RollAndMeasureStartSeconds;
AddInfo(CandidateTable);
FString ShapeDistribution = TEXT("Shape source distribution:");
for (int32 Index = 0; Index < 5; ++Index)
{
const EVoxelStrateOpClass ShapeClass[] = {
EVoxelStrateOpClass::RoomGraphSource,
EVoxelStrateOpClass::LatticeCorridorSource,
EVoxelStrateOpClass::ShaftFieldSource,
EVoxelStrateOpClass::IslandBlobSource,
EVoxelStrateOpClass::NoiseRibbonSource,
};
ShapeDistribution += FString::Printf(TEXT(" %s=%d"),
VF_ShapeName(ShapeClass[Index]), ShapeCounts[Index]);
}
FString KDistribution = TEXT("k distribution:");
for (int32 K = 4; K <= 8; ++K)
{
KDistribution += FString::Printf(TEXT(" k%d=%d"), K, KCounts[K]);
}
AddInfo(ShapeDistribution);
AddInfo(KDistribution);
AddInfo(FString::Printf(
TEXT("Structure diversity: %d distinct recipes of %d; invalid recipes emitted=%d; "
"parameter-block roll failures=%d; recipe/build/measure time=%.3fs."),
DistinctRecipes.Num(), NumCandidates, NumInvalidRecipes,
NumParameterRollFailures, RollAndMeasureSeconds));
AddInfo(FString::Printf(
TEXT("Survival: %d/%d (%.1f%%). Criteria: non-vacuous (valid, sampled, air>0, solid>0), "
"largest component share >= %.2f, and exact unsnapped arrival->departure law "
"Result=Connected. Intermediate counts: non-vacuous=%d, largest threshold=%d, "
"law pass=%d. Lower survival is expected for aggressive structure rolls."),
NumSurvivors, NumCandidates, 100.0f * (float)NumSurvivors / (float)NumCandidates,
LargestComponentSurvivalThreshold, NumNonVacuous, NumLargestEnough, NumLawPasses));
AddInfo(FString::Printf(
TEXT("Rolled custom-stack box verdicts: mixed=%d, AllSolid=%d, AllAir=%d, proved=%d, "
"brute-force voxels checked=%lld, violations=%d. %s"),
TotalBoxMixed, TotalBoxAllSolid, TotalBoxAllAir, TotalBoxProved,
TotalBoxCheckedVoxels, TotalBoxViolations,
FirstBoxViolation.IsEmpty() ? TEXT("No violations.") : *FirstBoxViolation));
TestEqual(TEXT("exactly 64 structure candidates were requested"), NumCandidates, 64);
TestEqual(TEXT("invalid recipes emitted by the roller/build path"), NumInvalidRecipes, 0);
TestEqual(TEXT("custom-stack box verdict violations"), TotalBoxViolations, 0);
TestTrue(TEXT("structure roller emits 64 distinct structural recipes"),
DistinctRecipes.Num() == NumCandidates);
const double TestSeconds = FPlatformTime::Seconds() - TestStartSeconds;
AddInfo(FString::Printf(TEXT("StructureRoll total runtime %.3fs; WorldRadiusVoxels=0."),
TestSeconds));
return true;
}
#endif // WITH_DEV_AUTOMATION_TESTS
@@ -19,6 +19,7 @@
// decomposition is right" into a proof.
#include "VoxelDensityOpStack.h"
#include "VoxelStrateComposer.h"
#include "VoxelDensityPrimitives.h" // VF_ApplyOriginSpine / seals / PassageCarving
#include "VoxelCaveMorphology.h" // VoxelSDF::Capsule, VoxelHash
@@ -296,6 +297,130 @@ namespace
float Value;
};
//=======================================================================
// TIER 4b — GENERIC ROLLED SOURCE/MODIFIER PRIMITIVES
//=======================================================================
// These two small primitives keep the non-room channel space large enough for the promised
// k=4..8 draw. They are deliberately bounded: their box contracts are amplitude proofs, not
// guesses. They do not carry any room/shaft/surface pointer, so the resource graph correctly
// treats them as legal after every SDF shape source.
class FNoiseRibbonSource final : public IVoxelDensityOp
{
public:
FNoiseRibbonSource(const FMazeGenerationParams& InP, int32 InSeed)
: CellSize(FMath::Max(InP.CellSize, 8.0f))
, CorridorRadius(FMath::Max(InP.CorridorRadius, 0.5f))
, SeedU(static_cast<uint32>(InSeed) ^ 0x5249626Eu) {}
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::None; }
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Sdf; }
bool IsAdditive() const override { return false; }
void PrepareChunk(const FVoxelOpContext&) override {}
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
{
const float Frequency = 1.0f / CellSize;
const FVector NoisePos(
WorldX * Frequency + VoxelHash::SeedOffset(SeedU, 3.17f),
WorldY * Frequency + VoxelHash::SeedOffset(SeedU, 7.31f),
WorldZ * Frequency + VoxelHash::SeedOffset(SeedU, 11.47f));
const float Ribbon = FMath::Clamp(
FMath::Abs(VoxelNoise::FBM((float)NoisePos.X, (float)NoisePos.Y, (float)NoisePos.Z,
VoxelGenLOD::Eff(3), 2.0f, 0.5f))
* VOXEL_NOISE_SCALE,
0.0f, 2.0f);
InOut.Sdf = Ribbon * CellSize - CorridorRadius;
}
EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override
{
return EVoxelOpEffect::Identity;
}
void PropagateSdfOverBox(FVoxelBoxSdfInterval& InOut, const FBox&,
const FVoxelOpContext&) const override
{
if (!FMath::IsFinite(CellSize) || !FMath::IsFinite(CorridorRadius)
|| CellSize <= 0.0f || CorridorRadius < 0.0f)
{
InOut.SetUnknown();
return;
}
// Eval clamps the FBM envelope to [0,2]. This interval is exact enough for safety
// and independent of the queried box, so it never claims a false empty region.
InOut.Set(-CorridorRadius, 2.0f * CellSize - CorridorRadius);
}
const TCHAR* DebugName() const override { return TEXT("NoiseRibbonSource"); }
private:
float CellSize;
float CorridorRadius;
uint32 SeedU;
};
class FDensityNoiseMod final : public IVoxelDensityOp
{
public:
FDensityNoiseMod(float InStrength, float InFrequency, int32 InOctaves,
int32 InSeed, bool bInFill)
: Strength(FMath::Max(InStrength, 0.0f))
, Frequency(FMath::Max(InFrequency, 0.0001f))
, Octaves(FMath::Clamp(InOctaves, 1, 8))
, SeedU(static_cast<uint32>(InSeed) ^ (bInFill ? 0x46696C6Cu : 0x43617276u))
, bFill(bInFill) {}
EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; }
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density; }
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {}
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
{
if (!(Strength > 0.0f)) { return; }
const FVector NoisePos(
WorldX * Frequency + VoxelHash::SeedOffset(SeedU, 13.2f),
WorldY * Frequency + VoxelHash::SeedOffset(SeedU, 17.8f),
WorldZ * Frequency + VoxelHash::SeedOffset(SeedU, 23.4f));
const float Noise01 = FMath::Clamp(
VoxelNoise::FBM((float)NoisePos.X, (float)NoisePos.Y, (float)NoisePos.Z,
VoxelGenLOD::Eff(Octaves), 2.0f, 0.5f) * 0.5f + 0.5f,
0.0f, 1.0f);
const float Delta = Strength * Noise01;
InOut.Density += bFill ? Delta : -Delta;
}
EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override
{
if (!(Strength > 0.0f)) { return EVoxelOpEffect::Identity; }
return bFill ? EVoxelOpEffect::FillOnly : EVoxelOpEffect::CarveOnly;
}
float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override
{
return bFill ? 0.0f : Strength;
}
float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override
{
return bFill ? Strength : 0.0f;
}
const TCHAR* DebugName() const override
{
return bFill ? TEXT("DensityNoiseFillMod") : TEXT("DensityNoiseCarveMod");
}
private:
float Strength;
float Frequency;
int32 Octaves;
uint32 SeedU;
bool bFill;
};
//=========================================================================
// RÔLE 1 — SOURCE : COULOIRS SUR TREILLIS 3D / 3D LATTICE CORRIDORS
//=========================================================================
@@ -950,6 +1075,7 @@ namespace
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::None; }
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
EVoxelOpResourceMask ProvidedResources() const override { return VoxelOpResources::SurfaceColumn; }
bool IsAdditive() const override { return false; }
bool IsXYPure() const override { return false; } // voir le bloc ci-dessus
@@ -1081,6 +1207,7 @@ namespace
EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; }
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density; }
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::SurfaceColumn; }
bool IsAdditive() const override { return false; }
void PrepareChunk(const FVoxelOpContext&) override {}
bool IsXYPure() const override { return false; } // franchement non : voir `Frac`
@@ -1771,6 +1898,7 @@ namespace
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::None; }
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Sdf; }
EVoxelOpResourceMask ProvidedResources() const override { return VoxelOpResources::ShaftGeometry; }
bool IsAdditive() const override { return false; }
void PrepareChunk(const FVoxelOpContext&) override {}
@@ -2194,6 +2322,7 @@ namespace
return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
}
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::ShaftGeometry; }
bool IsAdditive() const override { return false; }
void PrepareChunk(const FVoxelOpContext&) override {}
@@ -2577,6 +2706,7 @@ namespace
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::None; }
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Sdf; }
EVoxelOpResourceMask ProvidedResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return false; }
void PrepareChunk(const FVoxelOpContext& Ctx) override
@@ -3606,6 +3736,7 @@ namespace
return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
}
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {}
@@ -3715,6 +3846,7 @@ namespace
return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
}
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {}
@@ -3803,6 +3935,7 @@ namespace
return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
}
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {}
@@ -3886,6 +4019,7 @@ namespace
return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
}
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {}
@@ -3986,6 +4120,7 @@ namespace
return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
}
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {}
@@ -4060,6 +4195,7 @@ namespace
return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
}
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {}
@@ -4151,6 +4287,7 @@ namespace
return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
}
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {}
@@ -4267,6 +4404,7 @@ namespace
return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
}
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {}
@@ -4330,6 +4468,7 @@ namespace
return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
}
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {}
@@ -4437,6 +4576,7 @@ namespace
return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
}
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {}
@@ -4556,6 +4696,7 @@ namespace
return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
}
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {}
@@ -4794,6 +4935,23 @@ bool FVoxelOpStack::ValidateChannelOrder(FString* OutError) const
return Result.IsEmpty() ? TEXT("none") : Result;
};
auto DescribeResourceMask = [](EVoxelOpResourceMask Mask) -> FString
{
FString Result;
if ((Mask & VoxelOpResources::RoomGeometry) != 0) { Result += TEXT("RoomGeometry"); }
if ((Mask & VoxelOpResources::ShaftGeometry) != 0)
{
if (!Result.IsEmpty()) { Result += TEXT(", "); }
Result += TEXT("ShaftGeometry");
}
if ((Mask & VoxelOpResources::SurfaceColumn) != 0)
{
if (!Result.IsEmpty()) { Result += TEXT(", "); }
Result += TEXT("SurfaceColumn");
}
return Result.IsEmpty() ? TEXT("none") : Result;
};
auto Fail = [&](int32 Index, const FOpEntry* Entry, const TCHAR* Rule) -> bool
{
if (OutError != nullptr)
@@ -4803,8 +4961,10 @@ bool FVoxelOpStack::ValidateChannelOrder(FString* OutError) const
const EVoxelOpChannelMask Reads = Entry != nullptr ? Entry->Reads : VoxelOpChannels::None;
const EVoxelOpChannelMask Writes = Entry != nullptr ? Entry->Writes : VoxelOpChannels::None;
*OutError = FString::Printf(
TEXT("op %d (%s) violates channel DAG: %s; reads=[%s], writes=[%s]"),
Index, Name, Rule, *DescribeMask(Reads), *DescribeMask(Writes));
TEXT("op %d (%s) violates stack DAG: %s; reads=[%s], writes=[%s], requires=[%s], provides=[%s]"),
Index, Name, Rule, *DescribeMask(Reads), *DescribeMask(Writes),
*DescribeResourceMask(Entry != nullptr ? Entry->RequiredResources : VoxelOpResources::None),
*DescribeResourceMask(Entry != nullptr ? Entry->ProvidedResources : VoxelOpResources::None));
}
return false;
};
@@ -4812,6 +4972,7 @@ bool FVoxelOpStack::ValidateChannelOrder(FString* OutError) const
// The two array slots are deliberately explicit: FVoxelOpSample has exactly two fields, and
// adding a third field requires extending EVoxelOpChannel and this validator together.
int32 LastWriter[2] = { INDEX_NONE, INDEX_NONE };
EVoxelOpResourceMask AvailableResources = VoxelOpResources::None;
auto ChannelIndex = [](EVoxelOpChannelMask Channel) -> int32
{
@@ -4823,6 +4984,21 @@ bool FVoxelOpStack::ValidateChannelOrder(FString* OutError) const
const FOpEntry& Entry = Ops[Index];
if (Entry.Op.Get() == nullptr) { return Fail(Index, &Entry, TEXT("null operator")); }
if ((Entry.RequiredResources & VoxelOpResources::All) != Entry.RequiredResources)
{
return Fail(Index, &Entry, TEXT("requires an unknown op resource"));
}
if ((Entry.ProvidedResources & VoxelOpResources::All) != Entry.ProvidedResources)
{
return Fail(Index, &Entry, TEXT("provides an unknown op resource"));
}
if ((Entry.RequiredResources
& static_cast<EVoxelOpResourceMask>(~AvailableResources)) != 0)
{
return Fail(Index, &Entry,
TEXT("requires op state before an earlier provider published it"));
}
if ((Entry.Reads & VoxelOpChannels::All) != Entry.Reads)
{
return Fail(Index, &Entry, TEXT("reads an unknown sample channel"));
@@ -4883,6 +5059,8 @@ bool FVoxelOpStack::ValidateChannelOrder(FString* OutError) const
LastWriter[ChannelIndex(Channel)] = Index;
}
}
AvailableResources |= Entry.ProvidedResources;
}
return true;
@@ -4909,6 +5087,117 @@ void FVoxelOpStack::AppendStructuralPost(float StrateTopWorldZ, float StrateBott
namespace VoxelDensityOps
{
bool GetStrateOpContract(EVoxelStrateOpClass OpClass, FVoxelStrateOpContract& OutContract)
{
const FStrateGenerationParams TunnelParams;
const FSlabGenerationParams SlabParams;
const FMazeGenerationParams MazeParams;
const FSurfaceGenerationParams SurfaceParams;
const FVerticalShaftParams ShaftParams;
const FFloatingIslandParams IslandParams;
TUniquePtr<IVoxelDensityOp> Probe;
switch (OpClass)
{
case EVoxelStrateOpClass::ConstantRockSource:
Probe = MakeConstantRockSource(TunnelParams.BaseDensity);
break;
case EVoxelStrateOpClass::ConstantVoidSource:
Probe = MakeConstantVoidSource(IslandParams.BaseDensity);
break;
case EVoxelStrateOpClass::RoomGraphSource:
Probe = MakeUnique<FRoomGraphSource>(TunnelParams, 0, nullptr);
break;
case EVoxelStrateOpClass::LatticeCorridorSource:
Probe = MakeUnique<FLatticeCorridorSource>(MazeParams, 0);
break;
case EVoxelStrateOpClass::ShaftFieldSource:
Probe = MakeUnique<FShaftFieldSource>(ShaftParams, 0, 14.0f);
break;
case EVoxelStrateOpClass::IslandBlobSource:
Probe = MakeUnique<FIslandBlobSource>(IslandParams, 0);
break;
case EVoxelStrateOpClass::NoiseRibbonSource:
Probe = MakeUnique<FNoiseRibbonSource>(MazeParams, 0);
break;
case EVoxelStrateOpClass::SdfRoughnessMod:
Probe = MakeSdfRoughnessMod(MazeParams.SurfaceRoughness, 0.12f, 3, 8.0f);
break;
case EVoxelStrateOpClass::SdfCarve:
Probe = MakeSdfCarve(2.0f, TunnelParams.BaseDensity);
break;
case EVoxelStrateOpClass::SdfFill:
Probe = MakeSdfFill(2.0f, IslandParams.BaseDensity);
break;
case EVoxelStrateOpClass::GridColumnMod:
Probe = MakeUnique<FGridColumnMod>(SlabParams, 0);
break;
case EVoxelStrateOpClass::CaveRoughnessMod:
Probe = MakeUnique<FCaveRoughnessMod>(TunnelParams, 0);
break;
case EVoxelStrateOpClass::CaveTerraceMod:
Probe = MakeUnique<FCaveTerraceMod>(TunnelParams, 0, nullptr);
break;
case EVoxelStrateOpClass::LayerLineMod:
Probe = MakeUnique<FLayerLineMod>(TunnelParams, nullptr);
break;
case EVoxelStrateOpClass::RibbingMod:
Probe = MakeUnique<FRibbingMod>(TunnelParams, nullptr);
break;
case EVoxelStrateOpClass::CaveOverhangMod:
Probe = MakeUnique<FCaveOverhangMod>(TunnelParams, 0, nullptr);
break;
case EVoxelStrateOpClass::CaveCliffMod:
Probe = MakeUnique<FCaveCliffMod>(TunnelParams, 0, nullptr);
break;
case EVoxelStrateOpClass::ScallopMod:
Probe = MakeUnique<FScallopMod>(TunnelParams, 0, nullptr);
break;
case EVoxelStrateOpClass::CaveArchMod:
Probe = MakeUnique<FCaveArchMod>(TunnelParams, nullptr);
break;
case EVoxelStrateOpClass::RoomColumnMod:
Probe = MakeUnique<FRoomColumnMod>(TunnelParams, nullptr);
break;
case EVoxelStrateOpClass::DomeMod:
Probe = MakeUnique<FDomeMod>(TunnelParams, nullptr);
break;
case EVoxelStrateOpClass::PinchMod:
Probe = MakeUnique<FPinchMod>(TunnelParams, nullptr);
break;
case EVoxelStrateOpClass::FloorBiasMod:
Probe = MakeUnique<FFloorBiasMod>(TunnelParams, nullptr);
break;
case EVoxelStrateOpClass::WormFieldSource:
Probe = MakeUnique<FWormFieldSource>(TunnelParams, 0);
break;
case EVoxelStrateOpClass::ShaftLedgeMod:
Probe = MakeUnique<FShaftLedgeMod>(ShaftParams, nullptr);
break;
case EVoxelStrateOpClass::DensityNoiseCarveMod:
Probe = MakeUnique<FDensityNoiseMod>(1.0f, 0.02f, 3, 0, false);
break;
case EVoxelStrateOpClass::DensityNoiseFillMod:
Probe = MakeUnique<FDensityNoiseMod>(1.0f, 0.02f, 3, 0, true);
break;
default:
return false;
}
if (!Probe)
{
return false;
}
OutContract.Role = Probe->GetRole();
OutContract.Reads = Probe->ChannelReads();
OutContract.Writes = Probe->ChannelWrites();
OutContract.bAdditive = Probe->IsAdditive();
OutContract.RequiredResources = Probe->RequiredResources();
OutContract.ProvidedResources = Probe->ProvidedResources();
return true;
}
TUniquePtr<IVoxelDensityOp> MakeConstantRockSource(float BaseDensity)
{
return MakeUnique<FConstantFieldSource>(BaseDensity);
@@ -5149,3 +5438,549 @@ namespace VoxelDensityOps
P.BoundarySealThickness, P.BaseDensity, SpineRadius, StrateManager);
}
}
namespace VoxelStrateRecipePrivate
{
const FStrateGenerationParams* Tunnel(const FVoxelStrateArchetypeParams& Params)
{
return &Params.TunnelNetworkParams;
}
const FSlabGenerationParams* Slab(const FVoxelStrateArchetypeParams& Params)
{
return &Params.SlabParams;
}
const FMazeGenerationParams* Maze(const FVoxelStrateArchetypeParams& Params)
{
return &Params.MazeParams;
}
const FSurfaceGenerationParams* Surface(const FVoxelStrateArchetypeParams& Params)
{
return &Params.SurfaceParams;
}
const FVerticalShaftParams* Shaft(const FVoxelStrateArchetypeParams& Params)
{
return &Params.VerticalShaftParams;
}
const FFloatingIslandParams* Island(const FVoxelStrateArchetypeParams& Params)
{
return &Params.FloatingIslandParams;
}
float BaseDensity(const FVoxelStrateArchetypeParams& Params, EVoxelStrateParamBlock Block)
{
switch (Block)
{
case EVoxelStrateParamBlock::TunnelNetwork: return Tunnel(Params)->BaseDensity;
case EVoxelStrateParamBlock::Slab: return Slab(Params)->BaseDensity;
case EVoxelStrateParamBlock::Maze: return Maze(Params)->BaseDensity;
case EVoxelStrateParamBlock::Surface: return Surface(Params)->BaseDensity;
case EVoxelStrateParamBlock::VerticalShaft: return Shaft(Params)->BaseDensity;
case EVoxelStrateParamBlock::FloatingIsland: return Island(Params)->BaseDensity;
default: return 8.0f;
}
}
float BoundarySeal(const FVoxelStrateArchetypeParams& Params, EVoxelStrateParamBlock Block)
{
switch (Block)
{
case EVoxelStrateParamBlock::TunnelNetwork: return Tunnel(Params)->BoundarySealThickness;
case EVoxelStrateParamBlock::Slab: return Slab(Params)->BoundarySealThickness;
case EVoxelStrateParamBlock::Maze: return Maze(Params)->BoundarySealThickness;
case EVoxelStrateParamBlock::Surface: return Surface(Params)->BoundarySealThickness;
case EVoxelStrateParamBlock::VerticalShaft: return Shaft(Params)->BoundarySealThickness;
case EVoxelStrateParamBlock::FloatingIsland: return Island(Params)->BoundarySealThickness;
default: return 4.0f;
}
}
float Top(const FVoxelStrateArchetypeParams& Params, EVoxelStrateParamBlock Block)
{
switch (Block)
{
case EVoxelStrateParamBlock::TunnelNetwork: return Tunnel(Params)->StrateTopWorldZ;
case EVoxelStrateParamBlock::Slab: return Slab(Params)->StrateTopWorldZ;
case EVoxelStrateParamBlock::Maze: return Maze(Params)->StrateTopWorldZ;
case EVoxelStrateParamBlock::Surface: return Surface(Params)->StrateTopWorldZ;
case EVoxelStrateParamBlock::VerticalShaft: return Shaft(Params)->StrateTopWorldZ;
case EVoxelStrateParamBlock::FloatingIsland: return Island(Params)->StrateTopWorldZ;
default: return 0.0f;
}
}
float Bottom(const FVoxelStrateArchetypeParams& Params, EVoxelStrateParamBlock Block)
{
switch (Block)
{
case EVoxelStrateParamBlock::TunnelNetwork: return Tunnel(Params)->StrateBottomWorldZ;
case EVoxelStrateParamBlock::Slab: return Slab(Params)->StrateBottomWorldZ;
case EVoxelStrateParamBlock::Maze: return Maze(Params)->StrateBottomWorldZ;
case EVoxelStrateParamBlock::Surface: return Surface(Params)->StrateBottomWorldZ;
case EVoxelStrateParamBlock::VerticalShaft: return Shaft(Params)->StrateBottomWorldZ;
case EVoxelStrateParamBlock::FloatingIsland: return Island(Params)->StrateBottomWorldZ;
default: return 0.0f;
}
}
EVoxelStrateParamBlock ShapeBlock(EVoxelStrateOpClass OpClass)
{
switch (OpClass)
{
case EVoxelStrateOpClass::RoomGraphSource: return EVoxelStrateParamBlock::TunnelNetwork;
case EVoxelStrateOpClass::LatticeCorridorSource:
case EVoxelStrateOpClass::NoiseRibbonSource: return EVoxelStrateParamBlock::Maze;
case EVoxelStrateOpClass::ShaftFieldSource: return EVoxelStrateParamBlock::VerticalShaft;
case EVoxelStrateOpClass::IslandBlobSource: return EVoxelStrateParamBlock::FloatingIsland;
default: return EVoxelStrateParamBlock::None;
}
}
EVoxelStrateParamBlock FixedParamBlock(EVoxelStrateOpClass OpClass)
{
switch (OpClass)
{
case EVoxelStrateOpClass::GridColumnMod:
return EVoxelStrateParamBlock::Slab;
case EVoxelStrateOpClass::CaveRoughnessMod:
case EVoxelStrateOpClass::CaveTerraceMod:
case EVoxelStrateOpClass::LayerLineMod:
case EVoxelStrateOpClass::RibbingMod:
case EVoxelStrateOpClass::CaveOverhangMod:
case EVoxelStrateOpClass::CaveCliffMod:
case EVoxelStrateOpClass::ScallopMod:
case EVoxelStrateOpClass::CaveArchMod:
case EVoxelStrateOpClass::RoomColumnMod:
case EVoxelStrateOpClass::DomeMod:
case EVoxelStrateOpClass::PinchMod:
case EVoxelStrateOpClass::FloorBiasMod:
case EVoxelStrateOpClass::WormFieldSource:
return EVoxelStrateParamBlock::TunnelNetwork;
case EVoxelStrateOpClass::ShaftLedgeMod:
return EVoxelStrateParamBlock::VerticalShaft;
default:
return EVoxelStrateParamBlock::None;
}
}
float SdfBlend(const FVoxelStrateArchetypeParams& Params, EVoxelStrateParamBlock Block)
{
switch (Block)
{
case EVoxelStrateParamBlock::TunnelNetwork:
return Params.TunnelNetworkParams.SDFBlendRadius;
case EVoxelStrateParamBlock::Maze:
return 2.0f;
case EVoxelStrateParamBlock::VerticalShaft:
return 2.0f;
case EVoxelStrateParamBlock::FloatingIsland:
return FMath::Max(Params.FloatingIslandParams.SDFBlendRadius, 0.01f);
default:
return 2.0f;
}
}
float SdfRoughnessStrength(const FVoxelStrateArchetypeParams& Params,
EVoxelStrateParamBlock Block)
{
switch (Block)
{
case EVoxelStrateParamBlock::TunnelNetwork: return Params.TunnelNetworkParams.SurfaceRoughness;
case EVoxelStrateParamBlock::Maze: return Params.MazeParams.SurfaceRoughness;
case EVoxelStrateParamBlock::VerticalShaft: return Params.VerticalShaftParams.SurfaceRoughness;
case EVoxelStrateParamBlock::FloatingIsland: return Params.FloatingIslandParams.SurfaceRoughness;
default: return 2.0f;
}
}
float SdfRoughnessFrequency(const FVoxelStrateArchetypeParams& Params,
EVoxelStrateParamBlock Block)
{
switch (Block)
{
case EVoxelStrateParamBlock::TunnelNetwork: return Params.TunnelNetworkParams.RoughnessFrequency;
case EVoxelStrateParamBlock::Maze: return 0.12f;
case EVoxelStrateParamBlock::VerticalShaft: return 0.1f;
case EVoxelStrateParamBlock::FloatingIsland: return 0.08f;
default: return 0.1f;
}
}
int32 SdfRoughnessOctaves(EVoxelStrateParamBlock Block)
{
return Block == EVoxelStrateParamBlock::FloatingIsland ? 4 : 3;
}
float SdfRoughnessWindow(const FVoxelStrateArchetypeParams& Params,
EVoxelStrateParamBlock Block)
{
const float Strength = SdfRoughnessStrength(Params, Block);
switch (Block)
{
case EVoxelStrateParamBlock::TunnelNetwork:
return Params.TunnelNetworkParams.SDFBlendRadius * 3.0f + Strength + 2.0f;
case EVoxelStrateParamBlock::Maze:
return FMath::Max(Params.MazeParams.CorridorRadius, 0.5f) + Strength + 2.0f;
case EVoxelStrateParamBlock::VerticalShaft:
return Strength + 4.0f;
case EVoxelStrateParamBlock::FloatingIsland:
return Strength + SdfBlend(Params, Block) + 2.0f;
default:
return Strength + 4.0f;
}
}
float GenericStrength(const FVoxelStrateArchetypeParams& Params,
EVoxelStrateParamBlock Block)
{
return FMath::Max(0.25f, 0.35f * FMath::Abs(BaseDensity(Params, Block)));
}
float GenericFrequency(const FVoxelStrateArchetypeParams& Params,
EVoxelStrateParamBlock Block)
{
switch (Block)
{
case EVoxelStrateParamBlock::Maze:
return 1.0f / FMath::Max(Params.MazeParams.CellSize, 8.0f);
case EVoxelStrateParamBlock::VerticalShaft:
return 0.01f;
case EVoxelStrateParamBlock::FloatingIsland:
return 0.02f;
case EVoxelStrateParamBlock::TunnelNetwork:
return FMath::Max(Params.TunnelNetworkParams.RoughnessFrequency, 0.001f);
default:
return 0.02f;
}
}
bool IsShapeSource(EVoxelStrateOpClass OpClass)
{
switch (OpClass)
{
case EVoxelStrateOpClass::RoomGraphSource:
case EVoxelStrateOpClass::LatticeCorridorSource:
case EVoxelStrateOpClass::ShaftFieldSource:
case EVoxelStrateOpClass::IslandBlobSource:
case EVoxelStrateOpClass::NoiseRibbonSource:
return true;
default:
return false;
}
}
bool ValidateRecipeContracts(const FVoxelOpStackRecipe& Recipe, FString& OutError)
{
TArray<EVoxelStrateOpClass> Classes;
Classes.Reserve(3 + Recipe.Modifiers.Num());
Classes.Add(Recipe.Root.OpClass);
Classes.Add(Recipe.ShapeSource.OpClass);
Classes.Add(Recipe.Conversion.OpClass);
for (const FVoxelOpRecipeEntry& Entry : Recipe.Modifiers)
{
Classes.Add(Entry.OpClass);
}
auto ValidateParamBlock = [&](const FVoxelOpRecipeEntry& Entry, int32 EntryIndex)
{
if (Entry.ParamBlock == EVoxelStrateParamBlock::None)
{
OutError = FString::Printf(TEXT("recipe op %d has no parameter block"), EntryIndex);
return false;
}
const EVoxelStrateParamBlock Fixed = FixedParamBlock(Entry.OpClass);
if (Fixed != EVoxelStrateParamBlock::None && Entry.ParamBlock != Fixed)
{
OutError = FString::Printf(TEXT("recipe op %d uses the wrong parameter block"), EntryIndex);
return false;
}
return true;
};
if (!ValidateParamBlock(Recipe.Root, 0)
|| !ValidateParamBlock(Recipe.ShapeSource, 1)
|| !ValidateParamBlock(Recipe.Conversion, 2))
{
return false;
}
for (int32 ModifierIndex = 0; ModifierIndex < Recipe.Modifiers.Num(); ++ModifierIndex)
{
if (!ValidateParamBlock(Recipe.Modifiers[ModifierIndex], 3 + ModifierIndex))
{
return false;
}
}
int32 LastWriter[2] = { INDEX_NONE, INDEX_NONE };
EVoxelOpResourceMask AvailableResources = VoxelOpResources::None;
const EVoxelOpChannelMask Channels[] = { VoxelOpChannels::Density, VoxelOpChannels::Sdf };
auto ChannelIndex = [](EVoxelOpChannelMask Channel) { return Channel == VoxelOpChannels::Density ? 0 : 1; };
for (int32 Index = 0; Index < Classes.Num(); ++Index)
{
FVoxelStrateOpContract Contract;
if (!VoxelDensityOps::GetStrateOpContract(Classes[Index], Contract))
{
OutError = FString::Printf(TEXT("recipe op %d has no declaration"), Index);
return false;
}
if ((Contract.Reads & VoxelOpChannels::All) != Contract.Reads
|| (Contract.Writes & VoxelOpChannels::All) != Contract.Writes
|| (Contract.RequiredResources & VoxelOpResources::All) != Contract.RequiredResources
|| (Contract.ProvidedResources & VoxelOpResources::All) != Contract.ProvidedResources)
{
OutError = FString::Printf(TEXT("recipe op %d has an unknown declaration bit"), Index);
return false;
}
if ((Contract.RequiredResources
& static_cast<EVoxelOpResourceMask>(~AvailableResources)) != 0)
{
OutError = FString::Printf(TEXT("recipe op %d requires unavailable state"), Index);
return false;
}
if (Contract.bAdditive && Contract.Writes == VoxelOpChannels::None)
{
OutError = FString::Printf(TEXT("recipe op %d is additive but writes no channel"), Index);
return false;
}
if (Contract.bAdditive
&& (Contract.Writes & static_cast<EVoxelOpChannelMask>(~Contract.Reads)) != 0)
{
OutError = FString::Printf(TEXT("recipe op %d is additive without reading its writes"), Index);
return false;
}
for (const EVoxelOpChannelMask Channel : Channels)
{
if ((Contract.Reads & Channel) == 0) { continue; }
const int32 CI = ChannelIndex(Channel);
const bool bRootIdentity = Contract.Role == EVoxelOpRole::FieldSource
&& (Contract.Writes & Channel) != 0 && LastWriter[CI] == INDEX_NONE;
if (LastWriter[CI] == INDEX_NONE && !bRootIdentity)
{
OutError = FString::Printf(TEXT("recipe op %d reads an unpublished channel"), Index);
return false;
}
}
for (const EVoxelOpChannelMask Channel : Channels)
{
if ((Contract.Writes & Channel) == 0 || (Contract.Reads & Channel) != 0) { continue; }
if (LastWriter[ChannelIndex(Channel)] != INDEX_NONE)
{
OutError = FString::Printf(TEXT("recipe op %d clobbers a channel"), Index);
return false;
}
}
for (const EVoxelOpChannelMask Channel : Channels)
{
if ((Contract.Writes & Channel) != 0)
{
LastWriter[ChannelIndex(Channel)] = Index;
}
}
AvailableResources |= Contract.ProvidedResources;
}
return true;
}
TUniquePtr<IVoxelDensityOp> BuildRecipeOp(const FVoxelOpRecipeEntry& Entry,
const FVoxelStrateArchetypeParams& Params,
int32 Seed,
float SpineRadius,
const UVoxelStrateManager* StrateManager,
const FRoomGraphSource*& OutRoom,
const FShaftFieldSource*& OutShaft)
{
const EVoxelStrateParamBlock Block = Entry.ParamBlock;
switch (Entry.OpClass)
{
case EVoxelStrateOpClass::ConstantRockSource:
return VoxelDensityOps::MakeConstantRockSource(BaseDensity(Params, Block));
case EVoxelStrateOpClass::ConstantVoidSource:
return VoxelDensityOps::MakeConstantVoidSource(BaseDensity(Params, Block));
case EVoxelStrateOpClass::RoomGraphSource:
{
TUniquePtr<FRoomGraphSource> Op = MakeUnique<FRoomGraphSource>(*Tunnel(Params), Seed, StrateManager);
OutRoom = Op.Get();
return Op;
}
case EVoxelStrateOpClass::LatticeCorridorSource:
return MakeUnique<FLatticeCorridorSource>(*Maze(Params), Seed);
case EVoxelStrateOpClass::ShaftFieldSource:
{
TUniquePtr<FShaftFieldSource> Op = MakeUnique<FShaftFieldSource>(*Shaft(Params), Seed, SpineRadius);
OutShaft = Op.Get();
return Op;
}
case EVoxelStrateOpClass::IslandBlobSource:
return MakeUnique<FIslandBlobSource>(*Island(Params), Seed);
case EVoxelStrateOpClass::NoiseRibbonSource:
return MakeUnique<FNoiseRibbonSource>(*Maze(Params), Seed);
case EVoxelStrateOpClass::SdfRoughnessMod:
return VoxelDensityOps::MakeSdfRoughnessMod(
SdfRoughnessStrength(Params, Block),
SdfRoughnessFrequency(Params, Block),
SdfRoughnessOctaves(Block),
SdfRoughnessWindow(Params, Block));
case EVoxelStrateOpClass::SdfCarve:
return VoxelDensityOps::MakeSdfCarve(SdfBlend(Params, Block), BaseDensity(Params, Block));
case EVoxelStrateOpClass::SdfFill:
return VoxelDensityOps::MakeSdfFill(SdfBlend(Params, Block), BaseDensity(Params, Block));
case EVoxelStrateOpClass::GridColumnMod:
return MakeUnique<FGridColumnMod>(*Slab(Params), Seed);
case EVoxelStrateOpClass::CaveRoughnessMod:
return MakeUnique<FCaveRoughnessMod>(*Tunnel(Params), Seed);
case EVoxelStrateOpClass::CaveTerraceMod:
return MakeUnique<FCaveTerraceMod>(*Tunnel(Params), Seed, OutRoom);
case EVoxelStrateOpClass::LayerLineMod:
return MakeUnique<FLayerLineMod>(*Tunnel(Params), OutRoom);
case EVoxelStrateOpClass::RibbingMod:
return MakeUnique<FRibbingMod>(*Tunnel(Params), OutRoom);
case EVoxelStrateOpClass::CaveOverhangMod:
return MakeUnique<FCaveOverhangMod>(*Tunnel(Params), Seed, OutRoom);
case EVoxelStrateOpClass::CaveCliffMod:
return MakeUnique<FCaveCliffMod>(*Tunnel(Params), Seed, OutRoom);
case EVoxelStrateOpClass::ScallopMod:
return MakeUnique<FScallopMod>(*Tunnel(Params), Seed, OutRoom);
case EVoxelStrateOpClass::CaveArchMod:
return MakeUnique<FCaveArchMod>(*Tunnel(Params), OutRoom);
case EVoxelStrateOpClass::RoomColumnMod:
return MakeUnique<FRoomColumnMod>(*Tunnel(Params), OutRoom);
case EVoxelStrateOpClass::DomeMod:
return MakeUnique<FDomeMod>(*Tunnel(Params), OutRoom);
case EVoxelStrateOpClass::PinchMod:
return MakeUnique<FPinchMod>(*Tunnel(Params), OutRoom);
case EVoxelStrateOpClass::FloorBiasMod:
return MakeUnique<FFloorBiasMod>(*Tunnel(Params), OutRoom);
case EVoxelStrateOpClass::WormFieldSource:
return MakeUnique<FWormFieldSource>(*Tunnel(Params), Seed);
case EVoxelStrateOpClass::ShaftLedgeMod:
return MakeUnique<FShaftLedgeMod>(*Shaft(Params), OutShaft);
case EVoxelStrateOpClass::DensityNoiseCarveMod:
return MakeUnique<FDensityNoiseMod>(GenericStrength(Params, Block),
GenericFrequency(Params, Block), 3, Seed, false);
case EVoxelStrateOpClass::DensityNoiseFillMod:
return MakeUnique<FDensityNoiseMod>(GenericStrength(Params, Block),
GenericFrequency(Params, Block), 3, Seed, true);
default:
return nullptr;
}
}
}
bool VF_BuildStackFromRecipe(const FVoxelOpStackRecipe& Recipe,
const FVoxelStrateArchetypeParams& Params,
int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager,
FVoxelOpStack& OutStack,
FVoxelOpContext& OutContext,
FString* OutError)
{
auto Fail = [&](const FString& Reason) -> bool
{
if (OutError != nullptr) { *OutError = Reason; }
return false;
};
if (OutError != nullptr) { OutError->Reset(); }
if (Recipe.Modifiers.Num() < 4 || Recipe.Modifiers.Num() > 8)
{
return Fail(TEXT("A structure recipe must draw between 4 and 8 modifiers."));
}
const EVoxelStrateOpClass ExpectedRoot = Recipe.RootPolarity == EVoxelStrateRootPolarity::VoidFill
? EVoxelStrateOpClass::ConstantVoidSource : EVoxelStrateOpClass::ConstantRockSource;
const EVoxelStrateOpClass ExpectedConversion = Recipe.RootPolarity == EVoxelStrateRootPolarity::VoidFill
? EVoxelStrateOpClass::SdfFill : EVoxelStrateOpClass::SdfCarve;
if (Recipe.Root.OpClass != ExpectedRoot || Recipe.Conversion.OpClass != ExpectedConversion)
{
return Fail(TEXT("Recipe polarity does not match its root/conversion ids."));
}
if (!VoxelStrateRecipePrivate::IsShapeSource(Recipe.ShapeSource.OpClass))
{
return Fail(TEXT("Recipe shape source is not a rollable SDF source."));
}
const EVoxelStrateParamBlock ShapeBlock =
VoxelStrateRecipePrivate::ShapeBlock(Recipe.ShapeSource.OpClass);
if (ShapeBlock == EVoxelStrateParamBlock::None
|| Recipe.StructuralParamBlock != ShapeBlock
|| Recipe.Root.ParamBlock == EVoxelStrateParamBlock::None
|| Recipe.ShapeSource.ParamBlock != ShapeBlock
|| Recipe.Conversion.ParamBlock != ShapeBlock)
{
return Fail(TEXT("Recipe parameter blocks do not match its shape source."));
}
FString ContractError;
if (!VoxelStrateRecipePrivate::ValidateRecipeContracts(Recipe, ContractError))
{
return Fail(ContractError);
}
FVoxelOpStack Candidate;
const FRoomGraphSource* RoomPtr = nullptr;
const FShaftFieldSource* ShaftPtr = nullptr;
FVoxelOpRecipeEntry Root = Recipe.Root;
TUniquePtr<IVoxelDensityOp> RootOp =
VoxelStrateRecipePrivate::BuildRecipeOp(Root, Params, Seed, SpineRadius, StrateManager, RoomPtr, ShaftPtr);
if (!RootOp) { return Fail(TEXT("Recipe root could not be materialised.")); }
Candidate.Add(MoveTemp(RootOp));
TUniquePtr<IVoxelDensityOp> ShapeOp =
VoxelStrateRecipePrivate::BuildRecipeOp(Recipe.ShapeSource, Params, Seed, SpineRadius,
StrateManager, RoomPtr, ShaftPtr);
if (!ShapeOp) { return Fail(TEXT("Recipe shape source could not be materialised.")); }
Candidate.Add(MoveTemp(ShapeOp));
TUniquePtr<IVoxelDensityOp> ConversionOp =
VoxelStrateRecipePrivate::BuildRecipeOp(Recipe.Conversion, Params, Seed, SpineRadius,
StrateManager, RoomPtr, ShaftPtr);
if (!ConversionOp) { return Fail(TEXT("Recipe conversion could not be materialised.")); }
Candidate.Add(MoveTemp(ConversionOp));
for (const FVoxelOpRecipeEntry& Entry : Recipe.Modifiers)
{
TUniquePtr<IVoxelDensityOp> Modifier =
VoxelStrateRecipePrivate::BuildRecipeOp(Entry, Params, Seed, SpineRadius,
StrateManager, RoomPtr, ShaftPtr);
if (!Modifier)
{
return Fail(TEXT("Recipe modifier could not be materialised."));
}
Candidate.Add(MoveTemp(Modifier));
}
const float Top = VoxelStrateRecipePrivate::Top(Params, Recipe.StructuralParamBlock);
const float Bottom = VoxelStrateRecipePrivate::Bottom(Params, Recipe.StructuralParamBlock);
const float Seal = VoxelStrateRecipePrivate::BoundarySeal(Params, Recipe.StructuralParamBlock);
const float Base = VoxelStrateRecipePrivate::BaseDensity(Params, Recipe.StructuralParamBlock);
if (!FMath::IsFinite(Top) || !FMath::IsFinite(Bottom) || Top <= Bottom
|| !FMath::IsFinite(Seal) || !FMath::IsFinite(Base))
{
return Fail(TEXT("Recipe structural parameters do not define a finite positive strate."));
}
OutContext = FVoxelOpContext();
OutContext.Seed = Seed;
OutContext.LayoutVersion = StrateManager != nullptr ? StrateManager->GetLayoutVersion() : 0;
OutContext.WorldRadiusVoxels = 0.0f;
OutContext.EdgeSealThickness = Seal;
OutContext.StrateTopWorldZ = Top;
OutContext.StrateBottomWorldZ = Bottom;
// This is the only place a recipe can acquire world-law operators. There is no recipe field
// for posts, so a serialised manifest cannot forget them or reorder them.
Candidate.AppendStructuralPost(Top, Bottom, Seal, Base, SpineRadius, StrateManager);
FString ValidationError;
if (!Candidate.ValidateChannelOrder(&ValidationError))
{
return Fail(FString::Printf(TEXT("Materialised recipe failed ValidateChannelOrder: %s"),
*ValidationError));
}
OutStack = MoveTemp(Candidate);
return true;
}
@@ -13,6 +13,7 @@
#include "VoxelSettings.h"
#include "VoxelStrateDefinition.h"
#include "VoxelDensityOpStack.h"
#include <type_traits>
#include <utility>
@@ -992,7 +993,8 @@ namespace
}
FVoxelStrateRollInfo VF_RollInternal(const FVoxelStrateCorpus& Corpus,
int32 Seed, int32 Index)
int32 Seed, int32 Index,
const ECaveGeneratorType* ForcedArchetype = nullptr)
{
FVoxelStrateRollInfo Result;
if (!Corpus.IsValid())
@@ -1043,7 +1045,24 @@ namespace
}
int32 GroupIndex = 0;
if (TotalGroupWeight > 0.0f)
if (ForcedArchetype != nullptr)
{
GroupIndex = INDEX_NONE;
for (int32 CandidateGroup = 0; CandidateGroup < Groups.Num(); ++CandidateGroup)
{
if (Groups[CandidateGroup].Archetype == *ForcedArchetype)
{
GroupIndex = CandidateGroup;
break;
}
}
if (GroupIndex == INDEX_NONE)
{
Result.FailureReason = TEXT("The requested archetype has no corpus group.");
return Result;
}
}
else if (TotalGroupWeight > 0.0f)
{
float Cursor = Rng.FRand() * TotalGroupWeight;
for (int32 CandidateGroup = 0; CandidateGroup < Groups.Num(); ++CandidateGroup)
@@ -1141,6 +1160,165 @@ namespace
Result.bValid = true;
return Result;
}
struct FStructureModifierCandidate
{
EVoxelStrateOpClass OpClass;
EVoxelStrateParamBlock ParamBlock;
};
static const EVoxelStrateOpClass GStructureShapeSources[] =
{
EVoxelStrateOpClass::RoomGraphSource,
EVoxelStrateOpClass::LatticeCorridorSource,
EVoxelStrateOpClass::ShaftFieldSource,
EVoxelStrateOpClass::IslandBlobSource,
EVoxelStrateOpClass::NoiseRibbonSource,
};
// Fixed order is intentional. This is a source-controlled catalogue, not a hash-container
// iteration; changing it is a manifest-versioned design change rather than an accidental RNG
// change.
static const FStructureModifierCandidate GStructureModifiers[] =
{
{ EVoxelStrateOpClass::SdfRoughnessMod, EVoxelStrateParamBlock::None },
{ EVoxelStrateOpClass::SdfCarve, EVoxelStrateParamBlock::None },
{ EVoxelStrateOpClass::SdfFill, EVoxelStrateParamBlock::None },
{ EVoxelStrateOpClass::CaveRoughnessMod, EVoxelStrateParamBlock::TunnelNetwork },
{ EVoxelStrateOpClass::WormFieldSource, EVoxelStrateParamBlock::TunnelNetwork },
{ EVoxelStrateOpClass::GridColumnMod, EVoxelStrateParamBlock::Slab },
{ EVoxelStrateOpClass::DensityNoiseCarveMod, EVoxelStrateParamBlock::None },
{ EVoxelStrateOpClass::DensityNoiseFillMod, EVoxelStrateParamBlock::None },
{ EVoxelStrateOpClass::CaveTerraceMod, EVoxelStrateParamBlock::TunnelNetwork },
{ EVoxelStrateOpClass::LayerLineMod, EVoxelStrateParamBlock::TunnelNetwork },
{ EVoxelStrateOpClass::RibbingMod, EVoxelStrateParamBlock::TunnelNetwork },
{ EVoxelStrateOpClass::CaveOverhangMod, EVoxelStrateParamBlock::TunnelNetwork },
{ EVoxelStrateOpClass::CaveCliffMod, EVoxelStrateParamBlock::TunnelNetwork },
{ EVoxelStrateOpClass::ScallopMod, EVoxelStrateParamBlock::TunnelNetwork },
{ EVoxelStrateOpClass::CaveArchMod, EVoxelStrateParamBlock::TunnelNetwork },
{ EVoxelStrateOpClass::RoomColumnMod, EVoxelStrateParamBlock::TunnelNetwork },
{ EVoxelStrateOpClass::DomeMod, EVoxelStrateParamBlock::TunnelNetwork },
{ EVoxelStrateOpClass::PinchMod, EVoxelStrateParamBlock::TunnelNetwork },
{ EVoxelStrateOpClass::FloorBiasMod, EVoxelStrateParamBlock::TunnelNetwork },
{ EVoxelStrateOpClass::ShaftLedgeMod, EVoxelStrateParamBlock::VerticalShaft },
};
uint32 VF_StructureRollSeed(int32 Seed, int32 Index)
{
uint32 Value = VF_Avalanche(static_cast<uint32>(Seed) ^ 0xD1CEB00Bu);
Value = VF_Avalanche(Value ^ (static_cast<uint32>(Index) + 0x7F4A7C15u));
return Value;
}
bool VF_IsContractLegalAfter(EVoxelOpChannelMask AvailableChannels,
EVoxelOpResourceMask AvailableResources,
const FVoxelStrateOpContract& Contract)
{
if ((Contract.Reads & static_cast<EVoxelOpChannelMask>(~AvailableChannels)) != 0
|| (Contract.RequiredResources
& static_cast<EVoxelOpResourceMask>(~AvailableResources)) != 0)
{
return false;
}
if (Contract.bAdditive
&& (Contract.Writes == VoxelOpChannels::None
|| (Contract.Writes & static_cast<EVoxelOpChannelMask>(~Contract.Reads)) != 0))
{
return false;
}
for (const EVoxelOpChannelMask Channel :
{ VoxelOpChannels::Density, VoxelOpChannels::Sdf })
{
if ((Contract.Writes & Channel) == 0 || (Contract.Reads & Channel) != 0)
{
continue;
}
// All channels are already published by root + shape. A write-only candidate would
// therefore be a replacement clobber and is excluded before it can enter a recipe.
return false;
}
return true;
}
FVoxelOpRecipeEntry VF_MakeRecipeEntry(EVoxelStrateOpClass OpClass,
EVoxelStrateParamBlock ParamBlock)
{
FVoxelOpRecipeEntry Entry;
Entry.OpClass = OpClass;
Entry.ParamBlock = ParamBlock;
return Entry;
}
FVoxelOpStackRecipe VF_RollStructureInternal(int32 Seed, int32 Index)
{
FVoxelOpStackRecipe Recipe;
FRandomStream Rng(static_cast<int32>(VF_StructureRollSeed(Seed, Index)));
Recipe.RootPolarity = Rng.RandRange(0, 1) == 0
? EVoxelStrateRootPolarity::RockCarve
: EVoxelStrateRootPolarity::VoidFill;
const EVoxelStrateOpClass ShapeClass = GStructureShapeSources[
Rng.RandRange(0, UE_ARRAY_COUNT(GStructureShapeSources) - 1)];
const EVoxelStrateParamBlock ShapeBlock =
ShapeClass == EVoxelStrateOpClass::RoomGraphSource
? EVoxelStrateParamBlock::TunnelNetwork
: ShapeClass == EVoxelStrateOpClass::ShaftFieldSource
? EVoxelStrateParamBlock::VerticalShaft
: ShapeClass == EVoxelStrateOpClass::IslandBlobSource
? EVoxelStrateParamBlock::FloatingIsland
: EVoxelStrateParamBlock::Maze;
Recipe.Root = VF_MakeRecipeEntry(
Recipe.RootPolarity == EVoxelStrateRootPolarity::VoidFill
? EVoxelStrateOpClass::ConstantVoidSource
: EVoxelStrateOpClass::ConstantRockSource,
ShapeBlock);
Recipe.ShapeSource = VF_MakeRecipeEntry(ShapeClass, ShapeBlock);
Recipe.Conversion = VF_MakeRecipeEntry(
Recipe.RootPolarity == EVoxelStrateRootPolarity::VoidFill
? EVoxelStrateOpClass::SdfFill
: EVoxelStrateOpClass::SdfCarve,
ShapeBlock);
Recipe.StructuralParamBlock = ShapeBlock;
FVoxelStrateOpContract ShapeContract;
const bool bShapeDeclared = VoxelDensityOps::GetStrateOpContract(ShapeClass, ShapeContract);
checkf(bShapeDeclared, TEXT("Structure catalogue contains an undeclared shape source."));
const EVoxelOpChannelMask AvailableChannels = VoxelOpChannels::Density | VoxelOpChannels::Sdf;
const EVoxelOpResourceMask AvailableResources = ShapeContract.ProvidedResources;
TArray<FStructureModifierCandidate> Legal;
for (const FStructureModifierCandidate& Candidate : GStructureModifiers)
{
FVoxelStrateOpContract Contract;
if (!VoxelDensityOps::GetStrateOpContract(Candidate.OpClass, Contract))
{
checkf(false, TEXT("Structure catalogue contains an undeclared modifier."));
continue;
}
if (VF_IsContractLegalAfter(AvailableChannels, AvailableResources, Contract))
{
Legal.Add(Candidate);
}
}
const int32 K = Rng.RandRange(4, 8);
checkf(Legal.Num() >= K,
TEXT("Structure roller has fewer legal modifiers than its rolled k."));
for (int32 ModifierIndex = 0; ModifierIndex < K && Legal.Num() > 0; ++ModifierIndex)
{
const int32 Pick = Rng.RandRange(0, Legal.Num() - 1);
const FStructureModifierCandidate Candidate = Legal[Pick];
Recipe.Modifiers.Add(VF_MakeRecipeEntry(
Candidate.OpClass,
Candidate.ParamBlock == EVoxelStrateParamBlock::None
? ShapeBlock : Candidate.ParamBlock));
Legal.RemoveAt(Pick, 1, EAllowShrinking::No);
}
return Recipe;
}
}
const TArray<FVoxelStrateFieldExclusion>& FVoxelStrateCorpus::GetNonTunableFields()
@@ -1849,8 +2027,164 @@ FVoxelStrateRollInfo VF_RollStrateParamsDetailed(const FVoxelStrateCorpus& Corpu
return Result;
}
FVoxelStrateRollInfo VF_RollStrateParamsDetailedForArchetype(
const FVoxelStrateCorpus& Corpus, ECaveGeneratorType Archetype, int32 Seed, int32 Index)
{
const FVoxelStrateRollInfo Result = VF_RollInternal(Corpus, Seed, Index, &Archetype);
#if DO_CHECK
if (Result.bValid)
{
const FVoxelStrateRollInfo Repeat = VF_RollInternal(Corpus, Seed, Index, &Archetype);
checkf(Repeat.bValid && Repeat.Archetype == Result.Archetype
&& VF_AreStrateArchetypeParamsBitIdentical(
Repeat.ArchetypeParams, Result.ArchetypeParams, Result.Archetype)
&& Repeat.ParentEntryIndices == Result.ParentEntryIndices
&& Repeat.ParentWeights == Result.ParentWeights,
TEXT("VF_RollStrateParamsDetailedForArchetype lost determinism."));
}
#endif
return Result;
}
FStrateGenerationParams VF_RollStrateParams(const FVoxelStrateCorpus& Corpus,
int32 Seed, int32 Index)
{
return VF_RollStrateParamsDetailed(Corpus, Seed, Index).Params;
}
namespace
{
const TCHAR* VF_RecipeOpCode(EVoxelStrateOpClass OpClass)
{
switch (OpClass)
{
case EVoxelStrateOpClass::ConstantRockSource: return TEXT("rk");
case EVoxelStrateOpClass::ConstantVoidSource: return TEXT("vd");
case EVoxelStrateOpClass::RoomGraphSource: return TEXT("rg");
case EVoxelStrateOpClass::LatticeCorridorSource: return TEXT("lt");
case EVoxelStrateOpClass::ShaftFieldSource: return TEXT("sh");
case EVoxelStrateOpClass::IslandBlobSource: return TEXT("is");
case EVoxelStrateOpClass::NoiseRibbonSource: return TEXT("nr");
case EVoxelStrateOpClass::SdfRoughnessMod: return TEXT("sr");
case EVoxelStrateOpClass::SdfCarve: return TEXT("cv");
case EVoxelStrateOpClass::SdfFill: return TEXT("fl");
case EVoxelStrateOpClass::GridColumnMod: return TEXT("gc");
case EVoxelStrateOpClass::CaveRoughnessMod: return TEXT("cr");
case EVoxelStrateOpClass::CaveTerraceMod: return TEXT("tr");
case EVoxelStrateOpClass::LayerLineMod: return TEXT("ll");
case EVoxelStrateOpClass::RibbingMod: return TEXT("rb");
case EVoxelStrateOpClass::CaveOverhangMod: return TEXT("co");
case EVoxelStrateOpClass::CaveCliffMod: return TEXT("cc");
case EVoxelStrateOpClass::ScallopMod: return TEXT("sc");
case EVoxelStrateOpClass::CaveArchMod: return TEXT("ar");
case EVoxelStrateOpClass::RoomColumnMod: return TEXT("rc");
case EVoxelStrateOpClass::DomeMod: return TEXT("dm");
case EVoxelStrateOpClass::PinchMod: return TEXT("pn");
case EVoxelStrateOpClass::FloorBiasMod: return TEXT("fb");
case EVoxelStrateOpClass::WormFieldSource: return TEXT("wm");
case EVoxelStrateOpClass::ShaftLedgeMod: return TEXT("sl");
case EVoxelStrateOpClass::DensityNoiseCarveMod: return TEXT("nc");
case EVoxelStrateOpClass::DensityNoiseFillMod: return TEXT("nf");
default: return TEXT("??");
}
}
const TCHAR* VF_RecipeBlockCode(EVoxelStrateParamBlock Block)
{
switch (Block)
{
case EVoxelStrateParamBlock::TunnelNetwork: return TEXT("T");
case EVoxelStrateParamBlock::Slab: return TEXT("B");
case EVoxelStrateParamBlock::Maze: return TEXT("M");
case EVoxelStrateParamBlock::Surface: return TEXT("S");
case EVoxelStrateParamBlock::VerticalShaft: return TEXT("V");
case EVoxelStrateParamBlock::FloatingIsland: return TEXT("I");
default: return TEXT("-");
}
}
FString VF_FormatRecipeEntry(const FVoxelOpRecipeEntry& Entry)
{
return FString::Printf(TEXT("%s/%s"), VF_RecipeOpCode(Entry.OpClass),
VF_RecipeBlockCode(Entry.ParamBlock));
}
}
FVoxelOpStackRecipe VF_RollStrateStructure(int32 Seed, int32 Index)
{
const FVoxelOpStackRecipe Recipe = VF_RollStructureInternal(Seed, Index);
#if DO_CHECK
const FVoxelOpStackRecipe Repeat = VF_RollStructureInternal(Seed, Index);
checkf(VF_AreStrateStructureRecipesIdentical(Recipe, Repeat),
TEXT("VF_RollStrateStructure lost determinism for the same seed/index."));
#endif
return Recipe;
}
FString VF_FormatStrateStructureRecipe(const FVoxelOpStackRecipe& Recipe)
{
FString Modifiers;
for (int32 Index = 0; Index < Recipe.Modifiers.Num(); ++Index)
{
if (Index > 0) { Modifiers += TEXT(","); }
Modifiers += VF_FormatRecipeEntry(Recipe.Modifiers[Index]);
}
return FString::Printf(TEXT("%s|r=%s|s=%s|x=%s|m=[%s]|posts=%s"),
Recipe.RootPolarity == EVoxelStrateRootPolarity::VoidFill ? TEXT("VF") : TEXT("RC"),
*VF_FormatRecipeEntry(Recipe.Root),
*VF_FormatRecipeEntry(Recipe.ShapeSource),
*VF_FormatRecipeEntry(Recipe.Conversion),
*Modifiers,
VF_RecipeBlockCode(Recipe.StructuralParamBlock));
}
uint32 VF_HashStrateStructureRecipe(const FVoxelOpStackRecipe& Recipe)
{
uint32 Hash = 2166136261u;
auto AddByte = [&Hash](uint8 Value)
{
Hash ^= static_cast<uint32>(Value);
Hash *= 16777619u;
};
auto AddEntry = [&AddByte](const FVoxelOpRecipeEntry& Entry)
{
AddByte(static_cast<uint8>(Entry.OpClass));
AddByte(static_cast<uint8>(Entry.ParamBlock));
};
AddByte(static_cast<uint8>(Recipe.RootPolarity));
AddEntry(Recipe.Root);
AddEntry(Recipe.ShapeSource);
AddEntry(Recipe.Conversion);
AddByte(static_cast<uint8>(Recipe.Modifiers.Num()));
for (const FVoxelOpRecipeEntry& Entry : Recipe.Modifiers) { AddEntry(Entry); }
AddByte(static_cast<uint8>(Recipe.StructuralParamBlock));
return Hash;
}
bool VF_AreStrateStructureRecipesIdentical(const FVoxelOpStackRecipe& A,
const FVoxelOpStackRecipe& B)
{
if (A.RootPolarity != B.RootPolarity
|| A.Root.OpClass != B.Root.OpClass || A.Root.ParamBlock != B.Root.ParamBlock
|| A.ShapeSource.OpClass != B.ShapeSource.OpClass
|| A.ShapeSource.ParamBlock != B.ShapeSource.ParamBlock
|| A.Conversion.OpClass != B.Conversion.OpClass
|| A.Conversion.ParamBlock != B.Conversion.ParamBlock
|| A.StructuralParamBlock != B.StructuralParamBlock
|| A.Modifiers.Num() != B.Modifiers.Num())
{
return false;
}
for (int32 Index = 0; Index < A.Modifiers.Num(); ++Index)
{
if (A.Modifiers[Index].OpClass != B.Modifiers[Index].OpClass
|| A.Modifiers[Index].ParamBlock != B.Modifiers[Index].ParamBlock)
{
return false;
}
}
return true;
}
+176 -42
View File
@@ -75,23 +75,28 @@ namespace VoxelStrateMeasurePrivate
}
bool BuildSampleGrid(
const UVoxelGenerator& Generator,
const UVoxelStrateManager& Manager,
const UVoxelGenerator* Generator,
const UVoxelStrateManager* Manager,
const IVoxelStrateDensitySampler* Sampler,
int32 StrateIndex,
int32 ExplicitBottomWorldZ,
int32 ExplicitTopWorldZ,
float ExplicitBoundarySealThickness,
bool bUseExplicitBounds,
const FVoxelStrateMeasureSettings& Settings,
FSampleGrid& OutGrid,
FString& OutReason)
{
const TArray<FStrateSlot>& Layout = Manager.GetLayout();
if (!Layout.IsValidIndex(StrateIndex))
if (bUseExplicitBounds)
{
return Refuse(OutReason, TEXT("StrateIndex is outside the manager layout."));
if (Sampler == nullptr)
{
return Refuse(OutReason, TEXT("An explicit measurement window requires a density sampler."));
}
}
const FStrateSlot& Slot = Layout[StrateIndex];
if (Slot.Definition == nullptr)
else if (Generator == nullptr || Manager == nullptr)
{
return Refuse(OutReason, TEXT("The requested strate has no definition."));
return Refuse(OutReason, TEXT("A generator and manager are required for a layout measurement."));
}
if (Settings.SampleStep <= 0)
{
@@ -136,13 +141,37 @@ namespace VoxelStrateMeasurePrivate
return Refuse(OutReason, TEXT("CoverMarginVoxels must be finite and non-negative."));
}
}
if (Slot.TopChunkZ <= Slot.BottomChunkZ)
int64 StrateBottomZ = 0;
int64 StrateTopZ = 0;
if (bUseExplicitBounds)
{
return Refuse(OutReason, TEXT("The requested strate has no positive vertical extent."));
StrateBottomZ = static_cast<int64>(ExplicitBottomWorldZ);
StrateTopZ = static_cast<int64>(ExplicitTopWorldZ);
if (StrateTopZ <= StrateBottomZ)
{
return Refuse(OutReason, TEXT("The explicit strate has no positive vertical extent."));
}
}
else
{
const TArray<FStrateSlot>& Layout = Manager->GetLayout();
if (!Layout.IsValidIndex(StrateIndex))
{
return Refuse(OutReason, TEXT("StrateIndex is outside the manager layout."));
}
const int64 StrateBottomZ = static_cast<int64>(Slot.BottomChunkZ) * CHUNK_SIZE;
const int64 StrateTopZ = (static_cast<int64>(Slot.TopChunkZ) + 1) * CHUNK_SIZE;
const FStrateSlot& Slot = Layout[StrateIndex];
if (Slot.Definition == nullptr)
{
return Refuse(OutReason, TEXT("The requested strate has no definition."));
}
if (Slot.TopChunkZ <= Slot.BottomChunkZ)
{
return Refuse(OutReason, TEXT("The requested strate has no positive vertical extent."));
}
StrateBottomZ = static_cast<int64>(Slot.BottomChunkZ) * CHUNK_SIZE;
StrateTopZ = (static_cast<int64>(Slot.TopChunkZ) + 1) * CHUNK_SIZE;
}
const int64 StrateHeight = StrateTopZ - StrateBottomZ;
if (StrateHeight <= 0)
{
@@ -192,14 +221,23 @@ namespace VoxelStrateMeasurePrivate
return Refuse(OutReason, TEXT("The requested XY window is not a finite positive voxel box."));
}
const int32 MidChunkZ = Slot.BottomChunkZ
+ (Slot.TopChunkZ - Slot.BottomChunkZ) / 2;
const FIntVector RepresentativeChunk(
FMath::FloorToInt(WindowCenter.X / static_cast<float>(CHUNK_SIZE)),
FMath::FloorToInt(WindowCenter.Y / static_cast<float>(CHUNK_SIZE)),
MidChunkZ);
const FStrateGenerationParams RepresentativeParams =
Manager.GetGenerationParams(RepresentativeChunk);
FStrateGenerationParams RepresentativeParams;
if (bUseExplicitBounds)
{
RepresentativeParams.BoundarySealThickness = ExplicitBoundarySealThickness;
}
else
{
const TArray<FStrateSlot>& Layout = Manager->GetLayout();
const FStrateSlot& Slot = Layout[StrateIndex];
const int32 MidChunkZ = Slot.BottomChunkZ
+ (Slot.TopChunkZ - Slot.BottomChunkZ) / 2;
const FIntVector RepresentativeChunk(
FMath::FloorToInt(WindowCenter.X / static_cast<float>(CHUNK_SIZE)),
FMath::FloorToInt(WindowCenter.Y / static_cast<float>(CHUNK_SIZE)),
MidChunkZ);
RepresentativeParams = Manager->GetGenerationParams(RepresentativeChunk);
}
const int64 MaxMargin64 = FMath::Max<int64>(1, StrateHeight / 4);
const int32 MaxMargin = static_cast<int32>(
@@ -328,7 +366,9 @@ namespace VoxelStrateMeasurePrivate
for (int32 X = 0; X < OutGrid.NumX; ++X)
{
const FVector Sample = OutGrid.CellCenter(X, Y, Z);
const float Density = Generator.GetDensityAt(Sample.X, Sample.Y, Sample.Z);
const float Density = Sampler != nullptr
? Sampler->SampleDensity(Sample.X, Sample.Y, Sample.Z)
: Generator->GetDensityAt(Sample.X, Sample.Y, Sample.Z);
if (!FMath::IsFinite(Density))
{
bSawNonFiniteDensity = true;
@@ -772,7 +812,8 @@ namespace VoxelStrateMeasurePrivate
}
bool FullResolutionAirOnSegment(
const UVoxelGenerator& Generator,
const UVoxelGenerator* Generator,
const IVoxelStrateDensitySampler* Sampler,
const FVector& Start,
const FVector& End)
{
@@ -784,7 +825,9 @@ namespace VoxelStrateMeasurePrivate
{
const float Alpha = static_cast<float>(Step) / static_cast<float>(NumSteps);
const FVector Sample = Start + Delta * Alpha;
const float Density = Generator.GetDensityAt(Sample.X, Sample.Y, Sample.Z);
const float Density = Sampler != nullptr
? Sampler->SampleDensity(Sample.X, Sample.Y, Sample.Z)
: Generator->GetDensityAt(Sample.X, Sample.Y, Sample.Z);
if (!FMath::IsFinite(Density) || !(Density > 0.0f))
{
return false;
@@ -794,7 +837,8 @@ namespace VoxelStrateMeasurePrivate
}
bool FullResolutionPathIsAir(
const UVoxelGenerator& Generator,
const UVoxelGenerator* Generator,
const IVoxelStrateDensitySampler* Sampler,
const FSampleGrid& Grid,
const FVector& A,
const FVector& B,
@@ -812,7 +856,7 @@ namespace VoxelStrateMeasurePrivate
int32 EndZ = 0;
DecodeIndex(Grid, Path.Last(), EndX, EndY, EndZ);
if (!FullResolutionAirOnSegment(Generator, A, Grid.CellCenter(StartX, StartY, StartZ)))
if (!FullResolutionAirOnSegment(Generator, Sampler, A, Grid.CellCenter(StartX, StartY, StartZ)))
{
// The endpoint-to-cell-center segment has no coarse cell pair to exclude. It is
// common to every route from this endpoint, so report unknown rather than claiming
@@ -831,7 +875,7 @@ namespace VoxelStrateMeasurePrivate
int32 CurrentZ = 0;
DecodeIndex(Grid, Path[PathIndex], CurrentX, CurrentY, CurrentZ);
if (!FullResolutionAirOnSegment(
Generator,
Generator, Sampler,
Grid.CellCenter(PreviousX, PreviousY, PreviousZ),
Grid.CellCenter(CurrentX, CurrentY, CurrentZ)))
{
@@ -840,7 +884,7 @@ namespace VoxelStrateMeasurePrivate
}
}
if (!FullResolutionAirOnSegment(Generator, Grid.CellCenter(EndX, EndY, EndZ), B))
if (!FullResolutionAirOnSegment(Generator, Sampler, Grid.CellCenter(EndX, EndY, EndZ), B))
{
// As above, the final endpoint segment is shared by every route that reaches the
// goal cell and therefore has no precise coarse edge to exclude.
@@ -930,7 +974,8 @@ namespace VoxelStrateMeasurePrivate
}
EVoxelConnectivityResult EvaluateConnectivityOnGrid(
const UVoxelGenerator& Generator,
const UVoxelGenerator* Generator,
const IVoxelStrateDensitySampler* Sampler,
const FSampleGrid& Grid,
const FVector& AVoxel,
const FVector& BVoxel,
@@ -980,7 +1025,7 @@ namespace VoxelStrateMeasurePrivate
// spacing in the original voxel field. A single solid sample refutes only this
// route, so exclude its precise coarse edge and search again.
FCoarseEdge FailedEdge;
if (FullResolutionPathIsAir(Generator, Grid, AVoxel, BVoxel, Path, FailedEdge))
if (FullResolutionPathIsAir(Generator, Sampler, Grid, AVoxel, BVoxel, Path, FailedEdge))
{
return EVoxelConnectivityResult::Connected;
}
@@ -1126,9 +1171,14 @@ namespace VoxelStrateMeasurePrivate
}
EVoxelConnectivityResult QueryConnectivity(
const UVoxelGenerator& Generator,
const UVoxelStrateManager& Manager,
const UVoxelGenerator* Generator,
const UVoxelStrateManager* Manager,
const IVoxelStrateDensitySampler* Sampler,
int32 StrateIndex,
int32 ExplicitBottomWorldZ,
int32 ExplicitTopWorldZ,
float ExplicitBoundarySealThickness,
bool bUseExplicitBounds,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
@@ -1161,7 +1211,10 @@ namespace VoxelStrateMeasurePrivate
FSampleGrid Grid;
FString RefusalReason;
if (!BuildSampleGrid(Generator, Manager, StrateIndex, Settings, Grid, RefusalReason))
if (!BuildSampleGrid(Generator, Manager, Sampler, StrateIndex,
ExplicitBottomWorldZ, ExplicitTopWorldZ,
ExplicitBoundarySealThickness, bUseExplicitBounds,
Settings, Grid, RefusalReason))
{
if (OutDiagnostics != nullptr)
{
@@ -1174,6 +1227,7 @@ namespace VoxelStrateMeasurePrivate
int32 Goal = -1;
const EVoxelConnectivityResult Result = EvaluateConnectivityOnGrid(
Generator,
Sampler,
Grid,
AVoxel,
BVoxel,
@@ -1208,7 +1262,43 @@ FVoxelStrateMetrics VF_MeasureStrate(
FVoxelStrateMetrics Result;
VoxelStrateMeasurePrivate::FSampleGrid Grid;
if (!VoxelStrateMeasurePrivate::BuildSampleGrid(
Generator, Manager, StrateIndex, Settings, Grid, Result.RefusalReason))
&Generator, &Manager, nullptr, StrateIndex, 0, 0, 0.0f, false,
Settings, Grid, Result.RefusalReason))
{
return Result;
}
Result.ResolvedMarginVoxels = Grid.ResolvedMarginVoxels;
Result.SampledMinZ = Grid.SampledMinZ;
Result.SampledMaxZ = Grid.SampledMaxZ;
Result.SampledNumX = Grid.NumX;
Result.SampledNumY = Grid.NumY;
Result.SampledNumZ = Grid.NumZ;
Result.SampledMinX = Grid.MinX;
Result.SampledMaxX = Grid.MaxX;
Result.SampledMinY = Grid.MinY;
Result.SampledMaxY = Grid.MaxY;
VoxelStrateMeasurePrivate::BuildMetricsFromGrid(Grid, Settings, Result);
Result.bValid = Result.NumSampled > 0;
if (!Result.bValid)
{
Result.RefusalReason = TEXT("The measurement grid was empty.");
}
return Result;
}
FVoxelStrateMetrics VF_MeasureStrateWithSampler(
const IVoxelStrateDensitySampler& Sampler,
int32 StrateBottomWorldZ,
int32 StrateTopWorldZ,
float BoundarySealThickness,
const FVoxelStrateMeasureSettings& Settings)
{
FVoxelStrateMetrics Result;
VoxelStrateMeasurePrivate::FSampleGrid Grid;
if (!VoxelStrateMeasurePrivate::BuildSampleGrid(
nullptr, nullptr, &Sampler, INDEX_NONE, StrateBottomWorldZ, StrateTopWorldZ,
BoundarySealThickness, true, Settings, Grid, Result.RefusalReason))
{
return Result;
}
@@ -1244,9 +1334,11 @@ EVoxelConnectivityResult VF_AreConnected(
{
int32 NumRouteRetries = 0;
return VoxelStrateMeasurePrivate::QueryConnectivity(
Generator,
Manager,
&Generator,
&Manager,
nullptr,
StrateIndex,
0, 0, 0.0f, false,
AVoxel,
BVoxel,
Settings,
@@ -1268,9 +1360,11 @@ EVoxelConnectivityResult VF_AreConnected(
int32& OutNumRouteRetries)
{
return VoxelStrateMeasurePrivate::QueryConnectivity(
Generator,
Manager,
&Generator,
&Manager,
nullptr,
StrateIndex,
0, 0, 0.0f, false,
AVoxel,
BVoxel,
Settings,
@@ -1293,9 +1387,11 @@ EVoxelConnectivityResult VF_AreConnected(
{
int32 NumRouteRetries = 0;
return VoxelStrateMeasurePrivate::QueryConnectivity(
Generator,
Manager,
&Generator,
&Manager,
nullptr,
StrateIndex,
0, 0, 0.0f, false,
AVoxel,
BVoxel,
Settings,
@@ -1316,9 +1412,11 @@ FVoxelConnectivityDiagnostics VF_DiagnoseConnectivity(
FVoxelConnectivityDiagnostics Result;
int32 NumRouteRetries = 0;
VoxelStrateMeasurePrivate::QueryConnectivity(
Generator,
Manager,
&Generator,
&Manager,
nullptr,
StrateIndex,
0, 0, 0.0f, false,
AVoxel,
BVoxel,
Settings,
@@ -1328,3 +1426,39 @@ FVoxelConnectivityDiagnostics VF_DiagnoseConnectivity(
&Result);
return Result;
}
EVoxelConnectivityResult VF_AreConnectedWithSampler(
const IVoxelStrateDensitySampler& Sampler,
int32 StrateBottomWorldZ,
int32 StrateTopWorldZ,
float BoundarySealThickness,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
bool& bOutStartSnapped,
bool& bOutGoalSnapped,
FVoxelConnectivityDiagnostics* OutDiagnostics)
{
int32 NumRouteRetries = 0;
return VoxelStrateMeasurePrivate::QueryConnectivity(
nullptr, nullptr, &Sampler, INDEX_NONE,
StrateBottomWorldZ, StrateTopWorldZ, BoundarySealThickness, true,
AVoxel, BVoxel, Settings, bOutStartSnapped, bOutGoalSnapped,
NumRouteRetries, OutDiagnostics);
}
FVoxelConnectivityDiagnostics VF_DiagnoseConnectivityWithSampler(
const IVoxelStrateDensitySampler& Sampler,
int32 StrateBottomWorldZ,
int32 StrateTopWorldZ,
float BoundarySealThickness,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings)
{
FVoxelConnectivityDiagnostics Result;
VF_AreConnectedWithSampler(Sampler, StrateBottomWorldZ, StrateTopWorldZ,
BoundarySealThickness, AVoxel, BVoxel, Settings,
Result.bStartSnapped, Result.bGoalSnapped, &Result);
return Result;
}
+43
View File
@@ -154,6 +154,33 @@ namespace VoxelOpChannels
static constexpr EVoxelOpChannelMask All = Density | Sdf;
}
//=============================================================================
// OPERATOR RESOURCES / RESSOURCES D'OPERATEUR
//=============================================================================
// Sample-channel declarations describe the FVoxelOpSample only. Some operators also consume
// stateful geometry published by an earlier source (the room graph, shaft field, or surface
// column). Keep that dependency explicit as a second, tiny graph so the composer cannot place a
// room modifier on a stack that has no room source merely because its Density/Sdf masks happen to
// fit.
enum class EVoxelOpResource : uint8
{
None = 0,
RoomGeometry = 1 << 0,
ShaftGeometry = 1 << 1,
SurfaceColumn = 1 << 2,
};
using EVoxelOpResourceMask = uint8;
namespace VoxelOpResources
{
static constexpr EVoxelOpResourceMask None = 0;
static constexpr EVoxelOpResourceMask RoomGeometry = static_cast<EVoxelOpResourceMask>(EVoxelOpResource::RoomGeometry);
static constexpr EVoxelOpResourceMask ShaftGeometry = static_cast<EVoxelOpResourceMask>(EVoxelOpResource::ShaftGeometry);
static constexpr EVoxelOpResourceMask SurfaceColumn = static_cast<EVoxelOpResourceMask>(EVoxelOpResource::SurfaceColumn);
static constexpr EVoxelOpResourceMask All = RoomGeometry | ShaftGeometry | SurfaceColumn;
}
//=============================================================================
// COMPOSITION / COMBINERS
//=============================================================================
@@ -382,6 +409,22 @@ public:
virtual EVoxelOpChannelMask ChannelReads() const = 0;
virtual EVoxelOpChannelMask ChannelWrites() const = 0;
/**
* Non-sample state required by this op. A declaration is satisfied only by a provider that
* appeared earlier in the stack. Defaults are deliberately empty so existing independent
* operators and external test doubles remain source-compatible.
*/
virtual EVoxelOpResourceMask RequiredResources() const
{
return VoxelOpResources::None;
}
/** State published for later consumers, e.g. FRoomGraphSource's room geometry cache. */
virtual EVoxelOpResourceMask ProvidedResources() const
{
return VoxelOpResources::None;
}
/**
* True when this op contributes a delta to its written channels without replacing, clamping,
* selecting, or otherwise depending on the previous value of those written channels. Such an
@@ -39,6 +39,20 @@
#include "VoxelHeightOp.h" // IVoxelBiomeField — BuildSurfaceStack takes ownership of one
class UVoxelStrateManager;
struct FVoxelOpStackRecipe;
struct FVoxelStrateArchetypeParams;
enum class EVoxelStrateOpClass : uint8;
/** The declarations the offline structure roller reads before it ever materialises an op. */
struct VOXELFORGE_API FVoxelStrateOpContract
{
EVoxelOpRole Role = EVoxelOpRole::DetailModifier;
EVoxelOpChannelMask Reads = VoxelOpChannels::None;
EVoxelOpChannelMask Writes = VoxelOpChannels::None;
bool bAdditive = false;
EVoxelOpResourceMask RequiredResources = VoxelOpResources::None;
EVoxelOpResourceMask ProvidedResources = VoxelOpResources::None;
};
/**
* FVoxelOpStack — une liste ordonnée d'opérateurs + le pliage de verdict de boîte.
@@ -68,6 +82,8 @@ private:
EVoxelOpChannelMask Reads = VoxelOpChannels::None;
EVoxelOpChannelMask Writes = VoxelOpChannels::None;
bool bAdditive = false;
EVoxelOpResourceMask RequiredResources = VoxelOpResources::None;
EVoxelOpResourceMask ProvidedResources = VoxelOpResources::None;
};
public:
@@ -102,6 +118,8 @@ public:
Entry.Reads = Entry.Op->ChannelReads();
Entry.Writes = Entry.Op->ChannelWrites();
Entry.bAdditive = Entry.Op->IsAdditive();
Entry.RequiredResources = Entry.Op->RequiredResources();
Entry.ProvidedResources = Entry.Op->ProvidedResources();
}
Ops.Add(MoveTemp(Entry));
}
@@ -258,6 +276,10 @@ private:
namespace VoxelDensityOps
{
/** Query the concrete op declaration used by the offline recipe roller. */
VOXELFORGE_API bool GetStrateOpContract(EVoxelStrateOpClass OpClass,
FVoxelStrateOpContract& OutContract);
/** Rôle 1 — `Density = BaseDensity` partout. `ClassifyBox` → AllSolid, exact et gratuit.
* Racine de TunnelNetwork, Maze, VerticalShafts et des gaps de bedrock. */
VOXELFORGE_API TUniquePtr<IVoxelDensityOp> MakeConstantRockSource(float BaseDensity);
@@ -439,3 +461,12 @@ namespace VoxelDensityOps
int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager);
}
/** Materialise an offline recipe. Structural posts are appended internally and cannot be omitted. */
VOXELFORGE_API bool VF_BuildStackFromRecipe(const FVoxelOpStackRecipe& Recipe,
const FVoxelStrateArchetypeParams& Params,
int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager,
FVoxelOpStack& OutStack,
FVoxelOpContext& OutContext,
FString* OutError = nullptr);
@@ -8,10 +8,109 @@
#include "CoreMinimal.h"
#include "VoxelStrateTypes.h"
#include "VoxelDensityOpStack.h"
#include "VoxelStrateComposer.generated.h"
class UVoxelSettings;
class UVoxelStrateDefinition;
/** The one-bit identity choice at the root of a rolled structure. */
UENUM(BlueprintType)
enum class EVoxelStrateRootPolarity : uint8
{
RockCarve = 0,
VoidFill = 1,
};
/** Native parameter family an op reads when a recipe is materialised. */
UENUM(BlueprintType)
enum class EVoxelStrateParamBlock : uint8
{
None = 0,
TunnelNetwork = 1,
Slab = 2,
Maze = 3,
Surface = 4,
VerticalShaft = 5,
FloatingIsland = 6,
};
/** Stable manifest id for every op the offline structure roller may place. */
UENUM(BlueprintType)
enum class EVoxelStrateOpClass : uint8
{
ConstantRockSource = 0,
ConstantVoidSource = 1,
RoomGraphSource = 2,
LatticeCorridorSource = 3,
ShaftFieldSource = 4,
IslandBlobSource = 5,
NoiseRibbonSource = 6,
SdfRoughnessMod = 7,
SdfCarve = 8,
SdfFill = 9,
GridColumnMod = 10,
CaveRoughnessMod = 11,
CaveTerraceMod = 12,
LayerLineMod = 13,
RibbingMod = 14,
CaveOverhangMod = 15,
CaveCliffMod = 16,
ScallopMod = 17,
CaveArchMod = 18,
RoomColumnMod = 19,
DomeMod = 20,
PinchMod = 21,
FloorBiasMod = 22,
WormFieldSource = 23,
ShaftLedgeMod = 24,
DensityNoiseCarveMod = 25,
DensityNoiseFillMod = 26,
};
/** One creative op in the serialisable structure manifest. */
USTRUCT(BlueprintType)
struct VOXELFORGE_API FVoxelOpRecipeEntry
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Recipe")
EVoxelStrateOpClass OpClass = EVoxelStrateOpClass::ConstantRockSource;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Recipe")
EVoxelStrateParamBlock ParamBlock = EVoxelStrateParamBlock::None;
};
/**
* Serialisable creative structure. Structural posts are intentionally absent: the recipe builder
* appends spine, vertical seal, passage carve, and XY edge seal in that fixed order every time.
*/
USTRUCT(BlueprintType)
struct VOXELFORGE_API FVoxelOpStackRecipe
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Recipe")
EVoxelStrateRootPolarity RootPolarity = EVoxelStrateRootPolarity::RockCarve;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Recipe")
FVoxelOpRecipeEntry Root;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Recipe")
FVoxelOpRecipeEntry ShapeSource;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Recipe")
FVoxelOpRecipeEntry Conversion;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Recipe")
TArray<FVoxelOpRecipeEntry> Modifiers;
// Which native block supplies the mandatory structural post parameters.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Recipe")
EVoxelStrateParamBlock StructuralParamBlock = EVoxelStrateParamBlock::None;
};
/** The kind of value represented by one entry of VF_STRATE_PARAM_FIELDS. */
enum class EVoxelStrateFieldKind : uint8
{
@@ -177,6 +276,19 @@ VOXELFORGE_API const TCHAR* VF_GetStrateArchetypeName(ECaveGeneratorType Archety
VOXELFORGE_API FVoxelStrateRollInfo VF_RollStrateParamsDetailed(
const FVoxelStrateCorpus& Corpus, int32 Seed, int32 Index);
/** Roll one exact native family, used to feed the independent blocks of a novel recipe. */
VOXELFORGE_API FVoxelStrateRollInfo VF_RollStrateParamsDetailedForArchetype(
const FVoxelStrateCorpus& Corpus, ECaveGeneratorType Archetype, int32 Seed, int32 Index);
/** Pure structure roll: no retained RNG state and no corpus/runtime dependency. */
VOXELFORGE_API FVoxelOpStackRecipe VF_RollStrateStructure(int32 Seed, int32 Index);
/** Stable compact representation and equality/hash helpers for manifests and reports. */
VOXELFORGE_API FString VF_FormatStrateStructureRecipe(const FVoxelOpStackRecipe& Recipe);
VOXELFORGE_API uint32 VF_HashStrateStructureRecipe(const FVoxelOpStackRecipe& Recipe);
VOXELFORGE_API bool VF_AreStrateStructureRecipesIdentical(
const FVoxelOpStackRecipe& A, const FVoxelOpStackRecipe& B);
/** The compact API requested by the composer design. */
VOXELFORGE_API FStrateGenerationParams VF_RollStrateParams(
const FVoxelStrateCorpus& Corpus, int32 Seed, int32 Index);
@@ -7,6 +7,14 @@
class UVoxelGenerator;
class UVoxelStrateManager;
/** Read-only density source used by the offline Tier 2 pass for a materialised custom stack. */
class VOXELFORGE_API IVoxelStrateDensitySampler
{
public:
virtual ~IVoxelStrateDensitySampler() = default;
virtual float SampleDensity(float WorldX, float WorldY, float WorldZ) const = 0;
};
/** Explicit outcome of a coarse connectivity query plus its full-resolution route re-check. */
enum class EVoxelConnectivityResult : uint8
{
@@ -122,6 +130,14 @@ VOXELFORGE_API FVoxelStrateMetrics VF_MeasureStrate(
int32 StrateIndex,
const FVoxelStrateMeasureSettings& Settings);
/** The same Tier 2 pass over an explicit voxel Z window and a custom read-only sampler. */
VOXELFORGE_API FVoxelStrateMetrics VF_MeasureStrateWithSampler(
const IVoxelStrateDensitySampler& Sampler,
int32 StrateBottomWorldZ,
int32 StrateTopWorldZ,
float BoundarySealThickness,
const FVoxelStrateMeasureSettings& Settings);
/**
* Test coarse connectivity and re-check recovered coarse routes at full resolution.
*
@@ -188,3 +204,26 @@ VOXELFORGE_API FVoxelConnectivityDiagnostics VF_DiagnoseConnectivity(
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings);
/** Connectivity variant for the explicit custom-stack measurement window. */
VOXELFORGE_API EVoxelConnectivityResult VF_AreConnectedWithSampler(
const IVoxelStrateDensitySampler& Sampler,
int32 StrateBottomWorldZ,
int32 StrateTopWorldZ,
float BoundarySealThickness,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
bool& bOutStartSnapped,
bool& bOutGoalSnapped,
FVoxelConnectivityDiagnostics* OutDiagnostics = nullptr);
/** Full diagnostics variant for a custom stack. */
VOXELFORGE_API FVoxelConnectivityDiagnostics VF_DiagnoseConnectivityWithSampler(
const IVoxelStrateDensitySampler& Sampler,
int32 StrateBottomWorldZ,
int32 StrateTopWorldZ,
float BoundarySealThickness,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings);