Unreal Engine 勉強会 ~IKRigによるツタ登り中の実装例~

こんにちは。ゲーム事業部(東京)のエンジニアの笠松です。
今回もアドグローブのゲーム事業部内で定期的に開催しているUnreal Engine(以後、UE)勉強会について、自分が発表した内容をご紹介します。


目次


はじめに

「UE勉強会って何?」「どんなことをやっているの?」などの紹介については、以下の記事をご参照ください。
blog.adglobe.co.jp

前回では、UE5におけるIKの実装の基本を説明しました。
今回は、以前公開した、ツタ移動の処理にIKを追加で実装して、より自然な動きになるようにしてみようと思います。

なお、記事中のUEのバージョンは、5.0.2になります。


IKRigによるツタ登り中の実装例

下準備

IKを使って自然な動きにするために、最低限としてツタを登るアニメーションを用意します。
今回は、mixamoにある壁を登るモーションをダウンロードして、UnrealEngine のEditor内でリターゲットして使用しました。
※リターゲットについては、こちらの記事で説明しています。

モーションを再生した状態が以下になります。

モーションを再生するようにしたが、角度のついたツタは登れない

モーションの再生と移動自体は問題有りませんが、以前実装した移動先に障害物があるかどうかの判定によって、角度のついたツタに対しては登ることができない状態になってしまいます。
これを、IKを実装して解消しつつ、より自然な動きになるようにして行きます。

ツタに向かって手足のIKを有効にする

前提として、モーションを再生するようにしたため、移動先に障害物があるかどうかを判定するための手足の位置を固定位置から各Socketの位置に変更しておきます。
その上で、手足のIKを有効にしてツタに沿わせてみます。

今回のIKの実装は以下のように行いました。
※IKの基本的なフローや説明は前回を参考にしてもらうとして、異なる部分のみ説明します。
※処理の流れが分かる部分のみ記載しており、ヘッダーに定義している変数やら汎用的に定義した関数は省いているのでご留意下さい。

void AUE5_StudyCharacter_IKRig::UpdateIKRigGoals(float DeltaTime)
{
  // IKRigの各Goalの初期化処理
  TFunctionRef<void(FIKRigGoal&, FName)> ResetIKRigGoal = [this](FIKRigGoal& IKRigGoal, FName GoalName)
  {
     IKRigGoal.Name = GoalName;
     IKRigGoal.Position = FVector::ZeroVector;
     IKRigGoal.PositionAlpha = 0.0;
     IKRigGoal.Rotation = FRotator::ZeroRotator;
     IKRigGoal.RotationAlpha = 0.0;
  };

  // IKRigの各Goalの初期化
  ResetIKRigGoal(LeftFootIKRigGoal, TEXT("foot_l_Goal"));
  ResetIKRigGoal(RightFootIKRigGoal, TEXT("foot_r_Goal"));
  ResetIKRigGoal(LeftHandIKRigGoal, TEXT("hand_l_Goal"));
  ResetIKRigGoal(RightHandIKRigGoal, TEXT("hand_r_Goal"));

  // IKRigのGoalを更新(ツタ登り中の手足)
  UpdateIKRigGoalsForClimbingVineLimbs(DeltaTime);

  // IKRigの各IKにおけるGoalの反映
  IKRigComponent->SetIKRigGoal(LeftFootIKRigGoal);
  IKRigComponent->SetIKRigGoal(RightFootIKRigGoal);
  IKRigComponent->SetIKRigGoal(LeftHandIKRigGoal);
  IKRigComponent->SetIKRigGoal(RightHandIKRigGoal);
}

void AUE5_StudyCharacter_IKRig::UpdateIKRigGoalsForClimbingVineLimbs(float DeltaTime)
{
  // 両手両足のIKGoalの設定
  UpdateIKRigGoalForClimbingVineLimbs(LeftFootIKRigGoal, TEXT("foot_l"));
  UpdateIKRigGoalForClimbingVineLimbs(RightFootIKRigGoal, TEXT("foot_r"));
  UpdateIKRigGoalForClimbingVineLimbs(LeftHandIKRigGoal, TEXT("hand_l"));
  UpdateIKRigGoalForClimbingVineLimbs(RightHandIKRigGoal, TEXT("hand_r"));
}

void AUE5_StudyCharacter_IKRig::UpdateIKRigGoalForClimbingVineLimbs(FIKRigGoal& IKRigGoal, FName SocketName)
{
  // Hit確認する最大方向
  FVector MaxHitForwardVector = GetActorForwardVector() * GetCapsuleComponent()->GetScaledCapsuleRadius() + ClimbingVineHitLength;
  // Root位置
  FVector RootLocation = GetMesh()->GetSocketLocation(TEXT("Root"));

  // Socket上にあるツタ(Visibility)をTrace
  FHitResult HitResult;
  if (!TrySphereTraceIKTarget(TraceTypeQuery1, HitResult, SocketName, MaxHitForwardVector))
  {
    return;
  }

  // Root位置から離れている分だけ、ツタの位置に合わせる
  FVector TraceHitDirection = HitResult.Location - RootLocation;
  float Distance = FVector::DotProduct(GetActorForwardVector(), TraceHitDirection) + IKSphereTraceRadius - ClimbingVineKeepDistance;

  // Goalの軸(Goalは相対位置のため、SkeletalMeshの軸に合わせる)
  FVector GoalAxisVector = FVector::YAxisVector;
  IKRigGoal.Position = GoalAxisVector * Distance;
  IKRigGoal.PositionAlpha = 1.0f;

  // ツタの角度に合わせる(法線をMeshの座標系に変換し、必要な回転量を計算)
  FVector CorrectNormal = GetMesh()->GetComponentRotation().Quaternion().Inverse().RotateVector(HitResult.ImpactNormal);
  IKRigGoal.Rotation = FQuat::FindBetween(GoalAxisVector * -1, CorrectNormal).Rotator();
  IKRigGoal.RotationAlpha = 1.0f;
}

細かい数値以外で、処理として前回の足のIKと異なる部分は以下になります。

  • 下に向かってのレイトレース(GetActorUpVector() * -1)ではなく、ツタに向かってのレイトレース(GetActorForwardVector())であること
  • Meshの基準となる方向が上方向(FVector::ZAxisVector)ではなく、後ろ方向(FVector::YAxisVector)であること

上記コードを実行すると、以下のGIFのようになります。

手足のIKを実装することで、壁との衝突判定が発生せず移動できるようになった

IKにより手足のSocket位置がツタに沿うようになり、角度のついたツタでも問題なく移動できるようになりました。
ただし、IKで位置が変わっている部分は手足のみで、Actor自体の位置や角度はそのままのため、手足以外がツタの中に埋まってしまっています。

Actor自体をツタの角度に合わせる

次は、ツタに合わせてActor自体の位置や角度を調整して、Actor自体がツタに沿うように修正します。

ツタの角度に合わせる方法として、登っているツタの法線を取得してそれを使用する方法を試してみます。
まずは、単純にActor正面にあるツタの法線を取得する方法を実装すると、以下のGIFのようになります。

単純にActor正面にあるツタの法線を取得する方法(カクツキが目立つ)

シンプルな実装で対応可能ですが、カクついた感じになってしまい違和感が残ります。

それならばと、利用する法線を増やして平均を取ればいい感じになるのでは無いかと考えたため、試してみます。
手足それぞれの位置にあるツタの法線(全部で4つ)を取得して平均を利用する方法を実装すると、以下のGIFのようになります。

手足それぞれの位置にあるツタの法線(全部で4つ)を取得して平均を利用する方法(まだ少しカクつく)

レイトレースの回数を増やしただけですが、カクついた感じが減りはしました。
ただし、まだカクつきによる違和感が残っている状態です。(GIF画像だとわかりにくいですが、実際にはまだカクついている感じがあります。)

ここまで、ツタ自体の法線を利用する方法を試しましたが、法線をそのまま利用するには別途Lerpなどで補間する必要があったり、そもそもMeshの表面がガタついていたりする場合だったり、なども考慮するとカクつかないようにするには限界があるため、別の方法を検討します。

法線を求める別の方法として、一般的なものとして外積を計算する方法があるので、そちらを試してみます。
IKによって、手足の位置がツタの表面に沿っている状態のため、手足の位置を元にしたベクトルの外積から法線を求めることで、現在の位置における適切な法線が求められるはずです。

手足の位置を元にしたベクトルの外積から法線を求めて利用する方法を実装すると、以下のGIFのようになります。

手足の位置を元にしたベクトルの外積から法線を求めて利用する方法

外積を用いることで、補間処理を行わないでもスムーズな移動になり、Actor自体が現在の手足の位置に応じた適切な角度に変わるようになりました。


さいごに

以上、IKRigによるツタ登り中の実装例についてご紹介しました。
IKを実装することで、見た目の自然さだけでなく、手足の位置が適切な場所に移動されるため、その位置をもとにした調整の処理を実装する際に楽になることがあります。
最後まで読んでいただき、ありがとうございました。




アドグローブではゲームエンジニアを含め、さまざまなポジションで一緒に働く仲間を募集しています。
詳細については下記からご確認ください。みなさまからのご応募お待ちしております。

hrmos.co