#
# glmesh.py - The GLMesh class.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides the :class:`GLMesh` class, a :class:`.GLObject` used
to render :class:`.Mesh` overlays.
"""
import numpy as np
import numpy.linalg as npla
import OpenGL.GL as gl
from . import globject
import fsl.data.utils as dutils
import fsl.transform.affine as affine
import fsleyes.gl as fslgl
import fsleyes.gl.routines as glroutines
import fsleyes.gl.textures as textures
[docs]class GLMesh(globject.GLObject):
"""The ``GLMesh`` class is a :class:`.GLObject` which encapsulates the
logic required to draw 2D slices, and 3D renderings, of a
:class:`.Mesh` overlay. The ``GLMesh`` class assumes that the
:class:`.Display` instance associated with the ``Mesh`` overlay
holds a reference to a :class:`.MeshOpts` instance, which contains
``GLMesh`` specific display settings.
**2D rendering**
A ``GLMesh`` is rendered in one of two different ways, depending upon the
value of the :attr:`.MeshOpts.outline` and :attr:`.MeshOpts.vertexData`
properties.
*Cross-sections*
If ``outline is False and vertexData is None``, a filled cross-section of
the mesh is drawn. This is accomplished using a three-pass technique,
roughly following that described at
http://glbook.gamedev.net/GLBOOK/glbook.gamedev.net/moglgp/advclip.html:
1. The front face of the mesh is rendered to the stencil buffer.
2. The back face is rendered to the stencil buffer, and subtracted
from the front face.
3. This intersection (of the mesh with a plane at the slice Z location)
is then rendered to the canvas.
This cross-section is filled with the :attr:`.MeshOpts.colour` property.
*Outlines*
If ``outline is True or vertexData is not None``, the intersection of the
mesh triangles with the viewing plane is calculated. These lines are then
rendered as ``GL_LINES`` primitives.
When a mesh outline is drawn on a canvas, the calculated line vertices,
and corresponding mesh faces (triangles) are cached via the
:meth:`.OverlayList.setData` method. This is so that other parts of
FSLeyes can access this information if necessary (e.g. the
:class:`.OrthoViewProfile` provides mesh-specific interaction). For a
canvas which is showing a plane orthogonal to display axis X, Y or Z, this
data will be given a key of ``'crosssection_0'``, ``'crosssection_1'``, or
``'crosssection_2'`` respectively.
*Colouring*
These lines will be coloured in one of the following ways:
- With the ``MeshOpts.colour``.
- According to the :attr:`.MeshOpts.vertexData` if it is set, in which
case the properties of the :class:`.ColourMapOpts` class (from which
the :class:`.MeshOpts` class derives) come into effect.
- Or, if :attr:`vertexData` is set, and the :attr:`.MeshOpts.useLut`
property is ``True``, the :attr:`.MeshOpts.lut` is used.
When ``MeshOpts.vertexData is not None``, the ``GLMesh`` class makes use
of the ``glmesh`` vertex and fragment shaders. These shaders are managed
by two OpenGL version-specific modules - :mod:`.gl14.glmesh_funcs`, and
:mod:`.gl21.glmesh_funcs`. These version specific modules must provide the
following functions:
- ``compileShaders(GLMesh)``: Compiles vertex/fragment shaders.
- ``updateShaderState(GLMesh, **kwargs)``: Updates vertex/fragment
shaders. Should expect the following keyword arguments:
- ``useNegCmap``: Boolean which is ``True`` if a negative colour map
should be used
- ``cmapXform``: Transformation matrix which transforms from vertex
data into colour map texture coordinates.
- ``flatColour``: Colour to use for fragments which are outside
of the clipping range.
- ``lightPos`` Light position in display coordinates.
- ``preDraw(GLMesh)``: Prepare for drawing (e.g. load shaders)
- ``draw(GLMesh, glType, vertices, indices=None, normals=None, vdata=None)``: # noqa
Draws mesh using shaders.
- ``postDraw(GLMesh)``: Clean up after drawing
**3D rendering**
3D mesh rendering is much simpler than 2D rendering. The mesh is simply
rendered to the canvas, and coloured in the same way as described above.
Whether the 3D mesh is coloured with a flat colour, or according to vertex
data, shader programs are used which colour the mesh, and also apply a
simple lighting effect.
"""
[docs] def __init__(self, overlay, overlayList, displayCtx, canvas, threedee):
"""Create a ``GLMesh``.
:arg overlay: A :class:`.Mesh` overlay.
:arg overlayList: The :class:`.OverlayList`
:arg displayCtx: The :class:`.DisplayContext` managing the scene.
:arg canvas: The canvas drawing this ``GLMesh``.
:arg threedee: 2D or 3D rendering.
"""
globject.GLObject.__init__(
self, overlay, overlayList, displayCtx, canvas, threedee)
self.flatShader = None
self.dataShader = None
self.activeShader = None
# We use a render texture when
# rendering model cross sections.
# This texture is kept at display/
# screen resolution
self.renderTexture = textures.RenderTexture(
self.name, interp=gl.GL_NEAREST)
# Mesh overlays are coloured:
#
# - with a constant colour (opts.outline == False), or
#
# - with a +/- colour map, (opts.vertexData not None), or
#
# - with a lookup table (opts.useLut == True and
# opts.vertexData is not None)
self.cmapTexture = textures.ColourMapTexture( self.name)
self.negCmapTexture = textures.ColourMapTexture( self.name)
self.lutTexture = textures.LookupTableTexture(self.name)
self.lut = None
self.registerLut()
self.addListeners()
self.updateVertices()
self.refreshCmapTextures(notify=False)
self.compileShaders()
self.updateShaderState()
[docs] def destroy(self):
"""Must be called when this ``GLMesh`` is no longer needed. Removes
some property listeners and destroys the colour map textures and
off-screen :class:`.RenderTexture`.
"""
self.renderTexture .destroy()
self.cmapTexture .destroy()
self.negCmapTexture.destroy()
self.lutTexture .destroy()
self.removeListeners()
self.deregisterLut()
globject.GLObject.destroy(self)
if self.flatShader is not None: self.flatShader.destroy()
if self.dataShader is not None: self.dataShader.destroy()
self.dataShader = None
self.flatShader = None
self.activeShader = None
self.lut = None
self.renderTexture = None
self.cmapTexture = None
self.negCmapTexture = None
self.lutTexture = None
[docs] def ready(self):
"""Overrides :meth:`.GLObject.ready`. Always returns ``True``. """
return True
[docs] def addListeners(self):
"""Called by :meth:`__init__`. Adds some property listeners to the
:class:`.Display` and :class:`.MeshOpts` instances so the OpenGL
representation can be updated when the display properties are changed..
"""
name = self.name
display = self.display
canvas = self.canvas
opts = self.opts
def shader(*a):
self.updateShaderState()
self.notify()
def vertices(*a):
self.updateVertices()
self.updateShaderState()
self.notify()
def refreshCmap(*a):
self.refreshCmapTextures(notify=False)
self.updateShaderState()
self.notify()
def registerLut(*a):
self.deregisterLut()
self.registerLut()
self.refreshCmapTextures()
def refresh(*a):
self.notify()
self.overlay.register(name, vertices, 'vertices')
opts .addListener('bounds', name, vertices, weak=False)
opts .addListener('colour', name, shader, weak=False)
opts .addListener('outline', name, refresh, weak=False)
opts .addListener('outlineWidth', name, refresh, weak=False)
opts .addListener('wireframe', name, refresh, weak=False)
opts .addListener('vertexData', name, shader, weak=False)
opts .addListener('vertexDataIndex', name, shader, weak=False)
opts .addListener('clippingRange', name, shader, weak=False)
opts .addListener('invertClipping', name, shader, weak=False)
opts .addListener('discardClipped', name, shader, weak=False)
opts .addListener('cmap', name, refreshCmap, weak=False)
opts .addListener('useNegativeCmap', name, refreshCmap, weak=False)
opts .addListener('negativeCmap', name, refreshCmap, weak=False)
opts .addListener('cmapResolution', name, refreshCmap, weak=False)
opts .addListener('interpolateCmaps', name, refreshCmap, weak=False)
opts .addListener('invert', name, refreshCmap, weak=False)
opts .addListener('gamma', name, refreshCmap, weak=False)
opts .addListener('displayRange', name, refreshCmap, weak=False)
opts .addListener('useLut', name, shader, weak=False)
opts .addListener('lut', name, registerLut, weak=False)
display.addListener('alpha', name, refreshCmap, weak=False)
if self.threedee:
canvas.opts.addListener('light', name, shader, weak=False)
canvas.opts.addListener('lightPos', name, shader, weak=False)
# We don't need to listen for
# brightness or contrast, because
# they are linked to displayRange.
[docs] def removeListeners(self):
"""Called by :meth:`destroy`. Removes all of the listeners added by
the :meth:`addListeners` method.
"""
self.overlay.deregister(self.name, 'vertices')
self.opts .removeListener('bounds', self.name)
self.opts .removeListener('colour', self.name)
self.opts .removeListener('outline', self.name)
self.opts .removeListener('outlineWidth', self.name)
self.opts .removeListener('wireframe', self.name)
self.opts .removeListener('vertexData', self.name)
self.opts .removeListener('vertexDataIndex', self.name)
self.opts .removeListener('clippingRange', self.name)
self.opts .removeListener('invertClipping', self.name)
self.opts .removeListener('discardClipped', self.name)
self.opts .removeListener('cmap', self.name)
self.opts .removeListener('useNegativeCmap', self.name)
self.opts .removeListener('negativeCmap', self.name)
self.opts .removeListener('cmapResolution', self.name)
self.opts .removeListener('interpolateCmaps', self.name)
self.opts .removeListener('invert', self.name)
self.opts .removeListener('gamma', self.name)
self.opts .removeListener('displayRange', self.name)
self.opts .removeListener('useLut', self.name)
self.opts .removeListener('lut', self.name)
self.display.removeListener('alpha', self.name)
if self.threedee:
self.canvas.opts.removeListener('light', self.name)
self.canvas.opts.removeListener('lightPos', self.name)
[docs] def registerLut(self):
"""Registers property listeners with the currently registered
:class:`.LookupTable` (the :attr:`.MeshOpts.lut` property).
"""
self.lut = self.opts.lut
if self.lut is not None:
for topic in ['label', 'added', 'removed']:
self.lut.register(self.name, self.refreshCmapTextures, topic)
[docs] def deregisterLut(self):
"""De-registers property listeners from the currently registered
:class:`.LookupTable`.
"""
if self.lut is not None:
for topic in ['label', 'added', 'removed']:
self.lut.deregister(self.name, topic)
self.lut = None
[docs] def updateVertices(self, *a):
"""Called by :meth:`__init__`, and when certain display properties
change. (Re-)generates the mesh vertices, indices and normals (if
being displayed in 3D). They are stored as attributes called
``vertices``, ``indices``, and ``normals`` respectively.
"""
overlay = self.overlay
vertices = overlay.vertices
indices = overlay.indices
normals = self.overlay.vnormals
xform = self.opts.getTransform('mesh', 'display')
if not np.all(np.isclose(xform, np.eye(4))):
vertices = affine.transform(vertices, xform)
if self.threedee:
nmat = affine.invert(xform).T
normals = affine.transform(normals, nmat, vector=True)
self.vertices = np.asarray(vertices, dtype=np.float32)
self.indices = np.asarray(indices.flatten(), dtype=np.uint32)
self.vertices = dutils.makeWriteable(self.vertices)
self.indices = dutils.makeWriteable(self.indices)
if self.threedee:
self.normals = np.array(normals, dtype=np.float32)
[docs] def frontFace(self):
"""Returns the face of the mesh triangles which which will be facing
outwards, either ``GL_CCW`` or ``GL_CW``. This will differ depending
on the mesh-to-display transformation matrix.
This method is only used in 3D rendering.
"""
if not self.threedee:
return gl.GL_CCW
# Only looking at the mesh -> display
# transform, thus we are assuming that
# the MVP matrix does not have any
# negative scales.
xform = self.opts.getTransform('mesh', 'display')
if npla.det(xform) > 0: return gl.GL_CCW
else: return gl.GL_CW
[docs] def getDisplayBounds(self):
"""Overrides :meth:`.GLObject.getDisplayBounds`. Returns a bounding
box which contains the mesh vertices.
"""
return (self.opts.bounds.getLo(),
self.opts.bounds.getHi())
[docs] def draw2DOutlineEnabled(self):
"""Only relevent for 2D rendering. Returns ``True`` if outline mode
should be used, ``False`` otherwise.
"""
opts = self.opts
overlay = self.overlay
return ((overlay.trimesh is not None) and
(opts.outline or opts.vertexData is not None))
[docs] def needShader(self):
"""Returns ``True`` if a shader should be loaded, ``False`` otherwise.
Relevant for both 2D and 3D rendering.
"""
return (self.threedee or
(self.draw2DOutlineEnabled() and
self.opts.vertexData is not None))
[docs] def preDraw(self, xform=None, bbox=None):
"""Overrides :meth:`.GLObject.preDraw`. Performs some pre-drawing
configuration, which might involve loading shaders, and/or setting the
size of the backing :class:`.RenderTexture` instance based on the
current viewport size.
"""
useShader = self.needShader()
outline = self.draw2DOutlineEnabled()
useTexture = not (self.threedee or outline)
# A shader program is used either in 3D, or
# in 2D when some vertex data is being shown
if useShader:
fslgl.glmesh_funcs.preDraw(self)
if self.opts.vertexData is not None:
if self.opts.useLut:
self.lutTexture.bindTexture(gl.GL_TEXTURE0)
else:
self.cmapTexture .bindTexture(gl.GL_TEXTURE0)
self.negCmapTexture.bindTexture(gl.GL_TEXTURE1)
# An off-screen texture is used when
# drawing off-screen textures in 2D
if useTexture:
size = gl.glGetIntegerv(gl.GL_VIEWPORT)
width = size[2]
height = size[3]
# We only need to resize the texture when
# the viewport size/quality changes.
if self.renderTexture.shape != (width, height):
self.renderTexture.shape = width, height
[docs] def draw2D(self, zpos, axes, xform=None, bbox=None):
"""Overrids :meth:`.GLObject.draw2D`. Draws a 2D slice of the
:class:`.Mesh`, at the specified Z location.
"""
overlay = self.overlay
lo, hi = self.getDisplayBounds()
xax, yax, zax = axes
outline = self.draw2DOutlineEnabled()
# Mesh is 2D, and is
# perpendicular to
# the viewing plane
if np.any(np.isclose([lo[xax], lo[yax]], [hi[xax], hi[yax]])):
return
is2D = np.isclose(lo[zax], hi[zax])
# 2D meshes are always drawn,
# regardless of the zpos
if not is2D and (zpos < lo[zax] or zpos > hi[zax]):
return
# the calculateIntersection method caches
# cross section vertices - make sure they're
# cleared in case we are not doing an outline
# draw.
self.overlayList.setData(overlay, 'crosssection_{}'.format(zax), None)
if is2D:
self.draw2DMesh(xform, bbox)
elif outline:
self.drawOutline(zpos, axes, xform, bbox)
else:
lo, hi = self.calculateViewport(lo, hi, axes, bbox)
xmin = lo[xax]
xmax = hi[xax]
ymin = lo[yax]
ymax = hi[yax]
tex = self.renderTexture
self.drawCrossSection(zpos, axes, lo, hi, tex)
tex.drawOnBounds(zpos, xmin, xmax, ymin, ymax, xax, yax, xform)
[docs] def draw3D(self, xform=None, bbox=None):
"""Overrides :meth:`.GLObject.draw3D`. Draws a 3D rendering of the
mesh.
"""
opts = self.opts
verts = self.vertices
idxs = self.indices
normals = self.normals
blo, bhi = self.getDisplayBounds()
vdata = opts.getVertexData()
is2D = np.isclose(bhi[2], blo[2])
if opts.wireframe:
gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_LINE)
gl.glLineWidth(2)
else:
gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_FILL)
if xform is not None:
gl.glMatrixMode(gl.GL_MODELVIEW)
gl.glPushMatrix()
gl.glMultMatrixf(np.array(xform, dtype=np.float32).ravel('F'))
if is2D or opts.wireframe:
enable = (gl.GL_DEPTH_TEST)
else:
enable = (gl.GL_DEPTH_TEST, gl.GL_CULL_FACE)
gl.glDisable(gl.GL_CULL_FACE)
with glroutines.enabled(enable):
gl.glFrontFace(self.frontFace())
if not is2D:
gl.glCullFace(gl.GL_BACK)
fslgl.glmesh_funcs.draw(
self,
gl.GL_TRIANGLES,
verts,
normals=normals,
indices=idxs,
vdata=vdata)
if xform is not None:
gl.glPopMatrix()
[docs] def postDraw(self, xform=None, bbox=None):
"""Overrides :meth:`.GLObject.postDraw`. May call the
:func:`.gl14.glmesh_funcs.postDraw` or
:func:`.gl21.glmesh_funcs.postDraw` function.
"""
if self.needShader():
fslgl.glmesh_funcs.postDraw(self)
if self.opts.vertexData is not None:
if self.opts.useLut:
self.lutTexture.unbindTexture()
else:
self.cmapTexture .unbindTexture()
self.negCmapTexture.unbindTexture()
[docs] def drawOutline(self, zpos, axes, xform=None, bbox=None):
"""Called by :meth:`draw2D` when ``MeshOpts.outline is True or
MeshOpts.vertexData is not None``. Calculates the intersection of the
mesh with the viewing plane, and renders it as a set of
``GL_LINES``. If ``MeshOpts.vertexData is None``, the draw is
performed using immediate mode OpenGL.
Otherwise, the :func:`.gl14.glmesh_funcs.draw` or
:func:`.gl21.glmesh_funcs.draw` function is used, which performs
shader-based rendering.
"""
opts = self.opts
# Makes code below a bit nicer
if xform is None:
xform = np.eye(4)
vertices, faces, dists, vertXform = self.calculateIntersection(
zpos, axes, bbox)
if vertXform is not None:
xform = affine.concat(xform, vertXform)
vdata = self.getVertexData(faces, dists)
useShader = vdata is not None
vertices = vertices.reshape(-1, 3)
nvertices = vertices.shape[0]
gl.glMatrixMode(gl.GL_MODELVIEW)
gl.glPushMatrix()
gl.glMultMatrixf(np.array(xform, dtype=np.float32).ravel('F'))
gl.glLineWidth(opts.outlineWidth)
# Constant colour
if not useShader:
vertices = vertices.ravel('C')
gl.glColor(*opts.getConstantColour())
gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
gl.glVertexPointer(3, gl.GL_FLOAT, 0, vertices)
gl.glDrawArrays(gl.GL_LINES, 0, nvertices)
gl.glDisableClientState(gl.GL_VERTEX_ARRAY)
# Coloured from vertex data
else:
fslgl.glmesh_funcs.draw(
self,
gl.GL_LINES,
vertices,
vdata=vdata)
gl.glPopMatrix()
[docs] def draw2DMesh(self, xform=None, bbox=None):
"""Not to be confused with :meth:`draw2D`. Called by :meth:`draw2D`
for :class:`.Mesh` overlays which are actually 2D (with a flat
third dimension).
"""
opts = self.opts
vdata = opts.getVertexData()
useShader = self.needShader()
vertices = self.vertices
faces = self.indices
if opts.outline:
gl.glLineWidth(opts.outlineWidth)
gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_LINE)
else:
gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_FILL)
# Constant colour
if not useShader:
gl.glColor(*opts.getConstantColour())
gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
gl.glVertexPointer(3, gl.GL_FLOAT, 0, vertices.ravel('C'))
gl.glDrawElements(gl.GL_TRIANGLES,
faces.shape[0],
gl.GL_UNSIGNED_INT,
faces.ravel('C'))
gl.glDisableClientState(gl.GL_VERTEX_ARRAY)
# Coloured from vertex data
else:
vdata = vdata[:, opts.vertexDataIndex]
fslgl.glmesh_funcs.draw(
self,
gl.GL_TRIANGLES,
vertices,
indices=faces,
vdata=vdata)
[docs] def drawCrossSection(self, zpos, axes, lo, hi, dest):
"""Renders a filled cross-section of the mesh to an off-screen
:class:`.RenderTexture`. See:
http://glbook.gamedev.net/GLBOOK/glbook.gamedev.net/moglgp/advclip.html
:arg zpos: Position along the z axis
:arg axes: Tuple containing ``(x, y, z)`` axis indices.
:arg lo: Tuple containing the low bounds on each axis.
:arg hi: Tuple containing the high bounds on each axis.
:arg dest: The :class:`.RenderTexture` to render to.
"""
opts = self.opts
xax = axes[0]
yax = axes[1]
zax = axes[2]
xmin = lo[xax]
ymin = lo[yax]
xmax = hi[xax]
ymax = hi[yax]
vertices = self.vertices.ravel('C')
indices = self.indices
dest.bindAsRenderTarget()
dest.setRenderViewport(xax, yax, lo, hi)
# Figure out the equation of a plane
# perpendicular to the Z axis, and
# located at the z position. This is
# used as a clipping plane to draw
# the mesh intersection.
clipPlaneVerts = np.zeros((4, 3), dtype=np.float32)
clipPlaneVerts[0, [xax, yax]] = [xmin, ymin]
clipPlaneVerts[1, [xax, yax]] = [xmin, ymax]
clipPlaneVerts[2, [xax, yax]] = [xmax, ymax]
clipPlaneVerts[3, [xax, yax]] = [xmax, ymin]
clipPlaneVerts[:, zax] = zpos
planeEq = glroutines.planeEquation(clipPlaneVerts[0, :],
clipPlaneVerts[1, :],
clipPlaneVerts[2, :])
gl.glClearColor(0, 0, 0, 0)
gl.glClear(gl.GL_COLOR_BUFFER_BIT |
gl.GL_DEPTH_BUFFER_BIT |
gl.GL_STENCIL_BUFFER_BIT)
gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
gl.glEnable(gl.GL_CLIP_PLANE0)
gl.glEnable(gl.GL_CULL_FACE)
gl.glEnable(gl.GL_STENCIL_TEST)
gl.glFrontFace(gl.GL_CCW)
gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_FILL)
gl.glClipPlane(gl.GL_CLIP_PLANE0, planeEq)
gl.glColorMask(gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE)
# First and second passes - render front and
# back faces separately. In the stencil buffer,
# subtract the mask created by the second
# render from the mask created by the first -
# this gives us a mask which shows the
# intersection of the mesh with the clipping
# plane.
gl.glStencilFunc(gl.GL_ALWAYS, 0, 0)
direction = [gl.GL_INCR, gl.GL_DECR]
# If the mesh coordinate transformation
# has a negative determinant, it means
# the back faces will be facing the camera,
# so we need to render the back faces first.
if npla.det(opts.getTransform('mesh', 'display')) > 0:
faceOrder = [gl.GL_FRONT, gl.GL_BACK]
else:
faceOrder = [gl.GL_BACK, gl.GL_FRONT]
for face, direction in zip(faceOrder, direction):
gl.glStencilOp(gl.GL_KEEP, gl.GL_KEEP, direction)
gl.glCullFace(face)
gl.glVertexPointer(3, gl.GL_FLOAT, 0, vertices)
gl.glDrawElements(gl.GL_TRIANGLES,
len(indices),
gl.GL_UNSIGNED_INT,
indices)
# Third pass - render the intersection
# of the front and back faces from the
# stencil buffer to the render texture.
gl.glColorMask(gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE)
gl.glDisable(gl.GL_CLIP_PLANE0)
gl.glDisable(gl.GL_CULL_FACE)
gl.glDisableClientState(gl.GL_VERTEX_ARRAY)
gl.glStencilFunc(gl.GL_NOTEQUAL, 0, 255)
gl.glColor(*opts.getConstantColour())
# Disable alpha blending - we
# just want the colour copied
# into the texture as-is.
with glroutines.disabled(gl.GL_BLEND):
gl.glBegin(gl.GL_QUADS)
gl.glVertex3f(*clipPlaneVerts[0, :])
gl.glVertex3f(*clipPlaneVerts[1, :])
gl.glVertex3f(*clipPlaneVerts[2, :])
gl.glVertex3f(*clipPlaneVerts[3, :])
gl.glEnd()
gl.glDisable(gl.GL_STENCIL_TEST)
dest.unbindAsRenderTarget()
dest.restoreViewport()
[docs] def calculateViewport(self, lo, hi, axes, bbox=None):
"""Called by :meth:`draw2D`. Calculates an appropriate viewport (the
horizontal/vertical minimums/maximums in display coordinates) given
the ``lo`` and ``hi`` ``GLMesh`` display bounds, and a display
``bbox``.
"""
xax = axes[0]
yax = axes[1]
if bbox is not None and (lo[xax] < bbox[xax][0] or
hi[xax] < bbox[xax][1] or
lo[yax] > bbox[yax][0] or
hi[yax] > bbox[yax][1]):
xlen = float(hi[xax] - lo[xax])
ylen = float(hi[yax] - lo[yax])
ratio = xlen / ylen
bblo = [ax[0] for ax in bbox]
bbhi = [ax[1] for ax in bbox]
lo = [max(l, bbl) for l, bbl in zip(lo, bblo)]
hi = [min(h, bbh) for h, bbh in zip(hi, bbhi)]
dxlen = float(hi[xax] - lo[xax])
dylen = float(hi[yax] - lo[yax])
dratio = dxlen / dylen
if dratio > ratio:
ndxlen = xlen * (dylen / ylen)
lo[xax] += 0.5 * (ndxlen - dxlen)
hi[xax] -= 0.5 * (ndxlen - dxlen)
elif dratio < ratio:
ndylen = ylen * (dxlen / xlen)
lo[yax] += 0.5 * (ndylen - dylen)
hi[yax] -= 0.5 * (ndylen - dylen)
return lo, hi
[docs] def calculateIntersection(self, zpos, axes, bbox=None):
"""Uses the :func:`.Mesh.planeIntersection` method to
calculate the intersection of the mesh with the viewing plane at
the given ``zpos``.
:arg zpos: Z axis coordinate at which the intersection is to be
calculated
:arg axes: Tuple containing the ``(x, y, z)`` axis indices.
:arg bbox: A tuple containing a ``([xlo, ylo, zlo], [xhi, yhi, zhi])``
bounding box to which the calculation can be restricted.
:returns: A tuple containing:
- A ``(n, 2, 3)`` array which contains the two vertices of
a line for every intersected face (triangle) in the mesh.
- A ``(n, 3)`` array containing the intersected faces
(indices into the :attr:`.Mesh.vertices` array).
- A ``(n, 2, 3)`` array containing the barycentric
coordinates of the intersection line vertices.
- A ``(4, 4)`` array containing a transformation matrix
for transforming the line vertices into the display
coordinate system. May be ``None``, indicating that
no transformation is necessary.
.. note:: The line vertices, and corresponding mesh triangle face
indices, which make up the cross section are saved
via the :meth:`.OverlayList.setData` method, with a key
``'crosssection_[zax]'``, where ``[zax]`` is set to the
index of the display Z axis.
"""
overlay = self.overlay
zax = axes[2]
opts = self.opts
origin = [0] * 3
normal = [0] * 3
origin[zax] = zpos
normal[zax] = 1
vertXform = opts.getTransform( 'mesh', 'display')
origin = opts.transformCoords(origin, 'display', 'mesh')
normal = opts.transformCoords(normal, 'display', 'mesh',
vector=True)
# TODO use bbox to constrain? This
# would be nice, but is not
# supported by trimesh.
lines, faces, dists = overlay.planeIntersection(
normal, origin, distances=True)
lines = np.asarray(lines, dtype=np.float32)
faces = np.asarray(faces, dtype=np.uint32)
# cache the line vertices for other
# things which might be interested.
# See, for example, OrthoViewProfile
# pick mode methods.
self.overlayList.setData(overlay,
'crosssection_{}'.format(zax),
(lines, faces))
faces = overlay.indices[faces]
return lines, faces, dists, vertXform
[docs] def getVertexData(self, faces, dists):
"""If :attr:`.MeshOpts.vertexData` is not ``None``, this method
returns the vertex data to use for the line segments calculated
in the :meth:`calculateIntersection` method.
The ``dists`` array contains barycentric coordinates for each line
vertex, and is used to linearly interpolate between the values of the
vertices of the intersected triangles (defined in ``faces``).
If ``MeshOpts.vertexData is None``, this method returns ``None``.
"""
opts = self.opts
vdata = opts.getVertexData()
if vdata is None:
return None
vdata = vdata[:, opts.vertexDataIndex]
vdata = vdata[faces].repeat(2, axis=0).reshape(-1, 2, 3)
vdata = (vdata * dists).reshape(-1, 3).sum(axis=1)
return np.asarray(vdata, np.float32)
[docs] def refreshCmapTextures(self, *a, **kwa):
"""Called when various :class:`.Display` or :class:`.MeshOpts``
properties change. Refreshes the :class:`.ColourMapTexture` instances
corresponding to the :attr:`.MeshOpts.cmap` and
:attr:`.MeshOpts.negativeCmap` properties, and the
:class:`.LookupTableTexture` corresponding to the :attr:`.MeshOpts.lut`
property.
:arg notify: Must be passed as a keyword argument. If ``True`` (the
default) :meth:`.GLObject.notify` is called after the
textures have been updated.
"""
notify = kwa.pop('notify', True)
display = self.display
opts = self.opts
alpha = display.alpha / 100.0
cmap = opts.cmap
interp = opts.interpolateCmaps
res = opts.cmapResolution
negCmap = opts.negativeCmap
gamma = opts.realGamma(opts.gamma)
invert = opts.invert
dmin = opts.displayRange[0]
dmax = opts.displayRange[1]
if interp: interp = gl.GL_LINEAR
else: interp = gl.GL_NEAREST
self.cmapTexture.set(cmap=cmap,
invert=invert,
alpha=alpha,
resolution=res,
gamma=gamma,
interp=interp,
displayRange=(dmin, dmax))
self.negCmapTexture.set(cmap=negCmap,
invert=invert,
alpha=alpha,
resolution=res,
gamma=gamma,
interp=interp,
displayRange=(dmin, dmax))
self.lutTexture.set(alpha=display.alpha / 100.0,
brightness=display.brightness / 100.0,
contrast=display.contrast / 100.0,
lut=opts.lut)
if notify:
self.notify()
[docs] def compileShaders(self):
"""(Re)Compiles the vertex/fragment shader program(s), via a call
to the GL-version specific ``compileShaders`` function.
"""
if self.flatShader is not None: self.flatShader.destroy()
if self.dataShader is not None: self.dataShader.destroy()
self.activeShader = None
fslgl.glmesh_funcs.compileShaders(self)
[docs] def updateShaderState(self):
"""Updates the vertex/fragment shader program(s) state, via a call to
the GL-version specific ``updateShaderState`` function.
"""
dopts = self.opts
copts = self.canvas.opts
lightPos = None
flatColour = dopts.getConstantColour()
useNegCmap = (not dopts.useLut) and dopts.useNegativeCmap
if self.threedee:
lightPos = np.array(copts.lightPos)
lightPos *= (copts.zoom / 100.0)
else:
lightPos = None
if dopts.useLut:
delta = 1.0 / (dopts.lut.max() + 1)
cmapXform = affine.scaleOffsetXform(delta, 0.5 * delta)
else:
cmapXform = self.cmapTexture.getCoordinateTransform()
fslgl.glmesh_funcs.updateShaderState(
self,
useNegCmap=useNegCmap,
cmapXform=cmapXform,
flatColour=flatColour,
lightPos=lightPos)