本文简析UE5动画新功能Motion Warping的原理实现,实测不同Warp类型的动画效果,重点分析Simple Warp和Adjustment Blend Warp两种缩放实现及表现效果。如果只关注UE4适配细节,可以直接看最后一节。
Motion Warping原理
伴随UE5 EA版的发布,引擎也新增了一项动画新功能:Motion Warping(运动扭曲)。Motion Warping概念最早可以追溯到2017 GDC演讲 地平线:黎明时分所提出的Animation Warping,原理是对RootMotion动画的某一段区间进行缩放变形,让根骨骼运动到Gameplay自定义的位置,关于RootMotion动画实现原理可以参考Unreal的骨骼动画系统的RootMotion原理剖析。
在没有Motion Warping插件之前,Gameplay程序员要实现类似功能,需要禁用RootMotion动画的EnableRootMotion选项,使其变成不带位移的动画,播放动画的同时,自行做Update逻辑每帧更新Actor位置,直到Gameplay指定位置。而UE5的Motion Warping插件,巧妙地利用动画事件对RootMotion动画资源定义缩放区间,缩放区间内的根骨骼运动。只要简单配置Motion Warping动画事件,正常播放RootMotion动画,就能达到同样的效果,从而简化Gameplay开发逻辑。
Motion Warping实现
UE5的Motion Warping功能以插件形式内嵌引擎,其插件源码类图关系关于如上所示。
UMotionWarpingComponent作为核心组件,其职责:
- 负责监听对应Actor的运动组件UCharacterMovementComponent的RootMotion动画更新进度
void UMotionWarpingComponent::InitializeComponent()
{
Super::InitializeComponent();
CharacterOwner = Cast<ACharacter>(GetOwner());
UCharacterMovementComponent* CharacterMovementComp = CharacterOwner.IsValid() ? CharacterOwner->GetCharacterMovement() : nullptr;
if (CharacterMovementComp)
{
CharacterMovementComp->ProcessRootMotionPreConvertToWorld.BindUObject(this, &UMotionWarpingComponent::ProcessRootMotionPreConvertToWorld);
CharacterMovementComp->ProcessRootMotionPostConvertToWorld.BindUObject(this, &UMotionWarpingComponent::ProcessRootMotionPostConvertToWorld);
}
}
ProcessRootMotionPreConvertToWorld、ProcessRootMotionPostConvertToWorld这两个事件通过UCharacterMovementComponent::ConvertLocalRootMotionToWorld函数派发。而ConvertLocalRootMotionToWorld调用时机发生在:
- 在UCharacterMovementComponent::TickCharacterPose之后,此时已计算得到当前帧的RootMotion与上一帧的差值RootMotionDelta
- 在UMovementComponent::MoveUpdatedComponent之前,RootMotionDelta还未被转化成Velocity和Rotation用于计算当前帧移动
Motion Warping在此时机修改RootMotionDelta,进而缩放当前帧移动。ConvertLocalRootMotionToWorld函数由以下函数调用:
- 负责ROLE_AutonomousProxy(1P客户端)/ROLE_Authority(服务端)移动的PerformMovement函数
- 负责ROLE_SimulatedProxy(3P客户端)移动的SimulateRootMotion函数
因此Motion Warping适用于联网播放RootMotion的场景。
- 当前RootMotion动画进度存在配置的Motion Warping动画事件时,实例化动画事件所配置的具体URootMotionModifier类。
void UMotionWarpingComponent::Update()
{
const FAnimMontageInstance* RootMotionMontageInstance = GetCharacterOwner()->GetRootMotionAnimMontageInstance();
UAnimMontage* Montage = RootMotionMontageInstance ? RootMotionMontageInstance->Montage : nullptr;
if (Montage)
{
const float PreviousPosition = RootMotionMontageInstance->GetPreviousPosition();
const float CurrentPosition = RootMotionMontageInstance->GetPosition();
// Loop over notifies directly in the montage, looking for Motion Warping windows
for (const FAnimNotifyEvent& NotifyEvent : Montage->Notifies)
{
const UAnimNotifyState_MotionWarping* MotionWarpingNotify = NotifyEvent.NotifyStateClass ? Cast<UAnimNotifyState_MotionWarping>(NotifyEvent.NotifyStateClass) : nullptr;
if (MotionWarpingNotify)
{
const float StartTime = FMath::Clamp(NotifyEvent.GetTriggerTime(), 0.f, Montage->GetPlayLength());
const float EndTime = FMath::Clamp(NotifyEvent.GetEndTriggerTime(), 0.f, Montage->GetPlayLength());
if (PreviousPosition >= StartTime && PreviousPosition < EndTime)
{
if (!ContainsModifier(Montage, StartTime, EndTime))
{
MotionWarpingNotify->OnBecomeRelevant(this, Montage, StartTime, EndTime);
}
}
}
}
}
}
void UAnimNotifyState_MotionWarping::OnBecomeRelevant(UMotionWarpingComponent* MotionWarpingComp, const UAnimSequenceBase* Animation, float StartTime, float EndTime) const
{
URootMotionModifier* RootMotionModifierNew = AddRootMotionModifier(MotionWarpingComp, Animation, StartTime, EndTime);
}
- 提供添加/移除RootMotion定位点的接口,让Gameplay逻辑可以指定RootMotion动画某一帧的位置定位点,比如动态指定攀爬动画翻越帧的位置点,从而适配不同高度障碍物的攀爬表现
void UMotionWarpingComponent::AddOrUpdateSyncPoint(FName Name, const FMotionWarpingSyncPoint& SyncPoint)
{
if (Name != NAME_None)
{
FMotionWarpingSyncPoint& MotionWarpingSyncPoint = SyncPoints.FindOrAdd(Name);
MotionWarpingSyncPoint = SyncPoint;
}
}
int32 UMotionWarpingComponent::RemoveSyncPoint(FName Name)
{
return SyncPoints.Remove(Name);
}
- Motion Warping动画事件窗口期,每帧调用指定的URootMotionModifier类的ProcessRootMotion方法,对每帧的RootMotionDelta进行缩放,进而缩放当前帧的移动。而Simple Warp/Adjustment Blend Warp/Skew Warp/Scale等几种URootMotionModifier子类,负责真正的缩放计算逻辑,是Motion Warping的核心实现。
RootMotionModifier缩放算法实现
Motion Warping共有Simple Warp/Adjustment Blend Warp/Scale Warp/Skew Warp共4种不同缩放模式,其中Simple Warp,Adjustment Blend Warp缩放算法较有实用价值,做详细分析,Scale Warp/Skew Warp只做粗略介绍。
Simple Warp缩放
- 动画效果
- 实现分析
顾名思义,最简单的一种缩放实现,算法实现是每帧按DeltaTime/TotalTime计算当前帧缩放比例,分别缩放在世界空间下的RootMotion的位移及旋转。
RootMotionModifier类初始实例化时,会记录此次Warping的相关参数,其中最重要的就是Animation,StartTime,EndTime,RootMotionModifier类就知道要在具体哪个动画的某段时间(下文简称Motion Warping事件窗口)内,对RootMotion进行缩放。
在事件窗口期间,RootMotionModifier类每帧会先后调用Update、ProcessRootMotion函数。
Update函数更新动画当前帧进度(Position),世界空间下的定位点位置(FTransform)。这里可以注意到,通过UMotionWarpingComponent::AddOrUpdateSyncPoint添加的定位点位置,还可以再根据动画事件的WarpPointAnimProvider字段配置,再叠加一个静态的FTransform偏移值,或者叠加某根指定骨骼相对于root骨骼的偏移值,即:
最终定位点 = 调用AddOrUpdateSyncPoint传入定位点 + 动画事件配置的定位点偏移值(无偏移/固定值偏移/骨骼偏移3种类型可选)
而ProcessRootMotion函数传入FTransform类型的当前帧根骨骼移动差值RootMotionDelta,返回经过缩放后的RootMotionDelta。
FTransform URootMotionModifier_Warp::ProcessRootMotion(const FTransform& InRootMotion, float DeltaSeconds)
{
FTransform FinalRootMotion = InRootMotion;
// FinalRootMotion缩放计算
// ...
return FinalRootMotion;
}
FTransform缩放拆分为Translation(位移)、Rotation(旋转)分别进行缩放。其中位移缩放又细分为水平缩放(XY轴)和垂直缩放(Z轴):
- 位移缩放关键代码
const FTransform& CharacterTransform = CharacterOwner->GetActorTransform();
FTransform FinalRootMotion = InRootMotion;
// 核心是调用UAnimMontage::ExtractRootMotionFromTrackRange(float StartTrackPosition, float EndTrackPosition)函数,获取动画指定范围内的RootMotionDelta
// 计算上一帧到Motion Warping窗口期结束的RootMotionDelta(组件空间)
const FTransform RootMotionTotal = UMotionWarpingUtilities::ExtractRootMotionFromAnimation(Animation.Get(), PreviousPosition, EndTime);
// 是否缩放位移,可通过动画事件字段配置
if (bWarpTranslation)
{
// 当前帧RootMotionDelta(世界空间)
FVector DeltaTranslation = InRootMotion.GetTranslation();
// 当前帧RootMotionDelta(组件空间)
const FTransform RootMotionDelta = UMotionWarpingUtilities::ExtractRootMotionFromAnimation(Animation.Get(), PreviousPosition, FMath::Min(CurrentPosition, EndTime));
// 当前帧RootMotionDelta(组件空间)的水平移动距离
const float HorizontalDelta = RootMotionDelta.GetTranslation().Size2D();
// 当前Actor位置与Target位置的水平距离,即Gameplay实际需要的水平移动距离
const float HorizontalTarget = FVector::Dist2D(CharacterTransform.GetLocation(), GetTargetLocation());
// 上一帧到Motion Warping窗口期结束的RootMotionDelta(组件空间)的水平移动距离, 即RootMotion动画原本的水平移动距离
const float HorizontalOriginal = RootMotionTotal.GetTranslation().Size2D();
// 计算出当前帧实际需要的水平移动距离
const float HorizontalTranslationWarped = HorizontalOriginal != 0.f ? ((HorizontalDelta * HorizontalTarget) / HorizontalOriginal) : 0.f;
// 得出当前帧实际需要的水平位移 = Actor位置与Target位置的归一化水平向量 * 水平移动距离
DeltaTranslation = (GetTargetLocation() - CharacterTransform.GetLocation()).GetSafeNormal2D() * HorizontalTranslationWarped;
// 是否忽略垂直位移缩放,可通过动画事件字段配置
if (!bIgnoreZAxis)
{
// 与缩放水平位移同理
const FVector CapsuleBottomLocation = (CharacterOwner->GetActorLocation() - FVector::UpVector * CharacterOwner->GetSimpleCollisionHalfHeight());
const float VerticalDelta = RootMotionDelta.GetTranslation().Z;
const float VerticalTarget = GetTargetLocation().Z - CapsuleBottomLocation.Z;
const float VerticalOriginal = RootMotionTotal.GetTranslation().Z;
const float VerticalTranslationWarped = VerticalOriginal != 0.f ? ((VerticalDelta * VerticalTarget) / VerticalOriginal) : 0.f;
DeltaTranslation.Z = VerticalTranslationWarped;
}
// 重新设置缩放后的位移
FinalRootMotion.SetTranslation(DeltaTranslation);
}
- 旋转缩放关键代码
// 是否缩放旋转,可通过动画事件字段配置
if (bWarpRotation)
{
const FQuat WarpedRotation = WarpRotation(InRootMotion, RootMotionTotal, DeltaSeconds);
// 重新设置缩放后的旋转
FinalRootMotion.SetRotation(WarpedRotation);
}
FQuat URootMotionModifier_Warp::WarpRotation(const FTransform& RootMotionDelta, const FTransform& RootMotionTotal, float DeltaSeconds)
{
const ACharacter* CharacterOwner = GetCharacterOwner();
if (CharacterOwner == nullptr)
{
return FQuat::Identity;
}
// 当前帧Actor Transform(世界空间)
const FTransform& CharacterTransform = CharacterOwner->GetActorTransform();
// 当前帧Actor旋转(世界空间)
const FQuat CurrentRotation = CharacterTransform.GetRotation();
// Target点旋转(世界空间)
const FQuat TargetRotation = GetTargetRotation();
// 旋转缩放剩余时间,通过配置字段WarpRotationTimeMultiplier可以进一步控制旋转缩放速度,默认值为1.0f,值越小,越快旋转缩放至目标角度,值越大反之
const float TimeRemaining = (EndTime - PreviousPosition) * WarpRotationTimeMultiplier;
// 上一帧到窗口期结束的旋转差量,即窗口期RootMotion动画剩余旋转值
const FQuat RemainingRootRotationInWorld = RootMotionTotal.GetRotation();
// 当前帧Actor旋转左乘累加剩余旋转值,拆解此时旋转值构成 = Actor播RootMotion动画前旋转值 + RootMotion动画总旋转值 + 根据Target点的已缩放旋转值
const FQuat CurrentPlusRemainingRootMotion = RemainingRootRotationInWorld * CurrentRotation;
// 计算RootMotion动画旋转与Target点旋转的插值进度
const float PercentThisStep = FMath::Clamp(DeltaSeconds / TimeRemaining, 0.f, 1.f);
// 对两个旋转四元数做球面插值,得到当前帧理想旋转值
const FQuat TargetRotThisFrame = FQuat::Slerp(CurrentPlusRemainingRootMotion, TargetRotation, PercentThisStep);
// 根据公式: QuatDelta = Quat(To) * Quat(From).Inverse(),得到缩放旋转值
const FQuat DeltaOut = TargetRotThisFrame * CurrentPlusRemainingRootMotion.Inverse();
// 当前帧RootMotion旋转左乘累加缩放旋转值,得到当前帧实际的RootMotion旋转
return (DeltaOut * RootMotionDelta.GetRotation());
}
Adjustment Blend Warp缩放
- 动画效果
实际对比下面带RootMotion的攻击动画。第一张动图是仅缩放root骨骼的效果,左右腿与root骨骼的相对关系是没有变化的,当位移缩放变大后,可以看到右腿有几帧滑步。第二张动图是同时缩放root骨骼和腿部ik骨骼的效果,已经没有滑步表现,同时在不同的位移缩放下,腿部伸展度是有细微差别的,整体动作表现更真实。
- 实现分析
Adjustment Blend Warp缩放算法采用与Simple Warp完全不同的缩放思路。该算法是在第一次缩放时,就计算好窗口期间,root骨骼在组件空间下的缩放差值,也就是root骨骼的Mesh空间缩放叠加动画,每帧对RootMotion源动画并应用缩放叠加动画,从而实现缩放。采用这种算法,不仅可以针对root骨骼叠加动画数据进行缩放,理论还能针对骨架上的任意骨骼进行同样的缩放,因此Adjustment Blend Warp缩放支持配置,针对ik骨骼进行缩放。比如对带脚步移动的RootMotion动画应用Adjustment Blend Warp缩放,不仅能够灵活缩放位移,配合TwoBone IK,还能规避缩放位移导致的腿部滑步表现。
下面简析代码:
- 首先,在窗口期第一次ProcessRootMotion时,会调用PrecomputeWarpedTracks,计算所有指定骨骼的叠加骨骼数据,并按60帧/1秒采样率抽取RootMotion源动画对应骨骼数据,对其应用叠加数据,即生成了一份指定骨骼,在组件空间下,每一帧经缩放后的骨骼动画数据。
void URootMotionModifier_AdjustmentBlendWarp::PrecomputeWarpedTracks()
{
// First, extract pose at the end of the window for the bones we are going to warp
const ACharacter* CharacterOwner = GetCharacterOwner();
if (CharacterOwner == nullptr)
{
return;
}
const FBoneContainer& BoneContainer = CharacterOwner->GetMesh()->GetAnimInstance()->GetRequiredBones();
// Init FBoneContainer with only the bones that we are interested in
TArray<FBoneIndexType> RequiredBoneIndexArray;
// 添加root骨骼索引
RequiredBoneIndexArray.Add(0);
const bool bShouldWarpIKBones = bWarpIKBones && IKBones.Num() > 0;
if (bShouldWarpIKBones)
{
for (const FName& BoneName : IKBones)
{
const int32 BoneIndex = BoneContainer.GetPoseBoneIndexForBoneName(BoneName);
if (BoneIndex != INDEX_NONE)
{
// 添加指定ik骨骼索引
RequiredBoneIndexArray.Add(BoneIndex);
}
}
BoneContainer.GetReferenceSkeleton().EnsureParentsExistAndSort(RequiredBoneIndexArray);
}
// Init BoneContainer
FBoneContainer RequiredBones(RequiredBoneIndexArray, FCurveEvaluationOption(false), *BoneContainer.GetAsset());
// Extract pose
FCSPose<FCompactPose> CSPose;
// 计算指定骨骼在窗口期结束帧的动画数据
UMotionWarpingUtilities::ExtractComponentSpacePose(Animation.Get(), RequiredBones, EndTime, true, CSPose);
// Second, calculate additive pose
//Calculate additive translation for root bone
FVector RootTargetLocation = CachedMeshTransform.InverseTransformPositionNoScale(GetTargetLocation());
// 计算root骨骼叠加位移
FVector RootTotalAdditiveTranslation = FVector::ZeroVector;
if (bWarpTranslation)
{
RootTotalAdditiveTranslation = RootTargetLocation - CachedRootMotion.GetLocation();
if (bIgnoreZAxis)
{
RootTotalAdditiveTranslation.Z = 0.f;
}
}
// Calculate additive rotation for root bone
FQuat RootTotalAdditiveRotation = FQuat::Identity;
if (bWarpRotation)
{
// Target点旋转(世界空间)
const FQuat TargetRotation = GetTargetRotation();
// 将RootMotion动画旋转值转换到世界空间下
const FQuat OriginalRotation = CachedMeshRelativeTransform.GetRotation().Inverse() * (CachedRootMotion * CachedMeshTransform).GetRotation();
// 计算root骨骼叠加旋转
RootTotalAdditiveRotation = FQuat::FindBetweenNormals(OriginalRotation.GetForwardVector(), TargetRotation.GetForwardVector());
}
// Init Additive Pose
FCSPose<FCompactPose> AdditivePose;
AdditivePose.InitPose(&RequiredBones);
// root骨骼保存叠加动画数据
AdditivePose.SetComponentSpaceTransform(FCompactPoseBoneIndex(0), FTransform(RootTotalAdditiveRotation, RootTotalAdditiveTranslation));
// Calculate and add additive pose for IK bones
if (bShouldWarpIKBones)
{
// RootMotion动画缩放后的root骨骼Transform(组件空间)
const FTransform RootTargetPoseCS = FTransform((RootTotalAdditiveRotation * CachedRootMotion.GetRotation()), CachedRootMotion.GetTranslation() + RootTotalAdditiveTranslation);
for (int32 Idx = 1; Idx < CSPose.GetPose().GetNumBones(); Idx++)
{
const FName BoneName = RequiredBones.GetReferenceSkeleton().GetBoneName(RequiredBones.GetBoneIndicesArray()[Idx]);
if (IKBones.Contains(BoneName))
{
const int32 BoneIdx = Idx;
const FTransform BonePoseCS = CSPose.GetComponentSpaceTransform(FCompactPoseBoneIndex(BoneIdx));
// 指定ik骨骼缩放后的骨骼Transform(组件空间)
const FTransform BoneTargetPoseCS = BonePoseCS * RootTargetPoseCS;
const FTransform BoneOriginalPoseCS = BonePoseCS * CachedRootMotion;
const FVector TotalAdditiveTranslation = BoneTargetPoseCS.GetLocation() - BoneOriginalPoseCS.GetLocation();
// ik骨骼仅保存位移叠加数据
AdditivePose.SetComponentSpaceTransform(FCompactPoseBoneIndex(Idx), FTransform(TotalAdditiveTranslation));
}
}
}
// Finally, run adjustment blending to generate the warped poses for each bone
//@todo_fer: We could extract and cache this offline when the WarpingWindow is created
// 按上面UE5官方注释,目前叠加骨骼数据是运行时计算的,未来版本会考虑改为离线烘焙存储,运行时效率更高
const float SampleRate = 1 / 60.f;
FMotionDeltaTrackContainer MotionDeltaTracks;
// 对RootMotion动从[ActualStartTime, EndTime]区间内按60帧/1秒采样率存储指定骨骼数据,结果输出MotionDeltaTracks(可以理解为叠加动画的BasePose数据)
URootMotionModifier_AdjustmentBlendWarp::ExtractMotionDeltaFromRange(RequiredBones, Animation.Get(), ActualStartTime, EndTime, SampleRate, MotionDeltaTracks);
// MotionDeltaTracks叠加AdditivePose数据,结果输出Result变量(FAnimSequenceTrackContainer),Result记录的就是指定骨骼,每一帧缩放叠加后的骨骼数据(60帧/1秒)
URootMotionModifier_AdjustmentBlendWarp::AdjustmentBlendWarp(RequiredBones, AdditivePose, MotionDeltaTracks, Result);
}
- 后续每次调用ProcessRootMotion时,通过当前RootMotion蒙太奇当前帧/上一帧播放进度,分别抽取root骨骼数据,计算差值得到RootMotionDelta。
FTransform URootMotionModifier_AdjustmentBlendWarp::ExtractWarpedRootMotion() const
{
// 从计算好的骨骼动画数据中,抽取上一帧缩放后root骨骼数据
FTransform StartRootTransform;
ExtractBoneTransformAtTime(StartRootTransform, 0, PreviousPosition);
// 从计算好的骨骼动画数据中,抽取当前帧缩放后root骨骼数据
FTransform EndRootTransform;
ExtractBoneTransformAtTime(EndRootTransform, 0, CurrentPosition);
// 计算root骨骼数据差值,即为当前帧缩放后的RootMotionDelta
return EndRootTransform.GetRelativeTransform(StartRootTransform);
}
- 对于缩放后的ik骨骼数据,Adjustment Blend Warp提供了GetAdjustmentBlendIKBoneTransformAndAlpha静态方法,需要从动画蓝图侧每帧调用获取ik骨骼数据,再设置给TwoBone IK节点做缩放。这里要注意,获取到的ik骨骼数据是世界空间下的,所以TwoBone IK节点也要设置为World Space。
UFUNCTION(BlueprintPure, Category = "Motion Warping")
static void GetAdjustmentBlendIKBoneTransformAndAlpha(ACharacter* Character, FName BoneName, FTransform& OutTransform, float& OutAlpha);
// 计算缩放后的ik骨骼
void URootMotionModifier_AdjustmentBlendWarp::GetIKBoneTransformAndAlpha(FName BoneName, FTransform& OutTransform, float& OutAlpha) const
{
if (Result.GetNum() == 0 || !bWarpIKBones || !IKBones.Contains(BoneName))
{
OutTransform = FTransform::Identity;
OutAlpha = 0.f;
return;
}
// 抽取root骨骼事件窗口起始帧的FTransform
FTransform RootPrevPosition;
ExtractBoneTransformAtTime(RootPrevPosition, 0, 0.f);
// 抽取指定ik骨骼上一帧的FTransform
FTransform BoneTransform;
ExtractBoneTransformAtTime(BoneTransform, BoneName, PreviousPosition);
// 将世界空间下的Mesh位置->逆变换至播放RootMotion动画前位置->变化为上一帧的ik骨骼位置,因此得到是世界空间下的OutTransform,TwoBoneIK节点应在WorldSpace空间下使用该值
OutTransform = BoneTransform * RootPrevPosition.Inverse() * CachedMeshTransform;
OutAlpha = Weight;
}
Scaler缩放
Scaler缩放并不能指定Target点,仅仅是对原有RootMotion动画窗口期的RootMotion位移进行指定倍数的缩放,实现简单,在此不再赘述。
- 动画效果
Skew Warp缩放
Skew Warp缩放看字面意思是一种类似图像处理中的切变缩放,仅Translation缩放实现与Simple Warp有所不同。实测该缩放并没有提供额外字段,缩放效果与Simple Warp并无明显区别,在此不作分析。
- 动画效果
Motion Warping联网表现实测
UE4接入Motion Warping插件,笔者使用Advanced Locomotion System V4插件体验了攀爬表现,屏蔽了原本的Gameplay Update Actor Transform逻辑,改用Root Motion + Motion Warping实现攀爬逻辑。实测联网环境下,1P/3P客户端都能良好地适配不同高度障碍物做攀爬表现,推荐应用到各种带根骨骼位移的动作场景里。
UE4适配细节
UE5的Motion Warping插件仅需要少量代码改动,即可适配UE4。UE4.26项目只要做以下少量修改,就可以提前用上Motion Warping功能:
- Motion Warping插件源码适配
- 修改MotionWarpingComponent.h文件的FMotionWarpingWindowData结构体
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Defaults")
TObjectPtr<UAnimNotifyState_MotionWarping> AnimNotify = nullptr;UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Defaults")
UAnimNotifyState_MotionWarping* AnimNotify = nullptr;
- 修改MotionWarpingComponent.cpp的ExtractLocalSpacePose方法
UE::Anim::FStackAttributeContainer Attributes;FStackCustomAttributes Attributes;
- 修改MotionWarpingComponent.cpp的Update方法
UAnimMontage* Montage = RootMotionMontageInstance ? ToRawPtr(RootMotionMontageInstance->Montage) : nullptr;UAnimMontage* Montage = RootMotionMontageInstance ? RootMotionMontageInstance->Montage : nullptr;
- 修改RootMotionModifier.cpp的Update方法
const UAnimMontage* Montage = RootMotionMontageInstance ? ToRawPtr(RootMotionMontageInstance->Montage) : nullptr;const UAnimMontage* Montage = RootMotionMontageInstance ? RootMotionMontageInstance->Montage : nullptr;
- 修改RootMotionModifier_AdjustmentBlendWarp.cpp的AdjustmentBlendWarp方法及GetAdjustmentBlendIKBoneTransformAndAlpha方法
if (!FMath::IsNearlyZero(Total[Idx], (FVector::FReal)1.f))
{
const FVector::FReal Percent = Delta[Idx] / Total[Idx];
const FVector::FReal AdditiveDelta = FMath::Abs(Additive[Idx]) * Percent;
CurrentAdditive[Idx] = (Additive[Idx] > 0.f) ? PreviousAdditive[Idx] + AdditiveDelta : PreviousAdditive[Idx] - AdditiveDelta;
}if (!FMath::IsNearlyZero(Total[Idx], 1.f))
{
const float Percent = Delta[Idx] / Total[Idx];
const float AdditiveDelta = FMath::Abs(Additive[Idx]) * Percent;
CurrentAdditive[Idx] = (Additive[Idx] > 0.f) ? PreviousAdditive[Idx] + AdditiveDelta : PreviousAdditive[Idx] - AdditiveDelta;
}
const UAnimMontage* Montage = RootMotionMontageInstance ? ToRawPtr(RootMotionMontageInstance->Montage) : nullptr;const UAnimMontage* Montage = RootMotionMontageInstance ? RootMotionMontageInstance->Montage : nullptr;
- UE4.26源码适配
- 修改导出AnimCompositeBase.h的ConvertTrackPosToAnimPos方法
float ConvertTrackPosToAnimPos(const float& TrackPosition) const;ENGINE_API float ConvertTrackPosToAnimPos(const float& TrackPosition) const;
- 修改导出AnimationUtils.h的ExtractTransformFromTrack方法
static void ExtractTransformFromTrack(float Time, int32 NumFrames, float SequenceLength, const struct FRawAnimSequenceTrack& RawTrack, EAnimInterpolationType Interpolation, FTransform &OutAtom);ENGINE_API static void ExtractTransformFromTrack(float Time, int32 NumFrames, float SequenceLength, const struct FRawAnimSequenceTrack& RawTrack, EAnimInterpolationType Interpolation, FTransform &OutAtom);
- 修改CharacterMovementComponent.h的FOnProcessRootMotion定义,追加DeltaSecond参数
DECLARE_DELEGATE_RetVal_TwoParams(FTransform, FOnProcessRootMotion, const FTransform&, UCharacterMovementComponent*)DECLARE_DELEGATE_RetVal_ThreeParams(FTransform, FOnProcessRootMotion, const FTransform&, UCharacterMovementComponent*, float)
FTransform ConvertLocalRootMotionToWorld(const FTransform& InLocalRootMotion);FTransform ConvertLocalRootMotionToWorld(const FTransform& InLocalRootMotion, float DeltaSeconds);
- 修改CharacterMovementComponent.cpp的ConvertLocalRootMotionToWorld方法及调用相关代码
FTransform UCharacterMovementComponent::ConvertLocalRootMotionToWorld(const FTransform& LocalRootMotionTransform)
{
const FTransform PreProcessedRootMotion = ProcessRootMotionPreConvertToWorld.IsBound() ? ProcessRootMotionPreConvertToWorld.Execute(LocalRootMotionTransform, this) : LocalRootMotionTransform;
const FTransform WorldSpaceRootMotion = CharacterOwner->GetMesh()->ConvertLocalRootMotionToWorld(PreProcessedRootMotion);
return ProcessRootMotionPostConvertToWorld.IsBound() ? ProcessRootMotionPostConvertToWorld.Execute(WorldSpaceRootMotion, this) : WorldSpaceRootMotion;
}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;
}
//SimulateRootMotion方法内
const FTransform WorldSpaceRootMotionTransform = ConvertLocalRootMotionToWorld(LocalRootMotionTransform);const FTransform WorldSpaceRootMotionTransform = ConvertLocalRootMotionToWorld(LocalRootMotionTransform, DeltaSeconds);
//PerformMovement方法内
RootMotionParams.Set( ConvertLocalRootMotionToWorld(RootMotionParams.GetRootMotionTransform()) );RootMotionParams.Set( ConvertLocalRootMotionToWorld(RootMotionParams.GetRootMotionTransform(), DeltaSeconds));