1.Unity5.6目前对Skinning的处理有两种模式:
a)一种是CPU Skinning,多线程+SIMD,性能实测非常不错
b)还有一种是unity GPU Skinning,但它与传统的vertex shader做顶点混合不一样,是通过transform feedback+ vertex shader (opengl)/ stream output + geometry shader(dx11)做顶点混合,写到新的GPU端verticesbuffer后,再提交一次一般模型的渲染完成的.如果这个Skinning模型会触发多遍渲染(shadow pass/reflecionpass/..),理论上相对传统GPU Skinning能节省一些顶点混合的计算而变快,实测PC上确实有5%左右的提升(仍然CPU Bound).
2.因为实测中发现Unity这两种做法目前都是CPU Bound,理论上传统的GPU Skinning会更快(1 pass),所以我尝试在Unity5.6上实现了传统的GPU Skinning,并在C#上尽力做了一些优化和处理...
3.主要流程:
a)修改原Mesh,让顶点数据带上BoneWeight所有信息
b)替换SkinneMeshRenderer为普通MeshRender,切换新的传统GPUSkinning材质及shader
c)每帧动画做完后,计算Bone Palette,并传给shader渲染。
d)Vertex Shader里做顶点混合相关计算,这里v.texcoord1对应上面的indices,v.texcoord2对应上面的weights
4.尝试优化
a)通过上面的流程已经能完成传统的GPU Skining了,但是因为上面的‘c)计算Bone Palette’过程是在主线程完成的(C#),加上Unity自身对Transform非主线程访问的限制,相对Unity5.6的多线程CPU Skining,性能不理想,当然,如果是单线程应该是变快了...
b)第一个优化就是打包顶点数据,看上面代码,打开C#的宏PACK_WEIGHT_AND_BONEID,打开vertex shader里对应USE_PACKED_ID_WEIGHT,因为BoneIndex是整数索引,weight 0.0-1.0f,weight缩小一点点再加在一起传入,shader读出还原,指令简单。这个做法需要高精度浮点值支持,手机目前是不行的,(参考链接https://community.arm.com/graphics/b/blog/posts/benchmarking-floating-point-precision-in-mobile-gpus),理论上也可以仔细做数值分析与打包,但shader会复杂化,很可能得不偿失。
c)第二个优化,很有创意,通过多构建一个特殊的SkinnedMesh(顶点数 == Bone Num x 4), 通过unity的BakeMesh接口,计算出了当前模型的Bone Palette(结果存在这个特殊的SkinnedMesh做BakeMesh后的vertices上),性能上比默认实现提升了40%+,主要区别就是C#计算BonePalette换成了BakeMesh的native simd代码计算,但是仍然在主线程。Unity源码层面,BakeMesh和Skinning.Skin是一套代码,主线程与Worker线程计算的区别。
i.构建特殊用途的SkinnedMesh,通过特殊的Vector3,使BakeMesh的计算结果可以简单反推Bone Palette。见下面代码的几行注释,分别取到了矩阵的4列,实际因为Unity的Position只能是Vector3,结果不是直接取到4列,需要再做简单计算。
ii.动画做完后CPU BakeMesh,复制出BonePalette,简单计算还原。
iii.上面的转置其实可以通过shader调整左乘右乘调整,但会增加一个Vector4,加上开销较小,就不做这个处理了。
iv.实际的测试模型,有100+根骨骼,这Shader目前只支持最大64根,C#端做了拆分处理(代码很长,不贴了)。
v.性能测试
PC上,30个人渲2遍,增加了9.3ms的GPUSkinRender.LateUpdate开销,BonePalette通过BakeMesh的计算就在这里(理应快很多,因为BakeMesh做了些额外的事情)。如果是直接bones[j].localToWorldMatrix * bindPoses[j];会是15.0ms左右。这一段计算合理的话应该在Worker线程完成的,但在C#层面无法修改或者得不偿失(可以自己用额外的分线程去计算,但是Transform需要提前Copy,按以往经验,快不了多少).
PC上的同样的角色做CPU Skinning,Worker线程计算MeshSkinning.Skin非常快,主线程的overhead几乎可以忽略,性能上完胜.
5.结论
Unity CPU Skinning因为多线程与SIMD优化,性能非常好,在目前多核CPU时代非常有优势。在PC上,UnityGPU Skinning亦有更好的性能,实测下来是目前最优方案,也确实看到有游戏使用了(PC端的女仆2)。在Unity上实现的传统GPU Skinning,通过C#层面的优化,综合性能并不理想,主要原因是Unity的限制导致Bone Palette无法高效的多线程计算。但理论上,在GPU有余力时,Bone Palette如果能高效多线程计算,性能应该会上升.后续我会尝试在Unity源码层面做一些修改,希望能取得更优的移动平台性能。