【异星工厂】星期五报道 #390 - 噪声表达 2.0


3楼猫 发布时间:2023-12-30 11:26:06 作者:Wube Software Language

   你好,我们收到了很多关于地图生成方式的询问和建议。但如果不先解释噪声这个概念,就很难阐述地图是如何生成的。因此我们需要讨论噪声,因为它们是游戏的关键部分,但我认为我们之前没有很好地解释它们实际上是什么,首先,不要从字面上理解这个概念。我们将来会再次探讨星球生成,但现在我们将介绍它的基本概念,以此作为稍后的入门知识。


什么是噪声表达?

“表达” 部分

在制作异星工厂时,我们需要决定将哪些内容和物品放置在何处,需要解决的问题只是 X 和 Y 位置。由于地形生成器无法获知任何已放置的内容,因此需要一些代码将 X 和 Y 转换为要放置的图块类型,以及需放置的树木、岩石、资源、装饰品、悬崖或敌人。

【异星工厂】星期五报道 #390 - 噪声表达 2.0-第0张

地图中间 X 和 Y 都为 0 的地方是原点和起始位置。我们希望这里是陆地,否则你就会被困在水中。我们可以计算距原点的距离,得到一个距离“锥体”,并用它来制作一个圆形岛屿,其中高于某个值的所有东西都是陆地,否则都是水。

【异星工厂】星期五报道 #390 - 噪声表达 2.0-第1张

(以上图片未按比例绘制。)

我们并不需要同时更改两个图块,只需要确保我们想要的图块出现在我们想要它出现的地方。例如,土地的“权重”始终可以为 1,而当我们希望水出现时,水的权重可以高于 1。

如果我们可以在调用距离函数之前将值添加到 X 和 Y 坐标,那么它会将圆锥体移动到不同的位置。偏置圆锥体可用于制作新的岛屿、为现有岛屿添加一部分面积,或反转新的圆锥体以从岛屿中取出一大块面积。

【异星工厂】星期五报道 #390 - 噪声表达 2.0-第2张

我们还使用了很多算术运算符,例如绝对值、模、指数和三角函数。三角函数可用于旋转位置,而不仅仅是偏移,这是设计乌尔肯星球的主要工具。

【异星工厂】星期五报道 #390 - 噪声表达 2.0-第3张

我们可以将这些操作结合起来,例如:test_1 = A + B * C。一个噪声表达式引用另一个噪声表达式,例如:test_doubler = test_1 * 2。如果你愿意,你可以用它制作一个有趣的麦田圈图案,但这对于制作自然景观来说并不是最佳方式。为此我们需要一些噪声。

【异星工厂】星期五报道 #390 - 噪声表达 2.0-第4张

“噪声” 部分

在地形生成中,噪声并不意味着声音,它意味着随机数。

在生成随机数时,最基本的设置是每次需要时生成完全随机的数字,这称为不相干噪声,与其他点没有任何关系。如果缩放,这个效果将更随机,用处非常有限。

【异星工厂】星期五报道 #390 - 噪声表达 2.0-第5张

相干噪声则不同。它充分利用了 X 和 Y 坐标,使附近的位置具有相似的值。这意味着当你在景观上移动时,事物会平稳且连贯地变化。

我们使用的主要相干噪声是来自 FFF-112 (https://factorio.com/blog/post/fff-112) 的基础噪声(一种Perlin风格噪声)。输出值最终得到近似的特征尺寸,如果缩小,它与不连贯的噪声无法区分,但如果放大(使特征更大),那么一切都会变得平滑,直到几乎平坦。

【异星工厂】星期五报道 #390 - 噪声表达 2.0-第6张

我们可以使用一些大尺寸的噪声来表示大陆,使用中等尺寸的噪声来表示岛屿和海湾,以及使用较小的噪声来进一步分解海岸线,这是分形噪声背后的基本逻辑。不同大小的多个级别添加在一起,较小的层的影响逐渐减小,因为它们向某些区域添加了较小的细节。

【异星工厂】星期五报道 #390 - 噪声表达 2.0-第7张

下一种噪声类型是点噪声。它在景观上创建了许多具有一定间距的点,这就是我们通常用于资源放置的方式。每个点实际上都是一个圆锥体,因此我们可以在每块地的中间拥有更高的丰富度。然后通过添加一些基础噪声来扰动资源锥,因此它不是一个完美的圆圈。如果您想了解有关点噪声的更多信息,我建议您查看 FFF-258 (https://factorio.com/blog/post/fff-258)。

【异星工厂】星期五报道 #390 - 噪声表达 2.0-第8张

左:在添加噪声以破坏圆圈之前的瑙维斯资源锥。右:瑙维斯地图,如您通常看到的那样。

把它们放在一起

对这些噪声的控制,并找到有效使用这些噪声所需的算法很重要。比如说: 点噪声不仅可以用于资源,还可以用于火山。在海拔上添加了巨大的点锥体来制作主火山体,还可以倒转圆锥体并“缩小”尖端,将山峰倒转为熔岩坑。

【异星工厂】星期五报道 #390 - 噪声表达 2.0-第9张

倒置至关重要,如果你想要一张主要是水的地图,你可以降低海拔,但这往往会形成岛屿。如果你想要一张主要是水域的地图,但仍将大部分陆地连接起来怎么办?

为此,我们可以使用绝对值将任何负值反弹为正值。如果我们然后将其反转,所有值都是负值,少量添加值会导致一条窄带进入正值区域。这使得一系列狭窄的陆地路径几乎总是相连的,我们在乌尔肯星球熔岩区域使用这种模式来创造一条穿过迷宫的路。

【异星工厂】星期五报道 #390 - 噪声表达 2.0-第10张

噪声工具

在加入 Wube 公司之前,我在为我的太空探索模组开发新行星(现在仍然如此)。在太空探索游戏中,每种行星类型都会有独特的地图生成模式,有点像太空时代中的行星。制作 1 个全新的行星是一项巨大的工作量,因此当开始制作 14 个以上的新类型时,我决定制作一些新工具以使过程更容易。我创造了属于我自己的一套噪声工具,它可以做很多事情:

⚙ 自动清洁功能,测试的速度更快。 

⚙ 行星切换预设功能,能够方便预览其他行星。 

⚙ 添加临时调试滑块的便捷内联方式,以便可以从地图预览屏幕调整噪声的数值。 

⚙ 最后但并非最不重要的一点是,能够使用地图预览直接可视化噪声输出。

噪声可视化的最后一项很重要。没有它,系统将无法进行数据的输出。举例来说,在添加了一些新代码后,这些代码应该使地图上的一些沙子从黄色变为红色。但你没有看到任何红沙,什么地方出了错?可能红色的区域恰好在水和草下,而不是沙子下。但更有可能的是,如果你探索了一大片区域但仍然没有找到任何东西,那么它在某个地方被破坏了,但是在哪里呢?

噪声可视化工具允许我们选择特定的噪声并将其转换为预览屏幕中的图像。这样我们就可以看到噪声的分布和输出值之类的东西,比如,如果范围很大,你需要走 10 公里才能发现任何差异。或者输出范围太低,但不会大到足以做出改变,或者一个值可能意外变为负值,与另一个负值相乘并导致其他一些意想不到的问题。

【异星工厂】星期五报道 #390 - 噪声表达 2.0-第11张

左:瑙维斯地图,如您通常看到的那样。右:瑙维斯的海拔。蓝色表示海拔低于零,深色值表示深度。黄色表示海拔很高,绿色表示非常高,用于区分水和悬崖。

当处理诸如多个生物群系时,如果仅依靠图块变化作为指标,可以看到从一个生物群落到另一个生物群落时发生了变化,但无法判断变化的速度。通常,柔和的变化率会更好,因此随着天气变得干燥,植被会逐渐消失,但通常无法提前看到这一点。使用可视化工具后,可以判断生物群系过渡是硬还是软,它可以显示多达 255 个不同的值并显示渐变。

【异星工厂】星期五报道 #390 - 噪声表达 2.0-第12张

左:乌尔肯星球的海拔。蓝色表示海拔低于零,深色值更深。黄色高,绿色非常高。右:乌尔肯上的温度。黑色表示寒冷,红色表示温暖,黄色表示热。用于放置热图块集。

它的工作方式仍然有点粗糙,因为它作为模组运行,而不是游戏引擎的一部分。本质上,它获取游戏中的所有图块,将其地图颜色更改为从黑色到白色的不同值,或蓝色>红色>绿色,然后它重新分配这些图块,使其仅根据可视化的噪声表达式出现在某个位置。使用蓝色表示低于零,其他色调表示高于零,这使得零线过渡非常清晰,这很重要,因为那往往是我们的海岸线。它在新星球的早期阶段或调试阶段的作用很大。

所以很自然地,当我开始在 Wube 公司制作行星时,我更新了我的工具,这样我就可以将它们用于太空时代。我非常有信心地说,使用这些工具,让我在行星上的工作速度可以提高大约 10 倍。

不仅如此,根尼斯和我一直在研究一些奇特的新噪声函数,稍后我们将与新行星一起揭示这些函数,新功能有很多设置选项。事实上,我怀疑如果不通过某种方式了解我们正在做的事情,我们是否能够完成新系统的所有功能。当扩展包发布时,我将发布 Noise Tools 噪声工具 2.0。

C++ 中的地图生成

当第一次合并乌尔肯地图时,我们注意到启动游戏和生成行星地形时速度显着减慢。虽然这对于玩家来说只是一个轻微的不便,但是每天多次启动会让这个问题变得非常明显。因此,我们要么必须放弃花哨的地图生成,要么让它更快。

地图生成可以在多个线程中运行,我们尝试优化 SIMD 执行的代码(单指令、多数据),但地图的运行已经足够高效,这与我们对乌尔肯星球的观察结果相符。问题一定出在其他地方。当我越来越深入地研究噪声表达时,我发现了一些需要改进的地方。

在 C++ 中,噪声表达式的具体表现为数学运算的抽象语法树 (AST)。每个噪声表达式都是一个存储其子项的类实例。如果您不知道这意味着什么,只需将其想象为一个可以容纳其他袋子的袋子即可。它们可以组合和嵌套,直至达到硬件限制。该结构是根据 Lua 语言对应结构而构建的。创建画面时,噪声表达式将编译为噪声程序。一般来说,每个NamedNoiseExpression都是程序中的一个过程。过程很有用,因为我们可以在多个步骤中重复使用数据。该过程包含具有已解析的噪声操作的线性序列,因此可以保证后续操作的子项已被计算。当您需要所有数据时,此结构针对快速计算进行了优化,因此无法轻松完成诸如 if 语句之类的更改。此外,在编译噪声表达式之前,它们会被递归地简化 —— 如果它们的所有子项都是常数,那它们也可以折叠成常数。此步骤目前不能尽快完成,因为某些变量取决于地图的设置。

【异星工厂】星期五报道 #390 - 噪声表达 2.0-第13张

1.1 中的噪声表达式生命周期

优化内部结构

系统预计不会被发挥到极限,因此代码不会删除重复的相同表达式并单独分配它们。基础游戏分配了 31'000 个对象,乌尔肯分配的数量更多,达到 280'000 个。我添加了一个全局存储,使用哈希值进行缓存,并且可以在不先构造完整对象的情况下检索表达式。它将对象数量减少到 5,300 个,并节省了 125 MB 的 RAM内存。

有这么多重复的表达式肯定意味着还有其他地方不能重用的东西,对吗?例如,简化步骤。当它发现有机会应用常量折叠时,必须重新创建 AST 的整个分支,以用常量替换一个表达式。此步骤创建了许多临时对象 – 几乎与永久分配的表达式的数量一样多(乌尔肯为 200'000)。我想将其合并到编译步骤中并“及时”简化表达式。我的尝试很成功,从低效的噪声表达式树创建乌尔肯表面的速度提高了 15 倍。

【异星工厂】星期五报道 #390 - 噪声表达 2.0-第14张

编写出最佳的噪声表达式很困难,我的目标是让编译器帮助修改器优化步骤。因此,我尝试对多个过程中使用的重复表达式进行删除。新系统会将它们提取到单独的过程中,以便它们的结果可以在运行时重用。不再需要调用“noise.delimit_procedure()”,因为它是自动的!

然而,事实证明,弄清楚多个过程使用哪些表达式并不容易,并且问题开始出现。如果我想完全删除重复数据,我就必须牺牲编译性能。不仅如此,请求运行过程的成本会更高,因为跨过程依赖性是按需计算的。我想这并不重要,因为它仍然微不足道,但是当我们可以优化它并使代码更简单时,为什么要做一些效率低下的事情呢?所以我决定完全删除程序。我们现在不对每个表面都编写一个噪声程序,而是三个(图块,悬崖,实体+装饰物),这些部分是单独运行的,不会重用过程。运行地图生成的速度提高了 20%,噪声程序编译速度提高了 50%,但结果因噪声表达式的复杂性而异。

现在您可以将噪声程序视为具有多个输出的一个过程,这有其优点。没有依赖关系,一切都是内联的。如果不再需要,则将内存分配给另一个噪声。因此,它就像一个长 C++ 函数,其中堆栈变量被优化掉,尽管这只是一个稍微简单的版本。

【异星工厂】星期五报道 #390 - 噪声表达 2.0-第15张

Lua 的格式问题

Lua 噪声表达式是在 FFF-207(https://www.factorio.com/blog/post/fff-207) 中引入的。尽管许多人声称它难以使用,但它为模组制作者提供了极大的灵活性,并允许他们创建独特且具有氛围感的地图。尽管如此,它还是存在一些问题。当转储“data.raw”原型表时,很大一部分被噪声表达式占据。这是因为格式非常冗长,每个 AST 节点都是一个 Lua 表。创建如此多的单独字符串和表也会影响性能。

如果我们不使用噪声库提供的 Lua 函数和元表,则格式将如下所示,仅计算“x + 5”。

【异星工厂】星期五报道 #390 - 噪声表达 2.0-第16张

不可读?想象一下将它与其他函数和操作乘以 100 倍。虽然噪声库隐藏了这一点,但性能损失和“损坏”仍然存在,所以我们决定改变它。我的任务是让格式用数学表达。也就是说,创建一个可以将字符串表达式处理为 AST 的解析器。起初,我专注于尽可能节省性能,从而实现整体设计。功能齐全,但难以测试、维护和进一步扩展。然后,我开始阅读其他解析器是如何做到这一点的。我考虑使用外部语法工具来生成解析器,但我认为不值得花时间学习和使用它,并且生成的代码可能不是最佳的。最后,我决定采用内部解决方案。

解析器分为三个逻辑部分。 

1. Tokenizer,处理字符流并将各个字符组分类为标记类型。运算符字符集基于 Lua,但有一些例外,同时支持 C++ 和 Lua 语法。除常规数字外,还支持科学记数法,也可根据需要添加其他格式。 

2. PostfixTokenizer,它将中缀标记转换为后缀表示法。此步骤负责遵循运算符优先级规则并确保结果表达式明确。它使用调度场算法的修改版本来处理数据。 

3. NoiseExpressionParser,它采用后缀标记并将其转换为噪声表达式树 (AST)。

【异星工厂】星期五报道 #390 - 噪声表达 2.0-第17张

我想将尽可能多的代码移至 C++,以避免过多 Lua 字符串连接。因此,我扩展了噪声表达式并定义了噪声函数。我还添加了对未暴露于全局列表的局部噪声函数和对表达式的支持。关于这些改进我还有很多话要说,但它更适合编写文档,不成为一篇有趣的博客文章。

所以现在所有的噪声表达式都是从字符串中解析出来的,使用的 Lua 表在 2.0 中被删除了。噪声表达式在游戏初始化期间所用的时间减少了 50%,因此游戏现在的加载速度加快了 20%。

进一步改进

改进地图生成的旅程并没有就此结束。定义的噪声函数的引入意味着 AST 对结果没有任何影响,我使用算术恒等式实现了部分常量折叠,因此像“1*x + 0”这样的表达式被折叠为“x”,并且不会针对每个块进行计算。

此外,我注意到我们没有充分利用基础噪声(类似 Perlin 的噪声函数)。一些特殊情况(x=x,y=y)被优化,因为我们知道我们可以对网格进行改进。我们可以重用中等图块值,因此它比通用实现快 5 倍。当我们想要使用 x 和 y 参数偏移网格时,它将不再被解释为特殊情况。添加单独的偏移参数进一步提高了性能。

我在地图生成方面做了更多工作,但并非所有内容都适合写成博客文章。例如,我删除了噪声层原型,添加了更多噪声函数,并进行了一些其他调整。然而,我不得不做出一些妥协。一些噪声函数被删除,包括阵列构造。如果需要的话,可以向新的解析器添加数组支持。

成果

自第一代乌尔肯星球原型被创造出以来,已经经历了多次迭代。其单独的噪声表达也得到了优化。加上 C++ 的改进,每个图块的加载时间原本需要 18.35 毫秒,现在只需 2.83 毫秒。这是我们非常满意的结果。

我相信您很想知道这一切与 1.1 版本相比如何:

⚙ 基础游戏初始化原型的速度提高了 7%,花在噪声表达上的时间减少了 87%。 

⚙ 瑙维斯噪声程序的编译速度提高了 85%。 

⚙ 由于删除了不分程序,噪声操作减少了(6'016 → 2'233)。

⚙ 这使我们的图块生成速度平均提高了 25%(4.8 ms → 3.58 ms)。

大约 90% 的噪声表达引擎是从头开始重写的,我估计花了大约 4 个月的时间在编写地图生成的 C++ 上。这是非常值得的,因为我们不仅拥有更快的系统,而且更易于维护,我们可以根据需要轻松添加新的噪声表达类型。设计它是一个有趣的挑战。该系统对于地图生成来说可能有点过度设计,但至少我们有一个坚实的基础,如果我们愿意的话,我们可以在其他项目中重用它。

#steam游戏#    #自动化#   #基地建设#  #开发日志# 


© 2022 3楼猫 下载APP 站点地图 广告合作:asmrly666@gmail.com