柏林噪声
前言
柏林噪声主要用于生成平滑的地形、特效等
关于为什么要用柏林噪音,单纯的随机并不能适用于生成地形等场合,那会使得效果十分奇怪,忽高忽低,毫无美感
而柏林噪音就像名字般,靠模拟噪声来实现平滑
柏林噪音可以有多维,其本质都是一样的
例如:2D可用于创建平面物品纹路,3D可用于创建地形,4D则可将w轴化为时间创建动画(例如火焰动画)
思想
使用3D作为样例
对于一个浮点数 \(x\) 求出其所在的正方体(满足顶点坐标均为整数)
随后为正方体8个顶点生成梯度向量
梯度向量代表该顶点相对单元正方形内某点的影响是正向还是反向的(向量指向方向为正向,相反方向为反向)
3D一般使用以下十二个梯度向量
(1,1,0),(-1,1,0),(1,-1,0),(-1,-1,0),(1,0,1),(-1,0,1),(1,0,-1),(-1,0,-1), (0,1,1),(0,-1,1),(0,1,-1),(0,-1,-1)
生成出梯度向量再将其平滑的处理回一个值 (坐标满足 \(perlin(x, y, z) \in [0, 1]\) )
实现方法
Part.1
首先由于输入范围可能巨大无比,所以需要一个人为规定的变量 \(repeat\) 用于限定范围,并求出其在哪个正方体中与在正方体中的相对位置
关于 & 255
后面再提
public double perlin(double x, double y, double z)
{
if(repeat > 0)
{ // If we have any repeat on, change the coordinates to their "local" repetitions
x = x % repeat;
y = y % repeat;
z = z % repeat;
}
int xi = (int)x & 255; // Calculate the "unit cube" that the point asked will be located in
int yi = (int)y & 255; // The left bound is ( |_x_|,|_y_|,|_z_| ) and the right bound is that
int zi = (int)z & 255; // plus 1. Next we calculate the location (from 0.0 to 1.0) in that cube.
double xf = x - (int)x;
double yf = y - (int)y;
double zf = z - (int)z;
// ...
}
获得了坐标后我们考虑如何获取值
使用相对坐标用某种方式求出立方体中的一个点
并用其作为权值求出函数值
Part.2
因为我们要模拟噪声,所以生成出的函数不能是线性的,而此时一种函数 \(fade\) 便可以实现此要求
\[fade(x) = 6 \times x^5 - 15 \times x^4 + 10 \times x^3 \]public static double fade(double t) {
// Fade function as defined by Ken Perlin. This eases coordinate values
// so that they will ease towards integral values. This ends up smoothing
// the final output.
return t * t * t * (t * (t * 6 - 15) + 10); // 6t^5 - 15t^4 + 10t^3
}
public double perlin(double x, double y, double z) {
// ...
double u = fade(xf);
double v = fade(yf);
double w = fade(zf);
// ...
}
此时我们的梯度向量就有用了
Part.3
在生成向量之前需要一些准备工作
需要一个排列表存储一个 \(0 - 255\) 的排列,为防止缓存溢出,需要复制一份,总长512
private static readonly int[] permutation = { 151,160,137,91,90,15, // Hash lookup table as defined by Ken Perlin. This is a randomly
131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23, // arranged array of all numbers from 0-255 inclusive.
190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,
88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166,
77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,
102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196,
135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123,
5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,
223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9,
129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228,
251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107,
49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254,
138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180
};
private static readonly int[] p; // Doubled permutation to avoid overflow
static Perlin() {
p = new int[512];
for(int x = 0; x < 512; x++) {
p[x] = permutation[x % 256];
}
}
现在已经有了一个排列表了,接下来便使用Hash来生成梯度向量
public double perlin(double x, double y, double z) {
// ...
int aaa, aba, aab, abb, baa, bba, bab, bbb;
aaa = p[p[p[ xi ]+ yi ]+ zi ];
aba = p[p[p[ xi ]+inc(yi)]+ zi ];
aab = p[p[p[ xi ]+ yi ]+inc(zi)];
abb = p[p[p[ xi ]+inc(yi)]+inc(zi)];
baa = p[p[p[inc(xi)]+ yi ]+ zi ];
bba = p[p[p[inc(xi)]+inc(yi)]+ zi ];
bab = p[p[p[inc(xi)]+ yi ]+inc(zi)];
bbb = p[p[p[inc(xi)]+inc(yi)]+inc(zi)];
// ...
}
public int inc(int num) {
num++;
if (repeat > 0) num %= repeat;
return num;
}
inc(int num)
仅为传入的数+1
现在已经获取到了Hash值,接下来便利用Hash值生成向量
注:以下代码为人类可阅代码版本
public static double grad(int hash, double x, double y, double z)
{
switch(hash & 0xF)
{
case 0x0: return x + y;
case 0x1: return -x + y;
case 0x2: return x - y;
case 0x3: return -x - y;
case 0x4: return x + z;
case 0x5: return -x + z;
case 0x6: return x - z;
case 0x7: return -x - z;
case 0x8: return y + z;
case 0x9: return -y + z;
case 0xA: return y - z;
case 0xB: return -y - z;
case 0xC: return y + x;
case 0xD: return -y + z;
case 0xE: return y - x;
case 0xF: return -y - z;
default: return 0; // never happens
}
}
最后,使用线性插值处理以下,便可以完成了
public double perlin(double x, double y, double z) {
// ...
double x1, x2, y1, y2;
x1 = lerp( grad (aaa, xf , yf , zf), // The gradient function calculates the dot product between a pseudorandom
grad (baa, xf-1, yf , zf), // gradient vector and the vector from the input coordinate to the 8
u); // surrounding points in its unit cube.
x2 = lerp( grad (aba, xf , yf-1, zf), // This is all then lerped together as a sort of weighted average based on the faded (u,v,w)
grad (bba, xf-1, yf-1, zf), // values we made earlier.
u);
y1 = lerp(x1, x2, v);
x1 = lerp( grad (aab, xf , yf , zf-1),
grad (bab, xf-1, yf , zf-1),
u);
x2 = lerp( grad (abb, xf , yf-1, zf-1),
grad (bbb, xf-1, yf-1, zf-1),
u);
y2 = lerp (x1, x2, v);
return (lerp (y1, y2, w)+1)/2; // For convenience we bind the result to 0 - 1 (theoretical min/max before is [-1, 1])
}
// Linear Interpolate
public static double lerp(double a, double b, double x) {
return a + x * (b - a);
}