从 AnimBP 到 UAF

动画运行时讲解 · 写给程序、技术动画师和要做技术选型的人

从 AnimBP
到 UAF

角色每一帧的样子,是几十到几百根骨骼的变换,由动画片段采样、混合、修正之后算出来。Anim Blueprint 把这件事做了十年,Unreal Animation Framework(UAF)要换一种做法:把逻辑拆成能叠起来的 Trait,把求值写成一段程序交给虚拟机跑,并把整件事搬离游戏线程。这份讲解从骨骼和四元数讲起,沿着动画运行时的演进走到 UAF 的内部,最后落到 UE 5.8 里哪些模块现在可以试、怎么试、会踩什么坑。

8 个页签 · 九章加两个附录· 88 节 · 基于 UE 5.8 的公开 API 与社区资料,截至 2026 年 10 月

开场一个最小的骨架,两套关键帧。拖动混合权重,看姿态怎么从一套过渡到另一套;切换"混合方式",看为什么动画系统只在局部空间里混旋转。

0.1

问题从哪来:一个两百人的集市

先放下骨骼和四元数,看一个具体的场景。后面每一章都会回到它。

假设你在做一个开放世界,有一座集市。集市里有两类角色:

两百个 NPC。摊贩、路人、巡逻的卫兵。他们要走、跑、转身、停下来交谈,用 Motion Matching 让移动自然。镜头里同时能看到几十个,远处还有上百个在动。他们几乎不播蒙太奇,也不需要网络同步。

一个主角"猎人"。玩家操作。攻击、闪避、处决、攀爬,大量用蒙太奇的分段和分支点;近战位移靠根运动;要支持联机。

集市 NPC × 200主角"猎人" × 1
压力在哪数量:每个角色一份动画实例,游戏线程开销随人数增长复杂度:一张越长越大的 Anim Blueprint,多人同时修改
选片方式Motion Matching + Chooser状态机 + 蒙太奇
位移胶囊体驱动大量根运动
网络不需要或很少必须
远处需要更便宜的表示永远在镜头前
图 0.1贯穿全篇的两个角色。本例为虚构,用来对照 ABP 与 UAF 的取舍,不代表任何测量数据。

用 Anim Blueprint 做这两类角色,问题分别出在两个地方。NPC 的问题是量:每个 NPC 都是一个完整的 UAnimInstance,事件图在游戏线程上跑,两百个加起来,游戏线程被动画吃掉一大块。主角的问题是结构:一个蓝图里塞着状态机、几十个蒙太奇插槽、IK、修正和一堆数据读取,谁都不敢动。

UAF 想同时回答这两个问题:把动画搬离游戏线程、让实例变轻,解决量;把逻辑拆成可叠加的小单元,解决结构。但读完全篇你会看到,它目前对 NPC 的帮助远大于对主角的帮助,因为主角依赖的蒙太奇、根运动、网络,恰好是 UAF 最不成熟的部分。第 8 章会把这个结论落到具体的试点方案上。

整份讲解都围绕三个问题:谁描述一帧动画要做的事、谁执行、在哪个线程执行。

场景与角色为本文虚构;ABP 与 UAF 的对比依据见第 2–7 章

集市 NPC × 200

压力在数量:游戏线程开销随人数增长。

主角"猎人"

压力在结构:一张巨大的 ABP,蒙太奇、根运动、联机。

结论预告

UAF 目前帮 NPC 远多于帮主角。

要点

三个问题:谁描述、谁执行、在哪个线程执行。

0.2

一个姿态是怎么混出来的

开场的小人只有十几根骨骼,但它已经包含了动画运行时要做的全部基本动作。每根骨骼存的不是一个位置,而是相对父骨骼的旋转(再加上平移和缩放)。从骨盆出发,沿着骨骼链把这些相对旋转一层层乘下去,才得到每根骨骼在模型里的位置。这一步叫前向运动学(FK)。

图 0.2拖动权重在两套关键帧之间混合。切到"组件空间直接插位置",四肢会在中途缩短;打开"上半身遮罩",上身换成第三套姿态,下身不变。

把权重拖到 0.5。在"局部空间混旋转"模式下,每根骨骼的角度各取一半,再做 FK,四肢长度始终不变。切到"组件空间直接插位置",同样是各取一半,但取的是关节的最终位置:手腕会从两套姿态的连线中间穿过去,前臂明显变短。这就是动画系统几乎总在局部空间里混合旋转的原因。

再打开上半身遮罩。下半身照常在两套关键帧之间混,上半身换成第三套姿态。每根骨骼有自己的混合权重,这就是分层混合(Layered Blend)。UAF 用 Hierarchy Table(一种按骨骼层级存数据的通用资产)来存这样一份按骨骼的权重表。

一帧动画就是把很多这样的操作按依赖关系串起来:采样几段动画、按权重混合、叠加修正、做 IK,最后写给网格。集市里的每个 NPC,每一帧都要把这串操作做一遍;两百个 NPC 就是两百遍。

示意骨架与关键帧为本文自制,不是引擎数据

局部空间

每根骨骼存相对父骨骼的旋转;FK 一层层乘出最终位置。

在局部空间混旋转

四肢长度不变。直接插最终位置,关节会缩短。

分层混合

每根骨骼一个权重。UAF 用 Hierarchy Table 存这份表。

要点

一帧动画 = 采样 → 混合 → 修正 → 写出。区别只在谁描述、谁执行、在哪个线程执行。

0.3

六个比喻,后面会一直用到

后面的内容会越来越接近源码。先准备六个比喻,读到对应的地方时会再提一次。

乐高积木,对应 Trait。ABP 的一个节点是一整块预制件,Sequence Player 就是 Sequence Player。UAF 把它拆成更小的积木:一块负责播放,一块负责镜像,一块负责平滑,按需要叠在一起,组成一个节点。

点菜单和后厨,对应两次遍历。服务员先走一遍桌子,记下每桌要什么(Update),再把菜单交给后厨按顺序做(Evaluate 之后的 VM)。记菜单的人不碰锅,做菜的人不用跑桌子。

计算器的栈,对应求值 VM。老式逆波兰计算器先压两个数,再按"+"把它们弹出来、压回结果。UAF 的求值 VM 压的不是数,而是整套骨骼姿态(关键帧),"+"换成了"按权重混合"。

快照照片,对应变量绑定。动画线程不直接翻游戏世界的抽屉,而是在开工前拿到一张照片:速度、加速度、是否在空中。照片拍完,世界怎么变都不影响这一帧的计算。

插播广告,对应 Injection。电视台不用重排节目表,到了插播点就把一段广告接进来,播完再切回。UAF 没有蒙太奇,临时插一段动画靠的是图里预留的"插播点"。

预制菜,对应图工厂。同一道菜每次都从头备料太慢,就提前做成半成品。UAF 能从一个动画资产直接"做"出一张小图,下次同样的需求直接复用。

比喻只为建立直觉,机制在对应小节展开

乐高积木

对应 Trait:节点拆成可叠加的小块。

点菜单和后厨

对应 Update 与 Evaluate 两次遍历。

计算器的栈

对应求值 VM:栈里放的是整套姿态。

快照照片

对应变量绑定:动画线程只看照片。

插播广告

对应 Injection:没有蒙太奇后的插入方式。

预制菜

对应图工厂:从资产直接生成小图。

要点

现在不用全懂,读到对应小节时会再提一次。

0.4

十六个会反复出现的词

这张表是查阅用的。最容易混的一组是Trait和节点:在 UAF 里,节点只是一个容器,里面叠着若干 Trait;真正干活的是 Trait。

姿态 Pose
一组骨骼变换。UAF 里常叫关键帧(Keyframe)
局部 / 组件空间
相对父骨骼 / 相对整个网格的根
Anim Blueprint(ABP)
UE4 起的动画蓝图:事件图 + 动画图
UAF / AnimNext
新动画框架。AnimNext 是它在源码里的旧名
Workspace
把多个 UAF 资产放在一个编辑器里编辑的容器
System
每帧要做的事的总脚本,挂在组件上运行。旧版叫 Module
Anim Graph
描述姿态怎么算的图,由 Trait Stack 节点组成
Trait
最小的动画逻辑单元:播放、混合、镜像、平滑……
Trait Stack
一个节点里叠在一起的若干 Trait
Trait 接口
Trait 对外提供的能力,例如 IUpdate、IEvaluate
求值程序
Evaluate 遍历产出的一串任务,交给 VM 执行
求值 VM
执行任务的栈机,栈里放的是姿态
RigVM
Control Rig 底下的执行虚拟机,UAF 的 System 和图都跑在上面
变量绑定
把游戏数据按值送进 UAF 的机制
Injection
往运行中的图里临时插入一段动画或一个修饰
惯性化
切换动画时不同时算两段,只把差值衰减掉

类名以 UE 5.8 的 API 参考为准;Module 改名为 System 见 UUAFSystem 的显示名

姿态 / 关键帧

一组骨骼变换

System · Anim Graph

每帧脚本 · 姿态图

Trait · Trait Stack

最小单元 · 叠在一个节点里

求值程序 · 求值 VM

任务清单 · 以姿态为元素的栈机

变量绑定

按值送进动画线程

Injection · 惯性化

临时插入 · 只衰减差值

要点

节点只是容器,干活的是 Trait。

0.5

怎么读这份讲解

全篇九章加两个附录,分成 8 个页签。每个页签是一次可以坐下来读完的量。

完整课程8 个页签,按顺序

先打动画数学的底,再看 ABP 走到了哪一步,然后进 UAF 的内部。后面的取舍都靠前面解释。

程序向页签 1、3、4、7、8

四元数与惯性化,UAF 的资产、Trait 与两次遍历,求值 VM 与调度,最后是代码地图和动手实现。

技术动画师页签 1、3、5、6

混合、同步与过渡,Workspace 和 System 怎么搭,各功能模块与第 7 章的控制面,以及准入表和排错。

动画师 · 美术页签 1、5

开场与混合基础,然后直接读第 7 章:遮罩怎么画、人群的距离和数量档位怎么设、哪些参数归你调。

想自己实现页签 1、4、8

先读两次遍历和求值 VM,再跟着附录 B 的六个里程碑写一个迷你框架。

决策向页签 2、6

ABP 的结构性问题、官方的现状说法、按模块准入表和版本路线。

开场的"两百人集市"是贯穿全篇的例子:第 3 章给 NPC 搭 System,第 5 章跟踪它的一帧,第 8 章分别给 NPC 和主角做准入决定。

页签会记住你读到哪里。数字键 1–8 切换页签,地址后缀 #t3 可以直接分享某个页签。右上角的演示把同样内容切成一页一屏的幻灯片,← → 翻页,N 显示讲稿。按 T 打开目录。

UAF 还是实验性功能,资料以 API 参考和社区文章为主,所以每节末尾都标了出处。证据分三档:官方(API 参考、版本说明、Epic 员工的公开回复)可以直接引用;社区(源码阅读笔记、教程)结构可靠,细节可能随版本变;推断是本文按结构整理或用背景知识补的,对外讲之前请自己验证。附录 A 的最后一节列了完整的参考资料。

路线划分是建议,按听众调整

完整课程

8 个页签,按顺序。

程序向

页签 1、3、4、7、8

技术动画师

页签 1、3、5、6

想自己实现

页签 1、4、8

决策向

页签 2、6

证据分三档

官方 · 社区 · 推断。UAF 仍在变,对外讲之前先核对版本。

01

动画数学基础

动画运行时做的事,归结起来是在正确的空间里,用正确的方式,把几套姿态按权重合成一套。

6 节

开场的小人已经暴露了三个问题:混合要在哪个空间做,旋转怎么插值,切换动画时怎么不跳。这一章把它们讲清楚,再加上两个运行时绕不开的话题:多段动画的时间怎么对齐,以及角色是被动画推着走,还是带着动画走。

这些都不是 UAF 特有的。但 UAF 的求值任务清单里,每一种任务都对应这里的一个操作,读完这一章,第 5 章的任务名会一看就懂。

1.1

姿态:一组骨骼变换,三种坐标空间

一个骨骼网格有一棵骨骼树。每根骨骼存一个变换:旋转、平移、缩放。动画片段在每个关键帧上给每根骨骼一组这样的变换,运行时按时间采样,得到这一帧的姿态。

同一个变换可以放在三种空间里表达:

局部空间:相对父骨骼。动画数据就是这么存的,混合也在这里做。组件空间:相对网格的根,也叫模型空间。IK、瞄准、脚步贴地这类"要知道手在哪儿"的操作需要它。世界空间:再乘上组件在世界里的变换,物理和碰撞用它。

从局部到组件空间要沿骨骼链一路相乘,代价和骨骼数成正比,反过来也一样。所以运行时会尽量在一个空间里把能做的事做完,再统一转换一次。UAF 的求值任务里有专门的 ConvertRotationsToMeshSpace 和 ConvertRotationsToLocalSpace,就是这一步。

存储局部空间

相对父骨骼。动画数据、混合、加法都在这里

→ FK
按需组件空间

相对网格根。IK、瞄准、贴地

→ × 组件变换
输出世界空间

物理、碰撞、挂点

图 1.1三种空间之间的转换,每一次都要沿骨骼链走一遍。

还有一个容易忽略的细节:不是每一帧都要算所有骨骼。远处的角色可以只算几十根主干骨骼,手指和面部全部跳过。这份"这一帧要算哪些骨骼"的清单,在 ABP 里叫 Required Bones,在 UAF 里体现为 LOD 姿态和骨骼映射(第 5.5 节)。

空间划分是通用动画知识;任务名见 UAFAnimGraph API 参考中的 FAnimNextConvertRotationsTo*Task

局部空间

动画数据和混合都在这里。

组件空间

IK、瞄准、贴地需要它;转换要沿骨骼链走一遍。

世界空间

物理、碰撞、挂点。

要点

运行时尽量在一个空间里把事做完,再统一转换。UAF 有专门的转换任务。

1.2

旋转怎么混:四元数、Slerp 与 NLerp

欧拉角(俯仰、偏航、滚转三个角度)直观,但不能直接按分量插值:同一个朝向有多种写法,中途还可能遇到万向锁,两个相近的朝向插出来可能绕一大圈。

动画系统用四元数表示旋转。单位四元数是四维单位球面上的一个点,两个旋转之间的"最短路径"就是球面上的一段大圆弧。

Slerp(q₀, q₁, t) = q₀ · sin((1−t)θ) / sin θ + q₁ · sin(tθ) / sin θθ 是两个四元数的夹角。沿大圆弧匀速走,结果天然是单位长度

NLerp(q₀, q₁, t) = normalize((1−t)·q₀ + t·q₁)先按分量线性插值,再归一化。路径相同,只是速度不完全匀速

实时动画几乎都用 NLerp:它便宜,可以对任意多个姿态做加权和后统一归一化,而且在两个姿态夹角不大时,和 Slerp 的差别看不出来。代价是一条约束:加权求和之前,要先保证所有四元数在同一个半球。q 和 −q 表示同一个旋转,如果一个取了 q、另一个取了 −q,加起来会互相抵消,骨骼会突然翻转。实现里会先看点积,小于零就把其中一个取反。

这也解释了为什么 UAF 的任务表里有一个单独的 NormalizeKeyframeRotations:多次"加权累加"之后,统一做一次归一化,而不是每一步都做。

四元数与 NLerp 为通用知识;任务名见 FAnimNextNormalizeKeyframeRotationsTask

欧拉角不能直接插

多种写法、万向锁、可能绕远路。

Slerp

沿大圆弧匀速,贵。

NLerp

加权求和再归一化,便宜,可以一次混多个。

要点

先把四元数翻到同一半球,再累加,最后统一归一化。

1.3

加法动画与骨骼遮罩

加法动画存的不是姿态,而是相对某个参考姿态的差。比如"呼吸":用呼吸动画的每一帧减去它的第一帧,得到一组小小的偏移,可以叠到任意站姿、跑姿上。

Additive = Pose_anim ⊖ Pose_refResult = Base ⊕ α · Additive旋转的"减"是乘以逆四元数,"加"是相乘;α 缩放这份偏移

加法有局部空间和网格空间两种。局部空间加法把偏移加在每根骨骼自己的旋转上;网格空间加法把偏移加在组件空间的朝向上,适合"整个上身往左倾"这种不受下身朝向影响的修正。UAF 的 Apply Additive Trait 两种都支持,还有一个按骨骼遮罩的版本。

骨骼遮罩就是开场的"上半身遮罩":每根骨骼一个 0 到 1 的权重。ABP 里它叫 Blend Profile 或 Layered Blend per Bone 的设置;UAF 用 Hierarchy Table 存它,这是一个通用的"按层级存数据"的资产,Blend Mask 只是它的一种用法。

还有一种特殊的加法叫动态加法:不预先存差值,而是运行时拿两个当前姿态相减。UAF 里有 Make Dynamic Additive Trait 和对应的任务,可以把一段普通动画当成加法用。

图 1.3同一份"受伤前倾"加法叠到待机、走、跑三种动作上。灰色是原动画,橙色是叠加后;拖动 α 看偏移的强弱。

UAFAnimGraph API:FAnimNextApplyAdditiveTraitSharedData、FApplyAdditivePerBoneTraitSharedData、FMakeDynamicAdditiveTraitSharedData;Hierarchy Table 用作 BlendMask 见 RemRemRemRe 的 5.6 笔记

加法 = 差值

相对参考姿态的偏移,可叠到任意姿态上。

局部 / 网格空间

网格空间适合不随下身朝向变的修正。

Hierarchy Table

UAF 用它存按骨骼的遮罩权重。

要点

动态加法:运行时用两个当前姿态相减。

1.4

时间:同步组与同步标记

把走路和跑步按 0.5 混合,如果两段动画各自按自己的时间播放,会出现左脚还在走路的第 0.3 秒、右脚已经是跑步的第 0.1 秒,画面上就是一顿乱踩。

同步组的做法是选一个领头(通常是当前权重最大的那段),其他成员不按自己的时间走,而是按"归一化进度"跟随:领头走到 40%,大家都在 40%。走路一个周期 1.2 秒、跑步 0.8 秒也没关系,它们会被同时拉伸或压缩。

只按百分比对齐还不够,因为两段动画的左脚落地点不一定都在 0%。同步标记在动画上打"左脚落地""右脚落地"这样的标签,同步时按标记区间对齐,领头处在"左脚落地到右脚落地的 30%",跟随者也被拉到它自己那一段的 30%。

UAF 把这件事拆成了一个独立的 Trait(Trait 是 UAF 里最小的逻辑单元,第 4 章细讲):Synchronize Using Groups,按组名把时间线同步起来。时间线本身由 ITimeline 接口(Trait 之间协作用的接口之一,第 4.5 节)暴露,每个能报告进度的 Trait(比如 Sequence Player)都实现它,同步 Trait 通过接口读写,不需要知道下面挂的是什么。这是"组合"思路的一个典型例子。

图 1.4走和跑按权重混合。左边两段各按自己的时间播放,右边用同步组按共同相位播放。橙色、青色是两只脚尖的轨迹。

同步组与同步标记为 UE 通用机制;UAF 对应 FAnimNextSynchronizeUsingGroupsTraitSharedData、ITimeline、IGroupSynchronization、FTimelineSyncMarker

问题

两段不同周期的循环按权重混,脚步会乱。

同步组

跟随领头的归一化进度。

同步标记

按"左脚落地"这类标签分段对齐。

要点

UAF 把同步做成独立 Trait,通过 ITimeline 接口读写时间线。

1.5

过渡:交叉淡化、惯性化与 Dead Blending

从一段动画切到另一段,最朴素的做法是交叉淡化:在过渡时间里两段都算,权重从 1:0 渐变到 0:1。它的问题有两个。第一,过渡期间要同时采样两段动画,层层嵌套时代价成倍增长。第二,如果切换很频繁(比如 Motion Matching 每几帧就换一段),会同时挂着好几段在淡出。

惯性化(Inertialization)换了一个思路:切换的瞬间只记下旧姿态和新姿态之差,以及差值的变化速度,之后只算新动画,再把这份差值按一条平滑曲线在过渡时间内衰减到零。每帧只采样一段动画。

图 1.5四个小人播放同一段切换,分别用四种过渡方式。手(青)和脚(橙)后面拖着最近 0.6 秒的轨迹;下面的条是时间线,橙色块是过渡窗口;"采样"是这一帧要采样几段动画。

先看"走 → 跑",速度调到 0.25×。硬切在切换那一帧,手脚的轨迹直接折断,这就是"跳帧"。交叉淡化的轨迹最圆滑,但过渡期间"采样"变成 2。惯性化和 Dead Blending 的轨迹同样没有折断,采样却一直是 1。

再切到"频繁切换":交叉淡化的采样数会涨到 3 甚至更多,因为上一段还没淡完,下一段又来了。Motion Matching 每隔几帧就可能换一段,这就是它几乎都配惯性化的原因。最后看"挥臂反向",打开"目标姿态虚影":惯性化的前臂带着旧速度冲过虚影再拉回来,Dead Blending 冲得更少。

下面这张曲线图把同一件事压成一根骨骼的角度,能读出具体的过冲量。

图 1.5b一根骨骼的角度随时间变化。虚线是旧动画 A 和新动画 B,实线是实际输出;在 0.6 秒时切换。底部的条是每帧需要采样的动画段数。

切到"惯性化",过渡期间底部的条只剩一格,因为只算 B;实线从 A 的值出发,带着 A 的速度,平滑地落到 B 上。切到"交叉淡化",过渡期间是两格。

惯性化有一个已知的弱点:差值曲线只看切换瞬间,如果 B 的运动方向和 A 相反,衰减曲线会冲过头。Dead Blending 是近几年的改进:不再记录"差值",而是把旧动画 A 按切换时刻的速度向前外推,速度逐渐衰减,再和 B 做普通的交叉淡化。它仍然只采样一段真实动画,外推几乎不要钱,结果对方向相反的情况更稳。

UAF 三种都有:Blend Two Way 这类 Trait 做交叉淡化,Blend Inertializer 做惯性化,Dead Blending 有专门的过渡任务和应用任务(DeadBlendingTransitionTask / DeadBlendingApplyTask)。

惯性化见 David Bollo, GDC 2018(Gears of War 4);Dead Blending 见 Daniel Holden 2023 年的文章;UAF 任务名见 UAFAnimGraph API 参考 · 交互图曲线为示意

交叉淡化

过渡期间两段都算,频繁切换时代价叠加。

惯性化

只记差值和速度,之后只算新动画。

Dead Blending

把旧动画按速度外推再淡化,方向相反时更稳。

要点

UAF 三种都有:Blend Two Way · Blend Inertializer · Dead Blending 任务。

1.6

根运动:动画推着角色走,还是角色带着动画走

角色往前走,可以有两种因果关系。

胶囊体驱动:移动组件按输入算出速度和位置,动画系统读这个速度,挑一段合适的走路动画来播。脚和地面可能对不上(滑步),需要靠步幅调整、Stride Warping 之类的手段修。

根运动驱动:动画里根骨骼的位移被提取出来,交给移动组件,角色按动画走。脚步自然贴地,但响应慢,网络同步也更麻烦,因为移动结果取决于动画状态。

实际项目多是混用:日常移动用胶囊体驱动,翻越、攀爬、处决这类精确动作用根运动。Motion Matching 让这个界线更模糊:它用轨迹(角色过去和未来几秒的位置朝向)去搜动画,动画天然贴合预测的移动。

这件事对 UAF 有直接影响。根运动意味着动画结果要回流给移动组件,而 UAF 想在游戏线程之外算动画;两者之间的时序必须被显式安排。这就是为什么 UAF 组件上有一个 bSetupCharacterMovementDependency,以及为什么 5.8 的 Mover 和 UAF 有一个专门的桥接插件(第 6.11 节)。

图 1.6同一段走路或跑步动画,左边由胶囊体按输入速度带着走,右边由动画自己的根运动带着走。地面上的点是每一帧着地脚的位置:点散开成一串,就是滑步。

把胶囊体速度拖到和动画自带速度差不多,左边的点就重合了;拖快或拖慢,点就拉成一串。这就是"胶囊体驱动会滑步"的直观样子。右边永远不滑,但它的速度是动画决定的,玩家推摇杆的力度改变不了它,除非换一段动画。

通用知识;bSetupCharacterMovementDependency 见 UUAFComponent API;Mover UAF 插件见 UAF 插件索引

胶囊体驱动

移动决定速度,动画跟着选;会滑步。

根运动驱动

动画决定位移;贴地但响应慢、难同步。

轨迹

Motion Matching 用预测轨迹搜动画,模糊了两者的界线。

要点

根运动要回流给移动组件,所以 UAF 必须显式安排它与移动的时序。

1 / 1