Perlin噪声用于地形生成

4
我正在尝试实现2D Perlin噪声以创建类似Minecraft的地形(实际上Minecraft并不使用2D Perlin噪声),但没有悬崖或洞穴等。 我的方法是创建一个[50] [20] [50] 的方块数组,其中[20]将是数组的最大高度,并且它的值将由Perlin噪声确定。 然后,我将用方块数组填充该数组。
我从这篇文章中阅读到:此文章,但我不明白如何计算4个梯度向量并在我的代码中使用它? 每个相邻的2D数组,例如[2] [3]和[2] [4],是否具有不同的4个梯度向量?
此外,我读到一般的Perlin噪声函数还需要一个数字值作为种子,我在这种情况下应该放在哪里?

1
真正的Perlin噪声在数学上相当困难。尝试平滑哈希函数,这更容易。 或者只需下载代码。 (PDF) - markspace
@markspace 这是Simplex噪声。如果你想要Perlin噪声,可以尝试使用这个美妙的代码块,由其创造者亲自编写。 - mk.
3个回答

5

我将使用可工作的代码来解释Perlin噪声,而不依赖于其他解释。首先,您需要一种在2D点处生成伪随机浮点数的方法。每个点应该相对于其他点看起来是随机的,但诀窍在于相同的坐标应始终产生相同的浮点数。我们可以使用任何哈希函数来实现这一点 - 不仅仅是Ken Perlin在他的代码中使用的那个。这里是一个例子:

static float noise2(int x, int y) {
    int n = x + y * 57;
    n = (n << 13) ^ n;
    return (float) (1.0-((n*(n*n*15731+789221)+1376312589)&0x7fffffff)/1073741824.0);
}

我使用这个代码生成一个“景观”landscape[i][j] = noise2(i,j);(然后将其转换为图像),它总是产生相同的结果:

noise noise noise noise ...

但是那看起来太随机了——像山丘和山谷过于密集。我们需要一种方法来“拉伸”每个随机点,例如在5个点上。对于这些“关键”点之间的值,您需要平滑渐变。
static float stretchedNoise2(float x_float, float y_float, float stretch) {
    // stretch
    x_float /= stretch;
    y_float /= stretch;
    // the whole part of the coordinates
    int x = (int) Math.floor(x_float);
    int y = (int) Math.floor(y_float);
    // the decimal part - how far between the two points yours is
    float fractional_X = x_float - x;
    float fractional_Y = y_float - y;
    // we need to grab the 4x4 nearest points to do cubic interpolation
    double[] p = new double[4];
    for (int j = 0; j < 4; j++) {
        double[] p2 = new double[4];
        for (int i = 0; i < 4; i++) {
            p2[i] = noise2(x + i - 1, y + j - 1);
        }
        // interpolate each row
        p[j] = cubicInterp(p2, fractional_X);
    }
    // and interpolate the results each row's interpolation
    return (float) cubicInterp(p, fractional_Y);
}
public static double cubicInterp(double[] p, double x) {
    return cubicInterp(p[0],p[1],p[2],p[3], x);
}
public static double cubicInterp(double v0, double v1, double v2, double v3, double x) {
    double P = (v3 - v2) - (v0 - v1);
    double Q = (v0 - v1) - P;
    double R = v2 - v0;
    double S = v1;
    return P * x * x * x + Q * x * x + R * x + S;
}

如果您不理解细节,没关系-我不知道Math.cos()是如何实现的,但我仍然知道它是做什么的。而这个函数会给我们带来拉伸、平滑的噪声。 stretchedNoise2函数以一定的比例(大或小)生成一个“景观”,即具有平稳坡度的随机点的景观。现在我们可以在彼此之上生成一系列景观:
public static double perlin2(float xx, float yy) {
    double noise = 0;
    noise += stretchedNoise2(xx, yy,  5) * 1; // sample 1
    noise += stretchedNoise2(xx, yy, 13) * 2; // twice as influential

    // you can keep repeating different variants of the above lines
    // some interesting variants are included below.

    return noise / (1+2); // make sure you sum the multipliers above
}

更准确地说,我们从每个样本中获取点数的加权平均值。
noise2 + 2 * noise3)/ 3 = enter image description here 当您将一堆平滑噪声叠加在一起时,通常使用大约5个逐渐增加“拉伸”的样本,您会得到Perlin噪声。 (如果您理解上述句子,则理解Perlin噪声。)
还有其他实现方式,因为它们以不同的方式执行相同的操作而更快,但是因为现在不再是1983年,并且您正在开始编写景观生成器,所以您不需要了解所有特殊技巧和术语来理解Perlin噪声或进行有趣的事情。 例如:
1)noise 2)noise 3)noise
    // 1
    float smearX = interpolatedNoise2(xx, yy, 99) * 99;
    float smearY = interpolatedNoise2(xx, yy, 99) * 99;
    ret += interpolatedNoise2(xx + smearX, yy + smearY,  13)*1;

    // 2
    float smearX2 = interpolatedNoise2(xx, yy, 9) * 19;
    float smearY2 = interpolatedNoise2(xx, yy, 9) * 19;
    ret += interpolatedNoise2(xx + smearX2, yy + smearY2,  13)*1;

    // 3
    ret += Math.cos( interpolatedNoise2(xx , yy , 5)*4) *1;

抱歉,我不太明白你的意思:/ 你所描述的第一步骤,在维基百科文章中是哪一步呢? - Rei
我可以解释Perlin噪声,但我无法解释其他人的解释。其中许多是不必要的混乱(尽管有些很好)。如果您对上面的某些内容有困难,请告诉我。我进行了一些编辑并添加了一些图片,这可能会有所帮助。 - mk.
@mk。我知道我来晚了将近7年,但我只是想让你知道,这个答案对我在很长一段时间里一直苦苦挣扎的Perlin噪声实现问题帮助很大。非常感谢你! - M_Dragon

0

关于柏林噪声

柏林噪声是为了生成随机连续表面(实际上是程序纹理)而开发的。它的主要特点是噪声在空间中始终连续。

从文章中可以得知:

柏林噪声是一种用于在空间中生成连贯噪声的函数。连贯噪声意味着对于空间中的任意两个点,噪声函数的值在从一个点移动到另一个点时会平滑地变化,也就是说,没有不连续性。

简单来说,柏林噪声看起来像这样:

 _         _    __
   \    __/ \__/  \__
    \__/

但这绝对不是Perlin噪声,因为有间隙:

 _         _ 
  \_    __/  
    ___/    __/

计算噪声(或抑制梯度!)

正如@markspace所说,Perlin噪声在数学上很难。让我们通过生成1D噪声来简化。

想象以下1D空间:

________________

首先,我们定义一个网格(或者在一维空间中的点):
1    2    3    4
________________

然后,我们随机为每个网格点选择一个噪声值(该值等同于2D噪声中的梯度):
1    2    3    4
________________
-1   0    0.5  1  // random noise value

现在,计算网格点的噪声值很容易,只需选择该值:

noise(3) => 0.5

但是对于任意点p的噪声值需要基于最近的网格点p1p2的值和影响来计算:

// in 1D the influence is just the distance between the points
noise(p)   => noise(p1) * influence(p1) + noise(p2) * influence(p2)
noise(2.5) => noise(2)  * influence(2, 2.5) + noise(3) * influence(3, 2.5)
           => 0 * 0.5 + 0.5 * 0.5 => 0.25

结束了!现在我们能够计算1D噪声,只需再添加一个维度即可得到2D噪声。 :-)

希望这有助于您的理解!现在阅读@mk.'s answer以获取可工作的代码并享受愉快的噪声吧!

编辑:

评论中的后续问题:

我在维基百科文章中读到,2D Perlin中的梯度向量应该是长度为1(单位圆)和随机方向。由于向量具有X和Y,我该如何做到这一点?

这可以很容易地从原始Perlin噪声代码中提取并适应。以下是伪代码。

gradient.x = random()*2 - 1;
gradient.y = random()*2 - 1;
normalize_2d( gradient );

其中normalize_2d是:

// normalizes a 2d vector
function normalize_2d(v)
   size = square_root( v.x * v.x + v.y * v.y );
   v.x = v.x / size;
   v.y = v.y / size;

注意:我删除了一些注释,因为它们与2D柏林噪声无关(而是与1D柏林噪声有关),并且对您的后续问题没有建设性作用。 - Filipe Borges
我更新了答案,包括如何生成具有随机方向的归一化向量(大小为1)。 - Filipe Borges
@Filipee Borges谢谢你的回答。有些事情我还是不太明白,在大多数Perlin噪声库中(我在processing和unity中看到),Perlin噪声函数需要一个种子数,以便结果不同,但对于相同的种子数,结果始终相同(在我问题中的文章中,对于相同的X和Y,4个梯度也始终相同),这个种子数在这些数学步骤中是如何使用的? - Rei
简单来说,在调用随机函数之前,您必须使用种子初始化随机数生成器(例如:在Java中Random rnd = new Random(); rnd.setSeed(seed);)。对于相同的种子,rnd.next()调用返回相同的伪随机数序列。只需在Java或C#中尝试一下就可以理解了。 - Filipe Borges
@Rei 如果你想要注入一个种子,你可以将它插入到最低级别的(“哈希”)噪声函数中。或者你可以将其乘以1000并加到x坐标上。 - mk.
显示剩余3条评论

-1
计算在(x, y)坐标处的Perlin噪声。
function perlin(float x, float y) {

    // Determine grid cell coordinates
    int x0 = (x > 0.0 ? (int)x : (int)x - 1);
    int x1 = x0 + 1;
    int y0 = (y > 0.0 ? (int)y : (int)y - 1);
    int y1 = y0 + 1;

    // Determine interpolation weights
    // Could also use higher order polynomial/s-curve here
    float sx = x - (double)x0;
    float sy = y - (double)y0;

    // Interpolate between grid point gradients
    float n0, n1, ix0, ix1, value;
    n0 = dotGridGradient(x0, y0, x, y);
    n1 = dotGridGradient(x1, y0, x, y);
    ix0 = lerp(n0, n1, sx);
    n0 = dotGridGradient(x0, y1, x, y);
    n1 = dotGridGradient(x1, y1, x, y);
    ix1 = lerp(n0, n1, sx);
    value = lerp(ix0, ix1, sy);

    return value;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接