feat(render): weather-driven foliage wind for procedural scenery, shadows follow (Campaign VM VM6b)

Procedural-scenery foliage (trees/bushes — entity ids in the
ProceduralSceneryIdAllocator's 0x8XXYYIII namespace) sways with weather in
mesh_atmospheric.vert and all four directional_shadow_world_* caster vertex
shaders, both calling the identical new foliage_wind.glsl include so the
shadow moves with the leaf by construction.

Classification (FoliageWindClassification, AcDream.App.Rendering.Wb): two
new BatchData.flags bits, computed once per (entity, subset) from four
inputs — entity id (bit 31 for procedural scenery), the pack's declared
FoliageExclusions membership, the subset's TranslucencyKind, and
ObjectRenderData.HasCutoutSubset (computed once per mesh at build time, not
per frame). Bit 1 marks an alpha-cutout leaf subset; bit 2 marks an opaque
trunk subset (only when its own mesh also owns a cutout subset, so rocks
stay still). WbDrawDispatcher.ClassifyBatches (world receiver) and
AddDirectionalShadowBatches (caster) call this with the same four inputs, so
casters and receivers classify identically without needing to share state.
Retail's mesh_modern/terrain_modern/mesh_detail pipelines never read these
bits, so pack-off output is unaffected.

Motion model (foliage_wind.glsl, mirrored bit-for-bit in the new
FoliageWindModel for hermetic CPU tests): height-squared-scaled slow lean
for every foliage subset, plus branch swing and per-vertex-hash-decorrelated
flutter for cutout subsets only. AtmosphericPostProcessGraph.ResolveFoliageWind
resolves the wind block once per frame.Serial — advanced by whichever of
RenderDirectionalShadows (which runs first) or RenderPostProcess is called
first that frame, with the second reading the already-advanced state, which
is what keeps the caster and receiver reading byte-identical clock/strength
values. The per-day-group mean/gust target (AtmospherePolicyDeclaration.
FoliageWindByDayGroup, keyed by the same day-group index convention
ActiveDayGroupMultipliers already established: Clear/Cloudy/Overcast/Rainy)
eases toward its target over WeatherSystem.TransitionSeconds (10s) so a
weather change never snaps; wind-enabled off or indoor instead gates the
OUTPUT to an exact zero (not an asymptotic approach) so a settings toggle or
cell transition is immediate. The wind clock is a Stopwatch started at graph
construction (monotonic, session-relative magnitude for GPU sin() accuracy),
overridable by the same ACDREAM_SKY_PHASE_SECONDS pin SkyRenderer already
uses, for deterministic offline gates.

New settings: wind-enabled, wind-strength, wind-direction-degrees (225°
default — no authored retail wind direction exists to read),
wind-lean-metres, wind-branch-metres, wind-flutter-metres (0 on Low),
wind-canopy-height-metres.

Register row IA-25 files this as an intentional, strictly opt-in divergence:
retail applies no per-vertex wind displacement to any geometry. Known,
accepted limitation: classification is per mesh-subset (one BatchData.flags
word per indirect-draw batch), not per entity instance, so the rare case of
one mesh subset being reachable from both a procedural-scenery and a
non-scenery placement would classify all of that subset's instances alike.

Tests: FoliageWindClassificationTests (the full classification matrix),
FoliageWindModelTests (identity on non-foliage/calm-wind/base-vertex,
canopy-top displacement bound, z-never-increases, trunk has no flutter
term), RenderPackAtmospherePolicyEvaluationTests (exact day-group lookup,
no interpolation across day-group ids, easing convergence without overshoot
or discontinuity), AtmosphericShaderAbiTests (each of the five shaders calls
acdreamFoliageDisplace exactly once; mesh_modern/terrain/mesh_detail call it
never), and four AtmosphericPostProcessGraphTests additions (indoor/disabled
exact-zero gating, settings-to-UBO wiring, same-frame-Serial idempotency —
the last proxies the caster/receiver agreement invariant without needing
this hermetic harness's WbDrawDispatcher/TerrainModernRenderer dependency
chain to exercise RenderDirectionalShadows directly).

App hermetic filter: 6015/6017 (the same 2 pre-existing failures as VM6a,
confirmed unrelated). Core.Tests hermetic: 4697/4697. RenderPackValidator.Tests:
30/30. Full solution Debug and Release builds green. Shader recompile
touched exactly the 5 edited files' .spv (plus manifest); the retail oracle
set and every other pack shader are byte-identical.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-23 00:56:34 +02:00
parent 0930c35d1d
commit 39e8408c7d
34 changed files with 1536 additions and 64 deletions

View file

@ -94,6 +94,7 @@ resolution, and cascade count than on output resolution.
| Tier | Contents | Prerequisite | Rough incremental GPU cost at 1080p |
|---|---|---|---:|
| acdream default | Current authoritative retail-faithful rendering | Current mandatory Vulkan/RHI capabilities | 0 ms |
| 0 | **Weather-driven foliage wind** (Campaign VM VM6): procedural-scenery trees/bushes lean, branch-swing, and flutter; their directional-shadow casters apply the identical displacement so the shadow moves with the leaf | Shader ABI v2 (`AtmosphericFrame` gains `uAtmosphereClockWind`/`uAtmosphereWindAmplitude`); the existing `ProceduralSceneryIdAllocator` entity-id namespace for classification | ~0.05 ms (near-zero; ~25 ALU per displaced vertex, zero CPU cost, zero extra draw submissions — an early-out `uint` AND gates every non-foliage vertex) |
| 1 | Bloom, ACES filmic tonemap, colour grade, vignette | Main-world colour intermediate and fullscreen passes | 0.350.80 ms |
| 1 | Screen-space sun rays (crepuscular) | Authored sun screen position plus an occlusion mask; **no shadow maps** | 0.200.50 ms |
| 2 | **Moving authored sun-and-moon cascaded directional shadows from trees, monsters, players, and houses/buildings** | A second scene pass, selected-celestial view/projection matrices, sampled depth maps, caster pipeline variants | 1.503.00 ms |
@ -106,6 +107,15 @@ Tier 1 without shadows. Tier 2 always includes the complete shadow-caster
classes; weak-hardware presets reduce range, cascade count, and resolution
rather than silently dropping monsters, trees, or buildings.
**Foliage wind (Tier 0, Campaign VM VM6).** Deliberately numbered below Tier
1: it needs no fullscreen pass, no colour intermediate, and no shadow
infrastructure of its own — only the shader ABI v2 clock/wind block and the
per-batch classification bits `BatchData.flags` already carries. It composes
with every other tier (Tier 2's shadow casters read the same displacement so
a swaying tree's shadow tracks it) and, uniquely among these tiers, is cheap
enough that a future default-on consideration is plausible; today it ships
opt-in with the rest of the atmospheric pack, gated by `wind-enabled`.
## Tier 2 headline: Dereth's moving authored sun-and-moon shadows
The [measured renderer state](../research/2026-08-21-terrain-and-atmospheric-rendering-findings.md#5-renderer-state-relevant-to-atmospheric-work)

View file

@ -472,6 +472,42 @@ 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; owner visual gate outstanding)
Two commits (VM6a shader ABI v2 plumbing, VM6b the feature) shipped the exact
design above. Settings (`BuiltInAtmosphericRenderPack.Settings()`):
`wind-enabled` (bool, default on), `wind-strength` (02×, default 1.0),
`wind-direction-degrees` (0360°, default 225 — no authored retail wind
direction exists to read), `wind-lean-metres` (default 0.25),
`wind-branch-metres` (default 0.15), `wind-flutter-metres` (default 0.05,
forced to 0 on the Low preset), `wind-canopy-height-metres` (default 8). The
Clear/Cloudy/Overcast/Rainy mean/gust rows live in
`AtmospherePolicyDeclaration.FoliageWindByDayGroup`, keyed by the SAME
day-group index convention `ActiveDayGroupMultipliers` already established
two lines above it in `AtmosphericPolicy()` (0 Clear / 1 Cloudy / 2 Overcast;
index 3 Rainy is new). Classification bits live in
`FoliageWindClassification` (`AcDream.App.Rendering.Wb`): bit 1 (`0x2`)
cutout foliage, bit 2 (`0x4`) trunk, computed once per (entity, subset) in
`WbDrawDispatcher.ClassifyBatches` (world receiver) and
`AddDirectionalShadowBatches` (caster) from the identical four inputs
(entity id, `AtmospherePolicyDeclaration.FoliageExclusions` membership,
subset `TranslucencyKind`, and `ObjectRenderData.HasCutoutSubset` — computed
once per mesh, not per frame or instance). The shared displacement lives in
`foliage_wind.glsl`, called once each from `mesh_atmospheric.vert` and all
four `directional_shadow_world_*` (opaque/cutout, base/multiview) caster
vertex shaders, reading the ABI v2 `uAtmosphereClockWind`/
`uAtmosphereWindAmplitude` members `AtmosphericPostProcessGraph.ResolveFoliageWind`
resolves once per `frame.Serial` (so the caster, which runs first each
frame, and the receiver read byte-identical values). `FoliageWindModel` is
the CPU mirror pinned by hermetic tests. Known limitation, documented and
accepted rather than engineered around: classification is per mesh-subset
(a `BatchData.flags` word shared by every instance drawn in one indirect
command), not per entity instance — the rare case of the exact same mesh
subset being reachable from both a procedural-scenery and a non-scenery
placement would classify all of that subset's instances alike. Register row
IA-25. Owed: the six-step owner visual gate above (implementation is
otherwise code-complete and the automated acceptance criteria pass).
## VM7 — Closeout and merge
- Full gates: `tools/run-release-gate.ps1` (hermetic lanes), the AR