剖析波浪着色器:正弦、折射与偶然之美

本文深入解析了一个波浪着色器的实现原理,涵盖片段着色器、正弦波生成、折射效果和GPU实时渲染技术。通过数学公式和代码示例展示了如何创建有机流动的视觉效果,并探讨了性能优化和参数调校的实际应用。

剖析波浪着色器:正弦、折射与偶然之美

逐步解析一个意外动画实验背后的数学原理和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个笔"创意挑战的一部分。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计