ボーンの差し替えによる着せ替え

Last updated 3 months ago

Unity でボーンの差し替えによる着せ替えの実装を紹介します。

完成品

実際にハッピーおしゃれタイムで使っているコードです。

Equipper.cs
using UnityEngine;
using System.Linq;
using System.Collections.Generic;
public class Equipper : MonoBehaviour
{
[SerializeField]
public SkinnedMeshRenderer equipmentRenderer;
Transform rootBone;
Dictionary<string, Transform> bodyBones;
Dictionary<string, Transform> equipmentUniqueBones = new Dictionary<string, Transform>();
[SerializeField]
Transform[] bones;
public void Equip(Transform rootBone, Dictionary<string, Transform> bodyBones)
{
this.rootBone = rootBone;
this.bodyBones = bodyBones;
ReplaceSameNameBonesAndRecordUniqueBones();
TransplantUniqueEquipmentBones();
transform.SetParent(rootBone, false);
}
void OnDestroy()
{
foreach (var uniqueBone in equipmentUniqueBones.Values)
{
if (uniqueBone != null)
{
Destroy(uniqueBone.gameObject);
}
}
}
void ReplaceSameNameBonesAndRecordUniqueBones()
{
var equipmentBones = equipmentRenderer.bones.ToArray();
for (var i = 0; i < equipmentBones.Length; i++)
{
var equipmentBone = equipmentBones[i];
var equipmentBoneName = equipmentBone.name;
Transform sameNameBodyBone;
if (bodyBones.TryGetValue(equipmentBoneName, out sameNameBodyBone))
{
equipmentBones[i] = sameNameBodyBone;
}
else
{
equipmentUniqueBones[equipmentBoneName] = equipmentBone;
}
}
equipmentRenderer.bones = equipmentBones;
equipmentRenderer.rootBone = bodyBones.Values.Concat(equipmentUniqueBones.Values).First(b => b.name == equipmentRenderer.rootBone.name);
}
class TransplantBone
{
public Transform bone;
public bool transplanted;
public string Name { get { return bone.name; } }
public TransplantBone(Transform bone)
{
this.bone = bone;
}
}
void TransplantUniqueEquipmentBones()
{
var transplantBones = equipmentUniqueBones
.Values
.Select(bone => new TransplantBone(bone))
.ToDictionary(b => b.Name, b => b);
foreach (var transplantBone in transplantBones.Values)
{
if (transplantBone.transplanted) continue;
var currentBone = transplantBone.bone;
while (true)
{
if (currentBone == null)
{
Debug.LogWarning(transplantBone.Name + "は体に対応するボーンを持っていません");
break;
}
Transform sameNameBodyBone;
if (bodyBones.TryGetValue(currentBone.name, out sameNameBodyBone))
{
// おなじ名前のボーンが見つかったら、子供をそのボーンに移植する
var bonesToTransplant = new List<Transform>();
foreach (Transform child in currentBone)
{
if (transplantBones.ContainsKey(child.name))
{
bonesToTransplant.Add(child);
}
}
foreach (var bone in bonesToTransplant)
{
bone.SetParent(sameNameBodyBone, false);
// 自分を含めた子孫をチェック済みにする
foreach (var equipmentBone in bone.GetComponentsInChildren<Transform>(true))
{
if (transplantBones.ContainsKey(equipmentBone.name))
{
transplantBones[equipmentBone.name].transplanted = true;
}
}
}
break;
}
currentBone = currentBone.parent;
}
}
}
}

SkinnedMeshRenderer の仕組み

そもそも、SkinnedMeshRenderer の大雑把な仕組みを説明すると、まず、参照している MeshMesh.bindposes というのを持っています。これは何かというと、メッシュの初期状態のときの各ボーンの位置と回転とスケールの情報です。これは変換行列で表せるので型はMatrix4x4[]になっています。これらボーンの初期状態と今のボーンの状態の差分を、各頂点が持つ重み(Mesh.boneWeights)を踏まえて反映させるのが、基本的な仕組みとなっています。

では、SkinnedMeshRenderer はボーンへの参照をどのように持っているのでしょうか。SkinnedMeshRenderer は内部的に SkinnedMeshRenderer.bonesというフィールドを持っています。型は Transform[]です。実際に見てみましょう。以下のコンポーネントを任意の GameObject に貼り付けて、skinnedMeshRenderer への参照を入れて実行してみてください。

BoneViewer.cs
using UnityEngine;
public class BoneViewer : MonoBehaviour
{
[SerializeField]
SkinnedMeshRenderer skinnedMeshRenderer;
[SerializeField]
Transform[] bones;
void Start()
{
bones = skinnedMeshRenderer.bones;
}
}

ボーンの差し替え

このボーンの配列は上書きすることが可能です。例えば、以下のようにするとボーンの配列の最初のボーンが新しい GameObject と入れ替わります。

using UnityEngine;
public class BoneOverwriter : MonoBehaviour
{
[SerializeField]
SkinnedMeshRenderer skinnedMeshRenderer;
void Start()
{
var newBone = new GameObject("New Bone");
var bones = skinnedMeshRenderer.bones;
bones[0] = newBone.transform;
skinnedMeshRenderer.bones = bones;
}
}

ここまで来るとわかるかもしれませんが、ボーンの差し替えによる着せ替えは、

  • 素体 + ボーン

  • 服 + ボーン

の 2つのプレハブを用意しておき、服の SkinnedMeshRendererbonesを素体のボーンへの参照に差し替えることで実現します。

このとき必ず、SkinnedMeshRenderer.rootBoneも差し替えることを忘れないでください。画面外の計算をするかどうかの判断に関わるバウンディングボックスが、この rootBoneを基準としているからです。画面端で急にモデルが消える場合はこれを疑ってみてください。

差し替えは順番依存ではなく、ボーン名を比較して同じボーン名のところを差し替えるようにしましょう。ツールにもよるかもしれませんが、書き出しの際にボーンの順番が同じであることは保証されていないようです。

必要がある場合コンポーネントもコピーすることを忘れないでください。

追加ボーンの移植

服のボーンの参照を差し替えるときに、もし子供に体に含まれないボーンがいる場合は、構造を維持して体のボーンに移動することで、髪の毛やスカートなどの追加ボーンを扱うことができます。