Commit aca9c831 authored by Luke Campagnola's avatar Luke Campagnola
Browse files

Major overhaul for GLMeshItem, MeshData classes

   [ Note: These APIs have changed significantly. ]
  - MeshData and GLMeshItem now operate on numpy arrays instead of lists.
  - MeshData can handle per-vertex and per-triangle color information
Added GLSurfacePlotItem class based on new GLMeshItem
GLGraphicsItem now has per-item support for customizing GL state (setGLOptions method)
Added several new shader programs
Added new examples:
   GLIsosurface
   GLSurfacePlot
   GLshaders
parent 9b41c900
# -*- 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()
w.setCameraPosition(distance=40)
g = gl.GLGridItem()
g.scale(2,2,1)
w.addItem(g)
import numpy as np
## Define a scalar field from which we will generate an isosurface
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)))
print("Generating isosurface..")
verts = pg.isosurface(data, data.max()/4.)
md = gl.MeshData.MeshData(vertexes=verts)
colors = np.ones((md.faceCount(), 4), dtype=float)
colors[:,3] = 0.2
colors[:,2] = np.linspace(0, 1, colors.shape[0])
md.setFaceColors(colors)
m1 = gl.GLMeshItem(meshdata=md, smooth=False, shader='balloon')
m1.setGLOptions('additive')
#w.addItem(m1)
m1.translate(-25, -25, -20)
m2 = gl.GLMeshItem(meshdata=md, smooth=True, shader='balloon')
m2.setGLOptions('additive')
w.addItem(m2)
m2.translate(-25, -25, -50)
## Start Qt event loop unless running in interactive mode.
if sys.flags.interactive != 1:
app.exec_()
# -*- coding: utf-8 -*-
"""
Simple examples demonstrating the use of GLMeshItem.
## 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
......@@ -15,52 +16,117 @@ app = QtGui.QApplication([])
w = gl.GLViewWidget()
w.show()
w.setCameraPosition(distance=40)
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)
## Example 1:
## Array of vertex positions and array of vertex indexes defining faces
## Colors are specified per-face
verts = np.array([
[0, 0, 0],
[2, 0, 0],
[1, 2, 0],
[1, 1, 1],
])
faces = np.array([
[0, 1, 2],
[0, 1, 3],
[0, 2, 3],
[1, 2, 3]
])
colors = np.array([
[1, 0, 0, 0.3],
[0, 1, 0, 0.3],
[0, 0, 1, 0.3],
[1, 1, 0, 0.3]
])
## Mesh item will automatically compute face normals.
m1 = gl.GLMeshItem(vertexes=verts, faces=faces, faceColors=colors, smooth=False)
m1.translate(5, 5, 0)
m1.setGLOptions('additive')
w.addItem(m1)
## Example 2:
## Array of vertex positions, three per face
## Colors are specified per-vertex
verts = verts[faces] ## Same mesh geometry as example 2, but now we are passing in 12 vertexes
colors = np.random.random(size=(verts.shape[0], 3, 4))
#colors[...,3] = 1.0
m2 = gl.GLMeshItem(vertexes=verts, vertexColors=colors, smooth=False, shader='balloon')
m2.translate(-5, 5, 0)
w.addItem(m2)
## Example 3:
## icosahedron
md = gl.MeshData.sphere(rows=10, cols=20)
#colors = np.random.random(size=(md.faceCount(), 4))
#colors[:,3] = 0.3
#colors[100:] = 0.0
colors = np.ones((md.faceCount(), 4), dtype=float)
colors[::2,0] = 0
colors[:,1] = np.linspace(0, 1, colors.shape[0])
md.setFaceColors(colors)
m3 = gl.GLMeshItem(meshdata=md, smooth=False)#, shader='balloon')
#m3.translate(-5, -5, 0)
w.addItem(m3)
#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 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
##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)))
#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.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..")
#verts = pg.isosurface(data, data.max()/4.)
#md = gl.MeshData.MeshData(vertexes=verts)
#colors = np.ones((md.vertexes(indexed='faces').shape[0], 4), dtype=float)
#colors[:,3] = 0.3
#colors[:,2] = np.linspace(0, 1, colors.shape[0])
#m1 = gl.GLMeshItem(meshdata=md, color=colors, smooth=False)
#w.addItem(m1)
#m1.translate(-25, -25, -20)
#m2 = gl.GLMeshItem(vertexes=verts, color=colors, smooth=True)
#w.addItem(m2)
#m2.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:
......
# -*- coding: utf-8 -*-
"""
This example demonstrates the use of GLSurfacePlotItem.
"""
## 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
import scipy.ndimage as ndi
import numpy as np
## Create a GL View widget to display data
app = QtGui.QApplication([])
w = gl.GLViewWidget()
w.show()
w.setCameraPosition(distance=50)
## Add a grid to the view
g = gl.GLGridItem()
g.scale(2,2,1)
g.setDepthValue(10) # draw grid after surfaces since they may be translucent
w.addItem(g)
## Simple surface plot example
## x, y values are not specified, so assumed to be 0:50
z = ndi.gaussian_filter(np.random.normal(size=(50,50)), (1,1))
p1 = gl.GLSurfacePlotItem(z=z, shader='shaded', color=(0.5, 0.5, 1, 1))
p1.scale(16./49., 16./49., 1.0)
p1.translate(-18, 2, 0)
w.addItem(p1)
## Saddle example with x and y specified
x = np.linspace(-8, 8, 50)
y = np.linspace(-8, 8, 50)
z = 0.1 * ((x.reshape(50,1) ** 2) - (y.reshape(1,50) ** 2))
p2 = gl.GLSurfacePlotItem(x=x, y=y, z=z, shader='normalColor')
p2.translate(-10,-10,0)
w.addItem(p2)
## Manually specified colors
z = ndi.gaussian_filter(np.random.normal(size=(50,50)), (1,1))
x = np.linspace(-12, 12, 50)
y = np.linspace(-12, 12, 50)
colors = np.ones((50,50,4), dtype=float)
colors[...,0] = np.clip(np.cos(((x.reshape(50,1) ** 2) + (y.reshape(1,50) ** 2)) ** 0.5), 0, 1)
colors[...,1] = colors[...,0]
p3 = gl.GLSurfacePlotItem(z=z, colors=colors.reshape(50*50,4), shader='shaded', smooth=False)
p3.scale(16./49., 16./49., 1.0)
p3.translate(2, -18, 0)
w.addItem(p3)
## Animated example
## compute surface vertex data
cols = 100
rows = 100
x = np.linspace(-8, 8, cols+1).reshape(cols+1,1)
y = np.linspace(-8, 8, rows+1).reshape(1,rows+1)
d = (x**2 + y**2) * 0.1
d2 = d ** 0.5 + 0.1
## precompute height values for all frames
phi = np.arange(0, np.pi*2, np.pi/20.)
z = np.sin(d[np.newaxis,...] + phi.reshape(phi.shape[0], 1, 1)) / d2[np.newaxis,...]
## create a surface plot, tell it to use the 'heightColor' shader
## since this does not require normal vectors to render (thus we
## can set computeNormals=False to save time when the mesh updates)
p4 = gl.GLSurfacePlotItem(x=x[:,0], y = y[0,:], shader='heightColor', computeNormals=False, smooth=False)
p4.shader()['colorMap'] = np.array([0.2, 2, 0.5, 0.2, 1, 1, 0.2, 0, 2])
p4.translate(10, 10, 0)
w.addItem(p4)
index = 0
def update():
global p4, z, index
index -= 1
p4.setData(z=z[index%z.shape[0]])
timer = QtCore.QTimer()
timer.timeout.connect(update)
timer.start(30)
## Start Qt event loop unless running in interactive mode.
if sys.flags.interactive != 1:
app.exec_()
# -*- coding: utf-8 -*-
"""
Demonstration of some of the shader programs included with pyqtgraph.
"""
## 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()
w.setCameraPosition(distance=15, azimuth=-90)
g = gl.GLGridItem()
g.scale(2,2,1)
w.addItem(g)
import numpy as np
md = gl.MeshData.sphere(rows=10, cols=20)
x = np.linspace(-8, 8, 6)
m1 = gl.GLMeshItem(meshdata=md, smooth=True, color=(1, 0, 0, 0.2), shader='balloon', glOptions='additive')
m1.translate(x[0], 0, 0)
m1.scale(1, 1, 2)
w.addItem(m1)
m2 = gl.GLMeshItem(meshdata=md, smooth=True, shader='normalColor', glOptions='opaque')
m2.translate(x[1], 0, 0)
m2.scale(1, 1, 2)
w.addItem(m2)
m3 = gl.GLMeshItem(meshdata=md, smooth=True, shader='viewNormalColor', glOptions='opaque')
m3.translate(x[2], 0, 0)
m3.scale(1, 1, 2)
w.addItem(m3)
m4 = gl.GLMeshItem(meshdata=md, smooth=True, shader='shaded', glOptions='opaque')
m4.translate(x[3], 0, 0)
m4.scale(1, 1, 2)
w.addItem(m4)
m5 = gl.GLMeshItem(meshdata=md, smooth=True, color=(1, 0, 0, 1), shader='edgeHilight', glOptions='opaque')
m5.translate(x[4], 0, 0)
m5.scale(1, 1, 2)
w.addItem(m5)
m6 = gl.GLMeshItem(meshdata=md, smooth=True, color=(1, 0, 0, 1), shader='heightColor', glOptions='opaque')
m6.translate(x[5], 0, 0)
m6.scale(1, 1, 2)
w.addItem(m6)
#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..")
#verts = pg.isosurface(data, data.max()/4.)
#md = gl.MeshData.MeshData(vertexes=verts)
#colors = np.ones((md.vertexes(indexed='faces').shape[0], 4), dtype=float)
#colors[:,3] = 0.3
#colors[:,2] = np.linspace(0, 1, colors.shape[0])
#m1 = gl.GLMeshItem(meshdata=md, color=colors, smooth=False)
#w.addItem(m1)
#m1.translate(-25, -25, -20)
#m2 = gl.GLMeshItem(vertexes=verts, color=colors, smooth=True)
#w.addItem(m2)
#m2.translate(-25, -25, -50)
## Start Qt event loop unless running in interactive mode.
if sys.flags.interactive != 1:
app.exec_()
......@@ -43,9 +43,12 @@ examples = OrderedDict([
])),
('3D Graphics', OrderedDict([
('Volumetric', 'GLVolumeItem.py'),
('Isosurface', 'GLMeshItem.py'),
('Image', 'GLImageItem.py'),
('Isosurface', 'GLIsosurface.py'),
('Surface Plot', 'GLSurfacePlot.py'),
('Scatter Plot', 'GLScatterPlotItem.py'),
('Shaders', 'GLshaders.py'),
('Mesh', 'GLMeshItem.py'),
('Image', 'GLImageItem.py'),
])),
('Widgets', OrderedDict([
('PlotWidget', 'PlotWidget.py'),
......@@ -127,9 +130,8 @@ class ExampleLoader(QtGui.QMainWindow):
if fn is None:
return
if sys.platform.startswith('win'):
os.spawnl(os.P_NOWAIT, sys.executable, sys.executable, '"' + fn + '"', *extra)
os.spawnl(os.P_NOWAIT, sys.executable, '"'+sys.executable+'"', '"' + fn + '"', *extra)
else:
os.spawnl(os.P_NOWAIT, sys.executable, sys.executable, fn, *extra)
......
from pyqtgraph.Qt import QtGui, QtCore
from pyqtgraph import Transform3D
from OpenGL.GL import *
from OpenGL import GL
GLOptions = {
'opaque': {
GL_DEPTH_TEST: True,
GL_BLEND: False,
GL_ALPHA_TEST: False,
GL_CULL_FACE: False,
},
'translucent': {
GL_DEPTH_TEST: True,
GL_BLEND: True,
GL_ALPHA_TEST: False,
GL_CULL_FACE: False,
'glBlendFunc': (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA),
},
'additive': {
GL_DEPTH_TEST: False,
GL_BLEND: True,
GL_ALPHA_TEST: False,
GL_CULL_FACE: False,
'glBlendFunc': (GL_SRC_ALPHA, GL_ONE),
},
}
class GLGraphicsItem(QtCore.QObject):
def __init__(self, parentItem=None):
......@@ -11,6 +37,7 @@ class GLGraphicsItem(QtCore.QObject):
self.__visible = True
self.setParentItem(parentItem)
self.setDepthValue(0)
self.__glOpts = {}
def setParentItem(self, item):
if self.__parent is not None:
......@@ -23,7 +50,52 @@ class GLGraphicsItem(QtCore.QObject):
if self.view() is not None:
self.view().removeItem(self)
self.__parent.view().addItem(self)
def setGLOptions(self, opts):
"""
Set the OpenGL state options to use immediately before drawing this item.
(Note that subclasses must call setupGLState before painting for this to work)
The simplest way to invoke this method is to pass in the name of
a predefined set of options (see the GLOptions variable):
============= ======================================================
opaque Enables depth testing and disables blending
translucent Enables depth testing and blending
Elements must be drawn sorted back-to-front for
translucency to work correctly.
additive Disables depth testing, enables blending.
Colors are added together, so sorting is not required.
============= ======================================================
It is also possible to specify any arbitrary settings as a dictionary.
This may consist of {'functionName': (args...)} pairs where functionName must
be a callable attribute of OpenGL.GL, or {GL_STATE_VAR: bool} pairs
which will be interpreted as calls to glEnable or glDisable(GL_STATE_VAR).
For example::
{
GL_ALPHA_TEST: True,
GL_CULL_FACE: False,
'glBlendFunc': (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA),
}
"""
if isinstance(opts, basestring):
opts = GLOptions[opts]
self.__glOpts = opts.copy()
def updateGLOptions(self, opts):
"""
Modify the OpenGL state options to use immediately before drawing this item.
*opts* must be a dictionary as specified by setGLOptions.
Values may also be None, in which case the key will be ignored.
"""
self.__glOpts.update(opts)
def parentItem(self):
return self.__parent
......@@ -135,13 +207,30 @@ class GLGraphicsItem(QtCore.QObject):
"""
pass
def setupGLState(self):
"""
This method is responsible for preparing the GL state options needed to render
this item (blending, depth testing, etc). The method is called immediately before painting the item.
"""
for k,v in self.__glOpts.items():
if v is None:
continue
if isinstance(k, basestring):
func = getattr(GL, k)
func(*v)
else:
if v is True:
glEnable(k)
else:
glDisable(k)
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
self.setupGLState()
def update(self):
v = self.view()
......
......@@ -12,8 +12,16 @@ class GLViewWidget(QtOpenGL.QGLWidget):
- Export options
"""
ShareWidget = None
def __init__(self, parent=None):
QtOpenGL.QGLWidget.__init__(self, parent)
if GLViewWidget.ShareWidget is None:
## create a dummy widget to allow sharing objects (textures, shaders, etc) between views
GLViewWidget.ShareWidget = QtOpenGL.QGLWidget()
QtOpenGL.QGLWidget.__init__(self, parent, GLViewWidget.ShareWidget)
self.setFocusPolicy(QtCore.Qt.ClickFocus)