게임공장
[Learn OpenGL 번역] 5-1. 고급 OpenGL - Depth testing 본문
Depth testing
고급 OpenGL/Depth-testing
좌표 시스템 강좌에서 3D 컨테이너를 렌더링해보았고
depth-buffer는 16
, 24
, 32
비트 실수형으로 저장합니다. 대부분의 시스템에서 24
비트의 깊이 값을 사용하는 것을 볼 수 있을 것입니다.
depth testing을 사용가능하게 설정하면 OpenGL은 depth buffer의 내용에 따라 fragment의 깊이 값을 테스트합니다. OpenGL은 depth test를 수행하고 이 테스트가 통과되면 이 depth buffer는 새로운 깊이 값으로 수정됩니다. 이 테스트가 실패한다면 해당 fragment는 폐기됩니다.
Depth testing은 fragment shader가 수행된 후(그리고 다음 강좌에서 다룰 stencil testing이 수행된 후)에 screen space에서 수행됩니다. screen space 좌표는 OpenGL의
Fragment shader는 일반적으로 비용을 꽤 많이 차지하므로 실행하는 것을 최소한으로 피할 수 있으면 피해야 합니다. early depth testing을 위해서는 fragment shader에서 깊이 값을 작성하지 말아야합나다. fragment shader가 깊이 값을 작성하려고 한다면 early depth testing은 불가능해집니다. OpenGL이 사전에 깊이 값을 알 수 없습니다.
Depth testing은 기본값으로는 비활성화가 되어있습니다. depth testing을 활성화시키기 위해서는 GL_DEPTH_TEST 옵션을 사용하여 활성화 시켜주어야 합니다.
glEnable (GL_DEPTH_TEST);
활성화가 되면 OpenGL은 자동으로 depth test에 통과하면 fragment의 z 값을 depth buffer에 저장하고 실패하면 fragment를 폐기합니다. depth testing을 활성화했다면 각 렌더링 루프가 돌때마다 GL_DEPTH_BUFFER_BIT를 사용하여 depth buffer를 비워주어야 합니다. 그러지 않으면 마지막 렌더링 루프에서 작ㅈ성된 깊이 값이 쌓이게 될것입니다.
glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
모든 fragment에 대해 depth test를 수행하고 그에 따라 fragment를 폐기하지만 depth buffer를 수정하는 것을 원하지 않을 때도 있을 것입니다. 이럴 때 GL_FALSE
로 설정함으로써 depth buffer에 작성하는 것을 비활성화할 수 있도록 해줍니다.
glDepthMask (GL_FALSE);
이 효과는 depth testing을 활성화했을 때에만 사용 가능하다는 것을 알아두세요.
Depth test 함수
OpenGL은 depth test에서 사용하는 비교 연산을 조정할 수 있도록 해줍니다. 이는 fragment를 어떨 때에 통과 혹은 폐기시켜야할 지를 조정할 수 있도록 하고 또한 depth buffer를 언제 수정해야하는 지에 대해서도 조정할 수 있도록 해줍니다. 우리는
glDepthFunc (GL_LESS);
이 함수는 아래 표에 있는 여러가지 비교 연산자들을 설정할 수 있습니다.
함수 | 설명 |
---|---|
GL_ALWAYS |
depth test가 항상 통과됩니다. |
GL_NEVER |
depth test가 절대 통과되지 않습니다. |
GL_LESS |
fragment의 깊이 값이 저장된 깊이 값보다 작을 경우 통과시킵니다. |
GL_EQUAL |
fragment의 깊이 값이 저장된 깊이 값과 동일한 경우 통과시킵니다. |
GL_LEQUAL |
fragment의 깊이 값이 저장된 깂이 값과 동일하거나 작을 경우 통과시킵니다. |
GL_GREATER |
fragment의 깊이 값이 저장된 깊이 값보다 클 경우 통과시킵니다. |
GL_NOTEQUAL |
fragment의 깊이 값이 저장된 깊이 값과 동일하지 않을 경우 통과시킵니다. |
GL_GEQUAL |
fragment의 깊이 값이 저장된 깊이 값과 동일하거나 클 경우 통과시킵니다. |
기본값인 GL_LESS는 깊이 값이 현재 depth buffer의 값과 동일하거나 큰 모든 fragment들을 폐기합니다.
depth 함수를 수정함으로써 가져오는 시각적인 출력을 봐봅시다. 우리는 텍스처를 입힌 바닥 위에 텍스처를 입힌 2개의 큐브가 존재하고 조명이 없는 기본적인 scene을 렌더링하는 코드를 사용할 것입니다. 이 소스 코드는 여기에서 확인할 수 있습니다.
소스 코드에서 depth 함수를 GL_ALWAYS로 바꾸었습니다.
glEnable (GL_DEPTH_TEST);
glDepthFunc (GL_ALWAYS);
이는 우리가 depth testing을 비활성화했을 때 얻을 수 있는 결과를 나타냅니다. 이 depth testing은 항상 통과하므로 마지막에 그려진 fragment들은 전에 그려진 fragment위에 렌더링됩니다. 우리는 바닥을 마지막에 그렸기 때문에 이 평면의 fragment들은 컨테이너의 fragment들을 덮어 씌웁니다.
다시 GL_LESS로 설정하면 우리가 사용해왔던 유형의 scene을 볼 수 있습니다.
깊이 값 정확성
Depth buffer는 0.0
와 1.0
사이의 깊이 값을가지고 있고 viewer의 관점에서 scene의 모든 오브젝트들의 z 값과 비교됩니다. 이 view space의 z 값들은 projection 절두체의 near
와 far
사이의 어떠한 값이 될 수 있습니다. 따라서 이러한 view-space z 값들을 [0,1]
범위로 변환시키는 방법이 필요하고 이 방법 중 하나는 1차원 적으로 변환하는 방법입니다. 다음 (일차)방정식은 z 값을 0.0
와 1.0
사이의 값으로 변환시킵니다.
여기에서 near 와 far 는 절두체를 설정하기 위해 projection 행렬에 전달해왔던 near, far 값입니다. 이 방정식은 절두체 내부의 깊이 값 z를 [0,1]
범위의 값으로 변환시킵니다. z 값과 해당 깊이 값의 관계는 다음 그래프와 같습니다.
0.0
과 가까워지고 오브젝트가 far 평면에 가까이 있을 때 1.0
과 가까워진다는 것을 알아두세요.
하지만 이와 같은 1000
단위정도로 멀리 떨어진 오브젝트가 1
단위 거리에 있는 매우 상세화된 오브젝트와 동일한 깊이 값 정밀도를 가지기를 원합니까? 이 일차방정식은 이 점을 안중에 두지 않습니다.
이 비선형 함수는 1/z에 비례하고 예를들어 1.0
와 2.0
사이의 z 값을 0.5
, 1.0
사이의 깊이 값으로 변환합니다. 이는 작은 z 값에 대해 큰 정밀도를 가지도록 합니다. 50.0
과 100.0
사이의 z 값은 정밀도의 2%밖에 차지하지 않습니다. 이는 정확히 우리가 원하는 것입니다. near과 far 거리를 염두하는 이러한 방정식은 다음과 같습니다.
이 방정식이 이해가지 않더라도 걱정하지 마세요. 기억해야 할 중요한 것은 이 depth buffer 내부의 값들은 screen-space에서 비선형이라는 것입니다(projection 행렬이 적용되기 전의 view-space에서는 선형적입니다). 이 depth buffer 내부의 0.5
값은 오브젝트의 z 값이 절두체의 중간지점이 있다는 것을 의미하는 것이 아닙니다. 이 vertex의 z 값은 실제로 near 평면에 꽤 가까이에 있습니다! z 값과 이 depth buffer의 값의 관계는 다음과 같은 그래프로 나타낼 수 있습니다.
보시다시피 이 깊이 값들은 작은 z 값에서 큰 정밀도를 가집니다. z 값을 변환시키는 이 방정식은 projection 행렬에 포함되어 있으므로 vertex 좌표를 view에서 clip으로 변환하여 screen-space로 이동할 때 이 비선형 방정식이 적용됩니다. 만약 이 projection 행렬이 실제로 무엇을 수행하는지에 대해 자세히 알아보고 싶다면 great article을 추천합니다.
이 비선형 방적식의 효과는 depth buffer를 시각화 했을 때 쉽게 확인할 수 있습니다.
Depth buffer 시각화
fragment shader의 gl_FragCoord 벡터의 z 값은 특정 fragment의 깊이 값을 가지고 있다는 것을 알고 있습니다. fragment의 이 깊이 값을 컬러로 출력한다면 모든 fragment의 깊이 값들을 scene에 출력할 수 있습니다. fragment의 깊이 값을 기반으로 컬러 벡터를 리턴하여 이를 수행할 수 있습니다.
void main()
{
FragColor = vec4(vec3(gl_FragCoord.z), 1.0);
}
다시 동일한 프로그램을 실행시킨다면 아마 모든 것이 하얀색으로 보일 것입니다. 이는 모든 깊이 값들이 1.0
인 것처럼 보입니다. 왜 깊이 값들이 0.0
와 가까워지지 않고 또한 어두워지지 않을까요?
이전 섹션에서 screen space에서의 깊이 값들은 비선형이라고 했었습니다. 예를 들어 작은 z 값에서는 큰 정밀도를 가지고 큰 z 값에서는 작은 정밀도를 가진다는 것입니다. 이 깊이 값은 거리에 따라 급격히 증가하므로 대부분의 모든 vertex들은 1.0
에 가까운 값을 가지게 되는 것입니다. 오브젝트에 점점 아주 가까이 다가가면 결국에는 어두워지는 색을 볼 수 있을 것입니다. z 값이 점점 작아지는 것을 볼 수 있습니다.
이는 분명히 깊이 값의 비선형성을 잘 보여줍니다. 가까운 오브젝트들은 멀리 있는 오브젝트들보다 더 큰 효과를 가집니다. 약간만 움직여도 컬러는 어두운 색에서 완전히 하얀색으로 변홥니다.
하지만 우리는 fragment의 비선형 깊이 값을 다시 선형으로 변환할 수 있습니다. 이를 수행하기 위해 깊이 값을 위한 projection 과정을 반대로 해야합니다. 이는 먼저 [0,1]
범위의 깊이 값들을 [-1,1]
범위의 NDC 좌표로 변환해야 한다는 것을 의미합니다. 그런 다음 projection 행렬에서 수행된 비선형 방정식의 역함수를 구합니다. 그리고 이 역함수를 결과 깊이 값에 적용시킵니다. 이 결과로 선형 깊이 값이 도출됩니다.
먼저 깊이 값을 NDC 좌표로 변환합니다.
float z = depth * 2.0 - 1.0;
그런 다음 이 z 값에 역변환을 적용시켜 선형 깊이 값을 얻습니다.
float linearDepth = (2.0 * near * far) / (far + near - z * (far - near));
이 방정식은 비선형 깊이 값을 구하기 위한 방정식을 사용한 projection 행렬로부터 얻을 수 있습니다. math-heavy article에서 이 projection 행렬에 대해서 자세히 확인할 수 있습니다. 또한 이 방정식이 어떻게 얻어졌는지에 대해서도 알 수 있습니다.
screen-space에서의 비선형 깊이 값을 선형 깊이 값으로 변환하는 최종 fragment shader는 다음과 같습니다.
#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; // 다시 NDC로 변환
return (2.0 * near * far) / (far + near - z * (far - near));
}
void main()
{
float depth = LinearizeDepth(gl_FragCoord.z) / far; // 보여주기 위해 far로 나눕니다.
FragColor = vec4(vec3(depth), 1.0);
}
변환된 선형 깊이 값들은 near와 far 사이의 값이기 때문에 대부분의 값들은 1.0
보다 높아 완전한 하얀색으로 출력될 것입니다. 0
, 1
] 범위로 변환시킬 수 있습니다.
이제 프로그램을 실행시켜 보면 실제로 거리에 따라 선형적인 깊이 값을 얻습니다. scene을 돌아보면서 선형적인 방식으로 바뀌는 깊이 값을 확인해보세요.
이 컬러들은 대부분 검정색입니다. 이 깊이 값들이 0.1
에 위치한 near
평면과 100
에 위치한 far
평면 사이에 선형적으로 존재하고 far
평면은 여전히 우리와 꽤 멀기 때문입니다. 결과적으로 우리는 비교적 near 평면과 가깝고 따라서 낮은 (어두운) 깊이 값들을 얻게 되는 것입니다.
Z-fighting
흔한 시각적 결함은 두개의 평면이나 삼각형들이 아주 가깝게 서로 나란히 위치할 때 발생할 수 있습니다. 이 경우 depth buffer는 두 개의 도형 중에 어떠한 것이 앞에 있는지 알아내기위한 충분한 정밀도를 가지지 못합니다. 결과적으로 두 개의 도형이 계속해서 순서가 바뀌는 것처럼 보이게 됩니다. 이상한 패턴과 함께 말이죠. 이러한 현상을 도형들이 누가 위에있는지 싸우는 것과 같이 보이기 때문에
지금까지 우리가 사용했던 scene에는 z-fighting이 발생될만한 곳이 존재합니다. 컨테이너들은 바닥이 위치한 정확한 높이에 위치합니다. 이는 컨테이너의 밑면이 바닥 평면과 동일 평면상에 존재한다는 것을 의미합니다. 이 두 평면의 깊이 값들은 동일하므로 depth test는 무엇이 앞에 있는지 알아낼 방법이 없습니다.
한 컨테이너의 내부로 카메라를 움직여보면 이 현상이 명확히 보일 것입니다. 컨테이너의 밑부분이 계속해서 컨테이너의 평면과 바닥의 평면 사이에서 바뀌면서 지그재그 패턴을 표시하는 것을 볼 수 있습니다.
Z-fighting은 depth buffer와 관련된 흔한 문제입니다. 또한 멀리 있는 오브젝트에서 더 많이 발생합니다(depth buffer는 z 값이 클수록 더 작은 정밀도를 가지기 때문입니다). Z-fighting은 완전히 예방될수는 없지만 일반적으로 완화시키거나 완전히 예방하도록 도와주는 약간의 트릭이 존재합니다.
Z-fighting 방지
첫 번째이자 가장 중요한 트릭은 삼각형들이 겹쳐지지 않을정도로 오브젝트들을 절대로 가깝게 두지 않는 것입니다. 두 개의 오브젝트 사이에 사용자가 알아채리지 못할 정도리 작은 offset을 생성함으로써 두 개의 오브젝트에 대한 z-fighting을 완전히 없앨 수 있습니다. 이 컨테이너와 평면의 경우에는 간단히 컨테이너를 y 축의 양의 방향으로 약간 움직이는 것만으로 해결할 수 있습니다. 컨테이너 위치의 작은 변화는 아마 알아채리지 못할 것이고 z-fighting을 줄일 수 있습니다. 하지만 이는 testing 전반에 걸쳐 z-fighting을 만들 수 있는 오브젝트를 얻애기 위해 각 오브젝트에 대해 수작업으로 조정을 해야합니다.
두 번째 트릭은 near 평면을 가능한 멀리 설정하는 것입니다. 이전 섹션에서 near
평면에 가까울 때 정밀도는 극도로 커진다고 언급하였습니다. 만약 near
평면을 시점으로부터 멀리 이동시킨다면 전체 절두체 범위에 걸쳐서 굉장히 큰 정밀도를 가질 수 있을 것입니다. 하지만 near
평면을 멀리 설정하는 것은 가까이 있는 오브젝트들을 자를 수 있으므로 일반적으로 실험을 많이 하여 최적의 near
거리를 찾아야합니다.
또다른 훌륭한 트릭은 높은 정밀도의 depth buffer를 사용하는 것입니다. 대부분의 depth buffer들은 24
비트의 정밀도를 가지고 있습니다. 하지만 최근 대부분의 그래픽 카드들은 32
비트의 dpeth buffer를 지원합니다. 이 depth buffer는 상당히 크게 정밀도를 증가시켜줍니다. 그래서 일부 성능을 희생하면 depth testing에 대한 정밀도를 높여 z-fighting을 줄일 수 있습니다.
이 3가지의 기술들은 가장 많이 쓰이고 구현하기 쉬운 z-fighting 방지 기술들입니다. 많은 작업을 요구하지만 여전히 완벽히 z-fighting을 차단하지는 못하는 다른 여러가지 기술들이 존재합니다. Z-fighting은 흔한 문제이지만 적절한 기술들을 조합하면 z-fighting에 대해 걱정할 필요가 없을 것입니다.
'OpenGL' 카테고리의 다른 글
[Learn OpenGL 번역] 5-4. 고급 OpenGL - Face culling (0) | 2018.08.08 |
---|---|
[Learn OpenGL 번역] 5-2. 고급 OpenGL - Stencil testing (0) | 2018.08.05 |
[Learn OpenGL 번역] 4-3. 모델 불러오기 - Model (0) | 2018.07.27 |
[Learn OpenGL 번역] 4-2. 모델 불러오기 - Mesh (0) | 2018.07.24 |
[Learn OpenGL 번역] 4-1. 모델 불러오기 - Assimp (0) | 2018.07.18 |