1. Preface

Before we learn the motion warping, let’s play a mind exercise game, how should we implement a climbing system with a given animation sequence with root motion data? The animation artist made this animation sequence in DCC and originally it’s made for climbing a wall with 1m high.

Game is about interactivity, in game world there are always walls with different height, an animation sequence with fixed root motion data cannot match all the walls in game. Furthermore, Uncharted provides excellent climbing performance, have you ever thought about how does this game implement it?

Walls with differenent height
Walls with differenent height

In the past, programmers can disable the root motion and manually control the movement of the character to solve this problem, but it comes with the following problems:

  • Dirty Code : We move the character manually, which add dirty code to the movement component or something else.
  • Unnatural Performance : An animation sequence with root motion data contains a lot of key frames, which give the flexiable result for the character, it cannot be simulated by any interpolate algorithm easily.

Therefore, in 1995, Andrew Witkin and Zoran Popovic introduce the Motion Warping in siggraph. This technique focuses on modifying the root motion data to match the exact landscape. It’s a four-page short article and the mathemetic theorem behind it is not complicated.

Motion Warping helps you to create excellent games like Uncharted!

2. Mathemetic Theorem

The following image shows an example, the left is the original animation, and we want to modify the root motion data to get the result in the right.

Left : Original, Right : Target

Let the movement vector in left denoted as vo, and the right as vt, we have

v_t = M*v_o \\
v_t = \begin{bmatrix}
\frac{x_{t}}{x_{o}} & 0 & 0 \\
0 & \frac{y_t}{y_o} & 0 \\
0 & 0 & \frac{z_t}{z_o} \\
\end{bmatrix} * 
\begin{bmatrix}
x_o \\
y_o \\
z_o
\end{bmatrix}

in an exact implementation, we not only deal with the locaiton, but also the rotation, so we use the transform matrix instread of the location vector, but whatever, transform the original to target still only requires for a matrix to multiply it.

Just multiply a matrix? So easy. But, we only apply motion warping to an exact period of the animation sequence, which looks like the animatio notify state in Unreal Engine:

The motion warping in Unreal Engine uses animation notify state to indicate where to apply the motion warping.

What should we do? simply multiply the M to the root motion data in each frame? Is the result correct?

It is worthy to emphasize how does the root motion data work, it stores the data by curves. In some implementation, in each tick, the game engine sample the curve and use the curve value as the displacement to drive the character(Not recommended, I will explain you why later). For example, an animation sequence drive the character to move non-linearly in x-axis by the following curve:

Each tick the engine sample the curve and evaluate the curve value diff with the curve value last frame, and drive the character via the result.

As a result, the expected total distance driven by root motion between two give times in any axis can be denoted as:

x = x_0 + \Delta x \\
x = x_0 + \int_{t_0}^{t_1} f(x)dt \\
\Delta x = \int_{t_0}^{t_1} f(x)dt

In this situation, simply multiply the constant to the root motion data in each frame in given period is correct, We can prove it by the calculus basical theorem:

\int_a^bcf(x)dx = c\int_a^bf(x)dx

let t1 ~ t2 be the scaled period:

 x = x_0 + \Delta x_1 +c\Delta x_{2} + \Delta x_3\\
x = x_0 + \int_{t_0}^{t_1}f(x)dt + \int_{t_1}^{t_2}cf(x)dt + \int_{t_2}^{t_3}f(x)dt \\
x = x_0 + \int_{t_0}^{t_1}f(x)dt + c\int_{t_1}^{t_2}f(x)dt + \int_{t_2}^{t_3}f(x)dt

But another implementation uses the curve to store the relative value, in this situation, the expected total distance driven by root motion between two give times in any axis can be denoted as:

 x = x_0 + \Delta x \\x = x_0 + f(t)

In each period from t0 to t1 we have:

\Delta x = \sum_a^b (f(t_{n}) -f(t_{n-1})) \\ =f(b) -f(b-1) +f(b-1)+f(b-2) + \dots -f(a+1) +f(a+1)-f(a) \\=f(t_b) - f(t_a)

Of course in this situation it is correct. we use the denotation below

x = x_0 + \Delta x_1 + c\Delta x_2 + \Delta x_3 \\
x = x_0 + f(t_1) - f(t_0) + (cf(t_1) - cf(t_2)) + f(t_3) - f(t_2) \\
x = x_0 + f(t_1) - f(t_0) + c(f(t_1) - f(t_2)) + f(t_3) - f(t_2)

So no matter which implementation, simply multiply a cofactor to the root motion in each frame is correct. That is how Unreal Engine does.

Furthermore, sometimes we don’t need the root bone to target location, we need an exact given bone, like Uncharted, We need to hand bone to catch the rock. We know that at the end of the peroid, the relative transform between the root bone and hand bone is the same.

\Delta M =M_{root}^{-1}*M_{hand}\ \ \text{(from root to hand)}

Note that delta M is the relative transform of the last frame, not any other frame in the period, it is changing with the frame.

as a result, we can recalculate the result transform of the root bone:

M_{root-target} = \Delta M^{-1}*M_{hand-target} = M_{hand}^{-1}*M_{root}*M_{hand-target}

3. Why the former implementation is not recommended

In the inchoate games, game developers assume that players only play their game with locked frame rate. That is very common in a game console. For example, Resistant Evil has a famous bug that the knife will cause more damage when playing with a higher frame rate. The game is designed to locked in 60 fps, but nowadays player can play it with 240fps.

In fact, the world of the game is discrete, thus integration is not the proper mathemetic tool to describe our root motion model. A better model is the seires.

x = x_0+\Delta x \\
x = x_0 + \sum_{k=a}^{b}t_k*f(k) \neq x_0 + \int_a^bf(t)dt

In most cases, tk is not a constant because the frame rate of the game is not stable, the more fps the game has, the closer to the result calculated by the integration.

The red part : Intergration; The blue part : Series

In short, in the former implementation, the value of the curve represents the velocity, which is the derivative of displacement. In the latter one, the value of the curve correspounding to the displacement, which is the intergration of the velocity. The velocity curve brings the root motion frame-rate-unstable and the data inside it is hard to read. As a result, modern engine uses the displacement curve to implement the root motion, including the Unreal Engine.

4. Unreal Engine Implementation

First of all, unreal engine uses a component to give the actor the ability to be modified by motion warping, it is called UMotionWarpingComponent:

C++
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMotionWarpingPreUpdate, class UMotionWarpingComponent*, MotionWarpingComp);

UCLASS(ClassGroup = Movement, meta = (BlueprintSpawnableComponent))
class MOTIONWARPING_API UMotionWarpingComponent : public UActorComponent
{
	GENERATED_BODY()

public:

	/** Whether to look inside animations within montage when looking for warping windows */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Config")
	bool bSearchForWindowsInAnimsWithinMontages;

	/** Event called before Root Motion Modifiers are updated */
	UPROPERTY(BlueprintAssignable, Category = "Motion Warping")
	FMotionWarpingPreUpdate OnPreUpdate;

	UMotionWarpingComponent(const FObjectInitializer& ObjectInitializer);

	virtual void InitializeComponent() override;
	virtual void GetLifetimeReplicatedProps(TArray< FLifetimeProperty >& OutLifetimeProps) const;

	/** Gets the character this component belongs to */
	FORCEINLINE ACharacter* GetCharacterOwner() const { return CharacterOwner.Get(); }

	/** Returns the list of root motion modifiers */
	FORCEINLINE const TArray<URootMotionModifier*>& GetModifiers() const { return Modifiers; }

	/** Check if we contain a RootMotionModifier for the supplied animation and time range */
	bool ContainsModifier(const UAnimSequenceBase* Animation, float StartTime, float EndTime) const;

	/** Add a new modifier */
	int32 AddModifier(URootMotionModifier* Modifier);
C++

The component manages a array of URootMotionModifier, the modifier actually modify the root motion data via the method ProcessRootMotion. The URootMotionModifier is an abstract class.

C++
UCLASS(Abstract, BlueprintType, EditInlineNew)
class MOTIONWARPING_API URootMotionModifier : public UObject
{
	GENERATED_BODY()

public:

	/** Source of the root motion we are warping */
	UPROPERTY(BlueprintReadOnly, Category = "Defaults")
	TWeakObjectPtr<const UAnimSequenceBase> Animation = nullptr;

	/** Start time of the warping window */
	UPROPERTY(BlueprintReadOnly, Category = "Defaults")
	float StartTime = 0.f;

	/** End time of the warping window */
	UPROPERTY(BlueprintReadOnly, Category = "Defaults")
	float EndTime = 0.f;

	/** Previous playback time of the animation */
	UPROPERTY(BlueprintReadOnly, Category = "Defaults")
	float PreviousPosition = 0.f;

	/** Current playback time of the animation */
	UPROPERTY(BlueprintReadOnly, Category = "Defaults")
	float CurrentPosition = 0.f;

	/** Current blend weight of the animation */
	UPROPERTY(BlueprintReadOnly, Category = "Defaults")
	float Weight = 0.f;

	/** Character owner transform at the time this modifier becomes active */
	UPROPERTY(Transient, BlueprintReadOnly, Category = "Defaults")
	FTransform StartTransform;

	/** Actual playback time when the modifier becomes active */
	UPROPERTY(Transient, BlueprintReadOnly, Category = "Defaults")
	float ActualStartTime = 0.f;

	/** Delegate called when this modifier is activated (starts affecting the root motion) */
	UPROPERTY()
	FOnRootMotionModifierDelegate OnActivateDelegate;

	/** Delegate called when this modifier updates while active (affecting the root motion) */
	UPROPERTY()
	FOnRootMotionModifierDelegate OnUpdateDelegate;

	/** Delegate called when this modifier is deactivated (stops affecting the root motion) */
	UPROPERTY()
	FOnRootMotionModifierDelegate OnDeactivateDelegate;

	URootMotionModifier(const FObjectInitializer& ObjectInitializer);

	/** Called when the state of the modifier changes */
	virtual void OnStateChanged(ERootMotionModifierState LastState);

	/** Sets the state of the modifier */
	void SetState(ERootMotionModifierState NewState);

	/** Returns the state of the modifier */
	FORCEINLINE ERootMotionModifierState GetState() const { return State; }

	/** Returns a pointer to the component that owns this modifier */
	UMotionWarpingComponent* GetOwnerComponent() const;

	/** Returns a pointer to the character that owns the component that owns this modifier */
	class ACharacter* GetCharacterOwner() const;

	virtual void Update(const FMotionWarpingUpdateContext& Context);
	virtual FTransform ProcessRootMotion(const FTransform& InRootMotion, float DeltaSeconds) { return FTransform::Identity; }

	FORCEINLINE const UAnimSequenceBase* GetAnimation() const { return Animation.Get(); }

private:

	friend UMotionWarpingComponent;

	/** Current state */
	UPROPERTY()
	ERootMotionModifierState State = ERootMotionModifierState::Waiting;
};
C++

When after the animation system calculated the root motion data and before it convert the them to world space, and of course before applying them too.

C++
void UMotionWarpingComponent::InitializeComponent()
{
	Super::InitializeComponent();

	CharacterOwner = Cast<ACharacter>(GetOwner());

	UCharacterMovementComponent* CharacterMovementComp = CharacterOwner.IsValid() ? CharacterOwner->GetCharacterMovement() : nullptr;
	if (CharacterMovementComp)
	{
 		CharacterMovementComp->ProcessRootMotionPreConvertToWorld.BindUObject(this, &UMotionWarpingComponent::ProcessRootMotionPreConvertToWorld);
	}
}

FTransform UMotionWarpingComponent::ProcessRootMotionPreConvertToWorld(const FTransform& InRootMotion, UCharacterMovementComponent* CharacterMovementComponent, float DeltaSeconds)
{
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
	if (FMotionWarpingCVars::CVarMotionWarpingDisable.GetValueOnGameThread() > 0)
	{
		return InRootMotion;
	}
#endif

	// Check for warping windows and update modifier states
	Update(DeltaSeconds);

	FTransform FinalRootMotion = InRootMotion;

	// Apply Local Space Modifiers
	for (URootMotionModifier* Modifier : Modifiers)
	{
		if (Modifier->GetState() == ERootMotionModifierState::Active)
		{
			FinalRootMotion = Modifier->ProcessRootMotion(FinalRootMotion, DeltaSeconds);
		}
	}
	
	......
C++
C++
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;
}
C++

It uses an animation state notify to indicate when to use the motion warping, it exposes three blueprint implementable event for users to expanded it. In default, the three blueprint functions has no implementation:

C++
/** AnimNotifyState used to define a motion warping window in the animation */
UCLASS(meta = (DisplayName = "Motion Warping"))
class MOTIONWARPING_API UAnimNotifyState_MotionWarping : public UAnimNotifyState
{
	GENERATED_BODY()

public:

	//@TODO: Prevent notify callbacks and add comments explaining why we don't use those here.

	UPROPERTY(EditAnywhere, Instanced, BlueprintReadWrite, Category = "Config")
	TObjectPtr<URootMotionModifier> RootMotionModifier;

	UAnimNotifyState_MotionWarping(const FObjectInitializer& ObjectInitializer);

	/** Called from the MotionWarpingComp when this notify becomes relevant. See: UMotionWarpingComponent::Update */
	void OnBecomeRelevant(UMotionWarpingComponent* MotionWarpingComp, const UAnimSequenceBase* Animation, float StartTime, float EndTime) const;

	/** Creates a root motion modifier from the config class defined in the notify */
	UFUNCTION(BlueprintNativeEvent, Category = "Motion Warping")
	URootMotionModifier* AddRootMotionModifier(UMotionWarpingComponent* MotionWarpingComp, const UAnimSequenceBase* Animation, float StartTime, float EndTime) const;

	UFUNCTION()
	void OnRootMotionModifierActivate(UMotionWarpingComponent* MotionWarpingComp, URootMotionModifier* Modifier);

	UFUNCTION()
	void OnRootMotionModifierUpdate(UMotionWarpingComponent* MotionWarpingComp, URootMotionModifier* Modifier);

	UFUNCTION()
	void OnRootMotionModifierDeactivate(UMotionWarpingComponent* MotionWarpingComp, URootMotionModifier* Modifier);

	UFUNCTION(BlueprintImplementableEvent, Category = "Motion Warping")
	void OnWarpBegin(UMotionWarpingComponent* MotionWarpingComp, URootMotionModifier* Modifier) const;

	UFUNCTION(BlueprintImplementableEvent, Category = "Motion Warping")
	void OnWarpUpdate(UMotionWarpingComponent* MotionWarpingComp, URootMotionModifier* Modifier) const;

	UFUNCTION(BlueprintImplementableEvent, Category = "Motion Warping")
	void OnWarpEnd(UMotionWarpingComponent* MotionWarpingComp, URootMotionModifier* Modifier) const;

#if WITH_EDITOR
	virtual void ValidateAssociatedAssets() override;
#endif
};
C++

Note that it does not use the animation state notify callback like the following, the reason why should be writen in comments in the later version of the engine. It contains a URootMotionModifier, which actually implement the motio warping.

C++
ENGINE_API virtual void NotifyBegin(USkeletalMeshComponent * MeshComp, UAnimSequenceBase * Animation, float TotalDuration, const FAnimNotifyEventReference& EventReference);
ENGINE_API virtual void NotifyTick(USkeletalMeshComponent * MeshComp, UAnimSequenceBase * Animation, float FrameDeltaTime, const FAnimNotifyEventReference& EventReference);
ENGINE_API virtual void NotifyEnd(USkeletalMeshComponent * MeshComp, UAnimSequenceBase * Animation, const FAnimNotifyEventReference& EventReference);
C++

So let’s follow the calling stack, We begin with the following:

C++
FTransform UMotionWarpingComponent::ProcessRootMotionPreConvertToWorld(const FTransform& InRootMotion, UCharacterMovementComponent* CharacterMovementComponent, float DeltaSeconds)
{
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
	if (FMotionWarpingCVars::CVarMotionWarpingDisable.GetValueOnGameThread() > 0)
	{
		return InRootMotion;
	}
#endif

	// Check for warping windows and update modifier states
	Update(DeltaSeconds);

	FTransform FinalRootMotion = InRootMotion;

	// Apply Local Space Modifiers
	for (URootMotionModifier* Modifier : Modifiers)
	{
		if (Modifier->GetState() == ERootMotionModifierState::Active)
		{
			FinalRootMotion = Modifier->ProcessRootMotion(FinalRootMotion, DeltaSeconds);
		}
	}
C++

It calling the Update method of the component, which is a large methods, it’s not clear to paste all codes here, in short, the mission of the method is as followed:

  • Update a context, which contains what animation sequence the character is playing, what is the weight of the animation, the current position(frame time of the animation sequence) and previous tick postion.
  • Check that if current position falls into a motion warping window(period that contained by the animation state notify), if there is a new motion warping window, enable the modifier attached to the animation state notify, call the OnBecomeRelevant method of the animatio state notify.
  • Call the interval update method of the URootMotionModifier. this method update the state of the modifier, what’s more, in its child class, it also calculate the target transform of the root bone.
  • Remove the modifiers that is not used now.
C++
void URootMotionModifier::Update(const FMotionWarpingUpdateContext& Context)
{
	const ACharacter* CharacterOwner = GetCharacterOwner();
	if (CharacterOwner == nullptr)
	{
		return;
	}

	// Mark for removal if our animation is not relevant anymore
	if (!Context.Animation.IsValid() || Context.Animation.Get() != Animation)
	{
		UE_LOG(LogMotionWarping, Verbose, TEXT("MotionWarping: Marking RootMotionModifier for removal. Reason: Animation is not valid. Char: %s Current Animation: %s. Window: Animation: %s [%f %f] [%f %f]"),
			*GetNameSafe(CharacterOwner), *GetNameSafe(Context.Animation.Get()), *GetNameSafe(Animation.Get()), StartTime, EndTime, PreviousPosition, CurrentPosition);

		SetState(ERootMotionModifierState::MarkedForRemoval);
		return;
	}

	// Update playback times and weight
	PreviousPosition = Context.PreviousPosition;
	CurrentPosition = Context.CurrentPosition;
	Weight = Context.Weight;

	// Mark for removal if the animation already passed the warping window
	if (PreviousPosition >= EndTime)
	{
		UE_LOG(LogMotionWarping, Verbose, TEXT("MotionWarping: Marking RootMotionModifier for removal. Reason: Window has ended. Char: %s Animation: %s [%f %f] [%f %f]"),
			*GetNameSafe(CharacterOwner), *GetNameSafe(Animation.Get()), StartTime, EndTime, PreviousPosition, CurrentPosition);

		SetState(ERootMotionModifierState::MarkedForRemoval);
		return;
	}

	// Mark for removal if we jumped to a position outside the warping window
	if (State == ERootMotionModifierState::Active && PreviousPosition < EndTime && (CurrentPosition > EndTime || CurrentPosition < StartTime))
	{
		const float ExpectedDelta = Context.DeltaSeconds * Context.PlayRate;
		const float ActualDelta = CurrentPosition - PreviousPosition;
		if (!FMath::IsNearlyZero(FMath::Abs(ActualDelta - ExpectedDelta), KINDA_SMALL_NUMBER))
		{
			UE_LOG(LogMotionWarping, Verbose, TEXT("MotionWarping: Marking RootMotionModifier for removal. Reason: CurrentPosition manually changed. PrevPos: %f CurrPos: %f DeltaTime: %f ExpectedDelta: %f ActualDelta: %f"),
				PreviousPosition, CurrentPosition, Context.DeltaSeconds, ExpectedDelta, ActualDelta);

			SetState(ERootMotionModifierState::MarkedForRemoval);
			return;
		}
	}

	// Check if we are inside the warping window
	if (PreviousPosition >= StartTime && PreviousPosition < EndTime)
	{
		// If we were waiting, switch to active
		if (GetState() == ERootMotionModifierState::Waiting)
		{
			SetState(ERootMotionModifierState::Active);
		}
	}

	if (State == ERootMotionModifierState::Active)
	{
		if (UMotionWarpingComponent* OwnerComp = GetOwnerComponent())
		{
			OnUpdateDelegate.ExecuteIfBound(OwnerComp, this);
		}
	}
}
C++

When a root motion modifier is added to a motion warping component, it will register its callbacks:

C++
void UAnimNotifyState_MotionWarping::OnBecomeRelevant(UMotionWarpingComponent* MotionWarpingComp, const UAnimSequenceBase* Animation, float StartTime, float EndTime) const
{
	URootMotionModifier* RootMotionModifierNew = AddRootMotionModifier(MotionWarpingComp, Animation, StartTime, EndTime);

	if (RootMotionModifierNew)
	{
		if (!RootMotionModifierNew->OnActivateDelegate.IsBound())
		{
			RootMotionModifierNew->OnActivateDelegate.BindDynamic(this, &UAnimNotifyState_MotionWarping::OnRootMotionModifierActivate);
		}

		if (!RootMotionModifierNew->OnUpdateDelegate.IsBound())
		{
			RootMotionModifierNew->OnUpdateDelegate.BindDynamic(this, &UAnimNotifyState_MotionWarping::OnRootMotionModifierUpdate);
		}

		if (!RootMotionModifierNew->OnDeactivateDelegate.IsBound())
		{
			RootMotionModifierNew->OnDeactivateDelegate.BindDynamic(this, &UAnimNotifyState_MotionWarping::OnRootMotionModifierDeactivate);
		}
	}
}
C++

After update method, the component will call the ProcessRootMotion method to modify the result of the value evaluated by the root motion data, the simplest subclass of URootMotionModifier_SimpleWarp, it is deprecated in UE5.3 but it is easy for us to learn and match with out mathemetical model, in which the ProcessRootMotion is implemented as:

C++
FTransform UDEPRECATED_RootMotionModifier_SimpleWarp::ProcessRootMotion(const FTransform& InRootMotion, float DeltaSeconds)
{
	const ACharacter* CharacterOwner = GetCharacterOwner();
	if (CharacterOwner == nullptr)
	{
		return InRootMotion;
	}

	const FTransform& CharacterTransform = CharacterOwner->GetActorTransform();

	FTransform FinalRootMotion = InRootMotion;

	const FTransform RootMotionTotal = UMotionWarpingUtilities::ExtractRootMotionFromAnimation(Animation.Get(), PreviousPosition, EndTime);

	if (bWarpTranslation)
	{
		FVector DeltaTranslation = InRootMotion.GetTranslation();

		const FTransform RootMotionDelta = UMotionWarpingUtilities::ExtractRootMotionFromAnimation(Animation.Get(), PreviousPosition, FMath::Min(CurrentPosition, EndTime));

		const float HorizontalDelta = RootMotionDelta.GetTranslation().Size2D();
		const float HorizontalTarget = FVector::Dist2D(CharacterTransform.GetLocation(), GetTargetLocation());
		const float HorizontalOriginal = RootMotionTotal.GetTranslation().Size2D();
		const float HorizontalTranslationWarped = !FMath::IsNearlyZero(HorizontalOriginal) ? ((HorizontalDelta * HorizontalTarget) / HorizontalOriginal) : 0.f;

		const FTransform MeshRelativeTransform = FTransform(CharacterOwner->GetBaseRotationOffset(), CharacterOwner->GetBaseTranslationOffset());
		const FTransform MeshTransform = MeshRelativeTransform * CharacterOwner->GetActorTransform();
		DeltaTranslation = MeshTransform.InverseTransformPositionNoScale(GetTargetLocation()).GetSafeNormal2D() * HorizontalTranslationWarped;

		if (!bIgnoreZAxis)
		{
			const float CapsuleHalfHeight = CharacterOwner->GetCapsuleComponent()->GetScaledCapsuleHalfHeight();
			const FVector CapsuleBottomLocation = (CharacterOwner->GetActorLocation() - FVector(0.f, 0.f, CapsuleHalfHeight));
			const float VerticalDelta = RootMotionDelta.GetTranslation().Z;
			const float VerticalTarget = GetTargetLocation().Z - CapsuleBottomLocation.Z;
			const float VerticalOriginal = RootMotionTotal.GetTranslation().Z;
			const float VerticalTranslationWarped = !FMath::IsNearlyZero(VerticalOriginal) ? ((VerticalDelta * VerticalTarget) / VerticalOriginal) : 0.f;

			DeltaTranslation.Z = VerticalTranslationWarped;
		}
		else
		{
			DeltaTranslation.Z = InRootMotion.GetTranslation().Z;
		}

		FinalRootMotion.SetTranslation(DeltaTranslation);
	}

	if (bWarpRotation)
	{
		const FQuat WarpedRotation = WarpRotation(InRootMotion, RootMotionTotal, DeltaSeconds);
		FinalRootMotion.SetRotation(WarpedRotation);
	}

	// Debug
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
	const int32 DebugLevel = FMotionWarpingCVars::CVarMotionWarpingDebug.GetValueOnGameThread();
	if (DebugLevel == 1 || DebugLevel == 3)
	{
		PrintLog(TEXT("SimpleWarp"), InRootMotion, FinalRootMotion);
	}

	if (DebugLevel == 2 || DebugLevel == 3)
	{
		const float DrawDebugDuration = FMotionWarpingCVars::CVarMotionWarpingDrawDebugDuration.GetValueOnGameThread();
		DrawDebugCoordinateSystem(CharacterOwner->GetWorld(), GetTargetLocation(), GetTargetRotator(), 50.f, false, DrawDebugDuration, 0, 1.f);
	}
#endif

	return FinalRootMotion;
}
C++

It divide the movement into hotizontal and vertical, but it still matches out methmetical model discussed below.


References

By JiahaoLi

Hypergryph - Game Programmer 2023 - Now Shandong University - Bachelor 2019-2023

6 thoughts on “Motion Warping in Unreal Engine : Modify the Root Motion Data”
  1. I was just seeking this info for a while. After 6 hours of continuous Googleing, finally I got it in your site. I wonder what’s the lack of Google strategy that do not rank this type of informative websites in top of the list. Usually the top web sites are full of garbage.

Leave a Reply

Your email address will not be published. Required fields are marked *