由浅到浅入门批量渲染(四)

发表于2020-11-27
评论0 7.5k浏览

好久不见。

 

这是第35篇与游戏开发有关的文章。

 

上回(由浅到浅入门批量渲染(三)​)简单总结了一下实例化渲染,这次我们说说优化骨骼蒙皮动画

 

| 前言

 

试想一下,当我们在游戏场景中放置大量(成百上千)带有骨骼蒙皮动画的单位时,会发现帧数已经开始下降,这是为什么呢?

 

经过多年研究,我发现,造成这种情况的根本原因是:放的太多了。

 

然而,在开发某些类型的游戏(如策略或即时战略等)时,通常又需要尽可能的多放些小兵或者怪物,来烘托战场气氛。

 

需要呈现大量的角色,又需要保证性能,是一件挺麻烦的事情;如果你也在尝试解决这个问题,并且暂时还没有找到合适的方法,那接下来要讲的内容可能会帮到你;因为我们将进入这一系列(从浅到浅)的下半部分:总结目前较为成熟的针对骨骼蒙皮动画的优化方案。

 

| 骨骼蒙皮动画的开销

 

“欲练神功,必先自宫”是笑傲江湖中非常有名的一句台词,它也是神功《葵花宝典》和《辟邪剑法》武功秘籍上的第一句话。以前听到它时只是感觉这就是邪魔外道武功的一个符号罢了;但现在想来,当时还是年轻了,没看懂。它其实是一条学习任何技能、知识的诀窍,也是一把打开成功之门的钥匙。

sN19GfZGSWouXDmw5eog.png

葵花宝典上的斑斑血迹记录了多少悲伤故事

 

其实,这句话想传达的真正意思是:做事情,要打好基础

 

所以,要优化骨骼蒙皮动画,就要先简单了解下它的性能瓶颈所在。

 

| 骨骼蒙皮动画的流程

 

可以简粗(简单粗暴)的将骨骼蒙皮动画的工作流程分为以下几个阶段:

 

播放动画阶段

动画控制器会根据关键帧信息等,调整骨骼的空间属性(旋转、缩放、平移)。

 

计算骨骼矩阵阶段

根骨骼开始,根据层级关系,逐一计算出每一根骨骼的转换矩阵。

 

这个矩阵连接的是这根骨骼的本地坐标系角色坐标系(通常会是角色脚下);也就是说通过它可以在某一帧动画结束后,将(某一根)骨骼坐标系下的坐标或向量,转换到角色坐标系下。

 

蒙皮阶段

更新网格上每个顶点的属性。

 

由于动画改变的是骨骼而不是顶点的空间属性;而且网格中的顶点是相对于网格坐标系下的,并非在角色坐标系下。所以在这一阶段,我们首先要依据创建骨骼蒙皮动画时,被记录下来的顶点和骨骼的关系,找到对应的骨骼(Unity中通过Mesh.boneWeights获取,一个顶点最多可受到四根骨骼影响)。

 

其次,通过网格坐标系到骨骼本地坐标系的转换矩阵(Unity中通过Mesh.bindposes获取),来建立从网格坐标系骨骼坐标系的桥梁;结合上阶段得到的骨骼坐标系角色坐标系的转换矩阵,实现将动画对骨骼的影响最终作用到顶点上,并将其更新到角色坐标系下。

udcMhHRpvKJGqVvmGSop.gif

顶点的属性被动画控制器“间接”更新

 

渲染阶段

​当顶点变换到角色坐标系下后,就可以进行渲染了。这里与一次普通的渲染没什么太大差别,唯一需要注意的是,Unity不会对蒙皮网格渲染器进行合批,所以每一个骨骼蒙皮动画实例都至少需要一次DrawCall。

 

| 骨骼蒙皮动画的开销

 

可以将骨骼蒙皮动画的主要开销,也简粗的分成以下几个部分:

  • 更新动画的开销
  • 计算骨骼矩阵的开销
  • 蒙皮开销
  • 渲染开销

 

这里我创建了一个简单的场景,来简单测试下这些开销。

 

使用Unity 2018.4.14f1版本创建一个测试场景,场景中包含500个使用相同模型的骨骼蒙皮动画角色,并循环播放空闲、移动、攻击动画;测试机是华为P20(Geekbench5得分约为1400),通过Profiler查看运行耗时。

 

NsarWUP8akhDO4zZ4KHr.png

简单的步兵模型

mHzJJygvPqpCszbp7jqo.gif

场景运行后

 

我们把与骨骼蒙皮动画有关的主线程运算开销整理出来,看一下在骨骼蒙皮动画工作时哪些计算耗时最多。

cNMitb0MzDcDe6gY4VQg.png

与骨骼蒙皮动画有关的主线程耗时占比

 

可以发现,动画更新的耗时占比最高,其次是蒙皮网格的更新(计算矩阵、蒙皮等),最后是渲染。

 

需要指出的是,我使用的Unity(版本2018.4.14f1)将动画更新和蒙皮放到了工作线程中;所以像蒙皮这种“逐顶点、理论上应该开销很大的操作”带来的耗时增加,并没有体现在主线程中;而且我在打包时也没有勾选多线程渲染(Multi-threadedRenderer),所以渲染指令的调用也都发生在主线程。

gfUizMNpHuPtW3Hh0cM6.png

动画更新被放在工作线程中执行

MhwsjB3CHmixudNYpmeX.png

蒙皮被放在工作线程中执行

5OuD5sbW6l5zZlhnUvRG.png

渲染指令在主线程中调用


 

| 常见优化方式

 

Unity下可以通过以下两种方式快速优化骨骼蒙皮动画:

  • 在导入模型时进行的优化
  • 在打包设置中开启GPU蒙皮

 

这两者的优化效果怎么样呢。

 

导入模型时的优化

0j6vVKmncE6NBoHGyYzH.png

勾选模型导入设置进行优化

 

在相同的测试环境下,再次进行测试后可以发现,这种方法确实可以产生一定效果。

 

其原因我认为主要有以下两点:

 

不再为骨骼创建不必要的游戏对象

对导入模型进行优化后,Unity将不会为骨骼创建实际的游戏对象了(我们也可以暴露出一些骨骼作为挂点)。

cAkdGEjZhjYImuXiFZfM.png
nvDTS5NVmaV65eOIjtam.png

这些消失的游戏对象一定程度上也减少了CPU的性能开销

 

计算矩阵被移到工作线程

除此之外,Unity还会将计算骨骼矩阵的操作放到工作线程中,来减少主线程耗时。

ouLAJcMPXdDtQSlG3AtM.png

计算矩阵在工作线程中进行

zBqTxMiMHE7AtDEMu9xC.png

主线程耗时不再包含计算矩阵


GPU蒙皮

开启GPU蒙皮,Unity会通过ComputeShader的方式,使用GPU进行蒙皮。

gkTVtJ6frRs6ckzaCGHU.png

勾选设置开启GPU蒙皮


 

GPU上有大量的ALU(算数逻辑单元),可以并行大量的数值计算,效率较高,应该很适合这种针对顶点属性的数值计算。

 

但是实际情况是:在移动设备上使用GPU蒙皮反而会使主线程的耗时增加

0S174CQ1JKvJmqngU8kh.png

开启GPU蒙皮后主线程耗时增加
 

通过Profiler可以发现,CPU把更多的时间放在了执行ComputeShader上,由于骨骼动画的实例很多(500个),所以这个调用时间本身成为了性能热点。

ghJ8lE9FQ2UExocr9JIP.png

开启GPU后,蒙皮网格更新中增加了GPU蒙皮

 

389V3HLtMrvsfnU0MUZ3.png

执行GPU蒙皮耗时较高
 

所以,以目前的情况来看,这种在移动设备上使用GPU蒙皮的方式,似乎不适合处理大量骨骼蒙皮动画实例(也许是我使用的方式存在问题)。

 

| 写在最后

 

我们简单总结了骨骼蒙皮动画的工作方式,分析了主要的性能开销及耗时,以及比较了Unity中常用的两种优化方式。

 

但在面对数以千计的角色表现需求上时,无论使用何种Unity自带的优化,都显得有些力不从心;所以下次的更新,将介绍两种目前较为有效、成熟的“奇技淫巧”,来(一定程度上)解决这个问题。

 

下回见喽。

 

我的公众号 偶尔学学Unity 会特别不定期更新与游戏开发可能有关的文章,欢迎关注,谢谢。

z199lsl0Bj9nptFL9l1k.png


 


 

  • 允许他人重新传播作品,但他人重新传播时必须在所使用作品的正文开头的显著位置,注明用户的姓名、来源及其采用的知识共享协议,并与该作品在磨坊上的原发地址建立链接
  • 可对作品重新编排、修改、节选或者以作品为基础进行创作和发布
  • 可将作品进行商业性使用

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引