Chapter 8: Managing Our Text Strings

In the last chapter, we created the EDTextString class which will allow us to create and control text strings in our OpenGL ES environment, but we don’t yet have all of the pieces. Working with the EDFontImporter class, the EDTextString class can programmatically generate vertex and texture coordinate data for rendering text strings in OpenGL space, but there’s no one there to do the actual rendering calls. At least, not yet.

Our next class, the EDTextStringManager class, will manage EDTextString objects and draw them using their vertex and texture coordinate data. The EDTextStringManager class will also handle loading the font sheet texture and creating the OpenGL program with custom shaders to do the rendering.

Before we jump into the text string manager, though, we have one small administrative thing to take care of. We’re going to need to load an OpenGL texture in the text string manager, but we’re also going to need to load textures in a few more classes that we have yet to create. Instead of copying the texture loading code all over the place, let’s create a utility class to handle common OpenGL tasks like this.

We’re going to create an EDOpenGLTools class and put a class method loadTexture in there that we can use from anywhere we’ll be needing it. I’m going to list the header file and class source, but for a full explanation of how texture loading works, review Part Thirteen: Rendering Textures in the OpenGL ES 2.0 on the iPhone tutorial.

Here’s the EDOpenGLTools header file.

#import <Foundation/Foundation.h>

#import <OpenGLES/ES2/gl.h>
#import <OpenGLES/ES2/glext.h>

@interface EDOpenGLTools : NSObject {
   
}

+ (void)loadTexture:(GLuint *)textureName fromFile:(NSString *)fileName;

@end

I don’t want to have to allocate and initialize this class to use the utility methods, so I’m going to make this a class method. The little plus sign in front of the loadTexture:fromFile: method means that we don’t need to instantiate this class to use this method, we can call it right off of the class.

#import "EDOpenGLTools.h"

@implementation EDOpenGLTools

// Load a 2D texture from a named file

+ (void)loadTexture:(GLuint *)textureName fromFile:(NSString *)fileName {
    UIImage *image = [[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:fileName ofType:nil]];
    CGImageRef imageRef = [image CGImage];
   
    if(imageRef) {
        size_t imageWidth = CGImageGetWidth(imageRef);
        size_t imageHeight = CGImageGetHeight(imageRef);
       
        GLubyte *imageData = (GLubyte *)malloc(imageWidth * imageHeight * 4);
        memset(imageData, 0, (imageWidth * imageHeight * 4));
       
        CGContextRef imageContextRef = CGBitmapContextCreate(imageData, imageWidth, imageHeight, 8, imageWidth * 4, CGImageGetColorSpace(imageRef), kCGImageAlphaPremultipliedLast);
       
        // Invert Y axis
       
        CGContextTranslateCTM(imageContextRef, 0, imageHeight);
        CGContextScaleCTM(imageContextRef, 1.0, -1.0);
       
        CGContextDrawImage(imageContextRef, CGRectMake(0.0, 0.0, (CGFloat)imageWidth, (CGFloat)imageHeight), imageRef);
       
        CGContextRelease(imageContextRef);
       
        // Generate OpenGL texture and load
       
        glGenTextures(1, textureName);
       
        glBindTexture(GL_TEXTURE_2D, *textureName);
       
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, imageWidth, imageHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData);
       
        // Finished handing everything over to OpenGL, free any allocated memory
       
        free (imageData);
       
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
       
        glEnable(GL_BLEND);
       
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
       
        glGenerateMipmap(GL_TEXTURE_2D);
       
        glEnable(GL_TEXTURE_2D);
    }
   
    [image release];
}

@end

This little class method will be called from anywhere that we need to load an OpenGL texture. Right now it’s only used by the new EDTextStringManager class, but soon we’ll have targets and a background to worry about, too.

On to the text string manager. We know that the text string manager will be responsible for loading the OpenGL texture, creating an OpenGL program with custom shaders, and using the output from the EDTextString objects to draw in OpenGL. Since we’ll be loading the texture, that means we’ll need to manage the EDFontImporter object, too.

That’s about it, though. Since we’re a manager class, we won’t need to get too detailed, all of the detailed work will have already been done in each text string that we’re managing. Let’s take a look at the header file for our new EDTextStringManager class.

#import <Foundation/Foundation.h>

#import "EDOpenGLProgram.h"
#import "EDFontImporter.h"
#import "EDTextString.h"
#import "EDOpenGLTools.h"

@interface EDTextStringManager : NSObject {
    EDOpenGLProgram *myProgram; // Reference to associated OpenGL program
   
    GLuint myTexture;           // My texture (font character sheet)
   
    NSMutableArray *myTextStringArray; // My array of text strings to manage
   
    EDFontImporter *myFontImporter; // My font importer
}

- (EDTextStringManager *)initWithCharacterSheetName:(NSString *)newCharacterSheetName;
- (void)addTextString:(EDTextString *)newTextString;
- (void)drawTextString:(EDTextString *)textString;
- (void)drawAllTextStrings;
- (void)destroyAllTextStrings;

@end

After importing the header files we need, we create an instance variable to hold a reference to our associated OpenGL program object. We’re also going to have an associated texture, so we create an instance variable for that too.

We need an array to hold the text strings we’ll be managing, and we also need a reference to the font importer object we’ll be using to import the character coordinates for our text strings.

The method names pretty much describe what each one will be used for. Let’s look at these new methods in detail, in the EDTextStringManager class source file.

#import "EDTextStringManager.h"

enum {
    ATTRIBUTE_VERTEX,
    ATTRIBUTE_TEXTURE_COORD,
    NUM_ATTRIBUTES
};
GLint myAttributes[NUM_ATTRIBUTES];

enum {
    UNIFORM_TEXTURE,
    UNIFORM_COLOR,
    UNIFORM_ALPHA,
    NUM_UNIFORMS
};
GLint myUniforms[NUM_UNIFORMS];

For the performance reason we discussed back in Chapter 3: Converting to the New OpenGL Program Class, we’ll maintain a couple of small arrays to hold our shader attributes and uniforms.

@implementation EDTextStringManager

// Create a new text string manager instance with a particular character font sheet. We also create, compile, and link
// a new OpenGL program.

- (EDTextStringManager *)initWithCharacterSheetName:(NSString *)newCharacterSheetName {
    if((self = [super init])) {
        // Create an importer for managed text strings to use.

        myFontImporter = [[EDFontImporter alloc] init];
       
        [myFontImporter loadCharacterPage:newCharacterSheetName];
       
        // Load character sheet as a texture for this text string manager
       
        [EDOpenGLTools loadTexture:&myTexture fromFile:[myFontImporter getCharacterPageName]];
       
        // initialize OpenGL program
       
        myProgram = [[EDOpenGLProgram alloc] init];
       
        [myProgram setVertexShader:@"EDTextStringManager"];
        [myProgram setFragmentShader:@"EDTextStringManager"];
       
        [myProgram addAttributeLocation:@"ATTRIBUTE_VERTEX" forAttribute:@"vertex"];
        [myProgram addAttributeLocation:@"ATTRIBUTE_TEXTURE_COORD" forAttribute:@"texture_coord"];
       
        [myProgram addUniformLocation:@"UNIFORM_TEXTURE" forUniform:@"texture"];
        [myProgram addUniformLocation:@"UNIFORM_COLOR" forUniform:@"color"];
        [myProgram addUniformLocation:@"UNIFORM_ALPHA" forUniform:@"alpha"];
       
        [myProgram compileAndLink];
       
        myAttributes[ATTRIBUTE_VERTEX] = [myProgram getAttributeIDForIndex:@"ATTRIBUTE_VERTEX"];
        myAttributes[ATTRIBUTE_TEXTURE_COORD] = [myProgram getAttributeIDForIndex:@"ATTRIBUTE_TEXTURE_COORD"];
       
        myUniforms[UNIFORM_TEXTURE] = [myProgram getUniformIDForIndex:@"UNIFORM_TEXTURE"];
        myUniforms[UNIFORM_COLOR] = [myProgram getUniformIDForIndex:@"UNIFORM_COLOR"];
        myUniforms[UNIFORM_ALPHA] = [myProgram getUniformIDForIndex:@"UNIFORM_ALPHA"];
       
        myTextStringArray = nil;
    }
   
    return self;
}

Our initialization class is going to create and configure our font importer, load our font sheet as a texture, and create, configure, and compile our OpenGL program. Let’s break this method down a little.

- (EDTextStringManager *)initWithCharacterSheetName:(NSString *)newCharacterSheetName {
    if((self = [super init])) {
        // Create an importer for managed text strings to use.

        myFontImporter = [[EDFontImporter alloc] init];
       
        [myFontImporter loadCharacterPage:newCharacterSheetName];

The EDTextStringManager’s init method takes the name of a font sheet as a parameter, so we pass that sheet name into the font importer. Calling the loadCharacterPage in the font importer will process the page and make all of the character index and coordinate data available to us.

        // Load character sheet as a texture for this text string manager
       
        [EDOpenGLTools loadTexture:&myTexture fromFile:[myFontImporter getCharacterPageName]];

Next, we call our new loadTexture:fromFile: method with the font page name. I could have used either the passed-in value of newCharacterPageName or the getCharacterSheetName method in the font importer as the font page name parameter. Since we hadn’t seen the font importer’s getCharacterPageName method in action before, I figured I’d go with that.

       // initialize OpenGL program
       
        myProgram = [[EDOpenGLProgram alloc] init];
       
        [myProgram setVertexShader:@"EDTextStringManager"];
        [myProgram setFragmentShader:@"EDTextStringManager"];
       
        [myProgram addAttributeLocation:@"ATTRIBUTE_VERTEX" forAttribute:@"vertex"];
        [myProgram addAttributeLocation:@"ATTRIBUTE_TEXTURE_COORD" forAttribute:@"texture_coord"];
       
        [myProgram addUniformLocation:@"UNIFORM_TEXTURE" forUniform:@"texture"];
        [myProgram addUniformLocation:@"UNIFORM_COLOR" forUniform:@"color"];
        [myProgram addUniformLocation:@"UNIFORM_ALPHA" forUniform:@"alpha"];
       
        [myProgram compileAndLink];
       
        myAttributes[ATTRIBUTE_VERTEX] = [myProgram getAttributeIDForIndex:@"ATTRIBUTE_VERTEX"];
        myAttributes[ATTRIBUTE_TEXTURE_COORD] = [myProgram getAttributeIDForIndex:@"ATTRIBUTE_TEXTURE_COORD"];
       
        myUniforms[UNIFORM_TEXTURE] = [myProgram getUniformIDForIndex:@"UNIFORM_TEXTURE"];
        myUniforms[UNIFORM_COLOR] = [myProgram getUniformIDForIndex:@"UNIFORM_COLOR"];
        myUniforms[UNIFORM_ALPHA] = [myProgram getUniformIDForIndex:@"UNIFORM_ALPHA"];
       
        myTextStringArray = nil;
    }
   
    return self;
}

The last bit of processing we do is to create, configure, and compile a new OpenGL program with our custom shaders for drawing our text strings. We’ll look at the shaders as soon as we’ve been through the rest of the text string manager code.

// The addTextString: method will add a new text string object to the manager, and
// set the text string's font importer to whatever the manager has initialized.

- (void)addTextString:(EDTextString *)newTextString {
    if(myTextStringArray == nil) {
        myTextStringArray = [[NSMutableArray alloc] init];
    }
   
    [newTextString setImporter:myFontImporter];
   
    [myTextStringArray addObject:newTextString];
}

The addTextString method simply uses lazy initialization to make sure that the myTextStringArray is valid, sets the text string’s font importer to the current font importer, and adds it to our internal array of text strings.

// The drawTextString: method will draw a specific text string. This method is mainly used by the
// drawAllTextStrings method, but has been broken out for convenience.

- (void)drawTextString:(EDTextString *)textString {
    glUseProgram([myProgram programId]);
   
    glBindTexture(GL_TEXTURE_2D, myTexture);
   
    glUniform1i(myUniforms[UNIFORM_TEXTURE], 0);
    glUniform3fv(myUniforms[UNIFORM_COLOR], 1, [textString getOpenGLColors]);
    glUniform1f(myUniforms[UNIFORM_ALPHA], [textString getOpenGLAlpha]);
   
    glVertexAttribPointer(myAttributes[ATTRIBUTE_VERTEX], 3, GL_FLOAT, 0, 0, [textString getOpenGLVertexArray]);
    glEnableVertexAttribArray(myAttributes[ATTRIBUTE_VERTEX]);
   
    glVertexAttribPointer(myAttributes[ATTRIBUTE_TEXTURE_COORD], 2, GL_FLOAT, 0, 0, [textString getOpenGLTextureCoordArray]);
    glEnableVertexAttribArray(myAttributes[ATTRIBUTE_TEXTURE_COORD]);
   
#if defined(DEBUG)
    if(![myProgram validate]) {
        NSLog(@"Validate program [%d] failed!", [myProgram programId]);
        return;
    }
#endif
   
    glDrawArrays(GL_TRIANGLES, 0, [textString getOpenGLNumVertices]);
   
    [textString update];
}

Well, this has got to look familiar by now. look through the method code and note how nearly all of the uniform and vertex data is coming straight out of the EDTextString object. The manager’s only job is to get that information to OpenGL. After calling glDrawArrays(), the manager calls the text string’s update method to make sure that the text strings drift and fade as they should be doing.

You may also notice that this method only handles a single text string, where the manager is supposed to be managing any number of them. The next method will take care of that for us.

// The drawAllTextStrings runs through all of the text strings in this manager and
// calls drawTextString: for each one

- (void)drawAllTextStrings {
    if(myTextStringArray == nil) {
        return;
    }
   
    // Our text strings know if they're dead or alive, so cull any that don't
    // report an alive status. You're not supposed to modify an array as you iterate
    // through it, but going backwards is safe, since any element shifting will be
    // going on behind our processing.
   
    for(int i = [myTextStringArray count] - 1; i >= 0; i--) {
        if(![[myTextStringArray objectAtIndex:i] isAlive]) {
            [myTextStringArray removeObjectAtIndex:i];
        }
    }
   
    for(int i = 0; i < [myTextStringArray count]; i++) {
        [self drawTextString:[myTextStringArray objectAtIndex:i]];
    }
}

The drawAllTextStrings method will loop through our array of managed text strings an call the drawTextString: method we just looked at for each one.

First, this method checks to make sure that there are any text strings to draw. If the myTextStringArray variable is nil, there’s no reason to be here, so we return.

The next bit of code is bound to get me trouble with someone, but it works and I like it. Apple, and many others, say you must never update an array while you are iterating through it, and I agree with them, for the most part. I agree if you’re going forward, anyway.

Imagine that my array has 10 elements, and I want to remove elements 3 and 7. I begin to iterate from 0, and when I get to 3, I remove it. I continue on to 7, and then remove that. But there’s a problem: was that really 7? When I removed 3, didn’t everything shift down towards zero, making 7 become 6? maybe it did, maybe it didn’t; it really depends on how that particular array class was coded behind the scenes.

The recommended way to delete items from an array is either create another array of items to delete and use a method like removeObjectsInArray, or, alternately, create a new array of items you want to keep and then delete the other array, assigning your new array to the variable that used to point to the old array.

I think that there’s a third way: go through the array backwards and delete from the highest entry to the lowest. In my earlier example, if going backwards, when I deleted 7, 3 would still be 3. When I got to 3, I could delete it with confidence.

While I can see that this code works, I am open to debate as to whether it’s technically correct and safe to use or not. I’ll go with it for now, but if I ever find a real problem with doing things this way, I promise to update the code and tutorial, and explain why.

Once the dead text strings have been removed from our myTextStringArray array, we loop through the ones that are still alive and call drawTextString: for each one.

The final method will clear out our managed text strings.

// If you need to remove all text strings managed by this object, this is the
// method for you.

- (void)destroyAllTextStrings {
    [myTextStringArray removeAllObjects];
}

@end

There may be cases where you need to clear the screen or clear a bunch of text for some reason, and this method will do it quickly and efficiently.

That’s the entire EDTextStringManager class. The only thing left is to code our custom shaders.

First, the vertex shader, EDTextStringManager.vsh.

attribute vec4 vertex;
attribute vec4 texture_coord;

varying vec4 varTextureCoord;

void main()
{
    gl_Position = vertex;
   
    varTextureCoord = texture_coord;
}

Pretty standard vertex shader with texture coordinates. There’s not much to talk about here, so let’s go on to the fragment shader, EDTextStringManager.fsh.

varying highp vec4 varTextureCoord;

uniform sampler2D texture;
uniform highp vec3 color;
uniform highp float alpha;

void main()
{
    highp float threshold = 0.98;
   
    gl_FragColor = texture2D(texture, varTextureCoord.st);
   
    if(gl_FragColor.r > threshold && gl_FragColor.g > threshold && gl_FragColor.b > threshold)
        discard;
   
    gl_FragColor.rgb += color.rgb;
    gl_FragColor.a = alpha;
   
}

The fragment shader defines a threshold that will be used to discard the white character backgrounds. After the gl_FragColor has been set to the texture fragment color values, it’s checked against our threshold value. If the gl_FragColor is above the threshold, meaning it’s white, and therefore part of the background, it is discarded.

The OpenGL shader keyword ‘discard’ causes the fragment shader to return without drawing anything on the screen, and we’ll use this functionality to make our character backgrounds transparent.

If you use these text strings on a particularly dark background, you can lower the threshold value to prevent the brighter parts of the letter from being drawn.

Next, we add our color vector passed in from the text string to make this text string whatever color we had set it to. Since black is RGB 0,0,0, we can just add our color values to it and get that color. If my color was blue, 0,0,255, adding that to 0,0,0 would yield 0,0,255.

The last thing we do is set our alpha value to whatever the text string wanted it to be. This is how we’ll fade out text strings.

And now for something completely different.

When you create shaders from empty files in Xcode, like I do, Xcode doesn’t know what to do with them. It doesn’t recognize the .vsh and .fsh file extensions, but figures that they must be source code of some sort and flags them for compilation.

After creating shaders, you must go into the Xcode Target Build Phases and move the shaders around, otherwise they’ll not only create warnings as Xcode tries to compile them, but they won’t be copied to your resource bundle where they belong, either.

 

 

In the Build Phases window, look closely at the Compile Sources group, and remove your shaders from it.

 

 

Just highlight the shaders and click on the minus sign at the bottom, or press delete. After removing the shaders from the Compile Sources, add them to the Copy Bundle Resources group.

 


 

Click on the plus sign at the bottom of the Copy bundle Resources group, find your new shaders, highlight them, and click on the Add button.

Save your project and you’re ready to go.

Unfortunately, there’s nowhere to ‘go’ just yet, but don’t worry, we’re going to finally make use of all of these shiny new classes in the next chapter.

Chapter 7 | Index | Chapter 9