// 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
shadow_geo: vec4<f32>, // offset (down), spread
};
@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);
}
// Smooth outward normal of a rounded box whose corner radius matches the smoothed
// corner span. Used for the lens direction and lighting: the polyline SDF's finite
// difference normal is piecewise constant and would show as spokes in the refraction.
fn rounded_normal(pos: vec2<f32>, half: vec2<f32>, r: f32) -> vec2<f32> {
let q = abs(pos) - (half - vec2<f32>(r));
var n: vec2<f32>;
if (q.x > 0.0 && q.y > 0.0) {
n = normalize(q + vec2<f32>(1e-5, 0.0));
} else if (q.x > q.y) {
n = vec2<f32>(1.0, 0.0);
} else {
n = vec2<f32>(0.0, 1.0);
}
let sgn = select(vec2<f32>(-1.0), vec2<f32>(1.0), pos >= vec2<f32>(0.0));
return n * sgn;
}
// 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. A
// transparent fill means "nothing is outside" (the backdrop is the whole
// screen): clamp to the edge instead of fading.
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 = select(smoothstep(-r, r, edge), 1.0, u.fill.a < 0.001);
return mix(u.fill.rgb, textureSampleLevel(bd_tex, bd_samp, uv, lod).rgb, inside);
}
fn sample_backdrop(p: vec2<f32>, lod: f32) -> vec3<f32> {
// Centre tap plus an 8-tap ring at half the blur radius on top of the
// (already smooth) pyramid level: reads as a Gaussian without banding.
let s = max(u.blur, 0.0) * 0.5;
var sum = tap(p, lod) * 2.0;
for (var i = 0; i < 8; i++) {
let a = f32(i) * 0.7853982 + 0.3927;
sum += tap(p + s * vec2<f32>(cos(a), sin(a)), lod);
}
return sum / 10.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): the shape pushed down and grown by the spread.
let sh_off = vec2<f32>(0.0, u.shadow_geo.x);
let ds = sd_smooth_box(p - center - sh_off, half) - u.shadow_geo.y;
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: analytic and smooth (see rounded_normal).
let n = rounded_normal(p - center, half, min(u.corner_a.x, min(half.x, half.y)));
// 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: a thin bright crescent towards the light and a second, softer
// one opposite (light bouncing inside the slab), as on a polished glass edge.
let ndl = dot(n, u.light_dir);
// Highlights gather where the edge curves (corners, capsule ends); straight edges
// only get a faint line, as on a polished slab lit by a broad source.
let curved = 4.0 * n.x * n.x * n.y * n.y;
// Thin: the plate is flat, only its bevelled edge catches light. No top-down sheen,
// that made the surface read as a dome.
let rim = pow(t, 10.0) * (0.3 + 0.7 * curved) * (1.0 * max(ndl, 0.0) + 0.6 * max(-ndl, 0.0));
col += vec3<f32>(1.0) * u.specular * rim;
// A faint dark contour at the very edge reads as the plate's thickness and is what
// separates glass from a white page (iOS shows no drop shadow there); a bit
// stronger on the unlit side.
col *= 1.0 - 0.6 * u.border * pow(t, 8.0) * (0.6 + 0.4 * (1.0 - max(ndl, 0.0)));
// Hairline inner border, a little brighter on the lit side.
let ring = 1.0 - smoothstep(0.0, 1.2, abs(d + 0.7));
let ring_a = u.border * ring * (0.32 + 0.4 * max(ndl, 0.0) + 0.18 * 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);
}