#version 460 core #include precision highp float; // The texture's size; the order of the rest is the order in which // _LiquidGlassLayer in lib/ui/liquid_glass.dart writes them. uniform vec2 uSize; // Glass coordinates, in dp from its top left, of FlutterFragCoord: an origin, // then a scale per unit. uniform vec4 uFrame; // Glass coordinates of the texture's pixels, as uFrame. uniform vec4 uSample; // The first and last pixel centers the pane may read, as a capture holds the // backdrops of other panes beside its own. uniform vec4 uClamp; // Dp per device pixel, and the frost sigma in its own pixels the texture // already holds, as a capture is blurred before any pane reads it. uniform vec2 uTexels; // As uSample and uClamp, for a capture blurred by the style's haze, and the // share of the backdrop seen through it. uniform vec4 uHazeSample; uniform vec4 uHazeClamp; uniform float uHaze; // Width, height, bevel width, frost sigma. uniform vec4 uShape; // Corner radii: top left, top right, bottom right, bottom left. uniform vec4 uCorners; // The superellipse corners the pane is clipped to, three to a corner in the // order of uCorners: n, J and the arc's radius for the octant along the top or // bottom, the same for the one along the side, then both arcs' centers. A // sharp corner has n 0. uniform vec4 uCurves[12]; // Refraction depth at the rim, saturation, brightness. A negative depth makes // a raised lens, which bends outward on UIKit's profile. uniform vec3 uOptics; // Straight alpha. uniform vec4 uTint; // Highlight along the top and bottom of the rim, the dp it fades over, how // much a lens's rounded ends darken. uniform vec3 uLight; // Glow center, glow strength, how far the tint thickens over content much // darker than it, or when negative much lighter. uniform vec4 uGlow; // How far the glow reaches, and the share of it spread over the whole pane. uniform vec2 uGlowSpread; // How much the dark line around the rim darkens the backdrop, how far apart in // dp the color channels part along the rim, and the frost sigma the rim adds // at its edge, easing off across the bevel. uniform vec3 uEdge; // The highlight's share in a faint tail kRimTail long, how far its fall // round rounded ends goes from the tab bar's to iOS 27 controls', and its // share added as light rather than screened; then the line at the sides. uniform vec4 uRim; // Above 0.5 the pane takes the outline of uProfile in place of its corners. uniform float uOutlined; // The outline's distance from the center along 64 even turns, the first // toward +x and the rest toward +y, four to a vector. uniform vec4 uProfile[16]; // A menu's highlight (LiquidGlassRim): light added at eight turns from the // vertical through the center, rising from 0 to a right angle, and across // bands inward from the rim, where each ends in dp and its share. uniform vec4 uShineTurns[2]; uniform vec4 uShineLit[2]; uniform vec4 uShineEnds[3]; uniform vec4 uShineShares[3]; uniform sampler2D uTexture; out vec4 fragColor; const vec3 kLuma = vec3(0.2126, 0.7152, 0.0722); const float kTau = 6.28318530718; const float kTurns = 64.0; // Fewer taps leave a swirl over sharp text; 64 leave a faint one at 3x, too // fine to see. A count taken from a uniform costs as much as 96 constant taps, // so a narrower frost takes a constant count of its own, its taps no further // apart than 64 leave those of the widest. const int kFrostTaps = 64; const float kGoldenAngle = 2.39996322973; // 1.2 times the engine's kernel reach of (sigma - 0.5) * sqrt(3) pixels, which // on a Vogel disc fits its blur best. const float kFrostReach = 2.07846096908; // Measured on iOS 27: the width of the dark line in dp and the most it takes // off a channel of the backdrop. const float kLine = 0.67; const float kLineCap = 0.298; // The most the engine's superellipse corner lies inside a circular one, as a // share of the radius, with room to spare over the 1.8% it reaches. const float kCornerGap = 0.03; // How far in dp a lens's ends darken in from the rim. const float kEndShade = 3.0; // How far in dp from the rim the color channels part, and where they start // to close: iOS parts them nearly as far across a lens's band as at its rim. const float kPartReach = 10.0; const float kPartHold = 7.0; // Measured on iOS 27 controls: how far in dp the highlight's tail reaches. const float kRimTail = 4.5; // A frost this wide in dp reads the backdrop's luma once, as a haze does. const float kSteadyFrost = 8.0; vec4 texel(vec2 pixel) { return texture( uTexture, clamp(pixel, uClamp.xy, min(uClamp.zw, uSize - 0.5)) / uSize ); } vec4 sampleGlass(vec2 point) { return texel((point - uSample.xy) / uSample.zw); } vec4 sampleHaze(vec2 point) { vec2 pixel = (point - uHazeSample.xy) / uHazeSample.zw; return texture( uTexture, clamp(pixel, uHazeClamp.xy, min(uHazeClamp.zw, uSize - 0.5)) / uSize ); } #define FROST_DISC(taps) \ float decay = exp(-falloff / float(taps)); \ float weight = sqrt(decay); \ vec2 turn = vec2(cos(kGoldenAngle), sin(kGoldenAngle)); \ vec2 ray = vec2(1.0, 0.0); \ vec4 sum = vec4(0.0); \ float total = 0.0; \ for (int i = 0; i < taps; i++) { \ float spread = sqrt((float(i) + 0.5) / float(taps)); \ sum += texel(center + ray * reach * spread) * weight; \ total += weight; \ weight *= decay; \ ray = vec2(ray.x * turn.x - ray.y * turn.y, ray.x * turn.y + ray.y * turn.x); \ } \ return sum / total; vec4 frostDisc16(vec2 center, vec2 reach, float falloff) { FROST_DISC(16) } vec4 frostDisc32(vec2 center, vec2 reach, float falloff) { FROST_DISC(32) } vec4 frostDisc64(vec2 center, vec2 reach, float falloff) { FROST_DISC(kFrostTaps) } // The engine's Gaussian of a frost sigma in device pixels, past what the // texture holds, read on a Vogel disc. vec4 sampleFrosted(vec2 point, float frost) { vec2 sigma = frost / uSample.zw; sigma = sqrt(max(sigma * sigma - uTexels.y * uTexels.y, vec2(0.0))); vec2 reach = max(sigma - 0.5, 0.0) * kFrostReach; if (reach.x <= 0.0 || reach.y <= 0.0) { return sampleGlass(point); } vec2 ratio = reach / sigma; float falloff = 0.5 * ratio.x * ratio.y; vec2 center = (point - uSample.xy) / uSample.zw; float widest = max(sigma.x, sigma.y); if (widest <= 2.0) { return frostDisc16(center, reach, falloff); } if (widest <= 3.5) { return frostDisc32(center, reach, falloff); } return frostDisc64(center, reach, falloff); } // Its taps sit a frost sigma apart, so that a glyph passing under one point // does not flicker the tint; a haze or a wide frost is blurred enough to read // once. vec4 sampleBehind(vec2 point) { if (uHaze > 0.0) { return sampleHaze(point); } if (uShape.w >= kSteadyFrost) { return sampleGlass(point); } vec2 spread = vec2(uShape.w, -uShape.w); return 0.25 * (sampleGlass(point + spread.xx) + sampleGlass(point + spread.yy) + sampleGlass(point + spread.xy) + sampleGlass(point + spread.yx)); } float roundedBoxDistance(vec2 p, vec2 halfSize, float radius) { vec2 q = abs(p) - halfSize + radius; return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - radius; } vec2 roundedBoxNormal(vec2 p, vec2 halfSize, float radius) { vec2 q = abs(p) - halfSize + radius; vec2 n = q.x > 0.0 && q.y > 0.0 ? normalize(q) : (q.x > q.y ? vec2(1.0, 0.0) : vec2(0.0, 1.0)); return n * vec2(p.x < 0.0 ? -1.0 : 1.0, p.y < 0.0 ? -1.0 : 1.0); } // How far inside one octant of the engine's rounded superellipse a point lies, // in the octant's frame (impeller/geometry/round_superellipse_param.cc): a // superellipse of half side a up to J, then a circular arc. Close to exact // only within a few dp of the curve. float octantInset(vec2 point, float a, vec4 curve, vec2 center) { vec2 fromCenter = point - center; vec2 toJoint = curve.yz - center; if (toJoint.x * fromCenter.y < toJoint.y * fromCenter.x) { return curve.w - length(fromCenter); } float n = curve.x; vec2 s = max(point, 0.0) / a; float major = max(max(s.x, s.y), 1e-4); float norm = major * pow(1.0 + pow(min(s.x, s.y) / major, n), 1.0 / n); vec2 slope = pow(s / norm, vec2(n - 1.0)); return a * (1.0 - norm) / max(length(slope), 1e-4); } // A corner's quadrant lies about the pane's center, as the engine lays it when // all corners are alike, and its octants part along the diagonal through where // their arcs meet, half the difference of the sides off center. float cornerInset(vec2 p, vec2 halfSize) { vec2 side = step(0.0, p); vec4 along = mix( mix(uCurves[0], uCurves[3], side.x), mix(uCurves[9], uCurves[6], side.x), side.y ); vec4 across = mix( mix(uCurves[1], uCurves[4], side.x), mix(uCurves[10], uCurves[7], side.x), side.y ); vec4 centers = mix( mix(uCurves[2], uCurves[5], side.x), mix(uCurves[11], uCurves[8], side.x), side.y ); vec2 q = abs(p); if (along.x < 2.0) { return min(halfSize.x - q.x, halfSize.y - q.y); } float c = halfSize.x - halfSize.y; if (q.y + c >= q.x) { return octantInset(vec2(q.x, q.y + c), halfSize.x, along, centers.xy); } return octantInset(vec2(q.y, q.x - c), halfSize.y, across, centers.zw); } // Adds one sample of the profile to the radius at turn u and its slope, by // the Catmull-Rom (Keys) kernel, which crosses the samples smoothly. void addTurn(float u, float index, float radius, inout vec2 sum) { float x = u - index; x -= kTurns * floor(x / kTurns + 0.5); float d = abs(x); if (d >= 2.0) { return; } vec2 weight = d < 1.0 ? vec2((1.5 * d - 2.5) * d * d + 1.0, (4.5 * d - 5.0) * d) : vec2(((-0.5 * d + 2.5) * d - 4.0) * d + 2.0, (-1.5 * d + 5.0) * d - 4.0); sum += radius * vec2(weight.x, weight.y * sign(x)); } // The distance inward from a star-shaped outline and its outward normal, // both taken at the outline where the point's direction from the center // meets it, so they hold steady all the way in. void outlineGeometry(vec2 p, out float inset, out vec2 normal) { float len = max(length(p), 1e-3); float turn = atan(p.y, p.x) / kTau; float u = (turn - floor(turn)) * kTurns; vec2 sum = vec2(0.0); for (int i = 0; i < 16; i++) { vec4 radii = uProfile[i]; float index = float(i) * 4.0; addTurn(u, index, radii.x, sum); addTurn(u, index + 1.0, radii.y, sum); addTurn(u, index + 2.0, radii.z, sum); addTurn(u, index + 3.0, radii.w, sum); } float reach = max(sum.x, 1e-3); float slope = sum.y * kTurns / kTau / reach; vec2 radial = p / len; vec2 gradient = radial - slope * vec2(-radial.y, radial.x); float stretch = length(gradient); inset = max((reach - len) / stretch, 0.0); normal = gradient / stretch; } // The highlight's bands over depths a to b in from the rim, on average. float shineAcross(float a, float b) { float sum = 0.0; float start = 0.0; for (int i = 0; i < 3; i++) { vec4 ends = uShineEnds[i]; vec4 starts = vec4(start, ends.xyz); sum += dot( uShineShares[i], max(min(vec4(b), ends) - max(vec4(a), starts), 0.0) ); start = ends.w; } return sum / max(b - a, 1e-4); } // The highlight's brightness a turn from the vertical, as the sweep the // fallback draws it with lays it out. float shineAround(float turn) { vec4 startTurns = uShineTurns[0]; vec4 lastTurns = uShineTurns[1]; vec4 startLit = uShineLit[0]; vec4 lastLit = uShineLit[1]; vec4 firstEnds = vec4(startTurns.yzw, lastTurns.x); vec3 lastEnds = lastTurns.yzw; vec4 firstRise = vec4(startLit.yzw, lastLit.x) - startLit; vec3 lastRise = lastLit.yzw - lastLit.xyz; return startLit.x + dot(firstRise, clamp( (turn - startTurns) / max(firstEnds - startTurns, 1e-5), 0.0, 1.0)) + dot(lastRise, clamp( (turn - lastTurns.xyz) / max(lastEnds - lastTurns.xyz, 1e-5), 0.0, 1.0 )); } void main() { vec2 point = uFrame.xy + FlutterFragCoord().xy * uFrame.zw; vec2 halfSize = uShape.xy * 0.5; float halfMinor = min(halfSize.x, halfSize.y); vec2 centered = point - halfSize; float inset; vec2 normal; // The inset of the line and the highlight. A pane is clipped to the engine's // superellipse, up to half a dp inside its circular corners where a long side // runs into them, and they keep to it; the optics read deeper than its inset // holds true, so they keep the circles. Only the band the line and the // highlight can reach works it out. float edge; if (uOutlined > 0.5) { outlineGeometry(centered, inset, normal); edge = inset; } else { vec2 side = step(0.0, centered); float radius = min( mix( mix(uCorners.x, uCorners.y, side.x), mix(uCorners.w, uCorners.z, side.x), side.y ), halfMinor ); inset = max(-roundedBoxDistance(centered, halfSize, radius), 0.0); normal = roundedBoxNormal(centered, halfSize, radius); edge = inset; bool lit = uEdge.x > 0.0 || uRim.w > 0.0 || uLight.x > 0.0; float traced = max( lit ? kLine + uLight.y : 0.0, uShineLit[0].x > 0.0 ? uShineEnds[2].w : 0.0 ); if (traced > 0.0 && inset < traced + uTexels.x + kCornerGap * radius) { edge = max(cornerInset(centered, halfSize), 0.0); } } bool lens = uOptics.x < 0.0; float bevel = max(min(uShape.z, halfMinor * 0.5), 1e-3); float rise = 1.0 - clamp(inset / bevel, 0.0, 1.0); vec2 shift; if (lens) { // As measured on iOS 27, the raised lens reads outward along its normal // all around, by nothing at the rim and its full depth a bevel in, so its // outer band draws out what lies right under the rim; the depth then eases // off toward the middle. shift = normal * -uOptics.x * smoothstep(0.0, bevel, inset) * (1.0 - smoothstep(bevel, halfMinor, inset)); } else { // A bevel of circular profile, fitted to small panes: flat where it meets // the face and steepening toward the rim, which reads from so far inside // that its outermost band shows the content squeezed and mirrored. float depth = min(uOptics.x, halfMinor) * (1.0 - sqrt(1.0 - rise * rise)); shift = -normal * depth; } float frost = uShape.w + uEdge.z * rise * rise; vec4 base = sampleFrosted(point + shift, frost); if (uEdge.y > 0.0 && inset < kPartReach) { // iOS 27 parts red from blue along the normal's mirror image, across it // at the sides and along it at the corners. Along the top and bottom the // parting narrows from the rim to what it holds around the ends. Over // clear glass a channel shows past the others, so the cover is the widest // of them. vec2 part = normal.yx * 0.5 * uEdge.y * (0.6 + 0.4 * abs(normal.y) * max(1.0 - inset / kPartHold, 0.0)) * (1.0 - smoothstep(kPartHold, kPartReach, inset)); vec4 red = sampleFrosted(point + shift + part, frost); vec4 blue = sampleFrosted(point + shift - part, frost); base = vec4(red.r, base.g, blue.b, max(base.a, max(red.a, blue.a))); } if (uHaze > 0.0) { base = mix(base, sampleHaze(point + shift), uHaze); } float alpha = base.a; vec3 color = base.rgb / max(alpha, 1e-4); color = mix(vec3(dot(color, kLuma)), color, uOptics.y) * uOptics.z; color *= 1.0 - uLight.z * abs(normal.x) * exp(-inset / kEndShade); float clash = 0.0; if (uGlow.w != 0.0) { vec4 behind = sampleBehind(halfSize) + sampleBehind(halfSize * vec2(0.4, 0.6)) + sampleBehind(halfSize * vec2(1.6, 0.6)) + sampleBehind(halfSize * vec2(0.4, 1.4)) + sampleBehind(halfSize * vec2(1.6, 1.4)); float behindLuma = dot(behind.rgb / max(behind.a, 1e-4), kLuma); clash = clamp( (sign(uGlow.w) * (dot(uTint.rgb, kLuma) - behindLuma) - 0.3) / 0.5, 0.0, 1.0 ); } float cover = uTint.a + (1.0 - uTint.a) * clash * abs(uGlow.w); float coverage = alpha * (1.0 - cover) + cover; vec3 glass = color * alpha * (1.0 - cover) + uTint.rgb * cover; // iOS lights the top and bottom of its glass, just inside the line, and on // its tab bar fades that around the rounded ends with the normal's upward // part squared, out only at their tips; its controls dim far faster. All // screened toward white, a pale tint would clip into a flat white ring. // The tail reads the circular inset, as it reaches past where the edge's is // worked out, and moves too slowly for the difference to show. float within = max(edge - kLine, 0.0); float facing; if (lens) { facing = smoothstep(0.65, 0.95, abs(normal.y)); } else { float rise2 = normal.y * normal.y; float rise4 = rise2 * rise2; float rise16 = rise4 * rise4 * rise4 * rise4; facing = mix(rise2, 0.5 * rise2 * abs(normal.y) * (1.0 + rise16), uRim.y); } float band = clamp(1.0 - within / max(uLight.y, 1e-3), 0.0, 1.0); float tail = clamp(1.0 - max(inset - kLine, 0.0) / kRimTail, 0.0, 1.0); float shine = min(uLight.x * facing * mix(band, tail, uRim.x), 1.0); glass += (vec3(coverage) - (1.0 - uRim.z) * glass) * shine; // iOS 27 outlines its glass with a dark line, which UIKit draws just outside // the bounds, over the backdrop, where the pane cannot draw, so it takes the // outermost pixels here. iOS draws it darkest at the sides; the dock keeps // it as faint all around as along the top, as its darker sides read as // brackets. float line = clamp((kLine - edge) / uTexels.x + 0.5, 0.0, 1.0); float across = abs(normal.x); float darken = mix(uEdge.x, uRim.w, across * across * across); if ((uEdge.x > 0.0 || uRim.w > 0.0) && line > 0.0) { vec4 under = sampleGlass(point); vec3 dark = max(under.rgb - kLineCap * under.a, 0.22 * under.rgb); glass = mix(glass, mix(under.rgb, dark, darken), line); coverage = mix(coverage, under.a, line); } vec2 toGlow = point - uGlow.xy; float reach = max(uGlowSpread.x * uGlowSpread.x, 1.0); glass += uGlow.z * coverage * mix(exp(-dot(toGlow, toGlow) / reach), 1.0, uGlowSpread.y); // A pixel the rim crosses takes the bands it covers, as the rasterizer // weighs the rest by its coverage. if (uShineLit[0].x > 0.0 && edge < uShineEnds[2].w + uTexels.x) { float turn = atan(abs(centered.x), abs(centered.y)); float pixel = 0.5 * uTexels.x; glass += coverage * shineAround(turn) * shineAcross(max(edge - pixel, 0.0), edge + pixel); } fragColor = vec4(min(glass, vec3(coverage)), coverage); }