用 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


專題

《一起學 Shader》這個專題斷更了一段時間,很對不起小伙伴們,是時候續上了😹

《一起學 Shader》

《Shader 入門:GLSL ES(簡介和基本語法)》

《Shader 入門:GLSL ES(數據類型)》

《Shader 入門:GLSL ES(運算符和限定符)》


傳送門

微信推文版本

個人博客:菜鳥小棧

開源主頁:陳皮皮

Eazax-CCC 游戲開發腳手架

Eazax-CCC 示例在線預覽


更多分享

《為什么選擇使用 TypeScript ?》

《高斯模糊 Shader》

《一文看懂 YAML》

《Cocos Creator 性能優化:DrawCall》

《互聯網運營術語掃盲》

《在 Cocos Creator 里畫個炫酷的雷達圖》


公眾號

😺菜鳥小棧

💻我是陳皮皮,這是我的個人公眾號,專注但不僅限於游戲開發、前端和后端技術記錄與分享。

💖每一篇原創都非常用心,你的關注就是我原創的動力!

Input and output.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM