원문 : https://developer.nvidia.com/content/depth-precision-visualized

주의 : 번역이 개판이므로 이상하면 원문을 참조하세요.

주의 : 허락받고 번역한 것이 아니므로 언제든 내려갈 수 있습니다.


 

Depth Precision Visualized

 

By Nathan Reed, posted Jul 15 2015 at 03:54PM

Tags : Gameworks Expoert Developer, DX12, DX11

 

 

깊이 해상도( Depth Precision, 깊이 정밀도 ) 는 모든 그래픽 프로그래머가 이미 겪거나 나중에 겪게 되는 똥꼬의 고통( 골칫거리, pain in the ass )입니다. 많은 기사들과 논문들이 이 주제에 대해 다뤘으며, 다양한 깊이 버퍼 포맷과 설정들을 여러 게임들, 엔진들, 그리고 장치들에서 찾아 볼 수 있습니다.

 

그것이 원근 투영( perspective projection )과 상호작용하는 방식 때문에 GPU 하드웨어 깊이 매핑( mapping )은 다소 많이 알려져 있지 않으며, 그 공식에 대해 공부해도 즉각적으로 명확해지지 않을 것입니다. 그것이 동작하는 방식에 대한 직관을 얻기 위해서는 약간의 그림을 그려 보는 것이 도움이 됩니다.

 

그림 1.

 

이 기사는 세 개의 주요 파트로 나뉩니다. 첫 번째 파트에서는 비선형 깊이 매핑에 대한 약간의 동기( motivation )을 제공하려고 합니다. 두 번째 파트에서는 비선형 매핑이 서로 다른 상황에서 동작하는 방식을 이해할 수 있도록 돕는 몇 개의 다이어그래을 제공할 것입니다. 세 번째 파트에서는 Paul Upchurch 와 Mathieu Desbrun [2012 ] 에 의해 제시된 Tightening the Precision of Perspective Rendering 의 주요 결과에 대한 논의 및 재현( reproduction )을 제공합니다. 앞의 방법( 링크 )은 깊이 정밀도 상에서 실수( floating point ) 반올림( roundoff ) 에러의 효과와 관련이 있습니다.

 

 

 왜 1/z 인가

 

 

GPU 하드웨어 깊이 버퍼는 일반적으로 카메라 앞의 놓인 오브젝트에 대한 거리를 선형적으로 표현하지 않습니다. 이는 사람들이 깊이 버퍼를 처음 접했을 때 기대하는 것과는 반대입니다. 그 대신, 깊이 버퍼는 월드 공간 깊이에 대한 역( reciprocal of world-space depth )에 비례하는 값을 저장합니다. 이러한 관례가 생긴 동기에 대해서 간단하게 언급하고자 합니다.

 

이 기사에서, 필자는 깊이 버퍼에 ( [0, 1] 로 저장된 ) 값을 표현하기 위해 d 를 사용할 것입니다. 그리고 뷰 축을 따라 측정된 거리인 월드 공간 깊이를 표현하기 위해서 z 를 사용할 것입니다. 월드 공간의 단위( unit )는 미터같은 단위입니다. 일반적으로 그것들의 관계는 다음 같은 형태입니다.

 

그림 2.

 

여기에서 a, b 는 ( 뷰 프러스텀의 ) near plane 과 far plane 의 설정입니다. 다시 말해, d 는 항상 1/z 의 선형 리매핑( remapping )입니다.

 

그것을 살펴 보면, 여러분은 를 취한다는 것은 여러분이 원하는 z 에 대한 어떤 함수가 될 것이라 생각할 수 있습니다. 그러면 왜 이런 선택을 하는 것일까요? 두 가지 주요 이유가 있습니다.

 

먼저, 1/z 은 원근 투영의 구조와 자연스럽게 어울립니다. 이는 거의 대부분의 범용 변환( transformation ) 클래스들이 직선( straight line )을 보존할 수 있도록 해 줍니다 -- 이는 하드웨어 래스터화( rasterization )를 유용하게 만들어 줍니다. 왜냐하면 삼각형의 곧은 엣지( straight edge )가 스크린 공간에서도 여전히 곧게 유지되기 때문입니다. 우리는 원근 나눗셈의 이점을 취함으로써 1/z 에 대한 선형 리매핑을 생성할 수 있습니다. 그 원근 나눗셈은 하드웨어가 미리 수행해 줍니다:

 

그림 3.

 

물론 이 접근법의 진정한 힘은 그 투영 행렬이 다른 행렬들과 곱해질 수 있다는 점이며, 그것은 여러분으로 하여금 여러 변환 단계들을 하나로 결합할 수 있도록 해 줍니다.

 

두 번째 이유는 Emil Persson 이 언급했듯이 1/z 가 스크린 공간에서 선형이라는 점입니다. 그러므로 래스터화를 수행하는 동안 삼각형에 대해 d 를 보간하는 것이 쉽습니다. 그리고 계층적 Z 버퍼, early Z-culling, 깊이 버퍼 압축과 같은 것들을 수행하는 것이 더욱 쉬워집니다.

 

 

 깊이 맵 그려보기

 

 

공식은 어렵습니다; 그림을 몇 개 확인해 보죠.

 

그림 4.

 

이 그래프들은 읽는 방법은 오른쪽에서 왼쪽으로, 그리고 나서 위에서 아래로입니다. 왼쪽 축에 표시된 d 에서 시작합시다. d1/z 에 대한 임의의 선형 리매핑일 수 있기 때문에, 우리는 이 축에서 원하는 곳에 0 과 1 을 배치할 수 있습니다. 눈금( tick mark )들은 개별적인 깊이 버퍼 값들을 표시합니다. 설명을 위한 목적으로, 필자는 4 비트로 정규화된 정수 깊이 버퍼를 시뮬레이션하고 있습니다. 그래서 16 개의 고르게 매치된 눈금들이 존재합니다( 역주 : 24 = 16 ).

 

각 눈금에서 수평으로 갔을 때 1/z 커브와 만나는 곳에서 아래쪽 축으로 수선을 내려 봅시다. 이 축은 개별 값들이 월드 공간 깊이 범위로 나타나는 곳입니다.

 

위의 그래프는 "표준" 을 보여 줍니다. 이는 D3D 및 유사 API 들에서 사용되는 바닐라( vanilla ) 깊이 매핑입니다. 여러분은 1/z 커브가 near 평면과 가까운 곳의 값들을 모으고 far 평면과 가까운 값들을 흩트러 놓는다는 것을 즉각적으로 이해할 수 있을 겁니다.

 

또한 near 평면이 깊이 해상도에 있어서 엄청난 영향력을 미치는 이유도 쉽게 알 수 있습니다. Near 평면을 당기는 것은 d 범위를 1/z 커브의 점근선( asymptote )을 급등시키며( skyrocket ), 이는 값들이 더욱 한쪽으로 쏠리도록 분산( lopsided distribution of values )시키는 결과를 산출합니다:

 

그림 5.

 

이와 유사하게, 이러한 문맥에서 우리는 far 평면을 무한하게 밀어 내도 큰 영향을 주지 못하는 이유를 쉽게 알 수 있습니다. 그것은 단지 1/z = 0 으로 d 범위를 확장한다는 것을 의미할 뿐입니다:

 

그림 6.

 

실수 깊이 버퍼는 어떨까요? 다음 그래프는 3 개의 지수( exponent ) 비트와 3 개의 가수( mantissa ) 비트를 사용하는 실수 포맷을 시뮬레이션하는 것과 관련한 눈금들을 추가한 것입니다:

 

그림 7.

 

이제 [0, 1] 범위에 40 개의 눈금이 존재합니다 -- 앞의 16 개보다는 많지만, 대부분이 near 평면 근처에 불필요하게 몰려 있으며, 거기는 우리가 더 많은 정밀도를 필요로 하지 않는 곳입니다.

 

깊이 범위를 역전시키기 위해 요새 광범위하게 알려진 트릭은 d = 1 에 near 평면을 매핑하고 d = 0 에 far 평면을 매핑하는 것입니다 :

 

그림 8.

 

훨씬 낫군요! 이제 실수에 대한 quasi-logarithmic distribution 은 1/z 의 비선형성을 조금 없앱니다. 이는 우리에게 정수 깊이 버퍼와 비교했을 때 near 평면에서 유사한 정밀도를 제공하며, 원하는 곳에서 광범위하게 정밀도를 증가시킬 수 있게 해 줍니다. 이 정밀도는 여러분이 멀리 이동할 때마다 매우 천천히 악화됩니다.

 

역전된 Z 트릭은 아마도 여러 번 독립적으로 재창조되어 왔을 것입니다. 그러나 최소한 Eugene Lapidous 와 Guofang Jiao 에 의해 작성된 ( 안타깝게도 지금은 접근 가능한 무료 링크가 없는 ) GIGGRAPH '99 paper 까지 돌아갈 수 있습니다. 그것은 더욱 최근에 Matt PettineoBrano Kemen 의 블로그 포스트, 그리고 SIGGRAPH 2012 talk 에서 Emil Persson 의 Creating Vast Game Worlds 에서 다시 대중화되었습니다.

 

이전의 모든 다이어그램은 투영 후의 깊이 범위를 D3D 관례처럼 [0, 1] 이라 가정합니다. OpenGL 에서는 어떨까요?

 

그림 9.

 

 OpenGL 은 기본적으로 투영 후의 깊이 범위를 [-1, 1] 이라 가정합니다. 이는 정수 포맷에서는 별 차이를 만들지 않지만, 실수 포맷에서는 불필요하게 중간에 대부분의 정밀도가 몰립니다( 나중에 저장을 위해 깊이 버퍼에서 [0, 1] 로 매핑되기는 하지만, 그것은 도움이 되지 않습니다. 왜냐하면 [-1, 1] 로의 초기 매핑이 이미 그 범위의 뒤쪽 절반 정도를 날려먹었기 때문입니다 ). 그리고 대칭적인 관점에서 볼 때, 역전된 Z 트릭은 여기에서 아무것도 할 수 없을 것입니다.

 

운좋게도 데스크탑 OpenGL 에서는 광범위하게 지원되는 ARB_clip_control 을 사용해서 이 문제를 해결할 수 있습니다( 이제는 glClipControl 이라는 것을 통해서 OpenGL 4.5 의 코어에서 지원됩니다 ). 불운하게도, GL ES 에서는 못합니다.

 

 

 

 반올림( roundoff ) 에러의 효과

 

 

1/z 매핑과 실수 깊이 버퍼를 사용하는 방식과 정수 깊이 버퍼를 사용하는 방식을 비교하는 것은 정밀도 스토리의 큰 부분이지 전부는 아닙니다. 심지어 여러분이 렌더링하려고 시도하는 씬을 표현하는데 충분한 깊이 정밀도를 확보하고 있다고 하더라도, 정점 변환 과정에서 정밀도 에러가 발생하기 쉽습니다.

 

이전에 언급했듯이, Upchurch 와 Desbrun 은 이에 대해 연구했고, 반올림 에러를 최소화하기 위한 두 개의 주요한 권고사항을 제시했습니다.

 

1. 무한 far 평면을 사용하십시오.

2. 투영 행렬을 다른 행렬과 분리하십시오. 그리고 정점 쉐이더에서 그것들을 뷰 행렬과 합치기보다는 개별적인 연산을 적용하십시오.

 

Upchurch and Desbrun came up with these recommendations through an analytical techunique, based on treating roundoff errors as small random perturbations introduced at each arithmetic operation, and keeping track of them to first order through the transform process. 필자는 그 결과를 직접적인 시뮬레이션을 통해 확인해 보기로 했습니다.

 

필자의 소스 코드는 여기에 있습니다 -- Python 3.4 with numpy. 이는 일련의 랜덤 점을 생성하고, 점들을 깊이에 의해 정렬하고, near 및 far 평면 사이에 점들을 선형적으로 혹은 대수적으로( logarithmically ) 배치합니다. 그리고 나서 그 점들을 뷰 및 투영 행렬들과 원근 나눗셈을 통해서 넘기는데, 32 비트 실수 정밀도 처리량( throughput )을 사용합니다. 그리고 선택적으로 최종 결과를 24 비트 정수로 양자화( quantize )합니다. 마지막으로 그것을 여러 번 실행해 인접한 두 개의 점들이, 같은 깊이 값으로 매핑되거나 실질적으로 순서가 바뀜으로 인해, 몇 번이나 구분불가능해지는지를 셈합니다. 다시 말해 그것은 서로 다른 시나리오 하에서 깊이 비교 에러가 발생하는 비율을 측정합니다 -- 우리는 이를 Z 파이팅과 같은 이슈라고 봅니다.

 

여기에서 near = 0.1 로 far = 10K 인 선형적으로 배치된 깊이들에 대해서 획득된 결과를 보여 줍니다( 필자는 logarithmic 깊이 배치를 시도해 봤고 다른 near/far 비율들도 배치해 봤습니다. 세부적인 수치들이 다양해져도 일반적인 경향은 같았습니다 ).

 

이 표에서 "indist" 는 indistinguishable 을 의미합니다( 두 개의 인접한 깊이가 최종적으로 같은 깊이 값으로 매핑된 것입니다 ). 그리고 "swap" 은 두 개의 인접한 깊이의 순서가 바뀌는 것을 의미합니다.

 

그림 10.

 

이를 그래프화하지 않은 점은 죄송합니다. 그러나 너무 많은 차원들이 존재해서 그래프로 만들기 힘들었습니다! 어떤 경우라도, 수치들을 보면, 몇 가지 일반화가 가능합니다.

 

    • 대부분의 설정에서 실수냐 정수냐 하는 것은 의미가 없습니다. 수치 에러는 양자화 에러에서 발생합니다. 부분적으로 이는 ( float32 는 23 비트의 가수를 가지기 때문에 ) float32 와 int24 가 [0.5, 1] 에서 거의 유사한 크기여서 실제적으로는 대부분의 깊이 영역에서 추가적인 양자화 에러는 거의 존재하지 않기 때문에 발생합니다.
    • 대부분의 경우에, ( Upchurch 와 Desbrun 의 제안대로 ) 뷰 및 투영 행렬들을 분리하는 것이 좀 더 나은 결과를 산출합니다. 그것은 전체적인 에러율을 낮춰주지는 않지만, it does seem to turn swaps into indistinguishables, which is a step in the right direction.
    • 무한한 far 평면은 에러율에서 극소적인 차이를 보여줄 뿐입니다. upchurch 와 Desbrun 은 절대적인 수치 에러가 25% 감소한다고 했지만, 비교 에러의 비율을 감소시킨다고 해석하기는 어려워 보입니다.

 

그런데 위에서 지적한 것들은 실용적인 관점에서는 별로 없습니다. 왜냐하면 여기에서 중요한 실제 결과는 다음과 같기 때문입니다: 역전된 Z 매핑은 기본적으로 환상적입니다. 확인해 봅시다:

 

    • 실수 깊이 버퍼를 사용하는 역전된 Z 는 이 테스트에서 0 의 에러율을 제공합니다. 물론 여러분은 입력 깊이 값들의 배치를 좀 더 타이트하게 만들어서 에러를 생성할 수는 있겠죠. 그래도 여전히 역전된 Z 는 다른 옵션들보다는 더욱 정확한 결과들을 산출할 것입니다.
    • 정수 깊이 버퍼를 사용하는 역전된 Z 는 다른 정수 옵션들과 비교하면 좋은 결과를 산출합니다.
    • 역전된 Z 는 미리 결합된 뷰/투영 행렬을 사용하는 것과 분리된 뷰/투영행렬을 사용하는 것에서의 차이를 제거해 버립니다. 그리고 무한한 far 평면을 사용하는 것과 유한한 far 평면을 사용하는 것의 차이도 제거합니다. 다시 말해 역전된 Z 값을 사용하면, 정밀도에 영향을 주지 않으면서도, 투영 행렬을 다른 행렬들과 결합시킬 수 있으며, 원하는 far 평면을 지정할 수 있다는 이야기입니다.

 

필자는 여기에서의 결론은 명확하다고 생각합니다. 어떤 원근 투영 상황이라고 할지라도, 실수 깊이 버퍼를 역전된 Z 와 함께 사용하십시오! 그리고 여러분이 실수 깊이 버퍼를 사용하지 못하는 상황이라고 해도 여전히 역전된 Z 버퍼를 사용해야 합니다. 이는 모든 정밀도 문제에 대한 만병통치약은 아닙니다. 여러분이 극단적인 깊이 범위를 포함하는 오픈 월드 환경을 만들고 있다면 더욱 그러합니다. 그렇지만 그것은 시작일 뿐입니다.

 

Nathan 은 Graphics Programmer 이며, 현재 NVIDIA 의 DevTech 소프트웨어 팀에서 일하고 있습니다. 여러분은 그의 블로그를 여기에서 구독하실 수 있습니다.

+ Recent posts