Implementing a Virtual Trackball or Examiner Viewer

Roger Crawfis

The Ohio State University

A common interaction style for three-dimensional graphics mimics the paradigm of holding an object in your hand and inspecting, or examining it. You can easily rotate about any axis, as well as bring it closer towards you. Without a data glove or other type of 3D-input device, we need to provide this interface with the customary 2D mouse. This tutorial will assume a two (or more) button mouse. The major source for the mathematics behind this comes from Ed Angel's OpenGL: A Primer.

The algorithm for accomplishing this, needs to perform the following steps (not neseccarily in order).

  1. Detect the left-button of the mouse being depressed.
  2. Keep track of the last known mouse position.
  3. Treat the mouse position as the projection of a point on the hemi-sphere down to the image plane (along the z-axis), and determine that point on the hemi-sphere.
  4. Detect the mouse movement
  5. Determine the great circle connecting the old mouse-hemi-sphere point to the current mouse-hemi-sphere point.
  6. Calculate the normal to this plane. This will be the axis about which to rotate.
  7. Set the OpenGL state to modify the MODELVIEW matrix.
  8. Read off the current matrix, since we want this operation to be the last transformation, not the first, and OpenGL does things LIFO.
  9. Reset the model-view matrix to the identity
  10. Rotate about the axis
  11. Multiply the resulting matrix by the saved matrix.
  12. Force a redraw of the scene.

Let's walk through these steps in more detail, and with code.

  1. Detect the left-button of the mouse being depressed.
  2. Keep track of the last known mouse position.

This will be operating system dependent. Under Microsoft Visual Studio, you need to catch the WM_LBUTTONDOWN event. In CLassWizard, select the View class. Scroll down the messages to find WM_LBUTTONDOWN, select it and click on Add Function.

  1. Detect the left-button of the mouse being depressed.
  2. Keep track of the last known mouse position (mapped to the hemi-sphere).
  3. Set the OpenGL state to modify the MODELVIEW matrix.

//

// The OnLButtonDown method of the View class is called whever the

// left mouse button is depressed. We simply save the point and

// set the current interaction state to the trackball. Future

// calls to OnMouseMove will check the current state.

//

void CSierpinskiSolidsView::OnLButtonDown(UINT nFlags, CPoint point)

{

//

// Turn on user interactive rotations.

// As the user moves the mouse, the scene will rotate.

//

Movement = ROTATE;

//

// Map the mouse position to a logical sphere location.

// Keep it in the class variable lastPoint.

//

lastPoint = trackBallMapping( point );

//

// Make sure we are modifying the MODELVIEW matrix.

//

glMatrixMode( GL_MODELVIEW );

CView::OnLButtonDown(nFlags, point);

}

3. Treat the mouse position as the projection of a point on the hemi-sphere down to the image plane (along the z-axis), and determine that point on the hemi-sphere.

We want grab a sphere with the mouse and drag it. What we want is the intersection of the ray going through the pixel with a sphere centered on the image plane. This sphere will have a finite extent. If the ray does not intersect it, we will find the closest point on the sphere that does. This will lie on the 2D circle in the image plane (z=0). The mouse point lies within this circle, then it needs to be pulled up to the surface. If we consider the sphere as having a unit radius, then we simple add a z component to our point to make it a unit length vector from the center. This is the point on the sphere. See the class notes for diagrams. The current window size in terms of pixels is saved in windowSize for reference. Also, we make use of a simple vector class Vec3f.

  • Treat the mouse position as the projection of a point on the hemi-sphere down to the image plane (along the z-axis), and determine that point on the hemi-sphere.
//
// Utility routine to calculate the 3D position of a
// projected unit vector onto the xy-plane. Given any
// point on the xy-plane, we can think of it as the projection
// from a sphere down onto the plane. The inverse is what we
// are after.
//
Vec3f CSierpinskiSolidsView::trackBallMapping(CPoint point)
{
Vec3f v;
float d;
v.x = (2.0*point.x - windowSize.x) / windowSize.x;
v.y = (windowSize.y - 2.0*point.y) / windowSize.y;
v.z = 0.0;
d = v.Length();
d = (d<1.0) ? d : 1.0;
v.z = sqrtf(1.001 - d*d);
v.Normalize(); // Still need to normalize, since we only capped d, not v.
return v;
}

    Detect the mouse movements and update the display.
  • Detect the mouse movement
void CSierpinskiSolidsView::OnMouseMove(UINT nFlags, CPoint point)
{

//
// Handle any necessary mouse movements
//
Vec3f direction;
float pixel_diff;
float rot_angle, zoom_factor;
Vec3f curPoint;
switch (Movement)
{

case ROTATE : // Left-mouse button is being held down
{
curPoint = trackBallMapping( point ); // Map the mouse position to a logical
// sphere location.
direction = curPoint - lastPoint;
float velocity = direction.Length();
if( velocity > 0.0001 ) // If little movement - do nothing.
{

  • Determine the great circle connecting the old mouse-hemi-sphere point to the current mouse-hemi-sphere point.
  • Calculate the normal to this plane. This will be the axis about which to rotate.

//
// Rotate about the axis that is perpendicular to the great circle connecting the mouse movements.
//
Vec3f rotAxis;
rotAxis.crossProd( lastPoint, curPoint );
rot_angle = velocity * m_ROTSCALE;

  • Read off the current matrix, since we want this operation to be the last transformation, not the first, and OpenGL does things LIFO.
  • Reset the model-view matrix to the identity
  • Rotate about the axis
  • Multiply the resulting matrix by the saved matrix.

//
// We need to apply the rotation as the last transformation.
// 1. Get the current matrix and save it.
// 2. Set the matrix to the identity matrix (clear it).
// 3. Apply the trackball rotation.
// 4. Pre-multiply it by the saved matrix.
//
glGetFloatv( GL_MODELVIEW_MATRIX, (GLfloat *) objectXform );
glLoadIdentity();
glRotatef( rot_angle, rotAxis.x, rotAxis.y, rotAxis.z );
glMultMatrixf( (GLfloat *) objectXform );

  • Force a redraw of the scene.

//
// If we want to see it, we need to force the system to redraw the scene.
//
Invalidate( TRUE );
}
break;
}

  • Bonus: Code for zooming into the picture. Note, that this is done in GL_PROJECTION
    mode. We simply make an eye-space zoom the very first thing to occur.

case ZOOM : // Right-mouse button is being held down
//
// Zoom into or away from the scene based upon how far the
// mouse moved in the x-direction.
// This implementation does this by scaling the eye-space.
// This should be the first operation performed by the GL_PROJECTION matrix.
// 1. Calculate the signed distance
// a. movement to the left is negative (zoom out).
// b. movement to the right is positive (zoom in).
// 2. Calculate a scale factor for the scene s = 1 + a*dx
// 3. Call glScalef to have the scale be the first transformation.
//
pixel_diff = point.x - lastPoint.x;
zoom_factor = 1.0 + pixel_diff * m_ZOOMSCALE;
glScalef( zoom_factor, zoom_factor, zoom_factor );
//
// Set the current point, so the lastPoint will be saved properly below .
//
curPoint.x = (float) point.x; curPoint.y = (float) point.y; (float) curPoint.z = 0;
//
// If we want to see it, we need to force the system to redraw the scene.
//
Invalidate( TRUE );
break;
}

  • Save the current mouse point as the starting point for the next movement.

//
// Save the location of the current point for the next movement.
//
lastPoint = curPoint;
CView::OnMouseMove(nFlags, point);

}

That's almost it. The only thing left to do is to catch the WM_LBUTTONUP event and turn off rotations for when the mouse moves. FYI. The method OnMouseMove is being called all of the time, even when a mouse button is not depressed. In this case, it just passes right through without doing anything.

  • Turn off the rotations
void CSierpinskiSolidsView::OnLButtonUp(UINT nFlags, CPoint point)
{
//
// Turn-off the rotations.
//
Movement = NONE;

CView::OnLButtonUp(nFlags, point);
}

That's it. For the Zoom above, we simply add routines to set the Movement=Zoom when the right mouse button is depressed, and to unset it when it is released.

Source with a Sierpinski Gasket generator


Last updated Tuesday, January 21, 2003