#version 450 core #include "common.glsl" // Per-instance attributes; must match BillboardInstance (Billboard.h). layout(location = 0) in vec4 iPosSway; // pivot xyz, sway layout(location = 1) in vec4 iSizeFlipTint; // size xy, flip, tint layout(location = 2) in vec4 iRect; // uv sub-rect (minU, minV, maxU, maxV) layout(location = 3) in vec4 iAnchorFx; // anchor v (< 0 = uOpaque.y), turn, alpha, flash uniform vec4 uOpaque; // opaque pixel bounds in uv (minX, minY, maxX, maxY) uniform int uPass; // 0 = color, 1 = shadow out VS { vec2 uv; vec3 worldPos; vec3 pivot; vec2 local; // (horizontal offset, height above pivot) in world units float tint; flat vec2 fx; // alpha, flash } o; void main() { vec2 corner = vec2(float(gl_VertexID & 1), float(gl_VertexID >> 1)); vec2 size = iSizeFlipTint.xy; vec3 pivot = iPosSway.xyz; vec4 rect = iRect; float rectH = max(rect.w - rect.y, 1e-6); float anchor = iAnchorFx.x < 0.0 ? uOpaque.y : iAnchorFx.x; float anchorLocal = (anchor - rect.y) / rectH; float lx = (corner.x - 0.5) * size.x; float ly = (corner.y - anchorLocal) * size.y; // the feet row sits on the pivot // Shadow cards face the sun so silhouettes are never edge-on to the light; // `turn` yaws color cards away from camera-facing (paper turn). vec3 right = uBillboardRight.xyz; if (iAnchorFx.y != 0.0) { float ct = cos(iAnchorFx.y), st = sin(iAnchorFx.y); right = vec3(right.x * ct - right.z * st, 0.0, right.x * st + right.z * ct); } if (uPass == 1) right = uShadowRight.xyz; vec3 wp = pivot + right * lx + vec3(0.0, ly, 0.0); float sway = iPosSway.w; if (sway > 0.0) { // Feet to the top of the rect (or of the opaque pixels, if lower). float visibleHeight = max(size.y * (min(uOpaque.w, rect.w) - anchor) / rectH, 1e-3); float k = clamp(ly / visibleHeight, 0.0, 1.0); k *= k; float ph = dot(pivot.xz, vec2(0.37, 0.61)); vec2 wdir = normalize(uWind.xy + 1e-5); float gust = vnoise(pivot.xz * 0.15 - wdir * uTime * uWind.w); float w = (sin(uTime * 2.1 + ph) * 0.5 + sin(uTime * 3.7 + ph * 1.7) * 0.25 + gust * 1.3) * uWind.z; vec2 disp = wdir * w; // Bend away from the interactor (player) and rustle while it is close. vec3 toP = pivot - uInteractor.xyz; float d = length(toP.xz); float push = clamp(1.0 - d / uInteractor.w, 0.0, 1.0) * step(abs(toP.y), 1.0); disp += (d > 1e-3 ? toP.xz / d : vec2(0.0)) * push * 0.5; disp += vec2(sin(uTime * 19.0 + ph * 7.0), cos(uTime * 17.0 + ph * 5.0)) * push * 0.05; wp.xz += disp * k * sway * visibleHeight; wp.y -= push * k * sway * 0.3 * visibleHeight; } vec2 c = corner; if (iSizeFlipTint.z > 0.5) c.x = 1.0 - c.x; // mirror inside the sub-rect o.uv = mix(rect.xy, rect.zw, c); o.worldPos = wp; o.pivot = pivot; o.local = vec2(lx, ly); o.tint = iSizeFlipTint.w; o.fx = iAnchorFx.zw; gl_Position = uViewProj * vec4(wp, 1.0); gl_ClipDistance[0] = dot(vec4(wp, 1.0), uClipPlane); }