MotionWarping深度研究
前言
UE5新加了实验性的插件MotionWarping
, 先前已经有很多大佬写过类似的文章, 本文主要针对部分源码进行其工作原理的深入理解和相关RootMotion的思考
从哪里开始?
我们先从这里开始看, 我们需要找到这个模块是在哪里修改了RootMotion信息, 先看调用栈
1 | FTransform UCharacterMovementComponent::ConvertLocalRootMotionToWorld(const FTransform& LocalRootMotionTransform, float DeltaSeconds) |
角色移动组件里有这个把RootMotion数据从本地转换成世界的方法
这里有两个带返回值的代理, 分别针对预处理Pre和后处理Post
1 | void UMotionWarpingComponent::InitializeComponent() |
在MotionWarping
组件中绑定了移动组件的2个代理, 在Pre函数中调用了Update()
函数(后面讲)来刷新修改类器URootMotionModifier
信息
然后统一都执行修改器类的虚函数ProcessRootMotion()
修改变换信息
小结: MotionWarping的思路就是对移动组件需要用到RootMotion数据的前后(Pre/Post)进行数据额外处理, 最终反馈给移动组件进行更进步的RootMotion的处理
动画通知
MotionWarping
使用第一步就是往动画里添加动画通知
看了源码发现这个通知不按常理出牌, Begin/Tick/End
虚函数都没重写, 所以, 这个通知就真的只是一个配置作用
如果动画通知是放在AnimSequence上的, 那么需要勾选MotionWarping组件的bSearchForWindowsInAnimsWithinMontage选项, 因为默认是只在蒙太奇内查找, 勾选以后会额外从蒙太奇的每个片段动画序列中去查找, 同时会增加开销
UMotionWarpingComponent
Update()
看名字以为是Tick()
函数, 实则是上面从移动组件监听的代理的回调事件, 在Pre
过程中先调用Update()
函数, 换句话说就是RootMotion有更新的时候会先调用到这里
用伪代码来描述这个过程
1 | if(当前有播放蒙太奇) |
这里要稍微看一下通知里的一个方法 UAnimNotifyState_MotionWarping::OnBecomeRelevant()
1 | void UAnimNotifyState_MotionWarping::OnBecomeRelevant(UMotionWarpingComponent* MotionWarpingComp, const UAnimSequenceBase* Animation, float StartTime, float EndTime) const |
之前说的动画通知啥也没干也不完全正确, 这里动画通知创建了修改器(Modifier)对象, 把动画数据塞给它, 同时监听修改器类的回调事件, 然后调用到是蓝图事件
理论上可以进行蓝图扩展, 不过怎么感觉也不是很方便呢? 你给我一个const函数图我修改啥?
接下来就是遍历所有修改器对象刷新其状态, 如有必要广播修改器对象的内部代理事件, 然后移除标记为MarkedForRemoval
的修改器对象
WarpTargetMap
1 | UPROPERTY(Transient) |
只用通知不能运行MotionWarping, 需要通过代码或者蓝图Api往组件内添加WarpTargetMap成员
每个动画通知都配置有一个FName用来查找WarpTarget, 在完成以后会标记为移除
Modifier类
对RootMotion数据处理的核心就是这个类, 在插件内派生了下面几个类
URootMotionModifier
- URootMotionModifier_Warp
- URootMotionModifier_SkewWarp
- URootMotionModifier_AdjustmentBlendWarp
- UDEPRECATED_RootMotionModifier_SimpleWarp(这个在5.1中被作废了, 可以用SkewWarp代替)
- URootMotionModifier_Scale
接下来逐一细看
下面这部分主要是数据和计算, 可以略过
URootMotionModifier
基类, 声明了与动画相关的基础数据, 如开始/结束时间, 上一帧/当前帧的时间等等, 这些数据都不需要在动画通知中配置, 另外也定义了通知的状态枚举和相应的代理, 一部分声明属性如下
1 | TWeakObjectPtr<const UAnimSequenceBase> Animation = nullptr; |
FMotionWarpingTarget
作为适配目标信息的一个数据结构, 在MotionWarpingComponent中最为TMap存储起来, 主要属性如下
1 | FTransform Transform; |
URootMotionModifier_Warp
我们在蓝图中可以配置的属性都在这个类中申明, 在UE5.1中额外增加了WarpPointAnimProvider
以及于此相关属性
重写了update()
函数, 主要增加了EWarpPointAnimProvider
相关的逻辑
EWarpPointAnimProvider
主要用来定义目标Transform的空间, 默认为None, 即设定的目标Transform就是最终的目标, 可以选择静态或者骨骼两个类型来进行转换
- static
1 | if (WarpPointAnimProvider == EWarpPointAnimProvider::Static) |
所以就是静态的目标TargetTransform 进行了一个偏移运算
- Bone
1 | RootTransform = MeshCompRelativeRotInverse * Pose.GetComponentSpaceTransform(FCompactPoseBoneIndex(0)); |
类似的省略, 主要就是上述两行代码, 选择设定的BoneName的骨骼以及下一级骨骼的空间作为参考进行Transform变换, 暂时没想到可以用于哪种场景
EMotionWarpRotationType
旋转类型, 分Default
和Facing
两种
Default即保持原来的旋转
Facing即一直保持LookAt目标Location
URootMotionModifier_SkewWarp
计算都放在函数ProcessRootMotion()
中, 包括其他几个修改器类
用伪代码看看这个类怎么处理
1 | { |
简单概括就是通过改变RootMotion的delta位置和旋转信息来达到期望目标
URootMotionModifier_AdjustmentBlendWarp
计算比较复杂, 具体不展开, 参考知乎大佬的结论
Adjustment Blend Warp缩放算法采用与Simple Warp完全不同的缩放思路。该算法是在第一次缩放时,就计算好窗口期间,root骨骼在组件空间下的缩放差值,也就是root骨骼的Mesh空间缩放叠加动画,每帧对RootMotion源动画并应用缩放叠加动画,从而实现缩放。采用这种算法,不仅可以针对root骨骼叠加动画数据进行缩放,理论还能针对骨架上的任意骨骼进行同样的缩放,因此Adjustment Blend Warp缩放支持配置,针对ik骨骼进行缩放。比如对带脚步移动的RootMotion动画应用Adjustment Blend Warp缩放,不仅能够灵活缩放位移,配合TwoBone IK,还能规避缩放位移导致的腿部滑步表现。
URootMotionModifier_Scale
简单粗暴的在窗口期缩放RootMotion的数据, 适用场景有限
关于RootMotion
可以从上面得知, MotionWarping从动画资源中获取RootMotion数据, 那么有必要看一下这个数据怎么获取
还是从移动组件的PerformMovement()
开始
RootMotion数据是从下面代码中的TickCharacterPose()
获取的
1 | if( CharacterOwner->IsPlayingRootMotion() && CharacterOwner->GetMesh() ) |
首先需要判断是否处于RootMotion中, 然而看代码到深处, 看到如果RootMotion模式使用的默认RootMotionFromMontagesOnly的情况下, 那就取决于当前的MontageInstance
对象是否开启了RootMotion, 测试发现这个MontageInstance
在蒙太奇BlendOut的时候已经被清理了,
这个要从Montage的刷新开始看
上图看调用栈, 重点是要看一下什么时候清理, 看下面代码, 位于FAnimMontageInstance::Advance()
中
1 | if (PlayTimeToEnd <= FMath::Max<float>(BlendOutTriggerTime, KINDA_SMALL_NUMBER)) |
所以就解释清楚了
那么如果改成RootMotionForEveryThing
呢?
测试发现可以正常的进行计算, 但是RootMotion数据会因为混合权重值而进行缩放, 大致可以看下面位于AnimInstance中的代码
1 | // blend in any montage-blended root motion that we now have correct weights for |
这个ExtractedRootMotion
是被提取的RootMotion数据, 那么即使Modifier对象能拿到数据, 这个数据也是被缩放过的
那么理论上来说, 如果能拿到数据, 即使缩放过, 再反向补偿回去是否可行?
然而并不能
1 | void UMotionWarpingComponent::Update() |
上面这一行代码已经把MotionWarping的处理可以终止了, 因为已经拿不到MotionMontageInstance
对象了
总结
MotionWarping
改的是CharacterMovementComponent
对RootMotion处理阶段的中间数据MotionWarping
通过动态创建的Modifier修改器对象来修改- 动画通知用来配置时间窗口和修改器类型
- 需要配合动画通知窗口在通知运行之前设定目标位移/旋转
MotionWarping
依赖RootMotionInstance
对象, 此对象在蒙太奇BlendOut的时候会被清理, 所以BlendOut过程中没有MotionWarping
效果