Backdrop Path + Cybersteel (#23)
Co-authored-by: Zachary Levy <zachary@sunforge.is> Reviewed-on: #23
This commit was merged in pull request #23.
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
#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);
|
||||
}
|
||||
Reference in New Issue
Block a user