Struggling with ArcBall

I’ve found some good advice here in the past and am hoping that someone can set be straight with an ArcBall implementation.

I have been having a hard time making this work in an application and have stripped it down to the bare bones in a test application. It just draws a set of axes (arrows) at the origin, which the user can rotate. Still having the same problem. I thought I pretty much understood the whole matrix thing… but maybe not.

This is not the ‘gluUnproject’ method. Instead I am setting the sphere of rotation to a fixed size based on the window size. Start and end mouse coordinates are projected onto the surface of that sphere. The angle between them gives the rotation angle, and the normal vector gives the axis of rotation. Some of the methods e.g. MousePosToSphere() are third party code, but from testing I think they are good. I believe my problem is at the final stage. The rotation seen is around some axis which is not aligned with the direction of mouse movement, which to me suggests a problem with the cumulative rotations.

The active rotation is stored in a ‘current rotation’ matrix, and on completion (MouseUp event) this is multiplied with the ‘completed rotations’ matrix. At that point the ‘current rotation’ matrix is reset to the identity matrix.

When rendering the scene, again the ‘current’ and ‘complete’ rotation matrices are multiplied.

This code is where the rotations are applied (C#, OpenTK):

private void glControl1_MouseUp(object sender, MouseEventArgs e)
{
// Store the rotation...
    rtn_complete = rtn_active * rtn_complete; // combine the two matrices
    rtn_active = Matrix4.Identity; // reset
}

and in the _Paint event:

GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

GL.LoadIdentity(); // start from scratch
Matrix4 modelview = Matrix4.LookAt(camera.camerapos, camera.cameratarget, camera.cameraup);
GL.LoadMatrix(ref modelview);

// Rotate the view...
Matrix4 m = rtn_active * rtn_complete;
GL.MultMatrix(ref m);
                
GL.CallList(1); // draw the axes
glControl1.SwapBuffers();

In the above, I have tried both rotation sequences i.e. applying completed followed by active, and vice versa. I believe it should be active-then-completed in the code, since they are applied to the vertices in reverse order(?). Anyway, neither sequence worked.

Other relevant bits:

	private void glControl1_MouseDown(object sender, MouseEventArgs e)
        {
            MouseDownPos = MousePosToSphere(e.X, e.Y); // project the mouse position to a sphere and store it
        }

        private void glControl1_MouseMove(object sender, MouseEventArgs e)
        {

            if (e.Button == MouseButtons.Left) // click-and-drag
            {
                 
                double r = glControl1.Height / 2;
                Vector3 MousePos = MousePosToSphere(e.X, e.Y);

                // Find the vector between the two points...
                Vector3 vec = new Vector3(MousePos - MouseDownPos);

                // Calculate straight-line distance (a) between two points...
                double a = Math.Sqrt(Math.Pow(vec.X, 2) + Math.Pow(vec.Y, 2) + Math.Pow(vec.Z, 2)); // trig

                if (a > 0)
                {

                    // Bisect the angle to form two right angle triangles and calculate theta/2...
                    // we know opp and hyp
                    float theta = (float)(2 * Math.Asin(0.5 * a / r)); // sin theta = opp / hyp

                    // Find the normal to the vector between the two points (which is the rotation axis)
                    // This can be calculated from the cross product of two vectors from the centre of the sphere
                    // (At the moment we have got the mouse coordinates (from/to) stored.  We treat these as vectors here)
                    Vector3 normal = Vector3.Cross(MouseDownPos, MousePos);
                    normal = Vector3.Normalize(normal); // not sure whether this is necessary

                    lblVector.Text = Math.Round(normal.X, 2) + ", " +
                        Math.Round(normal.Y, 2) + ", " +
                        Math.Round(normal.Z, 2) + "   " +
                        Math.Round(theta * 180 / Math.PI,0);

                    // Build a matrix...
                    rtn_active = Matrix4.CreateFromAxisAngle(normal, theta);

                    glControl1.Invalidate(); // trigger redraw

                }

            }
	}

	private Vector3 MousePosToSphere(double MouseX, double MouseY)
        {

            // Find the smallest side of the window (width or height)...
            double min = (glControl1.Width < glControl1.Height) ? glControl1.Width : glControl1.Height;

            double r = min / 2; // sphere radius = half the narrowest side of the window

            // Convert pointer coordinates so they are relative to window centre...
            double x = MouseX - (glControl1.Width / 2); // relative to window centre
            double y = (glControl1.Height - MouseY) - (glControl1.Height / 2); // invert to be positive upwards, relative to window centre
            //double y = MouseY - (glControl1.Height / 2); // invert to be positive upwards, relative to window centre

            //lblMouseX.Text = "Mouse wrt screen centre x:" + x;
            //lblMouseY.Text = "y:" + y;

            // Constrain mouse position to a circle (and therefore to surface of sphere)...
            double dist = Math.Sqrt(Math.Pow(x, 2) + Math.Pow(y, 2)); // distance from centre to mouse position
            if (dist > r)
            {
                double prop = r / dist; // fraction (i.e. 0.9 if 10% reduction required)
                x *= prop;
                y *= prop;
            }

            double z = Math.Sqrt(Math.Pow(r, 2) - Math.Pow(x, 2) - Math.Pow(y, 2));

            return new Vector3((float)x, (float)y, (float)z); // (pixels)
        }

The matrices are all Matrix4. Vectors and other triplets are Vector3.

There’s a good example of an implementation of Arcball in opengl here: Arcball
I customized it for my own project: XFLR5 download | SourceForge.net
(Had to remove the http keyword, because otherwise this post is rejected )

Hope this helps

Thanks for the reply. Yes, I am aware of the rainwarrior page. In fact that was the source I initially referred to when first trying to implement this.

My method is slightly different in that I am trying to alter the viewer’s perspective rather than spinning an object. Therefore I am not using the gluUnproject approach - although of course the principle is similar. When it comes to applying the matrices, what I am doing is I think consistent with the rainwarrior description

Once you have your rotation matrix, however, you can multiply it by whatever matrix you were using when the starting vector was gathered, and pass the result to glMultMatrix

Rather than starting from square 1 with another method, I am hoping to pin down what is wrong with mine. It very nearly works, but there must be something I am missing somewhere.

I will have a look at your project ; see if that sheds any light…

It took me some time to get it right too. I had to do it in debug mode and output results at different steps.
First you should make double sure that the conversion from screen to sphere coordinates is correct. I did this by clicking on the cardinal points North Pole, South Pole, equator, of the sphere. Check that it returns 3d points such as (0, r, 0), (0,-r,0), etc.
After that I tried some simple 90° rotations by moving the mouse from North to equator, and compared the matrix data to the expected values. The matrices are of the type


   cos(a)     -sin(a)    0
   sin(a)      cos(a)    0
    0            0       1

so it’s easy to compare.

Once you know for sure that the conversion from mouse movement to rotation matrix is correct, it’s new_matrix = last_matrix * current_matrix.
At this stage again I tried to combine two simple rotations, sent the matrix to the debug output, and checked that it gave the intended result.

Here’s my code, if it’s of any help


/****************************************************************************

	ArcBall Class
	Copyright (C)  Bradley Smith, March 24, 2006
	Hideously modified in 2008 by Andre Deperrois for miserable selfish purposes

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

*****************************************************************************/


#include "ArcBall.h"
#include <QtOpenGL>
#include "math.h"


ArcBall::ArcBall(void)
{
	angle = 0.0;
	Quat.a  = 0.0;

	Quat.qx = Quat.qy = Quat.qz = 0.0;

	ax = ay = az = 0.0;

	ab_quat[0]	= -0.65987748f;
	ab_quat[1]	=  0.38526487f;
	ab_quat[2]	= -0.64508355f;
	ab_quat[3]	=  0.0f;
	ab_quat[4]	= -0.75137258f;
	ab_quat[5]	= -0.33720365f;
	ab_quat[6]	=  0.56721509f;
	ab_quat[7]	=  0.0f;
	ab_quat[8]	=  0.000f;
	ab_quat[9]	=  0.85899049f;
	ab_quat[10]	=  0.51199043f;
	ab_quat[11]	=  0.0f;
	ab_quat[12]	=  0.0f;
	ab_quat[13]	=  0.0f;
	ab_quat[14]	=  0.0f;
	ab_quat[15]	=  1.0f;

	memcpy(ab_last, ab_quat, 16*sizeof(float));
	memcpy(ab_next, ab_quat, 16*sizeof(float));

	// the distance from the origin to the eye
	ab_zoom  = 1.0;
	ab_zoom2 = 1.0;
	// the radius of the arcball
	ab_sphere  = 3.0;
	ab_sphere2 = 9.0;

	ab_start.set(0.0,0.0,1.0);
	ab_curr.set(0.0,0.0,1.0);
	ab_eye.set(0.0,0.0,1.0);
	ab_eyedir.set(0.0,0.0,1.0);
	ab_up.set(0.0,1.0,0.0);
	ab_out.set(1.0,0.0,0.0);


	sc.set(0.0,0.0,1.0);
	ec.set(0.0,0.0,1.0);

	memset(ab_crosspoint, 0, 16*sizeof(float));
}


/** update current arcball rotation*/
void ArcBall::move(double ax, double ay)
{
	sphereCoords(ax,ay, ab_curr);
	if(ab_curr == ab_start)
	{
		// avoid potential rare divide by tiny
		quatCopy(ab_quat, ab_last);
		return;
	}

	// use a dot product to get the angle between them
	// use a cross product to get the vector to rotate around

	cosa   = ab_start.dot(ab_curr);
	sina2  = sqrt((1.0 - cosa)*0.5);
	cosa2  = sqrt((1.0 + cosa)*0.5);
	angle = acos(cosa2)*180.0/PI;

	p = (ab_start*ab_curr);
	p.normalize();
	p *=sina2;
	Quat.Set(cosa2, p.x, p.y, p.z);

	quatToMatrix(ab_next, Quat);

	// update the rotation matrix
	quatNext(ab_quat, ab_last, ab_next);
}



/** reset the rotation matrix*/
void ArcBall::quatIdentity(float* q)
{
	q[0] =1; q[1] =0; q[2] =0; q[3] =0;
	q[4] =0; q[5] =1; q[6] =0; q[7] =0;
	q[8] =0; q[9] =0; q[10]=1; q[11]=0;
	q[12]=0; q[13]=0; q[14]=0; q[15]=1;
}


/** copy a rotation matrix*/
void ArcBall::quatCopy(float* dst, float* src)
{
	dst[0]=src[0]; dst[1]=src[1]; dst[2] =src[2];
	dst[4]=src[4]; dst[5]=src[5]; dst[6] =src[6];
	dst[8]=src[8]; dst[9]=src[9]; dst[10]=src[10];
}


/** convert the quaternion into a rotation matrix*/
void ArcBall::quatToMatrix(float* q, Quaternion Qt)
{
	x2 = Qt.qx*Qt.qx;
	y2 = Qt.qy*Qt.qy;
	z2 = Qt.qz*Qt.qz;
	xy = Qt.qx*Qt.qy;
	xz = Qt.qx*Qt.qz;
	yz = Qt.qy*Qt.qz;
	wx = Qt.a*Qt.qx;
	wy = Qt.a*Qt.qy;
	wz = Qt.a*Qt.qz;

	q[0] = (float)(1 - 2*y2 - 2*z2);
	q[1] = (float)(2*xy + 2*wz);
	q[2] = (float)(2*xz - 2*wy);

	q[4] = (float)(2*xy - 2*wz);
	q[5] = (float)(1 - 2*x2 - 2*z2);
	q[6] = (float)(2*yz + 2*wx);

	q[8] = (float)(2*xz + 2*wy);
	q[9] = (float)(2*yz - 2*wx);
	q[10]= (float)(1 - 2*x2 - 2*y2);
}

/** multiply two rotation matrices*/
void ArcBall::quatNext(float* dest, float* left, float* right)
{
	dest[0] = left[0]*right[0] + left[1]*right[4] + left[2] *right[8];
	dest[1] = left[0]*right[1] + left[1]*right[5] + left[2] *right[9];
	dest[2] = left[0]*right[2] + left[1]*right[6] + left[2] *right[10];
	dest[4] = left[4]*right[0] + left[5]*right[4] + left[6] *right[8];
	dest[5] = left[4]*right[1] + left[5]*right[5] + left[6] *right[9];
	dest[6] = left[4]*right[2] + left[5]*right[6] + left[6] *right[10];
	dest[8] = left[8]*right[0] + left[9]*right[4] + left[10]*right[8];
	dest[9] = left[8]*right[1] + left[9]*right[5] + left[10]*right[9];
	dest[10]= left[8]*right[2] + left[9]*right[6] + left[10]*right[10];
}


/** reset the arcball*/
void ArcBall::reset()
{
	quatIdentity(ab_quat);
	quatIdentity(ab_last);
}


/** affect the arcball's orientation on openGL*/
void ArcBall::rotate()
{
	glMultMatrixf(ab_quat);
}


void ArcBall::rotateCrossPoint()
{
	aa.set(1.0, 0.0, 0.0);

	cosa   = aa.dot(ab_curr);
	sina2  = sqrt((1.0 - cosa)*0.5);
	cosa2  = sqrt((1.0 + cosa)*0.5);
	angle = 2.0*acos(cosa2)*180.0/PI;

	p = aa * ab_curr;
	p.normalize();
}


void ArcBall::setQuat(Quaternion Qt)
{
	if(qAbs(Qt.a)<=1.0) angle = 2.0*acos(Qt.a) *  180.0/PI;
	Quat.a  = Qt.a;

	Quat.qx = Qt.qx;
	Quat.qy = Qt.qy;
	Quat.qz = Qt.qz;

	quatToMatrix(ab_quat, Quat);
}


void ArcBall::setQuat(double r, double qx, double qy, double qz)
{
	if(qAbs(r)<=1.0) angle = 2.0*acos(r) *  180.0/PI;
	Quat.a  = r;

	Quat.qx = qx;
	Quat.qy = qy;
	Quat.qz = qz;

	quatToMatrix(ab_quat, Quat);
}


void ArcBall::setZoom(double radius, CVector eye, CVector up)
{
	ab_eye     = eye; // store eye vector
	ab_zoom2   = ab_eye.dot(ab_eye);
	ab_zoom    = sqrt(ab_zoom2); // store eye distance
	ab_sphere  = radius;         // sphere radius
	ab_sphere2 = ab_sphere * ab_sphere;
	ab_eyedir  = ab_eye * (1.0 / ab_zoom); // distance to eye

	if(ab_sphere <= 0.0) // trackball mode
	{
		ab_up = up;
		ab_out = (ab_eyedir * ab_up);
	}
}


/** begin arcball rotation*/
void ArcBall::start(double ax, double ay)
{
	// saves a copy of the current rotation for comparison
	quatCopy(ab_last,ab_quat);
	sphereCoords(ax, ay, ab_start);
	ab_curr = ab_start;
}




void ArcBall::sphereCoords(double const &ax, double const &ay, CVector &V)
{
	// find the intersection with the sphere

	if(ab_sphere2>ax*ax+ay*ay) V.set(ax,ay,sqrt(ab_sphere2-ax*ax-ay*ay));
	else                       V.set(ax,ay,0.0);
//	else return EdgeCoords(ax, ay);

	V.normalize();
}


Many thanks. I will do some further investigation as you advise.