乐趣区

关于webgl:如何给普通图片加上水波纹shader-奇技淫巧

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 成果要如何制作:

退出移动版