技术博客

动画混合:BlendSpace 与 Layered Blend 的采样

第 54 / 100 篇 · 2025-11-27 · ue5 · 动画系统动画混合源码分析

状态机管"在哪",混合管"怎么合"。 locomotion 系统里那个经典的"Idle↔Walk↔Run 按速度混合"背后,是两套独立的机制:BlendSpace 解决连续参数空间里的插值,Layered Blend Per Bone 解决骨骼子集间的叠加。这篇讲两者的数学和实现,重点是那条贯穿一切的公理:权重必须归一化。

BlendSpace:把参数映射到采样权重

BlendSpace 是定义在参数空间(1D/2D,最多 3D)上的分段插值函数。样本(Sample)是用户在编辑器里摆放的动画序列 + 参数坐标。采样时:

  1. 找包围单元:2D BlendSpace 对样本做 Delaunay 三角剖分(FBlendSpace::GetSamplesFromBlendSpace 里的 FDelaunayTriangle),给定参数点找到所在的三角形(1D 是线段,3D 是四面体);
  2. 重心坐标计算:参数点在三角形内的归一化重心坐标 $(\lambda_1, \lambda_2, \lambda_3)$,$\lambda_i \geq 0$、$\sum \lambda_i = 1$,就是三个样本的混合权重。

重心坐标的几何意义是"面积比":$\lambda_1 = S_{P23P}/S_{123}$,$P$ 是参数点。这保证了两条性质——落在顶点处权重退化为单样本(精确复现原始动画),沿边走线性插值(样本间过渡均匀)。Editor 里拖拽样本时重新剖分,所以"样本摆位"直接决定插值场形状:同一组 Idle/Walk/Run 三个点,Walk 放中间还是放边上,walk 区的混合行为完全不同。

采样开销与样本数无关(单元定位是 $O(\log n)$ 的网格加速),只与单元顶点数(3 或 4)成正比——这意味着 BlendSpace 可以摆几十个样本而不怕采样成本,怕的是网格形状差导致的权重抖动(参数小幅变化跨单元时权重场不连续)。缓解手段是网格加密(加虚拟样本)或用 BlendSpace Graph 手动控制。

分层混合:骨骼掩码的权重矩阵

Layered Blend Per Bone 的机制一句话:每根骨骼一个混合权重,由掩码(Branch Filter 列表)定义。Branch Filter 指定"从某骨骼开始、深度若干层"的子树权重,没在子树里的骨骼权重走底层(base pose)。

实现上这是 FAnimNode_LayeredBoneBlend 持有的一个逐骨骼权重数组(BoneBlendWeights,长度 = 骨骼数),Initialize 时按 Branch Filter 展开成下标列表,Evaluate 时对每个骨骼按各自权重分别插值:

$$T_{final}(i) = \text{lerp}\big(T_{base}(i),\ T_{overlay}(i),\ w(i)\big)$$

$w(i)$ 就是第 $i$ 根骨骼的权重。矩阵视角:整个骨架是 $n$ 维的位形空间,混合不是全局标量而是逐维度的权重向量。这解释了它的两个特性——上身挥拳叠加在跑动腿上(上下半身权重分离)天然支持;但如果 Branch Filter 边界切在脊柱中间,弯腰动作会被撕裂,因为相邻骨骼权重差太大、位形不再满足骨骼链的运动学约束。

Root Motion 在这种混合下有专门的规矩:叠加层的根骨位移默认被剥掉(bBlendRootMotion 关闭时),由 base 层独占总位移,否则两层根骨位移插值出一个不伦不类的速度。

权重归一化:一切正确性的来源

多个混合源叠加时(Blend By Bool 套 Blend Space,或 Additive 挂在上层混合之后),UE 的 Pose 链路靠权重归一化维持语义:每个节点的输出 pose 附带累计权重,父节点按归一化后的比例分配。关键场景:

  • 父节点只对一个子节点求值(权重 1),子节点的混合结果原样通过;
  • 两个子节点权重 $w_1 + w_2 \neq 1$ 时,先归一化 $w_i' = w_i / (w_1 + w_2)$ 再插值。

这条规则有一个著名的推论:权重为 0 的分支会被完全跳过求值(FAnimationRuntime::BlendTwoPosesTogether 前的短路)。利用它做性能优化是合法的——把"当前不起作用的分支"权重压到 0,它的 Sequence 采样、IK 解算全部免费。反过来,"权重接近 0 但不等于 0" 是最差情况:分支照算不误,插值结果又几乎不可见。

四元数插值本身走 nlerp(normalize 后线性混合),近似 slerp 但省去三角函数——骨骼混合数量大、角度小的场景下两者差异不可见,这是另一个性能正确性取舍。

Additive 混合落位

资产篇给了 Additive 的存储公式 $\Delta T = T_{source} \cdot T_{base}^{-1}$,混合时的应用是:

$$T_{final} = T_{target} \cdot \Delta T^{w}$$

$w$ 是 Additive 权重,对增量做幂次(四元数球面幂、平移线性缩放)实现"叠加强度渐变"。注意乘法顺序:右乘(局部空间叠加),左乘会改变旋转的参考系,跨骨骼链累积成灾难。引擎源码里 TMeshPoseAdditive 系列路径严格遵守右乘,自定义节点调用 Additive 接口时传错顺序不会报错,只会诡异地扭。

源码入口

  • BlendSpace 采样:Animation/BlendSpace.cpp 的 GetSamplesFromBlendSpace,重心坐标计算在 FBlendSpace::GetSampleWeights;
  • 分层混合:AnimNode_LayeredBoneBlend.cpp,Initialize 里 Branch Filter 展开、Evaluate 里逐骨骼循环;
  • 混合原语:Animation/AnimationRuntime.cpp 的 BlendTwoPosesTogether / Accumulate 系,所有上层混合最终归约到这几个函数;
  • Additive 应用:AnimNode_Additive 系,FAnimationRuntime::Accumulate 里右乘路径。

机制总览:

两套混合机制:参数空间插值 vs 骨骼空间叠加 BlendSpace:参数 → 重心坐标 λ_i ≥ 0,Σλ_i = 1 样本数不影响采样成本 怕的是跨单元权重抖动 Layered Blend:逐骨骼权重 w = 1 w = 0(base) Branch Filter 切在脊柱中间会撕裂位形 公理:权重归一化 + 权重 0 短路求值 w_i′ = w_i / Σw(插值前先归一) 权重 0 的分支完全免费(采样/IK 全跳过) Additive:T_final = T_target · ΔT^w(右乘!) 最差情况:权重趋近 0 但不等于 0