서비스
교육 철학

10.3 좌표 공간과 투영 행렬 (Coordinate Spaces)

10.3 좌표 공간 (Coordinate Spaces)

이 절에서는 3D 장면을 바라볼 때 쓰는 몇 가지 중요한 좌표 공간을 복습합니다. 안타깝게도 이 주제의 문헌들은 개념에는 동의하면서도 용어는 일관되게 사용하지 않습니다. 여기서는 기하 정보가 그래픽스 파이프라인을 흘러가면서 만나는 순서대로 좌표 공간을 설명하겠습니다.

10.3.1 모델 공간, 월드 공간, 카메라 공간

물체의 기하 정보는 처음에 물체 공간(object space)으로 기술합니다. 기술하려는 물체에 붙어 있는 지역 좌표 공간입니다(3.2.2절). 보통 여기에 기술하는 정보는 정점의 위치와 표면 법선입니다. 물체 공간은 로컬 공간(local space)이라고도 하고, 특히 그래픽스에서는 모델 공간(model space)이라고 부릅니다.
모델 공간의 정점은 월드 공간(world space)으로 변환합니다(3.2.1절). 모델 공간에서 월드 공간으로 가는 변환을 흔히 모델 변환(model transform)이라고 합니다. 장면의 조명은 보통 월드 공간에서 지정합니다. 하지만 10.11절에서 보겠듯, 기하 정보와 광원을 같은 공간으로 표현할 수만 있으면 실제 조명 계산에 어느 좌표 공간을 사용하는지는 중요하지 않습니다.
월드 공간의 정점은 뷰 변환(view transform)을 거쳐 카메라 공간(camera space)으로 이동합니다(3.2.3절). 이를 아이 공간(eye space) 또는 뷰 공간(view space)이라고도 합니다. 뒤에서 설명할 표준 시야 부피 공간과 혼동하지 마세요.
카메라 공간은 투영 중심을 원점으로 하는 3차원 좌표 공간입니다. 한 축은 카메라가 바라보는 방향과 평행하고, 따라서 투영 평면과 수직입니다. 다른 한 축은 위·아래 클립 평면의 교선이며, 나머지 축은 왼쪽·오른쪽 클립 평면의 교선입니다. 카메라의 시점에서 보면 한 축은 ‘가로’, 다른 축은 ‘세로’가 됩니다.
왼손 좌표계를 쓰는 세계에서는 카메라가 바라보는 쪽을 +z, 카메라 기준 오른쪽을 +x, 위쪽을 +y로 두는 관례가 가장 흔합니다. 그림 10.7에서 보듯 꽤 직관적입니다. 오른손 좌표계에서는 보통 카메라가 바라보는 방향을 −z로 둡니다. 이 장의 나머지 부분에서는 왼손 좌표계의 관례를 사용합니다.
그림 10.7 — 왼손 좌표계에서 흔히 사용하는 카메라 공간의 축 방향

10.3.2 클립 공간과 클립 행렬

카메라 공간의 정점은 다시 클립 공간(clip space)으로 변환합니다. 표준 시야 부피 공간(canonical view volume space)이라고도 합니다. 카메라 공간의 정점을 클립 공간으로 옮기는 행렬을 클립 행렬(clip matrix) 또는 투영 행렬(projection matrix)이라고 부릅니다.
지금까지 정점의 위치는 ‘순수한’ 3차원 벡터였습니다. 좌표가 세 개뿐이거나, 네 번째 좌표가 있더라도 위치 벡터의 w는 항상 1, 표면 법선 같은 방향 벡터의 w는 항상 0이었습니다. 특별한 상황에서는 더 특이한 변환을 쓰기도 하지만, 대부분의 기본 변환은 3차원 아핀 변환입니다. 그런데 클립 행렬은 w에 의미 있는 정보를 넣습니다. 주요 목적은 두 가지입니다.
•
투영 준비: 동차 나눗셈이 원하는 투영을 만들도록 w에 알맞은 값을 넣습니다. 일반적인 원근 투영에서는 z를 w로 복사합니다. 10.3.3절에서 다룹니다.
•
줌 적용과 클리핑 준비: x·y·z를 w와 비교하여 클리핑할 수 있도록 크기를 조절합니다. 클리핑의 기준인 뷰 프러스텀 모양은 카메라의 줌 값에 영향을 받으므로, 이 크기 조절에는 줌 값도 반영합니다. 10.3.4절에서 다룹니다.

10.3.3 클립 행렬: 투영 준비

6.4.1절에서 보았듯, 4차원 동차 벡터를 실제 3차원 벡터로 바꿀 때는 w로 나눕니다.
[xyzw]⟹[x/wy/wz/w].\begin{bmatrix}x\\y\\z\\w\end{bmatrix} \Longrightarrow \begin{bmatrix}x/w\\y/w\\z/w\end{bmatrix}.
클립 행렬의 첫 번째 목표는 이 나눗셈이 원하는 원근 투영이나 직교 투영을 만들도록 w에 정확한 값을 넣는 것입니다. 그래서 이 행렬을 투영 행렬이라고도 부릅니다. 다만 이 이름은 조금 오해를 부릅니다. 투영 자체는 행렬을 곱할 때 일어나는 것이 아니라, x·y·z를 w로 나눌 때 일어납니다.
w에 값을 넣는 일만 필요하다면, 원근 투영을 위한 클립 행렬은 다음처럼 단순해집니다.
[1000010000110000].\begin{bmatrix} 1&0&0&0\\0&1&0&0\\0&0&1&1\\0&0&0&0 \end{bmatrix}.
행벡터 [x,y,z,1][x,y,z,1]에 이 행렬을 곱한 다음 w로 동차 나눗셈을 하면 다음 결과를 얻습니다.
[xyz1][1000010000110000]=[xyzz]⟹[x/zy/z1].\begin{bmatrix}x&y&z&1\end{bmatrix} \begin{bmatrix}1&0&0&0\\0&1&0&0\\0&0&1&1\\0&0&0&0\end{bmatrix} =\begin{bmatrix}x&y&z&z\end{bmatrix} \Longrightarrow\begin{bmatrix}x/z&y/z&1\end{bmatrix}.
이쯤에서 독자는 충분히 타당한 두 가지 질문을 할 수 있습니다. 첫째는 “왜 이렇게 복잡하게 하나요? 결국 z로 나눌 뿐인데 너무 많은 일을 하는 것 같습니다.”라는 질문입니다. 맞습니다. 투영 계산을 직접 작성하던 예전 소프트웨어 래스터라이저 상당수에는 w가 아예 없었고, z로 나누는 코드만 있었습니다. 그렇다면 이 복잡함을 감수하는 이유는 무엇일까요?
한 가지 이유는 동차 좌표가 더 다양한 카메라 설정을 자연스럽게 표현하기 때문입니다. 이 절의 끝에서 보겠지만, 예전의 직접 작성한 시스템에서 필요하던 if문 없이 직교 투영도 쉽게 처리할 수 있습니다. 다른 유용한 투영들도 같은 틀에 자연스럽게 들어옵니다. 예를 들어 프러스텀 평면들이 시선 방향에 대해 대칭일 필요는 없습니다.
시선 방향이 출력 창의 중심을 통과하지 않는 경우에 해당합니다. 아주 큰 해상도의 영상을 작은 조각으로 나누어 렌더링하거나, 화면 분할 시점을 동적으로 나누고 합치면서 경계를 매끄럽게 이어야 할 때 유용합니다.
동차 좌표의 또 다른 장점은 근거리·원거리 평면에 대한 z 클리핑을 x·y 클리핑과 똑같은 형태로 만든다는 것입니다. 처리 방식이 깔끔해질 뿐 아니라, 일부 하드웨어에서는 벡터 연산 장치를 활용해 클리핑 비교를 병렬로 수행할 수 있습니다. 일반적으로 동차 좌표와 4×4 행렬은 표현을 더 간결하고 범용적으로 만들고, 어떤 사람들의 눈에는 더 우아하게 만들기도 합니다.
하지만 4×4 행렬이 정말 더 나은 방식인지와 관계없이, 대부분의 API가 데이터를 이 형태로 요구하므로 좋든 싫든 이렇게 처리해야 합니다.
둘째 질문은 “d는 어디로 갔나요?”입니다. d는 초점 거리(focal distance), 즉 투영 평면에서 투영 중심인 ‘초점’까지의 거리였습니다. 6.5절에서는 동차 나눗셈으로 원근 투영을 설명하며, z축에 수직이고 원점에서 d만큼 떨어진 평면, 곧 z=dz=d에 투영했습니다. 그런데 위 설명에서는 d를 전혀 쓰지 않았습니다. 사실 여기서는 d로 어떤 값을 사용하느냐가 중요하지 않습니다. 따라서 가장 편한 값인 1을 선택합니다.
그림 10.8 — 실제 카메라에서 ‘필름’ 크기를 고정한 채 초점 거리 d를 늘리면 줌인 효과가 나타납니다
d가 왜 중요하지 않은지 이해하려면 컴퓨터의 투영과 실제 카메라의 투영을 비교해 봅시다. 실제 카메라에서는 이 거리를 늘리면 줌인되어 물체가 커 보이고, 줄이면 줌아웃되어 작아 보입니다. 그림 10.8이 이 현상을 보여줍니다.
각 도식의 왼쪽 세로선은 무한한 투영 평면 위에 놓인 필름, 또는 현대 카메라의 이미지 센서를 나타냅니다. 여기서 모든 도식의 필름 높이가 같다는 점을 보세요. d를 늘리면 필름이 초점에서 멀어지고, 프러스텀이 받아들이는 시야각이 줄어듭니다. 프러스텀이 좁아지면 그 안의 물체가 보이는 부피에서 더 큰 비율을 차지하므로 투영 영상에서 더 크게 보입니다.
우리는 이를 줌인으로 지각합니다. 핵심은 초점 거리를 바꿀 때 물체가 커 보이는 이유가 필름 크기에 비해 투영된 영상이 커지기 때문이라는 것입니다.
이번에는 컴퓨터 안을 봅시다. 컴퓨터의 ‘필름’은 투영 평면과 뷰 프러스텀이 만나는 직사각형 영역입니다.(원주 10-9) 초점 거리를 늘리면 실제 카메라에서처럼 투영된 영상이 커집니다. 그러나 컴퓨터에서는 프러스텀 모양이 달라지는 대신, 필름 자체가 정확히 같은 비율로 커집니다. 영상과 필름이 같은 비율로 커지므로 최종 렌더링 영상과 그 안의 물체가 차지하는 크기는 달라지지 않습니다.
정리하면 실제 카메라이든 컴퓨터이든 줌은 항상 프러스텀의 모양을 바꾸어서 만듭니다. 실제 카메라에서는 필름 크기가 고정되어 있으므로 초점 거리를 바꾸면 프러스텀 모양이 변합니다. 반면 여기서 설명하는 컴퓨터의 투영에서는 d를 바꾸어도 필름이 함께 커지고 프러스텀 모양은 유지되므로 렌더링 결과가 달라지지 않습니다.
일부 소프트웨어에서는 밀리미터 단위의 초점 거리로 시야각을 지정할 수 있습니다. 이 수치는 어떤 표준 필름 크기를 전제로 하며, 거의 언제나 35mm 필름을 기준으로 합니다.
직교 투영은 어떨까요? 이때는 z로 나누고 싶지 않으므로 클립 행렬의 맨 오른쪽 열을 단위행렬처럼 [0,0,0,1]T[0,0,0,1]^T로 둡니다. [x,y,z,1][x,y,z,1] 형태의 벡터를 곱하면 w가 z가 아니라 1인 벡터를 얻습니다. 동차 나눗셈은 여전히 수행하지만 이번에는 1로 나누는 셈입니다.
[xyz1][1000010000100001]=[xyz1]⟹[xyz].\begin{bmatrix}x&y&z&1\end{bmatrix} \begin{bmatrix}1&0&0&0\\0&1&0&0\\0&0&1&0\\0&0&0&1\end{bmatrix} =\begin{bmatrix}x&y&z&1\end{bmatrix} \Longrightarrow\begin{bmatrix}x&y&z\end{bmatrix}.
다음 절에서는 클립 행렬의 나머지 항을 채웁니다. 지금 기억할 핵심은 원근 투영 행렬의 맨 오른쪽 열은 항상 [0,0,1,0]T[0,0,1,0]^T, 직교 투영 행렬의 맨 오른쪽 열은 항상 [0,0,0,1]T[0,0,0,1]^T라는 것입니다. 여기서 ‘항상’이란 ‘저자들이 다른 경우를 본 적이 없다’는 뜻입니다. 특정 하드웨어의 드문 상황에서는 다른 값을 요구할지도 모릅니다.
1이 특별한 마법의 수라서 쓰는 것은 아니며, 가장 단순한 수라서 씁니다는 점이 중요합니다. 동차 변환은 나눗셈이므로 좌표의 절대 크기가 아니라 좌표 사이의 비율이 중요합니다.
행렬 전체에 같은 상수를 곱해도 투영된 x/w,y/w,z/wx/w,y/w,z/w는 변하지 않습니다. 하지만 원근 보정 래스터화에 사용하는 w 자체는 달라집니다. 따라서 어떤 이유로 다른 값이 필요할 수도 있습니다. 반대로 Wii 같은 일부 하드웨어는 앞의 두 경우만 있다고 가정하며, 맨 오른쪽 열에 다른 값을 허용하지 않습니다.
원주 10-9: 실제 카메라에서는 필름이 초점 뒤에 있지만 여기의 ‘필름’은 앞에 있습니다. 이 차이는 지금 설명하는 논점에는 중요하지 않습니다.
💡
행렬 곱과 나눗셈을 따로 확인하기
이 절의 행렬은 행벡터를 왼쪽에 두는 규약입니다. 열벡터를 사용하는 코드에 옮길 때는 행렬을 전치해야 합니다. ‘오른쪽 열’이라는 문장만 외우면 다른 규약에서 틀리기 쉽습니다.
점 (2,1,4)(2,1,4)에 단순한 원근 행렬을 적용하면 클립 좌표는 (2,1,4,4)(2,1,4,4)이고, w로 나눈 뒤에는 (0.5,0.25,1)(0.5,0.25,1)입니다. 아직 깊이 보존을 위한 항을 넣지 않았으므로 이 예의 z는 항상 1입니다. 다음 절의 행렬이 실제 깊이 범위를 처리합니다.
flowchart LR
 A["카메라 좌표 (x,y,z,1)"] --> B["클립 행렬 곱: w에 z 복사"]
 B --> C["클립 좌표 (x,y,z,z)"]
 C --> D["w로 나누기"]
 D --> E["원근에 따라 작아진 (x/z,y/z)"]
Mermaid
복사

10.3.4 클립 행렬: 줌 적용과 클리핑 준비

클립 행렬의 두 번째 목표는 여섯 클립 평면을 아주 단순하게 표현할 수 있도록 x·y·z 성분의 크기를 조절하는 것입니다. 다음 부등식 중 하나라도 만족하는 점은 뷰 프러스텀 밖에 있습니다.
평면
바깥쪽에 있는 조건
아래
y<−wy<-w
위
y>wy>w
왼쪽
x<−wx<-w
오른쪽
x>wx>w
근거리
z<−wz<-w
원거리
z>wz>w
따라서 시야 부피 안의 점은 다음을 만족합니다.
−w≤x≤w,−w≤y≤w,−w≤z≤w.-w\le x\le w,\qquad -w\le y\le w,\qquad -w\le z\le w.
이 범위를 벗어나는 기하 정보는 뷰 프러스텀에 맞게 잘라야 합니다. 클리핑 자체는 10.10.4절에서 설명합니다.
위·왼쪽·오른쪽·아래 클립 평면의 위치를 맞추려면 x와 y에 카메라의 줌 값을 곱합니다. 이 값의 계산은 10.2.4절에서 다뤘습니다. 근거리·원거리 평면에 대해서는 z 좌표에 오프셋과 배율을 적용하여, 근거리 평면에서 z/w=−1z/w=-1, 원거리 평면에서 z/w=1z/w=1이 되도록 합니다.
수평·수직 줌을 zoomx,zoomy\mathrm{zoom}_x,\mathrm{zoom}_y, 근거리·원거리 클립 평면까지의 거리를 n과 f라 합시다. x·y·z를 알맞게 조절하면서 동시에 원래 z를 w로 내보내는 행렬은 다음과 같습니다.
[zoomx0000zoomy0000f+nf−n100−2nff−n0].(10.6)\begin{bmatrix} \mathrm{zoom}_x&0&0&0\\ 0&\mathrm{zoom}_y&0&0\\ 0&0&\frac{f+n}{f-n}&1\\ 0&0&\frac{-2nf}{f-n}&0 \end{bmatrix}. \tag{10.6}
이 행렬은 z가 화면 안쪽을 향하는 일반적인 왼손 좌표계, 왼쪽에 놓인 행벡터, 근거리에서 원거리까지 [−w,w][-w,w]인 클립 z 범위를 가정합니다. 마지막 조건 역시 규약이 달라지는 지점입니다. 특히 DirectX 등의 API는 z가 [0,w][0,w] 범위에 놓이도록 하는 투영 행렬을 요구합니다. 즉 클립 공간에서 근거리 평면 밖인 조건은 z<0z<0, 원거리 평면 밖인 조건은 z>wz>w입니다.
이 DirectX식 규약에서는 내부의 점이 0≤z≤w0\le z\le w를 만족합니다. 사용하는 클립 행렬도 조금 달라집니다.
[zoomx0000zoomy0000ff−n100−nff−n0].(10.7)\begin{bmatrix} \mathrm{zoom}_x&0&0&0\\ 0&\mathrm{zoom}_y&0&0\\ 0&0&\frac{f}{f-n}&1\\ 0&0&\frac{-nf}{f-n}&0 \end{bmatrix}. \tag{10.7}
식 10.6과 10.7의 맨 오른쪽 열이 [0,0,1,0]T[0,0,1,0]^T라는 점만 보아도 둘 다 원근 투영 행렬임을 쉽게 알아볼 수 있습니다. 물론 여백의 설명문에도 힌트가 있기는 합니다.
직교 투영은 어떨까요? 첫째·둘째 열은 그대로이고, 넷째 열은 [0,0,0,1]T[0,0,0,1]^T로 바뀐다는 것을 알고 있습니다. 출력 z를 조절하는 셋째 열도 바뀌어야 합니다. 먼저 근거리·원거리 평면에서 z/wz/w가 각각 −1과 +1이 되는 첫 번째 규약을 적용하면 다음 행렬을 얻습니다.
[zoomx0000zoomy00002f−n000−f+nf−n1].\begin{bmatrix} \mathrm{zoom}_x&0&0&0\\ 0&\mathrm{zoom}_y&0&0\\ 0&0&\frac{2}{f-n}&0\\ 0&0&-\frac{f+n}{f-n}&1 \end{bmatrix}.
대신 DirectX식 클립 z 범위를 사용한다면 행렬은 다음과 같습니다.
[zoomx0000zoomy00001f−n000nn−f1].\begin{bmatrix} \mathrm{zoom}_x&0&0&0\\ 0&\mathrm{zoom}_y&0&0\\ 0&0&\frac{1}{f-n}&0\\ 0&0&\frac{n}{n-f}&1 \end{bmatrix}.
이 책에서는 왼손 좌표계와 왼쪽에 놓는 행벡터를 선호하며, 지금까지의 투영 행렬은 모두 이 규약을 전제로 했습니다. 그러나 이 두 선택은 모두 OpenGL의 관례와 다릅니다. 독자 중에는 OpenGL과 비슷한 환경에서 작업하는 분도 많을 것입니다. 혼동하기 쉬우므로 같은 행렬들을 오른손 좌표계와 열벡터를 사용하는 OpenGL 규약으로 다시 적어 보겠습니다. 여기서는 OpenGL이 사용하는 −1에서 +1까지의 정규화된 깊이 범위만 다룹니다.
어떻게 한 규약에서 다른 규약으로 변환하는지 생각해 보면 유익합니다. 먼저 OpenGL은 열벡터를 사용하므로 행렬을 전치합니다. 다음으로 오른손 카메라 공간, OpenGL 용어로 아이 공간에서는 −z가 화면 안쪽을 향하지만, 클립 공간에서는 앞서의 왼손 규약과 마찬가지로 +z가 화면 안쪽을 향합니다.
원문의 설명대로라면 OpenGL의 클립 공간은 실제로 왼손 방향성을 가집니다. 따라서 입력 z의 부호를 바꾸어야 합니다. 또는 전치한 행렬에서 z와 곱해지는 셋째 열 전체의 부호를 바꾸면 됩니다.
이 과정을 적용한 원근 투영 행렬은 다음과 같습니다.
[zoomx0000zoomy0000−f+nf−n−2nff−n00−10].\begin{bmatrix} \mathrm{zoom}_x&0&0&0\\ 0&\mathrm{zoom}_y&0&0\\ 0&0&-\frac{f+n}{f-n}&\frac{-2nf}{f-n}\\ 0&0&-1&0 \end{bmatrix}.
직교 투영 행렬은 다음과 같습니다.
[zoomx0000zoomy0000−2f−n−f+nf−n0001].\begin{bmatrix} \mathrm{zoom}_x&0&0&0\\ 0&\mathrm{zoom}_y&0&0\\ 0&0&\frac{-2}{f-n}&-\frac{f+n}{f-n}\\ 0&0&0&1 \end{bmatrix}.
따라서 OpenGL 규약에서는 맨 아래 행을 보고 투영 종류를 알아볼 수 있습니다. 원근 투영은 [0,0,−1,0][0,0,-1,0], 직교 투영은 [0,0,0,1][0,0,0,1]입니다.
클립 공간을 조금 알게 되었으니 근거리 클립 평면이 필요한 이유도 이해할 수 있습니다. 원점에는 원근 투영이 정의되지 않는 특이점이 있습니다. 원근 나눗셈에서 0으로 나누는 경우입니다. 실제로 정확히 이 특이점에 걸리는 일은 매우 드뭅니다. 점을 임의로 화면 중심에 투영하는 등 어떤 방식으로 처리해도 큰 문제가 없을 것입니다. 카메라를 정확히 다각형 위에 놓아야 하는 경우는 흔하지 않기 때문입니다.
하지만 다각형을 픽셀로 투영하는 것만이 문제는 아닙니다. 원문은 ‘양수이지만 임의로 작은 z를 허용하면 임의로 큰 w가 생긴다’고 설명합니다. 이 표현의 오류는 아래 추가 설명에서 바로잡습니다. 하드웨어에 따라 이런 상황이 원근 보정 래스터화에서 문제를 일으킬 수 있습니다. 깊이 버퍼도 문제가 생길 수 있는 영역입니다.
실용적인 이유로 z의 알려진 최솟값을 정해 범위를 제한해야 하는 일이 많으므로, 달갑지 않아도 근거리 클립 평면을 받아들여야 한다는 정도로 이해하면 됩니다.
‘달갑지 않다’고 한 이유는 근거리 클립 평면이 3차원 세계의 본질적인 속성이 아니라 구현 과정에서 생긴 제약이기 때문입니다. 레이 트레이서에는 반드시 이 문제가 있는 것은 아닙니다. 현실에서는 얼마든지 물체에 가까이 다가갈 수 있어야 하지만, 근거리 평면은 가까운 물체를 잘라 버립니다.
아주 큰 지면 다각형의 가운데에서 카메라를 지면보다 조금 위에 두었더니 화면 아래쪽에 틈이 열려 지면을 뚫고 보이는 현상을 경험한 독자가 많을 것입니다. 벽을 제외한 거의 어떤 물체든 너무 가까이 가면 비슷한 현상이 생깁니다. 물체 한가운데에 구멍이 나타나고, 더 다가갈수록 커집니다.

10.3.5 화면 공간

기하 정보를 뷰 프러스텀에 맞게 잘랐다면, 프레임 버퍼의 실제 픽셀에 대응하는 화면 공간(screen space)으로 투영합니다. 출력 창이 표시 장치 전체를 차지하지는 않을 수도 있다는 점을 기억하세요. 그래도 화면 공간의 좌표는 보통 렌더링 장치 전체를 기준으로 한 절대 좌표로 지정합니다. 그림 10.9를 보세요.
화면 공간은 물론 2차원입니다. 따라서 올바른 2차원 좌표를 얻으려면 클립 공간의 점을 화면 공간으로 투영해야 합니다. 먼저 보통의 동차 나눗셈, 즉 w로 나누기를 수행합니다. OpenGL은 그 결과를 정규화 장치 좌표(normalized device coordinates)라고 부릅니다. 그런 다음 x·y를 출력 창에 맞게 확대·축소하고 이동합니다.
screenx=clipx2clipw winResx+winCenterx.(10.8)\mathrm{screen}_x=\frac{\mathrm{clip}_x}{2\mathrm{clip}_w}\,\mathrm{winRes}_x+\mathrm{winCenter}_x. \tag{10.8}
screeny=−clipy2clipw winResy+winCentery.(10.9)\mathrm{screen}_y=\frac{-\mathrm{clip}_y}{2\mathrm{clip}_w}\,\mathrm{winRes}_y+\mathrm{winCenter}_y. \tag{10.9}
여기서 y의 부호를 바꾼 이유를 짚고 넘어갑시다. 이는 (0,0)을 왼쪽 위 모서리에 두는 DirectX식 좌표 규약을 반영합니다. 이 규약에서 +y는 클립 공간에서는 위쪽, 화면 공간에서는 아래쪽을 향합니다. +z가 여전히 화면 안쪽을 향한다고 생각하면 화면 공간은 실제로 오른손 방향성이 됩니다. 원문의 설명에서는 DirectX의 다른 공간들은 왼손 방향성이었습니다.
그림 10.9 — 화면 공간에서의 출력 창
OpenGL에서는 원점이 왼쪽 아래에 있으므로 y의 부호를 바꾸지 않습니다. 앞서 이야기했듯 OpenGL은 대신 아이 공간의 −z가 화면 안쪽을 향하다가 클립 공간의 +z가 화면 안쪽을 향하도록 z축을 뒤집는 지점에서 혼동을 줍니다.
z 이야기가 나온 김에 clipz\mathrm{clip}_z는 어떻게 되는지 봅시다. 일반적으로 깊이 버퍼에 어떤 방식으로든 사용합니다. 전통적인 방법은 정규화된 깊이 값 clipz/clipw\mathrm{clip}_z/\mathrm{clip}_w를 깊이 버퍼에 저장하는 것입니다. 정확한 세부 사항은 클리핑에 어떤 좌표 범위를 쓰는지, 깊이 버퍼가 어떤 값들을 받는지에 따라 달라집니다.
예를 들어 OpenGL의 개념적인 규약에서는 뷰 프러스텀이 −1≤clipz/clipw≤1-1\le\mathrm{clip}_z/\mathrm{clip}_w\le1을 만족하지만, 이것이 깊이 버퍼에 가장 적합한 표현이라는 뜻은 아닙니다. 드라이버 제작자는 API의 개념적인 규약을 하드웨어에 가장 적합한 방식으로 변환해야 합니다.
다른 방법인 w 버퍼링(w-buffering)은 clipw\mathrm{clip}_w를 깊이로 사용합니다. 대부분의 상황에서 이 값은 카메라 공간 z의 배율을 바꾼 것뿐입니다. 따라서 w를 깊이 버퍼에 사용하면 저장 값과 픽셀의 시선 방향 깊이가 선형 관계를 가집니다. 제한된 정밀도의 고정소수점 깊이 버퍼에서는 사용할 수 있는 정밀도를 더 고르게 분배하므로 매력적일 수 있습니다.
전통적인 clipz/clipw\mathrm{clip}_z/\mathrm{clip}_w 방식은 가까운 곳의 정밀도를 크게 높이는 대신, 원거리 클립 평면 부근의 정밀도를 때로는 급격히 낮춥니다. 원문은 부동소수점 깊이 버퍼에서는 이 문제가 훨씬 덜 중요하다고 설명합니다. 또한 직교 투영에서는 행렬이 항상 w=1을 내보내므로 w 버퍼링을 사용할 수 없습니다.
clipw\mathrm{clip}_w는 버리지 않습니다. 이미 말했듯 정규화 장치 좌표로 변환하는 동차 나눗셈의 분모라는 중요한 역할을 합니다. 또한 래스터화하면서 텍스처 좌표, 색상, 그 밖의 정점별 값을 원근에 맞게 보간할 때도 보통 필요합니다.
이 책을 집필하던 당시의 최신 그래픽스 API에서는 클립 공간에서 화면 공간으로 가는 정점 좌표 변환을 자동으로 처리합니다. 정점 셰이더는 클립 좌표를 출력합니다. API는 삼각형을 프러스텀에 맞게 자르고 화면으로 투영합니다. 그렇다고 이 절의 식을 직접 코드에서 쓸 일이 전혀 없다는 뜻은 아닙니다. 가시성 검사, 세부 수준 선택 등을 위해 소프트웨어에서 직접 계산해야 하는 경우가 꽤 많습니다.

10.3.6 좌표 공간 정리

그림 10.10 — 그래픽스 파이프라인을 통과하며 변환되는 정점 좌표
그림 10.10은 이 절에서 설명한 좌표 공간과 행렬을 정리하고, 물체 공간에서 화면 공간까지의 데이터 흐름을 보여줍니다.
지금까지 나온 공간들이 가장 중요하고 자주 쓰이지만, 컴퓨터 그래픽스에는 다른 좌표 공간도 있습니다. 예를 들어 투사 광원은 자신의 공간을 가질 수 있습니다. 카메라 공간과 본질적으로 같지만, 카메라 대신 광원이 장면을 ‘바라보는’ 시점이라는 차이가 있습니다. 광원이 고보(gobo)라고도 하는 이미지를 투사할 때 중요하며, 그림자 매핑에서 광원이 어떤 점을 ‘볼 수 있는지’ 판단할 때도 중요합니다.
또 하나 중요한 공간은 물체 표면에 붙은 로컬 공간인 접선 공간(tangent space)입니다. 기저 벡터 하나는 표면 법선이고 나머지 둘은 그 지점에서 표면에 접합니다. 결과적으로 표면의 해당 지점 위에 ‘평평하게 놓인’ 2차원 좌표계를 만드는 셈입니다. 기저 벡터를 정하는 방법은 다양하지만, 이런 좌표 공간을 만드는 가장 흔한 이유는 범프 매핑과 관련 기법들입니다.
접선 공간을 더 자세히 설명하려면 먼저 10.5절의 텍스처 매핑이 필요하므로, 10.9.1절에서 다시 다룹니다. 접선 공간은 표면 로컬 공간(surface-local space)이라고도 합니다.
💡
깊이 범위와 원문의 작은 오류
클립 좌표의 −w≤z≤w-w\le z\le w와 나눗셈 후의 −1≤z/w≤1-1\le z/w\le1은 서로 다른 단계의 표현입니다. 여기서는 카메라 앞쪽의 w가 양수라는 조건 아래 연결됩니다. 클리핑 검사와 나눗셈 순서를 섞지 마세요.
원문 오류: 10.3.4절의 ‘작은 양수 z가 큰 w를 만든다’는 문장은 앞서 정의한 w=z와 맞지 않습니다. z가 0에 가까워질수록 커지는 것은 1/w와 이를 사용하는 나눗셈 결과입니다. 근거리 평면이 필요한 실용적인 이유에 관한 논의는 이 수정과 함께 읽어야 합니다.
손으로 검산하기: DirectX식 행벡터 행렬에서 n=1, f=11이면 클립 z는 1.1z−1.11.1z-1.1, w는 z입니다. z=1은 나눗셈 뒤 0, z=11은 1이 됩니다. z=6은 0.5가 아니라 약 0.9167입니다. 원근 투영의 깊이가 거리와 선형 관계가 아니라는 점을 직접 확인할 수 있습니다.
규약 메모: 이 절의 API 설명은 원서의 당시 관례를 보존한 것입니다. 실제 코드에서는 자신이 선택한 행·열벡터, 카메라 전방, 클립 깊이 범위, 화면 원점을 각각 확인해야 합니다. API 이름만으로 모든 규약을 한꺼번에 추측하지 마세요.