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

@@ -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;
}