What is Shadow Mapping 에서 Shadow Mapping 에 대한 간단한 번역 & 설명을 적어놓았다. 해당 글에서 PCF 를 잠깐 언급했었다. 이 글에서는 PCF 를 포함해서 Shadow Map 을 필터링하는 방법에 대해서 알아보겠다.

첫번째는 PCF 다. 풀어쓰면 Percentage Closer Filtering 이라는 단어가 되며, Shadow Map 을 여러번 샘플링해 Percentage 를 소숫점으로 나타내서 Shadow 가 생긴 정도를 나타내주는 Filtering 기법이다. 쉽게 이해할 수 있도록 아래 그림을 보자.


출처 : Pixar : Rendering Antialiased Shadows with Depth Maps


위의 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 );
}
출처 : NVidia : Variance Shadow Mapping Website


자세한 코드는 위의 출처에서 코드를 구해서 보면 될듯하다. Texture2D::SampleCmpLevelZeroMipMap 참조 레벨은 0으로 한채 텍스쳐의 값을 샘플링하여 주어진 인자와 비교하여 Sampler 에 정해준 방식에 적합하면 1, 적합하지 않으면 0을 반환해준다.

해당 그림에서는 평균을 구하는 방법을 표기해놓았으나 다른 NDF 를 써서 구현할 수도 있다.(Gaussian Distribution) 또한 규칙적으로 샘플링하는게 아닌 jitter 를 사용해서 샘플링할 수도 있다고 한다. 일반적으로 Poisson disk Distribution 을 사용한다고 한다.

PCF 의 단순한 방법으로 Shadow MapAntiAliasing 할 수 있다. 하지만 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;
}
출처 : NVidia : Variance Shadow Mapping Website


눈여겨 볼것은 샘플러를 Linear 하게 설정해놓는 것이다.

하지만 VSM 은 큰 단점이 하나 있다. 바로 Light Leaking 이 일어나는 것이다. 이는 GDC 2006 : Variance Shadow Map 에서 참조할 수 있다. 이를 해결 하는 근본적인 방법은 없다고 한다.

두가지 기법의 차이는 텍스쳐 샘플링을 더 많이 하느냐, 메모리를 2배로 늘려주느냐의 차이에 있다. 속도를 따지면 VSM 이 빠르다고 한다. 하지만 굳이 퍼포먼스를 낼 필요가 없다면 PCF 를 사용하는 것도 나쁜 선택은 아닐것 같다. 선택에 대한 궁금증은 OpenGL Forum : Shadow filtering: PCF better than VSM? 글을 참조하길 바란다.

참조