Post Lists

2018년 7월 25일 수요일

Advanced OpenGL - Depth testing (1)

https://learnopengl.com/Advanced-OpenGL/Depth-testing

Depth testing
coordinate systems 튜토리얼에서 우리는 3D container를 렌더링 했고, 어떤 면 들이 다른 면들 뒤에 있는 동안 그 앞에서 렌더링하도록 하는 것을 방지하기 위해, depth buffer를 이용했었다. 이 튜토리얼에서, 우리는 이러한 depth values에 대해 좀 더 자세히 들어갈 것이다. depth value는 depth buffer(또는 z-buffer)가 저장하고 있는 것이다. 그리고 그것이 어떻게 한 fragment가 다른 fragments들 뒤에 있는지를 결정하도록 하는지에 대해서도 자세히 할 것이다.

depth-buffer는 color buffer처럼 (시각적인 결과인 모든 fragment colors들을저장하는 것) fragment당 정보를 저장하고, (보통) color buffer와 같은 너비와 높이를 갖는 버퍼이다. depth buffer는 자동적으로 windowing system에 의해 만들어지고, 그것의 depth 값을 16, 24 또는 32 비트 floats로 저장한다. 대부분의 시스템에서, 너는 24 비트의 정확도를 가진 depth buffer를 볼 것이다.

depth testing이 활성화 될 때, OpenGL은 depth buffer의 내용에 대해 한 fragment의 depth value를 test한다. OpenGL은 depth test를 수행하고, 만약 이 테스트를 통과한다면, 그 depth buffer는 새로운 depth value로 업데이트 된다. 만약 depth test가 실패한다면, 그 fragment는 버려진다.

Depth testing은 fragment shader가 작동한 후에 (그리고 우리가 다음 튜토리얼에서 다룰 stencil testing이 작동된 후에) screen space에서 처리된다. screen space 좌표는 직접적으로 OpenGL의 glViewport 함수에의해 정의되는 viewport와 연관이 있고, fragment shader에서 GLSL에 내장된 gl_FragCoord변수를 통해 접근될 수 있다. gl_FragCoord의 x와 y component는 fragment의 screen-space 좌표를 나타낸다. ( (0,0)이 왼쪽 하단 모서리이다.) 그 gl_FragCoord는 또한 fragment의 실제 depth value를 포함하는 z-component를포함한다. 이 z 값은 depth buffer의 content와 비교되는값이다.

  Green Box
  오늘날, 대부분의 GPU는 early depth testing이라고 불려지는 hardware feature를 지원한다. Early depth testing은 fragment shader가 작동하기 전에 depth test를 작동하도록 허용한다. 한 fragment가 보이지 않을 예정이여서 그것이 clear되는 곳 어디든, (그것이 다른 오브젝트들 뒤에 있어서) 우리는 매우 이르게 그 fragment를 버릴 수 있다.
Fragment shaders는 보통 꽤 비싸다. 그래서 우리가 그것들을 작동시키는 것을 피할 수 있는 곳 어디서든, 우리는 fragment shaders를 피해야만 한다. early depth testing에 대한 fragment shader의 제약은 너는 fragment의 depth value에 write를 해서는 안된다는 것이다. 만약 한 fragment shader가 그것의 depth value를 write하고자 한다면, early depth testing은 불가능하다; OpenGL은 사전에 depth value를 알아내지 못할 것이다.

Depth testing은 기본적으로 비활성화 되어있다. 그래서, depth tetsting을 활성화 하기 위해, 우리는 그것을 GL_DEPTH_TEST 옵션으로 활성화할 필요가 있다:

  glEnable(GL_DEPTH_TEST);

일단 활성화 된다면, OpenGL은 자동적으로 depth test를 통과한 fragment를 저장하고 통과하지 못한 fragment들은 버린다. 만약 너가 depth testing을 활성화 시킨다면, 너는 또한 GL_DEPTH_BUFFER_BIT를 사용하여 매 렌더링 반복전에 depth buffer를 clear해야만 한다. 그렇지 않으면, 너는 지난 render 반복의 쓰여진 depth 값에 막히게 된다:

  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

몇 가지 상상할수 있는 시나리오가 있다. 거기에서 너는 모든 fragments들에 대해 depth test를 실행하시를 원하고, 그것들을 그에 따라서 버리기를 원하지만, depth buffer를 업데이트하고 싶지않아한다. 기본적으로, 너는 read-only depth buffer를사용하고 있다. OpenGL은 그것의 depth mask를 GL_FALSE로 설정하여 depth buffer에 대해 writing 하는 것을 비활성화 할 수 있게 한다:

  glDepthMask(GL_FALSE);

이것은 오직 depth testing이 활성화 되었때에만 효과를 갖는다는 것에 유의해라.

Depth test function
OpenGL은 그것이 depth test를 위해 사용하는 비교 연산자를 수정할 수 있도록 한다. 이것은 OpenGL이 언제 fragments를 통과시키거나 버릴지와 언제 depth buffer를 업데이트 할지를 우리가 통제할 수 있도록 해준다. 우리는 비교연산자(또는 depth function)을 glDepthFunc로 설정할 수 있다:

  glDepthFunc(GL_LESS);

그 함수는 아래의 테이블에 기재된 몇 가지 비교 연산자를 받아 들인다:


  • GL_ALWAYS : depth test는 항상 통과한다.
  • GL_NEVER : depth test는 항상 통과하지 못한다.
  • GL_LESS : 만약 fragment의 depth value가 저장된 depth value보다 미만면 통과
  • GL_EQUAL : 만약 fragment의 depth value가 저장된 depth value와 같으면 통과
  • GL_LEQUAL : 만약  fragment의 depth value가  저장된 depth value이하라면 통과
  • GL_GREATER : 만약 fragment의 depth value가 저장된 depth value보다 초과면 통과
  • GL_NOTEQUAL : 만약 fragment의 depth value가 저장된 depth value와 같지 않다면 통과
  • GL_GEQUAL : 만약 fragment의 depth value가 저장된 depth value값 이상이면 통과


기본적으로 depth function GL_LESS가 사용되는데, 그것은 현재 depth buffer의 값보다 같거나 더 큰 모든 fragments들을 버린다.

depth function이 시작적 output에 대해 가지는 효과를 보여주자. 우리는 두 개의 텍스쳐입힌 큐브가 조명 없이 텍스쳐입힌 바닥위에 앉아 있는 기본적인 scene을 보여주는 새로운 코드 환경을 사용할 것ㅅ이다. 너는 소스코드를 여기에서 볼 수 있다.

소스코드 내에서 우리는depth function을 GL_ALWAYS로 바꾼다:

  glEnable(GL_DEPTH_TEST); glDepthFunc(GL_ALWAYS);

이것은 우리가 depth testing을 활성화 하지 않으면 얻는 결과를 재현한다. 그 depth test는 간단히 항상 통과된다. 그래서 마지막에 그려지는 fragments들이 이전에그려진 fragments들앞에서 렌더링 된다. 비록 그것들이 앞에 있어야 할지라도. 우리가 바닥 평면을 마지막으로 그리기 때문에, 그 바닥의 fragment가 container의 fragment각각에 대해 덮어 쓴다:



그것을 다시 GL_LESS로 설정하는 것은 우리에게 익숙한 scene의 종류를 준다:


Depth value precision
depth buffer는 0.0과 1.0 사이의 depth value를 포함한고, 그것은 그것의 내용물과 scene에 있는 모든 오브젝트들의 z 값을 비교한다. view space에 있는 이렇나 z-values는 projection frustum의 near과 far사이의 어떤 값이든 될 수 있다. 우리는 따라서 이러한 view-space z-values를 [0,1]의 범위로 바꿀 어떤 방법이 필요하다. 한 방법은 선형으로 그것들을 [0,1]의 범위로 바꾸는 것이다. 다음의 (선형의) 공식은 z-value를 0.0과 1.0사이의 depth value에 대한 값으로 바꾼다:



여기에서 near와 far는 visible frustum(see Coordinate Systems tutorial)을 설정하기 위해 projecction matrix에 제공하기 위해 사용했던 near와 far 값들이다. 그 방정식은 frustum내에서 depth value z를 취하고, 그것을 [0,1]의 범위로 변환한다. z-값과 그것에 대응되는 depth value사이의 관계는 다음의 그래프에서 보여진다:

  Green Box
  모드 방정식들이 오브젝트가 가까이 있을 때 0.0에 가까운 depth value를 주고, far plane에 가까이 있을 때 1.0에 가까운 값을 준다.

그러나 실제로, 이것과 같은 linear depth buffer는 거의 전혀 사용되지 않는다. 올바른 projection properties를 위해, 1/z에 비례하는 비선형 depth equation이 사용된다. 이것을 잠시 생각해보아라 : 우리는거리가 1인 매우 세부적인 오브젝트와 같은 정확도를 갖기 위해서 1000 단위인 depth value를 원하는가? linear equation은 이것을 고려하지 않는다.

non-linear function은 1/z에 비례하기 때문에,  예를들어, 1.0~2.0사이의 z-values는 1.0 ~ 0.5 사이의 depth value를만들어 낼 것이고, 이것은 한 float가 우리에게 제공하는 절반의 정확도이다. 그리고이것은 우리에게 작은 z 값에대해 엄청난 정확도를 준다. 50.0 ~ 100.0 사이의 Z 값은 float의 정확도에 2%만을 고려한다. 이것은 정확히 우리가 원하는 것이다. 또한 near와 far distances를 고려하는 그러한 방정식이 아래에 주어진다:



너가 이 방정식이 무엇을 하고 있는지 모른다고 걱정하지 말라. 기억해야할 중요한 것은 depth buffer에 있는 값들이 screen-space에서 선형이 아니라는것이다. (그것들은 projection matrix가 적용되기 전에 view-space에서는 선형이다) depth buffer에 있는 0.5의 값은 object의 z값이 frustum의 절반 정도에 있다는 것을 의미 하지 않는다; 그 정점의 z-value는 실제로 near plane에 거의 가깝다! 너는 z-value와 결과 depth buffer의 값 사이의 비선형 관계를 다음의 그래프에서 볼 수있다:

너도 볼 수 있듯이, depth values는 작은 z-values 값에 의해 크게 결정된다. 따라서 이것은 우리에게 가까이 있는 object에 대해 매우 많은 depth precision(깊이 정확도)를 주는 것이다. z-values를 변환하는 그 방정식은 (viewer의 관점으로부터) projection matrix내에서 삽입된다. 그래서 우리가 view에서 cliip으로 vertex좌표를 바꿀 때, 그리고 screen space로 바꿀 때, 비선형 방정식이 적용된다. 만약 너가 projection matrix가 실제로 세부적으로 무엇을 하는지에 대해 궁금하다면, 다음의 훌륭한 자료를 제안한다.

우리가 depth buffer를 시각화하려고 할 때, 이 비선형 방정식의 효과가 빠르게 명백해진다.

Visualizing the depth buffer
우리는 fragment shader에서 내장된 gl_FragCoord vector의 z-value가 그 특정 fragment의 depth value를 포함하는 것을안다. 만약 우리가 그 fragment의 이 depth value를 color로 otuput하려고 한다면, 우리는 scene에서 모든 fragments들의 depth values를 보일수 있다. 우리는 fragment의 depth value를 기반으로 한 color vector를 반환하여 이것을 할 수 있다:


1
2
3
4
void main()
{
    FragColor = vec4(vec3(gl_FragCoord.z), 1.0);
}

만약 같은 프로그램을다시 실행시킨다면, 너는 아마도 모든 것이 하얀색이라는 것을 볼 것이다. 이것은 우리의 depth values 모두가 1.0인 것처럼 보이게 만들것이다. 이것은 maximum depth value이다. 그래서 왜 그 depth values 어떤 것이든 0.0에 더 가까워지지 않고, 따라서 어두워 지지 않는 것이냐?

너는 이전 섹션으로부터 screen space에있는 depth values가 비선형이라는 것을 기억할지도 모른다. 예를들어, 그것들은 작은z-values에 대해 매우 높은 precision을 갖고, 높은 z-values에 낮은 precision을 갖는다. fragment의 depth value는거리에 따라급속하게 증가하고, 그래서 거의 모든 정점들은 1.0에 가까운 값들을 가진다. 만약 우리가 조심스럽게 오브젝트에 가까이 이동하려 한다면, 너는 결국에 그 컬러들이 어두워지는 것을 볼 지도 모른다. 이것은 그것들의 z값들이 작아지고 있는 것을 보여준다:



이것은 명백하게 depth value의 비선형성을 보여준다. 가까이에 있는 오브젝트들은 먼 거리에 있는 오브젝트들 보다 depth value에 더욱 큰 효과를 가진다. 오직 몇 인치만을 움직이는 것이 검정에서 완전히 하얀색이 되도록 색을 만들어 낸다.

그러나, 우리는 fragment의 비선형 depth value를 그것들의 linear sibling로 변환할 수 있다. 이것을 해내기 위해서, 우리는기본적으로 depth values에 대해 projection process를 reverse 할 필요가 있다. 이것은 우리가 처음에 [0,1]의 범위의 depth values를 [-1,1] 범위(clip space)의 normalized device coordinate로 다시 바꿔야 하는 것을 의미한다. 그러고 나서 우리는 projection matrix에서 처리되듯이  비선형 방정식(방정식 2)를 reverse하고, 이 역의 방정식을 최종 depth value에 적용한다. 그 결과는 그러고나서 linear depth value가 된다. 할만하지 않냐?

처음에 우리는depth value를 NDC로 바꾸길 원한다. 이것은 그렇게 어렵지 않다:

  float z = depth * 2.0 - 1.0;

우리는 그러고 나서, 결과 z 값을 받고, linear depth value를 얻기위해 inverse transformation을 적용한다:

  float linearDepth = (2.0 * near * far) / (far + near - z  (far - near));

이 방정식은 projection matrix로 부터 나오는데, depth values를 비선형화하기 위해 2번 방정식을 사용하고, near과 far사이의 depth values를반환한다. 이 수학이 가득한 자료는 흥미가 있는독자에게 엄청난세부사항으로 projection matrix를 설명한다; 그것은 또한 그방정식이 어디에서 왔는지를 보여준다.

scree-space에서 비선형 depth를 선형 depth value로 바꾸는 완전한 fragment shader는 다음과 같다:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#version 330 core

out vec4 FragColor;

float near = 0.1;
float far = 100.0;

float LinearizeDepth(float depth)
{
    float z = depth * 2.0 - 1.0; // back to NDC
    return (2.0 * near * far) / (far + near - z * (far - near));
}

void main()
{
    //divide by far for demonstration
    float depth = LinearizeDepth(gl_FragCoord.z) / far; 
    FragColor = vec4(vec3(depth), 1.0);
}

선형화된 depth values가 near과 far사이의 값이기 때문에, 그것의 값들은 대부분 1.0 이상일 것이고, 완전히 하얗게 보여질 것이다. main 함수에서 linear depth value를 far로 나누어서, 우리는 선형 depth value를 대충 [0,1]사이의 범위로 변환한다. 이 방식으로 우리는 fragments가 projection frustum의 far plane에 가까워질 수록 밝아지는 것을 점차적으로 볼 수 있다. 그리고 이것은 보여주기 목적으로 더 잘 적합하다.

만약 우리가 이제 프로그램을 작동시키면, 우리는 실제로 거리에 따라 선형인 depth values를 얻는다. scene 주변에서 움직여보고, depth values가 선형의 흐름으로 변하는 것을 보아라.


그 색들은 주로 검정색이다. 왜냐하면 depth values는 선형으로 0.1인 near plane부터 우리로부터 꽤 먼 100인 far plane사이의 범위이기 때문이다. 그 결과는 우리는 상대적으로 near plane에 가깝고, 따라서 낮은 (더 어두운) depth values를 얻는다.

Z-fighting
두 면 또는 삼각형이 서로에게 매우 가깝게 정렬해 있을때 흔한 visual artifact는 발생할지도 모른다. 그것은 depth buffer가 두 shapes 중 어떤 것이 다른 것 앞에 있는 지를 알아내기에 충분한 정확도를 가지지 못할 때 이다. 그 결과는 두 shapees가 지속적으로 이상한 glitch patterns을 야기시키는 순서로 switch하는 것처럼 보인다. 이것은 z-fighting이라고 불려진다. 왜냐하면 그것은 shapes들이 누가 위에 있는지에 대해 싸우고 있는 것처럼 보이기 때문이다.

우리가 이제까지 사용한 scene에서, z-fighting이 꽤 보일만한 장소들이 있다. 그 컨테이너들은 바닥이 위치해있는 정확히 같은 높이에 있었다. 이것은 그 컨테이너의 bottom 면이 floor plane과 동일 평면상에 있다는 것을 의미한다. 두 평면의 depth values는 같다. 그래서 결과 depth test는 어떤 것이 오른쪽인지를 알아낼 방법이 없다.

만약 너가 카메라를 컨테이너들 중에 하나로 움직인다면, 그 효과는 명백히 눈에 보인다.  그 컨테이너의 밑바닥 부분이 끊임 없이 container의 plane과 floor의 plane사이에서 지그재그패턴으로 바뀌고 있다.

Z-fighting은 depth buffers가 가지는 흔한 문제이고, 일반적으로 오브젝트들이 먼 거리에 있을 때 더 강해진다. (왜냐하면 depth buffer는 더 큰 z-values에 대해 더 낮은 정확도를 가지기 때문이다.) Z-fighting은 완전히 방지될 수 없지만, 너의 scene에서 z-fighting을 줄이거나 또는 완전히 방지하는데 도움이 되는 몇 가지 tricks들이 일반적으로 있다.

Prevent z-fighting
첫 번째이자 가장 중요한 트릭은 결코 오브젝트들을 그것들의 삼각형들이 너무 가깝게 겹치도록 서로에게 너무 가깝게 배치하지 않는 것이다. 사용자가 눈치채기 거의힘든 두 객체 사이의 작은 offset을만들어, 너는 두 오브젝트들 사이의 z-fighting을 완전히 없앨 것이다. containers와 plane의 경우에, 우리는 쉽게 컨테이너를 양의 y방향으로 옮길 수 있다. 그 컨테이너 위치의 작은 변화는 아마도 전혀 눈치챌만하지 않을 것이고, 완전히 z-fighting을 줄일것이다. 그러나, 이것은 그 오브젝트 각각에 대해 수동 개입을 요구하고, 어떠한 오브젝트도 scene에서 z-fighting을 만들어 내지 않도록 하기 위해 철저한 검증을 요구한다.
(나의 경우에, 같은 position component에 대해 최소 0.0001정도를 추가하니 z-fighting이 일어나지 않았다.)

두 번째 트릭은 near plane을 가능한한 멀리 설정하는 것이다. 이전 섹션중의 하나에서, 우리는 near plane에 다가갈 때  precision이 매우 크다고 말했었다. 그래서만약우리가 viewer로부터 nearplane을 멀리 이동시킨다면, 우리는 전체 frustum range에 대해 상당히 더 큰 정확도를 가질 것이다. 그러나, near plane을 너무 멀리 설정하는것은 가까운objects를 clipping하도록 야기한다. 그래서 그것은 보통 약간의 수정하는 문제이고, 너의 scene을위해 가장 좋은 near distance를 알아내는 실험의 문제이다.

performance를 사용하는 또 다른 훌륭한 트릭은 더 높은 precision depth buffer를 사용하는 것이다. 대부분의 depth buffers는 24 bits의 정확도를 가지지만, 대부분의 요즘 날의 그래픽 카드들은 32 bit depth buffers를 지원한다. 그리고 이것은 상당한 양으로 precision을 증가시킨다. 그래서, 성능을 희생하여, 너는 depth testing에 좀 더 많은 precision을 얻을 것이다. z-fighting을 줄이면서.

우리가 이야기 한 그 3가지 기법들은 가장 흔하고, 구현하기에 쉬운 anti z-fighting 기법들이다. 좀 더 많은 일을 요구하고, 완전히 z-fighting을 막지는 못할 몇 가지 다른 기법들이 있다. Z-fighting은 흔한 문제이지만, 만약 너가 기재된 기법들의 적절한 조합을 사용한다면, 너는 아마도 z-fighting을 다룰 필요가 없을 것이다.

댓글 없음:

댓글 쓰기