Glitch with quaternion-based camera...

Started by
8 comments, last by Maze Master 15 years, 9 months ago
I have implemented a camera into my game-engine that allows for six degrees of freedom using quaternions. My test-application displays a world in which you can look and move around using FPS-like controls (mouse-cursor is hidden, moving the mouse looks around, using W/A/S/D moves forwards/strafes and you can also, unlike in most shooters, hit Q/E in order to rise or sink). My problem is, when you move the mouse in a circular motion (imagine drawing a circle in any first-person shooter), it not only rotates around the y- (moving mouse left/right) and x- (moving mouse up/down), it also rotates around the z-axis a bit. I've searched around and read that I'm not the only one suffering from this problem - somewhere it was suggested that a correct implementation of a complete camera using quaternions will actually behave like that - it's correct, math-wise. Also, after having read that and then going through the math again, I kind see why it happens. Question is: how do I stop this unwanted behavior without getting rid of rotation around the z-axis altogether? Here's my complete implementation:

// camera.h

class CSimpleCamera
{
	public:

		CVector3	Position, Speed;
		CVector3	ForwardAxis, UpAxis, LeftAxis;
		
		CSimpleCamera::CSimpleCamera ( void )
		// initializes to default values
		{
			// place camera at origin and let it look along z-axis
			Position.Set ( 0.0f, 0.0f, 0.0f );
			Speed.Set ( 0.0f, 0.0f, 0.0f );
			ForwardAxis.Set ( 0.0f, 0.0f, -1.0f );
			UpAxis.Set ( 0.0f, -1.0f, 0.0f );
			LeftAxis.Set ( -1.0f, 0.0f, 0.0f );
		}

		CSimpleCamera::CSimpleCamera ( CVector3 position )
		// initializes to given values
		{
			// place camera and let it look along z-axis
			Position = position;
			Speed.Set ( 0.0f, 0.0f, 0.0f );
			ForwardAxis.Set ( 0.0f, 0.0f, -1.0f );
			UpAxis.Set ( 0.0f, -1.0f, 0.0f );
			LeftAxis.Set ( -1.0f, 0.0f, 0.0f );
		}

		void	Set ( CVector3 position );
		void	Set ( float x, float y, float z );
		void	MoveForward ( float distance );
		void	MoveUp ( float distance );
		void	MoveLeft ( float distance );
		void	StopMovingForward ( void );
		void	StopMovingUp ( void );
		void	StopMovingLeft ( void );
		void	RotateAroundLeftAxis ( float angle );
		void	RotateAroundUpAxis ( float angle );
		void	RotateAroundForwardAxis ( float angle );
		void	Update ( void );
};


// camera.cpp

void CSimpleCamera::Set ( CVector3 position )
{
	Position = position;
	Speed.Set ( 0.0f, 0.0f, 0.0f );
}

void CSimpleCamera::Set ( float x, float y, float z )
{
	Position = CVector3 ( x, y, z );
	Speed.Set ( 0.0f, 0.0f, 0.0f );
}

void CSimpleCamera::MoveForward ( float distance )
{
	if ( distance > 0.0f )
	{
		if ( Speed.Z < distance )
			Speed.Z = distance;
	}
	else if ( distance < 0.0f )
	{
		if ( Speed.Z > distance )
			Speed.Z = distance;
	}
}

void CSimpleCamera::MoveUp ( float distance )
{
	if ( distance > 0.0f )
	{
		if ( Speed.Y < distance )
			Speed.Y = distance;
	}
	else if ( distance < 0.0f )
	{
		if ( Speed.Y > distance )
			Speed.Y = distance;
	}
}

void CSimpleCamera::MoveLeft ( float distance )
{
	if ( distance > 0.0f )
	{
		if ( Speed.X < distance )
			Speed.X = distance;
	}
	else if ( distance < 0.0f )
	{
		if ( Speed.X > distance )
			Speed.X = distance;
	}
}

void CSimpleCamera::StopMovingForward ( void )
{
	if ( Speed.Z != 0.0f )
		Speed.Z = 0.0f;
}

void CSimpleCamera::StopMovingUp ( void )
{
	if ( Speed.Y != 0.0f )
		Speed.Y = 0.0f;
}

void CSimpleCamera::StopMovingLeft ( void )
{
	if ( Speed.X != 0.0f )
		Speed.X = 0.0f;
}

void CSimpleCamera::RotateAroundLeftAxis ( float angle )
{
	ForwardAxis = RotateVectorAroundAxis ( ForwardAxis, LeftAxis, angle );
	UpAxis = RotateVectorAroundAxis ( UpAxis, LeftAxis, angle );
}

void CSimpleCamera::RotateAroundUpAxis ( float angle )
{
	ForwardAxis = RotateVectorAroundAxis ( ForwardAxis, UpAxis, angle );
	LeftAxis = RotateVectorAroundAxis ( LeftAxis, UpAxis, angle );
}

void CSimpleCamera::RotateAroundForwardAxis ( float angle )
{
	UpAxis = RotateVectorAroundAxis ( UpAxis, ForwardAxis, angle );
	LeftAxis = RotateVectorAroundAxis ( LeftAxis, ForwardAxis, angle );
}

void CSimpleCamera::Update ( void )
{
	Position += ( ForwardAxis * Speed.Z );
	Position -= ( UpAxis * Speed.Y );
	Position -= ( LeftAxis * Speed.X );
}



And this is the function that takes a vector and returns another one that is rotated around a given axis by a given angle:

CVector3 RotateVectorAroundAxis ( CVector3 vector, CVector3 axis, float angle )
// rotates a vector a given angle around axis
{
	float short_term;
	CQuaternion vector_quaternion, rotation_quaternion, resulting_quaternion;

	// get quaternion from vector
	vector_quaternion.Set ( vector.X, vector.Y, vector.Z, 0.0f );

	// normalize length of axis for safety purposes
	Normalize ( axis );

	// convert angle from degrees to radians
	angle = angle * ( 3.14159265358979323846f / 180.0f );

	// use short-cut to avoid redundant calculations
	short_term = ( float ) ::sin ( angle / 2.0f );

	// get quaternion from axis
	rotation_quaternion.Set ( axis.X * short_term, axis.Y * short_term, axis.Z * short_term, ( float ) ::cos ( angle / 2.0f ) );

	// calculate result
	resulting_quaternion = rotation_quaternion * vector_quaternion * Conjugate ( rotation_quaternion );
	
	// return rotated vertex
	return CVector3 ( resulting_quaternion.GetA ( ), resulting_quaternion.GetB ( ), resulting_quaternion.GetC ( ) );
}


Please don't hesitate to ask for more code if you need it - any help or insight on the matter is greatly appreciated.
Advertisement
A few quick points:

1. The behavior you're seeing has nothing to do with the fact that you're using quaternions; it's simply a characteristic of 6DOF motion (which you can implement just as easily using matrices or vectors).

2. FPS-style character control and 6DOF motion are sort of at odds with each other. Typically, you would implement FPS-style motion using a 'from scratch' Euler-angle or spherical coordinate construction, which avoids any unwanted roll.

3. Your use of quaternions here is completely incidental (and, frankly, pointless). It would be more straightforward simply to build a rotation matrix from the axis-angle pair and apply it directly to the input vector.

4. As far as your 6DOF implementation goes, it doesn't look like you're orthonormalizing your basis vectors, which means that they may end up drifting out of alignment over time.

A good next step would probably be to decide exactly what sort of motion you want here; it looks like you've correctly implemented 6DOF motion, but it sounds like you're actually after something else.
Thanks for the reply.

Quote:Original post by jyk
[...] it looks like you've correctly implemented 6DOF motion, but it sounds like you're actually after something else.


(I guess this relates to points 1-3):

Okay, well, I'm working on a game-engine and thus I'm look for a reliable camera that will work for any kind of application, from first- or third-person shooters over real-time strategy and role-playing games to flight-simulators and racing games.

What do other engines do? Is it common to implement several different camera-models (one for a first-person camera using euler angles, one for flight-simulators using quaternions or whatever)? Or is there a camera-model that can handle all these things at once, with the same implementation? I thought the quaternion-based one could.


Quote:Original post by jyk
4. As far as your 6DOF implementation goes, it doesn't look like you're orthonormalizing your basis vectors, which means that they may end up drifting out of alignment over time.


Haven't noticed any change in the way the camera reacts over time, but I'll look into it.
Quote:Original post by d h kOr is there a camera-model that can handle all these things at once, with the same implementation? I thought the quaternion-based one could.


A camera should be just that, a camera. An object which you can tell: go here and look in that direction. Implementing different camera movement and orientation models should be done at a higher level - for example, with a node hierarchy.

First-person: create a parent yawing node and a child pitching node (and possibly a child rolling node). Now things are as easy as having the yaw node orientated with the correct up vector and yaw, and the pitch node orientated with the correct pitch in local-space.

Third-person: create a parent rotating node (possibly separated in yaw/pitch/roll for convenience again), and create a child node. Move the child back in local space, and you're pretty much all done. As the parent node rotates, the child node moves accordingly.

Catch my drift? This way you don't even need to worry about whether the underlying implementation is using Euler angles, or maybe quaternions, or maybe matrices, so on.
Quote:Okay, well, I'm working on a game-engine and thus I'm look for a reliable camera that will work for any kind of application, from first- or third-person shooters over real-time strategy and role-playing games to flight-simulators and racing games.

What do other engines do? Is it common to implement several different camera-models (one for a first-person camera using euler angles, one for flight-simulators using quaternions or whatever)? Or is there a camera-model that can handle all these things at once, with the same implementation? I thought the quaternion-based one could.
Whether or not you use quaternions won't have much (if any) bearing on what you can or cannot do with your camera; all it'll do is (perhaps) make certain operations more or less efficient, straightforward, or elegant.

Anyway, I would say that this is a good example of why you should 'make games, not engines' (Google that term, and you should find an article on the subject). If however you're dead set on creating a generic game engine, I'd recommend taking a component- and/or script-based approach. With this design, different camera behaviors would simply be plug-ins that you would attach to a game object; you could include some basic behaviors with the engine (e.g. FPS, 6DOF, third-person), while leaving the door open for adding other behaviors as needed.
Just curious as to why you maintaining ForwardAxis, UpAxis, and LeftAxis vectors and not a quaternion for the orientation? Isn't the whole point of a quaternion camera that you use a quaternion for the orientation instead of the basis vectors?
http://www.dhpoware.com
Sorry for not answering earlier, I was very busy this week.

Quote:Original post by jyk
Whether or not you use quaternions won't have much (if any) bearing on what you can or cannot do with your camera; all it'll do is (perhaps) make certain operations more or less efficient, straightforward, or elegant.


Makes sense. So, following that thought, how inefficient or inelegant would it be to implement a 6DOF-camera just like for flying simulators and then just taking one degree of freedom away for first- or third-person perspectives, for example?

Quote:Original post by jyk
Anyway, I would say that this is a good example of why you should 'make games, not engines' (Google that term, and you should find an article on the subject). If however you're dead set on creating a generic game engine, I'd recommend taking a component- and/or script-based approach. With this design, different camera behaviors would simply be plug-ins that you would attach to a game object; you could include some basic behaviors with the engine (e.g. FPS, 6DOF, third-person), while leaving the door open for adding other behaviors as needed.


Okay, that's the fallback plan then.

If you want to store the camera as a point/quaternion pair, read on:

Rotation can be achieved by "setting" the 2 of the vectors via rotation (which is what you are doing) OR by multiplying by a matrix:

Lets say we represent the camera as a 4x4 matrix, eye_matrix_world, so that

position_in_eye= eye_matrix_world * position_in_world.

now to move we just multiple by a translation matrix:

eye_matrix_world <-- transalation_matrix * eye_matrix_world

where translation_matrix is 1's on diaganol and the last column holds the translation.... we can do rotating in a similar way as well:

eye_matrix_world <-- rotation_matrix * eye_matrix_world

now, to use quaternions, you need to break down how the translation is stored and such, one way is as follows:

A(p,q) := translate by vector p, then rotate by q.
[Note: the camera is at -p, not p, since the point -p in world co-ordinates gets mapped to 0 in eye-coordinates here].

So now rotating the camera is represented by a quaternion, R, so then the new pair is: A(p,Rq).


you will need to chase some algebra to grok what happens, but it is a worth while exercise, which is one reason why I left out how to "move the camera forward".

One nice bit is that rather than needing to re-orthogonalize you just need to normalize the quaternion, which is not so bad...


Note: a rotation be a quaternion is always orientation preserving!

Quote:
Anyway, I would say that this is a good example of why you should 'make games, not engines'


I would argue that understanding co-ordinate transformations is elementary enough that it is worth implementing once on your own, even if one uses a ready made engine... this is the theory forum:) Without this understanding the code and engine look like black magic and one does not understand why things are the way they are and what are the limitations for each....

out of morbid curiosity I did a google and got the link: http://scientificninja.com/advice/write-games-not-engines which has a point, but is kind of extreme, and in my eyes short sighted, what one learns in making such an engine is the goal often, and after the first try or 2, then the real design and understanding happen as well, lots of engines started as hobbyist projects, but it is just someone's blog....

Close this Gamedev account, I have outgrown Gamedev.
Quote:
Quote:
Anyway, I would say that this is a good example of why you should 'make games, not engines'
I would argue that understanding co-ordinate transformations is elementary enough that it is worth implementing once on your own, even if one uses a ready made engine... this is the theory forum:) Without this understanding the code and engine look like black magic and one does not understand why things are the way they are and what are the limitations for each....
Oh, I wasn't saying that he shouldn't implement it himself. I was just referring to the OP's earlier statement:
Quote:...a reliable camera that will work for any kind of application, from first- or third-person shooters over real-time strategy and role-playing games to flight-simulators and racing games.
Which suggests that his aim is to develop a 'generic' engine first, and then make a game with it (which is what the article I mentioned cautions against).
One way to visualize why this is happening is this:

Pretend you start at the equator on the earth facing north and walk north. Once you travel a few thousand miles, stop going north and start sidestepping right. At first you will be going due west, but if you keep sidestepping in what seems to you a "straight line", you will be traveling on a great circle which also goes south.* Then when you hit the equator again, you will be facing some degree of northwest. Follow the equator back to your starting point with back-side steps, and you will be back where you started, except facing northwest instead of north.

This is exactly what is happening in your engine, with the analogy that walking forward is the same as moving the mouse in the y-direction, and sidestepping is the same as moving the mouse in the x-direction.

*if this doesn't make sense at first, consider what would happen if you got 1 foot from the north pole and then started sidestepping without turning your body. What would your path look like?

This topic is closed to new replies.

Advertisement