컨텐츠 검색
셰이더와 렌더링 파이프라인: 3D 데이터가 화면에 그려지기까지

2026. 1. 23. 21:11TA

오늘은 게임 개발을 공부하면서 가장 난해하면서도 흥미로웠던 분야, 렌더링 파이프라인(Rendering Pipeline)에 대해 정리해 보려 합니다.

엔진을 다루다 보면 "왜 셰이더 컴파일이 오래 걸리지?", "왜 반투명 물체가 많으면 렉이 걸리지?" 같은 의문이 생기곤 합니다. 단순히 엔진의 기능을 쓰는 것을 넘어, 내부에서 데이터가 어떤 과정을 거쳐 픽셀로 변환되는지를 이해해야만 최적화와 트러블 슈팅이 가능하다는 것을 느꼈습니다.


1. 셰이더(Shader)란 무엇인가?

과거의 셰이더는 이름 그대로 'Shade(명암)'를 표현하는 도구에 불과했습니다.

하지만 현대 그래픽스에서 셰이더는 GPU 위에서 실행되는 픽셀 단위의 프로그램을 의미합니다.

 

단순히 색을 칠하는 것을 넘어, 이제는 물결의 출렁임, 타오르는 불꽃 파티클, 혹은 카툰풍의 외곽선 처리까지 모든 시각적 연출을 담당하는 핵심 도구가 되었습니다.

 

C++ 코드가 CPU를 위한 것이라면, HLSL 코드는 GPU에게 "이 픽셀을 어떻게 그릴지" 명령하는 지침서라고 할 수 있습니다.


2. 렌더링 파이프라인 (Rendering Pipeline) 전체 공정

3차원 공간의 데이터(정점)를 모니터라는 2차원 평면의 픽셀로 변환하기 위해 GPU에서 단계적으로 수행하는 절차를 렌더링 파이프라인이라고 합니다. 이 과정은 크게 8단계로 나눌 수 있으며, 각 단계별로 발생할 수 있는 병목 현상이 다릅니다.

2.1. Application (CPU 단계)

"렌더링의 시작, 작업 지시서 발송"

파이프라인의 시작은 GPU가 아닌 CPU입니다. 이 단계에서 게임의 로직이 수행되고 무엇을 그릴지 결정합니다.

  • 주요 작업: 충돌 처리, 애니메이션 업데이트, 물리 연산, 그리고 오클루전 컬링(Occlusion Culling)을 통해 보이지 않을 객체를 미리 선별합니다.
  • 핵심: 모든 준비가 끝나면 CPU는 GPU에게 Draw Call을 보냅니다.
  • ⚠️ 성능 이슈 분석: Draw Call 병목
    • 증상: "폴리곤이 적은 단순한 물체라도, 개수가 많아지면 렉이 걸립니다."
    • 원인: 물체 하나를 그릴 때마다 CPU는 GPU에게 명령(Draw Call)을 내리는데, 이 통신 비용(Overhead)이 과다해지면 GPU가 놀고 있어도 프레임이 떨어지는 CPU Bound 상태가 됩니다.
    • 해결:
      1. 배칭(Batching): 여러 개의 메시를 하나의 큰 덩어리로 합쳐서 한 번에 그립니다.
      2. GPU 인스턴싱(Instancing): 동일한 물체(나무, 풀 등)는 한 번의 Draw Call로 수천 개를 복사해 그립니다

2.2 Input Assembler (IA)

"데이터 수집 및 기본 도형 조립"

GPU의 첫 관문입니다. CPU로부터 전달받은 정점(Vertex)과 인덱스(Index) 데이터를 읽어 들여 그래픽 파이프라인이 처리할 수 있는 형태인 프리미티브(Primitive)로 조립합니다.

  • 역할: 점(Point), 선(Line), 삼각형(Triangle) 등의 기본 도형을 구성합니다.
  • 특징: 정점의 연결 순서(Winding Order)를 통해 도형의 앞면과 뒷면을 결정할 기초 정보를 마련합니다.
  • ⚠️ 성능 이슈 분석: 메모리 대역폭 한계
    • 증상: "4K 텍스처를 많이 썼더니 화면이 뚝뚝 끊깁니다(Stuttering)."
    • 원인: GPU 연산 속도 문제가 아니라, 데이터를 VRAM에서 가져오는 대역폭(Bandwidth)이 한계에 도달한 것입니다. 너무 큰 데이터가 버스(Bus)를 막고 있어 연산을 시작조차 못 하는 상황입니다.
    • 해결:
      1. 텍스처 압축: BC7, ASTC 등 효율적인 압축 포맷을 사용합니다.
      2. 밉맵(Mipmap): 멀리 있는 물체는 저해상도 텍스처를 로드하도록 설정합니다.
      3. 텍스처 스트리밍: 필요한 텍스처만 메모리에 올리고 해제하는 스트리밍 설정을 최적화합니다.

2.3. Vertex Shader (VS)

"좌표계 변환의 핵심 (3D → 2D 준비)"

가장 중요한 프로그래머블 셰이더(Programmable Shader) 단계 중 하나입니다. 정점(Vertex) 하나하나의 위치를 계산합니다.

  • 좌표 변환(Coordinate Transformation): 로컬 좌표계에 있는 모델을 월드(World) → 뷰(View) → 클립(Clip) 공간으로 변환합니다.
  • 역할: 3차원 공간의 물체를 카메라 시점으로 옮기고, 원근감을 적용(Projection)하여 화면에 그릴 준비를 마칩니다.
  • ⚠️ 성능 이슈 분석: Vertex Bound
    • 증상 1: "캐릭터가 뼈대 애니메이션(Skeletal Mesh)을 할 때만 느려집니다."
      • 원인: 매 프레임 정점 위치를 뼈대에 맞춰 재계산(Skinning)해야 하므로 연산량이 폭발합니다.
      • ✅ 해결: LOD(Level of Detail)를 적용하여 거리에 따라 정점 수가 적은 모델로 교체합니다.
    • 증상 2: "오브젝트는 적은데 그림자(Shadow)만 켜면 프레임이 반토막 납니다."
      • 원인: 그림자를 만들기 위해 빛의 시점에서 한 번 더 그려야 하므로, 처리해야 할 정점의 양이 2배 이상 늘어납니다.
      • ✅ 해결: 그림자 렌더링 거리(Shadow Distance)를 줄이거나, 동적 그림자 대신 라이트맵(Lightmap)을 굽습니다(Bake).

2.4. Tessellation (선택 단계)

"디테일의 마술, 도형 쪼개기"

LOD(Level of Detail) 기술의 핵심으로, 폴리곤의 개수를 동적으로 늘려 곡면을 부드럽게 표현하는 단계입니다. 다음 3가지 하위 단계로 구성됩니다.

  • ① Hull Shader (HS): 제어점(Control Point)을 받아 표면을 얼마나 잘게 쪼갤지(Tessellation Factor) 결정합니다. (가까운 물체는 많이, 먼 물체는 적게)
  • ② Tessellator: 고정 하드웨어 단계로, HS의 요청대로 실제로 도형을 잘게 분할합니다.
  • ③ Domain Shader (DS): 분할된 정점들의 최종 위치를 계산합니다. 이때 디스플레이스먼트 맵(Displacement Map) 등을 활용해 실제 지형의 높낮이나 질감을 입체적으로 표현합니다.
  • ⚠️ 성능 이슈 분석: Quad Overdraw
    • 증상: "멀리서는 괜찮은데, 물체에 카메라를 가까이 대면 갑자기 느려집니다."
    • 원인: 카메라가 가까워질수록 폴리곤을 기하급수적으로 쪼개기 때문에 연산량이 폭발하거나, 픽셀보다 삼각형이 작아지는 Quad Overdraw 현상으로 효율이 급락합니다.
    • ✅ 해결:
      1. 거리 기반 테셀레이션: 카메라 거리에 따라 분할 횟수(Tessellation Factor)를 제한합니다.
      2. 최대 분할 수 제한: 과도하게 쪼개지지 않도록 Hard Limit을 둡니다.

2.5. Geometry Shader (GS)

"도형의 생성과 파괴"

기존 파이프라인이 정점의 위치만 바꿨다면, GS는 새로운 정점을 생성하거나 삭제할 수 있는 강력한 단계입니다.

  • 활용: 점 하나를 사각형(Billboard)으로 확장하여 파티클 효과를 만들거나, 털(Fur)을 생성하거나, 그림자 볼륨을 계산하는 데 사용됩니다.
  • 주의: 연산 비용이 비싸기 때문에 남용하면 성능 저하의 원인이 될 수 있습니다.

2.6. Rasterizer (RS)

"3D의 수학을 2D의 픽셀로"

수학적 데이터인 정점(Vertex)을 모니터에 출력할 수 있는 픽셀의 후보(Fragment)로 변환하는 단계입니다.

  • Clipping: 화면(절두체) 밖으로 나간 도형을 잘라냅니다.
  • Back-face Culling: 카메라를 등지고 있어 보이지 않는 뒷면을 제거하여 연산량을 절약합니다.
  • Scan Conversion: 삼각형 내부를 채울 픽셀들을 찾아내고, 정점 사이의 데이터를 보간(Interpolation)합니다.
  • 문제 예) "풀(Grass)이나 나무가 많은 숲 맵에서 전체적으로 느립니다."
    • ⚠️ 성능 이슈 분석: 마스크드 오버드로우
      • 증상: "풀(Grass)이나 나무가 많은 숲 맵에서 전체적으로 느립니다."
      • 원인: 나뭇잎 같은 마스크드(Masked) 재질이 겹쳐 있을 때, Early Z-Test가 제대로 작동하지 않아 가려진 잎사귀까지 모두 픽셀화하느라 과부하가 걸린 경우입니다.
      • ✅ 해결:
        1. 가능하면 Masked 대신 불투명(Opaque) 재질을 사용합니다.
        2. 먼 거리의 나무는 Masked 처리가 없는 임포스터(Imposter)나 빌보드로 대체합니다.

2.7. Pixel Shader (PS)

"빛과 색의 결정"

화면에 찍힐 각 픽셀의 최종 색상을 결정하는 단계입니다. 현대 그래픽스에서 가장 부하가 많이 걸리는 곳이기도 합니다.

  • 주요 작업: 텍스처를 읽어오고(Sampling), 조명(Lighting)을 계산하고, 노말 맵이나 그림자 처리를 수행합니다.
  • PBR: 최근 게임에서 쓰이는 물리 기반 렌더링(PBR) 연산도 대부분 이곳에서 이루어집니다. 셰이더 최적화가 필요한 가장 주된 단계입니다.
  • ⚠️ 성능 이슈 분석: Fill-rate 병목
    • 증상: "해상도를 낮췄더니 프레임이 급격히 올랐습니다."
    • 원인: 해상도가 높다는 건 처리해야 할 픽셀 수가 많다는 뜻입니다. 이 경우 픽셀 셰이더의 연산이 너무 복잡하거나 텍스처 샘플링이 과도한 Fill-rate(채움 비율) 병목 상태입니다.
    • ✅ 해결:
      1. 셰이더 코드 최적화: sin, cos, pow 같은 무거운 연산을 줄이거나 근사값(Approximation)을 사용합니다.
      2. 업스케일링 기술: DLSS나 FSR을 사용하여 실제 렌더링 해상도를 낮춥니다.

2.8. Output Merger (OM)

"최종 검수 및 출력"

Pixel Shader가 계산한 색상을 실제로 렌더 타겟(Render Target)에 기록할지 결정하고 합성하는 단계입니다.

  • Depth Stencil Test: 깊이 버퍼(Z-Buffer)를 확인하여 앞에 물체가 있다면 뒤에 있는 픽셀을 버립니다. (불필요한 그리기를 방지)
  • Alpha Blending: 반투명한 물체의 경우, 이미 그려진 배경색과 현재 색상을 섞어서 투명하게 보이도록 처리합니다
  • ⚠️ 성능 이슈 분석: Overdraw (오버드로우)
    • 증상: "연기나 폭발 이펙트(반투명 파티클)가 겹칠 때 프레임이 급락합니다."
    • 원인: 반투명 물체는 뒤의 색과 섞어야 하므로(Alpha Blending) 픽셀을 버리지 못하고 겹친 만큼 모두 계산해야 합니다. Read-Modify-Write 작업이 반복되며 발생하는 전형적인 Overdraw 현상입니다.
        • Read-Modify-Write: GPU는 현재 픽셀의 색을 읽어오고(Read), 반투명 수식을 계산해 섞고(Modify), 다시 메모리에 쓰는(Write) 무거운 작업을 수행합니다.
    • ✅ 해결:
      1. 파티클 컷아웃(Cutout): 투명한 영역이 많은 사각형 텍스처 대신, 모양에 맞춰 잘라낸 메시를 사용해 그릴 면적을 줄입니다.
      2. 이미터 제한: 겹치는 파티클의 최대 생성 개수를 제한합니다.


3. 좌표계 변환: 3D가 2D가 되는 마법 (VS 단계의 핵심)

Vertex Shader 단계에서 일어나는 좌표 변환은 수학적으로 가장 까다로운 부분이지만, 원리는 '사진 촬영'과 같습니다.

  1. 월드 변환 (World): "배우 배치"
    • 0,0,0에 있는 모델을 게임 세상의 특정 위치로 옮기고(이동), 돌리고(회전), 키웁니다(스케일).
  2. 뷰 변환 (View): "카메라 위치 선정"
    • 카메라를 원점으로 고정하고, 세상 전체를 움직여서 카메라 기준의 상대 좌표로 만듭니다. (D3DXMatrixLookAtLH)
  3. 투영 변환 (Projection): "렌즈 왜곡(원근감)"
    • 원근감을 주기 위해 피라미드 모양의 시야(Frustum)를 직육면체로 찌그러뜨립니다. 이때 깊이(z) 정보를 w 성분에 저장해 둡니다. (D3DXMatrixPerspectiveFovLH)
  4. 원근 나눗셈 (Perspective Divide):
    • 저장해 둔 w 값으로 x, y, z를 나눕니다. 멀리 있을수록 값이 작아져 중앙으로 모이게 됩니다. 이것이 소실점의 원리입니다.

4. HLSL과 UV: 셰이더의 언어

이론을 구현하기 위해서는 도구가 필요합니다.

  • HLSL (High Level Shader Language): DirectX 환경에서 셰이더를 작성하는 언어입니다. C++과 유사하지만 float3, float4 같은 벡터 연산에 최적화되어 있습니다.
  • UV (Texture Coordinate): 3D 모델(Mesh)에 2D 이미지(Texture)를 입히기 위한 좌표계입니다. X, Y, Z와 구분하기 위해 U, V를 쓰며, 0~1 사이의 비율로 위치를 지정합니다. 텍스처가 찌그러지거나 밀린다면 이 UV 좌표가 잘못된 경우가 많습니다.

5. 렌더링 기법: Forward vs Deferred

언리얼 엔진 프로젝트 세팅에서 항상 고민하게 되는 두 가지 렌더링 경로입니다.

구분 Forward Rendering (포워드) Deferred Rendering (디퍼드)
방식 물체를 그릴 때 조명까지 한 번에 계산 정보를 버퍼(G-Buffer)에 저장 후, 나중에 조명 계산
장점 반투명 처리에 강함, MSAA 사용 가능 (선명함) 라이트 개수 제한 없음, 복잡한 연산에 효율적
단점 라이트가 많아지면 성능 급락 메모리 사용량 높음, 반투명 처리가 어려움
활용 모바일, VR, 캐주얼 게임 PC/콘솔 AAA 게임 (언리얼 기본값)

 

Insight: 언리얼 엔진 5의 핵심 기술인 LumenNanite는 디퍼드 렌더링을 기반으로 작동합니다. 따라서 고퀄리티 PC 게임을 목표로 한다면 디퍼드가 필수적이지만, 모바일이나 VR 최적화가 필요하다면 포워드를 고려해야 합니다.


 

마치며

그래픽스 이론은 단순히 지식을 넘어, 더 나은 성능과 퀄리티를 타협하고 조율하기 위한 개발자의 기초 체력이라는 생각이 듭니다. 앞으로도 DirectX와 셰이더 프로그래밍을 직접 다뤄보며 더 깊이 있는 내용을 포스팅하겠습니다.

'TA' 카테고리의 다른 글

수학 기초 (1) - 공간의 수학  (1) 2026.01.24