Mercurial > projects > mde
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mde/font/FontTexture.d Fri Jun 27 18:35:33 2008 +0100 @@ -0,0 +1,495 @@ +/* LICENSE BLOCK +Part of mde: a Modular D game-oriented Engine +Copyright © 2007-2008 Diggory Hardy + +This program is free software: you can redistribute it and/or modify it under the terms +of the GNU General Public License as published by the Free Software Foundation, either +version 2 of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. */ + +/** Font caching system. + * + * This module also serves as the internals to the font module and shouldn't be used except through + * the font module. The two modules could be combined, at a cost to readability. + * + * Three types of coordinates get used in the system: FreeType coordinates for each glyph, texture + * coordinates, and OpenGL's model/world coordinates (for rendering). The freetype and texture + * coords are cartesian (i.e. y increases upwards), although largely this is too abstract to + * matter. However, for the model/world coords, y increases downwards. */ +module mde.font.FontTexture; + +import mde.types.Colour; +import mde.lookup.Options; +import mde.font.exception; + +import derelict.freetype.ft; +import derelict.opengl.gl; + +import Utf = tango.text.convert.Utf; +import tango.util.log.Log : Log, Logger; + +private Logger logger; +static this () { + logger = Log.getLogger ("mde.font.FontTexture"); +} + +static const int dimW = 256, dimH = 256; // Texture size +const wFactor = 1f / dimW; +const hFactor = 1f / dimH; + +/** A FontTexture is basically a cache of all font glyphs rendered so far. + * + * This class should be limited to code for rendering to (and otherwise handling) textures and + * rendering fonts to the screen. + * + * Technically, there's no reason it shouldn't be a static part of the FontStyle class. */ +class FontTexture +{ + this () {} + ~this () { + foreach (t; tex) { + glDeleteTextures (1, &(t.texID)); + } + } + + // Call if font(s) have been changed and glyphs must be recached. + void clear () { + foreach (t; tex) { + glDeleteTextures (1, &(t.texID)); + } + cachedGlyphs = null; + ++cacheVer; + } + + + /** Cache informatation for rendering a block of text. + * + * Recognises '\r', '\n' and "\r\n" as end-of-line markers. */ + void updateCache (FT_Face face, char[] str, ref TextBlock cache) + { + debug scope (failure) + logger.error ("updateCache failed"); + + if (cache.cacheVer == cacheVer) // Existing cache is up-to-date + return; + + cache.cacheVer = cacheVer; + + /* Convert the string to an array of character codes (which is equivalent to decoding UTF8 + * to UTF32 since no character code is ever > dchar.max). */ + static dchar[] chrs; // keep memory for future calls (NOTE: change for threading) + chrs = Utf.toString32 (str, chrs); + + // Allocate space. + // Since end-of-line chars get excluded, will often be slightly larger than necessary. + cache.chars.length = chrs.length; + cache.chars.length = 0; + + int lineSep = face.size.metrics.height >> 6; + bool hasKerning = (FT_HAS_KERNING (face) != 0); + int y = 0; + CharCache cc; // struct; reused for each character + + for (size_t i = 0; i < chrs.length; ++i) + { + // First, find yMax for the current line. + int yMax = 0; // Maximal glyph height above baseline. + for (size_t j = i; j < chrs.length; ++j) + { + if (chrs[j] == '\n' || chrs[j] == '\r') // end of line + break; + + GlyphAttribs* ga = chrs[j] in cachedGlyphs; + if (ga is null) { // Not cached + addGlyph (face, chrs[j]); // so render it + ga = chrs[j] in cachedGlyphs; // get the ref of the copy we've stored + assert (ga !is null, "ga is null: 1"); + } + + if (ga.top > yMax) + yMax = ga.top; + } + y += yMax; + + // Now for the current line: + int x = 0; // x pos for next glyph + uint gi_prev = 0; // previous glyph index (needed for kerning) + for (; i < chrs.length; ++i) + { + // If end-of-line, break to find yMax for next line. + if (chrs.length >= i+2 && chrs[i..i+2] == "\r\n"d) { + ++i; + break; + } + if (chrs[i] == '\n' || chrs[i] == '\r') { + break; + } + + cc.ga = chrs[i] in cachedGlyphs; + assert (cc.ga !is null, "ga is null: 2"); + + // Kerning + if (hasKerning && (gi_prev != 0)) { + FT_Vector delta; + FT_Get_Kerning (face, gi_prev, cc.ga.index, FT_Kerning_Mode.FT_KERNING_DEFAULT, &delta); + x += delta.x >> 6; + } + + // ga.left component: adding this slightly improves glyph layout. Note that the + // left-most glyph on a line may not start right on the edge, but this looks best. + cc.xPos = x + cc.ga.left; + cc.yPos = y - cc.ga.top; + x += cc.ga.advanceX; + + cache.chars ~= cc; + + // Update rect total size. Top and left coords should be zero, so make width and + // height maximal x and y coordinates. + if (cc.xPos + cc.ga.w > cache.w) + cache.w = cc.xPos + cc.ga.w; + if (cc.yPos + cc.ga.h > cache.h) + cache.h = cc.yPos + cc.ga.h; + } + // Now increment i and continue with the next line if there is one. + y += lineSep - yMax; + } + } + + /** Render a block of text using a cache. Updates the cache if necessary. + * + * Params: + * face = Current typeface pointer; must be passed from font.d (only needed if the cache is + * invalid) + * str = Text to render (only needed if the cache is invalid) + * cache = Cache used to speed up CPU-side rendering code + * x = Smaller x-coordinate of position + * y = Smaller y-coordinate of position + * col = Text colour (note: currently limited to black or white) */ + void drawCache (FT_Face face, char[] str, ref TextBlock cache, int x, int y, Colour col ) { + updateCache (face, str, cache); // update if necessary + debug scope (failure) + logger.error ("drawTextCache failed"); + + // opaque (GL_DECAL would be equivalent) + glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); + + drawCacheImpl (cache, x, y, col); + } + /** A variation of drawCache, for transparent text. + * + * Instead of passing the alpha value(s) as arguments, set the openGL colour prior to calling: + * --- + * glColor3f (.5f, .5f, .5f); // set alpha to half + * drawCacheA (face, ...); + * + * glColor3ub (0, 255, 127); // alpha 0 for red, 1 for green, half for blue + * drawCacheA (face, ...); + * --- + * + * The overhead of the transparency is minimal. */ + void drawCacheA (FT_Face face, char[] str, ref TextBlock cache, int x, int y, Colour col/+ = Colour.WHITE+/) { + updateCache (face, str, cache); // update if necessary + debug scope (failure) + logger.error ("drawTextCache failed"); + + // transparency alpha + // alpha is current colour, per component + glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); + + drawCacheImpl (cache, x, y, col); + } + + private void drawCacheImpl (ref TextBlock cache, int x, int y, Colour col) { + if (DerelictGL.availableVersion() >= GLVersion.Version14) { + glBlendFunc (GL_CONSTANT_COLOR, GL_ONE_MINUS_SRC_COLOR); + glBlendColor(col.r, col.g, col.b, 1f); // text colour + } else + glBlendFunc (col.nearestGLConst, GL_ONE_MINUS_SRC_COLOR); + + glEnable (GL_TEXTURE_2D); + glEnable(GL_BLEND); + + foreach (chr; cache.chars) { + GlyphAttribs* ga = chr.ga; + + glBindTexture(GL_TEXTURE_2D, ga.texID); + + int x1 = x + chr.xPos; + int y1 = y + chr.yPos; + int x2 = x1 + ga.w; + int y2 = y1 + ga.h; + float tx1 = ga.x * wFactor; + float ty1 = ga.y * hFactor; + float tx2 = (ga.x + ga.w) * wFactor; + float ty2 = (ga.y + ga.h) * hFactor; + + glBegin (GL_QUADS); + glTexCoord2f (tx1, ty1); glVertex2i (x1, y1); + glTexCoord2f (tx2, ty1); glVertex2i (x2, y1); + glTexCoord2f (tx2, ty2); glVertex2i (x2, y2); + glTexCoord2f (tx1, ty2); glVertex2i (x1, y2); + glEnd (); + } + + glDisable(GL_BLEND); + } + + void addGlyph (FT_Face face, dchar chr) { + debug scope (failure) + logger.error ("FontTexture.addGlyph failed!"); + + auto gi = FT_Get_Char_Index (face, chr); + auto g = face.glyph; + + // Use renderMode from options, masking bits which are allowable: + if (FT_Load_Glyph (face, gi, FT_LOAD_RENDER | (fontOpts.renderMode & 0xF0000))) + throw new fontGlyphException ("Unable to render glyph"); + + auto b = g.bitmap; + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + //glPixelStorei (GL_UNPACK_ROW_LENGTH, b.pitch); + + GlyphAttribs ga; + ga.w = b.width; + ga.h = b.rows; + ga.left = g.bitmap_left; + ga.top = g.bitmap_top; + ga.advanceX = g.advance.x >> 6; + ga.index = gi; + if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_LCD) + ga.w /= 3; + if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_LCD_V) + ga.h /= 3; + + foreach (ref t; tex) { + if (t.addGlyph (ga)) + goto gotTexSpace; + } + // if here, no existing texture had the room for the glyph so create a new texture + // NOTE: check if using more than one texture impacts performance due to texture switching + logger.info ("Creating a new font texture."); + tex ~= TexPacker.create(); + assert (tex[$-1].addGlyph (ga), "Failed to fit glyph in a new texture but addGlyph didn't throw"); + + gotTexSpace: + glBindTexture(GL_TEXTURE_2D, ga.texID); + GLenum format; + ubyte[] buffer; // use our own pointer, since for LCD modes we need to perform a conversion + if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_GRAY && b.num_grays == 256) { + assert (b.pitch == b.width, "Have assumed b.pitch == b.width for gray glyphs."); + buffer = b.buffer[0..b.pitch*b.rows]; + format = GL_LUMINANCE; + } else if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_LCD) { + // NOTE: Can't seem to get OpenGL to read freetype's RGB buffers properly, so convent. + /* NOTE: Sub-pixel rendering probably also needs filtering. I haven't tried, since it's + * disabled in my build of the library. For a tutorial on the filtering, see: + * http://dmedia.dprogramming.com/?n=Tutorials.TextRendering1 */ + buffer = new ubyte[b.width*b.rows]; + for (uint i = 0; i < b.rows; ++i) + for (uint j = 0; j < b.width; j+= 3) + { + buffer[i*b.width + j + 0] = b.buffer[i*b.pitch + j + 0]; + buffer[i*b.width + j + 1] = b.buffer[i*b.pitch + j + 1]; + buffer[i*b.width + j + 2] = b.buffer[i*b.pitch + j + 2]; + } + + format = (fontOpts.renderMode & RENDER_LCD_BGR) ? GL_BGR : GL_RGB; + } else if (b.pixel_mode == FT_Pixel_Mode.FT_PIXEL_MODE_LCD_V) { + // NOTE: Notes above apply. Only in this case converting the buffers seems essential. + buffer = new ubyte[b.width*b.rows]; + for (uint i = 0; i < b.rows; ++i) + for (uint j = 0; j < b.width; ++j) + { + // i/3 is the "real" row, b.width*3 is our width (with subpixels), j is column, + // i%3 is sub-pixel (R/G/B). i/3*3 necessary to round to multiple of 3 + buffer[i/3*b.width*3 + 3*j + i%3] = b.buffer[i*b.pitch + j]; + } + + format = (fontOpts.renderMode & RENDER_LCD_BGR) ? GL_BGR : GL_RGB; + } else + throw new fontGlyphException ("Unsupported freetype bitmap format"); + + glTexSubImage2D(GL_TEXTURE_2D, 0, + ga.x, ga.y, + ga.w, ga.h, + format, GL_UNSIGNED_BYTE, + cast(void*) buffer.ptr); + + cachedGlyphs[chr] = ga; + } + + // Draw the first glyph cache texture in the upper-left corner of the screen. + debug (drawGlyphCache) void drawTexture () { + if (tex.length == 0) return; + glEnable (GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, tex[0].texID); + glEnable(GL_BLEND); + glBlendFunc (GL_ONE, GL_ONE_MINUS_SRC_COLOR); + float[4] Cc = [ 1.0f, 1f, 1f, 1f ]; + glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, Cc.ptr); + glColor3f (1f, 0f, 0f); + + glBegin (GL_QUADS); + glTexCoord2f (0f, 0f); glVertex2i (0, 0); + glTexCoord2f (1f, 0f); glVertex2i (dimW, 0); + glTexCoord2f (1f, 1f); glVertex2i (dimW, dimH); + glTexCoord2f (0f, 1f); glVertex2i (0, dimH); + glEnd (); + + glDisable(GL_BLEND); + } + +private: + TexPacker[] tex; // contains the gl texture id and packing data + + GlyphAttribs[dchar] cachedGlyphs; + int cacheVer = 0; // version of cache, used to make sure TextBlock caches are current. +} + +// Use LinePacker for our texture packer: +alias LinePacker TexPacker; + +/** Represents one gl texture; packs glyphs into lines. */ +struct LinePacker +{ + // create a new texture + static LinePacker create () { + LinePacker p; + //FIXME: why do I get a blank texture when binding to non-0? + //glGenTextures (1, &(p.texID)); + p.texID = 0; + + // add a pretty background to the texture + debug (drawGlyphCache) { + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glPixelStorei (GL_UNPACK_ROW_LENGTH, 0); + ubyte[3][dimH][dimW] testTex; + for (size_t i = 0; i < dimW; ++i) + for (size_t j = 0; j < dimH; ++j) + { + testTex[i][j][0] = cast(ubyte) (i + j); + testTex[i][j][1] = cast(ubyte) i; + testTex[i][j][2] = cast(ubyte) j; + } + void* ptr = testTex.ptr; + } else + const void* ptr = null; + + // Create a texture without initialising values. + glBindTexture(GL_TEXTURE_2D, p.texID); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, + dimW, dimH, 0, + GL_RGB, GL_UNSIGNED_BYTE, ptr); + return p; + } + + /** Find space for a glyph of size attr.w, attr.h within the texture. + * + * Throws: fontGlyphException if glyph dimensions are larger than the texture. + * + * Returns false if unable to fit the glyph into the texture, true if successful. If + * successful, attr's x and y are set to suitible positions such that the rect given by attr's + * x, y, w & h is a valid subregion of the texture. */ + bool addGlyph (ref GlyphAttribs attr) { + if (attr.w > dimW || attr.h > dimH) + throw new fontGlyphException ("Glyph too large to fit texture!"); + + attr.texID = texID; // Set now. Possibly reset if new texture is needed. + if (attr.w == 0) return true; // 0 sized glyph; x and y are unimportant. + + bool cantFitExtraLine = nextYPos + attr.h >= dimH; + foreach (ref line; lines) { + if (line.length + attr.w <= dimW && // if sufficient length and + line.height >= attr.h && // sufficient height and + (cantFitExtraLine || // either there's not room for another line + line.height <= attr.h * WASTE_H)) // or we're not wasting much vertical space + { // then use this line + attr.x = line.length; + attr.y = line.yPos; + attr.texID = texID; + line.length += attr.w; + return true; + } + } + // If we didn't return, we didn't use an existing line. + if (cantFitExtraLine) // run out of room + return false; + + // Still room: add a new line. The new line has the largest yPos (furthest down texture), + // but the lines array must remain ordered by line height (lowest to heighest). + Line line; + line.yPos = nextYPos; + line.height = attr.h * EXTRA_H; + line.length = attr.w; + size_t i = 0; + while (i < lines.length && lines[i].height < line.height) ++i; + lines = lines[0..i] ~ line ~ lines[i..$]; // keep lines sorted by height + nextYPos += line.height; + + attr.x = 0; // first glyph in the line + attr.y = line.yPos; + return true; + } + + // Publically accessible data: + uint texID; // OpenGL texture identifier (for BindTexture) + +private: + const WASTE_H = 1.3; + const EXTRA_H = 1; // can be float/double, just experimenting with 1 + struct Line { + int yPos; // y position (xPos is always 0) + int height; + int length; + } + Line[] lines; + int nextYPos = 0; // y position for next created line (0 for first line) +} + +// this bit of renderMode, if set, means read glyph as BGR not RGB when using LCD rendering +enum { RENDER_LCD_BGR = 1 << 30 } +OptionsFont fontOpts; +class OptionsFont : Options { + /* renderMode should be FT_LOAD_TARGET_NORMAL, FT_LOAD_TARGET_LIGHT, FT_LOAD_TARGET_LCD or + * FT_LOAD_TARGET_LCD_V, possibly with bit 31 set (see RENDER_LCD_BGR). + * FT_LOAD_TARGET_MONO is unsupported. + * + * lcdFilter should come from enum FT_LcdFilter: + * FT_LCD_FILTER_NONE = 0, FT_LCD_FILTER_DEFAULT = 1, FT_LCD_FILTER_LIGHT = 2 */ + mixin (impl!("int renderMode, lcdFilter;")); + + static this() { + fontOpts = new OptionsFont; + Options.addOptionsClass (fontOpts, "font"); + } +} + +struct GlyphAttribs { + int x, y; // position within texture + int w, h; // bitmap size + + int left, top; // bitmap_left, bitmap_top fields + int advanceX; // horizontal advance distance + uint index; // glyph index (within face) + + uint texID; // gl tex identifier +} + +/** Cached information for drawing a block of text. + * + * Struct should be stored externally and updated via references. */ +struct TextBlock { + CharCache[] chars; // All chars. They hold x & y pos. info, so don't need to know about lines. + int cacheVer = -1; // this is checked on access, and must equal for cache to be valid. + int w, h; /// Size of the block. Likely the only fields of use outside the library. +} +struct CharCache { + GlyphAttribs* ga; // character + int xPos, yPos; // x,y position +}