![[Unity 잡학사전] Unity UI 시스템 성능 최적화](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc4Vndz%2Fbtsp35bvc6W%2FmULfc6NtFgoYhmVWfdCCKK%2Fimg.png)
유니티 UI에 대해서 많이 무지한거 같아서..
일단 최적화에 대해 올해 3월에 방송한 내용을 보고 정리를 해봤다.
처음듣고 신기한 내용들이 많았던 영상이다.
보지 않으신 분들은 한번쯤은 보는것을 추천드린다.
https://www.youtube.com/live/1e2mSCS7o1A?feature=share
미리 말하자면 이번 강의는 Screen Space - Overlay 일때의 기준이다.
UI 관련 유니티 소스코드
이 사진에서 보이듯이 유니티는 엔진 내부는 C++ 로 구현이 되어있다.
이 부분은 오픈소스가 아니기 때문에 일반 사용자들은 볼 수 없지만, 유니티와 기술지원 협약이 맺어져있는 회사들은 내부 코드를 볼 수있다고 한다.
그리고 나머지 코딩으로 접근 가능한 부분들이 C#으로 구현이 되어있는데, 이 부분은 공개되어 있어서 코드를 볼 수가 있다.
바로 Packages/UnityUI/Runtime/UI/Core 부분에 있다.
위 사진은 TMP_Text 클래스의 코드인데 MaskableGraphic을 상속받고 있다.
그리고 MaskableGraphic을 들어가보면, Graphic을 상속받고 있다.
영상에는 Image랑 Text 코드들이 다 보였는데.. 나는 접근이 안됐다.
버전 문제인지 아니면 영상에서는 젯브레인의 라이더를 써서 그런것인지 확인이 필요한 것 같다.
유니티의 UI도 하나의 Mesh다
이렇게 이미지를 만들어놓고 Shading Mode를 WireFrame으로 바꾸면, 위와 같이 3D 오브젝트에서 보이는 메쉬형태로 보이는것을 확인 가능하다.
그렇기 때문에 3D 그래픽에서 신경써야할 병목이 똑같이 적용된다고 보면 된다.
드로우콜, 오버드로우, 쉐이더, 필레이트, 버텍스 쉐이더 등등..
Mesh는 데이터의 조합이다.
점 -> 점들이 모여서 선이 되고 -> 선들이 모여서 삼각형이 되고 -> 삼각형들이 모여서 메쉬가 된다.
*버퍼 : 컴퓨터가 받아들일수 있는 데이터 공간
버텍스 버퍼에서 버텍스들의 정보를 가지고있고,
인덱스 버퍼에서 어떤 순서로 선을 이을지에 대한 정보를 가지고 있다.
그리고 렌더 상태에서는 이 선을 시계방향으로 돌릴것인지,
Vertex Decl(버텍스 정의) 에서는 위치, 컬러, UV 등을 담고 있다.
이 모든 정보가 합쳐져서 CPU에서 연산을 하여 GPU 메모리로 넘겨주고, 렌더링을 하라는 Draw Call을 내린다.
일반적인 3D오브젝트는 로딩타임에 CPU가 연산을 끝내고 GPU에게 넘겨주어 GPU가 계속 그 데이터를 가지고 렌더링을 하면 끝이라고 한다. (스키닝처럼 변하는것 제외)
하지만 UI는 계속 동적으로 변한다. (ex. 버튼 클릭, 스크롤 등)
즉, 우리가 육안으로 보이는게 변한다면 UI도 Mesh이기 때문에 CPU에서 연산을해서 Gpu로 넘겨주어야하는데, 이 과정에서 연산이 많아지면(변하는것이 많아지면) GPU보다는 오히려 CPU쪽에 병목이 많이 온다고 한다.
Graphic.cs
위처럼 그래픽 클래스는 캔버스에서 랜더링 가능한 모든 Unity UI C#클래스의 기본 클래스 이다.
해당 클래스에 Rebuild라는 함수가 있는데, 캔버스가 업데이트 되면 버텍스라던지, 쉐이더, 스프라이트의 변경에 대한 업데이트가 이루어진다.
그리고 UpdateGeometry로 파고 들어가보면 OnPopulateMesh라는 함수가 나오는데
실제로 버텍스를 추가하고 삼각형을 만드는 코드를 확인할 수 있다.
Canvas 클래스
그래픽이 그래픽적인 요소를 담고있다면, 실제 렌더링 하는 주체는 캔버스 클래스이다.
결국 이 클래스에서 메쉬를 구성하고 GPU에게 Draw Call을 날려준다.(모든 드로우콜이 캔버스 단위이다.)
캔버스 단위로 배칭이 일어나며, 캔버스 안에 그래픽 클래스를 가지고있는 이미지,UI 와 같은 요소들을 퉁쳐서 하나의 버퍼로 만들어서 GPU에게 준다.
캔버스가 버텍스,인덱스버퍼 등 렌더링에 필요한것들을 다 가지고 있기 때문에 캔버스 하위에 있는 어떤 UI가 하나라도 바뀌게 되면 통째로 렌더링을 하게 되는 것이다.(= 캔버스가 가지고있는 버텍스 버퍼를 갱신해야 함)
그렇기 때문에 우리가 최적화를 위해서 동적/정적 UI의 캔버스들을 나눠주는것이 성능에 이점이 있다.
배칭(Batching)
여러 개체나 요소들을 하나의 그룹으로 묶어서 함께 처리하는 기술
=>
객체를 하나하나 개별적으로 렌더링을 하다보면 성능 저하를 초래하기 때문에, 비슷한 유형의 개체들을 한 번에 묶어서 처리하는 최적화 기법이다.
Nested Canvas
Nested 는 '중첩된'이라는 뜻을 가지고 있다.
즉, 캔버스의 자식으로 캔버스를 가지고 있는 경우가 이에 해당된다.
이런식으로 캔버스 하위에 다른 캔버스를 만들어보면 부모 캔버스의 옵션을 상속받을건지에 대한 옵션도 있다.
다른분들이 어떻게 작업하시는지는 잘 모르지만 나같은 경우도 캔버스안에 캔버스가 있으면 무언가 문제가 있을거라고 생각해 넣지 않았는데, 자식 캔버스에 있는 객체들은 부모 캔버스에서 재구성을 할때 따로 영향을 받지 않는다고 한다.
(= 드로우콜 별개, 또 부모의 크기가 변경되는 경우는 예외라고 한다.)
그 근거로 캔버스를 렌더링하는 함수를 찾아보면, NestedCanvas일때 해당 캔버스의 RenderOverlays 함수를 재귀적으로 호출하는것을 볼 수 있다.
그래서 정적인 배경 하위로 동적인 UI들이 필요하다면 Nested Canvas를 만들어서 작업하는것도 좋아보인다.
Dirty Flag
UI는 계속 갱신되기 때문에 Dirty Flag를 사용한다고 한다.
Dirty Flag가 뭔지 몰라서 찾아보았는데, 개체나 컴포넌트의 속성이 변경됐음을 나타내는 논리 표시라고 한다.
즉, 무언가 변경이되면 변경되었다고 체크를 하는 것이다.
왜 Dirty라는 단어를 사용하는지도 찾아보았는데, 데이터나 변수등이 원래의 정상적인 상태(Clean)에서 벗어났다고 표현하여 Dirty라는 단어를 사용한다고 한다.
보통 계층 구조에서 많이 사용한다고 하는데.. 부모가 움직이면 하위의 객체들도 다 움직여야 하기 때문에 프레임마다 모든 연산을 할수가없어서 Dirty Flag를 체크하고 한번에 연산을 한다고 한다.
Image 클래스의 sprite 프로퍼티인데, Set을 살펴보면 스프라이트가 바뀌었을때 SetAllDirty함수가 실행되는것을 볼 수 있다.
SetAllDirty를 타고 들어가보면 레이아웃이 바뀌었는지, 매터리얼이 바뀌었는지도 확인하여 dirty flag를 체크한다.
만약에 레이아웃이 변경되었다면 ReBuild를 위해 마킹을 하는 MarkLayoutForRebuild 함수로 타고 들어가는데,
레이아웃 그룹을 사용하면 주변 UI들 또한 다 영향을 받기 때문에, 부모를 타고가서 컴포넌트를 가져오고 for문까지 돌려가며 체크를 한다.
한눈에만 봐도 더티를 체크하는것이 싼 작업은 아니다.
RectTransform
유니티 내부 cpp 코드인데, RectTrasform은 Transform을 상속받고 있는것을 볼 수 있다.
그래서 RectTransForm도 결국에는 계층 구조이기 때문에, 부모가 움직이면 하위에있는 자식들까지 모두 연산이 필요한 부분이라 이 부분을 항상 조심해야 한다.
또 부모를 바꾸면 Graphic 클래스안에 있는 위 함수가 호출이 되는데,
SetAllDirty로 더티 체크를 한것을 볼 수 있다.
오브젝트 풀링같은 기법을 사용할때 부모를 바꿔가며 껐다 켰다 하는 경우가 있는데, 육안으로는 아무 변화가 없어보이지만 내부적으로는 SetAllDirty로 더티 체크가 계속되고 갱신이 이루어진다.
이 계층 구조는 멀티 쓰레드를 활용하기 때문에 메모리를 이어서 붙여주는데, 부모를 바꾸면 메모리를 재정렬한다.
즉, 부모를 바꾸는 작업은 싼 작업이 아니다.(UI가 아니라 그냥 오브젝트 더라도)
ReBuild
더티체크가 된 UI들을 하나하나 재빌드하는 과정이다.
더티 플래그는 단순히 bool 변수를 껐다 켰다 하는 과정이 아니고, 큐에 더티체크된 객체들을 다 담는다.
그리고 해당 함수에서 하나하나 리빌드가 들어간다.
Batch Building
캔버스가 Dirty로 표시되기 전까지는 캐싱해둔 데이터로 사용한다 (=변동이 없으면 연산x)
우리는 이 연산을 줄이기위해 Batching 을 많이 만들어 내야 드로우콜을 줄일 수 있다.
이를 위해서는 같은 캔버스에 있어야하며 같은 머테리얼을 써야하고 , Atlas로 묶어주어도 된다.
그리고 처음 안 사실인데 Z값이 다르면 Batching으로 같이 묶을 수 없다.(= 드로우콜이 늘어난다)
불투명하면 z값의 영향을 받지 않지만, 투명하면 뒤에있는 물체부터 그려야 하기 때문이다.
Canvas - PixelPerfect
캔버스 픽셀 퍼펙트는 요소를 더 선명하게 표시하고 흐릿함을 방지하는 기능인데, 이 기능을 킨 요소들을 가지고 동적으로 움직이게 되면 엄청난 프레임 저하가 생긴다.
왜냐면 픽셀퍼펙트라는 이유 하나만으로 저 수많은 연산들이 들어가기 때문이다.
꼭 써야겠다면 동적으로 사용할때는 잠깐 꺼두자.
Layout Components
레이아웃 컴포넌트는 UI작업에서 빼둘수없는 작업이다.
RectTransform에만 의존하며, RectTrasForm이 변경되면 더티로 변경되기 때문에 주변 ui들까지 다 rebuild에 들어간다.
위의 과정을 겪는 상당히 무거운 작업이다.
에디터에서 쓸때는 작업을 마치고 비활성화 해두어도 좋고 런타임때 해상도때문에 쓸때는 처음 세팅하고 꺼두는 방법을 말씀 하셨다.
그리고 이런 스크롤뷰를 만들때 안의 요소들은 오브젝트 풀을 쓰는게 성능향상에 상당한 도움을 준다.
Animator
요즘은 화려한 UI들이 많아서 애니메이터를 쓰는 경우도 많은데, 애니메이터는 계속 돌아가기 때문에 프레임마다 더티로 변경되고 ReBuild의 과정을 겪는다.
Idle 상태에서도 계속 애니메이션이 일어난다면 어차피 다시 렌더링을 해야 하는거니까 상관이 없지만, 눌렀을때만 반응을 하는 UI라면 애니메이션을 쓰지말고 차라리 DoTween같은 트윈류를 쓰는게 낫다고 한다.
RayCaster
나도 작업을 할때 안쓴다면 꼭 끄는 편인데, 객체가 적을때는 유의미한 성능향상을 내지는 않지만..
객체가 많아지면 꺼주는게 유의미한 성능 향상이 있다고 한다.
Full Screen UI
전체 화면을 덮는 UI를 띄울때 화면에 3d 오브젝트들이 보이지 않더라도,
뒤에서 카메라는 계속해서 3D 오브젝트들을 렌더링하고 있다고 한다.
카메라를 키고 끄는것 보다는 오브젝트가 없는 쪽으로 카메라를 돌려주는것을 추천 하셨다.
Text 보단 Icon
텍스트보다는 아이콘을 쓰자.
Text Mesh Pro
Text Mesh Pro는 글자들을 미리 텍스쳐로 제작해놓는 방식이다.
영미권 언어는 구워놓을 글자가 많지않아 상당히 유용하지만..
한글로 쓰려면 수많은 문자들이 있기때문에 큰 텍스쳐 크기를 사용해야 하는데(ex.4096*4096),
이는 메모리 대역폭 문제를 야기 시킬수도 있기 때문에 고민을 해봐야 한다.
(GPU로 텍스쳐를 전송하는데 텍스쳐 크기가 크다면 전송하는 시간도 오래걸리며, 프레임 감소 야기)
static은 npc대화나 표준어같은 글자에 쓰이고,
Dynamic 은 예측 불가능한 채팅같은 글자에 쓴다고 한다.
static, dynamic 둘 다 폰트를 만들어서 아틀라스를 생성하는것은 같지만,
미리 만들어 두느냐 실시간으로 만드냐의 차이라고 한다.
마치며
여러 주의할 점들을 알아봤는데, 나에겐 꽤 깊은 내용들이였어서 조금더 정리가 필요할 것 같다.
다른 UI부분도 찾아봐야겠다는 생각이 든다.
'게임 엔진 > Unity' 카테고리의 다른 글
고양이 스낵바 블로그 보고 따라하기 - 커스텀 UI & 자동 코드 생성 (2) | 2024.03.04 |
---|---|
[Unity] Canvas - RenderMode & Scaler (0) | 2023.08.04 |
[Unity]코루틴으로 값을 리턴 받고 싶을 때 (0) | 2023.07.21 |
[유니티 3D 에셋 추천] MEGA Cute Pet Zoo (1) | 2023.05.26 |
[유니티 에셋 추천] Odin Inspector and Serializer (0) | 2023.05.13 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!