一.噪声定义
大理石表面的花纹、流沙、水墨画、山脉起伏、光照的海面等,这些纹理不可预测性叫随机,但是这种随机又不同于我们常规程序中用到的随机。计算机中用机器来绘制这些图就会运用到噪声,噪声可以定义为一个随机数生成器 ;但是这种随机是有规律的随机,看似杂乱无章实则有序,因此可以构造一些丰富逼真的图片。
二.噪声的生成
一维使用noise
float rand (in float _x) {
return fract(sin(_x)*1e4);
}
float i = floor(x); // 整数(i 代表 integer)
float f = fract(x); // 小数(f 代表 fraction)
y = rand(i);
我们把float类型的x分割成整数和小数部分,然后用rand把分割出来的整数部分生成一个随机值,效果如下:
这样当x取整的值相等时,即相同的i得到的y值相同,就会看到连续的水平线
既然x是一个小数的话,那我们想到是否能够用小数部分来做一个线性插值呢
如下:
y = mix(rand(i), rand(i + 1.0), f);
因f值呈线性变化,因此会出现折线。
曲线太过于尖锐的时候我们会想到前面文章中经常使用的smoothstep来使曲线平滑的过渡,看一下效果:
y = mix(rand(i), rand(i + 1.0), smoothstep(0.,1.,f));
在应用smoothstep之后我们注意顶点的变化变的顺滑了起来。
噪声实际运用中,会发现大部分的代码会用三次多项式来代替smoothstep
float u = f * f * (3.0 - 2.0 * f ); //其实smoothstep源码就是用了这个三次多项式
y = mix(rand(i), rand(i + 1.0), u);
三.梯度噪声
梯度噪声可用于产生连续性的纹理,所以经常用来模拟山脉,云朵等具有连续性的物质。著名的柏林噪声就是梯度噪声的代表
算法步骤
梯度噪声是通过多个随机梯度相互影响计算得到,通过梯度向量的方向与片元的位置计算噪声值。
二维梯度噪声示意图
以2d举例,主要分为四步:
1.网格生成
我们将2d平面分成mxn个大小相同的网格,具体数值取决于我们生成的纹理密度
GLSL中生成网格很简单 将要噪声化的点乘以一个scale即可 如uv *= SCALE
2.随机梯度的生成
梯度向量生成,这一步是根据第一步生成的网格的顶点来产生随机向量,四个顶点就有四个梯度向量
// 输入网格顶点位置,输出随机向量
vec2 random(vec2 p){
return -1.0 + 2.0 * fract(
sin(
vec2(
dot(p, vec2(127.1,311.7)),
dot(p, vec2(269.5,183.3))
)
) * 43758.5453
);
}
如上,借用三角函数sin来生成随机值,输入网格顶点坐标,输出随机变量
3.梯度点乘
这一步是通过计算四个梯度向量对当前片元点P的影响,主要先求出点P到四个顶点的距离向量,然后和对应的梯度向量进行点积。
如上图,网格内的片元点P的四个顶点距离向量为a1, a2, a3, a4。
此时将距离向量与梯度向量g1, g2, g3, g4进行点积运算:c[i] = a[i] · g[i];
4.平滑插值
得到四个点乘结果后,我们对其进行线性叠加,使用smoothstep方法来平滑网格的边界,最终得到当前片元的噪声值。
代码如下:
float noise_perlin (vec2 p) {
vec2 i = floor(p); // 获取当前网格索引i
vec2 f = fract(p); // 获取当前片元在网格内的相对位置
float a = dot(random(i),f); //周围四个点的梯度向量与距离向量点积运算
float b = dot(random(i + vec2(1., 0.)),f - vec2(1., 0.));
float c = dot(random(i + vec2(0., 1.)),f - vec2(0., 1.));
float d = dot(random(i + vec2(1., 1.)),f - vec2(1., 1.));
// 平滑插值
vec2 u = smoothstep(0.,1.,f);
// 叠加点乘结果
return mix(mix(a,b,u.x),mix(c,d,u.x),u.y);
}
经典柏林噪声
看懂了上面的算法步骤之后,我们再来看看经典柏林噪声的计算
vec4 mod289(vec4 x)
{
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
vec4 permute(vec4 x)
{
return mod289(((x*34.0)+10.0)*x);
}
vec4 taylorInvSqrt(vec4 r)
{
return 1.79284291400159 - 0.85373472095314 * r;
}
vec2 fade(vec2 t) {
return t*t*t*(t*(t*6.0-15.0)+10.0);
}
// Classic Perlin noise
float cnoise(vec2 P)
{
vec4 Pi = floor(P.xyxy) + vec4(0.0, 0.0, 1.0, 1.0);
vec4 Pf = fract(P.xyxy) - vec4(0.0, 0.0, 1.0, 1.0);
Pi = mod289(Pi); // To avoid truncation effects in permutation
vec4 ix = Pi.xzxz; //(0,1,0,1)
vec4 iy = Pi.yyww; //(0,0,1,1)
vec4 fx = Pf.xzxz;
vec4 fy = Pf.yyww;
vec4 i = permute(permute(ix) + iy);
vec4 gx = fract(i * (1.0 / 41.0)) * 2.0 - 1.0 ;
vec4 gy = abs(gx) - 0.5 ;
vec4 tx = floor(gx + 0.5);
gx = gx - tx;
vec2 g00 = vec2(gx.x,gy.x); //四个随机梯度向量 (0,0)点
vec2 g10 = vec2(gx.y,gy.y); // (1,0)点
vec2 g01 = vec2(gx.z,gy.z); // (0,1)点
vec2 g11 = vec2(gx.w,gy.w); // (1,1)点
vec4 norm = taylorInvSqrt(vec4(dot(g00, g00), dot(g01, g01), dot(g10, g10), dot(g11, g11)));
g00 *= norm.x; //归一化 消除正方形方向性的偏差
g01 *= norm.y;
g10 *= norm.z;
g11 *= norm.w;
float n00 = dot(g00, vec2(fx.x, fy.x)); //梯度向量和距离向量点乘 (0,0)点
float n10 = dot(g10, vec2(fx.y, fy.y)); // (1,0)点
float n01 = dot(g01, vec2(fx.z, fy.z)); // (0,1)点
float n11 = dot(g11, vec2(fx.w, fy.w)); // (1,1)点
vec2 fade_xy = fade(Pf.xy);
vec2 n_x = mix(vec2(n00, n01), vec2(n10, n11), fade_xy.x);
float n_xy = mix(n_x.x, n_x.y, fade_xy.y);
return 2.3 * n_xy;
}
可以看出,经典的柏林算法的代码看起来更加规范化和可扩展化,与之前的区别是添加了梯度的归一化,可以消除正方向方向性的偏差。
应用示例:
这里下面的山脉起伏效果就是用的经典柏林噪声来实现
四.三维柏林噪声
同理,三维柏林噪声和二维类似,计算量不同而已。二维是正方形,相邻的晶格点有四个,那三维就是8个,n维有2n个。
同样,计算该点到各个晶格点的距离向量,再分别与顶点上的梯度向量做点乘,得到2n个点乘结果。然后再使用缓和曲线来计算他们的权重和。这里就不再做详细讲解。