Sunday 27 January 2013

Rendering 3d bitmap text faster by creating a sprite mesh

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.
 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.