Source code for silx.gui.plot3d.scene.primitives

# 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 ####################################################################

[docs]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
[docs] 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()
[docs] 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)
[docs] 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)
[docs] 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
[docs] 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]] = \ numpy.nanmin(positions, axis=0)[:3] self.__bounds[1, :positions.shape[1]] = \ numpy.nanmax(positions, axis=0)[:3] self.__bounds[numpy.isnan(self.__bounds)] = 0. # Avoid NaNs 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 #######################################################################
[docs]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)
[docs]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()
[docs] 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)
[docs] 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)
[docs]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
[docs]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.)
[docs]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
[docs]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)
[docs] 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,)})
[docs]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)
[docs]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)
[docs]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 #####################################################################
[docs]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 ######################################################################
[docs]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?
[docs]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!
[docs]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 ##############################################################
[docs]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()