역자의 말: 꽤 오랜 시간동안 유니티를 사용해서 프로젝트를 진행 하면서 가장 불만족 스러운 부분은 그림자 처리 방식입니다. 물론 언리얼 엔진도 나나이트를 사용할 때 VSM 이 성능을 보장하기 때문에 아직까지는 CSM 의존적이라고 할 수 있지만요. 구정 연휴가 끝나면 고객사에 제공할 그림자 시스템 개발에 들어갈 텐데요. 2019년 가로 세로 32키로 크기의 심리스 월드 개발을 해 본 적이 있기 때문에 그때 사용했던 방식을 거의 그대로 답습할것 같네요. 아래 글에서도 나오는 케시 기반 셰도우와 공간 클러스터링 기법을 사용한 프록시 셰도우 메시등을 사용하는 것은 주로 근접 그림자 품질보다는 원경 그림자 보장과 전반적인 버택스 버퍼를 아끼는 기법이며 기본적으로는 셰도우 케시팅을 위해 호출해야하는 추가적인 콜을 줄이는데 있습니다. 그 외에 또 재미있는 글들을 쯔흐 아이디 " 傻头傻脑亚古兽" 분이 올려주셔서 같이 한번 읽어보고자 업데이트 했습니다.
저자: 傻头傻脑亚古兽
서문
ShadowMap은 게임에서 널리 쓰이는 기술로, 오랜 역사와 다양한 변형이 존재합니다. Unity의 프로그래머블 렌더 파이프라인 인터페이스가 점점 더 개방되면서, 예전에는 구현하기 어려웠던 그림자 기법들이 이제는 시도해볼 수 있게 되었습니다. 본 글에서는 신버전 URP의 그림자 시스템을 기반으로 개조하여, 시각적 품질과 성능을 모두 개선하는 방법을 다루겠습니다. (본 글은 URP 버전은 17.0.1이며, 일부 방법은 구버전에도 적용 가능합니다.)
레벨 간 접합(Cascade 경계)
Cascade 레벨 간에 정밀도가 다르기 때문에 이음매 문제가 발생합니다. 그림자 거리를 멀리 잡아야 할 경우, 각 레벨의 정밀도 차이를 줄이기가 어렵죠. 정밀도 차이가 클수록, 이음매는 더 눈에 띕니다.
한 가지 해결 방법은 두 레벨 사이를 블렌딩하는 것인데, 블렌딩의 비용이 상당히 큽니다.
또 다른 저렴한 방법은 노이즈를 활용하는 것입니다. 두 레벨 사이에서 랜덤 샘플링을 하고, TAA와 함께 사용하면 랜덤으로 생긴 노이즈까지 제거할 수 있습니다.
페이드 없음
디더 페이드
디더 페이드 + TAA

DitherFade 켜기/끄기 비교
이 코드는 HDRP의 HDShadowAlgorithms.hlsl에 구현되어 있습니다.
HDShadowAlgorithms.hlsl
HDShadowAlgorithms.hlsl
구체(Sphere) 최적화
관찰해 보면, Unity의 Cascade Sphere 계산 알고리즘이 좀 특이한데요. 와이드스크린(예: 16:9)에서는 구체가 니어 플레인을 꽉 감싸지 않습니다. 이런 경우 앞쪽 한두 레벨의 Cascade Sphere를 니어 플레인 쪽으로 밀어주면, 정밀도가 높은 부분이 최대한 앞쪽을 커버하도록 만들 수 있습니다.
(주의: 세로 해상도에서는 이 문제가 존재하지 않습니다.)
수정 전
최적화 후
오른쪽이 최적화 후 결과

동적 비교
신버전 URP에서 수정 방법도 간단합니다. ShadowCulling.cs의 ComputeShadowCasterCullingInfos 함수에 구체 위치를 수정하는 로직을 넣어주면 됩니다. 알고리즘도 단순한데, 카메라 위치, FOV, 구체 위치 정보 모두 가져올 수 있으니 자세한 설명은 생략하겠습니다.
또한 구체 좌표를 조정했으니, 그림자의 ViewMatrix도 조정이 필요합니다. 아래 참고 코드는 실제 Unity 내장 계산 방식과 약간 다릅니다:
Sphere 기반 ViewMatrix 계산
코드에서 빨간 박스 부분은 그림자 안정성을 높이고, 카메라 변화 시 발생하는 그림자 유영(shadow swimming) 문제를 완화하기 위한 것입니다.
소프트 셰도우
PCF
PCF는 일반적인 게임의 소프트 셰도우 기법으로, 고정된 부드러움 크기를 사용합니다.
보통 저사양 기기에서는 4개의 샘플 포인트와 SampleCmpLevelZero 혹은 Gather를 결합해서 소프트 셰도우 계산과 선형 보간을 수행합니다.
Unity의 최저 품질 소프트 셰도우는 3×3 픽셀까지만 커버합니다.
Unity 내장에서는 TentFilter도 제공하는데, 삼각형 면적으로 가중치를 계산하고, 거꾸로 샘플 포인트 위치를 추론하는 샘플링 방식입니다.
하지만 실제로 4탭으로도 4×4 픽셀을 커버할 수 있습니다. 여기서는 UE의 ManualPCF 알고리즘을 참고할 수 있는데, 전체적으로 소프트 셰도우 범위가 Unity보다 넓습니다.
물론 중고급 품질 소프트 셰도우 범위 역시 UE ManualPCF > Unity TentFilter 계열 알고리즘입니다.
디스크 랜덤 샘플링
동일한 샘플 수 조건에서, 랜덤 디스크 셰도우는 때로 더 넓은 소프트 셰도우 범위에 적용할 수 있습니다. 단점은 소프트 셰도우 범위가 크고 샘플 수가 부족할 때 노이즈가 눈에 띈다는 것입니다. TAA를 랜덤 디스크와 연계하면 노이즈 문제를 해소할 수 있습니다.
4탭 PCF
4탭 디스크 샘플링
4탭 디스크 샘플링 + TAA
디스크 소프트 셰도우의 또 다른 장점은 부드러움 정도를 동적으로 조절할 수 있다는 것, 즉 디스크의 반지름을 변경할 수 있다는 점입니다.

조절 가능한 부드러움
알고리즘은 HDRP의 HDPCSS.hlsl을 참고할 수 있습니다.
물론 이런 디스크 샘플링이 만들어내는 노이즈를 모든 사람이 받아들일 수 있는 것은 아닙니다.
PCSS
PCSS는 동적 필터 커널 크기를 사용하는 방식으로, 접촉 부분의 그림자는 딱딱하고, 투영 물체와의 거리가 멀수록 부드러워져서 사실적인 소프트 셰도우를 시뮬레이션합니다.
인터넷이나 HDRP에 완전한 코드가 있으므로, 여기서는 자세히 다루지 않겠습니다.
DCPF
PCF를 확장하는 방법 중 하나가 DCPF인데, 사실 커브 하나로 그림자의 딱딱함/부드러움 정도를 조절하고, 차폐물 거리를 이용해 커브 결과와 원본 결과를 보간하여 PCSS에 근사한 효과를 얻는 것입니다. 성능은 PCSS보다 좀 나은 편이죠.
UE ManualPCF + DCPF 결합
여기서는 IQ 대신님(Inigo Quilez)의 Inigo Quilez InvSmoothstep 함수를 쓸 수 있는데, SmoothStep(그림자를 더 딱딱하게)과 InvSmoothstep(그림자를 더 부드럽게) 모두 가능합니다.

DCPF
SMRT
UE5의 Virtual ShadowMap 소프트 셰도우 기법으로, 셰도우 맵에서 RayMarching을 수행하여 이론적으로 더 사실적인 소프트 셰도우를 구현합니다.
(상세 내용은 추후 보충 예정)
SMRT
SMRT + TSR
최적화
소프트 셰도우 마스크:
유명한 원신의 1/16 해상도 스크린 스페이스 소프트 셰도우 + 블러 마스크 방식입니다. 이후 연애와 프로듀서(恋与深空) 등의 게임에서도 이 방식을 이어 사용했습니다. 약간 꼼수스럽고 엄밀하지는 않은 방식이죠.
이 외에 GPU Pro 360 Guide To Shadows에서는 Hybrid Min/Max Plane-Based Shadow Maps 기술을 언급하고 있습니다. 셰도우 맵을 다운샘플링하여 Min, Max 또는 평면을 저장하는 작은 마스크를 구축하고, 그림자 안인지 밖인지, 소프트 셰도우인지를 빠르게 판별하는 것입니다. 이 방법이 더 엄밀하긴 하지만, 스크린 스페이스 방식이 더 편리한 것 같습니다.
그림자 캐싱
CSM의 단점 중 하나는 대량의 ShadowCaster를 여러 번 그려야 해서 무시무시한 성능 비용이 발생한다는 것입니다. 레벨이 많을수록, 그림자 거리가 멀수록 더 많은 드로우가 필요합니다.
전통적 그림자 캐싱
보통 한 가지 방식은 캐싱인데, 정적 투영 오브젝트를 캐시 ShadowMap에 그려두고, 정식 ShadowMap을 그리기 전에 캐시된 ShadowMap을 복사해 온 다음, 동적 투영 오브젝트만 추가로 그립니다. 이렇게 하면 소범위 이동 시 대량의 Caster 드로우를 절약할 수 있고, 카메라나 조명이 일정 각도 이상 회전했을 때만 캐시를 갱신하면 됩니다. 신버전 URP에서도 이 기능을 빠르게 구현할 수 있는 인터페이스를 제공합니다.
그림자 캐싱은 프로젝트 상황에 맞춰 다양한 메커니즘을 설계할 수 있으며, 각 레벨별 동적/정적 갱신 전략을 다르게 설정할 수 있어 매우 유연합니다.
유연하게 캐싱 전략 수립

캐시 셰도우 효과
동적 그림자 분리
위에서 말한 것은 흔히 쓰이는 그림자 캐싱 수단일 뿐입니다. Bo Li가 Siggraph 2019에서 발표한 A Scalable Real-Time Many-Shadowed-Light Rendering System(링크는 참고 섹션에)에서는 또 다른 방식을 제안했는데, 다음과 같은 문제를 지적합니다:
- 매 프레임 정적 캐시를 셰도우 맵에 복사해야 하며, 이는 고정적인 GPU 고비용 오버헤드
- 캐싱을 하지 않으면 정적 그림자가 매 프레임 반복 그려져 높은 CPU 비용
- 정적 그림자가 캐시되어 있고 복사를 하지 않는다면, 수신 오브젝트가 정적 그림자와 동적 그림자 두 장을 샘플링해야 하므로 GPU 필터링 비용이 높음
그래서 Mask를 사용한 최적화를 생각해냈습니다. Mask가 0이면 정적 그림자의 필터링만 고려하고, Mask가 1이면 동적 그림자와 정적 그림자 두 장을 모두 필터링합니다.
이 방식에서는 동적 셰도우 맵의 정밀도와 정적 셰도우 맵의 정밀도를 각각 독립적으로 설정할 수 있어, 캐릭터에 더 좋은 그림자 품질을 줄 수 있습니다.
물론 이 Mask는 보수적으로 생성해야, 동적 그림자를 정확히 감쌀 수 있습니다.
CSM-Scrolling
전통적 캐시 셰도우가 정지 상태에서 드로우 유닛을 절약할 수 있긴 하지만, 캐시를 갱신할 때는 여전히 비용이 발생합니다. 현재 그림자 개방 인터페이스로 보면 간소화된 CSM-Scrolling 스크롤링 셰도우를 구현할 수 있을 것 같습니다. Shadow Cull Plane을 수정하여 그림자의 컬링 결과를 조정하고, 정적 그림자는 아래 그림의 빨간 박스 부분만 갱신하며, 기존 캐시 결과는 초록 박스 위치로 이동 복사합니다.
CSM-Scrolling
Sparse Shadow Tree
또 다른 방법은 오프라인 베이킹 형태로 정적 그림자를 캐시하는 것입니다. 하지만 씬이 크면 셰도우 맵도 커지므로, 압축 방식으로 처리해야 합니다. (링크는 참고 섹션 참조)
주야 전환
그라데이션 전환
많은 게임이 주야 전환 조명 효과를 구현하는데, 조명 방향이 이동하면 그림자 캐시가 무효화됩니다. 한 가지 방식은 조명 각도 변화가 일정 임계값을 넘을 때, 새 조명 방향의 ShadowMap과 기존 조명 방향의 ShadowMap을 페이드 아웃/인하는 것입니다. 다만 이런 방식은 엄밀한 느낌이 좀 부족합니다.

그림자 캐시의 주야 변화
시차(Parallax) 전환
시차를 이용한 전환 방법도 있습니다: GPU Zen 2 - Parallax-Corrected Cached Shadow Maps. 기존 조명 방향에서 새 방향의 그림자를 "추측"하는 것이므로, 오류가 발생할 수 있습니다.


GPU Zen 2 - Parallax-Corrected Cached Shadow Maps
그림자 프록시 모델(Shadow Proxy)
ShadowMap 렌더링 과정에서 투영 오브젝트의 폴리곤 수를 줄이기 위해, 게임에서는 보통 저폴리 모델을 그림자 전용 모델로 사용합니다.

Unity에서는 원본 모델의 Cast Shadows를 Off로 바꾸고, 그림자 프록시를 Shadows Only로 설정하면 됩니다. 참고로 그림자 프록시는 잘못된 셀프 셰도우를 유발할 수 있는데, 이 부분은 뒤에서 좀 더 자세히 다루겠습니다.

Unity 그림자 프록시 설정
셀프 셰도우 오류
셀프 셰도우 오류 문제는 게임에서 자주 만나는 문제인데, 잘 처리하려면 단순하면서도 복잡합니다. 보통 그림자 바이어스로 처리하며, 이 분야의 대가가 쓴 아래 글에서 셰도우 바이어스 알고리즘을 매우 명확하게 설명하고 있습니다.
https://zhuanlan.zhihu.com/p/370951892
여기서 정리하면:
셰도우 바이어스로 셀프 셰도우 오류 문제를 해결할 수 있지만, 바이어스 양은 여러 조건에 영향을 받습니다. 바이어스를 맹목적으로 크게 잡으면 안 되는데, 바이어스가 커질수록 그림자 디테일이 더 많이 사라지고, 빛샘(Light Leak) 문제가 발생하며, 노멀 바이어스는 그림자 형태를 가늘게 만들기도 합니다.

정밀도가 다음과 같다고 가정하면:
- TexelSize가 클수록 더 큰 바이어스가 필요합니다. CSM의 각 레벨은 범위가 점점 커지므로, 바이어스도 레벨마다 증가합니다.
- 소프트 셰도우 필터 반경이 클수록 더 큰 바이어스가 필요합니다.
- 조명이 노멀에 대해 기울어질수록 더 큰 바이어스가 필요합니다.
- 깊이 정밀도(Unity 셰도우 깊이는 기본적으로 16비트 반정밀도를 사용하며, 디렉셔널 라이트에는 보통 충분합니다.)
URP에서는 조명 또는 파이프라인 설정에서 기본 바이어스 값을 지정하고, GetShadowBias 함수가 이 기본값에 1번과 2번 요소를 곱하여 최종 바이어스를 산출합니다. 3번은 버텍스 셰이더에서 처리되며, 추가로 슬로프 바이어스도 적용됩니다.

TexelSize 적응형 바이어스
아래 그림은 URP의 노멀 바이어스로, 조명 각도에 따라 스케일링됩니다.

상수 바이어스와 노멀 바이어스

슬로프 바이어스
또한 주의할 점은 URP가 Vertex Based Bias를 사용한다는 것, 즉 버텍스 셰이더에서 셰도우 바이어스를 계산합니다. 이렇게 하면 성능은 향상되지만, 샘플링 시 바이어스를 계산하는 것에 비해 효과가 약간 떨어집니다.

왼쪽: 버텍스 바이어스, 오른쪽: 샘플링 바이어스
수정 코드는 다음과 같습니다:

샘플링 시 바이어스로 버텍스 바이어스 대체
그리고 Unity가 사용하는 Shadow Pancaking 기술이 유발하는 셰도우 오류 문제가 있습니다. Unity는 ShadowMap의 Z 정밀도를 최적화하기 위해, 그림자 투영 행렬의 니어 플레인(원래는 투영 오브젝트를 감싸야 함)을 Cascade 바운딩 구체의 접선까지 당기는데(아래 그림 초록선), 초록선 밖으로 나간 투영 오브젝트를 주황색 화살표 위치로 Clamp합니다. 이렇게 하면 확실히 ShadowMap의 깊이 정밀도가 올라가고 셰도우 아크네 문제가 줄어들지만, 극단적인 경우에 셰도우 이상이 생길 수 있습니다. 해결 방법은 Shadow Near Plane Offset을 조정하여, 초록선을 조명 반대 방향으로 오프셋하는 것입니다.


Shadow Pancaking이 최적화된 니어 플레인으로 클램핑
앞서 저폴리 모델을 프록시 셰도우로 사용한다고 했는데, 이때 지오메트리의 요철 차이로 인해 셀프 셰도우 오류가 발생할 수 있습니다. 이런 모델 차이에서 오는 오류는 꽤 큰 바이어스가 필요한데, 개별 모델 문제 때문에 전역 바이어스를 키우면 씬 전체가 빛샘 투성이가 됩니다. 통일된 전역 바이어스 값으로는 해결할 수 없습니다.
- 여기서는 머티리얼에 PerObject 셰도우 바이어스를 추가하여 모델마다 조정하는 것을 권장합니다(투영 오브젝트의 버텍스 바이어스에만 적용).
- 또 다른 방법은 프록시 모델의 스케일을 수동 조정하는 것인데, 효과가 좋지 않고 리소스 제작 공수가 늘어납니다.

저폴리 셰도우 모델로 인한 잘못된 그림자

PerObject 바이어스 조정 후 오류 수정

Vertex Shadow Bias에 PerObjectBias 추가
캐릭터 그림자 처리
캐릭터에 고해상도 그림자 정밀도를 보장하고, 캐릭터가 CSM에서 여러 번 그려지는 것을 피하기 위해, 보통 캐릭터는 별도의 ShadowMap으로 구성합니다. 그런 다음 박스 데칼 방식으로 스크린 스페이스 셰도우 마스크 단계에서 다시 합칩니다.

왼쪽: CSM 씬 / 오른쪽: 캐릭터
스크린 스페이스에서 캐릭터 그림자를 통합하는데, 여기서 캐릭터와 씬은 별도 채널로 저장할 수 있습니다. 일부 2D 스타일 게임에서는 캐릭터가 깔끔해 보이도록 셀프 셰도우는 샘플링하지 않고, 씬 그림자만 샘플링하기 때문입니다.

참고:
【GDC2024】《호그와트 레거시》의 오픈 월드 렌더링 기술_bilibili
Shadows of Cold War: A Scalable Approach to Shadowing
A Scalable Real-Time Many-Shadowed-Light Rendering System
Sparse-Shadow-Trees
'TECH.ART.FLOW.IO' 카테고리의 다른 글
| [번역] Unity-URP-원신 스타일 프로시저럴 스카이박스(사례 정리) (2) | 2026.02.10 |
|---|---|
| [번역] REAC2025《오버워치 2》의 GI 솔루션 (0) | 2026.02.09 |
| [번역] 어떤 신입의 바이트덴스 게임 엔진 파트 인터뷰 경험담 (4) | 2026.02.04 |
| [번역] 깊이 오프셋을 활용한 나무 그림자 성능 최적화 (면 50% 감소) (0) | 2026.02.03 |
| [INDEX] SIGGRAPH 2025 ADAVANCED REALTIME RENDERING (5) | 2026.01.08 |