유니티에서 제어 해보자.
앞선 시간에선 드로우콜이 무엇인지 이것을 해결하기 위해 몇 가지 배칭에 대한 이야기를
했었다.
이번엔 유니티에서 직접 사용해보며, 몇 가지 확인되는 사항들에 대해서 설명하고자 한다.
복습
위 예시를 보면 SetPass Call과 Draw Call이 각각 3개씩 있는 것을 알 수 있다.
그려야할 오브젝트가 3개이고, 머테리얼이 각각 3개이니 당연하다.
Batching
다만, 머테리얼을 위 그림과 같이 하나로 하면 메시는 서로 달라도 SetPass Call은 줄어든다.
위치에 따른 이상현상
위 사진을 보면 Set Pass와 Draw Call 개수를 생각해보면 당연히 SetPass는 2개 Draw Call은 3개라
생각 할 수 있다. 그러나 결과를 보면 Set Pass의 개수가 3개로 되어있음을 볼 수 있다.
사실, 이 현상 때문에 이 포스팅을 꼭 학습노트에 적어야겠다고 생각했다.
이번엔 빨간 머테리얼을 가진 원통형 메시를 멀리 떨어뜨리자 이제서야 Set Pass 가 2개로 나오고 있다.
위 그림을 보면서 설명해보자면 Forward Renderer는 카메라부터 가까운 순서대로 오브젝트를
렌더링 한다. 또한 Batching되기 위해서는 묶이는 대상끼리 렌더링 순서가 연속되어야 하기 때문에
위와 같은 현상이 일어 났다고 볼 수 있다 .
배칭 깊게 파해치기.
위와 같은 현상을 배칭이 깨졌다고 할 수 있는데, 분명 배칭에 대해 지난 시간에 잘 이해했다고
생각했지만 막상, 저렇게 깨지는 것에 대해서 잘 이해가 되질 않았다. 그래서 이번엔 배칭에 대해
좀 더 깊게 파해쳐 보고자한다.
원본 메시 합치기.
가장 단순한 접근법은, 3개의 별도 메시를 3ds Max나 Mesh Baker와 같은 3d tool을 통해,
한 개의 메시로 만드는 것이다. 당연히 1개 오브젝트가 화면에 배치되어 있으니 드로우콜이
1 개가 되었다.
다만, 하나로 합쳐진 메시는 머테리얼까지 합쳐지진 않는다. 따라서 합쳐지는 메시끼리
머테리얼을 세팅 해줘야 한다.
아틀라스 2D에서 또한 최적화 용도로 많이 쓰이는 이것은 그냥 모든 스프라이트를 하나의 커다란
스프라이트로 저장해두고 잘라서 쓴다는 개념이다.
정적 배칭
위 원본 메시 합치기를 엔진에서 자동으로 합쳐주는 기능이 바로 Static Batching이다. 이전 시간에서
잠깐 언급했던 내용들을 복기 해보자. 인스펙터에서 static을 켜주면, 매 프레임마다 메시를 합친다.
라는 개념을 기억할 수 있다.
또 제대로 동작하기 위해선 동일한 머테리얼을 공유해야하며 런데 순서를 묶일 수 있게 세팅해줘야한다.
그냥 메시 합치기와 차이점은 Frustum Culling이 동작하여 화면 밖에 있는 메시는 그리지 않는다.
대신 합치지 않은 메시 + 합쳐진 메시까지 메모리를 차지하게 되어 너무 많은 메시가 Static Batching되면 메모리 이슈가 생길 위험이 생긴다.
GPU Instancing
일반적인 배칭은 CPU에서 지오매트리 정보들을 연산해 별도의 메시로 합치면 GPU가 이를 가져와 렌더링 하는 방식이다.
GPU Instancing은 GPU 메모리 버퍼에 메쉬 정보를 저장하여 추가 Draw Call 없이 렌더링하는 기술이다. 그래서 하나로 묶는다는 Batching이라는 단어 대신 GPU에서 생성한다는 의미로 GPU Instancing이라고 한다.
결론적으로 동일한 오브젝트를 GPU에서 복사해서 렌더링하는 기술이기 때문에, 동일한 메쉬를 사용해서 Draw call이 최적화된다.
따라서 CPU에서 메시를 재구성하는 오버헤드가 없고 원본 메시의 버텍스 갯수와 상관 없이 런타임에서 동적인 오브젝트를 배칭해줄 수 있다.
따라서 배칭과 달리 CPU 오버헤드나 메모리 문제에서 자유롭다. 그러나 디바이스의 GPU에 따라 불가능할 수 있으며 당연히 동일한 메시끼리만 한번의 드로우콜로 처리가 가능하다.
또, 쉐이더에서 해당 기능을 지원 해야한다.
왜냐하면 GPU 메모리 버퍼에 저장하는 정보는 정확히 MVP 행렬 정보이기 때문입니다.
위와 같이 오브젝트는 분명 3개 인데, 실제 Draw Calls의 수는 1개이다.
GPU 메모리 버퍼 용량
다만, GPU 메모리 버퍼에 저장하는 기술이기 때문에 위 사진과 같이 GPU 인스턴싱을
사용 하였음에도 배칭이 42가 되어있는 꼴을 볼 수 있는데.
인스턴스 당 사용할 수 있는 버퍼 용량이 정해져 있어, 위 사진처럼 5000개의 메시를
40개 정도로 줄일 수 있다.
이 용량은 GPU 칩셋 & API에 따라 전부 다르지만 보통 PC = 64KB, 모바일 = 16KB라고 합니다.
한 개의 오브젝트당 MVP연산에 필요한 메모리는 128 byte이며 PC플랫폼 기준으로 한 인스턴스에 대략 500개 오브젝트 정보를 담을 수 있다. 모바일엔 대략 120개 정도라 볼 수 있다.
GPU 인스턴싱 정보에 Material Property 추가 & 활용
GPU 인스턴싱 정보에 추가로 Material Property를 추가하여 활용할 수 있다.
즉, 런타임 도중 머테리얼의 색상이나 값을 변동할 수 있다는 뜻이다.
기존에 활용하던 _Color 변수를 제거하고, UNITY_INSTANCING_BUFFER_START라는 메크로에 새로 선언하면
기존에 MVP 정보만 인스턴싱 정보에 담았지만 추가로 해당 변수도 담겠다는 뜻이 된다.
당연히 추가하는 변수가 많아 질 수록 메모리 버퍼 용량이 늘어나서 인스턴싱 갯수도 제한된다.
SRP Batcher
유니티에서 지원하는 렌더링 파이프라인은 3가지다.
빌트인, URP,HDRP로 나뉜다. 자세한 설명은 아래 공식 문서에서 확인 가능하다.
https://docs.unity3d.com/kr/2021.1/Manual/render-pipelines-overview.html
이중 SRP Batcher는 URP와 HDRP에 사용할 수 있는 배칭 시스템이다.
위 사진과 같이, 각각 다른 메시 3개가 각각 다른 머테리얼을 사용하고 있다.
그러나 Set Pass는 1인 것을 확인할 수 있다.
해당 머테리얼들은 동일한 쉐이더를 사용하고 있으며 쉐이더단위로 SetPass Calls이 1로 배칭되는것이다.
하지만 Fream Deubgger에선 3 draw calls가 1개로 묶였다고 표시되고 있다.
위 두 가지 자료를 보면 Per Object large buffer에 메시 정보를 저장하고 CPU에서는 Transform 정보만 받아오고 있다.
여기서 Transform 정보를 받는 부분이 Batches로 표현되는 것 같다.
그래서 Per Object large buffer에 저장된 메시 정보와 CBuffer에 저장된 Material 정보가 일치하면 한번에 묶어서 드로우콜하는 원리로 추측된다.
CBUFFER_START(UnityPerMaterial) half4 _MainTex_ST; float4 _Color; CBUFFER_END
SRP Batcher는 HLSL기반의 쉐이더로 작성되면 동작되고, 기본적으로 머테리얼 값이 다르면 Set Pass Calls이 묶이지 않지만 쉐이더 내부에서 CBuffer에 저장할 변수를 지정하고, 해당 값은 GPU에서 처리되기 때문에 Set Pass Calls이 발생하지 않아 SRP Batcher에 묶일 수 있게 된다.
SRP Batcher와 다른 배칭 시스템과 조합
SRP Batcher는 Static Batching말고 같이 조합되지 않는다.
SRP Batcher가 동작하는 순간 Dynamic Batching과 GPU Instancing은 비활성화된다.
위 사진은 SRP Batcher + Static Batching 결과다.
근데, 왜 Set Pass Calls가 3개이고 Draw Calls가 5개 인지 이해가 되질 않았다.
일단 내가 이해하기론
아래 정적 배칭 관련된 항목에서 Batched Draw Calls 갯수가 메시 갯수 9개 이고 최종적으로 Batches는 1개 인 것으로 보아 저 9개의 메시를 하나로 묶었고.
내가 이해한게 맞다면 Set Pass Calls도 1로 나오고 Batches도 1이 나와야 하는 거 아닌가?
직접 실험해보자.
이해가 안되었기 때문에, 포스팅을 쓴 사람에게 코멘트를 달기도 하고 여러 단톡방을 통해 물어본
결과 직접 해보는게 빠를 것 같다는 피드백을 받아 직접 해보려 한다.
환경은 비슷하다. 다만 메인 카메라의 영향인지, Set Pass와 Draw Call에 1씩 추가 되었다.
각각 색만 다른 머테리얼을 만들어주고 정적 배칭을 할 수 있도록 인스펙터의 Static을 활성화 해주었다.
쉐이더는 URP 기본 Lit을 사용하고 있어 위 그림과 같이
Set Pass Calls는 2개( 배경 포함 )인 것을 보아 아까 위에서 Set Calls가 3개였던 것은, 각각 쉐이더가
달랐던 모양이다.
다만 이제 문제는 Draw Calls가 4개인 것에 대한 이유를 찾아야 했다.
이유를 찾는 것은 그렇게 어렵지 않았다. 일단 하나의 메시를 제외한 나머지 메시에 동일한 머테리얼을 씌우고 다시 확인해보니, 배치 수가 줄어들었다. 카메라를 포함한 총 3개.
해당 결과를 보고, 머테리얼의 개수 만큼 늘어남을 알 수 있었다.
또, 왜 배칭에서 제외 되었는지 그 이유를 Fream Debug에서 알려주고 있는데.
SRP: First call from Scriptable Render Loop Job 이라고 알려주고 있다.
다만 해당 에러에 대해선 서칭을 통해서도 알아 낼 수가 없었다.
여하튼 결과만 보자면 머테리얼의 개수가 늘어날 때 마다 배치의 개수 또한 늘어난다.