PyOpenGL transforming view coordinates into object coordinates for ArcBall navigation

Question:

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()
Asked By: J.J.

||

Answers:

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.

  • Projection matrix:

    The projection matrix describes the mapping from 3D points of a scene, to 2D points of the viewport.

  • View matrix:
    The view matrix describes the direction and position from which the scene is looked at. The view matrix transforms from the wolrd space to the view (eye) space.

  • Model matrix:
    The model matrix defines the location, oriantation and the relative size of a mesh in the scene. The model matrix transforms the vertex positions from of the mesh to the world space.

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

  • Transform the model by all the rotations and translations that you have done before the new rotation operation.

  • Apply the new rotation operation.

  • Apply the view translation

  • Apply the projection matrix

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
Answered By: Rabbid76