/ Ch13 动画系统

第13章:动画系统 — 零基础讲义

讲义说明
本讲义基于 Jason Gregory 所著《Game Engine Architecture Volume II》第4版第13章(Animation Systems, p.236-334),逐节翻译推导。
本版插画由 ImageGen + Guizang 材质插画标准流程生成。

学习目标

  1. 理解骨骼、关节、姿态的基本概念
  2. 掌握蒙皮(Skinning)的数学原理——骨骼如何驱动顶点
  3. 理解动画混合(Blending)的核心算法
  4. 了解动作状态机(ASM)的基本架构
  5. 了解动画压缩和动作匹配(Motion Matching)

13.1 角色动画类型

13.1.1 四种动画技术

13.1.1.1 从关键帧到程序化

类型原理用途
骨骼动画骨架驱动蒙皮网格角色行走/奔跑/战斗(本章重点)
顶点动画(Morph Target)顶点位置直接插值面部表情、口型同步
物理动画(Ragdoll)物理模拟驱动骨骼死亡倒地、受击反应
程序化动画(IK等)算法实时计算姿态脚踩地形、手抓物体

13.1.1.2 关键帧动画 vs 骨骼动画

关键帧动画(2D/老式3D): 美术师设定几个关键时刻的姿态,中间帧自动插值。适用于简单动画。骨骼动画(现代3D): 角色有一副内部骨架,动画数据是骨骼的旋转/平移信息,通过蒙皮驱动外层网格——这是本章的核心内容。


13.2 骨骼

骨骼蒙皮
图13-1:骨骼→姿态→蒙皮流程(Guizang流程生成)

13.2.1 骨骼层级结构

13.2.1.1 关节与骨骼

骨骼实际上是一个层级化的关节(Joint)树。每个关节有:父关节索引(根关节没有父)、绑定姿态的局部变换(相对于父关节)、逆绑定矩阵(把顶点从模型空间转到关节局部空间)

// 简化的骨架结构
struct Joint {
    int       parentIndex;     // 父关节(根=-1)
    Matrix4x4 invBindPose;     // 逆绑定姿态矩阵
    string    name;            // "Spine1", "LeftElbow" 等
};

// 典型的角色骨架层级
Root (Hips)
 ├── Spine1
 │    └── Spine2
 │         └── Neck → Head
 ├── LeftUpperLeg → LeftLowerLeg → LeftFoot
 └── RightUpperLeg → RightLowerLeg → RightFoot

13.2.2 绑定姿态(Bind Pose)

13.2.2.1 "T-Pose"或"A-Pose"

绑定姿态是美术师创建模型时的默认姿态——角色双臂展开(T-Pose)或自然下垂(A-Pose)。所有蒙皮权重以这个姿态为基准计算。动画的每一帧数据都是相对于绑定姿态的偏移


13.3 姿态

13.3.1 姿态的表示

13.3.1.1 局部姿态 vs 全局姿态

// 局部姿态:每个关节相对于父关节的变换
struct LocalPose {
    Quaternion rotation;   // 相对于父关节的旋转
    Vector3    translation; // 相对于父关节的平移
    Vector3    scale;      // 通常为 (1,1,1)
};

// 全局姿态:从递归计算得出——第5章学过的变换链
Matrix4x4 CalcGlobalPose(int jointIdx) {
    if (jointIdx == 0)  // 根关节
        return localPoses[0].ToMatrix();
    int parent = skeleton[jointIdx].parentIndex;
    return CalcGlobalPose(parent) * localPoses[jointIdx].ToMatrix();
}

13.4 动画片段

13.4.1 动画数据的结构

13.4.1.1 一个动画片段包含什么

// 动画片段 = 每根骨骼在每个关键帧的姿态信息
struct AnimationClip {
    float       duration;        // 时长(秒)
    float       frameRate;       // 采样率(通常 30fps)
    int         boneCount;       // 骨骼数量
    BoneTrack*  tracks;          // 每条骨骼的动画曲线
};

struct BoneTrack {
    Keyframe*   rotations;       // 四元数旋转关键帧
    Keyframe*   translations;    // 平移关键帧(通常只根骨骼有)
};
// 关键帧之间通过插值(LERP/SLERP)得到中间姿态

13.5 蒙皮

13.5.1 线性混合蒙皮(LBS)

13.5.1.1 核心公式

// 蒙皮:每个顶点 = 多个骨骼对它影响的加权和
for each vertex v:
    Vector3 skinnedPos = (0,0,0);
    for each bone b affecting v (通常 4 根):
        skinnedPos += v.bindPos * bonePalette[b] * weight[b];
    // bonePalette[b] = 全局姿态矩阵 × 逆绑定姿态矩阵

13.5.1.2 "糖果纸效应"问题

LBS 的经典缺陷:前臂旋转时肘部顶点会"内陷"(像糖果纸被拧)。解决方案:双四元数蒙皮(Dual Quaternion Skinning)——用双四元数替代矩阵,保持体积。代价略高但效果明显改善。


13.6 动画混合

13.6.1 混合类型

13.6.1.1 四种常见混合

混合类型做法用途
线性插值混合两个片段之间 LERP/SLERP走→跑过渡
加法混合(Additive)偏移量叠加到基础姿态上边跑边挥手、呼吸起伏
分层混合(Layered)上半身和下半身不同动画跑步+开枪、走路+挥手
交叉淡入淡出重叠时间窗内渐变权重动画切换时防"跳帧"

13.6.1.2 混合树示例

// 简单的1D混合:根据速度在Idle/Walk/Run之间混合
float blend = (speed - walkSpeed) / (runSpeed - walkSpeed);
Pose blendedPose = Lerp(walkPose, runPose, blend);

// 2D混合:根据水平/垂直速度在8个方向动画之间混合
Pose blendedPose = Blend2D(
    forwardPose, backwardPose, leftPose, rightPose,
    velocity.x, velocity.y
);

13.7 动画状态机

13.7.1 动作状态机(ASM)

13.7.1.1 状态 + 过渡

// 典型的角色动画状态机
[Idle] ←→ [Walk] ←→ [Run] ←→ [Sprint]
   ↕         ↕
[Jump]    [Attack1] → [Attack2] → [Attack3]
   ↓
[Fall] → [Land]

// 过渡条件
Idle → Walk: speed > 0.1
Walk → Run: speed > 3.0 且 按着Shift
Run → Sprint: speed > 6.0 且 按着Shift
Any → Jump: 按下空格 且 在地面上

本章核心洞察

🔑 动画 = 时间驱动 + 骨骼层级 + 混合艺术
动画系统是"时间艺术"在代码中的体现——它在每一帧把时间参数映射为几十根骨骼的姿态,再通过蒙皮驱动数万个顶点。它的工程挑战不在"某一帧",而在帧之间的平滑过渡——动画混合、状态机、交叉淡入淡出都是为了解决"不让玩家看到切换"的问题。

三条动画准则:
1. 四元数是骨骼动画的"第一公民": 存储和插值都用四元数
2. 混合比单独播放更复杂但更重要: 现代游戏中几乎没有"纯粹播一个动画片段"的时刻
3. IK让动画适应环境: 纯数据动画无法处理地形高度变化——IK 是让脚"踩在地面上"的答案

📝 课后练习题(含答案)

题1骨骼层级结构中,父关节变换如何影响子关节?

子关节的全局变换 = 父关节全局变换 × 子关节局部变换。动肩膀(父),整个手臂(子)跟着动——这就是层级变换链。这和第五章学的世界变换链是同一个原理。

题2线性混合蒙皮(LBS)的公式是什么?"糖果纸效应"怎么解决?

公式:v' = Σ(wi × Mi × v),其中 Mi=骨骼i的蒙皮矩阵,wi=权重。糖果纸效应源于LBS在旋转插值时的体积塌缩——用双四元数蒙皮(DQS)替代矩阵插值来解决。

题3加法混合(Additive Blending)和常规插值混合有什么不同?

常规混合:两个完整姿态取加权平均。加法混合:基础姿态 + 偏移姿态的差值。好处:"边跑边挥手"——跑是基础动画,挥手是叠加在上面的偏移,两者互不影响。

题4动作状态机(ASM)中为什么需要"过渡时长"?

如果状态瞬间切换(如Idle直接跳到Run),动画会"跳帧"——视觉上角色突然变了姿态。过渡时长(如0.2s)在两个动画之间做交叉淡入淡出,让切换平滑自然。


🤖 qAagent 零基础问答区

概念IK(逆向运动学)和FK(正向运动学)有什么区别?

FK: 从上往下算——"肩膀转了30°→肘在X→腕在Y"(已知关节角度求末端位置)。IK: 从下往上算——"手要碰到桌子上的杯子→肘应该在Z→肩膀应该转θ°"(已知末端目标求关节角度)。FK是"我动了这里所以那里到了"。IK是"我要那里到那里所以这里应该这样动"。IK在游戏中用于脚踩不同高度地面、手抓物体等自适应场景。

实践动画压缩有什么用?怎么做到10:1的压缩比还看不出来?

一个30fps的60秒动画,50根骨骼,每根4个通道(四元数),= 360KB未压缩。压缩技巧:① 去掉不动的通道(很多骨骼只有旋转,没有平移)② 降低精度(四元数从32bit降到16bit,误差 < 0.001)③ 关键帧剔除(连续帧值差不多就只保留端点)④ 曲线拟合(用贝塞尔曲线替代密集采样点)。综合可达 10:1-20:1 压缩比。