Unity3D教程:PerlinNoise原理及实现

发表于2016-05-21
评论6 1.15w浏览
柏林噪声算法有两个版本的柏林噪声定义,考虑到大家会存在混淆定义的情况造成了将分形噪声当做柏林噪声,然后就出现了两个柏林噪声的现象。为了帮助大家,下面就给大家介绍下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

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引