如何給普通圖片加上水波紋【shader 奇技淫巧】

這是上帝的傑作發表於2021-10-28

3D場景實現水波紋,我們往往會使用網格去模擬真實的水流動,無論是簡單的三角函式或是gerstner wave。然後通過真實物理渲染(base physcal render)來實現其中的折射與反射。這些實現可以參考《GPU GEMS》第一版。
image.png

原諒我,古早年代的書就這效果
image.png
但對於2D場景這樣的模擬就顯得開銷過大,2D場景往往會使用一些“投機取巧”的方式,例如使用沃羅諾伊紋理(voronoi)來模擬焦散效果。
image.png
而本文就來聊聊如何投機出一個2D的水波紋效果,最終效果如下:
image.png

最終程式碼:

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.);

其中帶有一個衰退函式:
image.png
這裡借用了布林馮反射模型的高光項:
image.png

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);
......

image.png
值得注意的是這裡用到了反射項一樣使用了向量n,但只用了向量的方向,而週期性則由intensity實現:
image.png
現在讓我們來看看如何實現波的疊加
image.png

實現疊加

實現一個水波很容易但如何實現波的疊加?最先想到的是通過noise生成隨機波源,用framBuffer記錄。本文提供了一個不錯的思路:
首先使用階梯函式讓畫面重複

vec2 frag = uv;
frag *= u_radio;
......
vec3 color = texture2D (u_image0, fract(frag)).rgb;
gl_FragColor = vec4 (color, 1.0);
......

image.png
這裡有一個小技巧,如果重複的不是uv座標而是紋理,我們就能讓效果重複展示在一個換面中,例如實現一些故障效果:
image.png
而本篇我們則需要使用迴圈來實現多波源效果:

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);

image.png

彼此影響

但這樣波源直接不會互相影響。此時我們就要通過迴圈把不同波源的影響累加到同一個向量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));
    }
  }

image.png
這裡MAX_RADIUS=1,所以每一個floor分割的區域不僅接受自己的波源,還同時接受以自己為中心的9宮格另外8個方向的波源。此外這裡並沒有採用正弦波,而採用了更為逼真的複合波形,加上(1-t)*(1-t)產生的衰減,保證只接受相鄰的波不至於穿幫:
image.png
如果沒有衰減而穿幫,因為波只能傳遞向相鄰的一個單位,無法再繼續傳播下去:
image.png
但這樣波就太過規則了,所以通過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));
......

image.png

最後選擇一種波形:

由於是for迴圈疊加的circles,所以最後要對它進行平均

// 兩輪迴圈新增了weight個波(取平均)
float weight = float ((MAX_RADIUS * 2 + 1) * (MAX_RADIUS * 2 + 1));
circles /= weight;

最終模擬一個向量n,參與上文的反射項方程,所以我們需要選擇一個波形,我這裡選擇sin(xx + yy)。不過這是模擬,各位看客也可以選擇自己喜歡的波形:
image.png
本篇就結束了,下一篇我們來說說,上文中提到的glitch效果要如何製作:
image.png

相關文章