diff --git a/examples/GLBarGraphItem.py b/examples/GLBarGraphItem.py index 5d1cbcb3..094354a2 100644 --- a/examples/GLBarGraphItem.py +++ b/examples/GLBarGraphItem.py @@ -13,9 +13,9 @@ import numpy as np app = pg.mkQApp("GLBarGraphItem Example") w = gl.GLViewWidget() -w.opts['distance'] = 40 w.show() w.setWindowTitle('pyqtgraph example: GLBarGraphItem') +w.setCameraPosition(distance=40) gx = gl.GLGridItem() gx.rotate(90, 0, 1, 0) diff --git a/examples/GLImageItem.py b/examples/GLImageItem.py index 8648391c..1fc30acf 100644 --- a/examples/GLImageItem.py +++ b/examples/GLImageItem.py @@ -15,9 +15,9 @@ import numpy as np app = pg.mkQApp("GLImageItem Example") w = gl.GLViewWidget() -w.opts['distance'] = 200 w.show() w.setWindowTitle('pyqtgraph example: GLImageItem') +w.setCameraPosition(distance=200) ## create volume data set to slice three images from shape = (100,100,70) diff --git a/examples/GLLinePlotItem.py b/examples/GLLinePlotItem.py index c77034fc..9e60921e 100644 --- a/examples/GLLinePlotItem.py +++ b/examples/GLLinePlotItem.py @@ -13,9 +13,9 @@ import numpy as np app = pg.mkQApp("GLLinePlotItem Example") w = gl.GLViewWidget() -w.opts['distance'] = 40 w.show() w.setWindowTitle('pyqtgraph example: GLLinePlotItem') +w.setCameraPosition(distance=40) gx = gl.GLGridItem() gx.rotate(90, 0, 1, 0) diff --git a/examples/GLScatterPlotItem.py b/examples/GLScatterPlotItem.py index f07e0091..5326952d 100644 --- a/examples/GLScatterPlotItem.py +++ b/examples/GLScatterPlotItem.py @@ -15,9 +15,9 @@ import numpy as np app = pg.mkQApp("GLScatterPlotItem Example") w = gl.GLViewWidget() -w.opts['distance'] = 20 w.show() w.setWindowTitle('pyqtgraph example: GLScatterPlotItem') +w.setCameraPosition(distance=20) g = gl.GLGridItem() w.addItem(g) diff --git a/examples/GLViewWidget.py b/examples/GLViewWidget.py index 910fa8eb..234f605e 100644 --- a/examples/GLViewWidget.py +++ b/examples/GLViewWidget.py @@ -11,9 +11,9 @@ import pyqtgraph.opengl as gl pg.mkQApp("GLViewWidget Example") w = gl.GLViewWidget() -w.opts['distance'] = 20 w.show() w.setWindowTitle('pyqtgraph example: GLViewWidget') +w.setCameraPosition(distance=20) ax = gl.GLAxisItem() ax.setSize(5,5,5) diff --git a/examples/GLVolumeItem.py b/examples/GLVolumeItem.py index 2babe0a9..d5c83653 100644 --- a/examples/GLVolumeItem.py +++ b/examples/GLVolumeItem.py @@ -14,9 +14,9 @@ from pyqtgraph import functions as fn app = pg.mkQApp("GLVolumeItem Example") w = gl.GLViewWidget() -w.opts['distance'] = 200 w.show() w.setWindowTitle('pyqtgraph example: GLVolumeItem') +w.setCameraPosition(distance=200) g = gl.GLGridItem() g.scale(10, 10, 1) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index 7096b7b4..723b288b 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -4,6 +4,7 @@ import OpenGL.GL.framebufferobjects as glfbo import numpy as np from .. import Vector from .. import functions as fn +from .. import getConfigOption import warnings from math import cos, sin, tan, radians ##Vector = QtGui.QVector3D @@ -21,10 +22,8 @@ class GLViewWidget(QtWidgets.QOpenGLWidget): ================ ============================================================== **Arguments:** parent (QObject, optional): Parent QObject. Defaults to None. - devicePixelRatio (float, optional): High-DPI displays Qt5 should automatically - detect the correct resolution. For Qt4, specify the - ``devicePixelRatio`` argument when initializing the widget - (usually this value is 1-2). Defaults to None. + devicePixelRatio No longer in use. High-DPI displays should automatically + detect the correct resolution. rotationMethod (str): Mechanimsm to drive the rotation method, options are 'euler' and 'quaternion'. Defaults to 'euler'. ================ ============================================================== @@ -34,8 +33,8 @@ class GLViewWidget(QtWidgets.QOpenGLWidget): self.setFocusPolicy(QtCore.Qt.FocusPolicy.ClickFocus) - if rotationMethod not in {"euler", "quaternion"}: - raise RuntimeError("Rotation method should be either 'euler' or 'quaternion'") + if rotationMethod not in ["euler", "quaternion"]: + raise ValueError("Rotation method should be either 'euler' or 'quaternion'") self.opts = { 'center': Vector(0,0,0), ## will always appear at the center of the widget @@ -46,7 +45,7 @@ class GLViewWidget(QtWidgets.QOpenGLWidget): 'azimuth': 45, ## camera's azimuthal angle in degrees ## (rotation around z-axis 0 points along x-axis) 'viewport': None, ## glViewport params; None == whole widget - 'devicePixelRatio': devicePixelRatio, + ## note that 'viewport' is in device pixels 'rotationMethod': rotationMethod } self.reset() @@ -56,8 +55,6 @@ class GLViewWidget(QtWidgets.QOpenGLWidget): self.keysPressed = {} self.keyTimer = QtCore.QTimer() self.keyTimer.timeout.connect(self.evalKeyState) - self.makeCurrent() - def _updateScreen(self, screen): self._updatePixelRatio() @@ -74,14 +71,13 @@ class GLViewWidget(QtWidgets.QOpenGLWidget): window.screenChanged.connect(self._updateScreen) self._updateScreen(window.screen()) - def width(self): - dpr = self.devicePixelRatio() - return int(super().width() * dpr) - - def height(self): - dpr = self.devicePixelRatio() - return int(super().height() * dpr) + def deviceWidth(self): + dpr = self.devicePixelRatioF() + return int(self.width() * dpr) + def deviceHeight(self): + dpr = self.devicePixelRatioF() + return int(self.height() * dpr) def reset(self): """ @@ -94,20 +90,15 @@ class GLViewWidget(QtWidgets.QOpenGLWidget): self.opts['azimuth'] = 45 ## camera's azimuthal angle in degrees ## (rotation around z-axis 0 points along x-axis) self.opts['viewport'] = None ## glViewport params; None == whole widget - self.setBackgroundColor('k') + self.setBackgroundColor(getConfigOption('background')) def addItem(self, item): self.items.append(item) if self.isValid(): - self.makeCurrent() - try: - item.initialize() - except: - self.checkOpenGLVersion('Error while adding item %s to GLViewWidget.' % str(item)) + item.initialize() item._setView(self) - #print "set view", item, self, item.view() self.update() def removeItem(self, item): @@ -131,6 +122,14 @@ class GLViewWidget(QtWidgets.QOpenGLWidget): """ Initialize items that were not initialized during addItem(). """ + ctx = self.context() + fmt = ctx.format() + if ctx.isOpenGLES() or fmt.version() < (2, 0): + verString = glGetString(GL_VERSION) + raise RuntimeError( + "pyqtgraph.opengl: Requires >= OpenGL 2.0 (not ES); Found %s" % verString + ) + for item in self.items: if not item.isInitialized(): item.initialize() @@ -146,33 +145,18 @@ class GLViewWidget(QtWidgets.QOpenGLWidget): def getViewport(self): vp = self.opts['viewport'] if vp is None: - return (0, 0, self.width(), self.height()) + return (0, 0, self.deviceWidth(), self.deviceHeight()) else: - dpr = self.devicePixelRatio() - return tuple([int(x * dpr) for x in vp]) + return vp - def devicePixelRatio(self): - dpr = self.opts['devicePixelRatio'] - if dpr is not None: - return dpr - - return self.devicePixelRatioF() - - 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()) + glLoadMatrixf(m.data()) def projectionMatrix(self, region=None): if region is None: - region = (0, 0, self.width(), self.height()) + region = (0, 0, self.deviceWidth(), self.deviceHeight()) x0, y0, w, h = self.getViewport() dist = self.opts['distance'] @@ -194,11 +178,9 @@ class GLViewWidget(QtWidgets.QOpenGLWidget): return tr def setModelview(self): - glMatrixMode(GL_MODELVIEW) - glLoadIdentity() m = self.viewMatrix() - a = np.array(m.copyDataTo()).reshape((4,4)) - glMultMatrixf(a.transpose()) + glMatrixMode(GL_MODELVIEW) + glLoadMatrixf(m.data()) def viewMatrix(self): tr = QtGui.QMatrix4x4() @@ -218,7 +200,7 @@ class GLViewWidget(QtWidgets.QOpenGLWidget): 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]) + region = (region[0], self.deviceHeight()-(region[1]+region[3]), region[2], region[3]) #buf = np.zeros(100000, dtype=np.uint) buf = glSelectBuffer(100000) @@ -273,14 +255,7 @@ class GLViewWidget(QtWidgets.QOpenGLWidget): 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) + print("Error while drawing item %s." % str(item)) finally: glPopAttrib() @@ -297,21 +272,40 @@ class GLViewWidget(QtWidgets.QOpenGLWidget): glPopMatrix() def setCameraPosition(self, pos=None, distance=None, elevation=None, azimuth=None, rotation=None): + if rotation is not None: + # Alternatively, we could define that rotation overrides elevation and azimuth + if elevation is not None: + raise ValueError("cannot set both rotation and elevation") + if azimuth is not None: + raise ValueError("cannot set both rotation and azimuth") + if pos is not None: self.opts['center'] = pos if distance is not None: self.opts['distance'] = distance - if rotation is not None: - # set with quaternion - self.opts['rotation'] = rotation + + if self.opts['rotationMethod'] == "quaternion": + # note that "quaternion" mode modifies only opts['rotation'] + if elevation is not None or azimuth is not None: + eu = self.opts['rotation'].toEulerAngles() + if azimuth is not None: + eu.setZ(-azimuth-90) + if elevation is not None: + eu.setX(elevation-90) + self.opts['rotation'] = QtGui.QQuaternion.fromEulerAngles(eu) + if rotation is not None: + self.opts['rotation'] = rotation else: - # set with elevation-azimuth, restored for compatibility - eu = self.opts['rotation'].toEulerAngles() - if azimuth is not None: - eu.setZ(-azimuth-90) + # note that "euler" mode modifies only opts['elevation'] and opts['azimuth'] if elevation is not None: - eu.setX(elevation-90) - self.opts['rotation'] = QtGui.QQuaternion.fromEulerAngles(eu) + self.opts['elevation'] = elevation + if azimuth is not None: + self.opts['azimuth'] = azimuth + if rotation is not None: + eu = rotation.toEulerAngles() + self.opts['elevation'] = eu.x() + 90 + self.opts['azimuth'] = -eu.z() - 90 + self.update() def cameraPosition(self): @@ -492,7 +486,7 @@ class GLViewWidget(QtWidgets.QOpenGLWidget): return try: del self.keysPressed[ev.key()] - except: + except KeyError: self.keysPressed = {} self.evalKeyState() @@ -516,54 +510,11 @@ class GLViewWidget(QtWidgets.QOpenGLWidget): else: self.keyTimer.stop() - def checkOpenGLVersion(self, msg): - """ - Give exception additional context about version support. - - Only to be called from within exception handler. - As this check is only performed on error, - unsupported versions might still work! - """ - - # Check for unsupported version - verString = glGetString(GL_VERSION) - ver = verString.split()[0] - # If not OpenGL ES... - if str(ver.split(b'.')[0]).isdigit(): - verNumber = int(ver.split(b'.')[0]) - # ...and version is supported: - if verNumber >= 2: - # OpenGL version is fine, raise the original exception - raise - - # Print original exception - from .. import debug - debug.printExc() - - # Notify about unsupported version - raise Exception( - msg + "\n" + \ - "pyqtgraph.opengl: Requires >= OpenGL 2.0 (not ES); Found %s" % verString - ) - 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) - - pixels = pixels[::-1].copy() # flip vertical - - qimg = fn.ndarray_to_qimage(pixels, QtGui.QImage.Format.Format_RGBA8888) - return qimg + return self.grabFramebuffer() def renderToArray(self, size, format=GL_BGRA, type=GL_UNSIGNED_BYTE, textureSize=1024, padding=256): w,h = map(int, size) @@ -573,7 +524,7 @@ class GLViewWidget(QtWidgets.QOpenGLWidget): fb = None depth_buf = None try: - output = np.empty((w, h, 4), dtype=np.ubyte) + output = np.empty((h, w, 4), dtype=np.ubyte) fb = glfbo.glGenFramebuffers(1) glfbo.glBindFramebuffer(glfbo.GL_FRAMEBUFFER, fb ) @@ -586,9 +537,9 @@ class GLViewWidget(QtWidgets.QOpenGLWidget): ## 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))) + raise RuntimeError("OpenGL failed to create 2D texture (%dx%d); too large for this hardware." % data.shape[:2]) + ## create texture + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texwidth, texwidth, 0, GL_RGBA, GL_UNSIGNED_BYTE, data) # Create depth buffer depth_buf = glGenRenderbuffers(1) @@ -614,8 +565,8 @@ class GLViewWidget(QtWidgets.QOpenGLWidget): ## 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] + data = np.frombuffer(data, dtype=np.ubyte).reshape(texwidth,texwidth,4)[::-1, ...] + output[y+padding:y2-padding, x+padding:x2-padding] = data[-(h2-padding):-padding, padding:w2-padding] finally: self.opts['viewport'] = None diff --git a/pyqtgraph/opengl/items/GLImageItem.py b/pyqtgraph/opengl/items/GLImageItem.py index 7bd0ec02..dabe1c41 100644 --- a/pyqtgraph/opengl/items/GLImageItem.py +++ b/pyqtgraph/opengl/items/GLImageItem.py @@ -99,5 +99,5 @@ class GLImageItem(GLGraphicsItem): glTexCoord2f(0,1) glVertex3f(0, self.data.shape[1], 0) glEnd() - glDisable(GL_TEXTURE_3D) + glDisable(GL_TEXTURE_2D) diff --git a/pyqtgraph/opengl/items/GLTextItem.py b/pyqtgraph/opengl/items/GLTextItem.py index 59a4741d..49e4c94e 100644 --- a/pyqtgraph/opengl/items/GLTextItem.py +++ b/pyqtgraph/opengl/items/GLTextItem.py @@ -1,8 +1,8 @@ from OpenGL.GL import * import numpy as np -from pyqtgraph.Qt import QtCore, QtGui -from pyqtgraph.opengl.GLGraphicsItem import GLGraphicsItem -import pyqtgraph.functions as fn +from ...Qt import QtCore, QtGui +from .. GLGraphicsItem import GLGraphicsItem +from ... import functions as fn __all__ = ['GLTextItem'] @@ -39,7 +39,7 @@ class GLTextItem(GLGraphicsItem): args = ['pos', 'color', 'text', 'font'] for k in kwds.keys(): if k not in args: - raise ArgumentError('Invalid keyword argument: %s (allowed arguments are %s)' % (k, str(args))) + raise ValueError('Invalid keyword argument: %s (allowed arguments are %s)' % (k, str(args))) for arg in args: if arg in kwds: value = kwds[arg] @@ -65,12 +65,11 @@ class GLTextItem(GLGraphicsItem): modelview = glGetDoublev(GL_MODELVIEW_MATRIX) projection = glGetDoublev(GL_PROJECTION_MATRIX) - viewport = glGetIntegerv(GL_VIEWPORT) + viewport = [0, 0, self.view().width(), self.view().height()] text_pos = self.__project(self.pos, modelview, projection, viewport) text_pos.setY(viewport[3] - text_pos.y()) - text_pos /= self.view().devicePixelRatio() painter = QtGui.QPainter(self.view()) painter.setPen(self.color) diff --git a/pyqtgraph/widgets/RawImageWidget.py b/pyqtgraph/widgets/RawImageWidget.py index 2a12127f..f36c5cd0 100644 --- a/pyqtgraph/widgets/RawImageWidget.py +++ b/pyqtgraph/widgets/RawImageWidget.py @@ -162,4 +162,4 @@ if HAVE_OPENGL: glTexCoord2f(0, 0) glVertex3f(-1, 1, 0) glEnd() - glDisable(GL_TEXTURE_3D) + glDisable(GL_TEXTURE_2D)