Motion Warping
Motion Warping
一种可以动态调整角色的根骨骼运动以对齐目标的功能。在UE中是以插件的方式集成。

使用该功能的动画一定要开启Root Motion
Example Facing Rotation
当主角背向敌人时,想要主角边播放动画边旋转至面向敌人,如下图效果:

实现该功能分为以下几步:
- 添加
MotionWarpingComponent
组件到主角上 - 设置转向目标位置:
- 在攻击蒙太奇中添加
MotionWarping
动画通知状态,其长度为旋转到目标点的时间 - 配置
MotionWarping
动画通知状态:
执行逻辑

以上面功能讲一下执行逻辑
Add Target
将目标信息添加到UMotionWarpingComponent::WarpTargets
数组中,在URootMotionModifier
使用时,直接根据URootMotionModifier::WarpTargetName
获取信息。
这里信息主要包含:
- Taransform
- Warp Target Name
ProcessRootMotionPreConvertToWorld
这是整个系统的入口,使用UCharacterMovementComponent::ProcessRootMotionPreConvertToWorld
回调触发,该回调函数会在角色播放开启RootMotion
的动画时触发,且是每一帧调用。
DECLARE_DELEGATE_RetVal_ThreeParams(FTransform, FOnProcessRootMotion, const FTransform&, UCharacterMovementComponent*, float)
回调是在将角色的根节点的位置信息转为世界坐标之前调用,这里传入根节点的当前帧变换的Transform,同样返回值也需要是这个。
UMotionWarpingComponent::ProcessRootMotionPreConvertToWorld
主要以下3个功能:
- 添加变形器
- 更新当前存在的变形器
- 应用变形器中的变形数据到返回值中,如果同时存在多个变形器,会使用最后一个变形器的结果
Add Modifier
获取当前角色播放动画的所有动画通知,筛选出UAnimNotifyState_MotionWarping
类型,并根据动画通知时间段和当前时间筛选。
筛选出的动画通知调用AnimNotifyState_MotionWarping::OnBecomeRelevant
方法,该方法将动画通知中配置的URootMotionModifier
添加到UMotionWarpingComponent::Modifiers
。
Modifier Update
根据当前动画播放的时间点,设置URootMotionModifier
的状态:
enum class ERootMotionModifierState : uint8
{
/** The modifier is waiting for the animation to hit the warping window */
Waiting,
/** The modifier is active and currently affecting the final root motion */
Active,
/** The modifier has been marked for removal. Usually because the warping window is done */
MarkedForRemoval,
/** The modifier will remain in the list (as long as the window is active) but will not modify the root motion */
Disabled
};
Modifier ProcessRootMotion
URootMotionModifier::ProcessRootMotion
方法将目标点的Transform应用根骨骼中,该方法可以重载,这里讲一下URootMotionModifier_SkewWarp
中的使用,简单分为两种:
- 位移:动画中当前根节点位移 + Warp Translation
- 旋转:动画中当前根节点旋转 + Warp Rotation
Warning
值得注意的是:这里计算的位移和旋转都是变化值。目标位置和旋转是世界坐标,最后输出的是模型空间根节点的信息。
Warp Translation
分为两种:动画中有位移和无位移
Translation In Animation
核心的方法是URootMotionModifier_SkewWarp::WarpTranslation
,它实现了一种称为 Skew Warp(倾斜扭曲) 的技术。Skew Warp 的目的是通过动态调整角色的根运动(Root Motion),使得角色在播放动画时能够更好地对齐目标位置(TargetLocation
)。以下是对这段代码的详细讲解:
Warning
该方法中提到的转回世界坐标,是CurrentTransform
的坐标空间,如果CurrentTransform
为原点值(Identity),那么这里提到的世界坐标就是本地坐标。
---
代码的功能概述
这段代码的主要功能是:
- 计算角色当前的运动状态:包括当前的位置、旋转以及未来的位置(基于根运动的位移)。
- 构建一个变换矩阵:将角色的运动从世界空间转换到一个特定的同步空间(
RootSyncSpace
),以便更容易计算扭曲。 - 计算倾斜角度:通过比较当前运动方向与目标方向的差异,计算出需要在 Yaw(偏航角)和 Pitch(俯仰角)方向上进行的倾斜调整。
- 应用扭曲:通过缩放和倾斜矩阵对根运动进行调整,使得角色的运动能够更好地对齐目标位置。
- 将结果转换回世界空间:将调整后的运动向量转换回世界空间,并返回最终的位移向量。
代码的详细解析
输入参数
CurrentTransform
:角色当前的变换(位置和旋转),传入的FTransform::Identity
。DeltaTranslation
:当前帧的根运动位移(增量位移)。TotalTranslation
:从动画开始到当前帧的总根运动位移。TargetLocation
:目标位置,角色需要对齐的位置。
计算当前和未来的位置
const FVector CurrentLocation = CurrentTransform.GetLocation(); const FVector FutureLocation = CurrentLocation + TotalTranslation;
CurrentLocation
:角色当前的位置。FutureLocation
:如果按照当前的根运动继续移动,角色未来的位置。
计算当前到目标和当前到根运动的偏移
const FVector CurrentToWorldOffset = TargetLocation - CurrentLocation; const FVector CurrentToRootOffset = FutureLocation - CurrentLocation;
CurrentToWorldOffset
:从当前位置到目标位置的向量。CurrentToRootOffset
:从当前位置到未来位置的向量(基于根运动)。
构建同步空间(RootSyncSpace)
FVector ToRootNormalized = CurrentToRootOffset.GetSafeNormal(); FMatrix ToRootSyncSpace = FRotationMatrix::MakeFromXZ(ToRootNormalized, CurrentRotation.GetAxisZ());
ToRootNormalized
:根运动方向的单位向量。ToRootSyncSpace
:一个变换矩阵,用于将世界空间的向量转换到以根运动方向为 X 轴的同步空间。
计算倾斜角度
- Yaw(偏航角):
float AngleAboutZ = FMath::Acos(FVector::DotProduct(FlatToWorld, FlatToRoot));
- 计算当前运动方向与目标方向在水平面上的夹角。
- Pitch(俯仰角):
const float AngleAboutY = FMath::Acos(FVector::DotProduct(ToWorldNoY, ToRootNoY));
- 计算当前运动方向与目标方向在垂直面上的夹角。
- Yaw(偏航角):
构建缩放和倾斜矩阵
- 缩放矩阵:
ScaleMatrix.SetAxis(0, FVector(ProjectedScale, 0.0f, 0.0f));
- 根据目标位置与根运动的比例,对 X 轴进行缩放。
- 倾斜矩阵:
ShearXAlongYMatrix.SetAxis(0, FVector(1.0f, FMath::Tan(AngleAboutZNorm), 0.0f)); ShearXAlongZMatrix.SetAxis(0, FVector(1.0f, 0.0f, FMath::Tan(AngleAboutYNorm)));
- 根据 Yaw 和 Pitch 角度,对 X 轴进行倾斜调整。
- 缩放矩阵:
应用扭曲
FMatrix ScaledSkewMatrix = ScaleMatrix * ShearXAlongYMatrix * ShearXAlongZMatrix; SkewedRootMotion = ScaledSkewMatrix.TransformVector(RootMotionInSyncSpace);
- 将缩放和倾斜矩阵应用到根运动向量上,得到扭曲后的运动向量。
将结果转换回世界空间
return ToRootSyncSpace.TransformVector(SkewedRootMotion);
- 将扭曲后的运动向量从同步空间转换回世界空间,并返回最终的位移向量。
代码的核心思想
- 同步空间:
- 通过构建一个以根运动方向为 X 轴的同步空间,简化了倾斜和缩放的计算。
- 倾斜和缩放:
- 通过计算
Yaw
和Pitch
角度,对根运动进行倾斜调整。 - 通过目标位置与根运动的比例,对根运动进行缩放。
- 通过计算
- 动态调整:
- 在运行时动态调整根运动,使得角色能够更好地对齐目标位置。
No Translation In Animation
直接将插值后的位移变化值添加到动画中根节点的位移变化值上:
float Alpha = FMath::Clamp((CurrentPosition - ActualStartTime) /(EndTime - ActualStartTime), 0.f, 1.f);
Alpha = FAlphaBlend::AlphaToBlendOption(Alpha,AddTranslationEasingFunc, AddTranslationEasingCurve);
const FVector NextLocation = FMath::Lerp<FVector, float>(StartTransformGetLocation(), TargetLocation, Alpha);
FVector FinalDeltaTranslation = (NextLocation - CurrentLocation);
FinalDeltaTranslation = (CurrentRotation.Inverse() * DeltaToTargetToOrientationQuat()).GetForwardVector() * FinalDeltaTranslation.Size();
FinalDeltaTranslation = CharacterOwner->GetBaseRotationOffset()UnrotateVector(FinalDeltaTranslation);
FinalRootMotion.SetTranslation(FinalDeltaTranslation + ExtraRootMotionGetLocation());
Warp Rotation
核心函数是 URootMotionModifier_Warp::WarpRotation
,用于在根运动(Root Motion)中调整角色的旋转,使其朝向目标方向。它主要用于动画扭曲(Motion Warping)系统,允许角色在播放动画时动态调整旋转方向。
以下是代码的详细讲解:
函数目的
- 输入:
RootMotionDelta
:当前帧的根运动变换(通常是模型空间的变换)。RootMotionTotal
:累计的根运动变换(通常是模型空间的变换)。DeltaSeconds
:当前帧的时间步长。
- 输出:
- 返回一个
FQuat
,表示经过扭曲处理后的旋转增量。
- 返回一个
代码逻辑分解
- 获取角色和初始数据
const ACharacter* CharacterOwner = GetCharacterOwner(); if (CharacterOwner == nullptr) { return FQuat::Identity; }
- 获取角色对象
CharacterOwner
,如果角色不存在,则返回一个单位四元数(FQuat::Identity
),表示没有旋转变化。
- 获取角色对象
- 计算当前旋转和目标旋转
const FQuat TotalRootMotionRotation = RootMotionTotal.GetRotation(); const FQuat CurrentRotation = CharacterOwner->GetActorQuat() * CharacterOwner->GetBaseRotationOffset(); const FQuat TargetRotation = CurrentRotation.Inverse() * (GetTargetRotation() * CharacterOwner->GetBaseRotationOffset());
TotalRootMotionRotation
:从累计的根运动变换中提取旋转部分。CurrentRotation
:角色的当前世界空间旋转,考虑了BaseRotationOffset
(角色相对于其基座的旋转偏移)。TargetRotation
:目标旋转,通过GetTargetRotation()
获取目标方向,并考虑BaseRotationOffset
。
- 计算插值系数(Alpha)
const float TimeRemaining = (EndTime - PreviousPosition) * WarpRotationTimeMultiplier; const float Alpha = FMath::Clamp(DeltaSeconds / TimeRemaining, 0.f, 1.f);
TimeRemaining
:剩余的扭曲时间,考虑了WarpRotationTimeMultiplier
(扭曲旋转时间倍率)。Alpha
:插值系数,用于控制旋转插值的进度。它被限制在[0, 1]
范围内。
- 插值计算目标旋转
FQuat TargetRotThisFrame = FQuat::Slerp(TotalRootMotionRotation, TargetRotation, Alpha);
- 使用球面线性插值(
Slerp
)在TotalRootMotionRotation
和TargetRotation
之间插值,得到当前帧的目标旋转TargetRotThisFrame
。
- 使用球面线性插值(
- 处理非插值旋转方法
if (RotationMethod != EMotionWarpRotationMethod::Slerp) { const float AngleDeltaThisFrame = TotalRootMotionRotation.AngularDistance(TargetRotThisFrame); const float MaxAngleDelta = FMath::Abs(FMath::DegreesToRadians(DeltaSeconds * WarpMaxRotationRate)); const float TotalAngleDelta = TotalRootMotionRotation.AngularDistance(TargetRotation); if (RotationMethod == EMotionWarpRotationMethod::ConstantRate && (TotalAngleDelta <= MaxAngleDelta)) { TargetRotThisFrame = TargetRotation; } else if ((AngleDeltaThisFrame > MaxAngleDelta) || RotationMethod == EMotionWarpRotationMethod::ConstantRate) { const FVector CrossProduct = FVector::CrossProduct(TotalRootMotionRotation.Vector(), TargetRotation.Vector()); const float SignDirection = FMath::Sign(CrossProduct.Z); const FQuat ClampedRotationThisFrame = FQuat(FVector(0, 0, 1), MaxAngleDelta * SignDirection); TargetRotThisFrame = ClampedRotationThisFrame; } }
- 如果旋转方法不是
Slerp
,则根据旋转方法(ConstantRate
或其他)调整目标旋转:AngleDeltaThisFrame
:当前帧的旋转角度变化。MaxAngleDelta
:允许的最大旋转角度变化(基于WarpMaxRotationRate
)。TotalAngleDelta
:总旋转角度变化。- 如果旋转方法是
ConstantRate
且总角度变化小于最大角度变化,则直接使用目标旋转。 - 如果当前帧的角度变化超过最大角度变化,或者旋转方法是
ConstantRate
,则对旋转进行限制,确保旋转速率不超过WarpMaxRotationRate
。
- 如果旋转方法不是
- 计算旋转增量
const FQuat DeltaOut = TargetRotThisFrame * TotalRootMotionRotation.Inverse();
- 计算当前帧的旋转增量
DeltaOut
,即目标旋转相对于累计根运动旋转的变化。
- 计算当前帧的旋转增量
- 返回最终的旋转增量
return (DeltaOut * RootMotionDelta.GetRotation());
- 将旋转增量
DeltaOut
与当前帧的根运动旋转相乘,得到最终的旋转增量并返回。
- 将旋转增量
总结
- 该函数的核心目标是通过插值或限制旋转速率,将角色的旋转从当前方向调整到目标方向。
- 它支持两种旋转方法:
- Slerp:平滑插值到目标旋转。
- ConstantRate:以恒定速率旋转到目标方向,避免旋转过快。
- 最终返回的旋转增量可以应用于角色的根运动,实现动态的旋转调整。
TODO
Rotation Cuve
对旋转添加曲线控制插值进度,可以在以下代码进行修改:

Target Rotation
当旋转使用面向时,计算目标旋转是使用的角色的实施位置。这会在距离非常近时出现问题,所以这里可以使用StartTransform
来解决。
Debug
- a.MotionWarping.Debug
- a.MotionWarping.DrawDebugLifeTime