基于GPU Skin的骨骼动画Instance的实现

时间:2024-04-12 10:44:49

基于GPU Skin的骨骼动画Instance的实现

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的形式。

GPU Skin与GPU Instance
一旦当我们在GPU上实现了Skin,那么一个更诱人的特性GPU Instance就可以进一步引入进来,因为对于同一种类的怪物,不管他们做什么动作,他们在做动作的某一帧,他们输送给GPU的数据其实是几乎一样的,都是一份相同的处于T-POSE的顶点buffer,那么不同的只是每个角色当前处于的动作帧。这很容易结合GPU Instance,只提交一份T-POSE给GPU,然后整理每个角色各自不同的帧号作为per instance数据给显卡,那么就完成了支持骨骼动画的Instance,这样场景里所有一个种类的怪(可以分别在做不同的动作)可以通过一个Draw Call完成可以,这才是令人激动的。事实上笔者也正是为了想解决骨骼动画物体的GPU Instance的目的才来实现GPU Skin的。

2、基于GPU Skin的GPU Instance实现

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与GPU Skin
1)纯CPU Skin:

通常CPU Skin的做法就是,在CPU内实时的计算上面的运动矩阵,然后循环的对每一个顶点进行矩阵相乘,然后在进行必要的融合等高级动作处理技术得到最终这个顶点的局部坐标,传给gpu。
显然从性能上,CPU要每帧去计算每根骨头的变换矩阵,优化后是O(N)次矩阵乘法(N是骨骼数量),然后再进行顶点蒙皮在进行O(v)次矩阵乘法(v是顶点数量),这里面最容易称为瓶颈的是后者,因为场景的角色顶点量经常是数以万计的。但是它可以方便的实现一些复杂的动作效果
2)Unity的内置GPU Skin:
Unity内置的GPU Skin做法为了不影响用户的vs的书写以及兼容复杂的animator机制,因此做了一套半GPU Skin的方法。它利用了现代硬件API所具有的一些transform feed back这种机制,骨骼变换矩阵这里还是每一帧在GPU上做,但是蒙皮那里放到GPU上,然后蒙皮之后利用transform feed back得到蒙皮后的顶点位置,再次传入用户的vs,或者在CPU内进行一些融合等操作。
这种做法还是需要每帧去计算骨骼变换矩阵(这个开销不大),节省了Skin,但是它要多走一次客户的vs处理,并且如果用到cpu内的复杂动作处理的话,还涉及到将vertex buffer从GPU到CPU之间的传递。
3)纯GPU Skin:
这也是本文的做法,我们将完全在GPU上做整个skin 流程,由于上文的Pl = Mf1*PLT,我们预先保存每个骨骼在每一帧的Mf1。我们将这些变换矩阵伪装成一个贴图传递给显卡,然后只要将TPOSE的VBO和这张贴图给显卡,上面的蒙皮过程就完全在显卡计算了,先根据当前的帧和骨骼sample贴图获取这个矩阵m,然后做矩阵变换进行蒙皮,然后走其他的vs处理。
这里的一个关键问题是如果把这些矩阵伪装成一张贴图,我们知道这个pose变换矩阵由4*4=16个float组成(其实去掉最后一列的其次化表示是3*4),而对于RGBfloat这样的贴图格式,一个像素可以存储3个float,这样我们就可以用4个像素去表示一个数组。我们可以按照我们自己定义的顺序,预先将一个角色的每一个动作的每一帧的每一个骨骼的转换矩阵用这4个像素排布在这张图上,就等于我们用一张图而不是一些animationclip来存储了这个角色的所有动作。(在我们的测试中,一个具有10几个动作的普通怪物所占用的贴图大约比一张256*256的图稍大一下,256*256的格式为RGBFLOAT的贴图大约占用0.75M内存)。
这种方式要比传统个骨骼动画存储方式要稍微浪费一些内存,但是完全省去了CPU上的处理时间,并且在GPU上并行蒙皮,骨骼矩阵的计算采用预先烘焙后采样的方式,效率是非常高的,更重要的是,这种方式的Skin可以支持Instance。
当然这种方式要求Vertex的textrue fetch,要求GPU支持float类型的贴图,一般需要gles3.0以上级别的机器(当然也已经算比较普遍)。

GPU Skin与GPU Instance
本文讨论在Unity下面实现基于GPU Skin的GPU Instance。为了Instance,我们需要提供一份顶点buffer给GPU,这个很好办,对于一个外形的角色,根据上面的原理,它的运动后的顶点位置
P = M1 * Ptpose,即多个同外形的角色,无论他们在做什么动作,他们所需的原始的Vertex buffer都是相同的一份TPOSE,而不同之处在于每个物体正在使用的动作转换矩阵M不同,而在我们gpu skin的实现中,一个角色的所有动作的矩阵都藏在了一张texture上,所以我们把这个texture也提供给shader 之后,每个物体不同之处就是texture上的offset了(或者说就是一个动作号和一个帧号)。而根据Instance的原理,我们把每个物体的动作号,帧号当成per instance data组成数组给GPU,那样GPU就可以对多个骨骼动画物体一次提交而在GPU上一次绘制了。
通过同时运用GPU Skin和 GPU Instance,我们对同一个角色的多个对象,一次提交给GPU,在GPU上绘制多个动作,同时进行蒙皮操作。如本文最开始的场景一样,我们用一个Draw Call来绘制大量相同角色的动画。