【GDC翻译】在“Control”中学到的程序化破坏系统的经验

2021/7/25 22:36:47

本文主要是介绍【GDC翻译】在“Control”中学到的程序化破坏系统的经验,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

0. 介绍

在这里插入图片描述
大家好,欢迎来到《从“Control”中学到的程序化破坏系统的经验》。

我的名字是 Johannes Richter,我是 Remedy 的首席特效师。
在这里插入图片描述
我曾经在电影领域做过十多年的特效美术指导,在2019年进入了游戏领域。

对于 Remedy 其实不用过多介绍了,因为它已经很出名了。但对于那些不了解的人,我还是介绍一下吧。
在这里插入图片描述
我们在芬兰的Espoo。
二十多年来,Remedy一直以标志性的叙事驱动的动作游戏而闻名。比如《马克思佩恩》、《马克思佩恩2》、《量子破碎》,当然还有最新的《Control》。
在这里插入图片描述
《Control》是一个超自然的第三人称动作冒险游戏。
你扮演 Jesse Faden,联邦控制局的局长,在游戏的一开始你就变成了这个角色。
你接管了这个调查超自然现象的政府机构。

下面是我今天的议程:
在这里插入图片描述

  • 我首先要讨论下我们的挑战是什么。
  • 然后介绍一下我们制作特效的核心的规范是什么,粒度的准则是什么。
  • 我将会讨论我们的工作流和其实现。
  • 然后强调我们从中学到的教训,以及对未来我们可能会做得更好的事情的一点展望

首先,我们先看一段《Control》中破坏系统的片段:
【视频见 1:31】

1. 挑战

挑战-地点

我们面临的很多挑战都是关于地点的。
在这里插入图片描述
整个游戏是在政府机构的大楼里进行的,这是一个有点超自然的建筑,所以他们有很多会移动的墙壁。 所有东西都和原本的不太一样,这本身就是一个特点。

还有个关键的特点是野兽派风格,它很适合政府建筑。
在这里插入图片描述
另一个关键点是可信度,因为它是一个政府机构,有成千上万的特工,因此它必须感觉像是这样的一个地方。这里住着很多特工,他们日复一日地工作。他们有电话、咖啡机、复印机,在不同的部门都有各自的办公桌。通过场景去讲故事是非常重要的。
在这里插入图片描述
灯光期望有非常电影化的外观风格。《Control》支持整个RTX光线追踪,所以灯光也必须做得电影化以迎合它。
在这里插入图片描述
“野兽派风格”意味着大量暴露的材质,会有很多混凝土板、木制品、玻璃等等。很适合这个政府建筑。
在这里插入图片描述

挑战-触觉

当我们讨论“破坏”的时候,我们会讨论 “触觉”
在这里插入图片描述
我们想要一个有丰富反应度的环境,传达出一种“你可以与里面任何东西持续交互”的感觉。

挑战-限制

很明显,限制是我们的另一个挑战。我们有必须要满足的目标平台。

在这里插入图片描述
我们追求的是真实值得的物理反应。
我们会说“去疯狂地破坏吧!但是恳请不要太过分”。因为很明显我们有内存方面的限制,还有AI的需求,而且我们还是个很小的团队。

2. 粒度的准则

我们还创造了一个非常重要的准则:粒度的准则(Principe of Granularity)
在这里插入图片描述
它决定了每个特效将代表自然中哪一层级的细节。这不仅仅是在制作《Control》所要考虑的事情,它是在制作任何特效的时候都要考虑的普适性准则,电影中也一样。制作特效时你其实是在模拟自然,然而自然不是“量化的”(或者说“离散的”),自然是连续的——从很大的物体到非常小的烟雾颗粒等等。因此需要封装:将跨越了丰富而不同的规模级别的自然,封装到不同的特效中。

下面看下我们在游戏引擎中如何做到这一点。
在这里插入图片描述
我们现在看的是三个不同的层,其所涵盖的内容有些重叠。

  • 第一层使用刚体模拟。这一层有碎片、道具的部件、环境中的道具。他们都是刚体。“environment” 代表了环境中大型的静态模型,你可以与之碰撞的。而 “object” 代表了桌子椅子等。
  • 接下来你会想用其他的东西来表示,因为东西越小则屏幕上的数目就会越多。所以你会想用 Mesh粒子rigid body hierarchies材质贴花也在一定程度上增添了丰富性。所以我们现在从 “object” 转向了 “chunks”,甚至是 “debris”。
  • 最后一层是纯粹的粒子,包括面片粒子Mesh粒子。代表了余烬、火花、烟雾、火、碎片、砾石、沙子或类似的东西。他们填充了自然中粒度最细的一个级别。

在《Control》中,我选择了一个视角来演示不同规模级别的内容。
在这里插入图片描述
现在你看到的是静态环境,其中所有的东西都是不能移动的。

接下来的一层——
在这里插入图片描述
这一层在这个视角下不太明显,不过你可以看到例如围栏被添加了。这一层是环境中实际上你可以与之交互的部分。

当你把所有物件都加入后,场景将会有很大的不同:
在这里插入图片描述
这一部分很丰富。游戏中一半的内容都被物件等可被放置的道具所填充,你可以与之交互和移动它们。这里要感谢我们的环境团队,他们做了了不起的工作,通过使用所有这些元素来提高这个地方的丰富性。

3. 工作流以及实现

工作流总览

我们的工作流很容易想象的到,它一个相对标准的流程。
在这里插入图片描述

  • 首先是“环境构建”。环境美术会提供关卡几何体、模块化套件、物件。
  • 随后它们被送往特效部门以设置破坏。我们有很多系统的设置、Prop Rigs、和一些特定场合的过场动画。
  • 最后是我们的内部引擎Northlight,一切在那里运行。

程序化

然而,我们还需要选择一种具体的方式来实现它。我们选择了程序化的方式。
在这里插入图片描述
很多人都在讨论“程序化”,那么它到底是什么意思呢?它是一种基于规则的对真实数据的处理和解释。也就是说:你有一些数据和一些规则,你应用这些规则然后改变数据,这就是关键。

对于我们来说,它是这个样子的:

  • 我们场景中的美术模型就是我们的数据。而这些数据实际上被修改或增加了一些额外的东西,额外的材质元数据基本上告诉我们所有东西是由什么组成的。所以我们就知道 “嘿,那个垫子是纺织物,那个是混凝土,那个是植物”。
  • 一旦你知道了一个对象是由什么组成,你就可以开始对它应用规则了。比如对于“草”,射它的时候就会生成一些叶片;混凝土就会碎成更小的混凝土块,并生成一些灰尘;射击金属管会从洞里流出水,再添加一个贴花来表示一小点变形。你有一套有限的规则来概括整个过程,并确保对世界上可能发生的一切事都有反应。
  • 然后,他们被送往引擎。引擎将基于材质元数据来生成打击的特效、破坏的特效、刚体行为。这创建了一个可交互的环境。

那么为什么要选择“程序化”呢?毕竟这些设置看起来有些麻烦。
在这里插入图片描述
但问题是:

  • 我们需要一个持续而快速的转变。
  • 我们需要行为是可预测的,我们被定义了指标,我们需要知道有多少东西可以破坏等等。
  • 我们有数百个资源和变种。在上图中你可以看到建筑组件,他们构成了房间、墙壁、柱子、混凝土栏杆等等。下图有一些道具比如桌子、厕所隔间、墙壁、隔板、植物、电脑、电话和其他东西。
  • 有太多东西了,但我们只有一个小团队,平均只能看到1到3人在一起工作。

因此我们需要一种方法,可以将特定东西放进管线,然后管线就以我们预先定义好的方式对其进行加工。我们是没办法对物件一个一个地加工的,因为那是不可能的,也没有收缩性。

行为由材质驱动

接下来,看下具体来说材质是如何驱动破坏行为的:
在这里插入图片描述
最左边是混凝土,当你朝混凝土射击时候就会发生混凝土特定的行为。中间是木头,当你射击木头的时候,它会碎成小片,或是这种尖锐的木刺。右边是玻璃。这里展示了三种不同的材料,所以你可以看到动态物体的交互行为是基于材质的。

然后你还可以看到细节的粒子,他们也是基于材质的,我将会在随后更详细地讨论它。

几何层

下面,首先我们讨论下“几何层”——
在这里插入图片描述

我们现在看到的是一根栏杆,底部是混凝土做的,中间是金属做的,顶部是木头做的。从左到右你可以看到这个东西逐渐被破坏的阶段。

  • 在A阶段中有一些破坏,混凝土上没有贴花,因为你不想在打碎一个物体时看到所有的裂缝。你可以看到木条有一些长长的木刺,金属也扭曲了。
  • 在B阶段中已经没有金属了,因为金属不会再破坏了,然而混凝土和木头会继续破坏。
  • 最后是C阶段,它是可选的,取决于其材质是否可能会继续破坏。

现在这里可能看起来有些滑稽,因为他们看起来超级普通,你会说“哦,这不就是Voronoi吗”。但你要想,你并不是每时每刻都在打碎物体的所有部分,你实际上是在打碎它的某个角落,在那里它会碎成碎片。

模拟实体

现在,我们有了不同的模拟实体
在这里插入图片描述

  • 刚体,你知道的,就是可以在世界中晃来晃去的物理体。
  • 我们还有个系统叫做 “碎片(chunk)”,碎片会有一个“范围(bounds)”。实际上是指一些刚体会共享一个复合的碰撞。有些情况下会称为 “composite” ,有时会称为 “aggregate”。
  • 我们也像往常一样使用“关节”。

正如刚才描述的,“chunk”是一种复合物。
在这里插入图片描述
他们在初始化时就被创建。可以看做二者是共享同一个碰撞体,一个会跟着另一个移动,直到断裂。
通常临近的叶节点会被绑定到一起。

而对于“关节”——
在这里插入图片描述
他们是基于元数据的描述和“几何层级(Geometry hierarchy)”来创建的。比如,这个刚体和那个刚体是被旋转关节或者是铰链所连接,可以是门、抽屉等等任何游戏场景中的一部分。
他们会由于冲力而被动态地破坏。

而关于关节的破坏,有一些特别之处值得说明:
在这里插入图片描述
本质上,当你为两个刚体创建关节的时候,关节所连接的实际上是内部更低一级别的“碎片(chunk)”。这意味着,你可以将门破坏留下一个洞,但是门还是会连接在铰链上。也就是说一个父物体比如图中的“RB1”被破坏成碎片,并不会影响其中碎片的关节(只要碎片还在关节上)。

这实际上让我们的破坏系统更具有丰富性,因为你可以在门上打一个洞,而门仍旧可以正常打开和关闭。不像某些游戏,其中的东西稍微碰一下就整体破碎成碎片。在我们的游戏中是可以保持结构的,而上述就是其实现的原理。

关于模拟——
在这里插入图片描述
模拟基本上就是在Northlight引擎中计算的。软件会处理数据与层级、破坏逻辑,在周围出现什么事件和粒子等等这些事情。而内部会由PhysX来模拟刚体和约束。

工具链

为了创建这些内容,我们有一个破坏系统的工具链,你也许会觉得它很普通:
在这里插入图片描述

  • 首先我们有场景或道具的几何体。
  • 接下来我们需要为模型做点准备。在某些情况下我们会创建一些包围盒,来说明“嘿,这些部分可以破坏,这些部分不能破坏”。
  • 然后就进入Houdini里的破坏工具,里面有大量的HDA来处理完整的基于材质的破坏。之后数据就会被存到磁盘上。
  • 有时,你不得不手动修补一些物理元数据来确保一些设置是正确的,特别是当你在配置复杂的东西时,有些值是不得不手动设置的。
  • 最后他们进入引擎,这些元数据和mesh数据被引擎用来创建和模拟世界。

破坏工具看起来像这样:
在这里插入图片描述
这个混凝土块就是输入几何体,我们会定义哪些区域会被破坏,这里就是两侧机翼形状的东西。然后,工具会看到它被标记为“混凝土”,因此它会以混凝土的方式来破坏它。之后就会创建出有层级结构的用于渲染和碰撞的几何体,并确保是我们想要的指标与风格。它还会添加所有的细节,例如对于混凝土就是钢筋。对于木头、玻璃、和其他所支持的材质,也是同样的方式。最终,他们被导入到引擎。

进入引擎后看起来像这样:
在这里插入图片描述
你会有节点的层级,里面有名字、材质、是否是静态的、还有一些关节和关节的种类等等。层级的深度代表了破坏的阶段。物理属性由命名的约定和分配的材质来驱动。最终引擎将驱动它们(仅当配置命名真的正确时,而这部分待会儿再谈)。

这是仅包括刚体的视频:
在这里插入图片描述
基本上把所有东西都破坏掉了,你可能会觉得有些空洞,但刚体模拟实际上就是这些了。

优化

接下来谈谈优化:
在这里插入图片描述

  • 在任何时间点上,我们的屏幕上最多有200个刚体。这是我们的预算,因为我们要让它运行在主机上。这意味我们可能会以某种方式让屏幕之外的刚体消失。
  • 在一些大型碰撞事件比如爆炸中,我们会在开始的几帧中禁止“自我碰撞”来确保你不会对快速移动的物体进行过于疯狂的碰撞计算。你并不需要它,因为很明显他们会在爆炸中分开,你不会对这些碰撞感兴趣的。这就是为什么要用“碰撞延迟”,我是在电影行业知道这种方法的,我们在其他问题上也做了同样的事情,比如它在“自我相交”问题上也有所帮助。
  • 我们使用了“休眠阙值”。当混凝土碎块掉到地上时,它们将很快“休眠”,没人会想让它们到处乱蹦的,那也很滑稽。越是靠近真实,你就会越能相信这一点。当你把什么东西掉到地板上时,它真的不会有很多反弹。也只有弹珠会多蹦几下。所以我们有一个很高的“休眠阙值”,这也是为什么我们能让物体固定在一起。
  • 然后,当然要用很多粒子来填补空白,这就引出了下一个部分。

粒子层

在这里插入图片描述
这是我们的粒子模拟,所有都是由系统和事件驱动的。

  • 当子弹撞击时,材质会决定播放什么样的粒子特效。
  • 当chunk或刚体断开连接时,会有一个 “bond breaking” 事件,那里会有粒子释放。这很棒,可以用来做开裂的混凝土、鹅卵石、或是其他东西。
  • 当我们想让对象消失时,我们不是把那部分分离弹开,而是将他们分解成粒子让它们消失,粒子会由材质来决定。如果你在屏幕上看到它们,你会觉得它们好像在分裂,这种方法非常高效地隐藏了物体,是一种优化。

下面可以简单看下我们是如何编辑粒子的:
在这里插入图片描述
它们全部都是通过可热加载的实时编辑器进行的。在游戏运行中,你可以设置一个特定的粒子特效,然后你可以进入并改变一些东西。比如也许火花太多了,我就可以修改火花的发射频率;再比如,我可以让模拟中有更多的石头。最酷的是你可以重新播放,这样你就能得到即时的反馈,然后你就能回去并继续迭代。这种快速的迭代方式让我们能够不断完善效果,直到我们真正感觉效果是正确的。

关于粒子还有另一个特别的部分——
在这里插入图片描述
我们的粒子模拟是标准的。但是我们也抓取了渲染的 SDF(Signed Distance Field,有向距离场),所以你可以选择与SDF碰撞,这比和其他东西碰撞要快得多。这解决了很多粒子碰撞的问题,既不会让你的粒子穿过地板,也不会看起来很奇怪,因此我们使用了它。

接下来的片段就添加了粒子:
在这里插入图片描述
这是同一个地方,我还是在尝试破坏所有东西,但你马上就会注意到:“哦天哪,这感觉更灵敏更有趣”。因为发生了很多新事情:空气中的灰尘、火花。它们填补了自然层级中更细微的部分,让感觉更加丰富。

材质贴花

最后一部分是材质贴花,
在这里插入图片描述
我们会动态地基于材质生成很多材质贴花。当有东西被破坏时就可能会产生像碎片一样的贴花。当你对地面猛敲时,就会有碎裂的贴花,它们是由Houdini或Substance之类的东西做的。
这也为静态物体带来了一些动感。在开始你也看到了我们有很多物体都是静态的,然而当你提供了一些冲击力时,就会变得不同了,当你打击地板时,它仍旧还是个简单的四边形面片,但是当加了贴花后看起来真得很不一样:
在这里插入图片描述
总的来说,贴花很棒,很多效果都可以令人震惊地由贴花实现。尽管你在纹理上可能没有太多预算,但是即使只是使用一些,也会增添一些破坏的感觉。比如这里往墙上扔东西,然后你就会在那里留下一个凹痕,虽然你知道混凝土不会真的留下凹痕,但在这里会让你感觉不错,毕竟我们也是个超能力题材的游戏。

现在,所有的东西都在一起了,我们有了刚体、粒子、贴花:
在这里插入图片描述
我想我拿了一把椅子扔了它,地板上就出现了贴花。虽然地板仍旧是静态的几何体,但是贴花让它看起来更好了。

自定义的道具和危险

在这里插入图片描述
另一部分是自定义的道具和危险,它们有很多。例如你可以扔的电脑和灯之类的东西,它们的破坏不是完全由程序化生成的。它们需要一些自定义的事件,和艺术家自定义创建的内容来增添丰富感。比如灭火器、还有电脑的火花与连接线等等。

4. 经验

关于我学到的经验,主要有以下四个方面:
在这里插入图片描述

  • 输入数据的几何体质量。因为程序化是由数据驱动的,所以这就是一切。
  • 基于命名的约定
  • 庞大的破坏工具
  • 性能检测

输入几何体质量

在这里插入图片描述
这是我在电影与游戏整个职业生涯中所一直在处理的问题。输入的几何体不规范包括:

  • 以错误的方式缩放、朝向错误。
  • 材质指定错误。有时它看起来是木头,却被标记为“植物”或是“肉类”。
  • 有时模型质量太低,这可能意味着细分不好、单面等等。有时你打破了一些东西,但里面什么都没有,但实际上是应该有的。

这是沟通中比较难的一部分,毕竟我们是使用程序化的方式来破坏东西的。

所以你需要做些事情来改进输入数据:

  • 对不同美术工具的模型制作管线进行标准化。
  • 确保在导出时一切都进行了完备的检查。这样在导出时,工具就会说 “嘿,这不符合它需要满足的标准,请在导出前修复它”。这是很有帮助的,避免了不同部门之间持续的反馈循环。这将把问题放在真正引起问题的地方,同时也有助于解决问题。
  • 还有,如果有内嵌的工具来简化设置会非常棒。因为你可以让建模的人能够立即看到如果它被破坏了会是什么样子。但很明显,这带来了很大的开发工具的压力,特别是你还希望有用户友好的界面,这其中有很大的工作量。因此你需要做权衡。

命名约定

在这里插入图片描述
基于命名的约定:我们给事物命名,然后解释成特定的东西。但问题是:

  • 名字可能是错的,例如“混凝土”会有17种不同的拼法。这不能责怪谁,因为只要是需要手动做的工作,都有一定比例的数据是不正确的,就是这样。
  • 你也不能仅使用名字来表达很多范围的东西,尤其是物理定义。
  • Mesh的大纲层级不是标准的形式。you know what happens if you do things in Maya, you shuffle things around and do all kinds of operations on it, and then your hierarchy just goes bust.(译注:这里大概指的是在Maya中可能会导致大纲层级毁掉,但译者Maya用得不多,不太确定精确的意思)

所以,我只能说:永远不要使用基于名字的约定!彻底舍弃掉它,直接在DCC中创建物理数据,例如让人在其中需要标记的地方进行标记。为此,你可能还需要统一的元数据API。所以无论美术们使用什么工具来创造他们的模型,他们总能够提供这种类型的数据,而不需要翻译。

庞大的破坏工具

在这里插入图片描述
“庞大的破坏工具”,听起来这个话题只和Houdini相关。但一般来说,你不希望有一个工具来做所有的事情。很常见的情况是,你从一件小事开始,然后你开始添加一些东西,然后再添加一些,你添加得越多,它就变得越强大,但维护起来也就越慢、越困难、越不灵活。

对此的解决方法是粒度更细的工具,但这也会带来挑战。我们会想用很多独立的、可互换的工具,但问题是你必须以某种方式维护它们,你需要确保你的版本是正确的,你要确保即使进入一个项目两年,你也可以从项目一开始就打开一些东西,尽管工具可能已经改变了20次但你仍然能够使用它。

对于我们来说,这意味改进标准化的HDA管理,确保所有东西都在命名空间下,所以东西都为美术们分布,所以你不会丢失工具的任何版本。

另外重要的一点是,对于工具,你可以完全自动化地进行,但也应可以手动地调整,二者不应该有区别。这会更灵活,比如说 “嘿,这是自动化生成的东西,但是我进去调整了一些东西”,你在10%的情况下需要这么做,只要他们有相同的结果就行,这很重要。

性能与测试

性能与测试几乎是最大的教训。
在这里插入图片描述
我们没有自动测试机制。因此就要在关卡中添加一些东西,绕着他们跑,然后朝他们射击,看看他们是否在工作。看起来很不错,但是当引擎发生变化后,一些东西可能被优化了,突然之间你就得重新测试。这是很危险的,因为你总是会忘记要测试的东西。所以随着时间的推移,问题会有潜在的可能性回归。这是需要改进的地方。

第二部分是性能测试。我们没有任何具体的度量标准。你不能简单地使用FPS,因为你不会在每一帧都持续地做相同的事情。你可以对特定的情况进行优化,但它并没有真正覆盖所有的事情。

通常情况下,性能问题只在它们被发现时才会爆发。比如关于物体破坏,有一些道具放在关卡中很棒,于是你就放了很多道具在关卡比如桌子、电脑,然后在游戏玩法中有一群敌人会扔手榴弹,然后就会变得一团糟了。然而你也不想因为这个特定的情景而降低所有东西的质量,所以你需要找到一种方法来处理这个问题,尤其是在项目进行到很晚的时候你才会了解到全部情况。

可以做两件事情来改善这方面:
在这里插入图片描述
第一件事是更好的性能指标。就算他们只是些脱离系统的“魔法数字”,但是给你这些数字会让你明白:这些值越高就越糟糕,越低就越好。这样你就可以根据这些数字来优化了。你可以用这些数字来建立正确的边界,像是物理预算。因为有200个刚体并不意味着就一定不行,仅当同时还有203个手榴弹时才不行。这才是需要正确平衡的东西。如果你有了指标,你就可以把边界再扩大一点。

第二件事是自动测试。在测试环境中对资源进行自动地评估,然后就可以得到差异并看到其对引擎的影响了。

另一方面是针对性能问题的应对措施:
在这里插入图片描述
也可以考虑在运行时做些事情来优化物理性能。显然,你不能改变物体的破坏层级,但是你可以说 “嘿,也许这个家伙可以从当前的阶段直接进入到最后分解为粒子并消失的阶段,而不是另一个阶段,因为天啊,现在太混乱了到处都是手榴弹”。另外,你最好让指标比PhysX所要求的指标更好些,因为当前很难从PhysX中读取这类信息。

第二部分是基于预期的加载来划分出一些区域。其想法是:“嘿,也许你一开始并不知道,但是现在在一个区域内有大量可破坏的物体,还有一群家伙拿着手榴弹,我们该怎么办?”,然后你就可以在这里放一个盒子,说:“嘿,在这里物理负荷太重了,也许你应该忽略掉最后一级别的破坏或类似的东西”。这将在某种程度上帮助我们手动划分某些我们知道的东西,避免因为特定区域而降低整个游戏的所有资产的质量。

总结

最后,来做个总结。
在这里插入图片描述
《Control》的反响很好,大家很喜欢它,特别是将一个物体朝着一大片桌子和敌人扔过去,然后一切都变成碎片的感觉。我们很高兴实现了这一点。工作流也比预期的要好,毕竟我们的团队很小。我们对此非常高兴,也很激动接下来可以将这个技术带到哪里。



这篇关于【GDC翻译】在“Control”中学到的程序化破坏系统的经验的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程