今回ブログを担当しますエンジニアの乾です、よろしくお願いします!
最近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生成する事で着せ替えられないかなぁ…(適当
知っている方いたら教えて欲しいです!
上手く出来たら続きを書きたいです、ありがとうございました!