← Journal

把动画搬上 GPU:一次首屏性能重写笔记

首屏 3D 卡顿的根因往往不在 GPU,而在每帧的 CPU 主线程。记录一次把晶格动画移进顶点着色器的重写。

这个博客的首屏是一片 GPU 驱动的区块晶体网络。它最初的版本在 Retina 屏上明显卡顿——而问题几乎不在 GPU。

真正的瓶颈:每帧 CPU 重写

旧实现每一帧都在主线程上做两件昂贵的事:

  • 对每个实例 setMatrixAt() 重写变换矩阵;
  • 重建所有哈希连线的顶点缓冲。
// 反例:每帧 CPU 重算 + 上传,主线程被打爆
for (let i = 0; i < count; i++) {
  dummy.position.copy(flow(base[i], t));
  mesh.setMatrixAt(i, dummy.matrix);
}
mesh.instanceMatrix.needsUpdate = true; // 每帧全量上传

把位移交给顶点着色器

更好的做法是:把基准坐标作为 instanced attribute 一次性传上去,位移、传播脉冲全部在顶点着色器里用 uTime 计算。JS 每帧只更新一个 uniform。

attribute vec3 aBase;
uniform float uTime;
vec3 flow(vec3 b){
  return vec3(
    sin(uTime*0.42 + b.y*0.5) * 0.17,
    sin(uTime*0.6  + b.x*0.45) * 0.34,
    cos(uTime*0.5  + b.x*0.3)  * 0.17
  );
}
void main(){
  vec3 p = position + aBase + flow(aBase);
  gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
}

配合几个常被忽视的细节:

  • setPixelRatio(min(dpr, 1.5))——Retina 是首屏卡顿的头号原因;
  • IntersectionObserver 在首屏离开视口时暂停渲染循环;
  • 半分辨率 bloom,且只在桌面强机开启;
  • 首帧前 renderer.compile() 预编译,避免首次滚动 jank。

经验法则:动画一旦逐帧触碰 CPU 上的「每个对象」,就该想办法把它整体搬进 shader。

重写后主线程几乎不再参与逐帧动画,60fps 稳定。这篇就是这次重写的笔记。

← 返回 Journal