Search code examples
pythonopenglpyopenglopengl-compat

PyOpenGL how to get the object the mouse is over


I want to create a guizmo system on the pyopengl project im working in guizmo

To do that i need a way to get what object in the schene the mouse is over, is there a way to find this out?

if it helps as a example this is the project im working on https://github.com/Thiago099/Experimental-3d-modeling/blob/master/main.ipynb


Solution

  • Based in a awser from this post and some adaptations i was able to get a code that works

        from pygame.locals import *
        from OpenGL.GL import *
        from OpenGL.GLU import *
        from OpenGL.GLUT import *
        import pygame
        import math
    
    def cube(mouse_near, mouse_far):
        vertices = (
            (1,-1,-1), (1,1,-1), (-1,1,-1), (-1,-1,-1),
            (1,-1,1), (1,1,1), (-1,-1,1), (-1,1,1)
        )
        edegs = (
            (0,1), (0,3), (0,4), (2,1), (2,3), (2,7),
            (6,3), (6,4), (6,7), (5,1), (5,4), (5,7),
        )
        faces = (
            (0, 1, 2, 3),
            (3, 2, 7, 6),
            (6, 7, 5, 4),
            (4, 5, 1, 0),
            (4, 0, 3, 6),
            (1, 5, 7, 2),   
            
        )
        
        glEnable(GL_POLYGON_OFFSET_FILL)
        glPolygonOffset(1.0, 1.0)
        glBegin(GL_QUADS)
        glColor3fv((0.4,0.4,0.4))    
        for face in faces:
            if(isectQuad(mouse_near, mouse_far, vertices[face[0]], vertices[face[1]], vertices[face[2]], vertices[face[3]])):
                glColor3fv((1,0.4,0.4)) 
            else:
                glColor3fv((0.4,0.4,0.4))    
            for vertex in face:
                glVertex3fv(vertices[vertex])
        glEnd()
    
        glBegin(GL_LINES)
        glColor3fv((1,1,1))
        for edeg in edegs:
            for vertex in edeg:
                glVertex3fv(vertices[vertex])
        glEnd()
    
    def subtract(v0, v1):
        return [v0[0]-v1[0], v0[1]-v1[1], v0[2]-v1[2]]
    def dot(v0, v1):
        return v0[0]*v1[0]+v0[1]*v1[1]+v0[2]*v1[2]
    def length(v):
        return math.sqrt(v[0]*v[0]+v[1]*v[1]+v[2]*v[2])
    def normalize(v):
        l = length(v)
        return [v[0]/l, v[1]/l, v[2]/l]
    def mults(v, s):
        return [v[0]*s, v[1]*s, v[2]*s]
    def add(v0, v1):
        return [v0[0]+v1[0], v0[1]+v1[1], v0[2]+v1[2]]
    def cross(v0, v1):
        return [
            v0[1]*v1[2]-v1[1]*v0[2],
            v0[2]*v1[0]-v1[2]*v0[0],
            v0[0]*v1[1]-v1[0]*v0[1]]
    
    def PointInOrOn( P1, P2, A, B ):
        CP1 = cross( subtract(B, A), subtract(P1, A) )
        CP2 = cross( subtract(B, A), subtract(P2, A) )
        return dot( CP1, CP2 ) >= 0
    
    
    # p0, p1   points on ray
    # PA, PB, PC  points of the triangle
    def isectPlane(p0, p1, PA, PB, PC):
        R0 = p0               # origin 
        D = normalize(subtract(p1, p0))
        P0 = PA
        NV = normalize( cross( subtract(PB, PA), subtract(PC, PA) ) )
        d = dot( D, NV )
        if(d == 0):
            d = 0.00001
        dist_isect = dot( subtract(P0, R0), NV ) / d
        P_isect    = add(R0, mults(D, dist_isect))
        return P_isect, dist_isect
    
    def PointInOrOnQuad( P, A, B, C, D ):
        return (PointInOrOn( P, A, B, C ) and PointInOrOn( P, B, C, D ) and
                PointInOrOn( P, C, D, A ) and PointInOrOn( P, D, A, B ))
    
    def isectQuad(p0, p1, PA, PB, PC, PD):
        P, t = isectPlane(p0, p1, PA, PB, PC)
        if t >= 0 and PointInOrOnQuad(P, PA, PB, PC, PD):
            return t
        return None
    
    pygame.init()
    screen = (800, 600)
    pygame.display.set_mode(screen, DOUBLEBUF|OPENGL)
    
    glEnable(GL_DEPTH_TEST)
    glEnable(GL_COLOR_MATERIAL)
    
    glMatrixMode(GL_PROJECTION)
    gluPerspective(45, (screen[0]/screen[1]), 0.1, 50.0)
    
    glMatrixMode(GL_MODELVIEW)  
    glTranslate(0.0,0.0,-5)
    
    rot_x, rot_y, zoom = 30, 45, -0.5
    
    clock = pygame.time.Clock()
    busy = True
    mv_mat = (GLdouble * 16)()
    p_mat  = (GLdouble * 16)()
    v_rect = (GLint * 4)() 
    while busy:
        try:
            mouse_buttons = pygame.mouse.get_pressed()
            button_down = mouse_buttons[0] == 1
        
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    busy = False
                elif event.type == pygame.MOUSEMOTION:
                    if button_down:
                        rot_x = (rot_x + event.rel[1]) % 360
                        if rot_x > 90 and rot_x < 270:
                            rot_y = (rot_y - event.rel[0]) % 360
                        else:
                            rot_y = (rot_y + event.rel[0]) % 360
                elif event.type == pygame.MOUSEBUTTONDOWN:
                    if event.button == 4:
                        zoom += 0.2
                    if event.button == 5:
                        zoom -= 0.2
    
            
            
    
            glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
            glPushMatrix()
            glTranslatef(0.0,0.0, zoom)
            glRotatef(rot_x, 1, 0, 0)  
            glRotatef(rot_y, 0, 1, 0)  
            glGetDoublev(GL_MODELVIEW_MATRIX, mv_mat)
            glGetDoublev(GL_PROJECTION_MATRIX, p_mat)
            glGetIntegerv(GL_VIEWPORT, v_rect)
            mouse_pos = pygame.mouse.get_pos()
            mouse_pos = mouse_pos[0], v_rect[3] - mouse_pos[1]
    
            temp_val = [GLdouble() for _ in range(3)]
            OpenGL.raw.GLU.gluUnProject(*mouse_pos, 0, mv_mat, p_mat, v_rect, *temp_val)
            mouse_near = [v.value for v in temp_val]    
            OpenGL.raw.GLU.gluUnProject(*mouse_pos, 1, mv_mat, p_mat, v_rect, *temp_val)
            mouse_far = [v.value for v in temp_val]
            cube(mouse_near, mouse_far)
            glPopMatrix()
            
            pygame.display.flip()
            clock.tick(100)
        except Exception as e:
            pygame.quit()
            raise e
            
    pygame.quit()
    

    Here is the parameters you will need to know if your mouse pointer is a part of a 3d object

        glGetDoublev(GL_MODELVIEW_MATRIX, mv_mat)
        glGetDoublev(GL_PROJECTION_MATRIX, p_mat)
        glGetIntegerv(GL_VIEWPORT, v_rect)
        mouse_pos = pygame.mouse.get_pos()
        mouse_pos = mouse_pos[0], v_rect[3] - mouse_pos[1]
    
    
        temp_val = [GLdouble() for _ in range(3)]
        OpenGL.raw.GLU.gluUnProject(*mouse_pos, 0, mv_mat, p_mat, v_rect, *temp_val)
        mouse_near = [v.value for v in temp_val]    
        OpenGL.raw.GLU.gluUnProject(*mouse_pos, 1, mv_mat, p_mat, v_rect, *temp_val)
        mouse_far = [v.value for v in temp_val]
    

    And here is the functions that can check that:

    import math
    
    def subtract(v0, v1):
        return [v0[0]-v1[0], v0[1]-v1[1], v0[2]-v1[2]]
        
    def dot(v0, v1):
        return v0[0]*v1[0]+v0[1]*v1[1]+v0[2]*v1[2]
    
    def length(v):
        return math.sqrt(v[0]*v[0]+v[1]*v[1]+v[2]*v[2])
    
    def normalize(v):
        l = length(v)
        return [v[0]/l, v[1]/l, v[2]/l]
    
    def isectSphere(p0, p1, C, R):
        A = p0               # origin 
        B = normalize(subtract(p1, p0)) # direction
        oc = subtract(A, C) 
        a = dot(B, B)
        b = 2 * dot(oc, B)
        c = dot(oc, oc) - R*R
        discriminant = b*b - 4*a*c
        if discriminant > 0:
            t1 = (-b - math.sqrt(discriminant)) / (2*a)
            t2 = (-b + math.sqrt(discriminant)) / (2*a)
            t = min(t1, t2)
            return t if t >= 0.0 else None
        return None
    
    def mults(v, s):
        return [v[0]*s, v[1]*s, v[2]*s]
    
    def add(v0, v1):
        return [v0[0]+v1[0], v0[1]+v1[1], v0[2]+v1[2]]
    
    def cross(v0, v1):
        return [
            v0[1]*v1[2]-v1[1]*v0[2],
            v0[2]*v1[0]-v1[2]*v0[0],
            v0[0]*v1[1]-v1[0]*v0[1]]
    
    def PointInOrOn( P1, P2, A, B ):
        CP1 = cross( subtract(B, A), subtract(P1, A) )
        CP2 = cross( subtract(B, A), subtract(P2, A) )
        return dot( CP1, CP2 ) >= 0
    
    def PointInOrOnTriangle( P, A, B, C ):
        return PointInOrOn( P, A, B, C ) and PointInOrOn( P, B, C, A ) and PointInOrOn( P, C, A, B )
    
    # p0, p1   points on ray
    # PA, PB, PC  points of the triangle
    def isectPlane(p0, p1, PA, PB, PC):
        R0 = p0               # origin 
        D = normalize(subtract(p1, p0))
        P0 = PA
        NV = normalize( cross( subtract(PB, PA), subtract(PC, PA) ) )
        dist_isect = dot( subtract(P0, R0), NV ) / dot( D, NV ) 
        P_isect    = add(R0, mults(D, dist_isect))
        return P_isect, dist_isect
    
    def isectTrianlge(p0, p1, PA, PB, PC):
        P, t = isectPlane(p0, p1, PA, PB, PC)
        if t >= 0 and PointInOrOnTriangle(P, PA, PB, PC):
            return t
        return None
    
    def PointInOrOnQuad( P, A, B, C, D ):
        return (PointInOrOn( P, A, B, C ) and PointInOrOn( P, B, C, D ) and
                PointInOrOn( P, C, D, A ) and PointInOrOn( P, D, A, B ))
    
    def isectQuad(p0, p1, PA, PB, PC, PD):
        P, t = isectPlane(p0, p1, PA, PB, PC)
        if t >= 0 and PointInOrOnQuad(P, PA, PB, PC, PD):
            return t
        return None
        
    def isectCuboid(p0, p1, pMin, pMax):
        t = None
        try:
            pl = [[pMin[0], pMin[1], pMin[2]], [pMax[0], pMin[1], pMin[2]],
                  [pMax[0], pMax[1], pMin[2]], [pMin[0], pMax[1], pMin[2]],
                  [pMin[0], pMin[1], pMax[2]], [pMax[0], pMin[1], pMax[2]],
                  [pMax[0], pMax[1], pMax[2]], [pMin[0], pMax[1], pMax[2]]]
            il = [[0, 1, 2, 3], [4, 5, 6, 7], [4, 0, 3, 7], [1, 5, 6, 2], [4, 3, 1, 0], [3, 2, 6, 7]]
            for qi in il:
                ts = isectQuad(p0, p1, pl[qi[0]], pl[qi[1]], pl[qi[2]], pl[qi[3]] )
                if ts != None and ts >= 0 and (t == None or ts < t):
                    t = ts
        except:
            t = None
        return t
    

    Note that those isect functions return the distance the interception point is from the screen