summaryrefslogtreecommitdiff
path: root/silx/gui/plot3d/scene
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot3d/scene')
-rw-r--r--silx/gui/plot3d/scene/__init__.py2
-rw-r--r--silx/gui/plot3d/scene/axes.py19
-rw-r--r--silx/gui/plot3d/scene/camera.py7
-rw-r--r--silx/gui/plot3d/scene/cutplane.py46
-rw-r--r--silx/gui/plot3d/scene/function.py36
-rw-r--r--silx/gui/plot3d/scene/interaction.py58
-rw-r--r--silx/gui/plot3d/scene/primitives.py1241
-rw-r--r--silx/gui/plot3d/scene/test/test_utils.py4
-rw-r--r--silx/gui/plot3d/scene/transform.py24
-rw-r--r--silx/gui/plot3d/scene/utils.py42
-rw-r--r--silx/gui/plot3d/scene/viewport.py59
-rw-r--r--silx/gui/plot3d/scene/window.py27
12 files changed, 1230 insertions, 335 deletions
diff --git a/silx/gui/plot3d/scene/__init__.py b/silx/gui/plot3d/scene/__init__.py
index 25a7171..9671725 100644
--- a/silx/gui/plot3d/scene/__init__.py
+++ b/silx/gui/plot3d/scene/__init__.py
@@ -29,6 +29,6 @@ __license__ = "MIT"
__date__ = "08/11/2016"
-from .core import Elem, Group, PrivateGroup # noqa
+from .core import Base, Elem, Group, PrivateGroup # noqa
from .viewport import Viewport # noqa
from .window import Window # noqa
diff --git a/silx/gui/plot3d/scene/axes.py b/silx/gui/plot3d/scene/axes.py
index 520ef3e..e35e5e1 100644
--- a/silx/gui/plot3d/scene/axes.py
+++ b/silx/gui/plot3d/scene/axes.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -87,6 +87,23 @@ class LabelledAxes(primitives.GroupBBox):
# Sync color
self.tickColor = 1., 1., 1., 1.
+ def _updateBoxAndAxes(self):
+ """Update bbox and axes position and size according to children.
+
+ Overridden from GroupBBox
+ """
+ super(LabelledAxes, self)._updateBoxAndAxes()
+
+ bounds = self._group.bounds(dataBounds=True)
+ if bounds is not None:
+ tx, ty, tz = (bounds[1] - bounds[0]) / 2.
+ else:
+ tx, ty, tz = 0.5, 0.5, 0.5
+
+ self._xlabel.transforms[-1].tx = tx
+ self._ylabel.transforms[-1].ty = ty
+ self._zlabel.transforms[-1].tz = tz
+
@property
def tickColor(self):
"""Color of ticks and text labels.
diff --git a/silx/gui/plot3d/scene/camera.py b/silx/gui/plot3d/scene/camera.py
index 8cc279d..acc5899 100644
--- a/silx/gui/plot3d/scene/camera.py
+++ b/silx/gui/plot3d/scene/camera.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2015-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2015-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -252,8 +252,9 @@ class Camera(transform.Transform):
:param float fovy: Vertical field-of-view in degrees.
:param float near: The near clipping plane Z coord (strictly positive).
:param float far: The far clipping plane Z coord (> near).
- :param size: Viewport's size used to compute the aspect ratio.
- :type size: 2-tuple of float (width, height).
+ :param size:
+ Viewport's size used to compute the aspect ratio (width, height).
+ :type size: 2-tuple of float
:param position: Coordinates of the point of view.
:type position: numpy.ndarray-like of 3 float32.
:param direction: Sight direction vector.
diff --git a/silx/gui/plot3d/scene/cutplane.py b/silx/gui/plot3d/scene/cutplane.py
index 79b4168..08a9899 100644
--- a/silx/gui/plot3d/scene/cutplane.py
+++ b/silx/gui/plot3d/scene/cutplane.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -29,7 +29,7 @@ from __future__ import absolute_import, division, unicode_literals
__authors__ = ["T. Vincent"]
__license__ = "MIT"
-__date__ = "05/10/2016"
+__date__ = "11/01/2018"
import string
import numpy
@@ -53,6 +53,7 @@ class ColormapMesh3D(Geometry):
uniform mat4 transformMat;
//uniform mat3 matrixInvTranspose;
uniform vec3 dataScale;
+ uniform vec3 texCoordsOffset;
varying vec4 vCameraPosition;
varying vec3 vPosition;
@@ -64,7 +65,7 @@ class ColormapMesh3D(Geometry):
vCameraPosition = transformMat * vec4(position, 1.0);
//vNormal = matrixInvTranspose * normalize(normal);
vPosition = position;
- vTexCoords = dataScale * position;
+ vTexCoords = dataScale * position + texCoordsOffset;
vNormal = normal;
gl_Position = matrix * vec4(position, 1.0);
}
@@ -113,6 +114,8 @@ class ColormapMesh3D(Geometry):
normal=normal)
self.isBackfaceVisible = True
+ self.textureOffset = 0., 0., 0.
+ """Offset to add to texture coordinates"""
def setData(self, data, copy=True):
data = numpy.array(data, copy=copy, order='C')
@@ -209,6 +212,7 @@ class ColormapMesh3D(Geometry):
shape = self._data.shape
scales = 1./shape[2], 1./shape[1], 1./shape[0]
gl.glUniform3f(program.uniforms['dataScale'], *scales)
+ gl.glUniform3f(program.uniforms['texCoordsOffset'], *self.textureOffset)
gl.glUniform1i(program.uniforms['data'], self._texture.texUnit)
@@ -275,21 +279,13 @@ class CutPlane(PlaneInGroup):
self._interpolation = interpolation
if self._mesh is not None:
self._mesh.interpolation = interpolation
+ self.notify()
def prepareGL2(self, ctx):
if self.isValid:
contourVertices = self.contourVertices
- if (self.interpolation == 'nearest' and
- contourVertices is not None and len(contourVertices)):
- # Avoid cut plane co-linear with array bin edges
- for index, normal in enumerate(((1., 0., 0.), (0., 1., 0.), (0., 0., 1.))):
- if (numpy.all(numpy.equal(self.plane.normal, normal)) and
- int(self.plane.point[index]) == self.plane.point[index]):
- contourVertices += self.plane.normal * 0.01 # Add an offset
- break
-
if self._mesh is None and self._data is not None:
self._mesh = ColormapMesh3D(contourVertices,
normal=self.plane.normal,
@@ -298,7 +294,7 @@ class CutPlane(PlaneInGroup):
mode='fan',
colormap=self.colormap)
self._mesh.alpha = self._alpha
- self._interpolation = self.interpolation
+ self._mesh.interpolation = self.interpolation
self._children.insert(0, self._mesh)
if self._mesh is not None:
@@ -310,6 +306,23 @@ class CutPlane(PlaneInGroup):
self._mesh.setAttribute('normal', self.plane.normal)
self._mesh.setAttribute('position', contourVertices)
+ needTextureOffset = False
+ if self.interpolation == 'nearest':
+ # If cut plane is co-linear with array bin edges add texture offset
+ planePt = self.plane.point
+ for index, normal in enumerate(((1., 0., 0.),
+ (0., 1., 0.),
+ (0., 0., 1.))):
+ if (numpy.all(numpy.equal(self.plane.normal, normal)) and
+ int(planePt[index]) == planePt[index]):
+ needTextureOffset = True
+ break
+
+ if needTextureOffset:
+ self._mesh.textureOffset = self.plane.normal * 1e-6
+ else:
+ self._mesh.textureOffset = 0., 0., 0.
+
super(CutPlane, self).prepareGL2(ctx)
def renderGL2(self, ctx):
@@ -348,10 +361,11 @@ class CutPlane(PlaneInGroup):
return cachevertices
# Cache is not OK, rebuild it
- boxvertices = bounds[0] + Box._vertices.copy()*(bounds[1] - bounds[0])
- lineindices = Box._lineIndices
+ boxVertices = Box.getVertices(copy=True)
+ boxVertices = bounds[0] + boxVertices * (bounds[1] - bounds[0])
+ lineIndices = Box.getLineIndices(copy=False)
vertices = utils.boxPlaneIntersect(
- boxvertices, lineindices, self.plane.normal, self.plane.point)
+ boxVertices, lineIndices, self.plane.normal, self.plane.point)
self._cache = bounds, vertices if len(vertices) != 0 else None
diff --git a/silx/gui/plot3d/scene/function.py b/silx/gui/plot3d/scene/function.py
index 73cdb72..ba4c4ca 100644
--- a/silx/gui/plot3d/scene/function.py
+++ b/silx/gui/plot3d/scene/function.py
@@ -33,6 +33,7 @@ __date__ = "08/11/2016"
import contextlib
import logging
+import string
import numpy
from ... import _glutils
@@ -296,9 +297,8 @@ class DirectionalLight(event.Notifier, ProgramFunction):
class Colormap(event.Notifier, ProgramFunction):
- # TODO use colors for out-of-bound values, for <=0 with log, for nan
- decl = """
+ _declTemplate = string.Template("""
uniform struct {
sampler2D texture;
bool isLog;
@@ -321,9 +321,17 @@ class Colormap(event.Notifier, ProgramFunction):
value = clamp(cmap.oneOverRange * (value - cmap.min), 0.0, 1.0);
}
+ $discard
+
vec4 color = texture2D(cmap.texture, vec2(value, 0.5));
return color;
}
+ """)
+
+ _discardCode = """
+ if (value == 0.) {
+ discard;
+ }
"""
call = "colormap"
@@ -346,7 +354,10 @@ class Colormap(event.Notifier, ProgramFunction):
super(Colormap, self).__init__()
# Init privates to default
- self._colormap, self._norm, self._range = None, 'linear', (1., 10.)
+ self._colormap = None
+ self._norm = 'linear'
+ self._range = 1., 10.
+ self._displayValuesBelowMin = True
self._texture = None
self._update_texture = True
@@ -363,6 +374,12 @@ class Colormap(event.Notifier, ProgramFunction):
self.range_ = range_
@property
+ def decl(self):
+ """Source code of the function declaration"""
+ return self._declTemplate.substitute(
+ discard="" if self.displayValuesBelowMin else self._discardCode)
+
+ @property
def colormap(self):
"""Color look-up table to use."""
return numpy.array(self._colormap, copy=True)
@@ -420,6 +437,19 @@ class Colormap(event.Notifier, ProgramFunction):
self._range = range_
self.notify()
+ @property
+ def displayValuesBelowMin(self):
+ """True to display values below colormap min, False to discard them.
+ """
+ return self._displayValuesBelowMin
+
+ @displayValuesBelowMin.setter
+ def displayValuesBelowMin(self, displayValuesBelowMin):
+ displayValuesBelowMin = bool(displayValuesBelowMin)
+ if self._displayValuesBelowMin != displayValuesBelowMin:
+ self._displayValuesBelowMin = displayValuesBelowMin
+ self.notify()
+
def setupProgram(self, context, program):
"""Sets-up uniforms of a program using this shader function.
diff --git a/silx/gui/plot3d/scene/interaction.py b/silx/gui/plot3d/scene/interaction.py
index 2911b2c..e5cfb6d 100644
--- a/silx/gui/plot3d/scene/interaction.py
+++ b/silx/gui/plot3d/scene/interaction.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2015-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2015-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -33,6 +33,7 @@ __date__ = "25/07/2016"
import logging
import numpy
+from silx.gui import qt
from silx.gui.plot.Interaction import \
StateMachine, State, LEFT_BTN, RIGHT_BTN # , MIDDLE_BTN
@@ -380,16 +381,16 @@ class FocusManager(StateMachine):
"""
class Idle(State):
def onPress(self, x, y, btn):
- for eventHandler in self.machine.eventHandlers:
- requestfocus = eventHandler.handleEvent('press', x, y, btn)
- if requestfocus:
+ for eventHandler in self.machine.currentEventHandler:
+ requestFocus = eventHandler.handleEvent('press', x, y, btn)
+ if requestFocus:
self.goto('focus', eventHandler, btn)
break
def _processEvent(self, *args):
- for eventHandler in self.machine.eventHandlers:
- consumeevent = eventHandler.handleEvent(*args)
- if consumeevent:
+ for eventHandler in self.machine.currentEventHandler:
+ consumeEvent = eventHandler.handleEvent(*args)
+ if consumeEvent:
break
def onMove(self, x, y):
@@ -424,8 +425,10 @@ class FocusManager(StateMachine):
def onWheel(self, x, y, angleInDegrees):
self.eventHandler.handleEvent('wheel', x, y, angleInDegrees)
- def __init__(self, eventHandlers=()):
- self.eventHandlers = list(eventHandlers)
+ def __init__(self, eventHandlers=(), ctrlEventHandlers=None):
+ self.defaultEventHandlers = eventHandlers
+ self.ctrlEventHandlers = ctrlEventHandlers
+ self.currentEventHandler = self.defaultEventHandlers
states = {
'idle': FocusManager.Idle,
@@ -433,31 +436,48 @@ class FocusManager(StateMachine):
}
super(FocusManager, self).__init__(states, 'idle')
+ def onKeyPress(self, key):
+ if key == qt.Qt.Key_Control and self.ctrlEventHandlers is not None:
+ self.currentEventHandler = self.ctrlEventHandlers
+
+ def onKeyRelease(self, key):
+ if key == qt.Qt.Key_Control:
+ self.currentEventHandler = self.defaultEventHandlers
+
def cancel(self):
- for handler in self.eventHandlers:
+ for handler in self.currentEventHandler:
handler.cancel()
# CameraControl ###############################################################
class RotateCameraControl(FocusManager):
- """Combine wheel and rotate state machine."""
+ """Combine wheel and rotate state machine for left button
+ and pan when ctrl is pressed
+ """
def __init__(self, viewport,
orbitAroundCenter=False,
- mode='center', scaleTransform=None):
+ mode='center', scaleTransform=None,
+ selectCB=None):
handlers = (CameraWheel(viewport, mode, scaleTransform),
CameraRotate(viewport, orbitAroundCenter, LEFT_BTN))
- super(RotateCameraControl, self).__init__(handlers)
+ ctrlHandlers = (CameraWheel(viewport, mode, scaleTransform),
+ CameraSelectPan(viewport, LEFT_BTN, selectCB))
+ super(RotateCameraControl, self).__init__(handlers, ctrlHandlers)
class PanCameraControl(FocusManager):
- """Combine wheel, selectPan and rotate state machine."""
+ """Combine wheel, selectPan and rotate state machine for left button
+ and rotate when ctrl is pressed"""
def __init__(self, viewport,
+ orbitAroundCenter=False,
mode='center', scaleTransform=None,
selectCB=None):
handlers = (CameraWheel(viewport, mode, scaleTransform),
CameraSelectPan(viewport, LEFT_BTN, selectCB))
- super(PanCameraControl, self).__init__(handlers)
+ ctrlHandlers = (CameraWheel(viewport, mode, scaleTransform),
+ CameraRotate(viewport, orbitAroundCenter, LEFT_BTN))
+ super(PanCameraControl, self).__init__(handlers, ctrlHandlers)
class CameraControl(FocusManager):
@@ -675,7 +695,11 @@ class PanPlaneRotateCameraControl(FocusManager):
class PanPlaneZoomOnWheelControl(FocusManager):
"""Combine zoom on wheel and pan plane state machines."""
def __init__(self, viewport, plane,
- mode='center', scaleTransform=None):
+ mode='center',
+ orbitAroundCenter=False,
+ scaleTransform=None):
handlers = (CameraWheel(viewport, mode, scaleTransform),
PlanePan(viewport, plane, LEFT_BTN))
- super(PanPlaneZoomOnWheelControl, self).__init__(handlers)
+ ctrlHandlers = (CameraWheel(viewport, mode, scaleTransform),
+ CameraRotate(viewport, orbitAroundCenter, LEFT_BTN))
+ super(PanPlaneZoomOnWheelControl, self).__init__(handlers, ctrlHandlers)
diff --git a/silx/gui/plot3d/scene/primitives.py b/silx/gui/plot3d/scene/primitives.py
index fc38e09..abf7dd4 100644
--- a/silx/gui/plot3d/scene/primitives.py
+++ b/silx/gui/plot3d/scene/primitives.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2015-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2015-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -47,6 +47,7 @@ from . import event
from . import core
from . import transform
from . import utils
+from .function import Colormap
_logger = logging.getLogger(__name__)
@@ -60,6 +61,7 @@ class Geometry(core.Elem):
lines, line_strip, loop, triangles, triangle_strip, fan
:param indices: Array of vertex indices or None
:param bool copy: True (default) to copy the data, False to use as is.
+ :param str attrib0: Name of the attribute that MUST be an array.
:param attributes: Provide list of attributes as extra parameters.
"""
@@ -91,12 +93,21 @@ class Geometry(core.Elem):
_TRIANGLE_MODES = 'triangles', 'triangle_strip', 'fan'
- def __init__(self, mode, indices=None, copy=True, **attributes):
+ def __init__(self,
+ mode,
+ indices=None,
+ copy=True,
+ attrib0='position',
+ **attributes):
super(Geometry, self).__init__()
+ self._attrib0 = str(attrib0)
+
self._vbos = {} # Store current vbos
self._unsyncAttributes = [] # Store attributes to copy to vbos
self.__bounds = None # Cache object's bounds
+ # Attribute names defining the object bounds
+ self.__boundsAttributeNames = (self._attrib0,)
assert mode in self._MODES
self._mode = mode
@@ -116,9 +127,16 @@ class Geometry(core.Elem):
nbvertices = len(self._indices)
else:
nbvertices = self.nbVertices
- assert nbvertices >= mincheck
- if modulocheck != 0:
- assert (nbvertices % modulocheck) == 0
+
+ if nbvertices != 0:
+ assert nbvertices >= mincheck
+ if modulocheck != 0:
+ assert (nbvertices % modulocheck) == 0
+
+ @property
+ def drawMode(self):
+ """Kind of primitive to render, in :attr:`_MODES` (str)"""
+ return self._mode
@staticmethod
def _glReadyArray(array, copy=True):
@@ -134,10 +152,19 @@ class Geometry(core.Elem):
# Makes sure it is an array
array = numpy.array(array, copy=False)
- # Cast all float to float32
dtype = None
- if numpy.dtype(array.dtype).kind == 'f':
+ if array.dtype.kind == 'f' and array.dtype.itemsize != 4:
+ # Cast to float32
+ _logger.info('Cast array to float32')
dtype = numpy.float32
+ elif array.dtype.itemsize > 4:
+ # Cast (u)int64 to (u)int32
+ if array.dtype.kind == 'i':
+ _logger.info('Cast array to int32')
+ dtype = numpy.int32
+ elif array.dtype.kind == 'u':
+ _logger.info('Cast array to uint32')
+ dtype = numpy.uint32
return numpy.array(array, dtype=dtype, order='C', copy=copy)
@@ -152,6 +179,11 @@ class Geometry(core.Elem):
return len(array)
return None
+ @property
+ def attrib0(self):
+ """Attribute name that MUST be an array (str)"""
+ return self._attrib0
+
def setAttribute(self, name, array, copy=True):
"""Set attribute with provided array.
@@ -169,29 +201,33 @@ class Geometry(core.Elem):
array = self._glReadyArray(array, copy=copy)
if name not in self._ATTR_INFO:
- _logger.info('Not checking attibute %s dimensions', name)
+ _logger.info('Not checking attribute %s dimensions', name)
else:
checks = self._ATTR_INFO[name]
- if (len(array.shape) == 1 and checks['lastDim'] == (1,) and
+ if (array.ndim == 1 and checks['lastDim'] == (1,) and
len(array) > 1):
array = array.reshape((len(array), 1))
# Checks
- assert len(array.shape) in checks['dims'], "Attr %s" % name
+ assert array.ndim in checks['dims'], "Attr %s" % name
assert array.shape[-1] in checks['lastDim'], "Attr %s" % name
+ # Makes sure attrib0 is considered as an array of values
+ if name == self.attrib0 and array.ndim == 1:
+ array.shape = 1, -1
+
# Check length against another attribute array
# Causes problems when updating
# nbVertices = self.nbVertices
- # if len(array.shape) == 2 and nbVertices is not None:
+ # if array.ndim == 2 and nbVertices is not None:
# assert len(array) == nbVertices
self._attributes[name] = array
- if len(array.shape) == 2: # Store this in a VBO
+ if array.ndim == 2: # Store this in a VBO
self._unsyncAttributes.append(name)
- if name == 'position': # Reset bounds
+ if name in self.boundsAttributeNames: # Reset bounds
self.__bounds = None
self.notify()
@@ -238,7 +274,7 @@ class Geometry(core.Elem):
array = self._attributes[name]
assert array is not None
- if len(array.shape) == 1:
+ if array.ndim == 1:
assert len(array) in (1, 2, 3, 4)
gl.glDisableVertexAttribArray(attribute)
_glVertexAttribFunc = getattr(
@@ -273,6 +309,7 @@ class Geometry(core.Elem):
# This might be a costy check
assert indices.max() < self.nbVertices
self._indices = indices
+ self.notify()
def getIndices(self, copy=True):
"""Returns the numpy.ndarray corresponding to the indices.
@@ -287,16 +324,59 @@ class Geometry(core.Elem):
else:
return numpy.array(self._indices, copy=copy)
+ @property
+ def boundsAttributeNames(self):
+ """Tuple of attribute names defining the bounds of the object.
+
+ Attributes name are taken in the given order to compute the
+ (x, y, z) the bounding box, e.g.::
+
+ geometry.boundsAttributeNames = 'position'
+ geometry.boundsAttributeNames = 'x', 'y', 'z'
+ """
+ return self.__boundsAttributeNames
+
+ @boundsAttributeNames.setter
+ def boundsAttributeNames(self, names):
+ self.__boundsAttributeNames = tuple(str(name) for name in names)
+ self.__bounds = None
+ self.notify()
+
def _bounds(self, dataBounds=False):
if self.__bounds is None:
+ if len(self.boundsAttributeNames) == 0:
+ return None # No bounds
+
self.__bounds = numpy.zeros((2, 3), dtype=numpy.float32)
- # Support vertex with to 2 to 4 coordinates
- positions = self._attributes['position']
- self.__bounds[0, :positions.shape[1]] = \
- numpy.nanmin(positions, axis=0)[:3]
- self.__bounds[1, :positions.shape[1]] = \
- numpy.nanmax(positions, axis=0)[:3]
+
+ # Coordinates defined in one or more attributes
+ index = 0
+ for name in self.boundsAttributeNames:
+ if index == 3:
+ _logger.error("Too many attributes defining bounds")
+ break
+
+ attribute = self._attributes[name]
+ assert attribute.ndim in (1, 2)
+ if attribute.ndim == 1: # Single value
+ min_ = attribute
+ max_ = attribute
+ else: # Array of values, compute min/max
+ min_ = numpy.nanmin(attribute, axis=0)
+ max_ = numpy.nanmax(attribute, axis=0)
+
+ toCopy = min(len(min_), 3-index)
+ if toCopy != len(min_):
+ _logger.error("Attribute defining bounds"
+ " has too many dimensions")
+
+ self.__bounds[0, index:index+toCopy] = min_[:toCopy]
+ self.__bounds[1, index:index+toCopy] = max_[:toCopy]
+
+ index += toCopy
+
self.__bounds[numpy.isnan(self.__bounds)] = 0. # Avoid NaNs
+
return self.__bounds.copy()
def prepareGL2(self, ctx):
@@ -593,9 +673,7 @@ class Box(core.PrivateGroup):
(0., 0., 1.), (1., 0., 1.), (1., 1., 1.), (0., 1., 1.)),
dtype=numpy.float32)
- def __init__(self, size=(1., 1., 1.),
- stroke=(1., 1., 1., 1.),
- fill=(1., 1., 1., 0.)):
+ def __init__(self, stroke=(1., 1., 1., 1.), fill=(1., 1., 1., 0.)):
super(Box, self).__init__()
self._fill = Mesh3D(self._vertices,
@@ -613,8 +691,27 @@ class Box(core.PrivateGroup):
self._children = [self._stroke, self._fill]
- self._size = None
- self.size = size
+ self._size = 1., 1., 1.
+
+ @classmethod
+ def getLineIndices(cls, copy=True):
+ """Returns 2D array of Box lines indices
+
+ :param copy: True (default) to get a copy,
+ False to get internal array (Do not modify!)
+ :rtype: numpy.ndarray
+ """
+ return numpy.array(cls._lineIndices, copy=copy)
+
+ @classmethod
+ def getVertices(cls, copy=True):
+ """Returns 2D array of Box corner coordinates.
+
+ :param copy: True (default) to get a copy,
+ False to get internal array (Do not modify!)
+ :rtype: numpy.ndarray
+ """
+ return numpy.array(cls._vertices, copy=copy)
@property
def size(self):
@@ -712,6 +809,23 @@ class Axes(Lines):
super(Axes, self).__init__(self._vertices,
colors=self._colors,
width=3.)
+ self._size = 1., 1., 1.
+
+ @property
+ def size(self):
+ """Size of the axes (sx, sy, sz)"""
+ return self._size
+
+ @size.setter
+ def size(self, size):
+ assert len(size) == 3
+ size = tuple(size)
+ if size != self.size:
+ self._size = size
+ self.setAttribute(
+ 'position',
+ self._vertices * numpy.array(size, dtype=numpy.float32))
+ self.notify()
class BoxWithAxes(Lines):
@@ -752,6 +866,7 @@ class BoxWithAxes(Lines):
indices=self._lineIndices,
colors=colors,
width=2.)
+ self._size = 1., 1., 1.
self.color = color
@property
@@ -769,6 +884,22 @@ class BoxWithAxes(Lines):
colors[len(self._axesColors):, :] = self._color
self.setAttribute('color', colors) # Do the notification
+ @property
+ def size(self):
+ """Size of the axes (sx, sy, sz)"""
+ return self._size
+
+ @size.setter
+ def size(self, size):
+ assert len(size) == 3
+ size = tuple(size)
+ if size != self.size:
+ self._size = size
+ self.setAttribute(
+ 'position',
+ self._vertices * numpy.array(size, dtype=numpy.float32))
+ self.notify()
+
class PlaneInGroup(core.PrivateGroup):
"""A plane using its parent bounds to display a contour.
@@ -788,6 +919,7 @@ class PlaneInGroup(core.PrivateGroup):
self._color = None
self.color = 1., 1., 1., 1. # Set _color
self._width = 2.
+ self._strokeVisible = True
self._plane = utils.Plane(point, normal)
self._plane.addListener(self._planeChanged)
@@ -825,6 +957,17 @@ class PlaneInGroup(core.PrivateGroup):
if self._outline is not None:
self._outline.width = self._width # Sync width
+ @property
+ def strokeVisible(self):
+ """Whether surrounding stroke is visible or not (bool)."""
+ return self._strokeVisible
+
+ @strokeVisible.setter
+ def strokeVisible(self, visible):
+ self._strokeVisible = bool(visible)
+ if self._outline is not None:
+ self._outline.visible = self._strokeVisible
+
# Plane access
@property
@@ -865,10 +1008,11 @@ class PlaneInGroup(core.PrivateGroup):
return cachevertices
# Cache is not OK, rebuild it
- boxvertices = bounds[0] + Box._vertices.copy()*(bounds[1] - bounds[0])
- lineindices = Box._lineIndices
+ boxVertices = Box.getVertices(copy=True)
+ boxVertices = bounds[0] + boxVertices * (bounds[1] - bounds[0])
+ lineIndices = Box.getLineIndices(copy=False)
vertices = utils.boxPlaneIntersect(
- boxvertices, lineindices, self.plane.normal, self.plane.point)
+ boxVertices, lineIndices, self.plane.normal, self.plane.point)
self._cache = bounds, vertices if len(vertices) != 0 else None
@@ -894,6 +1038,7 @@ class PlaneInGroup(core.PrivateGroup):
mode='loop',
colors=self.color)
self._outline.width = self._width
+ self._outline.visible = self._strokeVisible
self._children.append(self._outline)
# Update vertices, TODO only when necessary
@@ -906,303 +1051,362 @@ class PlaneInGroup(core.PrivateGroup):
super(PlaneInGroup, self).renderGL2(ctx)
+class BoundedGroup(core.Group):
+ """Group with data bounds"""
+
+ _shape = None # To provide a default value without overriding __init__
+
+ @property
+ def shape(self):
+ """Data shape (depth, height, width) of this group or None"""
+ return self._shape
+
+ @shape.setter
+ def shape(self, shape):
+ if shape is None:
+ self._shape = None
+ else:
+ depth, height, width = shape
+ self._shape = float(depth), float(height), float(width)
+
+ @property
+ def size(self):
+ """Data size (width, height, depth) of this group or None"""
+ shape = self.shape
+ if shape is None:
+ return None
+ else:
+ return shape[2], shape[1], shape[0]
+
+ @size.setter
+ def size(self, size):
+ if size is None:
+ self.shape = None
+ else:
+ self.shape = size[2], size[1], size[0]
+
+ def _bounds(self, dataBounds=False):
+ if dataBounds and self.size is not None:
+ return numpy.array(((0., 0., 0.), self.size),
+ dtype=numpy.float32)
+ else:
+ return super(BoundedGroup, self)._bounds(dataBounds)
+
+
# Points ######################################################################
-_POINTS_ATTR_INFO = Geometry._ATTR_INFO.copy()
-_POINTS_ATTR_INFO.update(value={'dims': (1, 2), 'lastDim': (1,)},
- size={'dims': (1, 2), 'lastDim': (1,)},
- symbol={'dims': (1, 2), 'lastDim': (1,)})
+class _Points(Geometry):
+ """Base class to render a set of points."""
+
+ DIAMOND = 'd'
+ CIRCLE = 'o'
+ SQUARE = 's'
+ PLUS = '+'
+ X_MARKER = 'x'
+ ASTERISK = '*'
+ H_LINE = '_'
+ V_LINE = '|'
+
+ SUPPORTED_MARKERS = (DIAMOND, CIRCLE, SQUARE, PLUS,
+ X_MARKER, ASTERISK, H_LINE, V_LINE)
+ """List of supported markers:
+
+ - 'd' diamond
+ - 'o' circle
+ - 's' square
+ - '+' cross
+ - 'x' x-cross
+ - '*' asterisk
+ - '_' horizontal line
+ - '|' vertical line
+ """
+ _MARKER_FUNCTIONS = {
+ DIAMOND: """
+ float alphaSymbol(vec2 coord, float size) {
+ vec2 centerCoord = abs(coord - vec2(0.5, 0.5));
+ float f = centerCoord.x + centerCoord.y;
+ return clamp(size * (0.5 - f), 0.0, 1.0);
+ }
+ """,
+ CIRCLE: """
+ float alphaSymbol(vec2 coord, float size) {
+ float radius = 0.5;
+ float r = distance(coord, vec2(0.5, 0.5));
+ return clamp(size * (radius - r), 0.0, 1.0);
+ }
+ """,
+ SQUARE: """
+ float alphaSymbol(vec2 coord, float size) {
+ return 1.0;
+ }
+ """,
+ PLUS: """
+ float alphaSymbol(vec2 coord, float size) {
+ vec2 d = abs(size * (coord - vec2(0.5, 0.5)));
+ if (min(d.x, d.y) < 0.5) {
+ return 1.0;
+ } else {
+ return 0.0;
+ }
+ }
+ """,
+ X_MARKER: """
+ float alphaSymbol(vec2 coord, float size) {
+ vec2 pos = floor(size * coord) + 0.5;
+ vec2 d_x = abs(pos.x + vec2(- pos.y, pos.y - size));
+ if (min(d_x.x, d_x.y) <= 0.5) {
+ return 1.0;
+ } else {
+ return 0.0;
+ }
+ }
+ """,
+ ASTERISK: """
+ float alphaSymbol(vec2 coord, float size) {
+ /* Combining +, x and circle */
+ vec2 d_plus = abs(size * (coord - vec2(0.5, 0.5)));
+ vec2 pos = floor(size * coord) + 0.5;
+ vec2 d_x = abs(pos.x + vec2(- pos.y, pos.y - size));
+ if (min(d_plus.x, d_plus.y) < 0.5) {
+ return 1.0;
+ } else if (min(d_x.x, d_x.y) <= 0.5) {
+ float r = distance(coord, vec2(0.5, 0.5));
+ return clamp(size * (0.5 - r), 0.0, 1.0);
+ } else {
+ return 0.0;
+ }
+ }
+ """,
+ H_LINE: """
+ float alphaSymbol(vec2 coord, float size) {
+ float dy = abs(size * (coord.y - 0.5));
+ if (dy < 0.5) {
+ return 1.0;
+ } else {
+ return 0.0;
+ }
+ }
+ """,
+ V_LINE: """
+ float alphaSymbol(vec2 coord, float size) {
+ float dx = abs(size * (coord.x - 0.5));
+ if (dx < 0.5) {
+ return 1.0;
+ } else {
+ return 0.0;
+ }
+ }
+ """
+ }
-class Points(Geometry):
- """A set of data points with an associated value and size."""
- _shaders = ("""
+ _shaders = (string.Template("""
#version 120
- attribute vec3 position;
- attribute float symbol;
- attribute float value;
+ attribute float x;
+ attribute float y;
+ attribute float z;
+ attribute $valueType value;
attribute float size;
uniform mat4 matrix;
uniform mat4 transformMat;
- uniform vec2 valRange;
-
varying vec4 vCameraPosition;
- varying float vSymbol;
- varying float vNormValue;
+ varying $valueType vValue;
varying float vSize;
void main(void)
{
- vSymbol = symbol;
-
- vNormValue = clamp((value - valRange.x) / (valRange.y - valRange.x),
- 0.0, 1.0);
+ vValue = value;
- bool isValueInRange = value >= valRange.x && value <= valRange.y;
- if (isValueInRange) {
- gl_Position = matrix * vec4(position, 1.0);
- } else {
- gl_Position = vec4(2.0, 0.0, 0.0, 1.0); /* Get clipped */
- }
- vCameraPosition = transformMat * vec4(position, 1.0);
+ vec4 positionVec4 = vec4(x, y, z, 1.0);
+ gl_Position = matrix * positionVec4;
+ vCameraPosition = transformMat * positionVec4;
gl_PointSize = size;
vSize = size;
}
- """,
+ """),
string.Template("""
#version 120
varying vec4 vCameraPosition;
varying float vSize;
- varying float vSymbol;
- varying float vNormValue;
-
- $clippinDecl
-
- /* Circle */
- #define SYMBOL_CIRCLE 1.0
+ varying $valueType vValue;
- float alphaCircle(vec2 coord, float size) {
- float radius = 0.5;
- float r = distance(coord, vec2(0.5, 0.5));
- return clamp(size * (radius - r), 0.0, 1.0);
- }
+ $valueToColorDecl
- /* Half lines */
- #define SYMBOL_H_LINE 2.0
- #define LEFT 1.0
- #define RIGHT 2.0
- #define SYMBOL_V_LINE 3.0
- #define UP 1.0
- #define DOWN 2.0
+ $clippingDecl
- float alphaLine(vec2 coord, float size, float direction)
- {
- vec2 delta = abs(size * (coord - 0.5));
-
- if (direction == SYMBOL_H_LINE) {
- return (delta.y < 0.5) ? 1.0 : 0.0;
- }
- else if (direction == SYMBOL_H_LINE + LEFT) {
- return (coord.x <= 0.5 && delta.y < 0.5) ? 1.0 : 0.0;
- }
- else if (direction == SYMBOL_H_LINE + RIGHT) {
- return (coord.x >= 0.5 && delta.y < 0.5) ? 1.0 : 0.0;
- }
- else if (direction == SYMBOL_V_LINE) {
- return (delta.x < 0.5) ? 1.0 : 0.0;
- }
- else if (direction == SYMBOL_V_LINE + UP) {
- return (coord.y <= 0.5 && delta.x < 0.5) ? 1.0 : 0.0;
- }
- else if (direction == SYMBOL_V_LINE + DOWN) {
- return (coord.y >= 0.5 && delta.x < 0.5) ? 1.0 : 0.0;
- }
- return 1.0;
- }
+ $alphaSymbolDecl
void main(void)
{
$clippingCall(vCameraPosition);
- gl_FragColor = vec4(0.5 * vNormValue + 0.5, 0.0, 0.0, 1.0);
-
- float alpha = 1.0;
- float symbol = floor(vSymbol);
- if (1 == 1) { //symbol == SYMBOL_CIRCLE) {
- alpha = alphaCircle(gl_PointCoord, vSize);
- }
- else if (symbol >= SYMBOL_H_LINE &&
- symbol <= (SYMBOL_V_LINE + DOWN)) {
- alpha = alphaLine(gl_PointCoord, vSize, symbol);
- }
+ float alpha = alphaSymbol(gl_PointCoord, vSize);
if (alpha == 0.0) {
discard;
}
+
+ gl_FragColor = $valueToColorCall(vValue);
gl_FragColor.a *= alpha;
}
"""))
- _ATTR_INFO = _POINTS_ATTR_INFO
+ _ATTR_INFO = {
+ 'x': {'dims': (1, 2), 'lastDim': (1,)},
+ 'y': {'dims': (1, 2), 'lastDim': (1,)},
+ 'z': {'dims': (1, 2), 'lastDim': (1,)},
+ 'size': {'dims': (1, 2), 'lastDim': (1,)},
+ }
- # TODO Add colormap, light?
+ def __init__(self, x, y, z, value, size=1., indices=None):
+ super(_Points, self).__init__('points', indices,
+ x=x,
+ y=y,
+ z=z,
+ value=value,
+ size=size,
+ attrib0='x')
+ self.boundsAttributeNames = 'x', 'y', 'z'
+ self._marker = 'o'
- def __init__(self, vertices, values=0., sizes=1., indices=None,
- symbols=0.,
- minValue=None, maxValue=None):
- super(Points, self).__init__('points', indices,
- position=vertices,
- value=values,
- size=sizes,
- symbol=symbols)
+ @property
+ def marker(self):
+ """The marker symbol used to display the scatter plot (str)
- values = self._attributes['value']
- self._minValue = values.min() if minValue is None else minValue
- self._maxValue = values.max() if maxValue is None else maxValue
+ See :attr:`SUPPORTED_MARKERS` for the list of supported marker string.
+ """
+ return self._marker
+
+ @marker.setter
+ def marker(self, marker):
+ marker = str(marker)
+ assert marker in self.SUPPORTED_MARKERS
+ if marker != self._marker:
+ self._marker = marker
+ self.notify()
- minValue = event.notifyProperty('_minValue')
- maxValue = event.notifyProperty('_maxValue')
+ def _shaderValueDefinition(self):
+ """Type definition, fragment shader declaration, fragment shader call
+ """
+ raise NotImplementedError(
+ "This method must be implemented in subclass")
+
+ def _renderGL2PreDrawHook(self, ctx, program):
+ """Override in subclass to run code before calling gl draw"""
+ pass
def renderGL2(self, ctx):
- fragment = self._shaders[1].substitute(
+ valueType, valueToColorDecl, valueToColorCall = \
+ self._shaderValueDefinition()
+ vertexShader = self._shaders[0].substitute(
+ valueType=valueType)
+ fragmentShader = self._shaders[1].substitute(
clippingDecl=ctx.clipper.fragDecl,
- clippingCall=ctx.clipper.fragCall)
- prog = ctx.glCtx.prog(self._shaders[0], fragment)
- prog.use()
+ clippingCall=ctx.clipper.fragCall,
+ valueType=valueType,
+ valueToColorDecl=valueToColorDecl,
+ valueToColorCall=valueToColorCall,
+ alphaSymbolDecl=self._MARKER_FUNCTIONS[self.marker])
+ program = ctx.glCtx.prog(vertexShader, fragmentShader,
+ attrib0=self.attrib0)
+ program.use()
gl.glEnable(gl.GL_VERTEX_PROGRAM_POINT_SIZE) # OpenGL 2
gl.glEnable(gl.GL_POINT_SPRITE) # OpenGL 2
# gl.glEnable(gl.GL_PROGRAM_POINT_SIZE)
- prog.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
- prog.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
-
- ctx.clipper.setupProgram(ctx, prog)
-
- gl.glUniform2f(prog.uniforms['valRange'], self.minValue, self.maxValue)
-
- self._draw(prog)
-
-
-class ColorPoints(Geometry):
- """A set of points with an associated color and size."""
+ program.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
+ program.setUniformMatrix('transformMat',
+ ctx.objectToCamera.matrix,
+ safe=True)
- _shaders = ("""
- #version 120
+ ctx.clipper.setupProgram(ctx, program)
- attribute vec3 position;
- attribute float symbol;
- attribute vec4 color;
- attribute float size;
+ self._renderGL2PreDrawHook(ctx, program)
- uniform mat4 matrix;
- uniform mat4 transformMat;
+ self._draw(program)
- varying vec4 vCameraPosition;
- varying float vSymbol;
- varying vec4 vColor;
- varying float vSize;
- void main(void)
- {
- vCameraPosition = transformMat * vec4(position, 1.0);
- vSymbol = symbol;
- vColor = color;
- gl_Position = matrix * vec4(position, 1.0);
- gl_PointSize = size;
- vSize = size;
- }
- """,
- string.Template("""
- #version 120
-
- varying vec4 vCameraPosition;
- varying float vSize;
- varying float vSymbol;
- varying vec4 vColor;
+class Points(_Points):
+ """A set of data points with an associated value and size."""
- $clippingDecl;
+ _ATTR_INFO = _Points._ATTR_INFO.copy()
+ _ATTR_INFO.update({'value': {'dims': (1, 2), 'lastDim': (1,)}})
- /* Circle */
- #define SYMBOL_CIRCLE 1.0
+ def __init__(self, x, y, z, value=0., size=1.,
+ indices=None, colormap=None):
+ super(Points, self).__init__(x=x,
+ y=y,
+ z=z,
+ indices=indices,
+ size=size,
+ value=value)
- float alphaCircle(vec2 coord, float size) {
- float radius = 0.5;
- float r = distance(coord, vec2(0.5, 0.5));
- return clamp(size * (radius - r), 0.0, 1.0);
- }
+ self._colormap = colormap or Colormap() # Default colormap
+ self._colormap.addListener(self._cmapChanged)
- /* Half lines */
- #define SYMBOL_H_LINE 2.0
- #define LEFT 1.0
- #define RIGHT 2.0
- #define SYMBOL_V_LINE 3.0
- #define UP 1.0
- #define DOWN 2.0
+ @property
+ def colormap(self):
+ """The colormap used to render the image"""
+ return self._colormap
- float alphaLine(vec2 coord, float size, float direction)
- {
- vec2 delta = abs(size * (coord - 0.5));
+ def _cmapChanged(self, source, *args, **kwargs):
+ """Broadcast colormap changes"""
+ self.notify(*args, **kwargs)
- if (direction == SYMBOL_H_LINE) {
- return (delta.y < 0.5) ? 1.0 : 0.0;
- }
- else if (direction == SYMBOL_H_LINE + LEFT) {
- return (coord.x <= 0.5 && delta.y < 0.5) ? 1.0 : 0.0;
- }
- else if (direction == SYMBOL_H_LINE + RIGHT) {
- return (coord.x >= 0.5 && delta.y < 0.5) ? 1.0 : 0.0;
- }
- else if (direction == SYMBOL_V_LINE) {
- return (delta.x < 0.5) ? 1.0 : 0.0;
- }
- else if (direction == SYMBOL_V_LINE + UP) {
- return (coord.y <= 0.5 && delta.x < 0.5) ? 1.0 : 0.0;
- }
- else if (direction == SYMBOL_V_LINE + DOWN) {
- return (coord.y >= 0.5 && delta.x < 0.5) ? 1.0 : 0.0;
- }
- return 1.0;
- }
+ def _shaderValueDefinition(self):
+ """Type definition, fragment shader declaration, fragment shader call
+ """
+ return 'float', self.colormap.decl, self.colormap.call
- void main(void)
- {
- $clippingCall(vCameraPosition);
+ def _renderGL2PreDrawHook(self, ctx, program):
+ """Set-up colormap before calling gl draw"""
+ self.colormap.setupProgram(ctx, program)
- gl_FragColor = vColor;
- float alpha = 1.0;
- float symbol = floor(vSymbol);
- if (1 == 1) { //symbol == SYMBOL_CIRCLE) {
- alpha = alphaCircle(gl_PointCoord, vSize);
- }
- else if (symbol >= SYMBOL_H_LINE &&
- symbol <= (SYMBOL_V_LINE + DOWN)) {
- alpha = alphaLine(gl_PointCoord, vSize, symbol);
- }
- if (alpha == 0.0) {
- discard;
- }
- gl_FragColor.a *= alpha;
- }
- """))
+class ColorPoints(_Points):
+ """A set of points with an associated color and size."""
- _ATTR_INFO = _POINTS_ATTR_INFO
+ _ATTR_INFO = _Points._ATTR_INFO.copy()
+ _ATTR_INFO.update({'value': {'dims': (1, 2), 'lastDim': (4,)}})
- def __init__(self, vertices, colors=(1., 1., 1., 1.), sizes=1.,
- indices=None, symbols=0.,
- minValue=None, maxValue=None):
- super(ColorPoints, self).__init__('points', indices,
- position=vertices,
- color=colors,
- size=sizes,
- symbol=symbols)
+ def __init__(self, x, y, z, color=(1., 1., 1., 1.), size=1.,
+ indices=None):
+ super(ColorPoints, self).__init__(x=x,
+ y=y,
+ z=z,
+ indices=indices,
+ size=size,
+ value=color)
- def renderGL2(self, ctx):
- fragment = self._shaders[1].substitute(
- clippingDecl=ctx.clipper.fragDecl,
- clippingCall=ctx.clipper.fragCall)
- prog = ctx.glCtx.prog(self._shaders[0], fragment)
- prog.use()
+ def _shaderValueDefinition(self):
+ """Type definition, fragment shader declaration, fragment shader call
+ """
+ return 'vec4', '', ''
- gl.glEnable(gl.GL_VERTEX_PROGRAM_POINT_SIZE) # OpenGL 2
- gl.glEnable(gl.GL_POINT_SPRITE) # OpenGL 2
- # gl.glEnable(gl.GL_PROGRAM_POINT_SIZE)
+ def setColor(self, color, copy=True):
+ """Set colors
- prog.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
- prog.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
+ :param color: Single RGBA color or
+ 2D array of color of length number of points
+ :param bool copy: True to copy colors (default),
+ False to use provided array (Do not modify!)
+ """
+ self.setAttribute('value', color, copy=copy)
- ctx.clipper.setupProgram(ctx, prog)
+ def getColor(self, copy=True):
+ """Returns the color or array of colors of the points.
- self._draw(prog)
+ :param copy: True to get a copy (default),
+ False to return internal array (Do not modify!)
+ :return: Color or array of colors
+ :rtype: numpy.ndarray
+ """
+ return self.getAttribute('value', copy=copy)
class GridPoints(Geometry):
@@ -1560,12 +1764,14 @@ class Mesh3D(Geometry):
colors,
normals=None,
mode='triangles',
- indices=None):
+ indices=None,
+ copy=True):
assert mode in self._TRIANGLE_MODES
super(Mesh3D, self).__init__(mode, indices,
position=positions,
normal=normals,
- color=colors)
+ color=colors,
+ copy=copy)
self._culling = None
@@ -1620,6 +1826,435 @@ class Mesh3D(Geometry):
gl.glDisable(gl.GL_CULL_FACE)
+class ColormapMesh3D(Geometry):
+ """A 3D mesh with color computed from a colormap"""
+
+ _shaders = ("""
+ attribute vec3 position;
+ attribute vec3 normal;
+ attribute float value;
+
+ uniform mat4 matrix;
+ uniform mat4 transformMat;
+ //uniform mat3 matrixInvTranspose;
+
+ varying vec4 vCameraPosition;
+ varying vec3 vPosition;
+ varying vec3 vNormal;
+ varying float vValue;
+
+ void main(void)
+ {
+ vCameraPosition = transformMat * vec4(position, 1.0);
+ //vNormal = matrixInvTranspose * normalize(normal);
+ vPosition = position;
+ vNormal = normal;
+ vValue = value;
+ gl_Position = matrix * vec4(position, 1.0);
+ }
+ """,
+ string.Template("""
+ varying vec4 vCameraPosition;
+ varying vec3 vPosition;
+ varying vec3 vNormal;
+ varying float vValue;
+
+ $colormapDecl
+ $clippingDecl
+ $lightingFunction
+
+ void main(void)
+ {
+ $clippingCall(vCameraPosition);
+
+ vec4 color = $colormapCall(vValue);
+ gl_FragColor = $lightingCall(color, vPosition, vNormal);
+ }
+ """))
+
+ def __init__(self,
+ position,
+ value,
+ colormap=None,
+ normal=None,
+ mode='triangles',
+ indices=None):
+ super(ColormapMesh3D, self).__init__(mode, indices,
+ position=position,
+ normal=normal,
+ value=value)
+
+ self._lineWidth = 1.0
+ self._lineSmooth = True
+ self._culling = None
+ self._colormap = colormap or Colormap() # Default colormap
+ self._colormap.addListener(self._cmapChanged)
+
+ lineWidth = event.notifyProperty('_lineWidth', converter=float,
+ doc="Width of the line in pixels.")
+
+ lineSmooth = event.notifyProperty(
+ '_lineSmooth',
+ converter=bool,
+ doc="Smooth line rendering enabled (bool, default: True)")
+
+ @property
+ def culling(self):
+ """Face culling (str)
+
+ One of 'back', 'front' or None.
+ """
+ return self._culling
+
+ @culling.setter
+ def culling(self, culling):
+ assert culling in ('back', 'front', None)
+ if culling != self._culling:
+ self._culling = culling
+ self.notify()
+
+ @property
+ def colormap(self):
+ """The colormap used to render the image"""
+ return self._colormap
+
+ def _cmapChanged(self, source, *args, **kwargs):
+ """Broadcast colormap changes"""
+ self.notify(*args, **kwargs)
+
+ def renderGL2(self, ctx):
+ if 'normal' in self._attributes:
+ self._renderGL2(ctx)
+ else: # Disable lighting
+ with self.viewport.light.turnOff():
+ self._renderGL2(ctx)
+
+ def _renderGL2(self, ctx):
+ fragment = self._shaders[1].substitute(
+ clippingDecl=ctx.clipper.fragDecl,
+ clippingCall=ctx.clipper.fragCall,
+ lightingFunction=ctx.viewport.light.fragmentDef,
+ lightingCall=ctx.viewport.light.fragmentCall,
+ colormapDecl=self.colormap.decl,
+ colormapCall=self.colormap.call)
+ program = ctx.glCtx.prog(self._shaders[0], fragment)
+ program.use()
+
+ ctx.viewport.light.setupProgram(ctx, program)
+ ctx.clipper.setupProgram(ctx, program)
+ self.colormap.setupProgram(ctx, program)
+
+ if self.culling is not None:
+ cullFace = gl.GL_FRONT if self.culling == 'front' else gl.GL_BACK
+ gl.glCullFace(cullFace)
+ gl.glEnable(gl.GL_CULL_FACE)
+
+ program.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
+ program.setUniformMatrix('transformMat',
+ ctx.objectToCamera.matrix,
+ safe=True)
+
+ if self.drawMode in self._LINE_MODES:
+ gl.glLineWidth(self.lineWidth)
+ with gl.enabled(gl.GL_LINE_SMOOTH, self.lineSmooth):
+ self._draw(program)
+ else:
+ self._draw(program)
+
+ if self.culling is not None:
+ gl.glDisable(gl.GL_CULL_FACE)
+
+
+# ImageData ##################################################################
+
+class _Image(Geometry):
+ """Base class for ImageData and ImageRgba"""
+
+ _shaders = ("""
+ attribute vec2 position;
+
+ uniform mat4 matrix;
+ uniform mat4 transformMat;
+ uniform vec2 dataScale;
+
+ varying vec4 vCameraPosition;
+ varying vec3 vPosition;
+ varying vec3 vNormal;
+ varying vec2 vTexCoords;
+
+ void main(void)
+ {
+ vec4 positionVec4 = vec4(position, 0.0, 1.0);
+ vCameraPosition = transformMat * positionVec4;
+ vPosition = positionVec4.xyz;
+ vTexCoords = dataScale * position;
+ gl_Position = matrix * positionVec4;
+ }
+ """,
+ string.Template("""
+ varying vec4 vCameraPosition;
+ varying vec3 vPosition;
+ varying vec2 vTexCoords;
+ uniform sampler2D data;
+ uniform float alpha;
+
+ $imageDecl
+
+ $clippingDecl
+
+ $lightingFunction
+
+ void main(void)
+ {
+ vec4 color = imageColor(data, vTexCoords);
+ color.a = alpha;
+
+ $clippingCall(vCameraPosition);
+
+ vec3 normal = vec3(0.0, 0.0, 1.0);
+ gl_FragColor = $lightingCall(color, vPosition, normal);
+ }
+ """))
+
+ _UNIT_SQUARE = numpy.array(((0., 0.), (1., 0.), (0., 1.), (1., 1.)),
+ dtype=numpy.float32)
+
+ def __init__(self, data, copy=True):
+ super(_Image, self).__init__(mode='triangle_strip',
+ position=self._UNIT_SQUARE)
+
+ self._texture = None
+ self._update_texture = True
+ self._update_texture_filter = False
+ self._data = None
+ self.setData(data, copy)
+ self._alpha = 1.
+ self._interpolation = 'linear'
+
+ self.isBackfaceVisible = True
+
+ def setData(self, data, copy=True):
+ assert isinstance(data, numpy.ndarray)
+
+ if copy:
+ data = numpy.array(data, copy=True)
+
+ self._data = data
+ self._update_texture = True
+ # By updating the position rather than always using a unit square
+ # we benefit from Geometry bounds handling
+ self.setAttribute('position', self._UNIT_SQUARE * self._data.shape[:2])
+ self.notify()
+
+ def getData(self, copy=True):
+ return numpy.array(self._data, copy=copy)
+
+ @property
+ def interpolation(self):
+ """The texture interpolation mode: 'linear' or 'nearest'"""
+ return self._interpolation
+
+ @interpolation.setter
+ def interpolation(self, interpolation):
+ assert interpolation in ('linear', 'nearest')
+ self._interpolation = interpolation
+ self._update_texture_filter = True
+ self.notify()
+
+ @property
+ def alpha(self):
+ """Transparency of the image, float in [0, 1]"""
+ return self._alpha
+
+ @alpha.setter
+ def alpha(self, alpha):
+ self._alpha = float(alpha)
+ self.notify()
+
+ def _textureFormat(self):
+ """Implement this method to provide texture internal format and format
+
+ :return: 2-tuple of gl flags (internalFormat, format)
+ """
+ raise NotImplementedError(
+ "This method must be implemented in a subclass")
+
+ def prepareGL2(self, ctx):
+ if self._texture is None or self._update_texture:
+ if self._texture is not None:
+ self._texture.discard()
+
+ if self.interpolation == 'nearest':
+ filter_ = gl.GL_NEAREST
+ else:
+ filter_ = gl.GL_LINEAR
+ self._update_texture = False
+ self._update_texture_filter = False
+ if self._data.size == 0:
+ self._texture = None
+ else:
+ internalFormat, format_ = self._textureFormat()
+ self._texture = _glutils.Texture(
+ internalFormat,
+ self._data,
+ format_,
+ minFilter=filter_,
+ magFilter=filter_,
+ wrap=gl.GL_CLAMP_TO_EDGE)
+
+ if self._update_texture_filter and self._texture is not None:
+ self._update_texture_filter = False
+ if self.interpolation == 'nearest':
+ filter_ = gl.GL_NEAREST
+ else:
+ filter_ = gl.GL_LINEAR
+ self._texture.minFilter = filter_
+ self._texture.magFilter = filter_
+
+ super(_Image, self).prepareGL2(ctx)
+
+ def renderGL2(self, ctx):
+ if self._texture is None:
+ return # Nothing to render
+
+ with self.viewport.light.turnOff():
+ self._renderGL2(ctx)
+
+ def _renderGL2PreDrawHook(self, ctx, program):
+ """Override in subclass to run code before calling gl draw"""
+ pass
+
+ def _shaderImageColorDecl(self):
+ """Returns fragment shader imageColor function declaration"""
+ raise NotImplementedError(
+ "This method must be implemented in a subclass")
+
+ def _renderGL2(self, ctx):
+ fragment = self._shaders[1].substitute(
+ clippingDecl=ctx.clipper.fragDecl,
+ clippingCall=ctx.clipper.fragCall,
+ lightingFunction=ctx.viewport.light.fragmentDef,
+ lightingCall=ctx.viewport.light.fragmentCall,
+ imageDecl=self._shaderImageColorDecl()
+ )
+ program = ctx.glCtx.prog(self._shaders[0], fragment)
+ program.use()
+
+ ctx.viewport.light.setupProgram(ctx, program)
+
+ if not self.isBackfaceVisible:
+ gl.glCullFace(gl.GL_BACK)
+ gl.glEnable(gl.GL_CULL_FACE)
+
+ program.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
+ program.setUniformMatrix('transformMat',
+ ctx.objectToCamera.matrix,
+ safe=True)
+ gl.glUniform1f(program.uniforms['alpha'], self._alpha)
+
+ shape = self._data.shape
+ gl.glUniform2f(program.uniforms['dataScale'], 1./shape[0], 1./shape[1])
+
+ gl.glUniform1i(program.uniforms['data'], self._texture.texUnit)
+
+ ctx.clipper.setupProgram(ctx, program)
+
+ self._texture.bind()
+
+ self._renderGL2PreDrawHook(ctx, program)
+
+ self._draw(program)
+
+ if not self.isBackfaceVisible:
+ gl.glDisable(gl.GL_CULL_FACE)
+
+
+class ImageData(_Image):
+ """Display a 2x2 data array with a texture."""
+
+ _imageDecl = string.Template("""
+ $colormapDecl
+
+ vec4 imageColor(sampler2D data, vec2 texCoords) {
+ float value = texture2D(data, texCoords).r;
+ vec4 color = $colormapCall(value);
+ return color;
+ }
+ """)
+
+ def __init__(self, data, copy=True, colormap=None):
+ super(ImageData, self).__init__(data, copy=copy)
+
+ self._colormap = colormap or Colormap() # Default colormap
+ self._colormap.addListener(self._cmapChanged)
+
+ def setData(self, data, copy=True):
+ data = numpy.array(data, copy=copy, order='C', dtype=numpy.float32)
+ # TODO support (u)int8|16
+ assert data.ndim == 2
+
+ super(ImageData, self).setData(data, copy=False)
+
+ @property
+ def colormap(self):
+ """The colormap used to render the image"""
+ return self._colormap
+
+ def _cmapChanged(self, source, *args, **kwargs):
+ """Broadcast colormap changes"""
+ self.notify(*args, **kwargs)
+
+ def _textureFormat(self):
+ return gl.GL_R32F, gl.GL_RED
+
+ def _renderGL2PreDrawHook(self, ctx, program):
+ self.colormap.setupProgram(ctx, program)
+
+ def _shaderImageColorDecl(self):
+ return self._imageDecl.substitute(
+ colormapDecl=self.colormap.decl,
+ colormapCall=self.colormap.call)
+
+
+# ImageRgba ##################################################################
+
+class ImageRgba(_Image):
+ """Display a 2x2 RGBA image with a texture.
+
+ Supports images of float in [0, 1] and uint8.
+ """
+
+ _imageDecl = """
+ vec4 imageColor(sampler2D data, vec2 texCoords) {
+ vec4 color = texture2D(data, texCoords);
+ return color;
+ }
+ """
+
+ def __init__(self, data, copy=True):
+ super(ImageRgba, self).__init__(data, copy=copy)
+
+ def setData(self, data, copy=True):
+ data = numpy.array(data, copy=copy, order='C')
+ assert data.ndim == 3
+ assert data.shape[2] in (3, 4)
+ if data.dtype.kind == 'f':
+ if data.dtype != numpy.dtype(numpy.float32):
+ _logger.warning("Converting image data to float32")
+ data = numpy.array(data, dtype=numpy.float32, copy=False)
+ else:
+ assert data.dtype == numpy.dtype(numpy.uint8)
+
+ super(ImageRgba, self).setData(data, copy=False)
+
+ def _textureFormat(self):
+ format_ = gl.GL_RGBA if self._data.shape[2] == 4 else gl.GL_RGB
+ return format_, format_
+
+ def _shaderImageColorDecl(self):
+ return self._imageDecl
+
+
# Group ######################################################################
# TODO lighting, clipping as groups?
@@ -1686,6 +2321,29 @@ class GroupDepthOffset(core.Group):
# TODO issue with picking in depth buffer!
+class GroupNoDepth(core.Group):
+ """A group rendering its children without writing to the depth buffer
+
+ :param bool mask: True (default) to disable writing in the depth buffer
+ :param bool notest: True (default) to disable depth test
+ """
+
+ def __init__(self, children=(), mask=True, notest=True):
+ super(GroupNoDepth, self).__init__(children)
+ self._mask = bool(mask)
+ self._notest = bool(notest)
+
+ def renderGL2(self, ctx):
+ if self._mask:
+ gl.glDepthMask(gl.GL_FALSE)
+
+ with gl.disabled(gl.GL_DEPTH_TEST, disable=self._notest):
+ super(GroupNoDepth, self).renderGL2(ctx)
+
+ if self._mask:
+ gl.glDepthMask(gl.GL_TRUE)
+
+
class GroupBBox(core.PrivateGroup):
"""A group displaying a bounding box around the children."""
@@ -1693,26 +2351,42 @@ class GroupBBox(core.PrivateGroup):
super(GroupBBox, self).__init__()
self._group = core.Group(children)
- self._boxTransforms = transform.TransformList(
- (transform.Translate(), transform.Scale()))
+ self._boxTransforms = transform.TransformList((transform.Translate(),))
+ # Using 1 of 3 primitives to render axes and/or bounding box
+ # To avoid z-fighting between axes and bounding box
self._boxWithAxes = BoxWithAxes(color)
self._boxWithAxes.smooth = False
self._boxWithAxes.transforms = self._boxTransforms
- self._children = [self._boxWithAxes, self._group]
+ self._box = Box(stroke=color, fill=(1., 1., 1., 0.))
+ self._box.strokeSmooth = False
+ self._box.transforms = self._boxTransforms
+ self._box.visible = False
+
+ self._axes = Axes()
+ self._axes.smooth = False
+ self._axes.transforms = self._boxTransforms
+ self._axes.visible = False
+
+ self.strokeWidth = 2.
+
+ self._children = [self._boxWithAxes, self._box, self._axes, self._group]
def _updateBoxAndAxes(self):
"""Update bbox and axes position and size according to children."""
bounds = self._group.bounds(dataBounds=True)
if bounds is not None:
origin = bounds[0]
- scale = [(d if d != 0. else 1.) for d in bounds[1] - bounds[0]]
+ size = bounds[1] - bounds[0]
else:
- origin, scale = (0., 0., 0.), (1., 1., 1.)
+ origin, size = (0., 0., 0.), (1., 1., 1.)
self._boxTransforms[0].translation = origin
- self._boxTransforms[1].scale = scale
+
+ self._boxWithAxes.size = size
+ self._box.size = size
+ self._axes.size = size
def _bounds(self, dataBounds=False):
self._updateBoxAndAxes()
@@ -1732,17 +2406,62 @@ class GroupBBox(core.PrivateGroup):
def children(self, iterable):
self._group.children = iterable
- # Give access to box color
+ # Give access to box color and stroke width
@property
def color(self):
"""The RGBA color to use for the box: 4 float in [0, 1]"""
- return self._boxWithAxes.color
+ return self._box.strokeColor
@color.setter
def color(self, color):
+ self._box.strokeColor = color
self._boxWithAxes.color = color
+ @property
+ def strokeWidth(self):
+ """The width of the stroke lines in pixels (float)"""
+ return self._box.strokeWidth
+
+ @strokeWidth.setter
+ def strokeWidth(self, width):
+ width = float(width)
+ self._box.strokeWidth = width
+ self._boxWithAxes.width = width
+ self._axes.width = width
+
+ # Toggle axes visibility
+
+ def _updateBoxAndAxesVisibility(self, axesVisible, boxVisible):
+ """Update visible flags of box and axes primitives accordingly.
+
+ :param bool axesVisible: True to display axes
+ :param bool boxVisible: True to display bounding box
+ """
+ self._boxWithAxes.visible = boxVisible and axesVisible
+ self._box.visible = boxVisible and not axesVisible
+ self._axes.visible = not boxVisible and axesVisible
+
+ @property
+ def axesVisible(self):
+ """Whether axes are displayed or not (bool)"""
+ return self._boxWithAxes.visible or self._axes.visible
+
+ @axesVisible.setter
+ def axesVisible(self, visible):
+ self._updateBoxAndAxesVisibility(axesVisible=bool(visible),
+ boxVisible=self.boxVisible)
+
+ @property
+ def boxVisible(self):
+ """Whether bounding box is displayed or not (bool)"""
+ return self._boxWithAxes.visible or self._box.visible
+
+ @boxVisible.setter
+ def boxVisible(self, visible):
+ self._updateBoxAndAxesVisibility(axesVisible=self.axesVisible,
+ boxVisible=bool(visible))
+
# Clipping Plane ##############################################################
diff --git a/silx/gui/plot3d/scene/test/test_utils.py b/silx/gui/plot3d/scene/test/test_utils.py
index 65c2407..4a2d515 100644
--- a/silx/gui/plot3d/scene/test/test_utils.py
+++ b/silx/gui/plot3d/scene/test/test_utils.py
@@ -27,11 +27,11 @@ from __future__ import absolute_import, division, unicode_literals
__authors__ = ["T. Vincent"]
__license__ = "MIT"
-__date__ = "25/07/2016"
+__date__ = "17/01/2018"
import unittest
-from silx.test.utils import ParametricTestCase
+from silx.utils.testutils import ParametricTestCase
import numpy
diff --git a/silx/gui/plot3d/scene/transform.py b/silx/gui/plot3d/scene/transform.py
index 71a6b74..4061e81 100644
--- a/silx/gui/plot3d/scene/transform.py
+++ b/silx/gui/plot3d/scene/transform.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2015-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2015-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -317,7 +317,7 @@ class Transform(event.Notifier):
def transformPoint(self, point, direct=True, perspectiveDivide=False):
"""Apply the transform to a point.
- If len(point) == 3, apply persective divide if possible.
+ If len(point) == 3, apply perspective divide if possible.
:param point: Array-like vector of 3 or 4 coordinates.
:param bool direct: Whether to apply the direct (True, the default)
@@ -757,8 +757,9 @@ class _Projection(Transform):
:param float near: Distance to the near plane.
:param float far: Distance to the far plane.
:param bool checkDepthExtent: Toggle checks near > 0 and far > near.
- :param size: Viewport's size used to compute the aspect ratio.
- :type size: 2-tuple of float (width, height).
+ :param size:
+ Viewport's size used to compute the aspect ratio (width, height).
+ :type size: 2-tuple of float
"""
def __init__(self, near, far, checkDepthExtent=False, size=(1., 1.)):
@@ -833,8 +834,9 @@ class Orthographic(_Projection):
:param float top: Coord of the top clipping plane.
:param float near: Distance to the near plane.
:param float far: Distance to the far plane.
- :param size: Viewport's size used to compute the aspect ratio.
- :type size: 2-tuple of float (width, height).
+ :param size:
+ Viewport's size used to compute the aspect ratio (width, height).
+ :type size: 2-tuple of float
"""
def __init__(self, left=0., right=1., bottom=1., top=0., near=-1., far=1.,
@@ -923,8 +925,9 @@ class Ortho2DWidget(_Projection):
:param float near: Z coordinate of the near clipping plane.
:param float far: Z coordinante of the far clipping plane.
- :param size: Viewport's size used to compute the aspect ratio.
- :type size: 2-tuple of float (width, height).
+ :param size:
+ Viewport's size used to compute the aspect ratio (width, height).
+ :type size: 2-tuple of float
"""
def __init__(self, near=-1., far=1., size=(1., 1.)):
@@ -942,8 +945,9 @@ class Perspective(_Projection):
:param float fovy: Vertical field-of-view in degrees.
:param float near: The near clipping plane Z coord (stricly positive).
:param float far: The far clipping plane Z coord (> near).
- :param size: Viewport's size used to compute the aspect ratio.
- :type size: 2-tuple of float (width, height).
+ :param size:
+ Viewport's size used to compute the aspect ratio (width, height).
+ :type size: 2-tuple of float
"""
def __init__(self, fovy=90., near=0.1, far=1., size=(1., 1.)):
diff --git a/silx/gui/plot3d/scene/utils.py b/silx/gui/plot3d/scene/utils.py
index 930a087..04abd04 100644
--- a/silx/gui/plot3d/scene/utils.py
+++ b/silx/gui/plot3d/scene/utils.py
@@ -184,6 +184,32 @@ def unindexArrays(mode, indices, *arrays):
return tuple(numpy.ascontiguousarray(data[indices]) for data in arrays)
+def triangleStripToTriangles(strip):
+ """Convert a triangle strip to a set of triangles.
+
+ The order of the corners is inverted for odd triangles.
+
+ :param numpy.ndarray strip:
+ Array of triangle corners of shape (N, 3).
+ N must be at least 3.
+ :return: Equivalent triangles corner as an array of shape (N, 3, 3)
+ :rtype: numpy.ndarray
+ """
+ strip = numpy.array(strip).reshape(-1, 3)
+ assert len(strip) >= 3
+
+ triangles = numpy.empty((len(strip) - 2, 3, 3), dtype=strip.dtype)
+ triangles[0::2, 0] = strip[0:-2:2]
+ triangles[0::2, 1] = strip[1:-1:2]
+ triangles[0::2, 2] = strip[2::2]
+
+ triangles[1::2, 0] = strip[3::2]
+ triangles[1::2, 1] = strip[2:-1:2]
+ triangles[1::2, 2] = strip[1:-2:2]
+
+ return triangles
+
+
def trianglesNormal(positions):
"""Return normal for each triangle.
@@ -514,3 +540,19 @@ class Plane(event.Notifier):
def move(self, step):
"""Move the plane of step along the normal."""
self.point += step * self.normal
+
+ def segmentIntersection(self, s0, s1):
+ """Compute the plane intersection with segment [s0, s1].
+
+ :param s0: First end of the segment
+ :type s0: 1D numpy.ndarray-like of length 3
+ :param s1: Second end of the segment
+ :type s1: 1D numpy.ndarray-like of length 3
+ :return: The intersection points. The number of points goes
+ from 0 (no intersection) to 2 (segment in the plane)
+ :rtype: list of 1D numpy.ndarray
+ """
+ if not self.isPlane:
+ return []
+ else:
+ return segmentPlaneIntersect(s0, s1, self.normal, self.point)
diff --git a/silx/gui/plot3d/scene/viewport.py b/silx/gui/plot3d/scene/viewport.py
index 72e1ea3..0cacbf0 100644
--- a/silx/gui/plot3d/scene/viewport.py
+++ b/silx/gui/plot3d/scene/viewport.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2015-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2015-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -198,12 +198,17 @@ class Viewport(event.Notifier):
@property
def background(self):
- """Background color of the viewport (4-tuple of float in [0, 1]"""
+ """Viewport's background color (4-tuple of float in [0, 1] or None)
+
+ The background color is used to clear to viewport.
+ If None, the viewport is not cleared
+ """
return self._background
@background.setter
def background(self, color):
- color = rgba(color)
+ if color is not None:
+ color = rgba(color)
if self._background != color:
self._background = color
self._changed()
@@ -295,12 +300,16 @@ class Viewport(event.Notifier):
gl.glHint(gl.GL_LINE_SMOOTH_HINT, gl.GL_NICEST)
gl.glEnable(gl.GL_LINE_SMOOTH)
- gl.glClearColor(*self.background)
+ if self.background is None:
+ gl.glClear(gl.GL_STENCIL_BUFFER_BIT |
+ gl.GL_DEPTH_BUFFER_BIT)
+ else:
+ gl.glClearColor(*self.background)
- # Prepare OpenGL
- gl.glClear(gl.GL_COLOR_BUFFER_BIT |
- gl.GL_STENCIL_BUFFER_BIT |
- gl.GL_DEPTH_BUFFER_BIT)
+ # Prepare OpenGL
+ gl.glClear(gl.GL_COLOR_BUFFER_BIT |
+ gl.GL_STENCIL_BUFFER_BIT |
+ gl.GL_DEPTH_BUFFER_BIT)
ctx = RenderContext(self, glContext)
self.scene.render(ctx)
@@ -454,11 +463,13 @@ class Viewport(event.Notifier):
winy = oy + height * 0.5 * (1. - ndcY)
return winx, winy
- def _pickNdcZGL(self, x, y):
+ def _pickNdcZGL(self, x, y, offset=0):
"""Retrieve depth from depth buffer and return corresponding NDC Z.
:param int x: In pixels in window coordinates, origin left.
:param int y: In pixels in window coordinates, origin top.
+ :param int offset: Number of pixels to look at around the given pixel
+
:return: Normalize device Z coordinate of depth in [-1, 1]
or None if outside viewport.
:rtype: float or None
@@ -476,10 +487,34 @@ class Viewport(event.Notifier):
# Get depth from depth buffer in [0., 1.]
# Bind used framebuffer to get depth
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.framebuffer)
- depth = gl.glReadPixels(
- x, y, 1, 1, gl.GL_DEPTH_COMPONENT, gl.GL_FLOAT)[0]
+
+ if offset == 0: # Fast path
+ # glReadPixels is not GL|ES friendly
+ depth = gl.glReadPixels(
+ x, y, 1, 1, gl.GL_DEPTH_COMPONENT, gl.GL_FLOAT)[0]
+ else:
+ offset = abs(int(offset))
+ size = 2*offset + 1
+ depthPatch = gl.glReadPixels(
+ x - offset, y - offset,
+ size, size,
+ gl.GL_DEPTH_COMPONENT, gl.GL_FLOAT)
+ depthPatch = depthPatch.ravel() # Work in 1D
+
+ # TODO cache sortedIndices to avoid computing it each time
+ # Compute distance of each pixels to the center of the patch
+ offsetToCenter = numpy.arange(- offset, offset + 1, dtype=numpy.float32) ** 2
+ sqDistToCenter = numpy.add.outer(offsetToCenter, offsetToCenter)
+
+ # Use distance to center to sort values from the patch
+ sortedIndices = numpy.argsort(sqDistToCenter.ravel())
+ sortedValues = depthPatch[sortedIndices]
+
+ # Take first depth that is not 1 in the sorted values
+ hits = sortedValues[sortedValues != 1.]
+ depth = 1. if len(hits) == 0 else hits[0]
+
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
- # This is not GL|ES friendly
# Z in NDC in [-1., 1.]
return float(depth) * 2. - 1.
diff --git a/silx/gui/plot3d/scene/window.py b/silx/gui/plot3d/scene/window.py
index 3c63c7a..baa76a2 100644
--- a/silx/gui/plot3d/scene/window.py
+++ b/silx/gui/plot3d/scene/window.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2015-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2015-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -124,18 +124,26 @@ class ContextGL2(Context):
# programs
- def prog(self, vertexShaderSrc, fragmentShaderSrc):
+ def prog(self, vertexShaderSrc, fragmentShaderSrc, attrib0='position'):
"""Cache program within context.
WARNING: No clean-up.
+
+ :param str vertexShaderSrc: Vertex shader source code
+ :param str fragmentShaderSrc: Fragment shader source code
+ :param str attrib0:
+ Attribute's name to bind to position 0 (default: 'position').
+ On some platform, this attribute MUST be active and with an
+ array attached to it in order for the rendering to occur....
"""
assert self.isCurrent
- key = vertexShaderSrc, fragmentShaderSrc
- prog = self._programs.get(key, None)
- if prog is None:
- prog = _glutils.Program(vertexShaderSrc, fragmentShaderSrc)
- self._programs[key] = prog
- return prog
+ key = vertexShaderSrc, fragmentShaderSrc, attrib0
+ program = self._programs.get(key, None)
+ if program is None:
+ program = _glutils.Program(
+ vertexShaderSrc, fragmentShaderSrc, attrib0=attrib0)
+ self._programs[key] = program
+ return program
# VBOs
@@ -318,7 +326,8 @@ class Window(event.Notifier):
"""Returns the raster of the scene as an RGB numpy array
:returns: OpenGL scene RGB bitmap
- :rtype: numpy.ndarray of uint8 of dimension (height, width, 3)
+ as an array of dimension (height, width, 3)
+ :rtype: numpy.ndarray of uint8
"""
height, width = self.shape
image = numpy.empty((height, width, 3), dtype=numpy.uint8)