조명과 그림자 (2) - 그림자와 후처리 - soo:bak
작성일 :
빛이 있으면 그림자가 있습니다
Part 1에서 실시간 조명과 베이크 조명의 비용 구조를 다루었습니다. 조명은 화면의 밝기와 색감을 결정합니다. 광원이 오브젝트 표면에 닿으면 밝아지고, 닿지 않으면 어두워집니다. 이 밝고 어두운 영역의 대비가 장면의 분위기를 형성합니다.
그런데 조명만으로는 공간감이 충분하지 않습니다.
실제 세계에서 빛이 오브젝트에 가로막히면 그 뒤에 어두운 영역, 즉 그림자(Shadow)가 생깁니다. 그림자는 오브젝트가 바닥 위에 서 있다는 느낌, 건물이 땅에 뿌리를 내리고 있다는 느낌을 만듭니다. 그림자가 없으면 오브젝트가 공중에 떠 있는 것처럼 보입니다.
실시간 그림자는 조명 계산과 별도의 렌더링 과정을 필요로 합니다. 광원의 위치에서 장면을 한 번 더 렌더링해야 하므로 비용이 큽니다.
렌더링이 끝난 뒤 화면 전체에 추가 효과를 적용하는 포스트 프로세싱(Post-Processing)도 별도의 렌더 패스를 추가하므로, 그림자와 함께 모바일 프레임 예산에서 큰 비중을 차지합니다.
Shadow Map의 원리
실시간 그림자를 구현하는 가장 일반적인 방법은 Shadow Map 기법입니다. 광원의 시점에서 보이지 않는 곳이 곧 그림자라는 원리를 두 단계로 구현합니다.
1단계: 라이트 시점에서 깊이 텍스처 생성
GPU는 광원의 위치와 방향을 카메라로 삼아 장면을 렌더링합니다.
색상을 계산하는 일반 렌더링과 달리, 광원에서 각 오브젝트 표면까지의 거리 — 깊이(depth) — 만 기록합니다. 광원에서 보이는 모든 픽셀에 대해 가장 가까운 표면까지의 거리가 저장된 텍스처가 Shadow Map입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Shadow Map 생성 (1단계)
광원이 카메라가 되어 장면을 바라봄:
광원 ●
|
|── 거리 5 ──→ 건물 ██
|
|── 거리 12 ─→ 나무 /▲\
|
|── 거리 20 ─→ 바닥 ───
이 거리를 픽셀마다 기록한 텍스처가 Shadow Map:
건물 영역 나무 영역 바닥 영역
┌────────┬────────┬────────┐
│ 5 │ 12 │ 20 │
└────────┴────────┴────────┘
Shadow Map은 색상이 아닌 깊이 값만 저장하는 단일 채널 텍스처입니다. 색상 텍스처가 R, G, B 채널로 색을 기록하는 것과 달리, 각 텍셀에 거리 값 하나만 기록합니다. 이 텍스처의 해상도가 Shadow Map의 품질을 결정합니다.
2단계: 카메라 시점에서 그림자 판별
GPU는 카메라 시점에서 장면을 렌더링합니다. 각 프래그먼트에 대해, 해당 위치를 1단계에서 사용한 광원 좌표계로 변환합니다. 변환된 좌표로 Shadow Map을 참조하면, 광원에서 해당 방향으로 가장 가까운 표면까지의 깊이를 읽을 수 있습니다.
이 깊이와 현재 프래그먼트가 광원에서 실제로 떨어진 거리를 비교합니다.
실제 거리가 Shadow Map의 깊이보다 크면, 프래그먼트와 광원 사이에 다른 오브젝트가 가로막고 있다는 뜻이므로 그 프래그먼트는 그림자 안에 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
그림자 판별 (2단계)
광원에서 각 점 방향으로 빛을 쏘았을 때:
광원 ●─── 점 A 방향 ──→ 건물(거리 5) ██ ··· 점 A(거리 15)
│ ↑ 가로막힘
│
└─── 점 B 방향 ────────────────────────→ 점 B(거리 20)
직접 도달
점 A: Shadow Map 깊이 5, 실제 거리 15 → 15 > 5 → 그림자
점 B: Shadow Map 깊이 20, 실제 거리 20 → 20 = 20 → 밝음
이 과정을 프레임의 모든 프래그먼트에 대해 수행하면, 장면에 그림자가 표현됩니다.
Shadow Map 해상도와 품질
Shadow Map은 깊이 정보를 텍셀 단위로 저장하므로, 해상도가 그림자의 선명도를 직접 결정합니다.
해상도가 높으면 하나의 텍셀이 차지하는 월드 공간 영역이 작아져 그림자 경계가 정밀해집니다. 반대로 해상도가 낮으면 하나의 텍셀이 넓은 영역을 대표하게 되어, 그림자 경계에서 계단 현상(aliasing)이 발생합니다.
1
2
3
4
5
6
7
8
9
10
11
Shadow Map 해상도에 따른 그림자 품질
낮은 해상도 (512×512) 높은 해상도 (2048×2048)
████ ██
██████ ████
████████ ██████
██████████ ████████
████████████ ██████████
████████████
→ 계단 현상 → 부드러운 경계
해상도를 높이면 품질이 향상되지만, 메모리와 렌더링 비용이 함께 증가합니다. 512×512 Shadow Map은 약 1MB, 2048×2048은 약 16MB를 차지합니다. 각 차원의 해상도를 4배 늘리면 텍셀 수가 16배 증가합니다. Shadow Map을 생성하기 위한 라이트 시점 렌더링 비용도 해상도에 비례합니다. 모바일에서는 1024×1024 또는 2048×2048이 일반적입니다.
Cascade Shadow Maps
해상도와 별개로, 단일 Shadow Map에는 구조적 한계가 있습니다.
하나의 Shadow Map으로 카메라가 보는 전체 영역을 커버하면, 가까운 오브젝트와 먼 오브젝트가 같은 텍셀 밀도를 공유합니다.
Shadow Map은 광원 시점에서 렌더링한 고정 해상도 텍스처이므로, 텍셀 하나가 대표하는 월드 공간 크기는 전체에 걸쳐 균일합니다. 하지만 카메라 시점에서는 가까운 오브젝트가 화면에서 크고, 먼 오브젝트가 화면에서 작습니다. 카메라 5m 앞의 캐릭터가 화면에서 수백 픽셀을 차지하더라도, 해당 영역을 표현하는 Shadow Map 텍셀은 수십 개에 불과할 수 있습니다.
적은 텍셀이 많은 화면 픽셀에 걸쳐 늘어나므로 계단 현상이 생깁니다.
반면 먼 곳의 오브젝트는 화면에서 작게 보이므로, 같은 텍셀 밀도라도 그림자 품질 저하가 눈에 띄지 않습니다.
가까운 곳일수록 텍셀이 부족하고 먼 곳일수록 텍셀이 남는 불균형이 생깁니다. 가까운 곳의 품질을 높이려면 전체 해상도를 올려야 하는데, 그만큼 먼 곳에는 불필요한 텍셀이 낭비됩니다.
Cascade Shadow Maps는 이 문제를 해결하기 위해 카메라의 시야 범위를 여러 구간(Cascade)으로 분할하고, 각 구간에 별도의 Shadow Map을 할당하는 기법입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Cascade Shadow Maps
단일 Shadow Map:
카메라 ───────────────────────────────────────▶ 먼 곳
[ Shadow Atlas 전체를 한 장으로 사용 ]
텍셀 밀도 균일 → 가까운 곳의 그림자가 거침
Cascade Shadow Maps (같은 Atlas를 4등분):
카메라 ───────────────────────────────────────▶ 먼 곳
[Cascade 0][ Cascade 1 ][ Cascade 2 ][ Cascade 3 ]
좁은 영역 중간 영역 넓은 영역 가장 넓은 영역
높은 밀도 중간 밀도 낮은 밀도 가장 낮은 밀도
각 캐스케이드의 해상도는 같지만 커버 영역이 다름
→ 좁은 구간일수록 텍셀 밀도가 높아 그림자가 선명
각 캐스케이드의 텍스처 해상도는 동일합니다.
다른 것은 각 캐스케이드가 담당하는 월드 공간의 범위입니다. 예를 들어 Cascade 0이 1024×1024로 0~10m를 커버하면 텍셀 하나당 약 1cm를 표현하고, Cascade 3이 같은 1024×1024로 50~100m를 커버하면 텍셀 하나당 약 5cm를 표현합니다.
가까운 구간의 그림자는 화면에서 크게 보이므로 텍셀 하나가 작은 영역을 담당해야 경계가 선명하고, 먼 구간의 그림자는 화면에서 작게 보이므로 텍셀 하나가 넓은 영역을 담당해도 충분합니다.
캐스케이드 분할은 이 시각적 중요도에 맞춰 텍셀을 배분합니다.
캐스케이드 수와 비용
캐스케이드를 늘리면 품질은 향상되지만, 캐스케이드 수만큼 라이트 시점에서 장면을 추가 렌더링해야 합니다.
1
2
3
4
5
캐스케이드 수에 따른 비용
캐스케이드 1개 렌더링 패스 1회 비용 낮음 품질 균일(전체적으로 낮음)
캐스케이드 2개 렌더링 패스 2회 비용 중간 가까운 곳 선명
캐스케이드 4개 렌더링 패스 4회 비용 높음 거리별 단계적 품질 배분
모바일에서는 1~2개 캐스케이드가 적당합니다.
캐스케이드 1개로도 Shadow Distance를 줄이면 가까운 곳의 그림자 품질을 확보할 수 있습니다. 4개 캐스케이드는 데스크톱이나 콘솔처럼 GPU 여유가 있는 환경에 적합합니다.
Shadow Distance
캐스케이드 수와 함께 그림자 품질에 큰 영향을 미치는 설정이 Shadow Distance입니다. Shadow Distance는 카메라로부터 Shadow Map이 적용되는 최대 거리입니다. 이 거리 너머의 오브젝트는 그림자를 생성하지도, 받지도 않습니다.
1
2
3
4
5
6
7
8
9
10
11
Shadow Distance에 따른 그림자 품질 (Shadow Map 해상도 1024×1024 동일)
Shadow Distance 100m:
카메라 ─────────────────────────────────────────▶ 100m
[ Shadow Map 커버 영역 ]
텍셀 하나당 ~10cm → 그림자 경계가 거침
Shadow Distance 30m:
카메라 ──────────────▶ 30m
[ Shadow Map 커버 ]
텍셀 하나당 ~3cm → 그림자 경계가 선명
모바일 게임의 카메라는 보통 캐릭터를 가까이에서 따라가며, 50m 이상 떨어진 오브젝트의 그림자 유무를 플레이어가 인지하기는 어렵습니다. Shadow Distance를 30~50m 수준으로 줄이면 가까운 곳의 그림자가 선명해지고, 먼 곳의 불필요한 그림자 계산을 아낄 수 있습니다.
모바일 그림자 대안
Shadow Map, Cascade, Shadow Distance는 모두 라이트 시점에서 장면을 추가로 렌더링하는 과정을 전제로 하므로 비용이 큽니다. 모바일 환경에서는 이 비용을 피하면서 그림자의 느낌을 유지하는 여러 대안이 사용됩니다.
Blob Shadow
Blob Shadow는 캐릭터의 발 아래에 단순한 원형 또는 타원형의 어두운 텍스처를 배치하는 기법입니다. 실시간 깊이 비교가 없으므로 Shadow Map이 전혀 필요하지 않습니다. 렌더링 비용은 텍스처가 입혀진 쿼드(quad, 사각형 폴리곤) 하나를 그리는 정도입니다.
1
2
3
4
5
6
7
8
Blob Shadow
캐릭터
■
/|\
/ \
░░░░░░ ← 광원 방향과 무관하게 발 아래에 원형 텍스처 배치
바닥────────
캐릭터가 팔을 벌리든 무기를 들고 있든, 그림자는 항상 같은 원형입니다. 광원의 방향이 바뀌어도 그림자의 위치와 형태가 변하지 않습니다.
시각적 사실성은 떨어지지만, 캐릭터가 바닥 위에 서 있다는 느낌을 전달하는 데는 충분합니다. 성능이 극도로 제한된 모바일 환경에서 가장 먼저 고려되는 그림자 기법입니다.
Projector / Decal
Projector(Built-in Render Pipeline) 또는 Decal(URP)은 텍스처를 특정 방향으로 표면에 투영하는 기법입니다.
예를 들어 캐릭터의 윤곽 형태를 그린 텍스처를 아래 방향으로 투영하면, 바닥에 캐릭터 형태의 그림자가 나타납니다. Blob Shadow의 원형 그림자보다 사실적입니다.
비용은 투영 범위가 닿는 표면의 픽셀 수에 비례합니다.
Blob Shadow는 작은 쿼드 하나의 픽셀만 처리하지만, Projector/Decal은 투영 범위 안의 바닥, 벽, 다른 오브젝트 등 모든 표면 픽셀을 처리합니다. 투영 범위가 넓을수록 처리할 픽셀이 많아집니다.
베이크 그림자
정적 오브젝트 — 움직이지 않는 건물, 지형, 소품 등 — 의 그림자는 런타임에 계산할 필요가 없습니다. Part 1에서 다룬 라이트맵 베이킹을 통해 그림자를 미리 텍스처에 기록해 둘 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
베이크 그림자
에디터(빌드 전):
광원의 위치와 방향에 따라 정적 오브젝트의 그림자를 계산
→ 결과를 라이트맵 텍스처에 기록
런타임:
라이트맵 텍스처를 표면에 입힘
→ 실시간 계산 없이 그림자가 표현됨
→ 비용: 추가 텍스처 샘플링 1회
제약:
→ 오브젝트가 움직이면 그림자가 따라가지 않음
→ 광원이 바뀌어도 그림자가 갱신되지 않음
베이킹은 오프라인에서 수행되므로 충분한 시간을 들여 고품질 그림자를 계산할 수 있고, 런타임에는 텍스처 샘플링만으로 그림자가 표현됩니다. 다만 그림자가 텍스처에 고정되어 있으므로, 오브젝트가 움직이거나 광원 방향이 바뀌어도 그림자가 갱신되지 않습니다.
하이브리드 접근
Blob Shadow, Projector/Decal, 베이크 그림자는 각각 장단점이 다릅니다. 실제 모바일 프로젝트에서는 이 기법들을 조합하는 하이브리드 방식이 가장 일반적입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
하이브리드 그림자 전략
══════════════════════════════════════════════════════════
배경(정적 오브젝트):
→ 베이크 그림자 (라이트맵)
→ 런타임 비용 없음
캐릭터(동적 오브젝트):
→ 실시간 Shadow Map (캐스케이드 1~2개, 낮은 거리)
→ 또는 Blob Shadow / Projector
기타 동적 소품:
→ Blob Shadow 또는 그림자 없음
──────────────────────────────────────────────────────────
배경의 그림자는 베이크로 품질을 확보하고,
캐릭터의 그림자만 실시간으로 처리하여
전체 비용을 억제하면서 시각적 완성도를 유지합니다.
══════════════════════════════════════════════════════════
배경의 건물, 지형, 나무 등 정적 오브젝트의 그림자는 라이트맵으로 베이크합니다. 동적 오브젝트만 실시간 Shadow Map을 적용하되, 캐스케이드 수와 Shadow Distance를 낮게 유지합니다. 성능이 특히 제한된 기기에서는 캐릭터에도 Blob Shadow만 적용하는 선택도 가능합니다.
Unity에서 이 하이브리드 방식을 구현하려면, 정적 오브젝트를 Static으로 표시하여 라이트맵 베이킹 대상으로 포함시키고, Mixed Lighting 모드를 사용하여 동적 오브젝트에만 실시간 그림자가 적용되도록 설정합니다. URP에서는 라이트의 Light Mode를 Mixed로 설정하면 이 동작이 활성화됩니다.
Shadow Map의 추가 비용
실시간 Shadow Map을 선택하면 네 가지 비용이 발생합니다.
첫째, 라이트 시점에서 장면을 렌더링하는 추가 패스가 캐스케이드 수만큼 발생합니다.
둘째, 각 패스에서 Shadow Map에 포함되는 오브젝트의 모든 정점에 대해 버텍스 셰이더가 실행됩니다.
셋째, Shadow Map 패스에서도 오브젝트별 드로우콜(Draw Call) — CPU가 GPU에게 “이 오브젝트를 그려라”라고 보내는 명령 — 이 발생하여 메인 카메라의 드로우콜에 추가됩니다.
넷째, Shadow Map 텍스처 자체가 GPU 메모리를 차지합니다.
Shadow Map은 광원 시점의 깊이를 기록하고, 메인 카메라는 카메라 시점의 색상을 기록하므로 서로 다른 렌더 타깃에 그려집니다. 메인 카메라의 프래그먼트 셰이더가 그림자 판별을 위해 Shadow Map 텍스처를 읽어야 하므로, Shadow Map은 메인 카메라 렌더링보다 먼저 완성되어 별도 텍스처로 존재해야 합니다.
GPU 아키텍처 (2)에서 다루었듯이, 모바일 GPU는 화면을 타일 단위로 나누어 칩 내부의 타일 메모리에서 렌더링합니다. 렌더 타깃이 바뀌면 현재 타일 메모리의 결과를 메인 메모리에 기록(Store)하고, 새 렌더 타깃의 내용을 메인 메모리에서 다시 읽어와야(Load) 합니다.
Shadow Map → 메인 카메라로 전환할 때 이 Store/Load가 발생하며, 모바일에서는 메인 메모리 대역폭이 제한적이므로 이 비용도 무시할 수 없습니다.
모든 오브젝트가 그림자를 생성할 필요는 없습니다. 작은 소품이나 먼 곳의 오브젝트는 그림자가 있어도 눈에 띄지 않습니다.
Unity의 MeshRenderer에서 Cast Shadows를 Off로 설정하면 해당 오브젝트가 Shadow Map 패스에서 제외되어, 그만큼 정점 처리와 드로우콜이 절약됩니다.
포스트 프로세싱(Post Processing)의 구조
렌더링 파이프라인은 3D 오브젝트를 셰이딩하여 2D 이미지로 변환하고, 그 결과를 GPU 메모리의 프레임버퍼(Framebuffer) — 화면에 출력될 최종 이미지를 픽셀별 색상 정보로 저장하는 메모리 영역 — 에 기록합니다.
포스트 프로세싱(Post Processing)은 이 프레임버퍼의 이미지가 완성된 후, 화면 전체에 추가 시각 효과를 적용하는 단계입니다.
포스트 프로세싱 시점에는 메쉬, 정점, 변환 행렬 같은 지오메트리 정보가 모두 2D 픽셀로 변환된 상태이므로, 순수하게 2D 이미지 처리만 수행합니다.
1
2
3
4
5
6
7
8
9
10
11
12
포스트 프로세싱의 위치
3D 장면 렌더링 포스트 프로세싱
───────────── ──────────────
메쉬 → 셰이딩 → 프레임버퍼 ──▶ Bloom → Color Grading
(2D 이미지) → Tone Mapping
→ Vignette
...
──▶ 최종 화면
3D 오브젝트 처리 2D 이미지 처리
포스트 프로세싱의 비용 구조는 3D 렌더링과 다릅니다. 3D 렌더링에서는 삼각형 수, 셰이더 복잡도, 오버드로우(Overdraw) — 같은 픽셀 위치에 여러 오브젝트가 겹쳐 그려지는 것 — 등이 비용을 결정합니다.
포스트 프로세싱에서는 화면의 전체 픽셀을 처리해야 하므로, 해상도에 비례하는 필레이트(Fill Rate) — GPU가 단위 시간에 처리할 수 있는 프래그먼트 수 — 비용이 핵심입니다.
1920×1080 해상도에서 포스트 프로세싱 효과 하나를 적용하면, 약 2백만 개의 프래그먼트에 대해 셰이더가 실행됩니다. 효과를 여러 개 중첩하면 그만큼 프래그먼트 셰이더 실행 횟수가 곱해집니다.
포스트 프로세싱 효과별 비용
포스트 프로세싱 효과는 모두 화면의 전체 픽셀을 처리하므로 필레이트 비용이 핵심이지만, 픽셀 하나를 처리하는 데 필요한 연산량은 효과마다 다릅니다.
텍스처를 한 번 참조하는 효과와 수십 번 참조하는 효과는 비용 차이가 큽니다.
저비용 효과
Color Grading(색 보정)은 LUT(Look-Up Table) — 입력 색상과 출력 색상의 대응 관계를 미리 기록해둔 텍스처 — 를 참조하여 화면의 색상을 변환합니다. 원래 RGB 값을 수식으로 계산하면 여러 단계의 연산이 필요하지만, LUT를 한 번 참조하면 결과를 바로 얻을 수 있습니다. 픽셀당 텍스처 샘플링 한 번으로 처리되므로 비용이 낮습니다.
Vignette(비네트)는 화면 가장자리를 어둡게 만듭니다. 화면 중앙으로부터의 거리에 기반한 간단한 수학 계산으로 어둡기를 결정합니다. 텍스처 샘플링 없이 수식만으로 완료되므로 비용이 낮습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
저비용 포스트 프로세싱
Color Grading:
입력 픽셀 색상 (R, G, B)
│
▼
LUT 텍스처에서 대응 색상 참조 (텍스처 샘플링 1회)
│
▼
출력 픽셀 색상 (R', G', B')
Vignette:
화면 중앙에서의 거리 d 계산
밝기 = 1.0 - (d / 최대거리)^강도
출력 = 원래 색상 × 밝기
→ 텍스처 샘플링 없음, 수학 연산만
중비용 효과
Bloom은 밝은 영역의 빛이 주변으로 번져 보이는 효과입니다. 화면에서 일정 밝기 이상의 픽셀만 추출(threshold)한 뒤, 추출된 이미지를 여러 단계로 다운샘플 — 해상도를 절반씩 줄이는 것 — 하면서 가우시안 블러를 적용합니다.
해상도가 낮아질수록 블러 연산이 더 적은 픽셀에 대해 수행되고, 같은 블러 반경이 원본 기준으로 더 넓은 영역에 적용됩니다.
블러된 이미지를 다시 업샘플 — 해상도를 두 배씩 키우는 것 — 하며 원래 이미지와 합성하면, 밝은 부분이 주변으로 부드럽게 퍼지는 결과가 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Bloom 처리 과정
원본 이미지 (1920×1080)
│
▼
밝은 픽셀 추출 (Threshold)
│
▼
다운샘플 단계 (해상도를 절반씩 줄이며 블러)
960×540 → 480×270 → 240×135 → 120×67
│ │ │ │
▼ ▼ ▼ ▼
업샘플 단계 (다시 확대하며 합성)
120×67 → 240×135 → 480×270 → 960×540
│
▼
원본과 합성 → 최종 이미지
텍스처 샘플링: 다운샘플 + 업샘플 단계마다 다수 발생
렌더 타깃 전환: 단계마다 해상도가 다른 텍스처 사용
→ 비용: 중간
Bloom의 각 다운샘플/업샘플 단계는 이전 단계의 텍스처를 읽고 새 해상도의 텍스처에 기록합니다. 단계마다 GPU가 기록하는 대상(렌더 타깃)이 바뀌므로, 렌더 타깃 전환이 여러 번 발생합니다.
모바일 GPU의 TBDR 아키텍처에서는 렌더 타깃이 바뀔 때마다 현재 타일의 결과를 메인 메모리에 기록(Store)하고, 새 렌더 타깃의 내용을 다시 읽어와야(Load) 합니다.
Bloom처럼 여러 해상도의 중간 텍스처를 거치는 효과는 이 Store/Load가 여러 번 발생하므로, 이 비용도 함께 고려해야 합니다. 다만 다운샘플된 저해상도에서 대부분의 블러가 수행되므로, 전체 해상도에서 블러를 수행하는 것보다는 효율적입니다.
Tone Mapping은 렌더링 결과의 밝기 범위를 모니터가 표현할 수 있는 범위로 압축하는 과정입니다.
렌더링 중에는 밝기 값에 상한이 없습니다. 태양은 10.0, 하늘은 3.0, 전등은 1.5처럼 실제 밝기 비율에 따라 자유롭게 표현됩니다. 이처럼 넓은 밝기 범위를 다루는 방식이 HDR(High Dynamic Range)입니다. 반면 모니터는 0(완전한 검정)~1(최대 밝기) 범위만 표시할 수 있으며, 이 좁은 범위를 LDR(Low Dynamic Range)이라 합니다.
Bloom처럼 1.0을 넘는 밝기를 기준으로 동작하는 효과는 HDR 렌더링이 전제이므로, HDR을 사용하는 한 Tone Mapping은 선택이 아니라 필수입니다. Tone Mapping 없이 HDR 값을 그대로 출력하면, 1.0을 넘는 태양(10.0)·하늘(3.0)·전등(1.5)이 모두 1.0으로 잘려 같은 흰색이 됩니다.
Tone Mapping은 이 넓은 범위를 0~1로 압축하되 밝기의 상대적 차이를 보존하여, 태양이 가장 밝고 하늘은 중간, 전등은 약간 밝은 식으로 구분을 유지합니다. ACES, Filmic 같은 수학적 함수가 이 압축 곡선을 정의하며, 함수마다 어두운 영역과 밝은 영역의 보존 비율이 다릅니다.
Tone Mapping 자체는 픽셀당 수학 연산만 수행하므로 단독으로는 저비용입니다.
URP에서는 렌더 타깃 전환을 줄이기 위해 Color Grading의 LUT 참조와 Tone Mapping의 수학 함수를 하나의 셰이더 패스에 합쳐서 실행합니다. 별도 패스로 나누는 것보다 구조적으로 효율적이지만, 픽셀 하나를 처리할 때 LUT 샘플링과 수학 함수가 함께 수행되므로 Tone Mapping 단독보다는 비용이 늘어 저~중비용에 해당합니다.
고비용 효과
SSAO(Screen Space Ambient Occlusion)는 오브젝트와 오브젝트가 맞닿는 부분 — 구석, 틈새, 접촉면 — 에 자연스러운 그림자를 추가하는 효과입니다. 깊이 버퍼를 읽어 각 픽셀 주변의 깊이 값을 여러 번 샘플링하고, 주변이 막혀 있는 정도(occlusion)를 계산합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SSAO의 비용 구조
각 픽셀에 대해:
(1) 깊이 버퍼에서 현재 픽셀의 깊이 읽기
(2) 주변 N개 방향으로 샘플 포인트 생성 (N = 8~32)
(3) 각 샘플 포인트의 깊이를 깊이 버퍼에서 읽기
(4) 각 샘플의 깊이를 비교하여 가려진 정도 계산
(5) 결과를 블러하여 노이즈 제거
픽셀당 텍스처 샘플링: N + 1회 (N = 샘플 수)
추가 블러 패스: 1~2회
1920×1080 해상도, 샘플 16개 기준:
총 텍스처 샘플링 = 2,073,600 × 17 ≒ 3,525만 회
+ 블러 패스의 추가 샘플링
→ 비용: 높음
SSAO는 픽셀당 텍스처 샘플링이 수십 회에 달하므로 필레이트 비용이 큽니다. 모바일에서는 사용하지 않으며, 시각적으로 유사한 효과가 필요하면 베이크 시점에 Ambient Occlusion을 라이트맵에 포함시키는 방식으로 대체합니다.
Motion Blur는 카메라나 오브젝트의 움직임에 따른 잔상 효과입니다. 현실의 카메라로 빠르게 움직이는 물체를 찍으면 셔터가 열려 있는 동안 물체가 이동하여 잔상이 생기지만, 게임 렌더링은 각 프레임이 한순간의 정지 화면이라 잔상이 없습니다. Motion Blur는 이 잔상을 인위적으로 만들어 자연스러운 움직임 느낌을 추가합니다.
먼저, 움직이는 모든 오브젝트에 대해 모션 벡터(motion vector)를 생성하는 추가 렌더링 패스가 필요합니다. 각 오브젝트의 현재 프레임 위치와 이전 프레임 위치를 비교하여 이동 방향과 거리를 계산하고, 별도 텍스처에 저장합니다. 화면의 각 픽셀에 “이 픽셀은 오른쪽으로 5px 이동했다”와 같은 화살표가 기록된다고 생각하면 됩니다.
포스트 프로세싱 단계에서는 각 픽셀의 모션 벡터(화살표)를 읽고, 그 방향을 따라 주변 픽셀을 여러 개 읽어 평균을 냅니다. 오른쪽으로 이동한 픽셀이라면 오른쪽으로 늘어선 픽셀들을 읽어 섞는 것이고, 그 결과가 이동 방향으로 늘어진 잔상입니다. 샘플 수가 많을수록 부드러운 잔상이 만들어지지만, 픽셀당 텍스처 읽기 횟수가 10~20회 이상으로 늘어나 필레이트 비용이 커집니다.
Depth of Field(피사계 심도)는 초점 영역 밖의 오브젝트를 흐리게 만드는 효과입니다. 카메라 렌즈로 인물을 찍을 때 배경이 흐려지는 것과 같습니다.
깊이 버퍼를 참조하여 각 픽셀이 초점에서 얼마나 떨어져 있는지를 판단하고, 거리 차이에 비례하여 블러 강도를 결정합니다. 예를 들어 초점 거리 5m를 기준으로, 2m~8m 범위는 선명하게 유지하고, 그 밖의 영역은 초점에서 멀수록 블러가 강해집니다. 이처럼 깊이마다 블러 크기가 다르므로, 화면을 여러 층으로 나누어 각각 다른 크기의 가우시안 블러를 적용한 뒤 합성합니다.
Bloom과 유사하게 화면을 1/2, 1/4 크기로 다운샘플하고, 각 레벨마다 블러를 수행한 뒤 다시 원래 크기로 업샘플하여 합칩니다. 여러 해상도에서 반복적인 블러 연산과 샘플링이 발생하므로 필레이트 비용이 높습니다.
비용 요약
1
2
3
4
5
6
7
8
9
10
11
12
13
포스트 프로세싱 효과별 비용 비교
비용 핵심 연산 모바일 권장
──────────────────────────────────────────────────────────
Color Grading 저 LUT 샘플링 1회 O
Vignette 저 수학 연산 O
Tone Mapping 저~중 수학 함수 적용 O
Bloom 중 다운/업샘플 + 블러 O (주의)
SSAO 고 픽셀당 N회 샘플링 X
Motion Blur 고 모션 벡터 + 블러 X
Depth of Field 고 깊이 기반 블러 X
→ 모바일 권장 조합: Bloom + Color Grading + Tone Mapping + Vignette
효과마다 비용이 다른 핵심 이유는 픽셀당 텍스처 샘플링 횟수입니다. 수학 연산이나 LUT 한 번 참조로 끝나는 효과는 저비용이고, 픽셀마다 주변 텍셀을 수십 번 읽는 효과는 필레이트를 크게 소모합니다. Bloom은 텍스처 읽기 횟수 자체는 많지만, 다운샘플로 해상도를 낮춘 뒤 블러를 수행하므로 중비용에 머뭅니다.
SSAO, Motion Blur, Depth of Field는 모바일 GPU의 필레이트 예산을 초과하기 쉬우므로 모바일에서는 사용을 피하며, 데스크톱에서도 품질 설정에 따라 샘플 수를 조절하여 비용을 관리합니다.
렌더 타깃 전환과 URP 포스트 프로세싱
개별 효과의 비용과 별개로, 효과를 여러 개 적용할 때 발생하는 구조적 비용도 있습니다.
포스트 프로세싱 효과를 여러 개 적용하면, 효과마다 렌더 타깃(Render Target) — GPU가 결과를 기록하는 대상 텍스처 — 이 전환됩니다. 한 효과의 출력이 다음 효과의 입력이 되므로, 중간 텍스처에 결과를 기록하고 다시 읽는 과정이 반복됩니다.
모바일 GPU의 TBDR 아키텍처(GPU 아키텍처 (2))에서 렌더 타깃 전환은 타일 메모리의 Store/Load를 수반합니다. 효과가 많아질수록 이 사이클이 반복되며, 대역폭 비용이 누적됩니다.
포스트 프로세싱의 구현 방식은 렌더 파이프라인에 따라 다릅니다.
Built-in Render Pipeline에서는 Post Processing Stack v2라는 별도 패키지를 사용하며, 렌더 파이프라인과 분리된 구조여서 효과마다 개별 패스가 실행되고 모바일에 특화된 최적화가 제한적입니다.
URP에서는 별도 패키지 없이 포스트 프로세싱이 Volume 시스템으로 파이프라인에 통합되어 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Built-in vs URP 포스트 프로세싱
Built-in:
┌─────────────────────────┐ ┌──────────────────────┐
│ Built-in Render │ │ Post Processing │
│ Pipeline │ ──▶ │ Stack v2 (별도) │
│ (렌더링 완료) │ │ (별도 패키지) │
└─────────────────────────┘ └──────────────────────┘
→ 효과별 개별 패스, 모바일 최적화 제한적
URP:
┌──────────────────────────────────────────────────────┐
│ URP Pipeline │
│ │
│ 렌더링 → [Uber Post Processing Pass] │
│ ├ Bloom │
│ ├ Color Grading + Tone Mapping │
│ ├ Vignette │
│ └ (여러 효과를 하나의 패스로 합침) │
│ │
└──────────────────────────────────────────────────────┘
→ 렌더 타깃 전환 최소화, 모바일 최적화 셰이더
URP는 Uber Post Processing으로 이 문제를 해결합니다.
Color Grading, Tone Mapping, Vignette 같은 저비용 효과들을 하나의 프래그먼트 셰이더에 합쳐, 순서대로 계산한 뒤 한 번에 출력합니다. 효과마다 렌더 타깃을 전환하지 않으므로 TBDR의 Store/Load 횟수가 줄고, 대역폭 비용이 절약됩니다.
셰이더에서 숫자를 계산할 때는 정밀도를 선택할 수 있습니다. float(32bit)는 소수점 이하 7자리까지 정밀하지만 연산이 느리고, half(16bit)는 3자리까지만 표현하지만 모바일 GPU에서 약 2배 빠르게 처리됩니다. 포스트 프로세싱에서 다루는 색상 값은 half 정밀도로도 시각적 차이가 없으므로, URP의 포스트 프로세싱 셰이더는 반정밀도(half precision) 연산을 적극 사용하여 모바일 GPU에서의 처리 속도를 높입니다.
포스트 프로세싱 적용 시 주의사항
Uber Post Processing으로 렌더 타깃 전환을 줄이더라도, 모바일에서 포스트 프로세싱 비용에 직접 영향을 미치는 요소가 세 가지 있습니다.
해상도 — 포스트 프로세싱은 화면의 모든 픽셀을 처리하므로, 렌더링 해상도에 비례하여 비용이 증가합니다. URP의 Render Scale로 내부 렌더링 해상도를 낮추면, 3D 렌더링과 포스트 프로세싱 비용이 동시에 줄어듭니다.
효과의 수 — Uber Post 최적화로 일부 효과는 하나의 패스로 합쳐지지만, Bloom처럼 별도의 다운/업샘플 패스가 필요한 효과는 추가 렌더 타깃 전환이 발생합니다.
HDR 렌더링 — Bloom과 Tone Mapping은 HDR 렌더링이 활성화되어야 정상 동작합니다. HDR은 일반 디스플레이 범위(0~1)를 벗어나는 밝은 값을 저장하고 처리하는 방식입니다. 이를 위해 GPU가 렌더링 결과를 기록하는 프레임버퍼(Framebuffer) — 화면 출력 전 최종 이미지를 담는 메모리 영역 — 의 픽셀 형식이 일반 RGB8(픽셀당 8bit × 3채널) 대신 RGB16F(픽셀당 16bit 부동소수점 × 3채널)로 바뀝니다. 픽셀당 비트 수가 두 배가 되므로 메모리 사용량과 대역폭 소비도 두 배로 늘어납니다. Bloom이나 Tone Mapping을 사용하지 않는다면 HDR이 불필요하고, 프레임버퍼 비용도 절반으로 줄어듭니다.
마무리
- Shadow Map은 광원 시점의 깊이 텍스처와 카메라 시점의 실제 거리를 비교하여 그림자를 판별합니다. 해상도가 높으면 그림자가 선명하지만 메모리와 렌더링 비용이 증가합니다.
- Cascade Shadow Maps는 카메라 거리에 따라 별도의 Shadow Map을 할당하여 가까운 곳에 텍셀을 집중시킵니다. 캐스케이드 수를 줄이고 Shadow Distance를 짧게 설정하면, 같은 해상도로 더 선명한 그림자를 얻습니다.
- 모바일에서는 Blob Shadow, Projector/Decal, 베이크 그림자를 조합하는 하이브리드 방식으로 비용을 억제합니다.
- 포스트 프로세싱은 해상도에 비례하는 필레이트 비용이 핵심입니다. 모바일에서는 Bloom + Color Grading 조합을 기본으로 사용하고, SSAO, Motion Blur, Depth of Field는 사용을 피합니다.
- URP의 Uber Post Processing은 여러 저비용 효과를 하나의 셰이더 패스로 합쳐 렌더 타깃 전환을 줄입니다.
그림자든 포스트 프로세싱이든, 모바일에서 비용을 관리하는 원칙은 같습니다. 필요한 곳에만 비용을 집중하고, 눈에 띄지 않는 곳은 과감히 생략하거나 더 가벼운 대안으로 대체합니다.
조명 계산, 그림자 비교, 포스트 프로세싱 효과 — 이 모든 연산은 셰이더 안에서 실행됩니다. 버텍스 셰이더가 정점을 변환하고, 프래그먼트 셰이더가 조명을 계산하고, 포스트 프로세싱 셰이더가 화면 효과를 적용합니다. 셰이더의 명령어 하나하나가 GPU의 연산 유닛에서 처리되며, 그 효율이 최종 프레임 레이트를 결정합니다.
셰이더 최적화 (1)에서는 정밀도 선택(half vs float), 분기(branching)의 비용, 텍스처 샘플링의 효율, 셰이더 복잡도가 모바일 GPU에 미치는 영향을 다룹니다.
관련 글
시리즈
- 조명과 그림자 (1) - 실시간 조명과 베이크
- 조명과 그림자 (2) - 그림자와 후처리 (현재 글)
전체 시리즈
- 게임 루프의 원리 (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) - 빌드와 품질 전략