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$而言的噪声强度计算过程如下:
- 计算到格点$i$的方向向量,$N_i=normalize(P-P_i)$,
- 将$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