Lab 1: Introduction to OpenGL


WARNING: OLD LAB. THIS IS WRITTEN FOR OpenGL 2, AND HAS BEEN REPLACED BY MODERNIZED MATERIAL.

Goal: In this lab, you will get acquainted with how OpenGL is designed. At the end of the lab, you should be able to display a textured and lit 3D object.

If you run into problems, you can either look in the textbook, or visit http://www.opengl.org. There you will, among many other things, find the entire OpenGL Programming Guide in on-line version.

Note that you should write down answers to all questions before you get examined!

 

Last minute note: We have revised some of the lab files for 2011. This will not affect your lab work much, but there are a few noteworthy differences:


1) Setup and getting acquainted with the lab shell

Download the lab package from lab2011-1alt.tar.gz and unpack it at a suitable location. (Old version: lab2008-1.tar.gz )

Download the texture package from lab2011-textures.tar.gz and unpack it at a suitable location. (Old version: lab2008-textures.tar.gz )

Download the cubemap package from lab2011-cubemaps.tar.gz and unpack it at a suitable location. (Old version: lab2008-cubemaps.tar.gz )

There are several files included in the lab environment:

makefile - contains rules for how the executable should be built; read by make.
lab1-1.c - the actual lab code; this is where the main program resides.
helpers.c - a set of helper functions for measuring time, loading textures etc.
LoadTGA2.c - code for loading in a TGA image from disk; written by Ingemar Ragnemalm.
zpr.c - object-control logic; originally written by Nigel Stewart.

You will be using makefile, lab1-1.c and helpers.h directly.

Compile the test program by entering the lab1 directory and performing make on the command line. This should produce a new executable file called lab1-1.

Run lab1-1. It should show a white square against a dark red background.

Open lab1-1.c and have a look inside it. There are two functions of interest to you, init() and display(). init() is called once during program startup and display() is called every time it is time to render a new frame of graphics.

Currently, display() does three things:
* Clearing of screen and Z-buffer
* Rendering of a quad using OpenGL Immediate Mode rendering commands
* Swapping front- and backbuffer

Most OpenGL settings are left unchanged. The default state setup is as follows:
* Lighting: Lighting is disabled.
* Transformation: The modelview transform defaults to identity; that is, vertex positions will not be changed during the transformation step.
* Projection: An ortographic projection is used.
* Backface culling: Culling is disabled, so polygons will be visible from both sides.

The polygon drawing commands deserve some more in-depth explanation.
glBegin(GL_POLYGON) is where OpenGL is told that a triangle fan will be submitted using the Immediate Mode functions.
Each glColor3f() sets the "active" color; this color will be used for all subsequently submitted vertices, until another glColor3f() is issued.
Each glVertex3f() submits a new vertex with the currently active color, and the specified vertex XYZ-position. A well-formed polygon is described by submitting three or more vertices.
Issue glEnd() to close the polygon, and start rendering.

Try changing the inputs to the glColor3f() and glVertex3f() commands. Give each vertex in the polygon a unique color. Change the shape and number of vertices in the polygon.

Questions:


2) Animation

Goal: To rotate your polygon around an arbitrary axis.

Copy lab1-1.c to lab1-2.c and add a new entry to the makefile. Make this section's changes to lab1-2.c.

Add the following code just after the glClear() command:

  glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
 
glRotatef(getElapsedTime() * 360, 0, 1, 0);

glMatrixMode(GL_MODELVIEW) selects the modelview matrix. All subsequent matrix commands will be applied to the modelview matrix.
glLoadIdentity() sets the currently selected matrix to identity.
glRotatef(getElapsedTime() * 360, 0, 1, 0) appends a rotation to the current matrix. The rotation is specified as an angle, which is varying, and a rotation axis, which is constant (0, 1, 0).

Questions:


3) Object control, depth, perspective and Z-buffering

Goal: To create a world with several polygons in it. To add a good sense of depth. To add manual rotation/translation control of the object.

Copy lab1-2.c to lab1-3.c and add a new entry to the makefile. Make this section's changes to lab1-3.c.

Remove the glRotatef() command. In its place, add the following:

  glLoadMatrixd(getObjectMatrix());

getObjectMatrix() retrieves an object transform matrix from the helper library. You use the mouse to control the orientation of the object.
glLoadMatrixd(getObjectMatrix()) overwrites the currently selected OpenGL matrix with a new matrix.

Test the above change. Can you rotate the object by holding down the left mouse button and moving the mouse?

Add a few extra polygons to your world. Place them at different depths/orientations. Try running your program with the new polygons included. Is it difficult to navigate in the 3D space? That is because the orthogonal projection gives you no indication of depth. Also, everything outside the (-1, +1) Z range is currently being culled away.

To add perspective projection, add these lines before the glMatrixMode(GL_MODELVIEW) command:

  glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glFrustum(-0.1, 0.1, -0.1, 0.1, 0.1, 100);

glMatrixMode(GL_PROJECTION) switches the active OpenGL matrix to the projection matrix. This matrix describes how to go from 3D coordinates to 2D (screen) coordinates.
glFrustum(-0.1, 0.1, -0.1, 0.1, 0.1, 100); constructs a perspective projection matrix, with a 90-degree view range vertically, 1:1 ratio between horizontal/vertical view ranges, and allows objects which are between 0.01 and 100 units away along the Z axis to be visible.
gluPerspective(90, 1, 0.01, 100) is an alternative to glFrustum, working with an angle instead of explicit frustum dimensions.

Run and see the result. Items farther away should now be rendered a bit smaller. If you are too close to the objects, you can move them by holding down the right mouse button and moving the mouse up/down.

Sometimes, when polygons are overlapping, a polygon far away will occlude a more nearby polygon. This is because there is currently no hidden surface removal -- OpenGL is happily painting polygons into the framebuffer with no regard to the depth information. Enable Z-buffering by adding the following command after the modelview matrix setup:

  glEnable(GL_DEPTH_TEST);

With perspective and depth test enabled, you now have a framework which allows you to show 3D worlds in a geometrically correct manner.

Questions:

 


4) Building a cube

Goal: To build a cube using OpenGL Immediate Mode commands. To familiarize with backface culling.

Copy lab1-3.c to lab1-4.c and add a new entry to the makefile. Make this section's changes to lab1-4.c.

Remove the old polygon rendering commands from lab1-4.c. Build a cube by creating six new polygons. If you give the cube a side length of approx. 0.2 units, then it will be suitably large to show on-screen without moving the object back/forth. Also, enable backface culling by inserting the following before the rendering commands:

  glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);

glEnable(GL_CULL_FACE) enables backface culling.
glCullFace(GL_BACK) specifies that triangles which have their back turned toward the camera should be culled away.

It is easier to spot errors if you give each vertex in each polygon a unique colour.

Backface culling may seem like an unnecessary obstacle now, but it doubles rendering performance, it is necessary when doing some kinds of polygon-based special effects, and it simplifies greatly when performing lighting computations. Because of this, a lot of hand-made 3D models are built with backface-culling in mind.

Questions:


5) Lighting

Goal: To add vertex-based dynamic lighting to the cube.

Copy lab1-4.c to lab1-5.c and add a new entry to the makefile. Make this section's changes to lab1-5.c.

OpenGL can perform per-vertex lighting computations. For this to work, every vertex must have a vertex normal specified. The glNormal3f() command specifies the vertex normal to be used by subsequent glVertex3f() commands, in much the same way that glColor3f() works.

Add vertex normals to your cube.

Before lighting can be enabled, the lightsources need to be specified. Insert the following before glMatrixMode(GL_PROJECTION):

  glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
GLfloat light_position[] = { 0.0, 0.0, 0.0, 1.0 };
glLightfv(GL_LIGHT0, GL_POSITION, light_position);

The above code places OpenGL light number 0 at the origin.
light_position specifies the XYZ position of the light in the first three elements, and the fourth element indicates that the light is a positional light. Setting the fourth element to 0.0 indicates that it is a directional light, and the first three elements are then a direction vector for the light.
glLightfv(GL_LIGHT0, GL_POSITION, light_position) sets the position of the light, relative to the camera location. The current modelview matrix is also applied to the light position when it is being set; therefore, it needs to be reset to identity before we set the light position.

OpenGL lighting is enabled through a few extra function calls. Insert the following after your light setup code:

  glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
glEnable(GL_NORMALIZE);

glEnable(GL_LIGHTING) enables OpenGL lighting. When lighting is enabled, any vertex colors you specify are ignored; instead, OpenGL calculates new vertex colors dynamically.
glEnable(GL_LIGHT0) enables lightsource 0. There are 8 lightsources available.
glEnable(GL_NORMALIZE) makes OpenGL re-normalize all vertex normals after modelview transformation. Without normalization, all vertex normals must be of unit length and the modelview matrix must not contain any scaling, or else the lighting calculations will yield incorrect results.

The lighting computations compute new vertex colors for the polygons. A set of material attributes are used during the computations. Insert this after the light setup code:

  GLfloat mat_shininess[] = { 50.0 };
GLfloat mat_diffuseColor[] = { 1.0, 1.0, 1.0, 1.0 };
GLfloat mat_specularColor[] = { 1.0, 1.0, 1.0, 1.0 };
 
glMaterialfv(GL_FRONT, GL_SHININESS, mat_shininess);
glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuseColor);
glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specularColor);

The above code sets scale factors and color values used in standard diffuse/specular lighting calculations. We will return to these lighting parameters when the 3D model under test is more detailed than a cube.

Questions:


6) Texture mapping

Goal: To apply a texture map to the cube.

Copy lab1-5.c to lab1-6.c and add a new entry to the makefile. Make this section's changes to lab1-6.c.

Adding texture mapping requires three changes. First, you need to load a texture. Do that by creating a global variable of type GLuint named textureId, and then adding the following line to your init() function:

  textureId = loadTexture("../textures/BrickModernLarge0119_S.jpg");

The loadTexture() function resides in the helper library. It loads the specified image from disk, creates a new OpenGL texture object, loads the image data into the texture object, and then makes some basic configurations to the texture object. (You may need to change the path to where you have unpacked the texture package.)

Secondly, you need to enable texturing. Inser the following code just before your polygon rendering commands:

  glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, textureId);

The glEnable(GL_TEXTURE_2D) enables texturing.
The glBindTexture(GL_TEXTURE_2D, textureId) selects the texture we recently created as active texture.

Thirdly, you need to specify texture coordinates in every vertex. Use glTexCoord2f() to give texture S and T coordinates to each vertex, before each corresponding glVertex() command. Texture itself resides within the 0.0 to 1.0 range along S and T axes. Try and see what happens if you specify S and T values outside this range, e.g. 0 to 4.

When the texture has been selected through the glBindTexture() call, some texture parameters can be controlled. See the implementation of loadTexture() in helpers.c. The following four lines can be found there:

  glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

GL_TEXTURE_WRAP_S and GL_TEXTURE_WRAP_T define how the texture behaves outside of the [0.0, 1.0] coordinate range. GL_REPEAT indicates that the texture should repeat infinitely along S and T axes, whereas GL_CLAMP_TO_EDGE indicates that just the texture's set of contour pixels should repeat.
GL_TEXTURE_MAG_FILTER and GL_TEXTURE_MIN_FILTER define what kind of texture interpolation should be performed when the texture is being magnified/minified, respectively. GL_LINEAR enables bilinear filtering; GL_NEAREST applies nearest-neighbor sampling.

Questions:


7) Creating a "skybox"

Goal: To create a panoramic view around the camera.

Copy lab1-6.c to lab1-7.c and add a new entry to the makefile. Make this section's changes to lab1-7.c.

One effective way of creating a surrounding to a 3D scene, is to take a set of photos in all six major directions (up, down, front, back, left, right), and put these photos on a large cube which encloses the entire scene. This technique is sometimes referred to as "panoramic images".

Flip all the faces in your cube, so that they point inward.

Make your cube much larger. A side length of 100 units is suitable.

Disable lighting, and ensure that all cube faces are drawn with glColor3f(1, 1, 1) set.

Choose one of the cubemaps from the cubemap package. Load all six faces in the init() function.

When rendering the cube, bind a different texture before each glBegin() call. There are some helper images in the cubemap package that show which image should go on which side of the cube.

Select texture coordinates that makes the textures fit each other at the borders. It will be easiest for you if you handle one side at a time.

Questions:


That concludes lab 1. Good work! In the next lab, you will experiment with more complex objects, and scenes containing multiple objects.