feat(world): the XY edge seal -- the fourth structural invariant

VF_ApplyBoundarySeal was purely VERTICAL: it sealed each strate's ceiling
and floor and there was no horizontal equivalent, because until the depths
were bounded the world had no sides. The GDD now says "bounded, but
gigantic", so a player who walked far enough reached unterminated
generation running off into nothing.

Built as a FORCING op, appended automatically and not author-omittable, so
an invented strate cannot forget its own edge. Because a forcing op proves
AllSolid in its band, the outer shell also becomes free to skip in tile
classification -- correctness and a perf win from one change.

  order  spine -> vertical seal -> passage carving -> XY EDGE SEAL
         -> disturbances -> diff
  A near-rim passage may pierce a vertical seal; the XY seal runs after and
  wins. A passage must never punch a hole through the world's edge.

ClassifyBox/ForcedMarginOverBox proved via the closest AABB radius with a
one-voxel margin: 8 boxes proved, 1 tile proved, 600 voxels brute-forced,
0 violations. FXYEdgeSealOp is IsXYPure. Max rim discontinuity .187 against
a 1.0 threshold -- a ramp, not a cylinder wall.

⚠️ WorldRadiusVoxels DEFAULTS TO 0, and that is deliberate. Codex flagged
that DA_Settings and BP_VoxelSettings predate the property and so do not
serialize it -- they inherit the C++ default silently. A default of 8192
would have walled in every existing world on next editor open with nothing
in the asset to explain it. 0 is a bitwise no-op (45 interior comparisons
against the unbounded path prove it). The GDD's intended ~8192 is a value
to set in the asset, deliberately, once. Documented at the declaration.

Codex found four spec errors, three of them mine: VF_ApplyBoundarySeal
lives in VoxelDensityPrimitives.h not VoxelCaveMorphology; the asset
default trap above; and my "density increases through the band" wording
contradicts the MC sign convention (solid is more NEGATIVE) -- the test
measures increasing solidness.

Build clean, 21 succeeded / 0 failed, run and verified by me.

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 01:17:12 +02:00
1 parent 44938f3f98
commit 9d2324879d
19 files changed
+1006 -126

No files matched your search

@@ -151,10 +151,9 @@ bool FVoxelForgeClassifyTileTest::RunTest(const FString& Parameters)
AddInfo(FString::Printf(
TEXT("ClassifyTile verdicts over %d scanned tiles: Mixed %d, AllSolid %d, AllAir %d ")
TEXT("(brute-forcing %d of them). NOTE: cave archetypes always return Mixed today — see ")
TEXT("VoxelGenerator.cpp \"archétype cave [...] pas prouvable en v1\". A low non-Mixed count ")
TEXT("is expected and is exactly the tile-skipping prize OPSTACK-PLAN wants EffectOverBox ")
TEXT("to unlock."),
TEXT("(brute-forcing %d of them). Cave archetypes now use the opt-in operator-stack ")
TEXT("ClassifyBox fold; the global XY edge proof also skips proven outer-shell tiles. A low ")
TEXT("non-Mixed count is expected: every uncertain case remains Mixed for safety."),
NumTilesScanned, NumMixed, NumAllSolid, NumAllAir, ToVerify.Num()));
if (ToVerify.Num() == 0)
@@ -315,10 +315,10 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters)
VoxelDensityOps::BuildSurfaceStack(Stack, P, World.Settings->Seed,
Gen->OriginSpineRadius, World.StrateManager.Get());
// 1 source + 1 overhang + 3 structurels. L'op overhang est présent mais inerte ici
// 1 source + 1 overhang + 4 structurels. L'op overhang est présent mais inerte ici
// (amp 0 ⇒ sortie immédiate) — la décomposition ne change pas selon les params.
TestEqual(TEXT("the surface density stack is source + overhang + 3 structural"),
Stack.Num(), 5);
TestEqual(TEXT("the surface density stack is source + overhang + 4 structural"),
Stack.Num(), 6);
FVoxelOpContext Ctx;
Ctx.Seed = (uint32)World.Settings->Seed;
@@ -103,8 +103,8 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters)
VoxelDensityOps::BuildFloatingIslandStack(Stack, P, World.Settings->Seed,
Gen->OriginSpineRadius, World.StrateManager.Get());
// void + blobs + roughness + fill + 3 structurels.
TestEqual(TEXT("the island stack is decomposed into 7 ops"), Stack.Num(), 7);
// void + blobs + roughness + fill + 4 structurels.
TestEqual(TEXT("the island stack is decomposed into 8 ops"), Stack.Num(), 8);
FVoxelOpContext Ctx;
Ctx.Seed = (uint32)World.Settings->Seed;
@@ -140,8 +140,8 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
// La décomposition doit être une DÉCOMPOSITION. Un `FMazeOp` monolithique passerait tous les
// tests numériques ci-dessous et aurait pourtant raté l'objet entier du refactor (§2.5).
TestEqual(TEXT("the Maze stack is decomposed, not wrapped (rock + corridors + roughness + carve + 3 structural)"),
Stack.Num(), 7);
TestEqual(TEXT("the Maze stack is decomposed, not wrapped (rock + corridors + roughness + carve + 4 structural)"),
Stack.Num(), 8);
FVoxelOpContext Ctx;
Ctx.Seed = (uint32)World.Settings->Seed;
@@ -91,8 +91,8 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
VoxelDensityOps::BuildVerticalShaftStack(Stack, P, World.Settings->Seed,
Gen->OriginSpineRadius, World.StrateManager.Get());
// rock + shafts + roughness + carve + ledges + 3 structurels.
TestEqual(TEXT("the shaft stack is decomposed into 8 ops"), Stack.Num(), 8);
// rock + shafts + roughness + carve + ledges + 4 structurels.
TestEqual(TEXT("the shaft stack is decomposed into 9 ops"), Stack.Num(), 9);
FVoxelOpContext Ctx;
Ctx.Seed = (uint32)World.Settings->Seed;
@@ -103,9 +103,9 @@ bool FVoxelForgeOpStackSlabTest::RunTest(const FString& Parameters)
VoxelDensityOps::BuildSlabStack(Stack, SlabParams, World.Settings->Seed,
Gen->OriginSpineRadius, World.StrateManager.Get());
// La décomposition doit rester une DÉCOMPOSITION : vide + colonnes + 3 structurels.
TestEqual(*FString::Printf(TEXT("%s decomposes into void + columns + 3 structural"), SlotName),
Stack.Num(), 5);
// La décomposition doit rester une DÉCOMPOSITION : vide + colonnes + 4 structurels.
TestEqual(*FString::Printf(TEXT("%s decomposes into void + columns + 4 structural"), SlotName),
Stack.Num(), 6);
FVoxelOpContext Ctx;
Ctx.Seed = (uint32)World.Settings->Seed;
@@ -464,11 +464,11 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
VoxelDensityOps::BuildTunnelNetworkStack(Stack, P, World.Settings->Seed,
Gen->OriginSpineRadius, World.StrateManager.Get());
// rock + roomgraph + carve + **les 12 modificateurs de détail** + worms + 3 structurels = 19.
// rock + roomgraph + carve + **les 12 modificateurs de détail** + worms + 4 structurels = 20.
// L'étape C n'ajoute AUCUN opérateur — elle change ce que onze d'entre eux LISENT — donc ce
// nombre DOIT bouger à chaque groupe de l'étape B — c'est un compteur de progression, pas une
// formalité : une pile qui ne grandit pas est une pile dont l'opérateur n'a pas été ajouté.
TestEqual(TEXT("the stage-A+B tunnel stack is decomposed into 19 ops"), Stack.Num(), 19);
TestEqual(TEXT("the stage-A+B tunnel stack is decomposed into 20 ops"), Stack.Num(), 20);
FVoxelOpContext Ctx;
Ctx.Seed = (uint32)World.Settings->Seed;
@@ -1408,7 +1408,7 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
UWCtx.StrateBottomWorldZ = UP.StrateBottomWorldZ;
UWStack.PrepareChunk(UWCtx);
TestEqual(TEXT("the Underwater stack is the tunnel stack: same 19 ops"), UWStack.Num(), 19);
TestEqual(TEXT("the Underwater stack is the tunnel stack: same 20 ops"), UWStack.Num(), 20);
// 8 → 24 chunks, comme le balayage principal. Le premier run vert a rapporté **0 de 2000
// échantillons en grotte ouverte** là où le slot TunnelNetwork en avait 16,7 % : trop peu de
@@ -2471,6 +2471,228 @@ bool FVoxelForgeStrateConnectivityRefinementTest::RunTest(const FString& Paramet
*ConnectivityProbeText(SeedReport.DepartureToSpine),
*ConnectivityProbeText(SeedReport.ArrivalToDeparture));
}
// PART 0: resolve the two remaining step-2 negatives without changing generation. The
// 12M cap is deliberately high enough for the 320x320x112 step-1/radius-160 fallback,
// but low enough that the requested 384x384x112 step-1/radius-192 attempt is explicitly
// reported as a MaxCells refusal rather than silently changing the measurement budget.
struct FSeedResolutionRow
{
int32 SampleStep = 0;
int32 RadiusInVoxels = 0;
bool bMaxCellsRefused = false;
FVoxelStrateMetrics Metrics;
FVoxelConnectivityDiagnostics Diagnostics;
};
static constexpr int32 ResolutionSeeds[] = {6, 14};
static constexpr FRefinementCase ResolutionCases[] = {
{2, 192},
{1, 192},
{2, 128},
{1, 128},
};
constexpr int32 ResolutionMaxCells = 12000000;
Summary += TEXT(
"PART 0 — residual VerticalShafts seed refinement (seeds 6 and 14; margin derived; "
"MaxCells=12000000):\n");
Summary += TEXT(
" seed | SampleStep | RadiusInVoxels | verdict | components | largest share | retries | "
"arrival mouth (x,y,z; component share) | departure mouth (x,y,z; component share)\n");
for (const int32 Seed : ResolutionSeeds)
{
FTestWorld ResolutionWorld;
ResolutionWorld.Build(Seed, /*InGapChunks=*/2);
if (!ResolutionWorld.IsValid())
{
AddError(FString::Printf(
TEXT("HARD FAILURE: Part 0 seed %d could not build the VerticalShafts fixture."),
Seed));
bAllChecksPassed = false;
continue;
}
FVector ResolutionArrivalPoint = FVector::ZeroVector;
FVector ResolutionDeparturePoint = FVector::ZeroVector;
if (!FindChainMouths(
ResolutionWorld.StrateManager->GetPassages(),
VerticalShaftsIndex,
ResolutionArrivalPoint,
ResolutionDeparturePoint))
{
AddError(FString::Printf(
TEXT("HARD FAILURE: Part 0 seed %d does not have exactly one arrival and "
"departure mouth."),
Seed));
bAllChecksPassed = false;
continue;
}
TArray<FSeedResolutionRow> ResolutionRows;
ResolutionRows.Reserve(UE_ARRAY_COUNT(ResolutionCases) + 1);
for (const FRefinementCase& TestCase : ResolutionCases)
{
FSeedResolutionRow& Row = ResolutionRows.AddDefaulted_GetRef();
Row.SampleStep = TestCase.SampleStep;
Row.RadiusInVoxels = TestCase.RadiusInVoxels;
FVoxelStrateMeasureSettings RowSettings = DiagnosticSettings;
RowSettings.SampleStep = TestCase.SampleStep;
RowSettings.RadiusInVoxels = TestCase.RadiusInVoxels;
RowSettings.MaxCells = ResolutionMaxCells;
RowSettings.InteriorMarginVoxels = -1;
Row.Metrics = VF_MeasureStrate(
*ResolutionWorld.Generator,
*ResolutionWorld.StrateManager,
VerticalShaftsIndex,
RowSettings);
const bool bMaxCellsRefusal = !Row.Metrics.bValid
&& Row.Metrics.RefusalReason.Contains(TEXT("MaxCells"));
Row.bMaxCellsRefused = bMaxCellsRefusal;
Row.Diagnostics = VF_DiagnoseConnectivity(
*ResolutionWorld.Generator,
*ResolutionWorld.StrateManager,
VerticalShaftsIndex,
ResolutionArrivalPoint,
ResolutionDeparturePoint,
RowSettings);
const bool bExpectedFallbackRefusal = TestCase.SampleStep == 1
&& TestCase.RadiusInVoxels == 192;
if (!Row.Metrics.bValid
&& !(bExpectedFallbackRefusal && bMaxCellsRefusal))
{
AddError(FString::Printf(
TEXT("HARD FAILURE: Part 0 seed %d row step=%d radius=%d was not a "
"bounded measurement (metrics valid=%s reason='%s')."),
Seed,
Row.SampleStep,
Row.RadiusInVoxels,
Row.Metrics.bValid ? TEXT("true") : TEXT("false"),
*Row.Metrics.RefusalReason));
bAllChecksPassed = false;
}
if (Row.Metrics.bValid
&& !Row.Diagnostics.bValid
&& Row.Diagnostics.Result != EVoxelConnectivityResult::OutOfWindow)
{
AddError(FString::Printf(
TEXT("HARD FAILURE: Part 0 seed %d row step=%d radius=%d produced an "
"invalid connectivity diagnostic with result %s."),
Seed,
Row.SampleStep,
Row.RadiusInVoxels,
ConnectivityResultName(Row.Diagnostics.Result)));
bAllChecksPassed = false;
}
}
// If the requested fine grid is over the cap, perform the specified radius-control
// substitution. This row is the one used to decide whether step refinement changes
// the verdict; the refused 192 row remains in the report as evidence of the cap.
if (ResolutionRows.IsValidIndex(1) && ResolutionRows[1].bMaxCellsRefused)
{
FSeedResolutionRow& FallbackRow = ResolutionRows.AddDefaulted_GetRef();
FallbackRow.SampleStep = 1;
FallbackRow.RadiusInVoxels = 160;
FVoxelStrateMeasureSettings FallbackSettings = DiagnosticSettings;
FallbackSettings.SampleStep = 1;
FallbackSettings.RadiusInVoxels = 160;
FallbackSettings.MaxCells = ResolutionMaxCells;
FallbackSettings.InteriorMarginVoxels = -1;
FallbackRow.Metrics = VF_MeasureStrate(
*ResolutionWorld.Generator,
*ResolutionWorld.StrateManager,
VerticalShaftsIndex,
FallbackSettings);
FallbackRow.Diagnostics = VF_DiagnoseConnectivity(
*ResolutionWorld.Generator,
*ResolutionWorld.StrateManager,
VerticalShaftsIndex,
ResolutionArrivalPoint,
ResolutionDeparturePoint,
FallbackSettings);
if (!FallbackRow.Metrics.bValid)
{
AddError(FString::Printf(
TEXT("HARD FAILURE: Part 0 seed %d step=1 radius=160 fallback was not "
"a bounded measurement (metrics valid=%s reason='%s')."),
Seed,
FallbackRow.Metrics.bValid ? TEXT("true") : TEXT("false"),
*FallbackRow.Metrics.RefusalReason));
bAllChecksPassed = false;
}
if (FallbackRow.Metrics.bValid
&& !FallbackRow.Diagnostics.bValid
&& FallbackRow.Diagnostics.Result != EVoxelConnectivityResult::OutOfWindow)
{
AddError(FString::Printf(
TEXT("HARD FAILURE: Part 0 seed %d step=1 radius=160 produced an "
"invalid connectivity diagnostic with result %s."),
Seed,
ConnectivityResultName(FallbackRow.Diagnostics.Result)));
bAllChecksPassed = false;
}
}
for (const FSeedResolutionRow& Row : ResolutionRows)
{
const bool bUsable = Row.Metrics.bValid && Row.Diagnostics.bValid;
const bool bOutOfWindow = Row.Diagnostics.Result == EVoxelConnectivityResult::OutOfWindow;
const FString Verdict = Row.bMaxCellsRefused
? TEXT("EXCEEDS_MAXCELLS")
: (Row.Metrics.bValid && bOutOfWindow)
? TEXT("OUT_OF_WINDOW")
: bUsable
? ConnectivityResultName(Row.Diagnostics.Result)
: TEXT("UNUSABLE");
const FString Components = Row.Metrics.bValid
? FString::Printf(TEXT("%d"), Row.Metrics.NumAirComponents)
: TEXT("N/A");
const FString LargestShare = Row.Metrics.bValid
? FString::Printf(TEXT("%.9g"), Row.Metrics.LargestComponentShare)
: TEXT("N/A");
const FString Retries = Row.Diagnostics.bValid || (Row.Metrics.bValid && bOutOfWindow)
? FString::Printf(TEXT("%d"), Row.Diagnostics.NumRouteRetries)
: TEXT("N/A");
const FString ArrivalShare = bUsable
? FString::Printf(TEXT("%.9g"), Row.Diagnostics.StartComponentShare)
: TEXT("N/A");
const FString DepartureShare = bUsable
? FString::Printf(TEXT("%.9g"), Row.Diagnostics.GoalComponentShare)
: TEXT("N/A");
Summary += FString::Printf(
TEXT(" %d | %d | %d | %s | %s | %s | %s | (%.3f, %.3f, %.3f; %s) | "
"(%.3f, %.3f, %.3f; %s)%s\n"),
Seed,
Row.SampleStep,
Row.RadiusInVoxels,
*Verdict,
*Components,
*LargestShare,
*Retries,
ResolutionArrivalPoint.X,
ResolutionArrivalPoint.Y,
ResolutionArrivalPoint.Z,
*ArrivalShare,
ResolutionDeparturePoint.X,
ResolutionDeparturePoint.Y,
ResolutionDeparturePoint.Z,
*DepartureShare,
Row.bMaxCellsRefused
? TEXT(" [requested row refused; use step=1/radius=160 below]")
: TEXT(""));
}
}
Summary += TEXT(
" Part 0 interpretation: step=1/radius=192 is attempted first; when it exceeds the "
"12M measurement cap, step=1/radius=160 is the prescribed radius control.\n");
}
else
{
@@ -79,6 +79,8 @@ namespace VoxelForgeTest
NewObject<UVoxelSettings>(GetTransientPackage(), NAME_None, RF_Transient));
Settings->Seed = InSeed;
Settings->InterStrateGapChunks = InGapChunks;
// WorldRadiusVoxels/EdgeSealThickness intentionally inherit the UVoxelSettings defaults;
// WorldEdgeSeal overrides them explicitly when it needs a small test world or radius 0.
// Une strate par archétype. PINNED via FixedStrates, pas via le pool : Initialize()
// mélange le pool avec le seed, ce qui rendrait la correspondance archétype → Z
@@ -0,0 +1,339 @@
// VoxelForgeWorldEdgeSealTest.cpp
// Falsifiable coverage for the bounded-world XY edge seal.
#if WITH_DEV_AUTOMATION_TESTS
#include "Misc/AutomationTest.h"
#include "VoxelForgeTestFixture.h"
#include "VoxelDensityOpStack.h"
#include "VoxelDensityPrimitives.h"
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FVoxelForgeWorldEdgeSealTest,
"VoxelForge.Generation.WorldEdgeSeal",
EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter)
namespace
{
constexpr float TestWorldRadius = 256.0f;
constexpr float TestEdgeThickness = 64.0f;
constexpr float RimJumpThreshold = 1.0f;
void SetEdgeSettings(VoxelForgeTest::FTestWorld& World, float Radius)
{
World.Settings->WorldRadiusVoxels = Radius;
World.Settings->EdgeSealThickness = TestEdgeThickness;
World.Generator->InitializeSettings(World.Settings.Get());
}
void ConfigureNearRimPassages(VoxelForgeTest::FTestWorld& World)
{
for (TStrongObjectPtr<UVoxelStrateDefinition>& DefPtr : World.Definitions)
{
if (!DefPtr.IsValid()) continue;
FStratePassageConfig& Cfg = DefPtr->PassageConfig;
Cfg.Connections = 1;
Cfg.Style = EVoxelPassageStyle::Straight;
Cfg.MouthRadius = 6.0f;
Cfg.MidRadius = 6.0f;
Cfg.ReachMin = 40.0f;
Cfg.ReachMax = 40.0f;
Cfg.DistanceMin = 240.0f;
Cfg.DistanceMax = 240.0f;
Cfg.Wander = 0.0f;
Cfg.Segments = 4;
Cfg.VerticalWobble = 0.0f;
}
World.Reinitialize();
}
bool IsSolidMC(const UVoxelGenerator& Generator, const FVector& Point)
{
return Generator.GetDensityAt(Point.X, Point.Y, Point.Z) < 0.0f;
}
}
bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
{
using namespace VoxelForgeTest;
int32 NumFailures = 0;
FString FailureKinds;
auto Fail = [&](const TCHAR* Kind)
{
++NumFailures;
if (!FailureKinds.IsEmpty()) FailureKinds += TEXT(", ");
FailureKinds += Kind;
};
// The ordinary fixture deliberately uses the requested default, so existing op-stack and
// legacy tests exercise the setting without having to know about the new post.
FTestWorld Legacy;
Legacy.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/false);
FTestWorld StackWorld;
StackWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/true);
FTestWorld Unbounded;
Unbounded.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/false);
if (!Legacy.IsValid() || !StackWorld.IsValid() || !Unbounded.IsValid())
{
AddError(Legacy.IsValid() && StackWorld.IsValid() && Unbounded.IsValid()
? TEXT("Unexpected edge-seal fixture state")
: (!Legacy.IsValid() ? Legacy.WhyInvalid()
: (!StackWorld.IsValid() ? StackWorld.WhyInvalid() : Unbounded.WhyInvalid())));
return false;
}
SetEdgeSettings(Legacy, TestWorldRadius);
SetEdgeSettings(StackWorld, TestWorldRadius);
SetEdgeSettings(Unbounded, 0.0f);
//==========================================================================
// POSITIVE CONTROL — far outside the radius, across several Z strata.
//==========================================================================
const FVector FarXY[] = {
FVector(384.0f, 0.0f, 0.0f),
FVector(-384.0f, 0.0f, 0.0f),
FVector(0.0f, 384.0f, 0.0f),
FVector(0.0f, -384.0f, 0.0f),
FVector(320.0f, 320.0f, 0.0f),
};
TArray<float> StrateZ;
for (int32 Slot = 0; Slot < 8; ++Slot)
{
int32 TopZ = 0, BottomZ = 0;
if (Legacy.GetSlotVoxelZRange(Slot, TopZ, BottomZ))
{
StrateZ.Add((float)((TopZ + BottomZ) / 2));
}
}
StrateZ.Add(Legacy.MidVoxelZ());
int32 NumPositiveSamples = 0;
for (const FVector& XY : FarXY)
{
for (const float Z : StrateZ)
{
const FVector P(XY.X, XY.Y, Z);
++NumPositiveSamples;
if (!IsSolidMC(*Legacy.Generator, P)) Fail(TEXT("positive-legacy"));
if (!IsSolidMC(*StackWorld.Generator, P)) Fail(TEXT("positive-opstack"));
}
}
//==========================================================================
// NEGATIVE CONTROL — the interior must be bit-identical to radius == 0.
//==========================================================================
const FVector InteriorXY[] = {
FVector(0.0f, 0.0f, 0.0f),
FVector(64.0f, -80.0f, 0.0f),
FVector(128.0f, 0.0f, 0.0f),
FVector(0.0f, -128.0f, 0.0f),
FVector(100.0f, 100.0f, 0.0f),
};
int32 NumInteriorComparisons = 0;
for (const FVector& XY : InteriorXY)
{
for (const float Z : StrateZ)
{
const FVector P(XY.X, XY.Y, Z);
const float BoundedD = Legacy.Generator->GetDensityAt(P.X, P.Y, P.Z);
const float UnboundedD = Unbounded.Generator->GetDensityAt(P.X, P.Y, P.Z);
++NumInteriorComparisons;
if (!BitEqual(BoundedD, UnboundedD)) Fail(TEXT("interior-not-no-op"));
}
}
//==========================================================================
// RIM SMOOTHNESS — measure solidness (the negative MC density magnitude), not its sign.
//==========================================================================
float MaxAdjacentSolidnessJump = 0.0f;
int32 NumRimDirections = 0;
const FVector RimDirections[] = {
FVector(1.0f, 0.0f, 0.0f),
FVector(0.0f, 1.0f, 0.0f),
FVector(-1.0f, 0.0f, 0.0f),
FVector(0.0f, -1.0f, 0.0f),
FVector(0.70710678f, 0.70710678f, 0.0f),
};
for (const FVector& Dir : RimDirections)
{
++NumRimDirections;
float PreviousSolidness = 0.0f;
bool bHasPrevious = false;
for (int32 R = (int32)(TestWorldRadius - TestEdgeThickness - 4.0f);
R <= (int32)(TestWorldRadius + 4.0f); ++R)
{
float InternalDensity = 0.0f;
VF_ApplyXYEdgeSeal(InternalDensity, Dir.X * (float)R, Dir.Y * (float)R,
TestWorldRadius, TestEdgeThickness, 8.0f);
const float Solidness = InternalDensity;
if (bHasPrevious)
{
MaxAdjacentSolidnessJump = FMath::Max(
MaxAdjacentSolidnessJump, FMath::Abs(Solidness - PreviousSolidness));
if (Solidness + KINDA_SMALL_NUMBER < PreviousSolidness)
Fail(TEXT("rim-not-monotonic"));
}
PreviousSolidness = Solidness;
bHasPrevious = true;
}
}
if (MaxAdjacentSolidnessJump > RimJumpThreshold) Fail(TEXT("rim-discontinuity"));
// Explicitly verify the true no-op contract, including the MC-facing wrapper.
float NoOpDensity = 3.25f;
const float NoOpBefore = NoOpDensity;
VF_ApplyXYEdgeSealMC(NoOpDensity, 100000.0f, -100000.0f,
0.0f, TestEdgeThickness, 8.0f);
if (!BitEqual(NoOpDensity, NoOpBefore)) Fail(TEXT("radius-zero-not-no-op"));
//==========================================================================
// CLASSIFYBOX — force boxes in the ramp and beyond the radius, then brute-force every voxel.
//==========================================================================
int32 TopShaftZ = 0, BottomShaftZ = 0;
if (!StackWorld.GetSlotVoxelZRange(FTestWorld::SlotVerticalShafts, TopShaftZ, BottomShaftZ))
{
Fail(TEXT("missing-shaft-slot"));
}
int32 NumBoxesProved = 0;
int64 NumVoxelsChecked = 0;
if (TopShaftZ > BottomShaftZ)
{
const float BoxZ0 = (float)(BottomShaftZ + 8);
const float BoxZ1 = (float)FMath::Min(BottomShaftZ + 10, TopShaftZ - 8);
const FBox ProvedBoxes[] = {
// Ramp boxes: closest radius is 240, safely past the 1-voxel proof margin.
FBox(FVector(240.0f, -2.0f, BoxZ0), FVector(244.0f, 2.0f, BoxZ1)),
FBox(FVector(-244.0f, -2.0f, BoxZ0), FVector(-240.0f, 2.0f, BoxZ1)),
FBox(FVector(-2.0f, 240.0f, BoxZ0), FVector(2.0f, 244.0f, BoxZ1)),
FBox(FVector(170.0f, 170.0f, BoxZ0), FVector(174.0f, 174.0f, BoxZ1)),
// Forced-solid boxes beyond the world radius.
FBox(FVector(320.0f, -2.0f, BoxZ0), FVector(324.0f, 2.0f, BoxZ1)),
FBox(FVector(-324.0f, -2.0f, BoxZ0), FVector(-320.0f, 2.0f, BoxZ1)),
FBox(FVector(-2.0f, 320.0f, BoxZ0), FVector(2.0f, 324.0f, BoxZ1)),
FBox(FVector(226.0f, 226.0f, BoxZ0), FVector(230.0f, 230.0f, BoxZ1)),
};
const int32 MidChunkZ = FMath::FloorToInt((BoxZ0 + BoxZ1) * 0.5f / CHUNK_SIZE);
const FVerticalShaftParams ShaftParams = StackWorld.StrateManager->GetVerticalShaftParamsForChunk(
FIntVector(0, 0, MidChunkZ));
FVoxelOpStack Stack;
VoxelDensityOps::BuildVerticalShaftStack(
Stack, ShaftParams, StackWorld.Settings->Seed,
StackWorld.Generator->OriginSpineRadius, StackWorld.StrateManager.Get());
FVoxelOpContext Ctx;
Ctx.ChunkCoord = FIntVector(0, 0, MidChunkZ);
Ctx.Seed = (uint32)StackWorld.Settings->Seed;
Ctx.LayoutVersion = StackWorld.StrateManager->GetLayoutVersion();
Ctx.StrateTopWorldZ = ShaftParams.StrateTopWorldZ;
Ctx.StrateBottomWorldZ = ShaftParams.StrateBottomWorldZ;
Ctx.WorldRadiusVoxels = TestWorldRadius;
Ctx.EdgeSealThickness = TestEdgeThickness;
Stack.PrepareChunk(Ctx);
for (const FBox& Box : ProvedBoxes)
{
if (Stack.ClassifyBox(Box, Ctx) != EVoxelTileClass::AllSolid)
{
Fail(TEXT("classifybox-missed-forced-band"));
continue;
}
++NumBoxesProved;
const int32 X0 = FMath::CeilToInt(Box.Min.X);
const int32 X1 = FMath::FloorToInt(Box.Max.X);
const int32 Y0 = FMath::CeilToInt(Box.Min.Y);
const int32 Y1 = FMath::FloorToInt(Box.Max.Y);
const int32 Z0 = FMath::CeilToInt(Box.Min.Z);
const int32 Z1 = FMath::FloorToInt(Box.Max.Z);
for (int32 Z = Z0; Z <= Z1; ++Z)
for (int32 Y = Y0; Y <= Y1; ++Y)
for (int32 X = X0; X <= X1; ++X)
{
++NumVoxelsChecked;
if (!IsSolidMC(*StackWorld.Generator, FVector((float)X, (float)Y, (float)Z)))
Fail(TEXT("classifybox-false-allsolid"));
}
}
}
// The T1.d path must be able to skip an edge tile without knowing which Z category it hits.
const EVoxelTileClass EdgeTile = StackWorld.Generator->ClassifyTile(
FIntVector(320, 0, BottomShaftZ + 8), 1, 8);
int32 NumTilesProved = (EdgeTile == EVoxelTileClass::AllSolid) ? 1 : 0;
if (EdgeTile != EVoxelTileClass::AllSolid) Fail(TEXT("classifytile-missed-edge"));
//==========================================================================
// PASSAGE ORDER — a near-rim passage carve is followed by the edge force.
//==========================================================================
float NearRimInternalDensity = 8.0f;
VF_ApplyPassageCarving(NearRimInternalDensity, 0.0f, 8.0f, TestEdgeThickness);
const float PassageOnlyDensity = NearRimInternalDensity;
VF_ApplyXYEdgeSeal(NearRimInternalDensity, TestWorldRadius - 16.0f, 0.0f,
TestWorldRadius, TestEdgeThickness, 8.0f);
int32 NumNearRimPassageChecks = 1;
if (!(PassageOnlyDensity < 0.0f && NearRimInternalDensity > 0.0f))
Fail(TEXT("passage-breaches-rim"));
// Try the same construction through the real deterministic passage generator. It is diagnostic
// coverage only: the synthetic helper case above is the hard assertion even if landing-site
// adjustment moves every generated mouth out of this small test rim.
FTestWorld GeneratedPassageWorld;
GeneratedPassageWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/false);
FTestWorld GeneratedPassageStackWorld;
GeneratedPassageStackWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/true);
auto CheckGeneratedPassages = [&](FTestWorld& World, const TCHAR* FailureKind)
{
if (!World.IsValid())
{
Fail(FailureKind);
return;
}
ConfigureNearRimPassages(World);
SetEdgeSettings(World, TestWorldRadius);
for (const FVoxelPassage& Passage : World.StrateManager->GetPassages())
{
TArray<FVector> Candidates = Passage.ControlPoints;
Candidates.Add(Passage.UpperPoint);
Candidates.Add(Passage.LowerPoint);
for (const FVector& P : Candidates)
{
if (P.X * P.X + P.Y * P.Y <= FMath::Square(TestWorldRadius - TestEdgeThickness)) continue;
++NumNearRimPassageChecks;
if (!IsSolidMC(*World.Generator, P)) Fail(FailureKind);
break;
}
}
};
CheckGeneratedPassages(GeneratedPassageWorld, TEXT("generated-passage-breaches-rim"));
CheckGeneratedPassages(GeneratedPassageStackWorld, TEXT("generated-opstack-passage-breaches-rim"));
// One sorted summary block; individual samples intentionally never log.
AddInfo(FString::Printf(
TEXT("WORLD_EDGE_SEAL SUMMARY\n")
TEXT(" classify_box_proved=%d\n")
TEXT(" classify_tile_proved=%d\n")
TEXT(" interior_comparisons=%d\n")
TEXT(" near_rim_passage_checks=%d\n")
TEXT(" positive_sample_points=%d (checked in both paths)\n")
TEXT(" rim_directions=%d\n")
TEXT(" rim_max_adjacent_solidness_jump=%.6f (threshold=%.6f)\n")
TEXT(" voxels_checked=%lld\n")
TEXT(" failures=%d (%s)"),
NumBoxesProved, NumTilesProved, NumInteriorComparisons, NumNearRimPassageChecks,
NumPositiveSamples, NumRimDirections, MaxAdjacentSolidnessJump, RimJumpThreshold,
(long long)NumVoxelsChecked, NumFailures, *FailureKinds));
if (NumFailures > 0)
{
AddError(FString::Printf(TEXT("WorldEdgeSeal aggregate checks failed: %d (%s)"),
NumFailures, *FailureKinds));
}
return NumFailures == 0;
}
#endif // WITH_DEV_AUTOMATION_TESTS
@@ -2,7 +2,7 @@
// Les opérateurs concrets de la Phase 1 : la décomposition de Maze + le post-traitement structurel.
// The concrete Phase 1 operators: the Maze decomposition + the structural post-process.
//
// ⚠️ AUCUN de ces opérateurs n'alimente le jeu. Voir l'en-tête de VoxelDensityOpStack.h.
// Ces opérateurs alimentent le jeu derrière l'opt-in décrit dans l'en-tête de VoxelDensityOpStack.h.
//
// FIDÉLITÉ / FIDELITY
// Chaque corps ci-dessous est une transcription LITTÉRALE du bloc correspondant de
@@ -20,7 +20,7 @@
#include "VoxelDensityOpStack.h"
#include "VoxelDensityPrimitives.h" // VF_ApplyOriginSpine / Seal / PassageCarving
#include "VoxelDensityPrimitives.h" // VF_ApplyOriginSpine / seals / PassageCarving
#include "VoxelCaveMorphology.h" // VoxelSDF::Capsule, VoxelHash
#include "VoxelGenerator.h" // VoxelGenLOD::Eff
#include "VoxelHeightOp.h" // FVoxelHeightStack — SurfaceWorld's two height stacks
@@ -1406,6 +1406,65 @@ namespace
float TopZ, BotZ, Thickness, Base;
};
//=========================================================================
// RÔLE 4 — STRUCTUREL : SCELLEMENT DE LA LIMITE XY (opérateur FORÇANT)
//=========================================================================
class FXYEdgeSealOp final : public IVoxelDensityOp
{
public:
explicit FXYEdgeSealOp(float InBase)
: Base(InBase) {}
EVoxelOpRole GetRole() const override { return EVoxelOpRole::StructuralPost; }
void PrepareChunk(const FVoxelOpContext& Ctx) override
{
// The settings are global, but PrepareChunk is the stack's one per-chunk hand-off.
// Keeping the prepared copy makes Eval hot and keeps the op independent of the asset.
WorldRadius = Ctx.WorldRadiusVoxels;
Thickness = Ctx.EdgeSealThickness;
}
void Eval(float X, float Y, float, FVoxelOpSample& InOut) const override
{
VF_ApplyXYEdgeSeal(InOut.Density, X, Y, WorldRadius, Thickness, Base);
}
/**
* The seal is forcing, not merely FillOnly: once every point in the box is at least one
* voxel into the ramp (or beyond the radius), the result is positive-solid regardless of
* all preceding source/carve operators.
*/
EVoxelTileClass ClassifyBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override
{
return VF_XYEdgeSealBoxIsForcedSolid(
VoxelBox, Ctx.WorldRadiusVoxels, Ctx.EdgeSealThickness, Base)
? EVoxelTileClass::AllSolid : EVoxelTileClass::Mixed;
}
float ForcedMarginOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override
{
return VF_XYEdgeSealForcedMarginOverBox(
VoxelBox, Ctx.WorldRadiusVoxels, Ctx.EdgeSealThickness, Base);
}
// In the inner world it is Identity; a box that reaches the radial band can only gain rock.
EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override
{
return VF_XYEdgeSealBoxTouchesBand(
VoxelBox, Ctx.WorldRadiusVoxels, Ctx.EdgeSealThickness)
? EVoxelOpEffect::FillOnly : EVoxelOpEffect::Identity;
}
bool IsXYPure() const override { return true; }
const TCHAR* DebugName() const override { return TEXT("XYEdgeSealOp"); }
private:
float Base = 8.0f;
mutable float WorldRadius = 8192.0f;
mutable float Thickness = 64.0f;
};
//=========================================================================
// RÔLE 4 — STRUCTUREL : CARVE DE PASSAGE
//=========================================================================
@@ -4294,12 +4353,15 @@ void FVoxelOpStack::AppendStructuralPost(float StrateTopWorldZ, float StrateBott
float SealThickness, float BaseDensity, float SpineRadius,
const UVoxelStrateManager* StrateManager)
{
// ORDRE NON NÉGOCIABLE, et c'est l'ordre que les six fonctions de densité utilisent déjà :
// la spine creuse l'intérieur (et ne touche JAMAIS les bandes de seal), le seal re-solidifie
// ses bandes, les passages percent tout — seal compris —, le joueur gagne en dernier.
// ORDRE NON NÉGOCIABLE : la spine creuse l'intérieur (et ne touche JAMAIS les bandes de seal),
// le seal vertical re-solidifie ses bandes, les passages percent les seals, puis la limite XY
// gagne sur tout ce qui précède. Ainsi, même un passage placé dans la rampe ou au-delà du rayon
// ne peut pas ouvrir la coque extérieure. Les éditions joueur restent le dernier post de
// GetDensityAt, hors de cette pile, comme avant.
Add(MakeUnique<FOriginSpineOp>(StrateTopWorldZ, StrateBottomWorldZ, SealThickness, BaseDensity, SpineRadius));
Add(MakeUnique<FBoundarySealOp>(StrateTopWorldZ, StrateBottomWorldZ, SealThickness, BaseDensity));
Add(MakeUnique<FPassageCarveOp>(StrateManager, BaseDensity, SealThickness));
Add(MakeUnique<FXYEdgeSealOp>(BaseDensity));
}
//=============================================================================
+58 -12
View File
@@ -13,7 +13,7 @@
#include "VoxelDiffLayer.h"
#include "VoxelBiomeDefinition.h"
#include "VoxelNoise.h" // T2.a: float, SIMD-batched gradient-noise core
#include "VoxelDensityPrimitives.h" // spine / seal / passage — shared with the operator stack
#include "VoxelDensityPrimitives.h" // spine / seals / passage — shared with the operator stack
#include "VoxelDensityOpStack.h" // OPSTACK Phase 1: the opt-in per-strate operator stack
#include "VoxelHeightOp.h" // IVoxelBiomeField — the adapter below implements it
#include "VoxelStats.h"
@@ -231,13 +231,13 @@ static float CellularNoise3D(const FVector& Position)
//=============================================================================
// Les CORPS de ces trois helpers ont déménagé dans Public/VoxelDensityPrimitives.h : la pile
// d'opérateurs (VoxelDensityOpStack) a besoin exactement des mêmes, et deux copies de trois
// INVARIANTS de monde (descente possible, seals qui tiennent, passages qui percent) finiraient par
// d'opérateurs (VoxelDensityOpStack) a besoin exactement des mêmes, et deux copies des INVARIANTS
// de monde (descente possible, seals qui tiennent, passages qui percent, bord fermé) finiraient par
// diverger. Ces trois lignes gardent les noms locaux pour que les ~20 sites d'appel ci-dessous ne
// bougent pas d'un caractère — le déplacement ne change AUCUN comportement.
//
// The BODIES moved to Public/VoxelDensityPrimitives.h; the operator stack needs the same three, and
// two copies of three world invariants would eventually drift. These forwarders keep the local names
// The BODIES moved to Public/VoxelDensityPrimitives.h; the operator stack needs the same primitives,
// and duplicate world invariants would eventually drift. These forwarders keep the local names
// so not one of the ~20 call sites below changes. No behavioural change.
//
// Convention INTERNE ici : positif = SOLIDE. La négation vers MC se fait sur le `return`.
@@ -558,10 +558,12 @@ UVoxelGenerator::UVoxelGenerator()
void UVoxelGenerator::InitializeSettings(const UVoxelSettings* Settings)
{
// Seul le seed est copié ici. Tout le reste (params de cave, transitions,
// blendings) vient des strate definitions via le StrateManager.
// Les paramètres globaux sont copiés ici une seule fois : le chemin voxel ne doit pas
// déréférencer l'asset de settings.
Seed = Settings ? Settings->Seed : 0;
OriginSpineRadius = Settings ? Settings->OriginSpineRadius : 14.0f;
WorldRadiusVoxels = Settings ? Settings->WorldRadiusVoxels : 0.0f;
EdgeSealThickness = Settings ? Settings->EdgeSealThickness : 64.0f;
}
float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) const
@@ -580,10 +582,12 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
{
// SOLID BEDROCK gap between two strates. The auto-carved passages still tunnel
// through it, but the (0,0) descent stays solid here so the player digs the gap
// to reach the next layer. No caves, no spine, no seal — just rock + passages.
// to reach the next layer. No caves, no spine, no vertical seal — just rock + passages;
// the global XY edge seal is applied below for bounded worlds.
float Density = 8.0f; // bedrock solidity (positive = solid)
const float ModSDF = StrateManager->EvaluateModifierSDF(WorldX, WorldY, WorldZ);
ApplyPassageCarving(Density, ModSDF, 8.0f, 0.0f);
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY, WorldRadiusVoxels, EdgeSealThickness, 8.0f);
Result = -Density;
}
else if (StrateManager)
@@ -687,6 +691,8 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
OpCtx.ChunkCoord = ChunkCoord;
OpCtx.Seed = (uint32)Seed;
OpCtx.LayoutVersion = LayoutVersion;
OpCtx.WorldRadiusVoxels = WorldRadiusVoxels;
OpCtx.EdgeSealThickness = EdgeSealThickness;
// ⚠️ LE MAPPING VIT DANS `VF_BuildOpStackForChunk` (haut de ce fichier) ET NULLE
// PART AILLEURS — `ClassifyTile` appelle la MÊME fabrique. Un verdict de tuile issu
@@ -807,6 +813,12 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
Result = GetDensityWithParams(WorldX, WorldY, WorldZ, FallbackParams, FallbackFP, 0);
}
// The edge is the final generated structural invariant. Passage carving is ordered before it
// in every archetype/stack, and this MC-facing pass also covers out-of-layout air plus any
// disturbance that might otherwise carve back into the shell. Player edits remain separate
// below and are deliberately still the user override path.
VF_ApplyXYEdgeSealMC(Result, WorldX, WorldY, WorldRadiusVoxels, EdgeSealThickness, 8.0f);
//=========================================================================
// PLAYER MODIFICATIONS (diff layer)
//=========================================================================
@@ -873,7 +885,8 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
// 4h. Pinch/Bottleneck: ellipsoidal passage narrowing for chokepoints
// 5. Worm tunnels: additional organic connectivity
// 6. Boundary seal: solid rock at strate top/bottom
// 7. Modifiers: passages between strates (punch through seals)
// 7. Modifiers: passages between strates (punch through vertical seals)
// 8. XY edge seal: global bounded-world shell wins over passages at the rim
//
// Convention: positive density = solid, negative = air (internally).
// At the end, we negate for the MC table (negative = solid there).
@@ -1945,6 +1958,10 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
ApplyPassageCarving(Density, ModSDF, Params.BaseDensity, Params.BoundarySealThickness);
}
// Fourth structural post: the XY edge wins over a passage near the rim.
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY,
WorldRadiusVoxels, EdgeSealThickness, Params.BaseDensity);
// Convention MC: négatif = solide, positif = air.
// La logique interne utilise positif = solide (plus lisible), donc on négate.
return -Density;
@@ -2146,7 +2163,7 @@ float UVoxelGenerator::GetSlabDensity(float WorldX, float WorldY, float WorldZ,
}
//=========================================================================
// STEP 5: BOUNDARY SEAL + STEP 6: PASSAGES
// STEP 5: BOUNDARY SEAL + STEP 6: PASSAGES + STEP 7: XY EDGE SEAL
//=========================================================================
// Même logique que TunnelNetwork — factorisée dans les helpers ci-dessus.
ApplyOriginSpine(Density, WorldX, WorldY, WorldZ,
@@ -2163,6 +2180,9 @@ float UVoxelGenerator::GetSlabDensity(float WorldX, float WorldY, float WorldZ,
ApplyPassageCarving(Density, ModSDF, Params.BaseDensity, Params.BoundarySealThickness);
}
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY,
WorldRadiusVoxels, EdgeSealThickness, Params.BaseDensity);
// Convention MC: négatif = solide.
return -Density;
}
@@ -2276,6 +2296,9 @@ float UVoxelGenerator::GetMazeDensity(float WorldX, float WorldY, float WorldZ,
ApplyPassageCarving(Density, ModSDF, Params.BaseDensity, Params.BoundarySealThickness);
}
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY,
WorldRadiusVoxels, EdgeSealThickness, Params.BaseDensity);
return -Density;
}
@@ -2536,6 +2559,9 @@ float UVoxelGenerator::SurfaceDensityFromColumn(float WorldX, float WorldY, floa
ApplyPassageCarving(Density, ModSDF, S.BaseDensity, S.BoundarySealThickness);
}
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY,
WorldRadiusVoxels, EdgeSealThickness, S.BaseDensity);
return -Density;
}
@@ -2681,8 +2707,6 @@ bool UVoxelGenerator::GetSurfaceHeightAt(float WorldX, float WorldY, int32 Chunk
EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis) const
{
if (!StrateManager) return EVoxelTileClass::Mixed;
// Mêmes clamps que GenerateMesh — le verdict doit couvrir le treillis réellement échantillonné.
Step = FMath::Max(1, Step);
const int32 CPA = FMath::Clamp(CellsPerAxis, 2, CHUNK_SIZE);
@@ -2708,6 +2732,20 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
if (DiffLayer->HasAnyModInChunkRange(MinChunk, MaxChunk)) return EVoxelTileClass::Mixed;
}
// The bounded shell is global: it also covers gaps and out-of-layout air, so prove it before
// consulting the strate layout. The final MC edge pass uses a positive base, hence 1.0 is a
// deliberately conservative proof input here; the stack op performs the same proof with the
// archetype base for its own sub-box.
const FBox TileVoxelBox(FVector((float)MinX, (float)MinY, (float)MinZ),
FVector((float)MaxX, (float)MaxY, (float)MaxZ));
if (VF_XYEdgeSealBoxIsForcedSolid(TileVoxelBox, WorldRadiusVoxels,
EdgeSealThickness, 1.0f))
{
return EVoxelTileClass::AllSolid;
}
if (!StrateManager) return EVoxelTileClass::Mixed;
bool bCanSolid = true; // "tout le treillis est solide" encore prouvable
bool bCanAir = true; // "tout le treillis est air" encore prouvable
@@ -3140,6 +3178,8 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
OpCtx.Seed = (uint32)Seed;
OpCtx.LayoutVersion = TC_LayoutVersion;
OpCtx.Step = Step;
OpCtx.WorldRadiusVoxels = WorldRadiusVoxels;
OpCtx.EdgeSealThickness = EdgeSealThickness;
FVoxelOpStack TileStack;
if (!VF_BuildOpStackForChunk(CaveType, Refs, Seed, OriginSpineRadius,
@@ -3940,6 +3980,9 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
ApplyPassageCarving(Density, ModSDF, Params.BaseDensity, Params.BoundarySealThickness);
}
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY,
WorldRadiusVoxels, EdgeSealThickness, Params.BaseDensity);
return -Density;
}
@@ -4097,5 +4140,8 @@ float UVoxelGenerator::GetFloatingIslandDensity(float WorldX, float WorldY, floa
ApplyPassageCarving(Density, ModSDF, Params.BaseDensity, Params.BoundarySealThickness);
}
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY,
WorldRadiusVoxels, EdgeSealThickness, Params.BaseDensity);
return -Density;
}