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:
parent
39e8408c7d
commit
6cc5e183b9
10 changed files with 325 additions and 106 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -369,21 +369,25 @@ internal static class BuiltInAtmosphericRenderPack
|
|||
new SunElevationResponsePoint(70, 0),
|
||||
new SunElevationResponsePoint(90, 0),
|
||||
],
|
||||
// Campaign VM VM6: the plan's Clear/Cloudy/Overcast/Rainy rows,
|
||||
// keyed by the SAME activeDayGroup index convention this pack
|
||||
// already established two lines above for ActiveDayGroupMultipliers
|
||||
// (0 brightest/clearest ... 2 dimmest). That table only needed
|
||||
// three rows; foliage wind needs a fourth (Rainy), so index 3 is
|
||||
// new here. Dereth's DAT declares many more named day groups than
|
||||
// these four categories distinguish — an index absent from this
|
||||
// table (including every index above 3) gets zero wind rather
|
||||
// than guessing a category.
|
||||
FoliageWindByDayGroup =
|
||||
// Campaign VM VM6, corrected in the fix round: keyed by the
|
||||
// DAT-classified AcDream.Core.World.WeatherKind, not the raw
|
||||
// activeDayGroup index — WeatherState.cs's existing name
|
||||
// classification (e.g. "Sunny" -> Clear, a name containing
|
||||
// "fog"/"cloud"/"overcast"/"dark"/"storm"/"snow"/"rain" -> the
|
||||
// matching kind) already turns a day group's authored DAT name
|
||||
// into one of these five real weather kinds; that fact was
|
||||
// already threaded through AtmosphericFrameInputs.Weather /
|
||||
// uAtmosphereWeather.x, so this table reuses it instead of
|
||||
// guessing a meaning for the raw index. Every WeatherKind member
|
||||
// is declared; nothing falls through to the Clear fallback in
|
||||
// production.
|
||||
FoliageWindByWeather =
|
||||
[
|
||||
new FoliageWindDayGroupPoint(0, 0.25, 0.15), // Clear
|
||||
new FoliageWindDayGroupPoint(1, 0.45, 0.30), // Cloudy
|
||||
new FoliageWindDayGroupPoint(2, 0.60, 0.35), // Overcast
|
||||
new FoliageWindDayGroupPoint(3, 0.85, 0.60), // Rainy
|
||||
new FoliageWindWeatherPoint("Clear", 0.25, 0.15),
|
||||
new FoliageWindWeatherPoint("Overcast", 0.60, 0.35),
|
||||
new FoliageWindWeatherPoint("Rain", 0.85, 0.60),
|
||||
new FoliageWindWeatherPoint("Snow", 0.35, 0.20),
|
||||
new FoliageWindWeatherPoint("Storm", 1.00, 0.75),
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using AcDream.Core.World;
|
||||
using AcDream.Plugin.Abstractions.Rendering;
|
||||
|
||||
namespace AcDream.App.Rendering.Packs;
|
||||
|
|
@ -45,26 +46,40 @@ internal static class RenderPackAtmospherePolicyEvaluation
|
|||
fallback);
|
||||
|
||||
/// <summary>
|
||||
/// Campaign VM VM6: exact day-group lookup for foliage wind — the same
|
||||
/// shape as <see cref="ActiveDayGroupMultiplier"/>'s exact-match pattern
|
||||
/// (day groups are not ordered by "how windy," so there is nothing to
|
||||
/// interpolate across them). A day group absent from the table returns
|
||||
/// (0, 0) — no wind is the safe default for an unclassified day group,
|
||||
/// not full wind. The caller is responsible for smoothing the resolved
|
||||
/// target over time; this method is a pure, stateless lookup.
|
||||
/// Campaign VM VM6, corrected in the fix round: exact weather-kind
|
||||
/// lookup for foliage wind, keyed by the DAT-classified
|
||||
/// <see cref="WeatherKind"/> the frame already carries
|
||||
/// (<c>AtmosphericFrameInputs.Weather</c> / <c>uAtmosphereWeather.x</c>)
|
||||
/// — not the raw <c>activeDayGroup</c> index, which carries no weather
|
||||
/// meaning by itself. Matches by <c>weather.ToString()</c> (ordinal)
|
||||
/// against each declared <see cref="FoliageWindWeatherPoint.WeatherKind"/>
|
||||
/// name. The five kinds are not ordered by "how windy," so this is an
|
||||
/// exact match, never an interpolation across them. A kind absent from
|
||||
/// the table falls back to the declared Clear row (a weather kind the
|
||||
/// classifier could not resolve is closer to "no weather data" than to
|
||||
/// "assume it's windy"); if Clear itself is undeclared, the fallback is
|
||||
/// (0, 0). The caller is responsible for smoothing the resolved target
|
||||
/// over time; this method is a pure, stateless lookup.
|
||||
/// </summary>
|
||||
internal static (float Mean, float Gust) FoliageWind(
|
||||
IReadOnlyList<FoliageWindDayGroupPoint>? points,
|
||||
int activeDayGroup)
|
||||
IReadOnlyList<FoliageWindWeatherPoint>? points,
|
||||
WeatherKind weather)
|
||||
{
|
||||
if (points is null)
|
||||
return (0f, 0f);
|
||||
foreach (FoliageWindDayGroupPoint point in points)
|
||||
string kind = weather.ToString();
|
||||
FoliageWindWeatherPoint? clear = null;
|
||||
foreach (FoliageWindWeatherPoint point in points)
|
||||
{
|
||||
if (point.ActiveDayGroup == activeDayGroup)
|
||||
if (string.Equals(point.WeatherKind, kind, StringComparison.Ordinal))
|
||||
return ((float)point.Mean, (float)point.Gust);
|
||||
if (clear is null
|
||||
&& string.Equals(point.WeatherKind, nameof(WeatherKind.Clear), StringComparison.Ordinal))
|
||||
{
|
||||
clear = point;
|
||||
}
|
||||
}
|
||||
return (0f, 0f);
|
||||
return clear is { } fallback ? ((float)fallback.Mean, (float)fallback.Gust) : (0f, 0f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1482,6 +1482,39 @@ internal static class RenderPackValidator
|
|||
|| !IsFiniteNonNegative(value.Multiplier))
|
||||
return Invalid($"Pack '{descriptor.Id}' has an invalid active-day-group mapping.");
|
||||
}
|
||||
|
||||
// Campaign VM VM6 fix round: FoliageWindByWeather is keyed by the
|
||||
// DAT-classified AcDream.Core.World.WeatherKind name (exact,
|
||||
// case-sensitive), not a raw day-group index — an unknown name or a
|
||||
// repeated kind is rejected outright rather than silently ignored.
|
||||
if (policy.FoliageWindByWeather is null)
|
||||
return Invalid($"Pack '{descriptor.Id}' has a null foliage-wind weather-point list.");
|
||||
var weatherKinds = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (FoliageWindWeatherPoint point in policy.FoliageWindByWeather)
|
||||
{
|
||||
if (!Enum.TryParse(
|
||||
point.WeatherKind,
|
||||
ignoreCase: false,
|
||||
out AcDream.Core.World.WeatherKind parsedKind)
|
||||
|| !Enum.IsDefined(parsedKind))
|
||||
{
|
||||
return Invalid(
|
||||
$"Pack '{descriptor.Id}' declares an unknown foliage-wind weather "
|
||||
+ $"kind '{point.WeatherKind}'.");
|
||||
}
|
||||
if (!weatherKinds.Add(point.WeatherKind))
|
||||
{
|
||||
return Invalid(
|
||||
$"Pack '{descriptor.Id}' declares the foliage-wind weather kind "
|
||||
+ $"'{point.WeatherKind}' more than once.");
|
||||
}
|
||||
if (!IsFiniteNonNegative(point.Mean) || !IsFiniteNonNegative(point.Gust))
|
||||
{
|
||||
return Invalid(
|
||||
$"Pack '{descriptor.Id}' has an invalid foliage-wind mean/gust value "
|
||||
+ $"for weather kind '{point.WeatherKind}'.");
|
||||
}
|
||||
}
|
||||
return RenderPackValidationResult.Valid();
|
||||
|
||||
RenderPackValidationResult ValidateCurve(
|
||||
|
|
|
|||
|
|
@ -387,12 +387,25 @@ public sealed record ActiveDayGroupMultiplier(
|
|||
double Multiplier);
|
||||
|
||||
/// <summary>
|
||||
/// Explicit mapping from an authored AC day group to the foliage-wind mean
|
||||
/// and gust strength for that group (both in the declared [0,1] range before
|
||||
/// the pack's global wind-strength setting scales them). Campaign VM VM6.
|
||||
/// Explicit mapping from a DAT-classified weather kind to the foliage-wind
|
||||
/// mean and gust strength for that weather (both in the declared [0,1] range
|
||||
/// before the pack's global wind-strength setting scales them). Campaign VM
|
||||
/// VM6, corrected in the fix round: the day-group's raw <c>activeDayGroup</c>
|
||||
/// INDEX carries no weather meaning by itself — <c>WeatherState.cs</c>'s
|
||||
/// existing name classification is what turns a day group's DAT name into
|
||||
/// one of the five real weather kinds, and that is what the host already
|
||||
/// threads through <c>AtmosphericFrameInputs.Weather</c> / <c>uAtmosphereWeather.x</c>.
|
||||
/// This is the same fact, keyed correctly instead of by index.
|
||||
/// </summary>
|
||||
public sealed record FoliageWindDayGroupPoint(
|
||||
int ActiveDayGroup,
|
||||
/// <param name="WeatherKind">
|
||||
/// The exact member name of <c>AcDream.Core.World.WeatherKind</c> — one of
|
||||
/// <c>"Clear"</c>, <c>"Overcast"</c>, <c>"Rain"</c>, <c>"Snow"</c>,
|
||||
/// <c>"Storm"</c>. A plain string because this project (Plugin.Abstractions)
|
||||
/// is BCL-only and cannot reference the Core enum; the host converts with
|
||||
/// <c>WeatherKind.ToString()</c> (ordinal) when resolving.
|
||||
/// </param>
|
||||
public sealed record FoliageWindWeatherPoint(
|
||||
string WeatherKind,
|
||||
double Mean,
|
||||
double Gust);
|
||||
|
||||
|
|
@ -424,18 +437,18 @@ public sealed record AtmospherePolicyDeclaration(
|
|||
{ get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Campaign VM VM6: exact per-day-group foliage-wind mean/gust targets,
|
||||
/// looked up by the same categorical <c>activeDayGroup</c> id
|
||||
/// <see cref="ActiveDayGroupMultipliers"/> already keys on (an exact
|
||||
/// match, not an interpolation across day-group ids — day groups are not
|
||||
/// ordered by "how windy"). The host smooths the resolved (mean, gust)
|
||||
/// target toward whatever this table returns over time using the
|
||||
/// existing weather delta-seconds clock, so a day-group change never
|
||||
/// snaps. A day group absent from this table gets zero wind. A pack
|
||||
/// using foliage wind should declare an entry for every day group it
|
||||
/// cares about distinguishing.
|
||||
/// Campaign VM VM6: exact per-weather-kind foliage-wind mean/gust
|
||||
/// targets, looked up by the DAT-classified
|
||||
/// <c>AcDream.Core.World.WeatherKind</c> (an exact match, not an
|
||||
/// interpolation — the five kinds are not ordered by "how windy"). The
|
||||
/// host smooths the resolved (mean, gust) target toward whatever this
|
||||
/// table returns over time using the existing weather delta-seconds
|
||||
/// clock, so a weather change never snaps. A kind absent from this table
|
||||
/// falls back to the declared Clear row, then to (0, 0) if Clear itself
|
||||
/// is undeclared. Each declared <see cref="FoliageWindWeatherPoint.WeatherKind"/>
|
||||
/// must be one of the five real kind names and must appear at most once.
|
||||
/// </summary>
|
||||
public IReadOnlyList<FoliageWindDayGroupPoint> FoliageWindByDayGroup
|
||||
public IReadOnlyList<FoliageWindWeatherPoint> FoliageWindByWeather
|
||||
{ get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue