Tutorial 2: Working in 3D

This tutorial will focus on drawing and 3D.  We will initially create a simple bar-bell that you can manipulate on the screen.  Then we'll play with the bar-bell to create a composite image.  Finally, your task is to create and manipulate an image with two tetrahedrons in it.

This tutorial is emphasizing using 3D coordinates, working with transformations, and composite objects.

Creating a 3D Viewing Environment

Either reuse or create a new COpenGLWnd application like you did in tutorial 1.  However, this time we'll configure the environment for 3D viewing.  Use the following code as the body of OnGLDraw():

 

glClearColor(0.0f, 0.0f, 0.0f, 0.0f) ;
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

//
// Set up the camera
//

glMatrixMode(GL_PROJECTION);
glLoadIdentity();

// Determine the screen size so we can determine the aspect ratio
int width, height;
GetSize(width, height);
GLdouble aspectratio = GLdouble(width) / GLdouble(height);

// Set the camera parameters
gluPerspective(25.,         // Vertical FOV degrees.
               aspectratio, // The aspect ratio.
               10.,         // Near clipping 40/130
               200.);       // Far clipping

// Set the camera location
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();

gluLookAt(20., 10., 50.,    // eye x,y,z
          0., 0., 0.,       // center x,y,z
          0., 1., 0.);      // Up direction

//
// Some standard parameters
//

// Enable depth test
glEnable(GL_DEPTH_TEST);

// Cull backfacing polygons
glCullFace(GL_BACK);
glEnable(GL_CULL_FACE);

// Draw a coordinate axis
glColor3d(0., 1., 1.);

glBegin(GL_LINES);
  glVertex3d(0., 0., 0.);
  glVertex3d(12., 0., 0.);
  glVertex3d(0., 0., 0.);
  glVertex3d(0., 12., 0.);
  glVertex3d(0., 0., 0.);
  glVertex3d(0., 0., 12.);
glEnd();


// 
// INSERT DRAWING CODE HERE
//

glFlush();

This only draws a coordinate axis.  Be sure it draws correctly.

Using my Box Code

Add the following code to your class.  Be sure to add the function definition for Box() to the header for the CChildView class.  

 

//
//        Name : Quad()
// Description : Inline function for drawing
// a quadralateral.
//

inline void Quad(GLdouble *v1, GLdouble *v2, GLdouble *v3, GLdouble *v4)
{
   glBegin(GL_QUADS);
   glVertex3dv(v1);
   glVertex3dv(v2);
   glVertex3dv(v3);
   glVertex3dv(v4);
   glEnd();
}

//
//        Name : CChildView::Box()
// Description : Draw an arbitrary size box. p_x,
//              p_y, and p_z are the height of
//              the box. We'll use this 
//               as a common primitive.
//      Origin : The back corner is at 0, 0, 0, and
//               the box is entirely in the
//               positive octant.
//

void CChildView::Box(GLdouble p_x, GLdouble p_y, 
                     GLdouble p_z, const GLdouble *p_color)
{
   GLdouble a[] = {0., 0., p_z};
   GLdouble b[] = {p_x, 0., p_z};
   GLdouble c[] = {p_x, p_y, p_z};
   GLdouble d[] = {0., p_y, p_z};
   GLdouble e[] = {0., 0., 0.};
   GLdouble f[] = {p_x, 0., 0.};
   GLdouble g[] = {p_x, p_y, 0.};
   GLdouble h[] = {0., p_y, 0.};

   // I'm going to mess with the colors a bit so 
   // the faces will be visible in solid shading
   glColor3d(p_color[0], p_color[1], p_color[2]);
   Quad(a, b, c, d); // Front

   glColor3d(p_color[0] * 0.95, p_color[1] * 0.95, p_color[2] * 0.95);
   Quad(c, b, f, g); // Right

   glColor3d(p_color[0] * 0.85, p_color[1] * 0.85, p_color[2] * 0.85);
   Quad(h, g, f, e); // Back

   glColor3d(p_color[0] * 0.90, p_color[1] * 0.90, p_color[2] * 0.90);
   Quad(d, h, e, a); // Left

   glColor3d(p_color[0] * 0.92, p_color[1] * 0.92, p_color[2] * 0.92);
   Quad(d, c, g, h); // Top

   glColor3d(p_color[0] * 0.80, p_color[1] * 0.80, p_color[2] * 0.80);
   Quad(e, f, b, a); // Bottom
}

Add the following code to your OnGLDraw() function where it says "INSERT DRAWING CODE HERE":

const double RED[] = {0.8, 0.0, 0.0};
Box(1., 0.5, 1., RED);

This will draw a single box.

Making a Bar Bell

I want to create a bar-bell out of three boxes.  This will take three boxes stacked on top of each other.  The ends will be boxes that are 2" by 1" by 2".  The bar between the ends will be a box that is 1/2" by 4" by 1/2".  I would like to have the bar-bell sitting on the origin with the Y axis through its center.  

If you examine my Box code, you'll find that it places the back-left-bottom corner of the box at the origin.  For a bar-bell end, we need the center of the bottom of the box on the origin.  If the box is 2" square, we need to translate the box back 1" in the X and Z dimensions.  Change the code you just added to this:

glTranslated(-1., 0., -1.);
const double RED[] = {0.8, 0.0, 0.0};
B
ox(2., 1., 2., RED);

Note that the box is now centered on the Y axis when you run the program.  

In OpenGL, all translation and rotation operations are cumulative.  Everything you draw after the glTranslated() call will also be translated back and to the left by 1".  You probably don't want that.  The following will create the two ends of our bar-bell by moving the second end up 5" and centering it as well.  But, this will fail.  Run it and try to figure out why:

glTranslated(-1., 0., -1.);
const double RED[] = {0.8, 0.0, 0.0};
B
ox(2., 1., 2., RED);

glTranslated(-1., 5., -1.);
Box(2., 1., 2., RED);

Now, you can fix this by not putting in the X and Z translations:

glTranslated(-1., 0., -1.);
const double RED[] = {0.8, 0.0, 0.0};
B
ox(2., 1., 2., RED);

glTranslated(0., 5., 0.);
Box(2., 1., 2., RED);

But, that won't help us much, later when we do the shaft. Let's try to add the shaft, now:

glTranslated(-1., 0., -1.);
const double RED[] = {0.8, 0.0, 0.0};
Box(2., 1., 2., RED);

glTranslated(0., 5., 0.);
Box(2., 1., 2., RED);

glTranslated(-0.25, 1., -0.25);
Box(0.5, 4.0, 0.5, RED);

Run this and see what you get.  

Now, we could come up with the right translation for the shaft, or we could write translation operations that cancel previous translation operations.  However, this gets too difficult to do pretty quick AND we will accumulate math tolerance errors. Instead, we can utilize the functions glPushMatrix() and glPopMatrix():

glPushMatrix() - Save the current transformation state.
glPopMatrix() - Restore a transformation state from the stack.

Basically, when you do a glPushMatrix(), you are saving the current state of all translations and rotations (and scale and skew).  When you do a glPopMatrix(), you are restoring that state, effectively canceling all operations since the last glPushMatrix().  Be sure your pushes and pops match up.  

So, we'll use glPushMatrix()/glPopMatrix() to make our bar-bell creation easier:

glPushMatrix();
glTranslated(-1., 0., -1.);
const double RED[] = {0.8, 0.0, 0.0};
B
ox(2., 1., 2., RED);

glTranslated(0., 5., 0.);
Box(2., 1., 2., RED);
glPopMatrix();

glPushMatrix();
glTranslated(-0.25, 1., -0.25);
Box(0.5, 4.0, 0.5, RED);

glPopMatrix();

Try this.  Note that I always push before I mess with the state and pop after I am done.  It's a good habit to get into.  Because translation and rotation operations are cumulative, you don't want a function to leave one active.

Go ahead and make this code into a function called BarBell().

Displaying Two and Three Bar-Bells

Suppose I want to draw two bar-bells, 5 inches apart.  I can do this this way:

BarBell();

glPushMatrix();
glTranslated(5., 0., 0.);
BarBell();
glPopMatrix();

If you trace carefully what is happening, you'll see that there are two push operations in a row for this example.  

Go ahead and create a third bar-bell 5" in front of the first one. 

Rotating the bar-bell around its center

For now, go ahead and remove the second two bar-bells, leaving only the first one on the Y axis.  I would like to rotate the bar-bell around the Y axis.  To do this, use the glRotated() function:

glRotated(GLdouble d, GLdouble x, GLdouble y, GLdouble z) - Rotate around the vector indicated by x,y,z by d degrees.  Rotation uses the right-hand rule:  If you hold your right hand with your fingers curved and your thumb pointing in the direction of the rotation vector, the direction your fingers curl is the direction of positive rotation.    So, if I want to rotate something around the Z axis such that it appears to be going clockwise when viewed from the front, I would use a negative angle of rotation.

Try this code:

glPushMatrix();
glRotated(45., 0., 0., 1.);
BarBell();
glPopMatrix();

Note the rotation is around the Z axis, so the object is NOT rotating around its center.  We need to translate the center of the object to the origin first.  Try this:

glPushMatrix();
glTranslated(0., -3., 0.);
BarBell();
glPopMatrix();

Note that you can now think of the combination of statements:  glTranslated(0., -3., 0.); BarBell(); as the unit:  BarBellCenteredOnOrigin().  You could actually create a function named that if you like.  

So, since it's centered on the origin, rotation is now easy:

glPushMatrix();
glRotated(45., 0., 0., 1.);
glTranslated(0., -3., 0.);
BarBell();
glPopMatrix();

Note the order.  You read the operations in reverse order.  We are:

  1. Translating down by 3 inches.
  2. Rotating counter-clockwise by 45 degrees.

To get the center back where it started, just translate again:

glPushMatrix();
glTranslated(0., 3., 0.);
glRotated(45., 0., 0., 1.);
glTranslated(0., -3., 0.);
BarBell();
glPopMatrix();

Try reversing the order of the operations here and see what happens.  Why does it fail?

Remember:  You can always think of a set of lines here as a complete thing and you read the operations in reverse order:

glTranslated(0., 3., 0.);
glRotated(45., 0., 0., 1.);
glTranslated(0., -3., 0.);
BarBell();

can be thought of as:

  1. Draw a BarBell()
  2. Move it down 3"
  3. Rotate it counter-clockwise by 45 degrees.
  4. Move it back up 3"

Making your Bar-Bell Spin

I think it would be fun to make the bar-bell spin. First, add two member variables to CChildView: double m_spinangle and UINT m_spintimer. We're going to create a Windows Timer that will be called regularly to force an update of the angle and a redraw of the bar-bell. In the constructor, the member variables are initialized automatically to 0 by the member variable wizard.

Change the glRotated function to rotate by m_spinangle rather than 45.

Create a menu option:  Spin under some existing menu or in a new menu using the ResourceView and menu editor.  Create a windows message handler for the menu option and put this code in it:

if(m_spintimer)
{
   KillTimer(m_spintimer);
   m_spintimer = 0;
}
else
{
   m_spintimer = SetTimer(1, 30, NULL);
}

If m_spintimer is zero, we don't have an active timer and nothing is spinning. Create a new timer using the SetTimer function. This is a member function of CWnd, the superclass for windows in MFC. The first parameter is a non-zero unique ID for the timer. The second is the timer duration in milliseconds. If m_spintimer is not zero, we already have an active timer, so we kill it.

Finally, we need to add a windows message handler to CChildView for the WM_TIMER message. In order to add message handlers to a class you have to click on the class in Class View. Then go to the properties box and click on the icon that represents messages. In the properties box find the WM_TIMER message and add it using the menu to its right. In the body of that handler, just increment m_spinangle by the amount you want it to spin each 30ms and call Invalidate() to force a new draw.

If you want to make something that looks really cool, try spinning around more than one axis at a time. You might also want to select more interesting colors.

Flicker

You may have noticed that animation can cause problems with flicker.  You can visibly see the window be cleared and then redrawn, so lines that don't move seem to have a flash characteristic.  The way you fix flickering is to use double buffering.  Most OpenGL implementations support the idea of drawing to an offscreen buffer, then switching that buffer onto the screen instantly.  When double buffering is used, only fully draw screens are seen.  This eliminates flicker.

You can enable double buffering by adding the following line to your CChildView constructor:

    SetDoubleBuffer(true);

Note that this must be done in the constructor before any windows are created.  Try this and see how much it changes the appearance of your spinning barbell.

A Good Exercise

  1. Create a function Tetrahedron that creates an equilateral tetrahedron with a given height passed as a parameter.
  2. Write the code to display two of these, one sitting centered on the origin, the other upside down such that the tips of the two tetrahedrons touch.
  3. Make the two tetrahedrons spin around the Y axis in opposite directions.

Some useful information about tetrahedrons

Let s be the height of an equilateral tetrahedron.  The length l of a leg is:  sqrt(3./2. * s * s).  The radius (distance from the center of a face to a vertex) is sqrt(1./3. * l * l).  If you draw a line from a vertex though the center of  a face to the center of the far edge, the center is 2/3 of the way to the far edge.  This should give you all you need to determine the vertices, though you may need to draw some pictures.  

Make your tetrahedron function center the object on the Y axis with the base sitting on the origin.