噪声函数与地图生成

作者:Aud42nce
2025-05-13
12 8 0

编者按

本文来自游戏设计博客 Red Blob Games,由 indienova 取得授权并译制发表,原文链接见文末。

  • 原文作者:Amit Patel
  • 译者:Aud42nce

前言

在学习声音信号处理期间,我不时将部分所学与此前程序化地图生成联系起来。因此,这里是一些关于信号处理概念如何与地图生成相关联的笔记。这些笔记中没有什么新知识,但有些内容对我而言是全新的,因此我将它们记录并分享出来。本文仅覆盖简单的主题(频率,振幅,噪声的颜色,噪声的使用),不多谈其它相关内容(离散与连续函数的对比,FIR/IIR 滤波器,FFT,复数)。文中涉及的数学内容主要是正弦波。

这篇文章是相关概念介绍,从最基础的内容开始,由简至繁。如果你想直接跳到使用噪声函数生成地图,可以参见我的另一篇文章《用噪声函数制作地图》

我将从使用随机数的基础知识开始,逐步解释一维地图是如何搭建的。这一过程同样适用于二维(见示例)与三维地图搭建。尝试移动滑块,看看单一参数是如何描述多种不同类型的噪声的:

0
1
2
3
4
指数: =

本文包含:

  • 如何用 15 行内的代码生成如上地图
  • 红色、粉色、白色、蓝色和紫色噪声的概念
  • 如何将噪声函数应用于程序化地图生成
  • 如何应用中点位移算法(Midpoint Displacement),Simplex/柏林噪声(Simplex/Perlin noise),以及分型布朗运动模型(fBm)

我也做了一些二维的噪声实验,包括二维高度图的三维演示。

1. “随机”为何实用?(Why is randomness useful?)

借助程序化地图生成算法,我们想要实现的是每次生成一组在某些地方相同,但在另一些地方不同的地图。比如,《我的世界》中的所有地图都有许多相似之处:群落的类型、网格的大小、群落的平均规模、高度、洞穴的平均大小、每一种方块的占比,等等。但它们也有许多不同:群落的位置、洞穴的位置与具体形状、金矿的位置,等等。作为设计者,你需要决定哪些方面是相同的,哪些方面有所区分,以及具体区别是什么。

针对那些需要区分的部分,通常会用到随机数生成器。让我们设计一个非常简单的地图生成器:它将生成 1 行共 20 个地图块(Block),其中之一埋着装有黄金宝藏的箱子。下面画出一些我们可能会看到的地图(“x”标记处为宝藏):

map 1 ........x...........
map 2 ...............x....
map 3 .x..................
map 4 ......x.............
map 5 ..............x.....

注意这些地图的共同点:它们都由块构成,这些块排列成行,每一行长度都是 20 块,块的类型有两种,每张地图有且仅有一个黄金宝藏箱。

但有一点是有区别的:宝藏块的位置。它可以被放置在位置 0(最左侧)至位置 19(最右侧)的任一位置。

我们可以使用随机数来选择该块的位置。最简单的做法是使用从 0 到 19 的均匀随机选择,这意味着从 0 到 19 的每个位置被选中的概率都相同。大多数编程语言都有一些生成均匀分布的随机数的函数,在 Python 中则是 random.randint(0,19), 本文中将其写作 random(0,19)。下面是相应的 Python 代码:

def gen():
    map = [0] * 20  # make an empty map
    pos = random.randint(0, 19)  # pick a spot
    map[pos] = 1  # put the treasure there
    return map

for i in range(5):  # make 5 different maps
    print_chart(i, gen())
0
1
2
3
4

但如果我们希望宝藏更多出现在地图左侧而不是右侧呢?为此可以使用非均匀随机选择。有许多方法能实现这一目标,其中之一是选择一个均匀随机的数字,然后将其向左移动。例如,我们可以尝试使用 random(0,19)/2。下面是对应的 Python 代码:

def gen():
    map = [0] * 20
    pos = random.randint(0, 19) // 2
    map[pos] = 1
    return map

for i in range(5):
    print_chart(i, gen())
0
1
2
3
4

然而,这并不是我想要的。我希望宝藏有时也出现在右侧,但更多时候出现在左侧。另一种将宝藏向左侧移动的方式是取数值的平方,再除以取值范围的跨度,比如 sqr(random(0,19))/19 。如果是 0,那么 0 的平方除以 20 就是 0。如果是 19,那么 19 的平方除以 19 就是 19。但是,在中间的数字,如 10,取平方后再除以 19,则为 5。我们保持了从 0 到 19 的分布范围,但将像 10 这样的中间数字移到了左侧。这种重新分配是一项非常有用的技术,我曾在以前的项目中使用过平方、平方根和其他函数。(https://easings.net/ 提供了一些用于动画的常用重塑函数; 将鼠标悬停在函数上即可查看演示。)以下是使用平方的 Python 代码:

def gen():
    map = [0] * 20
    pos = random.randint(0, 19)
    pos = int(pos * pos / 19)
    map[pos] = 1
    return map

for i in range(1, 6):
    print_chart(i, gen())
1
2
3
4
5

还有一种将目标向左侧移动的方法:先随机选择一个范围边界,再随机选择一个 0 到边界之间的数字。如果范围边界为 19,我们可以得到任何数字;如果边界为 10,则只能得到左半边的数字。以下是一些 Python 代码示例:

def gen():
    map = [0] * 20
    limit = random.randint(0, 19)
    pos = random.randint(0, limit)
    map[pos] = 1
    return map

for i in range(5):
    print_chart(i, gen())
0
1
2
3
4

还有很多技巧可以将均匀随机数转换为非均匀随机数,从而满足你的需求。作为游戏设计师,你可以选择自己想要的随机数分布。我写过一篇有关如何在角色扮演游戏中使用随机数进行伤害计算的文章,其中提供了一些这类技巧。

回顾:

  • 对于程序化地图生成,我们必须决定哪些地图特征保持相同,哪些要做出差异化;
  • 随机数对于实现差异化的部分非常有用;
  • 大多数编程语言提供的是均匀选择的随机数,但我们通常需要非均匀选择的随机数。有许多方法可以获得它们。

2. 何为噪声?(What is noise?)

噪声是一系列随机数,通常排列成一行或一个网格。

在老式电视机上,如果你调到一个没有电视频道的频率,就会在屏幕上看到随机的黑白点。那就是噪声(它们来自外太空!)。在收音机上,如果调到一个没有电台频道的频率,你将会听到噪声(我不确定它们是来自太空还是其它地方)。

在信号处理中,噪声通常是不需要的部分。相比于在安静的房间中,在嘈杂的房间里更难听清别人说话。音频噪声是排列成一行的随机数(1D)。相比于在干净的图像中,嘈杂图像中的图案更难看清。图像噪声是排列在网格中的随机数(2D)。当然也有 3D、4D 等维度的噪声。

虽然在大多数实践中,你都在试图去除噪声,但很多自然系统看起来都是嘈杂的,因此如果想要程序化地生成看似自然的东西,你通常需要添加噪声。虽然真实的系统看起来嘈杂,但通常存在一种潜在的结构;我们添加的噪声不会具有相同的结构,但相对于直接编程模拟真实噪声的结构,它们更简单易用,因此我们会用这种噪声,希望终端用户不会注意到它们和真实噪声的区别。这是一个我稍后会讨论的权衡。

让我们看一个应用噪声的简单例子。假设我们有一个像之前那样的 1D 地图,但不只有一个宝箱,我们想要创建一个包含山谷、丘陵和山峰的地貌。首先,我们可以在每个位置应用均匀随机选择。如果 random(1,3) 输出 1,我们将该位置设置为山谷,输出 2 则设置为丘陵,输出 3 设置为山峰。同时使用随机数来创建高度图:在数组中的每个位置,存储地貌的高度。下面是创建地貌的 Python 代码:

for i in range(5):
    random.seed(i)  # give the same results each run
    print_chart(i, [random.randint(1, 3) 
                    for i in range(mapsize)])

# note: I'm using Python's list comprehension syntax:
#     output = [abc for x in def] 
# is shorthand for:
#     output = []
#     for x in def:
#         output.append(abc)
0
1
2
3
4

唔,根据我们的需求来看,这些地图似乎“过于随机”了。也许我们希望有更大的山谷或丘陵区域,并且希望山峰比山谷更少见。从前面已经能看到,均匀选择随机数可能不是我们想要的,有时需要非均匀选择。后者会对此有帮助吗?我们可以使用一些让山谷比山峰出现概率更高的随机选择:

for i in range(5):
    random.seed(i)
    print_chart(i, [random.randint(1, random.randint(1, 3)) 
                    for i in range(mapsize)])
0
1
2
3
4

这虽然减少了山峰的数量,但并没有展现出任何有趣的模板。问题在于非均匀随机选择对每个位置的作用(改变)是孤立的,而我们希望某个位置的随机选择能与附近位置的随机选择相关。这就是所谓的“相干性(Coherence)”。

此时就会用到噪声函数:一次性生成一组随机数,而不是一个。在这个例子中,我们需要使用一个 1D 的噪声函数来生成一个随机数序列。让我们尝试一种相较于均匀选择序列有所修正的噪声函数,实现方式很多,而这里我们不妨取相邻两个数字的最小值。如果原始噪声是 1、5、2,则(1,5)的最小值是 1,(5,2)的最小值是 2。因此,生成的噪声将是 1、2。请注意,它删除了高点(5)。还要注意,生成的噪声比原始噪声少一个值。这意味着当我们生成 60 个随机数时,最终只会得到 59 个。让我们将这个函数应用于第一组地图:

def adjacent_min(noise):
    output = []
    for i in range(len(noise) - 1):
        output.append(min(noise[i], noise[i+1]))
    return output

for i in range(5):
    random.seed(i)
    noise = [random.randint(1, 3) for i in range(mapsize)]
    print_chart(i, adjacent_min(noise))
0
1
2
3
4

与我们之前制作的地图相比,这组地图有更大型的山谷、丘陵或山脉,而山脉经常出现在丘陵附近。由于我们修正噪声的方式(通过取最小值),山谷比山峰更常见。反之,如果我们取最大值,那么山峰就会比山谷更常见。如果我们不想让二者太频繁出现,则可以用平均值代替最小值或最大值。

现在,我们有了一个噪声修正程序,可以接收一些噪声并生成一些新的、更加平滑的噪声。

嘿,让我们再运行一次!

def adjacent_min(noise):  # same as before
    output = []
    for i in range(len(noise) - 1):
        output.append(min(noise[i], noise[i+1]))
    return output

for i in range(5):
    random.seed(i)
    noise = [random.randint(1, 3) for i in range(mapsize)]
    print_chart(i, adjacent_min(adjacent_min(noise)))
0
1
2
3
4

现在我们的地图更加平滑了,山脉数量也更少了。我认为平滑得有些太过了,因为现在很少看到山脉和丘陵相邻的情况。因此,在这个例子中,仅做一层平滑修正或许更好。

这是运用程序化生成时常见的过程:先做一些尝试,看看是否合适,如果不合适,就改回来或做其它尝试。

附注:在信号处理中,图像平滑的过程也被称作低通滤波 [1] 。有时它会用于消除不需要的噪声。

回顾:

  • 噪声是一组随机数,通常排列成行或者网格;
  • 在程序化生成中,我们经常需要加入噪声以制造差异;
  • 简单地选择随机数(无论是均匀还是非均匀)会导致噪声序列中的每个数字与其周围的数字不相关;
  • 我们经常需要带有特征的噪声,例如丘陵附近有山脉;
  • 有很多方法可以生成噪声;
  • 有些噪声函数直接产出噪声,另一些则采用现有的噪声并加以修正。

有时,需要通过猜测选择噪声函数。理解噪声的工作原理以及如何修正它,意味着你可以做出更有根据的猜测。

3. 生成噪声(Making noise)

在上一节中,我们通过使用随机数作为输出,然后进行平滑操作来选择噪声。这是一种常见的模式。最开始,我们使用将随机数作为参数的噪声函数:用一个随机数选择黄金宝箱所在的位置,然后使用另一个随机数来选择山谷/丘陵/山脉。然后,可以修正现有的噪声使其符合需求:我们通过对山谷/丘陵/山脉的噪声函数进行平滑操作来修正它。还有很多其它修正噪声函数的方法。

以下是一些基本的 1D/2D 噪声生成器:

  1. 直接使用随机数作为输出。这是我们在生成山谷/丘陵/山脉噪声时所用的方法;
  2. 将随机数作为正弦和余弦函数的参数,输出并生成噪声;
  3. 将随机数作为梯度函数的参数,输出并生成噪声。这种方法常用于 Simplex/柏林噪声。

以下是一些常用的噪声修正方法:

  1. 应用滤波器以减弱或放大某些特征。对于山谷/丘陵/山脉,我们通过平滑操作减少其崎岖感,增加山谷的大小,并使山脉出现在山谷附近;
  2. 将多个噪声函数加在一起。通常使用加权和,这样我们就能控制每个噪声函数对总和的贡献;
  3. 在噪声函数提供给我们的噪声值之间进行插值,以生成平滑的区域。

有这么多生成噪声的方式!

在某种程度上,噪声的生成方式并不重要。尽管这很有趣,但将其应用于游戏时,应着重关注以下两点:

  1. 你将如何使用噪声?
  2. 每次使用时,你希望噪声函数具有哪些特性?

4. 噪声的应用方式(Ways to use noise)

最直接的应用方式是直接用噪声函数生成高度。在前面的示例中,我通过在地图上的每个位置调用 random(1,3) 来生成山谷/丘陵/山脉。噪声值在这里直接用作海拔。

使用中点位移噪声或者 Simplex/柏林噪声作为高度也属于直接应用。

另一种应用噪音的方式是将其作为相对前一个数值的偏移距离。比如,如果噪声函数返回 [2, -1, 5] ,就可以认为,第一个位置为 2,第二个位置为 2 +(-1)=1,第三个位置为 1 + 5 = 6。相关内容也可参见“随机游走 [2] ”。你还可以反过来,使用噪声数值的差值。你也可以将其视为噪声函数的变种用法。

除了作为高度,噪声也可以用于音频。

或者你也可以用它来绘制形状。例如,你可以在极坐标图中用噪声作为半径。通过将输出用作半径(而不是高度),你可以像这样 [3] 将一个 1D 噪声函数转换为极坐标形式。此处 [4] 是同一个函数在极坐标形式下的样子。

你也可能正在将噪声用作图形纹理。Simplex/柏林噪声经常应用于此。

你还可以用噪声来选择对象的位置,比如树木、金矿、火山或地震断裂带。在此前的例子中,我使用了一个随机数来选择宝箱的位置。

同样,你可以将噪声用作阈值函数。例如你可以说,一旦输出值大于 3,就会发生一种情形,否则发生另一种情形。一个例子是使用 3D Simplex/柏林噪声生成洞穴:你可以说,任何高于某个密度阈值的地方是实心土壤,而低于该阈值的地方则是开放空间(洞穴)。

在我的多边形地图生成器中,我用多种不同方式应用了噪声,但没有一种是直接用作高度的:

  1. 最简单的图形结构是正方形网格或六边形网格(事实上我是从六边形网格开始的)。网格中的每个瓦片都是一个多边形。我想要为网格添加一些随机性,这可以通过随机移动顶点来实现,但我想要更随机一些。我使用蓝噪声生成器来生成多边形位置,并使用维诺图来重构它们。这会带来更多工作量,但幸运的是我有一个库(as3delaunay),可以完成所有工作。刚才提过,我是从网格开始的,这要简单得多,我建议从此开始尝试。

  2. 海岸线是陆地与水的分界。我可以用两种不同的噪声应用方式生成它,而你也可以让设计师直接绘制形状,我则用方形以及本博客网站 Logo 的形状做了演示。使用正弦和余弦函数生成噪声,再以极坐标形式绘制,便可得到径向形海岸线。Simplex/柏林形海岸线则是一种使用噪声并将径向衰减作为阈值的噪声生成器。这里可以使用任意数量的噪声函数。

  3. 河流源头是随机放置的。

  4. 多边形之间的边界从直线变为嘈杂的线条。它类似于中点替换,但我将其适配到多边形范围内。这是纯图形效果,代码位于 GUI(mapgen.as)而不是核心算法(Map.as)中。

目前大多数教程都直接使用噪声值,但噪声有很多不同的应用方式。

5. 噪声的频率(Frequency of noise)

频率是我们要关注的主要属性。借助正弦波来理解是最方便的。下面是一个低频正弦波,然后是一个中频正弦波,最后是一个高频正弦波:

print_chart(0, [math.sin(i*0.293) for i in range(mapsize)])
0
print_chart(0, [math.sin(i*0.511) for i in range(mapsize)])
0
print_chart(0, [math.sin(i*1.57) for i in range(mapsize)])
0

可以看到,较低的频率会形成更宽的丘陵,较高的频率会形成更窄的丘陵。频率描述了特征的横向尺寸;振幅描述了纵向尺寸。记得之前我说山谷/丘陵/山脉的地图看起来“太随机”了,想要更大的山谷或山脉区域吗?实际上,我是在要求更小的变化频率

如果你有一个连续函数(比如正弦函数),用它来产生噪声,那么增加频率就意味着给输入乘以某个因子:sin(2*x) 的频率是 sin(x) 的两倍。增加振幅就意味着将输出乘以某个因子:2*sin(x) 的振幅是 sin(x) 的两倍。在上面的代码中,你可以看到我通过给输入乘以不同的数字来改变频率。随后,我们将通过叠加正弦波来改变振幅。

试试改变频率:

频率:

试试改变振幅:

振幅:

上面讲述的所有内容都是关于 1D 的,但在 2D 情况下也一样。请看该页 [5] 的 Figure 1,你将看到高波长(低频率)和低波长(高频率)的 2D 噪声示例。请注意,频率越高,特征越小。

当噪声函数涉及频率、波长或频程时,即使使用的不是正弦波,这些规律也是相同的。

说到正弦波,你可以通过奇怪的组合方式做出有趣的事情;在这个例子中,左边有一些低频率波,右边有一些高频率波:

print_chart(0, [math.sin(0.2 + (i * 0.08) * math.cos(0.4 + i*0.3))
                for i in range(mapsize)])
0

通常情况下,你会同时获得多个频率的波,但没有一个标准答案可以告诉你应该使用哪些。这时要问自己:需要用到哪些频率?当然,这取决于你使用它们的方式。

6. 噪声的颜色(Colors of noise)

噪声“颜色”描述了它包含哪些频率。

白噪声 [6] 中,所有频率的权重相当。之前我们用白噪声举过例子,当时选择了 1、2 和 3 分别代表山谷、丘陵或山脉。以下是 8 个白噪声序列:

for i in range(8):
    random.seed(i)
    print_chart(i, [random.uniform(-1, +1) 
                    for i in range(mapsize)])
0
1
2
3
4
5
6
7

红噪声 [7](也称为布朗噪声)中,低频率更为突出(具有更高的振幅)。这意味着在输出结果中会出现更长的丘陵和山谷。我们可以通过对相邻的白噪声数值取平均来生成偏红噪声。以下是与此前相同的 8 个白噪声样本,经过平均处理后的结果:

def smoother(noise):
    output = []
    for i in range(len(noise) - 1):
        output.append(0.5 * (noise[i] + noise[i+1]))
    return output

for i in range(8):
    random.seed(i)
    noise = [random.uniform(-1, +1) for i in range(mapsize)]
    print_chart(i, smoother(noise))
0
1
2
3
4
5
6
7

如果仔细观察这 8 个样本中的任意一个,你会发现它比对应的白噪声更加平滑。其中存在更长的高值或低值结果。

粉噪声 [8] 介于白噪声和红噪声之间。它在自然界中大量存在,且其特征满足我们对于场景的要求:大的丘陵和山谷,同时地形上还带有一些小的颠簸。

在频谱另一端有蓝噪声,其高频部分更加突出。我们可以通过对相邻的白噪声样本求差值,来生成具有“蓝色”特征的噪声。以下是之前提到的 8 个白噪声样本,区别在于经过了差分处理:

def rougher(noise):
    output = []
    for i in range(len(noise) - 1):
        output.append(0.5 * (noise[i] - noise[i+1]))
    return output

for i in range(8):
    random.seed(i)
    noise = [random.uniform(-1, +1) for i in range(mapsize)]
    print_chart(i, rougher(noise))
0
1
2
3
4
5
6
7

如果仔细观察这 8 个样本中的任何一个,你会发现它比相应的白噪声更加粗糙。持续较高或较低的结果更多,且短时的变化更多。

处理物体分布时,蓝噪声通常能满足我们的需求:没有过于密集或过于稀疏的区域,而是让物体在整个地图大致均匀地分布。人眼睛中视杆细胞和视锥细胞的分布就具有蓝噪声的特征。蓝噪声也能用于创建酷炫的城市场景。

维基百科提供了一个页面,能够试听不同颜色的噪声 [9]大脑会将红噪声视作“自然的” [10] ,这也是我们将其用于地形构建的原因。

我们已经了解了如何生成白噪声、偏红噪声和偏蓝噪声。稍后,我们将更精细地观察更多种类(颜色)的噪声。

回顾:

  • 频率是正弦波等重复信号的属性,我们也可以用这种方式来观察噪声;
  • 白噪声是最简单的噪声。它包含所有频率,是均匀选择的随机数;
  • 红、粉、蓝和紫是其它的噪声颜色,对于程序化生成非常有用;
  • 你可以通过对白噪声取均值(+)将其转换为偏红噪声;
  • 你可以通过差分运算(-)将白噪声转换为偏蓝噪声。

7. 混合频率(Combining frequencies)

在之前的部分中,我们探讨了噪声的“频率”,以及存在各种不同的噪声“颜色”。白噪声意味着包含所有频率;粉噪声和红噪声具有较强的低频成分;蓝噪声和紫噪声则具有较强的高频成分。

一种生成具有所需频率特征的噪声的方法是,找到某种方式在特定频率上生成噪声,然后将它们混合在一起。例如,假设我们有一个噪声函数 noise,可以在特定频率 freq 上生成噪声。那么,如果你希望 1000 Hz 频率的噪声的强度是 2000 Hz 频率噪声的两倍,并且没有其他特定的频率,我们可以使用 noise(1000) + 0.5 * noise(2000)

我承认,正弦波看起来并不特别嘈杂,而我们可以很容易地为它指定一个频率,所以让我们从正弦波开始,看看能够通过它获得什么样的结果。

def noise(freq):
    phase = random.uniform(0, 2*math.pi)
    return [math.sin(2*math.pi * freq*x/mapsize + phase)
            for x in range(mapsize)]

for i in range(3):
    random.seed(i)
    print_chart(i, noise(1))
0
1
2

就是这样。我们的基本构建单元是一个通过随机量(称为相位)在水平方向上移动的正弦波。唯一的随机性就是平移距离。

让我们将一些噪声函数混合在一起。我会将把 8 个噪声函数相加,它们的频率分别为 1、2、4、8、16、32(在某些噪声函数中,2 的幂被称为频程)。我会将每个噪声函数乘以一些因子(参见 amplitudes 数组),然后将它们相加。我需要一种计算加权和的方法:

def weighted_sum(amplitudes, noises):
    output = [0.0] * mapsize  # make an array of length mapsize
    for k in range(len(noises)):
        for x in range(mapsize):
            output[x] += amplitudes[k] * noises[k][x]
    return output

现在我可以使用之前的 noise 函数以及新的 weighted_sum 函数了:

amplitudes = [0.2, 0.5, 1.0, 0.7, 0.5, 0.4]
frequencies = [1, 2, 4, 8, 16, 32]

for i in range(10):
    random.seed(i)
    noises = [noise(f) for f in frequencies]
    sum_of_noises = weighted_sum(amplitudes, noises)
    print_chart(i, sum_of_noises)
0
1
2
3
4
5
6
7
8
9

尽管我们最初使用的是看起来一点也不嘈杂的正弦波,但它们的混合看起来相当嘈杂,不是吗?

如果我使用 [1.0, 0.7, 0.5, 0.3, 0.2, 0.1] 作为权重会怎么样呢?这用到了更多低频噪声,完全不包含高频噪声。

0
1
2
3
4
5
6
7
8
9

如果我使用 [0.1, 0.1, 0.2, 0.3, 0.5, 1.0] 作为权重又会怎么样呢?这会使得低频占比很低,而高频的权重大得多。

0
1
2
3
4
5
6
7
8
9

我们所做的只是对不同频率的噪声函数加权求和,所有这些都在不到 15 行代码内完成,却能够生成多种不同风格的噪声。

回顾:

  • 我们选择一个而不是一组随机数,并使用它让正弦波左右平移;
  • 我们可以对不同频率的噪声函数加权求和,生成新的(所需的)噪声;
  • 我们可以通过调整加权求和时的权重来选择生成噪声的特征。

8. 生成彩虹(Generating the rainbow)

现在我们可以通过混合不同频率的噪声来生成噪声,那么先来回顾一下噪声的颜色。

再次查看关于噪声颜色的维基页面。注意页面中展示了频谱,这告诉了你噪声中各频率的振幅。白噪声是平坦的;粉色和红色是向下倾斜的;蓝色和紫色是向上倾斜的。

[注: <2018-10-15 周一>与其说是振幅,噪声应该对应于功率,即振幅的平方。我需要回顾一下这个页面并更新术语。让我困惑的可能是,Simplex/柏林噪声通常会将振幅减半,同时忽略大多数频率 - 这是否相当于更快地减小振幅,但包含所有频率呢?需要进一步研究。]

频谱对应我们在前序部分中用到的 frequenciesamplitudes 数组。

此前我们曾用 2 的幂次作为频率。各种类型的有色噪声具有比这更多的频率,因此我们需要一个更长的数组。针对这段代码,我将使用从 1 到 30 的所有整数频率,而不仅仅是 2 的幂次(1、2、4、8、16、32)。这里我不再手动设置振幅,而是编写一个函数 amplitude(f),它会返回任何给定频率的振幅,由此构建 amplitudes 数组。

我们可以复用之前的 weighted_sumnoise 函数,但这次将使用一个更长的数组来代替此前的一小组频率:

frequencies = range(1, 31)  # [1, 2, ..., 30]

def random_ift(rows, amplitude):
    for i in range(rows):
        random.seed(i)
        amplitudes = [amplitude(f) for f in frequencies]
        noises = [noise(f) for f in frequencies]
        sum_of_noises = weighted_sum(amplitudes, noises)
        print_chart(i, sum_of_noises)

random_ift(10, lambda f: 1)
0
1
2
3
4
5
6
7
8
9

在上面的代码中,amplitude 函数设置了噪声的形状。通过设置始终返回 1,它生成了白噪声。那么该如何生成其它颜色的噪声?为此,我将使用相同的随机数种子,但使用不同的振幅函数:

8.1 红噪声(red noise)
random_ift(5, lambda f: 1/f)
0
1
2
3
4
8.2 粉噪声(pink noise)
random_ift(5, lambda f: 1/math.sqrt(f))
0
1
2
3
4
8.3 白噪声(white noise)
random_ift(5, lambda f: 1)
0
1
2
3
4
8.4 蓝噪声(blue noise)
random_ift(5, lambda f: math.sqrt(f))
0
1
2
3
4
8.5 紫噪声(violet noise)
random_ift(5, lambda f: f)
0
1
2
3
4
8.6 噪声的颜色(colors of noise)

这确实非常简洁。通过改变振幅函数的指数,你可以获得一些简单的形状。

  • 红噪声为 f^-1
  • 粉噪声为 f^-½
  • 白噪声为 f^0
  • 蓝噪声为 f^+½
  • 紫噪声为 f^+1

[2018-10-13 周六] 正如 Eric S. 向我指出的那样,指数的实际意义取决于你的需求。如果你正在处理所有频率,指数 -2、-1、0、+1、+2 作用于功率,而功率是振幅的平方。如果你只使用频程(例如 Simplex/柏林噪声),指数 -2、-1、0、+1、+2 则作用于振幅。我对此并不完全确定。

尝试改变指数以查看第 8.1-8.5 节中的噪声是如何由相同的基础函数生成的:

0
1
2
3
4
指数: =

回顾:

  • 我们可以通过对不同频率的正弦波加权求和来生成噪声;
  • 各种颜色的噪声具有遵循函数 f^c(其中 c 是指数)的权重(功率谱,即振幅的平方);
  • 通过改变指数,你可以由相同的随机数集合生成不同颜色的噪声。

9. 更多形状的噪声(more shapes of noise)

为了生成各种颜色的噪声,我们设置振幅遵循一个简单的指数函数,并改变其指数。但你并不需要局限于这些形状。这是最简单的模式,在双对数坐标(log/log 图)上显示为直线。或许有其他一些频率集可以产生我们需要的有趣模式。你可以使用振幅数组,并根据需要自由设置它们,以代替简单的函数。这是一个值得探索的方向,但我还没有深入研究过。

一种思考我们此前工作的角度是引入傅里叶级数 [11] 。主要思路是,任何连续函数都可以表示为正弦和余弦波的加权和。你如何分配(这些波的)权重决定了最终函数的样貌。傅里叶变换将原始函数与其频率联系起来。通常,你从原始数据开始,并要求其告诉你频率/振幅。你在音乐播放器中看到的噪音的"波谱"就是一个例子。

正向傅里叶变换允许你将数据分析为频率;反向变换则允许从频率合成数据。对于噪声,我们主要关注的是合成,事实上,已经在这么做了。在前面的部分中,我们选用了一些频率和振幅,并生成了噪声。

傅里叶变换包含大量炫酷的理念,数学知识与应用。

这个页面 [12] 上有关于傅里叶变换原理的解释。该页面上的图表是可交互的 —— 你可以输入每个频率的强度,图表将展示它们如何组合。通过组合正弦波,你可以构成许多有趣的形状。例如,尝试在 Cycles 输入框中填写 0、-1、0.5、-0.3、0.25、-0.2、0.16、-0.14,然后取消勾选 Parts 复选框。看看它是不是像一座山脉?该页面的附录还提供了一个版本,可以显示正弦波在极坐标中的样子。

作为傅里叶变换在地图生成中的应用之一,可以参考 Paul Bourke 的频率合成 [13] 地图生成方法,该方法首先生成二维白噪声,然后通过傅里叶变换将其转换为频率表示,接着将其形状处理为粉噪声,最后使用逆傅里叶变换转换回来。
迄今为止,我对二维情形的有限实验表明,它不像一维情形那样直接。如果你想看看我的不完整实验,请浏览此页面 [14] 底部的交互图像,并移动滑块看看效果。

10. 其它噪声函数(Other noise functions)

在这篇文章中,我们使用正弦波生成噪声。这非常简单,只是将几个函数叠加在一起。尽管原始函数看起来并不嘈杂,但结果看起来相当不错,尤其是代码都不超过 15 行。

我并没有深入研究其它噪声函数,但在自己的项目中有使用到。我认为,其中许多函数生成的是粉噪声:

  • 我相信中点位移算法使用的是锯齿波而不是正弦波,然后逐渐加入频率越来越高(振幅越来越低)的波。我认为这是红噪声。最终你会得到可见的边缘,这可能是由于锯齿波(不如正弦波平滑)或者是因为仅使用了二的幂次作为频率而非所有频率所致。我不确定;

  • Diamond-Square 算法是中点位移算法的一种变体,旨在一定程度上隐藏中点位移算法产生的边缘;

  • 一个单独的 Simplex/柏林噪声频程会生成特定频率的平滑噪声。通常,你会将多个 Simplex 噪声频程相加,以生成红噪声;

  • 我相信分形布朗运动也是通过将多个噪声函数相加来生成红噪声的;

  • Voss-McCartney 算法 [15] 似乎类似于中点位移算法,它将多个不同频率的白噪声函数相加;

  • 你可以通过由所需频率进行逆傅里叶变换计算来直接生成粉噪声,这就是前一节中的示例所做的。Paul Bourke 将其描述为频率合成,并展示了用 2D 噪声生成 3D 高度图的效果。

一些关于 Voss-McCartney 算法的内容让我意识到,将不同频率的噪声相加并不能精准生成红噪声,但对于地图生成而言,或许已足够接近了。中点位移算法中出现的接缝可能是由此引起的,或者可能是由插值函数引起的。我不确定。

相比之下,我并没有找到太多生成蓝噪声的方法。

  • 在我的多边形地图生成器项目中,我使用了 Lloyd Relaxation 算法和 Voronoi 图生成所需的蓝噪声。我已经有了一个可以提供 Voronoi 图的库,复用它比实现一个单独的步骤更容易;

  • Poisson Disks(PDF) [16] 是另一种生成蓝噪声的方法。如果项目中没有现成的 Voronoi 库,Poisson Disk 可能会更简单易用。此外,还可以参考这篇文章 [17] ,它介绍了 Poisson Disks 在游戏中的一些应用;

  • 王浩砖也可以生成蓝噪声。虽然我还没有详细研究过这个方法,但希望有一天能够深入了解;

  • 根据蓝噪声频谱进行逆傅里叶变换计算也能够直接得到蓝噪声。我还没有尝试过这个方法;

  • 获得蓝噪声最简单的方式是下载一个 [18]

我甚至不确定自己的地图是否真的需要典型的蓝噪声,但这在文献中被描述为蓝噪声。使用正弦波生成的蓝噪声看起来与我理解的蓝噪声有些相似,但上述技术似乎更符合我在游戏中的需求。

11.更多……(more)

这个页面最初是我的个人笔记,保存在 Emacs 的 org-mode 中。将其发布至网络时,我改进了图表并添加了一些交互式内容。我在这篇文章上花费的时间远不及其它大多数文章,但我正在尝试用更简明的方式来覆盖一些主题,希望这样能让我分享更多的内容。

我还做了一些 2D 的实验,它们没有这个页面上的内容那么精细,其中包括一个 2D 高度图的 3D 可视化演示

其它我还没有仔细研究的内容:


[1] https://en.wikipedia.org/wiki/Low-pass_filter
[2] https://en.wikipedia.org/wiki/Random_walk
[3] 
https://www.wolframalpha.com/input?i=plot+y%3Dmax%280.4+%2B+0.3+cos%281.313+%2B+7+x+%2B+0.5+*+sin%2819+x%29%29%2C+0.7+-+0.3+sin%282.473+%2B+3+x+-+sin%287+x%29%29%29
[4] 
https://www.wolframalpha.com/input?i=polar+plot+r%3Dmax%280.4+%2B+0.3+cos%281.313+%2B+7+theta+%2B+0.5+*+sin%2819+theta%29%29%2C+0.7+-+0.3+sin%282.473+%2B+3+theta+-+sin%287+theta%29%29%29
[5] http://devmag.org.za/2009/04/25/perlin-noise/
[6] https://en.wikipedia.org/wiki/White_noise
[7] https://en.wikipedia.org/wiki/Brownian_noise
[8] https://en.wikipedia.org/wiki/Pink_noise
[9] https://en.wikipedia.org/wiki/Colors_of_noise
[10] http://steveharoz.com/research/natural/
[11] http://www.thefouriertransform.com/#introduction
[12] https://betterexplained.com/articles/an-interactive-guide-to-the-fourier-transform/
[13] http://paulbourke.net/fractals/noise/
[14] https://www.redblobgames.com/articles/noise/2d/
[15] https://www.firstpr.com.au/dsp/pink-noise/
[16] https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.78.3366&rep=rep1&type=pdf
[17] http://devmag.org.za/2009/05/03/poisson-disk-sampling/
[18] http://momentsingraphics.de/BlueNoise.html
[19] https://libnoise.sourceforge.net/noisegen/
[20] http://eastfarthing.com/blog/2015-04-21-noise/
[21] https://eev.ee/blog/2016/05/29/perlin-noise/
[22] https://www.cs.unm.edu/~brayer/vision/fourier.html
[23] https://www.jezzamon.com/fourier/
[24] https://old.reddit.com/r/proceduralgeneration/comments/31v5bg/any_guidesresources_on_using_noise_to_create/
[25] https://math.stackexchange.com/questions/1002/fourier-transform-for-dummies
[26] https://weber.itn.liu.se/~stegu/TNM022-2005/perlinnoiselinks/
[27] https://jeremykun.com/2012/07/18/the-fast-fourier-transform/
[28] http://jakevdp.github.io/blog/2013/08/28/understanding-the-fft/
[29] http://freespace.virgin.net/hugo.elias/models/m_perlin.htm
[30] https://sites.me.ucsb.edu/moehlis/APC591/tutorials/tutorial7/node2.html
[31] https://en.wikipedia.org/wiki/Brownian_bridge
[32] https://ccrma.stanford.edu/~jos/log/
[33] https://www.gamedeveloper.com/legacy/view/feature/131507/a_realtime_procedural_universe_.php?print%3D1
[34] http://www.stuffwithstuff.com/robot-frog/3d/hills/
[35] https://developer.nvidia.com/gpugems/gpugems3/part-i-geometry/chapter-1-generating-complex-procedural-terrains-using-gpu
[36] https://web.archive.org/web/20120209042104/https://notch.tumblr.com/post/3746989361/terrain-generation-part-1
[37] http://libnoise.sourceforge.net/examples/worms/
[38] http://accidentalnoise.sourceforge.net/minecraftworlds.html
[39] https://www.gamedev.net/blog/33/entry-2138456-seamless-noise/
[40] https://www.cs.umd.edu/~zwicker/publications/AnisotropicNoise-SIG08.pdf


原文链接:https://www.redblobgames.com/articles/noise/introduction.html
*本文内容系作者独立观点,不代表 indienova 立场。未经授权允许,请勿转载。

近期点赞的会员

 分享这篇文章

您可能还会对这些文章感兴趣

参与此文章的讨论

暂无关于此文章的评论。

您需要登录或者注册后才能发表评论

登录/注册