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

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, clear
RootMotionParams.Clear()
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容