《叠纸游戏》的资深物理算法工程师赵英杰先生在本届 Unite 大会上给大家详细介绍了游戏中高性能物理框架的实践,其中包括布料、碰撞检测等核心功能,以及如何将这些技术在玩法中优化应用。
![]()
赵英杰:大家好,接下来由我给大家带来《恋与深空》物理效果开发的相关内容分享。我叫赵英杰,来自叠纸游戏,曾参与过《闪耀暖暖》《恋与深空》等项目,目前在《恋与深空》制作组担任引擎程序,主要负责物理和动画相关内容的开发。本次分享主要分为四个部分:布料模拟实现、实时表演控制、基于 Unity DOTS 的开发和碰撞检测模块。
布料模拟实现
布料模拟实现在整个《恋与深空》的表现当中占了相当大的一块部分。比如说我们的剧情表演、战斗一些活动以及玩法都大量使用了布料的。我们使用的布料系统是自己开发的一套基于骨骼的布料模拟系统,内部名称的叫做 StrayCloth。采用的模拟方法是 XPBD 结合 SubStep 的方式。相比 PBD,XPBD 的优点是摆脱了迭代次数和时间步长的依赖,结合 SubStep 可以显著提升解算的收敛效果。比较特殊的地方在于,我们使用骨骼作为模拟粒子,也就说每个粒子除了位置以外还带旋转信息。在具体的 SubStep 实现中,我们针对不同性能压力场景采用动态的子步幅时间,在 1/200 -1/300 之间。并且对场景中的运动对象进行运动插值,这样碰撞的效果会更加稳定。事实上运动插值虽然性能开销不是很高,但是由于类型众多,比如有静态粒子,碰撞体,风场等,实践起来还是非常麻烦的。
![]()
这里其实有一个疑问,我们为什么使用骨骼而不使用代理网络,而是使用顶点的方式去模拟布料。《恋与深空》项目中对于布料表现的模拟需求其实是比较复杂的,很多时候需要动画和解算的共同介入。骨骼方案可以在这两者之间做一个很好的过渡和平衡。然后受限于移动端的性能,骨骼方案结合我们 cts 的一些配置,可以留给美术很大的自由调节空间。在骨骼的基础上,我们实际上构建了类似顶点模拟的约束方式,可以看下图右侧的图,是一个物理资产的 debug 图,可以达到和 Mesh 模拟相对近似的效果。
![]()
在已有的骨骼布料方案里,骨骼约束实现常采用基于 Local 和 Global 形状约束的实现方式,这种方式的优点就是简单快速。但是也有明显的缺点,在用来做布料模拟时,效果偏向卡通风格,这不符合《恋与深空》追求的3D写实风格,而且它的参数调整非常不直观,因为它有 gloabl 和 local 两个弯曲参数,不利于美术调整以及在不同场景下的效果匹配。
![]()
我们在骨骼约束方案上,选择了基于 Cosserat Rod 的骨骼约束。它有几个优点,第一是效果上更加自然贴近《恋与深空》整体的3D写实美术表现风格。第二是弯曲参数上只有一个参数,更加直观,并且三个轴向强度分离,在模拟一些特殊场合,比如带有裙撑的裙子的时候,可以通过各向异性的弯曲强度来模拟出近似裙撑的效果。第三,这个方法在正常情况下其实更多地使用于头发和绳索的模拟,所以我们头发和衣服一样也使用同一套约束。这样工程量就会简化不少。下方视频是《恋与深空》最新日卡的一个表现视频,总的来说基于 Cosserat Rod 的骨骼约束是可以满足项目的一些表现需求的。
布料和角色的连接通过两种方式连接。第一种是静态骨骼直接受角色的骨骼动画影响,根据层级关系进行移动。这种方式比较简单,在一些偏向于刚性的连接部位时表现良好。但是对于一些骨骼交界有多个骨骼影响或者存在一定幅度拉伸和收缩的较为复杂的位置,例如手肘、肩部、腰部,表现上容易出现布料和角色分离。这时候我们提供第二种吸附的方式,将静态粒子吸附到角色模型的某个三角形上,通过并行的 bake mesh 获取每帧顶点的更新位置,使用离线计算的重心坐标来更新粒子的 transform。对于三角形存在的退化的特殊情况,我们使用三角形顶点的蒙皮骨骼的变换,进行加权平权来更新静态粒子的 transform。
![]()
碰撞方案上,我们使用一个 dynamic Bvh 来作为场景碰撞的 broad phase 管理,每个角色作为 sub tree,其内部的碰撞体就作为 sub tree node。通过角色 ID、分享可见性还有部件类型,这个三个规则来实现不同角色、不同部件的碰撞规则的共享规则管理。在 narrow phase 当中,我们不直接生成 contact,而是缓存碰撞体对,在 substep 中再具体的解决它们的 overlap。因为我们采用的 sub step 的优点,大多数情况下直接使用 DCD 就可以避免一些快速运动下造成的穿透问题,不需要引入 ccd 或者可预测碰撞(predictive contact)等一些操作。
![]()
对于参数化的几何碰撞体,例如平面(plane)、胶囊体(capsule)、box,可以比较简单地解决它们和粒子以及 edge 的碰撞。在肩部、胸部、背部等比较复杂区域,参数化的几何体难以准确表达角色模型形态,表现上容易发生穿透,所以在这些部位我们大量的使用 Mesh collider,但是 mesh collider 作为不规则的凹体,甚至有些时候来说它都不是封闭的,只是一个面。想达到精准的碰撞效果相对参数化几何体就比较困难,特别是在移动设备下。我们采用散列哈希来作为三角形的粗略查找方式,结合缓存的邻近三角形结果,在迭代开始前生成一次粒子-三角形碰撞对,后续的迭代中判读粒子是否在三角形的范围,如果超出三角形的范围,通过邻接关系进行限制步幅的三角形查找来获取最近的三角形,并且缓存结果作为下一次的使用。下方的视频是目前我们项目中的一些具体表现示例,可以看到表现上是比较稳定的。
面部碰撞体可以看作是特殊的 Mesh collider,相对于基本的 mesh collider,它形态较为固定,也较为平滑,从模型中心出发基本上没有三角形重叠,所以我们使用 16x16 的 cubemap 来预计算各个方向上的三角形,这样碰撞计算时可以快速查找到邻近的三角形。
![]()
游戏当中布料模拟的自碰撞是最难处理的部分,出于性能上的考虑,我们给出的方案是由美术预先对布料进行分层,只考虑这些层之间的碰撞。使用散列哈希作为查找的加速结构,并且为了避免层之间卡住的情况,我们只考虑单法线方向的碰撞,如果已经穿透了则略过,交给后面的步骤来修复。实际实践中,我们使用上一次 substep 的粒子位置来和当前的粒子位置进行碰撞,这样可以很简单的就解耦数据避免依赖。下方视频是项目中的一些层间碰撞的例子。
对于层碰撞已经穿透的部分,我们参考了 untanging cloth 的方式,使用了一个轻量的解决办法,通过布料分层,从布料的固定点出发,计算不同层级的边和三角形的交点,因为我们的资产结构必定为一个 uniform 的网格,因此可以通过网格交点比较简单的推测出其它粒子的推出三角形,最后对穿透的粒子-三角形对施加弹簧约束来解决穿透。在实践中由于 substep 的关系,穿透的概率相对不大,因此我们采用分帧分块执行来减轻性能压力。下方视频当中是一个三层的穿透分离测试,可以看到各层布料可以正确的从已穿透的状态中恢复出来。

实时表演控制
实时表演在《恋与深空》中了占了相当大的一块部分。《恋与深空》剧情表现中大部分的物理表现,都是依托于 cutscene 来实现的各种物理效果的控制和调节。感谢我们的工具同学开发和维护了一套非常强大的 cutscene 工具,在他们的基础上我们开发了多种的功能轨道来具体调控物理效果。物理相关的功能轨道的种类非常多,下面是一些较为常用的功能轨道,后面我会细致的介绍一些它们的具体功能。
![]()
下方的动图是我们一个动卡的 Cutscene Physcs Track 的例子,因为我们美术同学对于画面表现扣得非常细,所以看上去很普通的一个镜头可以看到整个物理轨道的配置还是非常复杂的。从这个图中可以看出来,美术配置了大量的轨道来保证画面能够达到他们预期的效果。

SmoothBlendPose Track:在表现当中,一个非常常见的问题就是动作瞬间切换带来的物理抖动。这个无论是在剧情表演中还是换装中,都经常出现。我们开发了一个较为通用的办法,通过记录初始物理姿态,在切换的时候在初始姿态和当前姿态中进行姿态插值计算,这样就可以大幅度的缓解抖动,当然这个会带来一些时间开销,一般会在几毫秒到几十毫秒左右,在大多数情况下都可以接受的。我们也会额外提供一些参数,例如插值次数、插值的步幅大小,来让美术可以根据实际需要来去调整。下方是换装中的视频,在切动作的时候会有大幅度切换,可以看到动作表现还是相对来说比较稳定的。
当然,SmoothBlendPose 也存在局限性,是通过 pose 间插值得到结果,不能保证的完全顺畅,特别是在一些剧情表演的复杂镜头切镜下。在这种情况下,我们还提供了一个比较直接的方案,离线直接保存某个时间帧的物理状态,在播放时,将保存的物理状态直接应用到布料上,这样就可以完美避免切镜带来的一些布料抖动的问题。下方视频中是快速的切镜,动作切换的镜头,可以看到使用 Pose Track 就可以让布料和头发的表现相对来说非常稳定。
参数编辑轨道:单一的物理参数是很难满足剧情当中的各种不同场景下的表现的,比如有的时候希望布料软一些硬一些,阻尼大一些小一些。我们提供编辑参数的轨道,通过这个轨道来实时修改参数,绝大部分的参数都可以覆盖大,可以非常方便美术针对一小段时间帧进行参数修改。这个参数修改还可以用来做一些特殊的效果,比如下方视频当中,美术就会利用参数轨道对约束参数进行编辑来实现实时的类似于布料断开的效果。
动画轨道:完全的物理效果实际上不足以支持起整个画面方方面面的表现的,很多时候需要动画和物理的结合来做一些互动。我们通过动画轨道来实现动画和物理的衔接与融合,精细的控制不同时间帧范围下的表现。在实际制作流程当中,动作在 DCC 里和最终进引擎的表现差异是比较大的,包括一些引擎的实时 rig 系统修改后,动画可能和其它地方有穿透,所以我们会在动画融合的基础上,叠加上物理的碰撞效果,来避免一些穿插。视频当中展示是项链在物理和动画之间的交互效果,包括从物理到动画的状态切换以及在不同动画之前的切换。
碰撞体轨道(Collider Track)与风场轨道(Wind Track)都可以在 cutscene 中动态地创建、销毁。可以根据不同画面需求,灵活改变碰撞体和风场的状态。通过角色、部件类型、还有布料的层分组来细节控制所要影响的对象范围。并且,碰撞体和风场轨道的绝大部分参数可以添加动画帧控制,包括碰撞体的形态大小、风场的方向、范围、强度、湍流等,方便美术细节地把控物理效果,精准控制变化。视频当中是一些轨道胶囊体和风场的表现例子。
基于 Unity DOTS 的开发
我们整套物理系统都是构建在 DOTS上,DOTS 这套工具非常强大。在 C# 层就可以实现高性能的多线程开发功能,迭代和 Debug 都非常便利。目前来说最高可以支持 2000+ 骨骼粒子的模拟。在针对性的项目使用当中我们也做了一些优化来进一步的提升性能。
Cache Job
模拟中的 job 数量和依赖关系确定,job data 并不频繁变化,帧内一般为相同数量和依赖关系的 job 组多次循环执行,Unity Jobs 在发起任务时每次都需要重新创建 job,虽然可以提前发起任务缓解,在子线程上执行,但是依然会卡主线程。基于以上的观察,我们开发了 Cache Job 的方案,预先创建好 job data,然后每次执行时复用,避免每次重新创建 job 带来的性能开销。
在实现上的话相对来说比较简单。因为它是一个专用的结构,只考虑一些固定的使用场景。我们额外添加了一个 Atomic Queue 用来存 cache job,使用 fetch and add array 来存具体的所需的 job data。下图右侧就是 worker 执行 cache job 的流程示意图。
![]()
Neon Intrinsics
Burst 会针对不同的平台生成高性能的 simd code。在 Burst Inspector 中可以非常方便地查看。经过检查 Burst Inspector 和实机测试,在某些场合下也可以通过手写 Arm Neon Intrinsics 来进一步提升性能。
![]()
这里给出两个比较常使用的例子:一是,Float4 的点乘。对于点乘,我这里列出了 3 种方式,使用 neon intrinsics 相比于 mathematics 在测试用例中可以获得相当大的性能提升。如果目标机型支持 armv8.2 的话,可以使用新增的规约加法指令,来进一步地提升性能。一般来说,市场上大部分的流行机型,如果是重度游戏,都会支持 ArmV8.2 的。但是在我实际的测试当中,最新的 Burst 实际上可以直接生成 ArmV9.0 的指令。但是在当前,你想要使用一定要去手写它。不过它带来的性能提升,其实还是比较明显的。特别是你去做一些展开,利用指令级可以得到更高的性能提升。
二是 float4×4 的转置。对于转置计算,可以看到 mathematics 生成的 assembly code 看起来性能是非常低的,通过手写 neon intrinsics 就可以得到一个巨大的性能提升。如果只是纯粹的需要转置,可以直接使用交错读,这样可以得到更快的性能。这里这样实现因为在一般的实际使用中,是通过对 4 个 float4 转置来将点乘变成矢量乘。但是在实际的使用当中,因为 mathematics 的代码一般被内联到具体的上下文当中,最终生成的 assembly code 相对于你自己的单元测试差别会非常大。所以在具体优化时还需要根据代码的上下文进行具体的优化,可以结合 burst inspector、真机测试和 Arm 的优化手册,来具体针对你的项目做出一些性能之间的比较和修改。
碰撞检测模块
首先要回答一个问题,就是 Unity 已经有基于 physx 的一套非常成熟的物理模块了,我们为什么要脱离 Unity 成熟的物理模块重新开发呢?
因为《恋与深空》有相当多不同种类的玩法,玩法间的 layer 设置相对独立,有些模块比如战斗,希望需要有特殊的 Trigger 触发和退出机制,并且希望在底层就能够支持,对于执行流程也希望有更灵活的控制。我们在性能探索上,也有一些自己的想法。就是说在仅需要碰撞测试的情况下,我们利用 DOTS 能否提升它的性能?
在《恋与深空》的实现当中,我们的碰撞检测模块基本实现了 Unity 原生的所有碰撞查询功能,针对战斗和其他模块我们定制了 Update 和 Trigger 相关逻辑。并且对于查询接口,在底层就保证了线程安全,让上层可以无负担调用。结合 DOTS 进行轻量化的实现,以及结合一些实际需求的优化,在性能测试当中我们最高获得了 15% 的性能提升。下方视频是战斗当中的一些录屏,整个角色的移动、技能命中,都是用我们自己的碰撞检测模块来实现的。
查询流程示例。下图是一个简单的示例。由于真机上我们实际的线程数量是固定的为 4,所以对于 memory allocator 可以预先按照线程数量分配好,在分配时可以直接根据当前线程索引来获取所需的内存。我们使用基于 SAH 的 dynamic bvh 作为 broadphase 加速结构,在插入、删除以及超出范围的移动时,对当前操作节点的邻近的几个层级节点进行旋转平衡。因为碰撞检测的功能目标相对概括,对于精度要求没有那么高,所以我们也适当的牺牲一些精度简化了一些碰撞检测算法来提升性能。
![]()
为了满足战斗模块的需要,我们还设计了特殊的 trigger 触发逻辑,触发 Trigger的进入和退出必须要成对出现,可以看到下方的流程示意图,在 A 触发 B 的函数中移除 B 后,会触发所有和 B 存在 overlap 的 collider。这里是和 Unity 原生的不一样的,原生的 Unity 中在 trigger 逻辑中删除掉 B 是不会触发其它碰撞体的 trigger的。最后,我们通过 History 计数来标记 collider 的版本,解决战斗中角色或碰撞体等道具的复用可能会导致的一些潜在问题。
![]()
我的分享结束了。谢谢大家!
Unity 官方微信
第一时间了解Unity引擎动向,学习进阶开发技能
每一个“点赞”、“在看”,都是我们前进的动力

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.