1-1 최적화의 이해
최적화의 시기
흔한 실수 중 하나가 최적화를 개발 막바지에 하는 것. 마지막에 와서 모델링의 폴리곤 수를 반토막 내야 한다던가 쉐이더를 스탠다드에서 레거시로 바꾸는 작업을 한다면 시간이 많이 걸릴 수 밖에 없다. 반대로, 프로젝트 시작 부분인 프로토타입 제작 과정에서는 빠른 기능 구현이 중요하기 때문에 어차피 추후에 버려지는 초반 코드에 최적화를 신경쓰지 않아도 된다. 결론은 항상, 모든 개발 기간에 최적화를 신경써야 한다.
1-3 그래픽스 API
엔진과 그래픽스 API
PC, 아이폰, 안드로이는 각각의 그래픽 칩셋을 이용한다. 많은 GPU 생산 회사와 그에 따른 펌웨어와 드라이버가 있다. OS 별로 모든 GPU의 프로토콜에 맞춰서 소프트웨어를 만드는 것은 불가능하기 때문에 DirectX, OpenGL 등의 그래픽스 API를 사용한다. (IOS의 Metal, Android의 Vulkan)
OpenGL은 컴퓨터 그래픽스 표준 규격을 기반으로 발표한 2D, 3D 그래픽스 표준 API 규격. 크로노스 그룹에서 꾸준히 연구 개발 중.
OpenGL ES(Embedded System)은 모바일에서 사용하는 더 작고 가벼운 표준. (없어지는 추세인 듯?)
Metal은 IOS만 지원함으로써 기존의 오버헤드를 줄였다.
모바일에서 높은 수준의 그래픽을 표현함에 있어서 OpenGL ES의 오버헤드 이슈가 발생. OpenGL ES가 모든 GPU에서 구현되도록 하려면 GPU 명령들을 공통적인 API 인터페이스로 구현해서 처리해야 하는데 이 과정에서 불필요한 오버헤드가 존재하게 된다. CPU가 GPU에 무언가를 그리라는 명령인 드로우콜, 명령을 전송하기 전에 무엇을 어떻게 그려야 하는지의 상태변경을 발생시킨다. 이것들이 오버헤드를 발생시킴.
Metal은 (OpenGL ES에서 렌더링 파이프라인이 멀티쓰레드에 안전하게 설계되어 있지 않은 반면) 멀티쓰레딩에 대응할 수 있는 구조로 설계되어있다.
Metal은 메모리 관리에서도 효율적이다. 기존의 OpenGL은 CPU 시스템 메모리, GPU 그래픽 메모리를 별도로 관리하는 방식인데, 모바일에서는 물리적으로 하나의 메모리를 갖기 때문에 CPU 메모리 데이터를 GPU 메모리로 전송하는 과정을 거쳐야했다. Metal은 이런 구분 없이 메모리를 사용할 수 있기 때문에 메모리를 매우 효율적으로 관리할 수 있다.
Vulkan은 Metal과 비슷한 컨셉. 렌더링 파이프라인을 멀티쓰레드화 시킬 수 있도록 설계되어 있어서 CPU 오버헤드를 줄일 수 있다.
2-1 GPU의 의미
GPU 메모리에는 텍스쳐 및 메시 데이터 등 렌더링에 필요한 데이터들이 포함되어 있다. 렌더링 시점이 되면 GPU가 이 데이터들을 참고하여 그래픽 처리를 수행한다. 렌더링 결과를 저장하는 버퍼들 또한 이 메모리에 존재함.
그래픽스 API들은 렌더링 파이프라인을 기반으로 하고 있다. 렌더링 파이프라인은 3D 이미지를 2D 래스터 이미지로 표현하기 위한 단계적인 방법.
2-4 렌더링 파이프라인
애플리케이션 스테이지(Application Stage)
애플리케이션 스테이지에서는 현재 프레임에서 렌더링 가능한 오브젝트들이 컬링 연산에 의해 선별된다. 배칭 처리를 위한 연산도 GPU 파이프라인에 진입하기 전에 이루어진다.
지오메트리 스테이지(GeometryStage)
버텍스와 폴리곤을 화면상 적절한 위치에 배치하는 과정.
- 버텍스 트랜스폼
- 버텍스 데이터는 GPU 메모리에 저장되어 있다. 렌더링을 수행하는 시점에 GPU는 메모리로부터 버텍스 정보를 가져온다. 이 버텍스 정보를 적절한 위치에 그려주기 위해서 Transformation을 수행한다. 이런 메시가 아직 3D 공간에 배치되지 않았다면 Local Space만 가지고 있는 것이다. 3D 월드 공간에 배치될 때 Local Space에서 World Space로 변환되는 World Transform을 거친다. 카메라를 통해서 디스플레이 되기 때문에 오브젝트 버텍스는 카메라에 상대적인 위치로 변환해야 한다. Camera Space로 변환되는 View Transform을 거친다. 원근 perspective 투영, 직교 orthographic 투영을 결정하는 Projection Transform도 있다.
- 버텍스 쉐이더
- 위의 트랜스폼 변환은 버텍스 쉐이더에서 이루어진다. 어감은 라이팅만 처리할 것 같지만 트랜스폼 처리도 버텍스 쉐이더에서 이루어진다. 이런 월드-뷰-프로젝션 트랜스폼은 메시 버텍스에 행렬을 곱하며 수행된다.
- 버텍스 위치뿐 아니라 노멀 및 컬러도 버텍스 쉐이더에서 결정된다.
- 지오메트리 생성
- 버텍스 쉐이더를 거쳐 버텍스가 연결되어 선이 만들어지고 도형 형태가 되는데 이것을 지오메트리(Geometry)라고 부른다. 지오메트리 생성은 버텍스 쉐이더에서 버텍스 트랜스폼이 결정되고 나면 자동으로 이루어진다.
- 일부 그래픽스 API에서는 쉐이더로 지오메트리 생성 단계에 관여할 수 있는데, Geometry Shader, Hull Shader, Domain Shader 등을 통해서 테셀레이션(Tessellation)을 수행할 수 있다. 테셀레이션은 입력받은 버텍스 외에 추가로 버텍스를 생성하여 도형의 밀도 및 복잡도를 늘려서 디테일을 더해 주는 기법. Metal, Vulkan도 테셀레이션 기능을 지원한다.
- 카메라 화면 밖에 렌더링 되지 않는 버텍스도 모두 버텍스 쉐이더를 거쳐서 지오메트라화 된다. 버텍스가 많으면 지오메트리 스테이지에서 병목이 발생할 확률이 높아지게 된다. 따라서 카메라 밖의 오브젝트는 최대한 렌더링 파이프라인을 거치지 않도록 컬링(Culling)을 사용하는 것이 중요하다.
래스터라이저 스테이지(Rasterizer Stage)
오브젝트를 그리는 픽셀을 추리고 색을 결정한다. 메시의 폴리곤에 속한 영역을 픽셀로 매칭시키는 과정을 Rasterization이라고 한다.
픽셀의 최종 색상 정보는 컬러 버퍼에 저장되고 RGBA 4가지 채널 값이다. 카메라로부터의 거리인 Z 버퍼 또는 뎁스 버퍼를 이용해서 픽셀이 렌더링 될 때마다 깊이 판정을 수행하게 된다. Z 버퍼는 컬러 버퍼와 동일한 해상도를 가진다.
현대의 많은 그래픽 칩셋은 Fragment/Pixel 쉐이더로 넘어가기 전에 Z 테스트를 거쳐서 화면에 보이지 않게 될 fragment를 사전에 걸러 비용을 절약한다.
투명도가 있다면, 픽셀 출력의 알파값을 이용해서 해당 픽셀 위치의 컬러 버퍼의 기존 값과 적절하게 혼합하여 최종 출력 색상을 결정한다.
정리하며
포스트 프로세싱 역시 다른 렌더링과 마찬가지로 버텍스 쉐이더와 프래그먼트 쉐이더를 모두 거친다. 대부분의 처리는 프래그먼트 쉐이더에서 이루어진다.
버퍼가 두 벌 존재하여 하나는 화면에 보이는 프론트 버퍼, 다른 하나는 렌더링 되는 백 버퍼가 있어서 순차적으로 화면에 보여지게 된다. 이를 더블 버퍼링(Double Buffering)이라고 한다.
전체적인 렌더링 프로세스는 다음과 같다.
- 프레임이 시작되고 물리, 사용자 입력, 로직, 애니메이션 등 렌더링하기 전에 이루어져야 하는 연사들이 수행된다.
- 불필요한 렌더링 부하를 방지하기 위해 컬링 연산이 이루어진다. (프러스텀 컬링, 오클루전 컬링)
- 그려져야 하는 오브젝트들마다 드로우콜이 발생하며 버텍스 쉐이더와 프래그먼트 쉐이더 등의 GPU 파이프라인을 거쳐 버퍼에 순차적으로 렌더링 된다.
- 블룸이나 컬러 그레이딩 등 포스트 프로세싱을 처리한다.
- 프론트 버퍼는 다음 프레임 렌더링을 위한 백 버퍼가 되는 더블 버퍼링 과정을 통해 연속되는 프레임을 디스플레이한다.
3-1 병목의 이해
최적화란 현재를 최적의 상황으로 만드는 것을 의미한다. 성능 최적화를 한다는 것은 최적의 성능을 이끌어내는 의미이고, 이는 소프트웨어가 가급적 적은 자원을 사용하더라도 연산 효율이 높아지는 것을 의미한다. CPU와 GPU의 연산 비중을 낮추고, 메모리 사용도 낮추고, 전력 소모량을 줄여도 소프트웨어가 원할하게 구동될 수 있는 시스템 환경을 구축하는 과정.
게임 성능 최적화를 위해서 해야할 것들
- 메시 버텍스 줄이기
- 텍스쳐 크기 줄이기
- 가벼운 쉐이더 사용
- 드로우콜 줄이기
- 게임 로직 최적화
- 물리 연산 줄이기
사실은 최적화를 하기 전에 병목을 먼저 탐지해야 한다.
병목(Bottleneck)은 전체 프로세스가 갑자기 느려지거나 막혀서 정지하는 원인이나 장소를 부르는 말. 전체 시스템의 성능이나 용량이 하나의 구성 요소로 인해 제한받는 현상이 발생하면 병목현상이 발생했다고 한다.
어떤 것이 문제인지, 어떤 부분에서 병목인지를 정확하게 판단하는 것이 중요하다. 최대한 병목 지점 없이 원활하게 수행되도록 하는 것이 최적화 작업의 기본이 된다. 이렇게 병목 지점을 찾는 과정을 프로파일링(profiling)이라고 한다.
타깃 선정
게임의 최적화 방향을 결정할 때 가장 중요한 것은 FPS 값이 아닌 타긱 기기를 결정하는 것.
기기마다 병목 원인이 다를 수 있음.
FPS vs Frame Time
30FPS를 예로 들면 Frame Time은 1/30초(ms)
유니티 에디터 상에서 FPS를 확인하는 것은 의미가 없다. 실제 디바이스에서 값을 확인하려면 프로파일러를 연결하거나 FPS를 출력하는 스크립트를 작성해야 한다.
GitHub - ozlael/FPSCheckerSample
Contribute to ozlael/FPSCheckerSample development by creating an account on GitHub.
github.com
선형적인 측정
프로파일링은 병목을 찾기 위한 성능을 측정하는 것이기 때문에 단순 FPS 측정 값은 기준 수치로 삼기 부적합하다.
900FPS = 1000/900 = 1.1ms -> 450FPS = 1000/450 = 2.2ms
60FPS = 1000/60 = 16.6ms -> 56.5FPS = 1000/56.5 = 17.7ms
위의 예시와 같이 FPS가 아닌 Frame Time으로 보았을 때 변화량을 제대로 측정할 수 있다. (동일하게 1.1ms 증가)
측정 시나리오
Rendering Path가 Deferred인 경우는 기본적으로 대역폭에 요구되는 성능 비용이 크기 때문에 씬이 복잡하든 간단하든 FPS가 높게 나오지 않을 수 있다.
DOF나 Bloom이 활성화 되어있다면 씬이 복잡하든 간단하든 FPS가 높게 나오지 않을 수 있다.
VSync(수직동기화)
백 버퍼에 렌더링되고 있는 동안 프론트 버퍼와 전환이 이루어지면 화면이 찢어지는 것처럼 보이고, 이런 현상을 티어링(Tearing)이라고 한다.
티어링을 방지하기 위한 것이 VSync 이다. VSync를 활성화하면 디스플레이 모니터의 주파수에 맞게 렌더링 퍼포먼스도 조절되어 강제로 targetFrameRate가 설정된 것과 비슷한 효과가 발생된다.
성능 및 병목을 측정할 때는 VSync를 끄는 것이 좋다.
CPU 바운드 VS GPU 바운드
병목이 GPU에 몰려있으면 GPU 바운드, CPU에 몰려있으면 CPU 바운드라고 표현한다.
CPU와 GPU는 병렬처리 방식. CPU는 렌더링 해야하는 상황에서 GPU에게 명령을 던지고 할 일을 계속 한다. 이 때, CPU는 일처리를 끝냈지만 GPU가 일처리를 끝내지 못했다면 끝낼 때까지 기다린다. GPU의 일까지 끝나면 한 프레임이 끝나고 화면에 나온다. 이 상태에서 CPU의 연산을 줄여도 GPU의 연산이 줄지 않기 때문에 GPU 바운드인 상태이며 GPU의 연산을 줄여야 한다.
반대의 경우도 마찬가지. GPU의 일이 적고 CPU의 일이 많다면 CPU 바운드인 상태이다.
3-2 병목 측정
유니티 내장 프로파일러를 이용한 측정
특정 프레임 내에서 애니메이션 연산 또는 스크립트가 얼마나 차지하는지 정보 확인이 가능하다.
프로파일러를 위한 Development Build는 릴리즈 빌드와 달라서 오버헤드 발생 가능성이 있다.
에디터에서 돌리는 것보다 실제 타깃 디바이스에서 확인하는 것이 중요
타깃 디바이스가 안드로이드 핸드폰이라고 하면, 빌드 세팅을 안드로이드로 맞춘 후 안드로이드 폰과 PC를 USB로 연결하고 디바이스의 Wifi를 PC의 Wifi와 같게 설정한다. 이후 빌드 세팅에서 [Development Build]와 [Autoconnect Profiler]를 체크. 이후 [Build And Run] 버튼을 눌러 빌드를 진행한다. 프로파일러 창에서 AndroidPlayer를 선택한다.
"PlayerLoop - EarlyUpdate.PresentBeforeUpdate - Graphics.PresentAndSync - Device.Present" 부분이 CPU가 GPU의 일처리를 기다리는 부분.
3-3 GPU 병목 탐지
CPU 병목은 내장 프로파일러로 확인이 가능하지만 GPU 병목은 바운더리가 어느 정도인지만 유추할 수 있다.
GPU 프로파일러를 통해서 어떤 부분이 얼마나 GPU를 소비하는지 확인하고 분석할 수 있다.
→ 모바일에서는 사용 불가능 (안드로이드 Vulkan은 가능)
→ 스냅드래곤 칩셋은 Snapdragon Profiler로 GPU 프로파일링 가능
Snapdragon Profiler | Qualcomm Developer
Snapdragon Profiler is designed to help developers detect bottlenecks in their apps to optimize for performance and power.
www.qualcomm.com
→ 아이폰/아이패드 IOS는 XCode에서 제공하는 GPU Frame Debugger를 이용하여 GPU 프로파일링 가능
Xcode - Apple Developer
Xcode includes everything you need to develop, test, and distribute apps across all Apple platforms.
developer.apple.com
필레이트(Fillrate)
GPU 병목은 높은 확률로 필레이트가 원인인 경우가 많다. 필레이트는 그래픽카드가 1초에 스크린에 렌더링 할 수 있는 픽셀 수를 의미한다.
필레이트 = 화면의 픽셀 수 X 프래그먼트 쉐이더 복잡도 X 오버드로우
병목이 필레이트의 원인임을 알 수 있는 방법은 화면 해상도를 낮추었을 때 게임 성능이 대폭 향상되는 경우.
렌더링 해상도는 Screen.SetResolution() 메소드로 조절할 수 있다.
https://docs.unity3d.com/ScriptReference/Screen.SetResolution.html
오버드로우
하나의 픽셀이 여러 번 덧그려지는 현상.
불투명(Opaque) 오브젝트는 앞에서 뒤로 정렬(Front to Back Sorting) 한다.
투명(Transparent) 오브젝트는 반대로 뒤에서 앞으로 정렬(Back to Front Sorting) 한다. 투명 오브젝트는 뒤가 비춰져야 하기 때문.
투명 오브젝트는 오버드로우 뿐만 아니라 프레임 버퍼를 읽어오는 과정에서 병목이 발생한다. 블렌딩 연산을 위해서 현재 연산하는 새로운 픽셀의 컬러와 기존의 프레임 버퍼에 있는 픽셀의 컬러를 혼합한다. 최소한으로 사용하는 것이 유리하다.
최종 픽셀 컬러 = 현재 픽셀 컬러 X 알파 + 프레임 버퍼의 픽셀 컬러 X (1 - 알파)
파티클은 오버드로우가 과도하게 발생하기 쉽다. 파티클 밀도가 높을 수록 오버드로우가 발생된다. Scene의 좌측 상단에서 Draw 모드를 Overdraw로 두면 오버드로우 확인이 가능하다.
포스트 프로세싱(Post Processing)
프래그먼트 쉐이더가 무거워지는 흔한 원인은 포스트 프로세싱이다. 포스트 프로세싱이 주된 병목 원인인 경우 해상도를 줄이는 것이 쉬운 해결책.
Depth of Field(DOF)는 프래그먼트 쉐이더의 부담 뿐만 아니라 드로우 콜이 늘어나는 요인이 되기도 한다.
게임 그래픽의 전체적인 Look & Feel을 고품질로 유지하고 싶으면 해상도를 줄이는 것이 가장 간단한 해결 방법.
업스케일링 샘플링(Upscale Sampling)
UI는 원래 해상도로 렌더링하고 3D 씬만 낮은 해상도로 렌더링하는 트릭을 업스케일링 샘플링이라고 한다.
구현 스텝은 아래와 같다. 키포인트는 전체 해상도를 줄이지 않는 것.
저해상도 렌더 텍스쳐 생성
→ 3D 씬을 렌더 텍스쳐에 렌더링
→ 렌더 텍스쳐를 업스케일링 해서 현재의 백 버퍼에 렌더링
→ 오버레이 UI를 렌더링
폴리곤(Polygon)
렌더링 되는 폴리곤이 많으면 렌더링 파이프라인의 지오메트리 스테이지에서 병목이 발생한다. 버텍스가 많다는 건 GPU에서 버텍스 쉐이더를 많이 수행해야 한다는 의미.
버텍스 수를 줄여서 성능이 두드러지면 버텍스가 병목일 확률이 높고, 그렇지 않다면 다른 부분이 병목일 것이다.
LOD(Level of Detail)은 처리해야하는 버텍스 수를 줄임으로써 GPU의 부담을 줄여서 성능을 높여주는 것이다.
텍스쳐(Texture)
모바일 디바이스는 대역폭이 작게 설계되어 있기 때문에 일정 크기 이상의 텍스쳐를 사용하게 되면 성능 하락의 원인이 될 수 있다. 이 문제는 텍스쳐의 해상도를 조절해서 확인할 수 있다.
Project Settings > Quality > Rendering > Texture Quality 에서 전체 텍스쳐 해상도를 조절할 수 있다.
4-1 드로우콜(Draw Call)
보통 드로우콜이 병목의 원인인 경우가 흔하지만 단정지을 수 없고, 프로파일링을 통해서 정확히 진단하고 문제를 해결해야 한다.
드로우콜의 이해
CPU는 현재 프레임에서 어떤 것을 그려야 할지를 결정하고, 렌더링 하는 것은 GPU에 위임한다. 이 과정에서 CPU가 GPU에 오브젝트를 그리라는 명령을 호출하는 것이 드로우콜이다.
드로우콜 호출 과정에는 많은 정보와 데이터가 필요하다.
- 오브젝트 형태 메쉬 정보
- Albedo, Metallic, Opacity 등 텍스쳐 정보
- 라이팅 처리에 대한 쉐이더 정보
- 위치, 스케일 등 트랜스폼 정보
- 알파 블렌딩 여부
- 기타 등등
모바일 디바이스에서는 CPU 메모리와 GPU 메모리를 물리적으로 나누지 않고 하나의 메모리를 논리적으로 나누어서 사용한다.
GPU가 메쉬를 렌더링할 때 지오메트리 데이터를 읽어오는 공간이 GPU 메모리이다. GPU가 메쉬를 렌더링하려면 GPU 메모리에 메쉬 정보가 존재해야 한다는 것.
CPU가 HDD, SDD 등의 스토리지로부터 파일을 읽어 들이고 데이터를 파싱하여 CPU 메모리에 올린다. 그 후, CPU 메모리의 데이터를 GPU 메모리로 데이터를 복사하는 과정을 거친다. 이 과정을 매 프레임마다 수행하면 성능 하락을 일으키므로 로딩 시점에 메모리에 데이터를 올려두고 씬 전환 시점 등 적절한 시점에 데이터를 해제한다. 텍스쳐와 쉐이더 모두 마찬가지로 GPU 메모리에 존재해야 GPU가 가져다 사용할 수 있다.
CPU가 렌더 상태를 변경하는 명령들을 보내면, GPU는 이러한 렌더 상태에 오브젝트를 그리기 위한 정보들을 저장한다.
이러한 렌더 상태 명령들을 보낸 후, CPU는 마지막으로 GPU에 메쉬를 그리라는 명령을 보낸다. 이 명령을 Draw Primitive Call(DP Call)이라고 부른다.
하나의 메쉬를 렌더링 한 후 다른 오브젝트를 렌더링하기 위해서 상태 정보들을 변경(render state chnages)하는 명령을 내보내야 한다. 메쉬, 쉐이더, 텍스쳐 각각 변경되는 명령을 내리는데 알파 블렌딩 여부, Z 테스트는 바뀔 필요가 없으므로 명령을 보내지 않는다.
넓은 의미의 드로우콜은 렌더 상태 변경 명령부터 DP Call까지 포함한 모두를 말한다.
CPU는 GPU에 명령을 바로 보내는 것이 아니라 커맨드 버퍼(Command Buffer)에 명령을 쌓아두고 GPU는 작업이 끝나면 이 버퍼에서 할 일을 가져가는 식으로 작업한다. 서로의 간섭 없이 병렬 작업을 수행하게 된다.
*Vulkan과 Metal에는 여러개의 커맨드 버퍼를 이용하는 멀티쓰레드 병렬 처리가 가능하다.
유니티는 멀티플랫폼 엔진이기 때문에 다양한 OS의 그래픽 칩셋에서 렌더링이 진행된다. 그래픽스 API들은 CPU에서 GPU로 보내는 명령을 공통적인 API로 구성한다. API가 호출되면 드라이버 칩셋에 알맞은 신호를 전달하여 GPU에 맞게 명령을 해석하고 변형하는 과정을 거친다. 이 과정을 거치기 때문에 CPU가 GPU에 명령을 보낼 때 오버헤드가 발생한다. 그래서 드로우콜은 CPU 바운더리의 오버헤드가 된다.
이러한 이유들로 명령을 GPU로 보내는 오버헤드는 게임의 병목의 CPU 바운드가 되는 주요 원인이 된다. 렌더링에 필요한 작업을 별도의 쓰레드로 분리해서 렌더링 성능을 높이는 것을 멀티쓰레디드 렌더링(Multithreaded Rendering)이라고 한다. Project Settings > Player > Other Settings > Rendering > Multithreaded Rendering에서 활성화 할 수 있지만 모든 디바이스에서 작동하지는 않으며 드로우콜 병목이 아니라면 활성화 해도 성능에 영향을 주지 않는다.
드로우콜은 GPU 보다는 CPU의 성능에 의존적이다.
드로우콜의 발생 조건
기본적으로는 오브젝트 하나를 그릴 때 메쉬가 1개, 메테리얼이 1개라면 드로우콜이 한 번 일어난다. Batch가 1이 된다.
에셋의 메쉬가 17개로 이루어져있다면 하나의 메테리얼을 공유하더라도 드로우콜이 17번 발생한다.
에셋의 메쉬가 1개이고 2개의 메테리얼로 이루어졌다면 드로우콜이 2번 발생한다. 하나의 메쉬라도 메테리얼이 2개 쓰였다면 서브메쉬(submesh)가 존재한다.
에셋의 메쉬가 1개이고 1개의 메테리얼로 이루어졌더라도 쉐이더에 의해서 드로우콜이 2번 발생할 수 있다. 쉐이더 코드에서 Pass와 UsePass에 의해서 외곽선을 그려주는 쉐이더를 사용하였다면 가능한 상황이다.
드로우콜의 비용을 개당으로 단언하기 어려운 이유는, 그리기 전에 상태 변경이 많이 이루어져야 할 수도 있고, 어떤 경우는 그리기 전에 상태 변경이 적게 이루어져도 상관 없을 수 있기 때문.
Batch & SetPass
게임 뷰 화면의 Stats 뿐 아니라 유니티 프로파일러에서도 SetPass Call, Draw Calls 값을 확인할 수 있다.
Batch는 DP Call과 상태 변경들을 합친 넓은 의미의 드로우콜이다. 드로우콜이 일어날 때 상태 변경의 발생 여부. 메쉬의 변경은 포함하지 않는다.
Batch가 10번 발생했는데 SetPass는 1번만 일어났다면, '10번의 드로우콜 동안 쉐이더의 변경이 없었다' / '메쉬 및 트랜스폼 정보 등 최소한의 상태 변경만 이루어졌다' 라고 판단할 수 있다.
Batch가 10번 발생했고 SetPass도 10번 발생했다면 매번 쉐이더 변경이 이루어졌거나 많은 상태 변경이 동반되었을 수 있다.
SetPass는 쉐이더로 인한 렌더링 패스 횟수를 의미한다. 쉐이더의 변경 혹은 쉐이더 파라미터들의 변경이 일어나는 경우.
메테리얼이 바뀌면서 쉐이더 및 파라미터가 바뀌면 SetPass 카운터가 증가한다. 이 과정에서 상태 변경이 일어나기 때문에 CPU 성능이 소모된다.
게임이 CPU 바운드이고, GPU에 명령을 보내는 드로우콜이 병목이라면 SetPass를 줄이는 것이 가장 효율이 좋다.
각가 다른 메쉬에 하나의 메테리얼만 쓴다면 SetPass는 한 번만 일어난다.
4-2 배칭(Batching)
배칭(Batching)의 이해
드로우콜을 줄이기 위한 가장 효율적인 기능 중 하나가 배칭(Batching) 이다.
기준은 메테리얼이다. 다른 메쉬를 사용하더라도 같은 메테리얼을 사용한다면 하나의 배치로 구성하는 것이 가능하다.
메테리얼이 동일하다는 뜻은 동일한 메테리얼 인스턴스를 의미한다. 다른 메테리얼 에셋이라면 배칭이 되지 않는다.
Project Settings > Player 에서 Static Batching과 Dynamic Batching을 체크할 수 있다.
스태틱 배칭(Static Batching)
정적이고 움직이지 않는 오브젝트를 위한 배칭 기법.
오브젝트의 Inspector 창에서 Batching Static을 체크하면 된다. 이동, 회전, 스케일 조절이 되지 않는 정적인 오브젝트가 된다.
다이나믹 배칭은 매번 배칭 구성을 위한 버텍스 연산이 필요하지만, 스태틱 배칭은 버텍스 연산은 런타임에서 수행하지 않기 때문에 보다 효율적이다.
단점으로는 메모리가 추가로 필요하다는 것. 예를 들어 하나의 동일한 메쉬를 3개 배치한다고 했을 때, 배칭을 사용하지 않으면 메모리에는 1개의 메쉬만 상주하고 3개를 렌더링 하면된다. 하지만 배칭을 사용하면 3개를 합친 큰 메쉬를 새로 메모리에 저장해야 하기 때문에 추가적인 메모리가 필요하다.
추가적인 메모리를 희생하더라도 드로우콜을 줄일 수 있기 때문에 런타임 성능을 높일 수 있다. 하지만 메모리가 문제가 된다면 배칭을 줄여야 할 수도 있다.
StaticBatchingUtility.Combine() 메소드를 이용하면 런타임 중에 추가된 오브젝트도 스태틱 배칭이 가능하지만 시간이 걸릴 수 있으므로 동적으로 구성하는 것은 추천하지 않는다.
다이나믹 배칭(Dynamic Batching)
동적으로 움직이는 오브젝트들끼리 배칭처리를 하는 기능. Static 체크되어 있지 않은 오브젝트를 대상으로한다. Player Settings에서 플래그만 체크하면 엔진이 알아서 처리하기 때문에 추가적인 작업은 필요없다.
매 프레임, 씬에서 Static 플래그 체크가 되어있지 않은 오브젝트들의 버텍스들을 모아서 합치는 과정을 거친다. 이 버텍스들을 모아서 다이나믹 배칭에 쓰이는 버텍스 버퍼와 인덱스 버퍼에 담는다. GPU는 이를 가져가서 렌더링한다.
이 방식으로 매번 데이터 구축과 갱신이 발생하기 때문에 다이나믹 배칭은 매 프레임마다 오버헤드가 발생한다. 하지만 오버헤드를 갖더라도 드로우콜을 줄임으로써 전체적인 성능 향상을 가져오게 되는 것이다.
Skinned Mesh에 적용이 불가능하다. 스키닝은 GPU나 SIMD에서 고속으로 연산을 수행한다. 이를 다이나믹 배칭으로 묶으면 CPU 연산 효율이 떨어지기 때문에 배칭의 영향을 받지 않는다. 캐릭터 각각은 별도의 드로우콜로 렌더링 되어야 한다.
버텍스가 너무 많은 메쉬는 다이나믹 배칭의 대상에서 제외된다.오버헤드가 드로우콜의 비용보다 높아질 가능성이 있기 때문에 300 이하의 버텍스를 가진 모델만 다이나믹 배칭 적용이 가능하다. (책이 쓰여진 당시 기준)
일반적으로 메쉬가 렌더링 될 때에는 GPU에서 고속으로 연산되어 버텍스 쉐이더에서 월드스페이스로 변환이 이루어진다.
하지만 다이나믹 배칭을 위해서는 버텍스를 월드스페이스로 변환하는 연산이 CPU에서 일어나게 된다. 이런 과정이 드로우콜보다 많은 시간을 잡아먹게 되면 효율이 떨어지는 것이다.
특정 오브젝트의 오버헤드가 크다고 판단되면 쉐이더 태그에서 DisableBatching 플래그를 True로 설정하면 된다.
SubShader{
Tags { "RenderType"="Opaque" "DisableBatching" = "True"}
스케일이 -1.1 등으로 되어 있는 경우 odd negative 스케일이 되어 다이나믹 배칭이 지원되지 않는다.
스키닝
스켈레탈 애니메이션이 적용되지 않은 기본 형태의 메쉬를 현재의 애니메이션 포즈에 맞게 변형하는 과정.
Skinned Mesh Renderer가 적용된 메쉬는 렌더링 전에 스키닝 연산이 이루어지며, 버텍스 위치의 재계산이 일어난다. 따라서 스키닝 되는 메쉬의 폴리곤이 많을 수록 렌더링의 부담과 스키닝의 부담이 동반된다. 스키닝 연산은 CPU에서 이루어지기 때문에 버텍스 수가 많은 스키닝 메쉬는 CPU의 부담을 유발할 수 있다.
Project Settings > Player > Other Settings > GPU skinning 옵션을 체크하면 스키닝 연산을 GPU를 통해 할 수 있다.
GPU 스키닝 연산은 SIMD(Single Instruction Multiple Data) 아키텍쳐를 이용하여 고속 연산을 수행한다. GPU 병목인 상황에서는 CPU 스키닝으로 처리하는 것이 나은 선택일 수 있다. 프로파일링을 거쳐서 결정해야 하는 부분이다.
Vulkan과 Metal에서는 GPU 스키닝이 동작하지 않지만 지원 여부는 바뀔 수 있으므로 확인하고 사용해야 한다.
Mesh.CombineMeshes
유니티에서 제공하는 배칭 외에도 Mesh.CombineMeshes 메소드를 이용하여 동일한 메테리얼을 사용하는 메쉬끼리 합칠 수 있다.
DCC 프로그램에서 메쉬를 합쳐주는 것이 좋지만 런타임 동안 파츠가 조합되어 오브젝트가 만들어져야 하는 경우라면 해당 메소드를 이용하여 드로우콜을 줄일 수 있다.
https://docs.unity3d.com/ScriptReference/Mesh.CombineMeshes.html
https://github.com/ozlael/CombineMeshSample
GitHub - ozlael/CombineMeshSample
Contribute to ozlael/CombineMeshSample development by creating an account on GitHub.
github.com
2D 스프라이트 배칭(Sprite Batching)
플레이어 설정에서 스태틱 배칭, 다이나믹 배칭 설정이 되어있지 않더라도 자동으로 배칭이 이루어진다.
같은 메테리얼끼리 배칭이 가능한 것은 2D 스프라이트도 마찬가지이다. 따라서 텍스쳐 아틀라스와 같은 기법으로 스트라이트들을 하나의 이미지에 모아 놓는 스프라이트 시트를 사용한다.
Project Settings > Editor > Sprite Packer > Always Enabled로 설정한다. 이후 Project 뷰에서 Create > Sprite Altas를 선택하여 에셋을 만들어 사용한다.
https://github.com/ozlael/SpriteAtlasSample
GitHub - ozlael/SpriteAtlasSample
Contribute to ozlael/SpriteAtlasSample development by creating an account on GitHub.
github.com
GPU 인스턴싱
GPU 인스턴싱은 다른 배칭에 비해 런타임 오버헤드가 적다. 다른 배칭은 CPU에서 지오메트리 정보를 연한해서 메쉬로 합치는 과정을 거치고 GPU는 이를 렌더링하는 방식이기 때문에 오버헤드가 발생하거나 메모리 이슈가 발생할 수 있다. 하지만 GPU 인스턴싱은 별도의 메쉬를 생성하지 않고 인스턴싱되는 오브젝트들의 트랜스폼 정보를 별도의 버퍼에 담는다. GPU는 트랜스폼 정보가 담긴 버퍼와 원본 메쉬를 가져다가 여러 오브젝트들을 한번에 처리해서 렌더링한다. 이 과정에서 인스턴싱 처리를 GPU에서 하기 때문에 GPU 인스턴싱이라고 부른다.
오버헤드에 의한 제약이 적다보니 원본 메쉬의 버텍스 개수와는 상관 없이 런타임에서 동적인 오브젝트들을 처리해줄 수 있다.
메테리얼의 파라미터 중 Enable GPU Instancing을 체크하면 된다.
동일한 메쉬끼리만 한 번의 드로우콜로 처리가 가능하다. 동일한 모양의 오브젝트들이 여러 개 렌더링 되어야 할 때 유용한 기법이다. MeshRenderer에만 적용되며 Skinned Mesh에는 적용되지 않는다.
GPU에서 처리해야 하기 때문에 디바이스의 스펙에 의존적이다. OpenGL ES 3.0 이상, Vulkan, Metal에서 사용 가능하다.
https://github.com/ozlael/GpuInstancingSample
GitHub - ozlael/GpuInstancingSample
Contribute to ozlael/GpuInstancingSample development by creating an account on GitHub.
github.com
4-3 프레임 디버거(Frame Debugger)
프레임 디버거는 프레임이 어떻게 렌더링되는지 직관적으로 확인할 수 있다. 드로우콜을 순서대로 확인할 수 있고 어떤 메쉬가 렌더링되는지뿐 아니라 쉐이더의 속성도 확인할 수 있다. 배칭처리 중 드로우콜이 나뉜 이유도 표시된다.
4-4 컬링(Culling)
컬링 과정이 효과적일 수록 불필요한 오브젝트가 적게 그려지고, 렌더링되는 오브젝트가 줄어들게 되면 그만큼 드로우콜도 줄어들게 된다. 불필요한 렌더링을 수행함으로써 낭비될 수 있는 GPU 성능도 절약할 수 있다.
프러스텀 컬링(Frustum Culling)
Near Clipping Plane과 Far Clipping Plane으로 결정된다.
Far를 줄이면 렌더링되는 대상이 줄어들면서 드로우콜도 줄어든다. 하지만 원거리에 있는 오브젝트들이 잘려나감으로써 어색함이 발생한다. 이를 방지하기 위한 기능이 Fog 이다. Lighting 창에 Fog 옵션이 있으며 원거리에 있는 픽셀을 자연스럽게 숨기는 기능이다.
오클루전 컬링(Occlusion Culling)
오클루전 컬링은 다른 오브젝트에 의해 숨겨진 오브젝트를 걸러내는 기능이다. 벽에 의해 가려지는 오브젝트들에 대한 드로우콜을 절약할 수 있다. 드로우콜 뿐만 아니라 GPU의 부담도 줄어들기 때문에 최대한 활용하면 좋다.
Window > Rendering > Occlusion Culling을 선택하여 Bake를 진행해야 한다. Inspector에서 Static 설정을 해야 한다.
Occluder는 다른 오브젝트를 가리는 역할, Occludee는 다른 오브젝트에 의해 가려지는 역할이다. 보통은 둘 다 체크해서 Bake하면 된다. 사전 연산을 거쳐서 데이터를 만들어 놓아야 하기 때문에 Occluder는 스태틱 오브젝트만 가능하다. 하지만 Occludee는 다이나믹 오브젝트도 해당될 수 있다. Mesh Renderer 컴포넌트에 Dynamic Occluded 플래그를 켜면 캐릭터 등 다이나믹 오브젝트도 렌더링 대상에서 제외될 수 있다.
오클루전 컬링의 경우 Bake 탭에서 파라미터를 조절할 수 있다.
Smallest Occluder의 경우 씬을 일정 셀로 나누는 기준을 정한다. 이 수치가 작을수록 컬링의 정밀도가 올라간다. 하지만 데이터 크기가 늘어나고 연산 오버헤드가 발생한다. 가려진 오브젝트를 연산하기 위해서 추가적인 CPU 오버헤드가 발생할 수 있다. 잘못하면 드로우콜을 줄임으로써 얻는 이득보다 오버헤드가 더 큰 상황이 될 수도 있다. 따라서 야외 씬에서는 적합하지 않을 수 있다. 걸러지는 오브젝트는 없으면서 컬링 연산 오버헤드는 항상 존재하기 때문.
LOD(Level of Detail)
원본보다 적은 메쉬로 GPU 부담을 줄이는 것뿐 아니라 일찌감치 컬링시켜서 드로우콜을 줄이는 용도로 사용할 수도 있다. GPU의 부담 뿐만 아니라 CPU의 부담도 줄일 수 있다.
LOD Group 컴포넌트를 추가하여 사용할 수 있다.
참고 자료
유니티 그래픽스 최적화 스타트업 - 예스24
게임개발의 최대 난적, 그래픽스 최적화를 다루는 책이 책은 게임개발의 최대 난적이라 할 수 있는 게임개발의 최적화에 대해서 다루는 책이다. 특히 유니티 엔진을 기반으로 게임을 가볍게 만
www.yes24.com
'Unity > 내용 정리&Tip' 카테고리의 다른 글
| [Unity] 유니티 그래픽스 최적화 스타트업 정리(2) - 라이팅, 그림자, GI, 텍스쳐 (2) | 2025.09.27 |
|---|---|
| [Blender] glb/gltf 3D 포맷에서 글래스모피즘(GlassMorphism) 효과 만들기 (0) | 2025.05.28 |
| [Unity] 물 효과 연구(1) - VFX, 텍스쳐 파티클 시스템 사용하기 (3) | 2024.07.24 |
| [Unity] 나무와 풀이 바람에 영향을 받는 것처럼 보이는 셰이더 만들기 (0) | 2024.07.09 |
| [Unity] VFX를 이용해 모닥불 만들기 (0) | 2024.05.21 |