【GDC翻译】“地平线零之曙光”中基于GPU的程序化实时放置系统

原视频:GDC Vault - GPU-Based Run-Time Procedural Placement in ‘Horizon: Zero Dawn’

PDF:https://www.gdcvault.com/play/1024700/GPU-Based-Run-Time-Procedural

介绍

在这里插入图片描述
大家好,我是Japp van Muijden,我是 Guerrilla Games 的资深程序员。

我一直在研究《地平线零之曙光》里自然环境的 程序化放置、渲染、仿真。

今天,我将讨论我们为《地平线》所搭建的程序化系统。它的结果,以及GPU管线。

所以演讲基本上分为三个部分:

  • 第一部分,我将讨论我们选择实时放置系统的动机和原因。
  • 第二部分,美术工作流。
  • 第三部分,我将简单讨论我们在GPU管线中所用的算法和shader。

动机

在我们制作 Horizo​​n 之前,Guerrilla 以《杀戮地带》系列而闻名。在杀戮地带中,关卡中的每一寸土地都是手工打磨的。Guerrilla Games 在环境美术方面一直有很高的标准。

环境美术师是使用光线、构图和颜色来装饰场景以使其看起来有趣和可信的专家。而《地平线零之黎明》的开放世界需要我们研究,如何使用程序化系统创建和装饰大型开放世界,同时努力保持质量标准。
在这里插入图片描述
从历史上看,程序化系统通常看起来单调、乏味和机械化。但它确实让快速迭代成为可能,并且每平方公里减少的时间投资,也让更大的世界规模成为可能。

我们的目标是创造一个系统,让美术师可以在其中描述大量有趣且可信的环境,而这些环境可以应用于世界任何地方。但是我们有一些限制:系统以及生成的内容都应该具有高度的美术导向,并与手动放置的内容无缝集成。

最重要的是,我们的美术总监希望能够自由移动山脉、河流和游戏性内容,而无需不断修正世界。这意味着系统应该是完全数据驱动的、确定的和本地稳定的。

在这里插入图片描述

开始,我们使用传统的程序化工作流——根据程序化的参数来离线烘焙出资产应该放置的位置。我们已经在《杀戮地带:暗影坠落》期间对这个想法进行了一些试验,但是烘焙时间是一个大问题,而且迭代速度很慢。

在寻找解决方案时,我们尝试将程序化放置转移到GPU,以减少烘焙时间。

当我们做出第一个原型,看到它的放置速度时,我们很快意识到这就是我们想要的!事实上,结果看起来非常好,以至于我们决定尝试让系统完全实时,因为这不仅可以完全消除烘焙,而且还有助于减少我们必须存储和流加载的磁盘上的数据。

这意味着,我们将根据程序逻辑来主动地生成环境,并在玩家穿过世界时更新世界。

为了在 GPU 上实现这一点,同时仍然具有“确定性”和“局部稳定性”,我们选择了基于密度的系统,这意味着我们的程序化逻辑不直接放置物体。而是,程序化系统生成二维的密度图,随后将其离散化为物体的点云。
在这里插入图片描述
最终,我们可以在任何给定时间管理大约 500 种对象的程序化放置。在正常游戏过程中,当玩家探索世界时,专用的渲染后端会管理玩家周围大约 100.000 个放置的模型。

另外,系统负责的范围也比我们最初设定的要多得多:我们最终不仅放置了植被和岩石,还放置了特效、游戏性元素(如拾取物)和野生动物。为了支持所有的这些,我们不断修补我们的 GPU 放置管线以及专用的渲染管线以保持预算。我们的目标预算是当玩家在世界中移动时平均每帧约 250 微秒开销。

美术工作流

现在我们来看看美术师们是如何制作这些环境的。

生态环境

正如我之前所说,我们的目标之一是在 Horizon 世界中拥有大量的多样性。为了实现这一点,我们将世界分成不同的独特环境类型,然后为它们各自设计和构建。在现实世界中,自然环境的分类是通过生态环境的概念完成的。我们决定沿用这个概念,并开始定义我们版本的生态环境。
在这里插入图片描述
一个生态环境定义了一个特定区域的生物多样性和地貌特征。事实上,这包括了需要放置什么类型的资产以及如何放置,也驱动了岩石和植被的染色,决定了天气模式、特效声音野生动物

因此在程序化上,每个生态环境都需要有自己的设计,而这就是我们开始制作程序化的地方。

我们的目标是以自然、可信和有趣的方式填充世界,就像一个优秀的场景美术师手动完成的那样。为了尽可能接近这个目标,我们需要创建一个系统来捕捉我们美术师的逻辑、专业知识和技能。因此,系统的设计方式是使我们的美术师不仅可以完全控制输入数据,还可以完全控制系统的程序化逻辑。 (当然,对于单个的资产还是由手工制作的。)在这里插入图片描述

WorldData

那么,我们需要什么数据来创建令人信服的、看起来自然的生态环境呢?

好吧,我们也不知道。。。所以我们就在内存预算允许的范围内构建和添加需要的数据。最后,我们获得了大量数据来描述这个游戏世界,其中不仅包括放置系统,还包括游戏性内容。
在这里插入图片描述

我们称其为WorldData,它是一个2D纹理集,我们可以在游戏系统中访问它,同时它也用作我们的程序化系统的基本输入。这些纹理在玩家周围的部分分块流式加载,并输入到生态环境的放置逻辑中。

大多数地图最初是使用各种烘焙操作如 WorldMachine 和 Houdini 等工具生成的。我们在这些底图之上有额外的可绘制图层,因此美术师可以使用我们的游戏内编辑器通过笔刷或其他工具来编辑地图。

程序化放置系统仅使用约 4MB/km2 的纹理。也就是说每平方米约 32 位的数据。

下面看下我们在游戏中最终得到了什么样的数据。

在这里插入图片描述
这一部分来自WorldData中使用最广泛的一种纹理,称为Placement_Trees。 它最初是从WorldMachine 中烘焙出来的,但后来被大量的手动修改为了贴合美术方向或是游戏玩法需求。

美术师总是希望在这些类型的纹理中尽可能多地打包数据。为此,他们设计了一些共享的逻辑来编码这些纹理中附加的信息。这个特殊的纹理Placement_Trees充当我们主要的树木放置纹理。它可以用作密度图,但生态环境通常为其编码额外的含义。

在一个生态环境中,他们可以在接近 0 的值上生成郁郁葱葱的“边缘树”,在接近 1 的值上生成高大的无枝“内部树”。

这是一个示例,其中美术师对数据和放置逻辑的控制确实有助于创建更自然的外观。更好的是,它还解决了“可视性”的要求,而无需程序员的支持。

还有许多其他类型的WorldData。 其中大部分是 BC7 压缩的。分辨率因类型而异,从 1m 到 4m 的分辨率不等。在我们继续之前,让我们快速看一下更多的纹理。在这里插入图片描述
下面是由道路工具绘制的“道路数据”,以及根据游戏中非程序化生成的(即手动摆放的)物体生成的“物体数据”。由于程序化系统是基于纹理的,因此像这样的WorldData纹理是系统可以读取其余非程序化内容的主要方式。
在这里插入图片描述
你可以想象,当美术师为生态环境设计程序化逻辑时,美术师必须确保生态环境对道路、岩石和河流等事物做出自然的反应。这些逻辑的很大一部分围绕着读这类纹理和定义的区域,例如“道路的边缘”、“岩石的旁边”或是我们刚看的“森林的边缘”。

这些结构可能会变得相当复杂,幸运的是,这些定义可以只进行一次,随后在生态环境之间共享。
在这里插入图片描述
我们的高度图也是WorldData的一部分,这些纹理用于逻辑中,并作为我们程序化物体的放置高度。我们有多层高度图,所以我们不仅可以把东西放在地上,还可以放在物体的顶部和水面上。

最后是一些生成的纹理。 这些来自WorldMachine,几乎从未由美术师手动绘制。 我们使用这些纹理在我们的生态环境中创建逼真的变化和环境的反应。

逻辑网络

所以我们已经看到了美术师如何设置和操作WorldData。现在让我们来看看逻辑端。

简而言之,我们使用“逻辑网络”。类似于 nuke、substance 或任何其他shader制作工具。

逻辑网络的目的是使用WorldData作为输入,最后生成单个的密度图,而密度图与资产或资产集联系。然后这些密度图会被离散化成点云,以便作为实际在世界中放置的资产的位置。

让我们看看如何将WorldData纹理组合成密度图:
在这里插入图片描述

在这个例子中,假设我们正在设计一个生态环境中放置一棵树的逻辑。作为起点,我们将调出一个WorldData节点,该节点链接到我们之前看到的placement_trees图中。虽然,我们可以直接将其作为我们树木的密度,但那样的话我们就会将树木放在了水中、穿过了岩石、阻挡了道路。

因此,为了移除岩石上的树,我们将值乘以物体纹理(Topo_Objects)。随后,我们对水图和道路做同样的处理。最终,你会得到一张纹理,该纹理定义了树木真正的有效区域。

这个逻辑网络不仅可以被树引用,还可以被链接到其他的逻辑——如果它想要知道树可能被放置在哪里的话。

下面,是我们的“森林”生态环境的资产定义。它由树木和植物资产组成,并按层次结构分组。 右侧的叶节点链接到实际的资产,称为 (放置)目标,将会在整个生态环境中实例化。
在这里插入图片描述
对于每个资产,我们定义了footprint,它定义了放置系统中对象的有效直径。离散化算法将使用footprint来分隔对象并避免碰撞。 可以看到乔木等大型模型相距六米,而灌木丛模型相距仅一米。

由于当前还没有连接逻辑,因此所有资产都将具有完整的密度。每个资产的密度图将是全白的。

下面让我们将其加载到游戏中:
在这里插入图片描述
这是一个很好的起点,但下面我们要在其中加入一些逻辑。

首先,我们加载一个WorldData纹理,并将其直接应用到网络中的森林的根节点。现在,森林节点有一个链接到它的密度图,它就会把它的密度传递给它的子节点。

让我们更进一步,我们来在森林中定义一片空地。我们链接一个新的WorldData纹理,我们称之为Clearing。现在我们想从Clearing中移除东西,而不是添加东西,所以我们添加了一个Inverse节点。
然后,我们将它连接到乔木和灌木中。
在这里插入图片描述
因此,虽然整个森林仍会看树木图,但在森林里,我们现在可以绘制一块空地,以减少所有灌木和所有乔木的密:
在这里插入图片描述
我们刚刚制作的那个小网络在我们的编辑器框架中成品看起来像这样:
在这里插入图片描述
这是我们最复杂的逻辑网络之一,包含大量我们共享区域的逻辑,在多个生态环境中使用。但制作一次后,就可以在生态环境之间共享它们了。

GPU管线

下面让我们进入技术细节的讨论!

我们已经看到了美术师是如何建立逻辑的,我们知道了它们的输出是什么。但是让我们看看,接下来在GPU上如何处理数据。
在这里插入图片描述
在我们对数据做任何事情之前,需要将我们创作的网络编译成可用的形式,毕竟那些逻辑网络在GPU上并不适用。

这个可用的形式 称为。我们遍历生态环境逻辑中的所有资产(或者说逻辑网络的叶节点),把它们放在一起,变为层的列表。每层代表一个运行时的程序化负载,链接一个资产,它包含的信息可以从WorldData计算出密度图并填充世界中的一个区域。

层所包含的信息还包括与之关联的逻辑网络,它被编译成一个中间形式(关于密度图操作的中间指令语言)。这种表示可以编译成计算着色器的二进制文件,或者直接输入到我们用于调试接口的基于 GPU 的 解释着色器——它基本上就是一个在CPU上运行的GPU虚拟机。

中间形式还能做的一件事是允许不同子图的合并。一个资产有上百万种不同的放置方式,可能有上百万种不同的层都使用它,这很常见。由于有中间形式,所以你可以把它们都合并到一个层中,而美术师可以通过AUTHOR DRIVEN MERGING SEMANTICS来确地定义如何合并。在实际中,它会将玩家周围的层数从数千减少到数百。

现在有了我们的层列表,让我们看看这样一个层是如何在 GPU 上放置的。我们从最初的密度图生成开始,然后是离散化。

第1步:密度图着色器

在这里插入图片描述
我们运行时管线的第一部分是计算世界上给定区域内单个层的密度图。在正常情况下,这是由称为密度图着色器的预编译计算着色器完成的,该着色器通过存储在层数据中的中间形式预编译而来。

我们整个放置管线在粒度上伸缩,具体取决于资产的 footprint。 诸如树木之类的大型物体放置在 128x128m 的大块中,而草则放置在 32x32 的块中。另外,无关于粒度,我们有每块 64x64 像素的固定密度图分辨率。

在这里你可以看到游戏内调试界面,美术师可以逐步浏览密度计算,查看每一步离散化的结果。它还用于浏览和检查数百个活动图层。
在这里插入图片描述
这是在GPU解释着色器上运行的放置系统,因此可以轻松停止、单步执行和调试。 中间表示和解释器的使用非常灵活,我们在 Horizon 的整个开发过程中都使用了它。

你可以看到正在构建的密度图,然后是离散化步骤。

第2步:生成着色器

在这里插入图片描述
在密度图步骤之后,我们运行生成步骤,该步骤将密度图离散化为单独的位置。

我们的方法基于称为有 Ordered Dithering 的技术。

下面是一个理想化的密度图。现在,如果我们缩小它,并应用 Dither 过滤,然后对其应用 Ordered Dithering(在 photoshop 中),我们最终会得到这样的结果:
在这里插入图片描述
如果想象在每个白色像素上创建一个对象,我们就有了一种离散化形式。零密度像素上没有对象,密度随着值变高而增加,直到白色完全覆盖。

这背后的过程极其简单:每个像素的计算都是独立的,它基于一个小的重复的阙值图案,结果将作为像素的颜色。这种方式非常适合 GPU:无依赖,数据量小。

这些类型的 Dither 中最常用的图案是拜耳矩阵
这些数字定义了单个输出像素阈值增加的顺序。它下面的图像显示了当输入缓慢递增的结果。

但老实说,如果我们将所有资产放置在这样的图案中会有点明显。不过幸运的是,我们不受像素边界的束缚。

感谢我们在GPU上,我们有线性插值,我们可以定义自己的UV。所以我们的图案并不是一个常规的像素网格,而是一组精心安排的显式位置,每个位置都有自己的隐式阈值。

这是我们图案生成器的一张旧截图,是多年前我们设置时的截图。也许不太能看出来,但它基本上是一个disk packing,带有一些可配置的东西如随机数、点数、边界等。( 颜色编码有点奇怪 但基本上蓝色是很低的,当它在阈值上升时就会变成紫色)
在这里插入图片描述
所有人在进行 ordered dithering 时都使用拜耳矩阵,这并非巧合。因为它是一个数学构造的矩阵,具有一些不错的属性,而我们也将这些属性应用到了我们更自由的版本。遵循的规则是:阈值本身需要在 0 和 1 之间均匀分布;并且两个连续的阈值的点之间应具有最大距离

通过缩放图案,我们可以使 2D 距离 W 等于层的footprint。之后就可以直接在世界空间中应用图案,其生成的点云会拥有适当的间距,正如层中的设置。

在这里插入图片描述
森林中的树木是个很好的例子,它有一个非常大的footprint,并且密度完全覆盖。这样,我们就有了一个均匀的、明确定义的树木之间的最小距离,保证了玩家和敌人的正确导航。

但即便使用适当的拜耳图案,当密度变高时,生成仍然会产生令人讨厌的副作用,即产生视觉上的图案。你可以看到这个白色的三角形,后面的那些树是完全对齐的。有人花了很长时间才注意到这一点,但是一旦你注意到了它你就再也无法忽视掉它了。所以,我们添加了一个用户定义的噪声偏移:
在这里插入图片描述
我们还有计划做多个模板和“王浩瓷砖”,但最终发现并不真的需要。

在这里插入图片描述

这是生成着色器的四个线程组的概述,它离散化了一个区域的密度图。在这里,我们看到纯白的目标区域,以及跨越该区域平铺的四个图案。每个计算线程组运行在一个图案上,其中每个计算线程计算一个采样点。这很好地映射到线程组着色器结构中。

让我们逐步观察单个计算线程。

首先,我们必须确保我们 EARLY OUT 所有位于瓷砖外的点。

然后,我们读取密度并进行阈值测试

当通过阈值测试时,我们有2D位置,但没有高度与之匹配。因此,我们要对正确的高度图进行采样以生成完整位置。

由于我们在此处的纹理缓存邻域中,我们不妨与其一起构建高度法线

通过测试的线程将它们的点添加到 GROUP LOCAL MEMORY 中的缓冲区。最终,我们将数据附加到输出缓冲区的末尾,以减少输出缓冲区上的 atomic contention。

所以,现在我们有一个带有方向的点的点云缓冲区,但还没有完整的世界矩阵。我们也没有应用任何针对每个对象的逻辑,例如随机倾斜、旋转、高度等。

在这里,我们再次看到游戏中的屏幕来用来显示游戏中的一些生成的离散化。下面是一个覆盖具有粒子特效的区域的层。 放置系统被指示以完全覆盖简单地填充指定区域。 结果是一个紧密压扁的六边形网格:
在这里插入图片描述
这显示了更自然的放置,花旗松被放置在森林区域内。
在这里插入图片描述

第3步:放置着色器

正如之前说的,我们有了点云,但是还没有完整的世界矩阵。因此我们制作了第三个着色器叫做放置着色器。放置着色器会抓取一个点云,然后一个接一个地应用各种参数。比如,你想让树在地面上弯曲多少,或者你想让石头绕着它的角度随机旋转多少等等这些放置规则。
在这里插入图片描述
最后,它会应用所有行为参数,并从生成的点云生成世界矩阵和包围盒。对于每个输入点,都有一个输出矩阵。

到目前为止,所有操作都是确定性的,除了生成着色器的输出顺序。因此我们需要传递模板点和图块 ID 以及位置和法线,这样每个放置位置都有一个完全确定的 ID。

现在,我们已经完成了从一个密度图网络到世界矩阵的完整过程!

这是 GPU 计算管线的完整概述,每一层都运行该管线来实施放置。
在这里插入图片描述

WorldData进入密度图着色器,该着色器将其计算为 64x64 密度纹理。然后由生成着色器采样成点云,最后由放置着色器扩展成世界矩阵,最后复制到CPU内存。

你可以想象这一切是如何在多个层上进行的,所有这些层都可以在 GPU 上并行计算,没有太多问题。

解决碰撞问题:分层Dither

不过,我们还没有解决碰撞问题。就目前情况而言,你会将大量资产堆叠在一起,因此必须添加某种避免碰撞的功能。幸运的是,这个问题有一些有趣的解决方案。
在这里插入图片描述

在这些情况下,碰撞的一般解决方案是进行回读,这意味着你必须回读以前的放置,并在碰撞时丢弃或重新迭代。如果想要保持局部稳定性和确定性,就会在 GPU 管线中创建复杂的依赖关系。GPU 并不喜欢这些。

但还有另一种非常有趣的特殊情况,如果你有两个具有相同 footprint 的物体,你就可以得到非常快的碰撞解决。因此,第一种回读的方式实际上从未在游戏中使用过,我们只走捷径。这意味着我们只能碰撞具有相同 footprint 的物体,这有点奇怪。

But artists, they kind of liked it, they were starting to make all these cool things. but okay, we’re gonna use these objects you want these to collide, so put them in the same footprint. it may like categories of footprints and all that kind of stuff, so that actually worked out very well for us.(不确定的翻译:但是美术师们并不厌恶这个,它们仍旧制作很酷的东西,但是会保持一致的 footprint 。也就是说,他们会对 footprint 进行分类,然后为其分配物体。所以这种方法对于我们来说很顺利。)

下面,让我们讨论这个被称为分层Dither的算法。

在我们研究细节之前,让我们换个角度并关注密度图的一维切片,使其更容易被观察。
所以我们抓一张密度图,从中做一个切片。
在这里插入图片描述
在普通的Dither中,你只有一张密度图,而在分层Dither中,我们一次有多张密度图。

不过,我们还是从单一的密度图开始。我们运行生成着色器,它使用不同的阈值对不同点的密度进行采样。我们可以将其形象化为这些小圆点,位于密度曲线下方的每个黄点都会生成一个放置的对象:
在这里插入图片描述
现在,如果我们直接运行另一个使用相同 footprint 的层,会发生什么?
——它将具有完全相同的 Dither 图案。也就是说,它会将一些物体重合地放在之前的位置上。
在这里插入图片描述
这个问题的解决方案是简单地将密度层叠在一起。这样就不会有任何碰撞了,因为阈值上的点你只能放在其中一个区域,不能同时放在两个区域。
在这里插入图片描述
沿着给定采样点的层现在可以被视为多个资产的概率分布,其中样本点的阈值可以看做是均匀分布的随机值。
整个方法也同样适用于 2D 的 Dither 。
在这里插入图片描述
所以我们解决了碰撞。但还是有依赖关系,因为你会将层叠加在一起,所以如果你想放置第二层,首先要生成第一层。不过这个依赖关系只与密度图阶段相关。通过一些重构,你仍然可以使用原子操作独立地计算 GPU 上的所有层,从而节省昂贵的 GPU 刷新。

所以在这里你可以看到我们在点击生成之前运行了多个密度图着色器。
在这里插入图片描述
这实际上是成品的生态环境中最常见的情况。我们经常有 20 多个具有相同 footprint 的层,它们像这样堆叠在彼此的顶部,我们几乎总是对非常特定的几层感兴趣,这意味着我们在不想放置的层上运行密度图,但我们需要它们的密度图以到达堆栈中的更高的位置。

因此,我们的管线为实际需要放置的每一层运行 N 个密度图。 我们希望这个数字 N 尽可能小,因此我们使用各种启发式方法对层进行排序,使最常放置的层位于这些堆栈的底部。

GPU调度

最后一步,我们看看我们的 GPU 调度。
在这里插入图片描述

如果我们每帧只放置一层,它会非常慢。所以为了安排更多的工作,我们多次实例化我们的整个管线。请注意,我们无法跨不同管线调度依赖层。 所以我们在这一步主要是按照空间进行并行化,让每个管线选择一个区域来填充。

但问题是,生成的密度图仍然有依赖性,所以到处都是刷新。你得等它们完全完成,然后才能转到下一个Shader。这并不理想,因为它们不能在 GPU 上重叠。

所以这是我们最终的 GPU 加载布局:
在这里插入图片描述
我们首先在所有管线上独立运行密度图。一直这么做,直到碰到一个需要放置的层。然后我们运行生成着色器,和一个特殊的分配着色器来动态分配复制缓冲区中的内存。然后放置着色器将带有方向的点转换为复制的缓冲区。

整个过程可以重复,直到所有管线的所有工作都完成,但我们最多提供 4 个发射(译注:可能意思是同一时间最多只有4个管线真正运行?),以减少内存负载并防止 GPU 峰值。最后,复制缓冲区将所有数据一次性复制到 CPU,这为我们节省了数百微秒的同步和复制开销。

好吧,基本上就是这样。下面是开启了调试模式的游戏:
在这里插入图片描述
你可以看到那些依赖的堆栈、那些层、那些堆叠的层,他们在运行时解算。我们必须降低可以并行运行的管线的数量,因为你可以看到它实际上可以制造很多管线。

总结

在这里插入图片描述

这就是整个系统,它非常成功。我们在游戏中多处使用它,我们在模型、特效、玩法元素中使用它。

它的视觉质量非常好,非常适合我们的美术方向。我认为美术师真的可以按照他们的想法去做。甚至是未经修饰的区域,即美术师们从来没有手动修改的区域,也具备了可发行的质量。

我们的开销是250微秒的负载,这在预算之内。

这个工具用来制作开放世界非常强大。因为我们所有的自然资产都是由整个公司的三个人制作的。而关于生态环境的逻辑,都是一个人做的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值