《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个矩阵,并且可以合并成一个矩阵。合并后的矩阵就被称为关节仿射变换矩阵\( P_{j} \):
\[ P_{j} = \left[ \begin{matrix} S_{j}R_{j}&0\\ T_{j}&1\\ \end{matrix} \right] \]
(注意,JointPose存的不是矩阵,而是RTS,即1个四元数和2个向量。)
一个骨骼,就是所有关节仿射变换的集合:
\[ P^{skel} = { P_{j} } | _{j=0}^{N-1} \]
即:
struct SkeletonPose {
size_t jointCount; // 关节数量
JointPose* localPoses; // 多个局部关节姿势
}
把\( P_{j} \)应用到关节j的局部坐标系的某个点或向量\( v_{j} \),就能把它变换到父关节p的坐标系:
\[ v_{p} = v_{j} P_{j} \]
例如假设有\( v_{j} = (0,0,0) \),表示是关节j的局部坐标系的原点,\(P_{j} \)是一个Translate(100, 0, 0),那么\( P_{j} v_{j} \) 的结果就是(100, 0, 0),即\( v_{p} \)表示父关节p坐标系下的坐标(100,0,0)。
可以定义子关节j到父关节的变换为\( (P_{C\to P})j \)。这样的形式不太好看,可以换一种,先定义一个函数p(j),p(j)返回关节j的父关节索引。那么\( (P_{C\to P})j \) 可以写成 \( P_{j\to p(j)} \)。
全局关节姿势 Global Joint Poses
局部关节姿势是一种原始信息,实际上在渲染蒙皮动画前,需要做预处理,把局部关节姿势转换成全局关节姿势。
全局关节姿势变换,指的是把关节姿势,用模型空间坐标系表示。首先定义\( p(0) \equiv M \),即根关节的父节点为模型空间。
每个关节j的全局关节姿势变换P,可以用刚才的 \( P_{j\to p(j)} \)来表示:
\[ P_{2\to M} = P_{2\to 1} P_{1\to 0} P_{0\to M} \]
\[ P_{j\to M} = \prod _{i=j}^{0} P_{i\to p(i)} \]
对每个关节都做一遍这个公式,就能得到一个全局关节姿势数组。然后就可以写入SkeletonPose:
struct SkeletonPose {
size_t jointCount; // 关节数量
JointPose* localPoses; // 多个局部关节姿势 JointPose
Matrix44* globalPoses; // 多个全局关节姿势
}
全局关节姿势的存储,并不只限定于用RTS,而是既可以用RTS也可以用矩阵。因为实时渲染里矩阵更通用快速,所以得存成矩阵。
绑定姿势矩阵、绑定姿势逆矩阵 Bind Poses Matrix 、Inversed Bind Poses Matrix
定义矩阵 \( B_{j\to M} \)为关节j在模型空间的全局绑定姿势矩阵。根据上文, \( \mathbf v B_{j\to M} \) 可以把\( \mathbf v \)从关节j的局部空间变换到模型空间。
反过来说,要把一个点或向量(想象下模型的任意一个顶点),变换到关节j的空间,就是:
\[ \mathbf v' (B_{j\to M})^{-1} \]
\( (B_{j\to M})^{-1} \)就是绑定姿势逆矩阵。也可写成:
\[ (B_{j\to M})^{-1} = B_{M\to j} \]
再定义\(\mathbf v _{M}^{B} \)为模型任意顶点v在绑定姿势的模型空间坐标, 而 \(\mathbf v _{M}^{C} \) 为在当前姿势的模型空间坐标。如果要求\(\mathbf v _{M}^{B} \)在关节j的局部空间坐标\( v_{j} \),则公式为:
\[ \mathbf v _{j} = \mathbf v _{M}^{B} B_{M\to j} = \mathbf v _{M}^{B} (B_{j\to M})^{-1} \]
然后再乘以当前姿势的姿势矩阵C(不是绑定姿势!),得到当前姿势的模型空间坐标 \(\mathbf v _{M}^{C} \):
\[ v _{M}^{C} = \mathbf v _{j} C_{j \to M} \]
蒙皮矩阵 Skinning Matrix
\[ v _{M}^{C} = \mathbf v _{j} C_{j \to M} = \mathbf v _{M}^{B} (B_{j\to M})^{-1} C_{j \to M} = \mathbf v _{M}^{B} K_{j} \]
\[ K_{j} = (B_{j\to M})^{-1} C_{j \to M} \]
\( K_{j} \) 就是关节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个关节的\( C_{j \to M}\)。
mesh.inverse_bind_poses[j]就是\( (B_{j\to M})^{-1} \),可见,这个逆矩阵是预先算好的,比运行时再算逆矩阵要快得多,一般的蒙皮动画引擎都是这样做。
注意这里的乘法顺序和公式相反(公式是右乘,一般OGL程序中是用左乘)。
写作不易,您的支持是我写作的动力!