Lab (9) - Introduction to Lighting (Creating a Sphere)

Objectives:

To learn about Lighting in OpenGL
To create a sphere using recursive subdivision of a cube
To use lighting and shading together

General lighting can be very complicated. A light bulb is not a single point of light but rather a surface from which light is emitted. To find the net effect from a light source at a given point in space we would have to integrate all the contributions on the surface. To simplify life, we generally model light as a point source, a spot light with a preferential range of angles, or a light direction. (Think of the sun as an example of the latter.) Also, light should fall off with the square of the distance from the source, but this is rather expensive to calculate. OpenGL supports quadratic, linear, or constant light attenuation; constant is the default (for greatest speed).

We will use the Phong shading model. Light comes in ambient, diffuse, and specular flavors. Ambient light is just a cheap way to bring up the house lights -- we illuminate everything uniformly. (Actually, we will specify coefficents for different materials which specify the fractions of ambient, diffuse and specular light reflected, so we don't have to make everything uniform with ambient light.) Diffuse light models the effect of rough surfaces -- light is scattered equally in all directions, and depends only on the angle between the light vector and the normal vector. It works well to make the diffuse light proportional to the cosine of the angle between the light vector l and the normal vector n, which is just l * n if the two vectors are normalized.

The specular light models the shiny patches that indicate a surface to be hard and reflective, like chrome. Ideally we would find the cosine of the angle between the reflected vector (which makes the same angle with the normal that the light vector does, and which lies in the plane of the light and normal vectors), and raise this to some power (to create a rapid fall-off) to get the specular component. It is computationally more effecient to take the cosine of half this angle, since it is the angle between the normal and the "halfway vector" h which is the average of the light and viewer vectors. If we normalize h, we can raise n * h to a smaller power to obtain the same effect very cheaply.

It is important to realize how OpenGL uses these three components. The normals, light vectors and view vector are used at each polygon vertex to calculate proper colors at the vertices. Then these colors are simply linearly interpolated between vertices, a process called Gouraud shading (or smooth shading). If we interpolated the normals to recalculate the color at each point we would be using Phong shading -- a much more expensive prospect (but appropriate for creating one frame at a time for later sequencing). To get the shiny patches we associate with metal surfaces in OpenGL we must use lots of small polygons -- we won't get the effect in a large polygon.

To illustrate, the following program recursively subdivides a cube to approximate a sphere. A quadrilateral is simply drawn if the recursion  level is down to zero, otherwise it is carved into 4 pieces (with the vertices of each piece rescaled to be the same distance, sqrt(3.0), as the original 8 vertices of the cube) and these are drawn with recursion level one less. We specify specular, diffuse and ambient compenents of the light and material. A keypress increases the recursion depth. Here are the first few images:

 Depth 0 Depth 1 Depth 2 Depth 3 Depth 4 Depth 5

And here is the source code:

// recursive subdivision of cube
#include <math.h>
#include <GL/glut.h>
typedef GLfloat point3[3];
// List all vertices in array and reference later by index
point3 p[8]={
{-1,-1,-1},
{-1,-1,1},
{1,-1,1},
{1,-1,-1},
{-1,1,-1},
{-1,1,1},
{1,1,1},
{1,1,-1}
};
int depth; //recursion depth

void sub(point3 p2,point3 p1,point3 diff){
// diff = p2 - p1
for(int i=0;i<3;i++)diff[i]=p2[i]-p1[i];
}

void findNormal(point3 p1,point3 p2,point3 p3,point3 n){
// n = (p2-p1) x (p3-p2)
point3 u,v;
sub(p2,p1,u);
sub(p3,p2,v);
n[0]=u[1]*v[2]-u[2]*v[1];
n[1]=u[2]*v[0]-u[0]*v[2];
n[2]=u[0]*v[1]-u[1]*v[0];
}

void normalize(point3 p){
int i;
GLfloat x=0;
for(i=0;i<3;i++)x+=p[i]*p[i];
for(i=0;i<3;i++)p[i]/=sqrt(x/3);
}

void drawFace(int depth,point3 p1,point3 p2,point3 p3,point3 p4){
// Draw polygon p1 p2 p3 p4, or recursively subdivide
if(depth<=0){
point3 n;
findNormal(p1,p2,p3,n);
glBegin(GL_POLYGON);
glNormal3fv(n);
glVertex3fv(p1);
glVertex3fv(p2);
glVertex3fv(p3);
glVertex3fv(p4);
glEnd();
}
else{
point3 p,p34,p23,p12,p41;
int i;
for(i=0;i<3;i++){
p[i]=(p1[i]+p2[i]+p3[i]+p4[i])/4;
p34[i]=(p3[i]+p4[i])/2;
p23[i]=(p2[i]+p3[i])/2;
p12[i]=(p1[i]+p2[i])/2;
p41[i]=(p4[i]+p1[i])/2;
}
// make new points same distance from origin as original points
normalize(p);
normalize(p34);
normalize(p23);
normalize(p12);
normalize(p41);
// draw 4 faces in place of 1
drawFace(depth-1,p,p23,p3,p34);
drawFace(depth-1,p,p34,p4,p41);
drawFace(depth-1,p,p41,p1,p12);
drawFace(depth-1,p,p12,p2,p23);
}
}

void drawFace(int depth,int i1,int i2,int i3,int i4){
// to pass indices instead of points on initial call
drawFace(depth,p[i1],p[i2],p[i3],p[i4]);
}

void myinit(void)
{
GLfloat mat_specular[] = { 1.0, 1.0, 1.0, 1.0 };
GLfloat mat_shininess[] = { 50.0 };
GLfloat light_position[] = { 2.0, 4.0, 2.0, 0.0 };
/* For real-world objects, diffuse and ambient reflectance are normally
the same color. For this reason, OpenGL provides you with a convenient
way of assigning the same value to both simultaneously with glMaterial*():
*/
GLfloat mat_amb_diff[] = { 0.1, 0.5, 0.8, 1.0 };
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE,
mat_amb_diff);
glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);
glMaterialfv(GL_FRONT, GL_SHININESS, mat_shininess);
glLightfv(GL_LIGHT0, GL_POSITION, light_position);
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
glClearColor(1.0, 1.0, 1.0, 1.0); /* white background */
/* set up viewing */
glMatrixMode(GL_PROJECTION);
double w=.1;
glFrustum(-w,w,-w,w,.2,10);
}

void drawCube(int depth)
{
drawFace(depth,0,3,2,1); //bottom
drawFace(depth,1,2,6,5); //front
drawFace(depth,2,3,7,6); //right side
drawFace(depth,0,1,5,4); //left side
drawFace(depth,3,0,4,7); //back side
drawFace(depth,4,5,6,7); //top
}

void display( void )
{
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); /*clear the window */
glMatrixMode(GL_MODELVIEW);
gluLookAt(4,2,4,0,0,0,0,1,0);
drawCube(depth);
glFlush(); /* clear buffers */
}

void key(unsigned char k,int x, int y){
if(k==' ')depth++;
else if(depth>0)depth--;
glutPostRedisplay();
}

void main(int argc, char** argv)
{
/* Standard GLUT initialization */
glutInit(&argc,argv);
depth=0;
glutInitDisplayMode (GLUT_SINGLE | GLUT_RGB|GLUT_DEPTH);
glutInitWindowSize(500,500); /* 500 x 500 pixel window */
glutInitWindowPosition(0,0); /* place window top left on display */
glutCreateWindow("Cube"); /* window title */
glutDisplayFunc(display); /* display callback invoked when window opened */
glutKeyboardFunc(key);
myinit(); /* set attributes */
glEnable(GL_DEPTH_TEST); /* Enable hidden--surface--removal */
glEnable(GL_SMOOTH); /* Enable smooth shading and color interpolation */
glEnable(GL_NORMALIZE); /* Normalize normals */
glutMainLoop(); /* enter event loop */
}

Note that if the surface normal is not specified, by simply = commenting out the line

glNormal3fv(n);

in drawFace, then we get a much less interesting picture: