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中。