Part Seventeen: Putting It All Together

Now that we have been through all of the code to handle scale, rotation, translation, and projection matrices, let’s make our cube a little more interactive. We’ll do this in a few steps.

Currently, our cube is automatically rotating slowly around the X and Y axes, but we want to change the application so we can rotate the cube by dragging our finger around the iPhone screen. We’re going to need to somehow update the rotation matrix when the we touch the screen and drag, and the translation matrix will be updated if the user zooms by pinching. The scale matrix won’t be updated at all after program initialization. This being the case, we should break up our matrices and make them available to the entire program, instead of creating a local matrix in the drawFrame method.

Let’s create matrix variables in the EDCubeDemoViewController.h file so we can use and update these matrices from anywhere in the view controller. We’re also going to need to track where the user touches, for dragging to rotate and for pinching to zoom. The new EDCubeDemoViewController.h file will look like this.

#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>

@interface EDCubeDemoViewController : UIViewController {
@private
    EAGLContext *context;
    GLuint program;
   
    BOOL animating;
    NSInteger animationFrameInterval;
    CADisplayLink *displayLink;
   
    GLuint textureName;
   
    GLfloat scaleMatrix[16]; // ED: Added
    GLfloat rotationMatrix[16]; // ED: Added
    GLfloat translationMatrix[16]; // ED: Added
    GLfloat projectionMatrix[16]; // ED: Added
   
    float lastPinchDistance; // ED: Added
    float lastZoomDistance; // ED: Added
    CGPoint lastTouchPosition; // ED: Added
}

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

- (void)startAnimation;
- (void)stopAnimation;
- (void)loadTexture:(GLuint *)textureName fromFile:(NSString *)fileName;
- (void)rotateCubeAroundX:(float)x andY:(float)y; // ED: Added

@end

We’ve added a variable for each individual matrix, variables to handle touch tracking, and a definition for our new method to perform the cube rotation.

Let’s see how we make use of all of these new variables in the EDCubeDemoViewController.m code.

- (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)
        [self loadShaders];
   
    animating = FALSE;
    animationFrameInterval = 1;
    self.displayLink = nil;
   
    [self loadTexture:&textureName fromFile:@"apple_logo.png"];

    GLint framebufferWidth, framebufferHeight; // ED: Added
   
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &framebufferWidth); // ED: Added
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &framebufferHeight); // ED: Added
   
    GLfloat aspectRatio = (GLfloat)framebufferHeight / (GLfloat)framebufferWidth; // ED: Added
   
    lastZoomDistance = -5.0f; // ED: Added
   
    [EDMatrixTools applyIdentity:scaleMatrix]; // ED: Added
    [EDMatrixTools applyIdentity:rotationMatrix]; // ED: Added
    [EDMatrixTools applyIdentity:translationMatrix]; // ED: Added
    [EDMatrixTools applyIdentity:projectionMatrix]; // ED: Added
   
    [EDMatrixTools applyTranslation:translationMatrix x:0 y:0 z:lastZoomDistance]; // ED: Added
    [EDMatrixTools applyProjection:projectionMatrix fov:45 aspect:aspectRatio near:1 far:100]; // ED: Added
}

The first bit of new code is going to get the width and height of our screen area so we can calculate an aspect ratio to apply to our projection matrix. We’re also defaulting the zoom distance to 5 units away.

Since we’re going to be managing each matrix independently now, we’ll set each of them to the identity matrix at initialization, then apply any changes we’re ready to make to them. At this point, we’ll be applying our zoom distance default to the translation matrix, and setting up our projection matrix with the aspect ratio we just calculated.

Now let’s see what changes we need to make to the drawFrame method.

- (void)drawFrame
{
    [(EAGLView *)self.view setFramebuffer];
   
    static const GLfloat cubeVertices[] = {
        // Front face
        -1,-1,1, 1,-1,1, -1,1,1, 1,1,1,
        // Right face
        1,1,1, 1,-1,1, 1,1,-1, 1,-1,-1,
        // Back face
        1,-1,-1, -1,-1,-1, 1,1,-1, -1,1,-1,
        // Left face
        -1,1,-1, -1,-1,-1, -1,1,1, -1,-1,1,
        // Bottom face
        -1,-1,1, -1,-1,-1, 1,-1,1, 1,-1,-1,
       
        // move to top
        1,-1,-1, -1,1,1,
       
        // Top Face
        -1,1,1, 1,1,1, -1,1,-1, 1,1,-1
    };

    static const GLfloat cubeTexCoords[] = {
        // Front face
        0,0, 1,0, 0,1, 1,1,
        // Right face
        0,1, 0,0, 1,1, 1,0,
        // Back face
        0,0, 1,0, 0,1, 1,1,
        // Left face
        0,1, 0,0, 1,1, 1,0,
        // Bottom face
        0,1, 0,0, 1,1, 1,0,
       
        1,0, 0,0,
       
        // Top face
        0,0, 1,0, 0,1, 1,1
    };
   
    //static float transY = 0.0f; // ED: Removed
    //static float transX = 0.0f; // ED: Removed
   
    GLfloat mvpMatrix[16];
   
    glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
   
    if ([context API] == kEAGLRenderingAPIOpenGLES2) {
        glEnable(GL_TEXTURE_2D);
       
        glBindTexture(GL_TEXTURE_2D, textureName);
       
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

        // Use shader program.
        glUseProgram(program);
       
        [EDMatrixTools applyIdentity:mvpMatrix];
       
        //[EDMatrixTools applyScale:mvpMatrix x:0.50f y:0.50f z:0.50f]; // ED: Removed
        //[EDMatrixTools applyRotation:mvpMatrix x:(transX / 6) y:(transX / 6) z:0]; // ED: Removed
        //[EDMatrixTools applyTranslation:mvpMatrix x:0 y:0 z:-3]; // ED: Removed
       
        //[EDMatrixTools applyProjection:mvpMatrix fov:45 aspect:1.5 near:1 far:100]; // ED: Removed
       
        [EDMatrixTools applyIdentity:translationMatrix]; // ED: Added
        [EDMatrixTools applyTranslation:translationMatrix x:0 y:0 z:lastZoomDistance]; // ED: Added
       
        [EDMatrixTools multiplyMatrix:scaleMatrix by:mvpMatrix giving:mvpMatrix]; // ED: Added
        [EDMatrixTools multiplyMatrix:rotationMatrix by:mvpMatrix giving:mvpMatrix]; // ED: Added
        [EDMatrixTools multiplyMatrix:translationMatrix by:mvpMatrix giving:mvpMatrix]; // ED: Added
        [EDMatrixTools multiplyMatrix:projectionMatrix by:mvpMatrix giving:mvpMatrix]; // ED: Added
               
        // Update uniform value.
        glUniformMatrix4fv(uniforms[UNIFORM_MVP_MATRIX], 1, 0, mvpMatrix);
        glUniform1i(uniforms[UNIFORM_TEXTURE], 0);
        //transY += 0.075f; // ED: Remove
        //transX += 0.075f; // ED: Remove
       
        // Update attribute values.
        glVertexAttribPointer(ATTRIB_VERTEX, 3, GL_FLOAT, 0, 0, cubeVertices);
        glEnableVertexAttribArray(ATTRIB_VERTEX);
        glVertexAttribPointer(ATTRIB_TEXTURE_COORD, 2, GL_FLOAT, 0, 0, cubeTexCoords);
        glEnableVertexAttribArray(ATTRIB_TEXTURE_COORD);
       
        // 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]) {
            NSLog(@"Failed to validate program: %d", program);
            return;
        }
#endif
    }
   
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 26);
   
    [(EAGLView *)self.view presentFramebuffer];
}

Let’s look at each of the changes individually.

    //static float transY = 0.0f; // ED: Removed
    //static float transX = 0.0f; // ED: Removed

Since the user will now be rotating the cube by touch, we don’t need to be maintaing these variables to increment each frame anymore. We’ll be updating the rotational data in our new method, rotateCubeAroundX:andY:.

        //[EDMatrixTools applyScale:mvpMatrix x:0.50f y:0.50f z:0.50f]; // ED: Removed
        //[EDMatrixTools applyRotation:mvpMatrix x:(transX / 6) y:(transX / 6) z:0]; // ED: Removed
        //[EDMatrixTools applyTranslation:mvpMatrix x:0 y:0 z:-3]; // ED: Removed
       
        //[EDMatrixTools applyProjection:mvpMatrix fov:45 aspect:1.5 near:1 far:100]; // ED: Removed

We’ve already applied scale values to our scale matrix in the awakeFromNib: method, so we don’t need to do that here anymore. The rotation matrix will be updated when the user touches and drags, and the translation matrix will be updated when the user pinches. Finally, the projection matrix update is no longer needed because it was set up at initialization.

By moving these one-time matrix updates to the initialization method, which is only performed once, we can clean them out of the drawFrame: method, which is performed sixty times per second.

        [EDMatrixTools applyIdentity:translationMatrix]; // ED: Added
        [EDMatrixTools applyTranslation:translationMatrix x:0 y:0 z:lastZoomDistance]; // ED: Added

For the translation matrix, we need to apply the current zoom distance (which defaulted to -5) to an identity matrix to reset the value. If we didn’t apply the identity matrix, we’d be applying our zoom distance to whatever zoom distance was there before, pushing it farther away each time. The user will change the zoom distance by pinching, so we want to take whatever the zoom distance is currently and use it in creating our model-view-projection matrix for this rendering frame.

        [EDMatrixTools multiplyMatrix:scaleMatrix by:mvpMatrix giving:mvpMatrix]; // ED: Added
        [EDMatrixTools multiplyMatrix:rotationMatrix by:mvpMatrix giving:mvpMatrix]; // ED: Added
        [EDMatrixTools multiplyMatrix:translationMatrix by:mvpMatrix giving:mvpMatrix]; // ED: Added
        [EDMatrixTools multiplyMatrix:projectionMatrix by:mvpMatrix giving:mvpMatrix]; // ED: Added

Now we create the model-view-projection matrix by multiplying all of our matrices. The scale matrix will not change throughout the life of the application, but it still has to be included so the model will be scaled properly every time it’s rendered. The rotation matrix is populated in the rotateCubeAroundX:andY: method, and the translation matrix was updated just above.

Finally, the projection matrix is multiplied after everything else.

        //transY += 0.075f; // ED: Remove
        //transX += 0.075f; // ED: Remove

The only other change to the drawFrame method is the removal of the transX and transY variables, which are no longer used.

Now we need to add some code to detect when the user touches, drags, and pinches so we can make the cube interactive.

// ED: Added method
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    NSSet *allTouches = [event allTouches];
   
    if([allTouches count] == 1) {
        lastTouchPosition = [[touches anyObject] locationInView:self.view];
    } else if([allTouches count] == 2) {
        UITouch *t1 = [[allTouches allObjects] objectAtIndex:0];
        UITouch *t2 = [[allTouches allObjects] objectAtIndex:1];
       
        CGPoint p1 = [t1 locationInView:self.view];
        CGPoint p2 = [t2 locationInView:self.view];
       
        float x = p1.x - p2.x;
        float y = p1.y - p2.y;
       
        lastPinchDistance = sqrtf(x * x + y * y);
    }
}

The first new method will fire whenever the user touches the screen, with any number of fingers. If the user only uses one finger, we’ll make a note of the touch position by loading the lastTouchPosition variable.

If the user touched with two fingers, we’ll figure out how far apart those fingers were when he or she touched. We’ll need to know this so we can determine if the fingers get closer together or farther apart.

// ED: Added method
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    NSSet *allTouches = [event allTouches];
   
    if([allTouches count] == 1) {
        CGPoint currentTouchPosition = [[touches anyObject] locationInView:self.view];
       
        float xMovement = lastTouchPosition.x - currentTouchPosition.x;
        float yMovement = lastTouchPosition.y - currentTouchPosition.y;
       
        lastTouchPosition = currentTouchPosition;
       
        [self rotateCubeAroundX:yMovement andY:xMovement];
    } else if([allTouches count] == 2) {
        UITouch *t1 = [[allTouches allObjects] objectAtIndex:0];
        UITouch *t2 = [[allTouches allObjects] objectAtIndex:1];
       
        CGPoint p1 = [t1 locationInView:self.view];
        CGPoint p2 = [t2 locationInView:self.view];
       
        float x = p1.x - p2.x;
        float y = p1.y - p2.y;
       
        float currPinchDistance = sqrtf(x * x + y * y);
       
        float zoomDistance = lastPinchDistance - currPinchDistance;
       
        lastZoomDistance = lastZoomDistance - (zoomDistance / 100);
       
        lastPinchDistance = currPinchDistance;
    }
}

This method fires when the user, while touching the screen, drags his her finger(s) in some direction(s).

If it’s one finger being dragged, we look at the last position and subtract the current position, giving us the distance moved from the last touch to the current touch. We get the X axis travel and the Y axis travel and send them to a new method, rotateCubeAroundX:andY:.

If two fingers are being moved around, we determine the zoom distance by figuring out the current pinch distance and subtracting it from the last pinch distance. We then divide that zoom distance by 100 to make it more OpenGL-friendly (since large zoom values will have our cube flying out of sight in a heartbeat) and subtract it from the last zoom distance to get the new lastZoomDistance value.

// ED: Added method
- (void)rotateCubeAroundX:(float)x andY:(float)y {
    GLfloat totalXRotation = x * M_PI / 180.0f;
    GLfloat totalYRotation = y * M_PI / 180.0f;
   
    [EDMatrixTools applyRotation:rotationMatrix x:totalXRotation y:totalYRotation z:0.0];
}

The last new method, rotateCubeAroundX:andY: is pretty simple. All we need to do is convert the X and Y values to radians and update our rotation matrix with the applyRotation message. Note that we’re not clearing the matrix with an applyIdentity message, we want to keep a running value of where we are in space.

Let’s build and run and see how we did.

 

 

We can rotate and zoom. If we zoom in close enough, we can even see the inside of the cube!

One thing that would make this demo application complete is a snazzy app icon. Let’s take that last screen grab and create a 57×57 PNG file to use as an app icon.

 

 

Now, in my EDCubeDemo-info.plist file, let’s add the new icon.

 

 

Don’t forget to copy the icon.png file into your project if you’re following along in the code.

Build and run again, and you’ve got an app icon.

 

 

Obviously, there’s a lot more to OpenGL ES 2.0 on the iPhone than this, but what we’ve done so far should give you a pretty solid understanding of the basics. The next time you look at OpenGL sample code in a book or on a website somewhere, I hope you’ll find that the information you found here has helped you understand it better.

Have fun developing games and applications on the iPhone with OpenGL ES 2.0!

Part Sixteen | Index | Appendix A

4 Comments to “Part Seventeen: Putting It All Together”

  1. By Bryan, May 23, 2011 @ 5:28 pm

    The best OpenGL ES tutorial. Thank you very much.

  2. By Joe, May 23, 2011 @ 5:42 pm

    Thanks, Bryan!

  3. By opengles2learner, July 8, 2011 @ 7:58 am

    Hi, I was trying to implement this for my application in open gl es 2.0. I followed every step stated in your tutorial and got my 3d object (Bar graph) rotating. However, when the object rotates, it begins to scale down in size, as if I am moving far away from the object and over a point of time it disappears. I followed your instructions and also set the translation value of last zoom distance. Still I couldnt achieve much success. Could you please guide me in some way as to why this is happening? I have tried using both perspective projection and orthographic….but in both cases it appears that the image zooms out and disappears on touch. Please help….

    P.S A wonderful tutorial. Keep up the great work

    • By Joe, July 8, 2011 @ 9:32 am

      Hello! This has happened to me too. It’s always been a case where I was not handling some running variable properly, like you mentioned. The only thing I can suggest is to double check any variables that track scale or translation values and make sure that you’re setting the appropriate matrices to identity before applying the values each time.

      If you haven’t already, set a breakpoint right before you draw and check the variables and matrices that are being used by the active OpenGL program.

      Good luck!