참치김밥은 최고의 한식이다

[유니티] 유니티 그래픽스 최적화 : 드로우콜과 배칭 본문

Unity/최적화

[유니티] 유니티 그래픽스 최적화 : 드로우콜과 배칭

l__j__h 2024. 2. 26. 10:46

01. 드로우콜

드로우콜(Draw Call)이란?

CPU가 GPU에게 오브젝트를 그리라는 명령을 호출하는 것.

 

드로우콜 과정에는 무슨 일이 일어나나??

일단, CPU가 활용하는 메모리는 RAM, GPU가 사용하는 메모리는 VRAM(Video RAM 혹은 GPU 메모리)이다.

CPU가 HDD, SSD, SD카드 등의 저장소로부터 파일을 읽고, 데이터를 파싱하여 CPU 메모리에 올린다. 그 후, 일반적으로 CPU 메모리의 데이터는 GPU에서 바로 접근하는 것이 불가능하므로, CPU 메모리의 데이터를 GPU 메모리로 복사하는 과정을 거친다.

이때, 만약 데이터를 메모리에 복사하는 과정을 매 프레임마다 수행하면 성능적으로 매우매우 떨어질 것이다. 따라서, 로딩 시점에 메모리에 데이터를 올려두고, 씬 전환 시점 등에 데이터를 해제한다.

즉, 게임이 수행되는 동안에는 텍스처나 버텍스 등 렌더링에 필요한 데이터들이 GPU 메모리에 존재해야 한다.

또, GPU에는 그려야 하는 상태 정보, 즉 렌더 상태(Render States)을 가지고 있다. 텍스처는 무엇을 사용할지, 쉐이더는 무엇을 사용할지 등의 상태 정보를 기억해두는 것이다. (알파 블렌딩 모드, Z 테스트 사용 유무 등의 정보도 포함)

CPU가 이 렌더 상태를 변경하는 명령을 커맨드 버퍼(Command Buffer)에 저장해두면, GPU는 커맨드 버퍼로부터 명령들을 순차적으로 가져가서 수행한다

렌더 상태 명령들을 모두 보내면, CPU는 마지막으로 GPU에 메시를 그리라는 명령을 보낸다. 이 명령이 Draw Primitive Call(DP Call) 이다.

위의 과정을 통틀어 Draw Call 이라고 하고, 이 작업을 다른 오브젝트들에 대해서 반복한다.

 

그래픽스 API 간의 차이

Vulkan에서는 여러 개의 커맨드 버퍼를 이용하여 멀티쓰레드로 병렬 처리한다.

Metal도 비슷한 과정으로 병렬 처리한다.

-> 따라서, Vulkan과 Metal은 OpenGL ES보다 드로우콜로 인한 부담이 덜하다.

 

드로우콜은 CPU 성능에 의존적이다

그래픽스 API들은 CPU에서 GPU로 보내는 명령을 공통적인 API로 구성한다. API가 호출되면 드라이버 칩셋에 맞는 신호를 전달하여 GPU에 맞게 명령을 해석하고 변형한다. 이 과정을 거치기 때문에 CPU가 GPU로 명령을 보낼 때 오버헤드가 발생한다. 따라서 명령을 GPU로 보내는 오버헤드는 CPU 바운더리의 오버헤드가 되고, 게임의 병목으로 이어진다.

 

멀티쓰레디드 렌더링

위와 같은 오버헤드는 주로 CPU의 메인 쓰레드에서 이루어진다. 만약 렌더링에 필요한 작업들을 별도의 쓰레드에서 할 수 있다면, CPU에 대한 부담이 그만큼 줄어들 것이다.

-> 멀티 쓰레디드 렌더링의 목적!!

 

플레이어 설정 - Other Settings - Rendering 항목에서 멀티쓰레디드 렌더링 옵션을 확인할 수 있다.

이 옵션이 켜져 있으면 렌더링에서 CPU의 멀티 코어를 활용할 수 있다. 하지만, 이 기능은 디바이스의 성능에 따라 편차가 있다. 무슨 말이냐면, CPU의 코어 수가 많지 않은 구형 디바이스의 경우, 그다지 효율적이지 않을 수 있다.

 

02. 배칭

배칭(Batching)이란?

드로우콜을 줄이기 위한 가장 효율적인 기능 중 하나이다.

여러 드로우콜이 필요한 상황을 하나의 드로우콜로 묶는 과정을 뜻한다.

 

(1) 스태틱 배칭 (Static Batching)

Static한, 즉, 정적인 오브젝트들에 대해 배칭을 수행하는 기능이다. 이 기능을 적용하기 위해선, 정적인 오브젝트들의 인스펙터에서 static 옵션을 활성화해주면 된다. 사용법이 매우 간단하다.

런타임 전에 static 오브젝트들의 지오메트리 정보를 이용해서 별도의 Mesh 로 합친다. GPU는 이를 가져다가 렌더링한다.

단점 :

- 별도의 Mesh를 생성하는 과정에서 런타임 오브헤드 발생 가능

- 별도의 Mesh를 사용하므로 메모리 이슈 발생 가능

 

(2) 다이나믹 배칭 (Dynamic Batching)

동적으로 움직이는 오브젝트들끼리 배칭을 수행하는 기능이다. 따라서, static으로 체크되지 않은 오브젝트들에 수행한다. 다이나믹 배칭은 유니티가 자동으로 해준다.

런타임 상에서, 매 프레임마다 동일한 머터리얼을 사용하는 동적인 오브젝트들의 버텍스들을 모아서 합쳐준다. 이러한 정보를 다이나믹 배칭에 쓰이는 버텍스 버퍼와 인덱스 버퍼에 담는다. GPU는 이를 가져가서 렌더링한다.

단점 :

- 매번 데이터 재구성이 필요하기 때문에 매 프레임마다 오버헤드 발생 가능

(단, 오버헤드를 갖더라도 드로우콜을 줄임으로써 전체적인 성능 향상을 노릴 수 있다.)

- Skinned Mesh 에는 적용 불가능

- 버텍스가 너무 많은 메시는 다이나믹 배칭의 대상에서 제외됨

+)

메시를 렌더링할 때에는 버텍스 쉐이더에서 좌표계 변환이 이루어진다. (object -> world) 이 과정은 GPU에서 빠르게 연산된다.

하지만 다이나믹 배칭을 위해서는 버텍스의 좌표 변환이 CPU에서 이루어져야 한다. 따라서, CPU에서의 변환 연산이 드로우콜보다 더 많은 시간을 잡아먹게 되면 오히려 효율이 떨어지게 된다.

예를 들어, Metal 과 Vulkan 은 드로우콜이 꽤 빨라서, 다이나믹 배칭의 오버헤드가 크면 오히려 효율이 더 안 좋을 수 있다.

 

만일, 특정 오브젝트의 배칭 오버헤드가 더 크다고 판단되면 해당 오브젝트에 대해 배칭을 제외시켜줄 수 있다.

이는 쉐이더 태그에서 Diable Batching 플래그를 True로 설정해주면 된다.

SubShader {
	Tags { "RenderType" = "Opaque" "DisableBatching" = "True" }
    ...

 

 

(3) CombineMeshes

여러 개의 메시들을 하나의 메시로 Combine하여 드로우콜을 줄이는 방법이다. 실제로 사용해봤던 방법으로, 유니티에서 제공하는 기능도 있고 에셋 스토어에서도 여러 Combiner를 제공한다.

Mesh Renderer의 Mesh들을 합치는 것은 간단하지만, Skinned Mesh Renderer의 Mesh들을 합치기 위해선 Bone 구성이 동일한지, Bindposes의 x,y,z축 기준은 동일한지 등 고려해야 할 요소가 많았다.

 

(4) 2D 스프라이트 배칭

여러 개의 스프라이트를 포함하는 스프라이트 아틀라스를 생성하여 드로우콜을 줄이는 방법이다.

 

(5) GPU 인스턴싱

하나의 드로우콜로 오브젝트의 여러 복사본을 렌더링한다. (단, 스태틱 배칭이나 다이나믹 배칭과는 다르게 동일한 메시의 복사본을 만든다는 점에서 차이가 있다. 스태틱 배칭은 메시들을 합쳐 별도의 메시를 생성한다.

GPU 인스턴싱은 오브젝트들의 트랜스폼 정보를 별도의 버퍼에 담고, 이 버퍼와 원본 메시를 이용하여 GPU가 여러 오브젝트들을 한 번에 렌더링한다.

-> GPU에서 인스턴싱 처리를 하므로, CPU에서 메시를 별도로 재구성하지는 않는다. 따라서 CPU 오버헤드 or  메모리 이슈가 발생하지 않는다!!

-> 런타임에서 동적인 오브젝트들을 배칭할 수 있다. (단, Mesh Renderer만 가능. SMR은 X)

-> GPU에서 처리하기 때문에, 디바이스 성능에 의존적이다. 모바일 기기의 경우, OpenGL ES 3.0 이상이거나 Vulkan 또는 Metal을 사용하면 된다.

-> 동일한 메시들에 대해서만 수행된다.

 

 

03. 컬링

(1) 프러스텀 컬링 (Frustum Culling)

유니티에서 자동으로 수행해주는 컬링 기능이다. 뷰 프러스텀 내의 오브젝트들만 렌더링하고, 그 외의 오브젝트들은 렌더링하지 않는다. Far Clipping Plane의 거리를 잘 설정해주면 된다.

단, 이 기능은 횡뷰에서 좀 부자연스러워 보일 수 있다. 먼 거리의 오브젝트가 바로 지워져버리는 느낌. 이때, Fog 를 사용해서 보다 자연스럽게 연출할 수 있다. 먼 거리일 수록 희뿌연 효과를 넣는 것이다. (쉐이더의 Fog 설정이 이럴 때 쓰이는구나)

또는 탑뷰나 쿼터뷰의 게임을 제작해서 자연스럽게 만들 수 있다.

 

(2) 오클루전 컬링

벽, 사물 등에 의해 가려지는 오브젝트들을 컬링할 수 있다. 이때, 벽과 같이 가리는 오브젝트는 오클루더(Occluder)라고 하고, 가려지는 오브젝트는 오클루디(Occludee)라고 한다.

오클루전 컬링을 할 오브젝트들의 인스펙터창에서, Occluder Static 과 Occludee Static을 체크해주고, Window - Rendering - Occlustion Culling에서 Bake 버튼을 눌러 오클루전 데이터를 사전 연산해주면 적용된다.

사전 연산한 결과는 셀 데이터로 구성된다. (셀 데이터란 오클루전 컬링을 위해 씬을 일정 구역만큼씩 나눈 것이다)

셀의 크기를 조절하여 오클루전 컬링의 정밀도를 조절할 수 있다.

정밀도가 높을수록 차지하는 데이터가 증가하고, 연산을 위한 CPU 오버헤드가 증가할 수 있다.

따라서, 오클루전 컬링으로 드로우 콜을 줄이는 이득이 큰지, 혹시 오클루전 컬링 연산이 더 무거운 건 아닌지 잘 판단해야 한다.

오클루전 컬링은 오클루더와 오클루디가 많을 가능성이 높은, 실내 공간에서 적합하다.

 

(3) LOD (Level Of Detail)

오브젝트가 화면을 차지하는 비율에 따라 디테일을 조절 가능한 기능이다.

+) 오클루전 컬링과는 다른 장점

오클루전 컬링과 다르게 특별한 연산이 필요하진 않아 CPU 부담을 덜어준다. 렌더링을 덜 하는 기법이기 때문에 GPU의 부담도 줄어든다. RPG나 레이싱 게임 등 시야를 넓게 보아야하는 게임에서 효율적이다.

728x90