OpenGL 2.1 Gimbal lock

Started by
8 comments, last by Hdjo 8 years, 9 months ago

Hi everyone; I have this project for school where i need to create a software that manipulates 3D objects , i'm using OpenGL 2.1 along side with SDL 1.2.

I'm now trying to implement some kind of rotation on the objects , this way : The user selects the needed object , once it's done , he can access the rotation mode by hitting "R" , then he hits X ,Y or Z to select the desired rotation axis; at first i went with something like this :


glrotated(angles.x,1,0,0);
glrotated(angles.y,0,1,0);
glrotated(angles.z,0,0,1);
//Drawing my object

but of course it didn't work; the first rotations worked ok , but then it started to turn around an invisible axis every time ( i think it is the axis of the universe , the global one). Let me show you the video i have taken , to illustrate what i'm saying :

.

after looking on the net for a while , i found that quaternion could solve my problem ... Here's what my code looks like now :

The method that displays my teapot to the screen :


void Teapot::PrintObj()
{
        glLoadName(_NameStack);
        glPushMatrix();
        Translated(axeX,axeY,axeZ);

        RotatedX(angles.x);
        RotatedY(angles.y);
        RotatedZ(angles.z);
    
        glScaled(ObjSize.x,ObjSize.y,ObjSize.z);
glColor3ub(_Color.red,_Color.green,_Color.blue);
glutSolidTeapot(2);

if(_Selected){WiredTeapot();Guizmo();}

        glPopMatrix();
};

the angles.x , angles.y and angles.z are stored within the class ; they are modified by an other method this way :


void Scene::ProjRotation(SDL_MouseMotionEvent &event,int X,int Y,int Z)
{

int a;
int b;
int c;
    for(_compt=_OjebctScene.begin();_compt!=_OjebctScene.end();_compt++)
    {

    if ((*_compt)->IsSelected())
    {
        if(X==1 && Y==0 && Z==0)
                {
                    a=event.y % 360;
                    if(a % 5 == 0)
                    {
                    (*_compt)->setAlpha(a);
                    }

                }
       else if(Y==1 && X==0 && Z==0)
                {
                      b=event.y % 360;
                      if(b % 5 == 0)
                        {
                        (*_compt)->setBeta(b);

                        }
                }
        else if(Z==1 && Y==0 && X==0)
                {
                      c=event.y % 360;
                      if(c % 5 == 0)
                        {
                    (*_compt)->setGamma(c);

                        }
                }

    }
    }
}

the ObjScene vector contains pointers to all the objects on the scene , i first try to find the selected item ; then with an event on the mouse , i send the input to the angles.x , angles.y or angles.z;

all of this leads us now to the RotatedX() , RotatedY() and RotatedZ() methods , that are charged with handling the rotation ; here's the code of RotatedX() to show you what i have done :


void Object3D::RotatedX(double angle)
{

rotZ=vector_normalize(rotZ);//I normalize the 3 vectors : rotX , rotY and rotZ are the 3 axis , stored in the class too
rotX=vector_normalize(rotX);
rotY=vector_normalize(rotY);

rotX.x=1;
rotX.y=0;
rotX.z=0;

quaternion Q = make_rotation_quaternion_from_axis_and_angle(rotX,angle);// I create a quaternion based on the axis and angle
quaternion_fill_opengl_rotation_matrix(Q,mat);// i create the equivalent matrix to the quaternion
glm::mat4 RotationMatrix = glm::make_mat4(mat);
glm::vec4 vecRoty = glm::vec4(rotY.x,rotY.y,rotY.z,0)*RotationMatrix ;// i rotate the rotY vector , so next time the rotation won't be done around the (0,1,0) vector , but around the new position of Y;

rotY.x = vecRoty[0];
rotY.y = vecRoty[1];
rotY.z = vecRoty[2];// I update the new value of rotY so it is stored;

rotY=vector_normalize(rotY);// i think this is useless , but just to be cautious

glm::vec4 vecRotz = glm::vec4(rotZ.x,rotZ.y,rotZ.z,0)*RotationMatrix; // same thing with the Z axis , i rotate it to get a rotation around the new position of Z , not the (0,0,1) axis;
rotZ.x = vecRotz[0];
rotZ.y = vecRotz[1];
rotZ.z = vecRotz[2];// I store the new value in rotZ;

rotZ=vector_normalize(rotZ);//same , normalizing rotZ;


glMultMatrixf(mat); // then i apply the rotation to the scene to update the displaying of the teapot

}

i hope all of this is clear for you , i'm just getting started with openGL and had till yesterday no idea about quaternions , i don't know if the algorithm behind my code is wrong , but there's obviously something i am missing.

Advertisement


but of course it didn't work; the first rotations worked ok , but then it started to turn around an invisible axis every time ( i think it is the axis of the universe , the global one). Let me show you the video i have taken , to illustrate what i'm saying :
.

Thing is that a rotation around an axis ever occurs in the global space. You want to rotate around a local space axis. Before any rotation, the local axes are coincident with the global axes, so the first rotation seems to rotate around a local axis. So, if you want to rotate around an axis, compute its co-ordinates in global space and apply it.

An example: Without any prior rotation, the sequence

glrotated(angles.x,1,0,0);

glrotated(angles.y,0,1,0);
glrotated(angles.z,0,0,1);
means that the object is first rotated around the x axis which is coincident with the local x axis. The local y and z axes will remain as they are but only in object local space; in any space, especially the global one which is the important one here, those both axes will be altered. Then the rotation around the y axis is applied. At this moment the local y and z axes are no longer co-incident with their global ones. Since rotation occurs around the global axis, you see the effect shown in your video. For the 3rd rotation around the z axis the same principle holds, except that now 2 prior rotations have altered the situation.


after looking on the net for a while , i found that quaternion could solve my problem ...

Quaternions do not help against gimbal lock. Well, you are not suffering from gimbal lock either. Your problem is to not understand space transformations.

For example: You perform Euler rotation by rotating around the x axis, then rotating around the y axis, then rotating around the z axis, each time with a specific angle. For the next frame you start over, using separately accumulated angles. That's wrong for what you want. You should instead store the current orientation either as rotation matrix or as quaternion, and apply the next partial rotation on top of it. That means to incrementally change orientation without using Euler angles.

Please ask if you need more support. You know, the forum rules allow to help on homework; they just forbid to give ready-to-use solutions.

Thanks a lot for the quick replay biggrin.png .


For example: You perform Euler rotation by rotating around the x axis, then rotating around the y axis, then rotating around the z axis, each time with a specific angle. For the next frame you start over, using separately accumulated angles. That's wrong for what you want. You should instead store the current orientation either as rotation matrix or as quaternion, and apply the next partial rotation on top of it. That means to incrementally change orientation without using Euler angles.

So basically , from what i understood , every time i call one of my rotatedX() , rotatedY() or rotatedZ() method , i need to calculate the adequate matrix right ? for example if i'm the rotatedX() function , it means that the angles.x is varying (but here again i'm using angles huh ?) so i need to calculate this matrix :

|1 0 0 0|
|0 cos(a) -sin(a) 0|
|0 sin(a) cos(a) 0|
|0 0 0 1|

Where a is my angles.x in the case , then i need to just glMultMatrix() this matrix to the stack ?

EDIT : isn't that what already happens when i call the glrotated() ? i don't really understand how i can get rid of the angles by the way :/

The difference is this: ATM you have a triplet of Euler angles in use to store the current orientation. When you want to alter the orientation you change one of the Euler angles. To perform the transformation you call glRotate for each of the current angles. So you compute the current orientation matrix as

Rn := Z( cn ) * Y( bn ) * X( an )

where X, Y, and Z denote the rotation matrices with the respective axis, the triplet a, b, c denote the Euler angles, and n denotes the current time step index.

Now what is suggested is an incremental approach where you remember the orientation from the previous step and advance it accordingly to the currently selected axis and angle, like so

Rn := D( pn, dn ) * Rn-1 w/ R0 := I

where D denotes the delta rotation computed from the current axis of rotation pn and the current delta angle of rotation dn. In this approach you do not have the Euler angles anymore. Notice that the formerly used orientation matrix Rn-1 plays a role.

Notice please that the above solution of incremental rotation has an accuracy problem. The more steps are done (i.e. the higher the n) the more will the orientation matrix differ from a proper rotation transform. That is because in the matrix you have 9 variables for the rotation while only 3 are needed to express the rotation. Hence you need 6 constraints. These are known as pairwise orthogonality and unit length for each of the 3 column/row vectors in the matrix. So, from time to time you need to re-orthonormalize the matrix. There are standard methods to do so. At this point using a quaternion instead of a rotation matrix has an advantage. A quaternion has 4 variables only, which gives a higher numerical stability. Well, if you may have noticed, also a quaternion must have a problem because it has 4 variables although only 3 are needed. And yes, there is also a constraint for quaternions: It must be of unit length (so only the sub-class of so-called unit-quaternions are useful for us here). But this constraint is easier to maintain than the orthonormalization is. On the other hand, doing all the math with quaternion is not easy either. So I suggest you to first go with matrices.

i wrote this bit of code based on what you said (or based on what i understood to be more precise :P )


void Object3D::RotatedX(double angle)
{
float mat[16];
vector_normalize(rotX);//rotx is (1,0,0)
quaternion Q=make_rotation_quaternion_from_axis_and_angle(rotX,angle);// i created a quaternion from the axis and angle
quaternion_fill_opengl_rotation_matrix(Q,mat);// i converted it to a matrix , now i have the D( pn, dn ) you were talking about ( i guess ?)
glm::mat4 vecMat=glm::make_mat4(mat);//using the glm to do the maths

R = vecMat * R; // R is the recurrence matrix , i initialized it to the Id matrix in the constructor;

double dArray[16];
const float *pSource = (const float*)glm::value_ptr(R);
for (int i = 0; i < 16; ++i)
    dArray[i] = pSource[i]; // conversion of the R matrix to a double matrix[16]

glMultMatrixd(dArray);// i apply the changes;

}

well , after doing this , it is still not working , the teapot is now constantly spinning , it doesn't stop ; i don't know if the problem is still here or somewhere else though :P.

There is at least one issue:

That R is a matrix that differs from the identity matrix as soon as the first angle different from 0 is applied as transformation. So it follows the formula accordingly to my post above:

Rn := Dn * Rn-1

You apply it (as far as the shown code snippet let me follow the implementation) by multiplying it with the current matrix M on the stack:

Mn := Rn * Mn-1

Now, what happens at the next time step?

Mn+1 := Rn+1 * Mn = Dn+1 * Rn * Mn

However, it should have been something different, namely

Dn+1 * Rn

because R already accumulates all the incremental rotations (your solution accumulates them twice, in R and in M).

Therefore you either need to load the identity matrix (glLoadIdentity) before doing the glMultMatrix, so that

Mn := Rn * I

or else you use the shortcut to replace the current top matrix on the stack with your own by using glLoadMatrix, so that

Mn := Rn

BTW: The glMultMatrix and glLoadMatrix functions exist for float argument arrays, too; using them would unburden you from doing a float to double conversion.


That R is a matrix that differs from the identity matrix as soon as the first angle different from 0 is applied as transformation. So it follows the formula accordingly to my post above:

Yes , i initialized the R matrix to identity in the constructor , this way , whenever i enter that method the first time it's Id , but then its value changes following that equation.

I understand that i need to load the matrix and not multiply it because the stack automatically multiplies matrices passed by glMultMatrix() ; so i tried to glloadmatrix() the R matrix i computed , but the result is really weird ! is my code up above really correct ?

here's a screen of what it looks like :

gleaeWn.png?1

yes , that is the Teapot taking all the screen :p .

I admit that judging the rotation is a bit problematic now ;)

However, I'm a bit confused now. In the OP there is the routine Teapot::PrintObj(). It has a pair of glPushMatrix() / glPopMatrix() what made me thought that before Teapot::PrintObj() is invoked just the view matrix is on the stack. But then the new rotation as was active with post #5 should have worked well. Because you've reported that it does spin endlessly, I concluded that the push / pop was replaced. Perhaps not … my mistake.
Unfortunately I cannot see the entire relevant code. So I tell how it should look like.
a) Before Teapot::PrintObj() is invoked, only the view matrix is on the matrix stack.
b) Inside Teapot::PrintObj() and before any transformation is put on the stack, a glPushMatrix() is done.
c) The 1st transform is the glTranslate() as shown in the OP.
d) The 2nd transform is a glMultMatrix() with R (no glLoadIdentity() nor glLoadMatrix() here).
e) The 3rd transform is the glScale() as shown in the OP.
f) Then perform the rendering.
g) Before leaving Teapot::PrintObj() do glPopMatrix().
If the code follows those scheme, and the teapot still spins "without reason", then the problem lies perhaps in the input section. E.g. the Dn from above is allowed to differ from the identity matrix if and only if the user actually causes a rotation (i.e. you must not accumulate onto the angle used to compute Dn; the angle is either a delta angle when the belonging key is pressed (or whatever), or it is zero if not).

thanks for your answers , i'm getting a head ache with this , the teapot is still constantly spinning , i even changed the method to test something and i got the same result : here's the method that passes the angles from the mouse event to the angles.x , angles.y and angles.z attribute of the teapot :



void Scene::ProjRotation(SDL_MouseMotionEvent &event,int X,int Y,int Z)
{
int a;
int b;
int c;

    for(_compt=_OjebctScene.begin();_compt!=_OjebctScene.end();_compt++)
    {

    if ((*_compt)->IsSelected())
    {
        if(X==1 && Y==0 && Z==0)
                {
                    a=event.y % 360;
                    if(a % 5 == 0)
                    {
                    (*_compt)->setAlpha(a);
                    }

                }
       else if(Y==1 && X==0 && Z==0)
                {
                      b=event.y % 360;
                      if(b % 5 == 0)
                        {
                        (*_compt)->setBeta(b);

                        }
                }
        else if(Z==1 && Y==0 && X==0)
                {
                      c=event.y % 360;
                      if(c % 5 == 0)
                        {
                    (*_compt)->setGamma(c);

                        }
                }

    }
    }
}

the ObjScene attribute is a vector in a class named Scene ,that contains all the objects in the scene; i first check if the object is selected , then if it is , i select the Axis around which i want to rotate , and send the coordinates of the mouse the my rotation methods as an angle;

i'm really struggling here nothing seems to work , i tried multiple version and multiple methods but all lead to the same result : either infinit spinning , or rotation around the Global axis (as if i used 3 consecutive glrotated() ).

Hi , Finally solved the problem ; Thanks a lot for the help @Haegarr , it was all based on what you said ; i just committed few mistakes that i corrected , and that i'm going to explain if someone needs this one day :

first , the infinit spinning was due to the R matrix receiving a new value each frame (Rn := Dn * Rn-1) and being passed to the stack ; i added a boolean to control that so only the first transformation is passed (The first transformation when i choose the rotation axis)

Then , the second big mistake i made was to call the glMultMatrix() INSIDE my RotatedX , RotatedY and RotatedZ methods , which was in a way going back to the Rn := Z( cn ) * Y( bn ) * X( an ) ..

here's the final code of my teapot :


void Teapot::PrintObj()
{
        glLoadName(_NameStack);
        glPushMatrix();
        Translated(axeX,axeY,axeZ);


        RotatedX(Qy.getAngle());
        RotatedY(Qx.getAngle());
        RotatedZ(Qz.getAngle());
        glMultMatrixd(dArray);


        glScaled(ObjSize.x,ObjSize.y,ObjSize.z);

glColor3ub(_Color.red,_Color.green,_Color.blue);
glutSolidTeapot(2);

if(_Selected){WiredTeapot();Guizmo();}

        glPopMatrix();
};

And here's my final RotatedX() method (the method charged with the rotation)


void Object3D::RotatedX(double angle)
{
        Qx.FromAxis(glm::vec3(1,0,0),Qx.getAngle());
        Qx.normalise();

if(ActivateX)
{R=R*Qx.getMatrix();ActivateX=false;}
const float *pSource = (const float*)glm::value_ptr(R);
for (int i = 0; i < 16; ++i)
    dArray[i] = pSource[i];

}

ActivateX is the boolean i was talking about , it takes True when the user hits X to activate the X axis rotation .

Again , Thanks a lot for the help :D

This topic is closed to new replies.

Advertisement