김서버의 프론트엔드 일기

컴퓨터그래픽스 - 4 정점처리 본문

공부하는거/컴퓨터 그래픽스

컴퓨터그래픽스 - 4 정점처리

kimSerVer 2022. 9. 9. 14:38

 GPU는 폴리곤메시를 입력 받아, 3차원의 폴리곤 메시를 2차원 형태로 바꾸고, 2차원 폴리곤 내부를 차지하는 픽셀들의 색상을 결정한다. 이 픽셀들은 컬러 버퍼에 기록되고, 주기적으로 스크린으로 복사된다.

 GPU의 렌더링은 파이프라인으로 구조로 구현되며, 그 순서는 다음과 같다.

 

 정점쉐이더 -> 래스터라이저 -> 프레그먼트 쉐이더 -> 출력 병합기 -> 출력

 

 여기서 쉐이더는 프로그래머가 프로그래밍 할 수 있는 부분이다. GPU를 렌더링 하기 위해서는 정점쉐이더와 프래그먼트 쉐이더 프로그램을 작성하면, 래스터라이저 출력 병합기는 하드웨어 단계에서 정해진 연산을 수행한다.

 

 정점 쉐이더는 정점배열에 저장 된 모든 정점에 대해서 변환을 비롯한 다양한 연산을 수행한다. 래스터라이저는 삼각형들을 조립한 후, 각 삼각형에 프래그먼트를 생성하여, 컬러 버퍼의 한 픽셀을 갱신하는 데이터를 생성한다.

 예를 들면 100개의 픽셀을 차지하는 삼각형은 총 100개의 프래그먼트가 생성되고, 래스터라이저가 출력한 프래그먼트는 하나하나의 프래그먼트 쉐이더로 매개변수로 입력되어, 라이팅과 텍스처링 등의 작업을 거쳐 색상이 결정된다.

 출력병합기는 만들어진 프래그먼트와 현재 컬러 버퍼에 저장된 픽셀 중 하나를 선택하거나 색을 결합하여 컬러 버퍼를 갱신한다.

 

 이 중 정점 쉐이더는 3번의 변환을 하게 되는데, 이는 다음의 순서와 같다.

 

 오브젝트 공간  -- 월드 변환 --> 월드 공간 -- 뷰 변환 --> 카메라 공간 -- 투영 변환 --> 클립공간

 

 이제 이 하나하나의 변환에 대해서 알아보자

노멀의 월드 변환

 각자의 오브젝트 공간에서 하나의 월드 공간으로 모으는 것이 월드 변환의 역할이다.

 월드 변환이 아핀변환으로만 구성 되었다면 [ L | t ]와 같이 나타낼 수 있다. 여기서 L만을 가지고 변환을 일으키며 누적된 선형 변환만 계산을 하게 된다.

 (L^-1)^T 에 의해 변환된 노멀은 L에 의해 변환된 삼각형과 항상 수직관계를 유지한다. 즉 노멀변환을 위해 L대신 L의 역전치행렬을 사용해야한다.

 예를 들어 L은 (0.5, 1)의 2차원 축소 확대 행렬이고 (1,4)와 (4,1)을 연결하는 벡터를 변환하게 되었을 때, (1,4)와 (4,1)을 정규화 하면 (1 / Math.sqrt(2), 1 / Math.sqrt(2))이고 이를 (0.5, 1)의 역전치 행렬을 만들어 행렬-벡터 곱셈을 하게 되면 다음과 같은 식으로 곱셉을 하게 된다.

2 0 X 1 / Math.sqrt(2) = Math.sqrt(2)
0 1 1 / Math.sqrt(2) 1 / Math.sqrt(2)

 이제 역전치 행렬과 벡터를 곱셈을 한 것을 정규화를 하게 되면 (2 / Math.sqrt(5), 1 / Math.sqrt(5))가 되고, 이는 (1,4)와 (4,1)의 점을 각각 변환 행렬을 곱셈한 결과인 (0.5, 4), (2, 1)을 연결한 벡터의 수직인 벡터를 구한 값과 같게 된다.

 

뷰 변환

 월드 공간에 모든 물체를 모으고 나서는 월드 공간의 특정영역을 스크린에 렌더링하기 위한 가상 카멜의 위치와 방향을 잡아야한다.

 카메라의 위치와 방향은 EYE, AT, UP파라미터를 통해 정의된다.

 

 EYE: 월드 공간에서의 카메라의 위치이다.

 AT: 월드 공간에서 카메라가 바라보는 기준점이다.

 UP: 카메라의 상단이 가리키는 방향을 묘사하는 벡터이다. 대부분의 경우 UP은 월드 공간의 수직방향으로, 즉 Y축으로 설정이 된다.

 

 월드 공간에서 카메라공간으로의 이전을 뷰변환 혹은 카메라 변환이라고 부른다.

 뷰 변환의 첫단계인 이동은 변위 벡터 O-EYE로 정의 된다. O은 (0,0,0)이므로 변위 벡터는 (-EYEx, -EYEy, -EYEz)가 된다.

1 0 0 -EYEx
0 1 0 -EYEy
0 0 1 -EYEz
0 0 0 1

 예를 들어 EYE 월드 공간 좌표가 (18,8,0)이면, (10,2,0)에 있는 월드 공간에 있는 점은 위 행렬과 행렬-벡터 곱을 하면 (-8,-6,0)으로 이동하게 된다.

 카메라공간의 기저 (u,v,n)은 회전을 통해서 월드공간의 기저 (e1,e2,e3)와 이동을 통해서 원점을 공유하고, 회전을 통해서 포개어 질 수 있다.

 회전을 통해서 공간 이전을 하게 되는 것을 기저 이전이라고 부른다. 

 그래서 회전행렬 R과 이동행렬 T를 4 X 4 로 결합한 행려은 다읍과 같아진다.

ux uy uz -u * EYE
vx vy vz -v * EYE
nx ny nz -n * EYE
0 0 0 1

오른손 좌표계와 왼손 좌표계

 월드 공간에서 카메라 공간으로 좌표들을 변환 할 때, 엔진이 오른손 좌표계인지 왼손 좌표계인지에 따라서 실체로 물체가 투영되는 방향이 달라지게 된다.

 그래서 보통 OpenGL에서 direct3D로 포팅하게 될 경우, 물체와 카메라 파라미터의 z좌표의 부호를 바꾸면 동일하게 화면에 출력이 되는 것을 볼 수 있게 된다.

투영 변환

 투영 변환은 카메라로 보이지 않은 삼각형은 화면에 렌더링 하지 않고 오직 카메라에만 보이는 삼각형만 그릴 수 있도록 잘라내고 이를 화면에 뿌리기 위해서 변환하고, 이렇게 만들어진 공간을 클립 공간이라고 부른다.

 THREE.js를 사용하면 실제로 fov, aspect, n, f 네가지 파라미터를 받는다. 이런 매개변수를 받아서 만들어진 카메라의 가시 영역을 뷰 볼륨이라고 부른다. 각각의 매개변수는 다음과 같다.

 

 fov: y축 기준의 시야각을 말한다. 즉 시야를 얼마나 넓게 볼 것인가를 정하게 된다.

 aspect: 뷰 볼륨의 종횡비를 말하는데 fov와 aspect에 의해 정의된 뷰 볼륨은 꼭지점을 원점에 두고 -z축으로 무한한 크기의 피라미드가 된다.

 n: near로 전방 평면을 말한다. 원점으로 부터 거리를 넣어 주어서 어디부터 볼 것인가를 설정하고, 그보다 낮은 절대값이 작은 z를 가진 삼각형들은 자르게 된다.

 f: far로 후방평면을 말한다. 원점으로 부터 거리를 넣어 주어서 어디까지만 볼 것인가를 설정하고, 그보다 큰 절대값을 가진 z를 가진 삼각형들은 자르게 된다.

 

 이렇게 유한한 크기의 뷰 볼륨을 바꾸면 이를 뷰 프러스텀 혹은 절두체라고 부른다. 뷰 프러스텀 안에 있는 물체만을 보이도록 삼각형을 자르는 작업을 클리핑이라 부르는데, 이는 래스터라이저에 의해 수행된다.

 

 이제 클리핑이 된후에 피라미드의 모양의 뷰 프러스텀을 좌표계의 주축에 나란한 2 X 2 X 2 크기의 정육면체 뷰 볼륨으로 변형 하는 것 까지 하면 투영 변환이라고 부르고 이렇게 만들어진 공간을 클립 공간이라고 부른다.

 

 뷰 프러스텀과 원점 사이에 놓여 있으면서 z축에 수직인 가상의 투영 평면을 상상했을 때, 피라미드에서 넓게 퍼져 나가있는 부분을 줄여 나가면서 결국 일자로 만들게 될 것이다. 

 이렇게 일자로 만들었을 때에는 넓게 퍼져나가있는 부분에 가까울 수록 더 작아지게 되는데, 이런 방식으로 원근법이 구현이 된다.

 

 뷰 프러스텀을 정육면제 2 X 2 X 2 크기의 정육면체로 변영하는 투영 행렬은 다음과 같다.

cot(fov/2) / aspect 0 0 0
0 cot(fov/2) 0 0
0 0 (f + n) / (f - n) (2nf) / (f - n)
0 0 -1 0

 이 식은 오른손 좌표계를 기준으로 작성이 되어있고 왼손 좌표계를 기준으로는 z축에 해당하는 부분의 부호를변경만 해주면 된다.

 

 여기까지가 정점쉐이더를 이용해서 나타낸 변환에 대한 것들이다. 여태까지 게임하면서 큰 월드는 전부 렌더링 하지 않고, 보이는 것만 렌더링 한다고 하는데, 그 원리가 바로 이런 방식으로 구현이 된 다는 것을 알 수 있었다.