이 섹션에서는 다양한 성능 관련 문제에 대해 설명합니다. 이 내용은 주로 프로그래머를 대상으로 합니다.
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.jpusing System.Collections.Generic;
using UnityEngine;
namespaceMagicaCloth2
{
///<summary>/// Dress-up sample.///</summary>publicclassRuntimeDressUpDemo : 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>classEquipInfo
{
public GameObject equipObject;
public List<ColliderComponent> colliderList;
publicboolIsValid() => equipObject != null;
}
EquipInfo hairEquipInfo = new EquipInfo();
EquipInfo bodyEquipInfo = new EquipInfo();
//=========================================================================================privatevoidAwake()
{
Init();
}
voidStart()
{
}
voidUpdate()
{
}
//=========================================================================================publicvoidOnHairEquipButton()
{
if (hairEquipInfo.IsValid())
Remove(hairEquipInfo);
else
Equip(hariEqupPrefab, hairEquipInfo);
}
publicvoidOnBodyEquipButton()
{
if (bodyEquipInfo.IsValid())
Remove(bodyEquipInfo);
else
Equip(bodyEquipPrefab, bodyEquipInfo);
}
//=========================================================================================///<summary>/// Create an avatar bone dictionary in advance.///</summary>voidInit()
{
Debug.Assert(targetAvatar);
// Create all bone maps for the target avatarforeach (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>voidEquip(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 boneif (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>voidRemove(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>voidInit()
{
Debug.Assert(targetAvatar);
// Create all bone maps for the target avatarforeach (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 boneif (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()**하여 제거하면 됩니다.
이 섹션에서는 스케일(Scale) 변경 방법을 설명합니다. 편집 중 및 실행 중 스케일 변경에는 몇 가지 제한 사항이 있습니다.
제한 사항
먼저, MagicaCloth2에서는 균일한 스케일(Uniform Scaling)만 지원됩니다. 즉, X, Y, Z 축을 개별적으로 조정하는 것은 불가능하며, 전체적으로 2배, 3배, 0.5배 등 동일한 비율로 스케일 조정해야 합니다.
편집(Editing) 중 스케일 제한
편집 시에는 양수의 균일한 스케일만 허용됩니다.
음수(-) 스케일은 사용할 수 없습니다.
Inspector에서 잘못된 스케일이 적용된 경우 경고 메시지가 표시됩니다.
실행(Running) 중 스케일 제한
실행 중에는 균일한 스케일 변경이 자유롭게 가능합니다.
v2.8.0부터 음수(-) 스케일도 지원됩니다.
이를 이용하면 캐릭터 전체를 반전(Invert)할 수 있습니다.
주로 2D 게임에서 유용하게 사용됩니다.
단, 음수 스케일은 한 개의 축(X, Y, Z 중 하나)에서만 적용할 수 있습니다.
기본 스케일(Base Scale) 개념
런타임에서 Cloth 컴포넌트가 초기화될 때의 스케일 값을 **기본 스케일(Base Scale)**이라고 합니다. MagicaCloth는 런타임 중 어떤 스케일 변경을 하더라도 기본 스케일 및 흔들림 동작(Swaying Behavior)이 유지되도록 설계되었습니다.
예를 들어, 다음과 같이 캐릭터 세 명이 있다고 가정합니다.
가운데 캐릭터는 기본 스케일(1.0x)
왼쪽 캐릭터는 1.5배 크기(1.5x)
오른쪽 캐릭터는 0.7배 크기(0.7x)
이 경우, 크기가 다르더라도 흔들리는 방식은 모두 동일합니다. 즉, Cloth가 초기화될 때 설정된 기본 스케일이 매우 중요합니다.
스케일 변경 시 주의 사항
캐릭터의 스케일을 실시간으로 변경할 때 몇 가지 제한 사항이 있습니다. 하지만 v2.13.0 이후 버전에서는 이를 해결할 수 있으며, 사전 빌드(Pre-Building)를 사용하면 제한을 우회할 수 있습니다. 자세한 내용은 Character Instantiation(캐릭터 인스턴스화) 문서를 참고하세요.
다음 제한 사항은 v2.13.0 이전의 런타임 빌드에서만 적용됩니다.
초기화 전에 스케일을 변경할 수 없음
런타임 중 스케일 변경은 Cloth 컴포넌트가 초기화된 후에만 가능합니다. 초기화 전에 스케일을 변경하면 예기치 않은 동작이 발생할 수 있습니다. Cloth 컴포넌트는 기본적으로 Start()에서 초기화되며, 이때 참조 스케일(Reference Scale)이 기록됩니다. 따라서, 특정 이유로 Start() 이전에 스케일을 변경하려면 수동으로 초기화를 호출해야 합니다. 이를 위해 Initialize() 메서드를 사용하세요.
씬에 미리 배치할 때의 주의 사항
예를 들어, Cloth 컴포넌트가 포함된 캐릭터 프리팹 A가 있다고 가정합니다. 이 프리팹 A를 처음부터 스케일을 2배로 설정하여 씬에 배치하면 Cloth가 올바르게 작동하지 않습니다. 이는 초기화 시점에서 기본 스케일(Base Scale)이 결정되기 때문입니다. 즉, 스케일이 2배로 설정된 상태에서 초기화되면, 이 스케일이 기본 스케일로 인식되어 Cloth 동작이 예상과 다를 수 있습니다.
이 문제를 방지하려면:
씬에 배치할 때 원래 크기(1.0x)로 배치합니다.
Cloth 컴포넌트를 초기화한 후(Initialize() 실행 후), 스크립트를 통해 스케일을 변경합니다.
MagicaCloth2는 런타임 구성을 완전히 지원합니다. 여기서는 스크립트를 통해 런타임 중 MagicaCloth 구성 요소를 생성하는 방법을 설명합니다. 이 문서는 Unity에서 C# 프로그래밍에 익숙한 독자를 대상으로 합니다.
샘플 씬
MagicaCloth2의 런타임 구성을 위한 샘플 씬이 제공됩니다. 아래 폴더에서 사용할 렌더 파이프라인에 맞는 씬을 선택하여 테스트할 수 있습니다.
이 샘플 씬에는 RuntimeBuildDemo라는 오브젝트에 RuntimeBuildDemo.cs라는 테스트 코드가 포함되어 있습니다. 이 페이지에서는 해당 테스트 코드의 내용을 추출하여 설명합니다.
씬이 분리된 이유는 단순히 렌더링 머티리얼을 전환하기 위함이며, 내부에 포함된 샘플 코드는 동일합니다.
구성 절차
다음 단계에 따라 MagicaCloth를 빌드할 수 있습니다.
MagicaCloth 구성 요소 생성
파라미터 설정
옷감(Cloth) 데이터 생성 및 실행 시작
예제와 함께 각 단계를 설명하겠습니다. 또한 구성 시 주의해야 할 몇 가지 사항이 있습니다.
정점 속성 지정
MagicaCloth에서는 어떤 정점(vertex)이 움직이고 어떤 정점이 움직이지 않는지를 명확하게 지정해야 합니다. 이를 정점 속성(Vertex Attributes)이라고 합니다.
에디터에서는 정점 페인트 기능을 사용하여 수동으로 속성을 설정합니다. 그러나 이 방법은 런타임 구성에서는 사용할 수 없습니다.
따라서 런타임에서 빌드할 때는 옷감 유형에 따라 여러 가지 방법으로 정점 속성을 지정해야 합니다. 이러한 방법들을 예제를 통해 설명하겠습니다.
런타임 지연
MagicaCloth2는 런타임 중 옷감 데이터를 생성합니다. 이 과정은 별도의 스레드에서 수행되므로 메인 스레드에 미치는 영향은 적습니다. 그러나 데이터 구축에는 몇 프레임의 시간이 소요됩니다.
즉, 구성 요소를 빌드한 후 실제로 옷감 시뮬레이션이 시작되기까지 몇 프레임의 지연이 발생합니다.
예제
여기서는 런타임 구성 예제를 소개합니다. 이 예제는 샘플 씬 RuntimeBuildDemo.cs에서 발췌된 것이므로, 데모 씬도 함께 참고하는 것이 좋습니다. 사용된 API에 대한 자세한 내용은 ScriptingAPI 페이지를 확인하세요.
코드에서 "character"는 캐릭터의 GameObject를 의미합니다. 또한, "gameObjectContainer"는 이름을 통해 특정 GameObject를 가져올 수 있도록 하는 간단한 클래스입니다.
BoneCloth 구성 예제 (1)
///<summary>/// BoneCloth construction example (1)./// Set all parameters from a script.///</summary>voidSetupHairTail_BoneCloth()
{
if (character == null)
return;
var obj = new GameObject("HairTail_BoneCloth");
obj.transform.SetParent(character.transform, false);
// add Magica Clothvar cloth = obj.AddComponent<MagicaCloth>();
var sdata = cloth.SerializeData;
// bone cloth
sdata.clothType = ClothProcess.ClothType.BoneCloth;
sdata.rootBones.Add(gameObjectContainer.GetGameObject("J_L_HairTail_00_B").transform);
sdata.rootBones.Add(gameObjectContainer.GetGameObject("J_R_HairTail_00_B").transform);
// setup parameters
sdata.gravity = 3.0f;
sdata.damping.SetValue(0.05f);
sdata.angleRestorationConstraint.stiffness.SetValue(0.15f, 1.0f, 0.15f, true);
sdata.angleRestorationConstraint.velocityAttenuation = 0.6f;
sdata.tetherConstraint.distanceCompression = 0.5f;
sdata.inertiaConstraint.particleSpeedLimit.SetValue(true, 3.0f);
sdata.colliderCollisionConstraint.mode = ColliderCollisionConstraint.Mode.None;
// start build
cloth.BuildAndRun();
}
이 예제에서는 BoneCloth를 생성하고 모든 파라미터를 스크립트에서 직접 설정합니다. 가장 중요한 부분은 SerializeData 클래스로, 이 클래스에 포함된 속성을 조정하여 Cloth의 동작을 정의합니다.
이 예제에서는 별도의 정점 속성(Vertex Attributes)을 지정하지 않았습니다. 하지만 BoneCloth 및 BoneSpring에서는 지정된 루트 본(root bone)의 Transform이 자동으로 고정(Fixed) 속성으로 설정되며, 나머지는 변형(Translation) 속성으로 설정되므로 생략할 수 있습니다.
마지막으로 BuildAndRun()을 호출하여 Cloth 데이터를 별도의 스레드에서 생성하고 시뮬레이션을 자동 시작합니다.
BoneCloth 구성 예제 (2)
///<summary>/// BoneCloth construction example (2)./// Copy parameters from an existing component.///</summary>voidSetupFrontHair_BoneCloth()
{
if (character == null || frontHairSource == null)
return;
var obj = new GameObject("HairFront_BoneCloth");
obj.transform.SetParent(character.transform, false);
// add Magica Clothvar cloth = obj.AddComponent<MagicaCloth>();
var sdata = cloth.SerializeData;
// bone cloth
sdata.clothType = ClothProcess.ClothType.BoneCloth;
sdata.rootBones.Add(gameObjectContainer.GetGameObject("J_L_HairFront_00_B").transform);
sdata.rootBones.Add(gameObjectContainer.GetGameObject("J_L_HairSide2_00_B").transform);
sdata.rootBones.Add(gameObjectContainer.GetGameObject("J_L_HairSide_00_B").transform);
sdata.rootBones.Add(gameObjectContainer.GetGameObject("J_R_HairFront_00_B").transform);
sdata.rootBones.Add(gameObjectContainer.GetGameObject("J_R_HairSide2_00_B").transform);
sdata.rootBones.Add(gameObjectContainer.GetGameObject("J_R_HairSide_00_B").transform);
// Normal direction setting for backstop
sdata.normalAlignmentSetting.alignmentMode = NormalAlignmentSettings.AlignmentMode.Transform;
sdata.normalAlignmentSetting.adjustmentTransform = gameObjectContainer.GetGameObject("HeadCenter").transform;
// setup parameters// Copy from source settings
sdata.Import(frontHairSource, false);
// start build
cloth.BuildAndRun();
}
이 예제에서는 외부의 다른 Cloth 구성 요소인 "frontHairSource"에서 파라미터를 가져와 적용합니다. 이를 통해 파라미터 설정을 보다 간편하게 할 수 있습니다.
그러나 일부 파라미터는 가져올 수 있고, 일부는 가져올 수 없습니다. 이와 관련된 사항은 소스 코드의 주석으로 문서화되어 있습니다.
/// [OK] Runtime changes./// [NG] Export/Import with Presets
이 예제에서는 normalAlignmentSetting이 가져올 수 없는 항목이기 때문에 수동으로 설정하고 있습니다.
BoneCloth 구성 예제 (3)
///<summary>/// BoneCloth construction example (3)./// Load parameters from saved presets.///</summary>voidSetupRibbon_BoneCloth()
{
if (character == null || string.IsNullOrEmpty(ribbonPresetName))
return;
var obj = new GameObject("Ribbon_BoneCloth");
obj.transform.SetParent(character.transform, false);
// add Magica Clothvar cloth = obj.AddComponent<MagicaCloth>();
var sdata = cloth.SerializeData;
// bone cloth
sdata.clothType = ClothProcess.ClothType.BoneCloth;
sdata.rootBones.Add(gameObjectContainer.GetGameObject("J_L_HeadRibbon_00_B").transform);
sdata.rootBones.Add(gameObjectContainer.GetGameObject("J_R_HeadRibbon_00_B").transform);
// setup parameters// Load presets from the Resource folder.// Since presets are in TextAssets format, they can also be used as asset bundles.var presetText = Resources.Load<TextAsset>(ribbonPresetName);
sdata.ImportJson(presetText.text);
// start build
cloth.BuildAndRun();
}
이 예제에서는 **프리셋 파일(Json)**로부터 파라미터를 가져옵니다. 파라미터는 Json 형식으로 외부로 내보낼 수 있으며, 이는 단순한 텍스트 파일이므로 Resources 폴더 또는 AssetBundle에 배치하여 런타임 중 로드할 수 있습니다.
BoneCloth의 정점 속성(Vertex Attribute) 설정
BoneCloth를 구성할 때, 루트 본(root bone)은 자동으로 고정(Fixed) 속성으로 설정되며, 그 외의 본들은 자동으로 이동(Move) 속성으로 설정됩니다. 따라서 기본적으로 별도의 설정이 필요하지 않습니다.
그러나 ClothSerializeData2의 boneAttributeDict를 사용하여 수동으로 설정할 수도 있습니다. boneAttributeDict는 Transform과 속성(Vertex Attribute) 쌍으로 이루어진 딕셔너리(Dictionary)이며, 다음과 같이 지정할 수 있습니다.
///<summary>/// MeshCloth construction example (1)./// Reads vertex attributes from a paintmap.///</summary>voidSetupSkirt_MeshCloth()
{
if (character == null || skirtPaintMap == null)
return;
// skirt renderervar sobj = gameObjectContainer.GetGameObject(skirtName);
if (sobj == null)
return;
Renderer skirtRenderer = sobj.GetComponent<Renderer>();
if (skirtRenderer == null)
return;
// add Magica Clothvar obj = new GameObject("Skirt_MeshCloth");
obj.transform.SetParent(character.transform, false);
var cloth = obj.AddComponent<MagicaCloth>();
var sdata = cloth.SerializeData;
// mesh cloth
sdata.clothType = ClothProcess.ClothType.MeshCloth;
sdata.sourceRenderers.Add(skirtRenderer);
// reduction settings
sdata.reductionSetting.simpleDistance = 0.0212f;
sdata.reductionSetting.shapeDistance = 0.0244f;
// paint map settings// *** Paintmaps must have Read/Write attributes enabled! ***
sdata.paintMode = ClothSerializeData.PaintMode.Texture_Fixed_Move;
sdata.paintMaps.Add(skirtPaintMap);
// setup parameters
sdata.gravity = 1.0f;
sdata.damping.SetValue(0.03f);
sdata.angleRestorationConstraint.stiffness.SetValue(0.05f, 1.0f, 0.5f, true);
sdata.angleRestorationConstraint.velocityAttenuation = 0.5f;
sdata.angleLimitConstraint.useAngleLimit = true;
sdata.angleLimitConstraint.limitAngle.SetValue(45.0f, 0.0f, 1.0f, true);
sdata.distanceConstraint.stiffness.SetValue(0.5f, 1.0f, 0.5f, true);
sdata.tetherConstraint.distanceCompression = 0.9f;
sdata.inertiaConstraint.depthInertia = 0.7f;
sdata.inertiaConstraint.movementSpeedLimit.SetValue(true, 3.0f);
sdata.inertiaConstraint.particleSpeedLimit.SetValue(true, 3.0f);
sdata.colliderCollisionConstraint.mode = ColliderCollisionConstraint.Mode.Point;
// setup collidervar lobj = new GameObject("CapsuleCollider_L");
lobj.transform.SetParent(gameObjectContainer.GetGameObject("Character1_LeftUpLeg").transform);
lobj.transform.localPosition = new Vector3(0.0049f, 0.0f, -0.0832f);
lobj.transform.localEulerAngles = new Vector3(0.23f, 16.376f, -0.028f);
var colliderL = lobj.AddComponent<MagicaCapsuleCollider>();
colliderL.direction = MagicaCapsuleCollider.Direction.Z;
colliderL.SetSize(0.082f, 0.094f, 0.3f);
var robj = new GameObject("CapsuleCollider_R");
robj.transform.SetParent(gameObjectContainer.GetGameObject("Character1_RightUpLeg").transform);
robj.transform.localPosition = new Vector3(-0.0049f, 0.0f, -0.0832f);
robj.transform.localEulerAngles = new Vector3(0.23f, -16.376f, -0.028f);
var colliderR = robj.AddComponent<MagicaCapsuleCollider>();
colliderR.direction = MagicaCapsuleCollider.Direction.Z;
colliderR.SetSize(0.082f, 0.094f, 0.3f);
sdata.colliderCollisionConstraint.colliderList.Add(colliderL);
sdata.colliderCollisionConstraint.colliderList.Add(colliderR);
// start build
cloth.BuildAndRun();
}
이 예제에서는 MeshCloth를 구성하고, 정점 속성을 페인트 맵(Paint Map)으로 지정합니다. 페인트 맵은 설정한 렌더러와 동기화되어야 한다는 점에 유의하세요. 즉, 렌더러의 개수와 페인트 맵의 개수는 동일해야 합니다.
또한 두 개의 캡슐 콜라이더(Capsule Collider)를 생성하여 콜라이더 리스트에 추가했습니다. 콜라이더 생성은 Unity에서 일반적인 컴포넌트를 생성하는 방식과 동일합니다.
그 외의 과정은 BoneCloth 예제와 동일합니다.
MeshCloth의 정점 속성 직접 지정
BoneCloth와 달리 MeshCloth에서는 정점 속성을 명확하게 지정해야 합니다. 이를 생략할 수는 없습니다. MeshCloth의 정점 속성을 지정하는 방법은 두 가지가 있습니다. 첫 번째는 MeshCloth 구성 예제 (1)에서 설명한 페인트 맵(Paint Map)을 사용하는 방법, 두 번째는 정점 배열(Vertex Array)과 동일한 개수의 속성 배열을 준비하여 지정하는 방법입니다.
페인트 맵 대신 정점 속성 배열을 사용하려면 ClothSerializeData2의 vertexAttributeList를 사용하세요. 먼저, vertexAttributeList의 요소 개수는 MeshCloth에 등록된 렌더러(Renderer) 개수와 일치해야 합니다. 그리고 각 요소는 해당 렌더러의 메쉬 정점(Vertex) 개수와 동일해야 합니다.
다음은 페인트 맵 대신 vertexAttributeList를 사용하는 예제입니다. 이 예제는 하나의 렌더러가 있는 경우를 가정합니다.
// add vertex attributevar sdata2 = cloth.GetSerializeData2();
var attributes = new VertexAttribute[VertexCount];
// Initialize with movement attributesfor(int i = 0; i < VertexCount; i++)
attributes[i] = VertexAttribute.Move;
// Making a specific vertex a fixed attribute
attributes[0] = VertexAttribute.Fixed;
attributes[7] = VertexAttribute.Fixed;
attributes[21] = VertexAttribute.Fixed;
// Registering vertex attributes
sdata2.vertexAttributeList.Add(attributes);
MagicaCloth가 포함된 캐릭터를 런타임에 인스턴스화하면, MagicaCloth는 자동으로 초기화되며 작동을 시작합니다.
따라서 일반적으로 별도의 작업이 필요하지 않습니다.
그러나 초기화 과정에서 특정 상황에 따라 캐릭터의 자세 등에 제한이 가해질 수 있으며, 이는 MagicaCloth의 버전과 빌드 방식에 따라 다음 세 가지 유형으로 분류됩니다.
v2.13.0 이전 버전의 런타임 빌드
v2.13.0 이후 버전의 런타임 빌드
사전 빌드(Pre-built)
이제 각 방식의 차이점과 인스턴스화 내부 프로세스를 자세히 설명하겠습니다.
런타임 빌드와 사전 빌드 (Run-time and Pre-build)
MagicaCloth는 두 가지 방식으로 작동합니다: **런타임 빌드(Runtime Construction)**와 사전 빌드(Pre-construction). 일반적으로 런타임 빌드가 많이 사용됩니다.
런타임 빌드 (Runtime Construction)
런타임 빌드 방식에서는 Cloth 데이터가 런타임에 즉석에서 생성됩니다. 이 데이터 생성에는 평균적으로 10ms~30ms가 소요되며, 이 작업은 백그라운드에서 실행되므로 게임 진행에는 영향을 미치지 않습니다. 그러나 이 과정으로 인해 인스턴스화 후 Cloth 시뮬레이션이 실제로 시작되기까지 몇 프레임의 지연이 발생합니다.
사전 빌드 (Pre-built)
사전 빌드 방식에서는 Cloth 데이터를 편집 단계에서 미리 생성하여 에셋(Asset)으로 저장합니다. 이 방식은 런타임에 즉시 시뮬레이션을 시작할 수 있다는 장점이 있습니다. 그러나 에셋 데이터의 증가와 Cloth 데이터를 수동으로 빌드해야 하는 추가 작업이 필요하다는 단점이 있습니다. 자세한 내용은 사전 빌드(Pre-building) 문서를 참고하세요.
빌드 방식별 장단점 (Advantages and Disadvantages of Different Build Methods)
본격적인 설명에 앞서, 각 빌드 방식의 장점과 단점을 간략하게 정리합니다. 특히, v2.13.0 이후 런타임 빌드는 성능이 크게 개선되어 이전의 단점이 많이 줄어들었습니다.
장점(Pros)
단점(Cons)
v2.13.0 이전 런타임 빌드
MagicaCloth 버전 변경, 메쉬 또는 본(Bone) 구조 변경에도 영향 없음. 파라미터가 변경되어도 특별한 작업 없이 적용 가능.
초기화 부하가 높음. 데이터 빌드 시간이 필요하여 시뮬레이션 시작 전 지연 발생. 캐릭터가 편집 당시의자세 및 스케일로 초기화되어야 함.
v2.13.0 이후 런타임 빌드
MagicaCloth 버전 변경, 메쉬 또는 본 구조 변경에도 영향 없음. 파라미터 변경 시 별도의 작업이 필요 없음. 초기화 시 캐릭터의 자세와 스케일이 자유로움. 초기화 부하가 낮음.
데이터 빌드 시간이 필요하여 시뮬레이션 시작 전 지연 발생.
사전 빌드 (Pre-built)
초기화 비용이 낮고, 시뮬레이션이 즉시 시작됨. 초기화 시 캐릭터의 자세와 스케일이 자유로움.
Cloth 데이터를 생성하는 추가 작업 필요. MagicaCloth 버전, 메쉬, 본 구조가 변경되면 Cloth 데이터를 다시 생성해야 함. Cloth 파라미터가 변경될 때마다 데이터를 다시 빌드 해야 함.
v2.13.0 이전 런타임 빌드 (Runtime Build for v2.13.0 and Earlier)
v2.13.0 이전 버전에서는 캐릭터가 편집 당시와 동일한 자세로 초기화되어야 합니다. 일반적으로 이는 A 포즈 또는 T 포즈입니다. 초기화 시 편집할 때와 다른 자세가 적용되면 버텍스 속성 데이터가 어긋나 시뮬레이션이 정상적으로 작동하지 않을 수 있습니다.
특히 다음 항목은 편집할 때와 동일해야 합니다.
캐릭터 자세 (Character Posture, Animation Pose)
캐릭터 스케일 (Character Scale)
좌표(Position) 및 회전(Rotation)은 변경해도 문제가 없습니다. 만약 인스턴스화 직후 캐릭터의 자세를 변경하려면, 변경 전에 수동으로 초기화를 호출해야 합니다. 이에 대한 방법은 문서의 마지막에서 설명됩니다.
v2.13.0 이후 런타임 빌드 (Runtime Build for v2.13.0 and Later)
v2.13.0부터는 편집 시 캐릭터의 자세가 자동으로 저장됩니다. 이 데이터를 **초기화 데이터(Initialization Data)**라고 합니다.
이전에는 편집할 때와 초기화할 때 자세가 동일해야 했지만, v2.13.0 이후 버전에서는 이 제한이 제거되었습니다. 즉, 초기화 전에 캐릭터의 자세나 스케일을 변경하는 것이 가능합니다.
초기화 데이터 확인 (Check the Initialization Data)
컴포넌트가 초기화 데이터를 포함하고 있는지 여부는 Inspector의 Info 창에서 확인할 수 있습니다.
[Init Data]가 True이면, 해당 컴포넌트가 초기화 데이터를 보유하고 있음을 의미합니다.
Inspector의 Info 창에서는 런타임에서 초기화 데이터가 사용되었는지도 표시됩니다.
"Success"라고 표시되면, 빌드가 완료될 때 초기화 데이터가 올바르게 사용된 것입니다.
숫자와 메시지가 표시되면, 오류가 발생한 것이므로 초기화 데이터를 다시 생성해야 합니다.
초기화 데이터 생성 (Creating Initialization Data)
초기화 데이터는 자동으로 생성되므로 기본적으로 별도의 작업이 필요하지 않습니다. 그러나 어떤 이유로 초기화 데이터가 올바르게 생성되지 않은 경우, 다음 방법을 사용하여 수동으로 생성할 수 있습니다.
Vertex Paint를 한 번 실행한 후 즉시 종료하면 초기화 데이터가 생성됩니다.
또한, 컴포넌트 메뉴에서 "Rebuild InitData"를 선택하면 초기화 데이터를 강제로 다시 빌드할 수 있습니다.
v2.13.0 이전 컴포넌트에서 초기화 데이터를 활성화하는 방법
v2.13.0 이전의 컴포넌트에는 초기화 데이터가 포함되어 있지 않지만, 이를 쉽게 호환 가능하게 만들 수 있습니다. 아래 방법 중 하나를 수행하면 됩니다.
캐릭터가 프리팹(Prefab)으로 구성된 경우, 한 번 Prefab 모드로 들어가기
캐릭터를 씬(Scene)에 한 번 배치하기
위 방법을 수행하면 새로운 초기화 데이터가 자동으로 생성됩니다.
초기화 위치 변경 (Change the Initialization Location)
초기화는 기본적으로 MonoBehaviour의 Start()에서 수행되지만, 이를 Awake()에서 실행하도록 변경할 수 있습니다. 이 설정을 변경하려면 MagicaSettings 컴포넌트를 사용하면 됩니다.
바람 생성 기능을 사용하면 Cloth 시뮬레이션에 바람을 추가하여 보다 현실적인 표현이 가능합니다.
이는 MagicaWindZone 컴포넌트를 씬(Scene)에 배치하여 바람이 생성될 영역을 지정함으로써 제어됩니다.
또한 [Wind] 패널에서 설정을 조정하여 MagicaCloth 컴포넌트에 미치는 바람의 영향을 변경할 수도 있습니다.
샘플 씬 (Sample Scene)
바람 기능을 위한 샘플 씬이 제공되며, 해당 샘플은 다음 폴더에서 확인할 수 있으며, 자신의 렌더 파이프라인(Render Pipeline)에 맞는 씬을 선택하여 테스트하면 됩니다.
샘플 씬의 WindDemo에는 WindDemo.cs가 첨부되어 있으며, 이는 바람 기능을 테스트하는 코드입니다.
씬이 각각의 렌더 파이프라인에 따라 분리되어 있는 이유는 렌더링 머티리얼(Material)을 변경하기 위함이며, 포함된 샘플 코드는 모두 동일합니다.
윈드 존 설정 (Wind Zone Settings)
바람을 생성하려면 씬에서 바람의 범위를 지정해야 하며, 이를 위해 MagicaWindZone 컴포넌트를 사용합니다. 주의: Unity의 기본 WindZone 컴포넌트는 MagicaCloth에 영향을 미치지 않으므로, 반드시 전용 MagicaWindZone 컴포넌트를 추가해야 합니다.
MagicaWindZone 컴포넌트는 오른쪽 클릭 메뉴를 통해 추가할 수 있습니다.
바람 영역(Wind Zone)은 [Mode] 설정에 따라 여러 유형으로 나뉘며,
각 모드에 따라 설정 항목이 다르므로 이를 설명합니다.
글로벌 방향 (Global Direction)
씬 전체에 영향을 미치는 방향성 바람으로, 범위 제한이 없습니다.
Main
바람 속도 (m/s)
Turbulence
바람 난류(Turbulence) 비율
Direction Angle X
바람 방향 X축 각도 (Deg)
Direction Angle Y
바람 방향 Y축 각도 (Deg)
Is Addition
추가 방식(Addition Style) 플래그
구형 방향 (Sphere Direction)
구형 영역 내에서만 영향을 미치는 방향성 바람입니다.
Radius
바람이 영향을 미치는 영역 반경
Main
바람 속도 (m/s)
Turbulence
바람 난류(Turbulence) 비율
Direction Angle X
바람 방향 X축 각도 (Deg)
Direction Angle Y
바람 방향 Y축 각도 (Deg)
Is Addition
추가 방식(Addition Style) 플래그
박스 방향 (Box Direction)
박스 형태의 영역 내에서만 영향을 미치는 방향성 바람입니다.
Box Size
바람이 영향을 미치는 영역의 XYZ 크기
Main
바람 속도 (m/s)
Turbulence
바람 난류(Turbulence) 비율
Direction Angle X
바람 방향 X축 각도 (Deg)
Direction Angle Y
바람 방향 Y축 각도 (Deg)
Is Addition
추가 방식(Addition Style) 플래그
구형 방사형 (Sphere Radial)
구형 영역 내에서 중심에서 바깥쪽으로 퍼지는 방사형 바람입니다.
방사형 바람은 중심에서 바깥 방향으로 발생하므로 별도의 방향 지정이 필요 없습니다.
[Attenuation] 설정을 통해 중심에서 바깥쪽으로의 바람 강도 감쇠 곡선을 지정할 수 있습니다.
Radius
바람이 영향을 미치는 영역 반경
Main
바람 속도 (m/s)
Turbulence
바람 난류(Turbulence) 비율
Attenuation
바람의 감쇠(Attenuation) 곡선
Is Addition
추가 방식(Addition Style) 플래그
바람 존 우선순위 (Wind Zone Priority)
여러 개의 바람 존(Wind Zone)이 하나의 Cloth 컴포넌트에 겹쳐질 경우, 볼륨(Volume)이 가장 작은 바람 존만 활성화됩니다. 그러나 예외적으로 Global Direction 존은 볼륨 ∞(무한)으로 간주되어 가장 낮은 우선순위를 가집니다. 단, 이후 설명할 [IsAddition] 플래그가 활성화된 경우, 이 우선순위 규칙에서 제외됩니다.
IsAddition 플래그 (IsAddition Flag)
[IsAddition] 플래그를 활성화하면, 해당 바람 존이 '추가적(Additive)'으로 처리됩니다.
추가적 바람 존은 기존의 바람 존 우선순위 처리에서 제외되며, 동시에 여러 개의 바람이 Cloth에 영향을 줄 수 있습니다.
이 경우 바람이 하나에서 다른 것으로 전환되는 것이 아니라, 여러 개의 바람이 합산되어 적용됩니다.
단, 하나의 Cloth 컴포넌트에는 최대 3개의 추가적 바람(Additional Wind)만 적용할 수 있습니다.
MagicaCloth 설정 (MagicaCloth Settings)
각 Cloth 컴포넌트마다 바람 효과를 개별적으로 조정할 수 있으며, 이를 위해 [Wind] 패널을 사용합니다.
Influence
전체적인 바람 영향력 비율을 조정합니다. 값을 낮추면 바람의 영향을 약화시킬 수 있으며, 1.0(100%) 이상의 값도 설정 가능합니다.
Frequency
흔들림의 주기를 조정합니다. 값이 클수록 흔들림의 속도가 증가하며, 1.0(100%) 이상의 값도 설정 가능합니다.
Turbulence
바람의 난류(Turbulence) 비율을 조정합니다. 값이 클수록 흔들림이 더욱 불규칙해지며, 1.0(100%) 이상의 값도 설정 가능합니다.
Noise Blend
사인(Sine) 파형과 노이즈(Noise) 파형의 혼합 비율을 조정합니다. 두 파형은 이후 설명됩니다.
Synchronization
동기화 비율을 조정합니다. 1.0으로 설정하면 Cloth의 모든 부분이 동일한 방식으로 움직이며, 0.0으로 설정하면 각각 독립적으로 움직입니다.
Depth Weight
깊이 효과를 조정합니다. 값을 높이면 시작점(예: 허리 근처)의 버텍스가 바람의 영향을 덜 받도록 하여 스커트, 앞머리 등의 안정성을 향상할 수 있습니다.
Moving Wind
움직임에 따른 바람 효과를 자동으로 생성합니다. 값이 클수록 움직임에 따른 바람 효과가 더욱 강해집니다.
사인파(Sine Wave)와 노이즈파(Noise Wave)
바람의 변동을 표현하기 위해 사인(Sine) 파형과 노이즈(Noise) 파형 두 가지를 사용합니다.
사인(Sine) 파형은 일정한 주기로 바람이 변동하므로 일본 애니메이션과 같은 장면 연출에 적합합니다. 하지만 랜덤성이 부족한 단점이 있습니다.
노이즈(Noise) 파형은 불규칙한 주기를 가지며 보다 자연스럽고 현실적인 움직임을 생성할 수 있습니다.
이 두 가지 파형은 [Noise Blend] 값을 조정하여 혼합할 수 있습니다.
움직이는 바람 (Moving Wind)
[Moving Wind] 값을 증가시키면 캐릭터의 움직임에 따라 자동으로 바람이 생성됩니다.
움직이는 바람 효과는 관성(Inertia) 매개변수의 영향을 받습니다.
즉, 관성(Inertia) 설정을 통해 이동 영향이 감소하면, 움직이는 바람 효과도 함께 줄어듭니다.
튜닝 팁 (Tuning Tips)
바람 존(Wind Zone)의 바람 속도는 최대 30m/s까지 설정 가능하지만, 너무 강한 바람은 충돌 감지 시 튜널링(Tunneling) 문제를 유발할 수 있으므로 주의해야 합니다.
특정 Cloth 컴포넌트에서 바람 효과를 줄이고 싶다면 [Influence] 값을 조정하세요.
스커트가 허리에서 너무 많이 흔들리지 않도록 하려면 [Depth Weight] 값을 증가시키세요.
머리카락이 불규칙하게 흔들리도록 하려면 [Synchronization] 값을 낮추세요.
흔들림의 랜덤성을 조정하려면 [Noise Blend]와 함께 [Synchronization] 값도 조정하세요.
움직이는 바람(Moving Wind) 효과는 관성(Inertia) 매개변수 감소의 영향을 받습니다.
강한 돌풍 효과를 만들고 싶다면, [Sphere Radial] 모드로 설정하고 [IsAddition] 플래그를 활성화하는 것이 효과적입니다.