Quaternion camera, _translation_ problem

Started by
10 comments, last by longjumper 17 years, 1 month ago
I learned all about quaternions yesterday and successfully implemented them in OpenGL for my camera. I can spin about just wonderfully now with no gimbal lock. I'm making a ship-flying type game, and so obviously I want to be able to move the ship forward, in the local z direction. My code keeps track of the rotation quaternion, and with rotations I just have a small pitch or roll angle quaternion to multiply with and get the new rotation. To keep track of my world position, I have xpos, ypos, zpos. My problem seems to be getting the correct view vector out of the quaternion. My initial vector, for movement, is (0, 0, 1) because I want to move in Z. To rotate it with the quaternion, I looked up how to turn the quat into a rotation matrix. Multiplying that by (0,0,1) gives the third column of the matrix. These (I think) should be the new view vector, which I should add to my world position. Here's my code:

if (KeyDown(VK_SPACE))
	{
		xpos += 2*rotation.x*rotation.z - 2*rotation.w*rotation.y;
		ypos += 2*rotation.y*rotation.z + 2*rotation.w*rotation.x;
		zpos += rotation.w*rotation.w - rotation.x*rotation.x - rotation.y*rotation.y + rotation.z*rotation.z;
	}

...

glTranslatef(0.0f,0.0f,-6.0f);	// Move Into The Screen

// draw the ship here
	
glRotatef(114.6*acos(rotation.w),rotation.x,rotation.y,rotation.z);

glTranslatef(xpos,ypos,zpos);


From the start, when I move into Z, it works. When I pitch myself up and move into Y, it works. When I roll to one side and then pitch myself into the X, it moves in Y instead. I tried a random guess fix and swapped the + and - signs in the xpos and ypos formulae, to get the bottom row of the matrix instead. This actually fixed the x problem, so now movement on all three axes moved correctly by themselves. The big problem is still that this movement doesn't work. It seems to at first, but after a bit of flying around, I start sliding sideways, backwards, down, some combination of them, etc. Is my math wrong, or something else in my code?
Advertisement
glRotate isn't going to be useful in this case. In fact, glRotate is rarely useful at all. With quaternions and OpenGL, you'll usually be constructing a matrix from the quaternion. In this case, since you are just doing a camera, it's even simpler because gluLookAt will multiply the appropriate matrix for you, without having to do it by hand.

All you need to do is write a method that rotates a vector by a quaternion. Use that to rotate {0, 0, 1} and {0, 1, 0} to get your appropriate forward and up vectors, and just plug those directly into gluLookAt(position, position + forward, up);

Ok, that took a few times reading through, but I think I get it. Instead of rotate, do the use the entire quaternion->rotation matrix and multiply in my own method. Then use it on those two vectors, one for my forward flight direction and the other for my ship's roll / up direction. Use that for the camera instead of translating.

Will that fix my flight problem? I'm assuming I then take my transformed forward vector and add some speed multiple of it to my position vector.
Quote:Original post by Tesserex
Ok, that took a few times reading through, but I think I get it. Instead of rotate, do the use the entire quaternion->rotation matrix and multiply in my own method. Then use it on those two vectors, one for my forward flight direction and the other for my ship's roll / up direction. Use that for the camera instead of translating.

Will that fix my flight problem? I'm assuming I then take my transformed forward vector and add some speed multiple of it to my position vector.
Looking at the code you posted earlier:

glTranslatef(0.0f,0.0f,-6.0f);	// Move Into The Screen// draw the ship here	glRotatef(114.6*acos(rotation.w),rotation.x,rotation.y,rotation.z);glTranslatef(xpos,ypos,zpos);

I'm unclear as to whether this is intended to be a camera or object transform, but in either case the sequence of transforms appears to be incorrect.

Can you clarify the purpose of the transform? You mentioned that this was for a camera, but the comment 'draw the ship here' seems to indicate otherwise.

Anyway, as mentioned, when working with a rotation in quaternion form it's typical to convert it to a matrix before submitting it to OpenGL. Furthermore, the direction vectors can be extracted directly from this matrix; there's no need to perform additional vector rotations to derive these vectors.

As for 114.6, that is one 'magic number' :) I see what you're doing (converting to degrees and multiplying by two), but it would be far better to write a simple utility function to perform the conversion (even then using a named constant rather than 57.x), and then include the factor of 2 directly in the expression.

However, although it looks like this method should work, again, it would probably be better to use a matrix. Your method relies on the specifics of how a quaternion is used to represent a rotation; although it's important to understand these specifics, code that works with quaternions should treat them more like a 'black box'. In short, you should avoid mucking around with the quaternion elements directly in most cases.

If you can clarify what the purpose of the transform you posted is, we can probably point out more specifically where the code might be in error.
Ok, for the purpose. Think Star Fox. Third person view. The ship is in the center of the screen, at it's location. The camera is a fixed distance directly behind it. So the first set of commands moves the camera back from the ship and draws the ship. Then we spin the world around the ship to orient it, then move the ship to it's location in space. Because the ship was already drawn, it's tied to the camera and they move together.

For now, though, I've removed that part and am going to get first person flying working first. It has the same problems though. I implemented some matrix stuff this time, and now my problems are backwards. The moving seems to work, but the rotating around is busted. Here's my new code:

Vector Transform(Vector v)	{		Vector nv;		nv.x = v.x*(w*w + x*x - y*y - z*z) + v.y*(2*x*y - 2*w*z) + v.z*(2*x*z + 2*w*y);		nv.y = v.x*(2*x*y + 2*w*z) + v.y*(w*w - x*x + y*y - z*z) + v.z*(2*y*z - 2*w*x);		nv.z = v.x*(2*x*z - 2*w*y) + v.y*(2*y*z + 2*w*x) + v.z*(w*w - x*x - y*y + z*z);		return nv;	}

You can probably tell, this just transforms any vector by the rotation matrix derived from the quaternion. This function is a member of my quaternion class.

Here's the keypress stuff...
if (KeyDown(VK_LEFT))	{		rotation.Multiply(rollmq);		up = rotation.Transform(yvect);	}	if (KeyDown(VK_RIGHT))	{		rotation.Multiply(rollq);		up = rotation.Transform(yvect);	}	if (KeyDown(VK_UP))	{		rotation.Multiply(pitchq);		forward = rotation.Transform(zvect);	}	if (KeyDown(VK_DOWN))	{		rotation.Multiply(pitchmq);		forward = rotation.Transform(zvect);	}	if (KeyDown(VK_SPACE))	{		position.x += forward.x;		position.y += forward.y;		position.z += forward.z;	}

"rollmq" and "pitchmq" are the tiny angle quaternions for the negative turns. I figured that the up and forward vectors need only be updated if the roll and pitch change, respectively. If I update both each time, it still doesn't work, but it's behaves differently, so this might be a clue.

gluLookAt(position.x,position.y,position.z,position.x+forward.x,position.y+forward.y,position.z+forward.z,up.x,up.y,up.z);


This is now my only camera modifying line. It seems ok, giving position, position+forward, and up.
This isn't a complete answer to your question (I didn't look at your code carefully enough to comment in detail), but here are a few notes:

1. The problems of a) orienting and moving the ship, b) constructing an object matrix for the ship and rendering it, and c) constructing a view matrix for the camera should all be considered separately. I mention this because the code you posted seems to include elements of the solutions to all three problems, but itself is not the correct solution for any of them. Thinking about and solving the problems separately should help clear up some of this confusion.

2. Remember that when transforms are applied via OpenGL function calls, the order in which the transforms are applied is the opposite of the order in which the corresponding OpenGL function calls appear in the code.

3. The 'model transform' for an object typically consists of the transform sequence scale->rotate->translate (any of these is of course optional, and scale is often simply identity).

4. The 'view transform' that corresponds to a 'model transform' is, generally the speaking, the inverse of that transform. There are various ways the inverse can be computed. In your case it appears you're trying to do it manually by applying the inverse of the individual transforms in the opposite order. Leaving aside scale, this should translate to (translate^-1)->(rotation^1), where translate^-1 is the original translation negated, and rotation^-1 is the original rotation inverted (transpose for a matrix, conjugate for a quaternion, negation of angle or axis for an axis-angle pair).

5. Third-person cameras are a different problem. It looks like you're already taking this approach, but it would probably be best to get basic object motion and rendering and first-person camera mode working before trying to implement a proper 3rd-person camera.

I hope these notes will help you to identify some of the problems in your code. Feel free to post back if you have further questions.
Despite not having a clue what your post was saying, it allowed me somehow to fix the problem entirely.

Using the new vector approach with gluLookAt solved my translation problems but the gimbal lock came back. That was quite annoying. My fix?

Reverse the order in which I multiplied my quaternions to add rotations.

if (KeyDown(VK_LEFT))	{		Quaternion temp = rollmq;		temp.Multiply(rotation);		rotation = temp;		//rotation.Multiply(rollmq);	}


And if anyone would like to know, I intend this to eventually become a space fighter game where you aren't limited to fighting in one plane (the 2d space kind of plane, not the vehicle). Also, I plan to control it with wiimotes :-D
Unfortunately, you seem to be getting way ahead of yourself. You need to have a grasp on linear algebra(at least the parts that pertain to 3D graphics) and the OpenGL API. I'd suggest getting yourself a book, there are plenty of good ones out there on the subject. I personally liked "Mathematics for 3D Game Programming and Computer Graphics" and "3D Math Primer for Graphics and Game Development." Yes, it is true that you can learn plenty about all the fancy pants stuff out there just by using google, however, it *appears* as though you lack the basic understanding of what's really going on when you make these calls. It is incredibly important that you do understand that in order to use it properly.

That being said, here is the important parts of my camera class, which is far from perfect but may shine some light on it.

#import <OpenGL/gl.h>#import <OpenGL/glu.h>#import "OCCamera.h"@implementation OCCamera- (id)initWithLocation:(vector_t)loc width:(int)w height:(int)h{	[super init];		position = loc;	screenWidth = w;	screenHeight = h;	screenRatio = (float)(screenWidth/screenHeight);	near = 1.0f;	far = 768.0;	fov = 45.0f;		rotation = quaternion_identity();	//Completely unneccesary, but a good reminder.	forward = quaternion_rotate_vector(rotation, vector3(0,0,1));	up = quaternion_rotate_vector(rotation, vector3(0,1,0));	right = quaternion_rotate_vector(rotation, vector3(1,0,0));	yaw = pitch = 0.0f;		interpolationSpeed = 1.0f;		return self;}- (void)animate:(float)dt{	if(allowInterpolation)	{		elapsedTime += dt * interpolationSpeed;		if(elapsedTime > 1.0f)		{			elapsedTime = 1.0f;			allowInterpolation = false;		}		position = vector_add(initPosition, vector_scale(vector_subtract(destPosition, initPosition), elapsedTime));		rotation = Quaternion_SLERP(initRotation, destRotation, elapsedTime);				forward = quaternion_rotate_vector(rotation, vector3(0,0,1));		up = quaternion_rotate_vector(rotation, vector3(0,1,0));		right = quaternion_rotate_vector(rotation, vector3(1,0,0));		yaw = atan2(forward.x, forward.z);		pitch = acos(vector_dot_product(vector3(0, 1, 0), forward)) - OSML_PI / 2.0f;	}	glMatrixMode(GL_PROJECTION);		glLoadIdentity();			gluPerspective(fov, screenRatio, near, far);	glMatrixMode(GL_MODELVIEW);		glLoadIdentity();			gluLookAt(position.x, position.y, position.z,			  position.x + forward.x, position.y + forward.y, position.z + forward.z,			  up.x, up.y, up.z);}- (void)rotateYaw:(double)delta{	if(allowInterpolation)		return;			yaw += delta;	quaternion_t qPitch = quaternion_from_angle_around_axis(pitch, vector3(1,0,0));	quaternion_t qYaw = quaternion_from_angle_around_axis(yaw, vector3(0,1,0));		rotation = quaternion_product(qYaw, qPitch);		forward = quaternion_rotate_vector(rotation, vector3(0,0,1));	up = quaternion_rotate_vector(rotation, vector3(0,1,0));	right = quaternion_rotate_vector(rotation, vector3(1,0,0));}- (void)rotatePitch:(double)delta{	if(allowInterpolation)		return;		pitch += delta;	quaternion_t qPitch = quaternion_from_angle_around_axis(pitch, vector3(1,0,0));	quaternion_t qYaw = quaternion_from_angle_around_axis(yaw, vector3(0,1,0));		rotation = quaternion_product(qYaw, qPitch);		forward = quaternion_rotate_vector(rotation, vector3(0,0,1));	up = quaternion_rotate_vector(rotation, vector3(0,1,0));	right = quaternion_rotate_vector(rotation, vector3(1,0,0));}- (void)setPitch:(double)p{	if(allowInterpolation)		return;	pitch = p;	[self rotatePitch:0];}- (void)setYaw:(double)p{	if(allowInterpolation)		return;	yaw = p;	[self rotateYaw:0];}	- (vector_t)targetPoint:(vector_t)point distance:(float)f{	return vector3(point.x - forward.x * f, point.y - forward.y * f, point.z - forward.z * f);}- (void)targetOnPoint:(vector_t)point distance:(float)f{	if(allowInterpolation)		return;		position.y = point.y - forward.y * f;	position.z = point.z - forward.z * f;	position.x = point.x - forward.x * f;}- (void)orbitYaw:(double)amt aroundPoint:(vector_t)center{		if(allowInterpolation)		return;		vector_t newPos;	quaternion_t newRot;	float radius = sqrtf(pow(position.x - center.x, 2) + pow(position.z - center.z, 2));	yawOrbit += amt;	newPos.x = center.x + cos(yawOrbit + OSML_HALF_PI) * radius;	newPos.y = position.y;	newPos.z = center.z - sin(yawOrbit + OSML_HALF_PI) * radius;	yaw += amt;	quaternion_t qPitch = quaternion_from_angle_around_axis(pitch, vector3(1,0,0));	quaternion_t qYaw = quaternion_from_angle_around_axis(yaw, vector3(0,1,0));		newRot = quaternion_product(qYaw, qPitch);	position = newPos;	[self rotateTo:newRot];}- (void)rotateTo:(quaternion_t)q{	if(allowInterpolation)		return;			rotation = q;	forward = quaternion_rotate_vector(rotation, vector3(0,0,1));	up = quaternion_rotate_vector(rotation, vector3(0,1,0));	right = quaternion_rotate_vector(rotation, vector3(1,0,0));}- (bool)interpolateTo:(vector_t)pos withRotation:(quaternion_t)rot withSpeed:(float)speed cancelPrevious:(bool)cancel{	if(allowInterpolation && !cancel)		return false;		interpolationSpeed = speed;	allowInterpolation = true;		elapsedTime = 0.0f;	initPosition = position;	destPosition = pos;	initRotation = rotation;	destRotation = rot;	return true;}- (void)moveForward:(double)amt{	if(allowInterpolation)		return;		position.x += forward.x * amt;	position.y += forward.y * amt;	position.z += forward.z * amt;}- (void)moveRight:(double)amt{	if(allowInterpolation)		return;		position.x -= right.x * amt;	position.y -= right.y * amt;	position.z -= right.z * amt;}- (void)moveUp:(double)amt{	if(allowInterpolation)		return;		position.x += up.x * amt;	position.y += up.y * amt;	position.z += up.z * amt;}- (void)moveTo:(vector_t)pos{	if(allowInterpolation)		return;		position = pos;}@end
Well, first of all, it's fixed, thank you everyone for your insight.

Second, I'll not take offense to your comments, but your assumptions were wrong, Longjumper. I do have a grasp of linear algebra. I've been through a college course on it. Just last semester, in fact, with a primer on it (especially how it pertains to transformations) in my previous calculus 3 class. The only thing that was new to me here was the quaternion itself. I'm also in a class called Numerical Methods right now. You can probably guess I'm a CS major.

Thanks again to everyone.
No offense was intended, of course. And as always, I will admit when I am wrong, and in this case I may be. However, I took a class in linear algebra and numerical methods some years back, and it is only recently that I have truly grasped it intuitively. Perhaps I was just projecting myself onto you, though. ;)

This topic is closed to new replies.

Advertisement