OpenGL cube rotation using quaternions
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:
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();
}
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;
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.
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.
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.
上一篇: 如何通过glm中的quaternion旋转任何矢量?
下一篇: 使用四元数的OpenGL多维数据集旋转