알쓸유잡 Unity의 UI시스템 (UGUI) 최적화

* Overlay UI에 집중하여 작성된 내용이며 World Canvas는 다를 수 있습니다.

 

UGUI의 병목의 원인

GPU 바운드

Unity도 그래픽으로 렌더링하기 때문에 GPU를 사용하여 GPU 병목이 발생할 수 있다.

1. Fillrate 병목

- 높은 해상도, 중첩되어 그려 오버드로우의 발생, 무거운 픽셀 쉐이더(유니티 쉐이더의 한계로 확률이 낮음) 등...

* Fillrate : GPU가 초당 화면에 렌더링을 할 수 있는 픽셀의 수

 

CPU 바운드

CPU의 병목인 경우가 대부분이다.

 

1. 드로우콜 / 배치

2. 캔버스 배치 구축 연산 시간

- 동적인 UI를 매 프레임마다 재구축해야하는 연산 시간

3. 버텍스 생성시간

- GPU가 렌더링하기에 UI도 버텍스가 필요하여 생성시간이 필요하다.

4. 수 많은 UI엘리먼트 등

 

UI 메시

와이어프레임 뷰로 확인하면 UI도 메시로 이루어져 있다는 것을 알 수 있다.

따라서 3D 그래픽에서 신경써야할 병목현상을 UI에서도 동일하게 신경써야한다.

 

메시가 만들어지는 과정

1. 정점 정보(Vertex Buffer)와 정점을 어떻게 그릴지 순서를 담은 정보(Index Buffer)를 통해 폴리곤을 형성한다.

2. Render State에서 폴리곤을 반시계 방향(Counter ClockWise) 또는 시계 방향(ClockWise)으로 그릴 것인지를 설정한다.

3. Render State에서 폴리곤을 삼각형(Triangle List)으로 그릴 것인지를 설정한다.

* CCW, CW과 상관없이 후면제거를 하지 않고 양면을 그리는 설정도 있다고 한다.

4. Vertex에 대한 정의(Vertex Decl)를 해준다. Vertex Buffer는 데이터 공간으로 데이터(Position, Color, UV좌표)를 GPU가 어떻게 끊어서 읽어야하는지 정의해준다.

* UV좌표 : 3차원 공간의 폴리곤에 텍스처를 입히기 위한 기준이 되는 2차원 좌표계

 

렌더링 정보는 CPU Memory에서 데이터를 연산하여 GPU Memory에 넘겨주는 형태로 이루어진다.

정적인 3D오브젝트의 경우 로딩 타임에 한 번 연산하여 GPU Memory에 전달하고 저장된 데이터를 기반으로 렌더링을 진행하나

UI와 같이 메시가 변하는 동적인 오브젝트의 경우 매 프레임마다 CPU가 연산하여 GPU에 전달해야 하기 때문에 연산비용이 발생한다.

때문에 UI에서 CPU쪽 병목현상이 많이 발생한다.

 

Graphic(.cs)

그래픽적인 요소를 담고 있는 클래스

Graphic은 Unity UI C# 라이브러리에서 제공하는 기본 클래스이다.

유니티의 Image, Text, TMP_Text 클래스는 Graphic 클래스를 상속받고 있다.

 

Rebuild()

동적인 오브젝트의 Vertex의 변경(UpdateGeometry)과 머테리얼의 변경(UpdateMaterial)이 이루어지는 것을 알 수 있다.

 

DoMeshGeneration()

OnPopulateMesh() : 새로운 Vertex Buffer 설정(AddVert)과 Index Buffer 설정(AddTriangle)

FillMesh() : 메시에 설정된 데이터(Vertices, Colors, UVs, Normals, Tangents, Triangles)를 넣어주는 과정

 

Canvas(.cpp)

실제 렌더링하는 클래스로 메시를 구성하고 GPU에게 드로우콜을 한다.

Canvas 단위로 배칭이 이루어지기 때문에 Canvas의 하위 오브젝트가 변경되면 Canvas가 Rebuild된다.

 

Canvas 클래스는 RenderOverlays()를 상속받고 있으며 RenderOverlays는 재귀적으로 호출하고 있다.

Canvas별로 렌더링이 이루어진다는 것을 알 수 있으며, 자식 Canvas가 있다면 자신의 데이터는 스스로가 관리한다.

따라서 정적인 UI와 동적인 UI는 구분하여 Canvas를 구성하는 것을 권장하고 있으나, 갱신비용과 드로우콜비용 등 상황 따라 선택이 달라질 수 있다.

 

Nested Canvas

Canvas는 Canvas를 소유할 수 있다.

Canvas는 Nested Canvas List를 갖고 있고 RenderOverlays()를 호출한다.

Root Canvas에서 관리하는게 베스트지만 관리의 편의성 차원에서 Nested Canvas 기법을 사용할 수 있다

자식 Canvas 간에는 재구성 영향을 미치지 않는다 ( 부모의 크기가 변경되는 경우는 예외 )

 

Dirty flag

불필요한 연산을 최소화하기 위하여 변경이 이루어진 값에 Dirty라는 플래그를 세우고 한번에 연산하는 방식.

값의 사용보다 계산이 많이 발생하는 경우(계층 구조의 객체 등) 변경되는 값에 플래그를 세우고 사용할 때 계산하는 방식.

* Dirty flag 디자인 패턴 : https://mm5-gnap.tistory.com/353

UI도 계속적으로 갱신되는 것이 문제이기 때문에 Dirty flag 스타일로 구현되어 있다.

 

RectTransform(.cpp)

Transform을 상속받고 있어 Hierarchy(계층)구조로 이루어지며 오브젝트의 변경이 계층적으로 영향을 준다. (= 계층 구조가 깊어질수록 연산량이 많아진다.)

Re-parenting 비용 : 계층 구조에서 부모가 변경되면 구조의 데이터가 변경되어야 하기 때문에 비용이 발생한다.

(Transform은 계층구조로 이루어져있고 연산을 멀티쓰레드 멀티쓰레드를 활용하여 메모리에 연속적으로 저장하고 있다. Re-parenting이 발생하면 메모리를 재정렬이 발생하기 때문에 비쌀 수 있다.) 

* 오브젝트 풀링을 할 때 Re-parenting을 하는 경우도 있는데, 비용이 발생한다는 것을 염두해 두자

 

Rebuild

C# Graphic 컴포넌트의 레이아웃과 메시가 다시 계산되는 행위

- Dirty 컴포넌트/오브젝트를 기준으로 재계산

 

무엇이 변경되는지에 따라 비용이 다를 수 있다.

1) UI Element Layout 변경되는 경우 -> Dirty Layout

- 계층 구조의 깊이별로 정렬

2) UI Element Graphic 변경되는 경우 -> Dirty Graphic

- Vertex 데이터 Dirty(RectTransform 크기 변경) : 메시 다시 빌드

- Material 데이터 Dirty(텍스쳐 변경) 연결된 Canvas Renderer의 Material 업데이트

 

모든 enabled 요소들의 메시를 재생성

- 완전 투명(alpha==0)이라도 생성된다. (ex. fade in/out)

 

Batching을 기준으로 머테리얼 재생성

- Canvas 기준으로 렌더링 되지만 쉐이더나 머테리얼 등에 따라서 드로우콜이 여러번 발생할 수도 있다.

(Batching 요소를 신경써야 한다.)

 

Profiler

아래 프로파일러를 통해 확인할 수 있다.

- CanvasUpdate.Prelayout, CanvasUpdate.Layout, CanvasUpdate.PostLayout

- CanvasUpdate.PreRender, CanvasUpdate.LatePreRender

- Canvas.SendWillRenderCanvases

 

Batch building (Canvas)

Unity의 그래픽 파이프라인으로 보낼 적절한 렌더링 명령을 생성

캔버스가 Dirty로 표시될 때까지 GPU에 캐시되고 재사용 (CPU 연산이 발생하지 않음)

캔버스 렌더러 컴포넌트 기준

하위 캔버스에는 포함하지 않음

한 번의 드로우콜로 만들어낼 수 있도록 배치기준을 만들어 냄

배치를 계산하기 위해 많은 연산을 거침 (깊이별로 정렬, 중첩 확인, Shared Material 확인)

멀티 스레드 연산

모바일 SoC(적은 CPU코어)와 데스크톱 CPU(많은 코어) 간 성능이 매우 다름 -> 타겟 PC에서 프로파일링 해야함

 

Batching

동일한 캔버스

동일한 머티리얼 및 스프라이트 에셋 (아틀라스)

동일한 Z 깊이의 RectTransform (Z값이 다르면 alpha 오브젝트를 처리해야하는 특성 때문에 Batching이 깨짐)

동일한 마스크 적용

 

Frame Debug를 통해 드로우콜 추적을 할 수 있다.

그러나 3D의 경우 Batching이 깨지는 원인을 알려주나 UI는 알려주지 않는다.

따라서 UI Profiler를 통해 call마다 확인할 수 있으며 Batching이 깨지는 원인을 확인할 수 있다 (Profiler Modules -> UI / UI Details)

 

Rendering order : Front to Back vs Back to Front

불투명 오브젝트는 앞에서부터 그린다.

투명 오브젝트는 뒤에서부터 그린다.

 

UI도 동일하게 투명한 오브젝트는 뒤에서부터 그리기 때문에 Z축이 달라지면 드로우콜이 나눠진다. 

 

Pixel Perfect

RectTransform 변경 시 모든 정점을 재계산

UI에서 움직이는 엘리먼트에는 치명적인 성능 하락

(2D Sprite의 Pixel Perfect와는 연산량이 다름)

 

Pixel Perfect를 활용해야하는 경우 움직일 때는 Pixel Perfect를 키고 끄는 등의 처리가 필요

Nested Canvas를 활용해서 Pixel Perfect를 구분하는 처리가 필요

 

Layout components

Vertical, Horizontal, Grid, Layout Element, ...

UI를 구조화하고 UI 요소를 순서대로 설정하는 데 매우 유용

- 콘텐츠의 상대적 크기 또는 상대적 위치 지정이 필요한 복잡한 레이아웃에 사용

RectTransform의 크기와 위치를 제어

- RectTransform에만 의존하며 연결될 RectTransform의 속성에만 영향

- Graphic 클래스에 종속되지 않으며 Graphic 컴포넌트와 독립적으로 사용

Dirty flag 시스템 의존적

- RectTransform 변경 -> Layout이 'dirty'로 설정

- Layout 'dirty' -> Rebuild 프로세스에 추가됨

 

(1:22:11 ~)

728x90

'Program > Unity' 카테고리의 다른 글

직렬화 (Serialization)  (0) 2023.03.08
Mono & IL2CPP  (0) 2023.02.07
Unity  (0) 2022.09.19
나중에 활용  (0) 2022.08.04
Quaternion  (0) 2022.07.14

+ Recent posts