상세 컨텐츠

본문 제목

FreeType으로 Direct3D에 글자 출력하기

프로그래밍/3D

by ∫2tdt=t²+c 2013. 6. 1. 04:13

본문




FreeType을 이용해서 Direct3D에 폰트를 출력하는 방법에 대해서 자세한 문의가 자주 들어오더라구요. 

(2013/04/06 - [프로그래밍] - Direct3D에 FreeType로 폰트 출력하기) 그래서 혹시나마 도움이 되고자 그나마 얕은 코드 지식 드러내 가며 글을 씁니다.

요 코드는 이전에 개발하던 리듬게임과 현재 개발하는 포니게임에 사용하고 있는 자체제작 엔진인 Interstella의 일부분을 발췌한 것입니다. c 11코드와 ISI 클래스의 메소드들이 사용되어서 난해할수 있지만, 최대한 주석을 달고 넘어갈테니 알아서 보시길ㅠㅠ (저걸 일일히 D3D용으로 바꿔서 올릴만큼 잉여력이 있지는 않아서)

코드를 보기에 앞서 어떤 방식으로 구현을 했는지 간단하게 설명을 하자면,


1. 1024x1024 혹은 2048x2048 크기의 A8R8G8B8 포맷의 텍스쳐를 생성합니다.

2. 화면에 찍어야하는 글자가 있다면 이 텍스쳐에 Lock을 걸고 글리프를 새깁니다.

3. 이 텍스쳐를 화면에 찍어서 글자를 표시합니다.

4. 다음 번에 그 글자를 또 찍을 때는 이미 텍스쳐에 글리프가 새겨져 있으므로, 다시 Lock을 걸 필요가 없습니다. (캐싱 정책)


이를 위해서는 폰트와는 독립적으로 존재하는 글리프들을 담아두는 텍스쳐를 관리할 필요가 있습니다. 이를 GlyphBank라고 이름붙일게요. 이 클래스가 하는 일은 다음과 같습니다

1. 텍스쳐의 생성 및 해제

2. OnLostDevice, OnResetDevice에 대처하는 일

3. 찍을 문자 글리프가 캐시되어 있는지 확인하는 일

4. 캐시되어 있지 않다면 Lock을 걸고 텍스쳐에 글리프를 새기는 일

5. 새로운 글자를 찍어야 하는데 글리프를 새길 텍스쳐가 모자라다면 텍스쳐를 비우고 다시 글리프를 새기는 일


물론 GlyphBank 클래스만으로 폰트를 출력할 수는 없습니다. 실제 폰트출력을 담당하는 클래스가 필요하죠. FontFT라고 이름붙일게요. 이 클래스가 하는 일은 다음과 같습니다.

1. 폰트 파일을 열어서 폰트 데이터를 읽어온다.

2. GlyphBank에서 얻은 정보를 통해 실제 화면에 폰트를 출력한다.



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
/*
ISGlyphBank 클래스
 
FreeType 라이브러리와 Glyph를 담은 텍스쳐를 관리하는 클래스.
*/
class ISGlyphBank
{
public:
    struct Glyph
    {
        FT_Face face;
        wchar_t chr;
        int size;
        bool operator < (const Glyph& g) const
        {
            if(face < g.face) return true;
            else if(face > g.face) return false;
            if(chr < g.chr) return true;
            else if(chr > g.chr) return false;
            if(size < g.size) return true;
            return false;
        }
    };
//글리프 데이터입니다. 글꼴 모양인 face와 문자 chr, 글꼴크기 size면 하나의 글리프를 지정하기에 충분하죠?
//기울임이나 굵게 출력 같은 효과는 넣지 않았습니다. 나중에 필요해지면 추가하죠 뭐
    struct GlyphData
    {
        float u, v, w, h;
        int left, top;
        int bmW, bmH;
        int advX, advY;
        int height;
        int use;
    };
//캐시된 글리프에 대한 정보입니다. u,v,w,h는 글리프가 차지하는 텍스쳐 UV좌표상의 지점입니다.
//left, top은 찍을 글리프의 왼쪽/위쪽 여백이구요
//bmW, bmH는 글리프의 가로 픽셀, 세로 픽셀 갯수
//advX, advY는 이 글리프를 찍은 뒤 다음 문자를 찍기 위해 움직여야하는 x,y 변위입니다.
//height는 문자의 높이
//use는 텍스쳐 출력에 사용되었는지 여부입니다. (GC함수를 위해있는거임)
protected:
    ISI* m_pIS; //Interstella 엔진 포인터입니다.
    FT_Library m_ftL; // FreeType 라이브러리죠
    ISTexture* m_ftTexture; //Interstella 텍스쳐입니다. Direct3DTexture9를 래핑해놓은거에 불과해요.
    D3DLOCKED_RECT m_lr;
    int m_width; //텍스쳐 가로 크기입니다. 세로 크기는 가로랑 같아요
    std::vector<char> m_tMap; //텍스쳐의 특정 영역이 사용중인지를 저장해두는 텍스쳐맵입니다.
    std::map<Glyph, GlyphData> m_glyphPos; //캐싱된 글리프 데이터를 저장해둡니다.
     
    const char& MapAt(int x, int y) const {return m_tMap[x   y*(m_width/4)];}
    char& MapAt(int x, int y) {return m_tMap[x   y*(m_width/4)];}
    bool TestSpace(int x, int y, int w, int h) const;
// (x, y)에서 (x w, y h)에 이르는 텍스쳐 영역이 비어있으면 true
    void FillSpace(int x, int y, int w, int h, char d = 1);
// (x, y)에서 (x w, y h)에 이르는 텍스쳐 영역을 사용하면 d=1, 비우려면 d=0으로 설정
    std::pair<int, int> FindSpace(int w, int h) const;
// (w, h) 크기의 사각형이 들어갈 수 있는 공간을 찾습니다
public:
    ISGlyphBank(ISI* pIS, int width);
    ~ISGlyphBank();
    void OnLostDevice();
    void OnResetDevice();
    FT_Library GetFTL() const {return m_ftL;}
    ISTexture* GetFTTexture() const {return m_ftTexture;}
    int Lock();
    void Unlock();
    std::wstring PrepareGlyph(FT_Face face, LPCWSTR str, int len, int size);
//문자열 str을 사용할 수 있도록 텍스쳐에 캐시하도록 합니다.
//만약 폰트에 글리프가 포함되지 않은 문자가 있으면 다시 반환해줍니다.
    GlyphData GetGlyph(FT_Face face, wchar_t chr, int size);
//문자의 글리프를 얻어냅니다.
    void NewFrame();
//이 클래스에게 새로운 프레임이 시작되었음을 알립니다.
    void GC();
//사용하지 않는 글리프 캐시를 삭제합니다.
};



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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
ISGlyphBank::ISGlyphBank(ISI* pIS, int width) : m_pIS(pIS), m_width(width)
{
    FT_Init_FreeType(&m_ftL);
    m_ftTexture = m_pIS->CreateDynamicTexture(width, width);
//텍스쳐를 생성하는 함수입니다.
//D3DXCreateTexture(m_pd3dd, width, height, 1, D3DUSAGE_DYNAMIC, D3DFMT_A8R8G8B8, D3DPOOL_DEFAULT, &pt)
//와 같은 역할을 합니다.
    m_tMap.resize((width / 4) * (width / 4));
//텍스쳐맵의 크기를 조정합니다. 1픽셀마다 사용정보를 저장하면 메모리 낭비가 심하기에
//4x4픽셀을 한 블럭으로 묶어서 사용합니다.
    m_lr.pBits = nullptr;
}
 
ISGlyphBank::~ISGlyphBank()
{
    if(m_ftTexture)
    {
        delete m_ftTexture;
    }
    FT_Done_FreeType(m_ftL);
}
 
void ISGlyphBank::OnLostDevice()
{
    SAFE_DELETE(m_ftTexture);
}
 
void ISGlyphBank::OnResetDevice()
{
    m_ftTexture = m_pIS->CreateDynamicTexture(m_width, m_width);
    m_glyphPos.clear();
    m_tMap.clear();
    m_tMap.resize((m_width / 4) * (m_width / 4));
}
 
bool ISGlyphBank::TestSpace(int x, int y, int w, int h) const
{
//텍스쳐 맵을 검색하여, (x, y)에서 (x w, y h)의 공간 중 한 블럭이라도 사용중이면 false반환합니다.
    if(x   w >= m_width / 4) return false;
    if(y   h >= m_width / 4) return false;
 
    for(int i = x; i < x w;   i)
    {
        for(int j = y; j < y h;   j)
        {
            if(MapAt(i, j)) return false;
        }
    }
    return true;
}
 
void ISGlyphBank::FillSpace(int x, int y, int w, int h, char d)
{
//텍스쳐 맵을 채웁니다.
    for(int i = x; i < x w;   i)
    {
        for(int j = y; j < y h;   j)
        {
            MapAt(i, j) = d;
        }
    }
}
 
std::pair<int, int> ISGlyphBank::FindSpace(int w, int h) const
{
// (w, h)크기의 사각형이 들어갈 수 있는 공간을 찾습니다.
// 뭔가 더 효율적인 알고리즘이 있을거같은데ㅠㅠ 모르겠어서 일단 브루트포스로.
    for(int j = 0; j < m_width / 4;   j)
    {
        for(int i = 0; i < m_width / 4;   i)
        {
            if(TestSpace(i, j, w, h))
            {
                return std::make_pair(i, j);
            }
        }
    }
    return std::make_pair(-1, -1);
}
 
int ISGlyphBank::Lock()
{
//텍스쳐에 락을 걸어요.
//m_lr이 세팅되어있을 경우 이미 락이 걸려있는 겁니다.
    if(m_lr.pBits) return -1;
    m_ftTexture->GetD3DTexture()->LockRect(0, &m_lr, nullptr, 0);
    return 0;
}
 
void ISGlyphBank::Unlock()
{
//텍스쳐 락을 풀어요
    if(!m_lr.pBits)return;
    m_ftTexture->GetD3DTexture()->UnlockRect(0);
    m_lr.pBits = nullptr;
}
 
//캐시된 글리프가 있으면 그걸 반환하고, 없으면 텍스쳐에 새겨서 반환해주는 함수입니다
ISGlyphBank::GlyphData ISGlyphBank::GetGlyph(FT_Face face, wchar_t chr, int size)
{
//글리프 데이터입니다.
    Glyph g = {face, chr, size};
    GlyphData gd = {0, };
//글리프를 캐시에서 찾아요
    auto it = m_glyphPos.find(g);
    if(it != m_glyphPos.end())
    {
        it->second.use = 1;
        return it->second;
//만약 있다면, 그냥 그 정보를 리턴하면 끝
    }
//없을 경우는 텍스쳐에 새기고 캐싱해야겠죠.
//문자 chr의 글리프 인덱스를 얻습니다
    FT_UInt ix = FT_Get_Char_Index(face, chr);
//인덱스가 0이라는건 문자가 없다는거에요. 그냥 gd를 리턴하고 훼일
    if(ix == 0)
    {
        return gd;
    }
//글리프를 읽어들여요.
    if(FT_Load_Glyph(face, ix, FT_LOAD_RENDER | FT_LOAD_NO_BITMAP))
    {
//실패하면 gd를 리턴하고 훼일
        return gd;
    }
//글리프에 대한 정보를 알아냅니다.
    gd.bmW = face->glyph->bitmap.width;
    gd.bmH = face->glyph->bitmap.rows;
 
//해당 크기의 글리프가 들어갈 공간을 찾아요.
//텍스쳐 맵은 4x4픽셀을 한 블럭으로 한다고 그랬으니,
//픽셀단위의 bmW, bmH를 블럭 단위로 바꿔줘야겠죠
//그래서 3을 더하고 4로 나눈거에요.
    auto p = FindSpace((gd.bmW   3) / 4, (gd.bmH   3) / 4);
//빈 공간이 없으면 훼일입니다.
    if(p.first < 0 ) return gd;
//그렇지 않으면 이제 다 됐어요.
//나머지 정보 입력하고
    gd.advX = face->glyph->advance.x >> 6;
    gd.advY = face->glyph->advance.y >> 6;
    gd.height = face->height >> 6;
    gd.left = face->glyph->bitmap_left;
    gd.top = -face->glyph->bitmap_top;
    gd.u = p.first*4 / (float)m_width;
    gd.v = p.second*4 / (float)m_width;
    gd.w = gd.bmW / (float)m_width;
    gd.h = gd.bmH / (float)m_width;
    gd.use = 1;
 
    if(Lock() == 0)
    {
//락을 걸어서
        DWORD* pBits = (DWORD*)m_lr.pBits;
        for(int j = 0; j < gd.bmH;   j)
        {
            for(int i = 0; i < gd.bmW;   i)
            {
//텍스쳐의 해당 지점의 픽셀을 설정해줘요. 색은 흰색으로, 알파값만 조절해주면 됩니다.
                BYTE b = face->glyph->bitmap.buffer[i   j*gd.bmW];
                pBits[(i p.first*4)   (j p.second*4)*m_lr.Pitch/4] = D3DCOLOR_ARGB(b, 255, 255, 255);
            }
        }
        Unlock();
    }
//이제 이 공간은 글리프가 캐시되어 있으니, 사용중이라고 텍스쳐맵에 표시를 합시다
    FillSpace(p.first, p.second, (gd.bmW 3)/4, (gd.bmH 3)/4);
//그리고 캐시에 글리프 정보를 넣어두고
    m_glyphPos[g] = gd;
//반환하면서 끝
    return gd;
}
 
//사전에 문자 글리프를 캐싱해주는 함수에요.
//글리프 데이터가 없어서 캐시하지 못한 문자를 모아 wstring으로 만들어 반환해줍니다.
std::wstring ISGlyphBank::PrepareGlyph(FT_Face face, LPCWSTR str, int len, int size)
{
    std::wstring ret;
    for(int t = 0; t < len;   t)
    {
//글리프를 설정하구요
        Glyph g = {face, str[t], size};
//일단 캐시에 있나 찾아봅니다.
        auto it = m_glyphPos.find(g);
        if(it != m_glyphPos.end())
        {
//있으면 다음문자로 건너뛰고
            continue;
        }
//글리프 인덱스를 얻어요
        FT_UInt ix = FT_Get_Char_Index(face, str[t]);
        if(ix == 0)
        {
//인덱스가 0인건, 글리프가 없다는 거죠.
//반환할 목록에 추가
            ret.push_back(str[t]);
            continue;
        }
//글리프를 읽어오고
        if(FT_Load_Glyph(face, ix, FT_LOAD_RENDER | FT_LOAD_NO_BITMAP))
        {
            continue;
        }
//이제 여기서부터는 GetGlyph함수랑 같네요
        GlyphData gd = {0, };
        gd.bmW = face->glyph->bitmap.width;
        gd.bmH = face->glyph->bitmap.rows;
     
        auto p = FindSpace((gd.bmW   3) / 4, (gd.bmH   3) / 4);
        if(p.first < 0 )
        {
            GC();
            p = FindSpace((gd.bmW   3) / 4, (gd.bmH   3) / 4);
            if(p.first < 0) continue;
        }
 
        gd.advX = face->glyph->advance.x >> 6;
        gd.advY = face->glyph->advance.y >> 6;
        gd.height = face->size->metrics.height >> 6;
        gd.left = face->glyph->bitmap_left;
        gd.top = -face->glyph->bitmap_top;
        gd.u = p.first*4 / (float)m_width;
        gd.v = p.second*4 / (float)m_width;
        gd.w = gd.bmW / (float)m_width;
        gd.h = gd.bmH / (float)m_width;
        gd.use = 0;
 
//락이 안 걸려있으면 걸어줘요
        if(!m_lr.pBits) Lock();
 
        DWORD* pBits = (DWORD*)m_lr.pBits;
        for(int j = 0; j < gd.bmH;   j)
        {
            for(int i = 0; i < gd.bmW;   i)
            {
                BYTE b = face->glyph->bitmap.buffer[i   j*gd.bmW];
                pBits[(i p.first*4)   (j p.second*4)*m_lr.Pitch/4] = D3DCOLOR_ARGB(b, 255, 255, 255);
            }
        }
        FillSpace(p.first, p.second, (gd.bmW 3)/4, (gd.bmH 3)/4);
        m_glyphPos[g] = gd;
    }
//어차피 락은 한번만 걸고, 한번만 풀수 있는거니, 루프를 다 돌고나서 풀어주는게 좋죠
//매 루프마다 락 걸고 풀고 반복하면 무지 느려집니다.
    if(m_lr.pBits)Unlock();
    return ret;
}
 
//새로운 프레임이 시작할때 호출해줍니다.
//모든 캐시된 글리프의 사용횟수를 0으로 설정합니다.
void ISGlyphBank::NewFrame()
{
    for(auto& p : m_glyphPos)
    {
        p.second.use = 0;
    }
}
 
//사용되지 않는 글리프를 모두 날려버려요.
void ISGlyphBank::GC()
{
    std::map<Glyph, GlyphData> ng;
    for(auto p : m_glyphPos)
    {
        if(p.second.use == 1)
        {
//쓰이고 있으면 ng에 넣어두고
            ng.insert(p);
        }
        else
        {
//그렇지 않으면 그 텍스쳐 맵을 과감하게 날려버립니다. 방빼야죠
            FillSpace(p.second.u * m_width / 4, p.second.v * m_width / 4, (p.second.bmW   3) /4, (p.second.bmH   3) / 4, 0);
        }
    }
//스왑해서 마무리
    m_glyphPos.swap(ng);
}


길었죠... 이렇게 글리프 뱅크를 관리하는 클래스를 만들었구요

이 뱅크를 이용해서 실제로 폰트를 찍어주는 클래스가 필요하겠죠



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
/*
ISFontFT 클래스 : ISFont 상속
 
FreeType 라이브러리를 이용하여 폰트를 출력하는 클래스.
ISFont 클래스는 Interstella 엔진에서 폰트를 출력하는 기능을 갖춘 추상클래스입니다.
*/
 
class ISFontFT : public ISFont
{
    friend class ISI;
protected:
    ISI* m_pIS; //Interstella 엔진 포인터. ISGlyphBank의 인스턴스는 요놈이 가지고 있습니다.
//m_pIS->GetGlyphBank()로 얻어낼수 있어요
    ISData* m_data[3]; //파일 포인터를 관리하는 클래스입니다. 폰트 데이터를 담아요
    int m_height; //폰트 크기
    float m_scale; //폰트 스케일 (별 의미없는거에요)
    FT_Face m_face[3]; //FreeType Face
    ISFontFT(ISI* pIS, ISData* font, size_t index, ISData* font2 = nullptr, size_t index2 = 0, ISData* font3 = nullptr, size_t index3 = 0);
public:
    ~ISFontFT();
    void OnLostDevice() override {}
    void OnResetDevice() override {}
    int PrintText(LPCWSTR str, RECT* rect, IS_COLOR color, DWORD format = 0) override;
//텍스트를 출력하는 함수죠
    void SetHeight(int newHeight) {m_height = newHeight;}
    FT_Library GetFTL() const;
    ISTexture* GetFTTexture() const;
    void SetScale(float scale) override {m_scale = scale;}
};


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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
FT_Library ISFontFT::GetFTL() const
{
    return m_pIS->GetGlyphBank()->GetFTL();
}
ISTexture* ISFontFT::GetFTTexture() const
{
    return m_pIS->GetGlyphBank()->GetFTTexture();
}
 
ISFontFT::ISFontFT(ISI* pIS, ISData* font, size_t index, ISData* font2, size_t index2, ISData* font3, size_t index3) : m_pIS(pIS), m_height(8)
{
    m_scale = 1;
    m_data[0] = font;
    m_data[1] = font2;
    m_data[2] = font3;
//한 놈이 폰트를 최대 3개까지 열 수 있어요.
//CSS에서 대체폰트를 사용하는 것 마냥, 글리프가 없는 문자일 경우 다음 폰트를 사용해 출력하게 됩니다
    if(FT_New_Memory_Face(GetFTL(), (const FT_Byte*)font->GetSrc(), font->GetSize(), index, &m_face[0]))
    {
        throw "Failed";
    }
     
    if(font2)
    {
        FT_New_Memory_Face(GetFTL(), (const FT_Byte*)font2->GetSrc(), font2->GetSize(), index, &m_face[1]);
    }
    else
    {
        m_face[1] = nullptr;
    }
 
    if(font3)
    {
        FT_New_Memory_Face(GetFTL(), (const FT_Byte*)font3->GetSrc(), font3->GetSize(), index, &m_face[2]);
    }
    else
    {
        m_face[2] = nullptr;
    }
}
 
ISFontFT::~ISFontFT()
{
//해제될때 지워주는거 잊지마요
    for(int i = 0; i < 3;   i)
    {
        if(m_face[i])
        {
            FT_Done_Face(m_face[i]);
            m_face[i] = nullptr;
        }
        SAFE_DELETE(m_data[i]);
    }
}
 
int ISFontFT::PrintText(LPCWSTR str, RECT* rect, IS_COLOR color, DWORD format)
{
//폰트의 크기를 설정해요. 이때 m_face[0]->size 구조체가 세팅됩니다.
    FT_Set_Pixel_Sizes(m_face[0], m_height * m_scale, m_height * m_scale);
//세팅된 size 구조체에서 크기에 대한 정보를 얻어내요.
//값은 고정소수점 26.6포맷으로 되어있으므로 64로 나누어서 float로 바꿔줘야합니다.
    float fHeight = m_face[0]->size->metrics.height / 64.f;
//전체 폰트의 높이 : 줄간격으로 사용할 거에요
    float fAsc = m_face[0]->size->metrics.ascender / 64.f;
//기준 선으로부터 위로 올라간 최대 높이 : 폰트 높이를 맞추는데 중요합니다.
    float fDsc = m_face[0]->size->metrics.descender / 64.f;
//기준 선으로부터 아래로 내려간 최대 높이
 
//텍스쳐에 알파가 들어가 있으므로, 이거 설정해주고 가야돼요.
//폰트 출력하기에 앞서 한번만 출력하면 되지만, 그것까지 하면 머리아플까봐 일단은 여기에 쳐박아 놨습니다.
    m_pIS->GetDevice()->SetRenderState(D3DRS_ALPHATESTENABLE, true);
    m_pIS->GetDevice()->SetRenderState(D3DRS_ALPHAFUNC, D3DCMP_GREATER);
    m_pIS->GetDevice()->SetRenderState(D3DRS_ALPHAREF, 0);
    m_pIS->GetDevice()->SetRenderState(D3DRS_ALPHABLENDENABLE, true);
    m_pIS->GetDevice()->SetRenderState(D3DRS_BLENDOP, D3DBLENDOP_ADD);
    m_pIS->GetDevice()->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
    m_pIS->GetDevice()->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
//다양한 색깔의 폰트를 출력하기 위해서 MODULATE!
//ISGlyphBank에서 글자를 흰색으로 썼던거 기억나죠?
//거기에 COLOR로 넘어가는 인자를 곱해주면 다양한 색으로 문자를 출력할 수 있게되는거죠
    m_pIS->GetDevice()->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_MODULATE);
     
//폰트 출력의 시작 위치입니다. x는 박스의 왼쪽부터, y는 박스의 위쪽에서 fAsc만큼 내려와서 기준선에 맞춥니다.
    float x = rect->left, y = rect->top   fAsc;
//글리프를 준비해요.
    std::wstring m2 = m_pIS->GetGlyphBank()->PrepareGlyph(m_face[0], str, wcslen(str), m_height * m_scale);
    if(m_face[1])
    {
//만약 예비 폰트가 있다면 예비 폰트도 세팅하고, 글리프가 없는 문자들은 예비폰트에서 글리프를 준비하도록합니다.
        FT_Set_Pixel_Sizes(m_face[1], m_height * m_scale, m_height * m_scale);
        m2 = m_pIS->GetGlyphBank()->PrepareGlyph(m_face[1], m2.c_str(), m2.size(), m_height * m_scale);
        if(m_face[2])
        {
            FT_Set_Pixel_Sizes(m_face[2], m_height * m_scale, m_height * m_scale);
            m_pIS->GetGlyphBank()->PrepareGlyph(m_face[2], m2.c_str(), m2.size(), m_height * m_scale);
        }
    }
 
//줄별 가로 길이를 저장합니다.
    std::vector<int> width;
    width.push_back(0);
//실제 출력은 좀 이따가 하고, 먼저 매트릭스를 계산합니다.
//가로 가운데 정렬이거나, 세로 가운데 정렬일 경우엔 다음과 같이 계산
    if(format & DT_CENTER || format & DT_VCENTER)
    {
        for(LPCWSTR p = str;*p;  p)
        {
            if(*p == '\n')
            {
//줄 바꿈 처리
                width.push_back(0);
                continue;
            }
//글리프데이터를 가져와요
            ISGlyphBank::GlyphData gd = m_pIS->GetGlyphBank()->GetGlyph(m_face[0], *p, m_height * m_scale);
            if(gd.bmW == 0 && m_face[1])
            {
                gd = m_pIS->GetGlyphBank()->GetGlyph(m_face[1], *p, m_height * m_scale);
                if(gd.bmW == 0 && m_face[2])
                {
                    gd = m_pIS->GetGlyphBank()->GetGlyph(m_face[2], *p, m_height * m_scale);
                }
            }
 
//그 줄에 더 출력할 수 없으면, 자동 줄바꿈을 해줍니다.
            if(width.back()   gd.bmW   gd.left >= rect->right)
            {
                width.push_back(0);
            }
//문자 폭만큼 가로 길이를 늘려줘야겠지요
            width.back()  = gd.advX;
        }
    }
 
//매트릭스 계산이 끝났으니, 문자 찍을 위치를 재정비해봅시다.
 
//가로 가운데 정렬일 경우 x값을 재조정해줘요
    if(format & DT_CENTER)
    {
        x = (rect->right   rect->left - width[0]) / 2;
    }
//세로 가운데 정렬일 경우 y값을 재조정해줘요
    if(format & DT_VCENTER)
    {
        y = (rect->bottom   rect->top - width.size() * fHeight) / 2   fAsc;
    }
 
//이제 진짜 출력입니다 헤
    int line = 0;
    for(;*str;  str)
    {
        if(*str == '\n')
        {
//줄바꿈 처리
              line;
            if(format & DT_CENTER)
            {
                x = ((rect->right   rect->left) - width[line]) / 2;
            }
            else
            {
                x = rect->left;
            }
            y  = m_height;
            continue;
        }
        ISGlyphBank::GlyphData gd = m_pIS->GetGlyphBank()->GetGlyph(m_face[0], *str, m_height * m_scale);
        if(gd.bmW == 0 && m_face[1])
        {
            gd = m_pIS->GetGlyphBank()->GetGlyph(m_face[1], *str, m_height * m_scale);
            if(gd.bmW == 0 && m_face[2])
            {
                gd = m_pIS->GetGlyphBank()->GetGlyph(m_face[2], *str, m_height * m_scale);
            }
        }
 
        //오른쪽으로 더 출력할 수 없으면, 자동 줄바꿈
        if(x   gd.bmW   gd.left >= rect->right)
        {
              line;
            if(format & DT_CENTER && line < width.size())
            {
                x = ((rect->right   rect->left) - width[line]) / 2;
            }
            else
            {
                x = rect->left;
            }
            y  = fHeight;
        }
//텍스쳐를 찍어주는 ISI 메소드입니다. 화면 상의 좌표 (x, y)와 크기 (w, h)를 받고,
//텍스쳐 UV와 UV상의 w,h를 받고, 출력할 색상을 받아 출력해줍니다.
//아래쪽에 함수 몸체 넣어뒀으니 참고하시길 바래요.
        m_pIS->DrawTexture(GetFTTexture(), x   gd.left, y   gd.top, gd.bmW, gd.bmH,
            gd.u, gd.v, gd.w, gd.h, color);
        x  = gd.advX;
        y  = gd.advY;
    }
    return 0;
}
 
int ISI::DrawTexture(ISTexture* texture, float destX, float destY, float destW, float destH,
                    float srcU, float srcV, float srcW, float srcH, D3DCOLOR color)
{
    SelectTexture(texture->m_pTexture);
//같은 텍스쳐임에도 SetTexture 호출이 발생하지 않도록
//텍스쳐가 바뀔때만 SetTexture를 호출해주는 함수입니다.
    SelectFVF(D3DFVF_XYZRHW | D3DFVF_TEX1 | D3DFVF_DIFFUSE);
//SetFVF도 자주 호출하면 해로울테니, 바뀔 때만 호출하도록
    struct {
        float x, y, z, w;
        D3DCOLOR c;
        float u, v;
    } v[4] = {
        {destX   destW - .5f, destY - .5f, 0, 1.f, color, srcU   srcW, srcV,},
        {destX - .5f, destY - .5f, 0, 1.f, color, srcU, srcV,},
        {destX   destW - .5f, destY   destH - .5f, 0, 1.f, color, srcU   srcW, srcV   srcH,},
        {destX - .5f, destY   destH - .5f, 0, 1.f, color, srcU, srcV   srcH,},
    };
    if(m_pd3dd->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, v, 28)) return -1;
    return 0;
}


ISGlyphBank는 당연히 하나만 생성되어야하므로, ISI 엔진에서 시작할때 하나 생성하고, 끝날때 파괴합니다. 이식하실 분들은 전역변수로 생성하시던지 싱글톤을 이용하시면 될듯하네요.


난장판인 코드를 보느라 수고 많으셨습니다. ㅠㅠ


이렇게해서 폰트를 출력하면 짜잔.

중간에 있는 조그만 글씨들이 모여있는게, GlyphBank의 텍스쳐입니다. 저런식으로 캐시가 됩니다. 처음에는 텍스쳐를 새기느라 약간 버벅이지만, 그 뒤로는 거의 끊김이 없죠. 알파벳과 숫자 기호만 사용한다면 캐시가 넘칠일은 없습니다...만 한글 같은 경우는 워낙 문자가 많아서 넘치는 일이 발생하곤 합니다. 1024x1024 텍스쳐를 가로 세로 32 픽셀인 글리프로 가득채운다고 해도 1024개가 들어갑니다. 근데 보통은 더 작은 크기로 폰트를 출력하고, 자주 사용하는 문자만 자주 사용되기 때문에 너무 걱정안하셔도 됩니다. 정 그렇게 걱정되면 2048x2048혹은 2048x1024 크기의 텍스쳐를 이용하는 것도 방법일듯합니다.

관련글 더보기

댓글 영역