작성일 :

렌더 파이프라인의 역할

GPU 아키텍처 (2) - 모바일 GPU와 TBDR에서 모바일 GPU가 TBDR(Tile-Based Deferred Rendering) 구조로 동작하는 과정을 살펴보았습니다.

GPU 하드웨어는 정점을 변환하고, 삼각형을 래스터화하고, 픽셀의 색상을 계산하는 물리적인 실행 장치입니다.

하지만 GPU는 스스로 어떤 오브젝트를 그릴지, 어떤 순서로 그릴지, 조명을 어떻게 적용할지를 결정하지 않습니다.

이러한 결정은 CPU 측에서 담당하며, CPU가 GPU에 전달할 그리기 명령을 구성하는 일련의 처리 단계를 렌더 파이프라인(Render Pipeline)이라 합니다.

3D 씬 데이터가 최종 화면 이미지가 되기까지의 전체 렌더링 과정을 정의하고 제어합니다.


렌더 파이프라인이 씬(Scene)의 오브젝트 목록을 GPU가 실행할 수 있는 그리기 명령(드로우콜(Draw Call))으로 변환하는 과정은 네 단계로 나뉩니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
씬의 전체 오브젝트 목록
        │
        ▼
(1) 컬링 (Culling)
        카메라에 보이지 않는 오브젝트를 제외
        그릴 필요가 없는 것은 처음부터 걸러내어 비용을 줄임
        │
        ▼
(2) 정렬 (Sorting)
        남은 오브젝트의 렌더링 순서를 결정
        불투명 오브젝트: 앞에서 뒤로 (오버드로우 감소)
        투명 오브젝트: 뒤에서 앞으로 (올바른 블렌딩)
        │
        ▼
(3) 라이팅 패스 (Lighting Pass)
        조명을 어떻게 적용할지 결정
        조명마다 별도 패스 또는 한 번에 모든 조명
        │
        ▼
(4) 드로우콜 생성 및 제출
        GPU에 보낼 그리기 명령을 구성하여 제출
        "이 메쉬를, 이 머티리얼로, 이 셰이더로 그려라"
        │
        ▼
GPU가 화면을 렌더링


렌더 파이프라인이 컬링, 정렬, 라이팅, 드로우콜 생성을 자동으로 처리하므로, 개발자는 이 과정을 직접 구현할 필요 없이 씬 구성과 셰이더, 머티리얼 설정에 집중할 수 있습니다.


Unity에는 여러 종류의 렌더 파이프라인이 존재합니다.

가장 기본적인 것이 Built-in 렌더 파이프라인이고, 모바일을 포함한 넓은 범위의 플랫폼에서 성능을 우선시하는 것이 URP(Universal Render Pipeline), PC와 콘솔에서 고품질 비주얼을 목표로 하는 것이 HDRP(High Definition Render Pipeline)입니다.

URP는 넓은 플랫폼 호환성과 런타임 성능에 초점을 맞추고, HDRP는 볼류메트릭 라이팅, 서브서피스 스캐터링, 레이 트레이싱 등 고급 렌더링 기능을 지원하는 대신 높은 하드웨어 사양을 요구합니다.


이 시리즈에서는 모바일 최적화에 초점을 맞추어 Built-in과 URP를 중심으로 다룹니다.

두 파이프라인은 셰이더 시스템, 포스트 프로세싱, 카메라 구조 등 여러 면에서 다르지만, 렌더링 성능에 가장 직접적인 영향을 미치는 차이는 조명 처리 방식과 배칭 구조입니다.

이 글에서는 조명 처리 방식의 차이를 살펴보고, 배칭은 Part 2에서 다룹니다.


Built-in 렌더 파이프라인 – Forward Rendering

Built-in 렌더 파이프라인은 Unity가 초기부터 제공해 온 렌더 파이프라인입니다.

기본 렌더링 방식은 포워드 렌더링(Forward Rendering)입니다. 오브젝트를 하나씩 그리면서 지오메트리 처리와 조명 계산을 한 번에 수행하여, 최종 픽셀 색상을 바로 출력하는 구조입니다.

멀티패스 포워드 렌더링

Built-in 파이프라인의 포워드 렌더링은 멀티패스(Multi-pass) 방식으로 동작합니다.

멀티패스란 하나의 오브젝트를 그릴 때 조명 하나당 별도의 렌더링 패스를 실행하는 구조입니다.

오브젝트에 영향을 미치는 조명이 3개이면, 그 오브젝트를 3번 그립니다.


1
2
3
4
5
6
7
8
9
10
11
오브젝트 A에 조명 3개가 영향을 미치는 경우

패스 1 (ForwardBase)    메인 Directional Light + 환경광 + 그림자  → 드로우콜 1
        │
        ▼ 가산 블렌딩
패스 2 (ForwardAdd)     추가 Point Light                          → 드로우콜 2
        │
        ▼ 가산 블렌딩
패스 3 (ForwardAdd)     추가 Spot Light                           → 드로우콜 3

→ 오브젝트 1개 × 조명 3개 = 드로우콜 3회


첫 번째 패스인 ForwardBase는 씬 전체에 공통으로 적용되는 기본 조명을 처리합니다. 메인 Directional Light, 환경광(Ambient), 라이트맵(Lightmap), 그림자가 이 패스에서 한 번에 계산됩니다.

조명이 메인 Directional Light 하나뿐인 씬에서는 ForwardBase 패스만으로 렌더링이 완료됩니다.


추가 조명(Point Light, Spot Light 등)이 있으면, 조명 하나마다 ForwardAdd 패스를 실행하여 같은 오브젝트를 다시 그립니다.

각 패스의 결과는 가산 블렌딩(Additive Blending)으로 기존 픽셀 색상에 더해져, 여러 조명의 효과가 누적됩니다.

멀티패스의 비용 계산

멀티패스 구조에서 드로우콜 수는 오브젝트 수와 조명 수의 곱에 비례합니다. 조명이 많을수록 같은 오브젝트를 반복해서 그리는 횟수가 늘어나기 때문입니다.


1
2
3
4
5
6
7
8
9
10
11
12
오브젝트 10개, 픽셀 라이트 3개 (Directional 1 + Point 2)

오브젝트당:  ForwardBase 1회 + ForwardAdd 2회 = 3회
전체:        10 × 3 = 드로우콜 30회

         ForwardBase    ForwardAdd     ForwardAdd
         (메인+환경광)   (Point)        (Point)
오브젝트 A    ①             ②              ③
오브젝트 B    ④             ⑤              ⑥
오브젝트 C    ⑦             ⑧              ⑨
   ...
오브젝트 J    ㉘             ㉙              ㉚


위 예시에서는 추가 조명 2개가 모두 ForwardAdd 패스를 생성했습니다.

하지만 씬의 모든 추가 조명이 ForwardAdd 패스를 생성하는 것은 아닙니다. ForwardAdd 패스를 생성하는 조명은 픽셀 라이트(Pixel Light)로 분류된 조명뿐입니다. 픽셀 라이트는 프래그먼트 셰이더에서 픽셀 단위로 조명을 계산하므로 품질이 가장 높지만, ForwardAdd 패스를 추가하는 만큼 비용도 높습니다.

가장 밝은 픽셀 라이트 하나는 ForwardBase 패스에서 처리되고, 나머지 픽셀 라이트가 각각 ForwardAdd 패스를 생성합니다.

모든 조명을 픽셀 라이트로 처리하면 드로우콜이 빠르게 늘어나므로, Unity는 픽셀 라이트 수를 제한합니다. Quality Settings의 Pixel Light Count에서 제한값을 설정하며, 기본값은 4입니다.


이 제한을 초과하는 조명은 정점 라이트(Vertex Light)SH 라이트(Spherical Harmonics Light)로 대체됩니다. 두 방식 모두 ForwardBase 패스 안에서 함께 처리되므로 ForwardAdd 패스를 추가로 생성하지 않습니다. 드로우콜이 늘어나지 않는 대신, 조명 품질이 낮아집니다.

정점 라이트는 프래그먼트 셰이더가 아닌 정점 셰이더에서 정점 단위로 조명을 계산합니다. 정점 사이의 조명은 보간으로 채워지므로, 로우폴리 메쉬에서 조명 변화가 부자연스러워질 수 있습니다.

SH 라이트는 조명 환경을 몇 개의 수학적 계수로 요약하는 구면 조화 함수를 사용하여 더 저비용으로 근사합니다.

그렇더라도, 픽셀 라이트가 4개이고 오브젝트가 50개인 씬에서는 50 x 4 = 최대 200회의 드로우콜이 발생합니다.


멀티패스의 장점과 단점

멀티패스 포워드 렌더링은 각 패스가 하나의 조명만 처리하므로 셰이더 구현이 간결합니다.

오브젝트를 하나씩 순서대로 그리는 방식이므로 투명 오브젝트의 블렌딩 순서도 보장할 수 있어, 투명 오브젝트 처리에 유리합니다.

반면, 드로우콜이 조명 수에 비례하여 증가하는 구조적 단점이 있습니다. 드로우콜마다 CPU는 GPU 상태를 설정하고 명령을 제출해야 하므로, 조명이 늘어날수록 CPU 부담이 커집니다.


GPU 측 비용도 늘어납니다. 멀티패스에서는 같은 오브젝트의 같은 픽셀을 조명 수만큼 반복 셰이딩하므로, 조명이 3개이면 프래그먼트 셰이더가 3번 실행됩니다.

GPU 아키텍처 (2) - 모바일 GPU와 TBDR에서 살펴본 것처럼 모바일 GPU는 프래그먼트 처리 능력과 메모리 대역폭이 제한적이므로, 반복 셰이딩이 성능에 직접적인 영향을 미칩니다.

Built-in Deferred 모드

Built-in 파이프라인에는 드로우콜 증가를 다른 방식으로 해결하는 디퍼드 렌더링(Deferred Rendering) 모드도 있습니다.

디퍼드 렌더링은 오브젝트 그리기와 조명 계산을 분리하는 구조입니다. 이름 그대로 조명 계산을 뒤로 미룹니다(defer).

먼저 모든 오브젝트의 기하학적 정보(위치, 법선, 색상 등)만 G-Buffer라는 여러 장의 풀스크린 텍스처에 기록합니다. 이 단계에서는 조명 계산을 수행하지 않으므로, 오브젝트는 조명 수와 관계없이 한 번만 그리면 됩니다.

그 뒤 G-Buffer에 기록된 정보를 읽어, 화면의 각 픽셀에 대해 조명을 계산합니다.


1
2
3
4
5
6
7
Geometry Pass       모든 오브젝트의 기하학적 정보를 기록 (조명 계산 없음)
        │
        ▼
    G-Buffer        Diffuse(색상), Normal(법선), Specular(반사), Depth(깊이)
        │
        ▼
Lighting Pass       G-Buffer를 읽어 화면 픽셀 단위로 조명 계산


디퍼드 렌더링에서는 조명 수가 많아도 드로우콜이 크게 늘지 않습니다. 조명마다 별도 패스를 실행하는 대신, G-Buffer에 기록된 정보를 기반으로 화면 픽셀 단위로 조명을 계산하기 때문입니다.


다만, 드로우콜 대신 다른 종류의 비용이 발생합니다.

G-Buffer 자체가 Diffuse, Normal, Specular, Depth 등 여러 장의 풀스크린 텍스처로 구성되므로, 메모리 대역폭 소비가 큽니다.

예를 들어, 1920x1080 해상도 기준으로 G-Buffer 텍스처 하나의 크기만 해도 수 MB에 달하며, 4장을 동시에 유지하면 매 프레임 수십 MB의 메모리 읽기/쓰기가 발생합니다.


모바일 GPU는 데스크톱 GPU에 비해 메모리 대역폭이 제한적이고, TBDR 구조의 온칩 타일 메모리도 한정되어 있습니다.

G-Buffer 4장을 동시에 유지하려면 타일 메모리 용량과 대역폭 모두에 부담이 커지므로, 모바일 환경에서 Built-in Deferred는 실질적으로 사용이 어렵습니다.

Built-in 파이프라인에서 모바일 게임이 선택할 수 있는 현실적인 방식은 포워드 렌더링이며, 멀티패스로 인한 드로우콜 증가가 주요 병목이 됩니다.


SRP: 렌더 파이프라인을 직접 제어하다

Built-in 파이프라인의 한계

Built-in 렌더 파이프라인은 Unity 엔진 내부의 C++ 코드로 구현되어 있습니다. 렌더링 과정의 각 단계(컬링, 정렬, 라이팅 패스, 드로우콜 제출)가 엔진 내부에 고정되어 있어, 개발자가 렌더링 과정을 수정하거나 확장하기 어렵습니다.

불필요한 렌더링 패스를 제거하거나 특정 프로젝트에 맞는 커스텀 패스를 추가하려 해도, 엔진 소스 코드를 직접 수정하지 않는 한 불가능합니다.

이 구조적 한계를 해결하기 위해, Unity는 2018년(Unity 2018.1)에 SRP(Scriptable Render Pipeline) 아키텍처를 도입했습니다.

SRP란

SRP는 렌더링 과정을 C# 스크립트로 제어할 수 있게 하는 프레임워크입니다.

“Scriptable”이라는 이름이 의미하듯, 렌더 패스를 추가하거나 제거하거나 수정하는 작업을 C# 코드로 수행할 수 있습니다.

Built-in에서는 엔진이 강제하던 렌더링 흐름을 개발자가 직접 정의하는 구조입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Built-in 파이프라인

┌───────────────────────────────────────────────┐
│  Unity 엔진 (C++) — 렌더링 과정 고정           │
│                                               │
│  컬링 → 정렬 → ForwardBase → ForwardAdd      │
│                                               │
│  (수정 불가)                                   │
└───────────────────────────────────────────────┘


SRP 아키텍처

┌───────────────────────────────────────────────┐
│  Unity 엔진 (C++) — SRP Core                   │
│  컬링, 커맨드 버퍼, 렌더 타겟 관리              │
└────────────────────┬──────────────────────────┘
                     │ C# API
                     ▼
┌───────────────────────────────────────────────┐
│  렌더 파이프라인 (C# 스크립트)                  │
│  렌더 패스 구성, 실행 순서, 리소스 관리          │
│  (개발자가 수정 가능)                           │
└───────────────────────────────────────────────┘


SRP Core는 컬링 실행, 커맨드 버퍼(Command Buffer) 구성, 렌더 타겟(Render Target) 관리 등의 저수준 렌더링 기능을 C# API로 제공합니다. URP, HDRP 같은 파이프라인 패키지나 개발자가 직접 작성하는 커스텀 파이프라인은 모두 이 API 위에서 동작합니다.


SRP 위에 구축된 파이프라인은 두 가지입니다.

1
2
3
4
5
6
7
8
9
10
SRP 위에 구축된 파이프라인

          SRP Core (저수준 렌더링 C# API)
                    │
          ┌─────────┴──────────┐
          ▼                    ▼
        URP                  HDRP
   모바일/범용          고사양 PC/콘솔 전용
   성능 우선            시각 품질 우선
   싱글패스 포워드      디퍼드 + 포워드


URP는 모바일을 포함한 넓은 범위의 플랫폼을 대상으로 성능을 우선시하고, HDRP는 고사양 PC와 콘솔을 대상으로 최고 수준의 시각 품질을 추구합니다. 모바일 게임 최적화에서 핵심이 되는 파이프라인은 URP입니다.


URP: 모바일 우선 파이프라인

URP의 등장 배경

앞서 살펴본 것처럼 Built-in의 멀티패스 구조에서는 조명 수에 비례하여 드로우콜이 증가합니다.

URP는 SRP 위에 구축된 모바일 우선(Mobile-first) 파이프라인으로, 싱글패스 포워드 렌더링(Single-pass Forward Rendering)으로 이 문제를 해결합니다.

싱글패스 포워드 렌더링

싱글패스란 하나의 오브젝트를 그릴 때 모든 조명을 한 번의 패스에서 처리한다는 의미입니다. 조명이 3개이든 8개이든 오브젝트는 한 번만 그려지므로, 드로우콜이 조명 수에 영향을 받지 않습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
오브젝트 2개, 조명 3개인 씬

Built-in 멀티패스                          URP 싱글패스

오브젝트 A:                                오브젝트 A:
  메인 라이트     → 드로우콜 ①               모든 조명(3개) → 드로우콜 ①
  추가 라이트 1   → 드로우콜 ②
  추가 라이트 2   → 드로우콜 ③             오브젝트 B:
                                             모든 조명(3개) → 드로우콜 ②
오브젝트 B:
  메인 라이트     → 드로우콜 ④
  추가 라이트 1   → 드로우콜 ⑤             합계: 2회
  추가 라이트 2   → 드로우콜 ⑥

합계: 6회


URP의 싱글패스에서는 셰이더가 한 번 실행될 때 해당 오브젝트에 영향을 미치는 모든 조명을 한꺼번에 처리합니다. 조명이 몇 개이든 오브젝트를 다시 그리지 않으므로, 드로우콜은 오브젝트 수에만 비례합니다.

드로우콜 감소 효과

앞서 Built-in에서 계산했던 동일한 씬 구성을 URP로 다시 계산하면, 차이가 드러납니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
URP 싱글패스 드로우콜 계산

씬 구성:
  - 오브젝트: 10개
  - 조명: 3개 (Directional 1 + Point 2)

각 오브젝트당 패스 수:
  - ForwardLit:  1회 (모든 조명을 한 패스에서 처리)

전체 드로우콜 수:
  10개 오브젝트 × 1패스 = 10회 드로우콜


비교:
┌────────────────────────────────────────────┐
│             │ Built-in 멀티패스 │ URP 싱글패스 │
│─────────────┼──────────────────┼─────────────│
│ 오브젝트 10 │                  │             │
│ 조명 3      │    30회          │   10회      │
│─────────────┼──────────────────┼─────────────│
│ 오브젝트 50 │                  │             │
│ 조명 4      │   200회          │   50회      │
│─────────────┼──────────────────┼─────────────│
│ 오브젝트 100│                  │             │
│ 조명 4      │   400회          │  100회      │
└────────────────────────────────────────────┘


Built-in에서는 조명이 하나 추가될 때마다 전체 오브젝트 수만큼 드로우콜이 늘어나지만, URP에서는 조명이 추가되어도 오브젝트당 드로우콜은 1회로 유지됩니다.

공식으로 표현하면 Built-in은 오브젝트 수 × 조명 수, URP는 오브젝트 수입니다.


다만 URP에서도 오브젝트당 처리할 수 있는 추가 조명 수에는 제한이 있습니다.

메인 Directional Light를 제외한 추가 조명은 기본 8개까지 처리되며, 초과분은 셰이더에서 무시됩니다.

이 값은 URP Asset의 Lighting 설정에서 조정할 수 있지만, 모바일 게임에서 하나의 오브젝트에 8개 이상의 실시간 추가 조명이 동시에 영향을 미치는 경우는 드물므로 대부분의 프로젝트에서 문제가 되지 않습니다.

싱글패스가 가능한 이유

싱글패스로 여러 조명을 처리하려면, 셰이더가 런타임에 동적으로 변하는 조명 수를 다룰 수 있어야 합니다.

Built-in 파이프라인이 설계된 시점의 초기 셰이더 모델에서는 반복 횟수가 런타임에 결정되는 동적 루프를 지원하지 않았고, 상수 버퍼의 크기도 고정되어 있어서 가변 개수의 조명 데이터를 하나의 패스에서 처리하기 어려웠습니다. 조명 하나당 패스 하나를 실행하는 멀티패스 방식이 당시에는 가장 현실적인 해법이었습니다.


URP에서는 C# 렌더링 코드가 씬의 모든 활성 조명 정보를 하나의 상수 버퍼에 담아 GPU에 전달하고, 셰이더가 이 버퍼를 동적 루프로 순회하면서 모든 조명을 한 번의 패스에서 처리합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
URP의 조명 데이터 흐름

CPU (C# 렌더링 코드):
  씬의 활성 조명 수집
  → 조명 위치, 방향, 색상, 감쇠 등을 배열로 정리
  → 상수 버퍼(Constant Buffer)에 기록
  → GPU에 전달
        │
        ▼
GPU (셰이더):
  메인 라이트 계산
  → 추가 조명을 동적 루프로 순회 (i = 0 ~ additionalLightCount)
  → 각 조명의 기여를 누적
  → 최종 색상 출력

URP의 렌더링 구조

앞에서 살펴본 싱글패스 포워드 렌더링은 URP의 조명 처리 방식입니다.

하나의 프레임을 완성하려면 조명 처리 외에도 여러 단계가 필요하며, URP는 이 단계들을 각각 렌더 패스(Render Pass)로 나누어 순서대로 실행합니다.

Renderer와 Render Pass

URP의 렌더링 과정은 Renderer가 관리합니다. Renderer는 어떤 렌더 패스를 어떤 순서로 실행할지 정의하는 객체입니다. URP의 기본 Renderer는 Universal Renderer이며, 포워드 렌더링 방식으로 동작합니다.

Renderer가 실행하는 각 렌더 패스(Render Pass)는 렌더링의 개별 단계를 나타냅니다. 깊이 기록, 불투명 렌더링, 투명 렌더링 등 각 패스가 고유한 목적을 갖고, 정해진 순서대로 실행됩니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
URP의 기본 렌더 패스 실행 순서

(1) Depth Prepass          깊이값만 먼저 기록 (선택적)
         │
         ▼
(2) Opaque Rendering       불투명 오브젝트를 앞→뒤 정렬하여 싱글패스로 렌더링
         │
         ▼
(3) Skybox                 빈 영역을 하늘 배경으로 채움
         │
         ▼
(4) Transparent Rendering  투명 오브젝트를 뒤→앞 정렬하여 알파 블렌딩
         │
         ▼
(5) Post-Processing        블룸, 색보정, 안티앨리어싱 등 화면 전체 효과
         │
         ▼
(6) Final Blit             렌더링 결과를 화면(백버퍼)에 복사


Depth Prepass는 불투명 오브젝트의 깊이값만 먼저 기록하는 단계입니다. 색상 계산 없이 깊이값만 기록하므로 비용이 낮습니다.

깊이값이 미리 기록되어 있으면, 이후 Opaque Rendering 단계에서 가려진 픽셀의 프래그먼트 셰이더 실행을 Early-Z 테스트로 건너뛸 수 있습니다. Early-Z 테스트는 프래그먼트 셰이더를 실행하기 전에 깊이 비교를 먼저 수행하여, 이미 가려진 것으로 판단되는 픽셀의 셰이딩을 생략하는 GPU 기능입니다.

다만, Depth Prepass를 활성화하면 모든 불투명 오브젝트가 Depth Prepass에서 한 번, Opaque Rendering에서 한 번, 총 두 번 그려집니다.

오브젝트끼리 많이 겹치는 씬에서는 Early-Z로 절감되는 셰이딩 비용이 이 추가 드로우 비용보다 크므로 이득이 됩니다.

반대로 겹침이 적은 씬에서는 절감 효과가 작아 추가 드로우 비용만 늘어나므로, 비활성화하는 편이 유리할 수 있습니다.

URP에서는 Depth Priming Mode 설정으로 이 동작을 제어합니다.


Opaque Rendering은 불투명 오브젝트를 카메라로부터 가까운 순서대로(Front-to-Back) 정렬하여 그리는 단계입니다.

가까운 것부터 그리면 뒤에 가려진 픽셀이 깊이 테스트에서 걸러지므로, 불필요한 셰이딩이 줄어듭니다.

앞서 설명한 싱글패스 포워드 렌더링이 이 단계에서 적용되며, 프레임 전체 드로우콜의 대부분이 여기에서 발생합니다.


Skybox는 하늘 배경을 그리는 단계입니다. 불투명 렌더링 이후에 실행되므로, 이미 오브젝트가 그려진 픽셀에서는 깊이 테스트에 의해 스카이박스 셰이딩이 생략됩니다. 하늘이 실제로 보이는 픽셀만 처리하므로 비용이 줄어듭니다.


Transparent Rendering은 투명 오브젝트를 뒤에서 앞으로(Back-to-Front) 정렬하여 그리는 단계입니다.

불투명 렌더링의 Front-to-Back과 정렬 순서가 반대인데, 이는 알파 블렌딩(Alpha Blending) 때문입니다. 알파 블렌딩은 투명 오브젝트의 색상과 그 뒤에 이미 그려진 색상을 투명도(알파값)에 따라 혼합하는 연산입니다.

혼합하려면 뒤의 색상이 화면에 먼저 존재해야 하므로, 먼 오브젝트부터 그려야 합니다.


Post-Processing은 렌더링이 완료된 이미지에 블룸(Bloom), 색보정(Color Grading), 안티앨리어싱(Anti-Aliasing) 등의 화면 전체 효과를 적용하는 단계입니다. 각 효과마다 화면의 모든 픽셀을 처리하는 풀스크린 패스가 하나씩 추가되므로, 효과 수에 비례하여 GPU 비용이 증가합니다.

모바일 환경에서는 후처리 효과를 최소화하거나 저비용 대안을 선택하는 것이 일반적입니다.

예를 들어, LUT(Look-Up Table) 기반 색보정은 대표적인 저비용 대안입니다. 색상 변환 연산을 매 픽셀마다 수행하는 대신, 미리 계산된 테이블에서 결과를 조회하는 방식으로 GPU 비용을 절감합니다.


Final Blit는 렌더링 결과를 화면의 백버퍼(Back Buffer)에 복사하는 마지막 단계입니다.

후처리가 비활성화되어 있으면 URP는 백버퍼에 직접 렌더링하므로, 별도의 복사 없이 프레임이 완성됩니다.

후처리가 활성화되어 있으면 상황이 다릅니다. GPU는 같은 렌더 타겟을 동시에 읽고 쓸 수 없는데, 후처리 효과는 렌더링된 이미지 전체를 입력으로 읽어야 합니다.

그래서 URP는 별도의 오프스크린 렌더 타겟(Off-screen Render Target)에 먼저 그린 뒤, 후처리를 적용하고, 최종 결과를 백버퍼에 복사합니다.


Render Graph (Unity 6+)

기존 방식의 한계

앞에서 살펴본 것처럼 URP의 렌더 패스는 정해진 순서대로 실행됩니다.

각 패스는 중간 결과를 기록할 임시 텍스처가 필요한 경우가 있습니다. 예를 들어, Depth Prepass는 깊이값을 기록할 깊이 텍스처가 필요하고, Post-Processing은 화면 효과를 적용할 임시 텍스처가 필요합니다.

기존 URP(Unity 6 이전)에서는 각 패스가 이런 텍스처를 스스로 할당하고, 사용이 끝나면 해제하는 방식으로 동작했습니다.


각 패스가 텍스처를 스스로 관리하면, 패스 간에 어떤 텍스처가 어디서 쓰이는지 전체적으로 파악할 수 없습니다.

패스 A가 사용을 마친 텍스처를 패스 B가 재사용할 수 있는 상황에서도, 각 패스는 서로의 텍스처를 알지 못하므로 별도로 할당하여 메모리가 낭비됩니다.

또한, 결과를 아무도 읽지 않는 패스를 자동으로 건너뛸 수도 없습니다. 예를 들어, Depth Prepass가 기록한 깊이 텍스처를 이후 어떤 패스도 읽지 않는다면 이 패스는 실행할 필요가 없지만, 패스 간 의존 관계가 기록되어 있지 않으므로 이런 판단이 불가능합니다.

Render Graph의 도입

Unity 6(2024)부터 URP에 도입된 Render Graph는 이 문제를 해결합니다.

각 렌더 패스가 어떤 텍스처를 읽고 어떤 텍스처에 쓰는지를 명시적으로 선언하면, Render Graph가 이 정보를 바탕으로 패스 간 의존 관계를 방향 비순환 그래프(DAG, Directed Acyclic Graph)로 구성합니다.

의존 관계가 그래프로 표현되므로, 텍스처 재사용과 불필요한 패스 제거를 자동으로 수행할 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
기존 방식 (리스트):

  Depth → Shadow → Opaque → Post
  (패스 간 텍스처 관계가 기록되지 않음)


Render Graph 방식 (그래프):

  Depth  ─── DepthTexture ───┐
                              ▼
  Shadow ─── ShadowMap ────→ Opaque ─── ColorTexture ───→ Post


각 렌더 패스는 자신이 읽을 텍스처와 쓸 텍스처를 명시적으로 선언합니다. Render Graph는 이 선언들을 수집하여 패스 간 의존 관계 그래프를 자동으로 구축합니다.

Render Graph의 최적화

의존 관계가 그래프로 표현되면, Render Graph가 프레임 실행 전에 자동으로 수행하는 최적화가 두 가지 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(1) 패스 자동 제거 (Culling)

  그림자 활성화:
    Depth ─── DepthTexture ───┐
                               ▼
    Shadow ─── ShadowMap ───→ Opaque ─── ColorTexture ───→ Post

  그림자 비활성화 (ShadowMap을 아무도 읽지 않음):
    Depth ─── DepthTexture ───→ Opaque ─── ColorTexture ───→ Post
                                (Shadow 자동 제거)


(2) 텍스처 메모리 재사용 (Aliasing)

              Pass A      Pass B      Pass C      Pass D
  메모리 M:  [텍스처 X]  [텍스처 X]  [텍스처 X]  [텍스처 Y]
               쓰기        유지        읽기        쓰기
             ◄── X 수명 ──────────────►
                                        X 해제 → 같은 메모리 M을 Y가 사용


첫 번째 최적화는 불필요한 패스의 자동 제거(Culling)입니다. 어떤 패스의 출력을 이후의 어떤 패스도 읽지 않는다면, 그 패스는 실행할 필요가 없습니다.

Render Graph는 최종 출력(화면에 표시되는 결과)에서 출발하여 그래프를 역방향으로 탐색하고, 최종 출력에 기여하지 않는 패스를 자동으로 제거합니다. 예를 들어 그림자가 비활성화되어 ShadowMap을 아무 패스도 읽지 않으면, Shadow 패스가 자동으로 생략됩니다.


두 번째 최적화는 리소스 수명 관리와 메모리 재사용(Aliasing)입니다. 각 텍스처가 처음 쓰여지는 시점과 마지막으로 읽히는 시점을 그래프에서 파악할 수 있으므로, 텍스처의 수명을 정확히 알 수 있습니다.

위 다이어그램에서 텍스처 X는 Pass A에서 쓰이고 Pass C에서 마지막으로 읽히므로, Pass C 이후에는 텍스처 X가 차지하던 GPU 메모리가 비어 있게 됩니다. 텍스처 Y가 필요할 때 새 메모리를 할당하는 대신, 텍스처 X가 사용하던 메모리를 그대로 사용합니다.

수명이 겹치지 않는 텍스처끼리 같은 메모리를 공유하므로, 전체 메모리 사용량이 줄어듭니다. 메모리가 제한된 모바일 환경에서 이 최적화의 효과가 특히 큽니다.


Render Graph의 최적화는 URP의 기본 패스뿐 아니라, 개발자가 작성하는 커스텀 렌더 패스에도 적용됩니다. 커스텀 패스가 어떤 텍스처를 읽고 어떤 텍스처에 쓰는지를 선언하면, Render Graph가 이 정보를 바탕으로 패스 제거와 메모리 재사용을 자동으로 수행합니다.


Built-in vs URP 비교 요약

지금까지 살펴본 URP와 Built-in 파이프라인의 차이를 정리합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Built-in vs URP 비교

┌──────────────────┬──────────────────────┬──────────────────────────┐
│      항목        │  Built-in 파이프라인  │         URP              │
├──────────────────┼──────────────────────┼──────────────────────────┤
│ 조명 처리        │ 멀티패스 포워드       │ 싱글패스 포워드           │
│                  │ (조명마다 별도 패스)  │ (모든 조명을 한 패스에서) │
├──────────────────┼──────────────────────┼──────────────────────────┤
│ 드로우콜         │ 오브젝트 × 조명 수   │ 오브젝트 수에 비례       │
│                  │ 에 비례               │ (조명 수와 무관)         │
├──────────────────┼──────────────────────┼──────────────────────────┤
│ 셰이더 작성      │ Surface Shader       │ Shader Graph             │
│                  │ (.shader / Cg)       │ 또는 HLSL 직접 작성      │
├──────────────────┼──────────────────────┼──────────────────────────┤
│ 배칭             │ Static/Dynamic       │ SRP Batcher              │
│                  │ Batching (제한적)    │ (셰이더 호환만 되면 적용)│
├──────────────────┼──────────────────────┼──────────────────────────┤
│ 확장성           │ 제한적               │ ScriptableRendererFeature│
│                  │ (엔진 내부 고정)     │ 로 커스텀 패스 추가      │
├──────────────────┼──────────────────────┼──────────────────────────┤
│ 리소스 관리      │ 수동                 │ Render Graph             │
│ (Unity 6+)      │                      │ (자동 최적화)            │
├──────────────────┼──────────────────────┼──────────────────────────┤
│ 모바일 적합성    │ 낮음                 │ 높음                     │
│                  │ (멀티패스 비용,      │ (모바일 우선 설계,       │
│                  │  Deferred 비현실적)  │  싱글패스, SRP Batcher)  │
├──────────────────┼──────────────────────┼──────────────────────────┤
│ 유지 보수        │ 레거시               │ 활발히 업데이트 중       │
│                  │ (신규 기능 추가 없음)│ (Unity의 주력 파이프라인)│
└──────────────────┴──────────────────────┴──────────────────────────┘

조명 처리

Built-in의 멀티패스 포워드에서는 오브젝트마다 조명 수만큼 드로우콜이 발생합니다. 예를 들어 오브젝트 100개에 픽셀 라이트 4개가 있으면, 드로우콜이 최대 400회까지 늘어납니다. URP의 싱글패스 포워드에서는 같은 조건에서도 오브젝트당 1회, 총 100회로 유지됩니다. 조명이 많을수록 이 차이는 더 커집니다.

셰이더

Built-in 파이프라인에서는 Surface Shader라는 Unity 고유의 셰이더 작성 방식을 사용합니다. 개발자가 표면의 속성(색상, 반사율, 노멀 등)만 정의하면, 엔진이 이를 바탕으로 멀티패스 조명 처리에 필요한 버텍스/프래그먼트 셰이더 코드를 자동 생성합니다. 조명 모델을 직접 구현하지 않아도 되지만, 엔진이 생성한 최종 셰이더의 동작을 세밀하게 제어하기 어렵습니다.

URP에서는 Shader Graph(노드 기반 비주얼 셰이더 에디터)를 사용하거나, HLSL을 직접 작성합니다. URP의 셰이더는 싱글패스에 맞게 설계되어, 모든 조명을 하나의 셰이더 안에서 루프로 처리하는 방식입니다. Surface Shader는 URP에서 지원되지 않으므로, Built-in에서 URP로 전환할 때 셰이더를 다시 작성해야 합니다.

배칭

Built-in 파이프라인의 Static BatchingDynamic Batching은 같은 머티리얼을 공유하는 오브젝트의 메쉬를 합쳐 드로우콜을 줄이는 기법입니다. 하지만 Static Batching은 오브젝트가 움직이지 않아야 하고 메모리 사용량이 늘어나며, Dynamic Batching은 버텍스 수가 적은 메쉬에만 적용됩니다. 또한 멀티패스 구조에서는 배칭으로 드로우콜을 줄이더라도, 조명마다 패스가 반복되므로 절감 효과가 상쇄됩니다.

URP에서는 SRP Batcher라는 배칭 시스템이 도입되었습니다. SRP Batcher는 드로우콜 자체를 줄이는 대신, 같은 셰이더 배리언트(Shader Variant)를 사용하는 드로우콜 사이의 GPU 상태 변경 비용(SetPass Call)을 줄이는 방식으로 동작합니다. 각 배칭 기법의 상세한 동작 원리는 Part 2에서 다룹니다.

확장성

Built-in 파이프라인은 렌더링 과정이 엔진 내부에 고정되어 있어 커스텀 렌더 패스를 추가하기 어렵습니다. CommandBuffer를 통해 특정 시점에 추가 명령을 삽입하는 것은 가능하지만, 전체 렌더링 흐름 자체를 제어할 수는 없습니다.

URP에서는 ScriptableRendererFeature를 통해 커스텀 렌더 패스를 파이프라인의 원하는 시점에 삽입할 수 있습니다. 예를 들어, 아웃라인 효과를 위한 별도의 렌더 패스를 Opaque Rendering 이후에 추가하거나, 특정 레이어의 오브젝트만 별도로 렌더링하는 패스를 삽입하는 것이 가능합니다. 이 확장은 Universal Renderer의 설정에서 관리되며, 코드나 에디터 인터페이스를 통해 추가하거나 제거할 수 있습니다.


렌더 파이프라인 선택과 모바일

URP는 조명 처리, 배칭, 리소스 관리, 확장성 등 여러 항목에서 Built-in보다 효율적인 구조를 갖추고 있습니다. CPU, GPU, 메모리 자원이 모두 제한적인 모바일 환경에서 이 차이는 특히 큰 의미를 갖습니다.


1
2
3
4
5
6
7
8
모바일 환경에서 URP가 유리한 구조적 이유

URP 기능                     모바일에서의 이점                절감 자원
───────────────────────────────────────────────────────────────────
싱글패스 포워드               드로우콜 감소                   CPU
SRP Batcher                  셋 패스 콜 감소                 CPU
Render Graph                 불필요한 패스 제거, 메모리 재사용  GPU 메모리, 대역폭
ScriptableRendererFeature    불필요한 렌더링 기능 제거         CPU, GPU


Built-in 파이프라인은 더 이상 신규 기능이 추가되지 않으며, Unity 공식 문서에서도 신규 프로젝트에는 URP 또는 HDRP를 권장합니다. 기존 Built-in 프로젝트를 URP로 전환하면 셰이더 변환과 에셋 수정 등의 작업이 수반되지만, 싱글패스 렌더링, SRP Batcher, Render Graph 등 모바일 성능 최적화에 구조적으로 유리한 기반을 확보할 수 있습니다.


GPU 아키텍처에서 렌더 파이프라인까지

이전 글에서 다룬 GPU 아키텍처와 이 글에서 다룬 렌더 파이프라인을 하나의 흐름으로 연결하면, 씬이 화면에 표시되기까지의 전체 과정이 드러납니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
전체 렌더링 흐름

씬 (오브젝트, 머티리얼, 조명, 카메라)
        │
        ▼
렌더 파이프라인 (CPU)
  컬링 → 정렬 → 라이팅 패스 결정 → 드로우콜 생성
  Built-in: 멀티패스 → 드로우콜 多
  URP:      싱글패스 → 드로우콜 少
        │
        ▼ 드로우콜 제출
GPU 하드웨어
  데스크톱:  정점 처리 → 래스터화 → 프래그먼트 처리 → 프레임 버퍼
  모바일:    정점 처리 + 타일링 → 타일 메모리 → 타일별 프래그먼트 → 프레임 버퍼


렌더 파이프라인이 CPU 측에서 드로우콜을 구성하고, GPU가 실제 픽셀을 계산합니다. Built-in과 URP의 차이는 이 흐름에서 CPU가 GPU에 제출하는 드로우콜의 수에 나타납니다.


마무리

  • 렌더 파이프라인은 씬의 오브젝트를 GPU가 실행할 드로우콜로 변환하는 CPU 측 제어 계층입니다.
  • Built-in의 멀티패스 포워드에서는 조명 하나당 별도 패스를 실행하며, 드로우콜이 오브젝트 수 x 조명 수에 비례합니다.
  • SRP는 렌더링 과정을 C#으로 제어하는 프레임워크이며, URP와 HDRP가 그 위에 구축되어 있습니다.
  • URP의 싱글패스 포워드에서는 모든 조명을 한 패스에서 처리하여, 드로우콜이 오브젝트 수에만 비례합니다.
  • Render Graph(Unity 6+)는 렌더 패스 간 리소스 의존성을 그래프로 관리하여 불필요한 패스 제거와 메모리 재사용을 자동으로 수행합니다.

싱글패스 설계는 URP의 출발점입니다. SRP Batcher, Render Graph, ScriptableRendererFeature 같은 URP의 최적화 기능들은 모두 이 싱글패스 구조 위에서 동작합니다.


싱글패스가 드로우콜 수를 줄인다는 것은 확인했지만, 드로우콜 하나가 CPU와 GPU 사이에서 구체적으로 어떤 비용을 발생시키는지, SRP Batcher가 그 비용을 어떻게 줄이는지는 아직 다루지 않았습니다.

Part 2에서는 드로우콜의 내부 동작과 배칭 기법의 원리를 다룹니다.



관련 글

시리즈

Tags: Unity, URP, 렌더파이프라인, 모바일, 최적화

Categories: ,