feat(render): weather-driven foliage wind for procedural scenery, shadows follow (Campaign VM VM6b)
Procedural-scenery foliage (trees/bushes — entity ids in the ProceduralSceneryIdAllocator's 0x8XXYYIII namespace) sways with weather in mesh_atmospheric.vert and all four directional_shadow_world_* caster vertex shaders, both calling the identical new foliage_wind.glsl include so the shadow moves with the leaf by construction. Classification (FoliageWindClassification, AcDream.App.Rendering.Wb): two new BatchData.flags bits, computed once per (entity, subset) from four inputs — entity id (bit 31 for procedural scenery), the pack's declared FoliageExclusions membership, the subset's TranslucencyKind, and ObjectRenderData.HasCutoutSubset (computed once per mesh at build time, not per frame). Bit 1 marks an alpha-cutout leaf subset; bit 2 marks an opaque trunk subset (only when its own mesh also owns a cutout subset, so rocks stay still). WbDrawDispatcher.ClassifyBatches (world receiver) and AddDirectionalShadowBatches (caster) call this with the same four inputs, so casters and receivers classify identically without needing to share state. Retail's mesh_modern/terrain_modern/mesh_detail pipelines never read these bits, so pack-off output is unaffected. Motion model (foliage_wind.glsl, mirrored bit-for-bit in the new FoliageWindModel for hermetic CPU tests): height-squared-scaled slow lean for every foliage subset, plus branch swing and per-vertex-hash-decorrelated flutter for cutout subsets only. AtmosphericPostProcessGraph.ResolveFoliageWind resolves the wind block once per frame.Serial — advanced by whichever of RenderDirectionalShadows (which runs first) or RenderPostProcess is called first that frame, with the second reading the already-advanced state, which is what keeps the caster and receiver reading byte-identical clock/strength values. The per-day-group mean/gust target (AtmospherePolicyDeclaration. FoliageWindByDayGroup, keyed by the same day-group index convention ActiveDayGroupMultipliers already established: Clear/Cloudy/Overcast/Rainy) eases toward its target over WeatherSystem.TransitionSeconds (10s) so a weather change never snaps; wind-enabled off or indoor instead gates the OUTPUT to an exact zero (not an asymptotic approach) so a settings toggle or cell transition is immediate. The wind clock is a Stopwatch started at graph construction (monotonic, session-relative magnitude for GPU sin() accuracy), overridable by the same ACDREAM_SKY_PHASE_SECONDS pin SkyRenderer already uses, for deterministic offline gates. New settings: wind-enabled, wind-strength, wind-direction-degrees (225° default — no authored retail wind direction exists to read), wind-lean-metres, wind-branch-metres, wind-flutter-metres (0 on Low), wind-canopy-height-metres. Register row IA-25 files this as an intentional, strictly opt-in divergence: retail applies no per-vertex wind displacement to any geometry. Known, accepted limitation: classification is per mesh-subset (one BatchData.flags word per indirect-draw batch), not per entity instance, so the rare case of one mesh subset being reachable from both a procedural-scenery and a non-scenery placement would classify all of that subset's instances alike. Tests: FoliageWindClassificationTests (the full classification matrix), FoliageWindModelTests (identity on non-foliage/calm-wind/base-vertex, canopy-top displacement bound, z-never-increases, trunk has no flutter term), RenderPackAtmospherePolicyEvaluationTests (exact day-group lookup, no interpolation across day-group ids, easing convergence without overshoot or discontinuity), AtmosphericShaderAbiTests (each of the five shaders calls acdreamFoliageDisplace exactly once; mesh_modern/terrain/mesh_detail call it never), and four AtmosphericPostProcessGraphTests additions (indoor/disabled exact-zero gating, settings-to-UBO wiring, same-frame-Serial idempotency — the last proxies the caster/receiver agreement invariant without needing this hermetic harness's WbDrawDispatcher/TerrainModernRenderer dependency chain to exercise RenderDirectionalShadows directly). App hermetic filter: 6015/6017 (the same 2 pre-existing failures as VM6a, confirmed unrelated). Core.Tests hermetic: 4697/4697. RenderPackValidator.Tests: 30/30. Full solution Debug and Release builds green. Shader recompile touched exactly the 5 edited files' .spv (plus manifest); the retail oracle set and every other pack shader are byte-identical. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
0930c35d1d
commit
39e8408c7d
34 changed files with 1536 additions and 64 deletions
|
|
@ -2,6 +2,8 @@
|
|||
#extension GL_ARB_shader_draw_parameters : require
|
||||
|
||||
#include "directional_shadow_common.glsl"
|
||||
#include "atmospheric_common.glsl"
|
||||
#include "foliage_wind.glsl"
|
||||
|
||||
layout(location = 0) in vec3 aPosition;
|
||||
layout(location = 1) in vec3 aNormal;
|
||||
|
|
@ -31,10 +33,19 @@ flat out uint vShadowTextureLayer;
|
|||
|
||||
void main() {
|
||||
int instanceIndex = gl_BaseInstanceARB + gl_InstanceID;
|
||||
vec4 worldPosition = Instances[instanceIndex].transform * vec4(aPosition, 1.0);
|
||||
mat4 model = Instances[instanceIndex].transform;
|
||||
BatchData batch = Batches[uDrawIDOffset + gl_DrawIDARB];
|
||||
vec4 worldPosition = model * vec4(aPosition, 1.0);
|
||||
// Campaign VM VM6: weather-driven foliage sway — see foliage_wind.glsl.
|
||||
// A no-op unless batch.flags carries the cutout (0x2) classification bit.
|
||||
worldPosition.xyz = acdreamFoliageDisplace(
|
||||
worldPosition.xyz,
|
||||
model[3].xyz,
|
||||
batch.flags,
|
||||
uAtmosphereClockWind,
|
||||
uAtmosphereWindAmplitude);
|
||||
gl_Position = uShadowWorldToClip[uRenderPass] * worldPosition;
|
||||
|
||||
BatchData batch = Batches[uDrawIDOffset + gl_DrawIDARB];
|
||||
vShadowTexCoord = aTexCoord;
|
||||
vShadowTextureIndex = batch.textureIndex;
|
||||
vShadowTextureLayer = batch.textureLayer;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
#extension GL_EXT_multiview : require
|
||||
|
||||
#include "directional_shadow_common.glsl"
|
||||
#include "atmospheric_common.glsl"
|
||||
#include "foliage_wind.glsl"
|
||||
|
||||
layout(location = 0) in vec3 aPosition;
|
||||
layout(location = 1) in vec3 aNormal;
|
||||
|
|
@ -29,9 +31,18 @@ flat out uint vShadowTextureLayer;
|
|||
|
||||
void main() {
|
||||
int instanceIndex = gl_BaseInstanceARB + gl_InstanceID;
|
||||
vec4 worldPosition = Instances[instanceIndex].transform * vec4(aPosition, 1.0);
|
||||
gl_Position = uShadowWorldToClip[int(gl_ViewIndex)] * worldPosition;
|
||||
mat4 model = Instances[instanceIndex].transform;
|
||||
BatchData batch = Batches[uDrawIDOffset + gl_DrawIDARB];
|
||||
vec4 worldPosition = model * vec4(aPosition, 1.0);
|
||||
// Campaign VM VM6: weather-driven foliage sway — see foliage_wind.glsl.
|
||||
// A no-op unless batch.flags carries the cutout (0x2) classification bit.
|
||||
worldPosition.xyz = acdreamFoliageDisplace(
|
||||
worldPosition.xyz,
|
||||
model[3].xyz,
|
||||
batch.flags,
|
||||
uAtmosphereClockWind,
|
||||
uAtmosphereWindAmplitude);
|
||||
gl_Position = uShadowWorldToClip[int(gl_ViewIndex)] * worldPosition;
|
||||
vShadowTexCoord = aTexCoord;
|
||||
vShadowTextureIndex = batch.textureIndex;
|
||||
vShadowTextureLayer = batch.textureLayer;
|
||||
|
|
|
|||
|
|
@ -2,20 +2,44 @@
|
|||
#extension GL_ARB_shader_draw_parameters : require
|
||||
|
||||
#include "directional_shadow_common.glsl"
|
||||
#include "atmospheric_common.glsl"
|
||||
#include "foliage_wind.glsl"
|
||||
|
||||
layout(location = 0) in vec3 aPosition;
|
||||
layout(location = 1) in vec3 aNormal;
|
||||
layout(location = 2) in vec2 aTexCoord;
|
||||
|
||||
struct InstanceData { mat4 transform; };
|
||||
// Campaign VM VM6: this pipeline had no BatchData binding before — an opaque
|
||||
// caster never needed a texture lookup. It reads batch.flags here purely for
|
||||
// the foliage-wind trunk classification bit (0x4); textureIndex/textureLayer
|
||||
// stay unread.
|
||||
struct BatchData {
|
||||
uint textureIndex;
|
||||
uint _pad;
|
||||
uint textureLayer;
|
||||
uint flags;
|
||||
};
|
||||
layout(std430, binding = 0) readonly buffer InstanceBuffer {
|
||||
InstanceData Instances[];
|
||||
};
|
||||
layout(std430, binding = 1) readonly buffer BatchBuffer {
|
||||
BatchData Batches[];
|
||||
};
|
||||
|
||||
uniform int uDrawIDOffset;
|
||||
uniform int uRenderPass;
|
||||
|
||||
void main() {
|
||||
int instanceIndex = gl_BaseInstanceARB + gl_InstanceID;
|
||||
vec4 worldPosition = Instances[instanceIndex].transform * vec4(aPosition, 1.0);
|
||||
mat4 model = Instances[instanceIndex].transform;
|
||||
BatchData batch = Batches[uDrawIDOffset + gl_DrawIDARB];
|
||||
vec4 worldPosition = model * vec4(aPosition, 1.0);
|
||||
worldPosition.xyz = acdreamFoliageDisplace(
|
||||
worldPosition.xyz,
|
||||
model[3].xyz,
|
||||
batch.flags,
|
||||
uAtmosphereClockWind,
|
||||
uAtmosphereWindAmplitude);
|
||||
gl_Position = uShadowWorldToClip[uRenderPass] * worldPosition;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,18 +3,43 @@
|
|||
#extension GL_EXT_multiview : require
|
||||
|
||||
#include "directional_shadow_common.glsl"
|
||||
#include "atmospheric_common.glsl"
|
||||
#include "foliage_wind.glsl"
|
||||
|
||||
layout(location = 0) in vec3 aPosition;
|
||||
layout(location = 1) in vec3 aNormal;
|
||||
layout(location = 2) in vec2 aTexCoord;
|
||||
|
||||
struct InstanceData { mat4 transform; };
|
||||
// Campaign VM VM6: this pipeline had no BatchData binding before — an opaque
|
||||
// caster never needed a texture lookup. It reads batch.flags here purely for
|
||||
// the foliage-wind trunk classification bit (0x4); textureIndex/textureLayer
|
||||
// stay unread.
|
||||
struct BatchData {
|
||||
uint textureIndex;
|
||||
uint _pad;
|
||||
uint textureLayer;
|
||||
uint flags;
|
||||
};
|
||||
layout(std430, binding = 0) readonly buffer InstanceBuffer {
|
||||
InstanceData Instances[];
|
||||
};
|
||||
layout(std430, binding = 1) readonly buffer BatchBuffer {
|
||||
BatchData Batches[];
|
||||
};
|
||||
|
||||
uniform int uDrawIDOffset;
|
||||
|
||||
void main() {
|
||||
int instanceIndex = gl_BaseInstanceARB + gl_InstanceID;
|
||||
vec4 worldPosition = Instances[instanceIndex].transform * vec4(aPosition, 1.0);
|
||||
mat4 model = Instances[instanceIndex].transform;
|
||||
BatchData batch = Batches[uDrawIDOffset + gl_DrawIDARB];
|
||||
vec4 worldPosition = model * vec4(aPosition, 1.0);
|
||||
worldPosition.xyz = acdreamFoliageDisplace(
|
||||
worldPosition.xyz,
|
||||
model[3].xyz,
|
||||
batch.flags,
|
||||
uAtmosphereClockWind,
|
||||
uAtmosphereWindAmplitude);
|
||||
gl_Position = uShadowWorldToClip[int(gl_ViewIndex)] * worldPosition;
|
||||
}
|
||||
|
|
|
|||
91
src/AcDream.App/Rendering/Shaders/foliage_wind.glsl
Normal file
91
src/AcDream.App/Rendering/Shaders/foliage_wind.glsl
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
#ifndef ACDREAM_FOLIAGE_WIND_GLSL
|
||||
#define ACDREAM_FOLIAGE_WIND_GLSL
|
||||
|
||||
// Campaign VM VM6 (2026-08-22): weather-driven foliage sway for procedural
|
||||
// scenery (trees/bushes — see ProceduralSceneryIdAllocator, bit 31 of the
|
||||
// entity id). Shared verbatim between mesh_atmospheric.vert (the world
|
||||
// receiver) and the four directional_shadow_world_*.vert casters so a
|
||||
// displaced leaf's shadow moves with it by construction — the caster and
|
||||
// receiver read the identical uAtmosphereClockWind/uAtmosphereWindAmplitude
|
||||
// inputs from atmospheric_common.glsl and call this exact function.
|
||||
//
|
||||
// batchFlags bit 1 (0x2, FOLIAGE_CUTOUT) marks an alpha-cutout leaf/frond
|
||||
// subset of a procedural-scenery entity; bit 2 (0x4, FOLIAGE_TRUNK) marks an
|
||||
// opaque subset of a procedural-scenery entity that also owns a cutout
|
||||
// subset (a tree trunk, not a rock). Both bits are computed once in
|
||||
// WbDrawDispatcher.ClassifyBatches / AddDirectionalShadowBatches (host
|
||||
// C#) — this include never re-derives them from geometry.
|
||||
//
|
||||
// Motion model (see docs/plans/2026-08-22-visualmaster-campaign.md's VM6
|
||||
// section for the full derivation): the whole tree leans slowly with the
|
||||
// mean+gust wind, scaled by height-squared so the base never moves; cutout
|
||||
// subsets additionally get a faster branch swing and a fast, per-vertex
|
||||
// decorrelated flutter. Bend shortens the vertical extent (p.z -= ...) so a
|
||||
// bent canopy sinks slightly instead of stretching — the cheap
|
||||
// length-preserving correction. Normals are NOT rotated: Gouraud on a
|
||||
// flipped-normal cutout leaf would flicker, and AC's flat-lit foliage does
|
||||
// not need it.
|
||||
//
|
||||
// Budget: ~25 ALU ops per foliage vertex (early-out for every non-foliage
|
||||
// vertex — one uint AND and a branch), zero CPU cost, zero extra draw
|
||||
// submissions. See FoliageWindModel (AcDream.App.Rendering.Wb) for the
|
||||
// bit-exact CPU mirror used by the hermetic conformance tests.
|
||||
vec3 acdreamFoliageDisplace(
|
||||
vec3 worldPos,
|
||||
vec3 instanceOrigin,
|
||||
uint batchFlags,
|
||||
vec4 clockWind,
|
||||
vec4 amp)
|
||||
{
|
||||
if ((batchFlags & 0x6u) == 0u)
|
||||
return worldPos;
|
||||
|
||||
float t = clockWind.x;
|
||||
float mean = clockWind.y;
|
||||
float gust = clockWind.z;
|
||||
float dirA = clockWind.w;
|
||||
|
||||
vec2 dir = vec2(cos(dirA), sin(dirA));
|
||||
vec2 perp = vec2(-dir.y, dir.x);
|
||||
|
||||
// 0 at the instance's own base, 1 at the declared canopy top. Height is
|
||||
// measured from the instance ORIGIN (the translation column of its
|
||||
// transform), not the vertex's local Z, so a leaned tree's own leaves
|
||||
// still measure height from the trunk base correctly.
|
||||
float h = clamp((worldPos.z - instanceOrigin.z) / max(amp.w, 0.5), 0.0, 1.0);
|
||||
float k = h * h;
|
||||
|
||||
// Per-tree phase from world position so neighbouring trees fall out of
|
||||
// step with each other.
|
||||
float ph = dot(instanceOrigin.xy, vec2(0.137, 0.291));
|
||||
|
||||
// Gust envelope: two slow sines beat against each other so gusts arrive
|
||||
// and leave rather than pulsing at one fixed period.
|
||||
float g = 0.5 + 0.5 * sin(0.05 * t + ph) + 0.25 * sin(0.13 * t + 1.7 * ph);
|
||||
float s = mean + gust * g;
|
||||
|
||||
// Slow whole-tree lean, height-squared scaled.
|
||||
float lean = k * amp.x * s * (0.8 + 0.2 * sin(0.35 * t + ph));
|
||||
vec2 d = dir * lean;
|
||||
|
||||
if ((batchFlags & 0x2u) != 0u)
|
||||
{
|
||||
// Cutout (leaves): branch swing at ~1 Hz with a phase that runs up
|
||||
// the tree, plus a fast per-vertex flutter decorrelated by a hash of
|
||||
// the vertex's own world XY so leaves on the same tree do not move
|
||||
// in lockstep.
|
||||
float branch = k * amp.y * s * sin(1.1 * t + ph + 2.0 * h);
|
||||
float vh = fract(sin(dot(worldPos.xy, vec2(12.9898, 78.233))) * 43758.5453);
|
||||
float flutter = h * amp.z * s * sin(6.0 * t + 7.0 * vh);
|
||||
d += dir * branch + perp * 0.35 * branch
|
||||
+ vec2(cos(6.2832 * vh), sin(6.2832 * vh)) * flutter;
|
||||
}
|
||||
|
||||
vec3 p = worldPos;
|
||||
p.xy += d;
|
||||
// Bend shortens the vertical extent instead of stretching it.
|
||||
p.z -= 0.5 * dot(d, d) / max(h * amp.w, 0.5);
|
||||
return p;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -2,6 +2,8 @@
|
|||
#extension GL_ARB_shader_draw_parameters : require
|
||||
|
||||
#include "directional_shadow_common.glsl"
|
||||
#include "atmospheric_common.glsl"
|
||||
#include "foliage_wind.glsl"
|
||||
|
||||
layout(location = 0) in vec3 aPosition;
|
||||
layout(location = 1) in vec3 aNormal;
|
||||
|
|
@ -324,7 +326,21 @@ void main() {
|
|||
? instanceSelectionLighting[instanceIndex]
|
||||
: vec2(0.0, 1.0);
|
||||
|
||||
BatchData b = Batches[uDrawIDOffset + gl_DrawIDARB];
|
||||
|
||||
vec4 worldPos = model * vec4(aPosition, 1.0);
|
||||
// Campaign VM VM6: weather-driven foliage sway. acdreamFoliageDisplace is
|
||||
// a no-op unless b.flags carries the cutout (0x2) or trunk (0x4)
|
||||
// classification bit — see foliage_wind.glsl. Applied before gl_Position
|
||||
// so clip distances, lighting, and the fragment stage all see the
|
||||
// displaced position; the four directional-shadow caster vertex shaders
|
||||
// call the identical include so the shadow moves with the same vertex.
|
||||
worldPos.xyz = acdreamFoliageDisplace(
|
||||
worldPos.xyz,
|
||||
model[3].xyz,
|
||||
b.flags,
|
||||
uAtmosphereClockWind,
|
||||
uAtmosphereWindAmplitude);
|
||||
gl_Position = uViewProjection * worldPos;
|
||||
|
||||
// Phase U.3: per-instance clip gate. instanceClipSlot is indexed by the
|
||||
|
|
@ -354,10 +370,11 @@ void main() {
|
|||
: 0u;
|
||||
vTexCoord = aTexCoord;
|
||||
|
||||
BatchData b = Batches[uDrawIDOffset + gl_DrawIDARB];
|
||||
// Campaign V slice V6e: forward the table SLOT untouched. V2 looked the
|
||||
// handle up here and passed the handle; the lookup now lives at the sample
|
||||
// site in mesh_modern.frag, which is the only form Vulkan can express.
|
||||
// (b was fetched earlier, before worldPos, so acdreamFoliageDisplace
|
||||
// could read its flags — Campaign VM VM6.)
|
||||
vTextureIndex = b.textureIndex;
|
||||
vTextureLayer = b.textureLayer;
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -151,7 +151,7 @@
|
|||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "8331583c1d63ee59b3f7898e25b2e9730df98fd9da28273661caad948fb46ae5",
|
||||
"sourceSha256": "fe67e9bff0e528b08fa699f0be17d1c49660ad63ab34f7ac7dd31f79a2253006",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
|
|
@ -167,7 +167,7 @@
|
|||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "a4bb3b86283af310a22d3943dd1afadb8a72b42c9e5501efc9abf2b5124a1446",
|
||||
"sourceSha256": "2a155a7964ff15bd936394b8ffd3a331d5598e140ca36268759520bab86b1bdc",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
|
|
@ -183,7 +183,7 @@
|
|||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "c2586df5f09518c40d052bbacdfff2c00a82b1eefec4868749d0580571198067",
|
||||
"sourceSha256": "74b1aa64396dd74185d644de7e9f40626c9422f335da60deb7c606f2a45d92c2",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
|
|
@ -199,7 +199,7 @@
|
|||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "35b4d524153623691ab523a049212aa35551f96c169c28c08f62d93e31d9e68d",
|
||||
"sourceSha256": "f15425837f7a8f84337f4493989bcd7cb1af87f916f7678fc016947f68d26be5",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
|
|
@ -215,7 +215,7 @@
|
|||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "d4f8bc12ee84379ced84f5b703cf8be95798a23a7063773e214bc1eb6ebbb65e",
|
||||
"sourceSha256": "7e3e49483024145bb71929f715a68087380e5126e15b5bef2c1c5c7d5e2e3516",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue