diff options
Diffstat (limited to 'silx/gui/plot3d/scene/cutplane.py')
-rw-r--r-- | silx/gui/plot3d/scene/cutplane.py | 374 |
1 files changed, 374 insertions, 0 deletions
diff --git a/silx/gui/plot3d/scene/cutplane.py b/silx/gui/plot3d/scene/cutplane.py new file mode 100644 index 0000000..79b4168 --- /dev/null +++ b/silx/gui/plot3d/scene/cutplane.py @@ -0,0 +1,374 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016-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. +# +# ###########################################################################*/ +"""A cut plane in a 3D texture: hackish implementation... +""" + +from __future__ import absolute_import, division, unicode_literals + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "05/10/2016" + +import string +import numpy + +from ... import _glutils +from ..._glutils import gl + +from .function import Colormap +from .primitives import Box, Geometry, PlaneInGroup +from . import transform, utils + + +class ColormapMesh3D(Geometry): + """A 3D mesh with color from a 3D texture.""" + + _shaders = (""" + attribute vec3 position; + attribute vec3 normal; + + uniform mat4 matrix; + uniform mat4 transformMat; + //uniform mat3 matrixInvTranspose; + uniform vec3 dataScale; + + varying vec4 vCameraPosition; + varying vec3 vPosition; + varying vec3 vNormal; + varying vec3 vTexCoords; + + void main(void) + { + vCameraPosition = transformMat * vec4(position, 1.0); + //vNormal = matrixInvTranspose * normalize(normal); + vPosition = position; + vTexCoords = dataScale * position; + vNormal = normal; + gl_Position = matrix * vec4(position, 1.0); + } + """, + string.Template(""" + varying vec4 vCameraPosition; + varying vec3 vPosition; + varying vec3 vNormal; + varying vec3 vTexCoords; + uniform sampler3D data; + uniform float alpha; + + $colormapDecl + + $clippingDecl + $lightingFunction + + void main(void) + { + float value = texture3D(data, vTexCoords).r; + vec4 color = $colormapCall(value); + color.a = alpha; + + $clippingCall(vCameraPosition); + + gl_FragColor = $lightingCall(color, vPosition, vNormal); + } + """)) + + def __init__(self, position, normal, data, copy=True, + mode='triangles', indices=None, colormap=None): + assert mode in self._TRIANGLE_MODES + data = numpy.array(data, copy=copy, order='C') + assert data.ndim == 3 + self._data = data + self._texture = None + self._update_texture = True + self._update_texture_filter = False + self._alpha = 1. + self._colormap = colormap or Colormap() # Default colormap + self._colormap.addListener(self._cmapChanged) + self._interpolation = 'linear' + super(ColormapMesh3D, self).__init__(mode, + indices, + position=position, + normal=normal) + + self.isBackfaceVisible = True + + def setData(self, data, copy=True): + data = numpy.array(data, copy=copy, order='C') + assert data.ndim == 3 + self._data = data + self._update_texture = True + + def getData(self, copy=True): + return numpy.array(self._data, copy=copy) + + @property + def interpolation(self): + """The texture interpolation mode: 'linear' or 'nearest'""" + return self._interpolation + + @interpolation.setter + def interpolation(self, interpolation): + assert interpolation in ('linear', 'nearest') + self._interpolation = interpolation + self._update_texture_filter = True + self.notify() + + @property + def alpha(self): + """Transparency of the plane, float in [0, 1]""" + return self._alpha + + @alpha.setter + def alpha(self, alpha): + self._alpha = float(alpha) + + @property + def colormap(self): + """The colormap used by this primitive""" + return self._colormap + + def _cmapChanged(self, source, *args, **kwargs): + """Broadcast colormap changes""" + self.notify(*args, **kwargs) + + def prepareGL2(self, ctx): + if self._texture is None or self._update_texture: + if self._texture is not None: + self._texture.discard() + + if self.interpolation == 'nearest': + filter_ = gl.GL_NEAREST + else: + filter_ = gl.GL_LINEAR + self._update_texture = False + self._update_texture_filter = False + self._texture = _glutils.Texture( + gl.GL_R32F, self._data, gl.GL_RED, + minFilter=filter_, + magFilter=filter_, + wrap=gl.GL_CLAMP_TO_EDGE) + + if self._update_texture_filter: + self._update_texture_filter = False + if self.interpolation == 'nearest': + filter_ = gl.GL_NEAREST + else: + filter_ = gl.GL_LINEAR + self._texture.minFilter = filter_ + self._texture.magFilter = filter_ + + super(ColormapMesh3D, self).prepareGL2(ctx) + + def renderGL2(self, ctx): + fragment = self._shaders[1].substitute( + clippingDecl=ctx.clipper.fragDecl, + clippingCall=ctx.clipper.fragCall, + lightingFunction=ctx.viewport.light.fragmentDef, + lightingCall=ctx.viewport.light.fragmentCall, + colormapDecl=self.colormap.decl, + colormapCall=self.colormap.call + ) + program = ctx.glCtx.prog(self._shaders[0], fragment) + program.use() + + ctx.viewport.light.setupProgram(ctx, program) + self.colormap.setupProgram(ctx, program) + + if not self.isBackfaceVisible: + gl.glCullFace(gl.GL_BACK) + gl.glEnable(gl.GL_CULL_FACE) + + program.setUniformMatrix('matrix', ctx.objectToNDC.matrix) + program.setUniformMatrix('transformMat', + ctx.objectToCamera.matrix, + safe=True) + gl.glUniform1f(program.uniforms['alpha'], self._alpha) + + shape = self._data.shape + scales = 1./shape[2], 1./shape[1], 1./shape[0] + gl.glUniform3f(program.uniforms['dataScale'], *scales) + + gl.glUniform1i(program.uniforms['data'], self._texture.texUnit) + + ctx.clipper.setupProgram(ctx, program) + + self._texture.bind() + self._draw(program) + + if not self.isBackfaceVisible: + gl.glDisable(gl.GL_CULL_FACE) + + +class CutPlane(PlaneInGroup): + """A cutting plane in a 3D texture""" + + def __init__(self, point=(0., 0., 0.), normal=(0., 0., 1.)): + self._data = None + self._mesh = None + self._alpha = 1. + self._interpolation = 'linear' + self._colormap = Colormap() + super(CutPlane, self).__init__(point, normal) + + def setData(self, data, copy=True): + if data is None: + self._data = None + if self._mesh is not None: + self._children.remove(self._mesh) + self._mesh = None + + else: + data = numpy.array(data, copy=copy, order='C') + assert data.ndim == 3 + self._data = data + if self._mesh is not None: + self._mesh.setData(data, copy=False) + + def getData(self, copy=True): + return None if self._mesh is None else self._mesh.getData(copy=copy) + + @property + def alpha(self): + return self._alpha + + @alpha.setter + def alpha(self, alpha): + self._alpha = float(alpha) + if self._mesh is not None: + self._mesh.alpha = alpha + + @property + def colormap(self): + return self._colormap + + @property + def interpolation(self): + """The texture interpolation mode: 'linear' (default) or 'nearest'""" + return self._interpolation + + @interpolation.setter + def interpolation(self, interpolation): + assert interpolation in ('nearest', 'linear') + if interpolation != self.interpolation: + self._interpolation = interpolation + if self._mesh is not None: + self._mesh.interpolation = interpolation + + def prepareGL2(self, ctx): + if self.isValid: + + contourVertices = self.contourVertices + + if (self.interpolation == 'nearest' and + contourVertices is not None and len(contourVertices)): + # Avoid cut plane co-linear with array bin edges + for index, normal in enumerate(((1., 0., 0.), (0., 1., 0.), (0., 0., 1.))): + if (numpy.all(numpy.equal(self.plane.normal, normal)) and + int(self.plane.point[index]) == self.plane.point[index]): + contourVertices += self.plane.normal * 0.01 # Add an offset + break + + if self._mesh is None and self._data is not None: + self._mesh = ColormapMesh3D(contourVertices, + normal=self.plane.normal, + data=self._data, + copy=False, + mode='fan', + colormap=self.colormap) + self._mesh.alpha = self._alpha + self._interpolation = self.interpolation + self._children.insert(0, self._mesh) + + if self._mesh is not None: + if (contourVertices is None or + len(contourVertices) == 0): + self._mesh.visible = False + else: + self._mesh.visible = True + self._mesh.setAttribute('normal', self.plane.normal) + self._mesh.setAttribute('position', contourVertices) + + super(CutPlane, self).prepareGL2(ctx) + + def renderGL2(self, ctx): + with self.viewport.light.turnOff(): + super(CutPlane, self).renderGL2(ctx) + + def _bounds(self, dataBounds=False): + if not dataBounds: + vertices = self.contourVertices + if vertices is not None: + return numpy.array( + (vertices.min(axis=0), vertices.max(axis=0)), + dtype=numpy.float32) + else: + return None # Plane in not slicing the data volume + else: + if self._data is None: + return None + else: + depth, height, width = self._data.shape + return numpy.array(((0., 0., 0.), + (width, height, depth)), + dtype=numpy.float32) + + @property + def contourVertices(self): + """The vertices of the contour of the plane/bounds intersection.""" + # TODO copy from PlaneInGroup, refactor all that! + bounds = self.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] + + # Render transforms RW, TODO refactor this! + @property + def transforms(self): + return self._transforms + + @transforms.setter + def transforms(self, iterable): + self._transforms.removeListener(self._transformChanged) + if isinstance(iterable, transform.TransformList): + # If it is a TransformList, do not create one to enable sharing. + self._transforms = iterable + else: + assert hasattr(iterable, '__iter__') + self._transforms = transform.TransformList(iterable) + self._transforms.addListener(self._transformChanged) |