참치김밥은 최고의 한식이다

[유니티] 그래픽 최적화 : Shadow Map 본문

Unity/최적화

[유니티] 그래픽 최적화 : Shadow Map

l__j__h 2024. 2. 20. 16:32

그림자는 사실적인 그래픽을 표현할 때 빠질 수 없는 요소이다. 하지만 그림자는 직접 렌더링할 수 없다. 광원과 오브젝트들의 상호작용으로 만들어지는 매우 비싼 연산

하지만, 그림자의 특성 상, 아주 구체적일 필요는 없다. 따라서 정확도를 좀 낮추더라도 최대한 성능을 끌어올리는 다양한 최적화 기법이 존재한다.

 

 

Shadow Map

먼저 그림자를 구현하는 방법을 알아보자.

오른쪽 그림에서, 점 P를 렌더링하기 위해 점 P가 그림자 영역에 있는지를 알아야 한다. 이를 위해, 먼저 T를 통해서 점 P를 빛 기준 좌표로 변환한다. 변환할 경우, 이제 점 P를 빛의 시점에서 생각할 것이기 때문에, 점 P의 z좌표(깊이값)는 그림에서 볼 수 있듯 0.9가 된다.

이번엔 빛에서 P의 방향을 봤을 때, 볼 수 있는 가장 가까운 점의 z좌표(깊이)를 알아보자. 오른쪽 그림에서 볼 수 있듯 이 좌표는 0.4가 된다. (⇒ 이 과정은 Indexing depth map 에 해당한다. 빛의 시점에서 볼 수 있는 가장 가까운 점들의 z좌표(깊이)를 알아보는 것이므로)

이렇게,

빛의 시점에서 P방향으로 본 깊이값 < 점 P의 실제 깊이값 이므로, 우린 점 P가 그림자 안에 있다고 결정할 수 있다.

따라서 쉐도우 맵핑은 두 단계(two passes)를 거친다.

첫 번째, Depth map을 렌더링하는 것. 두 번째, 씬을 평소대로 렌더링하며, 위에서 생성된 Depth map으로 해당 픽셀이 그림자 안에 있는지 연산하는 것.

Depth Map?

위의 쉐도우 맵핑의 첫 번째 단계에서 Depth map이 생성된다. depth map은 shadow testing을 위해 빛의 시점에서 본 z값(depth)을 texture로 만든 것이다.

우리는 씬을 렌더링한 결과를 텍스처로 저장해야 하기 때문에, Framebuffers가 필요하다.

그림자 최적화 기법

1. Shadow Distance

Shadow Distance란 그림자가 그려질 거리를 뜻한다.

즉, Shadow Distance가 작을수록, 카메라와 가까운 오브젝트에 대해서 선명한 그림자를 그리고, 이는 즉 해당 오브젝트가 차지하는 픽셀의 갯수가 증가함(해상도 증가)을 뜻한다.

Shadow Distance를 사용한 최적화 기법은 보통 다음과 같다.

먼저, Shadow Distance를 낮게 설정해 카메라와 가까운, 중요한 오브젝트의 그림자 품질을 높인다.

멀리 떨어져 있지만, 그림자가 중요한 큰 오브젝트의 경우 그림자를 미리 구워두어(baked lightmap) 사용한다.

단, baked는 움직이지 않는 static 오브젝트에만 사용 가능하다. 따라서 움직이는 dynamic 오브젝트의 경우, Cascaded Shadow Maps 방식을 사용한다.

2. Cascaded Shadow Maps

멀리 있는 오브젝트의 그림자를 표현하기 어려운 이유는, 카메라의 원근법 때문이다.

첫 번째 그림에서 볼 수 있듯, 가까운 거리를 기준으로 오브젝트의 그림자를 렌더링하려면 적은 수의 픽셀(=4)로 Depth map을 구성하여 먼 거리 오브젝트의 그림자를 표현해야 하는 문제가 생긴다. (정확성 Down)

그렇다고 먼 거리를 기준으로 오브젝트의 그림자를 렌더링하려면, 너무 많은 수의 픽셀(=20)로 Depth map을 구성해야 하므로, 버퍼의 사이즈가 그만큼 커지는 메모리 이슈가 발생한다.

Cascaded Shadow Map은 이러한 문제를 해결할 수 있도록, 뷰 프러스텀(View Frustum) 영역을 몇 단계로 나누어 그림자를 구현하는 방식이다.

이렇게 하면 멀리 떨어진 오브젝트, 가까운 오브젝트 모두 적절한 해상도로 퀄리티를 유지하며 그림자를 구현할 수 있다.

하지만, 단계마다 Shadow Map을 따로 구현해야 하므로, N단계라면 *N만큼의 드로우콜 횟수와 메모리 양이 증가하게 된다. (첫 번째 그림 맨 오른쪽에선 2단계라고 할 수 있음)

 

더보기

참고로, 원신에서는 800미터가 떨어진 그림자까지도 렌더링을 하는데, 이때 Cascaded Shadow를 8개까지 사용한다고 한다. 8개나 사용하면, 한 픽셀 당 8바퀴를 도는 건가? 생각할 수 있지만, Cascaded Shadow 8개를 어떻게 업데이트했냐면,

4개의 cascaded는 매 프레임 업데이트한다.

나머지 4개의 cascaded는 8프레임마다 interleaving하게 업데이트한다.

→ 4개 + 1개(4개 중 1개씩 interleaving(교차, 상호 끼워맞춤 뜻)하게 업데이트)

→ 매 프레임마다 5개의 cascades 업데이트

→ 모든 cascades는 8프레임마다 업데이트

원신에서는 프아송 노이즈로 shadow banding 이라는 문제를 해결했다고 하는데, URP에선 Render feature로 Screen Space Shadow를 건드려서 구현할 수 있다고 한다.

소프트 쉐도우를 사용하면 속도가 너무 느려져서, 하드 쉐도우로 구현한 뒤, 쉐도우 마스크로 쉐도우의 경계면을 검출하고, 그 경계면에 프아송 노이즈를 주어 부드러운 그림자를 효율적으로 구현할 수 있었다고 한다.

 

3. Planar Shadow

그림자를 구현하는 방법 중 아주 단순한 방법이다. 엄밀히 말하면 Planar shadow는 그림자가 아니라, Mesh 이다.

오브젝트의 그림자를 미리 렌더링해두고, 오브젝트와 함께 움직이는 방법이다.

추가적인 버퍼나 드로우콜이 필요하지 않아 매우 효율적이지만, 그림자가 보일 바닥이 평면이 아니라면 이질감이 느껴진다.

 

참고 : 

https://inhopp.github.io/unity/Unity5/

 

[그래픽스 최적화] Shadow Map

Main Reference: - 유니티 그래픽스 최적화

inhopp.github.io

 

728x90