Blog

ENGINEERING NOTE

[ Rendering ] 2. Graphics Pipeline

해당 포스팅은 이상윤님께서 공유해주신 2025 TA Campus 수업 문서 링크를 바탕으로 작성했습니다. 0. 들어가며 게임을 개발하다보면 화려한 그래픽을 책임지는 TA라는 직군에 대해 항상 듣곤 합니다. 하지만 막상 관련 직무에 대한 인사이트나 공부를 하고 싶어도

rendering
해당 포스팅은 이상윤님께서 공유해주신 2025 TA Campus 수업 문서 링크를 바탕으로 작성했습니다.

2. Graphics Pipeline

렌더링의 과정을 이해해야 하는 이유

그래픽스 파이프라인은 컴퓨터가 하나의 화면을 렌더링하기까지 거치는 모든 단계의 흐름을 의미한다.

앞으로 다루게 될 각종 최적화 기법들은 이 파이프라인의 특정 단계에서 발생하는 비용을 줄이기 위한 방법들이다. 따라서 파이프라인을 이해하지 못하면,

  • 어떤 최적화가
  • 어느 단계에서
  • 무엇을 줄여주는지

를 정확히 파악하기 어렵다.

렌더링 과정에서 거치는 모든 단계는 곧 비용이다. CPU에서의 계산, GPU에서의 연산, 메모리 접근까지 하나하나가 모두 프레임 타임에 영향을 준다.

만약 내가 담당하는 작업이 파이프라인의 어느 단계와 관련되어 있는지 알고 있다면, 단순히 “성능이 느리다”가 아니라 어디서 비용이 발생하는지를 기준으로 최적화 방법을 고민할 수 있다.

또한 내가 만든 리소스에서

  • 실질적으로 필요한 부분과
  • 불필요한 부분을 구분할 수 있게 되고

불필요한 비용을 줄여 정말 중요한 영역에 성능 예산을 투자할 수 있다.

결과적으로 그래픽스 파이프라인을 이해하는 것은 비용은 줄이면서, 화면의 퀄리티는 높이기 위한 기반 지식이라고 할 수 있다.

2-1. CPU Works

위에서도 언급했듯이 그래픽스 파이프라인에서 CPU의 주된 역할은 GPU가 렌더링 작업을 수행할 수 있도록 준비하고 지시하는 것이다.

CPU는 직접 화면에 픽셀을 그리지는 않지만, 다음과 같은 작업들을 담당한다.

  • GPU에게 어떤 오브젝트를 어떤 순서로 어떤 상태에서 그릴지에 대한 렌더링 명령(Draw Call) 을 생성하고 전달한다.
  • GPU가 그림을 그리는 데 필요한 메시, 머티리얼, 텍스처와 같은 그래픽 리소스들을 메모리에 준비하고 관리한다.

즉, CPU는 렌더링 작업의 전체 흐름을 제어하며, GPU가 효율적으로 일할 수 있도록 사전 준비를 담당하는 역할을 수행한다.

2-2. GPU Works

GPU는 CPU로부터 전달받은 렌더링 명령을 바탕으로, 정해진 그래픽스 파이프라인을 따라 단계적으로 렌더링 작업을 수행한다.

GPU 내부에서 어떤 단계들이 순서대로 수행되는지 알아보자.

2-2-1. Input Assembler

Input Assembler 단계는 메모리(VRAM)에 저장된 메시 데이터를 읽어와 버텍스 셰이더에 전달할 형태로 준비하는 단계이다.

2-2-2. Vertex Shader

Vertex Shader 단계에서는 Input Assembler에서 준비된 각 정점(Vertex)에 대해 공간 변환과 기본적인 정점 단위 연산을 수행한다.

주된 역할은 3D 공간에 존재하는 정점 좌표를 카메라 기준의 좌표계로 변환하는 것이다.

일반적으로 다음과 같은 변환 과정을 거친다.

  • Local Space → World Space
  • World Space → View Space
  • View Space → Clip Space

이 과정을 통해 정점은 화면에 투영될 준비가 된 좌표로 변환되며, 이후 단계에서 최종적으로 2D 화면 좌표로 변환된다.

Vertex Shader에서의 Culling (Primitive Culling)

Vertex Shader를 거치면 각 정점은 화면에 투영될 준비가 된 좌표로 변환되고, 삼각형(프리미티브) 역시 2D 화면 공간 기준으로 판단할 수 있게 된다.

이 시점에서 한 가지 중요한 질문을 던질 수 있다.

이 삼각형들이 모두 실제로 필요한가?

화면 밖에 완전히 벗어나 있거나, 카메라를 향하지 않는 삼각형까지 모두 이후 단계로 넘긴다면 불필요한 연산 비용이 발생한다.

특히 이후에 실행되는 Pixel(Fragment) Shader는 삼각형이 차지하는 픽셀 수만큼 반복 실행되기 때문에 비용이 매우 크다.

따라서 가능한 한 이 단계에서 필요 없는 삼각형을 미리 제거하는 것이 중요하다.

2-2-3. Tessellation Stages (Hull Shader / Tessellation / Domain Shader)

테셀레이션은 기존의 삼각형(프리미티브)을 상황에 따라 더 잘게 쪼개어 기하학적 디테일을 동적으로 증가시키는 기능이다. 일반적으로 사용되지는 않으며, Displacement Mapping과 같은 기술 등을 적용하는 특수한 상황에서 이용한다.

이 과정은 하나의 단계가 아니라, 다음과 같은 세 개의 연속된 단계로 구성되어 있다.

  • Hull Shader
  • Tessellation (Fixed Function Stage)
  • Domain Shader

Hull Shader

Hull Shader는 입력으로 들어온 패치(보통 삼각형 또는 사각형)에 대해 테셀레이션 레벨을 결정하는 단계이다.

Hull Shader는 정점을 직접 쪼개지는 않지만, 이후 단계에서 사용할 분할 기준값(Tessellation Factor) 을 출력한다.

Tessellation

이 단계는 셰이더 코드가 아닌, GPU 내부에 고정된(Fixed Function) 단계이다.

Hull Shader에서 계산된 테셀레이션 레벨을 기준으로 삼각형을 실제로 여러 개의 작은 삼각형으로 분할한다.

즉, 이 단계에서

  • 기존 삼각형 1개 → 여러 개의 작은 삼각형

으로 변환된다.

Domain Shader

Domain Shader는 테셀레이션으로 새롭게 생성된 정점들에 대해 최종 위치를 계산하는 단계이다.

테셀레이션 단계는 보통 왜 스킵되는가?

실제 게임이나 실시간 렌더링 환경에서는 이 단계들을 아예 사용하지 않거나 비활성화하는 경우가 대부분이다.

그 이유는 단순히 “기능이 불필요해서”가 아니라, 비용 대비 효율이 낮은 경우가 많기 때문이다.

테셀레이션은 삼각형을 더 잘게 쪼개어 기하학적 디테일을 증가시키는 기능이다.

하지만 이는 곧

  • 정점(Vertex) 수 증가
  • 프리미티브(Triangle) 수 증가
  • 이후 Rasterization, Fragment Shader 비용 증가

로 이어진다.

즉, 디테일을 얻는 대신 파이프라인 전반의 연산 비용이 눈에 띄게 증가한다.

2-2-4. Rasterization

Rasterization은 3D 공간에서 처리되던 프리미티브(삼각형)를 2D 화면 위의 픽셀(Fragment) 후보들로 변환하는 단계이다.

이 단계부터 GPU는 "어디에 삼각형이 걸치는가?"를 기준으로 본격적인 화면 단위 연산을 시작한다.

Rasterization 단계에서는 다음과 같은 작업이 수행된다.

  • 삼각형이 화면의 어떤 픽셀 영역을 덮는지 계산
  • 삼각형 내부에 해당하는 픽셀(Fragment) 생성
  • 각 Fragment에 대해 정점 데이터(Vertex Shader 결과)를 보간

2-2-5. Pixel Shader (Fragment Shader)

Pixel Shader(Fragment Shader)는 Rasterization으로 생성된 각 Fragment(픽셀 후보)에 대해 최종 색상(Color)을 계산하는 단계이다.

Pixel Shader에서는 다음과 같은 작업들이 수행된다.

  • 픽셀 색상 계산
  • 텍스처 샘플링
  • 라이팅 연산
  • 노멀, UV, 머티리얼 정보 처리
  • 각종 이펙트 계산

따라서 이 단계는 앞선 Vertex Shader보다 훨씬 복잡하고 무거운 연산이 들어간다.

Vertex Shader Cost VS Pixel Shader Cost

해상도가 1080p라고 가정해보자. 화면에는 가로 1920, 세로 1080 픽셀이 존재하므로 한 프레임에 약 200만 개의 픽셀을 그려야 한다.

이때 화면의 절반을 덮는 커다란 이펙트나 지형(Terrain)을 렌더링한다면 어떨까? 최소 100만 개 이상의 픽셀을 칠해야 한다는 의미가 된다.

앞서 예시로 들었던 것처럼,정점이 8개인 메시라면 Vertex Shader는 8번만 실행된다. 하지만 화면을 넓게 차지하는 오브젝트라면 Pixel Shader는 수십만 ~ 수백만 번 실행된다.

예를 들어,

  • Vertex Shader: 8번 실행
  • Pixel Shader: 약 50만 번 실행

같은 연산을 사용하더라도, 실제로 GPU에 걸리는 부담은 비교 자체가 불가능할 정도로 차이가 난다.

버텍스 셰이더 비용이 얼마나 작은지 체감이 잘 안 될 수도 있다.

조금 극단적인 예시이긴 하지만, 실제 프로젝트 기준으로 생각해보면 이해가 쉽다.

지금까지 작업하면서 본 메시 중 정점 수가 가장 많았던 경우는 얼마나 될까?

아무리 많아도 보통 수천 ~ 많아야 만 단위일 것이다. 즉, Vertex Shader는 많아봐야 수만 번 정도 실행된다.

반면 Pixel Shader는 다르다. 화면의 절반만 차지하는 삼각형 하나를 그리더라도 최소 수십만 ~ 백만 번 이상 실행된다.

이 차이는 단순 계산으로도 100배 이상이다.

2-2-6. Output Merger

그래픽 파이프라인의 마지막 단계는 Output Merger다. 앞선 단계들을 거치며 우리는 이미 “어느 픽셀에 무슨 색을 칠할지”까지 모두 계산해 두었다. 하지만 실제 화면에 그릴 때는 한 가지 문제가 남아 있다.

우리는 보통 오브젝트 하나만 그리지 않는다. 큐브를 그리고, 그 뒤에 구를 그리고, 그 위에 반투명한 이펙트까지 겹쳐서 그린다. 즉, 여러 오브젝트가 같은 픽셀을 차지하려는 상황이 발생한다.

Output Merger는 이때

“이 픽셀을 정말 화면에 남길 것인가?”를 최종적으로 결정하는 단계다.

Stencil Test & Depth Test

먼저 Stencil Test가 수행된다. 스텐실 버퍼를 이용해 “이 영역 안에 들어오는 픽셀만 통과시킨다”와 같은 조건을 걸 수 있다. 예를 들어 특정 하트 모양 영역 안에 들어오는 픽셀만 살리고 싶다면, 그 영역 밖의 픽셀들은 여기서 바로 제거된다.

그 다음으로는 Depth Test(Z Test)가 진행된다. 카메라에 더 가까운 픽셀만 남기고, 뒤에 있어서 가려지는 픽셀들은 버려진다.

불투명(opaque) 오브젝트는 보통 여기까지만 처리된다.

투명 오브젝트와 오버드로우

문제는 반투명(Transparent) 오브젝트다.

반투명한 물체는 “가려진다”가 아니라 뒤에 있는 픽셀들과 섞여 보여야 한다. 그래서 깊이 테스트로 바로 버릴 수 없고, 뒤에 있는 픽셀들이 모두 계산된 후에 그 위에 덧칠해야 한다.

이때 발생하는 것이 오버드로우(Overdraw)다.

예를 들어,

화면의 절반을 덮는 반투명 이펙트가 하나 있으면 → Pixel Shader는 약 100만 번 실행된다.

같은 크기의 반투명 이펙트가 3개 겹치면? → 100만 × 3 = 300만 번 실행된다.

그래서 흔히 “오버드로우는 비싸다.”고 이야기한다.

이미 비싼 Pixel Shader를 겹쳐진 횟수만큼 다시 돌려야 하기 때문이다.

Early-Z Test

여기서 한 가지 의문이 들 수 있다.

“이미 Pixel Shader까지 다 돌려놓고 그 다음에 픽셀을 버리는 건 너무 아깝지 않나?”

그래서 현대 GPU는 불투명 오브젝트에 한해서 Pixel Shader를 실행하기 전에 미리 Depth Test / Stencil Test를 수행한다.

이 최적화를 Early-Z Test라고 한다.