ArcBall Rotation

ArcBall Rotation Control, Revisited
By Terence J. Grant (tjgrant@tatewake.com)

Wouldn’t it be great to rotate your model at will, just by using the mouse? With an ArcBall you can do just that. In this document, I’ll touch on my implementation, and considerations for adding it to your own projects.

My implementation of the ArcBall class is based on Bretton Wade’s, which is based on Ken Shoemake’s from the Graphic Gems series of books. However, I did a little bug fixing and optimization for our purposes.

The ArcBall works by mapping click coordinates in a window directly into the ArcBall’s sphere coordinates, as if it were directly in front of you.

To accomplish this, first we simply scale down the mouse coordinates from the range of [0...width), [0...height) to [-1...1], [1...-1] – (Keep in mind that we flip the sign of Y so that we get the correct results in OpenGL.) And this essentially looks like:

MousePt.X  =  ((MousePt.X / ((Width  – 1) / 2)) – 1);
MousePt.Y  = -((MousePt.Y / ((Height – 1) / 2)) – 1);

The only reason we scale the coordinates down to the range of [-1...1] is to make the math simpler; by happy coincidence this also lets the compiler do a little optimization.

Next we calculate the length of the vector and determine whether or not it’s inside or outside of the sphere bounds. If it is within the bounds of the sphere, we return a vector from within the inside the sphere, else we normalize the point and return the closest point to outside of the sphere.

Once we have both vectors, we can then calculate a vector perpendicular to the start and end vectors with an angle, which turns out to be a quaternion. With this in hand we have enough information to generate a rotation matrix from, and we’re home free.

The ArcBall is instantiated using the following constructor. NewWidth and NewHeight are essentially the width and height of the window.

ArcBall_t::ArcBall_t(GLfloat NewWidth, GLfloat NewHeight)

When the user clicks the mouse, the start vector is calculated based on where the click occurred.

void    ArcBall_t::click(const Point2fT* NewPt)

When the user drags the mouse, the end vector is updated via the drag method, and if a quaternion output parameter is provided, this is updated with the resultant rotation.

void    ArcBall_t::drag(const Point2fT* NewPt, Quat4fT* NewRot)

If the window size changes, we simply update the ArcBall with that information:

void    ArcBall_t::setBounds(GLfloat NewWidth, GLfloat NewHeight)

When using this in your own project, you’ll want some member variables of your own.

// Final Transform
Matrix4fT	Transform = {  1.0f,  0.0f,  0.0f,  0.0f,
			       0.0f,  1.0f,  0.0f,  0.0f,
			       0.0f,  0.0f,  1.0f,  0.0f,
			       0.0f,  0.0f,  0.0f,  1.0f };

Matrix3fT	LastRot   = {  1.0f,  0.0f,  0.0f,					// Last Rotation
			       0.0f,  1.0f,  0.0f,
			       0.0f,  0.0f,  1.0f };

Matrix3fT	ThisRot   = {  1.0f,  0.0f,  0.0f,					// This Rotation
			       0.0f,  1.0f,  0.0f,
			       0.0f,  0.0f,  1.0f };

ArcBallT	ArcBall(640.0f, 480.0f);						// ArcBall Instance
Point2fT	MousePt;								// Current Mouse Point
bool		isClicked  = false;							// Clicking The Mouse?
bool		isRClicked = false;							// Clicking The Right Mouse Button?
bool		isDragging = false;							// Dragging The Mouse?

Transform is our final transform- our rotation and any optional translation you may want to provide. LastRot is the last rotation we experienced at the end of a drag. ThisRot is the rotation we experience while dragging. All are initialized to identity.

When we click, we start from an identity rotation state. When we drag, we are then calculating the rotation from the initial click point to the drag point. Even though we use this information to rotate the objects on screen, it is important to note that we are not actually rotating the ArcBall itself. Therefore to have cumulative rotations, we must handle this ourselves.

This is where LastRot and ThisRot come into play. LastRot can be defined as “all rotations up till now”, whereas ThisRot can be defined by “the current rotation.” Every time a drag is started, ThisRot is modified by the original rotation. It is then updated to the product of itself * LastRot. (Then the final transformation is also updated.) Once a drag is stopped, LastRot is then assigned the value of ThisRot.

If we didn’t accumulate the rotations ourselves, the model would appear to snap to origin each time that we clicked. For instance if we rotate around the X-axis 90 degrees, then 45 degrees, we would want to see 135 degrees of rotation, not just the last 45.

For the rest of the variables (except for isDragged), all you need to do is update them at the proper times based on your system. ArcBall needs its bounds reset whenever your window size changes. MousePt gets updated whenever your mouse moves, or just when the mouse button is down. isClicked / isRClicked whenever the left/right mouse button is clicked, respectively. isClicked is used to determine clicks and drags. We’ll use isRClicked to reset all rotations to identity.

The additional system update code under NeHeGL/Windows looks something like this:

void ReshapeGL (int width, int height)
{
	. . .
	ArcBall.setBounds((GLfloat)width, (GLfloat)height);				// Update Mouse Bounds For ArcBall
}

// Process Window Message Callbacks
LRESULT CALLBACK WindowProc (HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	. . .
	// Mouse Based Messages For ArcBall
	case WM_MOUSEMOVE:
		MousePt.s.X = (GLfloat)LOWORD(lParam);
		MousePt.s.Y = (GLfloat)HIWORD(lParam);
		isClicked   = (LOWORD(wParam) & MK_LBUTTON) ? true : false;
		isRClicked  = (LOWORD(wParam) & MK_RBUTTON) ? true : false;
		break;

	case WM_LBUTTONUP:   isClicked  = false; break;
	case WM_RBUTTONUP:   isRClicked = false; break;
	case WM_LBUTTONDOWN: isClicked  = true;  break;
	case WM_RBUTTONDOWN: isRClicked = true;  break;
	. . .
}

Once we have the system update code in place, its time to put the click logic in place. This is very self-explanatory once you know everything above.

if (isRClicked)										// If Right Mouse Clicked, Reset All Rotations
{
	// Reset Rotation
	Matrix3fSetIdentity(&LastRot);

	// Reset Rotation
	Matrix3fSetIdentity(&ThisRot);

	// Reset Rotation
	Matrix4fSetRotationFromMatrix3f(&Transform, &ThisRot);
}

if (!isDragging)									// Not Dragging
{
	if (isClicked)									// First Click
	{
		isDragging = true;							// Prepare For Dragging
		LastRot = ThisRot;							// Set Last Static Rotation To Last Dynamic One
		ArcBall.click(&MousePt);						// Update Start Vector And Prepare For Dragging
	}
}
else
{
	if (isClicked)  //Still clicked, so still dragging
	{
		Quat4fT     ThisQuat;

		ArcBall.drag(&MousePt, &ThisQuat);					// Update End Vector And Get Rotation As Quaternion
		Matrix3fSetRotationFromQuat4f(&ThisRot, &ThisQuat);			// Convert Quaternion Into Matrix3fT
		Matrix3fMulMatrix3f(&ThisRot, &LastRot);				// Accumulate Last Rotation Into This One
		Matrix4fSetRotationFromMatrix3f(&Transform, &ThisRot);			// Set Our Final Transform's Rotation From This One
	}
	else										// No Longer Dragging
		isDragging = false;
}

This takes care of everything for us. Now all we need to do is apply the transformation to our models and we’re done. It’s really simple:

	glPushMatrix();									// Prepare Dynamic Transform
	glMultMatrixf(Transform.M);							// Apply Dynamic Transform

	glBegin(GL_TRIANGLES);								// Start Drawing Model
	. . .
	glEnd();									// Done Drawing Model

	glPopMatrix();									// Unapply Dynamic Transform

I have included a sample, which demonstrates everything above. You’re not locked in to using my math types or functions; in fact I would suggest fitting this in to your own math system if you’re confident enough. However, everything is self-contained otherwise and should work on its own.

Now after seeing how simple this is, you should be well on your way to adding ArcBall to your own projects. Enjoy!

Terence J. Grant

* DOWNLOAD Visual C++ Code For This Lesson.

* DOWNLOAD Borland C++ Builder 6 Code For This Lesson. ( Conversion by Le Thanh Cong )
* DOWNLOAD Delphi Code For This Lesson. ( Conversion by Michal Tucek )
* DOWNLOAD Dev C++ Code For This Lesson. ( Conversion by Victor Andrée )
* DOWNLOAD Linux/GLX Code For This Lesson. ( Conversion by George Gensure )
* DOWNLOAD Python Code For This Lesson. ( Conversion by Brian Leair )
* DOWNLOAD Visual Studio .NET Code For This Lesson. ( Conversion by Joachim Rohde )

< Lesson 47