157 lines
7.4 KiB
GLSL
157 lines
7.4 KiB
GLSL
#version 450 core
|
|
|
|
// Unified backdrop blur fragment shader.
|
|
// Handles both H-blur (mode 0, blurs the ¼-resolution downsample texture into
|
|
// the ¼-resolution h_blur texture) and V-blur+composite (mode 1, blurs h_blur
|
|
// vertically, masks via RRect SDF, applies tint, composites outline, and writes
|
|
// to the main render target with premultiplied alpha).
|
|
//
|
|
// Following RAD's pattern, V-mode replaces a separate composite pass: the SDF
|
|
// discard limits V-blur work to the masked region, and the per-primitive tint
|
|
// is folded in. Output blends with the main render target 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.
|
|
//
|
|
// Splitting V-blur out of the composite pass (an earlier version combined them) was needed
|
|
// to avoid a horizontal-vs-vertical asymmetry artifact: when the 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. Running V-blur as
|
|
// its own working→working pass (matching H's structure exactly) 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 (Option B semantics): 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 without re-introducing the bug
|
|
// where mid-panel pixels became semi-transparent.
|
|
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);
|
|
}
|