Tuesday, 9 February 2010

Font pages

I recently switched from rendering texture strings to font pages. The difference between the two is for texture strings you'd render the string into a bitmap and assign the image to an openGL handle. For a font page, you'd create all the characters of the font required and then when rendering a string, you'd fetch the characters required individually from the created page to render one by one.

The reason for switching was to display a more dynamic text such as an OSD counter. With the old system I'd have to have to constantly create and release different images representing the numbers required, which would be impractical.





So here's how rendering text using a font page works.

First we initialize our class, where we create a bitmap with all the characters from the font required, and paste them one by one onto the page, incrementing the width and height as we paste along..
 -(id)init  
{  
if( self = [super init] )  
{  
// Initialise our texture page  
UIFont *font = [UIFont fontWithName:@"Trebuchet MS" size:32];  
width = 512;  
height = 256;  
widthAspect = ( width * gView->actualScreenSizeMultiple.width );  
heightAspect = ( height * gView->actualScreenSizeMultiple.height );  
const CGSize textureSizeMultiple = CGSizeMake( 1.0f / width, 1.0f / height );

CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray();  
void *data = calloc( height, width );  
CGContextRef context = CGBitmapContextCreate( data, width, height, 8, width, colorSpace, kCGImageAlphaNone );  
CGColorSpaceRelease( colorSpace );  
CGContextSetGrayFillColor( context, 1.0f, 1.0f );  
UIGraphicsPushContext( context );  

// Create our characters  
float x = 0.0f, y = 0.0f;  
float maxY = 0.0f;  
for( uint i=0; i<128; ++i )  
{  
NSString *string = [[NSString alloc] initWithString:@"£"];  
letters[i].stringSize = [string sizeWithFont:font];  
float endX = x + letters[i].stringSize.width + 1.0f;  
if( endX > 512.0f )  
{  
x = 0.0f;  
y += maxY + 1.0f;  
maxY = letters[i].stringSize.height;  
endX = letters[i].stringSize.width + 1.0f;  
}  

// Check if we've reached the end of the page
if( letters[i].stringSize.height > maxY )  
{  
maxY = letters[i].stringSize.height;  
}  

letters[i].start.x = x;  
letters[i].start.y = y;  
[string drawInRect:CGRectMake( x, y, letters[i].stringSize.width, letters[i].stringSize.height) withFont:font lineBreakMode:UILineBreakModeWordWrap alignment:UITextAlignmentLeft ];  
x = endX;  
// Normalize  
letters[i].stringSize.width *= textureSizeMultiple.width;  
letters[i].stringSize.height *= textureSizeMultiple.height;  
letters[i].start.x *= textureSizeMultiple.width;  
letters[i].start.y = 1.0f - ( letters[i].start.y * textureSizeMultiple.height );  
letters[i].end.x = letters[i].start.x + letters[i].stringSize.width;  
letters[i].end.y = letters[i].start.y - letters[i].stringSize.height;  
[string release];  
}  
assert( maxY + y < height ); 

// Finish our texture page  
UIGraphicsPopContext();  
glGenTextures( 1, &name );  
BindTexture( name );  
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );  
glTexImage2D( GL_TEXTURE_2D, 0, GL_ALPHA, width, height, 0, GL_ALPHA, GL_UNSIGNED_BYTE, data );  
CGContextRelease( context );  
free( data );  
BindTexture( 0 );  
}  
return self;  
}  


Now to render any given string, we just look up the letters one by one and render each one with their stored sizes..
 -(void)renderText:(const char*)string length:(const uint)length x:(const float)x y:(const float)y centered:(const BOOL)centered size:(const float)size  
{    
BindTexture( name );  

// Find out our width so we can center the text  
float totalWidth = 0.0f, maxHeight = 0.0f;  
float widths[length], heights[length];  
for( uint i=0; i<length; ++i )  
{  
const Letters *letter = [self getLetter:string[i]];  
if( letter != nil )  
{  
widths[i] = widthAspect * letter->stringSize.width * size;  
totalWidth += widths[i];  
heights[i] = heightAspect * letter->stringSize.height * size;  
maxHeight = MAX( maxHeight, heights[i] );  
}  
}  

CGPoint start = CGPointMake( x, y );  
if( centered )  
{  
start.x -= totalWidth * 0.5f;  
start.y -= maxHeight * 0.5f;  
}  

CGPoint currentStart = start;  
for( uint i=0; i<length; ++i )  
{  
const Letters *letter = [self getLetter:string[i]];  
if( letter != nil )  
{  
// Calculate end point  
CGPoint orientatedStart = currentStart;  
CGPoint end;  
end.x = orientatedStart.x + widths[i];  
end.y = orientatedStart.y + heights[i];  

const GLfloat texCoords[] =   
{  
letter->start.x, letter->start.y,  
letter->end.x, letter->start.y,  
letter->start.x, letter->end.y,  
letter->end.x, letter->end.y  
};  
glTexCoordPointer( 2, GL_FLOAT, 0, texCoords );  
RenderSquare( orientatedStart, end );  
currentStart.x += widths[i];  
}  
}  

BindTexture( 0 );  
DefaultTexCoords();  
}  

Any issues, let me know.