姿势合成:Priority 不是覆盖顺序,而是逐骨骼的剩余权重预算

自动关联目录:姿势合成:Priority 不是覆盖顺序,而是逐骨骼的剩余权重预算

核心判断:跑动、上半身动作、过渡、Additive、脚本骨骼覆盖和 IK 不能靠“最高优先级覆盖整个人”组合。Priority 应按骨骼分配剩余权重,Mask 只决定该轨是否参与该骨骼,最终姿势必须经过固定且可诊断的阶段管线。

一、业务冲突:角色要边跑边攻击,简单覆盖却让腿停了

运行时既要播放基础移动,又要允许短动作、分段动作、创作者轨道和程序化修正叠加。上半身攻击应覆盖脊柱和手臂,腿仍由跑步驱动;两个同级动作要按权重混合,高优先级没有覆盖的骨骼还要由低优先级补齐。若把 Priority 理解为“挑最高的一条”,遮罩外骨骼会掉到参考姿势;若先做整身混合再套 Mask,低优先级也无法正确填补空位。

工程里还出现过一次更具体的交叉淡入错误。UE 的 BlendTwoPosesTogether 某个权重参数表示累计姿势的权重,旧实现却传入了新姿势的归一化权重,结果淡入开始时反而偏向新姿势,边界处又硬切并回弹。公式没有崩溃,单条动画也正常,只有两条轨道按时间观察时才会暴露。这说明姿势合成最危险的错误不是资源加载失败,而是“权重含义看似合理,实际方向相反”。

二、实际实现:Game Thread 生成快照,Worker 按阶段合成

当前实现使用自定义 UAnimInstance,Game Thread 管理轨道时钟、权重、优先级、层、Mask、BlendSpace、Montage 和 IK,再在 PreUpdate 生成不可变求值快照。快照只保留有效权重大于零且资源可求值的贡献,并钉住异步资源引用;Worker 不读取 Actor 或脚本状态,只消费本帧快照。

基础覆盖轨按 Priority 从高到低处理。同一优先级先归一化,再从该骨骼的剩余预算中取 min(同级权重和, 剩余权重);Mask 不参与的轨对该骨骼权重为零,因此更低优先级仍能补齐。多个 Mask 根取并集,参与集合相同的骨骼被分组复用一次权重计算。没有 Mask 时走整身快速路径,只要存在 Mask 才进入逐骨骼路径。

求值顺序固定为:基础 Override -> Additive -> 脚本骨骼覆盖 -> IK。没有基础贡献或总权重不足时,用上一帧有效姿势填补,避免瞬间 T-Pose;原生动画声明 Additive 但 UE 资源不满足 Additive 条件时,当前实现会降级成 Override 并限频告警。短动作使用 Montage 的 Slot、Section、跳段和打断语义,BlendSpace 则把输入、样本权重和归一化时间一起快照。

flowchart LR
    S[轨道/BlendSpace/Montage 快照] --> V[资源与骨架可求值校验]
    V --> B[逐骨骼 Priority 剩余预算]
    B --> G[缺口姿势填充]
    G --> A[Additive]
    A --> O[脚本骨骼覆盖]
    O --> I[IK / 程序化修正]
    I --> P[最终 CompactPose]

三、值得保留:把组合语义集中在一个求值器里

Game Thread/Worker 分界是正确的。UE 动画线程不能安全访问动态 Actor 状态,先生成不可变快照既避免竞态,也让资源替换、暂停和冻结有统一语义。求值前检查压缩或 Raw 轨道是否真的可提取,也避免了“UObject 已存在但 Cooked Pose 数据尚不可用”导致 Worker 越界。

逐骨骼剩余预算比“按优先级排序后连续 Lerp”更符合业务。高优先级只消耗它覆盖的骨骼和权重,低优先级不是被整体淘汰,而是填剩余空间。同级归一化、低级补洞、Additive 后置,三者使 locomotion、上半身动作和微调可以共存。

事件只从实际有效权重大于零的贡献排出,也避免完全被遮住的动作仍触发表现事件。上一帧姿势填洞虽然并非最终理想方案,但它比资源空窗直接输出参考姿势稳定。Slot 路由、Skeleton 签名和运行时 Additive 校验均采用失败可诊断的方式,而不是让错误资产悄悄进入 Worker。

四、结构性缺陷:阶段明确了,但策略仍散落在运行时代码里

BlendSpace、普通轨和 Montage 仍通过硬编码优先级接入,同一份内容无法显式声明自己属于哪个阶段、消耗哪类预算、被谁打断。Slot 语义、通用轨道 Priority 和命名 Layer 是三套部分重叠的组合语言,内容作者很难预测最终顺序。

只要任一轨带 Mask,所有基础轨就进入逐骨骼路径;骨骼参与签名和名字解析也在运行时完成。角色数量和骨骼数放大后,成本近似 活跃轨数 × 求值骨骼数,而不是只与真正被遮罩的局部有关。Mask 使用骨名作为入口是 UE 常规做法,但发布制品应提前编译成目标 Skeleton 的紧凑位集,并在骨架变更时使缓存失效。

上一帧姿势可以掩盖短空窗,也可能把过期姿势无限冻结;错误 Additive 降级为 Override 更危险,它可能从局部偏移变成全身动作。脚本覆盖先于 IK 意味着 IK 永远赢,这是代码顺序,不是内容契约。若某个玩法要求手部精确脚本姿势覆盖 IK,现结构只能再加特殊开关。根因是求值阶段存在,但没有一份可验证、可编译的 PosePlan。

五、目标设计:把 Layer、Mask 与阶段编译成 PosePlan

目标结构应在资源绑定或内容发布时完成 Skeleton 兼容校验、Mask 展开、阶段排序和成本估算,运行时只求值编译后的计划。每个 Layer 明确 Stage、Priority、Mask 位集、混合模式、回退策略、是否产生事件与最大贡献数;Montage/BlendSpace 是 Source 类型,不再隐式决定全局优先级。

Pose Evaluate(const CompiledPosePlan& plan, const FrameSnapshot& frame)
{
    Pose base = plan.Fallback.Resolve(frame.PreviousPose, frame.ReferencePose);
    BoneWeights remaining = BoneWeights::One(plan.CompactBoneCount);

    for (PriorityGroup group : plan.OverrideGroupsHighToLow)
        BlendPerBone(base, remaining, Sample(group, frame));

    for (Layer layer : plan.AdditiveLayers)
        ApplyAdditive(base, SampleValidated(layer, frame));

    ApplyAuthoredBoneConstraints(base, frame.BoneWrites);
    SolveProceduralConstraints(base, frame.IK, plan.ConstraintOrder);
    return base;
}

回退也要有限期:短空窗允许 Hold Last Pose,超过内容声明的期限后应转到基础 Idle 或参考姿势,并报告“缺少基础贡献”,不能永久静止。Additive 不兼容应由内容策略选择拒绝、明确降级或替代资产;默认不应静默变成 Override。每个阶段输出贡献轨、有效权重、Mask 命中骨骼数和拒绝原因,才能解释最终姿势。

六、决策规则:先定义逐骨骼数学,再选择 UE 节点

是否使用 AnimGraph、Montage、Control Rig 或自定义 AnimInstance,是实现选择;业务先要回答的是每根骨骼由谁贡献、剩余权重如何分配、缺口如何回退、程序化修正在何时覆盖。以下场景应成为合成器的最小回归集:

场景正确结果关键断言
高优先级上半身 Mask + 低优先级跑步手臂来自动作,腿来自跑步Mask 外剩余权重仍为一
两条同级轨交叉淡入权重连续、方向正确、无边界回弹首尾和中点姿势连续
多个 Mask 根参与骨骼取并集,不重复消耗权重编译位集与目标 Skeleton 一致
Additive 资源不合法按契约拒绝或显式替代不发生隐式全身覆盖
基础资源短暂缺失短时保持,超时进入已定义回退无 T-Pose、无无限冻结
脚本覆盖与 IK 同时修改手部按编译阶段得到确定结果阶段顺序可读回
Worker 遇到不可提取数据跳过该贡献并保留稳定基础姿势无越界,拒绝原因可诊断

这段实践说明,姿势合成不是“多播几条动画”,而是一套逐骨骼资源分配算法。自定义求值器解决了任意轨道、权重和 Mask 的表达力,也带来了必须自己定义数学、线程边界和回退语义的责任。下一步应把散落的运行时约定编译为 PosePlan,让错误尽量在内容绑定阶段暴露,让 Worker 只执行已经证明合法的计划。