Search code examples
pythonopenglpygame3dpyopengl

Rotating 3D Object In PyOpenGL Without Parent-Child Relation


I am relatively new to PyOpenGL and 3D Graphics, but I have a 3D object that I'm trying to rotate around its X and Y Axis.

I have been using glRotatef(), however if I rotate around the X axis first, the rotations around the Y axis act as if the X axis hasn't been rotated. If I rotate the Y axis first, the rotations around the X axis act as if the Y axis hasn't been rotated, sort of like the first rotation rotates both axis, but the second rotation rotates only itself (some of what I've read calls it a "parent-child" relationship or hierarchy).

I want the rotations to act as if the object is in space, where there is no up or down, and therefore no matter which direction the object is facing, it will rotate the same way on both axis. Instead of a parent-child relationship, I want a "parent-parent" relationship.

import pygame
from pygame.locals import *
from OpenGL.GL import *
from OpenGL.GLU import *
from random import uniform

# Object Rotation
angle_x = 0
angle_y = 0

stars = []
for a in range(500):
    stars.append([uniform(-5, 5), uniform(-5, 5)])


def init():
    pygame.init()
    display = (900, 700)
    pygame.display.set_mode(display, DOUBLEBUF | OPENGL)
    gluPerspective(45, (display[0] / display[1]), 0.1, 50.0)
    glEnable(GL_DEPTH_TEST)
    glClearColor(0.0, 0.0, 0.0, 1.0)
    glTranslatef(0, 0, -2)
    glMatrixMode(GL_MODELVIEW)


def draw():

    # Cube
    glPushMatrix()

    vertices = (
        (.25, .25, .25),
        (.25, -.25, .25),
        (-.25, -.25, .25),
        (-.25, .25, .25),

        (.25, .25, -.25),
        (.25, -.25, -.25),
        (-.25, -.25, -.25),
        (-.25, .25, -.25)

        )
    edges = (
        (0, 1),
        (1, 2),
        (2, 3),
        (3, 0),

        (4, 5),
        (5, 6),
        (6, 7),
        (7, 4),

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

    glColor3fv((0.0, 1.0, 0.0))
    glLineWidth(5)
    glBegin(GL_LINES)
    for edge in edges:
        for vertex in edge:
            glVertex3fv(vertices[vertex])
    glEnd()

    glPopMatrix()

    glPushMatrix()

    glRotatef(-angle_x, 1, 0, 0)
    glRotatef(-angle_y, 0, 1, 0)

    # Stars (For Reference)
    glColor3fv((1.0, 1.0, 1.0))
    glPointSize(2.0)
    glBegin(GL_POINTS)
    for c in stars:
        glVertex3f(c[0], c[1], -5)
        glVertex3f(c[0], c[1], 5)
        glVertex3f(5, c[0], c[1])
        glVertex3f(-5, c[0], c[1])
        glVertex3f(c[0], 5, c[1])
        glVertex3f(c[0], -5, c[1])
    glEnd()

    glPopMatrix()



def main():
    global angle_x, angle_y
    init()
    clock = pygame.time.Clock()

    rate_x = 0
    rate_y = 0
    while True:
        for event in pygame.event.get():
            # Check if the Player Quit
            if event.type == pygame.QUIT:
                pygame.quit()
                quit()
            # Check if the Player Pressed a Key
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_w:
                    rate_x = .5
                elif event.key == pygame.K_a:
                    rate_y = .5
                elif event.key == pygame.K_s:
                    rate_x = -.5
                elif event.key == pygame.K_d:
                    rate_y = -.5

            elif event.type == pygame.KEYUP:
                if event.key == pygame.K_w:
                    rate_x = 0
                elif event.key == pygame.K_a:
                    rate_y = 0
                elif event.key == pygame.K_s:
                    rate_x = 0
                elif event.key == pygame.K_d:
                    rate_y = 0

        angle_x += rate_x
        angle_y += rate_y
        # Clear the Screen
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

        # Draw
        draw()


        pygame.display.flip()
        clock.tick(60)


if __name__ == '__main__':
    main()

I drew "stars" in the background to visualize the space component, since I want the rotations to act as if the object is in space, meaning no matter the rotation around one axis, rotation around the other axis will always look the same. To put it very simply, if I rotate up or down, it always looks like the stars are moving the down or up (the opposite direction), and if I rotate left or right, it always looks like the stars are moving right or left (the opposite direction).

In the program, the rotation around the X axis happens first and then the Y axis, which makes the rotations around the X axis normal, but the Y axis only work normally if there have been no X axis rotations. Once there are X axis rotations, the Y axis rotations get messed up.

I've tried using glMultMatrixf(), but because order matters when multiplying two matrices, there is still this "parent-child" relationship.


Solution

  • Instead of summing the individual Euler angles for the rotations around the axes, you must multiply rotation matrices. With legacy OpenGL this is a bit tricky. But it can be solved with glPushMatrix/glPopMatrix/glLoadIdentity/glMultMatrixf and glGetFloatv(GL_MODELVIEW_MATRIX, ...):

    rotationMatrix = (GLfloat * 16)(1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1)
    while True:
        # [...]
    
        glPushMatrix()
        glLoadIdentity()
        angle_y = (keys[pygame.K_d] - keys[pygame.K_a]) * 0.5
        angle_x = (keys[pygame.K_s] - keys[pygame.K_w]) * 0.5
        glRotatef(-angle_x, 1, 0, 0)
        glRotatef(-angle_y, 0, 1, 0)
        glMultMatrixf(rotationMatrix)
        glGetFloatv(GL_MODELVIEW_MATRIX, rotationMatrix)
        glPopMatrix()
    

    Finally, you must apply the matrix to the object

    # instead of
    # glRotatef(-angle_x, 1, 0, 0)
    # glRotatef(-angle_y, 0, 1, 0)
    
    # do
    glMultMatrixf(rotationMatrix)
    

    Complete example:

    import pygame
    from pygame.locals import *
    from OpenGL.GL import *
    from OpenGL.GLU import *
    from random import uniform
    from OpenGL.GL import *
    
    stars = []
    for a in range(500):
        stars.append([uniform(-5, 5), uniform(-5, 5)])
    
    def init():
        pygame.init()
        display = (900, 700)
        pygame.display.set_mode(display, DOUBLEBUF | OPENGL)
        gluPerspective(45, (display[0] / display[1]), 0.1, 50.0)
        glEnable(GL_DEPTH_TEST)
        glClearColor(0.0, 0.0, 0.0, 1.0)
        glTranslatef(0, 0, -2)
        glMatrixMode(GL_MODELVIEW)
    
    def draw(rotationMatrix):
    
        # Cube
        glPushMatrix()
    
        vertices = (
            (.25, .25, .25),
            (.25, -.25, .25),
            (-.25, -.25, .25),
            (-.25, .25, .25),
    
            (.25, .25, -.25),
            (.25, -.25, -.25),
            (-.25, -.25, -.25),
            (-.25, .25, -.25)
    
            )
        edges = (
            (0, 1),
            (1, 2),
            (2, 3),
            (3, 0),
    
            (4, 5),
            (5, 6),
            (6, 7),
            (7, 4),
    
            (0, 4),
            (1, 5),
            (2, 6),
            (3, 7)
        )
    
        glColor3fv((0.0, 1.0, 0.0))
        glLineWidth(5)
        glBegin(GL_LINES)
        for edge in edges:
            for vertex in edge:
                glVertex3fv(vertices[vertex])
        glEnd()
        glPopMatrix()
    
        glPushMatrix()
        glMultMatrixf(rotationMatrix)
        glColor3fv((1.0, 1.0, 1.0))
        glPointSize(2.0)
        glBegin(GL_POINTS)
        for c in stars:
            glVertex3f(c[0], c[1], -5)
            glVertex3f(c[0], c[1], 5)
            glVertex3f(5, c[0], c[1])
            glVertex3f(-5, c[0], c[1])
            glVertex3f(c[0], 5, c[1])
            glVertex3f(c[0], -5, c[1])
        glEnd()
        glPopMatrix()
    
    def main():
        global angle_x, angle_y
        init()
        clock = pygame.time.Clock()
    
        rotationMatrix = (GLfloat * 16)(1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1)
        while True:
            for event in pygame.event.get():
                # Check if the Player Quit
                if event.type == pygame.QUIT:
                    pygame.quit()
                    quit()
    
            keys = pygame.key.get_pressed()
            glPushMatrix()
            glLoadIdentity()
            angle_y = (keys[pygame.K_d] - keys[pygame.K_a]) * 0.5
            angle_x = (keys[pygame.K_s] - keys[pygame.K_w]) * 0.5
            glRotatef(-angle_x, 1, 0, 0)
            glRotatef(-angle_y, 0, 1, 0)
            glMultMatrixf(rotationMatrix)
            glGetFloatv(GL_MODELVIEW_MATRIX, rotationMatrix)
            glPopMatrix()
    
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
            draw(rotationMatrix)
            pygame.display.flip()
            clock.tick(60)
    
    if __name__ == '__main__':
        main()