투영 행렬
고친 사람 github-actions[bot]
투영 행렬은 카메라에 보이는 3차원 범위를 그래픽 장치가 약속한 작은 상자 안으로 옮겨 줍니다. 장면의 점마다 이 행렬을 한 번 곱하면 화면에 보일 점과 안 보일 점이 갈립니다. 먼 것을 작게 그리는 준비도 이 곱셈이 맡습니다. 선형대수에서는 같은 이름이 점을 평면 위로 떨어뜨리는 다른 행렬을 가리킵니다.
쉽고 빠른 이해
투영 행렬은 카메라가 보는 범위를 가로·세로 -1부터 1까지인 상자 안으로 줄여 넣습니다. 가로로 긴 16:9 화면이면 넓게 퍼진 가로 범위를 세로보다 더 많이 좁혀서 같은 상자에 맞춥니다.
이 상자가 있어서 그래픽 장치는 카메라가 얼마나 넓게 보는지, 창이 얼마나 큰지 몰라도 됩니다. 상자 밖에 나간 것은 버립니다. 안에 든 것은 창 크기로 늘리기만 하면 됩니다.
어떻게 도나:
- 보는 각도, 화면의 가로세로 비율, 가장 가까운 거리, 가장 먼 거리로 행렬을 만듭니다
- 장면의 꼭짓점마다 이 행렬을 곱합니다
- 그래픽 장치가 결과를 거리만큼 나눠 먼 것을 작게 만듭니다. 그다음 상자 안 위치를 창 크기로 늘립니다
3차원 게임은 먼 것이 작아지는 원근 투영 행렬을 씁니다. 도면이나 메뉴 화면은 거리와 상관없이 크기가 같은 직교 투영 행렬을 씁니다. 다 그린 화면 전체에 효과를 입히는 후처리는 행렬 없이 상자 좌표를 바로 적습니다.
대가는 둘입니다. 첫째, 창의 가로세로 비율이 바뀌면 행렬을 다시 만들어야 합니다. 안 그러면 그림이 한쪽으로 늘어납니다.
둘째 대가는 깊이 값에서 생깁니다. 깊이 값은 카메라에서의 거리를 상자 안의 수로 옮긴 것입니다. 겹친 두 물체 중 어느 것이 앞인지를 이 수로 가릅니다.
이 행렬은 먼 곳의 깊이 값을 좁은 범위에 몰아넣습니다. 그래서 멀리 있는 두 면은 어느 것이 앞인지 가르기 어렵습니다.
상세
이 절은 투영 행렬이 점 하나를 어디로 옮기는지 따라갑니다. 행렬이 무엇이고 왜 곱셈으로 적는지부터 봅니다. 그다음 행렬에 숫자를 넣어 점 하나를 옮겨 봅니다.
뒤쪽 소절은 이 행렬을 누가 언제 만들고 곱하는지, 쓰는 도구마다 무엇이 다른지를 봅니다. 마지막 소절은 선형대수에서 같은 이름이 가리키는 다른 행렬을 짚습니다.
사진관에 사진을 맡기면 휴대폰으로 찍었든 필름으로 찍었든 먼저 정해진 규격의 인화지에 맞춰 뽑습니다. 그 사진을 거실 벽에 걸지 책상에 세울지는 뽑은 다음에 정합니다. 찍는 사람과 거는 사람은 인화지 규격 하나만 알면 서로를 몰라도 됩니다.
투영 행렬은 앞쪽 일을 맡습니다. 카메라에 보이는 범위를 정해진 크기의 상자에 맞춰 넣는 일입니다. 그 상자를 창 안의 그림 영역 크기로 늘리는 일은 뒤의 뷰포트 변환이 맡습니다. 창 안에서 그림을 그리는 이 직사각형 영역을 뷰포트라고 부릅니다.
점을 옮기는 행렬
행렬은 숫자를 가로세로로 늘어놓은 표입니다. 점의 좌표에 행렬을 곱하면 새 좌표가 나옵니다. 표에 어떤 숫자를 넣느냐에 따라 점을 늘이거나 돌리거나 옮기는 계산이 됩니다.
행렬로 적는 까닭은 합칠 수 있어서입니다. 늘이기 행렬과 돌리기 행렬을 미리 곱해 두면 그 결과 행렬 하나가 두 계산을 다 합니다. 장면의 꼭짓점이 아무리 많아도 꼭짓점마다 곱셈 한 번이면 됩니다.
3차원 점을 행렬로 옮기려면 좌표 셋에 넷째 성분 w 를 붙여 (x, y, z, 1) 로 적습니다. 동차 좌표는 이렇게 넷째 성분을 붙인 좌표입니다. 성분이 셋뿐이면 점을 평행하게 옮기는 계산을 곱셈으로 적을 수 없어서 붙인 성분입니다.
그래서 그래픽스의 투영 행렬은 가로세로 네 칸씩인 4×4 행렬입니다. 넷째 성분 w 는 뒤에서 먼 것을 작게 만드는 데에도 쓰입니다.
모델·뷰·투영 세 행렬
장면의 점은 화면에 닿기까지 행렬 셋을 차례로 지납니다. 투영 행렬은 그 셋 중 마지막입니다.
첫째는 모델 행렬입니다. 물체마다 자기 기준으로 적힌 꼭짓점 좌표를 장면 전체가 함께 쓰는 좌표로 옮깁니다.
옮기기 전과 뒤의 좌표계에는 이름이 따로 있습니다. 물체 자신을 기준으로 삼은 좌표계가 모델 좌표계입니다. 장면 전체가 함께 쓰는 좌표계는 월드 좌표계입니다.
둘째는 뷰 행렬입니다. 월드 좌표를 카메라 기준 좌표로 바꿉니다. 카메라를 원점에 두고 카메라가 보는 방향을 깊이 축으로 삼은 좌표계입니다. 이 좌표계가 뷰 공간입니다.
셋째가 투영 행렬입니다. 뷰 공간의 점을 받아 약속된 상자 기준의 좌표로 바꿉니다. 앞의 두 행렬은 점을 옮기고 돌리기만 합니다. 무엇이 보이고 얼마나 크게 보일지는 투영 행렬이 정합니다.
세 행렬을 미리 곱해 하나로 합쳐 두기도 합니다. 이 합친 행렬을 흔히 MVP(Model-View-Projection, 모델·뷰·투영) 행렬이라고 부릅니다.
보이는 범위와 약속된 상자
투영 행렬은 두 입체를 잇습니다. 카메라가 보는 범위와, 그래픽 장치가 약속해 둔 상자입니다. 이 소절은 두 입체가 각각 무엇으로 정해지는지를 봅니다.
투영에는 두 갈래가 있습니다. 원근 투영은 먼 것을 작게 그립니다. 직교 투영은 거리와 상관없이 크기를 바꾸지 않고 그립니다.
카메라가 보는 범위를 뷰 볼륨이라고 부릅니다. 이 범위 밖의 물체는 화면에 나오지 않습니다. 직교 투영에서 뷰 볼륨은 반듯한 직육면체입니다.
원근 투영의 뷰 볼륨은 꼭대기를 잘라 낸 피라미드 모양입니다. 카메라 쪽이 좁습니다. 멀어질수록 넓어집니다. 이 모양을 따로 뷰 절두체라고 부릅니다.
원근 투영의 범위는 값 넷으로 정합니다. 아래 표가 그 넷입니다.
| 값 | 무엇을 정하나 |
|---|---|
| 근평면 | 카메라에서 이만큼 떨어진 곳부터 그립니다 |
| 원평면 | 이보다 먼 것은 그리지 않습니다 |
| 시야각 | 카메라에서 위아래로 벌어진 각도입니다. 넓히면 더 많이 담기는 대신 하나하나가 작아집니다 |
| 종횡비 | 화면의 가로 대 세로 비율입니다. 위아래 폭에 이 비율을 곱해 좌우 폭을 정합니다 |
직교 투영은 시야각과 종횡비 대신 상자의 가로·세로 폭을 직접 적습니다. 근평면과 원평면은 똑같이 씁니다.
약속된 상자는 가로와 세로가 -1부터 1까지인 입체입니다. 이 상자 안의 좌표를 정규화 장치 좌표라고 부릅니다.
상자의 셋째 축에는 깊이 값이 담깁니다. 깊이 값은 카메라에서의 거리를 상자 안의 수로 옮긴 것입니다. 두 물체가 화면에서 겹칠 때 어느 것이 앞인지를 이 수로 가릅니다. 깊이 값의 범위는 뒤의 「API 마다 다른 약속」 소절에서 봅니다.
이 상자를 두는 까닭은 뒤 단계를 단순하게 만들려는 것입니다. 뒤 단계는 그래픽 장치, 곧 GPU(Graphics Processing Unit, 그래픽 처리 장치)가 맡습니다. GPU 는 그래픽 계산을 맡는 전용 칩입니다.
GPU 는 카메라가 몇 도로 보는지, 창이 몇 픽셀인지 알 필요가 없습니다. 상자 밖이면 버립니다. 안이면 창 크기로 늘리면 끝납니다.
원근 투영 행렬의 모양
이 소절은 원근 투영 행렬에 들어가는 숫자를 하나씩 봅니다. 아래 행렬은 약속 두 가지를 따른 모양입니다.
첫째, 뷰 공간에서 카메라는 깊이 축의 + 쪽을 봅니다. 둘째, 깊이 값은 0부터 1까지로 모읍니다. 다른 약속을 쓰면 숫자가 어떻게 바뀌는지는 뒤의 「API 마다 다른 약속」에서 봅니다.
[ sx 0 0 0 ]
[ 0 sy 0 0 ]
[ 0 0 a b ]
[ 0 0 1 0 ]
첫 두 줄의 sy 와 sx 는 세로와 가로를 줄이는 배율입니다. sy 는 1 ÷ tan(시야각 ÷ 2) 입니다. tan 은 각도를 기울기로 바꾸는 삼각함수입니다. 시야각이 넓을수록 sy 가 작아져 장면이 작게 담깁니다.
sx 는 sy ÷ 종횡비입니다. 가로로 긴 화면은 가로로 더 넓게 보므로, 그만큼 가로를 더 줄여야 같은 상자에 들어갑니다.
맨 아랫줄 0 0 1 0 이 원근의 열쇠입니다. 이 줄을 곱하면 w 칸에 카메라에서 점까지의 거리 z 가
담깁니다. 뒤 단계가 좌표를 w 로 나누므로, 먼 점일수록 큰 수로 나뉘어 작아집니다.
직교 투영 행렬은 맨 아랫줄이 0 0 0 1 입니다. 곱한 뒤에도 w 가 1 로 남아서, 나눠도 값이 안
바뀝니다. 두 투영 행렬을 가르는 것은 이 한 줄입니다.
셋째 줄의 a 와 b 는 깊이 값을 만드는 상수입니다. 두 상수를 적으려면 거리 둘이 필요합니다. 카메라에서 근평면까지의 거리를 n, 원평면까지의 거리를 f 라고 둡니다.
a 는 f ÷ (f − n), b 는 −n × f ÷ (f − n) 입니다. 이렇게 고르면 w 로 나눈 뒤에 근평면 위의 점은 깊이 값 0 을, 원평면 위의 점은 깊이 값 1 을 받습니다.
점 하나를 옮겨 보기
이 소절은 위 행렬에 숫자를 넣고 뷰 공간의 점 (4, 1, 4) 를 옮겨 봅니다. 시야각은 90도, 화면은 16:9 로 둡니다. 시야각의 절반인 45도의 tan 이 1 이라서 sy 는 1 입니다.
const sy = 1;
const sx = sy / (16 / 9); // 0.5625
// 뷰 공간의 점 (4, 1, 4)
const x = sx * 4; // 2.25
const y = sy * 1; // 1
const w = 1 * 4; // 4
x / w; // 0.5625
y / w; // 0.25
행렬을 곱한 직후의 가로는 2.25 입니다. -1부터 1까지인 상자 밖의 값처럼 보입니다. w 인 4 로 나누고 나서야 0.5625 가 되어 상자 안으로 들어옵니다.
같은 점을 카메라에서 두 배 멀리 옮기면 w 도 두 배가 됩니다.
// 두 배 먼 점 (4, 1, 8)
2.25 / 8; // 0.28125
1 / 8; // 0.125
가로와 세로가 둘 다 절반이 됐습니다. 행렬은 w 칸에 거리를 옮겨 담기만 했습니다. 먼 것을 작게 만든 것은 뒤의 나눗셈입니다.
곱한 뒤에 이어지는 단계
투영 행렬을 곱한 직후의 네 성분 좌표를 클립 공간 좌표라고 부릅니다. 아직 w 로 나누기 전의 좌표입니다.
3차원 물체는 작은 삼각형을 이어 붙여 적습니다. 그래서 투영 행렬을 곱하는 대상도 삼각형의 꼭짓점입니다.
클립 공간에서는 뷰 볼륨 밖으로 삐져나간 삼각형을 잘라 냅니다. 이 잘라 내기를 클리핑이라고 부릅니다. 클립 공간이라는 이름도 여기서 나왔습니다.
잘라 낸 뒤에 GPU 가 x·y·z 를 w 로 나눕니다. 이 나눗셈을 원근 나눗셈이라고 부릅니다. 나눈 결과가 정규화 장치 좌표입니다.
나눗셈을 행렬 밖으로 뺀 데는 까닭이 있습니다. 행렬 곱셈은 좌표에 정해진 수를 곱해 더하는 계산뿐이라, 점마다 다른 값으로 나누는 일을 담을 수 없습니다. 그래서 행렬은 나눌 값을 w 에 담아 두기만 합니다. 나눗셈은 GPU 에 맡깁니다.
마지막으로 뷰포트 변환이 상자를 뷰포트의 픽셀 크기로 늘립니다. 그 뒤 래스터화 단계가 삼각형이 어느 픽셀을 덮는지 골라냅니다. 점 하나가 거치는 좌표와 행렬을 한 줄로 모으면 이렇습니다.
flowchart TD
A["모델 좌표계 · 물체 기준"] -->|"모델 행렬"| B["월드 좌표계 · 장면 공통"]
B -->|"뷰 행렬"| C["뷰 공간 · 카메라 기준"]
C -->|"투영 행렬"| D["클립 공간 · 나누기 전 네 성분"]
D -->|"원근 나눗셈"| E["정규화 장치 좌표 · 약속된 상자"]
E -->|"뷰포트 변환"| F["뷰포트의 픽셀 위치"]
만드는 때와 곱하는 곳
투영 행렬은 CPU(Central Processing Unit, 중앙 처리 장치)에서 만듭니다. 입력은 시야각·종횡비·근평면·원평면 넷뿐입니다.
그래픽용 수학 라이브러리는 대개 이 넷을 받아 행렬을 돌려주는 함수를 둡니다. C++ 의 GLM(OpenGL Mathematics)
에 있는 glm::perspective, 자바스크립트의 gl-matrix 에 있는 mat4.perspective 가 그런 함수입니다.
만든 행렬은 정점 셰이더에 넘깁니다. 정점 셰이더는 꼭짓점마다 한 번씩 GPU 에서 도는 작은 프로그램입니다. 「정점」은 꼭짓점을 뜻합니다.
한 번 그리는 동안 모든 꼭짓점이 같이 쓰는 값을 유니폼이라고 부릅니다. 투영 행렬이 그런 값입니다. 꼭짓점마다 다른 것은 좌표입니다. 곱하는 행렬은 모두 같습니다.
투영 행렬은 카메라 설정이 바뀔 때만 다시 만듭니다. 흔한 경우가 창 크기 변경입니다. 뷰포트의 가로세로 비율과 행렬에 넣은 종횡비가 어긋나면 그림이 한쪽으로 늘어납니다.
API 마다 다른 약속
행렬의 모양은 같아도 세부 숫자는 그래픽 API(Application Programming Interface, 프로그램이 기능을 불러 쓰는 창구)마다 다릅니다. 프로그램은 GPU 를 직접 다루지 않고 API 를 거쳐 부립니다. 그래서 약속된 상자의 세부도 GPU 를 부리는 API 가 정합니다.
이 소절은 갈리는 축 셋을 봅니다. 어긋났을 때 화면에 무엇이 나타나는지는 끝의 표로 모읍니다.
첫째는 깊이 값의 범위입니다. OpenGL 은 기본값으로 -1부터 1까지를 씁니다. Direct3D·Vulkan·Metal 은 0부터 1까지를 씁니다. 앞에서 본 a 와 b 는 0부터 1까지를 따른 값이라, 다른 범위를 쓰면 a 와 b 가 바뀝니다.
둘째는 카메라가 보는 방향입니다. OpenGL 쪽 관례는 뷰 공간에서 카메라가 깊이 축의 − 쪽을 보게 둡니다. + 쪽을 보게 두는 약속도 있습니다. 앞의 행렬은 + 쪽을 따랐습니다.
− 쪽을 보는 약속이면 맨 아랫줄이 0 0 -1 0 이 됩니다. 그래야 카메라 앞의 점이 양수 w 를 받습니다.
셋째는 메모리에 담는 순서입니다. 4×4 행렬의 수 열여섯 개를 한 줄로 적을 때, 가로줄부터 적는 행 우선 순서와 세로줄부터 적는 열 우선 순서가 있습니다.
| 갈리는 축 | 어긋나면 화면에 나타나는 것 |
|---|---|
| 깊이 값의 범위 | 0부터 1까지로 만든 행렬을 -1부터 1까지 쓰는 API 에 주면 깊이 값 범위의 절반만 씁니다. 반대로 주면 깊이 값이 0 보다 작게 나오는 카메라 가까운 쪽이 잘려 나갑니다 |
| 카메라가 보는 방향 | 앞에 있는 점이 음수 w 를 받아 카메라 뒤로 계산됩니다. 아무것도 안 보입니다 |
| 담는 순서 | 행과 열이 뒤바뀐 행렬이 곱해져 그림이 엉뚱하게 일그러집니다 |
깊이 값의 치우침
w 로 나누고 나면 깊이 값은 a + b ÷ z 꼴이 됩니다. z 는 앞에서처럼 카메라에서의 거리입니다. 이 값은 깊이 버퍼에 적힙니다. 깊이 버퍼는 화면 픽셀마다 지금까지 그린 것 중 가장 가까운 것의 깊이 값을 적어 두는 메모리입니다.
b ÷ z 는 z 가 커질수록 느리게 변합니다. 그래서 깊이 값의 범위 대부분을 가까운 곳이 차지합니다. 근평면 1, 원평면 100 이면 거리 4 인 점이 벌써 깊이 값 0.76 쯤을 받습니다. 거리 4부터 100 까지는 남은 0.24 안에 몰려 담깁니다.
깊이 버퍼는 정해진 비트 수로 값을 적습니다. 그래서 먼 곳의 두 면이 같은 깊이 값으로 뭉개질 수 있습니다. 그러면 어느 면이 앞인지가 픽셀마다 달리 갈려 얼룩덜룩한 무늬가 생깁니다. 이 무늬를 Z 파이팅이라고 부릅니다.
치우침을 가장 크게 좌우하는 것은 근평면입니다. 근평면을 카메라에 가까이 둘수록(n 이 0 에 가까울수록) 깊이 값이 카메라 바로 앞에 더 몰립니다. 그래서 멀리까지 그리는 장면이면 근평면을 카메라에서 되도록 멀리 둡니다(n 을 키웁니다).
쓰는 곳과 안 쓰는 곳
어느 투영 행렬을 고를지는 화면이 무엇을 전해야 하는지가 정합니다.
| 화면 | 쓰는 행렬 | 까닭 |
|---|---|---|
| 3차원 게임 · 건물 둘러보기 | 원근 투영 행렬 | 먼 것이 작아야 거리감이 생깁니다 |
| 설계 도면 | 직교 투영 행렬 | 화면 위 길이를 재서 실제 길이로 읽어야 합니다 |
| 메뉴 · 버튼 · 2차원 게임 | 직교 투영 행렬 | 상자를 창의 픽셀 크기로 잡으면 좌표 1 이 픽셀 1 과 맞습니다 |
투영 행렬을 아예 안 쓰는 경우도 있습니다. 다 그린 화면 위에 흐림이나 색 보정 같은 효과를 덧입히는 단계를 후처리라고 부릅니다. 이 단계는 화면을 꽉 채우는 사각형 하나를 그립니다.
그 사각형의 꼭짓점은 처음부터 정규화 장치 좌표로 적습니다. 네 모서리를 (-1, -1) 부터 (1, 1) 까지로 적어 넣으면 이미 상자 안의 좌표라 곱할 행렬이 없습니다.
선형대수의 투영 행렬
선형대수에서 투영 행렬은 점을 평면이나 직선 위로 떨어뜨리는 행렬입니다. 3차원 점 (x, y, z) 의 z 를 0 으로 만드는 행렬이 한 예입니다. 이 행렬은 모든 점을 바닥 평면 위로 떨어뜨립니다.
이 행렬은 두 번 곱해도 한 번 곱한 것과 같습니다. 이미 평면 위에 있는 점은 더 떨어질 곳이 없기 때문입니다. 선형대수는 이 성질, 곧 멱등성을 가진 행렬을 투영 행렬이라고 부릅니다.
통계의 최소제곱법도 이 뜻의 투영 행렬을 씁니다. 흩어진 측정점 여러 개에 가장 잘 맞는 직선을 고를 때가 그렇습니다. 측정값을 그 직선이 낼 수 있는 값들 가운데 가장 가까운 쪽으로 떨어뜨립니다. 떨어진 값이 직선의 예측값입니다.
그래픽스의 투영 행렬은 이 성질이 없습니다. 원근 투영 행렬을 두 번 곱하면 한 번 곱한 것과 다른 점이 나옵니다.
까닭은 둘입니다. 거리를 버리지 않고 깊이 값으로 남깁니다. 범위도 상자에 맞춰 늘이고 줄입니다.
그래서 두 분야의 투영 행렬은 이름만 같고 정의는 다릅니다. 선형대수 쪽은 점을 평면 위로 바로 떨어뜨립니다. 그래픽스 쪽은 점을 상자로 옮깁니다. 평면인 화면에 그리는 일은 그 뒤 단계가 맡습니다.
관련 항목
투영 행렬이 속하는 상위 분류
그래픽스 · 3차원 그래픽스 · 렌더링 · 좌표 변환 · 변환 행렬 · 투영
투영 행렬이 담아내는 투영 방식
원근 투영 · 직교 투영 · 평행 투영 · 등각 투영 · 경사 투영
투영 행렬을 정하는 카메라 설정
카메라 · 시야각 · 종횡비 · 근평면 · 원평면 · 뷰 볼륨 · 뷰 절두체
투영 행렬과 차례로 곱해지는 행렬
모델 행렬 · 뷰 행렬 · 모델뷰 행렬 · MVP 행렬
투영 행렬을 적는 수학 도구
행렬 · 행렬 곱 · 동차 좌표 · 삼각함수 · 행 우선 순서 · 열 우선 순서
한 점이 투영 행렬 앞뒤로 거치는 좌표 공간
모델 좌표계 · 월드 좌표계 · 뷰 공간 · 클립 공간 · 정규화 장치 좌표 · 화면 좌표
투영 행렬을 곱하고 이어받는 처리 단계
그래픽스 파이프라인 · 정점 셰이더 · 유니폼 · 클리핑 · 원근 나눗셈 · 뷰포트 변환 · 뷰포트 · 래스터화 · 후처리
투영 행렬이 남기는 깊이 문제
깊이 버퍼 · 깊이 테스트 · 깊이 정밀도 · Z 파이팅 · 리버스 Z
투영 행렬을 받아 쓰는 그래픽 API
OpenGL · WebGL · Vulkan · Direct3D · Metal · WebGPU
투영 행렬을 만들어 주는 수학 라이브러리
GLM · gl-matrix · DirectXMath · three.js
선형대수의 투영 행렬이 기대는 개념
선형대수 · 벡터 · 부분 공간 · 직교 사영 · 멱등 행렬 · 최소제곱법
다른 이름: projection matrix · 프로젝션 행렬 · 투영 변환 행렬