Chapter 3: Converting to the New EDOpenGLProgram Class

In the last chapter, we finished up our new EDOpenGLProgram class, and now we’re ready to convert our view controller to use the new code.

Our first changes go into 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" // ED: Added

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

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

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

@end

We need to add an #import statement for our new EDOpenGLProgram class. We also add a new instance variable of that type.

Next, we go to the TouchTargetsViewController.m file. We’re going to make one slight change to the enum definitions at the top of the code.

// Uniform index.
enum {
    UNIFORM_TRANSLATE,
    NUM_UNIFORMS
};
GLint uniforms[NUM_UNIFORMS];

// Attribute index.
enum {
    ATTRIB_VERTEX,
    ATTRIB_COLOR,
    NUM_ATTRIBUTES
};
GLint attributes[NUM_ATTRIBUTES]; // ED: Added

I’ll explain the reason for this in a little while, for now, just go with it.

Next, we can remove some method definitions.

@interface TouchTargetsViewController ()
@property (nonatomic, retain) EAGLContext *context;
@property (nonatomic, assign) CADisplayLink *displayLink;
//- (BOOL)loadShaders; // ED: Removed
//- (BOOL)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file; // ED: Removed
//- (BOOL)linkProgram:(GLuint)prog; // ED: Removed
//- (BOOL)validateProgram:(GLuint)prog; // ED: Removed
@end

Since we’re moving all of the shader initialization and handling to our new class, we no longer need any of these methods.

The tradeoff to removing all of these methods is a bit more initialization code in the awakeFromNib method.

- (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) // ED: Removed
    //    [self loadShaders]; // ED: Removed
   
    if([context API] == kEAGLRenderingAPIOpenGLES2) { // ED: Added
        myOpenGLProgram = [[EDOpenGLProgram alloc] init]; // ED: Added
       
        [myOpenGLProgram setVertexShader:@"Shader"]; // ED: Added
        [myOpenGLProgram setFragmentShader:@"Shader"]; // ED: Added
       
        [myOpenGLProgram addAttributeLocation:@"ATTRIB_VERTEX" forAttribute:@"position"]; // ED: Added
        [myOpenGLProgram addAttributeLocation:@"ATTRIB_COLOR" forAttribute:@"color"]; // ED: Added
       
        [myOpenGLProgram addUniformLocation:@"UNIFORM_TRANSLATE" forUniform:@"translate"]; // ED: Added
       
        [myOpenGLProgram compileAndLink]; // ED: Added
       
        attributes[ATTRIB_VERTEX] = [myOpenGLProgram getAttributeIDForIndex:@"ATTRIB_VERTEX"]; // ED: Added
        attributes[ATTRIB_COLOR] = [myOpenGLProgram getAttributeIDForIndex:@"ATTRIB_COLOR"]; // ED: Added
       
        uniforms[UNIFORM_TRANSLATE] = [myOpenGLProgram getUniformIDForIndex:@"UNIFORM_TRANSLATE"]; // ED: Added
    } // ED: Added
   
    animating = FALSE;
    animationFrameInterval = 1;
    self.displayLink = nil;
}

The initialization logic for our new EDOpenGLProgram class makes up a nice big chunk of this method now. Let’s look at it line by line.

    //if ([context API] == kEAGLRenderingAPIOpenGLES2) // ED: Removed
    //    [self loadShaders]; // ED: Removed

Since we’ve removed the the loadShaders method from this file, we’ll get rid of this code.

In it’s place, if we’re an OpenGL ES 2.0 application, we’ll create and initialize our new EDOpenGLProgram object.

    if([context API] == kEAGLRenderingAPIOpenGLES2) { // ED: Added
        myOpenGLProgram = [[EDOpenGLProgram alloc] init]; // ED: Added
       
        [myOpenGLProgram setVertexShader:@"Shader"]; // ED: Added
        [myOpenGLProgram setFragmentShader:@"Shader"]; // ED: Added

After allocating and initializing our EDOpenGLProgram object, we set the names of our vertex and fragment shaders. The name does not include the file extension, which is implied by the method we use to set it. For example, using the setVertexShader method will result in “.vsh” being appended to our file during processing.

        [myOpenGLProgram addAttributeLocation:@"ATTRIB_VERTEX" forAttribute:@"position"]; // ED: Added
        [myOpenGLProgram addAttributeLocation:@"ATTRIB_COLOR" forAttribute:@"color"]; // ED: Added

The next two lines add a couple of attributes to our EDOpenGLProgram object.

        [myOpenGLProgram addUniformLocation:@"UNIFORM_TRANSLATE" forUniform:@"translate"]; // ED: Added

And then we add the uniform we need to pass in the translate value.

        [myOpenGLProgram compileAndLink]; // ED: Added

After all attributes and uniforms have been added, we send the compileAndLink message which will take all of the information we’ve given the EDOpenGLProgram object so far and use it to make our OpenGL program active and ready to use.

Now for the explanation of the new attributes[] variable, and why we’re keeping the enums at the top of the program even though we’re getting rid of all of the other OpenGL processing.

In the original OpenGL ES template view controller, in the loadShaders method, we used the attribute enums like this:

    // Bind attribute locations.
    // This needs to be done prior to linking.
    glBindAttribLocation(program, ATTRIB_VERTEX, "position");
    glBindAttribLocation(program, ATTRIB_COLOR, "color");

And the uniform enums like this:

    // Get uniform locations.
    uniforms[UNIFORM_TRANSLATE] = glGetUniformLocation(program, "translate");

Then, in the drawFrame method, when we actually drew our stuff, we used the enums like this:

        // Update uniform value.
        glUniform1f(uniforms[UNIFORM_TRANSLATE], (GLfloat)transY);
        transY += 0.075f;  
       
        // Update attribute values.
        glVertexAttribPointer(ATTRIB_VERTEX, 2, GL_FLOAT, 0, 0, squareVertices);
        glEnableVertexAttribArray(ATTRIB_VERTEX);
        glVertexAttribPointer(ATTRIB_COLOR, 4, GL_UNSIGNED_BYTE, 1, 0, squareColors);
        glEnableVertexAttribArray(ATTRIB_COLOR);

If we were to rely solely on our new class to replace this code, the drawFrame logic would need to look like this:

        // Update uniform value.
        glUniform1f([myOpenGLProgram getUniformIDForIndex:@"UNIFORM_TRANSLATE"], (GLfloat)transY);
        transY += 0.075f;  
       
        // Update attribute values.
        glVertexAttribPointer([myOpenGLProgram getAttributeIDForIndex:@"ATTRIB_VERTEX"], 2, GL_FLOAT, 0, 0, squareVertices);
        glEnableVertexAttribArray([myOpenGLProgram getAttributeIDForIndex:@"ATTRIB_VERTEX"]);
        glVertexAttribPointer([myOpenGLProgram getAttributeIDForIndex:@"ATTRIB_COLOR"], 4, GL_UNSIGNED_BYTE, 1, 0, squareColors);
        glEnableVertexAttribArray([myOpenGLProgram getAttributeIDForIndex:@"ATTRIB_COLOR"]);

When we needed uniform or attribute information, we’d need to make calls back to the EDOpenGLProgram object to get them. This method is our main rendering loop, and is supposed to be high-performance code. Every time I send my EDOpenGLProgram object the getUniformIDForIndex: or getAttributeIDForIndex: message, it’s running through internal arrays, finding matching objects, and passing back information from those objects.

While this code will work, it’s doing way more processing than is necessary. Since the attribute identifiers and uniform locations will never change after the OpenGL program has been compiled, we’re going to save that information into simple arrays using those enums. We’ll then use those arrays to avoid a whole bunch of unneeded processing in the drawFrame method.

These lines in the awakeFromNib method:

        attributes[ATTRIB_VERTEX] = [myOpenGLProgram getAttributeIDForIndex:@"ATTRIB_VERTEX"]; // ED: Added
        attributes[ATTRIB_COLOR] = [myOpenGLProgram getAttributeIDForIndex:@"ATTRIB_COLOR"]; // ED: Added
       
        uniforms[UNIFORM_TRANSLATE] = [myOpenGLProgram getUniformIDForIndex:@"UNIFORM_TRANSLATE"]; // ED: Added

Will allow us to streamline our drawFrame method logic to this:

        // 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]);

By taking the extra step in the initialization logic of saving off the attribute identifiers and uniform locations, we save a great deal of extra processing during the drawFrame loop.

Now that the EDOpenGLProgram object is initialized and ready to go, let’s look at the changes to 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);
    glClear(GL_COLOR_BUFFER_BIT);
   
    if ([context API] == kEAGLRenderingAPIOpenGLES2) {
        // Use shader program.
        //glUseProgram(program); // ED: Removed
        glUseProgram([myOpenGLProgram programId]); // ED: Added
       
        // Update uniform value.
        glUniform1f(uniforms[UNIFORM_TRANSLATE], (GLfloat)transY);
        transY += 0.075f;
       
        // Update attribute values.
        //glVertexAttribPointer(ATTRIB_VERTEX, 2, GL_FLOAT, 0, 0, squareVertices); // ED: Removed
        //glEnableVertexAttribArray(ATTRIB_VERTEX); // ED: Removed
        //glVertexAttribPointer(ATTRIB_COLOR, 4, GL_UNSIGNED_BYTE, 1, 0, squareColors); // ED: Removed
        //glEnableVertexAttribArray(ATTRIB_COLOR); // ED: Removed
       
        glVertexAttribPointer(attributes[ATTRIB_VERTEX], 2, GL_FLOAT, 0, 0, squareVertices); // ED: Added
        glEnableVertexAttribArray(attributes[ATTRIB_VERTEX]); // ED: Added
        glVertexAttribPointer(attributes[ATTRIB_COLOR], 4, GL_UNSIGNED_BYTE, 1, 0, squareColors); // ED: Added
        glEnableVertexAttribArray(attributes[ATTRIB_COLOR]); // ED: Added
       
        // 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 (![self validateProgram:program]) { // ED: Removed
        //    NSLog(@"Failed to validate program: %d", program); // ED: Removed
        if (![myOpenGLProgram validate]) { // ED: Added
            NSLog(@"Failed to validate program: %d", [myOpenGLProgram programId]); // ED: Added
            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);
   
    [(EAGLView *)self.view presentFramebuffer];
}

Not too many changes here. Let’s go through them.

   if ([context API] == kEAGLRenderingAPIOpenGLES2) {
        // Use shader program.
        //glUseProgram(program); // ED: Removed
        glUseProgram([myOpenGLProgram programId]); // ED: Added

The first change is to the glUseProgram() call. We’re no longer using an instance variable for this, but the programId property of our new EDOpenGLProgram object.

        // Update uniform value.
        glUniform1f(uniforms[UNIFORM_TRANSLATE], (GLfloat)transY);
        transY += 0.075f;
       
        // Update attribute values.
        //glVertexAttribPointer(ATTRIB_VERTEX, 2, GL_FLOAT, 0, 0, squareVertices); // ED: Removed
        //glEnableVertexAttribArray(ATTRIB_VERTEX); // ED: Removed
        //glVertexAttribPointer(ATTRIB_COLOR, 4, GL_UNSIGNED_BYTE, 1, 0, squareColors); // ED: Removed
        //glEnableVertexAttribArray(ATTRIB_COLOR); // ED: Removed
       
        glVertexAttribPointer(attributes[ATTRIB_VERTEX], 2, GL_FLOAT, 0, 0, squareVertices); // ED: Added
        glEnableVertexAttribArray(attributes[ATTRIB_VERTEX]); // ED: Added
        glVertexAttribPointer(attributes[ATTRIB_COLOR], 4, GL_UNSIGNED_BYTE, 1, 0, squareColors); // ED: Added
        glEnableVertexAttribArray(attributes[ATTRIB_COLOR]); // ED: Added

While the uniform logic remained unchanged, the logic to set attributes is now using the values we stored off into our new attributes array during program initialization.

        // 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 (![self validateProgram:program]) { // ED: Removed
        //    NSLog(@"Failed to validate program: %d", program); // ED: Removed
        if (![myOpenGLProgram validate]) { // ED: Added
            NSLog(@"Failed to validate program: %d", [myOpenGLProgram programId]); // ED: Added
            return;
        }

The final change is to the DEBUG validation step. We’ve change from a call to our own validateProgram: method to our new EDOpenGLProgram object’s validate method, and we’re using the programId property from the new class instead of an instance variable.

In the TouchTargetsViewController.m file, all code below the drawFrame method has been removed (commented out), since all of that processing has been moved to our new classes.

Compiling and running the new code results in the following.

Nothing appears to have changed, and yet our new EDOpenGLProgram class has been implemented and all of the OpenGL shader management code has been removed from the view controller.

Everything is nice and neat. Now we’re ready to start developing some fun stuff, starting with a complete font sheet importer and text management class.

Chapter 2 | Index | Chapter 4