Monday, July 16, 2007

A TV3D ArcBall implementation

A common need in 3D applications is the ability to rotate an object with the mouse. One of the most natural ways to implement this is to imagine that there is a sphere centered on the object, and that you are grabbing a point on the sphere and rotating it. The canonical example of this "ArcBall" technique is the implementation by Ken Shoemake in Graphics Gems IV, but many other versions are floating around the Net. I needed a C# implementation for TV3D, so I rolled my own. It is most closely based on a WPF implementation by Daniel Lehenbauer (check the link for a nice overview of the theory behind the implementation).

My version is a pretty direct translation, with one nice improvement. Instead of doing a delta each mouse move from the previous mouse movement, I always do a delta from the beginning of the mouse drag. I found that without this modification, floating point rounding error caused obvious visual problems. Always computing a delta from the start of the mouse drag ensures complete consistency during the drag.

The ArcBall class follows, but first here is an example of how to use it. At some point during your application initialization, create an instance of the ArcBall class like this:

ArcBall myArcBall = new ArcBall(windowWidth, windowHeight, mathLibrary);


where 'windowWidth' and 'windowHeight' are the width and height of your display window, and 'mathLibrary' is a instance of the TVMathLibrary class.

Then, in your game loop where you are checking mouse input, do something like the following:

InputEngine.GetAbsMouseState(ref mouseX, ref mouseY, ref mouseB1, ref mouseB2, ref mouseB3);

if (mouseB2)
{
if (mouseDragging)
{
TV_3DQUATERNION result = new TV_3DQUATERNION();

myArcBall.Update(new TV_2DVECTOR((float)mouseX, (float)mouseY), ref result);

myMesh.SetQuaternion(result);
}
else
{
if (selectedObject != null)
{
myArcBall.StartDrag(new TV_2DVECTOR((float)mouseX, (float)mouseY), myMesh.GetQuaternion());

mouseDragging = true;
}
}
}
else
{
mouseDragging = false;
}


This code starts a drag when the mouse button (in this case, mouse button 2) is pressed. It initializes the ArcBall with the initial mouse position and the initial quaternion of the mesh we want to rotate. While the mouse button is down, it updates the ArcBall with the current mouse position and gets the resulting quaternion. This is then applied to the mesh to get the desired rotation.

Here is the code for the ArcBall class:

class ArcBall
{
private TV_2DVECTOR startPoint;
private TV_3DVECTOR startVector = new TV_3DVECTOR(0.0f, 0.0f, 1.0f);
private TV_3DQUATERNION startRotation;
private float width, height;
private TVMathLibrary mathLib;

/// <summary>
/// ArcBall Constructor
/// </summary>
/// <param name="width">The width of your display window</param>
/// <param name="height">The height of your display window</param>
/// <param name="mathLib">An instance of the TV3D math library</param>
public ArcBall(float width, float height, TVMathLibrary mathLib)
{
this.width = width;
this.height = height;

this.mathLib = mathLib;
}

/// <summary>
/// Begin dragging
/// </summary>
/// <param name="startPoint">The X/Y position in your window at the beginning of dragging</param>
/// <param name="rotation"></param>
public void StartDrag(TV_2DVECTOR startPoint, TV_3DQUATERNION rotation)
{
this.startPoint = startPoint;
this.startVector = MapToSphere(startPoint);
this.startRotation = rotation;
}

/// <summary>
/// Get an updated rotation based on the current mouse position
/// </summary>
/// <param name="currentPoint">The curren X/Y position of the mouse</param>
/// <param name="result">The resulting quaternion to use to rotate your object</param>
public void Update(TV_2DVECTOR currentPoint, ref TV_3DQUATERNION result)
{
TV_3DVECTOR currentVector = MapToSphere(currentPoint);

TV_3DVECTOR axis = mathLib.VCrossProduct(startVector, currentVector);

float angle = mathLib.VDotProduct(startVector, currentVector);

TV_3DQUATERNION delta = new TV_3DQUATERNION(axis.x, axis.y, axis.z, -angle);

mathLib.TVQuaternionMultiply(ref result, startRotation, delta);
}

/// <summary>
/// Map a point in window space to our arc ball sphere
/// </summary>
/// <param name="point">The X/Y position to map</param>
/// <returns>The 3D position on the sphere</returns>
private TV_3DVECTOR MapToSphere(TV_2DVECTOR point)
{
float x = point.x / (width / 2.0f);
float y = point.y / (height / 2.0f);

x = x - 1.0f;
y = 1.0f - y;

float z2 = 1.0f - x * x - y * y;
float z = z2 > 0.0f ? (float)Math.Sqrt((double)z2) : 0;

TV_3DVECTOR outVec = new TV_3DVECTOR();

mathLib.TVVec3Normalize(ref outVec, new TV_3DVECTOR(x, y, z));

return outVec;
}
}

1 comment:

  1. You're taking the dot product of the start vector and the current vector, and assigning it directly to angle. Shouldn't angle get the inverse cosine of the dot product instead? Dot product returns the cosine of the angle between vectors, not the angle.

    ReplyDelete