Unity 렌더 파이프라인 (2) - 드로우콜과 배칭 - soo:bak
작성일 :
GPU에 명령을 보내는 비용
Part 1에서는 렌더 파이프라인이 씬을 렌더링하기 위해 GPU에 명령을 보내는 구조를 살펴보았습니다. 그런데 이 명령을 보내는 과정 자체에 비용이 발생합니다.
CPU는 오브젝트를 그릴 때마다 GPU에 렌더링 명령을 보내는데, 이를 드로우콜(Draw Call)이라 합니다. 씬의 오브젝트가 많아지면 드로우콜 수도 함께 늘어나고, 드로우콜을 준비하고 전달하는 CPU 측 비용이 병목이 되어 프레임을 제시간에 완성하지 못하게 됩니다.
이 비용을 줄이기 위해 여러 드로우콜을 묶거나 상태 변경을 줄이는 접근을 사용하는데, 이를 배칭(Batching)이라 합니다. Unity는 Static Batching, Dynamic Batching, SRP Batcher, GPU Instancing 네 가지 배칭 기법을 제공하며, 이 글에서는 드로우콜의 비용이 정확히 어디서 발생하는지 분석한 뒤 각 기법의 원리와 트레이드오프를 살펴봅니다.
드로우콜(Draw Call)의 정의
하나의 드로우콜에는 “이 메쉬의 삼각형들을, 이 머티리얼이 지정하는 셰이더와 프로퍼티로 렌더링하라”는 의미가 담겨 있습니다. 하나의 드로우콜은 하나의 메쉬-머티리얼 조합만 처리할 수 있으므로, 기본적으로 오브젝트마다 별도의 드로우콜이 필요합니다.
오브젝트가 많아지면 드로우콜도 늘어납니다. 하지만 성능 병목은 드로우콜의 수 자체보다, 각 드로우콜 직전에 발생하는 GPU 상태 변경을 CPU가 준비하는 과정에 있는 경우가 많습니다.
상태 변경 비용
CPU가 GPU에 렌더링 명령을 보내는 구조는 상태 머신(State Machine) 모델을 따릅니다. 상태 머신은 현재 설정된 상태에 따라 동작이 결정되는 구조이므로, GPU가 삼각형을 그리기 전에는 어떤 셰이더를 사용할 것인지, 어떤 텍스처를 바인딩할 것인지, 블렌딩 모드는 무엇인지 같은 상태를 먼저 설정해야 합니다. 나무를 그린 뒤 건물을 그리려면, 나무에 사용하던 셰이더와 텍스처를 건물용으로 교체해야 합니다. 이처럼 드로우콜 사이에서 상태를 전환할 때마다 비용이 발생합니다.
(1)~(7)이 상태 변경이고, (8)이 실제 드로우콜입니다.
이 외에도 렌더 타겟 전환(SetRenderTarget), 뷰포트·시저 설정(SetViewport/SetScissor), 텍스처 샘플러 설정(SetSampler), 정점 입력 형식 설정(SetInputLayout) 등의 상태 변경이 있습니다. 렌더 타겟과 뷰포트는 오브젝트 단위가 아니라 렌더 패스나 카메라 단위로 변경되고, 샘플러와 입력 형식은 셰이더 전환 시 함께 변경되므로 위 다이어그램에서는 오브젝트를 그릴 때마다 개별적으로 변경될 수 있는 항목만 표기했습니다.
오브젝트 단위로 발생하는 상태 변경 중 일반적으로 가장 비용이 큰 것은 셰이더 교체입니다. GPU 파이프라인이 처리량을 높이기 위해 여러 단계를 동시에 진행하는 구조이기 때문입니다.
CPU가 드로우콜을 실행하면, GPU는 해당 메쉬의 삼각형들을 파이프라인에 순서대로 투입합니다. 삼각형 A가 정점 처리를 마치고 프래그먼트 처리로 넘어가면, 비어진 정점 처리 자리에 삼각형 B가 곧바로 들어옵니다. 이때 다음 드로우콜이 다른 셰이더를 사용한다면, 이전 셰이더와 새 셰이더의 처리 로직이 다르므로 파이프라인에서 두 셰이더의 작업을 섞어 진행할 수 없습니다. 파이프라인에 남아 있는 이전 작업이 모두 정리될 때까지 지연(pipeline stall)이 발생하고, 셰이더 교체가 잦을수록 이 지연이 누적되어 성능에 영향을 줍니다.
같은 머티리얼을 사용하는 오브젝트를 연속으로 그리면 셰이더·텍스처·블렌딩·깊이/스텐실·래스터라이저 상태가 모두 동일하므로, 비용이 큰 상태 전환을 건너뛸 수 있습니다. 오브젝트마다 달라지는 것은 트랜스폼 등의 상수 버퍼(SetUniform)와 메쉬 데이터(BindMesh) 정도입니다.
SetPass Call
앞에서 살펴본 것처럼 드로우콜 사이에는 여러 종류의 상태 변경이 발생합니다. 이 중 상수 버퍼(SetUniform)나 메쉬(BindMesh)처럼 오브젝트마다 바뀌는 가벼운 변경도 있지만, 셰이더의 패스(Pass)가 바뀌는 경우에는 비용이 큽니다. 패스란 셰이더가 정점 처리부터 프래그먼트 처리까지를 한 번 수행하는 단위로, 셰이더 프로그램·블렌딩·깊이/스텐실·래스터라이저·텍스처 설정을 하나의 묶음으로 포함합니다. 패스가 바뀌면 이 설정이 한꺼번에 전환됩니다.
Unity는 이 패스 전환 횟수만을 따로 SetPass Call이라는 지표로 추적하며, 렌더링 성능을 판단하는 핵심 지표로 사용합니다.
같은 머티리얼을 사용하는 오브젝트를 연속으로 그리면 패스가 유지되므로 SetPass Call이 줄어듭니다. Part 1에서 다룬 렌더 파이프라인의 오브젝트 정렬에서 머티리얼이 정렬 기준 중 하나인 것도 이러한 이유입니다.
이어서 살펴볼 배칭 기법들은 드로우콜 수 또는 SetPass Call 수를 줄이는 것을 목표로 합니다.
Static Batching
Static Batching은 움직이지 않는(Static으로 표시된) 오브젝트들의 메쉬를 빌드 시점에 하나의 큰 메쉬로 합치는 기법입니다.
같은 머티리얼을 사용하는 나무가 씬에 50그루 있다면, 배칭 없이는 50번의 드로우콜이 필요합니다. Static Batching을 적용하면 빌드 시점에 50그루의 메쉬가 하나로 합쳐져, 런타임에는 1번의 드로우콜로 모두 그릴 수 있습니다.
작동 조건
첫째, 오브젝트의 Inspector에서 Static 체크박스 아래 Batching Static 플래그가 켜져 있어야 합니다. 이 플래그는 “이 오브젝트는 런타임에 이동하지 않는다”는 약속이므로, 엔진이 빌드 시점에 위치를 확정하고 메쉬를 합칠 수 있습니다.
둘째, 같은 머티리얼을 사용해야 합니다. 머티리얼이 다르면 GPU 상태가 달라지므로, 하나의 드로우콜로 합칠 수 없습니다.
장점과 단점
장점은 드로우콜 감소입니다. CPU가 GPU에 보내는 드로우콜 수가 줄어들어 CPU 측 렌더링 준비 비용이 감소합니다. 메쉬를 합치는 작업이 빌드 시점에 끝나므로, 런타임에 추가 CPU 비용이 발생하지 않습니다. 또한 메쉬를 합친 뒤에도 개별 오브젝트 단위의 Frustum Culling을 지원하므로, 보이지 않는 오브젝트는 GPU에 제출되지 않습니다.
컬링 기법에 대해서는 Part 3에서 자세히 다룹니다.
단점은 메모리 증가입니다. 나무 50그루가 모두 같은 프리팹에서 만들어졌다면, 배칭 없이는 메쉬 데이터 하나만 메모리에 올리고 50번 재사용합니다. 각 인스턴스에는 위치, 회전, 스케일 정보(변환 행렬)만 저장하면 됩니다. 하지만 Static Batching을 적용하면 각 나무의 정점이 월드 좌표로 변환되어 하나의 큰 메쉬에 복사됩니다. 합쳐진 메쉬가 원본과 별도로 메모리에 상주하므로, 정점 데이터가 약 51배로 늘어납니다.
메모리를 과도하게 사용하면, 오히려 메모리 부족으로 OS가 앱을 강제 종료하거나 다른 리소스의 로딩이 지연될 수 있습니다.
Dynamic Batching
Dynamic Batching은 움직이는 오브젝트에도 배칭을 적용하기 위해, 런타임에 매 프레임 CPU가 작은 메쉬들을 합치는 방식입니다.
매 프레임 모든 정점에 대해 로컬→월드 좌표 변환(행렬 곱셈)을 수행해야 하므로, 이 CPU 비용이 드로우콜 절약으로 얻는 이점보다 커지는 경우가 많습니다. 이 때문에 Dynamic Batching에는 엄격한 제한이 걸려 있습니다.
제한 사항
CPU가 매 프레임 처리해야 하는 데이터량은 정점 수와 정점당 속성(위치, 법선, UV 좌표 등) 수에 비례하므로, Unity는 두 가지 제한을 동시에 적용합니다.
정점 수는 300개 이하, 정점 속성 총합(정점 수 × 정점당 속성 수)은 900개 이하이며, 둘 중 먼저 도달하는 쪽이 실제 제한이 됩니다.
Dynamic Batching 정점 제한 (정점 속성 총합 900 이하)
| 속성 수 | 사용 속성 예시 | 최대 정점 |
|---|---|---|
| 3개 | 위치 + 법선 + UV | 300 |
| 4개 | 위치 + 법선 + UV + 탄젠트 | 225 |
| 5개 | 위치 + 법선 + UV0 + UV1 + 탄젠트 | 180 |
정점 수 외에 추가 조건도 있습니다. 같은 머티리얼을 사용해야 하고, 스케일 유형도 일치해야 합니다. 균일 스케일(Uniform Scale, x·y·z 비율이 동일)과 비균일 스케일(Non-uniform Scale, 축마다 비율이 다름)은 법선 벡터 계산 방식이 달라, 같은 유형끼리만 배칭됩니다. 음수 스케일(미러링)은 배칭에서 제외됩니다. 라이트맵을 사용하는 경우에는 같은 라이트맵 인덱스와 같은 오프셋/스케일을 가져야 하고, 멀티패스 셰이더를 사용하는 오브젝트도 배칭 대상이 되지 않습니다.
URP에서의 비활성화
제한이 엄격한 만큼 Dynamic Batching이 실제로 적용되는 상황은 한정적입니다. 300 정점 이하의 메쉬는 UI 요소나 단순한 파티클 정도에 국한되고, 적용되더라도 매 프레임 정점을 변환하는 CPU 비용이 드로우콜 절약 효과보다 큰 경우가 빈번합니다.
이런 이유로 URP에서는 Dynamic Batching이 기본적으로 비활성화되어 있으며, 대신 SRP Batcher를 기본 배칭 방식으로 사용합니다. URP Asset의 Inspector에서 Dynamic Batching을 강제로 활성화할 수는 있지만, SRP Batcher가 대부분의 상황에서 더 효율적입니다.
SRP Batcher
SRP Batcher는 메쉬를 합쳐 드로우콜 수를 줄이는 대신, 드로우콜 사이의 GPU 상태 변경을 최소화하여 CPU 측 비용을 줄이는 방식입니다. 드로우콜 수는 그대로지만 각 드로우콜의 준비 비용이 낮아집니다. URP와 HDRP의 기본 배칭 방식에 해당합니다.
기존 방식의 문제
기존 렌더링 방식에서는 머티리얼이 바뀔 때마다 해당 머티리얼의 속성 값(색상, 텍스처 오프셋, 반사 강도 등)을 CPU가 GPU 메모리에 새로 업로드합니다. 다양한 머티리얼이 교차하는 실제 씬에서는 이 전송이 빈번하게 발생하여 CPU 비용이 증가합니다.
SRP Batcher의 동작
기존 방식에서 이런 일이 발생하는 이유는, 머티리얼 속성을 저장하는 GPU의 상수 버퍼(Constant Buffer, CBUFFER)가 하나뿐이고, 머티리얼이 바뀔 때마다 덮어써지기 때문입니다. 상수 버퍼는 셰이더 실행 중 변하지 않는 데이터를 저장하는 GPU 메모리 영역으로, 머티리얼 A의 값을 올린 뒤 머티리얼 B를 그리면 같은 버퍼가 B의 값으로 교체됩니다. 머티리얼 A를 다시 그려야 할 때 A의 값은 이미 사라졌으므로, CPU가 처음부터 다시 업로드해야 합니다.
SRP Batcher는 머티리얼마다 별도의 CBUFFER를 GPU 메모리에 할당하여, 속성 값을 유지합니다. 머티리얼 A와 B가 각자의 CBUFFER에 동시에 상주하므로 서로 덮어쓰이지 않고, CPU는 머티리얼이 바뀔 때마다 데이터를 복사하는 대신 사용할 CBUFFER만 지정하면 됩니다.
머티리얼 수만큼 CBUFFER가 상주하므로 GPU 메모리 사용량은 늘어나지만, 머티리얼이 바뀔 때마다 CPU가 데이터를 재업로드하는 비용이 사라지는 것이 핵심 이점입니다.
기존 방식에서는 머티리얼이 바뀔 때마다 속성 값을 CPU에서 GPU로 복사해야 했지만, SRP Batcher에서는 GPU에 이미 상주하는 CBUFFER를 지정하기만 하면 되므로 머티리얼 전환에 따른 CPU 비용이 크게 줄어듭니다.
SRP Batcher의 배칭 단위
하나의 셰이더는 기능 옵션의 조합에 따라 서로 다른 컴파일 버전인 셰이더 variant(Shader Variant)로 나뉩니다. 예를 들어 URP Lit 셰이더는 안개(Fog), 그림자 수신(Receive Shadows) 같은 기능을 켜거나 끌 수 있는데, 안개를 켠 Lit과 끈 Lit은 서로 다른 variant입니다. 같은 셰이더라도 variant가 다르면 GPU 입장에서는 별개의 프로그램이므로 패스 전환이 발생합니다.
SRP Batcher는 이 셰이더 variant를 배칭의 기준으로 사용합니다. 앞에서 살펴본 것처럼 머티리얼마다 별도의 CBUFFER가 GPU에 상주하므로, 머티리얼이 달라도 데이터를 재업로드할 필요 없이 CBUFFER만 전환하면 됩니다. 따라서 같은 셰이더 variant를 사용하는 드로우콜은 머티리얼이 달라도 하나의 배치로 묶입니다. 같은 variant끼리 묶이면 셰이더 패스 전환은 원래 발생하지 않고, 머티리얼 속성도 각자의 CBUFFER에 이미 상주하므로 재업로드가 필요 없습니다. 드로우콜 자체가 합쳐지는 것은 아니지만, 배치 안에서 머티리얼 전환 비용이 사라지는 것이 핵심입니다.
Static Batching과 Dynamic Batching은 같은 머티리얼이어야 배칭할 수 있었습니다. SRP Batcher는 셰이더 variant만 일치하면 되므로, 머티리얼 종류가 다양한 씬에서도 효과적으로 배칭됩니다.
장점과 단점
앞에서 살펴본 것처럼 CBUFFER 상주를 통해 머티리얼 전환 비용이 크게 감소합니다. 메쉬를 합치지 않기 때문에 Static Batching과 달리 메쉬 복제로 인한 메모리 증가가 없고, 움직이는 오브젝트에도 동작합니다. Dynamic Batching처럼 매 프레임 정점을 변환하는 작업도 필요 없어 런타임 CPU 비용이 추가되지 않습니다.
반면, 머티리얼 수만큼 CBUFFER가 GPU 메모리에 상주하므로 머티리얼 종류가 많을수록 GPU 메모리 사용량이 증가합니다.
작동 조건
첫째, SRP 호환 셰이더를 사용해야 합니다. URP/HDRP에 포함된 Lit, Unlit 등의 기본 셰이더는 이미 호환됩니다. 커스텀 셰이더의 경우, 앞에서 설명한 머티리얼별 CBUFFER에 해당하는 UnityPerMaterial과 오브젝트별 데이터(변환 행렬 등)를 담는 UnityPerDraw를 셰이더 코드에 올바르게 선언해야 합니다. Inspector에서 셰이더를 선택하면 “SRP Batcher: compatible” 표시로 호환 여부를 확인할 수 있습니다.
둘째, URP Asset의 Inspector에서 SRP Batcher가 활성화되어 있어야 합니다. URP에서는 기본적으로 활성화 상태입니다.
GPU Instancing
GPU Instancing은 같은 메쉬와 머티리얼을 사용하는 오브젝트가 여러 개일 때, 하나의 드로우콜로 한꺼번에 그리는 기법입니다. 이때 같은 메쉬를 공유하는 개별 오브젝트를 각각 인스턴스(Instance)라 합니다.
GPU Instancing의 핵심은 GPU가 메쉬 데이터를 한 번만 읽고, 각 인스턴스를 다른 위치·회전·스케일로 반복 렌더링하는 것입니다. 셰이더에서 인스턴스 프로퍼티를 선언하면 색상이나 밝기 같은 속성도 인스턴스별로 다르게 지정할 수 있습니다.
동작 방식
일반 렌더링에서는 오브젝트마다 드로우콜이 하나씩 필요합니다. GPU Instancing에서는 모든 인스턴스의 변환 행렬·속성을 하나의 인스턴스 버퍼로 묶어, 단일 드로우콜로 N개를 한꺼번에 렌더링합니다. GPU는 같은 메쉬를 반복 사용하면서 인스턴스 버퍼에서 1번, 2번, …, N번 데이터를 각각 적용합니다. 메쉬 자체가 복제되지 않으므로, Static Batching이 나무 50그루를 합쳐 정점 50,000개로 늘리는 것과 달리 원본 메쉬(1,000 정점)만 GPU에 유지됩니다.
적합한 상황
GPU Instancing은 같은 메쉬가 많이 반복될수록 효과가 큽니다. 숲의 나무, 풀, 돌처럼 같은 에셋이 수백~수천 개 배치되는 환경이 대표적이며, 전략 게임의 유닛이나 총알 같은 반복 오브젝트에도 적합합니다. 반면 같은 메쉬가 2~3개뿐이라면, 인스턴스 버퍼를 준비하는 비용에 비해 드로우콜 절약이 적어 효과가 미미합니다.
SRP Batcher와의 관계
GPU Instancing과 SRP Batcher는 같은 머티리얼에 동시에 적용할 수 없습니다. 두 기법이 CBUFFER를 사용하는 방식이 서로 달라, 하나의 머티리얼이 두 구조를 동시에 만족할 수 없기 때문입니다.
기본적으로 SRP Batcher가 우선 적용됩니다. 동일 메쉬가 대량 반복되는 머티리얼만 Inspector에서 “Enable GPU Instancing”을 켜면, 해당 머티리얼이 SRP Batcher 대신 GPU Instancing으로 동작합니다.
배칭 기법 비교
| 항목 | Static Batching | Dynamic Batching | SRP Batcher | GPU Instancing |
|---|---|---|---|---|
| 배칭 방식 | 빌드 시 메쉬 합침 | 매 프레임 합침 | 합치지 않음 | 합치지 않음 |
| 머티리얼 조건 | 같은 머티리얼 | 같은 머티리얼 | 같은 셰이더 variant | 같은 머티리얼 |
| 메쉬 제한 | 없음 | 최대 300 정점 | 없음 | 같은 메쉬 |
| 움직이는 오브젝트 | 불가 | 가능 | 가능 | 가능 |
| 메모리 증가 | 있음 (메쉬 복제) | 없음 | 있음 (CBUFFER 상주) | 없음 |
| 런타임 CPU 비용 | 없음 (빌드 시점) | 있음 (매 프레임) | 없음 | 없음 |
| URP 기본 상태 | 활성화 | 비활성화 | 활성화 | 비활성화 (SRP Batcher 우선) |
URP 환경에서는 SRP Batcher를 기본 전략으로 유지합니다. 머티리얼이나 메쉬의 종류에 관계없이 셰이더 variant만 일치하면 배칭되므로, 대부분의 씬에서 별도 설정 없이 머티리얼 전환 비용이 줄어듭니다.
여기에 보완적으로, 움직이지 않는 배경 오브젝트 중 같은 머티리얼을 공유하며 메모리 증가를 감수할 수 있는 경우에는 Static Batching을, 동일 메쉬가 수백 개 이상 반복되는 상황에서는 GPU Instancing을 적용합니다. Dynamic Batching은 SRP Batcher가 이미 머티리얼 전환 비용을 크게 낮추므로, 매 프레임 정점을 변환하는 추가 비용 대비 실익이 적어 URP에서는 기본 비활성화 상태입니다.
드로우콜 흐름 요약
지금까지 다룬 개념들이 실제 렌더링에서 어떤 순서로 동작하는지 정리해보면 다음과 같습니다.
다이어그램에서 “배칭 적용” 단계가 CPU 쪽에 위치하는 이유는, 배칭이 CPU에서 드로우콜을 묶거나 상태 변경 비용을 줄이는 작업이기 때문입니다.
마무리
- 드로우콜은 CPU가 GPU에 보내는 렌더링 명령이며, 실제 비용은 드로우콜 직전의 GPU 상태 변경을 CPU가 준비하고 전달하는 과정에서 발생합니다.
- SRP Batcher는 머티리얼 속성을 GPU의 CBUFFER에 유지하여 머티리얼 전환 비용을 줄이며, URP의 기본 배칭 전략입니다.
- Static Batching은 빌드 시점에 메쉬를 합쳐 드로우콜을 줄이지만, 정점 데이터 복제로 메모리가 증가합니다.
- GPU Instancing은 동일 메쉬가 수백 개 이상 반복되는 상황에서 효과적이지만, 같은 머티리얼에 SRP Batcher와 동시에 적용할 수 없습니다.
이 글에서는 GPU에 렌더링 명령을 보내는 비용, 즉 “어떻게” 보내는지를 다뤘습니다. 그러나 비용을 줄이는 것만큼, 화면에 보이지 않는 오브젝트를 GPU에 아예 제출하지 않는 것도 중요합니다. Part 3에서는 “무엇을” 보낼지를 줄이는 방법, 즉 GPU에 제출할 오브젝트 수 자체를 줄이는 컬링(Culling)과 거리에 따라 메쉬 복잡도를 낮추는 LOD 시스템을 다룹니다.
관련 글
시리즈
- Unity 렌더 파이프라인 (1) - Built-in과 URP의 구조
- Unity 렌더 파이프라인 (2) - 드로우콜과 배칭 (현재 글)
- Unity 렌더 파이프라인 (3) - 컬링과 오클루전
전체 시리즈
- 게임 루프의 원리 (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) - 빌드와 품질 전략