Unity3D教程:PerlinNoise原理及实现
发表于2016-05-21
柏林噪声算法有两个版本的柏林噪声定义,考虑到大家会存在混淆定义的情况造成了将分形噪声当做柏林噪声,然后就出现了两个柏林噪声的现象。为了帮助大家,下面就给大家介绍下Unity3D中柏林噪声(PerlinNoise)的原理和实现方法。
一、前言
本文旨在与大家一起探讨学习新知识,如有疏漏或者谬误,请大家不吝指出。
二、概述
在学习GPU Gem 1和GPU Gem 2的时候看到了柏林噪声算法的相关知识,到网上搜索相关资料时发现竟然有两个版本的柏林噪声定义,深入学习后发现其中有一部分存在混淆定义的情况,造成了将分形噪声当做柏林噪声,然后就出现了两个柏林噪声的现象。(多个不同频率与振幅的噪声的叠加属于分形噪声的思想,这是对同样有此困惑的读者说的)
主要参阅资料有:
GPU Gem 1中柏林噪声的相关章节;
GPU Gem 2中第26章关于改进后的柏林噪声的实现;
维基百科上对于柏林噪声的定义;
candycat的博客中对于柏林噪声的描述;
Ken Perlin在2002年发表的改进版柏林噪声的论文;
Perlin Noise,译作柏林噪声,是指Ken Perlin发明的噪声算法。1983年,Ken Perlin在参与”电子世界争霸赛”这部动画电影制作的时候提出了柏林噪声算法,随后在2002年对原有的柏林噪声算法做了改进,并发表了论文ImprovingNoise。
那么我们先来了解下柏林噪声的用途。在游戏开发以及其他应用程序的开发中,其实经常会用到随机数生成器,有可能它是”random()”,或者U3D中的”Ramdom.Range()”。总之,如果我们直接用随机数生成器来生成一张图,那么它应该是下面这样子的:

整张图上面充满了尖锐的噪声,如同收不到信号的收音机发出的滋啦滋啦声。这种噪声我们一般称为白噪声,它一点也不美观,如果我们需要模拟自然界中的某些随机现象,那么它完全不可行。自然界中的随机现象有哪些?例如水波的扰动、树木的年轮或者纹理、山脉的高低起伏(想想大名鼎鼎的“我的世界”)、天上飘来飘去的云以及跳动的火焰等等。这些现象中包含有随机的成分,但是相互之间又有关联,主要表现为,它们是平滑的进行变化,而不是像白噪声那么尖锐。所以,柏林噪声就出现了,它就是用于程序模拟生成自然纹理。
如下图所示,是一张柏林噪声图:

三、原理
然后我们来讨论一下一维、二维柏林噪声的原理。
1、一维柏林噪声
首先,在X轴向上每个整数坐标随机生成一个数(范围为-1~1),我们称这个数为Gradient,译为梯度或者斜率。然后我们对相邻两个整数之间使用梯度进行插值计算,使得相邻两点之间平滑过渡。平滑度取决于所选用的插值函数,老版的柏林噪声使用f(t)=3*t*t-2*t*t*t,改进后的柏林噪声使用f(t)=t*t*t*(t*(t*6-15)+10)。
如图所示:

上图来自于KenPerlin的Simplex Noise论文,论文中提到了经典的柏林噪声定义。
2、二维柏林噪声
对于二维来说我们可以获取点P(x, y)最近的四个整数点ABCD,ABCD四个点的坐标分别为A(i, j)、B(i+1, j)、C(i, j+1)、D(i+1, j+1),随后获取ABCD四点的二维梯度值G(A)、G(B)、G(C)、G(D),并且算出ABCD到P点的向量AP、BP、CP以及DP。如下图所示:

红色箭头表示该点处的梯度值,绿色箭头表示该点到P点的向量。
接着,将G(A)与AP进行点乘,计算出A点对于P点的梯度贡献值,然后分别算出其余三个点对P点的梯度贡献值,最后将(u, v)代入插值函数中算出P点的最终噪声值。
以上类推到三维的柏林噪声,则需要算出八个顶点的梯度贡献值,然后进行插值计算。
还有一个问题没有解决,就是怎样随机生成梯度值。当然你可以通过使用一个伪随机函数生成一维到三维的梯度值(二维和三维就是梯度向量了),例如,对于一维可以使用下面的公式:
Fract(Sin(n)*753.5453123f);
另外一种,也是柏林使用的,是预先生成256个伪随机的梯度值(以及二维和三维的梯度向量)保存在G1[256]、G2[256][2]]以及G3[256][3]中,然后对于一维柏林噪声来说,我们可以直接去取G1[]数组中的梯度值使用,对于二维或者三维的怎么办?柏林预先又生成了一个排列P[256],将0~255的下标随机存放在P数组中,然后通过下面公式来取到随机的梯度值:
G2[P[x] + y] ——二维
G3[P[P[x] + y] +z] ——三维
以上就完成了随机取梯度值的算法。
当然,这里还要提一下在Improved Noise论文中柏林对于三维柏林噪声的改进。在论文中柏林的第一个改进就是插值函数的改进,也就是我们上面提到过的f(t)=t*t*t*(t*(t*6-15)+10),它使得插值出来的噪声值更平滑了,特别是在三维中。
下图左边是三维柏林噪声使用老版本插值函数计算的结果,右边是改进的插值函数计算的结果。可以看出效果提高了不少。


另外一个改进针对取随机梯度值。在三维中,P点周围的最近8个点构成了一个立方体,P点到立方体的每条边的中点的向量有12个,柏林使用这12个随机梯度向量替代了原先的256个梯度向量。当然取随机梯度向量的操作就变成了:G3[ P[P[P[x] +y] + z] ]。
四、实现
这里给出二维柏林函数的部分实现代码,二维的比较具有代表性。完整的一维到三维的柏林函数实现请下载附件查看。附件中对于三维柏林噪声使用的是改进后的算法,一维和二维则是用的经典算法。附件中也包含了着色器版的三维柏林噪声的实现。
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 | public static float Fade( float t) { returnt*t*t*(t*(t*6-15)+10); //return3*t*t-2*t*t*t; } public static float Gradient_2D( int x, Vector2 y) { return Vector2.Dot(gradient_2D[x%gradient_2D.Length], y); } public static int Perm(Vector2 p) { intp1 = permutation [(( int )p.x) % 256]; returnp1+( int )p.y; } //classic method publicstatic float PerlinNoise_2D(Vector2 p) { InitGradient(); Vector2ip = CMath.Floor(p); Vector2np = p - ip; Vector2t = Fade(np); floatcorner1 = Gradient_2D ( Perm(ip), np ); floatcorner2 = Gradient_2D ( Perm(ip+ new Vector2(1, 0)), np- new Vector2(1, 0) ); floatcorner3 = Gradient_2D ( Perm(ip+ new Vector2(0, 1)), np- new Vector2(0, 1) ); floatcorner4 = Gradient_2D ( Perm(ip+ new Vector2(1, 1)), np- new Vector2(1, 1) ); returnMathf.Lerp ( Mathf.Lerp(corner1,corner2, t.x), Mathf.Lerp(corner3,corner4, t.x), t.y); } |
单独的柏林噪声生成的图像在上面我们已经看过了,当然我们可以利用分形噪声的思想,对柏林噪声进行叠加:

noise(p)+0.5*noise(2p)+0.25*noise(4p)+0.125*noise(8p)
也可以稍微改变一下叠加公式,创造出不同的自然纹理效果:(这正是它被开发出来的理由)

Abs(noise(p)+0.5*noise(2p)+0.25*noise(4p)+0.125*noise(8p))
上面这张图被称为Turbulence湍流图。可以用于模拟太阳耀斑或者火焰。

Sin(x+Abs(noise(p)+0.5*noise(2p)+0.25*noise(4p)+0.125*noise(8p)))

(noise(p)-(int)noise(p))*20