骨骼动画与蒙皮矩阵

Tags: , ,

《Game Engine Architecture》这本书里最让我欣喜的就是动画相关的章节了,非常详细,比中文搜索引擎能搜到的资料都要系统、全面。据说作者以前就是做动画的。其他章节相对的只是抛砖引玉,例如阴影,只写了几页。

通过这本书并结合github上的ozz-animation源码,基本搞懂了骨骼蒙皮动画的核心原理。下面将简单做一份笔记。

基础概念

3D美术人员制作一个带骨骼动画的模型时,主要要分2个步骤,一是建模(可能包括画贴图),二是给模型加上骨骼并制作骨骼动画。

给一个模型加上骨骼前,一般要求这个模型摆成T字型,才方便动作师加骨骼和做动作。此时,加骨骼操作被称为骨骼绑定(Skeleton Binding);或者从模型角度讲叫做,模型蒙皮(Model Skinning)到骨骼。

这个初始骨骼摆位,就是绑定姿势(Bind Poses)。但要注意,Bind Poses本身只记录了骨骼各个关节的姿势信息,并不包括蒙皮信息。蒙皮信息是存储于模型数据里的,因为所谓蒙皮,即是让每一个顶点绑定至1-n个关节,这n个关节运动的时候,会影响到该顶点的当前位置。

局部关节姿势 Local Joint Poses

关节姿势分为局部关节姿势和全局关节姿势。先从局部关节姿势说起。

局部关节姿势是相对直属父关节而言的,可以用一个strcut表示:


struct JointPose {
    Quaternion rot; // R 关节旋转信息
    Vector3 trans;  // T 关节位移信息
    Vector3 scale;  // S 关节缩放信息
}

一个关节只需要存一组RTS信息。这3个信息可分别转换成3个矩阵,并且可以合并成一个矩阵。合并后的矩阵就被称为关节仿射变换矩阵Pj

Pj=[SjRj0Tj1]

(注意,JointPose存的不是矩阵,而是RTS,即1个四元数和2个向量。)

一个骨骼,就是所有关节仿射变换的集合:

Pskel=Pj|j=0N1

即:

struct SkeletonPose {
    size_t jointCount; // 关节数量
    JointPose* localPoses; // 多个局部关节姿势
}

Pj应用到关节j的局部坐标系的某个点或向量vj,就能把它变换到父关节p的坐标系:

vp=vjPj

例如假设有vj=(0,0,0),表示是关节j的局部坐标系的原点,Pj是一个Translate(100, 0, 0),那么Pjvj 的结果就是(100, 0, 0),即vp表示父关节p坐标系下的坐标(100,0,0)。

可以定义子关节j到父关节的变换为(PCP)j。这样的形式不太好看,可以换一种,先定义一个函数p(j),p(j)返回关节j的父关节索引。那么(PCP)j 可以写成 Pjp(j)

全局关节姿势 Global Joint Poses

局部关节姿势是一种原始信息,实际上在渲染蒙皮动画前,需要做预处理,把局部关节姿势转换成全局关节姿势。

全局关节姿势变换,指的是把关节姿势,用模型空间坐标系表示。首先定义p(0)M,即根关节的父节点为模型空间。

每个关节j的全局关节姿势变换P,可以用刚才的 Pjp(j)来表示:

P2M=P21P10P0M

PjM=i=j0Pip(i)

对每个关节都做一遍这个公式,就能得到一个全局关节姿势数组。然后就可以写入SkeletonPose:

struct SkeletonPose {
    size_t jointCount; // 关节数量
    JointPose* localPoses; // 多个局部关节姿势 JointPose
    Matrix44* globalPoses; // 多个全局关节姿势
}

全局关节姿势的存储,并不只限定于用RTS,而是既可以用RTS也可以用矩阵。因为实时渲染里矩阵更通用快速,所以得存成矩阵。

绑定姿势矩阵、绑定姿势逆矩阵 Bind Poses Matrix 、Inversed Bind Poses Matrix

定义矩阵 BjM为关节j在模型空间的全局绑定姿势矩阵。根据上文, vBjM 可以把v从关节j的局部空间变换到模型空间。

反过来说,要把一个点或向量(想象下模型的任意一个顶点),变换到关节j的空间,就是:

v(BjM)1

(BjM)1就是绑定姿势逆矩阵。也可写成:

(BjM)1=BMj

再定义vMB为模型任意顶点v在绑定姿势的模型空间坐标, 而 vMC 为在当前姿势的模型空间坐标。如果要求vMB在关节j的局部空间坐标vj,则公式为:

vj=vMBBMj=vMB(BjM)1

然后再乘以当前姿势的姿势矩阵C(不是绑定姿势!),得到当前姿势的模型空间坐标 vMC

vMC=vjCjM

蒙皮矩阵 Skinning Matrix

vMC=vjCjM=vMB(BjM)1CjM=vMBKj

Kj=(BjM)1CjM

Kj 就是关节j的蒙皮矩阵了。再解释下这个矩阵干了什么:把绑定姿势模型空间下的顶点,先转换到绑定姿势关节空间,然后再转换到当前姿势模型空间。

ozz-animation中的算K矩阵的代码片段:


for (size_t j = 0; j < models.Count(); ++j) {
    skinning_matrices[j] = models[j] * mesh.inverse_bind_poses[j];
}

models[j]就是当前姿势当前时刻第j个关节的CjM

mesh.inverse_bind_poses[j]就是(BjM)1,可见,这个逆矩阵是预先算好的,比运行时再算逆矩阵要快得多,一般的蒙皮动画引擎都是这样做。

注意这里的乘法顺序和公式相反(公式是右乘,一般OGL程序中是用左乘)。

(未经授权禁止转载)
Written on January 4, 2018

写作不易,您的支持是我写作的动力!