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 }