着せ替え処理

概要


ここではMagicaClothを別のキャラクターへ移植する方法について説明します。
これはゲームのキャラクターへ様々な髪や衣装を着せ替える場合に便利です。
なお読者はUnityでのC#プログラミングを理解していることを前提としています。

サンプルシーン


着せ替え処理についてはサンプルシーンが用意されています。
これらは次のフォルダにありテストする場合はお使いのレンダーパイプラインのシーンを選択してください。

このサンプルシーンにあるRuntimeDressUpDemoにアタッチされているRuntimeDressUpDemo.csがテストコードとなっています。
このページでもこのテストコードの内容を元に説明していきます。

なお、シーンが分かれている理由は単に描画マテリアルの切り替えのためです。
内部に含まれるサンプルコードは同一です。

サンプルデータ


サンプルシーンのデータについて説明します。

Utc_sum_humanoid (Skeleton)

まず、骨格のみのキャラクターであるUtc_sum_humanoid (Skeleton)があります。

このキャラクターは髪も服もMagicaClothも設定していないTransformのみの骨格キャラクターです。

このUtc_sum_humanoid (Skeleton)に髪や服を追加することになります。

Utc_sum_humanoid (Hair)

骨格キャラクターに髪のレンダラーとMagicaClothを設定したプレハブです。

このプレハブには骨格のすべてが内包されています。
(実際には必要なGameObjectのみで問題ありません)

Utc_sum_humanoid (Body)

骨格キャラクターに服のレンダラーとMagicaClothを設定したプレハブです。

このプレハブには骨格のすべてが内包されています。
(実際には必要なGameObjectのみで問題ありません)

着せ替え手順


着せ替えは次の手順で行います。

  1. 着せ替え用のプレハブを生成
  2. MagicaClothの初期化を呼び出す
  3. MagicaClothの自動構築を停止させる
  4. Rendererを骨格アバターへ移植する
  5. MagicaClothを骨格アバターへ移植する
  6. コライダーなどを骨格アバターへ移植する
  7. MagicaClothの実行を開始する

(4)のRendererの移植についてはMagicaClothのシステムとは関係がありません。
そのため別のプログラムやアセットを利用しても構いません。

解除手順


着せ替えの解除は次の手順で行います。

  1. RendererをDestroy()する
  2. MagicaClothをDestroy()する
  3. 不要なコライダーをDestroy()する
  4. 不要なGameObjectをDestroy()する

基本的にはMagicaClothを含め不要なGameObjectをDestroy()するだけです。
なお、(1)については着せ替え手順と同じく別のプログラムやアセットを利用しても構いません。

事例


ここではサンプルシーンのRuntimeDressUpDemo.csについて解説します。
上記の着せ替え手順と解除手順を元にコードを見てもらえば、大体の内容は把握できるかと思います。

// Magica Cloth 2.
// Copyright (c) 2023 MagicaSoft.
// https://magicasoft.jp
using System.Collections.Generic;
using UnityEngine;

namespace MagicaCloth2
{
  /// <summary>
  /// Dress-up sample.
  /// </summary>
  public class RuntimeDressUpDemo : MonoBehaviour
  {
    /// <summary>
    /// Avatar to change clothes.
    /// </summary>
    public GameObject targetAvatar;

    /// <summary>
    /// Hair prefab with MagicaCloth set in advance.
    /// </summary>
    public GameObject hariEqupPrefab;

    /// <summary>
    /// Clothes prefab with MagicaCloth set in advance.
    /// </summary>
    public GameObject bodyEquipPrefab;

    //=========================================================================================
    /// <summary>
    /// Bones dictionary of avatars to dress up.
    /// </summary>
    Dictionary<string, Transform> targetAvatarBoneMap = new Dictionary<string, Transform>();

    /// <summary>
    /// Information class for canceling dress-up.
    /// </summary>
    class EquipInfo
    {
      public GameObject equipObject;
      public List<ColliderComponent> colliderList;

      public bool IsValid() => equipObject != null;
    }
    EquipInfo hairEquipInfo = new EquipInfo();
    EquipInfo bodyEquipInfo = new EquipInfo();

    //=========================================================================================
    private void Awake()
    {
      Init();
    }

    void Start()
    {
    }

    void Update()
    {
    }

    //=========================================================================================
    public void OnHairEquipButton()
    {
      if (hairEquipInfo.IsValid())
        Remove(hairEquipInfo);
      else
        Equip(hariEqupPrefab, hairEquipInfo);
    }

    public void OnBodyEquipButton()
    {
      if (bodyEquipInfo.IsValid())
        Remove(bodyEquipInfo);
      else
        Equip(bodyEquipPrefab, bodyEquipInfo);
    }

    //=========================================================================================
    /// <summary>
    /// Create an avatar bone dictionary in advance.
    /// </summary>
    void Init()
    {
      Debug.Assert(targetAvatar);

      // Create all bone maps for the target avatar
      foreach (Transform bone in targetAvatar.GetComponentsInChildren<Transform>())
      {
        if (targetAvatarBoneMap.ContainsKey(bone.name) == false)
        {
          targetAvatarBoneMap.Add(bone.name, bone);
        }
        else
        {
          Debug.Log($"Duplicate bone name :{bone.name}");
        }
      }
    }

    /// <summary>
    /// Equip clothes.
    /// </summary>
    /// <param name="equipPrefab"></param>
    /// <param name="einfo"></param>
    void Equip(GameObject equipPrefab, EquipInfo einfo)
    {
      Debug.Assert(equipPrefab);

      // Generate a prefab with cloth set up.
      var gobj = Instantiate(equipPrefab, targetAvatar.transform);

      // All cloth components included in the prefab.
      var clothList = new List<MagicaCloth>(gobj.GetComponentsInChildren<MagicaCloth>());

      // All collider components included in the prefab.
      var colliderList = new List<ColliderComponent>(gobj.GetComponentsInChildren<ColliderComponent>());

      // All renderers included in the prefab.
      var skinList = new List<SkinnedMeshRenderer>(gobj.GetComponentsInChildren<SkinnedMeshRenderer>());

      // First stop the automatic build that is executed with Start().
      // And just in case, it does some initialization called Awake().
      foreach (var cloth in clothList)
      {
        // Normally it is called with Awake(), but if the component is disabled, it will not be executed, so call it manually.
        // Ignored if already run with Awake().
        cloth.Initialize();

        // Turn off auto-build on Start().
        cloth.DisableAutoBuild();
      }

      // Swap the bones of the SkinnedMeshRenderer.
      // This process is a general dress-up process for SkinnedMeshRenderer.
      // Comment out this series of processes when performing this process with functions such as other assets.
      foreach (var sren in skinList)
      {
        var bones = sren.bones;
        Transform[] newBones = new Transform[bones.Length];

        for (int i = 0; i < bones.Length; ++i)
        {
          Transform bone = bones[i];
          if (!targetAvatarBoneMap.TryGetValue(bone.name, out newBones[i]))
          {
            // Is the bone the renderer itself?
            if (bone.name == sren.name)
            {
              newBones[i] = sren.transform;
            }
            else
            {
              // bone not found
              Debug.Log($"[SkinnedMeshRenderer({sren.name})] Unable to map bone [{bone.name}] to target skeleton.");
            }
          }
        }
        sren.bones = newBones;

        // root bone
        if (targetAvatarBoneMap.ContainsKey(sren.rootBone?.name))
        {
          sren.rootBone = targetAvatarBoneMap[sren.rootBone.name];
        }
      }

      // Here, replace the bones used by the MagicaCloth component.
      foreach (var cloth in clothList)
      {
        // Replaces a component's transform.
        cloth.ReplaceTransform(targetAvatarBoneMap);
      }

      // Move all colliders to the new avatar.
      foreach (var collider in colliderList)
      {
        Transform parent = collider.transform.parent;
        if (parent && targetAvatarBoneMap.ContainsKey(parent.name))
        {
          Transform newParent = targetAvatarBoneMap[parent.name];

          // After changing the parent, you need to write back the local posture and align it.
          var localPosition = collider.transform.localPosition;
          var localRotation = collider.transform.localRotation;
          collider.transform.SetParent(newParent);
          collider.transform.localPosition = localPosition;
          collider.transform.localRotation = localRotation;
        }
      }

      // Finally let's start building the cloth component.
      foreach (var cloth in clothList)
      {
        // I disabled the automatic build, so I build it manually.
        cloth.BuildAndRun();
      }

      // Record information for release.
      einfo.equipObject = gobj;
      einfo.colliderList = colliderList;
    }

    /// <summary>
    /// Removes equipped clothing.
    /// </summary>
    /// <param name="einfo"></param>
    void Remove(EquipInfo einfo)
    {
      Destroy(einfo.equipObject);
      foreach (var c in einfo.colliderList)
      {
        Destroy(c.gameObject);
      }

      einfo.equipObject = null;
      einfo.colliderList.Clear();
    }
  }
}

Transform辞書を作成する

まず骨格アバターのTransformの辞書を作成しておきます。
これは名前をキーにした辞書です。
この辞書を使いボーンの置換を実行します。

/// <summary>
/// Bones dictionary of avatars to dress up.
/// </summary>
Dictionary<string, Transform> targetAvatarBoneMap = new Dictionary<string, Transform>();

/// <summary>
/// Create an avatar bone dictionary in advance.
/// </summary>
void Init()
{
  Debug.Assert(targetAvatar);

  // Create all bone maps for the target avatar
  foreach (Transform bone in targetAvatar.GetComponentsInChildren<Transform>())
  {
    if (targetAvatarBoneMap.ContainsKey(bone.name) == false)
    {
      targetAvatarBoneMap.Add(bone.name, bone);
    }
    else
    {
      Debug.Log($"Duplicate bone name :{bone.name}");
    }
  }
}

MagicaClothの初期化と自動構築の停止

まず重要なのがMagicaClothコンポーネントの初期化を手動で呼び出すことです。
ボーンの置換を行う前に必ず初期化するようにしてください。
次に自動的に実行されるクロスの構築作業を一旦停止させます。

// First stop the automatic build that is executed with Start().
// And just in case, it does some initialization called Awake().
foreach (var cloth in clothList)
{
  // Normally it is called with Awake(), but if the component is disabled, it will not be executed, so call it manually.
  // Ignored if already run with Awake().
  cloth.Initialize();

  // Turn off auto-build on Start().
  cloth.DisableAutoBuild();
}

SkinnedMeshRendererの移植

MagicaClothの前にレンダラーを骨格アバターに移植します。
これはSkinnedMeshRendererのボーンを骨格アバターのボーンに置換することで行います。

// Swap the bones of the SkinnedMeshRenderer.
// This process is a general dress-up process for SkinnedMeshRenderer.
// Comment out this series of processes when performing this process with functions such as other assets.
foreach (var sren in skinList)
{
  var bones = sren.bones;
  Transform[] newBones = new Transform[bones.Length];

  for (int i = 0; i < bones.Length; ++i)
  {
    Transform bone = bones[i];
    if (!targetAvatarBoneMap.TryGetValue(bone.name, out newBones[i]))
    {
      // Is the bone the renderer itself?
      if (bone.name == sren.name)
      {
        newBones[i] = sren.transform;
      }
      else
      {
        // bone not found
        Debug.Log($"[SkinnedMeshRenderer({sren.name})] Unable to map bone [{bone.name}] to target skeleton.");
      }
    }
  }
  sren.bones = newBones;

  // root bone
  if (targetAvatarBoneMap.ContainsKey(sren.rootBone?.name))
  {
    sren.rootBone = targetAvatarBoneMap[sren.rootBone.name];
  }
}

この処理はMagicaClothとは一切関係が無い点に留意してください。
つまり、この部分はユーザー独自の処理を行ったり他の着せ替えアセットを利用することも可能です。

MagicaClothコンポーネントの移植

MagicaClothを骨格アバターに移植します。
これはSkinnedMeshRendererと同じく内部のボーンを置換することで行います。
置換は予め作成したTransform辞書を渡すことで行います。

// Here, replace the bones used by the MagicaCloth component.
foreach (var cloth in clothList)
{
  // Replaces a component's transform.
  cloth.ReplaceTransform(targetAvatarBoneMap);
}

コライダーの移植

MagicaCloth用のコライダーを利用している場合は、それも骨格アバターに移植します。

// Move all colliders to the new avatar.
foreach (var collider in colliderList)
{
  Transform parent = collider.transform.parent;
  if (parent && targetAvatarBoneMap.ContainsKey(parent.name))
  {
    Transform newParent = targetAvatarBoneMap[parent.name];

    // After changing the parent, you need to write back the local posture and align it.
    var localPosition = collider.transform.localPosition;
    var localRotation = collider.transform.localRotation;
    collider.transform.SetParent(newParent);
    collider.transform.localPosition = localPosition;
    collider.transform.localRotation = localRotation;
  }
}

MagicaClothの実行開始

最後にMagicaClothの構築と実行を開始します。

// Finally let's start building the cloth component.
foreach (var cloth in clothList)
{
  // I disabled the automatic build, so I build it manually.
  cloth.BuildAndRun();
}

解除

着せ替えが不要となった場合は次のように解除します。
これは単純に不要となったすべてのGameObjectをDestroy()しているだけです。

/// <summary>
/// Removes equipped clothing.
/// </summary>
/// <param name="einfo"></param>
void Remove(EquipInfo einfo)
{
  Destroy(einfo.equipObject);
  foreach (var c in einfo.colliderList)
  {
    Destroy(c.gameObject);
  }

  einfo.equipObject = null;
  einfo.colliderList.Clear();
}

留意点


今回の事例コードはすべての可能性を網羅していないことに注意してください。
この事例はあくまで参考です。

例えば着せ替えプレハブ内には存在するが骨格アバターには存在しないGameObjectなどはこの事例では移植されません。
それらは別途処理を追加して移植する必要があります。