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

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

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

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

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

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

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

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

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

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

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

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

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

No files matched your search

+15 -5
View File
@@ -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`. | | `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. | | `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. | | `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. | | `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. | | `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. | | `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 | | 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` | — | 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. | | `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::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. | | `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 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. `UVoxelStrateDefinition::Biomes[]`. Generator-agnostic (surface biomes now, cave biomes later). §8.14.
**`Public/VoxelStrateComposer.h` + `Private/VoxelStrateComposer.cpp`** (Tier 4a) — offline **`Public/VoxelStrateComposer.h` + `Private/VoxelStrateComposer.cpp`** (Tier 4a/4b) — offline
`FVoxelStrateCorpus` and `VF_RollStrateParams`. `LoadFromAssetRegistry` enumerates every project `FVoxelStrateCorpus`, `VF_RollStrateParams`, and `VF_RollStrateStructure`. `LoadFromAssetRegistry` enumerates every project
`UVoxelStrateDefinition` through the Asset Registry, excludes `Saved/Autosaves` and `Saved/Cooked` `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. 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 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`, 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. 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` ### 3.9 Player edits — `Public/VoxelDiffLayer.h` + `.cpp`
`UVoxelDiffLayer : UObject` (h:77). Stores `FVoxelModification` (h:43: Center/Radius/Strength; `UVoxelDiffLayer : UObject` (h:77). Stores `FVoxelModification` (h:43: Center/Radius/Strength;
**negative Strength = carve, positive = fill**) grouped by chunk in `TMap ChunkMods`. **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. | | `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. | | `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`). | | `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. | | `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. | | `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. | | `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
View File
@@ -160,6 +160,40 @@ corpus is small.
**⇒ Rule: vary STRUCTURE aggressively, vary PARAMETERS conservatively.** Shift the ratio as the corpus **⇒ Rule: vary STRUCTURE aggressively, vary PARAMETERS conservatively.** Shift the ratio as the corpus
grows. 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 ### 3.3 Parameters: the range problem
**Verified facts, not remembered:** `FStrateGenerationParams` has **73 scalar fields** (+1 bool) — not **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. 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 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 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 arrival→departure verdict. The current run took **114.845 s** total (**0.014 s** corpus load,
load, **112.556 s** roll/measurement). **114.827 s** roll/measurement).
The §6.2 check is no longer vacuous: **1,224 boxes proved**, **1,629,144 voxels checked**, 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 **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 - ✅ **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 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. 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 - ✅ **Structure roll (§3.2, Tier 4b)** — serialisable recipes, root polarity, declaration-derived
pipeline** with Jahni's review and veto (§3.1). 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 ### Tier 5 — the long game
**Promotion** (good strates rejoin the corpus) · theme and tag draws for materials, creatures and audio · **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. // decomposition is right" into a proof.
#include "VoxelDensityOpStack.h" #include "VoxelDensityOpStack.h"
#include "VoxelStrateComposer.h"
#include "VoxelDensityPrimitives.h" // VF_ApplyOriginSpine / seals / PassageCarving #include "VoxelDensityPrimitives.h" // VF_ApplyOriginSpine / seals / PassageCarving
#include "VoxelCaveMorphology.h" // VoxelSDF::Capsule, VoxelHash #include "VoxelCaveMorphology.h" // VoxelSDF::Capsule, VoxelHash
@@ -296,6 +297,130 @@ namespace
float Value; 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 // RÔLE 1 — SOURCE : COULOIRS SUR TREILLIS 3D / 3D LATTICE CORRIDORS
//========================================================================= //=========================================================================
@@ -950,6 +1075,7 @@ namespace
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; } EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::None; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::None; }
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
EVoxelOpResourceMask ProvidedResources() const override { return VoxelOpResources::SurfaceColumn; }
bool IsAdditive() const override { return false; } bool IsAdditive() const override { return false; }
bool IsXYPure() const override { return false; } // voir le bloc ci-dessus bool IsXYPure() const override { return false; } // voir le bloc ci-dessus
@@ -1081,6 +1207,7 @@ namespace
EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; } EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; }
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density; }
EVoxelOpChannelMask ChannelWrites() 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; } bool IsAdditive() const override { return false; }
void PrepareChunk(const FVoxelOpContext&) override {} void PrepareChunk(const FVoxelOpContext&) override {}
bool IsXYPure() const override { return false; } // franchement non : voir `Frac` bool IsXYPure() const override { return false; } // franchement non : voir `Frac`
@@ -1771,6 +1898,7 @@ namespace
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; } EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::None; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::None; }
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Sdf; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Sdf; }
EVoxelOpResourceMask ProvidedResources() const override { return VoxelOpResources::ShaftGeometry; }
bool IsAdditive() const override { return false; } bool IsAdditive() const override { return false; }
void PrepareChunk(const FVoxelOpContext&) override {} void PrepareChunk(const FVoxelOpContext&) override {}
@@ -2194,6 +2322,7 @@ namespace
return VoxelOpChannels::Density | VoxelOpChannels::Sdf; return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
} }
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::ShaftGeometry; }
bool IsAdditive() const override { return false; } bool IsAdditive() const override { return false; }
void PrepareChunk(const FVoxelOpContext&) override {} void PrepareChunk(const FVoxelOpContext&) override {}
@@ -2577,6 +2706,7 @@ namespace
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; } EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::None; } EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::None; }
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Sdf; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Sdf; }
EVoxelOpResourceMask ProvidedResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return false; } bool IsAdditive() const override { return false; }
void PrepareChunk(const FVoxelOpContext& Ctx) override void PrepareChunk(const FVoxelOpContext& Ctx) override
@@ -3606,6 +3736,7 @@ namespace
return VoxelOpChannels::Density | VoxelOpChannels::Sdf; return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
} }
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; } bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {} void PrepareChunk(const FVoxelOpContext&) override {}
@@ -3715,6 +3846,7 @@ namespace
return VoxelOpChannels::Density | VoxelOpChannels::Sdf; return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
} }
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; } bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {} void PrepareChunk(const FVoxelOpContext&) override {}
@@ -3803,6 +3935,7 @@ namespace
return VoxelOpChannels::Density | VoxelOpChannels::Sdf; return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
} }
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; } bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {} void PrepareChunk(const FVoxelOpContext&) override {}
@@ -3886,6 +4019,7 @@ namespace
return VoxelOpChannels::Density | VoxelOpChannels::Sdf; return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
} }
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; } bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {} void PrepareChunk(const FVoxelOpContext&) override {}
@@ -3986,6 +4120,7 @@ namespace
return VoxelOpChannels::Density | VoxelOpChannels::Sdf; return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
} }
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; } bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {} void PrepareChunk(const FVoxelOpContext&) override {}
@@ -4060,6 +4195,7 @@ namespace
return VoxelOpChannels::Density | VoxelOpChannels::Sdf; return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
} }
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; } bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {} void PrepareChunk(const FVoxelOpContext&) override {}
@@ -4151,6 +4287,7 @@ namespace
return VoxelOpChannels::Density | VoxelOpChannels::Sdf; return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
} }
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; } bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {} void PrepareChunk(const FVoxelOpContext&) override {}
@@ -4267,6 +4404,7 @@ namespace
return VoxelOpChannels::Density | VoxelOpChannels::Sdf; return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
} }
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; } bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {} void PrepareChunk(const FVoxelOpContext&) override {}
@@ -4330,6 +4468,7 @@ namespace
return VoxelOpChannels::Density | VoxelOpChannels::Sdf; return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
} }
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; } bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {} void PrepareChunk(const FVoxelOpContext&) override {}
@@ -4437,6 +4576,7 @@ namespace
return VoxelOpChannels::Density | VoxelOpChannels::Sdf; return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
} }
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; } bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {} void PrepareChunk(const FVoxelOpContext&) override {}
@@ -4556,6 +4696,7 @@ namespace
return VoxelOpChannels::Density | VoxelOpChannels::Sdf; return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
} }
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; } bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {} void PrepareChunk(const FVoxelOpContext&) override {}
@@ -4794,6 +4935,23 @@ bool FVoxelOpStack::ValidateChannelOrder(FString* OutError) const
return Result.IsEmpty() ? TEXT("none") : Result; 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 auto Fail = [&](int32 Index, const FOpEntry* Entry, const TCHAR* Rule) -> bool
{ {
if (OutError != nullptr) if (OutError != nullptr)
@@ -4803,8 +4961,10 @@ bool FVoxelOpStack::ValidateChannelOrder(FString* OutError) const
const EVoxelOpChannelMask Reads = Entry != nullptr ? Entry->Reads : VoxelOpChannels::None; const EVoxelOpChannelMask Reads = Entry != nullptr ? Entry->Reads : VoxelOpChannels::None;
const EVoxelOpChannelMask Writes = Entry != nullptr ? Entry->Writes : VoxelOpChannels::None; const EVoxelOpChannelMask Writes = Entry != nullptr ? Entry->Writes : VoxelOpChannels::None;
*OutError = FString::Printf( *OutError = FString::Printf(
TEXT("op %d (%s) violates channel DAG: %s; reads=[%s], writes=[%s]"), TEXT("op %d (%s) violates stack DAG: %s; reads=[%s], writes=[%s], requires=[%s], provides=[%s]"),
Index, Name, Rule, *DescribeMask(Reads), *DescribeMask(Writes)); Index, Name, Rule, *DescribeMask(Reads), *DescribeMask(Writes),
*DescribeResourceMask(Entry != nullptr ? Entry->RequiredResources : VoxelOpResources::None),
*DescribeResourceMask(Entry != nullptr ? Entry->ProvidedResources : VoxelOpResources::None));
} }
return false; 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 // The two array slots are deliberately explicit: FVoxelOpSample has exactly two fields, and
// adding a third field requires extending EVoxelOpChannel and this validator together. // adding a third field requires extending EVoxelOpChannel and this validator together.
int32 LastWriter[2] = { INDEX_NONE, INDEX_NONE }; int32 LastWriter[2] = { INDEX_NONE, INDEX_NONE };
EVoxelOpResourceMask AvailableResources = VoxelOpResources::None;
auto ChannelIndex = [](EVoxelOpChannelMask Channel) -> int32 auto ChannelIndex = [](EVoxelOpChannelMask Channel) -> int32
{ {
@@ -4823,6 +4984,21 @@ bool FVoxelOpStack::ValidateChannelOrder(FString* OutError) const
const FOpEntry& Entry = Ops[Index]; const FOpEntry& Entry = Ops[Index];
if (Entry.Op.Get() == nullptr) { return Fail(Index, &Entry, TEXT("null operator")); } 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) if ((Entry.Reads & VoxelOpChannels::All) != Entry.Reads)
{ {
return Fail(Index, &Entry, TEXT("reads an unknown sample channel")); return Fail(Index, &Entry, TEXT("reads an unknown sample channel"));
@@ -4883,6 +5059,8 @@ bool FVoxelOpStack::ValidateChannelOrder(FString* OutError) const
LastWriter[ChannelIndex(Channel)] = Index; LastWriter[ChannelIndex(Channel)] = Index;
} }
} }
AvailableResources |= Entry.ProvidedResources;
} }
return true; return true;
@@ -4909,6 +5087,117 @@ void FVoxelOpStack::AppendStructuralPost(float StrateTopWorldZ, float StrateBott
namespace VoxelDensityOps 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) TUniquePtr<IVoxelDensityOp> MakeConstantRockSource(float BaseDensity)
{ {
return MakeUnique<FConstantFieldSource>(BaseDensity); return MakeUnique<FConstantFieldSource>(BaseDensity);
@@ -5149,3 +5438,549 @@ namespace VoxelDensityOps
P.BoundarySealThickness, P.BaseDensity, SpineRadius, StrateManager); 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 "VoxelSettings.h"
#include "VoxelStrateDefinition.h" #include "VoxelStrateDefinition.h"
#include "VoxelDensityOpStack.h"
#include <type_traits> #include <type_traits>
#include <utility> #include <utility>
@@ -992,7 +993,8 @@ namespace
} }
FVoxelStrateRollInfo VF_RollInternal(const FVoxelStrateCorpus& Corpus, FVoxelStrateRollInfo VF_RollInternal(const FVoxelStrateCorpus& Corpus,
int32 Seed, int32 Index) int32 Seed, int32 Index,
const ECaveGeneratorType* ForcedArchetype = nullptr)
{ {
FVoxelStrateRollInfo Result; FVoxelStrateRollInfo Result;
if (!Corpus.IsValid()) if (!Corpus.IsValid())
@@ -1043,7 +1045,24 @@ namespace
} }
int32 GroupIndex = 0; 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; float Cursor = Rng.FRand() * TotalGroupWeight;
for (int32 CandidateGroup = 0; CandidateGroup < Groups.Num(); ++CandidateGroup) for (int32 CandidateGroup = 0; CandidateGroup < Groups.Num(); ++CandidateGroup)
@@ -1141,6 +1160,165 @@ namespace
Result.bValid = true; Result.bValid = true;
return Result; 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() const TArray<FVoxelStrateFieldExclusion>& FVoxelStrateCorpus::GetNonTunableFields()
@@ -1849,8 +2027,164 @@ FVoxelStrateRollInfo VF_RollStrateParamsDetailed(const FVoxelStrateCorpus& Corpu
return Result; 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, FStrateGenerationParams VF_RollStrateParams(const FVoxelStrateCorpus& Corpus,
int32 Seed, int32 Index) int32 Seed, int32 Index)
{ {
return VF_RollStrateParamsDetailed(Corpus, Seed, Index).Params; return VF_RollStrateParamsDetailed(Corpus, Seed, Index).Params;
} }
namespace
{
const TCHAR* VF_RecipeOpCode(EVoxelStrateOpClass OpClass)
{
switch (OpClass)
{
case EVoxelStrateOpClass::ConstantRockSource: return TEXT("rk");
case EVoxelStrateOpClass::ConstantVoidSource: return TEXT("vd");
case EVoxelStrateOpClass::RoomGraphSource: return TEXT("rg");
case EVoxelStrateOpClass::LatticeCorridorSource: return TEXT("lt");
case EVoxelStrateOpClass::ShaftFieldSource: return TEXT("sh");
case EVoxelStrateOpClass::IslandBlobSource: return TEXT("is");
case EVoxelStrateOpClass::NoiseRibbonSource: return TEXT("nr");
case EVoxelStrateOpClass::SdfRoughnessMod: return TEXT("sr");
case EVoxelStrateOpClass::SdfCarve: return TEXT("cv");
case EVoxelStrateOpClass::SdfFill: return TEXT("fl");
case EVoxelStrateOpClass::GridColumnMod: return TEXT("gc");
case EVoxelStrateOpClass::CaveRoughnessMod: return TEXT("cr");
case EVoxelStrateOpClass::CaveTerraceMod: return TEXT("tr");
case EVoxelStrateOpClass::LayerLineMod: return TEXT("ll");
case EVoxelStrateOpClass::RibbingMod: return TEXT("rb");
case EVoxelStrateOpClass::CaveOverhangMod: return TEXT("co");
case EVoxelStrateOpClass::CaveCliffMod: return TEXT("cc");
case EVoxelStrateOpClass::ScallopMod: return TEXT("sc");
case EVoxelStrateOpClass::CaveArchMod: return TEXT("ar");
case EVoxelStrateOpClass::RoomColumnMod: return TEXT("rc");
case EVoxelStrateOpClass::DomeMod: return TEXT("dm");
case EVoxelStrateOpClass::PinchMod: return TEXT("pn");
case EVoxelStrateOpClass::FloorBiasMod: return TEXT("fb");
case EVoxelStrateOpClass::WormFieldSource: return TEXT("wm");
case EVoxelStrateOpClass::ShaftLedgeMod: return TEXT("sl");
case EVoxelStrateOpClass::DensityNoiseCarveMod: return TEXT("nc");
case EVoxelStrateOpClass::DensityNoiseFillMod: return TEXT("nf");
default: return TEXT("??");
}
}
const TCHAR* VF_RecipeBlockCode(EVoxelStrateParamBlock Block)
{
switch (Block)
{
case EVoxelStrateParamBlock::TunnelNetwork: return TEXT("T");
case EVoxelStrateParamBlock::Slab: return TEXT("B");
case EVoxelStrateParamBlock::Maze: return TEXT("M");
case EVoxelStrateParamBlock::Surface: return TEXT("S");
case EVoxelStrateParamBlock::VerticalShaft: return TEXT("V");
case EVoxelStrateParamBlock::FloatingIsland: return TEXT("I");
default: return TEXT("-");
}
}
FString VF_FormatRecipeEntry(const FVoxelOpRecipeEntry& Entry)
{
return FString::Printf(TEXT("%s/%s"), VF_RecipeOpCode(Entry.OpClass),
VF_RecipeBlockCode(Entry.ParamBlock));
}
}
FVoxelOpStackRecipe VF_RollStrateStructure(int32 Seed, int32 Index)
{
const FVoxelOpStackRecipe Recipe = VF_RollStructureInternal(Seed, Index);
#if DO_CHECK
const FVoxelOpStackRecipe Repeat = VF_RollStructureInternal(Seed, Index);
checkf(VF_AreStrateStructureRecipesIdentical(Recipe, Repeat),
TEXT("VF_RollStrateStructure lost determinism for the same seed/index."));
#endif
return Recipe;
}
FString VF_FormatStrateStructureRecipe(const FVoxelOpStackRecipe& Recipe)
{
FString Modifiers;
for (int32 Index = 0; Index < Recipe.Modifiers.Num(); ++Index)
{
if (Index > 0) { Modifiers += TEXT(","); }
Modifiers += VF_FormatRecipeEntry(Recipe.Modifiers[Index]);
}
return FString::Printf(TEXT("%s|r=%s|s=%s|x=%s|m=[%s]|posts=%s"),
Recipe.RootPolarity == EVoxelStrateRootPolarity::VoidFill ? TEXT("VF") : TEXT("RC"),
*VF_FormatRecipeEntry(Recipe.Root),
*VF_FormatRecipeEntry(Recipe.ShapeSource),
*VF_FormatRecipeEntry(Recipe.Conversion),
*Modifiers,
VF_RecipeBlockCode(Recipe.StructuralParamBlock));
}
uint32 VF_HashStrateStructureRecipe(const FVoxelOpStackRecipe& Recipe)
{
uint32 Hash = 2166136261u;
auto AddByte = [&Hash](uint8 Value)
{
Hash ^= static_cast<uint32>(Value);
Hash *= 16777619u;
};
auto AddEntry = [&AddByte](const FVoxelOpRecipeEntry& Entry)
{
AddByte(static_cast<uint8>(Entry.OpClass));
AddByte(static_cast<uint8>(Entry.ParamBlock));
};
AddByte(static_cast<uint8>(Recipe.RootPolarity));
AddEntry(Recipe.Root);
AddEntry(Recipe.ShapeSource);
AddEntry(Recipe.Conversion);
AddByte(static_cast<uint8>(Recipe.Modifiers.Num()));
for (const FVoxelOpRecipeEntry& Entry : Recipe.Modifiers) { AddEntry(Entry); }
AddByte(static_cast<uint8>(Recipe.StructuralParamBlock));
return Hash;
}
bool VF_AreStrateStructureRecipesIdentical(const FVoxelOpStackRecipe& A,
const FVoxelOpStackRecipe& B)
{
if (A.RootPolarity != B.RootPolarity
|| A.Root.OpClass != B.Root.OpClass || A.Root.ParamBlock != B.Root.ParamBlock
|| A.ShapeSource.OpClass != B.ShapeSource.OpClass
|| A.ShapeSource.ParamBlock != B.ShapeSource.ParamBlock
|| A.Conversion.OpClass != B.Conversion.OpClass
|| A.Conversion.ParamBlock != B.Conversion.ParamBlock
|| A.StructuralParamBlock != B.StructuralParamBlock
|| A.Modifiers.Num() != B.Modifiers.Num())
{
return false;
}
for (int32 Index = 0; Index < A.Modifiers.Num(); ++Index)
{
if (A.Modifiers[Index].OpClass != B.Modifiers[Index].OpClass
|| A.Modifiers[Index].ParamBlock != B.Modifiers[Index].ParamBlock)
{
return false;
}
}
return true;
}
+176 -42
View File
@@ -75,23 +75,28 @@ namespace VoxelStrateMeasurePrivate
} }
bool BuildSampleGrid( bool BuildSampleGrid(
const UVoxelGenerator& Generator, const UVoxelGenerator* Generator,
const UVoxelStrateManager& Manager, const UVoxelStrateManager* Manager,
const IVoxelStrateDensitySampler* Sampler,
int32 StrateIndex, int32 StrateIndex,
int32 ExplicitBottomWorldZ,
int32 ExplicitTopWorldZ,
float ExplicitBoundarySealThickness,
bool bUseExplicitBounds,
const FVoxelStrateMeasureSettings& Settings, const FVoxelStrateMeasureSettings& Settings,
FSampleGrid& OutGrid, FSampleGrid& OutGrid,
FString& OutReason) FString& OutReason)
{ {
const TArray<FStrateSlot>& Layout = Manager.GetLayout(); if (bUseExplicitBounds)
if (!Layout.IsValidIndex(StrateIndex))
{ {
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."));
}
} }
else if (Generator == nullptr || Manager == nullptr)
const FStrateSlot& Slot = Layout[StrateIndex];
if (Slot.Definition == 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) if (Settings.SampleStep <= 0)
{ {
@@ -136,13 +141,37 @@ namespace VoxelStrateMeasurePrivate
return Refuse(OutReason, TEXT("CoverMarginVoxels must be finite and non-negative.")); return Refuse(OutReason, TEXT("CoverMarginVoxels must be finite and non-negative."));
} }
} }
if (Slot.TopChunkZ <= Slot.BottomChunkZ) int64 StrateBottomZ = 0;
int64 StrateTopZ = 0;
if (bUseExplicitBounds)
{ {
return Refuse(OutReason, TEXT("The requested strate has no positive vertical extent.")); StrateBottomZ = static_cast<int64>(ExplicitBottomWorldZ);
StrateTopZ = static_cast<int64>(ExplicitTopWorldZ);
if (StrateTopZ <= StrateBottomZ)
{
return Refuse(OutReason, TEXT("The explicit strate has no positive vertical extent."));
}
} }
else
{
const TArray<FStrateSlot>& Layout = Manager->GetLayout();
if (!Layout.IsValidIndex(StrateIndex))
{
return Refuse(OutReason, TEXT("StrateIndex is outside the manager layout."));
}
const int64 StrateBottomZ = static_cast<int64>(Slot.BottomChunkZ) * CHUNK_SIZE; const FStrateSlot& Slot = Layout[StrateIndex];
const int64 StrateTopZ = (static_cast<int64>(Slot.TopChunkZ) + 1) * CHUNK_SIZE; if (Slot.Definition == nullptr)
{
return Refuse(OutReason, TEXT("The requested strate has no definition."));
}
if (Slot.TopChunkZ <= Slot.BottomChunkZ)
{
return Refuse(OutReason, TEXT("The requested strate has no positive vertical extent."));
}
StrateBottomZ = static_cast<int64>(Slot.BottomChunkZ) * CHUNK_SIZE;
StrateTopZ = (static_cast<int64>(Slot.TopChunkZ) + 1) * CHUNK_SIZE;
}
const int64 StrateHeight = StrateTopZ - StrateBottomZ; const int64 StrateHeight = StrateTopZ - StrateBottomZ;
if (StrateHeight <= 0) if (StrateHeight <= 0)
{ {
@@ -192,14 +221,23 @@ namespace VoxelStrateMeasurePrivate
return Refuse(OutReason, TEXT("The requested XY window is not a finite positive voxel box.")); return Refuse(OutReason, TEXT("The requested XY window is not a finite positive voxel box."));
} }
const int32 MidChunkZ = Slot.BottomChunkZ FStrateGenerationParams RepresentativeParams;
+ (Slot.TopChunkZ - Slot.BottomChunkZ) / 2; if (bUseExplicitBounds)
const FIntVector RepresentativeChunk( {
FMath::FloorToInt(WindowCenter.X / static_cast<float>(CHUNK_SIZE)), RepresentativeParams.BoundarySealThickness = ExplicitBoundarySealThickness;
FMath::FloorToInt(WindowCenter.Y / static_cast<float>(CHUNK_SIZE)), }
MidChunkZ); else
const FStrateGenerationParams RepresentativeParams = {
Manager.GetGenerationParams(RepresentativeChunk); const TArray<FStrateSlot>& Layout = Manager->GetLayout();
const FStrateSlot& Slot = Layout[StrateIndex];
const int32 MidChunkZ = Slot.BottomChunkZ
+ (Slot.TopChunkZ - Slot.BottomChunkZ) / 2;
const FIntVector RepresentativeChunk(
FMath::FloorToInt(WindowCenter.X / static_cast<float>(CHUNK_SIZE)),
FMath::FloorToInt(WindowCenter.Y / static_cast<float>(CHUNK_SIZE)),
MidChunkZ);
RepresentativeParams = Manager->GetGenerationParams(RepresentativeChunk);
}
const int64 MaxMargin64 = FMath::Max<int64>(1, StrateHeight / 4); const int64 MaxMargin64 = FMath::Max<int64>(1, StrateHeight / 4);
const int32 MaxMargin = static_cast<int32>( const int32 MaxMargin = static_cast<int32>(
@@ -328,7 +366,9 @@ namespace VoxelStrateMeasurePrivate
for (int32 X = 0; X < OutGrid.NumX; ++X) for (int32 X = 0; X < OutGrid.NumX; ++X)
{ {
const FVector Sample = OutGrid.CellCenter(X, Y, Z); 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)) if (!FMath::IsFinite(Density))
{ {
bSawNonFiniteDensity = true; bSawNonFiniteDensity = true;
@@ -772,7 +812,8 @@ namespace VoxelStrateMeasurePrivate
} }
bool FullResolutionAirOnSegment( bool FullResolutionAirOnSegment(
const UVoxelGenerator& Generator, const UVoxelGenerator* Generator,
const IVoxelStrateDensitySampler* Sampler,
const FVector& Start, const FVector& Start,
const FVector& End) const FVector& End)
{ {
@@ -784,7 +825,9 @@ namespace VoxelStrateMeasurePrivate
{ {
const float Alpha = static_cast<float>(Step) / static_cast<float>(NumSteps); const float Alpha = static_cast<float>(Step) / static_cast<float>(NumSteps);
const FVector Sample = Start + Delta * Alpha; 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)) if (!FMath::IsFinite(Density) || !(Density > 0.0f))
{ {
return false; return false;
@@ -794,7 +837,8 @@ namespace VoxelStrateMeasurePrivate
} }
bool FullResolutionPathIsAir( bool FullResolutionPathIsAir(
const UVoxelGenerator& Generator, const UVoxelGenerator* Generator,
const IVoxelStrateDensitySampler* Sampler,
const FSampleGrid& Grid, const FSampleGrid& Grid,
const FVector& A, const FVector& A,
const FVector& B, const FVector& B,
@@ -812,7 +856,7 @@ namespace VoxelStrateMeasurePrivate
int32 EndZ = 0; int32 EndZ = 0;
DecodeIndex(Grid, Path.Last(), EndX, EndY, EndZ); 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 // 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 // common to every route from this endpoint, so report unknown rather than claiming
@@ -831,7 +875,7 @@ namespace VoxelStrateMeasurePrivate
int32 CurrentZ = 0; int32 CurrentZ = 0;
DecodeIndex(Grid, Path[PathIndex], CurrentX, CurrentY, CurrentZ); DecodeIndex(Grid, Path[PathIndex], CurrentX, CurrentY, CurrentZ);
if (!FullResolutionAirOnSegment( if (!FullResolutionAirOnSegment(
Generator, Generator, Sampler,
Grid.CellCenter(PreviousX, PreviousY, PreviousZ), Grid.CellCenter(PreviousX, PreviousY, PreviousZ),
Grid.CellCenter(CurrentX, CurrentY, CurrentZ))) 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 // 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. // goal cell and therefore has no precise coarse edge to exclude.
@@ -930,7 +974,8 @@ namespace VoxelStrateMeasurePrivate
} }
EVoxelConnectivityResult EvaluateConnectivityOnGrid( EVoxelConnectivityResult EvaluateConnectivityOnGrid(
const UVoxelGenerator& Generator, const UVoxelGenerator* Generator,
const IVoxelStrateDensitySampler* Sampler,
const FSampleGrid& Grid, const FSampleGrid& Grid,
const FVector& AVoxel, const FVector& AVoxel,
const FVector& BVoxel, const FVector& BVoxel,
@@ -980,7 +1025,7 @@ namespace VoxelStrateMeasurePrivate
// spacing in the original voxel field. A single solid sample refutes only this // spacing in the original voxel field. A single solid sample refutes only this
// route, so exclude its precise coarse edge and search again. // route, so exclude its precise coarse edge and search again.
FCoarseEdge FailedEdge; FCoarseEdge FailedEdge;
if (FullResolutionPathIsAir(Generator, Grid, AVoxel, BVoxel, Path, FailedEdge)) if (FullResolutionPathIsAir(Generator, Sampler, Grid, AVoxel, BVoxel, Path, FailedEdge))
{ {
return EVoxelConnectivityResult::Connected; return EVoxelConnectivityResult::Connected;
} }
@@ -1126,9 +1171,14 @@ namespace VoxelStrateMeasurePrivate
} }
EVoxelConnectivityResult QueryConnectivity( EVoxelConnectivityResult QueryConnectivity(
const UVoxelGenerator& Generator, const UVoxelGenerator* Generator,
const UVoxelStrateManager& Manager, const UVoxelStrateManager* Manager,
const IVoxelStrateDensitySampler* Sampler,
int32 StrateIndex, int32 StrateIndex,
int32 ExplicitBottomWorldZ,
int32 ExplicitTopWorldZ,
float ExplicitBoundarySealThickness,
bool bUseExplicitBounds,
const FVector& AVoxel, const FVector& AVoxel,
const FVector& BVoxel, const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings, const FVoxelStrateMeasureSettings& Settings,
@@ -1161,7 +1211,10 @@ namespace VoxelStrateMeasurePrivate
FSampleGrid Grid; FSampleGrid Grid;
FString RefusalReason; 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) if (OutDiagnostics != nullptr)
{ {
@@ -1174,6 +1227,7 @@ namespace VoxelStrateMeasurePrivate
int32 Goal = -1; int32 Goal = -1;
const EVoxelConnectivityResult Result = EvaluateConnectivityOnGrid( const EVoxelConnectivityResult Result = EvaluateConnectivityOnGrid(
Generator, Generator,
Sampler,
Grid, Grid,
AVoxel, AVoxel,
BVoxel, BVoxel,
@@ -1208,7 +1262,43 @@ FVoxelStrateMetrics VF_MeasureStrate(
FVoxelStrateMetrics Result; FVoxelStrateMetrics Result;
VoxelStrateMeasurePrivate::FSampleGrid Grid; VoxelStrateMeasurePrivate::FSampleGrid Grid;
if (!VoxelStrateMeasurePrivate::BuildSampleGrid( 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; return Result;
} }
@@ -1244,9 +1334,11 @@ EVoxelConnectivityResult VF_AreConnected(
{ {
int32 NumRouteRetries = 0; int32 NumRouteRetries = 0;
return VoxelStrateMeasurePrivate::QueryConnectivity( return VoxelStrateMeasurePrivate::QueryConnectivity(
Generator, &Generator,
Manager, &Manager,
nullptr,
StrateIndex, StrateIndex,
0, 0, 0.0f, false,
AVoxel, AVoxel,
BVoxel, BVoxel,
Settings, Settings,
@@ -1268,9 +1360,11 @@ EVoxelConnectivityResult VF_AreConnected(
int32& OutNumRouteRetries) int32& OutNumRouteRetries)
{ {
return VoxelStrateMeasurePrivate::QueryConnectivity( return VoxelStrateMeasurePrivate::QueryConnectivity(
Generator, &Generator,
Manager, &Manager,
nullptr,
StrateIndex, StrateIndex,
0, 0, 0.0f, false,
AVoxel, AVoxel,
BVoxel, BVoxel,
Settings, Settings,
@@ -1293,9 +1387,11 @@ EVoxelConnectivityResult VF_AreConnected(
{ {
int32 NumRouteRetries = 0; int32 NumRouteRetries = 0;
return VoxelStrateMeasurePrivate::QueryConnectivity( return VoxelStrateMeasurePrivate::QueryConnectivity(
Generator, &Generator,
Manager, &Manager,
nullptr,
StrateIndex, StrateIndex,
0, 0, 0.0f, false,
AVoxel, AVoxel,
BVoxel, BVoxel,
Settings, Settings,
@@ -1316,9 +1412,11 @@ FVoxelConnectivityDiagnostics VF_DiagnoseConnectivity(
FVoxelConnectivityDiagnostics Result; FVoxelConnectivityDiagnostics Result;
int32 NumRouteRetries = 0; int32 NumRouteRetries = 0;
VoxelStrateMeasurePrivate::QueryConnectivity( VoxelStrateMeasurePrivate::QueryConnectivity(
Generator, &Generator,
Manager, &Manager,
nullptr,
StrateIndex, StrateIndex,
0, 0, 0.0f, false,
AVoxel, AVoxel,
BVoxel, BVoxel,
Settings, Settings,
@@ -1328,3 +1426,39 @@ FVoxelConnectivityDiagnostics VF_DiagnoseConnectivity(
&Result); &Result);
return Result; return Result;
} }
EVoxelConnectivityResult VF_AreConnectedWithSampler(
const IVoxelStrateDensitySampler& Sampler,
int32 StrateBottomWorldZ,
int32 StrateTopWorldZ,
float BoundarySealThickness,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
bool& bOutStartSnapped,
bool& bOutGoalSnapped,
FVoxelConnectivityDiagnostics* OutDiagnostics)
{
int32 NumRouteRetries = 0;
return VoxelStrateMeasurePrivate::QueryConnectivity(
nullptr, nullptr, &Sampler, INDEX_NONE,
StrateBottomWorldZ, StrateTopWorldZ, BoundarySealThickness, true,
AVoxel, BVoxel, Settings, bOutStartSnapped, bOutGoalSnapped,
NumRouteRetries, OutDiagnostics);
}
FVoxelConnectivityDiagnostics VF_DiagnoseConnectivityWithSampler(
const IVoxelStrateDensitySampler& Sampler,
int32 StrateBottomWorldZ,
int32 StrateTopWorldZ,
float BoundarySealThickness,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings)
{
FVoxelConnectivityDiagnostics Result;
VF_AreConnectedWithSampler(Sampler, StrateBottomWorldZ, StrateTopWorldZ,
BoundarySealThickness, AVoxel, BVoxel, Settings,
Result.bStartSnapped, Result.bGoalSnapped, &Result);
return Result;
}
+43
View File
@@ -154,6 +154,33 @@ namespace VoxelOpChannels
static constexpr EVoxelOpChannelMask All = Density | Sdf; 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 // COMPOSITION / COMBINERS
//============================================================================= //=============================================================================
@@ -382,6 +409,22 @@ public:
virtual EVoxelOpChannelMask ChannelReads() const = 0; virtual EVoxelOpChannelMask ChannelReads() const = 0;
virtual EVoxelOpChannelMask ChannelWrites() 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, * 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 * 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 #include "VoxelHeightOp.h" // IVoxelBiomeField — BuildSurfaceStack takes ownership of one
class UVoxelStrateManager; 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. * 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 Reads = VoxelOpChannels::None;
EVoxelOpChannelMask Writes = VoxelOpChannels::None; EVoxelOpChannelMask Writes = VoxelOpChannels::None;
bool bAdditive = false; bool bAdditive = false;
EVoxelOpResourceMask RequiredResources = VoxelOpResources::None;
EVoxelOpResourceMask ProvidedResources = VoxelOpResources::None;
}; };
public: public:
@@ -102,6 +118,8 @@ public:
Entry.Reads = Entry.Op->ChannelReads(); Entry.Reads = Entry.Op->ChannelReads();
Entry.Writes = Entry.Op->ChannelWrites(); Entry.Writes = Entry.Op->ChannelWrites();
Entry.bAdditive = Entry.Op->IsAdditive(); Entry.bAdditive = Entry.Op->IsAdditive();
Entry.RequiredResources = Entry.Op->RequiredResources();
Entry.ProvidedResources = Entry.Op->ProvidedResources();
} }
Ops.Add(MoveTemp(Entry)); Ops.Add(MoveTemp(Entry));
} }
@@ -258,6 +276,10 @@ private:
namespace VoxelDensityOps 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. /** Rôle 1 — `Density = BaseDensity` partout. `ClassifyBox` → AllSolid, exact et gratuit.
* Racine de TunnelNetwork, Maze, VerticalShafts et des gaps de bedrock. */ * Racine de TunnelNetwork, Maze, VerticalShafts et des gaps de bedrock. */
VOXELFORGE_API TUniquePtr<IVoxelDensityOp> MakeConstantRockSource(float BaseDensity); VOXELFORGE_API TUniquePtr<IVoxelDensityOp> MakeConstantRockSource(float BaseDensity);
@@ -439,3 +461,12 @@ namespace VoxelDensityOps
int32 Seed, float SpineRadius, int32 Seed, float SpineRadius,
const UVoxelStrateManager* StrateManager); 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 "CoreMinimal.h"
#include "VoxelStrateTypes.h" #include "VoxelStrateTypes.h"
#include "VoxelDensityOpStack.h"
#include "VoxelStrateComposer.generated.h"
class UVoxelSettings; class UVoxelSettings;
class UVoxelStrateDefinition; 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. */ /** The kind of value represented by one entry of VF_STRATE_PARAM_FIELDS. */
enum class EVoxelStrateFieldKind : uint8 enum class EVoxelStrateFieldKind : uint8
{ {
@@ -177,6 +276,19 @@ VOXELFORGE_API const TCHAR* VF_GetStrateArchetypeName(ECaveGeneratorType Archety
VOXELFORGE_API FVoxelStrateRollInfo VF_RollStrateParamsDetailed( VOXELFORGE_API FVoxelStrateRollInfo VF_RollStrateParamsDetailed(
const FVoxelStrateCorpus& Corpus, int32 Seed, int32 Index); 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. */ /** The compact API requested by the composer design. */
VOXELFORGE_API FStrateGenerationParams VF_RollStrateParams( VOXELFORGE_API FStrateGenerationParams VF_RollStrateParams(
const FVoxelStrateCorpus& Corpus, int32 Seed, int32 Index); const FVoxelStrateCorpus& Corpus, int32 Seed, int32 Index);
@@ -7,6 +7,14 @@
class UVoxelGenerator; class UVoxelGenerator;
class UVoxelStrateManager; 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. */ /** Explicit outcome of a coarse connectivity query plus its full-resolution route re-check. */
enum class EVoxelConnectivityResult : uint8 enum class EVoxelConnectivityResult : uint8
{ {
@@ -122,6 +130,14 @@ VOXELFORGE_API FVoxelStrateMetrics VF_MeasureStrate(
int32 StrateIndex, int32 StrateIndex,
const FVoxelStrateMeasureSettings& Settings); 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. * 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& AVoxel,
const FVector& BVoxel, const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings); 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);