Added basic OpenGL scenegraph system

- rotate/scalable view widget
 - volumetric data item
This commit is contained in:
Luke Campagnola 2012-03-06 01:22:02 -05:00
parent 6a7021797f
commit 872fcb17ff
6 changed files with 508 additions and 0 deletions

60
opengl/GLGraphicsItem.py Normal file
View File

@ -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

199
opengl/GLViewWidget.py Normal file
View File

@ -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()

23
opengl/__init__.py Normal file
View File

@ -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')

47
opengl/items/GLBoxItem.py Normal file
View File

@ -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()

View File

@ -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()

0
opengl/items/__init__.py Normal file
View File