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
+}