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
This commit is contained in:
Luke Campagnola 2012-11-23 17:34:22 -05:00
parent 9b41c90034
commit aca9c8310f
14 changed files with 1532 additions and 248 deletions

72
examples/GLIsosurface.py Normal file
View File

@ -0,0 +1,72 @@
# -*- 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_()

View File

@ -1,7 +1,8 @@
# -*- 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:

98
examples/GLSurfacePlot.py Normal file
View File

@ -0,0 +1,98 @@
# -*- 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_()

108
examples/GLshaders.py Normal file
View File

@ -0,0 +1,108 @@
# -*- 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_()

View File

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

View File

@ -1,5 +1,31 @@
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()

View File

@ -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)
self.opts = {
@ -131,6 +139,16 @@ class GLViewWidget(QtOpenGL.QGLWidget):
glMatrixMode(GL_MODELVIEW)
glPopMatrix()
def setCameraPosition(self, pos=None, distance=None, elevation=None, azimuth=None):
if distance is not None:
self.opts['distance'] = distance
if elevation is not None:
self.opts['elevation'] = elevation
if azimuth is not None:
self.opts['azimuth'] = azimuth
self.update()
def cameraPosition(self):
"""Return current position of camera based on center, dist, elevation, and azimuth"""

View File

@ -1,5 +1,6 @@
from pyqtgraph.Qt import QtGui
import pyqtgraph.functions as fn
import numpy as np
class MeshData(object):
"""
@ -10,148 +11,400 @@ class MeshData(object):
- list of triangles
- colors per vertex, edge, or tri
- normals per vertex or tri
This class handles conversion between the standard [list of vertexes, list of faces]
format (suitable for use with glDrawElements) and 'indexed' [list of vertexes] format
(suitable for use with glDrawArrays). It will automatically compute face normal
vectors as well as averaged vertex normal vectors.
The class attempts to be as efficient as possible in caching conversion results and
avoiding unnecessary conversions.
"""
def __init__(self):
self._vertexes = []
def __init__(self, vertexes=None, faces=None, edges=None, vertexColors=None, faceColors=None):
"""
============= =====================================================
Arguments
vertexes (Nv, 3) array of vertex coordinates.
If faces is not specified, then this will instead be
interpreted as (Nf, 3, 3) array of coordinates.
faces (Nf, 3) array of indexes into the vertex array.
edges [not available yet]
vertexColors (Nv, 4) array of vertex colors.
If faces is not specified, then this will instead be
interpreted as (Nf, 3, 4) array of colors.
faceColors (Nf, 4) array of face colors.
============= =====================================================
All arguments are optional.
"""
self._vertexes = None # (Nv,3) array of vertex coordinates
self._vertexesIndexedByFaces = None # (Nf, 3, 3) array of vertex coordinates
self._vertexesIndexedByEdges = None # (Ne, 2, 3) array of vertex coordinates
## mappings between vertexes, faces, and edges
self._faces = None # Nx3 array of indexes into self._vertexes specifying three vertexes for each face
self._edges = None
self._faces = []
self._vertexFaces = None ## maps vertex ID to a list of face IDs
self._vertexNormals = None
self._faceNormals = None
self._vertexColors = None
self._edgeColors = None
self._faceColors = None
self._meshColor = (1, 1, 1, 0.1) # default color to use if no face/edge/vertex colors are given
self._vertexFaces = None ## maps vertex ID to a list of face IDs (inverse mapping of _faces)
self._vertexEdges = None ## maps vertex ID to a list of edge IDs (inverse mapping of _edges)
def setFaces(self, faces, vertexes=None):
"""
Set the faces in this data set.
Data may be provided either as an Nx3x3 list of floats (9 float coordinate values per face)::
## Per-vertex data
self._vertexNormals = None # (Nv, 3) array of normals, one per vertex
self._vertexNormalsIndexedByFaces = None # (Nf, 3, 3) array of normals
self._vertexColors = None # (Nv, 3) array of colors
self._vertexColorsIndexedByFaces = None # (Nf, 3, 4) array of colors
self._vertexColorsIndexedByEdges = None # (Nf, 2, 4) array of colors
faces = [ [(x, y, z), (x, y, z), (x, y, z)], ... ]
## Per-face data
self._faceNormals = None # (Nf, 3) array of face normals
self._faceNormalsIndexedByFaces = None # (Nf, 3, 3) array of face normals
self._faceColors = None # (Nf, 4) array of face colors
self._faceColorsIndexedByFaces = None # (Nf, 3, 4) array of face colors
self._faceColorsIndexedByEdges = None # (Ne, 2, 4) array of face colors
## Per-edge data
self._edgeColors = None # (Ne, 4) array of edge colors
self._edgeColorsIndexedByEdges = None # (Ne, 2, 4) array of edge colors
#self._meshColor = (1, 1, 1, 0.1) # default color to use if no face/edge/vertex colors are given
if vertexes is not None:
if faces is None:
self.setVertexes(vertexes, indexed='faces')
if vertexColors is not None:
self.setVertexColors(vertexColors, indexed='faces')
if faceColors is not None:
self.setFaceColors(faceColors, indexed='faces')
else:
self.setVertexes(vertexes)
self.setFaces(faces)
if vertexColors is not None:
self.setVertexColors(vertexColors)
if faceColors is not None:
self.setFaceColors(faceColors)
or as an Nx3 list of ints (vertex integers) AND an Mx3 list of floats (3 float coordinate values per vertex)::
faces = [ (p1, p2, p3), ... ]
vertexes = [ (x, y, z), ... ]
#self.setFaces(vertexes=vertexes, faces=faces, vertexColors=vertexColors, faceColors=faceColors)
"""
if vertexes is None:
self._setUnindexedFaces(faces)
#def setFaces(self, vertexes=None, faces=None, vertexColors=None, faceColors=None):
#"""
#Set the faces in this data set.
#Data may be provided either as an Nx3x3 array of floats (9 float coordinate values per face)::
#faces = [ [(x, y, z), (x, y, z), (x, y, z)], ... ]
#or as an Nx3 array of ints (vertex integers) AND an Mx3 array of floats (3 float coordinate values per vertex)::
#faces = [ (p1, p2, p3), ... ]
#vertexes = [ (x, y, z), ... ]
#"""
#if not isinstance(vertexes, np.ndarray):
#vertexes = np.array(vertexes)
#if vertexes.dtype != np.float:
#vertexes = vertexes.astype(float)
#if faces is None:
#self._setIndexedFaces(vertexes, vertexColors, faceColors)
#else:
#self._setUnindexedFaces(faces, vertexes, vertexColors, faceColors)
##print self.vertexes().shape
##print self.faces().shape
#def setMeshColor(self, color):
#"""Set the color of the entire mesh. This removes any per-face or per-vertex colors."""
#color = fn.Color(color)
#self._meshColor = color.glColor()
#self._vertexColors = None
#self._faceColors = None
#def __iter__(self):
#"""Iterate over all faces, yielding a list of three tuples [(position, normal, color), ...] for each face."""
#vnorms = self.vertexNormals()
#vcolors = self.vertexColors()
#for i in range(self._faces.shape[0]):
#face = []
#for j in [0,1,2]:
#vind = self._faces[i,j]
#pos = self._vertexes[vind]
#norm = vnorms[vind]
#if vcolors is None:
#color = self._meshColor
#else:
#color = vcolors[vind]
#face.append((pos, norm, color))
#yield face
#def __len__(self):
#return len(self._faces)
def faces(self):
"""Return an array (Nf, 3) of vertex indexes, three per triangular face in the mesh."""
return self._faces
def setFaces(self, faces):
"""Set the (Nf, 3) array of faces. Each rown in the array contains
three indexes into the vertex array, specifying the three corners
of a triangular face."""
self._faces = faces
self._vertexFaces = None
self._vertexesIndexedByFaces = None
self.resetNormals()
self._vertexColorsIndexedByFaces = None
self._faceColorsIndexedByFaces = None
def vertexes(self, indexed=None):
"""Return an array (N,3) of the positions of vertexes in the mesh.
By default, each unique vertex appears only once in the array.
If indexed is 'faces', then the array will instead contain three vertexes
per face in the mesh (and a single vertex may appear more than once in the array)."""
if indexed is None:
if self._vertexes is None and self._vertexesIndexedByFaces is not None:
self._computeUnindexedVertexes()
return self._vertexes
elif indexed == 'faces':
if self._vertexesIndexedByFaces is None and self._vertexes is not None:
self._vertexesIndexedByFaces = self._vertexes[self.faces()]
return self._vertexesIndexedByFaces
else:
self._setIndexedFaces(faces, vertexes)
raise Exception("Invalid indexing mode. Accepts: None, 'faces'")
def setVertexes(self, verts=None, indexed=None, resetNormals=True):
"""
Set the array (Nv, 3) of vertex coordinates.
If indexed=='faces', then the data must have shape (Nf, 3, 3) and is
assumed to be already indexed as a list of faces.
This will cause any pre-existing normal vectors to be cleared
unless resetNormals=False.
"""
if indexed is None:
if verts is not None:
self._vertexes = verts
self._vertexesIndexedByFaces = None
elif indexed=='faces':
self._vertexes = None
if verts is not None:
self._vertexesIndexedByFaces = verts
else:
raise Exception("Invalid indexing mode. Accepts: None, 'faces'")
if resetNormals:
self.resetNormals()
def setMeshColor(self, color):
"""Set the color of the entire mesh. This removes any per-face or per-vertex colors."""
color = fn.Color(color)
self._meshColor = color.glColor()
self._vertexColors = None
self._faceColors = None
def resetNormals(self):
self._vertexNormals = None
self._vertexNormalsIndexedByFaces = None
self._faceNormals = None
self._faceNormalsIndexedByFaces = None
def hasFaceIndexedData(self):
"""Return True if this object already has vertex positions indexed by face"""
return self._vertexesIndexedByFaces is not None
def _setUnindexedFaces(self, faces):
verts = {}
self._faces = []
def hasEdgeIndexedData(self):
return self._vertexesIndexedByEdges is not None
def hasVertexColor(self):
"""Return True if this data set has vertex color information"""
for v in (self._vertexColors, self._vertexColorsIndexedByFaces, self._vertexColorsIndexedByEdges):
if v is not None:
return True
return False
def hasFaceColor(self):
"""Return True if this data set has face color information"""
for v in (self._faceColors, self._faceColorsIndexedByFaces, self._faceColorsIndexedByEdges):
if v is not None:
return True
return False
def faceNormals(self, indexed=None):
"""
Return an array (Nf, 3) of normal vectors for each face.
If indexed='faces', then instead return an indexed array
(Nf, 3, 3) (this is just the same array with each vector
copied three times).
"""
if self._faceNormals is None:
v = self.vertexes(indexed='faces')
self._faceNormals = np.cross(v[:,1]-v[:,0], v[:,2]-v[:,0])
if indexed is None:
return self._faceNormals
elif indexed == 'faces':
if self._faceNormalsIndexedByFaces is None:
norms = np.empty((self._faceNormals.shape[0], 3, 3))
norms[:] = self._faceNormals[:,np.newaxis,:]
self._faceNormalsIndexedByFaces = norms
return self._faceNormalsIndexedByFaces
else:
raise Exception("Invalid indexing mode. Accepts: None, 'faces'")
def vertexNormals(self, indexed=None):
"""
Return an array of normal vectors.
By default, the array will be (N, 3) with one entry per unique vertex in the mesh.
If indexed is 'faces', then the array will contain three normal vectors per face
(and some vertexes may be repeated).
"""
if self._vertexNormals is None:
faceNorms = self.faceNormals()
vertFaces = self.vertexFaces()
self._vertexNormals = np.empty(self._vertexes.shape, dtype=float)
for vindex in xrange(self._vertexes.shape[0]):
norms = faceNorms[vertFaces[vindex]] ## get all face normals
norm = norms.sum(axis=0) ## sum normals
norm /= (norm**2).sum()**0.5 ## and re-normalize
self._vertexNormals[vindex] = norm
if indexed is None:
return self._vertexNormals
elif indexed == 'faces':
return self._vertexNormals[self.faces()]
else:
raise Exception("Invalid indexing mode. Accepts: None, 'faces'")
def vertexColors(self, indexed=None):
"""
Return an array (Nv, 4) of vertex colors.
If indexed=='faces', then instead return an indexed array
(Nf, 3, 4).
"""
if indexed is None:
return self._vertexColors
elif indexed == 'faces':
if self._vertexColorsIndexedByFaces is None:
self._vertexColorsIndexedByFaces = self._vertexColors[self.faces()]
return self._vertexColorsIndexedByFaces
else:
raise Exception("Invalid indexing mode. Accepts: None, 'faces'")
def setVertexColors(self, colors, indexed=None):
"""
Set the vertex color array (Nv, 4).
If indexed=='faces', then the array will be interpreted
as indexed and should have shape (Nf, 3, 4)
"""
if indexed is None:
self._vertexColors = colors
self._vertexColorsIndexedByFaces = None
elif indexed == 'faces':
self._vertexColors = None
self._vertexColorsIndexedByFaces = colors
else:
raise Exception("Invalid indexing mode. Accepts: None, 'faces'")
def faceColors(self, indexed=None):
"""
Return an array (Nf, 4) of face colors.
If indexed=='faces', then instead return an indexed array
(Nf, 3, 4) (note this is just the same array with each color
repeated three times).
"""
if indexed is None:
return self._faceColors
elif indexed == 'faces':
if self._faceColorsIndexedByFaces is None and self._faceColors is not None:
Nf = self._faceColors.shape[0]
self._faceColorsIndexedByFaces = np.empty((Nf, 3, 4), dtype=self._faceColors.dtype)
self._faceColorsIndexedByFaces[:] = self._faceColors.reshape(Nf, 1, 4)
return self._faceColorsIndexedByFaces
else:
raise Exception("Invalid indexing mode. Accepts: None, 'faces'")
def setFaceColors(self, colors, indexed=None):
"""
Set the face color array (Nf, 4).
If indexed=='faces', then the array will be interpreted
as indexed and should have shape (Nf, 3, 4)
"""
if indexed is None:
self._faceColors = colors
self._faceColorsIndexedByFaces = None
elif indexed == 'faces':
self._faceColors = None
self._faceColorsIndexedByFaces = colors
else:
raise Exception("Invalid indexing mode. Accepts: None, 'faces'")
def faceCount(self):
"""
Return the number of faces in the mesh.
"""
if self._faces is not None:
return self._faces.shape[0]
elif self._vertexesIndexedByFaces is not None:
return self._vertexesIndexedByFaces.shape[0]
def edgeColors(self):
return self._edgeColors
#def _setIndexedFaces(self, faces, vertexColors=None, faceColors=None):
#self._vertexesIndexedByFaces = faces
#self._vertexColorsIndexedByFaces = vertexColors
#self._faceColorsIndexedByFaces = faceColors
def _computeUnindexedVertexes(self):
## Given (Nv, 3, 3) array of vertexes-indexed-by-face, convert backward to unindexed vertexes
## This is done by collapsing into a list of 'unique' vertexes (difference < 1e-14)
## I think generally this should be discouraged..
faces = self._vertexesIndexedByFaces
verts = {} ## used to remember the index of each vertex position
self._faces = np.empty(faces.shape[:2], dtype=np.uint)
self._vertexes = []
self._vertexFaces = []
self._faceNormals = None
self._vertexNormals = None
for face in faces:
for i in xrange(faces.shape[0]):
face = faces[i]
inds = []
for pt in face:
for j in range(face.shape[0]):
pt = face[j]
pt2 = tuple([round(x*1e14) for x in pt]) ## quantize to be sure that nearly-identical points will be merged
index = verts.get(pt2, None)
if index is None:
self._vertexes.append(QtGui.QVector3D(*pt))
#self._vertexes.append(QtGui.QVector3D(*pt))
self._vertexes.append(pt)
self._vertexFaces.append([])
index = len(self._vertexes)-1
verts[pt2] = index
self._vertexFaces[index].append(len(self._faces))
inds.append(index)
self._faces.append(tuple(inds))
self._vertexFaces[index].append(i) # keep track of which vertexes belong to which faces
self._faces[i,j] = index
self._vertexes = np.array(self._vertexes, dtype=float)
def _setIndexedFaces(self, faces, vertexes):
self._vertexes = [QtGui.QVector3D(*v) for v in vertexes]
self._faces = faces
self._edges = None
self._vertexFaces = None
self._faceNormals = None
self._vertexNormals = None
#def _setUnindexedFaces(self, faces, vertexes, vertexColors=None, faceColors=None):
#self._vertexes = vertexes #[QtGui.QVector3D(*v) for v in vertexes]
#self._faces = faces.astype(np.uint)
#self._edges = None
#self._vertexFaces = None
#self._faceNormals = None
#self._vertexNormals = None
#self._vertexColors = vertexColors
#self._faceColors = faceColors
def vertexFaces(self):
"""
Return list mapping each vertex index to a list of face indexes that use the vertex.
"""
if self._vertexFaces is None:
self._vertexFaces = [[]] * len(self._vertexes)
for i, face in enumerate(self._faces):
self._vertexFaces = [None] * len(self.vertexes())
for i in xrange(self._faces.shape[0]):
face = self._faces[i]
for ind in face:
if len(self._vertexFaces[ind]) == 0:
if self._vertexFaces[ind] is None:
self._vertexFaces[ind] = [] ## need a unique/empty list to fill
self._vertexFaces[ind].append(i)
return self._vertexFaces
def __iter__(self):
"""Iterate over all faces, yielding a list of three tuples [(position, normal, color), ...] for each face."""
vnorms = self.vertexNormals()
vcolors = self.vertexColors()
for i in range(len(self._faces)):
face = []
for j in [0,1,2]:
vind = self._faces[i][j]
pos = self._vertexes[vind]
norm = vnorms[vind]
if vcolors is None:
color = self._meshColor
else:
color = vcolors[vind]
face.append((pos, norm, color))
yield face
def faceNormals(self):
"""
Computes and stores normal of each face.
"""
if self._faceNormals is None:
self._faceNormals = []
for i, face in enumerate(self._faces):
## compute face normal
pts = [self._vertexes[vind] for vind in face]
norm = QtGui.QVector3D.crossProduct(pts[1]-pts[0], pts[2]-pts[0])
norm = norm / norm.length() ## don't use .normalized(); doesn't work for small values.
self._faceNormals.append(norm)
return self._faceNormals
def vertexNormals(self):
"""
Assigns each vertex the average of its connected face normals.
If face normals have not been computed yet, then generateFaceNormals will be called.
"""
if self._vertexNormals is None:
faceNorms = self.faceNormals()
vertFaces = self.vertexFaces()
self._vertexNormals = []
for vindex in range(len(self._vertexes)):
#print vertFaces[vindex]
norms = [faceNorms[findex] for findex in vertFaces[vindex]]
norm = QtGui.QVector3D()
for fn in norms:
norm += fn
norm = norm / norm.length() ## don't use .normalize(); doesn't work for small values.
self._vertexNormals.append(norm)
return self._vertexNormals
def vertexColors(self):
return self._vertexColors
def faceColors(self):
return self._faceColors
def edgeColors(self):
return self._edgeColors
#def reverseNormals(self):
#"""
#Reverses the direction of all normal vectors.
@ -168,7 +421,21 @@ class MeshData(object):
def save(self):
"""Serialize this mesh to a string appropriate for disk storage"""
import pickle
names = ['_vertexes', '_edges', '_faces', '_vertexFaces', '_vertexNormals', '_faceNormals', '_vertexColors', '_edgeColors', '_faceColors', '_meshColor']
if self._faces is not None:
names = ['_vertexes', '_faces']
else:
names = ['_vertexesIndexedByFaces']
if self._vertexColors is not None:
names.append('_vertexColors')
elif self._vertexColorsIndexedByFaces is not None:
names.append('_vertexColorsIndexedByFaces')
if self._faceColors is not None:
names.append('_faceColors')
elif self._faceColorsIndexedByFaces is not None:
names.append('_faceColorsIndexedByFaces')
state = {n:getattr(self, n) for n in names}
return pickle.dumps(state)
@ -178,6 +445,45 @@ class MeshData(object):
state = pickle.loads(state)
for k in state:
setattr(self, k, state[k])
def sphere(rows, cols, radius=1.0, offset=True):
"""
Return a MeshData instance with vertexes and faces computed
for a spherical surface.
"""
verts = np.empty((rows+1, cols, 3), dtype=float)
## compute vertexes
phi = (np.arange(rows+1) * np.pi / rows).reshape(rows+1, 1)
s = radius * np.sin(phi)
verts[...,2] = radius * np.cos(phi)
th = ((np.arange(cols) * 2 * np.pi / cols).reshape(1, cols))
if offset:
th = th + ((np.pi / cols) * np.arange(rows+1).reshape(rows+1,1)) ## rotate each row by 1/2 column
verts[...,0] = s * np.cos(th)
verts[...,1] = s * np.sin(th)
verts = verts.reshape((rows+1)*cols, 3)[cols-1:-(cols-1)] ## remove redundant vertexes from top and bottom
## compute faces
faces = np.empty((rows*cols*2, 3), dtype=np.uint)
rowtemplate1 = ((np.arange(cols).reshape(cols, 1) + np.array([[0, 1, 0]])) % cols) + np.array([[0, 0, cols]])
rowtemplate2 = ((np.arange(cols).reshape(cols, 1) + np.array([[0, 1, 1]])) % cols) + np.array([[cols, 0, cols]])
for row in range(rows):
start = row * cols * 2
faces[start:start+cols] = rowtemplate1 + row * cols
faces[start+cols:start+(cols*2)] = rowtemplate2 + row * cols
faces = faces[cols:-cols] ## cut off zero-area triangles at top and bottom
## adjust for redundant vertexes that were removed from top and bottom
vmin = cols-1
faces[faces<vmin] = vmin
faces -= vmin
vmax = verts.shape[0]-1
faces[faces>vmax] = vmax
return MeshData(vertexes=verts, faces=faces)

View File

@ -11,8 +11,9 @@ class GLGridItem(GLGraphicsItem):
Displays a wire-grame grid.
"""
def __init__(self, size=None, color=None):
def __init__(self, size=None, color=None, glOptions='translucent'):
GLGraphicsItem.__init__(self)
self.setGLOptions(glOptions)
if size is None:
size = QtGui.QVector3D(1,1,1)
self.setSize(size=size)
@ -34,10 +35,10 @@ class GLGridItem(GLGraphicsItem):
def paint(self):
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glEnable( GL_BLEND )
glEnable( GL_ALPHA_TEST )
self.setupGLState()
#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 )

View File

@ -16,65 +16,161 @@ class GLMeshItem(GLGraphicsItem):
Displays a 3D triangle mesh.
"""
def __init__(self, faces, vertexes=None):
def __init__(self, **kwds):
"""
See :class:`MeshData <pyqtgraph.opengl.MeshData>` for initialization arguments.
============== =====================================================
Arguments
meshdata MeshData object from which to determine geometry for
this item.
color Default color used if no vertex or face colors are
specified.
shader Name of shader program to use (None for no shader)
smooth If True, normal vectors are computed for each vertex
and interpolated within each face.
computeNormals If False, then computation of normal vectors is
disabled. This can provide a performance boost for
meshes that do not make use of normals.
============== =====================================================
"""
if isinstance(faces, MeshData):
self.data = faces
else:
self.data = MeshData()
self.data.setFaces(faces, vertexes)
self.opts = {
'meshdata': None,
'color': (1., 1., 1., 1.),
'shader': None,
'smooth': True,
'computeNormals': True,
}
GLGraphicsItem.__init__(self)
glopts = kwds.pop('glOptions', 'opaque')
self.setGLOptions(glopts)
shader = kwds.pop('shader', None)
self.setShader(shader)
def initializeGL(self):
self.shader = shaders.getShaderProgram('balloon')
self.setMeshData(**kwds)
l = glGenLists(1)
self.triList = l
glNewList(l, GL_COMPILE)
## storage for data compiled from MeshData object
self.vertexes = None
self.normals = None
self.colors = None
self.faces = None
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 face in self.data:
for (pos, norm, color) in face:
glColor4f(*color)
glNormal3f(norm.x(), norm.y(), norm.z())
glVertex3f(pos.x(), pos.y(), pos.z())
glEnd()
glEndList()
def setShader(self, shader):
self.opts['shader'] = shader
self.update()
def shader(self):
return shaders.getShaderProgram(self.opts['shader'])
#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 setMeshData(self, **kwds):
"""
Set mesh data for this item. This can be invoked two ways:
1. Specify *meshdata* argument with a new MeshData object
2. Specify keyword arguments to be passed to MeshData(..) to create a new instance.
"""
md = kwds.get('meshdata', None)
if md is None:
opts = {}
for k in ['vertexes', 'faces', 'edges', 'vertexColors', 'faceColors']:
try:
opts[k] = kwds.pop(k)
except KeyError:
pass
md = MeshData(**opts)
self.opts['meshdata'] = md
self.opts.update(kwds)
self.meshDataChanged()
self.update()
def meshDataChanged(self):
"""
This method must be called to inform the item that the MeshData object
has been altered.
"""
self.vertexes = None
self.faces = None
self.normals = None
self.colors = None
self.update()
def parseMeshData(self):
## interpret vertex / normal data before drawing
## This can:
## - automatically generate normals if they were not specified
## - pull vertexes/noormals/faces from MeshData if that was specified
if self.vertexes is not None and self.normals is not None:
return
#if self.opts['normals'] is None:
#if self.opts['meshdata'] is None:
#self.opts['meshdata'] = MeshData(vertexes=self.opts['vertexes'], faces=self.opts['faces'])
if self.opts['meshdata'] is not None:
md = self.opts['meshdata']
if self.opts['smooth'] and not md.hasFaceIndexedData():
self.vertexes = md.vertexes()
if self.opts['computeNormals']:
self.normals = md.vertexNormals()
self.faces = md.faces()
if md.hasVertexColor():
self.colors = md.vertexColors()
if md.hasFaceColor():
self.colors = md.faceColors()
else:
self.vertexes = md.vertexes(indexed='faces')
if self.opts['computeNormals']:
if self.opts['smooth']:
self.normals = md.vertexNormals(indexed='faces')
else:
self.normals = md.faceNormals(indexed='faces')
self.faces = None
if md.hasVertexColor():
self.colors = md.vertexColors(indexed='faces')
elif md.hasFaceColor():
self.colors = md.faceColors(indexed='faces')
return
def paint(self):
with self.shader:
glCallList(self.triList)
#shaders.glUseProgram(self.shader)
#glCallList(self.triList)
#shaders.glUseProgram(0)
#glCallList(self.meshList)
self.setupGLState()
self.parseMeshData()
with self.shader():
verts = self.vertexes
norms = self.normals
color = self.colors
faces = self.faces
if verts is None:
return
glEnableClientState(GL_VERTEX_ARRAY)
try:
glVertexPointerf(verts)
if self.colors is None:
color = self.opts['color']
if isinstance(color, QtGui.QColor):
glColor4f(*fn.glColor(color))
else:
glColor4f(*color)
else:
glEnableClientState(GL_COLOR_ARRAY)
glColorPointerf(color)
if norms is not None:
glEnableClientState(GL_NORMAL_ARRAY)
glNormalPointerf(norms)
if faces is None:
glDrawArrays(GL_TRIANGLES, 0, np.product(verts.shape[:-1]))
else:
faces = faces.astype(np.uint).flatten()
glDrawElements(GL_TRIANGLES, faces.shape[0], GL_UNSIGNED_INT, faces)
finally:
glDisableClientState(GL_NORMAL_ARRAY)
glDisableClientState(GL_VERTEX_ARRAY)
glDisableClientState(GL_COLOR_ARRAY)

View File

@ -12,6 +12,8 @@ class GLScatterPlotItem(GLGraphicsItem):
def __init__(self, **kwds):
GLGraphicsItem.__init__(self)
glopts = kwds.pop('glOptions', 'additive')
self.setGLOptions(glopts)
self.pos = []
self.size = 10
self.color = [1.0,1.0,1.0,0.5]
@ -71,27 +73,27 @@ class GLScatterPlotItem(GLGraphicsItem):
glBindTexture(GL_TEXTURE_2D, self.pointTexture)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, pData.shape[0], pData.shape[1], 0, GL_RGBA, GL_UNSIGNED_BYTE, pData)
self.shader = shaders.getShaderProgram('point_sprite')
self.shader = shaders.getShaderProgram('pointSprite')
#def getVBO(self, name):
#if name not in self.vbo:
#self.vbo[name] = vbo.VBO(getattr(self, name).astype('f'))
#return self.vbo[name]
def setupGLState(self):
"""Prepare OpenGL state for drawing. This function is called immediately before painting."""
#glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) ## requires z-sorting to render properly.
glBlendFunc(GL_SRC_ALPHA, GL_ONE)
glEnable( GL_BLEND )
glEnable( GL_ALPHA_TEST )
glDisable( GL_DEPTH_TEST )
#def setupGLState(self):
#"""Prepare OpenGL state for drawing. This function is called immediately before painting."""
##glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) ## requires z-sorting to render properly.
#glBlendFunc(GL_SRC_ALPHA, GL_ONE)
#glEnable( GL_BLEND )
#glEnable( GL_ALPHA_TEST )
#glDisable( GL_DEPTH_TEST )
#glEnable( GL_POINT_SMOOTH )
##glEnable( GL_POINT_SMOOTH )
#glHint(GL_POINT_SMOOTH_HINT, GL_NICEST)
#glPointParameterfv(GL_POINT_DISTANCE_ATTENUATION, (0, 0, -1e-3))
#glPointParameterfv(GL_POINT_SIZE_MAX, (65500,))
#glPointParameterfv(GL_POINT_SIZE_MIN, (0,))
##glHint(GL_POINT_SMOOTH_HINT, GL_NICEST)
##glPointParameterfv(GL_POINT_DISTANCE_ATTENUATION, (0, 0, -1e-3))
##glPointParameterfv(GL_POINT_SIZE_MAX, (65500,))
##glPointParameterfv(GL_POINT_SIZE_MIN, (0,))
def paint(self):
self.setupGLState()
@ -139,7 +141,7 @@ class GLScatterPlotItem(GLGraphicsItem):
glNormalPointerf(norm)
else:
glNormal3f(self.size,0,0)
glNormal3f(self.size, 0, 0) ## vertex shader uses norm.x to determine point size
#glPointSize(self.size)
glDrawArrays(GL_POINTS, 0, len(self.pos))
finally:

View File

@ -0,0 +1,139 @@
from OpenGL.GL import *
from GLMeshItem import GLMeshItem
from .. MeshData import MeshData
from pyqtgraph.Qt import QtGui
import pyqtgraph as pg
import numpy as np
__all__ = ['GLSurfacePlotItem']
class GLSurfacePlotItem(GLMeshItem):
"""
**Bases:** :class:`GLMeshItem <pyqtgraph.opengl.GLMeshItem>`
Displays a surface plot on a regular x,y grid
"""
def __init__(self, x=None, y=None, z=None, colors=None, **kwds):
"""
The x, y, z, and colors arguments are passed to setData().
All other keyword arguments are passed to GLMeshItem.__init__().
"""
self._x = None
self._y = None
self._z = None
self._color = None
self._vertexes = None
self._meshdata = MeshData()
GLMeshItem.__init__(self, meshdata=self._meshdata, **kwds)
self.setData(x, y, z, colors)
def setData(self, x=None, y=None, z=None, colors=None):
"""
Update the data in this surface plot.
========== =====================================================================
Arguments
x,y 1D arrays of values specifying the x,y positions of vertexes in the
grid. If these are omitted, then the values will be assumed to be
integers.
z 2D array of height values for each grid vertex.
colors (width, height, 4) array of vertex colors.
========== =====================================================================
All arguments are optional.
Note that if vertex positions are updated, the normal vectors for each triangle must
be recomputed. This is somewhat expensive if the surface was initialized with smooth=False
and very expensive if smooth=True. For faster performance, initialize with
computeNormals=False and use per-vertex colors or a normal-independent shader program.
"""
if x is not None:
if self._x is None or len(x) != len(self._x):
self._vertexes = None
self._x = x
if y is not None:
if self._y is None or len(y) != len(self._y):
self._vertexes = None
self._y = y
if z is not None:
#if self._x is None:
#self._x = np.arange(z.shape[0])
#self._vertexes = None
#if self._y is None:
#self._y = np.arange(z.shape[1])
#self._vertexes = None
if self._x is not None and z.shape[0] != len(self._x):
raise Exception('Z values must have shape (len(x), len(y))')
if self._y is not None and z.shape[1] != len(self._y):
raise Exception('Z values must have shape (len(x), len(y))')
self._z = z
if self._vertexes is not None and self._z.shape != self._vertexes.shape[:2]:
self._vertexes = None
if colors is not None:
self._colors = colors
self._meshdata.setVertexColors(colors)
if self._z is None:
return
updateMesh = False
newVertexes = False
## Generate vertex and face array
if self._vertexes is None:
newVertexes = True
self._vertexes = np.empty((self._z.shape[0], self._z.shape[1], 3), dtype=float)
self.generateFaces()
self._meshdata.setFaces(self._faces)
updateMesh = True
## Copy x, y, z data into vertex array
if newVertexes or x is not None:
if x is None:
if self._x is None:
x = np.arange(self._z.shape[0])
else:
x = self._x
self._vertexes[:, :, 0] = x.reshape(len(x), 1)
updateMesh = True
if newVertexes or y is not None:
if y is None:
if self._y is None:
y = np.arange(self._z.shape[1])
else:
y = self._y
self._vertexes[:, :, 1] = y.reshape(1, len(y))
updateMesh = True
if newVertexes or z is not None:
self._vertexes[...,2] = self._z
updateMesh = True
## Update MeshData
if updateMesh:
self._meshdata.setVertexes(self._vertexes.reshape(self._vertexes.shape[0]*self._vertexes.shape[1], 3))
self.meshDataChanged()
def generateFaces(self):
cols = self._z.shape[0]-1
rows = self._z.shape[1]-1
faces = np.empty((cols*rows*2, 3), dtype=np.uint)
rowtemplate1 = np.arange(cols).reshape(cols, 1) + np.array([[0, 1, cols+1]])
rowtemplate2 = np.arange(cols).reshape(cols, 1) + np.array([[cols+1, 1, cols+2]])
for row in range(rows):
start = row * cols * 2
faces[start:start+cols] = rowtemplate1 + row * (cols+1)
faces[start+cols:start+(cols*2)] = rowtemplate2 + row * (cols+1)
self._faces = faces

View File

@ -13,7 +13,7 @@ class GLVolumeItem(GLGraphicsItem):
"""
def __init__(self, data, sliceDensity=1, smooth=True):
def __init__(self, data, sliceDensity=1, smooth=True, glOptions='translucent'):
"""
============== =======================================================================================
**Arguments:**
@ -27,6 +27,7 @@ class GLVolumeItem(GLGraphicsItem):
self.smooth = smooth
self.data = data
GLGraphicsItem.__init__(self)
self.setGLOptions(glOptions)
def initializeGL(self):
glEnable(GL_TEXTURE_3D)
@ -62,15 +63,16 @@ class GLVolumeItem(GLGraphicsItem):
def paint(self):
self.setupGLState()
glEnable(GL_TEXTURE_3D)
glBindTexture(GL_TEXTURE_3D, self.texture)
glEnable(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 )
#glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
#glEnable( GL_BLEND )
#glEnable( GL_ALPHA_TEST )
glColor4f(1,1,1,1)
view = self.view()

View File

@ -1,18 +1,22 @@
from OpenGL.GL import *
from OpenGL.GL import shaders
import re
## For centralizing and managing vertex/fragment shader programs.
def initShaders():
global Shaders
Shaders = [
ShaderProgram('balloon', [ ## increases fragment alpha as the normal turns orthogonal to the view
ShaderProgram(None, []),
## increases fragment alpha as the normal turns orthogonal to the view
## this is useful for viewing shells that enclose a volume (such as isosurfaces)
ShaderProgram('balloon', [
VertexShader("""
varying vec3 normal;
void main() {
// compute here for use in fragment shader
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();
@ -27,7 +31,154 @@ def initShaders():
}
""")
]),
ShaderProgram('point_sprite', [ ## allows specifying point size using normal.x
## colors fragments based on face normals relative to view
## This means that the colors will change depending on how the view is rotated
ShaderProgram('viewNormalColor', [
VertexShader("""
varying vec3 normal;
void main() {
// compute here for use in fragment shader
normal = normalize(gl_NormalMatrix * gl_Normal);
gl_FrontColor = gl_Color;
gl_BackColor = gl_Color;
gl_Position = ftransform();
}
"""),
FragmentShader("""
varying vec3 normal;
void main() {
vec4 color = gl_Color;
color.x = (normal.x + 1) * 0.5;
color.y = (normal.y + 1) * 0.5;
color.z = (normal.z + 1) * 0.5;
gl_FragColor = color;
}
""")
]),
## colors fragments based on absolute face normals.
ShaderProgram('normalColor', [
VertexShader("""
varying vec3 normal;
void main() {
// compute here for use in fragment shader
normal = normalize(gl_Normal);
gl_FrontColor = gl_Color;
gl_BackColor = gl_Color;
gl_Position = ftransform();
}
"""),
FragmentShader("""
varying vec3 normal;
void main() {
vec4 color = gl_Color;
color.x = (normal.x + 1) * 0.5;
color.y = (normal.y + 1) * 0.5;
color.z = (normal.z + 1) * 0.5;
gl_FragColor = color;
}
""")
]),
## very simple simulation of lighting.
## The light source position is always relative to the camera.
ShaderProgram('shaded', [
VertexShader("""
varying vec3 normal;
void main() {
// compute here for use in fragment shader
normal = normalize(gl_NormalMatrix * gl_Normal);
gl_FrontColor = gl_Color;
gl_BackColor = gl_Color;
gl_Position = ftransform();
}
"""),
FragmentShader("""
varying vec3 normal;
void main() {
float p = dot(normal, normalize(vec3(1, -1, -1)));
p = p < 0. ? 0. : p * 0.8;
vec4 color = gl_Color;
color.x = color.x * (0.2 + p);
color.y = color.y * (0.2 + p);
color.z = color.z * (0.2 + p);
gl_FragColor = color;
}
""")
]),
## colors get brighter near edges of object
ShaderProgram('edgeHilight', [
VertexShader("""
varying vec3 normal;
void main() {
// compute here for use in fragment shader
normal = normalize(gl_NormalMatrix * gl_Normal);
gl_FrontColor = gl_Color;
gl_BackColor = gl_Color;
gl_Position = ftransform();
}
"""),
FragmentShader("""
varying vec3 normal;
void main() {
vec4 color = gl_Color;
float s = pow(normal.x*normal.x + normal.y*normal.y, 2.0);
color.x = color.x + s * (1.0-color.x);
color.y = color.y + s * (1.0-color.y);
color.z = color.z + s * (1.0-color.z);
gl_FragColor = color;
}
""")
]),
## colors fragments by z-value.
## This is useful for coloring surface plots by height.
## This shader uses a uniform called "colorMap" to determine how to map the colors:
## red = pow(z * colorMap[0] + colorMap[1], colorMap[2])
## green = pow(z * colorMap[3] + colorMap[4], colorMap[5])
## blue = pow(z * colorMap[6] + colorMap[7], colorMap[8])
## (set the values like this: shader['uniformMap'] = array([...])
ShaderProgram('heightColor', [
VertexShader("""
varying vec4 pos;
void main() {
gl_FrontColor = gl_Color;
gl_BackColor = gl_Color;
pos = gl_Vertex;
gl_Position = ftransform();
}
"""),
FragmentShader("""
#version 140 // required for uniform blocks
uniform float colorMap[9];
varying vec4 pos;
out vec4 gl_FragColor;
in vec4 gl_Color;
void main() {
vec4 color = gl_Color;
color.x = colorMap[0] * (pos.z + colorMap[1]);
if (colorMap[2] != 1.0)
color.x = pow(color.x, colorMap[2]);
color.x = color.x < 0 ? 0 : (color.x > 1 ? 1 : color.x);
color.y = colorMap[3] * (pos.z + colorMap[4]);
if (colorMap[5] != 1.0)
color.y = pow(color.y, colorMap[5]);
color.y = color.y < 0 ? 0 : (color.y > 1 ? 1 : color.y);
color.z = colorMap[6] * (pos.z + colorMap[7]);
if (colorMap[8] != 1.0)
color.z = pow(color.z, colorMap[8]);
color.z = color.z < 0 ? 0 : (color.z > 1 ? 1 : color.z);
color.w = 1.0;
gl_FragColor = color;
}
"""),
], uniforms={'colorMap': [1, 1, 1, 1, 0.5, 1, 1, 0, 1]}),
ShaderProgram('pointSprite', [ ## allows specifying point size using normal.x
## See:
##
## http://stackoverflow.com/questions/9609423/applying-part-of-a-texture-sprite-sheet-texture-map-to-a-point-sprite-in-ios
@ -58,52 +209,186 @@ CompiledShaderPrograms = {}
def getShaderProgram(name):
return ShaderProgram.names[name]
class VertexShader:
def __init__(self, code):
class Shader:
def __init__(self, shaderType, code):
self.shaderType = shaderType
self.code = code
self.compiled = None
def shader(self):
if self.compiled is None:
self.compiled = shaders.compileShader(self.code, GL_VERTEX_SHADER)
try:
self.compiled = shaders.compileShader(self.code, self.shaderType)
except RuntimeError as exc:
## Format compile errors a bit more nicely
if len(exc.args) == 3:
err, code, typ = exc.args
if not err.startswith('Shader compile failure'):
raise
code = code[0].split('\n')
err, c, msgs = err.partition(':')
err = err + '\n'
msgs = msgs.split('\n')
errNums = [()] * len(code)
for i, msg in enumerate(msgs):
msg = msg.strip()
if msg == '':
continue
m = re.match(r'\d+\((\d+)\)', msg)
if m is not None:
line = int(m.groups()[0])
errNums[line-1] = errNums[line-1] + (str(i+1),)
#code[line-1] = '%d\t%s' % (i+1, code[line-1])
err = err + "%d %s\n" % (i+1, msg)
errNums = [','.join(n) for n in errNums]
maxlen = max(map(len, errNums))
code = [errNums[i] + " "*(maxlen-len(errNums[i])) + line for i, line in enumerate(code)]
err = err + '\n'.join(code)
raise Exception(err)
else:
raise
return self.compiled
class FragmentShader:
class VertexShader(Shader):
def __init__(self, code):
self.code = code
self.compiled = None
Shader.__init__(self, GL_VERTEX_SHADER, code)
class FragmentShader(Shader):
def __init__(self, code):
Shader.__init__(self, GL_FRAGMENT_SHADER, code)
def shader(self):
if self.compiled is None:
self.compiled = shaders.compileShader(self.code, GL_FRAGMENT_SHADER)
return self.compiled
class ShaderProgram:
names = {}
def __init__(self, name, shaders):
def __init__(self, name, shaders, uniforms=None):
self.name = name
ShaderProgram.names[name] = self
self.shaders = shaders
self.prog = None
self.blockData = {}
self.uniformData = {}
## parse extra options from the shader definition
if uniforms is not None:
for k,v in uniforms.items():
self[k] = v
def setBlockData(self, blockName, data):
if data is None:
del self.blockData[blockName]
else:
self.blockData[blockName] = data
def setUniformData(self, uniformName, data):
if data is None:
del self.uniformData[uniformName]
else:
self.uniformData[uniformName] = data
def __setitem__(self, item, val):
self.setUniformData(item, val)
def __delitem__(self, item):
self.setUniformData(item, None)
def program(self):
if self.prog is None:
compiled = [s.shader() for s in self.shaders] ## compile all shaders
self.prog = shaders.compileProgram(*compiled) ## compile program
try:
compiled = [s.shader() for s in self.shaders] ## compile all shaders
self.prog = shaders.compileProgram(*compiled) ## compile program
except:
self.prog = -1
raise
return self.prog
def __enter__(self):
glUseProgram(self.program())
if len(self.shaders) > 0 and self.program() != -1:
glUseProgram(self.program())
try:
## load uniform values into program
for uniformName, data in self.uniformData.items():
loc = self.uniform(uniformName)
if loc == -1:
raise Exception('Could not find uniform variable "%s"' % uniformName)
glUniform1fv(loc, len(data), data)
### bind buffer data to program blocks
#if len(self.blockData) > 0:
#bindPoint = 1
#for blockName, data in self.blockData.items():
### Program should have a uniform block declared:
###
### layout (std140) uniform blockName {
### vec4 diffuse;
### };
### pick any-old binding point. (there are a limited number of these per-program
#bindPoint = 1
### get the block index for a uniform variable in the shader
#blockIndex = glGetUniformBlockIndex(self.program(), blockName)
### give the shader block a binding point
#glUniformBlockBinding(self.program(), blockIndex, bindPoint)
### create a buffer
#buf = glGenBuffers(1)
#glBindBuffer(GL_UNIFORM_BUFFER, buf)
#glBufferData(GL_UNIFORM_BUFFER, size, data, GL_DYNAMIC_DRAW)
### also possible to use glBufferSubData to fill parts of the buffer
### bind buffer to the same binding point
#glBindBufferBase(GL_UNIFORM_BUFFER, bindPoint, buf)
except:
glUseProgram(0)
raise
def __exit__(self, *args):
glUseProgram(0)
if len(self.shaders) > 0:
glUseProgram(0)
def uniform(self, name):
"""Return the location integer for a uniform variable in this program"""
return glGetUniformLocation(self.program(), name)
#def uniformBlockInfo(self, blockName):
#blockIndex = glGetUniformBlockIndex(self.program(), blockName)
#count = glGetActiveUniformBlockiv(self.program(), blockIndex, GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS)
#indices = []
#for i in range(count):
#indices.append(glGetActiveUniformBlockiv(self.program(), blockIndex, GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES))
class HeightColorShader(ShaderProgram):
def __enter__(self):
## Program should have a uniform block declared:
##
## layout (std140) uniform blockName {
## vec4 diffuse;
## vec4 ambient;
## };
## pick any-old binding point. (there are a limited number of these per-program
bindPoint = 1
## get the block index for a uniform variable in the shader
blockIndex = glGetUniformBlockIndex(self.program(), "blockName")
## give the shader block a binding point
glUniformBlockBinding(self.program(), blockIndex, bindPoint)
## create a buffer
buf = glGenBuffers(1)
glBindBuffer(GL_UNIFORM_BUFFER, buf)
glBufferData(GL_UNIFORM_BUFFER, size, data, GL_DYNAMIC_DRAW)
## also possible to use glBufferSubData to fill parts of the buffer
## bind buffer to the same binding point
glBindBufferBase(GL_UNIFORM_BUFFER, bindPoint, buf)
initShaders()