Introduction to OpenGL

Now that we have learned how to initialized SDL, it's time to initialize OpenGL and draw a pyramid!  In this tutorial, we are going to build off the last one and use the same code base.

The first step is to include the correct headers as shown below:

....

#include "SDL.h" // include the SDL headers

#include <GL/gl.h> // include GL headers

#include <GL/glu.h>

SDL_Surface *mainWindow;

....

Now lets create a new function called initializeGL() which will setup some openGL defaults such as the camera and lighting.

....

bool initializeSDL(); // return true on sucess

bool createWindow(); // returns true on sucess

bool initializeGL(); // return true on sucess

....

 if (!createWindow())

   return 1; // return an error code of 1

 // initalize opengl

 // returns false it theres an error

 if (!initializeGL())

   return 1; // return an error code of 1

 sleep(10);

....

bool initializeGL()

{

}

....

One important thing to remember about openGL is that it is a state machine.  This means that if you enable an attribute, it stays enabled through the whole program until you disable it.  This can be very useful as you will see below.

There are a couple things that we want to enable on startup.  Two of the most common things are depth testing and back face culling.  

Before there was depth testing, it was required that you draw your furthest away objects first, or else they would show up in front of closer objects.  Depth testing will let us draw objects in any order, and opengl will sort out what object is in front of another.

Back face culling is useful because it can speed up the rendering of the scene.  Every face has two sides, a front and a back.  Most of the time, you will only see one side, the front.  Back face culling tells opengl not to draw the back side of an object which cuts our rendering time in half.  One very important thing to know is which side of the face is the front.  OpenGL expects faces to be drawn in counter-clockwise order.  

The above triangle would be facing us because it is draw counter-clockwise so we would see this triangle.  Most objects that you create will be enclosed surfaces, and you will never be able to see the back of a triangle.

Attributes are enabled and disabled using the openGL function: glEnable(GL_ENUM) and glDisable(GL_ENUM);

....

bool initializeGL()

{

 glEnable(GL_DEPTH_TEST); // enable depth testing

 glEnable(GL_CULL_FACE); // enable backface culling

}

....

The last attribute we need to set is the clear color.  Everytime we draw the scene we will have to clear both the color buffer and the depth buffer.  The clear color is the color that opengl will clear the screen to.  Normally you want this to be white.

In openGL colors are defined using RGB (Red, Green, Blue).  A value of 1.0 is full, and a value of 0.0, is none of that color.  For example:

black: 0.0, 0.0, 0.0 // no colors at all

white: 1.0, 1.0, 1.0 // all colors are full

red: 1.0, 0.0, 0.0 // all red, nothing else

green: 0.0, 1.0, 0.0 // all green, nothing else

blue: 0.0, 0.0, 1.0 // all blue, nothing else

purple: 1.0, 0.0, 1.0 // all blue and red makes purple

To change the clear color, we use the command:

glClearColor(GLfloat R, GLfloat G, GLfloat B, GLfloat A);

The A stands for opacity.  A value of 1.0 is full opacity (no transparency), while a value of 0.0 is no opacity (full transparency).

....

 glEnable(GL_CULL_FACE); // enable backface culling

 glClearColor (1.0, 1.0, 1.0, 1.0); // clear color

}

....

The last thing we need to do is initialize our viewport and matrices. Everytime the window is resized, these HAVE to be called !

The viewport sets up what we are able to see.  It takes 4 parameters, the x and y of the lower left hand corner, the width, and the height.  Usually the x and y will both be zero, and the width and height will be the size of your window (800x600).

....

 glClearColor (1.0, 1.0, 1.0, 1.0); // clear color

 glViewport(0, 0, 800, 600); // set the viewport to the size of out window

}

....

Next we need to initialize our matrices.  In openGL their are two matrices: GL_PROJECTION and GL_MODELVIEW.  GL_PROJECTION is the projection matrix where you will usually define camera transformations.  GL_MODELVIEW is the matrix that you will use to apply transformations to your 3d objects.

The Matrices in openGL can be VERY confusing.  Most of the time when you are drawing, you will be in the GL_MODELVIEW matrix, and you will only use GL_PROJECTION when setting up the camera.

In the next section of code, we are going to setup the camera.  To do this, we switch to the GL_PROJECTION matrix, load the identity matrix, and setup the camera.  Once we are done doing that, we go back to the GL_MODELVIEW matrix and load the identity matrix.

OpenGL does all its transformations using matrix multiplication.  Matrix multiplication can get very tricky very quickly, and the results are not always what you expect !

Every matrix starts out at the identity matrix.  The identity matrix is defined as:

1 0 0 0

0 1 0 0

0 0 1 0

0 0 0 1

As you can see, matrices in openGL are 4x4 matrices.  This identity matrix is the base matrix without any transformations applied to it.  the loadIdentity() call deletes everything that was in the current matrix, and sets it to the identity matrix.  This lets us basically start over and remove all our past transformations !

....

 glViewport(0, 0, 800, 600); // set the viewport to the size of out window

 glMatrixMode(GL_PROJECTION); // load the projection matrix

 glLoadIdentity(); // remove all transformations

 

 // set the camera in perspective mode

 gluPerspective(45.0f, // angle

  800.0 / 600.0, // ratio of width / height

  1.0, // near clipping plane

  250.0); // far clipping plane

 

 glMatrixMode(GL_MODELVIEW); // go back to the modelview matrix and prepare to draw

 glLoadIdentity(); // remove all the transformations

 return true; // everything was created ok

}

....

The gluPerspective() function sets up our camera in perspective mode.  It sets the viewing angle at 45 degrees, and the near clipping plane at 1.0 and the far clipping plane at 250.  That means we will be able to see objects that are no more than 1 unit close to us and 250 units away from us.

Now that opengl is setup, it's time to draw something. In the main function we will start a loop that will run until the window is closed.  All the loop will do is call renderScene() as fast as it can.  renderScene() is where we will put our drawing code.

We'll use SDL to determine when the window has been closed, or if the user pressed 'q' to quit the program.

We'll also remove the sleep(10); call from the last code base as we won't need it anymore.

....

bool initializeGL(); // return true on sucess

void renderScene(); // render our scene

int main(int argc, char **argv)

....

 if (!initializeGL())

   return 1; // return an error code of 1

 bool done = false; // loop variable

 SDL_Event event; // hold the SDL event

 // loop until the user quits

 while (!done)

 {

   // see if SDL has recieved and event

   //  the event could be a keyboard/mouse

   //  event, or notification that the

   //  window was closed minimized

   //  this returns true if an event is waiting

   while (SDL_PollEvent(&event))

   {

     switch (event.type) // what type of event

     {

     case SDL_QUIT: // user click on the X to close th e window

done = true; // quit

break;

     case SDL_KEYDOWN: // user pressed a key

switch (event.key.keysym.sym) // which key ?

{

case SDLK_q: // q key ?

  done = true; // quit

  break;

}

break;

     default:

break;

     }

   }

   // draw the scene

   renderScene();

 }

 return 0; // all done

....

Now let's create our renderScene function.  Before we render, we want to clear both the color buffer and the depth buffer.  These are cleared using the glClear() command;

After that, we will call a glLoadIdentity();  It is a good idea to always clear out your transformations before you draw.

We will then change our color to blue using glColor3f();

Next we draw.  In openGL, it is best to draw everything as triangles.  Make sure you draw your triangles in counter-clockwise orientation or they will NOT show up !

To draw a triangle, we will specify it as 3 different points.  Each point is specified using the glVertex3f(float x, float y, float z) call which takes an x, y, and z coordinate.

One important thing to remember, is that in openGL's 3d coordinate system, 0,0,0 is in the bottom left hand corner of the screen.  To the right is positive X, up is positive Y, and coming OUT of the screen is positive Z.

We are going to draw a triangle at the points:

5.0, 0.0, -20.0

0.0, 8.0, -20.0

-5.0, 0.0, -20.0

The reason the Z is at -20.0 is because our camera is positioned at 0,0,0 and the object we are drawing has to be at least 1 unit away or our near clipping plane will cut it off.  We are drawing a triangle 20 units into the screen (away from the user and camera).  Our object will look like:

Since we are using double buffering we have to swap the buffers.  Double buffering is a trick that allows our scene to be drawn without tearing.  With double buffering, you are actually drawing to a back buffer that is not displayed, then when everything is done drawing, you swap the back buffer and front buffer.  If we didn't do this, you would be able to see the image as it was being drawn, and it would tear and not look realistic.  Here's the code for our renderScene function:

....

void renderScene()

{

 // clear the color and depth buffers

 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

 // delete our old transformations

 glLoadIdentity();

 // change the color to blue

 glColor3f(0.0, 0.0, 1.0);

// start drawing !

 //  we are going to draw in trinagles

 glBegin(GL_TRIANGLES);

   glVertex3f(5.0, 0.0, -20.0);

   glVertex3f(0.0, 8.0, -20.0);

   glVertex3f(-5.0, 0.0, -20.0);

 glEnd(); // done drawing

 // swap the buffers so we can see what was drawn !

 SDL_GL_SwapBuffers();

}

....

Compile this code with:

g++ -o GLIntro GLIntro.cpp `sdl-config --cflags --libs` -lGL -lGLU

Download the source here