Quaternion based camera shows odd behaviour

Started by
14 comments, last by Zakwayda 17 years, 6 months ago
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]
Advertisement
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]
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?
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 :)
You may be interested in other thread on similar topic there... i think the problem is similiar and i posted some pseudocode for it.
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).
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 :(
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!?)
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...
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.

This topic is closed to new replies.

Advertisement