* [유니티 그래픽스 최적화] 시리즈는 비엘북스 <유니티 그래픽스 최적화 스타트업>을 읽고 스스로 이해한 바를 정리하는 시간을 갖고자 작성하는 시리즈입니다. 책을 그대로 옮겨쓴 것이 아니고 이해한 대로 재 서술하는 것이기 때문에 틀린 점이 있을 수 있습니다. 정확히 알고 계시는 분은 댓글로 알려주시면 감사하겠습니다 !
01. 병목의 이해
성능 최적화란? 적은 자원을 사용하더라도 연산 효율이 높아지도록 최적의 성능을 이끌어 내는 것.
최적화를 위해 할 수 있는 노력들은...
- Mesh의 Vertex 줄이기
- 텍스처 크기 줄이기
- 가벼운 Shader 사용
- Draw Call 줄이기
- 게임 로직 최적화
- 물리 연산 줄이기
등등이 있는데 그 전에 선행되어야 할 것이 병목을 탐지하는 것.
Bottleneck, 병목
전체 프로세스가 갑자기 느려지거나 막혀서 정지하는 원인이나 그 장소.
병목현상이 발생했다 ! == 전체 성능이나 용량이 어떤 하나의 구성요소 때문에 제한 받는 일이 생겼다 !
특정 로직 수행이 오래걸리면 그 친구 때문에 전체 성능이 떨어지게 되는 것.
따라서 최적화를 하려면 병목 현상이 누구 때문에 일어나는지 찾아야 함.
프로파일링
병목 지점을 찾는 과정. 이걸 도와주는 툴은 프로파일러.
프로파일링을 통해 원인을 찾고 걔를 먼저 최적화해주고 다시 확인하면서 최적화 진행.
FPS vs Frame Time
Frame Time은 한 프레임을 처리하는데 걸리는 시간.
프로파일링 할 때는 프레임타임으로 하는 것이 좋다.
일반적으로 프레임 타임은 ms 단위로 측정.
30FPS
= 1초에 30프레임
= 1프레임에 1/30초
이건 초단위의 경우. ms = 1/1000초니까
FPS를 ms로 환산하면 ms = 1000/FPS.
반대로 FPS = 1000/ms.
>>Display FPS
유니티 에디터 좌측 상단의 Status를 누르면 FPS와 프레임 타임을 확인할 수 있음.
but 실제 타깃 디바이스에서 성능을 확인하고 싶다면 프로파일러를 연결하거나 FPS를 출력하는 스크립트를 짜서 직접 출력해야함.
아래 주소에서 DisplayFPS 샘플을 다운받을 수 있다.
https://github.com/ozlael/FPSCheckerSample
구간 측정
성능을 측정할 때 포인트는 각 프레임을 렌더링하는데 시간이 얼마나 걸리는지다.
FPS로만 측정하면 성능의 최종 결과만 확인할 수 있고, 어떤 구간에서 얼마나 시간이 걸렸는지는 모른다.
하지만 최적화를 하려면 어디에서 얼마나 시간이 걸렸는지를 알아야하기 때문에 프레임 타임으로 측정하는 것이 옳다.
선형적인 측정
FPS로 성능을 측정하고 비교할 때, FPS 값은 비선형적인 변화를 보이기 때문에 성능 저하가 일정하게 일어나고 있는지 판단하기 어렵다.
예를 들어, 90FPS였던 게임에서 오브젝트를 추가할 때마다 FPS가 떨어지는 것을 측정했는데 그 변화가
90.0 -> 45.0 -> 30.0 -> 22.5 -> 18.0 이었다고 가정하자.
오브젝트 하나를 추가할 때마다 일정하게 성능이 변화해야하는데, 일정하게 변화한 것처럼 보이지는 않다.
이것을 프레임 타임으로 환산해보면
1000/90.0 = 11.1ms,
1000/45.0 = 22.2ms,
1000/30.0 = 33.3ms,
1000/22.5 = 44.4ms,
1000/18.0 = 55.5ms.
두둥. ms로 보니 사실 일정하게 변화한 것이 맞았다.
그래서 FPS가 비선형적, ms가 선형적 측정.
프레임 타임으로 측정해야 변화량을 제대로 측정할 수 있다는 것이 결론이다.
측정 시나리오
데이터를 측정할 때는 여러 번 측정하여 평균값을 내고, 여러 씬, 여러 상황에서 다양하게 측정하는 것이 좋다.
씬마다 오브젝트 개수나 그래픽 설정이 달라 병목 원인이 다를 수 있기 때문이다.
디바이스에서 Randering Path가 Deferred로 설정되어 있는 경우, 기본적으로 대역폭에 요구되는 비용이 크기 때문에 씬이 간단하더라도 FPS가 잘 안나올 수 있다. 또 후처리 효과들도 기본 비용이 크다.
Target Frame Rate
강제로 최고 FPS를 설정해서 throttling 상황이 발생하지 않도록 하는 것이다.
Throttling은 주로 모바일 디바이스에서 자주 발생하는데, 기기의 발열이 심해져서 일정 수준 이상이 되면 발열을 낮추기 위해 자동으로 성능을 낮추는 기능이다. 쓰로틀링 상태가 되면 연산속도가 느려지고 렌더링 시간도 늘어나서 결국 FPS가 떨어지게 된다.
그래서 최고 FPS를 제한해두고 그 위로 성능이 나올 수 있음에도 발열을 막기 위해 렌더링 시간의 여유를 두고 한 프레임 수행을 마치고 남은 시간동안 프로세서가 잠시 쉴 수 있도록 하는 것이다. 프로파일링을 할 때는 FPS가 60까지 나오는 지 확인해야하기 때문에 60으로 설정해야하는 것이 맞다.
애플리케이션이 시작될 때
Application.targetFrameRate = 40;
하고 설정하면 40FPS로 강제 고정시킬 수 있다.
VSync, 수직동기화
Vertical Synchronization. 모니터 주파수와 렌더링 퍼포먼스를 맞춰 Tearing 현상을 방지하는 역할.
만약 targetFrameRate를 높게 설정해 뒀는데도 60FPS를 넘지 못한다면 외부적인 영향 때문일 수 있는데, Vsync의 영향일 가능성이 높다.
Tearing
모니터는 화면 갱신 주파수에 한계가 있는데 그를 넘는 신호를 입력하면 화면이 물결치거나 찢어지는 듯한 현상을 볼 수 있다. 이 현상을 Tearing이라고 하는데, 더블버퍼링을 할 때 Back Buffer가 아직 렌더링 되고 있는 동안 Front Buffer와의 전환이 이루어져 지금 렌더링 중이던 화면과 이전 화면이 섞여서 나타나는 현상이다.
이 Tearing을 방지하기 위한 것이 VSync이다.
VSync를 활성화하면 디스플레이하는 모니터의 주파수에 맞게 렌더링 퍼포먼스를 조절해줘서 마치 targetFrameRate가 설정된 효과를 보인다. 성능 측정 시에는 당연히 VSync를 꺼줘야 문제를 확인할 수 있을 것.
Edit > Project Settings > Quality > Other > V Sync Count 에서 설정해준다.
CPU 바운드 vs GPU 바운드
게임은 CPU, GPU 모두 연산이 들어가기 때문에 둘 모두에게서 병목이 발생할 수 있다.
병목이 CPU에 몰려있으면 CPU 바운드, GPU에 몰려있으면 GPU 바운드.
병목이 어느 바운드인지 인지하는 것이 중요.
CPU랑 GPU가 직렬적으로 작동하면 어디 병목인지 상관없이 무조건 일처리를 줄이면 전체 시간이 줄어들 것이지만
얘네는 병렬적으로 작동하기 때문에 어디 병목인지 알아서 처리해줘야 전체 시간이 줄어들 것이다.
프레임이 시작하고 나서 CPU가 뭔가 수행하다가 렌더링해야하는 상황이 오면 GPU를 불러 명령을 주고 (GPU가 처리를 끝낼 때까지 기다리는 것이 아니고) 자기는 또 하던 일을 한다. 그러다가 또 렌더링할게 있으면 명령을 휙 던지고 하던일을 한다...그리고 GPU는 받은 명령을 순서대로 수행한다.
그러다가 CPU가 할일을 모두 마쳤는데 GPU가 아직 일하는 중이라면 끝날 때까지 기다린다. GPU까지 일이 끝나면 비로소 한 프레임이 끝나고 화면에 출력한다. 이런 상황이면 GPU 때문에 처리 시간이 늦어졌으니 GPU 바운드인 것이고 아무리 CPU 연산을 줄여도 어차피 GPU를 기다려야하기 때문에 최종 성능에 영향을 주지 못한다.
반대로 GPU가 명령이 별로 없어서 일찍 끝나고 CPU가 연산을 계속 하고 있는 상황이라면 CPU 바운드이기 때문에 GPU의 일감을 줄여봤자 성능에 영향을 주지 못한다.
결과적으로 병목이 어느 바운드인지 먼저 알아야 해결을 할 수 있다는 얘기.
02. 병목의 측정
유니티는 프로파일러를 제공해준다!
빠르고 간단하게 CPU 바운드, GPU 바운드를 확인해 볼 수 있다.
Window > Analysis > Profiler 또는 Crtl + 7 을 눌러 창을 열 수 있다.
프로파일러를 연결하려면 Development 빌드로 해야하는데 이 때는 프로파일러 정보 등 각종 오버헤드가 포함되기 때문에 당연히 오버헤드가 더 많이 발생되므로 실제 배포용 빌드랑은 동일한 성능이 나오지는 않는다. 또 플랫폼, 디바이스마다 병목 지점이 다를 수 있으므로 실제 타깃 디바이스에서 확인해보는 것이 좋다.
하단에 Timeline 드롭박스 메뉴가 있는 것을 볼 수 있는데, 거기서 Hierarchy를 선택하면 어떤 항목들이 성능을 얼마나 잡아먹고 있는지 볼 수 있다.
GPU 성능이 좋지 않은 구형 디바이스일수록 Graphics.PresentAndSync가 성능 비용을 많이 잡아먹어 가장 상단에 위치할 것이다. GPU 바운더리에서 병목이 있다는 것은 알 수 있으나, 그 중 어디가 문제인지는 유니티 내장 프로파일러만으로는 알아내기 힘들다. 이 부분은 추가적인 테스트가 따로 필요함.
Setting for Android
>> Player Settings
Other Setting > Color Space를 Linear -> Gamma로 바꿔준다.
why? OpenGL ES 버전 호환성을 위해서.. Linear로 설정하면 OpenGL ES 2.0을 지원하지 못한대
Identification > Package Name을 변경해준다. 안드로이드 패키지의 이름을 설정해주는 건데 보통 [com.제작자이름.프로젝트명] 형식으로 쓴다.
>> Build Settings
Development Build, Autoconnect Profiler 두 개를 체크해준다.
안드로이드 폰과 PC를 USB로 연결하고 동일한 네트워크 상으로 설정한 후 빌드를 진행하면 된다.
빌드가 완료되면 폰에 apk가 자동으로 설치되고 게임이 실행된다. 제대로 Development Build를 체크했다면 화면 오른쪽 하단에 Development Build라고 뜰 것이다. 이게 떠야 프로파일러에 연결이 가능하고 PC에서 프로파일링이 가능하다.
03. GPU 병목 탐지
프로파일러 창의 Add Profiler 드롭박스 메뉴에서 GPU를 선택해서 추가하면 어떤 부분이 GPU를 잡아먹는지 확인할 수 있다.
대부분의 PC, 콘솔에서는 가능하지만 모바일 디바이스에서는 지원하지 않는다.
안드로이드 - Vulkan 지원가능, Open GL ES의 경우 NVIDIA, Intel GPU일 때만 지원가능.
외부 툴을 이용해서 GPU를 프로파일링하고 싶다면
스냅드래곤 칩셋을 사용하는 디바이스라면 Aualcomm에서 제공하는 전용 툴로 GPU 프로파일링이 가능하다.
iOS의 경우 XCode에서 제공하는 GPU Frame Debugger를 통해 가능하다.
Fillrate, 필레이트
그래픽 카드가 1초에 스크린에 렌더링할 수 있는 픽셀 수. 게임 렌더링에서는 더 많은 뜻을 내포하고 있다.
픽셀 처리에 대한 부담을 모두 포함한 개념이 필레이트.
필레이트 = 화면 픽셀 수 * 프래그먼트 쉐이더 복잡도 * 오버드로우
픽셀이 많거나, 프래그먼트 쉐이더가 너무 복잡하면 렌더링 파이프라인의 Rasterizer 부분에서 병목이 발생한다. 이때를 필레이트가 병목이 되었다고 표현한다. 필레이트에서 병목이 발생한 거니까 필레이트 바운드라고도 하지.
GPU 병목 중 높은 확률로 필레이트가 원인인 경우가 많다.
디스플레이 해상도를 변경했을 때 성능이 눈에 띄게 좋아진다면 해상도(렌더링 해야하는 픽셀 수)가 병목 원인이므로 이때는 필레이트 병목을 강하게 의심할 수 있다.
>>해상도 설정
Edit > Project Settings > Player 에서 Resolution Scaling Mode를 Fixed DPI로 변경해주면 특정 해상도가 아닌 DPI(Dots Per Inch)에 맞춰서 해상도가 자동 조정된다. DPI가 낮을수록 해상도가 낮게 조절된다.
특정 해상도로 고정해놓고 싶다면 Screen.SetResolution()으로 설정할 수 있다.
Overdraw, 오버드로우
화면에 렌더링되는 하나의 픽셀이 여러 번 덧그려지는 현상.
하나의 오브젝트를 렌더링했는데 그 위에 다른 오브젝트를 겹쳐서 그릴 때 픽셀을 여러번 그리게 되므로 발생하는 건데,
Z 버퍼 검사를 하면 뒤에 나올 친구는 안 그리도록 걸러줘서 오버드로우를 방지할 수 있다.
오버드로우를 막기 위해 유니티는 렌더링 전에 오브젝트들을 카메라와의 거리 순으로 정렬하고 그 순서대로 렌더링한다. 앞에 나오는 친구부터 그리니까 그 뒤 오브젝트를 그릴 때 겹칠부분은 아예 안그리게 되므로 오버드로우가 발생할 확률이 낮아진다.
투명 오브젝트
투명한 오브젝트는 반대로 정렬해야한다. 뒤에 있는 오브젝트여도 가려지지 않고 보여야하므로 뒤에 있는 애들부터 렌더링한다. 투명 오브젝트는...모든 픽셀에서 오버드로우가 발생할 수 밖에 없다.
오버드로우 뿐 아니라 프레임 버퍼를 읽어오는 과정에서도 병목이 발생할 수 있다.
또 투명 오브젝트를 그리려면 지금 그리는 픽셀 컬러와 기존의 프레임 버퍼에 있는(이미 그려진) 픽셀의 컬러를 블렌딩해야하므로 프레임 버퍼의 색을 읽어와야하는데, 이 과정에서 병목이 발생할 수 있다고한다.
파티클
파티클도 오버드로우가 발생하기 쉽다. 파티클의 밀도가 높을 수록 당연히 오버드로우가 많아지겠지.
씬 뷰의 좌측 상단 Draw모드를 Overdraw로 설정하면 시각적으로 확인할 수 있다. 픽셀의 색이 밝을 수록 오버드로우가 많이 일어나는 픽셀이다.
포스트 프로세싱
아까 필레이트에 영향을 미치는 요소로 프래그먼트 쉐이더의 복잡도도 한 몫했었다.
프래그먼트 쉐이더가 무거워지는 이유 중 하나는 포스트 프로세싱이다. 포스트 프로세싱이 병목의 원인이라면 가장 쉬운 방법은 역시 해상도를 줄이는 것이다.
포스트 프로세싱도 종류별로 성능 비용이 다르다. Color Grading은 모바일에서도 부담이 없고, Bloom까지도 요즘 모바일의 성능이 좋아지면서 사용되고 있다. DOF는 모바일에 적용하기에는 비용이 크기 때문에 인게임에서는 사용하기 힘들고 컷씬이나 로비등에서 사용한다. DOF는 프래그먼트 쉐이더에 부담을 줄 뿐 아니라 드로우콜이 늘어나는 요인이 되기도 한다.
Upscaling Sampling, 업스케일링 샘플링
해상도를 줄이는것이 가장 쉬운 방법이지만, 해상도를 줄이면 유저가 알아채기 쉽다.
이를 보완하기 위해 UI의 해상도는 그대로 유지하고, 3D 씬만 해상도를 낮추는 일종의 트릭을 쓰는 것이 업스케일링 샘플링이다. 왜 이렇게 길고 어려운이름인지는 모르겠다.
구현 순서는 다음과 같다.
- 저해상도 렌더 텍스쳐(Render Texture)를 생성한다
- 3D 씬을 렌더 텍서쳐에 렌더링한다
- 렌더 텍스쳐를 업스케일링해서 현재의 백 버퍼에 렌더링한다
- 오버레이(Overlay) UI를 렌더링한다
핵심은 전체 해상도를 줄이는 것이 아니라는 것. 현재 디스플레이 되는 해상도는 그대로 두고, 저해상도의 렌더 타깃(현재 렌더링을 수행하고 있는, 타깃이 되는 버퍼)으로 렌더 텍스쳐를 생성해서 렌더링 하는 것이다. 그러면 결과적으로 3D 씬은 저해상도로 렌더링되고, UI는 원래 해상도로 렌더링된다.
폴리곤
폴리곤 수가 많으면(=버텍스 수가 많으면) Geometry 부분에서 병목이 생길 것이다. 버텍스 수가 많다는 것은 그만큼 Vertex Shader를 수행해야 하기 때문. 폴리곤 수를 줄여봤을 때 성능이 눈에 띄게 좋아진다면 여기서 병목이 생긴다는 것을 알 수 있다.
LOD, Level of Detail
디테일에 단계를 나누는 것. 가까운 친구는 자세히 그려야되니까 디테일을 높게하고, 카메라에서 먼 친구는 대충 그려도 되니까 디테일을 줄이는 것이다. 카메라에서 먼 친구는 적은 폴리곤을 사용하도록 해서 그림으로서 버텍스 수를 줄이는 것. LOD 그룹을 나눠서 모델을 지정하고 해당 LOD레벨에 지정된 모델이 렌더링 되도록 하는 것이다.
LOD를 적용한 오브젝트에는 LOD Group 컴포넌트가 추가된다.
여기서 화면에서 오브젝트가 차지하는 비율에 따라 어떤 레벨의 LOD가 적용되는지 확인하고 카메라나 아래 LOD 영역을 클릭해서 조정할 수 있다.
LOD 레벨이 클 수록(숫자가 클 수록) 적은 폴리곤을 가진 모델이 렌더링된다.
Edit > Project Setting > Quality > Other에서 LOD Bias를 조절할 수 있다.
LOD Bias는 LOD 레벨이 결정되는 화면 내 차지 비율에 반영되는 수치이다.
아래 사진을 비교해보면 성능이 좋을수록 LOD Bias 수치가 크다.
1보다 작은 값으로 설정하면 높은 레벨의 LOD 메시가 렌더링 된다.
Texture, 텍스쳐
텍스쳐도 당연히 병목의 원인이 될 수 있다. 전체 텍스쳐의 해상도를 조절해보면서 텍스쳐가 병목 원인인지 아닌지 확인할 수 있다. 텍스쳐 해상도는 Edit > Project Settings > Quality > Rendering에서 Texture Quality를 조절할 수 있다. Full Res가 가장 높은 해상도, 아래로 갈수록 낮은 해상도이다. 텍스쳐로 인한 메모리 대역폭이 병목의 원인이라면 메모리의 부담을 줄여야하는데 이에 대해서는 '08. 텍스쳐'에서..!
'PROGRAMMING > Unity' 카테고리의 다른 글
[유니티 그래픽스 최적화] 04. 드로우콜과 배칭 (1) | 2020.02.19 |
---|---|
[유니티 그래픽스 최적화] 02. 렌더링 파이프라인 (0) | 2020.02.18 |
[유니티 그래픽스 최적화] 01. 개요 (0) | 2020.02.17 |