用 Shader 写个完美的波浪~

时间:2022-07-24
本文章向大家介绍用 Shader 写个完美的波浪~,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。
前言

皮皮最近接到了一个小需求:

?美术小姐姐:皮皮皮皮,你会不会做奶茶? ?我:??? ?美术小姐姐:就是那种,奶茶的轮廓加上动态水波纹~ ?我:吓死我还以为让我做喝的奶茶... ?美术小姐姐:炒鸡多图片都需要这种效果,用动画的话工作量太大了! ?我:波浪效果是吧,小意思,一个月的奶茶就够了,或者扫码提需求~ ?美术小姐姐:皮???? ?我:卒~

俗话说:遇事不决,量子力学写虽得儿。

根据我多年喝奶茶的经验,像这种效果用 Shader 做就再简单不过了,最终的效果如下:

趁此机会,本篇文章就来与小伙伴们分享动态波浪 Shader 的原理和制作思路吧。

要注意的是,这是一篇偏入门的文章,写得会相对比较详细,尽量让不懂 Shader 的小白也可以看懂,这也是我写文章的一贯风格。

好了,话不多说,进入正题~


正文

?整体思路

看到波浪的表现特点我第一时间想到的就是正弦曲线(或者说是正弦波,又让我想起了示波器)。

?正弦曲线(Sinusoid)

「正弦曲线」是三角函数中的一种正弦(Sine)比例的曲线。正弦曲线表现为一条波浪线,形状犹如海上完美的波浪。

标准的正弦函数公式为:

y = sin(x)

正弦函数属于周期函数,其值域为 [-1, 1]

如下图就是一个纯正标准的正弦曲线:

而一般我们常用的正弦曲线公式为:

y = A cdot sin(omega x ± phi) + k

这条公式比标准公式多了几个常数,含义如下:

  • A「振幅(Amplitude)」,曲线最高点与最低点的差值,表现为曲线的整体高度
  • ω「角速度(Angular Velocity)」,控制曲线的周期,表现为曲线的紧密程度
  • φ「初相(Initial Phase)」,即当 x = 0 时的相位,表现为曲线在坐标系上的水平位置
  • k「偏距(Offset)」,表现为曲线在坐标系上的垂直位置

相位(Phase):上方公式中的 ωx±φ 部分称为相位,相位发生在周期性的运动之中,最直接的理解就是角度。

☕稍加思索

有了公式之后,我们可以尝试调整其中的常数来改变函数曲线的形态。

在查看下方的示例时,请尝试将「曲线形态的变化」「右上角公式的变化」关联起来。

改变曲线的高度

我们可以调整常数 A(振幅)来改变曲线的值域(值域为 [-A, A]):

改变曲线的周期

我们可以调整常数 ω(角速度)来改变曲线的周期:

改变曲线的水平位置

我们可以调整常数 φ(初相)来改变曲线的水平位置:

多说一句

其实对于“曲线的水平位置”这个描述是不太准确的,因为初相实际上改变的是当 x = 0 时的相位,也就直接影响函数曲线在 x = 0 处的位置。

所以说曲线的位置并没有真正改变,而只是曲线的形态发生了改变。

但是由于正弦曲线的周期性特点,曲线的这种形态变化看起来像是曲线进行了位移。

改变曲线的垂直位置

我们可以调整常数 k(偏距)来改变曲线的垂直位置:

?动手实现

明白了正弦曲线的特性之后,接下来我们需要做的就是在代码中运用正弦函数。

慢着!正弦曲线确实如海上完美的波浪般优美,但是正弦曲线是静态的,我们要的波浪是动态的啊!

?如何让曲线动起来

别慌!还记得我们可以调整「初相」来改变曲线的“水平位置”吗?

既然如此,我们可以给初相加入「时间因素」,使得 y 值可以随着时间的增加发生「周期性变化」,看起来就像是曲线在进行“水平位移”。

就像这样:

得到新的公式

加入时间因素 t 后的曲线公式:

y = A cdot sin(omega x ± phi t) + k

?On Shadertoy

小贴士:由于 GLSL ES 没有办法进行调试,所以写 Shader 时可以先在 Shadertoy 中编写并在线预览,显著提高效率。

「一切尽在注释中,简单详细且直观。」

主函数代码如下:

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // 将像素坐标归一化(区间 [0.0, 1.0])
    // iResolution 是 Shadertoy 提供的视口分辨率全局变量(类型:vec3)
    vec2 uv = fragCoord / iResolution.xy;
	
    // 振幅(控制波浪顶端和底端的高度)
    float amplitude = 0.05;
    
    // 角速度(控制波浪的周期)
    float angularVelocity = 10.0;
    
    // 频率(控制波浪移动的速度)
    float frequency = 10.0;
    
    // 偏距(设为 0.5 使得波浪垂直居中于屏幕)
    float offset = 0.5;
    
    // 初相位(正值表现为向左移动,负值则表现为向右移动)
    // iTime 是 Shadertoy 提供的运行时间全局变量(类型:float)
    float initialPhase = frequency * iTime;
    
    // 代入正弦曲线公式计算 y 值
    // y = Asin(ωx ± φt) + k
    float y = amplitude * sin((angularVelocity * uv.x) + initialPhase) + offset;
    
    // 区分 y 值上下部分,设置不同颜色
    vec4 color = uv.y > y ? vec4(0.0, 0.0, 0.0, 1.0) : vec4(0.0, 0.7, 0.9, 1.0);
	
    // 输出到屏幕
    fragColor = color;
}

预览效果如下(?是不是有内味儿了):

在线预览:https://www.shadertoy.com/view/ttSfRh

?On Cocos Creator

我们主要关注片段着色器部分,这里就不展示整个 Effect 文件的代码了,直接上传送门吧。

Effect 文件:https://gitee.com/ifaswind/eazax-ccc/blob/master/resources/effects/eazax-sine-wave.effect

代码核心其实就是套用了公式,我们代码注释一起看吧。

「一切尽在注释中,简单详细且直观。」

片段着色器代码如下:

CCProgram fs %{
  precision highp float;

  // 引入 Cocos Creator 内置的全部变量
  #include <cc-global>
  
  // 顶点颜色(来自顶点着色器)
  in vec4 v_color;
  // UV 坐标(来自顶点着色器)
  in vec2 v_uv0;

  // 纹理
  uniform sampler2D texture;

  // 自定义属性
  uniform Properties {
    float amplitude;		// 振幅
    float angularVelocity;	// 角速度
    float frequency;		// 频率
    float offset;		// 偏距
  };

  void main () {
    // 保存顶点颜色
    vec4 color = v_color;
    
    // 叠加纹理颜色
    color *= texture(texture, v_uv0);
    
    // 直接丢弃原本就透明的像素
    if (color.a ==  0.0) discard;
    
    // 初相位(正值表现为向左移动,负值则表现为向右移动)
    // cc.time 是 Cocos Creator 提供的运行时间全局变量(类型:vec4)
    float initiaPhase = frequency * cc_time.x;
    
    // 代入正弦曲线公式计算 y 值
    // y = Asin(ωx ± φt) + k
    float y = amplitude * sin(angularVelocity * v_uv0.x + initiaPhase) + offset;
    
    // 丢弃 y 值以上的像素(左上角为原点 [0.0, 0.0])
    if (v_uv0.y < y) discard;
    
    // 输出颜色
    gl_FragColor = color;
  }
}%

运行效果如下:

使用 cc.tween 动态改变高度(偏距)实现波浪进度条:

cc.tween(this.sineWave)
    .to(3, { height: 1 })
    .to(0.5, { amplitude: 0 })
    .start();

在线预览:https://ifaswind.gitee.io/eazax-cases?case=sineWave SineWave 组件:https://gitee.com/ifaswind/eazax-ccc/blob/master/components/effects/SineWave.ts