본문 바로가기
개발 (Game)/Unity (General)

[Unity] Static Batching, GPU Instacing, SRP Batcher

by 진현개발일기 2025. 12. 20.

[용어 정리]

1. Static Batching (개체 단위)

동일 머테리얼을 쓰는 서로 다른 메쉬들을 하나의 거대한 Combined Mesh(Chunk)로 합쳐

메모리에 로드한다.

  • Static Batching은 CPU 단계에서 메시를 합쳐서 GPU에 넘긴다.
  • 행렬(ObjectToWorld, Transform, etc)은 bake 시점에 메시 정점에 이미 반영된다.
  • 그래서 런타임에 per-object 행렬 업데이트 자체가 없다.

※ 즉, 군집된 메시를 한 번에 렌더링해서 Drawcall 최적화를 해준다. 그래서 GPU Instancing 후보군에서 완전히 제외된다.

 

2. GPU Instancing (머테리얼 단위)

동일 메쉬 + 동일 머테리얼 사용 시, 원본 메쉬의 Vertex/Index Buffer를 GPU에 탑재 후 공유 상태로 유지시킨다.

 

Drawcall을 생성할 때는 원본 메시에 대한 참조 주소개별 개체에 대한 행렬(트랜스폼 행렬 등)들을

하나의 배열(Matrix4x4[])에 담고 넘겨서 한 번에 렌더링하도록 한다.

 

※ 즉, Drawcall 최적화를 해준다.

 

* GPU Instancing이 켜져있지만 그 머테리얼을 사용하는 개체가 Static Batching이 켜져있다면 그 개체의 GPU Instancing은 disable 된다. 하지만 GPU Instancing은 머테리얼 단위이기 때문에 머테리얼 자체에는 'Instancing' 태그가 붙게 되어 그 개체는 인스턴싱 후보군이 되어 엔진 스케쥴링에 영향을 주게 된다. (자세한 추가 설명은 맨 밑에 有)

 

3. SRP Batcher (쉐이더 단위)

위 두 기술의 목적[Drawcall 감소]과 다름.

 

동일 쉐이더 프로퍼티를 담을 머테리얼 상수 버퍼(Material CBUFFER)와

오브젝트별 행렬 정보 [트랜스폼 행렬 · 변환 행렬 등]를 담을 오브젝트 상수 버퍼(PerObjectLargeBuffer)를 GPU에 미리 상주시켜 놓고 고정된 offset 위치에 행렬 값만 업데이트 함으로써, 드로우콜 호출 시 발생하는, 세팅 비용을 절감하는 기술이다.

기존 SetShaderPass 구조 vs SRP Batcher 적용 후 SetShaderPass 구조

(출처: https://docs.unity3d.com/kr/2019.4/Manual/SRPBatcher.html)

 

[SRP Batcher 미적용 시 (Unity 전통적 방식)]

Drawcall 호출이 될 때마다 기존 CBUFFER에 값을 매번 채웠다.

즉, 아래와 같이 렌더링했다.

 

(1) CPU가 Drawcall 하나를 준비할 때마다 아래 내용을 채워 넣었다.

  • 머테리얼 상수들 (색, 텍스처 인덱스, 파라미터 등)
  • 오브젝트 행렬 (ObjectToWorld, Transform Matrix, 등등)

(2) 이 값들을 CPU에서 GPU로 전달했다.

(3) GPU 상태를 변경시킨다. (SetPass)
(4) Drawcall 이 실행된다.

 

※ 중요한 점

이 때도 '머테리얼 상수 버퍼', '오브젝트별 상수 버퍼' 등의 '논리적인' 구분은 존재했다.

하지만, 구조적으로 CPU가 이 둘을 구분해서 재활용하지는 않았다.

즉, 그냥 이번 Draw에 필요한 모든 상수를 다시 세팅하고 Drawcall을 날린 것이다.

 

[SRP Batcher 적용 시]

SRP Batcher는 위에서 언급한 '머테리얼 상수 버퍼', '오브젝트별 상수 버퍼'의 논리적 개념을

실체화하고 GPU에 상주시켜 재활용하도록 만들었다.

 

구조는 아래와 같다.

 

상수 버퍼 (CBUFFER) 두 종을 GPU에 탑재한다.

하나는 머테리얼 버퍼 (UnityPerMaterial)이고 하나는 개체별 버퍼(UnityPerDraw)이다.

 

(1) 머테리얼 버퍼(Material CBUFFER)

동일한 Shader Variant를 사용하는 머테리얼들의 프로퍼티 데이터를 GPU에 상주시켜 재사용하기 위한 버퍼다.

  • Shader Varaint (Pass & Keywords & Render State)가 같아야 SRP Batcher가 묶을 수 있다.
  • Material Instance가 달라도 가능하다.
  • 셰이더에서의 논리적인 인터페이스를 칭할 때는 'UnityPerMaterial'라고 칭하고
  • 실제 GPU 메모리에 상주 되어있는 버퍼 공간 'Material CBUFFER'라고 칭한다

 

(2) 개체별 버퍼 (PerObjectLargeBuffer)

개체 별 행렬의 데이터를 담는 공간이다. 기존에는 Drawcall 호출 할 때마다 버퍼에 상수 값 세팅 및 바인딩을 매 번 해줬다 (SetPass 비용).

 

하지만 SRP Batcher가 GPU에 미리 할당된 큰 상수 버퍼 풀(PerObjectLargeBuffer)을 상주시키고나서 아래와 같이 바뀌었다.

  • 정해진 Offset에 행렬 값만 업데이트 시켜준다.
  • 바인딩/레이아웃은 유지한다.
  • 셰이더에서의 논리적인 인터페이스를 칭할 때는 'UnityPerDraw'라고 칭하고
  • 실제 GPU 메모리에 상주 되어있는 하나의 큰 버퍼 공간PerObjectLargeBuffer라고 칭한다.

    (예시)
  • GPU Memory
    └── PerObjectLargeBuffer (하나의 큰 실제 GPU 버퍼)
        ├── offset 0  ← DrawCall A의 UnityPerDraw 데이터
        ├── offset 1  ← DrawCall B의 UnityPerDraw 데이터
        └── offset 2  ← DrawCall C의 UnityPerDraw 데이터

 

(3) 결론

Before SRP Batch After SRP Batch
· Drawcall마다 상수 값들을 세팅해줬다.
· Drawcall마다 바인딩을 했다.
· 이로 인하여 SetPass 비용이 발생했다.
· Material CBUFFER 같은 경우 상수 세팅을 안하는 혜택을 보게 되었다.

· 머테리얼 상수 버퍼 & 오브젝트 별 상수 버퍼 모두
바인딩/레이아웃은 유지하게 되어 바인딩 비용을 감소시켰다.

· UnityPerDraw [오브젝트 별 상수 버퍼]는 정해진 Offset에 행렬 값만 memcpy를 해주기 때문에 per-object 상수 구조체를 구성해서 세팅하는 비용을 최적화 시켜줬다. 즉, 상수 값을 세팅하지만 그 비용을 매우 싸게 바꿔줬다.

※ 공통적으로 CPU 렌더 스레드를 대폭 최적화 해줬다.

 

SRP Batcher 렌더링 워크플로

(출처: https://docs.unity3d.com/kr/2019.4/Manual/SRPBatcher.html)

 

 

※ 행렬 배열 데이터의 우선 순위

만약 어떤 오브젝트가 SRP Batcher와 GPU Instancing 조건을 동시에 만족한다면, 유니티는 다음과 같이 결정한다.

  • 1순위: GPU Instancing (작동 시)
    머테리얼에 인스턴싱이 켜져 있고, 조건(동일 메쉬/머테리얼)이 맞아서 'Instanced Draw Call'이 발생하는 순간,
    해당 Draw는 SRP Batcher 경로를 타지 않는다. 대신에 인스턴싱 전용 경로를 탄다.

    그에 따라 SRP Batcher의 UnityPerDraw CBUFFER/ UnityPerMaterial CBUFFER는 참조하지 않고
    Instancing 용 Structured Buffer 및 Instance Array(ObjectToWrold 행렬 등을 담는 공간)을 사용하여 한 번에 그려낸다.

    ※ 즉, SRP Batcher 적용 대상에서 제외된다. 대신 Draw Call 수 자체를 획기적으로 감소시킨다.
  • 2순위: SRP Batcher (나머지)
    인스턴싱 조건이 맞지 않거나 꺼져 있어서 개별 드로우콜로 날아갈 때는,
    SRP Batcher가 GPU 메모리에 미리 올려둔 상수 버퍼(CBUFFER)의 행렬 데이터를 사용한다.

 

※ 메모리를 좀 더 쓰더라도(인스턴싱용 배열 등), CPU의 Draw Call 오버헤드를 줄이는 게 성능 이득이 훨씬 크기 때문에 인스턴싱을 1순위로 쓰는 것이다.

 

4. 요약

기술 최적화 대상 적용 단위 메커니즘
Static
Batching
Draw call 개체 단위 동일 머테리얼을 쓰는 서로 다른 메쉬들을 하나의 거대한 Combined Mesh(Chunk)로 합쳐 메모리에 로드한다.

Drawcall 1회 요청으로 군집된 메시를
한 번에렌더링하는 기술이다.
GPU
Instancing
Draw call 머테리얼 단위 동일 메쉬 + 동일 머테리얼 사용 시, 원본 메쉬 하나만 GPU에 로드한다.

원본 메쉬에 대한 주소[참조]와 개별 개체에 대한 행렬(트랜스폼 · 변환 등) 값들을 하나의 배열로 넘겨 한 번에 렌더링한다.
SRP
Batcher
SetPass call 쉐이더 단위 동일 쉐이더 프로퍼티 데이터머테리얼 상수 버퍼(Material CBUFFER)에 담고, 개체별 행렬 정보 등을 담을 오브젝트 별 상수 버퍼(Object CBUFFER) 크게 이 두 가지를

GPU에 미리 로드해서 상주시켜놓고, 드로우콜 호출 시 발생하는 상수 세팅 비용 & 바인딩 비용을 절감하는 기술이다.

※ SRP Batcher는 새로운 '상수 버퍼'를 발명한 것이 아니라, 매번 새로 짐을 싸던 CPU에게 '전용 보관함(Persistent CBUFFER)'을 준 것이다. 이로 인하여 CPU는 짐을 싸는 대신 '주소(Offset) 쪽지'만 던진다.

 

 

 

 

[개념 추가 정리 & Shadow Pass 단계에서 주의]

 

※ 이 글을 쓰게 된 이유 자체가 개발 도중에 이 상황을 겪어서 난감했기 때문에 개념 및 지식을 정리하기 위해서였다.

 

1. 일반 Draw Opaque Object Pass, Draw Transparent Object Pass 에서는
Static Batching → GPU Instancing → 개별 DrawCall 순으로 처리된다.

 

 

2. 그 Drawcall을 구상할 때 SRP Batcher는 동일 Shader Variant에 대해
머테리얼 상수 버퍼(UnityPerMaterial)를 GPU에 상주시켜 재사용하고,
개체별 상수 버퍼(UnityPerDraw)는 고정된 레이아웃을 유지한 채 행렬 값만 갱신하여
DrawCall 직전의 상수 세팅 및 바인딩 비용을 줄인다.

 

 

3. Shadow Pass에서는 Shadow Cascades(그림자 거리 단계)에 따라서 범위 내 모든 개체들을 검사한다.

이 단계에서 Static Batching이 되어 있어도 효율적인 그림자 처리를 위해 다시 개별 단위로 쪼개서 검사를 하는 경우가 있다.

이때 'Instancing'태그로 인해 Static Batching과 뒤섞여 문제가 발생할 수 있다. (내가 겪은 부분)

 

 

4. 기본적으로 Unity는 Material이 GPU Instancing 가능 상태일 경우,

동일 Shader Variant 및 Render State를 공유하는 객체들을
Instanced Draw 후보군으로 분류하여 정렬 단계에서 함께 처리하려 한다.

 

 

5.  (★) 하지만 다른 pass와 달리 Shadow Pass는, 아까 말했듯이, 물체 단위로 개별 검사를 하는데

이때 material이 'Instancing 후보군'이라면 개체의 Static Batching 여부와 상관없이

같은 cascades 안에 있는, 서로 다른, instancing 개체들과 묶어서 처리하려고 한다.

 

 

6. 이 과정에서 렌더링 순서 내에
Static Batch → Instanced Draw → Static Batch
와 같은 패턴이 발생하면,
SRP Batcher는 Shader Variant 상태를 다시 확인·세팅해야 하므로
SetPass Call이 추가로 발생할 수 있다.

 

[예1: GPU Instancing (풀: On, 암벽 : on), 방해 받은 상황]
[암벽(Static) - 풀 (Instancing) - 암벽 (Static)]

암벽 사이에 풀이 배치 됨.
이 상황에서 암벽 쉐이더에 대한 SetPass 진행 중
풀의 SetPass를 추가적으로 세팅한 뒤
다시 암벽 Setpass를 추가 세팅해야 하는 상황이 발생함.

즉, 예로, 여기서는 SetPass Call 3회 발생

[실제 내 프로젝트에서 겪은 상황]


Shadows.DrawSRPBatcher 횟수가 늘어났는데,
Batch cause를 보니 아래와 같음.
"Objects either have different shadow caster shaders, or have different shader properties / keywords that affect the output of the shadow caster pass."

Meshes를 통해 확인해보니 SRP Batch 대상(3)이 Static -> Instancing Only -> Static 순서였음.
SetPassCall 수치도 67이었다.

[예2: GPU Instancing (풀: On, 암벽 : Off), 방해 받지 않은 상황]
 [풀(Instancing) - 암벽 A (static) - 암벽 B (Static)]

풀과 암벽 사이에서만 배치가 한 번 끊기고,
쉐이더 프로퍼티가 같은 암벽들 사이에는 배치가 끊기지 않아서
추가 SetPass Call이 발생하지 않음.

즉, 여기서는 SetPass Call 2회 발생


[아래 문제를 해결 후 내 상황]
Shadows.DrawSRPBatcher가 큰 Static Batching Chunk를 한 번만 그려내고 (마지막 그룹(50)아래에서 1회)
나머지 Render State가 다른 오브젝트들끼리 SRP Batching이 되었다.

SetPassCall이 64로 줄었다.


 

 

'개발 (Game) > Unity (General)' 카테고리의 다른 글

[Unity] Motion Vector  (0) 2025.05.01
[Unity] LOD + Lightmap vs APV  (0) 2025.05.01
[Unity] Renderer  (0) 2025.04.17
[Unity] Reflection Probes  (0) 2025.04.15
[Unity] Light Probes  (0) 2025.04.15