본문 바로가기
그래픽스/OpenGL

OpenGL 비트맵을 이용한 텍스트 그리기(Text Rendering)

by 니키티스 2020. 11. 30.

비트맵에서 폰트를 가져와 텍스트를 그릴 수 있다.

이번에 공부한 내용은 OpenGL에서 비트맵을 이용한 텍스트 렌더링이다.

수업 중 관련 프로젝트를 과제로 내주셔서 구현한다고 삽질을 며칠 했다.

소스 코드도 첨부해놓았으니 다른 분들에게도 도움이 된다면 좋겠다.

(다만 그렇다고 후배님들이 이걸 복붙하는 일은 앵간해서 없었으면 좋겠다...)

목표는 그림판의 텍스트 입력 상자 만들기다.

숫자와 영문자를 입력하면 해당하는 글자를 입력하고 백스페이스 바를 누르면 글자가 지워지게 하는 것이다.

다만 한글 입력은 외부 라이브러리가 없으면 번거로워서 제외하였다.

한글을 OpenGL에서 구현하고 싶다면 FTGL과 같은 라이브러리를 사용하는 방법을 찾아보는 걸 추천한다.

1. OpenGL 텍스트 출력하기

OpenGL은 기본적으로 텍스트를 지원하지 않는다. 따라서 텍스트 구현은 전적으로 사용자에게 달려있다. 텍스트 렌더링은 적어도 C언어 콘솔에서 printf 하나로 출력하거나 게임 엔진에서 텍스트 오브젝트를 만드는 것보다 어렵다. 텍스트 자체를 이미지로 만들어 출력해주거나 텍스쳐로 매핑해주어야 한다.

OpenGL 입문자의 관점에서 텍스트를 그리기 위해 사용할 수 있는 방법 중 대표적인 방법은 2가지가 있다.

비트맵(Bitmap)

첫 번째는 비트맵을 이용한 방법이다. 폰트에서 글자를 추출하여 하나의 비트맵으로 만드는 것이다. 메모리를 적게 사용하고 렌더링이 빠른 편이다. 가장 중요한 점은 구현이 가장 간단하다.

그러나 비트맵은 유연하게 여러 기능을 사용하기 어렵다. 다른 폰트를 사용하려면 재컴파일을 해야 하고, 해상도에 제한이 있어서 확대를 하면 깨지기 쉽다. 가장 큰 문제점은 비트맵은 많은 글자를 가져오기는 어렵다. 몇 글자의 문자 집합만을 사용할 수 있게 때문에 유니코드를 사용하거나 한글의 모든 문자를 사용하기 힘들다. 한글 전체를 이미지로 저장해서 텍스쳐 매핑을 하면 불가능한 것도 아니지만 이후 소개할 방법보다 많이 번거로울 수 있다.

최근에도 비트맵은 빠른 속도로 인해 텍스트 출력에 자주 사용하곤 한다. 이번 포스팅에서도 비트맵을 이용해 텍스트를 그려보고자 한다.

비트맵으로 폰트를 텍스처로 만든다, https://learnopengl.com/In-Practice/Text-Rendering

FreeType 또는 FTGL

두 번째는 FreeType 라이브러리나 FTGL(FreeType GL)를 사용하는 방법이다. FreeType은 오픈 소스 프로젝트로 GPL2 라이센스로 공개되어 있다.

FreeType 라이브러리는 폰트를 가져와 이를 비트맵의 방식으로 그릴 수 있게 하는데, 폰트와 관련하여 여러 연산을 제공한다. FreeType은 윈도우에서도 지원하지만 일반적으로 Mac OS X와 Linux에서 더 인기 있다고 한다.

FreeType 라이브러리는 트루타입 폰트(TTF: TrueType Font)를 가져와 사용한다. 트루타입 폰트는 폰트 글리프(Glyph, 글자 하나하나의 모양)의 집합으로, 글리프 하나하나는 수학적인 방정식으로 정의해서 폰트 크기가 들쭉날쭉해도 품질 저하가 없다고 한다.

트루타입 폰트(TTF)가 뭔지 궁금하다면 해당 글에 잘 정리되어 있다. 유익한 글이니 한 번 읽어보면 좋을 것 같다.

zeddios.tistory.com/198

 

TTF? OTF? 차이점 알아보기

안녕하세요:) Zedd입니다. 지금 <프로젝트에 Custom Font적용하는 법>으로 글을 하나 쓰고있는데, 문득 TTF와 OTF의 차이점이 궁금해져서..이렇게 글을 쓰게 되네요 XD 폰트를 받을려고 딱 다운로드버튼

zeddios.tistory.com

FTGL(FreeType GL)은 Freetype2를 이용해 OpenGL에서 폰트를 렌더링 하는 오픈 소스 라이브러리다.

이 둘은 비트맵 방식보다 훨씬 기능을 유연하게 사용할 수 있고, 이미 존재하는 폰트를 그대로 가져올 수 있어 폰트가 한글을 지원한다면 별다른 가공 없이 한글을 출력할 수 있다.

추가 라이브러리를 설치해야 하고 비트맵보다 좀 더 메모리적인 면에서 무겁다는 단점이 있다. 다만 임베디드 시스템처럼 메모리가 부족한 환경이 아니라면 크게 신경 쓸 정도는 아니라 생각한다.

이외

비트맵이나 FreeType을 사용하는 법 외에도 문자를 객체로 취급한다던가 Distance Field를 이용할 수도 있다. 

Distance Field를 이용하는 방법은 다음 링크를 참고하면 좋을 듯하다.

bytewrangler.blogspot.com/2011/10/signed-distance-fields.html

 

Signed Distance Fields

I was first exposed to Signed Distance Fields through this white paper written by Chris Green from Valve Software. Valve often make techni...

bytewrangler.blogspot.com

 

2. 비트맵을 이용한 텍스트 그리기

비트맵을 이용해 텍스트를 그리는 방법은 NEHE의 Tutorials를 참고하였다. 비트맵을 이용하는 방법뿐만 아니라 텍스처 매핑 등 MFC와 OpenGL을 활용한 광범위한 튜토리얼을 제공한다.

(NEHE tutorials: nehe.gamedev.net/tutorial/bitmap_fonts/17002/)

 

NeHe Productions: Bitmap Fonts

Bitmap Fonts Welcome to yet another Tutorial. This time on I'll be teaching you how to use Bitmap Fonts. You may be saying to yourself "what's so hard about putting text onto the screen". If you've ever tried it, it's not that easy! Sure you can load up an

nehe.gamedev.net

비트맵을 이용한 텍스트 그리기는 비트맵을 텍스트로 출력하는 것이 첫 번째이고 이걸 러버밴딩 기법으로 화면에 실시간으로 입력하거나 출력하는 것이 두 번째이다.

BitmapText.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#include "BitmapText.h"
 
#include <windows.h> // Header File For Windows
#include <stdio.h>   // Header File For Standard Input/Output
#include <stdarg.h>  // Header File For Variable Argument Routines
#include <gl\gl.h>   // Header File For The OpenGL32 Library
#include <gl\glu.h>  // Header File For The GLu32 Library
 
#define DEFAULTFONTSIZE 20
 
enum FONT
{
    ARIAL = 1,      // Arial = 1
    COURIERNEW,     // Courier New = 2
    CALIBRI,        // Calibri = 3
    TIMESNEWROMAN,  // Times New Roman = 4
    BOOKANTIQUA,    // Book Antiqua = 5
    TREBUCHETMS     // Trebuchet MS = 6
};
 
HDC hDC;              // Private GDI Device Context
HGLRC hRC;            // Permanent Rendering Context
HWND hWnd;            // Holds Our Window Handle
HINSTANCE hInstance;  // Holds The Instance Of The Application
 
GLuint  base;  // Base Display List For The Font Set
 
GLvoid BuildFontWithEnum(FONT fontEnum, int fontSize)
{
    switch (fontEnum)
    {
        case ARIAL:
            BuildFont("Arial", fontSize);
            break;
        case COURIERNEW:
            BuildFont("Courier New", fontSize);
            break;
        case CALIBRI:
            BuildFont("Calibri", fontSize);
            break;
        case TIMESNEWROMAN:
            BuildFont("Times New Roman", fontSize);
            break;
        case BOOKANTIQUA:
            BuildFont("Book Antiqua", fontSize);
            break;
        case TREBUCHETMS:
            BuildFont("Book Antiqua", fontSize);
            break;
    }
}
 
/** 새로운 폰트를 생성하는 함수입니다.
 *  char* fontName: 폰트 이름
 *  int fontSize: 폰트 크기
 */
GLvoid BuildFont(const char* fontName, int fontSize)
{
    HFONT   font;     // Windows Font ID
    HFONT   oldfont;  // Used For Good House Keeping
 
    base = glGenLists(96);  // Storage For 96 Characters
 
    font = CreateFont(-fontSize, // Height Of Fonts
        0,              // Width Of Font
        0,              // Angle Of Escapement
        0,              // Orientation Angle
        FW_BOLD,        // Font Weight
        FALSE,          // Italic     (취소선)
        FALSE,          // Underline (밑줄)
        FALSE,          // Strikeout (취소선)
        ANSI_CHARSET,   // Character Set Identifier
        OUT_TT_PRECIS,  // Output Precision
        CLIP_DEFAULT_PRECIS,        // Clipping Precision
        ANTIALIASED_QUALITY,        // Output Quality
        FF_DONTCARE | DEFAULT_PITCH,  // Family And Pitch
        fontName);         // Font Name
 
    oldfont = (HFONT)SelectObject(hDC, font); // Selects The Font We Want
    wglUseFontBitmaps(hDC, 3296, base);     // Builds 96 Characters Starting At Character 32
    SelectObject(hDC, oldfont);               // Selects The Font We Want
    DeleteObject(font);                       // Delete The Font
}
 
GLvoid KillFont(GLvoid)                     // Delete The Font List
{
    glDeleteLists(base, 96);                // Delete All 96 Characters
}
 
GLvoid glPrint(const char* fmt, ...)                // Custom GL "Print" Routine
{
    char        text[256];          // Holds Our String
    va_list     ap;                 // Pointer To List Of Arguments
 
    if (fmt == NULL)                // If There's No Text
        return;                     // Do Nothing
 
    va_start(ap, fmt);              // Parses The String For Variables
        vsprintf(text, fmt, ap);    // And Converts Symbols To Actual Numbers
    va_end(ap);                     // Results Are Stored In Text
 
    glPushAttrib(GL_LIST_BIT);              // Pushes The Display List Bits
    glListBase(base - 32);                  // Sets The Base Character to 32
 
    glCallLists(strlen(text), GL_UNSIGNED_BYTE, text);  // Draws The Display List Text
    glPopAttrib();                      // Pops The Display List Bits
}
 
int InitFont(GLvoid)                      // All Setup For OpenGL Goes Here
{
    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
 
    hDC = wglGetCurrentDC();            // 현재 openGL 윈도우의 hDC를 가져온다.
 
    BuildFontWithEnum(ARIAL, DEFAULTFONTSIZE);       // Build The Font
 
    return TRUE;                        // Initialization Went OK
}
cs

시간이 난다면 NEHE Tutorials를 모두 읽는 것이 유익하겠지만, 전체가 영어로 되어 있어서 시간이 촉박한 사람을 위해 소스 코드를 올려놓았다.

요약하면 위의 코드에서 BuildFont로 비트맵으로부터 폰트를 새로 구성하고, glPrint로 화면상에 텍스트를 출력한다.

간단히 하나씩 알아보자.

1
2
3
4
5
6
7
8
9
enum FONT
{
    ARIAL = 1,      // Arial = 1
    COURIERNEW,     // Courier New = 2
    CALIBRI,        // Calibri = 3
    TIMESNEWROMAN,  // Times New Roman = 4
    BOOKANTIQUA,    // Book Antiqua = 5
    TREBUCHETMS     // Trebuchet MS = 6
};
cs

이 부분은 Font를 지정하기 위한 부분이다.

그림판에서 폰트를 원하는 것으로 바꿀 수 있게 구현하려고 Enum을 추가하였다.

코드에서 중요한 부분은 아니니 제외해도 괜찮다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
HDC hDC;              // Private GDI Device Context
HGLRC hRC;            // Permanent Rendering Context
HWND hWnd;            // Holds Our Window Handle
HINSTANCE hInstance;  // Holds The Instance Of The Application
GLuint  base;  // Base Display List For The Font Set
GLvoid BuildFontWithEnum(FONT fontEnum, int fontSize)
{
    switch (fontEnum)
    {
        case ARIAL:
            BuildFont("Arial", fontSize);
            break;
        case COURIERNEW:
            BuildFont("Courier New", fontSize);
            break;
        case CALIBRI:
            BuildFont("Calibri", fontSize);
            break;
        case TIMESNEWROMAN:
            BuildFont("Times New Roman", fontSize);
            break;
        case BOOKANTIQUA:
            BuildFont("Book Antiqua", fontSize);
            break;
        case TREBUCHETMS:
            BuildFont("Book Antiqua", fontSize);
            break;
    }
}
cs

비트맵을 불러오기 위해서는 MFC를 사용하여야 한다.

따라서 HDC, HWND 등 Window의 핸들 값을 가져온다.

GLuint base는 처음에 문자 폰트를 저장하기 위해 디스플레이 리스트를 만들 때, 기준이 되는 숫자이다.

BuildFontWithEnum은 위의 Enum과 size를 입력받아 Enum에 해당하는 폰트를 빌드한다.

Enum을 통해 글자 폰트를 바꿀 수 있게 하기 위해 만들었으므로 제외해도 괜찮다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/** 새로운 폰트를 생성하는 함수입니다.
 *  char* fontName: 폰트 이름
 *  int fontSize: 폰트 크기
 */
GLvoid BuildFont(const char* fontName, int fontSize)
{
    HFONT   font;     // <1> Windows Font ID
    HFONT   oldfont;  // Used For Good House Keeping
    base = glGenLists(96);  // Storage For 96 Characters
    font = CreateFont(-fontSize, // Height Of Fonts
        0,              // Width Of Font
        0,              // Angle Of Escapement
        0,              // Orientation Angle
        FW_BOLD,        // Font Weight
        FALSE,          // Italic     (취소선)
        FALSE,          // Underline (밑줄)
        FALSE,          // Strikeout (취소선)
        ANSI_CHARSET,   // Character Set Identifier
        OUT_TT_PRECIS,  // Output Precision
        CLIP_DEFAULT_PRECIS,        // Clipping Precision
        ANTIALIASED_QUALITY,        // Output Quality
        FF_DONTCARE | DEFAULT_PITCH,  // Family And Pitch
        fontName);         // Font Name
    oldfont = (HFONT)SelectObject(hDC, font); // Selects The Font We Want
    wglUseFontBitmaps(hDC, 3296, base);     // Builds 96 Characters Starting At Character 32
    SelectObject(hDC, oldfont);               // Selects The Font We Want
    DeleteObject(font);                       // Delete The Font
}
 
cs

폰트를 비트맵으로부터 가져와 리스트에 저장하는 함수이다.

glGenLists(96)으로 폰트를 위한 리스트를 생성한다. base에는 기준 숫자를 저장하게 된다.

CreateFont를 이용하여 폰트를 지정한다.

1
2
3
4
GLvoid KillFont(GLvoid)                     // Delete The Font List
{
    glDeleteLists(base, 96);                // Delete All 96 Characters
}
cs

폰트를 저장하기 위해 만든 폰트 디스플레이 리스트를 메모리에서 해제한다.

프로그램을 종료할 때 윈도우에서 자동으로 리스트를 해제하는 것으로 안다. 하지만 NEHE에서는 안전을 위해 프로그램이 종료될 때 KillFont를 호출하여 폰트에 대한 비트맵을 메모리 상에서 해제하도록 하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
GLvoid glPrint(const char* fmt, ...)                // Custom GL "Print" Routine
{
    char        text[256];          // Holds Our String
    va_list     ap;                 // Pointer To List Of Arguments
    if (fmt == NULL)                // If There's No Text
        return;                     // Do Nothing
    va_start(ap, fmt);              // Parses The String For Variables
        vsprintf(text, fmt, ap);    // And Converts Symbols To Actual Numbers
    va_end(ap);                     // Results Are Stored In Text
    glPushAttrib(GL_LIST_BIT);              // Pushes The Display List Bits
    glListBase(base - 32);                  // Sets The Base Character to 32
    glCallLists(strlen(text), GL_UNSIGNED_BYTE, text);  // Draws The Display List Text
    glPopAttrib();                      // Pops The Display List Bits
}
cs

glPrint 함수는 빌드해놓은 폰트로 화면상에 텍스트를 출력한다.

printf와 동일하게 가변 인자를 사용하며 va_list를 통해 인자를 리스트로 받는다.

텍스트를 출력하고 싶을 때, glPrint("Hello, World! %s!", "My friend"); 와 같이 출력할 수 있다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
int InitFont(GLvoid)                      // All Setup For OpenGL Goes Here
{
    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
    hDC = wglGetCurrentDC();            // 현재 openGL 윈도우의 hDC를 가져온다.
    BuildFontWithEnum(ARIAL, DEFAULTFONTSIZE);       // Build The Font
    return TRUE;                        // Initialization Went OK
}
cs

InitFont 함수는 폰트를 초기화하기 위해 사용한다.

튜토리얼에서는 OpenGL 초기화를 위한 함수(InitGL)로 등장한다. 저 위에 주석을 친 부분은 기존 튜토리얼에 등장했던 부분이다.

여기서 필요한 부분은 다음 두 줄이다.

hDC = wglGetCurrentDC();

BuildFontWithEnum(ARIAL, DEFAULTFONTSIZE);

첫 번째 줄에서 wglGetCurrentDC()은 폰트 초기화를 위해 윈도우 핸들(hDC)을 가져온다.

이는 glut를 이용하는 OpenGL 프로그램에서 HDC가 필요할 때 유용하게 사용할 수 있다.

두 번째 줄은 폰트를 빌드하는 줄로, 폰트와 기본 폰트 크기를 통해 폰트를 만든다.

그림판을 만들 때에는 GL 프로그램을 실행할 때 처음에 InitFont를 실행하고, 이후 폰트를 바꿀 때마다 BuildFont나 BuildFontWithEnum으로 폰트 리스트를 새로 만드는 방식으로 구현했다.

여기서 한 가지 의문이 생겼는데, 만약 여러 개의 폰트를 동시에 사용하고 싶으면 어떻게 해야 할까?

화면에 텍스트를 5개 만들었는데, 텍스트마다 폰트를 다르게 사용해야 할 수도 있다.

그런데 GL에서 폰트 리스트는 하나라서 여러 폰트를 동시에 리스트에 저장할 수는 없다. 그러면 여러 폰트를 동시에 사용할 수 없는 것은 아닐까? 텍스트를 그리고 나서 폰트를 새로 빌드한다면, 기존 텍스트는 깨지지 않을까?

이건 실제로 실험해보니 그리 문제가 되지 않았다.

화면에 텍스트를 출력한 순간부터 텍스트는 화면에 화소 단위로 뿌려지기 때문이다.

화면에 뿌려진 후에는 프레임 버퍼에 화소 단위로 색상이 저장된다. 그래서 이후에 폰트를 바꾸든 말든 문제가 생기지 않았다.

직접 만든 그림판 프로그램에서 실험한 결과. 폰트를 바꿔도 아무런 문제 없다.

 

3. 텍스트 입력

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#define MAX_KEY 256
 
...
 
char key_buffer[MAX_KEY]; /* 텍스트 버퍼 */
int key_count = 0/* 텍스트 문자열의 현재 길이 */
 
// 마우스 콜백 함수
void mouse(int btn, int state, int x, int y)
{
    static int count;
 
    if (btn == GLUT_LEFT_BUTTON && state == GLUT_DOWN)
    {
        ...
 
        switch (draw_mode)
        {
            ...
            case(DRAW_TEXT):
                rx = x;
                ry = wh - y;
                glRasterPos2i(rx, ry);
                count = 0;
 
                // Clear key buffer.
                if (key_count > 0)
                {
                    int i;
                    for (i = 0; i < key_count; i++) {
                        key_buffer[i] = '\0';
                    }
                    key_count = 0;
                }
                break;
            ...
        }
        ...
    }
}
 
// 텍스트 입력을 위한 키 콜백 함수
void key(unsigned char k, int xx, int yy)
{
    if (draw_mode != DRAW_TEXT) return;
    if (k != 8 && k < 32return// 백스페이스(008) 빼고 제어 문자(000~031)가 들어오면 제외한다.
 
    // 텍스트를 지운다.
    glEnable(GL_COLOR_LOGIC_OP);
    glLogicOp(GL_XOR);
    glColor3f(1.0f - r, 1.0f - g, 1.0f - b);
    glRasterPos2i(rx, ry);
    glPrint(key_buffer);
 
    if (k == 8 && key_count > 0)
    {
        // if the users enters the Backspace, delete the character.
        key_count--;
        key_buffer[key_count] = '\0';
    }
    else if (key_count < MAX_KEY - 1)
    {
        // if the user enters normal keys, record it in the key buffer.
        key_buffer[key_count] = k;
        key_count++;
    }
 
    // 텍스트를 다시 그린다.
    glColor3f(1.0f - r, 1.0f - g, 1.0f - b);
    glRasterPos2i(rx, ry);
    glPrint(key_buffer);
    
    glFlush();
    glDisable(GL_COLOR_LOGIC_OP);
}
 
int main(int argc, char** argv)
{
    int c_menu, p_menu, f_menu, ft_menu, ft_size_menu;
 
    // GLUT Setting
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);
    glutInitWindowSize(500500);
    glutCreateWindow("square");
    glutDisplayFunc(display);
    ...
    // Initialize
    InitFont();
    ...
    glutKeyboardFunc(key);
    glutMouseFunc(mouse);
    glutMainLoop();
    atexit(KillFont); // EXIT
 
    return 0;
}
cs

구현한 것은 위와 같다.

고무줄 기법(러버 밴딩, Rubber banding)을 이용하여 텍스트를 실시간으로 갱신한다.

고무줄 기법은 그림판에서 선분을 그리는 것을 생각하면 이해하기 쉽다.

선분을 그을 때 시작점을 정하고 마우스를 옮겨 끝점을 정하게 된다. 마우스를 옮겨 끝점을 모두 정하지 않아도 화면은 마우스를 옮길 때마다 갱신되어서 선분이 동적으로 늘었다가 줄어들게 된다.

이를 구현하기 위한 방법으로 가장 좋은 것은 XOR을 사용하는 방법이다.

XOR (^)은 두 번 하면 원래의 값이 나온다는 특징이 있다. 이 개념은 아래 글에 잘 정리되어 있다.

(X ^ Y ^ Y = X)

sangmin-kim.tistory.com/121

 

OpenGL glLogicOp란 ? 고무줄 기법, Rubber banding

1. OpenGL glLogicOp란 ? glLogicOp 함수는 컬러 인덱스 렌더링을 위한 논리적 픽셀 연산을 한다. glLogicOp는 들어오는 색상 RGBA 색상과 프레임 버퍼의 해당 위치에서 RGBA 색상간에 적용되는 논리적 연산을

sangmin-kim.tistory.com

색상 값을 XOR을 하게 되면 기존 색의 보색을 얻을 수 있다.

텍스트를 배경색의 보색으로 그려준 다음에 또 배경색의 보색으로 그리자. 그렇게 하면 결국 배경색이 되어서 텍스트를 그리기 전으로 돌아가게 된다.

XOR로 한 번 그려줬다면? 지우기 위해서는 XOR 해서 다시 지워주면 되는 것이다!

이 개념을 이용한 것이 아래에 있는 key 함수이다.

key 함수는 문자를 입력했으면 텍스트 버퍼(key_buffer)에 문자를 추가하고, 백 스페이스를 입력하면 텍스트 버퍼(key_buffer)의 문자 하나를 지운다.

입력할 때마다 문자를 지우고 출력해야 하니, 맨 처음에 XOR로 기존의 텍스트를 모두 지운다. 그다음에 텍스트 버퍼에 문자를 추가하거나 지우고, 변경된 텍스트를 화면에 출력해준다.

글씨를 잘못 썼을 때
glLogicOp를 이용하여 쉽게 글자나 도형을 지우고 다시 그릴 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
if (k != 8 && k < 32return// 백스페이스(008) 빼고 제어 문자(000~031)가 들어오면 제외한다.
 
// <1> 텍스트를 지운다.
glEnable(GL_COLOR_LOGIC_OP);
glLogicOp(GL_XOR);
glColor3f(1.0f - r, 1.0f - g, 1.0f - b);
glRasterPos2i(rx, ry);
glPrint(key_buffer);
 
if (k == 8 && key_count > 0)
{
    // 백스페이스가 들어오면 문자 지우기
    key_count--;
    key_buffer[key_count] = '\0';
}
else if (key_count < MAX_KEY - 1)
{
    // 일반 문자값이 들어오면 기록하기
    key_buffer[key_count] = k;
    key_count++;
}
 
// <2> 텍스트를 다시 그린다.
glColor3f(1.0f - r, 1.0f - g, 1.0f - b);
glRasterPos2i(rx, ry);
glPrint(key_buffer);
    
glFlush();
glDisable(GL_COLOR_LOGIC_OP);
cs

위의 소스 코드는 key 함수에서 가져온 건데, <1>과 <2>를 중점적으로 보면 좋을 것 같다.

OpenGL은 glEnable(parameter), glLogicOp(GL_XOR), glDisable(parameter)로 XOR 연산을 실행할 수 있다.

10~21번 줄에서 텍스트 버퍼(key_buffer)를 갱신한다.

텍스트 버퍼는 현재 어떤 텍스트가 입력됐는지를 저장한다.

<1> 텍스트를 지운다.

glEnable(GL_COLOR_LOGIC_OP);

glLogicOp(GL_XOR);

4번 줄에서 이 코드로 화소의 XOR 연산을 활성화한다.

텍스트 버퍼를 갱신하기 전에 XOR로 이전 텍스트를 지운다.

<2> 텍스트를 다시 그린다.

glFlush();

glDisable(GL_COLOR_LOGIC_OP);

반대로 28번 줄에서는 위 코드로 XOR 연산을 비활성화한다.

텍스트 버퍼를 갱신하고 나서 텍스트를 다시 그려준다.

여기서 중요한 점은, 반드시 glDisable 이전에 glFlush()가 이루어져야 한다는 점이다.

마치고

NEHE Tutorial에 나온 내용을 자세하게 다룰 수 있다면 좋겠지만 무려 15년이나 전에 이 내용을 올린 사람이 있어서, 이 글을 참고하면 도움이 될 것 같다. (www.gisdeveloper.co.kr/?p=29)

프로젝트를 하면서 느낀 점은 MFC를 사용할 일이 전혀 없을 줄 알았는데 OpenGL에서 텍스트를 출력하려면 거의 필수적으로 MFC를 사용해야 한다는 것이다.

다른 방법은 모르겠는데 비트맵을 이용하는 방식에선 MFC를 사용해야 했다.

MFC는 하나도 아는 게 없어서 난감 그 자체였다...

처음에 텍스트 입력을 구현하려고 할 때에는 화면을 모두 캡처한다는 방법을 생각했었다. 캡처한 내용을 메모리 상에 저장하고, 텍스트를 갱신할 때마다 화면을 캡처 화면으로 되돌린 뒤에 텍스트를 새로 그리는 것이다.

텍스트를 입력하기 시작했을 때, glReadPixels()로 화면 전체를 메모리에 저장하는 것이다. 그러면 초기 화면을 저장해두었으니까, 문자를 한 글자 입력할 때마다 화면을 초기 화면으로 바꿔버리고 텍스트를 다시 그리려 했다.

이렇게 되면 텍스트 하나 구현하려고 메모리에 사진 하나를 복사하는 것과 다름이 없었다.

너무 비효율적이지만 머릿속에 떠오르는 아이디어가 이것 하나뿐이었다. 어쩔 수 없이 glReadPixels의 활용 방법을 구글링 하고 있는데, 마침 Xor을 이용한 고무줄 기법(러버밴딩)을 발견하게 되었다.

아마 그 글을 발견하지 못했다면 영원히 이 과제를 해결할 수 없지 않았을까...

내 부족함을 여실히 느낄 수 있는 프로젝트였다.

역시 개발자는 직접 프로젝트를 해야 실력이 는다.

 

이건 여담이지만 한글 텍스쳐를 만들 때 아래에 있는 링크를 사용하면 유용하다. 상용 한글을 하나씩 따와 텍스트 파일로 만든 파일을 제공한다. 비트맵에 사용할 한글 폰트를 가져올 때에도 좋고, Unity의 TextMeshPro를 위해 Font를 생성할 때에도 유용하게 사용할 수 있다.

링크: phlm7th.tistory.com/45

 

상용한글 2350자

상용한글 2350자입니다. 모든 한글을 모아둔 텍스트 파일을 구해다가 특정 폰트로 서체를 바꿔서 지원하지 않는 글자는 지웠더니 딱 2350자가 나오네요. 구하다가 못구해서 직접 노가다로 만들었

phlm7th.tistory.com

 

참고 문헌

LearnOpenGL, "Text Rendering", learnopengl.com/In-Practice/Text-Rendering, 2020년 11월 6일 접속

 

 

LearnOpenGL - Text Rendering

Text Rendering In-Practice/Text-Rendering At some stage of your graphics adventures you will want to draw text in OpenGL. Contrary to what you may expect, getting a simple string to render on screen is all but easy with a low-level API like OpenGL. If you

learnopengl.com

NEHE, "Bitmap Fonts, nehe.gamedev.net/tutorial/bitmap_fonts/17002/, 2020년 11월 6일 접속

 

NeHe Productions: Bitmap Fonts

Bitmap Fonts Welcome to yet another Tutorial. This time on I'll be teaching you how to use Bitmap Fonts. You may be saying to yourself "what's so hard about putting text onto the screen". If you've ever tried it, it's not that easy! Sure you can load up an

nehe.gamedev.net

GIS DEVELOPER, "Output String on the Screen(Bitmap)", www.gisdeveloper.co.kr/?p=29, 2020년 11월 6일 접속

 

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

OpenGL에서 문자를 출력해야 할 경우가 있다. 그렇다면 OpenGL에서는 문자를 출력하기 위한 루틴이 준비되어져 있는가? 대답은 “전혀, 아니다” 이다. 그 이유는 각 플랫폼 OS에 따라 문자를 출력하

www.gisdeveloper.co.kr

"OpenGL glLogicOp란? 고무줄 기법, Rubber banding", sangmin-kim.tistory.com/121, 2020년 11월 6일 접속

 

OpenGL glLogicOp란 ? 고무줄 기법, Rubber banding

1. OpenGL glLogicOp란 ? glLogicOp 함수는 컬러 인덱스 렌더링을 위한 논리적 픽셀 연산을 한다. glLogicOp는 들어오는 색상 RGBA 색상과 프레임 버퍼의 해당 위치에서 RGBA 색상간에 적용되는 논리적 연산을

sangmin-kim.tistory.com

 

댓글