エンジニア

VRoidを動的に着せ替えてみた!

投稿日:

今回ブログを担当しますエンジニアの乾です、よろしくお願いします!
最近VRoidを触る機会があり、Unity上で動的に着せ替えしてみたいなぁと思い実際にやってみました。

準備

書き出したVRoidをBlenderなどで頭、身体、服(上半身)、服(下半身)で分割し、それぞれPrefab化ておきます。

Animatorを追加したオブジェクト直下にBoneを追加し、キャラクターの元となるオブジェクトをScene上に配置します。

キャラクターアニメーションはAnime Girl Idle Animations Freeを使わせていただいてます!
Unityのバージョンは2019.3.11f1です。

Script作成

下記の通りにCharacterContoroller.csというscriptを作成し、準備しておいたキャラクターの元となるオブジェクトに付けてください。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Linq;
public class CharacterContoroller : MonoBehaviour
{
    // パーツ用enum
    public enum PartsType
    {
        Head,       // 頭
        Body,       // 体
        Top,        // 服(上半身)
        Bottom,     // 服(下半身)
    }
    public bool _isChange = true;          // 変更フラグ
    public GameObject _skeleton;        // skeletonオブジェクト
    // prefab
    public GameObject[] _headPrefabs;
    public GameObject[] _bodyPrefabs;
    public GameObject[] _topPrefabs;
    public GameObject[] _bottomPrefabs;
    private Animator _animator;
    private Dictionary<string, Transform> _transformDic = new Dictionary<string, Transform>();
    private List<Transform> _skeletonBones = new List<Transform>();
    private List<GameObject> _partsList = new List<GameObject>();
    private Dictionary<PartsType, int> _partsIndexDic = new Dictionary<PartsType, int>();
    // Start is called before the first frame update
    void Start()
    {
        _animator = GetComponent<Animator>();
        _skeletonBones = _skeleton.GetComponentsInChildren<Transform>().ToList();
        foreach (PartsType type in Enum.GetValues(typeof(PartsType)))
        {
            _partsIndexDic.Add(type, 0);
        }
    }
    // Update is called once per frame
    void Update()
    {
        if(_isChange)
        {
            _isChange = false;
            // 一旦生成されているパーツを削除
            foreach(var obj in _partsList)
            {
                Destroy(obj);
            }
            _partsList.Clear();
            // 選択されているパーツPrefab取得
            List<GameObject> prefabs = new List<GameObject>();
            prefabs.Add(_headPrefabs[_partsIndexDic[PartsType.Head]]);
            prefabs.Add(_bodyPrefabs[_partsIndexDic[PartsType.Body]]);
            prefabs.Add(_topPrefabs[_partsIndexDic[PartsType.Top]]);
            prefabs.Add(_bottomPrefabs[_partsIndexDic[PartsType.Bottom]]);
            // パーツ生成
            foreach (var prefab in prefabs)
            {
                // prefab生成
                GameObject resourceObject = Instantiate(prefab);
                var skinnedMeshRenderers = resourceObject.GetComponentsInChildren<SkinnedMeshRenderer>();
                for (int i = 0; i < skinnedMeshRenderers.Length; i++)
                {
                    var smr = skinnedMeshRenderers[i];
                    //  メッシュ用にGameObjectを作成
                    GameObject newMeshObject = new GameObject();
                    newMeshObject.transform.SetParent(this.transform);
                    newMeshObject.name = smr.gameObject.name;
                    _partsList.Add(newMeshObject);
                    // SkinnedMeshRenderer生成
                    SkinnedMeshRenderer r = newMeshObject.AddComponent<SkinnedMeshRenderer>();
                    // Mesh情報を複製して設定
                    r.sharedMesh = Instantiate(smr.sharedMesh);
                    r.materials = smr.sharedMaterials;
                    r.bones = smr.bones;
                    r.sharedMesh.boneWeights = smr.sharedMesh.boneWeights;
                    r.localBounds = smr.localBounds;
                    r.rootBone = _skeleton.transform.GetChild(0);
                    r.transform.position = smr.transform.position;
                    r.transform.rotation = smr.transform.rotation;
                    r.transform.localScale = smr.transform.localScale;
                    r.receiveShadows = true;
                    r.quality = SkinQuality.Auto;
                    // 念の為boneを並び替え
                    Transform[] bones = r.bones;
                    Transform[] newBones = new Transform[bones.Length];
                    for (int k = 0; k < bones.Length; k++)
                    {
                        int index = _skeletonBones.FindIndex((Transform t) =>
                        {
                            return t.name == bones[k].name;
                        });
                        if (index != -1)
                        {
                            newBones[k] = _skeletonBones[index];
                        }
                    }
                    r.bones = newBones;
                }
                // 削除
                DestroyImmediate(resourceObject);
            }
            // Animator反映
            _animator.Rebind();
        }
    }
    /// <summary>
    /// パーツのindexを設定
    /// </summary>
    /// <param name="type"></param>
    /// <param name="index"></param>
    private void SetPartsIndex(PartsType type, int index)
    {
        _partsIndexDic[type] = index;
        _isChange = true;
    }
    public void SetHeadIndex(int index)
    {
        SetPartsIndex(PartsType.Head, index);
    }
    public void SetBodyIndex(int index)
    {
        SetPartsIndex(PartsType.Body, index);
    }
    public void SetTopIndex(int index)
    {
        SetPartsIndex(PartsType.Top, index);
    }
    public void SetBottomIndex(int index)
    {
        SetPartsIndex(PartsType.Bottom, index);
    }
}

ここでポイントとなるのはこの部分です、生成したメッシュのBoneを並び替えて付け替えます。

// 念の為Boneを並び替え
Transform[] bones = r.bones;
Transform[] newBones = new Transform[bones.Length];
for (int k = 0; k < bones.Length; k++)
{
    int index = _skeletonBones.FindIndex((Transform t) =>
    {
        return t.name == bones[k].name;
    });
    if (index != -1)
    {
        newBones[k] = _skeletonBones[index];
    }
}
r.bones = newBones;

また、Rebind()を呼び出さないと反映されないので注意してください。

// Animator反映
_animator.Rebind();

実行

UIからパーツのindexを指定して更新する事で中央に表示されているキャラクターの見た目が変更されるようにした動画です。

まとめ

動的に着せ替えられると楽しいですねー!
今後はBone構造が一部異なる場合(ズボンとスカート)での着せ替えにチャレンジしたいです、パーツごとのBoneを1つに纏めて、avatar生成する事で着せ替えられないかなぁ…(適当
知っている方いたら教えて欲しいです!
上手く出来たら続きを書きたいです、ありがとうございました!

採用情報

ワンダープラネットでは、一緒に働く仲間を幅広い職種で募集しております。

-エンジニア
-,

© WonderPlanet Inc.