// Liquid Glass fragment shader. One instance = one rounded rect drawn as a
// full-viewport triangle; egui-wgpu sets the viewport to the callback rect.
// All coordinates are physical pixels (framebuffer space).
struct Uniforms {
rect_min: vec2<f32>,
rect_max: vec2<f32>,
bd_min: vec2<f32>, // where the full backdrop image is mapped on screen
bd_max: vec2<f32>,
tint: vec4<f32>, // straight alpha
radius: f32,
blur: f32, // blur radius in px
refraction: f32, // max edge displacement in px
edge_width: f32, // width of the lens zone in px
chroma: f32,
brightness: f32,
saturation: f32,
specular: f32,
border: f32,
shadow: f32,
shadow_radius: f32,
srgb_out: f32,
light_dir: vec2<f32>,
max_lod: f32,
_pad: f32,
corner_a: vec4<f32>, // p, a, b, c (smoothed corner, px, see renderer::corner_params)
corner_b: vec4<f32>, // d, r, theta3
fill: vec4<f32>, // colour shown where a sample falls outside the backdrop rect
};
@group(0) @binding(0) var<uniform> u: Uniforms;
@group(0) @binding(1) var bd_tex: texture_2d<f32>;
@group(0) @binding(2) var bd_samp: sampler;
@vertex
fn vs_main(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> {
// Full-viewport triangle.
let x = f32(i32(i & 1u) * 4 - 1);
let y = f32(i32(i >> 1u) * 4 - 1);
return vec4<f32>(x, y, 0.0, 1.0);
}
const CORNER_SEGMENTS: i32 = 18;
fn bezier(p0: vec2<f32>, p1: vec2<f32>, p2: vec2<f32>, p3: vec2<f32>, t: f32) -> vec2<f32> {
let u = 1.0 - t;
return u * u * u * p0 + 3.0 * u * u * t * p1 + 3.0 * u * t * t * p2 + t * t * t * p3;
}
// Point `i` (0..=18) of the smoothed corner polyline in the corner's local
// frame: x along the edge away from the corner, y inward from the edge.
fn corner_point(i: i32) -> vec2<f32> {
let p = u.corner_a.x;
let a = u.corner_a.y;
let b = u.corner_a.z;
let c = u.corner_a.w;
let d = u.corner_b.x;
let r = u.corner_b.y;
let theta3 = u.corner_b.z;
let p3 = vec2<f32>(p - a - b - c, d);
if (i <= 6) {
return bezier(vec2<f32>(p, 0.0), vec2<f32>(p - a, 0.0), vec2<f32>(p - a - b, 0.0), p3, f32(i) / 6.0);
} else if (i <= 12) {
// Arc around (r, r) from theta3 to its mirror across the diagonal, clockwise.
let theta = theta3 - (4.71238898 + 2.0 * theta3) * f32(i - 6) / 6.0;
return vec2<f32>(r, r) + r * vec2<f32>(cos(theta), sin(theta));
}
return bezier(p3.yx, vec2<f32>(0.0, p - a - b), vec2<f32>(0.0, p - a), vec2<f32>(0.0, p), f32(i - 12) / 6.0);
}
fn seg_dist(q: vec2<f32>, a: vec2<f32>, b: vec2<f32>) -> f32 {
let pa = q - a;
let ba = b - a;
let h = clamp(dot(pa, ba) / max(dot(ba, ba), 1e-6), 0.0, 1.0);
return length(pa - ba * h);
}
// Signed distance to a box with smoothed (continuous) corners, negative inside.
// Folded into one quadrant; the boundary there is: top edge, corner curve,
// right edge, walked clockwise so the inside is on the right of each segment.
// Segments of (almost) zero length are skipped: their cross product is pure
// float noise, and a neighbouring segment's endpoint yields the same distance.
struct Nearest { dist: f32, cross: f32 }
fn consider(q: vec2<f32>, a: vec2<f32>, b: vec2<f32>, n: Nearest) -> Nearest {
let ba = b - a;
if (dot(ba, ba) < 1e-4) {
return n;
}
let d = seg_dist(q, a, b);
if (d < n.dist) {
return Nearest(d, ba.x * (q - a).y - ba.y * (q - a).x);
}
return n;
}
fn sd_smooth_box(pos: vec2<f32>, half: vec2<f32>) -> f32 {
let q = abs(pos);
let p = u.corner_a.x;
var n = Nearest(1e9, 0.0);
var a = vec2<f32>(0.0, half.y);
var b = vec2<f32>(half.x - p, half.y);
n = consider(q, a, b, n);
a = b;
for (var i = 1; i <= CORNER_SEGMENTS; i++) {
let l = corner_point(i);
b = vec2<f32>(half.x - l.x, half.y - l.y);
n = consider(q, a, b, n);
a = b;
}
n = consider(q, a, vec2<f32>(half.x, 0.0), n);
return select(n.dist, -n.dist, n.cross < 0.0);
}
// One backdrop tap at a screen position (px). Beyond the image it fades to the
// fill colour over the blur radius, so a blurred image edge stays smooth.
fn tap(p: vec2<f32>, lod: f32) -> vec3<f32> {
let uv = (p - u.bd_min) / (u.bd_max - u.bd_min);
let edge = min(min(p.x - u.bd_min.x, u.bd_max.x - p.x), min(p.y - u.bd_min.y, u.bd_max.y - p.y));
let r = max(u.blur * 0.5, 0.5);
let inside = smoothstep(-r, r, edge);
return mix(u.fill.rgb, textureSampleLevel(bd_tex, bd_samp, uv, lod).rgb, inside);
}
fn sample_backdrop(p: vec2<f32>, lod: f32) -> vec3<f32> {
// 5-tap rotated cross on top of the mip level to hide box artifacts.
let s = max(u.blur, 0.0) * 0.5;
let c = tap(p, lod);
let a = tap(p + vec2<f32>( s, s * 0.5), lod);
let b = tap(p + vec2<f32>(-s, -s * 0.5), lod);
let d = tap(p + vec2<f32>( s * 0.5, -s), lod);
let e = tap(p + vec2<f32>(-s * 0.5, s), lod);
return (c * 2.0 + a + b + d + e) / 6.0;
}
fn srgb_to_linear(c: vec3<f32>) -> vec3<f32> {
let lo = c / 12.92;
let hi = pow((c + 0.055) / 1.055, vec3<f32>(2.4));
return select(hi, lo, c <= vec3<f32>(0.04045));
}
@fragment
fn fs_main(@builtin(position) frag: vec4<f32>) -> @location(0) vec4<f32> {
let p = frag.xy;
let center = (u.rect_min + u.rect_max) * 0.5;
let half = (u.rect_max - u.rect_min) * 0.5;
let d = sd_smooth_box(p - center, half);
let mask = 1.0 - smoothstep(-0.5, 0.5, d);
// Shadow (outside the shape only), offset downwards.
let sh_off = vec2<f32>(0.0, u.shadow_radius * 0.35);
let ds = sd_smooth_box(p - center - sh_off, half);
let sh_t = 1.0 - smoothstep(-u.shadow_radius * 0.25, u.shadow_radius, ds);
let shadow = u.shadow * sh_t * sh_t * (1.0 - mask); // quadratic tail: soft, mostly near the edge
if (mask <= 0.0 && shadow <= 0.0) {
discard;
}
// Outward normal via finite differences of the SDF.
let eps = 1.0;
let n = normalize(vec2<f32>(
sd_smooth_box(p - center + vec2<f32>(eps, 0.0), half) - sd_smooth_box(p - center - vec2<f32>(eps, 0.0), half),
sd_smooth_box(p - center + vec2<f32>(0.0, eps), half) - sd_smooth_box(p - center - vec2<f32>(0.0, eps), half),
) + vec2<f32>(1e-5, 0.0));
// Lens profile: 0 deep inside, 1 at the edge; rounded-glass falloff.
let t = clamp((d + u.edge_width) / max(u.edge_width, 1.0), 0.0, 1.0);
let lens = 1.0 - sqrt(max(1.0 - t * t, 0.0));
let disp = n * u.refraction * lens;
let lod = clamp(log2(max(u.blur * 0.5, 1.0)), 0.0, u.max_lod);
var col: vec3<f32>;
if (u.chroma > 0.001) {
let k = u.chroma * 0.5;
col = vec3<f32>(
sample_backdrop(p + disp * (1.0 + k), lod).r,
sample_backdrop(p + disp, lod).g,
sample_backdrop(p + disp * (1.0 - k), lod).b,
);
} else {
col = sample_backdrop(p + disp, lod);
}
// Vibrancy: saturation + brightness, then tint.
let lum = dot(col, vec3<f32>(0.2126, 0.7152, 0.0722));
col = mix(vec3<f32>(lum), col, u.saturation) * u.brightness;
col = mix(col, u.tint.rgb, u.tint.a);
// Specular rim: strong towards the light, faint counter-rim opposite.
let ndl = dot(n, u.light_dir);
let rim = pow(t, 5.0) * (max(ndl, 0.0) + 0.35 * max(-ndl, 0.0) + 0.08);
let sheen = 0.05 * (1.0 - clamp((p.y - u.rect_min.y) / max(half.y * 2.0, 1.0), 0.0, 1.0));
col += vec3<f32>(1.0) * u.specular * (rim + sheen);
// Thin inner border, brighter on the lit side.
let ring = 1.0 - smoothstep(0.0, 1.5, abs(d + 0.9));
let ring_a = u.border * ring * (0.25 + 0.55 * max(ndl, 0.0) + 0.15 * max(-ndl, 0.0));
col = mix(col, vec3<f32>(1.0), ring_a);
col = clamp(col, vec3<f32>(0.0), vec3<f32>(1.0));
if (u.srgb_out > 0.5) {
col = srgb_to_linear(col);
}
// Premultiplied output: glass over shadow.
let alpha = mask + shadow;
return vec4<f32>(col * mask, alpha);
}