# Gimbal Lock and Quaternions

## Recommended Posts

Regards, i am trying to use Quaternions to rotate an objet around the global X axis then around the global Y axis. i am converting the Quaternion to a matrix form to multiply it with the current matrix (using OpenGL). so i will have something like the follwoing: // the hidden global matrix, not implemented by myself CurrentMatrix(); // Quaternion-to-Matrix Rotatation around X MyQuatRotate(X, angle); // Quaternion-to-Matrix Rotatation around Y MyQuatRotate(Y, angle); but when this code is applied, the first rotation is performed around the global X axis but the second one is excuted around the object's local Y axis. is this what is called a Gimbal Lock??? and how can i solve it???? thanx

##### Share on other sites
I think its only gimbal lock if you rotate the Y by 90 degrees.
A simple search for 'gimbal lock' on [google] reveals all - Take a look here for a description of gimbal lock.
Its simple enough to solve, just don't rotate the XYZ components separately (This can't be done using euler angles and is why quaternions are so popular).
Presumably if you are trying to rotate and object, you know what angle to rotate to, so you can get the quaternion of that rotation and use that instead of computing the components separately.

Conceptualy, euler angles are easier to use, so let people suffer with gimbal lock to rotate an object to the correct angle but after that use quaternions to do all the work, since computers have no metal block dealing with them.

##### Share on other sites

rotating the object in the same function around the two axes will not benifit my application since i need to have the functionality of allowing the user to rotate the object around either axes (X or Y) separately. so, making the one rotation with one function call will not make it worw as desired.

do u suggest something else? i thought of trying SLERP, but i think it wont do what i an looking for since it is for animation smoothness adn i am not seeking this.

##### Share on other sites
gimble lock is really more of a problem with interpolation. if you're just using quaternions to represent rotations, and not using them to interpolate, then you got the concept wrong. For example if you're camara is interpolating x,y,z angles for it's movements, you get gimble lock if or if not using quaternions. what happens in when you're in a situation where your camera has an x,y,orz rotation that is close to 90degrees you get into a situation where small changes in x, y, z produce no noticible change in orientation. you camara get's stuck in a sense and there is no "small" rotation you can apply to get it unstuck. if you're just using quaternions to do rotations about the x, y, z axis this is mathematically equivilent to rotation using eular angles about the x, y, z axis. to see this simply convert the quaternion representing the rotation about the x axis into a matrix and you'll notice it's the same matrix you'd get using eular angles.

all that aside, the problem your having is this, it is NOT GIMBLE LOCK

suppose I have the matrix equation:
p = A*B*C*D*x

where A, B, C, D are matrix, and x is a vector.

say these matricies represent rotations.

there are two different ways to read what this is doing.
if I read right to left I have to think of the transformations being applied to the GLOBAL axis. if I read them right to left I have to think of the transformations being applied to the local axis, the reason you think the first rotation is being applied to the global x axis is because initially, the global x axis is the local x axis. let me try to think of a reasonable example

suppose I got a book that is initailly parallel to the x-y plane and I rotate it 90 degrees about the GLOBAL z-axis then 90 degrees about the GLOBAL x-axis. now we have a book that lies in the x-z plane. so suppose matrix A is a 90degree rotation about x and B is a 90 deg rotation about z then the equaiton we use to do this transformation is

p = A*B*x
note that if I read it right to left you get exactly what I described above.that is first we see B and see that its a 90 degreee rotation about the z axis, now what z axis?? The GLOBAL z. now we next read A and see that it's a 90 degree rotationo about the x axis, and since we're reading right to left it's the GLOBAL x axis.

so now lets read it from left to right, just to see the difference(this is the better way to read btw)

we first see A and we note that it's a rotation about x-axis, since we're reading left to right it's the LOCAL x axis. we then see B and we determin that A*B is taking the model and rotating it 90 deg about the LOCAL X-axis then rotation it 90 deg about the LOCAL z-axis. realize that this is two ways of thinking about the same thing, the end positoin of the model is the SAME it's all in how you think of it.

if you read left to right you have to think of the transformaitons as being applied to the local axis, if reading right to left you think of things being applied to global axis.

in openGl reading TOP to BOTTOM is like reading LEFT to RIGHT in the above equations. so when reading your code top to bottom you have to think of the transformations as being applied to the local axis. if you read bottom to top you can think of them as being applied to the global axis.

rotate model "angle" degrees about the local x-axis,
now rotate the model "angle" degrees about the local y axis

rotate the modeel "angle" degrees about the global y axis
then rotate the model "angle" degrees about the global x axis

it doesn't matter which one you do, the end orientation will be the same, I hope you see why it's rotating about the local axis now..

Tim

##### Share on other sites
yes, gimbal lock is if you rotate components separately for 90 degrees. if you do so, you basicly overlapp on axis this the other. to solve you problem, you need to have to scene nodes in your scene graph, one for x rotation and one for y rotation, and most important, the y node as a child for x node, and the object you want to rotate as child of the y node. something like that is when you make rotation within Ogre3d engine. hope it helps

##### Share on other sites
what the heck are you talking about? this is a simple order of operations problem..

##### Share on other sites
thank u very much timw....
u really explained something i never noticed. u r completely right about that misconception i had about the local and global axses i was doing the rotation around. now, u made me really feel bad though :( i though that i made some progress in making one axis rotation work fine :p
any ways, do u recommend any other solution to achieve a full global-realted rotation instead of a local one? like the object rotation that is in any 3D modeling application like 3D max when u rotate the object to view it from all directions??

##### Share on other sites
Quote:
 this is a simple order of operations problem..
Are you sure about that? I was under the impression that he wants to be able to rotate his object about any global axis, regardless of the object's current orientation. I'm not clear on how you would do this with Euler angles, regardless of order of operations.

@The OP: I proposed a solution to your problem in your other thread, here. Is there some reason that won't work for you? Or is it simply an implementation problem?

This is all assuming that I understand what the OP wants to do. But I could be misinterpreting...

##### Share on other sites

well, u r not misinterpretting at all jyk. my problem is exactly what u realise: rotating my object around two arbitrary global axes sequencially, not simultaneously.

after ur last proposal in my previous thread, i took off surfing for a way to get the current matrix in OpenGL to be able to extract the current view of it to multiply it with a proper rot matrix frame by frame. but i couldnt find any.
i encountered something that cought my attention which was the SLERP thingy. i thought it could solve my prob, but then i found out that it is too much far from what i am trying to do. and i had the misconception of whether i am facing a Gimbal Lock or not !!!

however, timw's point is 100% accurate in my case. if i read the matrices from left to right i get different result for the global axis around which the rotation takes place than when reading from right to left.

what i am trying to chieve is exactly a viewing tool just like that in any 3D modeling app, Maya, 3D Max, 3D Exploration... when u move an object in the -x then +y directions, when the orientation of the object is lets say towards the screen -z, the object rotates to the left-upper side. then if i rotate the object in the -x direction, it should flip over around the global x, not rotate around its x as it is happening to me.

all this hassle is blending me in a blender that i cant stop untill now.
can any body unplug it :p

##### Share on other sites
Hey

Quote:
 this is a simple order of operations problem..

Order of operation is an issue whether you use matrices or quaternions.

Quote:
 Are you sure about that? I was under the impression that he wants to be able to rotate his object about any global axis, regardless of the object's current orientation. I'm not clear on how you would do this with Euler angles, regardless of order of operations.

I don't see the problem here.
If we are talking about global main axes (XYZ) you simply construct the rotation matrix and multiply it with the current matrix.
If its ANY global axes, well, construct quaternion (axis angle) convert it to matrix and multiply it with the current matrix.
The fact that you may end up with gimbal lock is totally different issue.

Quote:
 @The OP: I proposed a solution to your problem in your other thread, here. Is there some reason that won't work for you? Or is it simply an implementation problem?This is all assuming that I understand what the OP wants to do. But I could be misinterpreting...

Ok jyk, never mind what I just said :) you've already talked about it.

People are generally confused about the gimbal lock and the ability of quaternions to avoid it.
Just because something looks like Gimbal lock it does not necessarily mean it is one.
If you start mixing global axes and local axes, quaternion math and matrix math
you can end up with more problems than just gimbal lock.

Quaternion is NOT a gimbal lock filter.
Lets say you have matrix M that "suffers" from gimbal lock. Converting M to quaternion DOES NOT remove the gimbal lock. You just have a different representations of the same problem.

Quote:
 i am trying to use Quaternions to rotate an object around the global X axis then around the global Y axis. i am converting the Quaternion to a matrix form to multiply it with the current matrix

This is the classic problem.
You convert quaternion to matrix and then you multiply it with another matrix.
This matrix multiplication can give you gimbal lock.
You've broken the "quaternion flow" and introduced flawed matrix operation.

I'll tell you what I usually do:

If I know that I may end up with gimbal, I create the Object and
unit quaternion at the same time. This unit quaternion is my initial
orientation representation. From this point I only do quaternion math.
At no point do I extract matrix just to multiply it with another one.
The only reason to extract matrix is to use it on your Object.
Once this matrix is applied it's discarded, and I am back to
quaternions where I left off.

1. Create object
2. Create unit quaternion Q
3. Create new qauternins Q1, Q2, Q3... representing desired rotations
4. Multiply two qauternions Q=Q*Q1*Q2*Q3...
6. If needed, extract matrix M from Q and use it on your Object
5. Go back to step 3

V

##### Share on other sites
suppose you want to do global opperations you simply do the orders in reverse.
what I usually do is when reading in the mouse movements, I construct a rotation(axis angle form) with vector defined by (dx, dy) with a magnatude of whatever you want to define, use this delta matrix(or quaternion) to rotate the object around the global x or y axis note the way I defined it doestn' give room for z axis rotation, but it does't detract it's usefullness, I gonna post a like to a program I wrote in school, tell me if you think it's object movement code is reasonable. To rotate with global axis, you'd do something like this

//initialize
orientation = initial orientation/scale

deltaQuaternion = quaternion(dx, dy,0, angle);

//draw
orientation = deltaQuaternion*orientation;

//convert to matrix and draw with it
//draw ...

in the program I'm linking to you, if you press space it gives you two teapot to play with, move them to different orientation and press i to see quaternion interpolation from the two orientation.

here is the progarm
http://ieng9.ucsd.edu/~twernett/Quaternion.exe

if it doesn't work, it's because you need the glut dll files
http://www.xmission.com/~nate/glut.html

try it and see if it has the kinda movement you want, if it does I'll post up source if you request it.

**Quote**
Are you sure about that? I was under the impression that he wants to be able to rotate his object about any global axis, regardless of the object's current orientation. I'm not clear on how you would do this with Euler angles, regardless of order of operations.
**

you can do rotations around the global axis using eular angles, why not? you just have to read them in the oposite manner, this is bottom to up for open gl.

**Quote**
just because something looks like gimble lock doesn't mean it is
**

you're right, but this problem doens't look like gimble lock at all, you'll notice gimble lock, when your camera get's "stuck" into an orientation that isn't easily escapable. just rotation in a manner not expected is not gimble lock. of course these problems could also be caused via the view vector matching exactly with the up vector in your camera construction.

btw how do you quote?

Tim

[Edited by - timw on September 1, 2005 11:57:38 AM]

##### Share on other sites
Hey

For camrea controll look at:
Quaternion Camera Control
(Full source code in C)

For quoting:
[quote]Include text between quotes[/quote]

V

##### Share on other sites
I should note the main reasons quaternion camera dont suffer from this problem is that there is no prefered up direction and thus your prefered up direction and view direction can never align, like I said above. but for your problem, that is just rotation of objects, I think you could do without quaternion, unless you want to do interpolation b/w orientations, in which case it works nicely. but you're not doing that as far as I can see.

thanks for the tip Vuli..

Tim

##### Share on other sites
jyk

Quote:
 Are you sure about that? I was under the impression that he wants to be able to rotate his object about any global axis, regardless of the object's current orientation. I'm not clear on how you would do this with Euler angles, regardless of order of operations.

you can use Matrix to rotate this object about the axis, I know it's a mindscrew these gimble lock, but here is why.

suppose we implimented this by keeing track of 3 rotation variables. one about the x,y,z. and if we rotated the object about the y we'd increase or decrease our y rotation value, and the same for x, and z. We then make the orientation matrix by concatining the rotations M = X*Y*Z or whatver order float you boat.

ok so with this system we DO have gimble lock. because it's relying on interpolation of the x, y, z values.

However, if we implimeted it slightly differently like I did above, replace quartinion with matrix and vola, it will work. in this system we aren't relying on interpolated x, y, z values, rather we are just taking the last orientation and concatenating another rotation to produce another orientation. in the system above we only ever have 3 rotations to represent our orientation, in this system, we have an arbitrary amount, in fact every time we move it we add another rotation, so our matrix is actually a concatination of MANY rotations, not just 3. at this point we at least realize that the two aren't mathematically equivilant. here is the method above slightly modifed for clarity.

//initialize
orientationMatrix = InitialOrientation;

//input
if(rotation == x)
rotationMatrix = EulerX(smallangle);
else if(rotation == y)
rotationMatrix = EulerY(smallangle);
else
rotationMatrix = EulerZ(smallangle);

//draw
orientationMatrix = rotationMatrix*orientationMatrix;
//plot points.....
...
//reset rotation matrix till next input(so it doesn't keep rotating)
rotationMatrix = Identity;

I'm hoping you see now how we can use euler angles to solve this, we just don't keep a running interpolation of our orientation. in this respect the method here and the method I showed in the previous post are equiviliant(except of coures, one uses matrix and another uses quaternion) gimble lock is not a dificency of euler angles, just how people use them, it's people deficency, lol.

Quote:
 This is the classic problem.You convert quaternion to matrix and then you multiply it with another matrix.This matrix multiplication can give you gimbal lock.You've broken the "quaternion flow" and introduced flawed matrix operation.

umm not really suppose I have two quaternions one represents a rotation of 90 degrees about the x axis, and another represents a rotation of 45 degrees about the y. call these q1 and q2 respectivley. and suppose we have two matrix that represent the same thing. and we do this

M = M1*M2
q = q1*q2;

if I converted this quaternion to a matrix that matrix would be M. if I converted q1 to a matrix that matrix would be M1, if I converted q2 to a matrix that matrix would be M2. a quaternion is just an orientation, so is a orthonormal matrix, so is an axis angle. these are all equivlient here, you can make camara systems using matrix multiplicaiton that are not prone to gimble lock(using the system above with the order of operations reversed), the only problem is after constantly concatinating the matrix with others numerical drift will tend to "un-orthonormilize" the matrix. if you're familiar how to transform points DIRECTLY in quaternion form, you can easily see why it's so.

Tim

[Edited by - timw on September 1, 2005 2:32:31 PM]

##### Share on other sites
@tim: I don't think we're in disagreement about anything. I had thought that the OP was constructing his matrix/quaternion from scratch each frame, in which case changing the order of operations wouldn't solve his problem. But now that I re-read his original post, I'm actually not sure what he's doing...

##### Share on other sites
lol come to think of it after re-reading the post Im not sure either, heh. care to enlighten us abolfoooud?

##### Share on other sites
From your description, it appears that your problem is not gimbal lock (as dmatter noted, gimbal lock occurs at 90 degrees and when using 3 angles).

I believe the solution to your problem is to rotate around Y first (turn right or left) and then rotate around the local X (pitch up or down). So, just reverse the order of your rotations -- Y first, then X.

Do you really want to rotate around the global X axis? What if you are facing it? If you want to rotate around a global axis, you can take the axis and do opposite of the rotations you've made so far to it. Then rotate around that. Your code would look something like this (but again, I don't think this is what you really want):
    // the hidden global matrix, not implemented by myself    CurrentMatrix();    // Quaternion-to-Matrix Rotatation around X    MyQuatRotate(X, angle);    globalY = RotateVectorAroundAxis( Y, X, -angle );    // Quaternion-to-Matrix Rotatation around Y    MyQuatRotate(globalY, angle);

##### Share on other sites
abolfooud2 the saver is back :p
i fell down laughing after reading ur posts guys.
i appologise if i coused any misunderstanding.

the functionality i am looking for is exactly like the one in timw's teapot example. that is what i am after. i want to do the same effect as rotating the teapot around the global axes not its local ones.
so would u mind timw to post pieces of the code in a new post or to add to ur kindness and give it to me :D ?

my initial problem was with the quaternion-to-matrix conversion. i did
construct the two quaternions adn directly converted them to matrices which made the quaternions' benifit inapplicable, since i did the converion directly before multiolying them. then, i perfomed the multiplication of the covnerted matrices using glMultMatrixf(); so, i encountered at the end to the same problem as rotating using glRotate*().

ur posts guys showed me where i was stuck and i was trying to solve my prob out until i sow timw's example where i started crying :'( that is exactly what i was trying to do :(

thanx for help any ways
abolfoooud

##### Share on other sites
Yello

Quote:
Original post by timw
jyk

Quote:
 This is the classic problem.You convert quaternion to matrix and then you multiply it with another matrix.This matrix multiplication can give you gimbal lock.You've broken the "quaternion flow" and introduced flawed matrix operation.

umm not really suppose I have two quaternions one represents a rotation of 90 degrees about the x axis, and another represents a rotation of 45 degrees about the y. call these q1 and q2 respectivley. and suppose we have two matrix that represent the same thing. and we do this

M = M1*M2
q = q1*q2;

if I converted this quaternion to a matrix that matrix would be M. if I converted q1 to a matrix that matrix would be M1, if I converted q2 to a matrix that matrix would be M2. a quaternion is just an orientation, so is a orthonormal matrix, so is an axis angle. these are all equivlient here, you can make camara systems using matrix multiplicaiton that are not prone to gimble lock(using the system above with the order of operations reversed), the only problem is after constantly concatinating the matrix with others numerical drift will tend to "un-orthonormilize" the matrix. if you're familiar how to transform points DIRECTLY in quaternion form, you can easily see why it's so.

Tim

I may have been a little to general, let me rephrase:

You've broken the "quaternion flow" and introduced POSSIBLY flawed matrix operation.

I was not trying to say that matrix operations are direct road to gimbal. Quaternions are just MORE elegant way of making sure gimbal doesn’t bite your ass :).

Quote:
 ...gimble lock is not a deficiency of Euler angles, just how people use them, it's people deficiency...

umm not really ;)
The Euler angles are deficient since they evaluate each axis independently in a set order, which causes the problem. So, people are really deficient in solving the Euler angle deficiency, rather then just being deficient themselves :)

BTW we are solving the problem abolfoooud2 does not have :D

V

##### Share on other sites
Quote:
 umm not really ;)The Euler angles are deficient since they evaluate each axis independently in a set order, which causes the problem. So, people are really deficient in solving the Euler angle deficiency, rather then just being deficient themselves :)

I agree with you, I think I considering a different problem, just using the wrong words.

Quote:
 I was not trying to say that matrix operations are direct road to gimbal. Quaternions are just MORE elegant way of making sure gimbal doesn’t bite your ass :).

I agree with you again, but you can still build a camera with gimble lock if you only use ONLY quaternion opertions. for example, if you still used euler angles in a quaternion representation, which to be honest, it seems he may be doing in the first post, not sure tho.

Tim

##### Share on other sites
Quote:
 so would u mind timw to post pieces of the code in a new post or to add to ur kindness and give it to me :D ?

here is the source, the files your interested in is QuaternionTeapots.cpp. there is no documentation, I wrote a little header to point you to the right funcitons, I think it's small and simple enough to figure out..

this is vstudio 6.0 project file, you'll need to have the proper glut headers to compile tho....

http://ieng9.ucsd.edu/~twernett/QuaternionTeapots.rar

hope it helps,

Tim

btw the math library is from a graphics teacher of mine, look at his comments for more information. I highly recomend his book btw, it covers this kinda stuff extensively, and has a VERY thuro treatment of curves and surfaces, (no subdivision tho).

##### Share on other sites
thank u tim for the code
i have solved the problem i am facing after understanding ur code.
the solution was sooooo simple actually. but, it was my misunderstanding of how to find the orientation of the object that sumbled me over.
there is a single bit of your code that i couldnt find an explanation of using it.
when u defined fNorm and fTheta as:

fNorm= fTheta = sqrt(iDx*iDx + iDy*iDy);
fTheta *= DTHETA; // where DTHETA = 0.012

and then u used them to define the rot quat as:

dQ.Set(iDy*fSin/fNorm, iDx*fSin/fNorm, 0, cos(fTheta));
//where iDx and iDy are the current orientations of the mouse

my questions are:
1) how did u calculate theta by using sqrt(iDx*iDx + iDy*iDy)? what is this procedure of finding the angle called?
2) what is iDy*fSin/fNorm for? is it the fields conversion needed when convrting a quaternion to matrix? and if so, why do u need to devide by fNorm?
3) what is fNorm? is it a normal? what normal? how did u find it using the length calculation in the sqrt? i know that u have to use cros prod to find norms.

thanx again mate
and thanx for all the guys out thr who participated in this post

##### Share on other sites
it's been a while since I wrote that, so bear with me...

Quote:
 1) how did u calculate theta by using sqrt(iDx*iDx + iDy*iDy)? what is this procedure of finding the angle called?

I don't think this has a name, you see there are many ways to solve this problem here is what I'm trying to accomplish here. basically our mouse funciton gets called and we calculate the deltaX, and deltaY. we want to rotate the object around the global X axis in proportion to deltaY, and we want to rotate the object about the Y axis in proportion to deltaX. now one way to do this, is to build 2 seperate rotations rotY, rotX. each of them with there rotation angles proportional to delta's given by the mouse. then multiply them together to obtain the final rotation, for example.

Quaternion qY, qX, qRot;(initialized to identity quaternion or somthing)

angleX = ((float)iDy)*DELTA; //you need to multiply by a small constant to scal the rotation, it's just to large otherwise.

angleY = ((float)iDx)*DELTA;

qX.set(1*sin(angleX), 0*sin(angleX), 0*sin(angleX), cos(angleX));
qY.set(0*sin(angleY), 1*sin(angleY), 0*sin(angleY), cos(angleY));

//set the final rotation
qRot = qX*qY;

ok, now the way I do it is slightly more efficent. instead of building two seperate rotations with angles proportional to iDx, and iDy I build a single rotation. the axis I rotate around is the axis (iDy, iDx, 0). this vector is closely related to the hat function, the hat vector is the 2d vector orthognal to the vector being hatted. another way to put it, is I rotate the object around the vector that is perpendicular to the line formued by the start and end points of the mouse's displacement.

...
HAT((x,y)) = (-y,x) because (x,y) DOT HAT((x,y)) = 0, thus the vector orthogonal.
...

the reason I didn't use a negative component in my fromulation. is strictly because of signess, my angles are strictly positive because of my use of the sqrt() otherwise I'd need to take sign in account, for example my angle is positive if the mouse goes right to left OR left to right, obviously those rotate the object in oposite directions, instead of using the angle to take care of this fact, I just use dx, which is positive left to right and negative right to left(hope that makes sense).

anyway, to calculate the angle(the magnitude of the rotation) I simply rotate the object in proportion to the distance travled by the mouse, that is to say
sqrt(dx^2 + dy^2) it's probably more efficent to use the distance squared, but that will give you uneaven rotations, on second thought, don't use the distance squared lol.

Quote:
 2) what is iDy*fSin/fNorm for? is it the fields conversion needed when convrting a quaternion to matrix? and if so, why do u need to devide by fNorm?

no, this has nothing to do with a matrix, in my program, everything is kept as a quaternion, until the final rendering stage.

recal the definiton of a quaternion:

q = (a*sin(T), b*sin(T), c*sin(T), cos(T))

where (a,b,c) is the vector to rotate around and is NORMALIZED.

IFF the vector a,b,c is normalized, then the resulting quaternion x,y,z,w is normalized as well.

my division by the norm of of the direction vector (dy, dx, 0) is used to normalize it.

recal that
fNorm = fTheta = sqrt(iDx*iDx + iDy*iDy);

(although, strictly speaking, it is not necessary that a quaternion be normalized, the magnitude of quaternion DOESNT effect it's rotation, however it does matter for the matrix conversion, which relies on the fact that the quaternion is normalized, but you could formulate it differently if you wanted, it also matters that all your quaternions have the same magnitude when you're applying spherical linear interpolation, for simplicity sake we make the magnitude 1, this is the standard way of doing it)

Quote:
 3) what is fNorm? is it a normal? what normal? how did u find it using the length calculation in the sqrt? i know that u have to use cros prod to find norms.

you've probably figured this one out by now, lol.

fNorm is the norm(ie magnitude) of the rotation vector (dy, dx, 0) and is used to normalize the rotation vector.

Tim

##### Share on other sites
GOT IT GOT IT...
the naming of the fNorm caused me some misconception, but now i understand what it refers to. and also i got what fTheta = sqr(...) means. i didnt think of it the way u did by using one quat instead of two where u need to have the distance between ur prev and curr x and y locations.
i have my prob solved now.
thanx

Abolfoooud

## Create an account

Register a new account

• ## Partner Spotlight

• ### Forum Statistics

• Total Topics
627664
• Total Posts
2978522

• 10
• 10
• 12
• 22
• 13