[게임 수학] #21 | 깊이 버퍼(Depth Buffer)

2023. 12. 7. 18:35Computer Sciences/Game Mathemathics

*인프런 <게임 엔진을 지탱하는 게임수학, 이득우 교수님> 강의를 듣고 공부한 글입니다.

 

 

1. 깊이 버퍼(Depth Buffer)

`깊이 버퍼(Depth Buffer)`는 카메라로부터 얼마나 깊은 곳에 있는지는 파악할 수 있는 데이터라고 할 수 있습니다. 3차원 공간에서 다수의 물체들을 그릴 때, 그리는 순서를 고려하지 않으면 어떤 물체가 앞에 있고 뒤에 있는지 구분할 수가 없습니다. 앞에 있어야 할 물체(1)가 먼저 그려지고, 뒤에 있어야 할 물체(2)가 나중에 그려지면, 사실상 우리 눈으로 봤을 때는 (1) 물체가 (2) 뒤에 있는 것처럼 보이기 때문이죠.

 

깊이 값을 적용하지 않았을 때 발생하는 결과

 

이러한 문제가 발생하는 이유는 각 물체마다 카메라로부터 얼만큼 뒤에 있는지에 대한 기준치가 없기 때문입니다. 이러한 기준치를 정하기 위해 깊이 버퍼라는 것을 도입했으며, 이 깊이 버퍼 값에 따라 뒤에 있는 물체가 앞에 있는 물체를 덮어 쓰지 않게 픽셀 별로 계산해 적용함으로써 위 문제를 해결할 수 있습니다.

 

이러한 깊이 버퍼를 구현할 수 있는 대표적인 방법으로 `Z-Buffer` 알고리즘이 있습니다. 이 글에서는 깊이 버퍼 구현 내용에 대해선 다루지 않고, 깊이 버퍼 값을 어떻게 계산하는 지에 대해서만 알아보도록 하겠습니다.

 

1) 절두체(Frustum)

카메라의 `화각(Field of View)`이 지정되면, 카메라에서 화각으로 무한대로 펼쳐질 겁니다. `사영 공간(Projective Space)`에서는 \(z\) 값이 \(x, \ y\) 값에 영향을 미치기 때문에, \(z\) 값이 커짐에 따라 \(x, \ y\) 값도 늘어나기 때문이죠.

 

사실 반대편 쪽에선 카메라 위치(0, 0, 0)으로 수렴하기 때문에 사실상 무한대 ~ 무한대 공간이다.

 

그런데 이렇게 무한대의 영역에서 깊이 값을 산출하기 보다는, NDC 좌표계처럼 일정한 범위를 두는 겁니다. 무한대로 펼쳐진 사영 공간에서 시작점끝 점을 주고, 이 영역만 딱 잘라서 이 공간에 속한 내용들만 그리는 겁니다. 이렇게 시작점과 끝 점을 자르게 되면 피라미드 꼭대기 부분을 가로로 자른 듯한 도형이 나오게 되는데, 이를 `절두체(Frustum)`라고 부르죠.

 

이 절두체의 시작점을 `근 평면(Near Plane)`, 끝 점을 `원 평면(Far Plane)` 이라고 부릅니다. 일반적으로 근 평면의 깊이 값은 -1, 원 평면의 깊이 값은 1을 주기에, 이 절두체 내에 속한 물체들의 깊이 값은 \(-1 \sim \ 1\) 의 범위를 가지게 됩니다.

 

절두체(Frustum)

 

이러한 절두체의 근 평면과 원 평면 개념을 기반으로 NDC 좌표계를 확장하면 다음과 같은 모양이 됩니다.

 

3차원으로 구성한 NDC 좌표 공간

 

그런데 보통 깊이 값을 계산할 때, 이미지로 나타내는 형태가 많습니다. 이미지라는 것은 결국에는 색상 정보로 이루어져 있고, 색상은 일반적으로 마이너스(-) 값이라는 게 존재하지 않습니다. 이미지에서 0은 주로 검은색, 1은 흰색으로 사용됩니다. 이러한 점에서 비롯하여, DirectX는 0 ~ 1까지의 색상 표현에 유용하게 사용하기 위해, 근 평면 값을 -1이 아닌 0부터 시작합니다.

 

 

2) 공간의 좌표계

오른손 좌표계를 사용하는 뷰 공간의 경우, 카메라 앞에 있는 물체들의 \(z\) 축 값은 항상 마이너스(-) 값이며, 카메라로부터 멀어질수록 \(z\) 축 값이 작아집니다. 하지만, 위에서 구성한 3차원 NDC 공간의 경우에는 물체가 카메라보다 앞에 있을 때 양수의 \(z\) 값을 가지게 됩니다.

 

이 이야기는 NDC 공간에서는 왼손 좌표계를 사용한다는 의미로 받아들일 수 있습니다.

 

이 때까지 사용했던 공간에 대한 좌표계 복습 시간
  • 로컬 공간(Local Space) : 모델링 프로그램에 따라 초기 설정이 되고, FBX로 변환되어 게임 엔진에 들어오면 게임 엔진이 지정한 좌표계로 변경된다.
  • 월드 공간(World Space) : 게임 엔진이 지정한 좌표계 사용
  • 뷰 공간(View Space) : 대부분의 게임 엔진들은 오른손 좌표계를 사용
  • 클립 공간(Clip Space) : 왼손 좌표계 사용
    👉🏻 원근 투영 행렬을 설계할 때 \(z\) 값에다가 -1을 곱했기 때문
  • NDC 공간(Normalized Device Coordinate Space) : 왼손 좌표계 사용

 

 


2. 최종 원근 투영 행렬

기존에 사용했던 행렬은 \(4 \times 4\) 크기를 맞춰주기 위해, 사용하지 않는 한 행(row)을 사용하여 원근 투영 행렬을 구성하였습니다.

 

$$ P=\left[\begin{matrix}\frac{d}{a}&0&0&0\\0&d&0&0\\0&0&-1&0\\0&0&0&1\\\end{matrix}\right] $$

 

이제 깊이 값을 계산하기 위해, \(z\) 축 행에 해당하는 3번째 행에다가 미지수(\(i, \ j, \ k, \ l\))를 넣어보도록 하겠습니다.

 

$$ P\cdot\ v_{view}=\left[\begin{matrix}\frac{d}{a}&0&0&0\\0&d&0&0\\i&j&k&l\\0&0&-1&0\\\end{matrix}\right]\left[\begin{matrix}v_x\\v_y\\v_z\\1\\\end{matrix}\right]=\left[\begin{matrix}\frac{d}{a}\cdot v_x\\d\cdot v_y\\?\\-v_z\\\end{matrix}\right] $$

 

그런데 우리가 계산하는 것은 깊이 값이죠. 깊이 값은 \(z\) 축에 해당하며, 이는 \(x, \ y\) 축에 직교하기 때문에 \(x, \ y\) 값들은 깊이 값에 전혀 영향을 주지 않습니다. 따라서, \(x, \ y\) 값들은 0으로 처리할 수 있죠.

 

$$ P\cdot\ v_{view}=\left[\begin{matrix}\frac{d}{a}&0&0&0\\0&d&0&0\\0&0&k&l\\0&0&-1&0\\\end{matrix}\right]\left[\begin{matrix}v_x\\v_y\\v_z\\1\\\end{matrix}\right]=\left[\begin{matrix}\frac{d}{a}\cdot v_x\\d\cdot v_y\\?\\-v_z\\\end{matrix}\right] $$

 

이제 \(k, \ l\) 값을 구해야 하는데, 이것은 근 평면원 평면의 특징을 사용하여 구할 수 있습니다. \(k, \ l\) 을 구할 수 있게, 근 평면과 원 평면에서 샘플 데이터를 얻어주는 것이죠. 근 평면과 원 평면의 값들은 사용자가 지정해주는 값이기 때문에 상수라고 볼 수 있고, 근 평면에 대한 값 \(n\), 원 평면에 대한 값\(f\) 로 줬다면 다음과 같이 표현할 수 있습니다.

 

뷰 좌표계에서 카메라 시선 방향은 -이기 때문에 각각 -n, -f 가 된 것

 

NDC 좌표계에서는 \(-1 \sim 1\) 의 범위를 가지기 때문에, \( (0, \ 0, \ -n) \rightarrow (0, \ 0, \ -1) \) 이 되고, \( (0, \ 0, \ -f) \rightarrow (0, \ 0, \ 1) \) 이 됩니다. 이러한 기본 데이터를 활용해, \(k, \ l\) 을 구해봅시다.

 

$$ P=\left[\begin{matrix}\frac{d}{a}&0&0&0\\0&d&0&0\\0&0&k&l\\0&0&-1&0\\\end{matrix}\right]\left[\begin{matrix}0\\0\\-n\\1\\\end{matrix}\right]=\left[\begin{matrix}0\\0\\?\\n\\\end{matrix}\right] $$

$$ P=\left[\begin{matrix}\frac{d}{a}&0&0&0\\0&d&0&0\\0&0&k&l\\0&0&-1&0\\\end{matrix}\right]\left[\begin{matrix}0\\0\\-f\\1\\\end{matrix}\right]=\left[\begin{matrix}0\\0\\?\\f\\\end{matrix}\right] $$

 

위 행렬을 곱해져서 나온 결과의 네 번째 요소는 행렬 곱을 통해 얻은 결과이니, 각각 \(n, \ f\) 인 것은 확실합니다. 그렇다면, \( ? \) 표시된 부분을 구해야 하는데, 여기서 생각해 볼 것은 위 행렬을 곱해서 나온 결과는 클립 공간의 좌표라는 겁니다.

 

위에서 얻었던 근 평면과 원 평면의 샘플 데이터의 NDC 공간 \(z\) 좌표는 각각 \(-1, \ 1\) 이었습니다. 클립 공간에서 NDC 공간으로 변환할 때, 마지막 차원의 값으로 나눠주었던 걸 기억하시나요? 근 평면에서는 마지막 차원의 값인 \(n\) 으로 나누었을 때, \(z = -1\) 이 되어야 하니, 세 번째 요소 \(? = -n\) 이 되어야 합니다.

 

원 평면에서도 마찬가지로, 마지막 차원의 값인 \(f\) 로 나누었을 때, \(z = 1\) 이 되어야 하니, 세 번째 요소 \( ? = f\) 가 되어야 하죠. 이에 따라, 근 평면원 평면의 각각 클립 좌표는 다음과 같습니다.

 

$$ (0, \ 0, -n, \ n) $$

$$ (0, \ 0, \ f, \ f) $$

 

이제 이를 기반으로 원근 투영 행렬을 구성하는 두 개의 미지수 \(k, \ l\)을 구할 수 있는 식을 만들 수 있게 됩니다.

 

$$ P=\left[\begin{matrix}\frac{d}{a}&0&0&0\\0&d&0&0\\0&0&k&l\\0&0&-1&0\\\end{matrix}\right]\left[\begin{matrix}0\\0\\-n\\1\\\end{matrix}\right]=\left[\begin{matrix}0\\0\\-n\\n\\\end{matrix}\right] $$

$$ P=\left[\begin{matrix}\frac{d}{a}&0&0&0\\0&d&0&0\\0&0&k&l\\0&0&-1&0\\\end{matrix}\right]\left[\begin{matrix}0\\0\\-f\\1\\\end{matrix}\right]=\left[\begin{matrix}0\\0\\f\\f\\\end{matrix}\right] $$

 

이로부터 다음과 같은 연립 방정식을 구할 수 있습니다.

 

$$ \begin{cases} -kn + l = -n \\ -kf + l = f \end{cases} $$

 

각각을 풀면, 다음과 같이 \(k, \ l\) 값을 계산할 수 있습니다.

 

$$ k=\frac{-(n+f)}{(-n+f)}=\frac{n+f}{n-f} $$

$$ l=\frac{2nf}{(n-f)} $$

 

이를 행렬에 적용하면, 이제 깊이 값이 적용된 다음과 같은 원근 투영 행렬을 얻을 수 있습니다.

 

$$ P=\left[\begin{matrix}\frac{d}{a}&0&0&0\\0&d&0&0\\0&0&\frac{n+f}{n-f}&\frac{2nf}{n-f}\\0&0&-1&0\\\end{matrix}\right] $$

 

 

728x90
반응형