OpenGL on OS/2 - Let There Be Lit Things

From EDM2
Jump to: navigation, search

Let There Be Lit Things

Written by Perry Newhook

Introduction

Welcome back from the holidays! I hope that those of you who got to take some time off for the holidays got to relax and enjoy it. (I'm writing this just a few days before I take a much needed break, and I'm planning on enjoying it as much as possible). I think that the new year looks bright for OS/2; IBM has released GRADD video drivers which is a necessary first step in producing accelerated 3D video drivers. For those not familiar with GRADD, it is a driver model where most of it is provided by IBM, and the manufacturer simply provides the missing piece that talks to the card's registers. This is a great improvement over the previous method that required each manufacturer to write the entire OS/2 video driver and a WIN-OS/2 video driver. This hopefully will reduce the possibility of buggy drivers as most of the driver is already written and tested by IBM. For OpenGL, the accelerated part to talk to OpenGL would be provided by IBM, and just the hardware-specific section would be written by manufacturers. This should also greatly increase the number of OpenGL accelerated cards available when (if) IBM provides this capability, as manufacturers should find it much easier to develop drivers. We have also seen at the recent fall Comdex IBM push OS/2 much more strongly than they have in the past. Let's hope they keep this up and don't lose interest again. Also interesting to note is that OS/2 is now being targeted first for Java development ahead of IBM's other platforms such as AIX. AIX is still beta-ing Java 1.1.4 whereas OS/2 should soon have Java 1.1.6. Maybe this will be a good year; let's keep our hopes up and our voices strong to companies in our support of OS/2.

Let there be lit textures!

Last month (think back through the haze of eggnog), we managed to do some real effects with OpenGL by implementing texture mapping. The month before we did lighting. Some of you may be wondering 'can you combine the two?' We'll the answer is YES!, and it is quite easy. This and some other effects are what we will be covering in this month's column.

To enable lighting with texture mapping all we have to do is to enable lighting just as if it was a normal polygon. What OpenGL will do is calculate the lighting intensity for the polygon, and then blend the resulting colour value with the texture colour values to come up with a final colour. As the polygon turns away from the lightsource, the texture will appear to get dimmer. Now I know some of you who have tried this are thinking to yourselves "Hey wait a minute! I tried this before and I couldn't get the lighting to affect the texture colour at all!" Well, there is one setting that you have to ensure is set properly for this to work. If you remember when we added the alpha parameter in last month's example so that we could have part of the texture transparent, we had to change a setting in the texture environment so that the alpha colour was blended in with the polygon colour. When the setting

glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE)

was applied the transparency would work. The same thing has to be done with lighting. OpenGL cannot compute lighting variances on a texture because it does not have any 3D geometry - it is just an image. OpenGL only computes the light for the polygon itself. If we then simply stick the texture on the polygon as is done when we specify

glTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);

we end up losing the intensity variation due to lighting. What we have to do is to specify GL_MODULATE so that the intensity variations are correctly blended with the texture. You can download source that shows this [glcol7a.zip here]. Try changing the GL_MODULATE to GL_DECAL to see the difference. In a real application you could mix the two effects to have some textures affected by lighting like a brick wall, and others not, like an internally lit sign.

OpenGL-col7a.gif OpenGL-col7b.gif

This software has simply taken December's example and added in November's lighting initialization. You'll also notice two things about this software. The first is that the texture is bigger than the maximum 64x64 that I said was supported last month. Actually what I said was 64x64 is the largest that is guaranteed to be supported by all implementations of OpenGL, but specific OpenGL implementations may allow you to go higher. OS/2's implementation actually allows you to go all the way up to 1024x1024! Don't depend upon this though for future versions, and don't expect every platform to be as capable as OS/2 if you are generating cross-platform OpenGL apps. The image we are loading is the official logo of OpenGL and has dimensions of 256x128. (The official OpenGL site where the logo originates is at www.opengl.org ). The other thing you may notice when running the demo is that the back of the polygon is not lit. This is because we are using the default lighting parameters that light only front facing polygons. You can either enable lighting of back facing polygons, or you can place another polygon on the back to make a billboard type sign (this would also alleviate the effect of the OpenGL sign becoming reversed as it spins around to the back side).

Let there be textured light!

Another cool effect that we can do is to project a texture as if it were light. This would be done if we wanted to simulate a light source that was non-uniform, ie brighter in the middle than the edges, or an effect such as a slide projector. Unfortunately, while I fully intended to demonstrate this effect in this column, in implementing it I quickly realized that this effect was really only meant to be done with hardware assistance. When the hardware accelerated drivers become available, I will revisit this topic as well as a few other advanced effects that can be done with texture mapping.

Is that image moving fast or is it the eggnog?

One effect that is performed often in computer graphics to simulate something moving very quickly is 'motion blur', similar to the effect seen in OS/2's 'Comet Cursor'. This effect is performed by jittering the moving object with each position drawn successively dimmer. You can see this effect in many of today's video games. One downside to performing this effect is that the scene (or part of the scene) must be rendered repeatedly for each jittered component. This means that if you want to show your current position and five jittered previous positions, you will have to render your scene a total of six times causing each frame to potentially take up to six times longer (Cases where this is not true are when the scene is relatively simple and the majority of the time taken is in transferring the rendered scene to the video buffer.)

Since we are going to be accumulating multiple images, we have to enable, tada! you guessed it, the accumulation buffer. This is done when we do our pglChooseConfig() in the GLPaintHandler::initOpenGL() function. We are simply going to modify the lighting with texture application given previously to show motion blur. Enable the accumulation buffer with the same minimum depth values as our regular colour buffer. Change the attribList[] to the following:

int attribList[] = { PGL_RGBA, PGL_RED_SIZE, 2, PGL_GREEN_SIZE, 2,
  PGL_BLUE_SIZE, 2, PGL_ALPHA_SIZE, 1, PGL_ACCUM_RED_SIZE, 2, PGL_ACCUM_GREEN_SIZE, 2,
  PGL_ACCUM_BLUE_SIZE, 2,  PGL_ACCUM_ALPHA_SIZE, 1, PGL_DOUBLEBUFFER, 0 };

The accumulation buffer should now be enabled. The only other change we have to do is to clear the accumulation buffer at the start of each frame just like we cleared the colour buffer with the command:

glClear( GL_ACCUM_BUFFER_BIT );

We do not however clear the accumulation buffer at the same time we clear the colour buffer because that would defeat the purpose of what we are trying to achieve. We clear the accumulation buffer at the start of each displayed frame, and the colour buffer at the rendered frame. Remember that there are several different rendered frames in each final frame, with each rendered frame being slightly dimmer than the last.

The function that we are using to manipulate the accumulation buffer is glAccum(). This function takes only two parameters; an operation and a value for that operation.

glAccum( GLenum operation, GLfloat value );

The allowed operations are:

  • GL_ACCUM - Takes RGBA values from the render buffer, scales the values so they are in the floating point range [0, 1], multiplies those values by value and adds the result to the values already in the accumulation buffer.
  • GL_LOAD - Same as GL_ACCUM except the values in the accumulation buffer are simply replaced instead of added to the new values.
  • GL_ADD - Adds value to each element in the accumulation buffer
  • GL_MULT - Multiplies each element in the accumulation buffer by value and returns the result to the accumulation buffer
  • GL_RETURN - Translates the values in the accumulation bufer back into the colour buffer. The values are first multiplied by value and then expanded back out to the proper bit resolution.

What we want to do is to draw the texturemap five times, each in a slightly different place and each previous image will be slightly dimmer than the one that comes after it. Therefore as we go back in time through the animation, the afterimage gets dimmer and dimmer.

OpenGL-col7c.gif OpenGL-col7d.gif

If you look carefully at the description of glAccum() you will notice that the values are not clipped when they are added together. What this implies is that if you add two images together that are each 80% of full intensity for one colour, you will end up with a colour that is 160% of max value. This colour component then overflows and affects other colour components around it. This means that you can cause some strange colour effects to happen if you want to with the accumulation buffer, but that is not the desired effect here. Since we are going to be drawing the scene five times, we multiply each scene by 0.2 before storing it in the colour bufer. That means that even if there is a pixel with full intensity in each subframe, the final rendering intensity can only be 5*0.2 = 1.0 or full intensity. The command to do this is:

// multiply the current scene by the scale factor
glAccum( GL_ACCUM, 0.2 );

Now we also want previously stored frames to be dimmer than the one after it. Therefore we can use the GL_MULT option in glAccum() to scale down the values in the image effectively dimming them. Before each frame is rendered, let's reduce the image already in the accumulation buffer by 80%:

// dimm the image in the accumulation buffer
glAccum( GL_MULT, 0.8 );

After we have all of the images rendered and sitting in the accumulation buffer, we have to transfer that image back into the colour buffer so that we can display it on the screen. This is done with the GL_RETURN parameter. We don't want to do any changes to this image so our scale value is 1.0.

// now copy the completed scene from the accumulation buffer to the colour buffer
glAccum( GL_RETURN, 1.0 );

So now our render thread gets changed to support the following logic:

  1. Clear the accumulation buffer
  2. Dim the contents of the accumulation buffer
  3. Clear the colour buffer and draw the scene
  4. Scale the colour buffer by 0.2 and add to values in the accumulation buffer
  5. If haven't drawn all images, go back to step 2 and move the scene to a new position
  6. Copy the completed accumulation buffer to the colour buffer
  7. Copy the colour buffer to the screen

In code this would turn into:

// clear the sccumulation buffer
glClear( GL_ACCUM_BUFFER_BIT );

for( float decay=1.0; decay > 0.0; decay-=0.2 )
{
  // dimm the image in the accumulation buffer
  glAccum( GL_MULT, 0.8 );

  // draw scene
  . . .

  // multiply the current scene by the scale factor
  glAccum( GL_ACCUM, 0.2 );
}

// now copy the completed scene from the accumulation buffer to the colour buffer
glAccum( GL_RETURN, 1.0 );

// swap the back draw buffer to the front
pglSwapBuffers( IThread::current().anchorBlock(), window.handle() );

Every time we draw the scene we rotate the object by five degrees. That's all there is to it! You can get the completed source and execuatable here. One word of warning though when running the final executable. Texture mapping takes a lot of computer power, especially when the rendering is done entirely in software as is currently done on OS/2. Now with motion blur enabled, we are drawing textures five times per frame which is very computationally intensive. You may notice when you run this on the slower machines, (or machines with inefficient video drivers) that CPU usage hits 100% and your system becmes unresponsive. Before running this, close any open programs to prevent system lockup. Better yet, reduce the rendering thread to idle priority so that the regular applications will be unaffected by this demo.

There are a lot of effects that we can do with either or both of texture mapping and the accumulation buffer: non-uniform spotlight modelling, scene antialiasing, text antialiasing, shadows and depth of field effects are just some of the effects that can be done. I will try to cover some of the more useful and popular ones in the upcoming months.

I hope you liked this month's column; we did some pretty tricky effects and I hope it starts to show the power of what you can do with OpenGL and OS/2. Now I hope all of you are working on that next "I gotta have it!" application that will cause OS/2 to soar in popularity and become the dominant desktop platform. If you are working on something I'd be glad to hear what you are doing, and if you are serious and hope to make it commercial one day, drop me a line and I'll link to you from my web page. Also, if any of you have topics that you want discussed, or you can't figure out how to do something, drop me a line and I'll consider it for an upcoming article.