pythonopenglpyopenglcoordinate-transformationarcball

PyOpenGL transforming view coordinates into object coordinates for ArcBall navigation


I am following this tutorial for arcball navigation in 3d:

https://en.wikibooks.org/wiki/OpenGL_Programming/Modern_OpenGL_Tutorial_Arcball

I managed to perform all the steps and the navigation works but I cant seem to comprehend last step in tutorial:

An extra trick is converting the rotation axis from camera coordinates to object coordinates. It's useful when the camera and object are placed differently. For instace, if you rotate the object by 90° on the Y axis ("turn its head" to the right), then perform a vertical move with your mouse, you make a rotation on the camera X axis, but it should become a rotation on the Z axis (plane barrel roll) for the object. By converting the axis in object coordinates, the rotation will respect that the user work in camera coordinates (WYSIWYG). To transform from camera to object coordinates, we take the inverse of the MV matrix (from the MVP matrix triplet).

The problem is that when i turn the model in the first step axis of rotation transform as well and they are not aligned with my "camera view". Of course I would like to keep my rotation axes always aligned with my camera view.

Can someone please give me an advice how to tackle it? In the tutorial there is a code but not much of explanation on what it is actually doing plus I only speak Python.

Thank you, Jacob

My code:

import pygame
from pygame.locals import *
from OpenGL.GL import *
from OpenGL.GLU import *
import math
import os
import numpy as np

size = 30
speed = 500
amplitude_amplificator = 80


color_table = ((1,0,0),
            (0,1,0),
            (0,0,1),
            (1,1,0),
            (1,0,1),
            (0,1,1),
            (1,0.5,0),
            (0.5,1,0),
            (0.5,1,0.5),
            (0,0.5,0)
            )


locations = ((0,-975, 0),
             (0, 975, 0),
             (-1273,-975, 0),
             (-1273, 975, 0),
             (-2482, -975, 0),
             (-2482, 975, 0),
             (-3737, -975, 0),
             (-3737, 975, 0)
             )

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

         )

amplitudes = ((3.38829249165602, 2.38305866657961, 2.52151563664636),
              (5.08487438107113, 2.36432294667884, 3.0843991148654),
              (3.44312569856563, 1.23112415468012, 1.29869765112226),
              (4.0421066637935, 1.40655294535107, 1.36083778879317),
              (3.78074337117764, 0.648255908566916, 0.752239154016233),
              (5.08887133464996, 0.607037324785205, 0.543523234321567),
              (4.49095206021647, 0.432732677308301, 2.18289872563964),
              (5.14707697114171, 0.335119576625248, 2.15666871777855)
              )

phases =   ((-146.873017352057,0,-95.316526141321),
             (-149.008372080797, 5.24886681104675, 78.3075732082314),
             (-148.241584335287, 5.54327579087787, -118.279685417256),
             (-151.844141596427, 6.48705235395368, -113.246406750217),
             (-148.14233553496, 27.9523171503408, 65.8254568277543),
             (-157.058723259828, 38.8760924034639, 85.2339573112435),
             (-153.417593784393, -120.329988461629, 16.0421535833842),
             (-156.779107376825, 83.2350395893582, 10.7592173681729)
             )

# DRAW CUBE
def Cube(po,si,co):

    POS = (
    (po[0]+si, po[1]-si, po[2]-si),
    (po[0]+si, po[1]+si, po[2]-si),
    (po[0]-si, po[1]+si, po[2]-si),
    (po[0]-si, po[1]-si, po[2]-si),
    (po[0]+si, po[1]-si, po[2]+si),
    (po[0]+si, po[1]+si, po[2]+si),
    (po[0]-si, po[1]-si, po[2]+si),
    (po[0]-si, po[1]+si, po[2]+si)
    )

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

    glBegin(GL_LINES)
    for edge in edges:
        for vertex in edge:
            glColor3f(co[0],co[1],co[2])
            glVertex3fv(POS[vertex])
    glEnd()

#DRAW ORIGINAL SHAPE IN LINES
def Line_orig(po):

    glBegin(GL_LINES)
    for edge in po:
        for vertex in edge:
            glVertex3fv(locations[vertex])
    glEnd()

#Hemisphere mapping

def map_hemisphere(x,y):
    z = math.sqrt(abs(1-math.pow(x,2)-math.pow(y,2)))
    return z

# Calculate angle of two spatial vectors

def angle_calculation(a,b):

    r = math.degrees(math.acos((np.dot(a, b))/(np.linalg.norm(a)*np.linalg.norm(b))))

    return r


def main():

    mouse_pressed = 0
    pygame.init()
    display = (1200,800)
    pygame.display.set_mode(display, DOUBLEBUF|OPENGL)


    gluPerspective(45, (display[0]/display[1]), 0.1, 30000.0)

    glTranslatef(0,0.0,-10000)

    #glRotatef(90, 1, 0, 0)


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


        glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)

        time = pygame.time.get_ticks()/1000

        norm_mouse_pos = (2*pygame.mouse.get_pos()[0]/display[0]-1,2*pygame.mouse.get_pos()[1]/display[1]-1,map_hemisphere(2*pygame.mouse.get_pos()[0]/display[0]-1,2*pygame.mouse.get_pos()[1]/display[1]-1))


        if pygame.mouse.get_pressed()[0]==1:

            if mouse_pressed == 0:

                mouse_pressed = 1

                clear = lambda: os.system('cls')
                clear()

                p1 = (norm_mouse_pos[0],norm_mouse_pos[1],map_hemisphere(norm_mouse_pos[0],norm_mouse_pos[1]))
                print(p1)

            else:

                p2 = (norm_mouse_pos[0],norm_mouse_pos[1],map_hemisphere(norm_mouse_pos[0],norm_mouse_pos[1]))
                cist = np.cross(p1, p2)
                print(angle_calculation(p1,p2))
                glRotatef( angle_calculation(p1,p2) , -cist[0] , cist[1] , cist[2] )

        else:

            mouse_pressed = 0






        # Translation of the model via keyboard handling

        keys=pygame.key.get_pressed()

        if keys[K_w]:
            glTranslatef(0, 100, 0)

        if keys[K_s]:
            glTranslatef(0, -100, 0)

        if keys[K_a]:
            glTranslatef(-100, 0, 0)

        if keys[K_d]:
            glTranslatef(100, 0, 0)

        # Drawing the Cubes at Nodes Loactions    

        for item, el in enumerate(locations):
            Cube((el[0] + amplitudes[item][0]*math.sin(time + phases[item][0]*(3.1415927/180))*amplitude_amplificator,
                  el[1] + amplitudes[item][1]*math.sin(time + phases[item][1]*(3.1415927/180))*amplitude_amplificator,
                  el[2] + amplitudes[item][2]*math.sin(time + phases[item][2]*(3.1415927/180))*amplitude_amplificator
                  ), size, color_table[item])

        # Drawing the Original Shapes (Specified nodes in Lines Tuple)

        Line_orig(lines)

        # Drawing the Deformed Shape

        glBegin(GL_LINES)
        for edge in lines:
            for vertex in edge:
                glVertex3fv((locations[vertex][0] + amplitudes[vertex][0]*math.sin(time + phases[vertex][0]*(3.1415927/180))*amplitude_amplificator,
                             locations[vertex][1] + amplitudes[vertex][1]*math.sin(time + phases[vertex][1]*(3.1415927/180))*amplitude_amplificator ,
                             locations[vertex][2] + amplitudes[vertex][2]*math.sin(time + phases[vertex][2]*(3.1415927/180))*amplitude_amplificator,
                             ))
        glEnd()

       # OpenGL Management



        pygame.display.flip()
        pygame.time.wait(10)

main()

Solution

  • The problem is that when i turn the model in the first step axis of rotation transform as well and they are not aligned with my "camera view". Of course I would like to keep my rotation axes always aligned with my camera view.

    In a rendering, each mesh of the scene usually is transformed by the model matrix, the view matrix and the projection matrix.


    If you want to rotate the szene around an axis in view space, the you have to do the following:


    Size the OpenGL fixed function pipeline has a matrix stack, this operations have to be done in the reverse order.

    e.g. See the documentation of glMultMatrix:

    glMultMatrix multiplies the current matrix with the one specified using m, and replaces the current matrix with the product.

    In OpenGL there is one matrix stack for each matrix mode (See glMatrixMode). The matrix modes are GL_MODELVIEW, GL_PROJECTION, and GL_TEXTURE.

    First you have to setup the projection matrix on the separated projection matrix stack:

    glMatrixMode( GL_PROJECTION );
    gluPerspective(45, (display[0]/display[1]), 0.1, 30000.0)
    

    Next create a model matrix

    a = (GLfloat * 16)()
    modelMat = glGetFloatv(GL_MODELVIEW_MATRIX, a)
    

    Init the model view matrix in the main loop:

    glMatrixMode( GL_MODELVIEW );    
    glLoadIdentity()
    

    Calcualte the new rotation and translation:

    axis = (p2[0]- p1[0], p2[1]- p1[1])
    glRotatef( angle_calculation(p1,p2), axis[1], axis[0], 0 )
    

    Multiply the model matrix by the previous model matrix and store the combined model matrix:

    glMultMatrixf( modelMat )
    modelMat = glGetFloatv(GL_MODELVIEW_MATRIX, a)
    

    Setup the view and apply the new model matrix:

    glLoadIdentity()
    glTranslatef(0,0.0,-10000)
    glMultMatrixf( modelMat )
    


    The final code may look like this:

    .....
    
    glMatrixMode( GL_PROJECTION );
    gluPerspective(45, (display[0]/display[1]), 0.1, 30000.0)
    
    a = (GLfloat * 16)()
    modelMat = glGetFloatv(GL_MODELVIEW_MATRIX, a)
    
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                quit()
    
        glMatrixMode( GL_MODELVIEW );    
        glLoadIdentity()
    
        norm_mouse_pos = (2*pygame.mouse.get_pos()[0]/display[0]-1,2*pygame.mouse.get_pos()[1]/display[1]-1,map_hemisphere(2*pygame.mouse.get_pos()[0]/display[0]-1,2*pygame.mouse.get_pos()[1]/display[1]-1))
        if pygame.mouse.get_pressed()[0]==1:
            if mouse_pressed == 0:
                mouse_pressed = 1
                clear = lambda: os.system('cls')
                clear()
                p1 = (norm_mouse_pos[0],norm_mouse_pos[1],map_hemisphere(norm_mouse_pos[0],norm_mouse_pos[1]))
            else:
                p2 = (norm_mouse_pos[0],norm_mouse_pos[1],map_hemisphere(norm_mouse_pos[0],norm_mouse_pos[1]))
                cist = np.cross(p1, p2)
                axis = (p2[0]- p1[0], p2[1]- p1[1])
                glRotatef( angle_calculation(p1,p2) , axis[1] , axis[0] , 0 )
        else:
            mouse_pressed = 0
    
        # Translation of the model via keyboard handling
        keys=pygame.key.get_pressed()
        if keys[K_w]:
            glTranslatef(0, 100, 0)
        if keys[K_s]:
            glTranslatef(0, -100, 0)
        if keys[K_a]:
            glTranslatef(-100, 0, 0)
        if keys[K_d]:
            glTranslatef(100, 0, 0)
    
        glMultMatrixf( modelMat )
        modelMat = glGetFloatv(GL_MODELVIEW_MATRIX, a)
    
        glLoadIdentity()
        glTranslatef(0,0.0,-10000)
        glMultMatrixf( modelMat )
    
        glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
        .....
    


    The "ValueError: math domain error", which sometimes occurs, is because the arcus cosine of a value is defined only, if the value is in the range [-1, 1]. Clamp the value to this range (min(1,max(cos_a,-1))):

    def angle_calculation(a,b):
        cos_a = np.dot(a, b) / (np.linalg.norm(a)*np.linalg.norm(b))
        r = math.degrees(math.acos( min(1,max(cos_a,-1)) ))
        return r