Unreal Engine 勉強会 ~IKRigによるFootIKの実装例~

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


目次


はじめに

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

前回に続きUE5におけるIKの実装について、IKRigによるFootIKの実装例をご紹介します。

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


IKRigによるFootIKの実装例

前回の記事で作成したIKRigAssetにコード側から値を渡すために、UIKRigComponentを利用します。

UIKRigComponentは、ActorからABPに設定されているIKRig ノードにIK Goalの設定を渡すために用意されているActorComponentで、以下のように実装できます。

  1. IKとして設定したい位置や角度を算出
    ※基本的な位置取得の処理は、ThirdPersonTemplateに実装されているのControllRigでのFootIKとほぼ同じ
  2. 算出した値を元に、FIKRigGoal 構造体を設定
  3. 設定したFIKRigGoal 構造体をUIKRigComponent::SetIKRigGoalにて、反映

UIKRigComponentを利用するための準備

UIKRigComponentをコードで利用するためには、以下の設定が必要になります。

  • Moduleの追加
    • [プロジェクト名].Build.cs にIKRigを追加して、IKRigの各種クラスを使用できるようにする
  • 必要なヘッダーのInclude
    • IKRigDataTypes.h
      • FIKRigGoal 構造体を利用するために必要
    • ActorComponents/IKRigComponent.h
      • UIKRigComponentを利用するために必要

次から、実際にどのようにコードで実装するかの例をご説明します。

FootIKの処理

FootIKIKの処理は以下のコードで実装可能です。
※処理の流れが分かる部分のみ記載しており、ヘッダーに定義している変数やら汎用的に定義した関数は省いているのでご留意下さい。

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を更新(地面に向けての足)
  UpdateIKRigGoalsForFeetToGround(DeltaTime);

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

void AUE5_StudyCharacter_IKRig::UpdateIKRigGoalsForFeetToGround(float DeltaTime)
{
  // 両足のIKGoalの設定
  FHitResult LeftFootHitResult;
  UpdateIKRigGoalForFeetToGround(LeftFootHitResult, LeftFootIKRigGoal, TEXT("foot_l"));
  FHitResult RightFootHitResult;
  UpdateIKRigGoalForFeetToGround(RightFootHitResult, RightFootIKRigGoal, TEXT("foot_r"));

  // 足のIKで更新がある場合
  if (LeftFootHitResult.bBlockingHit || RightFootHitResult.bBlockingHit)
  {
     // 手のIKRigのGoalを算出(足の平均を設定)
     LeftHandIKRigGoal.Position.Z = (LeftFootIKRigGoal.Position.Z + RightFootIKRigGoal.Position.Z) / 2;
     LeftHandIKRigGoal.PositionAlpha = 1.0f;
     RightHandIKRigGoal.Position = LeftHandIKRigGoal.Position;
     RightHandIKRigGoal.PositionAlpha = LeftHandIKRigGoal.PositionAlpha;
  }
}

void AUE5_StudyCharacter_IKRig::UpdateIKRigGoalForFeetToGround(FHitResult& HitResult, FIKRigGoal& IKRigGoal, FName SocketName)
{
  // 地面方向(許容距離)
  FVector MaxDirection = GetActorUpVector() * -1 * FootIKTraceLengthMax * 2;
  // Root位置
  FVector RootLocation = GetMesh()->GetSocketLocation(TEXT("root"));

  // Socket上にある地面(Visibility)に向けてSphereTrace
  if (!TrySphereTraceIKTarget(TraceTypeQuery1, HitResult, SocketName, MaxDirection))
  {
     return;
  }

  // 地面の位置に合わせる
  IKRigGoal.Position.Z = HitResult.Location.Z - RootLocation.Z - IKSphereTraceRadius;
  IKRigGoal.PositionAlpha = 1.0f;

  // 地面の角度に合わせる
  // 補正したいMeshの回転方向
  FQuat MeshQuat = GetMesh()->GetComponentRotation().Quaternion().Inverse();
  FVector CorrectNormal = MeshQuat.RotateVector(HitResult.ImpactNormal);
  // Goalの軸(Goalは相対位置のため、SkeletalMeshの軸に合わせる)
  FVector GoalAxisVector = FVector::ZAxisVector;
  IKRigGoal.Rotation = FQuat::FindBetween(GoalAxisVector, CorrectNormal).Rotator();
  IKRigGoal.RotationAlpha = 1.0f;
}

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

左がIKRigでの実装例で、右がThirdPersonTemplateで実装されているControllRigでのIK

以下から、関数やステップ毎に何をやっているのかを説明していきます。

FootIKの処理補足(位置調整の設定値について)

この実装例では、位置調整の設定値は、Root位置からの差分としています。
これは IKRigGoal.PositionSpace がデフォルトで EIKRigGoalSpace::Additive になっているためです。
Space設定は、他にも EIKRigGoalSpace::WorldEIKRigGoalSpace::Component があり、これらでも適切な値を設定すれば実装は可能です。
ただ、個人的には、EIKRigGoalSpace::Additiveが分かりやすいですし、計算や再利用が楽というのがあります。(再利用の例でいうと、後述する手の位置調整のようなケースがあります)

  // 地面の位置に合わせる
  IKRigGoal.Position.Z = HitResult.Location.Z - RootLocation.Z - IKSphereTraceRadius;
  IKRigGoal.PositionAlpha = 1.0f;
FootIKの処理補足(手の位置調整について)

FootIKであるのに、手の位置調整も行っている理由として、手のIKGoalも設定しているので手の位置が固定されてしまっているためです。
前回の記事のこちらのGIFがわかりやすいかと思います。(足を下げたときに手の位置が固定されている)

  // 足のIKで更新がある場合
  if (LeftFootHitResult.bBlockingHit || RightFootHitResult.bBlockingHit)
  {
     // 手のIKRigのGoalを算出(足の平均を設定)
     LeftHandIKRigGoal.Position.Z = (LeftFootIKRigGoal.Position.Z + RightFootIKRigGoal.Position.Z) / 2;
     LeftHandIKRigGoal.PositionAlpha = 1.0f;
     RightHandIKRigGoal.Position = LeftHandIKRigGoal.Position;
     RightHandIKRigGoal.PositionAlpha = LeftHandIKRigGoal.PositionAlpha;
  }

実際に上記の手の位置調整コードがない場合、以下のGIFのようになります。
※手のIKGoalを設定していない場合、不要な処理になります。

左がIKRigでの実装例で、手の位置調整をしていない状態のため手の位置がおかしくなる

FootIKの処理補足(回転値の計算について)

この実装例では、位置調整だけでなく回転値も設定することで、足裏が地面に沿うようにもしています。(ThirdPersonTemplateに実装されているControllRigでは実装されていないもの)

  // 地面の角度に合わせる
  // 補正したいMeshの回転方向
  FQuat MeshQuat = GetMesh()->GetComponentRotation().Quaternion();
  FVector CorrectNormal = MeshQuat.Inverse().RotateVector(HitResult.ImpactNormal);
  // Goalの軸(Goalは相対位置のため、SkeletalMeshの軸に合わせる)
  FVector GoalAxisVector = FVector::ZAxisVector;
  IKRigGoal.Rotation = FQuat::FindBetween(GoalAxisVector, CorrectNormal).Rotator();
  IKRigGoal.RotationAlpha = 1.0f;

位置と同様に IKRigGoal.RotationSpace はデフォルトの EIKRigGoalSpace::Additive を利用しているため、設定する回転値は、メッシュのデフォルト法線からの変化量を設定する必要があり、以下のように計算することが出来ます。

  1. 変化量自体は、FQuat::FindBetween(v1, v2)で、v1からv2に回転させるためのFQuatを求めることができ、計算可能
  2. デフォルトの法線とは、Mesh座標系での法線である FVector::ZAxisVector
  3. HitResult.ImpactNormal は、ワールド座標系の法線のため、回転元の法線であるMesh座標系に変換する必要があり、Mesh座標系への変換は、Meshの逆Quaternionで変換可能

以下は足に注目した際のGIFです。

足裏が地面に沿っているのが確認できる


さいごに

以上、UE5におけるIKRigによるFootIKの実装例についてご紹介しました。
IKにおける位置調整は、ThirdPersonTemplateに実装されているのControllRigでも実装されていますので、一歩踏み込んで回転についても触れてみました。
次回更新予定の記事(Unreal Engine 勉強会 ~IKRigによるツタ登り中の実装例~)では、今回の計算式に加えて、FootIK以外のIKの利用例をご紹介します。

最後まで読んでいただき、ありがとうございました。


これまでのUnreal Engine勉強会の記事はこちらから。 blog.adglobe.co.jp

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