[DirectX12]

게임 수학 6. World 변환 행렬, View 변환 행렬, Projection 변환 행렬

럭키🍀 2024. 4. 21. 14:33

 

오브젝트가 어떠한 좌표계 변환을 거쳐 게임상에서 보여지는지 정리해보자.

 

처음 오브젝트를 만들면 그 오브젝트의 로컬에 배치된다. 로컬좌표는 모델링 될때 만들어지므로 모델좌표라고도 한다.

주로 발을 기준으로 0,0,0으로 모델링한다.

이 오브젝트(혹은 언리얼에선 배치가능한 액터)를 월드상에 배치되면서 월드 좌표계로 변환되고(그치만 상위 부모 오브젝트가 있으면 그 오브젝트 기준 로컬좌표를 유니티/언리얼에선 보여준다)

또 카메라 위치와 방향에 따라서 카메라 좌표계로 변환된다.

마지막으로 카메라 좌표계에서 2D로 화면에 나타내기 위해 Projection 변환 행렬을 통해 다시한번 좌표계가 변환된다.

 

원점뿐만 아니라 오브젝트를 이루고 잇는 모든 3D오브젝트의 모든 삼각형이 연산되어야하는데 좌표계 변환을 순서에 따라 원리와 예시를 알아보자.

 

1. World 변환 행렬

아래 사진에서 유니티엔진의 뷰포트를 보면 월드 상에 큐브와 유니티짱이 배치되어있다.

큐브를 Transform을 reset을 통해 월드상의 좌표 0,0,0에 배치시키고 유니티짱을 큐브의 하위에 x축으로 2만큼, z축으로 3만큼 이동해 배치하였다.

월드 내에 배치되어있는 모든 오브젝트는 자기만의 로컬좌표를 가지고 있는데 이 좌표들이 월드에 배치되면서 어떻게 변할지를 알아보자. (참고로 바로 월드에 배치되어있으면 Transform에서 보여주는 정보는 월드좌표이며 부모가 있다면 그 부모오브젝트 기준 로컬좌표이다. 그리고 이 오브젝트를 열면 원점을 기준으로 메시가 개인좌표를 가지고 분표?되어있다.) 여기서는 월드가 아닌 부모가 있을때의 월드로의 좌표계변환 행렬/ 월드에 배치된 오브젝트의 개인좌표에서 월드로의 좌표계 변환 행렬이라고 보면된다. 

 

1-1. 월드에배치한 오브젝트가 원점에서 위치이동만 있을 경우

만능 좌표계 이동행렬을 통해 어떻게 유니티짱의 좌표를 월드 좌표에서 인식하게 되는지 알아보자.

여기서 A좌표계가 유니티짱의 로컬 좌표계이며 B가 월드좌표계가 된다.

우선 행렬 M의 가장 마지막 줄은 위치이동에 관한 것으로 B좌표계 기준 A좌표계의 원점은 x축으로 2만큼, z축으로 3만큼 이동하여 Qx가 2, Qz가 3값을 가지게 된다.

또 B좌표계 기준 A좌표의 단위벡터들의 성분을 넣으면 연산했을 때 B 좌표계 기준 A좌표를 알 수 있었 는데 회전이 이루어지지 않았으므로 A좌표계와 B좌표계가 같은 벡터값들을 갖게된다. 즉 right, up, look 벡터를 B기준으로 구해야하는데 A와 완전히 일치하므로(회전이 일어나지 않아)ux uy uz에 1 0 0 / vx vy vz에 0 1 0 / wx wy wz 0 0 1이 들어가게된다.

 

 

1-2 . 또 이번엔 이동도 있었고 y축으로 45도 회전했을 경우이다.

위 내용을 바탕으로 하면 월드의 원점에 있는 큐브(B) 기준으로 했을 때 유니티짱(A)의 원점 좌표를 구하면 된다.

유니티짱의 로컬 좌표는 당연히 손대지 않았으면 모든 로컬좌표는 0,0,0이므로 상대?상속?좌표는(2,0,3) 였다 이걸 Qx Qy Qz값에 넣고

바뀐 B좌표 기준 A좌표의 단위벡터 성분을 ux~wz까지 또 넣어야하는데

유니티짱을 회전시키기 전에는 유니티짱과 큐브가 같은 곳을 바라보고있었다. (같은 단위벡터, 다른 좌표)

유니티짱을 회전하므로써 큐브의 단위벡터값에다가 회전행렬을 구해서 곱해주면 B좌표 기준 A(유니티짱)의 x,y,z 축의 값을 구할 수 있다.

 

 

이전에 배웠던 SRT행렬에서 회전행렬을 이용할 것이다.(아래 왼쪽) y축 회전 행렬만 일어났으므로 Right벡터값에만 ㅛ축 회전 행렬(Ry(Θ) )을 곱해서 값을 ux~wz까지 넣어준다.

 

그래서 우선 첫번째 Right벡터값(1 0 0)에만 y축회전 행렬을 곱하면

(c 0 s 0)이 나온다.

두번째 Up벡터에 y축 회전 행렬을 곱하면

(0 1 0 0 )이 나오고

세번째 행 Look벡터에 y 축 회전 행렬을 곱하면

(-s 0 c 0)이 또 나오겠군

 

여기에 Y축 기준 45도 회전을 햇을 경우를 구해보면

회전행렬을 공부했던 것을 바탕으로 y를 45도 회전했으니깐 피치 값이 변화한 행렬은 cos45= sin45 = √2/2이므로

(c 0 s  )     (√2/2   0 -√2/2)

(0 1 0 )      ( 0      1      0  )

(-s 0 c)      (-√2/2  0   √2/2)

(피치요롤은 유니티와 언리얼에서도 다른 값이군..)

이 행렬을

ux uy uz

vx vy vz

wx wy wz 이 행렬에다가 곱해준다. 

 

만약에 다른 축 회전도 있었다면 Rx( Θ )와 Rz( Θ )의 행렬곱을 넣어주면 된다.

이이렇게 구한 변환 행렬로 유니티짱을 이루는 모든 삼각형의 좌표를 연산하면 월드상에서의 좌표를 구할 수 있다.

 

또 만약에 계층구조하위에 있었다면 월드까지 부모의SRT를 구해가면 되는것..

 

정리하면 구 하위에 유니티짱을 배치시키고 모든 Transform값을 reset하면 정확히 같은 좌표계값을 가지고 잇는데 이 상태에서 Scale, Rotation, Translation을 적용해 유니티짱의 상태를 바꾼것이다.

부모의 위치에서 S,R,T 순서로 행렬곱을 하면 자식의 위치가 구해질 것이다.

 

1-3 계층구조를 가지고 있는 오브젝트의 예시

유니티짱을 월드좌표 5,0,5에 넣었고 구를 월드좌표 2,0,2에 배치했다가 유니티짱은 구 하위에 넣으니 3,0,3으로 바뀌었다. 부모를 기준으로 하는 로컬좌표가 부모가 월드에서 다른 오브젝트로 바뀌어서 좌표도 바뀌었다.

월드 - 구 - 유니티짱

또 이 상태에서 구를 회전시킨다음 유니티짱을 Z값을 줄이면 유니티짱이 바라보는 위치에서 앞으로 나가는게 아니라 구의 z방향으로 나가는걸 볼 수 있다.

모든 오브젝트가 자기가 속해있는곳 즉 부모의 좌표(계층구조내에서) 기준으로 움직이는 것을 알 수 있다. (로컬좌표란 부모기준 위치를 의미한다. - 개인좌표?는 변하지 않는 로컬좌표라고 부르자고 하자..ㅠ)

 

2. View 변환 행렬

월드좌표를 카메라 좌표로 변환하는 View변환 행렬

 

카메라를 기준으로 모든 월드 좌표를 변환해줘야한다.

카메라의 위치를 원점(0,0,0)으로 놓고 카메라가 보는 방향으로 룩벡터로 하여 모든 물체를 인지한다.

만약에 카메라 밖에 있는 물체는 화면에 그려지지 않을것이므로 연산해줄 필요가 없다는 특징이 있다.

카메라의 로컬 좌표에서 월드좌표로 변환하는 행렬(World변환행렬)을 구한다음 역을 취하면 월드좌표에서 카메라좌표로 변환 하는(View 변환행렬)이 되는 특징이 있다.. 

 

 

월드스페이스에서 뷰스페이스(카메라 좌표계)로 넘어가려면 변환해주는 뷰행렬을 구해줘야한다.

즉, 어떤 월드상의 좌표를 가지고 잇는 물체에 뷰행렬을 곱했더니 카메라 기준의 좌표가 나오도록 해야한다.

 

어떻게 변환 행렬을 구할지 방향은 두가지가 있는데 어떻게 생각하든 구한 변환행렬은 같게된다.

방향1)

좌표계 변환은 물체가 변하는게 아니라 기준점이 변하는것이다. 어떻게 카메라 기준으로 좌표를 구할지를 생각해보면 사실 카메라의 로컬영역으로 모든 물체를 끌고가는것과 같다..!

모든 물체들이 카메라의 로컬스페이스로 들어온 것처럼 역으로 연산해주는 방법.

 

방향2)

만약에 카메라가 정확이 월드의 원점에 위치하다가 오른쪽으로 이동하면 모든 물체들이 왼쪽으로 이동하는것처럼 보이게 된다. 카메라가 위로 회전하면 아래로 이동하는것처럼 보이게 되고. 결국 카메라는 가만히 잇는데 모든 물체들이 반대방향으로 움직인다고 생각하면 된다.

 

 이 중 첫번째 방향으로 우선 변환 행렬을 구해보자.

 

만약에 카메라가 월드 좌표계의 원점에 있었다고 가정하면 위와같은 좌표를 가지고 잇을 것이다. B좌표계(카메라)기준 A좌표계(월드)의 단위벡터 성분이 일치하므로.

 이 상태에서 월드스페이스에서 카메라를 기준으로 하는 좌표계로 넘어간다는 것은 카메라의 로컬스페이스로 넘긴다는것과 같은 의미다.

그래서 카메라의 로컬스페이스를 월드의 좌표계로 변화하는 변환 행렬 SRT를 구한 다음에 여기에 역행렬을 구하면 다시 월드좌표에서 카메라의 좌표로 변환하는 행렬이 된다.

 

카메라이동의 반대로 물체가 이동한다고 생각해도 같은 공식이 나오게 된다. 공식 결과는 같군..

결국 카메라가 로컬에서 가지고 있는 Right,Up, Look벡터 기준으로 바뀌는것은 같다..

 

카메라도 뷰스페이스를 만들때 카메라를 변환하는 공식을 만들어 볼 것인데

먼저 카메라도 SRT를 통해 로컬에서 월드로 변환하는 행렬을 구할 수있는데 이것의 역행렬이라고 생각하면 된다.

SRT 순으로 행렬곱을 구하고 역행렬을 구하야하는데 카메라의 스케일을 변환하는게 아니므로 S는 의미가 없고 회전과 이동, R과T의 곱의 역행렬만 구하면 된다.

그리고 (RT)-1 = T^(-1)* R^(-1)이다.  당연히.. (2*4)(4*2)의 역행렬은 (4*2)(2*4)가 될 것이므로

 

Translate행렬의 역행렬은 어떤 방향으로 이동할 때 역방향을 표현해주면 돼서 성분에 -를 붙였다. -Cx -Cy -Cz

또 Rotation행렬은 x,y,z 직교행렬이다.(모든 행을 서로 곱하면 0 이된다 = 모든 방향이 직각을 이루고 있다.)

직교행렬에 대해서 역행렬을 구하는 공식에 대해서 바로 Transpose를(대각선을 \기준으로 뒤집는것) 때려버리면 된다.

그래서 Rightx Righty Rightz가

Rightx

Righty

Rightz 이런식으로 이동한것..

그리고 빨간 네모 안의 행렬 마지막행은 (-Cx Rightx)+(-Cy*Righty)+(-Cz Rightz)를 ->(-C) *->(Right)이라고 표현한것은 x,y,z가 있으면 벡터의 내적으로표현한것이다.ㅋㅋ

 

 

추가로)

직접적으로 SRT연산으로 구해도 되긴 하지만 보통은 하나의 함수로 이루어져잇다. 카메라의 위치와 카메라의 룩벡터를(빨간화살표) 정한 상태에서 임의로 업벡터(보통은 0,1,0 짧은 초록색화살표)를 지정해주면 룩벡터와 업벡터를 기준으로 외적을 한 다음에 함수 내부에서 반대방향으로 가는 둘과 수직하는 3의 방향으로 하나를 구한 다음에 이걸 이용해 다시 원래 의도햇던 모든 방향에 직각하는걸 구한다.?

 

그래서 XMatrixLookAtLH함수에 세 인자만 넣어주면 저 빨간 네모안에 있는 것같은 값이 구해진다.

 

 

 

 

 

 

 

3. Projection 변환 행렬

카메라가 비추는 공간에 있는 물체들을 카메라가 바라보는 방향을 기준으로 좌표계를 변환했었다면

다음엔 이들을 2D로 변환하는 방법을 즉, 투영하는 것을 알아본다. 카메라 좌표계(View Space)에서는 카메라의 위치를 원점으로 해서 카메라가 바라보는 방향을 Look벡터로 하는 축이 있었고 이 z축이 카메라로부터 얼마만큼 떨어져있는지 깊이값을 갖게한다. 그런데 이 값들을 다 없애서 납작하게 만들어 버릴것이다.

 

카메라와 물체가 멀리있으면 작게 보이게 되고 카메라에게 다가오면 크게보이는 원근법도 적용해야한다.(카메라 종류에 따라서?) 보통은 이게 아니라 원근투영을 통해서 카메라로부터 깊이 차이에 따라 2D로 커지거나 작아지거나 어떻게 변하는지 달라지는게 목표다.

투영좌표계에서는 물체 뒤에 가려진 물체는 그리지 않는것? 도 배우게 된다.

 

아래 그림에서 육면체모양을 절두체라고 한다. 절두체 안에 있는 오브젝트들을 모두 2D화면에 원근법을 적용해 그리는 방법을, Projection 변환 행렬을 알아보자.  

 

위 오른쪽 그림에서 파란 사각형은 x와 y축의 -1에서 1사이의 값을 가지고 있다. 그리고 이 사이의 값을 오브젝트들이 가지도록 하는 것이 목표이다.

절두체를 옆에서 봤다고 하면 z축과 y축을 가지고 있게 된다. (오른쪽 그림의 왼쪽 그래프?) 여기에서 깊이에 해당되는 z축으로 높이가 1 밑면이 1인 이등변 삼각형이 있고 이 방향으로 계속 뻗어가면 빨간색 선으로 된 사각형이 있다. z축의 깊이가 얕은 부분을 near의 n, 깊은 곳은 far의 f로 표시해 두었다.

그리고 n과 f사이에 있는 빨간색 사각형 영역이 카메라 영역이라 그 사이에만 있는 오브젝트들만 그릴것이라고 가정하자.

그러면 그 앞의 파란선에 투영해본다고 가정해보자. 아래처럼 😊그림이 빨간 영역에 있던것이 파란선에 그려지게 된다. 

그러면 월드에 있는 😊오브젝트가 어떤 좌표를, 어떤 크기를 가져야할지 기본적으로 생각해보면

😊의 위치 벡터가 (x,y,z)라고 했을 때 z값을 모든 오브젝트들이 같은 값을 가게 되고, 그 깊이 비에 따라서 달라지므로 x,y를 각각 z로 나눈 값이 된다.   

기본적으로 변화된 X,Y값은 x/z, y/z값을 갖게 되는데 고려해야할 사항 두 가지가 있다.

화면비와 카메라 앵글이다.

 

모니터 마다 다른 스크린 비율을 가지고 있기에 이를 반영해주려면 각각 x,y 중 한곳에만 같은 비율을 같도록 화면비 r = w/h 로 나누어줘야한다. 그래서 x에만 화면비를 곱하면X = x/rz, Y = y/z가 된다. 만약에 화면비가 800:600 이었다면 r은 1.3이 된다.

 

 

 

 

그리고 또다른 고려사항은 카메라 앵글이다. 위 예시에서는 높이와 밑변이 1인 삼각형 두개였으므로 카메라가 이루는 각이 45도 였다. 주황색 선이 z축을 가운데로 이루는 각을 α라고 했을 때 밑변이 1, 각이 α/2이므로 tan(α /2)가 파란색 선의 반절이라고 할 수 있다. 그래서 이것도 깊에이 비례해 x,y를 구한 값에 보정을 위해 나눠줘야한다.

그래서 x/z*r*tan(α /2)  , y/z*tan(α /2)  값을 가지게 된다.

 

 

다시한번 아래 파란색 사각형 안에 위치하도록 x,y 가 -1~1사의 같을 같도록 하는것이 목표이다.

  그런데 위 왼쪽 식으로 변환 행렬을 구하는데는 무리가 있다. 우선 z값을 무시하고 행렬을 구해보면 오른쪽 처름 구할 수 있다. 그러나 z값은 깊이값으로 이에 비례하게 만들어야하는데 z값은 변동값이라 M에 넣을 수 없고 또 v에 넣어도 z값이 이미 -1에서 1사이의 값으로 보정된 값이 들어갈 때도 있어 정확한 연산을 하기 힘들다.

그래서 M의 마지막 행의 마지막 요소와 그 위 요소의 위치를 바꾸고 z값이 보존되도록 초기 위치벡터v 의 마지막값에 v를 넣는 방법이 있다.

그러면 위처럼 연산 결과 V가 z값을 가지고 나오게 된다.

그리고 행렬 연산으로 나온 V = ( x/r*tan(α /2)  , y/tan(α /2) , z, z) 에서 모든 값을 z로 나누게 되면 모든 값이 -1과 1사이의 값을 가지게 되고 마지막 요소도 1을 갖게 된다.

그런데 여기서 끝이 아니다!

 

변환 행렬에 A,B 임의의 상수를 넣어서 깊이값 z도 0에서 1사이의 값을 갖게한다. 왜..? 평면이지 않나 싶은데, 촬영 대상은 되는 절두체의 near와 far사이에 있는 오브젝트들인데 near값이 0, far이 1로 그 사이의 값을 갖게 만든다. 그래서  near일때는 값이 0이되고 far일때는 1이 되는 임의의 상수를 넣어주게 된다. 

그럼 최종적으로 위처럼 변환행렬이 되는데 따로 외울것까지는 없고.. 구냥 뷰스페이스에 있던 것들을 투영시켜서 -1에서 1사이의 값으로 만드는 프로젝션 행렬이라고만 알 고 있자.. 

 

결국 프로젝션 변환 행렬은 뷰스페이스에 있는 오브젝트들의 벡터값을 (x,y의 값을)-1에서 1사이의 값으로 만드는 것이 목표이고, z값은 0에서 1사이의 값을 갖도록 하는 것이 목표이다. 또 변환행렬로 구한 값의 마지막 요소인 z값으로 다 나누는 단계까지 거쳐야 최종 값을 구할 수 있다. 

 

이렇게 투영단계를 거치면 rasterize단계에서 -1,1사이의 값을 벗어나는점들은 다 걸러내고 깊이도 0과 1 사이에만 있는 애들만 남기고 통과 된 애들을 z값으로 나누기 한 다음에 최종적으로 넘어가서 픽셀쉐이더 단계에서 색상만 구하면 된다.

 

 

지난 실습에서는 물체를 그릴때 변환 행렬을 신경쓴 적이 없고 삼격형 좌표를 그렸더니 화면에 잘 떴다. 그 이유는 우리가 CreateGeometry()에서 버텍스 배열에 처음부터 -1에서 1사이의 값들을 넣어줬기 때문이다. 처음부터 정점을 만들 때 실질적으로 예상하고 투영된 최종 결과물(NDC라고하는) 값인 -1에서 1사이의 값을 세팅해줘서 중간 과정을 세팅해도 문제가 없었다. 그래서 쉐이더에서도 이값을 그대로 받아서 그렸기 때문에 잘 보이는 것이었다.

그러나 실제로 앞으로는 이 좌표계 변환 과정을 거쳐서 연산되고 그려지도록 해야한다.

ㅇ로컬->월드->카메라->투영을 거쳐 -1과 1사이의 위치 벡터값들을 갖게 되면.. 마지막으로 하나가 더남았다!!

처음에 뷰포트를 만들어 화면 크기인 뷰포트의 크기를 예를들어 800,600으로 SetViewport로 지정해줬는데 이 크기에 따라서 화면의 최상단 좌표와 width, height는 얼마나 될것인지, 최소/최대 깊이는 얼마냐에 따라서 세팅해 두었던 값들이 이 비율에 맞게끔 픽셀로 전환되어야한다. 결국 화면에는 좌측 하단이 (0,0)으로 하고 우측 상단이 (800,600)으로 하는 픽셀 값을 갖고 잇으므로 -1에서 1사이의 값이 아닌 '스크린 스페이스'로 비율에 맞게 적용되어야한다. 

 

 

다음시간부턴 전에 만들었던 그 양식에 배웠던 좌표계 변환 행렬들을 적용할 건데 큰 양식은 같은데 Render안에서 Update내에서 그릴 데이터를넣어주는데 그 transform데이터가 달라지고 쉐이더도 이에 맞게 행렬을 곱하도록 바뀔 예정이다.

 

서버입장에서는 월드 좌표를 신경써야한다. 다만 클라 입장에서는 5단계의 모든 좌표를 고려해야한다. 바라보는 방향(로컬)도 고려해야하고 실제로 이동할 때는 월드 좌표로 어디로 이동하는지를 고려해야한다. 또 쉐이더에서도 모든 물체를 로컬 기준으로 연산해야할 때도 있고 반대로 UI처럼 카메라에만 보여줄 때는 뷰스페이스(카메라 영역으로)끌고가는게 편할 때도 있다. 그래서 클라 입장에서는 모든 전과정의 다 알아야한다. 엔진에서는 뷰포트의 오브젝트를 누르면 선택이 되지만 사실 스크린 스페이스에서 뷰스페이스에서 월드스페이스까지 타고 역으로 계산해서 그 좌표를 선택하면 오브젝트를 선택 것이다. 복잡한 과정을 거치는 것이었다.. 

 

 

4. Screen 변환 행렬

Projection 좌표계?로 변환된 좌표들을 실제로 스크린 비율에 맞게 변활 때는 어떻게 되는지도 간단히 알아보자.

 

'[DirectX12]' 카테고리의 다른 글

게임수학 5. 좌표계 변환 행렬  (0) 2022.08.03
게임수학 4. Scale, Rotation, Translation 변환행렬  (0) 2022.08.03
프로젝트 설정  (0) 2021.12.11
렌더링 파이프라인  (0) 2021.12.11