프로젝트 진행하면서 냉동 액션 쉐이더가 패스에서 버텍스 얼마나 쓰는지 알고 싶어서 찾아보다가 디버깅해볼 수 있는 Frame Debugger을 알게되었습니다.
레퍼런스 영상은 아래와 같습니다.
https://youtu.be/w14yjBlfNeQ?si=d8jfxsjaqnliI8hU
그래픽스 렌더링과 프레임 디버거 기초
렌더링 (Rendering): 3D 모델 데이터(정점, 텍스처 등)를 사용해서 화면에 2D 이미지를 만들어내는 과정입니다.
게임이나 그래픽 애플리케이션에서 우리가 보는 화면은 이러한 렌더링 과정을 통해 만들어집니다.
CPU와 GPU: 렌더링 과정에서는 CPU와 GPU가 협력합니다.
CPU (Central Processing Unit): 어떤 물체를 어디에, 어떻게 그릴지 결정하고, 렌더링 명령(Command)을 만들어 GPU에 전달하는 역할을 주로 합니다.
GPU (Graphics Processing Unit): CPU로부터 받은 명령을 바탕으로 실제 복잡한 그래픽 연산을 수행하여 픽셀 색상을 계산하고 화면에 이미지를 그리는 역할을 합니다. 병렬 처리에 특화되어 있습니다.
프레임 디버거 (Frame Debugger): 특정 시점(프레임)에 CPU가 GPU에게 보낸 모든 명령들과 GPU의 상태를 캡처하여 보여주는 도구입니다. RenderDoc, PIX, Xcode Frame Debugger 등이 있습니다. 개발자는 이를 통해 렌더링 파이프라인의 각 단계를 자세히 들여다보고, 문제가 되는 부분을 찾아내거나 최적화할 부분을 파악할 수 있습니다. 버텍스가 어떻게 변환되고 그려지는지 추적하는 데 매우 유용합니다.
렌더 패스 (Render Pass) 이해하기
렌더링 명령의 묶음: 최신 그래픽스 API(Vulkan, DirectX 12, Metal 등)에서는 효율성을 위해 관련된 렌더링 명령들을 '렌더 패스'라는 단위로 묶어서 처리하는 경우가 많습니다.
렌더 패스는 "어떤 렌더 타겟(결과 이미지가 저장될 메모리 버퍼, 예를 들어 화면이나 특정 텍스처)에 그릴 것인가?" 그리고 "그 렌더 타겟을 어떻게 시작하고(예: 특정 색으로 초기화) 어떻게 끝낼 것인가?"를 정의합니다.
상태 설정: 하나의 렌더 패스 내에서는 일반적으로 동일한 렌더 타겟 설정을 공유하며, 관련된 그리기 명령(Draw Call)들이 이어서 실행됩니다.
SetPass Call
명령의 시작: BeginRenderPass는 CPU가 GPU에게 "이제 새로운 렌더 패스를 시작할 거야"라고 알리는 명령입니다.
GPU 준비시키기: 이 호출을 통해 CPU는 GPU에게 해당 렌더 패스에서 사용할 렌더 타겟(들), 초기화 방법(예: 배경색), 깊이/스텐실 버퍼 설정 등 렌더링 환경 설정을 전달합니다.
작업 지시: SetPass Call이후에 오는 그리기 명령(Draw Call)들은 여기서 설정된 환경(렌더 타겟, 상태 등) 위에서 실행됩니다.
즉, "이 설정대로 다음 그리기 작업들을 수행해!"라는 지시의 시작점인 셈입니다.
SetPass Call 자체는 버텍스 데이터를 직접 다루는 명령은 아닙니다. 하지만 이 호출은 버텍스 셰이더를 포함한 후속 그리기 명령(Draw Call)들이 어떤 환경(렌더 타겟, 뷰포트, 기타 상태)에서 실행될지를 결정합니다.
디버깅의 핵심: 프레임 디버거에서 버텍스 관련 문제를 분석할 때, 문제가 발생한 그리기 명령(Draw Call)이 어떤 렌더 패스에 속해 있는지 파악하는 것이 매우 중요합니다. SetPass Call이 렌더 패스의 시작을 알리는 중요한 마커 역할을 합니다. 특정 버텍스가 이상하게 보이거나 잘못된 위치에 그려진다면, 해당 Draw Call이 속한 Pass의 설정(SetPass Call에서 지정된)부터 확인하여 렌더 타겟 설정이나 관련 상태가 올바른지 검토해야 합니다.
Draw Call
드로우콜은 배치(Batch)라고도 합니다. '배치(Batch) 수를 줄이는 최적화'는 결국 '화면에 무엇을 그리는가?'와 연결됩니다.
위의 사진은 유니티엔진에서 사용되는 다양한 렌더러 컴포넌트(Renderer Components)들을 볼 수 있습니다. 해당 컴포넌트들은 씬(Scene)에 있는 게임 오브젝트(Game Object)가 화면에 어떻게 그려질지를 담당하며, 렌더러 컴포넌트를 가진 게임 오브젝트들이 배칭의 대상이 됩니다.
렌더러와 배칭의 관계 설명
CPU가 GPU에게 내리는 그리기 명령(Draw Call, 즉 배치)은 바로 이 렌더러 컴포넌트들을 처리하기 위한 것입니다. 따라서 씬에 렌더러 컴포넌트가 많고, 각 컴포넌트가 사용하는 머티리얼(Material)이나 설정이 다르면 배치 수가 증가하게 됩니다.
결국, 배치 수를 줄이기 위한 최적화는 씬에 있는 이러한 렌더러 컴포넌트들을 어떻게 효율적으로 관리하는가의 문제입니다.
같은 머티리얼을 최대한 공유하도록 하고 (예: 텍스처 아틀라스 사용), 스태틱 배칭이나 GPU 인스턴싱 같은 엔진 기능을 적극적으로 활용하는 것이 중요합니다.
Frame Debugger 분석
Frame Debugger는 렌더링 파이프라인을 시각적으로 분석하고 최적화 포인트를 찾는 데 매우 강력한 도구입니다.
상단탭 Window > Analysis > Frame Debugger로 창을 열 수 있습니다.
이번에는 특정 커스텀 셰이더(`CustomFX/Ice`)를 사용하는 배치를 Frame Debugger로 분석하는 과정을 살펴보겠습니다.
위 사진은 Frame Debugger에서 특정 프레임의 렌더링 이벤트들을 보여줍니다.
왼쪽에는 이벤트 목록이 계층 구조로 나타나고, 오른쪽에는 선택된 이벤트(#71 SRP Batch)의 상세 정보가 표시됩니다.
먼저 선택된 배치(#71)의 기본 정보를 확인합니다.
RenderTarget:'_CameraNormalsTexture_...`
이 배치는 화면에 직접 그리는 것이 아니라, 카메라의 Normal 텍스처를 생성하는 패스(아마도 Deferred Shading의 G-Buffer 생성 단계나 별도의 Normal Prepass)에 그려지고 있음을 알 수 있습니다. 렌더링 결과가 의도한 텍스처에 올바르게 쓰이는지 확인하는 시작점입니다.
렌더링 상태 (Render States):
`ZClip: True`, `ZTest: LessEqual`, `ZWrite: On`
-> 깊이 버퍼를 사용하고, 기존 픽셀보다 가깝거나 같은 경우에만 그리며, 자신의 깊이 값을 기록합니다. 일반적인 불투명 오브젝트의 설정입니다.
`Cull: Back`: 뒷면 컬링이 활성화되어 있습니다. 얼음 메쉬의 뒷면은 그리지 않습니다.
`Blend Color/Alpha: One / Zero`, `BlendOp Color/Alpha: Add`: 블렌딩이 비활성화된 상태(덮어쓰기)입니다.
`CustomFX/Ice` 셰이더가 반투명 효과를 의도했다면 이 부분이 잘못 설정되었을 수 있습니다.
(하지만 Normal 텍스처 생성 패스라면 불투명으로 처리하는 것이 일반적입니다.)
`ColorMask: RGBA`: 모든 컬러 채널에 쓰기 작업을 수행합니다.
이제 드로우 콜 및 배치 정보 분석해봅시다
Draw Calls: 7 이 하나의 SRP 배치는 내부적으로 7개의 개별 드로우 콜을 포함하고 있습니다. 이는 7개의 다른 오브젝트(또는 메쉬 파트)가 이 배치에서 그려진다는 의미입니다.
Vertices: 12026, Indices: 47076
이 배치에서 처리되는 총 정점 및 인덱스 수입니다. 이 수치가 과도하게 높다면 메쉬 최적화나 LOD(Level of Detail) 적용을 고려해볼 수 있습니다.
Batch cause: SRP: Node use different shader keywords
Frame Debugger는 주로 이부분을 통해 최적화할 때 판단할 수 있습니다.
사진에서의 메시지는 SRP Batcher가 여러 오브젝트를 하나의 배치로 묶으려고 시도했지만, 각 오브젝트(또는 노드)가 서로 다른 셰이더 키워드(Shader Keywords) 조합을 사용하고 있기 때문에 이 배치가 생성되었거나 혹은 완전히 하나의 거대 배치로 묶이지 못했음을 나타냅니다.
분석 포인트 `Keywords` 섹션을 확장하여 어떤 키워드들이 활성화되어 있는지 확인해야 합니다. 만약 불필요하게 다양한 키워드 조합이 사용되고 있다면 (예: 머티리얼 설정 실수, 코드 로직 문제 등), 키워드 수를 줄여 배칭 효율을 개선할 여지가 있습니다. 예를 들어, 특정 기능(예: 반사 효과)을 켜고 끄는 키워드가 여러 오브젝트에서 다르게 설정되어 있다면, 이를 통일하거나 셰이더 로직을 변경하여 키워드 의존성을 줄이는 것을 고려할 수 있습니다.
위에서 우리는 이 배치의 생성 원인(Batch cause)이 "SRP: Node use different shader keywords" 임을 확인했습니다. 즉, SRP Batcher가 여러 오브젝트를 하나의 배치로 묶으려 했지만, 사용된 셰이더 키워드 조합이 달라서 현재의 배치(#71)로 나뉘었다는 뜻입니다. 이제 키워드 섹션을 자세히 살펴보겠습니다.
`_USE_OUTLINE_ON` (활성화됨, 범위: Local):
이 키워드는 이름으로 보아 외곽선(Outline) 렌더링 기능을 켜는 역할을 하는 것으로 보입니다.
범위(Scope)가 'Local'로 되어 있습니다. 이는 이 키워드가 각 머티리얼 인스턴스별로 다르게 설정될 수 있음을 의미합니다.
배칭 영향: 만약 `CustomFX/Ice` 셰이더를 사용하는 여러 오브젝트(예: `seahorse`, `merman` 등) 중 일부는 외곽선이 켜져 있고 일부는 꺼져 있다면, SRP Batcher는 이들을 하나의 배치로 묶을 수 없습니다. 키워드 조합이 다르기 때문입니다. 이것이 바로 "Node use different shader keywords" 메시지가 나타난 가장 유력한 원인일 수 있습니다.
`_WRITE_RENDERING_LAYERS` (활성화됨, 범위: Global):
이 키워드는 유니티의 렌더링 레이어(Rendering Layers) 기능과 관련된 것으로 보입니다. 프래그먼트 셰이더(fs)에서 사용되며, 범위가 'Global'입니다. 배칭 영향: 범위가 'Global'이므로, 이 키워드 자체는 보통 개별 오브젝트별로 달라지지 않습니다. 따라서 이 키워드가 직접적으로 오브젝트 간 배치를 깨뜨리는 원인일 가능성은 낮습니다. (다만, 렌더링 파이프라인 설정에 따라 간접적인 영향은 있을 수 있습니다.)
우선 `CustomFX/Ice` 셰이더를 사용하는 여러 오브젝트들이 실제로 `_USE_OUTLINE_ON` 키워드를 다르게 설정하고 있는지 확인합니다.
사진처럼 use Outline 필드를 다 체크했더군요
최적화 방안
1. 키워드 통일: 만약 모든 오브젝트에 외곽선이 필요 없거나, 또는 모든 오브젝트에 동일하게 외곽선을 적용해도 된다면, 머티리얼 설정을 통일하여 `_USE_OUTLINE_ON` 키워드 상태를 같게 만들어 줍니다. 이렇게 하면 SRP Batcher가 더 많은 오브젝트를 하나의 배치로 묶어 CPU 부하를 줄일 수 있습니다.
2. 셰이더 분리 또는 수정: 만약 오브젝트별로 외곽선 유무가 반드시 달라야 한다면, 이로 인한 배치 분리는 불가피할 수 있습니다. 하지만 이 비용을 인지하고 있어야 합니다. 경우에 따라 외곽선 기능이 없는 별도의 셰이더(`CustomFX/Ice_NoOutline` 등)를 만들어 사용하는 것을 고려해 볼 수도 있습니다.
3. 셰이더 코드 검토: `_USE_OUTLINE_ON` 키워드가 정말 필요한 경우에만 활성화되도록 셰이더 코드나 관련 스크립트 로직을 점검합니다.
이제 Frame Debugger의 다른 항목도 살펴봅시다.
Meshes: seahorse, merman, FXSM_Ice_New05
이 배치에는 `seahorse`, `merman`, `FXSM_Ice_New05` 라는 이름의 메쉬들이 포함되어 있습니다. 서로 다른 메쉬들이지만 동일한 `CustomFX/Ice` 셰이더와 머티리얼 속성 (키워드 제외)을 공유하여 SRP Batcher에 의해 하나의 배치로 묶인 것으로 보입니다.
다음으로 셰이더 및 텍스처 확인해봅니다
Used Shader: CustomFX/Ice 현재 사용 중인 셰이더가 `CustomFX/Ice` 임을 보여줍니다.
LightMode: DepthNormals (Pass: DepthNormals (4))
이 배치는 `DepthNormals` 라는 LightMode 태그를 가진 셰이더 패스를 사용하고 있습니다. 이는 주로 깊이와 노멀 정보를 별도로 렌더링하는 패스에서 사용됩니다. (Deferred Shading의 G-Buffer 패스 등)
Textures: `_BackIceTex` (vs), `_NormalTex` (fs), `_VertexOffsetTex` (vs) 세 개의 텍스처가 사용됩니다.
각 텍스처가 어느 셰이더 스테이지(vs: Vertex Shader, fs: Fragment Shader)에서 사용되는지 알 수 있습니다. 텍스처 크기(512x512, 256x256)와 포맷(DXT1)도 확인 가능합니다. 텍스처 메모리 사용량이나 압축 품질이 적절한지 검토할 수 있습니다. (예: Vertex Offset 텍스처는 더 저해상도여도 괜찮을 수 있습니다.)
이 `CustomFX/Ice` 셰이더 배치를 분석한 결과, 몇 가지 포인트를 알아낼 수 있었습니다.
1. 배치 원인(Shader Keywords): 가장 주목해야 할 부분입니다. `Keywords` 섹션을 상세히 분석하여 불필요한 키워드 조합을 줄일 수 있는지 확인합니다. 이것이 배칭 성능 개선의 핵심일 수 있습니다.
2. 렌더링 상태: 현재는 불투명 오브젝트 설정이며, Normal 텍스처 생성 패스에 적합해 보입니다. 만약 다른 패스(예: Forward 렌더링)에서 반투명 효과를 의도했다면, 해당 패스의 배치를 찾아 블렌딩 설정을 확인해야 합니다.
3. 셰이더 로직 및 텍스처: `CustomFX/Ice` 셰이더 코드(특히 `DepthNormals` 패스 부분)를 검토하여 비효율적인 연산은 없는지, 텍스처 샘플링은 최적화되어 있는지 확인합니다. 사용되는 텍스처들의 해상도와 포맷이 적절한지도 평가합니다.
4. 성능 측정: Frame Debugger에서 이 배치가 소요하는 GPU 시간을 확인하여 실제 성능 병목인지 판단합니다. 병목이라면 위의 분석 포인트들을 바탕으로 최적화를 진행합니다.
이러한 단계적인 분석을 통해 Frame Debugger에서 특정 셰이더 배치의 동작을 이해하고, 문제점이나 최적화 기회를 찾아낼 수 있습니다.
다음 글에서는 배칭 기법들(스태틱 배칭, GPU 인스턴싱, 텍스처 아틀라스 등)을 실제로 어떻게 적용하는지 구체적인 예시와 함께 살펴보겠습니다.
같이 읽어보시면 좋은글
https://chulin28ho.tistory.com/777
Depth Texture를 언제 그릴까? Depth Prepass / After Opaque
쉽게 설명해주시려고 노력해주신 고정석님께 감사 인사를 드립니다. Depth(깊이) Depth란 .. Z 버퍼라고도 불리던 그것입니다. 게임하면서 깊이값을 알아야 할 때가 있지요? 그게 depth입니다. 이게
chulin28ho.tistory.com
https://darkcatgame.tistory.com/139
[리메이크 예정] URP 드로우콜 최적화 이론 정리
이 포스팅에 잘못된 내용이 있고 내용도 난잡합니다, 추후 다시 정리할 예정입니다. 그 전에 맥락 참고용으로 봐주세요. Draw Call(드로우콜)이란? GPU에게 오브젝트를 화면에 그리라고 명령하는 것
darkcatgame.tistory.com
'유니티 > 유니티 관련 지식' 카테고리의 다른 글
Unity Physics System - 충돌 쿼리와 콜라이더 시스템 차이 (0) | 2025.01.28 |
---|---|
머티리얼을 위한 텍스처 사이트 모음 (0) | 2024.11.25 |
유니티 오브젝트 폴링, 더 좋은거 쓰자! Better Object Pooling (0) | 2024.11.17 |
유니티 에셋 다운로드 폴더 변경 - 윈도우 (2) | 2024.11.15 |