이 장에서는 OpenGL에서 공간상에 물체를 원하는 위치에 위치시키고 원하는 방향으로 움직이거나 회전하는 방법에 대해서 알아본다. 최종적 실습으로 두개의 구를 이용해서 첫번째 구는 화면 중앙에서 제자리에서 회전을 하며 두번째 구는 첫번째 구를 중심으로 일정한 거리를 유지하면서 동시에 자신의 중심점을 기준으로해서 회전하는 것을 구현해 본다. 좀더 다르게 표현한다면 첫번째 구는 자전만을 하며 두번째 구 역시 자전을 하면서 동시에 첫번째 구 주위를 공전하는 예를 말한다. OpenGL에서 이러한 일련은 동작은 간단한 수학적인 연산을 통해서 이루어진다고 강조하고 싶다. 물론 모든 컴퓨터의 동작의 모두가 수학적이기는 하지만 필자가 이 수학적이라는 것에 대해 강조하는 이유와 간단한 수학 연산이라고 말하는 이유는 다름 아닌 4X4 행렬, 이 하나만으로 모든 것이 이루어 진다는 것이다. Matrix(매트릭스)라는 영화를 보았는가? 매트릭스란 우리나라 말로 행렬이란 뜻이 있다. 즉, 공간상의 모든 원자, 요소들은 이 행렬을 통해서 통제되어질 수 있다. 고작 16개의 숫자들에 의해서 말이다. Matrix란 영화에서 인간들은 컴퓨터의 통제하에 움직이게 되는데 그 컴퓨터와 인간의 통제라는 내용과 Matrix란 영화의 제목이 그 어떤 강한 연관이 있다 느껴지지 않는가?
자, 이제 사설은 여기서 마무리 하기로 하고 잠시 간단한 수학 이야기를 해야겠다. 복잡한 내용은 빼고 꼭 필요한 쉬운 수학 이야기만을 할것이니 걱정할 필요는 없다. 우리는 이미 고등학교 시절에 행렬이라는 수학적 도구를 배웠다. 4X4행렬, 크지도 그렇다고 작지도 않은 16개의 값을 가진 벡터 공간일 뿐이다. 4X4행렬은 다음과 같이 나타낼수있다는 것을 우리는 이미 모두 알고 있을 것이다. (참고로 이장을 소화해 내기 위한 독자의 수준은 최소한 행렬의 곱을 할수있어야 한다)즉, 총 16개의 값을 가진 벡터 공간이다. 여기서 벡터 공간이라고 하는 것에 대해서 너무 신경 쓰지 말길 바란다. 그저 많은 벡터 공간중에 행렬도 포함된다는 정도만 알자. 자 이제 실제 행렬과 공간상의 좌표의 연산에 대해서 살펴보자.
위 연산은 공간상의 임이의 점의 좌표 (x,y,z)가 크기가 4X4인 단위 행렬을 통해 아무런 위치의 변화없이 원래의 (x,y,z)의 위치값이 나오는 예이다. 주의해서 보면 위의 행렬 연산에서 좌측의 4X4 행렬이 단위 행렬임을 알수있다. 어떠한 행렬이건 단위행렬과 곱하면 본래의 행렬이 나온다는 것을 알고있는가? 그렇다면 이제 위의 행렬 연산에서 주어진 좌측의 4X4 행렬이 단위행렬이 아닌 다른 행렬일때 주어진 좌표는 어떤 새로운 좌표값으로 바뀔것이라는 것을 알수있다. 다행이 우리는 많은 선행대수학 교제에서나 컴퓨터 3차원 그래픽 서적에서나 좋은 OpenGL 서적에서 유용한 4X4행렬을 볼수있다. 첫째는 좌표를 원하는 위치로 이동할수있는 이동행렬, 둘째는 좌표를 원점(0,0,0)을 기준으로 회전시키는 회전행렬, 그리고 이밖에도 크기 변환 행렬(Scale Matrix), 밀림 행렬, 물체의 그림자를 얻어낼수 있는 그림자 변환 행렬 등등, 독자의 수학적 지식이 뛰어나면 뛰어날 수록 엄청나고 무궁 무진한 응용을 할 수 있겠다. 그렇다면 OpenGL에서 가장 많이 사용하고 있는 이동행렬, 회전행렬, 크기변환행렬 이렇게 세가지에 대해서 결과만을 알아보도록 하자.
먼저 이동 행렬은 다음과 같다.
행렬의 원소중 X, Y, Z값은 각각 X축, Y축, Z축으로 그 값만큼 이동하고자 하는 값이다. 예를 들어서 공간상의 위치(3, 2, 4)를 X축으로 -1만큼, Y축으로 2만큼, Z축으로 1만큼 이동했을 경우에 대해서 알아보면 다음과 같을 것이다.결과로써 (2,4,2)가 나왔는데 실제로 (3,2,1)을 위에서 언급한 이동을 하게 되면 (2,4,2)가 된다. 다음에 언급되는 모든 행렬의 사용법은 이와 동일하다.
회전 행렬에 대해서 알아보자. 어떤 물체를 회전하기 위해서는 어떤 축을 기준으로해서 몇도만큼 회전할 것인지를 지정해야 한다. 축은 X축, Y축, Z축이 있으므로 우리는 이 세개의 축에 대해 각각의 회전 행렬에 대해서 알아보겠다.
X축을 기준으로 각 a만큼 회전시키는 회전 행렬은 다음과 같다.다음은 Y축을 기준으로 각 a만큼 회전시키는 회전 행렬이다.
다음은 Z축을 기준으로 각 a만큼 회전시키는 회전행렬이다.
다음으로 알아 볼것은 은 크기 변환 행렬이다. 점은 크기를 갖지 않지만 점들이 모여서 물체를 이뤄 크기를 갖게 될때 유용한 변환 행렬이다. 원점을 기준으로해서 변환된다. 아래의 행렬이 바로 크기 변환 행렬이다.
X값은 X축을 기준으로해서 X배의 크기로 변환되고 Y값은 Y축을 기준으로 해서 Y배의 크기로 변환되며 Z값은 Z축을 기준으로해서 Z배의 크리고 변환되게 된다.
자! 이렇게 해서 각각의 변환 행렬들에 대해서 살펴보았다. 그렇다면 어떤 한점을 어느 지점으로 이동시킨후에 다시 어떤 축을 중심으로 회전하고 그리고 크기를 변경하고자 할때가 있다고하면 먼저 이동행렬로 계산을 해서 그 결과값을 다시 회전행렬로 계산하고 또 새롭게 나온 결과값을 다시 크기변환 행렬로 계산을 하면되는데 이것을 각각의 변환행렬을 모두 곱한후에 변환하고자 하는 위치와 각각의 변환 행렬을 곱한 행렬에 곱해줘도 같은 결과를 얻는다. 무슨말인고하면 다음의 예를 들어보도록 하겠다. (3,2,1)의 점을 X축으로 -1만큼 이동후 Z축으로 90도 회전시켜보자. 먼저 (1,2,3)을 X축으로 3만큼 이동되어 변환되는 좌표는 다음과 같다.
이동되어 (2,2,1)이라는 점으로 변환되었다. 다시 이점을 Z축으로 90도 회전시켜 변환되는 좌표는 다음과 같이 얻어질수있다.즉, 최종적으로 (-2, 2, 1)의 좌표가 얻어졌다. 원래의 좌표를 먼저 이동행렬 연산후에 다시 회전행렬 연산을 시켜 원하는 좌표를 얻은 것이다. 이것을 다음과 같이 계산할 수 있도 있음을 주의해서 보기바란다.
즉, 이동행렬과 회전행렬을 먼저 계산해서 얻어진 행렬에 변환하고자하는 좌표를 연산하여 최종적인 변환 좌표를 얻는 것이다. OpenGL은 이 방법을 사용한다. 이 예에서 우리는 이동행렬 연산후에 회전 행렬 연산을 수행했음을 눈여겨 봐야 한다. 그렇다면 순서를 바꿀경우 어떻게 될것인가? 즉 회전 행렬 연산후 이동 행렬 연산을 수행할 경우를 말이다. 직접 해보면 알겠지만 다른 결과 값이 나온다는 것을 알수있다.
자, 여기까지 모두 이해가 되었는가? 이해가 되었다면 왜 필자가 OpenGL에서 물체를 구성하기 위해서 이런 수학 이야기를 썼는지를 앞으로의 내용을 통해서 알게될것이다. 이제 OpenGL에서 앞에서 살펴본 행렬연산와 물체의 구성에 대해서 살펴보도록하자. 지금까지 내용이 이해가 되지 않는 독자는 뒤에 이어지는 내용을 계속해 읽지 말고 앞의 내용이 이해가 될때까지 반복적으로 읽어나가길 당부한다.
OpenGL은 가장 처음에 물체의 좌표변환 행렬로써 단위행렬을 갖는다. 즉, 점들에 대해서 아무런 변환이 없는 것이다. OpenGL이 변환 행렬로써 단위행렬을 갖고 있을때를 전역좌표계라하도록 하자. 나중에 여러가지 변환 행렬을 통해서 이 단위 행렬이 다른 값을 갖는 행렬로 변경되는데 이때 다시 단위행렬로 되돌리기 위해서 우리는 glLoadIdentity라는 함수를 호출함으로써 간단이 단위행렬을 변환행렬로 대체할수있다.
우리는 이미 공간상의 어떤 하나의 점를 이동하는 OpenGL API 함수를 알고있다. 바로 glTranslatef이다. 모두가 다 잘알고 있듯 이 함수는 세개의 인자를 취하는데 각각 X축, Y축, Z축으로 이동할 만큼의 값을 갖는다. 즉 glTranslatef함수를 호출하게되면 전에 OpenGL이 가지고 있는 변환행렬에 이동행렬을 곱하게 되는 것이다. 이렇게 해서 나중에 공간상의 점들이 이 변환행렬을 통해서 원하는 위치로의 이동 변환이 이루어지는 것이다.
또 우리는 이미 공간상의 어떤 하나의 점을 회전하는 OpenGL API함수를 알고 있다. 바로 glRotatef이다. 역시 모두가 잘 알고 있듯이 이 함수는 네개의 인자를 취하는데 회전할 각, 그리고 나머지 세개는 하나의 좌표로 인식되어서 원점으로부터 세개의 인자로 결정되는 좌표까지 이은 축을 중심으로 회전하게 되는것이다. 일반적으로 회전연산은 X축, Y축, Z축으로 나눠서 회전하는 경우가 많다. 즉 X축으로 45도만큼 회전하고 Y축으로 30만큼 회전하고자 할경우라면 다음과 같은 2개의 함수를 연이어 호출하면 된다.
glRotatef(45.0f, 1.0f, 0.0f, 0.0f); glRotatef(30.0f, 0.0f, 1.0f, 0.0f);
눈치를 챈 독자가 있겠지만 역시 glRotatef 역시 기존의 가지고 있던 변환행렬에 회전 변환 행렬을 곱하게 되는데 이렇게 해서 나중에 공간상의 점들이 이 변환행렬을 통해서 원하는 위치로의 변환이 이루어지는 것이다.
또 우리는 이미 공간상에 어떤 물체의 크기를 변환하는 함수를 알고 있다. glScalef인데 모두 3개의 인자를 가지고 있다. 첫번째는 X축으로 몇배만큼 크기를 키울것인지, 두번째는 Y축으로 몇배만큼 크기를 키울것인지, 세번째는 Z축으로 몇배만큼 크기를 키울것인지를 지정하게 된다. 역시 이 함수도 기존의 변환행렬에 크기변환 행렬을 곱하게된다.
이렇게 glTranslatef, glRotatef, glScalef 함수들에 의해서 OpenGL의 좌표축을 새롭게 정의할수있는데 이렇게 정의된 좌표축을 지역좌표축이라고 한다.
OpenGL은 변환행렬을 위해서 중요한 여러가지 함수를 제공하는데 그중에서 glPushMatrix와 glPopMatrix 함수를 제공한다. glPushMatrix는 현제 가지고 있는 변환행렬의 값들을 스택(Stack)에 임시로 저장해 두는 함수이고 glPopMatrix는 다시 임시로 저장했던 행렬값들을 꺼내는 기능을 한다. 스택은 자료구조상 많은 변환행렬 값들을 저장할수있고 가장 최근에 저장했던 값이 가장 처음으로 꺼내지게 된다. 이 두개의 함수는 여러개의 물체를 공간상에 구성하는데 아주 중요한 함수이다. 왜냐하면 물체 하나당 우리는 변환행렬을 적용할 것이다. 즉, 물체 하나당 지역 좌표축을 하나씩 두는 것이다. 이 것에 대한 것은 실제 예를 통해서 알아보도록 한다.
여기서 우리는 변환 행렬과 좌표축이 사로 똑 같은 의미라는 것을 알수있다. 즉, 변환행렬을 이용해서 좌표축 전체를 이동시키거나 회전 또는 크기를 변환 시키는 경우로 해석할 수 있다는 의미이다. 이동 변환 행렬을 수행하면 좌표축은 이동 변환 행렬에 의해서 이동하게 되고 회전 변환 행렬을 수행하면 좌표축은 그 회전 변환 행렬에 의해서 회전하게 된다. 또한 크기 변환 행렬 연산을 수행하면 좌표축의 각각의 축들은 늘어나거나 줄어들게 되는 것이다. 앞에서도 언급했지만 행렬 연산은 그 순서가 중요하다. 이동 행렬 연산후 회전 행렬 연산의 결과와 회전 행렬 연산후 이동 행렬의 연산은 다른 값을 얻는다. 좌표축으로 생각봐도 쉽게 알수있다. 아래의 그림이 바로 그것인데 단지 회전과 이동 연산의 순서만을 바꾼 경우이다.자, 이제 최종적으로 실제 예제를 통해서 이장을 마무리 지어 볼까 한다. 1장의 소스 코드에서 완전이 새롭게 시작하도록 하자.
예로써 우리가 이 장의 가장 처음에 언급했던 두개의 구를 이용해서 첫번째 구는 화면 중앙에서 제자리에서 회전을 하며 두번째 구는 첫번째 구를 중심으로 일정한 거리를 유지하면서 동시에 자신의 중심점을 기준으로해서 회전하는 것을 구현해 본다. 첫번째 구를 지구라하고 두번째 구를 달이라 편의상 구분하자.
먼저 전역변수 지역에 다섯개의 변수를 추가한다.
GLUquadricObj *obj; // 구를 그리기 위한 객체 포인터 GLfloat o1_rot = 0.0f; // 지구의 자전각 GLfloat o2_rot1 = 0.0f; // 달의 공전각 GLfloat o2_rot2 = 0.0f; // 달의 자전각 GLfloat distance = 8.0f; // 달과 지구의 각 중심간의 거리
InitGL 함수에서 몇가지 초기화 시켜줘야할 추가 코드가 있는데 다음과 같다.
int InitGL(GLvoid) { glShadeModel(GL_SMOOTH); glClearColor(0.0f, 0.0f, 0.0f, 0.5f); glClearDepth(1.0f); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LEQUAL); glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); obj = gluNewQuadric(); gluQuadricNormals(obj, GLU_SMOOTH); gluQuadricOrientation(obj, GLU_OUTSIDE); gluQuadricTexture(obj, GL_FALSE); glEnable(GL_LIGHTING); glEnable(GL_LIGHT0); glEnable(GL_COLOR_MATERIAL); return TRUE; }
이미 이전 장에서 모두 설명되었던 코드들이므로 자세한 설명은 피하기로 하고 대략적으로 설명하자면 다음과 같다. 먼저 Quadric 객체 포인터 변수를 이용해서 인스턴스 변수를 생성하여 Quadric의 속성을 설정하고 광원과 색상을 추적해서 재질로 만드는 기능을 켠다.
자 이제 실제로 중요한 DrawGLScene 함수를 살펴보기로 하자.
int DrawGLScene(GLvoid) { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); //<1> glTranslatef(0.0f, 0.0f, -22.0f); //<2> glPushMatrix(); //<3> glRotatef(o1_rot, 0.0f, 1.0f, 0.0f); //<4> gluQuadricDrawStyle(obj, GLU_FILL); //<5> glColor3f(0.9f, 0.9f, 0.9f); //<5> gluSphere(obj, 2.0f, 24, 24); //<5> gluQuadricDrawStyle(obj, GLU_LINE); //<5> glColor3f(0.6f, 0.6f, 1.0f); //<5> gluSphere(obj, 2.1f, 24, 24); //<5> glPopMatrix(); //<6> glPushMatrix(); //<7> glRotatef(o2_rot1, 0.0f, 1.0f, 0.0f); //<8> glTranslatef(distance, 0.0f, 0.0f); //<9> glRotatef(o2_rot2, 0.0f, 1.0f, 0.0f); //<10> gluQuadricDrawStyle(obj, GLU_FILL); //<11> glColor3f(0.9f, 0.9f, 0.9f); //<11> gluSphere(obj, 0.7f, 12, 12); //<11> gluQuadricDrawStyle(obj, GLU_LINE); //<11> glColor3f(1.0f, 0.6f, 0.6f); //<11> gluSphere(obj, 0.75f, 12, 12); //<11> glPopMatrix(); //<12> o1_rot+=0.5f; if(o1_rot>359.5f) o1_rot=0.0f; //<13> o2_rot1+=0.05f; if(o2_rot1>359.95f) o2_rot1=0.0; //<13> o2_rot2+=2.0f; if(o2_rot2>358.0f) o2_rot2=0.0; //<13> return TRUE; }
자, 하나 하나 설명해 보도록 하자.
<1>번 코드는 변환 행렬을 단위 행렬로 초기화 시켜주는 예이다.
<2>번 코드는 이동 변환 행렬을 이용해서 좌표축을 Z축으로 -20만큼 이동한 것이다. 이렇게 이동한 이유는 우리가 OpenGL에서 물체를 바로보는 눈의 위치가 (0,0,0)에 있기 때문이다. 만약 이러한 이동이 없이 물체를 그려준다면 물체의 위치와 눈의 위치가 같게 되어서 물체가 보이지 않으므로 물체를 뒤쪽으로 이동시켜야 그려줌으로써 물체가 잘 보이도록 해주는 것이다.
<3>번 코드에서 <6>번 코드까지 지구를 그려주는 것이다. 지구는 제자리에서 자전만 한다.
<3>번 코드는 지금까지 변환행렬에 의해서 변환된 좌표축을 저장해 놓는 것이다.
<4>번 코드는 y축을 기준으로해서 회전을 o1_rot 각 만큼 회전을 시키는 것이다. 결과적으로 <2>번 코드에서의 이동 변환 행렬과 <4>번 코드의 회전 변환 행렬의 연산으로 좌표축이 변경될 것임을 알수있다.
<5>번 코드들은 지구를 그려주는 코드들이다. (자세한 설명은 Quadric 객체를 설명한 장을 참고하기 바란다)
<6>번 코드는 <3>번 코드에서 저장해둔 좌표축을 꺼내는 것인데 이렇게 함으로써 <4>번 코드에 의해 변환되기 이전의 좌표축으로 되돌릴수있다.
<7>부터 <12>번까지는 달을 그려주는 것이다. 달은 지구를 중심으로 공전하며 또 스스로 자전한다.
<7>번 코드 다시 변환행렬을 스택에 저장한다.
<8>번 코드는 y축을 기준으로 회전을 o2_rot1 각 만큼 하게 된다.
<9>번 코드는 x축으로 distance만큼 이동을 하게된다. 결과적으로 <8>과 <9>번 코드에 의해서 달은 지구를 중심으로 공전을 하게되는 것이다.
<10>번 코드는 y축을 기준으로 o2_rot2 각 만큼 회전하게 된다. 이 코드로써 달이 자전 하게 된다.
<8>, <9>, <10>번의 변환행렬의 순서는 중요하다. 순서가 바뀔 경우 우리가 원하는 동작과 위치는 잘못될 것이다.
<11>번 코드는 달을 그려주는 코드들이다. (자세한 설명은 Quadric 객체를 설명한 장을 참고하기 바란다)
<12>번 코드는 다시 <7>번 코드에 의해서 저장된 변환 행렬을 꺼내는 것이다.
<13>번 코드들은 지구와 달의 회전각들을 일정하게 증가시켜주는 코드들이다.
이렇게 정리를 하면 되겠다. 물체 마다 하나씩의 지역 좌표계를 둔다는 것이다. 즉 PushMatrix와 PopMatrix 사이에서 물체의 변환 행렬을 정의해주면 다른 물체에는 전혀 영향을 받지 않으므로 각 물체마다 독립적으로 생각해줄 수 있다.
이상으로 공간상에 물체를 구성하는 것에 대해서 마칠까 한다. 알고 보면 쉬운 내용이지만 처음 접할때는 어려울 수 있다. 비단 모든 공부가 그러한것 같다. 이 장을 두차례 장도만 읽어 본다면 꽤 여러운 공간상의 물체를 구성하는 방법에 대해서도 알수있지 않을까 싶다.
여기서 마치기 전에 변환행렬에 관련된 API에 대해서 몇가지 알아보도록 하겠다. 먼저 알아볼것은 glMulMatrix라는 함수인데 이 함수는 기존의 변환행렬에 이 함수의 인자로써 갖는 4X4 배열로 정의된 4X4행렬을 곱하게 된다. 자신만의 변환 행렬을 사용할수있는 중요한 API이다. 그리고 현제의 변환 행렬이 무었인지를 알아내는 함수로 glGetDoublev라는 함수를 사용하면 된다. 즉 다음과 같은 형태로 사용된다.
glGetDoublev(GL_MODELVIEW_MATRIX, v);
v는 4X4행렬의 16개의 값이 충분이 들어갈수있는 GLdouble형 배열 공간인데 4X4 배열이면 충분하겠다.
자, 이로써 공간상에 물체의 구성에 대해서 끝마칠까 한다.