docs(render): VM6 code-complete — foliage-wind pixel proof, repeat-floor apparatus, ledger (Campaign VM)
Closes the automated half of slice VM6 at754d59d9and records how it was proven. The first pixel method (clock pin 0 s vs 3 s, wind-off pair as the control) is recorded as confounded: the treeline silhouette carries a bimodal 0-or-~280 px rasterisation churn between runs of the same binary, and two conclusions drawn from it were retracted. The replacement apparatus (same pin, wind on vs off, two captures per arm, robust mask = AND of the four cross pairs minus both repeat pairs, shadows off so only geometry can move) found the round-4 defect (wind welded to the shadow gate: 1 m wind = 49-65 px vs a 22 px floor while the CPU state was correct) and, after round 5, gives 14,993 robust wind pixels vs a 65 px floor, every one on a treeline tree, hillside tree or shoreline bush, with the wind-off frame matching the pre-round-4 plain pipeline at mean |d| 0.007 (round 4'seec95535: 1.77, the overhead-sun regression the narrow review caught). tools/vm6/wind-pixel-proof.py is the analysis command the closeout names; evidence overlays under docs/research/evidence/vm6. Plan: VM6 ledger row CODE-COMPLETE 2026-08-23, owner visual gate owed (section VM6 Acceptance). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
754d59d949
commit
26adf75079
8 changed files with 212 additions and 6 deletions
|
|
@ -82,7 +82,7 @@ single-pass path darkens by ~10 %. Neither explains a scene-wide level.
|
|||
| VM3 | F4 — linear-light post stack | **CODE-COMPLETE 2026-08-22** — `87677f9c`, `51178f7c`, closeout; Opus APPROVE; **owner visual gate OWED** (brief below) |
|
||||
| VM4 | F5 — overclaiming docs, incl. the reviewer's own | **CLOSED 2026-08-22** |
|
||||
| VM5 | F8 — volumetric banding; F7 filed | **CLOSED 2026-08-22** (#421) |
|
||||
| VM6 | Foliage wind | **CODE-COMPLETE 2026-08-23** — `0930c35d`, `39e8408c`, `43e3abed`, `a82959f1`, `fccba839`, `eec95535`; four Opus review rounds (round 3 APPROVE, round 4 narrow re-review of `fccba839`+`eec95535`); pixel proof [note](../research/2026-08-23-vm6-foliage-wind-pixel-proof.md) (13,854 robust wind px vs 19 floor, scenery only); **owner visual gate OWED** (§VM6 Acceptance) |
|
||||
| VM6 | Foliage wind | **CODE-COMPLETE 2026-08-23** — `0930c35d`, `39e8408c`, `43e3abed`, `a82959f1`, `fccba839`, `eec95535`, `754d59d9`; five Opus review rounds (round 3 APPROVE; round 4 REJECT caught the overhead-sun regression, fixed in round 5); pixel proof [note](../research/2026-08-23-vm6-foliage-wind-pixel-proof.md) (14,993 robust wind px vs 65 floor, scenery only; wind-off frames match the plain pipeline at mean |Δ| 0.007); **owner visual gate OWED** (§VM6 Acceptance) |
|
||||
| VM7 | Closeout: full gates, register, roadmap, merge | **owner** |
|
||||
|
||||
Order is VM2 (done) → VM0 (done) → VM1 (done) → VM4 (done) → VM5 (done) → VM3 (code-complete, gate owed) → VM6 (code-complete, gate owed) → VM7. VM0 goes first because everything after it is measured
|
||||
|
|
@ -495,7 +495,7 @@ direction to read, and the register row says so.
|
|||
indoors — nothing moves; (6) pack off — nothing moves. The owner's words
|
||||
decide; the numbers above are starting points to tune live.
|
||||
|
||||
### VM6 outcome (implementation landed 2026-08-22; code-complete 2026-08-23 at `eec95535`; owner visual gate outstanding)
|
||||
### VM6 outcome (implementation landed 2026-08-22; code-complete 2026-08-23 at `754d59d9`; owner visual gate outstanding)
|
||||
|
||||
Two commits (VM6a shader ABI v2 plumbing, VM6b the feature) shipped the
|
||||
design above; a same-day fix-round commit corrected the weather-table key.
|
||||
|
|
@ -725,13 +725,15 @@ gated-off frame is numerically the plain pipeline. Wind is unaffected —
|
|||
`foliage_wind.glsl` reads only the AtmosphericFrame half of set 3, never
|
||||
the shadow block.
|
||||
|
||||
**Pixel proof after round 4 (2026-08-23, `eec95535`):** the same
|
||||
**Pixel proof after rounds 4–5 (2026-08-23, `eec95535` → `754d59d9`):** the same
|
||||
apparatus that found the defect — High preset, `sun-shadow-strength=0` so
|
||||
no shadow term can move, same pinned clock, wind on vs off, two captures per
|
||||
arm so the repeat pairs measure the run-to-run floor — gives a robust wind
|
||||
mask of **13,854 px** (amplified: strength 2, lean/branch 1 m, flutter
|
||||
0.5 m) against a **19 px** floor, and **1,402 px** at the default Clear
|
||||
strength; every marked pixel is a treeline tree, hillside tree or shoreline
|
||||
mask of **13,854 px** on `eec95535` and **14,993 px** on `754d59d9`
|
||||
(amplified: strength 2, lean/branch 1 m, flutter 0.5 m) against a 19–65 px
|
||||
floor, and **1,402 px** at the default Clear strength; the wind-off frame on
|
||||
`754d59d9` matches the pre-round-4 plain pipeline at mean |Δ| 0.007 (round
|
||||
4's `eec95535` was 1.77 — the overhead-sun regression); every marked pixel is a treeline tree, hillside tree or shoreline
|
||||
bush, and no house, fence, road, lifestone, ground, water or UI pixel moves.
|
||||
The first method tried (clock pin 0 s vs 3 s, wind-off pair as the control)
|
||||
is recorded as confounded in the note and must not be reused: the treeline
|
||||
|
|
|
|||
145
docs/research/2026-08-23-vm6-foliage-wind-pixel-proof.md
Normal file
145
docs/research/2026-08-23-vm6-foliage-wind-pixel-proof.md
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
# VM6 — does the wind move the trees, and only the trees?
|
||||
|
||||
**Date:** 2026-08-23 · **Campaign:** VM slice VM6 · **Status:** see the
|
||||
verdict at the end; the owner's visual gate (plan §VM6 Acceptance) is still
|
||||
owed regardless.
|
||||
|
||||
**Question.** With the built-in `acdream.atmospheric` pack, does the
|
||||
foliage-wind displacement reach the **production** world geometry, and is it
|
||||
confined to procedural scenery foliage?
|
||||
|
||||
This note records the apparatus that finally answered it, including the
|
||||
first method that did not — two wrong conclusions were drawn from it before
|
||||
the confound was found, and both are recorded here so nobody re-derives them.
|
||||
|
||||
## Apparatus
|
||||
|
||||
Offline pixel gate (`tools/run-offline-pixel-gate.ps1`, Release, `-SkipBuild`,
|
||||
`-Uncapped`, 1280×720, day group 0 "Sunny" → `WeatherKind.Clear`, day
|
||||
fraction 0.5, MSAA 0), isolated settings, the fixed offline scene the VM0/VM3
|
||||
captures use, drawn through `RetailPViewPassExecutor.DrawPackedProductionRoute`
|
||||
(the production entity route). `-RenderPackPreset high`; `ACDREAM_SKY_PHASE_SECONDS`
|
||||
pins the pack's per-graph clock so two captures at the same pin see the same
|
||||
wind phase. Pixel metric: max-channel |Δ| ≥ 8
|
||||
(`tools/vm6/wind-pixel-proof.py`, numpy).
|
||||
|
||||
### Method 1 (confounded — do not reuse): "t0→t3 with wind on, minus t0→t3 with wind off"
|
||||
|
||||
The idea was that the clock pin at 0 s vs 3 s moves only the wind, so the
|
||||
wind-off pair is a control for everything else the clock drives. It is not:
|
||||
the same binary, same settings, same pin differs run-to-run by **0 or ~280
|
||||
px**, bimodally, and those pixels are isolated single-pixel flips along the
|
||||
treeline's leaf edges (`artifacts/vm6amp/churn-crop.png`) — rasterisation
|
||||
edge flips, not content. Whether the churn lands in the wind pair or the
|
||||
control pair is luck, so "wind-only" counts of 23 / 314 / 568 / 703 px from
|
||||
this method on `43e3abed` / `a82959f1` were all within that churn. The
|
||||
overlay images looked convincing precisely because the churn sits on tree
|
||||
silhouettes. Two conclusions drawn from it — "the F1 packed-classifier fix
|
||||
is confirmed by pixels" and "wind reaches the geometry" — were unsupported.
|
||||
(The F1 fix itself is correct; the round-3 Opus review traced it to the GPU
|
||||
word by reading, and the round-4 apparatus below confirms geometry motion.)
|
||||
|
||||
### Method 2: same pin, wind on vs off, signal amplified, repeats as the floor
|
||||
|
||||
Settings `wind-strength = 2`, `wind-lean-metres = 1`, `wind-branch-metres = 1`,
|
||||
`wind-flutter-metres = 0.5` make the canopy displacement ~1 m — several
|
||||
pixels even at this scene's treeline distance, where the default Clear wind
|
||||
(≤ 11 cm lean) is sub-pixel. Two captures per arm; the repeat pairs measure
|
||||
the floor; the four cross pairs measure the effect.
|
||||
|
||||
## Results
|
||||
|
||||
### `fccba839` (round 3), `sun-shadow-strength = 0`
|
||||
|
||||
| Pair | px ≥ 8 |
|
||||
|---|---|
|
||||
| wind-off A vs wind-off B (floor) | 22 |
|
||||
| amplified-wind A vs amplified-wind B (floor + churn) | 317 |
|
||||
| amplified A vs wind-off A / B | 65 / 49 |
|
||||
| amplified B vs wind-off A / B | 284 / 273 |
|
||||
| robust mask (all four cross pairs, neither repeat pair) | **1** |
|
||||
|
||||
A 1 m wind produced nothing. A CPU probe in `ResolveFoliageWind` (removed
|
||||
after the run) showed the state was right — first advance at serial 2 snaps
|
||||
to Clear `(0.25, 0.15)`, strength 1, gate on, one graph instance — so the
|
||||
uniform was correct and the world pass never used it.
|
||||
|
||||
**Cause (by reading, round-4 finding):** the wind was welded to "directional
|
||||
shadows rendered this frame". `DirectionalSunShadowRenderer.Render` left the
|
||||
frame binding `Disabled` whenever the environment gate said no;
|
||||
`WbDrawDispatcher.PipelinesFor` then fell back to the plain `mesh_modern`
|
||||
pipeline, which has no `foliage_wind.glsl` at all; and
|
||||
`BindDirectionalShadowReceiver` returned early on `!Enabled`. The gate is
|
||||
`dayGroupPolicy × sunElevationResponse × strength`, so the wind also stopped
|
||||
every night, at user strength 0 and under the portal cover. Fixed in round 4
|
||||
by publishing a valid-but-disabled binding (zeroed shadow block, flags 0 —
|
||||
the receiver shader already returns visibility 1.0 on that bit — plus the
|
||||
wind frame slice) so the built-in pack's world pass always runs the receiver
|
||||
pipeline.
|
||||
|
||||
### `eec95535` (round 4), `sun-shadow-strength = 0`
|
||||
|
||||
| Pair | px ≥ 8 |
|
||||
|---|---|
|
||||
| wind-off A vs wind-off B (floor) | 19 |
|
||||
| amplified-wind A vs amplified-wind B (repeat) | 51 |
|
||||
| amplified A/B vs wind-off A/B (four cross pairs) | 13,876 / 13,887 / 13,893 / 13,903 |
|
||||
| **robust mask (all four cross pairs, neither repeat pair)** | **13,854** |
|
||||
| **default Clear strength vs both wind-off, floor-excluded** | **1,402** (2,655 at |Δ| ≥ 3) |
|
||||
|
||||
Per-60-row band of the robust mask: `[8580, 4101, 800, 61, 0, 110, 14, 188, 0, 0, 0, 0]` —
|
||||
the treeline (rows 0–180), the hillside trees (rows 300–360) and the
|
||||
shoreline bushes (rows 420–480). Overlays:
|
||||
`evidence/vm6/eec95535-amplified-wind-vs-off-shadows-off.png` (every tree
|
||||
and bush red; no house, fence, road, lifestone, ground, water or UI pixel),
|
||||
`evidence/vm6/eec95535-default-clear-wind-vs-off-shadows-off.png`, the
|
||||
amplified frame itself `evidence/vm6/eec95535-amplified-wind-frame.png`, and
|
||||
the Method-1 churn crop `evidence/vm6/method1-run-to-run-churn-crop.png`.
|
||||
|
||||
### `754d59d9` (round 5 — gated-off frames light from the authored sun again)
|
||||
|
||||
The round-4 narrow review found that round 4's decoupling lit every
|
||||
shadow-gated-off frame from straight overhead: both receiver vertex shaders
|
||||
take the sun *direction* from the shadow block, and the disabled block's
|
||||
`(0,0,1)` guard became the sun. Round 5 makes the receiver verts fall back
|
||||
to the plain pipeline's `uLights` expression when the flag bit is clear.
|
||||
Same apparatus, wind-off arm compared across binaries (shadows off, same
|
||||
pin):
|
||||
|
||||
| Pair | px ≥ 8 | mean |Δ| |
|
||||
|---|---|---|
|
||||
| `754d59d9` wind-off A vs B (floor) | 65 | 0.002 |
|
||||
| **`754d59d9` vs `fccba839` (plain pipeline, pre-round-4)** | **277** | **0.007** |
|
||||
| `eec95535` vs `fccba839` (the round-4 regression) | 32,538 | 1.77 |
|
||||
|
||||
Wind on `754d59d9`: robust mask **14,993 px** (amplified), same bands as
|
||||
round 4.
|
||||
|
||||
## Verdict
|
||||
|
||||
On `754d59d9` the foliage wind reaches the production world geometry with
|
||||
the shadow term removed (so it is geometry, not shadow), is 700× the
|
||||
run-to-run floor when amplified and 70× at the default Clear strength, and
|
||||
is confined to procedural scenery foliage in this scene. Earlier binaries:
|
||||
`43e3abed` had no geometry wind at all on the production route (round-2
|
||||
review F1, confirmed by reading); `a82959f1`/`fccba839` had it only while the
|
||||
shadow gate was open (round-4 defect); `eec95535` had the wind but lit
|
||||
shadow-gated-off frames from overhead (round-5 defect).
|
||||
|
||||
Commands: `tools/run-offline-pixel-gate.ps1 -Out <dir> -SkipBuild
|
||||
-RenderPackPreset high -SkyPhaseSeconds 3 -Uncapped
|
||||
-RenderPackSettingOverrides @{ 'wind-enabled'='true'|'false';
|
||||
'sun-shadow-strength'='0'; [amplified: 'wind-strength'='2';
|
||||
'wind-lean-metres'='1'; 'wind-branch-metres'='1'; 'wind-flutter-metres'='0.5'] }`
|
||||
(Release, two captures per arm as above); analysis
|
||||
`py tools/vm6/wind-pixel-proof.py <root>` (robust mask = AND of the four
|
||||
cross-pair masks minus both repeat-pair masks; writes `diff-robust-wind.png`).
|
||||
|
||||
## What this does and does not prove
|
||||
|
||||
- Method 2 with shadows off isolates geometry motion (no shadow term can
|
||||
change), and the robust mask confines it to where the wind model says it
|
||||
should be.
|
||||
- It does not prove the motion *looks* right — amplitude, rhythm and the
|
||||
Clear/Rain/Storm table are the owner's call at the visual gate — nor does
|
||||
the offline scene exercise weather other than Clear.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
BIN
docs/research/evidence/vm6/eec95535-amplified-wind-frame.png
Normal file
BIN
docs/research/evidence/vm6/eec95535-amplified-wind-frame.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
BIN
docs/research/evidence/vm6/method1-run-to-run-churn-crop.png
Normal file
BIN
docs/research/evidence/vm6/method1-run-to-run-churn-crop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 428 KiB |
59
tools/vm6/wind-pixel-proof.py
Normal file
59
tools/vm6/wind-pixel-proof.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
"""Campaign VM VM6 — robust foliage-wind pixel proof.
|
||||
|
||||
Usage: py tools/vm6/wind-pixel-proof.py <capture-root>
|
||||
|
||||
<capture-root> holds four run-offline-pixel-gate.ps1 output directories,
|
||||
all at the SAME ACDREAM_SKY_PHASE_SECONDS pin and sun-shadow-strength=0:
|
||||
amp-t3, amp-t3b wind on (amplified or default strength)
|
||||
nowind-t3a, nowind-t3b wind-enabled=false
|
||||
Each directory contains screenshots/world-offline.png.
|
||||
|
||||
Why four captures: the same binary/settings/pin differs run-to-run by 0 or
|
||||
~280 isolated leaf-edge pixels (rasterisation flips along the treeline), so a
|
||||
single on-vs-off pair cannot be trusted. The repeat pairs measure that floor;
|
||||
the robust mask is the AND of all four cross pairs minus both repeat pairs.
|
||||
See docs/research/2026-08-23-vm6-foliage-wind-pixel-proof.md.
|
||||
"""
|
||||
import sys
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
root = sys.argv[1]
|
||||
threshold = int(sys.argv[2]) if len(sys.argv) > 2 else 8
|
||||
|
||||
|
||||
def load(name):
|
||||
return np.asarray(
|
||||
Image.open(f"{root}/{name}/screenshots/world-offline.png").convert("RGB")
|
||||
).astype(int)
|
||||
|
||||
|
||||
def strong(a, b):
|
||||
return np.abs(a - b).max(axis=2) >= threshold
|
||||
|
||||
|
||||
def bands(mask):
|
||||
ys, _ = np.nonzero(mask)
|
||||
return np.bincount(ys // 60, minlength=12).tolist() if len(ys) else [0] * 12
|
||||
|
||||
|
||||
A, B, N1, N2 = load("amp-t3"), load("amp-t3b"), load("nowind-t3a"), load("nowind-t3b")
|
||||
noise = strong(N1, N2)
|
||||
repeat = strong(A, B)
|
||||
print(f"threshold |d|>={threshold}")
|
||||
print("floor wind-off A vs B :", int(noise.sum()), bands(noise))
|
||||
print("repeat wind-on A vs B :", int(repeat.sum()), bands(repeat))
|
||||
for name, (x, y) in {
|
||||
"wind-on A vs off A": (A, N1),
|
||||
"wind-on A vs off B": (A, N2),
|
||||
"wind-on B vs off A": (B, N1),
|
||||
"wind-on B vs off B": (B, N2),
|
||||
}.items():
|
||||
m = strong(x, y)
|
||||
print(f"{name:20s}: {int(m.sum()):6d} {bands(m)}")
|
||||
robust = strong(A, N1) & strong(A, N2) & strong(B, N1) & strong(B, N2) & ~noise & ~repeat
|
||||
print("ROBUST wind mask :", int(robust.sum()), bands(robust))
|
||||
out = A.copy()
|
||||
out[robust] = [255, 0, 0]
|
||||
Image.fromarray(out.astype(np.uint8)).save(f"{root}/diff-robust-wind.png")
|
||||
print(f"overlay: {root}/diff-robust-wind.png")
|
||||
Loading…
Add table
Add a link
Reference in a new issue