角色动画——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中。
相关文章:

角色动画——RootMotion全解
1. Unity(2022)的应用 由Animtor组件控制 在Animation Clip下可进行详细设置 官方文档的介绍(Animation选项卡 - Unity 手册) 上述动画类型在Rag选项卡中设置: Rig 选项卡上的设置定义了 Unity 如何将变形体映射到导入模型中的网格,以便能够将其动画化。 对于人…...

加密软件的桌面管理系统有什么?
1、IT资源管控:协助企事业单位管理者对内部计算机、宽带、打印、外围设备等IT资源进行管控,提高IT资源利用率。 2、规范内网行为:规范员工的计算机使用行为、网络使用行为、IT资产使用行为、设备使用行为 等,令员工活动在合规范围…...

【stm32】寄存器(stm32技术手册下载链接)
1、资料下载 RM0008_STM32F101xx,STM32F102xx,STM32F103xx,STM32F105xx和STM32F107xx单片机参考手册 | STMCU中文官网 2、代码 设置PB7 //设置PB7 #define SDA_IN() {GPIOB->CRL&0X0FFFFFFF;GPIOB->CRL|(u32)8<<28;} #define SDA_OUT() {GPIOB->…...

django的路由分发
前言: 在前面我们已经学习了基础的Django了,今天我们将继续学习,我们今天学习的是路由分发: 路由分发是Web框架中的一个核心概念,它指的是将不同的URL请求映射到对应的处理函数(视图)的过程。…...

《贪吃蛇小游戏 1.0》源码
好久不见! 终于搞好了简易版贪吃蛇小游戏(C语言版),邀请你来玩一下~ 目录 Snake.h Snake.c test.c Snake.h #include<stdio.h> #include<windows.h> #include<stdbool.h> #include<stdlib.h> #inclu…...

初入网络学习第一篇
引言 不磨磨唧唧,跟着学就好了,这个是我个人整理的学习内容梳理,学完百分百有收获。 1、使用的网络平台:eNSP 下载方法以及内容参考这篇文章 华为 eNSP 模拟器安装教程(内含下载地址)_ensp下载-CSDN博客https://b…...

(项目管理系列课程)项目规划阶段:项目范围管理-收集需求
在项目管理中,“规划过程组”是指一系列旨在定义和细化项目目标、规划如何达到这些目标并管理项目工作的过程。在这个过程中,“收集需求”是一个至关重要的活动,它涉及到识别和记录项目干系人的需求,以确保项目最终能够满足干系人…...

SQl注入文件上传及sqli-labs第七关less-7
Sql注入文件上传 1、sql知识基础 secure_file_priv 参数 secure_file_priv 为 NULL 时,表示限制mysqld不允许导入或导出。 secure_file_priv 为 /tmp 时,表示限制mysqld只能在/tmp目录中执行导入导出,其他目录不能导出导入。 secure_fil…...

想成为月薪过万的软件测试工程师?快看过来!
软件测试人员的工作主要是检测软件系统中的存在的BUG,但并不是毫无逻辑的盲目抓瞎。学会运用测试思维去完成测试工作,会使你的工作事半功倍。 01 软件测试的前提假设 测试人员进行软件测试的基本假设是“有罪推断”。即:认为被测程序一定是…...

找生网站方案———未来之窗行业应用跨平台架构
1)网站设计方面的考虑 主色调采用于公司深蓝色颜色,网页整体色彩明快、大气、简洁,每个细节均经过精心处 理,网页浏览快速,导航明确清晰。 网页设计要充分考虑网页的整体感觉,每个页面的图片与网站色调的过…...

全网都在找的Python生成器竟然在这里!简单几步,让你的代码更简洁、更高效!
博客主页:长风清留扬-CSDN博客系列专栏:Python基础专栏每天更新大数据相关方面的技术,分享自己的实战工作经验和学习总结,尽量帮助大家解决更多问题和学习更多新知识,欢迎评论区分享自己的看法感谢大家点赞ὄ…...

插入排序,希尔排序,和归并排序
每一本数据结构和算法的教科书中,都不厌其烦的介绍了排序算法。不厌其烦的介绍10余种不同的排序。那么实际编程中用得到那么多排序算法吗?当然用不到。那么为什么全世界的教科书都这么写呢?显然是醉翁之意不在酒。 数组,是每个编…...

Prompt 模版解析:诗人角色的创意引导与实践
Prompt 模版解析:诗人角色的创意引导与实践 Prompt 模版作为一种结构化工具,旨在为特定角色——本例中的“诗人”——提供明确的指导和框架。这一模版详尽地描绘了诗人的职责、擅长的诗歌形式以及创作规则,使其能在自动化系统中更加精确地执…...

zookeeper选举kafka集群的controller
zookeeper选举kafka集群的controller目录 文章目录 zookeeper选举kafka集群的controller目录前言一、实操体验controller的选举二、模拟controller选举四、删除controller节点 前言 kafka集群的controller是kafka集群中一个有特殊作用的broker,负责整个kafka集群的…...

吉如一线段树:区间最值和历史最值
区间最值和历史最值 问题一 给定一个长度为 n n n 的数组 a a a , 实现以下三种操作 : 0 l r x : 将 a r r [ l ∼ r ] arr[l\sim r] arr[l∼r] 范围的每个数 v v v , 更新为 min ( v , x ) \min (v, x) min(v,x) 1 l r : 查询 max i l r a r r i \max_{il}^r ar…...

数据库常见的安全特性有哪些
数据库的安全特性主要包括以下几个方面,以确保数据的机密性、完整性和可用性: 1. 身份验证(Authentication) 数据库系统会通过身份验证来确定用户的身份,常见的方式有用户名/密码验证、基于证书的验证、多因素验证&a…...

Debezium日常分享系列之:Debezium 3.0.0.Final发布
Debezium日常分享系列之:Debezium 3.0.0.Final发布 Debezium 核心的变化需要 Java 17基于Kafka 3.8 构建废弃的增量信号字段的删除每个表的详细指标 MariaDB连接器的更改版本 11.4.3 支持 MongoDB连接器的更改MongoDB sink connector MySQL连接器的改变MySQL 9MySQL…...

MVCC(多版本并发控制)
目录 1.MVCC的工作原理2.MVCC的优点3.例子 MVCC(多版本并发控制)是一种用于数据库管理系统中实现并发控制的技术。它允许多个事务同时对数据库进行读写操作,而不会相互干扰,从而提高数据库系统的性能和可用性。MVCC通过为每个事务…...

低代码可视化-uniapp响应式数据data-代码生成器
在uniapp框架中,data 是一个核心的概念,它代表了组件或uniapp实例中的响应式数据。这些数据是组件状态的基础,uniapp会根据这些数据的变化来更新DOM,从而保持视图与数据的同步。 data 的特点 响应式:uniapp使用一种称…...

10.7学习
1.安全认证 ●Session 认证中最常用的一种方式,也是最简单的。存在多节点session丢失的情况,可通过nginx粘性Cookie和Redis集中式Session存储解决 ●HTTP Basic Authentication 服务端针对请求头中base64加密的Authorization 和用户名和密码进行校验。…...

基础算法之前缀和--Java实现(下)--LeetCode题解:-和为 K 的子数组 - 和可被 K 整除的子数组 -连续数组-矩阵区域和
这里是Themberfue 和为 K 的子数组 题目解析 返回子数组中所有元素的和等于给定k的个数。 算法讲解 这题好像是用滑动窗口解决,但其实不能,因为 nums 中的元素可能存在负数,就不能保证其单调性的性质。 用前缀和求也不易想到,…...

序列化与反序列化基础及反序列化漏洞(附案例)
参考文章: [web安全原理]PHP反序列化漏洞 - 笑花大王 - 博客园 (cnblogs.com) 一、概念 为了能有效的存储数据而不丢失数据的类型和内容,经常需要通过序列化对数据进行处理,将数据进行序列化后,会生成一个字符串,字符…...

Khronos:动态环境下时空度量语义SLAM的统一方法
Khronos: A Unified Approach for Spatio-Temporal Metric-Semantic SLAM in Dynamic Environments 原文 项目 引言: 人类居住环境通常是高度动态的,人、机器人和其他实体不断移动、互动和改变场景。对于机器人在这种情况下的操作,仅仅建立一…...

一个迷茫的25岁前端程序员的自述
作者:一尾流莺 一直听说程序员的危机在 35 岁,没想到我的危机从 25 岁就开始了。 我甚至不知道自己是不是 25 岁,也可能是 26 岁,或者 27 岁,1998 年的生日,按照 2023 - 1998 的算法就是 25,按…...

多文件并发多线程MD5工具(相对快速的MD5一批文件),适配自定义MD5 Hash I/O缓存。
自己写的多文件 MD5校验工具,一个文件开一个线程,有最大I/O 缓存设置,兼容读写MD5后缀文件。 共计91个文件,合计180G左右 12分钟左右,UI基本卡废,但程序没蹦,属于正常。 卡的原因是基本是用 I/O…...

Pikachu-url重定向-不安全的url跳转
不安全的url跳转 不安全的url跳转问题可能发生在一切执行了url地址跳转的地方。如果后端采用了前端传进来的(可能是用户传参,或者之前预埋在前端页面的url地址)参数作为了跳转的目的地,而又没有做判断的话就可能发生"跳错对象"的问题。 url跳转比较直接的危害是: …...

如何下载和安装CLion,图文详解
一、下载 登录JetBrains官网,下载最新版本的Clion,Clion目前没有社区版,都是专业版。 二、安装 1、启动Clion安装程序,下一步。 2、修改安装目录,下一步。 3、创建桌面快捷方式,更新PATH变量࿰…...

vue3导入本地图片2种实现方法
在<script setup>中使用import语法: <template><img :src"logo" alt"Logo"> </template><script setup> import logo from ./assets/logo.png; </script> 使用Vue的ref来动态地在<script setup>中…...

leetcode 刷题day36动态规划Part05 背包问题(完全背包、518. 零钱兑换 II、377. 组合总和 Ⅳ、70. 爬楼梯 (进阶))
完全背包 完全背包的每件商品都有无限个,和01背包的一不同主要体现在遍历顺序上。为了保证每个物品仅被添加一次,01背包内嵌的循环是从大到小遍历。而完全背包的物品是可以添加多次的,所以要从小到大去遍历。 518. 零钱兑换 II 思路&#…...

检查jar冲突,查找存在相同class的jar
写在前面 本文看下如何查找jar冲突,即查找哪些jar包中存在相同的class。如果是存在相同jar的不同版本,基本一眼就能看出来,然后结合maven的依赖关系将其剔除掉即可,但是当你遇到了有人手动拷贝某些class到jar包中导致冲突的情况时…...