参考了 The Book of Shaders 的主题 Random ,Noise ,Cellular noise 及 Fractional brownian motion .
随机
原理
由于 GLSL 并不能从物理世界获得熵源,随机都是伪随机。
但我们并不需要真随机,只需要看起来有随机性即可。
一个简洁的生成方法是
float res = fract ( sin ( x ) * 100000.0 ) ;
在 2D 情形下,需要采取不可约的系数(读者可自行测试 col = vec3(fract(sin(uv.x) * 100000.0 + sin(uv.y) * 100.0)); 的结果)
float random ( vec2 uv ) {
return fract ( sin ( dot ( uv. xy , vec2 ( 11.9898 , 78.233 ) ) ) * 43758.5453123 ) ;
}
vec2 random2 ( vec2 uv ) {
return fract ( sin ( vec2 ( dot ( uv, vec2 ( 127.1 , 311.7 ) ) , dot ( uv, vec2 ( 269.5 , 183.3 ) ) ) ) * 43758.5453 ) ;
}
void mainImage ( out vec4 fragColor , in vec2 fragCoord )
{
vec2 uv = fragCoord/ iResolution. xy ;
vec3 col = vec3 ( random ( uv ) ) ;
fragColor = vec4 ( col, 1.0 ) ;
}
应用技巧
可以把一个系数调得很小,产生在该方向上长条形的效果。
如果你希望保持长条形的效果但将它改为曲线,则不应在浮点数上操作,而是在原来的像素整点上操作到整点再使用。
Random 中列举了很多应用技巧,其中包括一个迷宫生成器,整理如下:
#define PI 3.14159265358979323846
float random ( vec2 uv ) { ... }
vec2 transform ( vec2 uv , float rnd ) {
if ( rnd > 0.5 ) uv. x = 1.0 - uv. x ;
if ( mod ( rnd, 0.5 ) > 0.25 ) uv. y = 1.0 - uv. y ;
return uv;
}
void mainImage ( out vec4 fragColor , in vec2 fragCoord ) {
vec2 uv = fragCoord. xy / iResolution. y * 20.0 ;
vec2 tile = transform ( fract ( uv ) , random ( floor ( uv ) ) ) ;
float color = abs ( tile. x - tile. y ) < 0.1 ? 1.0 : 0.0 ;
fragColor = vec4 ( vec3 ( color) , 1.0 ) ;
}
噪声
上述的随机生成确实保证了随机性,类似于白噪声,但我们通常希望看到更平滑的图像。
噪声是算法生成的、具有随机性的图像,可以用于生成地形、纹理。
2D 噪声
我们来看 Perlin 噪声 的 2D 版本的代码:
float perlin ( vec2 st ) {
vec2 i = floor ( st ) ;
vec2 f = fract ( st ) ;
float a = random ( i ) ;
float b = random ( i + vec2 ( 1.0 , 0.0 ) ) ;
float c = random ( i + vec2 ( 0.0 , 1.0 ) ) ;
float d = random ( i + vec2 ( 1.0 , 1.0 ) ) ;
vec2 u = smoothstep ( 0.0 , 1.0 , f) ;
return mix ( a, b, u. x ) +
( c - a) * u. y * ( 1.0 - u. x ) +
( d - b) * u. x * u. y ;
}
这里 mix 函数用于插值,表达式为 mix(x, y, a) = x * (1 - a) + y * a
整个函数的思路是:划分成一个个单元格,格的顶点被赋予随机的值,而后格内部进行平滑的插值。
但是现在看起来有明显的方形轮廓。
对此,有以下优化思路:
对格的顶点赋予向量值,做两次插值
另外可以进行优化:将 smoothstep 中调用的 $\mathrm{lerp}(x) = 3x^2-2x^3$ 改为 $6x^5-15x^4+10x^3$
进行局部随机化
应用技巧
封面图的代码如下,使用了局部随机化(这产生了蜡笔效果),并引入了时间维度:
vec3 hsv2rgb ( vec3 c ) { ... }
float random ( vec2 uv ) {
return fract ( sin ( dot ( uv. xy , vec2 ( 11.143 , 78.233 ) ) ) * ( 43758.5453123 + iTime) ) ;
}
float perlin ( vec2 st ) { ... }
void mainImage ( out vec4 fragColor , in vec2 fragCoord )
{
vec2 uv = ( fragCoord * 2.0 - iResolution. xy ) / iResolution. y ;
vec2 uv2 = vec2 ( uv. x * 20.0 , uv. y * 1000.0 ) ;
float fr = perlin ( uv2 + random ( uv2 ) ) ;
float t = smoothstep ( 0.0 , 1.0 , fr) ;
vec3 col = t * hsv2rgb ( vec3 ( ( 1.0 - uv. y ) / 2.0 , 1.0 , 1.0 ) ) + ( 1.0 - t) * vec3 ( 1.0 ) ;
fragColor = vec4 ( col, 1.0 ) ;
}
Noise 中列举了更多应用实例。
时间优化
Simplex 噪声是对 Perlin 噪声的优化(在高维情形下,Perlin 的时间复杂度是 $O(2^n)$)。
我们以单形为晶格(在 2D 中是三角形,在 3D 中是四面体)。
这些单形是从立方晶格中裁得的。
元胞噪声
原理
元胞噪声可以产生类似细胞的效果。
对每个点,可以赋予一个距离场,例如取为到给定四点的距离的最小值。
Worley 噪声
现在对每个点,我们考虑它所在的方格及周围的 8 个方格。
我们随机地在每个方格中取一个固定的点,即可使用前述方法。
vec2 random2 ( vec2 uv ) { ... }
float worley ( vec2 uv ) {
vec2 grid = floor ( uv ) ;
vec2 uv2 = fract ( uv ) ;
float m_dist = 1 .;
for ( int y= - 1 ; y <= 1 ; y+ + ) {
for ( int x= - 1 ; x <= 1 ; x+ + ) {
vec2 neighbor = vec2 ( float ( x) , float ( y) ) ;
vec2 point = random2 ( grid + neighbor ) ;
float dist = length ( neighbor + point - uv2 ) ;
m_dist = min ( m_dist, dist ) ;
}
}
return m_dist;
}
void mainImage ( out vec4 fragColor , in vec2 fragCoord ) {
vec2 uv = fragCoord. xy / iResolution. y * 8.0 ;
fragColor = vec4 ( vec3 ( worley ( uv ) ) , 1.0 ) ;
}
Voronoi 噪声
改为生成直边界。
这是通过记录最近的点。
float voronoi ( vec2 uv ) {
vec2 grid = floor ( uv ) ;
vec2 uv2 = fract ( uv ) ;
float m_dist = 1 .;
vec2 closest;
for ( int y = - 1 ; y <= 1 ; y+ + ) {
for ( int x = - 1 ; x <= 1 ; x+ + ) {
vec2 neighbor = vec2 ( float ( x) , float ( y) ) ;
vec2 point = random2 ( grid + neighbor ) ;
float dist = length ( neighbor + point - uv2 ) ;
if ( dist < m_dist) {
m_dist = dist;
closest = point;
}
}
}
return closest. x ;
}
分形化
分形 Brownian 运动
Fractional Brownian Motion 一般被表为:
$$\mathrm{fbm}(x, y) = \sum_{i=1}^n w^i \cdot \mathrm{noise}(s^ix, s^iy)$$
其中取 $w=\frac{1}{2}, s=2$,此时称每次迭代为 octave.
有以下例子:
float random ( vec2 uv ) { ... }
float perlin ( vec2 uv ) { ... }
float fbm ( vec2 uv ) {
float value = 0.0 ;
float amplitude = .5 ;
float frequency = 0 .;
for ( int i = 0 ; i < 6 ; i+ + ) {
value += amplitude * perlin ( uv ) ;
uv *= 2 .;
amplitude *= .5 ;
}
return value;
}
void mainImage ( out vec4 fragColor , in vec2 fragCoord ) {
vec2 uv = fragCoord. xy / iResolution. y * 8.0 ;
fragColor = vec4 ( vec3 ( fbm ( uv * 3.0 ) ) , 1.0 ) ;
}
更多内容可参考 Inigo Quilez 的经典作品 Rainforest .