* [유니티 그래픽스 최적화] 시리즈는 비엘북스 <유니티 그래픽스 최적화 스타트업>을 읽고 스스로 이해한 바를 정리하는 시간을 갖고자 작성하는 시리즈입니다. 책을 그대로 옮겨쓴 것이 아니고 이해한 대로 재 서술하는 것이기 때문에 틀린 점이 있을 수 있습니다. 정확히 알고 계시는 분은 댓글로 알려주시면 감사하겠습니다 !
01. Draw Call
드로우콜은 수많은 병목 중 하나의 원인일 뿐,, 반드시 병목이 드로우콜에서 일어난다고 단정지어서는 안됨.
드로우콜이 강조되는 이유는 대부분 병목의 원인이 드로우콜에 있기 때문.
드로우 콜이란? CPU가 GPU에게 이거 그려! 하고 명령을 호출하는 것.
더 자세하게 ?
한 프레임의 렌더링은 매 오브젝트를 순차적으로 그려주면서 오브젝트를 다 그리면 화면에 보여지게 되는 것.
오브젝트를 화면에 렌더링하기 전에 우선 해당 오브젝트가 렌더링 대상에 포함되는지 체크한다. 현재 프레임 상에서 해당 오브젝트가 카메라의 시야 밖에 있다면 안 그려도 되는 것이므로 렌더링 대상에서 제외한다. 이런 검사 과정을 Culling이라고 한다. 컬링을 거친 오브젝트가 렌더링되려면 CPU로부터 GPU에게 정보가 전달되어야한다. (02에서 설명했던 내용이다)
이렇게 한 프레임 마다 오브젝트를 하나하나 그릴 때 마다 정보들이 CPU에서 GPU로 전달되고 그려진다. 이 과정을 반복해서 렌더링한 후 모든 오브젝트들이 다 그려지면 한 프레임이 끝나고 화면에 출력한다. 이때 CPU가 GPU에게 렌더링하라고 명령을 보내는 것을 Draw Call 이라고 한다.
CPU와 GPU가 어떻게 협업하는가
GPU가 Mesh를 렌더링하려면 GPU 메모리에서 데이터를 읽어와야한다. 그러려면 그 전에 GPU 메모리에 데이터가 있어야한다는 의미이다.
그래서 렌더링을 수행하기 전에 데이터 로딩이 이루어지면서 Mesh 정보가 GPU 메모리가 담기는 것.
CPU가 HDD, SDD, SD 카드 등의 스토리지에서 파일을 읽어들이고 데이터를 파싱하여 CPU 메모리에 데이터를 올린다.
그 후 CPU 메모리의 데이터를 GPU 메모리로 복사하는 과정을 거친다. (일반적으로 GPU에서 CPU의 메모리를 바로 접근할 수 없기 때문)
이렇게 데이터를 메모리에 전달하는 과정을 매 프레임마다 수행하게 되면 성능 하락이 생길 수 밖에 없다.
따라서 로딩 시점에 메모리에 데이터를 올려두고 씬 전환 시점 같은 때에 데이터를 해제한다. 즉, 게임이 실행되는 동안에는 데이터가 계속 메모리에 상주하게 된다. 텍스쳐, 쉐이더 등 렌더링에 필요한 데이터 모두 GPU 메모리에 존재하고 있어야 한다.
Render State와 DP Call
렌더링 루프를 돌면서 어떤 오브젝트를 렌더링해야하는 시점이 오면 GPU에 어떤 텍스쳐, 버텍스, 쉐이더 등을 사용해야할지 전달해줘야하는데 이런 정보들은 한 번의 명령으로 처리되는 것이 아니라 순서대로 일일히 알려줘야한다. 그렇다고 일일히 하나씩 알려주는 것은 비효율적이니 GPU는 해당 정보를 담는 테이블을 가지고 있다. 이런 테이블을 Render State (렌더 상태)라고 부른다. 렌더 상태의 테이블 정보들은 GPU 메모리의 어느 곳에 필요한 데이터가 있는지 데이터의 주소를 저장해둔다. (like 인덱스 파일)
CPU가 렌더 상태를 변경하는 명령을 보내면, GPU는 렌더 상태에다가 오브젝트를 그리기 위한 정보를 저장한다.
CPU는 명령을 보내고 마지막으로 GPU에게 메시를 그리라는 명령을 보낸다. 이 명령을 Draw Primitive Call, DP Call이라고 한다. GPU는 DP Call을 받으면 받아두었던 렌더 상태의 정보들을 기반으로 메시를 렌더링한다.
메시를 렌더링하고 난 뒤, CPU는 이제 또 다른 오브젝트를 렌더링하기 위해 상태 정보를 변경하는 명령을 보낸다. 이 때 바뀔 필요가 없는 상태의 명령은 보내지 않는다. 이렇게 렌더 상태를 변경해주고 마지막으로 GPU가 DP Call을 받으면
또 바뀐 상태대로 렌더링을 하는 것이다. 이런 방식으로 CPU는 필요한 정보를 갱신하는 렌더 상태 변경 명령을 보내고, GPU는 그대로 렌더 상태를 변경하고, CPU가 다시 DP Call을 주면 GPU가 렌더링하는 과정을 반복한다.
아까 이거 그려! 하고 명령을 보내는 것을 드로우 콜이라고 했는데, 단순히 그리라는 명령인 DP Call 뿐 아니라 넓은 의미로 상태 변경 명령부터 DP Call까지를 모두 포함한 것을 드로우 콜이라고 한다.
Command Buffer
또 CPU가 GPU에게 명령을 보낸다고 표현했지만 사실 바로 명령을 주지 않고 중간에 한 단계를 거친다. 바로 명령을 보낸다면 GPU가 다른 작업을 수행하고 있다면 CPU가 명령을 주고 할일을 해야되기 때문에 GPU가 하던일을 마칠 때까지 기다려야하는 불상사가 생길 것이다. 그래서 CPU는 명령을 버퍼에 쌓아놓고, GPU는 버퍼에서 명령을 가져가서 할 일을 하는 방식으로 일한다. 저번 편에서 설명한 병렬식 방법이 이것이다! 이렇게 일을 쌓아두는 버퍼를 Command Buffer라과 부른다. 커맨드 버퍼는 FIFO(First In First Out) 방식으로 명령을 처리한다.
>>Vulkan과 Metal은 여러개의 커맨드 버퍼를 이용하여 멀티 쓰레드로 처리하기도 한다. 따라서 기존 OpenGL ES보다 드로우콜 부담이 적다.
Draw Call은 CPU 바운드의 병목
문제는 CPU가 명령한 것이 GPU에서 사용하는 신호로 변환되어야한다는 것인데, 이는 결국 CPU에게 부담이 된다.
그래픽스 API들은 CPU에서 GPU로 보내는 명령을 공통적인 API로 구성한다. API가 호출되면 드라이버 칩셋에 알맞은 신호를 전달하여 GPU에 맞게 명령을 변형하는 과정을 거친다. 이 과정을 거치기 때문에 CPU가 GPU에게 명령을 보낼 때 오버헤드(어떤 정보 처리를 위한 간접적인 처리 시간)가 발생한다. 그래서 드로우콜은 CPU 바운더리의 오버헤드이다.
>>Multithreaded Rendering, 멀티 쓰레드 렌더링
렌더링에 필요한 작업들을 별도의 쓰레드로 분리해서 렌더링 성능을 높이는 것. CPU의 부담을 줄여주는 것임
Edit > Project Settings > Player > Other Settings > Rendering 에서 Multithreaded Rendering을 체크할 수 있다.
다만 모든 디바이스에서 멀티쓰레디드 렌더링이 잘 돌아간다는 보장은 없음.
CPU의 코어가 많지 않은 구형 디바이스는 잘 안돌아갈수있다. 또 iOS는 Metal API로 작동할 때만 멀티쓰레디드 렌더링이 활성화되고, WebGL 플랫폼은 아예 지원되지 않음.
중요한 것은 드로우콜 병목이 아닐 때는 이 옵션이 영향을 주지 못함. 반드시 타깃 디바이스에서 확인해보자~
드로우콜은 GPU의 성능보다 CPU의 성능에 의존적이다. 따라서 드로우 콜로 인한 성능 하락을 줄이려면 (텍스처나 폴리곤 수를 줄이는 것이 아니고) 드로우 콜 횟수를 줄여야한다.
드로우 콜의 발생 조건
기본적으로 오브젝트를 하나 그릴 때 Mesh 1개, Material 1개라면 드로우콜이 한 번 발생한다. 즉, Batch가 1이 된다.
하지만 오브젝트 하나가 메시 여러개로 구성되어있는 경우...메시가 17개라면 드로우콜도 17번 발생한다....
이 메시들이 1개의 메테리얼을 공유하더라도 메시 개수 대로 17번의 드로우 콜이 필요하다.
반대로 하나의 메시에 메테리얼이 여러 개여도 그 개수만큼의 드로우 콜이 필요하다.
쉐이더에 의해서도 드로우 콜이 늘어날 수 있다. 쉐이더 내에서는 멀티 패스(Multi Pass)라고, 두 번 이상 렌더링을 거치는 경우가 존재한다. 대표적인 예가 카툰 렌더링이다. 첫번째 패스에서 모델을 렌더링하고, 두번째 패스에서 모델 외곽선을 그려준다. 렌더링을 두 번 해야하므로 드로우 콜이 두 번, Batch도 두 번 ~~
따라서 메시가 하나, 메테리얼이 하나라고해서 반드시 하나의 드로우 콜만 발생하는 것이 아니다. 지레짐작하지 말자.
특히 모바일의 경우 드로우콜 횟수가 크면 성능에 많은 영향을 끼칠 수 있으므로 드로우 콜을 신경쓰면서 최대한 줄여야한다. 데스크톱에서는 1000개가 넘어도 가능하지만 모바일에서는 100개도 많은 편이다. 최신 모바일 디바이스는 200개가 넘는 것도 가능하지만...디바이스 마다 다르므로 드로우 콜의 기준을 확정지을 수 없다.
또 드로우콜을 개수로 따져왔지만, 사실 드로우콜 마다도 비용이 각각 다르다. 당연히 상태 변경이 많이 필요한 드로우 콜과 적게 필요한 드로우 콜은 비용 차이가 날 것이다.
Batch & SetPass
유니티에서는 드로우 콜을 Batch와 SetPass 두 용어로 나누어 표시한다.
Batch
DP Call과 상태 변경들을 합친 넓은 의미의 드로우 콜.
만약 Batch가 10번, SetPass가 1번 발생했다면 10번의 드로우 콜 동안 쉐이더의 변경은 없었고, 메시 및 트랜스 폼 정보 등 최소한의 상태 변경만 이뤄졌다는 것을 의미한다.
SetPass도 10번 일어났나면 10번의 드로우 콜 마다 매번 쉐이더의 변경이 이뤄졌고, 경우에 따라 많은 상태 변경이 일어났다는 것을 의미한다. 당연히 이 경우가 성능을 더 많이 잡아먹을 것이다.
SetPass Call
SetPass는 쉐이더로 인한 렌더링 패스 횟수를 의미한다.
SetPass에서 알려주는 상태 변경은 쉐이더의 변경 혹은 쉐이더 파라미터들의 변경이 일어나는 경우.
씬 오브젝트를 렌더링하는 과정에서 메테리얼이 바뀌면 그에 따라 쉐이더 및 파라미터들이 바뀌고 SetPass 카운트가 증가한다. 이 때 많은 상태 변경이 일어나야 하기 때문에 SetPass 횟수도 중요하다.
만약 게임이 CPU 바운드이고 GPU에 명령을 보내는 과정, 즉 드로우콜이 병목이라면 SetPass call 횟수를 줄이는게 가장 효율적이다.
서로 다른 메시를 사용한다고 SetPass call이 늘어나는 것은 아님. 다른 메시라도 같은 메테리얼을 쓰면 늘어나지 않음.
10개의 오브젝트가 서로 다른 메시여도 같은 메테리얼을 쓴다면 Batch는 10번 발생해도 SetPass는 1번만 발생한다.
이렇게 SetPass call이 적으면 Batch 구성이 잘 되어있는 것.
02. Batching
드로우 콜을 줄이는 작업.
유니티에서 배칭을 활용함으로써 드로우 콜을 많이 줄일 수 있기 때문에 거의 필수적으로 사용해야하는 기능.
여러 Batch를 묶어서 하나의 Batch로 만드는 것을 Batching이라고 한다. 굉장히..간단한 단어다.
즉 Batching은 여러 번 드로우 콜 할 상황을 하나의 드로우 콜로 묶는 과정이다.
다른 오브젝트, 메시를 사용하더라도 메테리얼이 같다면 하나의 Batch로 구성할 수 있다.
여러 개의 다른 오브젝트들이지만 메테리얼이 같다면 배칭처리를 통해 한 번에 그리는 것이 가능하다는 얘기이다.
된다면 완전 땡큐인 기능인 것...!
여기서 메테리얼이 같다는 것은 동일한 메테리얼 인스턴스를 의미한다.
같은 텍스쳐, 같은 쉐이더를 이용한 메테리얼이더라도 따로 두개를 만들어 두면 그 두 개는 다른 메테리얼로 인식, 배칭이 되지 않는다. 스크립트에서 메테리얼에 접근할 때도 이런 이유에서 조심해야할 것이 있다.
GetComponent<Renderer>().material.color = Color.red;
이렇게 메테리얼의 속성을 수정하면 메테리얼이 수정되는 것이 아니라 메테리얼의 복사본이 생성된다.
대신 Renderer.sharedMaterial로 수정하면 복사본이 생성되지 않고 공유된 메테리얼 원본을 수정한다. 공유하고 있던 다른 친구들도 수정된 결과가 적용되니 인지하고 유의해서 사용하자.
배칭을 하기 위해서는 하나의 메테리얼을 여러 메시들이 공유해서 사용해야한다.
즉 텍스쳐 하나를 공유해서 사용해야한다는 뜻.
그래서 텍스쳐 하나에다가 여러 개의 텍스쳐를 합쳐서 사용하는 텍스쳐 아틀라스(Texture Atlas) 기법으로 리소스가 제작된다.
그러면 모든 메시의 텍스쳐를 1개에다가 때려박으면 되는게 아닌가 ?
안돼요. 해상도 문제를 고려해야한다. 512 해상도의 텍스쳐 16개를 합치면 2048 해상도의 텍스쳐가 필요하다. 구형 디바이스에서는 성능 저하가 발생할 수 있기 때문에 해상도를 고려하면서 작업해야한다.
Batching은 Static Batching, Dynamic Batching 두 종류가 있다.
Edit > Project Settings > Player에서 Static Batching, Dynamic Batching을 체크할 수 있다.
체크하면 조건에 맞는 경우 자동 배칭이 된다. 간단하게 사용할 수 있지만 각 배칭 기법은 특성과 한계가 존재하니 알고 쓰도록 하자.
Static Batching
정적인 오브젝트를 위한 배칭 기법. 주로 배경 오브젝트들이 해당.
Static Batching을 적용할 오브젝트라면 인스펙터에서 Static을 체크해줘야 한다. 이걸 켜주면 스태틱 배칭의 대상으로 인정받아 로딩타임에서 자동으로 배칭처리가 될 것이다.
당연히 다이나믹 배칭보다 효율적이다. 런타임에서 수행할 버텍스 연산이 없기 때문이다.
Stats 창에서 Saved by batching, 즉 배칭으로 얼마나 드로우 콜을 절약했는 지 확인할 수 있다.
메테리얼이 1개라고 무조건 1개의 배치로 구성되는 건 아니고 라이트맵, 라이트프로브, 동적라이트 영향 여부 등 다양한 조건에 의해서 배칭이 나뉠 수 있음. 배칭이 왜 나뉘는지는 프레임 디버거(아래 03번에서 배워보자)로 확인할 수 있다.
로딩타임에서 배칭처리를 하기 때문에 처음부터 씬에 존재해야 스태틱 배칭에 껴준다. 나중에 추가되는 정적인 오브젝트들은 자동으로 스태틱 배칭이 되지 않고
StaticBatchingUtility.Combine()
로 런타임 상에 추가된 정적인 오브젝트들도 배칭처리를 받을 수 있게 해줘야한다.
But 스태틱 배칭에 껴주기 위해 데이터를 수집하고 메시를 재생성해야하기 때문에 많은 시간이 필요하므로 되도록 자제하도록 하자.
>> 예제 프로그램을 위해 추가적인 드로우 콜 요인 제거하기
- 카메라의 Clear Flags를 Solid Color로.
- Allow HDR과 Allow MSAA를 끈다
- Directional Light의 Shadow Type을 No Shadow로 설정.
주의할 점
메모리가 추가로 필요하다!
다른 메시들을 메테리얼이 같다는 이유로 한 번에 그리는 것이다. 따라서 배칭처리를 하면 오브젝트들을 합쳐서 내부적으로 하나의 메시로 만들어 놓는데, 1개의 메시만 사용하더라도 여러 개의 메시를 합친, 거대한 메시를 만들기 위한 추가 메모리가 필요한 것이다. 이렇게 새로 만들어낸 메시를 GPU가 가져가서 그대로 화면에 렌더링하므로 드로우 콜은 1번에 처리될 수 있던 것.
추가적으로 메모리를 희생하더라도 드로우 콜을 줄일 수 있기 때문에 런타임 성능을 높일 수 있다.
But 얘 때문에 메모리가 문제된다면 줄여야지.
>>배경 맵 모듈화
씬을 하나의 커다란 메시로 만드는 것 보다 모듈화해서 조립하듯이 제작하는 것이 성능면에서 더 좋다.
하나의 커다란 메시로 만든다면 메시 일부만 보여도 전체 메시의 폴리곤을 처리하기 때문 !
스태틱 배칭 처리가 되더라도 원래의 게임오브젝트 기준으로 컬링이 이뤄진다.
근데 야외 맵은 한 화면에 배경 오브젝트가 그렇게 많지않고 반복되는 모듈이 적어서 조금 애매하다..
또 모듈의 단위를 너무 작게나누면 오히려 비효율적.
Dynamic Batching
Static과 반대로, Static이 체크되지 않은 동적인 오브젝트들 중 동일한 메테리얼을 사용하고 특정 조건을 만족하는 오브젝트들을 대상으로 배칭처리를 하는 기능. 역시 Dynamic Batching을 체크해주면 별로의 추가 작업이 필요하진 않고 알아서 해준다.
하지만 제약사항이 많다 ! 런타임상에 배칭처리를 해야되기 때문에 어쩔수없다
매 프레임 씬에서 동적인 오브젝트들의 버텍스를 모아서 합쳐주는 과정을 거친다. 모은 버텍스들을 다이나믹 배칭에 쓰이는 버텍스 버퍼와 인덱스 버퍼에 담으면 GPU가 이것을 가져가서 렌더링한다. 결과적으로 매번 데이터 구축과 갱신이 발생하기 때문에 매 프레임마다 오버헤드가 발생한다. 일반적으로 렌더링 할 때는 버텍스 쉐이더에서 월드스페이스로 변환하는 과정에서 GPU에서 고속연산이 이뤄지는데, 다이나믹 배칭을 위해서는 오브젝트의 버텍스를 월드스페이스로 변환하는 연산이 CPU에서 이뤄진다. 따라서 이 연산과정이 드로우 콜보다 시간이 오래걸리면 오히려 효율이 떨어지는 것이다. 배칭 오버헤드와 드로우 콜 시간을 비교하여 더 빠른 쪽으로 하는 것이 맞다.
만약 특정 오브젝트의 배칭 오버헤드가 더 커서 배칭을 쓰지 않도록 하고 싶다면 쉐이더 태그에서 DisableBatching 플래그를 True로 설정해주면 된다. 아래는 쉐이더 코드에서 쉐이더 태그를 다는 예시.
SubShader {
Tags { "RenderType" = "Opaque" "DisableBatching" = "True" }
...
제약 사항
오버헤드가 발생하므로 제약 사항이 많다. 그래서 스태틱 배칭보다 잘 쓰이지 않는다.
- Skinned Mesh에는 적용이 불가하다. Skinned Mesh는 움직이는 캐릭터에 Skinning을 수행하는 기능인데, GPU나 SIMD에서 고속으로 연산을 수행해야한다. 다이나믹 배칭으로 묶어버리면 CPU 연산 효율이 떨어지기 때문에 Skinned Mesh는 다이나믹 배칭의 영향을 받지 않는다. 캐릭터 여러개가 렌더링 되야한다면 각각 별도의 드로우콜로 렌더링 되어야 하는 것이 맞다.
- 버텍스가 너무 많은 메시는 다이나믹 배칭의 대상에서 제외된다. 다이나믹 배칭은 버텍스를 모아서 합치는 과정이기 때문에 너무 많은 버텍스를 수집하게 되면 오히려 오버헤드가 드로우 콜의 비용보다 높아질 수 있다.포지션, 노말, UV를 사용하는 모델이라면 300이하의 버텍스를 가진 모델만 다이나믹 배칭이 적용가능한데, 사실상....요즘엔 모바일에서도 300이상의 버텍스를 가진 모델이 많으므로 적용하기가 쉽지 않다.
>>Skinning, 스키닝
메시를 애니메이션 포즈에 맞게 메시의 버텍스들을 보정해주면서 변형해주는 과정.
스키닝이 필요한 애니메이션을 가진 모델들은 Skinned Mesh Renderer라는 특별한 메시 렌더러를 컴포넌트로 가진다.
렌더링 전에 스키닝 연산이 이뤄지면서 버텍스 위치의 재계산이 일어난다. 따라서 스키닝 되는 메시의 폴리곤이 많을수록 렌더링도, 스키닝 연산도 부담이 생긴다. 스키닝 연산은 CPU에서 이뤄지므로 결과적으로 버텍스가 많은 스키닝 메시는 스키닝 처리에서 CPU의 부담을 유발할 수 있다. GPU를 통해 연산하고 싶다면 Edit > Project Settings > Player > Other Settings > GPU Skinning을 체크하면 된다. GPU가 더 빨라보이지만, CPU 스키닝 연산은 SIMD(Single Instruction Multiple Data)라는 아키텍처를 통해 고속연산이 가능하기 때문에 CPU, GPU 중 어디에 병목이 있는지 확인하고 병목이 없는 쪽에서 스키닝을 연산해서 병목이 있는 바운드의 부담을 덜어주는 것이 좋을 것이다. 대부분 게임은 GPU 자원이 많이 필요하므로 CPU에서 스키닝 연산을 하는 편이다.
GPU 스키닝은 OpenGL ES2에서는 지원 되지 않고, DirectX 11과 OpenGL ES3에서 유효하다. 모바일 디바이스 Vulkan, Metal 에서는 동작하지않는다.
Mesh.CombineMeshes
수동으로 배칭을 처리하는 방법. 처음부터 하나의 메시로 되어있는게 아니라 런타임동안 파츠가 조합되어 오브젝트가 만들어져야 하는 경우라면 이것을 이용해 드로우콜을 줄일 수 있다.
메테리얼을 공유하는 애들끼리 묶어두고 자식들의 메시를 수집한 뒤 하나의 메시로 합쳐주는 스크립트를 작성해서 부모가 되는 오브젝트에 추가해준다.
>>CombineMeshesSample.cs
using UnityEngine;
public class CombineMeshesSample : MonoBehaviour
{
void Start()
{
MeshFilter[] meshFilters = GetComponentsInChildren<MeshFilter>();
MeshRenderer[] meshRenderers = GetComponentsInChildren<MeshRenderer>();
if (CheckSameMaterial(meshRenderers) == true)
{
CombineInstance[] combine = new CombineInstance[meshFilters.Length];
int i = 0;
while (i < meshFilters.Length)
{
combine[i].mesh = meshFilters[i].sharedMesh;
combine[i].transform = meshFilters[i].transform.localToWorldMatrix;
meshFilters[i].gameObject.SetActive(false);
i++;
}
MeshFilter meshfilter = gameObject.AddComponent<MeshFilter>();
MeshRenderer meshRenderer = gameObject.AddComponent<MeshRenderer>();
meshRenderer.sharedMaterial = meshRenderers[0].sharedMaterial;
meshfilter.mesh = new Mesh();
meshfilter.mesh.CombineMeshes(combine);
transform.gameObject.SetActive(true);
}
}
bool CheckSameMaterial(MeshRenderer[] meshRenderers)
{
Material mtrl = meshRenderers[0].sharedMaterial;
for (int i = 1; i < meshRenderers.Length; ++i)
{
if (mtrl != meshRenderers[i].sharedMaterial)
return false;
}
return true;
}
}
2D Sprite Batching
2D Sprite도 Batching이 이뤄질 수 있다.
3D 보다 버텍스가 적기 때문에 배칭이 훨씬 효율적으로 가능하다.
Static이나 Dynamic Batching처럼 체크하지 않아도 자동으로 배칭이 이뤄진다.
텍스처 아틀라스와 같은 기법으로 스프라이트들을 하나의 이미지에 모아 넣는 스프라이트 시트로 많이 제작된다.
Sprite Atlas
스프라이트들을 하나의 아틀라스로 합치면 Batch를 줄일 수 있다. 당연
Edit > Project Settings > Editor > Sprite Packer의 Mode를 Alway Enabled로 설정한다. 그리고 Project > Create > Sprite Atlas를 선택해서 스프라이트 아틀라스 에셋을 생성한다.
Inspector에서 설정이 가능하다. Objects for Packing에서 원하는 스프라이트를 추가할 수 있다. 폴더나 여러개의 스프라이트를 통째로 끌어다 놓는 것도 가능하다. 하나씩 하지 말자..
그 후 Pack Preview 버튼을 누르면 하나의 텍스쳐로 패킹된 결과를 확인할 수 있다.
GPU Instancing, GPU 인스턴싱
Static, Dynamic Batching 외에도 GPU Instancing이라는 기법을 활용해서 드로우 콜을 줄일 수 있다.
인스턴싱이란 동일한 메시의 복사본을 만드는 것이다. 한번의 드로우 콜로 오브젝트의 여러 복사본을 렌더링한다는 점은 배칭과 같다. 인스턴싱은 배칭에 비해 런타임 오버헤드가 적다. 배칭은 CPU에서 지오메트리 정보들을 연산해 합친 메시를 새로 만들어내는 과정을 거치고 GPU가 이를 가져다 렌더링하는 방식이었는데, GPU 인스턴싱은 메시를 새로 만들어 내지 않고 인스턴싱 되는 오브젝트들의 트랜스폼 정보를 별도의 버퍼에 담는다. GPU는 이 버퍼와 원본 메시를 가져다 여러 오브젝트들을 한번에 처리해서 렌더링한다. 인스턴싱 처리를 GPU에서 처리하기 때문에 GPU에서 메시를 재구성하는 오버헤드나 메모리 이슈로부터 자유롭다. 오버헤드로 인한 제약이 적어서 원본 메시의 버텍스 개수와 상관없이 런타임에서 동적인 오브젝트들을 배칭처리할 수 있다. 다이나믹 배칭보다 제약이 적고 부담도 없다 !
Standard Shader를 쓰는 메테리얼의 마라미터 중 Enable GPU Instancing을 체크하면 이 메테리얼을 쓰는 오브젝트들은 유니티가 자동으로 인스턴싱 처리를 해준다. 동일한 메시끼리만 한 번의 드로우콜로 처리가 가능하다.
역시 Skinned Mesh Renderer를 쓰는 스키닝 메시에는 사용할 수 없다.
GPU에서 처리하는 것이므로 디바이스의 스펙에 의존적이다. 지원하는 디바이스에서만 사용할 수 있다.
모바일 기기에서는 OpenGL ES 3.0 이상이나 Vulkan, Metal을 이용하는 경우에만 사용 가능하다.
03. Frame Debugger
드로우 콜은 Stat 창이나 Profiler로 확인할 수 있었지만 상세하게는 알 수 없었다. 그럴 때 프레임 디버거를 쓴다.
프레임이 어떻게 렌더링되는지 직관적으로 확인할 수 있다. 게임 수행 중 프레임을 캡쳐해서 드로우콜을 순서대로 확인할 수 있고, 각 드로우콜 과정에서 어떤 메시가 렌더링되고 쉐이더의 속성은 무엇인지도 확인할 수 있다. 배칭 처리 중에 드로우콜이 나뉜다면 왜 나뉘는지도 확인할 수 있다.
04. Culling
'PROGRAMMING > Unity' 카테고리의 다른 글
[유니티 그래픽스 최적화] 03. 병목 (0) | 2020.02.19 |
---|---|
[유니티 그래픽스 최적화] 02. 렌더링 파이프라인 (0) | 2020.02.18 |
[유니티 그래픽스 최적화] 01. 개요 (0) | 2020.02.17 |