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>
146 lines
5.9 KiB
C#
146 lines
5.9 KiB
C#
using AcDream.Core.World;
|
|
using AcDream.Plugin.Abstractions.Rendering;
|
|
|
|
namespace AcDream.App.Rendering.Packs;
|
|
|
|
/// <summary>
|
|
/// Host evaluation for the public data-only atmosphere curves. Keeping the
|
|
/// three interpolation contracts here prevents a pack declaration from being
|
|
/// reinterpreted differently by the declared, shadow, and volumetric graphs.
|
|
/// </summary>
|
|
internal static class RenderPackAtmospherePolicyEvaluation
|
|
{
|
|
internal static DirectionalShadowAtmospherePolicy NeutralDirectionalShadowElevation { get; } =
|
|
DirectionalShadowAtmospherePolicy.BuiltIn with
|
|
{
|
|
MinimumLightElevationSin = -1.001f,
|
|
FullStrengthLightElevationSin = -1f,
|
|
};
|
|
|
|
internal static float Ray(
|
|
IReadOnlyList<SunElevationResponsePoint>? points,
|
|
float elevationDegrees,
|
|
float fallback = 1f) => Evaluate(
|
|
points,
|
|
elevationDegrees,
|
|
static value => (float)value,
|
|
static value => (float)value,
|
|
fallback);
|
|
|
|
internal static float DirectionalShadow(
|
|
IReadOnlyList<SunElevationResponsePoint>? points,
|
|
float elevationDegrees,
|
|
float fallback = 0f) => DirectionalShadowFromSin(
|
|
points,
|
|
MathF.Sin(elevationDegrees * (MathF.PI / 180f)),
|
|
fallback);
|
|
|
|
internal static float DirectionalShadowFromSin(
|
|
IReadOnlyList<SunElevationResponsePoint>? points,
|
|
float lightElevationSin,
|
|
float fallback = 0f) => Evaluate(
|
|
points,
|
|
Math.Clamp(lightElevationSin, -1f, 1f),
|
|
static degrees => MathF.Sin((float)degrees * (MathF.PI / 180f)),
|
|
static value => (float)value,
|
|
fallback);
|
|
|
|
/// <summary>
|
|
/// 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<FoliageWindWeatherPoint>? points,
|
|
WeatherKind weather)
|
|
{
|
|
if (points is null)
|
|
return (0f, 0f);
|
|
string kind = weather.ToString();
|
|
FoliageWindWeatherPoint? clear = null;
|
|
foreach (FoliageWindWeatherPoint point in points)
|
|
{
|
|
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 clear is { } fallback ? ((float)fallback.Mean, (float)fallback.Gust) : (0f, 0f);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Campaign VM VM6: one exponential-moving-average step toward
|
|
/// <paramref name="target"/>, used to smooth the foliage-wind mean/gust
|
|
/// strength across a day-group change so it never snaps. Pure and
|
|
/// stateless — the caller owns <paramref name="current"/> across calls.
|
|
/// <paramref name="deltaSeconds"/> should already be clamped to a sane
|
|
/// per-frame bound by the caller (a huge delta — e.g. after a long
|
|
/// pause — would otherwise jump the rate to 1 and snap anyway).
|
|
/// </summary>
|
|
internal static float EaseTowardTarget(
|
|
float current,
|
|
float target,
|
|
float deltaSeconds,
|
|
float transitionSeconds)
|
|
{
|
|
float rate = transitionSeconds <= 0f
|
|
? 1f
|
|
: Math.Clamp(deltaSeconds / transitionSeconds, 0f, 1f);
|
|
return current + ((target - current) * rate);
|
|
}
|
|
|
|
internal static float VolumetricShaft(
|
|
IReadOnlyList<SunElevationResponsePoint>? points,
|
|
float elevationDegrees,
|
|
float fallback = 0f) => Evaluate(
|
|
points,
|
|
elevationDegrees,
|
|
static value => (float)value,
|
|
static value => value * value * (3f - (2f * value)),
|
|
fallback);
|
|
|
|
private static float Evaluate(
|
|
IReadOnlyList<SunElevationResponsePoint>? points,
|
|
float input,
|
|
Func<double, float> transformPoint,
|
|
Func<float, float> transformInterpolation,
|
|
float fallback)
|
|
{
|
|
if (points is null || points.Count == 0)
|
|
return fallback;
|
|
float first = transformPoint(points[0].ElevationDegrees);
|
|
if (input <= first)
|
|
return (float)points[0].Multiplier;
|
|
for (int i = 1; i < points.Count; i++)
|
|
{
|
|
SunElevationResponsePoint upper = points[i];
|
|
float upperInput = transformPoint(upper.ElevationDegrees);
|
|
if (input > upperInput)
|
|
continue;
|
|
SunElevationResponsePoint lower = points[i - 1];
|
|
float lowerInput = transformPoint(lower.ElevationDegrees);
|
|
float span = upperInput - lowerInput;
|
|
float t = span <= 0f
|
|
? 0f
|
|
: Math.Clamp((input - lowerInput) / span, 0f, 1f);
|
|
t = transformInterpolation(t);
|
|
return (float)(lower.Multiplier
|
|
+ ((upper.Multiplier - lower.Multiplier) * t));
|
|
}
|
|
return (float)points[^1].Multiplier;
|
|
}
|
|
}
|