Lazy Foo' Productions


Matrices and Coloring Polygons

Matrices and Coloring Polygons screenshot

Last Updated: Jul 6th, 2014

In this tutorial, we're going to set up a 640x480 coordinate system and while we're at it we're going to give some color to our polygon.

We'll also cover a super nutshelled explaination of the OpenGL pipeline and how it turns vertices into pixels.
From LUtil.h
///Screen constants
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;
const int SCREEN_FPS = 60;

//Color modes
const int COLOR_MODE_CYAN = 0;
const int COLOR_MODE_MULTI = 1;

bool initGL();
/*
Pre Condition:
 -A valid OpenGL context
Post Condition:
 -Initializes matrices and clear color
 -Reports to console if there was an OpenGL error
 -Returns false if there was an error in initialization
Side Effects:
 -Projection matrix is set to an orthographic matrix
 -Modelview matrix is set to identity matrix
 -Matrix mode is set to modelview
 -Clear color is set to black
*/

void update();
/*
Pre Condition:
 -None
Post Condition:
 -Does per frame logic
Side Effects:
 -None
*/

void render();
/*
Pre Condition:
 -A valid OpenGL context
 -Active modelview matrix
Post Condition:
 -Renders the scene
Side Effects:
 -Clears the color buffer
 -Swaps the front/back buffer
 -Sets matrix mode to modelview
 -Translates modelview matrix to the center of the default screen
 -Changes the current rendering color
*/

void handleKeys( unsigned char key, int x, int y );
/*
Pre Condition:
 -None
Post Condition:
 -Toggles the color mode when the user presses q
 -Cycles through different projection scales when the user presses e
Side Effects:
 -If the user presses e, the matrix mode is set to projection
*/
Here's our utility function again with some tweaks to the pre/post conditions and side effects. What's new is the color mode constants and handleKeys() function. The color mode constants are just symbols we use to render our quad.

In SDL (I hear there's a good tutorial set here), events are handled with event queues. With GLUT, they are handled with callbacks. In our main() function, we'll set our handleKeys() function as a callback to handle keys.
From LUtil.cpp
#include "LUtil.h"

//The current color rendering mode
int gColorMode = COLOR_MODE_CYAN;

//The projection scale
GLfloat gProjectionScale = 1.f;
At the top of LUtil.cpp, we have two global variables. Note: they are only global inside of LUtil.cpp.

"gColorMode" controls whether we render a solid cyan square or a multicolored square.

"gProjectionScale" controls how large of a coordinate area we want to render.

We'll cover how these variables are used in the render() and handleKeys() functions.
From LUtil.cpp
bool initGL()
{
    //Initialize Projection Matrix
    glMatrixMode( GL_PROJECTION );
    glLoadIdentity();
    glOrtho( 0.0, SCREEN_WIDTH, SCREEN_HEIGHT, 0.0, 1.0, -1.0 );

    //Initialize Modelview Matrix
    glMatrixMode( GL_MODELVIEW );
    glLoadIdentity();

    //Initialize clear color
    glClearColor( 0.f, 0.f, 0.f, 1.f );

    //Check for error
    GLenum error = glGetError();
    if( error != GL_NO_ERROR )
    {
        printf( "Error initializing OpenGL! %s\n", gluErrorString( error ) );
        return false;
    }

    return true;
}
Here's our graphics library initialization function. It's largely the same, except now we call glOrtho().

What glOrtho() does is multiply the current matrix against an orthographic (or 2D) perspective matrix with the left, right, bottom, top, near, and far values in the arguments. Here, we're setting up the coordinate system like this (similar to SDL):
coord
Note: +Y points down and -Y points up. For those of you new to graphics programming this may seem weird, but inverted Y axis is actually pretty standard in 2D graphics programming.

Notice how we multiplied the orthographic matrix against the projection matrix. This is what the projection matrix is for, to control how we view our geometry. If we wanted 3D perspective, we'd multiply the projection matrix against the perspective matrix (done with either gluPerspective() or glFrustum()).
From LUtil.cpp
void update()
{

}
And once again since we have a stationary quad, we don't do much of anything in our update() function. Also, our runMainLoop() function is exactly the same.

From now on, expect to only go over relevant new code. Otherwise, these tutorials would get really repetitive really quickly.
From LUtil.cpp
void render()
{
    //Clear color buffer
    glClear( GL_COLOR_BUFFER_BIT );

    //Reset modelview matrix
    glMatrixMode( GL_MODELVIEW );
    glLoadIdentity();

    //Move to center of the screen
    glTranslatef( SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 0.f );
Here's the top of our rendering function. After we clear the screen, we set the current matrix mode to modelview. We do this because in our key handling function we're going to change the projection matrix. If we don't make sure the current matrix is the modelview matrix, the projection and modelview matrix operations are going to be done incorrectly and we'll get funky results.

The reason we need the modelview matrix is because we're going to be applying tranformations to our geometry. Where projection matrix controls how the geometry is viewed, modelview matrix tranformations control how geometry is placed in the rendering world.

Because of the way we set up our projection matrix, the origin of the scene is in the top left corner. We want our square to show up in the middle of the screen, so we translate it (or slide it) to the center of the screen using glTranslate(). glTranslate() multiplies a translation matrix against the current matrix (which in the case is the modelview matrix) so that any geometry that's rendered is translated the x, y, and z amount given in the arguments.

Another thing to note is that in every frame we load the identity modelview matrix before rendering. If we didn't, the translation transformations would accumulate. So if we translated 10 every frame, after 60 frames we would have translated 600. Eventually, we'd translate completely off the screen. Try commenting out glLoadIdentity() in this line of code and see what happens.
From LUtil.cpp
    //Render quad
    if( gColorMode == COLOR_MODE_CYAN )
    {
        //Solid Cyan
        glBegin( GL_QUADS );
            glColor3f( 0.f, 1.f, 1.f );
            glVertex2f( -50.f, -50.f );
            glVertex2f(  50.f, -50.f );
            glVertex2f(  50.f,  50.f );
            glVertex2f( -50.f,  50.f );
        glEnd();
    }
Now it's time to render our square. If the color mode is cyan (which is the initial value), we want to render a solid color quad.

To set the next vertex color, we call glColor() with the red, green, and blue values. Then we set the vertex position with glVertex().

For the second time we send a vertex (as well as the third and fourth), we give no color. So the OpenGL pipeline just uses the last color value that was given. This gives our quad a solid cyan color.

You'll notice that we're making a square with a width of 100. Thanks to our projection matrix, it should be 100 pixels wide.
From LUtil.cpp
    else
    {
        //RYGB Mix
        glBegin( GL_QUADS );
            glColor3f( 1.f, 0.f, 0.f ); glVertex2f( -50.f, -50.f );
            glColor3f( 1.f, 1.f, 0.f ); glVertex2f(  50.f, -50.f );
            glColor3f( 0.f, 1.f, 0.f ); glVertex2f(  50.f,  50.f );
            glColor3f( 0.f, 0.f, 1.f ); glVertex2f( -50.f,  50.f );
        glEnd();
    }
If "gColorMode" is not cyan, we assume it must be multicolor.

Notice that this time, we give an individual color for each vertex. Again, it's important to give the color before you give the vertex because OpenGL looks at the most recent color value when rendering that particular vertex.

Ok so we rendered our quad. Now how does OpenGL use matrices to turn polygons into pixels? Let's use a polygon from a 3D cube as an example. Here's a (poorly drawn) cube:
cube

First, the modelview matrix is applied to translate it/rotate it/scale it/skew it/whatever into place:
transformed cube

Remember back to high school art class and those perspective scenes they made you do?
perspective

What the projection matrix does is take your vertices from your polygon and multiplies them to transform them into normalized perspective coordinates that OpenGL can use:
dots

Then it connects your polygon vertices
connect_the_dots

And starts filling in the pixels (this is called rasterization)
fillfillfillfillfillfill

Obviously, there's more to the OpenGL pipeline with things to control texturing, coloring, lighting, etc. In terms of how we get from geometry to pixels, all OpenGL does is take vertex coordinates and rasterizes them into pixels with ProjectionMatrix * ModelviewMatrix * Vertex.
From LUtil.cpp
    //Update screen
    glutSwapBuffers();
At the end of our render() function after our vertices were rasterized into pixels, we swap the front/back buffer to update the screen.
From LUtil.cpp
void handleKeys( unsigned char key, int x, int y )
{
    //If the user presses q
    if( key == 'q' )
    {
        //Toggle color mode
        if( gColorMode == COLOR_MODE_CYAN )
        {
            gColorMode = COLOR_MODE_MULTI;
        }
        else
        {
            gColorMode = COLOR_MODE_CYAN;
        }
    }
At the top of a key press handling function, we check if the user presses q. If they did, we switch the square back and forth between cyan and multicolor.
From LUtil.cpp
    else if( key == 'e' )
    {
        //Cycle through projection scales
        if( gProjectionScale == 1.f )
        {
            //Zoom out
            gProjectionScale = 2.f;
        }
        else if( gProjectionScale == 2.f )
        {
            //Zoom in
            gProjectionScale = 0.5f;
        }
        else if( gProjectionScale == 0.5f )
        {
            //Regular zoom
            gProjectionScale = 1.f;
        }

        //Update projection matrix
        glMatrixMode( GL_PROJECTION );
        glLoadIdentity();
        glOrtho( 0.0, SCREEN_WIDTH * gProjectionScale, SCREEN_HEIGHT * gProjectionScale, 0.0, 1.0, -1.0 );
    }
}
At the bottom of a key handling function, we check if the user pressed e. If they did, we cycle through the various project scales.
  1. 100% scale: default
  2. 200% scale: we render an area twice as large so everything is smaller
  3. 50% scale: we render an area half as large so everything is close up and bigger
After the scale is selected, we reset the projection matrix identity matrix and multiply with the scaled orthographic perspective matrix.

Those of you that really know your linear algebra are probably thinking "Can't we just multiply a scaling matrix against the projection matrix?". The answer is yes, and we'll be covering making transformation to the projection matrix in the scrolling tutorial.
From main.cpp
int main( int argc, char* args[] )
{
    //Initialize FreeGLUT
    glutInit( &argc, args );

    //Create OpenGL 2.1 context
    glutInitContextVersion( 2, 1 );

    //Create Double Buffered Window
    glutInitDisplayMode( GLUT_DOUBLE );
    glutInitWindowSize( SCREEN_WIDTH, SCREEN_HEIGHT );
    glutCreateWindow( "OpenGL" );

    //Do post window/context creation initialization
    if( !initGL() )
    {
        printf( "Unable to initialize graphics library!\n" );
        return 1;
    }

    //Set keyboard handler
    glutKeyboardFunc( handleKeys );

    //Set rendering function
    glutDisplayFunc( render );

    //Set main loop
    glutTimerFunc( 1000 / SCREEN_FPS, runMainLoop, 0 );

    //Start GLUT main loop
    glutMainLoop();

    return 0;
}
Our main() function is pretty much the same as before, only now we use glutKeyboardFunc() to set our keyboard handler callback.