Chapter 7: Creating the Text String Class

In the last chapter, we thought through what we’re trying accomplish with our new text string classes, and how we could best make use of the new EDFontImporter class. In this chapter, we’ll build what we need to create a drifting text demo on the iPhone.

First, let’s look at the header file for our new EDTextString class.

#import <Foundation/Foundation.h>

#import "EDFontImporter.h"

@interface EDTextString : NSObject {
    NSString *myTextString;     // The text I draw
   
    GLfloat *vertexArray;       // Array of triangles I will be drawing text upon
    GLfloat *textureCoordArray; // Array of texture coordinates to use for drawing the text
   
    BOOL alive;                 // Indicates if I am active
   
    float position[3];          // My screen position (X, Y, & Z)
    float color[3];             // My text color (R, G, & B)
    float alpha;                // My alpha value (opacity)
    float lifespan;             // How long I should remain alive
    float decayAt;              // When I should start to decay (fade)
    float lifeleft;             // How much life I have left
    float drift[3];             // How much I should drift per update (X, Y, & Z)
    int size;                   // How big my text should be
    float sizeConversion;       // Calculate screen size to OpenGL size conversion
    BOOL centered;              // Should I be centered on my coordinates
    float centerOffset;         // Calculated offset for centering
   
    int myScreenWidth, myScreenHeight;  // Screen width and height, from OpenGL
   
    EDFontImporter *myFontImporter;
}

- (EDTextString *)initWithString:(NSString *)newTextString;

- (void)setString:(NSString *)newTextString;
- (void)setImporter:(EDFontImporter *)newImporter;

- (void)setPositionX:(int)newX andY:(int)newY andZ:(int)newZ;
- (void)setColorRed:(int)newRed andGreen:(int)newGreen andBlue:(int)newBlue;
- (void)setColorAlpha:(int)newAlpha;
- (void)setLifespan:(int)newLifespan withDecayAt:(int)newDecayAt;
- (void)setDriftX:(int)newX andY:(int)newY andZ:(int)newZ;
- (void)setSize:(int)newSize;
- (void)setCentered:(BOOL)newCentered;

- (NSString *)getString;
- (EDFontImporter *)getImporter;

- (float *)getOpenGLPosition;
- (float *)getOpenGLColors;
- (float)getOpenGLAlpha;
- (int)getSize;
- (BOOL)isCentered;
- (BOOL)isAlive;

- (float *)getOpenGLVertexArray;
- (float *)getOpenGLTextureCoordArray;
- (int)getOpenGLNumVertices;

- (void)populateVertexArray;
- (void)populateTextureCoordArray;
- (void)update;

@end

Even though we import the EDFontImporter class, we’re only doing so because we need to declare an instance variable to hold a reference to it. The text string manager will actually be responsible for instantiating and initializing the font importer, but it will pass a reference to it into any text strings it manages. The individual text strings will need access to the font importer in order to properly calculate the backing triangle vertices and texture coordinates needed to render themselves properly. It will pass this information back to the manager, who will take care of the OpenGL calls needed to actually draw the individual strings.

The instance variables are commented well enough that I don’t think we need to go over them in too much detail here. We’ll be looking at all of these instance variables in detail as we go through the class source file. Since the same goes for the method definitions, let’s go straight into the EDTextString class source.

#import "EDTextString.h"

@implementation EDTextString

// initialize defaults and set text string and importer object. Since we'll be relying on
// OpenGL for screen dimensions, this class can only be used AFTER an OpenGL context has
// been created.

- (EDTextString *)initWithString:(NSString *)newTextString {
    if((self = [super init])) {
        // Set defaults. Although the values are passed into this object as screen coordinates,
        // they are converted to and stored as OpenGL values
       
        vertexArray = NULL;
        textureCoordArray = NULL;
       
        alive = TRUE;  // Start active
       
        color[0] = 0;   // Black
        color[1] = 0;
        color[2] = 0;
       
        alpha = 1;      // Full opacity
       
        lifespan = 0;   // Live forever
        lifeleft = 0;
        decayAt = 0;
       
        drift[0] = 0;   // No drift in any direction
        drift[1] = 0;
        drift[2] = 0;
       
        centered = TRUE;    // Start centered
       
        size = 10; // Default to 10 pixels high
       
        // Get screen resolution from OpenGL

        glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &myScreenWidth);
        glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &myScreenHeight);
       
        [self setString:newTextString];
    }
   
    return self;
}

This is a pretty standard initialization routine, but it is important to note that we’re using the glGetRenderbufferParameteriv() function to get our screen width and height. In order for this to work, this class must be instantiated after the OpenGL context has been created by the EAGLView class.

We also use our own setString: method to set the initial string text, since there is some additional processing associated with it.

// The setString: method will assign the new string. Since new texture
// coordinates may be needed, we'll free any existing texture coordinates
// array and set it to NULL. Setting it to NULL will force a refresh the next time
// the populateTextureCoordinateAray method is called.

- (void)setString:(NSString *)newTextString {
    if(textureCoordArray != NULL) {
        free(textureCoordArray);
        textureCoordArray = NULL;
    }

    myTextString = newTextString;
   
    // Changing a text string will result in it being alive again, fully opaque, and
    // set to 'live forever'.
   
    alive = TRUE;
    alpha = 1;
    lifespan = 0;
}

The setString: method needs to do a little more than just assign a new value to our myTextString instance variable. Since the texture coordinates are related directly to the contents of the string, the texture array, if it exists, must be freed and set to nil so that it will be recalculated the next time this (new) string needs to be rendered.

Also, in the case where we might be reusing an EDTextString object after it’s no longer alive (after it’s faded out and been removed from the string manager), we’ll need to reset a few instance variable to guarantee that the string is visible once it’s added back to the manager.

// The setImporter: method will assign a new EDFontImporter object, which will be
// needed for most text manipulation functions of this class

- (void)setImporter:(EDFontImporter *)newImporter {
    myFontImporter = newImporter;
}

This method is called by the manager to let the individual text string know which font set it’s working with. The text string will need to use information from the font importer to properly create its vertex and texture coordinate arrays.

// The setPositionX:andY:andZ: method will take screen coordinates and store them as
// OpenGL coordinates to use later.

-(void)setPositionX:(int)newX andY:(int)newY andZ:(int)newZ {
    // The positions will be held in OpenGL coordinates, so we need to
    // convert them.
   
    float conversionXFactor = 2.0 / (float)myScreenWidth;
    float conversionYFactor = 2.0 / (float)myScreenHeight;
   
    position[0] = conversionXFactor * ((float)newX - ((float)myScreenWidth / 2.0)); // X
    position[1] = conversionYFactor * ((float)newY - ((float)myScreenHeight / 2.0)) * -1; // Y (inverted)
    position[2] = 0; // Ignore Z for now, but code for it later
}

Here we have our first coordinate conversion, and it’s not even one of the ones we talked about earlier! What the setPositionX:andY:andZ: method is doing is allowing the calling program to specify the position of this text string in normal screen coordinates. For example, the iPhone screen is normally 320 x 480, so if I touch near the middle of the top of the screen, my iPhone screen coordinates will be somewhere close to (160, 10).

An X coordinate of 160 is midway across the screen, and a Y coordinate of 10 is only 10 pixels down from the top (technically it’s 11, but let’s keep this simple). OpenGL is going to see this same location as (0, 0.9583). An OpenGL X coordinate of 0 is midway across OpenGL space (that ranges from -1 to 1, left to right), and an OpenGL Y coordinate of 0.9583 is very close to the top of OpenGL space, (which also ranges from -1 to 1, bottom to top).

So we have two problems to deal with in allowing the caller to use iPhone screen coordinates but save this information as OpenGL coordinates: we must convert the values from one range to another, and we must invert the Y axis. We need to invert the Y axis because the iPhone screen increases as it goes towards the bottom, but the OpenGL Y axis increases as it goes up.

For this first coordinate conversion, we’re going to walk through all of the math necessary to complete the conversion, and in the future we’ll just note the conversions at a higher level.

Let’s start by pretending that the user has touched the iPhone screen at (245, 400), somewhere towards the lower right-hand corner of the screen. The method call would look like this:

[textString setPositionX:245 andY:400 andZ:0]

We’re taking a Z value for future development, but we’re not doing anything with it in this tutorial. Someday, though, I might want text to fly towards the screen, increasing in size as it does so. In that case, I will need a Z coordinate, so I’ll code for the future.

The first thing our method does is calculate X and Y conversion factors for our screen. We do this every time we’re called just in case the screen resolution was changed between calls. It’s unlikely, but it would cause a mess if we didn’t handle it properly if it did.

The way we calculate the conversion is to divide our OpenGL space by our iPhone screen space. The OpenGL X axis has two full units, -1 to 0, and 0 to 1. The iPhone screen’s X axis has 320 units (pixels). The math to calculate the conversion factor looks like this:

2 / 320 = 0.00625

This tells us that one pixel on the iPhone screen’s X axis takes up 0.00625 units on OpenGL space’s X axis. Now for the Y axis:

2 / 480 = 0.004166666666667

This tells us that one pixel on the iPhone screen’s Y axis takes up 0.004166666666667 units in OpenGL space’s Y axis. Before we can use these values to calculate the OpenGL coordinates, there’s one other thing to consider: half of the OpenGL axes are negative, so we need to shift the iPhone coordinates by half of the screen width and height to make sure they end up in the proper quadrants.

To perform the sift, the equation looks like this:

shifted_x = x - (width / 2)
shifted_y - y - (height / 2)

Let’s apply the shift and see what we’re left with.

shifted_x = 245 - (320 / 2) = 245 - 160 = 85
shifted_y = 400 - (480 / 2) = 400 - 240 = 160

Why did we do this? Because OpenGL sees (0,0) as the center of the screen, but the iPhone sees (160,240) as the center. We must shift the coordinates to make sure that X coordinates that fall left of center are negative, and Y coordinates that fall below center are negative.

Now if we multiply the shifted X and Y coordinates by our conversion values, we get the following:

85 * 0.00625 = 0.53125
160 * 0.004166666666667 = 0.66666666666672

So our iPhone coordinates converted from (245,400) to OpenGL coordinates of (0.53125,0.66666666666672).

But wait, that’s not right! That Y coordinate is more than three quarters of the way up the screen, but our user touched near the bottom. Since the iPhone screen Y axis runs from top to bottom, and the OpenGL Y axis runs bottom to top, we invert the Y axis my multiplying by -1. After the inversion, the correct OpenGL coordinates are (0.53125,-0.66666666666672).

Let’s go over that logic one more time, using an X value of 245 and a Y value of 400. First, let’s look at that code again.

    float conversionXFactor = 2.0 / (float)myScreenWidth;
    float conversionYFactor = 2.0 / (float)myScreenHeight;
   
    position[0] = conversionXFactor * ((float)newX - ((float)myScreenWidth / 2.0)); // X
    position[1] = conversionYFactor * ((float)newY - ((float)myScreenHeight / 2.0)) * -1; // Y (inverted)

Let’s do the math manually and see what the program does.

conversionXFactor = 2.0 / 320 = 0.00625
conversionYFactor = 2.0 / 480 = 0.004166666666667

position[0] = 0.00625 * (245 - (320 / 2.0)) = 0.53125
position[1] = 0.004166666666667 * (400 - (480 / 2)) * -1 = -0.666666666666667

Throughout the code you’ll see small numbers being divided by larger ones, and these will be conversion factors used to convert between various coordinate systems and value ranges. Looking at the next method yields yet another example of a conversion needed for OpenGL.

// Take byte color values and convert them to OpenGL. Technically, we could make
// OpenGL do this, but it would have to do it over and over for each draw. If we
// do it here, it's only done once whenever a new color is set.

- (void)setColorRed:(int)newRed andGreen:(int)newGreen andBlue:(int)newBlue {
    // The colors will be in byte values (0 - 255), so convert them to
    // OpenGL values (0.0 - 1.0).
   
    float conversionFactor = 1.0 / (float)255;
   
    color[0] = conversionFactor * (float)newRed;
    color[1] = conversionFactor * (float)newGreen;
    color[2] = conversionFactor * (float)newBlue;
}

We normally look at color values as bytes, ranging from 0 through 255. OpenGL sees those same color values as floats, ranging from 0 to 1. If we’re going to store color values for OpenGL, we need to convert them from byte values to floats. In the setColorRed:andGreen:andBlue: method, we do just that.

OpenGL uses one unit to represent color, while a byte allows 256 (0 through 255). We can’t actually use the value ‘256’, though, so we will divide by 255 to make sure we don’t throw off the values. For example, if I passed in 255 (the highest possible color value) but my conversion factor was (1 / 256), or 0.00390625, I would end up with an OpenGL value of 0.99609375. It should have been 1, since 1 is the OpenGL version of ‘all the way on’, just like a byte value of 255.

This method stores the converted color values in the color array, just like the previous method stored the converted coordinate values in the position array.

// Works just like the setColorRed:andGreen:andBlue: method, but for alpha value

- (void)setColorAlpha:(int)newAlpha {
    float conversionFactor = 1.0 / (float)255;
   
    alpha = conversionFactor * (float)newAlpha;
}

The alpha value is converted and stored just like a color value, and will eventually be used by the shader to adjust the alpha value for fading text out.

// The setLifespan:withDecayAt: method simply sets the lifespan and decay values

- (void)setLifespan:(int)newLifespan withDecayAt:(int)newDecayAt {
    lifespan = newLifespan;
    decayAt = newDecayAt;
   
    lifeleft = lifespan; // Default lifeleft to full lifespan
}

Sometimes, we’ll want text to pop up for a limited period of time, then disappear. The lifespan is the number of screen refreshes this text will persist before it is set to not alive and removed from the text string manager. The decayAt value will set a point in its life at which it will begin to fade (decay). If I set a lifespan to 100 and a decayAt value of 100, the text will begin to fade right away, and slowly disappear.

If I set a lifespan of 100 and a decayAt value of 50, the text will remain completely opaque for half of its life, then start to fade for the last half.

The lifeleft variable is an internal value used for calculating the proper amount of fading to apply to the text.

// The setDriftX:andY:andZ: method will set the amount of drift per
// update for this text string. The user will pass in screen pixel
// values, so we'll convert to OpenGL values.

- (void)setDriftX:(int)newX andY:(int)newY andZ:(int)newZ {
    float conversionXFactor = 2.0 / (float)myScreenWidth;
    float conversionYFactor = 2.0 / (float)myScreenHeight;
   
    drift[0] = conversionXFactor * (float)newX; // X drift
    drift[1] = conversionYFactor * (float)newY * -1; // Y drift (invert for OpenGL)
    drift[2] = 0; // Z drift - ignore for now
}

We’re going to give text strings the agility to drift in some direction over the span of their lives. Specifically, when we touch a target (or miss), a score will pop up and drift slowly upwards as it fades out. Drift values are specified as pixels, but converted to OpenGL values before storing.

// The setSize: method will set the size modifier, which is used
// to adjust the size of the font when creating the vertex array.

- (void)setSize:(int)newSize {
    size = newSize;
}

The size of the character, in pixels. The size value is not converted to OpenGL space because it will be used to multiply against the X and Y conversion factors when assembling the vertex array for the backing triangles.

// Set whether text should be centered (TRUE) or not (FALSE)

- (void)setCentered:(BOOL)newCentered {
    centered = newCentered;
}

The text defaults to centered, but can be set to start rendering at a set of coordinates if desired. Centering is useful for the floating scores when targets are touched, but if I wanted to draw a text string starting at the left side of the screen, centering would be undesirable. I can use this method to turn off centering if I need to.

The next set of methods will be returning configuration values that we’ve set so far. They’re all pretty straightforward and have decent comments, so I’m just going to list them here.

// Returns my current text string

- (NSString *)getString {
    return myTextString;
}

// Returns my current font importer

- (EDFontImporter *)getImporter {
    return myFontImporter;
}

// Returns vector of X, Y, & Z coordinates. We've already converted these
// to OpenGL space, so just return

- (float *)getOpenGLPosition {
    return position;
}

// Returns a vector of R, G, & B colors. We've already converted these to
// OpenGL values, so just return.

- (float *)getOpenGLColors {
    return color;
}

// Return OpenGL alpha value

- (float)getOpenGLAlpha {
    return alpha;
}

// Return pixel size of text string

- (int)getSize {
    return size;
}

// Return centered (TRUE) or not centered (FALSE)

- (BOOL)isCentered {
    return centered;
}

// Return alive indicator

- (BOOL)isAlive {
    return alive;
}

The only thing here worth noting is that the getOpenGLPosition and getOpenGLColors methods are returning pointers to arrays of values. We’ll see how the text string manager makes use of these return values when we go over that code.

The next three methods are what make all of this work. The vertex array and the texture coordinate array are what the text string manager uses to instruct OpenGL to render this text string. The number of vertices to draw is required by OpenGL to use the first two arrays.

// This method will populate the vertex array needed to draw our
// character text string. We will populate the array and then return it.
// The vertex array needs to be populated every time, since the update
// logic (drift, etc) may affect it's positioning

- (float *)getOpenGLVertexArray {
    [self populateVertexArray];
    return vertexArray;
}

// This method will populate the texture coordinate array needed to draw our
// character text string. We will populate the array and then return it.
// The texture coordinate only needs to be populated if it's NULL (initialliy)
// or the text string has been changed (which will free the array and set it
// to NULL).

- (float *)getOpenGLTextureCoordArray {
    if(textureCoordArray == NULL) {
        [self populateTextureCoordArray];
    }
   
    return textureCoordArray;
}

// The OpenGLNumVertices method will return the number of vertices needed to render
// our text string. We need two triangles per letter, with 3 vertices per triangle.

- (int)getOpenGLNumVertices {
    return [myTextString length] * 6;
}

The getOpenGLPosition method will re-populate the vertex array every time, since the text may be drifting. Each text string is responsible for tracking its own position, the text string manager will simply be rendering them.

The getOpenGLTextureCoordArray method will only populate the texture coordinate array if it’s null. Once populated, the texture coordinate array does not need to be updated unless the text string changes its text. If you remember the setString: method, it frees the texture coordinate array (if needed) and sets it to nil for this very reason.

Finally, the getOpenGLNumVertices reports back the number of vertices in these arrays. The number of vertices will always by the number of characters times 6, which is the number of vertices it takes to make the two triangles that form the backing for the character’s texture fragment.

The next method is the core of this class, and does all of the hard work figuring out where in OpenGL space the backing triangles for this text string should go. I’m going to list the entire method, then go through it bit by bit.

// This method will create an array of vertices needed by OpenGL to render the
// triangles used for the texture coordinates to display individual characters.

- (void)populateVertexArray {
    // We'll walk through our text string, locating the vertex information for
    // each character in the data provided by the font importer.
   
    float x, y, z;      // Our vertex coordinates
    float ulX, ulY;     // Upper left X and Y
    float lrX, lrY;     // Lower right X and Y
   
    int textStringLength = [myTextString length];
   
    float totalLength = 0;
   
    if(vertexArray != NULL) {
        // If the vertex array is populated (from a previous render), free it
        free(vertexArray);
    }
   
    // Allocate space for our vertex array. 3 coordinates per vertex, 3 vertices per triangle,
    // 2 triangles per letter.
   
    vertexArray = malloc(textStringLength * sizeof(GLfloat) * 3 * 3 * 2);
   
    x = position[0]; // Text string's X position
    y = position[1]; // Text string's Y position
    z = position[2]; // Text string's Z position
   
    float conversionXFactor = 2.0 / (float)myScreenWidth;
    float conversionYFactor = 2.0 / (float)myScreenHeight;
   
    // Our height will be set to size, and the width will need to be calculated based on the ratio
    // between the raw height and width of the character coordinate data.
   
    // Get character information from importer
   
    char *characterIndex = [myFontImporter getCharacterIndex];
    int *characterCoords = [myFontImporter getCoordinateArray];
   
    // Walk through text string and calculate / store vertex data
   
    for(int i = 0; i < textStringLength; i++) {
        char currentChar = [myTextString characterAtIndex:i];
        BOOL charFound = FALSE; // Did we find the character?
       
        for(int j = 0; j < NUM_CHARACTERS; j++) {
            if(characterIndex[j] == currentChar) {
                // The character was located, so we need to build the vertex
                // information based on the appropriate coordinate information.
               
                int arrayIndex = j * 4; // There are four coordinates per character
               
                ulX = characterCoords[arrayIndex];
                ulY = characterCoords[arrayIndex + 1];
                lrX = characterCoords[arrayIndex + 2];
                lrY = characterCoords[arrayIndex + 3];
               
                int letterWidth = (lrX - ulX);
                int letterHeight = (lrY - ulY);
               
                // We know how high the character should be (from the 'size' variable),
                // so we can convert that and be fine. For the width, however, we need to
                // calculate an aspect ratio before we can accurately calculate.
               
                float characterAspectRatio = (float)letterWidth / (float)letterHeight;
               
                // Now apply that aspect ratio to our height and that will be our width.
               
                int adjLetterWidth = size * characterAspectRatio;
                int adjLetterHeight = size;
               
                totalLength += (adjLetterWidth * conversionXFactor);
               
                int vertexIndex = i * 18;
               
                // The first vertex is our current X, Y, & Z position

                vertexArray[vertexIndex    ] = x;
                vertexArray[vertexIndex + 1] = y;
                vertexArray[vertexIndex + 2] = z;
               
                // We then go right for our letter width
               
                x += adjLetterWidth * conversionXFactor;
               
                vertexArray[vertexIndex + 3] = x;
                vertexArray[vertexIndex + 4] = y;
                vertexArray[vertexIndex + 5] = z;
               
                // Next, go up and left for the final vertex of the first triangle
               
                x -= adjLetterWidth * conversionXFactor;
                y += adjLetterHeight * conversionYFactor;
               
                vertexArray[vertexIndex + 6] = x;
                vertexArray[vertexIndex + 7] = y;
                vertexArray[vertexIndex + 8] = z;
               
                // Now for our second triangle - go to the top right corner
               
                x += adjLetterWidth * conversionXFactor;
               
                vertexArray[vertexIndex + 9 ] = x;
                vertexArray[vertexIndex + 10] = y;
                vertexArray[vertexIndex + 11] = z;
               
                // Back to the top left corner
               
                x -= adjLetterWidth * conversionXFactor;
               
                vertexArray[vertexIndex + 12] = x;
                vertexArray[vertexIndex + 13] = y;
                vertexArray[vertexIndex + 14] = z;
               
                // And finally ending our second triangle in the lower right corner
               
                x += adjLetterWidth * conversionXFactor;
                y -= adjLetterHeight * conversionYFactor;
               
                vertexArray[vertexIndex + 15] = x;
                vertexArray[vertexIndex + 16] = y;
                vertexArray[vertexIndex + 17] = z;
               
                charFound = TRUE;
                break;
            }
        }
       
        // If the character was NOT found, we need to grab a known character as
        // a place holder. This character will not be rendered, so it's only used
        // for spacing. When the populateTextureCoordArray processing hits unknown
        // characters, it purposely mangles the texture coordinates to nothing will
        // be drawn.
       
        if(charFound == FALSE) {
            currentChar = '-';  // The dash is a nice spacing character, width-wise
           
            // If the dask character is not found, processing is undefined. We may crash or
            // render improperly, so make sure that your character sheets are formatted to spec.

            for(int j = 0; j < NUM_CHARACTERS; j++) {
                if(characterIndex[j] == currentChar) {
                    // The character was located, so we need to build the vertex
                    // information based on the appropriate coordinate information.
                   
                    int arrayIndex = j * 4; // There are four coordinates per character
                   
                    ulX = characterCoords[arrayIndex];
                    ulY = characterCoords[arrayIndex + 1];
                    lrX = characterCoords[arrayIndex + 2];
                    lrY = characterCoords[arrayIndex + 3];
                   
                    int letterWidth = (lrX - ulX);
                    int letterHeight = (lrY - ulY);
                   
                    // We know how high the character should be (from the 'size' variable),
                    // so we can convert that and be fine. For the width, however, we need to
                    // calculate an aspect ratio before we can accurately calculate.
                   
                    float characterAspectRatio = (float)letterWidth / (float)letterHeight;
                   
                    // Now apply that aspect ratio to our height and that will be our width.
                   
                    int adjLetterWidth = size * characterAspectRatio;
                    int adjLetterHeight = size;
                   
                    totalLength += (adjLetterWidth * conversionXFactor);
                   
                    int vertexIndex = i * 18;
                   
                    // The first vertex is our current X, Y, & Z position
                   
                    vertexArray[vertexIndex    ] = x;
                    vertexArray[vertexIndex + 1] = y;
                    vertexArray[vertexIndex + 2] = z;
                   
                    // We then go right for our letter width
                   
                    x += adjLetterWidth * conversionXFactor;
                   
                    vertexArray[vertexIndex + 3] = x;
                    vertexArray[vertexIndex + 4] = y;
                    vertexArray[vertexIndex + 5] = z;
                   
                    // Next, go up and left for the final vertex of the first triangle
                   
                    x -= adjLetterWidth * conversionXFactor;
                    y += adjLetterHeight * conversionYFactor;
                   
                    vertexArray[vertexIndex + 6] = x;
                    vertexArray[vertexIndex + 7] = y;
                    vertexArray[vertexIndex + 8] = z;
                   
                    // Now for our second triangle - go to the top right corner
                   
                    x += adjLetterWidth * conversionXFactor;
                   
                    vertexArray[vertexIndex + 9 ] = x;
                    vertexArray[vertexIndex + 10] = y;
                    vertexArray[vertexIndex + 11] = z;
                   
                    // Back to the top left corner
                   
                    x -= adjLetterWidth * conversionXFactor;
                   
                    vertexArray[vertexIndex + 12] = x;
                    vertexArray[vertexIndex + 13] = y;
                    vertexArray[vertexIndex + 14] = z;
                   
                    // And finally ending our second triangle in the lower right corner
                   
                    x += adjLetterWidth * conversionXFactor;
                    y -= adjLetterHeight * conversionYFactor;
                   
                    vertexArray[vertexIndex + 15] = x;
                    vertexArray[vertexIndex + 16] = y;
                    vertexArray[vertexIndex + 17] = z;

                    break;
                }
            }
        }
    }
   
    // If we're supposed to be centered, adjust our X positions by half of our
    // total length
   
    if(centered == TRUE) {
        for(int i = 0; i < (textStringLength * 18); i += 3) {
            vertexArray[i] -= (totalLength / 2.0);
        }
    }
}

First, a description of what’s going on here. We know we have a text string of a certain length, and we know that each of the characters in that text string is going to need its own pair of triangles to back the texture fragment that contains the letter image.

We also know that each letter is a different width, so we need to calculate the dimensions of each pair of triangles to match the width of the character that they will be used to represent.

We’re going to walk through all of our characters in our text string, looking for that character in our index array. Once we’ve located the character in the index array, we use that position to retrieve the character coordinate data from the character coordinate array. For every one character on the index array, there will be four pieces of data in the character coordinate array, upper left X and Y, and lower right X and Y. That being the case, we can just multiply the index value by 4, and that will be the start of the coordinate data in the character coordinate array.

Once we know where in the character coordinate array our values are, we use those values to calculate how wide our triangles should be.

If the character in our text string was not found, we go through the loop again, hardcoding a dash character (-). We use a dash character because it’s an average width, and since nothing will be rendered on those triangles, we don’t want the text around it to be spaced too far apart or too close together.

This is also how we render spaces, actually, since our font importer can’t detect a character that isn’t there (the space character), we rely on it not being in our character index to have it handled as a space, along with any other characters that aren’t in the array.

The last thing we do is check to see if the string should be centered. If so, we subtract half of its width from the starting location to center the entire string over the starting coordinates. The length of the string, in OpenGL space, is accumulated during the character processing.

Now we’ll look at the method in more detail.

- (void)populateVertexArray {
   
    float x, y, z;      // Our vertex coordinates
    float ulX, ulY;     // Upper left X and Y
    float lrX, lrY;     // Lower right X and Y
   
    int textStringLength = [myTextString length];
   
    float totalLength = 0;

The x, y, and z variables are working variables, and will be populated by our position array. The next four variables will hold the converted values from the character coordinate array for the current character. The textStringLength variable is a working variable used to control the length of our loop, and the totalLength will hold the total length of our string in OpenGL space at the end of the loop.

   if(vertexArray != NULL) {
        free(vertexArray);
    }
   
    vertexArray = malloc(textStringLength * sizeof(GLfloat) * 3 * 3 * 2);

If the vertex array is populated already, we must free it before we use it again, otherwise we’ll create a memory leak. Once it’s freed, or confirmed NULL, we’ll allocate enough space to hold all of the vertex array information for rendering our text string. We’ll need enough room for two triangles, each with three vertices with three coordinates (X, Y, and Z) for each letter.

    x = position[0]; // Text string's X position
    y = position[1]; // Text string's Y position
    z = position[2]; // Text string's Z position
   
    float conversionXFactor = 2.0 / (float)myScreenWidth;
    float conversionYFactor = 2.0 / (float)myScreenHeight;

Here we load our x, y, and z working variables, and create our X and Y conversion values for the current screen width and height.

    char *characterIndex = [myFontImporter getCharacterIndex];
    int *characterCoords = [myFontImporter getCoordinateArray];

This is where the font importer comes into play. The font importer has the index and character coordinate array for whatever font sheet we used to create this text string’s manager, so this text string will need to use that data to calculate the vertex information.

    for(int i = 0; i < textStringLength; i++) {
        char currentChar = [myTextString characterAtIndex:i];
        BOOL charFound = FALSE; // Did we find the character?
[cc]

The outer loop goes through each character in our plain text string. If our text string is “Game Over”, the first letter will be ‘G’, the next will be ‘a’, etc. The charFound switch will let us know if we need to use the filler logic to handle an unknown character.

With the current character set to whatever character we’re currently processing, we go into the inner loop.

[cc lang="objc"]
        for(int j = 0; j < NUM_CHARACTERS; j++) {
            if(characterIndex[j] == currentChar) {
                int arrayIndex = j * 4; // There are four coordinates per character

The inner loop looks for the current character in the character index that we got from the font importer. If a match is found, we calculate the position of the relevant data in the character coordinate array by multiplying our character index by 4.

                ulX = characterCoords[arrayIndex];
                ulY = characterCoords[arrayIndex + 1];
                lrX = characterCoords[arrayIndex + 2];
                lrY = characterCoords[arrayIndex + 3];
               
                int letterWidth = (lrX - ulX);
                int letterHeight = (lrY - ulY);

Once we have our index into the character coordinate array, we grab the upper left X and Y, and the lower right X and Y coordinates. We use that information to calculate this letter’s width and height, in pixels.

               float characterAspectRatio = (float)letterWidth / (float)letterHeight;
               
                // Now apply that aspect ratio to our height and that will be our width.
               
                int adjLetterWidth = size * characterAspectRatio;
                int adjLetterHeight = size;
               
                totalLength += (adjLetterWidth * conversionXFactor);

Remember that earlier we talked about the size of characters in pixels. If we allow the user to set a size, which we do, we must have some way of keeping the width consistent with the height so that the letters don’t look stretched or squashed. The way we can do that is to calculate an aspect ratio and apply it to the width based on the height. If a letter was read off of the font sheet as 20 x 40, we know that it’s height is twice it’s length. If I want to size the letter at 10 pixels high, I need to resize the width as well to keep it in ratio.

If I divide the width by the height, I get 0.5 (20 divided by 40). Now, when I resize my height to 10, I resize my width to the new size multiplied by the aspect ration, or 10 times 0.5, which equals 5. My new letter size is now 5 x 10, which is the same aspect ratio as 20 x 40.

I also start accumulating the total length of my string, in OpenGL space, by incrementing by the adjusted letter width multiplied by my X axis conversion factor.

               int vertexIndex = i * 18;
               
                // The first vertex is our current X, Y, & Z position

                vertexArray[vertexIndex    ] = x;
                vertexArray[vertexIndex + 1] = y;
                vertexArray[vertexIndex + 2] = z;
               
                // We then go right for our letter width
               
                x += adjLetterWidth * conversionXFactor;
               
                vertexArray[vertexIndex + 3] = x;
                vertexArray[vertexIndex + 4] = y;
                vertexArray[vertexIndex + 5] = z;
               
                // Next, go up and left for the final vertex of the first triangle
               
                x -= adjLetterWidth * conversionXFactor;
                y += adjLetterHeight * conversionYFactor;
               
                vertexArray[vertexIndex + 6] = x;
                vertexArray[vertexIndex + 7] = y;
                vertexArray[vertexIndex + 8] = z;
               
                // Now for our second triangle - go to the top right corner
               
                x += adjLetterWidth * conversionXFactor;
               
                vertexArray[vertexIndex + 9 ] = x;
                vertexArray[vertexIndex + 10] = y;
                vertexArray[vertexIndex + 11] = z;
               
                // Back to the top left corner
               
                x -= adjLetterWidth * conversionXFactor;
               
                vertexArray[vertexIndex + 12] = x;
                vertexArray[vertexIndex + 13] = y;
                vertexArray[vertexIndex + 14] = z;
               
                // And finally ending our second triangle in the lower right corner
               
                x += adjLetterWidth * conversionXFactor;
                y -= adjLetterHeight * conversionYFactor;
               
                vertexArray[vertexIndex + 15] = x;
                vertexArray[vertexIndex + 16] = y;
                vertexArray[vertexIndex + 17] = z;
               
                charFound = TRUE;
                break;
            }
        }

Once we’ve located our character and calculated all of the necessary dimensions and conversion factors, it’s time to add to our vertex array. Since we’re storing 18 floats per character (2 triangles * 3 vertices * 3 coordinates per vertex), we multiply our current character by 18 to get into the right position in the vertex array. From here, all we need to do is use our conversion factors and character coordinates (in pixels) to populate the array with enough vertices to draw two triangles that will make a square of the correct width and height, in OpenGL space, to properly display the texture fragment for this character.

The comments in the code should help you follow the construction of the triangles. They are wound counter-clockwise so that the textures will be applied properly.

After loading up the proper portion of the array, we set the charFound switch to TRUE and break out of our loop.

The next loop is only executed if we did not find our character. It sets the current character to a dash, as mentioned above, and uses it in place of the unfound character.

       if(charFound == FALSE) {
            currentChar = '-';  // The dash is a nice spacing character, width-wise
           
            // If the dash character is not found, processing is undefined. We may crash or
            // render improperly, so make sure that your character sheets are formatted to spec.

            for(int j = 0; j < NUM_CHARACTERS; j++) {
                if(characterIndex[j] == currentChar) {
                    // The character was located, so we need to build the vertex
                    // information based on the appropriate coordinate information.
                   
                    int arrayIndex = j * 4; // There are four coordinates per character
                   
                    ulX = characterCoords[arrayIndex];
                    ulY = characterCoords[arrayIndex + 1];
                    lrX = characterCoords[arrayIndex + 2];
                    lrY = characterCoords[arrayIndex + 3];
                   
                    int letterWidth = (lrX - ulX);
                    int letterHeight = (lrY - ulY);
                   
                    // We know how high the character should be (from the 'size' variable),
                    // so we can convert that and be fine. For the width, however, we need to
                    // calculate an aspect ratio before we can accurately calculate.
                   
                    float characterAspectRatio = (float)letterWidth / (float)letterHeight;
                   
                    // Now apply that aspect ratio to our height and that will be our width.
                   
                    int adjLetterWidth = size * characterAspectRatio;
                    int adjLetterHeight = size;
                   
                    totalLength += (adjLetterWidth * conversionXFactor);
                   
                    int vertexIndex = i * 18;
                   
                    // The first vertex is our current X, Y, & Z position
                   
                    vertexArray[vertexIndex    ] = x;
                    vertexArray[vertexIndex + 1] = y;
                    vertexArray[vertexIndex + 2] = z;
                   
                    // We then go right for our letter width
                   
                    x += adjLetterWidth * conversionXFactor;
                   
                    vertexArray[vertexIndex + 3] = x;
                    vertexArray[vertexIndex + 4] = y;
                    vertexArray[vertexIndex + 5] = z;
                   
                    // Next, go up and left for the final vertex of the first triangle
                   
                    x -= adjLetterWidth * conversionXFactor;
                    y += adjLetterHeight * conversionYFactor;
                   
                    vertexArray[vertexIndex + 6] = x;
                    vertexArray[vertexIndex + 7] = y;
                    vertexArray[vertexIndex + 8] = z;
                   
                    // Now for our second triangle - go to the top right corner
                   
                    x += adjLetterWidth * conversionXFactor;
                   
                    vertexArray[vertexIndex + 9 ] = x;
                    vertexArray[vertexIndex + 10] = y;
                    vertexArray[vertexIndex + 11] = z;
                   
                    // Back to the top left corner
                   
                    x -= adjLetterWidth * conversionXFactor;
                   
                    vertexArray[vertexIndex + 12] = x;
                    vertexArray[vertexIndex + 13] = y;
                    vertexArray[vertexIndex + 14] = z;
                   
                    // And finally ending our second triangle in the lower right corner
                   
                    x += adjLetterWidth * conversionXFactor;
                    y -= adjLetterHeight * conversionYFactor;
                   
                    vertexArray[vertexIndex + 15] = x;
                    vertexArray[vertexIndex + 16] = y;
                    vertexArray[vertexIndex + 17] = z;

                    break;
                }
            }
        }

The processing of this loop is identical to the one above it, but for a specific character.

    if(centered == TRUE) {
        for(int i = 0; i < (textStringLength * 18); i += 3) {
            vertexArray[i] -= (totalLength / 2.0);
        }
    }
}

The final piece of code in this method checks to see if the string should be centered over the X coordinate. If so, it quickly runs through the array we just built and adjusts the X coordinates of each of the vertices by half of the total length.

The next method does something very similar to the populateVertexArray method, but does it for texture coordinates instead of vertices. Since OpenGL space has two units on each axis, -1 through 1, our conversion factor was 2 divided by the iPhone screen space axes. Texture coordinates exist in only a single unit on each axis, 0 through 1. In order to properly calculate the texture coordinates for each individual character on our font sheet, we must divide that single unit by the number of pixels wide and high of that font sheet.

In the case where the character is not found, we allow all texture coordinates to be set to zero, which will prevent any texture from being rendered. This is how we cheat and get our space characters rendered properly, even though there is no space character in the index array.

Let’s look at the method in full.

// The populateTextureCoordArray method will create an array of texture
// coordinates that can be used by OpenGL to render the individual characters
// from the character sheet used by the font importer.

- (void)populateTextureCoordArray {
    // Walk through the character index, just like when building the vertex array
   
    float s, t;         // S & T texture coordinate values
    float ulX, ulY;     // Upper left X & Y
    float lrX, lrY;     // Lower right X & Y
   
    int textStringLength = [myTextString length];
   
    // If the texture coordinate array is populate, free it
   
    if(textureCoordArray != NULL) {
        free(textureCoordArray);
    }
   
    // Allocate pace to hold the array. There are 2 coordinates per vertex, 3 vertices per
    // triangle, and 2 triangles per letter.
   
    textureCoordArray = malloc(textStringLength * sizeof(GLfloat) * 2 * 3 * 2);
   
    s = t = 0;  // Clear S & T coordinate variables
   
    // We need to create a texel conversion to make the width and height
    // of the texture 1 OpenGL unit.
   
    float conversionXFactor = 1.0 / (float)myFontImporter.characterPageWidth;
    float conversionYFactor = 1.0 / (float)myFontImporter.characterPageHeight;
   
    char *characterIndex = [myFontImporter getCharacterIndex];
    int *characterCoords = [myFontImporter getCoordinateArray];
   
    for(int i = 0; i < textStringLength; i++) {
        char currentChar = [myTextString characterAtIndex:i];
       
        float textureCoords[8];
        memset(textureCoords, 0, sizeof(float) * 8); // initialize texture coords to 0
       
        for(int j = 0; j < NUM_CHARACTERS; j++) {
            if(characterIndex[j] == currentChar) {
                // The character was found, so create texture coordinates
                // based on the relative positions of the character images within
                // the largest image (texture). Apply the conversion factor to
                // keep the texture coordinates within a range or 0.0 through 1.0
               
                int arrayIndex = j * 4;
               
                ulX = characterCoords[arrayIndex];
                ulY = characterCoords[arrayIndex + 1];
                lrX = characterCoords[arrayIndex + 2];
                lrY = characterCoords[arrayIndex + 3];
               
                // Now that we have the character location data, create a set of four
                // texture coordinates - we only need four because there are only four corners
                // of our square (made fron two triangles) that we care about. These four coordinate
                // pairs will be used for all six vertices of the triangles being drawn.
               
                // Note: the Y coordinate is flipped
               
                int pageHeight = myFontImporter.characterPageHeight;
               
                textureCoords[0] = ulX;
                textureCoords[1] = pageHeight - lrY;
               
                textureCoords[2] = ulX;
                textureCoords[3] = pageHeight - ulY;
               
                textureCoords[4] = lrX;
                textureCoords[5] = pageHeight - lrY;
               
                textureCoords[6] = lrX;
                textureCoords[7] = pageHeight - ulY;
               
                break;
            }
        }
       
        // At this point, we've either found a character and loaded the texture coordinate array with
        // values from the character sheet, or we have NOT found a character and have a texture
        // coordinate array full of zeros. Either way, we'll load them into the OpenGL texture coordinate
        // array, since unknown characters will not be rendered anyway.
       
        int textureCoordIndex = i * 12; // Two triangles, three vertices, two texture coordinates per vertex
       
        textureCoordArray[textureCoordIndex    ] = textureCoords[0] * conversionXFactor;
        textureCoordArray[textureCoordIndex + 1] = textureCoords[1] * conversionYFactor;
       
        textureCoordArray[textureCoordIndex + 2] = textureCoords[4] * conversionXFactor;
        textureCoordArray[textureCoordIndex + 3] = textureCoords[5] * conversionYFactor;
       
        textureCoordArray[textureCoordIndex + 4] = textureCoords[2] * conversionXFactor;
        textureCoordArray[textureCoordIndex + 5] = textureCoords[3] * conversionYFactor;
       
        textureCoordArray[textureCoordIndex + 6] = textureCoords[6] * conversionXFactor;
        textureCoordArray[textureCoordIndex + 7] = textureCoords[7] * conversionYFactor;
       
        textureCoordArray[textureCoordIndex + 8] = textureCoords[2] * conversionXFactor;
        textureCoordArray[textureCoordIndex + 9] = textureCoords[3] * conversionYFactor;
       
        textureCoordArray[textureCoordIndex + 10] = textureCoords[4] * conversionXFactor;
        textureCoordArray[textureCoordIndex + 11] = textureCoords[5] * conversionYFactor;
    }
}

Building our array of character texture coordinates is very similar to building our array of vertices, but there are a few minor differences. Instead of build X, Y, and Z coordinates, we are using S and T coordinates. Also, we initialize our temporary texture coordinate array to zeros, and if we don’t find the character we’re looking for, we multiply the conversion factors against those zeros, yielding zeros. What this will do is create an empty spot on our chain of triangles, which is fine for unknown characters, and exactly what we want for space characters.

Let’s go through the method bit by bit.

- (void)populateTextureCoordArray {
   
    float s, t;         // S & T texture coordinate values
    float ulX, ulY;     // Upper left X & Y
    float lrX, lrY;     // Lower right X & Y
   
    int textStringLength = [myTextString length];

The first thing we do is declare some working variables and get our string length.

   if(textureCoordArray != NULL) {
        free(textureCoordArray);
    }
   
    // Allocate pace to hold the array. There are 2 coordinates per vertex, 3 vertices per
    // triangle, and 2 triangles per letter.
   
    textureCoordArray = malloc(textStringLength * sizeof(GLfloat) * 2 * 3 * 2);

If the texture coordinate array is populated, we free it. I don’t think we can ever get here if the array is populated, but who knows how we might change this code in the future, so it won’t hurt anything to have this check here.

We also allocate memory for our new texture coordinate array.

   s = t = 0;  // Clear S & T coordinate variables
   
    // We need to create a texel conversion to make the width and height
    // of the texture 1 OpenGL unit.
   
    float conversionXFactor = 1.0 / (float)myFontImporter.characterPageWidth;
    float conversionYFactor = 1.0 / (float)myFontImporter.characterPageHeight;
   
    char *characterIndex = [myFontImporter getCharacterIndex];
    int *characterCoords = [myFontImporter getCoordinateArray];

We initialize S and T to zero, create our conversion factors based on the font sheet image size translated to our one unit of texture space, and get the character index and coordinates array from the associated font importer.

    for(int i = 0; i < textStringLength; i++) {
        char currentChar = [myTextString characterAtIndex:i];
       
        float textureCoords[8];
        memset(textureCoords, 0, sizeof(float) * 8); // initialize texture coords to 0

The outer loop will be walking through our character array, using a temporary eight member texture coordinate array initialized to zeros. The reason we do this is that if we don’t find the character we’re looking for, we’ll end up applying our conversion factors to zero for each coordinate, and automatically create an empty spot for this character. By doing this, we avoid having to go through the inner loop again if we don’t find the character.

We had to go through the second loop if we didn’t find our character in the vertex array builder because we had to calculate a size for the triangles. In this loop, we only need texture coordinates to apply to the triangles that have already been sized, so we can be a bit craftier here.

       for(int j = 0; j < NUM_CHARACTERS; j++) {
            if(characterIndex[j] == currentChar) {
                // The character was found, so create texture coordinates
                // based on the relative positions of the character images within
                // the largest image (texture). Apply the conversion factor to
                // keep the texture coordinates within a range or 0.0 through 1.0
               
                int arrayIndex = j * 4;
               
                ulX = characterCoords[arrayIndex];
                ulY = characterCoords[arrayIndex + 1];
                lrX = characterCoords[arrayIndex + 2];
                lrY = characterCoords[arrayIndex + 3];
               
                // Now that we have the character location data, create a set of four
                // texture coordinates - we only need four because there are only four corners
                // of our square (made fron two triangles) that we care about. These four coordinate
                // pairs will be used for all six vertices of the triangles being drawn.
               
                // Note: the Y coordinate is flipped
               
                int pageHeight = myFontImporter.characterPageHeight;
               
                textureCoords[0] = ulX;
                textureCoords[1] = pageHeight - lrY;
               
                textureCoords[2] = ulX;
                textureCoords[3] = pageHeight - ulY;
               
                textureCoords[4] = lrX;
                textureCoords[5] = pageHeight - lrY;
               
                textureCoords[6] = lrX;
                textureCoords[7] = pageHeight - ulY;
               
                break;
            }
        }

If the character we’re looking for was found, we record the character coordinates in our temporary array. Note that the image was read in bottom to top, but the OpenGL texture will expect the texture to be read from top to bottom, so we have to invert the Y coordinates by subtracting them from the height of the image file.

Once we’ve recorded all of the character’s pixel coordinates, we break out of the loop.

        // At this point, we've either found a character and loaded the texture coordinate array with
        // values from the character sheet, or we have NOT found a character and have a texture
        // coordinate array full of zeros. Either way, we'll load them into the OpenGL texture coordinate
        // array, since unknown characters will not be rendered anyway.
       
        int textureCoordIndex = i * 12; // Two triangles, three vertices, two texture coordinates per vertex
       
        textureCoordArray[textureCoordIndex    ] = textureCoords[0] * conversionXFactor;
        textureCoordArray[textureCoordIndex + 1] = textureCoords[1] * conversionYFactor;
       
        textureCoordArray[textureCoordIndex + 2] = textureCoords[4] * conversionXFactor;
        textureCoordArray[textureCoordIndex + 3] = textureCoords[5] * conversionYFactor;
       
        textureCoordArray[textureCoordIndex + 4] = textureCoords[2] * conversionXFactor;
        textureCoordArray[textureCoordIndex + 5] = textureCoords[3] * conversionYFactor;
       
        textureCoordArray[textureCoordIndex + 6] = textureCoords[6] * conversionXFactor;
        textureCoordArray[textureCoordIndex + 7] = textureCoords[7] * conversionYFactor;
       
        textureCoordArray[textureCoordIndex + 8] = textureCoords[2] * conversionXFactor;
        textureCoordArray[textureCoordIndex + 9] = textureCoords[3] * conversionYFactor;
       
        textureCoordArray[textureCoordIndex + 10] = textureCoords[4] * conversionXFactor;
        textureCoordArray[textureCoordIndex + 11] = textureCoords[5] * conversionYFactor;
    }
}

Like the comments say, we either have the character’s pixel coordinates, or we’ve got an array full of zeros. Either way, we copy our values into the main texture coordinates array, applying our conversion factors as we go. We find our current location in the texture coordinate array by multiplying our text string index by 12, which is the number of values we store for each character.

Once you see how these methods are used by the text string manager, they’ll make more sense.

// Manage any instance variables based on any necessary frame-by-frame adjustments

- (void)update {
    position[0] += drift[0];
    position[1] += drift[1];
    position[2] += drift[2];
   
    if(lifespan > 0) {
        lifeleft--;
       
        if(lifeleft <= 0) {
            alive = FALSE;
        } else {
            if(lifeleft <= decayAt) {
                // It's time to start decaying
                alpha = lifeleft / decayAt;
            }
        }
    }
}

@end

The last method in our class is designed to be called after each render, and will update any positional or alpha values that need to be updated based on drift or lifespan.

That was probably the longest tutorial chapter I have ever written, but it was also a very complicated class. The EDTextString class is very versatile, and once we create the EDTextStringManager class in the next chapter, we can code a really neat demo app that will show off how cool all of this stuff can look.

Chapter 6 | Index | Chapter 8