UE5 Motion Warping(运动扭曲) 原理剖析及UE4适配

本文简析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概念图解
一个翻越动画,适配不同高度地形的翻越表现

在没有Motion Warping插件之前,Gameplay程序员要实现类似功能,需要禁用RootMotion动画的EnableRootMotion选项,使其变成不带位移的动画,播放动画的同时,自行做Update逻辑每帧更新Actor位置,直到Gameplay指定位置。而UE5的Motion Warping插件,巧妙地利用动画事件对RootMotion动画资源定义缩放区间,缩放区间内的根骨骼运动。只要简单配置Motion Warping动画事件,正常播放RootMotion动画,就能达到同样的效果,从而简化Gameplay开发逻辑。

Motion Warping实现

Motion Warping源码UML类图

UE5的Motion Warping功能以插件形式内嵌引擎,其插件源码类图关系关于如上所示。

UMotionWarpingComponent作为核心组件,其职责:

  1. 负责监听对应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);
    }
}

ProcessRootMotionPreConvertToWorldProcessRootMotionPostConvertToWorld这两个事件通过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的场景。

  1. 当前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);
}
  1. 提供添加/移除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);
}
  1. Motion Warping动画事件窗口期,每帧调用指定的URootMotionModifier类的ProcessRootMotion方法,对每帧的RootMotionDelta进行缩放,进而缩放当前帧的移动。而Simple Warp/Adjustment Blend Warp/Skew Warp/Scale等几种URootMotionModifier子类,负责真正的缩放计算逻辑,是Motion Warping的核心实现。
通过拖动事件起始/结束时间确定Motion Warping窗口期,一段RootMotion动画可定义多个不同Warp Target的Motion Warping事件
Motion Warping Anim Notify 可配置字段

RootMotionModifier缩放算法实现

Motion Warping共有Simple Warp/Adjustment Blend Warp/Scale Warp/Skew Warp共4种不同缩放模式,其中Simple Warp,Adjustment Blend Warp缩放算法较有实用价值,做详细分析,Scale Warp/Skew Warp只做粗略介绍。

Simple 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骨骼的效果,已经没有滑步表现,同时在不同的位移缩放下,腿部伸展度是有细微差别的,整体动作表现更真实。

只缩放root骨骼效果
同时缩放腿部ik骨骼效果
  • 实现分析

Adjustment Blend Warp缩放算法采用与Simple Warp完全不同的缩放思路。该算法是在第一次缩放时,就计算好窗口期间,root骨骼在组件空间下的缩放差值,也就是root骨骼的Mesh空间缩放叠加动画,每帧对RootMotion源动画并应用缩放叠加动画,从而实现缩放。采用这种算法,不仅可以针对root骨骼叠加动画数据进行缩放,理论还能针对骨架上的任意骨骼进行同样的缩放,因此Adjustment Blend Warp缩放支持配置,针对ik骨骼进行缩放。比如对带脚步移动的RootMotion动画应用Adjustment Blend Warp缩放,不仅能够灵活缩放位移,配合TwoBone IK,还能规避缩放位移导致的腿部滑步表现。

下面简析代码:

  1. 首先,在窗口期第一次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);
}
  1. 后续每次调用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);
}

  1. 对于缩放后的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;
}
缩放后ik骨骼传入TwoBone IK节点处理细节

Scaler缩放

Scaler缩放并不能指定Target点,仅仅是对原有RootMotion动画窗口期的RootMotion位移进行指定倍数的缩放,实现简单,在此不再赘述。

  • 动画效果
Scaler Warp缩放 - 攻击动画缩放3倍位移效果

Skew Warp缩放

Skew Warp缩放看字面意思是一种类似图像处理中的切变缩放,仅Translation缩放实现与Simple Warp有所不同。实测该缩放并没有提供额外字段,缩放效果与Simple Warp并无明显区别,在此不作分析。

  • 动画效果
Skew Warp缩放

Motion Warping联网表现实测

UE4接入Motion Warping插件,笔者使用Advanced Locomotion System V4插件体验了攀爬表现,屏蔽了原本的Gameplay Update Actor Transform逻辑,改用Root Motion + Motion Warping实现攀爬逻辑。实测联网环境下,1P/3P客户端都能良好地适配不同高度障碍物做攀爬表现,推荐应用到各种带根骨骼位移的动作场景里。

ALSV4原生效果 - 禁用RootMotion,自行Update更新位置
单RootMotion效果,无法适配不同高度障碍物攀爬
RootMotion + Motion Warping效果,仅需配置即达到原生效果

UE4适配细节

UE5的Motion Warping插件仅需要少量代码改动,即可适配UE4。UE4.26项目只要做以下少量修改,就可以提前用上Motion Warping功能:

  • Motion Warping插件源码适配
  1. 修改MotionWarpingComponent.h文件的FMotionWarpingWindowData结构体

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Defaults")
TObjectPtr<UAnimNotifyState_MotionWarping> AnimNotify = nullptr;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Defaults")
UAnimNotifyState_MotionWarping* AnimNotify = nullptr;

  1. 修改MotionWarpingComponent.cpp的ExtractLocalSpacePose方法

UE::Anim::FStackAttributeContainer Attributes;

FStackCustomAttributes Attributes;

  1. 修改MotionWarpingComponent.cpp的Update方法

UAnimMontage* Montage = RootMotionMontageInstance ? ToRawPtr(RootMotionMontageInstance->Montage) : nullptr;

UAnimMontage* Montage = RootMotionMontageInstance ? RootMotionMontageInstance->Montage : nullptr;

  1. 修改RootMotionModifier.cpp的Update方法

const UAnimMontage* Montage = RootMotionMontageInstance ? ToRawPtr(RootMotionMontageInstance->Montage) : nullptr;

const UAnimMontage* Montage = RootMotionMontageInstance ? RootMotionMontageInstance->Montage : nullptr;

  1. 修改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源码适配
  1. 修改导出AnimCompositeBase.h的ConvertTrackPosToAnimPos方法

float ConvertTrackPosToAnimPos(const float& TrackPosition) const;

ENGINE_API float ConvertTrackPosToAnimPos(const float& TrackPosition) const;

  1. 修改导出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);

  1. 修改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);

  1. 修改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));

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,324评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,356评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,328评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,147评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,160评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,115评论 1 296
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,025评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,867评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,307评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,528评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,688评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,409评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,001评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,657评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,811评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,685评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,573评论 2 353

推荐阅读更多精彩内容