3D場景實現水波紋,我們往往會使用網格去模擬真實的水流動,無論是簡單的三角函式或是gerstner wave。然後通過真實物理渲染(base physcal render)來實現其中的折射與反射。這些實現可以參考《GPU GEMS》第一版。
原諒我,古早年代的書就這效果
但對於2D場景這樣的模擬就顯得開銷過大,2D場景往往會使用一些“投機取巧”的方式,例如使用沃羅諾伊紋理(voronoi)來模擬焦散效果。
而本文就來聊聊如何投機出一個2D的水波紋效果,最終效果如下:
最終程式碼:
precision mediump float;
/*
變數申明
*/
varying vec2 uv;
uniform sampler2D u_image0;
uniform float u_time;
uniform float u_offset;
uniform float u_radio;
#define MAX_RADIUS 1
#define DOUBLE_HASH 0
#define HASHSCALE1 .1031
#define HASHSCALE3 vec3 (.1031, .1030, .0973)
/*
工具函式
*/
float hash12 (vec2 p) {
vec3 p3 = fract (vec3 (p.xyx) * HASHSCALE1);
p3 += dot (p3, p3.yzx + 19.19);
return fract ((p3.x + p3.y) * p3.z);
}
vec2 hash22 (vec2 p) {
vec3 p3 = fract (vec3 (p.xyx) * HASHSCALE3);
p3 += dot (p3, p3.yzx + 19.19);
return fract ((p3.xx + p3.yz) * p3.zy);
}
void main () {
vec2 frag = uv;
frag.x *= u_radio;
frag = frag * u_offset * 1.5;
vec2 p0 = floor (frag);
vec2 circles = vec2 (0.);
for (int j = -MAX_RADIUS; j <= MAX_RADIUS; ++j) {
for (int i = -MAX_RADIUS; i <= MAX_RADIUS; ++i) {
vec2 pi = p0 + vec2 (i, j);
vec2 hsh = pi;
vec2 p = pi + hash22(hsh) ;
// hash12 新增隨機
float t = fract (0.3 * u_time + hash12(hsh));
vec2 v = p - frag;
// 半徑:
float d = length (v) - (float (MAX_RADIUS) + 1. )*t ;
float h = 1e-3;
float d1 = d - h;
float d2 = d + h;
float p1 = sin (31. * d1) * smoothstep (-0.6, -0.3, d1) * smoothstep (0., -0.3, d1);
float p2 = sin (31. * d2) * smoothstep (-0.6, -0.3, d2) * smoothstep (0., -0.3, d2);
circles += 0.5 * normalize (v) * ((p2 - p1) / (2. * h) * (1. - t) * (1. - t));
}
}
// 兩輪迴圈新增了weight個波(取平均)
float weight = float ((MAX_RADIUS * 2 + 1) * (MAX_RADIUS * 2 + 1));
circles /= weight;
float intensity = mix (0.01, 0.05, smoothstep (0.1, 0.6, abs (fract (0.05 * u_time + .5) * 2. - 1.)));
vec3 n = vec3 (circles, sin ( dot (circles, circles)));
vec3 colorRipple = texture2D (u_image0, uv + intensity * n.xy).rgb;
float colorGloss = 5. * pow (clamp (dot (n, normalize (vec3 (1., 0.7, 0.5))), 0., 1.), 6.);
vec3 color = colorRipple + vec3(colorGloss);
gl_FragColor = vec4 (color, 1.0);
}
折射和反射
2D模擬水波紋,主要就是要實現水波的折射與反射。
它們分別由反射項vec3(colorGloss)和折射項colorRipple控制
其中反射項由colorGloss控制
float colorGloss =5.* pow (clamp (dot (n, normalize (vec3(1.,0.7,0.5))),0.,1.),6.);
其中帶有一個衰退函式:
這裡借用了布林馮反射模型的高光項:
float gloss = pow(max(0,dot(n, viewDir)),_Gloss);
而normalize (vec3(1.,0.7,0.5))則可以類比為布林馮反射模型的指向相機的向量。由於沒有3D場景只能虛假地模擬一個,關於這塊相關的圖形學內容就不展開了,感興趣的可以閱讀LearnOpenGL - Basic Lighting
colorRipple
讓我再來看看折射項colorRipple:
vec3 colorRipple = texture2D (u_image0, uv + intensity * n.xy).rgb;
這主要依賴texture2D實現,一般我們使用texture2D(u_image0, uv)來呈現紋理,但也可以使用texture2D(u_image0, uv+offset)來實現一些奇特的效果,例如此前使用在10行程式碼搞定“熱成像”實現的colorRamp,以及實現的幾款2077風格的shader賽博朋克效果。
今天則通過offset加上一個與定點有關的距離場實現波動效果,例如:
......
vec2 offset = sin(23.*length(uv-vec2(0.5))-u_time);
vec3 color = texture2D (u_image0, uv + offset).rgb;
gl_FragColor = vec4 (color, 1.0);
......
值得注意的是這裡用到了反射項一樣使用了向量n,但只用了向量的方向,而週期性則由intensity實現:
現在讓我們來看看如何實現波的疊加
實現疊加
實現一個水波很容易但如何實現波的疊加?最先想到的是通過noise生成隨機波源,用framBuffer記錄。本文提供了一個不錯的思路:
首先使用階梯函式讓畫面重複
vec2 frag = uv;
frag *= u_radio;
......
vec3 color = texture2D (u_image0, fract(frag)).rgb;
gl_FragColor = vec4 (color, 1.0);
......
這裡有一個小技巧,如果重複的不是uv座標而是紋理,我們就能讓效果重複展示在一個換面中,例如實現一些故障效果:
而本篇我們則需要使用迴圈來實現多波源效果:
vec2 frag = uv;
frag = frag * 1.5;
vec2 p0 = floor (frag);
vec2 pp = frag - p0;
float offset = 0.03*sin(31.*length(pp)-5.*u_time);
vec3 color = texture2D (u_image0, uv + normalize(pp)* offset).rgb;
gl_FragColor = vec4 (color, 1.0);
彼此影響
但這樣波源直接不會互相影響。此時我們就要通過迴圈把不同波源的影響累加到同一個向量circles上:
vec2 circles = vec2 (0.);
for (int j = -MAX_RADIUS; j <= MAX_RADIUS; ++j) {
for (int i = -MAX_RADIUS; i <= MAX_RADIUS; ++i) {
vec2 pi = p0 + vec2 (i, j);
vec2 hsh = pi;
vec2 p = pi ;
// hash12 新增隨機
float t = fract (0.3 * u_time);
vec2 v = p - frag;
// 半徑:
float d = length (v) - (float (MAX_RADIUS) + 1. )*t ;
float h = 1e-3;
float d1 = d - h;
float d2 = d + h;
float p1 = sin (31. * d1) * smoothstep (-0.6, -0.3, d1) * smoothstep (0., -0.3, d1);
float p2 = sin (31. * d2) * smoothstep (-0.6, -0.3, d2) * smoothstep (0., -0.3, d2);
circles += 0.5 * normalize (v) * ((p2 - p1) / (2. * h) * (1. - t) * (1. - t));
}
}
這裡MAX_RADIUS=1,所以每一個floor分割的區域不僅接受自己的波源,還同時接受以自己為中心的9宮格另外8個方向的波源。此外這裡並沒有採用正弦波,而採用了更為逼真的複合波形,加上(1-t)*(1-t)產生的衰減,保證只接受相鄰的波不至於穿幫:
如果沒有衰減而穿幫,因為波只能傳遞向相鄰的一個單位,無法再繼續傳播下去:
但這樣波就太過規則了,所以通過hash12,hash22兩個noise函式給波源加上隨機值:
float hash12 (vec2 p) {
vec3 p3 = fract (vec3 (p.xyx) * HASHSCALE1);
p3 += dot (p3, p3.yzx + 19.19);
return fract ((p3.x + p3.y) * p3.z);
}
vec2 hash22 (vec2 p) {
vec3 p3 = fract (vec3 (p.xyx) * HASHSCALE3);
p3 += dot (p3, p3.yzx + 19.19);
return fract ((p3.xx + p3.yz) * p3.zy);
}
......
vec2 pi = p0 + vec2 (i, j);
vec2 hsh = pi;
vec2 p = pi + hash22(hsh) ;
// hash12 新增隨機
float t = fract (0.3 * u_time + hash12(hsh));
......
最後選擇一種波形:
由於是for迴圈疊加的circles,所以最後要對它進行平均
// 兩輪迴圈新增了weight個波(取平均)
float weight = float ((MAX_RADIUS * 2 + 1) * (MAX_RADIUS * 2 + 1));
circles /= weight;
最終模擬一個向量n,參與上文的反射項方程,所以我們需要選擇一個波形,我這裡選擇sin(xx + yy)。不過這是模擬,各位看客也可以選擇自己喜歡的波形:
本篇就結束了,下一篇我們來說說,上文中提到的glitch效果要如何製作: