summaryrefslogtreecommitdiff
path: root/silx/gui/plot3d/scene/primitives.py
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot3d/scene/primitives.py')
-rw-r--r--silx/gui/plot3d/scene/primitives.py1764
1 files changed, 1764 insertions, 0 deletions
diff --git a/silx/gui/plot3d/scene/primitives.py b/silx/gui/plot3d/scene/primitives.py
new file mode 100644
index 0000000..ca2616a
--- /dev/null
+++ b/silx/gui/plot3d/scene/primitives.py
@@ -0,0 +1,1764 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2015-2017 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
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+
+from __future__ import absolute_import, division, unicode_literals
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "25/07/2016"
+
+
+import collections
+import ctypes
+from functools import reduce
+import logging
+import string
+
+import numpy
+
+from silx.gui.plot.Colors import rgba
+
+from ... import _glutils
+from ..._glutils import gl
+
+from . import event
+from . import core
+from . import transform
+from . import utils
+
+_logger = logging.getLogger(__name__)
+
+
+# Geometry ####################################################################
+
+class Geometry(core.Elem):
+ """Set of vertices with normals and colors.
+
+ :param str mode: OpenGL drawing mode:
+ 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 attributes: Provide list of attributes as extra parameters.
+ """
+
+ _ATTR_INFO = {
+ 'position': {'dims': (1, 2), 'lastDim': (2, 3, 4)},
+ 'normal': {'dims': (1, 2), 'lastDim': (3,)},
+ 'color': {'dims': (1, 2), 'lastDim': (3, 4)},
+ }
+
+ _MODE_CHECKS = { # Min, Modulo
+ 'lines': (2, 2), 'line_strip': (2, 0), 'loop': (2, 0),
+ 'points': (1, 0),
+ 'triangles': (3, 3), 'triangle_strip': (3, 0), 'fan': (3, 0)
+ }
+
+ _MODES = {
+ 'lines': gl.GL_LINES,
+ 'line_strip': gl.GL_LINE_STRIP,
+ 'loop': gl.GL_LINE_LOOP,
+
+ 'points': gl.GL_POINTS,
+
+ 'triangles': gl.GL_TRIANGLES,
+ 'triangle_strip': gl.GL_TRIANGLE_STRIP,
+ 'fan': gl.GL_TRIANGLE_FAN
+ }
+
+ _LINE_MODES = 'lines', 'line_strip', 'loop'
+
+ _TRIANGLE_MODES = 'triangles', 'triangle_strip', 'fan'
+
+ def __init__(self, mode, indices=None, copy=True, **attributes):
+ super(Geometry, self).__init__()
+
+ self._vbos = {} # Store current vbos
+ self._unsyncAttributes = [] # Store attributes to copy to vbos
+ self.__bounds = None # Cache object's bounds
+
+ assert mode in self._MODES
+ self._mode = mode
+
+ # Set attributes
+ self._attributes = {}
+ for name, data in attributes.items():
+ self.setAttribute(name, data, copy=copy)
+
+ # Set indices
+ self._indices = None
+ self.setIndices(indices, copy=copy)
+
+ # More consistency checks
+ mincheck, modulocheck = self._MODE_CHECKS[self._mode]
+ if self._indices is not None:
+ nbvertices = len(self._indices)
+ else:
+ nbvertices = self.nbVertices
+ assert nbvertices >= mincheck
+ if modulocheck != 0:
+ assert (nbvertices % modulocheck) == 0
+
+ @staticmethod
+ def _glReadyArray(array, copy=True):
+ """Making a contiguous array, checking float types.
+
+ :param iterable array: array-like data to prepare for attribute
+ :param bool copy: True to make a copy of the array, False to use as is
+ """
+ # Convert single value (int, float, numpy types) to tuple
+ if not isinstance(array, collections.Iterable):
+ array = (array, )
+
+ # 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':
+ dtype = numpy.float32
+
+ return numpy.array(array, dtype=dtype, order='C', copy=copy)
+
+ @property
+ def nbVertices(self):
+ """Returns the number of vertices of current attributes.
+
+ It returns None if there is no attributes.
+ """
+ for array in self._attributes.values():
+ if len(array.shape) == 2:
+ return len(array)
+ return None
+
+ def setAttribute(self, name, array, copy=True):
+ """Set attribute with provided array.
+
+ :param str name: The name of the attribute
+ :param array: Array-like attribute data or None to remove attribute
+ :param bool copy: True (default) to copy the data, False to use as is
+ """
+ # This triggers associated GL resources to be garbage collected
+ self._vbos.pop(name, None)
+
+ if array is None:
+ self._attributes.pop(name, None)
+
+ else:
+ array = self._glReadyArray(array, copy=copy)
+
+ if name not in self._ATTR_INFO:
+ _logger.info('Not checking attibute %s dimensions', name)
+ else:
+ checks = self._ATTR_INFO[name]
+
+ if (len(array.shape) == 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.shape[-1] in checks['lastDim'], "Attr %s" % name
+
+ # Check length against another attribute array
+ # Causes problems when updating
+ # nbVertices = self.nbVertices
+ # if len(array.shape) == 2 and nbVertices is not None:
+ # assert len(array) == nbVertices
+
+ self._attributes[name] = array
+ if len(array.shape) == 2: # Store this in a VBO
+ self._unsyncAttributes.append(name)
+
+ if name == 'position': # Reset bounds
+ self.__bounds = None
+
+ self.notify()
+
+ def getAttribute(self, name, copy=True):
+ """Returns the numpy.ndarray corresponding to the name attribute.
+
+ :param str name: The name of the attribute to get.
+ :param bool copy: True to get a copy (default),
+ False to get internal array (DO NOT MODIFY)
+ :return: The corresponding array or None if no corresponding attribute.
+ :rtype: numpy.ndarray
+ """
+ attr = self._attributes.get(name, None)
+ return None if attr is None else numpy.array(attr, copy=copy)
+
+ def useAttribute(self, program, name=None):
+ """Enable and bind attribute(s) for a specific program.
+
+ This MUST be called with OpenGL context active and after prepareGL2
+ has been called.
+
+ :param GLProgram program: The program for which to set the attributes
+ :param str name: The attribute name to set or None to set then all
+ """
+ if name is None:
+ for name in program.attributes:
+ self.useAttribute(program, name)
+
+ else:
+ attribute = program.attributes.get(name)
+ if attribute is None:
+ return
+
+ vboattrib = self._vbos.get(name)
+ if vboattrib is not None:
+ gl.glEnableVertexAttribArray(attribute)
+ vboattrib.setVertexAttrib(attribute)
+
+ elif name not in self._attributes:
+ gl.glDisableVertexAttribArray(attribute)
+
+ else:
+ array = self._attributes[name]
+ assert array is not None
+
+ if len(array.shape) == 1:
+ assert len(array) in (1, 2, 3, 4)
+ gl.glDisableVertexAttribArray(attribute)
+ _glVertexAttribFunc = getattr(
+ _glutils.gl, 'glVertexAttrib{}f'.format(len(array)))
+ _glVertexAttribFunc(attribute, *array)
+ else:
+ # TODO As is this is a never event, remove?
+ gl.glEnableVertexAttribArray(attribute)
+ gl.glVertexAttribPointer(
+ attribute,
+ array.shape[-1],
+ _glutils.numpyToGLType(array.dtype),
+ gl.GL_FALSE,
+ 0,
+ array)
+
+ def setIndices(self, indices, copy=True):
+ """Set the primitive indices to use.
+
+ :param indices: Array-like of uint primitive indices or None to unset
+ :param bool copy: True (default) to copy the data, False to use as is
+ """
+ # Trigger garbage collection of previous indices VBO if any
+ self._vbos.pop('__indices__', None)
+
+ if indices is None:
+ self._indices = None
+ else:
+ indices = self._glReadyArray(indices, copy=copy).ravel()
+ assert indices.dtype.name in ('uint8', 'uint16', 'uint32')
+ if _logger.getEffectiveLevel() <= logging.DEBUG:
+ # This might be a costy check
+ assert indices.max() < self.nbVertices
+ self._indices = indices
+
+ def getIndices(self, copy=True):
+ """Returns the numpy.ndarray corresponding to the indices.
+
+ :param bool copy: True to get a copy (default),
+ False to get internal array (DO NOT MODIFY)
+ :return: The primitive indices array or None if not set.
+ :rtype: numpy.ndarray or None
+ """
+ if self._indices is None:
+ return None
+ else:
+ return numpy.array(self._indices, copy=copy)
+
+ def _bounds(self, dataBounds=False):
+ if self.__bounds is None:
+ 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]] = positions.min(axis=0)[:3]
+ self.__bounds[1, :positions.shape[1]] = positions.max(axis=0)[:3]
+ return self.__bounds.copy()
+
+ def prepareGL2(self, ctx):
+ # TODO manage _vbo and multiple GL context + allow to share them !
+ # TODO make one or multiple VBO depending on len(vertices),
+ # TODO use a general common VBO for small amount of data
+ for name in self._unsyncAttributes:
+ array = self._attributes[name]
+ self._vbos[name] = ctx.glCtx.makeVboAttrib(array)
+ self._unsyncAttributes = []
+
+ if self._indices is not None and '__indices__' not in self._vbos:
+ vbo = ctx.glCtx.makeVbo(self._indices,
+ usage=gl.GL_STATIC_DRAW,
+ target=gl.GL_ELEMENT_ARRAY_BUFFER)
+ self._vbos['__indices__'] = vbo
+
+ def _draw(self, program=None, nbVertices=None):
+ """Perform OpenGL draw calls.
+
+ :param GLProgram program:
+ If not None, call :meth:`useAttribute` for this program.
+ :param int nbVertices:
+ The number of vertices to render or None to render all vertices.
+ """
+ if program is not None:
+ self.useAttribute(program)
+
+ if self._indices is None:
+ if nbVertices is None:
+ nbVertices = self.nbVertices
+ gl.glDrawArrays(self._MODES[self._mode], 0, nbVertices)
+ else:
+ if nbVertices is None:
+ nbVertices = self._indices.size
+ with self._vbos['__indices__']:
+ gl.glDrawElements(self._MODES[self._mode],
+ nbVertices,
+ _glutils.numpyToGLType(self._indices.dtype),
+ ctypes.c_void_p(0))
+
+
+# Lines #######################################################################
+
+class Lines(Geometry):
+ """A set of segments"""
+ _shaders = ("""
+ attribute vec3 position;
+ attribute vec3 normal;
+ attribute vec4 color;
+
+ uniform mat4 matrix;
+ uniform mat4 transformMat;
+
+ varying vec4 vCameraPosition;
+ varying vec3 vPosition;
+ varying vec3 vNormal;
+ varying vec4 vColor;
+
+ void main(void)
+ {
+ gl_Position = matrix * vec4(position, 1.0);
+ vCameraPosition = transformMat * vec4(position, 1.0);
+ vPosition = position;
+ vNormal = normal;
+ vColor = color;
+ }
+ """,
+ string.Template("""
+ varying vec4 vCameraPosition;
+ varying vec3 vPosition;
+ varying vec3 vNormal;
+ varying vec4 vColor;
+
+ $clippingDecl
+ $lightingFunction
+
+ void main(void)
+ {
+ $clippingCall(vCameraPosition);
+ gl_FragColor = $lightingCall(vColor, vPosition, vNormal);
+ }
+ """))
+
+ def __init__(self, positions, normals=None, colors=(1., 1., 1., 1.),
+ indices=None, mode='lines', width=1.):
+ if mode == 'strip':
+ mode = 'line_strip'
+ assert mode in self._LINE_MODES
+
+ self._width = width
+ self._smooth = True
+
+ super(Lines, self).__init__(mode, indices,
+ position=positions,
+ normal=normals,
+ color=colors)
+
+ width = event.notifyProperty('_width', converter=float,
+ doc="Width of the line in pixels.")
+
+ smooth = event.notifyProperty(
+ '_smooth',
+ converter=bool,
+ doc="Smooth line rendering enabled (bool, default: True)")
+
+ def renderGL2(self, ctx):
+ # Prepare program
+ isnormals = 'normal' in self._attributes
+ if isnormals:
+ fraglightfunction = ctx.viewport.light.fragmentDef
+ else:
+ fraglightfunction = ctx.viewport.light.fragmentShaderFunctionNoop
+
+ fragment = self._shaders[1].substitute(
+ clippingDecl=ctx.clipper.fragDecl,
+ clippingCall=ctx.clipper.fragCall,
+ lightingFunction=fraglightfunction,
+ lightingCall=ctx.viewport.light.fragmentCall)
+ prog = ctx.glCtx.prog(self._shaders[0], fragment)
+ prog.use()
+
+ if isnormals:
+ ctx.viewport.light.setupProgram(ctx, prog)
+
+ gl.glLineWidth(self.width)
+
+ prog.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
+ prog.setUniformMatrix('transformMat',
+ ctx.objectToCamera.matrix,
+ safe=True)
+
+ ctx.clipper.setupProgram(ctx, prog)
+
+ with gl.enabled(gl.GL_LINE_SMOOTH, self._smooth):
+ self._draw(prog)
+
+
+class DashedLines(Lines):
+ """Set of dashed lines
+
+ This MUST be defined as a set of lines (no strip or loop).
+ """
+
+ _shaders = ("""
+ attribute vec3 position;
+ attribute vec3 origin;
+ attribute vec3 normal;
+ attribute vec4 color;
+
+ uniform mat4 matrix;
+ uniform mat4 transformMat;
+ uniform vec2 viewportSize; /* Width, height of the viewport */
+
+ varying vec4 vCameraPosition;
+ varying vec3 vPosition;
+ varying vec3 vNormal;
+ varying vec4 vColor;
+ varying vec2 vOriginFragCoord;
+
+ void main(void)
+ {
+ gl_Position = matrix * vec4(position, 1.0);
+ vCameraPosition = transformMat * vec4(position, 1.0);
+ vPosition = position;
+ vNormal = normal;
+ vColor = color;
+
+ vec4 clipOrigin = matrix * vec4(origin, 1.0);
+ vec4 ndcOrigin = clipOrigin / clipOrigin.w; /* Perspective divide */
+ /* Convert to same frame as gl_FragCoord: lower-left, pixel center at 0.5, 0.5 */
+ vOriginFragCoord = (ndcOrigin.xy + vec2(1.0, 1.0)) * 0.5 * viewportSize + vec2(0.5, 0.5);
+ }
+ """, # noqa
+ string.Template("""
+ varying vec4 vCameraPosition;
+ varying vec3 vPosition;
+ varying vec3 vNormal;
+ varying vec4 vColor;
+ varying vec2 vOriginFragCoord;
+
+ uniform vec2 dash;
+
+ $clippingDecl
+ $lightingFunction
+
+ void main(void)
+ {
+ /* Discard off dash fragments */
+ float lineDist = distance(vOriginFragCoord, gl_FragCoord.xy);
+ if (mod(lineDist, dash.x + dash.y) > dash.x) {
+ discard;
+ }
+ $clippingCall(vCameraPosition);
+ gl_FragColor = $lightingCall(vColor, vPosition, vNormal);
+ }
+ """))
+
+ def __init__(self, positions, colors=(1., 1., 1., 1.),
+ indices=None, width=1.):
+ self._dash = 1, 0
+ super(DashedLines, self).__init__(positions=positions,
+ colors=colors,
+ indices=indices,
+ mode='lines',
+ width=width)
+
+ @property
+ def dash(self):
+ """Dash of the line as a 2-tuple of lengths in pixels: (on, off)"""
+ return self._dash
+
+ @dash.setter
+ def dash(self, dash):
+ dash = float(dash[0]), float(dash[1])
+ if dash != self._dash:
+ self._dash = dash
+ self.notify()
+
+ def getPositions(self, copy=True):
+ """Get coordinates of lines.
+
+ :param bool copy: True to get a copy, False otherwise
+ :returns: Coordinates of lines
+ :rtype: numpy.ndarray of float32 of shape (N, 2, Ndim)
+ """
+ return self.getAttribute('position', copy=copy)
+
+ def setPositions(self, positions, copy=True):
+ """Set line coordinates.
+
+ :param positions: Array of line coordinates
+ :param bool copy: True to copy input array, False to use as is
+ """
+ self.setAttribute('position', positions, copy=copy)
+ # Update line origins from given positions
+ origins = numpy.array(positions, copy=True, order='C')
+ origins[1::2] = origins[::2]
+ self.setAttribute('origin', origins, copy=False)
+
+ def renderGL2(self, context):
+ # Prepare program
+ isnormals = 'normal' in self._attributes
+ if isnormals:
+ fraglightfunction = context.viewport.light.fragmentDef
+ else:
+ fraglightfunction = \
+ context.viewport.light.fragmentShaderFunctionNoop
+
+ fragment = self._shaders[1].substitute(
+ clippingDecl=context.clipper.fragDecl,
+ clippingCall=context.clipper.fragCall,
+ lightingFunction=fraglightfunction,
+ lightingCall=context.viewport.light.fragmentCall)
+ program = context.glCtx.prog(self._shaders[0], fragment)
+ program.use()
+
+ if isnormals:
+ context.viewport.light.setupProgram(context, program)
+
+ gl.glLineWidth(self.width)
+
+ program.setUniformMatrix('matrix', context.objectToNDC.matrix)
+ program.setUniformMatrix('transformMat',
+ context.objectToCamera.matrix,
+ safe=True)
+
+ gl.glUniform2f(
+ program.uniforms['viewportSize'], *context.viewport.size)
+ gl.glUniform2f(program.uniforms['dash'], *self.dash)
+
+ context.clipper.setupProgram(context, program)
+
+ self._draw(program)
+
+
+class Box(core.PrivateGroup):
+ """Rectangular box"""
+
+ _lineIndices = numpy.array((
+ (0, 1), (1, 2), (2, 3), (3, 0), # Lines with z=0
+ (0, 4), (1, 5), (2, 6), (3, 7), # Lines from z=0 to z=1
+ (4, 5), (5, 6), (6, 7), (7, 4)), # Lines with z=1
+ dtype=numpy.uint8)
+
+ _faceIndices = numpy.array(
+ (0, 3, 1, 2, 5, 6, 4, 7, 7, 6, 6, 2, 7, 3, 4, 0, 5, 1),
+ dtype=numpy.uint8)
+
+ _vertices = numpy.array((
+ # Corners with z=0
+ (0., 0., 0.), (1., 0., 0.), (1., 1., 0.), (0., 1., 0.),
+ # Corners with z=1
+ (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.)):
+ super(Box, self).__init__()
+
+ self._fill = Mesh3D(self._vertices,
+ colors=rgba(fill),
+ mode='triangle_strip',
+ indices=self._faceIndices)
+ self._fill.visible = self.fillColor[-1] != 0.
+
+ self._stroke = Lines(self._vertices,
+ indices=self._lineIndices,
+ colors=rgba(stroke),
+ mode='lines')
+ self._stroke.visible = self.strokeColor[-1] != 0.
+ self.strokeWidth = 1.
+
+ self._children = [self._stroke, self._fill]
+
+ self._size = None
+ self.size = size
+
+ @property
+ def size(self):
+ """Size of the box (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._fill.setAttribute(
+ 'position',
+ self._vertices * numpy.array(size, dtype=numpy.float32))
+ self._stroke.setAttribute(
+ 'position',
+ self._vertices * numpy.array(size, dtype=numpy.float32))
+ self.notify()
+
+ @property
+ def strokeSmooth(self):
+ """True to draw smooth stroke, False otherwise"""
+ return self._stroke.smooth
+
+ @strokeSmooth.setter
+ def strokeSmooth(self, smooth):
+ smooth = bool(smooth)
+ if smooth != self._stroke.smooth:
+ self._stroke.smooth = smooth
+ self.notify()
+
+ @property
+ def strokeWidth(self):
+ """Width of the stroke (float)"""
+ return self._stroke.width
+
+ @strokeWidth.setter
+ def strokeWidth(self, width):
+ width = float(width)
+ if width != self.strokeWidth:
+ self._stroke.width = width
+ self.notify()
+
+ @property
+ def strokeColor(self):
+ """RGBA color of the box lines (4-tuple of float in [0, 1])"""
+ return tuple(self._stroke.getAttribute('color', copy=False))
+
+ @strokeColor.setter
+ def strokeColor(self, color):
+ color = rgba(color)
+ if color != self.strokeColor:
+ self._stroke.setAttribute('color', color)
+ # Fully transparent = hidden
+ self._stroke.visible = color[-1] != 0.
+ self.notify()
+
+ @property
+ def fillColor(self):
+ """RGBA color of the box faces (4-tuple of float in [0, 1])"""
+ return tuple(self._fill.getAttribute('color', copy=False))
+
+ @fillColor.setter
+ def fillColor(self, color):
+ color = rgba(color)
+ if color != self.fillColor:
+ self._fill.setAttribute('color', color)
+ # Fully transparent = hidden
+ self._fill.visible = color[-1] != 0.
+ self.notify()
+
+ @property
+ def fillCulling(self):
+ return self._fill.culling
+
+ @fillCulling.setter
+ def fillCulling(self, culling):
+ self._fill.culling = culling
+
+
+class Axes(Lines):
+ """3D RGB orthogonal axes"""
+ _vertices = numpy.array(((0., 0., 0.), (1., 0., 0.),
+ (0., 0., 0.), (0., 1., 0.),
+ (0., 0., 0.), (0., 0., 1.)),
+ dtype=numpy.float32)
+
+ _colors = numpy.array(((255, 0, 0, 255), (255, 0, 0, 255),
+ (0, 255, 0, 255), (0, 255, 0, 255),
+ (0, 0, 255, 255), (0, 0, 255, 255)),
+ dtype=numpy.uint8)
+
+ def __init__(self):
+ super(Axes, self).__init__(self._vertices,
+ colors=self._colors,
+ width=3.)
+
+
+class BoxWithAxes(Lines):
+ """Rectangular box with RGB OX, OY, OZ axes
+
+ :param color: RGBA color of the box
+ """
+
+ _vertices = numpy.array((
+ # Axes corners
+ (0., 0., 0.), (1., 0., 0.),
+ (0., 0., 0.), (0., 1., 0.),
+ (0., 0., 0.), (0., 0., 1.),
+ # Box corners with z=0
+ (1., 0., 0.), (1., 1., 0.), (0., 1., 0.),
+ # Box corners with z=1
+ (0., 0., 1.), (1., 0., 1.), (1., 1., 1.), (0., 1., 1.)),
+ dtype=numpy.float32)
+
+ _axesColors = numpy.array(((1., 0., 0., 1.), (1., 0., 0., 1.),
+ (0., 1., 0., 1.), (0., 1., 0., 1.),
+ (0., 0., 1., 1.), (0., 0., 1., 1.)),
+ dtype=numpy.float32)
+
+ _lineIndices = numpy.array((
+ (0, 1), (2, 3), (4, 5), # Axes lines
+ (6, 7), (7, 8), # Box lines with z=0
+ (6, 10), (7, 11), (8, 12), # Box lines from z=0 to z=1
+ (9, 10), (10, 11), (11, 12), (12, 9)), # Box lines with z=1
+ dtype=numpy.uint8)
+
+ def __init__(self, color=(1., 1., 1., 1.)):
+ self._color = (1., 1., 1., 1.)
+ colors = numpy.ones((len(self._vertices), 4), dtype=numpy.float32)
+ colors[:len(self._axesColors), :] = self._axesColors
+
+ super(BoxWithAxes, self).__init__(self._vertices,
+ indices=self._lineIndices,
+ colors=colors,
+ width=2.)
+ self.color = color
+
+ @property
+ def color(self):
+ """The RGBA color to use for the box: 4 float in [0, 1]"""
+ return self._color
+
+ @color.setter
+ def color(self, color):
+ color = rgba(color)
+ if color != self._color:
+ self._color = color
+ colors = numpy.empty((len(self._vertices), 4), dtype=numpy.float32)
+ colors[:len(self._axesColors), :] = self._axesColors
+ colors[len(self._axesColors):, :] = self._color
+ self.setAttribute('color', colors) # Do the notification
+
+
+class PlaneInGroup(core.PrivateGroup):
+ """A plane using its parent bounds to display a contour.
+
+ If plane is outside the bounds of its parent, it is not visible.
+
+ Cannot set the transform attribute of this primitive.
+ This primitive never has any bounds.
+ """
+ # TODO inherit from Lines directly?, make sure the plane remains visible?
+
+ def __init__(self, point=(0., 0., 0.), normal=(0., 0., 1.)):
+ super(PlaneInGroup, self).__init__()
+ self._cache = None, None # Store bounds, vertices
+ self._outline = None
+
+ self._color = None
+ self.color = 1., 1., 1., 1. # Set _color
+ self._width = 2.
+
+ self._plane = utils.Plane(point, normal)
+ self._plane.addListener(self._planeChanged)
+
+ def moveToCenter(self):
+ """Place the plane at the center of the data, not changing orientation.
+ """
+ if self.parent is not None:
+ bounds = self.parent.bounds(dataBounds=True)
+ if bounds is not None:
+ center = (bounds[0] + bounds[1]) / 2.
+ _logger.debug('Moving plane to center: %s', str(center))
+ self.plane.point = center
+
+ @property
+ def color(self):
+ """Plane outline color (array of 4 float in [0, 1])."""
+ return self._color.copy()
+
+ @color.setter
+ def color(self, color):
+ self._color = numpy.array(color, copy=True, dtype=numpy.float32)
+ if self._outline is not None:
+ self._outline.setAttribute('color', self._color)
+ self.notify() # This is OK as Lines are rebuild for each rendering
+
+ @property
+ def width(self):
+ """Width of the plane stroke in pixels"""
+ return self._width
+
+ @width.setter
+ def width(self, width):
+ self._width = float(width)
+ if self._outline is not None:
+ self._outline.width = self._width # Sync width
+
+ # Plane access
+
+ @property
+ def plane(self):
+ """The plane parameters in the frame of the object."""
+ return self._plane
+
+ def _planeChanged(self, source):
+ """Listener of plane changes: clear cache and notify listeners."""
+ self._cache = None, None
+ self.notify()
+
+ # Disable some scene features
+
+ @property
+ def transforms(self):
+ # Ready-only transforms to prevent using it
+ return self._transforms
+
+ def _bounds(self, dataBounds=False):
+ # This is bound less as it uses the bounds of its parent.
+ return None
+
+ @property
+ def contourVertices(self):
+ """The vertices of the contour of the plane/bounds intersection."""
+ parent = self.parent
+ if parent is None:
+ return None # No parent: no vertices
+
+ bounds = parent.bounds(dataBounds=True)
+ if bounds is None:
+ return None # No bounds: no vertices
+
+ # Check if cache is valid and return it
+ cachebounds, cachevertices = self._cache
+ if numpy.all(numpy.equal(bounds, cachebounds)):
+ return cachevertices
+
+ # Cache is not OK, rebuild it
+ boxvertices = bounds[0] + Box._vertices.copy()*(bounds[1] - bounds[0])
+ lineindices = Box._lineIndices
+ vertices = utils.boxPlaneIntersect(
+ boxvertices, lineindices, self.plane.normal, self.plane.point)
+
+ self._cache = bounds, vertices if len(vertices) != 0 else None
+
+ return self._cache[1]
+
+ @property
+ def center(self):
+ """The center of the plane/bounds intersection points."""
+ if not self.isValid:
+ return None
+ else:
+ return numpy.mean(self.contourVertices, axis=0)
+
+ @property
+ def isValid(self):
+ """True if a contour is defined, False otherwise."""
+ return self.plane.isPlane and self.contourVertices is not None
+
+ def prepareGL2(self, ctx):
+ if self.isValid:
+ if self._outline is None: # Init outline
+ self._outline = Lines(self.contourVertices,
+ mode='loop',
+ colors=self.color)
+ self._outline.width = self._width
+ self._children.append(self._outline)
+
+ # Update vertices, TODO only when necessary
+ self._outline.setAttribute('position', self.contourVertices)
+
+ super(PlaneInGroup, self).prepareGL2(ctx)
+
+ def renderGL2(self, ctx):
+ if self.isValid:
+ super(PlaneInGroup, self).renderGL2(ctx)
+
+
+# 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):
+ """A set of data points with an associated value and size."""
+ _shaders = ("""
+ #version 120
+
+ attribute vec3 position;
+ attribute float symbol;
+ attribute float value;
+ attribute float size;
+
+ uniform mat4 matrix;
+ uniform mat4 transformMat;
+
+ uniform vec2 valRange;
+
+ varying vec4 vCameraPosition;
+ varying float vSymbol;
+ varying float vNormValue;
+ varying float vSize;
+
+ void main(void)
+ {
+ vSymbol = symbol;
+
+ vNormValue = clamp((value - valRange.x) / (valRange.y - valRange.x),
+ 0.0, 1.0);
+
+ 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);
+
+ 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
+
+ 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);
+ }
+
+ /* 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
+
+ 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;
+ }
+
+ 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);
+ }
+ if (alpha == 0.0) {
+ discard;
+ }
+ gl_FragColor.a *= alpha;
+ }
+ """))
+
+ _ATTR_INFO = _POINTS_ATTR_INFO
+
+ # TODO Add colormap, light?
+
+ 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)
+
+ values = self._attributes['value']
+ self._minValue = values.min() if minValue is None else minValue
+ self._maxValue = values.max() if maxValue is None else maxValue
+
+ minValue = event.notifyProperty('_minValue')
+ maxValue = event.notifyProperty('_maxValue')
+
+ 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()
+
+ 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."""
+
+ _shaders = ("""
+ #version 120
+
+ attribute vec3 position;
+ attribute float symbol;
+ attribute vec4 color;
+ attribute float size;
+
+ uniform mat4 matrix;
+ uniform mat4 transformMat;
+
+ 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;
+
+ $clippingDecl;
+
+ /* Circle */
+ #define SYMBOL_CIRCLE 1.0
+
+ 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);
+ }
+
+ /* 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
+
+ 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;
+ }
+
+ void main(void)
+ {
+ $clippingCall(vCameraPosition);
+
+ 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;
+ }
+ """))
+
+ _ATTR_INFO = _POINTS_ATTR_INFO
+
+ 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 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()
+
+ 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)
+
+ self._draw(prog)
+
+
+class GridPoints(Geometry):
+ # GLSL 1.30 !
+ """Data points on a regular grid with an associated value and size."""
+ _shaders = ("""
+ #version 130
+
+ in float value;
+ in float size;
+
+ uniform ivec3 gridDims;
+ uniform mat4 matrix;
+ uniform mat4 transformMat;
+ uniform vec2 valRange;
+
+ out vec4 vCameraPosition;
+ out float vNormValue;
+
+ //ivec3 coordsFromIndex(int index, ivec3 shape)
+ //{
+ /*Assumes that data is stored as z-major, then y, contiguous on x
+ */
+ // int yxPlaneSize = shape.y * shape.x; /* nb of elem in 2d yx plane */
+ // int z = index / yxPlaneSize;
+ // int yxIndex = index - z * yxPlaneSize; /* index in 2d yx plane */
+ // int y = yxIndex / shape.x;
+ // int x = yxIndex - y * shape.x;
+ // return ivec3(x, y, z);
+ // }
+
+ ivec3 coordsFromIndex(int index, ivec3 shape)
+ {
+ /*Assumes that data is stored as x-major, then y, contiguous on z
+ */
+ int yzPlaneSize = shape.y * shape.z; /* nb of elem in 2d yz plane */
+ int x = index / yzPlaneSize;
+ int yzIndex = index - x * yzPlaneSize; /* index in 2d yz plane */
+ int y = yzIndex / shape.z;
+ int z = yzIndex - y * shape.z;
+ return ivec3(x, y, z);
+ }
+
+ void main(void)
+ {
+ vNormValue = clamp((value - valRange.x) / (valRange.y - valRange.x),
+ 0.0, 1.0);
+
+ bool isValueInRange = value >= valRange.x && value <= valRange.y;
+ if (isValueInRange) {
+ /* Retrieve 3D position from gridIndex */
+ vec3 coords = vec3(coordsFromIndex(gl_VertexID, gridDims));
+ vec3 position = coords / max(vec3(gridDims) - 1.0, 1.0);
+ gl_Position = matrix * vec4(position, 1.0);
+ vCameraPosition = transformMat * vec4(position, 1.0);
+ } else {
+ gl_Position = vec4(2.0, 0.0, 0.0, 1.0); /* Get clipped */
+ vCameraPosition = vec4(0.0, 0.0, 0.0, 0.0);
+ }
+
+ gl_PointSize = size;
+ }
+ """,
+ string.Template("""
+ #version 130
+
+ in vec4 vCameraPosition;
+ in float vNormValue;
+ out vec4 fragColor;
+
+ $clippingDecl
+
+ void main(void)
+ {
+ $clippingCall(vCameraPosition);
+
+ fragColor = vec4(0.5 * vNormValue + 0.5, 0.0, 0.0, 1.0);
+ }
+ """))
+
+ _ATTR_INFO = {
+ 'value': {'dims': (1, 2), 'lastDim': (1,)},
+ 'size': {'dims': (1, 2), 'lastDim': (1,)}
+ }
+
+ # TODO Add colormap, shape?
+ # TODO could also use a texture to store values
+
+ def __init__(self, values=0., shape=None, sizes=1., indices=None,
+ minValue=None, maxValue=None):
+ if isinstance(values, collections.Iterable):
+ values = numpy.array(values, copy=False)
+
+ # Test if gl_VertexID will overflow
+ assert values.size < numpy.iinfo(numpy.int32).max
+
+ self._shape = values.shape
+ values = values.ravel() # 1D to add as a 1D vertex attribute
+
+ else:
+ assert shape is not None
+ self._shape = tuple(shape)
+
+ assert len(self._shape) in (1, 2, 3)
+
+ super(GridPoints, self).__init__('points', indices,
+ value=values,
+ size=sizes)
+
+ data = self.getAttribute('value', copy=False)
+ self._minValue = data.min() if minValue is None else minValue
+ self._maxValue = data.max() if maxValue is None else maxValue
+
+ minValue = event.notifyProperty('_minValue')
+ maxValue = event.notifyProperty('_maxValue')
+
+ def _bounds(self, dataBounds=False):
+ # Get bounds from values shape
+ bounds = numpy.zeros((2, 3), dtype=numpy.float32)
+ bounds[1, :] = self._shape
+ bounds[1, :] -= 1
+ return bounds
+
+ 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()
+
+ 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.glUniform3i(prog.uniforms['gridDims'],
+ self._shape[2] if len(self._shape) == 3 else 1,
+ self._shape[1] if len(self._shape) >= 2 else 1,
+ self._shape[0])
+
+ gl.glUniform2f(prog.uniforms['valRange'], self.minValue, self.maxValue)
+
+ self._draw(prog, nbVertices=reduce(lambda a, b: a * b, self._shape))
+
+
+# Spheres #####################################################################
+
+class Spheres(Geometry):
+ """A set of spheres.
+
+ Spheres are rendered as circles using points.
+ This brings some limitations:
+ - Do not support non-uniform scaling.
+ - Assume the projection keeps ratio.
+ - Do not render distorion by perspective projection.
+ - If the sphere center is clipped, the whole sphere is not displayed.
+ """
+ # TODO check those links
+ # Accounting for perspective projection
+ # http://iquilezles.org/www/articles/sphereproj/sphereproj.htm
+
+ # Michael Mara and Morgan McGuire.
+ # 2D Polyhedral Bounds of a Clipped, Perspective-Projected 3D Sphere
+ # Journal of Computer Graphics Techniques, Vol. 2, No. 2, 2013.
+ # http://jcgt.org/published/0002/02/05/paper.pdf
+ # https://research.nvidia.com/publication/2d-polyhedral-bounds-clipped-perspective-projected-3d-sphere
+
+ # TODO some issues with small scaling and regular grid or due to sampling
+
+ _shaders = ("""
+ #version 120
+
+ attribute vec3 position;
+ attribute vec4 color;
+ attribute float radius;
+
+ uniform mat4 transformMat;
+ uniform mat4 projMat;
+ uniform vec2 screenSize;
+
+ varying vec4 vCameraPosition;
+ varying vec3 vPosition;
+ varying vec4 vColor;
+ varying float vViewDepth;
+ varying float vViewRadius;
+
+ void main(void)
+ {
+ vCameraPosition = transformMat * vec4(position, 1.0);
+ gl_Position = projMat * vCameraPosition;
+
+ vPosition = gl_Position.xyz / gl_Position.w;
+
+ /* From object space radius to view space diameter.
+ * Do not support non-uniform scaling */
+ vec4 viewSizeVector = transformMat * vec4(2.0 * radius, 0.0, 0.0, 0.0);
+ float viewSize = length(viewSizeVector.xyz);
+
+ /* Convert to pixel size at the xy center of the view space */
+ vec4 projSize = projMat * vec4(0.5 * viewSize, 0.0,
+ vCameraPosition.z, vCameraPosition.w);
+ gl_PointSize = max(1.0, screenSize[0] * projSize.x / projSize.w);
+
+ vColor = color;
+ vViewRadius = 0.5 * viewSize;
+ vViewDepth = vCameraPosition.z;
+ }
+ """,
+ string.Template("""
+ # version 120
+
+ uniform mat4 projMat;
+
+ varying vec4 vCameraPosition;
+ varying vec3 vPosition;
+ varying vec4 vColor;
+ varying float vViewDepth;
+ varying float vViewRadius;
+
+ $clippingDecl
+ $lightingFunction
+
+ void main(void)
+ {
+ $clippingCall(vCameraPosition);
+
+ /* Get normal from point coords */
+ vec3 normal;
+ normal.xy = 2.0 * gl_PointCoord - vec2(1.0);
+ normal.y *= -1.0; /*Invert y to match NDC orientation*/
+ float sqLength = dot(normal.xy, normal.xy);
+ if (sqLength > 1.0) { /* Length -> out of sphere */
+ discard;
+ }
+ normal.z = sqrt(1.0 - sqLength);
+
+ /*Lighting performed in NDC*/
+ /*TODO update this when lighting changed*/
+ //XXX vec3 position = vPosition + vViewRadius * normal;
+ gl_FragColor = $lightingCall(vColor, vPosition, normal);
+
+ /*Offset depth*/
+ float viewDepth = vViewDepth + vViewRadius * normal.z;
+ vec2 clipZW = viewDepth * projMat[2].zw + projMat[3].zw;
+ gl_FragDepth = 0.5 * (clipZW.x / clipZW.y) + 0.5;
+ }
+ """))
+
+ _ATTR_INFO = {
+ 'position': {'dims': (2, ), 'lastDim': (2, 3, 4)},
+ 'radius': {'dims': (1, 2), 'lastDim': (1, )},
+ 'color': {'dims': (1, 2), 'lastDim': (3, 4)},
+ }
+
+ def __init__(self, positions, radius=1., colors=(1., 1., 1., 1.)):
+ self.__bounds = None
+ super(Spheres, self).__init__('points', None,
+ position=positions,
+ radius=radius,
+ color=colors)
+
+ 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)
+ prog = ctx.glCtx.prog(self._shaders[0], fragment)
+ prog.use()
+
+ ctx.viewport.light.setupProgram(ctx, prog)
+
+ 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('projMat', ctx.projection.matrix)
+ prog.setUniformMatrix('transformMat',
+ ctx.objectToCamera.matrix,
+ safe=True)
+
+ ctx.clipper.setupProgram(ctx, prog)
+
+ gl.glUniform2f(prog.uniforms['screenSize'], *ctx.viewport.size)
+
+ self._draw(prog)
+
+ def _bounds(self, dataBounds=False):
+ if self.__bounds is None:
+ self.__bounds = numpy.zeros((2, 3), dtype=numpy.float32)
+ # Support vertex with to 2 to 4 coordinates
+ positions = self._attributes['position']
+ radius = self._attributes['radius']
+ self.__bounds[0, :positions.shape[1]] = \
+ (positions - radius).min(axis=0)[:3]
+ self.__bounds[1, :positions.shape[1]] = \
+ (positions + radius).max(axis=0)[:3]
+ return self.__bounds.copy()
+
+
+# Meshes ######################################################################
+
+class Mesh3D(Geometry):
+ """A conventional 3D mesh"""
+
+ _shaders = ("""
+ attribute vec3 position;
+ attribute vec3 normal;
+ attribute vec4 color;
+
+ uniform mat4 matrix;
+ uniform mat4 transformMat;
+ //uniform mat3 matrixInvTranspose;
+
+ varying vec4 vCameraPosition;
+ varying vec3 vPosition;
+ varying vec3 vNormal;
+ varying vec4 vColor;
+
+ void main(void)
+ {
+ vCameraPosition = transformMat * vec4(position, 1.0);
+ //vNormal = matrixInvTranspose * normalize(normal);
+ vPosition = position;
+ vNormal = normal;
+ vColor = color;
+ gl_Position = matrix * vec4(position, 1.0);
+ }
+ """,
+ string.Template("""
+ varying vec4 vCameraPosition;
+ varying vec3 vPosition;
+ varying vec3 vNormal;
+ varying vec4 vColor;
+
+ $clippingDecl
+ $lightingFunction
+
+ void main(void)
+ {
+ $clippingCall(vCameraPosition);
+
+ gl_FragColor = $lightingCall(vColor, vPosition, vNormal);
+ }
+ """))
+
+ def __init__(self,
+ positions,
+ colors,
+ normals=None,
+ mode='triangles',
+ indices=None):
+ assert mode in self._TRIANGLE_MODES
+ super(Mesh3D, self).__init__(mode, indices,
+ position=positions,
+ normal=normals,
+ color=colors)
+
+ self._culling = None
+
+ @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()
+
+ def renderGL2(self, ctx):
+ isnormals = 'normal' in self._attributes
+ if isnormals:
+ fragLightFunction = ctx.viewport.light.fragmentDef
+ else:
+ fragLightFunction = ctx.viewport.light.fragmentShaderFunctionNoop
+
+ fragment = self._shaders[1].substitute(
+ clippingDecl=ctx.clipper.fragDecl,
+ clippingCall=ctx.clipper.fragCall,
+ lightingFunction=fragLightFunction,
+ lightingCall=ctx.viewport.light.fragmentCall)
+ prog = ctx.glCtx.prog(self._shaders[0], fragment)
+ prog.use()
+
+ if isnormals:
+ ctx.viewport.light.setupProgram(ctx, prog)
+
+ 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)
+
+ prog.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
+ prog.setUniformMatrix('transformMat',
+ ctx.objectToCamera.matrix,
+ safe=True)
+
+ ctx.clipper.setupProgram(ctx, prog)
+
+ self._draw(prog)
+
+ if self.culling is not None:
+ gl.glDisable(gl.GL_CULL_FACE)
+
+
+# Group ######################################################################
+
+# TODO lighting, clipping as groups?
+# group composition?
+
+class GroupDepthOffset(core.Group):
+ """A group using 2-pass rendering and glDepthRange to avoid Z-fighting"""
+
+ def __init__(self, children=(), epsilon=None):
+ super(GroupDepthOffset, self).__init__(children)
+ self._epsilon = epsilon
+ self.isDepthRangeOn = True
+
+ def prepareGL2(self, ctx):
+ if self._epsilon is None:
+ depthbits = gl.glGetInteger(gl.GL_DEPTH_BITS)
+ self._epsilon = 1. / (1 << (depthbits - 1))
+
+ def renderGL2(self, ctx):
+ if self.isDepthRangeOn:
+ self._renderGL2WithDepthRange(ctx)
+ else:
+ super(GroupDepthOffset, self).renderGL2(ctx)
+
+ def _renderGL2WithDepthRange(self, ctx):
+ # gl.glDepthFunc(gl.GL_LESS)
+ with gl.enabled(gl.GL_CULL_FACE):
+ gl.glCullFace(gl.GL_BACK)
+ for child in self.children:
+ gl.glColorMask(
+ gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE)
+ gl.glDepthMask(gl.GL_TRUE)
+ gl.glDepthRange(self._epsilon, 1.)
+
+ child.render(ctx)
+
+ gl.glColorMask(
+ gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE)
+ gl.glDepthMask(gl.GL_FALSE)
+ gl.glDepthRange(0., 1. - self._epsilon)
+
+ child.render(ctx)
+
+ gl.glCullFace(gl.GL_FRONT)
+ for child in reversed(self.children):
+ gl.glColorMask(
+ gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE)
+ gl.glDepthMask(gl.GL_TRUE)
+ gl.glDepthRange(self._epsilon, 1.)
+
+ child.render(ctx)
+
+ gl.glColorMask(
+ gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE)
+ gl.glDepthMask(gl.GL_FALSE)
+ gl.glDepthRange(0., 1. - self._epsilon)
+
+ child.render(ctx)
+
+ gl.glDepthMask(gl.GL_TRUE)
+ gl.glDepthRange(0., 1.)
+ # gl.glDepthFunc(gl.GL_LEQUAL)
+ # TODO use epsilon for all rendering?
+ # TODO issue with picking in depth buffer!
+
+
+class GroupBBox(core.PrivateGroup):
+ """A group displaying a bounding box around the children."""
+
+ def __init__(self, children=(), color=(1., 1., 1., 1.)):
+ super(GroupBBox, self).__init__()
+ self._group = core.Group(children)
+
+ self._boxTransforms = transform.TransformList(
+ (transform.Translate(), transform.Scale()))
+
+ self._boxWithAxes = BoxWithAxes(color)
+ self._boxWithAxes.smooth = False
+ self._boxWithAxes.transforms = self._boxTransforms
+
+ self._children = [self._boxWithAxes, 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]]
+ else:
+ origin, scale = (0., 0., 0.), (1., 1., 1.)
+
+ self._boxTransforms[0].translation = origin
+ self._boxTransforms[1].scale = scale
+
+ def _bounds(self, dataBounds=False):
+ self._updateBoxAndAxes()
+ return super(GroupBBox, self)._bounds(dataBounds)
+
+ def prepareGL2(self, ctx):
+ self._updateBoxAndAxes()
+ super(GroupBBox, self).prepareGL2(ctx)
+
+ # Give access to _group children
+
+ @property
+ def children(self):
+ return self._group.children
+
+ @children.setter
+ def children(self, iterable):
+ self._group.children = iterable
+
+ # Give access to box color
+
+ @property
+ def color(self):
+ """The RGBA color to use for the box: 4 float in [0, 1]"""
+ return self._boxWithAxes.color
+
+ @color.setter
+ def color(self, color):
+ self._boxWithAxes.color = color
+
+
+# Clipping Plane ##############################################################
+
+class ClipPlane(PlaneInGroup):
+ """A clipping plane attached to a box"""
+
+ def renderGL2(self, ctx):
+ super(ClipPlane, self).renderGL2(ctx)
+
+ if self.visible:
+ # Set-up clipping plane for following brothers
+
+ # No need of perspective divide, no projection
+ point = ctx.objectToCamera.transformPoint(self.plane.point,
+ perspectiveDivide=False)
+ normal = ctx.objectToCamera.transformNormal(self.plane.normal)
+ ctx.setClipPlane(point, normal)
+
+ def postRender(self, ctx):
+ if self.visible:
+ # Disable clip planes
+ ctx.setClipPlane()