[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%를 나타내는 수치이다.

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

[OpenGL Tutorial] Using the Quadrics

사용자 삽입 이미지Quadrics가 무엇인지 부터 알아야 하겠다. Quadrics은 공간상에 기하학적인 모양을 생성하는 것이다. 여기서 말하는 기하학적 모양이란 원뿔, 원통, 원판, 구 등을 말한다. 즉 Quadrics를 사용하여 쉽고 간단하게 위에서 언급한 물체를 생성할 수 있다. 그렇다면 이제 Quadrics를 사용하여 물체를 만들고 시연해보자 !!

먼저 Quadrics를 이용하여 물체를 만들기 전에 Quadrics를 생성하여야 한다. Quadrics는 속성을 가지고 있는데, 그 속성이란 그리고자 하는 것, 그리고자 하는 것의 위치(원점)과 값(반지름 등등), 광원 모드, 텍스쳐 모드, 그리고 그리는 도중에 에러가 발생할 경우 호출할 CallBack 함수들을 말한다. Quadrics를 생성하는 방법은 쉽다. 제공되는 Factory(생성) 함수가 있으니 말이다.

GLUquadricObj *obj;
obj = gluNewQuadric();

생성은 위의 코드가 전부이다. 간단하다. 나중에 생성된 Quadric객체의 인스턴스 변수인 obj를 이용하여 물체를 그리는데 사용되는 함수는 gluCylinder, gluDisk, gluSphere가 있고 미래에 더 추가될 것이다. 이 함수들은 첫번째 인자로 Quadric 객체를 요구하고 각 함수에 따라 실린더 모양을 그릴것인지 디스크 모양을 그릴것인지 구 모양을 그릴 것인지를 지정하는 것이다. 그렇다면 정작 그릴 물체에 대한 속성은 어떻게 설정하는 것인가? 예를 들어서 물체를 Wire Frame 형태로 그릴것인지 Solid 형태로 그릴것인지의 결정은 어떻게 하는가? 또한 Solid 형태로 그린다면 물체의 면을 Flat형태로 그릴것인지 Smooth하게 그릴것인지의 결정은 어떻게 하는가? 법선벡터의 방향을 바깓쪽으로 할것인지 안쪽으로 할것인지의 결정은 어떻게 하는가? 그리고 나중에 Quadric로 생성한 객체에 대해 텍스쳐 맵핑을 할경우 텍스쳐 맵 좌표를 자동으로 생성시키게 할것인가 아니면 않할것인가의 지정은 어떻게 하는가? 이 많은 질문에 대한 답은 아래와 같다.

일단 Quadric 객체가 생성되면 다음의 함수를 통해서 그 속성을 설정할수있다.

1. void gluQuadricDrawStyle(GLUquadricObj *obj, GLenum drawStyle)

이 함수의 첫번째인자는 속성을 설정할 Quadric에 대한 인스턴스 변수에 대한 포인터가 오며 두번째 변수에 올수있는 값은 GLU_FILL, GLU_LINE, GLU_SILHOUETTE, GLU_POINT가 온다. Wire Frame 형태를 원한다면 GLU_LINE가 필요하고 Soild 형태를 원한다면 GUL_FILL이 그리고 단지 점만으로 그리기를 원한다면 GLU_POINT가 온다. GLU_SILHOUETTE는 선으로 외부 모서리만을 그리게 된다.

2. void gluQuadricNormals(GLUquadricObj *obj, GLenum normals)

위의 함수는 법선벡터를 제어하는데 사용되는데 법선 벡터에의해서 빛에 대한 영향이 결정된다는 것은 지난 “빛”에 대한 장에서 충분히 설명했다. 이 함수의 첫번째 인자는 역시 Quadric의 인스턴스 변수에 대한 포인터를 갖고 두번째 인자에 올수있는 인자의 값에는 GLU_NONE, GLU_FLAT, GLU_SMOOTH가 온다. GLU_NONE는 법선벡터를 생성하지 않는데 이렇게 되면 빛에 대해서 아무런 효과를 얻을 수 없다. GLU_FLAT는 면이 깍인것처럼 보이도록 법선벡터를 생성하고 GL_SMOOTH는 면이 부드럽게 보이도록 즉, 물체의 모서리를 부드럽게 보이게 법선 벡터를 생성한다.

3. void gluQuadricOrientation(GLUquadricObj *obj, GLenum orientation)

이 함수는 법선벡터의 방향을 지정하기 위해 사용된다. 첫번째 인자는 Quadric의 인스턴스 변수에 대한 포인터를 갖고 두번째 인자가 갖을 수 있는 값에는 GLU_OUTSIDE와 GLU_INSIDE가 온다. GLU_OUTSIDE는 법선벡터를 물체에 대해서 바깓쪽으로 법선벡터의 방향을 지정하고 GLU_INSIDE는 법선벡터를 물체에 대해서 안쪽으로 방향을 지정하도록 생성한다.

4. void gluQuadricTexture(GLUquadricObj *obj, GLboolean textureCoords)

이 함수는 텍스쳐 맵핑 좌표를 생성할 것인지 않할 것인지를 지정하는 것이다. 첫번째 인자는 Quadric의 인스턴스 변수에 대한 포인터를 갖고 두번째 인자가 갖을 수 있는 값에는 GL_TRUE와 GL_FALSE이다. GL_TRUE로 할경우 텍스쳐 맵핑 좌표를 생성하며 GL_FALSE는 텍스쳐 맵핑 좌표를 생성하지 않는다.

자 이렇게 해서 Quadric에 대한 속성을 설정하는 함수에 대한 소개를 마치고 이제 본격적으로 우리가 필요로하는 물체를 그려보는 것에 대해서 알아보자!!

사용할 기본 소스는 1장의 소스에서 시작한다.

먼저 Quadric 객체를 생성하기 위한 변수를 하나 선언하자. 코드 전체에서 이 변수를 참조하므로 전역 변수를 선언하는 곳에 선언한다.

GLUquadricObj *obj;

이제 Quadric를 생성해야 한다. OpenGL을 초기화 시키는 부분인 InitGL 함수부에서 끝부분쯤에 다음의 코드를 추가한다.

obj = gluNewQuadric(); // <1>
gluQuadricDrawStyle(obj, GLU_FILL); // <2>
gluQuadricNormals(obj, GLU_SMOOTH); // <3>
gluQuadricOrientation(obj, GLU_OUTSIDE); // <4>
gluQuadricTexture(obj, GL_FALSE); // <5>
   
glEnable(GL_LIGHTING); // <6>
glEnable(GL_LIGHT0); // <7>
glEnable(GL_COLOR_MATERIAL); // <8>

<1>번 코드는 실제적으로 Quadric 객체를 생성시키는 코드이다.

<2>번 코드는 Solid 형태로 물체를 그린다는 것이고 <3>번은 면에 대해서 부드럽게 그린다는 것이고 <4>번은 법선벡터의 방향을 물체에 대해서 바깓쪽으로 향하도록 법선벡터를 생성하라는 의미이다. <5>번은 텍스쳐 맵을 적용하지 않을 것으므로 텍스쳐 맵핑 좌표를 생성하지 말라는 의미이다.

<5>~<8>번은 빛을 활성화시키고 칼라 추적 기능을 이용해서 물체의 재질을 설정하라고 지시한다. 참고로 LIGHT0는 별도로 설정하지 않아도 기본적으로 그래픽 카드에서 값이 설정되어 있는 빛이다. 필자의 그래픽 카드(G400)에서는 휼룡하게 작동했다.

자! 이렇게 해서 Quadric 객체를 생성했고 그 속성을 설정했으며 나머지 빛과 재질에 대한 설정 또한 끝냈다.

그렇다면 무언가를 생성했으니 나중에 필요치 않으면 메모리에서 제거해야 하는 것은 프로그래머에 대한 불문율이다. Quadric 객체를 제거하는 방법은 무언인가? 실제적으로 제거하는 코드의 위치는 WinMain 함수의 끝부분쯤에 위치한다. 아래와 같다.

            .
            .
            .
   
    gluDeleteQuadric(obj); // 추가 코드
   
    // Shutdown
    KillGLWindow(); // Kill The Window
     return (msg.wParam); // Exit The Program
}

이제 실제적으로 Quadirc객체를 이용하여 물체를 그리는 작업만 남았다. 그리는 작업은 DrawGLScene 함수에 위치한다. 다음은 그 함수에 대한 코드 전체이다.

int DrawGLScene(GLvoid)
{
    static GLfloat rot = 0.0f;
   
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glLoadIdentity();
    glTranslatef(0.0f, 0.0f, -15.0f);
   
    glPushMatrix();
    glTranslatef(-5.0f, 0.0f, 0.0f);
    glRotatef(rot, 1.0f, 1.0f, 1.0f);
    glTranslatef(0.0f, 0.0f, -2.0f);
    glColor3f(0.6f, 0.6f, 1.0f);
    gluCylinder(obj, 1.0f, 1.0f, 4.0f, 16, 4); //<1>
    glPopMatrix();
   
    glPushMatrix();
    glTranslatef(0.0f, 0.0f, 0.0f);
    glRotatef(rot, 1.0f, 1.0f, 1.0f);
    glColor3f(0.6f, 0.6f, 1.0f);
    glColor3f(1.0f, 0.6f, 0.6f);
    gluDisk(obj, 0.2f, 1.6f, 16, 4); //<2>
    glPopMatrix();
   
    glPushMatrix();
    glTranslatef(5.0f, 0.0f, 0.0f);
    glRotatef(rot, 1.0f, 1.0f, 1.0f);
    glColor3f(0.6f, 0.6f, 1.0f);
    glColor3f(0.6f, 1.0f, 0.6f);
    gluSphere(obj, 1.5f, 24, 24); //<3>
    glPopMatrix();
   
    rot += 1.0f;
    if(rot > 359.0f) rot = 1.0f;
   
    return TRUE;
}

새롭게 추가된 코드는 노란색으로 나타냈다. 실제 중요한 Quadric 객체를 그리는 코드에 대해서만 설명하도록 한다. 나머지 부분은 지난장에서 이미 다루었던 내용이므로 해당 장들을 참고하기 바란다.

<1>번 코드에서는 gluCylinder 함수를 사용했는데 첫번째 인자는 Quadric의 인스턴스 변수에 대한 포인터가 온다. 두번째 인자는 실린더의 밑부분의 반지름 크기이고 세번째는 실린더의 윗부분의 반지름 크기이다. 네번째는 실린더의 길이를 지정하는 것이고 다섯번째는 Slice의 수를 여섯번째는 Stack의 수를 나타낸다. Slice를 높일수록 실린더의 모양이 보다 부드러워진다. WireFrame 모드로 설정해서 그려본다면 분명하게 그 내용을 파악할수있다. 실린더의 윗부분과 아랫부분은 뚫려있다.

<2>번 코드는 gluDisk를 사용하는데 첫번째 인자는 Quadric의 인스턴스 변수에 대한 포인터가 온다. 두번째는 디스크의 안쪽 뚤린 원에 대한 반지름이고 세번째는 디스크의 바깓쪽에 대한 반지름이다. 네번째는 Slice의 갯수이고 다섯번째는 loop의 갯수이다.

<3>번 코드는 gluSphere인데 첫번째 인자는 Quadric의 인스턴스 변수에 대한 포인터가 온다. 두번째는 구의 반지름값이고 세번째는 slice의 갯수이고 네번째는 stacks의 갯수가 온다.

자, 이제 실행하는 것만 남았다. 아래는 그 실행 결과이다

사용자 삽입 이미지

[OpenGL Tutorial] Output String on the Screen(Outline)

사용자 삽입 이미지지난장에서는 비트맵으로 문자를 출력했었다. 이번에는 문자를 객체로 처리하는 방법인 Outline Font에 대해서 알아보도록 한다. 이 장의 시작은 7장의 소스 코드에서 시작한다.

전역 변수를 2개 추가하자. Outline Font로써 문자를 처리하면 일반 객체처럼 회전이 가능하다. 이를 확인해 보기 위해 문자를 회전시키는데 사용하는 변수 하나와 Outline Font 문자들에 대한 정보를 저장하는 변수 변수인 gmf을 선언한다. 배열이 96개인 이유는 ASCII의 코드중에서 숫자, 영문자와 특수문자에 대해 사용할 수 있도록 하기 위함이다.

GLuint base;
GLfloat rot = 0.0f; // 
GLYPHMETRICSFLOAT gmf[96]; // 

이제 해야 할 일은 모든 문자들을 Outline Font로써 리스트에 등록 저장하는 것이다. 즉, OpenGL에서 사용할수 있도록 폰트의 리스트를 만들어야 한다. 다음의 코드가 폰트의 리스트를 생성하는 코드이다.

GLvoid BuildFont(GLvoid)
{
    HFONT font;
    base = glGenLists(96);
    font = CreateFont(-12,
                0,
                0,
                0,
                FW_BOLD,
                FALSE,
                FALSE,
                FALSE,
                HANGUL_CHARSET,  // <1-1>
                OUT_TT_PRECIS,
                CLIP_DEFAULT_PRECIS,
                ANTIALIASED_QUALITY,
                FF_DONTCARE|DEFAULT_PITCH,
                "Comic Sans MS"); // <1-2>
   
    SelectObject(hDC, font);
   
    wglUseFontOutlines(hDC,  // <2-1>
                32, // <2-2>
                96, // <2-3>
                base, // <2-4>
                0.1f, // <2-5>
                0.5f, // <2-6>
                WGL_FONT_POLYGONS, // <2-7>
                gmf); // <2-8>
}

지난장에서 설명했던 것은 제외하고 새로운 것만을 설명하도록 하겠다.

<1-1>의 코드를 살펴보자. 기억력이 좋은 독자는 지난장에서의 <1-1>의 코드가 무엇이었는지 기억하는가? ANSI_CHARSET이였다. 하지만 한글 윈도우즈에서는 Outline Font를 사용할때 ANSI_CHARSET로 해주면 Outline Font를 얻을수없다. 원하는 것을 얻기위해서는 HANGUL_CHARSET으로 지정해 주어야 한다.

<1-2>에서 폰트를 바꾸었다. 지난장과 같이 폰트를 바꾸지 않아도 상관없다.

이제 가장 핵심이 되는 실제 Outline Font의 리스트를 얻어오는 함수에 대해서 알아보도록 하겠는데 바로 <2-1>에서 보이는 wglUseFontOutlines가 바로 그것이다. 총 8개의 인자가 필요한데 하나 하나 알아보자.

<2-1> 코드는 OpenGL과 연결되어 있는 DC에서 폰트 정보를 얻기 위한 DC의 핸들이다.

<2-2> 코드는 ASCII에서 시작할 코드의 번호이고 <2-3> 코드는 <2-2>코드에서 지정된 코드에서 몇개의 문자를 얻을 것인지를 지정하는 갯수값이다.

<2-4> 코드는 실제 리스트를 만들때 기준이 되는 값을 지정하는 것이다.

<2-5> 코드는 폰트로부터 얼마나 정밀하게 객체를 생성할 것인지를 나타낸다. 0.0값이 가장 정말하게 만들수있다.

<2-6> 코드는 Z축으로 문자 객체의 두깨를 설정한다.

<2-7> 코드가 가질수있는 값은 2가지인데 첫째는 WGL_FONT_POLYGONS와 WGL_FONT_LINES이다. 첫째는 면으로 구성된 폰트 객체를 생성하고 두번째는 라인만으로 구성된 폰트 객체를 생성한다.

<2-8> 코드는 실제 생성된 각각의 문자에 대한 Outline 정보가 저장되는 배열 변수이다.

리스트를 생성했다면 생성된 것들을 반환하는 함수도 필요하다. 다음의 코드가 그와 같은 것이다. 이미 지난장에서 만들었고 그대로 사용하면 된다.

GLvoid KillFont(GLvoid)
{
    glDeleteLists(base, 96);
}

glDeleteLists 함수로써 첫번재 인자는 메모리 상에서 제거할 리스트의 기준값이 오고 두번째는 그 기준에서부터 몇개의 리스트를 제거할 것인지를 나타낸다.

이제 사용할 폰트 리스트가 완벽하게 준비가 되었다. 이제 이 준비된 리스트를 화면상에 찍을 수 있는 함수를 만들어보자. 출력한 물자열을 인자로 받아 출력해 주는 함수이다. 이미 지난장에서 만들었고 그대로 사용하면 된다.

GLvoid glPrint(const char *text)
{
    glPushAttrib(GL_LIST_BIT); //<1>
    glListBase(base - 32); //<2>
    glCallLists(strlen(text), GL_UNSIGNED_BYTE, text); //<3>
    glPopAttrib(); //<4>
}

자! 이제 화면상에 글자를 그려주는 함수까지 준비가 되었다. 이제 위에서 만든 것들을 사용만 하면 되겠다. 먼저 initGL(GLvoid) 함수에 위에서 만든 BuildFont 함수를 추가함으로써 폰트 리스트를 생성하도록 한다.

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);
   
    ///////////////////////// NEW //////////////////////////
    BuildFont();
    glEnable(GL_LIGHTING); // <1>
    glEnable(GL_LIGHT0); // <2>
    glEnable(GL_COLOR_MATERIAL); // <3>
    ///////////////////////// NEW //////////////////////////
   
    return TRUE;
}

위의 코드를 보면 새롭게 추가된 코드가 3개이다. <1>과 <2>는 빛을 켜는 코드이고 <3>은 색상을 추적하여 물체의 재질을 설정하는 코드이다. 이 코드가 새로운 독자는 빛과 재질에 대한 장을 보기 바란다.

그리고 WinProc 함수의 WM_CLOSE를 처리하는 부분(이 부분은 프로그램이 종료될때 실행된다)에서 KillFont 함수를 추가하여야 하는데 이미 지난장에서 추가하였다.

case WM_CLOSE:
{
    ///////////////////////// NEW //////////////////////////
    KillFont();
    //////////////////////// NEW //////////////////////////
    PostQuitMessage(0);

    return 0;
}

자 이제 문자열을 화면상에 그려주는 것만 남았다. 무언가를 그려주는 코드는 항상 DrawGLScene(GLvoid) 부에 위치했고 이번에도 역시 마찬가지이다. 다음의 코드를 살펴보자.

int DrawGLScene(GLvoid)
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glLoadIdentity();
    glTranslatef(0.0f, 0.0f, -6.0f);
   
    glColor3f(0.90f, 0.9f, 1.0f);
    glScalef(1.0f, 2.0f, 1.0f);
    glRotatef(rot, 0.0f, 1.0f, 0.0f);
    glTranslatef(-1.0f, -0.3f, 0.0f);
    glPrint("Dip2K");
    rot += 1.0f;
   
    return TRUE;
}

DrawGLScene 함수의 구현은 모두 새롭게 작성되어졌다.

기본 흐름은 옅은 파랑색으로 재질을 설정하며 Y축으로 물체(문자)를 늘렸다. 그리고 매 순간마다 Y축으로 1도씩 회전시키는 에니메이션으로써 분명하게 문자의 3차원임일 확인한다. 비트맵의 경우 특별하게 문자의 위치를 지정하는 함수가 따로 있었던데 반해서 Outline Font는 단지 객체이므로 따로 그럴 필요가 없다.

다음은 최종적인 이 프로그램의 실행 결과이다.


사용자 삽입 이미지
어떤가? 드디어 우리가 원하는 결과를 얻었다.