• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 众包
  • 赞助商
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录

逍遥流

  • 博客园
  • 联系
  • 订阅
  • 管理

公告

View Post

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

posted on 2021-03-07 22:49  逍遥流  阅读(1516)  评论(0)    收藏  举报

刷新页面返回顶部
 
博客园  ©  2004-2025
浙公网安备 33010602011771号 浙ICP备2021040463号-3