Percentage-Closer Filtering Shadows 에서 PCF 를 응용한 PCSS 라는 Soft Shadow 를 나타내는 기법에 대해서 알아보았다. 이번 글에서는 여태까지 알아본 것들에 비해 굉장히 최근에 나온 기법인 frustum-Traced Shadow 에 대해서 알아볼것이다.

해당 기법은 2015년에 Siggraph, Interactive 3D 같은 컨퍼런스에서 발표되었으며, 현재 Tom Clansy’s the DivisionPCSS 와 혼합된 형태(Hybrid frustum Traced Shadow)로 적용되어 있다. Frame Rate 에 조금 영향을 미쳐 대부분의 게이머들은 아직은 HFTS 를 사용하지 않는듯 하다.(Reddit : Nvidia HFTS (The Division)) 하지만 컴퓨팅 파워가 늘어나는 것을 가정한다면 앞으로 하이엔드 게임의 주 옵션이 될수도 있겠다.

이 기법의 저자는 Shadow Map 처럼 따로 붙은 기법없이 Aliasing 이 없어야 했으며, 현세대의 GPU 와 해상도를 Interactive 하게 지원하는 것이 완벽한 Hard Shadow 를 목표로 FTS 를 고안했다. 가장 많이 쓰이는 Shadow Map 기법은 공간적(Light-SpaceClipping-SpaceDiscretize 된 결과의 차이), 일시적인(필터링이 필요한 Aliasing)인 문제들이 산재한다. 이는 이 기법을 고안한 시발점이였다.

FTS 의 이론적인 뿌리를 정하기 위해 저자는 여태까지 존재하는 여러 기법을 언급한다. 빛을 하나의 직선단위로 시뮬레이팅 하는 Ray-Tracing, 볼륨을 통한 각각의 폴리곤들을 테스트 하는 Shadow Volume, Irregular Z-Buffers 를 언급했다.

Shadow Volume 은 3차원상으로 Shadow 가 생기는 부분을 정해 그 부분을 테스트해서 Shadow 를 정해주는 기법이다. 이는 Shadow Map 보다 픽셀 단위로 처리할 수 있지만, 여러 단점이 있다고 한다. 한번에 해결되는 깔끔한 방법이 없으며, 보이지 않는 부분도 처리하기 때문에 Fill-Rate 를 많이 소모한다. 게다가 처리 자체가 간단하지 않기 때문에 개발자들도 많이 쓰는 기법이 아니라고 한다. 필자도 Shadow Map 에 대한 자료는 굉장히 많이 봤지만 Shadow Volume 은 거의 본적이 없다.


출처 : GPU Gems : Efficient Shadow Volume Rendering


Ray-Tracing 은 빛을 직선 단위로 시뮬레이팅을 하는 기법으로, 계산 비용 자체가 비싸기 때문에 하드웨어와 구조에 굉장히 의존적이라고 한다. 게임에서도 쓰일 수 있는 기법이 있었지만 다른 후보에 밀려났다. 바로 Irregular Z-Buffer 다. 현대 GPU 의 Geometry -> Rasterize 구조에 맞춰 가장 걸맞는 방법이라고 한다. 자세한 설명은 아래에서 보자.

Key Idea

이 기법의 중요한 아이디어는 앞에서 소개한 Irregular Z-Bufferfrustum-Triangle Test 이 두가지다. Irregular Z-Buffer 는 앞서 Shadow Map 의 단점중에 공간적 괴리를 해결하는 데이터 구조이고, frustum-Triangle Test 는 논문에서 한 말을 이용하면 Sub-Pixel Accurate Pixel 을 구성하기 위한 시뮬레이션 테스트다. 이 두가지를 간단하게 살펴보자.

첫번째로는 바로 위에서 언급했던 Irregular Z-Buffer 다. 여기서의 IZB 는 우리가 알던 일반적인 Buffer 의 쓰임새와는 조금 다르게 쓰인다. 이 기법에서의 IZB 는 일반적인 Shadow Map 에서의 Eye-SpaceLight-Space 의 괴리를 없에기 위해 Light-Space 를 기준으로 Depth 를 쭉 저장하는게 아닌, Eye-Space 의 각각 픽셀별로 표현하는 물체에 영향을 미치는 광원을 방향으로 Ray 를 쏜다. 그리고 Light-Space 를 기준으로 만든 Grid 버퍼에 Ray 가 부딫치고, 부딫친 부분에서 가장 가까운 텍셀에 데이터를 저장한다. 위에서 설명한 IZB 를 구성하는 방법에 대한 그림이 아래에 있다.


출처 : Frustum-Traced Irregular Z-Buffers: Fast, Sub-pixel Accurate Hard Shadows


간단하게 이런식으로 IZB 가 구성되는 것을 알 수 있다. 이제 Geometry 와 비교하는 Visibility Test 가 필요하다. 일반적인 Shadow MappingVisibility Test 와는 조금 다르다. 기존의 Shadow Mapping 은 정점을 Light-Space 로 바꾸어 Z 값을 비교하여 Visibility Test 를 한다. 하지만 이 기법에서의 Visibility Test 는 다르다. 위에서 언급한 것과 같이 IZB 를 만든다. 그 다음 Occlluder Geometry 들을 Light-Space 를 기준으로 Conservative Rasterization 을 해준다.1 그렇게 나온 결과를 통해 IZB 와 함께 Visibility Test 를 한다. Conservative Rasterization 의 결과는 거의 Flag 로 사용될것으로 예측되고, Eye-Space 픽셀의 그림자 계산은 복잡한 계산을 통해 구한다. 아래는 논문에 있던 IZB 를 기준으로 쓰여진 수도 코드다.


// Step 1: Identify pixel locations we need to shadow
G(x, y) ← RenderGBufferFromEye()

// Step 2: Add pixels to our light-space IZB data structure
for pixel p ∈ G(x, y) do
    lsTexelp ← ShadowMapXform[ GetEyeSpacePos( p ) ]
    izbNodep ← CreateIZBNode[ p ]
    AddNodeToLightSpaceList[ lsTexelp, izbNodep ]
end for

// Step 3: Test each triangle with pixels in lists it covers
for tri t ∈ SceneTriangles do
    for frag f ∈ ConservateLightSpaceRaster( t ) do
        lsTexelf ← FragmentLocationInRasterGrid[ f ]
        for node n ∈ IZBNodeList( lsTexelf ) do
            p ← GetEyeSpacePixel( n )
            visMask[p] = visMask[p] | TestVisibility[ p, t ]
        end for
    end for
end for
출처 : Frustum-Traced Irregular Z-Buffers: Fast, Sub-pixel Accurate Hard Shadows


다음은 Visibility Test 다. 논문에서는 frustum-Triangle Test 라고 부르는데, 이는 조금 복잡한 과정으로 구성된다. 아래 그림을 보면서 알아보자.


출처 : Frustum-Traced Irregular Z-Buffers: Fast, Sub-pixel Accurate Hard Shadows


가장 처음에는 μQuad 라는 것을 생성한다. μQuadTangent-Space 를 기준으로 설정하며 각 픽셀별로 생성한다. 위 그림에서는 중간 그림에 구 위에있는 평면을 뜻한다. 그 다음 가리는 Geometry 의 폴리곤들의 각각의 Edge 를 사용하여 Shadow Plane 을 생성한다. 마지막으로 만들어진 Shadow PlaneμQuadProjection 한다. 이때 가지고 있던 LUT 를 통해 가려짐을 계산한다. 그리고 다른 Edge 들도 계속해서 누적시킨다.

간단하게 frustum-Triangle Test 의 단계에 대해 설명해보았다. 이제 각각의 과정 : μQuad Construction, Shadow Plane Construction, Visibility Computation 에 대해 조금 더 자세히 써보겠다.

μQuad 의 생성은 GeometryTangent-Space 를 기준으로 계산되는 것 빼고는 특이한 점이 없다. 하지만 생성되는 시기에 대해선 조금 특별한게 있다. 가시성을 계산할 때 생성할 수도 있지만 G-Buffer 를 생성할 때 미리 계산하는 것이 더 효율적이라고 한다.

가시성을 계산할 때 ray-triangle intersection 을 계산하기 보다는 앖에서 언급한 각각의 폴리곤의 Shadow Volume 을 각 점마다 계산한다고 한다. Shadow Volume 은 그림자를 생성하는 Occluder Triangle 을 기준으로 각각의 Edge 에서 뻗어나오는 직사각형 면으로 구성된다. 아래 그림을 보면 쉽게 이해할 수 있다. 그리고 μQuad 에서 샘플링한 각각의 점들을 기준으로 가시성을 계산한다면 한다면, 4번의 내적으로 가시성을 테스트할 수 있다는 것을 의미한다.


출처 : NVidia : Advanced Geometrically Correct Shadows for Modern Game Engines


또한 그림자를 멀티샘플링 하기 위해서는 여기서 언급한 Shadow VolumeShadow Plane 들을 이용한다고 한다.

마지막으로 Visibility Computation 이 남아있다. 이 부분의 대략적인 것은 위에서 언급했다. 자세한 계산방식을 말해보겠다. 위에서 언급한 μQuadShadow Plane 사용해서 해당 폴리곤들의 데이터 누적을 위해서는 μQuad 에서 이산화된 binary 샘플링이 필요하다. 논문의 저자는 32 번의 Halton sampling2 을 사용했다고 한다.

가시성을 계산하기 위해서는 Shadow PlaneμQuadProjection 해줘야 한다. 그러면 μQuad 는 최대 3개의 line 을 얻게 된다. 논문에서는 이 lineμQuad 를 기준으로 극좌표계3 데이터로 저장한다고 한다. 반지름과 각도가 5bit 크기로 저장된다. 해당 10bit 데이터를 사용하여 미리 계산된 테이블에서 32개의 이진 가시성 샘플을 가져온다. 결과와 bit 단위의 and 연산을 통해 μQuad 의 가시성을 계산할 수 있다고 한다.

해당 기법을 고안한 사람은 두가지의 아이디어 : Irregular Z-Bufferfrustum-Triangle Test 를 통해 Sub-pixel Hard Shadow 의 이론을 만들었다. 하지만 이 아이디어들과 구현을 위한 노력의 차이는 꽤 큰듯하다. 논문을 보면 아이디어에 대한 텍스트보다 최적화를 위한 텍스트가 2배가 될정도로 많다. 다음 글에서는 논문에서 나온 전체 과정과 디테일한 구현 사항에 대해 적어보겠다.

참조

  1. 일반적으로 오브젝트를 그리는 것과 다른 Conservative Rasterization 을 해주는 이유는 일반적인 Rasterization 은 픽셀의 반이상을 차지해야 해당 픽셀을 처리해준다. 하지만 정확한 Visibility 를 계산하기 위해서는 폴리곤이 해당되는 모든 픽셀들을 처리해주어야 한다. Conservative Rasterization 은 앞에서 말한바와 같이 모든 부분을 픽셀로 처리한다. Conservative Rasterization 에 대한 자세한 정보는 NVidia : Don’t be conservative with Conservative Rasterization 에서 확인할 수 있다. 

  2. 몬테카를로 시뮬레이션과 같은 방식의 점을 생성하는 방식이다. 쉽게 말하면 랜덤하게 생성하는 것이라고 생각하면 된다. 자세한 정보는 Wikipedia : Halton sequence 에서 확인할 수 있다. 

  3. 여기서는 각도와 반지름(거리)를 사용하여 나나탠다. 극좌표계에 대한 자세한 정보는 위키피디아 : 극좌표계 에서 확인할 수 있다.