7 분 소요

GPU와 렌더링 파이프라인의 기초에 대하여 알아보자

김하연 튜터님의 Notion 자료를 바탕으로 강의를 들으며
수정 및 재작성한 블로깅용 글

  • 여담 : Unreal은 자신만의 스킬 하나를 ‘깊게’ 다루는 것을 선호하는 편?

  • 그래픽스는 언리얼 이전에 ‘자체’를 공부해야 함

1. 화면에 그림이 나오기까지 🖌️

1-1. 픽셀과 GPU의 필요성

📍 픽셀(Pixel)의 이해

  • 정의: Picture Element의 줄임말, 화면을 구성하는 가장 작은 점
  • 색상 표현: RGB (빨강, 초록, 파랑) 3색의 조합
    • 각 색상: 0~255 단계 (8비트, 256단계)
    • 예시: 빨강 100% + 초록 100% = 노란색

📍 해상도와 픽셀 수

해상도 픽셀 수 계산
FHD (1920×1080) 약 207만 개 1920 × 1080
4K (3840×2160) 약 830만 개 3840 × 2160

📍 FPS와 초당 계산량

  • 60fps 게임의 경우
    • FHD: 207만 × 60 = 약 1.2억 픽셀/초
    • 4K: 830만 × 60 = 약 5억 픽셀/초
  • 프레임이 높아야 게임은 할만함!

📍 CPU의 한계

  • 문제점
    • 코어 수 제한 (일반적으로 8~16코어)
    • 순차 처리에 최적화
    • 픽셀 처리 외 다른 작업도 처리해야 함
  • 예시: 8코어 CPU로 5억 픽셀 처리 시 → 프레임당 수 시간 소요

아주 똑똑하지만
수가 별로 없다!

📍 GPU의 등장

  • 특징
    • 수천~수만 개의 단순한 코어 (RTX 4090: 16,384개 CUDA 코어)
    • 병렬 처리에 최적화 (SIMD 구조)
    • 단순 반복 작업에 특화

CPU에게 잡일(?)을 시키지 않기 위해
탄생한 장치!

1-2. 렌더링 파이프라인

📍 파이프라인 개념

공장의 조립 라인처럼 각 단계가 순서대로 진행되며,
여러 데이터를 동시에 처리

📍 5단계 렌더링 파이프라인

DX/OpenGL의 대표적인 파이프라인들
(물론 세부적으로는 이거보다 훨씬 복잡하다)
(ex : 테슬레이션 등등)

1️⃣ Input Assembly (입력 조립)

  • 3D 모델 데이터 준비

  • 구성 요소:
    • 버텍스(꼭짓점) 위치
    • UV 좌표 (텍스처 매핑)
    • 노말 (표면 방향)
    • 인덱스 (삼각형 구성)
  • RAM → VRAM으로 데이터 복사
    (CPU -> GPU 복사)
    (그래서 중간에 개입하려면 DX 등에선 mmap 등으로
    공유해달라고 요청했어야 하였음)

2️⃣ Vertex Shader (버텍스 셰이더)

  • 3D → 2D 변환
    • 다들 이 연극 무대에 서있어(World 변환)
    • 카메라 이쪽에 있어 (뷰 변환)
    • 자 이제 찍는다! (Proj 변환)
  • 변환 과정
    1. 월드 변환: 오브젝트를 게임 세계에 배치
    2. 뷰 변환: 카메라 시점 적용
    3. 프로젝션 변환: 원근법 적용
  • 각 버텍스 독립적으로 처리 (병렬 처리)
    (Vertex Shader를 만들어서 각 정점(버텍스)마다 처리!)

3️⃣ Rasterization (래스터화)

  • 삼각형 → 픽셀 변환

  • 깊이 테스트: 가까운 오브젝트만 그리기
    • 건물 뒤에 있는걸 왜 그려??
    • Z-Buffer를 통해 ‘깊이’를 비교
  • 보간: 버텍스 사이 값 계산
    • ex) a,b 의 노말값은 각각 이정도니까, 그 사이의 녀석들은 이렇게 주면 되겠군…

4️⃣ Pixel Shader (픽셀 셰이더)

  • 각 픽셀 색상 결정
    (실제 색을 칠하는 용도)

  • 주요 작업
    • 텍스처 샘플링
    • 노말 매핑
    • 조명 계산
    • 그림자 계산
    • PBR (물리 기반 렌더링)
  • GPU의 성능을 잡아먹는 녀석…
    • 물리적인 공식들을 매우 다양하게, 많이 사용함

5️⃣ Output Merger (출력 병합)

  • 최종 이미지 생성

  • 알파 블렌딩 (투명도)
    (유리창과 같은 특수 처리용도)
    • 이런 녀석들은 나중에 다시 가져다 쓰기도
      (VTR,STR…)
  • 안티앨리어싱 (계단 현상 제거)

  • 프레임버퍼에 저장
    (스왑 체인!)

이러한 파이프라인 중 어디에 문제가 있는지를 파악해야 최적화 가능!

1-3. 왜 삼각형인가?

  1. 평면성 보장: 3개 점은 항상 하나의 평면 구성
  2. 계산 단순성: 점이 삼각형 내부인지 판단 용이
  3. 하드웨어 최적화: GPU가 삼각형 처리에 특화
  4. 역사적 표준: 1970년대부터 사용된 업계 표준
  • 사각형은 3각형 2개로 표현이 가능하기도 하고…

2. CPU에서 GPU로 명령 전달 💫

GPU는 수동적?

  • 결국 CPU가 명령을 내려주어야 그제서야 일을 함

2-1. CPU와 GPU의 협업

📍 CPU의 역할

  • 게임 로직 처리
    • 입력 처리 (키보드, 마우스)
    • 게임 규칙 관리
    • AI 계산
    • 물리 연산
  • 렌더링 결정
    • 카메라 시야 내 오브젝트 판단
      (컬링 - 그릴 것과 안 그릴 것 판단)
    • 거리별 LOD 결정
    • 오클루전 컬링

📍 Draw Call 이해하기

  • 정의: CPU가 GPU에게 보내는 그리기 명령 패키지

  • 구성 요소
    1. 메시 정보 (버텍스, 인덱스 위치)
    2. 변환 정보 (위치, 회전, 크기)
    3. 머티리얼과 텍스처
    4. 렌더링 설정
  • Draw Call은 매우 많은 내용이 담겨 있음

2-2. Draw Call의 비용

📍 처리 과정과 시간

단계 소요 시간 설명
CPU 준비 2-3ms 오브젝트 정보 수집, 컬링 확인
명령 버퍼 생성 1-2ms GPU 언어로 번역
CPU→GPU 전송 1-2ms PCIe 버스 통과
GPU 준비 1-2ms 파이프라인 플러시, 상태 변경
실제 그리기 0.01ms 렌더링 수행

문제점: 준비 시간(5-7ms) vs 실제 작업(0.01ms) = 500~700배 차이

즉, 그리는 것보다 (GPU의 일)
Draw Call 자체가 문제!(CPU -> GPU 전송)

  • 그래픽 드라이버가 추가적인 검사 (주소, 컴파일 체크) 가능함…

  • CPU도 다른 일을 해야하기에 추가적인 병목 발생 가능함
    (CPU는 게임 로직 뿐 아니라 매우 다양한 일을 진행)

📍 성능 영향

  • 60fps 유지 조건: 프레임당 16.67ms 이내
  • 시간 배분 예시
    • CPU 게임 로직: 5ms
    • Draw Call 준비: 5ms
    • GPU 렌더링: 5ms
    • 여유분: 1.67ms
  • 권장 Draw Call 수
    • PC: 200-300개 이하
    • 모바일: 50-100개

넉넉잡으면 1000(PC) ~ 500 (Mobile)
로 잡을수는 있을지도?

2-3. 최적화 기법

📍 배칭(Batching)

  • 스태틱 배칭
    • 정적 오브젝트 미리 합치기
    • 장점: Draw Call 대폭 감소
    • 단점: 메모리 사용 증가, 개별 제어 불가
  • 다이나믹 배칭
    • 실시간으로 작은 메시 합치기
    • 조건: 300개 이하 버텍스
    • 단점: CPU 부하 발생

하나의 ‘덩어리’로 만들어서 Draw Call을 줄이는 방식

  • Merge Actor 같은 언리얼 기능이 존재(스태틱 배칭)
  • 언리얼은 추가로 ‘다이나믹 배칭’을 고려함(자체적 기능)

📍 인스턴싱(Instancing)

  • 동일 메시를 여러 위치에 그리기

  • 예시: 나무 1000그루 = Draw Call 1개

  • 제약: 같은 메시, 같은 머티리얼 필수

‘똑같이 생긴 녀석’들을 ‘위치’만 다르게 하여
하나의 Draw Call로 보냄
(그래도 Scale, Rotate 정도는 건들수 있음)

  • Transform 데이터를 수정은 가능함

  • ‘텍스쳐’도 고정된다는 점을 유의하자

📍 GPU 드리븐 렌더링

  • GPU가 직접 렌더링 결정
  • 예시: 언리얼 5 Nanite
  • 장점: Draw Call 걱정 없이 수백만 폴리곤 사용

나나이트를 통해

  • GPU가 Compute Shader를 돌려서 ‘그릴 요소’를 결정함
  • Draw Call을 호출 안함…!!

  • 언리얼 최적화를 공부할 때 필수가 되는 이유

  • 다만 이 기술만 ‘넣고 보는 방식’은 최적화가 아니므로 주의할 것!
    (나나이트만 믿고 최적화가 제대로 되지 않는 경우도 많음…)

3. GPU가 실제로 하는 일 😶‍🌫️

  • 쉐이더
  • 텍스쳐
  • 라이팅(+ 그림자)

3-1. 셰이더 시스템

HLSL로 작성

📍 셰이더 종류와 역할

  • 버텍스 셰이더
    • 역할: 3D 꼭짓점 → 2D 화면 좌표 변환
    • 특징
      • 각 버텍스 독립 처리
      • 버텍스 추가/삭제 불가
      • 애니메이션 처리
        (마치 흔들흔들 하는 느낌이라던가)
  • 픽셀 셰이더
    • 역할: 각 픽셀 색상 결정
    • 주요 작업
      • 텍스처 샘플링 (디퓨즈, 노말, 러프니스 맵)
      • 라이팅 계산
      • PBR 계산
      • 그림자 처리
  • 컴퓨트 셰이더
    • 역할: 범용 GPU 계산
      (자유로운 계산 작업 가능)
    • 활용: 물리 시뮬레이션, 파티클, AI 계산

3-2. 텍스처 시스템

📍 핵심 개념

  • UV 매핑
    • 3D 표면 ↔ 2D 텍스처 대응
    • U(가로), V(세로): 0~1 범위
    • 타일링: 1 초과 값으로 반복
  • 밉맵(Mipmap)
    • 텍스처를 절반씩 축소한 세트
    • 장점
      • 메모리 대역폭 절약
      • 앨리어싱 방지
    • 예시: 2048×2048 → 1024×1024 → … → 1×1
    • 텍스쳐 계의 LOD!
      (용량은 33% 정도 늘어남 -> 자동 생성)
      (그래도 성능상 큰 이득)
  • 텍스처 필터링
필터링 종류 특징 용도
포인트 가장 가까운 텍셀 선택 픽셀 아트
바이리니어 4개 텍셀 혼합 일반적인 경우
트라이리니어 밉맵 레벨 간 혼합 고품질
이방성(AF) 비스듬한 표면 선명화 바닥, 벽

텍스쳐들 역시 하나의 픽셀
컬링 등으로 잘리게 되는 경우, 어떻게 선택해야 할까?
싶을 때, 필터링을 진행하여 선택함

  • 텍스처 압축
    • BC 포맷: 4×4 블록 압축
    • 압축률: 1/4 ~ 1/6
    • 특징: GPU가 압축 상태로 직접 읽기
      (OpenWorld의 일부 텍스쳐는 매~우 무거움
      그런만큼 VRam에 일일이 다올리기는 매우 부담스러움)
      • ‘스트리밍’ 시스템이 갑자기 텍스쳐를 불러오다가 종종 텍스쳐가 깨지거나 이상하게 보일 수 있음

3-3. 라이팅과 그림자

빛을 어떻게 표현할 수 있을까?
가 라이팅의 역사…

📍 조명 모델 진화

  • Phong 모델 (고전적)
    • Diffuse (확산광): 표면에 고르게 퍼지는 빛
    • Specular (반사광): 반짝이는 하이라이트
    • Ambient (주변광): 간접광 단순 표현

물리 법칙을 다소 무시하는 경우가 존재
(‘그럴듯하게’ 가성비 좋게 표현)
(그렇기에 종종 ‘플라스틱’ 같은 느낌이…)

  • PBR (물리 기반 렌더링)
    • 핵심 파라미터
      • Base Color: 고유 색상
      • Metallic: 금속성 (0~1)
      • Roughness: 거칠기 (0~1)
    • 특징: 물리 법칙 준수, 에너지 보존

📍 그림자 구현 기법

기법 원리 특징
섀도우 매핑 광원 시점 깊이 저장 가장 일반적
캐스케이드 섀도우맵 거리별 해상도 차등 효율적
디스턴스 필드 복셀 기반 거리 정보 부드러운 그림자
레이트레이싱 광선 추적 정확하지만 비용 높음

그림자 연산은 매~우 비쌈

  • Ambient Occlusion 같은 ‘전역 조명’ 연산 비용 절약과는 살짝 다를지도?

📍 간접 조명

  • 라이트맵: 정적 간접광 미리 계산
  • 라이트 프로브: 공간별 빛 정보 캡처
  • SSGI: 화면 정보로 간접광 계산
  • Lumen: 언리얼 5의 복합 기술(레이 트레이싱을 일부만 사용)

실제 현실은 ‘조명’으로부터
물체 표면의 ‘반사율’에 따라 튕기는 쪽으로 구현되나
실제로 그러면 ‘매~우’ 무거움

그렇기에 미리 계산하거나
화면에 보이는 녀석들만 레이 트레이싱을 적용하는 등의 최적화 기법 사용

4. 성능과 한계 이해하기 👽

4-1. 해상도와 성능

📍 해상도별 픽셀 수와 부하

해상도 픽셀 수 FHD 대비
HD (720p) 92만 0.44x
FHD (1080p) 207만 1x
QHD (1440p) 369만 1.78x
4K (2160p) 830만 4x

📍 픽셀 필레이트와 실제 성능

  • 이론 vs 현실
    • RTX 4090: 443 GPixels/s
    • 4K 60fps 필요량: 0.5 GPixels/s
    • 하지만 실제로는 겨우 달성
  • 성능 저하 요인
    1. 오버드로우: 같은 픽셀 여러 번 그리기(해상도가 높아질수록 더 낭비가 심해짐!)
    2. 셰이더 복잡도: 픽셀당 수십~수백 명령어(가벼운 명령이라면 괜찮지만…)
    3. 메모리 대역폭: 데이터 전송 한계(4K는 초당 1TB의 메모리 전송으로도 부족할 수 있음…)
  • 해상도가 줄어든다고 성능이 비율만큼 늘어나진 않음!
    • 해상도와 상관없는 고정 비용 등도 존재함

4-2. GPU 병목 종류

📍 병목 유형과 진단

  • 지오메트리 병목
    • 원인: 버텍스 과다, 복잡한 버텍스 셰이더
    • 진단
      • 해상도 변경해도 fps 동일
      • 와이어프레임 모드에서 성능 향상
    • 해결: LOD 사용, 셰이더 단순화
  • 픽셀 병목
    • 원인: 복잡한 셰이더, 오버드로우, 높은 해상도
    • 진단
      • 해상도 절반 → 성능 2배
      • 셰이더 복잡도 뷰에서 빨간색 영역
    • 해결: 셰이더 최적화, 투명도 제한
  • 메모리 대역폭 병목
    • 원인: 큰 텍스처, G-버퍼, MSAA
    • 진단
      • GPU 프로파일러 메모리 처리량 80% 초과
      • 텍스처 품질 낮추면 성능 개선
    • 해결: 텍스처 압축, 대안 AA 사용

📍 병목 찾기 실전 가이드

  1. 해상도 테스트: 720p → 4K 성능 변화 측정
  2. 뷰 거리 테스트: 가까이/멀리 성능 비교
  3. 품질 설정 테스트: 각 설정별 영향 확인
  4. 프로파일러 활용: stat gpu, stat unit 명령

댓글남기기