CS/그래픽스

[Rendering Optimization] Stage 0 : Application Stage (CPU Strategy)

tae-woong 2026. 1. 20. 18:42

개발자가 100% 제어 가능한 영역입니다.(유니티 or 언리얼 같은 엔진에서 진행)

 

이곳은 개발자의 코드 설계(Culling, Batching 등)에 따라 성능이 천지차이로 갈리는 곳이므로, 가장 적극적인 최적화가 필요합니다.

 

 

개요

이 단계는 CPU의 영역(Application Stage)입니다.

 

게임 로직, 물리, AI 등을 처리한 후, 렌더링 파이프라인으로 데이터를 넘기기 직전에 수행하는 "전처리 및 선별 과정"입니다.

 

여기서 개발자가 어떻게 최적화하느냐에 따라 GPU가 헛고생(Overdraw)을 할지, 효율적(High Performance)으로 일할지가 결정됩니다.

 

<핵심 목표>

  1. Draw Call Reduction : CPU가 GPU에게 명령을 내리는 횟수와 통신 비용을 최소화합니다. (Batching)
  2. Culling (Visibility Determination) : 화면에 보이지 않을 물체는 아예 GPU로 보내지 않습니다.

 

최적화 방법

1. Frustum Culling (절두체 컬링)

  • 설명 : 가장 기초적이고 필수적인 컬링입니다. CPU가 매 프레임 모든 렌더러(Renderer)의 경계 박스(Bounding Box)가 카메라의 시야 절두체(View Frustum) 내부에 있는지, 걸쳐 있는지, 외부에 있는지를 수학적으로 계산합니다. 시야 밖의 물체는 렌더링 명령 리스트에서 즉시 제외됩니다.
  • 이후 단계에 효과 :
    • Pipeline 전체 : GPU가 처리해야 할 데이터 자체가 '0'이 됩니다. 정점(Vertex) 처리부터 픽셀(Pixel) 처리까지 모든 부하를 원천 차단하는 가장 가성비 높은 최적화입니다.
  • 장점 :
    • 무비용 고효율 : 대부분의 상용 엔진(Unity, Unreal)에서 기본적으로 활성화되어 있으며, 화면 밖 물체를 그리는 낭비를 확실하게 막아줍니다.
  • 단점 :
    • CPU 연산 비용 (Culling Overhead) : 맵에 물체(GameObject)가 수만 개 이상 존재하면, 단순히 "이게 화면 안에 있나?"를 검사하는 CPU 연산 비용 자체가 병목이 될 수 있습니다. (해결책: Culling Group API 사용 등)
  • ※ 헷갈리기 쉬운 포인트 : Frustum Culling(0단계) vs Backface Culling(5단계)
    • Frustum Culling (0단계/CPU) : "카메라 시야 범위 밖(화면 밖)에 있어서 안 보냄" (물체 단위, 아예 명령 안 내림)
    • Backface Culling (5단계/GPU) : "물체의 뒷면이라서 안 그림" (삼각형 단위, 명령은 내렸으나 GPU가 무시함)

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

  • 설명 : Frustum Culling은 "시야 안"에 있는 것은 다 그립니다. 하지만 "시야 안에는 있지만, 큰 벽에 가려져 안 보이는 물체"는 여전히 렌더링되는 문제가 있습니다. Occlusion Culling은 이를 해결하기 위해, 맵 데이터를 미리 분석(Bake)하여 가려진 물체를 CPU 단계에서 렌더링 목록에서 제외하는 기법입니다.
  • 이후 단계에서 효과 :
    • Stage 5 (Rasterization) & Stage 6 (Pixel Shader) : 어차피 그려봤자 앞에 있는 벽 때문에 덮어씌워질(Overdraw) 픽셀들에 대한 연산을 사전에 차단합니다.
  • 장점 :
    • 실내/도심 맵 최적화 : 복잡한 미로, 건물 내부, 빌딩 숲 등 가려지는 오브젝트가 많은 환경에서 프레임 레이트를 비약적으로 상승시킵니다.
  • 단점 :
    • 데이터 베이킹 필수 : 맵의 구조(Static Object)를 미리 분석해서 데이터를 저장해야 하므로, 빌드 용량과 메모리를 추가로 사용합니다.
    • 동적 물체 제한 : 움직이는 물체(Dynamic)가 가리는 상황은 실시간 계산 비용이 너무 비싸서 보통 적용하지 못합니다.
  • ※ 헷갈리기 쉬운 포인트 : Occlusion Culling(0단계) vs Early-Z(5단계)
    • Occlusion Culling (0단계/CPU) : "벽 뒤에 있어서 아예 명령 안 보냄" (큰 덩어리 단위, 1차 필터링)
    • Early-Z (5단계/GPU) : "픽셀을 찍으려고 깊이를 보니 앞에 뭐가 있어서 스킵함" (픽셀 단위, 2차 필터링)
    • 결론 : Occlusion Culling이 큰 덩어리를 1차로 거르고, 거기서 놓친 미세한 부분은 Early-Z가 막아주는 상호보완 관계입니다.

3. Static Batching (정적 배칭)

  • 설명 : 움직이지 않는(Static) 물체들의 메쉬를 빌드 시점(Pre-bake) 혹은 로딩 시점에 월드 좌표 기준으로 변환하여, 하나의 거대한 메쉬(Big Mesh)로 합쳐두는 기법입니다. 런타임에 CPU는 "이 큰 덩어리 하나 그려라"라고 명령(Draw Call)을 딱 한 번만 내립니다.
  • 이후 단계에서 효과 :
    • Stage 1 (IA) : 수백 번의 Draw Call 요청이 1번으로 줄어들어, CPU와 GPU 사이의 통신 병목(Driver Overhead)이 해소됩니다.
  • 장점 :
    • 런타임 CPU 프리 : 버텍스 변환 연산을 미리 다 해놓기 때문에, 게임 실행 중에는 CPU 연산 비용이 거의 '0'에 가깝습니다. 배경 최적화의 정석입니다.
  • 단점 :
    • 메모리 사용량 증가 (Memory Overhead) : 동일한 나무 모델이라도 위치가 다르면, 그 위치에 맞춰 변환된 데이터를 별도로 저장해야 합니다. 맵이 클수록 메모리 사용량이 급증할 수 있습니다.
    • Culling 효율 저하 : 거대한 덩어리로 합쳐졌기 때문에, 그 덩어리의 일부분만 화면에 보여도 전체가 다 렌더링 처리될 수 있습니다.
  • ※ 헷갈리기 쉬운 포인트 : Static Batching vs GPU Instancing
    • Static Batching : 메쉬를 물리적으로 합쳐서 메모리를 희생하고 CPU 연산을 아낌. (배경, 건물 등 고유한 위치의 사물들)
    • GPU Instancing : 메쉬와 메터리얼 각 하나로 모두 공유하고 좌표만 배열로 보냄. (풀, 나뭇잎, 타일 등 반복되는 사물들)

4. Dynamic Batching (동적 배칭)

  • 설명 : 움직이는 물체들이라도 버텍스 수가 적다면(보통 300개 미만), 매 프레임 CPU가 이들의 버텍스를 월드 공간(World Space)으로 직접 변환(Transform)하여 하나의 버퍼에 담은 뒤 GPU에 전송하는 기법입니다.
  • 이후 단계에서 효과 :
    • Stage 1 (IA) : 움직이는 물체들에 대해서도 Draw Call을 줄일 수 있습니다.
  • 장점 :
    • 유연성 : 파티클, UI 요소, 작은 투사체 등 움직이는 오브젝트들의 Draw Call을 줄이는 데 유용합니다.
  • 단점 :
    • CPU 오버헤드 (CPU Overhead) : 매 프레임 CPU가 모든 버텍스의 위치를 계산(Transform)해야 합니다. 버텍스 수가 많은 모델에 적용하면, 배칭으로 얻는 이득보다 CPU 연산 부하가 더 커져서 오히려 프레임이 떨어집니다.
    • 현대의 비효율성 : 최신 그래픽스 API(Vulkan, DX12, Metal)나 고성능 기기에서는 Draw Call 비용 자체가 낮아져서, 굳이 CPU를 괴롭히는 이 방식의 효율이 떨어지고 있습니다.
  • ※ 헷갈리기 쉬운 포인트 : Dynamic Batching의 병목 지점
    • 보통 그래픽 처리는 GPU가 하지만, Dynamic Batching은 "CPU가 버텍스 위치 계산"을 한다는 점이 특이점입니다. 따라서 CPU 사용량이 높은 게임에서는 끄는 게 나을 수도 있습니다.

5. GPU Instancing (GPU 인스턴싱)

  • 설명 : 동일한 메쉬(Mesh)재질(Material)을 사용하는 다수의 객체를 그릴 때, CPU가 따로따로 명령하는 대신 "이 메쉬 원본 하나랑, 1000개의 좌표 목록(Buffer) 여기 있다. 한 번에 그려라"라고 패키징해서 보내는 방식입니다.
  • 지금 + 이후에서 단계 효과 :
    • Stage 0 (CPU) : 루프를 돌며 Draw Call을 날리는 오버헤드가 사라집니다.
    • Stage 1 (IA) : 메쉬 데이터 전송량은 1개 분량이고, 가벼운 좌표 데이터만 추가되므로 대역폭 효율이 극대화됩니다.
  • 장점 :
    • 대량 렌더링의 핵심 : 풀, 나무, 총알, 군중 등 동일한 모양의 오브젝트가 떼거지로 나올 때 CPU와 GPU 양쪽 모두에게 최고의 효율을 보여줍니다.
    • 메모리 절약 : Static Batching과 달리 메쉬 데이터를 복제하지 않습니다.
  • 단점 :
    • 조건의 제약 : 반드시 같은 메쉬, 같은 재질이어야 합니다. 텍스처가 다르거나 모델이 다르면 묶을 수 없습니다.
    • 기기 호환성 : 아주 구형 모바일 기기(OpenGLES 3.0 미만 등)에서는 지원하지 않을 수 있습니다.

6. SRP Batcher (Scriptable Render Pipeline Batcher)

  • 설명 : Unity의 URP/HDRP에서 지원하는 최신 기법입니다. Draw Call 횟수를 줄이는 게 아니라, Draw Call 간의 "준비 시간(SetPass Call)"을 줄이는 것에 초점을 맞춥니다. 재질 데이터(Uniforms)는 GPU 메모리의 CBuffer(Constant Buffer)에 미리 상주시켰다가, CPU는 "몇 번 데이터 써라"라고 빠르게 바인딩(Fast Binding)만 바꿔줍니다.
  • 지금 단계에서 효과 :
    • Stage 0 (CPU) : 렌더링 루프에서 CPU가 GPU 설정을 바꾸는 시간(Validation & Setup)을 획기적으로 단축하여 CPU 병목을 해소합니다.
  • 장점 :
    • 압도적인 범용성 : 메쉬가 달라도, "동일한 셰이더 변형(Shader Variant)"을 사용하기만 하면 배칭 효과를 볼 수 있습니다. (Static/Instancing보다 훨씬 적용하기 쉽고 범위가 넓음)
  • 단점 :
    • 셰이더 작성 규칙 : 셰이더 코드 작성 시 변수들을 CBuffer 블록 안에 선언해야 하는 규칙(UnityPerMaterial)을 엄격히 지켜야 작동합니다.
    • Draw Call 수치 유지 : 프로파일러 상에서 Draw Call 숫자는 줄어들지 않습니다. (비용이 싸진 것이지 횟수가 준 게 아님)
  • ※ 헷갈리기 쉬운 포인트 : Draw Call 감소 vs SetPass Call 감소
    • Static/Dynamic/Instancing : 한 번의 명령으로 여러 개를 그리므로 "Draw Call 횟수 자체"가 줄어듭니다.
    • SRP Batcher : Draw Call 횟수는 그대로지만, 명령 사이사이의 "빈 시간(Gap)"이 줄어드는 것입니다.