Alright, three years ago, I blogged about rendering fonts dynamically in OpenGL. The way that was done was to create a texture atlas of all the character of a font, and render each letter as required as as square on the screen.
While this technique is awesome compared to the previous method of creating a new texture at runtime with font blitting (super slow). It's still not the most performing solution, as it requires a draw call to be made per letter. Now, with OpenGL being the driver for rendering on iPhones and Androids, this hasn't been a problem. However, DirectX hates draw calls moreso than OpenGL, so on Windows based platforms (Windows Phone or WebGL running in Windows) this becomes the performance bottle neck pretty fast.
To get around this, we created a mesh made up of triangles representing the entire string, then drew it with just one one draw call.
We then implemented a pool to delete messages that haven't been drawn in a while.
And now it runs fast again.
While this technique is awesome compared to the previous method of creating a new texture at runtime with font blitting (super slow). It's still not the most performing solution, as it requires a draw call to be made per letter. Now, with OpenGL being the driver for rendering on iPhones and Androids, this hasn't been a problem. However, DirectX hates draw calls moreso than OpenGL, so on Windows based platforms (Windows Phone or WebGL running in Windows) this becomes the performance bottle neck pretty fast.
To get around this, we created a mesh made up of triangles representing the entire string, then drew it with just one one draw call.
CCTextureFontPage::CachedTextMesh* CCTextureFontPage::buildTextMesh(const char *text, const uint length, const float height, const bool centeredX)
{
...
// We will dynamically create meshes from the lines to save draw calls
float *vertices = (float*)malloc( sizeof( float ) * 3 * 6 * length );
float *uvs = (float*)malloc( sizeof( float ) * 2 * 6 * length );
int vertexIndex = 0;
int texCoordIndex = 0;
lineIndex = 0;
characterIndex = 0;
for( uint i=0; i<length; ++i )
{
char character = text[i];
if( character == '\n' )
{
lineIndex++;
CCPoint &start = *startPositions.list[lineIndex];
currentStart.x = start.x;
currentStart.y = start.y;
characterIndex = 0;
}
else
{
const Letter *letter = getLetter( character );
if( letter != NULL )
{
CCPoint &size = charSize[lineIndex][characterIndex];
// Calculate end point
currentEnd.x = currentStart.x + size.x;
currentEnd.y = currentStart.y - size.y;
// Triangle 1
{
vertices[vertexIndex++] = currentStart.x; // Bottom left
vertices[vertexIndex++] = currentEnd.y;
vertices[vertexIndex++] = 0.0f;
uvs[texCoordIndex++] = letter->start.x;
uvs[texCoordIndex++] = letter->end.y;
vertices[vertexIndex++] = currentEnd.x; // Bottom right
vertices[vertexIndex++] = currentEnd.y;
vertices[vertexIndex++] = 0.0f;
uvs[texCoordIndex++] = letter->end.x;
uvs[texCoordIndex++] = letter->end.y;
vertices[vertexIndex++] = currentStart.x; // Top left
vertices[vertexIndex++] = currentStart.y;
vertices[vertexIndex++] = 0.0f;
uvs[texCoordIndex++] = letter->start.x;
uvs[texCoordIndex++] = letter->start.y;
}
// Triangle 2
{
vertices[vertexIndex++] = currentEnd.x; // Bottom right
vertices[vertexIndex++] = currentEnd.y;
vertices[vertexIndex++] = 0.0f;
uvs[texCoordIndex++] = letter->end.x;
uvs[texCoordIndex++] = letter->end.y;
vertices[vertexIndex++] = currentEnd.x; // Top right
vertices[vertexIndex++] = currentStart.y;
vertices[vertexIndex++] = 0.0f;
uvs[texCoordIndex++] = letter->end.x;
uvs[texCoordIndex++] = letter->start.y;
vertices[vertexIndex++] = currentStart.x; // Top left
vertices[vertexIndex++] = currentStart.y;
vertices[vertexIndex++] = 0.0f;
uvs[texCoordIndex++] = letter->start.x;
uvs[texCoordIndex++] = letter->start.y;
}
currentStart.x += size.x;
characterIndex++;
}
}
}
CachedTextMesh *mesh = new CachedTextMesh();
mesh->text = text;
mesh->textHeight = height;
mesh->centeredX = centeredX;
mesh->totalLineHeight = totalLineHeight;
mesh->vertices = vertices;
mesh->uvs = uvs;
mesh->vertexCount = vertexIndex/3;
return mesh;
}
We then implemented a pool to delete messages that haven't been drawn in a while.
void CCTextureFontPage::renderText(const char *text, const uint length,
const float height, const bool centeredX)
{
if( length == 0 )
{
return;
}
ASSERT( text != NULL );
ASSERT( length < MAX_TEXT_LENGTH );
const CachedTextMesh *mesh = getTextMesh( text, length, height, centeredX );
// Draw our text mesh
GLPushMatrix();
{
GLTranslatef( 0.0f, mesh->totalLineHeight*0.5f, 0.0f );
CCSetViewMatrix();
bindTexturePage();
CCSetTexCoords( mesh->uvs );
GLVertexPointer( 3, GL_FLOAT, 0, mesh->vertices, mesh->vertexCount );
GLDrawArrays( GL_TRIANGLES, 0, mesh->vertexCount );
}
GLPopMatrix();
}
const CCTextureFontPage::CachedTextMesh* CCTextureFontPage::getTextMesh(const char *text, const uint length, const float height, const bool centeredX)
{
for( int i=0; i<cachedMeshes.length; ++i )
{
CachedTextMesh *mesh = cachedMeshes.list[i];
if( mesh->textHeight == height )
{
if( mesh->centeredX == centeredX )
{
if( mesh->text.length == length )
{
if( CCText::Equals( mesh->text, text ) )
{
mesh->lastDrawTime = gEngine->time.lifetime;
return mesh;
}
}
}
}
}
if( cachedMeshes.length > 50 )
{
// Delete the oldest one
float oldestRenderTime = MAXFLOAT;
CachedTextMesh *oldestRender = NULL;
for( int i=0; i<cachedMeshes.length; ++i )
{
CachedTextMesh *mesh = cachedMeshes.list[i];
if( mesh->lastDrawTime < oldestRenderTime )
{
oldestRenderTime = mesh->lastDrawTime;
oldestRender = mesh;
}
}
if( oldestRender != NULL )
{
cachedMeshes.remove( oldestRender );
delete oldestRender;
}
}
CachedTextMesh *mesh = buildTextMesh( text, length, height, centeredX );
cachedMeshes.add( mesh );
mesh->lastDrawTime = gEngine->time.lifetime;
return mesh;
}
And now it runs fast again.