Added basic OpenGL scenegraph system
- rotate/scalable view widget - volumetric data item
This commit is contained in:
parent
6a7021797f
commit
872fcb17ff
60
opengl/GLGraphicsItem.py
Normal file
60
opengl/GLGraphicsItem.py
Normal 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
199
opengl/GLViewWidget.py
Normal 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
23
opengl/__init__.py
Normal 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
47
opengl/items/GLBoxItem.py
Normal 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()
|
||||
|
||||
|
179
opengl/items/GLVolumeItem.py
Normal file
179
opengl/items/GLVolumeItem.py
Normal 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
0
opengl/items/__init__.py
Normal file
Loading…
Reference in New Issue
Block a user