Chapter 9: Finally, OpenGL Says “Hello, World!”, and More

In chapter 8, we finished off our new text string management code and created some custom shaders for rendering. We are now ready to make some changes to the view controller and witness the awesome fruits of our labor.

First, two small changes to the TouchTargetsViewController.h file.

#import <UIKit/UIKit.h>

#import <OpenGLES/EAGL.h>

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

#import "EDOpenGLProgram.h"
#import "EDTextStringManager.h" // ED: Added

@interface TouchTargetsViewController : UIViewController {
@private
    EAGLContext *context;
   
    BOOL animating;
    NSInteger animationFrameInterval;
    CADisplayLink *displayLink;
   
    EDOpenGLProgram *myOpenGLProgram;
    EDTextStringManager *myTextStringManager; // ED: Added
}

@property (readonly, nonatomic, getter=isAnimating) BOOL animating;
@property (nonatomic) NSInteger animationFrameInterval;

- (void)startAnimation;
- (void)stopAnimation;

@end

We add an #import statement for our new EDTextStringManager header file, and an instance variable for an EDTextStringManager object.

Next, a few additions to the awakeFromNib method in our TouchTargetsViewController class source.

- (void)awakeFromNib
{
    EAGLContext *aContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
   
    if (!aContext) {
        aContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1];
    }
   
    if (!aContext)
        NSLog(@"Failed to create ES context");
    else if (![EAGLContext setCurrentContext:aContext])
        NSLog(@"Failed to set ES context current");
   
    self.context = aContext;
    [aContext release];
   
    [(EAGLView *)self.view setContext:context];
    [(EAGLView *)self.view setFramebuffer];
   
    if([context API] == kEAGLRenderingAPIOpenGLES2) {
        myOpenGLProgram = [[EDOpenGLProgram alloc] init];
       
        [myOpenGLProgram setVertexShader:@"Shader"];
        [myOpenGLProgram setFragmentShader:@"Shader"];
       
        [myOpenGLProgram addAttributeLocation:@"ATTRIB_VERTEX" forAttribute:@"position"];
        [myOpenGLProgram addAttributeLocation:@"ATTRIB_COLOR" forAttribute:@"color"];
       
        [myOpenGLProgram addUniformLocation:@"UNIFORM_TRANSLATE" forUniform:@"translate"];
       
        [myOpenGLProgram compileAndLink];
       
        attributes[ATTRIB_VERTEX] = [myOpenGLProgram getAttributeIDForIndex:@"ATTRIB_VERTEX"];
        attributes[ATTRIB_COLOR] = [myOpenGLProgram getAttributeIDForIndex:@"ATTRIB_COLOR"];
       
        uniforms[UNIFORM_TRANSLATE] = [myOpenGLProgram getUniformIDForIndex:@"UNIFORM_TRANSLATE"];
       
        myTextStringManager = [[EDTextStringManager alloc] initWithCharacterSheetName:@"FontCooperStd.png"]; // ED: Added
       
        EDTextString *myTextString = [[EDTextString alloc] initWithString:@"Hello, World!"]; // ED: Added
        [myTextString setPositionX:160 andY:240 andZ:0]; // ED: Added
        [myTextString setColorRed:0 andGreen:0 andBlue:128]; // ED: Added
        [myTextString setSize:20]; // ED: Added
       
        [myTextStringManager addTextString:myTextString]; // ED: Added

    }
   
    animating = FALSE;
    animationFrameInterval = 1;
    self.displayLink = nil;
}

Not too many changes here, so let’s look at only our new lines of code.

        myTextStringManager = [[EDTextStringManager alloc] initWithCharacterSheetName:@"FontCooperStd.png"]; // ED: Added
       
        EDTextString *myTextString = [[EDTextString alloc] initWithString:@"Hello, World!"]; // ED: Added
        [myTextString setPositionX:160 andY:240 andZ:0]; // ED: Added
        [myTextString setColorRed:0 andGreen:0 andBlue:128]; // ED: Added
        [myTextString setSize:20]; // ED: Added
       
        [myTextStringManager addTextString:myTextString]; // ED: Added

We’re instantiating an instance of the EDTextStringManager class with a character font sheet name of “FontCooperStd.png”, so be sure that you grab that from the downloadable Xcode project or create your own based on the specs we discussed earlier. You can also download it here.

After that, we instantiate an instance of EDTextString with an initial text string of “Hello, World!” and set some configuration options.

Once we’re finished setting EDTextString options, we add the text string to our text string manager.

The next changes are in the drawFrame method.

- (void)drawFrame
{
    [(EAGLView *)self.view setFramebuffer];
   
    // Replace the implementation of this method to do your own custom drawing.
    static const GLfloat squareVertices[] = {
        -0.5f, -0.33f,
        0.5f, -0.33f,
        -0.5f,  0.33f,
        0.5f,  0.33f,
    };
   
    static const GLubyte squareColors[] = {
        255, 255,   0, 255,
        0,   255, 255, 255,
        0,     0,   0,   0,
        255,   0, 255, 255,
    };
   
    static float transY = 0.0f;
   
    //glClearColor(0.5f, 0.5f, 0.5f, 1.0f); // ED: Removed
    glClearColor(0.90, 0.90, 0.90, 1.0); // ED: Added
    glClear(GL_COLOR_BUFFER_BIT);
   
    if ([context API] == kEAGLRenderingAPIOpenGLES2) {
        // Use shader program.
        glUseProgram([myOpenGLProgram programId]);
       
        // Update uniform value.
        glUniform1f(uniforms[UNIFORM_TRANSLATE], (GLfloat)transY);
        transY += 0.075f;
       
        // Update attribute values.
        glVertexAttribPointer(attributes[ATTRIB_VERTEX], 2, GL_FLOAT, 0, 0, squareVertices);
        glEnableVertexAttribArray(attributes[ATTRIB_VERTEX]);
        glVertexAttribPointer(attributes[ATTRIB_COLOR], 4, GL_UNSIGNED_BYTE, 1, 0, squareColors);
        glEnableVertexAttribArray(attributes[ATTRIB_COLOR]);
       
        // Validate program before drawing. This is a good check, but only really necessary in a debug build.
        // DEBUG macro must be defined in your debug configurations if that's not already the case.
#if defined(DEBUG)
        if (![myOpenGLProgram validate]) {
            NSLog(@"Failed to validate program: %d", [myOpenGLProgram programId]);
            return;
        }
#endif
    } else {
        glMatrixMode(GL_PROJECTION);
        glLoadIdentity();
        glMatrixMode(GL_MODELVIEW);
        glLoadIdentity();
        glTranslatef(0.0f, (GLfloat)(sinf(transY)/2.0f), 0.0f);
        transY += 0.075f;
       
        glVertexPointer(2, GL_FLOAT, 0, squareVertices);
        glEnableClientState(GL_VERTEX_ARRAY);
        glColorPointer(4, GL_UNSIGNED_BYTE, 0, squareColors);
        glEnableClientState(GL_COLOR_ARRAY);
    }
   
    //glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); // ED: Removed
   
    if([context API] == kEAGLRenderingAPIOpenGLES2) { // ED: Added
        [myTextStringManager drawAllTextStrings]; // ED: Added
    } // ED: Added
   
    [(EAGLView *)self.view presentFramebuffer];
}

I left the bulk of this method unchanged to more easily see the few changes necessary to implement our new text system. The first change was purely cosmetic, but will make the font look nicer. I brightened up the background color.

    //glClearColor(0.5f, 0.5f, 0.5f, 1.0f); // ED: Removed
    glClearColor(0.98, 0.98, 0.98, 1.0); // ED: Added

If the background is too dark, you’ll see artifacts around the letters because the edges were anti-aliased against white. Even though we changed the color to blue, you’d still see unpleasant spots of very light blue against a dark background unless you went back to the shader and lowered the threshold.

    //glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); // ED: Removed
   
    if([context API] == kEAGLRenderingAPIOpenGLES2) { // ED: Added
        [myTextStringManager drawAllTextStrings]; // ED: Added
    } // ED: Added

Finally, I commented out the glDrawArrays() function call so the rainbow box would not be drawn, and added a check to see if we were an OpenGL ES 2 application. If so, we cal the drawAllTextStrings method of our text manager.

If we run this code, we get this:

 

 

Wow! Talk about anti-climactic!

There’s no way we went through all of that work for this, we’ve got to make it do something. Let’s add some logic that detects touches and pops up random words of random colors that drift and fade.

At the bottom of the TouchTargetsViewController.m file, add the following method:

// ED: Added method to detect touches and pop up drifting text

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    // If user taps, generate a new text string with a few
    // attributes randomized.
   
    const NSArray *colorNameArray = [[NSArray alloc] initWithObjects:@"Red", @"Green", @"Blue", @"Yellow", @"Purple", @"Cyan", nil];
    static const int colorValueArray[] = { 128,0,0, 0,128,0, 0,0,128, 128,128,0, 128,0,128, 0,128,128 };
   
    UITouch *t = [[touches allObjects] objectAtIndex:0];
    CGPoint p = [t locationInView:self.view];
   
    int randomInt = (arc4random() % (5 - 0 + 1)) + 0;
    int randomDrift = (arc4random() % (3 - 1 + 1)) + 1;
    int randomSize = (arc4random() % (40 - 20 + 1)) + 20;
   
    EDTextString *textString = [[EDTextString alloc] initWithString:[colorNameArray objectAtIndex:randomInt]];
   
    [textString setPositionX:p.x andY:p.y andZ:0];
    [textString setColorRed:colorValueArray[randomInt * 3] andGreen:colorValueArray[randomInt * 3 + 1] andBlue:colorValueArray[randomInt * 3 + 2]];
    [textString setDriftX:0 andY:-randomDrift andZ:0];
    [textString setLifespan:100 withDecayAt:50];
    [textString setSize:randomSize];
   
    [myTextStringManager addTextString:textString];
}

What have we done? The first two lines create a couple of arrays of words and matching colors for those words.

    UITouch *t = [[touches allObjects] objectAtIndex:0];
    CGPoint p = [t locationInView:self.view];

The first of the next two lines gets the UITouch object associated with the touch(es) that fired this method. There may have been more then one touch (think of a pinch gesture), but we’re only going to process the first one, the object at position 0.

The second line gets the screen coordinates of the touch we got from the first line. The screen coordinates are in iPhone screen dimensions, not in OpenGL coordinates.

    int randomInt = (arc4random() % (5 - 0 + 1)) + 0;
    int randomDrift = (arc4random() % (3 - 1 + 1)) + 1;
    int randomSize = (arc4random() % (40 - 20 + 1)) + 20;

We’re going to create a text string made up of one of the words in our array of a color names and one of the matching color groups in the array of color values. We’re also going to have a random drift upwards of 1 to 3 pixels per frame, and have a random size between 20 and 40.

What’s all that junk we’re using to generate random numbers, you ask?

The arc4random() function returns a large, random number like 1922685010, and the percent sign is a modulo operator. A modulo operator returns the remainder of a division. All the numbers after the modulo operator are just arranged in a pattern for my own selfish purposes.

Let me explain.

Let’s say I wanted a number between 1 and 10. If a take a random number, say 8376, and divide it by 10, I get 837.6, so the modulo operator will return the remainder, or 6. When I divide by 10, I can have 10 possible remainders, 0 through 9, so I need to add one to the result to make sure that I end up with a 1 through a 10.

The simple version of this would be:

int randomNbr = (arc4random() % 10) + 1;

The parenthetical grouping is mainly for our benefit, since multiplication and division will occur before addition and subtraction.

What it I wanted a random number between 1 and 30?

int randomNbr = (arc4random() % 30) + 1;

Since the remainder from dividing something by thirty can be anywhere from 0 to 29, adding 1 to that will guarantee a range to 1 to 30.

But what if I want a random number between 25 and 50? First, I need to determine what the actual range of numbers will be. In this case, the range of numbers between 25 and 50 (inclusive) is 26, but we’ll consider it 25, since the number we divide by will never be a remainder.

But if I divide by 25, I’ll only get a range of 0 through 24, which will end up being 1 through 25 once I adjust it by one. How can I determine how to adjust the results to get the range I want?

If I subtract the lowest number in my range from the highest, I get the number of numbers in my range. In our 25 to 50 example, that would be 50 – 25, or 25. We know that part, but if we then add the lowest number in the range back to our result after using arc4random(), we can bump the result set into the range we really wanted, like this:

int randomNbr = (arc4random() % 25) + 1 + 25;

Now our result range of numbers will be 25 through 50. This is neat, but it’s wrong. Now I can’t randomly generate a number range that includes zero. Look at this logic:

int randomNbr = (arc4random() % 10) + 1;

If I want a random number between 0 and 10, I’m out of luck. We need to make the adjustment to the high end of our range without removing out ability to produce a zero. Let’s change the logic to this:

int randomNbr = arc4random() % (10 + 1);

Now I can get a random number from 0 through 10, because I’m actually getting the remainder of dividing by 11. But will this work for ranges like 25 through 50? Let’s see:

int randomNbr = arc4random() % (25 + 1) + 25;

That would be the remainder of a division operation by 26, which would be 0 through 25, plus 25, which would make the range 25 through 50. Perfect.

That’s pretty cool, but If I included the math I used to get the number of numbers, I could have a visual reference of the low and high values of my range, like this:

int randomNbr = arc4random() % (50 - 25 + 1) + 25;

Now I have the same operation, but I can visually see my ranges. Look at the class source code again.

    int randomInt = (arc4random() % (5 - 0 + 1)) + 0;
    int randomDrift = (arc4random() % (3 - 1 + 1)) + 1;
    int randomSize = (arc4random() % (40 - 20 + 1)) + 20;

The randomInt variable will be 0 through 5, the randomDrift variable will be 1 through 3, and the randomSize will be 20 through 40. It’s perfectly clear, now.

Here’s the formula for this code:

random_number - arc4random() % (max_nbr - min_nbr + 1) + min_nbr

Right, let’s finish this up.

    EDTextString *textString = [[EDTextString alloc] initWithString:[colorNameArray objectAtIndex:randomInt]];
   
    [textString setPositionX:p.x andY:p.y andZ:0];
    [textString setColorRed:colorValueArray[randomInt * 3] andGreen:colorValueArray[randomInt * 3 + 1] andBlue:colorValueArray[randomInt * 3 + 2]];
    [textString setDriftX:0 andY:-randomDrift andZ:0];
    [textString setLifespan:100 withDecayAt:50];
    [textString setSize:randomSize];
   
    [myTextStringManager addTextString:textString];

The last block of code creates a new EDTextString object with a color name from the color name array.

The text position is set to wherever the user touched the screen, and color values are pulled from the color values array using the same index as the color name.

A Y axis drift is set, and made negative since negative is up on the iPhone screen. All of these values will be converted to OpenGL values by the EDTextString class; by keeping them in iPhone screen coordinates here, they’re more readable.

The lifespan is set to 100 refreshes, with a decay starting at 50 refreshes, and the size is set to our random number between 20 and 40.

Finally, the text string is added to the text manager and we leave the method.

Don’t forget that the text manager is being instructed to draw all text strings every time the drawFrame method fires, so this is all we need to do here.

Let’s compile and run the application to see how we did.

 

 

Much better, now we have random text strings of various colors and random sizes floating up from wherever we touch at random speeds and gently fading away.

We have also finished the most complicated code in the TouchTargets project – it’s all downhill from here (and I mean that in a good way).

Chapter 8 | Index | Chapter 10