【图形学】谈谈噪声
写在前面
很早就想学习和整理下噪声,稍微接触过图形学的人大概都听到过噪声,然后就会发现有各种噪声,Perlin噪声,Worley噪声,分形(fractal)噪声等等。尤其是Perlin噪声,一搜资料发现大家说的各不相同,更加不明所以。我也总是困惑,后来发现还是要相信wiki和paper。
这篇文章在于总结上面这些常见的噪声(即图形学中常见的程序噪声),它们是什么,怎么算出来的,以及一些应用。文章里的所有代码可以在我的Shadertoy上找到:
2D版:
3D版:
什么是噪声
在图形学中,我们使用噪声就是为了把一些随机变量来引入到程序中。从程序角度来说,噪声很好理解,我们希望给定一个输入,程序可以给出一个输出:
value_type noise(value_type p) {
...
}
它的输入和输出类型的维数可以是不同的组合,例如输入二维输出一维,输入二维输出二维等。我们今天就是想讨论一下上面函数中的实现部分是长什么样的。
为什么我们需要这么多噪声
我对噪声的学习还没有很深,在此只想谈一点自己的想法。噪声其实就是为了把一些随机变量引入到程序中。在我们写一些C++这样的程序时,也经常会使用random这样的函数。这些函数通常会产生一些伪随机数,但很多情况下也足够满足我们的需要。同样,在图形学中我们也经常会需要使用随机变量,例如火焰、地形、云朵的模拟等等。相信你肯定听过大名鼎鼎的Minecraft游戏,这个游戏里面的地形生成也大量使用了随机变量。那么我们直接使用random这种函数不就好了吗?为什么要引入这么多名字的噪声呢?
这种直接使用随机生成器生成的随机值固然有它的好处,但它的问题在于生成的随机值太“随机”了。在图形学中,我们可以认为这种噪声就是白噪声(White noise)。wiki上说白噪声是功率谱密度在整个频域内均匀分布的噪声,听不懂对不对?通俗来讲,之所以称它为“白”噪声,是因为它类似于光学中包括全部可见光频率在内的白光。我相信你肯定听过白噪声,小时候电视机收音机没信号时,发出的那个沙沙声就是一种声音上的白噪声。我们这里只需要把白噪声理解为最简单的随机值,例如二维的白噪声纹理可以是下面这个样子:
可以看出白噪声非常不自然,听起来很刺耳,看起来也不好看。不光你这么想,图形学领域的前辈们也早发现了。如果你观察现实生活中的自然噪声,它们不会长成上面这个样子。例如木头纹理、山脉起伏,它们的形状大多是趋于分形状(fractal)的,即包含了不同程度的细节。比如地形,它有起伏很大的山脉,也有起伏稍小的山丘,也有细节非常多的石子等,这些不同程度的细节共同组成了一个自然的地形表面。那么,我们如何用程序来生成类似这样的自然的随机数(可以想象对应了地形不同的高度)呢?学者们根据效率、用途、自然程度(即效果好坏)等方面的衡量,提出了许多希望用程序模拟自然噪声的方法。例如,Perlin噪声被大量用于云朵、火焰和地形等自然环境的模拟;Simplex噪声在其基础上进行了改进,提到了效率和效果;而Worley噪声被提出用于模拟一些多孔结构,例如纸张、木纹等。
因此,学习和理解这些噪声在图形学中是十分必要的,因为它们的应用实在是太广泛了!
噪声的分类
根据wiki,由程序产生噪声的方法大致可以分为两类:
类别 | 名称 |
---|---|
基于晶格的方法(Lattice based) | 又可细分为两种: 第一种是梯度噪声(Gradient noise),包括Perlin噪声, Simplex噪声,Wavelet噪声等; 第二种是Value噪声(Value noise)。 |
基于点的方法(Point based) | Worley噪声 |
需要注意的是,一些文章经常会把Perlin噪声、Value噪声与分形噪声(Fractal noise)弄混,这实际在概念上是有些不一样的。分形噪声会把多个不同振幅、不同频率的octave相叠加,得到一个更加自然的噪声。而这些octave则对应了不同的来源,它可以是Gradient噪声(例如Perlin噪声)或Value噪声,也可以是一个简单的白噪声(White noise)。
一些非常出色的文章也错误把这种分形噪声声称为Perlin噪声,例如:
Hugo Elias的文章,这篇文章讲得挺有趣的,关于什么是octave、怎么混合它们都讲得很细致,也非常有名,但作者错误地把值噪声+分形噪声标识为Perlin噪声,他的文章链接也出现了wiki的值噪声(Value noise)的页面中。
Devmag的如何在你的游戏中使用Perlin噪声一文,同样非常有名,但同样错误地把白噪声+分形噪声认为是Perlin噪声。
如果读者常逛shadertoy的话,会发现很多shader使用了类似名为fbm的噪声函数。fbm实际就是分型布朗运动(Fractal Brownian Motion)的缩写,读者可以把它等同于我们上面所说的分形噪声(Fractal noise),我们以下均使用fbm来表示这种噪声的计算方法。如果要通俗地说fbm和之前提及的Perlin噪声、Simplex噪声、Value噪声、白噪声之间的联系,我们可以认为是很多个不同频率、不同振幅的基础噪声(指之前提到的Perlin噪声、Simplex噪声、Value噪声、白噪声等之一)之间相互叠加,最后形成了最终的分形噪声。这里的频率指的是计算噪声时的采样距离,例如对于基于晶格的噪声们,频率越高,单位面积(特指二维)内的晶格数目越多,看起来噪声纹理“越密集”;而振幅指的就是噪声的值域。下图显示了一些基础噪声和它们fbm后的效果:
说明:分割线左侧表示单层的基础噪声,右侧表示通过叠加不同频率噪声后的fbm效果。上面效果来源于shadertoy:Perlin噪声,Simplex噪声,Value噪声,Worley噪声。
由于Worley噪声的生成和其他噪声有明显不同,因此不是本文的重点。它主要用于产生孔状的噪声,有兴趣的读者可以参见偶像iq的文章:
- http://www.iquilezles.org/www/articles/smoothvoronoi/smoothvoronoi.htm
- http://www.iquilezles.org/www/articles/voronoise/voronoise.htm
Perlin噪声、Simplex噪声和Value噪声在性能上大致满足:Perlin噪声 > Value噪声 > Simplex噪声,Simplex噪声性能最好。Perlin噪声和Value噪声的复杂度是
下面的内容就是重点解释Perlin噪声、Perlin噪声和Simplex噪声这三种常见的噪声,最后再介绍fbm。
Perlin噪声
先介绍大名鼎鼎的Perlin噪声。很多人都知道,Perlin噪声的名字来源于它的创始人Ken Perlin。Ken Perlin早在1983年就提出了Perlin noise,当时他正在参与制作迪士尼的动画电影《电子世界争霸战》(英语:TRON),但是他不满足于当时计算机产生的那种非常不自然的纹理效果,因此提出了Perlin噪声。随后,他在1984年的SIGGRAPH Course上做了名为Advanced Image Synthesis1的课程演讲,并在SIGGRAPH 1985上发表了他的论文2。由于Perlin噪声的算法简单,被迅速应用到各种商业软件中。我们这位善良的Perlin先生却并没有对Perlin噪声算法申请专利(他说他的祖母曾叫他这么做过……),如果他这么做了那会是多大一笔费用啊!(不过在2001年的时候,旁人看不下去了,把三维以上的Simplex噪声的专利主动授予了Perlin。对,Simplex噪声也是人家提出的……)再后来Perlin继续研究程序纹理的生成,并和他的一名学生又在SIGGRAPH 1989上发表了一篇文章3,提出了超级纹理(hypertexture)。他们使用噪声+fbm+ray marching实现了各种有趣的效果。到1990年,已经有大量公司在他们的产品中使用了Perlin噪声。在1999年的GDCHardCore大会上,Ken Perlin做了名为Making Noise的演讲4,系统地介绍了Perlin噪声的发展、实现细节和应用。如果读者不想读论文的话,强烈建议你看一下Perlin演讲的PPT。
后来在2002年,Perlin又发表了一篇论文5来改进原始的Perlin噪声中的一些问题,例如原来的缓和曲线
- Perlin在2002年发表的论文:http://mrl.nyu.edu/~perlin/paper445.pdf
- GPU Gems的文章:http://http.developer.nvidia.com/GPUGems/gpugems_ch05.html
- GPU Gems 2的文章:http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter26.html
后面只介绍原始Perlin噪声的实现。
实现
Perlin噪声还是比较简单的,在1983年的计算机上实现的算法也不允许对计算量、内存有多大的要求。概括来说,Perlin噪声的实现需要三个步骤:
- 定义一个晶格结构,每个晶格的顶点有一个“伪随机”的梯度向量(其实就是个向量啦)。对于二维的Perlin噪声来说,晶格结构就是一个平面网格,三维的就是一个立方体网格。
- 输入一个点(二维的话就是二维坐标,三维就是三维坐标,n维的就是n个坐标),我们找到和它相邻的那些晶格顶点(二维下有4个,三维下有8个,n维下有
2n 个),计算该点到各个晶格顶点的距离向量,再分别与顶点上的梯度向量做点乘,得到2n 个点乘结果。 - 使用缓和曲线(ease curves)来计算它们的权重和。在原始的Perlin噪声实现中,缓和曲线是
s(t)=3t2−2t3 ,在2002年的论文6中,Perlin改进为s(t)=6t5−15t4+10t3 。这里简单解释一下,为什么不直接使用s(t)=t ,即线性插值。直接使用的线性插值的话,它的一阶导在晶格顶点处(即t = 0或t = 1)不为0,会造成明显的不连续性。s(t)=3t2−2t3 在一阶导满足连续性,s(t)=6t5−15t4+10t3 在二阶导上仍然满足连续性。
我们下面以二维的为例,再详细解释一下。我们可以用下面的图来表示上面的第一步和第二步:
一个问题是晶格顶点处的伪随机梯度向量是如何得到的,当然我们可以通过random这样的函数来计算单位正方形(二维)内的x和y分量值,但我们更愿意要那些在单位圆内的梯度向量。Perlin在他的实现中选择使用蒙特卡洛模拟方法来选取这些随机梯度向量。具体方法是(我把描述适应到了二维):首先按之前的方法生成在单位正方形内随机梯度向量,然后剔除那些不在单位圆内的向量,直到找到了需要数目的随机梯度向量。Perlin把这些预计算得到的向量存储在一个查找表G[n]中,n是纹理大小,例如256 x 256大小的纹理对应n为256。虽然我们实际上需要n x n个梯度向量,这样会造成有些顶点的梯度是重复的。Perlin认为,重复是可以允许的,只要它们的间距够大就不会被察觉。因此,Perlin还预计算了一个随机排列数组P[n],P[n]里面存储的是打乱后的0~n-1的排列值。这样一来,当我们想要得到(i, j)处晶格的梯度向量时,可以使用:
得到了梯度后,我们就可以进行点乘,得到4个点乘结果,然后使用缓和曲线对它们进行插值即可。这里使用的权重就是输入点到4个顶点的横纵距离。
在原始的Perlin噪声实现中,Perlin对梯度向量的选择一般是取单位圆(三维就是单位球)内一些不同方向的向量。在他后面的改进算法7中,Perlin建议把三维的梯度向量初始化为到立方体12条边的向量值,来保证各个方向之间的不关联性。
可以看出,Perlin噪声算法的时间复杂度为
效果
Shadertoy上有很多关于Perlin噪声的实现,我也山寨了一个:
它的主要代码如下:
vec2 hash22(vec2 p)
{
p = vec2( dot(p,vec2(127.1,311.7)),
dot(p,vec2(269.5,183.3)));
return -1.0 + 2.0 * fract(sin(p)*43758.5453123);
}
float perlin_noise(vec2 p)
{
vec2 pi = floor(p);
vec2 pf = p - pi;
vec2 w = pf * pf * (3.0 - 2.0 * pf);
return mix(mix(dot(hash22(pi + vec2(0.0, 0.0)), pf - vec2(0.0, 0.0)),
dot(hash22(pi + vec2(1.0, 0.0)), pf - vec2(1.0, 0.0)), w.x),
mix(dot(hash22(pi + vec2(0.0, 1.0)), pf - vec2(0.0, 1.0)),
dot(hash22(pi + vec2(1.0, 1.0)), pf - vec2(1.0, 1.0)), w.x),
w.y);
}
这里的实现实际是简化版的,我们在算梯度的时候直接取随机值,而没有归一化到单位圆内。
上图包含了四种噪声组合,这也是Perlin在1999年的GDC上做演讲时采用的一个示例,不过Perlin的例子是三维的,而且添加了光照等计算。
左上角的部分。这是最简单的单独的Perlin噪声,它的噪声模拟如下:
float noise_itself(vec2 p) { return noise(p * 8.0); }
上面的代码在整个屏幕上模拟了一个8 x 8的网格结构。在绘制时,我们只需要将一个深蓝色乘以噪声即可得到类似的效果。
单独一个Perlin噪声虽然也有一定用处,但是效果往往很无趣。因此,Perlin指出可以使用不同的函数组合来得到更有意思的结果,这些函数组合通常就是指通过分形叠加(fractal sum),也就是我们之前说的fbm。
左下角的部分。这个部分使用了fbm进行叠加来形成一个分形噪声。公式如下:
noise(p)+12noise(2p)+14noise(4p)+... 即每一次噪声的采样频率翻倍,而振幅减少一倍。很多文章错误地把这种计算认为是Perlin噪声,这是不对的。Perlin噪声只是对应了其中每一个octave。代码是:
float noise_sum(vec2 p) { float f = 0.0; p = p * 4.0; f += 1.0000 * noise(p); p = 2.0 * p; f += 0.5000 * noise(p); p = 2.0 * p; f += 0.2500 * noise(p); p = 2.0 * p; f += 0.1250 * noise(p); p = 2.0 * p; f += 0.0625 * noise(p); p = 2.0 * p; return f; }
上面叠加了5层,并把初始化采样距离设置为4,这都是可以自定义的。这种噪声可以用来模拟石头、山脉这类物体。
右下角的部分。这一部分只是在上一部分进行了一点修改,对噪声返回值进行了取绝对值操作。它使用的公式如下:
|noise(p)|+12|noise(2p)|+14|noise(4p)|+... 它对应的代码如下:
float noise_sum_abs(vec2 p) { float f = 0.0; p = p * 7.0; f += 1.0000 * abs(noise(p)); p = 2.0 * p; f += 0.5000 * abs(noise(p)); p = 2.0 * p; f += 0.2500 * abs(noise(p)); p = 2.0 * p; f += 0.1250 * abs(noise(p)); p = 2.0 * p; f += 0.0625 * abs(noise(p)); p = 2.0 * p; return f; }
由于进行了绝对值操作,因此会在0值变化处出现不连续性,形成一些尖锐的效果。通过合适的颜色叠加,我们可以用这种噪声来模拟火焰、云朵这些物体。Perlin把这个公式称为turbulence(湍流?),因为它看起来挺像的。
右上角的部分。这个部分是在之前turbulence公式的基础上使用了一个关于表面x分量的正弦函数:
sin(x+|noise(p)|+12|noise(2p)|+14|noise(4p)|+...) 这个公式可以让表面沿着x方向形成一个条纹状的结构。Perlin使用这个公式模拟了一些大理石材质。我们的代码如下:
float noise_sum_abs_sin(vec2 p) { float f = 0.0; p = p * 7.0; f += 1.0000 * abs(noise(p)); p = 2.0 * p; f += 0.5000 * abs(noise(p)); p = 2.0 * p; f += 0.2500 * abs(noise(p)); p = 2.0 * p; f += 0.1250 * abs(noise(p)); p = 2.0 * p; f += 0.0625 * abs(noise(p)); p = 2.0 * p; f = sin(f + p.x/32.0); return f; }
我们可以通过改变x分量前面的系数来控制条纹的疏密。
以上,我们就演示了基本的Perlin噪声的实现。Perlin在1999年Making Noise的演讲8中,还介绍了如何使用噪声来控制纹理动画。他指出,想要一个二维的turbulence纹理动画(例如云、火焰等),我们需要使用三维的噪声,其中第三维对应了时间变量。Shadertoy上有很多这样的例子,例如下面的就是模拟了Perlin演讲中实现的火球效果:
有兴趣的可以尝试实现Perlin演讲中实现的云彩飘动效果。
Value噪声
在理解了Perlin噪声的实现后,Value噪声就很简单了。它把原来的梯度替换成了一个简单的伪随机值,我们也不需要进行点乘操作,而直接把晶格顶点处的随机值按权重相加即可。
实现
和Perlin噪声一样,它也是一种基于晶格的噪声,也需要三个步骤:
- 定义一个晶格结构,每个晶格的顶点有一个“伪随机”的值(Value)。对于二维的Value噪声来说,晶格结构就是一个平面网格,三维的就是一个立方体网格。
- 输入一个点(二维的话就是二维坐标,三维就是三维坐标,n维的就是n个坐标),我们找到和它相邻的那些晶格顶点(二维下有4个,三维下有8个,n维下有
2n 个),得到这些顶点的伪随机值。 - 使用缓和曲线(ease curves)来计算它们的权重和。同样,缓和曲线可以是
s(t)=3t2−2t3 ,也可以是s(t)=6t5−15t4+10t3 (如果二阶导不连续对效果影响较大时)。
Value噪声比Perlin噪声的实现更加简单,并且需要的乘法和加法操作也更少,它只需要得到晶格顶点的随机值再把它们按权重相加即可。
效果
我们再次使用之前在Perlin噪声效果展示时使用的代码,但改变一些参数就可以得到下面的效果:
它的主要代码如下:
float value_noise(vec2 p)
{
vec2 pi = floor(p);
vec2 pf = p - pi;
vec2 w = pf * pf * (3.0 - 2.0 * pf);
return mix(mix(hash21(pi + vec2(0.0, 0.0)), hash21(pi + vec2(1.0, 0.0)), w.x),
mix(hash21(pi + vec2(0.0, 1.0)), hash21(pi + vec2(1.0, 1.0)), w.x),
w.y);
}
可以看出,单独的Value噪声(左上角)会有很明显的像素块效果。其实我认为,当把Value噪声纹理的大小调的很大时,它近似就是一张白噪声纹理。不过所幸的是,在通过fbm叠加后,形成的分形噪声效果还是可以接受的,这也是为什么它会被大量应用在很多程序中来代替基于梯度的Perlin噪声。
Simplex噪声
最后,我们来介绍本文最难理解的一种噪声。在2001年SIGGRAPH Course上,Ken Perlin进行了一次演讲9,他介绍了对Perlin噪声的一个改进版噪声——Simplex噪声。感兴趣的读者可以看一下Perlin原版的Course Note。我们之前提到过很多次,Simplex噪声的计算复杂度为
实现
Simplex噪声也是一种基于晶格的梯度噪声,它和Perlin噪声在实现上唯一不同的地方在于,它的晶格并不是方形(在2D下是正方形,在3D下是立方体,在更高纬度上我们称它们为超立方体,hypercube),而是单形,simplex。那么什么是单形呢?
通俗解释单形的话,可以认为是在N维空间里,选出一个最简单最紧凑的多边形,让它可以平铺整个N维空间。我们可以很容易地想到一维空间下的单形是等长的线段(1-单形),把这些线段收尾相连即可铺满整个一维空间。在二维空间下,单形是三角形(2-单形),我们可以把等腰三角形连接起来铺满整个平面。三维空间下的单形,即3-单形就是四面体。更高维空间的单形也是存在的。
那么使用单形有什么好处呢?这可以从之前对单形的解释看出来——它的顶点数很少,要远小于超立方体(hypercube)的顶点个数。总结起来,在N维空间下,超立方体的顶点数目是
在理解了单形后,Simplex噪声的计算过程其实和Perlin噪声基本一样。我们以二维空间下的为例。二维空间下的单形即是等边三角形,如下图所示。这些单形组成了一个单形网格结构,和Perlin噪声类似,这些网格顶点处也存储了伪随机梯度向量。
当输入一点后,我们找到该点所在的三角形(图中红色三角形),再找到该三角形三个顶点的梯度向量和每个顶点到输入点的差值向量,把每个顶点的梯度向量和插值向量做点乘,得到三个点乘结果。最后,我们把它们按权重进行叠加混合,这个权重与输入点到每个顶点的有关,即每个顶点的噪声贡献度为:
其中,
可以看出,Simplex噪声的实现过程和Perlin几乎完全一样。但是,在上面的实现中我们始终忽略了一个问题,就是如何找到输入点所在的单形?在计算Perlin噪声时,判断输入点所在的正方形是非常容易的,我们只需要对输入点下取整即可找到,那么这里能不能也这么计算呢?幸运的是,数学家们已经为我们解决了这个问题:我们可以把单形进行坐标偏斜(skewing),把平铺空间的单形变成一个新的网格结构,这个网格结构是由超立方体组成的,而每个超立方体又由一定数量的单形构成。听不懂是不是?2005年Stefan的一篇论文10里的一张图大概可以解救你!
我们之前讲到的单形网格如上图中的红色网格所示,它们有一些等边三角形组成(注意到这些等边三角形是沿空间对角线排列的)。经过坐标倾斜后,它们变成了后面的黑色网格,这些网格由正方形组成,每个正方形是由之前两个等边三角形变形而来的三角形组成。这个把N维空间下的单形网格变形成新网格的公式如下:
在二维空间下,取n为2即可。这样变换之后,我们就可以按照之前方法判断该点所在的超立方体,在二维下即为正方形。这样我们就有了Simplex噪声的第一步:
1 坐标偏斜:把输入点坐标进行坐标偏斜,对坐标下取整得到输入点所在的超立方体
xi=floor(x′),yi=floor(y′),... ,我们还可以得到小数部分xf=x′−xi,yf=y′−yi,... ,这些小数部分可以帮助我们进一步判断输入点所在的单形以及计算权重。
但我们的目标其实是要得到输入点所在的单形,而不是超立方体。因此我们需要继续做判断。还是如之前的图所示,经过坐标偏斜后,一个正方形由两个三角形组成,我们可以判断
2 单形分割:我们把之前得到的
(xf,yf,...) 中的数值按降序排序,来决定输入点位于变形后的哪个单形内。这个单形的顶点是由按序排列的(0, 0, …, 0)到(1, 1, …, 1)中的n+1 个顶点组成,共有n! 种可能性。我们可以按下面的过程来得到这n+1 个顶点:从零坐标(0, 0, …, 0)开始,找到当前最大的分量,在该分量位置加1,直至添加了所有分量。例如,对于二维空间来说,如果xf,yf 满足xf>yf ,那么对应的3个单形坐标为:首先找到(0, 0),由于x分量比较大,因此下一个坐标是(1, 0),接下来是y分量,坐标为(1, 1);对于三维空间来说,如果xf,yf,zf 满足xf>zf>yf ,那么对应的4个单形坐标位:首先从(0, 0, 0)开始,接下来在x分量上加1得(1, 0, 0),再在z分量上加1得(1, 0, 1),最后在y分量上加1得(1, 1, 1)。这一步的算法复杂度即为排序复杂度O(n2) 。
找到了对应的单形后,后面的工作就比较简单了。我们首先找到该单形各个顶点上的伪随机梯度向量,这就是第三步:
3 梯度选取:我们在偏斜后的超立方体网格上获取该单形的各个顶点的伪随机梯度向量。
现在我们需要的东西基本都准备好了,最后一步就是计算所有单形顶点对输出的噪声贡献度。
4 贡献度取和:我们首先需要把单形顶点变回到之前由单形组成的单形网格。这一步需要使用第一步公式的逆函数来求得:
x=x′+(x′+y′+...)⋅K2y=y′+(x′+y′+...)⋅K2...其中,K2=1n+1√−1n
我们由此可以得到输入点到这些单形顶点的位移向量。这些向量有两个用途,一个是为了和顶点梯度向量点乘,另一个是为了得到之前提到的距离值dist ,来据此求得每个顶点对结果的贡献度:
(r2−|dist|2)4×dot(dist,grad)
现在我们可以来解释
至此,我们解释了Simplex噪声的实现。虽然理解上Simplex噪声相比于Perlin噪声更难理解,但由于它的效果更好、速度更优,因此很多情况下会替代Perlin噪声。
效果
下面是Simplex噪声和fbm组合之后的一些效果:
它的主要代码如下:
float simplex_noise(vec2 p)
{
const float K1 = 0.366025404; // (sqrt(3)-1)/2;
const float K2 = 0.211324865; // (3-sqrt(3))/6;
vec2 i = floor(p + (p.x + p.y) * K1);
vec2 a = p - (i - (i.x + i.y) * K2);
vec2 o = (a.x < a.y) ? vec2(0.0, 1.0) : vec2(1.0, 0.0);
vec2 b = a - o + K2;
vec2 c = a - 1.0 + 2.0 * K2;
vec3 h = max(0.5 - vec3(dot(a, a), dot(b, b), dot(c, c)), 0.0);
vec3 n = h * h * h * h * vec3(dot(a, hash22(i)), dot(b, hash22(i + o)), dot(c, hash22(i + 1.0)));
return dot(vec3(70.0, 70.0, 70.0), n);
}
我还是要稍微解释下上面的代码。vec2 i即为我们在第二步中提到的超立方体索引号(也就是超立方体中的(0, 0)点)。vec2 a是在变形前输入点到(0, 0)点的距离向量。接着,我们通过判断a的x分量和y分量的大小(变形前后不会对结果有影响,因此这里直接使用变形前的距离向量),来得到输入点所在的单形顶点。接着,我们计算输入点到第二个单形顶点的距离向量vec2 b以及到第三个单形顶点的距离向量vec2 c。然后,我们计算每个顶点的
因此,我们最后把结果乘以70。那么,如果
可平铺的噪声
这部分是新加的。可平铺的噪声就是指那些可以tiling的、seamless的噪声,因为很多时候我们想要让噪声纹理可以无缝连接,例如在生成地形时。按照我们之前提到的方法直接产生噪声,得到的噪声纹理其实是不可以平铺的,你可以看生成纹理的左右、上下其实是不一样的。那么,怎么生成可平铺的噪声纹理呢?
我直接说目前公认比较好的一种方法,就是在2n维上计算n维可平铺噪声。我们以二维噪声为例,如果我们想要得到二维的无缝Perlin噪声,就需要用四维噪声算法来产生。这种方法是思想是,由于我们想要每个维度都是无缝的,也就是当该维度的值从0变成1的过程中,0和1之间比较是平滑过渡的,这让我们想起了“圆”,绕圆一周就是对该维度的采样过程,这样就可以保证无缝了。因此,对于二维噪声中的x轴,我们会在四维空间下的xz平面上的一个圆上进行采样,而二维噪声的y轴,则会在四维空间下的yw平面上的一个圆上进行采样。这个转化过程很简单,我们只需要使用三角函数sin和cos即可把二维采样坐标转化到单位圆上。同样,三维空间的也是类似的,我们会在六维空间下计算。这种方法不仅适用于Perlin噪声,像Worley噪声这种也同样是适合的。
当然上述方法也有自己的缺点,最明显的就是计算量大大增加,一般噪声的复杂度为
代码的话,Unity Wiki里给出了二维可平铺的Simplex噪声的实现,关键代码如下:
//X, Y is [0..1]
public static float SeamlessNoise( float x, float y, float dx, float dy, float xyOffset ) {
float s = x;
float t = y;
float nx = xyOffset + Mathf.Cos(s * 2.0f * Mathf.PI) * dx / (2.0f * Mathf.PI);
float ny = xyOffset + Mathf.Cos(t * 2.0f * Mathf.PI) * dy / (2.0f * Mathf.PI);
float nz = xyOffset + Mathf.Sin(s * 2.0f * Mathf.PI) * dx / (2.0f * Mathf.PI);
float nw = xyOffset + Mathf.Sin(t * 2.0f * Mathf.PI) * dy / (2.0f * Mathf.PI);
return Noise(nx, ny, nz, nw);
}
其中,xyOffset是指在四维空间某个平面上的偏移,即这个单位圆是以xyOffset为圆心的。所有可以用这种方法来产生无缝噪声的实现都和上面的代码是一样的,它们的区别仅仅在于Noise(nx, ny, nz, nw)的实现,如果它是四维Perlin噪声,那就会产生二维可平铺的Perlin噪声,如果是四维的Worley噪声,就会产生二维可平铺的Worley噪声。
如果你有兴趣更深入地理解这种方法,可以阅读下面的一些文章:
- 这是出处:http://www.gamedev.net/blog/33/entry-2138456-seamless-noise/
- 一个有图示的文章,帮助理解:http://ronvalstar.nl/creating-tileable-noise-maps
- Unity wiki对于二维无缝Simplex噪声的实现:http://wiki.unity3d.com/index.php/Tileable_Noise
- GameDev上关于可平铺噪声的讨论:http://gamedev.stackexchange.com/questions/23625/how-do-you-generate-tileable-perlin-noise
扩展阅读
网上有很多优秀的阅读的资料,这里只是我找到的一小部分(wiki我就不说了,一般是必看的):
Perlin噪声
- 这篇详细地解释了真正的Perlin噪声以及和fbm的结合:http://flafla2.github.io/2014/08/09/perlinnoise.html
- 2005年的这篇论文11的开头部分也给出了不错的Perlin噪声的解释:http://webstaff.itn.liu.se/~stegu/simplexnoise/simplexnoise.pdf
- GPU Gems的文章:http://http.developer.nvidia.com/GPUGems/gpugems_ch05.html
- GPU Gems 2的文章:http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter26.html
- 大牛scrawk的博客在Unity里实现了GPU Gems里面的方法:https://scrawkblog.com/2013/05/18/gpu-gems-to-unity-improved-perlin-noise/
Value噪声
- 这个课程比较详细地讲了噪声的来源、应用和实现:http://scratchapixel.com/old/lessons/3d-advanced-lessons/noise-part-1/
- 网络上很多号称是Perlin噪声的文章讲的其实都是Value噪声。比如这篇非常著名又挺有意思的文章:http://freespace.virgin.net/hugo.elias/models/m_perlin.htm
Simplex噪声
- 这篇文章讲得很细,如果你想再理解下三维的Simplex噪声可以看看:http://catlikecoding.com/unity/tutorials/simplex-noise/
- Shadertoy上也有三维Simplex噪声的实现:https://www.shadertoy.com/view/XsX3zB和我的https://www.shadertoy.com/view/4sc3z2。
fbm
- 其实几乎所有讲噪声的都会讲和fbm的结合。这篇文章单独拿出来讲了:https://code.google.com/p/fractalterraingeneration/wiki/Fractional_Brownian_Motion
hash
- 在计算随机值和随机梯度时往往都需要使用哈希函数来进行预计算或实时计算,Shadertoy上有个人总结了不同数据类型组合下的哈希函数:https://www.shadertoy.com/view/4djSRW
- 在计算随机值和随机梯度时往往都需要使用哈希函数来进行预计算或实时计算,Shadertoy上有个人总结了不同数据类型组合下的哈希函数:https://www.shadertoy.com/view/4djSRW
参考文献
- Perlin K. Course in advanced image synthesis[C]//ACM SIGGRAPH Conference. 1984, 18. ↩
- Perlin K. An image synthesizer[J]. ACM Siggraph Computer Graphics, 1985, 19(3): 287-296. ↩
- Perlin K, Hoffert E M. Hypertexture[C]//ACM SIGGRAPH Computer Graphics. ACM, 1989, 23(3): 253-262. ↩
- Perlin K. Making noise[C]//Proc. of the Game Developer Conference. 1999. ↩
- Perlin K. Improving noise[C]//ACM Transactions on Graphics (TOG). ACM, 2002, 21(3): 681-682. ↩
- Perlin K. Improving noise[C]//ACM Transactions on Graphics (TOG). ACM, 2002, 21(3): 681-682. ↩
- Perlin K. Improving noise[C]//ACM Transactions on Graphics (TOG). ACM, 2002, 21(3): 681-682. ↩
- Perlin K. Making noise[C]//Proc. of the Game Developer Conference. 1999. ↩
- Perlin K. Noise hardware[J]. Real-Time Shading SIGGRAPH Course Notes, 2001. ↩
- Gustavson S. Simplex noise demystified[J]. Linköping University, Linköping, Sweden, Research Report, 2005. ↩
- Gustavson S. Simplex noise demystified[J]. Linköping University, Linköping, Sweden, Research Report, 2005. ↩