Part Ten: Matrix Tools

One of the things that disappointed me about many other OpenGL tutorials out there was the way they handled all of the matrix manipulation code. As someone trying to learn OpenGL ES 2.0, I really needed to understand how to handle the matrices I was supposed to feed into the shaders. Instead of explaining all of that matrix handling code, however, a lot of tutorials would just have a link to a source file, then instruct the reader to download the file and use a bunch of utility functions in it for the matrix code.

That saves a lot of time, but if you’re trying to learn how to write that kind of code, it’s a little counter-productive. For demonstration purposes, we’re going to write our own utility class full of class methods to handle copying, multiplying, and managing the different types of OpenGL matrices.

Using a bunch of class methods for this might not be the best way to write a high-performance OpenGL program, but it’ll keep everything neatly in the Xcode project, and be easy to follow.

First, let’s create a new EDMatrixTools class. In Xcode, right click on the EDCubeDemo folder near the top of the file hierarchy and select ‘New File…’.

 

Creating a new file in Xcode

 

Then, on the next panel, make sure ‘Cocoa Touch’ is selected under the ‘iOS’ category, and double click on the ‘Objective-C class’ icon.

 

Selecting the file type

 

On the next screen, keep the default value of ‘NSObject’ in the ‘Subclass of’ field and click on the ‘Next’ button.

 

Setting the superclass

 

On the file chooser that pops up next, name the new file ‘EdMatrixTools’ and click on the ‘Save’ button.

 

Saving the file

 

You will now see ‘EDMatrixTools.h’ and ‘EDMatrixTools.m’ files in your project structure.

 

Xcode project listing

 

This is where we’re going to put all of our new matrix utility code.

The first method we’re going to create is one of the simplest and most useful, the copyMatrix:to: method.

In the EDMatrixTools.m file, code the following class method between the ‘@implementation EDMatrixTools’ and ‘@end’ lines.

+ (void)copyMatrix:(GLfloat *)mSource to:(GLfloat *)mTarget {
    // Copy all 16 matrix entries from the source into the target
    for(int i = 0; i < 16; i++) {
        mTarget[i] = mSource[i];
    }
}

This method will simply copy all 16 members from the mSource matrix into the mTarget matrix.

The plus sign in front of the method means that this is a class method, and doesn’t require that we instantiate the class to use it.

The next small but useful method will load the identity matrix into the passed-in matrix variable.

+ (void)applyIdentity:(GLfloat *)m {
    // First column
    m[0] = 1;
    m[1] = 0;
    m[2] = 0;
    m[3] = 0;
   
    // Second column
    m[4] = 0;
    m[5] = 1;
    m[6] = 0;
    m[7] = 0;
   
    // Third column
    m[8] = 0;
    m[9] = 0;
    m[10] = 1;
    m[11] = 0;
   
    // Fourth column
    m[12] = 0;
    m[13] = 0;
    m[14] = 0;
    m[15] = 1;
}

This class method just clobbers whatever was in the passed-in matrix with an identity matrix.

Of course, none of this matrix stuff will work without a matrix multiplication method. Multiplication of two 4×4 matrices is just like the matrix math we’ve already done, just more of it.

Look at the following diagram.

 

 

Multiplying a 4×4 matrix by a 4×4 matrix is just like multiplying a 4×4 matrix by a 4×1 matrix, just like we did earlier, but three more times.

What’s important to note is that the first matrix is on the left, and the second matrix has been raised. If you reverse the matrices, you may get different results.

So what would this look like in our code?

+ (void)multiplyMatrix:(GLfloat *)m1 by:(GLfloat *)m2 giving:(GLfloat *)m3 {
    GLfloat tempMatrix[16];
   
    // First column
    tempMatrix[0] = (m1[0] * m2[0]) + (m1[4] * m2[1]) + (m1[8] * m2[2]) + (m1[12] * m2[3]);
    tempMatrix[1] = (m1[1] * m2[0]) + (m1[5] * m2[1]) + (m1[9] * m2[2]) + (m1[13] * m2[3]);
    tempMatrix[2] = (m1[2] * m2[0]) + (m1[6] * m2[1]) + (m1[10] * m2[2]) + (m1[14] * m2[3]);
    tempMatrix[3] = (m1[3] * m2[0]) + (m1[7] * m2[1]) + (m1[11] * m2[2]) + (m1[15] * m2[3]);
   
    // Second column
    tempMatrix[4] = (m1[0] * m2[4]) + (m1[4] * m2[5]) + (m1[8] * m2[6]) + (m1[12] * m2[7]);
    tempMatrix[5] = (m1[1] * m2[4]) + (m1[5] * m2[5]) + (m1[9] * m2[6]) + (m1[13] * m2[7]);
    tempMatrix[6] = (m1[2] * m2[4]) + (m1[6] * m2[5]) + (m1[10] * m2[6]) + (m1[14] * m2[7]);
    tempMatrix[7] = (m1[3] * m2[4]) + (m1[7] * m2[5]) + (m1[11] * m2[6]) + (m1[15] * m2[7]);
   
    // Third column
    tempMatrix[8] = (m1[0] * m2[8]) + (m1[4] * m2[9]) + (m1[8] * m2[10]) + (m1[12] * m2[11]);
    tempMatrix[9] = (m1[1] * m2[8]) + (m1[5] * m2[9]) + (m1[9] * m2[10]) + (m1[13] * m2[11]);
    tempMatrix[10] = (m1[2] * m2[8]) + (m1[6] * m2[9]) + (m1[10] * m2[10]) + (m1[14] * m2[11]);
    tempMatrix[11] = (m1[3] * m2[8]) + (m1[7] * m2[9]) + (m1[11] * m2[10]) + (m1[15] * m2[11]);
   
    // Fourth column
    tempMatrix[12] = (m1[0] * m2[12]) + (m1[4] * m2[13]) + (m1[8] * m2[14]) + (m1[12] * m2[15]);
    tempMatrix[13] = (m1[1] * m2[12]) + (m1[5] * m2[13]) + (m1[9] * m2[14]) + (m1[13] * m2[15]);
    tempMatrix[14] = (m1[2] * m2[12]) + (m1[6] * m2[13]) + (m1[10] * m2[14]) + (m1[14] * m2[15]);
    tempMatrix[15] = (m1[3] * m2[12]) + (m1[7] * m2[13]) + (m1[11] * m2[14]) + (m1[15] * m2[15]);
   
    [self copyMatrix:tempMatrix to:m3];
}

Whew, that’s a lot of math. But wait, why did we create a temporary matrix variable and copy it into m3 at the end? Why didn’t we put everything directly into m3?

Because if I use the same variable for m3 as for m1 or m2, using m3 directly in this code would change the values in the matrix as we calculated with it! When I use this method, I will usually use it like this.

 [EDMatrixTools multiplyMatrix:scaleMatrix by:mvpMatrix giving:mvpMatrix]

I can’t be assigning values to mvpMatrix as I’m using it to multiply with, so I need to save the results and copy them over when I’m all finished.

This will make more sense as we start using these utility methods in our project code.

Now we need to create a method for applying translations to a matrix.

+ (void)applyTranslation:(GLfloat *)m x:(GLfloat)x y:(GLfloat)y z:(GLfloat)z {
    GLfloat tempMatrix[16];
   
    [self applyIdentity:tempMatrix];
   
    tempMatrix[12] = x;
    tempMatrix[13] = y;
    tempMatrix[14] = z;
   
    [self multiplyMatrix:tempMatrix by:m giving:m];
}

The applyTranslation:x:y:z: method will take an existing matrix, create a temporary identity matrix with the x, y, and z translate values loaded into it, and multiply the two matrices. When this method is complete, whatever model-view-projection matrix that was passed in will now have the translation values in it.

One more method and we can rewrite our drawFrame method code.

+ (void)applyScale:(GLfloat *)m x:(GLfloat)x y:(GLfloat)y z:(GLfloat)z {
    GLfloat tempMatrix[16];
   
    [self applyIdentity:tempMatrix];
   
    tempMatrix[0] = x;
    tempMatrix[5] = y;
    tempMatrix[10] = z;
   
    [self multiplyMatrix:tempMatrix by:m giving:m];
}

Just like the previous method, the applyScale:x:y:z: method creates a temporary matrix, loads identity values, then populates the values for scaling and multiplies against the passed-in model-view-projection matrix.

In the EDMatrixTools.h file, add the following #import statements so everything will compile correctly.

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

You should also add the method definitions to the header file to avoid compiler warnings.

The whole header file will end up looking like this.

#import <Foundation/Foundation.h>

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

@interface EDMatrixTools : NSObject {
   
}

+ (void)copyMatrix:(GLfloat *)mSource to:(GLfloat *)mTarget;
+ (void)applyIdentity:(GLfloat *)m;
+ (void)multiplyMatrix:(GLfloat *)m1 by:(GLfloat *)m2 giving:(GLfloat *)m3;
+ (void)applyTranslation:(GLfloat *)m x:(GLfloat)x y:(GLfloat)y z:(GLfloat)z;
+ (void)applyScale:(GLfloat *)m x:(GLfloat)x y:(GLfloat)y z:(GLfloat)z;

@end

Also, at the top of the EDDemoCubeViewController.m file, make sure you add an #include statement for our new EDMatrixTools class.

#import "EDMatrixTools.h"

After a few modifications to our drawFrame method, we’re finished converting from inline matrix-handling code to our shiny new utility methods.

- (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;
    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);
       
        /* ED: Removed, EDMatrixTools class now handles this
        // Column 1
        mvpMatrix[0] = 1;
        mvpMatrix[1] = 0;
        mvpMatrix[2] = 0;
        mvpMatrix[3] = 0;
       
        // Column 2
        mvpMatrix[4] = 0;
        mvpMatrix[5] = 1;
        mvpMatrix[6] = 0;
        mvpMatrix[7] = 0;
       
        // Column 3
        mvpMatrix[8] = 0;
        mvpMatrix[9] = 0;
        mvpMatrix[10] = 1;
        mvpMatrix[11] = 0;
       
        // Column 4
        mvpMatrix[12] = 0;
        mvpMatrix[13] = 0;
        mvpMatrix[14] = 0;
        mvpMatrix[15] = 1;
       
        mvpMatrix[12] = cosf(transX) / 2.0f;
        mvpMatrix[13] = sinf(transY) / 2.0f;
        mvpMatrix[14] = 0;
       
        mvpMatrix[0] = 0.5f;
        mvpMatrix[5] = 0.5f;
        mvpMatrix[10] = 1;
         */

       
        [EDMatrixTools applyIdentity:mvpMatrix]; // ED: Added
       
        [EDMatrixTools applyScale:mvpMatrix x:0.5f y:0.5f z:1.0f]; // ED: Added
        [EDMatrixTools applyTranslation:mvpMatrix x:(cosf(transX) / 2.0f) y:(sinf(transY) / 2.0f) z:0.0f]; // 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];
}

We were able to get rid of all of that array assignment code we were using to populate and modify our matrices and replace it with a just a few calls to our utility functions.

Now let’s make sure everything still works by building and running the program.

 

iPhone screen

 

Yes, everything still looks good.

Now that we have our matrix utility code separated, let’s work on adding some more functionality to it. Since we already know how to translate and scale our vertices, let’s learn how to rotate them.

Part Nine | Index | Part Eleven