이제는 사장되어버린 퇴물 기법


원문


Chapter 14. Perspective Shadow Maps: 관리와 적용

Simon Kozlov
SoftLab-NSK

그림자 생성은 실시간 3D 그래픽에서 항상 큰 문제였습니다. 한 점이 그림자 안에 있는지 결정하는 것은 현대 GPU에게 간단한 작업이 아니며, 특히 GPU가 레이 트레이싱 대신 폴리곤 래스터화 방식으로 작동하기 때문입니다.

오늘날의 그림자는 완전히 동적이어야 합니다. 장면의 거의 모든 객체가 그림자를 드리우고 받아야 하며, 자기 그림자(self-shadowing)가 있어야 하고, 모든 객체가 부드러운 그림자를 가져야 합니다. 이러한 요구사항을 충족시킬 수 있는 알고리즘은 두 가지뿐입니다: 그림자 볼륨(또는 스텐실 그림자)과 그림자 매핑입니다.

그림자 볼륨과 그림자 매핑의 알고리즘 차이는 객체 공간 대 이미지 공간으로 귀결됩니다:

그림자 볼륨은 그림자를 드리우는 객체를 찾아서 그림자 영역을 만들고 해당 객체에 대한 렌더링을 차단하는 스텐실 버퍼를 생성합니다. 하지만 그림자 볼륨에는 단점이 있습니다. 스텐실 버퍼가 빠르더라도 많은 채우기 속도(fill rate)를 소비합니다. 그림자를 드리우는 객체가 많을수록 더 많은 채우기 속도가 필요합니다. 복잡한 장면에서는 프레임 속도가 급격히 떨어집니다.

그림자 매핑은 조명의 관점에서 깊이 맵을 렌더링한 다음, 이 깊이 맵(그림자 맵이라고 함)을 렌더링할 각 픽셀에 투영하고 깊이 값을 비교합니다. 만약 픽셀이 조명으로부터 더 멀리 떨어져 있다면, 이는 그 픽셀이 그림자 안에 있다는 것을 의미합니다. 그림자 볼륨과는 달리 그림자 매핑은 채우기 속도가 아닌 텍스처 메모리를 소비합니다. 안타깝게도 그림자 매핑은 텍스처 해상도가 제한되어 있기 때문에 엘리어싱(aliasing) 문제가 있습니다.

Stamminger와 Drettakis(2002)가 SIGGRAPH 2002에서 발표한 원근 그림자 맵(Perspective Shadow Maps, PSM)은 모든 가까운 객체가 먼 객체보다 커지는 후-투영 공간(post-projective space)에서 그림자 맵을 사용하여 그림자 맵의 엘리어싱을 제거하려고 시도합니다. 안타깝게도 특정 경우에만 잘 작동하기 때문에 원래 알고리즘을 사용하기 어렵습니다.

제시된 PSM 알고리즘의 가장 중요한 문제는 다음 세 가지입니다:

  • 가상 카메라 문제: 카메라 뒤에 있는 객체가 그림자를 드리울 때
  • 빛 각도 문제: 빛이 카메라를 향할 때 그림자 품질이 급격히 저하됨
  • 바이어싱 문제: 표준 그림자 맵의 바이어싱 방법을 적용할 수 없음

이러한 각 문제는 다음 섹션에서 논의됩니다. 이 장은 방향성 조명(더 큰 엘리어싱 문제가 있기 때문)에 초점을 맞추지만, 모든 아이디어와 알고리즘은 다른 유형의 광원에도 쉽게 적용할 수 있습니다(적절한 경우 세부 사항이 제공됨). 또한, 그림자 맵을 필터링하고 블러링하여 그림의 품질을 높이는 트릭에 대해 논의합니다.

일반적으로, 이 장은 PSM 사용의 효과를 높일 수 있는 기술과 방법을 설명합니다. 그러나 이러한 아이디어의 대부분은 여전히 여러분의 특정 요구사항에 맞게 조정되어야 합니다.

14.1 가상 카메라 문제

먼저, 이 문제의 본질을 살펴보겠습니다. 일반적인 투영 변환은 카메라 뒤에 있는 객체를 후-투영 공간에서 무한대 평면의 반대편으로 이동시킵니다. 그러나 광원도 카메라 뒤에 있다면, 이러한 객체는 잠재적인 그림자 캐스터이며 그림자 맵에 그려져야 합니다.

그림 14-1의 원근 변환에서 광선 상의 점들의 순서가 변경됩니다. 원래 PSM 논문의 저자들은 그림 14-2에 표시된 것처럼 잠재적인 그림자 캐스터를 시야 프러스텀 안에 유지하기 위해 뷰 카메라를 “가상으로” 뒤로 밀어내는 것을 제안합니다. 이렇게 하면 PSM을 일반적인 방식으로 사용할 수 있습니다.

[그림 14-1] 후-투영 공간에서 카메라 뒤의 객체

[그림 14-2] 가상 카메라 사용

그러나 실제로는 가상 카메라를 사용하면 그림자 품질이 저하됩니다. “가상” 이동은 효과적인 그림자 맵의 해상도를 크게 감소시켜 실제 카메라 근처의 객체가 작아지고 그림자 맵에 사용되지 않는 공간이 많이 생깁니다. 또한, 카메라 뒤의 큰 그림자 드리우는 객체에 대해 카메라를 크게 뒤로 이동해야 할 수도 있습니다. 그림 14-3은 작은 이동만으로도 품질이 얼마나 극적으로 변하는지 보여줍니다.

[그림 14-3] 가상 이동이 그림자 품질에 미치는 영향

또 다른 문제는 이미지 품질을 최대화하는 실제 “슬라이드백 거리(slideback distance)“를 최소화하는 것입니다. 이를 위해서는 장면을 분석하고, 잠재적인 그림자 캐스터를 찾는 등의 작업이 필요합니다. 물론, 바운딩 볼륨, 장면 계층 구조 등의 기술을 사용할 수 있지만, 이는 CPU에 상당한 부담을 줍니다. 게다가, 객체가 잠재적인 그림자 캐스터가 아니게 될 때 그림자 품질이 급격하게 변합니다. 이 경우 슬라이드백 거리가 즉시 변경되어 그림자 품질도 갑자기 변합니다.

14.1.1 해결책: 역 투영 행렬

우리는 이 가상 카메라 문제에 대한 해결책을 제안합니다: 라이트 행렬에 특수 투영 변환을 사용하는 것입니다. 사실, 후-투영 공간은 일반적인 월드 공간에서는 할 수 없는 몇 가지 투영 트릭을 허용합니다. 알고 보니 “무한대보다 더 멀리” 볼 수 있는 특수 투영 행렬을 만들 수 있습니다.

방향성 “역” 광원과 뷰 카메라 뒤에 객체가 있는 원래(비가상) 카메라에 의해 형성된 후-투영 공간을 살펴보겠습니다. 그림 14-4에 표시된 것처럼.

[그림 14-4] 역 광원이 있는 후-투영 공간

이 솔루션의 단점은 광선이 광원에서 나와야 하지만(실제로는 그렇지 않음) 점 1을 잡고, 마이너스 무한대로 가고, 플러스 무한대로 넘어가서 광원으로 돌아와 점 2, 3, 4에서 정보를 캡처해야 한다는 것입니다. 다행히도, 이 “불가능한” 광선과 일치하는 투영 행렬이 있습니다. 여기서 근평면을 음수 값으로 설정하고 원평면을 양수 값으로 설정할 수 있습니다. 그림 14-5를 참조하십시오.

[그림 14-5] 역 투영 행렬

가장 간단한 경우,

여기서 a는 전체 단위 큐브를 맞추기에 충분히 작습니다. 그런 다음 일반적인 투영 행렬과 같이 이 역 투영을 구축합니다. 여기서 행렬은 행-주요(row-major) 스타일로 작성됩니다:

P = [1  0   0      0   ]
    [0  1   0      0   ]
    [0  0   0      1   ]
    [0  0   1/a   -1   ]

따라서 그림자 맵에 들어가는 변환된 z 좌표에 대한 공식은 다음과 같습니다:

Z_psm(-a) = 0이고, z 값을 마이너스 무한대로 계속 감소시키면 Z_psm은 ½로 경향합니다. 동일한 Z_psm = ½는 플러스 무한대에 해당하며, 플러스 무한대에서 원평면으로 이동하면 원평면에서 Z_psm이 1로 증가합니다. 이것이 광선이 올바른 순서로 모든 점을 치고 후-투영 공간을 생성하기 위해 “가상 슬라이드”를 사용할 필요가 없는 이유입니다.

이 트릭은 후-투영 공간에서만 작동합니다. 왜냐하면 일반적으로 무한대 평면 뒤의 모든 점은 w < 0을 가지므로 래스터화할 수 없기 때문입니다. 그러나 라이트 카메라에 의한 또 다른 투영 변환의 경우, 이러한 점들은 카메라 뒤에 위치하므로 w 좌표가 다시 반전되어 양수가 됩니다.

이 역 투영 행렬을 사용하면 가상 카메라를 사용할 필요가 없습니다. 결과적으로, CPU 장면 분석 및 관련 아티팩트 없이 훨씬 더 나은 그림자 품질을 얻습니다.

역 투영 행렬의 유일한 단점은 큰 z 값 범위를 사용하기 때문에 더 나은 그림자 맵 깊이 값 정밀도가 필요하다는 것입니다. 그러나 24비트 고정 소수점 깊이 값은 합리적인 경우에 충분합니다.

14.1.2 근평면 개선

가상 카메라는 여전히 유용할 수 있습니다. 왜냐하면 그림자 품질은 카메라 근평면의 위치에 따라 달라지기 때문입니다. 후-투영 z에 대한 공식은 다음과 같습니다:

Z_proj = Q × z + C, 여기서 Q = Z_f / (Z_f - Z_n)

보시다시피, Q는 1에 매우 가깝고 Z_n이 Z_f보다 훨씬 작은 한 크게 변하지 않으며, 이는 일반적입니다. 그렇기 때문에 Q 값에 영향을 미치려면 근평면과 원평면을 크게 변경해야 하는데, 이는 일반적으로 불가능합니다. 동시에, 근평면 값은 후-투영 공간에 큰 영향을 미칩니다. 예를 들어, Z_n = 1미터(m)인 경우, 근평면 이후 월드 공간의 첫 번째 미터는 후-투영 공간의 단위 큐브의 절반을 차지합니다. 이러한 점에서 Z_n을 2m로 변경하면 z 값 해상도를 효과적으로 두 배로 늘리고 그림자 품질을 향상시킵니다. 즉, 어떤 수단을 사용해서든 Z_n 값을 최대화해야 합니다.

원래 PSM 논문에서 제안된 완벽한 방법은 깊이 버퍼를 다시 읽고, 각 픽셀을 스캔하고, 각 프레임에 대해 가능한 최대 Z_n을 찾는 것입니다. 불행히도, 이 방법은 상당히 비쌉니다: 대량의 비디오 메모리를 다시 읽어야 하고, CPU/GPU 지연을 추가로 유발하며, 스위즐(swizzled) 및 계층적 깊이 버퍼에서는 잘 작동하지 않습니다. 따라서 PSM 렌더링에 적합한 근평면 값을 찾기 위해 다른(아마도 덜 정확한) 방법을 사용해야 합니다.

PSM 렌더링에 적합한 근평면 값을 찾는 다른 방법에는 다양한 CPU 장면 분석 방법이 포함될 수 있습니다:

  • 객체 바운딩 볼륨 사용
  • 가시성 분석 결과 사용
  • 객체 중요도별 분석 등

이러한 방법들은 실제 근평면 값을 높이려고 시도하지만, “가상으로” 값을 높일 수도 있습니다. 아이디어는 오래된 가상 카메라와 동일하지만 한 가지 차이가 있습니다. 카메라를 뒤로 밀 때, 원래 카메라와 가상 카메라의 근평면 쿼드가 동일한 평면에 유지되도록 근평면 값을 증가시킵니다. 그림 14-6을 참조하십시오.

[그림 14-6] 가상 카메라 간의 차이

가상 카메라를 뒤로 밀면 z 값 해상도가 개선됩니다. 그러나 이렇게 하면 가까운 객체에 대한 x 및 y 값의 값 분포가 악화되어 카메라 근처와 멀리 있는 그림자 품질의 균형을 맞춥니다. 후-투영 공간에서 매우 불규칙한 z 값 분포와 근평면 값의 큰 영향 때문에, 이 “가상” 슬라이드백 없이는 이러한 균형을 달성할 수 없습니다. 카메라 근처에서는 그림자가 멋지게 보이지만 먼 객체에서는 품질이 떨어지는 일반적인 문제는 불균형한 그림자 맵 텍셀 영역 분포의 전형적인 결과입니다.

14.2 빛 각도 문제

PSM의 또 다른 문제는 그림자 품질이 조명과 카메라 위치 간의 관계에 의존한다는 것입니다. 수직 방향성 조명의 경우, 엘리어싱 문제가 완전히 제거되지만, 조명이 카메라를 향하고 정면에 가까울 때는 상당한 그림자 맵 엘리어싱이 있습니다.

우리는 전체 단위 큐브를 단일 그림자 맵 텍스처에 유지하려고 하므로, 전체 큐브를 맞추기 위해 필요한 만큼 조명의 시야각을 크게 만들어야 합니다. 이는 근평면에 가까운 객체가 충분한 텍스처 샘플을 받지 못한다는 것을 의미합니다. 그림 14-7을 참조하십시오.

[그림 14-7] 낮은 빛 각도를 가진 라이트 카메라

광원이 단위 큐브에 가까울수록 품질이 떨어집니다. 우리가 알다시피,

Q = Z_f / (Z_f - Z_n)

따라서 Z_n = 1이고 Z_f = 4000인 대규모 야외 장면의 경우, Q = 1.0002입니다. 이는 광원이 단위 큐브에 극도로 가깝다는 것을 의미합니다. Z_f/Z_n 상관관계는 일반적으로 50보다 크며, 이는 Q = 1.02에 해당하며, 이는 문제를 일으킬 만큼 충분히 가깝습니다.

전체 단위 큐브를 단일 그림자 맵 텍스처에 맞추는 데는 항상 문제가 있습니다. 두 가지 솔루션이 각각 문제의 한 부분을 해결합니다: 단위 큐브 클리핑은 라이트 카메라를 단위 큐브의 필요한 부분에만 타겟팅하고, 큐브 맵 접근법은 여러 텍스처를 사용하여 깊이 정보를 저장합니다.

14.2.1 단위 큐브 클리핑

이 최적화는 실제 객체에 대해서만 그림자 맵 정보가 필요하고, 이러한 객체가 차지하는 부피가 일반적으로 전체 뷰 프러스텀 부피보다 훨씬 작다는 사실에 의존합니다(특히 원평면에 가까운 곳에서). 그렇기 때문에 라이트 카메라를 실제 객체만 유지하도록(전체 단위 큐브가 아닌) 조정하면 더 나은 품질을 얻을 수 있습니다. 물론, 바운딩 볼륨과 같은 단순화된 장면 구조를 사용하여 카메라를 조정해야 합니다.

큐브 클리핑은 원래 PSM 논문에서 언급되었지만, 가상 카메라를 구성하기 위해 뷰 프러스텀의 그림자 캐스터와 프러스텀 외부의 잠재적 그림자 캐스터를 포함한 장면의 모든 객체를 고려했습니다. 더 이상 가상 카메라가 필요하지 않으므로, 그림자 수신자에만 라이트 카메라를 집중할 수 있으며, 이는 더 효율적입니다. 그림 14-8을 참조하십시오. 여전히 후-투영 공간에서 라이트 카메라에 대한 근거리 및 원거리 클립 평면 값을 선택하여 그림자 맵에 모든 그림자 캐스터를 유지해야 합니다. 그러나 텍셀 영역 분포를 변경하지 않기 때문에 그림자 품질에는 영향을 미치지 않습니다.

[그림 14-8] 그림자 수신자의 바운딩 볼륨을 기반으로 라이트 카메라 집중

이러한 바운딩 볼륨의 먼 부분이 후-투영 공간에서 크게 수축하기 때문에, 라이트 카메라의 시야각은 조명이 장면의 나머지 부분에 가까워도 매우 크지 않습니다.

실제로, 충분한 품질을 유지하기 위해 대략적인 바운딩 볼륨을 사용할 수 있습니다. 우리가 관심 있는 장면의 어느 부분을 일반적으로 나타내면 됩니다. 야외 장면에서는 풍경의 객체의 대략적인 높이이고, 실내 장면에서는 현재 방의 바운딩 볼륨 등입니다.

바운딩 볼륨 세트를 구축한 후 장면의 그림자 수신자에 집중된 라이트 카메라를 계산하는 알고리즘을 공식화하고 싶습니다. 사실, 라이트 카메라는 위치, 방향, 업 벡터 및 투영 매개변수로 지정되며, 대부분은 사전 정의되어 있습니다:

  • 위치: 후-투영 공간의 원점
  • 방향: 미정 - 찾아야 함
  • 업 벡터: 수직 월드 벡터를 후-투영 공간으로 변환한 것
  • 투영 매개변수: 근거리 및 원거리 평면은 모든 그림자 캐스터를 포함하도록 계산되며, 시야각은 나중에 계산됩니다

따라서 가장 흥미로운 것은 바운딩 볼륨을 기반으로 라이트 카메라 방향을 선택하는 것입니다. 제안된 알고리즘은 다음과 같습니다:

  1. 모든 바운딩 볼륨의 중심 C를 계산합니다
  2. 방향성 조명의 경우 조명 방향 L을 사용하고, 다른 조명의 경우 C에서 광원 위치를 가리키는 벡터를 사용합니다
  3. 카메라 방향을 L의 반대 방향(즉, -L)으로 설정합니다
  4. 각 바운딩 볼륨을 이 카메라를 사용하여 라이트 공간으로 투영합니다
  5. 모든 투영된 볼륨의 x 및 y 좌표의 최소 및 최대 값을 찾습니다
  6. 이 값을 사용하여 카메라의 시야각을 조정합니다

이러한 방식으로, 바운딩 볼륨 수에 따라 선형 시간에 최적의 라이트 카메라를 찾을 수 있으며, 장면 구조에 대한 대략적인 정보만 필요하기 때문에 바운딩 볼륨 수가 많지 않습니다.

이 알고리즘은 대규모 야외 장면의 직접 조명에 효율적입니다. 그림자 품질은 빛 각도와 거의 무관하며 조명이 카메라를 향할 때 약간 감소합니다. 그림 14-9는 단위 큐브 클리핑을 사용하는 것과 사용하지 않는 것의 차이를 보여줍니다.

[그림 14-9] 단위 큐브 클리핑

큐브 클리핑은 일부 경우에 효율적이지만, 다른 경우에는 사용하기 어렵습니다. 예를 들어, 밀집된 단위 큐브(일반적임)가 있거나 바운딩 볼륨을 전혀 사용하고 싶지 않을 수 있습니다. 또한, 큐브 클리핑은 포인트 라이트에서 작동하지 않습니다.

14.2.2 큐브 맵 접근법

더 일반적인 방법은 그림자 매핑에 큐브 맵 텍스처를 사용하는 것입니다. 대부분의 광원은 후-투영 공간에서 포인트 라이트가 되며, 포인트 광원으로 그림자 매핑하는 데 큐브 맵을 사용하는 것이 자연스럽습니다. 그러나 후-투영 공간에서는 상황이 약간 달라지며 단위 큐브에 대한 정보만 저장하면 되므로 큐브 맵을 다르게 사용해야 합니다.

제안된 솔루션은 조명에 대해 후면(back facing)인 단위 큐브 면을 큐브 맵 면 텍스처의 플랫폼으로 사용하는 것입니다.

후-투영 공간의 직접 광원의 경우, 큐브 맵은 그림 14-10과 같이 보입니다.

[그림 14-10] 직접 조명에 큐브 맵 사용

사용되는 큐브 맵 면의 수(3개에서 5개 범위)는 조명의 위치에 따라 달라집니다. 조명이 장면의 나머지 부분에 가깝고 카메라를 향할 때 최대 면 수를 사용하므로 추가 텍스처 리소스가 필요합니다. 단위 큐브 외부에 위치한 다른 유형의 광원의 경우, 그림이 유사합니다.

단위 큐브 내부에 위치한 포인트 라이트의 경우, 6개의 큐브 맵 면을 모두 사용해야 하지만 여전히 단위 큐브 면에 집중되어 있습니다. 그림 14-11을 참조하십시오.

[그림 14-11] 포인트 라이트와 함께 큐브 맵 사용

“중심이 이동된 큐브 맵”을 형성한다고 말할 수 있습니다. 이는 일반 큐브 맵과 유사하지만 텍스처 좌표에 상수 벡터가 추가됩니다. 즉, 큐브 맵의 텍스처 좌표는 광원 위치만큼 이동된 후-투영 공간의 정점 위치입니다:

텍스처 좌표 = 정점 위치 - 광원 위치

단위 큐브 면을 큐브 맵 플랫폼으로 선택함으로써, 텍스처 영역을 화면 크기에 비례하여 분배하고 그림자 품질이 조명 및 카메라 위치에 의존하지 않도록 보장합니다. 사실, 후-투영 공간의 텍셀 크기는 보장된 범위에 있으므로, 화면에 대한 투영은 투영되는 평면에만 의존합니다. 이 투영은 텍셀을 많이 늘리지 않으므로 화면의 텍셀 크기도 보장된 범위 내에 있습니다.

그림자 맵을 렌더링할 때 정점 및 픽셀 셰이더가 상대적으로 짧기 때문에, 백 버퍼 및 깊이 그림자 맵 버퍼에 대한 순수 채우기 속도가 중요합니다. 따라서 단일 그림자 맵을 그리는 것과 동일한 총 텍스처 크기의 큐브 맵을 그리는 것 사이에는 거의 차이가 없습니다(좋은 가림 컬링이 있는 경우). 큐브 맵 접근법은 단일 텍스처와 동일한 총 텍스처 크기로 더 나은 품질을 제공합니다. 차이점은 렌더 타겟 전환 비용과 정점 및 픽셀 셰이더에서 큐브 맵 텍스처 좌표를 계산하기 위한 추가 명령입니다.

이러한 텍스처 좌표를 계산하는 방법을 살펴보겠습니다. 먼저, 그림 14-12에 표시된 그림을 고려하십시오. 파란색 사각형은 우리의 단위 큐브이고, P는 광원 점이며, V는 텍스처 좌표를 생성하는 점입니다. 그림자 맵에 대해 6개의 큐브 맵 면을 별도의 패스로 렌더링합니다. 각 패스의 근평면은 녹색으로 표시됩니다. 그들은 또 다른 작은 큐브를 형성하므로 Z1 = Zn/Zf는 모든 패스에서 일정합니다.

[그림 14-12] 후-투영 공간에서 큐브 맵의 상세 뷰

이제 점 V에 대한 텍스처 좌표와 비교할 깊이 값을 계산해야 합니다. 이것은 큐브와 교차할 때까지 (V - P) 방향으로 이 점을 이동해야 한다는 것을 의미합니다. d1, d2, d3, d4, d5, d6(그림 14-12의 면 번호 참조)을 P에서 각 큐브 맵 면까지의 거리로 간주하십시오.

우리가 찾고 있는 큐브의 점(큐브 맵 텍스처 좌표이기도 함)은 다음과 같습니다:

큐브 맵 텍스처 좌표 = P + (V - P) × max(1/d1, 1/d2, 1/d3, -1/d4, -1/d5, -1/d6)

텍스처의 값을 a 값의 투영 변환 결과와 비교합니다. 해당 d 값으로 이미 나눴기 때문에, 효과적으로 Zf = 1이고 Zn = Z1이 되었으므로, 해당 투영 변환을 적용하기만 하면 됩니다. 섹션 14.2.1의 역 카메라 투영의 경우 Zn = -Z1, Zf = Z1입니다.

(이러한 모든 계산은 단위 큐브가 실제로 단위 큐브인 OpenGL과 같은 좌표계에서 이루어집니다. Direct3D에서는 z 좌표가 [0..1] 범위에 있기 때문에 단위 큐브가 절반 크기입니다.)

리스팅 14-1은 셰이더 코드가 어떻게 보일 수 있는지의 예입니다.

리스팅 14-1. 큐브 맵 텍스처 좌표 계산

// c[d1] = 1/d1, 1/d2, 1/d3, 0
// c[d2] = -1/d4, -1/d5, -1/d6, 0
// c[z] = Q, -Q * Zn, 0, 0
// c[P] = P
// r[V] = V
// cbmcoord - 출력 큐브 맵 텍스처 좌표
// depth - 그림자 맵 값과 비교할 깊이

// 정점 레벨당
sub r[VP], r[V], c[P]
mul r1, r[VP], c[d1]
mul r2, r[VP], c[d2]

// 픽셀 레벨당
max r3, r1, r2
max r3.x, r3.x, r3.y
max r3.x, r3.x, r3.z
rcp r3.w, r3.x
mad cbmcoord, r[VP], r3.w, c[P]
rcp r3.x, r3.w
mad depth, r3.x, c[z].x, c[z].y

깊이 텍스처는 큐브 맵이 될 수 없기 때문에, 깊이 값을 컬러 채널에 패킹하는 컬러 텍스처를 사용할 수 있습니다. 이를 수행하는 방법은 많고 구현에 따른 트릭도 많지만, 그 설명은 이 장의 범위를 벗어납니다.

또 다른 가능성은 멀티텍스처링으로 이 큐브 맵 접근법을 에뮬레이션하는 것입니다. 여기서 모든 큐브 맵 면이 독립적인 텍스처가 됩니다(이 경우 깊이 텍스처가 훌륭함). 정점 셰이더에서 여러 텍스처 좌표 세트를 형성하고 픽셀 셰이더에서 그림자 결과를 곱합니다. 까다로운 부분은 장면의 객체에 대해 이러한 텍스처를 관리하는 것입니다. 왜냐하면 모든 객체가 6개 면을 모두 필요로 하는 경우는 거의 없기 때문입니다.

14.3 바이어싱 문제

앞서 언급했듯이, 균일한 그림자 맵에서 일반적으로 사용되는 상수 바이어스는 PSM과 함께 사용할 수 없습니다. 왜냐하면 z 값과 텍셀 영역 분포가 서로 다른 조명 위치 및 장면의 점에 따라 크게 달라지기 때문입니다.

깊이 텍스처를 사용할 계획이라면, 바이어싱에 z 기울기 스케일 바이어스(z slope-scaled bias)를 사용해 보십시오. 특히 매우 먼 객체가 카메라 안에 들어오지 않을 때 아티팩트를 수정하기에 충분한 경우가 많습니다. 그러나 일부 카드는 깊이 텍스처를 지원하지 않으며(DirectX에서 깊이 텍스처는 NVIDIA 카드만 지원함), 깊이 텍스처는 큐브 맵이 될 수 없습니다. 이러한 경우, 바이어스를 계산하기 위한 다른, 더 일반적인 알고리즘이 필요합니다. 또 다른 어려움은 z 기울기 스케일 바이어스를 에뮬레이션하고 조정하기 어렵다는 것입니다. 왜냐하면 현재 삼각형의 정점 좌표와 같은 추가 데이터를 픽셀 셰이더로 전달하고 일부 계산을 수행해야 하기 때문입니다. 이는 전혀 견고하지 않습니다.

어쨌든, 왜 더 이상 상수 바이어스를 사용할 수 없는지 살펴보겠습니다. 두 가지 경우를 고려하십시오: 광원이 단위 큐브 근처에 있는 경우와 광원이 단위 큐브에서 멀리 떨어진 경우입니다. 그림 14-13을 참조하십시오.

[그림 14-13] 단위 큐브에 가깝고 먼 조명

문제는 그림자 맵으로의 z 값 분포를 결정하는 Zf/Zn 상관관계가 이 두 경우에서 크게 달라진다는 것입니다. 따라서 상수 바이어스는 월드 공간과 후-투영 공간에서 완전히 다른 실제 바이어스를 의미합니다: 첫 번째 조명 위치에 맞춰진 상수 바이어스는 두 번째 조명에 대해 올바르지 않으며, 그 반대도 마찬가지입니다. 한편, Zf/Zn은 많이 변합니다. 왜냐하면 광원이 월드 공간에서 조명과 카메라의 상대적 위치에 따라 단위 큐브에 가까울 수도 있고 멀리 떨어져 있을 수도 있기 때문입니다(심지어 무한대에 있을 수도 있음).

고정된 광원 위치에서도 때로는 바이어스에 적합한 상수를 찾을 수 없습니다. 바이어스는 점 위치에 따라 달라져야 합니다. 왜냐하면 투영 변환이 가까운 객체를 확대하고 먼 객체를 축소하기 때문입니다. 따라서 바이어스는 카메라 근처에서 더 작고 먼 객체에 대해 더 커야 합니다. 그림 14-14는 이러한 상황에서 상수 바이어스를 사용하는 전형적인 아티팩트를 보여줍니다.

[그림 14-14] 상수 바이어스의 아티팩트

14.3.1 정점 셰이더에서 바이어스 계산

간단히 말해서, 제안된 솔루션은 월드 공간에서 바이어싱을 사용하고(이중 투영 행렬의 결과를 분석하지 않음) 이 월드 공간 바이어스를 후-투영 공간으로 변환하는 것입니다. 계산된 값은 이중 투영에 따라 달라지며 모든 조명 및 카메라 위치에 대해 올바릅니다. 이러한 작업은 정점 셰이더에서 쉽게 수행할 수 있습니다. 또한, 이 월드 공간 바이어스 값은 비균일 텍셀 영역 분포로 인한 아티팩트를 처리하기 위해 월드 공간의 텍셀 크기로 스케일되어야 합니다.

P_biased = (P_orig + L(a + bL_texel))M,

여기서 P_orig는 원래 점 위치이고, L은 월드 공간의 조명 벡터 방향이며, L_texel은 월드 공간의 텍셀 크기이고, M은 최종 그림자 맵 행렬이며, a와 b는 바이어스 계수입니다.

월드 공간의 텍셀 크기는 간단한 행렬 계산으로 대략적으로 계산할 수 있습니다. 먼저, 점을 그림자 맵 공간으로 변환한 다음, 깊이를 변경하지 않고 텍셀 크기만큼 이 점을 이동시킵니다. 다음으로, 이를 월드 공간으로 다시 변환하고 이 점과 원래 점 사이의 차이의 길이를 제곱합니다. 이것이 L_texel을 제공합니다:

L_texel = sqrt(||M’P_orig||²),

여기서:

M’ = M^(-1) [1/Sx 0 0 0] [0 1/Sy 0 0] M - I [0 0 1 0] [0 0 0 1]

그리고 Sx와 Sy는 그림자 맵 해상도입니다.

분명히, 좌표를 곱하는 것을 제외하고 모든 변환을 수행하는 단일 행렬을 만들 수 있습니다:

L_texel = sqrt(||M’P_orig||²),

여기서 M’은 변환, 이동, 다시 변환 및 빼기를 포함합니다.

이것은 다소 경험적인 솔루션으로 밝혀졌지만, 여전히 특정 요구사항에 맞게 조정되어야 합니다. 그림 14-15를 참조하십시오.

[그림 14-15] 정점 셰이더에서 계산된 바이어스

이러한 계산을 수행하는 정점 셰이더 코드는 리스팅 14-2와 같이 보일 수 있습니다.

리스팅 14-2. 정점 셰이더에서 바이어스 계산

def c0, a, b, 0, 0

// L_texel 계산
dp4 r1.x, v0, c[LtexelMatrix_0]
dp4 r1.y, v0, c[LtexelMatrix_1]
dp4 r1.z, v0, c[LtexelMatrix_2]
dp4 r1.w, v0, c[LtexelMatrix_3]

// 동차 좌표 변환
// (사실, 이 단계를 종종 건너뛸 수 있음)
rcp r1.w, r1.w
mul r1.xy, r1.w, r1.xy

// 이제 r1.x는 L_texel
mad r1.x, r1.x, c0.x, c0.y
dp3 r1.x, r1, r1

// 월드 공간에서 정점 이동
mad r1, v0, c[Lightdir], r1.x

// 정점을 후-투영 공간으로 변환
// (z와 w만 필요함)
dp4 r[out].z, r1, c[M_2]
dp4 r[out].w, r1, c[M_3]

r[out] 레지스터는 바이어싱의 결과를 보유합니다: 깊이 값과 삼각형 전체에 걸쳐 보간되어야 하는 해당 w입니다. 이 보간은 텍스처 좌표(x, y 및 해당 w)의 보간과 별도여야 합니다. 왜냐하면 이러한 w 좌표가 다르기 때문입니다. 이 바이어스된 값은 그림자 맵 값과 비교할 때 또는 실제 그림자 맵 렌더링 중에 사용할 수 있습니다(그림자 맵은 바이어스된 값을 보유함).

14.4 필터링 및 블러링

그림자 볼륨에 비해 그림자 매핑의 장점은 “그림자진” 샘플과 “그림자지지 않은” 샘플 사이에 색상 그라디언트를 만들어 부드러운 그림자를 시뮬레이션할 수 있는 가능성입니다. 이 그림자 “부드러움”은 차폐물로부터의 거리, 광원 크기 등에 의존하지 않지만 월드 공간에서 여전히 작동합니다. 반면에 스텐실 그림자를 블러링하는 것은 더 어렵지만, Assarsson 등(2003)은 상당한 진전을 이룹니다.

이 섹션에서는 일정한 범위의 블러링을 가진 가짜 그림자 부드러움을 만들지만 여전히 좋아 보이도록 그림자 맵을 필터링하고 블러링하는 방법을 다룹니다.

14.4.1 퍼센티지 근접 필터링(PCF)

그림자 맵 필터링의 대부분의 방법은 퍼센티지 근접 필터링(Percentage-Closer Filtering, PCF) 원리를 기반으로 합니다. 방법 간의 유일한 차이점은 하드웨어가 이를 사용하는 방법입니다. NVIDIA 깊이 텍스처는 깊이 값과 비교한 후 PCF를 수행합니다. 다른 하드웨어에서는 가장 가까운 텍셀에서 여러 샘플을 가져와 결과를 평균화해야 합니다(진정한 PCF의 경우). 일반적으로, 깊이 텍스처 필터링은 4개 샘플의 수동 PCF 기술보다 더 효율적입니다. (PCF는 비슷한 품질을 생성하기 위해 약 8개의 샘플이 필요합니다.) 또한, 깊이 텍스처 필터링을 사용한다고 해서 PCF가 금지되는 것은 아니므로, 그림자 품질을 더욱 향상시키기 위해 여러 개의 필터링된 샘플을 가져올 수 있습니다.

PSM과 함께 PCF를 사용하는 것은 표준 그림자 맵과 함께 사용하는 것과 다르지 않습니다: 인접한 텍셀의 샘플이 필터링에 사용됩니다. GPU에서는 텍스처 좌표를 각 방향으로 하나의 텍셀만큼 이동시켜 이를 달성합니다. PCF에 대한 더 자세한 논의는 11장 “그림자 맵 앤티앨리어싱”을 참조하십시오.

4개 샘플을 사용한 PCF의 셰이더 의사 코드는 리스팅 14-3 및 14-4와 같습니다.

리스팅 14-3. PCF 정점 셰이더

def c0, sample1x, sample1Y, 0, 0
def c1, sample2x, sample2Y, 0, 0
def c2, sample3x, sample3Y, 0, 0
def c3, sample4x, sample4Y, 0, 0

// 가장 간단한 경우:
// def c0, 1 / shadowmapsizeX, 1 / shadowmapsizeY, 0, 0
// def c1, -1 / shadowmapsizeX, -1 / shadowmapsizeY, 0, 0
// def c2, -1 / shadowmapsizeX, 1/ shadowmapsizeY, 0, 0
// def c3, 1 / shadowmapsizeX, -1 / shadowmapsizeY, 0, 0

...

// Point - 조명 공간의 정점 위치
mad oT0, point.w, c0, point
mad oT1, point.w, c1, point
mad oT2, point.w, c2, point
mad oT3, point.w, c3, point

리스팅 14-4. PCF 픽셀 셰이더

def c0, 0.25, 0.25, 0.25, 0.25

tex t0
tex t1
tex t2
tex t3

...

// 깊이 비교 후
mul r0, t0, c0
mad r0, t1, c0, r0
mad r0, t2, c0, r0
mad r0, t3, c0, r0

이러한 트릭은 그림자 품질을 향상시키지만 심각한 엘리어싱 문제를 숨기지는 못합니다. 예를 들어, 많은 화면 픽셀이 하나의 그림자 맵 텍셀에 매핑되면, 큰 계단식 아티팩트가 다소 블러되더라도 보일 것입니다. 그림 14-16은 필터링 없이 엘리어싱된 그림자를 보여주고, 그림 14-17은 PCF가 그림자 품질을 향상시키는 데 어떻게 도움이 되지만 엘리어싱 아티팩트를 완전히 제거할 수 없는지 보여줍니다.

[그림 14-16] 강한 엘리어싱

[그림 14-17] 필터링된 엘리어싱

14.4.2 핑퐁 블러링

투영 그림자에서 알다시피, 최상의 블러링 결과는 종종 픽셀 셰이더 블러를 사용하여 더 작은 해상도 텍스처로 렌더링한 다음, 이 결과 텍스처를 블러 픽셀 셰이더를 통해 여러 번 다시 공급하는 것(핑퐁 렌더링이라고 함)에서 나옵니다. 그림자 매핑과 투영 그림자는 유사한 기술이므로, 왜 이 방법을 사용할 수 없습니까? 답: 그림자 맵은 흑백 그림이 아니기 때문입니다. 깊이 값의 모음이며, “깊이 맵을 블러링”하는 것은 의미가 없습니다.

사실, 제안은 그림자 맵 렌더의 컬러 부분(거의 무료로 제공됨)을 일부 객체에 대한 투영 텍스처로 사용하는 것입니다. 예를 들어, 야외 풍경 장면이 있고 지면 그림자가 가장 눈에 띄기 때문에 지면에 고품질 블러된 그림자를 원한다고 가정합니다.

그림자 맵을 렌더링할 때 컬러 버퍼에 흰색(또는 다른 밝은 색상)을 렌더링하고 블러링 기술을 적용한 다음, 지면을 렌더링할 때 이 블러된 텍스처를 투영 텍스처로 사용합니다. 이렇게 하면 모든 그림자가 사라지고 지면의 그림자만 블러 텍스처에서 나옵니다. 따라서 지면은 블러되고 좋은 그림자를 가지며 모든 것은 깊이 텍스처로 렌더링됩니다. 이것이 Figure 14-18(원본 컬러 부분) 및 Figure 14-19(블러된 컬러 부분)에 표시됩니다.

[그림 14-18] 작은 테스트 장면의 원본 컬러 부분

[그림 14-19] 작은 테스트 장면의 블러된 컬러 부분

그림 14-20은 실제 장면에 블러링을 적용하는 것을 보여줍니다.

[그림 14-20] 실제 장면에 블러링 적용

품질의 차이는 극적입니다.

물론, 이 방법은 풍경뿐만 아니라 자기 그림자가 필요하지 않은 모든 객체(바닥, 벽, 지면 평면 등)에 사용할 수 있습니다. 다행히도, 이러한 영역에서 그림자가 가장 눈에 띄고 엘리어싱 문제가 가장 분명합니다. 여러 컬러 채널이 있기 때문에 여러 객체의 그림자를 동시에 블러할 수 있습니다:

  • 그림자 맵 렌더링 중에 채널 1에 지면 렌더링
  • 그림자 맵 렌더링 중에 채널 2에 바닥 렌더링
  • 그림자 맵 렌더링 중에 채널 3에 벽 렌더링

이렇게 하면 적절한 자기 그림자와 함께 다른 객체에 (PCF로 블러된) 다른 모든 그림자를 유지하면서 지면, 바닥, 벽 등에 멋진 블러된 그림자를 얻을 수 있습니다.

14.5 결과

그림 14-21, 14-22, 14-23 및 14-24의 스크린샷은 1600x1200 화면 해상도에서 NVIDIA GeForce4 Ti 4600에서 캡처되었으며, 100,000~500,000개의 가시 폴리곤이 있습니다. 모든 객체는 실시간 프레임 속도(30 이상)로 그림자를 받고 드리웁니다.

[그림 14-21, 14-22, 14-23, 14-24] [실제 구현 스크린샷들]

14.6 결론

Perspective Shadow Maps는 대규모 환경 그림자를 위한 유망한 접근 방식입니다. 다른 워핑 방법에 비해 많은 특수한 경우와 처리의 어려움이 없으며 화면 공간에서 상대적으로 균일한 언더샘플링 오류를 제공합니다. 따라서 그림자 맵 해상도를 증가시키는 것만으로도 뷰어로부터의 거리에 거의 독립적으로 장면의 모든 객체에 대해 그림자의 들쭉날쭉한 가장자리를 크게 줄일 수 있습니다.

이 장에서는 PSM을 실제로 사용 가능하게 만드는 몇 가지 기술을 제시했습니다:

  • 역 투영 행렬: 가상 카메라의 필요성을 제거하여 더 나은 그림자 품질을 제공합니다
  • 단위 큐브 클리핑: 관심 있는 장면의 부분에 그림자 맵을 집중시킵니다
  • 큐브 맵 접근법: 여러 텍스처를 사용하여 넓은 각도 범위에서 고품질 그림자를 제공합니다
  • 정점 셰이더 바이어싱: 모든 조명 및 카메라 위치에서 작동하는 적응형 바이어싱
  • 핑퐁 블러링: 선택된 표면에 매우 부드러운 그림자를 생성합니다

이러한 기술은 결합하여 사용하거나 개별적으로 사용할 수 있으며, 특정 응용 프로그램의 요구사항에 맞게 조정할 수 있습니다.

참고문헌

Assarsson, U., M. Dougherty, M. Mounier, and T. Akenine-Möller. 2003. “An Optimized Soft Shadow Volume Algorithm with Real-Time Performance.” In Proceedings of the SIGGRAPH/Eurographics Workshop on Graphics Hardware 2003.

Gartner, Bernd. 1999. “Smallest Enclosing Balls: Fast and Robust in C++.” 웹 페이지. http://www.inf.ethz.ch/personal/gaertner/texts/own_work/esa99_final.pdf

Stamminger, Marc, and George Drettakis. 2002. “Perspective Shadow Maps.” In Proceedings of ACM SIGGRAPH 2002.

감사의 말

저자는 많은 유익하고 생산적인 토론을 해준 Peter Popov에게 감사드립니다.