눈팅하는 게임개발자 블로그
OpenGL Advanced 5-1 Depth testing 본문
원문 사이트
learnopengl.com/Advanced-OpenGL/Depth-testing
LearnOpenGL - Depth testing
Depth testing Advanced-OpenGL/Depth-testing In the coordinate systems chapter we've rendered a 3D container and made use of a depth buffer to prevent triangles rendering in the front while they're supposed to be behind other triangles. In this chapter we'r
learnopengl.com
번역 사이트
heinleinsgame.tistory.com/24?category=757483
[Learn OpenGL 번역] 5-1. 고급 OpenGL - Depth testing
Depth testing 고급 OpenGL/Depth-testing 좌표 시스템 강좌에서 3D 컨테이너를 렌더링해보았고 depth buffer 를 사용하여 뒤에있는 면들이 앞에 그려지지 않도록 하였습니다. 이번 강좌에서는 depth values 에 대
heinleinsgame.tistory.com
Depth testing
좌표 시스템 강좌에서 3D 컨테이너를 렌더링 해보았고 depth buffer를 사용하여 뒤에 있는 면들이
앞에 그려지지 않도록 했었다.
이번에는 depth values에 대해서 좀 더 자세히 다루어 depth buffer를 더욱 강력하게 만들 것이고 실제로
특정 fragment가 다른 fragment들의 뒤에 있는지 판별하는 방법에 대해서 다룰 것이다.
depth-buffer는 colo buffer(모든 fragment의 최종 출력 컬러를 저장하는 버퍼)와 마찬가지로 버퍼의 한 종류로서
fragment의 정보를 저장하고 (일반적으로) color buffer와 동일한 크기를 가지고 있다.
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의 glViewport 함수에서 정의한 viewport와 직접적으로 관련이 있다.
그리고 GLSL에서 gl_FragCoord 변수를 통해 접근할 수 있다.
gl_FragCoord 변수의 x, y 요소는 fragment의 screen space 좌표((0, 0)이 화면 좌측 하단)를 나타낸다.
또한 gl_FragCoord 변수는 fragment의 실제 깊이 값을 가지고 있는 z 요소도 포함하고 있다.
이 z 값은 depth buffer의 내용과 비교할 값이다.
최근 대부분의 CPU들은 early depth testing이라고 불리는 기능을 지원한다. Early depth testing은 fragment shader를 실행하기 전에 depth test를 수행할 수 있도록 해준다. fragment가 보여지지 않게 될 때(다른 오브젝트의 뒤에 위치할 때)마다 해당 fragment를 미리 폐기할 수 있다. 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를 수정하는 것을 원하지 않을 때도 존재한다.
이럴 때 read-only depth buffer를 사용한다.
OpenGL은 depth mask를 GL_FALSE로 설정함으로써 depth buffer에 작성하는 것을 비활성화할 수 있도록 해준다.
glDepthMask(GL_FALSE);
이 효과는 depth testing을 활성화했을 때에만 사용 가능하다는 것을 유념해야 한다.
Depth test 함수
OpenGL은 depth test에서 사용하는 비교 연산을 조정할 수 있도록 해준다. 이는 fragment를 어떨 때에 통과 혹은
폐기시켜야 할 지를 조정할 수 있도록 하고 또한 depth buffer를 언제 수정해야 하는 지에 대해서도 조정할 수 있도록
한다. glDepthFunc 함수를 사용하여 비교 연산자(혹은 depth 함수)를 설정할 수 있다.
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_LSEE로 설정하면 기존에 사용해왔던 유형의 scene을 볼 수 있다.
Depth Value Precision
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과 가까워 진다는 것을 유념해야 한다. |
하지만 이와 같은 linear depth buffer는 일반적으로 사용되지 않는다.
올바른 투영 특성을 위해 비선형의 depth 방정식이 사용된다.
이 방정식은 1/z와 비례하는데, 이는 기본적으로 z값이 작을 때 큰 정밀도를 가지고 z값이 멀리 있을 때
정밀도가 떨어지게 된다. 이에 대해 잠시 생각해보면,
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로 이동할 때 이 비선형 방정식이 적용된다.
이 비선형 방정식의 효과는 depth buffer를 시각화 했을 때 쉽게 확인할 수 있다.
Visualizing the 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값에서는 작은 정밀도를 가지게 되는 것이다.
오브젝트에 점점 아주 가까이 다가가면 결국에는 어두워지는 색을볼 수 있을 것이다.
이는 분명히 깊이 값의 비선형성을 잘 보여준다. 가까운 오브젝트들은 멀리 있는 오브젝트들보다 더 큰 효과를 가진다.
약간만 움직여도 컬러는 어두운 색에서 완전히 하얀색으로 변화한다.
하지만 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 행렬로부터 얻을 수 있다.
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보다 높아 완전한 하얀색으로 출력될 것이다.
main 함수에서 선형 깊이 값을 far로 나눔으로써 선형 깊이 값을 대략적으로 0~1 범위로 변환시킬 수 있다.
이제 프로그램을 실행시켜 보면 실제로 거리에 따라 선형적인 깊이 값을 얻는다.
이 컬러들은 대부분 검정색이다. 이 깊이 값들이 0.1에 위치한 near 평면과 100에 위치한 far 평면 사이에
선형적으로 존재하고 far 평면은 여전히 현재 viewer의 위치와 꽤 멀기 때문이다.
결과적으로 비교적 near 평면과 가깝고 따라서 낮은(어두운) 깊이 값들을 얻게 되는 것이다.
Z-Fighting
흔한 시각적 결함은 두 개의 평면이나 삼각형들이 아주 가깝게 서로 나란히 위치할 때 발생할 수 있다.
이 경우 depth buffer는 두 개의 도형 중 어떠한 것이 앞에 있는지 알아내기 위한 충분한 정밀도를 가지지 못한다.
결과적으로 두 개의 도형이 계속해서 순서가 바뀌는 것처럼 보이는 이상한 패턴을 가지게 되는데,
이러한 현상을 도형들이 누가 위에 있는지 싸우는 것과 같이 보이기 때문에 z-fighting 이라고 부른다.
지금까지 사용했던 scene에는 z-fighting이 발생될 만한 곳이 존재한다.
컨테이너들은 바닥과 정확히 같은 높이에 위치한다.
이는 컨테이너의 밑면이 바닥 평면과 동일 평면상에 존재한다는 것을 의미한다.
이 두 평면의 깊이 값들은 동일하므로 depth test는 무엇이 앞에 있는지 알아낼 방법이 없다.
한 컨테이너의 내부로 카메라를 움직여보면 이 현상이 명확하게 보일 것이다.
컨테이너의 밑부분이 계속해서 컨테이너의 평면과 바닥의 평면 사이에서 바뀌면서 지그재그 패턴을
보이는 것을 확인할 수 있다.
Z-fighting은 depth buffer와 관련된 흔한 문제이다.
또한 멀리 있는 오브젝트에서 더 많이 발생한다. (depth buffer는 z값이 클수록 더 작은 정밀도를 가지기 때문이다.)
Z-fighting은 완전히 예방될 수는 없지만 일반적으로 완화시키거나 완전히 예방하도록 도와주는 약간의 트릭이 있다.
Prevention Z-fighting
첫 번째이자 가장 중요한 트릭은 삼각형들이 겹쳐지지 않을 정도로 오브젝트들을 절대 가깝게 두지 않는 것이다.
두 개의 오브젝트 사이에 사용자가 알아채지 못할 정도의 작은 offset을 형성함으로써 두개의 오브젝트에 대한
z-fighting을 완전히 없앨 수 있다. 이 컨테이너와 평면의 경우에는 간단히 컨테이너를 y축의 양의 방향으로
약간 움직이는 것만으로 해결할 수 있다.
컨테이너의 위치의 작은 변화는 사용자가 알아채기 힘들 정도이고 z-fighting을 방지할 수 있다.
하지만 이는 testing 전반에 걸쳐 z-fighting을 만들 수 있는 오브젝트를 없애기 위해 각 오브젝트에 대해 수작업으로
조정을 해줘야 한다.
두 번째 트릭은 near 평면을 가능한 멀리 설정하는 것이다.
이전 섹션에서 near 평면에 가까울 때 정밀도는 극도로 커진다고 언급했다.
만약 near 평면을 시점으로부터 멀리 이동시킨다면 전체 절두체 범위에 걸쳐서 굉장히 큰 정밀도를 가질 수 있을 것이다.
하지만 near 평면을 멀리 설정하는 것은 가까이 있는 오브젝트들을 자를 수 있으므로 일반적으로 실험을 많이 하여
최적의 near 거리를 찾아야 한다는 특성이 있다.
또 다른 훌륭한 트릭은 높은 정밀도의 depth buffer를 사용하는 것이다.
대부분의 depth buffer들은 24비트의 정밀도를 가지고 있다.
하지만 최근 대부분의 그래픽 카드들은 32비트의 depth buffer를 지원한다.
이 depth buffer는 상당히 크게 정밀도를 증가시켜 준다.
그래서 일부 성능을 희생하면 depth testing에 대한 정밀도를 높여 z-fighting을 줄일 수 있다.
이 3가지의 기술들은 가장 많이 쓰이고 구현하기 쉬운 z-fighting 방지 기술들이다.
많은 작업을 요구하지만 여전히 완벽히 z-fighting을 차단하지는 못하는 다른 여러가지 기술들이 존재한다.
z-fighting은 흔한 문제이지만 적절한 기술들을 조합하면 z-fighting에 대해 걱정할 필요가 없을 것이다.
'공부한거 > OpenGL' 카테고리의 다른 글
OpenGL Advanced 5-3 Blending (0) | 2020.12.09 |
---|---|
OpenGL Advanced 5-2 Stencil testing (0) | 2020.12.08 |
OpenGL Model Loading 4-3 Model (0) | 2020.12.07 |
OpenGL Model Loading 4-2 Mesh (0) | 2020.12.07 |
OpenGL Model Loading 4-1 Assimp (0) | 2020.12.07 |