While we are now able to add numerous lighting effects into our scene, the final rendering can appear rather poor. This is due to the coarse tessalation done by the pipeline particularly for large flat surfaces. Hence if we wish to improve the lighting effect, especially for spotlights, we need to add additional geometry (along with corresponding vertex normals) in the application. Doing this by hand is quite tedious, but for regular surfaces we can use the procedure of recursive subdivision to repeatedly subdivide our polygon into smaller ones using a recursive function. The net result is an outwardly identical surface that is defined using a greatly increased number of vertices. While this will drastically improve our lighting effects, it comes at a significant performance penalty so it should be used with caution (or possibly in a display list). I have also included code that demonstrates the creation of a simple menu to select between the two materials.

0. Getting Started

Download CS370_Lab14.zip, saving it into the labs directory.

Double-click on CS370_Lab14.zip and extract the contents of the archive into a subdirectory called CS370_Lab14

Navigate into the CS370_Lab14 directory and double-click on CS370_Lab14.sln (the file with the little Visual Studio icon with the 12 on it).

If the source file is not already open in the main window, open the source file by expanding the Source Files item in the Solution Explorer window and double-clicking recursiveCube.cpp.

If the header file is not already open in the main window, open the header file by expanding the Header Files item in the Solution Explorer window and double-clicking lighting.h.

If the shader files are not already open in the main window, open the shader files by expanding the Resource Files item in the Solution Explorer window and double-click lightvert.vs and lightfrag.fs.

1. Recursive Subdivision

One simple way to add internal vertices without changing the geometry is to divide each edge of our polygon in half and connect these midpoints. Then we recurse into each newly created region and perform the same subdividing computation. We can continue this process as many times as possible, however since the process creates an exponentially growing number of vertices usually only a few levels are needed. For example, consider the following square

image

One possible subdivision is shown below

image

where the new vertices can be found (per component) using the formulas

image

The new polygons are then given by (v1,v2',v5',v1'), (v2',v2,v3',v5'), (v1',v5',v4',v4), (v5',v3',v3,v4'). NOTE: Remember that the subdivided parts should maintain an identical orientation as the original polygon (beginning at the upper left corner in this case) for successful recursion. Each of these four squares can then be subdivided using an identical procedure into 4 subdivisions (for a total of 16) and so on.

One very nice way to accomplish the subdivision is to use a recursive function that continues to divide each square until a stopping criteria is met such as an edge length or maximum number of recursions. At this point, the routine renders the final polygon and returns to the previous level of recursion.

Tasks

2. Computing Normals via Cross Product

Any three non-colinear points define a triangle which will be a flat polygon (which is why the pipeline uses triangles for tesselation), and thus all three vertices lie in a single plane. The normal to this plane can be computed by taking one of the vertices (P) as the common endpoint of two vectors extending through the other two points (Q, R). Using a vector operation known as the cross product, a third vector can be found which is perpendicular to these two vectors - thus will be normal to the plane containing the points.

image

The vectors are computed, observing the right-hand rule, as follows

image

Hence a true normal n is computed for the above two vectors a and b as

image

Since this computation does not necessarily result in a unit normal, i.e. one with length 1, we must normalize the vector within our application (or make sure to normalize all normals in the shader). In order to perform normalization, we first compute the norm (or magnitude/length) of the vector as

image

Then divide each component of the original vector by this norm to produce a unit vector in the same direction as

image

Tasks

3. Ambient (Background) Lighting

So far we have added light sources into the scene to illuminate our objects, but if no light sources are in the direction of an object (e.g. a back face) they will appear black. While this may be realistic, it does not usually produce a visually appealing scene. Hence we can add an overall ambient background light into the scene so all objects have some illumination (thus allowing their ambient color to be seen regardless of light sources). This is done using the command

glLightModelfv(GL_LIGHT_MODEL_AMBIENT, *values);

where values is an RGBA vector containing the ambient light color components. We then simply use this color vector to initialize the ambient vector in the shader prior to accumulating additional light source contributions. In lighting.h is another utility function to set the background light

setAmbientLight(*background);

where background is a four element GLfloat array containing the background ambient color components.

Tasks

Compiling and running the program

Once you have completed typing in the code, you can build and run the program in one of two ways:

(On Linux/OSX: In a terminal window, navigate to the directory containing the source file and simply type make. To run the program type ./recursiveCube.exe)

The output should look similar to below

image

image

To quit the program simply close the window.

We have now completed adding light effects into our scenes. So far our scenes have consisted primarily of single objects, or multiple objects with no relationship between them. However for many scenes, e.g. the railroad assignment, objects are often related to one another - such as the wheels on the train. Thus when the train moves, the wheels should simply translate with it and only an additional local rotation is really needed. A convenient way to construct such objects (and in fact entire scenes) is by organizing things into a tree structure known as a scene graph. Each node of the tree contains information that applies to that node (and others below it) and rendering the scene is done by simply traversing the tree.