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.