技术博客

动画资产结构:USkeleton/AnimSequence 的数据布局

第 51 / 100 篇 · 2026-08-12 · ue5 · 动画系统资产管线源码分析内存优化

动画系统九篇从这个坐标系开始:资产层。动画在 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 开着但玩法不用,白白存了一份提取数据。

数据布局一张图收束:

动画资产三层:骨骼树定坐标系,序列存轨道,实例管状态 USkeleton BoneTree:父索引 + 名字 Reference Pose(差值基准) UAnimSequence 每骨轨道:四元数/平移/缩放 压缩位流 + Notify/曲线 UAnimInstance 当前时间 / 速率 / 权重 状态层,资产无关 下标 压缩三旋钮(按收益排序实测) 1. 静止骨骼剔除轨道(省内存大头,无损) 2. 关键帧线性抽稀(误差界 = 容差) 3. 分量量化 uint16(误差 1e-3 rad,长链骨骼慎用) 排查顺序:序列粒度过碎 > 压缩参数 > 量化位宽