Using OpenGL in Visual C++: Part IV Simple 3-D Graphics

by Alan Oursland

[Download the example source] Simple 3-D Graphics

The sample program presented in this section will show you how to use basic 3-D graphics. It will show you how to set up a perspective view, define and object and transform that object in space. This section assumes some knowledge of graphics. If you don't know what a word means, you can probably look it up in most graphics books. The Foley and Van Dam book listed on this page will definitely have the definitions.

Create an OpenGL window with double buffering enabled. Set up the view class OnSize and OnPaint message handlers just as they are in the previous program. Add a RenderScene function to the document class, but do not put any OpenGL commands into it yet.

First we need to change our viewing coordinate system. gluOrtho2D, the function we have been calling to set up our projection matrix, actually creates a 3 dimensional view with the near clipping plane at z=-1 and the far clipping plane at 1. All of the "2-D" commands we have been calling have actually been 3-D calls where the z coordinate was zero. Surprise! You've been doing 3-D programming all along. To view our cube, we would like to use perspective projection. To set up a perspective projection we need to change OnSize to the following:

void CGLSample4View::OnSize(UINT nType, int cx, int cy) 
{
CView::OnSize(nType, cx, cy);

GLsizei width, height;
GLdouble aspect;

width = cx;
height = cy;

if (cy==0)
aspect = (GLdouble)width;
else
aspect = (GLdouble)width/(GLdouble)height;

glViewport(0, 0, width, height);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(45, aspect, 1, 10.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();

glDrawBuffer(GL_BACK);
}

For those who didn't heed my warning above, orthogonal projection maps everything in three dimensional space onto a two dimensional surface at right angles. The result is everything looks the same size regardless of its distance from the eye point. Perspective project simulates light passing through a point (as if you were using a pinhole camera). The result is a more natural picture where distant objects appear smaller. The gluPerspective call above sets the eye point at the origin, gives us a 45 angle field of view, a front clipping plane at 1, and a back clipping plane at 10.

Now lets draw our cube. Edit RenderScene to look like this:

void CGLSample4Doc::RenderScene(void)
{
glClear(GL_COLOR_BUFFER_BIT);

glPushMatrix();
glTranslated(0.0, 0.0, -8.0);
glRotated(m_xRotate, 1.0, 0.0, 0.0);
glRotated(m_yRotate, 0.0, 1.0, 0.0);

glBegin(GL_POLYGON);
glNormal3d( 1.0, 0.0, 0.0);
glVertex3d( 1.0, 1.0, 1.0);
glVertex3d( 1.0, -1.0, 1.0);
glVertex3d( 1.0, -1.0, -1.0);
glVertex3d( 1.0, 1.0, -1.0);
glEnd();

glBegin(GL_POLYGON);
glNormal3d( -1.0, 0.0, 0.0);
glVertex3d( -1.0, -1.0, 1.0);
glVertex3d( -1.0, 1.0, 1.0);
glVertex3d( -1.0, 1.0, -1.0);
glVertex3d( -1.0, -1.0, -1.0);
glEnd();

glBegin(GL_POLYGON);
glNormal3d( 0.0, 1.0, 0.0);
glVertex3d( 1.0, 1.0, 1.0);
glVertex3d( -1.0, 1.0, 1.0);
glVertex3d( -1.0, 1.0, -1.0);
glVertex3d( 1.0, 1.0, -1.0);
glEnd();

glBegin(GL_POLYGON);
glNormal3d( 0.0, -1.0, 0.0);
glVertex3d( -1.0, -1.0, 1.0);
glVertex3d( 1.0, -1.0, 1.0);
glVertex3d( 1.0, -1.0, -1.0);
glVertex3d( -1.0, -1.0, -1.0);
glEnd();

glBegin(GL_POLYGON);
glNormal3d( 0.0, 0.0, 1.0);
glVertex3d( 1.0, 1.0, 1.0);
glVertex3d( -1.0, 1.0, 1.0);
glVertex3d( -1.0, -1.0, 1.0);
glVertex3d( 1.0, -1.0, 1.0);
glEnd();

glBegin(GL_POLYGON);
glNormal3d( 0.0, 0.0, -1.0);
glVertex3d( -1.0, 1.0, -1.0);
glVertex3d( 1.0, 1.0, -1.0);
glVertex3d( 1.0, -1.0, -1.0);
glVertex3d( -1.0, -1.0, -1.0);
glEnd();
glPopMatrix();
}

Add member variables to the document class for m_xRotate and m_yRotate (look at the function definitions to determine the correct type). Add member variables and event handlers to the view class to modify the document variables when you drag with the left mouse button just like we did in the last example (hint: Handle the WM_LBUTTONDOWN, WM_LBUTTONUP, and WM_MOUSEMOVE events. Look at the sample source code if you need help). Compile and run the program. You should see a white cube that you can rotate. You will not be able to see any discernible feature yet since the cube has no surface definition and there is no light source. We will add these features next.

Add the following lines to the beginning of RenderScene:

GLfloat RedSurface[] = { 1.0f, 0.0f, 0.0f, 1.0f};
GLfloat GreenSurface[] = { 0.0f, 1.0f, 0.0f, 1.0f};
GLfloat BlueSurface[] = { 0.0f, 0.0f, 1.0f, 1.0f};

These define surface property values. Once again, the numbers represent the red, green, blue and alpha components of the surfaces. The surface properties are set with the command glMaterial. Add glMaterialCalls to the following locations:

glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, RedSurface);
glBegin(GL_POLYGON);
   ...
glEnd();

glBegin(GL_POLYGON);
   ...
glEnd();

glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, GreenSurface);
glBegin(GL_POLYGON);
   ...
glEnd();                  

glBegin(GL_POLYGON);
   ...
glEnd();

glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, BlueSurface);
glBegin(GL_POLYGON);
   ...
glEnd();

glBegin(GL_POLYGON);
   ...
glEnd();

These new calls make two of the cube faces red, two faces green, and two faces blue. The commands set the ambient color for front and back of each face. However, the cube will still appear featureless until the lighting model is enabled. To do this add the following command to the end of CGLSample4View::OnSize:

glEnable(GL_LIGHTING);

Compile and run the program. You should see one of the blue faces of the cube. Rotate the cube with your mouse. You will notice the cube looks very strange. Faces seem to appear and disappear at random. This is because we are simply drawing the faces of the cube with no regard as to which is in front. When we draw a face that is in back, it draws over any faces in front of it that have been drawn. The solution to this problem is z-buffering.

The z-buffer holds a value for every pixel on the screen. This value represents how close that pixel is to the eye point. Whenever OpenGL attempts to draw to a pixel, it checks the z-buffer to see if the new color is closer to the eye point than the old color. If it is the pixel is set to the new color. If not, then the pixel retains the old color. As you can guess, z-buffering can take up a large amount of memory and CPU time. The cDepthBits parameter in the PIXELFORMATDESCRIPTOR we used in SetWindowPixelFormat defines the number of bits in each z-buffer value. Enable z-buffering by adding the following command at the end of OnSize:

glEnable(GL_DEPTH_TEST);

We also need to clear the z-buffer when we begin a new drawing. Change the glClear command in RenderScene to the following:

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

Compile and run the program to see the results.

We now have a colorful cube that rotates in space and draws correctly, but it is very faint. Let's add a light to the scene so that we can see the cube better. Add the following declaration to the beginning of RenderScene:

GLfloat LightAmbient[] = { 0.1f, 0.1f, 0.1f, 0.1f };
GLfloat LightDiffuse[] = { 0.7f, 0.7f, 0.7f, 0.7f };
GLfloat LightSpecular[] = { 0.0f, 0.0f, 0.0f, 0.1f };
GLfloat LightPosition[] = { 5.0f, 5.0f, 5.0f, 0.0f };

These will serve as the property values for our light. Now add the following commands just after glClear in RenderScene:

glLightfv(GL_LIGHT0, GL_AMBIENT, LightAmbient);
glLightfv(GL_LIGHT0, GL_DIFFUSE, LightDiffuse);
glLightfv(GL_LIGHT0, GL_SPECULAR, LightSpecular);
glLightfv(GL_LIGHT0, GL_POSITION, LightPosition);
glEnable(GL_LIGHT0);

glLight defines properties for light sources. OpenGL's light sources are all created within the implementation of OpenGL. Each light source has an identifier GL_LIGHTi where i is zero to GL_MAX_LIGHTS. The above commands set the ambient, diffuse, and specular properties, as well as the position, of light zero. glEnable(GL_LIGHT0) turns on the light.

The program is currently wasting time by drawing the interior faces of the cube with our colored surfaces. To fix this, change the GL_FRONT_AND_BACK parameter in all of the glMaterialfv calls to GL_FRONT. We also want to set the diffuse reflectivity of the cube faces now that we have a light source. To do this, change the GL_AMBIENT parameter in the glMaterialfv calls to GL_AMBIENT_AND_DIFFUSE. Compile and run the program.

You now have a program that displays a lighted, multi-colored cube in three dimensions that uses z-buffering and double buffering. Go ahead and pat yourself on the back. You deserve it.

Conclusion

This concludes the construction of GLSample4 and this tutorial. You should now know how to set up an OpenGL program in Windows, and should also understand some of the basic graphics commands. If you wish to explore OpenGL further, I recommend studying the sample programs in the Microsoft Platform SDK. If you would like to learn more about graphics in general, I recommend the following books. It really is necessary to understand the basics of the material in either of these books if you want to do any serious 3-D graphics.

  • Foley, J. D. and Dam, A. V. and Feiner, S. K. and Hughes., J. F. Computer Graphics, Principles and Practice. Addison-Wesley Publishing Company: Reading, Massachusetts, 1990
  • Hill, F. S. Computer Graphics. MacMillian Publishing Company: New York, 1990.
You may also visit these sites to learn more about OpenGL programming:
  • OpenGL WWW Center
  • Microsoft OpenGL Information Sheet
  • Microsoft also offers the following OpenGL articles in MSDN Online
    • Windows NT OpenGL: Getting Started
    • OpenGL I: Quick Start
    • OpenGL II: Windows Palettes in RGBA Mode
    • OpenGL III: Building an OpenGL C++ Class
    • OpenGL IV: Color Index Mode
    • OpenGL V: Translating Windows DIBs
    • Usenet Graphics Related FAQs
SAMPLE: MFCOGL a Generic MFC OpenGL Code Sample (Q127071)

Good luck and happy programming. I would appreciate any comments or suggestions for this tutorial. Please email me at naoursla@bellsouth.net. Thanks!

Developed Under:

Visual C++ 5+