광원 연산 최적화


Tiled Shading & Forward+

출처

Shader Model 5와 DirectX 11의 도입과 함께 compute shader가 등장했습니다. 이는 GPU를 범용 컴퓨팅 도구로 사용할 수 있게 열어주었으며, 병렬 함수 프로그래밍을 위한 스레드 및 메모리 공유 기능을 제공했습니다. Compute shader는 처음 접하는 경우 어려울 수 있는 주제입니다. 하지만 Part 1에서는 구현 세부사항을 다루지 않았으므로(Part 2에서 다룰 예정입니다!) compute shader에 대한 친숙도가 앞으로 나아가는 데 매우 중요하지는 않을 것입니다. 그러나 이에 대해 배우고 싶다면, 제가 도와드릴 수 있습니다! 시작하는 데 도움이 되는 학습 자료 목록이 있습니다.

Compute shader는 forward와 deferred 시스템 모두에 대해 효율적으로 조명을 컬링하기 위해 Tile-based shading에서 많이 사용됩니다. 아이디어는 화면을 타일 세트로 세분화하고 타일 내에 포함된 지오메트리에 영향을 미치는 조명을 추적하는 버퍼를 구축하는 것입니다. 그런 다음 셰이딩 프로세스 중에 주어진 타일에 기록된 조명만 평가합니다. 이는 평가해야 하는 조명의 수를 크게 줄입니다. 아래 애니메이션과 의사코드가 이 프로세스를 보여줍니다:

//버퍼:
Buffer display
Buffer GBuffer
Buffer tileArray

//셰이더:
Shader manyLightShader
Shader writeShadingAttributes
CompShader lightInTile

//가시성 & 머티리얼
for mesh in scene
    if mesh.depth < GBuffer.depth
        GBuffer = writeShadingAttributes(mesh)

//라이트 컬링
for tile in tileArray
    for light in scene
        if lightInTile(tile, light)
            tile += light

//셰이딩
display = manyLightShader(GBuffer, tileArray)

시작점은 이전의 single-pass deferred 알고리즘입니다. 이전에는 각 픽셀을 수신자로부터 얼마나 멀리 떨어져 있든 관계없이 각 광원의 개별 기여도를 합산하여 셰이딩했습니다. 이것은 기술적으로 말해서 물리적으로 올바른 일입니다. 빛은 거의 무한한 영향 범위를 가지고 있기 때문입니다. 결국 우리는 320억 광년 이상 떨어진 은하로부터 빛을 받고 있습니다. 그러나 빛의 감쇠(light attenuation) 때문에 셰이딩에 기여하는 에너지의 양은 거리의 함수로 감소합니다. 반지름 과 강도를 가진 구형 광원이 주어졌을 때, 거리에서의 조명도는 다음과 같습니다:

이를 통해 조명의 영향 범위에 인위적인 제한을 적용할 수 있습니다. 충분한 거리가 있으면 모든 광원이 너무 많이 감쇠되어 셰이딩에 대한 기여도가 사실상 감지할 수 없게 됩니다. 우리는 조명 기여가 없다고 간주하는 임계값을 설정하여 이를 나타냅니다. 이렇게 하면 광원에 구체적인 영향 볼륨이 생기며, 이는 각 광원 유형마다 다른 형태로 표현됩니다. 포인트 라이트의 경우 이 볼륨은 구의 형태를 취하고, 스포트라이트는 원뿔, 디렉셔널 라이트는 전체 화면 쿼드를 사용합니다.

위 애니메이션에서 영향 볼륨을 색상이 있는 와이어프레임 메시를 사용하여 나타냅니다. 이러한 메시를 사용하면 조명 볼륨과 타일 사이에 충돌 감지와 유사한 작업을 수행할 수 있습니다. 조명 “메시”의 일부가 타일 내에 포함되어 있으면 해당 조명을 타일별 조명 목록에 추가합니다. 장면의 모든 타일과 모든 조명에 대해 이 검사를 수행합니다. 마지막으로 타일별 조명 목록을 입력으로 사용하는 하나의 셰이더 패스로 전체 장면을 셰이딩합니다.

라이트 컬링 단계는 렌더링 파이프라인의 다른 부분에 의존하지 않으므로 deferred 또는 single-pass forward 렌더러를 사용하여 tiled shading을 구현할 수 있습니다. Tiled forward shading은 때때로 Forward+라고도 하며, 구현 세부사항은 동일합니다.

개념적으로 이것이 tiled shading의 핵심 아이디어를 이해하는 데 필요한 전부입니다. 하지만 우리가 넘어간 구현 세부사항 중 일부에 대해 궁금할 수 있습니다. 예를 들어, 타일의 최적 크기를 어떻게 결정할까요? 가능한 크기 범위는 픽셀당 하나의 타일에서 화면당 하나의 타일까지입니다. 이전 솔루션을 매우 비효율적인 단일 타일 시스템으로 생각할 수 있습니다. 그러나 스펙트럼의 다른 끝도 더 나을 것이 없습니다. 픽셀 크기의 타일은 저장하기 위해 큰 버퍼가 필요하고 라이트 컬링 단계를 상당히 느리게 만들 것입니다. 뿐만 아니라, 셰이딩 시에도 도움이 되지 않을 것입니다. GPU는 그룹으로 여러 스레드를 실행하며, 이는 지금은 단순히 픽셀과 동의어로 생각할 수 있습니다. 그룹의 각 스레드/픽셀이 서로 다른 조명을 가지고 있다면 서로 다른 코드를 실행하려고 할 것입니다.

이런 일이 발생하면 코드가 낮은 일관성(low coherency)을 가지며 고도로 분산(highly divergent)되어 있다고 말합니다. 고도로 분산된 코드를 실행하는 것은 GPU가 설계된 목적이 아니며 상당한 성능 저하를 초래할 것입니다. 이 난국에서 벗어나게 해주는 깨달음은 조명이 한 번에 많은 픽셀에 영향을 미치는 경향이 있으며 이러한 픽셀들이 공간적으로 가까운 곳에 함께 있는 경향이 있다는 것입니다. 화면을 타일링함으로써 이 속성을 활용하고 샘플을 함께 그룹화하여 동일한 코드를 실행하도록 합니다. 위에서 논의한 것처럼, GPU는 스레드/픽셀이 일관성이 있고 정확히 동일한 코드를 실행할 때 매우 행복합니다. 속담처럼, 행복한 GPU, 행복한 당신입니다.

따라서 최적의 타일 크기를 달성하려면 실행이 일관성 있도록 타일을 크게 만드는 것과 불필요한 계산이 적게 수행되도록 작게 유지하는 것 사이에서 균형을 찾아야 합니다. 그럼에도 불구하고 타일 크기는 일반적으로 GPU가 관리할 수 있는 가장 큰 스레드 그룹 픽셀로 설정되며, 이는 타일당 많은 불필요한 계산을 수행한다는 것을 의미하더라도 그렇습니다. 더 작은 타일을 포함하는 대안 솔루션은 더 많은 대역폭 비용이 들고, 앞서 살펴본 것처럼 대역폭은 과도하게 사용하고 싶지 않은 귀중한 리소스입니다. 이러한 이유로 우리는 대역폭을 절약하기 위해 컴퓨팅 성능과 트레이드오프를 받아들입니다.

그러나 위에서 만든 가정에서 비롯된 tiled shading의 상당히 근본적인 문제가 있습니다. 이 문제를 보여주는 예를 살펴보겠습니다:

위 애니메이션에서 익숙한 정면 시점과 가상 카메라가 캡처한 전체 공간 영역을 보여주는 오버헤드 샷, 두 가지 관점에서 같은 장면을 보고 있습니다. 뷰 프러스텀(view frustum)이라고도 하는 이 영역에서 타일은 더 이상 2D 쿼드로 나타나지 않고 프러스텀의 얇고 길쭉한 3D 슬라이스로 나타납니다. 곧 보게 되겠지만, 이것은 타일이 항상 공간적으로 가까이 있는 픽셀을 포함한다는 이전 가정을 무효화합니다. 화면에서는 서로 옆에 나타날 수 있지만 실제로는 깊이 불연속성(depth discontinuity)으로 분리되어 있을 수 있습니다. 간단히 말해서, 깊이 불연속성은 일반적으로 한 객체와 다른 객체의 경계에서 발견되는 카메라로부터 측정한 거리의 갑작스러운 변화입니다.

타일을 교차하는 메시나 조명 사이에 앞-뒤 관계가 있을 때마다 “공간적으로 가까운 가정”은 더 이상 성립하지 않습니다. 위 애니메이션에서 이것은 중앙의 큰 매끄러운 구체에서 볼 수 있습니다. 이미지의 중간 배경에 있는 노란색 조명에 의해서만 조명됩니다. 그러나 구체를 셰이딩하는 대부분의 타일에는 노란색 조명 이상의 것이 포함되어 있습니다. 사실, 그 중 일부는 장면의 모든 조명을 포함하고 있습니다! 이것이 이전 상황만큼 나쁘지는 않지만, 우리는 여전히 더 잘할 수 있다는 것을 알고 있습니다.

일반적인 최적화는 각 타일에서 가시 메시의 최소 및 최대 깊이를 찾고 이 범위에 맞게 타일의 프러스텀을 재정의하는 것입니다. 위의 예에서 이것은 중앙 타일에서 많은 불필요한 조명을 제거하는 훌륭한 최적화일 것입니다. 그러나 이 최적화를 수행하려면 깊이 버퍼를 읽기 전에 채워야 하므로 Z Pre-pass가 필요합니다. 이미 논의했듯이, 렌더링 파이프라인에서 이것이 실행 가능하지 않을 수 있는 많은 이유가 있습니다.

또한 이 솔루션은 타일 내 깊이 불연속성의 주요 문제를 해결하는 데 거의 도움이 되지 않습니다. 최소값과 최대값은 여전히 타일 내 메시의 개별 깊이에 의해 결정되기 때문입니다. 기본 문제는 훨씬 더 근본적입니다. 타일 셰이딩은 본질적으로 2D 개념이며 우리가 표현하려는 엔티티는 3D입니다. 이것이 더 실용적인 용어로 의미하는 것은 조명 할당이 화면 공간 조명 밀도를 기반으로 하기 때문에 뷰에 매우 강하게 의존한다는 것입니다. 동일한 장면 내의 다른 시점이 주어지면 셰이딩 시간이 크게 달라질 수 있으며 실시간 애플리케이션의 경우 예측할 수 없는 셰이딩은 매우 바람직하지 않습니다. 따라서 다음 섹션에서는 이 문제를 해결하고 tiled shading의 기본 개념을 개선하는 방법에 대해 논의할 것입니다.