由浅到浅入门批量渲染(四)
好久不见。
这是第35篇与游戏开发有关的文章。
上回(由浅到浅入门批量渲染(三))简单总结了一下实例化渲染,这次我们说说优化骨骼蒙皮动画。
| 前言
试想一下,当我们在游戏场景中放置大量(成百上千)带有骨骼蒙皮动画的单位时,会发现帧数已经开始下降,这是为什么呢?
经过多年研究,我发现,造成这种情况的根本原因是:放的太多了。
然而,在开发某些类型的游戏(如策略或即时战略等)时,通常又需要尽可能的多放些小兵或者怪物,来烘托战场气氛。
需要呈现大量的角色,又需要保证性能,是一件挺麻烦的事情;如果你也在尝试解决这个问题,并且暂时还没有找到合适的方法,那接下来要讲的内容可能会帮到你;因为我们将进入这一系列(从浅到浅)的下半部分:总结目前较为成熟的针对骨骼蒙皮动画的优化方案。
| 骨骼蒙皮动画的开销
“欲练神功,必先自宫”是笑傲江湖中非常有名的一句台词,它也是神功《葵花宝典》和《辟邪剑法》武功秘籍上的第一句话。以前听到它时只是感觉这就是邪魔外道武功的一个符号罢了;但现在想来,当时还是年轻了,没看懂。它其实是一条学习任何技能、知识的诀窍,也是一把打开成功之门的钥匙。
葵花宝典上的斑斑血迹记录了多少悲伤故事
其实,这句话想传达的真正意思是:做事情,要打好基础。
所以,要优化骨骼蒙皮动画,就要先简单了解下它的性能瓶颈所在。
| 骨骼蒙皮动画的流程
可以简粗(简单粗暴)的将骨骼蒙皮动画的工作流程分为以下几个阶段:
播放动画阶段
动画控制器会根据关键帧信息等,调整骨骼的空间属性(旋转、缩放、平移)。
计算骨骼矩阵阶段
从根骨骼开始,根据层级关系,逐一计算出每一根骨骼的转换矩阵。
这个矩阵连接的是这根骨骼的本地坐标系和角色坐标系(通常会是角色脚下);也就是说通过它可以在某一帧动画结束后,将(某一根)骨骼坐标系下的坐标或向量,转换到角色坐标系下。
蒙皮阶段
更新网格上每个顶点的属性。
由于动画改变的是骨骼而不是顶点的空间属性;而且网格中的顶点是相对于网格坐标系下的,并非在角色坐标系下。所以在这一阶段,我们首先要依据创建骨骼蒙皮动画时,被记录下来的顶点和骨骼的关系,找到对应的骨骼(Unity中通过Mesh.boneWeights获取,一个顶点最多可受到四根骨骼影响)。
其次,通过网格坐标系到骨骼本地坐标系的转换矩阵(Unity中通过Mesh.bindposes获取),来建立从网格坐标系到骨骼坐标系的桥梁;结合上阶段得到的骨骼坐标系到角色坐标系的转换矩阵,实现将动画对骨骼的影响最终作用到顶点上,并将其更新到角色坐标系下。
顶点的属性被动画控制器“间接”更新
渲染阶段
当顶点变换到角色坐标系下后,就可以进行渲染了。这里与一次普通的渲染没什么太大差别,唯一需要注意的是,Unity不会对蒙皮网格渲染器进行合批,所以每一个骨骼蒙皮动画实例都至少需要一次DrawCall。
| 骨骼蒙皮动画的开销
可以将骨骼蒙皮动画的主要开销,也简粗的分成以下几个部分:
- 更新动画的开销
- 计算骨骼矩阵的开销
- 蒙皮开销
- 渲染开销
这里我创建了一个简单的场景,来简单测试下这些开销。
使用Unity 2018.4.14f1版本创建一个测试场景,场景中包含500个使用相同模型的骨骼蒙皮动画角色,并循环播放空闲、移动、攻击动画;测试机是华为P20(Geekbench5得分约为1400),通过Profiler查看运行耗时。
简单的步兵模型
场景运行后
我们把与骨骼蒙皮动画有关的主线程运算开销整理出来,看一下在骨骼蒙皮动画工作时哪些计算耗时最多。
与骨骼蒙皮动画有关的主线程耗时占比
可以发现,动画更新的耗时占比最高,其次是蒙皮网格的更新(计算矩阵、蒙皮等),最后是渲染。
需要指出的是,我使用的Unity(版本2018.4.14f1)将动画更新和蒙皮放到了工作线程中;所以像蒙皮这种“逐顶点、理论上应该开销很大的操作”带来的耗时增加,并没有体现在主线程中;而且我在打包时也没有勾选多线程渲染(Multi-threadedRenderer),所以渲染指令的调用也都发生在主线程。
动画更新被放在工作线程中执行
蒙皮被放在工作线程中执行
渲染指令在主线程中调用
| 常见优化方式
Unity下可以通过以下两种方式快速优化骨骼蒙皮动画:
- 在导入模型时进行的优化
- 在打包设置中开启GPU蒙皮
这两者的优化效果怎么样呢。
导入模型时的优化
勾选模型导入设置进行优化
在相同的测试环境下,再次进行测试后可以发现,这种方法确实可以产生一定效果。
其原因我认为主要有以下两点:
不再为骨骼创建不必要的游戏对象
对导入模型进行优化后,Unity将不会为骨骼创建实际的游戏对象了(我们也可以暴露出一些骨骼作为挂点)。
这些消失的游戏对象一定程度上也减少了CPU的性能开销
计算矩阵被移到工作线程
除此之外,Unity还会将计算骨骼矩阵的操作放到工作线程中,来减少主线程耗时。
计算矩阵在工作线程中进行
主线程耗时不再包含计算矩阵
GPU蒙皮
开启GPU蒙皮,Unity会通过ComputeShader的方式,使用GPU进行蒙皮。
勾选设置开启GPU蒙皮
GPU上有大量的ALU(算数逻辑单元),可以并行大量的数值计算,效率较高,应该很适合这种针对顶点属性的数值计算。
但是实际情况是:在移动设备上使用GPU蒙皮反而会使主线程的耗时增加。
开启GPU蒙皮后主线程耗时增加
通过Profiler可以发现,CPU把更多的时间放在了执行ComputeShader上,由于骨骼动画的实例很多(500个),所以这个调用时间本身成为了性能热点。
开启GPU后,蒙皮网格更新中增加了GPU蒙皮
执行GPU蒙皮耗时较高
所以,以目前的情况来看,这种在移动设备上使用GPU蒙皮的方式,似乎不适合处理大量骨骼蒙皮动画实例(也许是我使用的方式存在问题)。
| 写在最后
我们简单总结了骨骼蒙皮动画的工作方式,分析了主要的性能开销及耗时,以及比较了Unity中常用的两种优化方式。
但在面对数以千计的角色表现需求上时,无论使用何种Unity自带的优化,都显得有些力不从心;所以下次的更新,将介绍两种目前较为有效、成熟的“奇技淫巧”,来(一定程度上)解决这个问题。
下回见喽。
我的公众号 偶尔学学Unity 会特别不定期更新与游戏开发可能有关的文章,欢迎关注,谢谢。