GPU Skinning 기술 구현
요약
GPU Skinning은 캐릭터 애니메이션의 스키닝 작업을 CPU에서 GPU로 전송하여 처리하는 기술입니다. 이를 통해 다수의 캐릭터가 등장하는 대규모 게임 환경(MMO, RTS)에서 발생하는 성능 오버헤드를 크게 줄일 수 있습니다. 핵심은 뼈대 변환 정보를 Texture2D에 담아 Shader가 직접 스키닝을 수행하도록 하는 것입니다.
핵심 포인트
- CPU 부하를 줄이고 GPU 활용도를 높여 애니메이션 효율 개선
- 다수 캐릭터 장면에 필수적인 기술로, MMO/RTS 게임에 적용 가능
- 뼈대 변환 정보를 Texture2D에 담아 Shader가 스키닝을 수행하도록 함
- GPU Instancing 사용으로 Draw Call 오버헤드도 함께 해결
GPU Skinning
CPU의 부하를 줄이고 GPU를 활용하여 캐릭터 애니메이션 효과를 구현하는 기술적 방법론
장면에 많은 인물 애니메이션 모델이 있을 경우, 성능에 막대한 오버헤드가 발생하며, 이 중 상당 부분은 뼈대(Bone) 애니메이션에서 기인합니다. GPU Skinning은 CPU의 스키닝(Skinning) 작업을 GPU로 전송하여 처리함으로써 다수의 캐릭터가 등장하는 장면에 대한 실행 효율을 크게 향상시킬 수 있습니다. 이는 MMO, RTS와 같은 대규모 집단 애니메이션 시뮬레이션 게임에 큰 적용 가능성을 가집니다.
발견할 수 있듯이, 렌더링해야 할 애니메이션 캐릭터의 수가 많을 때 주로 다음과 같은 두 가지 거대한 오버헤드가 발생합니다:
- CPU가 애니메이션을 처리하는 과정에서의 오버헤드.
- 각 캐릭터당 발생하는 Draw Call로 인한 오버헤드.
CPU의 이 두 가지 주요 오버헤드는 전통적인 방식으로 대규모 캐릭터를 렌더링할 가능성을 제한하며, 주된 병목 현상은 캐릭터 애니메이션 처리가 CPU에 집중되어 있다는 점입니다. 두 번째 병목은 CPU와 GPU 사이의 Draw Call 문제입니다. 이는 배치 처리(Static Batching 및 Dynamic Batching 포함)나 Unity 5.4 이후 도입된 GPU Instancing을 활용하여 해결할 수 있습니다. 하지만 불행하게도 이 두 가지 기술 모두 애니메이션 캐릭터의 SkinnedMeshRender를 지원하지 않습니다.
그렇다면 해결책은 명확해집니다. 바로 애니메이션 관련 콘텐츠를 CPU에서 GPU로 전송하는 것입니다. 또한, CPU가 더 이상 애니메이션 로직을 처리할 필요가 없기 때문에, CPU는 이 부분의 오버헤드를 절약할 뿐만 아니라 SkinnedMeshRender도 일반적인 Mesh Render로 대체될 수 있습니다. 따라서 우리는 GPU Instancing을 사용하여 Draw Call을 줄이는 것을 기쁘게 사용할 수 있게 됩니다.
목표가 SkinnedMeshRender를 제거하는 것이므로, 그 역할은 스키닝(Skinning)입니다. 스키닝에 필요한 핵심 정보는 다음과 같습니다:
- 각 정점(Vertex)이 연결된 뼈대 정보 (Bone Index와 Bone Weight)
- 프레임별 뼈대의 변환 정보 (프레임당 각 뼈대의 변환)
따라서 이 두 가지 정보를 GPU로 전달하고, GPU를 통해 뼈대를 계산하며 스키닝을 수행하는 방법을 고안해야 합니다. 아래는 간단한 단계 설명입니다;
1. 렌더링된 뼈대 행렬을 재질에 담기 : CreateBoneTex
각 프레임의 각 뼈대의 변환을 Matrix4X4 행렬로 변환하고, 이 변환 정보를 RGBA 값으로 만들어 재질(Texture2D)에 전달하여 Shader가 스키닝을 수행하도록 할 수 있습니다. 여기서 뼈대 변환 행렬을 얻는 방법은 다음과 같습니다:
// 현재 프레임을 샘플링
clip.SampleAnimation(gameObject, i / clip.frameRate);
...
뼈대 변환의 원리는 아래 그림과 같습니다:

행렬 정보를 얻은 후, 이를 Texture2D로 렌더링합니다. 정밀도를 유지하기 위해 EncodeFloatRGBA 함수를 사용하여 각 float 값을 RGBA 공간상의 한 점으로 변환한 다음, 이를 하나씩 Texture2D 위에 렌더링할 수 있습니다. 이렇게 렌더링된 이미지(일부만 표시됨)는 다음과 같습니다:

각 프레임의 각 뼈대는 12번 샘플링되어 재질 내에 12개의 Pixel로 저장됩니다.
2. 뼈대 인덱스 정보와 가중치 정보를 Mesh의 UV 채널에 추가하기 : MappingBoneIndexAndWeightToMeshUV
비록 우리가 프레임별 뼈대의 변환 정보는 갖게 되었지만, 아직 한 가지가 부족합니다. 바로 특정 정점이 어떤 뼈대에 영향을 받는지 그리고 그 정도가 어느 정도인지에 대한 정보입니다. 하지만 자세히 생각해보면 이 인덱스와 가중치는 Mesh의 정적 데이터라는 것을 알 수 있습니다.
Mesh가 정적이므로, 이를 Mesh 자체에 직접 작성하는 것이 가장 좋습니다. 가장 일반적인 방법은 UV 채널을 이용하는 것입니다. UV는 Vector2 형태의 벡터이므로 한 번에 하나의 쌍의 가중치 인덱스 데이터만 저장할 수 있습니다. 만약 더 높은 정밀도를 요구한다면 2개의 UV 채널 또는 4개의 UV 채널을 사용할 수 있습니다.
// 원래 Mesh를 깊은 복사(Deep Copy)
var bakedMesh = new Mesh();
bakedMesh = Instantiate(mesh);
...
3. 스킨 정보를 Shader에서 병합 (기존 CPU 스킨 대체): BoneAnimationShader
Shader에서 픽셀 단위로 디코딩하기 전에 베이킹된 BoneMatrix Material은 현재 정점이 두 개의 골격의 영향을 받으며, 이들의 행렬 정보를 추출하고 조립합니다:
// 첫 번째 골격이 BoneMatrix2Texture2D에서 위치를 찾는 코드
float total = (y * _BoneCount + (int)(index.x)) * 12;
float4 line0 = readInBoneTex(total);
...
하나의 정점이 두 개의 골격 가중치 제한을 받기 때문에, 해당 정점에 대한 스킨 연산 코드(Vertex Shader)는 다음과 같습니다:
// 인덱스와 가중치를 가져옴
float2 index = v.iuv;
float2 weight = v.wuv;
...
GPU Instancing
데이터를 한 번에 GPU로 전송하고, 하나의 드로우 함수를 사용하여 렌더링 파이프라인이 이 데이터를 이용해 여러 개의 동일한 오브젝트를 그리도록 하는 기술
GPU Skinning 기술을 통해 CPU의 스킨 작업을 GPU에서 실행하도록 옮기자 Unity 씬의 실행 효율성이 상당히 높아졌습니다. 우리는 또한 GPU Instancing 기술을 도입하여 추가적으로 최적화할 수 있습니다.
현재 캐릭터 하나를 렌더링할 때마다 CPU와 GPU가 한 번씩 통신해야 합니다. CPU는 GPU에게 캐릭터의 Mesh, Material 및 위치 정보를 알려주고, GPU는 지침에 따라 렌더링을 담당합니다. 하지만 문제는 명확합니다. 우리가 많은 수의 동일한 캐릭터를 렌더링하기 때문에, CPU가 GPU에게 전달하는 정보 중 상당 부분(Mesh & Material)은 동일하고 오직 위치 정보만 다릅니다. 만약 CPU가 한 번에 렌더링해야 할 모든 위치 정보를 GPU에게 알려주어 불필요한 통신을 줄일 수는 없을까요? 바로 여기서 GPU Instancing이 등장했습니다. 이 기술에는 두 가지 구현 방식이 있습니다:
Auto GPU Instancing
Shader에 키워드를 추가하여 Unity가 제공하는 자동 GPU Instancing 기능을 활성화합니다:
#pragma multi_compile_instancing
만약 GPU Instancing이 활성화된 Shader에서 Properties의 값(예: .SetFloat())을 동적으로 조정해야 한다면, 버퍼를 설정해야 합니다:
Properties
{
// 변경해야 할 파라미터
...
설정이 완료되면 Shader 인터페이스에서 GPU Instancing을 체크하여 자동 인스턴스화를 활성화할 수 있습니다.
- 장점:
- 일반적으로 사용되는 GameObject + MeshFilter 컴포넌트 + MeshRenderer 컴포넌트 형식을 사용하여 관리하기 용이합니다;
- 개별 캐릭터의 MaterialPropertyBlock을 독립적으로 제어할 수 있어 Shader 속성 변경이 편리합니다;
- 코드 양이 적고 오류가 발생하기 어렵습니다;
- 단점:
- 커스터마이징 공간이 거의 없어 유연성이 떨어집니다;
- 컴포넌트 기반이라 추가적인 메모리 오버헤드가 발생합니다.
Manual GPU Instancing
Unity3D는 Update 함수 내에서 Graphics.DrawMeshInstanced를 호출하여 배치 렌더링을 구현할 수 있습니다. 하지만 먼저 각 Batch에서 렌더링되는 오브젝트들을 관리해야 합니다:
public class RenderObjectData
{
// GPU가 오브젝트의 위치를 알아야 함
...
그리고 각 Batch를 관리합니다:
public class BatchData
{
//이 배치에 포함된 모든 오브젝트의 위치
...
모든 렌더링할 오브젝트를 추가하고, BatchData가 이를 관리합니다:
int batchIndexNum = 0;
List<RenderObjectData> currBatch = new List<RenderObjectData>();
for (int r = 0; r < row; r++)
...
모든 배치를 마지막으로 렌더링합니다:
private void Update()
{
if(GPUInstancingMode == GPUINSTANCINGMODE.Manual)
...
이 과정을 마치면 수동 GPU Instancing을 구현할 수 있습니다.
- 장점:
- GameObject를 생성하지 않아 메모리 공간을 절약합니다;
- 완벽하게 커스터마이징 가능하며, 고급 사용자에게 필수적입니다;
- 단점:
- 코드량이 많고 로직이 비교적 복잡하여 오류가 발생하기 쉽습니다;
- 같은 배치(batch)에는 하나의 MaterialPropertyBlock만 사용할 수 있어 관리가 불편합니다;
세부 사항
계속 보충 중......
AI 자동 생성 콘텐츠
본 콘텐츠는 GitHub GameDev의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기