SUN代码现看到了骨骼蒙皮动画模块,为防止自己记忆混淆,故记下文。(注:本文不涉及任何高级骨骼动画技术)

一:模型动画分类。

1:渐变动画。 这是个最基本的动画方式。它将动作每一关键帧所有顶点位置全部记录,其他帧数据按关键帧插值取得,没有可解释的价值。

2:关节动画。 这是一种特殊的渐变动画。它将一个实体对象拆分为多个Mesh,按照父子层次结构将这些Mesh统一组织为一个实体。 以Mesh为单位,每个子Mesh记录其父Mesh的权重影响比,父Mesh的运动带动其子Mesh的运动。 在关键帧中记录了每个子Mesh相对其父Mesh的变换比,从子到父层层累加矩阵变换,得到实体Mesh中每个组成Mesh在关键帧中的相对位置,然后转换为世界矩阵,然后以Mesh为单位Render。 这样做法最大问题是,由于各Mesh拥有自己的本地坐标系,在计算时稍加不注意,将会在Mesh连接处出现裂缝。

3:骨骼蒙皮动画。 这是一种现在最常用的Mesh动画方式。它是关节动画的一个优化方式,它将关节动画中的Mesh组整合为单一顶点,各自不再独立本地坐标系。 在关键帧中记录其子骨骼点相对父骨骼点的运动比,通过父子骨骼点累加矩阵变换,可得到一个在关键帧时的骨骼点各顶点位置。 然后,根据顶点混合动态计算蒙皮网格顶点的方式得到完整数据。 其他帧的数据也需要对骨骼节点进行插值计算后,再对蒙皮Mesh进行顶点混合。

二:骨骼蒙皮动画。

骨骼蒙皮重点包括两大部分,骨骼Bone和蒙皮SkinnedMesh。 其中骨骼方面要记录骨骼层次结构和骨骼在关键帧的数据信息,而蒙皮方面要记录蒙皮网格数据。 我们可以这样理解,我们每一帧让骨骼按照骨骼关键帧进行运动,蒙皮顶点受到多个骨骼影响拉伸改变位置,形成动作。每个骨骼记录着骨骼偏移矩阵,将蒙皮顶点从蒙皮空间转换为骨骼空间坐标。 例如:我们现在希望一个骨骼蒙皮动画的Mesh对象改变其世界坐标中的位置和朝向,我们只需要更改其根骨骼点的位置和朝向,那么,子骨骼点受到影响,计算得到其世界坐标位置,子骨骼又对其管理的蒙皮顶点进行影响,得到整个Mesh的新的世界坐标。

1:Bones 我们通常用以个4*4的矩阵来表示一个骨骼点,这样,我们不仅可记录骨骼的平移位置,又可记录骨骼的缩放和旋转。 下面是SUN骨骼点的基本信息结构

typedef struct
{
   WzVector m_vecPos;         // 骨骼点相对父骨骼的坐标
   union                      // 骨骼点相对世界的坐标
   {
      WxVector m_vecRot;
      WzQuaternion m_vecQuat;
   }
}WzTransform;

typedef struct
{
   int m_nParentNodeID;        // 父骨骼点唯一ID
   WzTransform m_wzCurrentTransform;
}WxNode;

该结构记录了骨骼点的层次关系和骨骼点的坐标。我们将骨骼按照层级关系摆放好之后,就获得了初始帧的骨骼点位置信息。但是我们依旧要将骨骼组放置到世界坐标中,此时我们会以根骨骼点做为世界坐标中的一个标准点。 即,假设我们将一个骨骼蒙皮Mesh看成一个点的话,那么这个点的坐标就是根骨骼点的坐标。通常这个点在人的盆骨附近,因为它居为人体骨骼的中心点,便于减少子骨骼点的层层计算。

2:BonesTransform 通过上面的说明,我们可以创建一套静态的骨骼和骨骼层次。但是我们需要动画,需要骨骼进行变化,这就需要更新骨骼矩阵。 因为骨骼的变换都是相对于父节点的变换,但是变换Mesh顶点,又是必须使用世界矩阵的变换,所以我们需要根据骨骼点的骨骼矩阵求出每个骨骼点的世界变换矩阵。而父骨骼点的变换又实时影响到了子骨骼点的世界坐标矩阵位置,所以,这里一定需要用递归,将每个骨骼点的世界矩阵坐标求到。

3:SkinMesh 我们之前获得了可以顺利动画的骨骼,但我们还需要Mesh根据骨骼进行运动才能真正产生一个动画。 我们知道Mesh是由一个个顶点构成的,在建模的时候,Mesh顶点是相对于本地矩阵的位置,和骨骼坐标完全没有关系,而在世界坐标系中,只有根骨骼坐标系才是蒙皮骨骼动画的标准位置,我们就需要一种途径将Mesh的本地矩阵转换为骨骼点的骨骼矩阵,再通过骨骼偏移矩阵求出整个Mesh在世界矩阵中的坐标。 下面是SUN中的蒙皮顶点信息,我们先来看下 #define MAX_BONE_FOR_SKINNING ( 4) // 每个蒙皮顶点最大接受4个骨骼影响,因为使用硬件加速,需要设置上限。

// 蒙皮顶点信息
typedef struct
{
BYTE m_byNumBlended;           // 影响到本蒙皮顶点的骨骼个数
int m_iNode[MAX_BONE_FOR_SKINNING];        // 每个骨骼点编号索引
float m_fWeight[MAX_BONE_FOR_SKINNING];       // 每个骨骼权重

WzVector m_wvOffset[MAX_BONE_FOR_SKINNING];      // 每个骨骼点偏移位置
} WzSkinningInfo;

我们可以看出SkinInfo中包含了影响这个MeshVectex的骨骼数目和骨骼ID以及这些骨骼分别对本顶点的权重(影响程度)。 这个结构的作用是使用各个骨骼点自身的变换矩阵对Mesh顶点进行变换(变换时需乘以权重),这样的话这个顶点就会受到多个骨骼的影响而发生位移。影响一个顶点的骨骼点权重和应当为1. 这样我们就获得了每帧与骨骼一同变换世界坐标系的蒙皮Mesh了。

4:骨骼偏移矩阵。 刚才我无意中提到了骨骼偏移矩阵,我想这里重复强调一下Mesh顶点变换过程分为2步: (1)Mesh顶点的本地坐标 * 骨骼偏移矩阵 -> Mesh点的骨骼空间坐标 (2)Mesh点的骨骼空间坐标 * 根骨骼(骨骼组)和世界的变换矩阵 -> Mesh点的世界空间坐标 此时我们又产生了一个问题,我们如何才能拿到骨骼偏移矩阵呢? 首先我们需要明白3DMAX下建模的时候,我们建Mesh模型的时候,假设将Mesh本地矩阵的原点设置为人的盆骨附近,同时Bone的骨骼矩阵原点也设为人的盆骨附近和Mesh矩阵原点设置在一起的话,那么根骨骼矩阵将与Mesh的矩阵达成一致,我们假设在这种条件下进行的运算(实际上美术也的确是这么做的)。 因为根骨骼点和Mesh本地矩阵原点坐标达成一致,那么他们在世界中的坐标也是一致,我们只需要解决掉根骨骼和其他子骨骼之间的变换就可以了,这样我们可以轻易得到一个公式。 Mesh本地坐标转为骨骼坐标偏移矩阵 = 子(本)骨骼变换矩阵 * 父骨骼变换矩阵 * 爷骨骼变换矩阵 * … * 根骨骼变换矩阵。 用Mesh本地坐标乘以上面的矩阵,则得到Mesh顶点在当前骨骼矩阵中的位置,再和根骨骼在世界坐标的变换矩阵,则能顺利获得了Mesh顶点在世界坐标中的坐标信息。 子骨骼和父骨骼的坐标关系在上面已经记录了相对坐标,这里就缺少一个Mesh顶点和影响它的骨骼顶点之间的位置关系了,于是我们还需要在Mesh顶点信息中加入相对于这些骨骼点的位置信息,即上面代码中的WzVector m_wvOffset[MAX_BONE_FOR_SKINNING];

5:总结 我们来回忆一下整个骨骼蒙皮动画的顶点转换过程: Mesh世界坐标 = (Mesh本地坐标 * Mesh相对影响骨骼点1的偏移矩阵 * 骨骼点1相对父骨骼的偏移矩阵 * 权重1) + (Mesh本地坐标 * Mesh相对影响骨骼点2的偏移矩阵 * 骨骼点2相对父骨骼的偏移矩阵 * 权重2) + …(所有影响该顶点的骨骼点)

三:总结

从上面叙述来看,一个完整的骨骼蒙皮动画包括:骨骼数据(层次数据),骨骼每关键帧的位置数据,Mesh顶点数据,骨骼相对世界坐标的偏移矩阵。 在程序运行开始,我们从资源中读取到骨骼层级结构数据,读取Mesh顶点数据,和骨骼每关键帧的位置数据,在运行时,实时计算骨骼相对世界坐标的偏移矩阵,求出所有Mesh在世界坐标系中的坐标。

……呼,搞定……明天开始SUN的骨骼蒙皮部分查阅和详细分析。