3D 수학과 소프트웨어 래스터라이저 · 벡터와 행렬 · 이론
왜 3차원인데 4x4 행렬인가
한 줄 요약
이동은 곱셈이 아니라 덧셈이라 3x3 행렬에 들어가지 않습니다. 좌표를 하나 늘려 (x, y, z, 1) 로 만들면 이동까지 곱셈 한 번으로 표현할 수 있고, 그래서 그래픽스의 변환 행렬은 4x4 입니다.
왜 이게 필요했나
물체 하나를 화면에 놓으려면 여러 가지를 연달아 해야 합니다. 크기를 줄이고, 돌리고, 제자리에 옮기고, 카메라 기준으로 다시 옮기고, 투영합니다. 이것을 함수 호출로 이어 붙이면 정점 하나마다 다섯 번의 서로 다른 계산이 붙습니다. 정점이 십만 개면 오십만 번입니다.
이 계산들이 전부 행렬 곱셈 하나로 표현되면 상황이 달라집니다. 행렬 곱은 결합법칙이 성립하므로 다섯 개를 미리 곱해 한 개로 만들어 둘 수 있고, 정점마다는 4x4 곱셈 한 번만 하면 됩니다. GPU 가 유니폼으로 MVP 행렬 하나만 받는 이유가 이것입니다. 문제는 이동이 곱셈이 아니라는 것입니다. 3x3 행렬로는 회전과 크기 조절은 되지만 평행이동은 표현할 수 없습니다.
어떻게 동작하나
해법은 차원을 하나 늘리는 것입니다. 점 (x, y, z) 를 (x, y, z, 1) 로 적고 4x4 행렬을 곱하면, 마지막 열이 그 1 과 곱해져 결과에 더해집니다. 그 열에 이동량을 넣으면 평행이동이 곱셈 안으로 들어옵니다.
| 1 0 0 tx | | x | | x + tx || 0 1 0 ty | * | y | = | y + ty || 0 0 1 tz | | z | | z + tz || 0 0 0 1 | | 1 | | 1 |이 표현을 동차 좌표(homogeneous coordinates) 라고 합니다. 네 번째 성분 w 는 그냥 자리 채우기가 아니라 의미가 있습니다. w 가 1 이면 점이고, 0 이면 방향입니다. 방향에는 이동을 적용하면 안 되는데(북쪽은 어디로 옮겨도 북쪽입니다), w=0 으로 두면 이동 열이 0 과 곱해져 저절로 무시됩니다. 노멀 벡터와 광원 방향을 (x, y, z, 0) 으로 다루는 이유입니다.
곱하는 순서가 결과를 바꿉니다. 행렬 곱은 교환법칙이 성립하지 않습니다. T * R 은 "먼저 돌리고 그다음 옮긴다" 이고 R * T 는 "먼저 옮기고 그다음 돌린다" 입니다. 열 벡터를 오른쪽에 곱하는 관례에서는 오른쪽에 있는 행렬이 먼저 적용됩니다. 점 (1,0,0) 에 z축 90도 회전과 x방향 2만큼 이동을 적용해 보면, 앞은 (2,1,0) 이고 뒤는 (0,3,0) 입니다. 물체가 제자리에서 도는지 궤도를 도는지가 여기서 갈립니다.
회전 행렬에는 손잡이가 하나 더 있습니다. 오른손 좌표계에서 z축 회전은 x축을 y축 쪽으로 돌립니다.
rotate_z(t) = | cos t -sin t 0 0 | | sin t cos t 0 0 | | 0 0 1 0 | | 0 0 0 1 |부호를 하나 잘못 넣으면 물체가 반대로 돕니다. 오류 메시지는 나오지 않습니다.
행 우선과 열 우선이라는 관례 차이도 있습니다. 이 실습은 행렬을 "길이 4짜리 리스트 네 개" 로 두고 점을 오른쪽에 곱합니다. 반대로 DirectX 계열의 문서와 코드는 흔히 점을 왼쪽에 놓고 행 벡터로 곱하는데, 그러면 같은 변환의 행렬이 전치된 모습이 됩니다. 두 관례 사이에서는 곱하는 순서도 뒤집힙니다. 남의 코드를 옮겨 올 때 결과가 이상하면 먼저 이것을 의심해야 합니다. 메모리에 어떤 순서로 담느냐는 또 다른 축이라, 관례를 두 개가 아니라 네 개로 셀 수도 있습니다.
회전을 표현하는 방법도 행렬만 있는 것은 아닙니다. 쿼터니언은 값 네 개로 회전을 담고, 두 회전 사이를 부드럽게 섞는 일(보간)이 행렬보다 훨씬 잘 됩니다. 행렬 두 개를 성분별로 섞으면 회전 행렬이 아닌 것이 나오기 때문입니다. 다만 GPU 에 넘길 때는 결국 행렬로 바꿔야 하므로, 상태는 쿼터니언으로 들고 그릴 때 행렬을 만드는 것이 흔한 구성입니다.
현장에서 만나는 모습
셰이더에서 노멀 벡터를 변환할 때 모델 행렬을 그대로 쓰면 조명이 틀립니다. 비균등 축소(예를 들어 x 만 절반으로 줄이는 것)가 들어가면 표면에 수직이던 벡터가 더 이상 수직이 아니게 되기 때문입니다. 올바른 답은 모델 행렬의 역행렬의 전치를 쓰는 것이고, 이 사실은 행렬을 직접 다뤄 보지 않으면 외워지지 않습니다.
또 하나는 부동소수점 누적입니다. 회전 행렬을 매 프레임 조금씩 곱해 누적하면 수백 프레임 뒤에는 직교성이 무너져 물체가 서서히 찌그러집니다. 그래서 엔진들은 각도나 쿼터니언을 상태로 들고 있다가 매 프레임 행렬을 새로 만듭니다. 행렬 자체를 상태로 두지 않습니다.
또 하나 자주 놓치는 것은 역행렬의 비용입니다. 일반적인 4x4 역행렬은 계산이 무겁지만, 그래픽스에서 다루는 대부분의 변환은 회전과 이동만으로 이루어져 있어 훨씬 싸게 뒤집을 수 있습니다. 회전 부분은 직교 행렬이라 전치가 곧 역행렬이고, 이동은 부호를 뒤집어 회전의 역을 적용하면 됩니다. 카메라 행렬을 만들 때 실제로 이 성질을 씁니다 — 앞에서 본 뷰 행렬이 카메라의 세 축을 행으로 세우고 마지막 열에 음수 내적을 넣는 모양인 것이 바로 그 계산입니다.
마지막으로 벡터 연산 중 무엇을 쓸지 고르는 감각도 필요합니다. 내적은 각도와 투영에, 외적은 수직 방향과 넓이에 쓰입니다. 두 벡터가 같은 방향을 보는지 알고 싶으면 내적의 부호를, 어느 쪽으로 도는지 알고 싶으면 외적의 부호를 봅니다. 이 두 문장이 그래픽스 코드의 상당 부분을 설명합니다.
다음 실습에서 할 것
3차원 벡터 연산과 4x4 행렬 곱을 직접 만들고, 이동·크기·회전 행렬을 조립합니다. 곱하는 순서를 바꿔 결과가 어떻게 달라지는지 숫자로 확인한 뒤, 변환한 사각형 세 개를 한 장의 PNG 에 그려 눈으로 봅니다. 마지막에는 외적으로 삼각형의 법선과 넓이를 구합니다.