【UE5.4】VRM4Uで読み込んだアバターの関節の角度を直接指定して動かす際の注意点

# はじめに
本記事では、はるべえ氏（[@ruyo_h](https://x.com/ruyo_h)）が公開しているVRM4Uプラグインを用いて、VRM形式のアバターをUE5.4へ読み込み、C++からアバターの関節を直接制御する際に自分がハマった注意点と解決策について共有します。
ポイントはローカル回転を実現するために`PoseableMeshComponent`を継承した自作クラスを作成することです。

:::note warn
当方Unityは数年間触っているものの、UEは今回初めて触ったためもっと楽なやり方があるかもしれないです。
ご存じの方はコメント欄で教えていただければと思います。
:::

# 実践
## 準備
Unreal Engineでゲーム→ブランクを選択し、C++を選んでプロジェクトを作成しておきます。
プロジェクトが立ち上がったら新規レベルをBasicで作成しておきます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3807732/201467a4-32b9-df79-fbf4-63dfa02d7550.png" width=70%>

### VRM4Uの導入
[公式のセットアップページ](https://ruyo.github.io/VRM4U/01_quick-start/)を見ながら進めれば特に問題はないはずです。

### VRMモデルのダウンロードと追加
今回はVRoid Hubから[AvatarSample_D](https://hub.vroid.com/characters/675572020956181239/models/4479743608263344465)をお借りします
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3807732/603f2321-480c-0cb6-c9bb-8c301a97902d.png" width=70%>
⇒ Pixiv IDでログイン後ダウンロード

ダウンロードしてきたvrmファイルをUEのコンテンツブラウザ上にドラッグ&ドロップします。
ポップアップが表示されたらimportをクリック
※この時、ドロップしたフォルダに各種ファイルが展開されるため、あらかじめフォルダを分けておくことをお勧めします。

以下のようにスケルタルメッシュやスケルトンといったファイルが展開されていれば導入成功です

　![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3807732/48432826-62ab-dcec-b21a-17f71e64baa3.png)
SK_○○の部分やSKEL_○○の部分などはファイル名がそのまま適用されるため、vroid.vrmやavatar.vrmなどわかりやすい名前にリネームしておくと良いです。

試しにプレイ画面上にSkeletalMeshを配置してみます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3807732/e5048bb7-3cf9-cce9-a54d-cff1538b53cf.png" width=45%> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3807732/456a4ef0-222e-a96b-bf6e-7811b6f7f82e.png" width=45%>
右側はMToonAttachActorを適用した後のモデルです。輪郭線がはっきりとし、肌の色味も自然ですね。

## 実装
### PoseableMeshComponentの場合
下記の記事のように`SkeletalMeshComponent`にはボーンを取得して`Transform`を変更する関数が実装されていないため、C++から関節を動かしたい場合は`PoseableMeshComponent`を使用します。

https://qiita.com/unknown_ds/items/9bc03ae3786ce1d18b52

まずは`PoseableMeshComponent`をRootObjectに持つようなアクターをC++で作成します。今回はコンストラクタでSK_vroidの`SkeletalMesh`を`PoseableMeshComponent`へコピーすることで生成します。

```cpp:PoseableMeshAvatar.cpp
// Sets default values
APoseableMeshAvatar::APoseableMeshAvatar()
{
	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	/* VRM4Uが生成したSkeletalMeshActorからSkeletalMeshをコピーする形で作成する */
	UPoseableMeshComponent *poseableAvatar = CreateDefaultSubobject<UPoseableMeshComponent>(TEXT("PoseableAvatar"));
	/* VRMファイルを読み込んだファイルパスを指定 */
	USkeletalMesh *skeletalMesh = LoadObject<USkeletalMesh>(nullptr, TEXT("/Game/StarterContent/Models/AvatarSample_D/SK_vroid.SK_vroid"));
	poseableAvatar->SetSkinnedAsset(skeletalMesh);
	RootComponent = poseableAvatar;
}
```
MToonAttachActorはTarget by Actorから生成したアクターを選択しておきます。
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3807732/beeb7bab-923a-d2c4-7f3d-81973a56e84c.png width=70%>

上記のオブジェクトを取得し、ボーンの名前と角度を指定して回転させるアクターを作成します。
```cpp:AvatarRotationSample.cpp
// Called when the game starts or when spawned
void AAvatarRotationSample::BeginPlay()
{
	Super::BeginPlay();

	/* PoseableMeshComponentのロード */
	LoadPoseableMeshComponentFromObject();

	/* わかりやすさのために上腕を180度回転、前腕は無回転とする */
	UpdateBoneRotation(FName("J_Bip_R_UpperArm"), FRotator(0, 0, 180));
	UpdateBoneRotation(FName("J_Bip_R_LowerArm"), FRotator(0, 0, 0));
}

/* AvatarのPoseableMeshComponentをロード */
void AAvatarRotationSample::LoadPoseableMeshComponentFromObject()
{
	/* Avatarオブジェクトの読み込み */
	TArray<AActor *> worldActors;
	UGameplayStatics::GetAllActorsOfClass(GetWorld(), AActor::StaticClass(), worldActors);

	for (auto actor : worldActors)
	{
		if (actor->GetActorLabel() == FName("PoseableMeshAvatar"))
		{
			/* PoseableMeshComponentの取得 */
			UPoseableMeshComponent *meshComponent = actor->FindComponentByClass<UPoseableMeshComponent>();
			if (meshComponent)
			{
				_PoseableAvatar = meshComponent;
			}
		}
	}
}

/* ボーンの名前と角度を指定して回転 */
void AAvatarRotationSample::UpdateBoneRotation(FName BoneName, FRotator Rotation)
{
	if (_PoseableAvatar)
	{
		_PoseableAvatar->SetBoneRotationByName(BoneName, Rotation, EBoneSpaces::ComponentSpace);
	}
}
```

上記のAvatarRotationSampleアクターを配置してPlayすると以下の写真のように腕が回転すると思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3807732/bafde222-a7a2-971b-912e-600030932929.png)
前腕が親ボーンである上腕の回転を無視して**絶対座標**における無回転になっていることがわかります。

### CustomPoseableMeshComponentの場合
次に以下のようにローカル回転のみを行うような`PoseableMeshComponent`を拡張した`CustomPoseableMeshComponent`クラスを作成します。
基本的には`PoseableMeshComponent`のコードを引用しています。
```cpp:CustomPoseableMeshComponent.h
#pragma once

#include "CoreMinimal.h"
#include "BoneContainer.h"
#include "Kismet/KismetSystemLibrary.h"
#include "Components/SkeletalMeshComponent.h"
#include "Components/PoseableMeshComponent.h"

/* PoseableMeshComponentを継承 */
class VRM_TEST_API UCustomPoseableMeshComponent : public UPoseableMeshComponent
{
public:
	FTransform GetBoneTransformByName(FName BoneName);
	void SetBoneLocalRotationByName(FName BoneName, FRotator InRotation);
	void SetBoneLocalTransformByName(FName BoneName, const FTransform &InTransform);
};
```

```cpp:CustomPoseableMeshComponent.cpp
#include "CustomPoseableMeshComponent.h"

template <class CompType>
FTransform GetBoneTransformByNameHelper(FName BoneName, FBoneContainer &RequiredBones, CompType *Component)
{
    int32 BoneIndex = Component->GetBoneIndex(BoneName);
    if (BoneIndex == INDEX_NONE)
    {
        FString Message = FString::Printf(TEXT("Invalid Bone Name '%s'"), *BoneName.ToString());
        FFrame::KismetExecutionMessage(*Message, ELogVerbosity::Warning);
        return FTransform();
    }
    return Component->BoneSpaceTransforms[BoneIndex];
}

FTransform UCustomPoseableMeshComponent::GetBoneTransformByName(FName BoneName)
{
    if (!GetSkinnedAsset() || !RequiredBones.IsValid())
    {
        return FTransform();
    }

    USkinnedMeshComponent *MPCPtr = LeaderPoseComponent.Get();
    if (MPCPtr)
    {
        if (USkeletalMeshComponent *SMC = Cast<USkeletalMeshComponent>(MPCPtr))
        {
            if (UAnimInstance *AnimInstance = SMC->GetAnimInstance())
            {
                return GetBoneTransformByNameHelper(BoneName, AnimInstance->GetRequiredBones(), SMC);
            }
            FString Message = FString::Printf(TEXT("Cannot return valid bone transform. Leader Pose Component has no anim instance"));
            FFrame::KismetExecutionMessage(*Message, ELogVerbosity::Warning);
            return FTransform();
        }
        FString Message = FString::Printf(TEXT("Cannot return valid bone transform. Leader Pose Component is not of type USkeletalMeshComponent"));
        FFrame::KismetExecutionMessage(*Message, ELogVerbosity::Warning);
        return FTransform();
    }
    return GetBoneTransformByNameHelper(BoneName, RequiredBones, this);
}

void UCustomPoseableMeshComponent::SetBoneLocalTransformByName(FName BoneName, const FTransform &InTransform)
{
    if (!GetSkinnedAsset() || !RequiredBones.IsValid())
    {
        return;
    }

    int32 BoneIndex = GetBoneIndex(BoneName);
    if (BoneIndex >= 0 && BoneIndex < BoneSpaceTransforms.Num())
    {
        BoneSpaceTransforms[BoneIndex] = InTransform;
        MarkRefreshTransformDirty();
    }
}

void UCustomPoseableMeshComponent::SetBoneLocalRotationByName(FName BoneName, FRotator InRotation)
{
    FTransform currentTransform = GetBoneTransformByName(BoneName);
    currentTransform.SetRotation(FQuat(InRotation));
    SetBoneLocalTransformByName(BoneName, currentTransform);
}
```

先ほどと同様に`CustomPoseableMeshComponent`をRootObjectに持つようなアクターをC++で作成し配置します。`PoseableMeshAvatar`との変更点は`poseableAvatar`の型のみです。

以下のように`CustomPoseableMeshAvatar`も回転させるようにAvatarRotationSampleを修正します。

```cpp:AvatarRotationSample
/* AvatarのPoseableMeshComponentを取得 */
void AAvatarRotationSample::LoadPoseableMeshComponentFromObject()
{
	/* Avatarオブジェクトの読み込み */
	TArray<AActor *> worldActors;
	UGameplayStatics::GetAllActorsOfClass(GetWorld(), AActor::StaticClass(), worldActors);

	for (auto actor : worldActors)
	{
		if (actor->GetActorLabel() == FName("PoseableMeshAvatar"))
		{
			/* PoseableMeshComponentの取得 */
			UPoseableMeshComponent *meshComponent = actor->FindComponentByClass<UPoseableMeshComponent>();
			if (meshComponent)
			{
				_PoseableAvatar = meshComponent;
			}
		}
		/* 追加 */
		else if (actor->GetActorLabel() == FName("CustomPoseableMeshAvatar"))
		{
			/* CustomPoseableMeshComponentの取得 */
			UCustomPoseableMeshComponent *meshComponent = actor->FindComponentByClass<UCustomPoseableMeshComponent>();
			if (meshComponent)
			{
				_CustomPoseableAvatar = meshComponent;
			}
		}
	}
}

/* ボーンの名前と角度を指定して回転 */
void AAvatarRotationSample::UpdateBoneRotation(FName BoneName, FRotator Rotation)
{
	if (_PoseableAvatar)
	{
		_PoseableAvatar->SetBoneRotationByName(BoneName, Rotation, EBoneSpaces::ComponentSpace);
		/* 追加 */
		_CustomPoseableAvatar->SetBoneLocalRotationByName(BoneName, Rotation);
	}
}
```
実行すると以下のように手のひらが上を向いた形になると思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3807732/74d6046c-c353-799c-753f-9cff1fab6e2c.png)
前腕が親ボーンの回転の影響を受けて**相対座標**における無回転になっていることが分かります。

先ほどの`PoseableMeshAvatar`と並べると以下のように手のひらの向きが真逆になっていることが分かります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3807732/bc7bac30-609a-d4af-835b-2e61390692a9.png)
左のアバターが`CustomPoseableMeshAvatar`、右のアバターが`PoseableMeshAvatar`です。

## 比較
既存の`PoseableMeshAvatar`は指定した回転を、**親ボーンの回転の影響を無視して**適用しています。
一方で、作成した`CustomPoseableMeshAvatar`は指定した回転を、**親ボーンの回転の影響を受けた状態で**適用しています。
一応`PoseableMeshComponent`単体でも親ボーンの回転を取得し、合成した状態で回転を適用することで`CustomPoseableMeshComponent`と同様の回転を実現できるはずです。（未検証）

一概にどちらが優れているということではないため、以下のように用途に応じて使い分けるのが良いかなと思います。
- 空間全体から見て指定したい角度を確実に適用したい場合は`PoseableMeshComponent`を使用する
- 指定したポーズを取らせるためにFK的に角度を適用したい場合は`CustomPoseableMeshComponent`を使用する

今回の自分の使用用途は後者の指定したポーズを取らせたいパターンだったため、上記のようなローカル回転が必要でした。

# まとめ
アバターの関節の角度を直接指定して動かしたい場合は、親ボーンの回転の影響を**受けて欲しい**回転なのか、**受けないで欲しい**回転なのか、を判断することが大事。
- **受けて欲しい**回転なら`PoseableMeshComponent`を使うだけでOK
- **受けないで欲しい**回転なら`CustomPoseableMeshComponent`のようにローカル回転を実現するための拡張クラスを実装することで実現できる

ちなみに、余談ですが`EBoneSpaces`の定義を見てみると`LocalSpace`がコメントアウトされてしまっていることがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3807732/de38f17c-ac65-6f9e-2357-01322251199a.png)
実装する必要がなかったため消されてしまったのでしょうか・・・

個人的にはアバターのような人型の場合、親のボーンの影響を受けた状態で指定する方が楽なのでローカル回転を使いたいです。

# 参考
- [UE4 ボーンのTransform制御について](https://qiita.com/unknown_ds/items/9bc03ae3786ce1d18b52)
- [Set bone rotations for Poseable mesh in local space](https://forums.unrealengine.com/t/set-bone-rotations-for-poseable-mesh-in-local-space/391296/5)