Egloos | Log-in


모바일 게임 최적화의 정석 - 렌더링 편

이전 글


일반적인 게임에는 시각적인 그래픽이 필요하다. 간단한 게임을 개발하고자 한다면 iOS의 Quartz와 같은 OS에서 제공되는 고급 API들을 사용하면 시각적 효과를 비교적 쉽게 개발할 수 있다. 그러나 주어진 모바일 기기에서 가능한 최상의 그래픽 효과와 성능을 뽑아내기 위해서는 골치 아프지만 OpenGL과 같은 저수준 렌더링 API의 사용이 필수이다. 주요 모바일 디바이스들은 모두 OpenGL ES 1.0/2.0(곧 3.0이 주류가 될 예정)을 지원하므로, 이 글에서는 OpenGL ES 환경과 주요 GPU들의 특성에 맞춘 렌더링 성능 최적화 기법들을 서술한다.

비록 모든 디바이스에서 OpenGL ES를 동일하게 사용한다 해도, 디바이스 종류별로 다소 다른 종류의 성능 이슈들을 갖고 있다는 점이 좀 더 머리를 아프게 한다. 하지만 모든 디바이스에서 공통적으로 다음의 사항들을 최적화 해야만 만족스러운 성능을 얻을 수 있다는 점에 주목해야 한다.
  • 적은 수의 draw call 횟수
  • 적은 수의 pixel 처리 (해상도 및 overdraw 횟수)
  • 가벼운 셰이더 코드
  • 빠른 blending 방식 사용
  • 작은 bandwidth(텍스처 용량) 사용
이 요소들이 렌더링 성능 미치는 영향을 간단한 수식으로 아래와 같이 표현할 수 있다.

렌더링 시간 = 그리는 픽셀 수(해상도 x 겹쳐서 그리기) x 셰이더 복잡도 x 블렌딩 성능 x bandwidth 병목 + draw call 횟수

렌더링은 하드웨어 내에서 파이프라인 구조로 이루어지므로, 병목이 어느 부분이냐에 따라 위의 요소들 중 일부는 성능에 거의 영향을 주지 않을수도 있다. 예를 들어, 셰이더 코드 수행에 의한 병목이 다른 무엇보다도 크다면 블렌딩 타입에 의한 영향은 매우 작을 수 있기 때문에 블렌딩 방식에 의한 성능차이는 발생하지 않게 된다. 그러나 두드러지는 병목을 하나 최적화 하고 나면 다른 부분이 성능의 병목이 되기 때문에 결국에는 위의 모든 사항을 고려하지 않을 수 없다.


위의 각 이슈들에 대해서 아래에서 자세히 설명할 것이나, 고품질 3D 장면에 대한 최적화 이슈는 UnrealEngine 개발사인 Epic의 Niklas Smedberg가 발표한 바 있는 Bringing AAA graphics to mobile platforms 자료나, Unity Technologies의 Fast mobile shaders ... 자료를 참고하는 것도 유용할 것이다.

 
Draw(API) Call 횟수 줄이기
Draw call의 횟수 문제는 렌더링 하드웨어 최적화에 대한 얘기가 나올 때마다 시대와 사양을 막론하고 늘 제일 먼저 거론되곤 한다. 매번 DirectX의 신버전이 나올 때 마다 M사에서는 "더 이상 Draw call 수에 구애받지 않아도 된다"라고 광고하지만, 몇 개월이 지나고 나면 M사의 "DirectX 최적화 기법" 자료에 draw call 최적화가 가장 중요한 요인으로 다시 등장하곤 한다.
PC의 경우에는 한 프레임당 1,000~3,000 정도의 draw call이 적정 수준이지만, 주요 모바일 디바이스의 경우 100~300 정도로 목표함이 좋을 것이다. 특히 초당 60프레임의 렌더링까지 얻기 위해서는 프레임당 100회 남짓의 draw call을 목표로 해야 한다. 아무래도 모바일 CPU의 성능이 상대적으로 낮기 때문에, 게임의 코드에서 OpenGL ES의 API들을 호출할 때 마다 일어나는 드라이버 내부의 연산량이 큰 병목이 될 수 있다. 구체적인 수치 예제를 제시하자면, 본인이 모바일 SNG를 개발 시작하면서 최적화 작업을 시작하기 이전에 프레임당 600정도의 draw call 상황에서 OpenGL ES의 연산량이 전체 CPU 연산의 절반 이상을 차지하였던 기억이 있다. 최적화를 통해 150회 미만의 draw call을 수행하고서야 60FPS를 달성할 수 있었다. (실 게임에서 늘 이정도를 유지하면 배터리 소모량의 문제가 있으므로 또 다른 측면의 최적화도 필요하다)
다행히도 Draw(API) call의 수를 줄이기 위한 기법들은 이미 많이 알려져 있다.
  • Batching (draw call 한번에 모아서 그리기)
  • Culling (불필요한 draw call 수행하지 않기)
Batching에도 여러가지 수준의 방법이 있는데, 우선 가장 간단하게 사용할 수 있는 방법은 유사한 렌더링 속성을 사용하는 오브젝트끼리 묶어서 그리는 것이다. 이렇게 되면 최소한 render state를 설정하는 glBindTexture, glUniformMatrix 등 API 호출들이라도 줄일 수 있으며, 상황이 허락한다면 vertex buffer와 index buffer도 재사용 가능할 것이다.
좀 더 나아가면 한번의 draw call에 가능한 많은 물체를 그릴 수 있도록 하나의 vertex buffer와 index buffer에 다수의 오브젝트를 합치는 방법이 필요하다. 각 draw call에 사용되는 vertex/index buffer 형태와 OpenGL draw 호출의 타입에 따라서도 성능이 크게 차이날 수 있으므로, 가능하면 Vertex Array Object와 glDrawElements와 같이 성능에 유리한 형식을 사용하도록 노력해야 한다.
OpenGL ES 2.0까지는 instancing 기능이 지원되지 않고, 셰이더가 많은 연산을 수행하기 어렵기 때문에 각 오브젝트별 transform이 모두 적용된 상태의 vertex들을 vertex buffer에 밀어 넣은 후 한번에 그리는 것이 유리할 때가 많다. 다행히 장면의 물체들이 정적이라면 게임의 로딩 단계에서 vertex buffer를 한 번 생성하면 될 것이나, 동적인 오브젝트의 경우에는 CPU에서 트랜스폼을 수행해야 할 수도 있다. 이 경우, CPU에서의 트랜스폼 및 vertex buffer 구성에 걸리는 시간과 draw call 횟수 사이의 절충(trade-off)이므로 그 균형점을 찾는 것이 중요하다.
본인이 개발했던 SNG의 경우를 예로 들면, 렌더링 되는 모든 것의 최소 단위는 사각형 스프라이트(quad) 이었기 때문에 화면에 나오는 1000+ 개의 스프라이트를 개별적인 draw call로 수행하게되면 게임을 플레이 할 수 없는 수준의 성능을 보여준다. 매 프레임 마다 동적인 스프라이트들을 모두 CPU에서 트랜스폼하고, 동일한 텍스처를 사용하는 스프라이트들을 단일 vertex buffer에 밀어넣음에 의해 만족스러운 성능을 얻을 수 있었다. 사실 이 경우에는 겹쳐서 보이는 스프라이트들에 대한 최적화와 CPU 트랜스폼에 대한 SIMD 명령(ARM의 경우 NEON, x86의 경우 SSE) 최적화와 함께 정적인 스프라이트에 대한 정적인 vertex/index buffer 생성도 중요한 요소였다.
여러 오브젝트를 한번의 draw call에서 처리하기 위해서는 그려지는 face들이 동일한 텍스처를 사용해야만 한다. 셰이더에서 다수의 텍스처를 동시에 참조하는 multi texturing에는 한계가 있으므로, texture atlas(하나의 텍스처에 여러 이미지를 붙여 넣음) 기법이 보편적으로 사용된다.
여러 장의 원본 텍스처를 합쳐서 하나의 atlas를 만드는 방법에는 다양한 접근법이 있는데, ImageMagick등의 외부 툴을 이용하는 스크립트를 작성하여 이 작업을 위한 툴을 직접 만들거나 TexturePacker등의 기존 툴을 사용할 수 있다. atlas를 생성하게되면 텍스처 해상도를 조절하거나 mipmapping 사용시 인접한 서로 다른 이미지간의 픽셀 간섭이 있을 수 있기 때문에 atlas내의 이미지 사이에 충분한 padding을 둘 필요가 있다.
하나의 atlas에 너무 많은 이미지 원본들을 조합하게 되어 atlas의 해상도가 너무 높아지는 경우에는, 렌더링시 텍스처 메모리의 bandwidth 때문에 성능이 저하될 수 있음에 유의해야 한다. 또한, 하드웨어에 따라 텍스처 최대 해상도에 제약이 있다는 점도 있지 말아야 한다. (본인이 테스트 해 본 모든 기기는 2048 이상의 해상도까지 지원함을 확인할 수 있었다)

culling을 통한 최적화에 대해 언급하자면 화면에 보이지 않을 오브젝트에 대한 API 호출을 원천적으로 차단하는 것이 목표인데, 구현 수준에서는 단위 메쉬와 scene graph를 얼마나 효율적으로 처리할 수 있느냐가 핵심이 될 것이다. 모바일 CPU의 연산능력을 고려하여 단순하면서도 CPU cache의 효율에 적합한 방식으로 구현함이 좋다. 자주 호출되는 함수가 virtual 호출이냐 inline 호출이냐에 따라 성능 차이가 눈에 보일 때도 있으므로, scene을 탐색할 때 사용되는 데이터의 사이즈를 최소화 하고 가급적 메모리 내에서 선형(linear)으로 순차 탐색이 가능한 방식이 좋다. 간단한 예를 들면, tree 구조의 탐색 순서와 메모리 내의 배치 순서가 동일하게 만드는 것을 생각할 수 있다.


렌더링 되는 Pixel 수 줄이기
GPU의 성능이 낮을 수록 각 렌더링 해야 하는 픽셀의 수와 성능 저하는 정비례에 가까워진다.
렌더링 픽셀의 수를 증가 시키는 요인으로는 높은 해상도, 반복(겹쳐서) 그리기 등이 주요 원인인데, 해상도의 문제는 게임 앱에서 직접 제어할 수 없는 문제이므로 겹쳐서 그려지는 것들의 최적화를 중점적으로 고민할 필요가 있다. 물론, shadow map 등의 생성을 위해 off-screen 렌더링을 사용한다면 적절한 임시 render target의 해상도 선택이 매우 중요하기는 하나, 모바일 환경에서 이런 경우는 흔치 않으므로 이것에 신경 쓸 경우가 거의 없을 것이다.

z-buffer에 의한 pixel culling의 고려: 일반적으로 z-buffer를 사용하는 환경에서는 성능을 위해 완전 불투명 객체에 대해서는 front-to-back, 반투명(블렌딩)이 있는 객체에 대해서는 back-to-front의 순서로 그리는 것이 정석이다. (PowerVR과 같은 구조에서는 이 순서가 바뀌어도 성능에 큰 영향이 없을 수도 있다)
그러나 모바일 게임에서는 메쉬가 아닌 스프라이트만으로 장면을 표현하는 경우가 많기 때문에 z-buffer 사용이 해결책이 되지 못하는 경우가 있다. 데스크탑 그래픽 카드에서는 alpha test를 사용하면 z-buffer의 이득을 볼 수 있지만, 많은 모바일 기기에서는 alpha test의 단점이 많기 때문에 스프라이트들은 z-buffer 없이 cpu 수준 정렬로 그려야 한다. 따라서 이런 경우에는 겹쳐지는 스프라이트 픽셀의 수를 최소화 하는 방향으로 아트 리소스 제작이 필요하다. 화면에 겹쳐서 보이는 오브젝트들과 그 정도를 쉽게 판별할 수 있는 'overdraw view' 와 같은 뷰 모드를 제공한다면 아티스트 들에게 큰 도움이 될 것이다. (오브젝트를 렌더링 할 떄 단색 텍스처를 이용하여 additiive blending또는 modulated blending을 사용하면 쉽게 구현 할 수 있을 것이다)


가벼운 셰이더의 사용
비록 OpenGL ES shader 언어에서 지원하는 기능이 충분히 강력하기는 하나, 모바일 GPU의 성능 한계 때문에 될 수 있으면 단순한 방법으로 셰이더를 사용해야 한다. 픽셀 셰이더(fragment shader)의 경우 texture fetch나 단순한 컬러 연산 이상의 것을 시도한다면 셰이더에 의한 성능 병목이 발생하기 쉽다.
구현한 셰이더의 기능은 유지하면서도 유사한 효과와 더욱 빠른 성능을 함께 얻을 수 있는 다양한 최적화 기법들이 존재한다.
  • 빠른 데이터 타입의 사용 - lowp등의 힌트를 사용하여 정확도가 낮은 타입의 변수를 사용할수록 성능에 유리하다. 매트릭스 연산시 가능한 작은 차원을 사용해야 하며, vector의 1~2개 component만이 필요하다 해도 GPU에 따라서는 4차원 vector 연산을 사용하는 것이 빠를 수도 있다.
  • 빠른 명령어의 사용 - 모든 명령어가 동일한 성능을 갖지는 않는다. 예를 들어, 대부분의 GPU에서 min/max/add/mul 등은 빠르지만 삼각함수나 지수연산 등은 현저하게 느리다. 동일한 효과를 얻을 수 있는 빠르고 적은 수의 명령어를 선택해야 한다. 또는, 아예 직접 연산 대신에 미리 계산된 결과를 텍스처로부터 읽어오는 방법을 사용할 수도 있다. 다행히도 다수의 텍스처를 사용하는 것이 셰이더 내에서 복잡한 연산을 수행하는 것보다 빠른 경우가 많다.
  • 종속성 있는 텍스처 읽기(dependent texture fetch) 회피 - 일부 GPU에서는 연산의 결과 혹은 다른 텍스처의 값에 의해 텍스처 좌표를 변형하여 읽는 것은 매우 느리다.
불가피하게 복잡한 셰이더를 사용해야 하는 경우라면 렌더링되는 오브젝트의 화면상의 크기를 줄이거나 겹쳐짐을 줄여서 그것이 그려지는 픽셀의 수를 최적화 해야 한다.


빠른 Blending의 사용
애플의 디바이스를 비롯한 PowerVR 계열의 GPU가 장착된 환경에서는 alpha test가 alpha blending 이상으로 느리기 때문에 완전 불투명 객체와 같은 방식으로 처리해서는 안되고 오히려 alpha blending을 사용해야 한다. 부득이한 경우 alpha test는 매우 주의해서 사용해야 하며, 그 사용 빈도를 최소화 하거나 위에서 소개된 Epic의 자료에서 언급 된 것 처럼 메쉬의 형태를 텍스처의 형태에 최대한 맞추는 등의 작업도 필요하다. 단순히 가장 빠른 방법은 blending을 될 수 있으면 사용하지 않는 것이므로, blending되어 보일 필요가 없는 메쉬에 대해서는 glDisable명령으로 blending을 완전히 꺼야 한다.
2D 스프라이트가 대부분인 게임이라면 blending을 반드시 사용해야 하므로, 위의 "pixel 수 줄이기"에서 설명한 바와 같이 스프라이트의 크기와 겹쳐그려지는 것을 최소화 하는 수 밖에 없다.


작은 bandwidth의 사용
GPU가 렌더링을 하기 위해서는 버텍스 데이터, 인덱스 데이터, 텍스처 데이터 등이 프로세서 내부로 전송되어야 하는데, 단위시간당 전송될 수 있는 데이터의 크기에는 한계가 있다. 동일한 시각적 효과를 렌더링 하기 위해 많은 데이터의 전송이 필요로 할수록 렌더링은 느려지게 된다.
가장 대표적인 것이 텍스처의 크기에 의한 병목이다. 큰 크기의 텍스처가 자주 렌더링 될수록 프로세서로의 데이터 전송량에 의해 느려짐이 눈에 띄게 된다. 따라서, 화면에 실제로 보이는 크기에 비해 불필요하게 높은 해상도의 텍스처를 쓰지 않도록 주의해야 한다. 텍스처가 적정 해상도인지 렌더러 내에서 확인할 수 있는 방편으로, 화면에 렌더링 되는 각 메쉬의 텍스처 밀도를 시각화하여 보여주는 방법이 있을 것이다. 이를 구현하기 위해서는 셰이더 코드가 메쉬의 화면(월드)상의 크기와 텍스처의 해상도를 입력으로 받아, 버텍스간 텍스처 좌표의 차이에 이들을 곱하여 색상으로 표현하면 된다. 아래과 같은 식으로 간단히 표현할 수 있다.
texture density = texture 해상도 / face size / 버텍스간 uv 차이

텍스처를 적용할 때 mipmapping을 사용하면 작게 보이는 물체를 렌더링 할 때 자동으로 작은 bandwidth만을 사용하게 되므로, 전송속도가 병목인 환경에서는 mipmapping 기법의 사용을 잊지 말아야 한다. (mipmap의 최대 메모리 사용량은 원본 텍스처 크기의 50%이다)
또한, 압축된 텍스처를 사용하면 훨씬 적은 텍스처 데이터를 GPU에 전송하면 되므로, 압축된 텍스처의 사용은 bandwidth 최적화에 필수라고 할 수 있다. (모바일 게임 최적화의 정석 - 텍스처 압축 편 참고)
텍스처 다음으로 전송량의 병목이 될 수 있는 것은 버텍스의 크기이다. 대부분의 GPU에서 vertex 데이터 전송 속도는 텍스처 전송속도보다 훨씬 느리기 때문에 각 버텍스의 크기를 최소화하고 프로세서가 연속적인 버텍스들을 빠르게 처리할 수 있는 구조로 만들어주는 것이 중요하다. Apple의 Best Practices for Working with Vertex Data 자료가 이러한 사항들을 iOS 디바이스 기준으로 매우 자세히 설명하고 있으며, iOS 뿐만이 아니라 대부분의 GPU에서 유효한 사항이다. 간단한 핵심만 정리하자면, 작은 크기의 데이터 타입을 사용하고, 적은 수의 버텍스 데이터가 순차적으로 처리될 수 있도록 배치하며, GPU가 빠르게 접근할 수 있는 메모리 타입의 버텍스 버퍼 객체를 생성하는 것이다. 특히, 렌더러 구현시 버텍스/인덱스 데이터의 업데이트와 렌더링 빈도에 따라 최적의 형식으로 관리할 수 있는 시스템의 설계가 매우 중요하다. 일반적으로 말하는 "최적화가 잘 된 렌더러"의 핵심에는, 최적의 버텍스/인덱스 버퍼 관리 -> 최적의 draw 호출로 이어지는 설계가 자리잡고 있다.


정리하며
모바일 기기를 위한 렌더링 성능 최적화 기법의 상당 부분은 PC 환경 및 모든 모바일 기기에 해당하는 공통적인 사항들이다.
다만 PowerVR, Qualcomm, NVidia 등 주요 모바일 GPU 공급자에 따라 다른 성능 이슈들이 몇가지 있는데, 그 모든 사항들에 대해 개별적인 최적화를 하기 위해서는 많은 노력이 필요할 것이다. 그러므로 일반적인 성능 문제들에 집중하여 최적화를 진행한다면 대부분의 모바일 기기에서 만족스러운 성능을 얻을 수 있을 것이다.

그런데 향후 다수의 모바일 GPU 공급자들이 서로 각자의 방식으로 하드웨어를 발전시킴에 따라 개발자들이 OS별, 모바일 기기 공급사별, GPU 타입별로 매우 다른 최적화 접근법을 택해야 할수도 있다는 걱정이 생기기도 한다. 게임 개발자의 입장에서는 과거 PC시장이 그랬듯이 모바일 GPU들 역시 유사한 형태의 아키텍처로 통합하여 발전하거나 점차 PC의 그것과 유사해지기를 기대한다. 다행히  OpenGL ES 3.0에서는 데스크톱 OpenGL과 더욱 유사한 기능들을 제공하게 되었으므로, 모바일 GPU들 역시 그 방향으로 발전하리라 예상한다.

by 김성균 | 2013/11/16 15:20 | 일(Work) | 트랙백 | 핑백(2) | 덧글(6)

트랙백 주소 : http://littles.egloos.com/tb/3440645
☞ 내 이글루에 이 글과 관련된 글 쓰기 (트랙백 보내기) [도움말]
Linked at 공돌이 옵빠 K : 모바일 게.. at 2014/01/04 00:33

... 이전 글 모바일 게임 최적화의 정석 - 텍스처 압축 편 모바일 게임 최적화의 정석 - 렌더링 편 모바일 게임의 성공적인 출시를 위해서는 매우 한정된 자원 내에서 최대한의 앱 성능과 멀티태스킹을 지원하기 위한 기기와 OS의 특성을 이해하고, ... more

Linked at 수까락의 프로그래밍 이야기 :.. at 2016/08/24 15:50

... 1. 렌더링 관련 그래픽 최적화로 가버렷! - Batch! Let's take a look at Batching, Unite Seoul 2016모바일 게임 최적화의 정석 - 렌더링 편 2. 메모리 관련 모바일 게임 최적화의 정석 - 메모리 편모바일 게임 최적화의 정석 - 텍스쳐 압축 편[unity3d] 메모리 최적화 3. E ... more

Commented by 항해자D at 2013/11/16 21:35
유용한 내용 잘 봤습니다
Commented by 알콜코더 at 2015/01/20 23:39
정말 유용하고, 잘 정리된 내용 잘봤습니다.
이렇게 잘 정리해주셔서 감사할 따릅입니다. 꾸벅.. (__)
Commented by 굿 at 2015/01/23 09:45
감사합니다.
Commented by Ableman at 2016/09/19 01:29
감사합니다. 노하우가 스며 있는 정말 꿀 같은 글이네요.
Commented by 엠군 at 2017/04/09 22:19
좋은 글 잘 봤습니다. 많은 도움이 되었습니다.
Commented by 버젼오 at 2017/06/24 04:41
좋은 자료 감사합니다.
모르는 용어 찾으려고 검색했다가 더 큰 지식을 얻네요!
인터넷 만세 작성자님 만세 !!

:         :

:

비공개 덧글

◀ 이전 페이지          다음 페이지 ▶