はじめに
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に関連するクラスについて、
それぞれの役割や簡単な詳細について触れていきたいと思います
クラス名 | 役割 | |
1 | UCharacterMovementComponent | Characterの移動に関する処理を行うComponent ルートモーションの移動量についてここで処理される |
2 | UMotionWarpingComponent | MotionWarpingについての管理・実行を行うComponent |
3 | UAnimNotifyState_MotionWarping | MotionWarpingの区間を指定するAnimNotifyStateクラス |
4 | URootMotionModifier_SkewWarp | MotionWarpingによる移動量を計算するクラス |
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を実行させる際に参照することになるクラスとなっています。
![](http://ox.crdg.jp/tech/wp-content/uploads/2024/05/image-1024x371.png)
3.UAnimNotifyState_MotionWarping
MotionWarpingにて動きを調整する区間を示すために使うAnimNotifyStateとなっています。
ここで設定した区間で、MotionWarpingさせる座標まで動きを調整していきます。
![](http://ox.crdg.jp/tech/wp-content/uploads/2024/05/image-2.png)
4.URootMotionModifier_SkewWarp
MotionWarpingでの移動量を計算するクラスとなります。
もしMotionWarpingを使うときに独自の動きをさせたいという時には、ここのModifierクラスを個別に追加してもらうことになります。
Modifierについては、前述したUAnimNotifyState_MotionWarpingの設定で付け替えることが出来るので、
独自のクラスを作成した時には設定を忘れずにするようにしましょう。
![](http://ox.crdg.jp/tech/wp-content/uploads/2024/05/image-4.png)
MotionWarpingの処理フロー
では実際にMotionWarpingを実行した時に、
上記のComponent達がどう処理を行っているかフローを追ってみたいと思います。
以下処理フローを図に纏めてみた形になりますが、
番号順に処理が行われていると思ってもらえれば問題ありません。
![](http://ox.crdg.jp/tech/wp-content/uploads/2024/05/image-9.png)
![](http://ox.crdg.jp/tech/wp-content/uploads/2024/05/image-15.png)
以下にそれぞれの番号の箇所が何を行っているか簡単に記載していきます。
(コードについては一部を抜粋して載せています)
1.UMotionWarpingComponent::AddOrUpdateWarpTarget
void UMotionWarpingComponent::AddOrUpdateWarpTarget(const FMotionWarpingTarget& WarpTarget)
MotionWarpingでWarpする先の情報を受け取り登録する処理になっています。
![](http://ox.crdg.jp/tech/wp-content/uploads/2024/05/image-7.png)
WarpTargetName | Warp先を示す名称 ここにはユニークな名前を付ける必要あり |
WarpTargetLocation | ワープ先の座標 |
WarpTargetRotation | ワープ先の回転値 |
WarpTargetComponent | Warp先とする対象のComponent情報 |
WarpTargetBoneName | Warp先とするBone名 指定したBoneのTransform情報に対してWarpするようになる |
WarpTargetFollowComponent | WarpTargetComponentで渡した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が楽しくなっていくはずです!
皆さんも、気になる機能や新しい機能が追加された時には使い方だけではなく、
内部でどうやっているのかまで把握し色々な機能を使いこなして楽しんでいきましょう!
今回は以上となります。