이 섹션에서는 다양한 성능 관련 문제에 대해 설명합니다. 이 내용은 주로 프로그래머를 대상으로 합니다.
CPU 의존성
MagicaCloth는 Unity DOTS(Data-Oriented Technology Stack)를 사용합니다. 따라서 시뮬레이션 성능은 전적으로 CPU에 의존합니다. 반대로, GPU는 전혀 사용되지 않습니다.
또한, DOTS는 멀티스레딩을 지원하므로 CPU에 코어(스레드)가 많을수록 병렬로 실행되어 성능이 향상됩니다.
모바일 기기의 성능
그러나 Android/iPhone에서 사용할 때는 약간의 주의가 필요합니다. 모바일 기기의 CPU는 일반적으로 고성능 코어(big core)와 저전력 코어(small core)로 구성됩니다. 이를 Big-Little 구성이라고 합니다. 예를 들어, 8코어 CPU를 가진 단말기의 경우 대부분 Big4/Little4 등의 방식으로 나뉘어 있습니다. 이 경우, (4-4 코어)로 표현됩니다.
Unity는 DOTS를 Big 코어에서만 실행합니다. 따라서 위와 같은 기기의 경우, 8코어 중 오직 4개의 코어만 DOTS에 사용할 수 있습니다. 이 점을 유의하시기 바랍니다.
이 문제는 데스크톱 PC의 CPU에서는 발생하지 않습니다.
Check with Profiler
Unity의 프로파일러 기능을 사용하여 시뮬레이션 부하를 쉽게 확인할 수 있습니다. 프로파일러에서는 타임라인에 MagicaManager 블록으로 표시됩니다. 또한, Job 항목에서 멀티스레딩 상태를 확인할 수 있습니다.
Cloth Data의 생성 및 실행
MagicaCloth는 시뮬레이션을 수행하기 위해 다양한 데이터를 필요로 합니다. 이것을 Cloth Data라고 합니다. Cloth Data는 요청에 따라 실행 시간에 동적으로 생성됩니다.
Cloth Data의 생성은 상당한 계산 작업을 필요로 하며, 보통 10ms에서 50ms 정도의 시간이 걸립니다. 이 생성 과정은 백그라운드 스레드에서 실행되므로 메인 스레드에는 영향을 거의 미치지 않습니다. 또한, 여러 개의 Cloth가 여러 스레드에서 생성되며 병렬로 실행됩니다.
하지만, 시뮬레이션은 이 Cloth Data가 완성될 때까지 기다려야 합니다. 이로 인해 캐릭터가 실제로 생성된 후 시뮬레이션이 시작되기까지 여러 프레임의 지연이 발생합니다.
Editor 실행 시 주의사항
MagicaCloth에서 사용하는 Burst와 JobSystem은 빌드할 때보다 에디터에서 실행할 때 더 많은 리소스를 소모합니다. 따라서 에디터에서 실행할 때의 프로파일러 내용은 빌드에서 실행할 때와 다를 수 있습니다. 이는 다음과 같은 요소들로 인해 발생합니다.
Burst JIT Compiler
Burst는 에디터에서만 런타임(Just-In-Time Compiler)에서 컴파일됩니다. 이 컴파일은 플레이가 시작된 후에 이루어지므로, MagicaCloth가 처음 사용될 때 컴파일 시간이 수백 ms 이상 걸릴 수 있습니다. 따라서 에디터 환경에서는 플레이 후 첫 번째 시뮬레이션이 시작되기 전에 상당한 지연이 발생합니다.
이 문제는 에디터 환경에서만 발생하며 빌드 시에는 발생하지 않습니다.
이 문제를 해결하려면 Enter Play Mode Options를 사용하십시오. 이 옵션은 PlayerSettings의 Editor 탭에 있습니다.
Enter Play Mode를 사용하면, 반복적인 플레이 작업 후에도 Burst가 다시 JIT 컴파일되지 않습니다.
JobsDebugger 처리 부하
에디터에서는 JobsDebugger가 작업의 실행을 계속 모니터링합니다. 이로 인해 작업 실행 시간이 평소보다 길어지고, 작업 간에 부자연스러운 간격이 발생할 수 있습니다. 부하가 걱정되면 JobsDebugger를 끄는 것이 좋습니다.
SafeCheck 처리 부하
마찬가지로, 에디터 환경에서는 Burst의 안전성도 모니터링됩니다. 이 부하는 어느 정도 발생하므로, 걱정된다면 두 가지 체크를 끄는 것이 좋습니다..
이 두 체크를 끄면 오류는 더 이상 보고되지 않습니다.
하지만 위에서 설명한 대로JobDebugger와SafeCheck를 끄면 Burst/Jobs 오류가 표시되지 않습니다. 따라서 MagicaCloth가 제대로 작동하지 않는다고 생각되면 모든 체크를 다시 켜고 오류를 확인해보세요
빌드에서 테스트 권장
위에서 언급한 바와 같이, 에디터에서 실행할 때는 여러 모니터링으로 인해 시뮬레이션 성능이 떨어집니다. 그러나 릴리스 빌드는 모든 모니터링을 제거합니다. 따라서 실제 기기에서 실제 성능을 확인하려면 빌드를 통해 테스트하는 것이 가장 좋습니다.
빌드 시 주의사항
Burst AOT 설정
빌드할 때 Burst를 활성화하는 것을 잊지 마세요. 이는 PlayerSettings에서 Burst AOT Settings에서 설정할 수 있습니다. 이 옵션을 체크 해제하면 빌드가 Burst가 비활성화된 상태로 빌드됩니다. 기본적으로는 활성화되어 있습니다.
IL2CPP 권장
빌드할 때 IL2CPP를 사용하는 것을 강력히 추천합니다. 이유는 C#의 처리 속도가 Mono에 비해 크게 향상되기 때문입니다.
처리 부하 목록
이 섹션에서는 MagicaCloth의 기능 중 가장 처리 집약적인 기능에 대해 설명합니다. ★이 많을수록 부하가 높아집니다.
천 데이터 생성 방법
Runtime build (default)
★★★
런타임 생성은 사용될 때 천 데이터를 그 자리에서 생성합니다. 이로 인해 초기화 시 부하가 증가합니다. 천 데이터는 백그라운드에서 생성되지만, 이 과정은 CPU를 소모합니다.
Pre-build
★
프리 빌드는 천 데이터를 생성하여 에셋으로 만들어 편집 중에 미리 준비합니다. 이 방법은 초기화 부하를 크게 줄여줍니다. 또한, 백그라운드 처리도 없습니다.
천 타입
MeshCloth
★★★★
MeshCloth는 시뮬레이션 외에도 프록시 메쉬 스키닝과 렌더 메쉬에 다시 쓰는 작업이 포함되기 때문에 BoneCloth보다 훨씬 더 많은 부하를 요구합니다. 따라서 모바일 기기에서 사용할 때 성능에 주의해야 합니다.
BoneCloth
★
BoneCloth는 매우 가벼운 타입입니다. 대부분의 경우, 대량으로 사용해도 문제가 발생하지 않습니다.
충돌 처리
Self Collision
★★★★★★★★★★
Self-collision은 모든 기능 중에서 가장 눈에 띄고 처리 부하가 큰 과정입니다. 따라서 기본적으로 CPU 코어가 많은 데스크톱 PC에서 사용을 권장합니다. 모바일 기기에서 사용할 경우, 프록시 메쉬의 꼭지점 수를 가능한 한 줄이고 성능에 주의해야 합니다.
Mutual Collision
★★★★★★
Mutual collision은 다른 대상과의 충돌만 결정하기 때문에 self-collision보다는 부하가 약간 적습니다. 하지만 프로세스는 self-collision과 다를 바 없으므로 성능에 주의해야 합니다.
Edge Collision
★★★★
Edge collisions는 점 충돌보다 몇 배 더 많은 부하를 요구합니다. 점 충돌에서 문제가 발생할 때만 사용하도록 하세요.
Point Collision
★★
Point collisions는 다른 충돌 판별 방식에 비해 처리 부하가 훨씬 적습니다.
Backstop
★
Backstop은 계산이 적게 필요하기 때문에 가장 낮은 처리 부하를 가집니다. 따라서 걱정 없이 사용할 수 있습니다.
시뮬레이션 주파수 및 최대 업데이트 수
MagicaCloth의 시뮬레이션은 자체적인 시간 관리 방식으로 인해 Unity의 프레임 업데이트와 다른 타이밍에 실행됩니다. 이것은 프레임 속도와 관계없이 일정한 간격으로 실행됩니다.
이 일정한 간격을 시뮬레이션 주파수라고 합니다. 예를 들어, 주파수가 90이면 시뮬레이션은 1초에 1/90초마다 업데이트됩니다. 이는 Unity의 물리 엔진 업데이트(FixedUpdate)와 프레임 업데이트 간의 관계와 같습니다. MagicaCloth는 초기 주파수로 90을 설정해 두었습니다. 즉, 시뮬레이션은 1초에 90번 업데이트됩니다.
또한, 한 프레임에서 실행할 수 있는 최대 시뮬레이션 횟수가 설정되어 있습니다. 이것은 과도한 부하로 인해 시뮬레이션이 무한히 반복되지 않도록 방지하는 안전 기능입니다. MagicaCloth는 초기값으로 3번이 설정되어 있습니다. 최대 횟수 때문에 시뮬레이션이 생략되면, 위치는 보간(interpolation) 함수로 보충됩니다. 이 보간 함수는 간단하고 정확도가 떨어집니다. 따라서 시뮬레이션이 생략되면 아티팩트가 발생할 수 있다는 점을 유의해야 합니다.
주파수와 성능
시뮬레이션 주파수는 성능과 밀접하게 관련이 있습니다. 주파수를 낮추면 시뮬레이션을 적게 실행하게 되어 성능이 향상됩니다. 하지만 주파수는 시뮬레이션 정확도에 큰 영향을 미칩니다. 따라서 주파수를 낮추면 시뮬레이션의 정확도도 낮아지게 됩니다. 주파수와 시뮬레이션 정확도 사이에는 트레이드오프가 있다는 점을 염두에 두어야 합니다.
주파수 및 최대 업데이트 수 변경
주파수와 최대 업데이트 횟수는 두 가지 방법으로 변경할 수 있습니다. 변경은 언제든지 가능합니다.
MagicaSettings라는 전용 컴포넌트를 제공하여 시스템의 상태를 변경할 수 있습니다. 이 컴포넌트를 사용하면 코딩 없이 주파수와 최대 업데이트 수를 변경할 수 있습니다. 설정 방법에 대해서는 MagicaSettings 문서를 참조하십시오.
주파수의 운영 효과
주파수를 변경하면 시뮬레이션 동작에 약간의 변화가 생깁니다. 예를 들어, 주파수를 90으로 설정한 상태에서 이동을 조정한 후, 주파수를 30이나 150으로 설정하면 이동이 달라지고 완전히 동일하지 않게 됩니다. 이는 주파수를 변경하면 파라미터의 효과에 약간의 차이가 발생하기 때문입니다. 따라서 주파수 변경은 파라미터 재조정이 필요할 수 있습니다.
설정예시
다음은 몇 가지 설정 예시입니다.
성능 우선 설정
성능이 중요하다면 주파수를 60으로 설정하고 최대 업데이트를 2로 설정해 보세요. 정확도는 조금 떨어지지만 성능이 향상됩니다.
고정 프레임 속도 설정
게임이 60fps와 같은 고정된 프레임 속도로 실행될 경우, 주파수를 이에 맞게 설정하는 것도 효과적입니다. 예를 들어, 주파수를 60으로 설정하고 최대 업데이트 횟수를 1로 제한하면 한 프레임의 부하가 안정화됩니다.
또한, 게임이 30fps로 실행된다면 주파수를 60으로 설정하고 최대 업데이트 횟수를 2로 설정하면 도움이 됩니다. 이렇게 하면 한 프레임에서 시뮬레이션이 두 번 업데이트되어 30fps에서도 주파수 60의 정확도를 확보할 수 있습니다.
성능 최우선 설정
성능을 최우선으로 한다면 주파수를 30으로 설정하고 최대 업데이트 횟수를 1로 설정해 보세요. 이 설정은 최대 성능을 발휘할 수 있습니다. 하지만 정확도가 크게 떨어지므로 매우 신중해야 합니다. 이 설정은 아티팩트보다 성능을 우선시하는 설정입니다.
컬링 시스템
컬링은 카메라에 표시되지 않거나 카메라에서 일정 거리 이상 떨어진 캐릭터들의 시뮬레이션을 중지시켜 성능을 향상시키는 기능입니다.
이 기능은 1인칭 FPS 게임이나 VR에서 성능을 크게 향상시킵니다. 컬링은 카메라 컬링과 거리 컬링의 두 가지 기능으로 구성됩니다.
DOTS는 Transforms의 읽기 및 쓰기 작업에 멀티스레딩을 사용합니다. 하지만 이 혜택을 활용하려면 캐릭터 배치 방식에 주의해야 합니다. DOTS에서는 Transform 처리가 계층 구조의 루트에 배치된 각 GameObject 그룹에 대해 멀티스레딩 방식으로 처리됩니다.
다음 예시에서, 모든 10개의 캐릭터가 루트에 배치되었으므로 각 캐릭터의 Transform 처리 과정은 여러 스레드에서 실행됩니다. 이것은 이상적인 배치입니다.
그러나 다음 예시에서는 모든 캐릭터가 "CharacterGroup" 객체의 자식으로 배치되어 있습니다. 이것은 매우 나쁜 예시로, Transform 처리가 전혀 멀티스레딩 방식으로 이루어지지 않습니다. 특히 많은 수의 캐릭터가 있을 경우 성능 저하가 두드러지게 나타날 수 있습니다.
시뮬레이션 작업
이 섹션에서는 천 시뮬레이션이 어떻게 수행되는지 설명합니다. 이 섹션은 시뮬레이션 프로세스가 최적화된 v2.14.0 이상 버전을 사용하고 있다고 가정합니다.
분할 작업
시뮬레이션 처리는 작업(job)이라는 처리 단위로 나누어져 실행됩니다. 이 작업들은 프로파일러에서 확인할 수 있습니다.
이미지에서 볼 수 있듯이 작업은 매우 작은 부분으로 나누어집니다. 이것은 각 처리 단계에서 데이터 동기화가 필요하기 때문입니다. 이 방법은 CPU 코어를 최대한 활용할 수 있다는 장점이 있지만, 작업 스케줄링 시간과 동기화 대기 시간 증가와 같은 단점도 있습니다.
배치 작업
따라서 v2.14.0부터는 작업을 나누지 않고 한 번에 처리하는 배치 작업을 추가하였습니다. 배치 작업에서는 하나의 MagicaCloth 컴포넌트 처리 작업이 하나의 작업으로 할당됩니다. 이렇게 하면 작업을 나누는 것에 비해 불필요한 동기화 시간이 제거되어 속도가 크게 향상됩니다.
하지만 단점도 존재합니다. 배치 작업은 작업을 나누지 않기 때문에 스레드별로 처리를 분배할 수 없습니다.따라서 처리 시간이 무겁게 드는 MagicaCloth 컴포넌트가 하나 있을 경우, 전체 처리 시간이 연장될 수 있습니다.
다음과 같이, 무거운 작업이 처리되는 동안 다른 CPU 코어는 완전히 유휴 상태가 됩니다. 이로 인해 CPU는 매우 비효율적이며, 작업이 분리된 작업으로 처리될 때보다 더 많은 시간이 소요됩니다.
분할 작업과 배치 작업 함께 사용하기
이 문제를 해결하기 위해, 우리는 경량 컴포넌트는 배치 작업으로 처리하고, 무거운 컴포넌트는 분할 작업으로 처리하는 하이브리드 시스템을 구현했습니다. 이렇게 하면 CPU 낭비를 없애고 코어를 최대한 활용할 수 있습니다. 배치 작업과 분할 작업은 병렬로 실행됩니다. 분할 작업은 다음 두 가지 조건에서 적용됩니다:
프록시 메쉬의 꼭지점 수가 300개 이상인 경우
Self-collision 또는 Mutual collision을 사용하는 경우
이 조건 중 하나라도 만족하면 분할 작업이 사용됩니다. 프록시 메쉬의 꼭지점 수는 인스펙터에서 확인할 수 있습니다.
분할 작업 임계값 변경
분할 작업의 조건은 프록시 메쉬의 꼭지점 수가 300개 이상이어야 하지만, 이 기준은 변경할 수 있습니다. 이를 변경하는 두 가지 방법은 다음과 같습니다:
MagicaSettings 코딩 없이 MagicaSettings 컴포넌트를 설치하여 변경할 수 있습니다.
이 변경은 런타임 중 언제든지 할 수 있으므로, 최대 효율성을 원한다면 플랫폼에 따라 임계값을 조정하는 것을 고려하세요.
기타
에디터 환경에서의 극단적인 성능 저하
지금까지 최종 사용자들로부터 에디터 환경에서 실행 시 성능이 매우 낮다는 보고를 받았습니다. 로드가 빌드할 때보다 수십 배 더 높은 것으로 나타났습니다. 하지만 이 문제는 재현되지 않으며, 일부 PC 환경에서만 발생하는 것으로 알려져 있습니다. 이 상황을 겪으셨다면, 프로젝트의 Library 폴더를 아래와 같이 삭제해 보세요:
일부 사용자들은 이 방법으로 문제를 해결했다고 보고했습니다. Library 폴더는 프로젝트의 작업 데이터를 저장하며, 삭제하면 자동으로 다시 빌드됩니다. 하지만 Library 폴더를 삭제하기 전에 다음 단계를 따르세요:
이 섹션에서는 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()**하여 제거하면 됩니다.