剖析波浪着色器:正弦、折射与偶然之美
逐步解析一个意外动画实验背后的数学原理和GPU逻辑。
概念起源
这个想法最初很简单:我们能否创造一种有机的运动感觉——不是机械的,不是线性的,而是某种流动的东西?
我们想要那种"两个世界之间的液体"的感觉——一种似乎在呼吸、伸展和放松的运动。
其核心是以下技术的混合:
- 片段着色器(用于生成几何波浪状单元格)
- 数学驱动的扭曲(正弦波、涟漪和折射)
- requestAnimationFrame用于平滑、连续的GPU更新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
function draw(tms) {
const t = tms * 0.001;
// 有机运动:随时间轻柔扭曲网格大小的正弦波
const wave = Math.sin(p.x * 0.01 + p.y * 0.015 + t) * 0.25;
const localCell = cell * (1.0 + wave * 0.2);
// 用户交互(点击)触发的涟漪效果
float ripple = sin(R * 0.06 - dt * 6.0) * env;
// 将玻璃状折射与基础图像混合
vec3 col = mix(base, glass, inside);
gl_FragColor = vec4(col, 1.0);
}
requestAnimationFrame(draw);
|
工作原理
这里的魔法技巧在于我们如何每帧重新计算波浪路径。
我们不是动画化DOM或CSS属性,而是在着色器中逐帧动态重建每个像素。
每个单元格都像一个活生生的表面——随时间脉动、折射和涟漪。
想象用橡皮筋画一条线——每个锚点都跟随前一个点,带有一点延迟和 overshoot。这种延迟给了我们那种美味的粘稠、有生命的运动。
出于性能考虑,我们使用:
- requestAnimationFrame实现平滑更新
- GPU驱动的数学计算——直接在着色器中计算正弦、折射、涟漪
- requestAnimationFrame将GPU帧与浏览器刷新率同步
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// 基于选定形状找到最近的单元格中心
void nearestCenter(int shape, vec2 p, float cell, out vec2 c, out vec2 lp) {
if (shape == 0) {
vec2 qr = hex_pixel_to_axial(p, cell);
vec2 qrr = hex_axial_round(qr);
c = hex_axial_to_pixel(qrr, cell);
lp = p - c;
} else {
vec2 g = floor(p / cell + 0.5);
c = g * cell;
lp = p - c;
}
}
|
预设与变体
核心系统运行后,我们忍不住不断调整它。
我们没有使用固定预设,而是构建了一个简单的控制面板——四个直接控制着色器参数的滑块:
- 单元格大小(uCell)——网格密度
- 振幅(uAmp)——折射强度
- 色差偏移(uChrom)——颜色分离量
- 速度(uSpeed)——波浪演变速度
仅改变其中一个参数就能立即转变运动的情绪——从柔和流畅到锐利有力。
每一帧,应用程序从滑块读取实时值并直接发送到着色器——因此任何微小变化都会立即涟漪到整个表面。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
function draw(tms) {
const t = tms * 0.001;
// 从UI到着色器的实时参数
gl.uniform1f(uCell, parseFloat(cellInp.value));
gl.uniform1f(uAmp, parseFloat(ampInp.value));
gl.uniform1f(uChrom, parseFloat(chromInp.value));
gl.uniform1f(uSpeed, parseFloat(speedInp.value));
gl.uniform1f(uTime, t);
// 更新视频纹理(如果激活)
if (videoReady) {
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, videoEl);
}
// 渲染 + 下一帧
gl.drawArrays(gl.TRIANGLES, 0, 6);
requestAnimationFrame(draw);
}
|
开发过程中的挑战
当然,并非一帆风顺。
有段时间波浪完全陷入混乱——尖峰、闪烁,一切都崩溃了。
结果发现,我们的平滑逻辑偏差了一个像素(真的只有一个像素)。
另一个有趣的bug:在高刷新率显示器上测试时,运动看起来过于平滑——就像失去了纹理。所以我们不得不添加一点不完美,以恢复"手工制作"的感觉。
亲自尝试
整个设置是开放的,易于重新混合——只需fork它并开始调整参数。
尝试改变:
我们很期待看到你们创造什么样的波浪——标记我们或发送你们的混音版本!
最终思考
有时候最好的想法来自于不过分努力。
这个项目原本应该是一个快速的内部实验——但它变成了某种异常令人满意且视觉丰富的东西。
我们希望你们能像我们享受破坏(和修复)它一样享受它。
由Blacklead Studio制作,作为我们"12个月12个笔"创意挑战的一部分。