Implementing a Virtual Trackball or Examiner Viewer
Roger Crawfis
The Ohio State University
A common interaction style for threedimensional 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 3Dinput 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).
 Detect the leftbutton of the mouse being depressed.
 Keep track of the last known mouse position.
 Treat the mouse position as the projection of a point on the hemisphere down to the image plane (along the zaxis), and determine that point on the hemisphere.
 Detect the mouse movement
 Determine the great circle connecting the old mousehemisphere point to the current mousehemisphere point.
 Calculate the normal to this plane. This will be the axis about which to rotate.
 Set the OpenGL state to modify the MODELVIEW matrix.
 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 modelview matrix to the identity
 Rotate about the axis
 Multiply the resulting matrix by the saved matrix.
 Force a redraw of the scene.
Let's walk through these steps in more detail, and with code.
 Detect the leftbutton of the mouse being depressed.
 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.
 Detect the leftbutton of the mouse being depressed.
 Keep track of the last known mouse position (mapped to the hemisphere).
 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 hemisphere down to the image plane (along the zaxis), and determine that point on the hemisphere.
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 hemisphere down to the image plane (along the zaxis), and determine that point on the hemisphere.

//
// Utility routine to calculate the 3D position of a
// projected unit vector onto the xyplane. Given any
// point on the xyplane, 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 : // Leftmouse 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 mousehemisphere point to the current mousehemisphere 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 modelview 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. Premultiply 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 eyespace zoom the very first thing to occur.

case ZOOM : // Rightmouse button is being held down
//
// Zoom into or away from the scene based upon how far the
// mouse moved in the xdirection.
// This implementation does this by scaling the eyespace.
// 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.

void CSierpinskiSolidsView::OnLButtonUp(UINT nFlags, CPoint point)
{
//
// Turnoff 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