#!/usr/bin/env python # # glvolume_funcs.py - OpenGL 1.4 functions used by the GLVolume class. # # Author: Paul McCarthy # """This module provides functions which are used by the :class:`.GLVolume` class to render :class:`.Image` overlays in an OpenGL 1.4 compatible manner. An :class:`.ARBPShader` is used to manage the ``glvolume`` vertex/fragment programs. """ import logging import numpy as np import OpenGL.GL as gl import fsl.transform.affine as affine import fsleyes.gl.shaders as shaders import fsleyes.gl.routines as glroutines import fsleyes.gl.glvolume as glvolume log = logging.getLogger(__name__) def init(self): """Calls :func:`compileShaders` and :func:`updateShaderState`.""" self.shader = None compileShaders( self) updateShaderState(self) def destroy(self): """Deletes handles to the vertex/fragment programs.""" if self.shader is not None: self.shader.destroy() self.shader = None def compileShaders(self): """Creates a :class:`.ARBPShader` instance. """ if self.shader is not None: self.shader.destroy() if self.threedee: frag = 'glvolume_3d' else: frag = 'glvolume' vertSrc = shaders.getVertexShader( 'glvolume') fragSrc = shaders.getFragmentShader(frag) texes = { 'imageTexture' : 0, 'colourTexture' : 1, 'negColourTexture' : 2, 'clipTexture' : 3, 'modulateTexture' : 4 } constants = { 'kill_fragments_early' : not self.threedee, 'texture_is_2d' : self.imageTexture.ndim == 2 } if self.threedee: if self.opts.clipMode == 'intersection': clipMode = 1 elif self.opts.clipMode == 'union': clipMode = 2 elif self.opts.clipMode == 'complement': clipMode = 3 else: clipMode = 0 constants['numSteps'] = self.opts.numInnerSteps constants['clipMode'] = clipMode constants['numClipPlanes'] = self.opts.numClipPlanes texes[ 'startingTexture'] = 5 texes[ 'depthTexture'] = 6 self.shader = shaders.ARBPShader(vertSrc, fragSrc, shaders.getShaderDir(), texes, constants) def updateShaderState(self): """Sets all variables required by the vertex and fragment programs. """ if not self.ready(): return opts = self.opts shader = self.shader # enable the vertex and fragment programs shader.load() # The voxValXform transformation turns # an image texture value into a raw # voxel value. The colourMapXform # transformation turns a raw voxel value # into a value between 0 and 1, suitable # for looking up an appropriate colour # in the 1D colour map texture. voxValXform = affine.concat(self.colourTexture.getCoordinateTransform(), self.imageTexture.voxValXform) voxValXform = [voxValXform[0, 0], voxValXform[0, 3], 0, 0] # And the clipping range, normalised # to the image texture value range invClip = 1 if opts.invertClipping else -1 useNegCmap = 1 if opts.useNegativeCmap else 0 imageIsClip = 1 if opts.clipImage is None else -1 # modalpha not applied in 3D modAlpha = 1 if opts.modulateAlpha else -1 imageIsMod = 1 if opts.modulateImage is None else -1 modXform = self.getModulateValueXform() imgXform = self.imageTexture.invVoxValXform if opts.clipImage is None: clipXform = imgXform else: clipXform = self.clipTexture.invVoxValXform clipLo = opts.clippingRange[0] * clipXform[0, 0] + clipXform[0, 3] clipHi = opts.clippingRange[1] * clipXform[0, 0] + clipXform[0, 3] texZero = 0.0 * imgXform[ 0, 0] + imgXform[ 0, 3] clipping = [clipLo, clipHi, invClip, imageIsClip] modulate = [modXform[0, 0], modXform[0, 3], modAlpha, imageIsMod] negCmap = [useNegCmap, texZero, modAlpha, 0] # disable clip image/modalpha for 3D if self.threedee: clipping[3] = 1 modulate[2] = -1 modulate[3] = 1 changed = False changed |= shader.setFragParam('voxValXform', voxValXform) changed |= shader.setFragParam('clipping', clipping) changed |= shader.setFragParam('modulate', modulate) changed |= shader.setFragParam('negCmap', negCmap) if self.threedee: clipPlanes = np.zeros((5, 4), dtype=np.float32) d2tmat = opts.getTransform('display', 'texture') for i in range(opts.numClipPlanes): origin, normal = self.get3DClipPlane(i) origin = affine.transform(origin, d2tmat) normal = affine.transformNormal(normal, d2tmat) clipPlanes[i, :] = glroutines.planeEquation2(origin, normal) changed |= shader.setFragParam('clipPlanes', clipPlanes) self.shader.unload() return changed def preDraw(self, xform=None, bbox=None): """Prepares to draw a slice from the given :class:`.GLVolume` instance. """ self.shader.load() self.shader.loadAtts() if isinstance(self, glvolume.GLVolume): clipCoordXform = self.getAuxTextureXform('clip') modCoordXform = self.getAuxTextureXform('modulate') self.shader.setVertParam('clipCoordXform', clipCoordXform) self.shader.setVertParam('modCoordXform', modCoordXform) def draw2D(self, zpos, axes, xform=None, bbox=None): """Draws a 2D slice of the image at the given Z location. """ vertices, voxCoords, texCoords = self.generateVertices2D( zpos, axes, bbox=bbox) if xform is not None: vertices = affine.transform(vertices, xform) vertices = np.array(vertices, dtype=np.float32).ravel('C') # Voxel coordinates are calculated # in the vertex program self.shader.setAtt('texCoord', texCoords) with glroutines.enabled((gl.GL_VERTEX_ARRAY)): gl.glVertexPointer(3, gl.GL_FLOAT, 0, vertices) gl.glDrawArrays(gl.GL_TRIANGLES, 0, 6) def draw3D(self, xform=None, bbox=None): """Draws the image in 3D on the canvas. :arg self: The :class:`.GLVolume` object which is managing the image to be drawn. :arg xform: A 4*4 transformation matrix to be applied to the vertex data. :arg bbox: An optional bounding box. """ opts = self.opts canvas = self.canvas display = self.display shader = self.shader shape = self.image.shape proj = canvas.projectionMatrix src = self.renderTexture1 dest = self.renderTexture2 w, h = src.shape vertices, voxCoords, texCoords = self.generateVertices3D(bbox) rayStep, texform = opts.calculateRayCastSettings(xform, proj) rayStep = affine.transformNormal( rayStep, self.imageTexture.texCoordXform(shape)) texform = affine.concat( texform, self.imageTexture.invTexCoordXform(shape)) if xform is not None: vertices = affine.transform(vertices, xform) vertices = np.array(vertices, dtype=np.float32).ravel('C') outerLoop = opts.getNumOuterSteps() screenSize = [ 1.0 / w, 1.0 / h, 1 if opts.blendByIntensity else -1, 0] rayStep = list(rayStep) + [0] texform = texform[2, :] settings = [ opts.blendFactor, 0, 0, display.alpha / 100.0] gl.glVertexPointer(3, gl.GL_FLOAT, 0, vertices) shader.setAtt( 'texCoord', texCoords) shader.setFragParam('rayStep', rayStep) shader.setFragParam('screenSize', screenSize) shader.setFragParam('tex2ScreenXform', texform) # Disable blending - we want each # loop to replace the contents of # the texture, not blend into it! with glroutines.enabled((gl.GL_VERTEX_ARRAY)), \ glroutines.disabled((gl.GL_BLEND)): # The depth value for a fragment will # not necessary be set at the same # time that the fragment colour is # set, so we need to use <= for depth # testing so that unset depth values # do not cause depth clipping. gl.glDepthFunc(gl.GL_LEQUAL) for i in range(outerLoop): settings = list(settings) dtex = src.depthTexture settings[1] = i * opts.numInnerSteps if i == outerLoop - 1: settings[2] = 1 else: settings[2] = -1 shader.setFragParam('settings', settings) dest.bindAsRenderTarget() src .bindTexture(gl.GL_TEXTURE5) dtex.bindTexture(gl.GL_TEXTURE6) gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT) gl.glDrawArrays(gl.GL_TRIANGLES, 0, 36) src .unbindTexture() dtex.unbindTexture() dest.unbindAsRenderTarget() dest, src = src, dest gl.glDepthFunc(gl.GL_LESS) shader.unloadAtts() shader.unload() self.renderTexture1 = src self.renderTexture2 = dest def drawAll(self, axes, zposes, xforms): """Draws mutltiple slices of the given image at the given Z position, applying the corresponding transformation to each of the slices. """ nslices = len(zposes) vertices = np.zeros((nslices * 6, 3), dtype=np.float32) texCoords = np.zeros((nslices * 6, 3), dtype=np.float32) indices = np.arange(nslices * 6, dtype=np.uint32) for i, (zpos, xform) in enumerate(zip(zposes, xforms)): v, vc, tc = self.generateVertices2D(zpos, axes) vertices[ i * 6: i * 6 + 6, :] = affine.transform(v, xform) texCoords[i * 6: i * 6 + 6, :] = tc vertices = vertices.ravel('C') self.shader.setAtt('texCoord', texCoords) with glroutines.enabled((gl.GL_VERTEX_ARRAY)): gl.glVertexPointer(3, gl.GL_FLOAT, 0, vertices) gl.glDrawElements(gl.GL_TRIANGLES, nslices * 6, gl.GL_UNSIGNED_INT, indices) def postDraw(self, xform=None, bbox=None): """Cleans up the GL state after drawing from the given :class:`.GLVolume` instance. """ self.shader.unloadAtts() self.shader.unload() if self.threedee: self.drawClipPlanes(xform=xform)