【图形学】谈谈噪声



写在前面

很早就想学习和整理下噪声,稍微接触过图形学的人大概都听到过噪声,然后就会发现有各种噪声,Perlin噪声,Worley噪声,分形(fractal)噪声等等。尤其是Perlin噪声,一搜资料发现大家说的各不相同,更加不明所以。我也总是困惑,后来发现还是要相信wiki和paper。

这篇文章在于总结上面这些常见的噪声(即图形学中常见的程序噪声),它们是什么,怎么算出来的,以及一些应用。文章里的所有代码可以在我的Shadertoy上找到:

2D版:


width=”500″ height=”320″ src=”https://www.shadertoy.com/embed/ldc3RB?gui=true&t=10&paused=true&muted=false” allowfullscreen=””>

3D版:


width=”500″ height=”320″ src=”https://www.shadertoy.com/embed/4sc3z2?gui=true&t=10&paused=false&muted=false” allowfullscreen=””>

什么是噪声

在图形学中,我们使用噪声就是为了把一些随机变量来引入到程序中。从程序角度来说,噪声很好理解,我们希望给定一个输入,程序可以给出一个输出:

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噪声,例如:

如果读者常逛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的文章:

Perlin噪声、Simplex噪声和Value噪声在性能上大致满足:Perlin噪声 > Value噪声 > Simplex噪声,Simplex噪声性能最好。Perlin噪声和Value噪声的复杂度是

O(2n)
,其中n是维数,但Perlin噪声比Value噪声需要进行更多的乘法(点乘)操作。而Simplex噪声的复杂度为

O(n2)
,在高纬度上优化明显。

下面的内容就是重点解释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噪声中的一些问题,例如原来的缓和曲线

s(t)=3t22t3
的二阶导

612t


t=0


t=1
时均不等于0,这使得在相邻的晶格处它们的二阶导并不连续。因此Perlin提出使用一个更好的缓和曲线

s(t)=6t515t4+10t3
;此外还改进了晶格顶点处随机梯度向量的选取。有兴趣的读者可以参考:

后面只介绍原始Perlin噪声的实现。

实现

Perlin噪声还是比较简单的,在1983年的计算机上实现的算法也不允许对计算量、内存有多大的要求。概括来说,Perlin噪声的实现需要三个步骤:

  1. 定义一个晶格结构,每个晶格的顶点有一个“伪随机”的梯度向量(其实就是个向量啦)。对于二维的Perlin噪声来说,晶格结构就是一个平面网格,三维的就是一个立方体网格。
  2. 输入一个点(二维的话就是二维坐标,三维就是三维坐标,n维的就是n个坐标),我们找到和它相邻的那些晶格顶点(二维下有4个,三维下有8个,n维下有

    2n
    个),计算该点到各个晶格顶点的距离向量,再分别与顶点上的梯度向量做点乘,得到

    2n
    个点乘结果。
  3. 使用缓和曲线(ease curves)来计算它们的权重和。在原始的Perlin噪声实现中,缓和曲线是

    s(t)=3t22t3
    ,在2002年的论文6中,Perlin改进为

    s(t)=6t515t4+10t3
    。这里简单解释一下,为什么不直接使用

    s(t)=t
    ,即线性插值。直接使用的线性插值的话,它的一阶导在晶格顶点处(即t = 0或t = 1)不为0,会造成明显的不连续性。

    s(t)=3t22t3
    在一阶导满足连续性,

    s(t)=6t515t4+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)处晶格的梯度向量时,可以使用:



G=G[(i+P[j])mod n]

得到了梯度后,我们就可以进行点乘,得到4个点乘结果,然后使用缓和曲线对它们进行插值即可。这里使用的权重就是输入点到4个顶点的横纵距离。

在原始的Perlin噪声实现中,Perlin对梯度向量的选择一般是取单位圆(三维就是单位球)内一些不同方向的向量。在他后面的改进算法7中,Perlin建议把三维的梯度向量初始化为到立方体12条边的向量值,来保证各个方向之间的不关联性。


可以看出,Perlin噪声算法的时间复杂度为

O(2n)
(n是维数),

2n
其实就是每个晶格的顶点个数,对于每个输入点我们需要进行这么多次的点乘操作。因此,随着维数的增大,例如计算三维四维的Perlin噪声,时间会迅速增加。Perlin在2002年介绍了他优化后的一种噪声——Simplex噪声,这种噪声的复杂度是

O(n2)
,在高纬度上时间优化明显,我们后面会讲到这种噪声。

效果

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演讲中实现的火球效果


width=”500″ height=”320″ src=”https://www.shadertoy.com/embed/lsf3RH?gui=true&t=10&paused=false&muted=false” allowfullscreen=””>

有兴趣的可以尝试实现Perlin演讲中实现的云彩飘动效果

Value噪声

在理解了Perlin噪声的实现后,Value噪声就很简单了。它把原来的梯度替换成了一个简单的伪随机值,我们也不需要进行点乘操作,而直接把晶格顶点处的随机值按权重相加即可。

实现

和Perlin噪声一样,它也是一种基于晶格的噪声,也需要三个步骤:

  1. 定义一个晶格结构,每个晶格的顶点有一个“伪随机”的值(Value)。对于二维的Value噪声来说,晶格结构就是一个平面网格,三维的就是一个立方体网格。
  2. 输入一个点(二维的话就是二维坐标,三维就是三维坐标,n维的就是n个坐标),我们找到和它相邻的那些晶格顶点(二维下有4个,三维下有8个,n维下有

    2n
    个),得到这些顶点的伪随机值。
  3. 使用缓和曲线(ease curves)来计算它们的权重和。同样,缓和曲线可以是

    s(t)=3t22t3
    ,也可以是

    s(t)=6t515t4+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噪声的计算复杂度为

O(n2)
,优于Perlin噪声的

O(2n)
。而且在效果上,Simplex噪声也克服了经典的Perlin噪声在某些视觉问题。但Simplex噪声的问题在于,它比较难理解,以至于当Perlin提出了后好几年的时间里它并没有被广泛使用。

实现

Simplex噪声也是一种基于晶格的梯度噪声,它和Perlin噪声在实现上唯一不同的地方在于,它的晶格并不是方形(在2D下是正方形,在3D下是立方体,在更高纬度上我们称它们为超立方体,hypercube),而是单形,simplex。那么什么是单形呢?

通俗解释单形的话,可以认为是在N维空间里,选出一个最简单最紧凑的多边形,让它可以平铺整个N维空间。我们可以很容易地想到一维空间下的单形是等长的线段(1-单形),把这些线段收尾相连即可铺满整个一维空间。在二维空间下,单形是三角形(2-单形),我们可以把等腰三角形连接起来铺满整个平面。三维空间下的单形,即3-单形就是四面体。更高维空间的单形也是存在的。

那么使用单形有什么好处呢?这可以从之前对单形的解释看出来——它的顶点数很少,要远小于超立方体(hypercube)的顶点个数。总结起来,在N维空间下,超立方体的顶点数目是

2n
,而单形的顶点数目是

n+1
,这使得我们在计算梯度噪声时可以大大减少需要计算的顶点权重数目。

在理解了单形后,Simplex噪声的计算过程其实和Perlin噪声基本一样。我们以二维空间下的为例。二维空间下的单形即是等边三角形,如下图所示。这些单形组成了一个单形网格结构,和Perlin噪声类似,这些网格顶点处也存储了伪随机梯度向量。


这里写图片描述

当输入一点后,我们找到该点所在的三角形(图中红色三角形),再找到该三角形三个顶点的梯度向量和每个顶点到输入点的差值向量,把每个顶点的梯度向量和插值向量做点乘,得到三个点乘结果。最后,我们把它们按权重进行叠加混合,这个权重与输入点到每个顶点的有关,即每个顶点的噪声贡献度为:



(r2|dist|2)4×dot(dist,grad)

其中,

dist
是输入点到该顶点的距离向量,

grad
是该点存储的伪随机梯度向量,

r2
的取值是0.5或0.6。取0.5时可以保证没有不连续的间断点,在连续性并不那么明显时可以取0.6得到更好的视觉效果。在Perlin原始的论文中,r的取值是0.6(后面我们会讲到0.5的值是如何得到的)。当得到单形每个顶点的噪声贡献度后,就可以把它们相加起来。为了把结果归一到-1到1的范围,我们往往还需要在最后乘以一个系数,这个系数值我们之后会讲到。

可以看出,Simplex噪声的实现过程和Perlin几乎完全一样。但是,在上面的实现中我们始终忽略了一个问题,就是如何找到输入点所在的单形?在计算Perlin噪声时,判断输入点所在的正方形是非常容易的,我们只需要对输入点下取整即可找到,那么这里能不能也这么计算呢?幸运的是,数学家们已经为我们解决了这个问题:我们可以把单形进行坐标偏斜(skewing),把平铺空间的单形变成一个新的网格结构,这个网格结构是由超立方体组成的,而每个超立方体又由一定数量的单形构成。听不懂是不是?2005年Stefan的一篇论文10里的一张图大概可以解救你!


这里写图片描述

我们之前讲到的单形网格如上图中的红色网格所示,它们有一些等边三角形组成(注意到这些等边三角形是沿空间对角线排列的)。经过坐标倾斜后,它们变成了后面的黑色网格,这些网格由正方形组成,每个正方形是由之前两个等边三角形变形而来的三角形组成。这个把N维空间下的单形网格变形成新网格的公式如下:



x=x+(x+y+...)K1y=y+(x+y+...)K1...K1=n+11n

在二维空间下,取n为2即可。这样变换之后,我们就可以按照之前方法判断该点所在的超立方体,在二维下即为正方形。这样我们就有了Simplex噪声的第一步:

1 坐标偏斜:把输入点坐标进行坐标偏斜,对坐标下取整得到输入点所在的超立方体

xi=floor(x),yi=floor(y),...
,我们还可以得到小数部分

xf=xxi,yf=yyi,...
,这些小数部分可以帮助我们进一步判断输入点所在的单形以及计算权重。

但我们的目标其实是要得到输入点所在的单形,而不是超立方体。因此我们需要继续做判断。还是如之前的图所示,经过坐标偏斜后,一个正方形由两个三角形组成,我们可以判断

xf


yf
之间的关系来判断输入点位于哪个三角形内,并得到该三角形的三个顶点。由此,我们有了Simplex噪声的第二步:

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+11n

我们由此可以得到输入点到这些单形顶点的位移向量。这些向量有两个用途,一个是为了和顶点梯度向量点乘,另一个是为了得到之前提到的距离值



dist
,来据此求得每个顶点对结果的贡献度:




(r2|dist|2)4×dot(dist,grad)

现在我们可以来解释

r2
取0.5的原因了。由于要求经过第一步坐标偏斜后得到的网格宽度为1,因此我们可以倒推出在变形前单形网格中每个单形边的边长为

23
,这样一来单形每个顶点到对面边的距离(即高)的长度为

22
,它的平方即为0.5。很奇妙的是,不仅是二维,在其他维度下,每个单形顶点到对面边/面的距离都是0.5。


至此,我们解释了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。然后,我们计算每个顶点的

r2|dist|2
,取

r2
为0.5,并把结果截取到不小于0的部分,得到每个顶点的权重vec3 h。之后,我们计算每个顶点的梯度向量和距离向量的点乘结果,并和它们的权重相乘得到vec3 n。在把它们相加并返回最终结果前,我们需要保证返回值的范围在-1到1,这就需要我们计算n每个分量相加后的最大值。这个最大值可以在输入点在某一边中点时取得,此时

|a|=22,|b|=|c|=16
,得

h=(0,13,13)
。取最大值时,梯度和距离向量的点乘结果可以认为是两者模的乘积,而梯度模的最大值为

2
,因此最后和的最大值为:



1341622170

因此,我们最后把结果乘以70。那么,如果

r2
取0.6,最后需要乘以多少呢?答案大约为24.51。利用这个想法,我们可以在任意维度下计算最后的伸缩值,例如在三维下,单形,即正四面体的边长为

32
,当

r2
取0.5时,最后大概需要乘以31.32。

可平铺的噪声

这部分是新加的。可平铺的噪声就是指那些可以tiling的、seamless的噪声,因为很多时候我们想要让噪声纹理可以无缝连接,例如在生成地形时。按照我们之前提到的方法直接产生噪声,得到的噪声纹理其实是不可以平铺的,你可以看生成纹理的左右、上下其实是不一样的。那么,怎么生成可平铺的噪声纹理呢?

我直接说目前公认比较好的一种方法,就是在2n维上计算n维可平铺噪声。我们以二维噪声为例,如果我们想要得到二维的无缝Perlin噪声,就需要用四维噪声算法来产生。这种方法是思想是,由于我们想要每个维度都是无缝的,也就是当该维度的值从0变成1的过程中,0和1之间比较是平滑过渡的,这让我们想起了“圆”,绕圆一周就是对该维度的采样过程,这样就可以保证无缝了。因此,对于二维噪声中的x轴,我们会在四维空间下的xz平面上的一个圆上进行采样,而二维噪声的y轴,则会在四维空间下的yw平面上的一个圆上进行采样。这个转化过程很简单,我们只需要使用三角函数sin和cos即可把二维采样坐标转化到单位圆上。同样,三维空间的也是类似的,我们会在六维空间下计算。这种方法不仅适用于Perlin噪声,像Worley噪声这种也同样是适合的。

当然上述方法也有自己的缺点,最明显的就是计算量大大增加,一般噪声的复杂度为

O(2n)
(Simplex噪声例外,是

O(n2)
),是指数增加的。但是由于噪声纹理一般只需要前期计算一次即可,因此大部分时候是可以承担的。

代码的话,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噪声。

如果你有兴趣更深入地理解这种方法,可以阅读下面的一些文章:

扩展阅读

网上有很多优秀的阅读的资料,这里只是我找到的一小部分(wiki我就不说了,一般是必看的):

参考文献


  1. Perlin K. Course in advanced image synthesis[C]//ACM SIGGRAPH Conference. 1984, 18.
  2. Perlin K. An image synthesizer[J]. ACM Siggraph Computer Graphics, 1985, 19(3): 287-296.
  3. Perlin K, Hoffert E M. Hypertexture[C]//ACM SIGGRAPH Computer Graphics. ACM, 1989, 23(3): 253-262.
  4. Perlin K. Making noise[C]//Proc. of the Game Developer Conference. 1999.
  5. Perlin K. Improving noise[C]//ACM Transactions on Graphics (TOG). ACM, 2002, 21(3): 681-682.
  6. Perlin K. Improving noise[C]//ACM Transactions on Graphics (TOG). ACM, 2002, 21(3): 681-682.
  7. Perlin K. Improving noise[C]//ACM Transactions on Graphics (TOG). ACM, 2002, 21(3): 681-682.
  8. Perlin K. Making noise[C]//Proc. of the Game Developer Conference. 1999.
  9. Perlin K. Noise hardware[J]. Real-Time Shading SIGGRAPH Course Notes, 2001.
  10. Gustavson S. Simplex noise demystified[J]. Linköping University, Linköping, Sweden, Research Report, 2005.
  11. Gustavson S. Simplex noise demystified[J]. Linköping University, Linköping, Sweden, Research Report, 2005.