반응형

이 포스팅에 잘못된 내용이 있고 내용도 난잡합니다, 추후 다시 정리할 예정입니다.

그 전에 맥락 참고용으로 봐주세요.


 

Draw Call(드로우콜)이란?

GPU에게 오브젝트를 화면에 그리라고 명령하는 것 입니다.

 

 

드로우콜을 줄여야(최적화)해야하는 이유?

GPU가 오브젝트를 화면에 그리는데 시간이 필요한데, 한 프레임에 그려야할 횟수가 많으면 게임 FPS가 낮아지고 모바일 기기에서는 GPU 부하로 인해 발열 이슈가 생깁니다.

 

 

드로우콜의 구성

https://www.youtube.com/watch?v=UsyvT36vqpU&t=1645s

1개의 드로우콜에는 Batch와 Set Pass Call 2가지 정보가 들어 있습니다.

Batch는 메쉬와 Transform(좌표, 회전, 크기 값) 정보를 의미하고

Set Pass Call은 Material 및 쉐이더 정보를 의미합니다.

따라서 예시처럼 메쉬 3개, Material 3개로 구성된 화면은 Batches 3 & SetPass Calls 3으로 프로파일링 결과가 나옵니다.

 

 

 Batching이란? - 최적화 원리

영어로 Batching이란 어떤 것들을 한번에 묶어서 처리한다는 의미입니다.

드로우콜을 최적화하는 핵심 원리는 오브젝트 혹은 Material을 한번에 묶어서 처리하는 것 입니다.

 

위 예시는 다른 종류의 메쉬 3개와 동일한 Material 1개로 구성된 화면입니다.

이전과는 다르게 SetPass Calls이 1개가 되었습니다.

https://www.youtube.com/watch?v=UsyvT36vqpU&t=1645s

왜냐하면 위 3개의 오브젝트를 드로우콜하는 과정에서 1개의 Material로 묶어서 처리 할 수 있기 때문입니다.

 

 

한번에 묶어서 처리하는 원리

가운데에 위치한 오브젝트의 Material을 다른것으로 교체하여 화면에 구성된 Material은 총 2개지만 SetPass Calls이 3개가 나오고 있습니다.

 

이번에는 다른 Material을 사용하는 오브젝트를 카메라로부터 가장 멀리 위치시켰는데 SetPass Calls가 2개 나오고 있습니다.

 

Forward Renderer는 카메라로부터 가까운 순서대로 오브젝트를 렌더링합니다.

또한 Batching(한번에 묶어서 처리)되기 위해서는 묶이는 대상끼리 렌더링 순서가 연속되야하기 때문에 위와 같은 현상이 발생했습니다.

 

한번에 묶어서 처리한다는 핵심 맥락 설명이 끝났으니, 엔진 기능을 활용한 드로우콜 최적화 방법에 대해 설명하겠습니다.

 

 

 

 

Draw Call Batching


드로우콜 배칭이란 렌더링할 오브젝트 자체를 묶어서 처리한다는 의미입니다.

 

 

1. 원본 메쉬를 합치기 (Mesh Combine)

가장 단순한 접근입니다.

3개의 별도 메쉬였던 것을 3ds Max나 Mesh Baker와 같은 에셋을 활용해 한개의 메쉬로 만든것 입니다.

당연히 1개 오브젝트가 화면에 배치되어 있으니 드로우콜이 1개가 되었습니다.

 

하나로 합쳐진 메쉬는 Material까지 합쳐지지 않습니다.

따라서 합쳐지는 메쉬끼리 Material 세팅이 동일해야합니다.

https://www.slideshare.net/ozlael/unite-seoul-2016-60714130

그래서 여러 오브젝트의 텍스처를 한개로 합치는 아틀라스 작업이 필요합니다.

 

 

 

2. Static Batching 

앞서 소개한 메쉬 합치는 것과 비슷하지만, 엔진에서 자동으로 합치는 기능입니다.

 

오브젝트들의 Inspector에서 Batching Static을 활성화하고 에디터 실행하거나 빌드하면 자동으로 메쉬가 합쳐지게 됩니다.

 

배칭이 제대로 동작하기 위해서는 앞서 소개한 내용과 동일하게 Material을 공유해야하며 렌더 순서를 묶일 수 있게 세팅해줘야합니다.

 

그냥 메쉬 합치기와 차이점은 Frustum Culling이 동작하여 화면 밖에 있는 메쉬는 그리지 않습니다.

대신 합치지 않은 메쉬 + 합쳐진 메쉬까지 메모리를 차지하게 되어 너무 많은 메쉬가 Static Batching되면 메모리 이슈가 생길 위험이 있습니다.

 

 

Dynamic Batching

Static Batching은 고정된 오브젝트만 묶을 수 있지만, Dynamic Batching은 런타임 도중에 움직이는 오브젝트도 묶을 수 있는 기능입니다.

GPU로 Draw call 보내기 전에 메쉬를 하나로 묶는 작업을 하기 때문에 CPU 부하가 있습니다.

또한 버텍스 수가 300 미만인 메쉬만 묶일 수 있습니다.

 

그런데 URP 빌드에서 다이나믹 배칭이 작동하지 않아서 따로 자세한 내용은 다루지 않겠습니다.

(파티클 시스템에서 작동하는 다이나믹 배칭은 별개 입니다.)

 

 

 

GPU Instancing

GPU Instancing은 GPU 메모리 버퍼에 메쉬 정보를 저장하여 추가 Draw Call 없이 렌더링하는 기술입니다.

그래서 하나로 묶는다는 Batching이라는 단어 대신 GPU에서 생성한다는 의미로 GPU Instancing이라고 합니다.

그러나 동일한 오브젝트를 GPU에서 복사해서 렌더링하는 기술이기 때문에, 동일한 메쉬를 사용해서 Draw call이 최적화됩니다.

 

GPU Instancing기능을 사용하기 위해서는 쉐이더에서 해당 기능을 지원해야합니다.

왜냐하면 GPU 메모리 버퍼에 저장하는 정보는 정확히 MVP 행렬 정보이기 때문입니다.

            #pragma multi_compile_instancing
            
            #pragma vertex vert
            #pragma fragment frag
            
            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);

            CBUFFER_START(UnityPerMaterial)
                half4 _MainTex_ST;
                float4 _Color;
            CBUFFER_END
            
            struct appdata
            {
                float4 positionOS : POSITION;
                float2 uv : TEXCOORD0;

                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f
            {
                float4 positionCS : SV_POSITION;
                float2 uv : TEXCOORD0;

                UNITY_VERTEX_INPUT_INSTANCE_ID
            }; 
            
            v2f vert(appdata v)
            {
                v2f o = (v2f)0;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_TRANSFER_INSTANCE_ID(v, o);

                o.positionCS = TransformObjectToHClip(v.positionOS.xyz);
                o.uv = v.uv;
                return o;
            }

            float4 frag(v2f i) : SV_Target
            {
                UNITY_SETUP_INSTANCE_ID(i);
                float2 mainTexUV = i.uv.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                float4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, mainTexUV);
                return col * _Color;
            }

 

#pragma multi_compile_instancing 코드를 작성하면 아래와 같이 Material에 GPU Instancing 활성화 메뉴가 나오고 체크해야 작동합니다.

 

 

GPU 메모리 버퍼 용량

https://catlikecoding.com/unity/tutorials/rendering/part-19/

GPU 메모리 버퍼에 저장하는 기술이기 때문에 무한하지 않습니다.

인스턴스 당 사용할 수 있는 버퍼 용량이 정해져 있어 위 예시처럼 5000개의 메쉬를 GPU Instancing으로 렌더링하면 40 Batches가 나오는 상황이 발생합니다.

 

이 용량은 GPU 칩셋 & API에 따라 전부 다르지만 보통 PC = 64KB, 모바일 = 16KB라고 합니다.

한개의 오브젝트당 MVP연산에 필요한 메모리는 128 byte이며 PC플랫폼 기준으로 한 인스턴스에 대략 500개 오브젝트 정보를 담을 수 있습니다.

 

 

GPU 인스턴싱 정보에 Material Property 추가 & 활용

GPU 인스턴싱 정보에 추가로 Material Property를 추가하여 활용할 수 있습니다.

즉, 런타임 도중 Material의 색상이나 값을 변동할 수 있다는 뜻입니다.

 

기존에 활용하던 _Color 변수를 제거하고, UNITY_INSTANCING_BUFFER_START라는 메크로에 새로 선언합니다.

기존에 MVP 정보만 인스턴싱 정보에 담았지만 추가로 해당 변수도 담겠다는 뜻입니다.

당연히 추가하는 변수가 많아 질 수록 메모리 버퍼 용량이 늘어나서 인스턴싱 갯수도 제한됩니다.

CBUFFER_START(UnityPerMaterial)
    half4 _MainTex_ST;
  //  float4 _Color;
CBUFFER_END

UNITY_INSTANCING_BUFFER_START(Props)
    UNITY_DEFINE_INSTANCED_PROP(float4, _Color)

    #define _Color UNITY_ACCESS_INSTANCED_PROP(Props, _Color)
UNITY_INSTANCING_BUFFER_END(Props)

 

다음 C# 스크립트입니다.

MaterialPropertyBlock 클래스를 활용해서 값을 제어해야합니다.

 

해당 클래스는 Material의 값 각 개별을 수정하는게 아닌, 통째로 묶어서 블록단위로 덮어씌운다는 기능 같습니다.

참고 - http://thomasmountainborn.com/2016/05/25/materialpropertyblocks/

    private Renderer renderer = null;
    [SerializeField] private Color color = Color.white;

    private void OnEnable()
    {
        renderer = GetComponent<Renderer>();
    }

    private void OnValidate()
    {
        if (renderer == null) return;
        MaterialPropertyBlock properties = new MaterialPropertyBlock();
        properties.SetColor("_Color", color);
        renderer.SetPropertyBlock(properties);
    }

GPU Instancing에서 인스턴싱 버퍼에 올린 값을 수정할때 사용하며, SRP Batcher에는 작동하지 않습니다.

 

 

 

 

 

 

SRP Batcher

SRP Batcher는 URP, HDRP 렌더파이프라인에서 사용할 수 있는 배칭 시스템입니다.

위 샘플은 다른 메쉬 3개가 각각 다른 Material을 사용하고 있는 상황입니다.

해당 Material들은 동일한 Shader를 사용하고 있으며 Shader 단위로 SetPass Calls이 1로 배칭되는것을 확인 할 수 있습니다.

 

하지만 Frame Debugger에서는 3 draw calls가 1개로 묶였다고 표시되고 있습니다.

 

https://zenn.dev/r_ngtm/articles/unity-study-srpbatcher
https://blog.unity.com/kr/technology/srp-batcher-speed-up-your-rendering

위 두가지 자료를 보면 Per Object large buffer에 메쉬 정보를 저장하고 CPU에서는 Transform 정보만 받아오고 있습니다.

여기서 Transform 정보를 받는 부분이 Batches로 표현되는 것 같습니다.

 

그래서 Per Object large buffer에 저장된 메쉬 정보와 CBuffer에 저장된 Material 정보가 일치하면 한번에 묶어서 드로우콜하는 원리로 추측됩니다.

 

결론적으로 SRP Batcher는 기존 빌트인 방식과 다르게 렌더링 프로세스가 돌아가는 것 같습니다.

 

SRP Batcher는 HLSL기반의 쉐이더로 작성되면 동작되고, 기본적으로 Material 값이 다르면 Set Pass Calls이 묶이지 않지만 쉐이더 내부에서 CBuffer에 저장할 변수를 지정하고, 해당 값은 GPU에서 처리되기 때문에 Set Pass Calls이 발생하지 않아 SRP Batcher에 묶일 수 있게 됩니다.

 

            CBUFFER_START(UnityPerMaterial)
                half4 _MainTex_ST;
                float4 _Color;
            CBUFFER_END

 

 

SRP Batcher와 다른 배칭 시스템과 조합

SRP Batcher는 Static Batching말고 같이 조합되지 않습니다.

SRP Batcher가 동작하는 순간 Dynamic Batching과 GPU Instancing은 비활성화됩니다.

 

아래는 SRP Batcher + Static Batching 결과입니다.

 

 

SRP Batcher를 무시하고 GPU Instancing을 사용하고 싶다면

풀과 같이 엄청난 수의 오브젝트 렌더링을 할 때는 GPU에서 복제해서 사용하는 것이 더 효율적일 수도 있을 것 같습니다.

하지만 기본적으로 SRP Batcher가 작동되면 GPU Instancing이 비활성화되기 때문에, 의도적으로 SRP Batcher를 호환하지 않는 쉐이더를 작성하거나, 스크립트 상에서 강제로 GPU Instancing 시키는 방법이 사용해야합니다.

 

저는 SRP Batcher와 GPU Instancing 둘다 스위칭되면서 스크립트 상에서 GPU Instancing시키는 방법을 소개드리겠습니다.

 

우선 쉐이더에서 _ DOTS_INSTANCING_ON 구문을 추가합니다, 정확한 용도는 모르겠습니다.

#pragma multi_compile_instancing
#pragma multi_compile _ DOTS_INSTANCING_ON

 

그리고 위에서 소개한 UNITY_INSTANCING_BUFFER_START 구문을 아래 코드로 대체합니다.

/*
CBUFFER_START(UnityPerMaterial)
    half4 _MainTex_ST;
  //  float4 _Color;
CBUFFER_END

UNITY_INSTANCING_BUFFER_START(Props)
    UNITY_DEFINE_INSTANCED_PROP(float4, _Color)

    #define _Color UNITY_ACCESS_INSTANCED_PROP(Props, _Color)
UNITY_INSTANCING_BUFFER_END(Props) 
*/
           
           
CBUFFER_START(UnityPerMaterial)
    half4 _MainTex_ST;
    float4 _Color;
CBUFFER_END 
           
#ifdef UNITY_DOTS_INSTANCING_ENABLED
    UNITY_DOTS_INSTANCING_START(MaterialPropertyMetadata)
         UNITY_DOTS_INSTANCED_PROP(float4, _Color)
         UNITY_DOTS_INSTANCING_END(MaterialPropertyMetadata)

         #define _Color          UNITY_ACCESS_DOTS_INSTANCED_PROP_FROM_MACRO(float4 , Metadata_Color)
 #endif

 

그리고 스크립트는 아래 자료를 참고해주시길 바랍니다.

원하는 오브젝트를 GPU Instancing으로 처리하는 스크립트입니다.

https://gist.github.com/andrew-raphael-lukasik/df4a36ff2ad89078258fd653c422a021

 

GPU Instancing for GameObjects

GPU Instancing for GameObjects. GitHub Gist: instantly share code, notes, and snippets.

gist.github.com

 

 

 

 

 

 

참고자료

그래픽 최적화로 가...가버렷! (부제: 배치! 배칭을 보자!

https://www.slideshare.net/ozlael/unite-seoul-2016-60714130

 

Catlike Coding - GPU Instancing

https://catlikecoding.com/unity/tutorials/rendering/part-19/

 

SRP Batcher에 대해서 공부하고 본다

https://zenn.dev/r_ngtm/articles/unity-study-srpbatcher

https://light11.hatenadiary.com/entry/2021/07/15/201733

 

유니티 도큐먼트

https://docs.unity3d.com/Manual/optimizing-draw-calls.html

https://docs.unity3d.com/kr/2019.4/Manual/GPUInstancing.html

https://docs.unity3d.com/2022.1/Documentation/Manual/dots-instancing-shaders.html

 

The magic of Material Property Blocks

http://thomasmountainborn.com/2016/05/25/materialpropertyblocks/

 

유니티 코리아 유투브 - 성능을 고려한 파이프라인 URP

https://www.youtube.com/watch?v=UsyvT36vqpU&t=1645s 

 

기타

https://forum.unity.com/threads/case-946946-dynamic-batching-not-working-in-a-player.491903/#post-3232697

https://forum.unity.com/threads/how-to-view-used-gpus-memory-reeee.1176740/

 

 

 

 

 

 

 

 

반응형

WRITTEN BY
CatDarkGame
Technical Artist dhwlgn12@gmail.com

,