fix(opstack): three ExtraReach box verdicts used sup|FBM| = 1.0; the proved bound is 1.5

CORRECTNESS on a box verdict, not perf. This file derives |Perlin3D| <= 1.5
rigorously, exposes it as PerlinAbsBound, uses it for the tunnel warp, and says
outright that the header's "~[-1,1]" is an observation and that a box verdict
resting on one is a hole. Three ExtraReach formulas in the same file assumed
sup|FBM| <= 1.0 -- and VoxelNoise::FBM normalises (Total / MaxValue), so
sup|FBM| = sup|Perlin3D| exactly. The bound was wrong by 1.5x.

At shipped defaults, soundness needed B <= 1.27 (VerticalShafts, Rough 3.0) and
B <= 1.40 (Maze, Rough 2.0); both are violated at B = 1.5. Break-even roughness
is 1.6. FloatingIslands survives only because its SDFBlendRadius is 5 -- sound
by parameter luck, not construction.

Nothing in the running game was affected (no strate has bUseOperatorStack
ticked) and the empirical Perlin sup ~1.0-1.1 is why nothing surfaced. But the
tile scans SAMPLE, and for shafts they sampled a source that proved zero tiles
until e002bd4 -- which is what makes this reachable rather than latent.

Fix: PerlinAbsBound hoisted to file scope as VF_PerlinAbsBound (one definition,
not a class-static plus three implicit 1.0s) and multiplied into all three
reaches; the three comments now state the real justification instead of the
refuted one.

Cannot change density: ExtraReach is read only inside EffectOverBox, verified
mechanically -- no Eval/GetCells line appears in the diff. Direction is strictly
conservative, so it can only cost CPU. Expect FEWER proved tiles for Maze and
VerticalShafts; that is the correct outcome, not a regression.

Not built -- Jahni builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 16:36:04 +02:00
parent b5294b2d5b
commit 7dbdf51b44
3 changed files with 253 additions and 62 deletions
+109
View File
@@ -0,0 +1,109 @@
# Codex task 003 — three `ExtraReach` formulas use an FBM bound this file already proved wrong
**Owner:** Codex (Model Luna, xHigh) · **Orchestrator:** Claude · **Branch:** `experimental`
**Status:** specified, not started
**Kind:** ⚠️ **correctness of a box verdict** — the class of bug that deletes collision. Not a perf task.
---
## Why this exists
`VoxelDensityOpStack.cpp` contains a rigorous, written derivation that `|Perlin3D| ≤ 1.5`, exposes it
as `PerlinAbsBound` (line ~2336), and uses it correctly for the tunnel warp dilation (~2452). The
comment there is explicit that the loose "~[-1,1]" figure from the noise header is **not** to be
relied on, and `OPSTACK-HANDOFF.md` records the standard: *a bound in a box verdict must be PROVED,
not observed — over-estimating costs CPU, under-estimating deletes collision.*
**Three `ExtraReach` formulas in the same file silently assume `sup|FBM| ≤ 1.0`.** Each carries the
comment "FBM ∈ [-1,1]", which is exactly the claim the file disproves 1800 lines earlier.
And `VoxelNoise::FBM` **is normalised** — it returns `Total / MaxValue` where `MaxValue = Σ Amp`
(`VoxelNoise.h` ~272). So `sup|FBM| = sup|Perlin3D|` exactly: **1.5, not 1.0.** The octave sum
neither amplifies nor attenuates the bound.
### What that costs, per archetype, at the shipped defaults
`Identity` from these sources means "no primitive within `ExtraReach` of the box", i.e. `Sdf ≥
ExtraReach` throughout. Roughness then does `Sdf += FBM · VOXEL_NOISE_SCALE · Strength`
(`FSdfRoughnessMod::Eval`), so worst case `Sdf' ≥ ExtraReach B·1.25·|Roughness|`. Soundness
requires `Sdf'` to stay at or above the downstream carve/fill threshold.
| archetype | `ExtraReach` at defaults | downstream threshold | needs `B ≤` | verdict at `B = 1.5` |
|---|---|---|---|---|
| **VerticalShafts** (`Rough 3.0`) | `1.25·3 + 2 + 1` = **6.75** | carve blend **2.0** | **1.27** | ⛔ **UNSOUND** (margin 0.875) |
| **Maze** (`Rough 2.0`) | `1.25·2 + 2 + 1` = **5.5** | carve blend **2.0** | **1.40** | ⛔ **UNSOUND** (margin 0.25) |
| **FloatingIslands** (`Rough 4.0`, `K 5.0`) | `1.25·4 + 2·5 + 1` = **16.0** | fill `K` **5.0** (+ `K/6` SmoothMin dip) | **2.03** | ✅ sound — but only because `K` is large. Sound by parameter luck, not by construction. |
Break-even roughness for the two carve archetypes is `|Rough| ≤ 1.6`; they ship at 3.0 and 2.0.
**How alarmed to be, stated honestly.** No strate has `bUseOperatorStack` ticked, so nothing in the
running game is affected today. The brute-force tile scans report 0 violations — but they *sample*,
and they were sampling against a shaft source that proved **zero** tiles until `e002bd4`, so the
shaft path has never been exercised at all. The empirical sup of this Perlin is estimated at
~1.01.1, which is *below* the 1.27 the shafts need — which is why nothing has been seen yet, and
also why the margin is uncomfortably thin. The bug is that the verdict rests on an unproved bound,
which is the thing this codebase has already decided it does not do.
## The fix
1. **Hoist `PerlinAbsBound` to file scope and rename it `VF_PerlinAbsBound`**, so there is **one**
definition rather than a class-static plus three implicit `1.0`s. Keep the existing derivation
comment with it — it is the justification, not decoration.
**Naming, resolved:** the file-scope helpers in this file are all `VF_`-prefixed
(`VF_NearCaveSurface`, `VF_DistPointSegment`, `VF_NoCaveOverBox`), so a file-scope constant takes
the same prefix. That means this is a **rename**, not just a move:
- delete the `static constexpr float PerlinAbsBound = 1.5f;` class-static inside `FRoomGraphSource`
(~2336), moving its whole derivation comment with it;
- **update `FRoomGraphSource`'s own use at ~2452** (`P.CaveWarpStrength * VOXEL_NOISE_SCALE *
PerlinAbsBound`) to the new name. This is the one place where the warp dilation is computed and
it must keep computing the identical value — the rename must not change its arithmetic.
- after the edit, `grep -n "PerlinAbsBound" ` must show **only** `VF_PerlinAbsBound` occurrences.
2. **Multiply the roughness term by it in all three `ExtraReach` formulas** (~4106 VerticalShafts,
~4215 FloatingIslands, ~4247 Maze):
```cpp
// before
FMath::Abs(P.SurfaceRoughness) * VOXEL_NOISE_SCALE
// after
FMath::Abs(P.SurfaceRoughness) * VOXEL_NOISE_SCALE * VF_PerlinAbsBound
```
3. **Fix the three comments.** Each says "FBM ∈ [-1,1]". Replace with the real statement: `FBM` is
normalised (`Total / MaxValue`), so `sup|FBM| = sup|Perlin3D| =` the proved `PerlinAbsBound`.
A comment that states a refuted bound is how this happened in the first place.
## ⚠️ Invariants
1. **This must not change density by one bit.** `ExtraReach` is read **only** inside
`EffectOverBox` (verified: every other occurrence is a comment or the `float ExtraReach;` member
declaration — no `Eval`, no `GetCells`). The eight equivalence tests compare `Eval` bit for bit
and must stay green. **If you find yourself editing an `Eval`, stop — you have the wrong site.**
2. **The change direction is strictly conservative**: larger `ExtraReach` ⇒ more `CarveOnly`, fewer
`Identity` ⇒ *fewer* tiles proved uniform. It can only cost CPU, never open a hole. Do not
"balance" it by tightening something else in the same edit.
3. **Anonymous-namespace placement.** Put the hoisted constant **above the labelled end of the
anonymous namespace**, not anchored on the FACTORIES banner — anchoring there puts it outside and
the brace added with it closes nothing. This mistake has been made twice in this file and the
file says so.
4. **`FRoomGraphSource`'s warp dilation changes NAME ONLY.** It already uses the bound correctly and
is the reference implementation for this fix; the value it computes must be bit-identical after
the rename. Do not alter its formula, its `√2` factor, or anything else in that function.
5. Comments are French + English; match the surrounding file.
## Acceptance
- `git diff --stat` shows **one** file: `Source/VoxelForge/Private/VoxelDensityOpStack.cpp`.
- All three `ExtraReach` definitions include the bound; no fourth site exists (`BuildTunnelNetworkStack`
has no `ExtraReach` — it uses `PerlinAbsBound` directly for the warp).
- No `Eval` body changed.
- After the build: the eight equivalence tests stay green (density unchanged), and the box-verdict
lines for **Maze** and **VerticalShafts** may report *fewer* proved tiles than before. **A drop
there is the expected, correct outcome, not a regression** — it is the cost of a sound bound.
Record the before/after in `OPSTACK-PROGRESS.md`.
## Notes for the reviewer (Claude)
- Confirm the constant is genuinely at file scope inside the anonymous namespace and that the
class-static is gone, not shadowed — two definitions that can drift is the failure this fixes.
- Confirm all three call sites got it. Two out of three is worse than none, because it looks done.
- Confirm no `Eval`, `GetCells`, or `GetCellsAt` body appears in the diff.
+75
View File
@@ -3553,3 +3553,78 @@ absolute percentage.** The overhang and cliff mods call `GetColumn` again at the
- `GetGenerationParams` is flagged as the safest of the six if any is ever wanted. - `GetGenerationParams` is flagged as the safest of the six if any is ever wanted.
Nothing here needs a decision from Jahni; it all needs the same next build. Nothing here needs a decision from Jahni; it all needs the same next build.
## 2026-08-16 (e) — ⛔ A REAL BUG: three box-verdict reaches used a bound this file had already refuted
Found by auditing the invariant the handoff calls "the one that can delete collision", extended to
the archetypes it does *not* cover. `CODEX-TASK-003`, written, executed by Codex, reviewed. **Not a
perf change — a correctness change to a box verdict.**
### The finding
This file derives `|Perlin3D| ≤ 1.5` rigorously (the `GradDot` two-distinct-axes form and the
per-axis weighted bound of 0.5), exposes it as `PerlinAbsBound`, uses it correctly for the tunnel
warp dilation — and states outright that the noise header's "~[-1,1]" is an observation, not a
theorem, and that a box verdict resting on one is a hole.
**Three `ExtraReach` formulas in the same file assumed `sup|FBM| ≤ 1.0`**, each with the comment
"FBM ∈ [-1,1]" — the exact claim disproved 1800 lines above. And `VoxelNoise::FBM` **normalises**
(`return Total / MaxValue`, `VoxelNoise.h` ~272), so `sup|FBM| = sup|Perlin3D|` **exactly**: the
octave sum neither amplifies nor attenuates it. The bound was simply wrong, by 1.5×.
`Identity` from these sources means `Sdf ≥ ExtraReach` over the box; roughness then does
`Sdf += FBM · VOXEL_NOISE_SCALE · Strength`, so soundness needs
`ExtraReach B·1.25·|Rough| ≥ (carve/fill threshold)`:
| archetype | `ExtraReach` (defaults) | threshold | needs `B ≤` | at proved `B = 1.5` |
|---|---|---|---|---|
| **VerticalShafts** (`Rough 3.0`) | 6.75 | carve 2.0 | 1.27 | ⛔ **UNSOUND**, margin 0.875 |
| **Maze** (`Rough 2.0`) | 5.5 | carve 2.0 | 1.40 | ⛔ **UNSOUND**, margin 0.25 |
| **FloatingIslands** (`Rough 4.0`, `K 5.0`) | 16.0 | fill 5.0 + `K/6` | 2.03 | ✅ sound — **by parameter luck**, not construction |
Break-even roughness for the two carve archetypes is `1.6`; they ship at **3.0** and **2.0**.
### How alarmed to be — stated honestly, because overstating this would be its own failure
**Nothing in the running game was ever affected**: no strate has `bUseOperatorStack` ticked. The
brute-force tile scans report 0 violations, but they **sample**, and for shafts they were sampling a
source that proved **zero** tiles until `e002bd4` — so that path had never been exercised at all.
The empirical sup of this Perlin is ~1.01.1, *below* the 1.27 the shafts needed, which is why
nothing surfaced. **The defect is that the verdict rested on an unproved bound** — precisely the
standard this codebase already adopted and wrote down.
⚠️ And note what made it findable: `e002bd4` (still unbuilt) is what first lets the shaft source
return `Identity` at all. It converted a latent unsoundness into a reachable one.
### The fix, and why it is safe
`PerlinAbsBound` hoisted to file scope as `VF_PerlinAbsBound` (**one** definition, not a class-static
plus three implicit `1.0`s — the same "one definition, not two kept in sync" rule that produced
`VF_BuildOpStackForChunk`), and multiplied into all three reaches. The three lying comments now state
the real justification.
**It cannot change density by one bit.** `ExtraReach` is read *only* inside `EffectOverBox` — every
other occurrence in the file is a comment or the `float ExtraReach;` member declaration. Verified
mechanically after the edit: no `Eval` / `GetCells` / `GetCellsAt` line appears in the diff. The
eight bit-for-bit equivalence tests are therefore unaffected by construction, not by hope.
The direction is strictly conservative: larger reach ⇒ more `CarveOnly`, fewer `Identity` ⇒ *fewer*
tiles proved. It can only cost CPU.
New values at defaults: shafts **6.75 → 8.625** (+28 %), maze **5.5 → 6.75** (+23 %), islands
**16.0 → 18.5** (+16 %).
### ⚠️ What to expect in the build, so a correct result is not misread as a regression
**The Maze and VerticalShafts box-verdict lines may prove FEWER tiles than before. That is the
correct outcome and the price of a sound bound — do not "fix" it.** Record the before/after.
`violations` must stay 0, as always. The eight equivalence tests must stay green; if any of them
moves, the change touched density and the diff is wrong.
### Process note — Codex refused the first run, correctly
The first attempt stopped without editing: my spec wrote `VF_PerlinAbsBound` in its code snippet and
`PerlinAbsBound` in its prose, and it asked which was intended rather than picking one. That is
exactly the behaviour the spec template asks for ("if the code contradicts the spec, stop and say so
rather than guessing"), and it is worth recording that it works — the cost was one clarification
instead of a plausible-looking wrong rename.
@@ -35,6 +35,59 @@
namespace namespace
{ {
/**
* BORNE **PROUVABLE** DE `|Perlin3D|`, ET ELLE N'EST PAS 1.0.
*
* L'en-tête de `VoxelNoise::Perlin3D` annonce « ~[-1,1] (typiquement [-0.7,0.7]) ». Le `~`
* est un aveu : c'est une observation, pas un théorème, et un verdict de boîte fondé sur une
* observation est exactement le genre de trou que ce fichier passe son temps à éviter.
*
* Ce qui EST démontrable, en lisant `GradDot` : il rend `ru + rv` où `ru` et `rv` sont des
* composantes de l'offset fractionnaire, donc chacune dans `[-1, 1]` ⇒ `|GradDot| ≤ 2`. La
* valeur finale est une interpolation trilinéaire de huit `GradDot`, et une interpolation
* convexe ne sort jamais de l'enveloppe de ses entrées ⇒ `|Perlin3D| ≤ 2`. (La vraie borne
* de Perlin 3D est `√3/2 ≈ 0.87` ; on ne s'appuie pas dessus, elle dépend du jeu de
* gradients.) Se tromper ici coûte une boîte de recherche un peu plus large, jamais un
* verdict faux : plus large ⇒ SUR-ensemble de primitives ⇒ `Identity` plus rare.
*
* ⚠️⚠️ **CORRIGÉ DE 2.0 À 1.5 LE 2026-07-28, ET CETTE CONSTANTE ÉTAIT LE TERME DOMINANT DE
* TOUTE LA FONCTION PENDANT TROIS BUILDS.** À lire avant d'y retoucher.
*
* La dilatation vaut `CaveWarpStrength · VOXEL_NOISE_SCALE · CETTE BORNE`. Avec les défauts
* (`CaveWarpStrength = 8`, `SCALE = 1.25`) elle valait **20 voxels** — appliquée des deux
* côtés de chaque axe d'une tuile de **10 voxels**, soit une boîte de requête de 50 voxels,
* **125× le volume de la tuile**. Trois passes de resserrement (le ver, les colonnes,
* l'échantillonneur, la disjonction des tunnels) ont été faites AUTOUR de ce terme sans que
* personne ne le mesure. Le test des tunnels, annoncé « un ordre de grandeur plus serré », ne
* gagnait en pratique que 25 % — exactement parce que `BoxHalfDiag` était dominé par cette
* dilatation et non par la géométrie.
*
* ⚠️ ET LE RESTE DU PLUGIN N'A JAMAIS ÉTÉ AUSSI PRUDENT : `BuildChunkCache` est appelée avec
* `Expansion = CaveWarpStrength + 2` (ici comme dans `GetDensityWithParams`), ce qui suppose
* `|Perlin3D| · SCALE ≤ CaveWarpStrength`, donc `|Perlin3D| ≤ 0.8`. Le code qui tourne en
* production depuis toujours parie déjà là-dessus. Prendre 2.0 était 2,5× plus conservateur
* que l'hypothèse dont dépend déjà la correction du cache.
*
* LA BORNE 1.5, DÉMONTRÉE (et non observée) :
* 1. `GradDot` rend `±u ± v` où `u` et `v` sont deux composantes **distinctes** de l'offset
* du coin — vérifié sur les quatre branches du `switch` de hash, pas supposé.
* 2. Pour l'axe x : les coins à `i=0` portent le poids `(1su)` et l'offset `fx`, ceux à
* `i=1` le poids `su` et l'offset `1fx`. Donc `Σ_c w_c·|dx_c| = (1su)·fx + su·(1fx)`,
* dont le maximum sur `[0,1]` vaut **0.5** (atteint en `fx = 0.5`, où `su = 0.5` ;
* 0.302 en 0.25 comme en 0.75).
* 3. `|Perlin| ≤ Σ_c w_c(|a_c| + |b_c|) ≤ S_x + S_y + S_z ≤ 3 × 0.5 = 1.5.`
* (Le vrai maximum est plus bas encore — seuls DEUX axes apparaissent par coin — mais 1.5
* est la borne qui se démontre sans analyse de cas sur les hash. `√3/2 ≈ 0.87`, la borne
* classique de Perlin 3D, dépend du jeu de gradients : on ne s'appuie pas dessus.)
*
* Was 2.0, and that constant was the dominant term of this whole function for three builds:
* it inflated a 10-voxel tile into a 50-voxel query box (125x the volume), which is why the
* "order of magnitude tighter" tunnel test only won 25%. The rest of the plugin has always
* assumed |Perlin3D| <= 0.8 (BuildChunkCache's Expansion = CaveWarpStrength + 2). 1.5 is
* PROVED above from GradDot's two-distinct-axes form and the per-axis weighted bound of 0.5.
*/
static constexpr float VF_PerlinAbsBound = 1.5f;
/** La même enveloppe que `FractalNoise3D` de VoxelGenerator.cpp (qui y est `static`, donc /** La même enveloppe que `FractalNoise3D` de VoxelGenerator.cpp (qui y est `static`, donc
* invisible ici). Le détour par `FVector` est délibéré — voir l'en-tête de ce fichier. */ * invisible ici). Le détour par `FVector` est délibéré — voir l'en-tête de ce fichier. */
FORCEINLINE float HFractal3D(const FVector& Position, int32 Octaves = 4, FORCEINLINE float HFractal3D(const FVector& Position, int32 Octaves = 4,
@@ -2282,59 +2335,6 @@ namespace
return S; return S;
} }
/**
* BORNE **PROUVABLE** DE `|Perlin3D|`, ET ELLE N'EST PAS 1.0.
*
* L'en-tête de `VoxelNoise::Perlin3D` annonce « ~[-1,1] (typiquement [-0.7,0.7]) ». Le `~`
* est un aveu : c'est une observation, pas un théorème, et un verdict de boîte fondé sur une
* observation est exactement le genre de trou que ce fichier passe son temps à éviter.
*
* Ce qui EST démontrable, en lisant `GradDot` : il rend `ru + rv` où `ru` et `rv` sont des
* composantes de l'offset fractionnaire, donc chacune dans `[-1, 1]` ⇒ `|GradDot| ≤ 2`. La
* valeur finale est une interpolation trilinéaire de huit `GradDot`, et une interpolation
* convexe ne sort jamais de l'enveloppe de ses entrées ⇒ `|Perlin3D| ≤ 2`. (La vraie borne
* de Perlin 3D est `√3/2 ≈ 0.87` ; on ne s'appuie pas dessus, elle dépend du jeu de
* gradients.) Se tromper ici coûte une boîte de recherche un peu plus large, jamais un
* verdict faux : plus large ⇒ SUR-ensemble de primitives ⇒ `Identity` plus rare.
*
* ⚠️⚠️ **CORRIGÉ DE 2.0 À 1.5 LE 2026-07-28, ET CETTE CONSTANTE ÉTAIT LE TERME DOMINANT DE
* TOUTE LA FONCTION PENDANT TROIS BUILDS.** À lire avant d'y retoucher.
*
* La dilatation vaut `CaveWarpStrength · VOXEL_NOISE_SCALE · CETTE BORNE`. Avec les défauts
* (`CaveWarpStrength = 8`, `SCALE = 1.25`) elle valait **20 voxels** — appliquée des deux
* côtés de chaque axe d'une tuile de **10 voxels**, soit une boîte de requête de 50 voxels,
* **125× le volume de la tuile**. Trois passes de resserrement (le ver, les colonnes,
* l'échantillonneur, la disjonction des tunnels) ont été faites AUTOUR de ce terme sans que
* personne ne le mesure. Le test des tunnels, annoncé « un ordre de grandeur plus serré », ne
* gagnait en pratique que 25 % — exactement parce que `BoxHalfDiag` était dominé par cette
* dilatation et non par la géométrie.
*
* ⚠️ ET LE RESTE DU PLUGIN N'A JAMAIS ÉTÉ AUSSI PRUDENT : `BuildChunkCache` est appelée avec
* `Expansion = CaveWarpStrength + 2` (ici comme dans `GetDensityWithParams`), ce qui suppose
* `|Perlin3D| · SCALE ≤ CaveWarpStrength`, donc `|Perlin3D| ≤ 0.8`. Le code qui tourne en
* production depuis toujours parie déjà là-dessus. Prendre 2.0 était 2,5× plus conservateur
* que l'hypothèse dont dépend déjà la correction du cache.
*
* LA BORNE 1.5, DÉMONTRÉE (et non observée) :
* 1. `GradDot` rend `±u ± v` où `u` et `v` sont deux composantes **distinctes** de l'offset
* du coin — vérifié sur les quatre branches du `switch` de hash, pas supposé.
* 2. Pour l'axe x : les coins à `i=0` portent le poids `(1su)` et l'offset `fx`, ceux à
* `i=1` le poids `su` et l'offset `1fx`. Donc `Σ_c w_c·|dx_c| = (1su)·fx + su·(1fx)`,
* dont le maximum sur `[0,1]` vaut **0.5** (atteint en `fx = 0.5`, où `su = 0.5` ;
* 0.302 en 0.25 comme en 0.75).
* 3. `|Perlin| ≤ Σ_c w_c(|a_c| + |b_c|) ≤ S_x + S_y + S_z ≤ 3 × 0.5 = 1.5.`
* (Le vrai maximum est plus bas encore — seuls DEUX axes apparaissent par coin — mais 1.5
* est la borne qui se démontre sans analyse de cas sur les hash. `√3/2 ≈ 0.87`, la borne
* classique de Perlin 3D, dépend du jeu de gradients : on ne s'appuie pas dessus.)
*
* Was 2.0, and that constant was the dominant term of this whole function for three builds:
* it inflated a 10-voxel tile into a 50-voxel query box (125x the volume), which is why the
* "order of magnitude tighter" tunnel test only won 25%. The rest of the plugin has always
* assumed |Perlin3D| <= 0.8 (BuildChunkCache's Expansion = CaveWarpStrength + 2). 1.5 is
* PROVED above from GradDot's two-distinct-axes form and the per-axis weighted bound of 0.5.
*/
static constexpr float PerlinAbsBound = 1.5f;
/** /**
* ✅ LA RÉPONSE SPATIALE. La dette annoncée ici pendant tout le portage est payée. * ✅ LA RÉPONSE SPATIALE. La dette annoncée ici pendant tout le portage est payée.
* *
@@ -2449,7 +2449,7 @@ namespace
// 3. LE CACHE POUR LA BOÎTE INTERROGÉE // 3. LE CACHE POUR LA BOÎTE INTERROGÉE
//----------------------------------------------------------------- //-----------------------------------------------------------------
const float Warp = (P.CaveWarpStrength > 0.0f) const float Warp = (P.CaveWarpStrength > 0.0f)
? P.CaveWarpStrength * VOXEL_NOISE_SCALE * PerlinAbsBound ? P.CaveWarpStrength * VOXEL_NOISE_SCALE * VF_PerlinAbsBound
: 0.0f; : 0.0f;
// `+ 2` : la même marge de gradient que la boîte de recherche de `Eval`. // `+ 2` : la même marge de gradient que la boîte de recherche de `Eval`.
@@ -4101,9 +4101,11 @@ namespace VoxelDensityOps
constexpr float CarveBlend = 2.0f; constexpr float CarveBlend = 2.0f;
// Portée que la source doit déclarer pour la paire source+carve : la rugosité peut élargir // Portée que la source doit déclarer pour la paire source+carve : la rugosité peut élargir
// le puits (FBM ∈ [-1,1] ⇒ ±Strength·VOXEL_NOISE_SCALE), puis le blend du carve. Sur-estimer // `FBM` est normalisé (`Total / MaxValue`), donc sup|FBM| = sup|Perlin3D| = la borne
// coûte du CPU ; sous-estimer serait un trou. // prouvée `VF_PerlinAbsBound` ; la rugosité peut élargir le puits, puis vient le blend du
const float ExtraReach = FMath::Abs(P.SurfaceRoughness) * VOXEL_NOISE_SCALE + CarveBlend + 1.0f; // carve. Sur-estimer coûte du CPU ; sous-estimer serait un trou.
const float ExtraReach = FMath::Abs(P.SurfaceRoughness) * VOXEL_NOISE_SCALE
* VF_PerlinAbsBound + CarveBlend + 1.0f;
TUniquePtr<FShaftFieldSource> ShaftSource = MakeUnique<FShaftFieldSource>(P, Seed, ExtraReach); TUniquePtr<FShaftFieldSource> ShaftSource = MakeUnique<FShaftFieldSource>(P, Seed, ExtraReach);
const FShaftFieldSource* ShaftPtr = ShaftSource.Get(); const FShaftFieldSource* ShaftPtr = ShaftSource.Get();
@@ -4210,9 +4212,12 @@ namespace VoxelDensityOps
const float BlendK = FMath::Max(P.SDFBlendRadius, 0.01f); const float BlendK = FMath::Max(P.SDFBlendRadius, 0.01f);
// Portée que la source doit déclarer pour la paire source+fill : la rugosité peut abaisser // Portée que la source doit déclarer pour la paire source+fill : la rugosité peut abaisser
// le SDF de `Rough·VOXEL_NOISE_SCALE` (FBM ∈ [-1,1]), le SmoothMin de `K/6` de plus, et le // `FBM` est normalisé (`Total / MaxValue`), donc sup|FBM| = sup|Perlin3D| = la borne
// fill s'applique dès `Sdf < BlendK`. Sur-estimer coûte du CPU ; sous-estimer serait un trou. // prouvée `VF_PerlinAbsBound` ; le SDF peut être abaissé par la rugosité, puis par le
// SmoothMin de `K/6`, et le fill s'applique dès `Sdf < BlendK`. Sur-estimer coûte du CPU ;
// sous-estimer serait un trou.
const float ExtraReach = FMath::Abs(P.SurfaceRoughness) * VOXEL_NOISE_SCALE const float ExtraReach = FMath::Abs(P.SurfaceRoughness) * VOXEL_NOISE_SCALE
* VF_PerlinAbsBound
+ BlendK * 2.0f + 1.0f; + BlendK * 2.0f + 1.0f;
OutStack.Add(MakeConstantVoidSource(P.BaseDensity)); OutStack.Add(MakeConstantVoidSource(P.BaseDensity));
@@ -4242,9 +4247,11 @@ namespace VoxelDensityOps
const float RoughApplyWithin = R + P.SurfaceRoughness + 2.0f; const float RoughApplyWithin = R + P.SurfaceRoughness + 2.0f;
// Portée que la source doit déclarer pour la paire source+carve : le rayon du couloir peut // Portée que la source doit déclarer pour la paire source+carve : le rayon du couloir peut
// être élargi par la rugosité (FBM ∈ [-1,1] ⇒ ±Strength·VOXEL_NOISE_SCALE) puis par le blend // `FBM` est normalisé (`Total / MaxValue`), donc sup|FBM| = sup|Perlin3D| = la borne
// du carve. Sur-estimer coûte du CPU ; sous-estimer serait un trou. // prouvée `VF_PerlinAbsBound` ; le rayon du couloir peut être élargi par la rugosité puis
const float ExtraReach = FMath::Abs(P.SurfaceRoughness) * VOXEL_NOISE_SCALE + CarveBlend + 1.0f; // par le blend du carve. Sur-estimer coûte du CPU ; sous-estimer serait un trou.
const float ExtraReach = FMath::Abs(P.SurfaceRoughness) * VOXEL_NOISE_SCALE
* VF_PerlinAbsBound + CarveBlend + 1.0f;
OutStack.Add(MakeConstantRockSource(P.BaseDensity)); OutStack.Add(MakeConstantRockSource(P.BaseDensity));
OutStack.Add(MakeLatticeCorridorSource(P, Seed, ExtraReach)); OutStack.Add(MakeLatticeCorridorSource(P, Seed, ExtraReach));