Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- from ..Qt import QtCore, QtGui, QtOpenGL, USE_PYQT5
- from OpenGL.GL import *
- import OpenGL.GL.framebufferobjects as glfbo
- import numpy as np
- from .. import Vector
- from .. import functions as fn
- ##Vector = QtGui.QVector3D
- ShareWidget = None
- class GLViewWidget(QtOpenGL.QGLWidget):
- """
- Basic widget for displaying 3D data
- - Rotation/scale controls
- - Axis/grid display
- - Export options
- """
- def __init__(self, parent=None):
- global ShareWidget
- if ShareWidget is None:
- ## create a dummy widget to allow sharing objects (textures, shaders, etc) between views
- ShareWidget = QtOpenGL.QGLWidget()
- QtOpenGL.QGLWidget.__init__(self, parent, ShareWidget)
- self.setFocusPolicy(QtCore.Qt.ClickFocus)
- self.opts = {
- 'center': Vector(0,0,0), ## will always appear at the center of the widget
- 'distance': 10.0, ## distance of camera from center
- 'fov': 60, ## horizontal field of view in degrees
- 'elevation': 30, ## camera's angle of elevation in degrees
- 'azimuth': 45, ## camera's azimuthal angle in degrees
- ## (rotation around z-axis 0 points along x-axis)
- 'viewport': None, ## glViewport params; None == whole widget
- }
- self.setBackgroundColor('k')
- self.items = []
- self.noRepeatKeys = [QtCore.Qt.Key_Right, QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown]
- self.keysPressed = {}
- self.keyTimer = QtCore.QTimer()
- self.keyTimer.timeout.connect(self.evalKeyState)
- self.makeCurrent()
- def addItem(self, item):
- self.items.append(item)
- if hasattr(item, 'initializeGL'):
- self.makeCurrent()
- try:
- item.initializeGL()
- except:
- self.checkOpenGLVersion('Error while adding item %s to GLViewWidget.' % str(item))
- item._setView(self)
- #print "set view", item, self, item.view()
- self.update()
- def removeItem(self, item):
- self.items.remove(item)
- item._setView(None)
- self.update()
- def initializeGL(self):
- self.resizeGL(self.width(), self.height())
- def setBackgroundColor(self, *args, **kwds):
- """
- Set the background color of the widget. Accepts the same arguments as
- pg.mkColor() and pg.glColor().
- """
- self.opts['bgcolor'] = fn.glColor(*args, **kwds)
- self.update()
- def getViewport(self):
- vp = self.opts['viewport']
- if vp is None:
- return (0, 0, self.width(), self.height())
- else:
- return vp
- def resizeGL(self, w, h):
- pass
- #glViewport(*self.getViewport())
- #self.update()
- def setProjection(self, region=None):
- m = self.projectionMatrix(region)
- glMatrixMode(GL_PROJECTION)
- glLoadIdentity()
- a = np.array(m.copyDataTo()).reshape((4,4))
- glMultMatrixf(a.transpose())
- def projectionMatrix(self, region=None):
- # Xw = (Xnd + 1) * width/2 + X
- if region is None:
- region = (0, 0, self.width(), self.height())
- x0, y0, w, h = self.getViewport()
- dist = self.opts['distance']
- fov = self.opts['fov']
- nearClip = dist * 0.001
- farClip = dist * 1000.
- r = nearClip * np.tan(fov * 0.5 * np.pi / 180.)
- t = r * h / w
- # convert screen coordinates (region) to normalized device coordinates
- # Xnd = (Xw - X0) * 2/width - 1
- ## Note that X0 and width in these equations must be the values used in viewport
- left = r * ((region[0]-x0) * (2.0/w) - 1)
- right = r * ((region[0]+region[2]-x0) * (2.0/w) - 1)
- bottom = t * ((region[1]-y0) * (2.0/h) - 1)
- top = t * ((region[1]+region[3]-y0) * (2.0/h) - 1)
- tr = QtGui.QMatrix4x4()
- tr.frustum(left, right, bottom, top, nearClip, farClip)
- return tr
- def setModelview(self):
- glMatrixMode(GL_MODELVIEW)
- glLoadIdentity()
- m = self.viewMatrix()
- a = np.array(m.copyDataTo()).reshape((4,4))
- glMultMatrixf(a.transpose())
- def viewMatrix(self):
- tr = QtGui.QMatrix4x4()
- tr.translate( 0.0, 0.0, -self.opts['distance'])
- tr.rotate(self.opts['elevation']-90, 1, 0, 0)
- tr.rotate(self.opts['azimuth']+90, 0, 0, -1)
- center = self.opts['center']
- tr.translate(-center.x(), -center.y(), -center.z())
- return tr
- def itemsAt(self, region=None):
- """
- Return a list of the items displayed in the region (x, y, w, h)
- relative to the widget.
- """
- region = (region[0], self.height()-(region[1]+region[3]), region[2], region[3])
- #buf = np.zeros(100000, dtype=np.uint)
- buf = glSelectBuffer(100000)
- try:
- glRenderMode(GL_SELECT)
- glInitNames()
- glPushName(0)
- self._itemNames = {}
- self.paintGL(region=region, useItemNames=True)
- finally:
- hits = glRenderMode(GL_RENDER)
- items = [(h.near, h.names[0]) for h in hits]
- items.sort(key=lambda i: i[0])
- return [self._itemNames[i[1]] for i in items]
- def paintGL(self, region=None, viewport=None, useItemNames=False):
- """
- viewport specifies the arguments to glViewport. If None, then we use self.opts['viewport']
- region specifies the sub-region of self.opts['viewport'] that should be rendered.
- Note that we may use viewport != self.opts['viewport'] when exporting.
- """
- if viewport is None:
- glViewport(*self.getViewport())
- else:
- glViewport(*viewport)
- self.setProjection(region=region)
- self.setModelview()
- bgcolor = self.opts['bgcolor']
- glClearColor(*bgcolor)
- glClear( GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT )
- self.drawItemTree(useItemNames=useItemNames)
- def drawItemTree(self, item=None, useItemNames=False):
- if item is None:
- items = [x for x in self.items if x.parentItem() is None]
- else:
- items = item.childItems()
- items.append(item)
- items.sort(key=lambda a: a.depthValue())
- for i in items:
- if not i.visible():
- continue
- if i is item:
- try:
- glPushAttrib(GL_ALL_ATTRIB_BITS)
- if useItemNames:
- glLoadName(i._id)
- self._itemNames[i._id] = i
- i.paint()
- except:
- from .. import debug
- debug.printExc()
- msg = "Error while drawing item %s." % str(item)
- ver = glGetString(GL_VERSION)
- if ver is not None:
- ver = ver.split()[0]
- if int(ver.split(b'.')[0]) < 2:
- print(msg + " The original exception is printed above; however, pyqtgraph requires OpenGL version 2.0 or greater for many of its 3D features and your OpenGL version is %s. Installing updated display drivers may resolve this issue." % ver)
- else:
- print(msg)
- finally:
- glPopAttrib()
- else:
- glMatrixMode(GL_MODELVIEW)
- glPushMatrix()
- try:
- tr = i.transform()
- a = np.array(tr.copyDataTo()).reshape((4,4))
- glMultMatrixf(a.transpose())
- self.drawItemTree(i, useItemNames=useItemNames)
- finally:
- glMatrixMode(GL_MODELVIEW)
- glPopMatrix()
- def setCameraPosition(self, pos=None, distance=None, elevation=None, azimuth=None):
- if distance is not None:
- self.opts['distance'] = distance
- if elevation is not None:
- self.opts['elevation'] = elevation
- if azimuth is not None:
- self.opts['azimuth'] = azimuth
- self.update()
- def cameraPosition(self):
- """Return current position of camera based on center, dist, elevation, and azimuth"""
- center = self.opts['center']
- dist = self.opts['distance']
- elev = self.opts['elevation'] * np.pi/180.
- azim = self.opts['azimuth'] * np.pi/180.
- pos = Vector(
- center.x() + dist * np.cos(elev) * np.cos(azim),
- center.y() + dist * np.cos(elev) * np.sin(azim),
- center.z() + dist * np.sin(elev)
- )
- return pos
- def orbit(self, azim, elev):
- """Orbits the camera around the center position. *azim* and *elev* are given in degrees."""
- self.opts['azimuth'] += azim
- #self.opts['elevation'] += elev
- self.opts['elevation'] = np.clip(self.opts['elevation'] + elev, -90, 90)
- self.update()
- def pan(self, dx, dy, dz, relative=False):
- """
- Moves the center (look-at) position while holding the camera in place.
- If relative=True, then the coordinates are interpreted such that x
- if in the global xy plane and points to the right side of the view, y is
- in the global xy plane and orthogonal to x, and z points in the global z
- direction. Distances are scaled roughly such that a value of 1.0 moves
- by one pixel on screen.
- """
- if not relative:
- self.opts['center'] += QtGui.QVector3D(dx, dy, dz)
- else:
- cPos = self.cameraPosition()
- cVec = self.opts['center'] - cPos
- dist = cVec.length() ## distance from camera to center
- xDist = dist * 2. * np.tan(0.5 * self.opts['fov'] * np.pi / 180.) ## approx. width of view at distance of center point
- xScale = xDist / self.width()
- zVec = QtGui.QVector3D(0,0,1)
- xVec = QtGui.QVector3D.crossProduct(zVec, cVec).normalized()
- yVec = QtGui.QVector3D.crossProduct(xVec, zVec).normalized()
- self.opts['center'] = self.opts['center'] + xVec * xScale * dx + yVec * xScale * dy + zVec * xScale * dz
- self.update()
- def pixelSize(self, pos):
- """
- Return the approximate size of a screen pixel at the location pos
- Pos may be a Vector or an (N,3) array of locations
- """
- cam = self.cameraPosition()
- if isinstance(pos, np.ndarray):
- cam = np.array(cam).reshape((1,)*(pos.ndim-1)+(3,))
- dist = ((pos-cam)**2).sum(axis=-1)**0.5
- else:
- dist = (pos-cam).length()
- xDist = dist * 2. * np.tan(0.5 * self.opts['fov'] * np.pi / 180.)
- return xDist / self.width()
- def mousePressEvent(self, ev):
- self.mousePos = ev.pos()
- def mouseMoveEvent(self, ev):
- diff = ev.pos() - self.mousePos
- self.mousePos = ev.pos()
- if ev.buttons() == QtCore.Qt.LeftButton:
- self.orbit(-diff.x(), diff.y())
- #print self.opts['azimuth'], self.opts['elevation']
- elif ev.buttons() == QtCore.Qt.MidButton:
- if (ev.modifiers() & QtCore.Qt.ControlModifier):
- self.pan(diff.x(), 0, diff.y(), relative=True)
- else:
- self.pan(diff.x(), diff.y(), 0, relative=True)
- def mouseReleaseEvent(self, ev):
- pass
- # Example item selection code:
- #region = (ev.pos().x()-5, ev.pos().y()-5, 10, 10)
- #print(self.itemsAt(region))
- ## debugging code: draw the picking region
- #glViewport(*self.getViewport())
- #glClear( GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT )
- #region = (region[0], self.height()-(region[1]+region[3]), region[2], region[3])
- #self.paintGL(region=region)
- #self.swapBuffers()
- def wheelEvent(self, ev):
- delta = 0
- if not USE_PYQT5:
- delta = ev.delta()
- else:
- delta = ev.angleDelta().x()
- if delta == 0:
- delta = ev.angleDelta().y()
- if (ev.modifiers() & QtCore.Qt.ControlModifier):
- self.opts['fov'] *= 0.999**delta
- else:
- self.opts['distance'] *= 0.999**delta
- self.update()
- def keyPressEvent(self, ev):
- if ev.key() in self.noRepeatKeys:
- ev.accept()
- if ev.isAutoRepeat():
- return
- self.keysPressed[ev.key()] = 1
- self.evalKeyState()
- def keyReleaseEvent(self, ev):
- if ev.key() in self.noRepeatKeys:
- ev.accept()
- if ev.isAutoRepeat():
- return
- try:
- del self.keysPressed[ev.key()]
- except:
- self.keysPressed = {}
- self.evalKeyState()
- def evalKeyState(self):
- speed = 2.0
- if len(self.keysPressed) > 0:
- for key in self.keysPressed:
- if key == QtCore.Qt.Key_Right:
- self.orbit(azim=-speed, elev=0)
- elif key == QtCore.Qt.Key_Left:
- self.orbit(azim=speed, elev=0)
- elif key == QtCore.Qt.Key_Up:
- self.orbit(azim=0, elev=-speed)
- elif key == QtCore.Qt.Key_Down:
- self.orbit(azim=0, elev=speed)
- elif key == QtCore.Qt.Key_PageUp:
- pass
- elif key == QtCore.Qt.Key_PageDown:
- pass
- self.keyTimer.start(16)
- else:
- self.keyTimer.stop()
- def checkOpenGLVersion(self, msg):
- ## Only to be called from within exception handler.
- ver = glGetString(GL_VERSION).split()[0]
- if int(ver.split('.')[0]) < 2:
- from .. import debug
- pyqtgraph.debug.printExc()
- raise Exception(msg + " The original exception is printed above; however, pyqtgraph requires OpenGL version 2.0 or greater for many of its 3D features and your OpenGL version is %s. Installing updated display drivers may resolve this issue." % ver)
- else:
- raise
- def readQImage(self):
- """
- Read the current buffer pixels out as a QImage.
- """
- w = self.width()
- h = self.height()
- self.repaint()
- pixels = np.empty((h, w, 4), dtype=np.ubyte)
- pixels[:] = 128
- pixels[...,0] = 50
- pixels[...,3] = 255
- glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, pixels)
- # swap B,R channels for Qt
- tmp = pixels[...,0].copy()
- pixels[...,0] = pixels[...,2]
- pixels[...,2] = tmp
- pixels = pixels[::-1] # flip vertical
- img = fn.makeQImage(pixels, transpose=False)
- return img
- def renderToArray(self, size, format=GL_BGRA, type=GL_UNSIGNED_BYTE, textureSize=1024, padding=256):
- w,h = map(int, size)
- self.makeCurrent()
- tex = None
- fb = None
- try:
- output = np.empty((w, h, 4), dtype=np.ubyte)
- fb = glfbo.glGenFramebuffers(1)
- glfbo.glBindFramebuffer(glfbo.GL_FRAMEBUFFER, fb )
- glEnable(GL_TEXTURE_2D)
- tex = glGenTextures(1)
- glBindTexture(GL_TEXTURE_2D, tex)
- texwidth = textureSize
- data = np.zeros((texwidth,texwidth,4), dtype=np.ubyte)
- ## Test texture dimensions first
- glTexImage2D(GL_PROXY_TEXTURE_2D, 0, GL_RGBA, texwidth, texwidth, 0, GL_RGBA, GL_UNSIGNED_BYTE, None)
- if glGetTexLevelParameteriv(GL_PROXY_TEXTURE_2D, 0, GL_TEXTURE_WIDTH) == 0:
- raise Exception("OpenGL failed to create 2D texture (%dx%d); too large for this hardware." % shape[:2])
- ## create teture
- glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texwidth, texwidth, 0, GL_RGBA, GL_UNSIGNED_BYTE, data.transpose((1,0,2)))
- self.opts['viewport'] = (0, 0, w, h) # viewport is the complete image; this ensures that paintGL(region=...)
- # is interpreted correctly.
- p2 = 2 * padding
- for x in range(-padding, w-padding, texwidth-p2):
- for y in range(-padding, h-padding, texwidth-p2):
- x2 = min(x+texwidth, w+padding)
- y2 = min(y+texwidth, h+padding)
- w2 = x2-x
- h2 = y2-y
- ## render to texture
- glfbo.glFramebufferTexture2D(glfbo.GL_FRAMEBUFFER, glfbo.GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0)
- self.paintGL(region=(x, h-y-h2, w2, h2), viewport=(0, 0, w2, h2)) # only render sub-region
- ## read texture back to array
- data = glGetTexImage(GL_TEXTURE_2D, 0, format, type)
- data = np.fromstring(data, dtype=np.ubyte).reshape(texwidth,texwidth,4).transpose(1,0,2)[:, ::-1]
- output[x+padding:x2-padding, y+padding:y2-padding] = data[padding:w2-padding, -(h2-padding):-padding]
- finally:
- self.opts['viewport'] = None
- glfbo.glBindFramebuffer(glfbo.GL_FRAMEBUFFER, 0)
- glBindTexture(GL_TEXTURE_2D, 0)
- if tex is not None:
- glDeleteTextures([tex])
- if fb is not None:
- glfbo.glDeleteFramebuffers([fb])
- return output
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement