개요


이 섹션에서는 MagicaCloth를 다른 캐릭터에 적용하는 방법을 설명합니다.
이 방법을 사용하면 게임 캐릭터에 다양한 헤어스타일과 의상을 변경할 수 있습니다.
이 문서는 Unity에서 C# 프로그래밍에 익숙한 사용자를 대상으로 합니다.

 

 

 

 

 

샘플 씬


의상 변경(Dress-up) 프로세스를 위한 샘플 씬이 제공됩니다.
다음 폴더에서 씬을 찾을 수 있으며, 사용하는 렌더 파이프라인에 맞는 씬을 선택하여 테스트하세요.

 

샘플 씬 내 RuntimeDressUpDemo 오브젝트에 RuntimeDressUpDemo.cs가 포함되어 있습니다.
이 페이지에서는 해당 테스트 코드를 기반으로 설명합니다.

씬이 분리된 이유

  • 씬을 분리한 이유는 단순히 렌더링 머티리얼(Rendering Material)을 전환하기 위해서입니다.
  • 내부에 포함된 샘플 코드 자체는 모든 씬에서 동일합니다.

 

 

 

샘플 데이터


샘플 씬에서 사용되는 데이터에 대해 설명합니다.

Utc_sum_humanoid (Skeleton)

먼저 Utc_sum_humanoid (Skeleton)뼈대(Skeleton)만 있는 캐릭터입니다.

 

이 캐릭터는 Transform만 포함된 스켈레톤 캐릭터이며,
헤어(Hair), 의상(Clothing), MagicaCloth가 설정되지 않은 상태입니다.

 

이제 이 Utc_sum_humanoid (Skeleton)에 헤어와 의상을 추가할 것입니다.

 

 

Utc_sum_humanoid (Hair)

헤어 렌더러(Renderer)와 MagicaCloth가 설정된 프리팹(Prefab) 형태의 스켈레톤 캐릭터입니다.

 

이 프리팹은 전체 스켈레톤(Skeleton)을 포함하고 있습니다.
(사실, 필요한 GameObject만 포함하면 충분하지만, 샘플에서는 전체 뼈대를 포함하고 있습니다.)

 

 

Utc_sum_humanoid (Body)

의상 렌더러(Renderer)와 MagicaCloth가 설정된 프리팹(Prefab) 형태의 스켈레톤 캐릭터입니다.

 

이 프리팹은 전체 스켈레톤(Skeleton)을 포함하고 있습니다.
(사실, 필요한 GameObject만 포함하면 충분하지만, 샘플에서는 전체 뼈대를 포함하고 있습니다.)

 

 

 

 

의상 변경 방법

의상을 변경하려면 아래 단계를 따르세요.

  1. 의상 프리팹을 생성하고,
  2. MagicaCloth 초기화를 호출한 후
  3. MagicaCloth가 자동으로 빌드되지 않도록 중지합니다.
  4. 그다음 Renderer를 스켈레톤 아바타에 이식하고,
  5. MagicaCloth를 스켈레톤 아바타에 이식하며,
  6. 콜라이더 및 기타 항목을 스켈레톤 아바타에 이식한 후
  7. MagicaCloth 실행을 시작합니다.

(4)에서 Renderer를 이식하는 과정은 MagicaCloth 시스템과 무관하므로 다른 프로그램이나 에셋을 사용할 수도 있습니다.

 

 

 

 

의상 제거 방법


의상을 제거하려면 아래 절차를 따릅니다.

  1. Destroy()를 사용하여 Renderer를 제거하고,
  2. Destroy()를 사용하여 MagicaCloth를 제거하며,
  3. Destroy()를 사용하여 불필요한 콜라이더를 제거하고,
  4. Destroy()를 사용하여 원하지 않는 GameObject를 제거합니다.

기본적으로 MagicaCloth를 포함한 불필요한 GameObject를 Destroy()하면 되며, (1)의 Renderer 제거는 의상 변경 과정과 마찬가지로 다른 프로그램이나 에셋을 사용할 수도 있습니다.

 

 

 

 

예제


이 섹션에서는 샘플 씬 RuntimeDressUpDemo.cs에 대해 설명합니다.
위에서 설명한 의상 변경(Dress-up) 및 제거(Undress-up) 절차를 기반으로 코드를 살펴보면,
전체적인 동작을 쉽게 이해할 수 있을 것입니다.

// 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 딕셔너리 생성

먼저, 스켈레톤 아바타(Skeletal Avatar)의 Transform 딕셔너리를 생성합니다.
이 딕셔너리는 이름(Name)을 키(Key)로 사용합니다.
이 딕셔너리는 본(Bone) 교체 작업을 수행하는 데 사용됩니다.

/// <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 컴포넌트의 초기화를 수동으로 호출하는 것이 중요합니다.
반드시 본(Bone) 교체 작업을 수행하기 전에 초기화해야 합니다.

다음으로, 자동 Cloth 빌드 작업을 중지(Pause)해야 합니다.

// 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와 직접적인 관련이 없습니다.
즉, 이 단계에서는 원하는 방식으로 처리할 수 있으며, 다른 의상 변경(Dress-up) 에셋을 사용할 수도 있습니다.

 

 

MagicaCloth 컴포넌트 이식(Porting)

MagicaCloth를 스켈레톤 아바타(Skeletal Avatar)에 이식(Implant) 합니다.
이 과정은 SkinnedMeshRenderer의 본 교체 방식과 동일하게 내부 본(Bone)을 교체하는 방식으로 수행됩니다.

본 교체는 미리 생성한 Transform 딕셔너리(Transform Dictionary)를 사용하여 진행됩니다.

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

 

 

콜라이더 이식(Transplantation of Collider)

MagicaCloth에서 콜라이더(Collider)를 사용하고 있다면,
이를 스켈레톤 아바타(Skeletal Avatar)로 함께 이식해야 합니다.

// 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를 빌드(Build)하고 실행(Run)합니다.

// 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();
}

 

 

의상 제거(Release)

의상 변경이 더 이상 필요하지 않을 경우, 다음과 같이 해제합니다.
불필요한 모든 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();
}

 

 

 

 

 

주의 사항(Notes)


이 예제 코드가 모든 가능성을 포함하는 것은 아닙니다.
따라서, 이 사례는 **참고용(Reference Only)**으로만 사용해야 합니다.

예를 들어, 의상 프리팹(Dress-up Prefab)에는 존재하지만, 스켈레톤 아바타(Skeletal Avatar)에는 존재하지 않는 GameObject
이 방식으로는 자동으로 이식되지 않습니다.
이러한 경우에는 추가적인 처리가 필요합니다.

끝.

'유니티 에셋 > Magica Cloth 2' 카테고리의 다른 글

성능(Performance)  (0) 2025.03.04
스케일 변경  (0) 2025.03.04
런타임 변경사항  (0) 2025.03.04
런타임 구성  (0) 2025.03.04
캐릭터 인스턴스화 (Character Instantiation)  (0) 2025.03.02

+ Recent posts