diff --git a/fsl/fsleyes/gl/gl21/glvolume_frag.glsl b/fsl/fsleyes/gl/gl21/glvolume_frag.glsl index 006509b51051ee001a4e2a870901d3dca23c6066..5b56722b1689e91bf76b86e20b90c7d2b0aad90d 100644 --- a/fsl/fsleyes/gl/gl21/glvolume_frag.glsl +++ b/fsl/fsleyes/gl/gl21/glvolume_frag.glsl @@ -18,6 +18,16 @@ uniform sampler3D imageTexture; */ uniform sampler1D colourTexture; +/* + * Texture containing the negative colour map. + */ +uniform sampler1D negColourTexture; + +/* + * Flag which determines whether to + * use the negative colour map. + */ +uniform bool enableNegCmap; /* * Shape of the imageTexture. @@ -37,15 +47,25 @@ uniform bool useSpline; uniform mat4 voxValXform; /* - * Clip voxels below this value. + * Clip voxels below this value. This must be specified + * in the image texture data range. */ uniform float clipLow; /* - * Clip voxels above this value. + * Clip voxels above this value. This must be specified + * in the image texture data range. */ uniform float clipHigh; +/* + * Value in the image texture data range which corresponds + * to zero - this is used to determine whether to use the + * regular, or the negative colour texture (if enableNegCmap + * is true). + */ +uniform float texZero; + /* * Invert clipping behaviour - clip voxels * that are inside the clipLow/High bounds. @@ -65,7 +85,10 @@ varying vec3 fragTexCoord; void main(void) { - vec3 voxCoord = fragVoxCoord; + float voxValue; + vec4 normVoxValue; + bool useNegCmap = false; + vec3 voxCoord = fragVoxCoord; /* * Skip voxels that are out of the image bounds @@ -79,7 +102,6 @@ void main(void) { /* * Look up the voxel value */ - float voxValue; if (useSpline) voxValue = spline_interp(imageTexture, fragTexCoord, imageShape, @@ -87,20 +109,35 @@ void main(void) { else voxValue = texture3D( imageTexture, fragTexCoord).r; + /* + * If we are using a negative colour map, + * and the voxel value is below the negative + * threshold (texZero) invert the voxel + * value, and set a flag telling the code + * below to use the neagtive colour map. + */ + if (enableNegCmap && voxValue <= texZero) { + + useNegCmap = true; + voxValue = texZero + (texZero - voxValue); + } + /* * Clip out of range voxel values */ - if ((!invertClip && (voxValue < clipLow || voxValue > clipHigh)) || - (invertClip && (voxValue >= clipLow && voxValue <= clipHigh))) { - gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0); - return; + ( invertClip && (voxValue >= clipLow && voxValue <= clipHigh))) { + + gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0); + return; } /* * Transform the voxel value to a colour map texture * coordinate, and look up the colour for the voxel value - */ - vec4 normVoxValue = voxValXform * vec4(voxValue, 0, 0, 1); - gl_FragColor = texture1D(colourTexture, normVoxValue.x); + */ + normVoxValue = voxValXform * vec4(voxValue, 0, 0, 1); + + if (useNegCmap) gl_FragColor = texture1D(negColourTexture, normVoxValue.x); + else gl_FragColor = texture1D(colourTexture, normVoxValue.x); } diff --git a/fsl/fsleyes/gl/gl21/glvolume_funcs.py b/fsl/fsleyes/gl/gl21/glvolume_funcs.py index 009ad30bcee83fafb86dfbe7b8f3e0983d3d7210..a0e87d615f59d0043b3f2c0993e19d1f88860a9d 100644 --- a/fsl/fsleyes/gl/gl21/glvolume_funcs.py +++ b/fsl/fsleyes/gl/gl21/glvolume_funcs.py @@ -58,28 +58,34 @@ def compileShaders(self): self.shaders = shaders.compileShaders(vertShaderSrc, fragShaderSrc) # indices of all vertex/fragment shader parameters - self.vertexPos = gl.glGetAttribLocation( self.shaders, - 'vertex') - self.voxCoordPos = gl.glGetAttribLocation( self.shaders, - 'voxCoord') - self.texCoordPos = gl.glGetAttribLocation( self.shaders, - 'texCoord') - self.imageTexturePos = gl.glGetUniformLocation(self.shaders, - 'imageTexture') - self.colourTexturePos = gl.glGetUniformLocation(self.shaders, - 'colourTexture') - self.imageShapePos = gl.glGetUniformLocation(self.shaders, - 'imageShape') - self.useSplinePos = gl.glGetUniformLocation(self.shaders, - 'useSpline') - self.voxValXformPos = gl.glGetUniformLocation(self.shaders, - 'voxValXform') - self.clipLowPos = gl.glGetUniformLocation(self.shaders, - 'clipLow') - self.clipHighPos = gl.glGetUniformLocation(self.shaders, - 'clipHigh') - self.invertClipPos = gl.glGetUniformLocation(self.shaders, - 'invertClip') + self.vertexPos = gl.glGetAttribLocation( self.shaders, + 'vertex') + self.voxCoordPos = gl.glGetAttribLocation( self.shaders, + 'voxCoord') + self.texCoordPos = gl.glGetAttribLocation( self.shaders, + 'texCoord') + self.imageTexturePos = gl.glGetUniformLocation(self.shaders, + 'imageTexture') + self.colourTexturePos = gl.glGetUniformLocation(self.shaders, + 'colourTexture') + self.negColourTexturePos = gl.glGetUniformLocation(self.shaders, + 'negColourTexture') + self.enableNegCmapPos = gl.glGetUniformLocation(self.shaders, + 'enableNegCmap') + self.texZeroPos = gl.glGetUniformLocation(self.shaders, + 'texZero') + self.imageShapePos = gl.glGetUniformLocation(self.shaders, + 'imageShape') + self.useSplinePos = gl.glGetUniformLocation(self.shaders, + 'useSpline') + self.voxValXformPos = gl.glGetUniformLocation(self.shaders, + 'voxValXform') + self.clipLowPos = gl.glGetUniformLocation(self.shaders, + 'clipLow') + self.clipHighPos = gl.glGetUniformLocation(self.shaders, + 'clipHigh') + self.invertClipPos = gl.glGetUniformLocation(self.shaders, + 'invertClip') def updateShaderState(self): @@ -105,10 +111,13 @@ def updateShaderState(self): xform = self.imageTexture.invVoxValXform clipLow = opts.clippingRange[0] * xform[0, 0] + xform[3, 0] clipHigh = opts.clippingRange[1] * xform[0, 0] + xform[3, 0] + texZero = 0.0 * xform[0, 0] + xform[3, 0] - gl.glUniform1f(self.clipLowPos, clipLow) - gl.glUniform1f(self.clipHighPos, clipHigh) - gl.glUniform1f(self.invertClipPos, opts.invertClipping) + gl.glUniform1f(self.clipLowPos, clipLow) + gl.glUniform1f(self.clipHighPos, clipHigh) + gl.glUniform1f(self.texZeroPos, texZero) + gl.glUniform1f(self.invertClipPos, opts.invertClipping) + gl.glUniform1f(self.enableNegCmapPos, opts.enableNegativeCmap) # Bind transformation matrix to transform # from image texture values to voxel values, @@ -121,8 +130,9 @@ def updateShaderState(self): gl.glUniformMatrix4fv(self.voxValXformPos, 1, False, vvx) # Set up the colour and image textures - gl.glUniform1i(self.imageTexturePos, 0) - gl.glUniform1i(self.colourTexturePos, 1) + gl.glUniform1i(self.imageTexturePos, 0) + gl.glUniform1i(self.colourTexturePos, 1) + gl.glUniform1i(self.negColourTexturePos, 2) gl.glUseProgram(0) diff --git a/fsl/fsleyes/gl/glvolume.py b/fsl/fsleyes/gl/glvolume.py index 2a9317a704350097e26566df7df5349f1ec6c167..9c19c478467afd626255286d892341aa603c1e96 100644 --- a/fsl/fsleyes/gl/glvolume.py +++ b/fsl/fsleyes/gl/glvolume.py @@ -81,10 +81,28 @@ class GLVolume(globject.GLImageObject): OpenGL version-specific module is used. The image data itself is stored as an :class:`.ImageTexture`. This ``ImageTexture`` is managed by the :mod:`.resources` module, so may be shared by many ``GLVolume`` instances. - The current colour map (defined by the :class:`.VolumeOpts.cmap` property) - is stored as a :class:`.ColourMapTexture`. A slice through the texture is + The current colour maps (defined by the :attr:`.VolumeOpts.cmap` and + :attr:`.VolumeOpts.negativeCmap` properties) are stored as + :class:`.ColourMapTexture` instances. A slice through the texture is rendered using six vertices, located at the respective corners of the - image bounds. + image bounds. + + + **Textures** + + + The ``GLVolume`` class uses three textures: + + - An :class:`.ImageTexture`, a 3D texture which contains image data. + This is bound to texture unit 0. + + - A :class:`.ColourMapTexture`, a 1D texture which contains the + colour map defined by the :attr:`.VolumeOpts.cmap` property. + This is bound to texture unit 1. + + - A :class:`.ColourMapTexture`, a 1D texture which contains the + colour map defined by the :attr:`.VolumeOpts.negativeCmap` property. + This is bound to texture unit 2. **Attributes** @@ -92,13 +110,17 @@ class GLVolume(globject.GLImageObject): The following attributes are available on a ``GLVolume`` instance: - ================= ======================================================= - ``imageTexture`` The :class:`.ImageTexture` which stores the image data. - ``colourTexture`` The :class:`.ColourMapTexture` used to store the - colour map. - ``texName`` A name used for the ``imageTexture`` and - ``colourTexture``. - ================= ======================================================= + ==================== ================================================= + ``imageTexture`` The :class:`.ImageTexture` which stores the image + data. + ``colourTexture`` The :class:`.ColourMapTexture` used to store the + colour map. + ``negColourTexture`` The :class:`.ColourMapTexture` used to store the + negative colour map. + ``texName`` A name used for the ``imageTexture``, + ``colourTexture``, and ``negColourTexture`. The + name for the latter is suffixed with ``'_neg'``. + ==================== ================================================= """ @@ -120,11 +142,13 @@ class GLVolume(globject.GLImageObject): # Create an image texture and a colour map texture self.texName = '{}_{}'.format(type(self).__name__, id(self.image)) - self.colourTexture = textures.ColourMapTexture(self.texName) - self.imageTexture = None - + self.imageTexture = None + self.colourTexture = textures.ColourMapTexture(self.texName) + self.negColourTexture = textures.ColourMapTexture( + '{}_neg'.format(self.texName)) + self.refreshImageTexture() - self.refreshColourTexture() + self.refreshColourTextures() fslgl.glvolume_funcs.init(self) @@ -138,14 +162,16 @@ class GLVolume(globject.GLImageObject): glresources.delete(self.imageTexture.getTextureName()) - self.colourTexture.destroy() + self.colourTexture .destroy() + self.negColourTexture.destroy() - self.imageTexture = None - self.colourTexture = None + self.imageTexture = None + self.colourTexture = None + self.negColourTexture = None self.removeDisplayListeners() - fslgl.glvolume_funcs.destroy(self) + fslgl.glvolume_funcs .destroy(self) globject.GLImageObject.destroy(self) @@ -168,7 +194,7 @@ class GLVolume(globject.GLImageObject): self.onUpdate() def colourUpdate(*a): - self.refreshColourTexture() + self.refreshColourTextures() fslgl.glvolume_funcs.updateShaderState(self) self.onUpdate() @@ -176,11 +202,6 @@ class GLVolume(globject.GLImageObject): fslgl.glvolume_funcs.updateShaderState(self) self.onUpdate() - def shaderCompile(*a): - fslgl.glvolume_funcs.compileShaders( self) - fslgl.glvolume_funcs.updateShaderState(self) - self.onUpdate() - def imageRefresh(*a): self.refreshImageTexture() fslgl.glvolume_funcs.updateShaderState(self) @@ -205,6 +226,9 @@ class GLVolume(globject.GLImageObject): opts .addListener('clippingRange', lName, shaderUpdate, weak=False) opts .addListener('invertClipping', lName, shaderUpdate, weak=False) opts .addListener('cmap', lName, colourUpdate, weak=False) + opts .addListener('negativeCmap', lName, colourUpdate, weak=False) + opts .addListener('enableNegativeCmap', + lName, colourUpdate, weak=False) opts .addListener('invert', lName, colourUpdate, weak=False) opts .addListener('volume', lName, imageUpdate, weak=False) opts .addListener('resolution', lName, imageUpdate, weak=False) @@ -239,16 +263,19 @@ class GLVolume(globject.GLImageObject): lName = self.name - display.removeListener( 'alpha', lName) - opts .removeListener( 'displayRange', lName) - opts .removeListener( 'clippingRange', lName) - opts .removeListener( 'invertClipping', lName) - opts .removeListener( 'cmap', lName) - opts .removeListener( 'invert', lName) - opts .removeListener( 'volume', lName) - opts .removeListener( 'resolution', lName) - opts .removeListener( 'interpolation', lName) - opts .removeListener( 'transform', lName) + display.removeListener( 'alpha', lName) + opts .removeListener( 'displayRange', lName) + opts .removeListener( 'clippingRange', lName) + opts .removeListener( 'invertClipping', lName) + opts .removeListener( 'cmap', lName) + opts .removeListener( 'negativeCmap', lName) + opts .removeListener( 'enableNegativeCmap', lName) + opts .removeListener( 'cmap', lName) + opts .removeListener( 'invert', lName) + opts .removeListener( 'volume', lName) + opts .removeListener( 'resolution', lName) + opts .removeListener( 'interpolation', lName) + opts .removeListener( 'transform', lName) if self.__syncListenersRegistered: opts.removeSyncChangeListener('volume', lName) @@ -298,15 +325,16 @@ class GLVolume(globject.GLImageObject): interp=interp) - def refreshColourTexture(self): - """Refreshes the :class:`.ColourMapTexture` used to colour image - voxels. + def refreshColourTextures(self): + """Refreshes the :class:`.ColourMapTexture` instances used to colour + image voxels. """ display = self.display opts = self.displayOpts alpha = display.alpha / 100.0 cmap = opts.cmap + negCmap = opts.negativeCmap invert = opts.invert dmin = opts.displayRange[0] dmax = opts.displayRange[1] @@ -316,6 +344,11 @@ class GLVolume(globject.GLImageObject): alpha=alpha, displayRange=(dmin, dmax)) + self.negColourTexture.set(cmap=negCmap, + invert=invert, + alpha=alpha, + displayRange=(dmin, dmax)) + def preDraw(self): """Binds the :class:`.ImageTexture` to ``GL_TEXTURE0`` and the @@ -324,8 +357,9 @@ class GLVolume(globject.GLImageObject): """ # Set up the image and colour textures - self.imageTexture .bindTexture(gl.GL_TEXTURE0) - self.colourTexture.bindTexture(gl.GL_TEXTURE1) + self.imageTexture .bindTexture(gl.GL_TEXTURE0) + self.colourTexture .bindTexture(gl.GL_TEXTURE1) + self.negColourTexture.bindTexture(gl.GL_TEXTURE2) fslgl.glvolume_funcs.preDraw(self) @@ -359,7 +393,8 @@ class GLVolume(globject.GLImageObject): version-dependent ``postDraw`` function. """ - self.imageTexture .unbindTexture() - self.colourTexture.unbindTexture() + self.imageTexture .unbindTexture() + self.colourTexture .unbindTexture() + self.negColourTexture.unbindTexture() fslgl.glvolume_funcs.postDraw(self)