Game Develop

[DirectX11] 3D에서의 WVP변환과정. (렌더링파이프라인) 본문

ComputerGraphics/DirectX

[DirectX11] 3D에서의 WVP변환과정. (렌더링파이프라인)

MaxLevel 2021. 5. 24. 19:28

WVP변환이란?

                                                                                                                         

WVP변환은 컴퓨터그래픽스에서 최종적으로 2D모니터에 출력하기 위한 위치변환과정 중 하나이고, 그중에서 Clip Space로 변환하기위한 과정이다. (DirectX 기준)

 

 

 

World

                                                                                                                         

어떤 공허한 공간이 있고 여기에 정사면체 하나가 있다고 가정하자.

이 정사면체의 (0,0,0)은 자신의 중심점이고, 이건 이 정사면체뿐만 아니라 공허한공간에 있는 모든 오브젝트들이 동일하다. 이 공허한 공간에서의 (1,0,0)은 중심점에서부터 1,0,0에 위치한 지점일 뿐이다. 

이런 각 오브젝트의 공간을 ObjectSpace, LocalSpace, ModelSpace 등으로 부른다.

각자의 0,0,0이 의미하는 지점이 다르기 때문에 world좌표라는 기준을 통해 우리가 컨트롤할 수 있는 실세계로 끌어내려야한다.

여기서 실세계로 끌어내린다는 말은, 각 오브젝트의위치를 같은 월드좌표계에 위치시킴으로써 월드좌표로 각 오브젝트의 위치설명할 수 있게되는 과정을 말한다.

그래서 우리는 각 오브젝트의 로컬좌표에 worldMatrix값을 곱해줌으로써 각 오브젝트들의 위치를 우리 세계(월드공간)로 끌어내릴 수 있게된다. 이 과정을 통해 일단 오브젝트들의 위치를 x,y,z로 설명할 수 있게 된다.

 

 

View

                                                                                                                         

ViewSpace(뷰공간)는 카메라를 기준으로 하는 공간이다. 결국 카메라로 비춘 공간을 모니터에 출력해야 하기때문에 오브젝트들을 카메라공간을 기준으로 재배치해야한다(ViewSpace로의 전환). 뷰공간으로의 전환을 위해 ViewMatrix(뷰행렬)를 구해야한다. 관련내용은 이득우님의 게임 수학블로그를 참고했다.  https://blog.naver.com/destiny9720/221423054915

 

3D에서 뷰행렬을 구하기 위한 방법으로는 2가지가 있다고 한다.

    1. 카메라의 오일러 회전값을 이용하는 방법.

    2. 카메라가 향해야 하는 물체의 위치를 알고 있는 경우.

 

그리고 이 2가지 방법에 대해 이득우님은 아래와같이 언급했다.

보통 뷰행렬에 대해 검색을 하면 2번방법으로 뷰행렬을 유도하는 글이 많은데, 실제 게임엔진에서는 1번을 많이 사용한다고 한다. 유저 인터페이스를 생각해볼 때 카메라가 바라보는 물체를 지정해서 구도를 잡기보다 사용자가 직접 카메라 회전값을 설정하는 것이 더 편리하기 때문이다. 따라서 두 가지 방법을 모두 알아두는 것이 좋다.

 

실제로 뷰행렬 구하는 과정에 대해 많은 검색을 했었는데 대부분이 2번방법이였고 학원에서도 프레임워크를 짤 때XMMatrixLookAtLH() 함수를 통해 뷰행렬을 구했었다. LookAtLH 함수는 내부구현이 2번방법으로 되어있는 함수이다.

어쨌든 두가지 방법 다 알아보도록 하자.

 

 

첫번째 방법같은 경우는, 카메라의 새로운 트랜스폼을 적용하는 순간 정해지는 오일러값인 Roll/Pitch/Yaw의 각 회전행렬을 이용한 방법이다. 어떠한 오브젝트의 트랜스폼 로테이션행렬은 다음과 같이 표현한다.

   R(euler) = R(roll) * R(pitch) * R(yaw)

하지만 이 회전은 월드공간에서의 카메라의 회전값이다. 우리가 변환하고자 하는 뷰공간은 카메라를 기준으로 하는 좌표계이다. 즉, 카메라는 가만히 있고 다른 오브젝트들이 회전해야한다. 월드상에서 카메라가 오른쪽으로 30도 돌았다고 가정하면 뷰공간에서는 카메라는 가만히있고 다른 오브젝트들이 왼쪽으로 30도를 돌려야 한다는 의미이다.

회전뿐만 아니라 이동도 마찬가지다. 스케일은 카메라에 의미가 없기 때문에 제외한다면 이 계산에서 사용할 카메라의 행렬은 SRT에서 S를 뺀 RT가 된다. 이것의 역행렬을 다른 오브젝트의 행렬에 곱해주면 된다.

정리하자면 다음과 같다.

 

 뷰행렬 = 카메라의 RT의 역행렬

 RT의 역행렬 = T의 역행렬 * R의 역행렬.

 R의 역행렬 = R(yaw)의 역행렬 * R(pitch)의 역행렬 * R(roll)의 역행렬 

 

 최종적으로는 T의 역행렬 * R(yaw)의 역행렬 * R(pitch)의 역행렬 * R(roll)의 역행렬 이다. (DirectX 기준입니다)

쉽게 비유하자면 월드에서 카메라가 원점기준으로 우측으로 10만큼 이동했다고 가정하면, 뷰공간 기준에서는 카메라는 가만히 있고 다른 오브젝트들이 좌측으로 10만큼 이동한 것이다. 그리고 바로 그 좌표가 오브젝트의 뷰공간에서의 좌표이다.

참고로 회전행렬은 직교행렬이기 때문에 전치행렬과 역행렬이 같다. 그렇기 때문에 굳이 계산비용이 더 큰 역행렬을 계산할 필요없이(DirectX에서 함수로 지원을 하긴한다) 전치행렬로 대신 쓰면된다.

 

 

 

두번째 방법도 최종적으론 '카메라의 이동행렬의 역행렬'과 '회전행렬의 역행렬'을 곱해서 구해지는건 동일하다.

두번째방법에대해서 아마 관련자료를 찾으면 아래와 같이 표현한 자료들이 있을 것이다.

 

'타겟의 포지션에서 카메라의 포지션을 빼서 구한 벡터를 Z축 기저벡터로 정한다음에 임시로 (0,1,0)이라는 업벡터와 외적해서 X축 기저벡터를 구한다. 그리고 Z축과 X축을 외적해서 Y축 기저벡터를 구하면 카메라의 3가지 축에대한 기저벡터를 모두 구할 수 있게 된다. 각 기저벡터를 4X4의 빈행렬에 행우선으로 넣고, _41,_42,_43 에는 각 기저벡터와 카메라의 포지션값(카메라의 월드위치벡터)을 내적한값을 넣으면 뷰매트릭스가 완성된다.'

 

즉, 두번째방법의 최종 뷰행렬은 아래와 같은 상태가 될 것이다.

m_matTransform[CT_VIEW] = D3DXMATRIX

(

     m_vAxis[AX_X].x, m_vAxis[AX_Y].x, m_vAxis[AX_Z].x, 0,    // X축 기저벡터

     m_vAxis[AX_X].y, m_vAxis[AX_Y].y, m_vAxis[AX_Z].y, 0,    // Y축 기저벡터

     m_vAxis[AX_X].z, m_vAxis[AX_Y].z, m_vAxis[AX_Z].z,  0,   // Z축 기저벡터

     DotX ,     DotY,     DotZ ,    1 // 각 축과 카메라위치벡터와 내적한값

);

 

그런데 두번째 방법도 결국 T역행렬 R역행렬을 곱해서 구해진다고 언급했는데 왜 첫번째 방법의 행렬의 표현과 많이 달라보이는 것일까? 위의 두번째방법의 뷰행렬도 사실 아래의 이미지의 유도식을 보면 알 수 있듯이, 두 행렬의 곱에대한 결과물인건 동일하나, 유도하는 과정이 다르기 때문이다. 

출처 : https://www.youtube.com/watch?v=_w8Q6WYjqj0&list=PLz0gLJgtHNcF90dMkTdAuMMP_2NRTD6IM&index=15

 

참고로 위 이미지의 이동행렬은 카메라의 T역행렬을 의미하는것이다.

 이미지를 보면, 기존의 뷰행렬을 구하는 과정을 생각해보면 R의 역행렬자리에 '좌표계변환행렬'이란것이 자리하고 있는것을 볼 수 있다. 결국 같은 기능을 하는 행렬이란건데(회전행렬의 역행렬) 차이점은 이 회전행렬을 구하는 방법이 첫번째방법과 두번째방법이 다르다는 것이다.

 

첫번째 방법은 그냥 말 그대로 카메라 트랜스폼의 각 축의 회전행렬을 사용한거고, 두번째 방법은 카메라의 기저벡터를 이용해서 회전행렬을 만드는 방식이다. 어떠한 오브젝트의 로컬기저벡터들을 행우선으로 행렬을 만들었을 경우(다렉기준), 그 행렬은 회전행렬과 동일하다는 것이다. 카메라의 기저벡터들로 유도했기 때문에 하나의 좌표계가 생성된것이고 이것의 역행렬은 카메라좌표계로 유도하는 행렬이기 때문에 '좌표계변환행렬'이라고 표현해도 무방한것이다.

'로컬기저벡터들로 구성한 행렬이 왜 회전행렬인가?'에 대한 의문점은 이득우님의 블로그에서 1강부터 벡터란 무엇인가부터 정독하면 이해할 수 있다.  벡터에 대해 수학적으로 깊게 접근해서 쉽게 설명해주셨기 때문에 게임수학을 공부하는 사람들이라면 보기를 권장한다.

사실 나도 아직 많이 헷갈린다.. (위의 내용은 6강에 나온다)

그리고 최종뷰행렬의 _41,_42,_43에 각 기저벡터와 카메라위치벡터를 내적한값을 대입했었다고 언급했었는데, 그 이유는 위 이미지의 유도식을 보면 알 수 있다. T역행렬과 좌표계변환행렬(R역행렬)을 곱했을때 _41 부분을 봐보자.

-(CxVx + CyVy + CzVz) 라는 결과물이 나오게 되는데 바로 -C라는 벡터와 V라는 벡터의 내적을 계산하는 식이다.

 

참고로 두 벡터의 내적은 아래와 같이 정의한다.

출처 : https://m.blog.naver.com/PostView.naver?blogId=mindo1103&logNo=90103350914&proxyReferer=https:%2F%2Fwww.google.com%2F

 

즉 -(CxVx + CyVy + CzVz)라는 식은 -C벡터와 X축기저벡터에 대한 내적식이라는 것이다. 나머지 _42, _43도 마찬가지로 -C벡터와 Y축,Z축 기저벡터를 내적한것이다. 결국 이러한 유도를 통해 두 행렬을 곱했을 때 결과적으로 두 벡터를 내적한값이 요소로 들어가기 때문에 그냥 최종뷰행렬만 바로 구하고자 할때는 바로 내적한값을 넣어버리면 되는것이였다. 

 

사실 이런 유도식에 큰관심이 없다면 일단은 그냥 뷰행렬은 카메라의 역행렬이란것만 알고 넘어가도 된다.

 

 

Projection

                                                                                                                         

 

WVP변환에서의 마지막 과정인 Projection(투영) 과정이다.

투영과정은 현재 뷰공간의 정점들을 Clipping Space라는 일종의 정규화된 3차원으로 끌고오는것(투영)을 말한다.

참고로 완전한 원근법을 적용시키기 위해서는 '원근나누기'도 거쳐야하지만 Directx 렌더링파이프라인에서는 나중에 레스터라이즈단계에서 원근나누기를 실행한다.

어쨌든 여기서의 투영행렬을 곱해줌으로써 생기는 결과는 클리핑공간으로의 변환이다.

이렇게 정규화된 공간으로 가져올 경우 나중에 레스터라이즈단계에서 수행할 최적화단계중 하나인 클리핑이 좀 더 수월해지는 장점도 있다.

 

출처 : https://carmencincotti.com/2022-05-02/homogeneous-coordinates-clip-space-ndc/#homogeneous-coordinates

 

이미 언급했지만 투영변환은 나중에 w값(변환전의 z값)을 기준으로 x,y를 나눔으로써 완성된다.(아직 몰라도된다)

하지만 나누기 전에 클리핑변환과정을 거쳐야하는 이유를 좀 더 알아보자.

 

일단 우리가 사용하는 모니터는 대부분 직사각형이다. width가 height보다 더 길다. 그러면 정육면체의 큐브를 화면좌표계로 표현 할 시 아래그림과같은 결과가 나오게 된다.

화면의 비율과 맞추려다보니 메쉬가 가로로 더 늘어난것이다. 그렇기때문에 화면비율에 맞춰 클리핑공간으로 이동하는 과정에서 메쉬를 조절하는 연산이 추가적으로 필요하게된다. 이 모든과정이 투영행렬을 곱하는과정에서 이루어지게된다. 

대충 투영행렬이 어떤 역할을 하는지 설명을 했다. 그렇다면 이 행렬식은 어떻게 유도되는지 확인해보자.

먼저 최종 결과식이다.

 

 

대충 최종결과식은 이렇다. 변환이후의 w값을 보면 z값이 위치해있는걸 볼 수 있는데 행렬식 보면 알 수 있듯이 변환전의 z값이다. 나중에 원근나누기를 할 때 이 값을 이용한다.

그리고 x,y를 변환시키는 식의 이름이 각각 xScale, yScale이라고 되어있는 이유는, 위에서 말했던것처럼 정규화뿐만 아니라 모니터비율에 맞춰서 크기를 조정해야하기 때문이다. 사실 뭐 이름이 뭐라붙어있는건 상관없다.

일단 먼저 x,y를 정규화하는 식을 알아보겠다 (위의 xScale, yScale)

 

DirectX는 기본적으로 FOV는 항상 90도이다. 하지만 우리는 임의로 FOV값을 셋팅할수있다. 45도라던가 60도라던가..
예를들어 60도로 셋팅해서 화면을 출력하고싶다고 하면은, 다이렉트카메라는 그대로 90도이고 대신 정점들을 이동시킨다. 마치 월드공간에서 뷰공간으로의 전환하는것처럼말이다.  
60도로 셋팅했다고 치면, 90도에서 보이던것들은 60도에서는 보이지 않을것이다. 왜냐하면 그 차이만큼 정점의 위치를 조정함으로써 마치 실제 60도로 보는것처럼 출력을 하는것이다.

 

아래의 이미지를 통해 쉽게 이해해보자.

 

왼쪽이미지를 먼저 보자면, FOV가 90도일때 A는 원래 시야에 보일것이다. 하지만 60도로 셋팅할 경우, A는 안보여야한다. 그러기위해서 시야각이 줄어든만큼, 정점의 위치를 (이 경우엔 정점의 y값을 더 높였다) 조절함으로써 안보이게 구현하는것이다.

오른쪽의 경우도 마찬가지이다. 90도일때 안보이던 정점은 FOV를 120도로 올렸을 경우 안보이던 정점이 보여야할것이다. 마찬가지로 정점의 Y값을 그만큼 다이렉트의 기본FOV인 90도 안으로 끌고내려온것이다.

즉 그냥 내부적인 카메라시야각은 고정되어있고 우리가 셋팅하는 FOV값에 따라서 모든 정점이 움직일뿐이다.

 

 

 

 

일단 결과적으로 말하자면 우리가 구해서 x,y값에 곱해줘야할 값은 위 이미지의 d이다. d는 카메라로부터 상이 맺히는곳까지의 거리이다. 저기서 'NDC의 Y/2 크기 = 1' 이라는 말은 NDC공간의 Y축 범위는 -1~1로 총 2의 크기를 가지고 있기 때문에 절반을 해서 1이라는 의미이다.

그렇기 때문에 아래와 같은 식이 나오기에  충분하다.

탄젠트는 세타가 커질수록 값이 커진다. 그렇다는 말은 FOV값이 늘어날수록 d값(초점거리)는 줄어들게 된다.

그리고 초점거리가 줄어들수록 기존의 물체들은 점점 작게출력된다.

이게 무슨말이냐면, 다이렉트는 내부적으로 FOV값을 90으로 고정시키고 처리한다고 언급했었다.

그래서 우리가 FOV값을 임의의 값으로 셋팅해도 결국 그 값만큼 정점의 X,Y좌표를 90도안으로 끌고오거나 밀어내는것이다. FOV값이 커질수록 안보이던 정점을 보이게 출력해야하니 90도 안으로 끌고 들어오게 되는데, 끌고 들어올수록 화면 중앙에 가까워진다. 좀더 쉬운 이해를 위해서 아래의 이미지를 봐보자.

 

위 비행기의 각 4개의 날개의 끝을 정점이라 생각해보자. FOV값이 높아질수록 각 정점이 점점 화면 중앙으로 몰린다.

시야각을 더 늘리면 더 작게 보일것이다. 줄이면 반대일것이다.

 

그렇다면 일단 지금까지의 프로젝션행렬의 요소는 아래와 같아진다.

 

 

x,y쪽 정규화는 위의 행렬을 곱해줌으로써 변환된다. 그러나 한가지 간과한게있다.

맨처음 말했다시피 모니터의 가로세로 비율은 1:1이 아니기 때문에 그만큼 보정을 해줘야 한다.

보통 종횡비(Aspect Ratio)라고 하며 다이렉트에서 프로젝션행렬을 리턴받는 함수의 매개변수로 넘겨줘야하기도 한다.

모니터는 보통 가로로 더 길기 때문에 x쪽부분에 값을 나눠준다. 그러면 최종적으로 아래와 같아진다.

 

 

자 이제 z값 정규화만 남았다. 모니터는 2차원이라 x,y값만 변환하면 될 것 같지만, 깊이값이 있어야 픽셀출력순위를 결정할 수 있기때문에 필요하다. 두 물체가 겹쳐있다면 어떤걸 출력해야할지 알아야 하기 때문이다.

여기서 z값 정규화의 의미는, 현재 z값은 카메라기준에서 특정정점까지의 z값이다.

근데 이것을 near far 기준으로 수치를 변환시키는 것이다.

 

먼저 아래의 이미지를 보자.

 

여기서 제일 우측값들인 wX, hY, A*Z + B, Z  라는 값들은 클립스페이스의 좌표이다.

우리는 A와 B가 어떤 식인지 알아내야 한다.

 

우리가 알고있는 정보는, NDC좌표에서의 z범위는 0~1이라는 것이다.

그렇다면 뷰공간의 z값이 Zn(근평면)이라면 aZn + B / Zn = 0이고

뷰공간의 z값이 Zf(원평면)이라면 aZf + B / Zf = 1이라는 것이다..!

 

왜? 우리는 NDC의 z범위가 0~1, 즉 클립스페이스에서의 z값을 w로 나눴을 시 값의 범위가 0~1이라는 것이다.

만약 뷰공간에서의 z값이 딱 근평면에 위치한 Zn이라면, 클립스페이스에서의 z좌표는 a*Zn + B / Zn이 된다.

(Zn이 w라는걸 까먹지 말자.)

아래는 관련되서 설명한 내용이다.

a + b / Zn(w) = 0

a + b / Zf(w) = 1  

이 두 식으로 연립방정식을 통해 a와 b를 구하는 내용이다.

 

 

위 이미지는 http://egloos.zum.com/EireneHue/v/985792

 

Direct3D : 카메라 (Camera) 2 - 투영행렬 (Projection Matrix)

뷰 행렬(View Matrix) 에 이어서 이번에는 투영행렬(Projection Matrix) 에 대해서 살펴보도록 하겠습니다. 뷰행렬이 랜더링 파이프라인 단계의 3번째 단계인 뷰 스페이스를 구성하는데 사용되는 설정을

egloos.zum.com

에서 가져온건데 여기 원본에서는 이미지의 A, B값을 잘못 적어놓으셨다. 아마 위링크타서 내리다보면 같은 이미지나올텐데 A,B값이 다르게되어있다. 그래서 순간 내가 방정식을 잘못푼줄알고 여러번 다시풀어보기도하고 다시 막 알아봤는데, 원본글에 보면 실제 코드도 적어놓으셨는데 거기에는 내가 계산한것과 같은식으로 코드가 짜여져있다;; 아마 저 이미지에서만 실수로 잘못 적어놓으신것같다.

어쨌든 이렇게 A,B를 구하고 위에 이미 올려놨던 최종 투영행렬식의 A,B값과 비교하면 일치하는걸 알 수 있다.