I've been trying for the past few days to make a working implementation of a virtual trackball for the user interface for a 3D graphing-like program. But I'm having trouble.
Looking at the numbers and many tests the problems seems to be the actual concatenation of my quaternions but I don't know or think so. I've never worked with quaternions or virtual trackballs before, this is all new to me. I'm using the Quaternion
class supplied by JOGL. I tried making my own and it worked (or at least as far a I know) but it was a complete mess so I just went with JOGL's.
When I do not concatenate the quaternions the slight rotations I see seem to be what I want, but of course It's hard when it's only moving a little bit in any direction. This code is based off of the Trackball Tutorial on the OpenGL wiki.
When I use the Quaternion
class's mult (Quaternion q)
method the graph hardly moves (even less than not trying to concatenate the quaternions).
When I tried Quaternionclass's
add (Quaternion q)` method for the fun of it I get something that at the very least rotates the graph but not in any coherent way. It spazzes out and rotates randomly as I move the mouse. Occasionally I'll get quaternions entirely filled with NaN.
In my code I will not show either of these, I'm lost with what to do with my quaternions. I know I want to multiply them because as far as I'm aware that's how they are concatenated. But like I said I've had no success, I'm assuming the screw up is somewhere else in my code.
Anyway, my setup has a Trackball
class with a public Point3f projectMouse (int x, int y)
method and a public void rotateFor (Point3f p1, Point3f p2)
, Where Point3f
is a class I made. Another class called Camera
has a public void transform (GLAutoDrawable g)
method which will call OpenGL methods to rotate based on the trackball's quaternion.
Here's the code:
public Point3f projectMouse (int x, int y)
{
int off = Screen.WIDTH / 2; // Half the width of the GLCanvas
x = x - objx_ - off; // obj being the 2D center of the graph
y = off - objy_ - y;
float t = Util.sq(x) + Util.sq(y); // Util is a class I made with
float rsq = Util.sq(off); // simple some math stuff
// off is also the radius of the sphere
float z;
if (t >= rsq)
z = (rsq / 2.0F) / Util.sqrt(t);
else
z = Util.sqrt(rsq - t);
Point3f result = new Point3f (x, y, z);
return result;
}
Here's the rotation method:
public void rotateFor (Point3f p1, Point3f p2)
{
// Vector3f is a class I made, I already know it works
// all methods in Vector3f modify the object's numbers
// and return the new modify instance of itself
Vector3f v1 = new Vector3f(p1.x, p1.y, p1.z).normalize();
Vector3f v2 = new Vector3f(p2.x, p2.y, p2.z).normalize();
Vector3f n = v1.copy().cross(v2);
float theta = (float) Math.acos(v1.dot(v2));
float real = (float) Math.cos(theta / 2.0F);
n.multiply((float) Math.sin(theta / 2.0F));
Quaternion q = new Quaternion(real, n.x, n.y, n.z);
rotation = q; // A member that can be accessed by a getter
// Do magic on the quaternion
}
EDIT:
I'm getting closer, I found out a few simple mistakes.
1: The JOGL implementation treats W as the real number, not X, I was using X for real
2: I was not starting with the quaternion 1 + 0i + 0j + 0k
3: I was not converting the quaternion into an axis/angle for opengl
4: I was not converting the angle into degrees for opengl
Also as Markus pointed out I was not normalizing the normal, when I did I couldn't see much change, thought it's hard to tell, he's right though.
The problem now is when I do the whole thing the graph shakes with a fierceness like you would never believe. It (kinda) moves in the direction you want it to, but the seizures are too fierce to make anything out of it.
Here's my new code with a few name changes:
public void rotate (Vector3f v1, Vector3f v2)
{
Vector3f v1p = v1.copy().normalize();
Vector3f v2p = v2.copy().normalize();
Vector3f n = v1p.copy().cross(v2p);
if (n.length() == 0) return; // Sometimes v1p equals v2p
float w = (float) Math.acos(v1p.dot(v2p));
n.normalize().multiply((float) Math.sin(w / 2.0F));
w = (float) Math.cos(w / 2.0F);
Quaternion q = new Quaternion(n.x, n.y, n.z, w);
q.mult(rot);
rot_ = q;
}
Here's the OpenGL code:
Vector3f p1 = tb_.project(x1, y1); // projectMouse [changed name]
Vector3f p2 = tb_.project(x2, y2);
tb_.rotate (p1, p2);
float[] q = tb_.getRotation().toAxis(); // Converts to angle/axis
gl.glRotatef((float)Math.toDegrees(q[0]), q[1], q[2], q[3]);
The reason for the name changes is because I deleted everything in the Trackball
class and started over. Probably not the greatest idea, but oh well.
EDIT2:
I can say with pretty good certainty that there is nothing wrong with projecting onto the sphere.
I can also say that as far as the whole thing goes it seems to be the VECTOR that is the problem. The angle looks just fine, but the vector seems to jump around.
EDIT3:
The problem is the multiplication of the two quaternions, I can confirm that everything else works as expected. Something goes whacky with the axis during multiplication!
I did it!
Thanks to this C++ implementation I was able to develop a working trackball/arcball interface. My goodness me, I'm still not certain what the problem was, but I rewrote everything and even wrote my own Quaternions
class and suddenly the whole thing works. I also made a Vectors
class for vectors. I had a Vector3f
class before but the Quaternions
and Vectors
classes are full of static methods and take in arrays. To make it easy to do vector computations on quaternions and vice versa. I will link the code for those two classes below, but only the Trackball
class will be show here.
I made those two classes pretty quickly this morning so if there are any mathematical errors, well, uh, oops. I only used what I needed to use and made sure they were correct. These classes are below:
Quaternions: http://pastebin.com/raxS4Ma9
Vectors: http://pastebin.com/fU3PKZB9
Here is my Trackball
class:
public class Trackball
{
private static final float RADIUS_ = Screen.DFLT_WIDTH / 2.0F;
private static final int REFRESH_ = 50;
private static final float SQRT2_ = (float) Math.sqrt(2);
private static final float SQRT2_INVERSE_ = 1.0F / SQRT2_;
private int count_;
private int objx_, objy_;
private float[] v1_, v2_;
private float[] rot_;
public Trackball ()
{
v1_ = new float[4];
v2_ = new float[4];
rot_ = new float[] {0, 0, 0, 1};
}
public void click (int x, int y)
{
v1_ = project(x, y);
}
public void drag (int x, int y)
{
v2_ = project(x, y);
if (Arrays.equals(v1_, v2_)) return;
float[] n = Vectors.cross(v2_, v1_, null);
float[] o = Vectors.sub(v1_, v2_, null);
float dt = Vectors.len(o) / (2.0F * RADIUS_);
dt = dt > 1.0F ? 1.0F : dt < -1.0F ? -1.0F : dt;
float a = 2.0F * (float) Math.asin(dt);
Vectors.norm_r(n);
Vectors.mul_r(n, (float) Math.sin(a / 2.0F));
if (count_++ == REFRESH_) { count_ = 0; Quaternions.norm_r(rot_); }
float[] q = Arrays.copyOf(n, 4);
q[3] = (float) Math.cos(a / 2.0F);
rot_ = Quaternions.mul(q, rot_, rot_);
}
public float[] getAxis ()
{
return Quaternions.axis(rot_, null);
}
public float[] project (float x, float y)
{
x = RADIUS_ - objx_ - x;
y = y - objy_ - RADIUS_;
float[] v = new float[] {x, y, 0, 0};
float len = Vectors.len(v);
float tr = RADIUS_ * SQRT2_INVERSE_;
if (len < tr)
v[2] = (float) Math.sqrt(RADIUS_ * RADIUS_ - len * len);
else
v[2] = tr * tr / len;
return v;
}
}
You can see there's a lot of similarities from the C++ example. Also I'd like to note there is no method for setting the objx_
and objy_
values yet. Those are for setting the center of the graph which can be moved around. Just saying, so you don't scratch your head about those fields.