Opus dual-lens review of the three VM6 commits (0930c35d,39e8408c,6cc5e183) found two blockers and two should-fix issues; all landed here along with the review's nits and documentation corrections. Blockers: - A1: the procedural-scenery classifier tested bit 31 alone instead of the full top nibble (0xF000_0000 == 0x8000_0000), so it also matched LandblockStaticEntityIdAllocator's 0xC... namespace (fences/gates/ building shells with a cutout subset), the 0xDA11_D0xx paperdoll id, and the 0xFFFF_FF01 portal-tunnel id as procedural scenery — all three would have swayed. ProceduralSceneryIdAllocator.IsInNamespace now does the exact top-nibble test; FoliageWindClassification delegates to it. - A2: GroupKey (the receiver's instance-batching key) did not carry FoliageFlags while the caster's dedup key already did, so a scenery instance and a non-scenery instance sharing a mesh subset coalesced into one receiver InstanceGroup whose flags were last-writer-wins — disagreeing with the correctly-keyed caster. GroupKey now carries FoliageFlags, computed before key construction and set exactly once at group creation; the imperative re-stamp is gone, and CachedBatch's now-redundant FoliageFlags field is removed. Should-fix: - A3: the world receiver pass bound UniformAtmosphericFrame only by accident (leftover from the caster pass, which runs first each frame, since Vulkan binding state isn't reset between passes). DirectionalShadowFrameBinding now carries the caster's exact AtmosphericFrameBufferBinding and BindDirectionalShadowReceiver binds it explicitly. - A4: a Setup-composed tree's opaque trunk part never got the trunk flag because HasCutoutSubset is cached per GfxObj part, not per entity. FoliageWindClassification.ComputeEntityHasCutoutSubset now ORs HasCutoutSubset across an entity's resolved sibling parts once per entity, threaded into ClassifyBatches/AddDirectionalShadowBatches via a new optional override parameter. Nits: A5 hashes the per-vertex flutter seed relative to the instance origin instead of absolute world XY (fp32 sin() precision loss at far landblock corners), mirrored in both foliage_wind.glsl and FoliageWindModel; A7 documents the max(maxHeight, 0.5) divide-guard as a deliberate pseudocode divergence; A8 switches FoliageWindExclusions' construction to ToFrozenSet() and softens the "never stale" doc comment to "no slower than one frame behind." Tests added: top-nibble classification (0xFFFFFFFFu now correctly false), GroupKey inequality across entity-driven scenery/landblock- static classification, a caster-batch test proving the same pairing never coalesces, ComputeEntityHasCutoutSubset unit + end-to-end two-part-Setup tests, the caster→receiver AtmosphericFrame binding carry-through, flutter-hash translation invariance relative to instance origin, and a Storm-wind mid-height displacement floor guarding against a "no motion" regression. Docs: plan VM6 body corrected to the five-row WeatherKind table, "bits 1 and 2", "all four" caster shaders, and top-nibble wording throughout; the owner gate checklist's Rain/Storm step; the stale v1-only shader- interface compatibility entry; semantic-bindings-v1.md's v2 members folded into the main 192-byte block; the IA-25 register row's top- nibble wording; AtmosphericFrameInputs.cs's ABI size reference. foliage_wind.glsl's A5 change recompiled exactly the five shaders that include it (mesh_atmospheric.vert, the four directional_shadow_world_* casters) plus the manifest; no other .spv changed. Verify: Release build 0 warnings/0 errors. App hermetic-lane filter 6,041/0 failed (no environment-specific failures this run). RenderPackValidator 30/30. Full hermetic-filtered solution: 15,269/0 failed across 15 projects. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
96 lines
3.8 KiB
C#
96 lines
3.8 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Rendering.Wb;
|
|
|
|
namespace AcDream.App.Rendering.Packs;
|
|
|
|
/// <summary>
|
|
/// CPU-side mirror of <c>foliage_wind.glsl</c>'s <c>acdreamFoliageDisplace</c>,
|
|
/// added by Campaign VM VM6.
|
|
///
|
|
/// <para>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
|
|
/// <see cref="AtmosphericColorPipeline"/>. Any change to
|
|
/// <c>acdreamFoliageDisplace</c> — 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.</para>
|
|
/// </summary>
|
|
internal static class FoliageWindModel
|
|
{
|
|
private const uint FoliageMask =
|
|
FoliageWindClassification.CutoutFoliageFlag | FoliageWindClassification.TrunkFlag;
|
|
|
|
/// <summary>
|
|
/// Mirrors <c>acdreamFoliageDisplace</c> exactly. <paramref name="clockWind"/>
|
|
/// is (elapsed seconds, wind mean [0..1], wind gust [0..1], wind
|
|
/// direction radians); <paramref name="amplitude"/> is (lean m, branch
|
|
/// m, flutter m, max canopy height m) — the same layout as
|
|
/// <c>uAtmosphereClockWind</c>/<c>uAtmosphereWindAmplitude</c>.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>GLSL <c>fract</c>: always non-negative, matches <c>x - floor(x)</c>.</summary>
|
|
private static float Frac(float x) => x - MathF.Floor(x);
|
|
}
|