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:
1 parent
190ff381c0
commit
859a9b25c1
10 files changed
+2187
-48
No files matched your search
+15
-5
@@ -106,7 +106,7 @@ reference path for opt-out strates. See [OPSTACK-PLAN.md](OPSTACK-PLAN.md) for t
|
||||
| `EVoxelOpCombine` | `Replace`/`Union`(min)/`Subtract`(max)/`SmoothUnion`/`SmoothSubtract`/`Add`/`Mask`. Sign reminder: negative = solid, so "add solid" is `min`. |
|
||||
| `EVoxelOpEffect` | `Identity`/`CarveOnly`/`FillOnly`/`Both`. Conservative: `Both` is always safe, the wrong one is a hole. |
|
||||
| `FVoxelOpContext` | Chunk-constant inputs. Carries `LayoutVersion` plus the copied global `WorldRadiusVoxels`/`EdgeSealThickness`, so a new op cannot forget them. |
|
||||
| `IVoxelDensityOp` | `ChannelReads` / `ChannelWrites` / `IsAdditive` plus `PrepareChunk` / `Eval` / intrinsic and state-aware `EffectOverBox` / `MaxCarveOverBox` / `MaxFillOverBox` / `PropagateSdfOverBox` / `ClassifyBox` / `IsXYPure`. Box-query methods are never called per voxel; channel declarations are snapshotted during stack assembly. |
|
||||
| `IVoxelDensityOp` | `ChannelReads` / `ChannelWrites` / `IsAdditive` plus `RequiredResources` / `ProvidedResources`, `PrepareChunk` / `Eval`, and intrinsic/state-aware `EffectOverBox` / `MaxCarveOverBox` / `MaxFillOverBox` / `PropagateSdfOverBox` / `ClassifyBox` / `IsXYPure`. Box-query methods are never called per voxel; channel/resource declarations are snapshotted during stack assembly. |
|
||||
| `IVoxelDensityOp::ClassifyBox` | ⚠️ **not source-only.** Forcing ops (the vertical seal and XY edge seal inside their proven bands) overwrite the input entirely, which pure direction cannot express. |
|
||||
| `FVoxelOpSample` | The state threaded through the stack: **two** channels, `Density` (INTERNAL convention, **positive = SOLID**, negated to MC once by the caller) and `Sdf` (standard SDF, negative = inside). ⚠️ `min()` therefore means opposite things on the two channels. |
|
||||
| `FVoxelBoxHypotheses` + `FVoxelBoxSdfInterval` + `VF_ForceHypotheses` / `VF_FoldEffect` / `VF_FoldOp` | The fold that turns a stack into an `EVoxelTileClass`, carrying the SDF interval between independent writers and consumers. Unknown is biased toward `Mixed`; the mapping and soundness rule are written out in the header. |
|
||||
@@ -144,7 +144,7 @@ bit. They are port-correctness oracles, not fidelity checks: the acceptance bar
|
||||
| Symbol | Role | Notes |
|
||||
|--------|------|-------|
|
||||
| `FVoxelOpStack` | — | Ordered move-only list of `TUniquePtr` entries. `Add` snapshots channel metadata; `PrepareChunk` / `EvalInternal` / `EvalMC` / `ClassifyBox` (the fold lets a later forcing post overwrite a dead hypothesis). |
|
||||
| `FVoxelOpStack::ValidateChannelOrder` | — | Assembly/diagnostic-only channel DAG validator. It requires prior producers, rejects write-only clobbers and invalid additive declarations, and permits the one root SDF identity fold used by Maze. `VoxelForge.OpStack.ChannelDAG` validates all 8 shipping stacks. |
|
||||
| `FVoxelOpStack::ValidateChannelOrder` | — | Assembly/diagnostic-only channel/resource DAG validator. It requires prior producers, rejects write-only clobbers and invalid additive declarations, and requires room/shaft/surface state providers before consumers. It permits the one root SDF identity fold used by Maze. `VoxelForge.OpStack.ChannelDAG` validates all 8 shipping stacks. |
|
||||
| `FVoxelOpStack::AppendStructuralPost` | 4 | Appends spine → vertical seal → passage → XY edge seal **in that fixed order**. An author cannot omit or reorder them. The diff layer is NOT here yet — it still lives in `GetDensityAt` after the MC negate, with disturbances. |
|
||||
| `VoxelDensityOps::MakeConstantRockSource` | 1 | `Density = BaseDensity`. `ClassifyBox` → **AllSolid**, exact and free. Shared by TunnelNetwork, Maze, VerticalShafts and bedrock gaps. Class is `FConstantFieldSource` (one class, two factories). |
|
||||
| `VoxelDensityOps::MakeConstantVoidSource` | 1 | The **same class, negated**: `Density = -BaseDensity`, and `ClassifyBox` → **AllAir** — the first source in the plugin that can prove it. FloatingIslands' root; that verdict is what makes a mostly-empty island strate skippable. |
|
||||
@@ -438,8 +438,8 @@ One asset = one biome: identity + `DebugColor`, climate placement box (`ReliefMi
|
||||
atmosphere override, `WaterMaterial`, `MaterialPaletteIndex` (F6 — baked to vertex colour, §8.15)), `GameplayTags`. Referenced from
|
||||
`UVoxelStrateDefinition::Biomes[]`. Generator-agnostic (surface biomes now, cave biomes later). §8.14.
|
||||
|
||||
**`Public/VoxelStrateComposer.h` + `Private/VoxelStrateComposer.cpp`** (Tier 4a) — offline
|
||||
`FVoxelStrateCorpus` and `VF_RollStrateParams`. `LoadFromAssetRegistry` enumerates every project
|
||||
**`Public/VoxelStrateComposer.h` + `Private/VoxelStrateComposer.cpp`** (Tier 4a/4b) — offline
|
||||
`FVoxelStrateCorpus`, `VF_RollStrateParams`, and `VF_RollStrateStructure`. `LoadFromAssetRegistry` enumerates every project
|
||||
`UVoxelStrateDefinition` through the Asset Registry, excludes `Saved/Autosaves` and `Saved/Cooked`
|
||||
copies, and adds one `VoxelStrateTypes.h` default vector for each of the eight exact archetypes.
|
||||
The corpus is grouped by exact `ECaveGeneratorType`: the 2026-09-04 run loaded **4 project
|
||||
@@ -454,6 +454,15 @@ from the dominant parent; integer/enum SNAP fields are not jittered. This API is
|
||||
runtime generation. The settings audit found **one unique path**, `/Game/VoxelForge/DA_Strate3`,
|
||||
which explains the old one-vector corpus; the project assets were never in that pool.
|
||||
|
||||
`FVoxelOpStackRecipe` is the serialisable Tier 4b manifest: root polarity, root/source/conversion
|
||||
class IDs, an ordered modifier ID list, and one native parameter-family ID per entry. Posts are not
|
||||
represented in the manifest; `VF_BuildStackFromRecipe` appends spine, vertical seal, passage carve,
|
||||
and XY edge seal in the fixed order. The roller derives modifier legality from the op channel and
|
||||
resource declarations, rolls 4–8 unique modifiers, and validates the recipe before materialising
|
||||
it. `RequiredResources` / `ProvidedResources` close the room-state hole that channel masks alone
|
||||
could not express. `VoxelForgeComposerStructureRollTest` rolls 64 candidates, measures them through
|
||||
the offline sampler, and brute-forces every uniform box verdict from the novel stack itself.
|
||||
|
||||
### 3.9 Player edits — `Public/VoxelDiffLayer.h` + `.cpp`
|
||||
`UVoxelDiffLayer : UObject` (h:77). Stores `FVoxelModification` (h:43: Center/Radius/Strength;
|
||||
**negative Strength = carve, positive = fill**) grouped by chunk in `TMap ChunkMods`.
|
||||
@@ -517,7 +526,8 @@ The plugin's first tests (`OPSTACK-PLAN.md` Phase 0.5). Run them from the editor
|
||||
| `VoxelForgeOpStackIslandTest.cpp` | `VoxelForge.OpStack.FloatingIslandEquivalence` | The port that runs the stack **backwards** — void + fill vs rock + carve, same classes with the opposite sign. Counts interior-solid and open-void samples separately (on this archetype an aggregate "N solid" is dominated by the seal bands and says nothing about the islands). Counts `AllSolid` and `AllAir` verdicts **separately** too: `AllAir` is the one no cave archetype could ever prove, and it is the entire perf argument here. |
|
||||
| `VoxelForgeOpStackMazeTest.cpp` | `VoxelForge.OpStack.MazeEquivalence` | **Phase 1's load-bearing test.** The 7-op Maze stack vs `GetMazeDensity` over 20k points (aiming for bit-identity; a side-of-iso disagreement is the hard fail), plus purity across workers and brute force on every box verdict the stack emits. Reports how many tiles the stack can prove uniform — today's `ClassifyTile` proves **zero** for any cave archetype. |
|
||||
| `VoxelForgeStrateParamCoverageTest.cpp` | `VoxelForge.Determinism.StrateParamBlendCoverage` | **The X-macro guard** (added 2026-08-17). `FStrateGenerationParams::Lerp` blends the hand-written `VF_STRATE_PARAM_FIELDS` list, **not** the struct — so a field added to one and not the other compiles, tests green, and silently takes its **default** inside every Gradient/Interleaved transition band. This expands the X-macro a **third** way (after LERP and SNAP), into a name list, and diffs it against the struct's UObject reflection. Pure shape test: no fixture, no world, instant. `GExemptFieldNames` is **empty** — every reflected field is covered today, and any exemption must be written down as a decision. Stakes rise with the world composer, which intends to invent parameter sets through this same `Lerp` (`COMPOSER-NOTES.md`). |
|
||||
| `VoxelForgeComposerParameterRollTest.cpp` | `VoxelForge.Composer.ParameterRoll` | Asset-Registry corpus audit + complete per-archetype spread/exclusion/clamp table; asserts bit-identical deterministic rerolls and same-archetype parents; measures 64 transient candidate strates with `VF_MeasureStrate` plus the exact unsnapped arrival→departure law; brute-forces every rolled production box verdict. The final full-suite run: **59/64 survival (92.2%)**, **1,336 Mixed + 549 AllSolid + 675 AllAir = 1,224 proved boxes**, **1,629,144 lattice voxels checked, 0 violations**, 112.574 s total. |
|
||||
| `VoxelForgeComposerParameterRollTest.cpp` | `VoxelForge.Composer.ParameterRoll` | Asset-Registry corpus audit + complete per-archetype spread/exclusion/clamp table; asserts bit-identical deterministic rerolls and same-archetype parents; measures 64 transient candidate strates with `VF_MeasureStrate` plus the exact unsnapped arrival→departure law; brute-forces every rolled production box verdict. Current run: **59/64 survival (92.2%)**, **1,336 Mixed + 549 AllSolid + 675 AllAir = 1,224 proved boxes**, **1,629,144 lattice voxels checked, 0 violations**, 114.845 s total. |
|
||||
| `VoxelForgeComposerStructureRollTest.cpp` | `VoxelForge.Composer.StructureRoll` | Rolls root polarity → legal shape source → polarity-derived conversion → 4–8 declaration-legal modifiers → mandatory structural posts; blends the six native parameter families independently, measures 64 novel stacks, checks exact arrival→departure connectivity, rerolls every recipe/stack for determinism, and brute-forces every uniform box verdict. Focused run: **46/64 survival (71.9%)**, **64 distinct recipes**, **RoomGraph 15 / Lattice 19 / Shaft 8 / Island 14 / Noise 8**, **k=4:11 / 5:13 / 6:11 / 7:10 / 8:19**, **489 proved boxes / 650,859 voxels / 0 violations**. |
|
||||
| `VoxelForgeLayoutOrderIndependenceTest.cpp` | `VoxelForge.Determinism.LayoutOrderIndependence` | Builds a known transient soft-pointer pool, then rebuilds it in original, reversed, and swapped orders. Requires a non-empty layout and passage set, and compares every slot's definition/Z/height plus passage endpoints, radius, type, control geometry, and bounds bit-for-bit. |
|
||||
| `VoxelForgePassageOpenSpaceTest.cpp` | `VoxelForge.Determinism.PassageLandsInOpenSpace` | Uses the real fixture density path to check every generated inter-strate passage whose destination query answers: a 16-point ring outside the mouth's carve/blend band has at least half its samples in destination air, and the endpoint matches the pure open-point result within the mouth's float envelope. This is a connectivity proxy, not a flood-fill proof. Reports checked passages and false/unanswerable archetypes; fails if it inspects zero passages. |
|
||||
| `VoxelForgeStrateConnectivityTest.cpp` | `VoxelForge.Generation.StrateConnectivity` / `VoxelForge.Generation.StrateConnectivityRefinement` / `VoxelForge.Generation.VerticalShaftSeamFreedom` | Bounded strate metrics with density-polarity and solid-gap controls, deterministic route rechecks, and refinement sweeps. The refinement test fits the measurement AABB to each arrival/departure mouth pair, reports exact before/after seed-6 cell counts, reruns every negative with doubled margin, measures tree orphan candidates/path reachability and roughness-bubble proxies, asserts axis landings, checks source/mirror physical paths, and requires 16/16 effective arrival→departure results across the 16 VerticalShafts seeds. Margin-binding negatives are reported as measurement limits, never as gap findings. The seam test re-evaluates cell-boundary positions after warming distinct neighbouring chunk contexts in both legacy and operator-stack paths. |
|
||||
|
||||
+41
-4
@@ -160,6 +160,40 @@ corpus is small.
|
||||
**⇒ Rule: vary STRUCTURE aggressively, vary PARAMETERS conservatively.** Shift the ratio as the corpus
|
||||
grows.
|
||||
|
||||
**Tier 4b implementation (2026-09-04):** `FVoxelOpStackRecipe` is the shipped-manifest shape: it
|
||||
stores the one-bit `RootPolarity`, root/source/conversion `EVoxelStrateOpClass` IDs, an ordered list
|
||||
of modifier IDs, and an `EVoxelStrateParamBlock` on every entry. The four structural posts are
|
||||
deliberately not a field in the recipe. `VF_BuildStackFromRecipe` appends spine, vertical seal,
|
||||
passage carving, and XY edge seal in that fixed order, so an invented strate cannot omit primordial
|
||||
law. The conversion ID is checked against polarity rather than rolled independently.
|
||||
|
||||
The roller's modifier catalogue is a deterministic array of declared ops, not a compatibility list.
|
||||
It asks each op for its channel reads/writes, additive contract, required resources, and provided
|
||||
resources, then draws 4–8 unique entries from the legal remainder. The channel declarations exposed
|
||||
a real missing dependency: `FCaveTerraceMod` and the other room-relative cave modifiers use the
|
||||
`FRoomGraphSource` room cache through a non-owning pointer, even though their channel masks only said
|
||||
`Density`. `FShaftLedgeMod` similarly consumes shaft state, and `FOverhangShelfMod` consumes the
|
||||
surface-column state. `RequiredResources` / `ProvidedResources` now declare and validate those
|
||||
dependencies (RoomGeometry, ShaftGeometry, SurfaceColumn); the roller cannot place such an op before
|
||||
its provider. `FCaveRoughnessMod` and `FWormFieldSource` were checked and do not require room state:
|
||||
they read only their declared channels and parameters.
|
||||
|
||||
The parameter decision is deliberately visible: structure and parameter rolls are separate. Each
|
||||
candidate rolls the six native family blocks independently from the exact-archetype corpus using the
|
||||
Tier 4a conservative blend/jitter procedure. An op consumes the block named on its recipe entry;
|
||||
structural posts consume the shape source's family block. There is no unsafe 73-field cross-family
|
||||
blend. The current `SurfaceWorld` block is still rolled and carried even when a structure has no
|
||||
surface-column op, so the recipe remains a complete family-parameter record.
|
||||
|
||||
The focused 64-candidate run produced 64 distinct recipes, 0 invalid recipes, and 0 box-verdict
|
||||
violations. It checked 650,859 voxels across 489 proved boxes. Survival was 46/64 (71.9%) under the
|
||||
same non-vacuous, largest-share ≥ 0.50, exact `Connected` criteria as Tier 4a; that lower rate is an
|
||||
expected consequence of aggressive structural exploration, not a tuning target. The test also
|
||||
rerolls every recipe and rebuilt stack, checks `WorldRadiusVoxels == 0`, and feeds the novel stack
|
||||
through the offline Tier 2 sampler; no runtime generation path calls this API. The final focused
|
||||
test took **76.466 s**. The complete `VoxelForge` namespace then passed **24/24 tests (0 failed,
|
||||
0 not run)** in **339.797 s**.
|
||||
|
||||
### 3.3 Parameters: the range problem
|
||||
|
||||
**Verified facts, not remembered:** `FStrateGenerationParams` has **73 scalar fields** (+1 bool) — not
|
||||
@@ -300,8 +334,8 @@ produced **59/64 survivors (92.2%)**. There were 60 non-vacuous candidates, 60 w
|
||||
component share ≥ 0.50, 63 exact unsnapped arrival→departure law passes, and no roll failures.
|
||||
The complete 64-row table is emitted by `VoxelForge.Composer.ParameterRoll`; each row reports
|
||||
archetype, weighted parents, air fraction, largest share, walkable fraction, feature scale, and
|
||||
arrival→departure verdict. The final full-suite run took **112.574 s** total (**0.015 s** corpus
|
||||
load, **112.556 s** roll/measurement).
|
||||
arrival→departure verdict. The current run took **114.845 s** total (**0.014 s** corpus load,
|
||||
**114.827 s** roll/measurement).
|
||||
|
||||
The §6.2 check is no longer vacuous: **1,224 boxes proved**, **1,629,144 voxels checked**,
|
||||
**0 violations**. Its verdict mix was 1,336 Mixed, 549 AllSolid, and 675 AllAir; the law check
|
||||
@@ -650,8 +684,11 @@ roughness and should stay late in the stack.
|
||||
- ✅ **Parameter roll (§3.3, Tier 4a)** — corpus spread + deterministic blend/jitter implementation
|
||||
and the 64-candidate measurement pass are built; the first run exposed a one-vector settings corpus
|
||||
and a vacuous cave box-verdict scan, both recorded above.
|
||||
- ⬜ Structure roll (§3.2) · reject-and-resample driven by Tier 2 (§3.5) · the **offline season
|
||||
pipeline** with Jahni's review and veto (§3.1).
|
||||
- ✅ **Structure roll (§3.2, Tier 4b)** — serialisable recipes, root polarity, declaration-derived
|
||||
resource-safe modifier legality, mandatory posts, 64-candidate measurement, and direct box-verdict
|
||||
brute force are built; focused result is 46/64 survival with 0 invalid recipes and 0 violations.
|
||||
- ⬜ Reject-and-resample driven by Tier 2 (§3.5) · the **offline season pipeline** with Jahni's review
|
||||
and veto (§3.1).
|
||||
|
||||
### Tier 5 — the long game
|
||||
**Promotion** (good strates rejoin the corpus) · theme and tag draws for materials, creatures and audio ·
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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."));
|
||||
}
|
||||
}
|
||||
int64 StrateBottomZ = 0;
|
||||
int64 StrateTopZ = 0;
|
||||
if (bUseExplicitBounds)
|
||||
{
|
||||
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 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."));
|
||||
}
|
||||
|
||||
const int64 StrateBottomZ = static_cast<int64>(Slot.BottomChunkZ) * CHUNK_SIZE;
|
||||
const int64 StrateTopZ = (static_cast<int64>(Slot.TopChunkZ) + 1) * CHUNK_SIZE;
|
||||
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."));
|
||||
}
|
||||
|
||||
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);
|
||||
const FStrateGenerationParams RepresentativeParams =
|
||||
Manager.GetGenerationParams(RepresentativeChunk);
|
||||
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;
|
||||
}
|
||||
@@ -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);
|
||||
Reference in new issue
Block a user