《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个矩阵,并且可以合并成一个矩阵。合并后的矩阵就被称为关节仿射变换矩阵
(注意,JointPose存的不是矩阵,而是RTS,即1个四元数和2个向量。)
一个骨骼,就是所有关节仿射变换的集合:
即:
struct SkeletonPose {
size_t jointCount; // 关节数量
JointPose* localPoses; // 多个局部关节姿势
}
把
例如假设有
可以定义子关节j到父关节的变换为
全局关节姿势 Global Joint Poses
局部关节姿势是一种原始信息,实际上在渲染蒙皮动画前,需要做预处理,把局部关节姿势转换成全局关节姿势。
全局关节姿势变换,指的是把关节姿势,用模型空间坐标系表示。首先定义
每个关节j的全局关节姿势变换P,可以用刚才的
对每个关节都做一遍这个公式,就能得到一个全局关节姿势数组。然后就可以写入SkeletonPose:
struct SkeletonPose {
size_t jointCount; // 关节数量
JointPose* localPoses; // 多个局部关节姿势 JointPose
Matrix44* globalPoses; // 多个全局关节姿势
}
全局关节姿势的存储,并不只限定于用RTS,而是既可以用RTS也可以用矩阵。因为实时渲染里矩阵更通用快速,所以得存成矩阵。
绑定姿势矩阵、绑定姿势逆矩阵 Bind Poses Matrix 、Inversed Bind Poses Matrix
定义矩阵
反过来说,要把一个点或向量(想象下模型的任意一个顶点),变换到关节j的空间,就是:
再定义
然后再乘以当前姿势的姿势矩阵C(不是绑定姿势!),得到当前姿势的模型空间坐标
蒙皮矩阵 Skinning Matrix
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个关节的
mesh.inverse_bind_poses[j]就是
注意这里的乘法顺序和公式相反(公式是右乘,一般OGL程序中是用左乘)。
写作不易,您的支持是我写作的动力!

