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:
- Draws a white square on a black background.
- Rotates, scales and translates the white square.
- Views the three transformations of the square in steps.
- Creates the square using a display list to increase efficiency.
- Uses the square display list to create a 3-dimensional cube.
- Rotates the cube in x, y and z directions according to user input.
- 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.
- It is streamlined.
In other words, it provides low-level functionality.
For example, all objects are built from points, lines and convex polygons.
Higher level objects like cubes are implemented as six four-sided polygons.
- It is system-independent.
It does not assume anything about hardware or operating system
and is only concerned with efficiently rendering mathematically
described scenes.
As a result, it does not provide any windowing capabilities.
- It is a state machine.
At any moment during the execution of a program
there is a current model transformation,
a current colour, a current light type and position, etc.
Consequently, when an object is drawn,
its colour is determined by the current colour and light
and its final projection onto the screen is determined by the
current transformation.
- It is a rendering pipeline.
The rendering pipeline consists of the following steps:
- Defines objects mathematically.
- Arranges objects in space relative to a viewpoint.
- Calculates the colour of the objects.
- Rasterizes the objects.
OpenGL supports features like 3-dimensions, lighting, anti-aliasing,
shadows, textures, depth effects, etc.
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.
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.
glut.h is the interface for
GLUT (OpenGL Utility Toolkit),
another utilities library that provides a platform-independent user interface.
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.
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.
Functions
main
Every C program contains a
main() function, so here is one
found in most OpenGL programs.
The main function is responsible for:
- initializing OpenGL, GLU and GLUT;
- creating a window using GLUT;
- initializing any data structures needed to draw the scene;
- telling GLUT which functions will handle window events;
- 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.
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.
init_scene is also user-defined. In it we prepare scene
objects for rendering.
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.
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.
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.
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.
Finally, we tell OpenGL that the scene just rendered can be
drawn on the screen.
render_scene
The purpose of
render_scene() is simply
to draw a white rectangle.
Set the current colour to white.
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.
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.
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;
}
}
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.
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.
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.
The red square is rotated by 45 degrees about the z-axis.
The green square is scaled by 50% along the x-axis and then rotated by 45 degrees.
The blue square is translated so that it is centered at (0,0,0)
before being scaled and rotated.
We use the space bar to step through the transformations.
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).
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();
}
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:
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):
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.
Start by rotating about the x-axis.
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.
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);
}
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.
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.