파티클과 애니메이션 (2) - 애니메이션 최적화 - soo:bak
작성일 :
파티클에서 애니메이션으로
Part 1에서 파티클 시스템의 비용 구조를 다루었습니다. 파티클 시스템은 시각 효과를 위한 서브시스템이었고, 반투명 오버드로우가 모바일에서 가장 큰 비용이었습니다.
애니메이션 시스템은 캐릭터와 오브젝트의 움직임을 위한 서브시스템입니다. 캐릭터가 걷고, 공격하고, 피격 반응을 보이는 것이 모두 애니메이션입니다. 문이 열리거나, UI 요소가 슬라이드하거나, 환경 오브젝트가 흔들리는 것도 애니메이션으로 구현됩니다.
애니메이션은 매 프레임 CPU에서 처리됩니다. 재생 중인 애니메이션 클립의 현재 시간에 해당하는 값을 읽어, 오브젝트의 Transform(위치, 회전, 크기)이나 프로퍼티(색상, 알파 등)를 갱신하는 과정입니다. 캐릭터 수가 많아지면 이 연산이 프레임 예산에서 무시할 수 없는 비중을 차지합니다.
이 글에서는 Unity의 두 가지 애니메이션 시스템의 차이, 애니메이션 데이터 압축, 리그 타입에 따른 CPU 비용, Culling 모드, 스키닝의 CPU/GPU 분배를 다룹니다.
Animator vs Animation (Legacy)
Unity에는 두 가지 애니메이션 시스템이 있습니다. Animator(Mecanim)와 Animation(Legacy)입니다. 두 시스템의 목적은 같지만, 내부 구조와 비용이 다릅니다.
Animator (Mecanim)
Animator는 Unity 4에서 도입된 현재의 기본 애니메이션 시스템입니다. Animator Controller라는 상태 머신(State Machine)을 기반으로 동작합니다. 상태 머신은 “현재 어떤 상태인가”와 “어떤 조건이 충족되면 다른 상태로 전환한다”를 정의하는 구조입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Animator (Mecanim) 구조
══════════════════════════════════════════════════════════
Animator Controller (상태 머신):
┌──────────┐ 달리기 → 점프 ┌──────────┐
│ Idle │ ──────────────────▶ │ Jump │
│ (대기) │ │ (점프) │
└──────────┘ └──────────┘
│ ▲ │
│ └──────────── 착지 ────────────┘
▼
┌──────────┐
│ Run │
│ (달리기) │
└──────────┘
──────────────────────────────────────────────────────────
매 프레임 처리:
1) 상태 머신 평가
현재 상태의 트랜지션 조건 검사
조건 충족 시 다음 상태로 전환
2) 블렌딩 계산
상태 전환 중이면 두 애니메이션 클립을 블렌딩
블렌드 트리 내의 가중치 계산
3) 클립 샘플링
현재 시간에 해당하는 키프레임 값 보간
4) Transform 적용
계산된 값을 본(Bone)의 Transform에 기록
══════════════════════════════════════════════════════════
Animator는 복잡한 애니메이션 흐름을 시각적으로 설계할 수 있습니다. 상태 전환(Transition)을 조건부로 설정하고, 블렌드 트리(Blend Tree)로 여러 애니메이션을 부드럽게 혼합할 수 있습니다. 캐릭터의 이동 속도에 따라 걷기와 달리기를 자연스럽게 섞는 것이 대표적인 예입니다.
다만, 매 프레임마다 상태 머신 평가, 트랜지션 조건 검사, 블렌딩 가중치 계산이 실행되므로 CPU 비용이 따릅니다. 상태, 트랜지션, 레이어가 많아질수록 이 비용도 함께 증가합니다.
Animation (Legacy)
Animation(Legacy)은 Unity의 초기 애니메이션 시스템으로, 상태 머신 없이 애니메이션 클립을 직접 재생하는 단순한 구조입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Animation (Legacy) 구조
══════════════════════════════════════════════════════════
Animation 컴포넌트:
클립 목록:
├── Walk 클립
├── Run 클립
└── Attack 클립
──────────────────────────────────────────────────────────
매 프레임 처리:
1) 현재 재생 중인 클립의 시간 갱신
시간 += deltaTime * 재생 속도
2) 키프레임 보간
현재 시간에 해당하는 값 계산
3) Transform / 프로퍼티 적용
계산된 값을 대상에 기록
──────────────────────────────────────────────────────────
상태 머신 없음
블렌드 트리 없음
트랜지션 자동 처리 없음
→ 오버헤드가 적음
══════════════════════════════════════════════════════════
Animation(Legacy)은 상태 머신 평가, 트랜지션 조건 검사, 블렌딩 계산이 없으므로 Animator보다 오버헤드가 적습니다.
클립 하나를 재생하는 것이 전부이며, 블렌딩이 필요하면 스크립트에서 CrossFade()를 수동으로 호출해야 합니다.
선택 기준
애니메이션의 복잡도에 따라 Animator, 트위닝 라이브러리, 스크립트 직접 제어 중 적합한 방식이 달라집니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
애니메이션 방식 선택 기준
══════════════════════════════════════════════════════════
사용 사례 권장 방식
──────────────────────────────────────────────────────────
플레이어 캐릭터 Animator
복잡한 상태 전환, 블렌딩
IK, 리타겟팅
NPC (복잡한 행동 패턴) Animator
여러 상태, 조건부 트랜지션
──────────────────────────────────────────────────────────
UI 애니메이션 트위닝 라이브러리
버튼 확대/축소, 패널 슬라이드 (DOTween 등)
코드에서 시작/끝 값만 지정
환경 오브젝트 트위닝 또는 스크립트
깃발 펄럭임, 문 열기/닫기
단순 반복이나 트리거 동작
단순 이펙트 트위닝 또는 스크립트
빛 깜빡임, 크기 변화
키프레임 클립이 필요 없는 경우
──────────────────────────────────────────────────────────
기존 프로젝트 호환 Animation (Legacy)
이미 Legacy로 작성된 에셋
단순 클립 재생만 필요
══════════════════════════════════════════════════════════
모바일 프로젝트에서 UI 요소나 환경 오브젝트에 Animator를 사용하는 경우가 흔합니다. Unity 에디터의 Animation 창에서 키프레임을 찍으면 자동으로 Animator Controller가 생성되기 때문입니다. 이 경우 상태 머신의 오버헤드가 불필요하게 발생합니다.
트위닝 라이브러리(DOTween 등)는 코드에서 시작 값과 끝 값만 지정하면 중간 값을 자동으로 보간해주므로, 상태 머신 없이 UI나 환경 오브젝트의 단순 애니메이션을 처리할 수 있습니다.
Animation(Legacy)도 단순 클립 재생용으로 사용할 수 있지만, 주로 기존 에셋의 하위 호환 목적으로 유지되는 시스템입니다.
애니메이션 압축
시스템 선택 외에도 애니메이션 비용을 줄이는 방법이 있습니다. 재생하는 클립 자체의 데이터 크기를 줄이면, 메모리 사용량과 매 프레임 샘플링 비용이 함께 줄어듭니다.
키프레임 데이터의 구조
애니메이션 클립은 시간에 따른 값의 변화를 키프레임(Keyframe)으로 기록합니다. 본(Bone) 하나에 대해 위치(x, y, z), 회전(x, y, z 또는 쿼터니언 x, y, z, w), 크기(x, y, z)로 9~10개의 커브가 생깁니다. 캐릭터 하나에 본이 50개이고, 클립이 30fps로 5초(150프레임)라면, 총 키프레임 값은 50 x 10 x 150 = 75,000개에 달합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
애니메이션 클립의 데이터 구조
══════════════════════════════════════════════════════════
본(Bone) 하나, 위치(x) 커브의 예:
프레임 0 1 2 3 4 5 6 7
값 0.0 0.1 0.2 0.3 0.3 0.3 0.3 0.5
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│0:0.0│1:0.1│2:0.2│3:0.3│4:0.3│5:0.3│6:0.3│7:0.5│
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
→ 8개의 키프레임이 하나의 커브를 구성
──────────────────────────────────────────────────────────
본 1개당 커브 수: 위치(3) + 회전(3~4) + 크기(3) = 9~10개
본 50개 x 커브 10개 x 프레임 150개 = 75,000개의 키프레임 값
각 값이 float(4바이트)이면:
75,000 x 4 = 300,000 바이트 ≒ 약 300 KB (비압축)
캐릭터 애니메이션 클립이 20개라면:
300 KB x 20 = 약 6 MB (비압축)
══════════════════════════════════════════════════════════
이 데이터는 메모리에 로드되어 매 프레임 샘플링됩니다. 클립 수가 많아지면 메모리 사용량이 늘어납니다. 데이터 총량이 커지면 CPU 캐시(CPU가 빠르게 접근할 수 있도록 소량의 데이터를 임시로 저장해 두는 고속 메모리)에 담을 수 있는 비율이 줄어들어 캐시 미스(필요한 데이터가 캐시에 없어 메인 메모리까지 읽으러 가는 상황)가 증가하고, 그만큼 샘플링 속도가 느려집니다.
키프레임 리덕션 (Keyframe Reduction)
키프레임 리덕션은 변화가 적은 키프레임을 제거하여 데이터를 줄이는 기법입니다. 위의 예에서, 프레임 3~6은 모두 같은 값(0.3)입니다. 프레임 3과 프레임 6만 남기고 나머지를 제거해도 보간으로 동일한 결과를 만들 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
키프레임 리덕션
══════════════════════════════════════════════════════════
원본 (8개 키프레임):
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│0:0.0│1:0.1│2:0.2│3:0.3│4:0.3│5:0.3│6:0.3│7:0.5│
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
리덕션 후 (5개 키프레임):
┌─────┬─────┬─────┬─────┬─────┐
│0:0.0│1:0.1│2:0.2│3:0.3│7:0.5│
└─────┴─────┴─────┴─────┴─────┘
프레임 4, 5, 6은 보간으로 복원 가능 → 제거
──────────────────────────────────────────────────────────
직선 구간에서도 리덕션 가능:
원본: 0:0.0 1:0.1 2:0.2 3:0.3
→ 0에서 3까지 균일 증가
리덕션: 0:0.0 3:0.3
→ 선형 보간으로 중간값 복원 가능
══════════════════════════════════════════════════════════
키프레임 리덕션은 오차 허용 범위(Error Tolerance)를 설정할 수 있습니다. 오차 범위가 크면 더 많은 키프레임이 제거되어 데이터가 줄어들지만, 원본과의 차이가 커집니다. 반대로 오차 범위를 줄이면 원본에 가깝지만 키프레임이 많이 남습니다.
Unity의 압축 모드
Unity의 애니메이션 Import Settings에서 Anim. Compression 옵션으로 압축 방식을 선택할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Unity 애니메이션 압축 모드
══════════════════════════════════════════════════════════
모드 설명 메모리
──────────────────────────────────────────────────────
Off 압축 없음 최대
원본 키프레임 모두 유지
Keyframe Reduction 변화 적은 키프레임 제거 중간
오차 범위 설정 가능
Optimal Keyframe Reduction에 최소
커브 정밀도 분석을 추가
══════════════════════════════════════════════════════════
Optimal 모드에서는 키프레임 리덕션에 더해, Unity가 커브 데이터의 정밀도를 분석하여 추가적인 압축을 적용합니다. 대부분의 경우 Optimal 모드가 시각적 차이 없이 가장 작은 데이터 크기를 만듭니다.
정밀도와 오차 설정
Unity의 Import Settings에서 Rotation Error, Position Error, Scale Error 값을 조절하면 압축 수준을 세밀하게 제어할 수 있습니다. 이 값이 클수록 더 많은 키프레임이 제거되어 데이터가 줄어들지만, 원본과의 차이도 커집니다.
위치나 회전의 미세한 차이(소수점 4자리 이하)는 시각적으로 구분되지 않는 경우가 많으므로, 오차 값을 약간 높여도 품질 저하 없이 메모리를 절약할 수 있습니다.
불필요한 커브 제거
3D 모델링 툴(Maya, Blender 등)에서 애니메이션을 익스포트하면, 실제로 변화하지 않는 프로퍼티의 커브까지 함께 포함되는 경우가 흔합니다. 예를 들어, 걷기 애니메이션에서 어떤 본도 크기가 변하지 않는데 모든 본의 Scale 커브가 (1, 1, 1)로 150프레임 내내 기록되어 있는 경우입니다. 값이 일정하므로 재생 결과에 아무런 영향이 없지만, 본 50개 x Scale 커브 3개 x 150프레임 = 22,500개의 키프레임 값이 메모리를 차지하고 매 프레임 샘플링 대상에 포함됩니다.
Scale 커브가 가장 흔한 대상인 이유는, 대부분의 캐릭터 애니메이션에서 본의 크기는 변하지 않기 때문입니다. 위치와 회전은 동작마다 달라지지만, Scale은 거의 항상 (1, 1, 1)로 일정합니다.
Unity의 Animation Import Settings에는 이를 위한 “Remove Constant Scale Curves” 옵션이 있습니다. 이 옵션을 활성화하면, 값이 일정한 Scale 커브를 임포트 시점에 자동으로 제거합니다. Scale 외에도 값이 변하지 않는 Position이나 Rotation 커브가 있을 수 있는데, 이런 경우에는 AssetPostprocessor의 OnPostprocessAnimation 콜백을 사용하여 모든 상수 커브를 스크립트로 일괄 제거할 수 있습니다.
Generic vs Humanoid 리그
앞에서 다룬 압축과 커브 제거는 클립에 저장된 데이터의 크기를 줄이는 방법이었습니다. 데이터가 작아지면 메모리 사용량과 샘플링 비용이 줄어듭니다. 하지만 애니메이션 비용은 데이터 크기만으로 결정되지 않습니다. 클립에서 값을 읽은 뒤 그 값을 본(Bone)에 적용하는 과정에서도 CPU 연산이 발생하며, 이 과정의 비용은 캐릭터 모델의 리그 타입에 따라 달라집니다.
리그 타입의 차이
지금까지 본(Bone)의 Transform을 샘플링하고 적용하는 과정을 다루었습니다. 이 본 구조를 Unity가 어떻게 해석할지를 결정하는 것이 리그 타입(Rig Type) 설정입니다. 캐릭터 모델을 임포트할 때 Generic과 Humanoid 두 가지 중 하나를 선택하며, 각각의 내부 처리 과정이 다릅니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Generic vs Humanoid 리그 — 매 프레임 처리 비교
══════════════════════════════════════════════════════════
Generic 리그:
FBX의 본 구조를 변환 없이 그대로 사용
──────────────────────────────────────────────────────────
1) 클립에서 각 본의 로컬 Transform 샘플링
2) 각 본의 로컬 Transform 적용
Humanoid 리그:
본 구조를 Unity의 표준 인체 모델(Avatar)에 매핑하여 사용
원본 본 구조 → (매핑) → Avatar 표준 본 구조
──────────────────────────────────────────────────────────
1) 클립에서 각 본의 로컬 Transform 샘플링
2) Avatar 매핑을 통해 표준 본 구조로 변환
3) Muscle 시스템으로 관절 범위 제한 적용
4) IK(Inverse Kinematics) 계산 (활성화된 경우)
5) 리타겟팅 변환 (다른 캐릭터에 적용 시)
6) 최종 본의 로컬 Transform 적용
══════════════════════════════════════════════════════════
Humanoid의 추가 비용
Humanoid 리그는 Generic에 비해 매 프레임 추가 연산이 발생하며, 추가 비용의 대부분은 Muscle 시스템에서 비롯됩니다.
Muscle 시스템은 인체의 관절이 자연스러운 범위 안에서만 움직이도록 제한합니다. 팔꿈치가 반대 방향으로 꺾이거나, 목이 360도 회전하는 것을 방지하기 위한 것입니다. 이런 비정상적인 회전은 주로 두 가지 상황에서 발생합니다.
체형이 다른 캐릭터에 애니메이션을 리타겟팅할 때 관절 비율 차이로 회전값이 원본과 달라지는 경우, 그리고 여러 애니메이션을 블렌딩할 때 합산된 회전값이 자연스러운 범위를 벗어나는 경우입니다.
Muscle 시스템은 매 프레임 각 관절의 회전 값을 미리 정의된 범위와 비교하고, 범위를 초과하면 경계값으로 강제 제한(clamping)합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Humanoid의 Muscle 시스템 비용
══════════════════════════════════════════════════════════
인체 표준 본 수: 최대 55개 (HumanBodyBones)
Muscle 값: 최대 95개 (관절당 축별 회전 범위)
각 Muscle 값에 대해 매 프레임 수행되는 처리:
1) 원본 회전값 → Muscle 공간으로 변환
2) Muscle 범위 내인지 검사
3) 범위 초과 시 클램핑
4) Muscle 공간 → 다시 회전값으로 변환
──────────────────────────────────────────────────────────
비용 비교 예시 (기기/클립에 따라 다름):
캐릭터 1개 캐릭터 20개
Generic ~0.1 ms ~2.0 ms
Humanoid ~0.15 ms ~3.0 ms
(30~50% 높음) (차이 ~1.0 ms)
30fps 예산(33.3ms) 기준, 캐릭터 20개에서 약 3% 차이
══════════════════════════════════════════════════════════
Humanoid 전용 기능
Humanoid 리그가 제공하는 기능 중 Unity의 내장 기능으로는 Generic에서 사용할 수 없는 것이 두 가지 있습니다. Humanoid의 추가 비용은 이 기능들을 지원하기 위해 발생하므로, 해당 기능이 필요하지 않다면 비용만 남게 됩니다.
리타겟팅(Retargeting). 한 캐릭터용으로 만든 애니메이션을 다른 체형의 캐릭터에 그대로 적용하는 기능입니다. Generic 리그에서는 본 이름과 계층 구조가 모델마다 다르므로, A 캐릭터의 클립을 B 캐릭터에 적용하면 본 이름이 일치하지 않아 동작하지 않습니다. Humanoid 리그에서는 모든 캐릭터가 Avatar라는 동일한 표준 본 구조에 매핑되어 있으므로, 키가 다르거나 팔 길이가 다른 캐릭터라도 같은 클립을 공유할 수 있습니다. 플레이어가 다양한 캐릭터 모델을 선택할 수 있는 게임이나, 하나의 애니메이션 세트로 여러 NPC를 커버해야 하는 경우에 유용합니다.
IK(Inverse Kinematics). 일반적인 애니메이션은 순방향 키네마틱스(FK)로 동작합니다. 어깨 본이 회전하면 팔꿈치가 따라가고, 팔꿈치가 회전하면 손이 따라가는 방식으로, 부모 본에서 자식 본 방향으로 Transform이 전파됩니다. IK는 이 방향을 뒤집습니다. “손이 이 위치에 있어야 한다”는 목표를 정하면, 엔진이 팔꿈치와 어깨의 회전을 역산하여 손이 목표 위치에 도달하도록 관절 체인 전체를 조정합니다. 캐릭터의 발이 경사면이나 계단에 자연스럽게 붙는 Foot IK, 손이 문 손잡이를 정확히 잡는 Hand IK가 대표적입니다. Unity의 내장 IK(OnAnimatorIK 콜백)는 Avatar의 표준 관절 체인 정보에 의존하므로 Humanoid 리그에서만 동작합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
리그 타입 선택 기준
══════════════════════════════════════════════════════════
리타겟팅 필요?
│
├── 예 → Humanoid
│
└── 아니오 → IK 필요?
│
├── 예 → Humanoid
│
└── 아니오 → Generic (CPU 비용 절약)
──────────────────────────────────────────────────────────
모바일 적용 예:
Humanoid:
플레이어 캐릭터 리타겟팅 또는 IK 사용
Generic:
보스 몬스터 전용 애니메이션, 리타겟팅 불필요
NPC (고유 모델) 모델별 전용 클립 사용
동물, 기계 인체 구조가 아니므로 Avatar 매핑 불가
상황에 따라 판단:
일반 몬스터 종류가 다양하면 리타겟팅으로
(종류 다양) 클립 수를 줄일 수 있으나,
종류가 적으면 Generic이 효율적
══════════════════════════════════════════════════════════
캐릭터 수가 많은 모바일 게임에서, 리타겟팅이나 IK가 필요하지 않은 캐릭터를 Humanoid로 설정하면 불필요한 CPU 비용이 누적됩니다. 모델을 Import할 때 리그 타입을 확인하고 필요에 맞게 설정해야 합니다.
Animation Culling 모드
화면 밖 애니메이션의 비용
리그 타입으로 캐릭터 한 개의 애니메이션 비용을 줄였다면, 다음은 화면에 보이지 않는 캐릭터의 비용을 아예 제거하는 것입니다. Part 1에서 파티클의 Culling Mode를 다루었는데, 애니메이션에도 같은 원리가 적용됩니다. 캐릭터가 카메라 밖에 있을 때, 애니메이션을 계속 평가할 것인지, 멈출 것인지를 결정하는 설정입니다.
Animator 컴포넌트의 Culling Mode 설정으로 이 동작을 제어하며, 세 가지 모드를 제공합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Animator Culling Mode
══════════════════════════════════════════════════════════
화면 안: 세 모드 모두 동일
애니메이션 평가 + Transform 적용 + 렌더링
차이는 화면 밖 동작에서 발생:
──────────────────────────────────────────────────────────
Always Animate
화면 밖에서도 모든 처리를 수행 (렌더링만 제외)
절약: 없음
복귀: 정확한 시간의 애니메이션이 이어짐
──────────────────────────────────────────────────────────
Cull Update Transforms
상태 머신과 루트 모션만 평가
(루트 모션: 애니메이션이 캐릭터 위치를 직접 이동시키는 기능)
절약: IK, 리타겟팅, Transform 갱신 비용 제거
복귀: 상태 머신은 정확하나 Transform이 한 프레임 점프
──────────────────────────────────────────────────────────
Cull Completely
애니메이션 완전 정지, 아무것도 하지 않음
절약: CPU 비용 전부 제거
복귀: 정지 시점에서 이어서 재생 (시간 흐름 끊김)
══════════════════════════════════════════════════════════
모드 선택 기준
캐릭터 30개 중 20개가 화면 밖에 있는 상황에서, Always Animate를 Cull Completely로 바꾸면 화면 밖 20개의 애니메이션 비용(상태 머신 + 블렌딩 + Transform)이 완전히 제거됩니다. 캐릭터당 약 0.1ms의 비용이라면 총 2.0ms를 절약할 수 있고, 이는 30fps 프레임 예산(33.3ms)의 약 6%에 해당합니다.
대부분의 캐릭터에는 Cull Completely가 적합합니다. 화면 밖 캐릭터의 애니메이션이 정확한 시간에 있든 아니든, 플레이어는 그 차이를 알 수 없습니다. 화면에 다시 들어오는 순간 정지했던 지점에서 이어서 재생되므로, 시각적으로 문제가 되는 경우는 드뭅니다.
Always Animate가 필요한 경우는 제한적입니다. 화면 밖에서 애니메이션 이벤트(Animation Event)가 발생하여 게임 로직에 영향을 미치는 경우가 대표적인 예입니다. 예를 들어, 화면 밖 캐릭터의 공격 애니메이션에서 특정 시점에 데미지 이벤트가 발생해야 한다면, 애니메이션이 정확한 시간에 도달해야 합니다. 이런 경우에만 Always Animate를 사용하고, 나머지는 Cull Completely를 기본으로 설정하는 것이 좋습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Culling Mode 설정 가이드
══════════════════════════════════════════════════════════
모드 적합한 대상
──────────────────────────────────────────────────────
Cull Completely 대부분의 캐릭터 (기본값)
Cull Update Transforms 화면 밖에서 상태 머신이
정확해야 하는 AI NPC
(패트롤 → 전투 전환 등)
Always Animate 화면 밖에서 Animation Event가
게임 로직에 영향을 미치는 경우
══════════════════════════════════════════════════════════
GPU Skinning vs CPU Skinning
지금까지 다룬 최적화는 애니메이션 평가 단계, 즉 클립에서 본의 Transform 값을 계산하는 과정에 집중되어 있었습니다. 하지만 계산된 본 Transform을 실제 메쉬 정점에 반영하는 단계가 하나 더 남아 있습니다. 이 단계를 스키닝(Skinning)이라 하며, 정점 수에 비례하는 별도의 연산 비용이 발생합니다. 스키닝은 CPU 또는 GPU에서 처리할 수 있으며, 어느 쪽에서 실행하느냐에 따라 CPU와 GPU 사이의 부하 분배가 달라집니다.
스키닝이란
앞 단계에서 애니메이션 시스템이 본(Bone)의 회전과 위치를 계산하지만, 본 자체는 눈에 보이지 않는 뼈대입니다. 플레이어가 실제로 보는 것은 캐릭터의 메쉬(3D 표면)입니다. 스키닝은 이 뼈대와 표면을 연결하는 단계로, 각 메쉬 정점을 자신에게 영향을 주는 본의 현재 Transform에 맞춰 이동시킵니다. 팔 본이 회전하면 팔 부분의 정점들이 함께 움직여서, 메쉬가 뼈대의 포즈에 맞게 변형됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
스키닝 과정 — 팔 본 체인 예시
══════════════════════════════════════════════════════════
바인드 포즈 (T-Pose, 팔을 편 상태):
[어깨]━━━━━━━[팔꿈치]━━━━━━━[손목]
상완 정점들 ···P··· 전완 정점들
정점 P는 팔꿈치 근처에 위치
상완본 가중치 50%, 전완본 가중치 50%
──────────────────────────────────────────────────────────
포즈 변경 (팔 구부림):
[어깨]━━━━━━━[팔꿈치]
상완 정점들 ╲
╲━━━━━━━[손목]
전완 정점들
정점 P는 두 본에 모두 영향을 받으므로
가중치에 따라 중간 위치로 이동
──────────────────────────────────────────────────────────
정점 하나의 계산:
각 본의 변환 = 현재 포즈 행렬 x 바인드 포즈 역행렬
(바인드 포즈에서 현재 포즈까지 얼마나 변했는지)
P의 최종 위치 = 상완본 변환 x 원본 위치 x 0.5
+ 전완본 변환 x 원본 위치 x 0.5
──────────────────────────────────────────────────────────
비용:
정점 5,000개 x 정점당 본 영향 4개 = 20,000번 행렬 연산
매 프레임 수행
══════════════════════════════════════════════════════════
CPU Skinning vs GPU Skinning
기본적으로 Unity는 CPU에서 스키닝을 처리합니다. 메인 스레드(또는 워커 스레드)에서 각 정점에 대해 본의 변환을 적용하고, 변형된 정점 버퍼를 GPU에 업로드하여 렌더링하는 방식입니다.
CPU Skinning은 모든 플랫폼에서 동작하지만, 캐릭터 수가 늘어나면 CPU 부하가 선형적으로 증가합니다. 정점 5,000개짜리 캐릭터 20개를 CPU에서 스키닝하면, 매 프레임 100,000개 정점의 변환을 CPU가 처리해야 합니다.
GPU Skinning은 이 스키닝 연산을 CPU 대신 GPU에서 수행하는 방식입니다. CPU Skinning에서 CPU가 정점 5,000개를 순차적으로 변환했다면, GPU Skinning에서는 GPU의 수천 개 코어가 정점을 동시에 변환합니다. GPU는 동일한 연산을 대량의 데이터에 병렬로 적용하는 구조이므로, 정점 단위의 반복 계산에 적합합니다.
CPU가 하는 일은 본 변환 행렬(본 50개라면 행렬 50개)만 계산하여 GPU에 전달하는 것으로 줄어듭니다. 정점 변환은 GPU의 Compute Shader(렌더링이 아닌 범용 계산을 GPU에서 실행하는 프로그램)에서 처리되며, 변형된 정점 데이터가 GPU 메모리에 바로 남으므로 CPU에서 GPU로 정점 버퍼를 업로드하는 과정도 사라집니다.
두 방식의 차이를 정리하면 다음과 같습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CPU Skinning vs GPU Skinning
══════════════════════════════════════════════════════════
CPU Skinning GPU Skinning
──────────────────────────────────────────────────────
연산 위치 CPU (메인/워커) GPU (Compute Shader)
정점 변환 CPU가 순차 처리 GPU가 병렬 처리
CPU 부하 정점 수에 비례 본 행렬 전달만
GPU 부하 없음 정점 변환 추가
데이터 전송 정점 버퍼 업로드 불필요 (GPU에 상주)
플랫폼 모든 플랫폼 Compute Shader 필요
──────────────────────────────────────────────────────────
CPU-bound 부하 가중 부하 경감 ←
GPU-bound 부하 경감 ← 부하 가중
══════════════════════════════════════════════════════════
모바일에서의 선택
모바일에서는 CPU와 GPU 모두 제한적이므로, 현재 병목이 어디인지에 따라 선택이 달라집니다.
CPU-bound 상태라면 GPU Skinning으로 CPU 부하를 GPU로 이전하는 것이 효과적입니다. GPU-bound 상태라면 GPU에 추가 부하를 주면 안 되므로 CPU Skinning을 유지해야 합니다.
캐릭터가 10개 이하라면 CPU Skinning으로도 충분하고, 20개 이상이면 GPU Skinning을 고려할 수 있습니다. Unity Profiler에서 CPU와 GPU의 Skinning 시간을 측정하고, 실제 기기에서 프레임 레이트 변화를 확인한 후 결정해야 합니다.
정점 수와 본 가중치
스키닝 비용은 정점 수와 본 가중치 수(Skin Weights)에 직접 비례합니다. 본 가중치 수는 하나의 정점이 영향을 받는 본의 최대 수입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
본 가중치 수에 따른 비용 (Quality Settings > Skin Weights)
══════════════════════════════════════════════════════════
설정 정점당 연산 정점 5,000개 기준 비고
──────────────────────────────────────────────────────
1 Bone 행렬 1회 5,000회 품질 낮음
2 Bones 행렬 2회 10,000회 모바일 권장
4 Bones 행렬 4회 20,000회 기본값
Unlimited 전체 사용 가변 최고 품질
──────────────────────────────────────────────────────────
모바일에서 2 Bones 권장 이유:
4 Bones 대비 행렬 연산 절반
관절 부위(팔꿈치, 무릎)의 변형 차이가 모바일 화면에서 미미
══════════════════════════════════════════════════════════
Unity의 Quality Settings에서 Skin Weights를 전역적으로 설정할 수 있습니다. 모바일에서는 2 Bones로 설정하면 대부분의 캐릭터에서 시각적 품질을 유지하면서 스키닝 비용을 절반으로 줄일 수 있습니다. 관절 부위(팔꿈치, 무릎)의 변형이 약간 부자연스러워질 수 있지만, 모바일 화면 크기에서는 구분하기 어렵습니다.
최적화 항목 정리
이 글에서 다룬 최적화 항목을 비용 발생 지점과 함께 정리합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
애니메이션 최적화 항목 정리
══════════════════════════════════════════════════════════
항목 비용 발생 지점 대응
──────────────────────────────────────────────────────
단순 애니메이션에 상태 머신 평가 트위닝 라이브러리
불필요한 Animator 트랜지션 검사 또는 스크립트로 대체
비압축 클립 데이터 메모리 사용량 Optimal 압축
샘플링 비용 상수 커브 제거
키프레임 오차 미조정 불필요한 키프레임 잔존 Error 값 조정
(시각 품질과 균형)
불필요한 Humanoid Muscle 연산 리타겟팅/IK 불필요 시
매 프레임 30~50% 추가 Generic으로 변경
Always Animate 남용 화면 밖 캐릭터에서 Cull Completely를
CPU 비용 지속 기본으로 설정
높은 Skin Weights 정점당 행렬 연산 증가 2 Bones로 설정
4 Bones는 2배 비용 (모바일 기준)
과다한 정점 수 스키닝 연산 총량 증가 LOD로 거리별 감소
비대한 Animator 상태/레이어 수에 불필요한 상태,
Controller 비례하는 평가 비용 트랜지션 정리
══════════════════════════════════════════════════════════
마무리
- 애니메이션 비용은 본 Transform을 계산하는 애니메이션 평가 단계와, 계산된 Transform을 메쉬 정점에 반영하는 스키닝 단계로 나뉩니다.
- 단순한 UI/환경 애니메이션에는 Animator 대신 트위닝 라이브러리나 스크립트 직접 제어가 효율적입니다. Animator의 상태 머신 오버헤드를 피할 수 있습니다.
- 애니메이션 압축은 Optimal 모드를 기본으로 사용하고, 3D 모델링 툴에서 함께 익스포트된 불필요한 상수 커브를 제거하면 메모리와 샘플링 비용이 줄어듭니다.
- Humanoid 리그는 리타겟팅과 IK를 위해 Generic보다 30~50% 높은 CPU 비용이 발생합니다. 이 기능이 불필요한 캐릭터에는 Generic을 사용합니다.
- Culling Mode를 Cull Completely로 설정하면 화면 밖 캐릭터의 애니메이션 비용을 완전히 제거할 수 있습니다.
- 스키닝은 정점 수와 Skin Weights에 비례하는 비용이 발생합니다. Skin Weights를 2 Bones로 설정하면 행렬 연산을 절반으로 줄일 수 있습니다.
- GPU Skinning은 CPU-bound 상황에서 스키닝 부하를 GPU로 이전하는 데 효과적이지만, GPU-bound 상태에서는 역효과가 날 수 있으므로 프로파일러로 확인한 뒤 결정합니다.
파티클과 애니메이션까지 포함하여, 게임 루프의 원리 (1)에서 시작한 전체 시리즈에서 각 서브시스템의 비용 구조와 최적화 방법을 하나씩 다루었습니다. 각 서브시스템의 비용 구조를 알고 있어도, 실제 게임에서 병목이 어디에 있는지 찾지 못하면 최적화를 적용할 수 없습니다. 프로파일링 시리즈에서 Unity Profiler를 비롯한 프로파일링 도구로 병목을 진단하는 방법을 이어서 다뤄봅니다.
관련 글
시리즈
- 파티클과 애니메이션 (1) - 파티클 시스템 최적화
- 파티클과 애니메이션 (2) - 애니메이션 최적화 (현재 글)
전체 시리즈
- 게임 루프의 원리 (1) - 프레임의 구조
- 게임 루프의 원리 (2) - CPU-bound와 GPU-bound
- 렌더링 기초 (1) - 메쉬의 구조
- 렌더링 기초 (2) - 텍스처와 압축
- 렌더링 기초 (3) - 머티리얼과 셰이더 기초
- GPU 아키텍처 (1) - GPU 병렬 처리와 렌더링 파이프라인
- GPU 아키텍처 (2) - 모바일 GPU와 TBDR
- Unity 렌더 파이프라인 (1) - Built-in과 URP의 구조
- Unity 렌더 파이프라인 (2) - 드로우콜과 배칭
- Unity 렌더 파이프라인 (3) - 컬링과 오클루전
- 스크립트 최적화 (1) - C# 실행과 메모리 할당
- 스크립트 최적화 (2) - Unity API와 실행 비용
- 메모리 관리 (1) - 가비지 컬렉션의 원리
- 메모리 관리 (2) - 네이티브 메모리와 에셋
- 메모리 관리 (3) - Addressables와 에셋 전략
- UI 최적화 (1) - 캔버스와 리빌드 시스템
- UI 최적화 (2) - UI 최적화 전략
- 조명과 그림자 (1) - 실시간 조명과 베이크
- 조명과 그림자 (2) - 그림자와 후처리
- 셰이더 최적화 (1) - 셰이더 성능의 원리
- 셰이더 최적화 (2) - 셰이더 배리언트와 모바일 기법
- 물리 최적화 (1) - 물리 엔진의 실행 구조
- 물리 최적화 (2) - 물리 최적화 전략
- 파티클과 애니메이션 (1) - 파티클 시스템 최적화
- 파티클과 애니메이션 (2) - 애니메이션 최적화 (현재 글)
- 프로파일링 (1) - Unity Profiler와 Frame Debugger
- 프로파일링 (2) - 모바일 프로파일링
- 모바일 전략 (1) - 발열과 배터리
- 모바일 전략 (2) - 빌드와 품질 전략