【零基础】充分理解WebGL(八)

avatar
掘金前首席打杂官

接上篇 juejin.cn/post/711380…

在上一节里我们聊了随机,随机是模拟世界的无序和不可预测性。

然而实际上,真正的大自然在无序中孕育着秩序。空气看似无序的扰动,产生了气流,气流汇聚形成季风;水波看似无序,然而产生水流,形成河流、洋流;岩石的生长看似无序,但形成山脉,连绵不断……随机无处不在,秩序也无处不在。

这次褐蚁来到故地,只是觅食途中偶然路过而已。它来到孤峰脚下,用触须摸了摸这顶天立地的存在,发现孤峰的表面坚硬光滑,但能爬上去,于是它向上爬去。没有什么目的,只是那小小的简陋神经网络中的一次随机扰动所致。这扰动随处可见,在地面的每一株小草和草叶上的每一粒露珠中,在天空中的每一片云和云后的每一颗星辰上……扰动都是无目的的,但巨量的无目的扰动汇集在一起,目的就出现了。 —— 《三体II 黑暗森林》

为了比较真实地模拟大自然,我们在随机中引入秩序,把无序与秩序结合在一起,就形成了噪声

噪声

我们还是通过两段代码来理解如何从随机到噪声。

float random(vec2 co)
{
  float a = 12.9898;
  float b = 78.233;
  float c = 43758.5453;
  float dt= dot(co.xy ,vec2(a,b));
  float sn= mod(dt,3.14);
  return fract(sin(sn) * c);
}

float random(float i) {
  return random(vec2(i));
}

void main() {
  vec2 st = gl_FragCoord.xy / dd_resolution;
  st.x *= 15.0;
  float i = floor(st.x);
  float f = fract(st.x);
  float d = random(i); 
  FragColor.rgb = step(st.y, d) * vec3(1.0);
  FragColor.a = 1.0;
}

上面这段代码是我们上一节学到的随机,我们利用它来绘制出高低不平的白色条纹。

code.juejin.cn/pen/7116322…

接下来,我们修改一下main函数,对相邻两个随机区间之间进行平滑插值:

void main() {
  vec2 st = gl_FragCoord.xy / dd_resolution;
  st.x *= 15.0;
  float i = floor(st.x);
  float f = fract(st.x);
  float d = mix(random(i), random(i + 1.0), f * f * (3.0 - 2.0 * f));
  FragColor.rgb = step(st.y, d) * vec3(1.0);
  FragColor.a = 1.0;
}

这样就形成下面的效果,这种效果就叫做噪声

code.juejin.cn/pen/7116328…

注意这里我们用了三次平滑曲线 f * f * (3.0 - 2.0 * f),这是一种数学技巧,我们也可以用glsl内置函数smoothstep,把

float d = mix(random(i), random(i + 1.0), f * f * (3.0 - 2.0 * f));

改成

float d = mix(random(i), random(i + 1.0), smoothstep(0.0, 1.0, f)); 

结果是一样的。

二维噪声

上面这个是一维噪声函数,我们可以将它扩展到二维:

float random(vec2 co)
{
  float a = 12.9898;
  float b = 78.233;
  float c = 43758.5453;
  float dt= dot(co.xy ,vec2(a,b));
  float sn= mod(dt,3.14);
  return fract(sin(sn) * c);
}

highp float noise(vec2 st) {
    vec2 i = floor(st);
    vec2 f = fract(st);
    vec2 u = f * f * (3.0 - 2.0 * f);
    return mix( mix( random( i + vec2(0.0,0.0) ),
                     random( i + vec2(1.0,0.0) ), u.x),
                mix( random( i + vec2(0.0,1.0) ),
                     random( i + vec2(1.0,1.0) ), u.x), u.y);
}

如上面的代码,我们对二维随机向量的x、y方向分别进行线性插值然后叠加,就形成了二维噪声。

void main() {
  vec2 st = gl_FragCoord.xy / dd_resolution;
  st *= 10.0;
  vec2 idx = floor(st);
  float d = noise(st);
  FragColor.rgb = vec3(d);
  FragColor.a = 1.0;
}

code.juejin.cn/pen/7116394…

噪声与生成艺术

利用噪声,我们可以生成一些有趣的纹理,比如随机条纹:

float lines(in vec2 pos, float b){
  float scale = 10.0;
  pos *= scale;
  return smoothstep(0.0, 0.5 + b * 0.5, abs((sin(pos.x * 3.1415) + b * 2.0)) * 0.5);
}

vec2 rotate(vec2 v0, float ang) {
  float sinA = sin(ang);
  float cosA = cos(ang);
  mat3 m = mat3(cosA, -sinA, 0, sinA, cosA, 0, 0, 0, 1);
  return (m * vec3(v0, 1.0)).xy;
}

uniform vec2 dd_resolution;
uniform float dd_time;

void main() {
  vec2 st = gl_FragCoord.yx / dd_resolution;
  st *= vec2(10.0, 3.0);
  st = rotate(st, noise(st + 0.1 * dd_time));
  float d = lines(st, 0.5);
  FragColor.rgb = 1.0 - vec3(d);
  FragColor.a = 1.0;
}

code.juejin.cn/pen/7116400…

还有水滴效果:

void main() {
  vec2 st = mix(vec2(-10, -10), vec2(10, 10), gl_FragCoord.xy / dd_resolution);
  float d = distance(st, vec2(0));
  d *= noise(dd_time + st);
  d = smoothstep(0.0, 1.0, d) - step(1.0, d);
  FragColor.rgb = vec3(d);
  FragColor.a = 1.0;
}

code.juejin.cn/pen/7116403…

雾状

我们将二维噪声按照与自身放大叠加若干次,可以得到更加平滑的雾状效果:

  float d = 0.5 * noise(st);
  st *= 2.0;
  d += 0.25 * noise(st);
  st *= 2.0;
  d += 0.125 * noise(st);
  st *= 2.0;
  d += 0.0625 * noise(st);
  st *= 2.0;
  d += 0.0375 * noise(st);

code.juejin.cn/pen/7116420…

我们可以将它封装成如下函数:

#ifndef OCTAVES
#define OCTAVES 6
#endif

float mist(vec2 st) {
  //Initial values
  float value = 0.0;
  float amplitude = 0.5;
  float frequency = 0.0;

  // Loop of octaves
  for(int i = 0; i < OCTAVES; i++) {
    value += amplitude * noise(st);
    st *= 2.0;
    amplitude *= 0.5;
  }
  return value;
}

如上面的代码,我们按照每次放大一倍的效果将噪声叠加6次,然后通过很简单的方法就可以实现类似于空中俯瞰的效果:

void main() {
  vec2 st = gl_FragCoord.xy / dd_resolution;
  st.x += random(vec2(dd_randseed0)) + 0.1 * dd_time; 
  FragColor.rgb = hsb2rgb(vec3(mist(st), 1.0, 1.0));
  FragColor.a = 1.0;
}

code.juejin.cn/pen/7116404…

其他噪声函数

除了上面常规的二维噪声函数(被称为Value Noise),常用的噪声函数还有Gradient NoiseSimplex Noise等等,

Gradient Noise基于随机的二维向量来插值:

vec2 grad( ivec2 z )  // replace this anything that returns a random vector
{
    // 2D to 1D  (feel free to replace by some other)
    int n = z.x+z.y*11111;

    // Hugo Elias hash (feel free to replace by another one)
    n = (n<<13)^n;
    n = (n*(n*n*15731+789221)+1376312589)>>16;

#if 0

    // simple random vectors
    return vec2(cos(float(n)),sin(float(n)));
    
#else

    // Perlin style vectors
    n &= 7;
    vec2 gr = vec2(n&1,n>>1)*2.0-1.0;
    return ( n>=6 ) ? vec2(0.0,gr.x) : 
           ( n>=4 ) ? vec2(gr.x,0.0) :
                              gr;
#endif                              
}

float noise( in vec2 p )
{
    ivec2 i = ivec2(floor( p ));
     vec2 f =       fract( p );
	
	vec2 u = f*f*(3.0-2.0*f); // feel free to replace by a quintic smoothstep instead

    return mix( mix( dot( grad( i+ivec2(0,0) ), f-vec2(0.0,0.0) ), 
                     dot( grad( i+ivec2(1,0) ), f-vec2(1.0,0.0) ), u.x),
                mix( dot( grad( i+ivec2(0,1) ), f-vec2(0.0,1.0) ), 
                     dot( grad( i+ivec2(1,1) ), f-vec2(1.0,1.0) ), u.x), u.y);
}

🎯 注意,上面的Gradient Noise 实现演示了另一种获得随机数的思路(还记得上一节课我们使用sin函数的小数位来构造随机?),即使用哈希算法来构造随机数:

n = (n<<13)^n;
n = (n*(n*n*15731+789221)+1376312589)>>16;

另外,除了使用四边形网格插值的二维噪声,Simplex Noise基于三角网格插值,与四边形插值相比,三角网格插值需要计算的点更少了,这样自然大大降低了计算量,从而提升了渲染性能。

//
// Description : GLSL 2D simplex noise function
//      Author : Ian McEwan, Ashima Arts
//  Maintainer : ijm
//     Lastmod : 20110822 (ijm)
//     License :
//  Copyright (C) 2011 Ashima Arts. All rights reserved.
//  Distributed under the MIT License. See LICENSE file.
//  https://github.com/ashima/webgl-noise
//
float noise(vec2 v) {
    // Precompute values for skewed triangular grid
    const vec4 C = vec4(0.211324865405187,
                        // (3.0-sqrt(3.0))/6.0
                        0.366025403784439,
                        // 0.5*(sqrt(3.0)-1.0)
                        -0.577350269189626,
                        // -1.0 + 2.0 * C.x
                        0.024390243902439);
                        // 1.0 / 41.0

    // First corner (x0)
    vec2 i  = floor(v + dot(v, C.yy));
    vec2 x0 = v - i + dot(i, C.xx);

    // Other two corners (x1, x2)
    vec2 i1 = vec2(0.0);
    i1 = (x0.x > x0.y)? vec2(1.0, 0.0):vec2(0.0, 1.0);
    vec2 x1 = x0.xy + C.xx - i1;
    vec2 x2 = x0.xy + C.zz;

    // Do some permutations to avoid
    // truncation effects in permutation
    i = mod289(i);
    vec3 p = permute(
            permute( i.y + vec3(0.0, i1.y, 1.0))
                + i.x + vec3(0.0, i1.x, 1.0 ));

    vec3 m = max(0.5 - vec3(
                        dot(x0,x0),
                        dot(x1,x1),
                        dot(x2,x2)
                        ), 0.0);

    m = m*m ;
    m = m*m ;

    // Gradients:
    //  41 pts uniformly over a line, mapped onto a diamond
    //  The ring size 17*17 = 289 is close to a multiple
    //      of 41 (41*7 = 287)
    vec3 x = 2.0 * fract(p * C.www) - 1.0;
    vec3 h = abs(x) - 0.5;
    vec3 ox = floor(x + 0.5);
    vec3 a0 = x - ox;

    // Normalise gradients implicitly by scaling m
    // Approximation of: m *= inversesqrt(a0*a0 + h*h);
    m *= 1.79284291400159 - 0.85373472095314 * (a0*a0+h*h);

    // Compute final noise value at P
    vec3 g = vec3(0.0);
    g.x  = a0.x  * x0.x  + h.x  * x0.y;
    g.yz = a0.yz * vec2(x1.x,x2.x) + h.yz * vec2(x1.y,x2.y);
    return 130.0 * dot(m, g);
}

code.juejin.cn/pen/7116411…

我们可以用Simplex Noise结合云雾,实现更加逼真的云:

code.juejin.cn/pen/7116418…