모바일 3D 그래픽스: 드로우콜(Draw Call)과 배칭(Static/Dynamic Batching & GPU Instancing) 최적화 가이드
모바일 3D 게임 개발 환경에서 화면에 수많은 오브젝트와 캐릭터가 등장할 때 발생하는 심각한 프레임 드랍(FPS 저하)과 기기 발열의 주원인은 대개 GPU의 폴리곤 연산 부족이 아닌 CPU의 렌더링 명령 과부하에 있습니다. CPU가 GPU에게 "이 메쉬를 이 셰이더와 텍스처를 사용해 그려라"라고 지시하는 단일 렌더 명령을 드로우콜(Draw Call)이라고 하며, 렌더 상태(Render State)가 바뀔 때 발생하는 부하를 셋패스 콜(SetPass Call)이라고 부릅니다. 모바일 기기의 제한된 CPU 대역폭에서 수백 번 이상의 드로우콜이 누적되면 커맨드 버퍼 전송 오버헤드로 인해 치명적인 병목이 발생합니다. 본 포스팅에서는 드로우콜과 셋패스 콜의 원리부터 정적/동적 배칭, GPU 인스턴싱, 그리고 최신 URP의 SRP Batcher까지 모바일 환경에 최적화된 실무 배칭 전략을 완벽히 정리합니다.
1. 드로우콜 vs 셋패스 콜과 4대 배칭 기법 비교
드로우콜 횟수 자체보다 더 위험한 것은 머티리얼, 텍스처, 셰이더가 교체될 때 발생하는 셋패스 콜입니다. GPU 내부의 렌더 파이프라인 상태 머신을 재설정하는 데 막대한 연산 비용이 들기 때문입니다. 엔진은 이를 묶어서 한 번에 전송하기 위해 다양한 배칭(Batching) 기술을 지원합니다.
| 배칭 기법 | 작동 원리 | 장점 및 최적 적용처 | 주의점 및 단점 |
|---|---|---|---|
| 정적 배칭 (Static Batching) | 움직이지 않는 메쉬들을 빌드/런타임에 단일 거대 복합 버퍼로 병합 | 배경 건물, 지형, 정적 프랍. 런타임 CPU 연산 없음 | 동일 메쉬도 중복 버텍스로 복사되어 메모리(VRAM) 점유율 급증 |
| 동적 배칭 (Dynamic Batching) | 움직이는 소형 메쉬를 매 프레임 CPU가 월드 좌표로 변환하여 묶음 | 매우 단순한 파티클, 소형 미사일/발사체 투사체 | 버텍스 300개 미만 제한. 매 프레임 CPU 연산으로 모바일 발열 유발 (비권장) |
| GPU 인스턴싱 (GPU Instancing) | 동일 메쉬 + 동일 머티리얼을 GPU 하드웨어 명령 1회로 대량 렌더링 | 풀밭, 수풀, 나무, 몬스터 무리, 총알. 메모리와 CPU 모두 최적화 | 인스턴스별 메쉬가 완전히 동일해야 함. 모바일 OpenGL ES 3.0 이상 필수 |
| SRP Batcher (URP 내장) | 동일 셰이더 변형을 쓰는 객체들의 머티리얼 CBuffer를 GPU VRAM에 유지 | 메쉬가 달라도 동일 셰이더면 SetPass Call을 1회로 단축 | 드로우콜 수치는 유지되나 CPU 디스패치 시간 대폭 단축. GPU Instancing과 상호 배타적 |
2. GPU 인스턴싱 실전 적용과 MaterialPropertyBlock 필수 노하우
GPU 인스턴싱을 사용할 때 초보 개발자가 가장 흔히 범하는 치명적인 실수는 스크립트에서 오브젝트의 색상이나 텍스처를 바꾸기 위해 renderer.material.color = Color.red;를 호출하는 것입니다. 이 코드는 머티리얼의 복제본(Instance)을 생성하여 배칭을 완전히 깨뜨리고 드로우콜을 폭발시킵니다.
2.1 MaterialPropertyBlock을 활용한 배칭 보존 코드
// 배칭을 깨뜨리지 않고 인스턴스별 색상/값을 변경하는 C# 정석 패턴
public class InstancedObjectColor : MonoBehaviour
{
private static MaterialPropertyBlock propBlock;
private static readonly int ColorPropertyID = Shader.PropertyToID("_BaseColor");
private Renderer targetRenderer;
void Awake()
{
targetRenderer = GetComponent<Renderer>();
if (propBlock == null) propBlock = new MaterialPropertyBlock();
}
public void SetCustomColor(Color color)
{
// 머티리얼 인스턴스를 생성하지 않고 블록에만 프로퍼티를 주입
targetRenderer.GetPropertyBlock(propBlock);
propBlock.SetColor(ColorPropertyID, color);
targetRenderer.SetPropertyBlock(propBlock);
}
}
3. 텍스처 아틀라스(Atlas) 팩킹과 메시 결합(Mesh Combine) 전략
배칭이 이루어지려면 기본 전제 조건이 "동일한 머티리얼을 공유해야 한다"는 점입니다. 서로 다른 머티리얼을 사용하는 수십 개의 환경 프랍(바위, 울타리, 상자 등)은 어떻게 배칭해야 할까요?
- 텍스처 아틀라싱(Texture Atlasing): 각각 512x512 해상도를 가진 16개의 독립된 텍스처를 2048x2048 크기의 단일 아틀라스 시트로 묶고, 3D 모델의 UV 좌표를 해당 영역에 매핑합니다. 이렇게 하면 서로 다른 16개의 프랍이 단 1개의 머티리얼을 공유하므로 완벽한 정적 배칭 및 GPU 인스턴싱이 가능해집니다.
- 런타임/에디터 타임 메시 결합: 방 하나에 배치된 수십 개의 소품 메쉬를
Mesh.CombineMeshesAPI를 통해 하나의 통합된 메시로 합치면, 50번 발생하던 드로우콜을 단 1번으로 압축할 수 있습니다. (단, 시야각 밖의 객체도 한꺼번에 렌더링되므로 컬링 효율과의 균형이 필요합니다).
4. 결론 및 모바일 드로우콜 최적화 실무 체크리스트
모바일 3D 프로젝트에서 목표 프레임(60 FPS)을 안정적으로 유지하려면 빌드 전후로 드로우콜 상태를 면밀히 점검해야 합니다. 아래 5가지 필수 체크리스트를 점검하시기 바랍니다.
- 플랫폼별 권장 드로우콜 준수: 저사양 모바일 기준 씬 전체 드로우콜 100~150 이하, 중고사양 기준 200~250 이하로 억제되었는가?
- 프레임 디버거(Frame Debugger) 검수: 유니티 Frame Debugger를 통해 배칭이 중간에 끊기는(Break Batching) 원인(렌더 큐 차이, 머티리얼 불일치, 라이트맵 인덱스 분리 등)을 추적하고 제거했는가?
- 동적 배칭(Dynamic Batching) 비활성화: URP 프로젝트 설정에서 모바일 CPU 발열을 일으키는 구형 동적 배칭 옵션을 끄고, SRP Batcher 또는 GPU Instancing 위주로 파이프라인을 재정비했는가?
- MaterialPropertyBlock 정석 적용: 인스턴스별 색상/외형 변화 시
renderer.material호출로 인한 머티리얼 누수 및 배칭 파괴가 없는가? - 환경 에셋 텍스처 아틀라스화: 던전, 마을 등 고밀도 씬에 배치된 오브젝트들이 공용 텍스처 아틀라스를 사용하여 셋패스 콜을 최소화했는가?
댓글 쓰기