GLSL From Scratch 01 - Perlin噪声生成

0. 在线 Demo

Value Noise: https://aeroraven.github.io/altina-cg-a/index.html?stage=value_noise

Perlin Noise: https://aeroraven.github.io/altina-cg-a/index.html?stage=perlin_noise

1. 伪随机数生成

Shader中不提供随机数函数,要实现随机数的效果,只能够定义一个函数$y=f(x)$,通过设计$f$,使其输入和输出看起来随机。常用生成函数类似于下列形式:
$$
y=fract(sin(ax+b)*c)
$$
其中$a,b,c$为自定义参数,$fract$为取小数函数

除了自变量外的参数构成这一个“随机数”生成器的“种子”。参数固定后,对于固定的输入,函数总是提供固定输出。实践中的输入$x$可以是世界坐标等等任意值,只要确保一个每一个像素的输入$x$不同即可。

上述的函数并不是唯一的,可以任意设置,只要看起来有随机性就行

1
2
3
4
float randomScalar(vec3 worldPos){
float d = dot(vec3(114.1919, 514.810, 37.719),sin(worldPos));
return fract(sin(d)*114514.1919810);
}

2. 白噪声 White Noise

有了随机函数,很容易就能生成白噪声图片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#version 300 es

precision highp float;

in highp vec3 vPos;

out vec4 fragmentColor;

float randomScalar(vec3 worldPos){
float d = dot(vec3(114.1919, 514.810, 37.719),sin(worldPos));
return fract(sin(d)*114514.1919810);
}

void main(){
fragmentColor = vec4(vec3(randomScalar(vPos)),1.0);
}

3. 值噪声 Value Noise

但是噪声过于随机,要平滑一点的噪声怎么办呢?这时,就需要一种新的算法了。

在生成Value Noise前,需要对当前画面划分成$N\times M$个格点,每一个$<i,j>$格点的坐标位于画面位置$P_{i,j}=(Wi/N,Hj/M)$上。按照之前的随机数,为每一个点赋一个随机数,作为噪声强度$M_{i,j}=f(P_{i,j})$。(注:此处认为画面长宽均为1)

Value Noise的思想是,给定一个像素点坐标$P$,找到距离其最近的四个格点坐标,之后按照到这四个点距离进行插值,插值结果作为$P$的噪声强度$M$。

先来实现第一步,划分格点,确定当前着色像素$P$的邻近四个格点。如下方代码,得到gridMinPos gridDeltaPos后,四个邻近格点即被确定。

1
2
3
4
5
6
7
8
9
10
11

float noiseFunc(vec3 worldPos){
float splitPartsf = float(splitParts);
vec3 magPos = splitPartsf * worldPos;
vec3 gridMinPos = floor(magPos)/splitPartsf;
vec3 gridDeltaPos = vec3(1.0,1.0,0.0)/splitPartsf;
vec3 gridDist = magPos - floor(magPos);

...
}

由于随机函数$f$在输入相同时,输出是定值。因此计算出相邻四点噪声强度。

1
2
3
4
float noise00 = randomScalar(gridMinPos+vec3(0.0,0.0,0.0));
float noise10 = randomScalar(gridMinPos+vec3(gridDeltaPos.x,0.0,0.0));
float noise01 = randomScalar(gridMinPos+vec3(0.0,gridDeltaPos.y,0.0));
float noise11 = randomScalar(gridMinPos+vec3(gridDeltaPos.x,gridDeltaPos.y,0.0));

之后考虑插值问题,最简单插值可以使用线性插值,即
$$
lerp(s,t,v)=(1-v)s+vt,\forall v\in[0,1]^n
$$
但是有更多方案,对输入$v\in [0,1]^n$,映射至$y=g(v)\in[0,1]^n$,此时插值可以写成下列形式。线性插值为$g(n)$是恒等变换时取得。
$$
lerp(s,t,v)=(1-g(v))s+g(v)t,\forall v\in[0,1]^n
$$
相比于恒等变换,$g(v)$选取有一个更好的方案[1]
$$
g(v)=6t^5-15t^4+10t^3
$$
这时,我们可以实现这一个插值过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
float perlinLerp_t(float x){
float x2 = x*x;
float x3 = x2*x;
return x3*(6.0*x2-15.0*x+10.0);
}

float perlinLerp(float s,float t,float x){
float p = perlinLerp_t(x);
return (1.0-p)*s+p*t;
}

float lerp(float s,float t,float x){
return perlinLerp(s,t,x);
}

之后,即可以实现对四点噪声强度插值

1
2
3
float noiseXIntp0 = lerp(noise00,noise10,gridDist.x);
float noiseXIntp1 = lerp(noise01,noise11,gridDist.x);
float noiseYIntp = lerp(noiseXIntp0,noiseXIntp1,gridDist.y);

4. 分型值噪声 Fractal Value Noise

上述噪声函数可以看成$y=Noise(x)$,将噪声函数的频率和幅度进行变换,如
$$
y_n(x)=\frac{1}{2^n}Noise(2^nx)
$$
之后将多个变换结果叠加即可产生分型噪声的效果
$$
y_f=\sum_{i=0}^ny_n(x)
$$

1
2
3
4
5
6
7
8
9
10
11
12
13
float fractalNoiseFunc(vec3 worldPos){
float freq = 1.0;
float ampl = 1.0;
float res = 0.0;
float sampl = 0.0;
for(int i=0;i<5;i=i+1){
res+=noiseFunc(worldPos*freq)*ampl;
freq = freq*2.0;
sampl += ampl;
ampl = ampl*0.5;
}
return res/sampl;
}

5. Perlin Noise

Value Noise直接在格点$<i,j>$上附加噪声强度,而Perlin Noise则在格点$<i,j>$上附加一个随机单位向量$M_{i,j}=f(P_{i,j})$

对于每一个正在着色的像素点$P$,其相邻四个格点坐标记作$P_i,i\in{1,2,3,4}$,则第$i$个格点对$P$而言的噪声强度计算过程如下:

  1. 计算到格点$i$的方向向量,$N_i=normalize(P-P_i)$,
  2. 将$P_i$的随机向量$M_i$和$N_i$点乘,得到$P$的噪声强度

之后,以和值噪声一样的方法进行插值即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

float noiseFunc(vec3 worldPos){
float splitPartsf = float(splitParts);
vec3 magPos = splitPartsf * worldPos;
vec3 gridMinPos = floor(magPos)/splitPartsf;
vec3 gridDeltaPos = vec3(1.0,1.0,0.0)/splitPartsf;
vec3 gridDist = magPos - floor(magPos);

vec3 p00 = gridMinPos+vec3(0.0,0.0,0.0);
vec3 p10 = gridMinPos+vec3(gridDeltaPos.x,0.0,0.0);
vec3 p01 = gridMinPos+vec3(0.0,gridDeltaPos.y,0.0);
vec3 p11 = gridMinPos+vec3(gridDeltaPos.x,gridDeltaPos.y,0.0);

vec3 dir00 = normalize(worldPos-p00);
vec3 dir01 = normalize(worldPos-p01);
vec3 dir10 = normalize(worldPos-p10);
vec3 dir11 = normalize(worldPos-p11);

vec3 grad00 = normalize(randomVec2(p00));
vec3 grad01 = normalize(randomVec2(p01));
vec3 grad10 = normalize(randomVec2(p10));
vec3 grad11 = normalize(randomVec2(p11));

float noise00 = dot(dir00,grad00);
float noise10 = dot(dir10,grad10);
float noise01 = dot(dir01,grad01);
float noise11 = dot(dir11,grad11);

float noiseXIntp0 = lerp(noise00,noise10,gridDist.x);
float noiseXIntp1 = lerp(noise01,noise11,gridDist.x);
float noiseYIntp = lerp(noiseXIntp0,noiseXIntp1,gridDist.y);
return (noiseYIntp+1.0)/2.0;
}

参考资料

[1] https://zhuanlan.zhihu.com/p/77596796


GLSL From Scratch 01 - Perlin噪声生成
https://aeroraven.github.io/2022/10/18/cg-glsl-1-perlin-noise/
Author
Aeroraven
Posted on
October 18, 2022
Licensed under