
Reimagined Glass:任意形状的实时玻璃折射
摘要 Abstract
Reimagined Glass 是一套在浏览器端实时运行、不依赖服务端渲染的玻璃折射实现。就本文所知,在「二维有向距离场 + 屏幕空间折射」这一技术路线内,它是目前公开实现中物理一致性最高的一个。该判断建立在四项可验证的技术差异之上:
- 表面法线由场梯度给出。 折射方向取自距离场的梯度
,而非「由形状中心指向外」这一仅对圆成立的近似。 - 距离经度量归一化。 非度量场统一由
校正为合法的有向距离场,使折射强度在超椭圆、动态水滴、形状插值等场合保持一致。 - 采样端引入预滤波。 折射偏移随
发散,边界附近的点采样必然混叠;实现按屏幕空间足迹解析地选取 mip 层级,等价于硬件纹理单元在动态控制流下无法完成的工作。 - 形状约束被解除。 圆、方 / 圆角矩形、超椭圆、胶囊、正多边形、星形、十字、心形、动态水滴共 9 类解析原语,以及由 PNG 蒙版、SVG 路径或文字烘焙得到的任意轮廓,共用同一套折射代码,无需任何逐形状标定。
同时需要明确该判断的适用范围:本实现是屏幕空间的二维近似,而非三维 Snell 求解。它不包含全内反射、焦散,也不包含随入射角变化的 Fresnel 项。其贡献在于——在该类方法内部,将每一个原本依赖圆形对称性才成立的量,替换为对任意形状普遍成立的量。第九节末尾会完整讨论这条边界。
本项目在 liquid-glass(Vue 版) 的基础上进阶开发而来。具体而言,是将参考实现中仅对圆成立的折射公式拆解为若干独立分量,逐项分析其成立条件,并替换为形状无关的等价形式。
一、技术谱系与横向对比
Web 端的玻璃折射效果大致经历了三个阶段,以下三个项目分别代表其中一段。
naughtyduk/liquidGL 将任意 position: fixed 的 DOM 元素转换为玻璃面板。
其设计以工程可用性为优先:借助 html2canvas 生成页面快照作为背景纹理,每个玻璃元素对应一块独立的 WebGL 画布,形状取自元素自身的 border-radius,即圆角矩形。
折射项采用启发式的边缘隆起模型:
方向 drawImage 合成进快照,并把标记为 dynamic 的 DOM 节点交由 Worker 分区重拍。这是目前公开实现中对动态背景处理最完整的一个,相应代价在第九节讨论。
能力:滚动同步、自动 resize、多面板、GSAP 动画兼容、生产可用。
局限:形状受限、法线为近似、深度模型为启发式、边界无预滤波。
Muggleee/liquid-glass 基于 Vue 3 + Vite + WebGL2 + GLSL ES 3.0 + SDF。
它将整个屏幕交由一个全屏四边形处理,以有向距离场定义玻璃几何。折射由单行代码给出:
vec2 offset = mix(vec2(0), normalize(st) / sd, length(st));
其中 mix 的插值系数为 length(st),是一个不受 mix 在此执行线性外推,因此该式等价于:
即整个折射项就是
能力:完整的 SDF 建模、色散、投影、高光、五点超采样抗锯齿。
局限:仅支持圆;mediump 精度;背景纹理未生成 mipmap;偏移在宽高比缩放空间中计算却直接叠加到原始 UV,在非正方形视口下产生横向拉伸;uDistort 项与形状相关;蒙版纹理为纯白画布,m.a * m.a 恒为 1。
Jason-JP-Yang/Reimagined-Glass 沿用相同技术栈与编程管线,将折射模型由圆专用重写为形状无关。
其核心是四项替换:
| 参考实现所用的量 | 其实际语义 | 仅对圆成立 | 替换为 |
|---|---|---|---|
normalize(st) | 表面法线 | 是 | |
sd | 度量距离 | 是 | |
length(st) | 深度权重 | 是 | |
硬编码 sdCircle 高光 | 边缘光 | 是 | 沿轮廓的法线调制光带 |
在此之上,实现还包含解析式 mip 预滤波、有界色散、黄金角磨砂采样、精确欧氏距离变换烘焙、至多 8 实例的 smooth-min 融合、面积守恒的速度形变,以及全文档快照配合 uTexTransform 的滚动映射。
三者的横向对比如下:
| 维度 | liquidGL | liquid-glass (Vue) | Reimagined Glass |
|---|---|---|---|
| 渲染载体 | 每个 DOM 元素一块画布 | 全屏四边形 | 全屏四边形 |
| 支持形状 | 圆角矩形(取自 border-radius) | 圆 | 9 类解析原语 + 任意烘焙轮廓 |
| 法线来源 | normalize(p)(元素中心) | normalize(st)(形状中心) | |
| 距离度量 | 圆角矩形 SDF | sdCircle | 任意场 |
| 深度模型 | edge 与 edge^10 的混合 | length(st) | |
| 着色精度 | mediump | mediump | highp |
| 背景 mip | 无 | 无(LINEAR,未生成 mipmap) | 完整 mip 链 + 解析选层 |
| 边界抗锯齿 | smoothstep | 五次完整重采样 | 五点覆盖率,无额外纹理开销 |
| 色散 | 逐通道比例 | 逐通道比例 | 比例并施加上界 DISP_MAX |
| 磨砂 | 高斯核 + 颗粒 | 不支持 | 黄金角螺旋,选层由采样间距决定 |
| 多实例 | 多画布独立渲染 | 单实例 | 8 实例,带 payload 的 smooth-min |
| 运动形变 | tilt | uDistort(形状相关) | 面积守恒的速度拉伸(仿射) |
| 动态背景 | 视频合成 + 动态节点重拍 | 不支持 | 全文档快照,滚动零成本 |
| 性能降级 | 无 | 无 | 帧时超标时降低渲染分辨率 |
二、参考实现的适用条件
参考实现的折射项化简后为
对于圆,这三个分量之间存在三重巧合:
圆的三重巧合
- ① 由中心指向外的方向恰好等于表面法线
; - ② 到中心的距离恰好等于
,即该像素在玻璃内部的深度; - ③ 场各向同性,因此投影与高光可直接复用
length(),无需独立求值。
一旦形状变为正方形,三条巧合同时失效。
- 方向分量失效。 在靠近角点的边上,径向方向与表面法线的夹角最大可达 45°,折射方向随之产生系统性偏差。
- 深度分量失效。
length(st)不再携带深度信息。正方形边中点到中心的距离为,角点为 ,但二者同处边界,深度均应为零。 - 高光分量失效。 参考实现的高光由硬编码的
sdCircle生成,与实际玻璃轮廓无关。
仅将 sdCircle 替换为 sdBox 而保持其余不变,会同时出现三类缺陷:边缘扭曲与轮廓错位、角点撕裂、色散方向发散。这说明问题不在于选用何种距离函数,而在于上述三重巧合本身。
三、折射模型的泛化
3.1 表面法线:来自场梯度
两条中心差分共需四次额外的场求值,一次得到两个量:精确的表面法线,以及无额外代价的
3.2 度量归一化
有向距离场的定义性质是
一阶修正为:
该步骤同时校正了超椭圆、动态水滴、形状过渡中的插值场,以及各向异性拉伸后的实例——它们均非度量场。这也是新增形状无需任何逐形状标定的原因。
实现中的表达式为:
1 | fd.sd = clamp(f0 / max(gl, 1.0), -60.0, 60.0); |
分母在 1 以下不生效,这一约束有明确依据:
仅当场的增长快于距离时才归一化
- 场本身非度量场,应予修正;
- 位于 kink 处——中心差分在此平均了两个方向相反的单位梯度。正方形对角线上该平均值为
。
若不加约束,除以 0.71 会使整条中轴上的
3.3 深度权重:内切圆半径
其中
该定义并非任意选取,可由代入验证。取半径
结果与参考实现逐比特一致。因此该式并非另一种视觉上可接受的近似,而是参考公式的严格推广:圆的呈现结果完全不变,其余形状获得同一条内部深度斜坡。
内切半径在 src/core/shapes.js 中逐形状给出,可解析求值者一律解析求值:
1 | { id: 'polygon', inradius: (s) => Math.cos(PI / Math.max(s.p1, 3)) } // 正 n 边形的边心距 |
烘焙形状无解析表达式,故在距离变换完成后取全场最深值作为内切半径——这正是内切圆半径的定义。
3.4 边缘高光:沿轮廓的光带
参考实现的圆形高光被替换为一条贴合轮廓、距边界固定深度的光带,并由表面法线与光照方向的点积调制:
1 | float band = max(fd.re * 0.18 * uHighlightSize, px * 2.0); |
其中宽度约 2 像素的 lip 项对最终观感的贡献显著。真实玻璃边缘存在一条极细的高亮线,该项的有无直接决定画面被识别为玻璃还是半透明色块。由于它同样由场的法线驱动,因此对星形或标志轮廓也同等适用。
四、片元着色器的实现
src/shaders/fragment.glsl 共 444 行,以下按求值顺序逐段说明。
4.1 场的合成:mapRaw
1 | vec2 mapRaw(vec2 p) { |
其中包含三项设计:
其一,uInvMat 将世界空间映射回 unit 形状空间。 全部原语均归一化为内接于 mat2(旋转、缩放与各向异性拉伸的复合)的逆矩阵即可处理所有实例变换,且同一个包围半径对所有形状类型有效。unit 距离乘以 uScaleAvg(
其二,smooth-min 携带 payload。 融合两个形状时不仅需混合距离,还需同步混合内切半径,否则深度斜坡将在融合处出现断裂:
1 | vec2 sminV(vec2 a, vec2 b, float k) { |
其三,包围圆早退。 skip 的取值需同时覆盖 smooth-min 的影响半径与投影的柔和外沿(投影同样通过该场求值),因此为 uMergeK * 4.0 + uShadowBlur * 1.8 + 0.04。
4.2 梯度采样与自适应差分步长
1 | float eMin = px * 0.9; |
差分步长 uSmoothNormal 默认 0.05)。
其依据如下:
在最近边界点发生切换的位置——正方形内部的对角线、任意形状的中轴、多边形的角——精确法线本身是不连续的。像素级宽度的中心差分会忠实复现该跳变,在画面上表现为一条横穿玻璃的硬边。加宽步长可将跳变展开为一段平滑旋转,但固定的宽步长同时会钝化边界,而边界既是轮廓所在,也是折射强度最大的区域。
因此步长随深度自适应:边界处保持精确,中轴处才引入平滑,而中轴所在深度的折射已充分衰减。角点附近的残余接缝由 §4.4 的足迹预滤波处理,该步骤会将接缝两侧模糊为同一颜色。
同一平均过程还消除了采样式 SDF 贴图的双线性折面。双线性插值仅为分段线性,其梯度在每个纹素边界处发生阶跃,这是烘焙蒙版与文字轮廓边缘出现缺口状伪影的成因。
4.3 轮廓覆盖率的解析抗锯齿
参考实现的抗锯齿方式是将完整的 refrakt() 执行五次,纹理采样量随之增至五倍。本实现仅对覆盖率作五点采样,且不产生任何额外纹理读取:
1 | cover = 0.2 * ( |
由于
4.4 折射偏移与足迹预滤波
1 | float damp = mix(0.55, 1.0, smoothstep(0.08, 0.38, fd.metric)); |
den 处的 min 用于处理部分覆盖像素。此类像素跨越边界,若按其自身的
随后是预滤波,这是抑制边界混叠的关键:
1 | float footprint = amp * fd.re * px / max(den * den, 1e-9); |
推导如下。偏移量
在距边界数个像素处,该差值已达背景尺寸的 30% 量级,即相邻两个屏幕像素的采样位置在背景中相距甚远。这属于对梯度无界的函数作点采样,提高超采样倍数无法使其收敛,结果表现为边界处的彩色噪点。
正确的处理方式是选取与屏幕空间足迹匹配的 mip 层级。该工作本应由硬件纹理单元自动完成,但在此场景下不可用:
texture() 的隐式 LOD 依赖屏幕空间导数指令,而按 GLSL ES 3.00 规范,此类指令在动态分支与动态循环内属未定义行为,ANGLE / D3D 会直接拒绝编译。因此必须改用 textureLod(),并由解析式给出层级。
uvToTexels() 的换算不可省略。fitUV 会先对屏幕 UV 作缩放,而在 Page 模式下整篇文档均位于纹理内,一个视口高度的跨度仅占纹理的
预滤波同时处理了 §4.2 遗留的问题:法线翻转接缝的两侧被模糊为同一颜色,角点因而呈现为平滑过渡。
4.5 色散的上界
1 | const float DISP_MAX = 0.02; |
基础偏移在边界处按
4.6 磨砂采样
textureLod(uv, frost * maxLod) 这一常见写法只读取高层 mip 的单个纹素,相当于将该层的纹素网格整体放大,画面上表现为块状伪影。本实现的方案为:
1 | const int N = 10; |
其中包含两项设计:
- 黄金角分布。 角度步进
配合 ,得到面积均匀且无方向性的采样点分布,消除网格伪影; - 由采样间距选层。
。据此选取的 mip 使单点模糊核半径约等于采样间距,各核叠合为一个连续卷积核;若改由总半径选层,相邻核之间会留下未覆盖的空隙。
4.7 主函数:分级剔除与合成
1 | float reach = uShadowBlur * 1.8 + length(uShadowOffset) + uMergeK * 4.0 + 0.02; |
屏幕上多数像素与玻璃无交集,两级剔除将高开销路径限制在实际需要的像素范围内。
合成阶段包含三项与参考实现不同的处理:
合成阶段的三项修正
其一,投影不作用于玻璃自身。 玻璃不会将阴影投射至自身内部。参考实现未作此区分,导致整个形状被整体压暗约 16%。
其二,着色采用混合而非乘法。 原式 col * tint 在白色 tint 下退化为恒等映射,使默认取值表现为无效果。本实现改为
col = mix(col, uTint * (0.25 + 0.75 * col), uTintAmount)
其语义为「玻璃自身的颜色,受背后内容调制」:白色呈现为乳白玻璃,有色相则着色,黑色则压暗。
其三,曝光曲线按高光强度混入。 clamp(h * 3.0, 0, 1) 混入,仅在存在高光处生效。
透明合成方面,上下文以 premultipliedAlpha: false 创建,因此着色器需输出直通 alpha:
1 | float aShadow = sh * (1.0 - cover); |
同时禁用混合(gl.disable(gl.BLEND))。全屏四边形本身即直接写入整帧,任何 src-alpha 混合都会在绘制缓冲中留下预乘值,导致合成器二次相乘,在 Page 模式下表现为玻璃周围的暗边。
4.8 关于形状相关形变项的取舍
参考实现包含一个液态拖拽项:
该项按一个仅取决于「与指针距离」的量收缩整个场。对圆而言,边界上各点到中心等距,效果为均匀收缩;对正方形而言,边中点到中心的距离远小于角点,被收缩的比例约为角点的两倍,直边因而被拗为星形。此外,min(-tweak, 0) 会在
由于该项的行为与形状强相关,本实现未予移植。运动感改由 instances.js 中面积守恒的速度拉伸提供——后者是仿射映射,因而与形状无关。
五、任意轮廓的距离场烘焙
解析原语覆盖了常见形状,但「任意形状」意味着无解析表达式的形状:标志、字形、SVG 路径、PNG 剪影。src/core/sdfBaker.js 负责将其转换为着色器可像其他原语一样采样的数据。
5.1 精确距离变换的必要性
折射项需除以距离,因此距离场中的任何误差都会被
一维情形下,距离变换定义为
每个样点 v[] 与相邻两条的交点 z[]),再以一次扫描判定每个
1 | for (let q = 1; q < n; q++) { |
欧氏距离的平方可分离,因此二维情形只需按列执行一次、再按行执行一次。
数值精度方面,全流程使用 Float32 已足够:即使在 2 倍超采样分辨率下,平方距离仍不超过 Float64 网格需 67 MB)。
5.2 亚像素修正与降采样次序
距离变换在二值化蒙版上执行,因此其恢复的边界被量化至采样格点。该量化在常规 SDF 应用中不可见,但在本场景中会被
1 | let d = Math.sqrt(outer[i]) - Math.sqrt(inner[i]); |
- 2 倍超采样。 整个变换在
上执行,随后对结果距离场作盒式降采样。次序不可颠倒:距离场在边界附近可正确平均,蒙版则不能; - 亚像素修正。 抗锯齿的 alpha 覆盖率携带了边界的亚采样位置信息,将其叠加回距离值,并随深度增加迅速衰减,以免影响内部区域。
5.3 纹理格式与场的外延
烘焙结果为一张 R16F sampler2DArray 图层,共 4 层,可同时挂载 4 个自定义形状。着色器的采样方式为:
1 | float sdTex(vec2 p, float layer) { |
末项使场在贴图之外保持连续且单调,投影依赖该性质,否则玻璃外围会出现硬边。此处同样必须使用 textureLod:该函数在动态循环内部、且位于动态分支之后被调用。
三个烘焙入口分别为 bakeFromSvgPath(Path2D 配合 getBBox)、bakeFromText(measureText 的 actualBoundingBox*)与 bakeFromImage(透明 PNG 取 alpha,完全不透明的图像按亮度阈值二值化,因此黑白剪影同样适用)。默认烘焙对象为一个凹形四角星——凹形是解析原语最难表达的形状类别,可作为该管线的验证用例。
六、实例模型与运动学
6.1 各向同性的 p 空间
所有几何量位于统一的 p 空间:原点为视口中心,单位长度为视口高度,
该空间为各向同性,这是折射偏移能够正确换算回 UV 的前提:
1 | vec2 offUv = offP * amp / vec2(uAspect, 1.0); |
参考实现在宽高比缩放后的空间中计算偏移,却将结果直接叠加到原始 UV,导致在任何非正方形视口下,折射被横向拉伸一个宽高比因子。该偏差在圆上不易察觉(圆本身已被拉伸为椭圆,观感上自洽),在正方形上则明显可见。
6.2 实例变换矩阵
中间的
由
1 | this.inv = mat2Inverse(M) // 世界空间 → unit 形状空间 |
包围半径采用谱范数(最大奇异值)而非任一缩放分量:
对同时含旋转与各向异性拉伸的 mat2,这是唯一正确的包围圆半径;剔除逻辑的正确性依赖于它构成真正的上界。
6.3 形变、形状过渡与运动积分
- 形状过渡:切换形状时两个场在约 0.6 s 内混合(
easeInOutCubic),并叠加幅度为 的尺寸涨落。由于插值场非度量场,其表现依赖 §3.2 的归一化。 - Follow 模式:首个实例跟随指针,其余实例依次跟随前一个,配合 smooth-min 融合,形成可分离并重新聚合的液态拖尾。
- Drag 模式:实例可被抛出,具备动量、墙壁反弹(系数 0.62)与沿速度方向的形变。
- 帧率无关的平滑:
1 | export function damp(current, target, s, dt) { |
参数
七、渲染器与工程实现
若干实现约束
上下文不应启用 desynchronized。 该选项可能将画布提升为硬件 overlay 平面,此时其合成顺序不再受 z-index 约束,会覆盖页面上的全部内容,包括控制面板。
着色器在 JS 侧装配。 #define MAX_GLASS 与 #define SDF_SPAN 由 JS 常量注入,确保 GLSL 常量与 JS 常量不会失步。String.replace 的第二个参数须传入 replacer 函数而非字符串——被插入的 GLSL 文本若含以美元符号起始的序列,会被解释为替换模式。
编译失败时输出带行号的完整源码。 驱动仅报告形如 ERROR: 0:137 的位置,而该源码为运行时装配、磁盘上不存在,缺少行号列表则无法定位。
降低分辨率而非丢帧。 半秒平均帧时超过 24 ms(约 42 fps)时,质量系数减 0.16(下限 0.6)并重建画布尺寸;低于 13.5 ms 时以 0.1 的步长恢复。玻璃属于运动效果,帧率下降的可感知程度显著高于分辨率下降。
八、适用场景
在背景静止或缓慢变化的场景中,该实现已具备直接使用的条件。
- 组件级拟物:dock、悬浮导航、通知卡片、播放器控制条、模态框背板。此类元素通常覆盖在主视觉图或渐变之上,背景本身静止。
- 品牌形状:这是相对其他实现最具区分度的能力。将标志的 SVG 路径直接烘焙为玻璃轮廓,可使品牌形状本身成为玻璃,而非在圆角矩形内放置标志。
- 文字标题:
bakeFromText产生的字形轮廓同样具备完整的折射、边缘光与色散。 - 数据可视化透镜:可拖动的玻璃透镜在图表上移动,边界处的强折射构成自然的焦点—上下文隐喻。
- 移动端系统级界面:提高
liquidity后的拖尾与融合行为,接近系统动效的质感。
工程选型建议如下:
| 场景 | 建议 |
|---|---|
| 背景为图片、渐变或视频封面 | 可直接使用 |
| 背景为真实 DOM 且基本静止 | 采用 Page 模式,注意首次快照的耗时 |
| 背景频繁变化(信息流、动画) | 不建议使用,参见第九节 |
| 移动端 | 实例数控制在 3 以内,谨慎使用 frost,依赖自适应分辨率 |
| 不支持 WebGL2 的环境 | 需准备 backdrop-filter 降级路径 |
九、能力边界与工程限制
9.1 背景纹理无法动态更新
这是该类实现最核心、且在当前 Web 平台上无解的限制。
其根源不在实现层面,而在浏览器的安全模型:
WebGL 无法读取页面已渲染的像素。 现有 API 中不存在任何途径可将「元素背后已合成的画面」提供给着色器。这是有意的设计约束:一旦合成结果可被读回,跨源 iframe 的内容、:visited 链接的颜色差异乃至用户浏览历史,都可能通过像素侧信道泄露。backdrop-filter 只能使用固定滤镜集合,同样源于其运行在合成器内、结果不回传至 JS。
因此唯一可行的路径是重建:html2canvas 并非截图工具,而是以 JavaScript 遍历 DOM、解析计算样式并重新绘制至 2D 画布。该路径的代价是结构性的。
其一,单次快照开销显著。
1 | const docW = Math.max(document.documentElement.scrollWidth, window.innerWidth); |
以一篇 1440 × 6000 的文档为例,按 1.5 倍缩放后为 texImage2D 上传与 generateMipmap 再耗时数十毫秒。该开销构成可感知的卡顿,无法按帧执行,甚至无法按秒执行。
其二,滚动不构成失效条件。
由于快照覆盖整篇文档而非视口,滚动时仅需更新四个 float:
第四项取补是因为纹理按 y 轴翻转上传。滚动的成本因此降至可忽略,玻璃与页面保持严格同步,不会出现「先显示陈旧画面、再等待昂贵重拍」的现象。
其三,除滚动外的任何变化均导致快照失效。 包括 hover 状态、折叠块展开、图片懒加载、字体替换(FOUT)、暗色模式切换、CSS 动画、<video>、<canvas> 以及跨源 iframe。
liquidGL 在该问题上的处理最为完整:它维护一份动态节点清单,对这些节点单独重拍并合成回快照,视频则逐帧 drawImage,重负载交由 Worker 执行。但其 README 的特性表中,Real-time Refraction (CSS animations) 一项标记为不支持——CSS 动画的中间状态不在 JS 可枚举的范围内。
其四,根本解法只能来自浏览器平台。 所需能力是可编程的 backdrop,即允许着色器直接获取合成器已计算完成的图层。目前最接近的机制为 Firefox 的 -moz-element()(非标准、仅 Firefox 支持)与 Screen Capture 系的 Element Capture / Region Capture(需用户授权,面向屏幕共享而非渲染)。Houdini 的 Paint API 同样不具备 backdrop 访问权限。
结论:该类效果的适用边界不取决于性能,而取决于背景是否变化。背景静止时表现完整;背景仅因滚动而变化时表现良好;背景任意动态时,在当前 Web 平台上无法实现。
9.2 其他限制
文字清晰度。 玻璃下方的文字来自快照,已丢失子像素抗锯齿,经折射与 mip 预滤波后进一步模糊。因此玻璃适合覆盖图片、渐变与装饰性排版,不适合覆盖正文。Page 模式的设计正是基于这一点:画布透明合成于原生 DOM 之上,玻璃之外的文字不受任何影响。
覆盖层的交互。 画布采用 position: fixed; inset: 0; pointer-events: none,页面自身的交互不受影响;但玻璃的拖拽需在 window 级事件中针对 SDF 自行完成命中测试(GlassScene.pick()),并显式跳过 [data-gl-ignore="1"] 子树。
实例数上限为 8。 该上限受限于 uniform 数组尺寸与逐像素执行的实例循环。包围圆早退可显著降低远处实例的开销,但循环本身无法消除。
功耗。 全屏片元着色器在 120 Hz 显示设备上持续运行对移动设备电池不友好。自适应分辨率可缓解但无法改变性质,该效果更适合局部、短时、有明确目的地使用。
html2canvas 的保真度。 它是 CSS 渲染的再实现,对 backdrop-filter、部分 mask、复杂 clip-path 与 filter 链的支持并不完整。跨源图片必须携带 CORS 响应头(配置为 useCORS: true, allowTaint: false,画布一旦被 taint,getImageData 与纹理上传均不可用)。
色彩管理。 全流程假设 sRGB,未处理 P3 与 HDR。在广色域显示设备上,折射结果与原生 DOM 之间存在可辨识的色差。
物理模型的适用范围。 这是一个屏幕空间的二维模型:玻璃厚度由内切半径斜坡近似,折射方向来自二维法线而非三维表面法线与 Snell 定律,不包含全内反射、焦散,以及随入射角变化的 Fresnel 反射率。
综合上述,摘要中判断的成立边界是明确的:在该类方法内部,每一个原本依赖圆形对称性的量都被替换为普遍成立的量。它并非在模拟一块真实的三维玻璃,而是将一项视觉技巧转化为一个自洽的模型。二者的差别,正体现为「更换形状即失效」与「对任意形状均成立」之间的差别。
十、后续方向
- WebGPU 与增量快照:以 compute shader 生成 mip 链,并按脏矩形增量更新纹理,可将重拍代价由整篇文档降至变化区域。
- 由 DOM 几何直接生成 SDF:元素的
border-radius本身即sdRoundBox的参数,跳过烘焙即可使每个 DOM 元素自动获得精确的解析场。 - 厚度贴图:当前厚度为内切半径的线性斜坡;若允许烘焙一张厚度图,则可表达真实透镜、棱镜与倒角。
- 主题集成:将其封装为 Redefine-X 的可选插件,是该项目较为自然的落地形态。
参考与致谢
- naughtyduk/liquidGL —— DOM 贴片路线中完成度最高的实现,其动态节点与视频合成方案具有参考价值。
- Muggleee/liquid-glass —— 本项目的直接前身,本文的分析即由其折射公式
mix(vec2(0), normalize(st)/sd, length(st))展开。 - Inigo Quilez ——
sdf.glsl中绝大多数精确原语的出处。 - P. Felzenszwalb, D. Huttenlocher, Distance Transforms of Sampled Functions —— 烘焙管线所用的
精确距离变换。
- Title: Reimagined Glass:任意形状的实时玻璃折射
- Author: Jason-JP-Yang
- Created at : 2026-07-31 15:30:00
- Updated at : 2026-07-31 15:30:00
- Link: https://blog.jason-yang.top/2026/07/31/Reimagined-Glass/
- License: This work is licensed under CC BY-NC-SA 4.0.