Shadow Map Filtering
What is Shadow Mapping 에서 Shadow Mapping 에 대한 간단한 번역 & 설명을 적어놓았다. 해당 글에서 PCF 를 잠깐 언급했었다. 이 글에서는 PCF 를 포함해서 Shadow Map 을 필터링하는 방법에 대해서 알아보겠다.
첫번째는 PCF 다. 풀어쓰면 Percentage Closer Filtering 이라는 단어가 되며, Shadow Map 을 여러번 샘플링해 Percentage 를 소숫점으로 나타내서 Shadow 가 생긴 정도를 나타내주는 Filtering 기법이다. 쉽게 이해할 수 있도록 아래 그림을 보자.
위의 a) 는 아무것도 필터링 하지 않을 때의 ShadowMap 샘플링하는 것을 보여주고, 아래 b) 는 PCF 를 사용해 샘플링하는 것을 보여준다. 위 그림에서 Surface at z = 49.8 은 그림자를 처리할 표면의 Depth 또는 Z 을 뜻한다. 그리고 Light-Space 를 기준으로 해당 값보다 Depth 값이 멀다고 판단될시에는 처리하지 않고, 가깝다고 판단될 때는 처리하는 걸로 해준다. Shadow Map 에서 한 부분만 샘플링해서 하는 것이 윗 부분의 그림이고, 한 부분이 아닌 근처의 여러 부분을 샘플링해서 값을 구하는 것이 PCF 다.
아래에 PCF 를 사용하는 코드가 있다.
float PCF_FILTER( float2 tex, float fragDepth )
{
//PreShader - This should all be optimized away by the compiler
//====================================
float fStartOffset = BoxFilterStart( fFilterWidth );
float texOffset = 1.0f / fTextureWidth;
//====================================
fragDepth -= 0.0001f;
tex -= fStartOffset * texOffset;
float lit = 0.0f;
for( int i = 0; i < fFilterWidth; ++i )
for( int j = 0; j < fFilterWidth; ++j )
{
lit += texShadowMap.SampleCmpLevelZero(
FILT_PCF,
float2( tex.x + i * texOffset, tex.y + j * texOffset ),
fragDepth
);
}
return lit / ( fFilterWidth * fFilterWidth );
}
자세한 코드는 위의 출처에서 코드를 구해서 보면 될듯하다. Texture2D::SampleCmpLevelZero 는 MipMap 참조 레벨은 0으로 한채 텍스쳐의 값을 샘플링하여 주어진 인자와 비교하여 Sampler 에 정해준 방식에 적합하면 1, 적합하지 않으면 0을 반환해준다.
해당 그림에서는 평균을 구하는 방법을 표기해놓았으나 다른 NDF 를 써서 구현할 수도 있다.(Gaussian Distribution) 또한 규칙적으로 샘플링하는게 아닌 jitter 를 사용해서 샘플링할 수도 있다고 한다. 일반적으로 Poisson disk Distribution 을 사용한다고 한다.
PCF 의 단순한 방법으로 Shadow Map 을 AntiAliasing 할 수 있다. 하지만 Shadow Map 의 샘플링 횟수가 PCF Kernel(3x3, 5x5..) 이 커지면 커질수록 많아지기 때문에 꽤나 큰 PCF Kernel 에서는 샘플링 부하가 걸릴 수 있다. 성능상 단점이 크나 PCF 는 굉장히 많이 사용되는 기법 중에 하나라고 한다.
다음은 Variance Shadow Map 이다. 이는 Chebyshev’s Inequality 라는 통계학의 개념을 사용해 Filtering 해준다. 먼저 Shadow Map 을 저장할 때 Depth 만 저장하는게 아닌 Depth 의 제곱값 또한 같이 저장한다. Filtering 에 쓰일 공식을 위해 같이 넣는다.
다음은 아래 코드와 같이 공식을 계산해준다.
float VSM_FILTER( float2 tex, float fragDepth )
{
float lit = (float)0.0f;
float2 moments = texShadowMap.Sample( FILT_LINEAR, float3( tex, 0.0f ) );
float E_x2 = moments.y;
float Ex_2 = moments.x * moments.x;
float variance = E_x2 - Ex_2;
float mD = (moments.x - fragDepth );
float mD_2 = mD * mD;
float p = variance / (variance + mD_2 );
lit = max( p, fragDepth <= moments.x );
return lit;
}
눈여겨 볼것은 샘플러를 Linear 하게 설정해놓는 것이다.
하지만 VSM 은 큰 단점이 하나 있다. 바로 Light Leaking 이 일어나는 것이다. 이는 GDC 2006 : Variance Shadow Map 에서 참조할 수 있다. 이를 해결 하는 근본적인 방법은 없다고 한다.
두가지 기법의 차이는 텍스쳐 샘플링을 더 많이 하느냐, 메모리를 2배로 늘려주느냐의 차이에 있다. 속도를 따지면 VSM 이 빠르다고 한다. 하지만 굳이 퍼포먼스를 낼 필요가 없다면 PCF 를 사용하는 것도 나쁜 선택은 아닐것 같다. 선택에 대한 궁금증은 OpenGL Forum : Shadow filtering: PCF better than VSM? 글을 참조하길 바란다.
참조
- Pixar : Rendering Antialiased Shadows with Depth Maps
- MSDN : SampleCmpLevelZero
- Variance Shadow Maps
- GDC 2006 : Variance Shadow Maps
- Wikipedia : Chebyshev’s inequality
- GPU Gems : Shadow Map Antialiasing
- NVidia : Variance Shadow Mapping Website
- Github : TheRealMJP - Shadows
- OpenGL Forum : Shadow filtering: PCF better than VSM?