1. 需求出现
最近项目有了新的需求, 需要敌我双方英雄各自带领数十(百)个小兵对冲, 乱战. 在之前的方案基础上去掉特效后效等进行测试, 发现性能开销简直惨不忍睹, 帧数只能勉强维持15-20的样子, 200个英雄+小兵的情况下CPU大概有40ms的耗时, DrawCall200多个. 分析后发现开销基本在处理动画和drawcall上.
2. 解决方案
针对上面的两个问题, DrawCall过多很容易联想到GPU Instancing, 因为小兵的mesh和材质都一样, 不过有一个问题, 小兵使用了SkinnedMeshRender渲染, 而GPU Instancing不支持SkinnedMeshRender. 同时骨骼动画也没有想到好的解决方案= =. 这时经过大佬指点, 知道了有一种叫动画贴图的东西, 可以(稍微, 还有很多坑)解决这个问题.
1. 动画贴图
传统骨骼动画都是在模型上面挂Animator或Animation上, 骨骼, 动作,蒙皮什么的都是CPU计算. 动画贴图的核心思想就是把这部分计算移植到GPU去计算, 毕竟GPU的强项就是计算, 肯定会大大减少计算时间. 同时SkinnedMeshRender也可以换成MeshRender, 为后面开启GPU Instancing做了铺垫.
动画贴图实现逻辑和顶点动画差不多, 都是要在vertex shader中改变顶点信息来实现动画. 于是我们要做的就是把之前的动作信息烘焙成一张贴图, 并且把之前Mesh重新烘焙. 具体实现的就是贴图的颜色值和顶点信息的对应, 不细说了, 根据动作精度要求往里塞就好, 记得塞的时候做一下数值转换就好.
按照时序逐帧烘焙的动画贴图大概长这样, 下一步要把之每个顶点受骨骼影响的权重信息存到新烘焙的Mesh里, 具体放在哪个语义块也自己定就好.
下面一步我们需要自己用CS实现Animation或者Animator, 建议如果模型动作数量不多的话可以把动作都烘到一张大图里, 实现一个Animator进行动作切换和循环等. 实现的主要思路就是给把要播放的动作在动画贴图中的偏移量根据帧率不停传到shader中. 之后我们只需要在vs中取到动画贴图的偏移量, 采样动画贴图取得顶点信息就可以得到近似骨骼动画的效果, 但是性能提高了非常多, 在1000个小兵同时播放动作的情况下, FPS也可以稳定在60, CPU占比比较高的是我们用CS实现的状态机, 但是也不足之前的十分之一.
当然动画贴图里面坑多的很. 最基础的是自己做一个状态机, 听着感觉很简单写起来也得一阵子, 动作融合也得写半天. 还有比如颜色精度不够高, 有时会出现动作不对的情况. 还有阴影也需要重新计算, 对应的一些描边,边缘高光什么的都需要重新处理. 不过考虑到模型数量过多, 很多表现效果可以根据情况优化掉或者伪造= =.
2. GPU Instancing
没什么好说的, 我们的模型经过刚才的处理后, 直接可以使用MeshRender渲染, 所以我们直接在shader里开启GPU Instancing就好, DrawCall瞬间从1000+降到了10+.
3.一句话总结
动画贴图方案适用于对表现细节精度要求比较低, 对表现氛围要求比较高, 有大量相同(换皮)角色播放动作的场景.
参考:https://medium.com/chenjd-xyz/how-to-render-10-000-animated-characters-with-20-draw-calls-in-unity-e30a3036349a