Unity 렌더 파이프라인 (1) - Built-in과 URP의 구조 - soo:bak
작성일 :
렌더 파이프라인의 역할
GPU 아키텍처 (2) - 모바일 GPU와 TBDR에서 모바일 GPU가 TBDR(Tile-Based Deferred Rendering) 구조로 동작하는 과정을 살펴보았습니다.
GPU 하드웨어는 정점을 변환하고, 삼각형을 래스터화하고, 픽셀의 색상을 계산하는 물리적인 실행 장치입니다. 하지만 어떤 오브젝트를 어떤 순서로 그릴지, 조명을 어떻게 적용할지는 GPU가 스스로 결정하지 않습니다.
이러한 결정을 CPU가 내리고, GPU가 이를 실행하여 3D 씬 데이터를 최종 화면 이미지로 만드는 전체 처리 흐름을 렌더 파이프라인(Render Pipeline)이라 합니다.
렌더 파이프라인에서 CPU가 GPU의 그리기 명령(드로우콜(Draw Call))을 준비하는 과정은 네 단계로 나뉩니다.
렌더 파이프라인이 이 네 단계를 처리하는 방식은 파이프라인의 종류에 따라 달라지며, 이 차이가 렌더링 성능과 표현 범위에 직접 영향을 줍니다.
Unity에는 여러 종류의 렌더 파이프라인이 존재합니다.
가장 먼저 제공된 것이 Built-in 렌더 파이프라인이고, 모바일을 포함한 넓은 범위의 플랫폼에서 성능을 우선시하는 것이 URP(Universal Render Pipeline), PC와 콘솔에서 고품질 비주얼을 목표로 하는 것이 HDRP(High Definition Render Pipeline)입니다.
URP는 넓은 플랫폼 호환성과 런타임 성능에 초점을 맞추고, HDRP는 Volumetric Lighting, Subsurface Scattering, Ray Tracing 등 고급 렌더링 기능을 지원하는 대신 높은 하드웨어 사양을 요구합니다.
Built-in과 URP는 셰이더 시스템, 포스트 프로세싱, 카메라 구조 등 여러 면에서 다르지만, 렌더링 성능에 가장 직접적인 영향을 미치는 차이는 조명 처리 방식과 배칭 구조입니다. 이 글에서는 조명 처리 방식의 차이를 다루고, 배칭은 Part 2에서 이어집니다.
포워드 렌더링 (Forward Rendering)
Built-in과 URP 모두 포워드 렌더링(Forward Rendering)을 기본 렌더링 방식으로 사용합니다.
포워드 렌더링은 오브젝트를 하나씩 그리면서 지오메트리 처리와 조명 계산을 분리하지 않고 수행하여, 최종 픽셀 색상을 바로 출력하는 방식입니다.
멀티패스 포워드 렌더링
Built-in 파이프라인의 포워드 렌더링은 하나의 오브젝트를 조명 수만큼 반복해서 그리는 멀티패스(Multi-pass) 방식으로 동작합니다. 조명 하나당 별도의 렌더링 패스를 실행하므로, 조명이 3개이면 같은 오브젝트를 3번 그립니다.
첫 번째 패스인 ForwardBase는 메인 Directional Light, 환경광(Ambient), 라이트맵(Lightmap), 그림자 등 씬 전체에 공통으로 적용되는 기본 조명을 한 번에 처리합니다.
추가 조명(Point Light, Spot Light, 추가 Directional Light 등)이 있으면, 조명 하나마다 ForwardAdd 패스가 추가로 실행됩니다. ForwardAdd는 같은 오브젝트를 다시 그리면서 해당 조명만 계산하고, 그 결과를 ForwardBase의 픽셀 색상 위에 가산 블렌딩(Additive Blending)으로 누적합니다.
조명이 메인 Directional Light 하나뿐인 씬에서는 ForwardAdd가 실행되지 않으므로, ForwardBase 패스만으로 렌더링이 완료됩니다.
멀티패스의 비용 계산
멀티패스 구조에서는 조명 하나마다 같은 오브젝트를 다시 그리므로, 드로우콜 수가 오브젝트 수와 조명 수의 곱에 비례합니다.
오브젝트 10개, 조명 3개(Directional 1 + Point 2) — 오브젝트당 ForwardBase 1회 + ForwardAdd 2회 = 3회, 전체 10 × 3 = 드로우콜 30회
| ForwardBase (메인+환경광) | ForwardAdd (Point) | ForwardAdd (Point) | |
|---|---|---|---|
| 오브젝트 A | ① | ② | ③ |
| 오브젝트 B | ④ | ⑤ | ⑥ |
| 오브젝트 C | ⑦ | ⑧ | ⑨ |
| … | |||
| 오브젝트 J | ㉘ | ㉙ | ㉚ |
조명의 품질 등급
이 비용을 줄이기 위해 Unity의 Built-in 파이프라인은 모든 조명에 ForwardAdd 패스를 생성하는 대신, 조명을 품질 등급으로 나누어 처리합니다. 각 오브젝트를 렌더링할 때 조명의 밝기와 오브젝트까지의 거리 등을 기준으로 중요도를 판단하여, 픽셀 라이트(Pixel Light), 정점 라이트(Vertex Light), SH 라이트(Spherical Harmonics Light) 세 등급으로 분류합니다. 이 중 ForwardAdd 패스를 생성하는 것은 픽셀 라이트뿐입니다.
픽셀 라이트로 분류되는 조명의 수는 Quality Settings의 Pixel Light Count로 제한되며, 기본값은 4입니다. 이 제한을 초과하는 조명은 정점 라이트 또는 SH 라이트로 강등됩니다.
픽셀 라이트는 화면의 모든 픽셀에서 각 조명을 개별 계산하며, 세 등급 중 가장 정밀합니다.
정점 라이트는 각 조명을 메쉬의 정점에서만 계산하고, 정점 사이의 표면은 그 값을 부드럽게 이어서 채웁니다. 계산 지점이 정점에만 있으므로, 정점이 적은 로우폴리 메쉬에서는 조명이 뭉개져 보일 수 있습니다.
SH 라이트는 각 조명을 개별적으로 계산하지 않고, 조명 환경 전체를 소수의 수학적 계수(Unity에서는 색상 채널당 9개)로 요약하여 근사합니다. 이 계수는 빛이 대체로 어느 방향에서 오고 전체적으로 어떤 색조를 띠는지 같은 큰 경향만 담을 수 있으므로, 비용이 가장 낮지만 날카로운 그림자 경계나 하이라이트 같은 세밀한 조명 변화는 표현하지 못합니다.
정점 라이트와 SH 라이트는 ForwardBase 패스 안에서 함께 처리되므로 추가 드로우콜이 발생하지 않습니다.
멀티패스의 장점과 단점
멀티패스에서는 각 패스가 조명 하나만 처리하므로 셰이더 구현이 간결합니다. 조명이 늘어도 같은 셰이더로 패스만 추가하면 되므로, 셰이더 자체에 조명 수 제한이 없습니다.
대신 조명이 늘어날수록 비용도 함께 커집니다. 드로우콜이 늘어나 CPU가 GPU 상태를 설정하고 명령을 제출하는 작업이 누적되고, GPU도 같은 오브젝트의 같은 픽셀을 조명마다 반복 셰이딩해야 합니다.
디퍼드라는 대안
Built-in 파이프라인은 디퍼드 렌더링(Deferred Rendering) 모드도 지원합니다. 포워드 렌더링에서는 오브젝트를 그릴 때 조명까지 함께 계산하지만, 디퍼드 렌더링에서는 이름 그대로 조명 계산을 뒤로 미루어(defer) 오브젝트를 그리는 단계와 조명을 계산하는 단계를 분리합니다.
디퍼드 렌더링은 Built-in만의 기능이 아닙니다. URP도 Unity 2021.2(URP 12) 이상에서 Deferred를 선택할 수 있고, HDRP는 Deferred가 기본 렌더링 방식입니다. 다만 G-Buffer의 메모리·대역폭 부담 때문에 모바일에서는 어느 파이프라인이든 Forward가 현실적인 선택입니다. 각 파이프라인의 디퍼드 지원 범위와 차이는 Unity 렌더링 (3) - Render Pipeline 개요에서 다룹니다.
디퍼드 렌더링의 첫 번째 단계는 모든 오브젝트를 한 번씩 그리는 Geometry Pass입니다. Geometry Pass에서는 각 오브젝트의 정점을 변환하고 래스터화하여 프래그먼트를 생성한 뒤, 프래그먼트 셰이더가 텍스처를 샘플링하고 법선을 계산합니다. 다만 이 단계의 프래그먼트 셰이더는 조명을 계산하지 않고, 표면 정보(색상, 법선, 반사, 깊이 등)를 여러 장의 화면 크기 텍스처에 나누어 기록합니다. 이 텍스처 전체를 G-Buffer(Geometry Buffer)라 합니다.
두 번째 단계는 Lighting Pass입니다. G-Buffer가 완성되면 CPU가 조명마다 드로우콜을 하나씩 제출합니다. 각 드로우콜은 씬의 오브젝트 대신 조명의 영향 범위를 나타내는 단순한 도형(Directional Light는 화면 전체를 덮는 사각형, Point Light는 구, Spot Light는 원뿔)을 그리고, 이 도형의 프래그먼트 셰이더가 G-Buffer에서 해당 픽셀의 표면 정보를 읽어 조명 결과를 계산합니다.
멀티패스에서 드로우콜이 오브젝트 수 × 조명 수에 비례한다면, 디퍼드에서는 오브젝트 수 + 조명 수에 비례합니다. 오브젝트는 Geometry Pass에서 한 번만 그리고, 조명이 추가되면 Lighting Pass 드로우콜이 하나만 추가되기 때문입니다.
디퍼드에서 드로우콜은 줄어들지만, G-Buffer에 Diffuse, Normal, Specular, Depth 등의 표면 정보를 각각 화면 크기 텍스처로 유지해야 합니다. 1920×1080 해상도 기준으로 텍스처 하나가 수 MB에 달하며, 이런 텍스처를 여러 장 동시에 유지하면 매 프레임 수십 MB의 메모리 읽기/쓰기가 발생합니다.
데스크톱·콘솔 GPU는 이 대역폭을 감당할 수 있어 디퍼드 렌더링이 널리 사용됩니다. 하지만 GPU 아키텍처 (2) - 모바일 GPU와 TBDR에서 살펴본 것처럼 모바일 GPU는 메모리 대역폭이 제한적이고, TBDR 구조의 온칩 타일 메모리도 한정되어 있습니다. 이 환경에서 매 프레임 G-Buffer 여러 장을 읽고 쓰는 부담은 크므로, 모바일에서 Built-in Deferred는 현실적인 선택이 되지 못합니다.
결국 Built-in 파이프라인에서 모바일 게임이 선택할 수 있는 현실적인 방식은 포워드 렌더링이며, 멀티패스로 인한 드로우콜 증가가 주요 병목으로 남습니다.
SRP와 URP의 등장
Built-in 파이프라인에는 성능 문제 외에 구조적 한계도 있었습니다. 렌더링 과정(컬링, 정렬, 라이팅 패스, 드로우콜 제출)이 Unity 엔진 내부의 C++ 코드에 고정되어 있어, 불필요한 패스를 제거하거나 프로젝트에 맞는 커스텀 패스를 추가하려면 엔진 소스 코드를 직접 수정해야 했습니다.
Unity는 이 한계를 해결하기 위해 2018년(Unity 2018.1)에 SRP(Scriptable Render Pipeline) 아키텍처를 도입했습니다.
SRP란
SRP는 렌더링 과정을 C# 스크립트로 제어할 수 있게 하는 프레임워크입니다. Built-in에서 엔진이 강제하던 렌더링 흐름을, 개발자가 렌더 패스를 추가하거나 제거하거나 수정하는 방식으로 직접 정의할 수 있습니다.
SRP Core는 컬링 실행, 커맨드 버퍼(Command Buffer) 구성, 렌더 타겟(Render Target) 관리 등 렌더링에 필요한 기본 기능을 C# API로 제공합니다. URP와 HDRP는 이 API를 사용하여 각자의 렌더링 방식을 구현한 파이프라인입니다.
URP의 포워드 렌더링
URP가 싱글패스로 드로우콜을 줄이는 구조와, 렌더 패스가 어떻게 구성되는지 살펴봅니다.
싱글패스 포워드 렌더링
URP는 멀티패스 대신 싱글패스 포워드 렌더링(Single-pass Forward Rendering)을 사용합니다. 멀티패스에서는 조명마다 별도의 드로우콜로 셰이더를 다시 실행했다면, 싱글패스에서는 오브젝트를 그리는 드로우콜 하나에서 프래그먼트 셰이더가 모든 조명을 순회하며 최종 색상을 계산합니다. Built-in의 ForwardBase와 ForwardAdd가 하나로 합쳐진 셈이며, URP에서는 이 통합 패스를 ForwardLit이라 합니다.
위 다이어그램처럼 URP 싱글패스에서는 조명 수와 관계없이 오브젝트당 드로우콜이 1회이므로, 전체 드로우콜은 오브젝트 수에만 비례합니다.
드로우콜 감소 효과
같은 씬 구성(오브젝트 10개, 조명 3개)으로 비교하면, ForwardLit 패스가 오브젝트당 1회 실행되므로 오브젝트 10개 × 1 = 드로우콜 10회입니다.
| 씬 구성 | Built-in 멀티패스 | URP 싱글패스 |
|---|---|---|
| 오브젝트 10, 조명 3 | 30회 | 10회 |
| 오브젝트 50, 조명 4 | 200회 | 50회 |
| 오브젝트 100, 조명 4 | 400회 | 100회 |
공식으로 표현하면 Built-in은 오브젝트 수 × 조명 수, URP는 오브젝트 수입니다. 조명이 늘어날수록 차이가 커집니다.
다만 URP에서도 오브젝트당 추가 조명 수에는 제한이 있습니다. 메인 Directional Light를 제외한 추가 조명은 오브젝트당 최대 8개까지 처리됩니다.
이 제한을 적용하기 위해 URP는 렌더링 전에 각 오브젝트에 영향을 미치는 조명을 선별합니다. Point Light와 Spot Light는 범위(Range)를 가지므로 오브젝트의 바운딩 볼륨이 이 범위와 겹치는 조명만 후보가 되고, 추가 Directional Light는 범위 제한이 없으므로 항상 후보에 포함됩니다. URP는 이 후보를 밝기와 오브젝트까지의 거리를 기준으로 중요도 순으로 정렬한 뒤, 상위 8개만 선별하여 오브젝트별 조명 인덱스 리스트로 구성하고 GPU 메모리의 상수 버퍼에 기록합니다.
상위 8개 밖의 조명은 해당 오브젝트에 아예 반영되지 않습니다. 예를 들어 씬에 추가 조명이 20개 있더라도, 각 오브젝트는 자기 주변에서 가장 중요한 8개만 처리하고 나머지 12개는 무시합니다. Built-in에서는 초과 조명이 정점 라이트나 SH 라이트로 강등되어 낮은 품질로라도 반영되었지만, URP에서는 강등 없이 제외됩니다.
이 제한은 URP Asset → Lighting → Additional Lights → Per Object Limit에서 0~8 범위로 조정할 수 있습니다. 모바일에서 하나의 오브젝트에 다수의 실시간 추가 조명이 동시에 영향을 미치는 경우는 드물어, 대부분 기본값으로 충분합니다.
싱글패스가 가능한 이유
싱글패스가 가능하려면, 프래그먼트 셰이더가 여러 조명을 하나의 루프로 순회할 수 있어야 합니다. 조명 수는 오브젝트마다 다르고 프레임마다 바뀌므로, 이 루프는 반복 횟수가 실행 중에 결정되는 동적 루프여야 합니다.
Built-in 파이프라인이 설계된 시기(2005)에 주류였던 셰이더 모델 SM 2.0에서는 루프 반복 횟수가 컴파일 시점에 확정되어야 했고, 상수 레지스터 수도 적어 여러 조명 데이터를 한 패스에 담을 수 없었습니다. 그래서 Built-in은 조명 하나당 별도의 패스를 실행하는 멀티패스 방식을 택할 수밖에 없었습니다.
이 제약은 데스크톱에서는 SM 3.0(2004)부터, 모바일에서는 그래픽스 API 표준인 OpenGL ES 3.0(2012)부터 해소되었습니다. 2015년 이후 대부분의 모바일 기기가 OpenGL ES 3.0에 대응하면서, 모든 플랫폼에서 싱글패스가 가능한 환경이 갖춰졌습니다.
URP는 OpenGL ES 3.0을 최소 요구 사양으로 설정하여 싱글패스를 구현합니다. CPU는 프레임마다 모든 추가 조명의 데이터를 GPU 메모리에 기록합니다. 이후 드로우콜마다 선별된 조명의 인덱스와 개수를 상수 버퍼에 설정하면, 프래그먼트 셰이더가 이 인덱스로 조명 데이터를 읽어 동적 루프로 순회하고 한 번의 패스에서 처리합니다.
URP의 렌더링 구조
프레임을 완성하려면 조명 외에도 여러 단계가 필요합니다. 깊이값을 기록하는 단계, 불투명 오브젝트를 그리는 단계, 투명 오브젝트를 그리는 단계, 후처리를 적용하는 단계 등이 각각 별도로 실행됩니다. URP는 이 단계 하나하나를 렌더 패스(Render Pass)라는 단위로 나누어 순서대로 실행합니다.
Renderer와 Render Pass
어떤 렌더 패스를 어떤 순서로 실행할지는 Renderer가 정의합니다. URP의 기본 Renderer인 Universal Renderer는 다음과 같은 순서로 렌더 패스를 실행합니다.
Depth Prepass는 GPU가 모든 불투명 오브젝트의 깊이값만 깊이 버퍼에 먼저 기록하는 단계입니다.
Depth Prepass가 가장 먼저 실행되는 이유는 Early-Z 테스트의 효과를 극대화하기 위해서입니다. 깊이 버퍼에 최종 깊이값이 미리 채워져 있으면, 이후 Opaque Rendering 단계에서 가려진 모든 픽셀의 셰이딩을 건너뛸 수 있습니다.
다만 Depth Prepass를 활성화하면 모든 불투명 오브젝트의 정점 변환과 래스터화가 Depth Prepass와 Opaque Rendering에서 두 번 수행됩니다. 정점 변환과 래스터화는 프래그먼트 셰이딩에 비해 비용이 낮으므로, 오브젝트끼리 많이 겹치는 씬에서는 Early-Z로 절감되는 셰이딩 비용이 이 중복을 충분히 상쇄합니다. 반면 겹침이 적은 씬에서는 절감 효과가 작아 오히려 손해가 될 수 있습니다.
Opaque Rendering은 불투명 오브젝트를 카메라로부터 가까운 순서대로(Front-to-Back) 정렬하여 그리는 단계입니다. 가까운 것부터 그리면 뒤에 가려진 픽셀이 깊이 테스트에서 걸러지므로 불필요한 셰이딩이 줄어듭니다.
Skybox는 하늘 배경을 그리는 단계입니다. 불투명 렌더링 이후에 실행되므로, 오브젝트가 이미 그려진 픽셀은 깊이 테스트에서 걸러지고 하늘이 보이는 픽셀만 셰이딩됩니다.
Transparent Rendering은 투명 오브젝트를 뒤에서 앞으로(Back-to-Front) 정렬하여 그리는 단계입니다. 알파 블렌딩으로 혼합할 배경색이 화면에 먼저 존재해야 하므로, 먼 오브젝트부터 그립니다.
Post-Processing은 렌더링이 완료된 이미지에 블룸(Bloom), 색보정(Color Grading), 안티앨리어싱(Anti-Aliasing) 등의 화면 전체 효과를 적용하는 단계입니다. 효과마다 화면의 모든 픽셀을 처리하는 풀스크린 패스가 추가되므로, 모바일에서는 효과 수를 최소화하는 것이 일반적입니다.
Final Blit는 렌더링 결과를 화면의 백버퍼(Back Buffer)에 복사하는 마지막 단계입니다.
후처리가 비활성화되어 있으면 URP는 백버퍼에 직접 렌더링하므로, 별도의 복사 없이 프레임이 완성됩니다.
후처리가 활성화되어 있으면 백버퍼에 직접 렌더링할 수 없습니다. GPU는 같은 렌더 타겟을 동시에 읽고 쓸 수 없는데, 후처리 효과는 렌더링된 이미지 전체를 입력으로 읽어야 하기 때문입니다. 그래서 URP는 별도의 오프스크린 렌더 타겟(Off-screen Render Target)에 먼저 그린 뒤, 후처리를 적용하고, 최종 결과를 백버퍼에 복사합니다.
Render Graph (Unity 6+)
렌더 패스가 사용하는 GPU 리소스를 효율적으로 관리하기 위해, Unity 6부터 URP에 Render Graph 시스템이 도입되었습니다.
기존 방식의 한계
앞서 살펴본 Depth Prepass, Opaque Rendering, Post-Processing 등 각 렌더 패스는 실행 과정에서 렌더 타겟이나 임시 텍스처 등의 GPU 리소스를 사용합니다.
기존 URP(Unity 6 이전)에서는 이러한 리소스를 각 패스가 스스로 할당하고 해제했습니다. 리소스를 총괄하는 주체가 없으므로 패스 간에 어떤 리소스가 어디서 쓰이는지 파악할 수 없습니다.
앞선 패스가 이미 할당한 리소스를 재사용할 수 있는 상황에서도 패스마다 별도로 할당하므로 메모리가 낭비됩니다. 어떤 패스의 출력이 최종 프레임에 기여하지 않는다면 그 패스는 실행할 필요가 없지만, 이런 의존 관계를 파악할 수 없으므로 불필요한 패스도 항상 실행됩니다.
Render Graph의 도입
Render Graph는 각 렌더 패스가 선언한 텍스처 입출력 정보를 바탕으로 패스 간 의존 관계를 방향 비순환 그래프(DAG, Directed Acyclic Graph)로 구성하고, 이를 통해 리소스 재사용과 불필요한 패스 제거를 자동으로 수행합니다.
위 그래프에서 Depth와 Shadow 사이에는 화살표가 없습니다. 서로의 출력을 필요로 하지 않으므로 Render Graph는 두 패스가 독립적임을 알 수 있고, 실행 순서를 자유롭게 결정하거나 리소스를 효율적으로 재사용할 수 있습니다.
Render Graph의 최적화
Render Graph는 프레임을 실행하기 전에 이 그래프를 분석하여 두 가지 최적화를 자동으로 수행합니다.
첫 번째 최적화는 불필요한 패스의 자동 제거(Culling)입니다. 어떤 패스의 출력을 이후의 어떤 패스도 읽지 않는다면, 그 패스는 실행할 필요가 없습니다.
Render Graph는 최종 출력(화면에 표시되는 결과)에서 출발하여 그래프를 역방향으로 탐색하고, 최종 출력에 기여하지 않는 패스를 자동으로 제거합니다. 예를 들어 그림자가 비활성화되어 ShadowMap을 아무 패스도 읽지 않으면, Shadow 패스가 자동으로 생략됩니다.
두 번째 최적화는 리소스 수명 관리와 메모리 재사용(Aliasing)입니다. 각 텍스처의 수명(처음 쓰여지는 시점부터 마지막으로 읽히는 시점까지)을 그래프에서 파악할 수 있으므로, 수명이 끝난 텍스처의 메모리를 이후 텍스처가 그대로 재사용합니다.
수명이 겹치지 않는 텍스처끼리 같은 물리 메모리를 공유하므로, 전체 메모리 사용량이 줄어듭니다. 메모리가 제한된 모바일 환경에서 이 최적화의 효과가 특히 큽니다.
Built-in vs URP 비교 요약
지금까지 살펴본 URP와 Built-in 파이프라인의 차이를 정리해보면 다음과 같습니다.
| 항목 | Built-in 파이프라인 | URP |
|---|---|---|
| 조명 처리 | 멀티패스 포워드 (조명마다 별도 패스) | 싱글패스 포워드 (모든 조명을 한 패스에서) |
| 드로우콜 | 오브젝트 × 조명 수에 비례 | 오브젝트 수에 비례 (조명 수와 무관) |
| 셰이더 작성 | Surface Shader (.shader / Cg) | Shader Graph 또는 HLSL 직접 작성 |
| 배칭 | Static/Dynamic Batching (제한적) | SRP Batcher (주) + Static/Dynamic Batching |
| 확장성 | 제한적 (엔진 내부 고정) | ScriptableRendererFeature로 커스텀 패스 추가 |
| 리소스 관리 (Unity 6+) | 패스별 개별 할당 | Render Graph (자동 최적화) |
| 모바일 적합성 | 낮음 (멀티패스 비용) | 높음 (싱글패스, SRP Batcher, 경량 설계) |
| 유지 보수 | 레거시 (신규 기능 추가 없음) | 활발히 업데이트 중 (Unity의 주력 파이프라인) |
셰이더
Built-in 파이프라인에서는 Surface Shader라는 Unity 고유의 셰이더 작성 방식을 사용합니다. 개발자가 표면의 속성(색상, 반사율, 노멀 등)만 정의하면, 엔진이 이를 바탕으로 멀티패스 조명 처리에 필요한 버텍스/프래그먼트 셰이더 코드를 자동 생성합니다. 조명 모델을 직접 구현하지 않아도 되지만, 엔진이 생성한 최종 셰이더의 동작을 세밀하게 제어하기 어렵습니다.
URP에서는 Shader Graph(노드 기반 비주얼 셰이더 에디터)를 사용하거나, HLSL을 직접 작성합니다. Surface Shader는 URP에서 지원되지 않으므로, Built-in에서 URP로 전환할 때 셰이더를 다시 작성해야 합니다.
배칭
Built-in 파이프라인의 Static Batching과 Dynamic Batching은 같은 머티리얼을 공유하는 오브젝트의 메쉬를 합쳐 드로우콜을 줄이는 기법입니다. 하지만 Static Batching은 오브젝트가 움직이지 않아야 하고 메모리 사용량이 늘어나며, Dynamic Batching은 버텍스 수가 적은 메쉬에만 적용됩니다. 또한 멀티패스 구조에서는 배칭으로 패스당 드로우콜을 줄이더라도, 조명마다 패스 자체가 반복되므로 드로우콜 총량의 근본적인 해결은 되지 않습니다.
URP에서도 Static Batching과 Dynamic Batching을 사용할 수 있지만, 주된 배칭 방식으로 SRP Batcher가 도입되었습니다. SRP Batcher는 드로우콜 자체를 줄이는 대신, 같은 셰이더 배리언트(Shader Variant)를 사용하는 드로우콜 사이에서 셰이더 패스 전환(SetPass Call)을 줄이는 방식으로 동작합니다. 각 배칭 기법의 상세한 동작 원리는 Part 2에서 다룹니다.
확장성
Built-in 파이프라인은 렌더링 과정이 엔진 내부에 고정되어 있어 커스텀 렌더 패스를 추가하기 어렵습니다. CommandBuffer를 통해 특정 시점에 추가 명령을 삽입하는 것은 가능하지만, 전체 렌더링 흐름 자체를 제어할 수는 없습니다.
URP에서는 ScriptableRendererFeature를 통해 커스텀 렌더 패스를 파이프라인의 원하는 시점에 삽입할 수 있습니다. 예를 들어, 아웃라인 효과를 위한 별도의 렌더 패스를 Opaque Rendering 이후에 추가하거나, 특정 레이어의 오브젝트만 별도로 렌더링하는 패스를 삽입하는 것이 가능합니다.
렌더 파이프라인 선택
지금까지 살펴본 URP의 구조적 이점을 정리하면 다음과 같습니다.
| URP 기능 | 이점 | 절감 자원 |
|---|---|---|
| 싱글패스 포워드 | 드로우콜 감소 | CPU |
| SRP Batcher | SetPass Call 감소 | CPU |
| Render Graph | 불필요한 패스 제거, 메모리 재사용 | GPU 메모리, 대역폭 |
Built-in 파이프라인은 더 이상 신규 기능이 추가되지 않으며, Unity 공식 문서에서도 신규 프로젝트에는 URP 또는 HDRP를 권장합니다. 기존 Built-in 프로젝트를 URP로 전환하면 셰이더 변환과 에셋 수정 등의 작업이 수반되지만, 싱글패스 렌더링, SRP Batcher, Render Graph 등 성능 최적화에 구조적으로 유리한 기반을 확보할 수 있습니다.
GPU 아키텍처에서 렌더 파이프라인까지
GPU 아키텍처와 렌더 파이프라인이 이어지는 전체 흐름은 다음과 같습니다.
렌더 파이프라인이 CPU에서 드로우콜을 구성하고, GPU가 이를 실행하여 최종 화면을 만듭니다.
마무리
- 렌더 파이프라인은 씬의 오브젝트를 GPU가 실행할 드로우콜로 변환하는 CPU 측 제어 계층입니다.
- Built-in의 멀티패스 포워드에서는 조명 하나당 별도 패스를 실행하며, 드로우콜이
오브젝트 수 × 조명 수에 비례합니다. - SRP는 렌더링 과정을 C#으로 제어하는 프레임워크이며, URP와 HDRP가 그 위에 구축되어 있습니다.
- URP의 싱글패스 포워드에서는 모든 조명을 한 패스에서 처리하여, 드로우콜이 오브젝트 수에만 비례합니다.
- Render Graph(Unity 6+)는 렌더 패스 간 리소스 의존성을 그래프로 관리하여 불필요한 패스 제거와 메모리 재사용을 자동으로 수행합니다.
싱글패스 설계는 URP의 핵심이며, SRP Batcher와 Render Graph가 이를 보완합니다.
Part 2에서는 드로우콜의 내부 동작과 배칭 기법의 원리를 다룹니다.
관련 글
시리즈
- Unity 렌더 파이프라인 (1) - Built-in과 URP의 구조 (현재 글)
- Unity 렌더 파이프라인 (2) - 드로우콜과 배칭
- Unity 렌더 파이프라인 (3) - 컬링과 오클루전
전체 시리즈
- 게임 루프의 원리 (1) - 프레임의 구조
- 게임 루프의 원리 (2) - CPU-bound와 GPU-bound
- 렌더링 기초 (1) - 메쉬의 구조
- 렌더링 기초 (2) - 텍스처와 압축
- 렌더링 기초 (3) - 머티리얼과 셰이더 기초
- GPU 아키텍처 (1) - GPU 병렬 처리와 렌더링 파이프라인
- GPU 아키텍처 (2) - 모바일 GPU와 TBDR
- Unity 렌더 파이프라인 (1) - Built-in과 URP의 구조 (현재 글)
- Unity 렌더 파이프라인 (2) - 드로우콜과 배칭
- Unity 렌더 파이프라인 (3) - 컬링과 오클루전
- 스크립트 최적화 (1) - C# 실행과 메모리 할당
- 스크립트 최적화 (2) - Unity API와 실행 비용
- 메모리 관리 (1) - 가비지 컬렉션의 원리
- 메모리 관리 (2) - 네이티브 메모리와 에셋
- 메모리 관리 (3) - Addressables와 에셋 전략
- UI 최적화 (1) - 캔버스와 리빌드 시스템
- UI 최적화 (2) - UI 최적화 전략
- 조명과 그림자 (1) - 실시간 조명과 베이크
- 조명과 그림자 (2) - 그림자와 후처리
- 셰이더 최적화 (1) - 셰이더 성능의 원리
- 셰이더 최적화 (2) - 셰이더 배리언트와 모바일 기법
- 물리 최적화 (1) - 물리 엔진의 실행 구조
- 물리 최적화 (2) - 물리 최적화 전략
- 파티클과 애니메이션 (1) - 파티클 시스템 최적화
- 파티클과 애니메이션 (2) - 애니메이션 최적화
- 프로파일링 (1) - Unity Profiler와 Frame Debugger
- 프로파일링 (2) - 모바일 프로파일링
- 모바일 전략 (1) - 발열과 배터리
- 모바일 전략 (2) - 빌드와 품질 전략