Mercurial > projects > mde
view mde/font/FontTexture.d @ 85:56c0ddd90193
Intermediate commit (not stable). Changes to init system.
author | Diggory Hardy <diggory.hardy@gmail.com> |
---|---|
date | Thu, 11 Sep 2008 11:33:51 +0100 |
parents | 2813ac68576f |
children | 79d816b3e2d2 |
line wrap: on
line source
/* 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 () {} // 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. But 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 have one of the following values, possibly with bit 31 set (see RENDER_LCD_BGR): * FT_LOAD_TARGET_NORMAL (0x00000) * FT_LOAD_TARGET_LIGHT (0x10000) * FT_LOAD_TARGET_LCD (0x30000) * FT_LOAD_TARGET_LCD_V (0x40000) * The mode FT_LOAD_TARGET_MONO (0x20000) 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 }