Computer Graphics Hardware and Algorithms 215

Laboratory 3

During this lab we will be looking at viewing transformations, and lighting, with a collection of other stuff thrown in.


Transformations

In computer graphics we use two sets of transformations to get an object we draw to its position on the screen.

The first of these transformations is the modelview transformation. If we have a scene which contains say 4 spheres then we don't want to have to write different sets of code to draw each of our four spheres. Instead we want to write one bit of code which draws a single sphere (for example, a sphere centred on the origin with radius 1), and then use transformations such as scaling, rotation and translation to move the sphere where we want it.

This transformation can be thought of as putting our object into its world co-ordinates. We were using this type of transformation at the end of last week to make our pyramid orbit around the y-axis.

Viewing Transformations

The second type of transformation is the projection, or viewing transformation. This is the transformation of our objects in world co-ordinates to screen co-ordinates. ie where they appear relative to our viewpoint. These type of transformations also include scaling, rotation and translation, as well as two new ones for orthogonal and perspective viewpoints.

We can modify the projection matrix in the same way that we change the modelview matrix. We first issue a glMatrixMode(GL_PROJECTION); command to say that the following transformations are to affect the projection matrix. glLoadIdentity(); is used to remove whatever is currently in the projection matrix, and replace it with the identity matrix.

Now we can use the glRotate* and glTranslate* to change the position of the viewpoint. As with the modelview matrix we can also specify our own matrices to multiply against the projection matrix using glMultMatrix().

Modify the program exp3a.c so that the eye point revolves around the sphere, whilst keeping the position of the light source and sphere constant.

Orthogonal Projection and Perspective

The projection matrix allows us to change more than just the position and orientation of the viewpoint. It also allows us to modify the type of transformation used to project the objects onto the viewing plane. To simple routines which do this are glOrtho, and glFrustum.

Orthogonal Projection

The default viewing projection is an orthogonal projection (also known as parallel projection). With this type of projection each point is projected onto the viewing plane by a ray which is perpendicular to the viewing plane. This means we get a rectangular prism shaped viewing volume. Everything outside of that volume will not be projected onto the viewing plane. The figure below shows how the orthogonal projection viewing volume works:

This image has been lifted straight out of the OpenGL Programming Guide

We can create an orthogonal projection using the following code:

glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(-1, 1, -1, 1, -1, 1);
This code produces a projection which is the same as the default viewing projection. The numbers represent a view where the bottom left of the screen has the co-ordinates (-1,-1) and the top right has the co-ordinates (1,1).

The last two numbers in the glOrtho command represent the z-near and z-far clipping planes. We normally don't want things up close the viewpoint because they will tend to obscure everything else (its worse in perspective), so we use a near clipping plane to prevent this. We usually don't want to display things which are a long distance from the viewing plane either, so we use a far clipping plane.

Perspective Projection

The command glFrustum() allows us to create a viewing projection which emulates perspective projection, ie. things that are further away appear smaller.

Rather than use a rectangular prism shaped viewing volume, for perspective projection the viewing volume is pyramidal. When we add the near and far z clipping planes, this pyramid becomes a frustum (and hence the name). The figure below shows how the perspective projection viewing volume works:


This image has been lifted straight out of the OpenGL Programming Guide

Modify the program exp3b.c from an orthogonal projection to a perspective projection.

The Matrix Stacks

So far I have talked about single matrices. In actual fact OpenGL provides us with 2 stacks to store our modelview and projection transformation matrices. These stacks allows us to store matrices, so that we don't have to repeatedly set up a particular matrix.

For instance, we would like to have an object at a particular location and orientation spinning about its axis. We could do this using:

glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glTranslatef(0.5,0.2,0.1);
glScalef(0.2,0.5,0.2);
glRotatef(-10,1,0,0);
glRotatef(angle,0,1,0);
glutWireCube(1.0);
The program exp3c.c includes this code. Each time we execute the display function we execute all of this code.

An alternative would be to execute all the code up to, but not including, the rotate transformation before we enter the display function. Then in the display function, we will make a copy of the current modelview matrix on the modelview stack, and do the new rotation. After we have drawn our object we can reload the saved matrix back off the stack. We do this using glPushMatrix(); and glPopMatrix();.

Modify exp3c.c so that the only code in the display function is

glMatrixMode(GL_MODELVIEW);
glPushMatrix();
glRotatef(angle,0,1,0);
glutWireCube(1.0);
glPopMatrix();
Now each time we execute the display function we only need to do one matrix multiplication to set up the modelview matrix instead of 4.


Lighting

When we talk about lighting in computer graphics there are three different types of:
Ambient light: This is analagous to background radiation. It shines on everything from all directions.
Diffuse light: This is analagous to direct sunlight on a rough surface. It is radiated in all directions by the surface, but the intensity is dependent on the angle of incidence.
Specular lighting: This is analagous to a laser beam on a shiny surface. The light will only radiate in a specific direction, depending on the normal of the surface.

In OpenGL we can set up at least 8 light sources. Each source can be set with varying ambient, diffuse and specular components. We can also control the attenuation (how quickly the light intensity decreased as the distance from the light source increases) of each lighting component.

To set up a light source we have to use the commands glEnable(GL_LIGHTING); and glEnable(GL_LIGHTi); where i is an integer specifying the light we are setting up. GL_LIGHT0 though GL_LIGHT8 are guaranteed to be set up on all OpenGL machines.

Once you enable your light you can add some properties to it, using glLight*(). Program exp3d.c contains a light with only an diffuse red component. Add an ambient green component to this light.

You'll notice that when you add a light source the colour of the light changes the colour of the object you have drawn. The color you have set using one of the glColor*() functions will have no affect on the color of light that the object reflects. To tell an object how it will react to different types of light we need to use the glMaterial*() function.

Change program exp3d.c so that it contains a white diffuse light. Now change the material properties of the sphere so that it reflects a blue colour under diffuse lighting.

Experiment with some of the elements of the glMaterial* command. In particular try different colour properties, under a particular lighting color. Also experiment with the shininess parameter.

Question: What type of shading is used in this scene? How can we change the shading model? What affect does it have? This question will become important as we get into the assessed labs on scan-line filling and light models.

Normals

To achieve effective lighting we need to make sure the normals are set correctly on our surfaces. Using the glNormal*() command we can set the normals of the surface for each vertex we provide. We normally call the glNormal* function before issuing each glVertex* command. There is no problem setting different normals at a single vertex for different polygons.

An easy way to calculate the normal of a vertex of a planar object such as a polygon is to find two vectors on the plane (ie vertex2 -vertex 1 and vertex3 - vertex1) and cross-product them to find the perpedicular vector to the plane.

The code below will draw a cube with the normals set to the perpedicular vectors of each plane.

 glBegin(GL_QUADS);

  /* The z=1 face */
  glNormal3f(0, 0, 1);
  glVertex3f(-1, 1, 1);
  glNormal3f(0, 0, 1);
  glVertex3f(-1, -1, 1);
  glNormal3f(0, 0, 1);
  glVertex3f(1, -1, 1);
  glNormal3f(0, 0, 1);
  glVertex3f(1, 1, 1);

  /* The z=-1 face */
  glNormal3f(0, 0, -1);
  glVertex3f(1, -1, -1);
  glNormal3f(0, 0, -1);
  glVertex3f(-1, -1, -1);
  glNormal3f(0, 0, -1);
  glVertex3f(-1, 1, -1);
  glNormal3f(0, 0, -1);
  glVertex3f(1, 1, -1);

  /* The x=1 face */
  glNormal3f(1, 0, 0);
  glVertex3f(1, 1, 1);
  glNormal3f(1, 0, 0);
  glVertex3f(1, -1, 1);
  glNormal3f(1, 0, 0);
  glVertex3f(1, -1, -1);
  glNormal3f(1, 0, 0);
  glVertex3f(1, 1, -1);

  /* The x=-1 face */
  glNormal3f(-1, 0, 0);
  glVertex3f(-1, 1, 1);
  glNormal3f(-1, 0, 0);
  glVertex3f(-1, 1, -1);
  glNormal3f(-1, 0, 0);
  glVertex3f(-1, -1, -1);
  glNormal3f(-1, 0, 0);
  glVertex3f(-1, -1, 1);

  /* The y=1 face */
  glNormal3f(0, 1, 0);
  glVertex3f(1, 1, 1);
  glNormal3f(0, 1, 0);
  glVertex3f(-1, 1, 1);
  glNormal3f(0, 1, 0);
  glVertex3f(-1, 1, -1);
  glNormal3f(0, 1, 0);
  glVertex3f(1, 1, -1);

  /* The y=-1 face */
  glNormal3f(0, -1, 0);
  glVertex3f(1, -1, 1);
  glNormal3f(0, -1, 0);
  glVertex3f(1, -1, -1);
  glNormal3f(0, -1, 0);
  glVertex3f(-1, -1, -1);
  glNormal3f(0, -1, 0);
  glVertex3f(-1, -1, 1);

glEnd();

Program exp3e.c includes this code above. Run this program and have a look at the resulting cube. Its not always desireable for the normals of a vertex to be the same as the perpendicular of a plane. In the case of a curve we would like the normals to be normal to the curve. Replace the cube drawing code in exp3e.c with the code below.

glBegin(GL_QUADS);

  /* The z=1 face */
  glNormal3f(-1, 1, 1);
  glVertex3f(-1, 1, 1);
  glNormal3f(-1, -1, 1);
  glVertex3f(-1, -1, 1);
  glNormal3f(1, -1, 1);
  glVertex3f(1, -1, 1);
  glNormal3f(1, 1, 1);
  glVertex3f(1, 1, 1);

  /* The z=-1 face */
  glNormal3f(1, -1, -1);
  glVertex3f(1, -1, -1);
  glNormal3f(-1, -1, -1);
  glVertex3f(-1, -1, -1);
  glNormal3f(-1, 1, -1);
  glVertex3f(-1, 1, -1);
  glNormal3f(1, 1, -1);
  glVertex3f(1, 1, -1);

  /* The x=1 face */
  glNormal3f(1, 1, 1);
  glVertex3f(1, 1, 1);
  glNormal3f(1, -1, 1);
  glVertex3f(1, -1, 1);
  glNormal3f(1, -1, -1);
  glVertex3f(1, -1, -1);
  glNormal3f(1, 1, -1);
  glVertex3f(1, 1, -1);

  /* The x=-1 face */
  glNormal3f(-1, 1, 1);
  glVertex3f(-1, 1, 1);
  glNormal3f(-1, 1, -1);
  glVertex3f(-1, 1, -1);
  glNormal3f(-1, -1, -1);
  glVertex3f(-1, -1, -1);
  glNormal3f(-1, -1, 1);
  glVertex3f(-1, -1, 1);

  /* The y=1 face */
  glNormal3f(1, 1, 1);
  glVertex3f(1, 1, 1);
  glNormal3f(-1, 1, 1);
  glVertex3f(-1, 1, 1);
  glNormal3f(-1, 1, -1);
  glVertex3f(-1, 1, -1);
  glNormal3f(1, 1, -1);
  glVertex3f(1, 1, -1);

  /* The y=-1 face */
  glNormal3f(1, -1, 1);
  glVertex3f(1, -1, -1);
  glNormal3f(1, -1, -1);
  glVertex3f(1, -1, -1);
  glNormal3f(-1, -1, -1);
  glVertex3f(-1, -1, -1);
  glNormal3f(-1, -1, 1);
  glVertex3f(-1, -1, 1);

glEnd();

Now we've changed the normals so that they radiate outward from the centre. The result is that the lighting creates a smooth looking curve on our sphere. The effect though is dependent on the light model we are using. If we use the GL_SMOOTH lighting model then the intensity of each pixel on our polygons is calculated at each vertex of the polygons and interpolated across each pixel. If we use the GL_FLAT lighting model then the intensity of each pixel on our polygons is calculated at only one of the polygons and each pixel of the polygon is designated that intensity.

Back and Front Lighting

OpenGL normally only calculates the lighting for the front face of a polygon. The front face of the polygon is determined by the counter-clockwise ordering of the vertices (we can change this using glFrontFace).

Sometimes we would like OpenGL to calculate lighting effects for both the front and back faces of a polygon. We can make this change using the glLightModeli function.

Back Face Culling

In other cases we don't want to draw the back face polygons at all. For instance if we are drawing a closed object then there is no need for us to draw polygons which are facing away from us. We can tell OpenGL not to do this using glCullFace();


Creating Complex Objects

OpenGL includes only simple drawing primitives. It offers us no method to create complex objects such as spheres or cylinders. The methods we have been using so far are glut functions created using simple OpenGL primitives. Often we can't rely on these methods however. For example the glut methods often don't set the normal vectors right for each vertex.

If we are going to create our own shapes we need to specify the position of each of our vertices. Often these can be calculated using simple trigonometric functions.

For example to draw a circle of radius 1 centred at the origin we could create a function like this (shown in exp3f.c):

void draw_circle (int segments)
{
  float loop;
  float x, y;

  glBegin(GL_LINE_LOOP);
    for (loop=0; loop<segments; loop++)
    {
      x = cosf (2 * loop * M_PI / segments);
      y = sinf (2 * loop * M_PI / segments);
      glVertex2f(x,y);
    }
  glEnd();
}
Alter this function so that it draws a cylinder of height 1 centred at the origin. Don't forget to set the vertex normals for each vertex.

Display Lists

While we are on the topic of optimisation, we will also look at display lists. These provide a simple way to name complex objects in opengl, and usually allow us to draw them much quicker.

A display list will contain a series of OpenGL commands often stored in a format which can be easily used by the graphics engine. In a network application the display list may be stored on the client machine, so that when the object is drawn its vertices do not have to be transmitted across the network.

To create a display list we would use the following code:

object = glGenLists(1);              /* find a vacant list */
glNewList(object, GL_COMPILE);       /* start the new list */
  ...                                /* our drawing routines */
glEndList();                         /* end the list */
We could then use glCallList(object); to execute the display list.

Typically we will create the display list in a graphics initialisation function which also sets up the viewing transformation and lighting etc. Inside the display list we add the normal routines to draw our object. When these routines are executed, the OpenGL commands are compiled into a list with all the values evaluated. When we call the list we don't need to re-evaluate all of the values again.

Edit the cylinder program you just created so that the cylinder you draw is created in a display list. Once you have done this you will be able to draw the cylinder by simply adding the command glCallList(object); to your display function.


Extra Bits

Here are some extra things that OpenGL can do. You may like to play around with some of them.

Blended Transperancy

Blended Transparency is a technique for creating mock transparency using the alpha bit. To do this we need to use the two commands:
glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA); to set up the blend function, and
glEnable(GL_BLEND); and glDisable(GL_BLEND); to turn on and off alpha blending.

Program exp3g.c shows an example of a program using alpha blending. In your own time, you can experiment with alpha blending to get different effects. Be aware that not everything should be drawn with alpha blending turned on. You are best to enable and disable alpha blending when you need it.

Shadows

OpenGL itself does model the way light interacts with objects, and therefore cannot calculate shadows. If there is a light source, it will treat all objects as if light from that source was cast onto it, regardless of whether the light source is occluded or not.

To create shadows then we have to do it ourselves. Program exp3h.c shows an example of a fake shadow (there is no lighting set for this scene). To create the shadow I have changed the projection matrix so that the object is projected onto the plane, and then redrawn the object.

If you are interested in adding shadows into your project then you should come and see me.

Bonus question: Why have we had to disable and re-enable depth buffering in this program?

Spot lights

OpenGL also allows you to create spotlight effects, such as you see when you shine a torch on an object. Add a spotlight effect to one of your programs using diffuse lighting, and the GL_SPOT_CUTOFF factor. Use the Online Programming Guide for help.

Texture Mapping

Texture mapping is the technique of mapping a 1D, 2D, 3D or 4D object (usually a 2D image) onto the surface of a set of polygons. Program exp3i.c shows an example of texture mapping.

Texture mapping in OpenGL is handled in a similiar way to the modelview and projection transformations. A texture matrix is used to describe how the object is to be mapped onto the co-ordinates of our polygons.

The texture mapping commands are quite complex and we won't cover them in the labs. Also, for our projects the texture mapping is too slow to make it worthwhile using the Indys.


The End

That brings to a close the intro labs for this unit. From here you will have lab assignments which will be collected every two weeks. The first assignment will be handed out this week.


Philip Dunstan, dunstan@ee.uwa.edu.au