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()
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,682评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,277评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,083评论 0 355
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,763评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,785评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,624评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,358评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,261评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,722评论 1 315
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,900评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,030评论 1 350
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,737评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,360评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,941评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,057评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,237评论 3 371
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,976评论 2 355

推荐阅读更多精彩内容