use crate::{Color, GlassProfileCurve, GlassSurfaceProfile, RenderEffect, RuntimeShader};
pub const GLASS_SURFACE_ENABLED_UNIFORM: usize = 113;
pub const GLASS_SURFACE_RADIAL_POWER_UNIFORM: usize = 114;
pub const GLASS_SURFACE_X_COUNT_UNIFORM: usize = 115;
pub const GLASS_SURFACE_Y_COUNT_UNIFORM: usize = 116;
pub const GLASS_SURFACE_DEPTH_UNIFORM: usize = 117;
pub const GLASS_SURFACE_X_UNIFORM_BASE: usize = 120;
pub const GLASS_SURFACE_Y_UNIFORM_BASE: usize = 140;
pub const GLASS_SURFACE_AXIS_COUPLING_UNIFORM: usize = 158;
pub const LIQUID_GLASS_WGSL: &str = include_str!("../shaders/liquid_glass.wgsl");
#[derive(Clone, Debug, PartialEq)]
pub struct LiquidGlassSpec {
pub corner_radius: f32,
pub bezel_width: f32,
pub displacement_scale: f32,
pub refractive_index: f32,
pub surface_profile: GlassSurfaceProfile,
pub highlight: f32,
pub tilt_angle: f32,
pub tilt_pitch: f32,
pub saturation: f32,
pub chromatic_aberration: f32,
pub dispersion_axes: (f32, f32),
pub lift: f32,
pub contrast: f32,
pub dither: f32,
pub light_direction: (f32, f32),
}
impl Default for LiquidGlassSpec {
fn default() -> Self {
Self {
corner_radius: 28.0,
bezel_width: 14.0,
displacement_scale: 24.0,
refractive_index: 1.5,
surface_profile: GlassSurfaceProfile::regular(),
highlight: 0.7,
tilt_angle: 0.0,
tilt_pitch: 0.0,
saturation: 1.0,
chromatic_aberration: 0.0,
dispersion_axes: (1.0, 1.0),
lift: 0.0,
contrast: 1.0,
dither: 0.5,
light_direction: (0.0, 1.0),
}
}
}
#[derive(Clone, Debug)]
pub struct LiquidGlassRect {
pub left: f32,
pub top: f32,
pub width: f32,
pub height: f32,
pub tint_color: Color,
}
pub fn liquid_glass_effect(
rect: &LiquidGlassRect,
spec: &LiquidGlassSpec,
area_width: f32,
area_height: f32,
) -> RenderEffect {
let mut shader = RuntimeShader::new(LIQUID_GLASS_WGSL);
let cx = rect.left + rect.width * 0.5;
let cy = rect.top + rect.height * 0.5;
shader.set_float2(0, area_width, area_height); shader.set_float2(2, cx, cy); shader.set_float2(4, rect.width, rect.height); shader.set_float(6, spec.corner_radius);
shader.set_float(7, spec.bezel_width);
shader.set_float(8, spec.displacement_scale);
shader.set_float(9, spec.refractive_index);
shader.set_float(11, spec.highlight);
shader.set_float2(12, spec.tilt_angle, spec.tilt_pitch);
shader.set_float4(
14,
rect.tint_color.r(),
rect.tint_color.g(),
rect.tint_color.b(),
rect.tint_color.a(),
);
shader.set_float(18, spec.saturation);
shader.set_float(19, spec.chromatic_aberration);
shader.set_float2(118, spec.dispersion_axes.0, spec.dispersion_axes.1);
shader.set_float(20, spec.lift);
shader.set_float(21, spec.dither);
shader.set_float2(22, spec.light_direction.0, spec.light_direction.1);
shader.set_float(24, spec.contrast);
apply_glass_surface_profile(&mut shader, spec.surface_profile, 1.0);
shader.set_input_padding(liquid_glass_input_padding(spec, rect.width, rect.height));
RenderEffect::runtime_shader(shader)
}
fn liquid_glass_input_padding(spec: &LiquidGlassSpec, width: f32, height: f32) -> f32 {
let bend = 1.0 - 1.0 / spec.refractive_index.max(1.0001);
let tilt = (spec.tilt_angle * spec.tilt_angle + spec.tilt_pitch * spec.tilt_pitch).sqrt();
let half_extent = 0.5 * width.min(height).max(1.0);
let slope = glass_surface_max_slope(spec.surface_profile, half_extent);
let reach = slope + tilt;
let axis_scale = spec.dispersion_axes.0.max(spec.dispersion_axes.1);
let spread = 1.0 + axis_scale * spec.chromatic_aberration.max(0.0) * 0.5;
let displacement = reach * bend * spec.displacement_scale.max(0.0) * slope * spread;
if displacement > 0.0 {
displacement.ceil() + 2.0
} else {
0.0
}
}
pub fn glass_surface_max_slope(profile: GlassSurfaceProfile, half_extent: f32) -> f32 {
let x_slope = curve_max_abs_tangent(profile.x_profile());
let y_slope = curve_max_abs_tangent(profile.y_profile());
let blend_slope = profile.radial_power() * 0.5;
(x_slope.max(y_slope) + blend_slope) * profile.depth() / half_extent.max(1.0)
}
fn curve_max_abs_tangent(profile: GlassProfileCurve) -> f32 {
(0..=64)
.map(|step| profile.evaluate(step as f32 / 64.0).1.abs())
.fold(0.0, f32::max)
}
pub fn apply_glass_surface_profile(
shader: &mut RuntimeShader,
profile: GlassSurfaceProfile,
depth_scale: f32,
) {
let depth = profile.depth() * depth_scale.max(0.0);
shader.set_float(
GLASS_SURFACE_ENABLED_UNIFORM,
if depth > f32::EPSILON { 1.0 } else { 0.0 },
);
shader.set_float(GLASS_SURFACE_RADIAL_POWER_UNIFORM, profile.radial_power());
shader.set_float(
GLASS_SURFACE_X_COUNT_UNIFORM,
profile.x_profile().knots().len() as f32,
);
shader.set_float(
GLASS_SURFACE_Y_COUNT_UNIFORM,
profile.y_profile().knots().len() as f32,
);
shader.set_float(GLASS_SURFACE_DEPTH_UNIFORM, depth);
shader.set_float(GLASS_SURFACE_AXIS_COUPLING_UNIFORM, profile.axis_coupling());
pack_profile_curve(shader, GLASS_SURFACE_X_UNIFORM_BASE, profile.x_profile());
pack_profile_curve(shader, GLASS_SURFACE_Y_UNIFORM_BASE, profile.y_profile());
}
fn pack_profile_curve(shader: &mut RuntimeShader, base: usize, profile: GlassProfileCurve) {
for (index, knot) in profile.knots().iter().enumerate() {
let offset = base + index * 3;
shader.set_float(offset, knot.position());
shader.set_float(offset + 1, knot.height());
shader.set_float(offset + 2, knot.tangent());
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct LiquidLoupeSpec {
pub magnification: f32,
pub focus_offset: (f32, f32),
pub band_start: f32,
pub fold_peak: f32,
pub dispersion: f32,
pub seam_lift: f32,
pub highlight: f32,
pub progress: f32,
pub corner_radius: f32,
}
impl Default for LiquidLoupeSpec {
fn default() -> Self {
Self {
magnification: 1.25,
focus_offset: (0.0, 75.0),
band_start: 0.60,
fold_peak: 0.80,
dispersion: 0.20,
seam_lift: 26.0,
highlight: 6.2,
progress: 1.0,
corner_radius: 0.0,
}
}
}
pub fn liquid_loupe_effect(node_size: (f32, f32), spec: &LiquidLoupeSpec) -> RenderEffect {
let (w, h) = (node_size.0.max(1.0), node_size.1.max(1.0));
let alpha = spec.progress.clamp(0.0, 1.0);
let mut shader = RuntimeShader::new(LIQUID_GLASS_WGSL);
shader.set_float2(0, w, h); shader.set_float2(2, w * 0.5, h * 0.5); shader.set_float2(4, w, h);
if spec.corner_radius > 0.0 {
shader.set_float(6, spec.corner_radius.min(0.5 * h.min(w)));
} else {
shader.set_float(6, -1.0); }
shader.set_float(7, 0.5 * h.min(w)); shader.set_float(11, spec.highlight);
shader.set_float4(14, 1.0, 1.0, 1.0, 0.0); shader.set_float(18, 1.0); shader.set_float(20, 0.0); shader.set_float(21, 0.5); shader.set_float2(22, 0.0, 1.0);
shader.set_float(24, 1.0); shader.set_float(28, 1.0); shader.set_float(29, 0.45); shader.set_float(80, 1.0); shader.set_float2(81, spec.focus_offset.0, spec.focus_offset.1);
shader.set_float(83, spec.magnification);
shader.set_float(84, spec.band_start);
shader.set_float(85, spec.fold_peak);
shader.set_float(86, spec.dispersion);
shader.set_float(87, spec.seam_lift);
shader.set_float(90, alpha.max(1.0e-3));
shader.set_float2(118, 1.0, 1.0);
apply_glass_surface_profile(&mut shader, GlassSurfaceProfile::lens(), 1.0);
let r_in = 0.5 * w.min(h);
let focus_reach = (spec.focus_offset.0.powi(2) + spec.focus_offset.1.powi(2)).sqrt();
let fold_reach = (spec.fold_peak.max(1.0) - 1.0) * r_in;
shader.set_input_padding((focus_reach + fold_reach + 8.0).ceil());
RenderEffect::runtime_shader(shader)
}
pub fn liquid_menu_glass_effect(
node_size: (f32, f32),
blur_radius_px: f32,
progress: f32,
) -> RenderEffect {
let (w, h) = (node_size.0.max(1.0), node_size.1.max(1.0));
let p = progress.clamp(0.0, 1.0);
let mut shader = RuntimeShader::new(LIQUID_GLASS_WGSL);
shader.set_float2(0, w, h); shader.set_float2(2, w * 0.5, h * 0.5);
shader.set_float2(4, w, h);
shader.set_float(6, -1.0); shader.set_float(7, 8.0); shader.set_float(8, 2.5 * p); shader.set_float(9, 1.4);
shader.set_float(11, 0.19 * p); shader.set_float4(14, 0.0, 0.0, 0.0, 0.04 * p);
shader.set_float(18, 1.0 + 0.10 * p); shader.set_float(19, 0.0); shader.set_float(20, -0.06 * p);
shader.set_float(24, 1.0 + 0.05 * p); shader.set_float(88, 0.7); shader.set_float(89, 0.45); shader.set_float(21, 0.5);
shader.set_float2(22, 0.0, 1.0);
shader.set_float2(118, 1.0, 1.0);
apply_glass_surface_profile(
&mut shader,
GlassSurfaceProfile::regular()
.with_depth(2.4)
.expect("menu surface depth is valid"),
1.0,
);
shader.set_input_padding(12.0);
let lens = RenderEffect::runtime_shader(shader);
if blur_radius_px > 0.5 {
let radius = (blur_radius_px * (1.0 - p)).max(blur_radius_px * 0.08);
RenderEffect::blur(radius).then(lens)
} else {
lens
}
}
pub fn liquid_glass_effect_multi(
rects: &[LiquidGlassRect],
spec: &LiquidGlassSpec,
area_width: f32,
area_height: f32,
) -> Option<RenderEffect> {
let mut result: Option<RenderEffect> = None;
for rect in rects {
let effect = liquid_glass_effect(rect, spec, area_width, area_height);
result = Some(match result {
Some(existing) => existing.then(effect),
None => effect,
});
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn liquid_glass_spec_defaults() {
let spec = LiquidGlassSpec::default();
assert_eq!(spec.corner_radius, 28.0);
assert_eq!(spec.bezel_width, 14.0);
assert_eq!(spec.refractive_index, 1.5);
assert_eq!(spec.saturation, 1.0);
assert_eq!(spec.chromatic_aberration, 0.0);
assert_eq!(spec.lift, 0.0);
assert_eq!(spec.contrast, 1.0);
assert_eq!(spec.light_direction, (0.0, 1.0));
}
#[test]
fn physical_surface_blends_oval_and_biconic_axis_profiles() {
assert!(LIQUID_GLASS_WGSL.contains("fn sample_profile"));
assert!(LIQUID_GLASS_WGSL.contains("fn sample_glass_surface"));
assert!(LIQUID_GLASS_WGSL.contains("let y_weight = b / q"));
assert!(LIQUID_GLASS_WGSL.contains("let profile_delta = y_sample.height - x_sample.height"));
assert!(LIQUID_GLASS_WGSL.contains("profile_delta * dw_du"));
assert!(LIQUID_GLASS_WGSL.contains("profile_delta * dw_dv"));
assert!(LIQUID_GLASS_WGSL.contains("let axis_coupling = clamp(get_float(158u)"));
assert!(LIQUID_GLASS_WGSL.contains("let coupling_weight = axis_coupling;"));
assert!(LIQUID_GLASS_WGSL.contains("let toric_height ="));
assert!(
LIQUID_GLASS_WGSL.contains("let height = oval_height + height_delta * coupling_weight")
);
assert!(LIQUID_GLASS_WGSL.contains("(toric_dz_du - oval_dz_du) * coupling_weight"));
assert!(LIQUID_GLASS_WGSL.contains("(toric_dz_dv - oval_dz_dv) * coupling_weight"));
assert!(!LIQUID_GLASS_WGSL.contains("coupling_gradient"));
assert!(!LIQUID_GLASS_WGSL.contains("recessed_face_gradient"));
assert!(!LIQUID_GLASS_WGSL.contains("d_height_dx"));
}
#[test]
fn one_surface_normal_drives_refraction_dispersion_and_lighting() {
assert!(LIQUID_GLASS_WGSL.contains("let optical_gradient = surface_gradient + tilt * 0.18"));
assert!(LIQUID_GLASS_WGSL.contains("let surface_normal = normalize"));
assert!(
LIQUID_GLASS_WGSL.contains("base_displacement = -optical_gradient * bend * disp_scale")
);
assert!(LIQUID_GLASS_WGSL.contains("spectral_displacement = base_displacement"));
assert!(
LIQUID_GLASS_WGSL.contains("base_displacement + spectral_displacement * (scale - 1.0)")
);
assert!(LIQUID_GLASS_WGSL.contains("dot(surface_normal, half_direction)"));
assert!(!LIQUID_GLASS_WGSL.contains("inner_contour"));
assert!(!LIQUID_GLASS_WGSL.contains("magnify != 1.0"));
}
#[test]
fn returning_meniscus_uses_normal_driven_transmission_loss() {
assert!(LIQUID_GLASS_WGSL.contains("let return_transmission ="));
assert!(LIQUID_GLASS_WGSL.contains("return_transmission * mix(0.008, 0.11, rim_style)"));
assert!(!LIQUID_GLASS_WGSL.contains("painted_ridge"));
}
#[test]
fn content_recolor_detects_bounded_broad_glyphs_at_multiple_scales() {
assert!(LIQUID_GLASS_WGSL.contains("fn sampled_luma"));
assert!(LIQUID_GLASS_WGSL.contains("let detail_step = vec2<f32>(4.5 * s)"));
assert!(LIQUID_GLASS_WGSL.contains("let support_step = vec2<f32>(18.0 * s)"));
assert!(LIQUID_GLASS_WGSL.contains("let bounded_dark_region"));
assert!(
LIQUID_GLASS_WGSL.contains("max(edge_detail, max(dark_stroke, bounded_dark_region))")
);
assert!(
LIQUID_GLASS_WGSL.contains("let recolor_face = mix(1.0, 1.0 - smoothstep(0.82, 0.98")
);
assert!(!LIQUID_GLASS_WGSL.contains("let recolor_face = mix(1.0, surface_interior"));
let recolor = LIQUID_GLASS_WGSL
.find("if content_detail > 0.0")
.expect("content recolor stage");
let tone = LIQUID_GLASS_WGSL
.find("// Tone pipeline")
.expect("tone stage");
let frost = LIQUID_GLASS_WGSL
.find("// Adaptive frost")
.expect("adaptive frost stage");
assert!(recolor < tone && tone < frost);
}
#[test]
fn liquid_glass_effect_uniforms() {
let rect = LiquidGlassRect {
left: 100.0,
top: 50.0,
width: 200.0,
height: 100.0,
tint_color: Color(0.5, 0.5, 1.0, 0.1),
};
let spec = LiquidGlassSpec {
saturation: 1.6,
chromatic_aberration: 0.4,
dispersion_axes: (0.25, 1.75),
lift: 0.12,
contrast: 1.05,
dither: 1.0,
light_direction: (0.3, 0.7),
surface_profile: GlassSurfaceProfile::lens()
.with_axis_coupling(0.35)
.expect("axis coupling"),
..LiquidGlassSpec::default()
};
let effect = liquid_glass_effect(&rect, &spec, 800.0, 600.0);
let RenderEffect::Shader { shader } = effect else {
panic!("expected Shader effect");
};
let u = shader.uniforms();
assert_eq!(u[0], 800.0);
assert_eq!(u[1], 600.0);
assert_eq!(u[2], 200.0);
assert_eq!(u[3], 100.0);
assert_eq!(u[4], 200.0);
assert_eq!(u[5], 100.0);
assert_eq!(u[6], 28.0);
assert_eq!(u[18], 1.6);
assert_eq!(u[19], 0.4);
assert_eq!(u[20], 0.12);
assert_eq!(u[21], 1.0);
assert_eq!(u[22], 0.3);
assert_eq!(u[23], 0.7);
assert_eq!(u[24], 1.05);
assert_eq!(u[113], 1.0);
assert_eq!(u[114], spec.surface_profile.radial_power());
assert_eq!(
u[115],
spec.surface_profile.x_profile().knots().len() as f32
);
assert_eq!(
u[116],
spec.surface_profile.y_profile().knots().len() as f32
);
assert_eq!(u[117], spec.surface_profile.depth());
assert_eq!(&u[118..120], &[0.25, 1.75]);
assert_eq!(u[158], 0.35);
}
#[test]
fn liquid_glass_declares_padding_for_static_lensing() {
let spec = LiquidGlassSpec::default();
let effect = liquid_glass_effect(
&LiquidGlassRect {
left: 0.0,
top: 0.0,
width: 140.0,
height: 100.0,
tint_color: Color(0.5, 0.5, 1.0, 0.1),
},
&spec,
140.0,
100.0,
);
let RenderEffect::Shader { shader } = effect else {
panic!("expected Shader effect");
};
let bend = 1.0 - 1.0 / spec.refractive_index;
let slope = glass_surface_max_slope(spec.surface_profile, 50.0);
let min_padding = bend * spec.displacement_scale * slope;
assert!(
shader.input_padding() >= min_padding,
"static lensing must capture backdrop beyond the rect: {} < {min_padding}",
shader.input_padding()
);
}
#[test]
fn surface_slope_scales_with_depth_and_inverse_extent() {
let profile = GlassSurfaceProfile::lens();
let base = glass_surface_max_slope(profile, 20.0);
let deep = glass_surface_max_slope(
profile.with_depth(profile.depth() * 2.0).expect("depth"),
20.0,
);
let wide = glass_surface_max_slope(profile, 40.0);
assert!((deep / base - 2.0).abs() < 1.0e-5);
assert!((wide / base - 0.5).abs() < 1.0e-5);
}
#[test]
fn liquid_glass_padding_covers_max_shader_displacement() {
let spec = LiquidGlassSpec {
tilt_angle: 0.5,
tilt_pitch: 0.3,
chromatic_aberration: 0.6,
..LiquidGlassSpec::default()
};
let effect = liquid_glass_effect(
&LiquidGlassRect {
left: 0.0,
top: 0.0,
width: 140.0,
height: 100.0,
tint_color: Color(0.5, 0.5, 1.0, 0.1),
},
&spec,
140.0,
100.0,
);
let RenderEffect::Shader { shader } = effect else {
panic!("expected Shader effect");
};
let bend = 1.0 - 1.0 / spec.refractive_index.max(1.0001);
let tilt = (spec.tilt_angle * spec.tilt_angle + spec.tilt_pitch * spec.tilt_pitch).sqrt();
let slope = glass_surface_max_slope(spec.surface_profile, 50.0);
let spread = 1.0 + spec.chromatic_aberration * 0.5;
let max_displacement = (1.0 + tilt) * bend * spec.displacement_scale * slope * spread;
assert!(
shader.input_padding() >= max_displacement,
"backdrop capture must cover the largest refracted sample: {} < {max_displacement}",
shader.input_padding()
);
}
#[test]
fn liquid_glass_effect_multi_chains() {
let rects = vec![
LiquidGlassRect {
left: 10.0,
top: 10.0,
width: 100.0,
height: 100.0,
tint_color: Color(1.0, 0.0, 0.0, 0.1),
},
LiquidGlassRect {
left: 200.0,
top: 200.0,
width: 100.0,
height: 100.0,
tint_color: Color(0.0, 0.0, 1.0, 0.1),
},
];
let spec = LiquidGlassSpec::default();
let effect = liquid_glass_effect_multi(&rects, &spec, 800.0, 600.0);
assert!(effect.is_some());
assert!(matches!(effect.unwrap(), RenderEffect::Chain { .. }));
}
#[test]
fn liquid_glass_effect_multi_empty() {
let spec = LiquidGlassSpec::default();
let effect = liquid_glass_effect_multi(&[], &spec, 800.0, 600.0);
assert!(effect.is_none());
}
}