using AcDream.Core.World;
using AcDream.Plugin.Abstractions.Rendering;
namespace AcDream.App.Rendering.Packs;
///
/// 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.
///
internal static class RenderPackAtmospherePolicyEvaluation
{
internal static DirectionalShadowAtmospherePolicy NeutralDirectionalShadowElevation { get; } =
DirectionalShadowAtmospherePolicy.BuiltIn with
{
MinimumLightElevationSin = -1.001f,
FullStrengthLightElevationSin = -1f,
};
internal static float Ray(
IReadOnlyList? points,
float elevationDegrees,
float fallback = 1f) => Evaluate(
points,
elevationDegrees,
static value => (float)value,
static value => (float)value,
fallback);
internal static float DirectionalShadow(
IReadOnlyList? points,
float elevationDegrees,
float fallback = 0f) => DirectionalShadowFromSin(
points,
MathF.Sin(elevationDegrees * (MathF.PI / 180f)),
fallback);
internal static float DirectionalShadowFromSin(
IReadOnlyList? 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);
///
/// Campaign VM VM6, corrected in the fix round: exact weather-kind
/// lookup for foliage wind, keyed by the DAT-classified
/// the frame already carries
/// (AtmosphericFrameInputs.Weather / uAtmosphereWeather.x)
/// — not the raw activeDayGroup index, which carries no weather
/// meaning by itself. Matches by weather.ToString() (ordinal)
/// against each declared
/// 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.
///
internal static (float Mean, float Gust) FoliageWind(
IReadOnlyList? 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);
}
///
/// Campaign VM VM6: one exponential-moving-average step toward
/// , used to smooth the foliage-wind mean/gust
/// strength across a day-group change so it never snaps. Pure and
/// stateless — the caller owns across calls.
/// 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).
///
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? 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? points,
float input,
Func transformPoint,
Func 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;
}
}