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:
@@ -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.0–1.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.
|
||||||
@@ -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.0–1.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 `(1−su)` et l'offset `fx`, ceux à
|
||||||
|
* `i=1` le poids `su` et l'offset `1−fx`. Donc `Σ_c w_c·|dx_c| = (1−su)·fx + su·(1−fx)`,
|
||||||
|
* 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 `(1−su)` et l'offset `fx`, ceux à
|
|
||||||
* `i=1` le poids `su` et l'offset `1−fx`. Donc `Σ_c w_c·|dx_c| = (1−su)·fx + su·(1−fx)`,
|
|
||||||
* 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));
|
||||||
|
|||||||
Reference in New Issue
Block a user