fix(render): foliage wind keys on the DAT-classified WeatherKind, not the day-group index (Campaign VM VM6)

FoliageWindByDayGroup / FoliageWindDayGroupPoint(int ActiveDayGroup, ...)
becomes FoliageWindByWeather / FoliageWindWeatherPoint(string WeatherKind,
...) in AtmospherePolicyDeclaration (Plugin.Abstractions is BCL-only, so
the key is the exact member name of AcDream.Core.World.WeatherKind rather
than the enum itself). The raw activeDayGroup index carries no weather
meaning by itself; WeatherState.cs already classifies each day group's
authored DAT name into one of five real weather kinds, and that fact was
already threaded through AtmosphericFrameInputs.Weather / uAtmosphereWeather.x
— this reuses it instead of guessing an index-to-category mapping.

Built-in table (BuiltInAtmosphericRenderPack.AtmospherePolicy()): Clear
0.25/0.15, Overcast 0.60/0.35, Rain 0.85/0.60, Snow 0.35/0.20, Storm
1.00/0.75 — all five WeatherKind members declared, the invented "Cloudy"
row dropped. RenderPackAtmospherePolicyEvaluation.FoliageWind now takes a
WeatherKind and matches by weather.ToString() (ordinal) against each
declared point's name; a kind absent from the table falls back to the
declared Clear row, then to (0,0) if Clear itself is undeclared. The
delta-seconds EMA interpolation (EaseTowardTarget) is unchanged.
AtmosphericPostProcessGraph.ResolveFoliageWind and its two callers
(RenderPostProcess via inputs.Weather; RenderDirectionalShadows via
foundation.Atmosphere.Kind) now pass WeatherKind instead of the day-group
int.

RenderPackValidation.ValidateAtmosphere (runs for every pack declaring an
AtmospherePolicy, not gated to Tier2/shadow packs) now rejects an unknown
or non-exact-case weather-kind name and a repeated kind, mirroring the
existing ActiveDayGroupMultiplier duplicate-key check.

Tests: RenderPackAtmospherePolicyEvaluationTests rewritten for the
kind-keyed API (all five kinds resolve to their declared row, an unlisted
kind falls back to Clear, ordinal exact-case matching, null-table
handling); RenderPackSpirvValidatorTests gains four descriptor-validation
cases (unknown name, wrong case, duplicate kind, the five-kind table
accepted); AtmosphericPostProcessGraphTests' three foliage-wind cases now
select WeatherKind.Storm via `with` instead of an assumed day-group index.

Spot-check (per the coordinator's ask, not changed here): yes —
ActiveDayGroupMultiplier / EvaluateDayGroupPolicy (pre-existing, Campaign
AR/VM3-era — BuiltInAtmosphericRenderPack.AtmospherePolicy()'s three rows
`new ActiveDayGroupMultiplier(0, 1.0), (1, 0.35), (2, 0.20)`) key the
sun-ray/shadow/volumetric day-group strength multiplier by the same raw
activeDayGroup index with an undocumented assumed meaning (0=brightest ...
2=dimmest), the identical class of issue this commit fixes for foliage
wind. Left unchanged per instruction; flagging for the coordinator to file.

Full solution Debug and Release builds green. App hermetic filter
6024/6026 — the same 2 pre-existing failures as VM6a/VM6b. Both were
re-run in isolation per the verification ask: both still fail alone (not
a load-flake in this environment) — confirmed via git stash earlier this
session that both already fail on the unmodified pre-VM6 baseline, so
they are pre-existing and unrelated to this change. Core.Tests hermetic
4697/4697. RenderPackValidator.Tests 30/30. No shader/spv changes in this
commit (pure C#/docs fix).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-23 01:07:54 +02:00
parent 39e8408c7d
commit 6cc5e183b9
10 changed files with 325 additions and 106 deletions

View file

@ -420,7 +420,7 @@ internal sealed class AtmosphericPostProcessGraph :
worldMeshes.FoliageWindExclusions = _foliageWindExclusions;
bool isOutdoor = world.Roots.RenderSky && !world.Roots.CameraInsideCell;
AtmosphericFrameBufferBinding shadowAtmosphericFrame =
BuildShadowAtmosphericFrameBinding(frame, activeDayGroup, isOutdoor);
BuildShadowAtmosphericFrameBinding(frame, foundation.Atmosphere.Kind, isOutdoor);
var input = new DirectionalSunShadowRenderInput(
environment,
world.Camera.Camera.View,
@ -480,12 +480,12 @@ internal sealed class AtmosphericPostProcessGraph :
/// </summary>
private AtmosphericFrameBufferBinding BuildShadowAtmosphericFrameBinding(
IGpuFrame frame,
int activeDayGroup,
AcDream.Core.World.WeatherKind weather,
bool isOutdoor)
{
(Vector4 clockWind, Vector4 windAmplitude) = ResolveFoliageWind(
frame.Serial,
activeDayGroup,
weather,
isOutdoor);
GpuRingAllocation allocation = frame.AllocateRing(
AtmosphericFrameUniforms.SizeInBytes,
@ -530,13 +530,15 @@ internal sealed class AtmosphericPostProcessGraph :
/// for its own animation clock — replaces it when set, which is every
/// run but a differential/offline gate's.</para>
///
/// <para><b>The smoothing.</b> The per-day-group (mean, gust) target
/// from <see cref="RenderPackAtmospherePolicyEvaluation.FoliageWind"/>
/// eases toward its target using an exponential moving average over
/// <see cref="AcDream.Core.World.WeatherSystem.TransitionSeconds"/> (10 s,
/// the same authored weather-transition constant retail-parity work
/// already established) — so a day-group change never snaps. This state
/// keeps evolving even while indoors or disabled; <c>wind-enabled</c>
/// <para><b>The smoothing.</b> The per-weather-kind (mean, gust) target
/// from <see cref="RenderPackAtmospherePolicyEvaluation.FoliageWind"/> —
/// keyed by the DAT-classified <see cref="AcDream.Core.World.WeatherKind"/>,
/// not the raw day-group index, which carries no weather meaning by
/// itself — eases toward its target using an exponential moving average
/// over <see cref="AcDream.Core.World.WeatherSystem.TransitionSeconds"/>
/// (10 s, the same authored weather-transition constant retail-parity
/// work already established) — so a weather change never snaps. This
/// state keeps evolving even while indoors or disabled; <c>wind-enabled</c>
/// off or <c>!isOutdoor</c> instead multiplies the OUTPUT by an exact
/// zero gate (never an asymptotic approach), so a settings toggle or a
/// cell transition reads exactly zero on the very next frame, and
@ -545,7 +547,7 @@ internal sealed class AtmosphericPostProcessGraph :
/// </summary>
private (Vector4 ClockWind, Vector4 WindAmplitude) ResolveFoliageWind(
long frameSerial,
int activeDayGroup,
AcDream.Core.World.WeatherKind weather,
bool isOutdoor)
{
float clockSeconds = _windClockSecondsOverride
@ -558,8 +560,8 @@ internal sealed class AtmosphericPostProcessGraph :
1f);
(float targetMean, float targetGust) = RenderPackAtmospherePolicyEvaluation
.FoliageWind(
Descriptor.AtmospherePolicy?.FoliageWindByDayGroup,
activeDayGroup);
Descriptor.AtmospherePolicy?.FoliageWindByWeather,
weather);
targetMean *= _foliageWind.Strength;
targetGust *= _foliageWind.Strength;
_windMean = RenderPackAtmospherePolicyEvaluation.EaseTowardTarget(
@ -689,7 +691,7 @@ internal sealed class AtmosphericPostProcessGraph :
: 0f;
(Vector4 clockWind, Vector4 windAmplitude) = ResolveFoliageWind(
frame.Serial,
inputs.ActiveDayGroup,
inputs.Weather,
inputs.IsOutdoor);
var frameUniforms = new AtmosphericFrameUniforms(
new Vector4(