用 fbm 噪声写一片极光:原生 WebGL 着色器入门
不引入 three.js,用几十行 GLSL 写一个会呼吸的极光背景,以及为什么"几 KB 的原生 WebGL"有时比 600KB 的库更合适。
给博客做背景的时候我想要一片”会呼吸的极光”:极慢地流动,颜色在深蓝和暖金之间过渡,安静但一直活着。
第一反应当然是 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:页面切走就停requestAnimationFrameprefers-reduced-motion时退化为静态渐变- WebGL 创建失败直接放弃,露出 CSS 背景色——装饰层的失败不允许影响任何功能
什么时候用 three.js
不是反对 three.js——加载模型、管理场景、做交互,它无可替代(我在数字孪生项目里重度使用)。但当需求本质上是”一张会动的图”时,一段片元着色器就是全部。区分这两种需求,比熟练任何一个库都值钱。