acdream/docs/research/2026-08-23-vm6-foliage-wind-pixel-proof.md
Erik 26adf75079 docs(render): VM6 code-complete — foliage-wind pixel proof, repeat-floor apparatus, ledger (Campaign VM)
Closes the automated half of slice VM6 at 754d59d9 and 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's eec95535: 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>
2026-08-23 04:01:18 +02:00

7.4 KiB
Raw Blame History

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

Per-60-row band of the robust mask: [8580, 4101, 800, 61, 0, 110, 14, 188, 0, 0, 0, 0] — the treeline (rows 0180), the hillside trees (rows 300360) and the shoreline bushes (rows 420480). 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.