1、GPU Skin与GPU Instance

骨骼动画是指通过定义骨架结构,然后在上面蒙皮,然后对骨架做动作驱动模型运行的动画,游戏中大部分的角色表现是通过骨骼动画进行的。骨骼动画本质上最终是通过Skin和Skeleton的Animation变换模型的顶点数据。骨骼动画既可以在CPU端实现,也可以在Gpu端实现。
在现有的很多游戏引擎中,骨骼动画的实现大多在CPU端进行实现,例如Unity,原因有很多:比如引擎希望在骨骼动画中加入一些复杂的如动作融合, 骨骼部位分离动画,IK,重定向,骨骼蒙版,跟骨骼动画等等复杂的动作机制,来表现更高质量的动作表现。
 

为什么需要GPU Skin?
但是我们知道Skin是一个对每个mesh上的点进行变换的并行问题,在CPU端实现会带来极大的性能限制,尤其在手游中,很多要求同屏人数多的游戏到了后期会受制于同屏的角色数。我在过往项目的统计在Unity中,一般中端机器可以支撑8-10W面左右的普通mesh,但是如果是Skinnedmesh,大约1.5W面就会遇到瓶颈。
总的来说CPU断的Skin可以做复杂的动画机制,但是效率很低。这时我们想到了GPU Skin。GPU Skin在GPU上并行的对mesh的点进行Skin变换,可以极大的提升效率,另外它节省了相对来说更宝贵的CPU资源。不过支持复杂的动画机制相对难度较大。
所以在一些游戏的实现中,可以思考对于怪物以及不重要的玩家对象采取GPU Skin,因为这些单元大多也不需要很复杂的融合,blend tree这种机制,但是却可以快速的渲染,而对重要的主角这种才保留GPU Skin的形式。
 

Skin的原理
假设我们有一个骨架Skeleton,骨架是由一些有父子关系的节点(即骨头)组成,每个骨头代表一个坐标系。一段骨骼动画通常记录了每个骨头在其父空间相对于初始姿态的几何变换(根骨骼的父空间可以认为就是模型的局部空间)。
在初始的时候,Skeleton被摆到一个初始的姿态,即T-POSE,同时和这个骨骼对其建立角色的模型。
这样对于任意一帧动画,模型上任何一个顶点P最终在模型局部坐标系下的位置Pl就等于
Pl = Mroot-local ..*M2-3 * M1-2 * MBind1 * PLT + Mroot-local ..*M2-3 * M1-2 * MBind2 * PLT + ...
这里面Plt是TPOSE下这个顶点的坐标。
MBind1是影响这个顶点的第一根骨骼的从模型局部坐标系到这根骨骼的TPOSE下的坐标系的转换矩阵(也被简单称为这根骨骼的绑定矩阵,它意味着把顶点从局部坐标变换到这根骨骼坐标系下面的坐标)。
Mm-n表示在m的父骨骼n的坐标系下,m骨骼在此运动帧相对于TPOSE时的运动,因此M1-2 * MBind1 * PLT就相当于将局部坐标系TPOSE下的一个模型点变换到了它的父骨骼坐标系下此帧时的坐标。
这样级联的网上乘,最终得到了这个运动帧下这个顶点在局部坐标系下的位置。
我们可以把Mroot-local ..*M2-3 * M1-2 * MBind1 用一个矩阵Mf来表示,即Pl = Mf1*PLT + Mf2*PLT+... 。
这个Mf就是在第f帧,将模型的TPOSE的局部坐标转变为此时姿态的局部坐标的转换,更简洁的说,这个Mf就是第f帧的运动变换矩阵,知道了这个Mf,我们就可以方便的将任意顶点进行动画驱动。
由于通常一个顶点会被很多根骨骼影响,例如两个手指间的某个顶点会同时受到两根甚至三根骨骼影响,所以这里要后面再加几根骨骼的影响,不过通常对于效率考虑,引擎会限制收影响的骨骼根数,如1,2或4等。
 

纯CPU Skin:
通常CPU Skin的做法就是,在CPU内实时的计算上面的运动矩阵,然后循环的对每一个顶点进行矩阵相乘,然后在进行必要的融合等高级动作处理技术得到最终这个顶点的局部坐标,传给gpu。
显然从性能上,CPU要每帧去计算每根骨头的变换矩阵,优化后是O(N)次矩阵乘法(N是骨骼数量),然后再进行顶点蒙皮在进行O(v)次矩阵乘法(v是顶点数量),这里面最容易称为瓶颈的是后者,因为场景的角色顶点量经常是数以万计的。
 

Unity的内置GPU Skin:
Unity内置的GPU Skin做法为了不影响用户的vs的书写以及兼容复杂的animator机制,因此做了一套半GPU Skin的方法。它利用了现代硬件API所具有的一些transform feed back这种机制,骨骼变换矩阵这里还是每一帧在CPU上做,但是蒙皮那里放到GPU上,然后蒙皮之后利用transform feed back得到蒙皮后的顶点位置,再次传入用户的vs,或者在CPU内进行一些融合等操作。
这种做法还是需要每帧去计算骨骼变换矩阵(这个开销不大),节省了Skin,但是它要多走一次客户的vs处理,并且如果用到cpu内的复杂动作处理的话,还涉及到将vertex buffer从GPU到CPU之间的传递。
 

纯GPU Skin:
由于上文的Pl = Mf1*PLT,我们预先保存每个骨骼在每一帧的Mf1。我们将这些变换矩阵伪装成一个贴图传递给显卡,然后只要将TPOSE的VBO和这张贴图给显卡,上面的蒙皮过程就完全在显卡计算了,先根据当前的帧和骨骼sample贴图获取这个矩阵m,然后做矩阵变换进行蒙皮,然后走其他的vs处理。
我们知道这个pose变换矩阵由4*4=16个float组成(其实去掉最后一列的其次化表示是3*4),而对于RGBfloat这样的贴图格式,一个像素可以存储3个float,这样我们就可以用4个像素去表示一个数组。我们可以按照我们自己定义的顺序,预先将一个角色的每一个动作的每一帧的每一个骨骼的转换矩阵用这4个像素排布在这张图上,就等于我们用一张图而不是一些animationclip来存储了这个角色的所有动作。