본문 바로가기

프로그래밍 -----------------------/C,C++ 팁

행렬(Matrix)


이번시간은 DirectX 3D 에 필요한 기초 수학 마지막 시간입니다.

오늘 배운 이외에의 것들은 모두 강좌 중 그때 그때 필요할때마다 언급하는 방식으로 하겠습니다.


여지껏 배운, 삼각함수나, 벡터와 같은 것들은 게임 프로그래밍에 있어서 중요한 개념임에는 틀림 없습니다.

그러나, 삼각 함수나, 벡터의 경우, 굳이 삼각함수와 벡터를 거치지 않고도 다른 알고리즘들이 많이 존재합니다. (물론, 삼각함수나 벡터로 처리하는 것보다 훨씬더 복잡해지는 알고리즘도 있습니다;;)

그러나 오늘 이 시간에 배우게 되는 행렬 개념은 3D 프로그래밍에 있어서 반 강제적 필수 요소라고 할수 있습니다.

즉, 행렬을 알지 못하면 3D 프로그래밍은 힘들다. 라고 말해도... 90%는 맞는 말입니다.


실제로 대부분의 D3DX API들은 화면상에 무언가를 렌더링 하고자 할때 거의 99.99% 행렬을 사용합니다.

그러나 앞시간에도 말했듯이, 우리는 Physicist아니라, Programmer 이므로, 완벽히 행렬을 마스터 할 필요는 없다고 봅니다.

언제든 필요할때 책이나, 제 강좌를 레퍼런스 하면 되겠지요.

그러나, 언제 행렬이 사용되어야 하는지를 감각적으로 느끼기 위해서는 행렬의 기본개념 정도는 확실히 꿰뚫고 있어야 한다고 생각 되네요.


대부분의 사람들은 중학교때 쯔음 행렬을 처음 접하는 걸로 알고 있습니다.(학교를 졸업하니 생각이 가물가물 하군요...)

그리고 고등학교때 행렬을 본격적으로 배우기 시작하고, 대학교에서 대수학 시간에 행렬을 마스터하게 되지요.

행렬을 잘 사용한다면,복잡한 방정식도 쉽게 풀 수 있습니다.


한 예로...

 X = 3a + 4b + 5c

 Y = a + 5b - 2c

 Z = 7a - 2b + 3c

와 같은 방정식을 풀려고 할때...

     [X Y Z] = ┌..3..4.-5.┐┌ a ┐
     [X Y Z] = ..1..5.-2.││ b │
     [X Y Z] = └..7.-2..3.┘└ c ┘

와 같이 표시를 해줄수 있죠.

행렬을 위처럼 사용해서 방정식을 푸는 것은, 수학에서도 많이 쓰이겠지만, 게임에서도 알고리즘 등에서 자주 사용되므로,

나중에 다른 알고리즘을 분석할 때에 헷갈리지 않게 잘 알아두시는 것이 좋을 것입니다.


3D 프로그래밍에서는 특히나, 프레임 수(Frame rates)를 높이려고 프로그래머들이 발악을 하곤 합니다.

심한 경우에는, 프로그래밍 소스 코드에서 밤을 새고 필요 없는 부분이나 최적화 할 부분을 찾아서,

소스 코드를 5줄 줄여서 프레임 레이트가 조금이라도 상승 하면, 그날은 회식을 하는 회사도 있다고 합니다.

하하하;;; 저도 들은 얘기라서 실화인지는 모르겠지만요. 그만큼이나, 3D 프로그래밍에서는 속도가 생명이라는 소리겠죠.


2D에서는 최적화가 솔직히 그렇게 까지 몸에 와닿을 정도로 느껴지지는 않았을 겁니다.

즉, 2D에서는 아무렇게나 짜도 결과물은 비슷했죠.
(물론 내부적인 실행 속도는 코딩 방법에 따라 약간씩 차이가 났겠지만...)

그러나, 3D에서는 정말 코드 몇줄을 줄임으로 인해서 코드의 질이 비약적으로 상승했다가 하락하기를 반복합니다.

잠깐 5분동안 어떤 코드를 삽입했는데, 갑자기 프레임 레이트가 반으로 떨어졌다!

그러면... 또 회의가 열리고, 어떻게 다시 프레임 레이트를 올릴 것인지 고민에 또 고민을 하게 되죠.

푸풉! 정말 웃기죠?

2D에선 그런일이 별로 없었지만, 3D에서는 소스 코드 한줄에 울고, 또 웃습니다.


그런데 이렇게 속도를 중요시 하는 프로그래밍에서, 일일히 for문을 돌려가면서, 계산을 하고 있다... 고 하면...

그야말로 생각이 없는 사람이겠지요.

그래서 존재하는 것이 바로 행렬(Matrix)입니다.

행렬은 여러가지 복잡한 연산이 있을 때 여러개의 순차연산을 하나의 행렬로 만들어서 계산할 수 있습니다.

즉, 행렬의 성질에 의해서 순차적인 연산을 행렬의 곱으로 바꾸어서 하나의 행렬로 만들어 줌으로써, 연산의 양을 현저히 줄일 수 있게 되는 것이지요.


   
연산의 양 = 속도

...라는 것을 생각해 본다면, 행렬을 안쓰고는 못배기겠죠?


벡터와 마찬가지로 D3DX에서는 행렬 연산을 위해 D3DXMATRIX 라는 구조체를 하나 제공하고 있습니다.

이 구조체는 4*4 사이즈의 행렬을 표현하며, 행렬들을 좀 더 쉽게 다룰수 있도록 도와줍니다.


자, 이제 행렬의 필요성은 어느정도 인식을 하셨으리라 봅니다.

그럼 실제로 DirectX에서 사용되는 행렬에 대해 하나하나 살펴보도록 하지요.


단위 행렬.

단위행렬(Unit matrix)은 가장 간단한 이동 행렬의 하나입니다.

단위 행렬은 행렬 연산에 아무런 영향도 미치지 않습니다.

즉, 자연수에서 "1"의 역할과 마찬가지의 역할을 하는 녀석입니다.

이건 벡터에서의 단위 벡터와도 마찬가지 개념이겠지요.


단위 행렬은 보통 I 로 표기 하며, 행렬 M과 단위행렬(I)의 연산 결과는 항상 M이 된다는 것이지요.

즉, MI = M 과 같으며,


단위 행렬은 다음과 같은 형태를 띄게 됩니다.


흠... 단위행렬은 즉, 게임에서 행렬을 초기화 하거나, 삭제 할때, 등에도 자주 사용 되므로,

단위 행렬의 구조를 눈에 익혀 놓으시길 바랍니다.


D3DX에서 제공하는 단위 행렬 생성 함수는 다음과 같습니다.

    // 단위 행렬 생성.
    D3DXMatrixIdentity(D3DXMATRIX* pOut);

함수를 호출하면, pOut으로 넘겨준 행렬 구조체에 단위 행렬 정보가 저장되게 됩니다.


이동 행렬.

이동 행렬(Translation matrix)은 그냥 단순한 덧셈 연산이며, 아래와 같은 방정식과 행렬 형식을 취합니다.

    X' = X + a
    Y' = Y + b
    Z' = Z + c


모든 이동 행렬은 위와 같은 형식을 취하고 있으므로, 우선은 위의 형태만 조금 눈여겨 보셔 두시면 됩니다.


D3DX에서 제공하는 이동행렬을 생성하는 함수는 다음과 같습니다.

    // 이동 행렬의 생성.
    D3DXMATRIX* D3DXMatrixTranslation(D3DXMATRIX* pOut, FLOAT x, FLOAT y, FLOAT z);

첫번째 인자, pOut은 벡터에서와 마찬가지로, 생성된 이동 행렬이 저장되는 변수이고,

나머지 x, y, z는 각각의 축에서의 이동 값을 나타냅니다.


크기 변환 행렬.

크기 변환 행렬은 몇가지 인자들을 데이터에 곱함으로써 데이터의 크기를 조정합니다.

    X' = X * a
    Y' = Y * b
    Z' = Z * c

그런데, 이동 행렬과 크기 변환 행렬에서, X, Y, Z 끼리의 연산임에도 불구하고,

계속해서, 매트릭스의 4번째 행과 열에, 1이라는 녀석이 끼어 있는 것을 볼 수 있습니다.

간단히 곱셈만을 할때에는 4번째 행과 열이 없어도 당연히 계산 결과는 동일 합니다.


그러나, 이 변환 행렬이 이동 행렬과 양립할 수 있도록(즉, 행과 열이 동일 하도록) 4번째 행을 유지 시켜주어야 합니다.

이것이 바로 호모지니어스 좌표라는 것입니다.


D3DX에서 제공하는 크기 변환(Scaling) 함수는다음과 같습니다.

    // 크기 변환 행렬 생성.
    D3DXMATRIX* D3DXMatrixScaling(D3DXMATRIX* pOut, FLOAT sx, FLOAT sy, FLOAT sz);

첫번째 인자, pOut은 마찬가지로, 생성된 이동 행렬이 저장되는 변수이고,

나머지 sx, sy, sz는, 축소/확대 비율을 나타내며, 1보다 크면 확대, 0일땐 변화 없고, 음수이면, 해당 축 방향으로 Mirroring 됩니다.


회전 행렬.

회전 행렬은 사실, 자세히 들어가자면, 몇십 페이지는 나올 정도로 다양한 응용이 가능한 행렬입니다.

그러나, 그것은 앞으로 강좌 중간 중간에 하나하나 알아보도록 할 것입니다.


3D 객체의 공간 상에서의 움직임은 크게, 직선 운동과 회전 운동으로 나누어 집니다.

직선 운동은 벡터의 합을 이용해서 구하면 된다고 지난 시간에 말씀 드렸습니다.

그러나 회전 운동은 꽤나 까다로운 분야임에 틀림없습니다.


정석을 펼쳐 보면, 2차원에서의 회전을 수식으로 나타낸 부분이 있죠

sin과 cos을 이용해서 회전을 시키는 방법을 설명하고 있는데, 3D 에서도 그것과 일맥 상통합니다.

다만, 수식이 조금더 복잡해 졌을 뿐이지요.


3D는 축이 세 개로 나뉘어 있으므로, 회전은 x축을 중심으로 회전하는 x축 회전, y축을 중심으로 회전하는 y축 회전, z축을 중심으로 회전하는 z축 회전이 있습니다.



여기서 θ(Theta)는 회전 각도를 라디안 단위(PI = 180도)로 바꾸어 주시면 됩니다.

위에서 각도는 회전축을 중심으로 하여 원점 방향을 향한 반시계 회전으로 측정한 것입니다.


대부분의 서적에는 시계 방향으로 회전하는 공식이 나와 있을텐데요.

회전 방향을 바꾸어 주고 싶다면, sin에 붙어 있는 부호를 바꾸어 주시면 됩니다.

즉, 위의 식에서 sinθ는 -sinθ로, -sinθ는 sinθ로 바꾸어 주시면 된다는 소리입니다.


또, 객체의 높이 방향을 y축 방향이라고 정의한다면, x축 회전은 Pitch, y축 회전은 Yaw, z축 회전은 Roll로 정의합니다.


D3DX에서 제공하는 회전 관련 함수들은 다음과 같습니다.

    // x 축을 기준으로 Angle 만큼 회전하는 행렬을 구한다. (Angle은 라디안)
    D3dXMATRIX* D3DXMatrixRotationX(D3DXMATRIX* pOut, FLOAT Angle);

    // y 축을 기준으로 Angle 만큼 회전하는 행렬을 구한다. (Angle은 라디안)
    D3dXMATRIX* D3DXMatrixRotationY(D3DXMATRIX* pOut, FLOAT Angle);    

    // z 축을 기준으로 Angle 만큼 회전하는 행렬을 구한다. (Angle은 라디안)
    D3dXMATRIX* D3DXMatrixRotationZ(D3DXMATRIX* pOut, FLOAT Angle);


    // Yaw, Pitch, Roll 값으로 회전하는 행렬을 구한다.
    D3DXMATRIX* D3DXMatrixRotationYawPitchRoll(D3DXMATRIX* pOut, FLOAT Yaw, FLOAT Pitch, FLOAT Roll);

    // 임의의 축을 중심으로 회전하는 하는 행렬을 구한다
    D3DXMATRIX* D3DXMatrixRotationAxis(D3DXMATRIX* pOut, CONST D3DXVECTOR3* pV, FLOAT Angle);

    // 사원수로부터 회전하는 행렬을 구한다
    D3DXMATRIX* D3DXMatrixRotationQuaternion(D3DXMATRIX* pOut, CONST D3DXQUATERNION* pQ);



이걸로, 게임 프로그래밍에 사용되는 행렬들은 다 살펴 보았습니다.

그러나 게임에서는 하나의 행렬 만 가지고 모든 연산이 끝나지는 않습니다.

대부분, 여러개의 행렬 끼리 치고 박고 연산을 하는 복잡한 공식을 사용하죠.

여러 행렬들의 효과를 결집 시키기 위해서는 행렬들을 함께 연결 시켜야 합니다.


즉, 이동행렬을 이용하여, 객체를 이동시키고, 회전 행렬을 이용하여, 회전을 시킨후, 변환 행렬을 사용하여 크기를 변환하고 싶을때,

이동 행렬(T), 크기 변환 행렬(S), 회전 행렬(R) 이 세 개를 곱해 주시면 됩니다.

그러나 이와 같이 행렬 끼리 연산 할때에는 반드시 Scale 행렬을 먼저 연산 해 주어야 합니다.

왜냐하면, 크기 변환 전의 이동과 크기 변환 후의 이동은 당연히 다를테니까요.

즉, M = S * T * R 과 같이 해주시면, 3개의 효과를 하나의 행렬로 처리해 줄 수가 있게 되겠지요.

이게 바로 행렬을 사용하는 이유 중 가장 큰 이유가 됩니다.


그러나 행렬의 성질상 행렬은 교환법칙이 성립하지 않으므로,

원하는 결과값을 제대로 계산하기 위해서는 반드시 앞에서부터 차례로 계산해 주어야 합니다.

가끔 가다가 분명히 맞게 코딩을 해주었는데도 결과값이 이상하게 나올때가 있는데,

그럴 때에는 행렬의 연산 순서를 먼저 체크 해 보시길 바랍니다.


D3DX에 포함된 행렬 연산 함수들은 다음과 같습니다.

   
// 두 행렬의 곱한 결과 행렬 생성.
    D3DXMATRIX* D3DXMatrixMultiply(D3DXMATRIX* pOut,
                                                     CONST D3DXMATRIX* pM1, CONST D3DXMATRIX* pM2);

    // 행렬의 역행렬을 구한다
    D3DXMatrixInverse(D3DXMATRIX* pOut, FLOAT* pDeterminant, CONST D3DXMATRIX* pM);

   
// 벡터와 행렬의 곱을 구한다. (벡터와 행렬을 곱하면 결과값이 벡터)
    D3DXVECTOR3* WINAPI D3DXVec3TransformCoord(D3DXVECTOR3* pOut,
                                                                             CONST D3DXVECTOR3* pV,
                                                                             CONST D3DXMATRIX* PM);


휴... 이걸로 어느정도 행렬과 벡터에 대해서는 다 알아본 것 같습니다.

벡터는 솔직히 쉽게 이해하셨으리라고 생각 됩니다만...

행렬에 대해서는 수학과 별로 친하지 않으신 분들이라면, 정말 어안이 벙벙하고...

나는 3D 프로그래밍을 포기해야 하는 것일까... 라는 생각 까지 하고 계실지도 모르겠습니다.

아니, 어쩌면 행렬은 자신있다 라고 생각하셨던 분들도 바짝 긴장을 하고 계실지도 모르겠네요.


그러나, 게임 프로그래밍에서의 수학은 수학이 아니라, 감각입니다.

즉, 아, 이럴때는 이동 행렬을 사용해야겠다, 하고 이동행렬을 사용하는 법을 찾아 보시면 됩니다.

물론, 이러한 경험이 점점 깊어지게 되면, 찾아보지 않아도 능숙하게 처리하는 날이 오겠지요^^;
(의외로 이렇게 되는데 까지는 별로 오래 걸리지 않을 겁니다...)


'프로그래밍 ----------------------- > C,C++ 팁' 카테고리의 다른 글

DirectX 펑션들  (0) 2008.10.13
billboard에서 D3DXMatrixInverse()  (0) 2008.10.13
3D 세계를 구성하는 필수 요소들 | [펌]DirectX  (0) 2008.10.13
DirectX 용어집  (0) 2008.10.13
4*4행렬 회전  (0) 2008.10.13