剖析波浪着色器:正弦波、折射与意外发现

本文深入解析了基于WebGL的波浪着色器实现,涵盖片段着色器编程、正弦波生成、折射效果和GPU实时渲染技术,通过数学公式和代码演示如何创建有机流动的视觉动画效果。

演示

代码

免费课程推荐:通过34个免费视频课程、分步项目和动手演示,掌握GSAP的JavaScript动画。立即报名 →

每个创意工作室都有其奇怪的内部仪式。 我们的?一个名为“12个月12支笔”的挑战——每月一个实验,没有规则,没有客户,只是玩耍。 在咖啡因和混乱之间,我们的一个开发者做出了这个——一个波浪状、催眠的、果冻般的运动效果,立即成为团队的最爱。我们没打算做任何“严肃”的东西,但人们开始问它是如何工作的,于是我们就在这里——在Codrops上写关于它的文章!

概念

这个想法起初很简单:

如果我们能做出感觉有机的运动——不是机械的,不是线性的,而是流动的东西,会怎样?

我们想要那种“两个世界之间的液体”的感觉——一种似乎会呼吸、拉伸和放松的运动。 其核心是:

  • 片段着色器(生成几何波浪状单元格)
  • 数学驱动的扭曲(正弦波、涟漪和折射)
  • 以及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);
}

遇到的挑战

当然,并非一帆风顺(双关语)。 有一次波浪完全陷入混乱——尖峰、闪烁,一切都崩溃了。 结果发现,我们的平滑逻辑偏差了一个像素(真的)。 另一个有趣的错误:在高刷新率显示器上测试时,运动看起来太流畅——就像失去了纹理。所以我们不得不添加一点不完美,以恢复“手工制作”的感觉。

自己尝试

整个设置是开放的,易于混音——只需fork它并开始调整参数。 尝试改变:

  • 点的数量
  • 缓动曲线
  • 颜色渐变

我们很想看看你创造了什么样的波浪——标记我们或发送你的混音!

最后思考

有时最好的想法来自于不过分努力。 这个本应是一个快速的内部实验——但它变成了某种奇怪地令人满意且视觉丰富的东西。 我们希望你喜欢它,就像我们喜欢破坏(和修复)它一样。 由Blacklead Studio制作,作为我们“12个月12支笔”创意挑战的一部分。

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