Rotating a Cube using Quaternions in PyOpenGL

3.9k views Asked by At

I've recently taken an interest in graphics programming and I'm trying to start out by making a simple cube using PyOpenGL and PyGame. I've managed to get it rotating but I can't get consecutive rotations working correctly.

I have a simple cube in space and I want to rotate it on global x and y axes using arrow keys (and eventually with a mouse). However, all rotations I've tried to apply to the cube rotate it on local x and y axes with respect to the orientation of the cube rather than a global set of axes with respect to the viewer's perspective.

The first thing I tried was using glRotatef but that of course resulted in local rotations. After looking around, I found a lot of people had similar problems not many answers provided much information besides "try this" or "use quaternions", so I tried out quaternions. I now understand (for the most part) how quaternions work, but for some reason my use of them still results in the same problem so I think I'm probably missing a step or two somewhere.

Here is the code I'm using:

import numpy
from math import *
from quat import *

import pygame
from pygame.locals import *

from OpenGL.GL import *
from OpenGL.GLU import *

axis_points = (
    (-6.0, 0.0, 0.0),
    ( 6.0, 0.0, 0.0),
    ( 0.0,-6.0, 0.0),
    ( 0.0, 6.0, 0.0),
    ( 0.0, 0.0,-6.0),
    ( 0.0, 0.0, 6.0)
    )

axes = (
    (0,1),
    (2,3),
    (4,5)
    )


verticies = (
    (-3.0,-3.0, 3.0),
    (-3.0, 3.0, 3.0),
    ( 3.0, 3.0, 3.0),
    ( 3.0,-3.0, 3.0),
    (-3.0,-3.0,-3.0),
    (-3.0, 3.0,-3.0),
    ( 3.0, 3.0,-3.0),
    ( 3.0,-3.0,-3.0),
    )

edges = (
    (0,1),
    (0,3),
    (0,4),
    (2,1),
    (2,3),
    (2,6),
    (5,1),
    (5,4),
    (5,6),
    (7,3),
    (7,4),
    (7,6)
    )

surfaces = (
    (0,1,2,3),
    (3,2,6,7),
    (7,6,5,4),
    (4,5,1,0),
    (1,5,6,2),
    (4,0,3,7),
    )

colors = (
    (0.769,0.118,0.227), # Red
    (  0.0,0.318,0.729), # Blue
    (  1.0,0.345,  0.0), # Orange
    (  0.0, 0.62,0.376), # Green
    (  1.0,  1.0,  1.0), # White
    (  1.0,0.835,  0.0), # Yellow
    )


def Axis():
    glBegin(GL_LINES)
    glColor3f(1,1,1)
    for axis in axes:
        for point in axis:
            glVertex3fv(axis_points[point])
    glEnd()

def Cube():
    glBegin(GL_QUADS)
    for color,surface in zip(colors,surfaces):
        glColor3fv(color)
        for vertex in surface:
            glVertex3fv(verticies[vertex])
    glEnd()

    glBegin(GL_LINES)
    glColor3fv((0,0,0))
    for edge in edges:
        for vertex in edge:
            glVertex3fv(verticies[vertex])
    glEnd()

def main():
    pygame.init()
    display = (1800,1600)
    pygame.display.set_mode(display, DOUBLEBUF|OPENGL)

    # Using depth test to make sure closer colors are shown over further ones
    glEnable(GL_DEPTH_TEST)
    glDepthFunc(GL_LESS)

    # Default view
    gluPerspective(45, (display[0]/display[1]), 0.05, 50.0)
    glTranslatef(0.0,0.0,-25)


    inc_x = 0
    inc_y = 0

    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                quit()

            if event.type == pygame.KEYDOWN:
                print "Modelview Matrix:"
                print glGetFloatv(GL_MODELVIEW_MATRIX)

                #Rotating about the x axis
                if event.key == pygame.K_UP:
                    inc_x =  pi/100
                if event.key == pygame.K_DOWN:
                    inc_x = -pi/100

                # Rotating about the y axis
                if event.key == pygame.K_LEFT:
                    inc_y =  pi/100
                if event.key == pygame.K_RIGHT:
                    inc_y = -pi/100

                # Reset to default view
                if event.key == pygame.K_SPACE:
                    glLoadIdentity()
                    gluPerspective(45, (display[0]/display[1]), 0.05, 50.0)
                    glTranslatef(0.0,0.0,-25)

            if event.type == pygame.KEYUP:
                # Stoping rotation
                if event.key == pygame.K_UP or event.key == pygame.K_DOWN:
                    inc_x = 0.0
                if event.key == pygame.K_LEFT or event.key == pygame.K_RIGHT:
                    inc_y = 0.0

        rot_x = normalize(axisangle_to_q((1.0,0.0,0.0), inc_x))
        rot_y = normalize(axisangle_to_q((0.0,1.0,0.0), inc_y))

        tot_rot = q_to_mat4(q_mult(rot_x,rot_y))
        glMultMatrixf(tot_rot)


        glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
        Cube()
        Axis()
        pygame.display.flip()
        pygame.time.wait(10)

main()

and

from math import *
import numpy

def normalize(v, tolerance=0.00001):
    mag2 = sum(n * n for n in v)
    if abs(mag2 - 1.0) > tolerance:
        mag = sqrt(mag2)
        v = tuple(n / mag for n in v)
    return v

def q_mult(q1, q2):
    w1, x1, y1, z1 = q1
    w2, x2, y2, z2 = q2
    w = w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2
    x = w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2
    y = w1 * y2 + y1 * w2 + z1 * x2 - x1 * z2
    z = w1 * z2 + z1 * w2 + x1 * y2 - y1 * x2
    return w, x, y, z

def q_conjugate(q):
    w, x, y, z = q
    return (w, -x, -y, -z)

def qv_mult(q1, v1):
    q2 = (0.0,) + v1
    return q_mult(q_mult(q1, q2), q_conjugate(q1))[1:]

def axisangle_to_q(v, theta):
    v = normalize(v)
    x, y, z = v
    theta /= 2
    w = cos(theta)
    x = x * sin(theta)
    y = y * sin(theta)
    z = z * sin(theta)
    return w, x, y, z

def q_to_axisangle(q):
    w, v = q[0], q[1:]
    theta = acos(w) * 2.0
    return normalize(v), theta

def q_to_mat4(q):
    w, x, y, z = q
    return numpy.array(
        [[1 - 2*y*y - 2*z*z, 2*x*y - 2*z*w, 2*x*z + 2*y*w, 0],
        [2*x*y + 2*z*w, 1 - 2*x*x - 2*z*z, 2*y*z - 2*x*w, 0],
        [2*x*z - 2*y*w, 2*y*z + 2*x*w, 1 - 2*x*x - 2*y*y, 0],
        [0, 0, 0, 1] ],'f')

I found the code for the quaterion methods in someone's answer to a question on stack overflow about implementing quaternions in python but I can't find the link to it.

The logic I'm using is based off of another stack overflow question as well (opengl matrix rotation quaternions). I'm having pretty much the exact same problem as the person that asked that question. However, I don't quite understand how his solution is different from his initial attempt with quaternions. This may be due to not being fluent in C/C++.

1

There are 1 answers

0
ricey On

So it turns out there were two main errors that caused the rotation to work incorrectly: accumulation of the rotation and the matrix mode used.

The way rotation was accumulated before was by calculating the quaternion for the current frame, converting it into a matrix, and then using glMultMatrixf to apply and "accumulate" the rotation. The way it is now being done is a quaternion accumulator is set before the main loop and each rotation is multiplied to the accumulator. The accumulator is then converted to a matrix and glLoadMatrix is used to set the current matrix to the accumulated rotation. While I an not 100% sure why this solved my problem, my best guess is that it has something to do with the order in which glMultMatrix multiplies the transformation vs. just keeping track of the overall rotation in a quaternion. I am also not entirely confident this is the most efficient solution.

The second problem I encountered after getting rotation working was that my cube was being warped into a frustum. This was due to using the incorrect matrix modes when setting gluPerspective and loading the rotation matrix.

The following is the working code I have for reference.

import numpy
from math import *
from quat import *

import pygame
from pygame.locals import *

from OpenGL.GL import *
from OpenGL.GLU import *


axis_verts = (
    (-7.5, 0.0, 0.0),
    ( 7.5, 0.0, 0.0),
    ( 0.0,-7.5, 0.0),
    ( 0.0, 7.5, 0.0),
    ( 0.0, 0.0,-7.5),
    ( 0.0, 0.0, 7.5)
    )

axes = (
    (0,1),
    (2,3),
    (4,5)
    )

axis_colors = (
    (1.0,0.0,0.0), # Red
    (0.0,1.0,0.0), # Green
    (0.0,0.0,1.0)  # Blue
    )


'''
       5____________6
       /           /|
      /           / |
    1/__________2/  |
    |           |   |
    |           |   |
    |           |   7
    |           |  /
    |           | /
    0___________3/
'''

cube_verts = (
    (-3.0,-3.0, 3.0),
    (-3.0, 3.0, 3.0),
    ( 3.0, 3.0, 3.0),
    ( 3.0,-3.0, 3.0),
    (-3.0,-3.0,-3.0),
    (-3.0, 3.0,-3.0),
    ( 3.0, 3.0,-3.0),
    ( 3.0,-3.0,-3.0)
    )

cube_edges = (
    (0,1),
    (0,3),
    (0,4),
    (2,1),
    (2,3),
    (2,6),
    (5,1),
    (5,4),
    (5,6),
    (7,3),
    (7,4),
    (7,6)
    )

cube_surfaces = (
    (0,1,2,3), # Front
    (3,2,6,7), # Right
    (7,6,5,4), # Left
    (4,5,1,0), # Back
    (1,5,6,2), # Top
    (4,0,3,7)  # Bottom
    )

cube_colors = (
    (0.769,0.118,0.227), # Red
    (  0.0,0.318,0.729), # Blue
    (  1.0,0.345,  0.0), # Orange
    (  0.0, 0.62,0.376), # Green
    (  1.0,  1.0,  1.0), # White
    (  1.0,0.835,  0.0)  # Yellow
    )


def Axis():
    glBegin(GL_LINES)
    for color,axis in zip(axis_colors,axes):
        glColor3fv(color)
        for point in axis:
            glVertex3fv(axis_verts[point])
    glEnd()

def Cube():
    glBegin(GL_QUADS)
    for color,surface in zip(cube_colors,cube_surfaces):
        glColor3fv(color)
        for vertex in surface:
            glVertex3fv(cube_verts[vertex])
    glEnd()

    glBegin(GL_LINES)
    glColor3fv((0.0,0.0,0.0))
    for edge in cube_edges:
        for vertex in edge:
            glVertex3fv(cube_verts[vertex])
    glEnd()

def main():
    pygame.init()
    display = (1800,1718)
    pygame.display.set_mode(display, DOUBLEBUF|OPENGL)

    # Using depth test to make sure closer colors are shown over further ones
    glEnable(GL_DEPTH_TEST)
    glDepthFunc(GL_LESS)

    # Default view
    glMatrixMode(GL_PROJECTION)
    gluPerspective(45, (display[0]/display[1]), 0.5, 40)
    glTranslatef(0.0,0.0,-17.5)


    inc_x = 0
    inc_y = 0
    accum = (1,0,0,0)

    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                quit()

            if event.type == pygame.KEYDOWN:
                #Rotating about the x axis
                if event.key == pygame.K_UP:
                    inc_x =  pi/100
                if event.key == pygame.K_DOWN:
                    inc_x = -pi/100

                # Rotating about the y axis
                if event.key == pygame.K_LEFT:
                    inc_y =  pi/100
                if event.key == pygame.K_RIGHT:
                    inc_y = -pi/100

                # Reset to default view
                if event.key == pygame.K_SPACE:
                    accum = (1,0,0,0)

            if event.type == pygame.KEYUP:
                # Stoping rotation
                if event.key == pygame.K_UP or event.key == pygame.K_DOWN:
                    inc_x = 0.0
                if event.key == pygame.K_LEFT or event.key == pygame.K_RIGHT:
                    inc_y = 0.0

        rot_x = normalize(axisangle_to_q((1.0,0.0,0.0), inc_x))
        rot_y = normalize(axisangle_to_q((0.0,1.0,0.0), inc_y))
        accum = q_mult(accum,rot_x)
        accum = q_mult(accum,rot_y)

        glMatrixMode(GL_MODELVIEW)
        glLoadMatrixf(q_to_mat4(accum))

        glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
        Cube()
        Axis()
        pygame.display.flip()
        pygame.time.wait(10)

main()