原视频链接:https://www.gdcvault.com/play/1025215/Procedural-World-Generation-of-Far
PPT链接:https://www.gdcvault.com/play/1025557/Procedural-World-Generation-of-Far
另一版视频:Procedural World Generation of Ubisoft’s Far Cry 5 | Etienne Carrier | Houdini HIVE Utrecht
介绍
欢迎大家来到《Far Cry 5 的程序化世界生成》。
我的名字是Etienne Carrier。
我已经在育碧蒙特利尔做了三年的TA。我在FarCry5项目初期就加入了。我并没有3A游戏的经历,但是当加入育碧的时候,我有一个“秘密武器”——我有程序化生成工具方面的经验。我关注程序化生成已经六年了,在加入育碧之前我是做独立游戏的,当时我就尝试将程序化引入我自己的管线。
所以今天,我很高兴和大家分享我们为 FarCry5 所开发的程序化生成管线。
在我们开始之前,先看下今天演讲的内容:
我开始会先做一个介绍,包括我们想要解决的问题与挑战。
然后是管线本身的目标。
然后看一看我们为项目所开发的程序化生成工具。以及从使用者角度看这些工具。
之后将更深入地研究管线本身。
我们还会看一看崖壁工具和生态工具。不幸的是我没有足够的时间在我的演讲中详细介绍我们制作的每一个工具。但至少我们可以看看这两个。
然后快速看看我们在开发过程中所改变的东西。
最后是总结。
1. 介绍
所以,我们最初想要解决的挑战是什么呢?
首先,我们的地形是在不断变化的。这是两年半期间 FarCry5 地形的演变:
当我们有这么多类似的迭代时,我们该如何管理我们的内容呢?
在较小的一个区域上也有相同的演化。如下图所示,我们有一个最初由手动绘制的森林。
最初,它的分布和地形的起伏变化是一致的。然而,随着时间的推移,它不再与地形所匹配。所以我们该如何解决这样的问题呢?每次森林区域变化时都重新绘制是一项乏味的工作。而且,如果真的这么做了,不同用户之间的编辑结果也会不一致。
难道我们应该在早期就将所有的内容确定下来以避免这样的问题吗?我不这样认为,因为我认为“迭代”对于游戏的开发是至关重要的。
2. 管线的目标
正是带着这些挑战,我们创建了程序化生成管线。
我们的第一个目标是:开发一个在宏观级别上的管理工具,它应使用看起来自然的内容填充世界。
而这个内容也需要与地形的拓扑结构相匹配。
这里我们有一个之前一样的例子。然而这一次,森林的分布是会适应地形的形状的。正如我们所看到的,结果与地形上的任何化变化相匹配。
另外,旧的管线也需要自动化。
我们使用 Houdini 和 HoudiniEngine,并在构建机上运行NightlyGeneration(夜间生成)。我们有几台构建机,可以一个接一个地处理世界中的不同地方,直到生成完整的世界。这个过程确保了我们的用户每天早上都会得到更新好的世界数据。
所有的数据还需要是确定的。
这意味着,在相同的输入条件下,产生的结果需要是相同的。所以,无论我们是用构建机A来构建左边,还是构建机B构建右边,中间部分总会是相同的结果,这对于导航网格的生成等方面是很重要的。我们有一个夜间构建,世界是被分成地图来生成的,因此地图之间的连接需要是无缝的。
最后,我们的目标是对用户友好。
仅仅用“夜间构建”是不足以确保用户拥有最新的数据的,他们还需要在工作的同时也进行Bake。另外,工具在我们的编辑器中当然是可用的。
3. 工具
现在,让我们快速看一下我们开发的程序化工具。
最初,我们的任务只是解决生物分布问题。我们开发了一种生物分布工具,但最终我们扩展了更多。
首先,我们有活水工具。它可以产生湖泊、河流、小溪、瀑布。
我们有围栏和电线杆工具。
我们有在陡峭的表面上生成崖壁的工具。
然后是生成植被的生物分布工具。
我们还有个雾密度生成工具,其数据会被雾着色器所使用。
我们还有生成世界地图上的地形的工具,上面有散布的微型树。
4. 用户视角
那么,从用户角度来看,工具是如何使用的呢?
接下来,我将快速展示,一个空白的地图是如何被程序化工具填充的。
首先,用户会使用 Dunia 编辑器工具来改变地形。
然后,美术师可以铺设一个活水网络。
这是通过曲线和样条来实现的,它们将被用作活水工具的输入。对于河流,我们可以直接控制宽度。一旦对输入感到满意,用户只需要点击活水工具按钮,这就会产生一个水面,一些地形纹理,以及一些水下的资产。
然后,用户可以生成崖壁。
这个工具仅仅是基于地形的坡度的,所以基本上你要做的就是在想要的地形区域上运行生成就行了。我们也有些额外的控制,如排除,遮罩和其他东西,但我们会在后面看到更多的细节。
然后,美术师可以添加植被。这是通过 “生态绘制工具” 完成的。
在这里,我们就是选择并应用了一个 “主生态”,然后运行程序化生成。而主生态会自然地分布草和森林的 “子生态”。而且它会在靠近水的地方生成特定的资产,它还会避免在“崖壁腐蚀线”(由崖工具生成的)上生成植被。而且,在这种情况下,海拔也会产生影响,如较高的海拔上的树会更小。
很好,但用户肯定还想对一个地点进行定制化。
所以美术师能做的就是从清理一片区域开始,把它画成另一个生态,比如草。然后铺设道路样条。
到目前为止,我们的结果很自然。但我们会添加人类对自然的影响。一旦我们对输入满意了,我们就可以计算道路工具,然后得到更新的结果。可以看到树木都被清除了。
当然,还是有一些资产是需要人工设置的。比如停车棚,载具等,所以关卡美术师还没有失业。
基于这些,或许需要重新调整下生态的绘制。
有一些生态并不会散布资源,而仅仅是改变地形的纹理。所以用户可以用它来创建某种车道等。我们还可以添加一些树木和森林,这是通过森林的子生态来实现的。最后,一旦对设置满意,就可以重新刷新生态工具。可以看到,我们的车道上没有任何草,我们在那里有一块小森林,因为我们在那里画了子生态。
下面让我们看看围栏工具。
它的输入同样是样条,所以用户会简单地在样条的参数上设置生成类型,然后运行生成。这里我们建造了一个漂亮的木围栏。
用户还可以在世界中添加电线网络。
电极将在样条的每个控制点上产生。在此示例中,我们添加了 3 种不同类型的电源线,它们将自动相互连接。我们添加了一个标准的线、单线,还有连接两个房屋的线。我们确保了将线连接在一起。然后运行生成,可以看到工具很好地在需要的地方添加了变压器。还有,这里用户没有把线连接在一起,但是系统会在一定阙值内自动连接。
这里还可以看到一个小问题,电线碰到了树。但它很容易被修复,只需要刷新生态工具就可以了,可以看到树木考虑了电线因此被移除了。因为它们是一个系统,它们之间可以相互交流。
5. 管线
现在让我们来看看一切是如何运作的。
我们将HoudiniEngine实现在了我们内部的引擎与编辑器中,也就是DUNIA2。
它打开了管线的输入与输出的大门,让数据可以在二者之间交换。
好的,下面为那些不知道Houdini是什么的人简单介绍一下它。
它是基于节点工作流程的 3D 软件,它还让你可以开发我们称为 Houdini Digital Assets(或简称 HDA)的工具。当特定应用程序(如游戏编辑器)有插件实现时,HDA 可以在 Houdini Engine API 中运行。
一些输入通过 python 脚本从 Dunia 发送到 Houdini:
- 所以我们有“世界信息”,包括世界的名字、尺寸。因为我们在制作过程中并不是只有一个世界,我们还有用于测试的。
- 然后我们还发送文件路径,因为我们不能期望所有开发者的电脑上的路径都相同。
- 然后是“地形区域”,基本上就代表着我们想要生成的地形区域。我们稍后会看到更多的细节。
- 然后是样条和形状,连同元数据作为几何属性。比如,围栏曲线将拥有“围栏种类”作为几何属性。
其他输入直接提取到磁盘上,然后在 Houdini 中,会使用 Dunia 提供的文件路径读取:
- 所以我们有高度图。
- png类型的生态绘制工具。
- 2D的地形mask。
- 我们还可以访问Houdini几何体(可能是其他特定程序化工具生成的)
但程序化输入最主要的还是地形本身,因为程序化生成总与特定的地形区域相关联。下面是地形在编辑器中划分的方式:
我们最小的粒度是一个 64x64 米的区域,这意味着它是用户可以bake的最小区域。
当某人想要重新bake程序化数据时,他将选择它的生成区域:
- 要么是所有加载的内容。值得注意的是,Dunia并不会将世界的所有内容都加载。世界被分成几张Map,当我们打开Dunia时可以选择加载哪些Map。
- 还有一些选择是当前相机之下的Map、Section、Sector。
- 另一个选择是,相机中可见的所有Sector。
下面,我们来看看Houdini会送回编辑器的数据:
- 实体点云
- 地形纹理层
- 地形高度层
- 2D地形数据(RGB或者灰度)
- 程序化生成的几何图形
- 逻辑地形区域,基本上就是地形上ID,用于环境预设和后处理。
然后,所有的这些都以一种特殊的格式保存在磁盘上的缓冲区,这让编辑器有时间按照自己的节奏加载数据。还避免了发送一个占用大量内存的大缓冲区。
下面是一些关于实体点云的快速说明。他真的可以是编辑器中有位置的任何类型的对象。
- 植被资产
- 石头
- 收集物
- 贴花
- 特效
- 预制体
- 等等,甚至可以是环境中的动物
这些点会连同要生成的资源的物体ID一起导出。
而数据的导出也是工具之间相互连接的关键。程序化生成是按照顺序的,因此构建机总是会按照下面的顺序计算他们:
因此,每个工具都可以导出数据,目的是影响之后的工具。因此一些工具只会写新的mask,而一些会读取前一个工具生成的mask。
作为一个简单的例子,我们有活水工具将产生水的mask。它将会被生态工具用来驱动特定物种的生成行为。