游戏中动画系统的分类
传统动画
游戏动画技术的前身是传统动画或手绘动画,此技术早期用于制作卡通动画。
这种动画的动感由连续快速显示一串静止的图片产生,这些图片称为帧
赛璐璐动画
赛璐璐动画对传统动画进行了拆分,把静态部分拿出来保持不变,每帧只绘制动态的部分。
赛璐璐动画的电子版本称为精灵动画,精灵是二维游戏时代主要的技术。
刚性层阶式动画
刚性层阶式动画的一个大问题是在关节位置产生裂缝
每顶点动画
动画师为网格的顶点添加动画,这中技术能产生任何想象的到的网格变形,然而,这个技术顶点数据非常大,在实时游戏中很少使用
变形目标动画
该技术是每顶点动画的变种,相当于使用每顶点动画制作多个固定的姿势,然后姿势之间使用线性插值进行顶点的计算。
蒙皮动画
蒙皮动画结合了刚性层阶式动画和变形目标动画的优势。
蒙皮动画由下面几部分组成
- 骨骼或者关节,起性质相当于刚性层阶式动画
- 姿势,相当于变形目标动画的姿势
- 蒙皮,关节进行动画过程中顶点如何受关节的影响而发生变化,蒙皮上的每个顶点可按权重绑定到多个关节,因此当关节移动时,蒙皮可以自然的拉伸。
蒙皮动画
骨骼
骨骼在蒙皮动画中通常由关节的层阶结构组成
而这个层阶结构通常是一个树结构,比如下面的关节结构可以表示人体的骨骼
Pelvis(髋关节/ 骨盘)
LowerSpine(脊椎下部)
MiddleSpine(脊椎中部)
UpperSpine(脊椎上部)
RightShoulder(右肩)
RightElbow(右肘)
RightHand(右手)
RightThumb(右拇指)
RightIndexFinger(右食指)
RightMiddleFinger(右中指)
RightRingFinger(右无名指)
RightPinkyEinger(右小指)
LeftShoulder(左肩)
LeftElbow(左肘)
LeftHand(左手)
LeftThumb(左拇指)
LeftIndexFinger(左食指)
LeftMiddleFinger(左中指)
LeftRingFinger(左无名指)
LeftPinkyFinger(左小指)
Neck(脖)
Head(头)
LeftEye(左眼)
RightEye(右眼)
多不面部关节
RightThigh(右大腿)
RightKnee(右膝)
RightAnkle(右脚踝)
LeftThigh(左大腿)
LeftKnee(左膝)
LeftAnkle(左脚踝)
通常会把关节赋予0~N-1的索引,因为每个关节只会有一个父关节,所以每个关节只需要保存父关节的索引即可保存整个关节结构。
下面是骨骼在内存中的表示
struct Skeleton
{
vector<string> names; // 保存每个关节的名称,便于调试
vector<int> parent; // 保存每个关节的父关节索引
vector<matrix4x4> poseInverse; // 每个关节的绑定姿势逆变换,后面介绍
}
绑定姿势
绑定姿势也称为参考姿势或者放松姿势,通常英文缩写有下面几种
- bindPose: 绑定姿势
- referencePose: 参考姿势
- restPose: 放松姿势
绑定姿势相当于初始姿势,每个运动姿势的三维运动相当于根据绑定姿势进行的运动
局部坐标系
通常来说,骨骼的运动是通过局部坐标系来定义的,所谓局部坐标系,就是每个关节都定义一个坐标系,子关节在当前关节的坐标系的运动称为局部运动。每个关节的运动也可以表示成下面三种变换:
- 平移变换
- 缩放变换
- 旋转变换
我们可以采用如下的结构在内存中表示每个姿势关节的局部运动:
struct JointPose
{
Quaternion mRotate; // 四元数表示旋转
Vector3 mTranslate; // 平移
Vector3 mScale; // 缩放
}
该结构通常称为SQT格式
采用四元数表示旋转是为了方便进行插值
同样,使用JointPose表示关节姿势而不是直接使用变换矩阵也是为了方便插值,并且存储数据会小一些
我们根据JointPose的数据可以计算出关节在局部坐标系的仿射变换矩阵
注意:放射变换可以认为把点或者矢量从一个坐标系变换到另一个坐标系
所以,当把关节姿势变换矩阵
P
j
P_j
Pj施于以关节
j
j
j坐标系表示的点或者矢量时,其变换结果是以父关节坐标系表示的该点或者矢量
因为前面讲过,关节是一个树形结构,所以,对于每个关节内的点,我们可以通过不断乘以父关节的姿势变换矩阵,把局部空间变换到模型空间
所谓的模型空间,可以理解为根关节所属的空间,根关节到模型空间的变换可以使用
P
0
−
>
M
P_{0->M}
P0−>M表示。
用数学公式表示关节
j
j
j的全局变换矩阵如下:
P
j
−
>
M
=
P
0
−
>
M
∏
i
=
1
j
P
i
−
>
i
−
1
P_{j->M} = P_{0->M}\prod_{i=1}^{j}P_{i->i-1}
Pj−>M=P0−>Mi=1∏jPi−>i−1
现在已经可以根据关节的局部变换获取到全局变换矩阵了,接下来,先介绍一下动画片段
动画片段
每个骨骼动画都可以包含多个动画片段,动画片段通常可以称为AnimationClip
显而易见的是动画片段表示的是关节的运动,但是该运动是离散的,就是每秒以一定的数量进行采样,一般是30/每秒或者60/每秒。采样的数据实际上就是上面我们介绍的关节的局部变换数据,即SQT数据。
所以,我们现在可以表示AnimationClip在内存的数据结构了:
struct AnimationSample
{
vector<JointPose> mJointPoses; // 关节的JointPose结构
}
struct AnimationClip
{
bool isLoop; // 是否循环
vector<AnimationSample> mSamples; // 采样点
int count; // 采样点数量
float fps; // 采样率
}
值得注意的是,虽然采样点是离散的,但是对于任意的时间点,我们可以通过包含该时间点的前后两个采样点进行插值就可以快速计算出当前时间点的JointPose结构,这也是为什么使用SQT结构保存关节姿势的局部变换
蒙皮
蒙皮用的网格是通过其顶点系上骨骼的。每个顶点可绑定至一个或者多个关节。
所以每个顶点的数据会多包含下面的数据:
- 该顶点要绑定的关节索引
- 每个关节索引还需要一个权重因子,以表示关节对顶点的影响力
注意:一般情况下,每个顶点绑定的所有关节权重因子的和为1
一般情况下,每个顶点绑定的关节索引和权重因子在动画过程中不变
现在,可以大体通过下面的数据结构表示蒙皮顶点:
struct Vertex
{
float position[3]; // 位置
float normal[3]; // 法线
float uv[2]; // 纹理坐标
short jointIndex[4]; // 关节索引
float jointWeight[4]; // 关节权重
}
蒙皮矩阵
接下来就是最重要的一步了,实现顶点的变换
先来考虑几点显而易见的事情:
- 顶点的位置应该是相对于模型空间的,变换后的位置也应该是相对于模型空间的。
- 关节的运动是相对于绑定姿势的
- 根据前面的介绍,我们使用全局变换矩阵公式可以把顶点从局部空间转换到模型空间
根据上面的介绍,我们发现缺少了一环,那就是我们在变换之前,需要先把顶点从模型空间变换到关节局部空间,因为顶点是相对于模型空间的。如果先不考虑关节索引和权重的话,顶点的变换步骤应该如下:
- 顶点从模型空间变换到关节局部空间
- 顶点在关节局部空间进行平移缩放旋转等变换
- 变换完成后把顶点从局部空间变换到模型空间
可以发现,第一步的变换与关节有关,并且因为绑定姿势是固定的,所以这个变换也是固定的,而这个变换就是我们在定义Skeleton时定义的poseInverse,即绑定姿势逆变换,用 B M − > j B_{M->j} BM−>j表示。
那么顶点的变换就可以用下面的公式表示:
V
=
V
0
∗
B
M
−
>
j
∗
P
j
−
>
M
V = V_0\ *\ B_{M->j}\ *\ P_{j->M}
V=V0 ∗ BM−>j ∗ Pj−>M
其中 K j = B M − > j ∗ P j − > M K_j = B_{M->j}\ *\ P_{j->M} Kj=BM−>j ∗ Pj−>M即称为蒙皮矩阵
加入关节索引和权重
到这里,我们已经计算出了每个关节在特定时间点的蒙皮矩阵,通常,蒙皮矩阵的计算在CPU阶段进行,每个关节的蒙皮矩阵和每个顶点的关节索引,关节权重则传到GPU,具体的顶点变换在GPU进行
因为只是简单的加权平均,所以计算公式比较简单:
V
=
∑
i
=
0
N
−
1
w
i
j
∗
V
0
∗
K
j
i
V = \sum_{i=0}^{N-1}w_{ij}\ *\ V_0\ *\ K_{j_i}
V=i=0∑N−1wij ∗ V0 ∗ Kji