이 포스팅은 이전 'DrawCall(드로우콜)과 렌더링 최적화' 에 이어서 작성된다.
2026.04.04 - [유니티 학습/C#과 유니티] - DrawCall(드로우콜)과 렌더링 최적화
DrawCall(드로우콜)과 렌더링 최적화
DrawCall(드로우콜)이란?모든 게임 프로젝트를 진행하다보면, 혹은 게임을 플레이 할 때면 화면에 각종 오브젝트가 그려지고 화려한 그래픽이 보일 것이다.이 그래픽 관련해서는 GPU, 그래픽카드가
puudding.tistory.com
정적 Batching, 말 그대로 움직이지 않는 오브젝트들을 대상으로 이루어지는 최적화다.
Unity에서 이 Static Batching 을 사용하려면 오브젝트에 Static 체크가 필수이다.
그 방법으론, Material 을 공유하는 메시들을 묶어서 DrawCall 한다는 생각보다 간단한 방법인데, 예시를 들어서 설명해보자.
레벨에 3개의 메시가 배치되어 있고, 모두 같은 메테리얼을 사용한다고 가정한다.
이 경우 Static Batching 을 사용하지 않으면 총 3번의 DrawCall 이 일어나지만, 메시를 하나로 묶어서 처리하면 1번의 DrawCall 만 수행된다.
계산해야 할 연산량 자체는 줄어들어 런타임 성능은 좋아지지만, 메모리를 많이 점유한다는 단점이 있는데, 흩어져있는 메시들을 묶어서 하나의 거대한 Combined Mesh 를 새로 만들기 때문이다.
원본 메시들은 그대로 메모리에 존재하고, Batching 을 통해서 합쳐진 거대한 메시가 추가로 생성되기 때문에 메모리가 거대하게 뻥튀기 될 수밖에 없는 것이다.
단순히 2배라고 생각할 수도 있지만, 동일 오브젝트가 많을 수록 중복 데이터를 저장하게 되고, 이로 인해 메모리 사용량이 급증하게 되는 현상이 일어난다.
이와 유사한 기법으로 Unity에서 제공하는 시스템인 CombineMeshs 가 있다.
Static Batching 은 오브젝트에 Static 체크만 해주면 Unity 시스템에 의해서 자동적으로 수행된다.
이 CombineMeshs는 개발자가 수동으로 원하는 오브젝트를 배칭처리 하는 것이다.
단순히 생각해서 Static Batching 이랑 동일하지 않나? 라고 볼 수 있지만, 정확히 이야기하자면 Static Batching 이 내부적으로 사용하는 핵심 기술을 개발자가 직접 꺼내 쓰는 것이라고 볼 수 있다.
Mesh.CombineMeshs 메서드는 개발자가 C# 에서 직접 호출하는 수동 방식으로, 지정한 여러 개의 메시 데이터를 런타임에 하나의 새로운 Mesh 에셋으로 물리적으로 합친다.
수동으로 제어하는 만큼 어떤 오브젝트를, 언제 합칠 지 정할 수 있고, 추가로 Combine 후 원본 메시들을 Destroy/Unload 해버리면 메모리를 절약할 수 있다.
메모리를 절약한다는 것은? DrawCall 을 줄일 수 있다는 것이다.
그렇다면 자연스럽게 Static Batching 을 사용하기보다 Mesh.CombineMeshs 메서드를 사용하는게 더 효율적이지 않나? 라는 의문을 가지게 된다.
결론부터 얘기하면 그렇다. 하여 실력있는 최적화 프로그래머라면 이 기법을 효율적으로 활용한다곤 한다.
다만 이 수동 방식이 무조건 성공하지 못하는 '트레이드 오프(Trade Off)'가 있다는 것을 인지해야 한다.
1. 구현 복잡도 : 모든 오브젝트를 수동으로 합치려면 Material 관리, 콜라이더 재배치 등 고려해야 할 사항이 기하급수적으로 많아진다.
2. 유연성 부족 : CombineMeshs 로 만들어진 거대한 하나의 메시는 다시 낱개로 움직이거나 수정하기 어렵다는 사항이 있다. 반면에 Static Batching 으로 합쳐진 메시는 원본 데이터가 남아있기 때문에 에디터에서 쉽게 원상복구가 가능하다.
3. 오버헤드(Overhead) : 런타임에 메시를 합치는 연산 자체가 CPU 를 꽤나 점유한다. 따라서 플레이의 원활함을 위해 로딩 화면이나 프레임 드랍이 없는 지점에서 정교하게 실행해야 한다는 제약이 있다.
그럼에도 Mesh.CombineMeshs 를 사용하고자 하여, Static Batching 을 사용하지 않겠다 하면 그 또한 방법은 물론 존재한다.
1. 전역 설정: Project Settings > Player > Other Settings에서 Static Batching과 Dynamic Batching 체크를 꺼버릴 수 있다
2. 개별 설정: 오브젝트의 Static 플래그 중 Batching Static만 체크 해제하면 해당 오브젝트는 자동 배칭에서 제외
실제로 대규모 오픈월드나 모바일 극한 최적화 프로젝트에서는 자동 배칭을 꺼버리고 오브젝트 컴바이너(Object Combiner) 시스템을 사용하는 경우가 있다고 한다.
Static 이 있다면, Dynamic 도 존재해야 하지 않겠는가? 물론 존재하고, 그 의미 또한 반대로 동작한다.
런타임에 움직이는 오브젝트들(Vertex 수가 적어야 한다는 제약이 있다)을 묶어서 DrawCall 하는 최적화 기법이다.
여기서 Vertex, 정점의 수가 적어야 한다는 기준은 Unity 공식 가이드 기준으로 개별 메시의 정점 속성(Vertex Attributes) 합계가 900개 미만이어야 한다는 것이다.
단순 정점 위치만 있는 경우 900개까지 가능하지만, 보통 정점은 '위치 + 법선(Normal) + UV1 + UV2 ...' 등 여러가지 속성을 가지게 되고, 속성이 많아지면 보통 정점 300개 미만인 오브젝트들만 Dynamic Batching 의 대상이 된다.
(여기서 말하는 Vertex 는 폴리곤을 의미하는게 아니니 혼동하지 말도록 하자)
Dynamic Batching 이 동작하면, CPU가 매 프레임 정점 위치를 계산해서 임시 버퍼에 합친 후 GPU로 전송한다.
다만 움직이는 물체인 만큼, 매 프레임마다 메시들을 수집&재생성해야 한다는 것이고, 그에 따라 추가적인 메모리 소모와 오버헤드 발생 가능성이 있다.
이러한 배칭 연산 비용이 드로우콜 비용보다 커지면 오히려 손해라는 점에서 사용한다면 주의해야 할 최적화 기법이 된다.
이 드로우콜 비용보다 커진다 라는 판단은 Unity 엔진(CPU)가 런타임에 실시간으로 수행한다.
1. 렌더링 할 오브젝트를 수집하고,
2. 이 메시가 Dynamic Batching 조건(정점 수, 메테리얼 일치 등) 만족 여부를 체크한다.
3. 만족할 경우, CPU는 이 정점들을 새로운 메모리 버퍼로 복사하고 좌표를 월드 공간으로 변환(Matrix Multiplication)한다.
이 때의 손익 분기점은 DrawCall 비용이 되는데, CPU가 GPU에 명령을 하나 보내는 고정 비용(ex 1)을 기준으로 한다.
Batching 비용은 정점 하나를 계산해서 메모리에 새로 쓰는 비용(ex 1.5)이 되는데, 그에 따라 정점 수에 비례하여 증가한다.
따라서 정점이 많아질 수록, 하나하나 계산해서 합치는 비용이 그냥 DrawCall 을 N번 수행하는 것보다 커지게 되는 것이다.
여기까지 두 가지의 최적화 기법을 봤을 때, 장점도 분명하지만 그만큼 단점도 분명한 최적화 기법들이라고 볼 수 있다.
그에 따라 현대에 들어서며 최신화 된 최적화 기법을 더 소개하고자 한다.
Instancing 이라는 단어에서 짐작할 수 듯이 '인스턴스화' 의 개념으로, 한번의 DrawCall 로 동일한 오브젝트의 여러 복사본을 렌더링한다는 최적화 기법이다.
예를 들어 레벨 디자인에 수만 개의 잔디가 있다고 해보자. 이러한 잔디는 동일한 오브젝트(메시, 메테리얼)로 위치와 회전값 등 Transform 정보만 다르게 하여 배치할 것이다.
이렇듯 1개의 메시 데이터를 대상으로 각 물체의 Transform 정보만 리스트로 전달하며 GPU가 알아서 여러개 그릴 수 있도록 처리하는 것이다.
여러 개의 메시를 묶어서 처리하는 Static Batching보다 런타임 오버헤드가 적고 메모리 효율이 좋다는 장점이 있지만, 쉐이더에서 인스턴싱을 지원해야 한다는 확인 사항이 존재한다.
현대 Unity의 새로운 렌더링 파이프라인(URP, HDRP)에서 가장 권장되는 방식이다.
Material 이 달라도 쉐이더 변수(CBuffer)가 같다면 Batching 을 유지해주는 방식으로, SetPass Call 을 줄이기보다 SetPass Call 의 비용 자체를 줄이는 최적화 기법이다.
이 방식이 어떻게 이루어지냐면, Render State 변경 비용 자체를 줄이는 것으로 GPU 메모리에 데이터를 미리 올려두고 값만 바꿔서 그린다는 것이다.
다만 URP, HDRP 전용이며, 쉐이더 코딩 규칙을 지켜야 한다는 제약이 존재한다.
3번과 4번에 대해서는 다소 설명이 빈약하게 느껴질 수 있는데, 이어서 확인하는 Key Question 을 보며 추가적으로 설명한다.
이 이유는 두 가지를 들며 설명할 수 있다.
1. 컬링(Culling) 효율 저하
Static Batching 을 사용하면 하나의 거대한 메시가 되어, 카메라뷰에 담기지 않는 메시에 대해서도 정점 계산이 이루어져 GPU에 부하가 걸리게 된다.
2. 메모리 관점
Static Batching 은 동일한 잔디라도 갯수에 따라 정점 데이터를 모두 메모리에 새로 복사해서 올리는 반면, GPU Instancing은 원본 잔디 메시 1개만 메모리에 올리고 Transform 정보만 다르게 전달하니 메모리 사용 측면에서 훨씬 이득을 볼 수 있다.
Static/Dynamic Batching 은 '메테리얼을 공유하는' 이라는 제약이 존재한다.
이는 과거에는 메테리얼이 바뀌면 GPU 에 Render State 에 대해 일일이 SetPass Call 을 진행하였다.
반면 SRP Batcher 는 메테리얼마다 다른 데이터들을 GPU 메모리의 CBuffer(전용 공간)을 미리 배치하고, CPU가 값이 바뀔 때만 Update한다.
게임이 시작될 때(메테리얼 을 로드 할 때) GPU 메모리에 공간을 할당하고, 스크립트나 에디터에서 개발자가 메테리얼의 속성을 바꿀 때만 GPU로 전송하는 것이다.
(여기서 주의할 점으로 Texture 는 CBuffer 에 들어가는 것이 아니라 별도의 슬롯에 바인딩된다)
이후 렌더링 시 CPU는 Render State를 모두 새로 보내는 것이 아니라, '이미 보냈던 데이터 중 1번 메테리얼의 주소값만 참고해서 그려라' 라고 지시가 가능하다.
메테리얼이 다르더라도 쉐이더(Shader Code)가 같다면 Batching 과 유사한 속도로 데이터 바인딩 비용을 절감할 수 있는 것이다.
동일한 메테리얼에 다른 Render State 가 가능한가? 하면 그것은 불가능하다.
Render State 가 조금이라도 다르다면 그것은 서로 다른 메테리얼이라는 것이고, SRP Batcher는 이 개별 메테리얼마다 고유한 CBuffer 영역을 할당한다.
'메테리얼이 다르더라고 쉐이더가 같다면' 이라는 말은 Render State 가 같다는 것이 아니라 그 값을 처리하는 공식(Shader Code, ex) Lit.Shader)가 같다는 것이다.
우리가 에디터에서 게임을 개발하다보면 동일한 메테리얼이라고 할 지라도 내부 속성값을 다르게 하는 등 많은 종류의 메테리얼을 다루게 된다.
하지만 사용하는 쉐이더 종류는 생각보다 많지 않고, 그것이 SRP Batcher 가 효율적인 이유로 연결된다.
이는 우리가 게임을 하다보면 흔히 접하는 그래픽 퀄리티가 높은 게임, AAA급 게임들을 대상으로 하는 말로 플레이 체감으로 쉽게 이해가 가능한데, '고사양 게임의 높은 폴리곤 수' 와 'CPU 연산 비용이 DrawCall 비용보다 높아서'가 그 이유이다.
현대 CPU는 DrawCall 성능 자체가 과거보다 많이 좋아졌다.
반면 정점(Vertex)를 매 프레임 계산해서 메모리를 새로 쓰는 작업은 여전히 CPU 캐시 히트율을 저하시킨다.
이는 그냥 따로 그리는게 합쳐서 주는 것보다 훨씬 빠른 상황(Dynamic Batching 의 단점과 연결)이 나오게 된 것이다.
그럼 CPU가 아니라 GPU를 고려하면 어떨까? GPU 사양이 높으면 Dynamic Batching 이 유리해질까?
아쉽게도 그것은 아니다. 오히려 더 안좋다 라고도 말할 수 있다.
Dynamic Batching 연산은 100% CPU에서 일어난다. GPU 사양은 DrawCall 에 대한 처리 능력에 연관이 있는 것이다.
따라서 GPU 사양이 높을 수록, 많은 수의 DrawCall 을 처리하는 능력이 좋다는 것이고, 결과적으로 Dynamic Batching 을 사용하지 않는 것이 정석이 된다.
| 내적(Dot Product)과 외적(Cross Product) (1) | 2026.04.11 |
|---|---|
| Unity의 생명주기(Life Cycle) (0) | 2026.04.11 |
| DrawCall(드로우콜)과 렌더링 최적화 (0) | 2026.04.04 |
| 박싱(Boxing), 언박싱(UnBoxing) (0) | 2026.04.03 |