※ 본 포스팅은 learnopengl.com을 번역 및 가감한 포스팅이며, learnopengl에서는 번역 작업 참여를 적극 장려하고 있습니다!
아래 링크에서 원문을 확인할 수 있습니다.
Transformations
이제 object를 생성해서 color나 texture를 입히기까지 성공했습니다. 근데 가만히 있으면 재미없죠. frame마다 vertex 정보에 변화를 주는 방법도 있지만 이는 성능상 효율적이지 않습니다. 그래서 object에 변화를 주는 방법으로 matrix를 이용합니다.
matrix는 처음에 어려울 수 있지만 익숙해지면 상당히 다방면으로 적용 할 수 있습니다. 하지만 matrix 이전에 vector에 대해서 좀 더 이해가 필요합니다. 여기서는 수학적인 지식을 좀 다루도록 하겠습니다.
Vectors
vector는 기본적으로 방향과 크기를 가집니다. 우리는 보통 2~4차원 안의 vector를 다루게 되고, 차원에 따라 vector가 갖는 요소의 개수가 다릅니다.
아래 그림은 2D 벡터들이죠. z값은 모두 없지만 3D에서 z=0으로 생각하면 됩니다. vector는 방향을 나타내므로 시작점이 다르더라도 방향과 크기가 같으면 같은 vector입니다. 즉 아래에서 는 \(\bar{w}\)와 \(\bar{v}\) vector는 같은 vector입니다.
보통 vector를 표현할 때 \(\bar{v}\)와 같이 위에 막대를 추가해서 표현하거나 화살표로 합니다. 그리고 수식으로 표현하면 이렇습니다.
$$\bar{\vec{v}} = \left( \begin{array}{c} x \\ y \\ z \end{array} \right)$$
vector를 시각화하려면 보통 시작을 원점으로 가정하고 특정 방향으로 가리키도록 표현하죠.
가령 vector (3,5)는 (0,0)에서 (3,5)를 가리키는 vector입니다.
(vector와 matrix에 대한 수학적인 내용은 learn opengl 원문에서 보시고, opengl 포스팅에서는 바로 transformation을 다루도록 하겠습니다)
Matrix-Vector multiplication
matrix는 기본적으로 N * 1 행렬과 동일합니다. N개의 요소를 가진 vector가 N * 1 행렬인 셈이죠. 그러면 이제 vector와 matrix을 곱할 수 있습니다. 방법은 M * N matrix와 N * 1 vector를 곱하는 것입니다. matrix의 colume과 vector이 row의 수가 같으므로 multiplication할 수 있습니다.
왜 vector에 matrix를 곱할까요? 이 과정이 바로 transformation의 핵심이기 때문입니다. matrix를 vector에 곱하여 2D / 3D transform 을 할 수 있습니다. 다음 과정들을 보면서 이해해봅시다.
Identity matrix
opengl에서는 4x4 matrix를 많이 사용하는데 이유는 단지 vector가 4개의 요소를 가졌기 때문입니다. tranformation중 가장 단순한 identity matrix는 대각선 값 1 이외에는 모두 0으로 이루어진 N x N matrix인데, 이걸 vector에 곱하면 곱하전 전과 같은 vector가 나옵니다.
$$\left[\begin{array}{cccc} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{array}\right] \cdot \left[\begin{array}{c} 1 \\ 2 \\ 3 \\ 4 \end{array}\right] = \left[\begin{array}{c} 1 \cdot 1 \\ 1 \cdot 2 \\ 1 \cdot 3 \\ 1 \cdot 4 \end{array}\right] = \left[\begin{array}{c} 1 \\ 2 \\ 3 \\ 4 \end{array}\right]
$$
vector가 왜 그대로일까요? 그건 대각선의 특징입니다. 첫 번째 행의 값은 첫번째 값 빼고 0이므로 첫 번째만 반영되고, 같은 식으로 N 번째 행의 값은 N번째 값만 1이 곱해지니 그대로 vector가 나오는 겁니다.
identity matrix는 값이 변하지 않는데 왜 필요할까요? 보통 identity matrix는 transformation matrix를 처음 생성하기 시작하는 출발점으로 사용되고, 선형대수에서 증명이나 방정식을 풀 때 유용하답니다.
Scaling
scaling은 vector 방향은 유지한 채 크기를 늘리거나 줄이는 걸 말합니다. 2차원이나 3차원에서 적용할 것이므로 x,y,z 많아야 3 개의 변수의 vector를 scaling하겠죠.
vector \(\bar{v}=(3,2)\)를 scaling한다고 합시다. x축으로 0.5배, y축으로 2배 scaling한다고 하면 \(\bar{s}=(0.5, 2)\)로 scaling하는 것입니다.
위 그림은 2D 이지만 opengl은 기본 3D입니다. 그러면 s의 z축 scale은 1로 설정해서 변경하지 않도록 하면 됩니다. 그림처럼 각 축의 scale 연산의 비율이 다른 것은 non-uniform scale이라고 합니다. 반대로 모든 축에 대해 동일한 비율로 변환한다면 uniform scale 이라고 합니다.
이제 scaling을 수행하는 transformation matrix가 필요합니다. 이전에 identity matrix에서 대각선의 요소들이 각 행에 맞는 vector 요소들과 곱해지기 때문에 그대로 라고 했었죠? 이번에는 그 값이 1이었던 identity matrix와 달리 3으로 설정하면, 각 vector 요소들에는 3이 곱해집니다. 그럼 3배로 scaling됩니다. scaling 변수를 \((S_1, S_2, S_3)\)으로 표현하면 vector \((x,y,z)\)에 대해 다음과 같이 수식을 정의할 수 있습니다.
$$\left[\begin{array}{cccc} S_1 & 0 & 0 & 0 \\ 0 & S_2 & 0 & 0 \\ 0 & 0 & S_3 & 0 \\ 0 & 0 & 0 & 1 \end{array}\right] \cdot \left(\begin{array}{c} x \\ y \\ z \\ 1 \end{array}\right) = \left(\begin{array}{c} S_1 \cdot x \\ S_2 \cdot y \\ S_3 \cdot z \\ 1 \end{array}\right)
$$
여기서 4 번째 scaling 요소는 값이 여전히 1인데요, w라는 요소인데 이건 추후 다른 용도로 사용되기 때문에 지금은 1로 유지하는 겁니다.
Translation
translation은 vector에 vector를 더해서 새로운 위치로 이동시키는 것입니다. scaling matrix 처럼 4x4 matrix를 사용하는데, 형태는 좀 다릅니다. translation matrix는 요소들이 4번째 행의 1~3열의 값에 주어집니다. translation vector를 \((T_x,T_y,T_z)\)라고 하면 다음과 같이 정의할 수 있습니다.
$$\left[\begin{array}{cccc} 1 & 0 & 0 & T_x \\ 0 & 1 & 0 & T_y \\ 0 & 0 & 1 & T_z \\ 0 & 0 & 0 & 1 \end{array}\right] \cdot \left(\begin{array}{c} x \\ y \\ z \\ 1 \end{array}\right) = \left(\begin{array}{c} x + T_x \\ y + T_y \\ z + T_z \\ 1 \end{array}\right)$$
이렇게 되는 이유는 4번째 행의 값들은 w값인 1과 곱해져서 translation vector의 값은 변하지 않고, 원래의 vector 요소들과 더해져서 translation 연산이 완성되기 때문입니다. translation은 단순히 값을 더한다고 보면 됩니다. 만약 3x3 matrix라면 이런 연산을 하지 못했겠죠.
Homogeneous Coordinates (동차 좌표)
vector의 w요소는 homogeneous coordinate라고 합니다. homegeneous coordinate에서는 3D vector를 얻기 위해서 x,y,z 좌표를 w 좌표로 나누어서 구할 수 있습니다. 일반적으로는 w가 1이므로 겉으로 변화가 보이지는 않습니다.
homogeneous coordinate를 이용하는 이유는 위에서 보았듯 3D vector의 matrix translation을 할 수 있게 해주고, 추후 3D perspective(원근법)를 만들 수 있게 해줍니다.
또 homogeneous coordinate가 0이면 vector는 크기를 가지지 않고 direction vector가 됩니다. w좌표가 0이면 translation을 할 수 없으니까요.
translation matrix를 사용하면 x,y,z 방향으로 이동할 수 있으니 transformation matrix에서 유용하게 사용됩니다.
Rotation
이전 translation과 scaling보다는 rotation은 좀 더 이해하기 어려운 편입니다. 이해가 어렵다면 선형대수에서 회전과 관련된 항목을 공부해보고 오시는 것을 추천드립니다.
vector에서 rotation이라는 것은 각도 degrees또는 radians으로 표현합니다. 원의 각은 360 degree 혹은 2π radian이죠. 우리는 여기서 degree를 사용해서 진행하겠습니다.
대부분 rotation 함수에서는 radians 각을 필요로하긴 해도, degrees와 radians는 아래 방법으로 쉽게 변환 할 수 있죠.
angle in degrees = angle in radians * (180 / PI)
angle in radians = angle in degrees * (PI / 180)
여기서 PI는 약 3.14159265359 입니다.
360도를 기준으로 회전하도록 합시다. 아래 그림은 2D vector \(\bar{v}\)가 \(\bar{k}\)에서 시계 방향으로 72도 회전한 것입니다. 72도 이면 원의 1/5 각도이죠.
3D에서 rotation은 각도와 회전 축을 지정해야합니다. 일정 각도만큼 주어진 축을 기준으로 object를 rotation하는 겁니다.
rotation 에서는 vector rotation을 위해서 삼각함수를 이용합니다. 삼각함수라고 하면 sin과 cos 함수입니다. rotation matrix가 왜 이렇게 구성되는지까지는 여기서다루기 어렵고 우리는 3D 공간에서 각 축에 대해 rotation matrix가 정의되고 각도는 θ(세타)로 정의해서 다음과 같이 나타낸다는 것을 알면 됩니다.
X축 중심 rotation
$$\left[\begin{array}{cccc} 1 & 0 & 0 & 0 \\ 0 & \cos \theta & -\sin \theta & 0 \\ 0 & \sin \theta & \cos \theta & 0 \\ 0 & 0 & 0 & 1 \end{array}\right] \cdot \left(\begin{array}{c} x \\ y \\ z \\ 1 \end{array}\right) = \left(\begin{array}{c} x \\ \cos \theta \cdot y - \sin \theta \cdot z \\ \sin \theta \cdot y + \cos \theta \cdot z \\ 1 \end{array}\right)
$$
Y축 중심 rotation
$$\left[\begin{array}{cccc} \cos \theta & 0 & \sin \theta & 0 \\ 0 & 1 & 0 & 0 \\ -\sin \theta & 0 & \cos \theta & 0 \\ 0 & 0 & 0 & 1 \end{array}\right] \cdot \left(\begin{array}{c} x \\ y \\ z \\ 1 \end{array}\right) = \left(\begin{array}{c} \cos \theta \cdot x + \sin \theta \cdot z \\ y \\ -\sin \theta \cdot x + \cos \theta \cdot z \\ 1 \end{array}\right)
$$
Z축 중심 rotation
$$\left[\begin{array}{cccc} \cos \theta & -\sin \theta & 0 & 0 \\ \sin \theta & \cos \theta & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{array}\right] \cdot \left(\begin{array}{c} x \\ y \\ z \\ 1 \end{array}\right) = \left(\begin{array}{c} \cos \theta \cdot x - \sin \theta \cdot y \\ \sin \theta \cdot x + \cos \theta \cdot y \\ z \\ 1 \end{array}\right)
$$
각 축에 따라 rotation matrix를 사용해서 회전시키는 matrix를 다릅니다. 그래서 여러 축을 포함해서 회전한다면 X,Y,Z 축에 대해서 따로따로 순서를 정해서 rotation matrix 를 적용하는 식으로 할 수 있는데, 이러면 회전축이 겹쳐버리는 gimbal lock 이라는 문제가 생깁니다. 이 gimbal lock이라는 문제를 해결하기 위해서 rotation matrix를 결합하는 방법이 아니라 (0.662, 0.2, 0.722) 이런식의 임의의 unit axis을 하나 정하고 그 축을 중심으로 rotation하는 방법이 있습니다.
그렇게 하려면 아래와 같이 matrix를 사용하여 할 수 있는데, 임의의 회전 축 \((R_x, R_y, R_z)\)를 중심으로 rotation하는 수식입니다.
$$\left[\begin{array}{cccc} \cos \theta + R_x^2 (1 - \cos \theta) & R_x R_y (1 - \cos \theta) - R_z \sin \theta & R_x R_z (1 - \cos \theta) + R_y \sin \theta & 0 \\ R_y R_x (1 - \cos \theta) + R_z \sin \theta & \cos \theta + R_y^2 (1 - \cos \theta) & R_y R_z (1 - \cos \theta) - R_x \sin \theta & 0 \\ R_z R_x (1 - \cos \theta) - R_y \sin \theta & R_z R_y (1 - \cos \theta) + R_x \sin \theta & \cos \theta + R_z^2 (1 - \cos \theta) & 0 \\ 0 & 0 & 0 & 1 \end{array}\right]
$$
이 rotation matrix를 어떻게 만들었냐? 이건 지금 다루기 버거운 주제입니다.. 이렇게 만든 matrix도 100%로 gimbal lock을 해결한다는 것은 아니지만 대부분의 문제는 해결됩니다. 100%로 gimbal lock 문제를 해결하기 위해서 나온 것은 quaternion이라는 더 효율적인 연산방식인데, 이건 이번 transformation에서 다룰 내용은 아닙니다.
Combining matrices
matrix를 사용해서 transformation을 하는 것은 여러 개의 transformation matrix를 하나의 matrix로 결합해서 계산할 수 있다는 장점이 있습니다. 여러 transformation을 결합해봅시다. 이렇게 가정해봅시다. (x,y,z) vector가 있고 이걸 2배 scaling 하고 (1,2,3)만큼 translation하는 상황입니다. 필요한 matrix는 scaling과 translation matrix로 이렇게 matrix를 결합하면 됩니다.
$$Trans. \ Scale = \left[\begin{array}{cccc} 1 & 0 & 0 & 1 \\ 0 & 1 & 0 & 2 \\ 0 & 0 & 1 & 3 \\ 0 & 0 & 0 & 1 \end{array}\right] \cdot \left[\begin{array}{cccc} 2 & 0 & 0 & 0 \\ 0 & 2 & 0 & 0 \\ 0 & 0 & 2 & 0 \\ 0 & 0 & 0 & 1 \end{array}\right] = \left[\begin{array}{cccc} 2 & 0 & 0 & 1 \\ 0 & 2 & 0 & 2 \\ 0 & 0 & 2 & 3 \\ 0 & 0 & 0 & 1 \end{array}\right]$$
주의할 점은 각 transformation matrix를 곱할 때 종류 간의 순서가 있다는 겁니다. matrix multiply는 교환 법칙이 성립하지 않으므로 순서가 바뀌면 결과가 바뀝니다. matrix 곱의 순서는 오른쪽 matrix가 먼저 vector에 곱해지기 때문에 오른쪽에서 왼쪽으로 순서가 진행됩니다. matrix combining을 할 때 순서는 scaling, rotation, translation 순서로 수행해야합니다. 순서를 따르지 않으면 서로 엉킬 수 있기 때문이죠.
결합한 matrix를 vector에 적용하면 다음과 같이 나옵니다.
$$\left[\begin{array}{cccc} 2 & 0 & 0 & 1 \\ 0 & 2 & 0 & 2 \\ 0 & 0 & 2 & 3 \\ 0 & 0 & 0 & 1 \end{array}\right] \cdot \left[\begin{array}{c} x \\ y \\ z \\ 1 \end{array}\right] = \left[\begin{array}{c} 2x + 1 \\ 2y + 2 \\ 2z + 3 \\ 1 \end{array}\right]
$$
이렇게 해서 2배 scaling과 (1,2,3) tanslation이 결합되어 matrix를 적용하는 방법을 공부했습니다.