【UE5】MotionWarpingの内部処理を見てみる

はじめに

OX ENGINEER STUDIOでクライアントエンジニアをしています、岩永です。
Unreal Engine 5(以下UE5)がリリースされてから2年ほど経ちましたが直近ではUE5.4がリリースされ、
更にUE5界隈が盛り上がっているのを感じています。

そんなUE5から追加した機能、「MotionWarping(モーションワープ)」について今回は触れてみたいと思います。

ただ、既に使い方については多くの有識者の方が記事にされていますので、
私の方ではMotionWarpingの内部処理はどんなことをやっているのか?というところにフォーカスを当てていきます。

本記事の目的

本記事の目的としては、MotionWarpingについてより理解を深めること、です。

具体的には、本記事を読むことでMotionWarpingについて独自に拡張できる、
不具合が起きてもすぐに対応が出来る、といったところまで知識を付けてもらうこと、です。

※本記事ではC++がメインの話になりますが、C++についての補足はしませんのでご承知おきください。

MotionWarpingとは?

MotionWarpingについて公式ドキュメントからの抜粋となりますが、簡単に説明すると以下になります

MotionWarpingは、キャラクターのルートモーションをターゲットに合わせてダイナミックに調整できる機能です。

Unreal Engine 公式ドキュメントより
https://dev.epicgames.com/documentation/ja-jp/unreal-engine/motion-warping-in-unreal-engine?application_version=5.3

ルートモーションとは、アニメーションに含まれているルートボーンの動きに沿ってキャラクターを動かすことを指します。
この時、ルートボーンの動きを調整しようとするとアニメーションデータそのものを調整する必要がありますが、MotionWarpingを使うことでアニメーションデータを調整することなく、BPなどの処理上から調整することが可能になります。
ルートモーション
https://dev.epicgames.com/documentation/ja-jp/unreal-engine/root-motion-in-unreal-engine?application_version=5.3

公式サンプル「古代の谷」にて瓦礫や障害物を乗り越える際に該当の機能が使われていますので、
そちらをDLして頂くと実際の動きや使い方を見ていただけるかと思います。
◆UEマーケットプレイス「古代の谷」
https://www.unrealengine.com/marketplace/ja/product/ancient-game-01

MotionWarpingの使い方

MotionWarpingの使い方についてですが、はじめに述べたように色々な方がブログなどで取り挙げています。

特にいつもお世話になっているhistoriaさんのブログが分かりやすく丁寧に記載されてますので、
そちらを見ていただくと触りやすいかなと思います。
◆historia:[UE5] Motion Warpingでアニメーションによる移動量を自由にコントロール!
https://historia.co.jp/archives/20586

今回は内部について見ていくので、MotionWarpingの使い方については触れずにいきますが、
この先MotionWarpingの使い方については知識として持っていることを前提として話していきますので、
historiaさんのブログなどを軽く見ていただいた後で、以下の内容を見てもらえると嬉しいです。

MotionWarpingの内部処理

ここから本題であるMotionWarpingの内部処理について触れていきたいと思います。
ざっくりとですが、以下の2点について見ていきます。

  • MotionWarpingに関連するクラスについて
  • MotionWarpingの処理フロー

MotionWarpingに関連するクラスについて

まずはMotionWarpingに関連するクラスについて、
それぞれの役割や簡単な詳細について触れていきたいと思います

クラス名役割
1UCharacterMovementComponentCharacterの移動に関する処理を行うComponent
ルートモーションの移動量についてここで処理される
2UMotionWarpingComponentMotionWarpingについての管理・実行を行うComponent
3UAnimNotifyState_MotionWarpingMotionWarpingの区間を指定するAnimNotifyStateクラス
4URootMotionModifier_SkewWarpMotionWarpingによる移動量を計算するクラス
1.UCharacterMovementComponent

ルートモーションを有効にする場合、必ずMovementComponentが必要となります。
処理について実際に内部を見てみるといくつか箇所があると思いますが、ひとまずクライアント動作時は UCharacterMovementComponent::PerformMovement の箇所を見ていただくと良いかと思います。


◆UCharacterMovementComponent 2630 ~ 2657行目

// Animation root motion overrides Velocity and currently doesn't allow any other root motion sources
if( HasAnimRootMotion() )
{
	// Convert to world space (animation root motion is always local)
	USkeletalMeshComponent * SkelMeshComp = CharacterOwner->GetMesh();
	if( SkelMeshComp )
	{
		// Convert Local Space Root Motion to world space. Do it right before used by physics to make sure we use up to date transforms, as translation is relative to rotation.
		RootMotionParams.Set( ConvertLocalRootMotionToWorld(RootMotionParams.GetRootMotionTransform(), DeltaSeconds) );
	}

	// Then turn root motion to velocity to be used by various physics modes.
	if( DeltaSeconds > 0.f )
	{
		AnimRootMotionVelocity = CalcAnimRootMotionVelocity(RootMotionParams.GetRootMotionTransform().GetTranslation(), DeltaSeconds, Velocity);
		Velocity = ConstrainAnimRootMotionVelocity(AnimRootMotionVelocity, Velocity);
		if (IsFalling())
		{
			Velocity += FVector(DecayingFormerBaseVelocity.X, DecayingFormerBaseVelocity.Y, 0.f);
		}
	}
	
	UE_LOG(LogRootMotion, Log,  TEXT("PerformMovement WorldSpaceRootMotion Translation: %s, Rotation: %s, Actor Facing: %s, Velocity: %s")
		, *RootMotionParams.GetRootMotionTransform().GetTranslation().ToCompactString()
		, *RootMotionParams.GetRootMotionTransform().GetRotation().Rotator().ToCompactString()
		, *CharacterOwner->GetActorForwardVector().ToCompactString()
		, *Velocity.ToCompactString()
		);
}
2.UMotionWarpingComponent

MotionWarpingの機能に必要なModifierや移動先となる座標情報を管理したり、
ルートモーションの移動量調整を行ったりするComponentになっています。

MotionWarpingの実行に必要な関数についてもMotionWarpingComponentにて実装されていますので、
Blueprintなど外部からMotionWarpingを実行させる際に参照することになるクラスとなっています。

3.UAnimNotifyState_MotionWarping

MotionWarpingにて動きを調整する区間を示すために使うAnimNotifyStateとなっています。
ここで設定した区間で、MotionWarpingさせる座標まで動きを調整していきます。

4.URootMotionModifier_SkewWarp

MotionWarpingでの移動量を計算するクラスとなります。
もしMotionWarpingを使うときに独自の動きをさせたいという時には、ここのModifierクラスを個別に追加してもらうことになります。

Modifierについては、前述したUAnimNotifyState_MotionWarpingの設定で付け替えることが出来るので、
独自のクラスを作成した時には設定を忘れずにするようにしましょう。

MotionWarpingの処理フロー

では実際にMotionWarpingを実行した時に、
上記のComponent達がどう処理を行っているかフローを追ってみたいと思います。

以下処理フローを図に纏めてみた形になりますが、
番号順に処理が行われていると思ってもらえれば問題ありません。

以下にそれぞれの番号の箇所が何を行っているか簡単に記載していきます。
(コードについては一部を抜粋して載せています)

1.UMotionWarpingComponent::AddOrUpdateWarpTarget
void UMotionWarpingComponent::AddOrUpdateWarpTarget(const FMotionWarpingTarget& WarpTarget)

WarpTargetNameWarp先を示す名称
ここにはユニークな名前を付ける必要あり
WarpTargetLocationワープ先の座標
WarpTargetRotationワープ先の回転値
WarpTargetComponentWarp先とする対象のComponent情報
WarpTargetBoneNameWarp先とするBone名
指定したBoneのTransform情報に対してWarpするようになる
WarpTargetFollowComponentWarpTargetComponentで渡したComponentに対してワープするかどうか
有効の場合、渡したComponentのTransform情報でワープ処理を行うようになる
if (WarpTarget.Name != NAME_None)
{
	// if we did not find the target, add it
    // ここから2の処理へ
	if (!FindAndUpdateWarpTarget(WarpTarget))
	{
		WarpTargets.Add(WarpTarget);
	}

	MARK_PROPERTY_DIRTY_FROM_NAME(UMotionWarpingComponent, WarpTargets, this);
}
2.UMotionWarpingComponent::FindAndUpdateWarpTarget
bool UMotionWarpingComponent::FindAndUpdateWarpTarget(const FMotionWarpingTarget& WarpTarget)

Warp先として登録しようとしている名前のものが既に存在しているかチェックを行い、
一致する情報があった場合は渡された情報で更新(上書き)を行っている処理になります。

for (int32 Idx = 0; Idx < WarpTargets.Num(); Idx++)
{
	if (WarpTargets[Idx].Name == WarpTarget.Name)
	{
		WarpTargets[Idx] = WarpTarget;
		return true;
	}
}
3.UCharacterMovementComponent::ConvertLocalRootMotionToWorld
FTransform UCharacterMovementComponent::ConvertLocalRootMotionToWorld(const FTransform& LocalRootMotionTransform, float DeltaSeconds)

CharacterMovementComponentによる移動処理のなかで、
RootMotionの移動量を取得しワールド座標に置き換える処理になります。

// ここから4の処理へ
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;
4.UMotionWarpingComponent::ProcessRootMotionPreConvertToWorld
FTransform UMotionWarpingComponent::ProcessRootMotionPreConvertToWorld(const FTransform& InRootMotion, UCharacterMovementComponent* CharacterMovementComponent, float DeltaSeconds)

MotionWarpingの更新やRootMotionModifierの更新を行う処理になります。
MotionWarpingによる移動量の計算などがここから始まる、と思ってもらえれば問題ないかと思います。

// Check for warping windows and update modifier states
// ここから5の処理へ
Update(DeltaSeconds);

FTransform FinalRootMotion = InRootMotion;

// Apply Local Space Modifiers
for (URootMotionModifier* Modifier : Modifiers)
{
	if (Modifier->GetState() == ERootMotionModifierState::Active)
	{
        // ここから9の処理へ
		FinalRootMotion = Modifier->ProcessRootMotion(FinalRootMotion, DeltaSeconds);
	}
}

こちらの関数については、UMotionWarpingComponent::InitizalizeComponent のところで
CharacterMovementComponentのProcessRootMotionPreConvertToWorldにてバインドされています。

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

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

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

主にRootMotionの再生状況を更新し、MotionWarpingのAnimNotifyState区間にいるかどうかをチェックしている処理になります。
区間内であればAnimNotifyStateのOnBecomeRelevant関数を呼び出し、Modifierが追加されるようにしMotionWarpingの移動量計算が行われるようにしています。

for (const FAnimNotifyEvent& NotifyEvent : Animation->Notifies)
{
	const UAnimNotifyState_MotionWarping* MotionWarpingNotify = NotifyEvent.NotifyStateClass ? Cast<UAnimNotifyState_MotionWarping>(NotifyEvent.NotifyStateClass) : nullptr;
	if (MotionWarpingNotify)
	{
		if(MotionWarpingNotify->RootMotionModifier == nullptr)
		{
			UE_LOG(LogMotionWarping, Warning, TEXT("MotionWarpingComponent::Update. A motion warping window in %s doesn't have a valid root motion modifier!"), *GetNameSafe(Animation));
			continue;
		}

		const float StartTime = FMath::Clamp(NotifyEvent.GetTriggerTime(), 0.f, Animation->GetPlayLength());
		const float EndTime = FMath::Clamp(NotifyEvent.GetEndTriggerTime(), 0.f, Animation->GetPlayLength());

		if (PreviousPosition >= StartTime && PreviousPosition < EndTime)
		{
			if (!ContainsModifier(Animation, StartTime, EndTime))
			{
                // ここから6の処理へ
				MotionWarpingNotify->OnBecomeRelevant(this, Animation, StartTime, EndTime);
			}
		}
	}
}

~~ 中略 ~~

// Update the state of all the modifiers
if (Modifiers.Num() > 0)
{
	for (URootMotionModifier* Modifier : Modifiers)
	{
        // ここから8の処理へ
		Modifier->Update(Context);
	}
   ~~ 一部省略 ~~
}
6.UAnimNotifyState_MotionWarping::OnBecomeRelevant
void UAnimNotifyState_MotionWarping::OnBecomeRelevant(UMotionWarpingComponent* MotionWarpingComp, const UAnimSequenceBase* Animation, float StartTime, float EndTime) const

MotionWarpingComponentから呼びだされる処理になります。
中身はMotionWarpingの移動量を計算するModifierを追加する処理を呼び出しています。

// ここから7の処理へ
URootMotionModifier* RootMotionModifierNew = AddRootMotionModifier(MotionWarpingComp, Animation, StartTime, EndTime);
7.UMotionWarpingComponent::AddModifierFromTemplate
URootMotionModifier* UMotionWarpingComponent::AddModifierFromTemplate(URootMotionModifier* Template, const UAnimSequenceBase* Animation, float StartTime, float EndTime)

OnBecomeRelevantから呼びだされてRootMotionModifierを作成し追加する処理になります。

if (ensureAlways(Template))
{
	FObjectDuplicationParameters Params(Template, this);
	URootMotionModifier* NewRootMotionModifier = CastChecked<URootMotionModifier>(StaticDuplicateObjectEx(Params));
	
	NewRootMotionModifier->Animation = Animation;
	NewRootMotionModifier->StartTime = StartTime;
	NewRootMotionModifier->EndTime = EndTime;

	AddModifier(NewRootMotionModifier);

	return NewRootMotionModifier;
}
8.URootMotionModifier_Warp::Update
void URootMotionModifier_Warp::Update(const FMotionWarpingUpdateContext& Context)

5.UMotionWarpingComponent::Update から呼ばれる、URootMotionModifierの更新処理になります。
主に移動先のTransform情報を更新しています。

// Get the warp point sent by the game
FTransform WarpPointTransformGame = WarpTargetPtr->GetTargetTrasform();

// Initialize our target transform (where the root should end at the end of the window) with the warp point sent by the game
FTransform TargetTransform = WarpPointTransformGame;
9.URootMotionModifier_SkewWarp::ProcessRootMotion
FTransform URootMotionModifier_SkewWarp::ProcessRootMotion(const FTransform& InRootMotion, float DeltaSeconds)

4.UMotionWarpingComponent::ProcessRootMotionPreConvertToWorld から呼ばれる、
MotionWarpingによる移動量や回転値を計算する処理になります。


ほぼMotionWarpingのメイン処理部分といっても過言ではなく、
ここで呼ばれているWarpTranslationやWarpRotation関数が実際に移動量や回転値の計算を行っている箇所であり、MotionWarpingの動きを作っているところになります。

const FVector WarpedTranslation = WarpTranslation(FTransform::Identity, DeltaTranslation, TotalTranslation, TargetLocation) + ExtraRootMotion.GetLocation();
FinalRootMotion.SetTranslation(WarpedTranslation);

const FQuat WarpedRotation = ExtraRootMotion.GetRotation() * WarpRotation(RootMotionDelta, RootMotionTotal, DeltaSeconds);
FinalRootMotion.SetRotation(WarpedRotation);

…これで処理フローは以上となります。

処理フローから見えてきたこと

ソースコードの中身を全て触れることは出来ませんでしたが、処理フローを追っかけてみるとMotionWarpingの動きを作っているのは9番目の「URootMotionModifier_SkewWarp」にて移動量や回転量の計算をしている箇所であることが分かります。

なので、MotionWarpingの動きを独自で実装したい・拡張したいという場合には、RootMotionModifierを継承した新たなクラスを作り、独自の処理を実装しAnimNotifyStateのMotionWarpingに設定すればいい、ということにも気づけるかと思います。

また、拡張対応だけでなく、MotionWarpingが上手く動かない、もしくはエラーが出てしまうといった不具合に遭遇し原因箇所を特定できていなかったとしても、処理フローの1番から順に見ていくことでいずれは特定出来るはずです。

ということは、拡張の方法と不具合対応について考えられるほど知識がついたことになるので、
つまりはMotionWarpingについて理解を深められた、と言っても問題ないかと思います。

さいごに

長くなってしまいましたが、如何でしたでしょうか?
後半はソースコードだらけで見づらかったかもしれませんが、ここまで見ていただけたらMotionWarpingについて理解を深めることが出来たかなと思います。

今回のように実装されている機能の内部処理を見ることで、色々と拡張することが出来るようになりますし、
処理をまねることで似たような機能を実装することも可能になります。
また、関連する問題が起きた時に対処がしやすくなることも大きいかなと思います。

UnrealEngineはソースが公開されているので、追加された機能がどうなっているのか自身で調べることができるため、機能を理解し新しく追加したり、拡張したりすることでより自分が思い描いていることを表現することが出来ます。

そうすることで、もっとUnrealEngineが楽しくなっていくはずです!

皆さんも、気になる機能や新しい機能が追加された時には使い方だけではなく、
内部でどうやっているのかまで把握し色々な機能を使いこなして楽しんでいきましょう!

今回は以上となります。

関連記事

  1. 技術ブログ開始のご挨拶

    2023-10-17

  2. 【UE5】Unreal Insightsを使ってみた

    2024-03-21

  3. 【UE5】Live Codingでビルドする際の注意点

    2023-12-01

  4. 【UE5】プレビューを停止すると「Assertion failed: bRegistered」とエラーメッセージが出てエディタが強制終了する現象

    2024-02-08

ABOUT

OX ENGINEER STUDIO

OX ENGINEER STUDIO並びにC&R CREATIVE STUDIOSの紹介サイトです。
本サイトでは、ゲーム開発や弊社スタジオに関する様々な情報を発信しています。

運営:
OX ENGEINEER STUDIO

所属:
C&R Creative Studios

カレンダー

2024年5月
 12345
6789101112
13141516171819
20212223242526
2728293031  
ページ上部へ戻る