Unity 의 Skinned Mesh Renderer component 에서 기존의 방식과 달라진 점은 GPU 에서 정점 변환을 병렬화하기 위해 Compute Dispatches를 활용한다는 점입니다. 각 스키닝된 메시마다 별도의 Compute Dispatch를 실행하여 그 메시의 정점 변환을 GPU에서 병렬로 처리합니다. 추가로 Blendshape는 별도의 Compute Shader를 통해 이루어지며, 이 작업은 직렬화된 방식으로 진행됩니다. 이어지는 항목에서 보다 자세하게 설명하도록 하겠습니다.
기존 방식에 대한 설명과 함께 각 방식들이 어떻게, 다른지, 왜 새로운 방법을 고안하게 되었는지 조금 더 자세하게 설명해보겠습니다. 이 단락에서는 이론적인 설명 위주로 진행하고, 그 다음 항목에서는 샘플 프로젝트를 통해 각각의 방식에 대한 동작 차이를 프로파일러 결과와 함께 살펴보도록 하겠습니다.
Skinned Mesh Rendering은 캐릭터 모델과 같이 애니메이션이 포함된 메시를 렌더링하는 기술로 주로 캐릭터 애니메이션에 사용됩니다. Skinning 은 특히 많은 수의 애니메이션 캐릭터나 복잡한 메시를 다룰 때 주요한 성능 병목을 일으킬 수 있습니다. 이 경우, 뼈와 가중치에 따라 스키닝 메시의 각 정점을 변형하는 프로세스는 주로 CPU에서 처리됩니다.
그리고 Skinned Mesh Renderer를 사용한 렌더링은 GPU에서 실행되는데, 많은 수의 애니메이션 캐릭터나 복잡한 메시를 다루는 경우 성능 병목의 원인이 됩니다.
일반적으로 스키닝 과정 중 모든 정점과 blend shape를 변환하는 것은 CPU 의 역할로, 작은 모델에서는 GPU 의 제한을 받지 않으므로 본 수나 메시 복잡성에 있어 상대적으로 유용하다는 장점이 있습니다. 하지만 CPU의 처리 능력에 따라 성능이 제한될 수 있으며 대규모 애니메이션을 처리할 때에는 병목 현상이 발생할 수 있다는 단점이 있습니다.
이렇게 GPU 기반의 Skinning이 CPU 기반의 Skinning 에 비해 크게 개선되었으나, 여전히 최적화의 여지가 있습니다.
GPU Skinning에서는 각 Blendshape 와 Skinning 된 메시 렌더러에 대한 컴퓨트 셰이더를 합니다. 이는 대량의 데이터 처리를 병렬로 수행하고 복잡한 애니메이션을 더 빠르게 처리할 수 있는 방법이지만, 특정한 작업에 최적화되어있어 복잡한 로직이나 동적으로 변경이 필요할 때에는 유연성이 떨어집니다. 복잡한 캐릭터의 경우 이로 인해 많은 Dispatch 와 GPU Barrier 가 발생하여 GPU 효율성이 저하될 수 있습니다.
Blendshapes 는 실제 skinning Dispatch 전에 별도의 Dispatch 시리즈로 처리됩니다. 각 Dispatch의 output이 다음 Dispatch 의 Input이기 때문에 다음 Active Blendshape 를 적용하기 전에 Memory Barrier 가 삽입되어야 합니다.
Dispatch 사이에 요구되는 동기화를 제거하는 간단한 방법은 없으며, skinning Dispatch 코드 내부로 Blendshape Dispatch 코드를 옮기는 것은 최적이 아닐 수 있습니다. 대신 Dispatch 순서를 조정하여 barrier 수를 줄일 수 있으며, 여기에서 batching 을 통해 최적화하는 아이디어를 얻게 되었습니다.
예를들어 4개의 active blendshape와 하나의 skin 변형을 가진 메시들이 있다고 가정해봅니다.
{A_blend0, A_blend1, A_blend2 A_blend3, A_Skin}
{B_blend0, B_blend1, B_blend2, B_blend3, B_Skin}
{C_blend0, C_blend1, C_blend2, C_blend3, C_Skin}
{D_blend0, D_blend1, D_blend2, D_blend3, D_Skin}
이 때, 이 메시들이 실행되면 다음과 같이 동작하게 됩니다.
20개의 Dispatches 와 16개의 barriers 가 생기는 것을 볼 수 있습니다.
이 순서를 아래와 같이 변경할 수 있습니다.
이 경우 5개의 Dispatches 와 4개의 barriers 로 줄일 수 있습니다.
앞서 확인한 예시처럼 Batched Compute Skinning 은 스키닝 계산을 GPU에서 수행하여 많은 수의 메시를 한번의 GPU 호출로 처리할 수 있습니다. 이는 GPU에서 스키닝을 한번에 처리하여 CPU의 부담을 줄이고 GPU의 리소스를 효율적으로 사용하는 방법입니다. 또한 이 구성은 가능한 많은 Skinning Dispatch 를 배치하여 병렬로 변형되는 정점의 양을 늘리고 GPU 동기화를 줄입니다. 결과적으로 모든 플랫폼에서 Skinned Mesh Renderer 의 GPU 성능이 향상될 수 있습니다.
GPU Skinning 방법은 Project Settings - Player 의 Other Settings 에서 설정할 수 있습니다. Batched Compute Skinning 은 GPU(Batched) 를 선택하면 적용됩니다.
실제로 Batched Compute Skinning 을 사용했을 때 어느 정도의 차이가 있는지 확인하기 위해 blendshape 를 사용하는 Skinned 캐릭터 포함된 샘플 프로젝트로 테스트를 진행했습니다.
에셋 스토어에서 무료로 다운받은 캐릭터들로 프로젝트를 구성하였습니다. 아래 링크에서 확인 가능합니다.
https://assetstore.unity.com/packages/3d/characters/aika-highschool-uniform-221860
https://assetstore.unity.com/packages/3d/characters/aika-sailor-uniform-222398
https://assetstore.unity.com/packages/3d/characters/akio-highschool-uniform-217443
https://assetstore.unity.com/packages/3d/characters/akio-sailor-uniform-222295
캐릭터들은 Standard Unity avatars 이며 표준 캐릭터 애니메이션을 적용할 수 있습니다. 이 캐릭터 애니메이션은 아래 표준 에셋 패키지에서 사용했습니다.
* 이 결과는 프로젝트에서 구현한 내용이나, 측정 환경 등에 따라 성능 차이가 있을 수 있습니다.
4 standard animations (idle, stand quarter turn left, stand quarter turn right, stand half turn right)
4 blendshapes animations ( 6 channels 185 frames, 10 channels 306 frame, 8 channels 256 frames, 11 channels 286 frames)
build : Windows Standalone
Graphics API : Direct3D11
GPU : Alienwarem 15r5 laptop - NVIDIA GeForce RTX 3070 Laptop GPU
CPU : 11th Gen Intel Core i7 - 11800H @2.3GHz
Skinning 방식에 따른 성능 차이 결과는 CPU <<< GPU < GPU(Batched) 로, CPU Skinning 보다는 GPU Skinning 이 훨씬 효율적이고, GPU Skinning 보다 batched GPU Skinning 이 조금 더 효율적으로 동작한다는 것을 확인할 수 있었습니다.
GPU skinning profiling
GPU Skinning 과 GPU(Batched) 를 프로파일링했을 때, Skinning 부분에서 호출되는 함수의 차이를 일부 확인할 수 있었습니다. GPU Skinning 일 때는 MeshSkinning.SkipOnGPU 에서 짧지만 다수의 SkipOnGPUNonBatched 를 호출하며 이 부분에서 2.63ms 가 소요되었는데, GPU Batched 에서는 MeshSkinning.SkipOnGPU에서 배치된 Blendshape 를 한번에 처리하여 0.78ms 로 시간이 크게 줄어든 것을 확인할 수 있었습니다.
GPU Skinning 에서는 다수의 Dispatch들로 나뉘어 각각의 명령이 다음 명령을 기다려야 해당 결과물을 입력으로 받아 다시 처리할 수 있게 되기 때문에 다수의 Memory barrier가 발생합니다. 이와는 다르게 GPU Batched 에서는 이 Blendshapes 들을 batch 하여 함께 처리할 수 있도록 변경하여 훨씬 적은 Memory barrier 가 발생한 것입니다.
이러한 개선 결과로 RenderingLoop 에서 걸리는 시간도 약 1ms 정도가 줄어든 것을 볼 수 있습니다.
GPU batched skinning profiling
CPU Skinning 에서는 전체적으로 시간이 오래 소요되고 있는 결과를 확인할 수 있었는데, Main Thread 에서 Skinning 정보를 업데이트하고나면 실제 Skinning 과 관련하여 정점을 변환하는 동작은 Job Thread 에서 이루어집니다. CPU 에서도 이러한 동작을 병렬로 나누어 이 작업에는 12.25ms 가 소요되었지만 이 작업시간 동안 Render Thread 에서는 다른 작업을 수행하지 않고 기다리다가 모든 동작이 끝난 뒤에 그리는 작업을 시작하는 것을 볼 수 있습니다. 변환된 정점들을 그리기 위해 CPU 에서 이 정보들을 받아와야 하므로 GPU 에서 정점 변환을 포함한 작업을 포함한 것보다 시간이 더 소요됩니다.
CPU skinning profiling
이 테스트 환경에서는 다수의 애니메이션과 Blendshape 를 사용하여 확인을 했기 때문에 일반적인 시나리오와는 차이가 있을 수 있습니다. 하지만 이 결과는 여러 애니메이션을 사용할 때, 적절하게 배치할 수 있는 대상이 있다면 Batched Compute Skinning 을 사용했을 때 더 좋은 성능 결과를 확인할 수 있다는 것을 보여줍니다. 추가로 Skinning 방식에 따라 리소스의 사용이나 엔진의 동작이 얼마나 달라지는지도 확인할 수 있었습니다.
다만 이 결과는 CPU 의 리소스를 사용하는 대신 GPU 의 리소스를 사용하게 되므로, GPU 의 리소스에 충분한 여유가 없는 상황에서는 성능에 크게 이득이 되지 않을 수 있습니다. 실제로 동일한 작업 환경에서 GPU 리소스를 사용하고 있는 다른 프로그램을 함께 수행하는 경우, 혹은 모바일 환경에서는 Skinning 타입 별 성능에 크게 차이가 없거나 GPU 를 사용하는 Skinning 에서 성능이 급격하게 떨어지는 상황도 확인할 수 있었습니다. 이 때문에 Skinning 방법을 결정하기 위해서는, 우선 현재 프로젝트에서 CPU, GPU 중 어느 리소스를 더 많이 사용하고 있고, 타겟 디바이스에서 어느 정도까지 리소스를 사용할 수 있는지에 대해 검토가 필요합니다.