diff --git a/examples/GLMeshItem.py b/examples/GLMeshItem.py new file mode 100644 index 00000000..07f9f949 --- /dev/null +++ b/examples/GLMeshItem.py @@ -0,0 +1,67 @@ +# -*- 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_() diff --git a/examples/GLViewWidget.py b/examples/GLViewWidget.py index 4fdbf129..802fa289 100644 --- a/examples/GLViewWidget.py +++ b/examples/GLViewWidget.py @@ -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_() diff --git a/examples/GLVolumeItem.py b/examples/GLVolumeItem.py new file mode 100644 index 00000000..163f4b7a --- /dev/null +++ b/examples/GLVolumeItem.py @@ -0,0 +1,57 @@ +# -*- 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_() diff --git a/examples/Plotting.py b/examples/Plotting.py index b4018b7d..84979f02 100644 --- a/examples/Plotting.py +++ b/examples/Plotting.py @@ -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_() diff --git a/functions.py b/functions.py index 3a249d9c..ab60e63e 100644 --- a/functions.py +++ b/functions.py @@ -120,6 +120,18 @@ def siEval(s): return v * 1000**n +class Color(QtGui.QColor): + def __init__(self, *args): + QtGui.QColor.__init__(self, mkColor(*args)) + + def glColor(self): + """Return (r,g,b,a) normalized for use in opengl""" + return (self.red()/255., self.green()/255., self.blue()/255., self.alpha()/255.) + + def __getitem__(self, ind): + return (self.red, self.green, self.blue, self.alpha)[ind]() + + def mkColor(*args): """ Convenience function for constructing QColor from a variety of argument types. Accepted arguments are: @@ -632,4 +644,526 @@ def rescaleData(data, scale, offset): data = newData.reshape(data.shape) return data + +#def isosurface(data, level): + #""" + #Generate isosurface from volumetric data using marching tetrahedra algorithm. + #See Paul Bourke, "Polygonising a Scalar Field Using Tetrahedrons" (http://local.wasp.uwa.edu.au/~pbourke/geometry/polygonise/) + + #*data* 3D numpy array of scalar values + #*level* The level at which to generate an isosurface + #""" + + #facets = [] + + ### mark everything below the isosurface level + #mask = data < level + + #### make eight sub-fields + #fields = np.empty((2,2,2), dtype=object) + #slices = [slice(0,-1), slice(1,None)] + #for i in [0,1]: + #for j in [0,1]: + #for k in [0,1]: + #fields[i,j,k] = mask[slices[i], slices[j], slices[k]] + + + + ### split each cell into 6 tetrahedra + ### these all have the same 'orienation'; points 1,2,3 circle + ### clockwise around point 0 + #tetrahedra = [ + #[(0,1,0), (1,1,1), (0,1,1), (1,0,1)], + #[(0,1,0), (0,1,1), (0,0,1), (1,0,1)], + #[(0,1,0), (0,0,1), (0,0,0), (1,0,1)], + #[(0,1,0), (0,0,0), (1,0,0), (1,0,1)], + #[(0,1,0), (1,0,0), (1,1,0), (1,0,1)], + #[(0,1,0), (1,1,0), (1,1,1), (1,0,1)] + #] + + ### each tetrahedron will be assigned an index + ### which determines how to generate its facets. + ### this structure is: + ### facets[index][facet1, facet2, ...] + ### where each facet is triangular and its points are each + ### interpolated between two points on the tetrahedron + ### facet = [(p1a, p1b), (p2a, p2b), (p3a, p3b)] + ### facet points always circle clockwise if you are looking + ### at them from below the isosurface. + #indexFacets = [ + #[], ## all above + #[[(0,1), (0,2), (0,3)]], # 0 below + #[[(1,0), (1,3), (1,2)]], # 1 below + #[[(0,2), (1,3), (1,2)], [(0,2), (0,3), (1,3)]], # 0,1 below + #[[(2,0), (2,1), (2,3)]], # 2 below + #[[(0,3), (1,2), (2,3)], [(0,3), (0,1), (1,2)]], # 0,2 below + #[[(1,0), (2,3), (2,0)], [(1,0), (1,3), (2,3)]], # 1,2 below + #[[(3,0), (3,1), (3,2)]], # 3 above + #[[(3,0), (3,2), (3,1)]], # 3 below + #[[(1,0), (2,0), (2,3)], [(1,0), (2,3), (1,3)]], # 0,3 below + #[[(0,3), (2,3), (1,2)], [(0,3), (1,2), (0,1)]], # 1,3 below + #[[(2,0), (2,3), (2,1)]], # 0,1,3 below + #[[(0,2), (1,2), (1,3)], [(0,2), (1,3), (0,3)]], # 2,3 below + #[[(1,0), (1,2), (1,3)]], # 0,2,3 below + #[[(0,1), (0,3), (0,2)]], # 1,2,3 below + #[] ## all below + #] + + #for tet in tetrahedra: + + ### get the 4 fields for this tetrahedron + #tetFields = [fields[c] for c in tet] + + ### generate an index for each grid cell + #index = tetFields[0] + tetFields[1]*2 + tetFields[2]*4 + tetFields[3]*8 + + ### add facets + #for i in xrange(index.shape[0]): # data x-axis + #for j in xrange(index.shape[1]): # data y-axis + #for k in xrange(index.shape[2]): # data z-axis + #for f in indexFacets[index[i,j,k]]: # faces to generate for this tet + #pts = [] + #for l in [0,1,2]: # points in this face + #p1 = tet[f[l][0]] # tet corner 1 + #p2 = tet[f[l][1]] # tet corner 2 + #pts.append([(p1[x]+p2[x])*0.5+[i,j,k][x]+0.5 for x in [0,1,2]]) ## interpolate between tet corners + #facets.append(pts) + + #return facets + + + + +def isosurface(data, level): + """ + Generate isosurface from volumetric data using marching tetrahedra algorithm. + See Paul Bourke, "Polygonising a Scalar Field" + (http://local.wasp.uwa.edu.au/~pbourke/geometry/polygonise/) + + *data* 3D numpy array of scalar values + *level* The level at which to generate an isosurface + + This function is SLOW; plenty of room for optimization here. + """ + + ## map from grid cell index to edge index. + ## grid cell index tells us which corners are below the isosurface, + ## edge index tells us which edges are cut by the isosurface. + ## (Data stolen from Bourk; see above.) + edgeTable = [ + 0x0 , 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c, + 0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00, + 0x190, 0x99 , 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c, + 0x99c, 0x895, 0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90, + 0x230, 0x339, 0x33 , 0x13a, 0x636, 0x73f, 0x435, 0x53c, + 0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30, + 0x3a0, 0x2a9, 0x1a3, 0xaa , 0x7a6, 0x6af, 0x5a5, 0x4ac, + 0xbac, 0xaa5, 0x9af, 0x8a6, 0xfaa, 0xea3, 0xda9, 0xca0, + 0x460, 0x569, 0x663, 0x76a, 0x66 , 0x16f, 0x265, 0x36c, + 0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963, 0xa69, 0xb60, + 0x5f0, 0x4f9, 0x7f3, 0x6fa, 0x1f6, 0xff , 0x3f5, 0x2fc, + 0xdfc, 0xcf5, 0xfff, 0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0, + 0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x55 , 0x15c, + 0xe5c, 0xf55, 0xc5f, 0xd56, 0xa5a, 0xb53, 0x859, 0x950, + 0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6, 0x2cf, 0x1c5, 0xcc , + 0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0, + 0x8c0, 0x9c9, 0xac3, 0xbca, 0xcc6, 0xdcf, 0xec5, 0xfcc, + 0xcc , 0x1c5, 0x2cf, 0x3c6, 0x4ca, 0x5c3, 0x6c9, 0x7c0, + 0x950, 0x859, 0xb53, 0xa5a, 0xd56, 0xc5f, 0xf55, 0xe5c, + 0x15c, 0x55 , 0x35f, 0x256, 0x55a, 0x453, 0x759, 0x650, + 0xaf0, 0xbf9, 0x8f3, 0x9fa, 0xef6, 0xfff, 0xcf5, 0xdfc, + 0x2fc, 0x3f5, 0xff , 0x1f6, 0x6fa, 0x7f3, 0x4f9, 0x5f0, + 0xb60, 0xa69, 0x963, 0x86a, 0xf66, 0xe6f, 0xd65, 0xc6c, + 0x36c, 0x265, 0x16f, 0x66 , 0x76a, 0x663, 0x569, 0x460, + 0xca0, 0xda9, 0xea3, 0xfaa, 0x8a6, 0x9af, 0xaa5, 0xbac, + 0x4ac, 0x5a5, 0x6af, 0x7a6, 0xaa , 0x1a3, 0x2a9, 0x3a0, + 0xd30, 0xc39, 0xf33, 0xe3a, 0x936, 0x83f, 0xb35, 0xa3c, + 0x53c, 0x435, 0x73f, 0x636, 0x13a, 0x33 , 0x339, 0x230, + 0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c, + 0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x99 , 0x190, + 0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c, + 0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x0 ] + + ## Table of triangles to use for filling each grid cell. + ## Each set of three integers tells us which three edges to + ## draw a triangle between. + ## (Data stolen from Bourk; see above.) + triTable = [ + [], + [0, 8, 3], + [0, 1, 9], + [1, 8, 3, 9, 8, 1], + [1, 2, 10], + [0, 8, 3, 1, 2, 10], + [9, 2, 10, 0, 2, 9], + [2, 8, 3, 2, 10, 8, 10, 9, 8], + [3, 11, 2], + [0, 11, 2, 8, 11, 0], + [1, 9, 0, 2, 3, 11], + [1, 11, 2, 1, 9, 11, 9, 8, 11], + [3, 10, 1, 11, 10, 3], + [0, 10, 1, 0, 8, 10, 8, 11, 10], + [3, 9, 0, 3, 11, 9, 11, 10, 9], + [9, 8, 10, 10, 8, 11], + [4, 7, 8], + [4, 3, 0, 7, 3, 4], + [0, 1, 9, 8, 4, 7], + [4, 1, 9, 4, 7, 1, 7, 3, 1], + [1, 2, 10, 8, 4, 7], + [3, 4, 7, 3, 0, 4, 1, 2, 10], + [9, 2, 10, 9, 0, 2, 8, 4, 7], + [2, 10, 9, 2, 9, 7, 2, 7, 3, 7, 9, 4], + [8, 4, 7, 3, 11, 2], + [11, 4, 7, 11, 2, 4, 2, 0, 4], + [9, 0, 1, 8, 4, 7, 2, 3, 11], + [4, 7, 11, 9, 4, 11, 9, 11, 2, 9, 2, 1], + [3, 10, 1, 3, 11, 10, 7, 8, 4], + [1, 11, 10, 1, 4, 11, 1, 0, 4, 7, 11, 4], + [4, 7, 8, 9, 0, 11, 9, 11, 10, 11, 0, 3], + [4, 7, 11, 4, 11, 9, 9, 11, 10], + [9, 5, 4], + [9, 5, 4, 0, 8, 3], + [0, 5, 4, 1, 5, 0], + [8, 5, 4, 8, 3, 5, 3, 1, 5], + [1, 2, 10, 9, 5, 4], + [3, 0, 8, 1, 2, 10, 4, 9, 5], + [5, 2, 10, 5, 4, 2, 4, 0, 2], + [2, 10, 5, 3, 2, 5, 3, 5, 4, 3, 4, 8], + [9, 5, 4, 2, 3, 11], + [0, 11, 2, 0, 8, 11, 4, 9, 5], + [0, 5, 4, 0, 1, 5, 2, 3, 11], + [2, 1, 5, 2, 5, 8, 2, 8, 11, 4, 8, 5], + [10, 3, 11, 10, 1, 3, 9, 5, 4], + [4, 9, 5, 0, 8, 1, 8, 10, 1, 8, 11, 10], + [5, 4, 0, 5, 0, 11, 5, 11, 10, 11, 0, 3], + [5, 4, 8, 5, 8, 10, 10, 8, 11], + [9, 7, 8, 5, 7, 9], + [9, 3, 0, 9, 5, 3, 5, 7, 3], + [0, 7, 8, 0, 1, 7, 1, 5, 7], + [1, 5, 3, 3, 5, 7], + [9, 7, 8, 9, 5, 7, 10, 1, 2], + [10, 1, 2, 9, 5, 0, 5, 3, 0, 5, 7, 3], + [8, 0, 2, 8, 2, 5, 8, 5, 7, 10, 5, 2], + [2, 10, 5, 2, 5, 3, 3, 5, 7], + [7, 9, 5, 7, 8, 9, 3, 11, 2], + [9, 5, 7, 9, 7, 2, 9, 2, 0, 2, 7, 11], + [2, 3, 11, 0, 1, 8, 1, 7, 8, 1, 5, 7], + [11, 2, 1, 11, 1, 7, 7, 1, 5], + [9, 5, 8, 8, 5, 7, 10, 1, 3, 10, 3, 11], + [5, 7, 0, 5, 0, 9, 7, 11, 0, 1, 0, 10, 11, 10, 0], + [11, 10, 0, 11, 0, 3, 10, 5, 0, 8, 0, 7, 5, 7, 0], + [11, 10, 5, 7, 11, 5], + [10, 6, 5], + [0, 8, 3, 5, 10, 6], + [9, 0, 1, 5, 10, 6], + [1, 8, 3, 1, 9, 8, 5, 10, 6], + [1, 6, 5, 2, 6, 1], + [1, 6, 5, 1, 2, 6, 3, 0, 8], + [9, 6, 5, 9, 0, 6, 0, 2, 6], + [5, 9, 8, 5, 8, 2, 5, 2, 6, 3, 2, 8], + [2, 3, 11, 10, 6, 5], + [11, 0, 8, 11, 2, 0, 10, 6, 5], + [0, 1, 9, 2, 3, 11, 5, 10, 6], + [5, 10, 6, 1, 9, 2, 9, 11, 2, 9, 8, 11], + [6, 3, 11, 6, 5, 3, 5, 1, 3], + [0, 8, 11, 0, 11, 5, 0, 5, 1, 5, 11, 6], + [3, 11, 6, 0, 3, 6, 0, 6, 5, 0, 5, 9], + [6, 5, 9, 6, 9, 11, 11, 9, 8], + [5, 10, 6, 4, 7, 8], + [4, 3, 0, 4, 7, 3, 6, 5, 10], + [1, 9, 0, 5, 10, 6, 8, 4, 7], + [10, 6, 5, 1, 9, 7, 1, 7, 3, 7, 9, 4], + [6, 1, 2, 6, 5, 1, 4, 7, 8], + [1, 2, 5, 5, 2, 6, 3, 0, 4, 3, 4, 7], + [8, 4, 7, 9, 0, 5, 0, 6, 5, 0, 2, 6], + [7, 3, 9, 7, 9, 4, 3, 2, 9, 5, 9, 6, 2, 6, 9], + [3, 11, 2, 7, 8, 4, 10, 6, 5], + [5, 10, 6, 4, 7, 2, 4, 2, 0, 2, 7, 11], + [0, 1, 9, 4, 7, 8, 2, 3, 11, 5, 10, 6], + [9, 2, 1, 9, 11, 2, 9, 4, 11, 7, 11, 4, 5, 10, 6], + [8, 4, 7, 3, 11, 5, 3, 5, 1, 5, 11, 6], + [5, 1, 11, 5, 11, 6, 1, 0, 11, 7, 11, 4, 0, 4, 11], + [0, 5, 9, 0, 6, 5, 0, 3, 6, 11, 6, 3, 8, 4, 7], + [6, 5, 9, 6, 9, 11, 4, 7, 9, 7, 11, 9], + [10, 4, 9, 6, 4, 10], + [4, 10, 6, 4, 9, 10, 0, 8, 3], + [10, 0, 1, 10, 6, 0, 6, 4, 0], + [8, 3, 1, 8, 1, 6, 8, 6, 4, 6, 1, 10], + [1, 4, 9, 1, 2, 4, 2, 6, 4], + [3, 0, 8, 1, 2, 9, 2, 4, 9, 2, 6, 4], + [0, 2, 4, 4, 2, 6], + [8, 3, 2, 8, 2, 4, 4, 2, 6], + [10, 4, 9, 10, 6, 4, 11, 2, 3], + [0, 8, 2, 2, 8, 11, 4, 9, 10, 4, 10, 6], + [3, 11, 2, 0, 1, 6, 0, 6, 4, 6, 1, 10], + [6, 4, 1, 6, 1, 10, 4, 8, 1, 2, 1, 11, 8, 11, 1], + [9, 6, 4, 9, 3, 6, 9, 1, 3, 11, 6, 3], + [8, 11, 1, 8, 1, 0, 11, 6, 1, 9, 1, 4, 6, 4, 1], + [3, 11, 6, 3, 6, 0, 0, 6, 4], + [6, 4, 8, 11, 6, 8], + [7, 10, 6, 7, 8, 10, 8, 9, 10], + [0, 7, 3, 0, 10, 7, 0, 9, 10, 6, 7, 10], + [10, 6, 7, 1, 10, 7, 1, 7, 8, 1, 8, 0], + [10, 6, 7, 10, 7, 1, 1, 7, 3], + [1, 2, 6, 1, 6, 8, 1, 8, 9, 8, 6, 7], + [2, 6, 9, 2, 9, 1, 6, 7, 9, 0, 9, 3, 7, 3, 9], + [7, 8, 0, 7, 0, 6, 6, 0, 2], + [7, 3, 2, 6, 7, 2], + [2, 3, 11, 10, 6, 8, 10, 8, 9, 8, 6, 7], + [2, 0, 7, 2, 7, 11, 0, 9, 7, 6, 7, 10, 9, 10, 7], + [1, 8, 0, 1, 7, 8, 1, 10, 7, 6, 7, 10, 2, 3, 11], + [11, 2, 1, 11, 1, 7, 10, 6, 1, 6, 7, 1], + [8, 9, 6, 8, 6, 7, 9, 1, 6, 11, 6, 3, 1, 3, 6], + [0, 9, 1, 11, 6, 7], + [7, 8, 0, 7, 0, 6, 3, 11, 0, 11, 6, 0], + [7, 11, 6], + [7, 6, 11], + [3, 0, 8, 11, 7, 6], + [0, 1, 9, 11, 7, 6], + [8, 1, 9, 8, 3, 1, 11, 7, 6], + [10, 1, 2, 6, 11, 7], + [1, 2, 10, 3, 0, 8, 6, 11, 7], + [2, 9, 0, 2, 10, 9, 6, 11, 7], + [6, 11, 7, 2, 10, 3, 10, 8, 3, 10, 9, 8], + [7, 2, 3, 6, 2, 7], + [7, 0, 8, 7, 6, 0, 6, 2, 0], + [2, 7, 6, 2, 3, 7, 0, 1, 9], + [1, 6, 2, 1, 8, 6, 1, 9, 8, 8, 7, 6], + [10, 7, 6, 10, 1, 7, 1, 3, 7], + [10, 7, 6, 1, 7, 10, 1, 8, 7, 1, 0, 8], + [0, 3, 7, 0, 7, 10, 0, 10, 9, 6, 10, 7], + [7, 6, 10, 7, 10, 8, 8, 10, 9], + [6, 8, 4, 11, 8, 6], + [3, 6, 11, 3, 0, 6, 0, 4, 6], + [8, 6, 11, 8, 4, 6, 9, 0, 1], + [9, 4, 6, 9, 6, 3, 9, 3, 1, 11, 3, 6], + [6, 8, 4, 6, 11, 8, 2, 10, 1], + [1, 2, 10, 3, 0, 11, 0, 6, 11, 0, 4, 6], + [4, 11, 8, 4, 6, 11, 0, 2, 9, 2, 10, 9], + [10, 9, 3, 10, 3, 2, 9, 4, 3, 11, 3, 6, 4, 6, 3], + [8, 2, 3, 8, 4, 2, 4, 6, 2], + [0, 4, 2, 4, 6, 2], + [1, 9, 0, 2, 3, 4, 2, 4, 6, 4, 3, 8], + [1, 9, 4, 1, 4, 2, 2, 4, 6], + [8, 1, 3, 8, 6, 1, 8, 4, 6, 6, 10, 1], + [10, 1, 0, 10, 0, 6, 6, 0, 4], + [4, 6, 3, 4, 3, 8, 6, 10, 3, 0, 3, 9, 10, 9, 3], + [10, 9, 4, 6, 10, 4], + [4, 9, 5, 7, 6, 11], + [0, 8, 3, 4, 9, 5, 11, 7, 6], + [5, 0, 1, 5, 4, 0, 7, 6, 11], + [11, 7, 6, 8, 3, 4, 3, 5, 4, 3, 1, 5], + [9, 5, 4, 10, 1, 2, 7, 6, 11], + [6, 11, 7, 1, 2, 10, 0, 8, 3, 4, 9, 5], + [7, 6, 11, 5, 4, 10, 4, 2, 10, 4, 0, 2], + [3, 4, 8, 3, 5, 4, 3, 2, 5, 10, 5, 2, 11, 7, 6], + [7, 2, 3, 7, 6, 2, 5, 4, 9], + [9, 5, 4, 0, 8, 6, 0, 6, 2, 6, 8, 7], + [3, 6, 2, 3, 7, 6, 1, 5, 0, 5, 4, 0], + [6, 2, 8, 6, 8, 7, 2, 1, 8, 4, 8, 5, 1, 5, 8], + [9, 5, 4, 10, 1, 6, 1, 7, 6, 1, 3, 7], + [1, 6, 10, 1, 7, 6, 1, 0, 7, 8, 7, 0, 9, 5, 4], + [4, 0, 10, 4, 10, 5, 0, 3, 10, 6, 10, 7, 3, 7, 10], + [7, 6, 10, 7, 10, 8, 5, 4, 10, 4, 8, 10], + [6, 9, 5, 6, 11, 9, 11, 8, 9], + [3, 6, 11, 0, 6, 3, 0, 5, 6, 0, 9, 5], + [0, 11, 8, 0, 5, 11, 0, 1, 5, 5, 6, 11], + [6, 11, 3, 6, 3, 5, 5, 3, 1], + [1, 2, 10, 9, 5, 11, 9, 11, 8, 11, 5, 6], + [0, 11, 3, 0, 6, 11, 0, 9, 6, 5, 6, 9, 1, 2, 10], + [11, 8, 5, 11, 5, 6, 8, 0, 5, 10, 5, 2, 0, 2, 5], + [6, 11, 3, 6, 3, 5, 2, 10, 3, 10, 5, 3], + [5, 8, 9, 5, 2, 8, 5, 6, 2, 3, 8, 2], + [9, 5, 6, 9, 6, 0, 0, 6, 2], + [1, 5, 8, 1, 8, 0, 5, 6, 8, 3, 8, 2, 6, 2, 8], + [1, 5, 6, 2, 1, 6], + [1, 3, 6, 1, 6, 10, 3, 8, 6, 5, 6, 9, 8, 9, 6], + [10, 1, 0, 10, 0, 6, 9, 5, 0, 5, 6, 0], + [0, 3, 8, 5, 6, 10], + [10, 5, 6], + [11, 5, 10, 7, 5, 11], + [11, 5, 10, 11, 7, 5, 8, 3, 0], + [5, 11, 7, 5, 10, 11, 1, 9, 0], + [10, 7, 5, 10, 11, 7, 9, 8, 1, 8, 3, 1], + [11, 1, 2, 11, 7, 1, 7, 5, 1], + [0, 8, 3, 1, 2, 7, 1, 7, 5, 7, 2, 11], + [9, 7, 5, 9, 2, 7, 9, 0, 2, 2, 11, 7], + [7, 5, 2, 7, 2, 11, 5, 9, 2, 3, 2, 8, 9, 8, 2], + [2, 5, 10, 2, 3, 5, 3, 7, 5], + [8, 2, 0, 8, 5, 2, 8, 7, 5, 10, 2, 5], + [9, 0, 1, 5, 10, 3, 5, 3, 7, 3, 10, 2], + [9, 8, 2, 9, 2, 1, 8, 7, 2, 10, 2, 5, 7, 5, 2], + [1, 3, 5, 3, 7, 5], + [0, 8, 7, 0, 7, 1, 1, 7, 5], + [9, 0, 3, 9, 3, 5, 5, 3, 7], + [9, 8, 7, 5, 9, 7], + [5, 8, 4, 5, 10, 8, 10, 11, 8], + [5, 0, 4, 5, 11, 0, 5, 10, 11, 11, 3, 0], + [0, 1, 9, 8, 4, 10, 8, 10, 11, 10, 4, 5], + [10, 11, 4, 10, 4, 5, 11, 3, 4, 9, 4, 1, 3, 1, 4], + [2, 5, 1, 2, 8, 5, 2, 11, 8, 4, 5, 8], + [0, 4, 11, 0, 11, 3, 4, 5, 11, 2, 11, 1, 5, 1, 11], + [0, 2, 5, 0, 5, 9, 2, 11, 5, 4, 5, 8, 11, 8, 5], + [9, 4, 5, 2, 11, 3], + [2, 5, 10, 3, 5, 2, 3, 4, 5, 3, 8, 4], + [5, 10, 2, 5, 2, 4, 4, 2, 0], + [3, 10, 2, 3, 5, 10, 3, 8, 5, 4, 5, 8, 0, 1, 9], + [5, 10, 2, 5, 2, 4, 1, 9, 2, 9, 4, 2], + [8, 4, 5, 8, 5, 3, 3, 5, 1], + [0, 4, 5, 1, 0, 5], + [8, 4, 5, 8, 5, 3, 9, 0, 5, 0, 3, 5], + [9, 4, 5], + [4, 11, 7, 4, 9, 11, 9, 10, 11], + [0, 8, 3, 4, 9, 7, 9, 11, 7, 9, 10, 11], + [1, 10, 11, 1, 11, 4, 1, 4, 0, 7, 4, 11], + [3, 1, 4, 3, 4, 8, 1, 10, 4, 7, 4, 11, 10, 11, 4], + [4, 11, 7, 9, 11, 4, 9, 2, 11, 9, 1, 2], + [9, 7, 4, 9, 11, 7, 9, 1, 11, 2, 11, 1, 0, 8, 3], + [11, 7, 4, 11, 4, 2, 2, 4, 0], + [11, 7, 4, 11, 4, 2, 8, 3, 4, 3, 2, 4], + [2, 9, 10, 2, 7, 9, 2, 3, 7, 7, 4, 9], + [9, 10, 7, 9, 7, 4, 10, 2, 7, 8, 7, 0, 2, 0, 7], + [3, 7, 10, 3, 10, 2, 7, 4, 10, 1, 10, 0, 4, 0, 10], + [1, 10, 2, 8, 7, 4], + [4, 9, 1, 4, 1, 7, 7, 1, 3], + [4, 9, 1, 4, 1, 7, 0, 8, 1, 8, 7, 1], + [4, 0, 3, 7, 4, 3], + [4, 8, 7], + [9, 10, 8, 10, 11, 8], + [3, 0, 9, 3, 9, 11, 11, 9, 10], + [0, 1, 10, 0, 10, 8, 8, 10, 11], + [3, 1, 10, 11, 3, 10], + [1, 2, 11, 1, 11, 9, 9, 11, 8], + [3, 0, 9, 3, 9, 11, 1, 2, 9, 2, 11, 9], + [0, 2, 11, 8, 0, 11], + [3, 2, 11], + [2, 3, 8, 2, 8, 10, 10, 8, 9], + [9, 10, 2, 0, 9, 2], + [2, 3, 8, 2, 8, 10, 0, 1, 8, 1, 10, 8], + [1, 10, 2], + [1, 3, 8, 9, 1, 8], + [0, 9, 1], + [0, 3, 8], + [] + ] + + ## translation between edge index and + ## the vertex indexes that bound the edge + edgeKey = [ + [(0,0,0), (1,0,0)], + [(1,0,0), (1,1,0)], + [(1,1,0), (0,1,0)], + [(0,1,0), (0,0,0)], + [(0,0,1), (1,0,1)], + [(1,0,1), (1,1,1)], + [(1,1,1), (0,1,1)], + [(0,1,1), (0,0,1)], + [(0,0,0), (0,0,1)], + [(1,0,0), (1,0,1)], + [(1,1,0), (1,1,1)], + [(0,1,0), (0,1,1)], + ] + + + + facets = [] + + ## mark everything below the isosurface level + mask = data < level + + ### make eight sub-fields and compute indexes for grid cells + index = np.zeros([x-1 for x in data.shape], dtype=np.ubyte) + fields = np.empty((2,2,2), dtype=object) + slices = [slice(0,-1), slice(1,None)] + for i in [0,1]: + for j in [0,1]: + for k in [0,1]: + fields[i,j,k] = mask[slices[i], slices[j], slices[k]] + vertIndex = i - 2*j*i + 3*j + 4*k ## this is just to match Bourk's vertex numbering scheme + #print i,j,k," : ", fields[i,j,k], 2**vertIndex + index += fields[i,j,k] * 2**vertIndex + #print index + #print index + + ## add facets + for i in xrange(index.shape[0]): # data x-axis + for j in xrange(index.shape[1]): # data y-axis + for k in xrange(index.shape[2]): # data z-axis + tris = triTable[index[i,j,k]] + for l in range(0, len(tris), 3): ## faces for this grid cell + edges = tris[l:l+3] + pts = [] + for m in [0,1,2]: # points in this face + p1 = edgeKey[edges[m]][0] + p2 = edgeKey[edges[m]][1] + v1 = data[i+p1[0], j+p1[1], k+p1[2]] + v2 = data[i+p2[0], j+p2[1], k+p2[2]] + f = (level-v1) / (v2-v1) + fi = 1.0 - f + p = ( ## interpolate between corners + p1[0]*fi + p2[0]*f + i + 0.5, + p1[1]*fi + p2[1]*f + j + 0.5, + p1[2]*fi + p2[2]*f + k + 0.5 + ) + pts.append(p) + facets.append(pts) + + return facets + + +def meshNormals(data): + """ + Return list of normal vectors and list of faces which reference the normals + data must be list of triangles; each triangle is a list of three points + [ [(x,y,z), (x,y,z), (x,y,z)], ...] + Return values are + normals: [(x,y,z), ...] + faces: [(n1, n2, n3), ...] + """ + + normals = [] + points = {} + for i, face in enumerate(data): + ## compute face normal + pts = [QtGui.QVector3D(*x) for x in face] + norm = QtGui.QVector3D.crossProduct(pts[1]-pts[0], pts[2]-pts[0]) + normals.append(norm) + + ## remember each point was associated with this normal + for p in face: + p = tuple(map(lambda x: np.round(x, 8), p)) + if p not in points: + points[p] = [] + points[p].append(i) + + ## compute averages + avgLookup = {} + avgNorms = [] + for k,v in points.iteritems(): + norms = [normals[i] for i in v] + a = norms[0] + if len(v) > 1: + for n in norms[1:]: + a = a + n + a = a / len(v) + avgLookup[k] = len(avgNorms) + avgNorms.append(a) + + ## generate return array + faces = [] + for i, face in enumerate(data): + f = [] + for p in face: + p = tuple(map(lambda x: np.round(x, 8), p)) + f.append(avgLookup[p]) + faces.append(tuple(f)) + + return avgNorms, faces + + + + + + + \ No newline at end of file diff --git a/graphicsItems/AxisItem.py b/graphicsItems/AxisItem.py index 11834f01..27323049 100644 --- a/graphicsItems/AxisItem.py +++ b/graphicsItems/AxisItem.py @@ -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]) diff --git a/opengl/GLGraphicsItem.py b/opengl/GLGraphicsItem.py index 7baa3b7e..da6a37c6 100644 --- a/opengl/GLGraphicsItem.py +++ b/opengl/GLGraphicsItem.py @@ -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 diff --git a/opengl/GLViewWidget.py b/opengl/GLViewWidget.py index ae579003..15077b74 100644 --- a/opengl/GLViewWidget.py +++ b/opengl/GLViewWidget.py @@ -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() diff --git a/opengl/MeshData.py b/opengl/MeshData.py new file mode 100644 index 00000000..f6d0ae7c --- /dev/null +++ b/opengl/MeshData.py @@ -0,0 +1,25 @@ +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 diff --git a/opengl/items/GLAxisItem.py b/opengl/items/GLAxisItem.py new file mode 100644 index 00000000..79d2149d --- /dev/null +++ b/opengl/items/GLAxisItem.py @@ -0,0 +1,51 @@ +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() diff --git a/opengl/items/GLBoxItem.py b/opengl/items/GLBoxItem.py index ffaa6861..648ac5e4 100644 --- a/opengl/items/GLBoxItem.py +++ b/opengl/items/GLBoxItem.py @@ -1,9 +1,42 @@ 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) - glVertex3f(-w, w, w) - glVertex3f( w, w, w) + glVertex3f(0, 0, 0) + glVertex3f(x, 0, 0) + glVertex3f(0, y, 0) + glVertex3f(x, y, 0) + glVertex3f(0, 0, z) + glVertex3f(x, 0, z) + glVertex3f(0, y, z) + glVertex3f(x, y, z) glEnd() diff --git a/opengl/items/GLGridItem.py b/opengl/items/GLGridItem.py new file mode 100644 index 00000000..e34929b3 --- /dev/null +++ b/opengl/items/GLGridItem.py @@ -0,0 +1,48 @@ +from OpenGL.GL import * +from .. GLGraphicsItem import GLGraphicsItem +from pyqtgraph import QtGui + +__all__ = ['GLGridItem'] + +class GLGridItem(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) + + 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(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) + + glEnd() diff --git a/opengl/items/GLMeshItem.py b/opengl/items/GLMeshItem.py new file mode 100644 index 00000000..1efc3ffe --- /dev/null +++ b/opengl/items/GLMeshItem.py @@ -0,0 +1,93 @@ +from OpenGL.GL import * +from .. GLGraphicsItem import GLGraphicsItem +from pyqtgraph.Qt import QtGui +import pyqtgraph as pg +from .. import shaders +import numpy as np + + + +__all__ = ['GLMeshItem'] + +class GLMeshItem(GLGraphicsItem): + def __init__(self, faces): + self.faces = faces + self.normals, self.faceNormals = pg.meshNormals(faces) + + GLGraphicsItem.__init__(self) + + def initializeGL(self): + + #balloonVertexShader = shaders.compileShader(""" + #varying vec3 normal; + #void main() { + #normal = normalize(gl_NormalMatrix * gl_Normal); + #//vec4 color = normal; + #//normal.w = min(color.w + 2.0 * color.w * pow(normal.x*normal.x + normal.y*normal.y, 2.0), 1.0); + #gl_FrontColor = gl_Color; + #gl_BackColor = gl_Color; + #gl_Position = ftransform(); + #}""", GL_VERTEX_SHADER) + #balloonFragmentShader = shaders.compileShader(""" + #varying vec3 normal; + #void main() { + #vec4 color = gl_Color; + #color.w = min(color.w + 2.0 * color.w * pow(normal.x*normal.x + normal.y*normal.y, 5.0), 1.0); + #gl_FragColor = color; + #}""", GL_FRAGMENT_SHADER) + #self.shader = shaders.compileProgram(balloonVertexShader, balloonFragmentShader) + + self.shader = shaders.getShader('balloon') + + l = glGenLists(1) + self.triList = l + glNewList(l, GL_COMPILE) + + 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 ) + glColor4f(1, 1, 1, .1) + glBegin( GL_TRIANGLES ) + for i, f in enumerate(self.faces): + pts = [QtGui.QVector3D(*x) for x in f] + if pts[0] is None: + print f + continue + #norm = QtGui.QVector3D.crossProduct(pts[1]-pts[0], pts[2]-pts[0]) + for j in [0,1,2]: + norm = self.normals[self.faceNormals[i][j]] + glNormal3f(norm.x(), norm.y(), norm.z()) + #j = (i+1) % 3 + glVertex3f(*f[j]) + glEnd() + glEndList() + + + l = glGenLists(1) + self.meshList = l + glNewList(l, GL_COMPILE) + 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 ) + glEnable( GL_DEPTH_TEST ) + glColor4f(1, 1, 1, .3) + glBegin( GL_LINES ) + for f in self.faces: + for i in [0,1,2]: + j = (i+1) % 3 + glVertex3f(*f[i]) + glVertex3f(*f[j]) + glEnd() + glEndList() + + + def paint(self): + shaders.glUseProgram(self.shader) + glCallList(self.triList) + shaders.glUseProgram(0) + #glCallList(self.meshList) diff --git a/opengl/items/GLVolumeItem.py b/opengl/items/GLVolumeItem.py index 548cd2bd..a261a573 100644 --- a/opengl/items/GLVolumeItem.py +++ b/opengl/items/GLVolumeItem.py @@ -6,23 +6,28 @@ import numpy as np __all__ = ['GLVolumeItem'] class GLVolumeItem(GLGraphicsItem): + def __init__(self, data, sliceDensity=1, smooth=True): + self.sliceDensity = sliceDensity + self.smooth = smooth + self.data = data + GLGraphicsItem.__init__(self) + 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) + if self.smooth: + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) + else: + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_NEAREST) + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_NEAREST) 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) + shape = self.data.shape + + glTexImage3D(GL_TEXTURE_3D, 0, GL_RGBA, shape[0], shape[1], shape[2], 0, GL_RGBA, GL_UNSIGNED_BYTE, self.data.transpose((2,1,0,3))) glDisable(GL_TEXTURE_3D) self.lists = {} @@ -40,14 +45,16 @@ class GLVolumeItem(GLGraphicsItem): glEnable(GL_TEXTURE_3D) glBindTexture(GL_TEXTURE_3D, self.texture) - glDisable(GL_DEPTH_TEST) + glEnable(GL_DEPTH_TEST) #glDisable(GL_CULL_FACE) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) glEnable( GL_BLEND ) glEnable( GL_ALPHA_TEST ) + glColor4f(1,1,1,1) view = self.view() - cam = view.cameraPosition() + center = QtGui.QVector3D(*[x/2. for x in self.data.shape[:3]]) + cam = self.mapFromParent(view.cameraPosition()) - center cam = np.array([cam.x(), cam.y(), cam.z()]) ax = np.argmax(abs(cam)) d = 1 if cam[ax] > 0 else -1 @@ -55,7 +62,6 @@ class GLVolumeItem(GLGraphicsItem): glDisable(GL_TEXTURE_3D) def drawVolume(self, ax, d): - slices = 256 N = 5 imax = [0,1,2] @@ -63,31 +69,35 @@ class GLVolumeItem(GLGraphicsItem): 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 + nudge = [0.5/x for x in self.data.shape] + tp[0][imax[0]] = 0+nudge[imax[0]] + tp[0][imax[1]] = 0+nudge[imax[1]] + tp[1][imax[0]] = 1-nudge[imax[0]] + tp[1][imax[1]] = 0+nudge[imax[1]] + tp[2][imax[0]] = 1-nudge[imax[0]] + tp[2][imax[1]] = 1-nudge[imax[1]] + tp[3][imax[0]] = 0+nudge[imax[0]] + tp[3][imax[1]] = 1-nudge[imax[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 + vp[0][imax[0]] = 0 + vp[0][imax[1]] = 0 + vp[1][imax[0]] = self.data.shape[imax[0]] + vp[1][imax[1]] = 0 + vp[2][imax[0]] = self.data.shape[imax[0]] + vp[2][imax[1]] = self.data.shape[imax[1]] + vp[3][imax[0]] = 0 + vp[3][imax[1]] = self.data.shape[imax[1]] + slices = self.data.shape[ax] * self.sliceDensity r = range(slices) if d == -1: r = r[::-1] glBegin(GL_QUADS) + tzVals = np.linspace(nudge[ax], 1.0-nudge[ax], slices) + vzVals = np.linspace(0, self.data.shape[ax], slices) for i in r: - z = float(i)/(slices-1.) - w = float(i)*10./(slices-1.) - 5. + z = tzVals[i] + w = vzVals[i] tp[0][ax] = z tp[1][ax] = z diff --git a/opengl/shaders.py b/opengl/shaders.py new file mode 100644 index 00000000..b1216e35 --- /dev/null +++ b/opengl/shaders.py @@ -0,0 +1,41 @@ +from OpenGL.GL import * +from OpenGL.GL import shaders + +## For centralizing and managing vertex/fragment shader programs. + + +Shaders = { + 'balloon': ( ## increases fragment alpha as the normal turns orthogonal to the view + """ + varying vec3 normal; + void main() { + normal = normalize(gl_NormalMatrix * gl_Normal); + //vec4 color = normal; + //normal.w = min(color.w + 2.0 * color.w * pow(normal.x*normal.x + normal.y*normal.y, 2.0), 1.0); + gl_FrontColor = gl_Color; + gl_BackColor = gl_Color; + gl_Position = ftransform(); + } + """, + """ + varying vec3 normal; + void main() { + vec4 color = gl_Color; + color.w = min(color.w + 2.0 * color.w * pow(normal.x*normal.x + normal.y*normal.y, 5.0), 1.0); + gl_FragColor = color; + } + """ + ), +} +CompiledShaders = {} + +def getShader(name): + global Shaders, CompiledShaders + + if name not in CompiledShaders: + vshader, fshader = Shaders[name] + vcomp = shaders.compileShader(vshader, GL_VERTEX_SHADER) + fcomp = shaders.compileShader(fshader, GL_FRAGMENT_SHADER) + prog = shaders.compileProgram(vcomp, fcomp) + CompiledShaders[name] = prog, vcomp, fcomp + return CompiledShaders[name][0]