美文网首页
UE5 Motion Warping(运动扭曲) 原理剖析及UE

UE5 Motion Warping(运动扭曲) 原理剖析及UE

作者: 立航 | 来源:发表于2021-06-08 11:13 被阅读0次

本文简析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));

相关文章

  • UE5 Motion Warping(运动扭曲) 原理剖析及UE

    本文简析UE5动画新功能Motion Warping的原理实现,实测不同Warp类型的动画效果,重点分析Simpl...

  • 程序打包

    关于UE5打包问题[https://www.bilibili.com/read/cv11679358] UE5 P...

  • 调试

    UE4/UE5的崩溃,卡死等问题处理[https://zhuanlan.zhihu.com/p/565680732]

  • 目录、资产命名规范

    【UE5】目录、资产命名规范[https://zhuanlan.zhihu.com/p/484119115]

  • 地理坐标转换

    关联GIS:条条道路通UE5城[https://zhuanlan.zhihu.com/p/528244402] 关...

  • 通讯

    开源篇-WebSocket搭建UE5通信桥梁[https://zhuanlan.zhihu.com/p/54621...

  • 源代码

    从零开始:编译UE5 source code[https://www.jianshu.com/p/4a6b8603...

  • UE 命名规范

    资产命名表格链接:UE5项目命名规则[https://link.zhihu.com/?target=https%3...

  • UE5蓝图-动态创建和查找模型

    UE5蓝图-动态创建和查找模型,并控制其显隐性 蓝图 BP_RedEarth 蓝图 BP_CreateRedEar...

  • UE4/UE5 动画的原理和性能优化

    动画在UE4/UE5项目中,往往不仅是GPU和渲染线程开销大户,也是游戏线程的开销大户。按照我的经验,大型游戏项目...

网友评论

      本文标题:UE5 Motion Warping(运动扭曲) 原理剖析及UE

      本文链接:https://www.haomeiwen.com/subject/tdyatltx.html