Log Stash

as an Industrial Personnel

프로그래밍

렌더링 파이프라인에서 왜 동차 좌표계를 쓸까

SavvyTuna 2017. 7. 9. 23:29

용책을 비롯한 각종 DirectX책, 3D 수학책들을 보면 대부분 렌더링 파이프라인 내부에서의 좌표계 변환을 필수적으로 설명하고 있다. 아마 이런 책들을 봤으면 이동변환이나 투형변환을 설명 할 때,

  • 벡터의 마지막 성분(w)을 1로 하나 추가한다,
  • 이것 때문에 3차원 벡터지만 변환행렬은 4x4 크기를 가진다,
  • 0이면 벡터고 1이면 점이다,

등의 설명과 '이게 동차 좌표계와 관련이 있다' 라는 말도 들어봤을 것이다. 나는 이게 이해하기가 어려웠어서 처음 책을 봤을땐 하나도 알아듣지 못하고 그냥 대충 '아.. 그냥 선 위의 점들을 한 평면으로 몰아넣는구나' 정도로만 이해하고 넘겼었다. 그러다 갑자기 이유없이 그냥 대체 왜 동차좌표계라는걸 쓰는지 궁금해져서 찾아보게 되었다.

사실 나름 찾아본다고 찾아보긴 했는데 아직도 직관이 잘 잡혀있지 않아 헷갈린다. 혹시나 틀린점이 있다면 태클 바람.

1. 동차 좌표계

위키에 따르면, 동차좌표계는 뫼비우스가 사영 기하학에서 좌표 시스템으로 써먹으려고 고안한 개념이라고 한다. (마치 유클리드 공간에서 직교 좌표계를 사용하는 것 처럼) 이를 사용하면 투영 공간에서 평행한 직선들이 교차하는 무한 원점(points at infinity)의 위치를 좌표값으로 표현할 수 있다.

80's grid

위의 80년대 느낌 물씬 나는 그림처럼, 유클리드 공간에서 평행선들이 나란히 있다고 생각해보자. 알다시피 평행선들은 좌표값이 얼마나 크든, 작던 간에 절대 서로 교차하지 않는다. 하지만 우리가 눈으로 보는, 원근감이 적용되는 투영공간에선 다르다. 평행선도 눈으로 보면 저 멀리있는 하나의 소실점에서 서로 만나게 된다.

유클리드 공간의 직교 좌표계에서 이를 표현할 방법은 없다. 직선의 방정식을 구해서 x값을 무한히 멀리 보내도 두 평행선은 당연히 만나지 않는다. 그냥 각자 무한을 향해 갈 뿐. 그래서 동차 좌표계라는 개념을 사용한다.

동차 좌표계는 직교 좌표계의 좌표에서 마지막 차원 w를 첨가한 형태로 나타낸다. 예를들어 직교 좌표계 위의 어떤 정점의 좌표를 (x, y, z)라고 하자. 이것을 동차 좌표계위의 한 점으로 변환시키려면 w값으로 0이 아닌 실수를 하나 정한 후, 이것을 모든 성분에 곱해준 다음, w를 마지막 차원으로 첨가시켜준다.

$$(x, y, z) \rightarrow (x \cdot w, y \cdot w, z \cdot w, w) $$

동차 좌표계위의 좌표를 다시 우리가 잘 알고 있는 직교 좌표계로 변환하는것도 간단하다. w를 각 성분에 나눠주고, w를 다시 떼버린다.

$$(x, y, z, w) \rightarrow (\frac{x}{w}, \frac{y}{w}, \frac{z}{w})$$

이렇게 하는게 무슨 의미일까? 동차 좌표계위의 두 점 (1, 1, 1, 1)과 (2, 2, 2 ,2)를 직교 좌표계로 변환해보자. 둘 다 (1, 1, 1)로 변환된다. 약간만 일반화 시켜보자. 0이 아닌 w에 대해 동차 좌표계 위의 (1 * w, 1 * w, 1 * w, w)는 직교 좌표계의 (1, 1, 1)로 변환된다. 즉, 동차 좌표계를 직교 좌표계로 변환하는 것은, 동차 좌표계 위의 한 직선에 있는 모든 점들을 w = 1인 평면의 한 점으로 몰아넣는것과 같다. 한 차원 늘려서 말하자면, 어떤 공간위의 점들을 하나의 평면에 대응시킨다. 투영 단계에서 해야하는 일이라고 볼 수 있다. (그게 원근투영이든 직교투영이든 상관 없이)

추가로, 위에서 두 평행선의 교차점을 표현하기위해 동차 좌표계를 사용한다고 언급했다. 예를들어 아래처럼 두개의 다른 2차원 직선이 있다고 생각해보자.

$$
\left\{
    \begin{array}{ll}
        Ax+By+C=0\\
        Ax+By+D=0
    \end{array}
\right.
(C \neq D)
$$당연히, 이 두 직선은 서로 평행하며 교차하지 않는다.

그리고 직교 좌표계 위의 점 (x, y)가 동차 좌표계 위의 점 (X, Y, W)로 대응 된다고 보면,

$$(x,y)=(\frac{X}{W}, \frac{Y}{W})\rightarrow(X,Y,W)$$

x,y를 각각 X/W, Y/W로 바꿔 쓸 수 있다. 위의 직선에서 x,y를 치환해보면,

$$
\left\{
    \begin{array}{ll}
        A\frac{X}{W}+B\frac{Y}{W}+C=0\\
        A\frac{X}{W}+B\frac{Y}{W}+D=0
    \end{array}
\right.
(C \neq D)
$$

이렇게 표현할 수 있고, 양변에 W를 곱해주면,

$$
\left\{
    \begin{array}{ll}
        AX+BY+CW=0\\
        AX+BY+DW=0
    \end{array}
\right.
(C \neq D)
$$

이렇게 모든 항의 차수가 같은 방정식을 만들 수 있게 된다. 이 때, 두 직선은 (X, Y, 0)에서 서로 만나게 된다. 이렇게 w가 0인 좌표를 무한원점(Point at infinity, Ideal point)이라고 한다. (그리고 이런 무한원점들이 이루는 선을 Line at infinity라고 한다)

W가 0인게 무슨 의미일까? W가 0인 점들을 다시 유클리드 공간의 직교 좌표계로 나타내려면 X,Y를 W로 나눠야하지만.. W가 0이라서 나누질 못한다. 그러면 W가 0에 계속 가까워진다고 생각해보자. 분모가 0에 가까워질 수록 X/W, Y/W의 값은 점점 커져서 원점과 멀어진다. 그러다 W가 0이 되는 순간 직교 좌표계에서의 점의 위치는 (∞,∞)이 된다. 이렇게 동차 좌표계에서 W를 0으로 만드는것으로 직교 좌표계에서의 무한의 위치를 표현할 수 있다.

2. 왜 쓸까?

그럼 이 동차 좌표계를 렌더링 파이프라인에서 왜 쓸까? 아니 정확히 궁금했던건 누구 때문에 w하나를 추가하나? 였다 물론 왠만한 3d 프로그래밍 책을 정독하면 이해하고 있었겠지만, 내가 책을 대충 읽는 습관이 있기 때문에 한참 동안 몰랐었다. 아무튼, 왜 쓰냐면 위치변환(translation)과 투영변환(projection)이 단순 선형변환이 아니기 때문에 3x3 행렬로 표현할 수 없는데, 이러면 렌더링 파이프라인이 복잡해지니, 얘네들의 변환을 행렬로 표현하기 위해 사용한다. 사실 책에서 변환 부분 설명할 때 행렬의 모양을 잘 보면 이 두 변환 외에는 w값을 건드리는 경우가 없다. (뷰 행렬도 건드리지만 이건 카메라 위치 변환 때문이니 넘어가자)

2.1 위치변환

위치변환은 선형변환으로 표현할 수 없다. 왜냐? 선형변환은 벡터 공간들 사이의 변환을 다루는데, 벡터에는 위치가 없다. 위치를 이동하는것을 선형변환으로 표현할 수 없다. 그래서 한 차원을 추가하고 아핀변환을 하는것으로 위치를 이동시킨다.

어떤 좌표 posdelta를 더한다고 생각해보자. 수식으로 생각하면 간단하다. pos += delta;가 되겠지. 하지만 버텍스 셰이더의 로컬, 월드, 뷰, 투영 행렬들의 곱셈 연산 속에서 위치변환 때문에 사이사이에 덧셈 연산이 끼어들면 보기도 좋지 않고, 수식도 조건부로 나뉘면서 복잡해진다. 그냥 다른 선형변환들처럼 행렬 하나로 나타낼 수 있다면 나중에 월드 행렬 하나로 퉁칠 수 도 있고 좋겠지. 그렇지만 3x3 행렬로 어떤 좌표을 '이동'시키는 변환 행렬을 만드는건 불가능하다. (궁금하면 한번 계산해보시길. 만들어도 아마 딱 그 점 하나만 이동시킬 수 있고 다른 점에 적용할 수 없는 변환 행렬이 될것이다)

$$
\begin{bmatrix}
    1 & 0 & 0 & delta_x \\
    0 & 1 & 0 & delta_y \\
    0 & 0 & 1 & delta_z \\
    0 & 0 & 0 & 1
\end{bmatrix}
\begin{bmatrix}
pos_x \\ pos_y \\ pos_z \\ 1
\end{bmatrix} =
\begin{bmatrix}
pos_x + delta_x \\ pos_y + delta_y \\ pos_z + delta_z \\ 1
\end{bmatrix}
$$

첨자를 보통 반대로 쓰겠지만 멤버 변수를 참조하는 느낌으로 저렇게 썼다 그냥

그래서 위 처럼 행렬들에게 차원을 하나 추가한다. pos 열행렬 마지막 차원으로 w를, w의 값으로 1을 추가한다. 행렬 곱셈식을 전개해보면 delta의 요소들이 w와 곱해져서 pos값에 달라 붙는다.

(정확히는 차원을 하나 늘린 동차좌표 공간속에서 밀기 변환을 수행하는것인데, 그건 나중에 시간나면 정리.. 궁금하면 링크참조)

2.2 투영변환

투영 변환은 3차원 좌표계를 2차원 평면에 투영시키는 변환이다. 위치변환을 수식으로 나타내면 그냥 단순 덧셈이었던 것 처럼, 투영 변환도 그냥 수식으로 나타내면 간단하다. d를 원점과 투영평면과의 거리, a를 종횡비(aspect ratio)라고 했을때,

$$(x,y,z)\rightarrow(\frac{d \cdot x}{a \cdot z},\frac{d \cdot y}{z})$$

요렇게 x,y값을 거리 z로 나눠주는 변환이라고 볼 수 있다. (물론 실리적인 이유 때문에 z값도 NDC 좌표계에 맞춰 살려놓긴 하지만 그것보단 '거리에 따라서 적당히 x,y값을 조절' 하는게 투영변환의 목적일테니 넘어가자)

위치변환과 마찬가지로, 얘는 선형변환이 아니다. 그렇다고 아핀변환도 아니다. 게다가 z값은 정점 좌표마다 다를텐데, z로 나눠줘야 한다니. 정점 갯수만큼 투영행렬을 만들어야하나? 알다시피, 그렇지 않다. 그냥 렌더러에게 투영행렬 하나 만들어서 건네주는것으로 투영 변환을 할 수 있다. 생각해보면 d를 곱하는것, d/a를 곱하는것은 충분히 아핀변환으로 표현할 수 있다. z로 나누는게 까다로울 뿐이지. 이 때 동차 좌표계의 개념을 사용하면 된다.

$$(x,y,z,w)\rightarrow(\frac{d \cdot x}{a}, d \cdot y, d \cdot z, z)$$

일단 투영행렬을 곱해주는것으로 기존 동차 좌표계위의 좌표에 필요한 값들을 계산한다. z로 나눠주는것 빼고. 대신 w를 1말고 z값으로 바꿔놓는다. (보통 버텍스 셰이더를 보면 여기까지만 계산하고 렌더러에게 좌표를 넘긴다) 그러면 렌더러는 나중에 자기가 알아서 이 동차좌표를 직교좌표로 바꾼다. 동차좌표에서 직교좌표로 바꿀땐, 위에서 말했듯 각 성분에 w값을 나누고, w차원을 빼버린다. 이러면서 각 성분을 z값을 가진 w로 나누게 되고, 아까 투영 변환에서 못 했던 z로 나누기를 여기서 수행하게 된다.

$$(\frac{d \cdot x}{a}, d \cdot y, d \cdot z, z)\rightarrow(\frac{d \cdot x}{a \cdot z},\frac{d \cdot y}{z})$$

이렇게 렌더링 파이프라인을 굴리는데 동차 좌표계의 특징을 써먹으면 애매한 부분이 깔끔하게 해결되기 때문에 사용한다.

3. 벡터? 점?

적어도 내가 본 렌더링 관련 서적에서 동차 좌표계라는 말이 나오면 거의 필수적으로 따라 나오는 말이 있다. w=0인 동차 좌표계의 좌표는 아핀 공간에서의 벡터로 대응시킬 수 있다. 반대로, w가 0이 아닌값이면 점(point)을 나타낸다. 이게 수학적으로 합의된 사항인지는 잘 모르겠다. 다만 적어도 두 개 이상의 3D 프로그래밍 관련 서적이 이렇게 언급했으면 그렇게 이해해도 괜찮겠지.

위에서 말했듯 동차 좌표계위에 있는 어떤 좌표의 w가 0이 아닌 어떤 실수값이면, 그 좌표는 직교 좌표계로 변환될 수 있다. 이걸 아핀 공간에서의 점(point)이라고 볼 수 있고. 하지만 만약 w가 0이면 그 점은 직교 좌표계 위에서 표현할 수 없으며, 어떤 방향으로 무한히 멀어지는 상태만 남는다. 이걸 벡터라고 볼 수 있는게 아닌가 싶다.

아무튼, 아핀공간에서 벡터와 점의 연산은 다음과 같은 관계를 가진다.

  1. 점 = 점 + 벡터
  2. 벡터 = 벡터 + 벡터
  3. 벡터 = 점 - 점

이 때 w값이 0인지 아닌지를 이용해서 점과 벡터를 구분하는 방법은, 위의 관계를 표현하는데 딱히 문제 없다.

  1. 점과 벡터를 더하면 '점'을 '벡터'만큼 이동시키게 된다.

$$
\begin{bmatrix}
P_x \\ P_y \\ P_z \\ 1
\end{bmatrix}
+\begin{bmatrix}
V_x \\ V_y \\ V_z \\ 0
\end{bmatrix}
=\begin{bmatrix}
P_x + V_x \\ P_y + V_y \\ P_z + V_z \\ 1
\end{bmatrix}
$$

  1. 벡터와 벡터를 더하면 그만큼의 벡터가 나온다.

$$
\begin{bmatrix}
V_1x \\ V_1y \\ V_1z \\ 0
\end{bmatrix}
+\begin{bmatrix}
V_2x \\ V_2y \\ V_2z \\ 0
\end{bmatrix}
=\begin{bmatrix}
V_1x + V_2x \\ V_1y + V_2y \\ V_1z + V_2z \\ 0
\end{bmatrix}
$$

  1. 두 점을 빼면 그만큼의 벡터가 나온다

$$
\begin{bmatrix}
P_1x \\ P_1y \\ P_1z \\ 1
\end{bmatrix}
+\begin{bmatrix}
P_2x \\ P_2y \\ P_2z \\ 1
\end{bmatrix}
=\begin{bmatrix}
P_1x - P_2x \\ P_1y - P_2y \\ P_1z - P_2z \\ 0
\end{bmatrix}
$$

이외에도 w=0인 벡터에 회전변환과 크기 변환을 수행하면 잘 되지만, 이동변환을 수행해 봤자 이동하고자 하는 값들이 w와 곱해지면서 죄다 무력화 된다. (궁금하면 해보시길)

이렇게 놓고 보니 이 방법이 점과 벡터가 공존하는 아핀공간에서 이 둘을 구분하기에 제일 적절한 수단이 아닐까 싶다.

추가로, 내가 잠깐 착각했던 것처럼 '동차 좌표계의 (1, 1, 1, 1)과 (2, 2, 2, 2)를 더하면 (3, 3, 3, 3)인데 그걸 직교 좌표계로 바꾸면 다시 (1, 1, 1)이니까 말도 안되는거 아냐?' 라고 생각하면 안된다. 점과 점은 더할 수 없으니까. 링크 참조.

References

글은 별거 없지만 헷갈리는 개념이라 이것저것 링크를 많이 참조했다. (원래는 각주 기능을 사용해야 하겠지만 아직 마크다운에서 티스토리로 변환할 때 각주에 대한 처리를 안 해놔서 그냥 일단 이렇게 씀)

기본적인 개념 설명

본문에는 적지 않았지만, 점과 선이 각각 원점을 통과하는 선과 면이라는 설명을 해준 링크

소실점과 무한원점에 대해서 잘 설명해준 수업자료들

투영에 대한 개념 찾기에 좋은 아티클들


'프로그래밍' 카테고리의 다른 글

Using Linq in Unity3d  (0) 2017.10.29
쿼터니온(Quaternion) 정리  (5) 2017.08.28
SAT(Separating Axis Theorem) 충돌처리 구현  (0) 2017.06.21
C#의 람다 변수 캡쳐  (0) 2017.05.29
Code jam 2016 quals round (D, Fractiles)  (0) 2016.11.11