Skip to content

Latest commit

 

History

History
960 lines (716 loc) · 33.6 KB

example_python_navigation_glm.md

File metadata and controls

960 lines (716 loc) · 33.6 KB

previous



Python OpenGL 4.6, GLM navigation

See also

TODO

  • gird
  • coordinate axis
  • zoom by scaling projection matrix

Overview

Data source

Vectors and matrices operations make use of the PyGlm, which is based on OpenGL Mathematics (GLM). Matrices are generated by glm.mat3 and glm.mat4 and vectors are generated by glm.vec3 and glm.vec4.

proj, inv_proj # projection matrix and inverse projection matrix
view, inv_view # view matrix and inverse view matrix
vp_rect        # (x, y, w, h) view port rectangle
               # x, y lower bottom origin of the viewport rectangle
               # w, h width and height of the viewport rectangle
cursor_pos     # (x, y) window position of the cursor (mouse)
cursor_depth   # representative depth at the current cursor position 

width  = vp_rect[2]
height = vp_rect[3]

inv_wnd = glm.translate(glm.mat4(1), glm.vec3(-1, -1, 0))
inv_wnd = glm.scale(inv_wnd, glm.vec3(2/vp_rect[2], 2/vp_rect[3], 1))
inv_wnd = glm.translate(inv_wnd, glm.vec3(-vp_rect[0], -vp_rect[1], 0))
wnd     = glm.inverse(inv_wnd)

Perspective "zoom" - move view position along view ray

See also How to implement zoom towards mouse like in 3dsMax?

Using glm.unproject

# get world space position on view ray
pt_wnd   = glm.vec3(*cursor_pos, 1.0)
pt_world = glm.unProject(pt_wnd, view, proj, vp_rect)

# get view position
eye = glm.vec3(inv_view[3])

# get "zoom" direction and amount
ray_cursor = glm.normalize(pt_world - eye)

# translate view position and update view matrix
inv_view = glm.translate(glm.mat4(1), ray_cursor * delta) * inv_view
view     = glm.inverse(inv_view)

Using the view projection and window matrix

# get world space position on view ray
pt_wnd     = glm.vec3(*cursor_pos, 1.0)
pt_h_world = inv_view * inv_proj * inv_wnd * glm.vec4(*pt_wnd, 1)
pt_world   = glm.vec3(pt_h_world) / pt_h_world.w

Pane

See also How to implement camera pan like in 3dsMax?

The amount of displacement for the eye and target position depends on the depth of the object which is dragged on the viewport.

If the object is close to the eye position, then a translation on the viewport leads to a small displacement of the eye and target positions:

pan near

If the distance from the object to the eye is far, then a translation on the viewport leads to a large displacement of the eye and target positions:

pan far

Store the current cursor position and depth when pan is started:

# get drag start  in window coordinates
wnd_from = glm.vec3(*cursor_pos, cursor_depth*2-1)

Translate the view planar to the viewport when the cursor position is updated:

# get drag end  in window coordinates
wnd_to   = glm.vec3(*cursor_pos, wnd_from[2])

# get drag start and world coordinates
pt_h_world = [inv_view * inv_proj * inv_wnd * glm.vec4(*pt, 1) for pt in [wnd_from, wnd_to]]
pt_world   = [glm.vec3(pt_h) / pt_h.w for pt_h in pt_h_world]

# get drag world translation
world_vec = pt_world[1] - pt_world[0]

# get drag world translation
world_vec = pt_world_to - pt_world_from

# translate view position and update view matrix
inv_view     = glm.translate(glm.mat4(1), world_vec * -1) * inv_view
view         = glm.inverse(inv_view)

# set the start position for the next pan operation
wnd_from = wnd_to

Rotate

Orbit

See also Proper way to handle camera rotations

Store the current cursor position and the pivot when orbit is started. In this case the pivot is the origin of the world (0, 0, 0), but it can be any other point, too:

# get drag start  in window coordinates
wnd_from = glm.vec3(*cursor_pos, cursor_depth*2-1)

# define pivot
pivot_world = glm.vec3(0, 0, 0)

Rotate the view around the pivot according to the drag direction and drag distance:

# get drag end point in window coordinates
wnd_to = glm.vec3(*cursor_pos, wnd_from[2])

# calculate the pivot, rotation axis and angle
pivot     = glm.vec3(view * glm.vec4(*pivot_world, 1))
orbit_dir = wnd_to - wnd_from 
axis  = glm.vec3(-orbit_dir.y, orbit_dir.x, 0)
angle = glm.length(glm.vec2(orbit_dir.x/view_rect[2], orbit_dir.y/view_rect[3])) * math.pi

# calculate the rotation matrix and the rotation around the pivot 
rot_mat   = glm.rotate(glm.mat4(1), angle, axis)
rot_pivot = glm.translate(glm.mat4(1), pivot) * rot_mat * glm.translate(glm.mat4(1), -pivot)

#transform and update view matrix
view     = rot_pivot * view
inv_view = glm.inverse(view)

# set the start position for the next pan operation
wnd_from = wnd_to
Rotate world up vector and local x axis

How to implement alt+MMB camera rotation like in 3ds max?

# get the drag start and end
wnd_to = glm.vec3(*cursor_pos, wnd_from[2])

# calculate the pivot, rotation axis and angle
pivot_view = glm.vec3(view * glm.vec4(*pivot_world, 1))
orbit_dir  = wnd_to - wnd_from

# get the projection of the up vector to the view port
# TODO

# calculate the rotation components for the rotation around the view space x axis and the world up vector 
orbit_dir_x  = glm.vec2(0, 1)
orbit_vec_x  = glm.vec2(0, orbit_dir.y)
orbit_dir_up = glm.vec2(1, 0)
orbit_vec_up = glm.vec2(orbit_dir.x, 0)

# calculate the rotation matrix around the view space x axis through the pivot
rot_pivot_x = glm.mat4(1)
if glm.length(orbit_vec_x) > 0.5: 
    axis_x      = glm.vec3(-1, 0, 0)
    angle_x     = glm.dot(orbit_dir_x, glm.vec2(orbit_vec_x.x/view_rect[2], orbit_vec_x.y/view_rect[3])) * math.pi
    rot_mat_x   = glm.rotate(glm.mat4(1), angle_x, axis_x)
    rot_pivot_x = glm.translate(glm.mat4(1), pivot_view) * rot_mat_x * glm.translate(glm.mat4(1), -pivot_view)

# calculate the rotation matrix around the world space up vector through the pivot
rot_pivot_up = glm.mat4(1)
if glm.length(orbit_vec_up) > 0.5: 
    axis_up      = glm.vec3(0, 0, 1)
    angle_up     = glm.dot(orbit_dir_up, glm.vec2(orbit_vec_up.x/view_rect[2], orbit_vec_up.y/view_rect[3])) * math.pi
    rot_mat_up   = glm.rotate(glm.mat4(1), angle_up, axis_up)
    rot_pivot_up = glm.translate(glm.mat4(1), pivot_world) * rot_mat_up * glm.translate(glm.mat4(1), -pivot_world)

#transform and update view matrix
view         = rot_pivot_x * view * rot_pivot_up
view_changed = True

# set the start position for the next pan operation
wnd_from = wnd_to

Code listing

Vertex shader

The vertex shader is the same as in the previous example

Fragment shader

The fragment shader is the same as in the previous example

Python script

import os
import sys
currentWDir = os.getcwd()
print( 'current working directory: {}'.format( str(currentWDir) ) )
fileDir = os.path.dirname(os.path.abspath(__file__)) # det the directory of this file
print( 'current location of self: {}'.format( str(fileDir) ) )
parentDir = os.path.abspath(os.path.join(fileDir, os.pardir)) # get the parent directory of this file
sys.path.insert(0, parentDir)
print( 'insert system directory: {}'.format( str(parentDir) ) )
os.chdir( fileDir )
baseWDir = os.getcwd()
print( 'changed current working directory: {}'.format( str(baseWDir) ) )
print ( '' )

import math
from time import time
import numpy as np

# PyGLM
import glm

# PyOpenGL import
from OpenGL.GL import *
from OpenGL.GLUT import *

from OpenGL.GL.shaders import *

from subprocess import call
from shutil import which

SIZEOF_FLAOT32 = np.finfo(np.float32).bits // 8

glsl_vert_draw_file = 'cube_glut_glm_draw.vert'
glsl_frag_draw_file = 'cube_glut_glm_draw.frag'
spv_vert_draw_file = 'cube_glut_glm_draw_vert.spv'
spv_frag_draw_file = 'cube_glut_glm_draw_frag.spv'

fileConvert = [(glsl_vert_draw_file, spv_vert_draw_file), (glsl_frag_draw_file, spv_frag_draw_file)]
glslVaidator = which('glslangValidator.exe')

if glslVaidator != None:
    print('Create Spire-V shader program')
    for glslFile, spvFile in fileConvert:
        glslPath = str(baseWDir) + '\\' + glslFile
        spvPath  = str(baseWDir) + '\\' + spvFile
        vspir_vert_cmd = '"' + glslVaidator + '" -V "' + glslPath + '" -o "' + spvPath + '"'
        call( vspir_vert_cmd )

#
# OpenGL context base class
#
class GL_Context:

    __printExtensions = False

    def __init__(self, debugcontext):

        self.__debugcontext = debugcontext

        self.__vendor       = glGetString( GL_VENDOR )
        self.__renderer     = glGetString( GL_RENDERER )
        self.__version      = glGetString( GL_VERSION )
        self.__glsl_version = glGetString( GL_SHADING_LANGUAGE_VERSION )
        self.__major        = glGetInteger( GL_MAJOR_VERSION )
        self.__minor        = glGetInteger( GL_MINOR_VERSION )
        self.__numOfExt     = glGetInteger( GL_NUM_EXTENSIONS )
        self.__extensions   = []
        for i in range(self.__numOfExt):
            self.__extensions.append( glGetStringi( GL_EXTENSIONS, i ) )

        print( str(self.__vendor ) )
        print( str(self.__renderer ) )
        print( str(self.__vendor ) )
        print( str(self.__glsl_version ) )
        print('')
        print( 'OpenGL Version {}.{}'.format( self.__major, self.__minor ) )
        print('')
        print( '{} extensions'.format( self.__numOfExt ) )
        if self.__printExtensions:
            for i in range(self.__numOfExt):
                print( '    {}'.format( str(self.__extensions[i]) ) )
        print('')

        if self.__debugcontext:

            glDebugMessageCallback(self.__CB_OpenGL_DebugMessage, None)

            errors_only = False
            if errors_only:
                glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, None, GL_FALSE)
                glDebugMessageControl(GL_DEBUG_SOURCE_API, GL_DEBUG_TYPE_ERROR, GL_DONT_CARE, 0, None, GL_TRUE)
            else:
                glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, None, GL_TRUE)

            glEnable(GL_DEBUG_OUTPUT)
            glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS)
            glDebugMessageInsert(GL_DEBUG_SOURCE_APPLICATION, GL_DEBUG_TYPE_MARKER, 0, GL_DEBUG_SEVERITY_NOTIFICATION, -1, "Starting debug messaging service")

    @GLDEBUGPROC
    def __CB_OpenGL_DebugMessage(source, type, id, severity, length, message, userParam):
        msg = message[0:length]
        print('debug:', msg)

#
# OpenGL program base class 
#
class GL_Program:

    __program = 0

    def __init__(self, program):
        self.__program = program

    def Object(self):
        return self.__program

    def Use(self):
        pass#glUseProgram(self.__program)

    def AttributeLocation(self, name):
        return glGetAttribLocation(self.__program, name)

    def UniformLocation(self, name):
        return glGetUniformLocation(self.__program, name)

    def AttributeLocations(self, names):
        return { a : self.AttributeLocation(a) for a in names }

    def UniformLocations(self, names):
        return { u : self.UniformLocation(u) for u in names }

#
# OpenGL GLSL progam
#
class GL_Program_GLSL(GL_Program):

    def __init__(self, vs, fs):

        vert_code = ""
        try:
            with open(vs, 'r') as vsFile:
                vert_code = vsFile.read()
        except:
            vert_code = vs

        frag_code = ""
        try:
            with open(fs, 'r') as vsFile:
                frag_code = vsFile.read()
        except:
            frag_code = fs

        sh_code_list = [(GL_VERTEX_SHADER, vert_code), (GL_FRAGMENT_SHADER, frag_code)]

        # OpenGL.GL.shaders
        # http://pyopengl.sourceforge.net/pydoc/OpenGL.GL.shaders.html
        #sh_objs = [compileShader(sh_code[1], sh_code[0]) for sh_code in sh_code_list]
        #program = compileProgram(vs_obj, fs_obj)

        sh_objs = []
        for sh_code in sh_code_list:
            sh_obj = glCreateShader(sh_code[0])
            glShaderSource(sh_obj, sh_code[1])
            glCompileShader(sh_obj)
            result = glGetShaderiv(sh_obj, GL_COMPILE_STATUS )
            #if verbose or not result:
            #    print( '\n%s shader code:' % nameMap.get( shaderStage, '' ) )
            #    print( sh_code[1] )
            if not result:
                print( glGetShaderInfoLog(sh_obj) )
            sh_objs.append(sh_obj)
        
        program = glCreateProgram()
        for shObj in sh_objs: glAttachShader(program, shObj)

        # programs has to be declare separable for the use with program pipeline - this is crucial!
        glProgramParameteri(program, GL_PROGRAM_SEPARABLE, GL_TRUE)

        glLinkProgram(program)
        if not glGetProgramiv(program, GL_LINK_STATUS):
            print( 'link error:' )
            print( glGetProgramInfoLog(program) )

        super().__init__(program)

#
# OpenGL Spir-V progam
#
class GL_Program_SpirV(GL_Program):

    def __init__(self, vs, fs):

        sh_objs = []
        sh_l = [(GL_VERTEX_SHADER, vs), (GL_FRAGMENT_SHADER, fs)]
        for sh_type, sh_file in sh_l:
            try:
                sh_code = None
                sh_size = 0
                with open(sh_file, 'rb') as vsFile:
                    sh_code = vsFile.read()
                    sh_size = os.stat(sh_file).st_size
                sh_obj = glCreateShader(sh_type)
                sho = np.array([sh_obj], dtype=np.uint32)
                glShaderBinary(1, sho, GL_SHADER_BINARY_FORMAT_SPIR_V, sh_code, sh_size)
                glSpecializeShader(sh_obj, 'main', 0, None, None)
                result = glGetShaderiv(sh_obj, GL_COMPILE_STATUS)
                if not result:
                    print(glGetShaderInfoLog(sh_obj))
                sh_objs.append(sh_obj)
            except:
                pass

        program = glCreateProgram()
        for sh_obj in sh_objs: glAttachShader(program, sh_obj)

        # programs has to be declare separable for the use with program pipeline - this is crucial!
        glProgramParameteri(program, GL_PROGRAM_SEPARABLE, GL_TRUE)

        glLinkProgram(program)
        if not glGetProgramiv(program, GL_LINK_STATUS):
            print( 'link error:' )
            print( glGetProgramInfoLog(program) )
        for sh_obj in sh_objs: glDeleteShader(sh_obj)

        super().__init__(program)

#
# OpenGL mesh base class
#
class GL_Mesh:

    __vao = 0
    __no_of_vertices = 0
    __no_of_indices = 0

    def __init__(self, vao, no_vertices, no_indices):
        self.__vao            = vao
        self.__no_of_vertices = no_vertices
        self.__no_of_indices  = no_indices

    def Bind(self):
        if self.__no_of_vertices > 0:
            glBindVertexArray(self.__vao)

    def Draw(self):
        if self.__no_of_vertices > 0:
            glBindVertexArray(self.__vao)
            glDrawArrays(GL_TRIANGLES, 0, self.__no_of_vertices)

#
# OpenGL cube mesh
#
class GL_MeshCube(GL_Mesh):

    def __init__(self):

        v = [ -1,-1,1,  1,-1,1,  1,1,1, -1,1,1, -1,-1,-1,  1,-1,-1,  1,1,-1, -1,1,-1 ]
        c = [ 1.0, 0.0, 0.0,   1.0, 0.5, 0.0,    1.0, 0.0, 1.0,   1.0, 1.0, 0.0,   0.0, 1.0, 0.0,   0.0, 0.0, 1.0 ]
        n = [ 0,0,1, 1,0,0, 0,0,-1, -1,0,0, 0,1,0, 0,-1,0 ]
        e = [ 0,1,2,3, 1,5,6,2, 5,4,7,6, 4,0,3,7, 3,2,6,7, 1,0,4,5 ]
        attr_array = []
        for si, vi, ci in [(si, vi, [0, 1, 2, 0, 2, 3][vi]) for si in range(6) for vi in range(6)]:
            i = si*4+ci
            attr_array.extend( [ v[e[i]*3], v[e[i]*3+1], v[e[i]*3+2] ] )
            attr_array.extend( [ n[si*3], n[si*3+1], n[si*3+2] ] )
            attr_array.extend( [ c[si*3], c[si*3+1], c[si*3+2], 1 ] ); 
        no_vert = len(attr_array) // 10

        vertex_attributes = np.array(attr_array, dtype=np.float32)

        self.__vbo = np.empty(1, dtype=np.uint32)
        glCreateBuffers(len(self.__vbo), self.__vbo)

        vao = np.empty(1, dtype=np.uint32)
        glCreateVertexArrays(len(vao), vao)

        dynamic_buffer = False
        code = 0 if not dynamic_buffer else GL_DYNAMIC_STORAGE_BIT | GL_MAP_WRITE_BIT| GL_MAP_PERSISTENT_BIT
        glNamedBufferStorage(self.__vbo, vertex_attributes.nbytes, vertex_attributes, code)
        glVertexArrayVertexBuffer(vao, 0, self.__vbo, 0, 10*vertex_attributes.itemsize)

        vertex_spec = [
            (0, 3, GL_FLOAT, False, 0),
            (1, 3, GL_FLOAT, False, 3*vertex_attributes.itemsize),
            (2, 4, GL_FLOAT, False, 6*vertex_attributes.itemsize)
        ]
        for vspec in vertex_spec:
            glVertexArrayAttribFormat(vao, *vspec)
            glVertexArrayAttribBinding(vao, vspec[0], 0)
            glEnableVertexArrayAttrib(vao, vspec[0])

        super().__init__(vao, no_vert, 0)

#
# Navigation controller
#
class CNavigationController:

    OFF    = 0
    ORBIT  = 1
    ROTATE = 2

    def __init__(self, view_mat, proj_mat, view_rect, depth_val, pivot_pt):

        self.__id = id
        self.__get_view_rect = view_rect
        self.__get_view_mat  = view_mat
        self.__get_proj_mat  = proj_mat
        self.__get_depth_val = depth_val
        self.__get_pivot     = pivot_pt

        self.__pan = False
        self.__pan_start = glm.vec3(0, 0, 1)
        self.__orbit = self.OFF
        self.__orbit_start = glm.vec3(0, 0, 1)
        self.__pivot_world = glm.vec3(0, 0, 0)

    def ProjectionMat(self):
        proj = self.__get_proj_mat()
        return proj, glm.inverse(proj)

    def ViewMat(self):
        view = self.__get_view_mat()
        return view, glm.inverse(view)

    def WindowMat(self):
        vp_rect = self.VpRect()
        inv_wnd = glm.translate(glm.mat4(1), glm.vec3(-1, -1, 0))
        inv_wnd = glm.scale(inv_wnd, glm.vec3(2/vp_rect[2], 2/vp_rect[3], 1))
        inv_wnd = glm.translate(inv_wnd, glm.vec3(-vp_rect[0], -vp_rect[1], 0))
        return glm.inverse(inv_wnd), inv_wnd

    def VpRect(self):
        return self.__get_view_rect()

    def Depth(self, cursor_pos):
        return self.__get_depth_val(*cursor_pos)

    def PivotWorld(self, cursor_pos):
        return self.__get_pivot(*cursor_pos)

    def StartPan(self, cursor_pos):
        self.__pan = True
        self.__pan_start = glm.vec3(*cursor_pos, self.Depth(cursor_pos)*2-1)
        
    def EndPan(self, cursor_pos):
        self.__pan = False

    def StartOrbit(self, cursor_pos, mode = ORBIT):
        self.__orbit = mode if mode >= self.ORBIT and mode <= self.ROTATE else self.ORBIT
        self.__orbit_start = glm.vec3(*cursor_pos, self.Depth(cursor_pos)*2-1)
        self.__pivot_world = self.PivotWorld(cursor_pos)
        
    def EndOrbit(self, cursor_pos):
        self.__orbit = self.OFF

    def MoveOnLineOfSight(self, cursor_pos, delta):

        # TODO $$$ algorithm without glm.unProject

        # get viewport rectangle
        #vp_rect = self.VpRect()
       
        # get view, projection and window matrix
        proj, inv_proj = self.ProjectionMat()
        view, inv_view = self.ViewMat()
        wnd,  inv_wnd  = self.WindowMat() 

        # get world space postion on view ray
        pt_wnd     = glm.vec3(*cursor_pos, 1.0)
        #pt_world  = glm.unProject(pt_wnd, view, proj, vp_rect)
        pt_h_world = inv_view * inv_proj * inv_wnd * glm.vec4(*pt_wnd, 1)
        pt_world   = glm.vec3(pt_h_world) / pt_h_world.w
         
        # get view postion
        eye  = glm.vec3(inv_view[3])
        
        # get "zoom" direction and amount
        ray_cursor = glm.normalize(pt_world - eye)
        
        # translate view position and update view matrix
        inv_view = glm.translate(glm.mat4(1), ray_cursor * delta) * inv_view
        
        # return new view matrix
        return glm.inverse(inv_view), True

        
    def MoveCursorTo(self, cursor_pos):
        
        view_changed = False 

        # get view matrix and  vieport rectangle
        view, inv_view = self.ViewMat()
        view_rect      = self.VpRect()

        if self.__pan:
 
            # get drag start and end
            wnd_from = self.__pan_start
            wnd_to   = glm.vec3(*cursor_pos, self.__pan_start[2])
            self.__pan_start = wnd_to

            # get projection and window matrix
            proj, inv_proj = self.ProjectionMat()
            wnd,  inv_wnd  = self.WindowMat() 

            # calcaulate drag start and world coordinates
            pt_h_world = [inv_view * inv_proj * inv_wnd * glm.vec4(*pt, 1) for pt in [wnd_from, wnd_to]]
            pt_world   = [glm.vec3(pt_h) / pt_h.w for pt_h in pt_h_world]
           
            # calculate drag world translation
            world_vec = pt_world[1] - pt_world[0]

            # translate view position and update view matrix
            inv_view     = glm.translate(glm.mat4(1), world_vec * -1) * inv_view
            view         = glm.inverse(inv_view)
            view_changed = True

        elif self.__orbit == self.ORBIT:

            # get the drag start and end
            wnd_from = self.__orbit_start
            wnd_to   = glm.vec3(*cursor_pos, self.__orbit_start[2])
            self.__orbit_start = wnd_to

            # calculate the pivot, rotation axis and angle
            pivot     = glm.vec3(view * glm.vec4(*self.__pivot_world, 1))
            orbit_dir = wnd_to - wnd_from 
            axis  = glm.vec3(-orbit_dir.y, orbit_dir.x, 0)
            angle = glm.length(glm.vec2(orbit_dir.x/view_rect[2], orbit_dir.y/view_rect[3])) * math.pi

            # calculate the rotation matrix and the rotation around the pivot 
            rot_mat   = glm.rotate(glm.mat4(1), angle, axis)
            rot_pivot = glm.translate(glm.mat4(1), pivot) * rot_mat * glm.translate(glm.mat4(1), -pivot)
            
            #transform and update view matrix
            view         = rot_pivot * view
            view_changed = True 

        elif self.__orbit == self.ROTATE:

            # get the drag start and end
            wnd_from = self.__orbit_start
            wnd_to   = glm.vec3(*cursor_pos, self.__orbit_start[2])
            self.__orbit_start = wnd_to

            # calculate the pivot, rotation axis and angle
            pivot_view   = glm.vec3(view * glm.vec4(*self.__pivot_world, 1))
            orbit_dir    = wnd_to - wnd_from 

            # get the projection of the up vector to the view port 
            # TODO

            # calculate the rotation components for the rotation around the view space x axis and the world up vector 
            orbit_dir_x  = glm.vec2(0, 1)
            orbit_vec_x  = glm.vec2(0, orbit_dir.y)
            orbit_dir_up = glm.vec2(1, 0)
            orbit_vec_up = glm.vec2(orbit_dir.x, 0)

            # calculate the rotation matrix around the view space x axis through the pivot
            rot_pivot_x = glm.mat4(1)
            if glm.length(orbit_vec_x) > 0.5: 
                axis_x      = glm.vec3(-1, 0, 0)
                angle_x     = glm.dot(orbit_dir_x, glm.vec2(orbit_vec_x.x/view_rect[2], orbit_vec_x.y/view_rect[3])) * math.pi
                rot_mat_x   = glm.rotate(glm.mat4(1), angle_x, axis_x)
                rot_pivot_x = glm.translate(glm.mat4(1), pivot_view) * rot_mat_x * glm.translate(glm.mat4(1), -pivot_view)
            
            # calculate the rotation matrix around the world space up vector through the pivot
            rot_pivot_up = glm.mat4(1)
            if glm.length(orbit_vec_up) > 0.5: 
                axis_up      = glm.vec3(0, 0, 1)
                angle_up     = glm.dot(orbit_dir_up, glm.vec2(orbit_vec_up.x/view_rect[2], orbit_vec_up.y/view_rect[3])) * math.pi
                rot_mat_up   = glm.rotate(glm.mat4(1), angle_up, axis_up)
                rot_pivot_up = glm.translate(glm.mat4(1), self.__pivot_world) * rot_mat_up * glm.translate(glm.mat4(1), -self.__pivot_world)
            
            #transform and update view matrix
            view         = rot_pivot_x * view * rot_pivot_up
            view_changed = True 

        # return the view matrix
        return view, view_changed
        
#
# GLUT window class
#
class GL_Window:

    def __init__(self, cx, cy, caption=b'OGL window', multisample=True):

        glutInit()

        self.__vp_size = ( cx, cy )
        self.__view    = glm.mat4(1)
        self.__proj    = glm.mat4(1)

        self.__navigate_control = CNavigationController(
            lambda : self.__view,
            lambda : self.__proj,
            lambda : glm.vec4(0, 0, *self.__vp_size),
            lambda x, y : self.__GetDepth(x, y),
            lambda _, __ : glm.vec3(0, 0, 0) )

        self.__glut_param = GLUT_RGBA | GLUT_DOUBLE | GLUT_ALPHA | GLUT_DEPTH
        if multisample == True:
            self.__glut_param = self.__glut_param | GLUT_MULTISAMPLE 
        self.__multisample_level = 8
        if multisample == True:
            glutSetOption( GLUT_MULTISAMPLE, self.__multisample_level )

        glutInitContextVersion( 3, 3 )
        glutInitContextProfile( GLUT_CORE_PROFILE )
        glutInitDisplayMode( self.__glut_param )

        glutInitWindowPosition( 0, 0 )
        glutInitWindowSize( self.__vp_size[0], self.__vp_size[1] )
        self.__id = glutCreateWindow( caption )

        self.__multisample  = multisample
        self.__context_info = GL_Context(True)

        print( 'Mutisample: {0}'.format( 'on ({})'.format(self.__multisample_level) if self.__multisample else 'off' ) )
        print('')
        print('')

        self._Init()

        glutIdleFunc(self.__Idle)

        glutReshapeFunc(self.__Resize)

        glutKeyboardFunc(self.__Keyboard)
        glutSpecialFunc(self.__SpecialKey)

        glutMouseFunc(self.__MouseButton)
        glutMotionFunc(self.__MouseMotion)
        glutPassiveMotionFunc(self.__MousePassiveMotion)
        glutEntryFunc(self.__MouseEntry)
        glutMouseWheelFunc(self.__MouseWheel)

        glutJoystickFunc(self.__Joystick)

        glutDisplayFunc(self.__Display)
        glutOverlayDisplayFunc(self.__OverlayDisplay)

    def Run(self):
        self.__vp_valid     = False
        self.__start_time   = time()
        self.__current_time = self.__start_time
        glutMainLoop()

    def __Idle(self):
        pass

    def __Resize(self, x, y):
        self.__vp_valid = False

    def __Keyboard(self, key, x, y):
        pass

    def __SpecialKey(self, key, x, y):
        pass

    def __MouseButton(self, button, state, x, y ):
        wnd_pos = (x, self.__vp_size[1]-y) 
        if button == GLUT_RIGHT_BUTTON and state == GLUT_DOWN:
            self.__navigate_control.StartPan(wnd_pos)
        elif button == GLUT_RIGHT_BUTTON and state == GLUT_UP:
            self.__navigate_control.EndPan(wnd_pos)
        if button == GLUT_LEFT_BUTTON and state == GLUT_DOWN:
            self.__navigate_control.StartOrbit(wnd_pos, self.__navigate_control.ORBIT)
            #self.__navigate_control.StartOrbit(wnd_pos, self.__navigate_control.ROTATE)
        elif button == GLUT_LEFT_BUTTON and state == GLUT_UP:
            self.__navigate_control.EndOrbit(wnd_pos)

    def __MouseMotion(self, x, y ):
        wnd_pos = (x, self.__vp_size[1]-y)
        self.__view, self.__update_view = self.__navigate_control.MoveCursorTo(wnd_pos) 

    def __MousePassiveMotion(self, x, y ): 
        pass

    def __MouseEntry(self, state):
        pass

    def __MouseWheel(self, wheel, direction, x, y):        
        wnd_pos = (x, self.__vp_size[1]-y) 
        self.__view, self.__update_view = self.__navigate_control.MoveOnLineOfSight(wnd_pos, direction)

    def __Joystick(self, buttons, xaxis, yaxis, zaxis):
        pass

    def __Display(self):
        self.__current_time = time()
        self.__time = self.__current_time - self.__start_time
        if self.__vp_valid == False:
            self.__vp_size  = ( glutGet( GLUT_WINDOW_WIDTH ), glutGet( GLUT_WINDOW_HEIGHT ) )
            self.__vp_valid = True
            glViewport(0, 0, *self.__vp_size)

        self._Draw(self.__time)
        glutSwapBuffers()
        glutPostRedisplay()

    def __OverlayDisplay(self):
        pass

    def __GetDepth(self, x, y):
        depth_buffer = glReadPixels(x, self.__vp_size[1]-y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT)    
        depth = float(depth_buffer[0][0])
        if depth == 1:
            pt_drag  = glm.vec3(0, 0, 0)
            clip_pos = self.__proj * self.__view * glm.vec4(pt_drag, 1)
            ndc_pos  = glm.vec3(clip_pos) / clip_pos.w
            if ndc_pos.z > -1 and ndc_pos.z < 1:
                depth = ndc_pos.z * 0.5 + 0.5
        return depth

    def _Init(self):

        # create cube mesh

        self.__cube = GL_MeshCube()

        # model view projection data shader storage block

        self.__model = glm.mat4(1)
        self.__view  = glm.lookAt(glm.vec3(0,-3,0), glm.vec3(0,0,0), glm.vec3(0,0,1))
        self.__proj  = glm.perspective(glm.radians(90), self.__vp_size[0]/self.__vp_size[1], 0.1, 100)

        buffer_data = np.zeros(3*16, dtype=np.float32)
        for i in range(4):
            for j in range(4):
                buffer_data

        self.__mvp_ssbo = np.empty(1, dtype=np.uint32)
        glCreateBuffers(len(self.__mvp_ssbo), self.__mvp_ssbo)
        dynamic_mvp_data = True
        code = 0 if not dynamic_mvp_data else GL_DYNAMIC_STORAGE_BIT | GL_MAP_WRITE_BIT| GL_MAP_PERSISTENT_BIT
        glNamedBufferStorage(self.__mvp_ssbo, 3*16*SIZEOF_FLAOT32, None, code)
        glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, self.__mvp_ssbo)

        glBufferSubData(GL_SHADER_STORAGE_BUFFER, 0,                      glm.sizeof(glm.mat4), glm.value_ptr(self.__model)) 
        glBufferSubData(GL_SHADER_STORAGE_BUFFER, 1*glm.sizeof(glm.mat4), glm.sizeof(glm.mat4), glm.value_ptr(self.__view)) 
        glBufferSubData(GL_SHADER_STORAGE_BUFFER, 2*glm.sizeof(glm.mat4), glm.sizeof(glm.mat4), glm.value_ptr(self.__proj)) 

        self.__update_view = False

        # light data shader storage block

        light_data = [-1.0, -0.5, -2.0, 0.0, 0.2, 0.8, 0.8, 10.0]
        light_data_buffer = np.array( light_data, dtype=np.float32 )

        self.__light_ssbo = np.empty(1, dtype=np.uint32)
        glCreateBuffers(len(self.__light_ssbo), self.__light_ssbo)
        dynamic_light_data = True
        code = 0 if not dynamic_light_data else GL_DYNAMIC_STORAGE_BIT | GL_MAP_WRITE_BIT| GL_MAP_PERSISTENT_BIT
        glNamedBufferStorage(self.__light_ssbo, light_data_buffer.nbytes, light_data_buffer, code)
        glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, self.__light_ssbo)

        # set states

        glEnable(GL_DEPTH_TEST)
        self.__cube.Bind()

        # initialize (compile and link) shader program 

        # This should be done after all OpenGL states have been set,
        # because NVIDIA optimize the current program for the current states an would have to recompile it if the states would change.
        # https://www.opengl.org/discussion_boards/showthread.php/175944-NVidia-280-x-and-GeForce-4xx?p=1229120&viewfull=1#post1229120 

        if os.path.isfile(spv_vert_draw_file) and os.path.isfile(spv_frag_draw_file):
            self.__draw_prog = GL_Program_SpirV(spv_vert_draw_file, spv_frag_draw_file)
        else:
            self.__draw_prog = GL_Program_GLSL(glsl_vert_draw_file, glsl_frag_draw_file)

        # create pipeline for draw mesh
 
        self.__draw_pipeline = np.empty(1, dtype=np.uint32)
        glGenProgramPipelines(1, self.__draw_pipeline)
        glUseProgramStages(self.__draw_pipeline[0], GL_VERTEX_SHADER_BIT | GL_FRAGMENT_SHADER_BIT, self.__draw_prog.Object())

        #al = ['inPos', 'inNV', 'inCol']
        #self.___attrib = self.__draw_prog.AttributeLocations(al)
        #print(self.___attrib)

        #ul = ['u_model', 'u_view', 'u_proj']
        #self.__uniform = self.__draw_prog.UniformLocations(ul)
        #print(self.__uniform)

        # activate program  and set states

        glBindProgramPipeline(self.__draw_pipeline[0])
        #self.__draw_prog.Use()

    def _Draw(self, time):

        self.__model = glm.mat4(1)

        angle1 = time * 2 * math.pi / 13
        angle2 = time * 2 * math.pi / 17
        #self.__model = glm.rotate(self.__model, angle1, glm.vec3(1, 0, 0) )
        #self.__model = glm.rotate(self.__model, angle2, glm.vec3(0, 0, 1) )

        glBindBuffer(GL_SHADER_STORAGE_BUFFER, self.__mvp_ssbo )
        glBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, glm.sizeof(glm.mat4), glm.value_ptr(self.__model)) 
        if self.__update_view:
            glBufferSubData(GL_SHADER_STORAGE_BUFFER, 1*glm.sizeof(glm.mat4), glm.sizeof(glm.mat4), glm.value_ptr(self.__view)) 

        glClearColor(0.3, 0.3, 0.3, 1.0)
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

        self.__cube.Draw()

window = GL_Window(800, 600)
window.Run()