Lighting

introduction

Introducing lighting into your game is potentially a step towards realism. In OpenGL you can introduce multiple light sources that are either static or dynamic. You have a large amount of control over the characteristics of the light sources at the expense of speed. It is important to realise that a scene can still have the effects of lighting without a light source. More about this at a later stage.

There are a number of properties of a light source.

All of these are controlled by OpenGL statements.

By default OpenGL offers a maximum of 8 light sources. However your system may have more. The following code will give you the number of light sources available on your system.

        IntBuffer lights = BufferUtils.createIntBuffer(16);
        GL11.glGetInteger(GL11.GL_MAX_LIGHTS, lights);
        System.out.println("maximum number of light sources = " + lights.get(0));
        

By default in OpenGL lighting capability is disabled. To enable lighting use the usual enable method.

        GL11.glEnable(GL11.GL_LIGHTING);
        

Then each light source you require needs to be enabled.

        GL11.glEnable(GL11.GL_LIGHTx);
        

Where x is the number of the light source in the range 0 to the maximum number of light sources. For example

        GL11.glEnable(GL11.GL_LIGHT0);
        

There is the corresponding disable method so that your light source can be turned off when required.

        GL11.glDisable(GL11.GL_LIGHT0);
        

Simple Square with Lighting

We are going to take the SimpleSquareApp and add some lighting. There are a number changes we need to make

  1. Add a normal to the square primitive geometry
  2. Enable depth testing
  3. Rotate the square about the Y axis
  4. Enable lighting
  5. Enable a specific light

When defining a surface in OpenGL we have so far defined the location of each vertex and a colour of the surface. The order in which you define the points defines which face of the surface will be the front and which is the back. We can also define the normal to the surface. As illustrated earlier the normal is a vector pointing directly out from the surface we define. When using lighting you must explicitly define the normal. OpenGL uses the normal to define which way a surface is facing with respect to a light source within your 3D scene. This then enables OpenGL to calculate what effect the light will have on theat surface.

So within our SimpleSquareApp we will define a normal vector for the surface. This vector needs to have a length of 1 for OpenGL to produce the correct result for its lighting calculation. We will include this statement for the normal.

   GL11.glNormal(0,0,1.0f);
   

This defines a normal pointing in the positive z direction which is at 90o to the front face of the square, which sits in the xy plane.

Note: make sure the normal is pointing out of the front face of your geometric primitive. Otherwise you will get unexpected lighting effects.

            GL11.glBegin(GL11.GL_QUADS);
            GL11.glColor3f(1.0f, 0.0f, 1.0f);  // set the colour for the quad
            GL11.glNormal(0,0,1.0f);  // set the normal for the quad
            GL11.glVertex3f(1.0f, 1.0f, -20); // define the front face of the QUAD
            GL11.glVertex3f(-1.0f, 1.0f, -20);
            GL11.glVertex3f(-1.0f, -1.0f, -20);
            GL11.glVertex3f(1.0f, -1.0f, -20);
            GL11.glEnd();
   

We only need to define the normal once because all the vertices will have the same normal. However in some applications we will define a different normal for every vertex.

Exercise : In your application include depth testing and rotate your square about the Y axis

Now we need to enable lighting. So under the Display.create () statement use the enables statmenet tot enable lighting and light0.

   Display.create();
   GL11.glEnable(GL11.GL_LIGHTING);
   GL11.glEnable(GL11.GL_LIGHT0);
   

Now execute your application. You should have a square effected by lighting.

By default the light source will be located at world coordinate (0,0,0) and be white in colour. Also note that the square is no longer purple. Lighting in OpenGL ignores the colour. Later we will add colour by defining a material.

Exercise : Slow down the rotation of your square to see the lighting effect better.

You may notice that the lighting effect is not quite correct. OpenGL, by default, uses the normal you have defined for each vertex for both front and back lighting calculations. Hense when you are looking at the back face of the geometric primitive the normal that is used for the lighting calculation is facing away from you. With your rotating square you will notiice that when the back of the geometric primitive is facing you will see it illuminated using the normal of the front face.

To get OpenGL to calculate the lighting for front and back correctly you need to change the lighting model.

GL11.glLightModeli(GL11.GL_LIGHT_MODEL_TWO_SIDE,GL11.GL_TRUE);
 

With this statement OpenGL reverses the direction of the normal when calculating the back face of each geometric primitive. This statement could be included anywhere after you have created your display, however a good place to put it would be just after you enable the lighting.

By default OpenGL does not use the colour defined in the geometric primitive

   GL11.glColor3f(1.0f, 0.0f, 1.0f);  // set the colour for the quad
   

to work out the colour of your object once lighting is enabled. OpenGL uses a more complex property called materials to attribute colour to a geometric primitive. You can however get OpenGL to accept the above defined colour as a material by including the following two statements. Again a good place to put these statements would be just after enabling lighting.

   GL11.glEnable(GL11.GL_COLOR_MATERIAL);
   GL11.glColorMaterial(GL11.GL_FRONT_AND_BACK, GL11.GL_AMBIENT_AND _DIFFUSE);
   

Position your light

To position your light we need to give an location for you light in Cartesian coordinates. s

    GL11.glLight(GL11.GL_LIGHT0,GL11.GL_POSITION,floatBuffer(20,0,-20,1)
     

Note I have used the following method to generate a FloatBuffer for the last parameters. you will need to add this to your class.

        public FloatBuffer floatBuffer(float a, float b, float c, float d)
            {
            float[] data = new float[]{a,b,c,d};
            FloatBuffer fb = BufferUtils.createFloatBuffer(data.length);
            fb.put(data);
            fb.flip();
            return fb;
             }
        

the program so far

The code that follows is the code so far. A purple square rotating about the y axis with a light source to the right.

package opengl;


import java.nio.FloatBuffer;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.lwjgl.BufferUtils;
import org.lwjgl.LWJGLException;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.GL11;


public class SimpleLighting {

    public static void main(String arg[])
    {
     new SimpleLighting();
    }

    public SimpleLighting()
    {
        try {
            boolean close = false;
            float rotation = 0;
            DisplayMode mode = Display.getDesktopDisplayMode();
            Display.create();
            GL11.glEnable(GL11.GL_DEPTH_TEST); // enable depth testing
            GL11.glEnable(GL11.GL_LIGHTING); // enable lighting
            GL11.glEnable(GL11.GL_LIGHT0); // enable light 0

            // calculate the lighting for both front and back faces correctly
            GL11.glLightModeli(GL11.GL_LIGHT_MODEL_TWO_SIDE,GL11.GL_TRUE);

            // use the defined color as the material for the square
            GL11.glEnable(GL11.GL_COLOR_MATERIAL);
            GL11.glColorMaterial(GL11.GL_FRONT_AND_BACK,GL11.GL_AMBIENT_AND_DIFFUSE);

            // set viewport to full screen
            GL11.glViewport(0, 0, mode.getWidth(), mode.getHeight());
            GL11.glMatrixMode(GL11.GL_PROJECTION);
            GL11.glLoadIdentity();
            float ratio = 1.0f * mode.getHeight() / mode.getWidth();
            GL11.glFrustum(-1.0f, 1.0f, -ratio, ratio, 5.0f, 60.0f);
            GL11.glMatrixMode(GL11.GL_MODELVIEW);

            while (!close) {
                rotation = rotation + 0.1f;
                GL11.glClearColor(0.0f, 0.2f, 0.5f, 1.0f);
                // clear both color and depth buffer
                GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
                GL11.glLoadIdentity();
                // position  thelight to the right of the square
                GL11.glLight(GL11.GL_LIGHT0,GL11.GL_POSITION,floatBuffer(20,0,-20,1));
                // translate the coordinates to -20 z
                GL11.glTranslatef(0, 0, -20);
                // rotate the coordinates about the y axis
                GL11.glRotatef(rotation, 0, 1, 0);

                // draw square
                GL11.glBegin(GL11.GL_QUADS);
                // define the normal to the square
                GL11.glNormal3f(0, 0, 1.0f);
                // define colour
                GL11.glColor3f(1.0f, 0.0f, 1.0f);
                // define the shape with front face facing you.
                GL11.glVertex3f(1.0f, 1.0f, 0);
                GL11.glVertex3f(-1.0f, 1.0f, 0);
                GL11.glVertex3f(-1.0f, -1.0f, 0);
                GL11.glVertex3f(1.0f, -1.0f, 0);
                GL11.glEnd();

                Display.update();
                close = Display.isCloseRequested();
            }
            Display.destroy();
            System.exit(0);
        } catch (LWJGLException ex) {
            Logger.getLogger(SimpleLighting.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

  // method to populate a FloatBuffer with 4 values.
    public FloatBuffer floatBuffer(float a, float b, float c, float d)
            {
            float[] data = new float[]{a,b,c,d};
            FloatBuffer fb = BufferUtils.createFloatBuffer(data.length);
            fb.put(data);
            fb.flip();
            return fb;
             }


}
   

Add light Colours

It is important to realise that lighting in OpenGL is an approximation of true lighting. To illustrate this I want you to imagine holding a shinny white plastic ball in a blue room illuminated by sun shinning in through a window. How will the ball be illuminated so that you can see it? You will notice that one half of the ball will be illuminated more brilliantly by the direct light from the sun. There will be a gradual shading effect from bright to darker across the face of the ball on the illuminated side, this is called diffuse lighting. Because the ball is shinny you will also notice that there is a bright shinny spot on the ball, which is the direct reflection of the sun off the surface of the ball. This spot is called the specular reflection and the smoother and shinnier the surface the smaller and brighter is this point. On the side of the ball facing away from the sun you will notice that it is illuminated less brilliantly by a uniform blue light. This blue light is the sunlight reflecting off the walls of the room. This is called ambient light and it will effect the whole surface of the ball.

The above illustration summaries the 3 lighting effects that OpenGL uses to simulate lighting on a surface

When defining a light source for objects within your an OpenGL scene you specify the colour and intensity for each of these components individually. Here is the code for light source 0, GL11.GL_LIGHT0

            GL11.glLight(GL11.GL_LIGHT0, GL11.GL_DIFFUSE, floatBuffer(1.0f, 1.0f, 1.0f, 1.0f));
            GL11.glLight(GL11.GL_LIGHT0, GL11.GL_AMBIENT, floatBuffer(0.1f, 0.1f, 0.1f, 1.0f));
            GL11.glLight(GL11.GL_LIGHT0, GL11.GL_SPECULAR, floatBuffer(1.0f, 1.0f, 1.0f, 1.0f));
        

Before examining light sources further we will now look at materials associated with objects. Each object in your scene will be given a material property to specify how the diffuse, specular and ambient components of a light source will react with its surfaces.

Exercise : Include the above 3 statements in your code. Place them just after you enabled GL_LIGHT0. Play with the values to see what effect they have on your graphics.