骨骼、姿势与动画片段
Skeletons, Poses & Animation Clips
角色动画系统的数据核心是三样东西:骨骼(关节层级)、姿势(每个关节的 SRT 变换)和动画片段(姿势随局部时间变化的采样)。GEA 把各种动画方法看成对"逐点控制表面"这一理想的不同压缩方式;骨骼蒙皮用少量关节驱动大量顶点,是今天最主流的方案(GEA4 卷 II 13.1–13.4)。
四类角色动画
| 方法 | 做法 | 优点 | 问题 |
|---|---|---|---|
| 赛璐珞/精灵动画 (Cel / Sprite) | 快速轮播一组位图;可循环的称为循环动画,如跑步循环 (run cycle) | 简单 | 只适合 2D 或远景(Doom 的怪物就是朝向相机的四边形 + 动画纹理) |
| 刚性层级动画 (Rigid Hierarchical) | 角色由刚性部件组成,按层级约束 | 适合机器人、机械 | 关节处"开裂" |
| 逐顶点动画与变形目标 (Per-Vertex / Morph Target) | 逐顶点导出运动;变形目标只存少数极端姿势,运行时 lerp | 任意形变;面部(约 50 块肌肉)常用 | 逐顶点数据量太大,实时少用 |
| 蒙皮动画 (Skinned) | 隐藏的骨骼驱动一张连续的皮肤网格,每个顶点可受多个关节加权影响 | 兼具形变能力与刚性层级的效率 | 见 顶点混合与蒙皮 |
GEA 说蒙皮动画最早由《超级马里奥 64》这类游戏使用,至今仍是游戏和电影的主流(侏罗纪公园的恐龙、Solid Snake、咕噜、Nathan Drake、巴斯光年、Marcus Fenix、Joel 等都全部或部分用蒙皮动画制作)。补充:《超级马里奥 64》游戏内的马里奥模型是一组刚性网格段,每个顶点只属于一个骨头,只在父子骨头交界处共享顶点来拉伸(N64 只支持"二值"蒙皮,见 fast64 SM64 导出工具 README),还不是现代那种逐顶点多关节加权的皮肤;标题画面可拖动的马里奥脸(Goddard 子系统)才是加权蒙皮。也有工作室用含数百关节的面部绑定替代变形目标,或两者结合:关节给出主姿势,变形目标做细微修正(卷 II 13.1.3–13.1.4)。
动画方法即数据压缩
最灵活的系统是控制表面上每个无穷小点,数据量无限。只动顶点是第一层压缩;变形目标再约束顶点只能沿预定义位置之间的直线移动;骨骼动画则约束大量顶点跟随少量关节。挑选方法时可以像挑视频编码一样,选在可接受瑕疵下压缩率最高的那种。肢体基本像刚体,骨骼压缩率高;面部各顶点运动更独立,要逼真需要的关节数接近顶点数,压缩优势消失,这是面部常用变形目标的原因之一(另一个原因是动画师更习惯)(卷 II 13.1.5)。
骨骼
- 骨骼由关节 (joint) 组成。业界常把 joint 和 bone 混用,但严格说动画师操纵的是关节,"骨头"只是关节之间的空隙;骨盆一个关节连四个子关节,看上去就伸出四根骨头。引擎只关心关节(卷 II 13.2)。
- 关节编号 0…N−1,每个关节只存父索引,根关节父索引为 −1 之类的无效值。数组中子关节总排在父关节之后,所以 0 号总是根。用 16 位索引引用关节,比按名字引用省空间、查找快(卷 II 13.2.1–13.2.2)。
- 每个关节通常存:名字(字符串或 32 位哈希 string id,见 字符串与唯一标识符)、父索引、逆绑定姿势矩阵。
struct Joint {
Matrix4x3 m_invBindPose; // 逆绑定姿势
const char* m_name;
U16 m_iParent; // 父索引,根为 0xFFFF
};
struct Skeleton { U32 m_jointCount; Joint* m_aJoint; };
- 原书问题(书内前后不一致):GEA 说 16 位关节索引"可以接受每个骨骼最多 65,536 个关节",但同一段代码把
0xFFFF留作根关节的"无父"标记,所以实际可用的索引最多 65,535 个(0…0xFFFE)。实践中无影响。
姿势
- 关节姿势是关节相对某参考系的位置、朝向和缩放,用 4×4 / 4×3 矩阵或 SRT(缩放、四元数旋转、平移)表示;骨骼姿势就是全部关节姿势的数组(卷 II 13.3)。
- 绑定姿势 (bind pose),又叫参考姿势、静止姿势、T 姿势:网格绑定到骨骼之前的姿势,四肢张开便于绑定(卷 II 13.3.1)。
- 局部姿势:相对父关节,几乎总以 SRT 存储(便于混合)。每个关节其实定义了一个坐标系,即关节空间。数学上关节姿势是一个仿射变换 ,由平移 、3×3 对角缩放矩阵 和 3×3 旋转矩阵 组成,它把关节空间的点变到父空间,记作 (卷 II 13.3.2)。
- 关节缩放:有的引擎不允许缩放,有的只允许均匀缩放(一个标量),有的允许非均匀缩放(三元组),几乎没有允许切变的。限制为均匀缩放既省内存,又保证关节的包围球不会变成椭球,简化逐关节的视锥和碰撞测试(卷 II 13.3.2.1)。
- 局部姿势可看作基变换: 把关节 空间里表示的点或向量变到父关节空间;反方向(父到子)就是它的逆,(卷 II 13.3.2.3)。
- 允许非均匀缩放时,GEA 把
JointPose改成Quaternion m_rot; Vector4 m_trans; Vector4 m_scale;(卷 II 13.3.2.2)。 - 全局姿势:关节到模型空间的变换,沿层级往根连乘局部姿势,根的父空间定义为模型空间:
(卷 II 13.3.3,式 13.1)
struct JointPose { Quaternion m_rot; Vector3 m_trans; F32 m_scale; }; // 四元数放首位以保证对齐与紧凑
struct SkeletonPose {
Skeleton* m_pSkeleton;
JointPose* m_aLocalPose; // 局部姿势
Matrix44* m_aGlobalPose; // 全局姿势
};
动画片段
电影可以把整场戏作为一个连续序列来做;游戏是交互的,角色运动必须拆成成千上万个细粒度的动画片段 (clip):有的循环(走、跑),有的只播一次(投掷、摔倒),有的影响全身,有的只影响局部。例外是非交互段落:游戏内过场 (IGC)、非交互序列 (NIS) 由引擎实时渲染,全动态视频 (FMV) 是预渲染的视频;快速反应事件 (QTE) 是半交互的变体(卷 II 13.4)。引擎内过场工具可参考 Sequencer 过场动画。
局部时间线
- 每个片段有局部时间 。动画师只做关键姿势,其余由线性或曲线插值补出,所以时间线是连续的:游戏帧率波动、片段可变速,玩家实际看到的可能是第 1.1、1.9、3.2 帧;负的时间缩放可倒放(卷 II 13.4.1.1)。
- 时间单位最好用秒(浮点、定点,或计量极小子帧间隔的整数),不要用整数帧计数(卷 II 13.4.1.2)。
- 帧与样本:GEA 作者用"样本"指时间点,"帧"指 1/30 或 1/60 秒的时段。30 fps 的 1 秒动画有 30 帧、31 个样本。非循环片段 N 帧有 N+1 个样本;循环片段最后一个样本与第一个重合,所以只存 N 个(卷 II 13.4.1.3–13.4.1.4)。
- 归一化时间(相位) :同步不同时长的片段很方便。例如从 2 秒的跑循环交叉淡入 3 秒的走循环,令 并以相同的归一化速率推进,脚步就能对上(卷 II 13.4.1.5)。
全局时间线与两种时钟
播放片段就是把局部时间线映射到角色的全局时间线 上。已知起始时间 、播放速率 、时长 、循环次数 (卷 II 13.4.2,式 13.2):
- 不循环: 夹到 ;无限循环:;有限循环:先夹到 再 。
- 原书问题:GEA 说取模"用 mod 运算符,即 C/C++ 的
%",但 C/C++ 的%只接受整数操作数,浮点时间要用std::fmod;而fmod结果与被除数同号(cppreference:std::fmod、算术运算符), 时会得到负值,需要再加 才落在 。同理,GEA 图 13.19 用 倒放、局部时间从 开始,但式 13.2 的 在 时给出负的 ,直接夹到 会一直停在 0;倒放时应写成 (或先取模再按上面处理负数)。 - 局部时钟:每个片段自己推进时间(乘以 ),简单直观。
- 全局时钟:角色有一个全局时钟,片段只记 。同步变得很简单:玩家出拳与 NPC 受击由不同子系统触发,用局部时钟时取决于更新顺序会差一帧(经事件系统还会更晚);用全局时钟只要把受击动画的 设成出拳的即可,不管哪段代码先跑(卷 II 13.4.3)。所有角色共享一个主时钟即可保证时钟一致。时钟与时间线的通用讨论见 游戏时间线与时钟。
简单数据格式、通道与元通道
- 从 Maya 以 30 或 60 Hz 采样导出,每关节每样本存 S(1 或 3 个分量)、Q(4)、T(3),即最多 10 个通道(卷 II 13.4.4)。
struct AnimationSample { JointPose* m_aJointPose; };
struct AnimationClip {
Skeleton* m_pSkeleton; // 实际引擎中常用骨骼唯一 id
F32 m_framesPerSecond; U32 m_frameCount;
AnimationSample* m_aSamples; // 非循环 frameCount+1 个,循环 frameCount 个
bool m_isLooping;
};
- 真实引擎不会这样存,而是压缩,见 动画压缩。
- 样本本质上定义了每个关节 10 个标量(或两个向量、一个四元数)的连续通道函数;理论上这些函数在整个片段上光滑连续(相机切镜这类人为设定的不连续除外);很多引擎在样本间线性插值,实际用的是分段线性近似(卷 II 13.4.5)。
- 元通道 (metachannel):与姿势无关但要与动画同步的数据。最常见的是事件触发通道:局部时间越过触发点时向引擎发事件,例如脚着地时播脚步声和尘土粒子;还有 Maya 定位器 (locator),常用来驱动过场相机(FOV 等放在额外浮点通道),以及纹理滚动、材质参数、灯光参数等(卷 II 13.4.6)。
网格、骨骼与片段的关系
一类角色有一套骨骼、一个或多个网格、一个或多个片段。骨骼是中心:皮肤附着在骨骼上,与片段无关;片段针对某个骨骼,不知道皮肤。新骨骼往往意味着一整套新动画,所以设计上尽量减少骨骼种类,多做蒙到同一骨骼的网格来制造角色多样性(卷 II 13.4.7)。
动画重定向:形态相同的骨骼只需重映射关节索引;不同时更复杂。Naughty Dog 让动画师定义重定向姿势 (retarget pose),记录源骨骼与目标骨骼绑定姿势的本质差异,运行时据此调整源姿势(卷 II 13.4.7.1)。更高级的方法 GEA 推荐 Dutreve 等《Feature Points Based Facial Animation Retargeting》和 Hecker 等《Real-time Motion Retargeting to Highly Varied User-Created Morphologies》(Spore)。UE5 的做法见 IK Rig 与动画重定向。
矩阵约定说明
- 13.3.2 把 写成 (平移在最下一行),与 13.5.2 的蒙皮公式 (式 13.4)和全局姿势连乘顺序 一样,都是行向量左乘的约定,书中前后一致。约定对照见 齐次坐标与矩阵约定。(2026-10-12 复核:此前本页称 13.3.2 为列向量写法、与 13.5.2 不一致,是误读 PDF 文本,已撤回。)
出处
- Game_Engine_Architecture_4th_Edtion_Volume_2_-_Jason_Gregory.pdf
- Jason Gregory《Game Engine Architecture》第 4 版(2026)卷 II 第 13 章 13.1–13.4 节