From e5f383fbb5e3953d44b9767d3a73b32d2a32e480 Mon Sep 17 00:00:00 2001
From: Luke Campagnola <>
Date: Fri, 23 Nov 2012 16:01:25 -0500
Subject: [PATCH 1/3] Bugfixes and updates to functions.py: - generalized
makeARGB API: can now process arrays of arbitrary shape. - affineSlice
automatically converts vector arguments to array - new function
applyLookupTable taken from makeARGB - isosurface function returns array
Updated VideoSpeedTest example to follow new makeARGB API
LayoutWidget: row argument now accepts 'next' as value
ParameterTree bugfix: avoid infinite recursion when accessing non-existent attributes
ViewBox: avoid exit error caused when cleanup callback is invoked while python is shutting down
---
examples/VideoSpeedTest.py | 26 ++-
examples/VideoTemplate.ui | 10 +-
examples/VideoTemplate_pyqt.py | 14 +-
examples/VideoTemplate_pyside.py | 14 +-
functions.py | 367 ++++++++++++++++++-------------
graphicsItems/GraphicsItem.py | 8 +-
graphicsItems/ViewBox/ViewBox.py | 4 +-
parametertree/Parameter.py | 7 +-
widgets/LayoutWidget.py | 7 +-
9 files changed, 273 insertions(+), 184 deletions(-)
diff --git a/examples/VideoSpeedTest.py b/examples/VideoSpeedTest.py
index d51798bd..0a20e9cf 100644
--- a/examples/VideoSpeedTest.py
+++ b/examples/VideoSpeedTest.py
@@ -61,37 +61,41 @@ ui.alphaCheck.toggled.connect(updateLUT)
def updateScale():
global ui
spins = [ui.minSpin1, ui.maxSpin1, ui.minSpin2, ui.maxSpin2, ui.minSpin3, ui.maxSpin3]
- if ui.rgbCheck.isChecked():
+ if ui.rgbLevelsCheck.isChecked():
for s in spins[2:]:
s.setEnabled(True)
else:
for s in spins[2:]:
s.setEnabled(False)
-ui.rgbCheck.toggled.connect(updateScale)
+ui.rgbLevelsCheck.toggled.connect(updateScale)
cache = {}
def mkData():
global data, cache, ui
- dtype = ui.dtypeCombo.currentText()
+ dtype = (ui.dtypeCombo.currentText(), ui.rgbCheck.isChecked())
if dtype not in cache:
- if dtype == 'uint8':
+ if dtype[0] == 'uint8':
dt = np.uint8
loc = 128
scale = 64
mx = 255
- elif dtype == 'uint16':
+ elif dtype[0] == 'uint16':
dt = np.uint16
loc = 4096
scale = 1024
mx = 2**16
- elif dtype == 'float':
+ elif dtype[0] == 'float':
dt = np.float
loc = 1.0
scale = 0.1
- data = np.random.normal(size=(20,512,512), loc=loc, scale=scale)
- data = ndi.gaussian_filter(data, (0, 3, 3))
- if dtype != 'float':
+ if ui.rgbCheck.isChecked():
+ data = np.random.normal(size=(20,512,512,3), loc=loc, scale=scale)
+ data = ndi.gaussian_filter(data, (0, 6, 6, 0))
+ else:
+ data = np.random.normal(size=(20,512,512), loc=loc, scale=scale)
+ data = ndi.gaussian_filter(data, (0, 6, 6))
+ if dtype[0] != 'float':
data = np.clip(data, 0, mx)
data = data.astype(dt)
cache[dtype] = data
@@ -100,7 +104,7 @@ def mkData():
updateLUT()
mkData()
ui.dtypeCombo.currentIndexChanged.connect(mkData)
-
+ui.rgbCheck.toggled.connect(mkData)
ptr = 0
lastTime = ptime.time()
@@ -113,7 +117,7 @@ def update():
useLut = None
if ui.scaleCheck.isChecked():
- if ui.rgbCheck.isChecked():
+ if ui.rgbLevelsCheck.isChecked():
useScale = [
[ui.minSpin1.value(), ui.maxSpin1.value()],
[ui.minSpin2.value(), ui.maxSpin2.value()],
diff --git a/examples/VideoTemplate.ui b/examples/VideoTemplate.ui
index 078e7ccf..3dddb928 100644
--- a/examples/VideoTemplate.ui
+++ b/examples/VideoTemplate.ui
@@ -25,7 +25,6 @@
0
- fpsLabel
-
@@ -84,7 +83,7 @@
-
-
+
RGB
@@ -218,6 +217,13 @@
+ -
+
+
+ RGB
+
+
+
diff --git a/examples/VideoTemplate_pyqt.py b/examples/VideoTemplate_pyqt.py
index 21e66635..c3430e2d 100644
--- a/examples/VideoTemplate_pyqt.py
+++ b/examples/VideoTemplate_pyqt.py
@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file './examples/VideoTemplate.ui'
#
-# Created: Sun Sep 9 14:41:31 2012
+# Created: Sun Nov 4 18:24:20 2012
# by: PyQt4 UI code generator 4.9.1
#
# WARNING! All changes made in this file will be lost!
@@ -55,9 +55,9 @@ class Ui_MainWindow(object):
self.scaleCheck = QtGui.QCheckBox(self.centralwidget)
self.scaleCheck.setObjectName(_fromUtf8("scaleCheck"))
self.gridLayout_2.addWidget(self.scaleCheck, 3, 0, 1, 1)
- self.rgbCheck = QtGui.QCheckBox(self.centralwidget)
- self.rgbCheck.setObjectName(_fromUtf8("rgbCheck"))
- self.gridLayout_2.addWidget(self.rgbCheck, 3, 1, 1, 1)
+ self.rgbLevelsCheck = QtGui.QCheckBox(self.centralwidget)
+ self.rgbLevelsCheck.setObjectName(_fromUtf8("rgbLevelsCheck"))
+ self.gridLayout_2.addWidget(self.rgbLevelsCheck, 3, 1, 1, 1)
self.horizontalLayout = QtGui.QHBoxLayout()
self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout"))
self.minSpin1 = SpinBox(self.centralwidget)
@@ -124,6 +124,9 @@ class Ui_MainWindow(object):
self.fpsLabel.setAlignment(QtCore.Qt.AlignCenter)
self.fpsLabel.setObjectName(_fromUtf8("fpsLabel"))
self.gridLayout_2.addWidget(self.fpsLabel, 0, 0, 1, 4)
+ self.rgbCheck = QtGui.QCheckBox(self.centralwidget)
+ self.rgbCheck.setObjectName(_fromUtf8("rgbCheck"))
+ self.gridLayout_2.addWidget(self.rgbCheck, 2, 1, 1, 1)
MainWindow.setCentralWidget(self.centralwidget)
self.retranslateUi(MainWindow)
@@ -138,12 +141,13 @@ class Ui_MainWindow(object):
self.dtypeCombo.setItemText(1, QtGui.QApplication.translate("MainWindow", "uint16", None, QtGui.QApplication.UnicodeUTF8))
self.dtypeCombo.setItemText(2, QtGui.QApplication.translate("MainWindow", "float", None, QtGui.QApplication.UnicodeUTF8))
self.scaleCheck.setText(QtGui.QApplication.translate("MainWindow", "Scale Data", None, QtGui.QApplication.UnicodeUTF8))
- self.rgbCheck.setText(QtGui.QApplication.translate("MainWindow", "RGB", None, QtGui.QApplication.UnicodeUTF8))
+ self.rgbLevelsCheck.setText(QtGui.QApplication.translate("MainWindow", "RGB", None, QtGui.QApplication.UnicodeUTF8))
self.label_2.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8))
self.label_3.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8))
self.label_4.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8))
self.lutCheck.setText(QtGui.QApplication.translate("MainWindow", "Use Lookup Table", None, QtGui.QApplication.UnicodeUTF8))
self.alphaCheck.setText(QtGui.QApplication.translate("MainWindow", "alpha", None, QtGui.QApplication.UnicodeUTF8))
self.fpsLabel.setText(QtGui.QApplication.translate("MainWindow", "FPS", None, QtGui.QApplication.UnicodeUTF8))
+ self.rgbCheck.setText(QtGui.QApplication.translate("MainWindow", "RGB", None, QtGui.QApplication.UnicodeUTF8))
from pyqtgraph import SpinBox, GradientWidget, GraphicsView, RawImageWidget
diff --git a/examples/VideoTemplate_pyside.py b/examples/VideoTemplate_pyside.py
index 5cbce05c..d19e0f23 100644
--- a/examples/VideoTemplate_pyside.py
+++ b/examples/VideoTemplate_pyside.py
@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file './examples/VideoTemplate.ui'
#
-# Created: Sun Sep 9 14:41:31 2012
+# Created: Sun Nov 4 18:24:21 2012
# by: pyside-uic 0.2.13 running on PySide 1.1.0
#
# WARNING! All changes made in this file will be lost!
@@ -50,9 +50,9 @@ class Ui_MainWindow(object):
self.scaleCheck = QtGui.QCheckBox(self.centralwidget)
self.scaleCheck.setObjectName("scaleCheck")
self.gridLayout_2.addWidget(self.scaleCheck, 3, 0, 1, 1)
- self.rgbCheck = QtGui.QCheckBox(self.centralwidget)
- self.rgbCheck.setObjectName("rgbCheck")
- self.gridLayout_2.addWidget(self.rgbCheck, 3, 1, 1, 1)
+ self.rgbLevelsCheck = QtGui.QCheckBox(self.centralwidget)
+ self.rgbLevelsCheck.setObjectName("rgbLevelsCheck")
+ self.gridLayout_2.addWidget(self.rgbLevelsCheck, 3, 1, 1, 1)
self.horizontalLayout = QtGui.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.minSpin1 = SpinBox(self.centralwidget)
@@ -119,6 +119,9 @@ class Ui_MainWindow(object):
self.fpsLabel.setAlignment(QtCore.Qt.AlignCenter)
self.fpsLabel.setObjectName("fpsLabel")
self.gridLayout_2.addWidget(self.fpsLabel, 0, 0, 1, 4)
+ self.rgbCheck = QtGui.QCheckBox(self.centralwidget)
+ self.rgbCheck.setObjectName("rgbCheck")
+ self.gridLayout_2.addWidget(self.rgbCheck, 2, 1, 1, 1)
MainWindow.setCentralWidget(self.centralwidget)
self.retranslateUi(MainWindow)
@@ -133,12 +136,13 @@ class Ui_MainWindow(object):
self.dtypeCombo.setItemText(1, QtGui.QApplication.translate("MainWindow", "uint16", None, QtGui.QApplication.UnicodeUTF8))
self.dtypeCombo.setItemText(2, QtGui.QApplication.translate("MainWindow", "float", None, QtGui.QApplication.UnicodeUTF8))
self.scaleCheck.setText(QtGui.QApplication.translate("MainWindow", "Scale Data", None, QtGui.QApplication.UnicodeUTF8))
- self.rgbCheck.setText(QtGui.QApplication.translate("MainWindow", "RGB", None, QtGui.QApplication.UnicodeUTF8))
+ self.rgbLevelsCheck.setText(QtGui.QApplication.translate("MainWindow", "RGB", None, QtGui.QApplication.UnicodeUTF8))
self.label_2.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8))
self.label_3.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8))
self.label_4.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8))
self.lutCheck.setText(QtGui.QApplication.translate("MainWindow", "Use Lookup Table", None, QtGui.QApplication.UnicodeUTF8))
self.alphaCheck.setText(QtGui.QApplication.translate("MainWindow", "alpha", None, QtGui.QApplication.UnicodeUTF8))
self.fpsLabel.setText(QtGui.QApplication.translate("MainWindow", "FPS", None, QtGui.QApplication.UnicodeUTF8))
+ self.rgbCheck.setText(QtGui.QApplication.translate("MainWindow", "RGB", None, QtGui.QApplication.UnicodeUTF8))
from pyqtgraph import SpinBox, GradientWidget, GraphicsView, RawImageWidget
diff --git a/functions.py b/functions.py
index 1342b527..4eafbbf3 100644
--- a/functions.py
+++ b/functions.py
@@ -434,8 +434,10 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False,
## make sure vectors are arrays
- vectors = np.array(vectors)
- origin = np.array(origin)
+ if not isinstance(vectors, np.ndarray):
+ vectors = np.array(vectors)
+ if not isinstance(origin, np.ndarray):
+ origin = np.array(origin)
origin.shape = (len(axes),) + (1,)*len(shape)
## Build array of sample locations.
@@ -580,171 +582,247 @@ def solveBilinearTransform(points1, points2):
return matrix
+def rescaleData(data, scale, offset, dtype=None):
+ """Return data rescaled and optionally cast to a new dtype::
+ data => (data-offset) * scale
+
+ Uses scipy.weave (if available) to improve performance.
+ """
+ global USE_WEAVE
+ if dtype is None:
+ dtype = data.dtype
+ try:
+ if not USE_WEAVE:
+ raise Exception('Weave is disabled; falling back to slower version.')
+
+ newData = np.empty((data.size,), dtype=dtype)
+ flat = np.ascontiguousarray(data).reshape(data.size)
+ size = data.size
+
+ code = """
+ double sc = (double)scale;
+ double off = (double)offset;
+ for( int i=0; i0 and max->*scale*::
- For 3D arrays (x, y, rgba):
- * The third axis must have length 3 or 4 and will be interpreted as RGBA.
- * The 'lut' argument is not allowed.
+ rescaled = (clip(data, min, max) - min) * (*scale* / (max - min))
- lut - Lookup table for 2D data. May be 1D or 2D (N,rgba) and must have dtype=ubyte.
- Values in data will be converted to color by indexing directly from lut.
- Lookup tables can be built using GradientWidget.
- levels - List [min, max]; optionally rescale data before converting through the
- lookup table. rescaled = (data-min) * len(lut) / (max-min)
- useRGBA - If True, the data is returned in RGBA order (useful for building OpenGL textures). The default is
- False, which returns in BGRA order for use with QImage.
-
+ It is also possible to use a 2D (N,2) array of values for levels. In this case,
+ it is assumed that each pair of min,max values in the levels array should be
+ applied to a different subset of the input data (for example, the input data may
+ already have RGB values and the levels are used to independently scale each
+ channel). The use of this feature requires that levels.shape[0] == data.shape[-1].
+ scale The maximum value to which data will be rescaled before being passed through the
+ lookup table (or returned if there is no lookup table). By default this will
+ be set to the length of the lookup table, or 256 is no lookup table is provided.
+ For OpenGL color specifications (as in GLColor4f) use scale=1.0
+ lut Optional lookup table (array with dtype=ubyte).
+ Values in data will be converted to color by indexing directly from lut.
+ The output data shape will be input.shape + lut.shape[1:].
+
+ Note: the output of makeARGB will have the same dtype as the lookup table, so
+ for conversion to QImage, the dtype must be ubyte.
+
+ Lookup tables can be built using GradientWidget.
+ useRGBA If True, the data is returned in RGBA order (useful for building OpenGL textures).
+ The default is False, which returns in ARGB order for use with QImage
+ (Note that 'ARGB' is a term used by the Qt documentation; the _actual_ order
+ is BGRA).
+ ============ ==================================================================================
"""
prof = debug.Profiler('functions.makeARGB', disabled=True)
+ if lut is not None and not isinstance(lut, np.ndarray):
+ lut = np.array(lut)
+ if levels is not None and not isinstance(levels, np.ndarray):
+ levels = np.array(levels)
+
## sanity checks
- if data.ndim == 3:
- if data.shape[2] not in (3,4):
- raise Exception("data.shape[2] must be 3 or 4")
- #if lut is not None:
- #raise Exception("can not use lookup table with 3D data")
- elif data.ndim != 2:
- raise Exception("data must be 2D or 3D")
+ #if data.ndim == 3:
+ #if data.shape[2] not in (3,4):
+ #raise Exception("data.shape[2] must be 3 or 4")
+ ##if lut is not None:
+ ##raise Exception("can not use lookup table with 3D data")
+ #elif data.ndim != 2:
+ #raise Exception("data must be 2D or 3D")
- if lut is not None:
- if lut.ndim == 2:
- if lut.shape[1] not in (3,4):
- raise Exception("lut.shape[1] must be 3 or 4")
- elif lut.ndim != 1:
- raise Exception("lut must be 1D or 2D")
- if lut.dtype != np.ubyte:
- raise Exception('lookup table must have dtype=ubyte (got %s instead)' % str(lut.dtype))
+ #if lut is not None:
+ ##if lut.ndim == 2:
+ ##if lut.shape[1] :
+ ##raise Exception("lut.shape[1] must be 3 or 4")
+ ##elif lut.ndim != 1:
+ ##raise Exception("lut must be 1D or 2D")
+ #if lut.dtype != np.ubyte:
+ #raise Exception('lookup table must have dtype=ubyte (got %s instead)' % str(lut.dtype))
+
if levels is not None:
- levels = np.array(levels)
- if levels.shape == (2,):
- pass
- elif levels.shape in [(3,2), (4,2)]:
- if data.ndim == 3:
- raise Exception("Can not use 2D levels with 3D data.")
- if lut is not None:
- raise Exception('Can not use 2D levels and lookup table together.')
+ if levels.ndim == 1:
+ if len(levels) != 2:
+ raise Exception('levels argument must have length 2')
+ elif levels.ndim == 2:
+ if lut is not None and lut.ndim > 1:
+ raise Exception('Cannot make ARGB data when bot levels and lut have ndim > 2')
+ if levels.shape != (data.shape[-1], 2):
+ raise Exception('levels must have shape (data.shape[-1], 2)')
else:
- raise Exception("Levels must have shape (2,) or (3,2) or (4,2)")
+ print levels
+ raise Exception("levels argument must be 1D or 2D.")
+ #levels = np.array(levels)
+ #if levels.shape == (2,):
+ #pass
+ #elif levels.shape in [(3,2), (4,2)]:
+ #if data.ndim == 3:
+ #raise Exception("Can not use 2D levels with 3D data.")
+ #if lut is not None:
+ #raise Exception('Can not use 2D levels and lookup table together.')
+ #else:
+ #raise Exception("Levels must have shape (2,) or (3,2) or (4,2)")
prof.mark('1')
- if lut is not None:
- lutLength = lut.shape[0]
- else:
- lutLength = 256
-
- ## weave requires contiguous arrays
- global USE_WEAVE
- if (levels is not None or lut is not None) and USE_WEAVE:
- data = np.ascontiguousarray(data)
+ if scale is None:
+ if lut is not None:
+ scale = lut.shape[0]
+ else:
+ scale = 255.
## Apply levels if given
if levels is not None:
- try: ## use weave to speed up scaling
- if not USE_WEAVE:
- raise Exception('Weave is disabled; falling back to slower version.')
- if levels.ndim == 1:
- scale = float(lutLength) / (levels[1]-levels[0])
- offset = float(levels[0])
- data = rescaleData(data, scale, offset)
- else:
- if data.ndim == 2:
- newData = np.empty(data.shape+(levels.shape[0],), dtype=np.uint32)
- for i in range(levels.shape[0]):
- scale = float(lutLength / (levels[i,1]-levels[i,0]))
- offset = float(levels[i,0])
- newData[...,i] = rescaleData(data, scale, offset)
- elif data.ndim == 3:
- newData = np.empty(data.shape, dtype=np.uint32)
- for i in range(data.shape[2]):
- scale = float(lutLength / (levels[i,1]-levels[i,0]))
- offset = float(levels[i,0])
- #print scale, offset, data.shape, newData.shape, levels.shape
- newData[...,i] = rescaleData(data[...,i], scale, offset)
- data = newData
- except:
- if USE_WEAVE:
- debug.printExc("Error; disabling weave.")
- USE_WEAVE = False
-
- if levels.ndim == 1:
- if data.ndim == 2:
- levels = levels[np.newaxis, np.newaxis, :]
- else:
- levels = levels[np.newaxis, np.newaxis, np.newaxis, :]
- else:
- levels = levels[np.newaxis, np.newaxis, ...]
- if data.ndim == 2:
- data = data[..., np.newaxis]
- data = ((data-levels[...,0]) * lutLength) / (levels[...,1]-levels[...,0])
+ if isinstance(levels, np.ndarray) and levels.ndim == 2:
+ ## we are going to rescale each channel independently
+ if levels.shape[0] != data.shape[-1]:
+ raise Exception("When rescaling multi-channel data, there must be the same number of levels as channels (data.shape[-1] == levels.shape[0])")
+ newData = np.empty(data.shape, dtype=int)
+ for i in range(data.shape[-1]):
+ minVal, maxVal = levels[i]
+ if minVal == maxVal:
+ maxVal += 1e-16
+ newData[...,i] = rescaleData(data[...,i], scale/(maxVal-minVal), minVal, dtype=int)
+ data = newData
+ else:
+ minVal, maxVal = levels
+ if minVal == maxVal:
+ maxVal += 1e-16
+ data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=int)
prof.mark('2')
## apply LUT if given
- if lut is not None and data.ndim == 2:
-
- if data.dtype.kind not in ('i', 'u'):
- data = data.astype(int)
-
- data = np.clip(data, 0, lutLength-1)
- try:
- if not USE_WEAVE:
- raise Exception('Weave is disabled; falling back to slower version.')
-
- newData = np.empty((data.size,) + lut.shape[1:], dtype=np.uint8)
- flat = data.reshape(data.size)
- size = data.size
- ncol = lut.shape[1]
- newStride = newData.strides[0]
- newColStride = newData.strides[1]
- lutStride = lut.strides[0]
- lutColStride = lut.strides[1]
- flatStride = flat.strides[0] / flat.dtype.itemsize
-
- #print "newData:", newData.shape, newData.dtype
- #print "flat:", flat.shape, flat.dtype, flat.min(), flat.max()
- #print "lut:", lut.shape, lut.dtype
- #print "size:", size, "ncols:", ncol
- #print "strides:", newStride, newColStride, lutStride, lutColStride, flatStride
-
- code = """
-
- for( int i=0; i
Date: Fri, 23 Nov 2012 16:05:14 -0500
Subject: [PATCH 2/3] New features for LegendItem: - Can be anchored to
parent item at any location - Support for filled plot styles -
Automatically resizes to fit contents - PlotItem can auto-generate legend
---
examples/Legend.py | 13 ++---
graphicsItems/GraphicsWidgetAnchor.py | 63 ++++++++++++++++++++++++
graphicsItems/LegendItem.py | 69 +++++++++++++++++++++++----
graphicsItems/PlotCurveItem.py | 4 ++
graphicsItems/PlotDataItem.py | 2 +
graphicsItems/PlotItem/PlotItem.py | 14 ++++++
6 files changed, 151 insertions(+), 14 deletions(-)
create mode 100644 graphicsItems/GraphicsWidgetAnchor.py
diff --git a/examples/Legend.py b/examples/Legend.py
index f615a6eb..af6fce8c 100644
--- a/examples/Legend.py
+++ b/examples/Legend.py
@@ -6,13 +6,14 @@ from pyqtgraph.Qt import QtCore, QtGui
plt = pg.plot()
-l = pg.LegendItem((100,60), (60,10)) # args are (size, position)
-l.setParentItem(plt.graphicsItem()) # Note we do NOT call plt.addItem in this case
+plt.addLegend()
+#l = pg.LegendItem((100,60), offset=(70,30)) # args are (size, offset)
+#l.setParentItem(plt.graphicsItem()) # Note we do NOT call plt.addItem in this case
-c1 = plt.plot([1,3,2,4], pen='r')
-c2 = plt.plot([2,1,4,3], pen='g')
-l.addItem(c1, 'red plot')
-l.addItem(c2, 'green plot')
+c1 = plt.plot([1,3,2,4], pen='r', name='red plot')
+c2 = plt.plot([2,1,4,3], pen='g', fillLevel=0, fillBrush=(255,255,255,30), name='green plot')
+#l.addItem(c1, 'red plot')
+#l.addItem(c2, 'green plot')
## Start Qt event loop unless running in interactive mode or using pyside.
diff --git a/graphicsItems/GraphicsWidgetAnchor.py b/graphicsItems/GraphicsWidgetAnchor.py
new file mode 100644
index 00000000..b71e781a
--- /dev/null
+++ b/graphicsItems/GraphicsWidgetAnchor.py
@@ -0,0 +1,63 @@
+from ..Qt import QtGui, QtCore
+from ..Point import Point
+
+
+class GraphicsWidgetAnchor:
+ """
+ Class used to allow GraphicsWidgets to anchor to a specific position on their
+ parent.
+
+ """
+
+ def __init__(self):
+ self.__parent = None
+ self.__parentAnchor = None
+ self.__itemAnchor = None
+ self.__offset = (0,0)
+ if hasattr(self, 'geometryChanged'):
+ self.geometryChanged.connect(self.__geometryChanged)
+
+ def anchor(self, itemPos, parentPos, offset=(0,0)):
+ """
+ Anchors the item at its local itemPos to the item's parent at parentPos.
+ Both positions are expressed in values relative to the size of the item or parent;
+ a value of 0 indicates left or top edge, while 1 indicates right or bottom edge.
+
+ Optionally, offset may be specified to introduce an absolute offset.
+
+ Example: anchor a box such that its upper-right corner is fixed 10px left
+ and 10px down from its parent's upper-right corner::
+
+ box.anchor(itemPos=(1,0), parentPos=(1,0), offset=(-10,10))
+ """
+ parent = self.parentItem()
+ if parent is None:
+ raise Exception("Cannot anchor; parent is not set.")
+
+ if self.__parent is not parent:
+ if self.__parent is not None:
+ self.__parent.geometryChanged.disconnect(self.__geometryChanged)
+
+ self.__parent = parent
+ parent.geometryChanged.connect(self.__geometryChanged)
+
+ self.__itemAnchor = itemPos
+ self.__parentAnchor = parentPos
+ self.__offset = offset
+ self.__geometryChanged()
+
+ def __geometryChanged(self):
+ if self.__parent is None:
+ return
+ if self.__itemAnchor is None:
+ return
+
+ o = self.mapToParent(Point(0,0))
+ a = self.boundingRect().bottomRight() * Point(self.__itemAnchor)
+ a = self.mapToParent(a)
+ p = self.__parent.boundingRect().bottomRight() * Point(self.__parentAnchor)
+ off = Point(self.__offset)
+ pos = p + (o-a) + off
+ self.setPos(pos)
+
+
\ No newline at end of file
diff --git a/graphicsItems/LegendItem.py b/graphicsItems/LegendItem.py
index a41201e1..14c690a5 100644
--- a/graphicsItems/LegendItem.py
+++ b/graphicsItems/LegendItem.py
@@ -2,12 +2,14 @@ from .GraphicsWidget import GraphicsWidget
from .LabelItem import LabelItem
from ..Qt import QtGui, QtCore
from .. import functions as fn
-
+from ..Point import Point
+from .GraphicsWidgetAnchor import GraphicsWidgetAnchor
__all__ = ['LegendItem']
-class LegendItem(GraphicsWidget):
+class LegendItem(GraphicsWidget, GraphicsWidgetAnchor):
"""
Displays a legend used for describing the contents of a plot.
+ LegendItems are most commonly created by calling PlotItem.addLegend().
Note that this item should not be added directly to a PlotItem. Instead,
Make it a direct descendant of the PlotItem::
@@ -15,17 +17,45 @@ class LegendItem(GraphicsWidget):
legend.setParentItem(plotItem)
"""
- def __init__(self, size, offset):
+ def __init__(self, size=None, offset=None):
+ """
+ ========== ===============================================================
+ Arguments
+ size Specifies the fixed size (width, height) of the legend. If
+ this argument is omitted, the legend will autimatically resize
+ to fit its contents.
+ offset Specifies the offset position relative to the legend's parent.
+ Positive values offset from the left or top; negative values
+ offset from the right or bottom. If offset is None, the
+ legend must be anchored manually by calling anchor() or
+ positioned by calling setPos().
+ ========== ===============================================================
+
+ """
+
+
GraphicsWidget.__init__(self)
+ GraphicsWidgetAnchor.__init__(self)
self.setFlag(self.ItemIgnoresTransformations)
self.layout = QtGui.QGraphicsGridLayout()
self.setLayout(self.layout)
self.items = []
self.size = size
self.offset = offset
- self.setGeometry(QtCore.QRectF(self.offset[0], self.offset[1], self.size[0], self.size[1]))
+ if size is not None:
+ self.setGeometry(QtCore.QRectF(0, 0, self.size[0], self.size[1]))
- def addItem(self, item, title):
+ def setParentItem(self, p):
+ ret = GraphicsWidget.setParentItem(self, p)
+ if self.offset is not None:
+ offset = Point(self.offset)
+ anchorx = 1 if offset[0] <= 0 else 0
+ anchory = 1 if offset[1] <= 0 else 0
+ anchor = (anchorx, anchory)
+ self.anchor(itemPos=anchor, parentPos=anchor, offset=offset)
+ return ret
+
+ def addItem(self, item, name):
"""
Add a new entry to the legend.
@@ -36,15 +66,30 @@ class LegendItem(GraphicsWidget):
title The title to display for this item. Simple HTML allowed.
=========== ========================================================
"""
- label = LabelItem(title)
+ label = LabelItem(name)
sample = ItemSample(item)
row = len(self.items)
self.items.append((sample, label))
self.layout.addItem(sample, row, 0)
self.layout.addItem(label, row, 1)
+ self.updateSize()
+
+ def updateSize(self):
+ if self.size is not None:
+ return
+
+ height = 0
+ width = 0
+ print "-------"
+ for sample, label in self.items:
+ height += max(sample.height(), label.height()) + 3
+ width = max(width, sample.width()+label.width())
+ print width, height
+ print width, height
+ self.setGeometry(0, 0, width+25, height)
def boundingRect(self):
- return QtCore.QRectF(0, 0, self.size[0], self.size[1])
+ return QtCore.QRectF(0, 0, self.width(), self.height())
def paint(self, p, *args):
p.setPen(fn.mkPen(255,255,255,100))
@@ -61,8 +106,16 @@ class ItemSample(GraphicsWidget):
return QtCore.QRectF(0, 0, 20, 20)
def paint(self, p, *args):
- p.setPen(fn.mkPen(self.item.opts['pen']))
+ opts = self.item.opts
+
+ if opts.get('fillLevel',None) is not None and opts.get('fillBrush',None) is not None:
+ p.setBrush(fn.mkBrush(opts['fillBrush']))
+ p.setPen(fn.mkPen(None))
+ p.drawPolygon(QtGui.QPolygonF([QtCore.QPointF(2,18), QtCore.QPointF(18,2), QtCore.QPointF(18,18)]))
+
+ p.setPen(fn.mkPen(opts['pen']))
p.drawLine(2, 18, 18, 2)
+
diff --git a/graphicsItems/PlotCurveItem.py b/graphicsItems/PlotCurveItem.py
index 17ee4566..d267f58e 100644
--- a/graphicsItems/PlotCurveItem.py
+++ b/graphicsItems/PlotCurveItem.py
@@ -65,6 +65,7 @@ class PlotCurveItem(GraphicsObject):
'fillLevel': None,
'brush': None,
'stepMode': False,
+ 'name': None
}
self.setClickable(kargs.get('clickable', False))
self.setData(*args, **kargs)
@@ -238,6 +239,9 @@ class PlotCurveItem(GraphicsObject):
self.fillPath = None
#self.xDisp = self.yDisp = None
+ if 'name' in kargs:
+ self.opts['name'] = kargs['name']
+
if 'pen' in kargs:
self.setPen(kargs['pen'])
if 'shadowPen' in kargs:
diff --git a/graphicsItems/PlotDataItem.py b/graphicsItems/PlotDataItem.py
index ce5b11aa..58158905 100644
--- a/graphicsItems/PlotDataItem.py
+++ b/graphicsItems/PlotDataItem.py
@@ -317,6 +317,8 @@ class PlotDataItem(GraphicsObject):
## pull in all style arguments.
## Use self.opts to fill in anything not present in kargs.
+ if 'name' in kargs:
+ self.opts['name'] = kargs['name']
## if symbol pen/brush are given with no symbol, then assume symbol is 'o'
diff --git a/graphicsItems/PlotItem/PlotItem.py b/graphicsItems/PlotItem/PlotItem.py
index 3177a176..8281f031 100644
--- a/graphicsItems/PlotItem/PlotItem.py
+++ b/graphicsItems/PlotItem/PlotItem.py
@@ -33,6 +33,7 @@ from .. PlotDataItem import PlotDataItem
from .. ViewBox import ViewBox
from .. AxisItem import AxisItem
from .. LabelItem import LabelItem
+from .. LegendItem import LegendItem
from .. GraphicsWidget import GraphicsWidget
from .. ButtonItem import ButtonItem
from pyqtgraph.WidgetGroup import WidgetGroup
@@ -528,6 +529,9 @@ class PlotItem(GraphicsWidget):
#c.connect(c, QtCore.SIGNAL('plotChanged'), self.plotChanged)
#item.sigPlotChanged.connect(self.plotChanged)
#self.plotChanged()
+ name = kargs.get('name', getattr(item, 'opts', {}).get('name', None))
+ if name is not None and self.legend is not None:
+ self.legend.addItem(item, name=name)
def addDataItem(self, item, *args):
@@ -596,6 +600,16 @@ class PlotItem(GraphicsWidget):
return item
+ def addLegend(self, size=None, offset=(30, 30)):
+ """
+ Create a new LegendItem and anchor it over the internal ViewBox.
+ Plots will be automatically displayed in the legend if they
+ are created with the 'name' argument.
+ """
+ self.legend = LegendItem(size, offset)
+ self.legend.setParentItem(self.vb)
+ return self.legend
+
def scatterPlot(self, *args, **kargs):
if 'pen' in kargs:
kargs['symbolPen'] = kargs['pen']
From aca9c8310fa3a64c6836772f7691c500081a43e2 Mon Sep 17 00:00:00 2001
From: Luke Campagnola <>
Date: Fri, 23 Nov 2012 17:34:22 -0500
Subject: [PATCH 3/3] 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
---
examples/GLIsosurface.py | 72 ++++
examples/GLMeshItem.py | 134 ++++++--
examples/GLSurfacePlot.py | 98 ++++++
examples/GLshaders.py | 108 ++++++
examples/__main__.py | 10 +-
opengl/GLGraphicsItem.py | 91 ++++-
opengl/GLViewWidget.py | 20 +-
opengl/MeshData.py | 530 +++++++++++++++++++++++-------
opengl/items/GLGridItem.py | 11 +-
opengl/items/GLMeshItem.py | 202 +++++++++---
opengl/items/GLScatterPlotItem.py | 30 +-
opengl/items/GLSurfacePlotItem.py | 139 ++++++++
opengl/items/GLVolumeItem.py | 12 +-
opengl/shaders.py | 323 ++++++++++++++++--
14 files changed, 1532 insertions(+), 248 deletions(-)
create mode 100644 examples/GLIsosurface.py
create mode 100644 examples/GLSurfacePlot.py
create mode 100644 examples/GLshaders.py
create mode 100644 opengl/items/GLSurfacePlotItem.py
diff --git a/examples/GLIsosurface.py b/examples/GLIsosurface.py
new file mode 100644
index 00000000..c4f06adb
--- /dev/null
+++ b/examples/GLIsosurface.py
@@ -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_()
diff --git a/examples/GLMeshItem.py b/examples/GLMeshItem.py
index 15d6bc33..049ad53d 100644
--- a/examples/GLMeshItem.py
+++ b/examples/GLMeshItem.py
@@ -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:
diff --git a/examples/GLSurfacePlot.py b/examples/GLSurfacePlot.py
new file mode 100644
index 00000000..d930fff7
--- /dev/null
+++ b/examples/GLSurfacePlot.py
@@ -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_()
diff --git a/examples/GLshaders.py b/examples/GLshaders.py
new file mode 100644
index 00000000..616b8b4c
--- /dev/null
+++ b/examples/GLshaders.py
@@ -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_()
diff --git a/examples/__main__.py b/examples/__main__.py
index 3035ddcf..9bd96d8a 100644
--- a/examples/__main__.py
+++ b/examples/__main__.py
@@ -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)
diff --git a/opengl/GLGraphicsItem.py b/opengl/GLGraphicsItem.py
index 7d1cf70b..1ed757b0 100644
--- a/opengl/GLGraphicsItem.py
+++ b/opengl/GLGraphicsItem.py
@@ -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()
diff --git a/opengl/GLViewWidget.py b/opengl/GLViewWidget.py
index 6911d849..37d42072 100644
--- a/opengl/GLViewWidget.py
+++ b/opengl/GLViewWidget.py
@@ -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"""
diff --git a/opengl/MeshData.py b/opengl/MeshData.py
index f5024f90..ad77f3d6 100644
--- a/opengl/MeshData.py
+++ b/opengl/MeshData.py
@@ -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])
-
-
-
\ No newline at end of file
+
+
+
+
+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[facesvmax] = vmax
+
+ return MeshData(vertexes=verts, faces=faces)
+
+
\ No newline at end of file
diff --git a/opengl/items/GLGridItem.py b/opengl/items/GLGridItem.py
index 35a6f766..630b2aba 100644
--- a/opengl/items/GLGridItem.py
+++ b/opengl/items/GLGridItem.py
@@ -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 )
diff --git a/opengl/items/GLMeshItem.py b/opengl/items/GLMeshItem.py
index 266b84c0..c29f29e0 100644
--- a/opengl/items/GLMeshItem.py
+++ b/opengl/items/GLMeshItem.py
@@ -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 ` 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)
+
diff --git a/opengl/items/GLScatterPlotItem.py b/opengl/items/GLScatterPlotItem.py
index 9ff22c37..d04a2ac7 100644
--- a/opengl/items/GLScatterPlotItem.py
+++ b/opengl/items/GLScatterPlotItem.py
@@ -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:
diff --git a/opengl/items/GLSurfacePlotItem.py b/opengl/items/GLSurfacePlotItem.py
new file mode 100644
index 00000000..69080fad
--- /dev/null
+++ b/opengl/items/GLSurfacePlotItem.py
@@ -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 `
+
+ 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
\ No newline at end of file
diff --git a/opengl/items/GLVolumeItem.py b/opengl/items/GLVolumeItem.py
index 9981f4ba..4980239d 100644
--- a/opengl/items/GLVolumeItem.py
+++ b/opengl/items/GLVolumeItem.py
@@ -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()
diff --git a/opengl/shaders.py b/opengl/shaders.py
index 7f4fa665..e9a4af9d 100644
--- a/opengl/shaders.py
+++ b/opengl/shaders.py
@@ -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()
\ No newline at end of file