using System.Numerics;
using AcDream.App.Rendering.Wb;
namespace AcDream.App.Rendering.Packs;
///
/// CPU-side mirror of foliage_wind.glsl's acdreamFoliageDisplace,
/// added by Campaign VM VM6.
///
/// The GLSL is the source of truth for what actually renders — this
/// class exists only so the motion model's shape (identity on non-foliage
/// flags, identity when the wind is calm, zero displacement at the trunk
/// base, bounded displacement at the canopy top, no flutter on a trunk
/// subset, never-increasing height) can be pinned by fast CPU tests instead
/// of a GPU capture, matching the same pattern as
/// . Any change to
/// acdreamFoliageDisplace — the gust envelope constants, the lean/
/// branch/flutter formulas, the bend-shortening term, the per-vertex hash —
/// MUST be mirrored here in the same commit, or this class silently stops
/// proving what the shader does.
///
internal static class FoliageWindModel
{
private const uint FoliageMask =
FoliageWindClassification.CutoutFoliageFlag | FoliageWindClassification.TrunkFlag;
///
/// Mirrors acdreamFoliageDisplace exactly.
/// is (elapsed seconds, wind mean [0..1], wind gust [0..1], wind
/// direction radians); is (lean m, branch
/// m, flutter m, max canopy height m) — the same layout as
/// uAtmosphereClockWind/uAtmosphereWindAmplitude.
///
internal static Vector3 Displace(
Vector3 worldPos,
Vector3 instanceOrigin,
uint batchFlags,
Vector4 clockWind,
Vector4 amplitude)
{
if ((batchFlags & FoliageMask) == 0u)
return worldPos;
float t = clockWind.X;
float mean = clockWind.Y;
float gust = clockWind.Z;
float dirA = clockWind.W;
var dir = new Vector2(MathF.Cos(dirA), MathF.Sin(dirA));
var perp = new Vector2(-dir.Y, dir.X);
float h = Math.Clamp(
(worldPos.Z - instanceOrigin.Z) / MathF.Max(amplitude.W, 0.5f),
0f,
1f);
float k = h * h;
float ph = Vector2.Dot(
new Vector2(instanceOrigin.X, instanceOrigin.Y),
new Vector2(0.137f, 0.291f));
float g = 0.5f
+ (0.5f * MathF.Sin((0.05f * t) + ph))
+ (0.25f * MathF.Sin((0.13f * t) + (1.7f * ph)));
float s = mean + (gust * g);
float lean = k * amplitude.X * s * (0.8f + (0.2f * MathF.Sin((0.35f * t) + ph)));
Vector2 d = dir * lean;
if ((batchFlags & FoliageWindClassification.CutoutFoliageFlag) != 0u)
{
float branch = k * amplitude.Y * s * MathF.Sin((1.1f * t) + ph + (2.0f * h));
// Review fix round A5: relative to the instance origin, not
// absolute world XY — matches foliage_wind.glsl exactly (see
// its comment for the fp32-precision-at-far-landblocks reason).
var relativeXY = new Vector2(
worldPos.X - instanceOrigin.X,
worldPos.Y - instanceOrigin.Y);
float vh = Frac(
MathF.Sin(Vector2.Dot(relativeXY, new Vector2(12.9898f, 78.233f))) * 43758.5453f);
float flutter = h * amplitude.Z * s * MathF.Sin((6.0f * t) + (7.0f * vh));
d += (dir * branch)
+ (perp * 0.35f * branch)
+ (new Vector2(MathF.Cos(6.2832f * vh), MathF.Sin(6.2832f * vh)) * flutter);
}
Vector3 p = worldPos;
p.X += d.X;
p.Y += d.Y;
p.Z -= 0.5f * Vector2.Dot(d, d) / MathF.Max(h * amplitude.W, 0.5f);
return p;
}
/// GLSL fract: always non-negative, matches x - floor(x).
private static float Frac(float x) => x - MathF.Floor(x);
}