CS 455
Syllabus
Policies
Projects
Calendar
Slides
 
Project Overview
   Project 1
   Project 2
   Project 3
   Project 4
   Project 5
 
Old Quizzes
 
 

Project 4: Lighting, etc

The required functionality in this lab is to implement perspective, normals, and lighting. It is worth 50 points.

The remaining 50 points of functionality can come from other lighting models, texturing, perspective-correct interpolation, picking interfaces, and related OpenGL extensions.

Positions, Directions and Infinity

If you don't care about math, skip to the last paragraph of this section.

In graphics we use 4-vectors in homogeneous coordinates to represent both points (positions) and directions (normals). Any homogeneous-coordinates vector may be multiplied by any positive constant without changing its meaning.

Given two positions, p, and q, the direction to q from p is q – p. However, to add or subtract position vectors, they must have the same w coordinate. Thus if q = (4,0,0,4) and p = (0,1,0,1), the direction is not (4,-1,0,3) but rather (4,-4,0,0).

4-vectors with 0 w coordinates represent either directions or points infinitely far away. Infinite positions are useful for describing things like the sun, which might reasonably be said to be at a position like (0,1,.2,0). Now, suppose I want to know, from a point close at hand, say (1,1,0,1), what direction the sun lies? I have (0,1,.2,0) - (1,1,0,1); to get the ws the same, I have to multiply the second vector by zero, giving me simply (0,1,.2,0).

In practice, this means that to get lighting to work right, your vector minus vector method needs to look like the following:

	vec p - vec q:
	  if p.w != q.w:
	    if abs(p.w) < abs(q.w):
	      q *= p.w/q.w
	    else:
	      p *= q.w/p.w
	  now do standard elementwise subtraction

Required Functionality

In order to receive any credit for this lab, you must implement the following portions of the OpenGL API. Completing the required functionality is worth 50 points.

Functionality Perspective projection
Description OpenGL provides two methods for perspective: glFrustum and gluPerspective. You must implement one of these, and can earn an extra ten points for implementing both.

In practice, call one of these two methods in GL_PROJECTION matrix mode, and do nothing else to that matrix. A lot of the extra OpenGL functionality (fog, mipmaps, lighting, clipping planes, etc) relies on the assumption that that is what the projection matrix contains.

The larger far/near is, the less effective a depth buffer will be. You should never set near ≤ 0 or near &ge far.

Both of these methods modify the currently active matrix to implement perspective projection. The projection is such that the eye is at (0,0,0) and looking down the negative z axis. Projection is accomplished by setting a non-one w-coordinate; in particular you want (*,*,-near,1) to become (*,*,-near,near) and (*,*,-far,1) to become (*,*,-far,far).

In glFrustum( left, right, bottom, top, near, far ), the width of the view is specified by giving the dimensions of the largest object that could be completely seen at near depth; that is, a rectangle from left to right in x, bottom to top in y, and z = –near is mapped to ([-1,1],[-1,1],n,n).

In gluPerspective( fovy, aspect, near, far ), fovy is the field of view angle, in degrees, in the y direction; aspect is the ratio width/height of the viewport, which is always equal to sin(fovx)/sin(fovy).

If you did point clipping in the last project, there is a strong chance that you will see points behind the camera once you have projection working. This is because you clipped -1 ≤ p/w ≤ 1 instead of -w ≤ p ≤ w, which allows you to draw points with negative w. To work around this, if any of the points of a primative has a negative w, ignore the whole primative. If you did frustum or plane clipping, this shouldn't effect you.

Example
	glViewport(0,0,640,480)
	glMatrixMode(GL_PROJECTION)
	glLoadIdentity()
	if frustum
		glFrustum(-0.1,0.1, -0.1*480/640,0.1*480/640,  0.1,10)
	else
		gluPerspective(90, double(640)/480, .1, 10)
	glMatrixMode(GL_MODELVIEW)
Now the camera is at (0,0) and can see things with z values between -.1 and -10.
Credit Required, plus 10 if you do both methods.
References pp. 351-356, 368-382; see also the "glFrustum" and "gluPerspective" man pages.

Functionality Normals
Description OpenGL allows the use of normal vectors, which are attached to vertices and specified much like colors are (the last normal specified by a call to glNormal is used for a vertex when it is generated, etc).

Normals are specified by glNormal3f(x,y,z), which indicates the 4-vector (x, y, z, 0). Note the last coordinate is 0, not 1. This prevents normals from being effected by translation matrices, which makes sense: the normals of an object do not change when you move it.

Normal vectors are modified by the matrices, but where a vertex position is modified by P M v, the normal is modified by the inverse transpose of the modelview matrix (M–T n). Normals are not modified by the projection matrix.

For speed and numerical stability I suggest storing the inverse of MT and updating it whenever M is updated, but that is up to you.

Example Nothing in this description shows up on the screen until you do Gouraud shading. You can test before then, though, by pretending the normal at each vertex is the color of that vertex.
Credit Required
References The "glNormal" man page.

Functionality Basic Light Equation (aka Lambert materials)
Description Usually, we want a scene to be lit. OpenGL makes this a little harder than it seems like it ought to be, but it's not too bad. Here I will note only the required part of lighting; other pieces are described later.

The main pieces of the lighting equation are the lights and the materials. There are 8 lights; each one has a position and two colors, and each can be enabled or disabled, like so:

	glEnable(GL_LIGHTING);
	glEnable(GL_LIGHT5);
	double diffuse_color[4] = {  0 1, 0.0, 0.0, 1};
	double ambient_color[4] = {  0.2, 0.1, 0.1, 1};
	double position[4]      = { 11  ,-2  , 3  , 1};
	glLightfv(GL_LIGHT5, GL_DIFFUSE, diffuse_color);
	glLightfv(GL_LIGHT5, GL_AMBIENT, ambient_color);
	glLightfv(GL_LIGHT5, GL_POSITION, position);
Note that it is always the case that GL_LIGHTi = GL_LIGHT0 + i, which allows easy array-of-lights access. Also, light positions are modified by the current modelview matrix (but not the current projection matrix) upon creation, just like vertices are.

Materials will be noted in the Gouraud shading requirement.

Given a set of lights with position l_p, diffuse color l_d, and ambient color l_a; and given a surface with position p (after M but before P) and normal n; then the light (a 4-vector) is computed by:

light = sum over all lights
          l_a + max(0, n <dot> unitlength(l_p - p)) *  l_d
This corresponds to diffuse-only (or Lambert) shading from a point light source like a lamp (for non-zero w) or a parallel source like the sun (for zero w) with no attenuation.
Note: unitlength means that you should normalize the vector. For example, for a vector v, v[0] = v[0]/sqrt(v[0]2+v[1]2+v[2]2),
v[1] = v[1]/sqrt(v[0]2+v[1]2+v[2]2), v[2] = v[2]/sqrt(v[0]2+v[1]2+v[2]2).
Example Nothing in this description shows up on the screen until you do Gouraud shading.
Credit Required
References pp. 558-567, 581; see also the "glLight" man page.

Functionality Gouraud shading
Description Gouraud shading is the process of interpolating the light linearly across polygons, the same way you do colors and depth now. This gives you a color and a light at each pixel, which is combined according to the active material.

A material controls how light impacts color. Only three are required:

  • if GL_LIGHTING is not enabled, use the interpolated color and ignore the light.
  • if GL_LIGHTING is enabled but GL_COLOR_MATERIAL is not, the color at each pixel is
      light <elementwise-times> (.8, .8, .8, 1) + (.2, .2, .2, 0)
  • if both GL_LIGHTING and GL_COLOR_MATERIAL are enabled, the color at each pixel is
      light <elementwise-times> color
    Some hardware implementations might add (.2, .2, .2) as well; you may too if you want.
Note that if you don't do the textures elective, you can safely compute the lit color at each vertex and interpolate that instead of interpolating color and light and computing the lit color at each pixel. If you do do textures, that shortcut will not work.

In OpenGL it is possible to disable Gouraud shading by specifying glShadeModel(GL_FLAT), in which case the renderer uses the same lit color for all pixels in the polygon. You are not asked to implement this functionality, though you are free to do so if you want.

Example You'll want to make sure the normals make sense for the vertices; a unit sphere is one of the easiest ways to do this because its normals and its vertices are the same (note, I have it centered at the origin; make sure you move it into view using a translation matrix):
dp = pi/16 // 16 picked arbitrarily; try other numbers too
glBegin(GL_QUADS)
for theta in 0..2pi by dp
  for phi in 0..pi by dp
    glNormal3f(cos(theta)*sin(phi), cos(phi), sin(theta)*sin(phi))
    glVertex3f(cos(theta)*sin(phi), cos(phi), sin(theta)*sin(phi))
    glNormal3f(cos(theta+dp)*sin(phi), cos(phi), sin(theta+dp)*sin(phi))
    glVertex3f(cos(theta+dp)*sin(phi), cos(phi), sin(theta+dp)*sin(phi))
    glNormal3f(cos(theta+dp)*sin(phi+dp), cos(phi+dp), sin(theta+dp)*sin(phi+dp))
    glVertex3f(cos(theta+dp)*sin(phi+dp), cos(phi+dp), sin(theta+dp)*sin(phi+dp))
    glNormal3f(cos(theta)*sin(phi+dp), cos(phi+dp), sin(theta)*sin(phi+dp))
    glVertex3f(cos(theta)*sin(phi+dp), cos(phi+dp), sin(theta)*sin(phi+dp))
glEnd()
Credit Required
References pp. 591-597

Elective Functionality

To earn the remaining 50 points for full credit on this lab, choose from among the following functionalities to implement.
Functionality Back-face culling
Description One simple way to speed up rendering is to throw away half of the triangles before you draw any of them. Triangles can be defined to have a front and a back; discard all of those that have their back face facing the camera.

To get credit for this, if GL_CULL_FACE is enabled and the x, y coordinates of a polygon (after division by w) come in clockwise order, stop processing the polygon. This corresponds to back-face culling with counter-clockwise front faces.

For additional credit, allow the user to choose between clockwise (glFrontFace(GL_CW)) and counter-clockwise (glFrontFace(GL_CCW)) front faces and between culling front (glCullFace(GL_FRONT)), back (glCullFaceGL_BACK)), or all (glCullFace(GL_FRONT_AND_BACK)) polygons.

An easy way to do this is to make two vectors between the points and take their crossproduct to find which way (clockwise or counter-clockwise) the triangle is wound. Talk to the TA for more details.

Example Place some faces wound one way, others wound the other.
Credit 10 for basic or 20 for fully customizable culling.
References pp. 530-531; see also the glCullFace and glFrontFace man pages.

Functionality Specularity
Description Objects can be shiny. OpenGL uses the Blinn-Phong lighting model to approximate those shines.

This elective involves adding functionality to your lighting model. In particular,

  • glLightfv(GL_LIGHTi, GL_SPECULAR, float[4]) specifies the shine color of a light.
  • glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, float) specifies the shininess of a material as a number between 0 and 128; larger numbers give smaller, crisper shine spots, while smaller numbers give larger, fuzzier shines.
  • glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, float[4]) specifies the shine color of an object
glMaterial calls are like glNormal and glColor calls; each vertex inherits the values of the last glMaterial call. Thus, we now have, for each vertex, a color, normal n, position p, specular color v_s, and shininess h. We compute the light as before, but in addition compute a shine color (given a set of lights with position l_p and specular color l_s):
specColor = sum over all lights of
              if (n dot unitlength(l_p - p) > 0){
                halfway = unitlength(unitlength(l_p - p) + (0,0,1,0));
                specColor += max(0, (n dot halfway)h) * v_s <elementwise-times> l_s;
              }
This specular color is interpolated directly across the polygon and added to the lit color at each pixel. Again, if you are not doing textures you can simply add this to the single color you are interpolating in advance.

This corresponds to the Blinn approximation of the Phong model of specular shading from a point light source like a lamp (for non-zero w) or a parallel source like the sun (for zero w) with no attenuation.

The vector (0,0,1,0) is an approximation of a vector from the object to the camera. You can use the actual vector to get a better shine if you are using perspective projection.

Note: unitlength means that you should normalize the vector. For example, for a vector v, v[0] = v[0]/sqrt(v[0]2+v[1]2+v[2]2),
v[1] = v[1]/sqrt(v[0]2+v[1]2+v[2]2), v[2] = v[2]/sqrt(v[0]2+v[1]2+v[2]2).

Example The unit sphere should now have a shiny area.
Credit 15
References pp. 591-597; see also the Wikipedia articles on Phong and Blinn lighting.

Functionality Phong shading
Description Implement Phong shading (not to be confused with Phong lighting). This applies only when lighting is enabled; when lighting is disabled, it should be treated as standard Gouraud shading.

The Phong shading model interpolates normals instead of lights and runs the lighting equation at each and every pixel. It is enabled by calling glShadeModel(GL_PHONG_WIN=0x80EA) (which might be defined already on your computer if you have a windows machine of the right age) and disabled via glShadeModel(GL_SMOOTH).

Implementing this can be a bit of a mess. I suggest you make a separate copy of your polygon drawing method(s) for this elective, since you will still need the old versions. The new copy will need, for each vertex,

  • Screen-coordinate positions for interpolation (V P M v)
  • World-coordinate positions for lighting (M v)
  • Normals for lighting (M–T n)
  • Color, specular color, and shininess
All of these values will be interpolated across the polygon. At each pixel, they will be used to light that pixel according to the Lambert and Blinn-Phong lighting model. Note you will need to re-normalize the normal at each pixel to get the light to show up right, but do not use the renormalized normal for interpolation—it won't work right at all.

Note that Phong shading is not in the OpenGL spec, nor even a common extension to it; Windows has a (little-used) OpenGL extension for it, the WIN_phong_shading extension, which this elective is based on. However, it is very common in 3D graphics packages as it is generally much faster than raytracing and quite a bit nicer looking than Gouraud shading.

Recent 3D games often have some type of Phong shading implemented via the combination of a vertex shader and a fragment shader. Hardware shader languages are somewhat beyond the scope of this project, but are basically a means of replacing the hardware-defined default behavior of the graphics card with custom code. Most of the algorithms you implement in Projects 3 and 4 are used frequently by people who write shader programs.

Example The specular highlights on the sphere should now show up as nice smooth circles.

A point light source near the center of a large polygon should illuminate the center much more than the edges with Phong shading, but not with Gouraud shading.

Credit 20, if and only specular materials are also implemented.
References pp. 591-597

Functionality Fog
Description Almost all real-time graphics of outdoor scenes will have fog. Trying to draw all the way to the horizon creates problems with fixed-point depth buffers and involves so many triangles that drawing can take a long time. Rather than have things simply disappear past a certain depth, it is nice to have fog slowly obscure them.

There are three different ways fog can be applied in OpenGL. You will receive credit for each one you choose to implement. For all of them you will use the same color, specified by glFogfv(GL_FOG_COLOR, float[4]) and will compute a number g based on the z value of each vertex before going through the P matrix. Given a pre-fogged pixel color pc (combining all lighting, texturing, etc) and a fog color fc, the color you draw is g * pc + (1 – g) * fc.

  • glFogf(GL_FOG_MODE, GL_LINEAR) anticipates two additional floating-point values
    • start, which is 0.0 unless the user calls glFogf(GL_FOG_START, start), and
    • end, which is 1.0 unless the user calls glFogf(GL_FOG_END, end).
    Then g = (end - |z|) / (end - start).
  • glFogf(GL_FOG_MODE, GL_EXP) anticipates one additional floating-point value
    • density, which is 1.0 unless the user calls glFogf(GL_FOG_DENSITY, density).
    Then g = exp(-(density * |z|)).
  • glFogf(GL_FOG_MODE, GL_EXP2) anticipates one additional floating-point value
    • density, which is 1.0 unless the user calls glFogf(GL_FOG_DENSITY, density).
    Then g = exp(-(density * |z|)2).
Note that OpenGL allows some slight leeway in how z is computed, so don't worry if your fog isn't exactly like the OpenGL version.
Example Distant objects should be obscured by fog. Make sure your projection matrix contains nothing except a perspective matrix; no rotation, scaling, or translation.
Credit 10 for only one mode, 15 for two modes, or 20 for all three.
References See the "glFog" man page.

Functionality Textures
Description Frequently, the quality of a rendered image has more to do with the quality of the textures than any other single factor. That said, this class is not about generating or reading texture files; we just want to be able to render them, if present. This involves several elements.
  • Image specification
    • Images are specified as flat arrays of values.
    • 2D RGBA float arrays are specified by
    •   glTexImage2D(GL_TEXTURE_2D, 0, 4, width, height, 0, GL_RGBA, GL_FLOAT, float[width*height*4])
    • Width and height must be powers of 2.
    • The float array is accessed much like the raster we have been using throughout.
  • Texture coordinates computation
    • Specify texture coordinates via glTexCoord2f(s, t). This gives a texture coordinate vector of (s, t, 0, 1).
    • Texture coordinates are like colors in most respects for assignment, interpolation, etc.
    • Texture coordinates are multiplied through the texture matrix, if you have one, before being assigned to each vertex.
    • No matter how large the texture is, it is scaled to fit within the square of texture coordinate (0, 0) through (1, 1).
    • Texture coordinates outside the [0,1] range are either
      • Wrapped ( x = x - floor(x) ); default behavior, specified by
          glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_[S or T], GL_REPEAT).
      • Clamped ( x = min(max(x,0),1) ); specified by
          glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_[S or T], GL_CLAMP).
    • Wrapping or clamping happens per pixel, not at texture coordinate specification time.
  • Texel identification
    • A texel is the name of a pixel in the texture image.
    • There are many ways OpenGL has for identifying one or more texel for a given texture coordinate.
    • We will only do one: scale the (wrapped or clamped) texture coordinate by (width, height) and round to the nearest integer texel value.
    • To get OpenGL to do this too, call
      	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
      	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST);
      Miss this step and textures will simply fail to show up in OpenGL.
  • Texture application
    • Given a pixel with a light, a color, a texture color (taken from the texture image at the given texel), and a specular color,
    • Display the color specColor + light <etimes> combine( color, tcolor )
    • Combination happens according to the mode specified by glTexEnvi(GL_TEXTURE_ENV_MODE, GLenum)
      • In GL_MODULATE mode, do an elementwise multiply of texture and interpolated colors.
      • In GL_DECAL mode, the new RGB is the texture RGB times the texture alpha plus (the interpolated RGB times (one minus the texture alpha)); the new alpha is the interpolated alpha.
      • We aren't using GL_BLEND mode.

Texturing can enabled and disabled with parameter GL_TEXTURE_2D.

Credit 15 for the basic functionality (e.g., RGBA 2D repeat and modulate). +5 for decal mode, and +5 for clamped borders.
You can earn an additional 5 points for doing 1D textures, 10 for doing 3D textures.
You can also earn 5 each for doing GL_LUMINANCE and GL_RGB texture formats.
You can also earn 10 each for doing GL_LINEAR min filter and GL_LINEAR mag filter.
Example A good 64x64 texture for testing purposes might be
float tex[64*64*4];
for x in 0..64
  for y in 0..64
    tex[((y)*64 + x)*4 + 0] = (x>>4)&1;
    tex[((y)*64 + x)*4 + 1] = (y>>4)&1;
    tex[((y)*64 + x)*4 + 2] = abs(x+y - 63) / 63.0;
    tex[((y)*64 + x)*4 + 3] = ((x^y)&3) / 3.0;
References pp. 628-634; see also the "glTexImage2D", "glTexParameter", and "glTexEnv" man pages, particularly for the 1D and 3D textures, other formats, and linear filters.

Functionality Normal auto-normalization
Description OpenGL does not normalize normals by default. A longer normal will be more brightly lit than a shorter normal. This is rarely desirable, but if you supply only unit-length normals it is faster to skip the normalization step.

If you enable GL_NORMALIZE, all normals will be scaled to length 1 after multiplication by the inverse transpose modelview matrix. Add this functionality to your project.

Example Scale up all your normals. With GL_NORMALIZE enabled, nothing will change. Without it, the lights will appear to get brighter.
Credit 5
References

Functionality Perspective-Correct Interpolation
Description For this elective you will need to not divide w by itself in the divide-by-w stage of the last project.

When perspective is used, simple linear interpolation of attributes across a surface will not produce correct results. As described in the referenced paper, there is an easy fix for this (note, the paper assumes z=w and uses z for its discussion; z will only work sometimes in OpenGL, but w always works).

Interpolate x and y and z the same way you always have.

Linearly interpolate (1 / w) to each pixel.

For every other value (color, normal, texture coordinates, etc) you are interpolating, linearly interpolate (value / w) to each pixel, then divide by (1 / w) to get the correct value.

Example Create a quad/triangle (with either glFrustum or gluPerspective active) which has a white edge close to the camera and a black edge/vertex far away and a different color. With perspective-correct, the center of the shape will be considerably lighter than it will without it.

The effect of this elective may also very easily be seen with textures.

Credit 10
References Perspective-Correct Interpolation

Functionality Picking
Description Often you want to know what object a user clicked on with the mouse. You can do this with raycasting or with the select buffer.

Raycasting
Using the notes from the Raytracing slides (see the slides section), create a ray from the camera to the mouse and intersect it with all objects in the scene. Simple to say, a bit trickier to do, but very versatile; it forms the basis of many collision detection algorithms too.

The select buffer
OpenGL provides a method for seeing what would be rendered without actually rendering it. Two such interfaces are provided; the feedback buffer, which is useful for creating print-quality images in postscript or other vector-based formats, and the select buffer, which we will describe here.

Select mode is enabled by calling glRenderMode(GL_SELECT) and disabled by glRenderMode(GL_RENDER). Rather than describe the full functionality of select mode, consider the following code snippet:

uint32 sbuffer[4*numberObjects];
glSelectBuffer(4*numberObjects, sbuffer); // tell OpenGL where to put results
glRenderMode(GL_SELECT);                   // enter select mode
glInitNames();                             // tell OpenGL to do necessary internal bookkeeping
foreach object
  glLoadName(numberOfObject);              // tell OpenGL what to put in our array
  // place render code for object here
int objectsHit = glRenderMode(GL_RENDER);  // get hit number and go to standard render mode
if (objectsHit < 0)
  error "the select buffer was too small";
else
  for (0 <= k < objectsHit){
    uint32 shouldBeOne = sbuffer[0+4*k];   // the depth of the name stack, always 1 for us
    float minDepthHit = sbuffer[1+4*k] / float(unit32_max);
    float maxDepthHit = sbuffer[2+4*k] / float(unit32_max);
    uint32 numberOfObject = sbuffer[3+4*k];
  }
Given the above, the remaining trick is to modify the frustum or add clipping planes such that only the (immediate neighborhood of the) pixel under the cursor is rendered.
Example When the user mouses over/clicks on an object, have that object disappear/change color/ do something else.
Credit At least 15 points if it works in either your renderer or OpenGL's.
Another 5 if it works in both renderers.
Another 10 if you can do both raycasting and select buffers.
Additional points if something cool happens to the objects when picked, the definition of cool and the value thereof to be determined arbitrarily and subjectively without justice or fairness by the TA.
References

Functionality Pick your own
Description This is the last standard OpenGL project. What do you wish it did? Make it do it. This includes all of the rest of OpenGL (including the remaining parameters for functions already implemented, spotlights, light attenuation, the glMap family of surface definition algorithms, gluTesselator, etc) and anything else you want a rasterizing rendering engine to do.
Example See the TA if you want help testing your elective.
Credit As arranged in advance with the TA (items not discussed with the TA in advance will get 0).
References See the TA if you want help understanding your elective.


E-mail: leemhoward@gmail.com