Post

[OpenGL ES를 이용한 3차원 컴퓨터 그래픽스 입문] 7. 래스터라이저

프리미티브를 프래그먼트로 바꾸는 래스터라이저의 클리핑, 원근 나눗셈, 뒷면 제거, 뷰포트 변환, 스캔 전환 단계를 정리했습니다.

[OpenGL ES를 이용한 3차원 컴퓨터 그래픽스 입문] 7. 래스터라이저

개요


GPU 파이프라인이 처리할 기하적인 개체들은 종종 프리미티브(primitive)라고 부른다. 그래픽스 응용에 따라 삼각형 말고도 선과 점도 독자적인 프리미티브가 될 수 있는데, 이들은 각각 정점 3개, 2개, 1개를 사용하여 정의된다. 하지만 이 장은 삼각형 프리미티브만을 다룰 것이다.

정점 쉐이더가 출력한 정점들은 프리미티브로 다시 조립된다. 이 프리미티브는 스크린에 그려질 형태로 변환된 후 프리미티브로 분해되는데 이를 래스터화(rasterization)라고 부른다. 5장 초반에 정의한 바와 같이, 프래그먼트는 컬러 버퍼의 픽셀을 갱신하는 데 필요한 데이터를 총칭하는 것이다. 정점들에 저장된 노멀과 텍스처 좌표 등은 프리미티브를 따라 보간되어 각 프래그먼트에 할당된다.

이러한 프리미티브 조립 및 래스터화(primitive assembly and rasterization)는 그림 5.1에 보인 GPU 파이프라인의 래스터라이저 (rasterizer)가 담당한다. 이는 쉐이더와 달리 하드웨어도 고정된 단계로, 클리핑(clipping), 원근 나눗셈(perspective division), 뒷면 제거(back-face culling), 뷰포트 전환(viewport transform), 스캔 전환(scan conversion) 등의 작업을 수행한다.


7.1 클리핑


img 그림 7.1. 삼각형 클리핑

클리핑은 2X2X2 크기의 클립 공간 뷰 볼륨 바깥에 놓인 폴리곤을 잘라내는 작업을 말한다. 이번 절에서는 직관적인 이해를 돕고자 ‘클립 공간 뷰 볼륨’ 대신 ‘카메라 공간 뷰 프러스텀’을 사용해 클리핑의 개념을 살펴보도록 한다. 삼각형과 뷰 프러스텀 사이의 공간적인 관계는 그림 7.1에 보인 것처럼 크게 세 가지 경우로 나눌 수 있다:

  1. 삼각형 t1은 뷰 프러스텀 바깥쪽에 위치하므로 제거된다.
  2. 삼각형 t2는 뷰 프러스텀 안쪽에 위치하므로 그대로 다음 단계로 넘겨진다.
  3. 삼각형 t3는 뷰 프러스텀과 교차하므로, 뷰 프러스텀 바깥에 놓인 부분을 잘라내는 작업이 필요한데, 기존 정점 일부가 제거되고 새로운 정점을 추가해서 새로운 프리미티브를 만든다.

2번과 3번의 프리미티브들은 래스터라이저의 다음 단계로 넘어간다.


7.2 원근 나눗셈


식 (5.12)의 투영 행렬 $M_{proj}$를 카메라 공간의 한 점 (x,y,z,1)에 적용하자.

\[\begin{pmatrix} m_{11} & 0 & 0 & 0 \\ 0 & m_{22} & 0 & 0 \\ 0 & 0 & m_{33} & m_{34} \\ 0 & 0 & -1 & 0 \\ \end{pmatrix} \begin{pmatrix} x \\ y \\ z \\ 1 \\ \end{pmatrix} = \begin{pmatrix} m_{11}x \\ m_{22}y \\ m_{33}z + m_{34} \\ -z \\ \end{pmatrix} \to \begin{pmatrix} -m_{11} \frac{x}{z} \\ -m_{22} \frac{y}{z} \\ -m_{33} - m_{34} \frac{1}{z} \\ 1 \\ \end{pmatrix} \tag{7.1}\]

$M_{proj}$의 첫 3개 행 중에서 0이 아닌 원소들은 $m_{11}$, $m_{22}$, $m_{33}$, $m_{34}$로 표기했다. 한편, 아핀 변환과는 달리 $M_{proj}$의 마지막 행은 (0 0 0 1)이 아니라 (0 0 -1 0)이다. 따라서, $M_{proj}$에 의해 변환된 정점의 w좌표는 -z가 된다. 이는 대개의 경우 1이 아니다. 투영 변환을 통해 얻어진 동차 좌표를 카테시안 좌표로 전환하기 위해서는 동차 좌표의 모든 원소를 w로 나눠야 한다. 식 (7.1)에서는 이 나눗셈을 $\to$로 표기했다.

그림 7.2는 투영 변환의 예를 보여주는데, $M_{proj}$를 두 선분 l1과 l2에 적용한 결과를 비교해 보자. 나눗셈에 사용된 w좌표는 -z로, 이는 카메라 공간의 xy평면으로부터 해당 정점까지의 수직 거리를 나타내는 양수 값이다. 선분 l1의 끝점 P1과 Q1의 -z는 2이고, l2의 끝점 P2와 Q2의 -z는 1이다. 이렇게 카메라로부터 멀리 떨어져 있는 정점은 투영 변환 후 w좌표가 크므로, w로 나누는 연산은 멀리 떨어진 물체를 작게 만든다. 그림 7.2의 카메라 공간에서 l1과 l2는 동일한 길이를 가지지만, $M_{proj}$를 적용하고 w로 나눈 결과, $l_1^{‘}$는 $l_2^{‘}$보다 작아졌다. 이는 바로 원근법 구현에 해당한다. 따라서, w로 나누는 연산을 원근 나눗셈(perspective division)이라고 부른다.

img

img

img 그림 7.2. 투영 변환의 결과는 동차 좌표계인 클립 공간에서 정의되는데, 클립 공간의 각 정점을 자신의 w좌표로 나누면 카테시안 좌표계의 정점으로 변한다. 이것은 원근법을 구현하는 효과를 가지므로 원근 나눗셈이라고 부른다.

원근 나눗셈의 결과로 얻은 카테시안 좌표를 정규화된 장치 좌표(normalized device coordinates) 혹은 간단히 NDC라고 부른다. 정규화라는 이름을 붙인 이유는 이 좌표의 x,y,z 범위가 모두 [-1, 1]이기 때문이다.


7.3 뒷면 제거


가상 카메라를 등지고 있는 폴리곤은 뒷면(back face)이라고 부른다. 이는 카메라에 보이지 않으므로 제거될 것이다. 반면, 카메라를 향하는 폴리곤은 앞면(front face)이라고 부르는데, 이는 보존되어 래스터라이저의 다음 단계로 넘어갈 것이다.


7.3.1 뒷면 제거 - 개념

우선 뒷면 제거의 개념을 카메라 공간에서 설명해 보자. 카메라(EYE)가 삼각형 노멀이 가리키는 방향의 반대쪽에 존재한다면 이 삼각형은 뒷면이다. 그림 7.3.a에서 t1은 뒷면이다. 반면 t2는 앞면이다. 이를 구분하기 위해서는 삼각형의 정점과 카메라를 연결하는 벡터가 필요하다. 이를 ci라 하고, 삼각형 노멀 ni와 ci의 내적 ni $\cdot$ ci를 계산하자. 2.3절에서 설명한대로, ni와 ci 사이 각도를 $\theta$라 할 때, ni $\cdot$ ci는 $| n_i | | c_i | cos \theta$이다. 따라서 그림 7.3.a의 t1처럼 n1과 c1이 둔각을 이루면 내적은 음수가 되고, 해당 삼각형은 뒷면으로 판정된다. 반대로, t2처럼 n2와 예각을 이루면, 내적은 양수가 되고, 해당 삼각형은 앞면으로 판정된다. 한편 그림 7.3.a의 t3에서는 n3와 c3가 수직이므로 내적은 0이 되는데, 이 경우 해당 삼각형은 변만 보이는 것으로 판정된다.

img 그림 7.3.a. 뒷면 제거의 개념 및 구현

img 그림 7.3.b. 뒷면 제거의 개념 및 구현

img 그림 7.3.c. 뒷면 제거의 개념 및 구현

img 그림 7.3.e와 d. 뒷면 제거의 개념 및 구현


7.3.2 뒷면 제거 - 구현

그림 7.3.a에서 ci는 5.4.2절에서 소개한 투영선과 일치하는데, 투영 변환 이후에는 모든 ci가 z축과 나란해 진다. 그림 7.3.b의 단면도는 이러한 단일한 투영선을 보여준다. 7.3.c와 같이, 투영 변환이 적용된 구가 주어졌을 때, 이 구의 삼각형들을 단일한 투영선을 따라 xy평면으로 투영해 보자. 이는 각 삼각형 정점의 z좌표를 버리고 xy좌표만을 취하는 것과 같다.

삼각형 t1을 투영한 결과는 그림 7.3.d와 같다. 정점이 시계방향으로 정렬되어 있는 것을 발견할 수 있다. 그런데 3차원 공간에서는 t1을 포함한 모든 삼각형의 정점이 반시계 방향으로 정렬되었었다. 이러한 불일치는 이상한 것이 아니다. 왜냐하면 3차원 구의 t1이 반시계 방향으로 정렬된 정점을 가졌다는 것은 우리가 그 구를 바깥에서 바라보았을 때의 성질인데, 그림 7.3.d는 구 안에서 t1을 관찰한 것과 같은 결과를 보여주기 때문이다. 반면, 그림 7.3.e의 t2 정점은 2차원 공간에서 반시계 방향으로 정렬되어 있다. 이 두 가지 예에서 확인할 수 있는 것처럼, 2차원으로 투영된 삼각형의 정점이 시계 방향으로 정렬되면 뒷면으로 판정하고, 반시계 방향으로 정렬되면 앞면으로 판정할 수 있다는 것이다.

2차원으로 투영된 삼각형 <v1,v2,v3>를 생각해보자. 각 정점 vi는 (xi,yi) 좌표를 가진다. 이 삼각형의 정점이 시계 방향 혹은 반시계 방향으로 정렬되어 있는지 판정하는 방법은 간단하다. 우선 v1과 v2를 잇는 벡터 (x2-x1, y2-y1), 그리고 v1과 v3를 잇는 벡터 (x3-x1, y3-y1)을 계산한 후 다음과 같은 행렬식(determinant)를 계산한다.

\[\begin{pmatrix} (x2 - x1) & (y2 - y1) \\ (x3 - x1) & (y3 - y1) \\ \end{pmatrix} = (x2 - x1)(y3 - y1) - (y2 - y1)(x3 - x1) \tag{7.2}\]

그림 7.3.d에 보인 것처럼, 위 행렬식의 값이 음수라면 시계 방향 즉 뒷면, 그림 7.3.e처럼 양수라면 반시계 방향 즉 앞면이 된다. 만약 0이라면 변만 보이는 삼각형을 의미한다.

그런데 항상 뒷면을 제거해야 하는 것은 아니다. 응용에 따라 다르다. 예를 들어, 반투명 물체를 렌더링하는 경우, 우리는 앞면을 통해 뒷면을 볼 수 있어야 하므로 뒷면을 제거하면 안 된다. 또 다른 예로, 하나의 구가 주어졌을 때 뒷면 대신 앞면을 모두 제거한다면, 우리는 그 구를 반으로 잘라서 내부를 보는 효과를 얻을 것이다.

뒷면 제거 알고리즘은 래스터라이저 내부에서 하드웨어로 구현되어 있다. 하지만, 클리핑이나 원근 나눗셈과 달리, 뒷면 제거는 선택적인 조절이 가능하다. GL의 glEnable 함수를 사용하여 GPU 파이프라인의 다양한 기능을 활성화할 수 있는데, 삼각형 제거를 위해서는 우선 glEnable(GL_CULL_FACE)를 호출해야 한다. 그 다음 glCullFace를 호출하여 뒷면과 앞면 중 어느 것을 제거할 것인지 정해야 한다. 기본값은 GL_BACK이다. 즉, glCullFace를 호출하지 않으면 뒷면이 제거되고, 앞면을 제거하고 싶으면 glCullFace(GL_FRONT)를 호출해야 한다. 마지막으로 glFrontFace를 호출하여 앞면의 정점 정렬 순서를 명시해야 한다. 이 함수의 기본값은 GL_CCW, 즉 반시계 방향이다. 이는 그림 7.3.e와 부합된다.

1
2
3
4
5
void glCullFace(GLenum mode);
mode: GL_FRONT, GL_BACK, or GL_FRONT_AND_BACK

void glFrontFace(GLenum mode);
mode: GL_CW or GL_CCW

우리가 일반적으로 불투명한 물체의 뒷면을 제거하는 이유는 그것이 보이지 않기 때문이다. 하지만, 앞면이라고 해서 반드시 보이는 것은 아니다. 또 다른 앞면에 의해 가려질 수 있기 때문이다. 이러한 앞면들은 z-버퍼링(z-buffering)이라고 하는 프래그먼트별 연산에 의해 처리된다. 이는 10.1절에서 다룬다.


7.4 뷰포트 변환


img 그림 7.4. 스크린 공간과 뷰포트. (a) 스크린 공간에서 뷰포트는 왼쪽 아래 모퉁이 점, 너비, 높이로 정의된다. (b) 3차원 뷰포트는 2차원 뷰포트에 깊이 범위를 더한 것이다.

컴퓨터 스크린 위의 윈도우는 그 자신의 좌표계를 가지는데, 이를 윈도우 공간(window space) 혹은 스크린 공간(screen space)이라 부른다. 이 좌표계는 그림 7.4.a와 같이 윈도우의 왼쪽 아래 모퉁이에 원점을 가진다. 한편, NDC 정육면체 뷰 볼륨 안의 내용이 최종적으로 그려질 스크린 영역을 뷰포트(viewport)라고 한다. 뷰포트는 때로 윈도우 전체를 차지하지만, 반드시 그럴 필요는 없다. 윈도우에서 뷰포트의 범위는 glViewport에 의해 정의된다. 아래 보인 것처럼, glViewport의 첫 두 파라미터 minX와 minY는 뷰포트 왼쪽 아래 모퉁이의 스크린 공간 좌표를, 마지막 파라미터 w와 h는 각각 뷰포트 너비와 높이를 지정한다. 뷰포트의 종횡비(aspect ratio)는 w/h가 되는데, 이는 5.4.1절에서 정의한 뷰 프러스텀 파라미터 aspect와 동일하게 설정하는 게 좋다.

1
2
3
void glViewport(GLint minX, GLint minY, GLsizei w, GLsizei h);
minX, minY: lower-left corner point
w, h: width and height

실제 스크린 공간은 그림 7.4.b에 보인 바와 같이 3차원이다(10장을 공부하면 왜 3차원 공간이 필요한지 이해될 것이다). 3차원 스크린 공간의 z축은 스크린 안쪽을 향한다. 3차원 스크린 공간에서는 뷰포트 역시 3차원인데, 이는 glDepthRange(minZ, maxZ)를 호출하여 2차원 뷰포트에 z범위 [minZ, maxZ]를 더하여 정의된다. z범위의 기본값은 [0, 1]이다.

img 그림 7.5. 뷰포트 변환은 축소확대에 이은 이동으로 표현된다. 클립 공간과 스크린 공간 모두 왼손 좌표계임에 유의하자.

NDC로 표현된 2X2X2 크기의 뷰 볼륨은 glViewport와 glDepthRange로 정의된 뷰포트로 변환되어야 한다. 이 변환은 그림 7.5에 보인 바와 같이, 축소확대와 이동의 결합인데, 결합된 행렬은 다음과 같다.

\[\begin{pmatrix} \frac{w}{2} & 0 & 0 & minX + \frac{w}{2} \\ 0 & \frac{h}{2} & 0 & minY + \frac{h}{2} \\ 0 & 0 & \frac{maxZ - minZ}{2} & \frac{maxZ + minZ}{2} \\ 0 & 0 & 0 & 1 \\ \end{pmatrix} \tag{7.3}\]

이를 뷰포트 변환(viewport transform)이라 부르는데, 이는 2X2X2 크기의 뷰 볼륨 안에 있는 모든 정점에 적용된다.

많은 응용에서 뷰포트는 윈도우 전체 영역을 차지한다. 이 경우, glViewport의 두 파라미터 minX와 minY는 모두 0이 된다. 또한, 뷰포트의 z범위 [minZ, maxZ]가 기본값 [0,1]을 가진다면, 위 행렬은 아래처럼 간단해진다.

\[\begin{pmatrix} \frac{w}{2} & 0 & 0 & \frac{w}{2} \\ 0 & \frac{h}{2} & 0 & \frac{h}{2} \\ 0 & 0 & \frac{1}{2} & \frac{1}{2} \\ 0 & 0 & 0 & 1 \\ \end{pmatrix} \tag{7.4}\]


7.5 스캔 전환


뷰포트 변환은 모든 삼각형들을 스크린 공간으로 옮긴다. 그 다음, 래스터라이저의 마지막 세부 단계인 스캔 전환(scan conversion)이 수행된다. 이는 삼각형 내부를 채우는 프래그먼트를 생성한다. 조금 더 구체적으로 말하면, 개별 삼각형이 차지하는 스크린 공간의 픽셀 위치를 결정하고, 삼각형의 정점별 애트리뷰트를 보간하여 이를 각 픽셀 위치에 할당한다.

그림 7.6.a에서 뷰포트 내부의 삼각형을 생각해 보자. 스캔 전환을 위해서는 삼각형 정점의 xy좌표만을 사용하므로, 그림 7.6.b와 같은 2차원 뷰포트를 사용해 스캔 전환 알고리즘을 설명할 것이다. 이를 확대해 본 것이 그림 7.6.c의 삼각형인데, 이 내부에는 18개의 픽셀이 있다. 이들 각각의 픽셀 위치에 정점별 애트리뷰트가 보간되어 할당될 것이다.

스캔 변환은 실시간 그래픽스의 핵심 사항이고, 또한 이를 정확히 이해해야 GPU 파이프라인의 다음 단계인 프래그먼트 쉐이더를 올바르게 작성할 수 있다. GPU 개발사들은 나름대로 최적화된 스캔 전환 알고리즘을 구현하고 있는데, 이 절은 그 중 하나의 구현 기법의 뼈대만을 소개할 것이다. 정점별 애트리뷰트는 노멀과 텍스처 좌표를 기본으로 가지고 있지만 대체로 색상은 포함하지 않는다. 하지만 간단한 논의를 위하여 이 절에서는 모든 정점에 RGB 색상 값이 할당되었다는 가정 하에 그 중 R이 어떻게 보간되는지 설명할 것이다.

정점별 애트리뷰트는 우선 삼각형의 변을 따라 선형 보간(linear interpolation)된다. 그럼 7.6.c는 R1과 R3가 왼쪽 변을 따라 보간되는 것을 보여준다. 보간을 위해서는 각 변마다 몇 가지 기울기를 계산하는 것이 필요하다. 우선 수직 거리 y의 변화에 따른 R의 변화율을 나타내는 $\Delta R / \Delta y$이 필요하다. 또한 $\Delta x / \Delta y$도 필요하다. 그림 7.6.c의 가운데 상자를 보자.

수평 방향으로 이어진 스크린 픽셀들을 스캔라인(scan line)이라 부른다. 그림 7.6.c에서 삼각형과 교차하는 첫 스캔라인의 y좌표는 1.5인데, 그림 오른쪽 상자는 $\Delta R / \Delta y$과 $\Delta x / \Delta y$를 사용해 어떻게 이 교차점에서의 R과 x좌표가 계산되는지 보여준다. 이러한 초기화 단계를 거친 후, 그 다음 스캔라인부터는 $\Delta R / \Delta y$과 $\Delta x / \Delta y$를 더하는 작업을 반복하여 왼쪽 변을 따라 R과 x좌표를 구할 수 있다. 그림 7.6.d는 R을 보간한 결과를 보여준다.

img 그림 7.6. 겹선형보간을 통한 스캔 전환 (a) 3차원 뷰포트 안의 삼각형. (b) 2차원 뷰포트 안의 삼각형. (c) 왼쪽 변을 따라 두 가지 애트리뷰트의 기울기가 계산되고, 이 변과 첫 번째 스캔라인의 교차점에 애트리뷰트 값이 초기화된다. (d) 왼쪽 변을 따른 애트리뷰트 보간.

삼각형의 다른 두 변에 대해서도 같은 작업이 이루어진다. 그러면 우리는 각 스캔라인에 대하여 양쪽 끝 교차점에서 R과 x좌표를 가지게 된다. 이를 ${R_l, x_l}$과 ${R_r, x_r}$로 표기하자(여기에서 아래 첨자 l과 r은 각각 left와 right를 의미한다). 이제는 스캔라인을 따라 이 애트리뷰트들을 보간할텐데, 삼각형 변을 따른 보간과 동일한 기법을 사용할 것이다. 그림 7.6.e에서 y좌표가 4.5인 스캔라인을 보자. 왼쪽 상자에 보인 것처럼 먼저 $\Delta x (=x_r - x_l)$와 $\Delta R (=R_r - R_l)$을 사용하여 $\Delta R / \Delta x$를 계산한다. 그리고 스캔라인 상의 첫 픽셀 (2.5, 4.5)에 대하여 R을 초기화한다. 그 다음에는 오른쪽으로 한 칸씩 가면서 $\Delta R / \Delta x$을 더해서 픽셀 위치마다 R을 계산한다.

스캔 전환 단계에서 선형보간은 두 단계로, 즉 먼저 변을 따라, 그 다음에는 스캔라인을 따라 수행되었음을 상기하자. 이를 겹선형보간(bilinear interpolation)이라고 부른다. G와 B 역시 같은 방법으로 보간된다. 그러면 프래그먼트별 색상을 얻을 수 있다. 그림 7.6.f는 정점의 색상이 보간된 삼각형의 몇 가지 예를 보여준다.

img 그림 7.6.e. 겹선형보간을 통한 스캔 전환(계속). 스캔라인을 따른 애트리뷰트 보간

img 그림 7.6.f. 겹선형보간을 통한 스캔 전환(계속). 정점 색상이 보간된 삼각형들.

6.2절 예제 코드 6-1의 정점 쉐이더는 각 정점마다 v_normal과 v_texCoord를 출력했다. 이들은 스캔 전환 단계에서 프래그먼트마다 보간된다. 그림 7.7은 v_normal이 한 삼각형의 위 두 변을 따라, 그 다음에는 y좌표가 4.5인 스캔라인을 따라 보간된 결과를 보여준다. GPU 파이프라인에서 래스터라이저 다음 단계인 프래그먼트 쉐이더는 이렇게 보간된 v_normal과 v_texCoord를 이용하여 한번에 하나씩 프래그먼트를 처리한다.

img 그림 7.7. 삼각형 정점 노멀이 변을 따라 먼저 보간되고 그 다음 스캔라인을 따라 보간된다. 노멀의 x, y, z 좌표는 독립적으로 보간된다.


참고

This post is licensed under CC BY 4.0 by the author.