|
CS 428 - Fall 2011 Project 2: Polygon Meshes and Shading
Due electronically: Wednesday, October 26, 12 noon |
|
Description
In computer graphics, surfaces are often represented with collections of polygons. There is additional structure as well; while the vertices of the mesh represent the geometry of the surface, polygon faces share vertices, so they not only describe where the surface is among the vertices, but also its connectivity.
Because of the broad support of polygon rendering in graphics hardware, polygon meshes are widely used in games and visualization applications. Recent trends in programmable hardware push this even further, with standards like GLSL.
Objective
This project will help you understand how to represent, create, manipulate, and render polygon meshes (in OpenGL and GLSL) in a variety of styles.
Program
You will be provided with skeleton code for this program, which supplies the necessary user interface and main program structure. A simple polygon mesh data structure is provided, along with some code in GLSL. Code which reads a polygon mesh from a file is also provided. You will be filling in the missing parts in functions (which are marked with "// ..."), and are listed in the README.txt file. You must develop the code for computing normals of the polygon mesh, drawing the mesh in various styles, and evaluating tessellated objects (an ellipsoid), in both OpenGL and GLSL.
The skeleton code for this project can be found (on the cereal
machines) in the directory
~decarlo/428/proj2
Also included is a Makefile
and a README.txt file describing the code structure and how
to compile and run the program.
There are some example polygon mesh files in:
~decarlo/428/obj/
Some of these files are big, so watch your quota (use quota -v).
You should probably just make a symbolic link to the objects
directory contained inside your code directory, after you copy the code:
cp -r ~decarlo/428/proj2 .
Make the link like this:
ln -s ~decarlo/428/obj proj2/obj
In addition to saving your disk space, should more objects be added
into that directory, you won't miss them.
GLSL
GLSL is a lot like C (well, the types are a little stronger). We'll be using just the basic features, so don't worry. The GLSL programs you'll be editing are in files that end in .vp for the vertex programs, and .fp for the fragment programs. Refer to the following GLSL materials:
You don't have to compile the program youself; it gets compiled at run-time, and only when you use it. (The main program is designed to work fine on a machine that doesn't support GLSL, as long as you don't turn it on.) The compile-time error messages are helpful for things like syntax errors, type-checking failures, etc... Run-time errors are more problematic, as the entire program aborts. It's a good idea to make small changes to these programs, and then test them, until you feel comfortable with GLSL.
One kind of run-time error you might encounter (that causes the program to exit) happens if your GLSL program is too long. Lengths vary by machine. Make sure your code isn't drawn out -- keep it compact. You don't need to overdo it; in our own development of this project, we didn't encounter this problem. We will be grading the programs in Hill 248 (since those machines are the newest). Note that the machines in Hill 252 are a bit older, and only permit relatively short GLSL programs.
Mathematical functions are available, and are named the same as in C (and on the whole, Java). For instance, sin(), cos(), and pow(). There is also a max() function---if you put 0 as one of the arguments instead of 0.0, that you'll get a type error. These work with float type variables. There are also vector types vec3 and vec4 for three and four dimensional vectors. These have the expected operations: normalize() for vector normalization, dot() for dot product, etc...
Handing in
Hand in the following:
You will need to create a tar archive to hand in, that contains your java and GLSL files and description:
tar cf proj2.tar Makefile *.java *.vp *.fp descrip.txtHere are the instructions on how to hand in this file.
This assignment is a bit vague so that you can make a lot of the design decisions yourself. There are several approaches to the problems here. Use your best judgement to try and get the most useful result. Ask for help or clarification when you need it.
Program use
To run the program, you either read in a polygon mesh from a file, or create one by tessellation. The files are in Wavefront OBJ format (a simplified version, actually, which only reads in the specification of vertex locations and polygon faces -- but you should still be able to use any other OBJ files you might find).
To read in a mesh from a file (for the file "cube.obj" in the objects
directory):
java Mesh obj/cube.obj
To create an ellipsoid (from a 20x30 grid):
java Mesh -ellipsoid 20 30
To create an ellipsoid (using the default 24x24 grid):
java Mesh -ellipsoid
Once the program is running, you can transform the object, as well as specify how you want the object rendered (polygons, wireframe, silhouettes, smooth or faceted shading, material properties, etc...) using sliders and checkboxes.
Data structure
All shapes are represented using the Shape class, which contains the parameters for the shape, as well as the mesh used to represent it.
The polygon mesh contained in the Shape is represented as an array vertices, and an array polygons. It is assumed that all polygons are stored in a counter-clockwise fashion (normal vectors point outward), and that their normal vectors are normalized (unit length). The class PolyMesh which extends Shape is for arbitrary meshes (which are read from files). And the class UVShape extends the Shape class to represent a uv-parameterized shape (like an ellipsoid). The Vertex interface (a purely abstract class) represents information about a specific vertex: its location is accessed using getPoint() and its normal vector (averaged from neighboring polygons) is accessed using getNormal(). The Polygon interface represents information about a specific polygon in the mesh. A particular Vertex is accessed using getVertex(), the total number of vertices using size(), and the averaged normal vector using getNormal(). The Polygon interface is implemented by PolygonAccess which just handles the array of vertices in each polygon. The Vertex and Polygon classes are implemented differently by PolyMesh and UVShape. On the whole, the details of these aren't important until you start with GLSL, in which case, you should look how getPoint() and getNormal() work for the UVShape.
All access to the vertices and polygons stored in the Shape class should be through the Vertex and Polygon interfaces. Perhaps you can convince yourself of this by looking at the code for the methods in these interfaces. The vertices stored in each polygon are accessible through Polygon.getVertex(). This returns the actual Vertex object (not a copy of it). It doesn't return an integer index into the array of vertices -- although it could have worked that way, too.
Drawing
The polygon mesh is transformed using an object transformation (like in Project 1, although the order of rotations is reversed here, to make it more intuitive to manipulate objects like the ellipsoid, which are aligned with the Z axis). This part of the code is already written for you. After this transformation, you draw the mesh in Shape.draw. You will be drawing the polygon mesh using one of several styles (perhaps more than one at a time). The following are the styles:
Here are examples of each of these shading methods:
![]() |
![]() |
|
| Flat shading | Smooth shading |
While you could draw this using lines directly (i.e. GL_LINE_LOOP), you must instead draw them as polygons, where you use an OpenGL feature that renders only the boundaries of polygons as lines (use glPolygonMode with GL_FRONT_AND_BACK and GL_LINE). If you don't do it this way, you won't get silhouettes working (below).
If drawing polygons is not enabled, you should see the entire mesh (as seen below, on the left). Otherwise, the wireframe is only seen on the visible part of the mesh (as on the right).
![]() |
![]() |
|
| Wireframe | Wireframe + polygons |
Note that the wireframe rendering in the picture on the right also implements one of the optional extensions, which prevents it from appearing "flickery".
There is a sneaky way of doing this in OpenGL. If you first draw the polygons in a solid color (unshaded), and then draw the back-facing polygons (selected using glCullFace) as thickened wireframe (the line width is set using glLineWidth), then the parts of the wireframe only show through where there is a silhouette. This way, you don't even need to figure out where the silhouette actually is!
If you are not drawing any polygons, you can actually draw the first pass in a way that doesn't affect the resulting image directly, but does still update the Z-buffer (use glColorMask) -- this is how you get the result as in the image below, on the left. The rest of the images show how silhouettes are drawn with other styles (wireframe, or smoothly shaded polygons).
![]() |
![]() |
![]() |
||
| Silhouettes only | Silhouettes + wireframe | Silhouettes + polygons |
You're given the vertex program for this in illum.vp. You'll see that it's essentially a pass-through shader, which additionally sets varying variables that hold the position and normal in eye coordinates. So these quantities will be available in the fragment shader (after they are interpolated for each pixel in the polygon), even though the geometry will have been projected by then. (It also sets gl_FrontColor so that when we bypass the fragment shader when drawing the wireframe, the color of the wireframe still gets through.) We will be using the Phong reflection model without attenuation, and no global ambient light. We will also use the Blinn-Phong specular model (the Phong specular model is below in the extra credit). Thus, you must compute the following in your fragment shader in illum.fp:

![]() |
![]() |
|
| Gouraud shading (OpenGL) |
Phong shading (GLSL) |
Note: You need to compute the vectors l and v yourself. The vector l points from the surface point (pos) to the position of the light (gl_LightSource[0].position). Everything is in eye-coordinates (the coordinate system of the OpenGL camera), so you just subtract the and normalize. The vector v proceeds similarly, except that the vector points from the surface point to the camera center (where is this in eye coordinates?). So again, subtract two points and normalize the result. (Note that v is not the vector (0,0,1); the book says this, but it also says this is for a "fixed viewing direction", such as an orthographic camera. That isn't what we're doing here, so compute v as describe above.)
Note: Do not use gl_LightSource[0].halfVector at all, but instead compute h yourself from l and v, as above. (This particular value is constant, and requires strong assumptions to be true for it to be useful.)
The result of the illumination computation should be stored in gl_FragColor. The light source is a positional (not directional) light, and using the above variables, this gives you enough information to compute the illumination equation above.
We need to make sure that the fragment shader doesn't interfere with the other drawing styles. For instance, the lighting will be applied to all drawing (including the wireframe) if we're not careful. We actually only want the fragment shader operating when we're drawing the shaded polygons. As a result, before drawing these polygons, you should call enableFragShader(), and afterwards you should call disableFragShader(). These functions set the variable useFragShader in illum.fp.

Each of these drawing styles is individually specified through the user interface and are defined as BooleanParameters in Shape.
Normal vectors
Given a polygon mesh, we must compute both the polygon normals (those returned by Polygon.getNormal()), and the vertex normals (those returned by Vertex.getNormal()), which are averaged from the adjoining polygon normals, weighted by area. The code for computing the polygon normals for a is provided for you: see PolyMesh.PolygonPM.computeNormal(). Is uses Newell's method (which is equivalent to adding up normal vectors at the corners), but does not normalize the normal vectors---the length of the vector is proportional to the area of the polygon. You'll need this when computing the vertex normals. For a UVShape, all normals are computed analytically; more on this below.
You must write the code that computes the vertex normals in PolyMesh.computeAllNormals(). This computation is performed on the entire mesh at once (using the algorithm described in class), so that the normal at a vertex is the area-weighted average of the polygon normals over all polygons in which it is contained.
Tessellation
To form the mesh of a parametric shape, you start from a grid of vertices over a 2D domain, and compute its geometry (both points and normal vectors) from analytic equations. So for each point (u,v) in the domain, we can compute its corresponding point on the surface using a function p(u,v) and its (unnormalized) normal vector using n(u,v). For instance, one possible parameterization of an ellipsoid (with three parameters: ax, ay and az - the axis lengths of the ellipsoid in the x, y, and z directions) has the equation:

Extensions
The following is a list of optional extensions; difficulties are marked.


Use the checkbox labeled "Phong model" to enable this feature, which corresponds to the variable phong in illum.fp. When you implement it, you'll see how the shininess values don't correspond when you switch between models. Put in an adjustment that makes the degree of shininess be comparable but not exactly equal, by determining α' in terms of α. See the Wikipedia Blinn-Phong shading model page for details. Explain how you came up with your adjustment in a comment, or perhaps in a separate file. The "right way" involves doing some math: I suggest doing it in Maple.
Add a method Shape.drawNormals() and call it from
Shape.draw(). Your code will need to act differently
when GLSL is on and you're drawing a UVShape; it's
fine to use a boolean expression like the following instead of
a more object-oriented solution:
useGLSL() && !getClass().getName().equals("PolyMesh")
Modulate the value of only the diffuse component on a per-pixel basis: replace kd in the lighting computation by C(x,y,z) kd.
Be sure the volume texture is applied in object coordinates, so that when you change the viewpoint it looks stable: like the object is carved out of that material, and not like the texture is flowing across the object.
This should work for both objects loaded from files and parameterized shapes.
Add variables to IllumProgram.java and illum.fp
that allow for texturing to be enabled and disabled, and also for
the "size" of your texture elements (i.e. the size of a
checkerboard cube). A useful control for the size is a
logarithmic one, where the value on the GUI slider is a number
s, and the size of the texture elements is
2s.
Hints
The following are some suggestions...