OpenGL Tutorial for 308-557

Please send comments, questions and corrections to Matthew Suderman.

In this tutorial you will see seven simple OpenGL programs each successive one building on the previous one. Here's a short description of each program:

  1. Draws a white square on a black background.
  2. Rotates, scales and translates the white square.
  3. Views the three transformations of the square in steps.
  4. Creates the square using a display list to increase efficiency.
  5. Uses the square display list to create a 3-dimensional cube.
  6. Rotates the cube in x, y and z directions according to user input.
  7. Rotates a cylinder instead of a cube.
Both OpenGL and GLUT have online manuals. For detailed information about OpenGL and GLU functions see the online man pages.

OpenGL Overview

OpenGL(Open Graphics Library) is the interface between a graphics program and graphics hardware. OpenGL supports features like 3-dimensions, lighting, anti-aliasing, shadows, textures, depth effects, etc.

1. Draw a white square on a black background (Source Code)

screen shot

Note: you will be using the Mesa graphics library in the computer lab MC103N. You can use this Makefile to compile 1.c. This Makefile can be easily modified to compile the other sample programs.

Includes

gl.h is the interface for OpenGL.
#include <GL/gl.h>
glu.h is the interface for GLU (OpenGL Graphics System Utility Library), a utilities library that compliments OpenGL by providing support for mipmapping, matrix manipulation, polygon tessellation, quadrics (e.g. circles, spheres, cylinders), NURBS, and error handling.
#include <GL/glu.h>
glut.h is the interface for GLUT (OpenGL Utility Toolkit), another utilities library that provides a platform-independent user interface.
#include <GL/glut.h>
stdio.h and stdlib.h are standard C header files.
#include <stdio.h> 
#include <stdlib.h> 
math.h provides support for mathematics. For example, it provides trigonometric functions like sin() and defines PI. In this tutorial we don't actually use anything from this library; however, we include here it because you'll need it for most useful graphics programs.
#include <math.h>

Defines

Initially our window will be 480x480.
#define WIDTH  480
#define HEIGHT 480
The scene background colour, specified in RGBA component values, will be black. The RGBA component values range between 0 and 1. The alpha value determines how opaque the colour is. In this case, it is completely opaque (0=completely transparent...1=completely opaque). Alpha values allow us to render objects composed of transparent materials like glass.
#define RED   0
#define GREEN 0
#define BLUE  0
#define ALPHA 1
The ascii code for the ESC key is 27. Pressing this key will terminate the program.
#define KEY_ESC 27

Functions

main

Every C program contains a main() function, so here is one found in most OpenGL programs. The main function is responsible for:
  1. initializing OpenGL, GLU and GLUT;
  2. creating a window using GLUT;
  3. initializing any data structures needed to draw the scene;
  4. telling GLUT which functions will handle window events;
  5. telling GLUT to start waiting for events.
int main(int argc, char **argv) 
{  
It is possible to pass command line options to the GLUT library. For example, the -geometry option is used to specify the initial window size. This function also initializes the GLUT library so it must be called before any other GLUT function.
  glutInit(&argc, argv);
Our display colours will be specified as red, green and blue component magnitudes (GLUT_RGBA).
  glutInitDisplayMode(GLUT_RGBA );
Define the initial height and width of any window created.
  glutInitWindowSize(WIDTH, HEIGHT);
Initially the window will be placed at the top, left corner of the screen.
  glutInitWindowPosition(0, 0);
Finally, create a window with the caption Square.
  glutCreateWindow("Square");
initGL is a user-defined function that initializes OpenGL typically describing the scene background and lighting as well as the material and textures of scene objects.
  initGL();  
init_scene is also user-defined. In it we prepare scene objects for rendering.
  init_scene();
Whenever the window needs to be redrawn, the system will call window_display (user-defined).
  glutDisplayFunc(&window_display);
Whenever the window is resized, the system will call window_reshape (user-defined).
  glutReshapeFunc(&window_reshape);
Whenever the user presses a key, the system will call window_key (user-defined) to redraw.
  glutKeyboardFunc(&window_key);
Finally, start the window event loop, waiting for key presses, window resizing, and window redrawing.
  glutMainLoop();  
  return 1;
}

initGL

For now, the only initializing we do is to tell OpenGL that we want the background colour to have the RGBA value (RED, BLUE, GREEN, ALPHA). The function glClearColor() tells OpenGL what colour to put in the colour buffer when glClear() is called.
GLvoid initGL()   
{
  glClearColor(RED, GREEN, BLUE, ALPHA);   
}

init_scene

At this point, our program is simple enough that we don't need init_scene() to do anything.
void init_scene()
{
}

window_reshape

Because of the call in main() to glutReshapeFunc() with window_reshape as the parameter, GLUT will call this function every time the window is resized.

Before a point reaches the screen, it passes through three transformations: modelview, projection and viewport. The modelview transformation transforms world coordinate points to view reference coordinates; in other words, it determines the location of scene objects in relationship to the location of the view reference point. The projection transformation projects the resulting view reference coordinates inside a view volume onto a plane. Then, finally the viewport transformation transforms projected points to screen coordinates.

We may want to modify our viewport and projection transformations in reponse to a window resize. For example, we might want the entire scene to fit inside the window at all times. As a result, we may need to change the size of the viewport based on the new size of the window. However, if our changes to the viewport dimensions also changes the viewport width-height ratio we will find that the width-height ratio of the objects drawn in the viewport will also change accordingly. For example, increasing just the width of the viewport will transform squares into rectangles. To compensate, we will need to change the width-height ratio of the projection view volume to match that of the viewport.

GLvoid window_reshape(GLsizei width, GLsizei height)
{
In this program, the scene fills the entire window. A call to glViewport(x,y,h,w) defines the viewport to be the rectangle with width w and height h whose bottom left corner is (x,y) in the window.
  glViewport(0, 0, width, height);
However, we also want to preserve the width-height ratio of the scene so we modify the projection matrix to represent a new view volume. We are using a perspective projection. Unlike the viewport transformation, before making any changes to the perspective and modelview transformations, we must tell OpenGL which projection we are modifying.
  glMatrixMode(GL_PROJECTION);
We reset the projection transformation by setting it to the identity matrix.
  glLoadIdentity();
Then, we call gluPerspective(fovy, ratio, near, far) to give us a perspective projection in which the field of view angle is fovy in the x-z plane, the width-height ratio is ratio, the distance of the closest clipping plane to the center of projection (COP) is equal to near, and the distance of the far clipping plane to the COP is far.
  gluPerspective(45, (GLdouble)width/(GLdouble)height, 1, 10); 
Finally, we tell OpenGL that subsequent transformations will apply to the modelview transformation. In other words, unless otherwise specified, all transformations in this program apply to the modelview.
  glMatrixMode(GL_MODELVIEW);
}

window_display

Because of the call in main() to glutDisplayFunc() with window_display as the parameter, GLUT will call this function every time the scene needs to be redrawn (e.g. after the perspective projection transformation has been modified).
GLvoid window_display()
{
First we erase what has been drawn on the screen by clearing the colour buffer. Remember that our call to glClearColor() in initGL() earlier told OpenGL to clear to the colour defined by (RED, GREEN, BLUE).
  glClear(GL_COLOR_BUFFER_BIT);
Then we reset the modelview transformation by setting it to the identity matrix. Theoretically, it is possible and even efficient to reuse the modelview transformation used in the previous call to window_display(). This may not, however, be advisable because transformations require floating point operations that have round-off errors. If the modelview matrix is reused then these errors may compound causing the scene to be rendered incorrectly.
  glLoadIdentity();
Next we define the center of projection (COP), the view reference point (VRP) and the view up vector (VUP) by calling gluLookAt(copx, copy, copz, vrpx, vrpy, vrpz, vupx, vupy, vupz).
  gluLookAt(0, 0, 5, 0, 0, 0, 0, 1, 0); 
We render the scene. In this case, render_scene() simply draws a white rectangle.
  render_scene();
Finally, we tell OpenGL that the scene just rendered can be drawn on the screen.
  glFlush();
}

render_scene

The purpose of render_scene() is simply to draw a white rectangle.
void render_scene()
{
Set the current colour to white.
  glColor3f(1, 1, 1);
Tell OpenGL that we are going to define the vertices of a polygon. We define the points in the order we would encounter them if we traversed the outside edge of the polygon, completing the traversal at the point we started from.
  glBegin(GL_POLYGON);
Define the four corners of the square in the z=0 plane. It might seem like a trivial point, but we should make sure that we can see the square from the perspective transformation defined by gluPerspective() (in window_resize()) and the view transformation defined by gluLookAt() (in window_display()).
  glVertex3f(0, 0, 0);
  glVertex3f(1, 0, 0);
  glVertex3f(1, 1, 0);
  glVertex3f(0, 1, 0);
Tell OpenGL that we are finished defining the polygon.
  glEnd();
}

window_key

Because of the call in main() to glutKeyboardFunc() with window_key as the parameter, GLUT will call this function every time a key is pressed. If the user presses the ESC key, exit the program.
GLvoid window_key(unsigned char key, int x, int y) 
{  
  switch (key) {    
  case KEY_ESC:  
    exit(1);                      
    break; 
    
  default:
    printf ("Pressing %d doesn't do anything.\n", key);
    break;
  }    
}

2. Rotates, scales and translates the white square (Source Code)

The only change from the first program is that we translate the square so that it is centered at (0,0,0), then we scale it by 0.5 along the x-axis and then rotate it 45 degrees about the z-axis.

screen shot

This accomplished by the following composition: Rz(45) * S(0.5, 1, 1) * T(-0.5, -0.5, 0). These translate into three OpenGL function calls. glRotatef(angle, x, y, z) is a rotation transformation about the vector (x,y,z).

Note the order of the calls relative to the composition order...it's very important.

void render_scene()
{
  glRotatef(45, 0, 0, 1);
  glScalef(0.5, 1, 1);
  glTranslatef(-0.5, -0.5, 0);
Following these transformations, we simply draw the square exactly as we did in the previous program.
  glColor3f(1, 1, 1);
  glBegin(GL_POLYGON);
  glVertex3f(0, 0, 0);
  glVertex3f(1, 0, 0);
  glVertex3f(1, 1, 0);
  glVertex3f(0, 1, 0);
  glEnd();
}
You may or may not have noticed the 3f at the end of glVertex3f and the f at the end of glRotatef. This is a way of achieving function overloading in C. The f stands for float (or more technically GLfloat) meaning the arguments to the functions must be 32-bit floating point variables or constants. The function glRotatei takes integer (or more technically GLint) arguments. The 3 means that the function takes three such arguments. Thus, glVertex2f is used to define 2-dimensional points.

3. Views the three transformations of the square in steps (Source Code)

In this third program, we demonstrate each of the three transformations used in the previous program.

At first there is no transformation. It is the same as the screen shot for the first program.

screen shot

The red square is rotated by 45 degrees about the z-axis.

screen shot

The green square is scaled by 50% along the x-axis and then rotated by 45 degrees.

screen shot

The blue square is translated so that it is centered at (0,0,0) before being scaled and rotated.

screen shot

We use the space bar to step through the transformations.

#define KEY_SPC 32
We use the global variable squares to keep track of how many times the user has pressed the space bar, implying the number of squares we've drawn (one for each transformation).
int squares = 0; 
We add code to window_key() to increment squares every time the user presses the space bar.
GLvoid window_key(unsigned char key, int x, int y) 
{  
  switch (key) {    
  case KEY_ESC:  
    exit(1);                      
    break; 
We increment squares modulo 4 because there's only three transformations.
  case KEY_SPC:  
    squares = (squares+1)%4;                    
The user has pressed the space bar so the window needs to be redrawn. glutPostRedisplay() tells GLUT that the window needs to be redrawn as soon as possible.
    glutPostRedisplay();
    break; 
    
  default:
    printf ("Pressing %d doesn't do anything.\n", key);
    break;
  }    
}
We modify render_scene() to draw a square before and after each transformation. Because we draw the square so many times, we moved the code for drawing a square to the function square() and then just call the function each time. We also change the current colour before drawing each square in order to differentiate between the squares being drawn. The number of squares drawn depends on the number of times the user has pressed the space bar.
void render_scene()
{
  glColor3f(1, 1, 1);
  square();

  if (squares >= 1) {
    glRotatef(45, 0, 0, 1);
    glColor3f(1, 0, 0);
    square();
  }

  if (squares >= 2) {
    glScalef(0.5, 1, 1);
    glColor3f(0, 1, 0);
    square();
  }

  if (squares >= 3) {
    glTranslatef(-0.5, -0.5, 0);
    glColor3f(0, 0, 1);
    square();
  }
}
void square()
{
  glBegin(GL_POLYGON);
  glVertex3f(0, 0, 0);
  glVertex3f(1, 0, 0);
  glVertex3f(1, 1, 0);
  glVertex3f(0, 1, 0);
  glEnd();
}

4. Creates the square using a display list to increase efficiency (Source Code)

One way to increase the efficiency of OpenGL is to precompile higher level objects into lists and then when the object is to be drawn, to tell which list to draw. In the third program, we called a function to draw each instance of the square. This time, we'll precompile the square into a list and then just refer to the list each time we draw the square. The list identifier, a global unsigned integer:
GLuint square;
At the beginning of the program, in init_scene(), we create and compile the list of OpenGL commands needed to draw a square.
void init_scene()
{
  square = glGenLists(1);
  glNewList(square, GL_COMPILE);
  glBegin(GL_POLYGON);
  glVertex3f(0, 0, 0);
  glVertex3f(1, 0, 0);
  glVertex3f(1, 1, 0);
  glVertex3f(0, 1, 0);
  glEnd();  
  glEndList();
}
Then, in render_scene() we replace each square() call with a call glCallList(square):
    glCallList(square);

5. Uses the square display list to create a 3-dimensional cube (Source Code)

screen shot
To create a 3-dimensional cube, we draw a square using the code from the previous program six times (here we won't call it a square, we'll call it a face instead). There are more efficient ways to draw a cube, however, we do it this way to further illustrate display lists and transformations. The global variables identifying the two face list and the cube list.
GLuint face;
GLuint cube;
To handle hidden surface removal because we're in 3-dimensions now, we tell OpenGL that we want to use a depth buffer (the z-buffer algorithm). More specifically, we tell OpenGL what value to put in the depth buffer when we clear it, how to do the depth test and finally to enable depth testing. GL_LESS means that objects behind other objects closer to the center of projection are not drawn--the standard test.
GLvoid initGL()   
{
  glClearColor(RED, GREEN, BLUE, ALPHA);   
  glClearDepth(1.0);              
  glDepthFunc(GL_LESS);                       
  glEnable(GL_DEPTH_TEST);                    
}
We also need to tell GLUT to enable depth buffers in the call to glutInitDisplay() in main().
  glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH);
We modify init_scene() to create the face and cube display lists. Each face of the cube will drawn with a different colour.
void init_scene()
{
  face = glGenLists(2);
  cube = face+1;
The face list:
  glNewList(face, GL_COMPILE);
    glBegin(GL_POLYGON);
      glVertex3f(0, 0, 0);
      glVertex3f(1, 0, 0);
      glVertex3f(1, 1, 0);
      glVertex3f(0, 1, 0);  
    glEnd();  
  glEndList();
The cube list created by drawing a face six times. There are more efficient ways to draw a cube, however, we do it this way to further illustrate display lists and transformations.
  glNewList(cube, GL_COMPILE);
The cube is drawn so that it's center is (0,0,0). Without this translation, the front, bottom, left corner would be at (0,0,0).
    glTranslatef(-0.5, -0.5, 0.5);
The front face is red.
  
    glColor3f(1, 0, 0);
    glCallList(face);
The rear face is yellow (red+green). Originally the corners of the face are (0,0,0), (1,0,0), (1,1,0) and (0,1,0). This is the same as the front face so we must translate it 1 unit in the negative z direction so that its corners are at (0,0,-1), (1,0,-1), (1,1,-1) and (0,1,-1).

This translation only applies to the rear face so we don't want it to affect the other faces. Consequently, we save the current modelview matrix before performing the translation using glPushMatrix() and then restore this matrix after the rear face is is drawn using glPopMatrix().

  
    glColor3f(1, 1, 0);
    glPushMatrix();
    glTranslatef(0, 0, -1);
    glCallList(face);
    glPopMatrix();
The left face is green.
  
    glColor3f(0, 1, 0);
    glPushMatrix();
    glRotatef(90, 0, 1, 0);
    glCallList(face);
    glPopMatrix();
The right face is cyan (green+blue).
  
    glColor3f(0, 1, 1);
    glPushMatrix();
    glTranslatef(1, 0, 0);
    glRotatef(90, 0, 1, 0);
    glCallList(face);
    glPopMatrix();
The bottom face is blue.
  
    glColor3f(0, 0, 1);
    glPushMatrix();
    glRotatef(-90, 1, 0, 0);
    glCallList(face);
    glPopMatrix();
The top face is magenta (red+blue).
  
    glColor3f(1, 0, 1);
    glPushMatrix();
    glTranslatef(0, 1, 0);
    glRotatef(-90, 1, 0, 0);
    glCallList(face);
    glPopMatrix();

  glEndList();
}
We modify the call to glClear() in window_display() to include the request to clear the depth buffer in addition to the colour buffer before redrawing the scene.
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);        
We modify render_scene() to draw the cube after rotated 20 degrees about the x and y-axes.
void render_scene()
{
  glRotatef(20, 0, 1, 0);
  glRotatef(20, 1, 0, 0);
  glCallList(cube);
}
Finally, we remove all code related to pressing the space bar.

6. Rotates the cube in x, y and z directions according to user input (Source Code)

Start by rotating about the x-axis.

screen shot

screen shot

screen shot
The up arrow key increases the rate of rotation; the down arrow key decreases the rate. The 'x' key toggles rotating about the x-axis; the 'y' key toggles rotating about the y-axis; the 'z' key toggles rotating about the z-axis.
#define KEY_ESC 27
#define KEY_UP 101
#define KEY_DOWN 103
#define KEY_X 120
#define KEY_Y 121
#define KEY_Z 122
DELTA is the speed-up or slow-down of the rate of rotation when an arrow key is pressed. x stores the total rotation about the x-axis; similarly for y and z. rotateX equals 1 if the cube is to rotate about the x-axis and 0 otherwise; similarly for rotateY and rotateZ. speed is the current rate of rotation.
#define DELTA 5
int x = 0;
int rotateX = 0;
int y = 0;
int rotateY = 0;
int z = 0;
int rotateZ = 0;
int speed = 0;
For the first time we're creating a moving scene. To make the movements smooth, we use double-buffering. We first tell GLUT to enable the double buffer in main().
  glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH);
The make the objects move, we add an idle function to change rotation values of cube when the system has nothing else to do. The arrow keys are special so also add a function to handle these keys just like window_key() handles regular key presses.
  glutIdleFunc(&window_idle);
  glutSpecialFunc(&window_special_key);
At the end of window_display() we call glutSwapBuffers() instead of glFlush() to make the drawing buffer the screen buffer and the screen buffer the drawing buffer.
  glutSwapBuffers();
The key-press handling functions:
GLvoid window_key(unsigned char key, int x, int y) 
{
  switch (key) {    
  case KEY_ESC:  
    exit(1);                      
    break; 
  case KEY_X:
    rotateX = !rotateX;
    glutPostRedisplay();
    break;
  case KEY_Y:
    rotateY = !rotateY;
    glutPostRedisplay();
    break;
  case KEY_Z:
    rotateZ = !rotateZ;
    glutPostRedisplay();
    break;
  default:
    printf ("Pressing %d doesn't do anything.\n", key);
    break;
  }    
}

GLvoid window_special_key(int key, int x, int y) 
{    
  switch (key) {    
  case KEY_UP: 
    speed = (speed + DELTA + 360) % 360;
    glutPostRedisplay();
    break;
    
  case KEY_DOWN: 
    speed = (speed - DELTA + 360) % 360;
    glutPostRedisplay();
    break;
    
  default:
    printf ("Pressing %d doesn't do anything.\n", key);
    break;
  }
}
The idle function updates the total rotations about the three axes and then if any changes are made we ask GLUT to redraw the window (call window_display()) again as soon as possible.
GLvoid window_idle()
{
  if (rotateX) x = (x + speed + 360) % 360;
  if (rotateY) y = (y + speed + 360) % 360;
  if (rotateZ) z = (z + speed + 360) % 360;
  if (speed > 0 && (rotateX || rotateY || rotateZ)) 
    glutPostRedisplay();
}
We modify render_scene() to perform the rotations about the axes before drawing the cube.
void render_scene()
{
  glRotatef(x, 1, 0, 0);
  glRotatef(y, 0, 1, 0);
  glRotatef(z, 0, 0, 1);
  glCallList(cube);
}

7. Rotates a cylinder instead of a cube (Source Code)

Instead of a cube, we rotate a cylinder. Actually we rotate a cylinder capped at both ends by disks. Both cylinders and disks are described by quadratic functions so we use quadrics from the GLU library. Start by rotating about the x-axis.

screen shot

screen shot
We modify init_scene() to create the display list for drawing our object.
void init_scene()
{
  GLUquadricObj *quadric;

  cylinder = glGenLists(1);

  glNewList(cylinder, GL_COMPILE);
    quadric = gluNewQuadric();
    gluQuadricDrawStyle(quadric, GLU_FILL);

    glColor3f(1, 0, 0);
gluCylinder(quadric, base, top, height, longitude, latitude) draws a cylinder whose base is centered at (0,0,0) and whose top is centered at (0,0,height). The cylinder is drawn using convex polygons as faces. The arguments longitude and latitude determine how closely the drawn cylinder approximates a real cylinder.

Consider a globe with lines of latitude and longitude drawn on its surface. Notice that these lines subdivide the globe's surface into curved squares and triangles. If we replace these squares and triangles with flat squares and triangles we have just created an object out of convex polygons whose shape is similar to that of a globe. Notice that the more lines of latitude and longitude we have, the more accurately the object resembles a globe.

To see how this applies to cylinders, replace GLU_FILL above with GLU_LINE to draw the cylinder as a wire frame and then try different values for longitude and latitude.

    gluCylinder(quadric, 1, 0.75, 1, 15, 15); 

    glColor3f(0, 1, 0);
gluDisk(quadric, inner, outer, slices, loops) draws a disk (a circle with a hole in it) centered at (0,0,0) whose radius is outer and whose hole has radius inner. The argument slices and loops determine how closely what is drawn resembles a real disk.
    gluDisk(quadric, 0.5, 1, 15, 15); 

    glColor3f(0, 0, 1);
    glTranslatef(0, 0, 1);
    gluDisk(quadric, 0.5, 0.75, 15, 15); 

    gluDeleteQuadric(quadric);  
  glEndList();
}
Please send comments, questions and corrections to Matthew Suderman.