离线重定向源码分析

前言

UE的动画重定向(离线模式)是个非常好用的功能, 但是最近遇到了一些特殊需求, 势必需要深入研究一下这个重定向的源码

离线重定向流程

配置

重定向的前提是需要创建一个Rig文件, Rig文件起到的是类似一个模板或者表的作用, 重点定义了骨骼的拓扑结构信息,即父子关系; 当然也有其他如骨骼变换的坐标系定义和骨骼名称

然后需要在Source骨骼和Target骨骼上都设置Rig信息, 同时也必须添加预览模型,然后就可以开始我们的重定向流程了, 下面简述一下大致流程

image-20211109162031935

image-20211109164250707

FTransformBase

Rig内保存的一个比较重要的数据结构, 保存了骨骼父子关系和其他重要信息

FTransformBase

  • FName Node
  • FTransformBaseConstraint[2] //2个成员,1个保存旋转,另一个位移
    • TArray
      • EConstraintTransform::Type> TranformType //坐标系, 绝对和相对两种
      • FName ParentSpace //父骨骼
      • float Weight = 0.f; //权重,一般为1

流程简要

  1. 入口在FAssetTypeActions_AnimationAsset::RetargetAnimationHandler()
  2. 然后调用EditorAnimUtils中的RetargetAnimations()
  3. 创建重定向非常重要的结构体↓FAnimationRetargetContext
  4. 从UPackage复制必要信息到RetargetContext, 同时在编辑器中创建(拷贝)若干份动画资源, 根据需求可能拷贝所有有引用关系的动画资源或蓝图
  5. 调用RetargetContext中的成员函数RetargetAnimations()开始重定向
    1. 刷新新旧骨骼的预览模型(所以预览模型是必须添加的)
    2. 处理曲线, 会移除掉UE中手动K帧的曲线
    3. 替换动画资源的骨骼ReplaceSkeleton(), 然后内部调用重点要研究的同时开销巨大的函数↓RemapTracksToNewSkeleton()
    4. 到此新的动画资源中已经保存了新的动画数据和骨架
  6. Runtime下根据骨架的重定向规则进行动画刷新 ↓重定向模式

FAnimationRetargetContext

重定向初期就构造的一个数据类型, 保存了如下图中重定向必要参数

image-20211109162253295

同时提供了几个非常重要的计算函数, 重定向核心计算就在下图中的两个函数中

image-20211109162323533

RemapTracksToNewSkeleton

重定向动画序列的时候这个函数会调用2次, 在查找所有引用的时候包含了自己, 结束了以后再调用一次自己的, 是否是多余的?

  1. 计算基于rig的原始动画模型空间变换信息, 骨骼空间的旋转和位移, ↓FillUpTransformBasedOnRig

image-20211109113148664

  1. 计算新旧模型空间的相对变换矩阵信息数组RelativeToNewSpaceBases 和 新旧骨骼空间位移比值OldToNewTranslationRatio

image-20211109113340561

这个比值计算有个注意点, 如果新骨架当前骨骼到父骨骼的位移为0, 那么这个缩放比也是0, 这个会影响后面计算↓请看

另外的, 如果新骨架的某一节骨骼没有设置rig, 那么下一级的骨骼就没有父骨骼, 在比值计算的时候会直接设置成1, 相对矩阵计算也会受影响, 这样导致的结果是很难预测的, 建议不要这样做

  1. 原始动画模型空间的动画数据转换成局部空间的Rig数据 ↓ConvertAnimationDataToRiggingData

image-20211109113723188

得到的数据结构是下图这样的

image-20211109134647219

  1. 2层遍历所有旧骨骼和动画帧数
    1. 从当前骨骼和动画帧提取局部变换信息
    2. 计算得到组件空间的动画信息TArray< TArray<FTransform> > ComponentSpaceAnimations, 该变量经过了比值缩放

image-20211109115201264

上面图中可以看到从动画中提取的局部变换信息进行了缩放, 系数是OldToNewTranslationRatio[NodeIndex],所以如果系数是0, 那么这一步就相当于没有位移了

  1. 同上2层遍历
    1. 通过上RelativeToNewSpaceBases * ComponentSpaceAnimations 得到转换过的模型空间的动画数据 ConvertedSpaceAnimations
    2. 填充转换过的本地空间的动画信息 ConvertedLocalSpaceAnimations

image-20211109133637304

  1. 通过上面5.6.7部的数据修改RiggingAnimationData, 这就是新骨骼的动画数据, 即本地空间变换信息

image-20211109134133740

image-20211109135425130

  1. 设置新骨骼

  2. 将上面最新的rig数据设置到新骨骼的动画数据

image-20211109135705036

image-20211109145656724

ConvertAnimationDataToRiggingData

  1. 通过↓GetSpaceBasedAnimationData获取下图动画数据FRawAnimSequenceTrack, 该数据是模型空间

image-20211109105529625

  1. 按序号查找Rig节点名称, 比如tag_origin, 然后去新骨架查找对应的骨骼名称和序号

image-20211109111234463

  1. 获取约束数据

image-20211109110306607

  1. 从上面的约束信息获取父骨骼名称和序号

image-20211109111526336

  1. 基于父骨骼的模型空间信息 计算出 每一帧的相对变换信息; 填充到结果数据的AnimTrack中, 这里的动画数据是相对空间

image-20211109110445385

image-20211109105101355

GetSpaceBasedAnimationData

1
int32 UAnimSequence::GetSpaceBasedAnimationData(TArray< TArray<FTransform> >& AnimationDataInComponentSpace, FAnimSequenceTrackContainer * RiggingAnimationData) const

这个函数有点奇葩, 要分为有没有传入指针变量来区分

  • 没有指针

从模型中提取局部空间动画帧变换信息

image-20211109162904122

  • 可指针

从指针变量中提取

然后再通过 关键帧变换信息 * 父骨骼模型空间变换信息 = 当前骨骼当前帧数的模型空间变换信息

image-20211109163454341

FillUpTransformBasedOnRig

基于Rig获得原始动画模型空间变换信息, 骨骼空间的旋转和位移

如果Rig匹配的骨骼是None, 那么会设置为Identity数据

image-20211109151720633

同样, 也会将原本存在的模型空间数据设置为Identity数据作为输出数据

image-20211109153142042

FAnimSequenceTrackContainer

1
2
3
4
5
6
7
8
9
10
struct ENGINE_API FAnimSequenceTrackContainer
{
GENERATED_USTRUCT_BODY()

UPROPERTY()
TArray<struct FRawAnimSequenceTrack> AnimationTracks;//每个骨骼对应的每一帧模型空间的transform

UPROPERTY()
TArray<FName> TrackNames; //所有骨骼名称
}

GetSpaceBasedAnimationData

1
int32 UAnimSequence::GetSpaceBasedAnimationData(TArray< TArray<FTransform> >& AnimationDataInComponentSpace, FAnimSequenceTrackContainer * RiggingAnimationData) const;

获取组件空间的2维数组变换数据, 在计算中经常用到

AnimationDataInComponentSpace[骨骼序号][帧]

需求简述

骨架从根骨开始大致是如下, 括号中是新骨架

Joints(Joints)

  • tag_origin(Bip01)
    • main_root(Pelvis)
      • spine01(spine01)

层级是一样的, 但是有这么个情况, 我用简化模型来模拟这个案例

image-20211109161554773

如上图, Source骨架的第二层骨骼是跟根骨骼完全重合的, 然后main_root就放到盆骨位置

image-20211109161706255

Target骨架的对应骨骼是跟盆骨重合的

再来看动画素材

录制_2021_11_09_16_18_59_147

盆骨是有局部空间位移的, 那么在这种情况下能否正确重定向?

测试

↑RemapTracksToNewSkeleton第二步 中计算的结果

image-20211109104543482

image-20211109104551377

image-20211109104921112

这里第三个成员变量 即新骨骼的 pelvis 的 比值即Ratio=0, 这就导致在后面计算中把位移给去掉了

image-20211109164852530

尝试把这行代码修改或注释掉??

试一下!!!!

我们只看第20帧

原版

joints

image-20211109180025098

tag_origin

image-20211109180043306

main_root

image-20211109180112554


注释掉

Joints

image-20211117095714875

tag_origin

image-20211117095734576

main_root

image-20211117095747465

看这架势似乎没啥问题, 盆骨骨骼的相对位移也有了, 看最后成品

录制_2021_11_09_17_50_01_925

基本效果达到预期了, 这个只是为了测试而注释的, 这段代码最终的目的是解决形态不一致的骨架的匹配问题, 必须保留, 那么可以考虑加入额外的编辑器扩展选项, 这个坑略大, 暂时不讨论了

魔改代码

脑洞大开魔改一下相关代码, 在Rig类中的Nodes数据结构中加入两个属性, 看下图

image-20211110105139099

image-20211110104850681

属性界面使用了自定义的类, 在RigDetails中

然后在关键地方加个判断

image-20211110104906603

然后就可以自定义这个缩放比例了, 虽然也没多大鸟用

重定向模式

在FAnimationRuntime::RetargetBoneTransform中使用, 该函数在Runtime下持续调用, 此坑先留着, 以后待填

补充要点

特殊情况讨论

  1. 新骨架拥有旧骨架没有的骨骼, 那么第一步获取模型空间的变换信息的时候这些新骨架旧是Identify
  2. 从Rig如果找不到父节点, 那么模型空间的动画数据旧直接使用局部数据