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()