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?

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.

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.

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:

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:

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.

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
:
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.
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.
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++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:
/** 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.
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:
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 theURootMotionModifier
. 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.
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:
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:
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.
And what would we do without your great idea
In my opinion, you are wrong. Let’s discuss. Email me at PM, we’ll talk.
I respectfully invite you to comment on and point out the incorrectness of my article, Please feel free to give your suggestion.
The article is excellent, the previous one is also very even
There is something in this. Thanks for the explanation.
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.