OpenGL on OS/2 - Let There Be Light

Written by Perry Newhook

Introduction
Last month we started an example that created a solar system, complete with a sun, planets and moons. This month we will describe OpenGL lighting and introduce lighting to our example. We will then add camera positioning to place ourselves on a planet to watch the days and night go by, and watch the moon travel overhead.

Bug Fix!
There was a small bug in last month's example that I'm sure most of you picked up on. In the paint window routine when we are drawing Venus, it accidentally rotated by the 'earth' amount instead of the 'venus' amount. The corrected section of code follows: // position venus glRotatef( venus, 0.0, 0.0, 1.0 ); glTranslatef( -40.0, 0.0, 0.0 );

Upgrading to OpenGLv1.1
A few weeks ago IBM released version 1.1 of the OpenGL libraries. They are available from IBM's OS/2 OpenGL ftp site: ftp://ps.boulder.ibm.com/rs6000/developer/os2/OpenGL/. The files you need are oglgold.zip, aux.zip and glut.zip.

Starting with this month, my executable will be compiled with the OpenGL v1.1 libraries. This does NOT mean that you have to upgrade if you do not want to; OpenGL v1.1 is fully backward compatible with the v1.0 dll's. However v1.0 compiled programs will no longer run with a v1.1 system. The good news is that there are no source changes required to upgrade to v1.1, it just requires a recompile.

If your newly recompiled source file now refuses to run under the new OpenGL v1.1 dll's check the following common mistakes (I've done them all!)
 * You've compiled using the old .lib files.
 * Solution: Make sure your compiler and/or the SET LIB in config.sys now references the newer lib files.


 * I just get a blank window or RASTER.DLL crashes.
 * Solution: You are probably mixing old and new dll's. When you installed OS/2, it put some OpenGL dll's in the \OS2\DLL directory. When you downloaded the OpenGL toolkit your new dll's are most likely in a \OPENGL directory but the other dll's are still around. Depending upon your LIBPATH setting in your config.sys it may still load up the old dll's. After making a backup copy (of course) delete the following dll's from your \OS2\DLL directory (only if you have duplicates in another directory of course): glut.dll libaux.dll libtk.dll opengl.dll raster.dll, or you can simply replace them with the new dll's. Replacing is not recommended because older v1.0 will not run on a v1.1 system


 * I've done all of the above but my program still crashes!
 * Solution: OpenGL v1.1 seems to require more stack space than v1.0 did. The sample programs in this column will no longer work with the default stack size whereas they did with the older libraries. Simply increase the stack size (2MB is good for these samples) with either a function call such as IThread::current.setStackSize or put it into your makefile. With VisualAge you can find the stacksize setting on the first page of your link options, or you can put it into your makefile yourself with a /ST:size parameter.

Hopefully at this point your app will compile for OpenGL v1.1. If you are still having problems, please feel free to e-mail me and I'll post more common problems in next month's issue.

OpenGL Lighting
It seems nothing is more difficult to grasp for the first-time OpenGL programmer than how to program lighting. However no other effect can add as much realism to a scene as proper lighting, and we will see that it is really not all that hard.

Lighting in OpenGL is not meant to provide a true replication of what light actually does, it is simply meant to be an approximation that is easy and fast to compute, looks realistic, and is fairly easy to set up. For this reason, the OpenGL model will not give you lighting effects such as reflections and shadows; for these you will have to create your own replacement lighting model. For anyone who has looked into lighting calculations, you will find that they can get quite complicated. Usually the effect that you want can be simulated with other supported effects rather than trying to redefine your own lighting model.

Even though the lighting model is not perfectly accurate, lighting does give your object realism. If you look at the planets in last month's example, even though they are spheres you cannot tell them apart from simple disks. Lighting will smoothly shade the spheres so that as the planet surface curves away from the light source, it will reflect less light, giving it a more realistic 3D appearance. Different aspects of how light is reflected off of an object can be controlled to produce a variety of materials such as plastic, metals such as gold or silver, and colours such as jade, obsidian or ruby.

The OpenGL lighting model makes the assumption that light can be broken down into individual R, G and B components. This means the colour of a light can be determined by the amount of red, green and blue light it emits, and the colour of an object is influenced by the amount of individual light components that it receives. The colour of an object can be determined by four independent lighting components: emitted, ambient, diffuse and specular. Each of these components is calculated independently and then added together to give a final colour.

Emitted light is simply the colour that the object is emitting. It is unaffected by the presence or absence of any light sources.

Ambient light is the light that has been so scattered by the environment that its source is impossible to determine. Backlighting in a room is mostly ambient because you cannot tell where the light is coming from after it has bounced off all the surfaces, while a spotlight has a small ambient component because except for what it is shining upon, it does not contribute much to the overall light in the scene.

Diffuse light comes from one direction so it's brighter if it comes directly down on a surface rather than glancing off at an angle. However, once it strikes the surface the light is scattered equally in all directions.

Specular light can be thought of as shininess. A highly specular object such as a mirror or polished metal will reflect almost all light it receives, regardless of the incident angle of the light. An object such as carpet has almost no specular component.

You can mix and match the components to produce different effects. For example a wall material will be mostly diffuse but also partly specular. If the wall was red and the light we shone on it white, the wall will obviously show up red, but will also have a white specular highlight where the light source hits it directly. A blue light source shining on a white object would show up blue, a pure blue light shining on a pure red object would not reflect any light at all and show up black.

Creating the light
You get at least 8 lights, identified by the labels GL_LIGHT0, GL_LIGHT1, ... and so on. For each light you can adjust ten different parameters. The colour of the light is affected by three parameters: GL_AMBIENT, GL_DIFFUSE and GL_SPECULAR. As mentioned before, ambient adds to the overall light of the scene, diffuse is what you would normally think of as the colour of a light, and the specular component affects the colour of the highlight on the object. Other parameters affect the position of the light, its direction, the angle of the beam spread, and the attenuation of the light. For now we will just create the light with the default settings and change when necessary.

The initial source code is our final source code from last month and can be downloaded here. The first thing we have to do is to enable a single light.

Just like our function initOpenGL, we will create a function to initialize lighting called initLighting inside the GLPaintHandler class. Inside this function, the first thing we do is to tell GL that we want lighting calculations enabled. This is a single command: glEnable( GL_LIGHTING );

Well, that was easy! There is a bit more however to do. We have to enable a light to turn it on. We will use GL_LIGHT0 because it has default parameters. glEnable( GL_LIGHT0 );

Call the initLighting function right after the initOpenGL call.

The default parameters of GL_LIGHT0 are no ambient, bright white diffuse, bright white specular, and a directional light along the -z axis. There is no spot cutoff angle or attenuation so it acts like a point source of light at an infinite distance. Be careful when adding a second or third light. While GL_LIGHT0 defaults some components on so you can see, but all other lights have their values defaulted to off so you will have to change them before you can see anything.

Run this program.

Well we can see that the spheres are now definitely 3D; as the edges of the sphere turn away from the light, it reflects less light so it appears darker. However the colours we specified are no longer present and everything appears grey. This is because lighting does not use the current colour in its calculations but uses the material colour of what it is shining upon. There are two ways we can add colour to the objects. The first is to use glMaterial to set colour parameters for each object. This gives us the most flexibility but is also a little more difficult to set up. The second choice is to enable the material colour to track the current colour. This is the choice we will take because we have already specified colours for each of our objects. Note that there is nothing stopping you from mixing the two methods.

Add the following line after glEnable( GL_LIGHT0 ) that allows the material colour to track the current colour. glEnable( GL_COLOR_MATERIAL ); Run this again.

We now have our colour back, but the lighting is not yet correct. The default light is an infinite light in the -z direction (in our case, into the screen). What we need to do is to change our light from a directional one to a positional one. This parameter, and all of the other parameters we can change on a light are done by the glLight*( light, parametername, parameter) command. This command takes three parameters, the first is the name of the light ( GL_LIGHT0, GL_LIGHT1 ... ), the second is the parameter name (GL_AMBIENT, GL_DIFFUSE, GL_SPECULAR, GL_POSITION, GL_SPOT_DIRECTION, GL_SPOT_EXPONENT, GL_SPOT_CUTOFF, GL_CONSTANT_ATTENUATION, GL_LINEAR_ATTENUATION, GL_QUADRATIC_ATTENUATION), and the third parameter contains the data for the parameter name we are changing. The first three parameter names take a vector to an RGBA component.

For example to change the ambient component of a light to blue we could write: GLfloat light_amb[] = { 0.0, 0.0, 1.0, 1.0 }; glLightfv( GL_LIGHT0, GL_AMBIENT, light_amb );

The next four parameter names go together. GL_POSITION takes a vector of four integers or four floating point values. If the fourth value is zero, then the light is directional and the first three values represent a vector. If the fourth value is non-zero then the first three values represent the position of the light. If the light is directional, attenuation is disabled and you probably wouldn't want to create a directional spotlight.

If you are making a positional spotlight, then you can change the direction of it with GL_SPOT_DIRECTION. This parameter expects an xyz direction of the spotlight. You can change the intensity distribution of the spotlight with GL_SPOT_EXPONENT. It expects a single integer or floating point value and is used in the following way: effective light intensity is attenuated by the cosine of the angle between the direction of the light and the direction of the light to the vertex being highlighted, raised to the power of the spot exponent. The default is zero. And that's all I have to say about that.

GL_SPOT_CUTOFF specifies a single value that determines the maximum spread of the light source. This value is measured from the axis of the light and is restricted to the range 0 to 90, unless it has the special value of 180 in which case it becomes a point source of light that radiates in all directions. The next three parameter names determine the attenuation factor of the light. The contribution of a light source is multiplied by an attenuation factor that is calculated by: ( Kc + Kl*d + Kq*d*d ) where:
 * d = distance between the light's position and vertex
 * Kc = GL_CONSTANT_ATTENUATION
 * Kl = GL_LINEAR_ATTENUATION
 * Kq = GL_QUADRATIC_ATTENUATION

Basically, with attenuation enabled, the further the light is away from an object, the less light reaches it.

Both the position and direction of light is affected by the inverse of the modelview matrix, so if you're careful, you can position your light anywhere in the scene you wish. We will now change our directional light to a positional one and place it in the centre of the sun. We are going to take advantage of the fact that we can move our specified position with the modelview matrix, so we will specify a position of (0,0,0) and draw it when we draw the sun.

Add the following code just before we draw the sun, but after we specify the viewing position: // put light in middle of sun GLfloat light_pos[] = { 0, 0, 0, 1 }; glLightfv( GL_LIGHT0, GL_POSITION, light_pos ); Run this.

Now we can see that the position of the sun is in the correct spot, but the sun is not lit up. Remember that while the sun should look yellow, it emits a white light, just like our own sun.

The sun and the dark side of the planets are slightly lit. This is due to default parameters in the glLightModel* command. This command allows you to specify three parameters that affect the lighting model:
 * GL_LIGHT_MODEL_AMBIENT specifies the RGBA component of the ambient light in the entire scene. Its default is (0.2, 0.2, 0.2, 1.0) which is why we see some light emitting from the dark side of the planets and the sun.
 * GL_LIGHT_MODEL_LOCAL_VIEWER specifies how specular reflection angles are computed. If the parameter is 0, then specular reflections are computed from the origin of the eye coordinate system. Otherwise reflection angles take the view direction to be along the -z axis. The default is 0.
 * GL_LIGHT_MODEL_TWO_SIDE specifies whether one or two sided lighting calculations are done for polygons. If the parameter is 0 (the default), one sided lighting calculations are performed. If the parameter specifies two-sided lighting, then vertices of back facing polygons are lit using back material parameters and have their normals reversed before the lighting equation is evaluated.

Lets change the ambient component to 10 percent instead of 20. At the end of the initLighting function add the following: // modify ambient light GLfloat amb_light[] = { 0.1, 0.1, 0.1, 1.0 }; glLightModelfv( GL_LIGHT_MODEL_AMBIENT, amb_light ); We also have to change the colour of the sun so that it looks like it is shining. It does not light up like the rest of the planets because the normals and the light direction are in the same direction. For lighting to be calculated properly, each vertex of each polygon must have a normal associated with it. The normal is generally, but not necessarily, perpendicular to the surface of the polygon.

The colour of each vertex is calculated as the sum of:
 * material emission
 * ambient light of light model * ambient light of material
 * for each light source the attenuation factor multiplied by
 * ambient light * ambient material plus
 * max( dot product of the vector from the vertex to the light source and the normal vector, 0 ) * diffuse light * diffuse material plus
 * max( dot product of ( the vector between the vertex plus the light position vector ) and the normal vector, 0 ) * specular light * specular material

When the normals of the planet point towards the light source, the diffuse and specular components become non zero, and when the normals point away, these components get clamped to 0 leaving only the emissive and ambient components. Once the colour of each vertex is determined, the colour of the polygon is determined by smooth shading the colours of the vertices.

Since we want the sun to be lit even though a light source will not light the sphere in the way we want, we will light it by increasing the ambient portion of the sun to full yellow. This is also intuitively correct; the sun is yellow regardless of any other light source. Instead of colouring the sun yellow with a glColor* command, we will change it using a glMaterial* command. // make sun full ambient GLfloat sun_emission[] = { 1.0, 1.0, 0.0, 1 }; glMaterialfv( GL_FRONT, GL_EMISSION, sun_emission ); // glColor3f( 1.0, 1.0, 0.0 ); We also have to remember that after we draw the sun, we have to put the emission component back to zero or every object will be yellow. Remember that OpenGL is a state machine and will continue to do the last thing commanded it. After the auxSolidSphere( 20.0 ) command place the following: // set planet lighting parameters GLfloat planet_emission[] = { 0.0, 0.0, 0.0, 1.0 }; glMaterialfv( GL_FRONT, GL_EMISSION, planet_emission ); Now we are done! We have a fairly realistic simulation of planets rotating about a sun with night and day for each planet. If you got lost along the way, the code so far can be downloaded [glcol5a.zip here].

For our next trick, we will place the viewpoint on the surface of the earth and watch the moon pass overhead, the planets go through the sky and the day turn into night. There are two things that we have to do first however. One is to slow everything down: the planets are rotating far too quickly to properly view a day passing. The second is to rotate the earth as the day goes by. This was one rotation that we omitted before because it was not visible to the eye from our initial viewpoint.

Divide each of the increments in GLPaintHandler::timerFn by 100 so that we can see day rotations more reasonably.

Add an earthSpin parameter to GLPaintHandler, and add the following spin calculation to GLPaintHandler::timerFn. earthSpin += 3.6525; if( earthSpin > 360.0 ) earthSpin -= 360.0; Now to do the rotation we have to wrap it in a glPushMatrix, glPopMatrix pair so that we do not affect any other rotations. Where we draw the earth in GLPaintHandler::paintWindow, replace that section with the following: // position the earth glRotatef( earth, 0.0, 0.0, 1.0 ); glTranslatef( -60.0, 0.0, 0.0 ); glPushMatrix; // do a day rotation glRotatef( earthSpin, 0.0, 0.0, 1.0 ); // and draw it blue glColor3f( 0.0, 0.0, 1.0 ); auxSolidSphere( 5.0 ); glPopMatrix; This will now rotate the earth properly. Without it we wouldn't see the days go by as we place ourselves on the surface.

The next part we have to add replaces the section in GLPaintHandler::paintWindow, that specifies out viewing position. Instead of simply backing away from the image, we are going to back away and rotate with the earth's rotation. Think of yourself as a flying camera and what you would have to do to place your viewpoint where you would like. To do this properly you really have to know your rotations and translations, and even so it is very easy to get lost. The best way to follow this is to draw out what you want to do on a sheet of paper.

Instead of: // move the image back from the camera glTranslatef( 0.0, 0.0, -500.0 ); // and rotate for the viewing position glRotatef( -70.0, 1.0, 0.0, 0.0 ); do the following // tilt viewpoint an place on edge of planet glRotatef( 30, 1.0, 0.0, 0.0 ); glTranslatef( 0.0, -5.0, 2.0 ); // do a day rotation glRotatef( -earthSpin, 0.0, 1.0, 0.0 ); glTranslatef( 0.0, 0.0, -60.0 ); glRotatef( -90.0, 1.0, 0.0, 0.0 ); // and rotate for the viewing position glRotatef( 90-earth, 0.0, 0.0, 1.0 ); Basically what we are doing (in reverse order) is to rotate the view by -earth (the 90 is because the planets start out at 90 offset with respect to the viewer. We are viewing along -z and they are translated along x ). Translate out the same distance as the earth and spin with it. The next translation puts us just above the surface of the earth (since our last translation put us in the centre of it ), and rotate a bit so instead of looking straight up we look slightly along the horizon.

We also have to change our field of view and our minimum view distance. Remember we defined these when we create our viewpoint with the gluPerspective call. Our FOV is currently defined as 20 degrees. Since this is a real-life situation, estimate what your real field of view would be (how wide of an angle can you see when you look up). For here I've estimated mine to be about 70 degrees. Also since we are on the planet our front clipping plane must be very small. Lets pick 0.1 instead of the current 300.0.

Now if you run this, you will see (hopefully) a blue horizon which is the earth that you are standing on, and various bodies of planets that pass overhead. You can tell what each is by their colour. These other bodies are awfully close, because we picked arbitrary distances for the orbit radii. You can pick properly scaled distances to match the planet sized, or do what I'm doing and simply multiply all of the planet radii by ten and double the moon distance. Things should now look a little more natural.

The completed code can be downloaded here.

There are far too many variations on lighting to explain in the little bit of room that I get here (or would want to write). Given all of the things that you can change, there are practically an infinite number of final variations produced as output. The only way to get more used to creating your own lighting conditions is to play with the parameters. Here are some things to try:
 * Change the GL_SPOT_CUTOFF and GL_SPOT_DIRECTION parameters so that the sun acts more like a lighthouse (or a pulsar if you want to keep the same theme)
 * Enable attenuation so that the more distant planets get less light than the closer planets.
 * Create a second light in the moon to shine upon the earth (A spotlight aimed at the earth will do here nicely)
 * If you've done 3) then there is a cool trick you can do. Light values are clamped to [-1, 1] not [0, 1] as you would think. Since light values are added together, this means that you can project a light that subtracts light from the object that it is hitting effectively creating a shadow. This is the cheapest method I know of to create shadowing in OpenGL but you have to get your values just right in order to cancel out the light perfectly on the destination.

Now that we have the basics of lighting down, next month I will show a few more lighting and colouring tricks, as well as texture mapping.

See you next month!