《游戏引擎架构》动画的基本构成

闲话

近日读完了《游戏引擎架构》这本书,以前感觉读不完,但是每天慢慢的磨完了。闲下来时间提炼一下其中自我觉得比较关键的部分。因本人是做Unity开发的,所以会把书中所讲部分结合Unity引擎来总结。闲话不多说开始吧

动画的基本构成

Unity的动画系统封装的已经非常好了,所以我们对其中的细节了解的少之又少。本文章着重在单个动画,骨骼,姿势,蒙皮的内存存储结构和应用方式

角色动画的类型

在Unity中比较常用的动画为蒙皮/骨骼动画(三维)和精灵动画(二维)

  • 赛璐璐动画(精灵动画):用一张细小的位图,叠在全屏的背景影响之上而不会扰乱背景。常用于二维游戏动画(Unity中的Sprite贴图)
  • 动画纹理:面向摄像机的四边形,并用一连串的位图连续播放。现今用于远景活低分辨率的物体
  • 刚性阶层动画:将角色通过部位进行拆分建模并以层级进行约束。但问题是在关节处会出现裂缝
  • 每顶点动画:移动每个顶点以产生更自然的动作(蛮力技术,数据量非常大。通常用于老式的离线渲染)
  • 变形目标动画:每顶点动画的变种,也是制作每个顶点的位置。但制作少量的固定极端姿势在运行时将其混合(线性插值混合)。通常用于面部动画
  • 蒙皮/骨骼动画:
    • 骨骼:隐藏的刚性关节层阶结构(树结构)所构成
    • 皮肤:绑定于骨骼上的圆滑三角形网格,顶点会按权重绑定至多个关节。当关节移动时,蒙皮可以自然的拉伸

为什么选择蒙皮/骨骼动画? 以更少的数据量来达到更好的效果。通过加入约束:相对大量的顶点只能跟随相对少量的骨骼关节移动。来压缩顶点动画

骨骼在内存中的表示

通常使用关节索引引用关节,子关节索引引用父关节。蒙皮三角形网格中,每个顶点索引引用其绑定关节

 1// 关节数据的信息
 2struct Joint {
 3    Matrix4x3 inv_bind_pose;    // 绑定姿势(蒙皮网格顶点绑定至骨骼时,关节的位置,定向及缩放)的逆变换
 4    const char* name;           // 关节名字(字符串或32位字符串散列表标识符)
 5    U8 parent;                  // 父索引(0xFF代表根关节)
 6};
 7
 8struct Skeletion {
 9    U32 joint_count;    // 关节数目
10    Joint* joint;       // 关节数组
11};

姿势

把角色摆出一连串离散,静止的姿势,并以通常30或60个姿势每秒的速率显示,已产生动感。实际游戏会以相邻姿势进行插值

绑定姿势:又称为T姿势。因此姿势四肢远离身体,较容易把顶点绑定至关节

局部姿势

相对于父关节指定的,其仿射变换相对于父节点空间

关节姿势:数学上就是一个仿射变换。4x4仿射变换矩阵$P_j$,此矩阵由平移矢量$T_j$,3x3对角缩放矩阵$S_j$,及3x3旋转矩阵$R_j$构成

$$ p_j = \begin{bmatrix} S_jR_j & 0 \newline T_j & 1 \end{bmatrix} $$

整个骨骼的姿势:

$$ P^{skel} = { P_j } |_{j=0}^{N-1} $$

有些引擎不允许关节$S_j$缩放,有些引擎则必须为同一缩放。此优化能节省内存。并简化每个关节计算平截头体剔除及碰撞测试

 1// 局部关节的内存表示
 2struct JointPose {
 3  Quaternion rot; // Q
 4  Vector3 trans;  // T
 5  F32 scale;      // S(仅为统一缩放)
 6};
 7
 8// 骨骼姿势:其所有关节姿势的集合
 9struct SkeletonPose {
10  Skeleton* pSkeleton;    // 骨骼 + 关节数量
11  JointPose* aLocalPose;  // 多个局部关节姿势
12}

全局姿势

关节姿势表示为相对于模型空间或世界空间。关节j的空间姿势(全局姿势),可通过从该关节遍历至根节点并乘上其局部姿势

任何关节$j$的全局姿势(关节至模型空间的变换)可写成:

$$ P_{j \rightarrow M} = \prod_{i=j}^0 P_{i \rightarrow p(i)} $$
1// 全局关节的内存表示
2struct SkeletonPose {
3  Skeleton* pSkeleton;    // 骨骼 + 关节数量
4  JointPose* aLocalPose;  // 多个局部关节姿势
5  Matrix44* aGlobalPose;  // 多个全局关节姿势
6};

动画片段

  • 每个动画片段是为特定骨骼设计的,通常不会用于其他骨骼
  • 动画重定目标:把为一个骨骼设计的动画,重订目标至不同骨骼

局部时间线

每个动画各自的时间线(AnimationClip)。时间索引$t$从$0$到$T$,$T$​为片段的持续时间

动画师在指定的时间点上设定一些关键姿势或关键帧,然后对应不同的时间索引$t$会用线性差值计算采样

帧:指一段时间,如1/30s或1/60s

采样:代指某时间点

相位:归一化表示时间单位,无论$T$多长,0代表动画开始,1代表结束

全局时间线

每个角色都有一个全局时间线(类似于Unity的Timeline)

播放动画可以简单的理解为把片段的局部时间映射到角色的全局时间

调整播放速率:把片段置于全局时间线之时缩放其比例,加快2倍播放=缩放1/2的局部时间片段

倒转播放:时间比例设置为-1

动画片段映射到全局时间线需要的信息:

  • 全局起始时间
  • 播放速率R
  • 持续时间T
  • 循环次数N

同步动画

  • 局部时间线同步动画:必须在完全相同的游戏帧数播放(实现比较麻烦)
  • 全局时间线同步动画:只要开始时间相同就可以完全同步

内存中的表示方法

采样由骨骼中的每个关节的完整姿势所组成。存储为SQT格式

如果缩放为标量。则一个未压缩的动画至多10个通道

  • 平移:三维矢量$V=\begin{bmatrix} V_x & V_y & V_z \end{bmatrix}$
  • 旋转:四元数$Q=\begin{bmatrix}Q_x & Q_y & Q_z & Q_w \end{bmatrix}$
  • 缩放:标量$S$
 1struct AnimationSample {
 2  JointPose *aJointPose;  // 关节姿势数组
 3};
 4
 5struct AnimationClip {
 6  Skeleton *pSkeleton;      // 骨骼关节(真实引擎中可能用骨骼标识符,而不是指针)
 7  F32 framesPerSecond;      // 帧每秒
 8  // 注释:非循环动画为frameCount + 1,循环动画最后一采样等于第一个采样会略去
 9  U32 frameCount;           // 采样数目
10  AnimationSample *aSample; // 采样数组
11  bool isLooping;           // 是否循环
12}

通道函数(Unity的Animation编辑面板)

函数通道在整个动画片段的时间线上是圆滑连续的(除非故意编辑成不连续的,例如镜头切换)。而游戏引擎中基本只会在采样间进行线性插值,实际上用到的是连续函数的分段线性逼近

可以加入额外的元通道数据。把游戏专用的信息编码,能和动画同步触发。如事件触发器(Unity的Animation面板中可触发事件)在动画脚步落地的时候播放声音

定位器:利用Maya中的定位器(类似于Unity中模型中的一个子节点,如手中武器的父节点)用于记录物体的位置及定向

蒙皮

把三维网格顶点联系至骨骼的过程

蒙皮信息:每个顶点可绑定至一个或多个关节,绑定至一个关节则完全跟随此关节移动。若绑定多个关节则等于多个关节位置的加权平均

  • 顶点绑定到(一个或多个)关节的索引
  • 对于每个绑定的关节,提供一个权重因子(权重之和为1),表示该关节对最终顶点位置的影响力

对于绑定的关节数目通常限制为每顶点4关节,首先4个8位索引可包装位32位字。其次超过4个质量差别就没有明显提升了

内存数据结构:

1struct SkinnedVertex {
2    float position[3];		// (Px,Py,Pz)
3    float normal[3];		// (Nx,Ny,Nz)
4    float u,v;				// 纹理坐标uv
5    U8 jointIndex[4];		// 关节索引
6    float jointWeight[3];	// 关节权重,略去一个可用1-其他求得
7}

蒙皮矩阵

将网格顶点从模型空间的原来位置(绑定姿势)变换至骨骼模型空间的当前姿势。非基变更变换

顶点绑定至关节的位置时,在该关节空间中的位置时不变的。所以可把顶点于模型空间的绑定姿势位置转换至关节空间,再把关节空间移至当前姿势,最后把该顶点转回模型空间。这个转换过程结合的转换矩阵就是蒙皮矩阵

单个关节的蒙皮矩阵

绑定姿势顶点的模型空间位置为$V_M^B$。矩阵$B_{j \rightarrow M}$把点或者矢量从关节$j$空间变换至模型空间。则矩阵$B_{M \rightarrow j}$就是从模型空间到关节$j$空间的变换矩阵。而$B_{M \rightarrow j} = (B_{j \rightarrow M})^{-1}$。则关节空间的顶点公式为:

$$ V_j = V_M^B B_{M \rightarrow j} = V_M^B(B_{j \rightarrow M}^{-1}) $$

矩阵$C_{j \rightarrow M}$表示关节空间转换到当前姿势的模型空间。则当前姿势顶点的模型空间位置$V_M^C$公式为:

$$ V_M^C = V_jC_{j \rightarrow M} $$

则联合后的蒙皮矩阵$K_j$为:

$$ V_M^C = V_jC_{j \rightarrow M} = V_M^B(B{j \rightarrow M})^{-1}C_{j \rightarrow M} = V_M^BK_j $$
$$ K_j =(B_{j \rightarrow M})^{-1}C_{j \rightarrow M} $$
多个关节的蒙皮矩阵

将单关节蒙皮矩阵扩展至多关节须计算矩阵调色板,就是一组蒙皮矩阵$K_j$,当中每个矩阵对应第j个关节。当渲染一个蒙皮网络时,矩阵调色板便要传送至渲染引擎。渲染器会为每个顶点查找调色板中合适的关节蒙皮矩阵,并用该矩阵把顶点从绑定姿势转换至当前姿势

  • 当前姿势矩阵$C_{j \rightarrow M}$需要每帧更新
  • 绑定姿势的逆矩阵是常量,计算后缓存在骨骼信息中
  • 动画引擎计算每个关节的局部姿势$C_{j \rightarrow p(j)}$,然后转换至全局姿势$C_{j \rightarrow M}$,最后把全局姿势乘以对应的绑定姿势逆矩阵$(B_{j \rightarrow M})^{-1}$,以生成每个关节的蒙皮矩阵$K_j$

运算效率优化:将蒙皮矩阵调色板预先乘于物体的模型至世界变换。因为最终都要转换到世界空间,所以可以预处理。但在多角色同时播放单个动画的情况时不能这么做。此技术为动画实例

(完)