유니티 게임 엔진에서의 드로우콜이란? / Draw Call in Unity Game Engine?

개발자인 아는 지인이 면접을 보았는데, “Draw Call에 대해서 아느냐?” 라는 식의 질문을 받았었다. 모바일 게임 업계에서도  De Facto Standard로서 Unity가 자리 잡혀가고 있는 시점에서 이 Draw Call이라는 단어는 심심치 않게 개발자 사이에서 들어볼 수 있게 되었다. 사실 게임에서 퍼포먼스에 영향을 미치는 것이 여럿 있다. 때깔에 과한 욕심을 내면 Texture Size를 크게 써서 가용 메모리가 줄어드는 동시에 GPU-CPU간 Bandwidth 병목이 생기게 되고, 각진 캐릭터와 배경들이 싫어서 빠방한 Polygon을 쓰면 Frame Rate이 팍팍 떨어지는 것이 보이게 된다. 퐌타스틱한 화면을 만들어낼 욕심으로 Shader를 덕지덕지 붙이면 Fill Rate이 바닥을 기게되고, 이 모든 것의 위에 게임 알고리즘과 로직을 엉망으로 만들면 게임이 사망하는 경우를 보게 된다.

 

어디 이뿐이던가? Transparency나 CutOff를 과하게 사용하진 않았나, Culling을 어떻게 했는가, Mipmap을 적절하게 잘 썻나, glReadPixels(), glCopyTexImage(), glTexSubImage() 같은 퍼포먼스를 잡아먹는 대형 API들을 마구 쓰진 않았는가?, Texture Compression Type은 적절했으며, Draw 호출 시엔 VBO를 썼는가? LOD는 적절했으며 쉐이더에 너무 많은 Pass를 때려가며 OverDraw를 하진 않았나 등등 게임을 만들면서 Performance에 영향을 미치는 Factor들은 하나의 게임 속에 다양하게 존재한다. Draw Call이 Performance의 모든 것이라고 생각하면 오산이다. 결국 CPU, GPU, Memory, Bandwidth, Shader, GC Call 등 각각의 체크 포인트에서 Bottleneck을 찾아서 해결해 주는 것이 퍼포먼스 최적화의 첫 걸음이다. Draw Call은 그  퍼포먼스에 영향을 미치는 녀석들 중 렌더링 영역에서 크리티컬한 녀석이다.

 

Unity에서는 친절하게 Draw Call의 표시를 해준다. 실시간으로, 디폴트로…ㅎ(5.0부터는 Batches로 바꼈다.)

image01

 

Unity의 Draw Call을 논하기에 앞서 Unity 기저에 사용되고 있는 OpenGL이나 DirectX Graphics Library들의 Draw Call에 대해서 잠시 짚고 넘어가자.
OpenGL에서의 Draw Call은 다음 두 함수 혹은 그로부터 파생된 함수의 호출이다.

 

이 함수류의 호출이 Performance에 영향을 미치는 이유는 이 함수들이 호출 될 때마다 CPU를 왕창 긁어다 쓰는 Overhead가 발생하기 때문이다. 따라서 Performance를 올리기 위해서는 이 Draw Call을 줄여야 되는데, 이를 줄이기 위해서는 그야말로 적게 호출하는 방법들을 찾아야 한다.

가령, 아래와 같이 여러개의 triangles 들을 그릴 때, 그리는 함수 호출을 묶어서 한번에 그리게 한다.

image05

 

그림 10.1은 작은 Draw Data를 그리기 위해 Draw Call을 여러 번 부르는 것이고, 10.2는 그것들을 한데 모아서 한번의 Draw Call로 그리는 것을 표현한 그림이다. 후자가 더 적은 연산 Overhead로 그려낸다.

 

그러므로 Draw Call 호출 시의 State의 변경에 대한 Overhead, 또한 Command Buffer의 Flushing Overhead를 최소화 시켜 주는 것이 Performance와 직결 되고 이는 Draw Call을 줄이는 주요한 이유가 된다. CPU와 GPU의 관점에서 봤을 때, Geomery Data를 보내어 주는 동작은 충분히 빨라서 CPU가 GPU에게 그려질 Geometry를 빠르게 전달해 주지 않으면 GPU가 딴짓하며 놀게 된다. 이런 상황이 작은 Vertex Data를 여러개로 쪼개어 여러 Draw Call을 호출했을 때 발생하는 것이다. CPU는 State를 변경하고, Command Buffer를 새로 업데이트하고 뭘 그릴지 셋팅하는 오버헤드를 버벅이며 감당하고 있을 때 GPU는 탱강탱강 놀고 있는 상황이 발생하는 것이다. 이는 마치 파일 복사와 비슷한데, 1MByte의 파일 1000개를 복사하는 것보다, 1GByte의 파일 1개를 복사하는 것이 훨씬 빠른 경우와 비슷하다.

cb

그래서 OpenGL에서는 Immediate 모드로 작은 Triangle들을 산발적으로 그려내는 코드들을, Triangle Strip으로 묶어서 Vertex의 갯수를 줄이거나, Display List, Standard Vertex Arrays, VBO로 변경해서 한번에 그려주면 Performance가 확 올라간다.

그런데 한번에 묶어 그려주기 위해서는 다음의 세가지 조건이 필요하다.

1) 동일한 Triangle Format
2) 동일한 Shader
3) 동일한 GL State

이런 Draw Call을 줄이기 위해서 Triangle들을 묶는 노력들은 여러가지로 할 수 있으나, 대표적인 것이 Texture Atlas이다. Texture Atlas를 사용하면 각각 호출하던 Draw Call을 뭉쳐서 한번에 Triangle 호출로 변경할 수 있어서 결과적으로 Draw Call을 줄일 수 있다.

texture_atlas_with_multiple_signs

State 변경에도 많은 Overhead가 드는데, State가 다르면 동일 Draw Call을 쓸 수가 없다. OpenGL에서 State의 변경을 줄이는 방법은 아래와 같다.

1) Triangle들을 그룹핑하고 동일한 Texture를 쓰게 만든다.
2) 동일한 State를 사용하는 Object들에 대해서 한번에 그려준다.
3) 불필요한 glEnable(), glDisable() 함수를 제거한다. 가령 아래와 같은 코드

는 다음과 같이 수정하는 것이다.

참고로 Graphics API들은(OpenGL) 내부적으로 State가 정말 많다. Light State만 보아도 이렇게 많다.

Table 14-3 Lighting State Variables

State Variable Description

GL_AUTO_NORMAL Automatically generate lighting normals from glMap parameters.
GL_COLOR_MATERIAL Assign material colors from the current drawing color.
GL_LIGHTING Enable lighting calculations.
GL_LIGHTx Enable lighx.
GL_MAP1_NORMAL Enable mapping of lighting normals from 1D coordinates.
GL_MAP2_NORMAL Enable mapping of lighting normals from 2D coordinates.
GL_NORMALIZE Normalize all lighting normals prior to doing calculations.

Texture State는 말할 것도 없고,

Table 14-4 Texturing State Variables

State Variable Description

GL_MAP1_TEXTURE_COORD_1 The s texture coordinate will be generated by calls to glEvalPoint1, glEvalMesh1, and glEvalCoord1.
GL_MAP1_TEXTURE_COORD_2 The s and t texture coordinates will be generated by calls to glEvalPoint1, glEvalMesh1, and glEvalCoord1.
GL_MAP1_TEXTURE_COORD_3 The s, t, and r texture coordinates will be generated by calls to glEvalPoint1, glEvalMesh1, and glEvalCoord1.
GL_MAP1_TEXTURE_COORD_4 The s, t, r, and q texture coordinates will be generated by calls to glEvalPoint1, glEvalMesh1, and glEvalCoord1.
GL_MAP2_TEXTURE_COORD_1 The s texture coordinate will be generated by calls to glEvalPoint2, glEvalMesh2, and glEvalCoord2.
GL_MAP2_TEXTURE_COORD_2 The s and t texture coordinates will be generated by calls to glEvalPoint2, glEvalMesh2, and glEvalCoord2.
GL_MAP2_TEXTURE_COORD_3 The s, t, and r texture coordinates will be generated by calls to glEvalPoint2, glEvalMesh2, and glEvalCoord2.
GL_MAP2_TEXTURE_COORD_4 The s, t, r, and q texture coordinates will be generated by calls to glEvalPoint2, glEvalMesh2, and glEvalCoord2.
GL_TEXTURE_1D Enable 1D texturing unless 2D texturing is enabled.
GL_TEXTURE_2D Enable 2D texturing.
GL_TEXTURE_GEN_Q Automatically generate the q texture coordinate from calls to glVertex.
GL_TEXTURE_GER Automatically generate the r texture coordinate from calls to glVertex.
GL_TEXTURE_GEN_S Automatically generate the s texture coordinate from calls to glVertex.
GL_TEXTURE_GEN_T Automatically generate the t texture coordinate from calls to glVertex.

Drawing State 도 많다.

Table 14-2 Drawing state variables

State Variable Description

GL_ALPHA_TEST Do alpha value testing.
GL_BLEND Perform pixel blending operations.
GL_CLIP_PLANEx Clip drawing operations outside the specified clipping plane.
GL_CULL_FACE Cull back- or front-facing polygons.
GL_DITHER Dither color values.
GL_LINE_SMOOTH Anti-alias lines.
GL_LINE_STIPPLE Apply a bit pattern to lines.
GL_LOGIC_OP Do logical operations on pixels when drawing.
GL_POINT_SMOOTH Anti-alias points.
GL_POLYGON_SMOOTH Anti-alias polygons.
GL_POLYGON_STIPPLE Apply a bit pattern to polygons.
GL_SCISSOR_TEST Clip drawing outside the glScissor region.

이 외에도 다양한 State들이 존재하고, 이런 State의 변경들이 모두 Draw Call의 Overhead로 귀결된다는 말이다.

 

Unity5_PhysShading_Quixel_Viking1

그런데, OpenGL이나 DirectX를 직접 바닥에서부터 써서 엔진을 만들고 게임을 만들지 않는 이상, 이 Draw Call을 줄이는 방법은 현재 프로젝트에 어떠한 엔진을 사용하는가에 따라 그 대응법이 조금씩 달라진다. Unity 엔진에서는 Draw Call을 줄이기 위한 방법으로 다음과 같이 설명하고 있다. 유니티 엔진의 Draw Call Reference를 옮겨 보면 아래와 같다. 

Unity는 Runtime에 Single Draw Call로 가능한 Object들을 묶어준다. 이걸 “Batching”이라고 하는데 자연히 Batching이 많이 이뤄지면 Draw Call이 줄 것이고 CPU Performance가 올라가게 된다.

Unity가 내부적으로 이런 Runtime Batching을 해준다는 것은 모델링 툴에서 오브젝트들을 묶어 주거나, Standard Assets Package에서 CombineChildren script로 묶는 것보다 훨씬 덜 귀찮은 일이다. 또한 묶는 다고해서 Culling이 발생하지 않는 것이 아닌 Culling은 개별 Object에 대해서 따로 계산이 되니 훌륭한 기능임이 분명하다. 다만 수동으로 작업자가 모델링 툴에서 한땀 한땀 잘 묶어 주는 것이 엔진 차원에서 자동 Batching되는 것보다 더 훌륭할 수 밖에 없다.

 

Unity에서는 동일한 Material을 사용하는 Object들에 대해서만 하나의 Draw Call로 묶는 Batching이 발생한다. 그러므로 Batching이 엔진에서 잘 일어나도록 하기 위해서는 가능한 동일한 Material을 사용해야 한다. Texture만 다르고 다른 모든 값들은 동일하다면 Texture Atlasing을 사용해서 하나의 Texture로 묶은 후에 단일 Material로 만들면 Batching이 발생하여 Draw Call이 줄게 된다.

또한 Script에서 Material을 사용할 때 Render.material 호출은 Material을 Copy하여 생성하는 것이므로 Draw Call이 추가 발생하게 된다. 대신 Render.sharedMaterial을 사용하여 Batching이 발생하도록 공유하는 형태로 사용하는 것이 좋다.

 

Unity의 Batching은 Dynamic, Static 두 가지 형태로 나뉜다.

1) Dynamic Batching

움직이고 있는 Object에 대해서 Unity가 자동으로 Batching을 하기 위해서는 동일 Material을 사용하는 것 외에 추가적인 조건이 필요하다.

(1) 일단 Dynamic Batching은 Per Vertex Overhead가 발생한다. 따라서 Batchig되는 Object의 Vertex가 많으면 CPU 부하가 많이 발생한다는 말. 그래서 Unity에서는 모두 합쳐서 900개의 Vertex Attribute 이하의 Object에 대해서만 Dynamic Batching이 발생하도록 설정되어 있다.

예를 들어, 쉐이더가 Vertex Position, Normal, UV Coordinate까지 가지고 있다면 각각 300, 300, 300 즉 300개의 Vertex를 가진  Object가 Dynamic Batching의 한계가 되고, UV0, UV1에 Tangent Vector값까지 포함한다면 900/5 즉 180개의 Vertex를 가진 Object가 Dynamic Batching의 한계가 된다. (물론 제한 값은 바뀔 수 있다고 한다.) 

(2) Object가 일반적으로 동일한 Scale을 가져야 Dynamic Batching이 일어난다. 다만 non-uniform scale, 즉 크기 값이 모두 제각각인 경우에는 Dynamic Batching이 발생할 수 있다.

(3) 다른 Material Instance를 사용할 경우 (동일한 Material을 사용한다고 할지라도) Dynamic Batching이 발생하지 않는다.

(4) 추가적인 렌더러 파라미터(lightmap index, offset, scale)가 있는 라이트맵 Object의 경우 Dynamic Batching이 발생하지 않는다. 그래서 일반적으로 Dynamic Lightmap Object의 경우 완벽하게 동일한 Lightmap 위치를 가져야만 Batching이 발생한다. 

(5) Multi Pass 쉐이더를 적용한 Object는 Batching 되지 않는다. 거의 모든 유니티 Shader는 Forward Rendering에서 효율적인 라이팅 계산을 위해서 여러 개의  라이트를 지원한다. 추가적인 per pixel 라이트들의 Draw Call은 Batching이 되지 않는다.

(6) Real-time Shadow를 받는 Object는 Batching되지 않는다.

 

2) Static Batchcing

Unity에서 Static Batching은 어떤 크기의 Vertex Size를 가진 Object들도 모두 지원한다. (단, 이동이 없어야 하고, 동일 Material을 사용해야 함). Static Batching은 Dynamic Batching에 비해서 CPU 사용히 현저하게 적게 들므로 가능한한 Static Batching을 사용해 한다.

Static Batching을 사용하기 위해서는 Object가 Static임을 표시해줘야 하는데, Unity의 Inspector 창을 보면 Static 체크 박스가 있다.

image03

Static Batching을 사용하게 되면 하나로 묶여진  Geometry를 메모리에 저장해야 되기 때문에 추가적인 메모리를 차지한다. 따라서 빽빽한 숲의 나무를 만든다고 했을 때, 해당 나무를 Static으로 셋팅하게 되면 묶여진 나무 Geometry가 모두 메모리에 올라가게 되어서 심각한 메모리 부족을 초래할 수 있다. 이 경우 과감히 나무를 Static Batching을 꺼서 렌더링쪽을 희생 시키는 것이 더 나은 선택일 수 있다. 따라서 Batching에 대한 맹목적인 믿음은 버리는 것이 좋고, 때때로 Static Batching을 끄는 것이 결과적인 Performance에 더 나은 선택일 수 있다. 이 Static Batching은 Unity Pro에서만 지원한다.

 또한 캐릭터 에니메이션에 쓰이는 Skinned Meshes나 Clothes, 공의 Trail Renderer들은 Batching되지 않는다. 4.6 버젼 현재 Mesh Renderer, Particle System만 Batching된다. 투명한 Object는 Ordering 때문에 불투명한 Object보다 Batching이 잘 되지 않는다.
즉, 유니티는 기본적으로 Dynamic Batching을 지원한다. 그러므로 Static 체크가 되지 않더라도 작은 갯수의 버텍스들은 머터리얼과 쉐이더만 공유한다면 내부적으로 자연스럽게 하나로 묶어서 드로우콜을 줄여준다. 다만 Static Batching은 폴리곤의 갯수에 상관없이 유니티의 Static Batching 기능을 사용하려 할 때 쓰는 것인데 주의해서 사용하는 것이 필요하다.

게임을 만들 때 이 Draw Call을 잘 잡는 전략을 초기부터 차근차근 해나가야 한다. 배경과 캐릭터, UI 그리고 Effect에 각각 적절한 Draw Call 갯수를 할당하고 작업자들이 그 기준을 지키게 하는 것도 중요하다. 일반적으로 모바일에서는 100 Draw Call을 넘으면 무거운 게임이라고 봐야할 것이다. 마구 진행된 프로젝트의 게임 퍼포먼스 작업 진행 중에 최대 피해자는 게임 Graphic Quality이다. 프로젝트 막판에 시간이 없으니 가장 쉬운 그래픽 리소스부터 무자비하게 줄여나가는 재앙을 많이 맛보게 된다. 그러므로 차근차근 Performance를 고민하여 여러 Factor들을 잘 조율해 나가는 것이 필요한데, 이건 마치 오케스트라 지휘자가 곡을 완벽하게 파악하여 적절하게 디자이너, 개발자들을 잘 지휘하는 것과 비슷한 일이다. Draw Call은 게임 엔진을 만들 경우에도 기본적으로 필요한 지식이고, 유니티나 언리얼을 쓰더라도 각 엔진의 특성을 이해하고 엔진에서 지원하는 Draw Call 호출 최적화 방법을 잘 알아야 좋은 게임을 만들 수 있다.

Reference :

http://docs.unity3d.com/Manual/DrawCallBatching.html
http://www.nvidia.com/docs/IO/8228/BatchBatchBatch.pdf
http://people.eecs.ku.edu/~miller/Courses/OpenGL/Architecture.html
http://stackoverflow.com/questions/4853856/why-are-draw-calls-expensive
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0555c/CHDFCCII.html
http://opengl.czweb.org/ch14/462-465.html
http://simonschreibt.de/gat/renderhell/
https://traxnet.wordpress.com/2011/07/18/understanding-modern-gpus-2/