动画系统九篇从这个坐标系开始:资产层。动画在 UE 里不是一堆 FBX 导入就完事的黑盒——USkeleton 定义骨骼命名空间,AnimSequence 存采样数据,压缩器决定内存和精度的天平。搞清数据布局,后面讲混合、状态机、IK 才有共同的词汇表。
USkeleton:命名的骨骼树
USkeleton 是动画资产的"坐标系原点"。它的核心数据只有两样:
- BoneTree:
FBoneNode数组,按深度优先顺序存骨骼的父子关系(ParentIndex+Name)。顺序即协议:所有动画轨道、蒙皮权重、Control Rig 都引用这个数组的下标(Bone Index),不重排、不删除骨骼的规矩就是从这里来的——改骨骼树等于让已有动画资产全部失效; - Reference Pose:骨骼的静止位形(每根骨骼一个 FTransform),是压缩差值的基准,也是"回到 T-Pose"的数据来源。
USkeleton 本身不持有网格。网格(USkeletalMesh)通过 Skeleton 指针挂到某棵骨骼树上,兼容判定只看骨骼名集合:网格用的骨骼名是 Skeleton 的子集就能绑定,多了就报 missing bone。这也是同名重定向(Retarget)的基础——名字对上了,轨道就能搬。
AnimSequence:轨道 + 元数据
AnimSequence 的资产本体。运行时真正被采样的是 UAnimSequence 序列化进来的压缩数据(CompressedData),原始未压缩轨道只在导入和压缩时短暂存在(DDC 里留档)。布局:
- 压缩轨道头(
FCompressedTrack系):每条轨道对应一根骨骼,存压缩格式标识、关键帧数、每帧时间戳; - 压缩位流:四元数、平移、缩放各自独立的字节流,按格式不同可能是量化到 16 位、线性关键帧抽取、或 Bitstream 编码;
- 元数据:Notify 轨道(
AnimNotifies)、曲线轨道(FloatCurves,SmartName索引命名)、Root Motion 提取结果(如果开了EnableRootMotion,RootMotionParams存提取好的根位移序列)。
一个容易混淆的点:AnimSequence 不存"动画状态",只存采样函数。状态(当前时间、播放速率、混合权重)住在 UAnimInstance 一侧的资产无关层。
压缩:三个旋钮的权衡
UE 的动画压缩是"每轨道选格式"的组合优化,主旋钮:
- 去掉可忽略的轨道(
Anim Compress Remove Linear Keys类):静止骨骼直接不产生轨道,采样时返回 Reference Pose。省内存的大头其实来自这里——大多数骨骼大多数时间不动; - 关键帧抽取:对时间维做线性误差抽稀,容差(
Max Curve Error之类)控制视觉差异。数学上等价于分段线性逼近,误差界由容差直接给出; - 数值量化:四元数分量缩放到 uint16/uint32。旋转的单位四元数满足 $x^2+y^2+z^2+w^2=1$,只有 3 个自由度,量化 4 个分量到 16 位带来的角度误差量级在 $10^{-3}$ 弧度——肉眼不可见,但乘以骨骼链长度会放大:50 根骨骼的链误差累积到末端能达到厘米级。长链骨骼(尾巴、披风骨)的轨道值得单独放宽。
选择策略很朴素:先开 Automatic Compression,在 Asset Details → Compression 里看压缩率(原始字节/压缩字节)和误差热力图;误差超标的序列手动指定 compressor(Anim Compress Remove Linear Keys 保精度,Bitstream 系保体积)。移动端项目内存吃紧时,牺牲的是采样 CPU(更激进的抽稀 → 更少的内存、不变的采样成本;更狠的量化 → 更少的内存、稍贵的解码)——两个方向都试,一般抽稀先见底。
Additive 与 Root Motion 的数据形态
Additive 动画(叠加动画,受击抖动、呼吸起伏)存的不是绝对位形而是相对基准的增量:
$$\Delta T = T_{source} \cdot T_{base}^{-1}$$
AdditiveAnimType 决定 base 是什么:RefPose 基准是 Reference Pose,MeshSpace 基准是另一段完整动画。采样时的应用是目标姿态右乘增量。增量对压缩更友好(数值范围小),但有一条铁律:基准必须和叠加目标同骨架、同坐标约定,跨骨架的 Additive 没有任何保护,直接出垃圾数据。
Root Motion 的提取发生在导入/压缩期:把根骨骼的位移从轨道里剥出来存进 RootMotionParams,骨骼轨道里的根骨位移清零。剥离让"播放动画"和"移动胶囊体"解耦——MovementComponent 消费 RootMotionParams 驱动位移,动画只管姿态。Montage 里切 Root Motion 片段时的一致性检查(CurrentRootMotion 的重叠判定)都在消费这一份预处理结果。
内存视角的实战数字
一个 30 秒 30fps、60 根骨骼的序列,未压缩约 30MB(60 骨 × 3 轨道 × 10 分量 × 4 字节 × 900 帧),自动压缩后典型掉到 1/20~1/50。项目的动画内存大头几乎从不是"压缩不够狠",而是:
- 序列粒度过碎(把连续动作切成几十个 1 秒片段,每段都有 Reference Pose 和头部开销);
- Notify/曲线塞了和采样无关的大对象;
bEnableRootMotion开着但玩法不用,白白存了一份提取数据。
数据布局一张图收束: