멈추지 않고 끈질기게

[Graphics] 드로우 콜(DrawCall) 본문

Graphics

[Graphics] 드로우 콜(DrawCall)

sam0308 2023. 2. 20. 09:29

※ 해당 포스팅은 개인의 공부 정리용 글입니다. 틀린 내용이 있다면 추후 수정될 수 있습니다.

※ 해당 포스팅은 Unity 2021.3.15f1 버전을 기준으로 작성되었습니다.

※ 해당 포스팅은 하기 출처들을 참조하였습니다.

- 오지현, 유니티 그래픽스 최적화 스타트업, 비엘북스, 2019

 

 

 

 

이번 포스팅에서는 그래픽스에서 중요한 개념 중 하나인 드로우 콜에 대해 알아보겠습니다.

.1. 드로우 콜(Draw Call)

 렌더링 파이프라인에서 우선 CPU에 의해 오브젝트의 렌더링 필요 여부를 검사하는 과정을 거친 후(컬링), 오브젝트의 렌더링 과정은 GPU에 의해 진행됩니다. 이 때 CPU에서 GPU에 필요한 데이터를 넘겨주며 말 그대로 오브젝트를 화면에 그리게(Draw) 호출(Call)하는 것드로우 콜이라 합니다. 다만 GPU는 독립적으로 작업을 수행하고 있기 때문에, 드로우 콜과 같은 CPU의 명령은 중간에 커맨드 버퍼(command buffer)에 저장되었다가 GPU가 현재 작업을 완료하면 전달됩니다. 

 

그림 1. 커맨드 버퍼(Command Buffer)

 드로우 콜 호출 시 CPU에서 GPU로 오브젝트의 형태(메시), 라이팅 처리 정보(쉐이더), 위치 및 스케일 정보(트랜스폼), 알파 블렌딩 필요 여부 등 다양한 데이터를 전달해야 합니다. 이 때, 이러한 정보들을 해당 디바이스의 그래픽스 칩셋에 맞는 신호로 전달되어야 하며, DirectX와 같은그래픽스 API가 이 과정을 수행하게 됩니다. 신호 변환에 걸리는 시간은 CPU 바운더리의 오버헤드이기 때문에 GPU보다 CPU 성능에 영향을 받으며, CPU 바운더리에서 병목 현상이 생긴다면 드로우콜이 너무 많은지 의심해봐야 합니다.

 

2. 컬링(Culling)

 렌더링 파이프라인 포스팅에서 언급했듯이, 오브젝트를 렌더링 하기 이전에 렌더링 해야할 오브젝트를 걸러내는 과정을 거치며 이를 컬링(Culling) 연산이라 합니다. 렌더링 해야할 오브젝트 자체를 줄여 드로우 콜을 줄이는 것은 물론, 렌더링 시간 자체도 줄어들기 때문에 효율적인 컬링 연산을 수행하는 것은 중요합니다. 컬링 연산에는 프러스텀 컬링, 오클루전 컬링 등이 있으며, LOD 설정을 통해 추가적으로 컬링을 수행할 수 있습니다.

 

1) 프러스텀 컬링(Frustum Culling)

그림 2. 뷰 프러스텀(View Frustum)

 프러스텀 컬링(Frustum Culling)이란 카메라의 뷰 프러스텀을 기준으로 프러스텀 바깥의 오브젝트를 렌더링 대상에서 제외하는 컬링 방식입니다. 위 사진은 유니티에서의 카메라 뷰 프러스텀을 캡쳐한 것이며, 카메라와 가까운 쪽 사각형(하늘색 사각형)을 근거리 클리핑 평면(Near Clipping Plane), 먼 쪽 사각형(빨간색 사각형)을 원거리 클리핑 평면(Far Clipping Plane)이라 합니다. 

 

그림 3. 프러스텀 컬링

 프러스텀 영역이 잘 구분되도록 위에서 찍은 사진입니다. 우측 하단의 메인 카메라에 비치는 화면을 보시면 프러스텀 영역 내에 있는 큐브와 구체만이 보이는 것을 확인할 수 있습니다. 프러스텀 영역 바깥에 있는 원통과 캡슐형 오브젝트는 그려지지 않고 있습니다. 프러스텀 컬링은 가장 기본적인 컬링으로서, 유니티 뿐만 아니라 대부분의 게임엔진에서 자동으로 수행해주는 편입니다. 

그림 4. 클리핑 평면 조절

 물론 자동으로 수행해준다고 해도 사용자는 알 필요가 있습니다. 이전 사진들은 프러스텀이 한눈에 보이도록 카메라 컴포넌트의 Clipping Plane에서 Far 값을 10으로 설정하였었고, 위 사진은  Far 값을 30으로 늘려준 모습입니다. 프러스텀 영역이 늘어나면서 원통 오브젝트도 영역에 들어왔고, 카메라 화면에 노출되는 것을 확인할 수 있습니다. 이렇게 프러스텀 영역을 조절하여 렌더링 대상을 줄임으로서 드로우 콜을 감소시킬 수 있습니다. 단, 프러스텀 영역을 너무 작게 만들 경우 멀리 있는 오브젝트가 부자연스럽게 끊겨 보이니 주의가 필요합니다.

 

2) 오클루전 컬링(Occlusion Culling)

 

그림 5. 오클루더(Occluder)와 오클루디(Occludee)

 오클루전 컬링(Occlusion Culling)이란 한마디로 오브젝트 뒤에 가려진 오브젝트는 렌더링 하지 않는 컬링 방식입니다. 여기서 가리는 쪽의 오브젝트를 오클루더(Occluder), 가려지는 쪽의 오브젝트를 오클루디(Occludee)라고 합니다. 여기서 오클루더의 정보는 사전에 미리 연산해두어야 하고 변동이 없어야 하기 때문에, static(정확히는 occluder static) 오브젝트만이 오클루더가 될 수 있습니다. 유니티의 경우 winodw - Rendering - Occlusion Culling 창에서 Bake를 실행하여 사전에 필요한 연산을 수행하고, 오클루전 컬링을 적용할 수 있습니다. 프러스텀 컬링과 더불어 오클루전 컬링을 적용하면, 카메라에 보이는 오브젝트만 렌더링하며 그 중에서도 다른 오브젝트에 가려지는 오브젝트는 제외함으로서 렌더링 대상이 줄어들고, 결과적으로 드로우 콜이 감소하게 됩니다.

 

 오클루전 컬링 사용 시 주의할 점이 있습니다. 유니티의 경우 Occlusion Culling 창에서 Smaleest Occluder 값을 조절하여 사전 연산 시 셀 단위를 조절할 수 있고, 이 값을 낮출수록 오클루전 컬링의 정밀도는 올라가게 됩니다. 다만 이 값을 높일수록 오클루전 연산에 의해 발생하는 오버헤드가 증가하게 됩니다. 실내와 같이 벽 등에 가려지는 오브젝트가 많은 공간에서는 그래도 효율적이겠지만, 탁 트인 평야와 같은 공간에서는 오클루전 컬링에 의한 오버헤드가 이득보다 더 커질 수도 있습니다. 따라서 게임의 성격에 따라 적절한 값을 찾아 조절해야 할 필요가 있습니다.

 

3) LOD(Level Of Detail)

 

 LOD(Level of Detail)는  오브젝트의 디테일을 레벨에 따라 다르게 표현하는 방식을 말합니다. 다만 뷰 프러스텀 내에 있는 오브젝트라도 카메라에서 멀리 떨어져있어 사실상 화면상에 차지하는 비중이 거의 없다면, 이런 오브젝트를 아예 컬링시키는 수단으로도 사용할 수 있습니다. 유니티의 경우 오브젝트에 LOD Group 컴포넌트를 추가하고, 오브젝트의 메시 렌더러를 드래그&드랍하여 적용할 수 있습니다.

 

그림 6. LOD를 이용한 컬링

 위 사진은 유니티에서 LOD를 이용한 컬링의 예시입니다. 빨간색 구체에 LOD 컴포넌트를 추가한 뒤, LOD 0, Culled를 제외한 나머지 단계를 삭제하고 화면에서의 비중이 20% 이하라면 컬링되도록 했습니다. 뷰 프러스텀 내에 큐브와 구체가 모두 들어와 있지만(구체는 원래 카메라에 보이도록 배치), 우측 하단의 카메라 화면을 보면 구체가 LOD가 적용되어 컬링되고 렌더링 되지 않은 모습을 확인할 수 있습니다. 이렇게 오브젝트에 LOD를 적용함으로서 드로우 콜을 줄일 수 있습니다.

 

 LOD는 오브젝트마다 직접 적용해주어야 하는 방식으로 프러스텀 컬링이나 오클루전 컬링에 비해 근본적인 해결책으로 보기는 어렵지만, 오클루전 컬링과 반대로 시야가 탁 트인 야외 환경에서 적용하기 좋습니다. 위와 같은 컬링 방법들의 특징을 이해하고 실내 환경이 대부분이라면 오클루전 컬링을, 야외 환경이 대부분이라면 LOD를 적극 이용하는 등 만드려는 게임의 성격에 맞게 이용할 줄 알아야겠습니다.

 

3.배칭(Batching)

 렌더링을 위한 상태값 변경 등을 포함한 포괄적 의미의 드로우 콜을 배치(Batch)라 부르며, 원래 여러개의 배치로 처리해야 될 사항을 하나의 배치로 묶는 과정을 배칭(Batching)이라 합니다. 배칭 기법에는 정적 배칭, 동적 배칭, 스프라이트 배칭(2D), GPU 인스턴싱 등이 있습니다.

 배칭에 있어 가장 기본적인 수단은 머티리얼을 통일시키는 것입니다. 위에서 언급한 정적 배칭, 동적 배칭 모두 동일 머티리얼을 사용하는 것이 전제조건이기 때문에, 여러개의 텍스처를 합쳐서 텍스처 아틀라스(texture atlas)로 만들어 여러개의 오브젝트가 하나의 머티리얼을 사용하여 배칭이 효과적으로 이루어지도록 하는 것이 좋습니다.

 

1) 정적 배칭(static batching)

 정적 배칭은 이름 그대로 static 오브젝트들에 적용되는 배칭 기법입니다. 유니티에서는 Edit - Project Settings - Player - Other Settings - Rendering 에서 Static Batching 옵션에 체크하여 적용할 수 있습니다(동적 배칭도 동일합니다). 해당 옵션이 체크된 상태에서 실행 시 동일 머티리얼을 사용하는 static 오브젝트들한 번의 드로우콜(배치)로 그려지게 됩니다. 

 

그림 7. 유니티의 배칭 설정

 아래 사진은 유니티에서 기본 3D 오브젝트들을 생성하고 static으로 체크한 상태입니다. 드로우콜에 영향을 줄 수 있는 shadow 같은 옵션들은 모두 꺼 둔 상태로 실행하였고, 보다시피 1번의 드로우 콜(배치)로 처리되고 있습니다. 모두 디폴트 머티리얼을 사용하고 있기 때문에 정적 배칭이 적용된 것입니다. 

 

그림 8. 정적 배칭(Static Batching)

 정적 배칭은 사실 추가적인 메모리를 필요로 합니다. 정적 배칭의 대상이 되는 오브젝트들의 메시를 하나로 모아 새로운 메시 정보를 만들기 때문입니다. 이로 인해 추가적인 메모리 소모는 있지만, 합쳐진 새로운 메시 정보를 이용해 한번의 드로우콜로 한꺼번에 그려내는 것이 가능하기 때문에 런타임에서 소모되는 시간을 줄일 수 있습니다. 정적 배칭의 대상이 너무 많아 추가 메모리 소모가 너무 크다면 조절해야 할 수도 있습니다. 다만 그런 경우가 아니라면 앞서 언급한 오클루전 컬링, 추후 포스팅에서 다룰 static 오브젝트에만 적용할 수 있는 라이팅 기법을 사용하기 위해서라도 움직일 필요가 없는 배경 오브젝트는 static으로 설정해두는 것이 좋습니다. 

 

2) 동적 배칭(dynamic batching)

 동적 배칭은 static이 아닌 오브젝트들에 적용되는 배칭 기법으로, 정적 배칭에 비해서는 다소 조건이 까다롭습니다.

 

- 동일 머티리얼 사용

- 버텍스 수가 많지 않을 것

- 스킨드 메시(Skinned Mesh)가 아닐 것

 

 동일 머티리얼이란 조건은 정적 배칭에서도 요구하는 부분이지만, 동적 배칭의 경우 버텍스 수가 많거나 스킨드 메시에도 적용할 수 없는 문제가 있습니다. 정적 배칭과 달리 동적 배칭은 버텍스 정보를 모아 합치는 과정을 런타임에서 수행하므로, 동적 배칭의 오버헤드는 프레임 단위로 발생하게 됩니다. 따라서 버텍스 수가 많은 오브젝트나 스키닝 연산을 필요로 하는 스킨드 메시에 적용하면, 드로우 콜 감소로 인한 이득보다 배칭의 오버헤드가 더 커질 수 있기 때문에 적용하지 않는 것입니다.

 

그림 9. 동적 배칭(Dynamic Batching)
그림 10. 동적 배칭 미적용

  그림 9와 10은 유니티에서 각각 static이 아닌 큐브 5개와 구체 5개를 생성해 둔 사진입니다. 큐브의 경우 동적 배칭이 적용되어 1번의 드로우 콜(배치)로 처리되었지만, 구체에는 적용되지 않아 5번의 드로우 콜이 발생했습니다. 이처럼 동적 배칭은 제약이 많고 런타임에 오버헤드를 발생시키기 때문에, 적용/미적용 시 프레임 타임을 비교해보며 적용 여부를 결정하는 것이 좋습니다.  

 

3) 스프라이트 배칭(sprite batching)

 스프라이트 배칭은 2D 게임에서 스프라이트들에 적용되는 배칭 기법이며, 3D 배칭 기법에 비해 조건이 단순한 편입니다. 동적 배칭처럼 버텍스 수에 따른 제약같은 것도 없기 때문에, 유니티에서는 별도 설정 없이 동일한 머티리얼(2D이므로 스프라이트)을 사용한다면 자동으로 적용됩니다. 따라서 2D 게임에서는 스프라이트들을 하나의 큰 이미지에 모아놓고, 사용할때는 스프라이트 모드를 Multiple로 설정하여 별도의 스프라이트로 사용합니다. 또한 유니티에서는 이미 만들어진 스프라이트들을 하나의 이미지로 합치는 스프라이트 아틀라스 기능을 지원합니다.

 

그림 11. 아틀라스 적용한 스프라이트들

 위 사진은 무료 에셋의 스프라이트들을 하나의 아틀라스로 묶은 뒤 설치한 모습입니다. 10개의 스프라이트들을 배치하였지만 한 번의 드로우 콜(배치)로 적용되고 있습니다. 2D 게임의 경우 스프라이트들을 한 데 모아 그린 시트를 이용하거나, 스프라이트 아틀라스를 활용하여 드로우 콜을 크게 줄일 수 있습니다.  

 

4) GPU 인스턴싱(GPU instancing)

 GPU 인스턴싱은 동일 메시를 사용하는 오브젝트들에 적용할 수 있는 기법입니다. 동일 메시를 사용하는 오브젝트들의 트랜스폼 정보를 별도의 버퍼에 저장해두고, 원본 메시 데이터를 이용해 한번에 처리해서 렌더링 하는 방식입니다. 동적 배칭처럼 static이 아닌 오브젝트들에도 적용할 수 있으며, 메시 정보를 새로 생성하는 것이 아니기 때문에 버텍스 수에 따른 제한도 없습니다. 유니티에서는 쉐이더의 Advanced Options에서 Enable GPU Instancing 옵션을 체크하여 적용할 수 있습니다. 

 

그림 12. GPU 인스턴싱(GPU Instancing)

 위 사진은 모든 구체들에 동일 머티리얼을 사용한 뒤, 해당 머티리얼에서 GPU 인스턴싱을 활성화 한 후 실행한 모습입니다. 구체들을 static으로 설정하지 않아서 정적 배칭에 해당하지도 않고, 버텍스 수가 많은 구체라 동적 배칭도 받지 않지만 1번의 드로우 콜(배치)로 처리되는 모습을 확인할 수 있습니다.

 

 

 지금까지 드로우 콜을 줄일 수 있는 방법들에 대하여 알아보았습니다. 다만 이러한 컬링, 배칭 기법에도 어느정도 오버헤드가 발생하므로, 각 방법들의 특징을 이해하고 실제로 적용해보며 만드는 게임에 가장 적합한 수단을 찾는 것이 좋겠습니다.