Sunday 29 April 2012

Adding Lighting: Basics

In order to add some basic lighting to the 3D model that I'm now able to load in, the lighting must first be initialised and then applied.

There are three main types of lights that can be simulated with OpenGL: directional, point and spotlight. We will talk about these and how to include them in the program later.

Initialising Lighting

Thankfully OpenGL does a lot of the lighting calculations for us. For every lit vertex in the scene a lighting model known as the Blinn-Phong shading model is used to calculate how much light the vertex is receiving from any light sources in the scene and so is able to decide on the colour at that vertex. A shading model called Gouraud shading is then applied to interpolate the lighting across the face, which means that every pixel across the face is efficiently coloured based on the colour of the nearest vertices and its distance from them. This is how our 2D faces are lit and coloured. Thankfully OpenGL does all this for us so we don't have to worry about (not until shaders I think, anyway!).

So how do we utilise this functionality? A simple call to glEnable(GL_LIGHTING) will turn on lighting for us and the corresponding glDisable(GL_LIGHTING), will turn it off. Every vertex sent to the hardware between these calls will be lit using the method described above. That's it!

There are a number of other options that can alter the appearance of the lighting but for now, this will do nicely.

Creating Lights

So now that we've told OpenGL we want to use lighting, how do we actually include this in our scene? After all, a lit scene with no lights isn't much use at all...

OpenGL allows us the use of 8 lights, each with the symbolic name GL_LIGHT0, GL_LIGHT_1...,GL_LIGHT7. In order to use these lights, we have to call glEnable like before. By default all lights apart from 0 are dark and their colours need to be set. Light 0 however, has a bright white light (1.0, 1.0, 1.0, 1.0) by default. We can just enable Light 0 and use that however it's much more fun to customise it to our needs...

The colour of the lights in OpenGL are mainly defined by three components:

  • Ambient: In real life, light rays bounce around and illuminate every object they collide with, gradually losing energy, this causes a room for example, to be filled with light. Modelling this efficiently in software however proves to be a very hard problem. To simplify matters, OpenGL simply applies a small amount of light to everything in the scene, somewhat simulating all the light rays bouncing around. This can be thought of as the "background lighting".


  • Diffuse: Many objects spread the light they receive in a uniform manner. This means that when light interacts with the object, the light bounces around the object and secondary rays are reflected in what appears to be from all directions away from the surface, providing a nice even light. To approximate this effect, if an object is in the area of a light, the calculations used to colour the surface heavily use the diffuse property of the light. Diffuse lighting can be thought of as the "colour" of the light.


  • Specular: Some objects do not reflect light in a uniform manner, they prefer to reflect light in a particular direction. This type of lighting is called specular reflection and occurs for "shiny" objects such as metals and a mirror. Typically we see this as a small white blob. For objects in our scene that are shiny (more on this when we get to texturing the model), we need to set the colour of the light that will appear reflected in this shiny section of the material. This can be thought of as the colour of the "shininess" of the interaction between the light and the object.


Each of these colour properties are set using three values representing a mix of the Red, Blue and Green colours and a 4th alpha property which comes into play when blending is used. All properties of a particular light are set using the glLight[f/i] and glLight[f/i]v functions (the f means that floats are supplied, i - integers). A typical light setting is applied in the example below:

 float ambientColour[4] = {0.2f, 0.2f, 0.2f, 1.0f};
 float diffuseColour[4] = {1.0f, 1.0f, 1.0f, 1.0f};
 float specularColour[4] = {1.0f, 1.0f, 1.0f, 1.0f};
 
 glLightfv(GL_LIGHT0, GL_AMBIENT, ambientColour);
 glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuseColour);
 glLightfv(GL_LIGHT0, GL_DIFFUSE, specularColour);

The 'v' form of the glLight function means you are providing an array argument. This should be a float or integer array of size four, describing the RGBA values. This is enough to configure the colouring of our simple light!

Positioning the Light

The final step of creating this light is to specify where in the scene the light is to be placed. This step is also used for stating the type of light that we want to use. In order to position the light, we call glLight again, providing it with GL_POSITION this time. This again is an array of size four with the first three floats representing the x, y and z coordinates in space in the world in which our camera will be placed. What then, is the 4th float used for?

The fourth float is when we finally get to specify the type of light that we want OpenGL to provide us with. As we said before, there are three types of light that we can utilise: Directional, point and spotlight.

A directional light is a light that originates from an infinite point in the distance. You define a vector which defines the direction from which the light emanates towards the world origin. All rays from the light can then be thought as being parallel to the direction vector defined, basically a wall of light. This light source approximates a large uniform light source, such as the sun.

As an example, we can define a directional light source with the following coordinates.

float position[4] = {-5.0f, 0.0f, 0.0f, 0.0f};

glLightfv(GL_LIGHT0, GL_POSITION, position);

While the coordinates {0, 0, -5} define a direction, it is easier to think of it as the position of the light but with the vector extended into infinity. The following figure hopefully demonstrates the concept a little better. A sphere is located at the world origin {0, 0, 0}, another at {-10, 5, 0}. The light direction is defined as {0, 0, -5}, denoted by the small red cube. The light then shines toward the origin parallel to this vector, from infinitely far away.



The second light type that we can include in our scene is the point light. Rather than being a global light like the directional light, the point light is a local light, an example of which would be a light-bulb. Like the directional light, you define a point light by using an array of size four however this time, the final member is 1 instead of 0, making it a point light. The other difference is that rather than defining a light direction, the first 3 parts of the array define a point in the world where the light is placed. Think of it as positioning any other object in your scene.

float position[4] = {-5.0f, 0.0f, 0.0f, 1.0f};

glLightfv(GL_LIGHT0, GL_POSITION, position);

In this example, we can see that the lighting effect is different. Rather than emanating from an infinite point towards the world origin, the light emanates from the position of the light outwards in all directions.


The final type of light is the spotlight. The spotlight is a specialized form of the point light in that the arc from which light emanates is limited to a cone. We see this type of light in lamps and torches (flash-lights). Rather than modifying the final member of the position array, we leave that as 1 and instead call additional glLight functions.

We will call three additional functions, one specifying the direction of the light source form which the light will emanate; one specifying the angle of the arc from which light will come and a final function which will set whether the light from the arc is concentrated in middle of the beam or spread uniformly out.

 float spotDirection[4] = {1.0f, 0.0f, 0.0f, 1.0f};
 float spotArc = 45.0f;
 float spotCoefficient = 0.0f; // Ranges from 0 to 128.
 
 glLightfv(GL_LIGHT0, GL_SPOT_DIRECTION, spotDirection);
 glLightf(GL_LIGHT0, GL_SPOT_CUTOFF, spotArc);
 glLightf(GL_LIGHT0, GL_SPOT_EXPONENT, spotCoefficient);

Here we have a spotlight that emanates light in the direction of the vector {1.0f, 0.0f, 0.0f}. We also define the arc to be 45 degrees. This value corresponds to the angle between the centre line of the cone and one of the edges of the cone. This means we have a cone of light, totalling 90 degrees. The final function defines a spot coefficient of 0. This value ranges from 0 to 128 with 0 being a uniform distribution of light and 128 being most of the light focused in the centre of the cone. By adding this spot light to the previous example, we get the following result.


As we can see, the other sphere is not lit as it is outside the spot light's cone from which light emanates.

Organising the Code

With these elements, we can accomplish simple lighting. We can also utilise the position values or even use OpenGL's transformation functions (glTranslate, glRotate) to transform the lights and create interesting lighting within our scene. Some caution however, has to be taken as to when these calls are made.

Some of the function calls to set-up the light can be called once at the beginning of the problem and left until the end or until you require to change it. These functions include enabling the light, setting the ambient, diffuse and specular colours, setting the spotlight arc angle and coefficient if a spotlight is to be used and, providing lighting is to be used for every element in the scene, the lighting itself can be enabled here. We can bundle this up into an initialise lighting function, called once at the beginning of the problem.

Two functions must be called every time the scene is rendered however. These are the call to glLight to position the light and if a spot light is used, the call to glLight to define the direction from which the light emanates. This is because these functions use the current modelView matrix to transform the light. The light will then be placed at different points in the scene depending on whether you call it before setting up your model view matrix (e.g. using gluLookAt()) or after. (This issue is the difference between the lighting being positioned in world coordinates or eye coordinates. Read this if this is not clear. I may write about the coordinate system myself sometime). Probably for most purposes, you will want to make these calls after setting up the model view matrix so it stays in the same place relative to the camera.

I have provided some example code of a program that introduces simple lighting into a scene.

#include < GL/gl.h >
#include < glut.h >

float playerX = 0.0f;
float playerY = 0.0f;
float playerZ = 15.0f;

/*
* Initialise the data used
* for creating our light.
*/

float ambient[4] = {0.2f, 0.2f, 0.2f, 1.0f};
float diffuse[4] = {1.0f, 1.0f, 1.0f, 1.0f};
float specular[4] = {1.0f, 1.0f, 1.0f, 1.0f};

float position[4] = {-5.0f, 0.0f, 0.0f, 1.0f};

float spotDirection[4] = {1.0f, 0.0f, 0.0f, 1.0f};
float spotArc = 45.0f;
float spotCoefficient = 0.0f; // Ranges from 0 to 128.

void positionLight(){
 
 /*
  * Tell OpenGL where we want our light
  * placed and since we're creating a spotlight,
  * we need to set the direction from which
  * light emanates.
  */
 glLightfv(GL_LIGHT0, GL_POSITION, position);
 glLightfv(GL_LIGHT0, GL_SPOT_DIRECTION, spotDirection);
 
}

void display(){

 glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

 glLoadIdentity();

 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

 gluLookAt(playerX, playerY, playerZ, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f);
 
 /*
  * Tell OpenGL we want all the following
  * objects in our scene to have lighting
  * applied to them.
  */
 glEnable(GL_LIGHTING);
 
 /*
  * Position the lights AFTER the model View matrix
  * has been set up.
  */
 positionLight();
 
 glutSolidSphere(1.0f, 50, 50);
 
 glPushMatrix();
 
 glTranslatef(-7.0f, 5.0f, 0.0f);
 
 glutSolidSphere(1.0f, 50, 50);
 
 glPopMatrix();
 
 /*
  * We don't need the lighting anymore
  * so disable it.
  */
 glDisable(GL_LIGHTING);
 
 glPushMatrix();
 
 glColor4f(1.0f, 0.0f, 0.0f, 1.0f);
 
 /*
  * create a small red cube where the light
  * is.
  */
 glTranslatef(position[0], position[1], position[2]);
 
 glutSolidCube(0.2f);
 
 glPopMatrix();

 glutSwapBuffers();

}

void reshape(int x, int y){

 glMatrixMode(GL_PROJECTION);

 glViewport(0, 0, x, y);

 glLoadIdentity();

 gluPerspective(60.0, (GLdouble)x / (GLdouble)y, 1.0, 100.0);

 glMatrixMode(GL_MODELVIEW);

}

void initLighting(){
 
 /*
  * Tell OpenGL we want to use
  * the first light, GL_LIGHT0.
  */
 glEnable(GL_LIGHT0);
 
 /*
  * Set the ambient, diffuse and specular
  * colour properties for LIGHT0.
  */
 glLightfv(GL_LIGHT0, GL_AMBIENT, ambient);
 glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuse);
 glLightfv(GL_LIGHT0, GL_SPECULAR, specular);
 
 /*
  * We're going to make GL_LIGHT0 a spotlight.
  * Set the angle of the cone of light and
  * how uniform the dispersion of light is.
  */
 glLightf(GL_LIGHT0, GL_SPOT_CUTOFF, spotArc);
 glLightf(GL_LIGHT0, GL_SPOT_EXPONENT, spotCoefficient);
 
}

int main(int argc, char **argv){

 glutInit(&argc, argv);
 glutInitDisplayMode(GLUT_DOUBLE | GLUT_DEPTH | GLUT_RGBA);
 
 glutInitWindowSize(500, 500);

 glutCreateWindow("Adding Lighting");

 glutDisplayFunc(display);
 glutReshapeFunc(reshape);

 glEnable(GL_DEPTH_TEST);
 
 /*
  * setup the lighting once
  * in our program.
  */
 initLighting();

 glutMainLoop();

 return 0;

}


You may note that using this example to light your own models will not work, this is why I have used GLUT to draw some spheres for us. The reason for this is that no normals nor materials have been set for your objects where as the GLUT routines include them. More on normals and materials and their importance in lighting a scene in the next few posts.

No comments:

Post a Comment