最近在为引擎升级64位的过程中GPU蒙皮也出现了异常,平常骨骼动画和网格蒙皮用的还是非常多的,但是底层的原理并没有深究过,想着还是有必要好好整理下这部分内容。

骨骼蒙皮动画

一般我们称为骨骼动画(Skeletal Animation),是模型动画的一种(模型动画有2种,顶点动画和骨骼动画),包含了骨骼(Bone)和蒙皮(Skinned Mesh)两部分数据。互相连接的骨骼组成骨架结构,通过改变骨骼的朝向和位置来生成动画。而蒙皮则是这个技术的关键点,这里有个误区,字面意义上新手可能会觉得蒙皮的“皮”指的是贴图,但实际上这里的皮指的是Mesh本身,蒙皮是指把Mesh的顶点附着(绑定)在骨骼上,并且每个顶点可以被多个骨骼控制,这样在关节处的顶点由于同时受到父子骨骼的拉扯而改变位置从而消除了缝隙。

模型在导入Unity会显示Mesh和Root Bone

顶点的蒙皮信息包含了顶点受哪些骨骼影响以及这些骨骼影响该顶点的权重。有了蒙皮信息,只要我们的骨骼正常运动,蒙皮便会跟着骨骼一起动起来。为了让骨骼动起来,我们需要动画数据。在Unity中,我们通过读取动画文件中的关键帧信息来获取动画数据。

使用Animation窗口我们可以看到动画文件中的关键帧数据

这里简单聊下制作流程,一般模型师制作好模型以后,会交由动画师来制作模型动画,常用的工具有3dmax(游戏制作用的比较多)和maya(影视级别)。首先绑定好骨骼,2足类一般使用的比较多的就是T-Pose,并根据设计需要添加一些额外的骨骼(翅膀、飘带等),根据程序需要添加一些额外的虚拟体挂点(武器、装备等),这一步通常需要提前规划好,否则后续需求更改重新绑定骨骼会比较麻烦。一般会选择盆骨的位置作为根骨骼,但是这种情况下根骨骼与世界原点并不重合,通常会在骨骼空间选择两脚之间的中点作为原点(模型空间的原点)构建一根骨骼,让根骨骼Bip01作为这根骨骼唯一的子骨骼。绑定好骨骼以后,根据设计的动画方案,调整骨骼位置和朝向并K帧,最终导出动画文件。

当我们把模型导入Unity,放到世界坐标系中的时候,如果模型带有骨骼,骨骼会决定模型在世界坐标系中的位置和朝向,因为Mesh的顶点都是依附在骨骼上的。我们调整的是根骨骼的位置和朝向,子骨骼会根据骨骼层次结构中的父子关系计算得出。而对于静态模型来说,没有骨骼,所有的顶点都是定义在模型坐标系上的,然后经过模型坐标系到世界坐标系的转换。

在骨骼的层级结构里面,我们用基于关节为原点的坐标空间来进行描述,所有的子骨骼都是基于关节为原点进行平移、旋转和缩放的(通常都是旋转矩阵,骨骼之间通过旋转变换就可以有复杂的动作组合)。如下图所示,父骨骼的变换会影响到自身和所有子骨骼。

数学实现

聊完骨骼动画原理部分,我们整理下其中的坐标空间变换。模型顶点需要从模型空间变换到所关联的骨骼自身的骨骼空间(BoneOffsetMatrix),再通过骨骼的世界变换计算出世界坐标(BoneCombinedTransformMatrix)。模型空间即模型师建模时的空间,如上所述,一般是模型两脚之间的中点,这时模型空间和世界空间是重合的,而根骨骼的Transform也是基于世界空间的。那如果想要得到子骨骼在世界空间的变化矩阵,我们要自下而上地通过一层层的矩阵变化。

有了蒙皮信息,完成了坐标空间的变换,下一个重要的步骤就是顶点混合。混合的意义是消除关节位置的缝隙,让动画更加自然。在蒙皮信息中的骨骼权重便是其中的关键数据,我们需要遍历影响该顶点的所有骨骼,计算出顶点的世界坐标,然后用骨骼权重进行加权平均。

Vworld = Vmesh * BoneOffsetMatrix1 * BoneCombindMatrix1 * BoneWeight1 + Vmesh * BoneOffsetMatrix2 * BoneCombindMatrix2 * BoneWeight2 + ... + Vmesh * BoneOffsetMatrixN * BoneCombindMatrixN * BoneWeightN

References:

  1. skinned-mesh-and-character-animation-with-directx9.pdf[https://www.gamedevs.org/uploads/skinned-mesh-and-character-animation-with-directx9.pdf]