状态机管"在哪",混合管"怎么合"。 locomotion 系统里那个经典的"Idle↔Walk↔Run 按速度混合"背后,是两套独立的机制:BlendSpace 解决连续参数空间里的插值,Layered Blend Per Bone 解决骨骼子集间的叠加。这篇讲两者的数学和实现,重点是那条贯穿一切的公理:权重必须归一化。
BlendSpace:把参数映射到采样权重
BlendSpace 是定义在参数空间(1D/2D,最多 3D)上的分段插值函数。样本(Sample)是用户在编辑器里摆放的动画序列 + 参数坐标。采样时:
- 找包围单元:2D BlendSpace 对样本做 Delaunay 三角剖分(
FBlendSpace::GetSamplesFromBlendSpace里的FDelaunayTriangle),给定参数点找到所在的三角形(1D 是线段,3D 是四面体); - 重心坐标计算:参数点在三角形内的归一化重心坐标 $(\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里右乘路径。
机制总览: