#version 450 core // Unified backdrop blur fragment shader. // Handles both the 1D separable blur passes (mode 0, used for BOTH the H-pass and V-pass; // `direction` picks the axis) and the composite pass (mode 1, reads the fully-blurred // working texture, masks via RRect SDF, applies tint, and writes to source_texture with // premultiplied-over blending). Working textures are sized at the full swapchain resolution; // downsampled content occupies only a sub-rect at downsample factor > 1 (set via viewport). // // The composite blends with source_texture via the standard premultiplied-over blend state // (ONE, ONE_MINUS_SRC_ALPHA). // // Backdrop primitives are tint-only — there is no outline. A specialized edge effect // (e.g. liquid-glass-style refraction outlines) would be implemented as a dedicated // primitive type with its own pipeline. // // Two modes, structurally distinct: // // Mode 0: 1D separable blur. Used for BOTH the H-pass and V-pass; `direction` (set in the // per-pass uniforms) picks (1,0) for H or (0,1) for V. Reads the previous working- // res texture and writes the next working-res texture. Fullscreen-triangle vertex // output; gl_FragCoord.xy is in working-res target pixel space; UV = // gl_FragCoord.xy * inv_working_size. // // Mode 1: composite. Reads the fully-blurred working-res texture, applies the SDF mask and // tint, writes to source_texture. Instanced unit-quad vertex output covering the // per-primitive bounds; gl_FragCoord.xy is in the full-resolution render target; // UV into the blurred working texture = // (gl_FragCoord.xy * inv_downsample_factor) * inv_working_size. // No kernel is applied here — the blur is already complete. // // V-blur is run as its own working→working pass rather than folded into the composite. The // folded variant produced a horizontal-vs-vertical asymmetry artifact: when V-blur sampled // the H-blur output through the bilinear-upsample/SDF-mask/tint pipeline in one shader // invocation, horizontal source features ended up looking sharper than vertical ones. // Matching V's structure exactly to H's restores symmetry. const uint MAX_KERNEL_PAIRS = 32; // --- Inputs from vertex shader --- layout(location = 0) in vec2 p_local; layout(location = 1) in mediump vec4 f_color; layout(location = 2) flat in vec2 f_half_size; layout(location = 3) flat in vec4 f_radii; layout(location = 4) flat in float f_half_feather; // --- Output --- layout(location = 0) out vec4 out_color; // --- Sampler --- // Mode 0: bound to downsample_texture. Mode 1: bound to h_blur_texture. layout(set = 2, binding = 0) uniform sampler2D blur_input_tex; // --- Uniforms (set 3) --- // Per-bracket-substage. `mode` matches the vertex shader's mode (0 = H, 1 = V). // `direction` selects the kernel axis for blur offsets. // `kernel` holds the per-sigma weight/offset pairs computed CPU-side using the // linear-sampling pair adjustment (RAD/Rákos). layout(set = 3, binding = 0) uniform Uniforms { vec2 inv_working_size; // 1.0 / working-resolution texture dimensions uint pair_count; // number of (weight, offset) pairs; pair[0] is the center uint mode; // 0 = H-blur, 1 = V-composite vec2 direction; // (1,0) for H, (0,1) for V — multiplied into the kernel offset float inv_downsample_factor; // 1.0 / downsample_factor (mode 1 only; mode 0 ignores) float _pad0; vec4 kernel[MAX_KERNEL_PAIRS]; // .x = weight (paired-sum for idx>0), .y = offset (texels) }; // --------------------------------------------------------------------------------------------------------------------- // ----- SDF helper -------------------- // --------------------------------------------------------------------------------------------------------------------- float sdRoundedBox(vec2 p, vec2 b, vec4 r) { vec2 rxy = (p.x > 0.0) ? r.xy : r.zw; float rr = (p.y > 0.0) ? rxy.x : rxy.y; vec2 q = abs(p) - b; if (rr == 0.0) { return max(q.x, q.y); } q += rr; return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - rr; } float sdf_alpha(float d, float h) { return 1.0 - smoothstep(-h, h, d); } // --------------------------------------------------------------------------------------------------------------------- // ----- Blur sample loop -------------- // --------------------------------------------------------------------------------------------------------------------- vec3 blur_sample(vec2 uv) { vec3 color = kernel[0].x * texture(blur_input_tex, uv).rgb; // Per-pair offset in texel space, projected onto the active axis. vec2 axis_step = direction * inv_working_size; for (uint i = 1u; i < pair_count; i += 1u) { float w = kernel[i].x; float off = kernel[i].y; vec2 step_uv = off * axis_step; color += w * texture(blur_input_tex, uv - step_uv).rgb; color += w * texture(blur_input_tex, uv + step_uv).rgb; } return color; } // --------------------------------------------------------------------------------------------------------------------- // ----- Main -------------------------- // --------------------------------------------------------------------------------------------------------------------- void main() { if (mode == 0u) { // ---- Mode 0: 1D separable blur (used for both H-pass and V-pass). // gl_FragCoord is in working-res target pixel space; sample the previous working-res // texture along `direction` with the kernel. vec2 uv = gl_FragCoord.xy * inv_working_size; vec3 color = blur_sample(uv); out_color = vec4(color, 1.0); return; } // ---- Mode 1: composite per-primitive. // RRect SDF — early discard for fragments well outside the masked region. float d = sdRoundedBox(p_local, f_half_size, f_radii); if (d > f_half_feather) { discard; } // fwidth-based normalization for AA (matches main pipeline approach). float grad_magnitude = max(fwidth(d), 1e-6); float d_n = d / grad_magnitude; float h_n = f_half_feather / grad_magnitude; // Sample the fully-blurred working-res texture. gl_FragCoord is full-res; convert to // working-res UV via inv_downsample_factor. No kernel is applied — the H+V blur passes // already produced the final blurred image; this is just an upsample + tint. vec2 uv = (gl_FragCoord.xy * inv_downsample_factor) * inv_working_size; vec3 color = texture(blur_input_tex, uv).rgb; // Tint composition: inside the masked region the panel is fully opaque — it completely // hides the original framebuffer content, just like real frosted glass and like iOS // UIBlurEffect / CSS backdrop-filter. f_color.rgb specifies the tint color; f_color.a // specifies the tint *mix strength* (NOT panel opacity). At alpha=0 we see the pure // blur; at alpha=255 we see the blur fully multiplied by the tint color. // // Output is premultiplied to match the ONE, ONE_MINUS_SRC_ALPHA blend state. Coverage // (the SDF mask's edge AA) modulates only the alpha channel, never the panel-vs-source // blend; that way edge pixels still feather correctly while mid-panel pixels stay fully // opaque. mediump vec3 tinted = mix(color, color * f_color.rgb, f_color.a); mediump float coverage = sdf_alpha(d_n, h_n); out_color = vec4(tinted * coverage, coverage); }