3

I have been trying to rotate a cube using OpenGL running in an SDL full-screen window. I managed to do this quite successfully with glRotatef(), however I experienced "gimbal lock" and other problems associated with Euler angles. Wanting to improve my program, I researched quaternions. I coded a quaternion class, following the directions on this page, and tried to use it to rotate my cube using glMultMatrixf(), however the cube gets warped when rotating around more than 1 axis with angles that aren't a multiple of 90 degrees. I checked my quaternion to matrix conversion and my quaternion multiplication code, but I can't find anything wrong.

Here is a photo of the problem: enter image description here

And here is the complete program that displayed those cubes (requires SDL and OpenGL)

//==============================================================================
#include <cmath>

#include <SDL/SDL.h>
#include <SDL/SDL_opengl.h>

namespace game_lib
{
    struct vec3
    {
        vec3() : x(0), y(0), z(0) { }
        vec3(float x, float y, float z) : x(x), y(y), z(z) { }

        vec3 normalize();
        inline float lenSqr() { return x*x + y*y + z*z; }
        float len();

        inline vec3 operator+(vec3 v) { v.x += x; v.y += y; v.z += z; return v; }
        inline vec3 operator-(vec3 v) { v.x = x - v.x; v.y = y - v.y; v.z = z - v.z; return v; }
        inline vec3 operator*(float f) { return vec3(f*x, f*y, f*z); }
        inline vec3 operator/(float f) { return vec3(x/f, y/f, z/f); }


        bool operator==(vec3 v);

        float x, y, z;

        enum faces { FRONT, BACK, LEFT, RIGHT, TOP, BOTTOM };
    };
    inline vec3 operator*(float f, vec3 v) { return v*f; }

    struct quaternion
    {
        quaternion() : w(1), x(0), y(0), z(0) { }
        quaternion(float w, float x, float y, float z) : w(w), x(x), y(y), z(z) { }
        quaternion(float angle, vec3 axis);

        quaternion normalize();
        inline float lenSqr() { return w*w + x*x + y*y + z*z; }
        float len();

        quaternion operator*(quaternion);

        float w, x, y, z;
    };

    void DrawGLCuboid(vec3 centre, vec3 dimensions, quaternion rotation, const vec3 colours[6]);
}

const int EPSILON = 0.001;

inline bool feq(float f1, float f2)
{
    const float diff = f1 - f2;
    return (diff > -EPSILON) && (diff < EPSILON);
}

//{=================== vec3 methods =================
game_lib::vec3 game_lib::vec3::normalize()
{
    const float lengthSqr = lenSqr();
    if (lengthSqr > 1-EPSILON*EPSILON and lengthSqr < 1+EPSILON*EPSILON) // Optimisation to not re-normalize a normalized vector
        return *this;
    const float length = std::sqrt(lengthSqr);
    return game_lib::vec3(x/length, y/length, z/length);
}

float game_lib::vec3::len() { return std::sqrt(lenSqr()); }

bool game_lib::vec3::operator==(vec3 v) { return feq(v.x,x) and feq(v.y, y) and feq(v.z, z); }
//}==================================================


//{================ quaternion methods ==============
game_lib::quaternion::quaternion(float angle, vec3 axis)
{
    const vec3 axisN = axis.normalize();
    const float sin_a_2 = std::sin(angle*M_PI/360);
    w = std::cos(angle*M_PI/360);
    x = axisN.x*sin_a_2;
    y = axisN.y*sin_a_2;
    z = axisN.z*sin_a_2;
}

game_lib::quaternion game_lib::quaternion::normalize()
{
    const float lengthSqr = lenSqr();
    if (lengthSqr > 1-EPSILON*EPSILON and lengthSqr < 1+EPSILON*EPSILON) // Optimisation to not re-normalize a normalized quaternion
        return *this;
    const float length = std::sqrt(lengthSqr);
    return game_lib::quaternion(w/length, x/length, y/length, z/length);
}

float game_lib::quaternion::len() { return std::sqrt(lenSqr()); }

game_lib::quaternion game_lib::quaternion::operator*(game_lib::quaternion q)
{
    return game_lib::quaternion(w*q.w - x*q.x - y*q.y - z*q.z, w*q.x + x*q.w + y*q.z - z*q.y, w*q.y - x*q.z + y*q.w + z*q.x, w*q.z + x*q.y - y*q.x + z*q.w);
}
//}==================================================


void game_lib::DrawGLCuboid(vec3 cen, vec3 dim, quaternion rot, const vec3 col[6])
{
    glPushMatrix();
    glTranslatef(cen.x, cen.y, cen.z);
    vec3 dim_2 = 1/2*dim;
    const quaternion r_norm = rot.normalize();

    // Quaternion to matrix
    const float x_x = r_norm.x*r_norm.x,   y_y = r_norm.y*r_norm.y,   z_z = r_norm.z*r_norm.z;
    const float w_x = r_norm.w*r_norm.x,   w_y = r_norm.w*r_norm.y,   w_z = r_norm.w*r_norm.z;
    const float x_y = r_norm.x*r_norm.y,   x_z = r_norm.x*r_norm.z,   y_z = r_norm.y*r_norm.z;

    GLfloat matrix[16];

    // Column 1                  // Column 2                  // Column 3                  // Column 4
    matrix[0] = 1-2*(y_y+z_z);   matrix[4] = 2*(x_y-w_z);     matrix[8] = 2*(x_z+w_y);     matrix[12] = 0;
    matrix[1] = 2*(x_y+w_z);     matrix[5] = 1-2*(x_x+z_z);   matrix[9] = 2*(y_z+w_x);     matrix[13] = 0;
    matrix[2] = 2*(x_z-w_y);     matrix[6] = 2*(y_z-w_x);     matrix[10] = 1-2*(x_x+y_y);  matrix[14] = 0;
    matrix[3] = 0;               matrix[7] = 0;               matrix[11] = 0;              matrix[15] = 1;

    /* From http://www.cprogramming.com/tutorial/3d/quaternions.html
    1-2y2-2z2   2xy-2wz     2xz+2wy     0

    2xy+2wz     1-2x2-2z2   2yz+2wx     0

    2xz-2wy     2yz-2wx     1-2x2-2y2   0

    0           0           0           1
    */

    glMultMatrixf(matrix);

    glBegin(GL_QUADS);
        int i = vec3::FRONT;
        glColor3f(col[i].x, col[i].y, col[i].z);

        glVertex3f(-1, 1, 1);
        glVertex3f(1, 1, 1);
        glVertex3f(1, -1, 1);
        glVertex3f(-1, -1, 1);


        i = vec3::BACK;
        glColor3f(col[i].x, col[i].y, col[i].z);

        glVertex3f(-1, 1, -1);
        glVertex3f(1, 1, -1);
        glVertex3f(1, -1, -1);
        glVertex3f(-1, -1, -1);


        i = vec3::LEFT;
        glColor3f(col[i].x, col[i].y, col[i].z);

        glVertex3f(-1, 1, 1);
        glVertex3f(-1, -1, 1);
        glVertex3f(-1, -1, -1);
        glVertex3f(-1, 1, -1);


        i = vec3::RIGHT;
        glColor3f(col[i].x, col[i].y, col[i].z);

        glVertex3f(1, 1, 1);
        glVertex3f(1, -1, 1);
        glVertex3f(1, -1, -1);
        glVertex3f(1, 1, -1);


        i = vec3::BOTTOM;
        glColor3f(col[i].x, col[i].y, col[i].z);

        glVertex3f(-1, -1, 1);
        glVertex3f(1, -1, 1);
        glVertex3f(1, -1, -1);
        glVertex3f(-1, -1, -1);


        i = vec3::TOP;
        glColor3f(col[i].x, col[i].y, col[i].z);

        glVertex3f(-1, 1, 1);
        glVertex3f(1, 1, 1);
        glVertex3f(1, 1, -1);
        glVertex3f(-1, 1, -1);

        glColor3f(1, 1, 1);

        // Following three quads are axes to help determine rotational correctness...
        // x-axis
        glVertex3f(-2, 0.05, 0.05);
        glVertex3f(2, 0.05, 0.05);
        glVertex3f(2, -0.05, -0.05);
        glVertex3f(-2, -0.05, -0.05);

        // y-axis
        glVertex3f(0.05, -2, 0.05);
        glVertex3f(0.05, 2, 0.05);
        glVertex3f(-0.05, 2, -0.05);
        glVertex3f(-0.05, -2, -0.05);

        // z-axis
        glVertex3f(0.05, 0.05, -2);
        glVertex3f(0.05, 0.05, 2);
        glVertex3f(-0.05, -0.05, 2);
        glVertex3f(-0.05, -0.05, -2);
    glEnd();
    glPopMatrix();
}

using namespace game_lib;

struct SDL_Surface;
union SDL_Event;

class CApp {
    private:
        bool m_running, m_init;
        SDL_Surface* m_screen;
        float depth;

    public:
        CApp();
        ~CApp();
        bool init();
        int execute();
        void cleanup();

    private:
        //void processEvent(SDL_Event* Event); // Usually I have this, but it's big and irrelevant (
        void render();
};


//==============================================================================
CApp::CApp()
: m_running(true), m_init(false), m_screen(NULL), depth(-6) { }

CApp::~CApp()
{
    if (m_init) cleanup();
}

//------------------------------------------------------------------------------

bool CApp::init()
{
    if(SDL_Init(SDL_INIT_EVERYTHING) < 0)
        return false;


    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);

    SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
    SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
    SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
    SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8);

    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16);
    SDL_GL_SetAttribute(SDL_GL_BUFFER_SIZE, 32);

    SDL_GL_SetAttribute(SDL_GL_ACCUM_RED_SIZE, 8);
    SDL_GL_SetAttribute(SDL_GL_ACCUM_GREEN_SIZE, 8);
    SDL_GL_SetAttribute(SDL_GL_ACCUM_BLUE_SIZE, 8);
    SDL_GL_SetAttribute(SDL_GL_ACCUM_ALPHA_SIZE, 8);

    SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1);
    SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 2);

    const SDL_VideoInfo* inf = SDL_GetVideoInfo();

    if((m_screen = SDL_SetVideoMode(inf->current_w, inf->current_h, 0, SDL_OPENGL | SDL_FULLSCREEN)) == NULL)
        return false;

    glClearColor(0, 0, 0, 0);

    glClearDepth(1);
    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LEQUAL);

    glViewport(0, 0, inf->current_w, inf->current_h);

    glMatrixMode(GL_PROJECTION); // Camera space
    glLoadIdentity();

    gluPerspective(45.0f, 1024.0f/600.0f, 0.1f, 100.0f);

    glEnable(GL_TEXTURE_2D);
    glMatrixMode(GL_MODELVIEW); // Model space
    glLoadIdentity();

    m_init = true;
    return true;
}

int CApp::execute()
{
    if(init() == false)
        return -1;
    SDL_Event event;
    while(m_running)
    {
        //while(SDL_PollEvent(&event))
            // process events removed to save space
        render();
        SDL_Delay(10);
    }
    cleanup();
    return 0;
}

void CApp::cleanup()
{
    if (m_screen)
    {
        SDL_FreeSurface(m_screen);
        m_screen = NULL;
    }
    SDL_Quit();
    m_init = false;
}

void CApp::render()
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glLoadIdentity();

    glTranslatef(0.0f, 0.0f, depth);

    //glRotatef(63, 0, 1, 0); // How I used to do rotation
    //glRotatef(47, 1, 0, 0);

    const vec3 c1colours[] = {vec3(1, 0, 0), vec3(0, 1, 0), vec3(1, 0.2, 1), vec3(0, 0, 1), vec3(1, 1, 0), vec3(1, 0.5, 1)};
    const vec3 c2colours[] = {vec3(1, 0, 0), vec3(0, 1, 1), vec3(1, 1, 0), vec3(0, 1, 0), vec3(1, 0.5, 0), vec3(0.5, 0, 0)};

    // New rotation method, but doesn't work...
    const quaternion c1Rotation = quaternion(63, vec3(0, 1, 0)) * quaternion(47, vec3(1, 0, 0));

    DrawGLCuboid(vec3(-2, 0, -2), vec3(2, 2, 2), c1Rotation, c1colours);
    DrawGLCuboid(vec3(2.5, 0.3, -1.2), vec3(2, 2, 2), quaternion(72, vec3(0, 0, 1)), c2colours);

    SDL_GL_SwapBuffers();
}




int main(int argc, char* argv[])
{
    CApp theApp;
    return theApp.execute();
}
steve9164
  • 426
  • 2
  • 11
  • 22
  • you do realise OpenGL comes with a (well, there is an ~official~) library for maths? It is called `glm`. – thecoshman Jul 03 '13 at 12:17
  • That's a lot of code. It looks like you're drawing the left cube with a product of two quaternions and the one on the right with just one quaternion. The left one looks messed up, but the right one looks okay. Is this the case? – GraphicsMuncher Jul 03 '13 at 20:56
  • @GraphicsMuncher: Yes. The left cube was to show how rotations don't work, the right cube (which is correctly drawn) is there to show that rotation around 1 axis at a time works. I'll include this in my question. – steve9164 Jul 04 '13 at 08:24

2 Answers2

5

As said before, you're probably best off using a library that already does the math for you.

The problem here is that you've swapped signs for w_x on matrix[6] and matrix[9].

The relevant lines should read thusly:

matrix[1] = 2*(x_y+w_z);     matrix[5] = 1-2*(x_x+z_z);   matrix[9] = 2*(y_z-w_x);     matrix[13] = 0;
matrix[2] = 2*(x_z-w_y);     matrix[6] = 2*(y_z+w_x);     matrix[10] = 1-2*(x_x+y_y);  matrix[14] = 0;
JCooper
  • 6,395
  • 1
  • 25
  • 31
  • 1
    Yes this works. I will have to tell the tutorialist [here](http://www.cprogramming.com/tutorial/3d/quaternions.html). – steve9164 Jul 08 '13 at 07:07
  • 2
    +1 for actually finding the bug and making this a good example of why you should use a library. Also, dashing use of the word *thusly*. – GraphicsMuncher Jul 08 '13 at 19:29
2
  1. Use GLM for math. Someone has already nicely done the grunt work of making a nice vector library specifically for OpenGL - it's set up to match GLSL to boot.

  2. Don't use glBegin or glVertex* or any of their friends. They've been deprecated since 3.0. Use VBOs, your GPU will thank you.

  3. Gimbal lock is completely avoidable. Rotate your axes as you rotate your object to maintain a native coordinate system (re-orthogonalizing with gram-schmidt every so often). This is much more intuitive, especially for a camera. Check out the Frenet frame for a good visual idea - scroll down for a bunch of good gifs. The X, Y, and Z columns of your rotation matrix are your right, forward, and up vectors, just as a useful tip.

GraphicsMuncher
  • 4,583
  • 4
  • 35
  • 50
  • How would I rotate my axes? I think that was what I was trying to do before. I managed to get it working for rotations around x and y axes with `glRotatef(angle, 0, 1, 0); glRotatef(angle2, std::cos(angle*M_PI/180), 0, std::sin(angle*M_PI/180));` which I came up with by drawing a few cubes on paper, but I couldn't work out equations with a z axis of rotation. Adding a z axis made it way too complex to work out empirically. – steve9164 Jul 04 '13 at 08:49
  • Also, if gimbal lock is easily avoidable with an intuitive system, why does everyone say quaternions or matrices are the only ways to solve gimbal lock? – steve9164 Jul 04 '13 at 08:56