角色动画——RootMotion全解

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 groupsUpdateMontageSyncGroup();// Update montage eval data, to be used by AnimGraph Update and Evaluate phases.UpdateMontageEvaluationData();
//....
}
void UAnimInstance::UpdateMontage(float DeltaSeconds)
{//...// update montage weightMontage_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 NULLensure(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 RootMotionFromEverythingbool 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 updatedconst float Weight = Blend.GetBlendedValue();AnimInstance.Get()->QueueRootMotionBlend(RootMotion, Montage-							>SlotAnimTracks[0].SlotName, Weight);}else{// Component Space中的RootMotion Transform数据AccumulateOutRootMotionParams->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_WARNINGSFAnimSegment(const FAnimSegment&) = default;FAnimSegment(FAnimSegment&&) = default;FAnimSegment& operator=(const FAnimSegment&) = default;FAnimSegment& operator=(FAnimSegment&&) = default;PRAGMA_ENABLE_DEPRECATION_WARNINGSUE_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_DATAUPROPERTY()float CachedPlayLength = 0.f;#endif#if WITH_EDITORfriend 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 SpaceRootTransformRefPose = 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 GetRelativeTransformStartTransform.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_WARNINGSbUseRawDataOnlyPRAGMA_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 = OtherTTransform<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 quatGetRelativeTransformUsingMatrixWithScale(&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 RotatedTranslationTransformVectorRegister 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_TRANSFORMTMatrix<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 poseif (CharacterOwner->IsPlayingRootMotion() || bWasPlayingRootMotion){// 获取Animation Instance的RootMotion数据=>   FRootMotionMovementParams RootMotion = CharacterMesh->ConsumeRootMotion();if (RootMotion.bHasRootMotion){// bHasRootMotion此时为trueRootMotion.ScaleRootMotionTranslation(CharacterOwner->GetAnimRootMotionTranslationScale());RootMotionParams.Accumulate(RootMotion);}}}
}
FRootMotionMovementParams USkeletalMeshComponent::ConsumeRootMotion()
{float InterpAlpha;if(bExternalTickRateControlled)InterpAlpha = ExternalInterpolationAlpha;elseInterpAlpha = 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此时为trueRootMotion.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 usif (!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 Velocityif( CurrentRootMotion.HasOverrideVelocity() || HasAnimRootMotion() ){// Animation root motion overrides Velocity and currently doesn't allow any other root motion sourcesif( 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 positionStartNewPhysics(DeltaSeconds, 0);if (!HasValidData()){return;}// Update character state based on change from movementUpdateCharacterStateAfterMovement(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, clearRootMotionParams.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 dateConditionalUpdateComponentToWorld();//Calculate new actor transform after applying root motion to this componentconst FTransform ActorToWorld = GetOwner()->GetTransform();// Component Transform => Actor Transformconst 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的Rotationreturn 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.rhkb.cn/news/439748.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux驱动开发——LED驱动开发

文章目录 1 概述1.1 说明 2 基础知识2.1 地址映射2.1.1 ioremap函数2.1.2 iounmap函数 2.2 I/O内存访问函数2.2.1 读操作函数2.2.2 写操作函数 3 硬件原理图分析4 RK3568 GPIO驱动原理4.1 引脚复用设置4.2 引脚驱动能力配置4.3 GPIO输入输出设置4.4 GPIO引脚高低电平设置 5 实验…

【GeekBand】C++设计模式笔记5_Observer_观察者模式

1. “组件协作”模式 现代软件专业分工之后的第一个结果是“框架与应用程序的划分”&#xff0c;“组件协作”模式通过晚期绑定&#xff0c;来实现框架与应用程序之间的松耦合&#xff0c;是二者之间协作时常用的模式。典型模式 Template MethodStrategyObserver / Event 2.…

HarmonyOS/OpenHarmony 自定义弹窗页面级层级控制解决方案

关键词&#xff1a;CuntomDialog自定义弹窗、SubWindow子窗口、页面级、弹窗层级控制、鸿蒙、弹窗展示层级异常 问题存在API版本&#xff1a;API10 - API12&#xff08;该问题已反馈&#xff0c;期望后续官方能增加页面级控制能力&#xff09; 在正常的鸿蒙app开发过程中&…

aws(学习笔记第二课) AWS SDK(node js)

aws(学习笔记第二课) 使用AWS SDK&#xff08;node js&#xff09; 学习内容&#xff1a; 使用AWS SDK&#xff08;node js&#xff09; 1. AWS SDK&#xff08;node js&#xff09; AWS支持多种SDK开发(除了AWS CLI&#xff0c;还支持其他的SDK) AndroidPythonNode.js(Javas…

约数个数约数之和

好久没发文章了.......不过粉丝还是一个没少...... 今天来看两道超级恶心的数论题目&#xff01; No.1 约数个数 No.2 约数之和 先来看第一道&#xff1a;约数个数 题目描述 给定 n 个正整数 ai​,请你输出这些数的乘积的约数个数,答案对 10^97 取模 输入格式 第一行包含…

五种IO模型与阻塞IO

一、前言 在网络中通信的本质其实是网络中的两台主机的进程间进行通信&#xff0c;而进程通信的本质就是IO。 IO分为输入&#xff08;input&#xff09;和输出&#xff08;output&#xff09;站在进程的角度讲&#xff0c;进程出去数据为输出&#xff0c;外部数据进入进程为输…

YOLOv8 基于NCNN的安卓部署

YOLOv8 NCNN安卓部署 前两节我们依次介绍了基于YOLOv8的剪枝和蒸馏 本节将上一节得到的蒸馏模型导出NCNN&#xff0c;并部署到安卓。 NCNN 导出 YOLOv8项目中提供了NCNN导出的接口&#xff0c;但是这个模型放到ncnn-android-yolov8项目中你会发现更换模型后app会闪退。原因…

[ComfyUI]Flux:太强了!任意扩图神器,小红书极致逼真风格出游打卡写实风

随着人工智能技术的不断发展&#xff0c;图像生成与反推技术已经成为了AI领域的一大热点。今天&#xff0c;我们就来为大家详细介绍一款由ComfyUI团队开发的超强图像反推工具——Flux&#xff0c;以及如何使用它实现任意扩图和极致逼真风格出游打卡写实风。 一、Flux&#xff…

【AI大模型】使用Embedding API

一、使用OpenAI API 目前GPT embedding mode有三种&#xff0c;性能如下所示&#xff1a; 模型每美元页数MTEB得分MIRACL得分text-embedding-3-large9,61554.964.6text-embedding-3-small62,50062.344.0text-embedding-ada-00212,50061.031.4 MTEB得分为embedding model分类…

centos7安装配置nginx

先安装依赖 安装依赖之前最好先执行下update yum update yum install gcc gcc-c pcre pcre-devel zlib zlib-devel openssl openssl-devel -y cd /usr/local/nginx wget http://nginx.org/download/nginx-1.18.0.tar.gz tar -zxvf nginx-1.18.0.tar.gz cd /usr/local/ngi…

双非本 985 硕,上岸快手大模型算法岗!

最近已有不少大厂都在秋招宣讲&#xff0c;也有一些已在 Offer 发放阶段了。 节前&#xff0c;我们邀请了一些互联网大厂朋友、今年参加社招和校招面试的同学。 针对新手如何入门算法岗、该如何准备面试攻略、面试常考点、大模型技术趋势、算法项目落地经验分享等热门话题进行…

高校校园交友系统小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;管理员管理&#xff0c;用户管理&#xff0c;基础数据管理&#xff0c;论坛管理&#xff0c;公告信息管理&#xff0c;轮播图信息管理 微信端账号功能包括&#xff1a;系统首页&#xff0c;用户&#…

反调试—1

IsDebuggerPresent() CheckRemoteDebuggerPresent() 其内部实际调用NtQueryInformationProcess() bool _stdcall ThreadCall() {while (true){BOOL pbDebuggerPresent FALSE;CheckRemoteDebuggerPresent(GetCurrentProcess(), &pbDebuggerPresent);if (pbDebuggerPres…

fiddler抓包18-2_导出jmeter、postman脚本(带请求头)

课程大纲 1. Fiddler导出请求为curl脚本 选中请求&#xff0c;“文件” - “导出会话” - “选中的会话” - “cURL Script”。 2. 导入jmeter ① 复制curl脚本。 ② 打开jmeter&#xff0c;“工具” - “import from cURL”&#xff0c;粘贴脚本&#xff0c;勾选“Add cooki…

二分查找一>寻找峰值

1.题目&#xff1a; 2.解析&#xff1a; 暴力遍历代码&#xff1a;O(N),由于该题数据很少所以可以通过 暴力遍历&#xff1a;O(N),由于该题数据很少所以可以通过int index 0;for(int i 1; i < nums.length-1; i) {//某段区域内一直递增&#xff0c;更新就indexif(nums[i]…

红黑树学习

红黑树: k v 方式 用在哪里&#xff1a; 1.hash 强查找的过程&#xff1a; 1.rbtree 2.hash 3.b/b tree 4.链表 红黑树&#xff1a; 1.每个结点是红的或者是黑的 2.根结点是黑的 3.每个叶子结点是黑的 4.如果一个结点是红的&#xff0c;则它的两个儿子是黑的 5.对每个节点&…

性能学习5:性能测试的流程

一.需求分析 二.性能测试计划 1&#xff09;测什么&#xff1f; - 项目背景 - 测试目的 - 测试范围 - ... 2&#xff09;谁来测试 - 时间进度与分工 - 交付清单 - ... 3&#xff09;怎么测 - 测试策略 - ... 三.性能测试用例 四.性能测试执行 五.性能分析和调优 六…

ElasticSearch备考 -- Search across cluster

一、题目 配置两个集群&#xff0c;集群名称为my-application-01、my-application-02&#xff0c;导入es自带Sample flight data数据集&#xff0c;配置扩集群检索&#xff0c;查询数据 二、思考 准备工作有两个集群&#xff0c;并需要对集群配置角色中增加 remote_cluster_cl…

【优选算法】(第八篇)

目录 串联所有单词的⼦串&#xff08;hard&#xff09; 题目解析 讲解算法原理 编写代码 最⼩覆盖⼦串&#xff08;hard&#xff09; 题目解析 讲解算法原理 编写代码 串联所有单词的⼦串&#xff08;hard&#xff09; 题目解析 1.题目链接&#xff1a;. - 力扣&#…

光伏组件模型模板在SketchUp中如何完成成模数化设计?

选中模板组件&#xff0c;点击左侧工具栏中移动工具&#xff0c;按住Ctrl再依次点击组件起始点和终点&#xff0c;完成组件复制&#xff0c;输入需要复制的组件数量&#xff08;*n&#xff09;后回车&#xff0c;即可完成模数化设计。 选中模组的多块模型右键进行创建组件或群…