角色动画——RootMotion全解

news2024/10/5 8:59:30

1. Unity(2022)的应用

由Animtor组件控制

在这里插入图片描述

在Animation Clip下可进行详细设置

在这里插入图片描述

​ 官方文档的介绍(Animation选项卡 - Unity 手册)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
上述动画类型在Rag选项卡中设置:

在这里插入图片描述

Rig 选项卡上的设置定义了 Unity 如何将变形体映射到导入模型中的网格,以便能够将其动画化。
对于人形 (Humanoid) 角色,这意味着需要分配或创建 Avatar(ps:有时,将动画限制为特定的身体部位会很有用。例如,在一个行走动画中,角色可能会挥动他们的手臂,但如果他们拿起火炬,他们应该将火炬举起来投光。可以使用 Avatar 身体遮罩 (Avatar BodyMask) 来指定应将动画限制在角色的哪些部位)。
对于非人形角色(通用(Generic)角色),这意味着需要在骨架中确定根骨骼。
默认情况下,在项目视图中选择模型时,Unity会确定哪个动画类型 (Animation Type)与所选的模型最匹配,然后将其显示在 Rig 选项卡中。如果 Unity 从未导入该文件,则 Animation Type 设置为 None。动画类型如下:

属性:功能:
Animation Type指定动画类型。
None不存在动画
Legacy使用旧版动画系统。与 Unity 3.x 及更早版本一样导入和使用动画。
Generic如果骨架为非人形(四足动物或任何要动画化的实体),请使用通用动画系统。Unity 会选择一个根节点,但可以确定另一个用作__根节点__的骨骼。
Humanoid如果骨架为人形(有两条腿、两条手臂和一个头),请使用人形动画系统。Unity 通常会检测骨架并将其正确映射到 Avatar。有些情况下,可能需要更改 Avatar 定义 (Avatar Definition) 并手动对映射进行__配置 (Configure)__。

2. Unreal(5.4)的应用

Unreal中根骨骼的定义是指向角色两脚中心的一个虚拟骨骼。要使用RootMotio功能,首先需要通过在动画序列编辑器编辑动画,设置RootMotion的启用:

在这里插入图片描述

强制根骨骼锁定和屏蔽根骨骼位移从效果上看是一样的,均是固定根骨骼的位置。RootMotion的使用设置如下表:

启用根运动(EnableRootMotion)启用后,将允许提取根运动。使用动画蓝图的类默认属性 根运动模式(Root Motion Mode) 来定义如何提取根运动。
根运动根锁(Root Motion Root Lock)在提取根运动时将根骨骼锁定在定义的位置。可以用以下选项来锁定根骨骼: 参考姿势(Ref Pose):将根骨骼锁定在其在骨骼网格体 参考姿势(Reference Pose) 中的位置。 动画第一帧(Anim First Frame):将根骨骼锁定在选中动画的 第一帧 的位置。 零(Zero):将根骨骼锁定在网格体相对坐标的0,0,0位置。
强制根锁(Force Root Lock)启用后,强制施加根骨骼锁定,即使未启用 根运动(Root Motion) 也是如此。
使用规格化根运动比例(Use Normalized Root Motion Scale)启用后,将对提取的根运动使用规格化比例值。FVector(1.0, 1.0, 1.0)。

​ 然后在动画蓝图中设置,选择是否导出编辑后动画的RootMotion。

在这里插入图片描述

​需要注意的是,即使动画开启了RootMotion开关,但在动画蓝图中没有设置导出RootMotion,场景中的角色依旧做原动画,即根骨骼的运动没有被导出。

无根运动提取(No Root Motion Extraction)根运动(Root Motion)按原样保留(应用到根骨骼)。
忽略根运动(Ignore Root Motion)提取根运动(Root Motion)(并从根骨骼中移除根运动),但不应用到角色。
来自每一项目的根运动(Root Motion from Everything)提取每个帮助构建最终角色姿势的动画资源的根运动。每一部分的提取根运动均根据构成该姿势的源资产的权重进行混合。
仅来自蒙太奇的根运动(Root Motion from Montages Only)仅从启用了根动作的动画蒙太奇中提取根动作。

​启用RootMotion并且在动画蓝图中定义好RootMotion提取的应用方式后,动画会在播放时驱动动作组件。如下图展示,开启RootMotion的角色蓝图会将他根骨骼的运动附加到角色网格体所属的胶囊体上,所以胶囊体会随着角色的移动而变化。

在这里插入图片描述

​ 虚幻引擎中的根运动 | 虚幻引擎 5.4 文档 | EpicDeveloper Community (epicgames.com)

3.使用视频

1.Unity2022的使用

RootMotion Unity的演示视频

2.Unreal 5.4的使用

RootMotion Unreal的演示视频

4.RootMotion Unreal5.4源码

分为两部分:1.RootMotion提取;2.RootMotion应用。

1.RootMotion提取

从UE5.4动画播放接口UpdateAnimation为entry point:

void UAnimInstance::UpdateAnimation(float DeltaSeconds, bool bNeedsValidRootMotion, EUpdateAnimationFlag UpdateFlag)
{
//....
// need to update montage BEFORE node update or Native Update.
// so that node knows where montage is
=>	UpdateMontage(DeltaSeconds);
	// now we know all montage has advanced
	// time to test sync groups
	UpdateMontageSyncGroup();

	// Update montage eval data, to be used by AnimGraph Update and Evaluate phases.
	UpdateMontageEvaluationData();
//....
}
void UAnimInstance::UpdateMontage(float DeltaSeconds)
{
	//...
	// update montage weight
	Montage_UpdateWeight(DeltaSeconds);

	// update montage should run in game thread
	// if we do multi threading, make sure this stays in game thread. 
	// This is because branch points need to execute arbitrary code inside this call.
=>	Montage_Advance(DeltaSeconds);
	// ...
}
void UAnimInstance::UpdateMontage(float DeltaSeconds)
{
	//...

	for (int32 InstanceIndex = 0; InstanceIndex < MontageInstances.Num(); InstanceIndex++)
	{
		FAnimMontageInstance* const MontageInstance = MontageInstances[InstanceIndex];
		// should never be NULL
		ensure(MontageInstance);
		if (MontageInstance && MontageInstance->IsValid())
		{
			// 动画混合的情况
			bool const bUsingBlendedRootMotion = (RootMotionMode == ERootMotionMode::RootMotionFromEverything);
			// 判断是否在动画序列中打开了RootMotion开关
			bool const bNoRootMotionExtraction = (RootMotionMode == ERootMotionMode::NoRootMotionExtraction);

			// Extract root motion if we are using blend root motion 								
            // (RootMotionFromEverything) or if we are set to extract root 
			// motion AND we are the active root motion instance. This is so we can make 			
            // root motion deterministic for networking when
			// we are not using RootMotionFromEverything
			bool const bExtractRootMotion = !MontageInstance->IsRootMotionDisabled() && (bUsingBlendedRootMotion || (!bNoRootMotionExtraction && (MontageInstance == GetRootMotionMontageInstance())));

			FRootMotionMovementParams LocalExtractedRootMotion;
			FRootMotionMovementParams* RootMotionParams = nullptr;
			if (bExtractRootMotion)
			{
			// 开启RootMotion的情况下:RootMotionParams = ExtractedRootMotion(为AnimInstantce.h的成员变量)。
				RootMotionParams = (RootMotionMode != ERootMotionMode::IgnoreRootMotion) ? &ExtractedRootMotion : &LocalExtractedRootMotion;
			}

			MontageInstance->MontageSync_PreUpdate();
=>			MontageInstance->Advance(DeltaSeconds, RootMotionParams, bUsingBlendedRootMotion);
			
			// ...
		}
	}
	// ...
}
AnimInstantce.h:
//...
// Root motion read from proxy (where it is calculated) 
// and stored here to avoid potential stalls by calling GetProxyOnGameThread.
// Utility struct to accumulate root motion.!!!
FRootMotionMovementParams ExtractedRootMotion;
void FAnimMontageInstance::Advance(float DeltaTime, struct FRootMotionMovementParams* OutRootMotionParams, bool bBlendRootMotion)
{
	if (IsValid())
	{
		// with custom curves, we can't just filter by weight
		// also if you have custom curve with longer 0, you'll likely to pause montage during that blending time
		// I think that is a bug. It still should move, the weight might come back later. 
		if (bPlaying)
		{
			const bool bExtractRootMotion = (OutRootMotionParams != nullptr) 
			&& Montage->HasRootMotion();
			// ...
			// Extract Root Motion for this time slice, and accumulate it.
			// IsRootMotionDisabled() can be changed by AnimNotifyState BranchingPoints 			
            // while advancing, so it needs to be checked here.
			if (bExtractRootMotion && AnimInstance.IsValid() && !IsRootMotionDisabled())
			{
=>				const FTransform RootMotion = Montage
				->ExtractRootMotionFromTrackRange(PreviousSubStepPosition, Position);
				if (bBlendRootMotion)
				{
					// Defer blending in our root motion until after we get our slot 						
                    // weight updated
					const float Weight = Blend.GetBlendedValue();
					AnimInstance.Get()->QueueRootMotionBlend(RootMotion, Montage-							>SlotAnimTracks[0].SlotName, Weight);
				}
				else
				{
                      // Component Space中的RootMotion Transform数据Accumulate
					OutRootMotionParams->Accumulate(RootMotion);
				}
			}
		}
 	}
}
/** Extract RootMotion Transform from a contiguous Track position range.
 * *CONTIGUOUS* means that if playing forward StartTractPosition < EndTrackPosition.
 * No wrapping over if looping. No jumping across different sections.
 * So the AnimMontage has to break the update into contiguous pieces to handle those cases.
 *
 * This does handle Montage playing backwards (StartTrackPosition > EndTrackPosition).
 *
 * It will break down the range into steps if needed to handle looping animations, or different animations.
 * These steps will be processed sequentially, and output the RootMotion transform in component space.
 */
FTransform UAnimMontage::ExtractRootMotionFromTrackRange(float StartTrackPosition, float EndTrackPosition) const
{
	FRootMotionMovementParams RootMotion;
	// For now assume Root Motion only comes from first track.
	if( SlotAnimTracks.Num() > 0 )
	{
		const FAnimTrack& SlotAnimTrack = SlotAnimTracks[0].AnimTrack;

		// Get RootMotion pieces from this track.
		// We can deal with looping animations, or multiple animations. So we break those 		  
         // up into sequential operations.
		// (Animation, StartFrame, EndFrame) so we can then extract root motion sequentially.
	=>	ExtractRootMotionFromTrack(SlotAnimTrack, StartTrackPosition, EndTrackPosition, RootMotion);
	}
	return RootMotion.GetRootMotionTransform();
}
void UAnimCompositeBase::ExtractRootMotionFromTrack(const FAnimTrack &SlotAnimTrack, float StartTrackPosition, float EndTrackPosition, FRootMotionMovementParams &RootMotion) const
{
        TArray<FRootMotionExtractionStep> RootMotionExtractionSteps;
        // 从Animation Track中根据Position获取动画数据
    =>	SlotAnimTrack.GetRootMotionExtractionStepsForTrackRange(RootMotionExtractionSteps, StartTrackPosition, EndTrackPosition);

    // Go through steps sequentially, extract root motion, and accumulate it.
    // This has to be done in order so root motion translation & rotation is applied properly (as translation is relative to rotation)
    for (int32 StepIndex = 0; StepIndex < RootMotionExtractionSteps.Num(); StepIndex++)
    {
        // 遍历所有的动画序列(Montage combine several animation sequunces int oa single asset)
        const FRootMotionExtractionStep & CurrentStep = RootMotionExtractionSteps[StepIndex];
        if (CurrentStep.AnimSequence->bEnableRootMotion)
        {
            // 开启RootMotion,提取RootMotion数据
    =>		FTransform DeltaTransform = CurrentStep.AnimSequence->ExtractRootMotionFromRange(CurrentStep.StartPosition, CurrentStep.EndPosition);
            // 设置Root Motion的Transfrom,将DeletaTransfrom应用为现有的根运动
            RootMotion.Accumulate(DeltaTransform);

        }
    }
}
void Accumulate(const FTransform& InTransform)
{
	if (!bHasRootMotion)
	{
     	// 在开启RootMotion后,根骨骼Transform被导出
        Set(InTransform);
	}
	else
	{
		RootMotionTransform = InTransform * RootMotionTransform;
		RootMotionTransform.SetScale3D(RootMotionScale);
	}
}

从AnimSegment(存储动画序列–this is animation segment that defines what animation and how)对象中获取相应的动画序列AnimatonSequence和开始位置以及结束位置。

/** 
 * Given a Track delta position [StartTrackPosition, EndTrackPosition]
 * See if any AnimSegment overlaps any of it, and if any do, break them up into a sequence of FRootMotionExtractionStep.
 * Supports animation playing forward and backward. Track range should be a contiguous range, not wrapping over due to looping.
 */
void FAnimTrack::GetRootMotionExtractionStepsForTrackRange(TArray<FRootMotionExtractionStep> & RootMotionExtractionSteps, const float StartTrackPosition, const float EndTrackPosition) const
{
	for(int32 AnimSegmentIndex=0; AnimSegmentIndex<AnimSegments.Num();AnimSegmentIndex++)
	{
		const FAnimSegment& AnimSegment = AnimSegments[AnimSegmentIndex];
=>		AnimSegment.GetRootMotionExtractionStepsForTrackRange(RootMotionExtractionSteps, StartTrackPosition, EndTrackPosition);
	}
}

AnimSegment.GetRootMotionExtractionStepsForTrackRange主要是根据StartTrackPosition和EndTrackPosition从对FRootMotionExtractionStep容器的Add,FRootMotionExtractionStep结构如下所示:

/** Struct defining a RootMotionExtractionStep.
 * When extracting RootMotion we can encounter looping animations (wrap around), or different animations.
 * We break those up into different steps, to help with RootMotion extraction, 
 * as we can only extract a contiguous range per AnimSequence.
 */
USTRUCT()
struct FRootMotionExtractionStep
{
	GENERATED_USTRUCT_BODY()

	/** AnimSequence ref */
	UPROPERTY()
	TObjectPtr<UAnimSequence> AnimSequence;

	/** Start position to extract root motion from. */
	UPROPERTY()
	float StartPosition;

	/** End position to extract root motion to. */
	UPROPERTY()
	float EndPosition;

	FRootMotionExtractionStep() 
		: AnimSequence(nullptr)
		, StartPosition(0.f)
		, EndPosition(0.f)
		{
		}

	FRootMotionExtractionStep(UAnimSequence * InAnimSequence, float InStartPosition, float InEndPosition) 
		: AnimSequence(InAnimSequence)
		, StartPosition(InStartPosition)
		, EndPosition(InEndPosition)
	{
	}
};

其中TObjectPtr AnimSequence传入的是FAnimSegment对象的TObjectPtr AnimReference;FAnimaSegement的主要成员变量如下所示:

/** this is anim segment that defines what animation and how **/
USTRUCT()
struct FAnimSegment
{
	GENERATED_USTRUCT_BODY()
    PRAGMA_DISABLE_DEPRECATION_WARNINGS
    FAnimSegment(const FAnimSegment&) = default;
    FAnimSegment(FAnimSegment&&) = default;
    FAnimSegment& operator=(const FAnimSegment&) = default;
    FAnimSegment& operator=(FAnimSegment&&) = default;
    PRAGMA_ENABLE_DEPRECATION_WARNINGS

    UE_DEPRECATED(5.1, "Public access to AnimReference has been deprecated, use Set/Get-AnimReference instead")
    /** Anim Reference to play - only allow AnimSequence or AnimComposite **/
    UPROPERTY(EditAnywhere, Category=AnimSegment, meta=(DisplayName = "Animation Reference"))
    TObjectPtr<UAnimSequenceBase> AnimReference;
    #if WITH_EDITORONLY_DATA
        UPROPERTY()
        float CachedPlayLength = 0.f;
    #endif
    #if WITH_EDITOR
        friend class UEditorAnimSegment;
        friend class UEditorAnimCompositeSegment;
        ENGINE_API void UpdateCachedPlayLength();
    #endif // WITH_EDITOR
 ...   
}

从AnimationSegment中获取到AnimSequence以及startPosition和endPosition后,接下来遍历获取到的AnimSequence,根据对应的start/end position来获取Transform。

回到 UAnimCompositeBase::ExtractRootMotionFromTrack方法中,获取到Animation Sequence中的数据后,接下来就是导出根骨骼运动数据:

// Extract Root Motion transform from a contiguous position range (no looping)
FTransform UAnimSequence::ExtractRootMotionFromRange(float StartTrackPosition, float EndTrackPosition) const
{
	const FVector DefaultScale(1.f);
	FTransform RootTransformRefPose = FTransform::Identity;
	if (const USkeleton* MySkeleton = GetSkeleton())
	{
		const FReferenceSkeleton& RefSkeleton = MySkeleton->GetReferenceSkeleton();
		if (RefSkeleton.GetNum() > 0)
		{
            // 获取根骨骼的世界变化:Component Space => RootBone Space
			RootTransformRefPose = RefSkeleton.GetRefBonePose()[0];
		}
	}
	// 读取Offset Transform
 => FTransform StartTransform = ExtractRootTrackTransform(StartTrackPosition, nullptr);
    FTransform EndTransform = ExtractRootTrackTransform(EndTrackPosition, nullptr);

    // Use old calculation if needed.
    if (bUseNormalizedRootMotionScale)
    {
        //Clear scale as it will muck up GetRelativeTransform
        StartTransform.SetScale3D(FVector(1.f));
        EndTransform.SetScale3D(FVector(1.f));
    }
    else
    {
        if (IsValidAdditive())
        {
            StartTransform.SetScale3D(StartTransform.GetScale3D() + DefaultScale);
            EndTransform.SetScale3D(EndTransform.GetScale3D() + DefaultScale);
        }
    }

    // Transform to Component Space
    // 取逆:RootBone Space => Component Space; 最终变化矩阵 = RootToComponent Matrix * Offset Transform(Root Space)
    const FTransform RootToComponent = RootTransformRefPose.Inverse();
    StartTransform = RootToComponent * StartTransform;
    EndTransform = RootToComponent * EndTransform;

	return EndTransform.GetRelativeTransform(StartTransform);
}

读取根骨骼坐标系下的Offset Transform

// Time = StartTrackPosition / EndTrackPosition 在根骨骼运动Track中的位置
FTransform UAnimSequence::ExtractRootTrackTransform(float Time, const FBoneContainer * RequiredBones) const
{
	FTransform RootTransform;
    // 根据根骨骼id 获取RootTransform
=>	GetBoneTransform(RootTransform, FSkeletonPoseBoneIndex(RootBoneIndex), 						static_cast<double>(Time), 
		PRAGMA_DISABLE_DEPRECATION_WARNINGS
		bUseRawDataOnly
		PRAGMA_ENABLE_DEPRECATION_WARNINGS);
	return RootTransform;
}
	/** IAnimationDataModel instance containing (source) animation data */
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Animation Model")
	TScriptInterface<IAnimationDataModel> DataModelInterface;
...
void UAnimSequence::GetBoneTransform(FTransform& OutAtom, FSkeletonPoseBoneIndex BoneIndex, double Time, bool bUseRawData) const
{
    //...
    // 这里实际Run的是压缩后的transform_track数据,但不存在可读性,所以只展示未压缩的代码
    const FName BoneName = GetSkeleton()->GetReferenceSkeleton().GetBoneName(BoneIndex.GetInt());
    // 根据Frame获取骨骼的原始变化信息
    OutAtom = DataModelInterface->EvaluateBoneTrackTransform(BoneName, DataModelInterface->GetFrameRate().AsFrameTime(Time), Interpolation);

    // 查找相应骨骼Track的变化曲线,并根据id获取Curve曲线
    const FAnimationCurveIdentifier TransformCurveId(BoneName, ERawCurveTrackTypes::RCT_Transform);
    if (const FTransformCurve* TransformCurvePtr = DataModelInterface->FindTransformCurve(TransformCurveId))
    {
        // 根据变化曲线获取当前位置的附加变化
        const FTransform AdditiveTransform = TransformCurvePtr->Evaluate(Time, 1.f);
        const FTransform LocalTransform = OutAtom;
        OutAtom.SetRotation
            (LocalTransform.GetRotation() * AdditiveTransform.GetRotation());
        OutAtom.SetTranslation
            (LocalTransform.TransformPosition(AdditiveTransform.GetTranslation()));
        OutAtom.SetScale3D
            (LocalTransform.GetScale3D() * AdditiveTransform.GetScale3D());	
    }
}
template<typename T>
FORCEINLINE TVector<T> TTransform<T>::TransformPosition(const TVector<T>& V) const
{
    DiagnosticCheckNaN_All();
   
    const TransformVectorRegister InputVectorW0 = VectorLoadFloat3_W0(&V);

    //Transform using QST is following
    //QST(P) = Q.Rotate(S*P) + T where Q = quaternion, S = scale, T = translation

    //RotatedVec = Q.Rotate(Scale*V.X, Scale*V.Y, Scale*V.Z, 0.f)
    // 先缩放,再旋转,最后平移
    // 在原始向量上应用缩放变化
    const TransformVectorRegister ScaledVec = VectorMultiply(Scale3D, InputVectorW0);
    // 在缩放后的向量上应用旋转变化
    const TransformVectorRegister RotatedVec = VectorQuaternionRotateVector(Rotation, ScaledVec);
	// 在缩放,旋转后的向量上应用位移变化
    const TransformVectorRegister TranslatedVec = VectorAdd(RotatedVec, Translation);

    TVector<T> Result;
    VectorStoreFloat3(TranslatedVec, &Result);
    return Result;
}

StartTransform和EndTransform都获取到后,EndTransform.GetRelativeTransform(StartTransform)计算Relative Transform。

template<typename T>
TTransform<T> TTransform<T>::GetRelativeTransform(const TTransform<T>& Other) const
{
	// A * B(-1) = VQS(B)(-1) (VQS (A))
	// 
	// Scale = S(A)/S(B)
	// Rotation = Q(B)(-1) * Q(A)
	// Translation = 1/S(B) *[Q(B)(-1)*(T(A)-T(B))*Q(B)]
	// where A = this, B = Other
	TTransform<T> Result;
if (Other.IsRotationNormalized() == false)
{
	return TTransform<T>::Identity;
}

if (Private_AnyHasNegativeScale(this->Scale3D, Other.Scale3D))
{
	// @note, if you have 0 scale with negative, you're going to lose rotation as it can't convert back to quat
	GetRelativeTransformUsingMatrixWithScale(&Result, this, &Other);
}
else
{
    // 计算相对变化
	// Scale = S(A)/S(B)
	static ScalarRegister STolerance(UE_SMALL_NUMBER);
    
	TransformVectorRegister VSafeScale3D = VectorSet_W0(GetSafeScaleReciprocal(Other.Scale3D, STolerance));
	// 相对缩放
    TransformVectorRegister VScale3D = VectorMultiply(Scale3D, VSafeScale3D);
	// 计算相对位移
	//VQTranslation = (  ( T(A).X - T(B).X ),  ( T(A).Y - T(B).Y ), ( T(A).Z - T(B).Z), 0.f );
	TransformVectorRegister VQTranslation = VectorSet_W0(VectorSubtract(Translation, Other.Translation));

    // 计算旋转后的相对位移,将StartTransform的Rotation取逆然后应用到相对位移VQTranslation上
	// Inverse RotatedTranslation
	TransformVectorRegister VInverseRot = VectorQuaternionInverse(Other.Rotation);
	TransformVectorRegister VR = VectorQuaternionRotateVector(VInverseRot, VQTranslation);
	
    // 将StartTransform的Scale应用到相对旋转上,得到旋转,缩放后的相对位移
	//Translation = 1/S(B)
	TransformVectorRegister VTranslation = VectorMultiply(VR, VSafeScale3D);

    // 将StartTransfrom的Inverse Rotation应用到EndTransform的Rotation,计算出相对旋转
	// Rotation = Q(B)(-1) * Q(A)
	TransformVectorRegister VRotation = VectorQuaternionMultiply2(VInverseRot, Rotation);

	Result.Scale3D = VScale3D;
	Result.Translation = VTranslation;
	Result.Rotation = VRotation;

	Result.DiagnosticCheckNaN_All();
	#if DEBUG_INVERSE_TRANSFORM
		TMatrix<T> AM = ToMatrixWithScale();
		TMatrix<T> BM = Other.ToMatrixWithScale();

		Result.DebugEqualMatrix(AM *  BM.InverseFast());
#endif
	}
	return Result;
}

关于RootMotion开启后,根骨骼运动的提取方式如下:

在这里插入图片描述

2.RootMotion应用

经过的上面RootMotion提取,根骨骼的Transform数据被存储在AnimInstance.h的FRootMotionMovementParams ExtractedRootMotion;变量中,若要观察RootMotion数据的应用,只需要追踪这个变量的使用。

void UCharacterMovementComponent::TickCharacterPose(float DeltaTime)
{
	if (DeltaTime < UCharacterMovementComponent::MIN_TICK_TIME)
	{
		return;
    }
    check(CharacterOwner && CharacterOwner->GetMesh());
    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)
    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)
        {
        	// 获取Animation Instance的RootMotion数据
       =>   FRootMotionMovementParams RootMotion = CharacterMesh->ConsumeRootMotion();
            if (RootMotion.bHasRootMotion)
            {
                // bHasRootMotion此时为true
                RootMotion.ScaleRootMotionTranslation(CharacterOwner->GetAnimRootMotionTranslationScale());
                RootMotionParams.Accumulate(RootMotion);
            }
		}
	}
}
FRootMotionMovementParams USkeletalMeshComponent::ConsumeRootMotion()
{
    float InterpAlpha;
    if(bExternalTickRateControlled)
        InterpAlpha = ExternalInterpolationAlpha;
    else
        InterpAlpha = ShouldUseUpdateRateOptimizations() ? AnimUpdateRateParams->GetRootMotionInterp() : 1.f;
 => return ConsumeRootMotion_Internal(InterpAlpha);
}

这个AnimScriptInstance对象类型正是之前UpdateAnimation的接口UAnimInstance

FRootMotionMovementParams USkeletalMeshComponent::ConsumeRootMotion_Internal(float InAlpha)
{
	FRootMotionMovementParams RootMotion;
	if(AnimScriptInstance)
	{
	=>	RootMotion.Accumulate(AnimScriptInstance->ConsumeExtractedRootMotion(InAlpha));
        for(UAnimInstance* LinkedInstance : LinkedInstances)
        {
            RootMotion.Accumulate(LinkedInstance->ConsumeExtractedRootMotion(InAlpha));
        }
    }
    if(PostProcessAnimInstance)
    {
        RootMotion.Accumulate(PostProcessAnimInstance->ConsumeExtractedRootMotion(InAlpha));
    }
	return RootMotion;
}

ps:
{
	/** The active animation graph program instance. */
	UPROPERTY(transient, NonTransactional)
	TObjectPtr<UAnimInstance> AnimScriptInstance;
}

执行Animation Instance的ConsumeExtractedRootMotion,获取之前提取的RootMotion数据并返回。

FRootMotionMovementParams UAnimInstance::ConsumeExtractedRootMotion(float Alpha)
{
	if (Alpha < ZERO_ANIMWEIGHT_THRESH)
	{
		return FRootMotionMovementParams();
	}
	else if (Alpha > (1.f - ZERO_ANIMWEIGHT_THRESH))
	{
		FRootMotionMovementParams RootMotion = ExtractedRootMotion;
		ExtractedRootMotion.Clear();
		return RootMotion;
	}
	else
	{
		return ExtractedRootMotion.ConsumeRootMotion(Alpha);
	}
}

获取之后,回到 UCharacterMovementComponent::TickCharacterPose方法,

void UCharacterMovementComponent::TickCharacterPose(float DeltaTime)
{
       if (CharacterOwner->IsPlayingRootMotion() || bWasPlayingRootMotion)
        {
        	// 获取Animation Instance的RootMotion数据
       =>   FRootMotionMovementParams RootMotion = CharacterMesh->ConsumeRootMotion();
            if (RootMotion.bHasRootMotion)
            {
                // bHasRootMotion此时为true
                RootMotion.ScaleRootMotionTranslation(CharacterOwner->GetAnimRootMotionTranslationScale());
                RootMotionParams.Accumulate(RootMotion);
            }
		}
    	....
            
            
}
//CharacterMovementComponent对象中的RootMotionParams变量的定义,实为Animation Instance中的ExtractedRootMotion变量
/** Root Motion movement params. Holds result of anim montage root motion during PerformMovement(), and is overridden
*   during autonomous move playback to force historical root motion for MoveAutonomous() calls */
UPROPERTY(Transient)
FRootMotionMovementParams RootMotionParams;

而CharacterMovementComponent中的执行堆栈为:TickComponent->ControlledCharacterMove->PerfomMovement->TickCharacterPose。关于TickCharacterPose,在PerformMovement方法的执行如下:

void UCharacterMovementComponent::PerformMovement(float DeltaSeconds)
{
    // Prepare Root Motion (generate/accumulate from root motion sources to be used later)
    if (bHasRootMotionSources && !CharacterOwner->bClientUpdating && !CharacterOwner->bServerMoveIgnoreRootMotion)
    {
        // Animation root motion - If using animation RootMotion, tick animations before running physics.
        if( CharacterOwner->IsPlayingRootMotion() && CharacterOwner->GetMesh() )
        {
            =>		TickCharacterPose(DeltaSeconds);
            // Make sure animation didn't trigger an event that destroyed us
            if (!HasValidData())
            {
                return;
            }

            // For local human clients, save off root motion data so it can be used by movement networking code.
            if( CharacterOwner->IsLocallyControlled() && (CharacterOwner->GetLocalRole() == ROLE_AutonomousProxy) && CharacterOwner-				>IsPlayingNetworkedRootMotionMontage() )
            {
                CharacterOwner->ClientRootMotionParams = RootMotionParams;
            }
        }

        // Generates root motion to be used this frame from sources other than animation
        {
            SCOPE_CYCLE_COUNTER(STAT_CharacterMovementRootMotionSourceCalculate);
            CurrentRootMotion.PrepareRootMotion(DeltaSeconds, *CharacterOwner, *this, true);
        }

        // For local human clients, save off root motion data so it can be used by movement networking code.
        if( CharacterOwner->IsLocallyControlled() && (CharacterOwner->GetLocalRole() == ROLE_AutonomousProxy) )
        {
            CharacterOwner->SavedRootMotion = CurrentRootMotion;
        }
    }

    // Apply Root Motion to Velocity
    if( CurrentRootMotion.HasOverrideVelocity() || HasAnimRootMotion() )
    {
        // Animation root motion overrides Velocity and currently doesn't allow any other root motion sources
        if( HasAnimRootMotion() )
        {
            // Convert to world space (animation root motion is always local)
            USkeletalMeshComponent * SkelMeshComp = CharacterOwner->GetMesh();
            if( SkelMeshComp )
            {
                // Convert Local Space Root Motion to world space. Do it right before used by physics to make sure we use up to date 					// transforms, as translation is relative to rotation.
                =>		RootMotionParams.Set( ConvertLocalRootMotionToWorld(RootMotionParams.GetRootMotionTransform(), DeltaSeconds) );
            }

            // Then turn root motion to velocity to be used by various physics modes.
            if( DeltaSeconds > 0.f )
            {
                AnimRootMotionVelocity = CalcAnimRootMotionVelocity(RootMotionParams.GetRootMotionTransform().GetTranslation(), DeltaSeconds, Velocity);
                Velocity = ConstrainAnimRootMotionVelocity(AnimRootMotionVelocity, Velocity);
                if (IsFalling())
                {
                    Velocity += FVector(DecayingFormerBaseVelocity.X, DecayingFormerBaseVelocity.Y, 0.f);
                }
            }
        }
    }	
    // Clear jump input now, to allow movement events to trigger it for next update.
    CharacterOwner->ClearJumpInput(DeltaSeconds);
    NumJumpApexAttempts = 0;

    // change position
    StartNewPhysics(DeltaSeconds, 0);

    if (!HasValidData())
    {
        return;
    }

    // Update character state based on change from movement
    UpdateCharacterStateAfterMovement(DeltaSeconds);

    if (bAllowPhysicsRotationDuringAnimRootMotion || !HasAnimRootMotion())
    {
        PhysicsRotation(DeltaSeconds);
    }

    // Apply Root Motion rotation after movement is complete.
    if( HasAnimRootMotion() )
    {
        const FQuat OldActorRotationQuat = UpdatedComponent->GetComponentQuat();
        const FQuat RootMotionRotationQuat = RootMotionParams.GetRootMotionTransform().GetRotation();
        if( !RootMotionRotationQuat.IsIdentity() )
        {
            const FQuat NewActorRotationQuat = RootMotionRotationQuat * OldActorRotationQuat;
=>          MoveUpdatedComponent(FVector::ZeroVector, NewActorRotationQuat, true);
        }
        ...
        // Root Motion has been used, clear
        RootMotionParams.Clear();
    }
}

RootMotionParams中存储的Transfrom数据是位于Component Space坐标系下的,应用时需要转为Actor Space。

FTransform UCharacterMovementComponent::ConvertLocalRootMotionToWorld(const FTransform& LocalRootMotionTransform, float DeltaSeconds)
{
	const FTransform PreProcessedRootMotion = ProcessRootMotionPreConvertToWorld.IsBound() ? 										 	 ProcessRootMotionPreConvertToWorld.Execute(LocalRootMotionTransform, this, DeltaSeconds) : LocalRootMotionTransform;
=>	const FTransform WorldSpaceRootMotion = CharacterOwner->GetMesh()->ConvertLocalRootMotionToWorld(PreProcessedRootMotion);
	return ProcessRootMotionPostConvertToWorld.IsBound() ? ProcessRootMotionPostConvertToWorld.Execute(WorldSpaceRootMotion, this, 			DeltaSeconds) : WorldSpaceRootMotion;
}
FTransform USkeletalMeshComponent::ConvertLocalRootMotionToWorld(const FTransform& InTransform)
{
	// Make sure component to world is up to date
	ConditionalUpdateComponentToWorld();

	//Calculate new actor transform after applying root motion to this component
	const FTransform ActorToWorld = GetOwner()->GetTransform();
	// Component Transform => Actor Transform
	const FTransform ComponentToActor = ActorToWorld.GetRelativeTransform(GetComponentTransform());
	const FTransform NewComponentToWorld = InTransform * GetComponentTransform();
	const FTransform NewActorTransform = ComponentToActor * NewComponentToWorld;

	const FVector DeltaWorldTranslation = NewActorTransform.GetTranslation() - ActorToWorld.GetTranslation();

	const FQuat NewWorldRotation = GetComponentTransform().GetRotation() * InTransform.GetRotation();
	const FQuat DeltaWorldRotation = NewWorldRotation * GetComponentTransform().GetRotation().Inverse();
	
	const FTransform DeltaWorldTransform(DeltaWorldRotation, DeltaWorldTranslation);
	return DeltaWorldTransform;
}

Root Motion的坐标系数据已经处理完成,经过了三个阶段:Bone Space=>Component Space=>Actor Space。然后便是UCharacterMovementComponent中的应用。MoveUpdatedComponent为UMovementComponent组件提供的方法。

bool UMovementComponent::MoveUpdatedComponentImpl( const FVector& Delta, const FQuat& NewRotation, bool bSweep, FHitResult* OutHit, ETeleportType Teleport)
{
    if (UpdatedComponent)
    {
        const FVector NewDelta = ConstrainDirectionToPlane(Delta);
        // 更新SceneComponent的Rotation
        return UpdatedComponent->MoveComponent(NewDelta, NewRotation, bSweep, OutHit, MoveComponentFlags, Teleport);
    }

    return false;
}
/**
  <A SceneComponent has a transform and supports attachment>
 * The component we move and update.
 * If this is null at startup and bAutoRegisterUpdatedComponent is true, the owning Actor's root component will automatically be set as    	* our UpdatedComponent at startup.
 * @see bAutoRegisterUpdatedComponent, SetUpdatedComponent(), UpdatedPrimitive
 */
UPROPERTY(BlueprintReadOnly, Transient, DuplicateTransient, Category=MovementComponent)
TObjectPtr<USceneComponent> UpdatedComponent;

如上,使用USceneComponent类型的UpdatedComponent进行实际Transfrom的更新。

对于RootMotion的应用,主要是在CharacterComponent每次Tick过程中,对角色移动的控制(ControlledCharacterMove方法),此时需要用到另一个组件UCharacterMovementComponent的TickCharacterPose方法中,将Component Space中的Root Motion Transfrom转换到Actor Space中。然后应用到SceneComponent中。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2189619.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

污水排放口细粒度检测数据集,污-水排放口的类型包括10类目标,10000余张图像,yolo格式目标检测,9GB数据量。

污水排放口细粒度检测数据集&#xff0c;污-水排放口的类型包括10类目标&#xff08;1 合流下水道&#xff0c;2 雨水&#xff0c;3 工业废水&#xff0c;4 农业排水&#xff0c;5 牲畜养殖&#xff0c;6 水产养殖&#xff0c;7 地表径流&#xff0c;8 废水处理厂&…

yub‘s Algorithmic Adventures_Day5

Day5 反转链表 link&#xff1a;206. 反转链表 - 力扣&#xff08;LeetCode&#xff09; 思路分析 与数组不同&#xff0c;链表没必要定义新的链表进行存储【对内存空间的浪费】 直接改变next指针即可. 注意头节点指向的下一个节点为null 双指针法 class Solution {publi…

杂谈c语言——6.浮点数的存储

1.浮点数在内存中的存储 常⻅的浮点数&#xff1a;3.14159、1E10等&#xff0c;浮点数家族包括&#xff1a; float、double、long double 类型。 浮点数表⽰的范围&#xff1a; float.h 中定义 1.1 练习 #include<stdio.h>int main() {int n 9;float* pFloat (floa…

Golang | Leetcode Golang题解之第456题132模式

题目&#xff1a; 题解&#xff1a; func find132pattern(nums []int) bool {candidateI, candidateJ : []int{-nums[0]}, []int{-nums[0]}for _, v : range nums[1:] {idxI : sort.SearchInts(candidateI, 1-v)idxJ : sort.SearchInts(candidateJ, -v)if idxI < idxJ {ret…

智能视界·大模型驱动视频矩阵管理系统

开头先配两张ER图 一张不带字段&#xff0c;一张带字段&#xff0c;剩下的内容按需拿取 1.产品介绍 产品名称&#xff1a; 智能视界大模型驱动视频矩阵管理系统 主要功能&#xff1a; 智能视频分析与识别 功能介绍&#xff1a;该系统集成先进的人工智能大模型&#xff0c;能…

热轧钢带缺陷数据集,Xsteel表面缺陷数据集(X-SDD),其中包含七种典型的热轧带钢缺陷类型,共有1360个缺陷图像。

热轧钢带缺陷数据集&#xff0c;称为Xsteel表面缺陷数据集&#xff08;X-SDD&#xff09;&#xff0c;其中包含七种典型的热轧带钢缺陷类型&#xff0c;共有1360个缺陷图像。与常用的NEU表面缺陷数据库&#xff08;NEU-CLS&#xff09;的六种缺陷类型相比&#xff0c;X-SDD包含…

Ray_Tracing_The_Next_Week

1动态模糊 动态模糊在摄影中就是快门的速度慢&#xff0c;捕捉光的时间长&#xff0c;物体运动时进行捕捉成像&#xff0c;拍出来的结果是这个运动过程每一帧的平均值 我们的思路是&#xff1a; 每一条光线都拥有自己存在的一个时间点。随着时间变化随机生成光线,一般来说我…

全新芒果YOLOv10改进135:最新注意力机制EMA:即插即用,具有跨空间学习的高效多尺度注意力模块,ICCASSP 2023

💡本篇内容:芒果YOLOv10改进135:最新注意力机制EMA:即插即用,具有跨空间学习的高效多尺度注意力模块,ICCASSP 2023 **具有跨空间学习的高效多尺度注意力模块EMA | 即插即用 该模块通常包括多个并行的注意力子模块,每个子模块关注于输入数据的不同尺度或分辨率。这些子模块…

Study-Oracle-10-ORALCE19C-RAC集群维护

一路走来&#xff0c;所有遇到的人&#xff0c;帮助过我的、伤害过我的都是朋友&#xff0c;没有一个是敌人。 一、RAC的逻辑架构与进程 1、RAC 与单实例进程的对比 2、RAC相关进程功能 3、在主机查看RAC进程 其他的不列举了 4、RAC集群启停命令 检查集群状态 ORACLE 19C …

2-112基于matlab的协同干扰功率分配模型

基于matlab的协同干扰功率分配模型&#xff0c;带操作界面的功率分配GUI&#xff0c;可以实现对已有功率的分配优化&#xff0c;可以手动输入参数值。4个干扰山区分二批总干扰功率&#xff0c;每个扇区包括威胁总系数、综合压制概率、目标函数增量等。程序已调通&#xff0c;可…

无源有损耗导电介质的平面电磁波——复数介电常数带来复波数k(导致幅度衰减)和复波阻抗(带来磁场电场相位不同)

推导中以εμσ是实数为假设 注意在线性介质中J 0和σ等于0其实是一个条件&#xff0c;因为J σE 线性介质的麦克斯韦方程 线性介质无源无损耗条件下 线性介质无源有损耗导电介质下 无源有损耗的复数麦克斯韦方程组&#xff0c;只有方程二与无源无损耗的麦克斯韦方程组不同…

【Linux】使Ubuntu自适应窗口大小并与主机共享文件

LInux虚拟机版本ubuntu-20.04.6&#xff0c;VM版本VMware Workstation 17 Pro VMware Tools™ 是一组服务和模块&#xff0c;是VMware公司在其虚拟化平台中提供的一套工具集&#xff0c;旨在提高虚拟机的性能和稳定性。它们支持 VMware 产品中的多种功能特性&#xff0c;有助于…

TX-LCN框架 分布式事务

一、三种事务模式 1&#xff09;LCN 基于XA协议&#xff0c;事务提交或回滚的操作由事务管理服务器统一告诉它管理的多个项目&#xff0c;也就是说在A事务&#xff0c;B事务的事务提交操作或回滚操作都是在同一时刻发生&#xff0c;并且要么都提交&#xff0c;要么都回滚。 LCN…

.NET开源跨平台桌面和移动应用的统一框架 - Eto.Forms

前言 今天大姚给大家分享一个.NET开源、跨平台桌面和移动应用的统一框架&#xff1a;Eto.Forms。 项目介绍 Eto.Forms是一个.NET开源、跨平台的桌面和移动应用的统一框架&#xff0c;该框架允许开发者使用单一的UI代码库构建在多个平台上运行的应用程序&#xff0c;并利用各…

如何修改银河麒麟高级服务器操作系统修改网卡名称(如从ens33到eth0)

如何修改银河麒麟高级服务器操作系统修改网卡名称&#xff08;如从ens33到eth0&#xff09; &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 查找并修改配置文件&#xff1a; 使用ifconfig查看网卡名称&#xff08;如enp1s0&#xff09;。找…

小白快速上手 Docker 03 | Docker数据卷

数据卷 在前面使用Docker时&#xff0c;可能会遇到以下几个问题&#xff1a; 当Docker 里的容器挂了以后打不开&#xff0c;这时候只有删除该容器了&#xff0c;但删除容器会连容器中的产生的数据也一起删除了&#xff0c;大部分场景下这是不能接受的。Docker容器与容器之间不…

【D3.js in Action 3 精译_028】3.4 小节 DIY 实战:使用 Observable 在线绘制 D3 条形图

当前内容所在位置&#xff08;可进入专栏查看其他译好的章节内容&#xff09; 第一部分 D3.js 基础知识 第一章 D3.js 简介&#xff08;已完结&#xff09; 1.1 何为 D3.js&#xff1f;1.2 D3 生态系统——入门须知1.3 数据可视化最佳实践&#xff08;上&#xff09;1.3 数据可…

渗透测试入门学习——使用python脚本自动跟踪csrf_token实现对网站登录界面的暴力破解

目录 写在前面 使用方法 相关代码 写在前面 最近在学习使用Burp Suite时发现其intruder模块无法实现多种模式的混合使用&#xff0c;就如想要暴力破解账号和口令两个区域并同时跟踪网页的csrf_token时BP似乎不能很方便的实现这一功能&#xff0c;于是自己在练习时就想到了用…

【DataLoom】智能问数 - 自然语言与数据库交互

探索DataLoom的智能问数功能&#xff1a;简化数据库查询 在数据驱动的决策制定中&#xff0c;数据库查询是获取洞察的关键步骤。但是&#xff0c;传统的数据库查询方法往往复杂且技术性强&#xff0c;这限制了非技术用户的使用。DataLoom的智能问数功能正是为了解决这一问题而…

【WebGis开发 - Cesium】如何确保Cesium场景加载完毕

目录 引言一、监听场景加载进度1. 基础代码2. 加工代码 二、进一步封装代码1. 已知存在的弊端2. 封装hooks函数 三、使用hooks方法1. 先看下效果2. 如何使用该hooks方法 三、总结 引言 本篇为Cesium开发的一些小技巧。 判断Cesium场景是否加载完毕这件事是非常有意义的。 加载…