找回密码
 立即注册
查看: 450|回复: 0

Unreal的骨骼动画系统的RootMotion原理剖析

[复制链接]
发表于 2021-3-24 19:15 | 显示全部楼层 |阅读模式
RootMotion

RootMotion的本质上就是先锁住动画位移,然后将动画轨迹提取出来,最后将动画轨迹应用到角色控制器。

背景:以下分析主要基于unreal4.21版本的源码
RootMotion的lockBone原理


将根骨骼锁住不动的核心原理在AnimSequence.cpp中:
void UAnimSequence::ResetRootBoneForRootMotion(FTransform& BoneTransform, const FBoneContainer& RequiredBones, ERootMotionRootLock::Type InRootMotionRootLock) const{    switch (InRootMotionRootLock)    {        case ERootMotionRootLock::AnimFirstFrame: BoneTransform = ExtractRootTrackTransform(0.f, &RequiredBones); break;        case ERootMotionRootLock::Zero: BoneTransform = FTransform::Identity; break;        default:        case ERootMotionRootLock::RefPose: BoneTransform = RequiredBones.GetRefPoseArray()[0]; break;    }    if (IsValidAdditive() && InRootMotionRootLock != ERootMotionRootLock::AnimFirstFrame)    {        //Need to remove default scale here for additives        BoneTransform.SetScale3D(BoneTransform.GetScale3D() - FVector(1.f));    }}
ERootMotionRootLock::Zero 将根骨骼的位置和旋转,lock在父空间的原点,x、y、z、pitch、yaw、roll都为0。

ERootMotionRootLock::AnimFirstFrame 将根骨骼的位置和旋转,lock在当前动画的第一帧

ERootMotionRootLock::RefPose 将根骨骼的位置和旋转,lock在骨骼的原始位置。所谓骨骼的原始位置,俗称"TPos", 与动画无关,角色做出来骨骼的时候就有的位置。
RootMotion轨迹的提取


读取轨迹的入口在USkeletalMeshComponent::ConsumeRootMotion
每帧提取的轨迹最终就存储UAnimInstance.ExtractedRootMotion


USkeletalMeshComponent::ConsumeRootMotion中InterpAlpha, 绝大多数的情况为1。

InterpAlpha不是1的情况: 开启一种特殊的优化开关(OptimizeMode == LookAheadMode),此开关默认关闭。

所以从入口函数直接跟进去,直接返回了UAnimInstance.ExtractedRootMotion
FRootMotionMovementParams UAnimInstance::ConsumeExtractedRootMotion(float Alpha){    // 这里的Alpha 就是 InterpAlpha, 只列举为1的情况     if (Alpha > (1.f - ZERO_ANIMWEIGHT_THRESH))    {        FRootMotionMovementParams RootMotion = ExtractedRootMotion;        //Clear,以保证下一帧的轨迹是全新的        ExtractedRootMotion.Clear();        return RootMotion;    }}
UAnimInstace.ExtractedRootMotion 每帧都在UAnimInstance::PostUpdateAnimation函数中赋值
void UAnimInstance::PostUpdateAnimation(){    bNeedsUpdate = false;    // acquire the proxy as we need to update    FAnimInstanceProxy& Proxy = GetProxyOnGameThread<FAnimInstanceProxy>();    // flip read/write index    // Do this first, as we'll be reading cached slot weights, and we want this to be up to date for this frame.    Proxy.TickSyncGroupWriteIndex();    Proxy.PostUpdate(this);    // 1 先取Proxy里的轨迹    if(Proxy.GetExtractedRootMotion().bHasRootMotion)    {        FTransform ProxyTransform = Proxy.GetExtractedRootMotion().GetRootMotionTransform();        ProxyTransform.NormalizeRotation();        ExtractedRootMotion.Accumulate(ProxyTransform);        Proxy.GetExtractedRootMotion().Clear();    }    // 2 再取Montage里面的轨迹    // blend in any montage-blended root motion that we now have correct weights for    for(const FQueuedRootMotionBlend& RootMotionBlend : RootMotionBlendQueue)    {        const float RootMotionSlotWeight = GetSlotNodeGlobalWeight(RootMotionBlend.SlotName);        const float RootMotionInstanceWeight = RootMotionBlend.Weight * RootMotionSlotWeight;        ExtractedRootMotion.AccumulateWithBlend(RootMotionBlend.Transform, RootMotionInstanceWeight);    }    // We may have just partially blended root motion, so make it up to 1 by    // blending in identity too    // 3 如果当前权重小于1, 则用FTransform::Identity补齐到1    if (ExtractedRootMotion.bHasRootMotion)    {        ExtractedRootMotion.MakeUpToFullWeight();    }}Proxy中轨迹: FAnimInstanceProxy::ExtractedRootMotion


具体提取轨迹的代码在FAnimInstanceProxy::UpdateAnimation => FAnimInstanceProxy::TickAssetPlayerInstances

提取轨迹的具体原理和Montage类似,见下文。

这部分提取的是BlendSpace或者AnimSequence的轨迹, 不包括montage的轨迹。结果保存在FAnimInstanceProxy::ExtractedRootMotion

注意: 只有当RootMotionMode == ERootMotionMode::RootMotionFromEverything, 才会提取这些轨迹
montage的轨迹


montage提取轨迹的入口在: UAnimInstance::UpdateAnimation => UpdateMontage => FAnimMontageInstance::Advance

在Advance函数中,最后通过AnimInstance::QueueRootMotionBlend将轨迹传回到AnimInstance

可以看到提取轨迹用的是 UAnimMontage::ExtractRootMotionFromTrackRange => UAnimCompositeBase::ExtractRootMotionFromTrack => UAnimSequence::ExtractRootMotionFromRange

其实提取轨迹,无论是BlendSpace,还是Montage,最终都会用UAnimSequence::ExtractRootMotionFromRange,核心代码摘抄如下:
FTransform UAnimSequence::ExtractRootMotionFromRange(float StartTrackPosition, float EndTrackPosition) const{        const FVector DefaultScale(1.f);    // 第0帧的Transform    FTransform InitialTransform = ExtractRootTrackTransform(0.f, NULL);    // 上一帧的Transform    FTransform StartTransform = ExtractRootTrackTransform(StartTrackPosition, NULL);    // 当前帧的Transform    FTransform EndTransform = ExtractRootTrackTransform(EndTrackPosition, NULL);    // Transform to Component Space Rotation (inverse root transform from first frame)    // 第0帧的逆矩阵    const FTransform RootToComponentRot = FTransform(InitialTransform.GetRotation().Inverse());    // 上一帧相对于第0帧的Transform    StartTransform = RootToComponentRot * StartTransform;    // 当前帧相对于第0帧的Transform    EndTransform = RootToComponentRot * EndTransform;    // 返回 当前帧的Transform - 上一帧的Transform    return EndTransform.GetRelativeTransform(StartTransform);}RootMotion轨迹的应用


RootMotion的应用主要在CharacterMovementComponent.cpp

对于ROLE_Authority类型的Character(例如Player), rootMotion的应用入口在: TickComponent => PerformMovement

对于ROLE_SimulatedProxy类型的Character(例如非Player的Avatar), rootMotion的入口在: TickComponent => SimulatedTick => SimulateRootMotion
获取轨迹的提取结果


首先会调用函数 TickCharacterPose

TickCharacterPose 函数中,会读取USkeletalMeshComponent中提取的RootMotion轨迹,更新到RootMotionParams
void UCharacterMovementComponent::TickCharacterPose(float DeltaTime){    USkeletalMeshComponent* CharacterMesh = CharacterOwner->GetMesh();    // bAutonomousTickPose is set, we control TickPose from the Character's Movement and Networking updates, and bypass the Component's update.    // (Or Simulating Root Motion for remote clients)    //@zvn6761 bIsAutonomousTickPose 保证了: 即使当前帧已经调用过TickPose, ShouldTickPose可以return True。这个算是UE4自己的黑科技了    CharacterMesh->bIsAutonomousTickPose = true;    if (CharacterMesh->ShouldTickPose())    {        // Keep track of if we're playing root motion, just in case the root motion montage ends this frame.        const bool bWasPlayingRootMotion = CharacterOwner->IsPlayingRootMotion();        CharacterMesh->TickPose(DeltaTime, true);        // Grab root motion now that we have ticked the pose        if (CharacterOwner->IsPlayingRootMotion() || bWasPlayingRootMotion)        {            FRootMotionMovementParams RootMotion = CharacterMesh->ConsumeRootMotion();            if (RootMotion.bHasRootMotion)            {                RootMotion.ScaleRootMotionTranslation(CharacterOwner->GetAnimRootMotionTranslationScale());                RootMotionParams.Accumulate(RootMotion);            }        }    }    //和上文呼应,将bIsAutonomousTickPose 改为默认值    CharacterMesh->bIsAutonomousTickPose = false;}RootMotion实现角色移动 和 旋转


RootMotion 最后是通过改变CharacterMovementComponent.Velocity来实现角色移动
AnimRootMotionVelocity = CalcAnimRootMotionVelocity(RootMotionParams.GetRootMotionTransform().GetTranslation(), DeltaSeconds, Velocity);Velocity = ConstrainAnimRootMotionVelocity(AnimRootMotionVelocity, Velocity);
RootMotion 最后是通过MoveUpdateComponent
const FQuat OldActorRotationQuat = UpdatedComponent->GetComponentQuat();const FQuat RootMotionRotationQuat = RootMotionParams.GetRootMotionTransform().GetRotation();if( !RootMotionRotationQuat.IsIdentity() ){    const FQuat NewActorRotationQuat = RootMotionRotationQuat * OldActorRotationQuat;    MoveUpdatedComponent(FVector::ZeroVector, NewActorRotationQuat, true);}最后清理当前帧用过的轨迹


每帧, RootMotionParams用完之后都清理一下。

这样,下次执行RootMotionParams.Accumulate时候,才会相当于直接RootMotionParams.Set。
// Root Motion has been used, clearRootMotionParams.Clear()
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-20 17:31 , Processed in 0.131266 second(s), 25 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表