https://gabrielfelipess.wordpress.com/2017/10/30/deriving-the-perspective-projection-matrix/
1. The Geometry of Projection
3차원 가상 공간에 있는 대부분의 물체는 정점으로 이루어져 있다. 이런 특징 덕분에 물체의 렌더링 작업은 반복적으로 물체의 정점을 순회하면서 우리가 원하는 방식으로 물체를 이동 시키면 된다. 또한 각 정점에 접근하는 작업을 동시에 진행할 수 있으므로 병행 작업을 통한 성능 향상에도 도움이 된다. 따라서 물체를 투영하려면 모든 정점을 투영해야 하며, 정점 사이에는 공간 관계가 필요하지 않기 때문에 투영 작업에서 정점의 접근 순서는 중요하지 않다.
이제 어떻게 투영을 어떻게 하는지 확인해보자. Figure 1
에 있는 $v$ 는 $v_{z}$ 와 $v_{y}$ 두 요소로 분해할 수 있고, 이렇게 되면 삼각형의 닮음을 이용하여 $v'$ 의 각 요소들을 구해볼 수 있다. 위 그림에서 오렌지 색의 선은 투영 평면(projection plane)을 의미하며, $d$ 는 투영 원점 $C$ 와 투영 평면 사이의 거리를 의미한다.
$$ v'_{y} = \frac{v_{y}}{v_{z}}d, v'_{x} = \frac{v_{x}}{v_{z}}d, v_{z}' = d$$
Figure 1
은 옆에서 바라본 그림으로, 위에서 바라보았을 때를 생각하면 $v'_{x}$ 를 쉽게 구할 수 있을 것이다.
흥미로운 부분은 $d$ 가 단순히 거리만 의미하는 것이 아니라 확대/축소의 기능까지 한다는 것이다. $d$ 의 값이 커질수록 더 많은 물체가 나타나게 된다.
이제 $v'$ 을 구하는 방법을 알았으니 행렬 변환 방법에 대해 알아볼 차례이다. 이를 위해 동차 좌표가 필요하다.
2. Homogeneous Coordinates
3차원 공간에서의 회전(rotation), 스케일링(scailing)과 같은 선형 변환 작업을 하기 위해서는 3 x 3 행렬이 필요하다. 그러나 이동(translation)과 같은 애파인 변환을 하기 위해서는 차원이 하나 더 필요하기 때문에 4 x 4 행렬을 사용해야 한다. 원근 투영은 선형 변환이 아니기 때문에 4 x 4 행렬을 사용한다.
여기서 동차 좌표계가 사용되면, 원근 투영의 개념과 동차 좌표의 표현이 매우 일치하게 된다. 다음 그림으로 보자.
Figure 1
에서 본 상황과 비슷하지만, 4차원 공간에 들어와 있고 $p$ 는 $w$ 라는 한 차원이 더 있는 3차원 벡터이며, 투영 원점 $C$ 를 가지고 있다. 그러나 여기에서 $C$ 는 $w = 0$인 무한대에 있는 점이라고 부른다. $w = 1$ 이면 $p$ 를 동차 형태로 간주하고 $p_{w}$ 라고 부른다.($w$ 를 1로 만들어 주는 작업을 해야 한다는 소리이다.)
따라서 $C$ 와 $p$ 사이에 있는 모든 점들은 동차 좌표계 변환을 통해 $p_{w}$ 가 되고 나면 모두 같아지게 된다. 더 수학적인 용어로 표현하면 다음과 같다.
$$ \forall{p} = (x, y, z, w) $$
같은 방향에 있는 모든 점 $p$ 는 4차원에 속해 있기 때문에 4개의 원소를 가지고 있으며, 동차 좌표계 변환을 하고 나면 다음과 같아진다.
$$ p_{w} = (\frac{x}{w}, \frac{y}{w}, \frac{z}{w}, 1) $$
이제 $w$ 가 0에 가까워질 수록 $p$ 가 $C$ 에 가까워지는 것을 볼 수 있다.
그러면 어떻게 동차 좌표계가 원근 투영 행렬을 만들 때 도움을 주는 것인가? 앞에서 $v$ 를 $v'$ 으로 만들면서 원근 투영된 정점으로 표현하는 것을 확인했다. 그리고 GPU 수준에서 실행되는 z-divide에 대해서도 들어보았을 것이다. 우리가 $z$ 값을 $w$ 좌표에 저장했다고 한다면, z-divide는 동차 좌표계 변환과 같다고 할 수 있다.
- 변환을 하기 전에 다음과 같은 정점을 가지고 있다.(원점)
$$ v = (x, y, z, 1) $$ - 투영을 하면 다음과 같은 정점을 가진다.(투영된 정점)
$$ v = (x, y, z, \frac{z}{d}) $$ - 이 정점에 대해 동차 좌표계 변환을 하면 다음과 같은 정점을 가질 수 있다.
$$ v = (d\frac{x}{z}, d\frac{y}{z}, d, 1) $$
이제 각 단계들을 세부적으로 살펴보자. 첫 번째로 흥미로운 부분은 1번 단계의 정점 $v$ 는 동차 좌표계로 변환되어 있고 3차원 공간에 있다. 그러나 2번 단계에서는 투영 공간에 있지 않은 것처럼 보인다. 그러나 3번 단계에서 동차 좌표계 변환을 하고 나면 이 정점은 다시 3차원으로 돌아오지만, 원점과 다른 투영된 위치를 가지게 된다.
또 다른 중요한 부분은 실제 투영 작업은 3번 단계에서 동차 좌표계 변환을 하며 발생하고, 3번 단계는 일반적으로 GPU의 전용 하드웨어에서 진행된다는 것이다.
그래서 이제 목표는 1번 단계에 있는 정점 $v$ 를 2번 단계에 있는 정점 $v$ 로 만들어 줄 수 있는 행렬 $P$ 를 찾는 것이다. 2단계 정점 $v$ 까지만 만들 수 있다면, 3단계로 변환하는 작업은 GPU에서 진행해줄 것이기 때문이다.
$$ (x, y, z, \frac{z}{d}) = P(d)v $$
컬럼 메이저 형태로 작성하면 다음과 같은 행렬 $P$ 가 우리가 원하는 원근 투영 행렬이 된다.
$$ P = \begin{pmatrix}
1 & 0 & 0 & 0 \\
0 & 1 & 0 & 0 \\
0 & 0 & 1 & 0 \\
0 & 0 & \frac{1}{d} & 0
\end{pmatrix} $$
행렬 $P$ 는 원근 투영 행렬 1이다. 그러나 DX나 OpenGL의 문서에서 볼 수 있는 원근 투영 행렬과는 다른 형태를 가지고 있다. 무엇이 빠져있는 것일까? 문서에 있는 행렬들은 투영보다 더 많은 작업을 하기 때문이다. 이 행렬들은 절두체(frustum) 모양의 공간을 직육면체 모양의 공간으로 변환을 하여 클리핑 작업을 쉽게 할 수 있게 만드는 작업도 함께 하기 때문이다. DX에서는 어떻게 이 작업을 하는지 살펴보자.
3. From Frustum to Canonical Cuboid
이제 거의 다 왔다. 절두체 공간을 직육면체 공간으로 변환하는 방법을 찾아야 한다. 변환 과정에서 물체 사이의 크기와 거리와 관련된 비율을 유지해야 한다. 즉, 절두체 공간에서 직육면체 공간으로 매핑을 해야 한다.
$p_{1}$ 은 $(r, t, n)$ 이다. $f$ 는 오타이다.
우리는 절두체 공간과 두 개의 흥미로운 점 $p_{0}$ 와 $p_{1}$ 을 가지고 있다. 이 두 점은 가까운 평면(near plane)의 min, max에 위치하고 있다. 이 평면에 위치한 모든 점들을 정규화된 공간으로 위치하기 위해 우리는 반드시 이 점들을 near plane에서 정규화된 직육면체 공간으로 매핑을 해야 한다.
물체가 화면에 보여지게 하려면 반드시 View Furstum 내부에 정점이 포함되어야 하고 이것이 절두체 공간을 사용하는 이유가 된다. 따라서 투영 원점 $C$ 에서 어떤 점을 잇는 선을 그렸을 때, 이 선이 반드시 near plane과 만나야 하며, 그렇지 않은 경우에는 절두체 공간 외부에 존재하기 때문에 화면에 보이지 않게 된다.
이제 왜 절두체 공간에 있는 물체를 near plane
에 매핑해야 되는지 알게 됐으니, 계산을 할 차례이다.(near plane
외부에 있으면 투영 평면에도 포함이 안 됨)
절두체 단면에서 우리는 다음과 같은 점들을 가질 수 있다. 절두체 공간 내부에 있는 물체의 정점 좌표는 이 범위 안에 포함되게 된다.(이 때 z 축에 대한 값은 잠깐 잊는다. 절두체 단면을 절두체를 x축 방향으로 자른 것이라고 생각해보자.)
$$ \forall{p}(x, y) {p(x, y)/l < x < r \land b < y < t} $$
모든 점 $p$ 는 이제 near plane
의 left와 right 사이의 x값, bottom과 top 사이의 y값을 가져야만 한다. 그리고 이 범위를 가진 점 $p$ 를 다음과 같은 범위를 가질 수 있도록 매핑해야 한다. 그래야만 절두체 공간 안에 있는 물체의 좌표를 투영 평면으로 옮길 수 있기 때문이다.
$$ \forall{p}(x, y) { p(x, y) / -1 < x < 1 \land -1 < y < 1 } $$
x에 대해서 -1에서 1 사이의 범위를 가지도록 정규화를 하면 다음과 같은 절차를 가지게 된다.
$$ l < x < r $$
$$ 0 < x-l < r-l $$
$$ 0 < \frac{x-l}{r-l} < 1 $$
$$ 0 < 2\frac{x-l}{r-l} < 2 $$
$$ -1 < 2\frac{x-l}{r-l} - 1 < 1 $$
y에 대해서도 동일한 방식으로 정규화를 진행하면 near plane
에 위치한 점들의 x, y 좌표는 다음과 같이 직육면체 공간에 존재하게 될 것이다.
$$ x_{c} = 2\frac{x-l}{r-l} - 1 \land y_{c} = 2\frac{y-b}{t-b} - 1 $$
이제 z에 대해서 작업을 할 차례이다. 하지만 먼저 확인해야 되는 부분은 DX나 OpenGL에서 z 값의 범위를 -1에서 1 사이가 아닌 0에서 1 사이로 매핑을 한다는 점이다. 이 점을 기억하고 진행하면 z를 정규화하기 위해 $n$ 과 $f$ 라는 새로운 변수를 사용해야 한다. $n$ 은 near plane
이고 $f$ 는 far plane
이다. 그래서 절두체 공간 안에 있는 z는 다음과 같은 범위를 가지게 된다.
$$ n < z < f$$
$$ 0 < z - n < f - n $$
$$ 0 < \frac{z-n}{f-n} < 1 $$
그러므로 정규화된 공간에 존재하는 우리의 정점은 다음과 같은 형태가 된다.
$$ v_{c} = (2\frac{x-l}{r-l}-1, 2\frac{y-b}{t-b}-1, \frac{z-n}{f-n}) $$
이제 할 일은 3차원 공간에 있는 정점 $v$ 를 정규화된 공간에 존재하는 정점 $v_c$ 로 변환할 수 있는 행렬을 찾는 것이다. DX에서 사용하는 row-major 표기법을 따른 행렬로 나타내면 다음과 같다.
$$ \begin{pmatrix} 2\frac{x - l}{r - l} - 1 & 2\frac{y - b}{t - b} - 1 & \frac{z - n}{f - n} & 1 \end{pmatrix} = \begin{pmatrix} x & y & z & 1 \end{pmatrix} \begin{pmatrix} \frac{2}{r - l} & 0 & 0 & 0 \\ 0 & \frac{2}{t - b} & 0 & 0 \\ 0 & 0 & \frac{1}{f - n} & 0 \\ -\frac{r + l}{r - l} & -\frac{t + b}{t - b} & -\frac{n}{f - n} & 1 \end{pmatrix} $$
위의 행렬은 절두체 공간에 있는 물체를 정규화된 직육면체 공간으로 매핑할 수 있는 행렬이다. 그러나 이 행렬은 정투영을 위한 행렬이다. 물체의 거리를 고려하지 않고 오로지 직육면체 공간에 투영을 하고 있기 때문이다. 원근 투영을 위해서는 추가적인 작업을 진행해주어야 한다.
x 케이스에 대해서 먼저 분석해 보자.(y도 동일한 방식으로 진행할 수 있다.) 위의 행렬을 전개하면 x는 다음과 같다.
$$ x_p = \frac{2x}{r - l} - \frac{r + l}{r - l} $$
그런데 여기서 우리가 동차 좌표계를 이용해서 원근 투영을 할 때 어떻게 했는지 기억해보자. 투영을 할 때 투영 원점 $C$ 와 투영 평면과의 거리 $d$ 를 통해 투영된 x 좌표를 구할 수 있었다. 그리고 이렇게 구한 좌표를 동차 좌표계로 변환하기 위해서 정점의 각 요소들을 $\frac{z}{d}$ 로 나누기도 했다. 이는 곧 정점의 각 요소가 $d$ 에 의해 스케일되고, 투영 원점 $C$ 로부터 원본 정점 $z$ 에 의해 나누어지는 것을 의미한다.($d$ 가 곱해지고 $z$ 가 나누어진다.)
따라서 원래의 정점 $v$ 는 $(x, y, z, 1)$ 로 구성되어 있지만, 원근 투영된 좌표는 $(d\frac{x}{z}, d\frac{y}{z}, d, 1)$ 로 구성이 된다. 그리고 이 값을 대신 넣어주면 $x_{p}$ 는 다음과 같아지게 된다.
$$ x_p = \frac{2}{r - l}(\frac{dx}{z}) - \frac{r + l}{r - l} $$
y에 대해서도 동일하게 적용하여 $y_{p}$ 를 구하면 다음과 같다.
$$ y_p = \frac{2}{t - b}(\frac{dy}{z}) - \frac{t + b}{t - b} $$
이제 z에 대한 계산을 할 차례이다. 앞에서 본 것처럼 GPU는 자체적으로 z-divide를 통해 동차 좌표계 변환을 진행해준다. 그러나 $x_{p}$ 와 $y_{p}$ 를 보면 이 두 값이 모두 원근 투영된 값을 적용하였기 때문에 GPU에서 진행해야 할 z-divide가 이미 적용되어 있는 것을 볼 수 있다. 따라서 계산하는 방법을 바꿔야 할 필요가 있다. $x_{p}$ 와 $y_{p}$ 에 $z$ 를 곱해주면 x-divide가 진행되어도 정상적인 좌표를 가지게 된다.
$$ x_{p}z = \frac{2d}{r - l}x - z\frac{r + l}{r - l} $$
$$ y_{p}z = \frac{2d}{t - b}y - z\frac{t + b}{t - b} $$
이제 $z_{p}$ 를 볼 차례이다. 앞에서 적용했던 것과 동일한 방식으로 $z_{p}$ 를 만들려고 한다.(스케일링(p) + 이동(q))
$$ z_{p}z = pz + q $$
그런데 이 작업을 $z_{p}$ 를 $d$ 로 설정하지 않고 $z_{p}$ 로 직접 진행하는 것일까? 우리는 절두체 공간에서 직육면체 공간으로 매핑하는 것이 작동하는 이유를 확인했다. 따라서 절두체 공간을 벗어나는 정점은 시야에서 벗어나기 때문에 깊이에 대한 값을 0에서 1 사이의 범위에 매핑해야 한다는 것을 기억해야 한다. 또한 클리핑을 위해서 $z = n$ 인 경우에는 $z_{p} = 0$ 이고, $z = f$ 인 경우에는 $z_{p} = 1$ 이라는 사실 역시 기억해야 한다.
위에서 본 것처럼 $z_{p}$ 는 0에서 1 사이의 값을 가져야 한다. 따라서 $z_p$ 를 $d$ 와 같이 고정된 값으로 대신할 수 없다.
$z = n$ 인 경우에는 다음과 같이 작업을 할 수 있다. 이 과정을 통해 $q$ 를 찾을 수 있다.
$$ z_{p}z = pz + q $$
$$ 0n = pn + q $$
$$ 0 = pn + q $$
$$ q = -pn$$
이제 $q = -pn$ 으로 대체하고 $z = f$ 인 경우를 보면 다음과 같다. 이 과정을 통해 $p$ 를 찾을 수 있다.
$$ z_{p}z = pz + q$$
$$ 1f = pf + q $$
$$ f = pf - pn $$
$$ f = p(f-n) $$
$$ p = \frac{f}{f-n} $$
따라서 다음과 같은 결과를 얻을 수 있다.
$$ z_{p}z = \frac{f}{f - n}z -\frac{nf}{f - n} $$
최종으로 얻게 된 DX에서의 원근 투영 행렬은 다음과 같다. 투영 평면이 $d = n$ 인 경우다.
$$ P_{[0,1]} = \begin{pmatrix} \frac{2n}{r - l} & 0 & 0 & 0 \\ 0 & \frac{2n}{t - b} & 0 & 0 \\ -\frac{r + l}{r - l} & -\frac{t + b}{t - b} & \frac{f}{f - n} & 0 \\ 0 & 0 & -\frac{fn}{f - n} & 1 \end{pmatrix} $$
여기에 투영 평면이 원점을 기준으로 대칭적인 형태를 가지고 있다면, $|t| = |b|$ 이고 $|r| = |l|$ 이기 때문에 행렬을 단순화 할 수 있다.
$$ P_{[0,1]} = \begin{pmatrix} \frac{2n}{r - l} & 0 & 0 & 0 \\ 0 & \frac{2n}{t - b} & 0 & 0 \\ 0 & 0 & \frac{f}{f - n} & 0 \\ 0 & 0 & -\frac{fn}{f - n} & 1 \end{pmatrix} $$
이제 마지막으로 fov의 관점에서 원근 투영을 생각해보자. 투영 평면의 너비와 높이를 기준으로 보는 것이다. 이제 투영 평면의 높이와 너비를 $t - b$ 과 $r - l$ 이 아닌 $h$ 와 $w$ 를 사용할 것이다. 대칭인 경우에도 마찬가지이다.
간단한 그림이다. 삼각함수를 이용하면 각도를 구할 수 있다.
$$ tan(\frac{\theta}{2}) = \frac{w}{2n} $$
여기에 역삼각함수를 이용하면 $\theta$ 를 구할 수 있다.
$$ \frac{1}{tan(\frac{\theta}{2})} = cot(\frac{\theta}{2}) = \frac{2n}{w} $$
이 값이 익숙한 느낌이 들 것이다. 이는 위에서 구한 대칭 투영 행렬의 스케일링 팩터이기 때문이다.($w = r - l$, $h = t - b$) 스케일링 팩터를 대체하여 작성하면 행렬은 다음과 같아진다.
$$ P_{[0,1]} = \begin{pmatrix} cot(\frac{\theta_{w}}{2})& 0 & 0 & 0 \\ 0 & cot(\frac{\theta_{h}}{2}) & 0 & 0 \\ 0 & 0 & \frac{f}{f - n} & 0 \\ 0 & 0 & -\frac{fn}{f - n} & 1 \end{pmatrix} $$
이제 높이에 대한 fov와 aspect ratio $\alpha$ 를 이용하면 너비에 대한 fov 를 구할 수 있다.
$$ \alpha = \frac{w}{h} $$
$$\frac{cot(\frac{\theta_{h}}{2})}{\alpha} = \frac{\frac{2n}{h}}{\frac{w}{h}} = \frac{2n}{w}$$
그리고 이를 반영한 행렬이 DX에서 현재 사용하고 있는 원근 투영 행렬이다.
$$ P_{[0,1]} = \begin{pmatrix} \frac{cot(\frac{\theta_{h}}{2})}{\alpha}& 0 & 0 & 0 \\ 0 & cot(\frac{\theta_{h}}{2}) & 0 & 0 \\ 0 & 0 & \frac{f}{f - n} & 0 \\ 0 & 0 & -\frac{fn}{f - n} & 1 \end{pmatrix} $$
'그래픽스' 카테고리의 다른 글
yaw, pitch, roll (0) | 2024.06.09 |
---|---|
림(rim) 효과 (0) | 2024.06.03 |
퐁(Phong) 반사 (0) | 2024.06.01 |
조명(Lighting) (0) | 2024.06.01 |
Spinning Cube (0) | 2024.05.17 |