feat(composer): the structure roll -- 64 distinct stacks nobody wrote
Tier 4b. Until now the composer varied PARAMETERS within an archetype:
differently-sized caves. This rolls STRUCTURE, which is what makes a strate
a different KIND of place.
Per COMPOSER-NOTES §3.2: roll root polarity (rock+carve or void+fill) ->
a shape source legal for that polarity -> the conversion FOLLOWS from
polarity rather than being rolled -> k modifiers drawn from what the
CHANNEL DECLARATIONS say is legal, not from a hand-written list ->
structural posts appended automatically and not omittable.
64 distinct recipes of 64 (perfect structural diversity)
invalid recipes emitted: 0 (the roller cannot produce an
order the DAG validator rejects)
survival 46/64 (71.9%) non-vacuous 62, largest-share 60,
law pass 48
⭐ AND THEY ARE REAL PLACES NOBODY DESIGNED. Root polarity as a one-bit
identity lever, exactly as §3.2 predicted:
VF|r=vd/T|s=rg/T|x=fl/T TunnelNetwork's ROOM GRAPH rooted in void with
a FILL -- rooms become solid blobs hanging in
open space. air .978, feature scale 64.
VF|r=vd/M|s=lt/M|x=fl/M Maze's lattice corridors filled instead of
carved. air .938, feature scale 16.
RC|r=rk/V|s=sh/V|x=cv/V shafts carved from rock with a Maze modifier
set they never ship with.
Same parts, different worlds. That is the whole thesis of the op stack,
finally exercised.
⭐⭐ AND THE VERDICTS HOLD ON STACKS NOBODY VERIFIED:
489 proved (213 AllSolid, 276 AllAir), 2,071 mixed
650,859 voxels brute-forced
0 VIOLATIONS
This is what 8e346a3 was for. A false uniform verdict on an invented stack
means no geometry, no collision, and a player falls through the floor --
and it is the failure that cannot be seen without brute force. Zero.
Survival is 71.9% against 92.2% for the parameter roll, and that is the
EXPECTED direction, not a regression. Rolling structure aggressively is
supposed to fail sometimes; §3.5's reject-and-resample is what that is for.
A composer that only emits safe stacks is not exploring. Deliberately not
tuned.
Recipes are serialisable (op ids + param-block bindings), so a season
manifest can ship one -- which is also what will let an archetype become a
spatial REGION inside a strate rather than a label on it.
Build clean, 24 succeeded / 0 failed, verified by me in VoxelM.log.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
This commit is contained in:
1 parent
190ff381c0
commit
859a9b25c1
10 files changed
+2194
-55
No files matched your search
+15
-5
@@ -106,7 +106,7 @@ reference path for opt-out strates. See [OPSTACK-PLAN.md](OPSTACK-PLAN.md) for t
|
|||||||
| `EVoxelOpCombine` | `Replace`/`Union`(min)/`Subtract`(max)/`SmoothUnion`/`SmoothSubtract`/`Add`/`Mask`. Sign reminder: negative = solid, so "add solid" is `min`. |
|
| `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
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
Reference in new issue
Block a user