[OpenGL Tutorial] Bill-board(빌보드) 기법

사용자 삽입 이미지빌보드 기법이란 사각형이 항상 시점을 바라보도록 만드는 것을 말한다. 결과적으로 관찰자가 어느 위치로 이동하더라도 사각형은 항상 같은 면을 관찰자에게 보여주게 된다. 응용예로써 넓은 대지위에 나무를 나타내고자 할때, 나무를 메쉬(Mesh)를 사용하여 모델링하지 않고 나무 이미지만으로 구현이 가능하다.

구현에 대한 핵심은, 항상 시점을 바라보도록 사각형을 이루는 네 좌표를 조정하여야 하는데, 이를 위해 Model View 행렬을 이용한다. Model-View 행렬에는 관찰자의 시점에 대한 정수직벡터와 정우측벡터에 대한 정보가 담겨있다.

일단 OpenGL에서 Model View 행렬을 얻는 코드를 알아보자.

Glfloat viewMatrix[16];
GlGetFloatv(GL_MODELVIEW_MATRIX, viewMatrix);

ViewMatrix에 저장된 Model View 행렬이 다음 그림과 같다고 할 때 벡터(m1, m5, m9)은 시선 벡터와 수직인 위쪽 벡터이고, 벡터(m0, m4, m8)는 시선 벡터와 수직인 오른쪽 벡터이다.사용자 삽입 이미지시선과 위쪽으로 수직인 벡터를 UpVector라 하고 오른쪽 방향으로 수직인 벡터를 RightVector라 하자. 이 두 벡터를 이용하여 사각형을 이루는 4개의 좌표를 조정하기 위해, 사각형의 중심 벡터를 centerVector라고 하면, 사각형을 이루는 4개의 좌표는 다음 그림과 같다. 사용자 삽입 이미지위의 네 좌표를 스칼라값 Size를 적용하면 다음과 같다. Size는 사각형의 크기로 생각할 수 있다.

CenterPoint + (RightVector + UpVector) * (-Size))
CenterPoint + (RightVector -UpVector) * Size
CenterPoint + (RightVector + UpVector) * Size
CenterPoint + (UpVector -RightVector) * Size

실제로 구현 코드를 살펴보자.

void GL_Window::Draw(void)
{
    // Clear Screen And Depth Buffer
    glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);     
    glLoadIdentity (); // Reset The Modelview Matrix
    glTranslatef (0.0f, 0.0f, -6.0f); // Translate 6 Units Into The Screen
    glRotatef (angle, 0.0f, 1.0f, 0.0f); // Rotate On The Y-Axis By angle
   
    // 모델뷰 행렬을 얻는다
    float mat[16];
    glGetFloatv(GL_MODELVIEW_MATRIX, mat);
   
    // 모델뷰 행렬로부터 시선방향에 대해 수직으로 오른쪽 벡터와 
    // 수직으로 윗쪽 벡터를 정의한다.
    vector3_t right(mat[0], mat[4], mat[8]);
    vector3_t up(mat[1], mat[5], mat[9]);
   
    glBindTexture(GL_TEXTURE_2D, texture[0]); // Select Our Texture
   
    vector3_t pos(0.0f, 0.0f, 1.0f);
   
    glBegin(GL_QUADS);
    // 빌보드 기법을 적용하지 않은 폴리곤의 경우
    // Bottom Left Of The Texture and Quad
    // glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f); 
    // Bottom Right Of The Texture and Quad
    // glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f);
    // Top Right Of The Texture and Quad
    // glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f); 
    // Top Left Of The Texture and Quad
    // glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f); 
   
    // 빌보드 기법이 적용된 폴리곤의 경우
    // Bottom Left Of The Texture and Quad
    glTexCoord2f(0.0f, 0.0f); glVertex3fv((pos+(right+up)*(-1)).v);
    // Bottom Right Of The Texture and Quad 
    glTexCoord2f(1.0f, 0.0f); glVertex3fv((pos+(right-up)*(+1)).v);
    // Top Right Of The Texture and Quad
    glTexCoord2f(1.0f, 1.0f); glVertex3fv((pos+(right+up)*(+1)).v); 
    // Top Left Of The Texture and Quad
    glTexCoord2f(0.0f, 1.0f); glVertex3fv((pos+(up-right)*(+1)).v); 
    glEnd();

    glFlush();
} 

위의 소스에서 이미 제작된 vector 클래스를 사용하였다. vector 클래스의 사용은 크게 어렵지 않으므로 10여분정도 투자하여 소스를 분석해보면 쉽게 이해할 수 있을 것이다.

빌보드 기법을 적용하지 않은 경우에 대한 소스 코드를 주석처리하였 놓았다. 주석을 제거하여 실행해 보면서 비교해보기 바란다.

[OpenGL Tutorial] Display List

사용자 삽입 이미지이번장에서는 오랬동안 길게 끌어온 OpenGL의 렌더링 성능을 향상 시켜주는 Display List에 대해 알아보고자 한다. 사실 이 부분을 16장에 위치시키기에는 그 내용이 기본적이긴 하지만 필자 스스로의 혼란을 줄이기 위해 그냥 순서대로 번호를 매겼다. 이 강좌는 1장에 이여 두번째로 NeHe의 Display List를 그대로 번역한다. 이제 시작해 보자.

이 강좌에서는 Display List에 대한 설명이다. 간단한 장면을 연출하고자 할때 Display List를 사용함으로써 속도의 개선 뿐만이 아니라 코드 줄수 또한 줄일 수 있다.

예를 들어서, 소행성 게임을 만들고 있다고 해 보자. 각 레벨은 최소한 2개 이상의 소행성으로 시작된다. 그래서 당신은 3차원의 소행성을 어떻게 만들 것인지 계산한다. 일단 계산이 끝난후 폴리곤이나 Quad를 이용하여 소행성을 만든다. 이 소행성이 여덜개의 면으로 이루어졌다고 해보자. 이렇게 만든 하나의 소행성을 여러개 만들기 위해, 간결하게 반복문을 이용하여 여러개의 소행성들을 그리게 될 것이다. 소행성을 만들기 위해 18줄 이상의 코드를 쳐 넣을 것이다. 소행성 하나 하나를 시스템 상에서 계산하여 매번 소행성을 그려내는 것은 비효율적이다. 소행성이 복잡하면 복잡할 수록 그 효율은 극적으로 떨어지게 된다.

그렇다면 그 해결책은 무엇인가? 바로 Display Lists이다. Display List를 사용함으로써, 소행성을 단 한번만 만들기만 하면 된다. 여기에 텍스쳐 맵핑을 할 수도 있고 색을 입힌다거나 원하는 모든 것들을 적용할 수 있다. 이 Display List에 이름을 부여할 수 있는데, 예를 들어 소행성의 Display List로 생성을 하고 이름을 ‘Asteroid’라고 한후에 나중에 소행성이 필요할때 마다 단지 이미 정해둔 이름인 ‘Asteroid’로써 Display List를 호출하기만 하면 된다. 즉, 앞으로 소행성을 그리고자 한다면 단지 단 한줄의 코드인 glCallList(asteroid)를 호출하기만 하면 된다. 이미 만들어진 소행성은 즉시 화면에 나타나게 된다. 소행성은 이미 Display List에서 모든 계산이 이루어져 생성되었기 때문에 OpenGL은 다시 소행성을 그리기 위해 계산할 필요가 없어진다. 이것은 메모리 상에 이미 계산의 결과를 저장해 놓았기 때문이다. 이것이 바로 왜 Dislpay List를 사용함으로써 속도가 극적으로 향상되는지하는지에 대한 해답니다.

이제 Display List에 대해 배워볼 마음이 생기는가? 🙂 우리는 앞으로 Q-Bert Display List 데모 프로그램을 만들어 볼 것이다. 화면상에 15개의 큐브를 올려 놓아 보는 데모이다. 각각의 큐브는 BOX와 TOP으로 구성되져 있다. TOP은 별도의 Display List로서 만들어질 것인데 이것은 이 것에 좀더 어두운 색으로 연출하고자 함이다. BOX는 윗면이 없는 큐브이다.

이 코드는 강좌 6의 것을 사용한다. 무엇을 전달하고자 하는지를 쉽게 하기 위해 프로그램을 거의 전체적으로 다시 작성할 것이다. 다음의 코드 라인은 대부분의 강좌에서의 표준 코드들이다. (역자주: 강좌 6의 소스 코드를 일단 다운 로드 받아 살펴보기 바란다. 단지 텍스쳐 맵핑에 대한 내용을 다룬 것이다.)

#include  // Header File For Windows
#include  // Header File For Standard Input/Output 
#include  // Header File For The OpenGL32 Library
#include  // Header File For The GLu32 Library 
#include  // Header File For The GLaux Library

HDC hDC=NULL; // Private GDI Device Context 
HGLRC hRC=NULL; // Permanent Rendering Context
HWND hWnd=NULL; // Holds Our Window Handle 
HINSTANCE hInstance; // Holds The Instance Of The Application
bool keys[256]; // Array Used For The Keyboard Routine 
bool active=TRUE; // Window Active Flag Set To TRUE By Default 
bool fullscreen=TRUE; // Fullscreen Flag Set To Fullscreen Mode By Default 

이제, 앞으로 사용할 변수를 선언해 보자. 먼저 하나의 텍스쳐를 저장하기 위한 변수를 선언하고 앞에서 언급한 2개의 Display List를 위한 변수 2개를 선언하자. 이러한 변수는 Display List가 램에서 어디에 저장되어 있는가 하는 지시자로써의 행동한다. 이 2개의 Display List를 BOX와 TOP이라 부를 것이다.

2개의 변수를 선언한 후에 xloop와 yloop라는 변수를 선언하는데 이것은 화면상의 큐브를 위치시키는데 사용된다. 또 xrot와 yrot라는 2개의 변수를 선언하는데 이것은 x와 y축으로 큐브들을 회전시키는데 사용되는 변수들이다.

GLuint texture[1]; // Storage For One Texture 
GLuint box; // Storage For The Display List 
GLuint top; // Storage For The Second Display List 
GLuint xloop; // Loop For X Axis 
GLuint yloop; // Loop For Y Axis 
GLfloat xrot; // Rotates Cube On The X Axis 
GLfloat yrot; // Rotates Cube On The Y Axis 

다음에 우리는 2개의 색상 배열을 생성한다. 첫번째의 boxcol은 밝은 계열의 색상들로 빨간색, 오랜지색, 초록색 그리고 파란색의 색상값을 저장한다. {}안에 지정된 각각의 값은 Red, Green Blue 값을 나타낸다. 각각의 {}의 구룹은 지정된 색이다.
두번째 색상의 배열은 어두운 계열의 색상들로 빨간색, 오랜지색, 초록색 그리고 파란색을 위한 것이다. 어두운 색들은 박스의 윗면을 나타내는데 사용된다. 우리는 박스의 나머지 부분보다 박스의 뚜껑을 좀더 어둡게 하고자 한다.

static GLfloat boxcol[5][3]= // Array For Box Colors 
 {// Bright: Red, Orange, Yellow, Green, Blue 
 {1.0f,0.0f,0.0f},
 {1.0f,0.5f,0.0f},
 {1.0f,1.0f,0.0f},
 {0.0f,1.0f,0.0f},
 {0.0f,1.0f,1.0f}}; 
   
static GLfloat topcol[5][3]= // Array For Top Colors 
 { // Dark: Red, Orange, Yellow, Green, Blue 
 {.5f,0.0f,0.0f},
 {0.5f,0.25f,0.0f},
 {0.5f,0.5f,0.0f},
 {0.0f,0.5f,0.0f},
 {0.0f,0.5f,0.5f}}; 
  // Declaration For WndProc
 LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);   


이제 실제 Display List를 만들어 본다. 이미 짐작했겠지만, 박스를 만들기 위한 모든 코드는 첫번째 List로 하고 뚜껑으로 사용할 박스의 Top은 다른 List에 들어가게 된다. 다음 절에서 더 자세한 내용을 설명하도록 하겠다.

GLvoid BuildLists() // Build Box Display List 
{ 

우리는 OpenGL에게 우리가 2개의 List를 필요로 한다고 알리는 것으로 시작한다. glGenLists(2)는 두개의 List를 위한 공간을 생성하고 첫번재 List를 가르키는 포인터를 반환한다. box가 첫번째 List의 위치를 가지게 된다. 언제든지 box를 호출한다면 첫번째 list에 담긴 물체가 그려지게 된다.

    box=glGenLists(2); // Building Two Lists 

List를 위한 공간을 마련했으므로 실제 List를 생성해 보자. 우리는 이미 두개의 List를 위한 메모리 공간을 마련했고 box가 첫번째 list를 저장하는 공간을 가르킨다는 것을 알고 있다. 이제 OpenGL에게 알려할 모든 것은 리스드가 어디로 가야하며 만들어질 리스트가 어떤 타입인가 하는 것들이다.

작업을 하기 위해 glNewList() 명령을 사용한다. 우리는 첫번째 인자가 box라는 것을 알게 될 것이다. 이것은 box가 가르키고 있는 메모리의 위치에 리스트가 저장되어져 있다고 openGL에게 알리는 것이다. 두번째 인자인 GL_COMPILE는 OpenGL에게 우리가 메모리 상에 모든 계산을 미리 해 놓으라는 지시인데 이렇게 함으로써 나중에 다시 불피요한 재계산을 하지 않아도 되도록 하는 것이다.


GL_COMPILE은 프로그래밍과 유사하다. 프로그램을 작성한다면 컴파일러에 적재해야 한다. 코드를 실행하기 위해서 매번 컴파일을 해야 한다. 코드가 이미 실행파일 형태로 컴파일 되어있다면 이후부터 컴파일 할 필요없이 실행파일만을 클릭하여 실행하기만 하면 된다. 컴파일이 필요없는 것이다. 일단 GL이 display list를 컴파일했다면 이미 렌더링 준비가 되어진 것이고 나중에 또 컴파일이 요구되지 않는다. 이것이 바로 display list를 사용함으로써 속도를 향상시키는 이유이다.

    glNewList(box,GL_COMPILE); // New Compiled box Display List


코드의 다음부는 뚜껑(top)이 없는 박스를 그리는 것이다. 이것은 화면상에 나타나지 않고 단지 display list상에 저장된다.

glNewList()와 glEndList()사이에 당신이 원하는 코드들을 추가할 수 있다. 색상을 설정하거나 텍스쳐 맵핑을 바꾼다거나 하는 것등. 추가할 수 없는 유일한 코드는 list에 지정된 객체의 형태를 변경시키는 코드이다. 일단 list로 만들어지면 변경할 수 없다.

만약 glColor3ub(rand()%255, rand()%255, rand()%255)의 코드를 아래에 제시한 코드에 추가했다면, 아마도 당신은 화면상에 물체가 그려지는 때마다 이것의 색이 바뀐다고 생각할 것이다. 그러나 이미 리스트를 생성할때 색상이 지정되졌기 때문에 색은 변경되지 않는다. (역자주: 만약 변경하고자 한다면 list를 생성할때 색상을 지정하는 코드를 사용해서는 않된다)

    glBegin(GL_QUADS); // Start Drawing Quads 
    // Bottom Face 
    // - Top Right Of The Texture and Quad
    glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, -1.0f, -1.0f); 
    // - Top Left Of The Texture and Quad 
    glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, -1.0f, -1.0f);
    // - Bottom Left Of The Texture and Quad 
    glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f); 
    // - Bottom Right Of The Texture and Quad    
    glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f); 
   
    // Front Face 
    // - Bottom Left Of The Texture and Quad
    glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f); 
    // - Bottom Right Of The Texture and Quad 
    glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f);
    // - Top Right Of The Texture and Quad 
    glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f);  
    // - Top Left Of The Texture and Quad    
    glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f); 
    
    // Back Face 
    // - Bottom Right Of The Texture and Quad
    glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f);
    // - Top Right Of The Texture and Quad  
    glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f); 
    // - Top Left Of The Texture and Quad 
    glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f); 
    // - Bottom Left Of The Texture and Quad
    glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f);  
   
    // Right face 
    // - Bottom Right Of The Texture and Quad
    glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f);
    // - Top Right Of The Texture and Quad   
    glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f);
    // - Top Left Of The Texture and Quad  
    glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f); 
    // - Bottom Left Of The Texture and Quad 
    glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f); 
   
    // Left Face 
    // - Bottom Left Of The Texture and Quad 
    glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f);
    // - Bottom Right Of The Texture and Quad 
    glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f);
    // - Top Right Of The Texture and Quad   
    glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f); 
    // - Top Left Of The Texture and Quad
    glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f);  
    glEnd(); // Done Drawing Quads 

우리는 List를 다 만들었다는 것을 OpenGL에게 알리기 위해 glEndList() 명령을 내린다. glNewList()와 glEndList() 사이의 부분은 Display list의 부분이다. glNewList() 앞의 부분이나 glEndList()의 뒷부분은 현재 display list의 부분과는 상관없는 부분이다.

    glEndList(); // Done Building The box List 

이제, 두번째 Display list를 만들 것이다. 두번째 display list가 메모리에 저장될 곳을 찾기 위해 우리는 예전의 display list(box)에 1을 추가한다. 아래의 코드는 두번째 display list의 위치와 같은 ‘top’을 만들 것이다.

    top=box+1; // top List Value Is box List Value +1 

이제 두번째 display list을 저장할 곳을 알았으므로 뚜껑에 대한 list를 만들 수 있다. 첫번재 list를 만드는 같은 방법을 사용하지만 이번에는 ‘box’대신 ‘top’에 list를 저장한다고 OpenGL에게 알린다.

    glNewList(top,GL_COMPILE); // New Compiled top Display List

다음 코드는 단지 박스의 꼭대기를 그려주는 것이다. Z 평면 상의 간단한 사각형이다.

    glBegin(GL_QUADS); // Start Drawing Quad 
    // Top Face 
    // - Top Left Of The Texture and Quad 
    glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f);
    // - Bottom Left Of The    Texture and Quad 
    glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, 1.0f, 1.0f); 
    // - Bottom Right Of    The Texture and Quad 
    glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, 1.0f, 1.0f);  
    // - Top Right Of The    Texture and Quad 
    glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f); 
    glEnd(); // Done Drawing Quad 

다시 우리는 OpenGL에게 두번째 list를 생성했다고 glEndList() 명령을 이용해 알린다. 바로 아래처럼. 우리는 성공적으로 2개의 display list를 생성했다.

    glEndList(); // Done Building The top Display List 
}

비트맵/텍스쳐를 생성하는 코드는 이전에 강좌의 비트맵을 로딩하고 텍스쳐를 생성하는 코드와 같다. (역자주: Dip2K의 튜터리얼도 NeHe의 텍스쳐 맵핑 방법과 동일하다. 우리는 각각의 큐브의 모든 6면 상에 맵핑할 텍스쳐를 원한다.좀더 부드러운 텍스쳐 맵핑을 위해 Mipmapping을 사용하고자 한다. 필자는 픽셀을 보는 것을 무척 싫어한다. (역자주: 픽셀을 본다는 것은 알리아싱 현상을 의미한다) ‘cube.bmp’라는 비트맵을 로딩한다. data 디렉토리에 이 비트맵 파일이 위치한다. LoadBMP 함수를 찾아서 다음과 같이 수정한다.

if (TextureImage[0]=LoadBMP("Data/Cube.bmp")) // Load The Bitmap 

폼의 크기가 재조정될때 발생하는 이벤트의 코드는 강좌 6의 코드와 동일하다.

init 코드는 약간의 수정만이 필요하다. BuildList() 코드를 추가한다. 이것은 앞에서 display list를 생성하는 코드를 담고 있다. LoadGLTextures()함수 이후에 BuildList() 를 호출한다는 것에 주의해야 한다. 이것은 BuildList() 함수에서 텍스처를 필요로 하므로 먼저 텍스쳐를 생성해야 한다는 것이다.

int InitGL(GLvoid) // All Setup For OpenGL Goes Here 
{ 
    if (!LoadGLTextures()) // Jump To Texture Loading Routine 
    { 
        return FALSE; // If Texture Didn't Load Return FALSE 
    } 

    BuildLists(); // Jump To The Code That Creates Our Display Lists 
    glEnable(GL_TEXTURE_2D); // Enable Texture Mapping 
    glShadeModel(GL_SMOOTH); // Enable Smooth Shading 
    glClearColor(0.0f, 0.0f, 0.0f, 0.5f); // Black Background 
    glClearDepth(1.0f); // Depth Buffer Setup 
    glEnable(GL_DEPTH_TEST); // Enables Depth Testing 
    glDepthFunc(GL_LEQUAL); // The Type Of Depth Testing To Do 

다음의 세줄의 코드는 quick & dirty 빛을 활성화 하는 것이다. (역자주: quick & dirty 광원은 비디오 카드에 기본적으로 설정된 광원이다) Light0는 대부분의 비디오 카드에 미리 정의된 것이다. light0을 활성화 함으로써 빛을 사용한다. 만약 독자의 비디오 카드에서 light0가 작동하지 않는다면(화면에 마냥 검다면) 그냥 광원을 꺼라.마지막 라인인 GL_COLOR_MATRIAL은 텍스쳐 맵핑에 색을 추가하도록 해 준다. 이 재질의 컬러링을 가능하지 않게 한다면, 텍스쳐는 항상 원래의 색상으로 나타난다. 즉 glColor3f(r,g,b)가 아무런 효과를 내지 못한다. 그래서 이것을 가능하게 해주는 것이 중요하다.

    glEnable(GL_LIGHT0); // Quick And Dirty Lighting (Assumes Light0 Is Set Up)    
    glEnable(GL_LIGHTING); // Enable Lighting 
    glEnable(GL_COLOR_MATERIAL); // Enable Material Coloring 

최종적으로 멋지게 보이도록 하기 위해서 투영에 대한 힌트를 GL에게 제공하고 초기화가 잘되었음을 알리기 위해 TRUE를 반환한다.

    // Nice Perspective Correction 
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); 
   
    return TRUE; // Initialization Went OK 


이제 그려주는 코드에 대한 것이다. 일단 화면과 Depth Buffer를 청소하는 일부터 시작한다.

그 다음에 큐브에 텍스쳐 맵핑을 바인드한다. Display list 안에 이 텍스쳐 맵핑을 추가할 수 있었으나 그 밖으로 빼 놓았다. 이것으로써 원한다면 언제든 텍스쳐 맵핑을 변경할 수 있는 것이다. (역자주: 이미 앞에서 언급한대로 List를 만들때 색상이라든지 텍스쳐 맵핑을 지정하면 나중에 그 List의 객체의 색상, 텍스쳐 맵핑을 변경할 수 없다)

int DrawGLScene(GLvoid) // Here's Where We Do All The Drawing 
{ 
    // Clear The Screen And The Depth Buffer
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);      
    glBindTexture(GL_TEXTURE_2D, texture[0]); // Select The Texture   

이제 재미있는 것에 대해 살펴보자. yloop라 불리는 반복문이 있다. 이 반복문은 큐브들의 Y 축상(위에서 아래)에 위치시키는데 사용된다. 위에서 아래로 5개의 큐브를 원하므로 6보다 하나 작은 반복을 한다(즉, 다섯번)

for (yloop=1;yloop<6;yloop++) // Loop Through The Y Plane 
{ 

xloop라는 다른 반복문이 있다. 이것은 X 축상(왼쪽에서 오른쪽)에 큐브들을 위치시키는데 사용된다. 왼쪽에서 오른쪽으로 그리고자 하는 큐브는 정해져 있지 않다. 만약 가장 위쪽의 줄이라면 xloop는 0에서 1까지의 xloop 반복이면 된다. 그리고 그 다음 줄의 xloop는 0에서 2가 될것이다(즉, 2개의 큐브가 그려져야 한다) (역자주: 계속 하나씩 증가한다, 실제 결과 프로그램을 실행하여 살펴보면 확실히 알수있다)

    // Loop Through The X Plane
    for (xloop=0;xloop    { 

glLoadIdentity()를 호출함으로써 View를 초기화한다.

            glLoadIdentity(); // Reset The View  

다음은 화면상의 지정된 지점으로 이동시키는 코드이다. 이것은 다소 헤깔리지만 실제로는 그렇지 않다. X축 상에서, 다음의 내용이 이뤄진다 :

큐브들이 모여 이뤄진 피라미드가 화면상의 중심에 놓여지도록 오른쪽으로 1.4 유닛만큼 이동한다. 그 다음에 xloop에 2.8을 곱한후 다시 여기에 1.4를 더한다. 마지막으로 yloop*1.4를 뺀다. 이것은 큐브들을 그들이 위치한 줄에 따라 왼쪽으로 이동하게 된다.

Y축상에서 6에서 yloop를 뺌으로써 아래 방향으로 피라미드가 만들어진다. 다음에 그 결과에 2.4를 곱한다. 큐브들은 y축 상의 각각의 꼭대기 상에 놓여진다. 다음에 7을 빼는데 이것은 피라미드가 화면의 아래쪽에서 시작하고 윈쪽으로 만들어지게 한다.

최종적으로 Z축 상에서 화면 안쪽으로 20 유닛 이동한다. 이것은 피라미드를 멋지게 만든다.

    // Position The Cubes On The Screen 
    glTranslatef(1.4f+(float(xloop)*2.8f)-(float(yloop)*1.4f),
        ((6.0f-float(yloop))*2.4f)-7.0f,-20.0f);  

이제 x축 상으로 회전시켜 보자. 우리는 큐브를 뷰쪽으로 45도에서 yloop*2를 뺀 상태로 기울인다.. 퍼스펙티브 모드는 자동으로 큐브들을 기울이므로 이 기울어짐을 제거 했다. 이것은 최선은 아니지만 원하는대로 작동한다. 🙂

최종적으로 xrot를 더한다. 이것은 키보드를 통해 각을 조정할 수있도록 한다.

x축으로 회전한후에 y축상으로 45도 회전하고 yrot을 더하는데 이것은 키보드를 통해서 yrot값을 조정할 수 있다.

glRotatef(45.0f-(2.0f*yloop)+xrot,1.0f,0.0f,0.0f); // Tilt The Cubes Up And Down 
glRotatef(45.0f+yrot,0.0f,1.0f,0.0f); // Spin Cubes Left And Right 

다음에 큐브들이 위치한 줄에 따라 달리 색상을 지정한다.

            glColor3fv(boxcol[yloop-1]); // Select A Box Color

이제 박스에 대한 List를 호출한다.

            glCallList(box); // Draw The Box

큐브의 꼭대기에 놓여질 뚜껑의 색상을 지정한다.

            glColor3fv(topcol[yloop-1]); // Select The Top Color

뚜껑에 대한 List를 호출하고 모든것이 잘되었으므로 TRUE를 호출한다.

            glCallList(top); // Draw The Top 
        } 
    } 
    return TRUE; // Jump Back 
} 

이제 남은 코드들은 WinMain 함수에서 이뤄지는데 다음은 키보드에 대해 xrot와 yrot의 값을 변경해 주는 키보드 입력 처리 코드이다. SwapBuffer(hDC) 코드 다음에 놓여진다.

SwapBuffers(hDC); // Swap Buffers (Double Buffering) 

if (keys[VK_LEFT]) // Left Arrow Being Pressed? 
{ 
    yrot-=0.2f; // If So Spin Cubes Left 
} 

if (keys[VK_RIGHT]) // Right Arrow Being Pressed? 
{ 
    yrot+=0.2f; // If So Spin Cubes Right 
}

if (keys[VK_UP]) // Up Arrow Being Pressed? 
{ 
    xrot-=0.2f; // If So Tilt Cubes Up 
} 

if (keys[VK_DOWN]) // Down Arrow Being Pressed? 
{ 
    xrot+=0.2f; // If So Tilt Cubes Down 
} 

<변역 끝>

변역을 여기서 끝마친다. 사실 이 강좌는 Display List에 대한 장이다. Display List에 대한 사용법이 쉽다는 것을 알 수 있다. 필자는 이 강좌를 번역할때 List를 제작하고 사용하는 방법에 대해서는 최대한 모든내용을 번역했지만 후반부의 큐브를 화면상에 피라미드 모양으로 위치시키는 세세한 내용은 그냥 술~ 술~ 넘기며 번역했음을 고백한다.

[OpenGL Tutorial] Composition Objects

사용자 삽입 이미지이 장에서는 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 배열이면 충분하겠다.

자, 이로써 공간상에 물체의 구성에 대해서 끝마칠까 한다.

[OpenGL Tutorial] TESSELLATION

OpenGL에서 폴리곤의 제약 중에 폴리곤은 오목한 부분이 있어서는 않된다는 것이다. 즉 아래와 같은 별 모양의 폴리곤은 규칙을 위반한 폴리곤이 되겠다.


사용자 삽입 이미지
위의 폴리곤은 총 8개의 점으로 이루어진 폴리곤이다. 튀어나온 부분(볼록한 부분)이 총 4개인데 폴리곤에서 볼록한 부분은 상관없지만 들어간 부분(오목한 부분) 역시 4개로써 이것은 OpenGL에서는 다음과 같은 모습으로 연출되고만다.


사용자 삽입 이미지


오목한 부분중에 두개의 부분이 무시되어져 버린 상태에서 연출되었다는 것을 알수있다. 그렇다면 이런 현상을 어떻게 막을수있겠는가? 그것은 폴리곤을 오목한 부분이 없도록 쪼개서 나누어 연출해야만 한다. 어떠한 폴리곤이라도 오목한 부분이 없이 나눌 수는 있지만 쪼개기가 어려운 경우가 많다. 그렇다면 좀더 쉽게 위의 오동작 아닌 오동작을 방지하는 방법은 없는가? 다행이도 TESSELLATION이라는 기능이 있다. 이것은 폴리곤의 각 꼭지점만을 명시해 주면 자동으로 폴리곤을 오목한 부분이 없도록 쪼개서 연출해 주는 아주 강력한 기능이다. 바로 이장이 이 TESSELLATION 기능에 대해서 알아보는 장이다.

(왜 OpenGL은 오목한 부분을 가진 폴리곤을 연출하지 못하는가? 그것은 폴리곤을 아주 빠르게 연출하기 위해서이다)

자, 이제 TESSELLATION에 대해서 알아보도록 하자. 소스 코드는 1장의 코드에서 시작한다.

먼저 TESSELLATION(앞으로 Tess라 부르겠다) 기능을 사용하기 위한 객체를 하나 생성해야 겠다. 이를 위해서 먼저 Tess 객체를 위한 포인터 변수를 전역 변수 선언 지역에 하나 정의하자. Tess는 여러가지 속성을 가지고 있으므로 이런 절차가 필요하다.

GLUtriangulatorObj *tess;

이제 tess라는 Tess를 위한 포인터 변수가 만들어졌다. 포인터 변수이므로 이제 실제 인스턴스를 생성해야 한다.

int initGL(GLvoid)
{
    glShadeModel(GL_SMOOTH); // Enable Smooth Shading
    glClearColor(0.0f, 0.0f, 0.0f, 0.5f); // Black Background
    glClearDepth(1.0f); // Depth Buffer Setup
   
    glEnable(GL_DEPTH_TEST); // Enables Depth Testing
    glDepthFunc(GL_LEQUAL); // The Type Of Depth Testing To Do
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); 
    // Really Nice Perspective Calculations
   
    // New Adding
    tess=gluNewTess(); //<*>
    gluTessCallback(tess, GLU_TESS_BEGIN, 
        (void (__stdcall *)(void))CustomTessBegin); //<1>
    gluTessCallback(tess, GLU_TESS_VERTEX, 
        (void (__stdcall *)(void))CustomTessVertex); //<2>
    gluTessCallback(tess, GLU_TESS_END, 
        (void (__stdcall *)(void))CustomTessEnd); //<3>
    gluTessCallback(tess, GLU_TESS_ERROR, 
        (void (__stdcall *)(void))CustomTessError); //<4>
   
    return TRUE; // Initialization Went OK
}

위의 코드는 우리에게 익숙한 초기화 코드가 오는 함수이다. 바로 <*>번의 코드가 Tess객체의 인스턴스를 생성하여 전역변수에서 정의한 tess 포인터 변수에게 넘겨주는 기능을 한다. (주의: Tess는 TESSELLATION을 의미하고 tess는 앞서 정의한 포인터 변수명) Tess 인스턴스를 생성했으므로 우리가 프로그램을 종료할때 해제해 주는 코드가 있어야 한다는 것은 자명한 사실이다. glKillWindow 함수의 맨 아래 부분에 다음 코드를 추가하자.

gluDeleteTess(tess);

이제 실펴보아야 할것은 <1>~<4>번 코드들인데 이것들은 무엇인가? Tess는 Core(핵심) OpenGL API군이 아닌 GLU라는 유틸리티 API군이다. 따라서 코어 OpenGL API가 아닌 Tess가 폴리곤을 그리기 위해서는 코어 OpenGL API의 도움을 받아야 하는데 바로 이 도움을 받기 위한 코어 OpenGL API 함수들을 콜백하기 위한 코드들이다.

gluTessCallback함수는 세개의 인자를 취하는데 첫번째는 Tess 객체의 포인터 변수이고 둘째는 어떤 기능에 대한 콜백함수를 지정할 것인지에 대한 식별자이고 세번째 인자는 실제 콜백될 함수이다. 이제 하나 하나 살펴보자.

<1>번은 Tess가 폴리곤을 그리기 시작할때 호출될 콜백함수를 지정하는 것이다.

<2>번은 Tess가 폴리곤을 그리기 위해서 각 꼭지점을 지정할때 호출할 콜백수를 지정하는 것이다.

<3>번은 Tess가 폴리곤을 다 그렸다고 판단할때 호출할 콜백함수를 지정하는 것이다.

<4>번은 Tess가 폴리곤을 그리는 도중에 에러가 발생할때 호출할 콜백함수를 지정하는 것이다.

그렇다면 각 콜백 함수는 어떤 내용을 담고 있는가. 위에 제시된 4개의 콜백함수의 모습은 다음과 같다.

void __stdcall CustomTessBegin(GLenum prim)
{
    glBegin(prim);
}

void __stdcall CustomTessVertex(void *data)
{
    glVertex3dv((GLdouble *)data);
}

void __stdcall CustomTessEnd()
{
    glEnd();
}

void __stdcall CustomTessError(GLenum errorCode)
{
    char err[256];
   
    sprintf(err, "Tessellation Error: %s\n", gluErrorString(errorCode));
    if(errorCode!=GL_NO_ERROR) 
        MessageBox(hWnd, err, "TESSELLATION Error", MB_OK);
}

CustomTessBegin은 glBegin이라는 코어 OpenGL API를 호출하고 있고 CustomTessVertex는 glVertex3dv라는 코어 OpenGL API를 호출하고 있다. CustomTessEnd는 glEnd라는 코어 OpenGL API를 호출하고 있고 CustomTessError는 에러가 발생할을때 처리할 코드들이 담겨있는데 여기서는 그 에러가 무엇인지를 표현해주기만 한다. 실제로 위의 코드들은 여러분이 직접 타이핑해야 한다. 즉, 정리하자면 Tess는 폴리곤을 그리기 위해서 코어 OpenGL API의 직접적인 도움을 받아 생성한다는 것이다.

자, 이제 Tess를 이용해서 물체를 그려보는 코드를 작성하는 것만이 남았다. 어떤 폴리곤을 그릴까? 우리가 이 장의 맨 첫부분에서 소개했던 별로 하자. 먼저 그 별을 그리기 위해 그 별 모양의 폴리곤의 꼭지점의 데이타를 정의해야한다. 전역 변수에서 다음과 같은 배열을 정의하자.

GLdouble ov[8][3] = {{0.0f, 1.0f, 0.0f},
  {0.4f, 0.4f, 0.0f},
  {1.0f, 0.0f, 0.0f},
  {0.4f, -0.4f, 0.0f},
  {0.0f, -1.0f, 0.0f},
  {-0.4f, -0.4f, 0.0f},
  {-1.0f, 0.0f, 0.0f},
  {-0.4f, 0.4f, 0.0f} };

이미 짐작하고 있던대로 각각의 8개의 꼭지점을 정의하고 있다. 이 배열을 토대로 해서 별 모양의 폴리곤을 그리려고 하는 것이다. 이제 실제로 그려주는 코드를 살펴 보도록하자. DrawGLScene 함수를 살펴보자.

int DrawGLScene(GLvoid) // Here's Where We Do All The Drawing
{
    // Clear Screen And Depth Buffer
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 
    glLoadIdentity(); // Reset The Current Modelview Matrix
    glTranslatef(0.0f, 0.0f, -3.0f);
   
    gluTessBeginPolygon(tess, NULL);    // <1>
    gluTessBeginContour(tess);          // <2>
    gluNextContour(tess, GLU_EXTERIOR); // <3>
    gluTessVertex(tess, ov[0], ov[0]);  // <4>
    gluTessVertex(tess, ov[1], ov[1]);  // <5>
    gluTessVertex(tess, ov[2], ov[2]);  // <6>
    gluTessVertex(tess, ov[3], ov[3]);  // <7>
    gluTessVertex(tess, ov[4], ov[4]);  // <8>
    gluTessVertex(tess, ov[5], ov[5]);  // <9>
    gluTessVertex(tess, ov[6], ov[6]);  // <10>
    gluTessVertex(tess, ov[7], ov[7]);  // <11>
    gluTessVertex(tess, ov[0], ov[0]);  // <12>
    gluTessEndContour(tess);            // <13>
    gluTessEndPolygon(tess);            // <14>
   
    return TRUE; // Everything Went OK
}

자, 노란색의 코드가 새롭게 추가된 코드이다. 새롭게 등장한 녀석들에 대해서 살펴보도록 하자.

<1>번 코드는 우리가 생성한 Tess객체의 인스턴스 포인터 변수를 이용해서 폴리곤을 그린다는 시작을 알리는 것이다.

<2>번 코드는 폴리곤의 윤곽의 한계를 지정한다는 의미이고 <13>코드와 쌍으로 사용된다.

<3>번 코드는 앞으로 지정될 폴리곤의 점들은 폴리곤의 가장자리의 점들임을 알리는 것이다.

<4>~<12>번 코드는 폴리곤의 점들을 지정하는 코드인데 이상하게도 2개의 점좌표값을 똑 같이 주었는데 첫번째 좌표값은 Tess가 사용하는 것이고 세번재 좌표는 우리가 앞서 지정한 CustomTessVertex 콜백 함수에 전달되는 좌표값이다.

<13>번은 <2>번과 쌍을 이루는 것으로써 윤곽의 한계 지정을 끝마친다는 것이다.

<14>번은 Tess의 폴리곤 생성을 끝마친다는 의미이다.

이제 실행을 해보자. 아래는 그 실행 결과이다.

사용자 삽입 이미지
어떤가? 독자가 생각했던 대로 실행이 되었는가? 이제 여기서 우리는 이 폴리곤의 가운데에 구멍을 뚤어보자. 즉 다음과 같은 모습을 얻기 위해서 말이다.

사용자 삽입 이미지

일단 가운데 구멍을 뚫기 위해서 사용할 점들의 좌표가 필요하다. 구멍이 사각형이므로 4개의 좌표값만 있으면 충분하다. 그 좌표값들을 정의하기 위해서 다시 전역변수 선언부에 다음과 같은 배열을 정의한다.

GLdouble iv[4][3] = {{0.0f, 0.2f, 0.0f},
   {0.2f, 0.0f, 0.0f},
   {0.0f, -0.2f, 0.0f},
   {-0.2f, 0.0f, 0.0f} };

이제 DrawGLScene에 어떤 코드가 또 새롭게 추가되었는지를 살펴보자.

int DrawGLScene(GLvoid) // Here's Where We Do All The Drawing
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear Screen And Depth Buffer
    glLoadIdentity(); // Reset The Current ModelviewMatrix
    glTranslatef(0.0f, 0.0f, -3.0f);
   
    gluTessBeginPolygon(tess, NULL);
    gluTessBeginContour(tess);
    gluNextContour(tess, GLU_EXTERIOR);
    gluTessVertex(tess, ov[0], ov[0]);
    gluTessVertex(tess, ov[1], ov[1]);
    gluTessVertex(tess, ov[2], ov[2]);
    gluTessVertex(tess, ov[3], ov[3]);
    gluTessVertex(tess, ov[4], ov[4]);
    gluTessVertex(tess, ov[5], ov[5]);
    gluTessVertex(tess, ov[6], ov[6]);
    gluTessVertex(tess, ov[7], ov[7]);
    gluTessVertex(tess, ov[0], ov[0]);
   
    gluNextContour(tess, GLU_INTERIOR); //<1>
    gluTessVertex(tess, iv[0], iv[0]); //<2>
    gluTessVertex(tess, iv[1], iv[1]); //<3>
    gluTessVertex(tess, iv[2], iv[2]); //<4>
    gluTessVertex(tess, iv[3], iv[3]); //<5>
    gluTessVertex(tess, iv[0], iv[0]); //<6>
    gluTessEndContour(tess);
    gluTessEndPolygon(tess);
   
    return TRUE; // Everything Went OK
}

<1>번을 주의 깊게 실펴보자. 두번째 인자로 GLU_INTERIOR이라고 되어있다. 바로 이것이 키포인트인데 <1>번 코드 이후로 지정되는 점들의 좌표는 모두 구멍을 뚫기 위한 폴리곤의 좌표로 인식되는 것이다.

<2>~<6>은 모두 구멍을 뚫기 위한 폴리곤의 좌표를 지정한다.

이제 실행해 보라. 그러면 우리가 원하는 결과를 얻을수있다.

자, 이제 마지막으로 필자가 소개할 것이 하나 남았다. 바로 그것은 지금까지는 폴리곤을 내부가 채워진 것으로 그려왔는데 이제는 그것이 아닌 내부가 칠해지지 않는, 즉 가장자리 선만을 그리고자 하는 것이다. 아마도 독자중에서는 그거야 쉽지 않은가할 사람도 있을 것이다. 바로 glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)로 해주면 되겠다 할것이다. 그렇다면 이 코드를 지금까지 우리가 만든(구멍 뚫린 폴리곤) 코드의 InitGL 함수에 추가해서 실행시켜 보자. 그 결과는 다음과 같다.


사용자 삽입 이미지


어떤가? 우리는 가장 자리의 선만을 원했지만 그 내부의 선들까지도 모두 그려져 버렸다. 이것은 우리가 원하는 결과가 아니다. 그렇다면 우리가 원하는 결과를 얻기 위해서는 어떻게 해야 하는가? 다행이도 Tess는 한 단줄의 코드로 우리에게 그 결과를 제공한다. gluTessProperty(tess, GLU_TESS_BOUNDARY_ONLY, true), 이 코드를 InitGL함수의 맨 아래부분에 추가한후 실행해 보라. 그 결과는 다음과 같다. (이제 더 이상 glPolygonMode 코드는 필요치 않다)


사용자 삽입 이미지


이제, 우리가 원하는 결과가 얻어졌다.

자 이로써 TESSELLATION에 대한 것에 대해 살펴보는 것을 마치도록 하겠다.