NGUI 최적화의 핵심은 정적인 UI 들과 동적인 UI 들을 나누는 것이다. 나누는 기준은 패널이다. 예를 들어 다음과 같은 하이어라키를 생각해볼 수 있다.


Panel1

정적인 UI들

Panel2

동적인 UI들


이렇게 하는 이유는 UI 가 갱신될 때마다 UI 가 속한 패널의 지오매트리가 재구성되기 때문이다. 재구성 작업에 많은 양의 가비지가 발생한다. 여기서 주의해야할 점은 위치 이동같은 기본 변환은 '갱신' 에 포함되지 않는다는 것이다. 지오매트리가 변해야 하는 작업들, 예를 들어 SetActive, UILabel 의 text 변경 등이 갱신에 포함된다.


패널의 UI 들중 하나라도 갱신이 되면 패널의 모든 UI 들에 대한 지오매트리 재구성 작업이 수행된다. 따라서 제일 최악의 시나리오는 패널을 루트에 단 하나 만들어두는 것이다. 이러면 UI 의 변경은 곧 씬의 모든 UI 들의 갱신이라는 결과로 작용한다.


동적인 UI 들 역시 하나의 패널로 몰아넣는건 같은 이유로 피해야 한다. 그렇다고 해서 패널의 개수를 너무 많이 늘려서는 안되는데, 패널 단위로 드로우콜이 분리되기 때문에 렌더링 성능은 낮아진다. 따라서 성능 측정을 결과로 패널의 개수를 조절할 필요가 있다.

Posted by 카코데몬
,

게임의 리소스중 대부분은 텍스처이다. 따라서 텍스처 최적화는 게임 성능에 가장 큰 영향을 미친다.


아래는 텍스처 최적화에서 자주 논의되는 개념들을 나열한 것이다.


1. POT

POT 은 Power Of Two 의 약어로, 2의 거듭제곱을 말한다. 많은 하드웨어에서 2의 거듭제곱 크기인 텍스처만을 사용할 수 있다. 하지만 게임에서는 2의 거듭제곱이 아닌 텍스처들도 분명 존재하고, 유니티는 이를 사용할 수 있도록 하기 위해 내부적으로 작업을 수행한다. 이 작업은 원본 텍스처보다 큰 2의 거듭제곱 텍스처를 만들고 원본 크기만큼을 복사, uv 를 조절하는 작업이다. 따라서 메모리 비용이 증가한다. 원본 크기를 제외한 영역은 재활용할 수 없으므로 낭비이다. 따라서 가능한 2의 거듭제곱 텍스처를 사용해야만 한다.


2. Compression

1024 x 1024 크기의 32비트 텍스처는 4MB의 메모리를 차지한다. 안드로이드를 기준으로, 이 텍스처를 ETC1 포맷으로 압축하면 512KB 로 정확히 8배 감소한다. 메모리 최적화 면에서 압축이 가장 높은 효율을 보인다. 하지만 압축에는 다음과 같은 제약사항이 있다.

- 대부분 POT 크기의 텍스처만 지원한다

- iOS 에서 사용하는 압축 방식인 PVRTC 같이 정방 크기의 텍스처만 지원하는 경우도 있다

- 알파를 지원하지 않는 경우도 있다. ETC1 같은 포맷은 지원하지 않는다. 이는 아래의 Alpha Channel Split 을 참고한다

압축은 대부분의 하드웨어에서 지원하고 원본과 비교했을 때 품질 저하가 적기 때문에 적극 권장되는 방법이다.


3. Color Channel Bits 낮추기

압축 포맷을 사용하지 않을 경우 RGBA 32비트 혹은 RGB 24비트 포맷이 많이 사용된다. 둘의 차이는 알파 채널의 존재 여부이다. 각 채널당 8비트 를 할당한 포맷으로 품질면에서 제일 좋은 반면 메모리는 가장 많이 차지한다. 채널에 할당되는 비트수를 낮추는 것으로 품질은 떨어뜨리면서 차지하는 메모리를 낮출 수 있다. RGBA 32비트, RGB 24비트 모두 16비트로 낮출 수 있는데 RGBA 32비트의 경우 각 채널당 4비트, RGBA 24비트의 경우 R채널은 5비트, G채널은 6비트, B채널은 5비트가 된다. 그라데이션같이 색의 분포가 넓은 경우 비트를 낮췄을 때 디더링( Dithering ) 현상이 심하게 나타나는데 이를 줄이기 위해 포토샵 필터 등의 외부 툴이 필요한 경우도 있다.

비트를 낮추는 방법은 품질을 크게 떨어뜨리므로 주의해서 적용해야 한다.


4. Alpha Channel Split

알파 채널이 존재하는 텍스처를 알파를 지원하지 않는 방식으로 압축하고자 할 때 알파 채널을 분리하는 방법을 사용할 수 있다. UI 같이 대부분 알파 채널을 포함하는 곳에 자주 적용되는 방법이다. 개념은 간단한데 원본 텍스처에서 알파 채널을 분리하여 별도의 텍스처로 만들고 각각을 압축, 렌더링할 때 두 장의 텍스처를 읽어서 연산해주는 것이다. 텍스처가 두 장으로 늘어나서 비효율적일 것 같으나 RGBA 32비트 텍스처 기준으로 여전히 4배 적은 크기를 차지한다. 유니티4 까지는 직접 쉐이더를 구현해야 하고 유니티5 부터는 텍스처 타입이 Legacy GUI 혹은 Sprite 인 경우에 아래처럼 지원해준다.



5. Max Size

2048 크기를 1024로, 1024크기를 512로 낮추는 것처럼 텍스처 크기를 한 단계 내리는 것으로 메모리 크기를 4배 줄일 수 있다.


6. Mipmap

밉맵은 더 적은 bandwidth 로 텍스처를 렌더링할 수 있도록 해서 렌더링 성능과 품질을 높여주는 반면 원본 텍스처가 차지하는 메모리보다 약 33% 증가한 메모리를 차지한다. 따라서 필요치 않은 경우 밉맵을 꺼서 메모리를 아낄 수 있다. 또한 밉맵 텍스처들은 빌드에 포함되므로 비활성화시 파일 크기가 줄어드는 것도 확인할 수 있다.


7. Load From File

빌드 크기를 최소화하는 방법 중 jpg 혹은 png 같은 원본 이미지 파일을 실행 중에 동적으로 로드하는 방법이 있다. 압축 포맷이 지정된 텍스처라 하더라도 jpg 보다는 훨씬 큰 경우가 많다. 따라서 원본 이미지 파일을 bytes 형태로 Resources 하위 폴더에 넣어두고 실행 중에 Resources.Load 로 읽어서 텍스처로 만들게 되면 메모리는 증가하겠지만 빌드 크기를 줄일 수 있다.

Posted by 카코데몬
,

오브젝트를 렌더링할 때 기본적으로 Renderer 컴포넌트의 sharedMaterial 이 사용된다. 이는 에디터에서 오브젝트에 설정해둔 재질과 정확히 일치한다. 즉 sharedMaterial 은 매트리얼 에셋과 대응한다. 이를 확인하는 방법은 실행 중에 sharedMaterial 의 속성을 변경해보는 것이다. 그럼 에셋이 바뀌어 있는 것을 확인할 수 있다. 심지어 종료해도 변경 상태가 남아있게 되므로 주의..

기본적으로, 매트리얼에 수정을 가하지 않으면 sharedMaterial 을 참조하여 렌더링된다. 그리고 이는 배치 렌더링의 핵심이다. 동일한 재질을 지녀야 한번에 렌더링할 수 있기 때문.

그러나 상당히 많은 경우에서 특정 오브젝트의 재질 속성을 변경할 필요가 생긴다( 일시적으로라도 ). 컬러값, 빛의 세기, 텍스처 등.. 이러한 이유 때문에 sharedMaterial 과 함께 material 이 존재한다. 유니티는 sharedMaterial 이 아닌 material 에 접근하는 순간 sharedMaterial 의 사본을 생성하여 material 에 할당해준다. material 은 Renderer 에 존재하며 다시 Renderer 는 오브젝트가 소유하므로 즉 오브젝트에 할당된 재질이다. 따라서 다른 오브젝에 영향을 주지 않으면서 재질 속성을 달리할 수가 있게 된다. 이렇게 사본이 생성되면 에디터상에서 오브젝트의 재질을 확인했을 때 (Instance) 라는게 붙는다는 것을 알 수 있다.

사본이 생성되는 순간 이 오브젝트는 배치 렌더링을 포기하게 되는 것이다. 한번 material 을 사용하게 되면 해당 오브젝트는 다시는 배치 렌더링을 할 수 없게 된다. 그리고 material 의 값을 변경하지 않더라도 참조하는 순간 사본이 생성된다( material, material.color, 이하 material 을 참조하는 순간.. ).

다행히, 일시적으로 재질 속성을 변경하기 위한 좋은 대안이 있는데 바로 재질 속성 블럭( Material Property Block ) 이다. 방법부터 정리하면 아래와 같다.

 

// 재질 속성 블럭을 생성한다. 생성은 한 번만 하면 되므로 Awake 같은 곳에서 호출하자
MaterialPropertyBlock mpb = new MaterialPropertyBlock();

// 방금 생성한  객체에 변경하길 원하는 재질 속성들을 설정한다
mpb.SetColor(Shader.PropertyToID("_Color"), Color.black);

// 렌더러에 객체를 설정해준다( 만약 이후에 재질 속성 블럭을 수정했다면 다시 아래 코드를 호출해줄 것 )
renderer.SetPropertyBlock(materialProp);

// 재질 속성을 원래대로 돌리는건 아래처럼 해주면 된다
renderer.SetPropertyBlock(null);

 

위처럼 재질 속성 블럭을 이용하면 사본이 생성되지 않으며 객체가 set 되어 있는 동안에만 배칭되지 않는다.

'유니티 - 최적화' 카테고리의 다른 글

NGUI 최적화의 핵심  (1) 2016.01.14
텍스처 최적화  (0) 2016.01.13
최적화의 기본  (0) 2016.01.08
Update 와 FixedUpdate  (0) 2016.01.08
Application.Integrate Assets in Background 에서 렉이 발생하는 경우  (0) 2016.01.08
Posted by 카코데몬
,

최적화는 비효율적인 부분을 좀 더 효율적으로 바꾸는 작업이다.

최적화에 있어 가장 중요한 것은 최적화하려는 대상이 정말 비효율적이냐는 것이다. 비효율적인지를 판단하는데에 자신의 경험도 중요하지만 프로파일러같은 툴을 이용하는 것이 낫다. 섵부른 최적화는 대상의 복잡도만 증가시키고 그다지 효율적이진 못한 결과를 낳게 된다. 툴을 이용하면 이런 사태를 줄일 수 있다.

최적화는 많은 경우에서 트레이드 오프 특성이 나타난다. 예를 들어 드로우콜 비용을 줄이기 위해 Static Batching 을 사용했다고 해보자. 이는 렌더링 속도를 높이는 대신 메모리를 낭비하는 결과를 낳는다. 최적화가 다른 부분을 비효율적으로 바꿀 수 있는지, 그렇다 했을 때 어느 쪽이 더 가치가 있는지를 판단해야 한다.

클라이언트 사이드에서 최적화는 단지 프로그래머만의 몫은 아니다. 최적화에 관한 가이드라인을 정하고 프로그래머, 아티스트, 디자이너 등의 여러 직군들이 이를 공유하고 숙지하며 작업에 임해야 한다. 최소 사양 하드웨어가 무엇이고 텍스처 크기 제한은 얼마이며 몬스터는 최대 몇 명이 등장해야 하는 지 등에 대한 기준을 정한 뒤 작업을 진행하도록 하자.

프로파일링은 자주 하는 것이 좋다. 프로파일링을 자주 하면 최적화 하려는 대상이 언제, 어떻게 비효율적으로 되었는지를 보다 쉽게 유추할 수 있다.

Posted by 카코데몬
,

한 프레임에 Update 함수는 단 한번 호출되지만 FixedUpdate 는 한번 이상이 호출될 수 있다.

따라서 FixedUpdate 에서 오래 걸리는 연산을 최소화할 필요가 있다. 물리에 영향을 주는 요소만 FixedUpdate 함수에서 수정을 하고 나머지 로직은 최대한 Update 함수에서 처리하도록 하자.

Posted by 카코데몬
,

가끔 프로파일링 중에 Loading.UpdatePreloading 하위의 Application.Integrate Assets in Background 에서 렉이 발생하는 경우가 있다.

이 경우 Resources.UnloadUnusedAssets() 함수를 호출했는지 체크해보자. 리소스가 많이 로드되어 있는 상태에서 이 함수를 호출했을 때 이런 렉이 발생할 수 있다. 만약 그렇다면 함수 호출 시점을 바꾸거나 더 적은 리소스를 로드하도록 변경할 필요가 있다.

'유니티 - 최적화' 카테고리의 다른 글

NGUI 최적화의 핵심  (1) 2016.01.14
텍스처 최적화  (0) 2016.01.13
material 과 sharedMaterial, 그리고 Material Property Block  (3) 2016.01.08
최적화의 기본  (0) 2016.01.08
Update 와 FixedUpdate  (0) 2016.01.08
Posted by 카코데몬
,