离线重定向源码分析
前言
UE的动画重定向(离线模式)是个非常好用的功能, 但是最近遇到了一些特殊需求, 势必需要深入研究一下这个重定向的源码
配置
重定向的前提是需要创建一个Rig文件, Rig文件起到的是类似一个模板或者表的作用, 重点定义了骨骼的拓扑结构信息,即父子关系; 当然也有其他如骨骼变换的坐标系定义和骨骼名称
然后需要在Source骨骼和Target骨骼上都设置Rig信息, 同时也必须添加预览模型,然后就可以开始我们的重定向流程了, 下面简述一下大致流程
FTransformBase
Rig内保存的一个比较重要的数据结构, 保存了骨骼父子关系和其他重要信息
FTransformBase
- FName Node
- FTransformBaseConstraint[2] //2个成员,1个保存旋转,另一个位移
- TArray
- EConstraintTransform::Type> TranformType //坐标系, 绝对和相对两种
- FName ParentSpace //父骨骼
- float Weight = 0.f; //权重,一般为1
- TArray
流程简要
- 入口在
FAssetTypeActions_AnimationAsset::RetargetAnimationHandler()
- 然后调用
EditorAnimUtils
中的RetargetAnimations()
- 创建重定向非常重要的结构体↓FAnimationRetargetContext
- 从UPackage复制必要信息到
RetargetContext
, 同时在编辑器中创建(拷贝)若干份动画资源, 根据需求可能拷贝所有有引用关系的动画资源或蓝图 - 调用
RetargetContext
中的成员函数RetargetAnimations()
开始重定向- 刷新新旧骨骼的预览模型(所以预览模型是必须添加的)
- 处理曲线, 会移除掉UE中手动K帧的曲线
- 替换动画资源的骨骼
ReplaceSkeleton()
, 然后内部调用重点要研究的同时开销巨大的函数↓RemapTracksToNewSkeleton() - 到此新的动画资源中已经保存了新的动画数据和骨架
- Runtime下根据骨架的重定向规则进行动画刷新 ↓重定向模式
FAnimationRetargetContext
重定向初期就构造的一个数据类型, 保存了如下图中重定向必要参数
同时提供了几个非常重要的计算函数, 重定向核心计算就在下图中的两个函数中
RemapTracksToNewSkeleton
重定向动画序列的时候这个函数会调用2次, 在查找所有引用的时候包含了自己, 结束了以后再调用一次自己的, 是否是多余的?
- 计算基于rig的原始动画的模型空间变换信息, 骨骼空间的旋转和位移, ↓FillUpTransformBasedOnRig
- 计算新旧模型空间的相对变换矩阵信息数组
RelativeToNewSpaceBases
和 新旧骨骼空间位移比值OldToNewTranslationRatio
这个比值计算有个注意点, 如果新骨架当前骨骼到父骨骼的位移为0, 那么这个缩放比也是0, 这个会影响后面计算↓请看
另外的, 如果新骨架的某一节骨骼没有设置rig, 那么下一级的骨骼就没有父骨骼, 在比值计算的时候会直接设置成1, 相对矩阵计算也会受影响, 这样导致的结果是很难预测的, 建议不要这样做
- 把原始动画模型空间的动画数据转换成局部空间的Rig数据 ↓ConvertAnimationDataToRiggingData
得到的数据结构是下图这样的
- 2层遍历所有旧骨骼和动画帧数
- 从当前骨骼和动画帧提取局部变换信息
- 计算得到组件空间的动画信息
TArray< TArray<FTransform> > ComponentSpaceAnimations
, 该变量经过了比值缩放
上面图中可以看到从动画中提取的局部变换信息进行了缩放, 系数是
OldToNewTranslationRatio[NodeIndex]
,所以如果系数是0, 那么这一步就相当于没有位移了
- 同上2层遍历
- 通过上
RelativeToNewSpaceBases * ComponentSpaceAnimations
得到转换过的模型空间的动画数据ConvertedSpaceAnimations
- 填充转换过的本地空间的动画信息
ConvertedLocalSpaceAnimations
- 通过上
- 通过上面5.6.7部的数据修改
RiggingAnimationData
, 这就是新骨骼的动画数据, 即本地空间变换信息
设置新骨骼
将上面最新的rig数据设置到新骨骼的动画数据
ConvertAnimationDataToRiggingData
- 通过
↓GetSpaceBasedAnimationData
获取下图动画数据FRawAnimSequenceTrack
, 该数据是模型空间的
- 按序号查找Rig节点名称, 比如tag_origin, 然后去新骨架查找对应的骨骼名称和序号
- 获取约束数据
- 从上面的约束信息获取父骨骼名称和序号
- 基于父骨骼的模型空间信息 计算出 每一帧的相对变换信息; 填充到结果数据的AnimTrack中, 这里的动画数据是相对空间的
GetSpaceBasedAnimationData
1 | int32 UAnimSequence::GetSpaceBasedAnimationData(TArray< TArray<FTransform> >& AnimationDataInComponentSpace, FAnimSequenceTrackContainer * RiggingAnimationData) const |
这个函数有点奇葩, 要分为有没有传入指针变量来区分
- 没有指针
从模型中提取局部空间动画帧变换信息
- 可指针
从指针变量中提取
然后再通过 关键帧变换信息 * 父骨骼模型空间变换信息 = 当前骨骼当前帧数的模型空间变换信息
FillUpTransformBasedOnRig
基于Rig获得原始动画的模型空间变换信息, 骨骼空间的旋转和位移
如果Rig匹配的骨骼是None, 那么会设置为Identity数据
同样, 也会将原本存在的模型空间数据设置为Identity数据作为输出数据
FAnimSequenceTrackContainer
1 | struct ENGINE_API FAnimSequenceTrackContainer |
GetSpaceBasedAnimationData
1 | int32 UAnimSequence::GetSpaceBasedAnimationData(TArray< TArray<FTransform> >& AnimationDataInComponentSpace, FAnimSequenceTrackContainer * RiggingAnimationData) const; |
获取组件空间的2维数组变换数据, 在计算中经常用到
AnimationDataInComponentSpace[骨骼序号][帧]
需求简述
骨架从根骨开始大致是如下, 括号中是新骨架
Joints(Joints)
- tag_origin(Bip01)
- main_root(Pelvis)
- spine01(spine01)
- main_root(Pelvis)
层级是一样的, 但是有这么个情况, 我用简化模型来模拟这个案例
如上图, Source骨架的第二层骨骼是跟根骨骼完全重合的, 然后main_root就放到盆骨位置
Target骨架的对应骨骼是跟盆骨重合的
再来看动画素材
盆骨是有局部空间位移的, 那么在这种情况下能否正确重定向?
测试
在↑RemapTracksToNewSkeleton第二步 中计算的结果
这里第三个成员变量 即新骨骼的 pelvis 的 比值即Ratio=0, 这就导致在后面计算中把位移给去掉了
尝试把这行代码修改或注释掉??
试一下!!!!
我们只看第20帧
原版
joints
tag_origin
main_root
注释掉
Joints
tag_origin
main_root
看这架势似乎没啥问题, 盆骨骨骼的相对位移也有了, 看最后成品
基本效果达到预期了, 这个只是为了测试而注释的, 这段代码最终的目的是解决形态不一致的骨架的匹配问题, 必须保留, 那么可以考虑加入额外的编辑器扩展选项, 这个坑略大, 暂时不讨论了
魔改代码
脑洞大开魔改一下相关代码, 在Rig类中的Nodes数据结构中加入两个属性, 看下图
属性界面使用了自定义的类, 在RigDetails中
然后在关键地方加个判断
然后就可以自定义这个缩放比例了, 虽然也没多大鸟用
重定向模式
在FAnimationRuntime::RetargetBoneTransform中使用, 该函数在Runtime下持续调用, 此坑先留着, 以后待填
补充要点
特殊情况讨论
- 新骨架拥有旧骨架没有的骨骼, 那么第一步获取模型空间的变换信息的时候这些新骨架旧是Identify
- 从Rig如果找不到父节点, 那么模型空间的动画数据旧直接使用局部数据