depth-precision-visualized
원문을 보시는 것을 추천드립니다.
깊이 정밀도 시각화
깊이 정밀도는 모든 그래픽스 프로그래머가 조만간 고민해 봐야 하는 골칫거리다.
많은 아티클과 논문에서 이미 이 주제가 다루어졌고, 서로 다른 게임들, 엔진들,
그리고 디바이스들에서 다양한 깊이버퍼 포멧과 설정을 볼 수 있다.
원근투영과의 상호작용하는 방식 때문에 GPU 하드웨어 깊이 매핑은 다소 난해하며
방정식을 연구한다고 해서 바로 상황이 명확해 지지는 않는다.
어떻게 동작하는지 알기 위해서는 몇장의 그림을 그려보는게 좋다.
이 글은 3개의 메인 파트가 있다.
첫번째 파트에서는, 비선형(nonlinear) 깊이 매핑을 하는 이유에 대해 다룬다.
두번째 파트에서는, 서로 다른 상황에서 비선형 깊이 매핑이 어떻게 수행되는지 이해를 돕는
직관적이며, 시각적인 몇개의 도표를 제공한다.
세번째 파트에서는 Paul Upchurch와 Mathieu Desrun이 2012년도에 제시한 "원근 렌더링의 정밀도 강화"의
주요 결과인 부동 소수점(floating-point) 반올림 오차가 깊이 정밀도에 미치는 영향에 관한 대한 및 재연을 다룬다.
왜 1/z 를 하는걸까?
GPU 하드웨어 깊이 버퍼는 일반적으로 카메라에서 오브젝트까지의 거리를 선형 방식으로 저장하지 않으며,
이는 사람들이 일반적으로 처음 이 부분을 접했을 때 생각하는 방식과 반대다.
그 대신, 깊이버퍼는 월드공간에서의 깊이값에 반비례하는 값을 저장한다.
왜 이렇게 하는지에 대해 간단히 알아보자.
이 글에서, 깊이 버퍼[0~1]에 저장되어 있는값을 d, 월드공간에서의 깊이(즉, 뷰 축에 따른 거리)를 z로
사용할 것이며 월드공간에서의 단위는 미터(meter) 같은 거다.
일반적으로 이들의 관계는 아래와 같은 모양이다.
위 식에서 a와 b는 근거리 평면(near plane)과 원거리평면(far plane)설정 값이다.
다시 말하자면, d는 항상 1/z의 선형으로 재설정(remapping) 된다.
표면상으로는, d를 얻는게 z에 대한 어떤 함수가 될 거라고 생각할 수 있다.
왜 이렇게 특이하게 만들었을까?
이에 대해서 두가지 중요한 이유가 있다.
첫번째로, 1/z 은 원근 투영의 구조에 자연스럽게 맞아떨어진다.
이는 거의 대부분의 변환 범용 클래스가 직선을 유지할수 있도록 보장해 주며,
하드웨어 레스터화가 편리하도록 만들고, 그 후에 화면공간에서 삼각형의 직선 테두리들(straighte edges)이
직선으로 남아있을 수 있도록 해 준다.
하드웨어가 이미 수행하고 있는 원근 나누기(perspective divide)를 이용하여 1/z의 선형 재설정을 만들 수 있다.
각주 : 위에서 1/z가 원근 투영의 구조에 자연스럽게 맞아 떨어진다는 말을 생각해 보자.
원근투영이란 3D를 2D에 투영할 때 원근감을 느낄 수 있도록 하는 투영방법을 말한다.
그렇다면 1/z는 무었일까? 원근감의 핵심은 가까운것은 크게, 먼 것은 작게 보이도록 하는 소실점을 사용한다.
이 소실점을 표현하기 위해서는 어떻게 해야 하는가?
눈(카메라)으로부터 떨어진 거리(z)로 각 값(x,y)을 나누어 주면 된다.
그렇게 되면 z 값이 클 수록(멀어질수록) 물체의 위치는 정점 부근으로 모이게 되며 삼각형을
예로 보면 삼각형을 이루는 세 점 또한 직선을 유지하게 된다.
위의 글은 아마 이 개념을 이야기 하는 것 같다.
동차 좌표계에서 z 값은 z/w이며 위 행렬에서 보면 zc = bz + a, wc = z 이다.
그러므로 zc/wc = (bz+a)/z 가 된다.
물론 이러한 접근 방식의 강점은 투영행렬이 다른 행렬들과 곱해질 수 있다는 점이며,
이는 곧 여러 변환 단계들을 하나로 조합할 수 있다는 뜻이다.
두번째 이유는 Emil Persson이 언급했듯이, 1/z값이 화면공간에서 선형이라는 점이다.
그래서 래스터화를 하는 동안 삼각형에 대해 d를 보간하는 것이 쉽다.
그리고 계층적 Z버퍼(Hierarchical Z-buffer), 초기 Z제외(early Z-culling),
깊이버퍼 압축(depth buffer compression)과 같은 작업들이 모두 수행하기 용이해 진다.
깊이맵(depth map) 그리기.
공식은 어려우니까 그림을 우선 보자.
위 그림은 왼쪽에서 오른쪽으로, 그리고 아래방향으로 보면 된다.
왼쪽 축에 표시되어져 있는 d에서 부터 보자.
d가 위에서 이야기 되었던 내용대로 1/z의 임의 선형 리매핑이 될 수 있으므로,
이 축에서 원하는 위치에 0과 1을 배치할 수 있다.
위 그림에서 눈금 표시는 각각의 깊이 버퍼값을 나타낸다.
설명을 위해, 4bit 정규화된 정수형 깊이버퍼를 시뮬레이션 해 보면, 위와 같이 16개의 균등한 간격의 눈금이 생긴다. (세로축)
세로축을 눈금에서 가로로 1/z 곡선을 연결해서 아래로 내려보면, 그 개별 값들이 월드공간 깊이 범위이다.
위의 그래프는 d3d 및 유사 API들에서 사용되고 있는 전형적인 깊이매핑의 "표준" 을 보여준다.
그림을 보면 1/z 그래프에서 가까운 평면(near plane)쪽에 값들이 몰려서 연결되어 있고 먼 평면(far plane)으로 갈수록
값이 분산되어 있다는 것을 알 수 있다.
또한 near plane이 왜 깊이 정밀도에 큰 영향을 끼치는지를 알 수 있다.
near plane을 당기면 d 범위가 1/z 곡선의 점근선 방향으로 급등하게 만들 수 있으며, 위 그림 보다
훨씬 더 near plane 쪽으로 값이 몰리게 된다.
이와 유사하게, far plane을 무한대로 미는 것이 왜 그다지 효과적이지 못한지 이 문맥에서 알 수 있다.
단지 d 범위를 1/z = 0에 이르기 까지 약간 확장하는 것이다.
floating-point(부동소수점) 깊이는 어떨까?
아래 그래프는 3개의 지수비트와 3개의 가수비트를 사용하는 부동소수점 형식으로 시뮬레이션 된 눈금을 추가한 것이다.
이제 [0,1] 사이에 40개의 값이 있으므로 이전16개 였을 때 보다 많지만, 그림에서 알 수 있듯이 대부분이
더이상의 정밀도가 필요없는 near plane 쪽으로 몰려있다.
현재 깊이 범위를 역전시키기 위해 널리 알려져 있는 트릭은, near plane에 d=1을 매핑하고
far plane에 d=0을 매핑하는 것이다.
위 그림의 결과물을 보면 정밀도의 분배가 이전 보다 훨씬 나아졌다.
이제 부동소수점의 준 로그 분포(quasi-logarithmic distribution)는 1/z 비선형화를 다소 줄여주며,
위에서 언급됐던 정수깊이 버퍼와 비교 했을 때 near plane에서 유사한 정밀도를 제공해 주며,
원하는 곳에 광범위하게 정밀도를 증가시킬 수 있게 해 준다.
위 그래프를 보면, 이 정밀도는 멀리 이동할 때 마다 아주 천천히 안좋아 진다.
각주 : float의 설계상 0에 가까운 값일 수록 정밀도가 높다.
그래서 0에 몰빵되서 저장하고 있던 값을 반전시키면 1에 몰빵된 값이 되는데
float이 0에 가까울수록 정밀도가 좋기에 위와 같이 선형적인 그래프가 된다.
아래 표는 각 영역에서 사용하는 float의 정밀도 간격값이다.
2의 n승 정밀도 간격
-24 : 0.000000059604644775390625
-14 : 0.00006103515625
-13 : 0.0001220703125
-12 : 0.000244140625
-11 : 0.00048828125
-10 : 0.0009765625
-9 : 0.001953125
-8 : 0.00390625
-7 : 0.0078125
-6 : 0.015625
-5 : 0.03125
-4 : 0.0625
-3 : 0.125
-2 : 0.25
-1 : 0.5
참고 문서 : float의 정밀도
이 reversed-Z(역전된 Z) 트릭은 아마도 몇번 독자적으로 재연구 되어졌겠지만,
최소한 Eugene Lapidous와 Guofang Jiao에 의해 작성된 SIGGRAPH '99paper 까지 거슬러 올라간다.
이 연구는 최근에 Matt Pettineo와 Brano Kemen의 블로그 포스트와, SIGGRAPH 2012에서 Emil Persson의
Creating Vast Game World 에서 다시 재조명 되었다.
이전의 모든 다이어그램은 투영 후의 깊이 범위를 D3D 방식대로 [0,1]이라 가정했는데, OpenGL이라면 어떨까?
OpenGL은 기본적으로 투영 후 깊이 범위를 [-1,1]로 가정한다.
정수 형식에서는 차이가 없지만 부동소수점을 사용하면 모든 정밀도가 중간에 쓸데없이 몰려있다.
이 값은 나중에 깊이버퍼에서 [0,1]로 매핑되어 저장되기는 하지만, 초기에 이미 [-1,1] 범위로 매핑되어
범위의 뒤쪽 정밀도를 모두 날려먹었기에 그다지 도움이 되지 않는다.
그리고 대칭으로 보면, reversed-Z 트릭은 여기서는 그다지 필요가 없어 보인다.
다행히도, 데스크탑 OpenGL에서는 ARB_clip_conrol 명령어를 사용하여 이 문제를 수정할 수 있다.
(이제는 OpenGL 4.5의 코에에서 glClipControl를 통해 지원한다.)
불행히도, GL ES에서는 방법이 없다.
반올림(roundoff) 오류의 영향
1/z 매핑과, float 버퍼 와 정수버퍼중 어느것을 선택할건지는 정밀도 부분에서 큰 부분을 차지하지만 전부는 아니다.
화면을 렌더하기에 충분한 깊이 정밀도라 할지라도,정점 변환 프로세스의 산술적 오류에 의해 정밀도 오류가 쉽게 발생한다.
앞에서 이야기 했듯이, Upchurch 와 Desbrun 은 이 부분에 대해 연구하였고,
반올림 에러를 줄이기 위한 두가지 중요 권장사항을 제시하였다.
1. 무한 far plane을 사용해라.
2. 투영행렬을 다른 행렬과 분리하고, 정점 셰이더에서 뷰행렬에 합치기 보다는 따로 적용시켜라.
Upchurch와 Desbrun은 각 산술연산마다 작은 무작위 변화를 추가함으로써 반올림 에러를 처리하고,
이를 변환과정을 통해 첫번째 순서로 추척한다.
직접 시뮬레이션을 통해 결과를 확인해 보기로 했다.
파이썬(Python) 3.4 numpy로 만든 소스는 이곳을 보면 된다.
이 소스는 near와 far planes 사이에서 무작위 점들을 선형적 또는
대수적으로 간격을 두면서 생성하여 depth(깊이)에 따라 정렬한다.
그리고 나서 이 점들을 32비트 부동 소수점 정밀도를 사용하여 뷰, 투영 행렬과 원근 나누기를 통해 넘기고
최종 결과를 24비트 정수로 양자화 한다.
마지막으로, 시퀀스를 따라 실행하면서 얼마나 여러번 인접해 있는 두 점들이(실제로는 다른 깊이값을 가진)
같은 깊이 값으로 매핑되어 구분하기 어려워지거나, 순서가 뒤바뀌는지를 센다.
다시 말하자면, Z Fighting 문제처럼 깊이 비교 에러의 발생 비율를 측정한다.
여기 아래 표에 near = 0.1, far =10k 선형적으로 배치된 깊이값들로 얻은 결과값을 보여준다.
(logarithmic depth 방식과 다른 near/far 비율로도 테스트 해 봤는데 세세한 값들이 다양해도 결과는 같은 경향을 보였다.
이 표에서, "indist"는 구분 불가(두 이웃한 깊이값이 최종적으로 같은 깊이 버퍼 값으로 매핑되는)를 의미하며,
"swap"은 두 이웃 깊이값이 순서가 바뀐것을 의미한다.
그래프로 만들지 않아서 미안하지만, 너무 많은 그래프 축을 그려야 해서 그래프로 하기는 힘들다.
어쨋든, 숫자들을 보면, 몇가지 명백한 결과를 알 수 있다.
■ 대부분의 환경에서 보면 float과 integer 깊이 버퍼간에는 차이가 없다.
산술오류는 양자화 오류로 이어진다.
이는 부분적으로 봤을 때, float32와 int24는 [0.5, 1]에서 ulp(Unit in Last Place)가 거의 같은 크기여서
(float32 는 23bit의 가수를 가지기 때문에), 실제로는 거의 모든 깊이 영역에서 추가적인 양자화 오류가 거의 없다.
■ 대부분의 경우, Upchurch와 Desbrun의 권고에 따라 뷰와 투영행렬을 분리하면 약간의 개선이 이루어 진다.
전체적으로 에러률을 낮춰주지는 않지만, 구분하기 힘든 부분에서 맞는 방향으로 바꿔주는 것 처럼 보인다.
■ 무한 far plane은 오류률 매우 작은 차이를 보여준다. Upchurch와 Desbrun은 수치적으로 에러가 25%감소한다고
했지만, 비교 에러률이 줄여준다고 보기에는 어렵다.
위의 언급한 세가지와는 실제적으로 거의 관련이 없지만, reversed-Z(역전된 Z) 매핑은 기본적으로 환상적이다.
왜 그런지 확인해 보자.
■ float 깊이 버퍼는 이 테스트에서 오류룰이 0%다.
물론 입력 깊이 값의 간격을 더 촘촘하게 해서 약간의 에러를 만들어 낼 수도 있다.
하지만 그럼에도 불구하고, float으로 reversed-Z를 사용하는것은 다른 방법들 보다 훨씬 정확하다.
■ integer 깊이버퍼로 reversed-Z를 사용하는것은 다른 integer 옵션보다 좋다.
■ reversed-Z는 미리 view projection 이 미리 합성된 것을 사용하는 대신 별개로 분리하면서 얻는 이점과,
유한 far 평면에서 무한 far 평면을 사용하면서 얻는 이점을 다 없애버릴 만큼 좋다.
다시 말해 reversed-Z를 사용하면, 정밀도에 영향을 주지 않으면서 투영 행렬을 다른 행렬들과 결합시킬 수 있고,
원하는 방식의 far 평면을 사용할 수 있다는 말이다.
이제 결론은 명백하다.
어떤 원근 투영상황에서도, reversed-Z 부동소수점 깊이 버퍼를 사용하는것이 가장 좋다!!
부동 소수점 깊이버퍼를 사용할 수 없다 하더라도 reversed-Z는 사용해야 한다.
모든 정밀도 에러 상황에서(특히 극단적인 깊이를 포함하는 개방형 월드 환경에서는 더욱) 만병통치약은 아니지만
reversed-Z는 그럼에도 불구하고 깊이 정밀도 문제를 해결하는 데 좋은 시작점이다.
Nathan은 그래픽스 프로그래머로, 현재 NVIDIA의 DevTech 소프트웨어 팀에서 일하고 있다.
그의 글을 더 읽어보길 원한다면 이곳을 참조해라.
---------------------------------------------------------------------------------------------------------------------------------
번역 내용 이외에 개인적인 궁금증.
1. 보통 rendertarget(결국은 텍스쳐)에 값을 쓸 때 0.0~1.0의 값을 쓰는데 integer format은 0~1로 값이 끊기지만
floating format은 -값도, 그리고 1 이상의 값도 넣을 수 있다.
그런데 대부분 사람들은, 그리도 대부분의 아티클이나 샘플 예제들은 0~1로 값을 제한한다.
이 이유는 무었일까? 그냥 float의 정밀도에 따른 신뢰도의 정도 때문일까?
Reference Link
- 원문 : Depth Precision Visualized
- Tightening the Precision of Perspective Rendering
- 원근투영
- 점근선
- Unity 5.5 graphics changes and improvements
- msdn, Depth Buffers (Direct3D 9)
- You Can Never Have a Custom Z Buffer Distribution andEarly Z at the Same Time
- gamasutra, Logarithmic Depth Buffer
- NVIDIA GPU Programming Guide
- Advanced DX9 Capabilites for ATI Randeon Cards
- Maximizing Depth Buffer Range and Precision
- 원문 번역 https://blog.naver.com/zmfltbsk2/221207477425
- 원문 번역 http://lifeisforu.tistory.com/365
'Study > Graphics ' 카테고리의 다른 글
texture format (0) | 2018.02.21 |
---|---|
shader에서 채널값이 모자를 때... (0) | 2018.02.13 |
디더링 (Dithering) (0) | 2018.01.26 |
Texture types (1) | 2017.02.05 |
rgbm (0) | 2016.12.29 |