During this lab we will be looking at viewing transformations, and lighting, with a collection of other stuff thrown in.
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.
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.
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.
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:
Modify the program exp3b.c from an orthogonal projection to a perspective projection.
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.
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.
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.
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.
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();
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.
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.
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.
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?
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.