【Unity(C#)】流行りのメタバースでよく見るアバター着せ替えシステムを作ってみる

## はじめに
動画のようなアバターの着せ替えシステムについてどのように実装すれば実現可能か検証してみたのでメモします。

<iframe width="560" height="560" src="https://www.youtube.com/embed/m3aZSS8JLFk" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

【引用元】：[chaosru.com](https://www.chaosru.com/post/%E3%82%A2%E3%83%90%E3%82%BF%E3%83%BC%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0)

単純にモデルのアクティブの切り替えでもそれっぽいことはできそうですが、さすがに世の中のサービスがそういう実装で乗り切っているとは考えにくいので、動的に生成して着せ替えたオブジェクトは破棄するような実装で試みてみます。

## デモ
以下のようにUnityChanの衣装を動的に生成したうえでアニメーションさせてみました。
赤枠の部分を見ると着せ替え用オブジェクトが動的に生成されていることが見てとれます。
![AvatarSystemDemo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/38ef1628-b503-1cf0-cec2-135db97f9f81.gif)

利用したモデル：[UnityChan](https://unity-chan.com/)

## 考え方
考え方としては衣装の3Dモデルにもボーンを持たせ、アバター本体と組み合わせるような処理を行うイメージです。
以下画像がイメージしやすいと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/b7f5f037-3e74-755a-1a79-ce880561fff8.png)
【引用元】：[【cocone TECH TALK VOL.5・後編】AvatarMakerを支える技術【イベントレポート】](https://engineering.cocone.io/2022/04/28/technology_behind_avatarmaker/)

`アバター本体と組み合わせる`と表現しましたが、実際には衣装の3DモデルのSkinMeshRendererのBoneをアバター本体のBoneに設定しなおすような処理を行います。

## サンプルコード
以下が実際のサンプルです。

```CS
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// アバターの衣装データを置き換えるサンプル
/// </summary>
public class AvatarSystemSample : MonoBehaviour
{
    /// <summary>
    /// BoneのRootとなるオブジェクト。
    /// rootBoneとして認識されるBoneはConfigureAvatar設定でHipsに登録したBoneとなるので注意。
    /// </summary>
    [SerializeField] private GameObject _rootBone;
    [SerializeField] private List<ItemChangeDataSet> _itemChangeDataSetList;

    private Dictionary<string, Transform> _bodyBones;
    private GameObject _currentItem;

    private string RootBoneName => _rootBone.name;
    
    [Serializable]
    private class ItemChangeDataSet
    {
        [SerializeField] public Button ItemChangeButton;
        [SerializeField] public GameObject Item;
    }

    private void Start()
    {
        InitializeBoneDictionary();
        InitializeItemChangeButton();
    }

    /// <summary>
    /// ボーンの初期化
    /// </summary>
    private void InitializeBoneDictionary()
    {
        _bodyBones = new Dictionary<string, Transform>();
        var bones = GetAll(_rootBone);
        _bodyBones.Add(_rootBone.name, _rootBone.transform);
        foreach (var bone in bones)
        {
            _bodyBones.Add(bone.name, bone.transform);
        }
    }

    /// <summary>
    /// ボタンの初期化処理
    /// </summary>
    private void InitializeItemChangeButton()
    {
        foreach (var dataSet in _itemChangeDataSetList)
        {
            dataSet.ItemChangeButton.onClick.AddListener(() => ItemChange(dataSet.Item));
        }
    }
        
    
    /// <summary>
    /// アイテム置き換え処理
    /// </summary>
    /// <param name="item">置き換える予定のアイテム</param>
    private void ItemChange(GameObject item)
    {
        //既存のアイテムがあれば削除
        if (_currentItem != null)
        {
            Destroy(_currentItem);
        }
            
        //Item生成処理
        var itemGameObject = Instantiate(item);
        _currentItem = itemGameObject;
            
        //ボーン情報書き換え処理
        var skinMeshRenderer = itemGameObject.GetComponent<ItemData>().ItemSkinnedMeshRenderer;
        ReplaceSameNameBones(skinMeshRenderer);
            
        //素体とアイテムの原点をそろえて作っていればこの処理でぴったり合う
        itemGameObject.transform.SetParent(_rootBone.transform, false);
    }

    /// <summary>
    /// 対象のSkinMeshRendererに設定されたBoneを置き換える。
    /// "素体のBoneと同名のBone"を対象のSkinMeshRendererの新しいBoneとして設定をする。
    /// </summary>
    /// <param name="itemRenderer">対象となるSkinMeshRenderer</param>
    private void ReplaceSameNameBones(SkinnedMeshRenderer itemRenderer)
    {
        var equipmentBones = itemRenderer.bones.ToArray();

        for (var i = 0; i < equipmentBones.Length; i++)
        {
            var equipmentBone = equipmentBones[i];
            if (equipmentBone == null) continue;
            
            var equipmentBoneName = equipmentBone.name;

            if (_bodyBones.TryGetValue(equipmentBoneName, out var sameNameBodyBone))
            {
                equipmentBones[i] = sameNameBodyBone;
            }
        }

        itemRenderer.bones = equipmentBones;
        itemRenderer.rootBone = _bodyBones[RootBoneName]; 
    }

    /// <summary>
    /// 自信を含む子階層の全てのオブジェクトのリストを返す
    /// </summary>
    /// <param name="target">対象となるオブジェクト</param>
    /// <returns>自信を含む子階層の全てのオブジェクトのList</returns>
    private List<GameObject> GetAll(GameObject target)
    {
        var allChildren = new List<GameObject>();
        GetChildren(target, ref allChildren);
        return allChildren;
    }

    /// <summary>
    /// 子要素を取得してリストに追加
    /// </summary>
    /// <param name="target">対象となるオブジェクト</param>
    /// <param name="allChildren">子階層のオブジェクトリスト</param>
    private void GetChildren(GameObject target, ref List<GameObject> allChildren)
    {
        var children = target.GetComponentInChildren<Transform>();
        
        //子要素がいなければ終了
        if (children.childCount == 0)
        {
            return;
        }

        foreach (Transform child in children)
        {
            allChildren.Add(child.gameObject);
            GetChildren(child.gameObject, ref allChildren);
        }
    }
}
```

`ReplaceSameNameBones`がSkinMeshRendererのBoneをアバター本体のBoneに設定しなおす処理を担っています。

## Humanoidについて
MixamoでDLしてきたFBXやUnityChanがHumanoidとして動作する原理がわからなかったので少し調べました。
Unity対応のヒューマノイドアバターをインポートすると、自動的に以下のようなアバター設定を行ってくれます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/1932623a-1921-e5f1-3eb1-bfec69930214.png)

どうやら、以下の公式ドキュメントの言及から察するに、Boneの名前と階層構造で自動判定しているようです。

>ジョイント/ボーンの階層は、作成しているキャラクターの自然な構造に従う必要があります。腕と足がペアになっている場合、一貫した名前の命名規則に従います (例えば、左腕の場合は “arm_L”、右腕の場合は “arm_R”)。ヒエラルキーに使用可能な構造は以下のとおりです。

```
* HIPS - spine - chest - shoulders - arm - forearm - hand
* HIPS - spine - chest - neck - head
* HIPS - UpLeg - Leg - foot - toe - toe_end
```

【引用元】：[ヒューマノイドアセットの準備](https://docs.unity3d.com/ja/2018.4/Manual/UsingHumanoidChars.html)

ただ、細かい判定方法については資料を見つけられなかったので、
左右の違いや小文字大文字などの判定条件については結局よくわかりませんでした。

解決策として、BlenderにVRM対応アバターのRigを簡単に追加できるアドオンがあるので、
そのアドオンの命名規則及び階層に準拠してアバターを作るとUnityでHumanoidアバターの設定がうまくいきました。

【参考リンク】：[VRM Add-on for Blender](https://vrm-addon-for-blender.info/ja/)

以下は実際にBlender上で生成されたボーン情報です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/7b90943e-a738-6170-cd88-4239713b36f6.png)

## rootBoneについて

以下の個所でrootBoneの再設定を行っています。
```CS:AvatarSystemSample.cs
itemRenderer.rootBone = _bodyBones[RootBoneName]; 
```

これはバウンディングボックスがrootBoneによって決まるからです。
実際に誤った設定を行った場合の挙動を例とともに挙げます。

まず前提として、rootBoneはConfigureAvatarでHipに設定したBoneとなります。
(この前提は推測なので別の条件をご存じの方がいたらご指摘ください)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/22566d6e-35ee-f290-987b-bc280099b52b.png)

そこで今回はHipsではなく、その1つ上の階層をrootBoneに設定して実験してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/91c85dc7-fe2a-59fc-ee28-c9dbe979d261.png)

以下のようにバウンディングボックス(白い枠線)がずれて表示されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/026fc9a7-8de9-a420-3b4c-9a029e0b7cfc.png)

カメラを動かしてみると、画角によって服が表示されなくなる現象が発生しました。
![ASBoundingBoxSample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/58b60798-9d70-2061-0a25-405a6585d072.gif)

これは描画の可否について、バウンディングボックスがカメラの画角に入っているかどうかで判定しているからです。

HipsをrootBoneにすることでバウンディングボックスがそれっぽい位置に表示され、描画がおかしくなる現象を回避できるようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/f10491ec-02b7-6625-d2ca-e258fcda97a6.png)



## 参考リンク
[ボーンの差し替えによる着せ替え](https://chiepomme.gitbook.io/chiepomme/tips/other/dressing-up-by-bone-replacing)
[【Unity】動的にキャラクターの衣装を差し替える](https://tsubakit1.hateblo.jp/entry/2015/10/23/234955)
[ヒューマノイドアバター](https://docs.unity3d.com/ja/2021.3/Manual/AvatarCreationandSetup.html)
[【cocone TECH TALK VOL.5・後編】AvatarMakerを支える技術【イベントレポート】](https://engineering.cocone.io/2022/04/28/technology_behind_avatarmaker/)
[Unityで「装備品によって見た目が変わる」を実現する（あるいはコスチューム変更）](https://qiita.com/kaiware007/items/4c7a4b9862fae84d4023)
[BlenderからUnityのHumanoid互換でfbxをエクスポートする](https://qiita.com/ousttrue/items/aead1c943855561b62e7)
[全ての子要素を取得する(子要素の子要素の子要素の‥)](https://kazuooooo.hatenablog.com/entry/2015/08/07/010938)