动画压缩
Animation Compression
未压缩的动画每关节每样本最多 10 个 32 位浮点通道,数据量巨大。GEA 列出的压缩手段按力度递增:省略无用通道、量化(如 16 位 RL 编码)、降低采样率或省略近似线性的关键帧、用 B 样条拟合曲线、小波压缩,以及最根本的:只把需要的片段放进内存(选择性加载与流式加载)(GEA4 卷 II 13.8)。
数据量有多大
每通道 4 字节 × 10 通道 × 30 样本/秒 = 每关节每秒 1200 字节(约 1.17 KiB)。100 关节的骨骼(以今天的标准算小)约每秒 117 KiB;1000 秒动画(对现代游戏偏少)约 114.4 MiB。PS3 只有 256 MiB 主内存和 256 MiB 显存;PS4 有 8 GiB、PS5 有 16 GiB 统一内存,但内存更应该花在动画的丰富程度上(卷 II 13.8)。
数据大的三个原因:通道多;关节多(PS3/Xbox 360 时代人形角色 250 个以上,部分 PS4/Xbox One 游戏超过 800,PS5/Xbox Series 更多);采样率高(卷 II 13.8.3)。
通道省略
- 多数角色不需要非均匀缩放,三个缩放通道可减为一个;有些游戏除面部外完全不存缩放。
- 人形骨头不能拉长,平移通道通常只给根、面部关节,有时加锁骨。
- 四元数是单位长度,只存 x、y、z,运行时重建 w。(补充: 的符号无法从三个分量恢复。因为 与 表示同一旋转,导出时可把 的四元数整体取反,保证 ;但逐帧取反会破坏相邻样本的连续性,影响插值,见 四元数。另一种常见做法是“最小三分量”(smallest three):丢弃绝对值最大的分量并用 2 位记录其下标。GEA 未提这一点。)
- 整段不变的通道只存 t=0 的一个样本加一个"常量"标志位。
- 例:100 关节、无缩放、只有根有平移,只需 303 个通道(每关节 3 个四元数分量 + 根的 3 个平移),而全量是 1000 个(卷 II 13.8.1)。
量化
把 32 位浮点转成 n 位整数称为量化,包括编码与解码,是有损的。四元数分量落在 ,经验表明 16 位精度就够,用 32 位浮点等于每通道浪费 16 位(卷 II 13.8.2)。浮点格式本身见 数值表示与浮点数。
做法:把取值区间等分成 段,用段索引表示值。(注意:下面 RL 代码实际把 映射到 个等距格点 ,即间隔是 个,两端 0 和 1 都能精确重建;和正文“ 个区间”的说法略有出入。)GEA 引用 Jonathan Blow 在 Game Developer 杂志 Inner Product 专栏的文章,编码有截断到下边界 (T) 与四舍五入到区间中心 (R) 两种,重建有返回左端 (L) 与返回中心 (C) 两种:
| 组合 | 评价 |
|---|---|
| TL、RC | 避免:会给数据整体减少或增加"能量" |
| TC | 带宽效率最高,但无法精确表示 0(0.0f 解码后变成小正数) |
| RL | 通常最佳,GEA 示例采用 |
U32 CompressUnitFloatRL(F32 unitFloat, U32 nBits) {
U32 nIntervals = 1u << nBits;
F32 scaled = unitFloat * (F32)(nIntervals - 1u); // 映射到 [0, nIntervals-1]
U32 rounded = (U32)(scaled + 0.5f); // 四舍五入
if (rounded > nIntervals - 1u) rounded = nIntervals - 1u;
return rounded;
}
F32 DecompressUnitFloatRL(U32 quantized, U32 nBits) {
U32 nIntervals = 1u << nBits;
return (F32)quantized * (1.0f / (F32)(nIntervals - 1u));
}
代码只在上端钳制;输入为负时 (U32)(scaled + 0.5f) 把负浮点转成无符号整数,在 C++ 中是未定义行为(结果不在目标类型范围内,见 cppreference“Floating–integral conversions”),所以调用方必须先把值钳制到 (平移示例就先钳制了)。
任意区间 先归一化到 。四元数用 min=−1、max=1、16 位。平移理论上无界,但关节实际移动不远,可定一个合理范围(示例 ±2 m),超出就报错;世界空间制作的过场里根关节平移可能很大,所以可以逐片段或逐关节选取范围并与压缩数据一起存储,开销可忽略(卷 II 13.8.2)。顶点数据的同类做法见 顶点压缩与精度。
采样率与关键帧省略
- 整体降低采样率:有些动画以 15 Hz 导出依然好看,数据减半。
- 省略近似线性区间内除端点外的样本,运行时线性插值恢复。需要额外存每个样本的时间,可能抵消部分收益,但确有引擎成功使用(卷 II 13.8.3)。
基于曲线的压缩
RAD Game Tools 的 Granny(已停售,GEA 作者称其为用过的设计最好的动画 API 之一)不存等间隔样本,而是把每个关节的 S、Q、T 通道表示为 n 阶非均匀非有理 B 样条:先按固定间隔采样,再在用户给定容差内拟合 B 样条。曲率大的通道也只需少量控制点,结果通常比等间隔线性插值的片段小得多(卷 II 13.8.4)。样条基础见 埃尔米特与 Kochanek-Bartels 样条。
小波压缩
把动画曲线分解为一组正交归一小波之和(小波:像波一样振荡但持续时间很短的函数),类似把信号表示为冲激串或正弦和。GEA 指向第 15 章的信号处理基础,并推荐 Eidos Montreal 在《Thief》(2014) 中实现小波压缩的文章(卷 II 13.8.5)。
选择性加载与流式加载
最便宜的片段是不在内存里的片段。多数游戏启动时加载一组核心片段常驻(玩家核心动作集、武器和道具等反复出现的物体),其余按需加载;一次性时刻的片段在需要前载入或流入,播完即卸载。很多引擎把片段打包成逻辑组整体加载和卸载(卷 II 13.8.6),加载机制见 资源管理器。
GEA 原书中的问题
- 13.8 开头写 100 关节骨骼的未压缩片段"117 KiB per joint per second"。按前文每关节每秒 1.17 KiB 计算,100 关节应为每秒 117 KiB(不是"每关节"每秒);后面"1000 秒约 114.4 MiB"正是按每秒 117 KiB 算出的。
出处
- Game_Engine_Architecture_4th_Edtion_Volume_2_-_Jason_Gregory.pdf
- Jason Gregory《Game Engine Architecture》第 4 版(2026)卷 II 第 13 章 13.8 节