From 872fcb17fffb7e355e780d8813f40478658aa7fb Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Tue, 6 Mar 2012 01:22:02 -0500 Subject: [PATCH] Added basic OpenGL scenegraph system - rotate/scalable view widget - volumetric data item --- opengl/GLGraphicsItem.py | 60 +++++++++++ opengl/GLViewWidget.py | 199 +++++++++++++++++++++++++++++++++++ opengl/__init__.py | 23 ++++ opengl/items/GLBoxItem.py | 47 +++++++++ opengl/items/GLVolumeItem.py | 179 +++++++++++++++++++++++++++++++ opengl/items/__init__.py | 0 6 files changed, 508 insertions(+) create mode 100644 opengl/GLGraphicsItem.py create mode 100644 opengl/GLViewWidget.py create mode 100644 opengl/__init__.py create mode 100644 opengl/items/GLBoxItem.py create mode 100644 opengl/items/GLVolumeItem.py create mode 100644 opengl/items/__init__.py diff --git a/opengl/GLGraphicsItem.py b/opengl/GLGraphicsItem.py new file mode 100644 index 00000000..7baa3b7e --- /dev/null +++ b/opengl/GLGraphicsItem.py @@ -0,0 +1,60 @@ +from pyqtgraph.Qt import QtGui, QtCore + +class GLGraphicsItem(QtCore.QObject): + def __init__(self, parentItem=None): + QtCore.QObject.__init__(self) + self.__parent = None + self.__view = None + self.__children = set() + self.setParentItem(parentItem) + self.setDepthValue(0) + + def setParentItem(self, item): + if self.__parent is not None: + self.__parent.__children.remove(self) + if item is not None: + item.__children.add(self) + self.__parent = item + + def parentItem(self): + return self.__parent + + def childItems(self): + return list(self.__children) + + def _setView(self, v): + self.__view = v + + def view(self): + return self.__view + + def setDepthValue(self, value): + """ + Sets the depth value of this item. Default is 0. + This controls the order in which items are drawn--those with a greater depth value will be drawn later. + Items with negative depth values are drawn before their parent. + (This is analogous to QGraphicsItem.zValue) + The depthValue does NOT affect the position of the item or the values it imparts to the GL depth buffer. + '""" + self.__depthValue = value + + def depthValue(self): + """Return the depth value of this item. See setDepthValue for mode information.""" + return self.__depthValue + + def initializeGL(self): + """ + Called after an item is added to a GLViewWidget. + The widget's GL context is made current before this method is called. + (So this would be an appropriate time to generate lists, upload textures, etc.) + """ + pass + + def paint(self): + """ + Called by the GLViewWidget to draw this item. + It is the responsibility of the item to set up its own modelview matrix, + but the caller will take care of pushing/popping. + """ + pass + \ No newline at end of file diff --git a/opengl/GLViewWidget.py b/opengl/GLViewWidget.py new file mode 100644 index 00000000..ae579003 --- /dev/null +++ b/opengl/GLViewWidget.py @@ -0,0 +1,199 @@ +from pyqtgraph.Qt import QtCore, QtGui, QtOpenGL +from OpenGL.GL import * +import numpy as np + +Vector = QtGui.QVector3D + +class GLViewWidget(QtOpenGL.QGLWidget): + """ + Basic widget for displaying 3D data + - Rotation/scale controls + - Axis/grid display + - Export options + + """ + def __init__(self, parent=None): + QtOpenGL.QGLWidget.__init__(self, parent) + 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) + } + self.items = [] + + def addItem(self, item): + self.items.append(item) + if hasattr(item, 'initializeGL'): + self.makeCurrent() + item.initializeGL() + item._setView(self) + #print "set view", item, self, item.view() + self.updateGL() + + def initializeGL(self): + glClearColor(0.0, 0.0, 0.0, 0.0) + glEnable(GL_DEPTH_TEST) + + glEnable( GL_ALPHA_TEST ) + self.resizeGL(self.width(), self.height()) + self.generateAxes() + #self.generatePoints() + + def resizeGL(self, w, h): + glViewport(0, 0, w, h) + #self.updateGL() + + def setProjection(self): + ## Create the projection matrix + glMatrixMode(GL_PROJECTION) + glLoadIdentity() + w = self.width() + h = self.height() + dist = self.opts['distance'] + fov = self.opts['fov'] + + nearClip = dist * 0.001 + farClip = dist * 1000. + + r = nearClip * np.tan(fov) + t = r * h / w + glFrustum( -r, r, -t, t, nearClip, farClip) + + def setModelview(self): + glMatrixMode(GL_MODELVIEW) + glLoadIdentity() + glTranslatef( 0.0, 0.0, -self.opts['distance']) + glRotatef(self.opts['elevation']-90, 1, 0, 0) + glRotatef(self.opts['azimuth']+90, 0, 0, -1) + center = self.opts['center'] + glTranslatef(center.x(), center.y(), center.z()) + + + def paintGL(self): + self.setProjection() + self.setModelview() + + glClear( GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT ) + glDisable( GL_DEPTH_TEST ) + #print "draw list:", self.axisList + glCallList(self.axisList) ## draw axes + #glCallList(self.pointList) + #self.drawPoints() + #self.drawAxes() + + self.drawItemTree() + + def drawItemTree(self, item=None): + 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(lambda a,b: cmp(a.depthValue(), b.depthValue())) + for i in items: + if i is item: + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + i.paint() + glMatrixMode(GL_MODELVIEW) + glPopMatrix() + else: + self.drawItemTree(i) + + + 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 generateAxes(self): + self.axisList = glGenLists(1) + glNewList(self.axisList, GL_COMPILE) + + #glShadeModel(GL_FLAT) + #glFrontFace(GL_CCW) + #glEnable( GL_LIGHT_MODEL_TWO_SIDE ) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + glEnable( GL_BLEND ) + glEnable( GL_ALPHA_TEST ) + #glAlphaFunc( GL_ALWAYS,0.5 ) + glEnable( GL_POINT_SMOOTH ) + glDisable( GL_DEPTH_TEST ) + glBegin( GL_LINES ) + + glColor4f(1, 1, 1, .3) + for x in range(-10, 11): + glVertex3f(x, -10, 0) + glVertex3f(x, 10, 0) + for y in range(-10, 11): + glVertex3f(-10, y, 0) + glVertex3f( 10, y, 0) + + + glColor4f(0, 1, 0, .6) # z is green + glVertex3f(0, 0, 0) + glVertex3f(0, 0, 5) + + glColor4f(1, 1, 0, .6) # y is yellow + glVertex3f(0, 0, 0) + glVertex3f(0, 5, 0) + + glColor4f(0, 0, 1, .6) # x is blue + glVertex3f(0, 0, 0) + glVertex3f(5, 0, 0) + glEnd() + glEndList() + + def generatePoints(self): + self.pointList = glGenLists(1) + glNewList(self.pointList, GL_COMPILE) + width = 7 + alpha = 0.02 + n = 40 + glPointSize( width ) + glBegin(GL_POINTS) + for x in range(-n, n+1): + r = (n-x)/(2.*n) + glColor4f(r, r, r, alpha) + for y in range(-n, n+1): + for z in range(-n, n+1): + glVertex3f(x, y, z) + glEnd() + glEndList() + + + def mousePressEvent(self, ev): + self.mousePos = ev.pos() + + def mouseMoveEvent(self, ev): + diff = ev.pos() - self.mousePos + self.mousePos = ev.pos() + self.opts['azimuth'] -= diff.x() + self.opts['elevation'] = np.clip(self.opts['elevation'] + diff.y(), -90, 90) + #print self.opts['azimuth'], self.opts['elevation'] + self.updateGL() + + def mouseReleaseEvent(self, ev): + pass + + def wheelEvent(self, ev): + self.opts['distance'] *= 0.999**ev.delta() + self.updateGL() + + + diff --git a/opengl/__init__.py b/opengl/__init__.py new file mode 100644 index 00000000..3d501e9d --- /dev/null +++ b/opengl/__init__.py @@ -0,0 +1,23 @@ +from GLViewWidget import GLViewWidget + +import os +def importAll(path): + d = os.path.join(os.path.split(__file__)[0], path) + files = [] + for f in os.listdir(d): + if os.path.isdir(os.path.join(d, f)): + files.append(f) + elif f[-3:] == '.py' and f != '__init__.py': + files.append(f[:-3]) + + for modName in files: + mod = __import__(path+"."+modName, globals(), locals(), fromlist=['*']) + if hasattr(mod, '__all__'): + names = mod.__all__ + else: + names = [n for n in dir(mod) if n[0] != '_'] + for k in names: + if hasattr(mod, k): + globals()[k] = getattr(mod, k) + +importAll('items') diff --git a/opengl/items/GLBoxItem.py b/opengl/items/GLBoxItem.py new file mode 100644 index 00000000..ffaa6861 --- /dev/null +++ b/opengl/items/GLBoxItem.py @@ -0,0 +1,47 @@ +from OpenGL.GL import * +from .. GLGraphicsItem import GLGraphicsItem + +__all__ = ['GLBoxItem'] + +class GLBoxItem(GLGraphicsItem): + def paint(self): + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + glEnable( GL_BLEND ) + glEnable( GL_ALPHA_TEST ) + #glAlphaFunc( GL_ALWAYS,0.5 ) + glEnable( GL_POINT_SMOOTH ) + glDisable( GL_DEPTH_TEST ) + glBegin( GL_LINES ) + + glColor4f(1, 1, 1, .3) + w = 10 + glVertex3f(-w, -w, -w) + glVertex3f(-w, -w, w) + glVertex3f( w, -w, -w) + glVertex3f( w, -w, w) + glVertex3f(-w, w, -w) + glVertex3f(-w, w, w) + glVertex3f( w, w, -w) + glVertex3f( w, w, w) + + glVertex3f(-w, -w, -w) + glVertex3f(-w, w, -w) + glVertex3f( w, -w, -w) + glVertex3f( w, w, -w) + glVertex3f(-w, -w, w) + glVertex3f(-w, w, w) + glVertex3f( w, -w, w) + glVertex3f( w, w, w) + + glVertex3f(-w, -w, -w) + glVertex3f( w, -w, -w) + glVertex3f(-w, w, -w) + glVertex3f( w, w, -w) + glVertex3f(-w, -w, w) + glVertex3f( w, -w, w) + glVertex3f(-w, w, w) + glVertex3f( w, w, w) + + glEnd() + + \ No newline at end of file diff --git a/opengl/items/GLVolumeItem.py b/opengl/items/GLVolumeItem.py new file mode 100644 index 00000000..548cd2bd --- /dev/null +++ b/opengl/items/GLVolumeItem.py @@ -0,0 +1,179 @@ +from OpenGL.GL import * +from .. GLGraphicsItem import GLGraphicsItem +from pyqtgraph.Qt import QtGui +import numpy as np + +__all__ = ['GLVolumeItem'] + +class GLVolumeItem(GLGraphicsItem): + def initializeGL(self): + n = 128 + self.data = np.random.randint(0, 255, size=4*n**3).astype(np.uint8).reshape((n,n,n,4)) + self.data[...,3] *= 0.1 + for i in range(n): + self.data[i,:,:,0] = i*256./n + glEnable(GL_TEXTURE_3D) + self.texture = glGenTextures(1) + glBindTexture(GL_TEXTURE_3D, self.texture) + #glTexImage3D( GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLenum format, GLenum type, void *data ); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER) + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER) + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_BORDER) + #glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_BORDER_COLOR, ) ## black/transparent by default + glTexImage3D(GL_TEXTURE_3D, 0, GL_RGBA, n, n, n, 0, GL_RGBA, GL_UNSIGNED_BYTE, self.data) + glDisable(GL_TEXTURE_3D) + + self.lists = {} + for ax in [0,1,2]: + for d in [-1, 1]: + l = glGenLists(1) + self.lists[(ax,d)] = l + glNewList(l, GL_COMPILE) + self.drawVolume(ax, d) + glEndList() + + + def paint(self): + + glEnable(GL_TEXTURE_3D) + glBindTexture(GL_TEXTURE_3D, self.texture) + + glDisable(GL_DEPTH_TEST) + #glDisable(GL_CULL_FACE) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + glEnable( GL_BLEND ) + glEnable( GL_ALPHA_TEST ) + + view = self.view() + cam = view.cameraPosition() + cam = np.array([cam.x(), cam.y(), cam.z()]) + ax = np.argmax(abs(cam)) + d = 1 if cam[ax] > 0 else -1 + glCallList(self.lists[(ax,d)]) ## draw axes + glDisable(GL_TEXTURE_3D) + + def drawVolume(self, ax, d): + slices = 256 + N = 5 + + imax = [0,1,2] + imax.remove(ax) + + tp = [[0,0,0],[0,0,0],[0,0,0],[0,0,0]] + vp = [[0,0,0],[0,0,0],[0,0,0],[0,0,0]] + tp[0][imax[0]] = 0 + tp[0][imax[1]] = 0 + tp[1][imax[0]] = 1 + tp[1][imax[1]] = 0 + tp[2][imax[0]] = 1 + tp[2][imax[1]] = 1 + tp[3][imax[0]] = 0 + tp[3][imax[1]] = 1 + + vp[0][imax[0]] = -N + vp[0][imax[1]] = -N + vp[1][imax[0]] = N + vp[1][imax[1]] = -N + vp[2][imax[0]] = N + vp[2][imax[1]] = N + vp[3][imax[0]] = -N + vp[3][imax[1]] = N + r = range(slices) + if d == -1: + r = r[::-1] + + glBegin(GL_QUADS) + for i in r: + z = float(i)/(slices-1.) + w = float(i)*10./(slices-1.) - 5. + + tp[0][ax] = z + tp[1][ax] = z + tp[2][ax] = z + tp[3][ax] = z + + vp[0][ax] = w + vp[1][ax] = w + vp[2][ax] = w + vp[3][ax] = w + + + glTexCoord3f(*tp[0]) + glVertex3f(*vp[0]) + glTexCoord3f(*tp[1]) + glVertex3f(*vp[1]) + glTexCoord3f(*tp[2]) + glVertex3f(*vp[2]) + glTexCoord3f(*tp[3]) + glVertex3f(*vp[3]) + glEnd() + + + + + + + + + + ## Interesting idea: + ## remove projection/modelview matrixes, recreate in texture coords. + ## it _sorta_ works, but needs tweaking. + #mvm = glGetDoublev(GL_MODELVIEW_MATRIX) + #pm = glGetDoublev(GL_PROJECTION_MATRIX) + #m = QtGui.QMatrix4x4(mvm.flatten()).inverted()[0] + #p = QtGui.QMatrix4x4(pm.flatten()).inverted()[0] + + #glMatrixMode(GL_PROJECTION) + #glPushMatrix() + #glLoadIdentity() + #N=1 + #glOrtho(-N,N,-N,N,-100,100) + + #glMatrixMode(GL_MODELVIEW) + #glLoadIdentity() + + + #glMatrixMode(GL_TEXTURE) + #glLoadIdentity() + #glMultMatrixf(m.copyDataTo()) + + #view = self.view() + #w = view.width() + #h = view.height() + #dist = view.opts['distance'] + #fov = view.opts['fov'] + #nearClip = dist * .1 + #farClip = dist * 5. + #r = nearClip * np.tan(fov) + #t = r * h / w + + #p = QtGui.QMatrix4x4() + #p.frustum( -r, r, -t, t, nearClip, farClip) + #glMultMatrixf(p.inverted()[0].copyDataTo()) + + + #glBegin(GL_QUADS) + + #M=1 + #for i in range(500): + #z = i/500. + #w = -i/500. + #glTexCoord3f(-M, -M, z) + #glVertex3f(-N, -N, w) + #glTexCoord3f(M, -M, z) + #glVertex3f(N, -N, w) + #glTexCoord3f(M, M, z) + #glVertex3f(N, N, w) + #glTexCoord3f(-M, M, z) + #glVertex3f(-N, N, w) + #glEnd() + #glDisable(GL_TEXTURE_3D) + + #glMatrixMode(GL_PROJECTION) + #glPopMatrix() + + + diff --git a/opengl/items/__init__.py b/opengl/items/__init__.py new file mode 100644 index 00000000..e69de29b