Sign in to follow this  
Subotron

Quaternion based camera shows odd behaviour

Recommended Posts

I'm trying to implement a camera into OpenGL (But since it is basically about quaternion math I posted it here.) using quaternions. I got it to do something, but the behaviour isn't quite right. For the camera I have 3 vectors: position, view and up. Position is actually a point, the camera position. View is the point it is looking at (so it's more like target, where direction = view - position.) and up is the up vector. I use the standard gluLookAt(vector3 position, vector3 view, vector3 up) function to position the camera based on these 3 vectors. It's a first-person camera, so as far as I can tell I should only have to update the view (or target) vector, and perhaps the up vector if I rotate over the z-axis. When I rotate around the y-axis (pointing up: {0.0, 1.0, 0.0}) the camera behaves well. However, when I do a full 360 degree rotation over time at a constant angle per frame, the rotation goes faster at some points than at others. This is odd, since I update it independent of the frame rate, and also the frame rate is quite constant. However, even weirder things happen when I try to rotate over the x or z-axis to produce a full 360 degree rotation. The camera starts out right, but it soon starts to rotate in different directions, which I cannot make any sense of. My sense of math is quite good, but quaternions are a first to me, and I have hardly any understanding of them yet. (complex numbers will be though at school in two weeks, but atm I don't know anything about them except they use i^2 = -1.) As far as I can tell all my OpenGL functions are implemented right, so the fault must be somewhere in the math. Who can spot it for me? Any help is greatly appreciated! The code is in C++, with some functions of the OpenGL API. CQuaternion and CVector3 are classes I wrote.
///////////////////////////////////////////////////////////////////////////////////////////////
// QUATERNION OPERATORS
///////////////////////////////////////////////////////////////////////////////////////////////

CQuaternion CQuaternion::operator/(float factor)
{
	return CQuaternion(x / factor, y / factor, z / factor, w / factor);
}
	
void CQuaternion::operator/=(float factor)
{
	x /= factor;
	y /= factor;
	z /= factor;
	w /= factor;
}

bool CQuaternion::operator==(CQuaternion q)
{
	if (q.x == x && q.y == y && q.z == z && q.w == w) return true;

	return false;
}

bool CQuaternion::operator!=(CQuaternion q)
{
	if (q.x == x && q.y == y && q.z == z && q.w == w) return false;

	return true;
}

///////////////////////////////////////////////////////////////////////////////////////////////
//	MULTIPLIES QUATERNIONS (NON-COMMUTATIVE)
///////////////////////////////////////////////////////////////////////////////////////////////

CQuaternion	mult(CQuaternion q1, CQuaternion q2)
{
	CQuaternion r;

	/*r.x = q1.w * q2.x + q1.x * q2.w + q1.y * q2.z - q1.z * q2.y;
	r.y = q1.w * q2.y - q1.x * q2.z + q1.y * q2.w + q1.z * q2.x;
	r.z = q1.w * q2.z + q1.x * q2.y - q1.y * q2.x + q1.z * q2.w;
	r.w = q1.w * q2.w - q1.x * q2.x - q1.y * q2.y - q1.z * q2.z;*/

	r.x = q1.x * q2.w + q1.w * q2.x + q1.y * q2.z - q1.z * q2.y;
	r.y = q1.y * q2.w + q1.w * q2.y + q1.z * q2.x - q1.x * q2.z;
	r.z = q1.z * q2.w + q1.w * q2.z + q1.x * q2.y - q1.y * q2.x;
	r.w = q1.w * q2.w - q1.x * q2.x - q1.y * q2.y - q1.z * q2.z;

	return r;
}

///////////////////////////////////////////////////////////////////////////////////////////////
// RETURNS THE NORM OF A QUATERNION
///////////////////////////////////////////////////////////////////////////////////////////////

float norm(CQuaternion q)
{
	return sqrtf((q.x * q.x) + (q.y * q.y) + (q.z * q.z) + (q.w * q.w));
}

///////////////////////////////////////////////////////////////////////////////////////////////
// NORMALIZES A QUATERNION
///////////////////////////////////////////////////////////////////////////////////////////////

void normalize(CQuaternion &q)
{
	q /= norm(q);
}

///////////////////////////////////////////////////////////////////////////////////////////////
// COMPUTES THE CONJUGATE OF A QUATERNION
///////////////////////////////////////////////////////////////////////////////////////////////

CQuaternion	conjugate(CQuaternion q)
{
	return CQuaternion(-q.x, -q.y, -q.z, q.w);
}

///////////////////////////////////////////////////////////////////////////////////////////////
// CONVERSION FROM QUATERNION TO VECTOR3
///////////////////////////////////////////////////////////////////////////////////////////////

CVector3 quat_to_vector3(CQuaternion q)
{
	return CVector3(q.x, q.y, q.z);
}

///////////////////////////////////////////////////////////////////////////////////////////////
// CONVERSION FROM VECTOR3 TO QUATERNION
///////////////////////////////////////////////////////////////////////////////////////////////

CQuaternion vector3_to_quat(CVector3 v)
{
	return CQuaternion(v.x, v.y, v.z, 0.0f);
}




///////////////////////////////////////////////////////////////////////////////////////////////
// ROTATES THE CAMERA AROUND THE PROVIDED AXIS
///////////////////////////////////////////////////////////////////////////////////////////////

void CCamera::rotate(float angle, float x, float y, float z)
{
	CQuaternion rotate, view;

	rotate.x	= x * sin(angle / 2.0f);
	rotate.y	= y * sin(angle / 2.0f);
	rotate.z	= z * sin(angle / 2.0f);
	rotate.w	= cos(angle / 2.0f);

	// this->view is the camera's view vector, as mentioned in the problem description
	view		= vector3_to_quat(this->view);
	view		= mult(mult(rotate, view), conjugate(rotate));
	this->view	= quat_to_vector3(view);
}




If a look at the behaviour is needed I can make an exe showing it. Just ask me for it :) [Edited by - Subotron on October 8, 2006 3:38:33 PM]

Share this post


Link to post
Share on other sites
I made one alteration:

this->view is no longer the view vector as described, it is now more like a 'direction', this->view = target - position. This works better, since movement around the y-axis works. However, when I move around the x-axis, I still seem to suffer from gimbal lock, although I read quaternions should solve this (actually the most important reason I use them).

Edit: fixed z-rotation. Figured my position didn't allow for z-rotation, and I was right...

Also, I'm not sure if it is actually a 'gimbal lock' that's occuring. When I now try to rotate around the x-axis, only a half rotation is applied, and when the camera's view is either totally up or down, the rotation switches direction (so it is 'trapped' in a half circle, the motion of an old clock)

[Edited by - Subotron on October 8, 2006 12:40:20 PM]

Share this post


Link to post
Share on other sites
My favorite subject - the dreaded 'quaternion-based camera' :-)

What kind of camera are you trying to implement exactly? Do you want a standard FPS or spectator camera as found in Quake or Unreal? Or something else?

I think I already know the answer, but I'll wait and see, just to be sure. Also, if your code is based on a tutorial, could you post a link to it, and/or post the entirety of your camera code?

Share this post


Link to post
Share on other sites
The code is based on these articles:

http://www.gamedev.net/reference/articles/download/DA-Quaternion.pdf
http://www.gamedev.net/reference/articles/article1997.asp

Actually, I don't really want to create FPS camera, but I thought it was a good start since I already have a working (except for gimbal lock) one using euler methods. 3rd person (with SLERP) is my goal, however I think the class should support both. FPS in the sense of Quake.

The full camera class code: (doesn't feature any movement yet since I want the rotations to work first)


class CCamera
{
private:

protected:

CVector3 pos;
CVector3 view;
CVector3 up;

public:

CCamera() { }
~CCamera() { }

void set (float position_x, float position_y, float position_z,
float view_x, float view_y, float view_z,
float up_x, float up_y, float up_z);

void look ();

void rotate (float angle, float x, float y, float z);

CVector3& get_position ();
CVector3& get_view ();
CVector3& get_up_vector ();
CVector3 get_direction ();
};








///////////////////////////////////////////////////////////////////////////////////////////////
// SETS THE CAMERA AT A GIVEN POSITION, VIEW AND UP VECTOR
///////////////////////////////////////////////////////////////////////////////////////////////

void CCamera::set(float position_x, float position_y, float position_z,
float view_x, float view_y, float view_z,
float up_x, float up_y, float up_z)
{
pos = CVector3(position_x, position_y, position_z);
view = CVector3(view_x, view_y, view_z);
up = CVector3(up_x, up_y, up_z);
}

///////////////////////////////////////////////////////////////////////////////////////////////
// PLACES THE CAMERA IN THE SCENE
///////////////////////////////////////////////////////////////////////////////////////////////

void CCamera::look()
{
gluLookAt(pos.x, pos.y, pos.z,
view.x, view.y, view.z,
up.x, up.y, up.z);
}

///////////////////////////////////////////////////////////////////////////////////////////////
// ROTATES THE CAMERA AROUND THE PROVIDED AXIS
///////////////////////////////////////////////////////////////////////////////////////////////

void CCamera::rotate(float angle, float x, float y, float z)
{
CQuaternion rotate, view;

rotate.x = x * sin(angle / 2.0f);
rotate.y = y * sin(angle / 2.0f);
rotate.z = z * sin(angle / 2.0f);
rotate.w = cos(angle / 2.0f);

view = vector3_to_quat(get_direction());
view = mult(mult(rotate, view), conjugate(rotate));
this->view = quat_to_vector3(view) + pos;
}

///////////////////////////////////////////////////////////////////////////////////////////////
// RETURNS THE CAMERA'S POSITION
///////////////////////////////////////////////////////////////////////////////////////////////

CVector3& CCamera::get_position()
{
return pos;
}

///////////////////////////////////////////////////////////////////////////////////////////////
// RETURNS THE CAMERA'S VIEWPOINT
///////////////////////////////////////////////////////////////////////////////////////////////

CVector3& CCamera::get_view()
{
return view;
}

///////////////////////////////////////////////////////////////////////////////////////////////
// RETURNS THE CAMERA'S UP VECTOR
///////////////////////////////////////////////////////////////////////////////////////////////

CVector3& CCamera::get_up_vector()
{
return up;
}

///////////////////////////////////////////////////////////////////////////////////////////////
// GIVES THE DIRECTION THE CAMERA IS FACING
///////////////////////////////////////////////////////////////////////////////////////////////

CVector3 CCamera::get_direction()
{
return view - pos;
}







Thanks a lot :)

Share this post


Link to post
Share on other sites
The requirements for each of the camera types you mention are somewhat different. Here are a few things to consider:

1. FPS/FPS-spectator

The aforementioned tutorial notwithstanding, quaternions are in general entirely unnecessary for this type of camera, and in fact just get in the way. Gimbal lock is not a problem in this context, and in any case whether or not you use quaternions has absolutely nothing to do with whether you encounter gimbal lock.

For this type of camera, Euler angles are actually a perfectly reasonable solution. Generally roll is ignored with this type of camera, so you are left with two angles, yaw and pitch, which you can update incrementally and then use to construct a matrix to submit to OpenGL.

2. 'Free' 6DOF

This type of camera can be implemented equally effectively using vectors, matrices, or quaternions. The only reason I can think of at the moment to prefer the latter is if you're going to be doing a lot of interpolation.

3. 3rd-person

I haven't had occasion to write a 'serious' third-person camera (i.e. with damping and collision avoidance), so I won't comment on this except to say that quaternions might be useful here for interpolation. (It seems though that such a camera could be implemented purely in terms of azimuth and elevation, in which case quaternions would probably be overkill.)

Combining all these camera types into one class might be kind of tricky design-wise. If I were you I would implement each camera type individually using whatever methods are most appropriate to that type, and then think about what aspects could be factored out (and perhaps placed in a base class).

Share this post


Link to post
Share on other sites
jyk, you catch my drift. Those are the types of camera I want to be able to use. The reason why I want more, is because I'm designing the class from an engine perspective. The 3 don't necessarily have to work next to eachother, but it should be possible to have 2 camera's, one FP and one 3rd person, so you can switch ingame. I would like to make one class allowing both, but I'm just happy if I can fix this bug, I will delve into the rest later (already have done some designing).

Ok, this is the first time I hear quaternions don't actually do much for gimbal lock. I knew it was possible to still create one, but I thought this would be better. (think again) Still, I'd like to use them, both because I want to learn to use them (I have reason to believe I will be doing something with inverse kinematics later this year for school, and I am told quaternions might come in handy there) and because of the interpolation advantages.

So well, I guess it's confirmed the problem is actually a gimbal lock? I really want to get that out, because even though Quake-style FPS doesn't suffer from it, I really don't want a bug stopping me from having full camera freedom...

Could you please point out the problem to me, since I am really out of ideas :(

Share this post


Link to post
Share on other sites
I'm not sure if I can easily identify the problem without digging through the code carefully (for one thing, I see the rotate() function, but I don't see where it's actually being used - you might post that as well).

I will say that whenever you perform incremental rotations as you're doing here (which isn't really what you should be doing for this sort of camera, but anyway...), you will need to compensate for drift occasionally. In this case, that means normalizing the direction vector after you rotate it.

Another thing that might help is to clean up the naming convention, which is currently somewhat misleading. The point at which you're looking is the target; 'view' is a fairly confusing name for it, and IMO should be reserved for the forward view vector.

Also, there's no reason to track the 'view' in terms of the target; it just leads to unnecessary conversions back and forth between the two. Just store the forward vector for the camera and perform the operations on it directly.

One last thing. IMO a camera should be thought of just like any other object; the only real functional difference is that with a camera you generally invert the matrix before submitting it to your API of choice (this is what gluLookAt() does, among other things).

A first-person camera (whether FPS or 6DOF) is really then just an entity that attaches itself to the transform of an in-game object, be it a spaceship, airplane, or a humanoid character. In some cases the camera may need to be shifted or re-oriented somewhat in relation to the model, but the concept is the same.

A third-person camera can be thought of as an object that is 'tethered' to another, and follows its target while obeying certain contraints. Anyway, the point here is not to think of a camera as something 'special' which requires special considerations, since really the math under discussion here can be applied to any in-game object.

So anyway, I'm not exactly sure where the problem in your code is, but I have to admit I think the implementation is conceptually poor for a camera of this type; I understand you want to use quaternions, but you're not doing yourself any favors by using them where they're not an appropriate solution (IMO). I don't know what kind of time contraints you're up against, but if you have the time I would scratch that implementation and approach it differently. I can provide links to some references that might be useful if it would help.

(How did this post get so long!?)

Share this post


Link to post
Share on other sites
hehe thanks for the long post :)

First of all, the rotate() function is called each frame:

void apply_physics(float elapsed_time)
{
camera.rotate(elapsed_time, 1.0f, 0.0f, 0.0f);
}

I use the view and direction vector this way because gluLookAt uses (position, view, up), so I kind of stuck to that. It works in my head, but I can see it is confusing to others. I am planning on rewriting the gluLookAt though, in such a way that I don't need to convert anymore. I put this conversion in, because the 'view' (you call it target) is absolute, not relative to the position. Again, this will be changed later. But for now I have to substract (and later add) the position to it, else only rubbish happens...

Quote:

I will say that whenever you perform incremental rotations as you're doing here (which isn't really what you should be doing for this sort of camera, but anyway...)


I'm not sure what you mean by this. What alternative is there? And what exactly do you mean with incremental rotations? I really want to understand all this well, and judging by all your posts on the subject (in other topics) you have a good understanding. I actually don't even know the difference between what I'm doing and axis-angle, since it seems to me the x, y, z (roll pitch jaw I guess) ARE an axis, and the other parameter is an angle...

And yeah well, I have the time to rewrite my class, I'm actually in the process of rewriting the complete engine (not that it's that big at the moment) and I decided I would fix the camera, and quaternions seemed like a good idea, especially because I thought they would fix my gimbal problems, but also because of the interpolation advantages. But you're saying I shouldn't use them? Then when would be a good time to use them? :)

Please understand, I always thought I understood how the camera worked, but at this point I feel like I'm totally in the dark again...

Share this post


Link to post
Share on other sites
This is kind of a big topic, but I'll try and chip away at it a bit and see if I can help clear some things up.

First of all, I'm wondering whether your main interest is to learn the underlying concepts, or to create a working game or game engine? If the former, then you're on the right track; if the latter, I could recommend some third-party libraries and code that would take care of a lot of this work for you.

Meahwhile, back to the subject at hand. Let's start with the use of gluLookAt(). This function does two things:

1. Creates a transform matrix given a position, a target, and a reference up vector

2. Inverts this matrix so that it can be used as a view matrix

Strictly speaking, this function is intended to be used when you have this particular information (position, target, and up) available, and nothing more. For example, in a simple demo scene you might use it to position the camera at a particular location and orient it so that it's looking at the object you wish to view.

Now, people often use this function in other contexts because it's convenient, in that it hides the details of matrix construction and inversion from you. However (and this is IMO) if you want to understand what's going on it's better not to use this function unless 'look at' functionality is really what you want. In your case it's not.

So what are the alternatives? It depends on the type of camera, but OpenGL provides glLoadMatrix*(), glMultMatrix*(), and glRotate*(), any and all of which can be used (depending on the circumstances) to set or modify the OpenGL modelview matrix. gluLookAt() is by no means the only option.

I'll try to write some more later in another post, but post back if you have any specific questions about the above.

Share this post


Link to post
Share on other sites
Ok, thanks a lot. I understand what you mean why gluLookAt isn't the way to go. I learned quite some vector and matrix maths at school, so I should be able to work with them directly. However, from what I've always heard, gluLookAt has no gimbal lock problems (just like quaternions, both dreams shattered), but glRotate actually forces you to do the rotations one by one, therefore you cannot avoid the gimbal lock problem there.

However, I'd really PREFER actually to change to doing stuff myself, since now I'm totally in the dark, and at the time that I only used glTranslate and glRotate I was fine :)

Share this post


Link to post
Share on other sites
Next I'll explain what I mean by 'incremental' vs. 'from scratch' rotations.

A 'from scratch' rotation (at least the way I'm using the term) always starts with identity, and then applies a series of rotations to yield the final rotation. Typically the rotations are Euler angles, a sequence of rotations about specific axes.

OpenGL does not have a function for this, although many math APIs (such as DirectX) do. In OpenGL you can easily accomplish the same thing using glLoadIdentity() followed by a sequence of glRotate*() calls.

This type of Euler-angle rotation has the advantage that it's intuitive (it's easy to think of a series of rotations about specific axes), but has many drawbacks as well, such as poor interpolation and the aforementioned gimbal lock.

As for gimbal lock, it's something of a misunderstood scapegoat. Quite often, any problem encountered when working with rotations is attributed to gimbal lock, but quite often the actual phenomenon is misunderstood. In some contexts it really is a problem; in others it's more or less benign.

IMO, from-scratch Euler-angle rotations are perfectly suitable for any motion that can be characterized, more or less, in terms of spherical angles or azimuth and elevation. One reason for this is that roll is typically ignored in this context, and therefore gimbal lock is not an issue.

Types of motion that fall into this category include a typical FPS spectator cam or character, or a gun turret. For various reasons, I recommend using from-scratch Euler-angle rotations for these types of objects (this includes your FPS cam).

With incremental rotation, we do not re-build the orientation from scratch each update. Instead, we set it to some initial value on startup, and then make incremental adjustments to it from there on. IMO, this is not suitable for FPS motion, since in this context there's no reason to deal with the drift and alignment issues associated with this technique.

To clear up one small point of confusion, even in an FPS the invididual Euler angles themselves (pitch and yaw) are in fact updated incrementally in response to mouse input. However, the object orientation itself is created from scratch using the angles, and is not adjusted incrementally.

To close, incremental rotations are appropriate (and necessary) when there is no particular frame of reference for orientation, and/or arbitrary rotations or rotations about local rather than global axes are required. Incremental rotations of this type can be implemented using vectors, matrices, or quaternions, but not Euler angles (at least not in a straightforward manner).

Share this post


Link to post
Share on other sites
Quote:
Original post by Subotron
Ok, thanks a lot. I understand what you mean why gluLookAt isn't the way to go. I learned quite some vector and matrix maths at school, so I should be able to work with them directly. However, from what I've always heard, gluLookAt has no gimbal lock problems (just like quaternions, both dreams shattered), but glRotate actually forces you to do the rotations one by one, therefore you cannot avoid the gimbal lock problem there.
I was writing my last post when you posted yours :) Yeah, saying that 'gluLookAt() has no gimbal lock problems' isn't particularly meaningful, IMO. I mean, technically it doesn't in and of itself (it does have degenerate cases though), but that's not particularly relevant.

You're right about glRotate*() though. A sequence of glRotate*() calls is equivalent to the from-scratch Euler construction I described in my previous post, and is therefore subject to gimbal lock. To avoid this you'll have to use incremental rotations.

Share this post


Link to post
Share on other sites
ok, I think I fully understand your last two posts. I can see now why incrementing isn't the best thing to do for FPS cameras. Also, I suppose gimbal lock will hardly ever be a problem, since most cameras dont have to flip over the x-axis anyway. However, suppose I would make a demo, where the camera might do unnatural things, I'd still like having the possibility. You say this requires incremental updates. I'm using this, but with my old method I still got the gimbal lock. So then I thought I'd have to use a different representation, quaternions. It doesn't seem to matter. So my biggest question is, what method (I don't need code, but I am in need of theory :)) would be used to convert a certain representation (whether it is axis/angle, quaternions, or whatever) to a camera that does have full freedom? Or is that too big of a question to directly answer?

And well, thanks again. I keep getting amazed how many people here devote their spare time to helping others.

Edit: by the way, is re-calculating the up vector going to help? The problem happens when the direction vector is pointing along the global up vector (ok, you knew that)

Share this post


Link to post
Share on other sites
I fixed the gimbal lock!

I have rewritten the class, and now store the camera based on a position and a direction. The former is a point, the latter a quaternion. I now provide angle/axis to the rotation function of the camera, which converts it to a quaternion, and applies
direction = mult(rotationquat, old_direction), conjugate(rotationquat);
then I normalize the direction

My look functions now constructs a matrix out of the direction quaternion, and I use glMultMatrixf() to apply the matrix. Then I use a translatef to translate according to the position (only inverse).

Still a few bugs, but no gimbal lock :)

Edit: just one bug, every axis seems flipped, and I can't fix the matrix to solve this. (I can make the flip correct, but there's still a small fault then)

[Edited by - Subotron on October 8, 2006 7:02:12 PM]

Share this post


Link to post
Share on other sites
Quote:
Original post by Subotron
I just found the article that told me this way of using quaternions would solve gimbal lock:

http://www.gamedev.net/reference/programming/features/qpowers/page5.asp

Can you tell if I interpreted it wrong or if the writer is just wrong? :)
Briefly, I only saw a couple of problems with the article. First, the author suggests that coordinate system handedness and matrix basis vector orientation is somehow related, which is wrong. Also (at least this is the way I read it) he seems to imply that it is the concatenation of rotations using quaternions specifically ('in 4D space') that prevents gimbal lock. This is also wrong: it is simply that he is using incremental rotations and local-axis rotations, which (again) can be done just as easily with vectors or matrices.

Other than that, his 'no gimbal lock' example looks correct (although I didn't study it too carefully).

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this