Lighting & Shading

By default, OpenGL renders the colour of a primitive depending on the value set at the vertices by glColor*(). This produces a very artificial view of the scene. In reality, the apparent color of objects depends on three things;

  1. The material of the object (material colour, shininess)
  2. The light falling on the object (colour & type of light)
  3. Relative position and orientation of the surface to the light(s) & to the viewer.

In order to display lighting effects, lighting needs to be enabled (lighting is disabled by default). When lighting is enabled, OpenGL ignores the effects of glColor*() [unless glColorMaterial() is enabled).

OpengGL allows us to set various properties of an objects material as well as the lighting of the scene. OpenGL will use these settings in a lighting model to attempt the render each part of the scene accurately give the various material and lighting settings.

The lighting model is calculated at the vertices and the results used to colour the faces.

Check out this application to demonstrate the effects of materials and lighting.

Rough Definitions

Ambient light: Light bouncing around a scene (usually assumed constant throughout a scene).

Diffuse Light: Light direct from a light source.

Specular Reflection: Very bright intense spot of light reflected from shiny objects.

Material [glMaterial*()]

Diffuse and Ambient Reflection

The apparent color of an object is a result of the object reflecting or absorbing the light that strikes it. A 'Red' object appears red because it absorbs the Green and blue components of the light and reflects most of the red light. A red object under a blue light appears black, as there is no red light to reflect.

OpenGL allows us to specify how much red, green and blue light is reflected for both ambient and diffuse light. For most materials the setting for ambient and diffuse reflection is the same.

GLfloat materialAmbDiff[] = {0.9f, 0.1f, 0.1f, 1.0f}; // create an array of RGBA values


glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, materialAmbDiff); 
	// set the diffuse & ambient reflection colour for the front of faces

Specular Reflection

There are two settings to consider for specular reflection;

  1. Colour of bright spot (this should be white for most materials).
  2. Size of the specular spot (shininess). Smooth surfaces have a wide dull specular spot, polished surfaces have a small bright spot.
GLfloat materialSpecular[] = {1.0f, 1.0f, 1.0f, 1.0f}; // create an array of RGBA values (White)
GLfloat materialShininess[] = {128.0f}; // select value between 0-128, 128=shiniest


glMaterialfv(GL_FRONT, GL_SPECULAR, materialSpecular); // set the colour of specular reflection
glMaterialfv(GL_FRONT, GL_SHININESS, materialShininess); // set shininess of the material

Once material properties have been set, all rendered surfaces will take these settings, until glMaterial*() is called again.

A list of relectivity settings for some materials is available here.

 

The windows application at H:\Graphics\OpenGLTutorials\lightmaterial.exe allows you to dynamically play with material settings. Drag the mouse over the green numbers to change them. Right click on the model, to change it.

 

Lights

Enabling Lighting

If lighting is disabled, vertices are give the current colour. To enable lighting calculations;

glEnable(GL_LIGHTING); // switch on lighting
...
glDisable(GL_LIGHTING); // switch off lighting

Lighting can be switched on and off in the same scene, e.g. display text would normally be rendered without lighting, while the scene should be lit.

By default, there is a small amount of ambient light in the scene, so objects are visible, even if no light sources are defined. Global ambient light can be changed using glLightModel*().

Creating Light Sources

OpenGL supports at least 8 individual light sources (known as GL_LIGHTo, GL_LIGHT1, ..., GL_LIGHT7). Lights need to be enabled before their effects are seen.

glEnable(GL_LIGHT0); // switch on light0

The most important properties of a light are it's colour and position. The following code gives GL_LIGHT0 a Blue colour and moves it high on the y-axis;

GLfloat light_color[]={0.0, 0.0, 1.0, 1.0};
GLfloat light_position[]={0.0, 10.0, 0.0, 0.0};


glLightfv(GL_LIGHT0, GL_DIFFUSE, light_color); // set color of diffuse component
glLightfv(GL_LIGHT0, GL_SPECULAR, light_color); // set color of specular component


glLightfv(GL_LIGHT0, GL_POSITION, position);   // set position

The function glLight*() can be used to set many more light settings, such as attenuation, spot light properties, additional ambient light.

Stationary Light Sources

OpenGL treats the position and direction of a light source just as it treats the position of a geometric primitive. In other words, a light source is subject to the same matrix transformations as a primitive. More specifically, when glLight*() is called to specify the position or the direction of a light source, the position or direction is transformed by the current modelview matrix and stored in eye coordinates. This means you can manipulate a light source's position or direction by changing the contents of the modelview matrix stack.

Moving Light Sources

Now suppose you want to rotate or translate the light position so that the light moves relative to a stationary object. One way to do this is to set the light position after the modeling transformation, which is itself changed specifically to modify the light position. You can begin with the same series of calls in an init() routine early in the program. Then, probably within an event loop, you need to perform the desired modeling transformation (on the modelview stack) and reset the light position. Here's what such code might look like:

void display(GLint spin)
{
    GLfloat light_position[] = { 0.0, 0.0, 1.5, 1.0 };

    glPushMatrix();
        glTranslatef(0.0, 0.0, -5.0); 
        glPushMatrix();
            glRotated((GLdouble) spin, 1.0, 0.0, 0.0);
            glLightfv(GL_LIGHT0, GL_POSITION, light_position);
        glPopMatrix();
        auxSolidTorus();
    glPopMatrix();
	
	...
}

The windows application at H:\Graphics\OpenGLTutorials\lightposition.exe will let you play with relative position of the light. Right Click on the model, to change it.

Surface Orientation (specifying vertex normals)

An important factor in calculating the appearance of a surface with respect to light sources is the relative orientation of the surface. A normal vector a is a vector perpendicular to a surface. A normal vector should be specified for each vertex. OpenGL performs lighting calculations for each vertex and interpolates the vertex lighting across the polygon to which it belongs.

Each vertex needs a surface normal vector

Specifying a normal for a vertex is simple, just call glNormal*() before specifying the vertex;

  GLfloat normalA[]={1.0,1.0,0.0);  // fill an array with 3 components of a vector
  
  glNormal3fv(normalA); 		// specify the current normal
  glVertex3f(1.0,2.0,5.0);	// specify a vertex
  

Calculating Surface Normals

 

void NormalVector(GLfloat p1[3], GLfloat p2[3], GLfloat p3[3], GLfloat n[3]){
	
	GLfloat v1[3],v2[3]; // two vectors
	
	//calculate two vectors lying on the surface
	// v1=p2-p1
	// v2=p3-p2
	
	for(int i=0;i<3;i++){
		v1[i]=p2[i]-p1[i];
		v2[i]=p3[i]-p2[i];
	}
	
	// calculate cross product of two vectors ( n= v1 x v2)
	n[0]=v1[1]*v2[2] - v2[1]*v1[2];
	n[1]=v1[2]*v2[0] - v2[2]*v1[0];
	n[2]=v1[0]*v2[1] - v2[0]*v1[1];
	
} //done

Putting it all together

This code displays a lit rotating cube (here is the win32 .exe).


bool Prog_Init()
{
	////////////////////////////////////
	//your initialization code goes here
	////////////////////////////////////
	
	glEnable(GL_DEPTH_TEST); // check for depth
	glEnable(GL_NORMALIZE);	// automatically convert normals to unit normals


	glEnable(GL_LIGHTING); // switch on lighting
	glEnable(GL_LIGHT0); // switch on light0

	GLfloat light_color[]={1.0, 1.0, 1.0, 1.0};
	GLfloat light_position[]={10.0, 10.0, 10.0,0.0};


	glLightfv(GL_LIGHT0, GL_DIFFUSE, light_color); // set color of diffuse component
	glLightfv(GL_LIGHT0, GL_SPECULAR, light_color); // set color of specular component

	glLightfv(GL_LIGHT0, GL_POSITION, light_position);   // set position


	return(true);//return success
}

//////////////////////////////////////////////////////////////////////////////
//CLEANUP
//////////////////////////////////////////////////////////////////////////////
void Prog_Done()
{
	//////////////////////////
	//clean up code goes here
	//////////////////////////
}

//Calculate a normal to a the surface containing 3 points
//last parameter is an array to be filled with components of the normal vector
//A normal vector is the cross product of two vectors lying on the surface
//NOTE: code does not produce a unit normal!!

void NormalVector(GLfloat p1[3], GLfloat p2[3], GLfloat p3[3], GLfloat n[3]){
	
	GLfloat v1[3],v2[3]; // two vectors
	
	//calculate two vectors lying on the surface
	// v1=p1-p2
	// v2=p3-p2
	
	for(int i=0;i<3;i++){
		v1[i]=p2[i]-p1[i];
		v2[i]=p3[i]-p2[i];
	}
	
	// calculate cross product of two vectors
	n[0]=v1[1]*v2[2] - v2[1]*v1[2];
	n[1]=v1[2]*v2[0] - v2[2]*v1[0];
	n[2]=v1[0]*v2[1] - v2[0]*v1[1];
	
} //done
//////////////////////////////////////////////////////////////////////////////
//MAIN GAME LOOP
//////////////////////////////////////////////////////////////////////////////


void Prog_Loop()
{
	///////////////////////////
	//main game logic goes here
	///////////////////////////
	
	static float angle =0.0f; // angle of rotation (in degrees)


	
	// Prepare to draw
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);//clear buffers
	glMatrixMode(GL_MODELVIEW); // reset modelview matrix
	glLoadIdentity();
	

	if(!stopped)angle+=0.2f; // increase angle of rotation
	
	if(angle>=360.0f){angle-=360.0f;} // reset angle

	glTranslatef(0.0f,0.0f,-5.0f); // move everything back 5 units ...
	glRotatef(30, 1.0f, 0.0f, 0.0f); //... then rotate everything a little around x-axis

	glRotatef(angle, 0.0f, 1.0f, 0.0f); //... then rotate everything around y-axis


	// points of a cube
	static GLfloat points[][3]={	{1.0f,1.0f,1.0f},
									{1.0f,1.0f,-1.0f},
									{-1.0f,1.0f,-1.0f},
									{-1.0f,1.0f,1.0f},
									{1.0f,-1.0f,1.0f},
									{1.0f,-1.0f,-1.0f},
									{-1.0f,-1.0f,-1.0f},
									{-1.0f,-1.0f,1.0f}};

	GLfloat normal[3];

	GLfloat materialAmbDiff[] = {0.9f, 0.5f, 0.3f, 1.0f}; // create an array of RGBA values


	glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, materialAmbDiff); // set the diffuse & ambient reflection colour for the front of faces

	GLfloat materialSpecular[] = {1.0f, 1.0f, 1.0f, 1.0f}; // create an array of RGBA values (White)
	GLfloat materialShininess[] = {128.0f}; // select value between 0-128, 128=shiniest


	glMaterialfv(GL_FRONT, GL_SPECULAR, materialSpecular); // set the colour of specular reflection
	glMaterialfv(GL_FRONT, GL_SHININESS, materialShininess); // set shininess of the material


	// there are more efficient ways of specifying vertexes and normals, 
	// we'll look at those later in the course
	glBegin(GL_QUADS);
		//top 
		
		NormalVector(points[0],points[1],points[2],normal);
		glNormal3fv(normal);
		glVertex3fv(points[0]); glVertex3fv(points[1]); glVertex3fv(points[2]); glVertex3fv(points[3]);
		
		//front
		NormalVector(points[0],points[3],points[7],normal);
		glNormal3fv(normal);
		glVertex3fv(points[0]); glVertex3fv(points[3]); glVertex3fv(points[7]); glVertex3fv(points[4]);

		//back
		NormalVector(points[1],points[5],points[6],normal);
		glNormal3fv(normal);
		glVertex3fv(points[1]); glVertex3fv(points[5]); glVertex3fv(points[6]); glVertex3fv(points[2]);

		//left
		NormalVector(points[3],points[2],points[6],normal);
		glNormal3fv(normal);
		glVertex3fv(points[3]); glVertex3fv(points[2]); glVertex3fv(points[6]); glVertex3fv(points[7]);

		//right
		NormalVector(points[1],points[0],points[4],normal);
		glNormal3fv(normal);
		glVertex3fv(points[1]); glVertex3fv(points[0]); glVertex3fv(points[4]); glVertex3fv(points[5]);

		//bottom
		NormalVector(points[4],points[7],points[6],normal);
		glNormal3fv(normal);
		glVertex3fv(points[4]); glVertex3fv(points[7]); glVertex3fv(points[6]); glVertex3fv(points[5]);

	glEnd();

	// finished drawing, show our work	
	SwapBuffers(g_hdc); // bring backbuffer to foreground
}



Further Reading

OpenGL Programming Guide (Red Book) Chapter 6 - Lighting