我在 React Canvas 里做了一个液态玻璃透镜效果

0 阅读3分钟

我在 React Canvas 里做了一个液态玻璃透镜效果

最近我在 react-canvas 里做了一版「液态玻璃透镜」后处理,目标是:

  • 透镜中心有放大感
  • 透镜边缘有明显的玻璃扭曲和轻微色散
  • 不引入第二套渲染栈,直接复用现有 CanvasKit 渲染管线

这篇文章记录一下这个效果的实现思路、关键代码和踩坑。

image.png

项目与线上地址

为什么要做成后处理(Post Process)

一开始看起来最直觉的方案是:在页面上叠一个额外 canvas 或 WebGL 层专门做透镜。
但这会带来两个问题:

  1. 两套渲染结果同步成本高(滚动、缩放、相机状态都要对齐)
  2. 交互命中(pick)和视觉效果容易打架

最终采用的是:在同一条 CanvasKit 管线中做全屏后处理

流程非常直接:

  1. 先把场景完整绘制到离屏 surface
  2. 对离屏图像应用 SkSL RuntimeEffect
  3. 再把结果一次性绘制到主画布

这样做的好处是:

  • 场景逻辑完全复用,不需要维护双渲染树
  • 效果控制集中在 shader 和 uniforms
  • 与既有渲染架构兼容性高

透镜效果拆解

当前液态玻璃由 4 部分组成:

  1. 中心放大:透镜内部有明显放大
  2. 边缘扭曲:扭曲主要集中在外圈
  3. 色散(RGB 偏移):边缘轻微彩边,增强“玻璃感”
  4. 边缘高光 + 阴影:让透镜有实体感

其中我重点优化了一个点:
中心不需要扭曲,只要放大;扭曲集中在边缘约 20% 的环带。

具体做法是引入归一化半径 t,再用:

  • rim = smoothstep(0.8, 1.0, t) 作为边缘权重
  • 中心(t < 0.8)扭曲权重接近 0
  • 靠近边缘(t -> 1)扭曲和色散逐渐增强

这样视觉上更像“镜片边缘折射”,而不是整块区域都在抖动。

动画与性能:从“永远重绘”到“按需重绘”

透镜是纯 uniform 驱动的后处理,如果每帧都强制重绘,虽然简单,但成本偏高。
我把策略改成了:

  • 指针移动时通过 requestCanvasRepaint(canvas) 主动唤醒
  • shouldContinueRepaint 只在透镜还在追目标点时继续下一帧
  • 透镜静止后自动停止连续重绘

这个改动对体感影响很明显:移动时顺滑,停下时不再空转。

坐标与边界处理

Web 端最容易出错的是坐标系和边界采样:

  • 指针使用 getBoundingClientRect() + backing store 比例换算到像素坐标
  • shader 采样 UV 做 clamp(0..1),避免边缘出现黑边或抽样越界伪影

这些看似小问题,实际会直接影响“高级感”。

我这次的收获

  1. 视觉特效里,“哪里不做效果”往往和“做什么效果”一样重要
  2. 透镜这种效果,中心稳定 + 边缘强表达,比全域扭曲更自然
  3. 架构层做对(统一后处理管线)之后,后续微调成本会非常低

如果你也在做 Canvas/Skia 场景里的玻璃、景深、调色类效果,我非常建议优先走「场景离屏 + RuntimeEffect」这条路,扩展性会好很多。