毛玻璃与液态玻璃是怎么做出来的

毛玻璃和 Liquid Glass 不是同一个东西,也不是改了名。前者靠模糊「抹平」背景,后者靠 feDisplacementMap「弯折」背景。这篇从最小实现讲到两类折射方案,每种都有可以拖动、可以调参的现场演示。

这两个词经常被当成同义词用,列出对比表

毛玻璃液态玻璃
英文Frosted Glass / GlassmorphismLiquid Glass
出处iOS 7(2013)WWDC 2025
做什么抹平背景:模糊掉细节,只留颜色弯折背景:细节还在,在边缘被掰弯
CSSbackdrop-filter: blur()backdrop-filter: url(#svg滤镜)
代价便宜贵,且只有 Chromium 认

液态玻璃不是毛玻璃的新叫法,是另一种材质。毛玻璃也没被废弃——大多数场景它依然是更合适的那个。

一、毛玻璃

最小实现

只要两条 CSS:半透明底色 + 背景模糊

css
.card {
  background: rgba(255, 255, 255, 0.7);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
}

backdrop-filter 处理的是元素背后那一层,元素自身的内容不受影响——所以卡片里的字始终清晰,只有透过来的背景被糊掉。

网上绝大多数教程到这里就结束了。但这样做出来的东西,离「材质感」还差三步。

第一步:saturate

这是基础教程里几乎都缺的一项,也是差别最大的一项。模糊的本质是把邻近像素做加权平均。颜色一平均就会互相抵消,于是背景透过来时饱和度被冲淡——玻璃发灰、发闷,像蒙了一层雾,而不像玻璃。

补回来的办法是在模糊后面串一个 saturate()

css
backdrop-filter: blur(20px) saturate(180%);

下面这块可以拖动。把「饱和度」从 180 拉到 100,就是「有没有这一步」的差别:

拖我

拉到 100 时它是一块灰扑扑的塑料板;拉回 180,背后的颜色重新透出来,才像玻璃。

差的第二步:不透明度和模糊要配着调

教程里常见的 rgba(255,255,255,.7) + blur(10px),两个值都偏保守:70% 的白已经基本不透,再加模糊,结果更接近「一张白卡片」而不是玻璃。

这两个值是此消彼长的:

底色浓度低底色浓度高
模糊小太透,背后花,字读不了像一层薄膜
模糊大玻璃感最好白卡片,玻璃感尽失

上面那个演示里把「底色浓度」拉到 20、「模糊」拉到 30 试试——那才是常见的玻璃观感。真要落地时再往回收一点,保证文字对比度。

差的第三步:高光边

玻璃是有厚度的,边缘会有一道极细的反光。少了它,卡片看着像贴纸。

css
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.75);

后面讲液态玻璃时会看到,这道高光还能做得更讲究——左上和右下强度不同,光才有方向。

评价

如果你要的只是「透而不糊、内容能读」,毛玻璃就够了。

它便宜、兼容性好(全球支持率 95.69%)、没有降级噩梦。下面那套折射方案贵得多。

二、液态玻璃 = 折射

为什么 backdrop-filter 本身做不到

blursaturatebrightness 这些都是逐像素处理:每个像素只看自己,算出一个新颜色。

而折射是另一回事——它要回答「这个位置该显示的,其实是背后别处的像素」。这叫位移采样,普通的 backdrop-filter 函数做不到。

能做到的是 SVG 滤镜里的 feDisplacementMap

先搞清楚:它同时吃两张图

这是理解全篇的地基,绕过去后面全看不懂:

是什么谁来当
in要被弄弯的画面背景。所有实现都一样
in2「往哪弯、弯多少」的说明书各家实现的区别全在这一张

第二张就是所谓的位移图。它是一张和元素等大的真图片,但不给人看——是给滤镜查表用的。每个像素的颜色就是一条指令:

(128,128)  →  原地不动
(255,128)  →  往右偏最大值
(0,128)    →  往左偏最大值

R 通道管横向、G 通道管纵向(也可以指定别的通道),128 是「不动」的中点,偏多远由 scale 决定。

滤镜工作时逐像素查这张表:「我在 (x,y),查到说偏 10px,那我就去背景的 (x+10,y) 把颜色取过来。」

所以后面说的「造图」,造的是第二张——那张说明书,不是背景

有一点容易绕:取样方向和看到的方向是相反的。 让某个位置去右下取样,等于把右下的内容搬到这儿来,看起来就是画面整体往左上挪了。

它有五个属性,各家实现的差别几乎全在这五个上:

属性作用默认值
in被位移的图源上一个滤镜的结果,没有则 SourceGraphic
in2当位移图用的图源同上
scale位移的最大像素数0(不位移)
xChannelSelectorin2 的哪个通道当 X 位移A
yChannelSelector用哪个通道当 Y 位移A

注意后两个的默认值是 A(alpha),不是 R/G 这条看着不起眼,下一节就靠它。

再把 backdrop-filter 指向这个滤镜,折射就作用在背景上:

css
.glass { backdrop-filter: url(#my-filter); }

试一下:不给位移图会怎样

既然所有参数都有默认值,那一个都不写能跑吗?

html
<filter id="bare" filterUnits="objectBoundingBox">
  <feDisplacementMap scale="150" />
</filter>

能跑,但结果值得看一眼——inin2 双双落到背景本身,两个方向又都读默认的 A 通道。而背景是全不透明的,alpha 处处相同。于是每个像素算出来的位移完全一样:

拖我

拖动「位移量」一下就明白了:拖动的元素内画面朝左上方向斜着平移,仅此而已。

它不是透镜。方向锁死在一个 45° 的对角(横竖读同一个通道、拿到同一个数,位移量必然相等),距离处处相同(它压根不知道边缘在哪儿)。放大位移量只会让整块挪得更远,不会产生任何弯曲。

这个演示的意义是划清界限feDisplacementMap 本身不提供任何「玻璃感」,它只是个查表搬运工。想让位移随位置变化——中心不动、边缘弯——那张说明书就必须逐像素都不同

两类实现

一、造一张位移图

想让折射「准」,就得自己画那张说明书:每个像素明确写上该往哪偏、偏多少。

「造图」具体就是这四步,全在 JS 里做:

  1. 建一个和元素等大的 <canvas>
  2. 逐像素算「这里该偏多少」,把结果写成 R/G 通道的颜色值
  3. canvas.toDataURL() → 得到一个 PNG 的 data URL
  4. 把它塞进 <feImage href="…">

注意这四步不是每帧都跑——只在组件挂载、尺寸变化、参数调整时跑一次。之后浏览器每帧做的是「拿着这张现成的说明书去查表采样」。这两件事发生在完全不同的时刻,后面算性能账时要分开看。

最顺的写法是把它当成 fragment shader——每个像素回答一个问题:「我该从哪儿取样?」

js
// 到圆角矩形边缘的有符号距离
const d = roundRectSDF(ix, iy, hw, hh, radius);
// 越靠近边缘,t 越接近 1
const t = smoothStep(0, soft, d + inset);
// 坐标向中心收缩 —— 这一步就是透镜
const k = 1 - t * 0.14;
return { x: ix * k, y: iy * k };

中心 k = 1,原样取样,所以中间清晰;边缘 k < 1,从更靠内的位置取样,于是产生放大和畸变。

smoothStep 那两个端点的顺序极易写反,而且反了不报错、看着也还是「玻璃」——只是效果整个里外翻面:中间那片字被拉弯,最外一圈反而纹丝不动。判断方法不用看代码,看结果就行:彩边和畸变出现在哪儿,位移就在哪儿。 出现在字上就是写反了。

然后编码成图片。这里有个容易做反的地方

js
// ① 先跑一遍,记下真实的最大位移
let maxScale = 0;
for (…) { maxScale = Math.max(maxScale, Math.abs(dx), Math.abs(dy)); }
maxScale *= 0.5;

// ② 用它归一化,把 0–255 用满
R = dx / maxScale + 0.5;
G = dy / maxScale + 0.5;

// ③ 最后把这个值写回滤镜
feDisplacementMap.setAttribute('scale', maxScale);

如果反过来——先定死一个 scale,再算位移幅度——当实际位移远小于 scale 时,位移图只用到 0–255 中间很窄的一段,量化台阶会肉眼可见,边缘出现一圈一圈的色阶。

拖我

「折射范围」控制弯光的宽度,「过渡柔和」控制从弯到平的过渡有多陡。中间那块字始终清晰——这就是它和上一节那个均匀平移的根本差别:位移随位置变化了。

代价是尺寸一变就要重算位移图

这两个滑块到底在雕什么

上面那张卡片只能看到结果。把玻璃沿短边剖开、再把光路画出来,两个参数各管什么就一目了然了:

↑ 眼睛在这一侧。光线从下方背景出发,穿过玻璃上来。折射范围背景(被玻璃盖住的那一层)这一圈背景永远看不到这一段被画了两遍 → 重影
边缘弯曲强度 t = 1.000外圈看不到的背景 14.0%重影 0.87%柔和调到 13 以上才不重影

先看形状。折射范围是斜面有多宽——从边缘往里吃掉多少,是一道硬边界。过渡柔和是内侧那个肩磨掉多少——它只磨内侧,卡片真正的边缘那一头永远是最陡的,这也符合直觉:真玻璃就该在最边上弯得最厉害。

注意两件容易搞反的事:

一、柔和不会加宽斜面,它是往里啃sf 从折射范围的内边界往外量,调大柔和是在吃掉「弯到满」的那段平台。啃过头就会出现下面这种情况:

二、柔和 > 2 × 折射范围时,肩还没磨完就顶到边了,边缘根本弯不满。此时最外缘的 t 不到 1,整条斜面被整体压低。把柔和拉到 99,你会看到光线几乎不再偏折,红色那条「看不到的背景」也跟着缩短——折射被削弱了,不是被移走了

再看光路。等间距射出的光线,落到背景上就不再等间距了:靠边缘那几根挤成一堆,这就是放大。挤没的部分是红色那条——玻璃边缘把最外圈的背景吞掉了,那圈内容在屏幕上不存在。

过渡柔和有个功能下限,不只是审美

把柔和拖到很小,会看到光线开始交叉

交叉意味着采样映射不再单调:同一段背景被取样了两次,于是在屏幕上被画了两遍。那不是模糊也不是错位,是一道复制出来的接缝——图里那条琥珀色的带子。

原因不难理解:t 在肩部上升得越快,取样坐标 x·k 往回缩得就越猛,猛到超过 x 自身的增长时,映射就掉头了。所以柔和有一个必须达到的下限

过渡柔和的实际宽度要大于「中心平坦区半宽」的约 1/4。 换算到滑块上是 柔和 ≳ 25 − 折射范围 ÷ 2

折射范围515253545
不重影所需的最小柔和22.518.51383

图里的读数是实时搜出来的,不是套这条近似公式,拖动时可以直接看当前配置够不够。

这道接缝在纯色背景上几乎看不出来,压到文字上才现形——所以调参时一定要拿正文当底衬,这也是本文所有演示都压着一段文字的原因。

二、同一张图跑三遍(色散)

真玻璃不同波长的光折射率不同,所以边缘会泛出极淡的彩边。做法是拿上面那张位移图跑三遍scale 依次差一点点,每遍只留一个颜色通道,最后合成:

html
<feDisplacementMap in="SourceGraphic" in2="map" scale="31" result="dR" />
<feColorMatrix in="dR" values="1 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 1 0" result="cR" />

<feDisplacementMap in="SourceGraphic" in2="map" scale="30" result="dG" />
<feColorMatrix in="dG" values="0 0 0 0 0  0 1 0 0 0  0 0 0 0 0  0 0 0 1 0" result="cG" />

<feDisplacementMap in="SourceGraphic" in2="map" scale="29" result="dB" />
<feColorMatrix in="dB" values="0 0 0 0 0  0 0 0 0 0  0 0 1 0 0  0 0 0 1 0" result="cB" />

<feBlend in="cR" in2="cG" mode="screen" result="rg" />
<feBlend in="rg" in2="cB" mode="screen" />
拖我

把「色散」从 0 拉到 40,边缘会渐渐出现红青分离。过头会显得廉价——真实玻璃的色散很含蓄,10 左右就够。

滤镜开销大约是单张位移图的三倍。

比算法更要紧的一件事:只让边缘弯

早期几个流传较广的实现被评价为「浑浊」「像拉丝」,根因都不是位移算得不准,而是整块都在折射

真玻璃中间是平的,只在边缘弯光。上面那份位移场之所以看着对,关键就在中心 k = 1——那一片原样取样,字才保持清晰。「折射范围」这个滑块调的就是这条界线。

先把这件事做对,比在位移算法上抠细节收益大得多。 一个粗糙但只作用于边缘的实现,观感会好过一个精确却铺满整块的。

光:折射之外的另一半

只有折射,玻璃会显得「薄」,像一层膜。真正让它有厚度的是光,而光要分层做,每层职责单一:

做什么
折射层backdrop-filter: url(#…)
雾化层轻微 blur + 一点点底色,压住背景的高频噪声
软反光inset 大扩散阴影,给体积
硬高光inset 1px 锐边,勾出玻璃的边界

硬高光那层有个细节:左上和右下的强度要不一样。

css
box-shadow:
  inset  1px  1px 0 0 rgba(255,255,255,.55),
  inset -1px -1px 0 0 rgba(255,255,255,.66);

两边给同样的值,看着就是一圈描边;给不同的值,才读得出「光是从某个方向来的」。这是廉价感和质感之间很小但很关键的一步。


三、用之前必须知道

折射只有 Chromium 系认

backdrop-filter 引用 SVG 滤镜,目前只有 Chromium 系支持。 Safari 和 Firefox 会接受这条属性,却静默丢掉 SVG 部分。

注意后果不是「退化成模糊」——是什么都没有,玻璃直接变成一块裸的半透明。

而且 @supports 在这里帮不上忙:Safari 和 Firefox 对 backdrop-filter: url() 同样报告支持。只能运行时判断内核,检测不通过就切回毛玻璃那套。

它比模糊贵得多

开销要拆成两块看,混在一起会得出错误结论:

准备成本(偶尔一次)每帧成本(滚动、合成时)
毛玻璃0一次 blur —— GPU 高度优化过
造位移图canvas 逐像素算 + toDataURL一次全区域位移采样
加色散同上(一张图复用)三次位移采样 + 色矩阵 + 混合

准备成本只在挂载、尺寸变化、调参时付,不是每帧。所以真正影响滚动流畅度的是右边那列——而右边那列里,毛玻璃和折射差的是数量级,不在一张表上比。

响应式布局、内容会撑高的卡片、窗口频繁缩放的场景要留意左边那列:每变一次尺寸就要重画一整张位移图,卡顿发生在这些时刻。

适合卡片、按钮、弹层这类小面积的常驻元素;不适合整屏遮罩,更不适合长列表里成百上千个实例。

底衬决定成败

最后一条不是技术,但比技术更能决定效果好坏:玻璃好不好看,几乎完全取决于背后有什么。

那些教程的示例之所以好看,靠的是底下那条饱和度不低、色相跨度明显的渐变。同一套 CSS 压在纯白或浅灰上,立刻变成一块看不出所以然的白卡片。

所以决定用玻璃之前,先看看你的页面背后有没有值得透出来的东西。没有的话,先做背景,再做玻璃。

怎么选

需求选哪个
只想要「透而不糊」毛玻璃,别上折射
想要玻璃的「体积感」,但不想碰兼容性毛玻璃 + 那四层光效(纯 CSS,全浏览器)
要真的边缘弯光造一张位移图
追求真玻璃质感,且元素不多再加色散

第二行值得单独说:「像玻璃」这件事,光效层的贡献远大于折射。 圆角、小半径模糊、软反光加硬高光,这套全是标准 CSS,没有 Chromium 限制、没有位移图要维护、开销约等于毛玻璃。折射是点睛,不是主体。

所以我的取向是:先把光做好,再看要不要上折射;真上了折射,先保证只作用在边缘。 前两步决定像不像玻璃,最后一步只决定像得有多精确。

评论

评论加载中……

登录后再评论

注册要用邮箱收个验证码,只为确认邮箱能收信,不会拿去做别的。账号设置

先选一种读起来舒服的样子

随时可以改:右上角那个调色盘图标里还有天光、极简两档,也能调深浅色和主色。