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