前言
皮皮最近接到了一個小需求:
👧美術小姐姐:皮皮皮皮,你能不能做奶茶?
😱我:???
👧美術小姐姐:就是那種,奶茶的輪廓加上動態水波紋~
🙄我:嚇死我還以為讓我做喝的奶茶...
👧美術小姐姐:炒雞多圖片都需要這種效果,用動畫的話工作量太大了!
🤓我:波浪效果是吧,小意思,一個月的奶茶就夠了,或者掃碼提需求~
👧美術小姐姐:皮?🔨🔨🔨
🤪我:卒~
俗話說:遇事不決,量子力學寫雖得兒。
根據我多年喝奶茶的經驗,像這種效果用 Shader 做就再簡單不過了,最終的效果如下:
趁此機會,本篇文章就來與小伙伴們分享動態波浪 Shader 的原理和制作思路吧。
要注意的是,這是一篇偏入門的文章,寫得會相對比較詳細,盡量讓不懂 Shader 的小白也可以看懂,這也是我寫文章的一貫風格。
好了,話不多說,進入正題~
正文
🧐整體思路
看到波浪的表現特點我第一時間想到的就是正弦曲線(或者說是正弦波,又讓我想起了示波器)。
🔢正弦曲線(Sinusoid)
正弦曲線是三角函數中的一種正弦(Sine)比例的曲線。正弦曲線表現為一條波浪線,形狀猶如海上完美的波浪。
標准的正弦函數公式為:
正弦函數屬於周期函數,其值域為 [-1, 1]
。
如下圖就是一個純正標准的正弦曲線:
而一般我們常用的正弦曲線公式為:
這條公式比標准公式多了幾個常數,含義如下:
A
:振幅(Amplitude),曲線最高點與最低點的差值,表現為曲線的整體高度ω
:角速度(Angular Velocity),控制曲線的周期,表現為曲線的緊密程度φ
:初相(Initial Phase),即當x = 0
時的相位,表現為曲線在坐標系上的水平位置k
:偏距(Offset),表現為曲線在坐標系上的垂直位置
相位(Phase):上方公式中的
ωx±φ
部分稱為相位,相位發生在周期性的運動之中,最直接的理解就是角度。
☕稍加思索
有了公式之后,我們可以嘗試調整其中的常數來改變函數曲線的形態。
在查看下方的示例時,請嘗試將曲線形態的變化與圖中右上角公式的變化關聯起來。
改變曲線的高度
我們可以調整常數 A
(振幅)來改變曲線的值域(值域為 [-A, A]
):
改變曲線的周期
我們可以調整常數 ω
(角速度)來改變曲線的周期:
改變曲線的水平位置
我們可以調整常數 φ
(初相)來改變曲線的水平位置:
多說一句
其實對於“曲線的水平位置”這個描述是不太准確的,因為初相實際上改變的是當 x = 0
時的相位,也就直接影響函數曲線在 x = 0
處的位置。
所以說曲線的位置並沒有真正改變,而只是曲線的形態發生了改變。
但是由於正弦曲線的周期性特點,曲線的這種形態變化看起來像是曲線進行了位移。
改變曲線的垂直位置
我們可以調整常數 k
(偏距)來改變曲線的垂直位置:
🤠動手實現
明白了正弦曲線的特性之后,接下來我們需要做的就是在代碼中運用正弦函數。
慢着!正弦曲線確實如海上完美的波浪般優美,但是正弦曲線是靜態的,我們要的波浪是動態的啊!
🌊如何讓曲線動起來
別慌!還記得我們可以調整初相來改變曲線的“水平位置”嗎?
既然如此,我們可以給初相加入時間因素,使得 y 值可以隨着時間的增加發生周期性變化,看起來就像是曲線在進行“水平位移”。
就像這樣:
得到新的公式
加入時間因素 t
后的曲線公式:
🧩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;
}
預覽效果如下(🌊是不是有內味兒了):
🥥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》這個專題斷更了一段時間,很對不起小伙伴們,是時候續上了😹
傳送門
更多分享
公眾號
😺菜鳥小棧
💻我是陳皮皮,這是我的個人公眾號,專注但不僅限於游戲開發、前端和后端技術記錄與分享。
💖每一篇原創都非常用心,你的關注就是我原創的動力!
Input and output.