개요
이 섹션에서는 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만 포함하면 충분하지만, 샘플에서는 전체 뼈대를 포함하고 있습니다.)
의상 변경 방법
의상을 변경하려면 아래 단계를 따르세요.
- 의상 프리팹을 생성하고,
- MagicaCloth 초기화를 호출한 후
- MagicaCloth가 자동으로 빌드되지 않도록 중지합니다.
- 그다음 Renderer를 스켈레톤 아바타에 이식하고,
- MagicaCloth를 스켈레톤 아바타에 이식하며,
- 콜라이더 및 기타 항목을 스켈레톤 아바타에 이식한 후
- MagicaCloth 실행을 시작합니다.
(4)에서 Renderer를 이식하는 과정은 MagicaCloth 시스템과 무관하므로 다른 프로그램이나 에셋을 사용할 수도 있습니다.
의상 제거 방법
의상을 제거하려면 아래 절차를 따릅니다.
- Destroy()를 사용하여 Renderer를 제거하고,
- Destroy()를 사용하여 MagicaCloth를 제거하며,
- Destroy()를 사용하여 불필요한 콜라이더를 제거하고,
- 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 |