大量动画模型渲染的性能优化
最近在对支持的项目组进行性能优化工作,其中遇到的大量动画角色同屏渲染的问题,经过一些调研和实践,正好就大量动画渲染性能优化技术进行一些讨论。
骨骼模型由骨骼数据加骨骼动画进行驱动,每一帧动画系统会计算出当前帧的骨骼的变换矩阵,然后通过蒙皮的方式来计算每一个顶点的实时位置,最后渲染系统会根据蒙皮之后的顶点数据进行渲染。具体蒙皮的计算方法,不同的蒙皮技术会有不同的时机和实现方式。
CPU Skin
CPU蒙皮指在CPU端先计算好每个顶点的蒙皮位置,然后再将顶点数据发送到GPU端进行渲染,GPU端的顶点buffer里保存的是蒙皮后的顶点数据。我们知道CPU是擅长逻辑控制而不善于大量运算的,因此CPU蒙皮会严重的影响CPU性能,特别是场景中存在大量动画模型的时候,CPU基本会被动画更新,蒙皮计算和数据传输所占满,甚至会无法运行。
GPU Skin
骨骼蒙皮会逐顶点进行计算,在顶点数量比较多的时候,总的运算量是非常大的,但是我们注意到,每个顶点的计算是相互独立的,也就是说他们是并行的。这种大量的并行计算,那天然就适合在GPU端进行。这种在GPU端进行蒙皮计算的就叫做GPU蒙皮。
VS Skin
GPU蒙皮最简单的实现就是VS蒙皮,也就是在Vertex Shader中进行蒙皮计算:每个顶点保存自己的骨骼索引和骨骼权重,更新当前帧的骨骼数据并绑定,然后在Vertex Shader读取对应的骨骼变换矩阵进行计算,最后输出蒙皮之后的顶点坐标。VS蒙皮的优点在于将繁重的计算从CPU端移到了GPU端,大大的减少了CPU端的性能压力,对于CPU Bound的场景尤为重要。但是这个计算压力并没有完全消失,而是转移到了GPU端,在Vertex Shader里进行蒙皮计算会增加GPU的运算量和带宽。特别是在多pass渲染的场景,每个pass的Vertex Shader都会进行一次全量重复的蒙皮计算,在移动端,带宽的增加往往会直接影响耗电,因此也是一个需要进行权衡考虑。
CS Skin
在Vertex Shader中进行蒙皮计算存在一个问题,就是在多个pass中,每个pass的VS都要进行蒙皮计算,并且每个vs的蒙皮计算都是相同的,因此存在重复计算,需要渲染的pass越多,重复计算就越多。为了减少这些重复计算,可以使用CS蒙皮,也就是在所有pass开始之前,使用Compute Shader进行蒙皮计算,将蒙皮计算的结果保存为VertexBuffer,用于后续的各个pass的渲染,这样不管后续的pass数量有多少,我们都只需要蒙皮一次。UE里使用Compute Shader进行Skin的方案叫做GPUSkinCache,Cache的意思实际就是提前计算蒙皮,然后缓存给后续的pass使用。CS蒙皮的好处是可以有效的减少重复的蒙皮计算,但是ComputeShader的Dispatch和计算并不是免费的,在动画模型数量较多的时候,Compute Shader的派发和计算都是需要消耗GPU时间的,而且会对整个管线造成阻塞。
VS Skin Instance
场景中大量的动画模型不仅会增加蒙皮的计算,而且会显著增加DrawCall的数量。静态模型可以通过实例化渲染来减少DrawCall数量,但是不同的动画模型播放的动画帧不一定相同,因此不满足实例化的顶点数据相同的条件,无法直接进行合批渲染。
那如何才能实现实例化渲染呢?我们观察到,动画模型的蒙皮是在初始的顶点数据上应用骨骼的Transform,这里每个动画模型顶点的蒙皮前的原始数据都是相同的,因此我们可以将动画所有帧的骨骼变换矩阵保存到一个大的buffer或者texture上,然后针对每个动画的Instance,给定一个骨骼变换的数据的Offset即可,在Vertex Shader阶段,每个顶点根据自身的骨骼索引和Offset获取当前的骨骼变换矩阵,然后进行蒙皮计算。因此我们只需要绑定初始的TPose的VertexBuffer和记录BoneMatrix的offset的InstanceBuffer即可实现动画模型的实例化渲染。
Vertex Animation Texture
前面介绍的GPU蒙皮的方案都是将CPU端的蒙皮计算过程放到了GPU端来进行计算,而VAT(Vertex Animation Texture,顶点动画贴图)技术则是直接通过空间来换时间,在离线阶段将每一个动画帧的所有顶点位置都烘焙到一张贴图上,在运行时,给定动画帧索引,每个顶点可以直接在顶点动画贴图上采样得到当前的顶点位置,这样整个蒙皮过程只需要一次贴图采样即可,没有任何的计算过程,因此速度非常快。VAT技术的细节可以参考该文章。
前面说过,VAT实际上是通过空间来换取时间,相比较于存储骨骼矩阵,存储所有帧的顶点位置的数据量要大得多,特别是顶点数量远大于骨骼数量的场景。所以VAT适合那个网格相对简单的动画模型,比如场景中大量的NPC小兵或者怪物,这些动画模型的动画和几何都比较简单,占用的内存较少。
另外以上的GPU Skin的动画模型都存在一个问题就是,当CPU端需要使用蒙皮之后的数据的时候(如射击游戏的射线检测),往往在CPU端还需要进行一次蒙皮计算,会产生额外的性能消耗。
CPU Skin Instance
在项目的游戏场景中,大量的动画模型并不会所有模型都是不同的动画状态,而是可以分为若干组,每个组的动画模型播放的动画和序列帧都是相同的,这种情况下,还有一种更加高效的优化方法,我称作为 “CPUSKinInstance”,具体是指:针对每个动画和序列中相同的动画模型组,可以在CPU端进行一次蒙皮操作,然后将蒙皮之后的模型作为实例化渲染的VertexBuffer,这样场景中所有N个动画模型只需要蒙皮M次(M为组的数量),可以同时节省CPU和GPU的开销。这种方案适合于N远大于M的场景(比如大几个数量级)。
以上针对不同的动画模型渲染方法做了简单的介绍,每个方案各有优缺点,实际使用中需要针对具体的使用场景进行选择,还是那句话:没有最好的技术,只有最合适的技术。

