vlachoudis / bCNC

GRBL CNC command sender, autoleveler and g-code editor
GNU General Public License v2.0
1.56k stars 532 forks source link

OpenGL support: Enable 3D view to be freely rotated using mouse #591

Open Harvie opened 7 years ago

Harvie commented 7 years ago

It can be really usefull to be able to freely rotate 3D view by mouse draging. The predefined ISO1-3 views are kinda doing the job, but it helps to see objects in slight movement to get faster grasp of their shapes.

vlachoudis commented 7 years ago

I am doing the 3D rendering manually (not using any 3d library) and python is not very fast on that. I could potentially do it but it wont be interactive.

Harvie commented 7 years ago

Is there reason for that? I guess there are well tested OpenGL bindings for python, or something like that...

vlachoudis commented 7 years ago

Yes, bCNC is not using OpenGL, it uses the minimum on external dependencies.

Harvie commented 7 years ago

I know, but is there sensible reason for that? This "not invented here" approach is not always that wise. Especialy when it not only lacks basic rotating functionality, but you obviously run to performance problems (timeouting renders), etc...

BTW i've made linux packages for like hundred programs, so i know how annoying external dependencies can be. But sometimes it makes sense to use few external libs. Especialy when they are widely common and already packaged. It's about some kind of balance. You don't want dependency hell of 100 external libs, but using something as common as opengl for 3d rendering makes some sense...

vlachoudis commented 7 years ago

I would be more than happy if you could help in adding OpenGL as a rendered.

Harvie commented 6 years ago

Do you know which methods need to be ported to OpenGL?

Harvie commented 6 years ago

Just accidentaly found this: https://docs.enthought.com/mayavi/mayavi/ Not sure if that can be reused as display in bCNC...

Harvie commented 6 years ago

VisPy seems to have all we need, but i can't figure out how to integrate it to the tkinter window :-(

http://vispy.org https://github.com/vispy/vispy https://glumpy.github.io/ https://github.com/glumpy/glumpy https://kivy.org/doc/stable/examples/gen__3Drendering__main__py.html

Harvie commented 6 years ago

Maybe GL glue can put this together? https://github.com/ousttrue/glglue

Maybe GLUT or PyGame?

Harvie commented 6 years ago

Seems to me that Tkinter is horrible choice for writing GL applications. We'll have to reinvent some basics to get it working.

Harvie commented 6 years ago

https://github.com/jonwright/pyopengltk

Harvie commented 6 years ago

Basicaly only way of having OpenGL without abandoning Tkinter is using some 10 years old library called Togl, which is b*tch to install and distribute. Such a PITA.

http://pyopengl.sourceforge.net/pydoc/OpenGL.Tk.html says:

Traditional PyOpenGL interface to Togl

This module provides access to the Tkinter Togl widget with a relatively "thick" wrapper API that creates a set of default "examination" operations.

Note that many (most) Linux distributions have now split out the Tkinter bindings into a separate package, and that Togl must be separately downloaded (a script is provided in the source distribution which downloads and installs Togl 2.0 binaries for you).

Because of the complexity and fragility of the installation, it is recommended that you use Pygame, wxPython, PyGtk, or PyQt for real-world projects, and GLUT or Pygame for simple demo/testing interfaces.

The Togl project is located here: http://togl.sourceforge.net/

Harvie commented 6 years ago

https://stackoverflow.com/questions/8584272/using-pygame-features-in-tkinter This seems to enable us to embed pygame into tkinter. Probably best we can get with tkinter.

import Tkinter as tk
import os

w, h = 500, 200

# Add a couple widgets. We're going to put pygame in `embed`.
root = tk.Tk()
embed = tk.Frame(root, width=w, height=h)
embed.pack()
text = tk.Button(root, text='Blah.')
text.pack()

# Tell pygame's SDL window which window ID to use    
os.environ['SDL_WINDOWID'] = str(embed.winfo_id())

# The wxPython wiki says you might need the following line on Windows
# (http://wiki.wxpython.org/IntegratingPyGame).
#os.environ['SDL_VIDEODRIVER'] = 'windib'

# Show the window so it's assigned an ID.
root.update()

# Usual pygame initialization
import pygame as pg
pg.display.init()
screen = pg.display.set_mode((w,h))

pos = 0
while 1:
    # Do some pygame stuff
    screen.fill(pg.Color(0,0,0))
    pos = (pos + 1) % screen.get_width()
    pg.draw.circle(screen, pg.Color(255,255,255), (pos,100), 30)

    # Update the pygame display
    pg.display.flip()

    # Update the Tk display
    root.update()
Harvie commented 6 years ago

That basicaly means, you can include any SDL or PyGame application into Tkinter Frame by setting env SDL_WINDOWID to Frame.winfo_id()

Eg.: here i received the window id of bCNC canvas using print("canvas wid",self.canvasFrame.canvas.winfo_id()) and then launched simple SDL game with that ID, which resulted in embeding of that game into bCNC. However mouse/keyboard inputs are still going to bCNC, so the game is unplayable.

image

Also i am not sure, if this will work on windows or mac.

Harvie commented 6 years ago

Does the Tkinter canvas has to be slow? This seems to be relatively fast:

https://www.youtube.com/watch?v=rkNToAMcIcs

But it does not have antialiasing, which sucks...

Harvie commented 6 years ago

This does realtime tkinter canvas rotation using mouse and does not seem too slow in my opinion:

https://github.com/ActiveState/code/tree/master/recipes/Python/578876_Rotatable_Tetrahedron http://code.activestate.com/recipes/578876-rotatable-tetrahedron/

#!/usr/bin/env python
# coding: UTF-8
#
## @package _12_tet
#
#  Draws a 3D tetrahedron and allows a user to rotate it
#  (mouse left button and wheel).
#
#  The Tetrahedron is represented by a 3 x 4 matrix. 
#  Each column represents a 3D vertex.
#
#  note: a m x n matrix is represented by a list of lines:
#     [[l_1] [l_2] .. [l_m]].
#  m = len(mat), n = len(mat[0]), mat(i,j) = mat[i][j]
#
#  @author Paulo Roma
#  @since 01/05/2014
#  @see http://www.orimosenzon.com/wiki/index.php/Python_samples
#  @see http://mathworld.wolfram.com/RotationMatrix.html

try:
   from tkinter import *     # python 3
except ImportError:
   from Tkinter import *     # python 2
from math import *

def createZeroMat(m,n):
    """Return a matrix (m x n) filled with zeros."""

    ret = [0] * m
    for i in range(m):
        ret[i] = [0] * n
    return ret   

def matMul(mat1, mat2):
    """Return mat1 x mat2 (mat1 multiplied by mat2)."""

    m = len(mat1)
    n = len(mat2[0])
    common = len(mat2)

    ret = createZeroMat(m,n)
    if  len(mat1[0]) == len(mat2):
      for i in range(m):
          for j in range(n):
              for k in range(common):
                  ret[i][j] += mat1[i][k] * mat2[k][j]
    return ret

def matTrans(mat):
    """Return mat (n x m) transposed (m x n)."""

    m = len(mat[0])
    n = len(mat)

    ret = createZeroMat(m,n)
    for i in range(m):
        for j in range(n):
            ret[i][j] = mat[j][i]
    return ret

def translate(x,y,dx,dy):
    """Translate vector(x,y) by (dx,dy)."""

    return x+dx, y+dy

def drawTet(tet,col):
    """Draw a tetrahedron."""

    w = canvas.winfo_width()/2
    h = canvas.winfo_height()/2
    canvas.delete(ALL) # delete all edges
    nv = len(tet[0])   # number of vertices in tet (4)

    # draw the 6 edges of the tetrahedron
    for p1 in range(nv):
        for p2 in range(p1+1,nv):
            canvas.create_line(translate(tet[0][p1], tet[1][p1], w, h),
                               translate(tet[0][p2], tet[1][p2], w, h), fill = col)

def init():
    """Initialize global variables."""

    global ROT_X, ROT_Y, ROT_Z
    global eps, EPS, tet
    global lastX, lastY, tetColor, bgColor

    tet = matTrans([[0,-100,0],[-100,100,0],[100,100,0],[0,0,200]])

    # counter-clockwise rotation about the X axis
    ROT_X = lambda x: matTrans([[1,0,0],           [0,cos(x),-sin(x)], [0,sin(x),cos(x)] ])

    # counter-clockwise rotation about the Y axis
    ROT_Y = lambda y: matTrans([[cos(y),0,sin(y)], [0,1,0],            [-sin(y),0,cos(y)]])

    # counter-clockwise rotation about the Z axis
    ROT_Z = lambda z: matTrans([[cos(z),sin(z),0], [-sin(z),cos(z),0], [0,0,1]])

    eps = lambda d: pi/300 if (d>0) else -pi/300
    EPS = lambda d: d*pi/300

    lastX = 0 
    lastY = 0
    tetColor = 'black'
    bgColor = 'white'

def cbClicked(event):
    """Save current mouse position."""

    global lastX, lastY

    lastX = event.x
    lastY = event.y

def cbMottion(event):
    """Map mouse displacements in Y direction to rotations about X axis,
       and mouse displacements in X direction to rotations about Y axis.""" 

    global tet

    # Y coordinate is upside down
    dx = lastY - event.y 
    tet = matMul(ROT_X(EPS(-dx)),tet)

    dy = lastX - event.x
    tet = matMul(ROT_Y(EPS(dy)),tet)

    drawTet(tet,tetColor)
    cbClicked(event)   

def wheelUp(event):
    """Map mouse wheel up displacements to rotations about Z axis."""

    global tet
    tet = matMul(ROT_Z(EPS(1)),tet)
    drawTet(tet,tetColor)

def wheelDown(event):
    """Map mouse wheel down displacements to rotations about Z axis."""

    global tet
    tet = matMul(ROT_Z(EPS(-1)),tet)
    drawTet(tet,tetColor)

def wheel(event):
    """Map mouse wheel displacements to rotations about Z axis."""

    global tet
    tet = matMul(ROT_Z(EPS(event.delta/120)),tet)
    drawTet(tet,tetColor)

def resize(event):
    """Redraw the tetrahedron, in case of a window change due to user resizing it.""" 

    drawTet(tet,tetColor)

def main():
    global canvas
    root = Tk()
    root.title('Tetrahedron')
    root.geometry('+0+0')

    init()

    canvas = Canvas(root, width=400, height=400, background=bgColor)
    canvas.pack(fill=BOTH,expand=YES)               
    canvas.bind("<Button-1>", cbClicked)
    canvas.bind("<B1-Motion>", cbMottion)
    canvas.bind("<Configure>", resize)

    from platform import uname
    os = uname()[0]
    if ( os == "Linux" ):
         canvas.bind('<Button-4>', wheelUp)      # X11
         canvas.bind('<Button-5>', wheelDown)
    elif ( os == "Darwin" ):
         canvas.bind('<MouseWheel>', wheel)      # MacOS
    else: 
         canvas.bind_all('<MouseWheel>', wheel)  # windows

    drawTet(tet,tetColor)

    mainloop()

if __name__=='__main__':
    sys.exit(main())
Harvie commented 6 years ago

Just tried add more 3D lines to that code. It seems to be reasonably fast when working with 10000 3D lines, and i would say that up to 3000 lines it runs almost smoothly. But still... No way to get antialiasing :-(

Harvie commented 6 years ago

Another solution may be to rewrite bCNC to GTK3 (PyGTK) or wxWidgets (wxPython), which are more up to date than Tkinter and have no troubles with OpenGL. But that would be at least labour intensive.

Harvie commented 6 years ago

Just tried this and it looks very promissing:

https://github.com/jonwright/pyopengltk

I have OpenGL context in Tkinter window without any hacks!

image

Guenni75 commented 5 years ago

Looks very promising!! Good work.

Harvie commented 2 years ago

https://github.com/glumpy/glumpy/blob/master/examples/gloo-picking.py