LoginSignup
27
33

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-02-09

はじめに

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

【引用元】:chaosru.com

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

デモ

以下のようにUnityChanの衣装を動的に生成したうえでアニメーションさせてみました。
赤枠の部分を見ると着せ替え用オブジェクトが動的に生成されていることが見てとれます。
AvatarSystemDemo.gif

利用したモデル:UnityChan

考え方

考え方としては衣装の3Dモデルにもボーンを持たせ、アバター本体と組み合わせるような処理を行うイメージです。
以下画像がイメージしやすいと思います。
image.png
【引用元】:【cocone TECH TALK VOL.5・後編】AvatarMakerを支える技術【イベントレポート】

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

サンプルコード

以下が実際のサンプルです。

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

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

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

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

【引用元】:ヒューマノイドアセットの準備

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

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

【参考リンク】:VRM Add-on for Blender

以下は実際にBlender上で生成されたボーン情報です。
image.png

rootBoneについて

以下の個所でrootBoneの再設定を行っています。

AvatarSystemSample.cs
itemRenderer.rootBone = _bodyBones[RootBoneName]; 

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

まず前提として、rootBoneはConfigureAvatarでHipに設定したBoneとなります。
(この前提は推測なので別の条件をご存じの方がいたらご指摘ください)
image.png

そこで今回はHipsではなく、その1つ上の階層をrootBoneに設定して実験してみます。
image.png

以下のようにバウンディングボックス(白い枠線)がずれて表示されました。
image.png

カメラを動かしてみると、画角によって服が表示されなくなる現象が発生しました。
ASBoundingBoxSample.gif

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

HipsをrootBoneにすることでバウンディングボックスがそれっぽい位置に表示され、描画がおかしくなる現象を回避できるようになります。
image.png

参考リンク

ボーンの差し替えによる着せ替え
【Unity】動的にキャラクターの衣装を差し替える
ヒューマノイドアバター
【cocone TECH TALK VOL.5・後編】AvatarMakerを支える技術【イベントレポート】
Unityで「装備品によって見た目が変わる」を実現する(あるいはコスチューム変更)
BlenderからUnityのHumanoid互換でfbxをエクスポートする
全ての子要素を取得する(子要素の子要素の子要素の‥)

27
33
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
33