Lab 2: A simple virtual world in OpenGL

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

Goal: In this lab, you will construct a 3D world with multiple objects in it. You will be able to fly around freely in the world. Focus will be on how to construct a world, with hierarchies between objects and dynamic lighting, and the original artwork has been built by people specializing in 3D art using tailor-made 3D art packages.

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.


1) Setup, and drawing a model with arrays

If you haven't done so already, download and unpack all packages required for lab 1. It will help you a lot if you have completed lab 1's assignments.

Download the lab package from lab2008-2.tar.gz and unpack it at a suitable location.

Download the model package from lab2008-models.tar.gz and unpack it at a suitable location.

To compile this assignment, perform make lab2-1 on the command line.

Run the assignment by performing ./lab2-1. It is the same cube from the previous lab, but the actual polygon rendering code is implemented differently:

  glEnableClientState(GL_COLOR_ARRAY);
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_NORMAL_ARRAY);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glColorPointer(3, GL_FLOAT, 0, cubeColors);
glVertexPointer(3, GL_FLOAT, 0, cubeVertices);
glNormalPointer(GL_FLOAT, 0, cubeNormals);
glTexCoordPointer(2, GL_FLOAT, 0, cubeTexCoords);
glDrawElements(GL_TRIANGLES, numCubeIndices, GL_UNSIGNED_INT,
cubeIndices);

The cube's coordinate/color/normal/vertex data is stored in four arrays. You can find these in cube.c.
The first four lines in the snippet above enable the corresponding array type. Once an array type is enabled, a base pointer must also be specified; otherwise the glDrawElements() call will crash.
glColorPointer(3, GL_FLOAT, 0, cubeColors) specifies that the color-array begins at the symbol cubeColors, there are 3 color elements per entry, each entry is a GLfloat, and there is 0 bytes of padding between each RGB triplet.
glVertexPointer(3, GL_FLOAT, 0, cubeVertices) similarly specifies beginning and format of vertex-position.
glTexCoordPointer() works the same way.
glNormalPointer() requires normal vectors to have 3 components (X, Y and Z) and does therefore not have a component-count parameter.
Finally, glDrawElements(GL_TRIANGLES, numCubeIndices, GL_UNSIGNED_INT, cubeIndices) will process numCubeIndices number of indices. Since the primitive type is specified as GL_TRIANGLES, each triplet of indices will be treated as an individual triangle. The index array begins at cubeIndices, and GL_UNSIGNED_INT specifies the element format of the array. Once three primitives have been read from the array, actual vertex data will be read from the corresponding locations in the color/vertex/normal/texcoord arrays, and a triangle will be rendered using the resulting data.

 

Questions:


2) Drawing a model from a file

Goal: To load and draw a complex model which is stored on-disk.

Copy lab2-1.c to lab2-2.c. Make this section's changes to lab2-2.c.

Look in helpers.h. There you find the following function:

Model* loadModel(char* name);

It loads in a 3D model and prepares the data such that it is easily usable. The data will be available in the Model struct. All models have vertex, normal and index data. Some have texture coordinates. Currently, none has color data. Missing data means that the array pointer is 0.

Use the loadModel() method to load the bunny object ("../models/various/bunny.obj"). Display it by changing the polygon drawing commands to fetch data from the model struct instead of from the cube data structures. Remember not to enable any array types whose data pointers are 0. Also, since the bunny does not have texture coordinates, you should disable texturing.


3) Projecting a texture onto a mesh

Goal: To project a texture onto geometry which lacks pre-made texture coordinates.

Copy lab2-2.c to lab2-3.c. Make this section's changes to lab2-3.c.

Enable texturing again. After the glBindTexture() call, insert the following lines:

      glEnable(GL_TEXTURE_GEN_S);
glEnable(GL_TEXTURE_GEN_T);
glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);

The first two lines enable texture generation for texture S and T coordinates.
The second pair of lines specify exactly how the texture coordinates should be generated. GL_OBJECT_LINEAR says that the texture coordinate for a vertex should be generated from the position of the vertex, before any transformations take place. This means that whenever the object moves/rotates, the texture will follow it.

 

Insert the following lines directly after the ones above:

      GLfloat planeS[] = {0.5, 0.0, 0.0, 0.5};
GLfloat planeT[] = {0.0, 0.5, 0.0, 0.5};
glTexGenfv(GL_S, GL_OBJECT_PLANE, planeS);
glTexGenfv(GL_T, GL_OBJECT_PLANE, planeT);

The first two lines specify the A,B,C,D coefficients for a pair of planes on the form Ax + By + Cz + D = 0. The first plane sits vertically, with its normal vector pointing toward the right; the second plane is horizontal with its normal vector going upward.
The texGenFv() calls set the planes as references for texture generation, when mode GL_OBJECT_LINEAR is active. The S coordinate for a given vertex will then be calculated as the signed distance between the vertex and the reference plane. The T coordinate is calculated in an analogous manner, but from the other plane.

You can also use texture generation mode GL_EYE_LINEAR. The vertex position used for coordinate generation is the vertex position when transformed to camera space.

Also, for simulating lenses, reflection and other things, there are modes GL_SPHERE_MAP and GL_REFLECTION_MAP. These have no reference planes.

 

Questions:


4) Using pre-projected textures

Goal: To render geometry which has pre-made texture coordinates.

Copy lab2-3.c to lab2-4.c. Make this section's changes to lab2-4.c.

Disable the automatic texture generation. Have a look inside the "../models/LittleNell/" directory. There is one object per directory here. We recommmend the "Tree" directory for this experiment; avoid the "Hawk". Each object is split into several .obj files, and the corresponding texture is stored next to it with the same name. Select one of the objects. Load in the models and their associated textures, and render them appropriately.

These models are intended to be rendered without dynamic lighting. Therefore, you should turn off OpenGL's dynamic lighting. With lighting disabled, you can see that the textures used on the objects already have light/dark areas in them; a radiosity light simulation has been run on the objects, and the result has been added to the textures.

 

Questions:


5) Managing large scenes

Goal: To present better tools for managing worlds containing multiple different objects.

You will soon render worlds containing different types of geometry. Some loaded from disk, some drawn using glBegin()/glEnd. Some having textures, some without textures. Keeping track of how the different states in OpenGL have been set will quickly become messy and error-prone as you add more objects. Therefore, OpenGL has several different stacks:

glPushClientAttrib(GL_CLIENT_ALL_ATTRIB_BITS) stores all client state. This is the information on which array types have been enabled using glEnableClientState() and also how the corresponding array pointers have been setup. When you do glPopClientAttrib(), you restore the previous settings for all those states.

glPushMatrix() stores the contents of the currently active matrix (GL_PROJECTION or GL_MODELVIEW). You can make temporary changes to the matrix, render polygons using the new matrix, and then perform glPopMatrix() when you wish to undo your changes. There is one separate stack each for GL_PROJECTION and GL_MODELVIEW.

glPushAttrib(GL_ALL_ATTRIB_BITS) stores all state which is not mentioned above. This includes lighting settings, texturing settings, backface culling, and so on. Restore it with glPopAttrib().

Push and Pop calls can be nested several times. When drawing a complex scene, first set up a good set of default states (enable backface culling, enable depth testing, set up a projection and a modelview matrix); let the rest of the code rely on this state being set. Whenever one particular object needs a special setting (perhaps an object should be rendered with lighting disabled), add Push and Pop operations around the rendering of that object.


6) Hierarchial models

Goal: To construct an animated object using transform hierarchies.

Copy lab2-4.c to lab2-6.c. Make this section's changes to lab2-6.c.

Replace the currently loaded object with the windmill. It is located in the "../models/windmill/" directory. You will notice that the piece called "blade.obj" is not placed properly in relation to the rest of the windmill.

Try adding textures or changing the colour of the different pieces of the windmill. Not only will that make it look better; it also makes it easier for the you to perceive the shape of the object.

To make it easier to view large objects, you need to switch to a new controller. Replace the line which says:

  glLoadMatrixd(getObjectMatrix());

with:

  glLoadMatrixd(getCameraMatrix());

getCameraMatrix() returns the matrix for a free-flight camera controller that is being maintained by the helper library. Rotate by holding down the left mouse button and moving the mouse; move along your local Z axis by holding down the right mouse button, and move along your local XY axes by holding down the middle mouse button. Also, the QWEASD buttons on the keyboard move along the XYZ axes. Familiarize yourself with the new controller.

Now that you can fly freely around the windmill, it is time to fix its blades. Add glRotatef() and glTranslatef() commands to position the base of the blade at the cylindrical knob which sticks out on the right side of the windmill.

Once the blade has been attached to the cylindrical knob, add code to render the blade three more times, each time rotated 90 degrees around the cylindrical knob. When this is done, the blades should form a cross with the knob in the center.

Once you have four blades visible, it is time for you to have them start rotating. Add one or more calls to glRotatef() with time-dependent arguments to make the blades rotate around the knob.

 

Questions:


7) Virtual world

Goal: To build a world containing several different types of objects, where the camera can navigate freely.

Copy lab2-6.c to lab2-7.c. Make this section's changes to lab2-7.c.

The time has come to attempt building a larger world.

Create a ground plane, and put a texture on it.

Place out at least three windmills in your world.

Add at least one more static object. Some objects may turn out to have an unsuitable size; you can apply scaling to the modelview matrix through the glScalef() call.

Add at least one more dynamic object.

Place a lightsource somewhere between the buildings. Earlier, you have only positioned lightsources relative to the camera. Now you need to position it relative to the world; this is how you position light number 1 at location (14, 22, 39) in the world:

    glMatrixMode(GL_MODELVIEW);
glLoadMatrixd(getCameraMatrix());
GLfloat light_position[] = { 14.0, 22.0, 39.0, 1.0 };
glLightfv(GL_LIGHT1, GL_POSITION, light_position);

So, by having the camera matrix loaded when placing the light, the light's position will then be given in worldspace.

You also need to change the colour of light 1. By default, light 1 is entirely dark. Add this code:

    GLfloat ambientColor [] = { 0.0, 0.0, 0.0, 0.0 };
GLfloat diffuseColor[] = { 0.0, 1.0, 1.0, 1.0 };
GLfloat specularColor[] = { 1.0, 1.0, 1.0, 1.0 };
glLightfv(GL_LIGHT1, GL_AMBIENT, ambientColor);
glLightfv(GL_LIGHT1, GL_DIFFUSE, diffuseColor);
glLightfv(GL_LIGHT1, GL_SPECULAR, specularColor);

The above code sets light 1's diffuse colour to tourquiose and its specular color to white. The ambient color is a base color that is always applied, regardless of the direction/position of the light or the geometry. Increase the ambient level if you find that non-lit areas are too dark.

Finally, you must also enable light 1. That is done as follows:

  glEnable(GL_LIGHT1);

You can also try adding some moving lights (this is optional).


8) "Skybox", revisited

Goal: To add a panoramic view around the world.

Copy lab2-7.c to lab2-8.c. Make this section's changes to lab2-8.c.

You implemented a skybox in lab 1-7. Add the polygon drawing code from that assignment to the current code. Now that you have a free-flight camera controller, it is time to correct some issues with that code.

The skybox gets Z-buffered against the world. If you render the skybox first of all, you can render it with Z-buffering disabled. That will remove any risk of interactions between the skybox and other geometry. The skybox will then not have to be large enough to enclose the entire world, either. You disable the Z-buffer by doing a glDisable(GL_DEPTH_TEST) call.

The skybox should always remain centered around the camera. Currently, the skybox will bend more the farther you fly from the center of the world. In order to center the skybox, make a copy of the 4x4 GLdoubles that getCameraMatrix() return a pointer to; force the translation portion of the matrix to zero, and then load the new camera matrix by performing glLoadMatrixd(newMatrix). After this, the skybox should be centered around the camera, but still aligned with the world's orientation.

 

Questions:


9) Transparency

Goal: To render transparent and opaque surfaces in the same world, without any obvious visual artifacts.

Copy lab2-8.c to lab2-9.c. Make this section's changes to lab2-9.c.

When OpenGL is rendering polygons, so far it has always been overwriting the old colour values of pixels with entirely new colour values. When blending is enabled, the new colour values can instead be combined with (blended against) the old colour values.

To enable blending, you do the following:

  glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

The glEnable(GL_BLEND) enables blending. When blending is enabled, the final color value will be a combination of the old and the new pixel value. More specifically, the final colour value will be computed as:

      newPixelColor = newPixelColor * sourceBlendFactor
+ oldPixelColor * destBlendFactor

sourceBlendFactor is set to GL_SRC_ALPHA, which means to use the alpha value of the new (source) pixel. destBlendFactor is set to GL_ONE_MINUS_SRC_ALPHA which means to use one minus the alpha of the source pixel. This creates a linear interpolation between the old and the new pixel value, where the alpha value of the new pixel value controls the interpolation position.

Enable alpha blending with the above mode for the walls of your windmills. The alpha value which controls the opacity of the walls will be calculated from the alpha values you have specified for light and material color settings (ambient/diffuse/specular); decrease the alpha components until the meshes get semi-transparent.

But wait... now you have strange artefacts: Only some of the geometry can be seen through the wall of each windmill. This is dependent on the order in which you render things. Can you reduce or eliminate the problem somehow?

 

Questions:


That concludes lab 2. In the next lab, you will try your hands at shader programming.