[OpenGL Tutorial] Selection

이 장에서는 OpenGL에서 우리가 어떤 물체들를 화면상에 그렸고 화면상의 물체들중에 하나의 물체를 마우스로 클릭했을때 어떤 물체가 클릭되었는지 즉, 어떤 물체가 픽킹(Picking)되었는지를 아는 방법에 대해서 알아본다.

OpenGL에서 사용자와의 대화를 위한 아주 강력한 방법이고 손쉬운 방법이다.

현제 필자는 이 Selection 기법에 대한 충분한 사용법만을 알고 있을뿐이며 그 내부적인 원리에 대해서는 이해하지 못하고 있다. 그러므로 원리에 대해 설명은 현재로써는 필자의 능력밖이며 나중에 능력이 닺는데로 이 장의 내용을 보완해 나갈 것이다.

바로 소스 코드로 들어가 보자. 이 장은 1장에서 얻은 소스 코드에서 시작한다.

먼저 화면상에 무엇을 그릴것인지 생각해보자. 다음의 화면이 우리가 그릴 최종적인 모양새이다.

사용자 삽입 이미지

위의 그림을 필자와 같이 살펴보자. 화면상에는 노란색, 파랑색, 시얀색, 초록색의 솔리드 구가 있으며 그 중앙에 빨간색 와이어 구가 있다. 그리고 각각의 구들을 보기 좋게 연결해 놓은 것처럼 밝은 파란색의 선들을 그려놓았다. 여기서 우리는 마우스로 각각의 다섯개의 구와 구들을 연결해 놓은듯한 선에 대해서 클릭할 경우 무엇이 클릭되었는지를 알려주도록 한다. 예를 들어서 마우스로 초록색 구를 클릭하면 화면상에 “Green Solid Sphere”라는 메세지가 나오도록 하고 각각의 구들을 연결해 놓은 선들을 클릭하면 단순하게 “Line”이라는 메세지를 출력해 보는 것이다. 이 모든 것을 위한 것은 OpenGL 내부에서 처리되므로 별다른 복잡한 연구가 필요치 않아 다행이다. 최근에 필자는 이 홈페이지의 방문자로부터 이 방법으로 선택되지 않는 물체가 있다는 글을 읽었다. 필자가 시험해 본 바로는 OpenGL에서 제공되는 모든 물체에 대해서 선택됨을 확인했다. 단 주의해야할 것은 Nurb인데 Selection을 위해 Nurbs의 GLU_AUTO_LOAD_MATRIX를 꺼야만 한다는 점이다.

자, 이제 우리가 그릴 것들과 선택할 것들에 대한 결정이 끝났으므로 이제 위의 모양새로 그려져주는 코드를 작성해보자. 다음은 우리에게 아주 익숙한 DrawGLScean 함수의 구현부이다.

int DrawGLScene(GLvoid)
{
    static GLfloat rot = 0.0f;
   
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glLoadIdentity();
   
    glInitNames(); // <1>
   
    glEnable(GL_LIGHTING);
   
    glPushName(100); // <2-1>
    glPushMatrix();
    glColor3f(1.0f, 0.0f, 0.0f);
    auxWireSphere(0.3);
    glPopMatrix();
    glPopName(); // <2-2>
   
    glPushName(101); // <3-1>
    glPushMatrix();
    glColor3f(0.0f, 1.0f, 0.0f);
    glTranslatef(-1.0f, -1.0f, 0.0f);
    auxSolidSphere(0.3f);
    glPopMatrix();
    glPopName(); // <3-2>
   
    glPushName(102); // <4-1>
    glPushMatrix(); 
    glColor3f(0.0f, 0.0f, 1.0f);
    glTranslatef(1.0f, 1.0f, 0.0f);
    auxSolidSphere(0.3f);
    glPopMatrix();
    glPopName(); // <4-2>
   
    glPushName(103); // <5-1>
    glPushMatrix();
    glColor3f(1.0f, 1.0f, 0.0f);
    glTranslatef(-1.0f, 1.0f, 0.0f);
    auxSolidSphere(0.3f);
    glPopMatrix();
    glPopName(); // <5-2>
   
    glPushName(104); // <6-1>
    glPushMatrix();
    glColor3f(0.0f, 1.0f, 1.0f);
    glTranslatef(1.0f, -1.0f, 0.0f);
    auxSolidSphere(0.3f);
    glPopMatrix();
    glPopName(); // <6-2>
   
    glDisable(GL_LIGHTING);
    glPushName(105); // <7-1>
    glColor3f(0.7f, 0.7f, 1.0f);
    glBegin(GL_LINES);
    glVertex2f(-1.0f, -1.0f);
    glVertex2f(1.0f, -1.0f);
   
    glVertex2f(1.0f, -1.0f);
    glVertex2f(1.0f, 1.0f);
  
    glVertex2f(1.0f, 1.0f);
    glVertex2f(-1.0f, 1.0f);
   
    glVertex2f(-1.0f, 1.0f);
    glVertex2f(-1.0f, -1.0f);
   
    glVertex2f(0.0f, 1.5f);
    glVertex2f(0.0f, -1.5f);
   
    glVertex2f(1.5f, 0.0f);
    glVertex2f(-1.5f, 0.0f);
   
    glVertex2f(1.5f, 1.5f);
    glVertex2f(-1.5f, -1.5f);
    glEnd();
    glPopName(); // <7-2>
   
    glColor3f(1.0f, 1.0f, 1.0f);
    glBegin(GL_LINES); // <8>
    glVertex2f(-1.5f, 1.5f);
    glVertex2f(1.5f, -1.5f);
    glEnd();
   
    return TRUE;
}

위의 코드중에 대부분은 이미 알고 있는 것들로 설명은 피하기로 한다. 중요한 코드만들 살펴보자.

<1> 번 코드는 Name Stack을 초기화하는 코드이다. Name Stack에 대해서 알아보기 전에 먼저 설명되어야할 것이 있다. 우리가 마우스를 이용해서 물체를 선택했을때 어떤 물체인지 아는 방법은 무엇인가? 그것은 간단하고 명확하게 그 물체에 이름을 지어주는 것이다. 물체의 이름은 간단이 숫자로 정해준다. Name Stack이란 바로 이 물체의 이름을 저장할 공간이다.

<2-1>번 코드인 glPushName(100)은 이 코드 이후로부터 <2-2>코드, glPopName() 이전까지 그려지는 모든 물체에 대해서 지정된 이름(여기는 100이다)을 붙이겠다는 의미이다. 즉, 빨간색 와이어 구에 대해서 100이라는 이름을 붙여주는 것이다.

<3-1>번 코드인 glPushName(101)은 이 코드 이후로부터 <3-2>코드, glPopName() 이전까지 그려지는 모든 물체에 대해서 지정된 이름(여기는 101이다) 을 붙이겠다는 의미이다. 즉, 초록색 솔리드 구에 대해서 101이라는 이름을 붙여주는 것이다.

<4>~<7>번 코드들은 모두 동일한 의미이므로 설명을 생략하고 주목해야 할것은 <8>번인데 이 코드에는 아무 이름도 붙여주지 않았다는 점을 기억해 두기 바란다. <8>번 코드는 하얀색 선을 그려주는데 나중에 이 하얀색 선을 클릭하였을때 어떤 일이 발생하겠는지 상상해 보기 바란다.

설명하지 못하고 넘어간게 있다. 바로 InitGL 함수의 초기화 부분이다. 간단하니 살펴보기 바란다. 추가된 부분은 따로 명시했다.

int InitGL(GLvoid)
{
    glShadeModel(GL_SMOOTH);
    glClearColor(0.0f, 0.0f, 0.0f, 0.5f);
    glClearDepth(1.0f);
    glEnable(GL_DEPTH_TEST);
    glEnable(GL_COLOR_MATERIAL);
    glEnable(GL_LIGHT0);
    glDepthFunc(GL_LEQUAL);
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
   
    return TRUE;
}

자 이렇게 해서 물체를 그려줬고 물체에 이름까지 붙여주는 것에 대해서 모두 끝마쳤다. 이제 마우스로 클릭한 점을 인자로 받아 그 위치에 어떤 물체가 있는지를 판별하는 함수를 작성해보자. 다음은 그 함수이다.

void SelectObjects(GLint x, GLint y)
{
    GLuint selectBuff[64];                                // <1>
    GLint hits, viewport[4];                              // <2>
   
    glSelectBuffer(64, selectBuff);                       // <3>
    glGetIntegerv(GL_VIEWPORT, viewport);                 // <4>
    glMatrixMode(GL_PROJECTION);                          // <5>
    glPushMatrix();                                       // <6>
    glRenderMode(GL_SELECT);                              // <7>
    glLoadIdentity();                                     // <8>
    gluPickMatrix(x, viewport[3]-y, 2, 2, viewport);      // <9>
    gluPerspective(45.0f,ratio,0.1f,100.0f);              // <10>
    glMatrixMode(GL_MODELVIEW);                           // <11>
    glLoadIdentity();                                     // <12>
    DrawGLScene();                                        // <13>
    hits = glRenderMode(GL_RENDER);                       // <14>
    if(hits>0) ProcessSelect(selectBuff);                 // <15>
    glMatrixMode(GL_PROJECTION);                          // <16>
    glPopMatrix();                                        // <17>
    glMatrixMode(GL_MODELVIEW);                           // <18>
}

참으로 설명할게 많다. 하나 하나 짚어보도록 하자.

<1>번 코드는 나중에 물체가 선택되면 그 선택된 물체의 이름이 바로 이 selectBuff에 저장되게 된다.

<2>번 코드에는 두개의 변수가 선언되어 있다. hits 변수는 마우스로 클릭해서 선택된 물체가 몇개나 되는지 하는것인데 만약 물체 두개가 겹쳐있을때 그 겹친 부분을 클릭했을시에 물체는 모두 2개가 선택되게 된다. 이때 hits의 값은 2가 될것이다. 그리고 viewport는 OpenGL을 초기화하는 코드에서 glViewPort라는 함수를 기억하는지 모르겠다. 그 함수에서 윈도우의 클라이언트 시작점과 크기를 명시해줌으로써 OpenGL이 사용하게 되는 영역을 알려주는데 그때 전해 주었던 값들을 다시 얻어와 저장해주는 변수이다. viewport[0]에는 윈도우의 클라이언트 영역의 원점의 x좌표인 0이 viewport[1]은 윈도우의 클라이언트 영역의 원점의 y좌표인 0이 담기며 viewport[2]는 클라이언트 영역의 너비값이 viewport[3]은 클라이언트 영역의 높이 값이 담긴다.

<3>번 코드는 Select Buffer로 사용될 버퍼의 크기와 그 버퍼로 사용될 메모리 영역을 잡아주는 것이다. glSelectBuffer의 첫번째 변수가 그 크기이고 두번째 변수가 그 메모리 영역이다. 여기서는 64개로 주어졌으며 <1>번 코드에서 정의한 selectBuffer를 사용한다. 64개를 선언했으므 우리는 동시에 총 16개(64/4)의 겹친 물체에 대해서도 판별할 수 있다. 나중에 보게 되겠지만 우리가 물체를 클릭하게 되면 처음 선택된 물체는 버퍼의 4번째 셀에 클릭된 물체의 이름을 저장하고 다 다음 물체는 버퍼의 8번째에 저장되며 또 그 다음은 12번째에 저장되게 된다. 이렇게 4의 배수로 저장될 버퍼의 셀의 위치가 증가된다. 그렇다면 그 외의 셀들에는 어떤 값들이 저장되는 것인가? 필자도 분명하게 알지 못하므로 여기서 소개하지 않겠다. 하지만 어떤 물체를 선택했느냐를 알아보는데는 중요치 않은 값들임에 틀임없는 것 같다.

<4>번 코드는 <2>번 코드에서 설명한 View Port의 영역값을 얻어오는 것이다.

<5>번 코드는 Projection Mode로 전환되는데 먼저 어떤 물체가 선택하게 되었는지를 알아보기 위해서는 먼저 물체를 선택을 위한 버퍼 영역에 다시금 모든 물체를 한번 더 Selection을 위한 버퍼에 그려줘야 하는 과정이 필요하다. 필자가 처음 이장을 쓸때 막막했던 것이 바로 이 부분이다. 어찌하야 모든 물체를 한번더 그려줘야 하는지, 그 내부적으로 어떻게 처리되는지, 이렇게 함으로써 프로그램의 수행능력이 반으로 줄어들지나 않을런지(같은 장면을 2번 그려줘야 하므로)와 같은 의구심과 불안감이 필자의 머릿속을 어지럽게 했다. 어찌되었든 <5>코드는 모든 물체를 다시금 그려주기 위해서 일단 Projection Mode 로 변환하고 Selection을 위한 버퍼의 투시법을 설정해주는 것이다.

<6>번 코드는 Projection Mode의 행렬값을 저장해 놓는 코드이다. 나중에 복원해야 하므로 필요하다.

<7>번 코드는 물체를 그려줄때, 즉 렌더링할때의 Render Mode를 Selection Buffer에 렌더링 하도록 지정하는 것이다.

<8>번 코드는 Projection Mode를 초기화(단위 행렬) 시켜주는 코드이다.

<9>번 코드는 내부적으로 Selection을 위한 Picking 행렬을 생성해 주는 코드인데 첫번째 인자는 마우스로 클릭한 곳의 x좌표이고 두번째는 y좌표인데 클라이언트 영역의 높이 값에서 y좌표값을 빼주었는데 이것은 OpenGL의 좌표체계가 y축은 아래로 갈수록 감소하는것에 기인한 것이라 짐작할수있는데 필자의 또 다른 생각은 OpenGL의 Bug로 보인다(OpenGL 1.0에서는 단지 y좌표값을 사용하지만 1.1~1.2부터는 이 방법이 적용되지 않고 클라이언트 영역의 높이 값에서 빼줘야만 하는 것으로 바뀌었다). 세번째와 네번째 값은 마우스로 클릭했을시에 꼭 그 좌표(x,y)만이 아닌 그 주변으로 얼마만큼의 위치에 있는 물체까지도 선택되도록 하는 여유분값이다. 즉 값을 2로 줌으로써 (x,y)위치로부터 1~2픽셀에 위치한 물체도 선택된 것으로 간주한다. 다섯번째 인자는 우리가 앞서 구한 viewport의 값이다.

<10>번 코드는 Projection Mode의 투영값을 설정하는 것인데 맨처음 OpenGL을 설정할때 사용했던 투영값과 동일한 값으로 설정해야 한다. 그래야 똑 같은 위치에 물체가 그려지기 때문이다. ratio 변수는 전역 변수로써 다음과 같이 선언되어 있다.

GLfloat ratio;

이 변수의 값의 설정은 기존에 있는 ReSizeGLScene 함수에서 해주는데 그 함수를 살펴보자.

GLvoid ReSizeGLScene(GLsizei width, GLsizei height)
{
    if (height==0) {
        height=1;
    }
   
    glViewport(0,0,width,height);
   
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
   
    ratio = (GLfloat)width/(GLfloat)height; // NEW
    gluPerspective(45.0f,ratio,0.1f,100.0f); // MODIFIED
   
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}

<11>번 코드는 이제 물체를 그리기 위에 Model View 모드로 전화하는 것이다.

<12>번 코드는 Model View를 단위벡터로 초기화한다.

<13>번 코드는 DrawGLScene 함수를 호출해서 한번 더 Selection Buffer에 그려준다.

<14>번 코드는 이제 물체를 Selection Buffer에 그리지 말고 일번적인 Render Buffer에 물체를 그리라는 것이다. 이 코드의 따 다른 중요한 것인 그 반환값에 있다. 몇개의 물체가 선택되었는지를 반환하기 때문에 이 값을 조사함으로써 물체가 선택되었는지를 않되었는지를 판별할수있다.

<15>번 코드가 hits 값을 조사하여 다시 ProcessSelect라는 새로 만든 함수를 통해서 선택된 물체를 조사하게 된다.

<16>번 코드와 <17>코드는 Projection Mode를 다시 원상태로 복귀하기 위한 것이다.

<18>번 코드는 다시 Model View 모드로 전환하는 코드이다.

참으로 짧은 코드이지만 이리 저리 빙빙 도는 정리가 않되는 코드라고 생각하는 독자가 있을지도 모르겠다. 필자 역시 처음에 그렇게 마찬가지였다. Selection의 내부 과정을 알수없기 때문인것 같다. 하지만 Selectiion 을 위한 코드는 위의 형태가 항상 반복적으로 되풀이 되므로 이해가 가지 않으면 그냥 이렇다 라고 그냥 사용해도 될 것같다.

이제 <15>번 코드에서 새롭게 선보였던 ProcessSelect 함수를 보도록 하자. 다음이 그 함수이다.

void ProcessSelect(GLuint index[64])
{
  switch(index[3]) {
    case 100: MessageBox(hWnd, "Red Wire Sphere", "Selection", MB_OK); break;
    case 101: MessageBox(hWnd, "Green Solid Sphere", "Selection", MB_OK); break;
    case 102: MessageBox(hWnd, "Blue Solid Sphere", "Selection", MB_OK); break;
    case 103: MessageBox(hWnd, "Yellow Solid Sphere", "Selection", MB_OK); break;
    case 104: MessageBox(hWnd, "Cyan Solid Sphere", "Selection", MB_OK); break;
    case 105: MessageBox(hWnd, "Line", "Selection", MB_OK); break;
   
    default: MessageBox(hWnd, "What?", "Selection", MB_OK); break;
  }
}

이 함수는 선택된 하나의 물체에 대해서만 검사하도록 되어져 있다. 왜냐하면 전달받은 index 배열에서 3번 셀만을 검사했기때문이다. 만약 선택된 두개 이상일 경우 4번째 셀, 8번째 셀, 12번째 셀, … 등의 셀에 그 선택된 물체의 이름이 저장된다고 설명한 적이 있다. 기억하는가? ProcessSelect 함수의 구성은 명확하고 쉽다.

거의 모든 것이 완성되었다. 이제 마우스가 클릭되는 이벤트가 발생할때 우리가 앞서 만든 SelectObject 함수를 사용하는 부분만을 만들면 끝이다.

WndProc 함수가 윈도우의 모든 메세지를 처리하는 함수인데 마우스의 왼쪽 버튼이 눌러지면 발생하는 메세지의 이름은 WM_LBUTTONDOWN이다. 다음과 같은 코드를 추가하자.

case WM_LBUTTONDOWN:
{
    SelectObjects(LOWORD(lParam), HIWORD(lParam));
    return 0;
}

lParam의 하위워드 값이 x좌표이고 lParam의 상위워드 값이 y좌표의 값인 것을 참고하기 바란다.

자, 이제 실행해보고 그 결과를 보라~!!! 모든 것이 이루어졌는가? (Do All come true?)

[OpenGL Tutorial] Fog

사용자 삽입 이미지이장에서는 OpenGL에서 안개 효과를 얻는 것에 대해서 알아보자. OpenGL은 안개 효과를 얻기 위해 특별한 트릭을 사용해야하는 것이 아니라 안개 효과를 위한 API를 제공함으로써 쉽게 안개 효과를 얻을 수 있다.

OpenGL에서 제공하는 안개의 종류는 3가지가 있는데 사실 2가지로 구분된다. 하나는 GL_EXP와 GL_EXP2이고 다른 하나는 GL_LINEAR이다. GL_EXP와 GL_EXP2는 밀도라는 값을 이용해서 화면 전체에 지정된 안개의 색으로 마치 자욱한 연기 안에 물체들이 놓여있는 듯한 효과를 낸다. GL_LINEAR은 안개가 이제 막 시작되는 깊이와 안개가 완전하게 들이워져서 더 이상 물체가 보이지 않을 깊이를 지정함으로써 사실 가장 실제적인 안개 효과를 연출할 수 있다. 이론 설명은 여기서 마치기로 하고 코딩에 들어가보자. 6장에서 만든 코드에서부터 시작해보자.

먼저 안개에 대해서 설정해야 할 값들이 있다. initGL 함수의 구현부에 다음과 같은 배열을 선언한다.

GLfloat fogColor[] = { 1.0f, 1.0f, 1.0f, 1.0f };

위의 배열은 안개의 색상을 지정하데 쓰인다. 안개에도 빨간 안개, 노란 안개, 파란 안개 등 찢어진 안개만 빼고 어떤 색의 안개가 가능하다. ^^;;

다음으로 initGL에서 수정해야할 것이 있는데 이것은 배경색을 다르게 지정하는 것이다. 우리가 지금까지 배경색을 완전한 검정색으로 사용했음을 알고 있는가? 즉 다음의 코드가 이와같은 일을 처리했었다.

glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

그러나 우리는 지금 안개를 보다 리얼하게 나타내기 위해서 배경색 조차도 안개의 색에 마춰야 한다. 다음과 같이 변경하자.

glClearColor(1.0f, 1.0f, 1.0f, 1.0f);

다음에 initGL에서 추가해야할 것은 안개에 대한 설정값들이다. 다음의 코드를 추가하자.

glFogi(GL_FOG_MODE, GL_LINEAR); // <1>
glFogfv(GL_FOG_COLOR, fogColor); // <2>
glFogf(GL_FOG_DENSITY, 0.3f); // <3>
glHint(GL_FOG_HINT, GL_NICEST); // <4>
glFogf(GL_FOG_START, 5.5f); // <5>
glFogf(GL_FOG_END, 7.0f); // <6>
glEnable(GL_FOG); // <7>

<1>번은 안개 모드를 GL_LINEAR로 해준다는 것이다. 앞에서 우리는 GL_LINEAR와 GL_EXP, GL_EXP2와 같은 안개의 종류가 있다는 것에 대해 알고 있다.

<2>번은 안개의 색을 지정하는 코드이다.

<3>번은 안개의 밀도를 지정하는 것인데 GL_EXP, GL_EXP2에서만 사용되는 값이다.

<4>번은 안개에 대한 연출에 있어서 가장 멋있게 보이도록 OpenGL에게 신경써 달라는 요청이다.

<5>번은 GL_LINEAR에서만 적용되는 것으로써 안개가 나타나기 시작하는 깊이(Z축 값) 거리를 설정하는 것이다.

<6>번은 GL_LINEAR에서만 적용되는 것으로써 안개가 완전이 들이워져서 물체가 더이상 보이지 않을 정도가 되는 깊이(Z축 값) 거리를 설정하는 것이다.

<7>번은 이렇게 설정한 값들을 이용해서 안개 효과를 사용하겠다는 것이다.

끝이다! ^^

이렇게 해주면 안개는 우리가 원하는 모습으로 나타날 것이다. 아래는 그 결과 화면이다.


사용자 삽입 이미지


어떤가? 차라리 안개라기 보다 눈속에 파뭍친 것 같지 않은가? ^^

안개 효과에 대해서 마치기 전에 이제 GL_EXP모드의 안개에 대해서 알아보자. GL_EXP 모드를 사용하기 위해서 변경해야 코드는 위에서 설펴본 코드중에 <1>번 코드의 두번재 인자를 GL_EXP로 변경하고 <3>번 코드에서 적절한 밀도값을 설정하기만 하면된다. 그렇게 설정했을때 나타나는 결과 화면은 다음과 같다.


사용자 삽입 이미지


마치 구름속에 파뭍힌 것 같다. 참고로 GL_EXP2는 GL_EXP보다 같은 밀도라고 해도 더 짙은 안개 효과를 나타낸다.

자!! 이렇게 해서 안개에 대한 것들을 마친다.

[OpenGL Tutorial] Sprite Processing by The Blending

이번장에서는 블랜딩의 또 다른 활용에 대해서 알아보겠는데 그 주제로 2차원 게임에서 많이 사용되는 스프라이트 처리 기법에 대한 예이다. 다음과 같은 그림이 준비되어있다. 첫째는 바탕화면 그림이고 둘째는 스프라이트가 될 이미지, 그리고 셋째는 스프라이트 이미지와 배경과의 조화를 위한 마스크 이미지이다.

사용자 삽입 이미지(배경 이미지)

사용자 삽입 이미지(스프라이트 이미지)

사용자 삽입 이미지(마스크 이미지)

위의 이미지가 섞여서 다음과 같은 스프라이트 효과를 얻는것이 이장의 목표이다.

사용자 삽입 이미지(최종 결과 화면)

실제로 우리는 OpenGL을 사용해서 위에서 주어진 스프라이트 이미지에서 위와 아래에 그려진 캐릭터의 두개의 동작을 주기적으로 반복해서 실제로 캐릭터가 움직이는 것 같은 모습으로 마무리를 짓겠다.


이장은 6장의 소스 코드에서 시작하도록 하겠다.


가장 먼저 해야할 것들은 배경 이미지와 스프라이트 이미지와 마스크 이미지를 읽어들어야 하는데 이것은 텍스쳐 맵핑 소스의 형태로 읽어들어야 한다. 먼어 이 세가지의 이미지를 읽어들이는 코드에 대해서 살펴보자. 이미지를 읽어들이는 코드는 InitGL 함수에서 해준다. InitGL 함수의 구현 부분을 완전이 새롭게 기술한다. 그 코드는 아래와 같다.

int InitGL(GLvoid)
{
    AUX_RGBImageRec *texRec[3];
    memset(texRec, 0, sizeof(AUX_RGBImageRec *));
    if((texRec[0]=LoadBMPFile("Image/back.bmp")) &&
       (texRec[1]=LoadBMPFile("Image/sprite.bmp")) &&
       (texRec[2]=LoadBMPFile("Image/mask.bmp"))) {
        glGenTextures(3, &texture[0]);
        for(int i=0; i<3; i++) {
          glBindTexture(GL_TEXTURE_2D, texture[i]);
          glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);//<*>
          glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);//<*>
          gluBuild2DMipmaps(GL_TEXTURE_2D, 3, texRec[i]->sizeX, texRec[i]->sizeY,
                GL_RGB, GL_UNSIGNED_BYTE, texRec[i]->data);
        }
    } else return FALSE;
   
    for(int i=0; i<3; i++) {
        if(texRec[i]) {
            if(texRec[i]->data) free(texRec[i]->data);
            free(texRec[i]);
        } else return FALSE;
    }
   
    glEnable(GL_TEXTURE_2D);
    glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
    glShadeModel(GL_SMOOTH);
    glClearColor(0.0f, 0.0f, 0.0f, 0.5f);
    glClearDepth(1.0f);
    glDepthFunc(GL_LEQUAL);
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
    return TRUE;
}

6장의 소스 코드에 크게 달라진 것은 없다. 바뀐 중요한 것은 맵핑 소스로 사용될 그림 파일이 3개로 늘어났다. 하지만 눈에 잘 보이지 않는 중요한 중요한 변화가 있는데 위 코드의 노란색 코드가 바로 그것이다. glTexParameteri의 세번째 인자가 GL_NEAREST로 변경되었다. GL_LINEAR로 사용할 경우 그림이 나쁘게 표현하면 누그러져(?)버리게 되는데 이점을 막기위함이다.

세개의 텍스쳐 맵핑 소스를 사용하는데 텍스쳐 맵핑 소스의 식별자로 사용되는 전역변수로 선언된 GLuint tex[3] 코드의 변수이름을 texture로 변경하기 바란다. 즉 다음과 같게 말이다.

GLuint texture[3];

자 이제 텍스쳐 맵핑 소스를 얻는 것은 이쯤에서 됬고 이제 실제로 스프라이트 효과를 나타내보자. 먼저 원리에 대해서 설명해 보겠다. 먼저 사각형의 폴리곤을 이용해서 그 사각형의 폴리곤에 배경 이미지로 텍스쳐 맵핑을 한다. 이렇게 하면 배경은 간단이 완성된다. 그리고 작은 사각형의 폴리곤을 이용해서 마스크 이미지로 텍스쳐 맵핑을 시키는데 이때 블랜딩 함수로 (GL_DST_COLOR, GL_ZERO)를 사용한다. 그리고 바로 다음에 또 다른 작은 사각형의 폴리곤을 이용해서 스프라이트 이미지로 텍스쳐 맵핑을 시키는데 이때의 블랜딩 함수로 (GL_ONE, GL_ONE)을 사용한다. 이때 두개의 작은 사각형의 폴리곤은 모두 똑 같은 크기이며 동일한 좌표에 위치해야만 한다. 이렇게 되면 스프라이트 효과를 간단이 얻을 수 있게 된다. 아래는 그 구현 코드이다.

int DrawGLScene(GLvoid)
{
    static GLuint frame = 0; // <1>
    static GLfloat x = -5.0f; // <2>
   
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glLoadIdentity();
   
    glTranslatef(0.0f, 0.0f, -10.0f);
   
    glEnable(GL_DEPTH_TEST); // <3>
    glDisable(GL_BLEND); // <4>
    glBindTexture(GL_TEXTURE_2D, texture[0]); // <5>
    glBegin(GL_QUADS); // <6-1>
    glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.5f, -1.5f, 0.0f); // <6-2>
    glTexCoord2f(1.0f, 0.0f); glVertex3f(1.5f, -1.5f, 0.0f); // <6-3>
    glTexCoord2f(1.0f, 1.0f); glVertex3f(1.5f, 1.5f, 0.0f); // <6-4>
    glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.5f, 1.5f, 0.0f); // <6-5>
    glEnd();
   
    glDisable(GL_DEPTH_TEST); // <7>
    glEnable(GL_BLEND); // <8>
    glBlendFunc(GL_DST_COLOR, GL_ZERO); // <9>
    glBindTexture(GL_TEXTURE_2D, texture[2]); // <10>
    glBegin(GL_QUADS); // <11-1>
    glTexCoord2f(0.0f, frame*0.5f); glVertex3f(-.5f+x, -.5f, 0.0f); // <11-2>
    glTexCoord2f(1.0f, frame*0.5f); glVertex3f(.5f+x, -.5f, 0.0f); // <11-3>
    glTexCoord2f(1.0f, frame*0.5f+0.5f); glVertex3f(.5f+x, .5f, 0.0f); // <11-4>
    glTexCoord2f(0.0f, frame*0.5f+0.5f); glVertex3f(-.5f+x, .5f, 0.0f); // <11-5>
    glEnd();
   
    glBlendFunc(GL_ONE, GL_ONE); // <12>
    glBindTexture(GL_TEXTURE_2D, texture[1]); // <13>
    glBegin(GL_QUADS); // <14-1>
    glTexCoord2f(0.0f, frame*0.5f); glVertex3f(-.5f+x, -.5f, 0.0f); // <14-2>
    glTexCoord2f(1.0f, frame*0.5f); glVertex3f(.5f+x, -.5f, 0.0f); // <14-3>
    glTexCoord2f(1.0f, frame*0.5f+0.5f); glVertex3f(.5f+x, .5f, 0.0f); // <14-4>
    glTexCoord2f(0.0f, frame*0.5f+0.5f); glVertex3f(-.5f+x, .5f, 0.0f); // <14-5>
    glEnd();
   
    if(frame == 0) frame = 1; // <15-1>
    else frame = 0; // <15-2>
   
    Sleep(50); // <15-3>
   
    x += 0.025f; // <15-4>
    if(x>6.0f) x=-5.0f; // <15-5>
   
    return TRUE;
}

코드 하나하나 짚어 보며 살펴보자.

<1> 번 코드를 살펴보기에 앞서 스프라이트 이미지에 대해서 다시 보자. 이 이미지는 두개의 캐릭터 모습을 담고 있다. 즉 위쪽과 아래쪽에 각각 모습을 담고 있는데 바로 <1>번 코드의 frame이 위쪽과 아래쪽 모습중에 어떤 모습을 보여 줄것인지를 나타내게 된다. 즉 frame가 0이면 아랫쪽 모습을 1이면 위쪽 모습을 보여주게 된다.

<2> 번 코드는 캐릭터가 앞으로 움직이는데 그때 사용되는 좌표 변수이다.

<3>, <4> 번 코드는 배경 이미지를 그리기에 앞서 Depth Buffer를 사용하게 하고 블랜딩 기능을 사용하지 않도록 한다. 배경이미지를 그릴때는 블랜딩 기능을 사용해서는 않된다. 배경은 배경 자체로 그대로 그려져야 하기때문이다.

<5> 번 코드는 배경을 그리기 위해 배경 텍스쳐 맵핑 소스를 사용하도록 지시한다.

<6> 번 코드들은 실제로 사각형 폴리곤을 그리고 배경 텍스쳐 맵핑을 사용해서 배경 그림을 화면상에 그려준다.

<7> 번 코드 부터는 드디어 스프라이트를 그려주는 코드의 시작인데 먼저 Depth Buffer의 사용을 막는다. 이 프로그램에서는 굳이 필요치 않으나 일반적으로 블랜딩 기능을 사용할때는 블랜딩 함수에 적용에 방해를 받지 않도록 Depth Buffer를 사용하지 않는다.

<8> 번 코드는 블랜딩 기능을 활성 시킨다.

<9> 번 코드는 블랜딩 함수를 지정하게 된는데 마스크 이미지에 대한 블랜딩 처리에 대한 함수는 반드시 (GL_DST_COLOR, GL_ZERO)이여야만 한다.

<10> 번 코드는 마스크 이미지의 텍스쳐 맵핑 소스를 사용하도록 지시한다.

<11> 번 코드들은 작은 사각형에 마스크 이미지의 텍스쳐 맵핑을 해주는 코드이다. frame 변수와 x변수의 사용을 눈여겨 보기 바란다. frame 변수를 사용해서 텍스쳐 좌표의 각각 정확히 위, 아래의 반만을 취한다는 것을 알수있다. 여기까지 코드가 도달하면 아래와 같은 결과까지 얻게 된다.

사용자 삽입 이미지

이제 위의 그림 위에 스프라이트 이미지를 올려 놓기만 하면 되는데 <12> 번 코드 이후가 바로 그런 일을 하게 된다.

<12> 번 코드는 스프라이트 이미지를 위한 블랜딩 함수를 지정하는데 (GL_ONE, GL_ONE)여야만 한다.

<13> 번 코드는 스프라이트 이미지의 텍스쳐 맵핑 소스를 사용하도록 지시한다.

<14> 번 코드들은 역시 <11>번 코드들과 동일한 일을 한다.

<15-1>과 <15-2>코드는 스프라이트 이미지에 담긴 두개의 동작을 서로 반복해서 보여주기 위해서 frame 변수를 항상 0이나 1의 값을 반복적으로 갖도록 해주는 코드이다.

<15-3>은 화면의 갱신이 너무 빨라서 0.2초간 지연을 시켜주는 임시적으로 사용한 함수이다.

<15-4>와 <15-5>의 코드는 스프라이트를 앞으로 움직이게 해주는 x변수를 증가시키는 코드들이다.

이상으로 이렇게 하면 실제 스프라이트의 최종적인 결과를 얻을 수 있다. 간단하지 않은가?? 이제 OpenGL을 이용해서 2차원 게임도 만들수 있다는 느낌을 받을수도 있을 것이다. 하지만 OpenGL을 이용해서 스프라이트를 구현할 경우 많은 장점이 있는데 그것은 스프라이트를 원하는 크기로 쉽게 키우거나 줄일수있다는 것이고 또한 원하는 각도로 회전이 가능하다는 점이다. 게다가 OpenGL의 하드웨어 가속 기능을 지원받을 경우 상상을 초월할 정도의 속도를 얻을 수 있을지도 모르겠다.

[OpenGL Tutorial] Transparent by The Blending

사용자 삽입 이미지이 장은 OpenGL의 Blend 기능을 이용하여 투명한 물체를 만들어 보는 것을 예로써 Blend를 설명하고 한다.

OpenGL에서 대부분의 특수효과는 Blending를 이용한다. 블랜딩은 이미 화면상에 그려진 픽셀의 색과 이제 바로 같은 위치에 그려질 픽셀의 색의 조합하는 방식이다. 어떤식으로 색상을 조합하는 지는 알파값과 블랜딩 함수에 의해 정해진다. 알파값이란 보통 색상을 지정할때 4가지 구성요소중 마지막 네번째 값이다. 지금까지 색상을 지정하는 방식으로 GL_RGB를 사용했었는데 여기에는 알파값이 없다. 알파값의 추가를 위해 GL_RGBA를 사용할수있다. 그리고 알파값을 포함한 색상을 지정하기 위해 glColor3f 대신 glColor4f를 사용할 수 있다.

대부분의 사람들은 알파값을 물체의 불투명 정도라고 생각한다. 알파값 0.0은 완전한 투명이고 1.0은 완전이 불투명하다.

블랜딩 공식
(Rs Sr + Rd Dr , Gs Sg + Gd Dg , Bs Sb + Bd Db , As Sa + Ad Da)

OpenGL은 두픽셀간의 블랜딩 결과를 계산하기 위해 위의 공식을 이용한다. s와 d의 꼬리 글자는 원본(Source)와 대상(Destination) 픽셀을 나타낸다. S와 D 요소는 블랜딩 요소이다. 이러한 값들이 어떤 방식으로 블랜딩할것인지를 지정한다. 또한 r, g, b, a의 첨자는 색의 3요소(빨강, 초록, 파랑)과 알파값이다. S와 D의 일반적인 값으로는 S에 대해서는 (As, As, As, As) (줄여 말하면, 원본 알파값)이며 D에 대해서는 (1, 1, 1, 1) – (As, As, As, As) (줄여 말하면, 1 – 원본 알파값)이다. 이것은 다음과 같은 블랜딩 공식을 만들어 낸다.

( Rs As + Rd (1 – As), Gs As + Gd (1 – As), Bs As + Bs (1 – As), As As + Ad (1 – As) )

이 공식은 투명/반투명 효과를 낼수 있다.

우리는 다른 것들과 마찬가지로 블랜딩을 사용할 수 있다. 물체를 투명하게 그릴때는 Depth Buffer 사용을 막는다. 어떤 물체의 앞에 투명한 물체를 그릴때 투명한 물체를 통해서 그 뒤의 그 물체가 보여야하기 때문이다.

자 이제 블랜딩에 대한 수학적, 개론적인 설명을 접고 실제로 프로그래밍 예를 통해 블랜딩을 접해보자. 사용할 소스는 6장에서 만든 코드를 이용하기로 한다.

우리가 원하는 결과는 투명한 정육면체(?), 바로 이것이다. 실제 결과를 미리 보인다. 아래를 보라.

사용자 삽입 이미지

6장에서 보았던 것과 같은 내용인데 차이점은 정육면체의 면이 투명해서 반대쪽 면까지도 보인다는 것이다. 텍스쳐 맵핑 소스를 보다 그럴싸한 것으로 변경해서 다시 실행해 보면 또 다른 멋진 결과가 나온다. 직접 해보기 바란다.

이제 코드에 대해서 알아보자.

먼저 블랜딩을 위해서 초기화 시켜줘야 할 것들에 대해서 알아보자. 우리가 잘알고 있듯이 초기화는 InitGL 함수에서 해준다. 기존의 소스 코드에서 삭제되고 추가거나 변경될 코드를 설명하자면 먼저 glEnable(GL_CULL_FACE)를 삭제한다. 왜냐하면 이 코드는 면의 앞면에 대해서만 그리고 뒷면을 그리지 않게 하는 코드인데 그렇게 하면 투명한 면을 통해서 그 뒷면이 보이지 않기 때문이다. 그리고 glEnable(GL_DEPTH_TEST)를 glDisable(GL_DEPTH_TEST)로 변경한다. 이유는 앞서 설명했던 바와 같다. 그리고 물체의 재질에 대해 설정해 주는 모든 코드를 삭제한다. 사실 텍스쳐 맵핑을 입힌 물체는 더 이상 재질이 필요없을 뿐더러 블랜딩 효과에서는 재질의 성질이 블랜딩 효과를 방해함으로써 둔탁한 느낌의 결과를 얻을수밖에 없다. 재질에 관계되는 코드들을 제거한다. 제거할 코드는 다음 InitGL 함수의 전체 구현 소스를 보이겠다. 그리고 가장 중요한 블랜딩 기능을 사용하는 것을 지정하고 블랜딩 함수를 지정하는 코드이다.그 두 코드는 다음과 같다.

glBlendFunc(GL_SRC_ALPHA, GL_ONE);
glEnable(GL_BLEND);

첫번째는 블랜딩 함수를 지정해 주는 코드이다. 첫번째 인자인 GL_SRC_ALPHA는 원본 픽셀에 대한 블랜딩 계수를 계산하는 방식인데 원본칼라를 원본 알파값으로 곱하는 것이다. 그리고 두번째 인자는 대상 픽셀에 대한 블랜딩 계수를 계산하는 방식인데 그냥 대상 칼라를 사용한다는 것이다. 즉 블랜딩 계수는 1이 되겠다. 최종적으로 이렇게 처리된 두 픽셀값이 합해져서 최종 픽셀값으로 처리되어 화면상에 나타나게 된다.


여기까지가 블랜딩을 이용한 투명한 물체를 생성하는 초기화 코드이다. 아래에 전체 코드를 기록하니 참고바란다. 노랜색 부분이 변경된 부분이다.

int InitGL(GLvoid)
{
    GLfloat ambientLight[] = { 0.25f, 0.25f, 0.25f, 1.0f };
    GLfloat diffuseLight[] = { 0.9f, 0.9f, 0.9f, 1.0f };
    GLfloat lightPos[] = { -100.0f, 130.0f, 150.0f, 1.0f };
    GLfloat specular[] = { 1.0f, 1.0f, 1.0f, 1.0f };
   
    AUX_RGBImageRec *texRec[3];
    memset(texRec, 0, sizeof(void *)*3);
   
    if((texRec[0]=LoadBMPFile("img.bmp")) &&
       (texRec[1]=LoadBMPFile("img2.bmp")) &&
       (texRec[2]=LoadBMPFile("img3.bmp"))) {
        glGenTextures(3, &tex[0]);
        for(int i=0; i<3; i++) {
            glBindTexture(GL_TEXTURE_2D, tex[i]);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
            glTexImage2D(GL_TEXTURE_2D, 
            0, 
            3, 
            texRec[i]->sizeX, 
            texRec[i]->sizeY, 
            0, 
            GL_RGB, 
            GL_UNSIGNED_BYTE, 
            texRec[i]->data);
        }
    } else return FALSE;
   
    for(int i=0; i<3; i++) {
        if(texRec[i])
        {
            if(texRec[i]->data) free(texRec[i]->data);
            free(texRec[i]);
        } else return FALSE;
    }
   
    glEnable(GL_TEXTURE_2D);
    glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
   
    glShadeModel(GL_SMOOTH);
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    glClearDepth(1.0f);
    // glEnable(GL_CULL_FACE); //
    glFrontFace(GL_CCW);
    glEnable(GL_LIGHTING);
   
    /////////// NEW ///////////////////////////////
    glBlendFunc(GL_SRC_ALPHA, GL_ONE);
    glEnable(GL_BLEND);
    /////////// NEW ///////////////////////////////
    glDisable(GL_DEPTH_TEST); //
   
    glLightfv(GL_LIGHT0, GL_AMBIENT, ambientLight);
    glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuseLight);
    glLightfv(GL_LIGHT0, GL_POSITION, lightPos);
    glLightfv(GL_LIGHT0, GL_SPECULAR, specular);
    glEnable(GL_LIGHT0);
   
    // glEnable(GL_COLOR_MATERIAL); // 
    // glColorMaterial(GL_FRONT, GL_AMBIENT_AND_DIFFUSE); // 
    // glMaterialfv(GL_FRONT, GL_SPECULAR, specref); // 
    // glMateriali(GL_FRONT, GL_SHININESS, 10); // 
   
    glDepthFunc(GL_LEQUAL);
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
   
    return TRUE;
}

자 이제 실제 물체를 그려주는 코드를 살펴보자. 변경된 부분은 단 한줄이다. 즉 색상을 지정할때 알파값을 추가하는 것이다. 아래는 glDrawScene 함수의 일부분이다.

int DrawGLScene(GLvoid)
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glLoadIdentity();
    glTranslatef(0.0f, 0.0f, -7.0f);
   
    glColor4f(1.0f, 1.0f, 1.0f, 0.2f); // 
   
    glRotatef(rot, 1.0f, 0.1f, 0.4f);
    glBindTexture(GL_TEXTURE_2D, tex[0]);
    glBegin(GL_QUADS);
    glNormal3f(0.0f, 0.0f, 1.0f);
    glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, 1.0f, 1.0f);
    glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, -1.0f, 1.0f);
    glTexCoord2f(1.0f, 1.0f); glVertex3f(1.0f, -1.0f, 1.0f);
   
        .
        .
        .

위의 노란색 코드가 유일한 변경 코드인데 알파값으로 0.2를 주었다. 이것은 투명도가 80%를 나타내는 수치이다.

이것으로 블랜딩을 이용한 투명한 객체를 만들어 보는 것을 마친다. 재미 삼아 다른 텍스쳐 맵핑 소스에 대한 그림 파일만을 바꿔서 실행본 결과를 아래에 제시한다. 보기 바란다.
사용자 삽입 이미지