So, I bit the bullet the other day and implemented something that I knew I’d be needing soon. Annoyed that I didn’t have a text-rendering system in place to display things like the current FPS on the screen (and because I hate reading console output), I sought out a decent portable text rendering solution for OpenGL that I could easily port to Direct3D later down the road if needed. Normally, on the mac platform, I’d just drop into Core Graphics and render the text string to a blank bitmap and roll that bitmap into a texture. But since I’m trying to be portable, I can’t rely on anything specific to mac os, or windows, or anything!
One thing became readily apparent was that any solution I devise will use FreeType2, a very useful portable font library for rendering TrueType fonts. One thing that I was not predicting however, was that there exists a multitude of libraries that run on top of FreeType2 (such FreeTypeGL) as whose sole purpose is to use freetype to render text on OpenGL. This annoyed me. I don’t want to drop in an entire heavy-weight font library complete with 30+ source files, with its own license, just to render a single static string to a texture. Furthermore, these libraries become another barrier for portability in terms of OpenGL/Direct3D abstraction. I’m ok with linking graphics-independent libs such as freetype, but entire subsystems relying on OpenGL, that’s where I draw the line.
So, I decided to do it myself. Freetype can’t be that hard anyway right? So it really only takes about 100 lines of code to use freetype to render to a image buffer suitable for rolling into an OpenGL texture. So here you have it, my experimental, non-optimized Text Texture class..
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <ft2build.h>
#include <freetype/freetype.h>
#include "Texture2D.h"
namespace ssf
{
class TextTexture2D : public Texture2D
{
public:
TextTexture2D(const string &text,
const string &fontFilename, int fontPointSz, int texWidth, int texHeight);
protected:
void draw(FT_Bitmap &bitmap, int x, int y, GLubyte *data);
};
} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
//http://www.freetype.org/freetype2/docs/tutorial/step2.html
#include <ft2build.h>
#include <freetype/freetype.h>
#include <freetype/ftglyph.h>
#include <freetype/ftoutln.h>
#include <freetype/fttrigon.h>
#include <vector>
#include <iostream>
#include "TextTexture2D.h"
using namespace std;
namespace ssf
{
///This function gets the first power of 2 >= the
///int that we pass it.
inline int next_p2(int a)
{
int rval=1;
while(rval<a) rval<<=1;
return rval;
}
void TextTexture2D::draw(FT_Bitmap &bitmap, int x, int y, GLubyte *data)
{
//TODO - use memcpy or mmx/sse to speed this up
for(int j = 0; j < bitmap.rows; j++)
{
for(int i = 0; i < bitmap.width; i++)
{
if(i+x > 0 && i+x < texWidth && j+y > 0 && j+y < texHeight)
{
data[(i+x+(j+y)*texWidth)] = bitmap.buffer[i + bitmap.width*j];
}
}
}
}
static void flip(GLubyte *src, GLubyte *dest, int w, int h, int pSz)
{
for(int i = 0; i < h; i++)
{
memcpy(dest+(h-1-i)*w*pSz, src+(i*w*pSz), w*pSz);
}
}
TextTexture2D::TextTexture2D(const string &text,
const string &fontFilename, int fontPointSz, int texWidth, int texHeight)
{
vector<float2> textureAtlasOffsets;
FT_Library library;
if(FT_Init_FreeType( &library ))
throw std::runtime_error("FT_Init_FreeType failed");
//TODO cache this stuff better
FT_Face face;
if(FT_New_Face(library, fontFilename.c_str(), 0, &face))
throw std::runtime_error("FT_New_Face failed (there is probably a problem with your font file)");
//For some twisted reason, Freetype measures font size
//in terms of 1/64ths of pixels. Thus, to make a font
//h pixels high, we need to request a size of h*64.
FT_Set_Char_Size(face, fontPointSz << 6, fontPointSz << 6, 96, 96);
GLubyte *data = new GLubyte[texWidth * texHeight];
memset(data, 0, texWidth*texHeight);
width = this->texWidth = texWidth;
height = this->texHeight = texHeight;
alpha = true;
mipmap = false;
FT_GlyphSlot slot = face->glyph; /* a small shortcut */
FT_UInt glyph_index;
FT_Bool use_kerning = FT_HAS_KERNING(face);
FT_UInt previous = 0;
int pen_x = 0, pen_y = texHeight, error;
for(int n = 0; n < text.length(); n++)
{
/* convert character code to glyph index */
glyph_index = FT_Get_Char_Index(face, text[n]);
/* retrieve kerning distance and move pen position */
if(use_kerning && previous && glyph_index)
{
FT_Vector delta;
FT_Get_Kerning(face, previous, glyph_index,
FT_KERNING_DEFAULT, &delta);
pen_x += delta.x >> 6;
}
/* load glyph image into the slot (erase previous one) */
error = FT_Load_Glyph(face, glyph_index, FT_LOAD_RENDER);
if(error)
continue; /* ignore errors */
/* now draw to our target surface */
draw(slot->bitmap, pen_x + slot->bitmap_left, pen_y - slot->bitmap_top, data);
/* increment pen position */
pen_x += slot->advance.x >> 6;
/* record current glyph index */
previous = glyph_index;
}
GLubyte *dataFlipped = new GLubyte[texWidth * texHeight];
flip(data, dataFlipped, texWidth, texHeight, 1);
glGenTextures(1, &tex);
glBindTexture( GL_TEXTURE_2D, tex);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
//Here we actually create the texture itself
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, width, height,
0, GL_RED, GL_UNSIGNED_BYTE, dataFlipped);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
delete dataFlipped;
delete data;
FT_Done_Face(face);
FT_Done_FreeType(library);
optimalTexCoords = NULL;
}
} |
Not very efficient yet, but it works!
To save memory, I decided to store the text image in a single channel texture (GL_RED format). So, I have to use a special swizzle in the GLSL code so the texture doesn’t render red.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#ifdef GL_ES
precision highp float;
#endif
uniform sampler2D texture0;
varying vec2 varTexCoord0;
varying vec4 varColor;
void main()
{
vec4 texCol = texture2D(texture0, varTexCoord0).rrrr;
gl_FragColor = varColor * texCol;
} |
And.. it works!
Finally. I can show my FPS on the screen like a proper game.