65.9K
CodeProject 正在变化。 阅读更多。
Home

一个通用的晶格噪声算法,是Perlin噪声的演进版。

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (22投票s)

2014年6月12日

MIT

11分钟阅读

viewsIcon

36367

downloadIcon

1282

开发一种新的通用晶格噪声算法,它超越了Perlin噪声,并扩展了程序化纹理创建的可能性。

引言

Perlin噪声是目前最重要的纹理生成算法。这一点已经持续了二十多年。尽管此后设计出了更新更好的算法,但它们仍然无法在质量与性能的比率上超越Perlin噪声。

本文将开发一种通用的噪声算法,Perlin噪声是其一个特例,但该算法能极大扩展可创建程序化噪声的多样性。这里有一些例子。

 

文章顶部提供了使用该算法创建纹理的小型应用程序的源代码。请注意,代码的编写目的是便于改编和修改,而不是为了优化性能。在性能方面有巨大的改进空间(例如,在中等配置的笔记本电脑上,生成2048x2048数组的时间大约在50-500毫秒之间是可以实现的)。

 

晶格噪声概述

晶格噪声是用于程序化噪声生成的一类算法。晶格噪声的共同点是有一个晶格或网格,它将区域细分为更小的部分(通常是正方形,但不一定是)。算法遍历网格,在每个正方形内生成噪声函数。这种噪声通常看起来“模糊”,因此通过使用更小的网格重复几次该过程,将这些更小的网格叠加在之前的网格之上,以提高清晰度。每个层都有一个与其正方形大小成比例的窄带频率范围。随着我们添加更小的网格层,我们增加了噪声中的高频成分,提高了其清晰度。

这些层中的每一层都将被称为一个“倍频程”。

最主要的(也是几乎唯一)常用的晶格噪声算法是Perlin噪声及其衍生物(如Simplex算法)。

在网格正方形内生成噪声函数

让我们考虑网格内的一个大小为λ的单独正方形。传统的Perlin噪声函数是通过在正方形内插值梯度场生成的。单元格内一个由(u,v)定义的点的噪声值将通过梯度au+bv的值进行插值,其中(a,b)对应于单元格每个角点的梯度场值。

这就是传统Perlin噪声的定义方式。现在,让我们改变一下方法。

在不改变底层逻辑算法的情况下,我们将尝试从不同的角度来看待问题,以便更容易理解算法。

与其认为噪声函数是在每个单元格内部插值生成的,不如认为噪声函数是由该点定义的函数在该网格点的邻域内确定的。从数学角度来看,我们将一个点(xi, yi)的邻域内的噪声函数定义为两个不同函数的乘积:一个邻域函数,它取决于场值fk(xi, yi),以及一个衰减函数,它会随着我们离开点(xi, yi)而衰减至零。

n(xi+u,yi+v) = fprox(u, v, λ, fk(xi, yi)) * ffade(u, v, λ)

这通过一个例子可以很容易理解:让我们将fprox定义为由使用(xi, yi)计算的PRN(伪随机数)定义的常量值(这可以使用伪随机哈希函数或白噪声轻松完成)。

作为衰减函数,我们例如使用以下函数:

fhermite(uλ, vλ) = (1 – 3uλ2 + 2uλ3) (1 – 3vλ2 + 2vλ3)

其中uλ = u / λvλ = v / λ。这个函数是经典的三次Hermite插值函数。

将这两个函数相乘后得到的结果如下。

“山丘”的顶部对应于网格点(xi, yi),它会逐渐衰减并在接近的网格点处达到零。这个函数将被加在每个网格点附近,创建一个平滑的噪声函数,如下所示(请注意,图像中的网格纹理只是为了更好地可视化形状,与网格点无关)。

现在我们可以添加一个大小为一半的第二个倍频程。我们得到的结果是这样的。

我们可以继续添加更多倍频程,使用越来越小的尺寸。这将是最终的结果。

我们实际上已经得到一个程序化生成的噪声标量函数(我们可以将其表示为纹理、地形或其他任何方式),对于这些特定的函数,它对应于一个称为中点位移的晶格噪声算法。原始的中点位移算法使用线性插值,但最终结果(在添加了最小可能的倍频程之后)将是相同的。

现在让我们想象一下,我们使用以下邻域函数:

fprox(u, v) = fa(xi, yi) * u + fb(xi, yi) * v

这听起来是否熟悉?一方面,它似乎类似于Perlin噪声的一个步骤(这并非偶然)。另一方面,它会让你想起泰勒级数。事实上,我们可以将前面的邻域函数视为二维泰勒级数的零阶项,正如这个可以被视为一阶项一样。

将其与衰减函数(与之前相同的函数)相乘,我们得到的结果如下。

请注意,中心对应于网格点(xi, yi),z=0。我们在这里所做的是在网格点创建一个平坦的“斜坡”,当接近相邻网格点时,它会平滑地衰减。如果我们沿着整个网格重复这个操作,我们就会得到这样的结果。

经过多个倍频程

这就是使用Perlin噪声算法可以获得的结果。区别在于,我们不受特定函数的约束:如果我们使用不同的邻域函数,我们将获得不同类型的纹理。

事实上,您可以在图像左侧看到一张应用了前面Perlin噪声状函数获得的纹理。右侧的纹理是使用不同函数获得的。

 

 

代码概述

本文中的源代码是一个程序化纹理生成器,包含以下文件。

- MainWindow.xaml。WPF界面。

- MainWindow.xaml.cs。主应用程序。代码非常直观,算法代码可以在这里找到。Redraw( )方法执行程序化纹理算法。此纹理存储在锯齿状数组heightmap[ ][ ]中(该算法的开发目的是创建地形,这是晶格噪声的另一种用途)。SetPicture( )方法使用提供的对比度和亮度值将此锯齿状数组转换为图形图像。CreatePictureTransparent( )方法仅在导出图像并选择导出半透明图像时使用。

- ProximityFunctions.cs。包含邻域函数委托。

- FadeFunctions.cs。包含衰减函数委托。

- PseudoRandom.cs。PRN生成器。静态方法floatHashRandom(float x, float y, int index)根据(x, y)提供一个范围在-1到+1之间的哈希伪随机数。这意味着每个(x, y)都定义了一个浮点数。由于没有空间连续性,它可以被认为是随机的。基本上,它是一个类似白噪声的函数。整数index允许选择不同的PRN函数,其范围从0到4。要重新初始化PRN,请使用方法Initiate(int seed)

- Auxiliary.cs。辅助类。它提供了一些方法来处理锯齿状数组,并将System.Drawing.Bitmap类加载到BitmapSource(WPF需要)中。

代码细节

邻域函数和衰减函数委托

这两个委托是算法的核心。更改它们将导致获得不同类型的纹理。例如:使用TriangularEdgeII委托不会创建像之前看到的平滑表面,而是这种类型的表面

这个是用Curvature rigged委托创建的

这个是用Semirigged gradient一个创建的

源代码提供了一些不错的委托供您尝试,但它们并非唯一。您可以使用任何您想尝试的函数。这仅仅是创造力的问题。

这两个委托都在MainWindow.xaml.cs中定义。

public delegate float FadeFunction(float x, float y);
FadeFunction Fade;
public delegate float ProximityFunction(float x, float y, float lambda, float xHash, float yHash);
ProximityFunction Proximity;

并在Redraw( )方法中实例化。

FadeFunction Fade = FadeF();
ProximityFunction Proximity = ProximityF();

FadeF( )ProximityF( )函数根据您在WPF界面上的选择返回正确的委托。

邻域函数委托

例如,让我们来分析一下梯度委托,它代表了传统的Perlin噪声。

public static float Gradient(float x, float y, float lambda, float xHash, float yHash)
{
    return (x * PseudoRandom.HashRandom(xHash, yHash, 0) + y * PseudoRandom.HashRandom(xHash, yHash, 1)) / (lambda);
}

这个委托在点(xi, yi)的邻域内创建函数。变量代表:

- xy。计算点的相对坐标,将(xi, yi)视为坐标原点。

- lambda。网格正方形的大小。

- xHashyHash(xi, yi)的绝对坐标值。

该委托返回一个线性函数ax + by,其中ab是两个随机值(范围在-1到1之间),它们取决于(xi, yi)的值。代码通过PseudoRandom.HashRandom(xHash, yHash, 0)和PseudoRandom.HashRandom(xHash, yHash, 1)获取这两个值。

衰减函数委托

衰减函数用于乘以邻域函数,使其在我们离开网格点时“衰减”。基本上它是一个连续函数,在坐标原点的值应为1,对于任何| x | ≥ 1| y | ≥ 1的点,其值为零。

例如,让我们考虑Hermite Cubic函数:

public static float Hermite(float x, float y)
{
    return (1 - (x * x * (3 - 2 * x))) * (1 - (y * y * (3 - 2 * y)));
}

(x,y) = (0,0)时,此函数返回1,当x ≥ 1y ≥ 1时返回0(当x或y达到1时函数取零值。它不会在此之后返回零,但也不会在此之外计算,所以这不会是问题)。除此之外,一阶导数在原点以及x=1或y=1处都为零。二阶导数在这些点不为零。Hermite Quintic符合最后一个条件(这使其稍微更平滑)。

算法本身

这就是算法。

size = PictureSize();
int numberOfOctaves = Octaves();
int initialStep = InitialStep();
FadeFunction Fade = FadeF();
ProximityFunction Proximity = ProximityF();
PseudoRandom.SetCSharpRandomClasUsed(IsCSRandomUsed());
PseudoRandom.SetScale(1024f / size);

for (int x = 0; x < size + 1; x += 1)
{
    for (int y = 0; y < size + 1; y += 1)
    {
        heightmap[x][y] = 0.5f;
    }
}

for (int octave = 0; octave < numberOfOctaves; octave++)
{
    int sizeGrid;
    float sizeGridF;
    sizeGrid = initialStep / (int)Math.Pow(2.0, octave);
    int init = 0;
    if (rb_G01.IsChecked == true) { init = 0; }
    if (rb_G02.IsChecked == true) { init = - sizeGrid / 2; }
    if (rb_G03.IsChecked == true) { init = - sizeGrid / 4; }

    sizeGridF = (float)sizeGrid;
    float relativeSize = sizeGridF / size;
    float persistence = (float)Math.Pow(relativeSize, 1.0 - Persistence_slider.Value);

    int gridU, gridV;
    float hBase;                          

    if (sizeGrid >= 1)
    {
        for (int x = init; x < size + 1; x += sizeGrid)
        {
            if (x == size - sizeGrid) { gridU = sizeGrid + 1; } else { gridU = sizeGrid; }
            for (int y = init; y < size + 1; y += sizeGrid)
            {
                if (y == size - sizeGrid) { gridV = sizeGrid + 1; } else { gridV = sizeGrid; }

                for (float u = 0; u < gridU; u++)
                {
                    for (float v = 0; v < gridV; v++)
                    {
                        if ((x + (int)u > size - 1 || x + (int)u < 0 || y + (int)v > size -1 || y + (int)v < 0) == false)
                        {
                            float us = u / sizeGridF;
                            float vs = v / sizeGridF;
                            hBase =
                                        Proximity(u, v, sizeGridF, (float)x, (float)y)
                                        * Fade(us, vs)

                                        + Proximity(u - sizeGridF, v, sizeGridF, (float)x + sizeGridF, (float)y)
                                        * Fade(1 - us, vs)

                                        + Proximity(u, v - sizeGridF, sizeGridF, (float)x, (float)y + sizeGridF)
                                        * Fade(us, 1 - vs)

                                        + Proximity(u - sizeGridF, v - sizeGridF, sizeGridF, (float)x + sizeGridF, (float)y + sizeGridF)
                                        * Fade(1 - us, 1 - vs);

                            heightmap[x + (int)u][y + (int)v] += hBase * persistence;
                        }
                    }
                }
            }
        }
    }
}

 

让我们分开一步一步来看。

size = PictureSize();

这是纹理的大小。PictureSize( )只是返回WPF界面上选择的大小。建议使用2的幂作为尺寸。512、1024、2048或4096都是不错的选择。

int numberOfOctaves = Octaves();
int initialStep = InitialStep();

在WPF界面上选择的更多值。numberOfOctaves控制算法重复的次数(每个周期称为“倍频程”)。initialStep是初始λ,即初始网格正方形的大小。例如,如果整个数组是1024x1024,这个初始值可以是1024、512、256、128等。更改应用程序中的“初始倍频程大小”选项以查看效果。

FadeFunction Fade = FadeF();
ProximityFunction Proximity = ProximityF();

这里是我们之前讨论过的委托。

PseudoRandom.SetCSharpRandomClasUsed(IsCSRandomUsed());

如果设置为false,这允许使用辅助PRN生成器。目前未使用。

PseudoRandom.SetScale(1024f / size);

这会调整哈希PRN函数的尺度。否则,更改图片大小会导致不同的纹理(这不是一个好的结果;如果您更改大小,您更希望获得相同纹理但分辨率更高,而不是一个全新的不同纹理)。

for (int x = 0; x < size + 1; x += 1)
{
    for (int y = 0; y < size + 1; y += 1)
    {
        heightmap[x][y] = 0.5f;
    }
}

heightmap[ ][ ]的范围应为0到1(预期,但不限于此范围)。此循环将数组初始化在此范围的中间点。

for (int octave = 0; octave < numberOfOctaves; octave++)
{
    int sizeGrid;
    float sizeGridF;
    sizeGrid = initialStep / (int)Math.Pow(2.0, octave);

sizeGrid是网格正方形的大小。每个倍频程,我们将使用比前一个尺寸小一半的尺寸。

    int init = 0;
    if (rb_G01.IsChecked == true) { init = 0; }
    if (rb_G02.IsChecked == true) { init = - sizeGrid / 2; }
    if (rb_G03.IsChecked == true) { init = - sizeGrid / 4; }

这确定了数组的入口点。这是一个小的调整,您可以在界面下的“网格位移”选项中看到效果。初步看起来可以忽略它,它只是一个调整,并非真正必要。您可以认为init = 0。

    sizeGridF = (float)sizeGrid;
    float relativeSize = sizeGridF / size;
    float persistence = (float)Math.Pow(relativeSize, 1.0 - Persistence_slider.Value);

persistence表示一个倍频程的幅度。为了不产生混沌噪声,更高频率需要更小的幅度。relativeSize是网格正方形大小与整个数组大小的比率。使用此值和滑块值,我们可以为每个倍频程计算persistence值。请注意,当滑块值为零时,persistence将与relativeSize值成正比。

    int gridU, gridV;
    float hBase;      

几个局部值。

  if (sizeGrid >= 1)
    {

这可以防止当网格正方形小于1时计算倍频程(这没有意义)。

        for (int x = init; x < size + 1; x += sizeGrid)
        {
            if (x == size - sizeGrid) { gridU = sizeGrid + 1; } else { gridU = sizeGrid; }
            for (int y = init; y < size + 1; y += sizeGrid)
            {
                if (y == size - sizeGrid) { gridV = sizeGrid + 1; } else { gridV = sizeGrid; }

然后我们开始遍历数组中的网格点。请注意我们如何使用sizeGrid值进行步进:我们正在从网格点移动到网格点。

请注意gridU和gridV。它们旨在允许计算数组的右边界和下边界,因为在它们之外将不再有另一个正方形来执行此操作。

                for (float u = 0; u < gridU; u++)
                {
                    for (float v = 0; v < gridV; v++)
                    {

这些循环遍历网格正方形内的点。

                        if ((x + (int)u > size - 1 || x + (int)u < 0 || y + (int)v > size -1 || y + (int)v < 0) == false)
                        {

这个条件与init变量和注释掉的“网格位移”选项有关。忽略它,因为它只是一个调整,对算法并不重要。如果init = 0,则不需要此条件,可以删除。

                            float us = u / sizeGridF;
                            float vs = v / sizeGridF;

相对值。usvs的范围为0到1。

                            hBase =
                                        Proximity(u, v, sizeGridF, (float)x, (float)y)
                                        * Fade(us, vs)

                                        + Proximity(u - sizeGridF, v, sizeGridF, (float)x + sizeGridF, (float)y)
                                        * Fade(1 - us, vs)

                                        + Proximity(u, v - sizeGridF, sizeGridF, (float)x, (float)y + sizeGridF)
                                        * Fade(us, 1 - vs)

                                        + Proximity(u - sizeGridF, v - sizeGridF, sizeGridF, (float)x + sizeGridF, (float)y + sizeGridF)
                                        * Fade(1 - us, 1 - vs);

在每个点,hBase通过将每个正方形的四个角点的Proximity( ) * Fade( )值相加来计算。

                            heightmap[x + (int)u][y + (int)v] += hBase * persistence;
                        }
                    }
                }
            }
        }
    }
}

最后,hBase的值乘以persistence,然后将其添加到heightmap[ ][ ]数组中。

最后的想法

在我看来,晶格噪声算法仍然有很大的发展空间。我希望这能帮助进一步拓宽这些可能性。

这是我在CodeProject上的第一篇文章。任何提示、更正或建议都将受到欢迎。

 

更新

许可证已从Codeproject Open License更改为MIT License,以便代码可以在Blender中使用并符合Blender的许可证要求。

 

© . All rights reserved.