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;
}
}

Thursday, July 12, 2007

WPF Text Woes

WPF text rendering currently has a complete show-stopper of a problem. I'm not talking about the general "blurriness" of text. That's just anti-aliasing, and while I personally prefer small text to be aliased, I can live with it.

The problem that is driving me mad is the behavior of text when it scrolls. After scrolling stops, sections of the text will slowly adjust focus over a period of a second or two. At first, I thought the issue was just eyestrain on my part, but it really is doing it. It is very uncomfortable to look at, and in my opinion makes WPF useless in text-heavy applications until Microsoft finds a way to fix it.

Other people have noticed the issue as well. This post sheds some light on the issue, and implies that it was a conscious design decision.

There has to be a better way...