在上一节里我们聊了随机,随机是模拟世界的无序和不可预测性。
然而实际上,真正的大自然在无序中孕育着秩序。空气看似无序的扰动,产生了气流,气流汇聚形成季风;水波看似无序,然而产生水流,形成河流、洋流;岩石的生长看似无序,但形成山脉,连绵不断……随机无处不在,秩序也无处不在。
这次褐蚁来到故地,只是觅食途中偶然路过而已。它来到孤峰脚下,用触须摸了摸这顶天立地的存在,发现孤峰的表面坚硬光滑,但能爬上去,于是它向上爬去。没有什么目的,只是那小小的简陋神经网络中的一次随机扰动所致。这扰动随处可见,在地面的每一株小草和草叶上的每一粒露珠中,在天空中的每一片云和云后的每一颗星辰上……扰动都是无目的的,但巨量的无目的扰动汇集在一起,目的就出现了。 —— 《三体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;
}
上面这段代码是我们上一节学到的随机,我们利用它来绘制出高低不平的白色条纹。
接下来,我们修改一下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;
}
这样就形成下面的效果,这种效果就叫做噪声。
注意这里我们用了三次平滑曲线 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;
}
噪声与生成艺术
利用噪声,我们可以生成一些有趣的纹理,比如随机条纹:
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;
}
还有水滴效果:
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;
}
雾状
我们将二维噪声按照与自身放大叠加若干次,可以得到更加平滑的雾状效果:
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);
我们可以将它封装成如下函数:
#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;
}
其他噪声函数
除了上面常规的二维噪声函数(被称为Value Noise),常用的噪声函数还有Gradient Noise
和Simplex 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);
}
我们可以用Simplex Noise
结合云雾,实现更加逼真的云: