经过一段长篇大论,我们终于清楚骨骼和骨骼层次是啥了,但是为什么要将骨骼组织成层次结构呢?答案是为了做动画方便,设想如果只有一块骨骼,那么让他动起来就太简单了,动画每一帧直接指定他的位置即可。如果是 n 块呢?通过组成一个层次结构,就可以通过父骨骼控制子骨骼的运动,牵一发而动全身,改变某骨骼时并不需要设置其下子骨骼的位置,子骨骼的位置会通过计算自动得到。上文已经说过,父子骨骼之间的关系可以理解为,子骨骼位于父骨骼的坐标系中。我们知道物体在坐标系中可以做平移变换,以及自身的旋转和缩放变换。子骨骼在父骨骼的坐标系中也可以做这些变换来改变自己在其父骨骼坐标系中的位置和朝向等。那么如何表示呢?由于 4X4 矩阵可以同时表示上述三种变换,所以一般描述骨骼在其父骨骼坐标系中的变换时使用一个矩阵,也就是 DirectX SkinnedMesh 中的 FrameTransformMatrix 。实际上这不是唯一的方法,但应该是公认的方法,因为矩阵不光可以同时表示多种变换还可以方便的通过连乘进行变换的组合,这在层次结构中非常方便。在本文的例子 - 最简单的 skinned mesh 实例中,我只演示了平移变换,所以只用一个 3d 坐标就可以表示子骨骼在父骨骼中的位置。下面是 Bone Class 最初的定义:
class Bone
{
public :
float m_x , m_y , m_z ; // 这个坐标是定义在父骨骼坐标系中的
};
OK, 除了使用矩阵,坐标或某东西描述子骨骼的位置,我们的 Bone Class 定义中还需要一些指针来建立层次结构,也就是说我们要能通过父骨骼找到子骨骼或反之。问题是我们需要什么指针呢?从父指向子还是反之?结论是看你需要怎么用了。如果使用矩阵,需要将父子骨骼矩阵级联相乘,无论你的矩阵是左乘列向量还是右乘行向量,从哪边开始乘不重要,只要乘法中父子矩阵的左右位置正确,所以可以在骨骼中只存放指向父的指针,从子到父每次得到父矩阵循环相乘。也可以像DX中那样从根开始相乘并递归。在文本的DEMO中由于没用矩阵,直接使用坐标相加计算坐标,所以要指定父的位置,然后计算出子的位置,那么需要在 Bone Class 中加入子骨骼的指针,因为子骨骼有 n 个,所以需要 n 个指针吗?不一定,看看 DirectX 的做法,只需要两个就搞定了,指向第一子的和指向兄弟骨骼的。这样事先就不需要知道有多少子了。下面是修改后的 Bone Class :
class Bone
{
Bone * m_pSibling ;
Bone * m_pFirstChild ;
float m_x , m_y , m_z ; //pos in its parent's space
注意我增加了一个成员变量, Bone * m_pFather ,这是指向父骨骼的指针,在这个例子中计算骨骼动画时本不需要这个指针,但我为了画一条从父骨骼关节到子骨骼关节的连线,增加了它,因为每个骨骼只有第一子骨骼的指针,绘制父骨骼时从父到子画线就只能画一条,所以记录每个骨骼的父,在绘制子骨骼时画这根线。
以上的分析是通过将 mesh space 和 world space 重合得到 Offset Matrix 的计算方法。那么如果他们不重合呢?那就要先计算顶点从 mesh space 变换到 world space 的变换矩阵,并乘上(还是右乘为例) Combined Matrix 的 Inverse Matrix 从而得到 Offset Matrix 。但是这不是找麻烦吗?因为 Mesh 的原点在哪儿并不重要,为啥不让他们重合呢?
还有一个问题是,既然 Offset Matrix 可以计算出来,为啥还要在骨骼动画文件中同时提供 TransformMatrix 和 OffsetMatrix 呢?实际上文件中确实可以不提供 OffsetMatrix ,而只在载入时计算。但 TransformMatrix 不可缺少,动画关键帧数据一般只存储骨骼的旋转和根骨骼的位置,骨骼间的相对位置还是要靠 TransformMatrix 提供。在微软的 X 文件结构中提供了 OffsetMatrix ,原因是什么呢?我不知道。我猜想一个可能的原因是为了兼容性和灵活性,比如 mesh 并没有定义在世界坐标系,而是作为一个 object 放置在 3d max 中,在导出骨骼动画时不能简单的认为 mesh 的顶点坐标是相对于世界原点的,还要把这个 object 的位置考虑进去,于是导出插件要计算出 OffsetMatrix 并保存在 x 文件中以避免兼容性问题。