Home [learn-opengl] Advanced OpenGL: Depth testing
Post
Cancel

[learn-opengl] Advanced OpenGL: Depth testing

Depth testing

  • 앞에서(좌표시스템) depth buffer를 사용하여 뒤에있는 면들이 앞에 그려지지 않도록 했다.

    • 이번에는 depth values를 다룰것이며, 이는 이 버퍼를 더 강력하게 만들 것이다.
    • 또한 특정 fragment가 다른 fragment들의 뒤에 있는지 판별하는 방법에 대해서 다룰 것이다.
  • depth-buffer

    • color buffer와 마찬가지로, 버퍼의 한 종류
    • fragment 정보를 저장, color buffer와 동일한크기를 가짐.
    • 윈도우 시스템에 의해(by the windowing system) 자동적으로 생성되고,
    • depth values들을 16, 24, 32 비트 실수형으로 저장한다.
    • 대부분은 24비트를 사용한다.
  • depth testing을 활성화

    • OpenGL은 depth buffer의 내용에 따라 fragment의 깊이 값을 테스트한다.
    • 통과되면, 버퍼는 새로운 깊이 값으로 수정된다.
    • 실패하면, 해당 fragment 는 폐기된다.
  • depth testing은 fragment shader가 수행된 후(그리고 stencil testing이 수행된 후)에 screen space에서 수행된다.

    • screen space 좌표는 OpenGL의 glViewport 함수에서 정의한 viewport와 관련이 있다. GLSL에서 gl_FragCoord 변수를 통해 접근할 수 있다.
    • gl_FragCoord 변수의 x, y 요소는 fragment의 screenspace 좌표를 나타낸다. ((0,0)이 화면의 좌측 하단)
    • 이 변수는 fragment의 실제 깊이 값을 가지고 있는 z요소 또한 포함한다.
    • 이 z값이 depth buffer의 내용과 비교할 값이다.

      최근 대부분의 GPU들은 early depth testing이라고 불리는 기능을 지원한다. 이는 fragment shader를 실행하기 전, depth test를 수행하도록한다. fragment가 보여지지 않게될 때(뒤에 위치할 때)마다 미리 fragment 를 폐기할 수 있다.

      fragment shader는 일반적으로 비용을 꽤 많이 차지하므로, 실행하는 것을 최소한으로 피할 수 있으면 피해야한다. early depth testing을 위해서는 fragment shader에서 깊이 값을 wirte하지 말아야한다. (작성하려고 하면, early depth testing 은 불가능, OpenGL은 depth value를 미리 알아낼 수 없다)

  • depth testing은 기본으로 비활성화 상태

    • 활성화는 GL_DEPTH_TEST 옵션을 사용해야한다.
    1
    
    glEnable(GL_DEPTH_TEST);
    
  • 활성화 되면, OpenGL은 자동으로 depth test를 통과한 경우에만 fragment의 z 값을 depth buffer에 저장한다.
  • 활성화되면 GL_DEPTH_BUFFER_BIT 를 사용하여 버퍼를 비워주어야 한다.
    • 그렇지 않으면 마지막 렌더링 루프에서 작성된 값들이 그대로 유지
1
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  • depth buffer를 수정하는 것을 원치 않을 경우
    • read-only depth buffer를 사용해야함.
    • depth mask를 GL_FALSE로 설정하여 덮어쓰기 비활성화
1
glDepthMask(GL_FALSE);

Depth test function

  • OpenGL은 depth test에서 사용하는 비교 연산을 조정할 수 있도록 해준다.
    • 이는 fragment를 어떨 때에 통과 혹은 폐기시켜야할 지를 조정할 수 있도록 하고,
    • 또한 depth buffer를 언제 수정해야하는 지에 대해서도 조정할 수 있도록 해준다.
    • glDepthFunc 함수를 사용하여 비교연산자(혹은 depth 함수)를 설정할 수 있다.
1
glDepthFunc(GL_LESS);
FunctionDescription
GL_ALWAYSThe depth test always passes.
GL_NEVERThe depth test never passes.
GL_LESSPasses if the fragment’s depth value is less than the stored depth value.
GL_EQUALPasses if the fragment’s depth value is equal to the stored depth value.
GL_LEQUALPasses if the fragment’s depth value is less than or equal to the stored depth value.
GL_GREATERPasses if the fragment’s depth value is greater than the stored depth value.
GL_NOTEQUALPasses if the fragment’s depth value is not equal to the stored depth value.
GL_GEQUALPasses if the fragment’s depth value is greater than or equal to the stored depth value.
  • 기본값은 GL_LESS이다.
    • 현재 depth buffer의 값과 동일하거나 큰 fragment를 폐기한다

depth 함수 예제

  • depth 함수를 수정해보자
    • 텍스처를 입힌 바닥 위에 텍스처를 입힌 2개의 큐브가 존재하고 조명이 없는 기본적인 scene을 렌더링하는 코드를 사용할 것이다.

소스 코드

  • depth 함수를 GL_ALWAYS로 바꾸어보자
1
2
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_ALWAYS);
  • 이는 depth testing 을 비활성화한 것과 같은 효과이다. - 항상 통과 => 마지막에 그려진 fragment가 렌더링 - 바닥을 마지막에 그리면, 바닥이 다음과 같이 보이게됨.

  • 다시 GL_LESS로 설정하면, 우리가 사용해왔던 유형의 scene을 볼 수 있다.

Depth value precision

  • Depth buffer는 0.01.0 사이의 깊이 값을 가진다.
    • viewr의 관점에서 scene의 모든 오브젝트들의 z값과 비교됨.
    • view space의 z값들은 projection 절두체의 nearfar 사이의 어떠한 값이 될 수 있음.
    • view space의 z값들은 0.01.0 사이의 값으로 변환해야함.
    • 이는 1차원적으로 변환할 수 있다.
    • 다음 일차 방정식은 z값을 0.01.0사이의 값으로 변환시킨다.
    • near, far는 projection 행렬에 설정했던 값들
\[\begin{equation} F_{depth} = \frac{z - near}{far - near} \end{equation}\]
  • 이 방정식은 절두체 내부의 깊이 값 z의 범위를 변환한다.
    • z 그래프는 다음과 같다.

  • 하지만, 이런 방정식은 사용되지 않음.

    • 올바른 투영 특성을 위해, 비선형 depth 방정식이 사용된다.
  • 비선형 방정식

    • 1/z와 비례
    • 기본적으로 z 값이 작을 때 큰 정밀도를 가지고, z 값이 멀리 있을 때 정밀도가 떨어지게 한다.
      • 1000단위 뒤의 오브젝트와 1단위 오브젝트가 동일한 값의 정밀도를 가지는것은 좋지않다.
    • 비선형 함수는 1/z에 비례하고, 1.02.0 사이의 z값을 0.5, 1.0 사이의 깊이 값으로 변환한다.
      • 이는 작은 z값에 대해 큰 정밀도를 가지게 한다.
  • 비선형 방정식은 아래와 같다.

    \[\begin{equation} F_{depth} = \frac{1/z - 1/near}{1/far - 1/near} \end{equation}\]
  • 이 방정식은 screen-space에서 비선형이다.

    • projection 행렬이 적용되기 전의 view-space에서는 선형적

  • 위 그래프에서 볼 수 있듯이 깊이값들은 작은 z값에서 큰 정밀도를 가진다.
    • z값을 변환시키는 이 방정식은 projection 행렬에 포함되어 있으므로
    • vertex 좌표를 view에서 clip 변환하여 screen-space로 이동할 때 이 비선형 방정식이 적용된다.
    • projection 행렬 수행 원리에 관한 내용

멀어질수록 간격이 좁아진다

가능한 전방 절단면을 시점에서 멀리 => 물체 간격이 상대적으로 보존 => 깊이테스트 올바르게 가능

전방절단면의 위치를 시점 쪽으로 바짝 잡아당기면 => 물체가 전방절단면의 훨씬 뒤쪽에 분포해, 촘촘한 간격의 깊이로 사상되어 버린다.

전방절단면을 오히려 시점에 떨어뜨려 물체에 최대한 근접시켜야함 => 물체의 간격이 상대적으로 보존됨

깊이 버퍼의 비트수의 제한으로인해 정밀도 문제

Visualizing the depth buffer

  • fragment shader의 gl_FragCoord 의 z값을 컬러로 출력하면,

    • 모든 fragment의 깊이 값들을 scene에 출력할 수 있다.
    • fragment의 깊이 값을 기반으로 컬러 벡터를 리턴
    1
    2
    3
    4
    
    void main()
    {
        FragColor = vec4(vec3(gl_FragCoord.z), 1.0);
    }
    
  • 프로그램을 실행한다면, 모든것이 하얀색으로 보일 것
    • 모든 깊이 값들이 1.0인것처럼
  • screenspace 에서는 깊이 값들은 비선형이다.
    • 작은 z 값 => 큰 정밀도
    • 큰 z 값 => 작은 정밀도
    • 깊이 값은 거리에 따라 급격히 증가하므로, 대부분의 모든 vertex들은 1.0에 가까운 값을 가지게 됨.
    • 오브젝트에 가까이 가면 결국 어두워짐.(z값이 점점 작아져서)

  • 가까운 오브젝트들은 멀리있는 오브젝트들보다 더 큰 효과를 가짐.
  • 이런 비선형 깊이 값을 다시 선형으로 변환할 수 있다.

    • 깊이 값을 위한 projection 과정을 반대로 해야함.
    • [0,1] 범위의 깊이 값들을 [-1,1] 범위의 NDC 좌표로 변환해야한다는 것을 의미.
    • projection 행렬에서 수행된 비선형 방정식의 역함수를 구하여 깊이 값에 적용해야함.
    • 그 결과로 선형 깊이 값이 도출됨.
  • NDC 좌표로 변환
1
float ndc = depth * 2.0 - 1.0;
  • 이 z 값에 역변환을 적용 시켜 선형 깊이 값을 얻는다.
1
float linearDepth = (2.0 * near * far) / (far + near - ndc * (far - near));
  • 위 방정식은 projection 행렬로부터 얻은것

  • screen-space에서의 비선형 깊이 값을 선형 깊이 값으로 변환하는 최종 fragment shader는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#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()
{
    float depth = LinearizeDepth(gl_FragCoord.z) / far; // divide by far for demonstration
    FragColor = vec4(vec3(depth), 1.0);
}
  • 변환된선형 깊이 값들은 near 와 far 사이의 값이기 때문에 대부분의 값들은 1.0 보다 높다. 그러므로 far로 나눔으로써 선형 깊이 값을 대략 [0,1] 범위로 변환시킬 수 있다.

  • 이제 프로그램을 실행시켜보면, 실제로 거리에 따라 선형적인 깊이 값을 얻을 수 있다.

  • 0.1에 위치한 near평면과 100에 위치한 far 평면 사이에 선형적으로 존재하는 깊이값을 얻을 수 있다.
    • 결과는 근거리 평면에 상대적으로 가깝기 때문에 더 낮은(어두운) 깊이 값을 던는다.

Z-fighting

  • 시각적 결함

    • 두개의 평면이나 삼각형들이 아주 가깝게 서로 나란히 위치할 때 발생
    • 이 경우 depth buffer는 두 개의 도형 중 어떠한 것이 앞에 있는지 알아내기 위한 충분한 정밀도를 가지지 못한다.
    • 결과적으로 두 도형이 계속해서 순서가 바뀌는 것과 같은 패턴이 보임.
    • 이 현상을 z-fighting이라고 한다.(보여지기위해 싸우는 것과 같이 보이기 때문에)
    • 컨테이너들은 바닥이 위치한 정확한 높이에 위치해있다.
    • 즉, 컨테이너의 밑면이 바닥 평면과 바닥이 동일한 위치에 있음.
    • depth test에서 문제 생김
    • 카메라를 내부로 이동하여 보면 이 현상이 보일 것이다.(아래 이미지에서 지그재그 패턴을 볼 수 있음)

    • 이는 depth buffer에서 흔히 발생하는 문제이다.
    • 멀리 있는 오브젝트에서 더 많이 발생한다.(더 촘촘하므로, 정밀도 문제 발생)
  • 이런 문제는 완전히 해결 불가능하지만, 완화시키거나, 이처럼 안보이게하는 트릭이 존재한다.

Prevent z-fighting

  • 가장 중요한 트릭은 삼각형들이 겹치지 않을 정도로 가깝게 두지 않는것이다.
    • 엄청 작은 offset 값을 설정한다.
    • 이는 수작업으로 조정해야한다.
  • 두번째 트릭은, near 평면을 가능한 멀리 설정하는 것이다.

    • near 평면에 가까울 수록 정밀도는 커짐
    • near 평면을 시점으로부터 멀리 이동시킨다면, 전체 절두체 범위에 걸쳐 큰 정밀도를 가질 수 있다.
    • 하지만, 시점 가까이에 있는 오브젝트들을 폐기할 수 있으므로 일반적으로 최적의 near 거리를 찾아야한다,
  • 세번째 트릭은, 높은 정밀도의 depth buffer를 사용하는 것이다.

    • 대부분 24 비트의 정밀도
    • 요즘 그래픽 카드들은 32 비트도 지원함
    • 이는 정밀도를 크게 증가시켜준다.
    • 일부 성능을 희생하면, depth testing에 대한 정밀도를 높여 z-fighting을 줄일 수 있다.
  • Z-fighting은 흔한 문제이지만, 적절히 이런 트릭들을 조합하면 크게 걱정할 필요는 없다.

출처

Depth-testing

Depth-testing: 번역

This post is licensed under CC BY 4.0 by the author.

[백준][C++] 2206: 벽부수고 이동하기 (bfs)

[learn-opengl] Advanced OpenGL: Stencil testing