Why the fudge won't my quaternion-rotation work!?

Started by
7 comments, last by Nyarlath 16 years, 2 months ago
Okay, I'm still somewhat new to these beasts, but after having read several articles on the topic - well... no, I gotta be honest, I was STILL confused. But then I found a small paper on how to actually use them in a game-context and it all seemed easy. Too easy, I suppose. ;) Anyways, this [PDF] is where I got the paper. I post this here, because there might be something wrong with it, who knows. This is my source-code (in C++): Quaternion.cpp

#include "quaternion.h"

void CQuaternion::Set ( float a, float b, float c, float d )
// sets a quaternion to given values
{
	A = a;
	B = b;
	C = c;
	D = d;
}

CQuaternion CQuaternion::operator * ( const CQuaternion &quaternion ) const
// performs multiplication of two quaternions
{
	float a, b, c, d;

	// calculate
	a = ( A * quaternion.D ) + ( D * quaternion.A ) + ( B * quaternion.C ) - ( C * quaternion.B );
	b = ( B * quaternion.D ) + ( D * quaternion.B ) + ( C * quaternion.A ) - ( A * quaternion.C );
	c = ( C * quaternion.D ) + ( D * quaternion.C ) + ( A * quaternion.B ) - ( B * quaternion.A );
	d = ( D * quaternion.D ) - ( A * quaternion.A ) - ( B * quaternion.B ) - ( C * quaternion.C );

	return CQuaternion ( a, b, c, d );
}

CQuaternion & CQuaternion::operator *= ( const CQuaternion &quaternion )
// performs multiplication of two quaternions
{
	// calculate
	A = ( A * quaternion.D ) + ( D * quaternion.A ) + ( B * quaternion.C ) - ( C * quaternion.B );
	B = ( B * quaternion.D ) + ( D * quaternion.B ) + ( C * quaternion.A ) - ( A * quaternion.C );
	C = ( C * quaternion.D ) + ( D * quaternion.C ) + ( A * quaternion.B ) - ( B * quaternion.A );
	D = ( D * quaternion.D ) - ( A * quaternion.A ) - ( B * quaternion.B ) - ( C * quaternion.C );

	return *this;
}

CQuaternion Conjugate ( CQuaternion &quaternion )
// conjugates a quaternion
{
	CQuaternion ConjugatedQuaternion;

	// negate first three elements
	ConjugatedQuaternion.Set ( -quaternion.A, -quaternion.B, -quaternion.C, quaternion.D );

	return ConjugatedQuaternion;
}

CVertex RotateVertexAroundAxis ( CVertex vertex, CVector3 axis, float angle )
// rotates a vertex a given angle around axis
{
	float short_term;
	CQuaternion VertexQuaternion, RotationQuaternion, ResultingQuaternion;

	// get quaternion from vertex
	VertexQuaternion.Set ( vertex.GetX ( ), vertex.GetY ( ), vertex.GetZ ( ), 1.0f );

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

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

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

	// calculate result
	//ResultingQuaternion = RotationQuaternion * VertexQuaternion * Conjugate ( RotationQuaternion );
	ResultingQuaternion = Conjugate ( RotationQuaternion ) * VertexQuaternion * RotationQuaternion;

	// return rotated vertex
	return CVertex ( ResultingQuaternion.A, ResultingQuaternion.B, ResultingQuaternion.C );
}


main.cpp

CVertex Point;
CVector3 Axis;

Point.Set ( 3.0f, 7.0f, -2.0f );
Axis.Set ( 0.156f, -0.312f, 0.937f );
CVertex Result = RotateVertexAroundAxis ( Point, Axis, 30.0f );
printf ( "result: %f %f %f", Result.GetX ( ), Result.GetY ( ), Result.GetZ ( ) );


I played around with the lines that are commented out, because I suspected errors there (the normalization of the axis and the order of multiplications), but I tried all combinations and - while it did change the result - it never fixed my problem... Okay, as you can see, this is what I'm doing (or, rather, trying to do): I set up a point/vertex (like in the example in the paper, at 3/7/-2), then I set up an axis (again, like in the example, with normalized values by the way) and then I call my function "RotateVertexAroundAxis ( [...])" - problem is: I get wrong values. Take a look:

result: 2.999555 6.998961 -1.999703
Look at the paper to see what I should be getting. I already checked my conjugate-function and if the paper is correct (and I understood it correctly), it does exactly what I want it to do (invert first three elements, leave fourth untouched). So, please, take a look at my code, tell me where it does something or doesn't that should or should not be done according to the paper and then let me know. I suspect this might be easy for experts to solve, or maybe it's a dumb typo somewhere, but I can't find it. While you're at it, general comments and critique on the quaternion-implementation are always appreciated as well. Thanks ahead of time.
Advertisement
Assuming you've defined quaternion multiplication in the standard way, it should not be:
ResultingQuaternion = Conjugate ( RotationQuaternion ) * VertexQuaternion * RotationQuaternion;
But rather:
ResultingQuaternion = RotationQuaternion * VertexQuaternion * Conjugate ( RotationQuaternion );
I may post again with some suggestions for your implementation as well :)
First, whenever I see someone implementing a general game math library from scratch, I feel compelled to point out there are many such libraries already available. I point this out not because I happen to be co-author of such a library (well, not only because I happen to be co-author of such a library), but rather because I think there's a lot to be said for streamlining the development process wherever possible (which means, among other things, using good third-party resources where available).

Now, some suggestions:

1. I would call the elements x, y, z, and w, rather than A, B, C, and D. It is the convention, and will make your code easier to read and to compare with written examples. It will also make it easier to follow the code, since (at least in the context of rotations), the named elements x, y, z, and w each has its own significance. (I see that the paper you're referencing uses A, B, C, and D, but I would still suggest that you use the other convention.)

2. If you don't already, you should include a constructor that takes four real-number values (i.e. x, y, z, and w). Then, many of the places where you create a quaternion and then assign values to it can be streamlined, code-wise.

3. I would argue that there is nothing in particular to be gained by prefixing your class names with 'C'.

4. You'll want a function to build a rotation quaternion from an axis-angle pair (this is a common enough operation that you won't want to code it from scratch every time you encounter it).

5. In C++ it's preferred to declare and initialize variables at first use, rather than declaring them at the top of the function (as was required by earlier versions of the C standard).

6. I would suggest starting variable names with a lower-case letter rather than upper-case, to distinguish them from function and class names.

Oh, and I see now that you already tried the other multiplication order, so at this point I'm not sure why your results are wrong. Here's a guess though: you appear to be passing the angle argument in degrees, but C++ trig functions work with radians.
I say this only as a side-note, since it appears that the operator *= is not used anywhere in the code: To calculate B you should use the previous value of A, not the one computed at the line above. Same for C and D.
Quote:Original post by jyk
Here's a guess though: you appear to be passing the angle argument in degrees, but C++ trig functions work with radians.


That did it, so it was a rather stupid mistake all along. Thanks for the help! :)

One more question: is there a nicer way than writing your own deg2rad-conversion functions (in my engine, there's no real place for these functions as of now)? I'd really prefer using functions from cmath for that, but it seems it has no such functions (or I can't find them). Right now, I just do the conversion on the spot, but that's very ugly, obviously.

Quote:Original post by jyk
First, whenever I see someone implementing a general game math library from scratch, I feel compelled to point out there are many such libraries already available.


It's definitely good to know how to use the work of others efficiently, no question there, but in this case, I'm actually working on a framework/engine-project and not only do I want to keep the dependencies down to the minimum, I also kinda want to actually learn something along the way. ;) (Yes, I realize you could argue that, for these kind of projects, there's just SO much work involved in general that there's plenty to learn without the need to worry about all abstract mathematical calculations - vectors, matrices, quaternions for example - and directly skipping to the "interesting" parts, but I'm somewhat puristic in the sense that, when I created this project, I wanted to get deep down into everything that's involved in a game-engine - PLUS, if I ever gonna use the final project as an application, it might sound better to not add a list of other libraries that take all the hard work from you, but to be able to say that everything is implemented in the code directly).

Quote:Original post by jyk
1. I would call the elements x, y, z, and w, rather than A, B, C, and D. It is the convention, and will make your code easier to read and to compare with written examples. It will also make it easier to follow the code, since (at least in the context of rotations), the named elements x, y, z, and w each has its own significance. (I see that the paper you're referencing uses A, B, C, and D, but I would still suggest that you use the other convention.)


This would make sense, I see. I'll probably change this then.

Quote:Original post by jyk
3. I would argue that there is nothing in particular to be gained by prefixing your class names with 'C'.

5. In C++ it's preferred to declare and initialize variables at first use, rather than declaring them at the top of the function (as was required by earlier versions of the C standard).


These two are regarding "style" and to be honest, I've just had it with these discussions... ;) I respect your comments and maybe, if looked at from this or that angle, they would actually make this or that aspect a bit more clear or read better or whatever, but, to be honest, everybody has a different style, and back when I posted my vector header-file for criticism, people told me to use the C-prefix, for example (well, some did, others said: "NOO! DON'T!") - I prefer concentrating on getting the code to work in a good way (as long as nobody comes up with real, rational reasons of why people should use this or that way which are a little more elaborate than "doing this might improve readibility").

Quote:Original post by jyk
6. I would suggest starting variable names with a lower-case letter rather than upper-case, to distinguish them from function and class names.


This is another one regarding style, but that makes absolutely sense and is actually how it is like in any other function of the engine-project, these are mistakes, I'll correct them to lower-case.

Quote:Original post by jyk
2. If you don't already, you should include a constructor that takes four real-number values (i.e. x, y, z, and w). Then, many of the places where you create a quaternion and then assign values to it can be streamlined, code-wise.


Already have that in the header directly.

Quote:Original post by jyk
4. You'll want a function to build a rotation quaternion from an axis-angle pair (this is a common enough operation that you won't want to code it from scratch every time you encounter it).


Ah, I see. I'll implement this when I need it.

Quote:Original post by Nyarlath
I say this only as a side-note, since it appears that the operator *= is not used anywhere in the code: To calculate B you should use the previous value of A, not the one computed at the line above. Same for C and D.


Good observation, didn't caught that, but you're right. That would return wrong results. However, you're also right in suspecting that I don't use the *= operator anywhere. Will get it fixed.


Thanks to both of you for your help and response. Thumbs up!
Quote:Original post by d h k
These two are regarding "style" and to be honest, I've just had it with these discussions... ;) I respect your comments and maybe, if looked at from this or that angle, they would actually make this or that aspect a bit more clear or read better or whatever, but, to be honest, everybody has a different style, and back when I posted my vector header-file for criticism, people told me to use the C-prefix, for example (well, some did, others said: "NOO! DON'T!") - I prefer concentrating on getting the code to work in a good way (as long as nobody comes up with real, rational reasons of why people should use this or that way which are a little more elaborate than "doing this might improve readibility").
Fair enough :)

However, I do strongly suggest that you at least reconsider on the subject of variable declaration and initialization. There's style, and then there's writing idiomatic and robust code, and the variable declaration issue is related more to the latter than the former.

A couple key ideas in C++ are a) to prefer initialization over assignment, and b) to make sure objects (and this includes primitives such as float) are always in a valid state.

There are other reasons not to declare your variables up front, such as readability (it clutters the code unnecessarily) and efficiency (sometimes there are performance gains to be had by delaying the declaration until needed).

I understand that style is subjective, but this one is really more of an issue of good programming practice.
Points 3 & 5 from jyk's post seems to be stylistic only, but after years of programmers experience I'd say - they are not. Both makes code a bit easier to read and also means less work for you(this is maybe not obvious but try it)!
I see. In the case of variable-definition, I have to agree. I'll change this. My paragraph was talking more about the C-prefix for classes... ;)

Okay, so, this is solved (thanks again), but this last question still haunts me:

Quote:
One more question: is there a nicer way than writing your own deg2rad-conversion functions (in my engine, there's no real place for these functions as of now)? I'd really prefer using functions from cmath for that, but it seems it has no such functions (or I can't find them). Right now, I just do the conversion on the spot, but that's very ugly, obviously.
If you are building an engine you will (soon or later) for sure need a personalized "math_utility" class/namespace. Put your inline conversion functions there (it's just a multiplication, isn't it?)!

One more hint: It'a good practice to call your files as your classes. Call your class "Quaternion", or your files "CQuaternion.h/.cpp".

This topic is closed to new replies.

Advertisement