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>
380 lines
18 KiB
GLSL
380 lines
18 KiB
GLSL
#version 430 core
|
||
#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 V slice V2 (2026-07-27): textureHandle (uvec2, a 64-bit
|
||
// GL_ARB_bindless_texture handle) became textureIndex (uint) plus an explicit
|
||
// pad word. textureIndex is a slot into the global texture table (set 2,
|
||
// injected by tools/ShaderCompiler/VulkanGlslPreamble.cs — see
|
||
// ACDREAM_TEXTURE_HANDLE/ACDREAM_SAMPLE_ARRAY) which main() below forwards to
|
||
// the fragment stage. The pad word keeps textureLayer/flags at their original
|
||
// std430 offsets (8/12), so the struct is still 16 bytes and every existing
|
||
// CPU writer's layout is unchanged (GpuBindingModel.GpuBatchDataStrideBytes).
|
||
struct BatchData {
|
||
uint textureIndex; // slot into the global texture table
|
||
uint _pad; // keeps textureLayer/flags at offsets 8/12
|
||
uint textureLayer; // layer in the shared WB or pooled composite array
|
||
uint flags; // reserved — N.5 dispatcher owns all blend state
|
||
// (glBlendFunc per pass). If a future phase wants
|
||
// shader-side per-batch additive flag (Decision 2
|
||
// fallback), encode it here as bit 0.
|
||
};
|
||
|
||
layout(std430, binding = 0) readonly buffer InstanceBuffer {
|
||
InstanceData Instances[];
|
||
};
|
||
|
||
// binding=1 here is the SSBO namespace — distinct from the UBO namespace.
|
||
// SceneLighting UBO also uses binding=1 in the fragment shader; GL keeps
|
||
// GL_SHADER_STORAGE_BUFFER and GL_UNIFORM_BUFFER binding tables separate.
|
||
// Task 10 dispatcher binds:
|
||
// glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, instanceSsbo)
|
||
// glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, batchSsbo)
|
||
// Existing SceneLightingUboBinding handles the UBO side.
|
||
layout(std430, binding = 1) readonly buffer BatchBuffer {
|
||
BatchData Batches[];
|
||
};
|
||
|
||
// === Phase U.3: per-cell screen-space clip gate (gl_ClipDistance) =============
|
||
// Two SSBOs add the clip mechanism without disturbing binding=0/1 above.
|
||
//
|
||
// binding=2 — SHARED per-frame clip regions, one CellClip per "slot". Uploaded
|
||
// ONCE per frame by ClipFrame.UploadShared (shared across WbDrawDispatcher +
|
||
// EnvCellRenderer). Slot 0 is RESERVED = no-clip (count 0 ⇒ every plane passes).
|
||
//
|
||
// binding=3 — PER-RENDERER per-instance slot index, parallel to the binding=0
|
||
// instance buffer and indexed by the IDENTICAL per-instance index
|
||
// (gl_BaseInstanceARB + gl_InstanceID). instanceClipSlot[i] selects which
|
||
// CellClip region instance i is clipped against. Default all-zeros in U.3 ⇒
|
||
// every instance maps to slot 0 ⇒ no clipping ⇒ identical render to pre-U.3.
|
||
//
|
||
// CellClip std430 layout (144 bytes/slot): a uint count + 3 pad uints (16 bytes)
|
||
// then vec4 planes[8] (8 × 16 = 128 bytes). vec4 array stride is 16 under std430.
|
||
// ClipFrame on the CPU side lays out the bytes to match exactly (verified by
|
||
// ClipFrameLayoutTests). A clip-space vertex is INSIDE iff dot(plane, gl_Position)
|
||
// >= 0 for every active plane (see ClipPlaneSet for the plane convention).
|
||
struct CellClip {
|
||
uint count;
|
||
uint _p0;
|
||
uint _p1;
|
||
uint _p2;
|
||
vec4 planes[8];
|
||
};
|
||
layout(std430, binding = 2) readonly buffer ClipRegionBuf {
|
||
CellClip clipRegions[];
|
||
};
|
||
layout(std430, binding = 3) readonly buffer ClipSlotBuf {
|
||
uint instanceClipSlot[];
|
||
};
|
||
|
||
// === Fix B (A7 #3): per-OBJECT light selection — minimize_object_lighting =====
|
||
// retail picks up-to-8 point/spot lights PER OBJECT by the object's own position
|
||
// (minimize_object_lighting 0x0054d480), so a torch always lights the wall it
|
||
// sits on, camera-INDEPENDENTLY. The previous single global nearest-8-to-CAMERA
|
||
// UBO set (LightManager.Tick) made a wall brighten as the camera approached
|
||
// (its torches swapping into the global top-8). Two SSBOs replace that for
|
||
// point/spot lights (the SUN + ambient still come from the SceneLighting UBO):
|
||
//
|
||
// binding=4 — GLOBAL point/spot light array, uploaded once per frame from
|
||
// LightManager.PointSnapshot. The index of a light here is stable for the frame.
|
||
// binding=5 — per-instance light SET: MaxLightsPerObject(8) int indices per
|
||
// instance INTO gLights[] (-1 = unused slot), parallel to the binding=0
|
||
// instance buffer and indexed by the SAME instanceIndex. WbDrawDispatcher fills
|
||
// it once per entity (the set is constant across the entity's parts/tuples).
|
||
struct GlobalLight {
|
||
vec4 posAndKind;
|
||
vec4 dirAndRange;
|
||
vec4 colorAndIntensity;
|
||
vec4 coneAngleEtc;
|
||
};
|
||
layout(std430, binding = 4) readonly buffer GlobalLightBuf {
|
||
GlobalLight gLights[];
|
||
};
|
||
layout(std430, binding = 5) readonly buffer InstanceLightSetBuf {
|
||
int instanceLightIdx[]; // 8 per instance; -1 = unused
|
||
};
|
||
|
||
// #142: per-instance "indoor" flag, 1 per instance, parallel to the binding=0
|
||
// instance buffer (same instanceIndex). 1 = object parented to an EnvCell (skip the
|
||
// sun — retail's useSunlight==0 interior stage); 0 = outdoor object (gets the sun).
|
||
// Read ONLY inside the uniform `uLightingMode == 0` branch below, so the mode-1
|
||
// (EnvCell shell) path provably never touches it — EnvCellRenderer need not bind it.
|
||
layout(std430, binding = 6) readonly buffer InstanceIndoorBuf {
|
||
uint instanceIndoor[];
|
||
};
|
||
|
||
// #188: per-instance opacity multiplier, 1 per instance, parallel to the
|
||
// binding=0 instance buffer (same instanceIndex). 1.0 = unmodified; <1.0
|
||
// while a TransparentPartHook translucency fade is in flight for the
|
||
// entity/part this instance belongs to (e.g. the "fading wall" secret-
|
||
// passage doors). Multiplied against the sampled texture alpha in
|
||
// mesh_modern.frag.
|
||
layout(std430, binding = 7) readonly buffer InstanceAlphaBuf {
|
||
float instanceAlpha[];
|
||
};
|
||
|
||
// Retail SmartBox click confirmation. One vec2 per OBJECT instance, parallel
|
||
// to binding=0: x = CMaterial luminosity, y = CMaterial diffuse. Normal
|
||
// rendering is (0,1); SmartBox alternates LOW=(0,.35) and HIGH=(.99,1).
|
||
// EnvCellRenderer uses uLightingMode=1 and deliberately never reads this
|
||
// object-only binding.
|
||
layout(std430, binding = 8) readonly buffer InstanceSelectionLightingBuf {
|
||
vec2 instanceSelectionLighting[];
|
||
};
|
||
|
||
// Core profile: redeclare gl_PerVertex so writing gl_ClipDistance[] is legal
|
||
// alongside gl_Position. The array is sized 8 to match the CellClip plane budget
|
||
// and the GL guarantee (GL_MAX_CLIP_DISTANCES >= 8). The host enables
|
||
// GL_CLIP_DISTANCE0..7 once at startup; unused planes are set to +1.0 below so
|
||
// they pass everything (no clipping) when the slot's count < 8.
|
||
out gl_PerVertex {
|
||
vec4 gl_Position;
|
||
float gl_ClipDistance[8];
|
||
};
|
||
|
||
uniform mat4 uViewProjection;
|
||
// Absolute transform prefix in the shared shadow/world pose arena. Every
|
||
// parallel per-instance array remains local to this submission, so only the
|
||
// transform lookup keeps the absolute index.
|
||
uniform uint uTextureIndexB;
|
||
|
||
// Phase Post-A.5 (ISSUE #52, 2026-05-10): per-pass offset into Batches[].
|
||
// gl_DrawIDARB resets to 0 at the start of each glMultiDrawElementsIndirect
|
||
// call, so the transparent pass — which begins later in the indirect buffer
|
||
// — was fetching Batches[0..transparentCount) instead of its actual section
|
||
// at Batches[opaqueCount..end). The lifestone crystal (a transparent draw)
|
||
// ended up reading the FIRST OPAQUE batch's TextureHandle every frame. As
|
||
// the camera moved and the opaque front-to-back sort reordered which group
|
||
// landed at BatchData[0], the lifestone's apparent texture flickered to
|
||
// whatever was first — frequently the player character's body parts.
|
||
//
|
||
// WbDrawDispatcher.Draw sets this to 0 before the opaque MDI call and to
|
||
// _opaqueDrawCount before the transparent MDI call, matching WorldBuilder's
|
||
// uDrawIDOffset pattern in BaseObjectRenderManager.cs line 845.
|
||
uniform int uDrawIDOffset;
|
||
uniform int uLightingMode; // A7 Fix D: 0 = OBJECT (plain Lambert + sun), 1 = ENVCELL (half-Lambert wrap, no sun)
|
||
// #176 stripe-hunt isolation modes (ACDREAM_LIGHT_DEBUG, throwaway diagnostic):
|
||
// 0 = off; 1 = ambient-only vLit (all point/sun contributions killed);
|
||
// 2 = DYNAMIC point lights killed (purples + viewer fill off, statics stay);
|
||
// 3 = handled in the frag (raw vLit visualization, texture ignored).
|
||
uniform int uLightDebug;
|
||
|
||
// SceneLighting UBO — binding=1 in the UBO namespace (GL keeps the SSBO and UBO
|
||
// binding tables separate, so this coexists with the binding=1 BatchBuffer SSBO
|
||
// above). IDENTICAL std140 layout to mesh_modern.frag.
|
||
//
|
||
// A7 (2026-06-15): lighting moved from the FRAGMENT shader to HERE (per-VERTEX) so
|
||
// torch/point lights Gouraud-interpolate across each triangle the way retail's
|
||
// fixed-function T&L does (D3D DrawEnvCell vertex bake + minimize_object_lighting for
|
||
// objects). A per-PIXEL evaluation made a tight bright "spotlight" pool on flat walls;
|
||
// per-vertex spreads it into a soft, broad gradient with no hard edge.
|
||
struct Light {
|
||
vec4 posAndKind;
|
||
vec4 dirAndRange;
|
||
vec4 colorAndIntensity;
|
||
vec4 coneAngleEtc;
|
||
};
|
||
layout(std140, ACDREAM_UBO_SET binding = 1) uniform SceneLighting {
|
||
Light uLights[8];
|
||
vec4 uCellAmbient;
|
||
vec4 uFogParams;
|
||
vec4 uFogColor;
|
||
vec4 uCameraAndTime;
|
||
};
|
||
|
||
// Faithful calc_point_light (0x0059c8b0) contribution from ONE point/spot light —
|
||
// the wrap + norm shape, factored out so the per-object SSBO loop shares it. D =
|
||
// light − vertex, used UN-normalised (length = dist); N is the unit vertex normal.
|
||
// Returns the RGB to ADD, already per-channel capped to the light's own colour.
|
||
vec3 pointContribution(vec3 N, vec3 worldPos, GlobalLight L) {
|
||
int kind = int(L.posAndKind.w);
|
||
vec3 toL = L.posAndKind.xyz - worldPos; // D (un-normalised)
|
||
float distsq = dot(toL, toL);
|
||
float d = sqrt(distsq);
|
||
float range = L.dirAndRange.w; // falloff_eff = Falloff × 1.3 (static) / × 1.5 (dynamic)
|
||
if (d >= range || range <= 1e-4) return vec3(0.0);
|
||
float intensity = L.colorAndIntensity.w;
|
||
vec3 baseCol = L.colorAndIntensity.xyz;
|
||
|
||
// #143: DYNAMIC lights (viewer fill, portal, server-object lights — flagged by
|
||
// coneAngleEtc.y==1 from GlobalLightPacker) use retail's D3D hardware attenuation
|
||
// (config_hardware_light 0x0059ad30): a POINT light is given Attenuation1=1 ⇒
|
||
// att = 1/d (inverse-LINEAR), plain Lambert N·L, hard range cutoff. That spreads
|
||
// softly across the room (the portal tint, the viewer fill) instead of the static
|
||
// bake's 1/d³ distance-cube, which makes a tight concentrated pool. No per-light
|
||
// cap — D3D accumulates then saturates, which accumulateLights does via min(pointAcc,1).
|
||
if (L.coneAngleEtc.y > 0.5) {
|
||
if (uLightDebug == 2) return vec3(0.0); // #176 stripe hunt: dynamics killed
|
||
vec3 Ldir = toL / max(d, 1e-4);
|
||
float ndl = max(0.0, dot(N, Ldir));
|
||
if (ndl <= 0.0) return vec3(0.0);
|
||
if (kind == 2) { // dynamic spot: hard cos-cone gate
|
||
if (dot(-Ldir, L.dirAndRange.xyz) <= cos(L.coneAngleEtc.x * 0.5)) return vec3(0.0);
|
||
}
|
||
return (intensity * ndl / max(d, 1e-3)) * baseCol; // att = 1/d
|
||
}
|
||
|
||
// ── STATIC dat-baked lights: retail's per-vertex bake (calc_point_light 0x0059c8b0) ──
|
||
// A7 Fix D D-3: angular term by lighting path. ENVCELL bake (mode 1) keeps the
|
||
// half-Lambert wrap (lights surfaces angled away, retail calc_point_light); OBJECT
|
||
// mode (0) uses plain Lambert max(0,N·L) so a torch BEHIND a character contributes
|
||
// nothing (retail's hardware path). toL is un-normalised (length d).
|
||
float angular = (uLightingMode == 1)
|
||
? (1.0 / 1.5) * (dot(N, toL) + 0.5 * d) // half-Lambert wrap (EnvCell bake)
|
||
: max(0.0, dot(N, toL)); // plain Lambert (object/hardware)
|
||
if (angular <= 0.0) return vec3(0.0);
|
||
// NORM branch (distance-cube): >1 m → distsq·d ≈ inverse-square soft far halo;
|
||
// <1 m → just d (dodge the near singularity). "Punchy near, soft far."
|
||
float norm = (distsq > 1.0) ? (distsq * d) : d;
|
||
float scale = (1.0 - d / range) * intensity * (angular / norm);
|
||
if (kind == 2) {
|
||
// Spotlight: hard-edged cos-cone gate layered on the point ramp.
|
||
vec3 Ldir = toL / max(d, 1e-4);
|
||
float cos_edge = cos(L.coneAngleEtc.x * 0.5);
|
||
float cos_l = dot(-Ldir, L.dirAndRange.xyz);
|
||
if (cos_l <= cos_edge) scale = 0.0;
|
||
}
|
||
// Per-channel no-blowout cap to the light's OWN colour (un-intensity-scaled):
|
||
// a single light can't push a channel past its colour. Summed lit clamped in frag.
|
||
return min(scale * baseCol, baseCol);
|
||
}
|
||
|
||
vec3 accumulateAmbientLocalLights(
|
||
vec3 N,
|
||
vec3 worldPos,
|
||
int instanceIndex,
|
||
out vec3 directionalLit)
|
||
{
|
||
vec3 lit = uCellAmbient.xyz;
|
||
directionalLit = vec3(0.0);
|
||
if (uLightDebug == 1) return lit; // #176 stripe hunt: ambient only
|
||
|
||
// SUN / directional — OBJECT path only (mode 0). retail's EnvCell path
|
||
// (minimize_envcell_lighting) enables only dynamic lights, NEVER the sun, so
|
||
// EnvCell walls (mode 1) get no directional sun wash (A7 Fix D D-4).
|
||
// #142: within mode 0, also skip the sun for indoor objects (ParentCellId is an
|
||
// EnvCell). This mirrors retail's per-draw-stage useSunlight toggle: the interior
|
||
// stage runs useSunlightSet(0) (PView::DrawCells 0x005a49f3), so indoor objects
|
||
// get no sun even in windowed buildings where the player's frame is not sun-killed.
|
||
if (uLightingMode == 0) {
|
||
if (instanceIndoor[instanceIndex] == 0u) { // #142: outdoor objects only get the sun
|
||
int activeLights = int(uCellAmbient.w);
|
||
for (int i = 0; i < 8; ++i) {
|
||
if (i >= activeLights) break;
|
||
if (int(uLights[i].posAndKind.w) != 0) continue; // directional only
|
||
vec3 Ldir = normalize(uShadowLightDirectionAndSource.xyz);
|
||
float ndl = max(0.0, dot(N, Ldir));
|
||
directionalLit += uLights[i].colorAndIntensity.xyz
|
||
* uLights[i].colorAndIntensity.w * ndl;
|
||
}
|
||
}
|
||
}
|
||
|
||
// POINT / SPOT torches: their OWN accumulator (A7 Fix D, D-1). Retail's
|
||
// SetStaticLightingVertexColors sums the static point lights from BLACK and
|
||
// clamps the SUM to [0,1] before anything else (a baked emissive term), so a
|
||
// few warm intensity-100 torches can't push the whole pixel to white the way
|
||
// folding them into ambient+sun did. Mirrors LightBake.ComputeVertexColor
|
||
// (LightBakeConformanceTests). Per-light cap inside pointContribution is unchanged.
|
||
vec3 pointAcc = vec3(0.0);
|
||
int base = instanceIndex * 8;
|
||
for (int k = 0; k < 8; ++k) {
|
||
int gi = instanceLightIdx[base + k];
|
||
if (gi < 0) continue;
|
||
pointAcc += pointContribution(N, worldPos, gLights[gi]);
|
||
}
|
||
lit += min(pointAcc, vec3(1.0)); // clamp the torch sum on its own (retail baked emissive)
|
||
|
||
return lit; // frag still does the final min(lit, 1.0)
|
||
}
|
||
|
||
out vec3 vNormal;
|
||
out vec2 vTexCoord;
|
||
out vec3 vWorldPos;
|
||
out vec3 vAmbientLocalLit; // authored ambient + capped local/point lights
|
||
out vec3 vDirectionalLit; // authored outdoor directional sun, shadowable
|
||
// Campaign V slice V6e: was `flat uvec2 vTextureHandle` — a raw 64-bit
|
||
// GL_ARB_bindless_texture handle handed across the stage boundary. A varying
|
||
// cannot carry a Vulkan descriptor, so what travels is the table SLOT and the
|
||
// fragment stage does the lookup (see mesh_modern.frag). Under GL the value
|
||
// sampled is bit-for-bit the one the vertex stage used to forward; the SSBO
|
||
// read simply happens one stage later, and `flat` keeps it one scalar load per
|
||
// primitive rather than per fragment.
|
||
out flat uint vTextureIndex;
|
||
out flat uint vTextureLayer;
|
||
out flat float vOpacityMultiplier; // #188
|
||
out flat vec2 vSelectionLighting;
|
||
out flat uint vReceivesDirectionalShadow;
|
||
|
||
void main() {
|
||
int transformIndex = gl_BaseInstanceARB + gl_InstanceID;
|
||
int instanceIndex = transformIndex - int(uTextureIndexB);
|
||
mat4 model = Instances[transformIndex].transform;
|
||
vOpacityMultiplier = instanceAlpha[instanceIndex]; // #188
|
||
vSelectionLighting = (uLightingMode == 0)
|
||
? 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
|
||
// SAME instanceIndex used for the binding=0 transform above, so the slot
|
||
// travels with the instance through the MDI BaseInstance offsets. Slot 0
|
||
// (the U.3 default) has count 0 ⇒ the second loop sets all 8 distances to
|
||
// +1.0 ⇒ nothing is clipped.
|
||
uint _slot = instanceClipSlot[instanceIndex];
|
||
CellClip _c = clipRegions[_slot];
|
||
for (uint i = 0u; i < _c.count; ++i)
|
||
gl_ClipDistance[i] = dot(_c.planes[i], gl_Position);
|
||
for (uint i = _c.count; i < 8u; ++i)
|
||
gl_ClipDistance[i] = 1.0;
|
||
|
||
vWorldPos = worldPos.xyz;
|
||
vNormal = normalize(mat3(model) * aNormal);
|
||
vAmbientLocalLit = accumulateAmbientLocalLights(
|
||
vNormal,
|
||
vWorldPos,
|
||
instanceIndex,
|
||
vDirectionalLit);
|
||
// EnvCell-parented objects keep the authored indoor result. The separate
|
||
// EnvCell shell renderer never selects this receiver variant at all.
|
||
vReceivesDirectionalShadow = (uLightingMode == 0
|
||
&& instanceIndoor[instanceIndex] == 0u)
|
||
? 1u
|
||
: 0u;
|
||
vTexCoord = aTexCoord;
|
||
|
||
// 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;
|
||
}
|