Mercurial > projects > mde
comparison mde/font/FontTexture.d @ 63:66d555da083e
Moved many modules/packages to better reflect usage.
author | Diggory Hardy <diggory.hardy@gmail.com> |
---|---|
date | Fri, 27 Jun 2008 18:35:33 +0100 |
parents | mde/resource/FontTexture.d@7cab2af4ba21 |
children | 2813ac68576f |
comparison
equal
deleted
inserted
replaced
62:960206198cbd | 63:66d555da083e |
---|---|
1 /* LICENSE BLOCK | |
2 Part of mde: a Modular D game-oriented Engine | |
3 Copyright © 2007-2008 Diggory Hardy | |
4 | |
5 This program is free software: you can redistribute it and/or modify it under the terms | |
6 of the GNU General Public License as published by the Free Software Foundation, either | |
7 version 2 of the License, or (at your option) any later version. | |
8 | |
9 This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; | |
10 without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | |
11 See the GNU General Public License for more details. | |
12 | |
13 You should have received a copy of the GNU General Public License | |
14 along with this program. If not, see <http://www.gnu.org/licenses/>. */ | |
15 | |
16 /** Font caching system. | |
17 * | |
18 * This module also serves as the internals to the font module and shouldn't be used except through | |
19 * the font module. The two modules could be combined, at a cost to readability. | |
20 * | |
21 * Three types of coordinates get used in the system: FreeType coordinates for each glyph, texture | |
22 * coordinates, and OpenGL's model/world coordinates (for rendering). The freetype and texture | |
23 * coords are cartesian (i.e. y increases upwards), although largely this is too abstract to | |
24 * matter. However, for the model/world coords, y increases downwards. */ | |
25 module mde.font.FontTexture; | |
26 | |
27 import mde.types.Colour; | |
28 import mde.lookup.Options; | |
29 import mde.font.exception; | |
30 | |
31 import derelict.freetype.ft; | |
32 import derelict.opengl.gl; | |
33 | |
34 import Utf = tango.text.convert.Utf; | |
35 import tango.util.log.Log : Log, Logger; | |
36 | |
37 private Logger logger; | |
38 static this () { | |
39 logger = Log.getLogger ("mde.font.FontTexture"); | |
40 } | |
41 | |
42 static const int dimW = 256, dimH = 256; // Texture size | |
43 const wFactor = 1f / dimW; | |
44 const hFactor = 1f / dimH; | |
45 | |
46 /** A FontTexture is basically a cache of all font glyphs rendered so far. | |
47 * | |
48 * This class should be limited to code for rendering to (and otherwise handling) textures and | |
49 * rendering fonts to the screen. | |
50 * | |
51 * Technically, there's no reason it shouldn't be a static part of the FontStyle class. */ | |
52 class FontTexture | |
53 { | |
54 this () {} | |
55 ~this () { | |
56 foreach (t; tex) { | |
57 glDeleteTextures (1, &(t.texID)); | |
58 } | |
59 } | |
60 | |
61 // Call if font(s) have been changed and glyphs must be recached. | |
62 void clear () { | |
63 foreach (t; tex) { | |
64 glDeleteTextures (1, &(t.texID)); | |
65 } | |
66 cachedGlyphs = null; | |
67 ++cacheVer; | |
68 } | |
69 | |
70 | |
71 /** Cache informatation for rendering a block of text. | |
72 * | |
73 * Recognises '\r', '\n' and "\r\n" as end-of-line markers. */ | |
74 void updateCache (FT_Face face, char[] str, ref TextBlock cache) | |
75 { | |
76 debug scope (failure) | |
77 logger.error ("updateCache failed"); | |
78 | |
79 if (cache.cacheVer == cacheVer) // Existing cache is up-to-date | |
80 return; | |
81 | |
82 cache.cacheVer = cacheVer; | |
83 | |
84 /* Convert the string to an array of character codes (which is equivalent to decoding UTF8 | |
85 * to UTF32 since no character code is ever > dchar.max). */ | |
86 static dchar[] chrs; // keep memory for future calls (NOTE: change for threading) | |
87 chrs = Utf.toString32 (str, chrs); | |
88 | |
89 // Allocate space. | |
90 // Since end-of-line chars get excluded, will often be slightly larger than necessary. | |
91 cache.chars.length = chrs.length; | |
92 cache.chars.length = 0; | |
93 | |
94 int lineSep = face.size.metrics.height >> 6; | |
95 bool hasKerning = (FT_HAS_KERNING (face) != 0); | |
96 int y = 0; | |
97 CharCache cc; // struct; reused for each character | |
98 | |
99 for (size_t i = 0; i < chrs.length; ++i) | |
100 { | |
101 // First, find yMax for the current line. | |
102 int yMax = 0; // Maximal glyph height above baseline. | |
103 for (size_t j = i; j < chrs.length; ++j) | |
104 { | |
105 if (chrs[j] == '\n' || chrs[j] == '\r') // end of line | |
106 break; | |
107 | |
108 GlyphAttribs* ga = chrs[j] in cachedGlyphs; | |
109 if (ga is null) { // Not cached | |
110 addGlyph (face, chrs[j]); // so render it | |
111 ga = chrs[j] in cachedGlyphs; // get the ref of the copy we've stored | |
112 assert (ga !is null, "ga is null: 1"); | |
113 } | |
114 | |
115 if (ga.top > yMax) | |
116 yMax = ga.top; | |
117 } | |
118 y += yMax; | |
119 | |
120 // Now for the current line: | |
121 int x = 0; // x pos for next glyph | |
122 uint gi_prev = 0; // previous glyph index (needed for kerning) | |
123 for (; i < chrs.length; ++i) | |
124 { | |
125 // If end-of-line, break to find yMax for next line. | |
126 if (chrs.length >= i+2 && chrs[i..i+2] == "\r\n"d) { | |
127 ++i; | |
128 break; | |
129 } | |
130 if (chrs[i] == '\n' || chrs[i] == '\r') { | |
131 break; | |
132 } | |
133 | |
134 cc.ga = chrs[i] in cachedGlyphs; | |
135 assert (cc.ga !is null, "ga is null: 2"); | |
136 | |
137 // Kerning | |
138 if (hasKerning && (gi_prev != 0)) { | |
139 FT_Vector delta; | |
140 FT_Get_Kerning (face, gi_prev, cc.ga.index, FT_Kerning_Mode.FT_KERNING_DEFAULT, &delta); | |
141 x += delta.x >> 6; | |
142 } | |
143 | |
144 // ga.left component: adding this slightly improves glyph layout. Note that the | |
145 // left-most glyph on a line may not start right on the edge, but this looks best. | |
146 cc.xPos = x + cc.ga.left; | |
147 cc.yPos = y - cc.ga.top; | |
148 x += cc.ga.advanceX; | |
149 | |
150 cache.chars ~= cc; | |
151 | |
152 // Update rect total size. Top and left coords should be zero, so make width and | |
153 // height maximal x and y coordinates. | |
154 if (cc.xPos + cc.ga.w > cache.w) | |
155 cache.w = cc.xPos + cc.ga.w; | |
156 if (cc.yPos + cc.ga.h > cache.h) | |
157 cache.h = cc.yPos + cc.ga.h; | |
158 } | |
159 // Now increment i and continue with the next line if there is one. | |
160 y += lineSep - yMax; | |
161 } | |
162 } | |
163 | |
164 /** Render a block of text using a cache. Updates the cache if necessary. | |
165 * | |
166 * Params: | |
167 * face = Current typeface pointer; must be passed from font.d (only needed if the cache is | |
168 * invalid) | |
169 * str = Text to render (only needed if the cache is invalid) | |
170 * cache = Cache used to speed up CPU-side rendering code | |
171 * x = Smaller x-coordinate of position | |
172 * y = Smaller y-coordinate of position | |
173 * col = Text colour (note: currently limited to black or white) */ | |
174 void drawCache (FT_Face face, char[] str, ref TextBlock cache, int x, int y, Colour col ) { | |
175 updateCache (face, str, cache); // update if necessary | |
176 debug scope (failure) | |
177 logger.error ("drawTextCache failed"); | |
178 | |
179 // opaque (GL_DECAL would be equivalent) | |
180 glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); | |
181 | |
182 drawCacheImpl (cache, x, y, col); | |
183 } | |
184 /** A variation of drawCache, for transparent text. | |
185 * | |
186 * Instead of passing the alpha value(s) as arguments, set the openGL colour prior to calling: | |
187 * --- | |
188 * glColor3f (.5f, .5f, .5f); // set alpha to half | |
189 * drawCacheA (face, ...); | |
190 * | |
191 * glColor3ub (0, 255, 127); // alpha 0 for red, 1 for green, half for blue | |
192 * drawCacheA (face, ...); | |
193 * --- | |
194 * | |
195 * The overhead of the transparency is minimal. */ | |
196 void drawCacheA (FT_Face face, char[] str, ref TextBlock cache, int x, int y, Colour col/+ = Colour.WHITE+/) { | |
197 updateCache (face, str, cache); // update if necessary | |
198 debug scope (failure) | |
199 logger.error ("drawTextCache failed"); | |
200 | |
201 // transparency alpha | |
202 // alpha is current colour, per component | |
203 glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); | |
204 | |
205 drawCacheImpl (cache, x, y, col); | |
206 } | |
207 | |
208 private void drawCacheImpl (ref TextBlock cache, int x, int y, Colour col) { | |
209 if (DerelictGL.availableVersion() >= GLVersion.Version14) { | |
210 glBlendFunc (GL_CONSTANT_COLOR, GL_ONE_MINUS_SRC_COLOR); | |
211 glBlendColor(col.r, col.g, col.b, 1f); // text colour | |
212 } else | |
213 glBlendFunc (col.nearestGLConst, GL_ONE_MINUS_SRC_COLOR); | |
214 | |
215 glEnable (GL_TEXTURE_2D); | |
216 glEnable(GL_BLEND); | |
217 | |
218 foreach (chr; cache.chars) { | |
219 GlyphAttribs* ga = chr.ga; | |
220 | |
221 glBindTexture(GL_TEXTURE_2D, ga.texID); | |
222 | |
223 int x1 = x + chr.xPos; | |
224 int y1 = y + chr.yPos; | |
225 int x2 = x1 + ga.w; | |
226 int y2 = y1 + ga.h; | |
227 float tx1 = ga.x * wFactor; | |
228 float ty1 = ga.y * hFactor; | |
229 float tx2 = (ga.x + ga.w) * wFactor; | |
230 float ty2 = (ga.y + ga.h) * hFactor; | |
231 | |
232 glBegin (GL_QUADS); | |
233 glTexCoord2f (tx1, ty1); glVertex2i (x1, y1); | |
234 glTexCoord2f (tx2, ty1); glVertex2i (x2, y1); | |
235 glTexCoord2f (tx2, ty2); glVertex2i (x2, y2); | |
236 glTexCoord2f (tx1, ty2); glVertex2i (x1, y2); | |
237 glEnd (); | |
238 } | |
239 | |
240 glDisable(GL_BLEND); | |
241 } | |
242 | |
243 void addGlyph (FT_Face face, dchar chr) { | |
244 debug scope (failure) | |
245 logger.error ("FontTexture.addGlyph failed!"); | |
246 | |
247 auto gi = FT_Get_Char_Index (face, chr); | |
248 auto g = face.glyph; | |
249 | |
250 // Use renderMode from options, masking bits which are allowable: | |
251 if (FT_Load_Glyph (face, gi, FT_LOAD_RENDER | (fontOpts.renderMode & 0xF0000))) | |
252 throw new fontGlyphException ("Unable to render glyph"); | |
253 | |
254 auto b = g.bitmap; | |
255 glPixelStorei(GL_UNPACK_ALIGNMENT, 1); | |
256 //glPixelStorei (GL_UNPACK_ROW_LENGTH, b.pitch); | |
257 | |
258 GlyphAttribs ga; | |
259 ga.w = b.width; | |
260 ga.h = b.rows; | |
261 ga.left = g.bitmap_left; | |
262 ga.top = g.bitmap_top; | |
263 ga.advanceX = g.advance.x >> 6; | |
264 ga.index = gi; | |
265 if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_LCD) | |
266 ga.w /= 3; | |
267 if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_LCD_V) | |
268 ga.h /= 3; | |
269 | |
270 foreach (ref t; tex) { | |
271 if (t.addGlyph (ga)) | |
272 goto gotTexSpace; | |
273 } | |
274 // if here, no existing texture had the room for the glyph so create a new texture | |
275 // NOTE: check if using more than one texture impacts performance due to texture switching | |
276 logger.info ("Creating a new font texture."); | |
277 tex ~= TexPacker.create(); | |
278 assert (tex[$-1].addGlyph (ga), "Failed to fit glyph in a new texture but addGlyph didn't throw"); | |
279 | |
280 gotTexSpace: | |
281 glBindTexture(GL_TEXTURE_2D, ga.texID); | |
282 GLenum format; | |
283 ubyte[] buffer; // use our own pointer, since for LCD modes we need to perform a conversion | |
284 if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_GRAY && b.num_grays == 256) { | |
285 assert (b.pitch == b.width, "Have assumed b.pitch == b.width for gray glyphs."); | |
286 buffer = b.buffer[0..b.pitch*b.rows]; | |
287 format = GL_LUMINANCE; | |
288 } else if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_LCD) { | |
289 // NOTE: Can't seem to get OpenGL to read freetype's RGB buffers properly, so convent. | |
290 /* NOTE: Sub-pixel rendering probably also needs filtering. I haven't tried, since it's | |
291 * disabled in my build of the library. For a tutorial on the filtering, see: | |
292 * http://dmedia.dprogramming.com/?n=Tutorials.TextRendering1 */ | |
293 buffer = new ubyte[b.width*b.rows]; | |
294 for (uint i = 0; i < b.rows; ++i) | |
295 for (uint j = 0; j < b.width; j+= 3) | |
296 { | |
297 buffer[i*b.width + j + 0] = b.buffer[i*b.pitch + j + 0]; | |
298 buffer[i*b.width + j + 1] = b.buffer[i*b.pitch + j + 1]; | |
299 buffer[i*b.width + j + 2] = b.buffer[i*b.pitch + j + 2]; | |
300 } | |
301 | |
302 format = (fontOpts.renderMode & RENDER_LCD_BGR) ? GL_BGR : GL_RGB; | |
303 } else if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_LCD_V) { | |
304 // NOTE: Notes above apply. Only in this case converting the buffers seems essential. | |
305 buffer = new ubyte[b.width*b.rows]; | |
306 for (uint i = 0; i < b.rows; ++i) | |
307 for (uint j = 0; j < b.width; ++j) | |
308 { | |
309 // i/3 is the "real" row, b.width*3 is our width (with subpixels), j is column, | |
310 // i%3 is sub-pixel (R/G/B). i/3*3 necessary to round to multiple of 3 | |
311 buffer[i/3*b.width*3 + 3*j + i%3] = b.buffer[i*b.pitch + j]; | |
312 } | |
313 | |
314 format = (fontOpts.renderMode & RENDER_LCD_BGR) ? GL_BGR : GL_RGB; | |
315 } else | |
316 throw new fontGlyphException ("Unsupported freetype bitmap format"); | |
317 | |
318 glTexSubImage2D(GL_TEXTURE_2D, 0, | |
319 ga.x, ga.y, | |
320 ga.w, ga.h, | |
321 format, GL_UNSIGNED_BYTE, | |
322 cast(void*) buffer.ptr); | |
323 | |
324 cachedGlyphs[chr] = ga; | |
325 } | |
326 | |
327 // Draw the first glyph cache texture in the upper-left corner of the screen. | |
328 debug (drawGlyphCache) void drawTexture () { | |
329 if (tex.length == 0) return; | |
330 glEnable (GL_TEXTURE_2D); | |
331 glBindTexture(GL_TEXTURE_2D, tex[0].texID); | |
332 glEnable(GL_BLEND); | |
333 glBlendFunc (GL_ONE, GL_ONE_MINUS_SRC_COLOR); | |
334 float[4] Cc = [ 1.0f, 1f, 1f, 1f ]; | |
335 glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, Cc.ptr); | |
336 glColor3f (1f, 0f, 0f); | |
337 | |
338 glBegin (GL_QUADS); | |
339 glTexCoord2f (0f, 0f); glVertex2i (0, 0); | |
340 glTexCoord2f (1f, 0f); glVertex2i (dimW, 0); | |
341 glTexCoord2f (1f, 1f); glVertex2i (dimW, dimH); | |
342 glTexCoord2f (0f, 1f); glVertex2i (0, dimH); | |
343 glEnd (); | |
344 | |
345 glDisable(GL_BLEND); | |
346 } | |
347 | |
348 private: | |
349 TexPacker[] tex; // contains the gl texture id and packing data | |
350 | |
351 GlyphAttribs[dchar] cachedGlyphs; | |
352 int cacheVer = 0; // version of cache, used to make sure TextBlock caches are current. | |
353 } | |
354 | |
355 // Use LinePacker for our texture packer: | |
356 alias LinePacker TexPacker; | |
357 | |
358 /** Represents one gl texture; packs glyphs into lines. */ | |
359 struct LinePacker | |
360 { | |
361 // create a new texture | |
362 static LinePacker create () { | |
363 LinePacker p; | |
364 //FIXME: why do I get a blank texture when binding to non-0? | |
365 //glGenTextures (1, &(p.texID)); | |
366 p.texID = 0; | |
367 | |
368 // add a pretty background to the texture | |
369 debug (drawGlyphCache) { | |
370 glPixelStorei(GL_UNPACK_ALIGNMENT, 1); | |
371 glPixelStorei (GL_UNPACK_ROW_LENGTH, 0); | |
372 ubyte[3][dimH][dimW] testTex; | |
373 for (size_t i = 0; i < dimW; ++i) | |
374 for (size_t j = 0; j < dimH; ++j) | |
375 { | |
376 testTex[i][j][0] = cast(ubyte) (i + j); | |
377 testTex[i][j][1] = cast(ubyte) i; | |
378 testTex[i][j][2] = cast(ubyte) j; | |
379 } | |
380 void* ptr = testTex.ptr; | |
381 } else | |
382 const void* ptr = null; | |
383 | |
384 // Create a texture without initialising values. | |
385 glBindTexture(GL_TEXTURE_2D, p.texID); | |
386 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, | |
387 dimW, dimH, 0, | |
388 GL_RGB, GL_UNSIGNED_BYTE, ptr); | |
389 return p; | |
390 } | |
391 | |
392 /** Find space for a glyph of size attr.w, attr.h within the texture. | |
393 * | |
394 * Throws: fontGlyphException if glyph dimensions are larger than the texture. | |
395 * | |
396 * Returns false if unable to fit the glyph into the texture, true if successful. If | |
397 * successful, attr's x and y are set to suitible positions such that the rect given by attr's | |
398 * x, y, w & h is a valid subregion of the texture. */ | |
399 bool addGlyph (ref GlyphAttribs attr) { | |
400 if (attr.w > dimW || attr.h > dimH) | |
401 throw new fontGlyphException ("Glyph too large to fit texture!"); | |
402 | |
403 attr.texID = texID; // Set now. Possibly reset if new texture is needed. | |
404 if (attr.w == 0) return true; // 0 sized glyph; x and y are unimportant. | |
405 | |
406 bool cantFitExtraLine = nextYPos + attr.h >= dimH; | |
407 foreach (ref line; lines) { | |
408 if (line.length + attr.w <= dimW && // if sufficient length and | |
409 line.height >= attr.h && // sufficient height and | |
410 (cantFitExtraLine || // either there's not room for another line | |
411 line.height <= attr.h * WASTE_H)) // or we're not wasting much vertical space | |
412 { // then use this line | |
413 attr.x = line.length; | |
414 attr.y = line.yPos; | |
415 attr.texID = texID; | |
416 line.length += attr.w; | |
417 return true; | |
418 } | |
419 } | |
420 // If we didn't return, we didn't use an existing line. | |
421 if (cantFitExtraLine) // run out of room | |
422 return false; | |
423 | |
424 // Still room: add a new line. The new line has the largest yPos (furthest down texture), | |
425 // but the lines array must remain ordered by line height (lowest to heighest). | |
426 Line line; | |
427 line.yPos = nextYPos; | |
428 line.height = attr.h * EXTRA_H; | |
429 line.length = attr.w; | |
430 size_t i = 0; | |
431 while (i < lines.length && lines[i].height < line.height) ++i; | |
432 lines = lines[0..i] ~ line ~ lines[i..$]; // keep lines sorted by height | |
433 nextYPos += line.height; | |
434 | |
435 attr.x = 0; // first glyph in the line | |
436 attr.y = line.yPos; | |
437 return true; | |
438 } | |
439 | |
440 // Publically accessible data: | |
441 uint texID; // OpenGL texture identifier (for BindTexture) | |
442 | |
443 private: | |
444 const WASTE_H = 1.3; | |
445 const EXTRA_H = 1; // can be float/double, just experimenting with 1 | |
446 struct Line { | |
447 int yPos; // y position (xPos is always 0) | |
448 int height; | |
449 int length; | |
450 } | |
451 Line[] lines; | |
452 int nextYPos = 0; // y position for next created line (0 for first line) | |
453 } | |
454 | |
455 // this bit of renderMode, if set, means read glyph as BGR not RGB when using LCD rendering | |
456 enum { RENDER_LCD_BGR = 1 << 30 } | |
457 OptionsFont fontOpts; | |
458 class OptionsFont : Options { | |
459 /* renderMode should be FT_LOAD_TARGET_NORMAL, FT_LOAD_TARGET_LIGHT, FT_LOAD_TARGET_LCD or | |
460 * FT_LOAD_TARGET_LCD_V, possibly with bit 31 set (see RENDER_LCD_BGR). | |
461 * FT_LOAD_TARGET_MONO is unsupported. | |
462 * | |
463 * lcdFilter should come from enum FT_LcdFilter: | |
464 * FT_LCD_FILTER_NONE = 0, FT_LCD_FILTER_DEFAULT = 1, FT_LCD_FILTER_LIGHT = 2 */ | |
465 mixin (impl!("int renderMode, lcdFilter;")); | |
466 | |
467 static this() { | |
468 fontOpts = new OptionsFont; | |
469 Options.addOptionsClass (fontOpts, "font"); | |
470 } | |
471 } | |
472 | |
473 struct GlyphAttribs { | |
474 int x, y; // position within texture | |
475 int w, h; // bitmap size | |
476 | |
477 int left, top; // bitmap_left, bitmap_top fields | |
478 int advanceX; // horizontal advance distance | |
479 uint index; // glyph index (within face) | |
480 | |
481 uint texID; // gl tex identifier | |
482 } | |
483 | |
484 /** Cached information for drawing a block of text. | |
485 * | |
486 * Struct should be stored externally and updated via references. */ | |
487 struct TextBlock { | |
488 CharCache[] chars; // All chars. They hold x & y pos. info, so don't need to know about lines. | |
489 int cacheVer = -1; // this is checked on access, and must equal for cache to be valid. | |
490 int w, h; /// Size of the block. Likely the only fields of use outside the library. | |
491 } | |
492 struct CharCache { | |
493 GlyphAttribs* ga; // character | |
494 int xPos, yPos; // x,y position | |
495 } |