feat: height-space operator family — SurfaceWorld step 1, and C10 is solved
Maze and Slab now report BIT-IDENTICAL: FPSemantics = Precise, set for cross-platform play, dissolved the ULP residue. Hypothesis 3 had the right mechanism all along — under /fp:fast the compiler transforms by surrounding context with no isolable axis, which is exactly why five one-variable experiments all came back negative. Removing the permission removed the difference. Nobody solved C10; C9 got fixed for an unrelated reason and C10 fell out of it. SurfaceWorld step 1 forced an architectural decision. DECOMPOSITION section 5 notes the height ops operate on Z values rather than density, then lists them as children of FHeightfieldSource. Writing them made the consequence unavoidable: they do not fit IVoxelDensityOp. No input Z (they produce one), XY-pure per column rather than per voxel, and they write neither channel. Forcing them in would need a per-voxel channel for a column property, or one opaque op — section 2.5's failure mode. So height space gets its own contract: VoxelHeightOp.h (FVoxelHeightSample with Height + Relief, IVoxelHeightOp, FVoxelHeightStack) and five ops. Relief is the original's M — produced by the structural source, consumed by the terrace gate. Section 0.1 found density needed a second channel; this found terrain needs a second space. The type system now forbids for free what AUDIT 6.3 warns about: a height stack cannot hold Z-dependent data because there is no Z in the signature. Deliberately staged — this touches nothing on the density path. If height space had not decomposed cleanly, it shows up here for one test rather than after building the adapter, the column cache integration and the dispatch on top. The test runs twice; the second pass is load-bearing because the F20 terrain ops are off by default, so a defaults-only run leaves all four modifiers untested. It also brute-forces MaxDisplacement, since a false bound would later be a hole. ComputeSurfaceTerrainZ moved private -> public for the test, same justification as GetSlabDensity. Old declaration removed. UNVERIFIED: not compiled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+26
-7
@@ -391,14 +391,33 @@ risk is real. That is one build, and it settles it.
|
||||
|
||||
### C10 — The op-stack ULP residue: ~~PARKED~~ **CLOSED PERMANENTLY**, 2026-07-27
|
||||
|
||||
> **⛔ CLOSED, and now provably irrelevant — not merely parked.** Jahni, 2026-07-27: *"I do not need
|
||||
> your work to be identical or near identical to what I had before, only having it 99.99% at worst
|
||||
> reproducible if two people share the same seed."* This entry measures **old path vs new path**
|
||||
> agreement, and the two paths will never both exist in a shipped world. **No requirement anywhere
|
||||
> in the project depends on this number.** Do not spend another minute on it.
|
||||
> # ✅ SOLVED, 2026-07-27 — and the cause was `/fp:fast`, exactly as hypothesis 3 said.
|
||||
>
|
||||
> What survives is the *method* lesson at the bottom of this entry, and the redirect: the
|
||||
> reproducibility that IS required is **`§C9`** — across binaries and platforms, not across refactors.
|
||||
> **Setting `FPSemantics = Precise` (see `§C9`) made both `MazeEquivalence` and `SlabEquivalence`
|
||||
> report BIT-IDENTICAL.** The residue is gone — not tolerated, *gone*.
|
||||
>
|
||||
> **So hypothesis 3 had the right mechanism and every isolation experiment built on it was doomed.**
|
||||
> Under `/fp:fast` the compiler may reassociate and contract *based on surrounding context*, with
|
||||
> **no single isolable axis** — which is precisely why five carefully-designed one-variable tests
|
||||
> (FVector round-trip, transcription, cross-TU, inlining, const-vs-runtime) all came back negative
|
||||
> while the difference stayed. There was no variable to find. Removing the *permission* removed the
|
||||
> difference.
|
||||
>
|
||||
> ### The lesson, and it is not the one I expected
|
||||
>
|
||||
> **The answer arrived for free, from work done for an unrelated reason.** Nobody solved C10; C9 got
|
||||
> fixed because Jahni wants Linux/Windows cross-play, and C10 fell out of it. Six more builds spent
|
||||
> hunting would have found nothing, because the thing that resolved it was a *build setting nobody
|
||||
> was looking at* while I was busy bisecting source code.
|
||||
>
|
||||
> **The decision to stop was therefore right on its own terms and right in hindsight** — the
|
||||
> information was not obtainable by continuing along that path at any price. Park a question whose
|
||||
> every consequence is measured and benign; it may well be answered later by something else.
|
||||
>
|
||||
> **What this changes going forward:** ports can now aim for and *achieve* bit-identity, so the
|
||||
> equivalence tests are much sharper instruments than they were — any diff at all is now a real
|
||||
> finding rather than noise to be graded. The ULP-grading machinery in both tests is kept, because it
|
||||
> is what would tell us if this ever regresses.
|
||||
|
||||
**Status: accepted and closed by decision (Jahni), not by explanation.** Do not reopen this without
|
||||
reading the whole entry — five hypotheses have already been measured and refuted, and re-deriving
|
||||
|
||||
+22
@@ -140,6 +140,26 @@ inherent (AUDIT §C10). The acceptance bar is visual (OPSTACK-PLAN §2.6).
|
||||
| `VoxelDensityOps::BuildSlabStack` | — | 5 ops, **no branch on archetype**: FlatPlain and CrystalChamber differ only in defaults, exactly as `GetSlabDensity` already had it. 8 archetypes → 7. |
|
||||
| `VoxelDensityOps::BuildMazeStack` | — | The 7-op Maze stack. If this ever becomes one op, the refactor failed its own test (§2.5). Callers must skip it on a **degenerate strate** (top−bottom ≤ 0): `GetMazeDensity` early-outs to air there and the stack has no such early-out by design — `GetDensityAt` falls back to the `switch`. |
|
||||
|
||||
### 3.2e Height-space operators — `Public/VoxelHeightOp.h` + `Private/VoxelHeightOpStack.cpp`
|
||||
⚠️ **Feeds nothing yet** — built and exercised only by `VoxelForge.OpStack.SurfaceHeightEquivalence`.
|
||||
**A SECOND op family, and it exists for a reason worth knowing:** SurfaceWorld's terrain ops (cliff /
|
||||
terrace / layer lines / beach) read and write an **altitude**, not a density. They have no input Z
|
||||
(they produce one), are XY-pure (once per column, not per voxel), and touch neither density nor SDF —
|
||||
so they do not fit `IVoxelDensityOp` at all. Forcing them in would need a per-voxel channel for what
|
||||
is a **column** property, or one opaque op (`OPSTACK-PLAN §2.5`'s failure mode). Same lesson as
|
||||
`§0.1` one step further: some things are not another channel, they are another **space**.
|
||||
|
||||
| Symbol | Notes |
|
||||
|--------|-------|
|
||||
| `FVoxelHeightSample` | Two channels: `Height` (voxel Z) + `Relief` (the original's `M`). Relief is produced by the structural source and consumed by the terrace gate — threading it beats resampling it. |
|
||||
| `IVoxelHeightOp` | `Eval(X, Y, FVoxelHeightSample&)`. No `IsXYPure` (XY-purity is structural here — there is no Z to wrongly put in), no `PrepareChunk` (already per-column). `MaxDisplacement()` is the conservative vertical bound for a future heightfield `ClassifyBox`. |
|
||||
| `FVoxelHeightStack` | Move-only, like `FVoxelOpStack`. `EvalHeight` / `EvalSample` / `MaxTotalDisplacement`. |
|
||||
| `VoxelHeightOps::MakeStructuralHeightSource` | Continents + mountains + detail under a warp frame. Hands back a **non-owning pointer** so the cliff mod can resample it. |
|
||||
| `VoxelHeightOps::MakeCliffHeightMod` | Slope-gated steepening; 4 resamples of the **structural** field (never the modified height — that would feed back). |
|
||||
| `VoxelHeightOps::MakeTerraceHeightMod` | Relief-gated plateaus. The `* Relief` is the original's `* M`. |
|
||||
| `VoxelHeightOps::MakeLayerLineHeightMod` / `MakeBeachHeightMod` | Sine bands; flatten toward the water line. Both have exact `MaxDisplacement`. |
|
||||
| `VoxelHeightOps::BuildSurfaceHeightStack` | 5 ops in `ComputeSurfaceTerrainZ`'s order — structural → cliff → terrace → layer lines → beach. **Order is not negotiable.** |
|
||||
|
||||
### 3.3 Chunk identity
|
||||
`VoxelChunk.h` (the old `FVoxelChunk` coord wrapper) was DELETED — dead since the tile
|
||||
redesign; tile identity lives in `FVoxelTileKey` (VoxelWorld.h).
|
||||
@@ -380,6 +400,8 @@ The plugin's first tests (`OPSTACK-PLAN.md` Phase 0.5). Run them from the editor
|
||||
| `VoxelForgeClassifyTileTest.cpp` | `VoxelForge.Determinism.ClassifyTileSoundness` | Scans for a non-`Mixed` verdict, then brute-forces the exact mesher lattice (`g ∈ [-1, Cells+1]`). **A false verdict is an invisible, collisionless hole** — T1.d v1 was reverted for exactly this. Errors out rather than passing if it found nothing to check. |
|
||||
| `VoxelForgeDiffLayerTest.cpp` | `VoxelForge.Determinism.DiffLayerContention` | N readers running the worker call mix while the game thread writes and `Clear()`s. Survival + monotonic `ModsVersion`. |
|
||||
| `VoxelForgeClassifyTileTest.cpp` | `VoxelForge.OpStack.BoxVerdictFold` | Pure-logic walk of the fold in `VoxelDensityOp.h`, case by case — including the seal-forces-AllSolid case that justifies `ClassifyBox` existing. Also the only `.cpp` that includes the op header, so the build actually sees it. |
|
||||
| `VoxelForgeHeightStackTest.cpp` | `VoxelForge.OpStack.SurfaceHeightEquivalence` | The height-space stack vs `ComputeSurfaceTerrainZ`, in **altitudes**. Runs twice: defaults, then **all F20 terrain ops ON** — the load-bearing pass, since the ops are off by default and the defaults pass exercises only the structural source. Also brute-forces `MaxDisplacement` (a false bound would be a hole). Bar is bit-identity; a height delta is a visibly different world, not rounding. |
|
||||
| `VoxelForgeCrossPlatformTest.cpp` | `VoxelForge.Determinism.CrossPlatformDigest` | SHAPE digest (sign of density = the world) + FIELD digest (bit-for-bit) over a fixed integer grid, plus `NearIso` bounding how many samples could flip sign. Reports rather than asserts until pinned. Run on Windows and Linux and compare. |
|
||||
| `VoxelForgeOpStackSlabTest.cpp` | `VoxelForge.OpStack.SlabEquivalence` | **Phase 2's first port.** The same 5-op slab stack vs `GetSlabDensity` over 20k points, run twice — FlatPlain **and** CrystalChamber — which is what demonstrates the two archetypes really are one op. Plus window-invariance and box-verdict brute force. Compares against the reference **as it is now** (post Z-term removal), so green = pure refactor and any visual delta is attributable to §3.1 alone. |
|
||||
| `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. |
|
||||
|
||||
|
||||
@@ -361,6 +361,28 @@ FOverhangShelfMod Union ⚠️ per-voxel, NOT XY-pure — the
|
||||
[structural post ×4]
|
||||
```
|
||||
|
||||
> ### ⚠️ RESOLVED 2026-07-27 — the height ops needed a SECOND OP FAMILY, not a sub-list
|
||||
>
|
||||
> This section says the height ops *"operate on Z values in the column, not on density"* and then
|
||||
> lists them as children of `FHeightfieldSource`. Writing them made the consequence unavoidable:
|
||||
> **they do not fit `IVoxelDensityOp` at all.** Its signature is `Eval(x, y, z, FVoxelOpSample&)` —
|
||||
> per voxel, density + SDF. A height op has **no input Z** (it produces one), is XY-pure (once per
|
||||
> column), and writes neither channel.
|
||||
>
|
||||
> The two ways to force it were both bad: a per-voxel third channel for what is a **column**
|
||||
> property, or collapsing all five into one opaque op — `OPSTACK-PLAN §2.5`'s explicit failure mode.
|
||||
>
|
||||
> **So height space got its own contract: `VoxelHeightOp.h`** (`FVoxelHeightSample` with
|
||||
> `Height` + `Relief`, `IVoxelHeightOp`, `FVoxelHeightStack`). Same lesson as `§0.1`, one step
|
||||
> further: §0.1 found that density needed a second *channel*; this found that terrain needs a second
|
||||
> *space*. Verified by `VoxelForge.OpStack.SurfaceHeightEquivalence` before anything was built on
|
||||
> top of it — deliberately, so a wrong answer would have cost one test rather than a whole port.
|
||||
>
|
||||
> **Bonus the type system gives for free:** a height stack cannot contain Z-dependent data, because
|
||||
> there is no Z in the signature to put there. `AUDIT §6.3` warns that Z-dependent data smuggled into
|
||||
> `FSurfaceColumn` silently corrupts every chunk in the vertical stack and that `ValidateDeterminism`
|
||||
> would not catch it. Here the *type* forbids it rather than a convention.
|
||||
|
||||
**Critical distinction the port must preserve:** the height ops (`FCliffHeightMod` and friends)
|
||||
operate on **Z values in the column**, not on density. They are XY-pure and belong in
|
||||
`PrepareChunk`/the column cache. `FOverhangShelfMod` operates per voxel and re-samples the
|
||||
|
||||
@@ -1207,3 +1207,71 @@ headers. Everything else in the module compiled, so this is the whole tail: one
|
||||
path, checked against `ARCHITECTURE §8.10`. Then back to the opstack: `SurfaceWorld` (§5).
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-27 — ✅ C10 SOLVED (it WAS /fp:fast). SurfaceWorld opened: height space needed its own family.
|
||||
|
||||
**Maze and Slab are now BIT-IDENTICAL to their originals.** `FPSemantics = Precise`, set for
|
||||
cross-platform play, dissolved the ULP residue.
|
||||
|
||||
**So hypothesis 3 had the right mechanism all along, and every experiment built on it was doomed by
|
||||
construction.** Under `/fp:fast` the compiler reassociates and contracts *by surrounding context*,
|
||||
with **no single isolable axis** — which is exactly why five carefully-designed one-variable tests
|
||||
all came back negative while the difference stayed. There was no variable to find. Removing the
|
||||
*permission* removed the difference.
|
||||
|
||||
**The lesson is not the one I expected.** Nobody solved C10; C9 got fixed for an unrelated
|
||||
requirement and C10 fell out of it. Six more builds of bisecting source would have found nothing,
|
||||
because the answer was a **build setting nobody was looking at**. Parking a question whose every
|
||||
consequence is measured and benign was right on its own terms *and* right in hindsight — the
|
||||
information was not obtainable along the path I was on, at any price. Recorded in `AUDIT §C10`.
|
||||
|
||||
**Immediate consequence:** the equivalence tests are now much sharper instruments. Any diff at all is
|
||||
a real finding rather than noise to grade. The ULP machinery stays as a float-model regression alarm.
|
||||
|
||||
### SurfaceWorld, step 1 of 2 — and it forced an architectural decision
|
||||
|
||||
`§5` says the height ops *"operate on Z values in the column, not on density"* and then lists them as
|
||||
children of `FHeightfieldSource`. Writing them made the consequence unavoidable: **they do not fit
|
||||
`IVoxelDensityOp` at all.** No input Z (they produce one), XY-pure per column rather than per voxel,
|
||||
and they write neither density nor SDF. The two ways to force them in were a per-voxel third channel
|
||||
for what is a **column** property, or one opaque op — `§2.5`'s named failure mode.
|
||||
|
||||
**So height space got its own contract:** `VoxelHeightOp.h` — `FVoxelHeightSample` (`Height` +
|
||||
`Relief`), `IVoxelHeightOp`, `FVoxelHeightStack`, and five ops in `VoxelHeightOpStack.cpp`.
|
||||
`Relief` is the original's `M`: produced by the structural source, consumed by the terrace gate.
|
||||
Same shape of lesson as `§0.1` — that one found density needed a second *channel*; this found
|
||||
terrain needs a second *space*.
|
||||
|
||||
**A property the type system now gives for free:** a height stack **cannot** hold Z-dependent data,
|
||||
because there is no Z in the signature to put there. `AUDIT §6.3` warns that Z-dependent data
|
||||
smuggled into `FSurfaceColumn` silently corrupts every chunk in the vertical stack and that
|
||||
`ValidateDeterminism` would not catch it. That hazard is now a type error instead of a convention.
|
||||
|
||||
**Deliberately staged.** This step touches **nothing** on the density path — no `FHeightfieldSource`
|
||||
adapter, no `FSkyCapSource`, no `FOverhangShelfMod`, no column cache, no wiring. If height space had
|
||||
not decomposed cleanly, that would show up here for the price of one test rather than after building
|
||||
the adapter, the cache integration and the dispatch on top of it. Same method Phase 1 used on
|
||||
density, applied to the question Phase 2 actually raised.
|
||||
|
||||
**The test runs twice, and the second pass is the one that matters:** the F20 terrain ops are **off
|
||||
by default**, so a defaults-only run would exercise the structural source and leave all four
|
||||
modifiers — i.e. everything new — untested. The second pass turns them all on. It also brute-forces
|
||||
`MaxDisplacement` against observed movement, because a false bound would later be a hole.
|
||||
`WaterLevelRelative` must be set for that pass or `FBeachHeightMod` early-outs and the fifth op is
|
||||
never touched — a green test that measured nothing.
|
||||
|
||||
**Also:** `ComputeSurfaceTerrainZ` moved from private to public on `UVoxelGenerator` (same
|
||||
justification as `GetSlabDensity` / `GetMazeDensity` — exposed for isolated tests). Its old private
|
||||
declaration was removed; two declarations of one member would not compile.
|
||||
|
||||
**UNVERIFIED:** none of this is compiled. Likely error spots: the new `.cpp`/`.h` pair being picked
|
||||
up; `FVoxelHeightStack` as a move-only local; the non-owning `const IVoxelHeightOp**` out-param in
|
||||
`MakeStructuralHeightSource` and its `static_cast` back down in `MakeCliffHeightMod`; `FVector2D`
|
||||
members being double in UE5 (cast at every use); and the `ComputeSurfaceTerrainZ` access move.
|
||||
|
||||
**Next single action:** build + run `SurfaceHeightEquivalence`. If green, step 2 — `FHeightfieldSource`
|
||||
(the adapter that turns a column into density, preserving the T1.a cache), `FSkyCapSource`,
|
||||
`FOverhangShelfMod` (the one genuinely 3D op here), then biome blending and the wiring. `§C1`
|
||||
(bounded seed offsets) still open.
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
// VoxelForgeHeightStackTest.cpp
|
||||
// LA QUESTION D'ARCHITECTURE DE LA PHASE 2, POSÉE AVANT D'ÉCRIRE CE QUI EN DÉPEND.
|
||||
// PHASE 2'S ARCHITECTURAL QUESTION, ASKED BEFORE WRITING WHAT DEPENDS ON THE ANSWER.
|
||||
//
|
||||
// SurfaceWorld a forcé une décision que ni Maze ni Slab n'avaient forcée : ses opérateurs de
|
||||
// terrain (cliff / terrace / layer lines / plage) n'opèrent PAS sur la densité. Ils lisent et
|
||||
// écrivent **une altitude**. Ils ne rentrent donc pas dans `IVoxelDensityOp`, et les y forcer
|
||||
// voudrait dire soit un canal par-voxel pour une propriété de COLONNE, soit un seul opérateur
|
||||
// opaque — ce que `OPSTACK-PLAN §2.5` appelle exactement l'échec du refactor.
|
||||
//
|
||||
// D'où une seconde famille, `VoxelHeightOp.h`. **Ce test est ce qui dit si elle était une bonne
|
||||
// idée** — la même méthode que la Phase 1 a appliquée à la densité : décomposer, puis MESURER
|
||||
// contre l'original, avant de construire par-dessus.
|
||||
//
|
||||
// ⚠️ Ce test ne touche PAS au chemin densité. `FHeightfieldSource` / `FSkyCapSource` /
|
||||
// `FOverhangShelfMod` (OPSTACK-DECOMPOSITION §5) sont l'étape SUIVANTE, délibérément séparée : si
|
||||
// l'espace-hauteur ne se décomposait pas proprement, on l'apprendrait ici, pour le prix d'un test,
|
||||
// et pas après avoir écrit l'adaptateur, le cache de colonne et le branchement.
|
||||
//
|
||||
// LA BARRE : **bit à bit.** Depuis `FPSemantics = Precise` (AUDIT §C9/§C10), Maze et Slab sont
|
||||
// bit-identiques à leur original ; il n'y a plus de « plancher ULP » à tolérer. Un écart ici est
|
||||
// donc une vraie trouvaille — un offset de bruit faux, un ordre d'op inversé, un gate oublié.
|
||||
// Ces fonctions sont des ALTITUDES en voxels, pas des densités : un écart d'un demi-voxel est un
|
||||
// terrain visiblement différent, pas du bruit d'arrondi.
|
||||
|
||||
#if WITH_DEV_AUTOMATION_TESTS
|
||||
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "Async/ParallelFor.h"
|
||||
#include "HAL/PlatformMisc.h"
|
||||
|
||||
#include "VoxelForgeTestFixture.h"
|
||||
#include "VoxelHeightOp.h"
|
||||
|
||||
#include <atomic>
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FVoxelForgeHeightStackTest,
|
||||
"VoxelForge.OpStack.SurfaceHeightEquivalence",
|
||||
EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int32 NumHeightSamples = 20000;
|
||||
|
||||
/** Les params du terrain ne sont intéressants que si les ops sont ALLUMÉS. Ceux de la fixture
|
||||
* sont les défauts, et `§5` note que les ops F20 sont « all off by default ». Un test qui ne
|
||||
* ferait tourner que les défauts vérifierait la source structurelle et RIEN des quatre
|
||||
* modificateurs — c'est-à-dire l'essentiel de ce qui est nouveau ici. */
|
||||
void EnableAllTerrainOps(FSurfaceGenerationParams& P)
|
||||
{
|
||||
P.CliffStrength = 0.6f;
|
||||
P.CliffSampleDist = 2.0f;
|
||||
P.CliffSlopeThreshold = 0.15f;
|
||||
P.CliffSharpness = 1.4f;
|
||||
|
||||
P.TerraceStrength = 0.7f;
|
||||
P.TerraceHeight = 9.0f;
|
||||
P.TerraceHardness = 0.8f;
|
||||
|
||||
P.LayerLineDepth = 1.3f;
|
||||
P.LayerLineSpacing = 7.0f;
|
||||
|
||||
// ⚠️ `WaterLevelRelative` DOIT être > 0, sinon `FBeachHeightMod` sort immédiatement et le
|
||||
// cinquième op n'est jamais exercé — un test vert qui n'a rien testé. Le défaut de la
|
||||
// struct est 0.0f, donc l'oublier est le piège naturel ici.
|
||||
// The beach op early-outs unless WaterLevelRelative > 0, so without this the fifth op is
|
||||
// never exercised at all — a green test that measured nothing.
|
||||
P.WaterLevelRelative = 0.30f;
|
||||
P.BeachWidth = 6.0f;
|
||||
}
|
||||
}
|
||||
|
||||
bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
using namespace VoxelForgeTest;
|
||||
|
||||
FTestWorld World;
|
||||
World.Build();
|
||||
if (!World.IsValid())
|
||||
{
|
||||
AddError(World.WhyInvalid());
|
||||
return false;
|
||||
}
|
||||
|
||||
const UVoxelGenerator* Gen = World.Generator.Get();
|
||||
|
||||
int32 TopVoxelZ = 0, BottomVoxelZ = 0;
|
||||
if (!World.GetSlotVoxelZRange(FTestWorld::SlotSurfaceWorld, TopVoxelZ, BottomVoxelZ))
|
||||
{
|
||||
AddError(TEXT("The fixture layout has no SurfaceWorld slot. Check FTestWorld::Build's ")
|
||||
TEXT("Archetypes[] against FTestWorld::SlotSurfaceWorld."));
|
||||
return false;
|
||||
}
|
||||
|
||||
const int32 MidChunkZ = ((TopVoxelZ + BottomVoxelZ) / 2) / CHUNK_SIZE;
|
||||
|
||||
// Les points d'échantillonnage : XY seulement, la hauteur ne dépend pas de Z (c'est le point).
|
||||
TArray<FVector2D> Points;
|
||||
Points.Reserve(NumHeightSamples);
|
||||
{
|
||||
FRandomStream Rng(90210);
|
||||
for (int32 i = 0; i < NumHeightSamples; ++i)
|
||||
{
|
||||
Points.Add(FVector2D(
|
||||
(float)Rng.RandRange(-6 * CHUNK_SIZE, 6 * CHUNK_SIZE),
|
||||
(float)Rng.RandRange(-6 * CHUNK_SIZE, 6 * CHUNK_SIZE)));
|
||||
}
|
||||
}
|
||||
|
||||
//=========================================================================
|
||||
// LA BATTERIE, PARAMÉTRÉE PAR JEU DE PARAMS
|
||||
//=========================================================================
|
||||
auto RunForParams = [&](const FSurfaceGenerationParams& P, const TCHAR* Label, int32 SeedSalt)
|
||||
{
|
||||
FVoxelHeightStack Stack;
|
||||
VoxelHeightOps::BuildSurfaceHeightStack(Stack, P, World.Settings->Seed);
|
||||
|
||||
// Une DÉCOMPOSITION, pas une enveloppe : source + 4 modificateurs.
|
||||
TestEqual(*FString::Printf(TEXT("%s: the height stack is decomposed into 5 ops"), Label),
|
||||
Stack.Num(), 5);
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// 1. ÉQUIVALENCE — contre ComputeSurfaceTerrainZ, en ALTITUDE
|
||||
//---------------------------------------------------------------------
|
||||
int32 NumDiff = 0, WorstIdx = -1;
|
||||
float WorstDelta = 0.0f, WorstOld = 0.0f;
|
||||
|
||||
for (int32 i = 0; i < NumHeightSamples; ++i)
|
||||
{
|
||||
const float X = (float)Points[i].X, Y = (float)Points[i].Y;
|
||||
|
||||
const float Old = Gen->ComputeSurfaceTerrainZ(X, Y, P);
|
||||
const float New = Stack.EvalHeight(X, Y);
|
||||
|
||||
if (!BitEqual(Old, New))
|
||||
{
|
||||
++NumDiff;
|
||||
const float Delta = FMath::Abs(Old - New);
|
||||
if (Delta > WorstDelta) { WorstDelta = Delta; WorstIdx = i; WorstOld = Old; }
|
||||
}
|
||||
}
|
||||
|
||||
if (NumDiff == 0)
|
||||
{
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("%s: bit-identical across %d samples. The height-space decomposition ")
|
||||
TEXT("reproduces ComputeSurfaceTerrainZ exactly."), Label, NumHeightSamples));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Pas de gradation ULP ici, à dessein : ce sont des ALTITUDES. Depuis /fp:precise la
|
||||
// barre est l'égalité binaire, et un écart de hauteur se voit dans le monde.
|
||||
AddError(FString::Printf(
|
||||
TEXT("%s: %d of %d samples differ from ComputeSurfaceTerrainZ (largest |delta| ")
|
||||
TEXT("%.9g voxels at (%.0f, %.0f), where the reference height is %.4f). These are ")
|
||||
TEXT("ALTITUDES, not densities -- this is a real port error, not rounding. Check, ")
|
||||
TEXT("in order: the op ORDER (structural -> cliff -> terrace -> layer lines -> ")
|
||||
TEXT("beach), the terrace's `* Relief` gate (that is the original's `* M`), the ")
|
||||
TEXT("cliff resampling the STRUCTURAL field rather than the modified height, and ")
|
||||
TEXT("the noise offsets (3.1/5.7/0.7, 11/22/1.3, 99/77/0.9, 7.3/2.1/0.5)."),
|
||||
Label, NumDiff, NumHeightSamples, WorstDelta,
|
||||
WorstIdx >= 0 ? Points[WorstIdx].X : 0.0f,
|
||||
WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f,
|
||||
WorstOld));
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// 2. INVARIANCE DE FENÊTRE
|
||||
//---------------------------------------------------------------------
|
||||
// Une pile de hauteur alimente le cache de colonne T1.a, qui est PARTAGÉ sur toute la pile
|
||||
// verticale de chunks. Une impureté ici ne fait pas une couture locale : elle se propage à
|
||||
// tous les Z d'un coup (AUDIT §6.3).
|
||||
{
|
||||
std::atomic<int32> Impure{ 0 };
|
||||
const int32 NumBlocks = FMath::Max(4, FMath::Min(16, FPlatformMisc::NumberOfCores()));
|
||||
|
||||
TArray<float> Ref;
|
||||
Ref.SetNumUninitialized(NumHeightSamples);
|
||||
for (int32 i = 0; i < NumHeightSamples; ++i)
|
||||
{
|
||||
Ref[i] = Stack.EvalHeight((float)Points[i].X, (float)Points[i].Y);
|
||||
}
|
||||
|
||||
ParallelFor(NumBlocks, [&](int32 Block)
|
||||
{
|
||||
TArray<int32> LocalOrder;
|
||||
BuildShuffledOrder(NumHeightSamples, 1200 + Block + SeedSalt, LocalOrder);
|
||||
for (const int32 i : LocalOrder)
|
||||
{
|
||||
const float V = Stack.EvalHeight((float)Points[i].X, (float)Points[i].Y);
|
||||
if (!BitEqual(V, Ref[i])) { Impure.fetch_add(1, std::memory_order_relaxed); }
|
||||
}
|
||||
});
|
||||
|
||||
TestEqual(*FString::Printf(
|
||||
TEXT("%s: the height stack is window-invariant across order and threads"), Label),
|
||||
Impure.load(), 0);
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// 3. LES MAJORANTS DE DÉPLACEMENT SONT-ILS HONNÊTES ?
|
||||
//---------------------------------------------------------------------
|
||||
// `MaxDisplacement` servira à borner une colonne pour un `ClassifyBox` de heightfield, la
|
||||
// même mécanique qui fait prouver 36-40 tuiles sur 60 à la dalle. Un majorant FAUX serait
|
||||
// un TROU, donc on le teste par force brute AVANT de construire quoi que ce soit dessus.
|
||||
//
|
||||
// On mesure le déplacement des trois mods bornables en comparant la pile complète à une
|
||||
// pile tronquée (source + cliff seuls) : la différence est exactement ce que terrace +
|
||||
// layer lines + plage ont déplacé.
|
||||
{
|
||||
FVoxelHeightStack Base;
|
||||
const IVoxelHeightOp* Structural = nullptr;
|
||||
Base.Add(VoxelHeightOps::MakeStructuralHeightSource(P, World.Settings->Seed, &Structural));
|
||||
Base.Add(VoxelHeightOps::MakeCliffHeightMod(P, Structural));
|
||||
|
||||
FVoxelHeightStack Bounded;
|
||||
const IVoxelHeightOp* Structural2 = nullptr;
|
||||
Bounded.Add(VoxelHeightOps::MakeStructuralHeightSource(P, World.Settings->Seed, &Structural2));
|
||||
Bounded.Add(VoxelHeightOps::MakeCliffHeightMod(P, Structural2));
|
||||
Bounded.Add(VoxelHeightOps::MakeTerraceHeightMod(P));
|
||||
Bounded.Add(VoxelHeightOps::MakeLayerLineHeightMod(P));
|
||||
Bounded.Add(VoxelHeightOps::MakeBeachHeightMod(P));
|
||||
|
||||
const float Claimed = FMath::Max(P.TerraceStrength > 0.0f ? P.TerraceHeight : 0.0f, 0.0f)
|
||||
+ FMath::Max(P.LayerLineSpacing > 0.0f ? P.LayerLineDepth : 0.0f, 0.0f)
|
||||
+ FMath::Max(P.WaterLevelRelative > 0.0f ? P.BeachWidth : 0.0f, 0.0f);
|
||||
|
||||
float WorstObserved = 0.0f;
|
||||
int32 NumOverBound = 0;
|
||||
for (int32 i = 0; i < NumHeightSamples; ++i)
|
||||
{
|
||||
const float X = (float)Points[i].X, Y = (float)Points[i].Y;
|
||||
const float Moved = FMath::Abs(Bounded.EvalHeight(X, Y) - Base.EvalHeight(X, Y));
|
||||
WorstObserved = FMath::Max(WorstObserved, Moved);
|
||||
if (Moved > Claimed) { ++NumOverBound; }
|
||||
}
|
||||
|
||||
TestEqual(*FString::Printf(
|
||||
TEXT("%s: no sample exceeds the claimed MaxDisplacement (a false bound is a hole)"),
|
||||
Label),
|
||||
NumOverBound, 0);
|
||||
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("%s: MaxDisplacement claims %.3f voxels, worst observed %.3f (%.0f%% of the ")
|
||||
TEXT("claim). A loose bound only costs CPU later; a tight-but-wrong one would be a hole."),
|
||||
Label, Claimed, WorstObserved,
|
||||
Claimed > 0.0f ? 100.0f * WorstObserved / Claimed : 0.0f));
|
||||
}
|
||||
};
|
||||
|
||||
//=========================================================================
|
||||
// DEUX PASSES — et la seconde est celle qui compte
|
||||
//=========================================================================
|
||||
const UVoxelStrateDefinition* SurfaceDef =
|
||||
World.StrateManager->GetStrateForChunk(FIntVector(0, 0, MidChunkZ));
|
||||
if (!SurfaceDef)
|
||||
{
|
||||
AddError(TEXT("No strate definition resolved for the SurfaceWorld slot's mid chunk."));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Les bornes Z de runtime sont posées à la main : `GetSlabParamsForChunk` a un équivalent pour
|
||||
// la dalle, mais le chemin surface passe par `ResolveSurfaceChunkParams`, qui est privé et
|
||||
// mêle la résolution de biome. La pile de hauteur ne dépend que des params + du seed, donc
|
||||
// fournir les params directement est à la fois suffisant et plus lisible en cas d'échec.
|
||||
FSurfaceGenerationParams Defaults = SurfaceDef->SurfaceParams;
|
||||
Defaults.StrateTopWorldZ = (float)TopVoxelZ;
|
||||
Defaults.StrateBottomWorldZ = (float)BottomVoxelZ;
|
||||
|
||||
RunForParams(Defaults, TEXT("SurfaceWorld(defaults)"), 0);
|
||||
|
||||
// ⚠️ LA PASSE LOAD-BEARING. Les ops de terrain F20 sont éteints par défaut, donc la passe
|
||||
// ci-dessus n'exerce que la source structurelle et laisse les QUATRE modificateurs — c'est-à-
|
||||
// dire tout ce qui est nouveau dans cette décomposition — non testés. Celle-ci les allume.
|
||||
FSurfaceGenerationParams AllOps = Defaults;
|
||||
EnableAllTerrainOps(AllOps);
|
||||
RunForParams(AllOps, TEXT("SurfaceWorld(all terrain ops on)"), 64);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif // WITH_DEV_AUTOMATION_TESTS
|
||||
@@ -8,8 +8,26 @@
|
||||
// sans contorsion, le SDF est reproduit BIT POUR BIT, et aucun échantillon ne change de côté de
|
||||
// l'isosurface.
|
||||
//
|
||||
// ═════════════════════════════════════════════════════════════════════════════════════════
|
||||
// ✅ MISE À JOUR 2026-07-27 : LE PLANCHER ULP N'EXISTE PLUS. C'ÉTAIT `/fp:fast`.
|
||||
// ═════════════════════════════════════════════════════════════════════════════════════════
|
||||
// `FPSemantics = Precise` sur le module (AUDIT §C9, posé pour le cross-play Linux/Windows) fait
|
||||
// passer ce test à **BIT-IDENTIQUE**. La section ci-dessous décrit un état RÉVOLU ; elle est gardée
|
||||
// parce qu'elle explique pourquoi les cinq expériences d'isolation avaient toutes échoué (sous
|
||||
// `/fp:fast` le compilateur transforme selon le CONTEXTE — il n'y avait aucune variable à isoler)
|
||||
// et parce qu'elle dit quoi regarder si la bit-identité régresse un jour.
|
||||
//
|
||||
// **Conséquence pratique : ce test est maintenant un instrument BEAUCOUP plus fin.** Le moindre
|
||||
// écart est désormais une vraie trouvaille, pas du bruit à noter. La machinerie de gradation ULP
|
||||
// est conservée exprès — c'est elle qui signalerait une régression du modèle flottant.
|
||||
//
|
||||
// UPDATE: the ULP floor is GONE — FPSemantics = Precise makes this test bit-identical. The section
|
||||
// below describes a past state, kept because it explains why five isolation experiments all failed
|
||||
// (under /fp:fast the compiler transforms by CONTEXT — there was no variable to isolate) and what to
|
||||
// look at if bit-identity ever regresses.
|
||||
//
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────
|
||||
// ⚠️ LE PLANCHER ULP — lire ceci avant de « corriger » un écart résiduel
|
||||
// ⚠️ LE PLANCHER ULP (HISTORIQUE) — lire ceci avant de « corriger » un écart résiduel
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────
|
||||
// La pile reproduit `GetMazeDensity` à ~1-2 ULP près sur ~2 % des échantillons (ceux qui tombent
|
||||
// dans la coquille de blend du SDF, où `Blend - Sdf` annule catastrophiquement et amplifie le
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
// CE QUE CE TEST DOIT PROUVER / WHAT THIS TEST HAS TO PROVE
|
||||
// Trois choses, et la troisième est la raison d'être du portage :
|
||||
//
|
||||
// 1. ÉQUIVALENCE — la pile reproduit `GetSlabDensity`. Même barre que Maze : un changement de
|
||||
// côté d'isosurface est un ÉCHEC DUR, un écart d'ULP est le plancher accepté.
|
||||
// 1. ÉQUIVALENCE — la pile reproduit `GetSlabDensity`. ✅ **BIT-IDENTIQUE depuis 2026-07-27**,
|
||||
// quand `FPSemantics = Precise` (AUDIT §C9/§C10) a supprimé le résidu d'ULP : il venait de
|
||||
// `/fp:fast`. Un changement de côté d'isosurface reste l'ÉCHEC DUR ; la gradation ULP est
|
||||
// gardée comme détecteur de régression du modèle flottant, pas comme tolérance attendue.
|
||||
// 2. UN OPÉRATEUR, DEUX ARCHÉTYPES — la MÊME pile est vérifiée contre FlatPlain ET
|
||||
// CrystalChamber. `GetSlabDensity` ne les distingue par aucun branchement ; si la pile a
|
||||
// besoin d'en faire un, la fusion est fausse et ce test le dit.
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
// VoxelHeightOpStack.cpp
|
||||
// Les cinq opérateurs d'espace-hauteur de SurfaceWorld.
|
||||
// The five height-space operators of SurfaceWorld.
|
||||
//
|
||||
// FIDÉLITÉ / FIDELITY
|
||||
// Chaque corps est une transcription LITTÉRALE du bloc correspondant de
|
||||
// `SampleSurfaceStructuralZ` / `ComputeSurfaceTerrainZ` — mêmes offsets, mêmes octaves, même ordre
|
||||
// d'opérations flottantes. Depuis que `FPSemantics = Precise` est posé (AUDIT §C9), l'égalité
|
||||
// BIT À BIT est atteignable et atteinte pour Maze et Slab : c'est donc la barre ici aussi, et
|
||||
// `VoxelForge.OpStack.SurfaceHeightEquivalence` la vérifie.
|
||||
//
|
||||
// ⚠️ LE DÉTOUR PAR `FVector` EST DÉLIBÉRÉ, comme ailleurs dans ce refactor : `FractalNoise3D` prend
|
||||
// un `FVector` (donc des DOUBLES en UE5) et re-descend en float. Passer directement des floats
|
||||
// saute un arrondi. Reproduire le détour, c'est reproduire l'arrondi.
|
||||
// The FVector round-trip is deliberate: FVector is double in UE5, so the original rounds through a
|
||||
// double. Going straight through floats skips a rounding step.
|
||||
|
||||
#include "VoxelHeightOp.h"
|
||||
|
||||
#include "VoxelNoise.h" // VoxelNoise::FBM / Ridged / Perlin3D
|
||||
#include "VoxelTypes.h" // SmoothStep01, VOXEL_NOISE_SCALE
|
||||
|
||||
namespace
|
||||
{
|
||||
//=========================================================================
|
||||
// HELPERS — les mêmes enveloppes que VoxelGenerator.cpp, transcrites
|
||||
//=========================================================================
|
||||
// `FractalNoise3D` et `RidgedNoise3D` sont `static` dans VoxelGenerator.cpp, donc invisibles
|
||||
// ici. Elles sont recopiées à l'identique plutôt qu'exportées : les exporter changerait leur
|
||||
// contexte d'inlining, et sous /fp:precise comme sous /fp:fast la règle est la même — on ne
|
||||
// touche à rien de ce qui entoure une expression flottante qu'on veut reproduire.
|
||||
FORCEINLINE float HFractalNoise3D(const FVector& Position, int32 Octaves = 4,
|
||||
float Lacunarity = 2.0f, float Persistence = 0.5f)
|
||||
{
|
||||
return VoxelNoise::FBM((float)Position.X, (float)Position.Y, (float)Position.Z,
|
||||
Octaves, Lacunarity, Persistence);
|
||||
}
|
||||
|
||||
FORCEINLINE float HRidgedNoise3D(const FVector& Position, int32 Octaves = 4,
|
||||
float Lacunarity = 2.0f, float Persistence = 0.5f)
|
||||
{
|
||||
return VoxelNoise::Ridged((float)Position.X, (float)Position.Y, (float)Position.Z,
|
||||
Octaves, Lacunarity, Persistence);
|
||||
}
|
||||
|
||||
/** Transcription de `UVoxelGenerator::SampleRelief`. Champ [0,1] partagé avec la carte de
|
||||
* biomes, pour que la géographie et le terrain qu'elle module restent d'accord. */
|
||||
FORCEINLINE float HSampleRelief(float WorldX, float WorldY, float SeedF,
|
||||
float Frequency, float Contrast)
|
||||
{
|
||||
float R = HFractalNoise3D(FVector(
|
||||
WorldX * Frequency + SeedF * 7.3f,
|
||||
WorldY * Frequency + SeedF * 2.1f,
|
||||
SeedF * 0.5f), 2) * 0.5f + 0.5f; // [0,1]
|
||||
R = FMath::Clamp((R - 0.5f) * Contrast + 0.5f, 0.0f, 1.0f);
|
||||
return SmoothStep01(R);
|
||||
}
|
||||
|
||||
//=========================================================================
|
||||
// SOURCE — CHAMP STRUCTUREL / STRUCTURAL HEIGHT FIELD
|
||||
//=========================================================================
|
||||
// Continents + montagnes + détail, sous une frame de domain-warp. Produit les DEUX canaux.
|
||||
class FStructuralHeightSource final : public IVoxelHeightOp
|
||||
{
|
||||
public:
|
||||
FStructuralHeightSource(const FSurfaceGenerationParams& InP, int32 InSeed)
|
||||
: P(InP), SeedF((float)InSeed) {}
|
||||
|
||||
void Eval(float WorldX, float WorldY, FVoxelHeightSample& InOut) const override
|
||||
{
|
||||
float M = 1.0f;
|
||||
InOut.Height = SampleZ(WorldX, WorldY, M); // Replace : racine de pile
|
||||
InOut.Relief = M;
|
||||
}
|
||||
|
||||
/**
|
||||
* Le champ nu, exposé parce que `FCliffHeightMod` doit le RÉ-ÉCHANTILLONNER en différences
|
||||
* centrées. C'est une dépendance réelle du code d'origine (`ComputeSurfaceTerrainZ` appelle
|
||||
* `SampleSurfaceStructuralZ` quatre fois de plus), pas un raccourci : la pente doit venir du
|
||||
* champ STRUCTUREL, sans rétroaction des ops, sinon le cliff se nourrirait de lui-même.
|
||||
*/
|
||||
float SampleZ(float WorldX, float WorldY, float& OutM) const
|
||||
{
|
||||
const float H = P.StrateTopWorldZ - P.StrateBottomWorldZ;
|
||||
const float BottomZ = P.StrateBottomWorldZ;
|
||||
|
||||
const float GroundBase = BottomZ + H * P.BaseGroundRelative;
|
||||
|
||||
// Domain-warp des coords STRUCTURELLES (continents + montagnes). Le bruit de détail
|
||||
// reste sur le vrai XY pour que les bosses fines restent nettes et décorrélées.
|
||||
float QX = WorldX, QY = WorldY;
|
||||
if (P.HeightWarpStrength > 0.0f)
|
||||
{
|
||||
const float WF = P.HeightWarpFrequency;
|
||||
const float wx = VoxelNoise::Perlin3D(FVector(WorldX * WF + SeedF * 0.31f, WorldY * WF + 4.2f, SeedF * 1.7f));
|
||||
const float wy = VoxelNoise::Perlin3D(FVector(WorldX * WF + 8.6f, WorldY * WF + SeedF * 0.53f, SeedF * 2.9f));
|
||||
QX += wx * VOXEL_NOISE_SCALE * P.HeightWarpStrength;
|
||||
QY += wy * VOXEL_NOISE_SCALE * P.HeightWarpStrength;
|
||||
}
|
||||
|
||||
const float Relief = HSampleRelief(WorldX, WorldY, SeedF, P.ReliefFrequency, P.ReliefContrast);
|
||||
const float M = FMath::Lerp(1.0f, Relief, P.ReliefStrength);
|
||||
|
||||
float Cont = HFractalNoise3D(FVector(
|
||||
QX * P.ContinentFrequency + SeedF * 3.1f,
|
||||
QY * P.ContinentFrequency + SeedF * 5.7f,
|
||||
SeedF * 0.7f), 4); // [-1,1]
|
||||
|
||||
float Detail = HFractalNoise3D(FVector(
|
||||
WorldX * P.DetailFrequency + 11.0f,
|
||||
WorldY * P.DetailFrequency + 22.0f,
|
||||
SeedF * 1.3f), 3); // [-1,1]
|
||||
|
||||
float Mountain = 0.0f;
|
||||
if (P.MountainStrength > 0.0f)
|
||||
{
|
||||
float Ridge = HRidgedNoise3D(FVector(
|
||||
QX * P.MountainFrequency + 99.0f,
|
||||
QY * P.MountainFrequency + 77.0f,
|
||||
SeedF * 0.9f), 4); // [-1,1]
|
||||
Ridge = Ridge * 0.5f + 0.5f; // [0,1] sommets
|
||||
Mountain = Ridge * P.MountainStrength * M; // les montagnes ne montent qu'en haut relief
|
||||
}
|
||||
|
||||
// Les plaines gardent une fraction du gonflement continental ; les hautes terres tout.
|
||||
const float ContScale = FMath::Lerp(0.45f, 1.0f, M);
|
||||
|
||||
float Terrain = GroundBase
|
||||
+ Cont * P.ElevationRange * 0.5f * ContScale
|
||||
+ Mountain * P.ElevationRange
|
||||
+ Detail * P.SurfaceRoughness;
|
||||
|
||||
OutM = M;
|
||||
return Terrain;
|
||||
}
|
||||
|
||||
// Une SOURCE pose l'altitude, elle ne la déplace pas : la notion de « déplacement max » ne
|
||||
// s'applique pas. La borne d'une colonne se calcule à partir de la source elle-même
|
||||
// (GroundBase ± ElevationRange ± SurfaceRoughness), pas ici — d'où FLT_MAX, honnête.
|
||||
float MaxDisplacement() const override { return FLT_MAX; }
|
||||
|
||||
private:
|
||||
FSurfaceGenerationParams P;
|
||||
float SeedF;
|
||||
};
|
||||
|
||||
//=========================================================================
|
||||
// MOD — FALAISE / CLIFF (raidissement conditionné par la pente)
|
||||
//=========================================================================
|
||||
class FCliffHeightMod final : public IVoxelHeightOp
|
||||
{
|
||||
public:
|
||||
FCliffHeightMod(const FSurfaceGenerationParams& InP, const FStructuralHeightSource* InSrc)
|
||||
: P(InP), Src(InSrc) {}
|
||||
|
||||
void Eval(float WorldX, float WorldY, FVoxelHeightSample& InOut) const override
|
||||
{
|
||||
if (P.CliffStrength <= 0.0f || Src == nullptr) { return; }
|
||||
|
||||
const float D = FMath::Max(P.CliffSampleDist, 0.5f);
|
||||
float Ms; // relief scratch — on ne veut que les hauteurs
|
||||
const float Zxp = Src->SampleZ(WorldX + D, WorldY, Ms);
|
||||
const float Zxm = Src->SampleZ(WorldX - D, WorldY, Ms);
|
||||
const float Zyp = Src->SampleZ(WorldX, WorldY + D, Ms);
|
||||
const float Zym = Src->SampleZ(WorldX, WorldY - D, Ms);
|
||||
const float dZdX = (Zxp - Zxm) / (2.0f * D);
|
||||
const float dZdY = (Zyp - Zym) / (2.0f * D);
|
||||
const float Slope = FMath::Sqrt(dZdX * dZdX + dZdY * dZdY);
|
||||
|
||||
const float Thr = FMath::Max(P.CliffSlopeThreshold, 0.05f);
|
||||
const float SlopeGate = FMath::Clamp((Slope - Thr) / Thr, 0.0f, 1.0f);
|
||||
if (SlopeGate > 0.0f)
|
||||
{
|
||||
const float Ref = 0.25f * (Zxp + Zxm + Zyp + Zym);
|
||||
const float Gain = P.CliffStrength * SlopeGate * P.CliffSharpness;
|
||||
InOut.Height += (InOut.Height - Ref) * Gain;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
FSurfaceGenerationParams P;
|
||||
const FStructuralHeightSource* Src;
|
||||
};
|
||||
|
||||
//=========================================================================
|
||||
// MOD — TERRASSES / TERRACE (gaté par le relief : le canal Relief sert ICI)
|
||||
//=========================================================================
|
||||
class FTerraceHeightMod final : public IVoxelHeightOp
|
||||
{
|
||||
public:
|
||||
explicit FTerraceHeightMod(const FSurfaceGenerationParams& InP) : P(InP) {}
|
||||
|
||||
void Eval(float, float, FVoxelHeightSample& InOut) const override
|
||||
{
|
||||
if (P.TerraceStrength <= 0.0f || P.TerraceHeight <= 0.0f) { return; }
|
||||
|
||||
const float StepH = P.TerraceHeight;
|
||||
const float T = InOut.Height / StepH;
|
||||
const float K = FMath::FloorToFloat(T);
|
||||
const float Frac = T - K;
|
||||
const float W = FMath::Lerp(0.5f, 0.03f, FMath::Clamp(P.TerraceHardness, 0.0f, 1.0f));
|
||||
const float Fs = SmoothStep01(FMath::Clamp((Frac - (0.5f - W)) / (2.0f * W), 0.0f, 1.0f));
|
||||
const float Stepped = (K + Fs) * StepH;
|
||||
// `* InOut.Relief` : c'est le `* M` de l'original — la raison d'être du second canal.
|
||||
InOut.Height = FMath::Lerp(InOut.Height, Stepped, P.TerraceStrength * InOut.Relief);
|
||||
}
|
||||
|
||||
// Le terrace interpole VERS une hauteur quantifiée : l'écart ne dépasse jamais un palier.
|
||||
float MaxDisplacement() const override
|
||||
{
|
||||
return (P.TerraceStrength > 0.0f) ? FMath::Max(P.TerraceHeight, 0.0f) : 0.0f;
|
||||
}
|
||||
|
||||
private:
|
||||
FSurfaceGenerationParams P;
|
||||
};
|
||||
|
||||
//=========================================================================
|
||||
// MOD — LIGNES DE STRATES / LAYER LINES
|
||||
//=========================================================================
|
||||
class FLayerLineHeightMod final : public IVoxelHeightOp
|
||||
{
|
||||
public:
|
||||
explicit FLayerLineHeightMod(const FSurfaceGenerationParams& InP) : P(InP) {}
|
||||
|
||||
void Eval(float, float, FVoxelHeightSample& InOut) const override
|
||||
{
|
||||
if (P.LayerLineDepth <= 0.0f || P.LayerLineSpacing <= 0.0f) { return; }
|
||||
|
||||
const float Phase = InOut.Height * (2.0f * PI / P.LayerLineSpacing);
|
||||
InOut.Height -= FMath::Sin(Phase) * P.LayerLineDepth;
|
||||
}
|
||||
|
||||
// `sin` ∈ [-1,1] ⇒ borne exacte.
|
||||
float MaxDisplacement() const override
|
||||
{
|
||||
return (P.LayerLineSpacing > 0.0f) ? FMath::Max(P.LayerLineDepth, 0.0f) : 0.0f;
|
||||
}
|
||||
|
||||
private:
|
||||
FSurfaceGenerationParams P;
|
||||
};
|
||||
|
||||
//=========================================================================
|
||||
// MOD — PLAGE / BEACH (aplatissement vers la ligne d'eau)
|
||||
//=========================================================================
|
||||
class FBeachHeightMod final : public IVoxelHeightOp
|
||||
{
|
||||
public:
|
||||
explicit FBeachHeightMod(const FSurfaceGenerationParams& InP) : P(InP) {}
|
||||
|
||||
void Eval(float, float, FVoxelHeightSample& InOut) const override
|
||||
{
|
||||
// Le niveau d'eau est GLOBAL à la strate (forcé depuis la strate) pour que le plan
|
||||
// d'eau reste continu — d'où le calcul depuis les bornes de strate, pas depuis un param
|
||||
// par biome.
|
||||
const float H = P.StrateTopWorldZ - P.StrateBottomWorldZ;
|
||||
const float WaterZ = P.StrateBottomWorldZ + H * P.WaterLevelRelative;
|
||||
if (P.WaterLevelRelative <= 0.0f || P.BeachWidth <= 0.0f) { return; }
|
||||
|
||||
const float DAbs = FMath::Abs(InOut.Height - WaterZ);
|
||||
if (DAbs < P.BeachWidth)
|
||||
{
|
||||
float T = SmoothStep01(DAbs / P.BeachWidth);
|
||||
InOut.Height = FMath::Lerp(WaterZ, InOut.Height, T);
|
||||
}
|
||||
}
|
||||
|
||||
// N'agit que dans `BeachWidth` de l'eau, et ne fait qu'y RAPPROCHER.
|
||||
float MaxDisplacement() const override
|
||||
{
|
||||
return (P.WaterLevelRelative > 0.0f) ? FMath::Max(P.BeachWidth, 0.0f) : 0.0f;
|
||||
}
|
||||
|
||||
private:
|
||||
FSurfaceGenerationParams P;
|
||||
};
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// FABRIQUES / FACTORIES
|
||||
//=============================================================================
|
||||
|
||||
namespace VoxelHeightOps
|
||||
{
|
||||
TUniquePtr<IVoxelHeightOp> MakeStructuralHeightSource(const FSurfaceGenerationParams& P, int32 Seed,
|
||||
const IVoxelHeightOp** OutSource)
|
||||
{
|
||||
TUniquePtr<FStructuralHeightSource> Src = MakeUnique<FStructuralHeightSource>(P, Seed);
|
||||
if (OutSource) { *OutSource = Src.Get(); }
|
||||
return Src;
|
||||
}
|
||||
|
||||
TUniquePtr<IVoxelHeightOp> MakeCliffHeightMod(const FSurfaceGenerationParams& P,
|
||||
const IVoxelHeightOp* StructuralSource)
|
||||
{
|
||||
// `static_cast` plutôt que `Cast<>` : ce ne sont pas des UObject, et le contrat de la
|
||||
// fabrique est qu'on lui rend exactement le pointeur sorti de MakeStructuralHeightSource.
|
||||
return MakeUnique<FCliffHeightMod>(
|
||||
P, static_cast<const FStructuralHeightSource*>(StructuralSource));
|
||||
}
|
||||
|
||||
TUniquePtr<IVoxelHeightOp> MakeTerraceHeightMod(const FSurfaceGenerationParams& P)
|
||||
{
|
||||
return MakeUnique<FTerraceHeightMod>(P);
|
||||
}
|
||||
|
||||
TUniquePtr<IVoxelHeightOp> MakeLayerLineHeightMod(const FSurfaceGenerationParams& P)
|
||||
{
|
||||
return MakeUnique<FLayerLineHeightMod>(P);
|
||||
}
|
||||
|
||||
TUniquePtr<IVoxelHeightOp> MakeBeachHeightMod(const FSurfaceGenerationParams& P)
|
||||
{
|
||||
return MakeUnique<FBeachHeightMod>(P);
|
||||
}
|
||||
|
||||
void BuildSurfaceHeightStack(FVoxelHeightStack& OutStack, const FSurfaceGenerationParams& P, int32 Seed)
|
||||
{
|
||||
// L'ORDRE EST CELUI DE `ComputeSurfaceTerrainZ`, et il porte du sens :
|
||||
// le cliff raidit le champ brut, le terrace quantifie le résultat raidi, les lignes de
|
||||
// strates se posent dessus, et la plage écrase tout près de l'eau.
|
||||
const IVoxelHeightOp* Structural = nullptr;
|
||||
OutStack.Add(MakeStructuralHeightSource(P, Seed, &Structural));
|
||||
OutStack.Add(MakeCliffHeightMod(P, Structural));
|
||||
OutStack.Add(MakeTerraceHeightMod(P));
|
||||
OutStack.Add(MakeLayerLineHeightMod(P));
|
||||
OutStack.Add(MakeBeachHeightMod(P));
|
||||
}
|
||||
}
|
||||
@@ -177,6 +177,17 @@ public:
|
||||
*/
|
||||
float SampleRelief(float WorldX, float WorldY, float Frequency, float Contrast) const;
|
||||
|
||||
/**
|
||||
* La chaîne de hauteur complète de SurfaceWorld : structural → cliff → terrace → layer lines →
|
||||
* plage. Rend une ALTITUDE monde en voxels, pas une densité.
|
||||
*
|
||||
* PUBLIQUE pour la même raison que `GetSlabDensity` / `GetMazeDensity` : permettre un test
|
||||
* isolé. C'est la référence de `VoxelForge.OpStack.SurfaceHeightEquivalence`, qui compare la
|
||||
* pile d'opérateurs de hauteur (`VoxelHeightOp.h`) à cette fonction point par point.
|
||||
* Public so the height-op stack can be measured against it — same reason as GetSlabDensity.
|
||||
*/
|
||||
float ComputeSurfaceTerrainZ(float WorldX, float WorldY, const FSurfaceGenerationParams& Params) const;
|
||||
|
||||
/**
|
||||
* Moisture field at a world XY → [0,1]. The second climate axis for biome placement.
|
||||
*/
|
||||
@@ -267,9 +278,9 @@ private:
|
||||
/** Pick the biome (index into Ctx.Biomes) for a Voronoi site, by its climate. */
|
||||
int32 ClassifyBiomeAtSite(float SiteX, float SiteY, const FBiomeContext& Ctx, uint32 SiteHash) const;
|
||||
|
||||
/** The SurfaceWorld heightfield: world XY → terrain surface Z (voxel coords). Pure
|
||||
* per-XY; the part that's evaluated per biome and blended in GetSurfaceDensity. */
|
||||
float ComputeSurfaceTerrainZ(float WorldX, float WorldY, const FSurfaceGenerationParams& Params) const;
|
||||
// ComputeSurfaceTerrainZ a été DÉPLACÉE en `public` (voir plus haut) pour que
|
||||
// VoxelForge.OpStack.SurfaceHeightEquivalence puisse s'y comparer. Une seule déclaration.
|
||||
// Moved to public above so the height-stack test can compare against it. One declaration only.
|
||||
|
||||
/** F20 — the RAW structural heightfield (continents + mountains + detail), BEFORE any
|
||||
* terrain op (cliff/terrace/layer-lines/beach). Ops in ComputeSurfaceTerrainZ build on
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
// VoxelHeightOp.h
|
||||
// L'ESPACE DES HAUTEURS — une seconde famille d'opérateurs, et pourquoi elle DOIT exister.
|
||||
// HEIGHT SPACE — a second operator family, and why it has to exist.
|
||||
//
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────
|
||||
// LE CONSTAT QUI FORCE CE FICHIER
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────
|
||||
// `OPSTACK-DECOMPOSITION §5` décompose SurfaceWorld ainsi :
|
||||
//
|
||||
// FHeightfieldSource ← toute la chaîne de colonne, XY-pure
|
||||
// ├─ FStructuralHeightField
|
||||
// ├─ FCliffHeightMod
|
||||
// ├─ FTerraceHeightMod
|
||||
// ├─ FLayerLineHeightMod
|
||||
// └─ FBeachHeightMod
|
||||
//
|
||||
// et note, sans en tirer la conséquence : *« les ops de hauteur opèrent sur des valeurs Z dans la
|
||||
// colonne, pas sur la densité »*. En lisant `ComputeSurfaceTerrainZ`, c'est littéralement vrai :
|
||||
// c'est une suite de blocs qui lisent et écrivent **un seul float `Terrain`**, une altitude.
|
||||
//
|
||||
// **Ils ne rentrent donc PAS dans `IVoxelDensityOp`.** Sa signature est
|
||||
// `Eval(x, y, z, FVoxelOpSample&)` — par voxel, deux canaux densité/SDF. Un op de hauteur n'a pas
|
||||
// de Z d'entrée (il en PRODUIT un), ne veut pas être appelé par voxel (il est XY-pur, une fois par
|
||||
// colonne), et n'écrit ni densité ni SDF. Les forcer dans le contrat densité demanderait soit un
|
||||
// troisième canal par voxel — alors que la hauteur est une propriété de COLONNE, pas de voxel —,
|
||||
// soit de replier les cinq en un seul op opaque, ce que `§2.5` appelle précisément l'échec du
|
||||
// refactor.
|
||||
//
|
||||
// **Donc : une seconde famille, dans son propre espace.** C'est la même leçon que `§0.1` (il fallait
|
||||
// un canal SDF en plus de la densité), un cran plus loin : certaines choses ne sont pas un canal de
|
||||
// plus, elles sont un ESPACE de plus.
|
||||
//
|
||||
// The height ops read and write a single float ALTITUDE. They have no input Z (they produce one),
|
||||
// are XY-pure (once per column, not per voxel), and write neither density nor SDF. Forcing them into
|
||||
// IVoxelDensityOp would need either a per-voxel third channel for what is a COLUMN property, or
|
||||
// collapsing all five into one opaque op — which §2.5 calls the failure mode. Hence a second family.
|
||||
//
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────
|
||||
// CE QUE ÇA ACHÈTE / WHAT IT BUYS
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────
|
||||
// • **Le cache de colonne T1.a tombe naturellement.** Une pile de hauteur est XY-pure PAR
|
||||
// CONSTRUCTION — il n'y a pas de Z à mettre dedans par erreur. `AUDIT §6.3` avertit qu'une donnée
|
||||
// dépendante de Z glissée dans `FSurfaceColumn` corrompt silencieusement toute la pile verticale
|
||||
// de chunks, et que `ValidateDeterminism` ne le verrait pas. Ici c'est le TYPE qui l'interdit.
|
||||
// • **La composition d'idées de terrain devient de l'authoring**, comme pour la densité.
|
||||
// • Les mêmes ops resserviront à VerticalShafts (ledges) et FloatingIslands.
|
||||
//
|
||||
// ⚠️ CE FICHIER NE TOUCHE PAS AU JEU. Il est bâti et exercé par
|
||||
// `VoxelForge.OpStack.SurfaceHeightEquivalence`, qui le compare à `ComputeSurfaceTerrainZ` point par
|
||||
// point. Le branchement dans le chemin densité est l'étape SUIVANTE (§5 : `FHeightfieldSource`,
|
||||
// `FSkyCapSource`, `FOverhangShelfMod`), délibérément séparée pour que la question d'architecture
|
||||
// — *« l'espace des hauteurs se décompose-t-il vraiment ? »* — reçoive une réponse MESURÉE avant
|
||||
// qu'on écrive l'adaptateur qui en dépend.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Templates/UniquePtr.h"
|
||||
#include "VoxelStrateTypes.h" // FSurfaceGenerationParams
|
||||
|
||||
/**
|
||||
* L'état qui traverse une pile de hauteur. DEUX canaux, exactement comme `FVoxelOpSample` — et
|
||||
* pour la même raison : le code le fait déjà.
|
||||
*
|
||||
* `Relief` (le `M` de `SampleSurfaceStructuralZ`) est PRODUIT par la source structurelle et CONSOMMÉ
|
||||
* par le gate du terrace (`TerraceStrength * M`). Sans ce second canal, le terrace devrait
|
||||
* ré-échantillonner le champ de relief — plus lent, et surtout une occasion de diverger de la valeur
|
||||
* que la source a réellement utilisée.
|
||||
*
|
||||
* Two channels, for the same reason as FVoxelOpSample: Relief (the `M` of the structural field) is
|
||||
* produced by the source and consumed by the terrace gate. Threading it beats resampling it.
|
||||
*/
|
||||
struct FVoxelHeightSample
|
||||
{
|
||||
/** Altitude monde en VOXELS (pas cm). */
|
||||
float Height = 0.0f;
|
||||
|
||||
/** « Montagnosité » [0,1]. 1 = uniforme (ReliefStrength = 0). */
|
||||
float Relief = 1.0f;
|
||||
};
|
||||
|
||||
/**
|
||||
* Un opérateur d'espace-hauteur. Trois différences avec `IVoxelDensityOp`, toutes voulues :
|
||||
* • pas de Z d'entrée — la pile en PRODUIT un ;
|
||||
* • XY-pur par construction, donc pas de `IsXYPure()` à déclarer ni à oublier ;
|
||||
* • pas de `PrepareChunk` — ces ops sont déjà appelés une fois par colonne, ce qui EST la
|
||||
* granularité que `PrepareChunk` sert à obtenir côté densité.
|
||||
*/
|
||||
class IVoxelHeightOp
|
||||
{
|
||||
public:
|
||||
virtual ~IVoxelHeightOp() = default;
|
||||
|
||||
/**
|
||||
* INVARIANCE DE FENÊTRE (ARCHITECTURE §8.4) : fonction PURE de (X, Y, seed, params). Le même XY
|
||||
* évalué depuis une autre tuile, un autre ordre, un autre thread doit rendre le float
|
||||
* BIT-IDENTIQUE — le cache de colonne T1.a est partagé sur toute la pile verticale de chunks,
|
||||
* donc une impureté ici se propage à tous les Z d'un coup.
|
||||
*/
|
||||
virtual void Eval(float WorldX, float WorldY, FVoxelHeightSample& InOut) const = 0;
|
||||
|
||||
/**
|
||||
* Majorant CONSERVATIF du déplacement vertical que cet op peut ajouter, en voxels.
|
||||
* Sert à borner la colonne pour un futur `ClassifyBox` exact du heightfield — la même logique
|
||||
* que les bandes de `FSlabVoidSource`, qui prouvent 36-40 tuiles sur 60.
|
||||
* Rendre trop grand coûte du CPU ; rendre trop petit serait un TROU. `FLT_MAX` = « je ne sais
|
||||
* pas », toujours sûr, et c'est le défaut.
|
||||
*/
|
||||
virtual float MaxDisplacement() const { return FLT_MAX; }
|
||||
};
|
||||
|
||||
/**
|
||||
* Pile de hauteur : source → modificateurs, dans l'ordre. Déplaçable, pas copiable, exactement
|
||||
* comme `FVoxelOpStack` et pour la même raison (elle POSSÈDE ses opérateurs).
|
||||
*/
|
||||
class FVoxelHeightStack
|
||||
{
|
||||
public:
|
||||
FVoxelHeightStack() = default;
|
||||
FVoxelHeightStack(FVoxelHeightStack&&) = default;
|
||||
FVoxelHeightStack& operator=(FVoxelHeightStack&&) = default;
|
||||
FVoxelHeightStack(const FVoxelHeightStack&) = delete;
|
||||
FVoxelHeightStack& operator=(const FVoxelHeightStack&) = delete;
|
||||
|
||||
void Add(TUniquePtr<IVoxelHeightOp> Op) { Ops.Add(MoveTemp(Op)); }
|
||||
int32 Num() const { return Ops.Num(); }
|
||||
|
||||
/** L'altitude après toute la pile. */
|
||||
float EvalHeight(float WorldX, float WorldY) const
|
||||
{
|
||||
return EvalSample(WorldX, WorldY).Height;
|
||||
}
|
||||
|
||||
/** L'état complet (altitude + relief). */
|
||||
FVoxelHeightSample EvalSample(float WorldX, float WorldY) const
|
||||
{
|
||||
FVoxelHeightSample S;
|
||||
for (const TUniquePtr<IVoxelHeightOp>& Op : Ops) { Op->Eval(WorldX, WorldY, S); }
|
||||
return S;
|
||||
}
|
||||
|
||||
/** Somme des majorants. `FLT_MAX` dès qu'un seul op ne sait pas répondre. */
|
||||
float MaxTotalDisplacement() const
|
||||
{
|
||||
float Total = 0.0f;
|
||||
for (const TUniquePtr<IVoxelHeightOp>& Op : Ops)
|
||||
{
|
||||
const float D = Op->MaxDisplacement();
|
||||
if (D >= FLT_MAX) { return FLT_MAX; }
|
||||
Total += D;
|
||||
}
|
||||
return Total;
|
||||
}
|
||||
|
||||
private:
|
||||
TArray<TUniquePtr<IVoxelHeightOp>> Ops;
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// FABRIQUES / FACTORIES
|
||||
//=============================================================================
|
||||
|
||||
namespace VoxelHeightOps
|
||||
{
|
||||
/**
|
||||
* La source structurelle : continents + montagnes + détail, sous une frame de warp.
|
||||
* Produit `Height` ET `Relief`. Transcription littérale de `SampleSurfaceStructuralZ`.
|
||||
*
|
||||
* ⚠️ Rend un pointeur NON-POSSÉDANT via `OutSource` : `FCliffHeightMod` doit pouvoir
|
||||
* RÉ-ÉCHANTILLONNER ce champ (4 fois, en différences centrées) et doit le faire sur la MÊME
|
||||
* fonction, pas sur une copie qui pourrait dériver. La pile garde la propriété ; la source vit
|
||||
* donc aussi longtemps que le modificateur qui la référence, parce que le constructeur de pile
|
||||
* les ajoute ensemble et que la pile ne réordonne jamais.
|
||||
*/
|
||||
VOXELFORGE_API TUniquePtr<IVoxelHeightOp> MakeStructuralHeightSource(
|
||||
const FSurfaceGenerationParams& P, int32 Seed, const IVoxelHeightOp** OutSource);
|
||||
|
||||
/** Raidissement conditionné par la pente. Le seul op qui coûte des échantillons en plus
|
||||
* (4 resamples structurels), et seulement quand il est activé. */
|
||||
VOXELFORGE_API TUniquePtr<IVoxelHeightOp> MakeCliffHeightMod(
|
||||
const FSurfaceGenerationParams& P, const IVoxelHeightOp* StructuralSource);
|
||||
|
||||
/** Plateaux quantifiés, gatés par le relief (`TerraceStrength * M`) — d'où le canal Relief. */
|
||||
VOXELFORGE_API TUniquePtr<IVoxelHeightOp> MakeTerraceHeightMod(const FSurfaceGenerationParams& P);
|
||||
|
||||
/** Bandes sédimentaires : `Height -= sin(Height · 2π / Spacing) · Depth`. */
|
||||
VOXELFORGE_API TUniquePtr<IVoxelHeightOp> MakeLayerLineHeightMod(const FSurfaceGenerationParams& P);
|
||||
|
||||
/** Aplatissement vers la ligne d'eau dans `BeachWidth`. */
|
||||
VOXELFORGE_API TUniquePtr<IVoxelHeightOp> MakeBeachHeightMod(const FSurfaceGenerationParams& P);
|
||||
|
||||
/**
|
||||
* La pile de hauteur complète de SurfaceWorld, dans l'ordre de `ComputeSurfaceTerrainZ` :
|
||||
* structural → cliff → terrace → layer lines → beach
|
||||
*
|
||||
* L'ordre n'est PAS négociable : le terrace quantifie une hauteur que le cliff a déjà raidie,
|
||||
* les layer lines se posent sur le résultat, et la plage écrase tout près de l'eau. C'est
|
||||
* l'ordre du code d'origine, et le test échouerait bruyamment sur toute permutation.
|
||||
*/
|
||||
VOXELFORGE_API void BuildSurfaceHeightStack(FVoxelHeightStack& OutStack,
|
||||
const FSurfaceGenerationParams& P, int32 Seed);
|
||||
}
|
||||
Reference in New Issue
Block a user