UE4

[UE4] 언리얼 ui 최적화 기법

Honey Badger 2023. 2. 21. 01:28

[영어원문]

https://topic.alibabacloud.com/a/ui-optimization-tips-in-unreal-engine-4_8_8_10274886.html 

 

UI optimization tips in Unreal Engine 4

At the Unreal Open Day 2017 event, Epic Games developer support engineer Mr. Guo Chunbiao introduced the UI optimization techniques in Unreal Engine 4 to the developers present. The following is a speech record. Hello everyone, I'm Guo Chunbiao, a develope

topic.alibabacloud.com

 

  위 문서에서 에픽게임즈 개발자가 설명한 언리얼 ui 최적화 기법에 대해 정리해보았다.  이 방법들은 모바일 플랫폼에 적용할 수 있을 뿐 아니라 다른 플랫폼의 복잡한 ui 시스템에 대해서도 성능 향상 효과가 크다고 한다.

 

1. UI의 기본 개념

1.1 용어 설명

 

User Widget : User 인터페이스에 해당됩니다.

 

Widget Tree : 각 UserWidget은 트리 구조에 저장됩니다.

 

Panel Widget : Canvas Panel, Grid Panel, Horizontal Box 등과 같은 하위 위젯을 배치하는데 사용되며 렌더링되지 않습니다. 

 

Common Widget : 렌더링에 사용되는 Button, Image, Text 등과 같은 최종 그리기 요소로 생성됩니다.

 

 

 

 

1.2 렌더링 프로세스

 

  < 기본 렌더링 프로세스의 개략도 >

 

 

Game Thread(게임 스레드)에서 슬레이트틱은 프레임당 두 번 위젯 트리를 통과합니다.

 

- Prepass : 트리를 아래에서 위로 탐색하여 각 위젯의 이상적인 크기(원하는 크기)를 계산합니다.

- OnPaint : 트리를 위에서 아래로 탐색하여 렌더링에 필요한 Draw Elements 를 계산합니다. 이 과정에서

 

   Common 위젯의 Type과 매개변수에 따라 해당하는 버텍스 버퍼가 생성되고, Common 위젯의 Render Transform이 Vertex Buffer로 계산되며, 레이어 ID 및 Material과 같은 정보에 따라 Batch Merge가 수행됩니다. 마지막 UserWidget은 하나 이상의 Draw Elements를 생성하고, 각 Draw Element가 Draw Call에 해당하는 렌더링 스레드에 Draw Element를 전달합니다. 

 

렌더 스레드에서 슬레이트 렌더링은 다음 두 단계로 나뉩니다:

- 위젯 렌더 : UI의 RTT를 수행합니다. 만약 Retainer Box를 사용하는 경우, Draw Elements가 Retainer Box의 Retain Target으로 렌더링됩니다. 

- 슬레이트 렌더 : Draw Elements를 백버퍼에 렌더링합니다. Retainer Box가 사용되는 경우, Retainer Box에 해당하는 Texture 리소스가 백버퍼로 렌더링됩니다. 

 

# Retainer Box 란?

 

 

2. 최적화 계획

 

 

2.1 게임 스레드 최적화

 

 

2.1.1 무효화 상자

 

  모든 프레임을 계산하지 않고 Slate Tick 데이터를 캐시하도록 UserWidget을 캡슐화하려면 Invalidation Box를 사용합니다. 작동 방법은 다음과 같습니다.

 

  Invalidation Box 아래에 있는 모든 Prepass 및 OnPaint 계산 결과가 캐시됩니다.  하위 위젯의 렌더링 정보가 변경되면 Invalidation Box에 캐시 정보를 업데이트하기 위해 Prepass 및 OnPaint를 다시 계산하라는 알림이 표시됩니다.

  

 

아래 그림은 영웅 아이콘이 각각 Invalidation Box에 캡슐화된 재사용 가능한 UserWidget인 특별한 경우를 보여줍니다. 전체 영웅 리스트는 Scroll Box입니다. Scroll Box를 위아래로 슬라이드하면 영웅 아이콘의 UserWidget에 해당하는 Transform 정보또한 변경됩니다. 

  이때 아래와 같이 Invalidation Box에 해당하는 Cache Relative Transforms를 선택할 수 있습니다.

   그런 다음 UserWidget의 위치가 변경되면 엔진은 모든 Draw Element(즉, Vertex Buffer)를 업데이트 하지 않고, 위치 변경을 반영하도록 셰이더 매개변수(View Projection Matrix)를 수정합니다. 이 방법은 위치 변경에만 적용됩니다. 확대/축소를 할 경우에는 Draw Element를 다시 계산해야 합니다. 캐시 Relative Transform은 게임 스레드에 소량의 추가 계산을 추가하여 필요할 때만 확인되도록 합니다.

 

  위젯의 렌더링 정보가 변경되면 Vertex Buffer를 다시 캐시하기 위해 위젯이 위치한 Invalidation Box에 알립니다. 복잡한 사용자 위젯에서 Invalidation Box는 종종 전체 위젯 트리를 캐시하여 고성능 오버헤드를 발생시킵니다. 이 문제를 해결하는 두 가지 방법이 있습니다.

 

1. Invalidation Box 를 분할하고 위젯이 자주 변경되는지 여부에 따라 다른 Invalidation Box로 분할하는 방법. 레이아웃 상의 이유로 인해 여러 Invalidation Box를 분할하는 것이 편리하지 않은 경우가 있습니다. 그럴땐 두번째 방법을 사용하세요.

 

2. 위젯을 Is Volatile로 설정하면 캐시될 때 위쪽 Invalidation Box에서 이 위젯을 제외할 수 있습니다. Tick 은 Prepass 및

OnPaint를 계산하지만 전체 위젯 트리 캐시는 영향을 받지 않습니다. 

 

 

 

   위 그림의 레벨업 아이콘은 평소에는 숨겨져 있으며 캐릭터가 업그레이드 될 때 표시됩니다. 레벨업 Anim은 위젯의 위치를 변경하여 애니메이션 효과를 실행합니다. 이 이미지를 렌더링 할 때 위치가 변경되었기 때문에 Invalidation Box가 프레임마다 전체 위젯 트리의 캐시를 다시 계산해야 하고, 이렇게 되면 성능이 상대적으로 낮아지게 됩니다. 성능을 향상시키기 위해 이 위젯을 Volatile(휘발성)으로 설정할 수 있습니다. 

 

   에디터의 IsVolatile 옵션을 사용하여 Volatile을 명시적으로 설정하여 Invalidation Box의 성능을 향상시킬 수 있습니다. 때때로 위젯 바인딩은 위젯을 휘발성으로 암시적으로 표시하여, 이 위젯이 모든 프레임을 Tick하게 하고 성능이 저하되는 경우가 있습니다. 

 

- 각 위젯에는 Draw Element(Vertex Buffer)에 영향을 미치는 특성이 ComputeVolatility 함수에 나열됩니다.  

- 텍스트 위젯은  Draw Element의 Properties(속성)에 영향을 미칩니다:

- Progress bar Widget은 Draw Element에 영향을 줍니다.

 

Draw Element에 영향을 미치는 특성에 위젯 바인딩을 사용하면 엔진이 모든 프레임을 체크하여 특성(Attribute)이 변경되었는지 확인해, Draw Element를 업데이트해야 하는지 여부를 경정할 수 있으므로 위젯 바인딩을 사용하지 않는 것이 좋습니다. 

 

Invalidation Box와 Volatile들이 올바르게 설정되어 있는지 확인하려면 Slate.InvalidationDebugging을 사용하면 됩니다.

 

- 녹색 와이어프레임 : Invalidation Box Cache를 사용하는 위젯.

- 파란색 와이어 프레임 : Relative Transform이 체크되어있는 Invalidation Box Cache.

- 점선 상자 : Volatile이 지정된 위젯입니다.

- 빨간색 와이어 프레임 : Invalidation Box를 사용하지 않는 위젯.

 

   Slate.AlwaysInvalidate 커맨드는 InvalidationBox가 모든 프레임에서 캐시를 업데이트하도록 강제할 수 있으며, 이 명령을 사용하여 갑작스러운 고착 여부를 테스트할 수 있습니다. UserWidget이 너무 복잡한 경우 여러개의 Invalidation Box로 분할하고 업데이트 빈도에 따라 위젯을 다른 Invalidation Box에 넣을 수 있습니다.

 

 

 

 

2.1.2 가시성(Visibility)

 

Widget Visibility에는 5가지 유형이 있습니다:

 

- Visible : Visible 및 Clickable.

- HitTestInvisible : 히트 테스트 보이지 않음.

- SelfHitTextInvisible : Visible, 현재 위젯은 클릭할 수 없으며 하위 위젯에는 영향을 주지 않습니다.

- Hidden : Invisible, 레이아웃 스페이스 사용.

- Collapsed(접힘): Invisible, 레이아웃 스페이스를 차지하지 않음.

 

  많은 위젯의 Default property는 Visible이며, HitTextInvisible과 SelfHitTestInvisible 속성은 수동으로 설정해야 합니다. 다수의 위젯을 Visible로 설정하면 응답을 클릭할 때 엔진 효율이 크게 떨어져 게임 스레드의 오버헤드도 커진다. 

 

Colladed는 레이아웃 공간을 차지하지 않기 때문에 숨긴 후 Prepass 계산을 수행하지 않으며, Hidden보다 성능이 우수합니다. 

 

Widget Reflector를 사용하면 잘못 설정된 Visibility property가 있는지 확인할 수 있습니다.

 

 

 

 

2.1.3 위젯 바인딩

 

   Volatile을 분석할 때 위젯 바인딩으로 인해 Volatile이 UI성능을 저하시킨다는 점이 언급되었습니다. 또한 위젯 바인딩은 프레임 Tick마다 실행되므로 성능이 상대적으로 떨어집니다. 프로젝트에서 이 함수를 사용하는 것이 아니라 C++(또는 Blueprint)에서 함수를 호출하여 값을 전달하는 것이 좋습니다.

 

  RemoveFromViewprot / AddToViewport는 UserWidget을 삭제하고 재구성합니다. Collapsed(축소) / SelfHitTestIncisible 속성을 사용하여 더 나은 성능을 얻을 수 있습니다. 

 

또한 Blueprint Tick의 복잡한 계산 로직을 모바일 플랫폼의 C++코드로 이동하는 것이 좋습니다.

 

 

 

 

2.2 렌더링 스레드 최적화

 

2.2.1 배치 Merging

 

  GPU의 발달로 Draw Call의 수가 성능에 미치는 영향은 점점 작아지고 있습니다. Draw Call 수를 줄인다고 FPS가 증가하지 않는 경우가 많습니다. 그러나 Draw Call을 줄이면 GPU에 대한 API 호출을 줄일 수 있어 모바일 측면에서 휴대폰 발열을 제어하는 데 도움이 됩니다. 

 

A. Panel Widget

  4.15 이전 버전의 엔진에서는 Canvas Panel이 배치 머징을 지원하지 않기때문에 캔버스 패널을 사용하지 않는 것이 좋습니다. Grid Panel, Vertical Box, Horizontal Box 및 배치 머징을 지원하는 다른 컨테이너를 사용해보십시오. 

 

4.15 버전에서 Canvas Panel의 배치 머징에 대한 지원이 추가되었습니다. 여는 방법은 프로젝트 설정: "엔진->Slate Settings-> Constraint Canvas-> Explicit Canvas Child ZOrder"에 있습니다. 그런 다음 Canvas Panel의 하위 위젯의 ZOrder속성을 설정할 수 있습니다. 동일한 ZOrder(동일한 렌더링 매개변수)를 가진 배치는 배치를 머지합니다. Canvas Panel은 Grid Panel과 Horizontal Box에 비해 추가 레이아웃 계산이 없으며, OnPaint 효율이 약간 높습니다.(게임 스레드)

 

B. Merge Textures

 

 # UE4의 Sprite는 머지된 텍스쳐의 편집 및 사용을 편리하게 지원합니다. 

  코드 로직상으로 independent 텍스처와 merged 텍스처 사이를 전환해야 하는 경우 Manager Class에서 independent 텍스처(UTexture2D)와 merged 텍스처 리소스(UPaperSprite)를 초기화하고 FslateBrush를 생성한 다음 SetResourceObject를 통해 리소스를 FslateBrush로 설정합니다. 그러면 스위치 변수를 통해 UImage::SetBrush로 전달되는 파라미터를 제어할 수 있습니다. 

 

  프로젝트 후반부에서 UserWidget의 모든 텍스처를 Merged텍스처로 대체해야 한다면 매우 지루한 작업이 될 것입니다. 에픽게임즈의 Dmitry Dyomin은 쉽고 빠른 교체를 위한 아이디어를 제공하고 있습니다. 

 

- 먼저 Commandlet을 구현합니다:

다음 커멘드를 사용하여 Commandlet을 실행할 수 있습니다:

 

Commandlet의 특정 함수들 : 모든 WidgetBluprint 에셋을 이동하고, AssetRegistry를 사용하여 Asset을 로드하고, UImage 및 UBorder에서 사용하는 텍스처를 확인하고, 명명 규칙에 따라 해당하는 스프라이트 Asset이 있는지 확인합니다. AssetRegistry를 사용하여 Texture를 Sprite로 바꾼 다음 마지막으로 Widget Blueprint Asset을 저장합니다. 

 

 

2.2.2 Retainer Box

 

  배치를 머지하고 텍스처를 머지하면 UI에서 DrawCall 수가 상대적으로 낮은 수준으로 줄어들 수 있지만 여전히 픽셀 fill rate가 높습니다. 

 

  대부분의 경우 UI를 모든 프레임에 렌더링 할 필요가 없으므로 렌더링 결과를 RetainerBox(보관함)을 통해 캐시하고 몇 프레임마다 업데이트할 수 있습니다. Retainer Box의 원리는 렌더링 타깃에서 UI 렌더링을 캐시한 다음 렌더링 타깃을 화면으로 렌더링하는 것입니다. 

 

  아래 그림에서는 메인 인터페이스의 UI를 4개의 Retainer Box로 나누어 3프레임마다 업데이트하여 렌더링합니다.

 

 

   렌더링 효율을 향상시키고 비디오 메모리 사용을 줄이려면, Retainer Box 영역을 가능한 작게 만들어야 합니다. 일반적으로 Retainer Box에는 UserWidget의 배경 이미지가 포함되어야 합니다. 배경 이미지는 픽셀 fill rate가 크기 때문입니다. 

 

  Retainer Box는 각 UserWidget 인스턴스에 대해 렌더 타깃을 작성하므로 코드를 변경하지 않고 재사용된 UserWidget은 Retainer Box을 사용하지 않아야 합니다. 예를 들어, 아래 그림에서 Scroll Box Item이 있는 UserWidget이 아닌 Scroll Box가 위치해있는 UserWidget에 대해 Retainer Box를 만들어야 합니다. 

 

 

  다음 그림은 또 다른 상황을 보여줍니다. UserWidget B_HeroIcon은 HEROS 및 SOCIAL과 같은 여러 주요 인터페이스에서 반복적으로 사용됩니다. 배틀 브레이커는 UI가 많은 모바일 게임이기 때문에 비디오 메모리를 많이 차지할 메인 인터페이스의 모두에 Retainer Box를 할당하기가 어렵습니다. 물론, 우리는 각 B_HeroIcon에 대해 Retainer Box를 만들고 싶지는 않습니다. 

 

  이때 코드를 확장하면 더 나은 Retainer Box 효과를 얻을 수 있습니다. 화면에 동시에 나타나는 B_HeroIcon의 상한이 20이라는 것을 알고 있다고 가정하면, 20개의 렌더 타깃을 포함하는 렌더 타깃 풀을 만들어 동일한 렌더 타깃을 공유하는 서로 다른 Retainer Box를 만들 수 있습니다. 

 

  Retainer Box는 추가적인 비디오 메모리를 소비하게 되므로 사용량을 조절하고 성능 향상이 가장 큰 UserWidget에 우선순위를 부여해야 합니다. 하나는 기본 인터페이스의 UserWidget이고, 다른 하나는 렌더 타깃을 공유한 후 자주 사용하는 UserWidget입니다. 

 

  Retainer Box를 사용하면 렌더링 스레드의 효율성이 향상될 뿐만 아니라 게임 스레드의 Tick도 그에 따라 몇 프레임마다 실행됩니다. Retainer Box에 클릭 가능한 Widget이 포함된 경우 엔진이 클릭 테스트 영역을 Retainer Box에 매핑하도록 Retainer Box를 Visible로 설정해야 합니다. 

 

  Continuous Expression(3D 캐릭터, Material 이펙트 등)은 Retainer Box와 분리할 수 있지만 픽셀 Fill Rate에 주의해야 특수효과 설계 측면에서도 해결할 수 있습니다. 

 

  Invalidation Box를 Retainer Box위에 놓는 것은 의미가 없습니다. 일반적인 방법은 Invalidation Box를 Retainer Box 아래에 놓는 것입니다. 

 

  Retainer Box의 Phase Count를 전체적으로 고려해야 합니다. 예를 들어, 다음 그림은 Retainer Box가 3프레임마다 업데이트 되고 프레임 0에서 업데이트 되는 것을 보여줍니다. :

다음 그림은 5프레임마다 업데이트되고 두 번째 프레임에서 업데이트됨을 보여줍니다:

그런 다음 15프레임마다 프레임 내에서 이 두개의 RetainerBox가 동시에 업데이트 되어 프레임 수가 감소합니다.

 

 

 

 

2.2.3 Event-driven Retainer Box

 

  현재 Retainer Box는 몇 프레임마다 업데이트되도록 지정되어야 하지만, UserWidget은 고정된 빈도로 업데이트할 필요가 없고 사용자가 작업할때만 업데이트해도 되는 경우가 있습니다.(그리고 그 작업은 자주 안일어나는 것이어야 합니다.)이 경우, 이벤트 중심 접근 방식을 지원하도록 Retainer Box를 확장할 수 있습니다. 

 

  구현 아이디어는 URetainerBox와 SRetainerWidget을 상속하고 PaintRetainedContent(4.16 이전 함수 이름은 OnTickRetainers)에서 이벤트가 업데이트를 트리거하는지 여부를 결정하는 것입니다. 업데이트가 필요한 경우 상위 클래스의 PaintRetainedContent를 Call하고 그게 아닌 경우 return합니다.

 

 

 

2.2.4 Swithching materials

 

  UE4는 풍부한 Material 이펙트들을 제공합니다. Low-end Machine에서는 이러한 효과를 끄거나 성능을 향상시키기 위해 낮은 퀄리티 Material로 전환하는 것을 고려할 수 있습니다. 

 

  엔진에서 제공하는 DYNAMIC_MULTICAST  프레임워크를 사용하여 영향을 받는 모든 위젯을 스위치 변수에 바인딩하여 전체적인 스위칭을 수행할 수 있습니다. 

 

 

 

 

2.3 기타 최적화

 

2.3.1 C++ 개발

 

   UI 애니메이션의 디자인적인 이유를 제외하고는 이 저장구조는 C++에서 구현할 수 없으며, 다른 UI 기능은 C++에서 구현할 수 있습니다.  그 방법은 다음과 같습니다.

 

1. UUserWidget에서 상속된 C++클래스 UWExpHeroIcon을 구현합니다.

2. Reparent Blueprint를 사용하여 부모 클래스를 UWExpHeroIcon으로 수정합니다.

3. 에디터에서 노출되어야 하는 변수와 유형을 찾습니다.

4. BindWidget 변수를 C++로 선언합니다. 그러면 엔진은 자동으로 데이터를 연결합니다.

 

 

 

2.3.2 Manager Class

 

  프로젝트에서 모든 UserWidget과 브러시 및 글꼴과 같은 모든 UI 리소스를 관리하는 Manager Class를 만드는 것이 좋습니다. Manager 클래스는 C++ 또는 Blueprint 형식으로 제작할 수 있습니다.

 

 

2.3.3 Free texture memory (여유 텍스처 메모리)

 

  텍스처 메모리를 Releasing하기 위한 한가지 전제는, 텍스처(아래 그림의 이미지 항목)를 설정하는 것이 아니라 프로그램을 통해 텍스처를 수동으로 텍스처를 로드,설정,파괴하는 것입니다. 에디터에서 텍스처를 설정하지 않으면 CDO(Class Default Object)에서 이 텍스처 객체를 참조하는 일을 피할 수 있습니다. CDO의 참조는 SharedPtr의 참조 카운트를 최소 1로 만들며 응용 프로그램을 종료하기 전까지 삭제되지 않습니다. 

 

  에디터에서 이미지 속성이 설정되어 있고 텍스처를 삭제하려는 경우, Cook Stage에서 UImage와 UTexture 사이의 참조 관계를 제거하여 이 UserWidget의 CDO가 UTexture를 참조하지 않도록 해야 합니다. 

Cook 단계에서 기존 관계를 제거하는 코드는 다음과 같습니다:

 

 

 

텍스처를 로드하는 코드는 다음과 같습니다:

 

 

텍스처를 해제하는 코드는 다음과 같습니다:

 

 

 

 

2.3.4 3D RTT 최적화

 

  기본적으로 SceneCaptureConponent2D는 모든 프레임에 tick하며, 일반적으로 모든 프레임에서 이미지 업데이트를 취소할 수 있습니다:

  애니메이션의 업데이트 빈도는 초당 30회로 충분하므로, 블루프린트를 통해 SceneCaptureConponent2D의 Tick 간격 설정을 설정할 수 있습니다:

  그런 다음 블루프린트에서 Capture를 수동으로 호출합니다:

또한 성능 향상을 위해서는 SceneCaptureComponent2D의 Render Target 크기가 너무 크면 안됩니다.

 

 

 

2.3.5 새로운 기능

 

  우리는 배틀 브레이커에 두개의 디버그 커멘드를 추가했는데, 이 커멘드는 버전 4.17 게임 인터페이스에서 머지될 수 있습니다: 

- 픽셀 오버드로우를 보려면 Slate.ShowOverdraw 를 사용하세요.

- Batch를 보려면 Slate.ShowBatching을 사용하세요.

 

 

 

 

3. 효과 테스트

 

  우리는 최적화 효과를 테스트하기 위해 테스트 프로젝트를 만들었습니다. 아래 그림의 UI에는 800개 이상의 위젯이 있습니다:

 

  애플리케이션이 모바일 HDR을 개방한 이후 GPU에 병목 현상이 발생해 FPS가 크게 개선되지 않았습니다만, Invalidation Box를 연 후 Slate Tick 시간이 크게 단축되었습니다. 

  

  다음 그림은 Invalidation Box, Retain Box 및 이벤트 구동식 Retain Box의 성능 매개 변수를 켠 후 쉽게 비교할 수 있습니다. (렌더링 스레드의 개선으로 FPS가 크게 향상됨을 알 수 있습니다.)

 

 

 

 

 

4. Summary

 

  대부분의 UI 최적화 작업(Invalidation Box, Retain Box)는 프로젝트 후반(기본 UI 개발 완료 후)에 수행됩니다. UE4는 풍부한 기능과 디버깅 도구를 제공하며, 이러한 기능을 마스터하면 개발자가 고성능 UI를 달성하는 데 도움이 될 수 있습니다.