Perlin noise(二)
通过前面两篇文章,我们现在对噪声已经有了初步的了解,对Perlin噪声生成原理也进行了阐述,现在是时候开始学习噪声生成了,即将进入程序纹理这个神秘而又让人觊觎的国度了。
(一)、1D Perlin Noise
前面我们是以2D Perlin 噪声来讲解Perlin噪声的原理的,对于1D Perlin噪声,我们首先要明白,1D Perlin噪声就是点,通过插值我们可以得到线。对1D Perlin噪声来说,我们不需要晶格,或者说我们只需要一个点就可以了,但Perlin噪声生成的流程我们都得过一遍。回顾一下2D Perlin噪声生成,第一步我们得到晶格的顶点梯度向量,第二步得到距离向量并将距离向量与梯度向量作点积,第三步我们对得到的点做插值从而得到二维纹理。对1D Perlin噪声也是一样。
第一步:生成伪随机梯度值(Pseudo-Random Number Generator(PRNG ))。对1D Perlin噪声来说,在晶格上他只有一个点,所以只需要一个随机梯度值,根据我们之前说的,伪随机梯度值其实并不是完全随机的值,他需要保持一定的稳定性,对于给定的输入要求得到确实的输出。因此对我们来说Math.random()并不是最好的选择,在前面的文章中,我们学习了LCG(Linear Congruential Generator),正好在这里派上用场。在这里,我们用一个随机数初始化Z,但在下一次计算的时候就会用上一次生成的Z来取代这个随机Z,同时,我们将返回值限制在[0,1]这个区间内,为方便使用,我们将PRNG单独写成一个类。
public class PRNG
{
static long M = 4294967296;
static long A = 1664525;
static long C = 1;
static long Z;
public PRNG()
{
Random rdm = new Random();
Z = (long)Math.Floor((rdm.NextDouble() * M));
}
public double Next()
{
Z = (A * Z + C) % M;
return (double)Z / (double)M;
}
}
我们知道LCG输出其实是跟上一个输出有关系的,在这里我们使用了Math.random(),在参数选择上,我们选择了最大的 uint 值作为M的值,M值越大,生成随机数重复的可能性就越小。好了,现在梯度向量我们已经生成了。
第二步:生成距离向量,在1D perlin噪声中,使用的晶格只有一个点,因此没有办法计算这个距离向量,这里我们将距离向量看成是1,点积后不改变梯度向量即可。
第三步:插值。根据前面所学,插值方法多种多样,这里我们选择Cosine插值算法进行插值处理,兼顾计算量与平滑度。
private double Cos_Interpolate(double a, double b, double t)
{
double ft = t * Math.PI;
t = (1 - Math.Cos(ft)) * 0.5;
return a * (1 - t) + t * b;
}
至此,所有准备工作都已准备了,剩下的就是把这些整合起来得到最终的1D perlin 噪声了。
public int Perlin1D(int x)
{
Wavelength = (int)(1.0f / Frequency);
double y;
if (x % Wavelength == 0)
{
pointA = pointB;
pointB = myPRNG.Next();
y = pointA * Amplitude;
}
else
{
y = Cos_Interpolate(pointA, pointB, (double)(x % Wavelength) / Wavelength) * Amplitude;
}
return (int)y;
}
简单解释一下代码,首先通过频率得到波长,得到波长的目的是在合适的点生成Perlin噪声点,其他点则使用插值法进行计算,现在1D perlin噪声我们就生成了,其余的工作就是将生成的曲线在界面上表现出来,不再赘述(代码在本文末,界面我们使用GDI+来画,代码在VS2015+WIN10上测试通过,以下同)。生成的1D Perlin噪声如下图所示:
这张图是完全用离散的点来画的,当然,我们也可以在点与点之间用直线连接起来形成连续的曲线。上面这张图看起来很平滑,也就是说缺乏细节,我们可以使用分形布朗运动方法生成带有丰富细节的曲线,下面这张图就是octaves = 4时的分形曲线。
1D Perlin噪声可以用来扭曲曲线,增加曲线或图形的随机感,稍后我们将会学习到。1D Perlin噪声生成相对简单,因为没有距离向量的参与让事情简单不少,1D Perlin噪声生成的关键是PRNG,使用LCG保证了PRNG的相关性,所以生成的点不是完全随机而是有一定规律的点。
(二)、2D Perlin Noise
1D Perlin噪声因为没有距离的参与让事情简单不少,在学完1D Perlin噪声之后,现在是时候学习2D Perlin噪声了,我们遵照Perlin噪声生成原理,一步一步的实现之。
第一步:生成伪随机梯度值PRNG ,在1D中,我们使用了LCG来生成PRNG,在2D中,我们不再使用这种方法,我们采用在单位圆上分布均匀的单位向量作为PRNG值,对2D Perlin噪声来说,8或16个梯度值就基本能满足要求了。这里我们使用8个梯度值。我们让这8个梯度向量在圆上均匀分布。
static float[,] gradients;
gradients = new float[8,2];
private void GenerateGradients()
{
for (int i = 0; i < 8; ++i)
{
gradients[i,0] = (float)Math.Cos(0.785398163f * (float)i); // ( 2 * PI / 8) * i
gradients[i,1] = (float)Math.Sin(0.785398163f * (float)i);
}
}
gradients[i,0] = (float)Math.Cos(0.785398163f * (float)i) 这行代码就是计算梯度向量的x分量值,0.785398163f = ( 2 * PI / 8) ,这是把圆平均分成了8份。然后我们依次取各向量的值作为梯度向量。好了,现在梯度向量有了,如何选择他们呢?也就是说对于每一个顶点,我们该选择哪一个值作为他的梯度向量呢?根据前文叙述我们知道这个梯度值不能随便选择,得保持一定的稳定性,对于给定的值,梯度向量应该选择同一个值,这样才能保证生成的Perlin噪声的连续性。对此,我们先初始化一个排列permutation,然后我们初始的时候我们打乱这个排列表以得到不同的结果。这个排列表permutation也是Perlin本人给出的,这里我们只是拿过来用。
private static readonly int[] permutation = { 151,160,137,91,90,15,
131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,
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 int[] p;
p = new int[256];
Random rdm = new Random();
int j;
for (int x = 0; x < 256; x++)
{
j = (int)rdm.Next(256);
p[x] = permutation[j];
}
有了排列表,我们就可以根据输入值所在晶格选择相应的梯度值了。选择梯度值的代码如下:
grad11 = p[(Xi + p[Yi & 255]) & 255] & 7;
grad12 = p[(Xi + 1 + p[Yi & 255]) & 255] & 7;
grad21 = p[(Xi + p[(Yi + 1) & 255]) & 255] & 7;
grad22 = p[(Xi + 1 + p[(Yi + 1) & 255]) & 255] & 7;
grad11,grad12,grad21,grad22分别代表输入点所在晶格四个顶点的梯度值索引,得到排列值后我们让他与7相与,保证梯度值索引小于等7(因为我们只有8个梯度值)。有了梯度值索引后,我们通过gradients就可以得到梯度值了。
第二步:生成距离向量。根据前文的生成原理,我们要得到距离向量,我们首先要得到输入点所在的晶格四个点的索引值,这个通过取输入点的Foor值就可以得到。输入点在所在晶格(四边形)的位置其实就是输入点的小数部分,得到位置用他去减晶格四个索引值就得一距离向量了。
Xi = GetFloor((float)x * Frequency); //x,y为输入值
Yi = GetFloor((float)y * Frequency);
fracX = (float)x * Frequency - (float)Xi; //x * Frequency是为了将输入值与频率关联起来。
fracY = (float)y * Frequency - (float)Yi;
得到了距离向量和梯度值,我们将这两个值作点积。
noise11 = DotProduct(gradients[grad11,0], gradients[grad11, 1], fracX, fracY);
noise12 = DotProduct(gradients[grad12,0], gradients[grad12, 1], fracX - 1.0f, fracY);
noise21 = DotProduct(gradients[grad21,0], gradients[grad21, 1], fracX, fracY - 1.0f);
noise22 = DotProduct(gradients[grad22,0], gradients[grad22, 1], fracX - 1.0f, fracY - 1.0f);
因为是2D,总共有四个点,所以点积后得到四个值,得到四个值后我们对这四个值进行双向性插值,这就是第三步了。
第三步:插值。我们先得到插值参数,这个利用改进的Perlin算法缓和曲线就可以实现,不再赘述。
fracX = Fade(fracX); //复用了变量fracX ,fracY
fracY = Fade(fracY);
interpolatedx1 = (float)Cos_Interpolate(noise11, noise12, fracX);
interpolatedx2 = (float)Cos_Interpolate(noise21, noise22, fracX);
return (float)Cos_Interpolate(interpolatedx1, interpolatedx2, fracY) * Amplitude;
好了,至此,2D Perlin噪声生成完毕,剩下的工作就是将其显示出来(或者保存为文件),这里不再赘述。生成图片的时候我们对颜色进行了一些处理,所以看起来跟之前看到的2D Perlin噪声图像有一点不一样。
同样,我们可以使用分形布朗运动方法生成带有丰富细节的图像,下面这张图就是octaves = 4时的分形图像。
我们采用了两种方式来展示生成的噪声图像,一种采用VS2015+C#,另一种采用UNIYT2017.1.1f1+Shader,这两种方式一种使用CPU来计算,另一种则使用GPU来计算,因为底层架构不同,实现方式也不一样。
(三)、小结
这篇文章我们主要讲解了1D和2D Perlin噪声的生成算法,1D Perlin生成中距离向量没有参与或者说是以单位向量的形式参与运算,所以大大简化了复杂性,2D Perlin噪声生成中完全遵照了Perlin论文中生成噪声的算法来生成,排列表permutation主要是保证选择的梯度向量有很强的随机性,但同时他还保证对于同样的输入有同样的输出,这个对生成Perlin噪声至关重要,否则生成的噪声可能就是白噪声了。
(四)、代码下载
一、VS2015+C#版的1D和2D 噪声生成代码:代码
二、Unity+Shader版本: 代码
参考文献:
1、Understanding Perlin Noise https://flafla2.github.io/2014/08/09/perlinnoise.html