Part Twelve: The Projection Matrix

In the last chapter, we added code to rotate the rainbow square as it traveled around the iPhone screen, but when we did that it became misshapen, stretching as it rotated. But why?

    static const GLfloat squareVertices[] = {
        -0.5f, -0.33f,
        0.5f, -0.33f,
        -0.5f,  0.33f,
        0.5f,  0.33f,
    };

Look back at the vertices for our square. Remember how we determined that the length of our square along the Y axis was actually shorter than the along the X axis to account for the iPhone’s aspect ratio? This is fine as long as the orientation doesn’t change, but when it does, like it did when we started to rotate the square, the longer side will end up along the Y axis and the shorter side will end up along the X axis. That’s the exact opposite of what we wanted in the first place, so it’s no wonder it looks stretched out as it rotates around.

The solution is to think of OpenGL space as completely independent from the iPhone screen space. Don’t worry about aspect ratios when you define your objects, just think in OpenGL space coordinates.

If we were to change our thinking for the square that’s defined in our program, it would simply be half of the X axis and half of the Y axis. The new definition would be a little more clear.

    static const GLfloat squareVertices[] = {
        -0.5f, -0.5f,
        0.5f, -0.5f,
        -0.5f, 0.5f,
        0.5f, 0.5f,
    };

Ah, now we have defined a square that runs one unit high and one unit wide. If we were running our OpenGL program on a screen that was 480 pixles wide by 480 pixels high (a 1:1 aspect ratio), our object would be perfectly square.

The iPhone is 320 pixels wide by 480 pixels high, however, so our shape will look stretched. How can we fix this?

One way would be to add the aspect ratio to the scale matrix, so that when the object was scaled, it would be adjusted to match the aspect ratio. We could divide the width by the height to get an aspect ratio that we could use to adjust the height of the shape.

For example, 320 / 480 (an aspect ratio of 1:1.5) is 0.66 repeating, so we could apply that ratio to the scale matrix like this.

[EDMatrixTools applyScale:mvpMatrix x:1 y:(1 * (width / height)) z:1];

In our new squareVertices array, the square is one unit wide and one unit high. After applying the scale from the line of code above, the square would be one unit wide and 0.66 units high, just like it was when we hardcoded the aspect ratio into the vertex coordinates.

There’s still a problem, though. OpenGL performs all of the scaling and rotating operations around the origin, (0, 0). If I scale this object, it looks right, but if I then rotate the square, it looks wrong again for the same reason it’s being stretched now. It was adjusted for a specific aspect ratio and then rotated out of it.

We could just make sure that we always rotate first and then scale, and that will work, but it adds additional complexity to the matrix management and creates a dependency on scaling for the rotation logic. I think there’s a cleaner way to logically separate the aspect ratio code from the model-view matrices, leaving us with cleaner code and clearer logic.

Remember how the model matrix was associated with the manipulation of the rendered objects, and the view matrix was associated with the way the observer sees those rendered objects? The third matrix, the projection matrix, is responsible for the field of vision, perspective, and aspect ratio. Let’s handle the aspect ratio adjustments in the projection matrix and keep the model and view matrices as clean as possible.

Later, when we have a 3D object, we’ll make the projection matrix responsible for field of vision and perspective, but for now it’s just going to handle the aspect ratio adjustment. In fact, it will basically be a scale matrix that we save for the end of the render matrix calculations, allowing us to uniformly apply that aspect ratio to all of the other calculations we’ve already completed.

First, we’re going to fix our squareVertices array to get rid of any adjustments for aspect ratios.

    /* ED: Removed
    static const GLfloat squareVertices[] = {
        -0.5f, -0.33f,
        0.5f, -0.33f,
        -0.5f,  0.33f,
        0.5f,  0.33f,
    };
     */

   
    // ED: Added
    static const GLfloat squareVertices[] = {
        -0.5f, -0.5f,
        0.5f, -0.5f,
        -0.5f, 0.5f,
        0.5f, 0.5f,
    };

Then we’ll add a line to create and apply a projection matrix after all of the other calculations have taken place.

        [EDMatrixTools applyProjection:mvpMatrix aspect:1.5]; // ED: Added

The entire drawFrame method in EDCubeDemoViewController.m will now look like this.

- (void)drawFrame
{
    [(EAGLView *)self.view setFramebuffer];
   
    // Replace the implementation of this method to do your own custom drawing.
    /* ED: Removed
    static const GLfloat squareVertices[] = {
        -0.5f, -0.33f,
        0.5f, -0.33f,
        -0.5f,  0.33f,
        0.5f,  0.33f,
    };
     */

   
    // ED: Added
    static const GLfloat squareVertices[] = {
        -0.5f, -0.5f,
        0.5f, -0.5f,
        -0.5f, 0.5f,
        0.5f, 0.5f,
    };
   
    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;
    static float transX = 0.0f;
   
    GLfloat mvpMatrix[16];
   
    glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
   
    if ([context API] == kEAGLRenderingAPIOpenGLES2) {
        // Use shader program.
        glUseProgram(program);
       
        [EDMatrixTools applyIdentity:mvpMatrix];
       
        [EDMatrixTools applyScale:mvpMatrix x:0.5f y:0.5f z:1.0f];
        [EDMatrixTools applyRotation:mvpMatrix x:0 y:0 z:transY];
        [EDMatrixTools applyTranslation:mvpMatrix x:(cosf(transX) / 2.0f) y:(sinf(transY) / 2.0f) z:0.0f];
       
        [EDMatrixTools applyProjection:mvpMatrix aspect:1.5]; // ED: Added
               
        // Update uniform value.
        glUniformMatrix4fv(uniforms[UNIFORM_MVP_MATRIX], 1, 0, mvpMatrix);
        transY += 0.075f;  
        transX += 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);
       
        // 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, 4);
   
    [(EAGLView *)self.view presentFramebuffer];
}

Now we need to add this method to our EDMatrixTools class. In the EDMatrixTools.h file, add the following method definition.

+ (void)applyProjection:(GLfloat *)m aspect:(GLfloat)aspect; // ED: Added

In the EDMatrixTools.m file, add the following method.

// ED: Added method
+ (void)applyProjection:(GLfloat *)m aspect:(GLfloat)aspect {
    GLfloat tempMatrix[16];
   
    [self applyIdentity:tempMatrix];
   
    tempMatrix[0] = 1;
    tempMatrix[5] = 1 / aspect;
   
    [self multiplyMatrix:tempMatrix by:m giving:m];
}

It’s a very simple method right now, but it’ll get more complex soon.

Save your changes and try building and running the new code.

 

iPhone screen

 

That’s better. Now we have a square defined from vertices in normal OpenGL space, and a projection matrix that’s making it all look right just before we draw it all to the screen.

I’m getting sick of rainbows, though. Let’s try drawing something on that square now.

Part Eleven | Index | Part Thirteen