# /*##########################################################################
#
# Copyright (c) 2016-2020 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...
"""
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "11/01/2018"
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
from silx._utils import NP_OPTIONAL_COPY
[docs]
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;
uniform vec3 texCoordsOffset;
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 + texCoordsOffset;
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
$sceneDecl
$lightingFunction
void main(void)
{
$scenePreCall(vCameraPosition);
float value = texture3D(data, vTexCoords).r;
vec4 color = $colormapCall(value);
color.a *= alpha;
gl_FragColor = $lightingCall(color, vPosition, vNormal);
$scenePostCall(vCameraPosition);
}
"""
),
)
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 or NP_OPTIONAL_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.0
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
self.textureOffset = 0.0, 0.0, 0.0
"""Offset to add to texture coordinates"""
def setData(self, data, copy=True):
data = numpy.array(data, copy=copy or NP_OPTIONAL_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 or NP_OPTIONAL_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)
[docs]
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)
[docs]
def renderGL2(self, ctx):
fragment = self._shaders[1].substitute(
sceneDecl=ctx.fragDecl,
scenePreCall=ctx.fragCallPre,
scenePostCall=ctx.fragCallPost,
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.0 / shape[2], 1.0 / shape[1], 1.0 / shape[0]
gl.glUniform3f(program.uniforms["dataScale"], *scales)
gl.glUniform3f(program.uniforms["texCoordsOffset"], *self.textureOffset)
gl.glUniform1i(program.uniforms["data"], self._texture.texUnit)
ctx.setupProgram(program)
self._texture.bind()
self._draw(program)
if not self.isBackfaceVisible:
gl.glDisable(gl.GL_CULL_FACE)
[docs]
class CutPlane(PlaneInGroup):
"""A cutting plane in a 3D texture"""
def __init__(self, point=(0.0, 0.0, 0.0), normal=(0.0, 0.0, 1.0)):
self._data = None
self._mesh = None
self._alpha = 1.0
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 or NP_OPTIONAL_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
self.notify()
[docs]
def prepareGL2(self, ctx):
if self.isValid:
contourVertices = self.contourVertices
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._mesh.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)
needTextureOffset = False
if self.interpolation == "nearest":
# If cut plane is co-linear with array bin edges add texture offset
planePt = self.plane.point
for index, normal in enumerate(
((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0))
):
if (
numpy.all(numpy.equal(self.plane.normal, normal))
and int(planePt[index]) == planePt[index]
):
needTextureOffset = True
break
if needTextureOffset:
self._mesh.textureOffset = self.plane.normal * 1e-6
else:
self._mesh.textureOffset = 0.0, 0.0, 0.0
super(CutPlane, self).prepareGL2(ctx)
[docs]
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.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 = Box.getVertices(copy=True)
boxVertices = bounds[0] + boxVertices * (bounds[1] - bounds[0])
lineIndices = Box.getLineIndices(copy=False)
vertices = utils.boxPlaneIntersect(
boxVertices, lineIndices, self.plane.normal, self.plane.point
)
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)