文章

用 fbm 噪声写一片极光:原生 WebGL 着色器入门

不引入 three.js,用几十行 GLSL 写一个会呼吸的极光背景,以及为什么"几 KB 的原生 WebGL"有时比 600KB 的库更合适。

用 fbm 噪声写一片极光:原生 WebGL 着色器入门

给博客做背景的时候我想要一片”会呼吸的极光”:极慢地流动,颜色在深蓝和暖金之间过渡,安静但一直活着。

第一反应当然是 three.js。但想了想:这个需求里没有模型、没有相机运动、没有光照——它本质上就是一张全屏的、随时间变化的渐变图。为此引入 600KB 的库,就像为了烧一壶水买一套厨房。

最后用原生 WebGL 写完,全部代码不到 3KB。这篇把思路拆开讲。

全屏着色器的骨架

不需要场景图,只需要让片元着色器跑满屏幕。两个三角形拼一个覆盖裁剪空间的矩形即可:

1
2
3
// 顶点着色器:原样传递
attribute vec2 aPos;
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }

JS 侧就是标准的 WebGL 八股:创建上下文、编译着色器、传一个 6 顶点的 buffer、每帧把时间和分辨率塞进 uniform。骨架部分五十行封顶。

噪声:从 hash 到 fbm

极光的”有机感”全部来自噪声。GLSL 没有内置噪声,自己搭三层:

第一层 hash——把整数坐标变成伪随机数:

1
2
3
float hash(vec2 p) {
  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}

第二层值噪声——取网格四角的 hash 值做双线性插值,得到连续的随机场。

第三层 fbm(分形布朗运动)——把值噪声按不同频率叠四次,每次频率翻倍、振幅减半:

1
2
3
4
5
6
7
8
float fbm(vec2 p) {
  float v = 0.0, a = 0.5;
  for (int i = 0; i < 4; i++) {
    v += a * noise(p);
    p *= 2.0; a *= 0.5;
  }
  return v;
}

低频给大形状,高频给细节,叠出来的东西就有了云和光的质感。

让它流动,但要”极慢”

流动就是让噪声的采样坐标随时间漂移。诀窍在于用 fbm 扭曲 fbm——先算一次 fbm 作为偏移量,再用它去扰动第二次 fbm 的坐标,流动就不再是整体平移,而是内部的翻涌:

1
2
vec2 q = vec2(fbm(uv + t * 0.03), fbm(uv - t * 0.02));
float f = fbm(uv + q * 0.8);

时间系数是效果的灵魂。0.3 是屏保,0.03 才是极光。我的标准是:盯着看能察觉在动,扫一眼察觉不到

颜色用噪声值在深蓝和暖金之间 mix,再压低整体亮度到接近背景色——它是背景,不是主角。

工程上的收尾

背景不能偷走正文的性能,几件事必须做:

  • DPR 上限 1.5,4K 屏没必要按物理像素跑片元
  • IntersectionObserver + visibilitychange:页面切走就停 requestAnimationFrame
  • prefers-reduced-motion 时退化为静态渐变
  • WebGL 创建失败直接放弃,露出 CSS 背景色——装饰层的失败不允许影响任何功能

什么时候用 three.js

不是反对 three.js——加载模型、管理场景、做交互,它无可替代(我在数字孪生项目里重度使用)。但当需求本质上是”一张会动的图”时,一段片元着色器就是全部。区分这两种需求,比熟练任何一个库都值钱。

本文由作者按照 CC BY 4.0 进行授权