大家好,欢迎来到《从“Control”中学到的程序化破坏系统的经验》。
我的名字是 Johannes Richter,我是 Remedy 的首席特效师。
我曾经在电影领域做过十多年的特效美术指导,在2019年进入了游戏领域。
对于 Remedy 其实不用过多介绍了,因为它已经很出名了。但对于那些不了解的人,我还是介绍一下吧。
我们在芬兰的Espoo。
二十多年来,Remedy一直以标志性的叙事驱动的动作游戏而闻名。比如《马克思佩恩》、《马克思佩恩2》、《量子破碎》,当然还有最新的《Control》。
《Control》是一个超自然的第三人称动作冒险游戏。
你扮演 Jesse Faden,联邦控制局的局长,在游戏的一开始你就变成了这个角色。
你接管了这个调查超自然现象的政府机构。
下面是我今天的议程:
首先,我们先看一段《Control》中破坏系统的片段:
【视频见 1:31】
我们面临的很多挑战都是关于地点的。
整个游戏是在政府机构的大楼里进行的,这是一个有点超自然的建筑,所以他们有很多会移动的墙壁。 所有东西都和原本的不太一样,这本身就是一个特点。
还有个关键的特点是野兽派风格,它很适合政府建筑。
另一个关键点是可信度,因为它是一个政府机构,有成千上万的特工,因此它必须感觉像是这样的一个地方。这里住着很多特工,他们日复一日地工作。他们有电话、咖啡机、复印机,在不同的部门都有各自的办公桌。通过场景去讲故事是非常重要的。
灯光期望有非常电影化的外观风格。《Control》支持整个RTX光线追踪,所以灯光也必须做得电影化以迎合它。
“野兽派风格”意味着大量暴露的材质,会有很多混凝土板、木制品、玻璃等等。很适合这个政府建筑。
当我们讨论“破坏”的时候,我们会讨论 “触觉”。
我们想要一个有丰富反应度的环境,传达出一种“你可以与里面任何东西持续交互”的感觉。
很明显,限制是我们的另一个挑战。我们有必须要满足的目标平台。
我们追求的是真实但值得的物理反应。
我们会说“去疯狂地破坏吧!但是恳请不要太过分”。因为很明显我们有内存方面的限制,还有AI的需求,而且我们还是个很小的团队。
我们还创造了一个非常重要的准则:粒度的准则(Principe of Granularity)。
它决定了每个特效将代表自然中哪一层级的细节。这不仅仅是在制作《Control》所要考虑的事情,它是在制作任何特效的时候都要考虑的普适性准则,电影中也一样。制作特效时你其实是在模拟自然,然而自然不是“量化的”(或者说“离散的”),自然是连续的——从很大的物体到非常小的烟雾颗粒等等。因此需要封装:将跨越了丰富而不同的规模级别的自然,封装到不同的特效中。
下面看下我们在游戏引擎中如何做到这一点。
我们现在看的是三个不同的层,其所涵盖的内容有些重叠。
在《Control》中,我选择了一个视角来演示不同规模级别的内容。
现在你看到的是静态环境,其中所有的东西都是不能移动的。
接下来的一层——
这一层在这个视角下不太明显,不过你可以看到例如围栏被添加了。这一层是环境中实际上你可以与之交互的部分。
当你把所有物件都加入后,场景将会有很大的不同:
这一部分很丰富。游戏中一半的内容都被物件等可被放置的道具所填充,你可以与之交互和移动它们。这里要感谢我们的环境团队,他们做了了不起的工作,通过使用所有这些元素来提高这个地方的丰富性。
我们的工作流很容易想象的到,它一个相对标准的流程。
然而,我们还需要选择一种具体的方式来实现它。我们选择了程序化的方式。
很多人都在讨论“程序化”,那么它到底是什么意思呢?它是一种基于规则的对真实数据的处理和解释。也就是说:你有一些数据和一些规则,你应用这些规则然后改变数据,这就是关键。
对于我们来说,它是这个样子的:
那么为什么要选择“程序化”呢?毕竟这些设置看起来有些麻烦。
但问题是:
因此我们需要一种方法,可以将特定东西放进管线,然后管线就以我们预先定义好的方式对其进行加工。我们是没办法对物件一个一个地加工的,因为那是不可能的,也没有收缩性。
接下来,看下具体来说材质是如何驱动破坏行为的:
最左边是混凝土,当你朝混凝土射击时候就会发生混凝土特定的行为。中间是木头,当你射击木头的时候,它会碎成小片,或是这种尖锐的木刺。右边是玻璃。这里展示了三种不同的材料,所以你可以看到动态物体的交互行为是基于材质的。
然后你还可以看到细节的粒子,他们也是基于材质的,我将会在随后更详细地讨论它。
下面,首先我们讨论下“几何层”——
我们现在看到的是一根栏杆,底部是混凝土做的,中间是金属做的,顶部是木头做的。从左到右你可以看到这个东西逐渐被破坏的阶段。
现在这里可能看起来有些滑稽,因为他们看起来超级普通,你会说“哦,这不就是Voronoi吗”。但你要想,你并不是每时每刻都在打碎物体的所有部分,你实际上是在打碎它的某个角落,在那里它会碎成碎片。
现在,我们有了不同的模拟实体:
正如刚才描述的,“chunk”是一种复合物。
他们在初始化时就被创建。可以看做二者是共享同一个碰撞体,一个会跟着另一个移动,直到断裂。
通常临近的叶节点会被绑定到一起。
而对于“关节”——
他们是基于元数据的描述和“几何层级(Geometry hierarchy)”来创建的。比如,这个刚体和那个刚体是被旋转关节或者是铰链所连接,可以是门、抽屉等等任何游戏场景中的一部分。
他们会由于冲力而被动态地破坏。
而关于关节的破坏,有一些特别之处值得说明:
本质上,当你为两个刚体创建关节的时候,关节所连接的实际上是内部更低一级别的“碎片(chunk)”。这意味着,你可以将门破坏留下一个洞,但是门还是会连接在铰链上。也就是说一个父物体比如图中的“RB1”被破坏成碎片,并不会影响其中碎片的关节(只要碎片还在关节上)。
这实际上让我们的破坏系统更具有丰富性,因为你可以在门上打一个洞,而门仍旧可以正常打开和关闭。不像某些游戏,其中的东西稍微碰一下就整体破碎成碎片。在我们的游戏中是可以保持结构的,而上述就是其实现的原理。
关于模拟——
模拟基本上就是在Northlight引擎中计算的。软件会处理数据与层级、破坏逻辑,在周围出现什么事件和粒子等等这些事情。而内部会由PhysX来模拟刚体和约束。
为了创建这些内容,我们有一个破坏系统的工具链,你也许会觉得它很普通:
破坏工具看起来像这样:
这个混凝土块就是输入几何体,我们会定义哪些区域会被破坏,这里就是两侧机翼形状的东西。然后,工具会看到它被标记为“混凝土”,因此它会以混凝土的方式来破坏它。之后就会创建出有层级结构的用于渲染和碰撞的几何体,并确保是我们想要的指标与风格。它还会添加所有的细节,例如对于混凝土就是钢筋。对于木头、玻璃、和其他所支持的材质,也是同样的方式。最终,他们被导入到引擎。
进入引擎后看起来像这样:
你会有节点的层级,里面有名字、材质、是否是静态的、还有一些关节和关节的种类等等。层级的深度代表了破坏的阶段。物理属性由命名的约定和分配的材质来驱动。最终引擎将驱动它们(仅当配置命名真的正确时,而这部分待会儿再谈)。
这是仅包括刚体的视频:
基本上把所有东西都破坏掉了,你可能会觉得有些空洞,但刚体模拟实际上就是这些了。
接下来谈谈优化:
这是我们的粒子模拟,所有都是由系统和事件驱动的。
下面可以简单看下我们是如何编辑粒子的:
它们全部都是通过可热加载的实时编辑器进行的。在游戏运行中,你可以设置一个特定的粒子特效,然后你可以进入并改变一些东西。比如也许火花太多了,我就可以修改火花的发射频率;再比如,我可以让模拟中有更多的石头。最酷的是你可以重新播放,这样你就能得到即时的反馈,然后你就能回去并继续迭代。这种快速的迭代方式让我们能够不断完善效果,直到我们真正感觉效果是正确的。
关于粒子还有另一个特别的部分——
我们的粒子模拟是标准的。但是我们也抓取了渲染的 SDF(Signed Distance Field,有向距离场),所以你可以选择与SDF碰撞,这比和其他东西碰撞要快得多。这解决了很多粒子碰撞的问题,既不会让你的粒子穿过地板,也不会看起来很奇怪,因此我们使用了它。
接下来的片段就添加了粒子:
这是同一个地方,我还是在尝试破坏所有东西,但你马上就会注意到:“哦天哪,这感觉更灵敏更有趣”。因为发生了很多新事情:空气中的灰尘、火花。它们填补了自然层级中更细微的部分,让感觉更加丰富。
最后一部分是材质贴花,
我们会动态地基于材质生成很多材质贴花。当有东西被破坏时就可能会产生像碎片一样的贴花。当你对地面猛敲时,就会有碎裂的贴花,它们是由Houdini或Substance之类的东西做的。
这也为静态物体带来了一些动感。在开始你也看到了我们有很多物体都是静态的,然而当你提供了一些冲击力时,就会变得不同了,当你打击地板时,它仍旧还是个简单的四边形面片,但是当加了贴花后看起来真得很不一样:
总的来说,贴花很棒,很多效果都可以令人震惊地由贴花实现。尽管你在纹理上可能没有太多预算,但是即使只是使用一些,也会增添一些破坏的感觉。比如这里往墙上扔东西,然后你就会在那里留下一个凹痕,虽然你知道混凝土不会真的留下凹痕,但在这里会让你感觉不错,毕竟我们也是个超能力题材的游戏。
现在,所有的东西都在一起了,我们有了刚体、粒子、贴花:
我想我拿了一把椅子扔了它,地板上就出现了贴花。虽然地板仍旧是静态的几何体,但是贴花让它看起来更好了。
另一部分是自定义的道具和危险,它们有很多。例如你可以扔的电脑和灯之类的东西,它们的破坏不是完全由程序化生成的。它们需要一些自定义的事件,和艺术家自定义创建的内容来增添丰富感。比如灭火器、还有电脑的火花与连接线等等。
关于我学到的经验,主要有以下四个方面:
这是我在电影与游戏整个职业生涯中所一直在处理的问题。输入的几何体不规范包括:
这是沟通中比较难的一部分,毕竟我们是使用程序化的方式来破坏东西的。
所以你需要做些事情来改进输入数据:
基于命名的约定:我们给事物命名,然后解释成特定的东西。但问题是:
所以,我只能说:永远不要使用基于名字的约定!彻底舍弃掉它,直接在DCC中创建物理数据,例如让人在其中需要标记的地方进行标记。为此,你可能还需要统一的元数据API。所以无论美术们使用什么工具来创造他们的模型,他们总能够提供这种类型的数据,而不需要翻译。
“庞大的破坏工具”,听起来这个话题只和Houdini相关。但一般来说,你不希望有一个工具来做所有的事情。很常见的情况是,你从一件小事开始,然后你开始添加一些东西,然后再添加一些,你添加得越多,它就变得越强大,但维护起来也就越慢、越困难、越不灵活。
对此的解决方法是粒度更细的工具,但这也会带来挑战。我们会想用很多独立的、可互换的工具,但问题是你必须以某种方式维护它们,你需要确保你的版本是正确的,你要确保即使进入一个项目两年,你也可以从项目一开始就打开一些东西,尽管工具可能已经改变了20次但你仍然能够使用它。
对于我们来说,这意味改进标准化的HDA管理,确保所有东西都在命名空间下,所以东西都为美术们分布,所以你不会丢失工具的任何版本。
另外重要的一点是,对于工具,你可以完全自动化地进行,但也应可以手动地调整,二者不应该有区别。这会更灵活,比如说 “嘿,这是自动化生成的东西,但是我进去调整了一些东西”,你在10%的情况下需要这么做,只要他们有相同的结果就行,这很重要。
性能与测试几乎是最大的教训。
我们没有自动测试机制。因此就要在关卡中添加一些东西,绕着他们跑,然后朝他们射击,看看他们是否在工作。看起来很不错,但是当引擎发生变化后,一些东西可能被优化了,突然之间你就得重新测试。这是很危险的,因为你总是会忘记要测试的东西。所以随着时间的推移,问题会有潜在的可能性回归。这是需要改进的地方。
第二部分是性能测试。我们没有任何具体的度量标准。你不能简单地使用FPS,因为你不会在每一帧都持续地做相同的事情。你可以对特定的情况进行优化,但它并没有真正覆盖所有的事情。
通常情况下,性能问题只在它们被发现时才会爆发。比如关于物体破坏,有一些道具放在关卡中很棒,于是你就放了很多道具在关卡比如桌子、电脑,然后在游戏玩法中有一群敌人会扔手榴弹,然后就会变得一团糟了。然而你也不想因为这个特定的情景而降低所有东西的质量,所以你需要找到一种方法来处理这个问题,尤其是在项目进行到很晚的时候你才会了解到全部情况。
可以做两件事情来改善这方面:
第一件事是更好的性能指标。就算他们只是些脱离系统的“魔法数字”,但是给你这些数字会让你明白:这些值越高就越糟糕,越低就越好。这样你就可以根据这些数字来优化了。你可以用这些数字来建立正确的边界,像是物理预算。因为有200个刚体并不意味着就一定不行,仅当同时还有203个手榴弹时才不行。这才是需要正确平衡的东西。如果你有了指标,你就可以把边界再扩大一点。
第二件事是自动测试。在测试环境中对资源进行自动地评估,然后就可以得到差异并看到其对引擎的影响了。
另一方面是针对性能问题的应对措施:
也可以考虑在运行时做些事情来优化物理性能。显然,你不能改变物体的破坏层级,但是你可以说 “嘿,也许这个家伙可以从当前的阶段直接进入到最后分解为粒子并消失的阶段,而不是另一个阶段,因为天啊,现在太混乱了到处都是手榴弹”。另外,你最好让指标比PhysX所要求的指标更好些,因为当前很难从PhysX中读取这类信息。
第二部分是基于预期的加载来划分出一些区域。其想法是:“嘿,也许你一开始并不知道,但是现在在一个区域内有大量可破坏的物体,还有一群家伙拿着手榴弹,我们该怎么办?”,然后你就可以在这里放一个盒子,说:“嘿,在这里物理负荷太重了,也许你应该忽略掉最后一级别的破坏或类似的东西”。这将在某种程度上帮助我们手动划分某些我们知道的东西,避免因为特定区域而降低整个游戏的所有资产的质量。
最后,来做个总结。
《Control》的反响很好,大家很喜欢它,特别是将一个物体朝着一大片桌子和敌人扔过去,然后一切都变成碎片的感觉。我们很高兴实现了这一点。工作流也比预期的要好,毕竟我们的团队很小。我们对此非常高兴,也很激动接下来可以将这个技术带到哪里。