美文网首页基础知识视觉处理
图形学中常见噪声生成算法综述

图形学中常见噪声生成算法综述

作者: 离原春草 | 来源:发表于2021-02-01 20:14 被阅读0次

    对于普通人而言,噪声通常是都是有害的,而在图形学中,噪声却经常被用来生成一些非常优美的效果,比如天空的云层,地形,水面波形等,还可以用于生成迷宫。

    噪声生成云层 - iq 噪声生成地形 - Ian Parberry 噪声水波 - frankenburgh 噪声迷宫 - Patricio

    对于图形学而言,噪声通常会用作程序化效果生成(procedural generation,如前面列举的地形水面云层等),其最开始在图形学中引进,是为了代替贴图给物件添加纹理以解决电脑内存不足的问题(不过噪声的计算通常比贴图采样要慢一点,因此在内存重组的现在通常是直接使用噪声贴图来代替shader的随机数计算),但是并不是所有的噪声都是有用的,只有那些数据具有一定的连贯性的噪声才算是有用的噪声,而如果噪声不连贯的话,在进行贴图采样后,得到的结果就会呈现一种混乱的状态,这种对于程序化生成而言并没有什么作用,因此图形学中的一个理想的噪声应该具备如下几个特性:

    1. 伪随机(不变性):所谓的噪声只是看起来随机而已,实际上,需要保证在同样的输入下,肯定能够得到同样的输出,否则可能出现渲染的结果随着时间或者观察位置而变化,这就不够物理了,而且结果不可控也跟实际需要不符合。
    2. 只返回一个float值,不管输入是几维的,只返回一个float。
    3. 噪声通常是带限的(band-limited),噪声频率过高通常会导致锯齿(镜头旋转等情况下常见),因此通常其频率范围都是有限的,不过对于一些平缓(大尺寸)变化的情形需要一些低频噪声,而对于一些细节变化则需要一些高频噪声。
    4. 噪声需要具有一定的连续性,比如某些情况下需要计算噪声的导数,甚至需要计算高阶微分,因此对于噪声的连续性有一定的要求。
    5. 四方连续,为了保证tiling时不会出现肉眼可辩的缝隙,需要保证上下左右四个方向都是连续的(如果使用了大量tiling可能会导致重复纹样,而解决重复的做法就是将tiling尺寸设得足够大,虽然可能会引入其他问题,但是这个问题可以通过其他方式来规避)。

    图形学中常用的噪声种类很多,我们这边可能无法一一覆盖,只能先介绍几种已经遇到过的噪声的实现算法,后面再遇到新的噪声会不断添加进来。

    1. Value Noise

    Value Noise是最简单的一类噪声,其实现算法非常简单,以2D为例,我们在一个规整的2D网格上的每个顶点(如下图中的每个红色小圆点)放置一个随机数(通常范围在[0, 1]之间),之后使用线性插值填充每个小方格,得到的结果就是Value Noise。

    2D网格

    Value Noise的生成算法可以用如下的代码表示:

    vec3 valueNoise(vec2 uv)
    {
        //int position used for random number generation
        vec2 intPos = floor( uv );
        
        //frac position used for interpolation
        vec2 fracPos = fract( uv );
        
        //get the interpolation weights(u) and weights derivatives(du)
    #if 1
        // quintic interpolation
        vec2 u = fracPos * fracPos * fracPos * (fracPos *( fracPos * 6.0 - 15.0) + 10.0);
        vec2 du = 30.0 * fracPos * fracPos * (fracPos * (fracPos - 2.0) + 1.0);
    #else
        // cubic interpolation
        vec2 u = fracPos * fracPos * (3.0 - 2.0 * fracPos);
        vec2 du = 6.0 * fracPos *( 1.0 - fracPos);
    #endif    
        
        //generate 4 different random value on 4 neighbor vertices
        float va = hash2d( intPos + vec2(0.0, 0.0) );
        float vb = hash2d( intPos + vec2(1.0, 0.0) );
        float vc = hash2d( intPos + vec2(0.0, 1.0) );
        float vd = hash2d( intPos + vec2(1.0, 1.0) );
        
        float k0 = va;
        float k1 = vb - va;//horizontal
        float k2 = vc - va;//vertical
        float k4 = va - vb - vc + vd;
        
        //mix(mix(va, vb, u.x), mix(vc, vd, u.x), u.y);
        float value = k0 + k1 * u.x + k2 * u.y + k4 * u.x * u.y;
        
        //vec2(d value / du.xy)
        vec2 derivative = du * (u.yx * k4 + vec2(k1, k2));
        return vec3(value, derivative); 
    }
    

    注释中有比较详细的解释,这里就不赘述其实现原理了,输出结果如下图所示:

    Value Noise

    其中左侧是仅仅输出Value Noise的效果,右边则是输出了Gradient后的效果

    2. Gradient Noise

    前面介绍的Value Noise是通过对周边顶点的随机Value进行插值来得到噪声贴图的,而Gradient Noise的实现原理与Value Noise类似,不同的是,这里是通过对周边顶点的Gradient(梯度,可以理解为某个点的速度,常用向量来表示)进行插值来输出噪声贴图。

    根据插值顶点选取算法的不同,这里又有不同的细分,Perlin Noise与前面的Value Noise类似,都是选取周边四个顶点(如果是3D的,就是周边8个顶点,以此类推)的数据进行插值,而Simplex Noise则不同,选取的是等边三个顶点的数据(如果是3D,选取的就是正四面体的四个顶点进行插值),下面来看一下实现细节。

    对梯度进行插值,这里有一个问题需要解决,那就是对向量的插值,得到的结果肯定还是向量,而前面说过,噪声的输出结果应该是一个浮点数,那么要怎么实现这二者的转换呢?

    这里的做法是将当前像素点到对应顶点的连线作为一个向量,与这个顶点的梯度进行点乘,就得到了对应的浮点数,之后再对这个浮点数应用与Value Noise一样的插值算法,就能得到对应的噪声结果了。

    2.1 Perlin Noise

    Perlin Noise是一种非常常见的Gradient Noise,其实现算法给出如下,其插值算法与Value Noise相同,不同的只是插值是将梯度与当前点到对应顶点的方向向量的点乘结果作为数据:

    // return gradient noise (in x) and its derivatives (in yz)
    //  https://www.shadertoy.com/view/XdXBRH
    vec3 perlin2DNoise(in vec2 uv, bool revert)
    {
        vec2 i = floor(uv);
        vec2 f = fract(uv);
    
    #if 1
        // quintic interpolation
        vec2 u = f * f * f * (f * (f * 6.0 - 15.0)+10.0);
        vec2 du = 30.0 * f * f * (f * (f - 2.0)+1.0);
    #else
        // cubic interpolation
        vec2 u = f * f * (3.0 - 2.0 * f);
        vec2 du = 6.0 * f * (1.0 - f);
    #endif    
        
        //random gradients
        vec2 ga = hash2d2(i + vec2(0.0,0.0));
        vec2 gb = hash2d2(i + vec2(1.0,0.0));
        vec2 gc = hash2d2(i + vec2(0.0,1.0));
        vec2 gd = hash2d2(i + vec2(1.0,1.0));
        
        //random values by random gradients
        float va = dot(ga, f - vec2(0.0,0.0));
        float vb = dot(gb, f - vec2(1.0,0.0));
        float vc = dot(gc, f - vec2(0.0,1.0));
        float vd = dot(gd, f - vec2(1.0,1.0));
    
        //mix(mix(va, vb, u.x), mix(vc, vd, u.x), u.y);
        float value = va + u.x * (vb - va) + u.y * (vc - va) + u.x * u.y * (va - vb - vc + vd);
        
        //mix(mix(ga, gb, u.x), mix(gc, gd, u.x), u.y);
        vec2 derivatives = ga + u.x * (gb - ga) + u.y * (gc - ga) + u.x * u.y * (ga - gb - gc +gd) + du * (u.yx * (va - vb - vc + vd) + vec2(vb,vc) - va);
        
        if(revert)
            value = 1.0 - 2.0 * value;
            
        return vec3(value, derivatives);
    }
    

    对应的结果图如下所示:

    Perlin Noise - revert off Perlin Noise - revert on

    根据这个思路,还可以将噪声继续扩展到3d,基本实现没有太大区别,这里就直接跳过了。

    2.2 Simplex Noise

    实际上,Simplex噪声跟Perlin噪声都是Ken Perlin发明的,后者是对前者的优化替代,Simplex实际上是一种算法,既可以用于实现Value Noise,同样也可以用于实现Gradient Noise,不过由于Gradient Noise的应用范围更广,因此这里我们就直接跳过Value Noise部分,只介绍用于实现Gradient Noise的部分。

    Simplex Noise与Perlin Noise的区别在于其插值时所选取的周边顶点的算法不同,具体而言,是选取此像素所从属的grid中的正三角形(等边三角形)的三个顶点(即将Perlin Noise中的插值正方形沿着对角线一分为二,选取当前像素所在的那个正三角形的三个顶点,对应到3D空间,Perlin使用的是立方体的8个顶点,而Simplex使用的则是连接相邻三个面的对角线组成的四面体转换后的正立方体的四个顶点)作为插值的数据源。

    相对Perlin Noise,Simplex的实现更为简洁,其成本也更低。与前面计算某个像素对应的噪声值需要通过对周边顶点数据进行插值不同,Simplex采用的是衰减函数,比如根据某个顶点到此像素的距离来计算此顶点数据对于此像素的贡献,之后将周边顶点的贡献进行累加就得到了最终的输出结果。

    衰减Perlin 插值Perlin

    虽然也可以使用衰减来计算Perlin等噪声,但是如上面两图所示,使用衰减函数来计算Perlin噪声等通过hypercube(正方形,立方体以及更多维的超立方体,统称hypercube)进行影响的算法输出的结果会存在问题,结果并不一致。

    前面说到,Simplex噪声来自于正三角形(正四面体)的数据衰减,那么这个正三角形是怎么来的呢?我们知道,一个2D平面,既可以使用正方形进行无缝平铺,这种tiling方式对应的就是前面Value/Perlin Noise的计算基础,同时也可以使用正三角形进行平铺,而这对应的则是Simplex噪声的实现基础,这里的一个问题就是这二者是如何转换的,毕竟我们平常使用的基本上都是grid,也就是正方形的平铺方式。

    这个转换过程,在Simplex Noise, keeping it simple有比较详细的说明,这里我们做一下简单的搬运。

    正方形到两个正三角形

    如上图所示,一个正方形,经过一定的skew(挤压形变)之后,可以变换成两个正三角形,维持左下角顶点位置以及挤压方向所对应的正方形的对角线(图中两个蓝色正三角形重叠的边)方向不变,我们可以推断,右上角的顶点的位移长度肯定是左上角以及右下角顶点的位移长度的两倍(将正方形沿着连接左上右下两个顶点的对角线进行划分,刚好将正方形一分为二,在这种布局下沿着右上-左下对角线进行挤压,就能算出 右上顶点的挤压幅度正好等于处于对角线上的两个顶点的挤压幅度的两倍)。

    实际上,相对于上面这种口头描述的变换关系,正三边形与正四边形之间的变换还有一种公式化的理论推导,Simplex noise -wikipedia中说明了多维空间中从单形(在2D空间中,对应的就是等边三角形)到正超晶格体(在2D空间中,对应的就是正方形)之间的skew变换是有一套固有的公式的。

    对于等边三角形上的某个点<x, y>,变换到四边形上的某点<x1, y1>,有:
    x1 = x + (x + y) * F \\ y1 = y + (x + y) * F \\ F = \frac{\sqrt 3 - 1}{2}

    反过来,则有:
    x = x1 - (x1 + y1) * G \\ y = y1 - (x1 + y1) * G \\ G = \frac{3 - \sqrt 3}{6}

    有了这两者之间的变换关系,那么我们就可以来进行相应的变换以及噪声计算了

    变换示意图

    如上图所示,在正方形空间的三个顶点A<0, 0>,B<1, 0>,C<1,1>经过变换后,变成了上图右小图中的蓝色等边三角形,那么最终的噪声计算程序就如下所示:

    // uv lies in triangle space
    float simplexNoise(in vec2 uv)
    {
        //transform from triangle to quad
        const float K1 = 0.366025404; // (sqrt(3)-1)/2;
        //transform from quad to triangle
        const float K2 = 0.211324865; // (3 - sqrt(3))/6;
    
        //Find the rectangle vertex
        vec2  quadIntPos = floor(uv + (uv.x + uv.y)*K1);
        //relative coorindates from origin vertex A
        vec2  vecFromA = uv - quadIntPos + (quadIntPos.x + quadIntPos.y)*K2;
        float IsLeftHalf = step(vecFromA.y,vecFromA.x); 
        vec2  quadVertexOffset = vec2(IsLeftHalf,1.0 - IsLeftHalf);
        //vecFromA - (quadVertexOffset + (quadVertexOffset.x + quadVertexOffset.y ) * K2)
        vec2  vecFromB = vecFromA - quadVertexOffset + K2;
        //vecFromA - (vec(1, 1) + (1 + 1 ) * K2)
        vec2  vecFromC = vecFromA - 1.0 + 2.0 * K2;
        vec3  falloff = max(0.5 - vec3(dot(vecFromA,vecFromA), dot(vecFromB,vecFromB), dot(vecFromC,vecFromC)), 0.0);
        vec2 ga = hash2d2(quadIntPos + 0.0);
        vec2 gb = hash2d2(quadIntPos + quadVertexOffset);
        vec2 gc = hash2d2(quadIntPos + 1.0);
        float simplexGradient = vec3(dot(vecFromA,ga), dot(vecFromB,gb), dot(vecFromC, gc));
        vec3  n = falloff * falloff * falloff * falloff * simplexGradient;
        //blend all vertices' contribution
        return dot(n, vec3(70.0));
    }
    

    这个程序相对此前的实现略显复杂,这里做一个简单的解释,可以结合注释进行理解:

    1. 所有的计算包括输入默认是在三角形空间中进行的
    2. 先将uv坐标从等边三角形空间转换到正方形空间,转换参数F对应的是K1,得到对应的正方形空间的左下角顶点的坐标
    3. 根据左下角坐标,计算当前uv坐标到三角形空间中三个顶点的方向向量:vecFromA/B/C
    4. 根据到三个顶点的距离,计算衰减系数falloff
    5. 使用随机函数计算三个顶点上的随机梯度向量
    6. 使用点乘计算三个梯度对应的噪声结果
    7. 使用衰减系数计算真实的贡献值
    8. 使用点乘将三个顶点的贡献累加到一起

    最终结果如下图所示(左边输出的是Simplex Noise,右边输出的则是三个顶点的贡献值):

    Simplex Noise

    3. Voronoi Noise与Worley Noise

    Voronoi Noise与Worley Noise在形态上十分相似,在图形学中的应用也基本一致,比如同样用于进行云层创建,水底焦散现象模拟等,那同样的噪声为什么会有两个名字呢?

    实际上图形学中最开始使用的是Voronoi噪声,只是这种噪声的实现算法消耗比较高,后面Steven Worley对齐进行了改进,提出了以其名字命名的Worley噪声。下面我们一起来看一下这两种噪声的实现算法。

    voronoi

    如上图所示,Voronoi噪声是通过在空间中生成随机分布的多个特征点,之后对于每个需要计算的像素,对所有的特征点进行遍历,找到距离其最近的特征点,以其对应的特征值作为此像素的值进行输出。算法思路很简单,但是由于需要对每个特征点进行遍历,整个算法的复杂度就变得很高了,为了降低计算的消耗,Worley噪声就应运而生了。

    Worley噪声是通过将空间(2D/3D)划分成一个个的cell(正方形/立方体),在每个cell中的随机位置随机生成一个特征点,之后对于每个待计算的像素,搜寻周边的cell,找到距离其最近的噪点,之后以距离此噪点的距离作为当前像素的噪声结果,就得到了对应的Worley噪声。相对于Voronoi噪声,Worley算法的改进点在于将搜寻范围从所有特征点限定在了周边的若干个cell之中(理论上最正确的搜索范围是周边25个cell,但实际上如果噪声函数选取得当,使用九宫格进行搜索也能得到正确的结果)

    Worly噪声在2D空间中的示例代码如下所示:

    float Worley2D(vec2 uv, bool revert)
    {
        float Dist = 16.0;
        vec2 intPos = floor(uv);
        vec2 fracPos = fract(uv);
        //search range
        const int Range = 2;
        for(int X = -Range; X <= Range; X++)
        {
            for(int Y = -Range; Y <= Range; Y++)
            {
                float D = distance(hash2d2(intPos + vec2(X,Y)) + vec2(X,Y), fracPos);
                // take the feature point with the minimal distance
                Dist = min(Dist,D);
            }
        }
        //use the distance as output
        if(revert)
            return 1.0 - 2.0 * Dist;
        else
            return Dist;
    }
    

    输出结果如下图所示:

    Worley Noise - revert off Worley Noise - revert on

    注意这里使用的搜索范围为周边25个cell,如果将之更换成9个cell,会发现结果会存在异常,这是因为在某些随机函数作用下,九宫格搜索会漏掉一些正确解导致:

    9cell Worley

    4. Noise FBM

    有时候单一频率的噪声不足以满足需求,会需要使用多级噪声累加的结果来实现程序化生成,这种方式我们称之为Fractal Brownian Motion,简称FBM,下图展示了FBM的基本形式,简单来说就是将多个不同频率的噪声按照不同的振幅进行混合:

    FBM

    下面以Worley噪声为例,给出混合的代码实现:

    float Worley2DFBM(vec2 uv, int octave, bool revert)
    {
        float noise = 0.0;
        float frequency = 1.0;
        float amplitude = 1.0;
        for(int i = 0; i < octave; i++)
        {
            noise += Worley2D(uv * frequency, revert) * amplitude;
            frequency *= 2.0;
            amplitude *= 0.5;
        }
        return noise;
    }
    

    输出结果如下图所示:

    Worley FBM revert on

    对Simplex噪声应用FBM,得到结果如下图:

    Simplex FBM

    对Perlin噪声应用FBM,得到结果如下图:

    Perlin FBM

    5. Curl Noise

    Curl噪声在图形学中有着广泛的应用,比如可以用于对粒子位置进行调制,使之产生卷曲的效果;比如可以对烟雾水流效果进行调制,生成湍流扰动效果等。

    相对于其他的流体模拟算法,Curl Noise的生成算法算是十分简单的,但是应用起来效果却并没有减色多少。

    Curl噪声中的Curl可以看成是跟加减乘除号同等的一种运算符号,其输入数据是一个向量,经过curl运算之后,就得到了一个divergence free(无散度)的向量场,这里先介绍下什么是向量的divergence,divergence的中文称谓是‘散度’:
    div \overrightarrow a = \triangledown \cdot \overrightarrow a = \frac{\partial a_{x}}{\partial x} + \frac{\partial a_y}{\partial y} + \frac{\partial a_z}{\partial z}

    散度指的是向量三个分量在对应坐标轴方向上的偏微分之和,从物理上来说,指的是一个向量场在某个给定的位置散开或者说收敛的程度,日常生活中常见的流体比如水流,空气,烟雾等都是divergence-free(无散)的。

    curl噪声从物理上来说,可以用来表征用于对向量进行转向的力的大小。

    下面我们来介绍一下Curl噪声的实现算法,对一个潜在的3D向量场\Psi而言,令:
    \overrightarrow \Psi = (\Psi_1, \Psi_2, \Psi_3)

    我们可以计算出其Curl Velocity算子\triangledown \times
    \overrightarrow v(x, y, z) = (\frac{\partial \Psi_3}{\partial y} - \frac{\partial \Psi_2}{\partial z}, \frac{\partial \Psi_1}{\partial z} - \frac{\partial \Psi_3}{\partial x}, \frac{\partial \Psi_2}{\partial x} - \frac{\partial \Psi_1}{\partial y})
    而对于2D情景而言,向量场的Curl算子是一个标量,可以用如下公式进行计算:
    \overrightarrow v(x,y) = (\frac{\partial \Psi}{\partial y} ,- \frac{\partial \Psi}{\partial x})

    根据流体力学原理可以得知,上述的速度场是无散的,即\triangledown \cdot \overrightarrow v = 0

    具体要怎么做呢,以2D为例,我们这里取2D Perlin噪声作为向量场\Psi = PerlinNoise(x, y),那么最终的Curl噪声\overrightarrow v(x, y)就可以用如下的公式表示:
    \overrightarrow v(x,y) = (\frac{PerlinNoise(x, y)}{\partial y} ,- \frac{PerlinNoise(x,y)}{\partial x})

    相关实现代码给出如下:

    vec2 curlNoise(vec2 uv)
    {
        float eps = 0.00001;
        float x = uv.x;
        float y = uv.y;
        //Find rate of change in X direction
        int firstOctave = 3;
        int accumOctaves = 3;
        bool revertPerlin = false;
        float n1 = perlin2DNoise(vec2(x, y + eps), revertPerlin).x;
        float n2 = perlin2DNoise(vec2(x, y - eps), revertPerlin).x;
        //Average to find approximate derivative
        float a = (n1 - n2)/(2.0 * eps);
    
        //Find rate of change in Y direction
        float n3 = perlin2DNoise(vec2(x + eps, y), revertPerlin).x;
        float n4 = perlin2DNoise(vec2(x - eps, y), revertPerlin).x;
        //Average to find approximate derivative
        float b = (n3 - n4)/(2.0 * eps);
    
        //Curl
        return vec2(a, -b);
    }
    

    输出的Curl Noise如下图所示:

    Curl Noise

    将之用速度向量来表示,如下图所示:

    Curl Velocity Field

    其中灰色部分表示的是原始的Perlin噪声,而白色箭头表示的则是Curl噪声向量的方向与大小。

    提高噪声频率,得到的结果如下面两图所示:

    High Freq Curl Noise High Freq Curl Velocity Field

    6. White Noise

    白噪声(White Noise)是一种在各个频率上的强度都十分均匀的噪声,这种噪声并不平滑,而自然界的各种纹理实际上都是连续的,因此通常不适合用于贴图生成(比如生成树皮纹路)。

    白噪声 vs 普通噪声

    实际上,所谓的白噪声并不是特指的某一种噪声,而是一种信号的统计模型。在离散采样中,白噪声具有如下特点:

    1. 各个采样点之间完全没有数值上的联系
    2. 信号的均值为0,方差有限。

    实现白噪声最简单的算法就是直接使用一个随机数作为返回值,比如我们采用如下的算法:

    vec3 whiteNoise(vec2 uv)
    {
        return vec3(hash2dsin(uv));
    }
    

    得到的结果如下图所示:

    white noise

    此外,通过对算法进行仔细设计,还可以保证输出的白噪声在各个数值上的概率基本一致。

    Source Code

    本文的所有源码与对应的效果,都已放入到shadertoy中,有兴趣的同学可前往测试修正。

    Reference

    1. Waterworld - frankenburgh
    2. Clouds - iq
    3. Designer Worlds: Procedural Generation of Infinite Terrain from Real-World Elevation Data
    4. Value Noise and Procedural Patterns: Part 1
    5. Generative designs
    6. Simplex Noise, keeping it simple
    7. Simplex Noise(一)
    8. Simplex noise -wikipedia
    9. Worley Noise的Shader生成
    10. White noise -wikipedia
    11. Curl Noise - Peter Werner
    12. Divergence(散度) of a vector field
    13. Intro to Curl Noise
    14. Curl-Noise for Procedural Fluid Flow - Siggraph 2007

    相关文章

      网友评论

        本文标题:图形学中常见噪声生成算法综述

        本文链接:https://www.haomeiwen.com/subject/oxtctltx.html