看游戏如何用UE高效生成逼真的野外场景
以下内容来自:NExT Studios
“过程化内容生成”也叫“程序内容生成”(Procedural Content Generation=PCG),是一种自动为游戏、模拟或电影创建数字资产的方式,可以大大提高内容生成的效率。
NExT Studios 在使用虚幻引擎4开发《重生边缘》(SYNCED:Off-Planet)的过程中,在过程化生成场景方面进行了一些尝试:除了介绍各种生成内容的思路外,还分享了针对过程化生成工具加入场景制作后,如何解决新的工具在实际工作中遇到的各种问题,以及一些实验性工作分享。
《重生边缘》主地图
主地图3x3公里,大约有 20 个 POI、16 层地表、9 万多棵树、30 多种植被、 20 公里长的路和 6 公里的河流。最初除了Unreal Editor之外,我们没有过程化生成的积累,在加入过程化生成内容的时候,也不能覆盖已有的 prototype 关卡,同时需要兼顾关卡美术和关卡策划的操作习惯(他们在Unreal Editor中进行数据编辑工作,但过程化生成算法是在Houdini中实现,需要导入导出数据),各个生成元素之间有一定数据依赖关系(比如河流改动会影响地形的高度图和材质层,地形高度图改动也会影响到植被的分布等)......我们面临许多挑战。
我们开发了约30个工具,但因为场景中每个 POI 的风格和设计都非常不一样,较难提取统一的理性规律,所以工具的目的是调整关卡布局时,加速周边环境的修改,大部分的生成内容集中在野外区域。
场景结构
地表侵蚀的作用?地表材质层的权重分布如何计算?
把mask转成地形后做侵蚀美化是基础功能,侵蚀会改变高度图的表现,也会生成地表材质层的权重分布。
侵蚀前后地形对比
地表权重图是在Houdini当中进行计算的,大概原理是:根据高度图的斜率计算出大体分布,然后配合例如雨水冲刷的效果,根据每一层的石头和沙地的规则,定义出不同的权重图,然后导入引擎,就可以得到混合好的地表效果。
混合好的地表效果
垂直面地表崖壁岩石如何生成?如何在地表上呈现更丰富的细节?人物卡进mesh里如何处理?
我们当时参考了一些业界资料,比如《孤岛惊魂5》(Far Cry 5)、《刺客信条》(Assassin's Creed)。一开始我们根据地形的坡度,提取它的区域转成mesh,然后生成UV,贴上我们想要的纹理后,用displacement map模拟了岩石表面的凹凸,但是近看的效果不理想。于是我们尝试了另外一种方案——mesh贴片。像贴瓦片一样,把固定的mesh往所需区域里重复粘贴,这样可以用比较低的面数达到更好的效果,因为它的几何细节会更多一些。
mesh贴片效果
在生成崖壁的mesh的时候,崖壁下方和与地表过渡的地方,可以用与岩石纹理类似的地表材质来表现,因此生成了相应的地表材质权重层。还需要处理生成的岩石与地表的过渡:生成mesh的时候,把权重写到了顶点色里面,然后采样地形的纹理做柔和地过渡。岩石下方我们可以生成一些碎石或砂石,这样会有更具细节的地表表现。
增加细节前后的岩壁对比
在实际运用中还会遇到一些尴尬的问题,例如人物有时会卡在生成的mesh里。这是因为Unreal根据不规则的mesh默认生成的碰撞体有很大瑕疵,这时我们会针对这些不是闭包类的mesh生成特殊碰撞体,然后再一起导入引擎。
如何计算获得视觉均匀的植被分布?植被间距如何考量?如何进行撒点操作?
我们参考了《孤岛惊魂5》(Far Cry 5),这几乎是业界最详细的分享。我们的整体生态和植被组成是不一样的,我们选择的是温带针叶林。具体做法是:我们根据高度图生成各种各样的mask,再配合噪声模拟随机分布。在此之上,我们可以把光照、风向和气候影响也考虑在内,生成更多的mask,通过运算得到我们想要的分布。
有了植被的区域后,我们可以在区域中进行撒点操作:一种操作是直接在区域中进行随机撒点,另一种是围绕某个目标点周围进行随机撒点。我们可以在第二种情况下生成伴生的灌木,一般情况下我们不可能一次只生成一种植被。
两种撒点方法
我们针对每种植被定义了三种半径,用来防止树与树之间的重合。根据这些规则,我们量化这些点的位置,让离得较近的点更易被判断。如果这些点都被包围的时候,我们无法移动把它排除掉,我们就删掉它。
通过这样不断迭代,我们可以获得非常均匀、没有互相穿插的效果。但在不同的树种之间(例如高大的树木和低矮的灌木间),其实可以有一定的穿插,这是我们定义外半径和内半径的原因。对于大地图上的不同的区域,会有不同的生态分布。我们可以通过全地图刷mask来区分我们每一部分使用怎样的生成规则。比如说在海边,我们会以一些草地沙石为主,山上则以森林为主。
滑动查看植被效果图
河流如何生成?下游比上游还要高如何处理?转折度大的河流如何处理平滑?生成完河道之后,地表的纹理细节如何调整?
在《重生边缘》中,河流是表现类的效果,不影响游戏的玩法,所以过高的高度差、过深的水面都是策划和美术不想要的,所以我们希望做很浅的溪流。我们算法上也学习过《地平线》(Horizon)的分享,整个生成过程大概分成:
• 我们先有大概的曲线,根据地形高度图做自然滑落。相当于把一条绳子扔在地表上,它会自然地弯曲;
• 有些地方地势比较高,我们可以挖一条河道,这时会改变地形的高度;
• 因为我们不想生成过大高度差,从高地往低地过渡时,我们需要形成多级小瀑布;
• 然后我们根据河流的地形地势分布和弯曲度,生成河面河道宽度的变化。
• 最后我们在河道里撒上一些碎石和水花去装饰,并生成河流的流向。
河流生成过程
河道生成的其他问题:
• 支流跟主流交叉的地方,可能高度并不一样,我们需要对齐高度;
• 有可能下游高于上游高度,这时候需要用下游的高度去往上游去做修正;
• 转折度较大和多个支流交叉的区域,我们不可能生成很多层的河面的mesh,所以做平滑处理,从平滑后的河道形状提取出河面,再对河面mesh进行切割和减面,这样对性能的优化很有帮助。
如何减少河面流向图的内存占用?如何编辑河流的走向和效果?
对于河面的流向图,如果用全地图的flow map一张纹理来覆盖的话,会浪费非常多的UV空间。考虑到更高效性能和更低内存占用,我们把流向信息写到顶点色里,只需占用两个通道。
生成完河道后,地表的纹理会随之变化,我们可以铺设鹅卵石,或在河岸边缘生成潮湿泥沙的效果增加河岸表现,生成相应的植被分布增加细节等。
我们使用Unreal Landscape Spline的内置功能来对河流进行曲线编辑,因为它比较符合美术的操作习惯。我们先拖拽出河流经过的区域,然后编辑各个支流大致的路径,设置每条支流的起始点,之后一键生成。这时候我们可以根据地势做自然弯曲,挖出河道、生成地表的纹理,生成河面的mesh,还有河面流向的数据,包括水花、石头等等。
滑动查看河流实际效果
裂缝长草、土路破损、路口过渡、车轮印记等道路上的“细节加分项”如何实现?多层贴花的优先级如何制定?
游戏当中的道路基本是关卡策划在编辑,它对玩法是有一定影响的,当我们加入做生成工具的时候,路网已有大约百分之八九十的完成度,所以我们并不是生成道路本身的路网,而是选择去增加一些细节“加分项”。比如裂缝长草、公路破损、不同道路之间的过渡、交叉口的车轮印记等。
实现的思路就是使用海量贴花(在 GDC 2017 的 Ghost Recon Wildlands: Terrain Tools and Technology 中有类似分享)来实现,包括路面的破损、道路中间的车轮印、车道标记、水迹效果、路边的落叶的尘土的效果,都是通过贴花的方式来实现的。但裂缝里长的草不是贴花,是在生成裂缝贴花的时候,顺便把裂缝草的位置一起计算出来。
还有一个比较实用的功能,用Unreal Landscape Spline做道路的时候并不能很好地处理交叉口,我们生成了任意角度交叉口的贴花,掩盖了衔接处的接缝问题。
道路交叉口贴花
有了多层贴花,我们需要定义呈现层次的优先级问题,所以我们制定了“同级融合,高级覆盖低级”的贴花规则。全体的半透明材质的贴花数量加起来有数万个,有很重的overdraw,会带来非常严重的性能问题。所以我们使用了 Unreal 的Runtime Virtual Texture 来进行优化,把地表混合的结果跟道路和贴花混合的结果缓存到了一张巨大的虚拟纹理上面,可以大大降低地表绘制的开销。
另外我们在道路曲线计算完毕之后,可以根据道路曲线的分布来调整地表的权重分布。比如我们可以在道路的周边去生成相应地表的过渡效果(裂缝、草、破损尘土、水迹、路口交叉口车轮的印记),另外还有道路的附属物(比如护栏、电线杆)等。
滑动查看更多路口效果图
下面将针对工具加入场景制作后,如何解决新的工具在实际工作中遇到的各种问题,以及分享一些试验性工作。
过程化内容生成中容易遇到哪些流程问题?
比如生成的效果达不到美术的要求;工具的使用门槛太高,他们不想用;工具不够稳定,他们觉得折腾的成本太高;或者涉及多人协作的问题,这个事情到底是程序员做、TA做、还是关卡美术做?多个关卡美术的协作需求如何解决?等等。只有解决了这些问题才算是一个合格可用的工具链。
基于前面的分享,相信大家也能看出,我们的目的是:并不追求全地图自动化生成,而是根据需求做一些定制,提高制作效率。
过程化内容生成管线
自动生成内容和人工编辑内容之间的冲突问题如何解决?生成内容之间的关卡切分问题如何考虑?不同子关卡的生成限制如何保证结果的稳定性?
我们总结出来两个原则,第一是生成的内容不能覆盖人工编辑的内容。第二是各个子关卡之间尽量地独立地编辑和生成。就像 Unreal 大世界里的地图,通常会编辑成多个子关卡,方便多个美术协作,然而对于过程化生成的内容,也需要去做这样的支持。比如可以对world composite做一个支持,支持地形自动切分到子关卡。
自动切分到子关卡
关于生成的内容(大量贴花、崖壁、河流等)之间,也都需要切到子关卡。只有切分了子关卡,才能做独立地剔除和 level streaming,同时也能支持不同的人来编辑和生成不同的子关卡。
对于不同的子关卡,我们生成的限制是需要保证结果的稳定性:不管是多块一起计算出来的结果,还是单块单独计算出来的结果,应该是一致的。这样才能够保证按照子关卡的方式去工作。
地形和建筑的稳定性如何解决?
对于地形来说,我们没办法很好地解决。因为关卡与关卡之间会有一道必然的交界,如果只更新了其中的一块,另外一块就接不上了。
虽然可以对整个地形做整体的侵蚀或美化,做一次全量更新,但这对于整个开发来说是非常不友好的行为,因为需要协调所有人,把所有的关卡文件都解锁。我觉得更实用的做法是做局部的更新,比如只更新刷过mask的区域。
局部更新mask
对于有些建筑区域,比如某处建了一栋房子,要求地基是平的,我们不想在生成地形的时候改变地表,很可能房子就悬空了。我们可以使用volume把区域框起来,跳过这个区域的侵蚀生成。
volume排除侵蚀区域
跟美术编辑的冲突问题如何解决?
有时候过程化生成的内容跟美术编辑的内容在同一个数据集里面,就会产生冲突问题。到底用谁的呢?Unreal提供了Edit Layer的功能,它跟Photoshop的图层一样,可以把生成的单独放一层,人工编辑的单独放一层,甚至还可以开更多的层,这样就可以混合人工编辑和我们生成的数据,得到想要的效果。但有的时候可能并不想要混合效果,而想要替换效果(比如说河流河道可以替换掉原有的地形)。我们又开发了一个Edit Layer的覆盖混合模式,用来支持这样的需求,同时也扩展了Unreal权重绘制的工具,用来绘制mask,支持这种覆盖混合和很多层的表现。
Edit Layer覆盖混合模式
还有一种冲突比较常见,比如说美术在某处放棵树,只是用来做装饰的,但是我们生成植被的时候很可能就把这棵树给生成没了,那这不是他们想看到的。所以我们针对生成的树都会打标记,每个instance上都会有一个特殊的属性,这样就能够区分出来到底哪些树是我们生成的,哪些树是美术编辑的。再重新生成的时候,就可以把原来生成的树全部删掉,做一次全量更新,但美术摆的树并不会被删掉,而且也可以选择避开他们。
很容易树没了
数据导入导出的问题如何处理?
我们沿用了Unreal Landscape Spline曲线生成,这些数据需要导出到Houdini当中进行运算,其实这也是为编辑的操作习惯而做的妥协,也避免了再开发全新的一套曲线编辑工具。
我们把Unreal Landscape Spline重新做了算法量化,导出成一个 JSON,然后在Houdini当中提取这些关键点做重建,这样就可以基于这条曲线去做算法实现。我们也加了各种各样的自定义属性,比如说曲线的类型,因为它会有不同的内容(路、护栏、河等),他们是属于不同曲线的。曲线的宽度还有优先级也都是通过这种方式添加到 JSON 文件里。
针对生成的内容。我们也做了一个简单的热力图扫描工具,可以选择任意一个Unreal当中的某个stat,比如针对面数去做统计,生成可视化的表现,方便美术在生成完或者关卡编辑完后去做性能自查。
热力图性能分析面板
一些试验性的探索
如果只是针对之前提的那几个功能的话,可能整个工作流是大同小异的,但是每个项目其实需求都不一样。比如说有的项目完全不用Houdini,生成算法全在Unreal里程序员自己实现。还有的项目可能因为不喜欢数据互相导来导去,选择整个的关卡布局是在Houdini当中进行制作,然后再导入Unreal,这个时候就不能改了。还有的像我们一样,频繁导入导出,需要很多数据交换,那我们需要程序员不断地处理这些特殊的需求。
所以我们在把这种工具推广到不同项目的时候,就需要重构整个管线。我们支持了蓝图节点编辑,让原来Houdini的HDA文件生成一个蓝图的异步节点,其所需要的数据准备是通过蓝图来进行的(包括数据输出的后处理),这样就具备了非常高的灵活性,能满足很多奇奇怪怪的需求。
可定制的生成流程
我们生成完,可以给它打个 tag 或者设置一些类似 virtual texture的属性,然后生成一个actor,甚至改变它的材质。在这套机制下,解放了程序员的生产力,不用他们专门开发特性。因为有的时候我发现不是这些功能实现不了,而是美术等不了,或者用户等不了,反复折腾的时候他们就不想用了。
另外,我们把数据的输入输出做了抽象,比如抽象成图,或者曲线,或是一些点,然后把这些数据做了抽象之后,甚至可以通过网络来传输这些数据,把生成的服务放到一台更强健的GPU工作站上。而且把数据做完抽象之后,也不限于只用Houdini来做,可以搭建一些自己的服务,用 Maya或Blender也都可以,因为数据的交换变得非常简单。
生成服务器
我们也做了些机器学习的一些尝试,比如说基于GAN算法的现实世界生成,或者是地表地形的风格化迁移等。还有一个很常见的问题是在用Houdini做这些工作的时候,数据的导入导出和计算都需要非常长的等待,有些团队会倾向于在引擎中自己实现生成算法,我们在这方面也做了一些简单的尝试,参考了《地平线:零之曙光》(Horizon Zero Dawn)的做法,使用GPU加速的方式做地表地形的生成和植被的分布。
过程化内容生成的技术除了应用在游戏开发中,还有其他应用场景。例如NExT跟新华社合作的数字航天员小诤的火星视频中,我们使用了过程化内容生成的技术模拟火星的地貌。UE5的Nanite可以在不限制三角形数量的情况下做大量的几何细节,这应该也是后续游戏制作的趋势之一。