H5 WebGL實現水波特效

2023-07-11 18:00:44

前言

零幾年剛開始玩電腦的時候,經常在安裝程式上看到一種水波特效,滑鼠划過去的時候,就像用手在水面劃過一樣,感覺特別有意思。但是後來,就慢慢很少見過這種特效了。最近突然又想起了這種特效,於是開始折磨怎麼實現這種效果。

思路

我們知道,水波的運動軌跡可以看成隨時間變化的三角函數,那麼我們可以記錄每個水波形成的原點和執行時間,就能知道任一時刻水波的高度。但是,這種方法的運算量會隨著水波數量而線性增長,當水波數量很多時,就很可能出現效能問題。

有沒有一種辦法,可以根據上一時刻的水波幅度,計算出下一時刻的水波幅度呢?如果對於一個點,如果我們把它與其周圍的幾個點的均值的差作為加速度運動,會怎樣呢?

以二維平面為例,即加速度a有

a = (h(x-1) + h(x+1)) / 2 - h(x)

先用三角函數得到我們的水波的初始狀態:

然後套用上面的公式把結果視覺化

可見,水波會從中心想四周散開,這和我們平時看到的水波運動軌跡不正好相似嗎?我們把它推導成3維的:

a = ( 
    h(x-1, y-1) + h(x-1, y+1) 
    + h(x+1, y-1) + h(x+1, y+1) 
    )/4 - h(x, y)

所以,我們只需要畫出第一幀的水波影象,就能通過上面的公式計算出下一幀水波的影象了。

有了水波的影象,再根據馮氏光照模型去計算鏡面光,就能模擬出較為真實的水波了。

先來看看最終效果:

線上體驗地址:https://kason.site/

實現

首先,我們需要一個生成初始化水波的著色器

precision highp float;
const float PI = 3.141592653589793;
uniform sampler2D texture;
uniform vec2 centerCoord;
uniform float radius;
uniform float strength;			
varying vec2 coord;
void main() {
    vec4 info = texture2D(texture, coord);
    float d = min(distance(centerCoord, coord) / radius, 1.0);
    info.r += (cos(d * PI) * 0.5 + 0.5) * strength;
    gl_FragColor = info;
}

然後,再搞個更新水波的著色器

precision highp float;
uniform sampler2D texture;
uniform vec2 delta;
varying vec2 coord;
void main() {
    vec4 old = texture2D(texture, coord);
    vec2 dx = vec2(delta.x, 0.0);
    vec2 dy = vec2(0.0, delta.y);
    float avg = (
        texture2D(texture, coord - dx).r + texture2D(texture, coord - dy).r +
        texture2D(texture, coord + dx).r + texture2D(texture, coord + dy).r
    ) / 4.0;
    old.g += avg - old.r;
    old.g *= 0.995;
    old.r += old.g;
    gl_FragColor = old;
}

最後,再來個計算鏡面光並完成最終渲染的著色器

precision highp float;
attribute vec2 vertex;
uniform vec2 ripplesRatio;
varying vec2 ripplesCoord;
varying vec2 backgroundCoord;
void main() {
    gl_Position = vec4(vertex, 0.0, 1.0);
    backgroundCoord = vertex * 0.5 + 0.5;
    ripplesCoord = backgroundCoord  * ripplesRatio;
}
`, `
precision highp float;
uniform sampler2D samplerBackground;
uniform sampler2D samplerRipples;
uniform vec2 delta;
uniform float perturbance;
varying vec2 ripplesCoord;
varying vec2 backgroundCoord;

void main() {
    float height = texture2D(samplerRipples, ripplesCoord).r;
    float heightX = texture2D(samplerRipples, vec2(ripplesCoord.x + delta.x, ripplesCoord.y)).r;
    float heightY = texture2D(samplerRipples, vec2(ripplesCoord.x, ripplesCoord.y + delta.y)).r;
    vec3 dx = vec3(delta.x, heightX - height, 0.0);
    vec3 dy = vec3(0.0, heightY - height, delta.y);
    vec2 v = normalize(vec2(1.0, 1.0));
    vec2 r = -normalize(cross(dy, dx)).xz;
    vec4 specular = vec4(0.8, 0.8, 0.8, 1) * pow(max(0.0, dot(v, r)), 5.0);
    gl_FragColor = texture2D(samplerBackground, backgroundCoord + r * perturbance) + specular;
}

至此,核心著色器的程式碼都寫完了,只需要結合requestAnimationFrame呼叫WebGL介面完成最終渲染即可。

完整程式碼可以檢視本人Github: https://github.com/kasonyang/ripples.js