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.


Saturday, 19 January 2013

A Smarter UK, AllJoyn and Windows

Hey guys, it's been a great few weeks here.

Towards the end of last year, we managed to investigate and integrate Qualcomm's AllJoyn SDK which makes pairing devices over WIFI easier. We also entered a version of Phone Wars featuring WIFI play into the AllJoynAppChallenge, so please check out our entry and well as all the other cool submissions.

We also started working on the Windows Phone 8 port of our engine last week and have made some good progress, to the degree that the game menus and play works, and we have an OpenGL wrapper for Direct3D going. We just need to finish up our internet connectivity plugins and of course device testing before release. But we're close. I promise to create a write up or screen cast explaining the port as well as Open Sourcing a version of the code similar to how we presented our Intel port of our codebase.



Our MultiPlay.io game editor has been getting a public beta trial for the past few weeks also, and we've been here fixing up bugs and issues along the way. Currently the maps created can only be played via our Phone Wars game client, but we're working on our publication platform, where you'll be able to soon publish them out as your own Games. In the meantime, we've been working on a few ideas about better explaining the project, we have a survey here to help us pick a better name for our Play Editor.

And finally, the best news of today, we've been officially shortlisted for Smart UK Project’s Most Innovative Mobile Company award! We'll now get to pitch it out against 19 other great startups at UKTI’s pre-Mobile World Congress media event.

Wish us luck.
Ash