are you ready for a mindfuck ? are you ready for a mindfuck?

....::::Menu::::....
...::About::...
...::Articles::...
...::Contact::...
...::Home & News::...
...::Links & Credits::... OpenGL logo
Valid XHTML 1.0!

Rendering the Stanford bunny

by Elie De Brauwer

Goal of this file

This file covers the creation of a 3D object using Vertex Arrays and Lighting in three steps.

History of this file

  • 27 june 2004: created
  • 28 june 2004: continued
  • 29 june 2004: continued & finished

Step 0: About

History

First a word of gratitude towards the people of the Stanford 3D Scanning Repository who scanned the orignal terra cotta in 1993-1994 and provided the internet/graphics community with this well known test model. The history of the bunny is available here.

bunny.h

For the purpose of this article I have created a bunny.h file. This file contains the data to create the bunny the data originates from the original archives from Stanford and are based on the bunny.ply file from this site. I just took the needed information vertex coordinates and triangle definition from that file. I used Greg Turk's ply tools to calculate the vertices' normal vectors. Basicly the structure of bunny.h can be summarized to:

#define NUM_POINTS 35947
#define NUM_TRIANGLES 69451

GLfloat bunny[]={
-0.0378297,0.12794,0.00447467,
// vertex1x, vertex1y, vertex1z,
// vertex2x, vertex2y, vertex2z,
// add a whole lot of coordinates here
};

GLint triangles [] = { 
20399,21215,21216,
// triangle1vertex1, triangle1vertex2, triangle1vertex3
// triangle2vertex1, triangle2vertex2, triangle2vertex3
// add a whole lot of triangles here 
};

GLfloat normals [] = { 
0.196151,0.972595,-0.124833,
// vertex1normalx, vertex1normaly, vertex1normalz
// vertex2normalx, vertex2normaly, vertex2normalz
// add a whole lot of coordinates here 
};

The bunny consists of 35947 points that create together 69451 triangles. The bunny.h file is 3433645 bytes large which is about 3.3 megabyte. Which is only 400 kilbyte larger than the original bunny.ply file which did not contain the normal vector information. You can include this file in your C/C++ program, your program will get a couple megabytes large but no file IO will be required when running the program and the three arrays are immediatly at your disposal.

Step 1: Wireframe bunny

The code

In the first step we'll connect the dots of the bunny. This is rather basic so jumping to the source right away isn't a bad idea:

#include <GL/glut.h>
#include "../inc/bunny.h"

// Function prototypes
void disp();
void keyb(unsigned char key, int x, int y);

// Is the bunny rotating ?
bool rotate=0;

Nothing complicated happens here, we include the GLUT header and the bunny header containing the bunny data. We define two function prototypes, one for the display callback and one for the keyboard callback. And all that follows is a global bool that we use to animate our model.

int main(int argc, char **argv){
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);
  glutCreateWindow("Stanford Bunny");
  glClearColor(0.0,0.0,0.0,0.0);
  glEnable(GL_DEPTH_TEST);
  glutDisplayFunc(disp);
  glutKeyboardFunc(keyb);
  glutMainLoop();
  return 0;
}

We create a new window that has depth testing enabled since this is a 3D model.

void disp(void){
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  if(rotate==1){
    glRotatef(1,0.0,1.0,0.0);
  }
  glPolygonMode(GL_FRONT_AND_BACK,GL_LINE);
  glBegin(GL_TRIANGLES);
  for(int i=0;i<NUM_TRIANGLES*3;i++){
      glVertex3f(10*bunny[3*triangles[i]]+0.2,10*bunny[3*triangles[i]+1]-1,10*bunny[3*triangles[i]+2]);
  }  
  glEnd();

  glutSwapBuffers();
  glutPostRedisplay();
}

Like usual the magic happens in the display callback. But if you compare the amount of lines to the result you see the power of a well structured programming language. First we clear the color buffer and the depth buffer using glClear after that we rotate the bunny one degree around the Y-axis if the rotate flag is set. Next we use glPolygonMode to define we want to see a wireframe model (filled polygons are showed by default) but since we don't have any lighting we would only see a solid white 2D bunny. The next step will draw all the triangles. Drawing the triangles is a glBegin(GL_TRIANGLES) which contains a for loop this foor loop uses the data in the triangles array in bunny.h. It draws vertices specified in the order they appear in in the triangles array. The coordinates are multiplied by 10, 0.2 is added to the x coordinate and 1 is subtracted from the Y value in order to center the bunny on the screen.

void keyb(unsigned char key, int x, int y){
  if(key=='l'){
    rotate=1;
  }else{
    rotate=0;
  }  
}

If the key pressed is 'l' then we rotate in the other case we don't.

The result

Below you can see two (resized) screenshots of the first step result, click them in order to enlarge them.

Bunny Step 1 Image 1 Bunny Step 1 Image 2
Fig 1: Initial viewFig 2: After a rotation

Step 2: Vertex Arrays

Introduction to Vertex Arrays

To be honest the first step wasn't such a big deal. The only piece of code that might look complicated is the for loop where we were adding some 'mistery' constants. Now OpenGL provides the programmer with something handy called Vertex Arrays. This comes in handy in cases where you have an array of vertex coordinates, normal coordinates, color information, texture coordinates, ... and you want to access these on a transparant way.
You should know that vertex arrays have many purposes and each purpose contains its own solution to the problem, this article can't and won't cover all possible problems and solutions using Vertex Arrays. If you want more information on Vertex Arrays I'd recommend reading pages 67 to 81 of the red book ( The OpenGL Programming guide, the pages match in the 1.2 guide).
Now in this step we'll only use the vertex array. We enable using the Vertex array by a call to void glEnableClientState( GLenum cap); where GLenum is one of: GL_VERTEX_ARRAY, GL_COLOR_ARRAY, GL_INDEX_ARRAY,GL_NORMAL_ARRAY, GL_TEXTURE_COORD_ARRAY or GL_EDGE_FLAG_ARRAY ( void glEnableClientState( GLenum cap); is also available).
So now we've told OpenGL we want to use vertex arrays we should provide OpenGL with the array containing the data it should use. Each of the clientstates has it's own function call to define the data array:

       void glVertexPointer( GLint size,
                             GLenum type,
                             GLsizei stride,
                             const GLvoid *ptr )
       void glColorPointer( GLint size,
                            GLenum type,
                            GLsizei stride,
                            const GLvoid *ptr )
       void glIndexPointer( GLenum type,
                            GLsizei stride,
                            const GLvoid *ptr )
       void glNormalPointer( GLenum type,
                             GLsizei stride,
                             const GLvoid *ptr )
       void glTexCoordPointer( GLint size,
                               GLenum type,
                               GLsizei stride,
                               const GLvoid *ptr )
       void glEdgeFlagPointer( GLsizei stride,
                               const GLvoid *ptr )
-> size      : how many elements are needed to create on (e.g. 3 elements to create one vertex
               when x,y,z coordinates are defined, 2 when only x and y are defined).
-> type      : which is the type of the data e.g. GL_INT for integers GL_FLOAT for floats
-> stride    : the offset between consecutive groups of elements (see interleaved arrays)
-> ptr       : the pointer to the data

At this point we can use void glArrayElement( GLint i ) between a glBegin() and a glEnd() for example to get all the information of the ith element out of all the currently enabled arrays. So when we have a color array and a vertex array enabled glArrayElement(1) would be equale to a call to glVertex3f() and a call to glColor3f(). At this point we could alter the body of the for loop to:

  for(int i=0;i<NUM_TRIANGLES*3;i++){
     glArrayElement(triangles[i]);
  }  

This already looks much better and much more readable but we still have a for loop. OpenGL also provides a method to call all the vertices when you have an array of indices available (which is our triangles array !).Observer the following prototype: void glDrawElements( GLenum mode,GLsizei count,GLenum type, const GLvoid *indices ). Here is the mode each parameter that can be passed to glBegin() in this case it will be GL_TRIANGLES, count is the amount of indices in the array, type is the datatype contained in the array (e.g GL_UNSIGNED_SHORT or GL_UNSIGNED_INT and finally indices is the array. So the for loop becomes:

  glDrawElements(GL_TRIANGLES,3*NUM_TRIANGLES,GL_UNSIGNED_INT,triangles);

Now I don't think it can get any shorter. OpenGL provides some other functions like glDrawRangeElements to draw a range of elements or glDrawArrays() to draw a sequence of geometric primitives. For more information look at the man pages. Another subject I'd briefly like to mention is the subject of Interleaved Arrays, this is an array where we pack all the arrays together in one large interleaved array. (So bunny.h could exist out of a single array). In this case the stride value of the call to gl*Pointer should be set to a value representing the data between two consecutive elements and a call to void glInterleavedArrays( GLenum format,GLsizei stride,const GLvoid *pointer ) should be made to define the structure of the array. Again for more information see the manpages.

The Code

Below you can find the code of the second step. The big differences are that we make use of vertex arrays and that we defined a perspective projection to get a different view of the bunny.

#include <GL/glut.h>
#include "../inc/bunny.h"


void disp();
void keyb(unsigned char key, int x, int y);
void reshape(int x, int y);
bool rotate=0;

We added a reshape callback

int main(int argc, char **argv){
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);
  glutCreateWindow("Stanford Bunny");
  glClearColor(0.0,0.0,0.0,0.0);
  glEnable(GL_DEPTH_TEST);
  glEnableClientState(GL_VERTEX_ARRAY);
  glVertexPointer(3,GL_FLOAT,0,bunny);
  glutDisplayFunc(disp);
  glutKeyboardFunc(keyb);
  glutReshapeFunc(reshape);
  glutMainLoop();

  return 0;
}

A call to glEnableClientState(GL_VERTEX_ARRAY); is made to enable vertex arrays and glVertexPointer(3,GL_FLOAT,0,bunny); defines which array contains the vertex data. The 3 says we have bot x,y and z coordinates, the GL_FLOAT says the array consits of GLfloat variables.

void disp(void){
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  if(rotate==1){
    glRotatef(1,0.0,1.0,0.0);
  }
  glPolygonMode(GL_FRONT_AND_BACK,GL_LINE);
  glDrawElements(GL_TRIANGLES,3*NUM_TRIANGLES,GL_UNSIGNED_INT,triangles);
  glutSwapBuffers();
  glutPostRedisplay();
}

We are still using the wireframe model to show the bunny (one step at a time). But the for loop has been changed to a single call to glDrawElements. If you read the explanation above this speaks for itself.

void keyb(unsigned char key, int x, int y){
  if(key=='l'){
    rotate=1;
  }else{
    rotate=0;
  }  
}

void reshape(int x, int y){
  glViewport(0,0,x,y);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(50,x/(y*1.0),0.1,2.0);  
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();  
  gluLookAt(0.25,0.11,0,0,0.11,0,0,1,0);
}

The keyboard callback hasn't changed and it won't change anymore. The reshape function lets the application use the available viewport performs a perspective projection using gluPerspective and a call to gluLookAt is used to position the eye in front of the bunny.

The Result

When you look at the results below it gives you a more 3D feeling, you can see the bottom and the holes in the bottom. Click on the image for a larger version.

Bunny Step 2 Image 1 Bunny Step 2 Image 2
Fig 3: Perspective projectionFig 4: Another view

Step 3: Lighting

Introduction

Lighting is a very complex topic covering many aspects. The more realism you want to add to your scene the more complex the lighting will become. Most books like the red book (OpenGL Programming Guide) or a general computer graphics book will start to explain the entire lighting model using images, vectors, etcetera. So if you want to know this go get a good book. I will explain the practical part of lighting assuming that you know what a normal vector is and how light reflects. I will explain the basics of using lighting by a little example where we will create a scene of glut primitives and we'll enlighten it. In the next step we'll implement that lighting process on the bunny.

Turning the lights on

1: Initial scene

Consider the following easy to understand program:

#include <GL/glut.h>

void disp();
void init();

int main(int argc, char **argv){
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);
  glutCreateWindow("Turn the light on");
  init();
  glutDisplayFunc(disp);
  glutMainLoop();
  return 0;
}

void init(){
  glClearColor(0.0,0.0,0.0,0.0);
  glEnable(GL_DEPTH_TEST);
}

void disp(){
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  glLoadIdentity();
  glPushMatrix();
  glTranslatef(-0.5,0,0);
  glColor3f(1.0,0.0,0.0);
  glutSolidSphere(0.2,50,50);
  glTranslatef(1,0,0);
  glScalef(0.4,0.4,0.4);
  glColor3f(0.0,1.0,0.0);
  glutSolidIcosahedron();
  glPopMatrix();
  glTranslatef(0,0.5,0);
  glColor3f(0.0,0.0,1.0);
  glutSolidTorus(0.1,0.2,10,100);
  glTranslatef(0,-1.0,0);
  glColor3f(0.0,1.0,1.0);
  glutSolidTeapot(0.2);
  glutSwapBuffers();
}

This programs displays a scene containing of four glut primitives. A sphere, an icosahedron, a torus and a teapot. Each painted in a different color. We have put all the initialisation in a separate init() function.
The result of this program looks like this:

The initial scene without lights

2: Enabling lighting

In this step we enable lights by adding glEnable(GL_LIGHTING) in the init() function. So the init() function now looks like:

void init(){
  glClearColor(0.0,0.0,0.0,0.0);
  glEnable(GL_DEPTH_TEST);
  glEnable(GL_LIGHTING);
}

The output of the program looks like this:

The initial scene with lighting enabled

3: Enabling lights

The previous snapshot looks rather dark. Why's that ? Simple, lighting is enabled but no lights are turned on. You can enable light by calls glEnable(GL_LIGHTi) where i is a number ≥ 0 and ≤ GL_MAX_LIGHTS which depends on the implementation. Now we enable the first two lights by calling glEnable(GL_LIGHT0); and glEnable(GL_LIGHT1);, we also add this in the init() function.

The default position of a light is {0.0,0.0,1.0,0.0} which means the light incoming light comes from the back keeping everything dark. So we define two light coordinates one in the upper left and one in the lower right corner with a z coordinate of -1. Next we set this position with void glLightf( GLenum light,GLenum pname,GLfloat param ) where light is the light (e.g. GL_LIGHT0), pname is the light parameter we want to change (e.g. GL_POSITION) and param is the new value for the parameter. If param is an array call glLightfv also integer variants exist. For more information about the available parameters please refer to the glLight manpage.
The init() function becomes:

void init(){
  static GLfloat l0_pos[] = { 1.0, 1.0,-1.0,1.0};
  static GLfloat l1_pos[] = {-1.0,-1.0,-1.0,1.0};
  glClearColor(0.0,0.0,0.0,0.0);
  glShadeModel(GL_SMOOTH);
  glEnable(GL_DEPTH_TEST);
  glEnable(GL_LIGHTING);
  glEnable(GL_LIGHT0);
  glEnable(GL_LIGHT1);
  glLightfv(GL_LIGHT0, GL_POSITION, l0_pos);
  glLightfv(GL_LIGHT1, GL_POSITION, l1_pos);
}

This results in the following output :

The initial scene with lighting and lights  enabled

4: Configuring the lights

Ok now we can see something we're on our way. But we enabled two lights and only one appears to be shining.
That's because you haven't read the glLight manpage. All the lights that you enable emit no light (GL_DIFFUSE and GL_AMBIENT colors both set to {0.0,0.0,0.0,1.0}). The exception is the first light which has its GL_AMBIENT parameter set to {1.0,1.0,1.0,1.0}. Now we'll change the parameters so GL_LIGHT0 emits white ambient light (the default) and that GL_LIGHT1 emits red light.
The init function now looks like:

void init(){
  static GLfloat l0_pos[] = { 1.0, 1.0,-1.0,1.0};
  static GLfloat l1_pos[] = {-1.0,-1.0,-1.0,1.0};
  static GLfloat l0_col[] = { 1.0, 1.0, 1.0,1.0};
  static GLfloat l1_col[] = { 1.0, 0.0, 0.0,1.0};

  glClearColor(0.0,0.0,0.0,0.0);
  glShadeModel(GL_SMOOTH);
  glEnable(GL_DEPTH_TEST);
  glEnable(GL_LIGHTING);
  glEnable(GL_LIGHT0);
  glEnable(GL_LIGHT1);
  glLightfv(GL_LIGHT0, GL_POSITION, l0_pos);
  glLightfv(GL_LIGHT1, GL_POSITION, l1_pos);
  glLightfv(GL_LIGHT0, GL_DIFFUSE , l0_col);
  glLightfv(GL_LIGHT1, GL_DIFFUSE , l1_col);
}

With the red light enabled we get:

The initial scene with a red and a white light enabled

Other light parameters can be set this way: GL_SPOT_EXPONENT,GL_SPOT_CUTOFF,GL_CONSTANT_ATTENUATION, GL_LINEAR_ATTENUATION and GL_QUADRATIC_ATTENUATION can be set using glLightf() or glLighti() and GL_AMBIENT, GL_DIFFUSE, GL_SPECULAR,GL_POSITION, GL_SPOT_CUTOFF, GL_SPOT_DIRECTION, GL_SPOT_EXPONENT,GL_CONSTANT_ATTENUATION, GL_LINEAR_ATTENUATION and GL_QUADRATIC_ATTENUATION can be set using glLightfv() or glLightiv(). For more information concerning the meaning of this parameters and the default values please consult the manual pages.

5: Material properties

But in the orignal scene each object has it's own color defined using glColor3f() but when lighting is enabled it looks like OpenGL makes each object white. True, when lighting is enabled there's a difference between a red car, a red bal and a red brick. Each are red but reflect the light on their own different way. In order to obtain a certain degree of realism each object should have a defined material in OpenGL.
We can set the material properties under OpenGL with a call to void glMaterialf( GLenum face,GLenum pname,GLfloat param ) which resembles the glLight*() functions. Also suffixes fv,i and iv with obvious results exists. The face is the same face that is passed to glPolygonMode() which can be GL_FRONT,GL_BACK or GL_FRONT_AND_BACK for front, back or front and back facing polygons respectively.
Possible pname values are:GL_AMBIENT, GL_DIFFUSE,GL_SPECULAR, GL_EMISSION, GL_SHININESS, GL_AMBIENT_AND_DIFFUSE or GL_COLOR_INDEXES. For more information about the meaning, options and defaults, consult the manual pages.

void disp(){
  // Color definitions
  static GLfloat sphere_col[] = {1.0,0.0,0.0,1.0};
  static GLfloat icos_col[] =   {0.0,1.0,0.0,1.0};
  static GLfloat torus_col[] =  {0.0,0.0,1.0,1.0};
  static GLfloat tea_col[] =    {0.0,1.0,1.0,1.0};

  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  glLoadIdentity();
  glPushMatrix();
  glTranslatef(-0.5,0,0);
  glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, sphere_col);
  glutSolidSphere(0.2,50,50);
  glTranslatef(1,0,0);
  glScalef(0.4,0.4,0.4);
  glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, icos_col);
  glutSolidIcosahedron();
  glPopMatrix();
  glTranslatef(0,0.5,0);
  glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, torus_col);
  glutSolidTorus(0.1,0.2,10,100);
  glTranslatef(0,-1.0,0);
  glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, tea_col);
  glutSolidTeapot(0.2);
  glutSwapBuffers();
}

Now with the material properties enabled we get the following result:

Lighted scene with material properties set.

Which doesn't look that bad.

End of this scene

Beware, this is only the tip of the iceberg. I've used some glut primitives here who al have their normals defined internally. So when adapting this to our bunny each single call to glVertex*() should have a call to glNormal* to define the coordinates of the normal vector ! Much more options and properties are available but at this point you should be able to get started and understand the rest of this article.
The source of this test scene is also available here.

The Code

#include <GL/glut.h>
#include "../inc/bunny.h"

void disp();
void keyb(unsigned char key, int x, int y);
void reshape(int x, int y);
void init();
bool rotate=0;

The prototype for an init() function has been added.

int main(int argc, char **argv){
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);
  glutCreateWindow("Stanford Bunny");
  init();
  glutDisplayFunc(disp);
  glutKeyboardFunc(keyb);
  glutReshapeFunc(reshape);
  glutMainLoop();

  return 0;
}

All the initialisation has been moved to an init function called init

void init(){
  glClearColor(0.0,0.0,0.0,0.0);
  glShadeModel(GL_SMOOTH);
  glEnable(GL_LIGHTING);
  glEnable(GL_LIGHT0);
  glEnable(GL_DEPTH_TEST);
  glEnableClientState(GL_VERTEX_ARRAY);
  glEnableClientState(GL_NORMAL_ARRAY);
  glVertexPointer(3,GL_FLOAT,0,bunny);
  glNormalPointer(GL_FLOAT,0,normals);
}

This is the initilisation function. We initialise the Vertex Arrays (we enable the client state for an arary of vertices and an array of normal vectors). We also enable lighting and enable the light GL_LIGHT0 the first light. And we tell OpenGL we want smooth shading (or else each triangle would be in a single color, with this model it won't be such a big problem since we have enough and small enough triangles).

void disp(void){
  static int spin=0;

  static GLfloat position [] = {0.0,0.15,0.1,1.0};
  static GLfloat color [] = {0.5,0.25,0.0,1.0};

  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  // Draw the light 
  glPushMatrix();
  glRotatef(spin,0.0,1.0,0.0);
  if(rotate==1){
    spin=(spin+1)%360;
  }
  glLightfv(GL_LIGHT0,GL_POSITION,position);

  // Draw a blue wireframe cube wherle the light is 
  glPushMatrix();
  glDisable(GL_LIGHTING);
  glTranslatef(position[0],position[1],position[2]); 
  glColor3f(0.0,0.0,1.0);
  glutWireCube(0.01);
  glEnable(GL_LIGHTING);
  glPopMatrix();
  glPopMatrix();

  // Draw the bunny
  glPolygonMode(GL_FRONT_AND_BACK,GL_FILL);
  glMaterialfv(GL_FRONT_AND_BACK,GL_AMBIENT_AND_DIFFUSE,color);
  glDrawElements(GL_TRIANGLES,3*NUM_TRIANGLES,GL_UNSIGNED_INT,triangles);

  // Swap the buffers && redraw
  glutSwapBuffers();
  glutPostRedisplay();
}

Now more magic. First we define three static variables the first one is the number of degrees the light has rotated around the Y-axis. Next are 2 arrays (which should be declared const) the first one defines the position of the light and the second one defines the abient and diffuse color of the bunny. After that the buffers are cleared.
Now we clear draw the light but first we make the rotation of the light happen. Next we put the light on the position where we want it.
Now we translate to the position where the light is,we disable lighting and draw the blue wireframe cube where the light is. (This is not true since the position don't exactly match and the lightsource is in fact a dot etcetera but it gives you a good impression about the origin of the light).
After the cube is draw we define the properties of the bunny. First we say we want filled polygons instead of wireframe polygons using a glPolygonMode call, next we set the ambient and the diffuse color of the bunny (a material property) using glMaterialfv(). Note that setting a color with glColor*() won't have any effect.
After that we fall back to step two and draw the bunny.

void keyb(unsigned char key, int x, int y){
  if(key=='l'){
    rotate=1;
  }else{
    rotate=0;
  }  
}

Still unchanged only the rotate flag nog lets the light rotate and not the bunny.

void reshape(int x, int y){
  glViewport(0,0,x,y);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(50,x/(y*1.0),0.1,2.0);  
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();  
  gluLookAt(-0.2,0.11,0.25,0,0.11,0,0,1,0);
}

The reshape function hasn't changed from step 2.

The Result

Below you can see four screenshots of the program with the light on different positions and the bunny colored brown. The position of the light is shown by the blue wireframe cube. Click on the images for a larger version.

Bunny Step 3 Image 1 Bunny Step 3 Image 2
Fig 5: Light position left front Fig 6: Light position right front
Bunny Step 3 Image 3 Bunny Step 3 Image 4
Fig 7: Light on the back Fig 8: Light on the left back

The Code

The bunny sourcecode for each of the three steps and the bunny header file is available here. Due to the amount of points needed to build the bunny this archive has a size of approximately 3.3 megabytes.
The source of this test scene is also available here.