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

사용자 삽입 이미지OpenGL에서 문자를 출력해야 할 경우가 있다. 그렇다면 OpenGL에서는 문자를 출력하기 위한 루틴이 준비되어져 있는가? 대답은 “전혀, 아니다” 이다. 그 이유는 각 플랫폼 OS에 따라 문자를 출력하는 방식이 너무나도 다르기 때문이라고 하는 이도 있다. 그래서인지 각 플랫폼마다 각각 그에 맞는 OpenGL API를 확장하여 제공하는데 문자 출력 부분의 함수도 같이 제공된다. 윈도우즈에서는 위글(WGL)이라 불리우며 Applet은 아글(AGL), 유닉스 계열의 OS은 XGL의 이름으로 재공된다. 본장은 여러가지 플랫폼들중에 우리가 가장 많이 사용하는 윈도우즈에서 문자를 출력해 보는 것으로 구성하려고 한다. 즉, 이 장은 다른 OS에서는 이용될수 없다.

우리의 컴퓨터에는 많은 글꼴이 하드 디스크에 저장되어져 있고 또 사용되어진다. 이 많은 글꼴들을 OpenGL에서 사용하는 방법에는 2가지가 있는데 첫째는 문자 하나 하나를 Bitmap 방식으로 사용하는 경우와 문자 하나 하나를 객체로 처리하여 사용하는 경우가 있다. Bitmap 방식으로 사용할 경우 이것은 객체가 아니기 때문에 회전이라든지 확대/축소라든지 텍스쳐 맵핑등 OpenGL에서 객체에 적용할 수 있는 것들이 크게 제약된다. 하지만 객체 단위로 처리할 경우 말 그대로 문자 하나 하나가 객체이므로 모든 특수효과를 적용할수 있다. 그러나 Bitmap에 비하여 객체단위로 문자를 처리하는 것은 많은 어버헤드와 연산 시간이 소요된다. 이장에서는 Bitmap으로 문자를 처리하는 것에 대해 다루고 다음에 장에서 객체를 이용한 문자 출력을 설명하기로 한다. 참고로 객체 단위로 문자를 처리하는 경우 그 문자를 Outline 형태의 문자라 한다.

이곳에서 사용할 소스 코드는 1장에서 만들어진 것에서 부터 시작된다.

전역 변수를 하나 설정하자. 이 변수는 폰트 리스트의 기준으로 사용된다.

GLuint base;

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

GLvoid BuildFont(GLvoid)
{
    HFONT font; // <1>
    base = glGenLists(96); // <2>
    font = CreateFont(-24, // <3-1>
                0,
                0,
                0,
                FW_BOLD, // <3-2>
                FALSE, // <3-3>
                FALSE, // <3-4>
                FALSE, // <3-5>
                ANSI_CHARSET, // <3-6>
                OUT_TT_PRECIS,
                CLIP_DEFAULT_PRECIS,
                ANTIALIASED_QUALITY,
                FF_DONTCARE|DEFAULT_PITCH,
                "Courier New"); // <3-6>
   
    SelectObject(hDC, font); // <4>
    wglUseFontBitmaps(hDC, 32, 96, base); // <5>
}

<1>번 코드는 윈도우즈에 설치되어 있는 폰트를 사용하기 위해 폰트를 생성하기 위해 사용하는 변수이다.

<2>번 코드는 총 96개의 리스트를 만든다. 숫자와 영문자(대, 소문자) 그리고 특수 기호를 모두 합한 갯수이다. 잠깐 리스트에 대해서 소개하자면 리스트란 우리가 어떤 물체를 만든다고 할때 그 물체를 하나의 리스트에 지정해 두면 이후에 또 그 물체가 필요할 경우 지정된 리스트만을 호출해서 필요한 물체를 사용할 수 있도록 하는 기능이다. 장점이라면 리스트로 미리 물체를 생성해 놓으면 그렇지 않을때보다 속도가 많이 개선된다. 단점이라면 메모리를 차지 하고 있다는 것이다. 리스트를 생성하는 함수가 glGenLusts인데 몇개의 리스트를 생성하는지를 인자로 갖는다. 반환값은 생성된 리스트의 첫번째를 참조하는 기준(Base)값을 리턴한다.

<3-1>번 코드에서 보이는 CreateFont는 Font를 생성하는하여 반환하는 것으로 아주 많은 인자를 갖는다. 필자도 이 모든 인자에 대해서 자세히 알지 못하지만 중요하다고 생각되는 인자에 대해서만 언급하도록 하겠다. 첫번재 인자는 폰트의 크기이다. 이 경우 -24인 음수를 주었는데 이는 만약 크기 24인 폰트를 생성하는데 24인 폰트를 만들수없다면 그와 가장 가까운 크기로 만들어라는 의미에서 음수 형태로 지정한 것이다. 그리고 <3-2>의 인자는 폰트를 어느 정도로 두껍게 할지에 대해 지정하는 것이다. 주로 갖는 값은 FW_SHIN, FW_BOLD, FW_NORMAL 등이 있다. <3-3>은 Italic 모양인지를 지정하는 것이고 <3-4>는 언더라인 형태인지를, <3-5>는 Strike Out 형태인지를 Boolean 형태로 지정한다. <3-6>은 생성할 폰트의 Char Set를 지정하는 것으로 대부분의 영문자에 대해서는 ANSI_CHARSET이면 충분하고 한국 윈도우즈의 경우 HANGUL_CHARSET으로 지정할 수도 있는데 다음장에서 설명할 Outline 형태의 문자 출력에서는 HANGUL_CHARSET으로 지정해야만 된다. 그리고 벳딩과 같은 그림문자 기호는 SYMBOL_CHARSET으로 지정해야 원하는 바를 얻을수 있다. 그리고 <3-6>은 사용하기를 원하는 폰트의 이름을 지정한다. 이 함수에 대한 나머지 자세한 것들은 관련 자료를 찾아보길 바란다.

<4>번 코드는 생성한 폰트를 해당 DC에서 사용하도록 지정하는 코드이다.

<5>번 코드는 OpenGL이 사용하는 DC에서 사용하는 폰트를 통해서 폰트 리스트를 생성하는 위글 함수이다. 위글 함수는 윈도우즈에서만 제공되는 함수이다. 첫번째 인자는 DC의 헨들, 두번째는 코드값과 밀접한데 우리가 ASCII 코드는 0에서 255까지의 값을 갖는다. 즉 두번째는 ASCII 코드값중에서 몇번부터 사용할 것인지를 지정한다. 세번째는 두번째에서 지정한 코드에서부터 몇개의 코드를 취할 것인지를 지정한다. 위의 코드는 32번 코드에서부터 총 96개의 문자를 취해 리스트를 생성하고자 한다는 의미이다. 네번째 인자는 생성될 리스트의 기준이 될 변수이다. 이 코드가 가장 핵심적인 코드가 되겠다.

리스트를 생성했다면 생성된 것들을 반환하는 함수도 필요하다. 다음의 코드가 그와 같은 것이다.

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>
}

<1>번 코드는 리스트에 대한 상태를 저장하는 것인데 이는 우리가 리스트를 하나만 사용하는 경우에는 별로 중요치 않으나 또 다른 다른 리스트 사용할 수도 있다. 또 다른 리스트를 위해서 리스트 상태를 저장해 두는 것이다.

<2>번 코드는 사용하고자 하는 리스트의 기준을 OpenGL에게 알려주는 것이다. 기준에서 32를 뺐는데(base-32) 우리가 위에서 폰트 리스트를 생성할때 ASCII 코드중에서 32번부터 시작을 했기 때문이다.

<3>번 코드는 실제적으로 리스트에서 필요한 것들을 가져와 그려주는 핵심이 된다. text 변수는 문자열 배열이다. 우리가 ASCII에서 ‘A’는 65번이다. ‘B’는 66번이다. text 변수에 “ABC”라는 문자가 있다고 해보자. text는 문자열 배열이라고 볼수있고 각 셀의 값은 부호없는 Byte형이다. 모두 3개의 문자로 이루어져 있다. 이것은 곧 첫번재 셀에는 65가 두번째 셀에는 66이 세번째 셀에는 67의 Unsigned Byte Type의 값이 들어있는 것과 동일하다. 이부분을 이해했다면 glCallLists를 살펴보자. 첫번재 인자는 해석해야될 셀의 겟수를 나타내고 두번째는 셀의 데이타 타입을 세번째는 그 셀의 배열을 나타내게 된다. 결국 하나 하나의 셀을 순서대로 처리해서 화면상에 그려주는 것이다. 일단 하나의 셀이 처리되면 자동으로 다음에 처리될 셀을 위해 좌표가 오른쪽으로 이동된다.

자! 이제 화면상에 글자를 그려주는 함수까지 준비가 되었다. 이제 위에서 만든 것들을 사용만 하면 되겠다. 먼저 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();
    ///////////////////////// NEW //////////////////////////
   
    return TRUE;
}

그리고 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, -4.0f);
    glBegin(GL_QUADS);
    glColor3f(0.0f, 0.0f, 0.0f); glVertex2f(-1.0f, -1.0f);
    glColor3f(1.0f, 0.0f, 0.0f); glVertex2f(1.0f, -1.0f);
    glColor3f(0.0f, 1.0f, 0.0f); glVertex2f(1.0f, 1.0f);
    glColor3f(0.0f, 0.0f, 1.0f); glVertex2f(-1.0f, 1.0f);
    glEnd();
   
    glColor3f(1.0f, 1.0f, 1.0f);      //<1>
    glRasterPos2f(-1.4f, 0.0f);       //<2>
    glPrint("Dip2K's OpenGL Forum");  //<3>
   
    return TRUE;
}

노란색의 코드가 새롭게 추가된 것들인데 중요한 것은 <1>번과 <2>번 코드이다. 나머지 것들은 모두 지난장에서 살펴보았으므로 설명은 생략하기로 하고 중요한 것들만 살펴보자.

<1>번 코드는 그려줄 비트맵의 색을 지정하는 코드이다. 비트맵 역시 glColor로써 색을 지정한다.

<2>번 코드는 우리가 어디에 문자를 그려줄 것인가를 지정하는 것이다. Bitmap의 문자는 특별이 위치를 지정해 주는 함수가 따로 준비되어 있는데 바로 glRasterPos2f가 그 함수이다. 첫번째는 X좌표이고 두번째는 Y좌표를 나타낸다. Bitmap은 Perspective가 적용되지 않으며 항상 Ortho 모드로만 적용된다.

<3>번 코드는 우리가 이미 만들어 놓은 함수이다. 출력하고자 하는 문자열을 넣어주기만 하면 그만이다.

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

사용자 삽입 이미지

어떤가? 드디어 우리가 원하는 결과를 얻었다.

“[OpenGL Tutorial] Output String on the Screen(Bitmap)”에 대한 22개의 댓글

  1. 감사드립니다.
    많은도움이 되었고 특히 개념적으로 이해가 되었습니다
    다시한번 감사드립니다

  2. 영어말고 한글을 출력하고 싶은데..
    HANGUL_CHARSET로 바꾸기만 하면 되나요? 바꿔도 한글이 출력이 안되는데..
    한글을 출력할려면 어떡해 해야 하나요?

    1. OpenGL과 라스터 방식의 한글 출력은 않되는걸로 알고 있습니다. 물론 아웃라인 방식의 벡터처리로는 가능하지만 속도가 느려지고 그닦 효율성이 없습니다. 속도도 빠르고 좋은 방식은 텍스쳐 맵핑 방식하시는게 좋겠습니다. 텍스쳐 맵핑이란.. 사용하고 하는 한글 전체를 이미지로 저장해서 실제로 사각형 영역에 맵핑하는 방식입니다..

  3. 그렇군요… 감사합니다…
    그런데 타이머를 만들려고 하건든요. 그럼 glPrint를 1초마다 계속 다른걸 출력해야 되는데 int형이나 char배열로 만들어도 에러가 계속 뜨더라고요.. 어떡해 해야할까요?

  4. 음… 먼저 문자열배열 count[5]를 만들어서 glPrint(count[?]); 이렇게 해서 한번 출력하고 ?값을 증가시키고 KillFont를 실행시켜 삭제하고 다시 증가된 값을 출력시켜 봤는데 count[5]는 그대로 나오지만 count[4]는 count[5]에 들어있는것도

  5. 이 코드와 같이 택스쳐 활용해서 2D를 화면에 띄우니
    글자 색이 택스쳐 색상과 섞여서 나옵니다. 어떻게 해야 해결가능 한 겁니까?

  6. 지금 이 예제를 제 예제에 추가 시키려고 하는데요 똑같이 했는데 텍스트가 뜨질 않습니다 ㅠㅠ
    DrawGLScene 함수안에 glTranslatef(0.0f, 0.0f, -4.0f);
    이건 왜 해주는 건가요??

    1. 모델의 위치를 옮기기 위함입니다. 카메라가 원점에 존재하므로 모델이 원점에 존재하면 아무것도 보이지 않겠죠?

  7. glRasterPos2f(); 가 아닌
    glRasterPos3f();로 삼차원 좌표 선상에 위치 시키는 경우 z 축에 대하여 거울 처럼 반사되어 뒷부분에도 텍스쳐가 찍혀 나옵니다. 이런 경우에는 어떻게 처리 가능 한가요 ?ㅠㅠㅠ

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다