Commit 920fd933 authored by Luke Campagnola's avatar Luke Campagnola
Browse files

OpenGL scenegraph updates

 - volumetric rendering
 - isosurfaces, mesh rendering
 - basic transformation and parent/child functionality
parent 269374ef
# -*- coding: utf-8 -*-
## This example uses the isosurface function to convert a scalar field
## (a hydrogen orbital) into a mesh for 3D display.
## Add path to library (just for examples; you do not need this)
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph as pg
import pyqtgraph.opengl as gl
app = QtGui.QApplication([])
w = gl.GLViewWidget()
w.show()
g = gl.GLGridItem()
g.scale(2,2,1)
w.addItem(g)
import numpy as np
def psi(i, j, k, offset=(25, 25, 50)):
x = i-offset[0]
y = j-offset[1]
z = k-offset[2]
th = np.arctan2(z, (x**2+y**2)**0.5)
phi = np.arctan2(y, x)
r = (x**2 + y**2 + z **2)**0.5
a0 = 1
#ps = (1./81.) * (2./np.pi)**0.5 * (1./a0)**(3/2) * (6 - r/a0) * (r/a0) * np.exp(-r/(3*a0)) * np.cos(th)
ps = (1./81.) * 1./(6.*np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 * np.exp(-r/(3*a0)) * (3 * np.cos(th)**2 - 1)
return ps
#return ((1./81.) * (1./np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 * (r/a0) * np.exp(-r/(3*a0)) * np.sin(th) * np.cos(th) * np.exp(2 * 1j * phi))**2
print "Generating scalar field.."
data = np.abs(np.fromfunction(psi, (50,50,100)))
#data = np.fromfunction(lambda i,j,k: np.sin(0.2*((i-25)**2+(j-15)**2+k**2)**0.5), (50,50,50));
print "Generating isosurface.."
faces = pg.isosurface(data, data.max()/4.)
m = gl.GLMeshItem(faces)
w.addItem(m)
m.translate(-25, -25, -50)
#data = np.zeros((5,5,5))
#data[2,2,1:4] = 1
#data[2,1:4,2] = 1
#data[1:4,2,2] = 1
#tr.translate(-2.5, -2.5, 0)
#data = np.ones((2,2,2))
#data[0, 1, 0] = 0
#faces = pg.isosurface(data, 0.5)
#m = gl.GLMeshItem(faces)
#w.addItem(m)
#m.setTransform(tr)
## Start Qt event loop unless running in interactive mode.
if sys.flags.interactive != 1:
app.exec_()
......@@ -8,12 +8,21 @@ import pyqtgraph.opengl as gl
app = QtGui.QApplication([])
w = gl.GLViewWidget()
w.opts['distance'] = 20
w.show()
ax = gl.GLAxisItem()
ax.setSize(5,5,5)
w.addItem(ax)
b = gl.GLBoxItem()
w.addItem(b)
v = gl.GLVolumeItem()
w.addItem(v)
ax2 = gl.GLAxisItem()
ax2.setParentItem(b)
b.translate(1,1,1)
## Start Qt event loop unless running in interactive mode.
if sys.flags.interactive != 1:
app.exec_()
# -*- coding: utf-8 -*-
## Add path to library (just for examples; you do not need this)
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph.opengl as gl
app = QtGui.QApplication([])
w = gl.GLViewWidget()
w.opts['distance'] = 200
w.show()
#b = gl.GLBoxItem()
#w.addItem(b)
g = gl.GLGridItem()
g.scale(10, 10, 1)
w.addItem(g)
import numpy as np
## Hydrogen electron probability density
def psi(i, j, k, offset=(50,50,100)):
x = i-offset[0]
y = j-offset[1]
z = k-offset[2]
th = np.arctan2(z, (x**2+y**2)**0.5)
phi = np.arctan2(y, x)
r = (x**2 + y**2 + z **2)**0.5
a0 = 2
#ps = (1./81.) * (2./np.pi)**0.5 * (1./a0)**(3/2) * (6 - r/a0) * (r/a0) * np.exp(-r/(3*a0)) * np.cos(th)
ps = (1./81.) * 1./(6.*np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 * np.exp(-r/(3*a0)) * (3 * np.cos(th)**2 - 1)
return ps
#return ((1./81.) * (1./np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 * (r/a0) * np.exp(-r/(3*a0)) * np.sin(th) * np.cos(th) * np.exp(2 * 1j * phi))**2
data = np.fromfunction(psi, (100,100,200))
positive = np.log(np.clip(data, 0, data.max())**2)
negative = np.log(np.clip(-data, 0, -data.min())**2)
d2 = np.empty(data.shape + (4,), dtype=np.ubyte)
d2[..., 0] = positive * (255./positive.max())
d2[..., 1] = negative * (255./negative.max())
d2[..., 2] = d2[...,1]
d2[..., 3] = d2[..., 0]*0.3 + d2[..., 1]*0.3
d2[..., 3] = (d2[..., 3].astype(float) / 255.) **2 * 255
v = gl.GLVolumeItem(d2)
v.translate(-50,-50,-100)
w.addItem(v)
## Start Qt event loop unless running in interactive mode.
if sys.flags.interactive != 1:
app.exec_()
......@@ -67,6 +67,21 @@ y = np.sin(np.linspace(0, 10, 1000)) + np.random.normal(size=1000, scale=0.1)
p7.plot(y, fillLevel=-0.3, brush=(50,50,200,100))
x2 = np.linspace(-100, 100, 1000)
data2 = np.sin(x2) / x2
p8 = win.addPlot(title="Region Selection")
p8.plot(data2, pen=(255,255,255,200))
lr = pg.LinearRegionItem([400,700])
lr.setZValue(-10)
p8.addItem(lr)
p9 = win.addPlot(title="Zoom on selected region")
p9.plot(data2)
def update():
p9.setXRange(*lr.getRegion())
lr.sigRegionChanged.connect(update)
update()
## Start Qt event loop unless running in interactive mode.
if sys.flags.interactive != 1:
app.exec_()
This diff is collapsed.
......@@ -297,7 +297,18 @@ class AxisItem(GraphicsWidget):
pw = 10 ** (np.floor(np.log10(dif))-1)
scaledIntervals = intervals * pw
scaledTickCounts = dif / scaledIntervals
i1 = np.argwhere(scaledTickCounts < optimalTickCount)[0,0]
try:
i1 = np.argwhere(scaledTickCounts < optimalTickCount)[0,0]
except:
print "AxisItem can't determine tick spacing:"
print "scaledTickCounts", scaledTickCounts
print "optimalTickCount", optimalTickCount
print "dif", dif
print "scaledIntervals", scaledIntervals
print "intervals", intervals
print "pw", pw
print "pixelSpacing", pixelSpacing
i1 = 1
distBetweenIntervals = (optimalTickCount-scaledTickCounts[i1]) / (scaledTickCounts[i1-1]-scaledTickCounts[i1])
......
......@@ -6,6 +6,8 @@ class GLGraphicsItem(QtCore.QObject):
self.__parent = None
self.__view = None
self.__children = set()
self.__transform = QtGui.QMatrix4x4()
self.__visible = True
self.setParentItem(parentItem)
self.setDepthValue(0)
......@@ -16,6 +18,11 @@ class GLGraphicsItem(QtCore.QObject):
item.__children.add(self)
self.__parent = item
if self.__parent is not None and self.view() is not self.__parent.view():
if self.view() is not None:
self.view().removeItem(self)
self.__parent.view().addItem(self)
def parentItem(self):
return self.__parent
......@@ -42,6 +49,69 @@ class GLGraphicsItem(QtCore.QObject):
"""Return the depth value of this item. See setDepthValue for mode information."""
return self.__depthValue
def setTransform(self, tr):
self.__transform = tr
self.update()
def applyTransform(self, tr, local):
"""
Multiply this object's transform by *tr*.
If local is True, then *tr* is multiplied on the right of the current transform:
newTransform = transform * tr
If local is False, then *tr* is instead multiplied on the left:
newTransform = tr * transform
"""
if local:
self.setTransform(self.transform() * tr)
else:
self.setTransform(tr * self.transform())
def transform(self):
return self.__transform
def translate(self, dx, dy, dz, local=False):
"""
Translate the object by (*dx*, *dy*, *dz*) in its parent's coordinate system.
If *local* is True, then translation takes place in local coordinates.
"""
tr = QtGui.QMatrix4x4()
tr.translate(dx, dy, dz)
self.applyTransform(tr, local=local)
def rotate(self, angle, x, y, z, local=False):
"""
Rotate the object around the axis specified by (x,y,z).
*angle* is in degrees.
"""
tr = QtGui.QMatrix4x4()
tr.rotate(angle, x, y, z)
self.applyTransform(tr, local=local)
def scale(self, x, y, z, local=True):
"""
Scale the object by (*dx*, *dy*, *dz*) in its local coordinate system.
If *local* is False, then scale takes place in the parent's coordinates.
"""
tr = QtGui.QMatrix4x4()
tr.scale(x, y, z)
self.applyTransform(tr, local=local)
def hide(self):
self.setVisible(False)
def show(self):
self.setVisible(True)
def setVisible(self, vis):
self.__visible = vis
self.update()
def visible(self):
return self.__visible
def initializeGL(self):
"""
Called after an item is added to a GLViewWidget.
......@@ -57,4 +127,15 @@ class GLGraphicsItem(QtCore.QObject):
but the caller will take care of pushing/popping.
"""
pass
\ No newline at end of file
def update(self):
v = self.view()
if v is None:
return
v.updateGL()
def mapFromParent(self, point):
tr = self.transform()
if tr is None:
return point
return tr.inverted()[0].map(point)
\ No newline at end of file
......@@ -33,14 +33,15 @@ class GLViewWidget(QtOpenGL.QGLWidget):
#print "set view", item, self, item.view()
self.updateGL()
def removeItem(self, item):
self.items.remove(item)
item._setView(None)
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)
......@@ -75,15 +76,7 @@ class GLViewWidget(QtOpenGL.QGLWidget):
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):
......@@ -94,14 +87,19 @@ class GLViewWidget(QtOpenGL.QGLWidget):
items.append(item)
items.sort(lambda a,b: cmp(a.depthValue(), b.depthValue()))
for i in items:
if not i.visible():
continue
if i is item:
i.paint()
else:
glMatrixMode(GL_MODELVIEW)
glPushMatrix()
i.paint()
tr = i.transform()
a = np.array(tr.copyDataTo()).reshape((4,4))
glMultMatrixf(a.transpose())
self.drawItemTree(i)
glMatrixMode(GL_MODELVIEW)
glPopMatrix()
else:
self.drawItemTree(i)
def cameraPosition(self):
......@@ -118,65 +116,7 @@ class GLViewWidget(QtOpenGL.QGLWidget):
)
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()
......
class MeshData(object):
"""
Class for storing 3D mesh data. May contain:
- list of vertex locations
- list of edges
- list of triangles
- colors per vertex, edge, or tri
- normals per vertex or tri
"""
def __init__(self ...):
def generateFaceNormals(self):
def generateVertexNormals(self):
"""
Assigns each vertex the average of its connected face normals.
If face normals have not been computed yet, then generateFaceNormals will be called.
"""
def reverseNormals(self):
\ No newline at end of file
from OpenGL.GL import *
from .. GLGraphicsItem import GLGraphicsItem
from pyqtgraph import QtGui
__all__ = ['GLAxisItem']
class GLAxisItem(GLGraphicsItem):
def __init__(self, size=None):
GLGraphicsItem.__init__(self)
if size is None:
size = QtGui.QVector3D(1,1,1)
self.setSize(size=size)
def setSize(self, x=None, y=None, z=None, size=None):
"""
Set the size of the axes (in its local coordinate system; this does not affect the transform)
Arguments can be x,y,z or size=QVector3D().
"""
if size is not None:
x = size.x()
y = size.y()
z = size.z()
self.__size = [x,y,z]
self.update()
def size(self):
return self.__size[:]
def paint(self):
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glEnable( GL_BLEND )
glEnable( GL_ALPHA_TEST )
glEnable( GL_POINT_SMOOTH )
#glDisable( GL_DEPTH_TEST )
glBegin( GL_LINES )
x,y,z = self.size()
glColor4f(0, 1, 0, .6) # z is green
glVertex3f(0, 0, 0)
glVertex3f(0, 0, z)
glColor4f(1, 1, 0, .6) # y is yellow
glVertex3f(0, 0, 0)
glVertex3f(0, y, 0)
glColor4f(0, 0, 1, .6) # x is blue
glVertex3f(0, 0, 0)
glVertex3f(x, 0, 0)
glEnd()
from OpenGL.GL import *
from .. GLGraphicsItem import GLGraphicsItem
from pyqtgraph.Qt import QtGui
import pyqtgraph as pg
__all__ = ['GLBoxItem']
class GLBoxItem(GLGraphicsItem):
def __init__(self, size=None, color=None):
GLGraphicsItem.__init__(self)
if size is None:
size = QtGui.QVector3D(1,1,1)
self.setSize(size=size)
if color is None:
color = (255,255,255,80)
self.setColor(color)
def setSize(self, x=None, y=None, z=None, size=None):
"""
Set the size of the box (in its local coordinate system; this does not affect the transform)
Arguments can be x,y,z or size=QVector3D().
"""
if size is not None:
x = size.x()
y = size.y()
z = size.z()
self.__size = [x,y,z]
self.update()
def size(self):
return self.__size[:]
def setColor(self, *args):
"""Set the color of the box. Arguments are the same as those accepted by functions.mkColor()"""
self.__color = pg.Color(*args)
def color(self):
return self.__color
def paint(self):
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glEnable( GL_BLEND )
......@@ -13,34 +46,34 @@ class GLBoxItem(GLGraphicsItem):
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)
glColor4f(*self.color().glColor())
x,y,z = self.size()
glVertex3f(0, 0, 0)
glVertex3f(0, 0, z)
glVertex3f(x, 0, 0)
glVertex3f(x, 0, z)
glVertex3f(0, y, 0)
glVertex3f(0, y, z)
glVertex3f(x, y, 0)
glVertex3f(x, y, z)
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(0, 0, 0)
glVertex3f(0, y, 0)
glVertex3f(x, 0, 0)
glVertex3f(x, y, 0)
glVertex3f(0, 0, z)
glVertex3f(0, y, z)
glVertex3f(x, 0, z)
glVertex3f(x, y, z)
glVertex3f(-w, -w, -w)
glVertex3f( w, -w, -w)
glVertex3f(-w, w, -w)
glVertex3f( w, w, -w)
glVertex3f(-w, -w, w)
glVertex3f( w, -w, w)