유니티/유니티 UGUI 최적화

Unity의 UGUI 최적화 공부하기(Overlay만 다룬다!)(2) - Rebuild와 배칭(Batching)에 대해 자세히 알아보자.

강목근 2024. 6. 28. 04:35

본 포스트는 알쓸유잡을 보며 공부하고 배운 내용을 기록하는 포스트입니다.

https://www.youtube.com/live/1e2mSCS7o1A?si=BAbKuXCTQBdKMMDb

 

이전 포스트를 보지 않았다면 꼭 보는 것을 추천한다!

https://woodroot.tistory.com/21

 

Unity의 UGUI 최적화 공부하기(Overlay만 다룬다!)(1) - Dirty Flag까지, UGUI의 시스템과 구조를 파악하자.

본 포스트는 알쓸유잡을 보며 공부하고 배운 내용을 기록하는 포스트입니다.https://www.youtube.com/live/1e2mSCS7o1A?si=BAbKuXCTQBdKMMDb UI 객체도 메시다!2D게임, 3D 게임 구분 없이 UI도 객체로 취급되어 화면

woodroot.tistory.com

 

RectTransform (cpp 클래스)

Transform을 상속받는 클래스로, 계층구조이다.

그래서 Transform이 겪는 문제들이 동일하게 적용된다. 예를 들어서 부모 객체가 움직일 때 계층구조로 이루어진 하위 객체들도 전부 움직이는 연산을 하여 계층구조가 깊을수록 부하가 큰 문제 같은 것...

 

부모가 바뀌는 Reparenting이 발생하면, TransformParentChanged와 관련된 메시지가 발생한다.

Graphics 클래스의 OnTransformParentChanged 함수

더 상위로 가면 가상함수로 선언되어 있는데, UIBehaviour를 상속받는 Graphics에서는 위와 같이 모든 항목에 대해 Dirty Flag를 전달한다.

즉, 부모를 바꾸는 행동(Reparent)를 할 때는 이것이 생각보다 부하가 크다는 것을 명심하고 해야 한다는 것이다.

 

Rebuild

Graphic 컴포넌트의 레이아웃이나 메시가 다시 계산되는 행위를 뜻한다.

여러 효과 등을 만들 때, 레이아웃이 변경되면 부하가 크고, 단순히 스프라이트 이미지만 바꾼다면 매터리얼만 수정하면 되므로 부하가 상대적으로 적을 것이다.

 

가장 간과하는 것 중 하나는, 페이드 인/아웃 등을 구현하거나 할 때 화면 전체 크기의 이미지를 알파 값 0으로 투명하게 두고 그걸 조절하는 경우가 있는데, 알파가 0이어도 항상 렌더링되고 있는 것이므로 객체의 활성화 비활성화를 조절하여 이런 쓸데없는 연산을 최대한 줄이도록 하자.

Rebuild 과정에서 메시에 변경이 생길 경우 알파가 0이지만 활성화된 객체도 전부 메시 재생성 과정에 포함된다는 것을 명심하자.

 

또한 UI 객체들도 배칭을 통해 드로우 콜을 관리하는데, 배치된 UI의 셰이더가 다르거나 하는 이유로 드로우 콜이 여러번 발생할 수 있다.

 

이러한 Rebuild 과정을 프로파일러를 통해 볼 수 있다. 

프로파일러에서 

  • CanvasUpdate.Prelayout, Layout, PostLayout
  • CanvasUpdate.PreRender, LateRender
  • Canvas.SendWillRenderCanvases

와 같은 것을 보게 된다면, '아 지금 UI에 Rebuild 과정이 진행되는 거구나'라고 판단하면 된다.

CanvasUpdateRegistry 클래스의 멤버 변수, 프로파일러를 위한 스트링 변수가 저장되어 있다.

 

CanvasUpdateRegister 클래스의 PerformUpdate 함수

위 함수를 살펴보면, Layout에 대한 작업을 하는 것으로 보인다.

  • m_PerformingLayoutUpdate - 아마 현재 레이아웃 업데이트를 하고 있음을 알리기 위한 변수로 보인다. 업데이트 구문에 들어가기 전에 true로 바뀌고, 끝나면 false로 바뀐다.
  • m_LayoutRebuildQueue - Rebuild 작업을 해야하는 것들을 컨테이너로 보관하고 있다. 작업을 하기 전에 transfrom 정렬을 해서 업데이트를 진행하게끔 하는 것으로 보인다.
  • 이후 프로파일러에 현재 뭘 진행하고 있는지 전달하고, 순차적으로 큐에서 Rebuild 대상을 꺼내와 Null체크를 한 뒤 Rebuild를 진행한다.
  • 마지막으로 큐를 비우고, 프로파일링도 종료한다.

그럼 저 LayoutRebuildQueue는 어떻게 만들어지는 것일까?

Graphics 클래스의 SetLayoutDirty 함수

해당 함수를 통해 레이아웃에 Dirty Flag를 두고자 하면, MarkLayoutForRebuild 함수로 들어가게 된다.

이제 천천히 함수를 타고 들어가면서 확인하면 되는데, 이미지로 전부 첨부하기도 글이 너무 길어지니 글로 간단히 설명하자면

 

  • RectTransform의 가장 root를 찾아 해당 root를 가지고 MarkLayoutRootForRebuild 함수를 호출한다.
  • TryRegisterCanvasElementForLayoutRebuild 함수를 통해 해당 root를 전달하여, Registry 클래스의 인스턴스를 통해 멤버 함수인 InternalRegisterCanvasElementForLayoutRebuild 함수를 실행한다.
  • 이미 큐에 담겨져 있지 않다면, AddUnique를 통해 독립성을 보장하여 큐에 추가한다.

이 일련의 과정을 통해 LayoutRebuildQueue에 각 Rect Transform이 전달되는 것이다.

Layout 말고도 다른 Rebuild 대상들도 비슷한 과정을 통해 Rebuild 된다. 

이 과정을 이해하고 있다면, 문제가 발생했을 때 해결하거나 최적화하는 것에 도움이 될 것이다.

 

Batch building (Canvas)

결국 모든 객체는 메시를 그려야 하기 때문에 최적화를 위해 Batching이 필요하다.

 

Batching이 뭔데?

GPU Instancing을 통해 완전 동일한 메시를 가지는 빌보드 텍스처를 17000개 동시 렌더링한 결과물, 해당 프로젝트는 블로그에 업로드 되어있다!(https://woodroot.tistory.com/15)

 

Batching은 여러 작업(Batch)를 하나의 Batch로 합쳐서 렌더링하는 것을 말하고, 이는 Draw Call을 줄여 게임을 최적화하는 기법이다.

 

Instancing은 메시 구조가 완전히 같은 객체들에 대해 정점 정보를 한번만 전달하고, 이 동일한 정점 정보를 가지고 GPU에서 Instancing과 관련된 정보를 받아와 해당 정보에 따라 한번의 Draw Call로 여러 객체를 그리는 기법이다.

 

위 사진은, 과거 DirectX12 관련 전공 과목을 수강할 때 과제로 제출했던 게임의 화면이다.

Scene에 약 17,000개의 빌보드를 한번의 Draw Call로 그렸으며, Instancing을 구현하지 못했을 때는 2FPS 정도 나오던 것이 이후에는 최대 144FPS까지 출력이 되었다.

 

아무튼, Instancing과 Batching을 아예 별개로 보는 사람이 있던데, 그렇게 생각하지 않는다.

Batching이 Instancing에 비해 조금 더 일반적인 개념인 거지, 결국 두 방법 모두 Draw Call을 줄이는 것이 목표이다.

 

이 개념을 알고 보면, 결국 Canvas도 Batching을 통해 최적화를 해야 한다는 사실을 알게됐을 것이다.

 

Canvas의 Batching

Batching을 할 때, 매 프레임마다 Batching 데이터를 만드는 게 아니라 Canvas의 어떤 것이라도 Dirty 상태가 될 때에만 Batching 데이터를 만들게 한다.

 

정적 UI와 동적 UI를 구분해서 캔버스를 생성하라고 한 것도 이 이유에서 나온다. 정적인 애들은 Batching 데이터를 한번만 만들어도 변화가 없으니 새로 갱신할 필요가 없지 않은가?

 

Canvas의 Batch Building은 다음과 같은 특징을 지닌다.

  • Batch 데이터는 캔버스가 Dirty로 표시될 때까지(Flag) 캐시되어 재사용된다.
  • 하위 캔버스는 포함하지 않는다.
  • Batch를 계산하기 위해 아래 연산을 거친다.
    • 깊이값 정렬
    • 중첩 확인
    • 공유되는 매터리얼 확인
  • 멀티 스레드로 작동하여 코어 수에 따라 속도 차이가 많이 난다.

그럼 Canvas가 Batching을 할 때 기준이 되는 것은 무엇일까? 어떤 조건을 만족해야 하나의 Batch로 만들어 Draw Call을 수행할까?

  • 동일한 캔버스 안에 존재해야 한다. (렌더링은 캔버스 단위로 실행된다. 당연한 것)
  • 동일한 매터리얼 및 스프라이트 에셋을 사용해야 한다. (3D도 보통 매터리얼 단위로 묶는다.)
  • 동일한 Z 깊이의 RectTransform이어야만 한다. (Z가 바뀌거나 하면 Batch가 깨진다.)
  • 동일한 마스크가 적용되어 있어야 한다. (마스크가 셰이더를 건드는 것이다.)

영상에 나오는 예제는 스프라이트가 전부 분리되어 있어서 Batch가 200개가 넘어가지만, 분리되어 있는 스프라이트더라도 유니티의 Sprite Atlas를 사용하면 논리적으로 묶을 수 있다.

 

https://woodroot.tistory.com/10

 

DirectX12 프로젝트를 제작할 때, 텍스처를 종류에 따라 Sheet로 합쳤던 이유

DirectX12의 텍스처 리소스대학에서 배운 지식을 토대로 DirectX12 프로젝트를 제작하면서, 텍스처 리소스를 상당히 많이 사용했었다. UI, 캐릭터 등 여러 장소에서 사용했는데 이런 텍스처를 사용하

woodroot.tistory.com

위 글에서 DirectX12 프로젝트를 제작할 때 스프라이트 이미지를 Sheet 형태로 합친 이유에 대해 고찰하면서 Sprite Altas에 대해서도 언급하니, 관심 있으면 읽어보면 좋을 것 같다.

 

보통 유니티 작업을 할 때 Batch를 확인하려고 하면, Frame Debug를 많이 사용할 것이다. 3D 작업을 할 때는 Batch가 왜 깨졌는지 알려주는 등 편리하고 좋지만, UI를 디버깅할 때는 알려주지 않는다

그러므로 유니티 Profiler의 UI 모듈을 사용하는 것이 좋다.

 

Sprite Atlas에 분리된 스프라이트를 넣어 패킹할 때의 팁

해당 옵션의 Allow Rotation과 Tight Packing을 해제하면 스프라이트가 손상되지 않고(다른 스프라이트의 침범을 받지 않고) 예쁘게 렌더링 될 것이다.