diff --git a/TODO b/TODO
index 7674d753e8637a3a27e03f51c94e9dc87f4feb67..7796c8ed3074c7a116120b4168bb1e76125c85e0 100644
--- a/TODO
+++ b/TODO
@@ -82,17 +82,18 @@ Also, redo the way that global HasProps validation works - it is currently perfo
 ** PyInstaller menu bar not showing
 http://dvitonis.net/blog/2015/01/07/menu-bar-not-visible-when-building-pyqt-app-bundle-pyinstaller-mac-osx-mavericks-yosemite/
 * Bugs to fix
-** VolumeOpts DisplayRange<->Bricon synchronisation.
-Breaks when brightness/contrast/displayRange sync (between multiple displays)
-is turned off .. I think the sync logic has to occur somewhere other than the
-VolumeOpts class ...
+** Clipping range not working for some volumes in ~/analysis_prac_2015/rest/ICA/Group/groupmelodic_fix.ica/melodic_IC.nii.gz 
 ** LightBox - grid lines are drawn below canvas area where slices are drawn
 ** LocationPanel in lightbox view - voxel value lookup is wrong?
 ** Lightbox scrollbar under linux not visible?
-
 ** WONTFIX OSMesa render doesn't work with circle voxels
 Circle voxels were just an experiment anyway. Removed from code
 ** WONTFIX World location is clamped to voxel centre when voxel location changes. Not sure if this really needs to be fixed.
+** DONE Spline intrpolation doing weird things
+** DONE VolumeOpts DisplayRange<->Bricon synchronisation.
+Breaks when brightness/contrast/displayRange sync (between multiple displays)
+is turned off .. I think the sync logic has to occur somewhere other than the
+VolumeOpts class ...
 ** DONE SpacePanel throwing errors on av.nii.gz (and seemingly not deregistering listeners)
 ** DONE Image order synchronisation
 ** DONE Ortho canvas resizing
@@ -160,6 +161,15 @@ perhaps into a standalone module...
 ** DONE Graceful handling of bad input filenames
 ** DONE Aspect ratio on slicecanvas zoom, and panning is broken.
 * Little things
+** Arrow keys on number widgets
+** Buffer sharing 
+A gl/buffers.py module which checks for buffer capability. If buffer-capable,
+vertex data is copied to a buffer, and the buffer name/ID returned. Otherwise,
+the vertex data is returned unmodified.
+** Absolute clipping (e.g. clip [-3, 3])
+** Make zoom more flexible
+** Make ortho aspect ratio/zoom thing better
+** RunWindow should print to stdout
 ** Startup - ability to set $FSLDIR
 ** Time Series - option to 'hold' voxel time course
 You could have a list panel which lists the image and voxel coordinates of the
@@ -372,3 +382,61 @@ on the image display object.
 ** (Ged).    First/third angle orthographic projection. 
 ** (Paul).   Image groups - may make life easier for grouping images which belong together for analysis purposes.
 ** (Paul).   An integrated python shell with images available in-memory to manipulate.
+
+* February/March 2015 internal release
+** DONE Installation instructions on wiki
+*** Make sure they're up to date - test in a VM
+*** Usage - keyboard shortcuts
+** DONE OSX installer
+*** Make sure you disable GL error checking/logging
+*** Test screenshots
+*** Test GL14 and GL21 (shaders)
+*** Test two-stage rendering
+*** Test installing colour maps
+*** Test saving edited images
+*** Test atlas tools (this will only be available when it is run from command line)
+
+** DONE Linux compatibility
+*** DONE Combobox drop down lists in dialogs are shown beneath the dialog?!? This may only be occurring under X11/SSH/OSX
+*** WONTFIX Toolbars not being resized appropriately when the parent viewpanel is resized?
+Can't reproduce right now ..
+** WONTFIX Ludoweird interpolation effect - wtf?
+It's an artifact of the fact that interpolation is applied in voxel space, rather than mm space.
+** DONE Fix all 'listener still registered' warnings.
+** DONE Progress dialog during screen shot
+** DONE VNC/X11 ortho panning 
+** DONE Change 'profile' to 'mode'
+** DONE Volume Option to invert colour map
+** DONE Make ImageDisplayPanel change interpolation when transformation is changed
+** DONE Disable image display widgets for disabled images
+** DONE Rename 'sync image order' -> 'sync overlay order'
+** DONE Remove 'sync volume' option
+** DONE -ISH Make VNC two stage render approach stablish
+** DONE Add 'blue-lightblue' colour map
+** DONE Rename 'autumn' to 'red-yellow' (change colour map)
+** DONE Colour map ordering
+** DONE Change 'ss' back to 'twostage'
+** DONE Command line
+** DONE? Little things
+*** DONE ViewPanel: New toolbars in a lower layer
+*** DONE Number spinboxes are no longer clamped
+*** Keyboard on number widgets .. Should be working by default?
+** DONE Default layout
+** DONE UI design
+Good enough for the time being
+** DONE Support for double precision images
+** DONE Histogram
+** DONE Atlas tools
+** DONE Keyboard shortcut for pan mode - can't use middle click when using a shitty laptop trackpad
+** DONE Git release pipeline
+We now have an 'oxford' branch, which is linked to the jalapeno installation.
+When you want to 'release' something, merge from the master branch to the
+oxford branch, and push to jalapeno
+
+** DONE Offscreen rendering
+** Other things/stretch objectives:
+*** Tooltips
+*** Save/restore window layout
+*** Movie mode
+*** Document all the code
+*** Fix all the bugs
diff --git a/fsl/data/image.py b/fsl/data/image.py
index e2af8913c643ed3714d5ae9b654f50f15f0f6c63..e757d020deb6a2f6d83fc2ca8c06bdba0b17bb12 100644
--- a/fsl/data/image.py
+++ b/fsl/data/image.py
@@ -67,9 +67,10 @@ class Image(props.HasProperties):
 
     imageType = props.Choice(
         collections.OrderedDict([
-            ('volume', '3D/4D volume'),
-            ('mask',   '3D/4D mask image'),
-            ('vector', '3-direction vector image')]),
+            ('volume',     '3D/4D volume'),
+            ('mask',       '3D/4D mask image'),
+            ('rgbvector',  '3-direction vector image (RGB)'),
+            ('linevector', '3-direction vector image (Line)')]),
         default='volume')
     """This property defines the type of image data."""
 
@@ -211,8 +212,8 @@ class Image(props.HasProperties):
         shape = data.shape
         
         for i in reversed(range(len(shape))):
-            if shape[i - 1] == 1:
-                data = data.squeeze(axis=i - 1)
+            if shape[i] == 1: data = data.squeeze(axis=i)
+            else:             break
 
         data.flags.writeable = False
 
diff --git a/fsl/data/strings.py b/fsl/data/strings.py
index 6a25de1e7eb7c4a0900800d85c687a42e6bff66b..187056ab5d7bbca42f2d181360b41a3382f9d461 100644
--- a/fsl/data/strings.py
+++ b/fsl/data/strings.py
@@ -157,7 +157,7 @@ properties = TypeDict({
 
     'SceneOpts.showCursor'         : 'Show location cursor',
     'SceneOpts.showColourBar'      : 'Show colour bar',
-    'SceneOpts.twoStageRender'     : 'Two-stage rendering',
+    'SceneOpts.performance'        : 'Rendering performance',
     'SceneOpts.zoom'               : 'Zoom',
     'SceneOpts.colourBarLocation'  : 'Colour bar location',
     'SceneOpts.colourBarLabelSide' : 'Colour bar label side',
@@ -212,7 +212,6 @@ properties = TypeDict({
     'MaskOpts.invert'         : 'Invert',
     'MaskOpts.threshold'      : 'Threshold',
 
-    'VectorOpts.displayMode'   : 'Display mode',
     'VectorOpts.xColour'       : 'X Colour',
     'VectorOpts.yColour'       : 'Y Colour',
     'VectorOpts.zColour'       : 'Z Colour',
@@ -222,6 +221,9 @@ properties = TypeDict({
     'VectorOpts.suppressZ'     : 'Suppress Z value',
     'VectorOpts.modulate'      : 'Modulate by',
     'VectorOpts.modThreshold'  : 'Modulation threshold',
+
+    'LineVectorOpts.directed'  : 'Interpret vectors as directed',
+    'LineVectorOpts.lineWidth' : 'Line width',
 })
 
 
@@ -257,6 +259,12 @@ choices = TypeDict({
     'SceneOpts.colourBarLocation.left'   : 'Left',
     'SceneOpts.colourBarLocation.right'  : 'Right',
 
+    'SceneOpts.performance.1' : 'Fastest',
+    'SceneOpts.performance.2' : 'Faster',
+    'SceneOpts.performance.3' : 'Good looking',
+    'SceneOpts.performance.4' : 'Better looking',
+    'SceneOpts.performance.5' : 'Best looking',
+
     'HistogramPanel.dataRange.min' : 'Min.',
     'HistogramPanel.dataRange.max' : 'Max.',
     
diff --git a/fsl/fslview/colourmaps.py b/fsl/fslview/colourmaps.py
index de39a83b869ba25540f8c024d9e1ec7bea60c1bc..d69dca402d452923d91e6079c569c04191db38c5 100644
--- a/fsl/fslview/colourmaps.py
+++ b/fsl/fslview/colourmaps.py
@@ -39,6 +39,16 @@ This module provides a number of functions, the most important of which are:
                               loads the data and registers it  with
                               :mod:`matplotlib`.
 
+
+Some utility functions are also kept in this module, related to calculating
+the relationship between a data display range, and brightness/contrast
+scales:
+
+ - :func:`displayRangeToBricon`: Given a data range, converts a display range
+                                 to brightness/contrast values.
+
+ - :func:`briconToDisplayRange`: Given a data range, converts brigtness/
+                                 contrast values to a display range.
 """
 
 import glob
@@ -238,3 +248,81 @@ def initColourMaps():
         except:
             log.warn('Error processing custom colour '
                      'map file: {}'.format(cmapFile))
+
+
+def briconToDisplayRange(dataRange, brightness, contrast):
+    """Converts the given brightness/contrast values to a display range,
+    given the data range.
+
+    :arg dataRange:  The full range of the data being displayed, a
+                     (min, max) tuple.
+    
+    :arg brightness: A brightness value between 0 and 1.
+    
+    :arg contrast:   A contrast value between 0 and 1.
+    """
+
+    # Turn the given bricon values into
+    # values between 1 and 0 (inverted)
+    brightness = 1.0 - brightness
+    contrast   = 1.0 - contrast
+
+    dmin, dmax = dataRange
+    drange     = dmax - dmin
+    dmid       = dmin + 0.5 * drange
+
+    # The brightness is applied as a linear offset,
+    # with 0.5 equivalent to an offset of 0.0.                
+    offset = (brightness * 2 - 1) * drange
+
+    # If the contrast lies between 0.0 and 0.5, it is
+    # applied to the colour as a linear scaling factor.
+    scale = contrast * 2
+
+    # If the contrast lies between 0.5 and 1, it
+    # is applied as an exponential scaling factor,
+    # so lower values (closer to 0.5) have less of
+    # an effect than higher values (closer to 1.0).
+    if contrast > 0.5:
+        scale += np.exp((contrast - 0.5) * 6) - 1
+
+    # Calculate the new display range, keeping it
+    # centered in the middle of the data range
+    # (but offset according to the brightness)
+    dlo = (dmid + offset) - 0.5 * drange * scale 
+    dhi = (dmid + offset) + 0.5 * drange * scale
+
+    return dlo, dhi
+
+
+def displayRangeToBricon(dataRange, displayRange):
+    """Converts the given brightness/contrast values to a display range,
+    given the data range.
+
+    :arg dataRange:    The full range of the data being displayed, a
+                       (min, max) tuple.
+    
+    :arg displayRange: A (min, max) tuple containing the display range.
+    """    
+
+    dmin, dmax = dataRange
+    dlo,  dhi  = displayRange
+    drange     = dmax - dmin
+    dmid       = dmin + 0.5 * drange
+
+    # These are inversions of the equations in
+    # the briconToDisplayRange function above,
+    # which calculate the display ranges from
+    # the bricon offset/scale
+    offset = dlo + 0.5 * (dhi - dlo) - dmid
+    scale  = (dhi - dlo) / drange
+
+    brightness = 0.5 * (offset / drange + 1)
+
+    if scale <= 1: contrast = scale / 2.0
+    else:          contrast = np.log(scale + 1) / 6.0 + 0.5
+
+    brightness = 1.0 - brightness
+    contrast   = 1.0 - contrast
+
+    return brightness, contrast
diff --git a/fsl/fslview/displaycontext/display.py b/fsl/fslview/displaycontext/display.py
index 81367cac7057a5b92e236da03a209902c925caf5..80b63f148e93a93976d228b077a97c97ead4eb83 100644
--- a/fsl/fslview/displaycontext/display.py
+++ b/fsl/fslview/displaycontext/display.py
@@ -125,6 +125,10 @@ class Display(props.SyncableHasProperties):
     
     contrast   = props.Percentage()
 
+
+    softwareMode = props.Boolean(default=False)
+    """If possible, optimise for software-based rendering."""
+
         
     def is4DImage(self):
         """Returns ``True`` if this image is 4D, ``False`` otherwise.
@@ -185,8 +189,7 @@ class Display(props.SyncableHasProperties):
                       'volume',
                       'resolution',
                       'transform',
-                      'brightness',
-                      'contrast',
+                      'softwareMode', 
                       'imageType'])
 
         # Set up listeners after caling Syncabole.__init__,
@@ -350,9 +353,10 @@ class Display(props.SyncableHasProperties):
             oParent = self.getParent().getDisplayOpts()
 
         optsMap = {
-            'volume' : volumeopts.VolumeOpts,
-            'vector' : vectoropts.VectorOpts,
-            'mask'   : maskopts.  MaskOpts
+            'volume'     : volumeopts.VolumeOpts,
+            'rgbvector'  : vectoropts.VectorOpts,
+            'linevector' : vectoropts.LineVectorOpts,
+            'mask'       : maskopts.  MaskOpts
         }
 
         optType = optsMap[self.imageType]
diff --git a/fsl/fslview/displaycontext/sceneopts.py b/fsl/fslview/displaycontext/sceneopts.py
index d5191df213b2974f6b21d52e964f5aaff0bfd6e6..7e725c8cb258de603009caf2294a30bea2a2d953 100644
--- a/fsl/fslview/displaycontext/sceneopts.py
+++ b/fsl/fslview/displaycontext/sceneopts.py
@@ -9,18 +9,26 @@ import copy
 
 import props
 
+import fsl.fslview.gl.slicecanvas     as slicecanvas
 import fsl.fslview.gl.colourbarcanvas as colourbarcanvas
 
 import fsl.data.strings as strings
 
 class SceneOpts(props.HasProperties):
+    """The ``SceneOpts`` class defines settings which are applied to
+    :class:`.CanvasPanel` views.
+    """
+
     
     showCursor = props.Boolean(default=True)
 
+    
     zoom = props.Percentage(minval=10, maxval=1000, default=100, clamped=True)
 
+    
     showColourBar = props.Boolean(default=False)
 
+    
     colourBarLocation  = props.Choice(
         ('top', 'bottom', 'left', 'right'),
         labels=[strings.choices['SceneOpts.colourBarLocation.top'],
@@ -31,9 +39,92 @@ class SceneOpts(props.HasProperties):
     
     colourBarLabelSide = copy.copy(colourbarcanvas.ColourBarCanvas.labelSide)
 
-    twoStageRender = props.Boolean(default=False)
+    
+    performance = props.Choice(
+        (1, 2, 3, 4, 5),
+        default=5,
+        labels=[strings.choices['SceneOpts.performance.1'],
+                strings.choices['SceneOpts.performance.2'],
+                strings.choices['SceneOpts.performance.3'],
+                strings.choices['SceneOpts.performance.4'],
+                strings.choices['SceneOpts.performance.5']])
+    """User controllable performacne setting.
+
+    This property is linked to the :attr:`twoStageRender`,
+    :attr:`resolutionLimit`, and :attr:`softwareMode` properties. Setting the
+    performance to a low value will result in faster rendering time, at the
+    cost of reduced features, and poorer rendering quality.
+
+    See the :meth:`_onPerformanceChange` method.
+    """
+
+
+    resolutionLimit = copy.copy(slicecanvas.SliceCanvas.resolutionLimit)
+    """The highest resolution at which any image should be displayed.
+
+    See :attr:`~fsl.fslview.gl.slicecanvas.SliceCanvas.resolutionLimit` and
+    :attr:`~fsl.fslview.displaycontext.display.Display.resolution`.
+    """
+    
+
+    renderMode = copy.copy(slicecanvas.SliceCanvas.renderMode)
     """Enable two-stage rendering, useful for low-performance graphics cards/
     software rendering.
 
     See :attr:`~fsl.fslview.gl.slicecanvas.SliceCanvas.twoStageRender`.
     """
+
+
+    
+    softwareMode = copy.copy(slicecanvas.SliceCanvas.softwareMode)
+    """If ``True``, all images should be displayed in a mode optimised for
+    software based rendering.
+
+    The definition of 'software mode' is intentionally left unspecified, but
+    will generally mean using GL vertex/fragment shaders which are optimised
+    for speed, possibly at the cost of omitting some features.
+
+    See :attr:`.SliceCanvas.softwareMode` and :attr:`.Display.softwareMode`.
+    """
+
+
+    def __init__(self):
+        
+        name = '{}_{}'.format(type(self).__name__, id(self))
+        self.addListener('performance', name, self._onPerformanceChange)
+        
+        self._onPerformanceChange()
+
+
+    def _onPerformanceChange(self, *a):
+        """Called when the :attr:`performance` property changes.
+
+        Changes the values of the :attr:`renderMode`, :attr:`softwareMode`
+        and :attr:`resolutionLimit` properties accoridng to the performance
+        setting.
+        """
+
+        if   self.performance == 5:
+            self.renderMode      = 'onscreen'
+            self.softwareMode    = False
+            self.resolutionLimit = 0
+            
+        elif self.performance == 4:
+            self.renderMode      = 'onscreen'
+            self.softwareMode    = True
+            self.resolutionLimit = 0
+
+        elif self.performance == 3:
+            self.renderMode      = 'offscreen'
+            self.softwareMode    = True
+            self.resolutionLimit = 0 
+            
+        elif self.performance == 2:
+            self.renderMode      = 'prerender'
+            self.softwareMode    = True
+            self.resolutionLimit = 0
+
+        elif self.performance == 1:
+            self.renderMode      = 'prerender'
+            self.softwareMode    = True
+            self.resolutionLimit = 1
diff --git a/fsl/fslview/displaycontext/vectoropts.py b/fsl/fslview/displaycontext/vectoropts.py
index 47cc6e1b779eb2842c4b6f6b30e7b80064df795c..4fb40e7a588831dc2b3e17aa3542c0827fd58ea3 100644
--- a/fsl/fslview/displaycontext/vectoropts.py
+++ b/fsl/fslview/displaycontext/vectoropts.py
@@ -16,13 +16,6 @@ import display          as fsldisplay
 
 class VectorOpts(fsldisplay.DisplayOpts):
 
-    displayMode = props.Choice(
-        ('line', 'rgb'),
-        default='rgb',
-        labels=[strings.choices['VectorOpts.displayType.line'],
-                strings.choices['VectorOpts.displayType.rgb']])
-    """Mode in which the ``GLVector`` instance is to be displayed."""
-
 
     xColour = props.Colour(default=(1.0, 0.0, 0.0))
     """Colour used to represent the X vector magnitude."""
@@ -59,7 +52,14 @@ class VectorOpts(fsldisplay.DisplayOpts):
     """Hide voxels for which the modulation value is below this threshold."""
 
     
-    def __init__(self, image, display, imageList, displayCtx, parent=None):
+    def __init__(self,
+                 image,
+                 display,
+                 imageList,
+                 displayCtx,
+                 parent=None,
+                 *args,
+                 **kwargs):
         """Create a ``VectorOpts`` instance for the given image.
 
         See the :class:`~fsl.fslview.displaycontext.display.DisplayOpts`
@@ -70,7 +70,9 @@ class VectorOpts(fsldisplay.DisplayOpts):
                                         display,
                                         imageList,
                                         displayCtx,
-                                        parent)
+                                        parent,
+                                        *args,
+                                        **kwargs)
 
         imageList.addListener('images', self.name, self.imageListChanged)
         self.imageListChanged()
@@ -120,3 +122,24 @@ class VectorOpts(fsldisplay.DisplayOpts):
 
         if modVal in images: self.modulate = modVal
         else:                self.modulate = 'none'
+
+
+# TODO RGBVector/LineVector subclasses for any type
+# specific options (e.g. line width for linevector)
+
+class LineVectorOpts(VectorOpts):
+
+    lineWidth = props.Int(minval=1, maxval=10, default=1)
+
+    directed  = props.Boolean(default=False)
+    """
+
+    The directed property cannot be unbound across multiple LineVectorOpts
+    instances, as it affects the OpenGL representation
+    """
+
+    def __init__(self, *args, **kwargs):
+
+        kwargs['nounbind'] = ['directed']
+
+        VectorOpts.__init__(self, *args, **kwargs)
diff --git a/fsl/fslview/displaycontext/volumeopts.py b/fsl/fslview/displaycontext/volumeopts.py
index 9b0a1d0db9d9130dacd91f4bc48f7de7d2e6a5cb..91b24539f38425fd88418b87eee2c54be6994b32 100644
--- a/fsl/fslview/displaycontext/volumeopts.py
+++ b/fsl/fslview/displaycontext/volumeopts.py
@@ -139,121 +139,127 @@ class VolumeOpts(fsldisplay.DisplayOpts):
                                         display,
                                         imageList,
                                         displayCtx,
-                                        parent,
-                                        nounbind=('displayRange'))
-
-        # Bricon values are synchronised with
-        # displayRange values on the parent
-        # VolumeOpts instance. If these listeners
-        # are registered on child instances, and
-        # there are multiple children, horrible
-        # semi-infniite recursive listener callback
-        # madness will entail ...
-        #
-        # TODO Problem here is that if a child
-        #      instance disables synchronisation
-        #      on brightness/contrast with the
-        #      parent, the bricon-displayRange
-        #      synchronsiation no longer occurs
-        #      on the child .. This is why
-        #      displayRange currently cannot be
-        #      unbound between parent/child
-        #      instances (constructor above). 
-        #      Same goes for Display.bricon
-        #      properties
-        
-        if parent is None:
+                                        parent)
+
+        # The displayRange property of every child VolumeOpts
+        # instance is linked to the corresponding 
+        # Display.brightness/contrast properties, so changes
+        # in one are reflected in the other.
+        if parent is not None:
             display.addListener('brightness', self.name, self.briconChanged)
             display.addListener('contrast',   self.name, self.briconChanged)
             self   .addListener('displayRange',
                                 self.name,
                                 self.displayRangeChanged)
 
+            # Because displayRange and bri/con are intrinsically
+            # linked, it makes no sense to let the user sync/unsync
+            # them independently. So here we are binding the boolean
+            # sync properties which control whether the dRange/bricon
+            # properties are synced with their parent. So when one
+            # property is synced/unsynced, the other ones are too.
+            self.bindProps(self   .getSyncPropertyName('displayRange'),
+                           display,
+                           display.getSyncPropertyName('brightness'))
+            self.bindProps(self   .getSyncPropertyName('displayRange'), 
+                           display,
+                           display.getSyncPropertyName('contrast')) 
+
     def destroy(self):
-        self.display.removeListener('brightness',   self.name)
-        self.display.removeListener('contrast',     self.name)
-        self        .removeListener('displayRange', self.name)
 
+        if self.getParent() is not None:
+            display = self.display
+            display.removeListener('brightness',   self.name)
+            display.removeListener('contrast',     self.name)
+            self   .removeListener('displayRange', self.name)
+            self.unbindProps(self   .getSyncPropertyName('displayRange'),
+                             display,
+                             display.getSyncPropertyName('brightness'))
+            self.unbindProps(self   .getSyncPropertyName('displayRange'), 
+                             display,
+                             display.getSyncPropertyName('contrast')) 
+
+
+    def __toggleListeners(self, enable=True):
+        """This method enables/disables the property listeners which
+        are registered on the :attr:`displayRange` and
+        :attr:`.Display.brightness`/:attr:`.Display.contrast`/ properties.
+        
+        Because these properties are linked via the :meth:`displayRangeChanged`
+        and :meth:`briconChanged` methods, we need to be careful about avoiding
+        recursive callbacks.
+
+        Furthermore, because the properties of both :class:`VolumeOpts` and
+        :class:`.Display` instances are possibly synchronised to a parent
+        instance (which in turn is synchronised to other children), we need to
+        make sure that the property listeners on these other sibling instances
+        are not called when our own property values change. So this method
+        disables/enables the property listeners on all sibling ``VolumeOpts``
+        and ``Display`` instances.
+        """
+
+        parent = self.getParent()
+
+        # this is the parent instance
+        if parent is None:
+            return
+
+        # The parent.getChildren() method will
+        # contain this VolumeOpts instance,
+        # so the below loop toggles listeners
+        # for this instance, the parent instance,
+        # and all of the other children of the
+        # parent
+        peers  = [parent] + parent.getChildren()
+
+        for peer in peers:
+
+            if enable:
+                peer.display.enableListener('brightness',   peer.name)
+                peer.display.enableListener('contrast',     peer.name)
+                peer        .enableListener('displayRange', peer.name)
+            else:
+                peer.display.disableListener('brightness',   peer.name)
+                peer.display.disableListener('contrast',     peer.name)
+                peer        .disableListener('displayRange', peer.name) 
+                
 
     def briconChanged(self, *a):
         """Called when the ``brightness``/``contrast`` properties of the
         :class:`~fsl.fslview.displaycontext.display.Display` instance change.
         
         Updates the :attr:`displayRange` property accordingly.
+
+        See :func:`.colourmaps.briconToDisplayRange`.
         """
 
-        display = self.display
-
-        # Turn the bricon percentages into
-        # values between 1 and 0 (inverted)
-        brightness = 1 - self.display.brightness / 100.0
-        contrast   = 1 - self.display.contrast   / 100.0
-
-        dmin, dmax = self.dataMin, self.dataMax
-        drange     = dmax - dmin
-        dmid       = dmin + 0.5 * drange
-
-        # The brightness is applied as a linear offset,
-        # with 0.5 equivalent to an offset of 0.0.                
-        offset = (brightness * 2 - 1) * drange
-
-        # If the contrast lies between 0.0 and 0.5, it is
-        # applied to the colour as a linear scaling factor.
-        scale = contrast * 2
-
-        # If the contrast lies between 0.5 and 0.1, it
-        # is applied as an exponential scaling factor,
-        # so lower values (closer to 0.5) have less of
-        # an effect than higher values (closer to 1.0).
-        if contrast > 0.5:
-            scale += np.exp((contrast - 0.5) * 6) - 1
-            
-        # Calculate the new display range, keeping it
-        # centered in the middle of the data range
-        # (but offset according to the brightness)
-        dlo = (dmid + offset) - 0.5 * drange * scale 
-        dhi = (dmid + offset) + 0.5 * drange * scale
-
-        self   .disableListener('displayRange', self.name)
-        display.disableListener('brightness',   self.name)
-        display.disableListener('contrast',     self.name)
-        
+        dlo, dhi = fslcm.briconToDisplayRange(
+            (self.dataMin, self.dataMax),
+            self.display.brightness / 100.0,
+            self.display.contrast   / 100.0)
+
+        self.__toggleListeners(False)
         self.displayRange.x = [dlo, dhi]
-        
-        self   .enableListener('displayRange', self.name)
-        display.enableListener('brightness',   self.name)
-        display.enableListener('contrast',     self.name) 
+        self.__toggleListeners(True)
 
         
     def displayRangeChanged(self, *a):
+        """Called when the `attr`:displayRange: property changes.
 
-        display    = self.display
-        
-        dmin, dmax = self.dataMin, self.dataMax
-        drange     = dmax - dmin
-        dmid       = dmin + 0.5 * drange
-
-        dlo, dhi = self.displayRange.x
+        Updates the :attr:`.Display.brightness` and :attr:`.Display.contrast`
+        properties accordingly.
 
-        # Inversions of the equations in briconChanged
-        # above, which calculate the display ranges
-        # from the bricon offset/scale
-        offset = dlo + 0.5 * (dhi - dlo) - dmid
-        scale  = (dhi - dlo) / drange
-
-        brightness = 0.5 * (offset / drange + 1)
-
-        if scale <= 1: contrast = scale / 2.0
-        else:          contrast = np.log(scale + 1) / 6.0 + 0.5
+        See :func:`.colourmaps.displayRangeToBricon`.
+        """
 
-        self   .disableListener('displayRange', self.name)
-        display.disableListener('brightness',   self.name)
-        display.disableListener('contrast',     self.name)
+        brightness, contrast = fslcm.displayRangeToBricon(
+            (self.dataMin, self.dataMax),
+            self.displayRange.x)
+        
+        self.__toggleListeners(False)
 
         # update bricon
-        display.brightness = 100 - brightness * 100
-        display.contrast   = 100 - contrast   * 100
+        self.display.brightness = 100 - brightness * 100
+        self.display.contrast   = 100 - contrast   * 100
 
-        self   .enableListener('displayRange', self.name)
-        display.enableListener('brightness',   self.name)
-        display.enableListener('contrast',     self.name)
+        self.__toggleListeners(True)
diff --git a/fsl/fslview/frame.py b/fsl/fslview/frame.py
index 1ed409d944f8e4824af59bdd3627412608a9cbf4..4b2429eb261824b7cb127440c806fad13ce4b380 100644
--- a/fsl/fslview/frame.py
+++ b/fsl/fslview/frame.py
@@ -354,11 +354,26 @@ class FSLViewFrame(wx.Frame):
         else:
             self.Centre()
 
-
         # TODO Restore the previous view panel layout
         if restore:
+
             self.addViewPanel(views.OrthoPanel)
 
+            viewPanel = self.getViewPanels()[0][0]
+
+            # Set up a default for ortho views
+            # layout (this will hopefully eventually
+            # be done by the FSLViewFrame instance)
+            import fsl.fslview.controls.imagelistpanel      as ilp
+            import fsl.fslview.controls.locationpanel       as lop
+            import fsl.fslview.controls.imagedisplaytoolbar as idt
+            import fsl.fslview.controls.orthotoolbar        as ot
+
+            viewPanel.togglePanel(ilp.ImageListPanel)
+            viewPanel.togglePanel(lop.LocationPanel)
+            viewPanel.togglePanel(idt.ImageDisplayToolBar, False, viewPanel)
+            viewPanel.togglePanel(ot .OrthoToolBar,        False, viewPanel) 
+
             
     def _makeMenuBar(self):
         """Constructs a bunch of menu items for working with the given
diff --git a/fsl/fslview/gl/__init__.py b/fsl/fslview/gl/__init__.py
index 76f26233f6541287cd2a957186f7cc8745ea81bd..c39f170321c82b039e30dc441a01ef8ad61d20cd 100644
--- a/fsl/fslview/gl/__init__.py
+++ b/fsl/fslview/gl/__init__.py
@@ -179,9 +179,7 @@ def bootstrap(glVersion=None):
         verstr, renderer))
 
     # If we're using a software based renderer,
-    # make two-stage rendering (off-screen rendering
-    # to a texture, then rendering said texture to the
-    # screen) the default.
+    # reduce the default performance settings
     # 
     # There doesn't seem to be any quantitative
     # method for determining whether we are using
@@ -189,29 +187,25 @@ def bootstrap(glVersion=None):
     # necessary. 
     if 'software' in renderer.lower():
         
-        log.debug('Software-based rendering detected - two-stage '
-                  'rendering will be the default method.')
+        log.debug('Software-based rendering detected - '
+                  'lowering default performance settings.')
 
-        import fsl.fslview.displaycontext.display    as di
-        import fsl.fslview.displaycontext.vectoropts as vo
-        import fsl.fslview.displaycontext.sceneopts  as so
-        import fsl.fslview.gl.slicecanvas            as sc
+        import fsl.fslview.displaycontext.display   as di
+        import fsl.fslview.displaycontext.sceneopts as so
 
-        # Make two-stage rendering the default
-        so.SceneOpts  .twoStageRender.setConstraint(None, 'default',  True)
-        sc.SliceCanvas.twoStageRender.setConstraint(None, 'default',  True)
+        so.SceneOpts.performance.setConstraint(None, 'default', 2)
 
         # And disable some fancy options - spline
         # may have been disabled above, so absorb
-        # the IndexError if it occurs
-        vo.VectorOpts  .displayMode  .removeChoice('line')
+        # the ValueError if it occurs
         try: di.Display.interpolation.removeChoice('spline')
-        except IndexError: pass
+        except ValueError: pass
 
-    thismod.GL_VERSION     = verstr
-    thismod.glvolume_funcs = glpkg.glvolume_funcs
-    thismod.glvector_funcs = glpkg.glvector_funcs
-    thismod._bootstrapped  = True
+    thismod.GL_VERSION         = verstr
+    thismod.glvolume_funcs     = glpkg.glvolume_funcs
+    thismod.glrgbvector_funcs  = glpkg.glrgbvector_funcs
+    thismod.gllinevector_funcs = glpkg.gllinevector_funcs
+    thismod._bootstrapped      = True
 
 
 def getWXGLContext(parent=None):
diff --git a/fsl/fslview/gl/annotations.py b/fsl/fslview/gl/annotations.py
index 521b8ad43f10d274e236fa6aa66de0112d85b2a5..b88850cd053dd0cca8eba5f0b192fe0f16e199fd 100644
--- a/fsl/fslview/gl/annotations.py
+++ b/fsl/fslview/gl/annotations.py
@@ -21,6 +21,7 @@ import OpenGL.GL as gl
 
 
 import fsl.fslview.gl.globject as globject
+import fsl.fslview.gl.routines as glroutines
 import fsl.fslview.gl.textures as textures
 import fsl.utils.transform     as transform
 
@@ -166,9 +167,6 @@ class Annotations(object):
             gl.glMultMatrixf(xform.ravel('C')) 
 
         for obj in objs:
-
-            if not obj.ready():
-                continue
             
             obj.setAxes(self._xax, self._yax)
 
@@ -388,7 +386,7 @@ class VoxelGrid(AnnotationObject):
                 off = 0
             voxels[:, ax] += off + self.offsets[ax]
 
-        verts, idxs = globject.voxelGrid(voxels, xax, yax, 1, 1)
+        verts, idxs = glroutines.voxelGrid(voxels, xax, yax, 1, 1)
 
         gl.glVertexPointer(3, gl.GL_FLOAT, 0, verts.ravel('C')) 
         gl.glDrawElements(gl.GL_LINES, len(idxs), gl.GL_UNSIGNED_INT, idxs)
@@ -417,35 +415,33 @@ class VoxelSelection(AnnotationObject):
         self.voxToDisplayMat = voxToDisplayMat
         self.offsets         = offsets
         
-        self.texture = textures.getTexture(
-            selection,
-            '{}_{}'.format(type(self).__name__, id(selection)))
+        self.texture = textures.SelectionTexture(
+            '{}_{}'.format(type(self).__name__, id(selection)),
+            selection)
+
+    def destroy(self):
+        self.texture.destroy()
+        self.texture = None
 
 
     def draw(self, zpos):
 
         xax   = self.xax
         yax   = self.yax
-        zax   = self.zax
         shape = self.selection.selection.shape
 
-        verts, _ = globject.slice2D(shape,
-                                    xax,
-                                    yax,
-                                    self.voxToDisplayMat)
-
-        verts[:, zax] = zpos
-
-        texs  = transform.transform(verts, self.displayToVoxMat) + 0.5
-        texs /= shape
+        verts, _, texs = glroutines.slice2D(shape,
+                                            xax,
+                                            yax,
+                                            zpos,
+                                            self.voxToDisplayMat,
+                                            self.displayToVoxMat)
 
         verts = np.array(verts, dtype=np.float32).ravel('C')
         texs  = np.array(texs,  dtype=np.float32).ravel('C')
-        idxs  = np.arange(4,    dtype=np.uint32)
 
-        gl.glActiveTexture(gl.GL_TEXTURE0)
-        gl.glEnable(gl.GL_TEXTURE_3D)
-        gl.glBindTexture(gl.GL_TEXTURE_3D, self.texture.texture)
+        self.texture.bindTexture(gl.GL_TEXTURE0)
+
         gl.glTexEnvf(gl.GL_TEXTURE_ENV, gl.GL_TEXTURE_ENV_MODE, gl.GL_MODULATE)
 
         # gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
@@ -453,13 +449,12 @@ class VoxelSelection(AnnotationObject):
 
         gl.glVertexPointer(  3, gl.GL_FLOAT, 0, verts)
         gl.glTexCoordPointer(3, gl.GL_FLOAT, 0, texs)
-        gl.glDrawElements(gl.GL_TRIANGLE_STRIP, 4, gl.GL_UNSIGNED_INT, idxs)
+        gl.glDrawArrays(gl.GL_TRIANGLES, 0, 6)
 
         # gl.glDisableClientState(gl.GL_VERTEX_ARRAY)
-        gl.glDisableClientState(gl.GL_TEXTURE_COORD_ARRAY)        
+        gl.glDisableClientState(gl.GL_TEXTURE_COORD_ARRAY)
 
-        gl.glBindTexture(gl.GL_TEXTURE_3D, 0)
-        gl.glDisable(gl.GL_TEXTURE_3D)
+        self.texture.unbindTexture()
         
         
 # class Text(AnnotationObject) ?
diff --git a/fsl/fslview/gl/colourbarcanvas.py b/fsl/fslview/gl/colourbarcanvas.py
index 0ff1e882afa46c4f8fb3779387b796d5dbf919a5..84995b1aed4e282ab4f8263f7627007d165eca44 100644
--- a/fsl/fslview/gl/colourbarcanvas.py
+++ b/fsl/fslview/gl/colourbarcanvas.py
@@ -24,6 +24,7 @@ import props
 
 import fsl.utils.colourbarbitmap as cbarbmp
 import fsl.data.strings          as strings
+import fsl.fslview.gl.textures   as textures
 
 
 class ColourBarCanvas(props.HasProperties):
@@ -116,42 +117,18 @@ class ColourBarCanvas(props.HasProperties):
                 self.label,
                 self.orientation,
                 labelSide)
-            bitmap = np.flipud(bitmap)
 
         if self._tex is None:
-            self._tex = gl.glGenTextures(1)
-            log.debug('Created GL texture: {}'.format(self._tex))
-
-        # Allow textures of any size
-        gl.glPixelStorei(gl.GL_PACK_ALIGNMENT,   1)
-        gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1)
-
-        gl.glBindTexture(  gl.GL_TEXTURE_2D, self._tex)
-        gl.glTexParameteri(gl.GL_TEXTURE_2D,
-                           gl.GL_TEXTURE_MAG_FILTER,
-                           gl.GL_LINEAR)
-        gl.glTexParameteri(gl.GL_TEXTURE_2D,
-                           gl.GL_TEXTURE_MIN_FILTER,
-                           gl.GL_LINEAR)
-        gl.glTexParameteri(gl.GL_TEXTURE_2D,
-                           gl.GL_TEXTURE_WRAP_S,
-                           gl.GL_CLAMP_TO_BORDER)
-        gl.glTexParameteri(gl.GL_TEXTURE_2D,
-                           gl.GL_TEXTURE_WRAP_T,
-                           gl.GL_CLAMP_TO_BORDER)
-
-        bitmap = bitmap.ravel('C')
-
-        gl.glTexImage2D(gl.GL_TEXTURE_2D,
-                        0,
-                        gl.GL_RGBA8,
-                        w,
-                        h,
-                        0,
-                        gl.GL_RGBA,
-                        gl.GL_UNSIGNED_BYTE,
-                        bitmap)
-        gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
+            self._tex = textures.Texture2D('{}_{}'.format(
+                type(self).__name__, id(self)), gl.GL_LINEAR)
+
+        # The bitmap has shape W*H*4, but the
+        # Texture2D instance needs it in shape
+        # 4*W*H
+        bitmap = np.fliplr(bitmap).transpose([2, 0, 1])
+            
+        self._tex.setData(bitmap)
+        self._tex.refresh()
 
 
     def _draw(self):
@@ -175,26 +152,6 @@ class ColourBarCanvas(props.HasProperties):
         gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
         gl.glShadeModel(gl.GL_FLAT)
 
-        gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1)
-        gl.glPixelStorei(gl.GL_PACK_ALIGNMENT,   1)
-
-        gl.glActiveTexture(gl.GL_TEXTURE0) 
-        gl.glEnable(gl.GL_TEXTURE_2D)
-        gl.glTexEnvf(gl.GL_TEXTURE_ENV, gl.GL_TEXTURE_ENV_MODE, gl.GL_REPLACE) 
-        gl.glBindTexture(gl.GL_TEXTURE_2D, self._tex)
-
-        gl.glBegin(gl.GL_QUADS)
-        gl.glTexCoord2f(0, 0)
-        gl.glVertex3f(  0, 0, 0)
-        gl.glTexCoord2f(0, 1)
-        gl.glVertex3f(  0, 1, 0)
-        gl.glTexCoord2f(1, 1)
-        gl.glVertex3f(  1, 1, 0)
-        gl.glTexCoord2f(1, 0)
-        gl.glVertex3f(  1, 0, 0)
-        gl.glEnd()
-
-        gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
-        gl.glDisable(gl.GL_TEXTURE_2D)
-
+        self._tex.drawOnBounds(0, 0, 1, 0, 1, 0, 1)
+        
         self._postDraw()
diff --git a/fsl/fslview/gl/gl14/__init__.py b/fsl/fslview/gl/gl14/__init__.py
index 5f5a552bc887d6b2249a90bd70cc2f40004c756b..360c9cafe687bebc15183151878599ac7ae73b12 100644
--- a/fsl/fslview/gl/gl14/__init__.py
+++ b/fsl/fslview/gl/gl14/__init__.py
@@ -6,4 +6,5 @@
 #
 
 import glvolume_funcs
-import glvector_funcs
+import glrgbvector_funcs
+import gllinevector_funcs
diff --git a/fsl/fslview/gl/gl14/briconalpha.prog b/fsl/fslview/gl/gl14/briconalpha.prog
deleted file mode 100644
index 3dbbc4fa26a09f590ccd03746b33da58a3250a8b..0000000000000000000000000000000000000000
--- a/fsl/fslview/gl/gl14/briconalpha.prog
+++ /dev/null
@@ -1,122 +0,0 @@
-# Fragment program routine which applies brightness, contrast,
-# and opacity values to a given colour, and writes the result
-# to result.color.
-#
-# Inputs:
-#   bca          - Vector which contains brightness (x), contrast (y), 
-#                  and alpha (z) levels, as values in the range [0.0, 1.0]
-# 
-#   fragColour   - The colour to be adjusted.
-#
-# Outputs:
-#   result.color - The resulting fragment colour.
-#
-# Author: Paul McCarthy <pauldmccarthy@gmail.com>
-#
-
-TEMP brightness;
-TEMP contrast;
-TEMP alpha;
-TEMP tmpAlpha;
-
-TEMP offset;
-TEMP scale;
-TEMP linScale;
-TEMP expScale;
-TEMP expTemp;
-
-TEMP tempColour;
-
-MOV brightness, bca.x;
-MOV contrast,   bca.y;
-MOV alpha,      bca.z;
-
-#
-# Calculate brightness
-# 
-
-# The brightness is applied as
-# a linear offset, with 0.5
-# equivalent to an offset of 0.0.
-MOV offset, brightness;
-MUL offset, offset, { 2, 2, 2, 0 };
-SUB offset, offset, { 1, 1, 1, 0 };
-
-#
-# Calculate contrast
-#
-
-# If the contrast lies between 0.0
-# and 0.5, it is applied to the
-# colour as a linear scaling factor.
-MOV linScale, contrast;
-MUL linScale, linScale, { 2, 2, 2, 0 };
-
-# If the contrast lies between 0.5 and 0.1, it
-# is applied as an exponential scaling factor,
-# so lower values (closer to 0.5) have less of
-# an effect than higher values (closer to 1.0).
-MOV expScale, linScale;
-MOV expTemp,  contrast;
-SUB expTemp,  expTemp, { 0.5, 0.5, 0.5, 0  };
-MUL expTemp,  expTemp, {   6,   6,   6, 0  };
-EX2 expTemp,  expTemp.x;
-SUB expTemp,  expTemp,  { 1, 1, 1, 0 };
-ADD expScale, expScale, expTemp;
-
-# If contrast is <= 0.5, apply the
-# linear scaling factor, otherwise
-# apply the exponential factor.
-SUB contrast, contrast, { 0.5, 0.5, 0.5, 0.0 };
-CMP scale,    contrast, linScale, expScale;
-MAD scale,    scale, { 1, 1, 1, 0 }, { 0, 0, 0, 1 };
-
-#
-# Prepare to apply the calculated
-# brightness/contrast/alpha settings
-#
-MOV tempColour, fragColour;
-
-#
-# Apply alpha.
-#
-
-# If the existing colour
-# has an alpha less than 1.0, use it
-# instead of the global alpha.
-MOV tmpAlpha, fragColour.a;
-SUB tmpAlpha, tmpAlpha, { 1, 1, 1, 1 };
-CMP tmpAlpha, tmpAlpha, fragColour.a, alpha;
-
-MUL tmpAlpha,   tmpAlpha,   { 0.0, 0.0, 0.0, 1.0 };
-MAD tempColour, tempColour, { 1.0, 1.0, 1.0, 0.0 }, tmpAlpha;
-
-#
-# Apply brightness
-#
-
-ADD tempColour, tempColour, offset;
-
-# Clamp the RGBA values
-# to the range [0, 1]
-MIN tempColour, tempColour, { 1.0, 1.0, 1.0, 1.0 };
-MAX tempColour, tempColour, { 0.0, 0.0, 0.0, 0.0 };
-
-#
-# Apply contrast
-#
-
-# Apply the scaling factor, but
-# keep the new range centred at 0.5.
-SUB tempColour, tempColour,        { 0.5, 0.5, 0.5, 0.0 };
-MAD tempColour, tempColour, scale, { 0.5, 0.5, 0.5, 0.0 };
-
-# Clamp again to [0, 1]
-MIN tempColour, tempColour, { 1.0, 1.0, 1.0, 1.0 };
-MAX tempColour, tempColour, { 0.0, 0.0, 0.0, 0.0 };
-
-#
-# Write the final colour
-#
-
-MOV result.color, tempColour;
diff --git a/fsl/fslview/gl/gl14/common_vert.prog b/fsl/fslview/gl/gl14/common_vert.prog
deleted file mode 100644
index 3c8e1d04b65795e3e1d37c67cda0d21aec0aa119..0000000000000000000000000000000000000000
--- a/fsl/fslview/gl/gl14/common_vert.prog
+++ /dev/null
@@ -1,107 +0,0 @@
-#
-# Vertex program routine used for rendering GLObject instances.
-#
-# This routine does three things:
-#
-#  - Transforms vertex coordinates from display coordinates into screen
-#    coordinates.
-#
-#  - Transforms vertex coordinates from display coordinates into voxel
-#    coordinates.
-#
-#  - Sets the vertex texture coordinate from its display coordinates.
-#
-# Required inputs:
-#
-#   state.matrix.mvp   - Matrix which transforms from the display coordinate
-#                        system to the screen coordinate system.
-#
-#   program.local[0]
-#   program.local[1]
-#   program.local[2]
-#   program.local[3]   - Matrix which transforms from the display coordinate
-#                        system to the image voxel coordinate system.
-#
-#   program.local[4]
-#   program.local[5]
-#   program.local[6]
-#   program.local[7]   - Matrix which performs an arbitrary transformation in 
-#                        the display coordinate system.
-#
-#   vertex.texcoord[0] - Optional texture coordinates - these are transformed
-#                        to voxel coordinates, and passed through to the 
-#                        fragment shader in result.texcoord[2]. This gives
-#                        the option of using the vertex coordinates as texture
-#                        coordinates, or to use independent texture
-#                        coordinates, for the image texture lookup. Fragment
-#                        programs get passed both, and will need to decide
-#                        which coordinates to use.
-#
-# Outputs:
-#
-#   result.position    - vertex position in the screen coordinate system
-#   result.texcoord[0] - vertex position in the display coordinate system
-#   result.texcoord[1] - vertex position in the image voxel coordinate system
-#   result.texcoord[2] - texture coordinates in the image voxel coordinate
-#                        system
-#
-# Author: Paul McCarthy <pauldmccarthy@gmail.com>
-#
-
-TEMP vertexScreenPos;
-TEMP vertexVoxelPos;
-TEMP vertexTexCoord;
-
-PARAM dispToScreenMat[4] = { state.matrix.mvp };
-PARAM dispToVoxMat[   4] = { program.local[0],
-                             program.local[1],
-                             program.local[2],
-                             program.local[3] };
-PARAM worldToWorldMat[4] = { program.local[4],
-                             program.local[5],
-                             program.local[6],
-                             program.local[7] };
-
-
-# Transform the vertex position
-# from display coordinates to
-# screen coordinates, incorporating
-# the arbitrary display space
-# transformation
-DP4 vertexScreenPos.x, worldToWorldMat[0], vertex.position;
-DP4 vertexScreenPos.y, worldToWorldMat[1], vertex.position;
-DP4 vertexScreenPos.z, worldToWorldMat[2], vertex.position;
-DP4 vertexScreenPos.w, worldToWorldMat[3], vertex.position;
-
-DP4 vertexScreenPos.x, dispToScreenMat[0], vertexScreenPos;
-DP4 vertexScreenPos.y, dispToScreenMat[1], vertexScreenPos;
-DP4 vertexScreenPos.z, dispToScreenMat[2], vertexScreenPos;
-DP4 vertexScreenPos.w, dispToScreenMat[3], vertexScreenPos;
-
-# Transform the vertex position
-# from display coordinates to
-# voxel coordinates
-DP4 vertexVoxelPos.x, dispToVoxMat[0], vertex.position;
-DP4 vertexVoxelPos.y, dispToVoxMat[1], vertex.position;
-DP4 vertexVoxelPos.z, dispToVoxMat[2], vertex.position;
-DP4 vertexVoxelPos.w, dispToVoxMat[3], vertex.position;
-
-# And do the same for the 
-# texture0 coordinates
-DP4 vertexTexCoord.x, dispToVoxMat[0], vertex.texcoord[0];
-DP4 vertexTexCoord.y, dispToVoxMat[1], vertex.texcoord[0];
-DP4 vertexTexCoord.z, dispToVoxMat[2], vertex.texcoord[0];
-DP4 vertexTexCoord.w, dispToVoxMat[3], vertex.texcoord[0];
-
-# Offset voxel coordinates by 0.5 
-# so they are centred within a voxel.
-# See comments in gl21/common_vert.glsl
-# for an explanation.
-ADD vertexVoxelPos, vertexVoxelPos, { 0.5, 0.5, 0.5, 0.0 };
-ADD vertexTexCoord, vertexTexCoord, { 0.5, 0.5, 0.5, 0.0 };
-
-# Write the outputs
-MOV result.position,    vertexScreenPos;
-MOV result.texcoord[0], vertex.position;
-MOV result.texcoord[1], vertexVoxelPos;
-MOV result.texcoord[2], vertexTexCoord;
diff --git a/fsl/fslview/gl/gl14/gllinevector_funcs.py b/fsl/fslview/gl/gl14/gllinevector_funcs.py
new file mode 100644
index 0000000000000000000000000000000000000000..7204194878c9f619544fca69763d09bf0086c7de
--- /dev/null
+++ b/fsl/fslview/gl/gl14/gllinevector_funcs.py
@@ -0,0 +1,193 @@
+#!/usr/bin/env python
+#
+# gllinevector_funcs.py -
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+
+import logging
+
+import numpy                          as np
+
+import OpenGL.GL                      as gl
+import OpenGL.GL.ARB.fragment_program as arbfp
+import OpenGL.GL.ARB.vertex_program   as arbvp
+import OpenGL.raw.GL._types           as gltypes
+
+import fsl.utils.transform            as transform
+import fsl.fslview.gl.gllinevector    as gllinevector
+import fsl.fslview.gl.resources       as glresources
+import fsl.fslview.gl.shaders         as shaders
+
+
+log = logging.getLogger(__name__)
+
+
+def init(self):
+
+    self.vertexProgram   = None
+    self.fragmentProgram = None
+    self.lineVertices    = None
+
+    self._vertexResourceName = '{}_{}_vertices'.format(
+        type(self).__name__, id(self.image)) 
+
+    compileShaders(   self)
+    updateShaderState(self)
+    updateVertices(   self)
+
+    display = self.display
+    opts    = self.opts
+
+    def vertexUpdate(*a):
+        updateVertices(self)
+        self.onUpdate()
+
+    display.addListener('resolution', self.name, vertexUpdate)
+    opts   .addListener('directed',   self.name, vertexUpdate)
+
+
+def destroy(self):
+    arbvp.glDeleteProgramsARB(1, gltypes.GLuint(self.vertexProgram))
+    arbfp.glDeleteProgramsARB(1, gltypes.GLuint(self.fragmentProgram))
+
+    self.display.removeListener('resolution', self.name)
+    self.opts   .removeListener('directed',   self.name)
+
+    glresources.delete(self._vertexResourceName)
+
+
+def compileShaders(self):
+    if self.vertexProgram is not None:
+        arbvp.glDeleteProgramsARB(1, gltypes.GLuint(self.vertexProgram))
+        
+    if self.fragmentProgram is not None:
+        arbfp.glDeleteProgramsARB(1, gltypes.GLuint(self.fragmentProgram)) 
+
+    vertShaderSrc = shaders.getVertexShader(  self,
+                                              sw=self.display.softwareMode)
+    fragShaderSrc = shaders.getFragmentShader(self,
+                                              sw=self.display.softwareMode)
+
+    vertexProgram, fragmentProgram = shaders.compilePrograms(
+        vertShaderSrc, fragShaderSrc)
+
+    self.vertexProgram   = vertexProgram
+    self.fragmentProgram = fragmentProgram
+
+    updateVertices(self)
+
+
+def updateVertices(self):
+    
+    image   = self.image
+    display = self.display
+    opts    = self.opts
+
+    if self.lineVertices is None:
+        self.lineVertices = glresources.get(
+            self._vertexResourceName, gllinevector.GLLineVertices, self) 
+
+    newHash = (hash(display.transform)    ^
+               hash(display.resolution)   ^
+               hash(opts   .directed))
+
+    if hash(self.lineVertices) != newHash:
+
+        log.debug('Re-generating line vertices for {}'.format(image))
+        self.lineVertices.refresh(self)
+        glresources.set(self._vertexResourceName,
+                        self.lineVertices,
+                        overwrite=True)
+
+
+def updateShaderState(self):
+    opts = self.displayOpts
+
+    gl.glEnable(arbvp.GL_VERTEX_PROGRAM_ARB) 
+    gl.glEnable(arbfp.GL_FRAGMENT_PROGRAM_ARB)
+
+    arbvp.glBindProgramARB(arbvp.GL_VERTEX_PROGRAM_ARB,
+                           self.vertexProgram)
+    arbfp.glBindProgramARB(arbfp.GL_FRAGMENT_PROGRAM_ARB,
+                           self.fragmentProgram)
+    
+    voxValXform  = self.imageTexture.voxValXform
+    cmapXform    = self.xColourTexture.getCoordinateTransform()
+    shape        = np.array(list(self.image.shape[:3]) + [0], dtype=np.float32)
+    invShape     = 1.0 / shape
+    modThreshold = [opts.modThreshold / 100.0, 0.0, 0.0, 0.0]
+
+    # Vertex program inputs
+    shaders.setVertexProgramVector(  0, invShape)
+
+    # Fragment program inputs
+    shaders.setFragmentProgramMatrix(0, voxValXform)
+    shaders.setFragmentProgramMatrix(4, cmapXform)
+    shaders.setFragmentProgramVector(8, shape)
+    shaders.setFragmentProgramVector(9, modThreshold)
+
+    gl.glDisable(arbvp.GL_VERTEX_PROGRAM_ARB) 
+    gl.glDisable(arbfp.GL_FRAGMENT_PROGRAM_ARB)
+    
+
+def preDraw(self):
+    gl.glEnable(arbvp.GL_VERTEX_PROGRAM_ARB) 
+    gl.glEnable(arbfp.GL_FRAGMENT_PROGRAM_ARB)
+
+    gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
+
+    if self.display.softwareMode:
+        gl.glEnableClientState(gl.GL_TEXTURE_COORD_ARRAY)
+
+    arbvp.glBindProgramARB(arbvp.GL_VERTEX_PROGRAM_ARB,
+                           self.vertexProgram)
+    arbfp.glBindProgramARB(arbfp.GL_FRAGMENT_PROGRAM_ARB,
+                           self.fragmentProgram) 
+
+
+def draw(self, zpos, xform=None):
+
+    display             = self.display
+    opts                = self.displayOpts
+    vertices, texCoords = self.lineVertices.getVertices(self, zpos)
+
+    if vertices.size == 0:
+        return
+
+    if display.softwareMode:
+        texCoords = texCoords.ravel('C')
+        gl.glClientActiveTexture(gl.GL_TEXTURE0)
+        gl.glTexCoordPointer(3, gl.GL_FLOAT, 0, texCoords)
+
+    vertices = vertices.ravel('C')
+    v2d      = self.display.getTransform('voxel', 'display')
+
+    if xform is None: xform = v2d
+    else:             xform = transform.concat(v2d, xform)
+ 
+    gl.glPushMatrix()
+    gl.glMultMatrixf(np.array(xform, dtype=np.float32).ravel('C'))
+
+    gl.glVertexPointer(3, gl.GL_FLOAT, 0, vertices)
+    
+    gl.glLineWidth(opts.lineWidth)
+    gl.glDrawArrays(gl.GL_LINES, 0, vertices.size / 3)
+
+    gl.glPopMatrix()
+
+
+def drawAll(self, zposes, xforms):
+    for zpos, xform in zip(zposes, xforms):
+        draw(self, zpos, xform)
+
+
+def postDraw(self):
+    
+    gl.glDisable(arbvp.GL_VERTEX_PROGRAM_ARB) 
+    gl.glDisable(arbfp.GL_FRAGMENT_PROGRAM_ARB)
+    
+    gl.glDisableClientState(gl.GL_VERTEX_ARRAY)
+
+    if self.display.softwareMode:
+        gl.glDisableClientState(gl.GL_TEXTURE_COORD_ARRAY)
diff --git a/fsl/fslview/gl/gl14/gllinevector_sw_vert.prog b/fsl/fslview/gl/gl14/gllinevector_sw_vert.prog
new file mode 100644
index 0000000000000000000000000000000000000000..15f1280bcac9b3484cbc79d2e1eb0534348cb577
--- /dev/null
+++ b/fsl/fslview/gl/gl14/gllinevector_sw_vert.prog
@@ -0,0 +1,29 @@
+!!ARBvp1.0
+#
+# Vertex program for rendering GLLineVector instances. Identical to
+# gllinevector_vert.prog, but does not calculate voxel coordinates.
+#
+# Inputs:
+#    state.matrix.mvp   - MVP transformation matrix
+#    vertex.position    - Vertex position in the voxel coordinate system.
+#    vertex texcoord[0] - Texture coordinates
+#
+# Outputs:
+#    result.position    - the vertex position
+#    result.texcoord[0] - the texture coordinates
+#
+
+TEMP texCoord;
+
+# Transform the vertex position (which is in voxel
+# coordinates) into display coordinates.  It is
+# assumed that a voxel->display transformation has
+# been encoded into the mvp matrix.
+DP4 result.position.x, state.matrix.mvp.row[0], vertex.position;
+DP4 result.position.y, state.matrix.mvp.row[1], vertex.position;
+DP4 result.position.z, state.matrix.mvp.row[2], vertex.position;
+DP4 result.position.w, state.matrix.mvp.row[3], vertex.position;
+
+MOV result.texcoord[0], vertex.texcoord[0];
+
+END
diff --git a/fsl/fslview/gl/gl14/gllinevector_vert.prog b/fsl/fslview/gl/gl14/gllinevector_vert.prog
new file mode 100644
index 0000000000000000000000000000000000000000..88cf875568b08046ba203c74695a3b73f63ea86d
--- /dev/null
+++ b/fsl/fslview/gl/gl14/gllinevector_vert.prog
@@ -0,0 +1,45 @@
+!!ARBvp1.0
+#
+# Vertex program for rendering GLLineVector instances.
+#
+# Inputs:
+#    state.matrix.mvp - MVP transformation matrix
+#    vertex.position  - Vertex position in the voxel coordinate system.
+# 
+#    program.local[0] - (first three components) inverse of image shape
+#
+# Outputs:
+#    result.position    - the vertex position
+#    result.texcoord[0] - the texture coordinates
+#    result.texcoord[1] - the voxel coordinates
+#
+
+TEMP texCoord;
+
+PARAM invImageShape = program.local[0];
+
+# Transform the vertex position (which is in voxel
+# coordinates) into display coordinates.  It is
+# assumed that a voxel->display transformation has
+# been encoded into the mvp matrix.
+DP4 result.position.x, state.matrix.mvp.row[0], vertex.position;
+DP4 result.position.y, state.matrix.mvp.row[1], vertex.position;
+DP4 result.position.z, state.matrix.mvp.row[2], vertex.position;
+DP4 result.position.w, state.matrix.mvp.row[3], vertex.position;
+
+# Transform the vertex coordinates
+# into integer voxel coordinates
+MOV texCoord, vertex.position;
+ADD texCoord, texCoord, { 0.5, 0.5, 0.5, 0.0 };
+FLR texCoord, texCoord;
+
+MOV result.texcoord[1], texCoord;
+
+# Transform those integer voxel
+# coordinates into texture coordinates
+ADD texCoord, texCoord, { 0.5, 0.5, 0.5, 0.0 };
+MUL texCoord, texCoord, invImageShape;
+
+MOV result.texcoord[0], texCoord;
+
+END
diff --git a/fsl/fslview/gl/gl14/glrgbvector_funcs.py b/fsl/fslview/gl/gl14/glrgbvector_funcs.py
new file mode 100644
index 0000000000000000000000000000000000000000..22fba25ccdd849583b3f1fdb67cb7c5406c84859
--- /dev/null
+++ b/fsl/fslview/gl/gl14/glrgbvector_funcs.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python
+#
+# glrgbvector_funcs.py -
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+
+import OpenGL.GL                      as gl
+import OpenGL.GL.ARB.fragment_program as arbfp
+import OpenGL.GL.ARB.vertex_program   as arbvp
+import OpenGL.raw.GL._types           as gltypes
+
+import                           glvolume_funcs
+import fsl.fslview.gl.shaders as shaders
+
+
+def init(self):
+    
+    self.vertexProgram   = None
+    self.fragmentProgram = None
+    
+    compileShaders(   self)
+    updateShaderState(self)
+
+
+def destroy(self):
+    arbvp.glDeleteProgramsARB(1, gltypes.GLuint(self.vertexProgram))
+    arbfp.glDeleteProgramsARB(1, gltypes.GLuint(self.fragmentProgram)) 
+
+    
+def compileShaders(self):
+    if self.vertexProgram is not None:
+        arbvp.glDeleteProgramsARB(1, gltypes.GLuint(self.vertexProgram))
+        
+    if self.fragmentProgram is not None:
+        arbfp.glDeleteProgramsARB(1, gltypes.GLuint(self.fragmentProgram)) 
+
+    vertShaderSrc = shaders.getVertexShader(  self,
+                                              sw=self.display.softwareMode)
+    fragShaderSrc = shaders.getFragmentShader(self,
+                                              sw=self.display.softwareMode)
+
+    vertexProgram, fragmentProgram = shaders.compilePrograms(
+        vertShaderSrc, fragShaderSrc)
+
+    self.vertexProgram   = vertexProgram
+    self.fragmentProgram = fragmentProgram        
+
+
+def updateShaderState(self):
+
+    opts = self.displayOpts
+    
+    gl.glEnable(arbvp.GL_VERTEX_PROGRAM_ARB) 
+    gl.glEnable(arbfp.GL_FRAGMENT_PROGRAM_ARB)
+
+    arbvp.glBindProgramARB(arbvp.GL_VERTEX_PROGRAM_ARB,
+                           self.vertexProgram)
+    arbfp.glBindProgramARB(arbfp.GL_FRAGMENT_PROGRAM_ARB,
+                           self.fragmentProgram)
+
+    voxValXform  = self.imageTexture.voxValXform
+    cmapXform    = self.xColourTexture.getCoordinateTransform()
+    shape        = list(self.image.shape[:3]) + [0]
+    modThreshold = [opts.modThreshold / 100.0, 0.0, 0.0, 0.0]
+
+    shaders.setFragmentProgramMatrix(0, voxValXform)
+    shaders.setFragmentProgramMatrix(4, cmapXform)
+    shaders.setFragmentProgramVector(8, shape + [0])
+    shaders.setFragmentProgramVector(9, modThreshold)
+    
+    gl.glDisable(arbvp.GL_VERTEX_PROGRAM_ARB) 
+    gl.glDisable(arbfp.GL_FRAGMENT_PROGRAM_ARB)
+
+
+preDraw  = glvolume_funcs.preDraw
+draw     = glvolume_funcs.draw
+drawAll  = glvolume_funcs.drawAll
+postDraw = glvolume_funcs.postDraw
diff --git a/fsl/fslview/gl/gl14/glvector_frag.prog b/fsl/fslview/gl/gl14/glvector_frag.prog
index 86df8554c3be11ae87afdec9d8a784169368be2a..8cb39dc43229449a7d76eb1b719953ea6a5646e4 100644
--- a/fsl/fslview/gl/gl14/glvector_frag.prog
+++ b/fsl/fslview/gl/gl14/glvector_frag.prog
@@ -17,6 +17,9 @@
 #  - Uses those voxel values to colour the fragment.
 #
 # Required inputs:
+# 
+#   fragment.texcoord[0] - Fragment texture coordinates
+#   fragment.texcoord[1] - Fragment voxel coordinates 
 #
 #   program.local[0]
 #   program.local[1]
@@ -25,22 +28,20 @@
 #                      image voxel values from their texture value
 #                      to the original data range.
 #
-#   program.local[4] - Image shape - number of voxels along the xyz
+#   program.local[4]
+#   program.local[5]
+#   program.local[6]
+#   program.local[7] - Transformation matrix which transforms the vector
+#                      image voxel values from their texture value
+#                      to the original data range. 
+#
+#   program.local[8] - Image shape - number of voxels along the xyz
 #                      dimensions in the image
-#   program.local[5] - Inverse of image shape
 #
-#   program.local[6] - Modulation threshold (x component) - if the 
+#   program.local[9] - Modulation threshold (x component) - if the 
 #                      modulation value is less than this, the fragment
 #                      is set to fully transparent.
 #
-#   program.local[7] - Vector which contains global brightness, contrast, 
-#                      and alpha settings, to pass to briconalpha.prog.
-#
-#   program.local[8] - If greater than or equal to 0, fragment.texcoord[1]
-#                      is used as the coordinates for the texture lookup.
-#                      Otherwise, fragment.texcoord[2] is used for the
-#                      texture lookup.
-#
 # Outputs:
 #
 #   result.color     - The fragment colour (written by briconalpha.prog)
@@ -49,57 +50,50 @@
 #
 
 TEMP  voxCoord;
-TEMP  normVoxCoord;
 TEMP  modValue;
 TEMP  voxValue;
 TEMP  xColour;
 TEMP  yColour;
 TEMP  zColour;
 TEMP  fragColour;
-PARAM imageValueXform[4] = { program.local[0],
-                             program.local[1],
-                             program.local[2],
-                             program.local[3] };
-PARAM imageShape         =   program.local[4];
-PARAM imageShapeInv      =   program.local[5];
-PARAM modThres           =   program.local[6];
-PARAM bca                =   program.local[7];
-PARAM useTexCoords       =   program.local[8];
+PARAM voxValXform[4] = { program.local[0],
+                         program.local[1],
+                         program.local[2],
+                         program.local[3] };
+PARAM cmapXform[4]   = { program.local[4],
+                         program.local[5],
+                         program.local[6],
+                         program.local[7] };
+                         
+PARAM imageShape     =   program.local[8];
+PARAM modThres       =   program.local[9];
 
 # retrieve the voxel coordinates 
-CMP voxCoord, useTexCoords, fragment.texcoord[2], fragment.texcoord[1];
+MOV voxCoord, fragment.texcoord[1];
 
 # Bail if the voxel coordinate
 # is out of the image space
 #pragma include test_in_bounds.prog
 
-# Normalise voxel coordinates to 
-# lie in the range (0, 1), so they 
-# can be used for texture lookup
-MUL normVoxCoord, voxCoord, imageShapeInv;
-
 # look up vector values
 # from the 3D RGB texture
-TEX voxValue, normVoxCoord, texture[0], 3D;
+TEX voxValue, fragment.texcoord[0], texture[0], 3D;
+
+# Look up the modulation value
+# from the modulation texture
+TEX modValue, fragment.texcoord[0], texture[1], 3D;
 
 # Transform vector values from their normalised 
 # texture range to their original data range,
 # and take the absolue value
-MAD voxValue, voxValue, imageValueXform[0].x, imageValueXform[0].w;
+MAD voxValue, voxValue, voxValXform[0].x, voxValXform[3].x;
 ABS voxValue, voxValue;
+MAD voxValue, voxValue, cmapXform[  0].x, cmapXform[  3].x;
 
-# Reset the opacity component
-MAD voxValue, voxValue, { 1, 1, 1, 0 }, { 0, 0, 0, 1 }; 
+# Apply the modulation value
+MUL voxValue, voxValue, modValue.x;
 
-# Look up the modulation value
-# from the modulation texture -
-# initialise modValue transparency
-# to 1.0, so it doesn't corrupt
-# our voxel colour value later on
-TEX modValue, normVoxCoord, texture[1], 3D;
-MAD modValue, modValue, { 1, 1, 1, 0 }, { 0, 0, 0, 1 }; 
-  
-# Use those values to look up the
+# Use the vector values to look up the
 # colours for each xyz direction
 TEX xColour, voxValue.x, texture[2], 1D;
 TEX yColour, voxValue.y, texture[3], 1D;
@@ -111,20 +105,18 @@ MOV fragColour,             xColour;
 ADD fragColour, fragColour, yColour;
 ADD fragColour, fragColour, zColour;
 
-# Take the average of the alpha channel
-MUL fragColour, fragColour, { 1.0, 1.0, 1.0, 0.333333 };
-
-# Apply the modulation factor
-MUL fragColour, fragColour, modValue;
+# Take the highest alpha of the three colour maps
+MAX fragColour.a, xColour.a,    yColour.a;
+MAX fragColour.a, fragColour.a, zColour.a;
 
-# But clear the fragment if the modulation
+# Clear the fragment if the modulation
 # vaue does not meet the threshold
 MOV modValue,   modValue.x;
 SUB modValue,   modValue, modThres.x;
 CMP fragColour, modValue.x, { 0, 0, 0, 0 }, fragColour;
 
 # Colour the pixel!
-#pragma include briconalpha.prog
+MOV result.color, fragColour;
 
 END
 
diff --git a/fsl/fslview/gl/gl14/glvector_funcs.py b/fsl/fslview/gl/gl14/glvector_funcs.py
deleted file mode 100644
index bedf6358dbcbd3e26e7004115381c9cbf15dd055..0000000000000000000000000000000000000000
--- a/fsl/fslview/gl/gl14/glvector_funcs.py
+++ /dev/null
@@ -1,288 +0,0 @@
-#!/usr/bin/env python
-#
-# glvector_funcs.py - Logic for rendering GLVector instances in an OpenGL 1.4
-#                     compatible manner.
-#
-# Author: Paul McCarthy <pauldmccarthy@gmail.com>
-#
-"""This module contains functions used by the
-:class:`~fsl.fslview.gl.glvector.GLVector` class, for rendering
-:class:`~fsl.data.image.Image` instances as vectors in an OpenGL 1.4
-compatible manner.
-
-See the ``GLVector`` documentation for more details.
-"""
-
-import numpy                          as np
-import scipy.ndimage                  as ndi
-
-import OpenGL.GL                      as gl
-import OpenGL.raw.GL._types           as gltypes
-import OpenGL.GL.ARB.fragment_program as arbfp
-import OpenGL.GL.ARB.vertex_program   as arbvp
-
-import fsl.utils.transform     as transform
-import fsl.fslview.gl.shaders  as shaders
-import fsl.fslview.gl.globject as globject
-
-
-def init(self):
-    """Compiles the vertex and fragment programs used for rendering. The
-    same programs are used for both ``line`` and ``rgb`` mode.
-    """
-  
-    vertShaderSrc = shaders.getVertexShader(  self)
-    fragShaderSrc = shaders.getFragmentShader(self)
-
-    vertexProgram, fragmentProgram = shaders.compilePrograms(
-        vertShaderSrc, fragShaderSrc)
-    
-    self.vertexProgram   = vertexProgram
-    self.fragmentProgram = fragmentProgram
-
-
-def destroy(self):
-    """Deletes the vertex/fragment programs. """
-
-    arbvp.glDeleteProgramsARB(1, gltypes.GLuint(self.vertexProgram))
-    arbfp.glDeleteProgramsARB(1, gltypes.GLuint(self.fragmentProgram))
-
-
-def setAxes(self):
-    """Calls one of :func:`rgbModeSetAxes` or :func:`lineModeSetAxes`,
-    depending upon the current display mode.
-    """
-    mode = self.displayOpts.displayMode
-
-    if   mode == 'rgb':  rgbModeSetAxes( self)
-    elif mode == 'line': lineModeSetAxes(self)
-
-    
-def rgbModeSetAxes(self):
-    """Creates four vertices which represent a slice through the image
-    texture, oriented according to the plane defined by
-    ``self.xax`` and  ``self.yax``.
-
-    See :func:`~fsl.fslview.globject.slice2D` for more details.
-    """
-    worldCoords, idxs = globject.slice2D(
-        self.image.shape,
-        self.xax,
-        self.yax,
-        self.display.getTransform('voxel', 'display'))
-
-    self.worldCoords = worldCoords
-    self.indices     = idxs 
-
-    
-def lineModeSetAxes(self):
-    """Creates an array of points forming a rectangular grid, one point
-    in the centre of each voxel, oriented according to the plane defined by
-    ``self.xax`` and  ``self.yax``. These points are used by
-    :func:`lineModeDraw` to generate lines representing vectors at every
-    voxel.
-
-    See :func:`~fsl.fslview.globject.calculateSamplePoints` for more details.
-    """
-    worldCoords, xpixdim, ypixdim, lenx, leny = \
-        globject.calculateSamplePoints(
-            self.image,
-            self.display,
-            self.xax,
-            self.yax)
-
-    self.worldCoords = worldCoords
-    self.xpixdim     = xpixdim
-    self.ypixdim     = ypixdim
-
-
-def preDraw(self):
-    """Loads the vertex/fragment programs, and sets some program parameters.
-    """
-
-    gl.glEnable(arbvp.GL_VERTEX_PROGRAM_ARB) 
-    gl.glEnable(arbfp.GL_FRAGMENT_PROGRAM_ARB)
-
-    gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
-    gl.glEnableClientState(gl.GL_TEXTURE_COORD_ARRAY)
-
-    arbvp.glBindProgramARB(arbvp.GL_VERTEX_PROGRAM_ARB,
-                           self.vertexProgram)
-    arbfp.glBindProgramARB(arbfp.GL_FRAGMENT_PROGRAM_ARB,
-                           self.fragmentProgram)
-
-    # the vertex program needs to be able to
-    # transform from display space to voxel
-    # space
-    shaders.setVertexProgramMatrix(
-        0, self.display.getTransform('display', 'voxel').T)
-
-    if self.displayOpts.displayMode == 'line':
-        shaders.setFragmentProgramMatrix(0, self.imageTexture.voxValXform.T)
-    else:
-        shaders.setFragmentProgramMatrix(0, np.eye(4))
-    
-    # The fragment program needs to know the image
-    # shape and its inverse, so it can scale voxel
-    # coordinates to the range [0.0, 1.0], and so
-    # it can clip fragments outside of the image
-    # space. It also needs to know the global
-    # brightness, contrast, and alpha values.
-    shape    = list(self.image.shape)
-    invshape = [1.0 / s for s in shape]
-    bca      = [self.display.brightness       / 100.0,
-                self.display.contrast         / 100.0,
-                self.display.alpha            / 100.0]
-    modThres = [self.displayOpts.modThreshold / 100.0]
-    
-    shaders.setFragmentProgramVector(4, shape    + [0])
-    shaders.setFragmentProgramVector(5, invshape + [0])
-    shaders.setFragmentProgramVector(6, modThres + [0, 0, 0])
-    shaders.setFragmentProgramVector(7, bca      + [0])
-
-
-def draw(self, zpos, xform=None):
-    """Calls one of :func:`lineModeDraw` or :func:`rgbModeDraw`, depending
-    upon the current display mode.
-    """
-    if xform is None:
-        xform = np.eye(4, dtype=np.float32)
-        
-    drawAll(self, [zpos], [xform])
-
-    
-def drawAll(self, zposes, xforms):
-    
-    if not self.display.enabled:
-        return
-    
-    if self.displayOpts.displayMode == 'line':
-        lineModeDrawAll(self, zposes, xforms)
-        
-    elif self.displayOpts.displayMode == 'rgb':
-        rgbModeDrawAll( self, zposes, xforms)
-
-        
-def lineModeDrawAll(self, zposes, xforms):
-    """Creates a line, representing a vector, at each voxel at the specified
-    ``zpos``, and renders them.
-    """
-
-    image       = self.image
-    display     = self.display
-    worldCoords = self.worldCoords
-    nVerts      = worldCoords.shape[0]
-    nSlices     = len(zposes)
-
-    worldCoords = np.vstack([worldCoords] * nSlices)
-    worldCoords[:, self.zax] = np.repeat(zposes, nVerts)
-    texCoords   = np.array(worldCoords)
-
-    # Transform the world coordinates to
-    # floating point voxel coordinates
-    dToVMat = display.getTransform('display', 'voxel')
-    vToDMat = display.getTransform('voxel',   'display')
-    
-    voxCoords  = transform.transform(worldCoords, dToVMat).transpose()
-    imageData  = image.data
-    nVoxels    = worldCoords.shape[0]
-
-    # Get the image data at those 
-    # voxel coordinates, using
-    # nearest neighbour interpolation.
-    # 
-    # Three separate calls to map_coordinates
-    # is generally faster than constructing
-    # a 4D coordinate array, and performing
-    # one call to map_coordinates.
-    xvals = ndi.map_coordinates(imageData[:, :, :, 0],
-                                voxCoords,
-                                order=0,
-                                mode='nearest',
-                                prefilter=False)
-    yvals = ndi.map_coordinates(imageData[:, :, :, 1],
-                                voxCoords,
-                                order=0,
-                                mode='nearest',
-                                prefilter=False)
-    zvals = ndi.map_coordinates(imageData[:, :, :, 2],
-                                voxCoords,
-                                order=0,
-                                mode='nearest',
-                                prefilter=False)
-
-    # make a N*3 list of vectors
-    vecs = np.array([xvals, yvals, zvals]).transpose()
-
-    # make a bunch of vertices which represent lines 
-    # (two vertices per line), centered at the origin
-    # and scaled appropriately
-    vecs *= 0.5
-    vecs  = np.hstack((-vecs, vecs)).reshape((2 * nVoxels, 3))
-
-    # Scale the vector by the minimum voxel length, 
-    # so it is a unit vector within real world space
-    vecs /= (image.pixdim[:3] / min(image.pixdim[:3]))
-
-    # Offset each of those vertices by
-    # their original voxel coordinates
-    vecs += voxCoords.T.repeat(2, 0)
-
-    # Translate those line vertices
-    # into display coordinates
-    worldCoords = transform.transform(vecs, vToDMat)
-
-    for i, (zpos, xform) in enumerate(zip(zposes, xforms)):
-        
-        start = i     * (nVerts * 2)
-        end   = start + (nVerts * 2)
-        c     = worldCoords[start:end, :]
-        
-        c[:, self.zax] = zpos
-        
-        worldCoords[start:end, :] = transform.transform(c, xform)
-
-    texCoords   = np.repeat(texCoords, 2, axis=0)
-    worldCoords = np.array(worldCoords, dtype=np.float32).ravel('C')
-
-    # Draw all the lines!
-    shaders.setVertexProgramMatrix(  4,  np.eye( 4, dtype=np.float32))
-    shaders.setFragmentProgramVector(8, -np.ones(4, dtype=np.float32))    
- 
-    gl.glLineWidth(2)
-    gl.glVertexPointer(  3, gl.GL_FLOAT, 0, worldCoords)
-    gl.glTexCoordPointer(3, gl.GL_FLOAT, 0, texCoords)
-    
-    gl.glDrawArrays(gl.GL_LINES, 0, 2 * nVoxels) 
-
-    
-def rgbModeDrawAll(self, zposes, xforms):
-    """Renders a rectangular slice through the vector image texture at the
-    specified ``zpos``.
-    """
-
-    worldCoords  = np.array(self.worldCoords)
-    indices      = np.array(self.indices)
-
-    worldCoords[[2, 3], :] = worldCoords[[3, 2], :]
-
-    worldCoords, texCoords, indices = globject.broadcast(
-        worldCoords, indices, zposes, xforms, self.zax)
-
-    shaders.setVertexProgramMatrix(  4,  np.eye( 4, dtype=np.float32))
-    shaders.setFragmentProgramVector(8, -np.ones(4, dtype=np.float32))
-
-    gl.glVertexPointer(  3, gl.GL_FLOAT, 0, worldCoords)
-    gl.glTexCoordPointer(3, gl.GL_FLOAT, 0, texCoords)
-
-    gl.glDrawElements(gl.GL_QUADS, len(indices), gl.GL_UNSIGNED_INT, indices) 
-
-    
-def postDraw(self):
-    """Disables the vertex/fragment programs used for drawing."""
-
-    gl.glDisable(arbfp.GL_FRAGMENT_PROGRAM_ARB)
-    gl.glDisable(arbvp.GL_VERTEX_PROGRAM_ARB)
-
-    gl.glDisableClientState(gl.GL_VERTEX_ARRAY)
-    gl.glDisableClientState(gl.GL_TEXTURE_COORD_ARRAY)
diff --git a/fsl/fslview/gl/gl14/glvector_sw_frag.prog b/fsl/fslview/gl/gl14/glvector_sw_frag.prog
new file mode 100644
index 0000000000000000000000000000000000000000..235a73b7ed11b5a130725e946bbf2e2bec52f553
--- /dev/null
+++ b/fsl/fslview/gl/gl14/glvector_sw_frag.prog
@@ -0,0 +1,118 @@
+!!ARBfp1.0
+#
+# Fragment program used for rendering GLVector instances.
+# Identical to glvector_frag.prog, but omits voxel coordinate
+# bounds checking.
+#
+# This fragment program does the following:
+# 
+#  - Retrieves the voxel coordinates corresponding to the fragment
+# 
+#  - Uses those voxel coordinates to look up the corresponding xyz
+#    directions value in the 3D RGB image texture.
+#
+#  - Looks up the colours corresponding to those xyz directions.
+#
+#  - Modulates those colours by the modulation texture.
+#
+#  - Uses those voxel values to colour the fragment.
+#
+# Required inputs:
+# 
+#   fragment.texcoord[0] - Fragment texture coordinates
+#
+#   program.local[0]
+#   program.local[1]
+#   program.local[2]
+#   program.local[3] - Transformation matrix which transforms the vector
+#                      image voxel values from their texture value
+#                      to the original data range.
+#
+#   program.local[4]
+#   program.local[5]
+#   program.local[6]
+#   program.local[7] - Transformation matrix which transforms the vector
+#                      image voxel values from their texture value
+#                      to the original data range. 
+#
+#   program.local[8] - Image shape - number of voxels along the xyz
+#                      dimensions in the image
+#
+#   program.local[9] - Modulation threshold (x component) - if the 
+#                      modulation value is less than this, the fragment
+#                      is set to fully transparent.
+#
+# Outputs:
+#
+#   result.color     - The fragment colour (written by briconalpha.prog)
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+
+TEMP  voxCoord;
+TEMP  modValue;
+TEMP  voxValue;
+TEMP  xColour;
+TEMP  yColour;
+TEMP  zColour;
+TEMP  fragColour;
+PARAM voxValXform[4] = { program.local[0],
+                         program.local[1],
+                         program.local[2],
+                         program.local[3] };
+PARAM cmapXform[4]   = { program.local[4],
+                         program.local[5],
+                         program.local[6],
+                         program.local[7] };
+                         
+PARAM imageShape     =   program.local[8];
+PARAM modThres       =   program.local[9];
+
+# retrieve the voxel coordinates 
+MOV voxCoord, fragment.texcoord[1];
+
+# look up vector values
+# from the 3D RGB texture
+TEX voxValue, fragment.texcoord[0], texture[0], 3D;
+
+# Look up the modulation value
+# from the modulation texture
+TEX modValue, fragment.texcoord[0], texture[1], 3D;
+
+# Transform vector values from their normalised 
+# texture range to their original data range,
+# and take the absolue value
+MAD voxValue, voxValue, voxValXform[0].x, voxValXform[3].x;
+ABS voxValue, voxValue;
+MAD voxValue, voxValue, cmapXform[  0].x, cmapXform[  3].x;
+
+# Apply the modulation value
+MUL voxValue, voxValue, modValue.x;
+
+# Use the vector values to look up the
+# colours for each xyz direction
+TEX xColour, voxValue.x, texture[2], 1D;
+TEX yColour, voxValue.y, texture[3], 1D;
+TEX zColour, voxValue.z, texture[4], 1D;
+
+# Cumulatively combine the rgb
+# channels of those three colours
+MOV fragColour,             xColour;
+ADD fragColour, fragColour, yColour;
+ADD fragColour, fragColour, zColour;
+
+# Take the highest alpha of the three colour maps
+MAX fragColour.a, xColour.a,    yColour.a;
+MAX fragColour.a, fragColour.a, zColour.a;
+
+# Clear the fragment if the modulation
+# vaue does not meet the threshold
+MOV modValue,   modValue.x;
+SUB modValue,   modValue, modThres.x;
+CMP fragColour, modValue.x, { 0, 0, 0, 0 }, fragColour;
+
+# Colour the pixel!
+MOV result.color, fragColour;
+
+END
+
diff --git a/fsl/fslview/gl/gl14/glvector_vert.prog b/fsl/fslview/gl/gl14/glvector_vert.prog
deleted file mode 100644
index f8210528d8b58960418a4317d99684b79c30463e..0000000000000000000000000000000000000000
--- a/fsl/fslview/gl/gl14/glvector_vert.prog
+++ /dev/null
@@ -1,6 +0,0 @@
-!!ARBvp1.0
-#
-# Vertex program for rendering GLVector instances.
-#
-#pragma include common_vert.prog
-END
diff --git a/fsl/fslview/gl/gl14/glvolume_frag.prog b/fsl/fslview/gl/gl14/glvolume_frag.prog
index 0e9bf8e0139c14d6ad3e77074c1937aa5d8d160f..4de3ebdece1915d5c16066d6c1aad06fff870beb 100644
--- a/fsl/fslview/gl/gl14/glvolume_frag.prog
+++ b/fsl/fslview/gl/gl14/glvolume_frag.prog
@@ -17,11 +17,8 @@
 #
 # Required inputs:
 #
-#   fragment.texcoord[0] - Fragment position in the display coordinate system
-#   fragment.texcoord[1] - Fragment position in the image voxel coordinate
-#                          system
-#   fragment.texcoord[2] - Optionally used as texture coordinates (see
-#                          program.local[7]). 
+#   fragment.texcoord[0] - Fragment texture coordinates
+#   fragment.texcoord[1] - Fragment voxel coordinates 
 #
 #   program.local[0]  
 #   program.local[1]
@@ -31,40 +28,26 @@
 #
 #   program.local[4]     - Image shape - number of voxels along the xyz
 #                          dimensions in the image
-#   program.local[5]     - Inverse of image shape
 # 
-#   program.local[6]     - Vector containing global brightness (x), contrast
-#                          (y), and alpha (z) values to pass to the
-#                          briconalpha.prog routine.
-#
-#   program.local[7]     - Vector containing clipping values - voxels with a
+#   program.local[5]     - Vector containing clipping values - voxels with a
 #                          value below the low threshold (x), or above the
 #                          high threshold (y) will not be shown. Assumed to
 #                          be normalised to the image texture value range.
 #
-#   program.local[8]     - If greater than or equal to 0, fragment.texcoord[1]
-#                          is used as the coordinates for the texture lookup.
-#                          Otherwise, fragment.texcoord[2] is used for the
-#                          texture lookup.
 #
 # Outputs:
 #
-#   result.color         - The fragment colour (written by briconalpha.prog)
+#   result.color         - The fragment colour
 #
 # Author: Paul McCarthy <pauldmccarthy@gmail.com>
 #
 
 TEMP  voxCoord;
 TEMP  voxClip;
-TEMP  normVoxCoord;
 TEMP  voxValue;
-TEMP  fragColour;
 
-PARAM imageShape    = program.local[4];
-PARAM imageShapeInv = program.local[5];
-PARAM bca           = program.local[6];
-PARAM clipping      = program.local[7];
-PARAM useTexCoords  = program.local[8];
+PARAM imageShape = program.local[4];
+PARAM clipping   = program.local[5];
   
 # This matrix scales the voxel value to
 # lie in a range which is appropriate to
@@ -75,27 +58,14 @@ PARAM voxValXform[4] = { program.local[0],
                          program.local[3] };
 
 # retrieve the voxel coordinates,
-# which should have been calculated
-# by the vertex program
-#
-# If useTexCoords is < 0, we use
-# fragment.texcoord[2] for the
-# texture lookup, otherwise we use
-# fragment.texcoord[1].
-CMP voxCoord, useTexCoords, fragment.texcoord[2], fragment.texcoord[1];
+# bail if they are are out of bounds  
+MOV voxCoord, fragment.texcoord[1];
 
-# bail if the voxel coordinates
-# are out of bounds
 #pragma include test_in_bounds.prog
 
-# Normalise voxel coordinates to 
-# lie in the range (0, 1), so they 
-# can be used for texture lookup
-MUL normVoxCoord, voxCoord, imageShapeInv;
-
 # look up image voxel value
 # from 3D image texture
-TEX voxValue, normVoxCoord, texture[0], 3D;
+TEX voxValue, fragment.texcoord[0], texture[0], 3D;
 
 # If the voxel value is outside the
 # clipping range, don't draw it
@@ -110,7 +80,7 @@ KIL voxClip;
 
 # Scale voxel value according
 # to the current display range
-MAD voxValue, voxValue, voxValXform[0].x, voxValXform[0].w;
+MAD voxValue, voxValue, voxValXform[0].x, voxValXform[3].x;
 
 # look up the appropriate colour
 # in the 1D colour map texture
diff --git a/fsl/fslview/gl/gl14/glvolume_funcs.py b/fsl/fslview/gl/gl14/glvolume_funcs.py
index b18f7a2c3f976ba4a7c9cfb9a7fd84b8b0a69564..0d42cd78be04ae5b7bebf65d2333e95ad46f75dc 100644
--- a/fsl/fslview/gl/gl14/glvolume_funcs.py
+++ b/fsl/fslview/gl/gl14/glvolume_funcs.py
@@ -14,20 +14,7 @@ This module depends upon two OpenGL ARB extensions, ARB_vertex_program and
 ARB_fragment_program which, being ancient (2002) technology, should be
 available on pretty much any graphics card in the wild today.
 
-This module provides the following functions:
-
- - :func:`init`: Compiles the vertex/fragment programs used in rendering.
-
- - :func:`genVertexData`: Generates and returns vertex and texture coordinates
-   for rendering a single 2D slice of a 3D image.
-
- - :func:`preDraw: Prepares the GL state for drawing.
-
- - :func:`draw`: Renders the current image slice.
-
- - :func:`postDraw: Resets the GL state after drawing.
-
- - :func:`destroy`: Deletes handles to the vertex/fragment programs
+See the :mod:`.gl21.glvolume_funcs` module for more details.
 
 This PDF is quite useful:
  - http://www.renderguild.com/gpuguide.pdf
@@ -41,25 +28,41 @@ import OpenGL.raw.GL._types           as gltypes
 import OpenGL.GL.ARB.fragment_program as arbfp
 import OpenGL.GL.ARB.vertex_program   as arbvp
 
-import fsl.utils.transform     as transform
-import fsl.fslview.gl.globject as globject
-import fsl.fslview.gl.shaders  as shaders
+import fsl.utils.transform    as transform
+import fsl.fslview.gl.shaders as shaders
 
 
 log = logging.getLogger(__name__)
 
 
-def init(self):
-    """Compiles the vertex and fragment programs used for rendering."""
+def compileShaders(self):
 
-    vertShaderSrc = shaders.getVertexShader(  self)
-    fragShaderSrc = shaders.getFragmentShader(self) 
+    if self.vertexProgram is not None:
+        arbvp.glDeleteProgramsARB(1, gltypes.GLuint(self.vertexProgram))
+        
+    if self.fragmentProgram is not None:
+        arbfp.glDeleteProgramsARB(1, gltypes.GLuint(self.fragmentProgram)) 
+
+    vertShaderSrc = shaders.getVertexShader(  self,
+                                              sw=self.display.softwareMode)
+    fragShaderSrc = shaders.getFragmentShader(self,
+                                              sw=self.display.softwareMode)
 
     vertexProgram, fragmentProgram = shaders.compilePrograms(
         vertShaderSrc, fragShaderSrc)
 
     self.vertexProgram   = vertexProgram
-    self.fragmentProgram = fragmentProgram
+    self.fragmentProgram = fragmentProgram    
+
+
+def init(self):
+    """Compiles the vertex and fragment programs used for rendering."""
+
+    self.vertexProgram   = None
+    self.fragmentProgram = None
+    
+    compileShaders(   self)
+    updateShaderState(self)
 
     
 def destroy(self):
@@ -68,27 +71,9 @@ def destroy(self):
     arbvp.glDeleteProgramsARB(1, gltypes.GLuint(self.vertexProgram))
     arbfp.glDeleteProgramsARB(1, gltypes.GLuint(self.fragmentProgram))
 
-    
-def genVertexData(self):
-    """Generates vertex coordinates required to render the image. See
-    :func:`fsl.fslview.gl.glvolume.genVertexData`.
-    """
-    
-    worldCoords, indices = self.genVertexData()
-    return worldCoords, indices, len(indices)
-
-
-def preDraw(self):
-    """Prepares to draw a slice from the given
-    :class:`~fsl.fslview.gl.glvolume.GLVolume` instance.
-    """
-
-    display = self.display
-    opts    = self.displayOpts
 
-    # enable drawing from a vertex array
-    gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
-    gl.glEnableClientState(gl.GL_TEXTURE_COORD_ARRAY)
+def updateShaderState(self):
+    opts = self.displayOpts
 
     # enable the vertex and fragment programs
     gl.glEnable(arbvp.GL_VERTEX_PROGRAM_ARB) 
@@ -99,12 +84,6 @@ def preDraw(self):
     arbfp.glBindProgramARB(arbfp.GL_FRAGMENT_PROGRAM_ARB,
                            self.fragmentProgram)
 
-    # The vertex program needs to be
-    # able to transform from display
-    # coordinates to voxel coordinates
-    shaders.setVertexProgramMatrix(
-        0, display.getTransform('display', 'voxel').T)
-
     # The voxValXform transformation turns
     # an image texture value into a raw
     # voxel value. The colourMapXform
@@ -112,25 +91,11 @@ def preDraw(self):
     # into a value between 0 and 1, suitable
     # for looking up an appropriate colour
     # in the 1D colour map texture
-    cmapXForm = transform.concat(self.imageTexture.voxValXform,
-                                 self.colourMapXform)
-    
-    shaders.setFragmentProgramMatrix(0, cmapXForm.T)
-
-    # The fragment program needs to know
-    # the image shape, and its inverse,
-    # because there's no division operation,
-    # and the RCP operation only works on
-    # scalars
+    voxValXform = transform.concat(self.imageTexture.voxValXform,
+                                   self.colourTexture.getCoordinateTransform())
     
-    # We also need to pass the global
-    # brightness/contrast/alpha values to
-    # the fragment program 
-    shape    = list(self.image.shape)
-    invshape = [1.0 / s for s in shape]
-    bca      = [display.brightness / 100.0, 
-                display.contrast   / 100.0,
-                display.alpha      / 100.0]
+    # The fragment program needs to know the image shape
+    shape = list(self.image.shape[:3])
 
     # And the clipping range, normalised
     # to the image texture value range
@@ -140,36 +105,55 @@ def preDraw(self):
     clipHi = opts.clippingRange[1]             * \
         self.imageTexture.invVoxValXform[0, 0] + \
         self.imageTexture.invVoxValXform[3, 0]
+
+    shaders.setVertexProgramVector(  0, shape + [0])
     
-    shaders.setFragmentProgramVector(4, shape    + [0])
-    shaders.setFragmentProgramVector(5, invshape + [0])
-    shaders.setFragmentProgramVector(6, bca      + [0])
-    shaders.setFragmentProgramVector(7, [clipLo, clipHi, 0, 0])
+    shaders.setFragmentProgramMatrix(0, voxValXform)
+    shaders.setFragmentProgramVector(4, shape + [0])
+    shaders.setFragmentProgramVector(5, [clipLo, clipHi, 0, 0])
 
+    gl.glDisable(arbvp.GL_VERTEX_PROGRAM_ARB) 
+    gl.glDisable(arbfp.GL_FRAGMENT_PROGRAM_ARB) 
 
-def draw(self, zpos, xform=None):
-    """Draws a slice of the image at the given Z location. """
 
-    worldCoords  = self.worldCoords
-    indices      = self.indices
-    worldCoords[:, self.zax] = zpos
+def preDraw(self):
+    """Prepares to draw a slice from the given
+    :class:`~fsl.fslview.gl.glvolume.GLVolume` instance.
+    """
+
+    # enable drawing from a vertex array
+    gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
+
+    gl.glClientActiveTexture(gl.GL_TEXTURE0)
+    gl.glEnableClientState(gl.GL_TEXTURE_COORD_ARRAY)
+
+    # enable the vertex and fragment programs
+    gl.glEnable(arbvp.GL_VERTEX_PROGRAM_ARB) 
+    gl.glEnable(arbfp.GL_FRAGMENT_PROGRAM_ARB)
+
+    arbvp.glBindProgramARB(arbvp.GL_VERTEX_PROGRAM_ARB,
+                           self.vertexProgram)
+    arbfp.glBindProgramARB(arbfp.GL_FRAGMENT_PROGRAM_ARB,
+                           self.fragmentProgram)
 
-    # Apply the custom xform if provided.
-    if xform is None:
-        xform = np.eye(4)
 
-    shaders.setVertexProgramMatrix(4, xform.T)
+def draw(self, zpos, xform=None):
+    """Draws a slice of the image at the given Z location. """
 
-    worldCoords = worldCoords.ravel('C')
+    
+    vertices, voxCoords, texCoords = self.generateVertices(zpos, xform)
+    
+    # Tex coords are texture 0 coords
+    # Vox coords are texture 1 coords
+    vertices  = np.array(vertices,  dtype=np.float32).ravel('C')
+    texCoords = np.array(texCoords, dtype=np.float32).ravel('C')
 
-    gl.glActiveTexture(gl.GL_TEXTURE0)
-    gl.glTexCoordPointer(3, gl.GL_FLOAT, 0, worldCoords)
-    gl.glVertexPointer(3, gl.GL_FLOAT, 0, worldCoords)
+    gl.glVertexPointer(3, gl.GL_FLOAT, 0, vertices)
 
-    gl.glDrawElements(gl.GL_TRIANGLE_STRIP,
-                      len(indices),
-                      gl.GL_UNSIGNED_INT,
-                      indices)
+    gl.glClientActiveTexture(gl.GL_TEXTURE0)
+    gl.glTexCoordPointer(3, gl.GL_FLOAT, 0, texCoords)
+    
+    gl.glDrawArrays(gl.GL_TRIANGLES, 0, 6)
 
     
 def drawAll(self, zposes, xforms):
@@ -177,38 +161,25 @@ def drawAll(self, zposes, xforms):
     applying the corresponding transformation to each of the slices.
     """
 
-    # Don't use a custom world-to-world
-    # transformation matrix.
-    shaders.setVertexProgramMatrix(4, np.eye(4))
-    
-    # Instead, tell the vertex
-    # program to use texture coordinates
-    shaders.setFragmentProgramVector(8, -np.ones(4))
+    nslices   = len(zposes)
+    vertices  = np.zeros((nslices * 6, 3), dtype=np.float32)
+    texCoords = np.zeros((nslices * 6, 3), dtype=np.float32)
 
-    worldCoords = np.array(self.worldCoords)
-    indices     = np.array(self.indices)
-    
-    # The world coordinates are ordered to 
-    # be rendered as a triangle strip, but
-    # we want to render as quads. So we
-    # need to re-order them
-    worldCoords[[2, 3], :] = worldCoords[[3, 2], :]
-
-    # Replicate the world coordinates
-    # across all z positions, and with
-    # each corresponding transformation
-    worldCoords, texCoords, indices = globject.broadcast(
-        worldCoords, indices, zposes, xforms, self.zax)
-
-    worldCoords = worldCoords.ravel('C')
-    texCoords   = texCoords  .ravel('C')
-
-    # Draw all of the slices with 
-    # these four function calls.
-    gl.glActiveTexture(gl.GL_TEXTURE0)
+    for i, (zpos, xform) in enumerate(zip(zposes, xforms)):
+        
+        v, vc, tc = self.generateVertices(zpos, xform)
+        vertices[ i * 6: i * 6 + 6, :] = v
+        texCoords[i * 6: i * 6 + 6, :] = tc
+
+    vertices  = vertices .ravel('C')
+    texCoords = texCoords.ravel('C')
+
+    gl.glVertexPointer(3, gl.GL_FLOAT, 0, vertices)
+
+    gl.glClientActiveTexture(gl.GL_TEXTURE0)
     gl.glTexCoordPointer(3, gl.GL_FLOAT, 0, texCoords)
-    gl.glVertexPointer(  3, gl.GL_FLOAT, 0, worldCoords)
-    gl.glDrawElements(gl.GL_QUADS, len(indices), gl.GL_UNSIGNED_INT, indices)
+
+    gl.glDrawArrays(gl.GL_TRIANGLES, 0, nslices * 6) 
 
 
 def postDraw(self):
@@ -217,6 +188,7 @@ def postDraw(self):
     """
 
     gl.glDisableClientState(gl.GL_VERTEX_ARRAY)
+    gl.glClientActiveTexture(gl.GL_TEXTURE0)
     gl.glDisableClientState(gl.GL_TEXTURE_COORD_ARRAY)
 
     gl.glDisable(arbfp.GL_FRAGMENT_PROGRAM_ARB)
diff --git a/fsl/fslview/gl/gl14/glvolume_sw_frag.prog b/fsl/fslview/gl/gl14/glvolume_sw_frag.prog
new file mode 100644
index 0000000000000000000000000000000000000000..f7c28841ec5b2f153793d6d775b05c311a3a0d63
--- /dev/null
+++ b/fsl/fslview/gl/gl14/glvolume_sw_frag.prog
@@ -0,0 +1,67 @@
+!!ARBfp1.0
+#
+# Fragment program used for rendering GLVolume instances.
+#
+# This fragment program does the following:
+# 
+#  - Retrieves the display space/voxel coordinates corresponding to the
+#    fragment
+# 
+#  - Uses those voxel coordinates to look up the corresponding voxel
+#    value in the 3D image texture.
+# 
+#  - Uses that voxel value to look up the corresponding colour in the
+#    1D colour map texture.
+# 
+#  - Sets the fragment colour.
+#
+# Required inputs:
+#
+#   fragment.texcoord[0] - Fragment texture coordinates
+#
+#   program.local[0]  
+#   program.local[1]
+#   program.local[2]
+#   program.local[3]     - Matrix which transforms voxel values into the range
+#                          [0, 1], for use as a colour map texture coordinate
+#
+#   program.local[5]     - Vector containing clipping values - voxels with a
+#                          value below the low threshold (x), or above the
+#                          high threshold (y) will not be shown. Assumed to
+#                          be normalised to the image texture value range.
+#
+#
+# Outputs:
+#
+#   result.color         - The fragment colour
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+
+TEMP voxClip;
+TEMP voxValue;
+  
+# look up image voxel value
+# from 3D image texture
+TEX voxValue, fragment.texcoord[0], texture[0], 3D;
+
+# If the voxel value is outside the
+# clipping range, don't draw it
+
+# Test the low clipping range
+SUB voxClip, voxValue.x, program.local[5].x;
+KIL voxClip;
+
+# And the high clipping range
+SUB voxClip, program.local[5].y, voxValue.x;
+KIL voxClip;
+
+# Scale voxel value according
+# to the current display range
+MAD voxValue, voxValue, program.local[0].x, program.local[3].x;
+
+# look up the appropriate colour
+# in the 1D colour map texture
+TEX result.color, voxValue.x, texture[1], 1D;
+
+END
diff --git a/fsl/fslview/gl/gl14/glvolume_sw_vert.prog b/fsl/fslview/gl/gl14/glvolume_sw_vert.prog
new file mode 100644
index 0000000000000000000000000000000000000000..c5040438129255d30ff815c9d0d98b565eab04c9
--- /dev/null
+++ b/fsl/fslview/gl/gl14/glvolume_sw_vert.prog
@@ -0,0 +1,23 @@
+!!ARBvp1.0
+#
+# Vertex program for rendering GLVolume instances.
+#
+# Inputs:
+#    state.matrix.mvp
+#    vertex.position
+#    vertex.texcoord[0]
+#
+#
+# Outputs:
+#    result.position
+#
+
+# Transform the vertex position into display coordinates
+DP4 result.position.x, state.matrix.mvp.row[0], vertex.position;
+DP4 result.position.y, state.matrix.mvp.row[1], vertex.position;
+DP4 result.position.z, state.matrix.mvp.row[2], vertex.position;
+DP4 result.position.w, state.matrix.mvp.row[3], vertex.position;
+
+MOV result.texcoord[0], vertex.texcoord[0];
+
+END
diff --git a/fsl/fslview/gl/gl14/glvolume_vert.prog b/fsl/fslview/gl/gl14/glvolume_vert.prog
index 3d0bbff43755ee4c1d43499a3282da2d90e9ba58..91fa583fc3174431174b13ffd8cd345c3fd924cb 100644
--- a/fsl/fslview/gl/gl14/glvolume_vert.prog
+++ b/fsl/fslview/gl/gl14/glvolume_vert.prog
@@ -2,5 +2,35 @@
 #
 # Vertex program for rendering GLVolume instances.
 #
-#pragma include common_vert.prog
+# Inputs:
+#    state.matrix.mvp
+#    vertex.position
+#    vertex.texcoord[0]
+#    program.local[0] - image shape
+#
+#
+# Outputs:
+#    result.position
+#    result.texcoord[0]
+#    result.texcoord[1]
+#
+
+PARAM imageShape = program.local[0];
+
+TEMP voxCoord;
+
+# Transform the vertex position into display coordinates
+DP4 result.position.x, state.matrix.mvp.row[0], vertex.position;
+DP4 result.position.y, state.matrix.mvp.row[1], vertex.position;
+DP4 result.position.z, state.matrix.mvp.row[2], vertex.position;
+DP4 result.position.w, state.matrix.mvp.row[3], vertex.position;
+
+MOV result.texcoord[0], vertex.texcoord[0];
+
+# Transform the voxel coordinates into texture coordinates
+MOV voxCoord, vertex.texcoord[0];
+MUL voxCoord, voxCoord, imageShape;
+
+MOV result.texcoord[1], voxCoord;
+
 END
diff --git a/fsl/fslview/gl/gl21/__init__.py b/fsl/fslview/gl/gl21/__init__.py
index 5f5a552bc887d6b2249a90bd70cc2f40004c756b..360c9cafe687bebc15183151878599ac7ae73b12 100644
--- a/fsl/fslview/gl/gl21/__init__.py
+++ b/fsl/fslview/gl/gl21/__init__.py
@@ -6,4 +6,5 @@
 #
 
 import glvolume_funcs
-import glvector_funcs
+import glrgbvector_funcs
+import gllinevector_funcs
diff --git a/fsl/fslview/gl/gl21/briconalpha.glsl b/fsl/fslview/gl/gl21/briconalpha.glsl
deleted file mode 100644
index 16c5b95a3bd957ce1f5e4a3ad9cd59317f995fb5..0000000000000000000000000000000000000000
--- a/fsl/fslview/gl/gl21/briconalpha.glsl
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Provides the briconalpha function, which applies global brightness,
- * contrast and alpha settings to a specified colour.
- *
- * Author: Paul McCarthy <pauldmccarthy@gmail.com>
- */
-
-/*
- * Brightness factor, assumed to lie between 0.0 and 1.0.
- */
-uniform float brightness;
-
-/*
- * Contrast factor, assumed to lie between 0.0 and 1.0.
- */
-uniform float contrast;
-
-/*
- * Opacity, assumed to lie between 0.0 and 1.0.
- */
-uniform float alpha;
-
-
-vec4 briconalpha(vec4 inputColour) {
-
-  float scale;
-  float offset;
-  vec4  outputColour = vec4(inputColour);
-
-  if (outputColour.a >= 1.0)
-    outputColour.a = alpha;
-
-  /*
-   * The brightness is applied as a linear offset,
-   * with 0.5 equivalent to an offset of 0.0.
-   */
-  offset = (brightness * 2 - 1);
-
-  /*
-   * If the contrast lies between 0.0 and 0.5, it is
-   * applied to the colour as a linear scaling factor.
-   */
-  scale = contrast * 2;
-
-  /*
-   * If the contrast lies between 0.5 and 0.1, it
-   * is applied as an exponential scaling factor,
-   * so lower values (closer to 0.5) have less of
-   * an effect than higher values (closer to 1.0).
-   */
-  if (contrast > 0.5) 
-    scale += exp((contrast - 0.5) * 6) - 1;
-
-  /*
-   * The contrast factor scales the existing colour
-   * range, but keeps the new range centred at 0.5.
-   */
-  outputColour.rgb += offset;
-  
-  outputColour.rgb  = clamp(outputColour.rgb, 0.0, 1.0);
-  outputColour.rgb  = (outputColour.rgb - 0.5) * scale + 0.5;
-    
-  return clamp(outputColour, 0.0, 1.0);
-}
diff --git a/fsl/fslview/gl/gl21/common_vert.glsl b/fsl/fslview/gl/gl21/common_vert.glsl
deleted file mode 100644
index 37246e94908236a9df2e528dee206ba7d0813e47..0000000000000000000000000000000000000000
--- a/fsl/fslview/gl/gl21/common_vert.glsl
+++ /dev/null
@@ -1,85 +0,0 @@
-/**
- * The common_vert function (and associated uniform/attribute/varying
- * definitions) contains logic which is common to all vertex shader
- * programs.
- *
- * The common_vert function calculates and sets vertex/texture
- * coordinates, for pass-through to the fragment shader, in both
- * screen space and voxel space.
- *
- * Author: Paul McCarthy <pauldmccarthy@gmail.com>
- */
-
-uniform mat4 displayToVoxMat;
-
-/*
- * Optional transformation matrix which is applied to all
- * vertex coordinates (used by the e.g. lightbox canvas to
- * organise individual slices in a row/column fashion).
- */
-uniform mat4 worldToWorldMat;
-
-/*
- * Display axes (xax = horizontal, yax = vertical, zax = depth)
- */
-uniform int xax;
-uniform int yax;
-uniform int zax;
-
-/*
- * X/Y vertex location
- */
-attribute vec2 worldCoords;
-
-/*
- * Z location
- */
-uniform float zCoord;
-
-/* 
- * Vertex display coordinates passed through to fragment shader.
- */ 
-varying vec3 fragDisplayCoords;
-
-/* 
- * Image voxel coordinates corresponding to this vertex.
- */ 
-varying vec3 fragVoxCoords;
-
-
-void common_vert(void) {
-
-    vec4 worldLoc = vec4(0, 0, 0, 1);
-    worldLoc[xax] = worldCoords.x;
-    worldLoc[yax] = worldCoords.y;
-    worldLoc[zax] = zCoord;
-
-    /*
-     * Transform the texture coordinates into voxel coordinates
-     */
-    fragVoxCoords = (displayToVoxMat * worldLoc).xyz;
-
-    /*
-     * Centre voxel coordinates - the display space of a voxel
-     * at a particular location (x, y, z) extends from
-     *
-     * (x-0.5, y-0.5, z-0.5)
-     *
-     * to
-     *
-     * (x+0.5, y+0.5, z+0.5),
-     *
-     * so we need to offset the coordinates by 0.5 to
-     * make the coordinates usable as voxel indices
-     */
-    fragVoxCoords += 0.5;
-
-    /*
-     * Pass the vertex coordinates as texture
-     * coordinates to the fragment shader
-     */
-    fragDisplayCoords = worldLoc.xyz; 
-
-    /* Transform the vertex coordinates to display space */
-    gl_Position = gl_ModelViewProjectionMatrix * worldToWorldMat * worldLoc;
-}
diff --git a/fsl/fslview/gl/gl21/gllinevector_funcs.py b/fsl/fslview/gl/gl21/gllinevector_funcs.py
new file mode 100644
index 0000000000000000000000000000000000000000..b0967f35cf79212e171a8b00b4de0b740d39c92a
--- /dev/null
+++ b/fsl/fslview/gl/gl21/gllinevector_funcs.py
@@ -0,0 +1,317 @@
+#!/usr/bin/env python
+#
+# gllinevector_funcs.py -
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+
+import logging
+
+import numpy                       as np
+import OpenGL.GL                   as gl
+import OpenGL.raw.GL._types        as gltypes
+
+import fsl.utils.transform         as transform
+import fsl.fslview.gl.resources    as glresources
+import fsl.fslview.gl.routines     as glroutines
+import fsl.fslview.gl.gllinevector as gllinevector
+import fsl.fslview.gl.shaders      as shaders
+
+
+log = logging.getLogger(__name__)
+
+
+def init(self):
+    
+    self.shaders        = None
+    self.vertexBuffer   = gl.glGenBuffers(1)
+    self.texCoordBuffer = gl.glGenBuffers(1)
+    self.vertexIDBuffer = gl.glGenBuffers(1)
+    self.lineVertices   = None
+
+    self._vertexResourceName = '{}_{}_vertices'.format(
+        type(self).__name__, id(self.image))
+    
+    display = self.display
+    opts    = self.opts
+
+    def vertexUpdate(*a):
+        
+        updateVertices(self)
+        self.updateShaderState()
+        self.onUpdate()
+
+    display.addListener('transform',  self.name, vertexUpdate)
+    display.addListener('resolution', self.name, vertexUpdate)
+    opts   .addListener('directed',   self.name, vertexUpdate)
+
+    compileShaders(   self)
+    updateShaderState(self)
+
+    
+def destroy(self):
+    gl.glDeleteBuffers(1, gltypes.GLuint(self.vertexBuffer))
+    gl.glDeleteBuffers(1, gltypes.GLuint(self.vertexIDBuffer))
+    gl.glDeleteBuffers(1, gltypes.GLuint(self.texCoordBuffer))
+    gl.glDeleteProgram(self.shaders)
+
+    self.display.removeListener('transform',  self.name)
+    self.display.removeListener('resolution', self.name)
+    self.opts   .removeListener('directed',   self.name)
+
+    if self.display.softwareMode:
+        glresources.delete(self._vertexResourceName)
+
+
+def compileShaders(self):
+    
+    if self.shaders is not None:
+        gl.glDeleteProgram(self.shaders)
+    
+    vertShaderSrc = shaders.getVertexShader(  self,
+                                              sw=self.display.softwareMode)
+    fragShaderSrc = shaders.getFragmentShader(self,
+                                              sw=self.display.softwareMode)
+    
+    self.shaders = shaders.compileShaders(vertShaderSrc, fragShaderSrc)
+
+    self.vertexPos          = gl.glGetAttribLocation( self.shaders,
+                                                      'vertex')
+    self.vertexIDPos        = gl.glGetAttribLocation( self.shaders,
+                                                      'vertexID')
+    self.texCoordPos        = gl.glGetAttribLocation( self.shaders,
+                                                      'texCoord') 
+    self.imageShapePos      = gl.glGetUniformLocation(self.shaders,
+                                                      'imageShape')
+    self.imageDimsPos       = gl.glGetUniformLocation(self.shaders,
+                                                      'imageDims') 
+    self.directedPos        = gl.glGetUniformLocation(self.shaders,
+                                                      'directed')
+    self.imageTexturePos    = gl.glGetUniformLocation(self.shaders,
+                                                      'imageTexture')
+    self.modTexturePos      = gl.glGetUniformLocation(self.shaders,
+                                                      'modTexture')
+    self.xColourTexturePos  = gl.glGetUniformLocation(self.shaders,
+                                                      'xColourTexture')
+    self.yColourTexturePos  = gl.glGetUniformLocation(self.shaders,
+                                                      'yColourTexture') 
+    self.zColourTexturePos  = gl.glGetUniformLocation(self.shaders,
+                                                      'zColourTexture')
+    self.modThresholdPos    = gl.glGetUniformLocation(self.shaders,
+                                                      'modThreshold') 
+    self.useSplinePos       = gl.glGetUniformLocation(self.shaders,
+                                                      'useSpline')
+    self.voxValXformPos     = gl.glGetUniformLocation(self.shaders,
+                                                      'voxValXform')
+    self.voxToDisplayMatPos = gl.glGetUniformLocation(self.shaders,
+                                                      'voxToDisplayMat') 
+    self.displayToVoxMatPos = gl.glGetUniformLocation(self.shaders,
+                                                      'displayToVoxMat') 
+    self.cmapXformPos       = gl.glGetUniformLocation(self.shaders,
+                                                      'cmapXform')
+    
+    updateVertices(self)
+
+    
+def updateShaderState(self):
+    
+    display = self.display
+    opts    = self.displayOpts
+
+    # The coordinate transformation matrices for 
+    # each of the three colour textures are identical,
+    # so we'll just use the xColourTexture matrix
+    cmapXform   = self.xColourTexture.getCoordinateTransform()
+    voxValXform = self.imageTexture.voxValXform
+    useSpline   = display.interpolation == 'spline'
+    imageShape  = np.array(self.image.shape[:3], dtype=np.float32)
+
+    voxValXform = np.array(voxValXform, dtype=np.float32).ravel('C')
+    cmapXform   = np.array(cmapXform,   dtype=np.float32).ravel('C')
+
+    gl.glUseProgram(self.shaders)
+
+    gl.glUniform1f( self.useSplinePos,     useSpline)
+    gl.glUniform3fv(self.imageShapePos, 1, imageShape)
+    
+    gl.glUniformMatrix4fv(self.voxValXformPos, 1, False, voxValXform)
+    gl.glUniformMatrix4fv(self.cmapXformPos,   1, False, cmapXform)
+
+    gl.glUniform1f(self.modThresholdPos, opts.modThreshold / 100.0)
+
+    gl.glUniform1i(self.imageTexturePos,   0)
+    gl.glUniform1i(self.modTexturePos,     1)
+    gl.glUniform1i(self.xColourTexturePos, 2)
+    gl.glUniform1i(self.yColourTexturePos, 3)
+    gl.glUniform1i(self.zColourTexturePos, 4)
+
+    if not display.softwareMode:
+        
+        directed  = opts.directed
+        imageDims = self.image.pixdim[:3]
+        d2vMat    = display.getTransform('display', 'voxel')
+        v2dMat    = display.getTransform('voxel',   'display')
+
+        imageDims = np.array(imageDims, dtype=np.float32)
+        d2vMat    = np.array(d2vMat,    dtype=np.float32).ravel('C')
+        v2dMat    = np.array(v2dMat,    dtype=np.float32).ravel('C')
+
+        gl.glUniform1f( self.directedPos,     directed)
+        gl.glUniform3fv(self.imageDimsPos, 1, imageDims)
+
+        gl.glUniformMatrix4fv(self.displayToVoxMatPos, 1, False, d2vMat)
+        gl.glUniformMatrix4fv(self.voxToDisplayMatPos, 1, False, v2dMat) 
+
+    gl.glUseProgram(0) 
+
+
+def updateVertices(self):
+
+    image   = self.image
+    display = self.display
+    opts    = self.opts
+
+    if not display.softwareMode:
+
+        self.lineVertices = None
+        
+        if glresources.exists(self._vertexResourceName):
+            log.debug('Clearing any cached line vertices for {}'.format(image))
+            glresources.delete(self._vertexResourceName)
+        return
+
+    if self.lineVertices is None:
+        self.lineVertices = glresources.get(
+            self._vertexResourceName, gllinevector.GLLineVertices, self)
+    
+    newHash = (hash(display.transform)  ^
+               hash(display.resolution) ^
+               hash(opts   .directed))
+
+    if hash(self.lineVertices) != newHash:
+
+        log.debug('Re-generating line vertices for {}'.format(image))
+        self.lineVertices.refresh(self)
+        glresources.set(self._vertexResourceName,
+                        self.lineVertices,
+                        overwrite=True)
+
+
+def preDraw(self):
+    gl.glUseProgram(self.shaders)
+
+
+def draw(self, zpos, xform=None):
+    if self.display.softwareMode: softwareDraw(self, zpos, xform)
+    else:                         hardwareDraw(self, zpos, xform)
+
+
+def softwareDraw(self, zpos, xform=None):
+
+    opts                = self.displayOpts
+    vertices, texCoords = self.lineVertices.getVertices(self, zpos)
+
+    if vertices.size == 0:
+        return
+    
+    vertices  = vertices .ravel('C')
+    texCoords = texCoords.ravel('C')
+
+    v2d = self.display.getTransform('voxel', 'display')
+
+    if xform is None: xform = v2d
+    else:             xform = transform.concat(v2d, xform)
+ 
+    gl.glPushMatrix()
+    gl.glMultMatrixf(np.array(xform, dtype=np.float32).ravel('C'))
+
+    # upload the vertices
+    gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.vertexBuffer)
+    gl.glBufferData(
+        gl.GL_ARRAY_BUFFER, vertices.nbytes, vertices, gl.GL_STATIC_DRAW)
+    gl.glVertexAttribPointer(
+        self.vertexPos, 3, gl.GL_FLOAT, gl.GL_FALSE, 0, None)
+    gl.glEnableVertexAttribArray(self.vertexPos)
+
+    # and the texture coordinates
+    gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.texCoordBuffer)
+    gl.glBufferData(
+        gl.GL_ARRAY_BUFFER, texCoords.nbytes, texCoords, gl.GL_STATIC_DRAW)
+    gl.glVertexAttribPointer(
+        self.texCoordPos, 3, gl.GL_FLOAT, gl.GL_FALSE, 0, None)
+    gl.glEnableVertexAttribArray(self.texCoordPos) 
+        
+    gl.glLineWidth(opts.lineWidth)
+    gl.glDrawArrays(gl.GL_LINES, 0, vertices.size / 3)
+
+    gl.glPopMatrix()
+    gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0)
+    gl.glDisableVertexAttribArray(self.vertexPos)
+
+
+def hardwareDraw(self, zpos, xform=None):
+
+    image      = self.image
+    display    = self.display
+    opts       = self.displayOpts
+    v2dMat     = self.display.getTransform('voxel', 'display')
+    resolution = np.array([display.resolution] * 3)
+
+    if display.transform == 'id':
+        resolution = resolution / min(image.pixdim[:3])
+    elif display.transform == 'pixdim':
+        resolution = map(lambda r, p: max(r, p), resolution, image.pixdim[:3])
+
+    vertices = glroutines.calculateSamplePoints(
+        image.shape,
+        resolution,
+        v2dMat,
+        self.xax,
+        self.yax)[0]
+    
+    vertices[:, self.zax] = zpos
+
+    vertices = np.repeat(vertices, 2, 0)
+    indices  = np.arange(vertices.shape[0], dtype=np.uint32)
+    vertices = vertices.ravel('C')
+
+    if xform is None: xform = v2dMat
+    else:             xform = transform.concat(v2dMat, xform)
+    
+    xform = np.array(xform, dtype=np.float32).ravel('C') 
+    gl.glUniformMatrix4fv(self.voxToDisplayMatPos, 1, False, xform)
+
+    # bind the vertex ID buffer
+    gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.vertexIDBuffer)
+    gl.glBufferData(
+        gl.GL_ARRAY_BUFFER, indices.nbytes, indices, gl.GL_STATIC_DRAW)
+    gl.glVertexAttribPointer(
+        self.vertexIDPos, 1, gl.GL_UNSIGNED_INT, gl.GL_FALSE, 0, None)
+    gl.glEnableVertexAttribArray(self.vertexIDPos)
+
+    # and the vertex buffer
+    gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.vertexBuffer)
+    gl.glBufferData(
+        gl.GL_ARRAY_BUFFER, vertices.nbytes, vertices, gl.GL_STATIC_DRAW)    
+    gl.glVertexAttribPointer(
+        self.vertexPos, 3, gl.GL_FLOAT, gl.GL_FALSE, 0, None)
+
+    gl.glEnableVertexAttribArray(self.vertexPos) 
+    gl.glEnableVertexAttribArray(self.vertexIDPos) 
+        
+    gl.glLineWidth(opts.lineWidth)
+    gl.glDrawArrays(gl.GL_LINES, 0, vertices.size / 3)
+
+    gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0)
+    gl.glDisableVertexAttribArray(self.vertexPos)
+    gl.glDisableVertexAttribArray(self.vertexIDPos) 
+
+
+def drawAll(self, zposes, xforms):
+
+    for zpos, xform in zip(zposes, xforms):
+        self.draw(zpos, xform)
+
+
+def postDraw(self):
+    gl.glUseProgram(0)
diff --git a/fsl/fslview/gl/gl21/gllinevector_sw_vert.glsl b/fsl/fslview/gl/gl21/gllinevector_sw_vert.glsl
new file mode 100644
index 0000000000000000000000000000000000000000..b82395d2b5d3b50eebe8e1ecdc709867a9377154
--- /dev/null
+++ b/fsl/fslview/gl/gl21/gllinevector_sw_vert.glsl
@@ -0,0 +1,19 @@
+/*
+ * OpenGL vertex shader used for rendering GLLineVector instances.
+ *
+ * Author: Paul McCarthy <pauldmccarthy@gmail.com>
+ */
+#version 120
+
+attribute vec3 vertex;
+
+attribute vec3 texCoord;
+
+varying vec3 fragTexCoord;
+
+void main(void) {
+
+  fragTexCoord = texCoord;
+
+  gl_Position = gl_ModelViewProjectionMatrix * vec4(vertex, 1);
+}
diff --git a/fsl/fslview/gl/gl21/gllinevector_vert.glsl b/fsl/fslview/gl/gl21/gllinevector_vert.glsl
new file mode 100644
index 0000000000000000000000000000000000000000..ad5865da1719f205352cf6d884c35cf530550543
--- /dev/null
+++ b/fsl/fslview/gl/gl21/gllinevector_vert.glsl
@@ -0,0 +1,130 @@
+/*
+ * OpenGL vertex shader used for rendering GLVector instances in
+ * line mode.
+ *
+ * Author: Paul McCarthy <pauldmccarthy@gmail.com>
+ */
+#version 120
+
+#pragma include spline_interp.glsl
+
+/*
+ * Vector image containing XYZ vector data.
+ */
+uniform sampler3D imageTexture;
+
+
+uniform mat4 displayToVoxMat;
+uniform mat4 voxToDisplayMat;
+
+
+/*
+ * Transformation matrix which transforms the
+ * vector texture data to its original data range.
+ */
+uniform mat4 voxValXform;
+
+
+/*
+ * Shape of the image texture.
+ */
+uniform vec3 imageShape;
+
+
+uniform bool directed;
+
+/*
+ * Dimensions of one voxel in the image texture.
+ */
+uniform vec3 imageDims;
+
+
+attribute vec3 vertex;
+
+/*
+ * Vertex index - the built-in gl_VertexID
+ * variable is not available in GLSL 120
+ */
+attribute float vertexID;
+
+
+varying vec3 fragVoxCoord;
+varying vec3 fragTexCoord;
+
+
+void main(void) {
+
+  vec3 texCoord;
+  vec3 vector;
+  vec3 voxCoord;
+  vec3 vertVoxCoord;
+
+  /*
+   * The vertVoxCoord vector contains the floating
+   * point voxel coordinates which correspond to the
+   * display coordinates of the current vertex.
+   */
+  vertVoxCoord = (displayToVoxMat * vec4(vertex, 1)).xyz;
+
+  /*
+   * The voxCoord vector contains the exact integer
+   * voxel coordinates - we cannot interpolate vector
+   * directions.
+   *
+   * There is no round function in GLSL 1.2, so we use
+   * floor(x + 0.5).
+   */  
+  voxCoord = floor(vertVoxCoord + 0.5);
+  
+  /*
+   * Normalise the voxel coordinates to [0.0, 1.0],
+   * so they can be used for texture lookup. Add
+   * 0.5 to the voxel coordinates first, to re-centre
+   * voxel coordinates from  from [i - 0.5, i + 0.5]
+   * to [i, i + 1].
+   */
+  texCoord = (voxCoord + 0.5) / imageShape;
+
+  /*
+   * Retrieve the vector values for this voxel
+   */
+  vector = texture3D(imageTexture, texCoord).xyz;
+
+  /*
+   * Transform the vector values  from their
+   * texture range of [0,1] to the original
+   * data range
+   */
+  vector *= voxValXform[0].x;
+  vector += voxValXform[3].x;
+
+  // Scale the vector so it has length 0.5 
+  vector /= 2 * length(vector);
+
+  /*
+   * Scale the vector by the minimum voxel length,
+   * so it is a unit vector within real world space 
+   */
+  vector /= imageDims / min(imageDims.x, min(imageDims.y, imageDims.z));
+
+  /*
+   * Vertices are coming in as line pairs - flip
+   * every second vertex about the origin
+   */
+  if (mod(vertexID, 2) == 1) {
+    if (directed) vector = vec3(0, 0, 0);
+    else          vector = -vector;
+  }
+
+  /*
+   * Output the final vertex position - offset
+   * the voxel coordinates by the vector values,
+   * and transform back to display coordinates
+   */
+  gl_Position = gl_ModelViewProjectionMatrix *
+                voxToDisplayMat              *
+                vec4(vertVoxCoord + vector, 1);
+
+  fragVoxCoord = voxCoord;
+  fragTexCoord = texCoord;
+}
diff --git a/fsl/fslview/gl/gl21/glrgbvector_funcs.py b/fsl/fslview/gl/gl21/glrgbvector_funcs.py
new file mode 100644
index 0000000000000000000000000000000000000000..65378fd73906d1a6ffade780efc00a7a476b554d
--- /dev/null
+++ b/fsl/fslview/gl/gl21/glrgbvector_funcs.py
@@ -0,0 +1,103 @@
+#!/usr/bin/env python
+#
+# glrgbvector_funcs.py -
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+
+import numpy                as np
+import OpenGL.GL            as gl
+import OpenGL.raw.GL._types as gltypes
+
+import fsl.fslview.gl.shaders as shaders
+import                           glvolume_funcs
+
+
+def init(self):
+    self.shaders = None
+
+    compileShaders(   self)
+    updateShaderState(self)
+
+    self.vertexAttrBuffer = gl.glGenBuffers(1)
+
+
+def destroy(self):
+    gl.glDeleteBuffers(1, gltypes.GLuint(self.vertexAttrBuffer))
+    gl.glDeleteProgram(self.shaders)
+
+    
+def compileShaders(self):
+
+    if self.shaders is not None:
+        gl.glDeleteProgram(self.shaders) 
+    
+    vertShaderSrc = shaders.getVertexShader(  self,
+                                              sw=self.display.softwareMode)
+    fragShaderSrc = shaders.getFragmentShader(self,
+                                              sw=self.display.softwareMode)
+    
+    self.shaders = shaders.compileShaders(vertShaderSrc, fragShaderSrc)
+
+    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.modTexturePos      = gl.glGetUniformLocation(self.shaders,
+                                                      'modTexture')
+    self.xColourTexturePos  = gl.glGetUniformLocation(self.shaders,
+                                                      'xColourTexture')
+    self.yColourTexturePos  = gl.glGetUniformLocation(self.shaders,
+                                                      'yColourTexture') 
+    self.zColourTexturePos  = gl.glGetUniformLocation(self.shaders,
+                                                      'zColourTexture')
+    self.modThresholdPos    = gl.glGetUniformLocation(self.shaders,
+                                                      'modThreshold') 
+    self.useSplinePos       = gl.glGetUniformLocation(self.shaders,
+                                                      'useSpline')
+    self.imageShapePos      = gl.glGetUniformLocation(self.shaders,
+                                                      'imageShape')
+    self.voxValXformPos     = gl.glGetUniformLocation(self.shaders,
+                                                      'voxValXform')
+    self.cmapXformPos       = gl.glGetUniformLocation(self.shaders,
+                                                      'cmapXform') 
+
+
+def updateShaderState(self):
+
+    display = self.display
+    opts    = self.displayOpts
+
+    # The coordinate transformation matrices for 
+    # each of the three colour textures are identical
+    voxValXform = self.imageTexture.voxValXform
+    cmapXform   = self.xColourTexture.getCoordinateTransform()
+    useSpline   = display.interpolation == 'spline'
+    imageShape  = np.array(self.image.shape, dtype=np.float32)
+
+    gl.glUseProgram(self.shaders)
+
+    gl.glUniform1f( self.useSplinePos,     useSpline)
+    gl.glUniform3fv(self.imageShapePos, 1, imageShape)
+
+    gl.glUniformMatrix4fv(self.voxValXformPos, 1, False, voxValXform)
+    gl.glUniformMatrix4fv(self.cmapXformPos,   1, False, cmapXform)
+
+    gl.glUniform1f(self.modThresholdPos,   opts.modThreshold / 100.0)
+    gl.glUniform1i(self.imageTexturePos,   0)
+    gl.glUniform1i(self.modTexturePos,     1)
+    gl.glUniform1i(self.xColourTexturePos, 2)
+    gl.glUniform1i(self.yColourTexturePos, 3)
+    gl.glUniform1i(self.zColourTexturePos, 4)
+
+    gl.glUseProgram(0)
+
+
+preDraw  = glvolume_funcs.preDraw
+draw     = glvolume_funcs.draw
+drawAll  = glvolume_funcs.drawAll
+postDraw = glvolume_funcs.postDraw
diff --git a/fsl/fslview/gl/gl21/glvector_frag.glsl b/fsl/fslview/gl/gl21/glvector_frag.glsl
index 5e21fdf2e667a17853315ff705012a014cb41824..971f5c9f4557b79cf3c6484f9130ad5cc2e19504 100644
--- a/fsl/fslview/gl/gl21/glvector_frag.glsl
+++ b/fsl/fslview/gl/gl21/glvector_frag.glsl
@@ -1,6 +1,6 @@
 /*
- * OpenGL fragment shader used for colouring GLVector instances in
- * both line and rgb modes.
+ * OpenGL fragment shader used for colouring GLRGBVector and GLLineVector
+ * instances.
  *
  * Author: Paul McCarthy <pauldmccarthy@gmail.com>
  */
@@ -8,7 +8,6 @@
 
 #pragma include spline_interp.glsl
 #pragma include test_in_bounds.glsl
-#pragma include briconalpha.glsl
 
 /*
  * Vector image containing XYZ vector data.
@@ -47,7 +46,10 @@ uniform sampler1D zColourTexture;
  * Matrix which transforms from vector image
  * texture values to their original data range.
  */
-uniform mat4 imageValueXform;
+uniform mat4 voxValXform;
+
+
+uniform mat4 cmapXform;
 
 /*
  * Shape of the image texture.
@@ -59,23 +61,21 @@ uniform vec3 imageShape;
  */
 uniform bool useSpline;
 
-
 /*
- * Coordinates of the fragment in display
+ * Coordinates of the fragment in voxel
  * coordinates, passed from the vertex shader.
  */
-varying vec3 fragDisplayCoords;
+varying vec3 fragVoxCoord;
 
 /*
- * Coordinates of the fragment in voxel
- * coordinates, passed from the vertex shader.
+ * Corresponding texture coordinates
  */
-varying vec3 fragVoxCoords;
+varying vec3 fragTexCoord;
 
 
 void main(void) {
 
-  vec3 voxCoords = fragVoxCoords;
+  vec3 voxCoords = fragVoxCoord;
 
   if (!test_in_bounds(voxCoords, imageShape)) {
 
@@ -83,41 +83,41 @@ void main(void) {
     return;
   }
 
-  /* 
-   * Normalise voxel coordinates to (0.0, 1.0)
-   */
-  voxCoords = voxCoords / imageShape;
-
   /*
    * Look up the xyz vector values
    */
   vec3 voxValue;
   if (useSpline) {
-    voxValue.x = spline_interp(imageTexture, voxCoords, imageShape, 0);
-    voxValue.y = spline_interp(imageTexture, voxCoords, imageShape, 1);
-    voxValue.z = spline_interp(imageTexture, voxCoords, imageShape, 2);
+    voxValue.x = spline_interp(imageTexture, fragTexCoord, imageShape, 0);
+    voxValue.y = spline_interp(imageTexture, fragTexCoord, imageShape, 1);
+    voxValue.z = spline_interp(imageTexture, fragTexCoord, imageShape, 2);
   }
   else {
-    voxValue = texture3D(imageTexture, voxCoords).xyz;
+    voxValue = texture3D(imageTexture, fragTexCoord).xyz;
+  }
+
+  /* Look up the modulation value */
+  float modValue;
+  if (useSpline) {
+    modValue = spline_interp(modTexture, fragTexCoord, imageShape, 0);
   }
+  else {
+    modValue = texture3D(modTexture, fragTexCoord).x;
+  }  
 
   /*
    * Transform the voxel texture values 
    * into a range suitable for colour texture
    * lookup, and take the absolute value
    */
-  voxValue *= imageValueXform[0].x;
-  voxValue += imageValueXform[0].w;
+  voxValue *= voxValXform[0].x;
+  voxValue += voxValXform[3].x;
   voxValue  = abs(voxValue);
+  voxValue *= cmapXform[0].x;
+  voxValue += cmapXform[3].x;
 
-  /* Look up the modulation value */
-  float modValue;
-  if (useSpline) {
-    modValue = spline_interp(modTexture, voxCoords, imageShape, 0);
-  }
-  else {
-    modValue = texture3D(modTexture, voxCoords).x;
-  }
+  /* Apply the modulation value */
+  voxValue *= modValue;
 
   /* Look up the colours for the xyz components */
   vec4 xColour = texture1D(xColourTexture, voxValue.x);
@@ -127,11 +127,12 @@ void main(void) {
   /* Combine those colours */
   vec4 voxColour = xColour + yColour + zColour;
 
-  /* Apply the modulation value */
-  voxColour.rgb = voxColour.rgb * modValue;
+  /* Take the highest alpha of the three colour maps */
+  voxColour.a = max(max(xColour.a, yColour.a), zColour.a);
 
+  /* Knock out voxels where the modulation value is below the threshold */
   if (modValue < modThreshold)
-    voxColour.a = 0.0;
+      voxColour.a = 0.0;
 
-  gl_FragColor = briconalpha(voxColour);
+  gl_FragColor = voxColour;
 }
diff --git a/fsl/fslview/gl/gl21/glvector_funcs.py b/fsl/fslview/gl/gl21/glvector_funcs.py
deleted file mode 100644
index 87324522f813b857e296ccd1536f23bd44c200a4..0000000000000000000000000000000000000000
--- a/fsl/fslview/gl/gl21/glvector_funcs.py
+++ /dev/null
@@ -1,287 +0,0 @@
-#!/usr/bin/env python
-#
-# glvector_funcs.py - Logic for rendering GLVector instances in an OpenGL 2.1
-#                     compatible manner.
-#
-# Author: Paul McCarthy <pauldmccarthy@gmail.com>
-#
-"""This module contains functions used by the
-:class:`~fsl.fslview.gl.glvector.GLVector` class, for rendering
-:class:`~fsl.data.image.Image` instances as vectors in an OpenGL 2.1
-compatible manner.
-
-See the ``GLVector`` documentation for more details.
-
-This OpenGL 2.1 implementation improves upon the OpenGL 1.4 implementation
-(see :mod:`~fsl.fslview.gl.gl14.glvector_funcs`) in that, in ``line`` mode,
-the vertices which represent vector lines are positioned by a custom vertex
-shader running on the GPU.
-"""
-
-import numpy                as np
-import OpenGL.GL            as gl
-import OpenGL.raw.GL._types as gltypes
-
-import fsl.fslview.gl.shaders  as shaders
-import fsl.fslview.gl.globject as globject
-
-
-def init(self):
-    """Compiles the vertex/fragment shaders used for rendering. A custom
-    vertex shader is used for ``line`` mode, but the same fragment shader
-    is used for both ``line`` and ``rgb`` modes. Also creates 
-    vertex and index buffers needed for storing vertices and indices.
-    """
-    mode      = self.displayOpts.displayMode
-    self.mode = mode
-
-    vertShaderSrc = shaders.getVertexShader(  'glvector_{}'.format(mode))
-    fragShaderSrc = shaders.getFragmentShader('glvector')
-
-    self.shaderParams = {}
-    self.shaders      = shaders.compileShaders(vertShaderSrc, fragShaderSrc)
-    
-    s = self.shaders
-    p = self.shaderParams 
-
-    self.worldCoordBuffer = gl.glGenBuffers(1)
-    self.indexBuffer      = gl.glGenBuffers(1)
-
-    # Line mode needs an extra vertex array
-    # which contains vertex indices (which
-    # are not built-in in OpenGL2.1), and
-    # some extra parameters.
-    if mode == 'line':
-        self.vertexIDBuffer   = gl.glGenBuffers(1)
-        p['voxToDisplayMat']  = gl.glGetUniformLocation(s, 'voxToDisplayMat')
-        p['vertexID']         = gl.glGetAttribLocation( s, 'vertexID')    
-
-    # parameers for glvector_rgb_vert.glsl/glvector_line_vert.glsl
-    p['displayToVoxMat'] = gl.glGetUniformLocation(s, 'displayToVoxMat')
-    p['worldToWorldMat'] = gl.glGetUniformLocation(s, 'worldToWorldMat')
-    p['xax']             = gl.glGetUniformLocation(s, 'xax')
-    p['yax']             = gl.glGetUniformLocation(s, 'yax')
-    p['zax']             = gl.glGetUniformLocation(s, 'zax')
-    p['zCoord']          = gl.glGetUniformLocation(s, 'zCoord')
-    p['worldCoords']     = gl.glGetAttribLocation( s, 'worldCoords')
-
-    # parameters for glvector_frag.glsl
-    p['imageTexture']    = gl.glGetUniformLocation(s, 'imageTexture')
-    p['modTexture']      = gl.glGetUniformLocation(s, 'modTexture')
-    p['xColourTexture']  = gl.glGetUniformLocation(s, 'xColourTexture')
-    p['yColourTexture']  = gl.glGetUniformLocation(s, 'yColourTexture')
-    p['zColourTexture']  = gl.glGetUniformLocation(s, 'zColourTexture')
-    p['imageValueXform'] = gl.glGetUniformLocation(s, 'imageValueXform')
-    p['imageShape']      = gl.glGetUniformLocation(s, 'imageShape')
-    p['imageDims']       = gl.glGetUniformLocation(s, 'imageDims')
-    p['useSpline']       = gl.glGetUniformLocation(s, 'useSpline')
-    p['modThreshold']    = gl.glGetUniformLocation(s, 'modThreshold')
-
-    p['alpha']           = gl.glGetUniformLocation(s, 'alpha')
-    p['brightness']      = gl.glGetUniformLocation(s, 'brightness')
-    p['contrast']        = gl.glGetUniformLocation(s, 'contrast')
-
-    
-def destroy(self):
-    """Deletes the vertex/fragment shader programs, and the vertex/index
-    buffers created in :func:`init`.
-    """
-
-    gl.glDeleteProgram(self.shaders)
-
-    gl.glDeleteBuffers(1, gltypes.GLuint(self.worldCoordBuffer))
-    gl.glDeleteBuffers(1, gltypes.GLuint(self.indexBuffer))
-
-    # extra vertex array buffer for line mode
-    if self.mode == 'line':
-        gl.glDeleteBuffers(1, gltypes.GLuint(self.vertexIDBuffer))
-
-        
-def setAxes(self):
-    """Generates geometry for rendering the vector image in either ``line``
-    mode or ``rgb`` mode.
-    """
-    mode = self.mode
-
-    if mode == 'line':
-        worldCoords, xpixdim, ypixdim, lenx, leny = \
-            globject.calculateSamplePoints(
-                self.image,
-                self.display,
-                self.xax,
-                self.yax)
-
-        worldCoords = np.repeat(worldCoords, 2, 0) 
-        indices     = np.arange(worldCoords.shape[0])
-
-    elif mode == 'rgb':
-        worldCoords, indices = globject.slice2D(
-            self.image.shape,
-            self.xax,
-            self.yax,
-            self.display.getTransform('voxel', 'display'))
-
-    worldCoords    = worldCoords[:, [self.xax, self.yax]]
-    worldCoords    = np.array(worldCoords, dtype=np.float32).ravel('C')
-    indices        = np.array(indices,     dtype=np.uint32) .ravel('C')
-    self.nVertices = len(indices)
-
-    gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.worldCoordBuffer)
-    gl.glBufferData(gl.GL_ARRAY_BUFFER, 
-                    worldCoords.nbytes,
-                    worldCoords,
-                    gl.GL_STATIC_DRAW)
-
-    gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, self.indexBuffer)
-    gl.glBufferData(gl.GL_ELEMENT_ARRAY_BUFFER,
-                    indices.nbytes,
-                    indices,
-                    gl.GL_STATIC_DRAW)
-        
-    if mode == 'line':
-
-        vertexIDs = np.array(indices, dtype=np.float32)
-        gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.vertexIDBuffer)
-        gl.glBufferData(gl.GL_ARRAY_BUFFER,
-                        vertexIDs.nbytes,
-                        vertexIDs,
-                        gl.GL_STATIC_DRAW)
-
-    gl.glBindBuffer(gl.GL_ARRAY_BUFFER,         0)
-    gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, 0)
-
-        
-def preDraw(self):
-    """Loads the vertex/fragment shaders, and binds shader parameters and
-    vertex/index arrays ready for drawing.
-    """
-
-    display = self.display
-    opts    = self.displayOpts
-    mode    = self.mode
-
-    modThreshold = opts.modThreshold / 100.0
-
-    gl.glUseProgram(self.shaders)
-    
-    pars            = self.shaderParams
-    imageShape      = np.array(self.image.shape[ :3], dtype=np.float32)
-    imageDims       = np.array(self.image.pixdim[:3], dtype=np.float32) 
-    displayToVoxMat = np.array(
-        display.getTransform('display', 'voxel'), dtype=np.float32)
-
-    if mode == 'line':
-        useSpline       = False
-        imageValueXform = np.array(self.imageTexture.voxValXform.T,
-                                   dtype=np.float32)
-    elif mode == 'rgb':
-        useSpline       = display.interpolation == 'spline'
-        imageValueXform = np.eye(4, dtype=np.float32)
-
-    displayToVoxMat = displayToVoxMat.ravel('C')
-    imageValueXform = imageValueXform.ravel('C')
-
-    gl.glUniform1f(       pars['modThreshold'],              modThreshold)
-    gl.glUniform1f(       pars['useSpline'],                 useSpline)
-    gl.glUniform3fv(      pars['imageShape'], 1,             imageShape)
-    gl.glUniform3fv(      pars['imageDims'],  1,             imageDims)
-    gl.glUniform1i(       pars['xax'],                       self.xax)
-    gl.glUniform1i(       pars['yax'],                       self.yax)
-    gl.glUniform1i(       pars['zax'],                       self.zax)
-    gl.glUniformMatrix4fv(pars['displayToVoxMat'], 1, False, displayToVoxMat)
-    gl.glUniformMatrix4fv(pars['imageValueXform'], 1, False, imageValueXform)
-    
-    gl.glUniform1i(       pars['imageTexture'],   0)
-    gl.glUniform1i(       pars['modTexture'],     1)
-    gl.glUniform1i(       pars['xColourTexture'], 2)
-    gl.glUniform1i(       pars['yColourTexture'], 3)
-    gl.glUniform1i(       pars['zColourTexture'], 4)
-
-    gl.glUniform1f(       pars['alpha'],      display.alpha      / 100.0)
-    gl.glUniform1f(       pars['brightness'], display.brightness / 100.0)
-    gl.glUniform1f(       pars['contrast'],   display.contrast   / 100.0)
-
-    # Bind the world x/y coordinate buffer
-    gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.worldCoordBuffer)
-    gl.glVertexAttribPointer(
-        pars['worldCoords'],
-        2,
-        gl.GL_FLOAT,
-        gl.GL_FALSE,
-        0,
-        None)
-    gl.glEnableVertexAttribArray(pars['worldCoords'])
-
-    # vox to display matrix, and vertex
-    # index buffer only needed for line mode
-    if mode == 'line':
-    
-        voxToDisplayMat = np.array(display.getTransform('voxel', 'display'),
-                                   dtype=np.float32)
-        voxToDisplayMat = voxToDisplayMat.ravel('C')                
-        gl.glUniformMatrix4fv(pars['voxToDisplayMat'],
-                              1,
-                              False,
-                              voxToDisplayMat)
-
-        gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.vertexIDBuffer)
-        gl.glVertexAttribPointer(
-            pars['vertexID'],
-            1,
-            gl.GL_FLOAT,
-            gl.GL_FALSE,
-            0,
-            None)
-        gl.glEnableVertexAttribArray(pars['vertexID'])
-        
-    # Bind the index buffer
-    gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, self.indexBuffer) 
-
-
-def draw(self, zpos, xform=None):
-    """Draws the vector image."""
-
-    if xform is None: xform = np.identity(4)
-    
-    xform = np.array(xform, dtype=np.float32).ravel('C')
-    pars  = self.shaderParams
-    mode  = self.mode
-
-    # Bind the current world z position, and
-    # the xform transformation matrix
-    gl.glUniform1f(       pars['zCoord'],                    zpos)
-    gl.glUniformMatrix4fv(pars['worldToWorldMat'], 1, False, xform)
-
-    # Draw all of the triangles!
-    if mode == 'line':
-        gl.glLineWidth(2)
-        gl.glDrawElements(gl.GL_LINES,
-                          self.nVertices,
-                          gl.GL_UNSIGNED_INT,
-                          None)
-        
-    elif mode == 'rgb':
-        gl.glDrawElements(gl.GL_TRIANGLE_STRIP,
-                          self.nVertices,
-                          gl.GL_UNSIGNED_INT,
-                          None)
-
-        
-def drawAll(self, zposes, xforms):
-    """Delegates to the :meth:`~fsl.fslview.gl.globject.GLObject.drawAll`
-    method.
-    """
-    globject.GLObject.drawAll(self, zposes, xforms)
-
-
-def postDraw(self):
-    """Unbinds vertex/index buffers."""
-
-    if self.mode == 'line':
-        gl.glDisableVertexAttribArray(self.shaderParams['vertexID'])
-        
-    gl.glDisableVertexAttribArray(self.shaderParams['worldCoords'])
-    gl.glBindBuffer(gl.GL_ARRAY_BUFFER,         0)
-    gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, 0)
-        
-    gl.glUseProgram(0) 
diff --git a/fsl/fslview/gl/gl21/glvector_line_vert.glsl b/fsl/fslview/gl/gl21/glvector_line_vert.glsl
deleted file mode 100644
index b7240414f1bd3126388ee4c1950a5b0186732f9b..0000000000000000000000000000000000000000
--- a/fsl/fslview/gl/gl21/glvector_line_vert.glsl
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * OpenGL vertex shader used for rendering GLVector instances in
- * line mode.
- *
- * Author: Paul McCarthy <pauldmccarthy@gmail.com>
- */
-#version 120
-
-#pragma include common_vert.glsl
-#pragma include spline_interp.glsl
-
-/*
- * Vector image containing XYZ vector data.
- */
-uniform sampler3D imageTexture;
-
-
-/*
- * Transformation matrix which transforms the
- * vector texture data to its original data range.
- */
-uniform mat4 imageValueXform;
-
-
-/*
- * Matrix which transforms from voxel
- * coordinates to display coordinates.
- */
-uniform mat4 voxToDisplayMat;
-
-/*
- * Shape of the image texture.
- */
-uniform vec3 imageShape;
-
-/*
- * Dimensions of one voxel in the image texture.
- */
-uniform vec3 imageDims;
-
-/*
- * Vertex index - the built-in gl_VertexID
- * variable is not available in GLSL 120
- */
-attribute float vertexID;
-
-
-void main(void) {
-
-  common_vert();
-
-  vec3 voxCoords = fragVoxCoords;
-  vec3 vertexPos = fragVoxCoords - 0.5;
-  vec3 vector;
-
-  /*
-   * Normalise the vertex coordinates to [0.0, 1.0],
-   * so they can be used for texture lookup. And make
-   * sure the voxel coordinates are exact integers 
-   * (actually, that they are centred within the
-   * voxel - see common_vert.glsl), as we cannot
-   * interpolate vector directions.
-   */
-  voxCoords = (floor(voxCoords) + 0.5) / imageShape;
-
-  /*
-   * Retrieve the vector values for this voxel
-   */
-  vector = texture3D(imageTexture, voxCoords).xyz;
-
-  /*
-   * Transform the vector values  from their
-   * texture range of [0,1] to the original
-   * data range
-   */
-  vector *= imageValueXform[0].x;
-  vector += imageValueXform[0].w;
-
-  vector *= 0.5;
-
-  /*
-   * Vertices are coming in as line pairs - flip
-   * every second vertex about the origin
-   */
-  if (mod(vertexID, 2) == 1) 
-    vector = -vector;
-
-  /*
-   * Scale the vector by the minimum voxel length,
-   * so it is a unit vector within real world space 
-   */
-  vector /= imageDims / min(imageDims.x, min(imageDims.y, imageDims.z));
-
-  /*
-   * Offset the vertex position 
-   * by the vector direction
-   */ 
-  vertexPos += vector;
-
-  /*
-   * Output the final vertex position
-   */
-  gl_Position = gl_ModelViewProjectionMatrix * 
-                worldToWorldMat * 
-                voxToDisplayMat * 
-                vec4(vertexPos, 1);
-}
diff --git a/fsl/fslview/gl/gl21/glvector_rgb_vert.glsl b/fsl/fslview/gl/gl21/glvector_rgb_vert.glsl
deleted file mode 100644
index 2f3d965b6e22b1c332863d62d9f72adf0382fb7a..0000000000000000000000000000000000000000
--- a/fsl/fslview/gl/gl21/glvector_rgb_vert.glsl
+++ /dev/null
@@ -1,12 +0,0 @@
-/*
- * OpenGL vertex shader for drawing GLVector instances in rgb mode.
- *
- * Author: Paul McCarthy <pauldmccarthy@gmail.com>
- */
-#version 120
-
-#pragma include common_vert.glsl
-
-void main(void) {
-  common_vert();
-}
diff --git a/fsl/fslview/gl/gl21/glvector_sw_frag.glsl b/fsl/fslview/gl/gl21/glvector_sw_frag.glsl
new file mode 100644
index 0000000000000000000000000000000000000000..a3fb6cbb17ca22c32908f0ac943b8253d4ac52c1
--- /dev/null
+++ b/fsl/fslview/gl/gl21/glvector_sw_frag.glsl
@@ -0,0 +1,97 @@
+/*
+ * OpenGL fragment shader used for colouring GLRGBVector and GLLineVector
+ * instances.
+ *
+ * Author: Paul McCarthy <pauldmccarthy@gmail.com>
+ */
+#version 120
+
+/*
+ * Vector image containing XYZ vector data.
+ */
+uniform sampler3D imageTexture;
+
+/*
+ * Modulation texture containing values by
+ * which the vector colours are to be modulated.
+ */
+uniform sampler3D modTexture;
+
+/*
+ * If the modulation value is below this
+ * threshold, the fragment is made
+ * transparent.
+ */
+uniform float modThreshold;
+
+/*
+ * Colour map for the X vector component.
+ */
+uniform sampler1D xColourTexture;
+
+/*
+ * Colour map for the Y vector component.
+ */
+uniform sampler1D yColourTexture;
+
+/*
+ * Colour map for the Z vector component.
+ */
+uniform sampler1D zColourTexture;
+
+/*
+ * Matrix which transforms from vector image
+ * texture values to their original data range.
+ */
+uniform mat4 voxValXform;
+
+
+uniform mat4 cmapXform;
+
+/*
+ * Corresponding texture coordinates
+ */
+varying vec3 fragTexCoord;
+
+
+void main(void) {
+
+  /*
+   * Look up the xyz vector values
+   */
+  vec3 voxValue = texture3D(imageTexture, fragTexCoord).xyz;
+
+  /* Look up the modulation value */
+  float modValue = texture3D(modTexture, fragTexCoord).x;
+
+  /*
+   * Transform the voxel texture values 
+   * into a range suitable for colour texture
+   * lookup, and take the absolute value
+   */
+  voxValue *= voxValXform[0].x;
+  voxValue += voxValXform[3].x;
+  voxValue  = abs(voxValue);
+  voxValue *= cmapXform[0].x;
+  voxValue += cmapXform[3].x;
+
+  /* Apply the modulation value */
+  voxValue *= modValue;
+
+  /* Look up the colours for the xyz components */
+  vec4 xColour = texture1D(xColourTexture, voxValue.x);
+  vec4 yColour = texture1D(yColourTexture, voxValue.y);
+  vec4 zColour = texture1D(zColourTexture, voxValue.z);
+
+  /* Combine those colours */
+  vec4 voxColour = xColour + yColour + zColour;
+
+  /* Take the highest alpha of the three colour maps */
+  voxColour.a = max(max(xColour.a, yColour.a), zColour.a);
+
+  /* Knock out voxels where the modulation value is below the threshold */
+  if (modValue < modThreshold)
+      voxColour.a = 0.0;
+
+  gl_FragColor = voxColour;
+}
diff --git a/fsl/fslview/gl/gl21/glvolume_frag.glsl b/fsl/fslview/gl/gl21/glvolume_frag.glsl
index 4481549d2166fc955f98851b0351c6fcb27af317..d24efa14d0d59847f23003312fcd5ad728a249bb 100644
--- a/fsl/fslview/gl/gl21/glvolume_frag.glsl
+++ b/fsl/fslview/gl/gl21/glvolume_frag.glsl
@@ -7,22 +7,22 @@
 
 #pragma include spline_interp.glsl
 #pragma include test_in_bounds.glsl
-#pragma include briconalpha.glsl
 
 /*
- * image data texture
+ * image data texture.
  */
 uniform sampler3D imageTexture;
 
 /*
- * Image/texture dimensions
+ * Texture containing the colour map.
  */
-uniform vec3 imageShape;
+uniform sampler1D colourTexture;
+
 
 /*
- * Texture containing the colour map
+ * Shape of the imageTexture.
  */
-uniform sampler1D colourTexture;
+uniform vec3 imageShape;
 
 /*
  * Use spline interpolation?
@@ -30,7 +30,9 @@ uniform sampler1D colourTexture;
 uniform bool useSpline;
 
 /*
- * Transformation matrix to apply to the 1D texture coordinate.
+ * Transformation matrix to apply to the voxel value,
+ * so it can be used as a texture coordinate in the
+ * colourTexture.
  */
 uniform mat4 voxValXform;
 
@@ -45,42 +47,40 @@ uniform float clipLow;
 uniform float clipHigh;
 
 /*
- * Image display coordinates. 
+ * Image voxel coordinates.
  */
-varying vec3 fragDisplayCoords;
+varying vec3 fragVoxCoord;
 
 
 /*
- * Image voxel coordinates
+ * Corresponding texture coordinates.
  */
-varying vec3 fragVoxCoords;
+varying vec3 fragTexCoord;
 
 
 void main(void) {
 
-    vec3 voxCoords = fragVoxCoords;
+    vec3 voxCoord = fragVoxCoord;
 
-    if (!test_in_bounds(voxCoords, imageShape)) {
+    /*
+     * Skip voxels that are out of the image bounds
+     */
+    if (!test_in_bounds(voxCoord, imageShape)) {
         
         gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
         return;
     }
 
-    /* 
-     * Normalise voxel coordinates to (0.0, 1.0)
-     */
-    voxCoords = voxCoords / imageShape;
-
     /*
      * Look up the voxel value 
      */
     float voxValue;
     if (useSpline) voxValue = spline_interp(imageTexture,
-                                            voxCoords,
+                                            fragTexCoord,
                                             imageShape,
                                             0);
     else           voxValue = texture3D(    imageTexture,
-                                            voxCoords).r;
+                                            fragTexCoord).r;
 
     /*
      * Clip out of range voxel values
diff --git a/fsl/fslview/gl/gl21/glvolume_funcs.py b/fsl/fslview/gl/gl21/glvolume_funcs.py
index 3171547f85f0de8b5a3932f969247fbadde64faa..36066ab9f4a0b75dba363e7d4b90e95ea78a9d61 100644
--- a/fsl/fslview/gl/gl21/glvolume_funcs.py
+++ b/fsl/fslview/gl/gl21/glvolume_funcs.py
@@ -14,156 +14,111 @@ program ``glvolume_frag.glsl``.
 
 This module provides the following functions:
 
- - :func:`init`: Compiles vertex and fragment shaders.
+ - :func:`init`:              Initialises GL objects and memory buffers.
 
- - :func:`genVertexData`: Generates and returns vertex and texture coordinates
-   for rendering a single 2D slice of a 3D image. Actually returns handles to
-   VBOs which encapsulate the vertex and texture coordinates.
+ - :func:`compileShaders`:    Compiles vertex/fragment shaders.
 
- - :func:`preDraw`:  Prepares the GL state for drawing.
+ - :func:`updateShaderState`: Refreshes the parameters used by the shader
+                              programs, controlling clipping and interpolation.
 
- - :func:`draw`:     Draws the scene.
+ - :func:`preDraw`:           Prepares the GL state for drawing.
 
- - :func:`postDraw`: Resets the GL state after drawing.
+ - :func:`draw`:              Draws the scene.
 
- - :func:`destroy`:  Deletes the vertex and texture coordinate VBOs.
+ - :func:`drawAll`:           Draws multiple scenes.
+
+ - :func:`postDraw`:          Resets the GL state after drawing.
+
+ - :func:`destroy`:           Deletes the vertex and texture coordinate VBOs.
 """
 
 import logging
+import ctypes
 
 import numpy                  as np
 import OpenGL.GL              as gl
 import OpenGL.raw.GL._types   as gltypes
 
-import fsl.fslview.gl.globject as globject
 import fsl.fslview.gl.shaders  as shaders
 import fsl.utils.transform     as transform
 
 
 log = logging.getLogger(__name__)
 
-def _compileShaders(self):
+
+def compileShaders(self):
     """Compiles and links the OpenGL GLSL vertex and fragment shader
     programs, and attaches a reference to the resulting program, and
-    all GLSL variables, to the given GLVolume object. 
+    all GLSL variables, to the given :class:`.GLVolume` object. 
     """
 
-    vertShaderSrc = shaders.getVertexShader(  self)
-    fragShaderSrc = shaders.getFragmentShader(self)
+    if self.shaders is not None:
+        gl.glDeleteProgram(self.shaders)
+
+    vertShaderSrc = shaders.getVertexShader(  self,
+                                              sw=self.display.softwareMode)
+    fragShaderSrc = shaders.getFragmentShader(self,
+                                              sw=self.display.softwareMode)
     self.shaders = shaders.compileShaders(vertShaderSrc, fragShaderSrc)
 
     # indices of all vertex/fragment shader parameters
-    self.worldToWorldMatPos = gl.glGetUniformLocation(self.shaders,
-                                                       'worldToWorldMat')
-    self.xaxPos             = gl.glGetUniformLocation(self.shaders,
-                                                       'xax')
-    self.yaxPos             = gl.glGetUniformLocation(self.shaders,
-                                                       'yax')
-    self.zaxPos             = gl.glGetUniformLocation(self.shaders,
-                                                       'zax')
-    self.worldCoordPos      = gl.glGetAttribLocation( self.shaders,
-                                                       'worldCoords') 
-    self.zCoordPos          = gl.glGetUniformLocation(self.shaders,
-                                                       'zCoord')
-    
+    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.imageShapePos      = gl.glGetUniformLocation(self.shaders,
-                                                       'imageShape')
+                                                      'imageTexture')
     self.colourTexturePos   = gl.glGetUniformLocation(self.shaders,
-                                                       'colourTexture') 
+                                                      'colourTexture') 
+    self.imageShapePos      = gl.glGetUniformLocation(self.shaders,
+                                                      'imageShape')
     self.useSplinePos       = gl.glGetUniformLocation(self.shaders,
-                                                       'useSpline')
-    self.displayToVoxMatPos = gl.glGetUniformLocation(self.shaders,
-                                                       'displayToVoxMat')
+                                                      'useSpline')
     self.voxValXformPos     = gl.glGetUniformLocation(self.shaders,
-                                                       'voxValXform')
+                                                      'voxValXform')
     self.clipLowPos         = gl.glGetUniformLocation(self.shaders,
-                                                       'clipLow')
+                                                      'clipLow')
     self.clipHighPos        = gl.glGetUniformLocation(self.shaders,
-                                                       'clipHigh') 
-
-    # self.alphaPos      = gl.glGetUniformLocation(self.shaders, 'alpha')
-    # self.brightnessPos = gl.glGetUniformLocation(self.shaders, 'brightness')
-    # self.contrastPos   = gl.glGetUniformLocation(self.shaders, 'contrast')
+                                                      'clipHigh') 
 
 
 def init(self):
     """Compiles the vertex and fragment shaders used to render image slices.
     """
-    _compileShaders(self)
-
-    self.worldCoordBuffer = gl.glGenBuffers(1)
-    self.indexBuffer      = gl.glGenBuffers(1) 
 
+    self.shaders = None
+    
+    compileShaders(   self)
+    updateShaderState(self)
+    
+    self.vertexAttrBuffer = gl.glGenBuffers(1)
+                    
 
 def destroy(self):
-    """Cleans up VBO handles."""
+    """Cleans up the vertex buffer handle and shader programs."""
 
-    gl.glDeleteBuffers(1, gltypes.GLuint(self.worldCoordBuffer))
-    gl.glDeleteBuffers(1, gltypes.GLuint(self.indexBuffer))
+    gl.glDeleteBuffers(1, gltypes.GLuint(self.vertexAttrBuffer))
     gl.glDeleteProgram(self.shaders)
 
 
-def genVertexData(self):
-    """Generates vertex and texture coordinates required to render the
-    image, and associates them with OpenGL VBOs . See
-    :func:`fsl.fslview.gl.glvolume.genVertexData`.
-    """ 
-    xax = self.xax
-    yax = self.yax
-
-    worldCoordBuffer     = self.worldCoordBuffer
-    indexBuffer          = self.indexBuffer
-    worldCoords, indices = self.genVertexData()
-
-    worldCoords = worldCoords[:, [xax, yax]]
-
-    worldCoords = worldCoords.ravel('C')
-    indices     = indices    .ravel('C')
-    
-    gl.glBindBuffer(gl.GL_ARRAY_BUFFER, worldCoordBuffer)
-    gl.glBufferData(gl.GL_ARRAY_BUFFER, 
-                    worldCoords.nbytes,
-                    worldCoords,
-                    gl.GL_STATIC_DRAW)
-
-    gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, indexBuffer)
-    gl.glBufferData(gl.GL_ELEMENT_ARRAY_BUFFER,
-                    indices.nbytes,
-                    indices,
-                    gl.GL_STATIC_DRAW)
-
-    gl.glBindBuffer(gl.GL_ARRAY_BUFFER,         0)
-    gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, 0)
-
-    return worldCoordBuffer, indexBuffer, len(indices)
-
-
-def preDraw(self):
-    """Sets up the GL state to draw a slice from the given
-    :class:`~fsl.fslview.gl.glvolume.GLVolume` instance.
+def updateShaderState(self):
+    """Updates the parameters used by the shader programs, reflecting the
+    current display properties.
     """
 
     display = self.display
     opts    = self.displayOpts
 
-    # load the shaders
     gl.glUseProgram(self.shaders)
-
+    
     # bind the current interpolation setting,
     # image shape, and image->screen axis
     # mappings
     gl.glUniform1f( self.useSplinePos,     display.interpolation == 'spline')
     gl.glUniform3fv(self.imageShapePos, 1, np.array(self.image.shape,
                                                      dtype=np.float32))
-    gl.glUniform1i( self.xaxPos,           self.xax)
-    gl.glUniform1i( self.yaxPos,           self.yax)
-    gl.glUniform1i( self.zaxPos,           self.zax)
-
-    # gl.glUniform1f( self.alphaPos,      display.alpha      / 100.0)
-    # gl.glUniform1f( self.brightnessPos, display.brightness / 100.0)
-    # gl.glUniform1f( self.contrastPos,   display.contrast   / 100.0)
 
     # The clipping range options are in the voxel value
     # range, but the shader needs them to be in image
@@ -183,39 +138,71 @@ def preDraw(self):
     # display coordinates to voxel coordinates,
     # and to scale voxel values to colour map
     # texture coordinates
-    tcx = transform.concat(self.imageTexture.voxValXform,
-                           self.colourMapXform)
-    w2v = np.array(
-        display.getTransform('display', 'voxel'), dtype=np.float32).ravel('C')
-    
-    vvx = np.array(tcx, dtype=np.float32).ravel('C')
+    vvx = transform.concat(self.imageTexture.voxValXform,
+                           self.colourTexture.getCoordinateTransform())
+    vvx = np.array(vvx, dtype=np.float32).ravel('C')
     
-    gl.glUniformMatrix4fv(self.displayToVoxMatPos, 1, False, w2v)
-    gl.glUniformMatrix4fv(self.voxValXformPos,     1, False, vvx)
+    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.colourTexturePos, 1)
+
+    gl.glUseProgram(0)
+
+
+def preDraw(self):
+    """Sets up the GL state to draw a slice from the given
+    :class:`~fsl.fslview.gl.glvolume.GLVolume` instance.
+    """
+
+    # load the shaders
+    gl.glUseProgram(self.shaders)
+
+
+def _prepareVertexAttributes(self, vertices, voxCoords, texCoords):
+    """Prepares a data buffer which contains the given vertices,
+    voxel coordinates, and texture coordinates, ready to be passed in to
+    the shader programs.
+    """
+
+    buf    = np.zeros((vertices.shape[0] * 3, 3), dtype=np.float32)
+    verPos = self.vertexPos
+    voxPos = self.voxCoordPos
+    texPos = self.texCoordPos
+
+    # We store each of the three coordinate
+    # sets in a single interleaved buffer
+    buf[ ::3, :] = vertices
+    buf[1::3, :] = voxCoords
+    buf[2::3, :] = texCoords
+
+    buf = buf.ravel('C')
+    
+    gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.vertexAttrBuffer)
+    gl.glBufferData(gl.GL_ARRAY_BUFFER, buf.nbytes, buf, gl.GL_STATIC_DRAW)
 
-    # Bind the world x/y coordinate buffer
-    gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.worldCoords)
     gl.glVertexAttribPointer(
-        self.worldCoordPos,
-        2,
-        gl.GL_FLOAT,
-        gl.GL_FALSE,
-        0,
-        None)
-    gl.glEnableVertexAttribArray(self.worldCoordPos)
+        verPos, 3, gl.GL_FLOAT, gl.GL_FALSE, 36, None)
+    gl.glVertexAttribPointer(
+        texPos, 3, gl.GL_FLOAT, gl.GL_FALSE, 36, ctypes.c_void_p(24))
 
-    # Bind the vertex index buffer
-    gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, self.indices)
+    # The fast shader does not use voxel coordinates
+    # so, on some GL drivers, attempting to bind it
+    # will cause an error
+    if not self.display.softwareMode:
+        gl.glVertexAttribPointer(
+            voxPos, 3, gl.GL_FLOAT, gl.GL_FALSE, 36, ctypes.c_void_p(12))
+        gl.glEnableVertexAttribArray(self.voxCoordPos)
 
+    gl.glEnableVertexAttribArray(self.vertexPos)
+    gl.glEnableVertexAttribArray(self.texCoordPos) 
 
+    
 def draw(self, zpos, xform=None):
     """Draws the specified slice from the specified image on the canvas.
 
-    :arg image:   The :class:`~fsl.fslview.gl.glvolume.GLVolume` object which
+    :arg self:    The :class:`~fsl.fslview.gl.glvolume.GLVolume` object which
                   is managing the image to be drawn.
     
     :arg zpos:    World Z position of slice to be drawn.
@@ -223,27 +210,32 @@ def draw(self, zpos, xform=None):
     :arg xform:   A 4*4 transformation matrix to be applied to the vertex
                   data.
     """
-    
-    if xform is None: xform = np.identity(4)
-    
-    w2w = np.array(xform, dtype=np.float32).ravel('C')
 
-    # Bind the current world z position, and
-    # the xform transformation matrix
-    gl.glUniform1f(       self.zCoordPos,                    zpos)
-    gl.glUniformMatrix4fv(self.worldToWorldMatPos, 1, False, w2w)
+    vertices, voxCoords, texCoords = self.generateVertices(zpos, xform)
+    _prepareVertexAttributes(self, vertices, voxCoords, texCoords)
+
+    gl.glDrawArrays(gl.GL_TRIANGLES, 0, 6)
 
-    # Draw all of the triangles!
-    gl.glDrawElements(gl.GL_TRIANGLE_STRIP,
-                      self.nVertices,
-                      gl.GL_UNSIGNED_INT,
-                      None)
 
 def drawAll(self, zposes, xforms):
     """Delegates to the default implementation in
     :meth:`~fsl.fslview.gl.globject.GLObject.drawAll`.
     """
-    globject.GLObject.drawAll(self, zposes, xforms)
+
+    nslices   = len(zposes)
+    vertices  = np.zeros((nslices * 6, 3), dtype=np.float32)
+    voxCoords = np.zeros((nslices * 6, 3), dtype=np.float32)
+    texCoords = np.zeros((nslices * 6, 3), dtype=np.float32)
+
+    for i, (zpos, xform) in enumerate(zip(zposes, xforms)):
+        
+        v, vc, tc = self.generateVertices(zpos, xform)
+        vertices[ i * 6: i * 6 + 6, :] = v
+        voxCoords[i * 6: i * 6 + 6, :] = vc
+        texCoords[i * 6: i * 6 + 6, :] = tc
+
+    _prepareVertexAttributes(self, vertices, voxCoords, texCoords)
+    gl.glDrawArrays(gl.GL_TRIANGLES, 0, 6 * nslices)
 
 
 def postDraw(self):
@@ -251,7 +243,11 @@ def postDraw(self):
     :class:`~fsl.fslview.gl.glvolume.GLVolume` instance.
     """
 
-    gl.glDisableVertexAttribArray(self.worldCoordPos)
-    gl.glBindBuffer(gl.GL_ARRAY_BUFFER,         0)
-    gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, 0)
+    gl.glDisableVertexAttribArray(self.vertexPos)
+    gl.glDisableVertexAttribArray(self.texCoordPos)
+
+    if not self.display.softwareMode:
+        gl.glDisableVertexAttribArray(self.voxCoordPos)
+    
+    gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0)
     gl.glUseProgram(0)
diff --git a/fsl/fslview/gl/gl21/glvolume_sw_frag.glsl b/fsl/fslview/gl/gl21/glvolume_sw_frag.glsl
new file mode 100644
index 0000000000000000000000000000000000000000..441aa999b3272a22eadd334c1cac812c0836aedc
--- /dev/null
+++ b/fsl/fslview/gl/gl21/glvolume_sw_frag.glsl
@@ -0,0 +1,74 @@
+/*
+ * Fast but limited OpenGL fragment shader used by
+ * fsl/fslview/gl/gl21/glvolume_funcs.py.
+ *
+ * Author: Paul McCarthy <pauldmccarthy@gmail.com>
+ */
+#version 120
+
+#pragma include spline_interp.glsl
+#pragma include test_in_bounds.glsl
+
+/*
+ * image data texture.
+ */
+uniform sampler3D imageTexture;
+
+/*
+ * Texture containing the colour map.
+ */
+uniform sampler1D colourTexture;
+
+
+/*
+ * Shape of the imageTexture.
+ */
+uniform vec3 imageShape;
+
+/*
+ * Transformation matrix to apply to the voxel value,
+ * so it can be used as a texture coordinate in the
+ * colourTexture.
+ */
+uniform mat4 voxValXform;
+
+/*
+ * Clip voxels below this value.
+ */
+uniform float clipLow;
+
+/*
+ * Clip voxels above this value.
+ */
+uniform float clipHigh;
+
+
+/*
+ * Corresponding texture coordinates.
+ */
+varying vec3 fragTexCoord;
+
+
+void main(void) {
+
+    /*
+     * Look up the voxel value 
+     */
+    float voxValue = texture3D(imageTexture, fragTexCoord).r;
+
+    /*
+     * Clip out of range voxel values
+     */
+    if (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
+     */
+    voxValue     = voxValXform[0].x * voxValue + voxValXform[3].x;
+    gl_FragColor = texture1D(colourTexture, voxValue);
+}
diff --git a/fsl/fslview/gl/gl21/glvolume_sw_vert.glsl b/fsl/fslview/gl/gl21/glvolume_sw_vert.glsl
new file mode 100644
index 0000000000000000000000000000000000000000..f7f4d0f8e587382fb764a3cfd1413d46c268ec8e
--- /dev/null
+++ b/fsl/fslview/gl/gl21/glvolume_sw_vert.glsl
@@ -0,0 +1,16 @@
+/*
+ * OpenGL vertex shader used for rendering GLVolume instances.
+ *
+ * Author: Paul McCarthy <pauldmccarthy@gmail.com>
+ */
+#version 120
+
+attribute vec3 vertex;
+attribute vec3 texCoord;
+varying   vec3 fragTexCoord;
+
+void main(void) {
+
+  fragTexCoord = texCoord;
+  gl_Position = gl_ModelViewProjectionMatrix * vec4(vertex, 1);
+}
diff --git a/fsl/fslview/gl/gl21/glvolume_vert.glsl b/fsl/fslview/gl/gl21/glvolume_vert.glsl
index 2a4739bb4b4200ac22b51603becf7b3b9f0d672d..474f98c4853aedbcaa76438a2d09d66682bc8531 100644
--- a/fsl/fslview/gl/gl21/glvolume_vert.glsl
+++ b/fsl/fslview/gl/gl21/glvolume_vert.glsl
@@ -1,12 +1,23 @@
 /*
  * OpenGL vertex shader used for rendering GLVolume instances.
+ * All this shader does is set the vertex position, and pass the
+ * voxel and texture coordinates through to the fragment shader.
  *
  * Author: Paul McCarthy <pauldmccarthy@gmail.com>
  */
 #version 120
 
-#pragma include common_vert.glsl
+attribute vec3 vertex;
+attribute vec3 voxCoord;
+attribute vec3 texCoord;
+
+varying vec3 fragVoxCoord;
+varying vec3 fragTexCoord;
 
 void main(void) {
-  common_vert();
+
+  fragVoxCoord = voxCoord;
+  fragTexCoord = texCoord;
+
+  gl_Position = gl_ModelViewProjectionMatrix * vec4(vertex, 1);
 }
diff --git a/fsl/fslview/gl/gllinevector.py b/fsl/fslview/gl/gllinevector.py
new file mode 100644
index 0000000000000000000000000000000000000000..949a62b4869df4c10e0b9f295434b6317a036381
--- /dev/null
+++ b/fsl/fslview/gl/gllinevector.py
@@ -0,0 +1,248 @@
+#!/usr/bin/env python
+#
+# gllinevector.py - Displays vector data as lines.
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+
+import logging
+
+import numpy                   as np
+
+import fsl.utils.transform     as transform
+import fsl.fslview.gl          as fslgl
+import fsl.fslview.gl.glvector as glvector
+import fsl.fslview.gl.routines as glroutines
+
+
+log = logging.getLogger(__name__)
+
+class GLLineVertices(object):
+    
+    def __init__(self, glvec):
+        
+        self.__hash = None
+        self.refresh(glvec)
+
+
+    def destroy(self):
+        self.vertices  = None
+        self.texCoords = None
+        self.starts    = None
+        self.steps     = None
+
+        
+    def __hash__(self):
+        return self.__hash
+
+        
+    def refresh(self, glvec):
+
+        display = glvec.display
+        opts    = glvec.opts
+        image   = glvec.image
+
+        # Extract a sub-sample of the vector image
+        # at the current display resolution
+        data, starts, steps = glroutines.subsample(image.data,
+                                                   display.resolution,
+                                                   image.pixdim)
+
+        # Pull out the xyz components of the 
+        # vectors, and calculate vector lengths
+        vertices = np.array(data, dtype=np.float32)
+        x        = vertices[:, :, :, 0]
+        y        = vertices[:, :, :, 1]
+        z        = vertices[:, :, :, 2]
+        lens     = np.sqrt(x ** 2 + y ** 2 + z ** 2)
+
+        # scale the vector lengths to 0.5
+        vertices[:, :, :, 0] = 0.5 * x / lens
+        vertices[:, :, :, 1] = 0.5 * y / lens
+        vertices[:, :, :, 2] = 0.5 * z / lens
+
+        # Scale the vector data by the minimum
+        # voxel length, so it is a unit vector
+        # within real world space
+        vertices /= (image.pixdim[:3] / min(image.pixdim[:3]))
+        
+        # Duplicate vector data so that each
+        # vector is represented by two vertices,
+        # representing a line through the origin.
+        # Or, if displaying directed vectors,
+        # add an origin point for each vector.
+        if opts.directed:
+            origins  = np.zeros(vertices.shape, dtype=np.float32)
+            vertices = np.concatenate((origins, vertices), axis=3)
+        else:
+            vertices = np.concatenate((-vertices, vertices), axis=3)
+            
+        vertices = vertices.reshape((data.shape[0],
+                                     data.shape[1],
+                                     data.shape[2],
+                                     2,
+                                     3))
+
+        # Offset each vertex by the corresponding
+        # voxel coordinates, making sure to
+        # transform from the sub-sampled indices
+        # to the original data indices (offseting
+        # and scaling by the starts and steps)
+        for i in range(data.shape[0]):
+            vertices[i, :, :, :, 0] += starts[0] + i * steps[0]
+            
+        for i in range(data.shape[1]):
+            vertices[:, i, :, :, 1] += starts[1] + i * steps[1]
+            
+        for i in range(data.shape[2]):
+            vertices[:, :, i, :, 2] += starts[2] + i * steps[2]
+
+        texCoords = vertices.round()
+        texCoords = (texCoords + 0.5) / np.array(image.shape[:3],
+                                                 dtype=np.float32)
+
+        self.vertices  = vertices
+        self.texCoords = texCoords
+        self.starts    = starts
+        self.steps     = steps
+        self.__hash    = (hash(display.transform)  ^
+                          hash(display.resolution) ^
+                          hash(opts   .directed))
+ 
+
+    def getVertices(self, glvec, zpos):
+
+        display = glvec.display
+        image   = glvec.image
+        xax     = glvec.xax
+        yax     = glvec.yax
+        zax     = glvec.zax
+
+        vertices  = self.vertices
+        texCoords = self.texCoords
+        starts    = self.starts
+        steps     = self.steps
+        
+        # If in id/pixdim space, the display
+        # coordinate system axes are parallel
+        # to the voxeld coordinate system axes
+        if display.transform in ('id', 'pixdim'):
+
+            # Turn the z position into a voxel index
+            if display.transform == 'pixdim':
+                zpos = zpos / image.pixdim[zax]
+
+            zpos = round(zpos)
+
+            # Return no vertices if the requested z
+            # position is out of the image bounds
+            if zpos < 0 or zpos >= image.shape[zax]:
+                return (np.array([], dtype=np.float32),
+                        np.array([], dtype=np.float32))
+
+            # Extract a slice at the requested
+            # z position from the vertex matrix
+            coords      = [slice(None)] * 3
+            coords[zax] = np.floor((zpos - starts[zax]) / steps[zax])
+
+        # If in affine space, the display
+        # coordinate system axes may not
+        # be parallel to the voxel
+        # coordinate system axes
+        else:
+            # Create a coordinate grid through
+            # a plane at the requested z pos 
+            # in the display coordinate system
+            coords = glroutines.calculateSamplePoints(
+                image.shape[ :3],
+                [display.resolution] * 3,
+                display.getTransform('voxel', 'display'),
+                xax,
+                yax)[0]
+            
+            coords[:, zax] = zpos
+
+            # transform that plane of display
+            # coordinates into voxel coordinates
+            coords = transform.transform(
+                coords, display.getTransform('display', 'voxel'))
+
+            # The voxel vertex matrix may have
+            # been sub-sampled (see the
+            # generateLineVertices method),
+            # so we need to transform the image
+            # data voxel coordinates to the
+            # sub-sampled data voxel coordinates.
+            coords = (coords - starts) / steps
+            
+            # remove any out-of-bounds voxel coordinates
+            shape  = vertices.shape[:3]
+            coords = np.array(coords.round(), dtype=np.int32)
+            coords = coords[((coords >= [0, 0, 0]) &
+                             (coords <  shape)).all(1), :].T
+
+        # pull out the vertex data, and the
+        # corresponding texture coordinates
+        vertices  = vertices[ coords[0], coords[1], coords[2], :, :]
+        texCoords = texCoords[coords[0], coords[1], coords[2], :, :]
+        
+        return vertices, texCoords
+
+
+class GLLineVector(glvector.GLVector):
+
+
+    def __init__(self, image, display):
+        
+        glvector.GLVector.__init__(self, image, display)
+        
+        self.opts = display.getDisplayOpts()
+        
+        fslgl.gllinevector_funcs.init(self)
+
+        def update(*a):
+            self.onUpdate()
+
+        self.opts.addListener('lineWidth', self.name, update)
+
+        
+    def destroy(self):
+        glvector.GLVector.destroy(self)
+        fslgl.gllinevector_funcs.destroy(self)
+        
+        self.opts.removeListener('lineWidth', self.name)
+
+
+    def getDataResolution(self, xax, yax):
+
+        res       = list(glvector.GLVector.getDataResolution(self, xax, yax))
+        res[xax] *= 16
+        res[yax] *= 16
+        
+        return res
+
+
+    def compileShaders(self):
+        fslgl.gllinevector_funcs.compileShaders(self)
+        
+
+    def updateShaderState(self):
+        fslgl.gllinevector_funcs.updateShaderState(self)
+ 
+
+    def preDraw(self):
+        glvector.GLVector.preDraw(self)
+        fslgl.gllinevector_funcs.preDraw(self)
+
+
+    def draw(self, zpos, xform=None):
+        fslgl.gllinevector_funcs.draw(self, zpos, xform)
+
+    
+    def drawAll(self, zposes, xforms):
+        fslgl.gllinevector_funcs.drawAll(self, zposes, xforms) 
+
+    
+    def postDraw(self):
+        glvector.GLVector.postDraw(self)
+        fslgl.gllinevector_funcs.postDraw(self) 
diff --git a/fsl/fslview/gl/glmask.py b/fsl/fslview/gl/glmask.py
index 36231c7c0e21a5e99f2db8cc1f6960edf4ddca2a..26ee689e6c2a893c9f97a39a110ca38286ccd766 100644
--- a/fsl/fslview/gl/glmask.py
+++ b/fsl/fslview/gl/glmask.py
@@ -21,7 +21,6 @@ import logging
 
 
 import numpy                   as np
-import OpenGL.GL               as gl
 
 import fsl.fslview.gl.glvolume as glvolume
 
@@ -50,9 +49,11 @@ class GLMask(glvolume.GLVolume):
         """
         def vertexUpdate(*a):
             self.setAxes(self.xax, self.yax)
+            self.onUpdate()
 
         def colourUpdate(*a):
             self.refreshColourTexture()
+            self.onUpdate()
 
         lnrName = '{}_{}'.format(type(self).__name__, id(self))
 
@@ -96,61 +97,23 @@ class GLMask(glvolume.GLVolume):
         is set).
         """
 
-        opts   = self.displayOpts
-        colour = opts.colour
-        imin   = opts.threshold[0]
-        imax   = opts.threshold[1]
+        display = self.display
+        opts    = self.displayOpts
+        alpha   = display.alpha / 100.0
+        colour  = opts.colour
+        dmin    = opts.threshold[0]
+        dmax    = opts.threshold[1]
         
         colour[3] = 1.0
 
-        # This transformation is used to transform voxel values
-        # from their native range to the range [0.0, 1.0], which
-        # is required for texture colour lookup. Values below
-        # or above the current display range will be mapped
-        # to texture coordinate values less than 0.0 or greater
-        # than 1.0 respectively.
-        if imax == imin: scale = 1
-        else:            scale = imax - imin
-            
-        cmapXform = np.identity(4, dtype=np.float32)
-        cmapXform[0, 0] = 1.0 / scale
-        cmapXform[3, 0] = -imin * cmapXform[0, 0]
-
-        self.colourMapXform = cmapXform
-
         if opts.invert:
-            colourmap = np.tile([[0.0, 0.0, 0.0, 0.0]], (16, 1))
-            border    = np.array(opts.colour, dtype=np.float32)
+            cmap   = np.tile([0.0, 0.0, 0.0, 0.0], (4, 1))
+            border = np.array(opts.colour, dtype=np.float32)
         else:
-            colourmap = np.tile([[opts.colour]], (16, 1))
-            border    = np.array([0.0, 0.0, 0.0, 0.0], dtype=np.float32)            
-
-        colourmap = np.floor(colourmap * 255)
-        colourmap = np.array(colourmap, dtype=np.uint8)
-        colourmap = colourmap.ravel('C')
+            cmap   = np.tile([opts.colour],        (4, 1))
+            border = np.array([0.0, 0.0, 0.0, 0.0], dtype=np.float32)
 
-        gl.glBindTexture(gl.GL_TEXTURE_1D, self.colourTexture)
-
-        gl.glTexParameterfv(gl.GL_TEXTURE_1D,
-                            gl.GL_TEXTURE_BORDER_COLOR,
-                            border)
-        
-        gl.glTexParameteri(gl.GL_TEXTURE_1D,
-                           gl.GL_TEXTURE_MAG_FILTER,
-                           gl.GL_NEAREST)
-        gl.glTexParameteri(gl.GL_TEXTURE_1D,
-                           gl.GL_TEXTURE_MIN_FILTER,
-                           gl.GL_NEAREST)
-        gl.glTexParameteri(gl.GL_TEXTURE_1D,
-                           gl.GL_TEXTURE_WRAP_S,
-                           gl.GL_CLAMP_TO_BORDER)
-
-        gl.glTexImage1D(gl.GL_TEXTURE_1D,
-                        0,
-                        gl.GL_RGBA8,
-                        16,
-                        0,
-                        gl.GL_RGBA,
-                        gl.GL_UNSIGNED_BYTE,
-                        colourmap)
-        gl.glBindTexture(gl.GL_TEXTURE_1D, 0)        
+        self.colourTexture.set(cmap=cmap,
+                               border=border,
+                               displayRange=(dmin, dmax),
+                               alpha=alpha)
diff --git a/fsl/fslview/gl/globject.py b/fsl/fslview/gl/globject.py
index d89500263f65cb5c22592eea9024b3ecae006e1e..88e10133759c808c69bdf4634cfd79c9a5a6ae29 100644
--- a/fsl/fslview/gl/globject.py
+++ b/fsl/fslview/gl/globject.py
@@ -6,25 +6,15 @@
 # Author: Paul McCarthy <pauldmccarthy@gmail.com>
 #
 """This module defines the :class:`GLObject` class, which is a superclass for
-all 2D OpenGL representations of :class:`fsl.data.image.Image` instances.
+all 2D representations of objects in OpenGL.
 
 This module also provides the :func:`createGLObject` function, which provides
 mappings between :class:`~fsl.data.image.Image` types, and their corresponding
 OpenGL representation.
-
-Some other convenience functions are also provided, for generating
-OpenGL vertex data.
 """
 
 
-import logging
-
-import itertools           as it
-import numpy               as np
-import fsl.utils.transform as transform
-
-
-log = logging.getLogger(__name__)
+import numpy as np
 
 
 def createGLObject(image, display):
@@ -35,14 +25,16 @@ def createGLObject(image, display):
     :arg display: A :class:`~fsl.fslview.displaycontext.Display` instance.
     """
 
-    import fsl.fslview.gl.glvolume as glvolume
-    import fsl.fslview.gl.glmask   as glmask
-    import fsl.fslview.gl.glvector as glvector
+    import fsl.fslview.gl.glvolume     as glvolume
+    import fsl.fslview.gl.glmask       as glmask
+    import fsl.fslview.gl.glrgbvector  as glrgbvector
+    import fsl.fslview.gl.gllinevector as gllinevector
 
     _objectmap = {
-        'volume' : glvolume.GLVolume,
-        'mask'   : glmask  .GLMask,
-        'vector' : glvector.GLVector
+        'volume'     : glvolume    .GLVolume,
+        'mask'       : glmask      .GLMask,
+        'rgbvector'  : glrgbvector .GLRGBVector,
+        'linevector' : gllinevector.GLLineVector
     } 
 
     ctr = _objectmap.get(display.imageType, None)
@@ -60,23 +52,49 @@ class GLObject(object):
         """Create a :class:`GLObject`.  The constructor adds one attribute to
         this instance, ``name``, which is simply a unique name for this
         instance.
+
+        Subclass implementations must call this method, and should also
+        perform any necessary OpenGL initialisation, such as creating
+        textures.
         """
 
         self.name = '{}_{}'.format(type(self).__name__, id(self))
-                
+        self.__updateListeners = {}
+
+        
+    def addUpdateListener(self, name, listener):
+        """Adds a listener function which will be called whenever this
+        ``GLObject`` representation changes.
 
-    def init(self):
-        """Perform any necessary OpenGL initialisation, such as
-        creating textures and vertices.
+        The listener function must accept a single parameter, which is
+        a reference to this ``GLObject``.
         """
-        raise NotImplementedError()
+        self.__updateListeners[name] = listener
 
-    
-    def ready(self):
-        """This method should return ``False`` if this :class:`GLObject` is
-        not yet ready to be displayed, ``True`` otherwise.
+        
+    def removeUpdateListener(self, name):
+        """Removes a listener previously registered via
+        :meth:`addUpdateListener`.
         """
-        raise NotImplementedError()
+        self.__updateListeners.pop(name, None)
+
+
+    def onUpdate(self):
+        """This method must be called by subclasses whenever the GL object
+        representation changes - it notifies any registered listeners of the
+        change.
+        """
+        for name, listener in self.__updateListeners.items():
+            listener(self)
+
+
+    def getDisplayBounds(self):
+        raise NotImplementedError('The getDisplayBounds method must be '
+                                  'implemented by GLObject subclasses')
+
+
+    def getDataResolution(self, xax, yax):
+        return None
 
     
     def setAxes(self, xax, yax):
@@ -167,18 +185,14 @@ class GLSimpleObject(GLObject):
 
     def __init__(self):
         GLObject.__init__(self)
-        self.__ready = False
-
-    def init(   self): pass
-    def destroy(self): pass
-    def ready(  self): return self.__ready
 
     def setAxes(self, xax, yax):
-        self.xax     =  xax
-        self.yax     =  yax
-        self.zax     = 3 - xax - yax
-        self.__ready = True
+        self.xax =  xax
+        self.yax =  yax
+        self.zax = 3 - xax - yax
 
+
+    def destroy(self): pass
     def preDraw( self): pass
     def postDraw(self): pass
 
@@ -209,466 +223,25 @@ class GLImageObject(GLObject):
         self.displayOpts = display.getDisplayOpts()
 
 
-def calculateSamplePoints(image, display, xax, yax):
-    """Calculates a uniform grid of points, in the display coordinate system
-    (as specified by the given
-    :class:`~fsl.fslview.displaycontext.ImageDisplay` object properties) along
-    the x-y plane (as specified by the xax/yax indices), at which the given
-    image should be sampled for display purposes.
-
-    This function returns a tuple containing:
-
-     - a numpy array of shape `(N, 3)`, containing the coordinates of the
-       centre of every sampling point in real world space.
+    def getDisplayBounds(self):
+        return self.display.getDisplayBounds()
 
-     - the horizontal distance (along xax) between adjacent points
 
-     - the vertical distance (along yax) between adjacent points
+    def getDataResolution(self, xax, yax):
 
-     - The number of samples along the horizontal axis (xax)
-
-     - The number of samples along the vertical axis (yax)
-
-    :arg image:   The :class:`~fsl.data.image.Image` object to
-                  generate vertex and texture coordinates for.
-
-    :arg display: A :class:`~fsl.fslview.displaycontext.ImageDisplay`
-                  object which defines how the image is to be
-                  rendered.
-
-    :arg xax:     The world space axis which corresponds to the
-                  horizontal screen axis (0, 1, or 2).
-
-    :arg yax:     The world space axis which corresponds to the
-                  vertical screen axis (0, 1, or 2).
-    """
-
-    transformCode = display.transform
-    transformMat  = display.getTransform('voxel', 'display')
-    worldRes      = display.resolution
-    
-    xVoxelRes     = np.round(worldRes / image.pixdim[xax])
-    yVoxelRes     = np.round(worldRes / image.pixdim[yax])
-
-    if xVoxelRes < 1: xVoxelRes = 1
-    if yVoxelRes < 1: yVoxelRes = 1
-
-    # These values give the min/max x/y values
-    # of a bounding box which encapsulates
-    # the entire image
-    xmin, xmax = transform.axisBounds(image.shape, transformMat, xax)
-    ymin, ymax = transform.axisBounds(image.shape, transformMat, yax)
-
-
-    # The width/height of a displayed voxel.
-    # If we are displaying in real world space,
-    # we use the world display resolution
-    if transformCode == 'affine':
-
-        xpixdim = worldRes
-        ypixdim = worldRes
-
-    # But if we're just displaying the data (the
-    # transform is 'id' or 'pixdim'), we display
-    # it in the resolution of said data.
-    elif transformCode == 'pixdim':
-        xpixdim = image.pixdim[xax] * xVoxelRes
-        ypixdim = image.pixdim[yax] * yVoxelRes
+        image   = self.image
+        display = self.display
+        res     = display.resolution 
         
-    elif transformCode == 'id':
-        xpixdim = 1.0 * xVoxelRes
-        ypixdim = 1.0 * yVoxelRes
-
-    # Number of samples across each dimension,
-    # given the current sample rate
-    xNumSamples = np.floor((xmax - xmin) / xpixdim)
-    yNumSamples = np.floor((ymax - ymin) / ypixdim)
-
-    # the adjusted width/height of our sample points
-    xpixdim = (xmax - xmin) / xNumSamples
-    ypixdim = (ymax - ymin) / yNumSamples
-
-    log.debug('Generating coordinate buffers for {} '
-              '({} resolution {}/({}, {}), num samples {})'.format(
-                  image.name, transformCode, worldRes, xVoxelRes, yVoxelRes,
-                  xNumSamples * yNumSamples))
-
-    # The location of every displayed
-    # point in real world space
-    worldX = np.linspace(xmin + 0.5 * xpixdim,
-                         xmax - 0.5 * xpixdim,
-                         xNumSamples)
-    worldY = np.linspace(ymin + 0.5 * ypixdim,
-                         ymax - 0.5 * ypixdim,
-                         yNumSamples)
-
-    worldX, worldY = np.meshgrid(worldX, worldY)
-    
-    coords = np.zeros((worldX.size, 3), dtype=np.float32)
-    coords[:, xax] = worldX.flatten()
-    coords[:, yax] = worldY.flatten()
-
-    return coords, xpixdim, ypixdim, xNumSamples, yNumSamples
-
-
-def samplePointsToTriangleStrip(coords,
-                                xpixdim,
-                                ypixdim,
-                                xlen,
-                                ylen,
-                                xax,
-                                yax):
-    """Given a regular 2D grid of points at which an image is to be sampled
-    (for example, that generated by the :func:`calculateSamplePoints` function
-    above), converts those points into an OpenGL vertex triangle strip.
-
-    A grid of M*N points is represented by M*2*(N + 1) vertices. For example,
-    this image represents a 4*3 grid, with periods representing vertex
-    locations::
-    
-        .___.___.___.___.
-        |   |   |   |   |
-        |   |   |   |   |
-        .---.---.---.---.
-        .___.___.__ .___.
-        |   |   |   |   |
-        |   |   |   |   |
-        .---.---.---.---.
-        .___.___.___.___.
-        |   |   |   |   |
-        |   |   |   |   |
-        .___.___.___.___.
-
-    
-    Vertex locations which are vertically adjacent represent the same point in
-    space. Such vertex pairs are unable to be combined because, in OpenGL,
-    they must be represented by distinct vertices (we can't apply multiple
-    colours/texture coordinates to a single vertex location) So we have to
-    repeat these vertices in order to achieve accurate colouring of each
-    voxel.
-
-    We draw each horizontal row of samples one by one, using two triangles to
-    draw each voxel. In order to eliminate the need to specify six vertices
-    for every voxel, and hence to reduce the amount of memory used, we are
-    using a triangle strip to draw each row of voxels. This image depicts a
-    triangle strip used to draw a row of three samples (periods represent
-    vertex locations)::
-
-
-        1  3  5  7
-        .  .  .  .
-        |\ |\ |\ |
-        | \| \| \|
-        .  .  .  .
-        0  2  4  6
-      
-    In order to use a single OpenGL call to draw multiple non-contiguous voxel
-    rows, between every column we add a couple of 'dummy' vertices, which will
-    then be interpreted by OpenGL as 'degenerate triangles', and will not be
-    drawn. So in reality, a 4*3 slice would be drawn as follows (with vertices
-    labelled from [a-z0-9]:
-
-         v  x  z  1  33
-         |\ |\ |\ |\ |
-         | \| \| \| \|
-        uu  w  y  0  2
-         l  n  p  r  tt
-         |\ |\ |\ |\ |
-         | \| \| \| \|
-        kk  m  o  q  s  
-         b  d  f  h  jj
-         |\ |\ |\ |\ |
-         | \| \| \| \|
-         a  c  e  g  i
-    
-    These repeated/degenerate vertices are dealt with by using a vertex index
-    array.  See these links for good overviews of triangle strips and
-    degenerate triangles in OpenGL:
-    
-     - http://www.learnopengles.com/tag/degenerate-triangles/
-     - http://en.wikipedia.org/wiki/Triangle_strip
-
-    A tuple is returned containing:
-
-      - A 2D `numpy.float32` array of shape `(2 * (xlen + 1) * ylen), 3)`,
-        containing the coordinates of all triangle strip vertices which
-        represent the entire grid of sample points.
-    
-      - A 2D `numpy.float32` array of shape `(2 * (xlen + 1) * ylen), 3)`,
-        containing the centre of every grid, to be used for texture
-        coordinates/colour lookup.
-    
-      - A 1D `numpy.uint32` array of size `ylen * (2 * (xlen + 1) + 2) - 2`
-        containing indices into the first array, defining the order in which
-        the vertices need to be rendered. There are more indices than vertex
-        coordinates due to the inclusion of repeated/degenerate vertices.
-
-    :arg coords:  N*3 array of points, the sampling locations.
-    
-    :arg xpixdim: Length of one sample along the horizontal axis.
-    
-    :arg ypixdim: Length of one sample along the vertical axis.
-    
-    :arg xlen:    Number of samples along the horizontal axis.
-    
-    :arg ylen:    Number of samples along the vertical axis.
-    
-    :arg xax:     Display coordinate system axis which corresponds to the
-                  horizontal screen axis.
-    
-    :arg yax:     Display coordinate system axis which corresponds to the
-                  vertical screen axis.
-    """
-
-    coords = coords.reshape(ylen, xlen, 3)
-
-    xlen = int(xlen)
-    ylen = int(ylen)
-
-    # Duplicate every row - each voxel
-    # is defined by two vertices 
-    coords = coords.repeat(2, 0)
-
-    texCoords   = np.array(coords, dtype=np.float32)
-    worldCoords = np.array(coords, dtype=np.float32)
-
-    # Add an extra column at the end
-    # of the world coordinates
-    worldCoords = np.append(worldCoords, worldCoords[:, -1:, :], 1)
-    worldCoords[:, -1, xax] += xpixdim
-
-    # Add an extra column at the start
-    # of the texture coordinates
-    texCoords = np.append(texCoords[:, :1, :], texCoords, 1)
-
-    # Move the x/y world coordinates to the
-    # sampling point corners (the texture
-    # coordinates remain in the voxel centres)
-    worldCoords[   :, :, xax] -= 0.5 * xpixdim
-    worldCoords[ ::2, :, yax] -= 0.5 * ypixdim
-    worldCoords[1::2, :, yax] += 0.5 * ypixdim 
-
-    vertsPerRow  = 2 * (xlen + 1) 
-    dVertsPerRow = 2 * (xlen + 1) + 2
-    nindices     = ylen * dVertsPerRow - 2
-
-    indices = np.zeros(nindices, dtype=np.uint32)
-
-    for yi, xi in it.product(range(ylen), range(xlen + 1)):
-        
-        ii = yi * dVertsPerRow + 2 * xi
-        vi = yi *  vertsPerRow + xi
-        
-        indices[ii]     = vi
-        indices[ii + 1] = vi + xlen + 1
-
-        # add degenerate vertices at the end
-        # every row (but not needed for last
-        # row)
-        if xi == xlen and yi < ylen - 1:
-            indices[ii + 2] = vi + xlen + 1
-            indices[ii + 3] = (yi + 1) * vertsPerRow
-
-    worldCoords = worldCoords.reshape((xlen + 1) * (2 * ylen), 3)
-    texCoords   = texCoords  .reshape((xlen + 1) * (2 * ylen), 3)
-
-    return worldCoords, texCoords, indices
-
-
-def voxelGrid(points, xax, yax, xpixdim, ypixdim):
-    """Given a ``N*3`` array of ``points`` (assumed to be voxel
-    coordinates), creates an array of vertices which can be used
-    to render each point as an unfilled rectangle.
-
-    :arg points:  An ``N*3`` array of voxel xyz coordinates
-
-    :arg xax:     XYZ axis index that maps to the horizontal scren axis
-    
-    :arg yax:     XYZ axis index that maps to the vertical scren axis
-    
-    :arg xpixdim: Length of a voxel along the x axis.
-    
-    :arg ypixdim: Length of a voxel along the y axis.
-    """
-
-    if len(points.shape) == 1:
-        points = points.reshape(1, 3)
-
-    npoints  = points.shape[0]
-    vertices = np.repeat(np.array(points, dtype=np.float32), 4, axis=0)
-
-    xpixdim = xpixdim / 2.0
-    ypixdim = ypixdim / 2.0
-
-    # bottom left corner
-    vertices[ ::4, xax] -= xpixdim 
-    vertices[ ::4, yax] -= ypixdim
-
-    # bottom right
-    vertices[1::4, xax] += xpixdim
-    vertices[1::4, yax] -= ypixdim
-    
-    # top left
-    vertices[2::4, xax] -= xpixdim
-    vertices[2::4, yax] += ypixdim
-
-    # top right
-    vertices[3::4, xax] += xpixdim
-    vertices[3::4, yax] += ypixdim
-
-    # each square is rendered as four lines
-    indices = np.array([0, 1, 0, 2, 1, 3, 2, 3], dtype=np.uint32)
-    indices = np.tile(indices, npoints)
-    
-    indices = (indices.T +
-               np.repeat(np.arange(0, npoints * 4, 4, dtype=np.uint32), 8)).T
-    
-    return vertices, indices
-
-
-def slice2D(dataShape, xax, yax, voxToDisplayMat):
-    """Generates and returns four vertices which denote a slice through an
-    array of the given ``dataShape``, parallel to the plane defined by the
-    given ``xax`` and ``yax``, in the space defined by the given
-    ``voxToDisplayMat``.
-
-    :arg dataShape:       Number of elements along each dimension in the
-                          image data.
-    
-    :arg xax:             Index of display axis which corresponds to the
-                          horizontal screen axis.
-
-    :arg yax:             Index of display axis which corresponds to the
-                          vertical screen axis. 
-    
-    :arg voxToDisplayMat: Affine transformation matrix which transforms from
-                          voxel/array indices into the display coordinate
-                          system.
-    
-    Returns a tuple containing:
-    
-      - A ``4*3`` ``numpy.float32`` array containing the vertex locations
-        of a slice through the data. The values along the ``Z`` axis are set
-        to ``0``.
-    
-      - A ``numpy.uint32`` array to be used as vertex indices.
-    """
-        
-    xmin, xmax = transform.axisBounds(dataShape, voxToDisplayMat, xax)
-    ymin, ymax = transform.axisBounds(dataShape, voxToDisplayMat, yax)
-
-    worldCoords = np.zeros((4, 3), dtype=np.float32)
-
-    worldCoords[0, [xax, yax]] = (xmin, ymin)
-    worldCoords[1, [xax, yax]] = (xmin, ymax)
-    worldCoords[2, [xax, yax]] = (xmax, ymin)
-    worldCoords[3, [xax, yax]] = (xmax, ymax)
-
-    indices = np.arange(0, 4, dtype=np.uint32)
-
-    return worldCoords, indices 
-
-
-def subsample(data, resolution, pixdim=None, volume=None):
-    """Samples the given data according to the given resolution.
-
-    :arg data:       The data to be sampled.
-
-    :arg resolution: Sampling resolution, proportional to the values in
-                     ``pixdim``.
-
-    :arg pixdim:     Length of each dimension in the input data (defaults to
-                     ``(1.0, 1.0, 1.0)``).
-
-    :arg volume:     If the image is a 4D volume, the volume index of the 3D
-                     image to be sampled.
-    """
-
-    if pixdim is None:
-        pixdim = (1.0, 1.0, 1.0)
-
-    xstep = np.round(resolution / pixdim[0])
-    ystep = np.round(resolution / pixdim[1])
-    zstep = np.round(resolution / pixdim[2])
-
-    if xstep < 1: xstep = 1
-    if ystep < 1: ystep = 1
-    if zstep < 1: zstep = 1
-
-    xstart = xstep / 2
-    ystart = ystep / 2
-    zstart = zstep / 2
-
-    if volume is not None:
-        if len(data.shape) > 3: sample = data[xstart::xstep,
-                                              ystart::ystep,
-                                              zstart::zstep,
-                                              volume]
-        else:                   sample = data[xstart::xstep,
-                                              ystart::ystep,
-                                              zstart::zstep]
-    else:
-        if len(data.shape) > 3: sample = data[xstart::xstep,
-                                              ystart::ystep,
-                                              zstart::zstep,
-                                              :]
-        else:                   sample = data[xstart::xstep,
-                                              ystart::ystep,
-                                              zstart::zstep]        
-
-    return sample
-
-
-def broadcast(vertices, indices, zposes, xforms, zax):
-    """Given a set of vertices and indices (assumed to be 2D representations
-    of some geometry in a 3D space, with the depth axis specified by ``zax``),
-    replicates them across all of the specified Z positions, applying the
-    corresponding transformation to each set of vertices.
-
-    :arg vertices: Vertex array (a ``N*3`` numpy array).
-    
-    :arg indices:  Index array.
-    
-    :arg zposes:   Positions along the depth axis at which the vertices
-                   are to be replicated.
-    
-    :arg xforms:   Sequence of transformation matrices, one for each
-                   Z position.
-    
-    :arg zax:      Index of the 'depth' axis
-
-    Returns three values:
-    
-      - A numpy array containing all of the generated vertices
-    
-      - A numpy array containing the original vertices for each of the
-        generated vertices, which may be used as texture coordinates
-
-      - A new numpy array containing all of the generated indices.
-    """
-
-    vertices = np.array(vertices)
-    indices  = np.array(indices)
-    
-    nverts   = vertices.shape[0]
-    nidxs    = indices.shape[ 0]
-
-    allTexCoords  = np.zeros((nverts * len(zposes), 3), dtype=np.float32)
-    allVertCoords = np.zeros((nverts * len(zposes), 3), dtype=np.float32)
-    allIndices    = np.zeros( nidxs  * len(zposes),     dtype=np.uint32)
-    
-    for i, (zpos, xform) in enumerate(zip(zposes, xforms)):
-
-        vertices[:, zax] = zpos
-
-        vStart = i * nverts
-        vEnd   = vStart + nverts
-
-        iStart = i * nidxs
-        iEnd   = iStart + nidxs
+        if display.transform in ('id', 'pixdim'):
 
-        allIndices[   iStart:iEnd]    = indices + i * nverts
-        allTexCoords[ vStart:vEnd, :] = vertices
-        allVertCoords[vStart:vEnd, :] = transform.transform(vertices, xform)
+            pixdim = np.array(image.pixdim[:3])
+            steps  = [res, res, res] / pixdim
+            res    = image.shape[:3] / steps
+            
+            return np.array(res.round(), dtype=np.uint32)
         
-    return allVertCoords, allTexCoords, allIndices
+        else:
+            lo, hi = display.getDisplayBounds()
+            minres = int(round(((hi - lo) / res).min()))
+            return [minres] * 3
diff --git a/fsl/fslview/gl/glrgbvector.py b/fsl/fslview/gl/glrgbvector.py
new file mode 100644
index 0000000000000000000000000000000000000000..6d78c29f045d1c86c501f9358432e680c1307427
--- /dev/null
+++ b/fsl/fslview/gl/glrgbvector.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python
+#
+# glrgbvector.py - Display vector images in RGB mode.
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+"""This mdoule provides the :class:`GLRGBVector` class, for displaying 3D
+vector images in RGB mode.
+"""
+
+import numpy                   as np
+import fsl.fslview.gl          as fslgl
+import fsl.fslview.gl.routines as glroutines
+import fsl.fslview.gl.glvector as glvector
+import fsl.utils.transform     as transform
+
+
+class GLRGBVector(glvector.GLVector):
+
+
+    def __prefilter(self, data):
+        return np.abs(data)
+
+
+    def __init__(self, image, display):
+
+        glvector.GLVector.__init__(self, image, display, self.__prefilter)
+        fslgl.glrgbvector_funcs.init(self)
+
+
+    def generateVertices(self, zpos, xform):
+        vertices, voxCoords, texCoords = glroutines.slice2D(
+            self.image.shape[:3],
+            self.xax,
+            self.yax,
+            zpos, 
+            self.display.getTransform('voxel',   'display'),
+            self.display.getTransform('display', 'voxel'))
+
+        if xform is not None: 
+            vertices = transform.transform(vertices, xform)
+
+        return vertices, voxCoords, texCoords 
+
+
+    def compileShaders(self):
+        fslgl.glrgbvector_funcs.compileShaders(self)
+        
+
+    def updateShaderState(self):
+        fslgl.glrgbvector_funcs.updateShaderState(self)
+
+
+    def preDraw(self):
+        glvector.GLVector.preDraw(self)
+        fslgl.glrgbvector_funcs.preDraw(self)
+
+
+    def draw(self, zpos, xform=None):
+        fslgl.glrgbvector_funcs.draw(self, zpos, xform)
+
+    
+    def drawAll(self, zposes, xforms):
+        fslgl.glrgbvector_funcs.drawAll(self, zposes, xforms) 
+
+    
+    def postDraw(self):
+        glvector.GLVector.postDraw(self)
+        fslgl.glrgbvector_funcs.postDraw(self) 
diff --git a/fsl/fslview/gl/glvector.py b/fsl/fslview/gl/glvector.py
index 04f50585811485360fd81e79d21de83c817aa3ef..dfd6ffc029fbf3bdc7150970963d8ae75cd2cd3c 100644
--- a/fsl/fslview/gl/glvector.py
+++ b/fsl/fslview/gl/glvector.py
@@ -8,94 +8,40 @@
 """Defines the :class:`GLVector` class, which encapsulates the logic for
 rendering 2D slices of a ``X*Y*Z*3`` image as a vector. The ``GLVector`` class
 provides the interface defined by the
-:class:`~fsl.fslview.gl.globject.GLObject` class.
+:class:`.GLObject` class.
 
-A ``GLVector`` instance may be used to render an
-:class:`~fsl.data.image.Image` instance which has an ``imageType`` of
-``vector``. It is assumed that this ``Image`` instance is associated with
-a :class:`~fsl.fslview.displaycontext.display.Display` instance which contains
-a :class:`~fsl.fslview.dislpaycontext.vectoropts.VectorOpts` instance,
-containing options specific to vector rendering.
 
-Vectors can be displayed in one of several 'modes', defined by the
-``VectorOpts.displayMode`` option:
-
- - ``rgb``: The magnitude of the vector at each voxel along each axis is
-            displayed as a combination of three colours. 
-
- - `line``: The vector at each voxel is rendered as an undirected line.
-
-The :class:`GLVector` class makes use of the functions defined in the
-:mod:`fsl.fslview.gl.gl14.glvector_funcs` or the
-:mod:`fsl.fslview.gl.gl21.glvector_funcs` modules, which provide OpenGL 
-version specific details for creation/storage of vertex data, and for
-rendering.
-
-These version dependent modules must provide the following functions:
-
- - ``init(GLVector)``:  Initialise any necessary OpenGL shaders, textures,
-   buffers, etc.
-
- - ``destroy(GLVector)``: Clean up any OpenGL data/state.
-
- - ``setAxes(GLVector)``: Create the necessary geometry, ensuring that it is
-    oriented in such a way as to be displayed with ``GLVector.xax`` mapping to
-    the horizontal screen axis, ``GLVector.yax`` to the vertical axis, and
-    ``GLVector.yax`` to the depth axis.
-
- - ``preDraw(GLVector)``: Prepare GL state ready for drawing.
-
- - ``draw(GLVector, zpos, xform=None)``: Draw a slice of the vector image
-   at the given ``zpos``.
-                                    
- - ``postDraw(GLVector)``: Clean up GL state after drawing.
+The ``GLVector`` class is a base class whcih is not intended to be
+instantiated directly. The :class:`.GLRGBVector` and :class:`.GLLineVector`
+subclasses should be used instead.  These two subclasses share the
+functionality provided by this class.
 
 
 The vector image is stored on the GPU as a 3D RGB texture, where the ``R``
 channel contains the ``x`` vector values, the ``G`` channel the ``y`` values,
-and the ``B`` channel the ``z`` values. In ``line`` mode, this 3D texture
-contains the vector data normalised, but unchanged. However, in ``rgb`` mode,
-the absolute vaues of the vector data are stored. This is necessary to
-allow for GPU based interpolation of RGB vector images.
-
-In both ``rgb`` and ``line`` mode, three 1D textures are used to store a
-colour table for each of the ``x``, ``y`` and ``z`` components. A custom
-fragment shader program looks up the ``xyz`` vector values, looks up colours
-for each of them, and combines the three colours to form the final fragment
-colour.
-
-When in ``rgb`` mode, 2D slices of the image are rendered using a simple
-rectangular slice through the texture. When in ``line`` mode, each voxel
-is rendered using two vertices, which are aligned in the direction of the
-vector.
+and the ``B`` channel the ``z`` values. 
 
-The colour of each vector may be modulated by another image, specified by the
-:attr:`~fsl.fslview.displaycontext.vectoropts.VectorOpts.modulate` property.
-This modulation image is stored as a 3D single-channel texture.
-"""
 
-import numpy                   as np
-import OpenGL.GL               as gl
+Three 1D textures are used to store a colour table for each of the ``x``,
+``y`` and ``z`` components. A custom fragment shader program looks up the
+``xyz`` vector values, looks up colours for each of them, and combines the
+three colours to form the final fragment colour.
 
-import fsl.data.image          as fslimage
-import fsl.fslview.gl          as fslgl
-import fsl.fslview.gl.textures as fsltextures
-import fsl.fslview.gl.globject as globject
+The colour of each vector may be modulated by another image, specified by the
+:attr:`.VectorOpts.modulate` property.  This modulation image is stored as a
+3D single-channel texture.
 
+"""
 
-def _lineModePrefilter(data):
-    """Prefilter method for the vector image texture, when it is being
-    displayed in ``line`` mode - see
-    :meth:`~fsl.fslview.gl.textures.ImageTexture.setPrefilter`.
-    """
-    return data.transpose((3, 0, 1, 2))
+import numpy                    as np
+import OpenGL.GL                as gl
 
+import fsl.data.image           as fslimage
+import fsl.fslview.colourmaps   as fslcm
+import fsl.fslview.gl.resources as glresources
+import fsl.fslview.gl.textures  as textures
+import fsl.fslview.gl.globject  as globject
 
-def _rgbModePrefilter(data):
-    """Prefilter method for the vector image texture, when it is being
-    displayed in ``rgb`` mode.
-    """ 
-    return np.abs(data.transpose((3, 0, 1, 2)))
 
 
 class GLVector(globject.GLImageObject):
@@ -103,175 +49,160 @@ class GLVector(globject.GLImageObject):
     to render 2D slices of a ``X*Y*Z*3`` image as vectors.
     """
 
-    def __init__(self, image, display):
+    def __init__(self, image, display, prefilter=None):
         """Create a :class:`GLVector` object bound to the given image and
         display.
 
-        :arg image:        A :class:`~fsl.data.image.Image` object.
-        
-        :arg imageDisplay: A :class:`~fsl.fslview.displaycontext.Display`
-                           object which describes how the image is to be
-                           displayed .
-        """
-
-        if not image.is4DImage() or image.shape[3] != 3:
-            raise ValueError('Image must be 4 dimensional, with 3 volumes '
-                             'representing the XYZ vector angles')
-
-        globject.GLImageObject.__init__(self, image, display)
-        self._ready = False
-
-        
-    def init(self):
-        """Initialise the OpenGL data required to render the given image.
-
+        Initialises the OpenGL data required to render the given image.
         This method does the following:
         
           - Creates the image texture, the modulate texture, and the three
             colour map textures.
 
-          - Adds listeners to the
-            :class:`~fsl.fslview.displaycontext.display.Display` and
-            :class:`~fsl.fslview.displaycontext.vectoropts.VectorOpts`
+          - Adds listeners to the :class:`.Display` and :class:`.VectorOpts`
             instances, so the textures and geometry can be updated when
             necessary.
 
-          - Calls the GTL version specific ``glvector_funcs.init`` function.
+        :arg image:     An :class:`.Image` object.
+        
+        :arg display:   A :class:`.Display` object which describes how the
+                        image is to be displayed.
+
+        :arg prefilter: An optional function which filters the data before it
+                        is stored as a 3D texture. See :class:`.ImageTexture`.
+                        Whether or not this function is provided, the data is
+                        transposed so that the fourth dimension is the fastest
+                        changing.
         """
 
+        if not image.is4DImage() or image.shape[3] != 3:
+            raise ValueError('Image must be 4 dimensional, with 3 volumes '
+                             'representing the XYZ vector angles')
+
+        globject.GLImageObject.__init__(self, image, display)
+
         display = self.display
         opts    = self.displayOpts
         name    = self.name
 
-        self.xColourTexture = gl.glGenTextures(1)
-        self.yColourTexture = gl.glGenTextures(1)
-        self.zColourTexture = gl.glGenTextures(1)
+        self.xColourTexture = textures.ColourMapTexture('{}_x'.format(name))
+        self.yColourTexture = textures.ColourMapTexture('{}_y'.format(name))
+        self.zColourTexture = textures.ColourMapTexture('{}_z'.format(name))
         self.modTexture     = None
         self.imageTexture   = None
         
         def modUpdate( *a):
             self.refreshModulateTexture()
+            self.updateShaderState()
+            self.onUpdate()
 
         def cmapUpdate(*a):
             self.refreshColourTextures()
+            self.updateShaderState()
+            self.onUpdate()
+            
+        def shaderUpdate(*a):
+            self.updateShaderState()
+            self.onUpdate() 
+
+        def shaderCompile(*a):
+            self.compileShaders()
+            self.updateShaderState()
+            self.onUpdate()
+
+        display.addListener('interpolation', name, shaderUpdate)
+        display.addListener('softwareMode',  name, shaderCompile)
+        display.addListener('alpha',         name, cmapUpdate)
+        display.addListener('brightness',    name, cmapUpdate)
+        display.addListener('contrast',      name, cmapUpdate) 
+        opts   .addListener('xColour',       name, cmapUpdate)
+        opts   .addListener('yColour',       name, cmapUpdate)
+        opts   .addListener('zColour',       name, cmapUpdate)
+        opts   .addListener('suppressX',     name, cmapUpdate)
+        opts   .addListener('suppressY',     name, cmapUpdate)
+        opts   .addListener('suppressZ',     name, cmapUpdate)
+        opts   .addListener('modulate',      name, modUpdate)
+        opts   .addListener('modThreshold',  name, shaderUpdate)
+
+        # the fourth dimension (the vector directions) 
+        # must be the fastest changing in the texture data
+        if prefilter is None:
+            realPrefilter = lambda d:           d.transpose((3, 0, 1, 2))
+        else:
+            realPrefilter = lambda d: prefilter(d.transpose((3, 0, 1, 2)))
 
-        def modeChange(*a):
-            self._onModeChange()
-
-        def coordUpdate(*a):
-            self.setAxes(self.xax, self.yax)
-
-        display.addListener('alpha',       name, cmapUpdate)
-        display.addListener('transform',   name, coordUpdate)
-        display.addListener('resolution',  name, coordUpdate) 
-        opts   .addListener('xColour',     name, cmapUpdate)
-        opts   .addListener('yColour',     name, cmapUpdate)
-        opts   .addListener('zColour',     name, cmapUpdate)
-        opts   .addListener('suppressX',   name, cmapUpdate)
-        opts   .addListener('suppressY',   name, cmapUpdate)
-        opts   .addListener('suppressZ',   name, cmapUpdate)
-        opts   .addListener('modulate',    name, modUpdate)
-        opts   .addListener('displayMode', name, modeChange)
-
-        if   opts.displayMode == 'line': prefilter = _lineModePrefilter
-        elif opts.displayMode == 'rgb':  prefilter = _rgbModePrefilter
-
-        self.imageTexture = fsltextures.getTexture(
+        texName = '{}_{}'.format(type(self).__name__, id(self.image))
+        self.imageTexture = glresources.get(
+            texName,
+            textures.ImageTexture,
+            texName,
             self.image,
-            type(self).__name__,
             display=self.display,
             nvals=3,
             normalise=True,
-            prefilter=prefilter) 
+            prefilter=realPrefilter) 
 
         self.refreshModulateTexture()
         self.refreshColourTextures()
 
-        fslgl.glvector_funcs.init(self)
-        
-        self._ready = True
-
         
     def destroy(self):
-        """Deletes the GL textures, deregisters the listeners
-        configured in :meth:`init`, and calls the GL version specific
-        ``glvector_funcs.destroy`` function.
-        """
-
-        gl.glDeleteTextures(self.xColourTexture)
-        gl.glDeleteTextures(self.yColourTexture)
-        gl.glDeleteTextures(self.zColourTexture)
+        """Deletes the GL textures, and deregisters the listeners configured in
+        :meth:`__init__`.
 
-        fsltextures.deleteTexture(self.imageTexture)
-        fsltextures.deleteTexture(self.modTexture) 
-
-        self.display    .removeListener('alpha',       self.name)
-        self.display    .removeListener('transform',   self.name)
-        self.display    .removeListener('resolution',  self.name)
-        self.displayOpts.removeListener('xColour',     self.name)
-        self.displayOpts.removeListener('yColour',     self.name)
-        self.displayOpts.removeListener('zColour',     self.name)
-        self.displayOpts.removeListener('suppressX',   self.name)
-        self.displayOpts.removeListener('suppressY',   self.name)
-        self.displayOpts.removeListener('suppressZ',   self.name)
-        self.displayOpts.removeListener('modulate',    self.name)
-        self.displayOpts.removeListener('displayMode', self.name)
+        This method must be called by subclass implementations.
+        """
 
-        fslgl.glvector_funcs.destroy(self)
+        self.xColourTexture.destroy()
+        self.yColourTexture.destroy()
+        self.zColourTexture.destroy()
 
-        
-    def ready(self):
-        """Returns `True` when the OpenGL data/state has been initialised,
-        and the image is ready to be drawn, `False` before.
-        """ 
-        return self._ready
+        glresources.delete(self.imageTexture.getTextureName())
+        glresources.delete(self.modTexture  .getTextureName())
 
+        self.imageTexture = None
+        self.modTexture   = None
 
-    def _onModeChange(self, *a):
-        """Called when the
-        :attr:`~fsl.fslview.displaycontext.vectoropts.VectorOpts.displayMode`
-        property changes.
+        self.display    .removeListener('interpolation', self.name)
+        self.display    .removeListener('softwareMode',  self.name)
+        self.display    .removeListener('alpha',         self.name)
+        self.display    .removeListener('brightness',    self.name)
+        self.display    .removeListener('contrast',      self.name)
+        self.displayOpts.removeListener('xColour',       self.name)
+        self.displayOpts.removeListener('yColour',       self.name)
+        self.displayOpts.removeListener('zColour',       self.name)
+        self.displayOpts.removeListener('suppressX',     self.name)
+        self.displayOpts.removeListener('suppressY',     self.name)
+        self.displayOpts.removeListener('suppressZ',     self.name)
+        self.displayOpts.removeListener('modulate',      self.name)
+        self.displayOpts.removeListener('modThreshold',  self.name)
 
-        Initialises data and GL state for the newly selected vector display
-        mode.
-        """
 
-        mode = self.displayOpts.displayMode
+    def updateShaderState(self):
+        """This method must be provided by subclasses."""
+        raise NotImplementedError('updateShaderState must be implemented by '
+                                  '{} subclasses'.format(type(self).__name__))
 
-        # Disable atexture interpolation in line mode
-        if mode == 'line':
-            
-            if self.display.interpolation != 'none':
-                self.display.interpolation = 'none'
-                
-            self.display.disableProperty('interpolation')
-            
-        elif mode == 'rgb':
-            self.display.enableProperty('interpolation')
+    
+    def compileShaders(self):
+        """This method must be provided by subclasses."""
+        raise NotImplementedError('compileShaders must be implemented by '
+                                  '{} subclasses'.format(type(self).__name__)) 
 
-        if   mode == 'line': prefilter = _lineModePrefilter
-        elif mode == 'rgb':  prefilter = _rgbModePrefilter 
-            
-        fslgl.glvector_funcs.destroy(self)
-        self.imageTexture.setPrefilter(prefilter)
-        fslgl.glvector_funcs.init(self)
-        self.setAxes(self.xax, self.yax)
-        
 
     def refreshModulateTexture(self):
-        """Called when the
-        :attr`~fsl.fslview.displaycontext.vectoropts.VectorOpts.modulate`
-        property changes.
+        """Called when the :attr`.VectorOpts.modulate` property changes.
 
         Reconfigures the modulation texture. If no modulation image is
         selected, a 'dummy' texture is creatad, which contains all white
         values (and which result in the modulation texture having no effect).
         """
 
-        modImage = self.displayOpts.modulate
-
         if self.modTexture is not None:
-            fsltextures.deleteTexture(self.modTexture)
+            glresources.delete(self.modTexture.getTextureName())
+            self.modTexture = None
+
+        modImage = self.displayOpts.modulate
 
         if modImage == 'none':
             textureData = np.zeros((5, 5, 5), dtype=np.uint8)
@@ -284,39 +215,49 @@ class GLVector(globject.GLImageObject):
             modDisplay = self.display
             norm       = True
 
-        self.modTexture = fsltextures.getTexture(
+        texName = '{}_{}_{}_modulate'.format(
+            type(self).__name__, id(self.image), id(modImage))
+        self.modTexture = glresources.get(
+            texName,
+            textures.ImageTexture,
+            texName,
             modImage,
-            '{}_{}_modulate'.format(type(self).__name__, id(self.image)),
             display=modDisplay,
             normalise=norm)
 
 
     def refreshColourTextures(self, colourRes=256):
-        """Called when the component colour maps need to be updated, when
-        one of the
-        :attr:`~fsl.fslview.displaycontext.vectoropts.VectorOpts.xColour`,
-        ``yColour``, ``zColour``, ``suppressX``, ``suppressY``, or
-        ``suppressZ`` properties change.
+        """Called when the component colour maps need to be updated, when one
+        of the :attr:`.VectorOpts.xColour`, ``yColour``, ``zColour``,
+        ``suppressX``, ``suppressY``, or ``suppressZ`` properties change.
 
         Regenerates the colour textures.
         """
 
-        xcol = self.displayOpts.xColour
-        ycol = self.displayOpts.yColour
-        zcol = self.displayOpts.zColour
+        display = self.display
+        opts    = self.displayOpts
+
+        xcol = opts.xColour
+        ycol = opts.yColour
+        zcol = opts.zColour
 
         xcol[3] = 1.0
         ycol[3] = 1.0
         zcol[3] = 1.0
 
-        xsup = self.displayOpts.suppressX
-        ysup = self.displayOpts.suppressY
-        zsup = self.displayOpts.suppressZ 
+        xsup = opts.suppressX
+        ysup = opts.suppressY
+        zsup = opts.suppressZ 
 
         xtex = self.xColourTexture
         ytex = self.yColourTexture
         ztex = self.zColourTexture
 
+        drange = fslcm.briconToDisplayRange(
+            (0.0, 1.0),
+            display.brightness / 100.0,
+            display.contrast   / 100.0)
+        
         for colour, texture, suppress in zip(
                 (xcol, ycol, zcol),
                 (xtex, ytex, ztex),
@@ -325,127 +266,50 @@ class GLVector(globject.GLImageObject):
             if not suppress:
                 
                 cmap = np.array(
-                    [np.linspace(0.0, i, colourRes) for i in colour])
+                    [np.linspace(0.0, i, colourRes) for i in colour]).T
                 
                 # Component magnitudes of 0 are
                 # transparent, but any other
                 # magnitude is fully opaque
-                cmap[3, :] = 1.0
-                cmap[3, 0] = 0.0 
+                cmap[:, 3] = display.alpha / 100.0
+                cmap[0, 3] = 0.0 
             else:
-                cmap = np.zeros((4, colourRes))
-
-            cmap = np.array(np.floor(cmap * 255), dtype=np.uint8).ravel('F')
-
-            gl.glBindTexture(gl.GL_TEXTURE_1D, texture)
-            gl.glTexParameteri(gl.GL_TEXTURE_1D,
-                               gl.GL_TEXTURE_MAG_FILTER,
-                               gl.GL_NEAREST)
-            gl.glTexParameteri(gl.GL_TEXTURE_1D,
-                               gl.GL_TEXTURE_MIN_FILTER,
-                               gl.GL_NEAREST)
-            gl.glTexParameteri(gl.GL_TEXTURE_1D,
-                               gl.GL_TEXTURE_WRAP_S,
-                               gl.GL_CLAMP_TO_EDGE)
-
-            gl.glTexImage1D(gl.GL_TEXTURE_1D,
-                            0,
-                            gl.GL_RGBA8,
-                            colourRes,
-                            0,
-                            gl.GL_RGBA,
-                            gl.GL_UNSIGNED_BYTE,
-                            cmap)
-
-        gl.glBindTexture(gl.GL_TEXTURE_1D, 0)
-    
+                cmap = np.zeros((colourRes, 4))
+
+            texture.set(cmap=cmap, displayRange=drange)
+        
 
     def setAxes(self, xax, yax):
-        """Calls the GL version-specific ``glvector_funcs.setAxes`` function,
-        which should make sure that the GL geometry representation is up
-        to date.
-        """
+        """Stores the new x/y/z axes."""
 
         self.xax = xax
         self.yax = yax
         self.zax = 3 - xax - yax
 
-        fslgl.glvector_funcs.setAxes(self)
-
         
     def preDraw(self):
-        """Ensures that the five textures (the vector and modulation images,
-        and the three colour textures) are bound, then calls
-        ``glvector_funcs.preDraw``.
-        """
-        if not self.display.enabled:
-            return
-
-        gl.glActiveTexture(gl.GL_TEXTURE0)
-        gl.glEnable(gl.GL_TEXTURE_3D)
-        gl.glBindTexture(gl.GL_TEXTURE_3D, self.imageTexture.texture)
-
-        gl.glActiveTexture(gl.GL_TEXTURE1)
-        gl.glEnable(gl.GL_TEXTURE_3D)
-        gl.glBindTexture(gl.GL_TEXTURE_3D, self.modTexture.texture) 
-
-        gl.glActiveTexture(gl.GL_TEXTURE2)
-        gl.glEnable(gl.GL_TEXTURE_1D)
-        gl.glBindTexture(gl.GL_TEXTURE_1D, self.xColourTexture)
-
-        gl.glActiveTexture(gl.GL_TEXTURE3)
-        gl.glEnable(gl.GL_TEXTURE_1D)
-        gl.glBindTexture(gl.GL_TEXTURE_1D, self.yColourTexture)
-
-        gl.glActiveTexture(gl.GL_TEXTURE4)
-        gl.glEnable(gl.GL_TEXTURE_1D)
-        gl.glBindTexture(gl.GL_TEXTURE_1D, self.zColourTexture) 
- 
-        fslgl.glvector_funcs.preDraw(self)
-
-        
-    def draw(self, zpos, xform=None):
-        """Calls the ``glvector_funcs.draw`` function. """
-        
-        if not self.display.enabled:
-            return
-        
-        fslgl.glvector_funcs.draw(self, zpos, xform)
+        """Must be called by subclass implementations.
 
-    def drawAll(self, zposes, xforms=None):
-        """Calls the ``glvector_funcs.drawAll`` function. """
-        
-        if not self.display.enabled:
-            return
+        Ensures that the five textures (the vector and modulation images,
+        and the three colour textures) are bound to texture units 0-4
+        respectively.
+        """
         
-        fslgl.glvector_funcs.drawAll(self, zposes, xforms) 
+        self.imageTexture  .bindTexture(gl.GL_TEXTURE0)
+        self.modTexture    .bindTexture(gl.GL_TEXTURE1)
+        self.xColourTexture.bindTexture(gl.GL_TEXTURE2)
+        self.yColourTexture.bindTexture(gl.GL_TEXTURE3)
+        self.zColourTexture.bindTexture(gl.GL_TEXTURE4)
 
         
     def postDraw(self):
-        """Unbindes the five GL textures, and calls the
-        ``glvector_funcs.postDraw`` function.
-        """
-        if not self.display.enabled:
-            return
+        """Must be called by subclass implementations.
 
-        gl.glActiveTexture(gl.GL_TEXTURE0)
-        gl.glBindTexture(gl.GL_TEXTURE_3D, 0)
-        gl.glDisable(gl.GL_TEXTURE_3D)
-
-        gl.glActiveTexture(gl.GL_TEXTURE1)
-        gl.glBindTexture(gl.GL_TEXTURE_3D, 0)
-        gl.glDisable(gl.GL_TEXTURE_3D)
-
-        gl.glActiveTexture(gl.GL_TEXTURE2)
-        gl.glBindTexture(gl.GL_TEXTURE_1D, 0)
-        gl.glDisable(gl.GL_TEXTURE_1D)
-
-        gl.glActiveTexture(gl.GL_TEXTURE3)
-        gl.glBindTexture(gl.GL_TEXTURE_1D, 0)
-        gl.glDisable(gl.GL_TEXTURE_1D)
+        Unbindes the five GL textures.
+        """
 
-        gl.glActiveTexture(gl.GL_TEXTURE4)
-        gl.glBindTexture(gl.GL_TEXTURE_1D, 0)    
-        gl.glDisable(gl.GL_TEXTURE_1D)
-        
-        fslgl.glvector_funcs.postDraw(self) 
+        self.imageTexture  .unbindTexture()
+        self.modTexture    .unbindTexture()
+        self.xColourTexture.unbindTexture()
+        self.yColourTexture.unbindTexture()
+        self.zColourTexture.unbindTexture()
diff --git a/fsl/fslview/gl/glvolume.py b/fsl/fslview/gl/glvolume.py
index f0eb5bbefb7fb4024cf1362136d9254e2d02bfc6..55800ec6818f96ed3f1a56ed1ad90368ffe654fc 100644
--- a/fsl/fslview/gl/glvolume.py
+++ b/fsl/fslview/gl/glvolume.py
@@ -29,8 +29,10 @@ These version dependent modules must provide the following functions:
 
   - ``destroy(GLVolume)``: Perform any necessary clean up.
 
-  - ``genVertexData(GLVolume)``: Create and prepare vertex coordinates, using
-    the :meth:`GLVolume.genVertexData` method.
+  - ``compileShaders(GLVolume)``: (Re-)Compile the shader programs.
+
+  - ``updateShaderState(GLVolume)``: Updates the shader program states
+    when display parameters are changed.
 
   - ``preDraw()``: Initialise the GL state, ready for drawing.
 
@@ -38,25 +40,29 @@ These version dependent modules must provide the following functions:
     given Z position. If xform is not None, it must be applied as a 
     transformation on the vertex coordinates.
 
+  - ``drawAll(Glvolume, zposes, xforms)`` - Draws slices at each of the
+    specified ``zposes``, applying the corresponding ``xforms`` to each.
+
   - ``postDraw()``: Clear the GL state after drawing.
 
 Images are rendered in essentially the same way, regardless of which OpenGL
 version-specific module is used.  The image data itself is stored on the GPU
 as a 3D texture, and the current colour map as a 1D texture. A slice through
-the texture is rendered using four vertices, located at the respective corners
+the texture is rendered using six vertices, located at the respective corners
 of the image bounds.
-
 """
 
 import logging
 log = logging.getLogger(__name__)
 
-import OpenGL.GL               as gl
-import numpy                   as np
+import OpenGL.GL                as gl
 
-import fsl.fslview.gl          as fslgl
-import fsl.fslview.gl.textures as fsltextures
-import fsl.fslview.gl.globject as globject
+import fsl.utils.transform      as transform
+import fsl.fslview.gl           as fslgl
+import fsl.fslview.gl.textures  as textures
+import fsl.fslview.gl.resources as glresources
+import fsl.fslview.gl.globject  as globject
+import fsl.fslview.gl.routines  as glroutines
 
 
 class GLVolume(globject.GLImageObject):
@@ -68,6 +74,8 @@ class GLVolume(globject.GLImageObject):
         """Creates a GLVolume object bound to the given image, and associated
         image display.
 
+        Initialises the OpenGL data required to render the given image.
+
         :arg image:   A :class:`~fsl.data.image.Image` object.
         
         :arg display: A :class:`~fsl.fslview.displaycontext.Display`
@@ -76,57 +84,37 @@ class GLVolume(globject.GLImageObject):
         """
 
         globject.GLImageObject.__init__(self, image, display)
-        
-        self._ready = False
-
-
-    def ready(self):
-        """Returns `True` when the OpenGL data/state has been initialised,
-        and the image is ready to be drawn, `False` before.
-        """
-        return self._ready
 
-        
-    def init(self):
-        """Initialise the OpenGL data required to render the given image.
-
-        The real initialisation takes place in this method - it must
-        only be called after an OpenGL context has been created.
-        """
-        
         # Add listeners to this image so the view can be
         # updated when its display properties are changed
         self.addDisplayListeners()
 
-        fslgl.glvolume_funcs.init(self)
+        # Create an image texture and a colour map texture
+        texName = '{}_{}'.format(type(self).__name__, id(self.image))
 
-        self.imageTexture = fsltextures.getTexture(
-            self.image, type(self).__name__, self.display)
+        # The image texture may be used elsewhere,
+        # so we'll use the resource management
+        # module rather than creating one directly
+        self.imageTexture = glresources.get(
+            texName, 
+            textures.ImageTexture,
+            texName,
+            self.image,
+            self.display)
 
-        # The colour map, used for converting 
-        # image data to a RGBA colour.
-        self.colourTexture = gl.glGenTextures(1)
-        log.debug('Created GL texture: {}'.format(self.colourTexture))
+        self.colourTexture = textures.ColourMapTexture(texName)
         
-        self.colourResolution = 256
-        self.refreshColourTexture(self.colourResolution)
+        self.refreshColourTexture()
         
-        self._ready = True
+        fslgl.glvolume_funcs.init(self)
 
 
     def setAxes(self, xax, yax):
-        """This method should be called when the image display axes change.
+        """This method should be called when the image display axes change."""
         
-        It regenerates vertex information accordingly.
-        """
-        
-        self.xax         = xax
-        self.yax         = yax
-        self.zax         = 3 - xax - yax
-        wc, idxs, nverts = fslgl.glvolume_funcs.genVertexData(self)
-        self.worldCoords = wc
-        self.indices     = idxs
-        self.nVertices   = nverts
+        self.xax = xax
+        self.yax = yax
+        self.zax = 3 - xax - yax
 
 
     def preDraw(self):
@@ -134,21 +122,43 @@ class GLVolume(globject.GLImageObject):
         instance.
         """
         
-        if not self.display.enabled:
-            return
-
-        # Set up the image data texture 
-        gl.glActiveTexture(gl.GL_TEXTURE0)
-        gl.glEnable(gl.GL_TEXTURE_3D)
-        gl.glBindTexture(gl.GL_TEXTURE_3D, self.imageTexture.texture)
-
-        # Set up the colour map texture
-        gl.glActiveTexture(gl.GL_TEXTURE1)
-        gl.glEnable(gl.GL_TEXTURE_1D)
-        gl.glBindTexture(gl.GL_TEXTURE_1D, self.colourTexture)
-        
+        # Set up the image and colour textures
+        self.imageTexture .bindTexture(gl.GL_TEXTURE0)
+        self.colourTexture.bindTexture(gl.GL_TEXTURE1)
+
         fslgl.glvolume_funcs.preDraw(self)
 
+
+    def generateVertices(self, zpos, xform=None):
+        """Generates vertex coordinates for a 2D slice through the given
+        ``zpos``, with the optional ``xform`` applied to the coordinates.
+        
+        This method is called by the :mod:`.gl14.glvolume_funcs` and
+        :mod:`.gl21.glvolume_funcs` modules.
+
+        A tuple of three values is returned, containing:
+        
+          - A ``6*3 numpy.float32`` array containing the vertex coordinates
+        
+          - A ``6*3 numpy.float32`` array containing the voxel coordinates
+            corresponding to each vertex
+        
+          - A ``6*3 numpy.float32`` array containing the texture coordinates
+            corresponding to each vertex
+        """
+        vertices, voxCoords, texCoords = glroutines.slice2D(
+            self.image.shape[:3],
+            self.xax,
+            self.yax,
+            zpos, 
+            self.display.getTransform('voxel',   'display'),
+            self.display.getTransform('display', 'voxel'))
+
+        if xform is not None: 
+            vertices = transform.transform(vertices, xform)
+
+        return vertices, voxCoords, texCoords
+
         
     def draw(self, zpos, xform=None):
         """Draws a 2D slice of the image at the given real world Z location.
@@ -163,17 +173,12 @@ class GLVolume(globject.GLImageObject):
         :meth:`preDraw`, and followed by a call to :meth:`postDraw`.
         """
         
-        if not self.display.enabled:
-            return
-        
         fslgl.glvolume_funcs.draw(self, zpos, xform)
 
         
     def drawAll(self, zposes, xforms):
         """Calls the module-specific ``drawAll`` function. """
         
-        if not self.display.enabled:
-            return
         fslgl.glvolume_funcs.drawAll(self, zposes, xforms)
 
         
@@ -181,16 +186,9 @@ class GLVolume(globject.GLImageObject):
         """Clears the GL state after drawing from this :class:`GLVolume`
         instance.
         """
-        if not self.display.enabled:
-            return
 
-        gl.glActiveTexture(gl.GL_TEXTURE0)
-        gl.glBindTexture(gl.GL_TEXTURE_3D, 0)
-        gl.glDisable(gl.GL_TEXTURE_3D)
-        
-        gl.glActiveTexture(gl.GL_TEXTURE1)
-        gl.glBindTexture(gl.GL_TEXTURE_1D, 0)
-        gl.glDisable(gl.GL_TEXTURE_1D)
+        self.imageTexture .unbindTexture()
+        self.colourTexture.unbindTexture()
         
         fslgl.glvolume_funcs.postDraw(self) 
 
@@ -201,111 +199,33 @@ class GLVolume(globject.GLImageObject):
         deleting texture handles).
         """
 
-        fsltextures.deleteTexture(self.imageTexture)
+        glresources.delete(self.imageTexture.getTextureName())
+        
+        self.colourTexture.destroy()
+        
+        self.imageTexture  = None
+        self.colourTexture = None
+        
         self.removeDisplayListeners()
         fslgl.glvolume_funcs.destroy(self)
 
-
-    def genVertexData(self):
-        """Generates coordinates at the corners of the image bounds, along the
-        xax/yax plane, which define a slice through the 3D image.
-
-        This method is provided for use by the version-dependent
-        :mod:`fsl.fslview.gl.gl14.glvolume_funcs` and 
-        :mod:`fsl.fslview.gl.gl21.glvolume_funcs` modules, in their
-        implemntation of the ``genVertexData` function.
-
-        :arg image:   The :class:`~fsl.data.image.Image` object to
-                      generate vertex and texture coordinates for.
-
-        :arg display: A :class:`~fsl.fslview.displaycontext.ImageDisplay`
-                      object which defines how the image is to be
-                      rendered.
-
-        :arg xax:     The world space axis which corresponds to the
-                      horizontal screen axis (0, 1, or 2).
-
-        :arg yax:     The world space axis which corresponds to the
-                      vertical screen axis (0, 1, or 2).
-        """
-
-        return globject.slice2D(
-            self.image.shape,
-            self.xax,
-            self.yax,
-            self.display.getTransform('voxel', 'display'))
-
     
-    def refreshColourTexture(self, colourResolution):
-        """Configures the colour texture used to colour image voxels.
-
-        Also createss a transformation matrix which transforms an image voxel
-        value to the range (0-1), which may then be used as a texture
-        coordinate into the colour map texture. This matrix is stored as an
-        attribute of this :class:`GLVolume` object called
-        :attr:`colourMapXForm`. See also the :meth:`genImageTexture` method
-        for more details.
-        """
+    def refreshColourTexture(self):
+        """Configures the colour texture used to colour image voxels."""
 
         display = self.display
         opts    = self.displayOpts
 
-        imin = opts.displayRange[0]
-        imax = opts.displayRange[1]
-
-        # This transformation is used to transform voxel values
-        # from their native range to the range [0.0, 1.0], which
-        # is required for texture colour lookup. Values below
-        # or above the current display range will be mapped
-        # to texture coordinate values less than 0.0 or greater
-        # than 1.0 respectively.
-        if imax == imin: scale = 1
-        else:            scale = imax - imin
-        
-        cmapXform = np.identity(4, dtype=np.float32)
-        cmapXform[0, 0] = 1.0 / scale
-        cmapXform[3, 0] = -imin * cmapXform[0, 0]
-
-        self.colourMapXform = cmapXform
-
-        # Create [self.colourResolution] rgb values,
-        # spanning the entire range of the image
-        # colour map
-        if opts.invert: colourRange = np.linspace(1.0, 0.0, colourResolution)
-        else:           colourRange = np.linspace(0.0, 1.0, colourResolution)
-        
-        colourmap = opts.cmap(colourRange)
+        alpha  = display.alpha / 100.0
+        cmap   = opts.cmap
+        invert = opts.invert
+        dmin   = opts.displayRange[0]
+        dmax   = opts.displayRange[1]
 
-        # Apply global transparency
-        colourmap[:, 3] = display.alpha / 100.0
-        
-        # The colour data is stored on
-        # the GPU as 8 bit rgba tuples
-        colourmap = np.floor(colourmap * 255)
-        colourmap = np.array(colourmap, dtype=np.uint8)
-        colourmap = colourmap.ravel(order='C')
-
-        # GL texture creation stuff
-        gl.glBindTexture(gl.GL_TEXTURE_1D, self.colourTexture)
-        gl.glTexParameteri(gl.GL_TEXTURE_1D,
-                           gl.GL_TEXTURE_MAG_FILTER,
-                           gl.GL_NEAREST)
-        gl.glTexParameteri(gl.GL_TEXTURE_1D,
-                           gl.GL_TEXTURE_MIN_FILTER,
-                           gl.GL_NEAREST)
-        gl.glTexParameteri(gl.GL_TEXTURE_1D,
-                           gl.GL_TEXTURE_WRAP_S,
-                           gl.GL_CLAMP_TO_EDGE)
-
-        gl.glTexImage1D(gl.GL_TEXTURE_1D,
-                        0,
-                        gl.GL_RGBA8,
-                        colourResolution,
-                        0,
-                        gl.GL_RGBA,
-                        gl.GL_UNSIGNED_BYTE,
-                        colourmap)
-        gl.glBindTexture(gl.GL_TEXTURE_1D, 0)
+        self.colourTexture.set(cmap=cmap,
+                               invert=invert,
+                               alpha=alpha,
+                               displayRange=(dmin, dmax))
 
         
     def addDisplayListeners(self):
@@ -322,18 +242,31 @@ class GLVolume(globject.GLImageObject):
 
         display = self.display
         opts    = self.displayOpts
-
-        lName = self.name
+        lName   = self.name
         
-        def vertexUpdate(*a):
-            self.setAxes(self.xax, self.yax)
-
         def colourUpdate(*a):
-            self.refreshColourTexture(self.colourResolution)
+            self.refreshColourTexture()
+            fslgl.glvolume_funcs.updateShaderState(self)
+            self.onUpdate()
+
+        def shaderUpdate(*a):
+            fslgl.glvolume_funcs.updateShaderState(self)
+            self.onUpdate()
+
+        def shaderCompile(*a):
+            fslgl.glvolume_funcs.compileShaders(   self)
+            fslgl.glvolume_funcs.updateShaderState(self)
+            self.onUpdate()
+
+        def update(*a):
+            self.onUpdate()
 
-        display.addListener('transform',     lName, vertexUpdate)
+        display.addListener('resolution',    lName, update)
+        display.addListener('interpolation', lName, shaderUpdate)
+        display.addListener('softwareMode',  lName, shaderCompile)
         display.addListener('alpha',         lName, colourUpdate)
         opts   .addListener('displayRange',  lName, colourUpdate)
+        opts   .addListener('clippingRange', lName, shaderUpdate)
         opts   .addListener('cmap',          lName, colourUpdate)
         opts   .addListener('invert',        lName, colourUpdate)
 
@@ -348,8 +281,11 @@ class GLVolume(globject.GLImageObject):
 
         lName = self.name
 
-        display.removeListener('transform',     lName)
+        display.removeListener('resolution',    lName)
+        display.removeListener('interpolation', lName)
+        display.removeListener('softwareMode',  lName)
         display.removeListener('alpha',         lName)
         opts   .removeListener('displayRange',  lName)
+        opts   .removeListener('clippingRange', lName)
         opts   .removeListener('cmap',          lName)
         opts   .removeListener('invert',        lName)
diff --git a/fsl/fslview/gl/lightboxcanvas.py b/fsl/fslview/gl/lightboxcanvas.py
index 8bca8c62a1a143932f772e7363927f60bd8b3992..7a15fe3fe458de0f638b2ebef9e985626e348dc7 100644
--- a/fsl/fslview/gl/lightboxcanvas.py
+++ b/fsl/fslview/gl/lightboxcanvas.py
@@ -9,17 +9,20 @@
 slices along a single axis from a collection of 3D images.
 """
 
+import sys
 import logging
 
-log = logging.getLogger(__name__)
-
 import numpy     as np
 import OpenGL.GL as gl
 
 import props
 
 import fsl.fslview.gl.slicecanvas as slicecanvas
-import fsl.fslview.gl.textures    as fsltextures
+import fsl.fslview.gl.resources   as glresources
+import fsl.fslview.gl.textures    as textures
+
+
+log = logging.getLogger(__name__)
 
 
 class LightBoxCanvas(slicecanvas.SliceCanvas):
@@ -74,33 +77,6 @@ class LightBoxCanvas(slicecanvas.SliceCanvas):
     """If True, a box will be drawn around the slice containing the current
     location.
     """
-
-    
-    _labels = dict(
-        slicecanvas.SliceCanvas._labels.items() +
-        [('sliceSpacing',   'Slice spacing'),
-         ('ncols',          'Number of columns'),
-         ('nrows',          'Number of rows'),
-         ('topRow',         'Top row'),
-         ('zrange',         'Slice range'),
-         ('showGridLines',  'Show grid lines'),
-         ('highlightSlice', 'Highlight current slice')])
-    """Labels for the properties which are intended to be user editable."""
-
-
-    _tooltips = dict(
-        slicecanvas.SliceCanvas._tooltips.items() +
-        [('sliceSpacing',   'Distance (mm) between consecutive slices'),
-         ('ncols',          'Number of slices to display on one row'),
-         ('nrows',          'Number of rows to display on the canvas'),
-         ('topRow',         'Index number of top row (from '
-                            '0 to nrows-1) to display'),
-         ('zrange',         'Range (mm) along Z axis of slices to display'),
-         ('showGridLines',  'Show grid lines between slices'),
-         ('highlightSlice', 'Highlights the currently selected slice')])
-    """Tooltips to be used as help text."""
-
-    _propHelp = _tooltips
     
     
     def worldToCanvas(self, xpos, ypos, zpos):
@@ -204,6 +180,10 @@ class LightBoxCanvas(slicecanvas.SliceCanvas):
         self._nslices   = 0
         self._totalRows = 0
 
+        # This will point to a RenderTexture if
+        # the offscreen render mode is enabled
+        self.__offscreenRenderTexture = None
+
         slicecanvas.SliceCanvas.__init__(self, imageList, displayCtx, zax)
 
         # default to showing the entire slice range
@@ -254,33 +234,76 @@ class LightBoxCanvas(slicecanvas.SliceCanvas):
         self._refresh()
 
 
-    def _onTwoStageRenderChange(self, *args, **kwargs):
-        """Overrides
-        :class:`~fsl.fslview.gl.slicecanvas.SliceCanvas._onTwoStageRenderChange`.
+    def _renderModeChange(self, *a):
+        """Overrides :meth:`.SliceCanvas._renderModeChange`.
+        """
+        
+        if self.__offscreenRenderTexture is not None:
+            self.__offscreenRenderTexture.destroy()
+            self.__offscreenRenderTexture = None
+            
+        slicecanvas.SliceCanvas._renderModeChange(self, *a)
+
+
+    def _updateRenderTextures(self):
+        """Overrides :meth:`.SliceCanvas._updateRenderTextures`.
         """
 
-        # The LightBoxCanvas does two-stage rendering
+        if self.renderMode == 'onscreen':
+            return
+
+        # The LightBoxCanvas does offscreen rendering
         # a bit different to the SliceCanvas. The latter
         # uses a separate render texture for each image,
         # whereas here we're going to use a single
-        # render texture for all images.
-        if self._renderTextures == {}:
-            self._renderTextures = None
+        # render texture for all images. 
+        elif self.renderMode == 'offscreen':
+            if self.__offscreenRenderTexture is not None:
+                self.__offscreenRenderTexture.destroy()
+
+            self.__offscreenRenderTexture = textures.RenderTexture(
+                '{}_{}'.format(type(self).__name__, id(self)),
+                gl.GL_LINEAR)
+
+            self.__offscreenRenderTexture.setSize(768, 768)
+
+        # The LightBoxCanvas handles re-render mode
+        # the same way as the SliceCanvas - a separate
+        # RenderTextureStack for eacn globject
+        elif self.renderMode == 'prerender':
             
-        # nothing to be done
-        if self.twoStageRender and self._renderTextures is not None:
-            return
-        
-        if not self.twoStageRender and self._renderTextures is None:
-            return
+            # Delete any RenderTextureStack instances for
+            # images which have been removed from the list
+            for image, (tex, name) in self._prerenderTextures.items():
+                if image not in self.imageList:
+                    self._prerenderTextures.pop(image)
+                    glresources.delete(name)
+
+            # Create a RendeTextureStack for images
+            # which have been added to the list
+            for image in self.imageList:
+                if image in self._prerenderTextures:
+                    continue
+
+                globj = self._glObjects.get(image, None)
+                
+                if globj is None:
+                    continue
+                
+                name = '{}_{}_zax{}'.format(
+                    id(image),
+                    textures.RenderTextureStack.__name__,
+                    self.zax)
 
-        if not self.twoStageRender:
-            self._renderTextures.destroy()
-            self._renderTextures = None
+                if glresources.exists(name):
+                    rt = glresources.get(name)
+                else:
 
-        else:
-            self._renderTextures = fsltextures.RenderTexture(
-                256, 256, gl.GL_LINEAR)
+                    rt = textures.RenderTextureStack(globj)
+                    rt.setAxes(self.xax, self.yax)
+                    glresources.set(name, rt)
+
+                self._prerenderTextures[image] = rt, name
 
         self._refresh()
 
@@ -381,8 +404,25 @@ class LightBoxCanvas(slicecanvas.SliceCanvas):
 
             # Pick a sensible default for the
             # slice spacing - the smallest pixdim
-            # across all images in the list
-            newZGap   = min([i.pixdim[self.zax] for i in self.imageList])
+            # across all images in the list 
+            newZGap = sys.float_info.max
+
+            for image in self.imageList:
+                display = self.displayCtx.getDisplayProperties(image)
+
+                # TODO this is specific to the Image type,
+                # and shouldn't be. We're going to need to
+                # support other overlay types soon...
+                if   display.transform == 'id':
+                    zgap = 1
+                elif display.transform == 'pixdim':
+                    zgap = image.pixdim[self.zax]
+                else:
+                    zgap = min(image.pixdim[:3])
+
+                if zgap < newZGap:
+                    newZGap = zgap
+
             newZRange = self.displayCtx.bounds.getRange(self.zax)
 
             # Changing the zrange/sliceSpacing properties will, in most cases,
@@ -634,8 +674,10 @@ class LightBoxCanvas(slicecanvas.SliceCanvas):
         yverts[0, 0] = xmin + (col)     * xlen
         yverts[1, 0] = xmin + (col + 1) * xlen
 
-        self.getAnnotations().line(xverts[0], xverts[1], colour=(0, 1, 0))
-        self.getAnnotations().line(yverts[0], yverts[1], colour=(0, 1, 0))
+        annot = self.getAnnotations()
+
+        annot.line(xverts[0], xverts[1], colour=(0, 1, 0), width=1)
+        annot.line(yverts[0], yverts[1], colour=(0, 1, 0), width=1)
 
         
     def _draw(self, *a):
@@ -645,16 +687,28 @@ class LightBoxCanvas(slicecanvas.SliceCanvas):
         if not self._setGLContext():
             return
 
-        if self.twoStageRender:
-            log.debug('Rendering to off-screen frame buffer')
-            self._renderTextures.bindAsRenderTarget()
-            self._setViewport(size=self._renderTextures.getSize())
+        if self.renderMode == 'offscreen':
+            
+            log.debug('Rendering to off-screen texture')
+
+            rt = self.__offscreenRenderTexture
+            
+            lo = [None] * 3
+            hi = [None] * 3
+            
+            lo[self.xax], hi[self.xax] = self.displayBounds.x
+            lo[self.yax], hi[self.yax] = self.displayBounds.y
+            lo[self.zax], hi[self.zax] = self.zrange
+            
+            rt.bindAsRenderTarget()
+            rt.setRenderViewport(self.xax, self.yax, lo, hi)
+            gl.glClear(gl.GL_COLOR_BUFFER_BIT)
             
         else:
             self._setViewport()
 
-        startSlice   = self.ncols * self.topRow
-        endSlice     = startSlice + self.nrows * self.ncols
+        startSlice = self.ncols * self.topRow
+        endSlice   = startSlice + self.nrows * self.ncols
 
         if endSlice > self._nslices:
             endSlice = self._nslices    
@@ -666,25 +720,42 @@ class LightBoxCanvas(slicecanvas.SliceCanvas):
 
             globj = self._glObjects.get(image, None)
 
-            if (globj is None) or (not globj.ready()) or not display.enabled:
+            if (globj is None) or (not display.enabled):
                 continue
 
-            log.debug('Drawing {} slices ({} - {}) for image {}'.format(
-                endSlice - startSlice, startSlice, endSlice, image))
-
-            globj.preDraw()
+            log.debug('Drawing {} slices ({} - {}) for '
+                      'image {} directly to canvas'.format(
+                          endSlice - startSlice, startSlice, endSlice, image))
 
             zposes = self._sliceLocs[ image][startSlice:endSlice]
             xforms = self._transforms[image][startSlice:endSlice]
 
-            globj.drawAll(zposes, xforms)
+            if self.renderMode == 'prerender':
+                rt, name = self._prerenderTextures.get(image, (None, None))
+
+                if rt is None:
+                    continue
+                
+                log.debug('Drawing {} slices ({} - {}) for image {} '
+                          'from pre-rendered texture'.format(
+                              endSlice - startSlice,
+                              startSlice,
+                              endSlice,
+                              image))
+                
+                for zpos, xform in zip(zposes, xforms):
+                    rt.draw(zpos, xform)
+            else:
 
-            globj.postDraw()
+                globj.preDraw()
+                globj.drawAll(zposes, xforms)
+                globj.postDraw()
 
-        if self.twoStageRender:
-            self._renderTextures.unbind()
+        if self.renderMode == 'offscreen':
+            rt.unbindAsRenderTarget()
             self._setViewport()
-            self._renderTextures.drawRender(
+            rt.drawOnBounds(
+                0,
                 self.displayBounds.xlo,
                 self.displayBounds.xhi,
                 self.displayBounds.ylo,
diff --git a/fsl/fslview/gl/resources.py b/fsl/fslview/gl/resources.py
new file mode 100644
index 0000000000000000000000000000000000000000..de9735671a0f98e83a6219f88440705e38178307
--- /dev/null
+++ b/fsl/fslview/gl/resources.py
@@ -0,0 +1,81 @@
+#!/usr/bin/env python
+#
+# resources.py - Simple manager for shared OpenGL resources.
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+
+import logging
+log = logging.getLogger(__name__)
+
+
+class _Resource(object):
+
+    def __init__(self, key, resource):
+        self.key      = key
+        self.resource = resource
+        self.refcount = 0
+        
+
+_resources = {}
+
+def exists(key):
+    return key in _resources
+
+
+def get(key, createFunc=None, *args, **kwargs):
+
+    r = _resources.get(key, None)
+
+    if r is None and createFunc is None:
+        raise KeyError('Resource {} does not exist'.format(str(key)))
+
+    if r is not None:
+        r.refcount += 1
+
+        log.debug('Resource {} reference count '
+                  'increased to {}'.format(str(key), r.refcount))
+
+        return r.resource
+
+    if createFunc is not None:
+        return set(key, createFunc(*args, **kwargs))
+
+
+def set(key, resource, overwrite=False):
+
+    if (not overwrite) and (key in _resources):
+        raise KeyError('Resource {} already exists'.format(str(key)))
+
+    if not overwrite:
+        log.debug('Adding resource {}'.format(str(key)))
+
+        r               = _Resource(key, resource)
+        r.refcount     += 1
+        _resources[key] = r
+
+        log.debug('Resource {} reference count '
+                  'increased to {}'.format(str(key), r.refcount)) 
+        
+    else:
+        log.debug('Updating resource {}'.format(str(key)))
+
+        _resources[key].resource = resource
+
+    return resource
+
+    
+def delete(key):
+
+    r           = _resources[key]
+    r.refcount -= 1
+
+    log.debug('Resource {} reference count '
+              'decreased to {}'.format(str(key), r.refcount))
+
+    if r.refcount <= 0:
+
+        log.debug('Destroying resource {}'.format(str(key)))
+
+        _resources.pop(key)
+        r.resource.destroy()
diff --git a/fsl/fslview/gl/routines.py b/fsl/fslview/gl/routines.py
new file mode 100644
index 0000000000000000000000000000000000000000..6b1ff056212090eec1300706456304b1b87d4e27
--- /dev/null
+++ b/fsl/fslview/gl/routines.py
@@ -0,0 +1,542 @@
+#!/usr/bin/env python
+#
+# routines.py - A collection of disparate utility functions related to OpenGL.
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+
+import itertools           as it
+
+import OpenGL.GL           as gl
+import numpy               as np
+
+import fsl.utils.transform as transform
+
+
+def show2D(xax, yax, width, height, lo, hi):
+
+    zax = 3 - xax - yax
+
+    xmin, xmax = lo[xax], hi[xax]
+    ymin, ymax = lo[yax], hi[yax]
+    zmin, zmax = lo[zax], hi[zax]
+
+    gl.glViewport(0, 0, width, height)
+    gl.glMatrixMode(gl.GL_PROJECTION)
+    gl.glLoadIdentity()
+
+    zdist = max(abs(zmin), abs(zmax))
+
+    gl.glOrtho(xmin, xmax, ymin, ymax, -zdist, zdist)
+    gl.glMatrixMode(gl.GL_MODELVIEW)
+    gl.glLoadIdentity()
+
+    # Rotate world space so the displayed slice
+    # is visible and correctly oriented
+    # TODO There's got to be a more generic way
+    # to perform this rotation. This will break
+    # if I add functionality allowing the user
+    # to specifty the x/y axes on initialisation. 
+    if zax == 0:
+        gl.glRotatef(-90, 1, 0, 0)
+        gl.glRotatef(-90, 0, 0, 1)
+    elif zax == 1:
+        gl.glRotatef(270, 1, 0, 0)
+
+
+def calculateSamplePoints(shape, resolution, xform, xax, yax):
+    """Calculates a uniform grid of points, in the display coordinate system
+    (as specified by the given
+    :class:`~fsl.fslview.displaycontext.Display` object properties) along the
+    x-y plane (as specified by the xax/yax indices), at which the given image
+    should be sampled for display purposes.
+
+    This function returns a tuple containing:
+
+     - a numpy array of shape `(N, 3)`, containing the coordinates of the
+       centre of every sampling point in the display coordinate system.
+
+     - the horizontal distance (along xax) between adjacent points
+
+     - the vertical distance (along yax) between adjacent points
+
+     - The number of samples along the horizontal axis (xax)
+
+     - The number of samples along the vertical axis (yax)
+
+    :arg shape:      The shape of the data to be sampled.
+
+    :arg xform:      A transformation matrix which converts from data 
+                     coordinates to the display coordinate system.
+
+    :arg resolution: The desired resolution in display coordinates, along
+                     each display axis.
+
+    :arg xax:        The horizontal display coordinate system axis (0, 1, or
+                     2).
+
+    :arg yax:        The vertical display coordinate system axis (0, 1, or 2).
+    """
+
+    xres = resolution[xax]
+    yres = resolution[yax]
+
+    # These values give the min/max x/y
+    # values of a bounding box which
+    # encapsulates the entire image,
+    # in the display coordinate system
+    xmin, xmax = transform.axisBounds(shape, xform, xax)
+    ymin, ymax = transform.axisBounds(shape, xform, yax)
+
+    # Number of samples along each display
+    # axis, given the requested resolution
+    xNumSamples = np.floor((xmax - xmin) / xres)
+    yNumSamples = np.floor((ymax - ymin) / yres)
+
+    # adjust the x/y resolution so
+    # the samples fit exactly into
+    # the data bounding box
+    xres = (xmax - xmin) / xNumSamples
+    yres = (ymax - ymin) / yNumSamples
+
+    # Calculate the locations of every 
+    # sample point in display space
+    worldX = np.linspace(xmin + 0.5 * xres,
+                         xmax - 0.5 * xres,
+                         xNumSamples)
+    worldY = np.linspace(ymin + 0.5 * yres,
+                         ymax - 0.5 * yres,
+                         yNumSamples)
+
+    worldX, worldY = np.meshgrid(worldX, worldY)
+    
+    coords = np.zeros((worldX.size, 3), dtype=np.float32)
+    coords[:, xax] = worldX.flatten()
+    coords[:, yax] = worldY.flatten()
+
+    return coords, xres, yres, xNumSamples, yNumSamples
+
+
+def samplePointsToTriangleStrip(coords,
+                                xpixdim,
+                                ypixdim,
+                                xlen,
+                                ylen,
+                                xax,
+                                yax):
+    """Given a regular 2D grid of points at which an image is to be sampled
+    (for example, that generated by the :func:`calculateSamplePoints` function
+    above), converts those points into an OpenGL vertex triangle strip.
+
+    A grid of M*N points is represented by M*2*(N + 1) vertices. For example,
+    this image represents a 4*3 grid, with periods representing vertex
+    locations::
+    
+        .___.___.___.___.
+        |   |   |   |   |
+        |   |   |   |   |
+        .---.---.---.---.
+        .___.___.__ .___.
+        |   |   |   |   |
+        |   |   |   |   |
+        .---.---.---.---.
+        .___.___.___.___.
+        |   |   |   |   |
+        |   |   |   |   |
+        .___.___.___.___.
+
+    
+    Vertex locations which are vertically adjacent represent the same point in
+    space. Such vertex pairs are unable to be combined because, in OpenGL,
+    they must be represented by distinct vertices (we can't apply multiple
+    colours/texture coordinates to a single vertex location) So we have to
+    repeat these vertices in order to achieve accurate colouring of each
+    voxel.
+
+    We draw each horizontal row of samples one by one, using two triangles to
+    draw each voxel. In order to eliminate the need to specify six vertices
+    for every voxel, and hence to reduce the amount of memory used, we are
+    using a triangle strip to draw each row of voxels. This image depicts a
+    triangle strip used to draw a row of three samples (periods represent
+    vertex locations)::
+
+
+        1  3  5  7
+        .  .  .  .
+        |\ |\ |\ |
+        | \| \| \|
+        .  .  .  .
+        0  2  4  6
+      
+    In order to use a single OpenGL call to draw multiple non-contiguous voxel
+    rows, between every column we add a couple of 'dummy' vertices, which will
+    then be interpreted by OpenGL as 'degenerate triangles', and will not be
+    drawn. So in reality, a 4*3 slice would be drawn as follows (with vertices
+    labelled from [a-z0-9]:
+
+         v  x  z  1  33
+         |\ |\ |\ |\ |
+         | \| \| \| \|
+        uu  w  y  0  2
+         l  n  p  r  tt
+         |\ |\ |\ |\ |
+         | \| \| \| \|
+        kk  m  o  q  s  
+         b  d  f  h  jj
+         |\ |\ |\ |\ |
+         | \| \| \| \|
+         a  c  e  g  i
+    
+    These repeated/degenerate vertices are dealt with by using a vertex index
+    array.  See these links for good overviews of triangle strips and
+    degenerate triangles in OpenGL:
+    
+     - http://www.learnopengles.com/tag/degenerate-triangles/
+     - http://en.wikipedia.org/wiki/Triangle_strip
+
+    A tuple is returned containing:
+
+      - A 2D `numpy.float32` array of shape `(2 * (xlen + 1) * ylen), 3)`,
+        containing the coordinates of all triangle strip vertices which
+        represent the entire grid of sample points.
+    
+      - A 2D `numpy.float32` array of shape `(2 * (xlen + 1) * ylen), 3)`,
+        containing the centre of every grid, to be used for texture
+        coordinates/colour lookup.
+    
+      - A 1D `numpy.uint32` array of size `ylen * (2 * (xlen + 1) + 2) - 2`
+        containing indices into the first array, defining the order in which
+        the vertices need to be rendered. There are more indices than vertex
+        coordinates due to the inclusion of repeated/degenerate vertices.
+
+    :arg coords:  N*3 array of points, the sampling locations.
+    
+    :arg xpixdim: Length of one sample along the horizontal axis.
+    
+    :arg ypixdim: Length of one sample along the vertical axis.
+    
+    :arg xlen:    Number of samples along the horizontal axis.
+    
+    :arg ylen:    Number of samples along the vertical axis.
+    
+    :arg xax:     Display coordinate system axis which corresponds to the
+                  horizontal screen axis.
+    
+    :arg yax:     Display coordinate system axis which corresponds to the
+                  vertical screen axis.
+    """
+
+    coords = coords.reshape(ylen, xlen, 3)
+
+    xlen = int(xlen)
+    ylen = int(ylen)
+
+    # Duplicate every row - each voxel
+    # is defined by two vertices 
+    coords = coords.repeat(2, 0)
+
+    texCoords   = np.array(coords, dtype=np.float32)
+    worldCoords = np.array(coords, dtype=np.float32)
+
+    # Add an extra column at the end
+    # of the world coordinates
+    worldCoords = np.append(worldCoords, worldCoords[:, -1:, :], 1)
+    worldCoords[:, -1, xax] += xpixdim
+
+    # Add an extra column at the start
+    # of the texture coordinates
+    texCoords = np.append(texCoords[:, :1, :], texCoords, 1)
+
+    # Move the x/y world coordinates to the
+    # sampling point corners (the texture
+    # coordinates remain in the voxel centres)
+    worldCoords[   :, :, xax] -= 0.5 * xpixdim
+    worldCoords[ ::2, :, yax] -= 0.5 * ypixdim
+    worldCoords[1::2, :, yax] += 0.5 * ypixdim 
+
+    vertsPerRow  = 2 * (xlen + 1) 
+    dVertsPerRow = 2 * (xlen + 1) + 2
+    nindices     = ylen * dVertsPerRow - 2
+
+    indices = np.zeros(nindices, dtype=np.uint32)
+
+    for yi, xi in it.product(range(ylen), range(xlen + 1)):
+        
+        ii = yi * dVertsPerRow + 2 * xi
+        vi = yi *  vertsPerRow + xi
+        
+        indices[ii]     = vi
+        indices[ii + 1] = vi + xlen + 1
+
+        # add degenerate vertices at the end
+        # every row (but not needed for last
+        # row)
+        if xi == xlen and yi < ylen - 1:
+            indices[ii + 2] = vi + xlen + 1
+            indices[ii + 3] = (yi + 1) * vertsPerRow
+
+    worldCoords = worldCoords.reshape((xlen + 1) * (2 * ylen), 3)
+    texCoords   = texCoords  .reshape((xlen + 1) * (2 * ylen), 3)
+
+    return worldCoords, texCoords, indices
+
+
+def voxelGrid(points, xax, yax, xpixdim, ypixdim):
+    """Given a ``N*3`` array of ``points`` (assumed to be voxel
+    coordinates), creates an array of vertices which can be used
+    to render each point as an unfilled rectangle.
+
+    :arg points:  An ``N*3`` array of voxel xyz coordinates
+
+    :arg xax:     XYZ axis index that maps to the horizontal scren axis
+    
+    :arg yax:     XYZ axis index that maps to the vertical scren axis
+    
+    :arg xpixdim: Length of a voxel along the x axis.
+    
+    :arg ypixdim: Length of a voxel along the y axis.
+    """
+
+    if len(points.shape) == 1:
+        points = points.reshape(1, 3)
+
+    npoints  = points.shape[0]
+    vertices = np.repeat(np.array(points, dtype=np.float32), 4, axis=0)
+
+    xpixdim = xpixdim / 2.0
+    ypixdim = ypixdim / 2.0
+
+    # bottom left corner
+    vertices[ ::4, xax] -= xpixdim 
+    vertices[ ::4, yax] -= ypixdim
+
+    # bottom right
+    vertices[1::4, xax] += xpixdim
+    vertices[1::4, yax] -= ypixdim
+    
+    # top left
+    vertices[2::4, xax] -= xpixdim
+    vertices[2::4, yax] += ypixdim
+
+    # top right
+    vertices[3::4, xax] += xpixdim
+    vertices[3::4, yax] += ypixdim
+
+    # each square is rendered as four lines
+    indices = np.array([0, 1, 0, 2, 1, 3, 2, 3], dtype=np.uint32)
+    indices = np.tile(indices, npoints)
+    
+    indices = (indices.T +
+               np.repeat(np.arange(0, npoints * 4, 4, dtype=np.uint32), 8)).T
+    
+    return vertices, indices
+
+
+def slice2D(dataShape,
+            xax,
+            yax,
+            zpos,
+            voxToDisplayMat,
+            displayToVoxMat,
+            geometry='triangles'):
+    """Generates and returns vertices which denote a slice through an
+    array of the given ``dataShape``, parallel to the plane defined by the
+    given ``xax`` and ``yax`` and at the given z position, in the space
+    defined by the given ``voxToDisplayMat``.
+
+    If ``geometry`` is ``triangles`` (the default), six vertices are returned,
+    arranged as follows:
+
+         4---5
+        1 \  |
+        |\ \ |
+        | \ \| 
+        |  \ 3
+        0---2
+
+    Otherwise, if geometry is ``square``, four vertices are returned, arranged
+    as follows:
+
+         
+        3---2
+        |   |
+        |   |
+        |   |
+        0---1
+
+    :arg dataShape:       Number of elements along each dimension in the
+                          image data.
+    
+    :arg xax:             Index of display axis which corresponds to the
+                          horizontal screen axis.
+
+    :arg yax:             Index of display axis which corresponds to the
+                          vertical screen axis.
+
+    :arg zpos:            Position of the slice along the screen z axis.
+    
+    :arg voxToDisplayMat: Affine transformation matrix which transforms from
+                          voxel/array indices into the display coordinate
+                          system.
+
+    :arg displayToVoxMat: Inverse of the ``voxToDisplayMat``.
+    
+    Returns a tuple containing:
+    
+      - A ``N*3`` ``numpy.float32`` array containing the vertex locations
+        of a slice through the data, where ``N=6`` if ``geometry=triangles``,
+        or ``N=4`` if ``geometry=square``,
+
+      - A ``N*3`` ``numpy.float32`` array containing the voxel coordinates
+        that correspond to the vertex locations.
+
+      - A ``N*3`` ``numpy.float32`` array containing the texture coordinates
+        that correspond to the voxel coordinates.
+
+    """
+
+    zax        = 3 - xax - yax
+    xmin, xmax = transform.axisBounds(dataShape, voxToDisplayMat, xax)
+    ymin, ymax = transform.axisBounds(dataShape, voxToDisplayMat, yax)
+
+    if geometry == 'triangles':
+
+        vertices = np.zeros((6, 3), dtype=np.float32)
+
+        vertices[ 0, [xax, yax]] = [xmin, ymin]
+        vertices[ 1, [xax, yax]] = [xmin, ymax]
+        vertices[ 2, [xax, yax]] = [xmax, ymin]
+        vertices[ 3, [xax, yax]] = [xmax, ymin]
+        vertices[ 4, [xax, yax]] = [xmin, ymax]
+        vertices[ 5, [xax, yax]] = [xmax, ymax]
+        
+    elif geometry == 'square':
+        vertices = np.zeros((4, 3), dtype=np.float32)
+
+        vertices[ 0, [xax, yax]] = [xmin, ymin]
+        vertices[ 1, [xax, yax]] = [xmax, ymin]
+        vertices[ 2, [xax, yax]] = [xmax, ymax]
+        vertices[ 3, [xax, yax]] = [xmin, ymax]
+    else:
+        raise ValueError('Unrecognised geometry type: {}'.format(geometry))
+
+    vertices[:, zax] = zpos
+
+    voxCoords = transform.transform(vertices, displayToVoxMat)
+
+    # offset by 0.5, because voxel coordinates are by
+    # default centered at 0 (i.e. the space of a voxel
+    # lies in the range [-0.5, 0.5]), but we want voxel
+    # coordinates to map to the effective range [0, 1]
+    voxCoords = voxCoords + 0.5
+    texCoords = voxCoords / dataShape
+
+    return vertices, voxCoords, texCoords
+
+
+def subsample(data, resolution, pixdim=None, volume=None):
+    """Samples the given 3D data according to the given resolution.
+
+    Returns a tuple containing:
+
+      - A 3D numpy array containing the sub-sampled data.
+
+      - A tuple containing the ``(x, y, z)`` starting indices of the
+        sampled data.
+
+      - A tuple containing the ``(x, y, z)`` steps of the sampled data.
+
+    :arg data:       The data to be sampled.
+
+    :arg resolution: Sampling resolution, proportional to the values in
+                     ``pixdim``.
+
+    :arg pixdim:     Length of each dimension in the input data (defaults to
+                     ``(1.0, 1.0, 1.0)``).
+
+    :arg volume:     If the image is a 4D volume, the volume index of the 3D
+                     image to be sampled.
+    """
+
+    if pixdim is None:
+        pixdim = (1.0, 1.0, 1.0)
+
+    if volume is None:
+        volume = slice(None, None, None)
+
+    xstep = np.round(resolution / pixdim[0])
+    ystep = np.round(resolution / pixdim[1]) 
+    zstep = np.round(resolution / pixdim[2])
+
+    if xstep < 1: xstep = 1
+    if ystep < 1: ystep = 1
+    if zstep < 1: zstep = 1
+
+    xstart = np.floor(xstep / 2)
+    ystart = np.floor(ystep / 2)
+    zstart = np.floor(zstep / 2)
+        
+    if len(data.shape) > 3: sample = data[xstart::xstep,
+                                          ystart::ystep,
+                                          zstart::zstep,
+                                          volume]
+    else:                   sample = data[xstart::xstep,
+                                          ystart::ystep,
+                                          zstart::zstep]
+
+    return sample, (xstart, ystart, zstart), (xstep, ystep, zstep)
+
+
+def broadcast(vertices, indices, zposes, xforms, zax):
+    """Given a set of vertices and indices (assumed to be 2D representations
+    of some geometry in a 3D space, with the depth axis specified by ``zax``),
+    replicates them across all of the specified Z positions, applying the
+    corresponding transformation to each set of vertices.
+
+    :arg vertices: Vertex array (a ``N*3`` numpy array).
+    
+    :arg indices:  Index array.
+    
+    :arg zposes:   Positions along the depth axis at which the vertices
+                   are to be replicated.
+    
+    :arg xforms:   Sequence of transformation matrices, one for each
+                   Z position.
+    
+    :arg zax:      Index of the 'depth' axis
+
+    Returns three values:
+    
+      - A numpy array containing all of the generated vertices
+    
+      - A numpy array containing the original vertices for each of the
+        generated vertices, which may be used as texture coordinates
+
+      - A new numpy array containing all of the generated indices.
+    """
+
+    vertices = np.array(vertices)
+    indices  = np.array(indices)
+    
+    nverts   = vertices.shape[0]
+    nidxs    = indices.shape[ 0]
+
+    allTexCoords  = np.zeros((nverts * len(zposes), 3), dtype=np.float32)
+    allVertCoords = np.zeros((nverts * len(zposes), 3), dtype=np.float32)
+    allIndices    = np.zeros( nidxs  * len(zposes),     dtype=np.uint32)
+    
+    for i, (zpos, xform) in enumerate(zip(zposes, xforms)):
+
+        vertices[:, zax] = zpos
+
+        vStart = i * nverts
+        vEnd   = vStart + nverts
+
+        iStart = i * nidxs
+        iEnd   = iStart + nidxs
+
+        allIndices[   iStart:iEnd]    = indices + i * nverts
+        allTexCoords[ vStart:vEnd, :] = vertices
+        allVertCoords[vStart:vEnd, :] = transform.transform(vertices, xform)
+        
+    return allVertCoords, allTexCoords, allIndices
diff --git a/fsl/fslview/gl/shaders.py b/fsl/fslview/gl/shaders.py
index 17f8967ca9f55a73147cb55ddac5f2004565073c..548723fdb0bcca086f3a50dd787c46cef21eaf5d 100644
--- a/fsl/fslview/gl/shaders.py
+++ b/fsl/fslview/gl/shaders.py
@@ -24,12 +24,57 @@ import logging
 
 import os.path as op
 
-import fsl.fslview.gl as fslgl
+import fsl.fslview.gl     as fslgl
+import fsl.utils.typedict as td
 
 
 log = logging.getLogger(__name__)
 
 
+_shaderTypePrefixMap = td.TypeDict({
+    
+    ('GLVolume',     'vert', False) : 'glvolume',
+    ('GLVolume',     'vert', True)  : 'glvolume_sw',
+    ('GLVolume',     'frag', False) : 'glvolume',
+    ('GLVolume',     'frag', True)  : 'glvolume_sw',
+    
+    ('GLRGBVector',  'vert', False) : 'glvolume',
+    ('GLRGBVector',  'vert', True)  : 'glvolume_sw',
+    
+    ('GLRGBVector',  'frag', False) : 'glvector',
+    ('GLRGBVector',  'frag', True)  : 'glvector_sw',
+
+    ('GLLineVector', 'vert', False) : 'gllinevector',
+    ('GLLineVector', 'vert', True)  : 'gllinevector_sw',
+    
+    ('GLLineVector', 'frag', False) : 'glvector',
+    ('GLLineVector', 'frag', True)  : 'glvector_sw', 
+})
+"""This dictionary provides a mapping between :class:`.GLObject` types,
+and file name prefixes, identifying the shader programs to use.
+"""
+
+
+def getShaderPrefix(globj, shaderType, sw):
+    """Returns the prefix identifying the vertex/fragment shader programs to use
+    for the given :class:`.GLObject` instance. If ``globj`` is a string, it is
+    returned unchanged.
+    """
+    
+    if isinstance(globj, str):
+        return globj
+    
+    return _shaderTypePrefixMap[globj, shaderType, sw]
+
+
+def setShaderPrefix(globj, shaderType, sw, prefix):
+    """Updates the prefix identifying the vertex/fragment shader programs to use
+    for the given :class:`.GLObject` type or instance.
+    """
+    
+    _shaderTypePrefixMap[globj, shaderType, sw] = prefix
+
+
 def setVertexProgramVector(index, vector):
     """Convenience function which sets the vertex program local parameter
     at the given index to the given 4 component vector.
@@ -181,28 +226,29 @@ def compileShaders(vertShaderSrc, fragShaderSrc):
     return program
 
 
-def getVertexShader(globj):
+def getVertexShader(globj, sw=False):
     """Returns the vertex shader source for the given GL object."""
-    return _getShader(globj, 'vert')
+    return _getShader(globj, 'vert', sw)
 
 
-def getFragmentShader(globj):
+def getFragmentShader(globj, sw=False):
     """Returns the fragment shader source for the given GL object.""" 
-    return _getShader(globj, 'frag')
+    return _getShader(globj, 'frag', sw)
 
 
-def _getShader(globj, shaderType):
+def _getShader(globj, shaderType, sw):
     """Returns the shader source for the given GL object and the given
     shader type ('vert' or 'frag').
     """
-    fname = _getFileName(globj, shaderType)
+    fname = _getFileName(globj, shaderType, sw)
     with open(fname, 'rt') as f: src = f.read()
     return _preprocess(src)    
 
 
-def _getFileName(globj, shaderType):
+def _getFileName(globj, shaderType, sw):
     """Returns the file name of the shader program for the given GL object
-    and shader type.
+    and shader type. The ``globj`` parameter may alternately be a string,
+    in which case it is used as the prefix for the shader program file name.
     """
 
     if   fslgl.GL_VERSION == '2.1':
@@ -215,18 +261,7 @@ def _getFileName(globj, shaderType):
     if shaderType not in ('vert', 'frag'):
         raise RuntimeError('Invalid shader type: {}'.format(shaderType))
 
-    # callers can request a specific
-    # shader by passing the name, rather
-    # than passing a GLObject instance
-    import fsl.fslview.gl.glvolume as glvolume
-    import fsl.fslview.gl.glvector as glvector
-    
-    if   isinstance(globj, str):               prefix =  globj
-    elif isinstance(globj, glvolume.GLVolume): prefix = 'glvolume'
-    elif isinstance(globj, glvector.GLVector): prefix = 'glvector'
-    else:
-        raise RuntimeError('Unknown GL object type: '
-                           '{}'.format(type(globj)))
+    prefix = getShaderPrefix(globj, shaderType, sw)
 
     return op.join(op.dirname(__file__), subdir, '{}_{}.{}'.format(
         prefix, shaderType, suffix))
diff --git a/fsl/fslview/gl/slicecanvas.py b/fsl/fslview/gl/slicecanvas.py
index eb204d46f326a25238d2d395866d294f05205715..f645a4e0d1eb6c3a2ea8d39ddd53583f33c96f96 100644
--- a/fsl/fslview/gl/slicecanvas.py
+++ b/fsl/fslview/gl/slicecanvas.py
@@ -29,8 +29,10 @@ import OpenGL.GL              as gl
 import props
 
 import fsl.data.image             as fslimage
+import fsl.fslview.gl.routines    as glroutines
+import fsl.fslview.gl.resources   as glresources
 import fsl.fslview.gl.globject    as globject
-import fsl.fslview.gl.textures    as fsltextures
+import fsl.fslview.gl.textures    as textures
 import fsl.fslview.gl.annotations as annotations
 
 
@@ -39,6 +41,7 @@ class SliceCanvas(props.HasProperties):
     collection of 3D images (see :class:`fsl.data.image.ImageList`).
     """
 
+    
     pos = props.Point(ndims=3)
     """The currently displayed position. The ``pos.x`` and ``pos.y`` positions
     denote the position of a 'cursor', which is highlighted with green
@@ -50,6 +53,7 @@ class SliceCanvas(props.HasProperties):
     to 'depth'.
     """
 
+    
     zoom = props.Percentage(minval=100.0,
                             maxval=1000.0,
                             default=100.0,
@@ -67,15 +71,6 @@ class SliceCanvas(props.HasProperties):
     """
 
     
-    twoStageRender = props.Boolean(default=False)
-    """If ``True``, the scene is rendered off-screen to a fixed-size texture;
-    this texture is then rendered to the canvas. If ``False``, the scene is
-    rendered directly to the canvas. Two-stage rendering will probably give
-    better performance on old graphics cards, and when software-based
-    rendering is being used.
-    """
-
-    
     showCursor = props.Boolean(default=True)
     """If ``False``, the green crosshairs which show
     the current cursor location will not be drawn.
@@ -93,27 +88,24 @@ class SliceCanvas(props.HasProperties):
     
     invertY = props.Boolean(default=False)
     """If True, the display is inverted along the Y (vertical screen) axis.
-    """ 
+    """
 
     
-    _labels = {
-        'zoom'       : 'Zoom level',
-        'showCursor' : 'Show cursor',
-        'zax'        : 'Z axis',
-        'invertX'    : 'Invert X axis',
-        'invertY'    : 'Invert Y axis'}
-    """Labels for the properties which are intended to be user editable."""
-
+    renderMode = props.Choice(('onscreen', 'offscreen', 'prerender'))
+    """How the GLObjects are rendered to the canvas - onscreen is the
+    default, but the other options will give better performance on
+    slower platforms.
+    """
     
-    _tooltips = {
-        'zoom'       : 'Zoom level (min: 1, max: 10)',
-        'showCursor' : 'Show/hide a green cursor indicating '
-                       'the currently displayed location',
-        'zax'        : 'Image axis which is used as the \'depth\' axis'}
-    """Property descriptions to be used as help text."""
+    
+    softwareMode = props.Boolean(default=False)
+    """If ``True``, the :attr:`.Display.softwareMode` property for every
+    displayed image is set to ``True``.
+    """
 
     
-    _propHelp = _tooltips
+    resolutionLimit = props.Real(default=0, minval=0, maxval=5, clamped=True)
+    """The minimum resolution at which image types should be drawn."""
 
 
     def calcPixelDims(self):
@@ -280,10 +272,12 @@ class SliceCanvas(props.HasProperties):
         # stored in this dictionary
         self._glObjects = {}
 
-        # If two stage rendering is enabled, this
-        # attribute will contain a RenderTexture
-        # instance for each image in the image list
-        self._renderTextures = {}
+        # If render mode is offscren or prerender, these
+        # dictionaries will contain a RenderTexture or
+        # RenderTextureStack instance for each image in
+        # the image list
+        self._offscreenTextures = {}
+        self._prerenderTextures = {}
 
         # The zax property is the image axis which maps to the
         # 'depth' axis of this canvas. The _zAxisChanged method
@@ -307,13 +301,13 @@ class SliceCanvas(props.HasProperties):
                          self.name,
                          lambda *a: self._updateDisplayBounds())
 
-        # When the two stage rendering option changes,
-        # make sure that, if it has been enabled, a
-        # rendering texture exists for every image
-        # in the list
-        self.addListener('twoStageRender',
+        self.addListener('renderMode',
                          self.name,
-                         self._onTwoStageRenderChange)
+                         self._renderModeChange)
+
+        self.addListener('resolutionLimit',
+                         self.name,
+                         self._resolutionLimitChange) 
         
         # When the image list changes, refresh the
         # display, and update the display bounds
@@ -329,56 +323,141 @@ class SliceCanvas(props.HasProperties):
 
 
     def _initGL(self):
-        # Call the _imageListChanged method - it  will generate
-        # any necessary GL data for each of the images
+        """Call the _imageListChanged method - it  will generate
+        any necessary GL data for each of the images
+        """
         self._imageListChanged()
 
+        
+    def _updateRenderTextures(self):
+        """Called when the :attr:`renderMode` changes, when the image
+        list changes, or when the  GLObject representation of an image
+        changes.
 
-    def _onTwoStageRenderChange(
-            self,
-            value=None,
-            valid=None,
-            ctx=None,
-            name=None,
-            recreate=False):
-        """Called when the :attr:`twoStageRender` property changes.
-        """
-
-        needRefresh = False
-        if recreate or not self.twoStageRender:
+        If the :attr:`renderMode` property is ``onscreen``, this method does
+        nothing.
 
-            for image, texture in self._renderTextures.items():
-                self._renderTextures.pop(image)
-                texture.destroy()
-                needRefresh = True
+        Otherwise, creates/destroys :class:`.RenderTexture` or
+        :class:`.RenderTextureStack` instances for newly added/removed images.
+        """
 
-        if self.twoStageRender:
+        if self.renderMode == 'onscreen':
+            return
 
-            # If any images have been removed from the image
-            # list, destroy the associated render texture
-            for image, texture in self._renderTextures.items():
+        # If any images have been removed from the image
+        # list, destroy the associated render texture stack
+        if self.renderMode == 'offscreen':
+            for image, texture in self._offscreenTextures.items():
                 if image not in self.imageList:
-                    self._renderTextures.pop(image)
+                    self._offscreenTextures.pop(image)
                     texture.destroy()
-                    needRefresh = True
+            
+        elif self.renderMode == 'prerender':
+            for image, (texture, name) in self._prerenderTextures.items():
+                if image not in self.imageList:
+                    self._prerenderTextures.pop(image)
+                    glresources.delete(name)
 
-            # If any images have been added to the list,
-            # create a new render textures for them
-            for image in self.imageList:
+        # If any images have been added to the list,
+        # create a new render textures for them
+        for image in self.imageList:
 
-                if image in self._renderTextures:
+            if self.renderMode == 'offscreen':
+                if image in self._offscreenTextures:
                     continue
+                
+            elif self.renderMode == 'prerender':
+                if image in self._prerenderTextures:
+                    continue 
+
+            globj = self._glObjects.get(image, None)
+
+            if globj is None:
+                continue
 
-                display = self.displayCtx.getDisplayProperties(image)
-                rt      = fsltextures.ImageRenderTexture(
-                    image, display, self.xax, self.yax)
+            # For offscreen render mode, GLObjects are
+            # first rendered to an offscreen texture,
+            # and then that texture is rendered to the
+            # screen. The off-screen texture is managed
+            # by a RenderTexture object.
+            if self.renderMode == 'offscreen':
+                
+                name = '{}_{}_{}'.format(image.name, self.xax, self.yax)
+                rt   = textures.GLObjectRenderTexture(
+                    name,
+                    globj,
+                    self.xax,
+                    self.yax)
+
+                self._offscreenTextures[image] = rt
                 
-                self._renderTextures[image] = rt
+            # For prerender mode, slices of the
+            # GLObjects are pre-rendered on a
+            # stack of off-screen textures, which
+            # is managed by a RenderTextureStack
+            # object.
+            elif self.renderMode == 'prerender':
+                name = '{}_{}_zax{}'.format(
+                    id(image),
+                    textures.RenderTextureStack.__name__,
+                    self.zax)
+
+                if glresources.exists(name):
+                    rt = glresources.get(name)
+                    
+                else:
+                    rt = textures.RenderTextureStack(globj)
+                    rt.setAxes(self.xax, self.yax)
+                    glresources.set(name, rt)
+
+                self._prerenderTextures[image] = rt, name
+
+        self._refresh()
+
                 
-                needRefresh = True
+    def _renderModeChange(self, *a):
+        """Called when the :attr:`renderMode` property changes."""
 
-        if needRefresh:
+        log.debug('Render mode changed: {}'.format(self.renderMode))
+
+        # destroy any existing render textures
+        for image, texture in self._offscreenTextures.items():
+            self._offscreenTextures.pop(image)
+            texture.destroy()
+            
+        for image, (texture, name) in self._prerenderTextures.items():
+            self._prerenderTextures.pop(image)
+            glresources.delete(name)
+
+        # Onscreen rendering - each GLObject
+        # is rendered directly to the canvas
+        # displayed on the screen, so render
+        # textures are not needed.
+        if self.renderMode == 'onscreen':
             self._refresh()
+            return
+
+        # Off-screen or prerender rendering - update
+        # the render textures for every GLObject
+        self._updateRenderTextures()
+
+
+    def _resolutionLimitChange(self, *a):
+        """Called when the :attr:`resolutionLimit` property changes.
+
+        Updates the minimum resolution of all images in the image list.
+        """
+
+        for image in self.imageList:
+            
+            display = self.displayCtx.getDisplayProperties(image)
+            minres  = min(image.pixdim[:3])
+
+            if self.resolutionLimit > minres:
+                minres = self.resolutionLimit
+
+            if display.resolution < minres:
+                display.resolution = minres
 
 
     def _zAxisChanged(self, *a):
@@ -421,11 +500,12 @@ class SliceCanvas(props.HasProperties):
                         pos[self.yax],
                         pos[self.zax]]
 
-        # If two stage rendering is enabled, the
+        # If pre-rendering is enabled, the
         # render textures need to be updated, as
         # they are configured in terms of the
-        # display axes
-        self._onTwoStageRenderChange(recreate=True)
+        # display axes. Easiest way to do this
+        # is to destroy and re-create them
+        self._renderModeChange()
  
             
     def _imageListChanged(self, *a):
@@ -463,7 +543,8 @@ class SliceCanvas(props.HasProperties):
                             ctx=None,
                             name=None,
                             disp=display,
-                            img=image):
+                            img=image,
+                            updateRenderTextures=True):
 
                 log.debug('GLObject representation for {} '
                           'changed to {}'.format(disp.name,
@@ -477,24 +558,43 @@ class SliceCanvas(props.HasProperties):
                 globj = self._glObjects.get(img, None)
                 if globj is not None:
                     globj.destroy()
-                
+                    
+                    if updateRenderTextures:
+                        if self.renderMode == 'offscreen':
+                            tex = self._offscreenTextures.pop(img, None)
+                            if tex is not None:
+                                tex.destroy()
+                                
+                        elif self.renderMode == 'prerender':
+                            tex, name = self._prerenderTextures.pop(
+                                img, (None, None))
+                            if tex is not None:
+                                glresources.delete(name)
+
                 globj = globject.createGLObject(img, disp)
-                self._glObjects[img] = globj
 
                 if globj is not None:
-                    globj.init()
                     globj.setAxes(self.xax, self.yax)
 
+                self._glObjects[img] = globj
+
+                if updateRenderTextures:
+                    self._updateRenderTextures()
+
                 opts = disp.getDisplayOpts()
                 opts.addGlobalListener(self.name, self._refresh)
                 self._refresh()
                 
-            genGLObject()
+            genGLObject(updateRenderTextures=False)
+
+            # Bind Display.softwareMode to SliceCanvas.softwareMode
+            display.bindProps('softwareMode', self)
                 
             image  .addListener('data',          self.name, self._refresh)
             display.addListener('imageType',     self.name, genGLObject)
             display.addListener('enabled',       self.name, self._refresh)
             display.addListener('transform',     self.name, self._refresh)
+            display.addListener('softwareMode',  self.name, self._refresh)
             display.addListener('interpolation', self.name, self._refresh)
             display.addListener('alpha',         self.name, self._refresh)
             display.addListener('brightness',    self.name, self._refresh)
@@ -502,7 +602,8 @@ class SliceCanvas(props.HasProperties):
             display.addListener('resolution',    self.name, self._refresh)
             display.addListener('volume',        self.name, self._refresh)
 
-        self._onTwoStageRenderChange()
+        self._updateRenderTextures()
+        self._resolutionLimitChange()
         self._refresh()
 
 
@@ -667,13 +768,17 @@ class SliceCanvas(props.HasProperties):
         :arg zmin: Minimum z (depth) location
         :arg zmax: Maximum z location
         """
+
+        xax = self.xax
+        yax = self.yax
+        zax = self.zax
         
         if xmin is None: xmin = self.displayBounds.xlo
         if xmax is None: xmax = self.displayBounds.xhi
         if ymin is None: ymin = self.displayBounds.ylo
         if ymax is None: ymax = self.displayBounds.yhi
-        if zmin is None: zmin = self.displayCtx.bounds.getLo(self.zax)
-        if zmax is None: zmax = self.displayCtx.bounds.getHi(self.zax)
+        if zmin is None: zmin = self.displayCtx.bounds.getLo(zax)
+        if zmax is None: zmax = self.displayCtx.bounds.getHi(zax)
 
         # If there are no images to be displayed,
         # or no space to draw, do nothing
@@ -698,49 +803,23 @@ class SliceCanvas(props.HasProperties):
 
         log.debug('Setting canvas bounds (size {}, {}): '
                   'X {: 5.1f} - {: 5.1f},'
-                  'Y {: 5.1f} - {: 5.1f}'.format(
-                      width, height, xmin, xmax, ymin, ymax))
-
-        # set up 2D orthographic drawing
-        gl.glViewport(0, 0, width, height)
-        gl.glMatrixMode(gl.GL_PROJECTION)
-        gl.glLoadIdentity()
+                  'Y {: 5.1f} - {: 5.1f},'
+                  'Z {: 5.1f} - {: 5.1f}'.format(
+                      width, height, xmin, xmax, ymin, ymax, zmin, zmax))
 
         # Flip the viewport if necessary
         if self.invertX: xmin, xmax = xmax, xmin
         if self.invertY: ymin, ymax = ymax, ymin
-        
-        gl.glOrtho(xmin,        xmax,
-                   ymin,        ymax,
-                   zmin - 1000, zmax + 1000)
-        # I don't know why the above +/-1000 is necessary :(
-        # The '1000' is empirically arbitrary, but it seems
-        # that I need to extend the depth clipping range
-        # beyond the range of the data. This is despite the
-        # fact that below, I'm actually translating the
-        # displayed slice to Z=0! I don't understand OpenGL
-        # sometimes. Most of the time.
-
-        gl.glMatrixMode(gl.GL_MODELVIEW)
-        gl.glLoadIdentity()
-
-        # Rotate world space so the displayed slice
-        # is visible and correctly oriented
-        # TODO There's got to be a more generic way
-        # to perform this rotation. This will break
-        # if I add functionality allowing the user
-        # to specifty the x/y axes on initialisation.
-        if self.zax == 0:
-            gl.glRotatef(-90, 1, 0, 0)
-            gl.glRotatef(-90, 0, 0, 1)
-            
-        elif self.zax == 1:
-            gl.glRotatef(270, 1, 0, 0)
 
-        # move the currently displayed slice to screen Z coord 0
-        trans = [0, 0, 0]
-        trans[self.zax] = -self.pos.z
-        gl.glTranslatef(*trans)
+        lo = [None] * 3
+        hi = [None] * 3
+
+        lo[xax], hi[xax] = xmin, xmax
+        lo[yax], hi[yax] = ymin, ymax
+        lo[zax], hi[zax] = zmin, zmax
+
+        # set up 2D orthographic drawing
+        glroutines.show2D(xax, yax, width, height, lo, hi)
 
         
     def _drawCursor(self):
@@ -771,24 +850,26 @@ class SliceCanvas(props.HasProperties):
         yverts[:, 0] = [xmin, xmax]
         yverts[:, 1] = y 
         
-        self._annotations.line(xverts[0], xverts[1], colour=(0, 1, 0))
-        self._annotations.line(yverts[0], yverts[1], colour=(0, 1, 0))
+        self._annotations.line(xverts[0], xverts[1], colour=(0, 1, 0), width=1)
+        self._annotations.line(yverts[0], yverts[1], colour=(0, 1, 0), width=1)
 
 
-    def _drawRenderTextures(self):
-        """
+    def _drawOffscreenTextures(self):
+        """Draws all of the off-screen :class:`.ImageRenderTexture` instances to the
+        canvas.
+
+        This method is called by :meth:`_draw` if :attr:`twoStageRender` is
+        enabled.
         """
         log.debug('Combining off-screen image textures, and rendering '
                   'to canvas (size {})'.format(self._getSize()))
 
-        self._setViewport()
-
         for image in self.displayCtx.getOrderedImages():
-            renderTexture = self._renderTextures.get(image, None)
-            display       = self.displayCtx.getDisplayProperties(image)
-            lo, hi        = display.getDisplayBounds()
+            rt      = self._offscreenTextures.get(image, None)
+            display = self.displayCtx.getDisplayProperties(image)
+            lo, hi  = display.getDisplayBounds()
 
-            if renderTexture is None or not display.enabled:
+            if rt is None or not display.enabled:
                 continue
 
             xmin, xmax = lo[self.xax], hi[self.xax]
@@ -798,10 +879,10 @@ class SliceCanvas(props.HasProperties):
                       '{:0.3f}-{:0.3f}'.format(
                           image, xmin, xmax, ymin, ymax))
 
-            renderTexture.drawRender(
-                xmin, xmax, ymin, ymax, self.xax, self.yax) 
-
+            rt.drawOnBounds(
+                self.pos.z, xmin, xmax, ymin, ymax, self.xax, self.yax) 
 
+            
     def _draw(self, *a):
         """Draws the current scene to the canvas. """
         
@@ -812,9 +893,9 @@ class SliceCanvas(props.HasProperties):
         if not self._setGLContext():
             return
 
-        # If normal rendering, set the viewport to match
-        # the current display bounds and canvas size
-        if not self.twoStageRender:
+        # Set the viewport to match the current 
+        # display bounds and canvas size
+        if self.renderMode is not 'offscreen':
             self._setViewport()
 
         for image in self.displayCtx.getOrderedImages():
@@ -822,50 +903,72 @@ class SliceCanvas(props.HasProperties):
             display = self.displayCtx.getDisplayProperties(image)
             globj   = self._glObjects.get(image, None)
             
-            if (globj is None) or (not globj.ready()) or not display.enabled:
+            if (globj is None) or (not display.enabled):
                 continue
 
-            # Two-stage rendering - each image is
+            # On-screen rendering - the globject is
+            # rendered directly to the screen canvas
+            if self.renderMode == 'onscreen':
+                log.debug('Drawing {} slice for image {} '
+                          'directly to canvas'.format(
+                              self.zax, image.name))
+
+                globj.preDraw()
+                globj.draw(self.pos.z)
+                globj.postDraw() 
+
+            # Off-screen rendering - each image is
             # rendered to an off-screen texture -
             # these textures are combined below.
-            # Set up this texture as the rendering
-            # target
-            if self.twoStageRender:
-                renderTexture = self._renderTextures.get(image, None)
-                lo, hi        = display.getDisplayBounds()
+            # Set up the texture as the rendering
+            # target, and draw to it
+            elif self.renderMode == 'offscreen':
+                
+                rt     = self._offscreenTextures.get(image, None)
+                lo, hi = display.getDisplayBounds()
 
                 # Assume that all is well - the texture
                 # just has not yet been created
-                if renderTexture is None:
-                    return
+                if rt is None:
+                    continue
+                
+                log.debug('Drawing {} slice for image {} '
+                          'to off-screen texture'.format(
+                              self.zax, image.name))
 
-                renderTexture.bindAsRenderTarget()
+                rt.bindAsRenderTarget()
+                rt.setRenderViewport(self.xax, self.yax, lo, hi)
+                
+                gl.glClear(gl.GL_COLOR_BUFFER_BIT)
 
-                self._setViewport(
-                    xmin=lo[self.xax],
-                    xmax=hi[self.xax],
-                    ymin=lo[self.yax],
-                    ymax=hi[self.yax],
-                    size=renderTexture.getSize())
+                globj.preDraw()
+                globj.draw(self.pos.z)
+                globj.postDraw()
 
-                log.debug('Rendering image {} to offscreen '
-                          'texture {} (size {})'.format(
-                              image,
-                              renderTexture.texture,
-                              renderTexture.getSize())) 
-                
-            log.debug('Drawing {} slice for image {}'.format(
-                self.zax, image.name))
+            # Pre-rendering - a pre-generated 2D
+            # texture of the current z position
+            # is rendered to the screen canvas
+            elif self.renderMode == 'prerender':
                 
-            globj.preDraw()
-            globj.draw(self.pos.z)
-            globj.postDraw()
+                rt, name = self._prerenderTextures.get(image, (None, None))
 
-            if self.twoStageRender:
-                fsltextures.ImageRenderTexture.unbind()
+                if rt is None:
+                    continue
+
+                log.debug('Drawing {} slice for image {} '
+                          'from pre-rendered texture'.format(
+                              self.zax, image.name)) 
 
-        if self.twoStageRender:
-            self._drawRenderTextures()
+                rt.draw(self.pos.z)
+
+        # For off-screen rendering, all of the globjects
+        # were rendered to off-screen textures - here,
+        # those off-screen textures are all rendered on
+        # to the screen canvas.
+        if self.renderMode == 'offscreen':
+            textures.GLObjectRenderTexture.unbindAsRenderTarget()
+            self._setViewport()
+            self._drawOffscreenTextures() 
 
         if self.showCursor:
             self._drawCursor()
diff --git a/fsl/fslview/gl/textures/__init__.py b/fsl/fslview/gl/textures/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..a10bec9ac935815a9a1cb983b6339d76dcaebcd5
--- /dev/null
+++ b/fsl/fslview/gl/textures/__init__.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python
+#
+# textures.py - Management of OpenGL image textures.
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+"""This package is a container for a collection of classes which use OpenGL
+textures for various purposes. 
+
+
+The :mod:`.texture` sub-module contains the definition of the :class:`Texture`
+class, the base class for all texture types.
+"""
+
+
+# All *Texture classes are made available at the
+# textures package level due to these imports
+from texture            import Texture
+from texture            import Texture2D
+from imagetexture       import ImageTexture
+from colourmaptexture   import ColourMapTexture
+from selectiontexture   import SelectionTexture
+from rendertexture      import RenderTexture
+from rendertexture      import GLObjectRenderTexture
+from rendertexturestack import RenderTextureStack
diff --git a/fsl/fslview/gl/textures/colourmaptexture.py b/fsl/fslview/gl/textures/colourmaptexture.py
new file mode 100644
index 0000000000000000000000000000000000000000..ef150ab3499212f7023d613d8ae4e87d0bf66730
--- /dev/null
+++ b/fsl/fslview/gl/textures/colourmaptexture.py
@@ -0,0 +1,162 @@
+#!/usr/bin/env python
+#
+# colourmaptexture.py -
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+
+import logging
+
+
+import numpy     as np
+import OpenGL.GL as gl
+
+import texture
+
+
+log = logging.getLogger(__name__)
+
+
+class ColourMapTexture(texture.Texture):
+
+
+    def __init__(self, name, resolution=256):
+        
+        texture.Texture.__init__(self, name, 1)
+        
+        self.__resolution   = resolution
+        self.__cmap         = None
+        self.__invert       = False
+        self.__alpha        = None
+        self.__displayRange = None
+        self.__border       = None
+        self.__coordXform   = None
+
+
+    # CMAP can be either a function which transforms
+    # values to RGBA, or a N*4 numpy array containing
+    # RGBA values
+    def setColourMap(   self, cmap):   self.set(cmap=cmap)
+    def setAlpha(       self, alpha):  self.set(alpha=alpha)
+    def setInvert(      self, invert): self.set(invert=invert)
+    def setDisplayRange(self, drange): self.set(displayRange=drange)
+    def setBorder(      self, border): self.set(border=border)
+
+
+    def getCoordinateTransform(self):
+        return self.__coordXform
+
+    
+    def set(self, **kwargs):
+
+        
+        # None is a valid value for any attributes,
+        # so we are using 'self' to test whether
+        # or not an attribute value was passed in
+        cmap         = kwargs.get('cmap',         self)
+        invert       = kwargs.get('invert',       self)
+        alpha        = kwargs.get('alpha',        self)
+        displayRange = kwargs.get('displayRange', self)
+        border       = kwargs.get('border',       self)
+
+        if cmap         is not self: self.__cmap         = cmap
+        if invert       is not self: self.__invert       = invert
+        if alpha        is not self: self.__alpha        = alpha
+        if displayRange is not self: self.__displayRange = displayRange
+        if border       is not self: self.__border       = border
+
+        self.__refresh()
+
+    
+    def __refresh(self):
+
+        if self.__displayRange is None:
+            imin = 0.0
+            imax = 1.0
+        else:
+            imin = self.__displayRange[0]
+            imax = self.__displayRange[1]
+
+        if self.__cmap is None: cmap = np.zeros((4, 4), dtype=np.float32)
+        else:                   cmap = self.__cmap
+            
+        invert     = self.__invert
+        resolution = self.__resolution
+        alpha      = self.__alpha
+        border     = self.__border
+
+        # This transformation is used to transform input values
+        # from their native range to the range [0.0, 1.0], which
+        # is required for texture colour lookup. Values below
+        # or above the current display range will be mapped
+        # to texture coordinate values less than 0.0 or greater
+        # than 1.0 respectively.
+        if imax == imin: scale = 0.000000000001
+        else:            scale = imax - imin
+        
+        coordXform = np.identity(4, dtype=np.float32)
+        coordXform[0, 0] = 1.0 / scale
+        coordXform[3, 0] = -imin * coordXform[0, 0]
+
+        self.__coordXform = coordXform
+
+        # Assume that the provided cmap
+        # value contains rgb values
+        if isinstance(cmap, np.ndarray):
+            resolution = cmap.shape[0]
+            colourmap  = cmap
+            
+        # Create [self.colourResolution] 
+        # rgb values, spanning the entire 
+        # range of the image colour map            
+        else:
+            colourmap = cmap(np.linspace(0.0, 1.0, resolution))
+
+        # Apply global transparency
+        if alpha is not None:
+            colourmap[:, 3] = alpha
+
+        # invert if needed
+        if invert:
+            colourmap = colourmap[::-1, :]
+        
+        # The colour data is stored on
+        # the GPU as 8 bit rgba tuples
+        colourmap = np.floor(colourmap * 255)
+        colourmap = np.array(colourmap, dtype=np.uint8)
+        colourmap = colourmap.ravel(order='C')
+
+        # GL texture creation stuff
+        self.bindTexture()
+
+        if border is not None:
+            if alpha is not None:
+                border[3] = alpha
+                
+            gl.glTexParameterfv(gl.GL_TEXTURE_1D,
+                                gl.GL_TEXTURE_BORDER_COLOR,
+                                border)
+            gl.glTexParameteri( gl.GL_TEXTURE_1D,
+                                gl.GL_TEXTURE_WRAP_S,
+                                gl.GL_CLAMP_TO_BORDER) 
+        else:
+            gl.glTexParameteri(gl.GL_TEXTURE_1D,
+                               gl.GL_TEXTURE_WRAP_S,
+                               gl.GL_CLAMP_TO_EDGE)
+ 
+        gl.glTexParameteri(gl.GL_TEXTURE_1D,
+                           gl.GL_TEXTURE_MAG_FILTER,
+                           gl.GL_NEAREST)
+        gl.glTexParameteri(gl.GL_TEXTURE_1D,
+                           gl.GL_TEXTURE_MIN_FILTER,
+                           gl.GL_NEAREST)
+
+        gl.glTexImage1D(gl.GL_TEXTURE_1D,
+                        0,
+                        gl.GL_RGBA8,
+                        resolution,
+                        0,
+                        gl.GL_RGBA,
+                        gl.GL_UNSIGNED_BYTE,
+                        colourmap)
+        self.unbindTexture()
diff --git a/fsl/fslview/gl/textures.py b/fsl/fslview/gl/textures/imagetexture.py
similarity index 51%
rename from fsl/fslview/gl/textures.py
rename to fsl/fslview/gl/textures/imagetexture.py
index 17e0be28fb8660d44aff8e40920e455adbb42968..78d275bc8d6cb13a2071b377f8e6f52cd3063fc6 100644
--- a/fsl/fslview/gl/textures.py
+++ b/fsl/fslview/gl/textures/imagetexture.py
@@ -1,84 +1,26 @@
 #!/usr/bin/env python
 #
-# textures.py - Management of OpenGL image textures.
+# imagetexture.py -
 #
 # Author: Paul McCarthy <pauldmccarthy@gmail.com>
 #
-"""This module contains logic for creating OpenGL 3D textures, which contain
-3D image data, and which will potentially be shared between multiple GL
-canvases.
-
-The main interface to this module comprises two functions:
-
-  - :func:`getTexture`:    Return an :class:`ImageTexture` instance for a
-                           particular :class:`~fsl.data.image.Image` instance,
-                           creating one if it does not already exist.
-
-  - :func:`deleteTexture`: Cleans up the resources used by an
-                           :class:`ImageTexture` instance when it is no longer
-                           needed.
-"""
 
 import logging
 
-import OpenGL.GL                        as gl
-import OpenGL.raw.GL._types             as gltypes
-import OpenGL.GL.EXT.framebuffer_object as glfbo
 
-import numpy                            as np
+import numpy     as np
+import OpenGL.GL as gl
 
 import fsl.utils.transform     as transform
-import fsl.utils.typedict      as typedict
-import fsl.fslview.gl.globject as globject
+import fsl.fslview.gl.routines as glroutines
 
+import texture
 
 
 log = logging.getLogger(__name__)
 
 
-_allTextures = {}
-"""This dictionary contains all of the textures which currently exist. The
-key is the texture tag (see :func:`getTexture`), and the value is the
-corresponding :class:`ImageTexture` object.
-"""
-
-
-def getTexture(target, tag, *args, **kwargs):
-    """Retrieve a texture  object for the given target object (with
-    the given tag), creating one if it does not exist.
-
-    :arg target:     
-    :arg tag:    An application-unique string associated with the given image.
-                 Future requests for a texture with the same image and tag
-                 will return the same :class:`ImageTexture` instance.
-    """
-
-    textureMap = typedict.TypeDict({
-        'Image'     : ImageTexture,
-        'Selection' : SelectionTexture
-    })
-
-    tag        = '{}_{}'.format(id(target), tag)
-    textureObj = _allTextures.get(tag, None)
-
-    if textureObj is None:
-        textureObj = textureMap[type(target)](target, tag, *args, **kwargs)
-        _allTextures[tag] = textureObj
-
-    return textureObj
-
-
-def deleteTexture(texture):
-    """Releases the OpenGL memory associated with the given
-    :class:`ImageTexture` instance, and removes it from the
-    :attr:`_allTextures` dictionary.
-    """
-    
-    texture.destroy()
-    _allTextures.pop(texture.tag, None)
-
-
-class ImageTexture(object):
+class ImageTexture(texture.Texture):
     """This class contains the logic required to create and manage a 3D
     texture which represents a :class:`~fsl.data.image.Image` instance.
 
@@ -93,7 +35,7 @@ class ImageTexture(object):
     Once created, the following attributes are available on an
     :class:`ImageTexture` object:
 
-     - ``texture``:        The OpenGL texture identifier. 
+     - todo
 
      - ``voxValXform``:    An affine transformation matrix which encodes an
                            offset and scale, for transforming from the
@@ -102,17 +44,17 @@ class ImageTexture(object):
     """
     
     def __init__(self,
+                 name,                 
                  image,
-                 tag,
                  display=None,
                  nvals=1,
                  normalise=False,
                  prefilter=None):
         """Create an :class:`ImageTexture`.
-
-        :arg image:     The :class:`~fsl.data.image.Image` instance.
         
-        :arg tag:       The texture tag (see the :func:`getTexture` function).
+        :arg name:      A name for the texture.
+        
+        :arg image:     The :class:`~fsl.data.image.Image` instance.
         
         :arg display:   A :class:`~fsl.fslview.displaycontext.Display`
                         instance which defines how the image is to be
@@ -130,6 +72,8 @@ class ImageTexture(object):
                         GPU - see the :meth:`_prepareTextureData` method.
         """
 
+        texture.Texture.__init__(self, name, 3)
+
         try:
             if nvals > 1 and image.shape[3] != nvals:
                 raise RuntimeError()
@@ -140,7 +84,6 @@ class ImageTexture(object):
 
         self.image     = image
         self.display   = display
-        self.tag       = tag
         self.nvals     = nvals
         self.prefilter = prefilter
 
@@ -163,10 +106,6 @@ class ImageTexture(object):
         self.texDtype       = texDtype
         self.voxValXform    = voxValXform
         self.invVoxValXform = transform.invert(voxValXform)
-        self.texture        = gl.glGenTextures(1)
-        
-        log.debug('Created GL texture for {}: {}'.format(self.tag,
-                                                         self.texture)) 
 
         self._addListeners()
         self.refresh()
@@ -174,20 +113,13 @@ class ImageTexture(object):
 
     def destroy(self):
         """Deletes the texture identifier, and removes any property
-        listeners which were registered on the ``Image`` and ``Display``
+        listeners which were registered on the ``.Image`` and ``.Display``
         instances.
         """
 
-        if self.texture is None:
-            return
-
+        texture.Texture.destroy(self)
         self._removeListeners()
 
-        log.debug('Deleting GL texture for {}: {}'.format(self.tag,
-                                                          self.texture))
-        gl.glDeleteTextures(self.texture)
-        self.texture = None
-
         
     def setPrefilter(self, prefilter):
         """Updates the method used to pre-filter the data, and refreshes the
@@ -215,7 +147,6 @@ class ImageTexture(object):
         def refreshInterp(*a):
             self._updateInterpolationMethod()
             
-
         name = '{}_{}'.format(type(self).__name__, id(self))
 
         image.addListener('data', name, self.refresh)
@@ -286,6 +217,10 @@ class ImageTexture(object):
         """        
 
         data  = self.image.data
+
+        if self.prefilter is not None:
+            data = self.prefilter(data)
+        
         dtype = data.dtype
         dmin  = float(data.min())
         dmax  = float(data.max()) 
@@ -413,7 +348,7 @@ class ImageTexture(object):
         This process potentially involves:
         
           - Resampling to a different resolution (see the
-            :mod:`~fsl.fslview.gl.globject.subsample` function).
+            :mod:`~fsl.fslview.gl.routines.subsample` function).
         
           - Pre-filtering (see the ``prefilter`` parameter to
             :meth:`__init__`).
@@ -432,14 +367,14 @@ class ImageTexture(object):
             
         else:
             if self.nvals == 1 and self.image.is4DImage():
-                data = globject.subsample(self.image.data,
-                                          self.display.resolution,
-                                          self.image.pixdim, 
-                                          self.display.volume)
+                data = glroutines.subsample(self.image.data,
+                                            self.display.resolution,
+                                            self.image.pixdim, 
+                                            self.display.volume)[0]
             else:
-                data = globject.subsample(self.image.data,
-                                          self.display.resolution,
-                                          self.image.pixdim)
+                data = glroutines.subsample(self.image.data,
+                                            self.display.resolution,
+                                            self.image.pixdim)[0]
 
         if self.prefilter is not None:
             data = self.prefilter(data)
@@ -475,16 +410,12 @@ class ImageTexture(object):
         else:
             interp = gl.GL_LINEAR
 
-        gl.glBindTexture(gl.GL_TEXTURE_3D, self.texture)
+        self.bindTexture()
         
-        gl.glTexParameteri(gl.GL_TEXTURE_3D,
-                           gl.GL_TEXTURE_MAG_FILTER,
-                           interp)
-        gl.glTexParameteri(gl.GL_TEXTURE_3D,
-                           gl.GL_TEXTURE_MIN_FILTER,
-                           interp)
+        gl.glTexParameteri(gl.GL_TEXTURE_3D, gl.GL_TEXTURE_MAG_FILTER, interp)
+        gl.glTexParameteri(gl.GL_TEXTURE_3D, gl.GL_TEXTURE_MIN_FILTER, interp)
         
-        gl.glBindTexture(gl.GL_TEXTURE_3D, 0)
+        self.unbindTexture()
 
         
     def refresh(self, *a):
@@ -503,8 +434,8 @@ class ImageTexture(object):
 
         log.debug('Refreshing 3D texture (id {}) for '
                   '{} (data shape: {})'.format(
-                      self.texture,
-                      self.tag,
+                      self.getTextureHandle(),
+                      self.getTextureName(),
                       self.textureShape))
 
         # The image data is flattened, with fortran dimension
@@ -521,7 +452,7 @@ class ImageTexture(object):
         # (interpolation) method
         self._updateInterpolationMethod()
 
-        gl.glBindTexture(gl.GL_TEXTURE_3D, self.texture)
+        self.bindTexture()
 
         # Clamp texture borders to the edge
         # values - it is the responsibility
@@ -551,366 +482,4 @@ class ImageTexture(object):
                         self.texDtype,
                         data)
 
-        gl.glBindTexture(gl.GL_TEXTURE_3D, 0)
-
-
-class SelectionTexture(object):
-
-    def __init__(self, selection, tag):
-
-        self.tag       = tag
-        self.selection = selection
-        self.texture   = gl.glGenTextures(1)
-
-        log.debug('Created GL texture for {}: {}'.format(self.tag,
-                                                         self.texture))         
-
-        selection.addListener('selection', tag, self._selectionChanged)
-
-        self._init()
-        self.refresh()
-
-
-    def _init(self):
-        gl.glBindTexture(gl.GL_TEXTURE_3D, self.texture)
-        
-        gl.glTexParameteri(gl.GL_TEXTURE_3D,
-                           gl.GL_TEXTURE_MAG_FILTER,
-                           gl.GL_NEAREST)
-        gl.glTexParameteri(gl.GL_TEXTURE_3D,
-                           gl.GL_TEXTURE_MIN_FILTER,
-                           gl.GL_NEAREST)
-
-        gl.glTexParameterfv(gl.GL_TEXTURE_3D,
-                            gl.GL_TEXTURE_BORDER_COLOR,
-                            np.array([0, 0, 0, 0], dtype=np.float32))
-        
-        gl.glTexParameteri(gl.GL_TEXTURE_3D,
-                           gl.GL_TEXTURE_WRAP_S,
-                           gl.GL_CLAMP_TO_BORDER)
-        gl.glTexParameteri(gl.GL_TEXTURE_3D,
-                           gl.GL_TEXTURE_WRAP_T,
-                           gl.GL_CLAMP_TO_BORDER)
-        gl.glTexParameteri(gl.GL_TEXTURE_3D,
-                           gl.GL_TEXTURE_WRAP_R,
-                           gl.GL_CLAMP_TO_BORDER)
-
-        shape = self.selection.selection.shape
-        gl.glTexImage3D(gl.GL_TEXTURE_3D,
-                        0,
-                        gl.GL_ALPHA8,
-                        shape[0],
-                        shape[1],
-                        shape[2],
-                        0,
-                        gl.GL_ALPHA,
-                        gl.GL_UNSIGNED_BYTE,
-                        None)
-        
-        gl.glBindTexture(gl.GL_TEXTURE_3D, 0)
-
-        
-    def refresh(self, block=None, offset=None):
-        
-        if block is None or offset is None:
-            data   = self.selection.selection
-            offset = [0, 0, 0]
-        else:
-            data = block
-
-        data = data * 255
-
-        log.debug('Updating selection texture (offset {}, size {})'.format(
-            offset, data.shape))
-        
-        gl.glBindTexture(gl.GL_TEXTURE_3D, self.texture)
-        gl.glTexSubImage3D(gl.GL_TEXTURE_3D,
-                           0,
-                           offset[0],
-                           offset[1],
-                           offset[2],
-                           data.shape[0],
-                           data.shape[1],
-                           data.shape[2],
-                           gl.GL_ALPHA,
-                           gl.GL_UNSIGNED_BYTE,
-                           data.ravel('F'))
-        gl.glBindTexture(gl.GL_TEXTURE_3D, 0)
- 
-    
-    def _selectionChanged(self, *a):
-        
-        old, new, offset = self.selection.getLastChange()
-
-        if old is None or new is None:
-            data   = self.selection.selection
-            offset = [0, 0, 0]
-        else:
-            data = new
-
-        self.refresh(data, offset)
-
-        
-class RenderTexture(object):
-    """A 2D texture and frame buffer, intended to be used as a target for
-    off-screen rendering of a scene.
-    """
-    
-    def __init__(self, width, height, defaultInterp=gl.GL_NEAREST):
-        """
-
-        Note that a current target must have been set for the GL context
-        before a frameBuffer can be created ... in other words, call
-        ``context.SetCurrent`` before creating a ``RenderTexture``).
-        """
-
-        
-        self.texture     = gl   .glGenTextures(1)
-        self.frameBuffer = glfbo.glGenFramebuffersEXT(1)
-        
-        log.debug('Created GL texture {} and fbo: {}'.format(
-            self.texture, self.frameBuffer))
-
-        self.defaultInterp = defaultInterp
-        self.width         = width
-        self.height        = height
-        self.refresh()        
-
-        
-    def destroy(self):
-
-        log.debug('Deleting GL texture {} and fbo {}'.format(
-            self.texture, self.frameBuffer))
-        gl   .glDeleteTextures(                      self.texture)
-        glfbo.glDeleteFramebuffersEXT(gltypes.GLuint(self.frameBuffer))
-
-        
-    def setSize(self, width, height):
-        self.width  = width
-        self.height = height
-        self.refresh()
-
-
-    def getSize(self):
-        return self.width, self.height
-
-        
-    def bindAsRenderTarget(self):
-        glfbo.glBindFramebufferEXT(glfbo.GL_FRAMEBUFFER_EXT, self.frameBuffer) 
-
-
-    @classmethod
-    def unbind(cls):
-        glfbo.glBindFramebufferEXT(glfbo.GL_FRAMEBUFFER_EXT, 0) 
-
-        
-    def refresh(self, interp=None):
-        if interp is None:
-            interp = self.defaultInterp
-
-        log.debug('Configuring texture {}, fbo {}, size {}'.format(
-            self.texture, self.frameBuffer, (self.width, self.height)))
-
-        # Configure the texture
-        gl.glBindTexture(gl.GL_TEXTURE_2D, self.texture)
-
-        gl.glTexImage2D(gl.GL_TEXTURE_2D,
-                        0,
-                        gl.GL_RGBA8,
-                        self.width,
-                        self.height,
-                        0,
-                        gl.GL_RGBA,
-                        gl.GL_UNSIGNED_BYTE,
-                        None)
-
-        gl.glTexParameteri(gl.GL_TEXTURE_2D,
-                           gl.GL_TEXTURE_MIN_FILTER,
-                           interp)
-        gl.glTexParameteri(gl.GL_TEXTURE_2D,
-                           gl.GL_TEXTURE_MAG_FILTER,
-                           interp)
-
-        # And configure the frame buffer
-        glfbo.glBindFramebufferEXT(     glfbo.GL_FRAMEBUFFER_EXT,
-                                        self.frameBuffer)
-        glfbo.glFramebufferTexture2DEXT(glfbo.GL_FRAMEBUFFER_EXT,
-                                        glfbo.GL_COLOR_ATTACHMENT0_EXT,
-                                        gl   .GL_TEXTURE_2D,
-                                        self.texture,
-                                        0)
-            
-        if glfbo.glCheckFramebufferStatusEXT(glfbo.GL_FRAMEBUFFER_EXT) != \
-           glfbo.GL_FRAMEBUFFER_COMPLETE_EXT:
-            raise RuntimeError('An error has occurred while '
-                               'configuring the frame buffer')
-
-        glfbo.glBindFramebufferEXT(glfbo.GL_FRAMEBUFFER_EXT, 0)
-        gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
-        
-    
-    def drawRender(self, xmin, xmax, ymin, ymax, xax, yax):
-
-        # gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
-        # gl.glEnable(gl.GL_BLEND)
-        # gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
-
-        indices    = np.arange(6,     dtype=np.uint32)
-        vertices   = np.zeros((6, 3), dtype=np.float32)
-        texCoords  = np.zeros((6, 2), dtype=np.float32)
-
-        vertices[ 0, [xax, yax]] = [xmin, ymin]
-        texCoords[0, :]          = [0,    0]
-        vertices[ 1, [xax, yax]] = [xmin, ymax]
-        texCoords[1, :]          = [0,    1]
-        vertices[ 2, [xax, yax]] = [xmax, ymin]
-        texCoords[2, :]          = [1,    0]
-        vertices[ 3, [xax, yax]] = [xmax, ymin]
-        texCoords[3, :]          = [1,    0]
-        vertices[ 4, [xax, yax]] = [xmin, ymax]
-        texCoords[4, :]          = [0,    1]
-        vertices[ 5, [xax, yax]] = [xmax, ymax]
-        texCoords[5, :]          = [1,    1]
-
-        texCoords = texCoords.ravel('C')
-        vertices  = vertices .ravel('C')
-
-        gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
-        gl.glEnableClientState(gl.GL_TEXTURE_COORD_ARRAY)
-
-        gl.glActiveTexture(gl.GL_TEXTURE0)
-        gl.glEnable(gl.GL_TEXTURE_2D)
-
-        gl.glBindTexture(gl.GL_TEXTURE_2D, self.texture)
-
-        gl.glTexEnvf(gl.GL_TEXTURE_ENV,
-                     gl.GL_TEXTURE_ENV_MODE,
-                     gl.GL_REPLACE)
-
-        gl.glVertexPointer(  3, gl.GL_FLOAT, 0, vertices)
-        gl.glTexCoordPointer(2, gl.GL_FLOAT, 0, texCoords)
-
-        gl.glDrawElements(gl.GL_TRIANGLES, 6, gl.GL_UNSIGNED_INT, indices) 
-
-        gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
-        gl.glDisable(gl.GL_TEXTURE_2D)
-
-        gl.glDisableClientState(gl.GL_VERTEX_ARRAY)
-        gl.glDisableClientState(gl.GL_TEXTURE_COORD_ARRAY)        
-
-
-class ImageRenderTexture(RenderTexture):
-    """A :class:`RenderTexture` for off-screen volumetric rendering of a 3D
-    image.
-    """
-    
-    def __init__(self, image, display, xax, yax):
-        """
-
-        Note that a current target must have been set for the GL context
-        before a frameBuffer can be created ... in other words, call
-        ``context.SetCurrent`` before creating a ``RenderTexture``).
-        """
-
-        self.name    = '{}_{}'.format(type(self).__name__, id(self))
-        self.image   = image
-        self.display = display
-        self.xax     = xax
-        self.yax     = yax
-
-        self._addListeners()        
-        self._updateSize()
-        
-        RenderTexture.__init__(self, self.width, self.height)
-
-        
-    def destroy(self):
-        
-        RenderTexture.destroy(self)
-        self.display.removeListener('resolution',    self.name)
-        self.display.removeListener('interpolation', self.name)
-        self.display.removeListener('transform',     self.name)
-        
-
-    def _addListeners(self):
-
-        # TODO Could change the resolution when
-        #      the image type changes - vector
-        #      images will need a higher
-        #      resolution than voxel space
-
-        self.display.addListener('resolution',    self.name, self._updateSize)
-        self.display.addListener('interpolation', self.name, self.refresh)
-        self.display.addListener('transform',     self.name, self._updateSize)
-
-        
-    def _updateSize(self, *a):
-        image      = self.image
-        display    = self.display
-
-        resolution = display.resolution / np.array(image.pixdim)
-        resolution = np.round(resolution)
-
-        if resolution[0] < 1: resolution[0] = 1
-        if resolution[1] < 1: resolution[1] = 1
-        if resolution[2] < 1: resolution[2] = 1
-        
-        # If the display transformation is 'id' or
-        # 'pixdim', then the display coordinate system
-        # axes line up with the voxel coordinate system
-        # axes, so we can just match the voxel resolution        
-        if display.transform in ('id', 'pixdim'):
-            
-            width  = image.shape[self.xax] / resolution[self.xax]
-            height = image.shape[self.yax] / resolution[self.yax]
-
-        # However, if we're displaying in world coordinates,
-        # we cannot assume any correspondence between the
-        # voxel coordinate system and the display coordinate
-        # system. So we'll use a fixed size render texture
-        # instead.
-        elif display.transform == 'affine':
-            width  = 256 / resolution.min()
-            height = 256 / resolution.min()
-
-        # Limit the width/height to an arbitrary maximum
-        if width > 256 or height > 256:
-            oldWidth, oldHeight = width, height
-            ratio = min(width, height) / max(width, height)
-            
-            if width > height:
-                width  = 256
-                height = width * ratio
-            else:
-                height = 256
-                width  = height * ratio
-
-            log.debug('Limiting texture resolution to {}x{} '
-                      '(for image resolution {}x{})'.format(
-                          *map(int, (width, height, oldWidth, oldHeight))))
-
-        width  = int(round(width))
-        height = int(round(height))
-            
-        self.width  = width
-        self.height = height 
-
-    
-    def setSize(self, width, height):
-        raise NotImplementedError(
-            'Texture size cannot be set for {} instances'.format(
-                type(self).__name__))
-
-    
-    def setAxes(self, xax, yax):
-        self.xax = xax
-        self.yax = yax
-        self.refresh()
-
-        
-    def refresh(self, *a):
-
-        if self.display.interpolation == 'none': interp = gl.GL_NEAREST
-        else:                                    interp = gl.GL_LINEAR
-
-        RenderTexture.refresh(self, interp)
+        self.unbindTexture()
diff --git a/fsl/fslview/gl/textures/rendertexture.py b/fsl/fslview/gl/textures/rendertexture.py
new file mode 100644
index 0000000000000000000000000000000000000000..fe216b85f59d95cc84bb22e622e165d316c26710
--- /dev/null
+++ b/fsl/fslview/gl/textures/rendertexture.py
@@ -0,0 +1,172 @@
+#!/usr/bin/env python
+#
+# rendertexture.py -
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+
+import logging
+
+import OpenGL.GL                        as gl
+import OpenGL.raw.GL._types             as gltypes
+import OpenGL.GL.EXT.framebuffer_object as glfbo
+
+import                            texture
+import fsl.fslview.gl.routines as glroutines
+
+
+log = logging.getLogger(__name__)
+
+
+class RenderTexture(texture.Texture2D):
+    """A 2D texture and frame buffer, intended to be used as a target for
+    off-screen rendering of a scene.
+    """
+    
+    def __init__(self, name, interp=gl.GL_NEAREST):
+        """
+
+        Note that a current target must have been set for the GL context
+        before a frameBuffer can be created ... in other words, call
+        ``context.SetCurrent`` before creating a ``RenderTexture``).
+        """
+
+        texture.Texture2D.__init__(self, name, interp)
+        
+        self.__frameBuffer = glfbo.glGenFramebuffersEXT(1)
+        log.debug('Created fbo: {}'.format(self.__frameBuffer))
+
+        
+    def destroy(self):
+        texture.Texture.destroy(self)
+
+        log.debug('Deleting fbo {}'.format(self.__frameBuffer))
+        glfbo.glDeleteFramebuffersEXT(gltypes.GLuint(self.__frameBuffer))
+
+
+    def setData(self, data):
+        raise NotImplementedError('Texture data cannot be set for {} '
+                                  'instances'.format(type(self).__name__))
+
+
+    def bindAsRenderTarget(self):
+        glfbo.glBindFramebufferEXT(glfbo.GL_FRAMEBUFFER_EXT,
+                                   self.__frameBuffer) 
+
+
+    def setRenderViewport(self, xax, yax, lo, hi):
+
+        width, height = self.getSize()
+        
+        self.__oldSize    = gl.glGetIntegerv(gl.GL_VIEWPORT)
+        self.__oldProjMat = gl.glGetFloatv(  gl.GL_PROJECTION_MATRIX)
+        self.__oldMVMat   = gl.glGetFloatv(  gl.GL_MODELVIEW_MATRIX)
+
+        glroutines.show2D(xax, yax, width, height, lo, hi)
+            
+
+    def restoreViewport(self):
+
+        gl.glViewport(*self.__oldSize)
+        gl.glMatrixMode(gl.GL_PROJECTION)
+        gl.glLoadMatrixf(self.__oldProjMat)
+        gl.glMatrixMode(gl.GL_MODELVIEW)
+        gl.glLoadMatrixf(self.__oldMVMat)        
+        
+
+    @classmethod
+    def unbindAsRenderTarget(cls):
+        glfbo.glBindFramebufferEXT(glfbo.GL_FRAMEBUFFER_EXT, 0) 
+
+        
+    def refresh(self):
+        texture.Texture2D.refresh(self)
+
+        # Configure the frame buffer
+        self.bindTexture()
+        self.bindAsRenderTarget()
+        glfbo.glFramebufferTexture2DEXT(glfbo.GL_FRAMEBUFFER_EXT,
+                                        glfbo.GL_COLOR_ATTACHMENT0_EXT,
+                                        gl   .GL_TEXTURE_2D,
+                                        self.getTextureHandle(),
+                                        0)
+            
+        if glfbo.glCheckFramebufferStatusEXT(glfbo.GL_FRAMEBUFFER_EXT) != \
+           glfbo.GL_FRAMEBUFFER_COMPLETE_EXT:
+            raise RuntimeError('An error has occurred while '
+                               'configuring the frame buffer')
+
+        self.unbindAsRenderTarget()
+        self.unbindTexture()
+
+        
+class GLObjectRenderTexture(RenderTexture):
+    
+    def __init__(self, name, globj, xax, yax, maxResolution=1024):
+        """
+        """
+        
+        self.__globj         = globj
+        self.__xax           = xax
+        self.__yax           = yax
+        self.__maxResolution = maxResolution
+
+        RenderTexture.__init__(self, name)
+
+        name = '{}_{}'.format(self.getTextureName(), id(self))
+        globj.addUpdateListener(name, self.__updateSize)
+
+        self.__updateSize()        
+
+    
+    def setAxes(self, xax, yax):
+        self.__xax = xax
+        self.__yax = yax
+        self.__updateSize()
+
+        
+    def destroy(self):
+
+        name = '{}_{}'.format(self.getTextureName(), id(self))
+        self.__globj.removeUpdateListener(name) 
+        RenderTexture.destroy(self)
+
+    
+    def setSize(self, width, height):
+        raise NotImplementedError(
+            'Texture size cannot be set for {} instances'.format(
+                type(self).__name__))
+        
+        
+    def __updateSize(self, *a):
+        globj  = self.__globj
+        maxRes = self.__maxResolution
+
+        resolution = globj.getDataResolution(self.__xax, self.__yax)
+
+        width  = resolution[self.__xax]
+        height = resolution[self.__yax]
+
+        if width > maxRes or height > maxRes:
+            oldWidth, oldHeight = width, height
+            ratio = min(width, height) / max(width, height)
+            
+            if width > height:
+                width  = maxRes
+                height = width * ratio
+            else:
+                height = maxRes
+                width  = height * ratio
+
+            width  = int(round(width))
+            height = int(round(height))
+
+            log.debug('Limiting texture resolution to {}x{} '
+                      '(for {} resolution {}x{})'.format(
+                          width,
+                          height,
+                          type(globj).__name__,
+                          oldWidth,
+                          oldHeight))
+
+        RenderTexture.setSize(self, width, height) 
diff --git a/fsl/fslview/gl/textures/rendertexturestack.py b/fsl/fslview/gl/textures/rendertexturestack.py
new file mode 100644
index 0000000000000000000000000000000000000000..560bca0beef92a2086733ff33a14f223674ec109
--- /dev/null
+++ b/fsl/fslview/gl/textures/rendertexturestack.py
@@ -0,0 +1,241 @@
+#!/usr/bin/env python
+#
+# rendertexturelist.py -
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+
+import logging
+
+import              wx
+import numpy     as np
+import OpenGL.GL as gl
+
+import fsl.fslview.gl.routines as glroutines
+import fsl.utils.transform     as transform
+import                            rendertexture
+
+
+log = logging.getLogger(__name__)
+
+
+class RenderTextureStack(object):
+
+    def __init__(self, globj):
+
+
+        self.name = '{}_{}_{}'.format(
+            type(self).__name__,
+            type(globj).__name__, id(self))
+
+        self.__globj              = globj
+        self.__maxNumTextures     = 256
+        self.__maxWidth           = 1024
+        self.__maxHeight          = 1024
+        self.__defaultNumTextures = 64
+        self.__defaultWidth       = 256
+        self.__defaultHeight      = 256
+
+        self.__textureDirty       = []
+        self.__textures           = []
+
+        self.__lastDrawnTexture   = None
+        self.__updateQueue        = []
+
+        self.__globj.addUpdateListener(
+            '{}_{}'.format(type(self).__name__, id(self)),
+            self.__refreshAllTextures)
+
+        wx.GetApp().Bind(wx.EVT_IDLE, self.__textureUpdateLoop)
+
+            
+    def __refreshAllTextures(self, *a):
+
+        if self.__lastDrawnTexture is not None:
+            lastIdx = self.__lastDrawnTexture
+        else:
+            lastIdx = len(self.__textures) / 2
+            
+        aboveIdxs = range(lastIdx, len(self.__textures))
+        belowIdxs = range(lastIdx, 0, -1)
+
+        idxs = [0] * len(self.__textures)
+
+        for i in range(len(self.__textures)):
+            
+            if len(aboveIdxs) > 0 and len(belowIdxs) > 0:
+                if i % 2: idxs[i] = aboveIdxs.pop(0)
+                else:     idxs[i] = belowIdxs.pop(0)
+                
+            elif len(aboveIdxs) > 0: idxs[i] = aboveIdxs.pop(0)
+            else:                    idxs[i] = belowIdxs.pop(0) 
+
+        self.__textureDirty = [True] * len(self.__textures)
+        self.__updateQueue  = idxs
+
+
+    def __zposToIndex(self, zpos):
+        zmin  = self.__zmin
+        zmax  = self.__zmax
+        ntexs = len(self.__textures)
+        index = ntexs * (zpos - zmin) / (zmax - zmin)
+
+        limit = len(self.__textures) - 1
+
+        if index > limit and index <= limit + 1:
+            index = limit
+
+        return int(index)
+
+    
+    def __indexToZpos(self, index):
+        zmin  = self.__zmin
+        zmax  = self.__zmax
+        ntexs = len(self.__textures)
+        return index * (zmax - zmin) / ntexs + zmin
+
+
+    def __textureUpdateLoop(self, ev):
+        ev.Skip()
+
+        if len(self.__updateQueue) == 0 or len(self.__textures) == 0:
+            return
+
+        idx = self.__updateQueue.pop(0)
+
+        if not self.__textureDirty[idx]:
+            return
+
+        tex = self.__textures[idx]
+        
+        log.debug('Refreshing texture slice {} (zax {})'.format(
+            idx, self.__zax))
+        
+        self.__refreshTexture(tex, idx)
+
+        if len(self.__updateQueue) > 0:
+            ev.RequestMore()
+
+            
+    def getGLObject(self):
+        return self.__globj
+
+    
+    def setAxes(self, xax, yax):
+
+        zax        = 3 - xax - yax
+        self.__xax = xax
+        self.__yax = yax
+        self.__zax = zax
+
+        lo, hi = self.__globj.getDisplayBounds()
+        res    = self.__globj.getDataResolution(xax, yax)
+
+        if res is not None: numTextures = res[zax]
+        else:               numTextures = self.__defaultNumTextures
+
+        if numTextures > self.__maxNumTextures:
+            numTextures = self.__maxNumTextures
+
+        self.__zmin = lo[zax]
+        self.__zmax = hi[zax]
+
+        self.__destroyTextures()
+        
+        for i in range(numTextures):
+            self.__textures.append(
+                rendertexture.RenderTexture('{}_{}'.format(self.name, i)))
+
+        self.__textureDirty = [True] * numTextures
+        self.__refreshAllTextures()
+
+        
+    def __destroyTextures(self):
+        texes = self.__textures
+        self.__textures = []
+        for tex in texes:
+            wx.CallLater(50, tex.destroy)
+        
+    
+    def destroy(self):
+        self.__destroyTextures()
+
+
+    def __refreshTexture(self, tex, idx):
+
+        zpos = self.__indexToZpos(idx)
+        xax  = self.__xax
+        yax  = self.__yax
+
+        lo, hi = self.__globj.getDisplayBounds()
+        res    = self.__globj.getDataResolution(xax, yax)
+
+        if res is not None:
+            width  = res[xax]
+            height = res[yax]
+        else:
+            width  = self.__defaultWidth
+            height = self.__defaultHeight
+
+        if width  > self.__maxWidth:  width  = self.__maxWidth
+        if height > self.__maxHeight: height = self.__maxHeight
+
+        log.debug('Refreshing render texture for slice {} (zpos {}, '
+                  'zax {}): {} x {}'.format(idx, zpos, self.__zax,
+                                            width, height))
+
+        tex.setSize(width, height)
+
+        oldSize       = gl.glGetIntegerv(gl.GL_VIEWPORT)
+        oldProjMat    = gl.glGetFloatv(  gl.GL_PROJECTION_MATRIX)
+        oldMVMat      = gl.glGetFloatv(  gl.GL_MODELVIEW_MATRIX)
+
+        glroutines.show2D(xax, yax, width, height, lo, hi)
+
+        tex.bindAsRenderTarget()
+        gl.glClear(gl.GL_COLOR_BUFFER_BIT)
+        self.__globj.preDraw()
+        self.__globj.draw(zpos)
+        self.__globj.postDraw()
+        tex.unbindAsRenderTarget()
+        
+        gl.glViewport(*oldSize)
+        gl.glMatrixMode(gl.GL_PROJECTION)
+        gl.glLoadMatrixf(oldProjMat)
+        gl.glMatrixMode(gl.GL_MODELVIEW)
+        gl.glLoadMatrixf(oldMVMat)
+
+        self.__textureDirty[idx] = False
+
+    
+    def draw(self, zpos, xform=None):
+
+        xax     = self.__xax
+        yax     = self.__yax
+        zax     = self.__zax
+
+        texIdx                  = self.__zposToIndex(zpos)
+        self.__lastDrawnTexture = texIdx
+
+        if texIdx < 0 or texIdx >= len(self.__textures):
+            return
+
+        lo, hi  = self.__globj.getDisplayBounds()
+        texture = self.__textures[texIdx]
+
+        if self.__textureDirty[texIdx]:
+            self.__refreshTexture(texture, texIdx)
+
+        vertices = np.zeros((6, 3), dtype=np.float32)
+        vertices[:, zax] = zpos
+        vertices[0, [xax, yax]] = lo[xax], lo[yax]
+        vertices[1, [xax, yax]] = lo[xax], hi[yax]
+        vertices[2, [xax, yax]] = hi[xax], lo[yax]
+        vertices[3, [xax, yax]] = hi[xax], lo[yax]
+        vertices[4, [xax, yax]] = lo[xax], hi[yax]
+        vertices[5, [xax, yax]] = hi[xax], hi[yax]
+
+        if xform is not None:
+            vertices = transform.transform(vertices, xform=xform)
+
+        texture.draw(vertices)
diff --git a/fsl/fslview/gl/textures/selectiontexture.py b/fsl/fslview/gl/textures/selectiontexture.py
new file mode 100644
index 0000000000000000000000000000000000000000..8911949c4a26da38780f198d97d88023c28f315c
--- /dev/null
+++ b/fsl/fslview/gl/textures/selectiontexture.py
@@ -0,0 +1,111 @@
+#!/usr/bin/env python
+#
+# selectiontexture.py - see fsl.fslview.editor.selection.Selection
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+
+import logging
+
+import numpy     as np
+import OpenGL.GL as gl
+
+import texture
+
+
+log = logging.getLogger(__name__)
+
+
+class SelectionTexture(texture.Texture):
+
+    def __init__(self, name, selection):
+
+        texture.Texture.__init__(self, name, 3)
+
+        self.selection = selection
+
+        selection.addListener('selection', name, self._selectionChanged)
+
+        self._init()
+        self.refresh()
+
+
+    def _init(self):
+
+        self.bindTexture()
+        
+        gl.glTexParameteri(gl.GL_TEXTURE_3D,
+                           gl.GL_TEXTURE_MAG_FILTER,
+                           gl.GL_NEAREST)
+        gl.glTexParameteri(gl.GL_TEXTURE_3D,
+                           gl.GL_TEXTURE_MIN_FILTER,
+                           gl.GL_NEAREST)
+
+        gl.glTexParameterfv(gl.GL_TEXTURE_3D,
+                            gl.GL_TEXTURE_BORDER_COLOR,
+                            np.array([0, 0, 0, 0], dtype=np.float32))
+        
+        gl.glTexParameteri(gl.GL_TEXTURE_3D,
+                           gl.GL_TEXTURE_WRAP_S,
+                           gl.GL_CLAMP_TO_BORDER)
+        gl.glTexParameteri(gl.GL_TEXTURE_3D,
+                           gl.GL_TEXTURE_WRAP_T,
+                           gl.GL_CLAMP_TO_BORDER)
+        gl.glTexParameteri(gl.GL_TEXTURE_3D,
+                           gl.GL_TEXTURE_WRAP_R,
+                           gl.GL_CLAMP_TO_BORDER)
+
+        shape = self.selection.selection.shape
+        gl.glTexImage3D(gl.GL_TEXTURE_3D,
+                        0,
+                        gl.GL_ALPHA8,
+                        shape[0],
+                        shape[1],
+                        shape[2],
+                        0,
+                        gl.GL_ALPHA,
+                        gl.GL_UNSIGNED_BYTE,
+                        None)
+        
+        self.unbindTexture()
+
+        
+    def refresh(self, block=None, offset=None):
+        
+        if block is None or offset is None:
+            data   = self.selection.selection
+            offset = [0, 0, 0]
+        else:
+            data = block
+
+        data = data * 255
+
+        log.debug('Updating selection texture (offset {}, size {})'.format(
+            offset, data.shape))
+        
+        self.bindTexture()
+        gl.glTexSubImage3D(gl.GL_TEXTURE_3D,
+                           0,
+                           offset[0],
+                           offset[1],
+                           offset[2],
+                           data.shape[0],
+                           data.shape[1],
+                           data.shape[2],
+                           gl.GL_ALPHA,
+                           gl.GL_UNSIGNED_BYTE,
+                           data.ravel('F'))
+        self.unbindTexture()
+ 
+    
+    def _selectionChanged(self, *a):
+        
+        old, new, offset = self.selection.getLastChange()
+
+        if old is None or new is None:
+            data   = self.selection.selection
+            offset = [0, 0, 0]
+        else:
+            data = new
+
+        self.refresh(data, offset)
diff --git a/fsl/fslview/gl/textures/texture.py b/fsl/fslview/gl/textures/texture.py
new file mode 100644
index 0000000000000000000000000000000000000000..0fac58404796aba126c27375f311d5c2b263c9dd
--- /dev/null
+++ b/fsl/fslview/gl/textures/texture.py
@@ -0,0 +1,266 @@
+#!/usr/bin/env python
+#
+# texture.py -
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+
+import logging
+
+import numpy     as np
+import OpenGL.GL as gl
+
+
+log = logging.getLogger(__name__)
+
+
+class Texture(object):
+    """All subclasses must accept a ``name`` as the first parameter to their
+    ``__init__`` method, and must pass said ``name`` through to this
+    ``__init__`` method.
+    """
+
+    def __init__(self, name, ndims):
+
+        self.__texture     = gl.glGenTextures(1)
+        self.__name        = name
+        self.__ndims       = ndims
+        
+        self.__textureUnit = None
+
+        if   ndims == 1: self.__ttype = gl.GL_TEXTURE_1D
+        elif ndims == 2: self.__ttype = gl.GL_TEXTURE_2D
+        elif ndims == 3: self.__ttype = gl.GL_TEXTURE_3D
+        
+        else:            raise ValueError('Invalid number of dimensions')
+
+        log.debug('Created {} ({}) for {}: {}'.format(type(self).__name__,
+                                                      id(self),
+                                                      self.__name,
+                                                      self.__texture))
+
+    def getTextureName(self):
+        return self.__name
+
+        
+    def getTextureHandle(self):
+        return self.__texture
+
+
+    def destroy(self):
+
+        log.debug('Deleting {} ({}) for {}: {}'.format(type(self).__name__,
+                                                       id(self),
+                                                       self.__name,
+                                                       self.__texture))
+ 
+        gl.glDeleteTextures(self.__texture)
+        self.__texture = None
+
+
+    def bindTexture(self, textureUnit=None):
+
+        if textureUnit is not None:
+            gl.glActiveTexture(textureUnit)
+            gl.glEnable(self.__ttype)
+
+        gl.glBindTexture(self.__ttype, self.__texture)
+
+        self.__textureUnit = textureUnit
+
+
+    def unbindTexture(self):
+
+        if self.__textureUnit is not None:
+            gl.glActiveTexture(self.__textureUnit)
+            gl.glDisable(self.__ttype)
+            
+        gl.glBindTexture(self.__ttype, 0)
+
+        self.__textureUnit = None
+
+
+class Texture2D(Texture):
+
+    def __init__(self, name, interp=gl.GL_NEAREST):
+        Texture.__init__(self, name, 2)
+
+        self.__data      = None
+        self.__width     = None
+        self.__height    = None
+        self.__oldWidth  = None
+        self.__oldHeight = None 
+        self.__interp    = interp
+
+        
+    def setInterpolation(self, interp):
+        self.__interp = interp
+        self.refresh()
+
+
+    def setSize(self, width, height):
+        """
+        Sets the width/height for this texture.
+
+        This method also clears the data for this texture, if it has been
+        previously set via the :meth:`setData` method.
+        """
+
+        self.__setSize(width, height)
+        self.__data = None
+        
+        self.refresh()
+        
+
+    def __setSize(self, width, height):
+        """Sets the width/height attributes for this texture, and saves a
+        reference to the old width/height - see comments in the refresh
+        method.
+        """
+        self.__oldWidth  = self.__width
+        self.__oldHeight = self.__height
+        self.__width     = width
+        self.__height    = height        
+
+
+    def getSize(self):
+        """
+        """
+        return self.__width, self.__height
+
+
+    def setData(self, data):
+        """
+        Sets the data for this texture - the width and height are determined
+        from data shape (which is assumed to be 4*width*height).
+        """
+
+        self.__setSize(data.shape[1], data.shape[2])
+        self.__data = data
+
+        self.refresh()
+
+        
+    def refresh(self):
+
+        if self.__width is None or self.__height is None:
+            return
+
+        self.bindTexture()
+        gl.glPixelStorei(gl.GL_PACK_ALIGNMENT,   1)
+        gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1)
+
+        gl.glTexParameteri(gl.GL_TEXTURE_2D,
+                           gl.GL_TEXTURE_MAG_FILTER,
+                           self.__interp)
+        gl.glTexParameteri(gl.GL_TEXTURE_2D,
+                           gl.GL_TEXTURE_MIN_FILTER,
+                           self.__interp)
+        gl.glTexParameteri(gl.GL_TEXTURE_2D,
+                           gl.GL_TEXTURE_WRAP_S,
+                           gl.GL_CLAMP_TO_BORDER)
+        gl.glTexParameteri(gl.GL_TEXTURE_2D,
+                           gl.GL_TEXTURE_WRAP_T,
+                           gl.GL_CLAMP_TO_BORDER)
+
+        data = self.__data
+
+        if data is not None:
+            print data.shape, data.dtype
+            data = data.ravel('F')
+
+        log.debug('Configuring {} ({}) with size {}x{}'.format(
+            type(self).__name__,
+            self.getTextureHandle(),
+            self.__width,
+            self.__height))
+
+        # If the width and height have not changed,
+        # then we don't need to re-define the texture.
+        if self.__width  == self.__oldWidth  and \
+           self.__height == self.__oldHeight:
+
+            # But we can use glTexSubImage2D 
+            # if we have data to upload
+            if data is not None:
+                gl.glTexSubImage2D(gl.GL_TEXTURE_2D,
+                                   0, 
+                                   0,
+                                   0,
+                                   self.__width,
+                                   self.__height,
+                                   gl.GL_RGBA,
+                                   gl.GL_UNSIGNED_BYTE,
+                                   data)
+                
+        # If the width and/or height have
+        # changed, we need to re-define
+        # the texture properties
+        else:
+            gl.glTexImage2D(gl.GL_TEXTURE_2D,
+                            0,
+                            gl.GL_RGBA8,
+                            self.__width,
+                            self.__height,
+                            0,
+                            gl.GL_RGBA,
+                            gl.GL_UNSIGNED_BYTE,
+                            data)
+        self.unbindTexture()
+
+        
+    def draw(self, vertices):
+        
+        if vertices.shape != (6, 3):
+            raise ValueError('Six vertices must be provided')
+
+        vertices  = np.array(vertices, dtype=np.float32)
+        texCoords = np.zeros((6, 2),   dtype=np.float32)
+        indices   = np.arange(6,       dtype=np.uint32)
+
+        texCoords[0, :] = [0, 0]
+        texCoords[1, :] = [0, 1]
+        texCoords[2, :] = [1, 0]
+        texCoords[3, :] = [1, 0]
+        texCoords[4, :] = [0, 1]
+        texCoords[5, :] = [1, 1]
+
+        vertices  = vertices .ravel('C')
+        texCoords = texCoords.ravel('C')
+
+        gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
+
+        self.bindTexture(gl.GL_TEXTURE0)
+
+        gl.glClientActiveTexture(gl.GL_TEXTURE0)
+        gl.glEnableClientState(gl.GL_TEXTURE_COORD_ARRAY)
+
+        gl.glTexEnvf(gl.GL_TEXTURE_ENV,
+                     gl.GL_TEXTURE_ENV_MODE,
+                     gl.GL_REPLACE)
+
+        gl.glVertexPointer(  3, gl.GL_FLOAT, 0, vertices)
+        gl.glTexCoordPointer(2, gl.GL_FLOAT, 0, texCoords)
+
+        gl.glDrawElements(gl.GL_TRIANGLES, 6, gl.GL_UNSIGNED_INT, indices) 
+
+        self.unbindTexture()
+
+        gl.glDisableClientState(gl.GL_VERTEX_ARRAY)
+        gl.glDisableClientState(gl.GL_TEXTURE_COORD_ARRAY) 
+ 
+        
+    def drawOnBounds(self, zpos, xmin, xmax, ymin, ymax, xax, yax):
+
+        zax              = 3 - xax - yax
+        vertices         = np.zeros((6, 3), dtype=np.float32)
+        vertices[:, zax] = zpos
+
+        vertices[ 0, [xax, yax]] = [xmin, ymin]
+        vertices[ 1, [xax, yax]] = [xmin, ymax]
+        vertices[ 2, [xax, yax]] = [xmax, ymin]
+        vertices[ 3, [xax, yax]] = [xmax, ymin]
+        vertices[ 4, [xax, yax]] = [xmin, ymax]
+        vertices[ 5, [xax, yax]] = [xmax, ymax]
+
+        self.draw(vertices)
diff --git a/fsl/fslview/gl/wxgllightboxcanvas.py b/fsl/fslview/gl/wxgllightboxcanvas.py
index 2ea95cf5b980f8de77be2d0e2cadbb499db0fb87..06fedc2fa4a948422d8c6dac808720818568d6c7 100644
--- a/fsl/fslview/gl/wxgllightboxcanvas.py
+++ b/fsl/fslview/gl/wxgllightboxcanvas.py
@@ -64,6 +64,7 @@ class WXGLLightBoxCanvas(lightboxcanvas.LightBoxCanvas,
             self.removeListener('showGridLines',  self.name)
             self.removeListener('highlightSlice', self.name)
             self.removeListener('topRow',         self.name)
+            self.removeListener('renderMode',     self.name)
 
             self.imageList .removeListener('images',     self.name)
             self.displayCtx.removeListener('bounds',     self.name)
@@ -77,6 +78,7 @@ class WXGLLightBoxCanvas(lightboxcanvas.LightBoxCanvas,
                 disp .removeListener('imageType',     self.name)
                 disp .removeListener('enabled',       self.name)
                 disp .removeListener('transform',     self.name)
+                disp .removeListener('softwareMode',  self.name)
                 disp .removeListener('interpolation', self.name)
                 disp .removeListener('alpha',         self.name)
                 disp .removeListener('brightness',    self.name)
diff --git a/fsl/fslview/gl/wxglslicecanvas.py b/fsl/fslview/gl/wxglslicecanvas.py
index 4a0bd189eea5e7170b3af4bfa0e1bea156ba0bd1..69b192001b942c0830a9fd4fee80e32302a73099 100644
--- a/fsl/fslview/gl/wxglslicecanvas.py
+++ b/fsl/fslview/gl/wxglslicecanvas.py
@@ -50,14 +50,14 @@ class WXGLSliceCanvas(slicecanvas.SliceCanvas,
             if ev.GetEventObject() is not self:
                 return
 
-            self.removeListener('zax',            self.name)
-            self.removeListener('pos',            self.name)
-            self.removeListener('displayBounds',  self.name)
-            self.removeListener('showCursor',     self.name)
-            self.removeListener('invertX',        self.name)
-            self.removeListener('invertY',        self.name)
-            self.removeListener('zoom',           self.name)
-            self.removeListener('twoStageRender', self.name)
+            self.removeListener('zax',           self.name)
+            self.removeListener('pos',           self.name)
+            self.removeListener('displayBounds', self.name)
+            self.removeListener('showCursor',    self.name)
+            self.removeListener('invertX',       self.name)
+            self.removeListener('invertY',       self.name)
+            self.removeListener('zoom',          self.name)
+            self.removeListener('renderMode',    self.name)
             
             self.imageList .removeListener('images',     self.name)
             self.displayCtx.removeListener('bounds',     self.name)
@@ -69,6 +69,7 @@ class WXGLSliceCanvas(slicecanvas.SliceCanvas,
                 disp .removeListener('imageType',     self.name)
                 disp .removeListener('enabled',       self.name)
                 disp .removeListener('transform',     self.name)
+                disp .removeListener('softwareMode',  self.name)
                 disp .removeListener('interpolation', self.name)
                 disp .removeListener('alpha',         self.name)
                 disp .removeListener('brightness',    self.name)
diff --git a/fsl/fslview/layouts.py b/fsl/fslview/layouts.py
index 21ac7637087c6de4fddb791c9487536350f24617..c7f6e5f09405a9d19b3c082b63d58b4d0166d14c 100644
--- a/fsl/fslview/layouts.py
+++ b/fsl/fslview/layouts.py
@@ -30,6 +30,7 @@ from fsl.fslview.displaycontext               import Display
 from fsl.fslview.displaycontext.volumeopts    import VolumeOpts
 from fsl.fslview.displaycontext.maskopts      import MaskOpts
 from fsl.fslview.displaycontext.vectoropts    import VectorOpts
+from fsl.fslview.displaycontext.vectoropts    import LineVectorOpts
 
 from fsl.fslview.displaycontext.sceneopts     import SceneOpts
 from fsl.fslview.displaycontext.orthoopts     import OrthoOpts
@@ -37,10 +38,9 @@ from fsl.fslview.displaycontext.lightboxopts  import LightBoxOpts
 
 
 def widget(labelCls, name, *args, **kwargs):
-    return props.Widget(name,
-                        label=strings.properties[labelCls, name],
-                        *args,
-                        **kwargs)
+
+    label = strings.properties.get((labelCls, name), name)
+    return props.Widget(name, label=label, *args, **kwargs)
 
 
 ########################################
@@ -99,7 +99,7 @@ CanvasPanelLayout = props.VGroup((
 
 SceneOptsLayout = props.VGroup((
     widget(SceneOpts, 'showCursor'),
-    widget(SceneOpts, 'twoStageRender'),
+    widget(SceneOpts, 'performance', spin=False, showLimits=False),
     widget(SceneOpts, 'showColourBar'),
     widget(SceneOpts, 'colourBarLabelSide'),
     widget(SceneOpts, 'colourBarLocation')))
@@ -162,7 +162,8 @@ MaskOptsToolBarLayout = [
 
 
 VectorOptsToolBarLayout = [
-    widget(VectorOpts, 'displayMode'),
+    widget(VectorOpts, 'modulate'),
+    widget(VectorOpts, 'modThreshold', showLimits=False, spin=False),
     actions.ActionButton(ImageDisplayToolBar, 'more')] 
 
 
@@ -193,7 +194,6 @@ MaskOptsLayout = props.VGroup(
 
 
 VectorOptsLayout = props.VGroup((
-    widget(VectorOpts, 'displayMode'),
     props.HGroup((
         widget(VectorOpts, 'xColour'),
         widget(VectorOpts, 'yColour'),
@@ -205,7 +205,23 @@ VectorOptsLayout = props.VGroup((
         widget(VectorOpts, 'suppressZ')),
         vertLabels=True),
     widget(VectorOpts, 'modulate'),
-    widget(VectorOpts, 'modThreshold', showLimits=False)))
+    widget(VectorOpts, 'modThreshold', showLimits=False, spin=False)))
+
+LineVectorOptsLayout = props.VGroup((
+    props.HGroup((
+        widget(LineVectorOpts, 'xColour'),
+        widget(LineVectorOpts, 'yColour'),
+        widget(LineVectorOpts, 'zColour')),
+        vertLabels=True),
+    props.HGroup((
+        widget(LineVectorOpts, 'suppressX'),
+        widget(LineVectorOpts, 'suppressY'),
+        widget(LineVectorOpts, 'suppressZ')),
+        vertLabels=True),
+    widget(LineVectorOpts, 'directed'),
+    widget(LineVectorOpts, 'lineWidth', showLimits=False),
+    widget(LineVectorOpts, 'modulate'),
+    widget(LineVectorOpts, 'modThreshold', showLimits=False, spin=False)))
 
 
 ##########################
@@ -232,15 +248,16 @@ layouts = td.TypeDict({
 
     'SceneOpts' : SceneOptsLayout,
 
-    ('ImageDisplayToolBar', 'Display')    : DisplayToolBarLayout,
-    ('ImageDisplayToolBar', 'VolumeOpts') : VolumeOptsToolBarLayout,
-    ('ImageDisplayToolBar', 'MaskOpts')   : MaskOptsToolBarLayout,
-    ('ImageDisplayToolBar', 'VectorOpts') : VectorOptsToolBarLayout,
+    ('ImageDisplayToolBar', 'Display')        : DisplayToolBarLayout,
+    ('ImageDisplayToolBar', 'VolumeOpts')     : VolumeOptsToolBarLayout,
+    ('ImageDisplayToolBar', 'MaskOpts')       : MaskOptsToolBarLayout,
+    ('ImageDisplayToolBar', 'VectorOpts')     : VectorOptsToolBarLayout,
 
-    ('ImageDisplayPanel',   'Display')    : DisplayLayout,
-    ('ImageDisplayPanel',   'VolumeOpts') : VolumeOptsLayout,
-    ('ImageDisplayPanel',   'MaskOpts')   : MaskOptsLayout,
-    ('ImageDisplayPanel',   'VectorOpts') : VectorOptsLayout, 
+    ('ImageDisplayPanel',   'Display')        : DisplayLayout,
+    ('ImageDisplayPanel',   'VolumeOpts')     : VolumeOptsLayout,
+    ('ImageDisplayPanel',   'MaskOpts')       : MaskOptsLayout,
+    ('ImageDisplayPanel',   'VectorOpts')     : VectorOptsLayout,
+    ('ImageDisplayPanel',   'LineVectorOpts') : LineVectorOptsLayout, 
 
     'OrthoToolBar'    : OrthoToolBarLayout,
     'LightBoxToolBar' : LightBoxToolBarLayout,
diff --git a/fsl/fslview/profiles/__init__.py b/fsl/fslview/profiles/__init__.py
index b3b75b916ca19e80be3113546b05b6fe25c98f0f..a8e955de2e7dbfc8b1bad020dab9fb74fe8c820f 100644
--- a/fsl/fslview/profiles/__init__.py
+++ b/fsl/fslview/profiles/__init__.py
@@ -235,6 +235,9 @@ class Profile(actions.ActionProvider):
     def register(self):
         """This method must be called to register this :class:`Profile`
         instance as the target for mouse/keyboard events.
+
+        Subclasses may override this method to performa any initialisation,
+        but must make sure to call this implementation.
         """
         for t in self.getEventTargets():
             t.Bind(wx.EVT_LEFT_DOWN,   self.__onMouseDown)
@@ -251,7 +254,10 @@ class Profile(actions.ActionProvider):
     def deregister(self):
         """This method de-registers this :class:`Profile` instance from
         receiving mouse/keybouard events.
-        """  
+        
+        Subclasses may override this method to performa any initialisation,
+        but must make sure to call this implementation.        
+        """
         for t in self.getEventTargets():
             t.Bind(wx.EVT_LEFT_DOWN,  None)
             t.Bind(wx.EVT_MIDDLE_UP,  None)
diff --git a/fsl/fslview/profiles/orthoeditprofile.py b/fsl/fslview/profiles/orthoeditprofile.py
index cd07bb17b4f062c56f8c5f8e359a5c4d9d3433c1..72ca026bd623db3147f2b84979718140a7dfbfdc 100644
--- a/fsl/fslview/profiles/orthoeditprofile.py
+++ b/fsl/fslview/profiles/orthoeditprofile.py
@@ -150,10 +150,12 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile):
         # If there's already an existing
         # selection object, clear it 
         if self._selAnnotation is not None:
-            xannot.dequeue(self._selAnnotation,  hold=True)
-            yannot.dequeue(self._selAnnotation,  hold=True)
-            zannot.dequeue(self._selAnnotation,  hold=True)
-            self._selAnnotation  = None
+            xannot.dequeue(self._selAnnotation, hold=True)
+            yannot.dequeue(self._selAnnotation, hold=True)
+            zannot.dequeue(self._selAnnotation, hold=True)
+            
+            self._selAnnotation.destroy()
+            self._selAnnotation = None
 
         self._currentImage = image
 
@@ -203,9 +205,13 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile):
 
     
     def deregister(self):
-        self._xcanvas.getAnnotations().dequeue(self._selAnnotation,  hold=True)
-        self._ycanvas.getAnnotations().dequeue(self._selAnnotation,  hold=True)
-        self._zcanvas.getAnnotations().dequeue(self._selAnnotation,  hold=True)
+        if self._selAnnotation is not None:
+            sa = self._selAnnotation
+            self._xcanvas.getAnnotations().dequeue(sa, hold=True)
+            self._ycanvas.getAnnotations().dequeue(sa, hold=True)
+            self._zcanvas.getAnnotations().dequeue(sa, hold=True)
+            sa.destroy()
+            
         orthoviewprofile.OrthoViewProfile.deregister(self)
 
         
diff --git a/fsl/fslview/profiles/orthoviewprofile.py b/fsl/fslview/profiles/orthoviewprofile.py
index df0da9003138457238217f90ad39acb3b9798ae5..7589bd647b0123adc9511a2e7c3c5f04fc04722c 100644
--- a/fsl/fslview/profiles/orthoviewprofile.py
+++ b/fsl/fslview/profiles/orthoviewprofile.py
@@ -70,6 +70,11 @@ class OrthoViewProfile(profiles.Profile):
         self._ycanvas = canvasPanel.getYCanvas()
         self._zcanvas = canvasPanel.getZCanvas()
 
+        # This attribute will occasionally store a
+        # reference to a gl.annotations.Rectangle -
+        # see the _zoomModeLeftMouse* handlers
+        self._lastRect = None
+
 
     def getEventTargets(self):
         """
@@ -229,7 +234,9 @@ class OrthoViewProfile(profiles.Profile):
 
         mouseDownPos, canvasDownPos = self.getMouseDownLocation()
 
-        canvas.getAnnotations().dequeue(self._lastRect)
+        if self._lastRect is not None:
+            canvas.getAnnotations().dequeue(self._lastRect)
+            self._lastRect = None
 
         rectXlen = abs(canvasPos[canvas.xax] - canvasDownPos[canvas.xax])
         rectYlen = abs(canvasPos[canvas.yax] - canvasDownPos[canvas.yax])
diff --git a/fsl/fslview/toolbar.py b/fsl/fslview/toolbar.py
index 34400efe167c7a492561db237a8d0d7c5c666a4a..26a2dd595cf83305f812a764e94f06a2afbef277 100644
--- a/fsl/fslview/toolbar.py
+++ b/fsl/fslview/toolbar.py
@@ -206,7 +206,9 @@ class FSLViewToolBar(fslpanel._FSLViewPanel, wx.PyPanel):
             if isinstance(toolSpec, actions.ActionButton):
                 label = None
             else:
-                label = strings.properties[targets[toolSpec.key], toolSpec.key]
+                
+                label = strings.properties.get(
+                    (targets[toolSpec.key], toolSpec.key), toolSpec.key)
 
             tools .append(tool)
             labels.append(label)
diff --git a/fsl/fslview/views/canvaspanel.py b/fsl/fslview/views/canvaspanel.py
index 611e1424df7a97de050fd7b011b98f506ebe01f3..42d12d57bcaff97e8bb91b26402769d0b5c71f3a 100644
--- a/fsl/fslview/views/canvaspanel.py
+++ b/fsl/fslview/views/canvaspanel.py
@@ -188,19 +188,28 @@ class CanvasPanel(viewpanel.ViewPanel):
 
         self.__opts = sceneOpts
         
-        # If the provided DisplayContext  does not
-        # have a parent, this will raise an error.
-        # But I don't think a CanvasPanel will ever
-        # be created with a 'master' DisplayContext.
-        self.bindProps('syncLocation',
-                       displayCtx,
-                       displayCtx.getSyncPropertyName('location'))
-        self.bindProps('syncImageOrder',
-                       displayCtx,
-                       displayCtx.getSyncPropertyName('imageOrder'))
-        self.bindProps('syncVolume',
-                       displayCtx,
-                       displayCtx.getSyncPropertyName('volume'))
+        # Bind the sync* properties of this
+        # CanvasPanel to the corresponding
+        # properties on the DisplayContext
+        # instance. 
+        if displayCtx.getParent() is not None:
+            self.bindProps('syncLocation',
+                           displayCtx,
+                           displayCtx.getSyncPropertyName('location'))
+            self.bindProps('syncImageOrder',
+                           displayCtx,
+                           displayCtx.getSyncPropertyName('imageOrder'))
+            self.bindProps('syncVolume',
+                           displayCtx,
+                           displayCtx.getSyncPropertyName('volume'))
+            
+        # If the displayCtx instance does not
+        # have a parent, this means that it is
+        # a top level instance
+        else:
+            self.disableProperty('syncLocation')
+            self.disableProperty('syncImageOrder')
+            self.disableProperty('syncVolume')
 
         self.__canvasContainer = wx.Panel(self)
         self.__canvasPanel     = wx.Panel(self.__canvasContainer)
diff --git a/fsl/fslview/views/lightboxpanel.py b/fsl/fslview/views/lightboxpanel.py
index e5cc029b89ced6da1a2fb5d6997095e80525bd5d..4c63de0ef7a2f1965315f5c2d4b56e115ef2a56d 100644
--- a/fsl/fslview/views/lightboxpanel.py
+++ b/fsl/fslview/views/lightboxpanel.py
@@ -62,16 +62,18 @@ class LightBoxPanel(canvaspanel.CanvasPanel):
             displayCtx)
 
         # My properties are the canvas properties
-        sceneOpts.bindProps('zax',            self._lbCanvas)
-        sceneOpts.bindProps('nrows',          self._lbCanvas)
-        sceneOpts.bindProps('ncols',          self._lbCanvas)
-        sceneOpts.bindProps('topRow',         self._lbCanvas)
-        sceneOpts.bindProps('sliceSpacing',   self._lbCanvas)
-        sceneOpts.bindProps('zrange',         self._lbCanvas)
-        sceneOpts.bindProps('showCursor',     self._lbCanvas)
-        sceneOpts.bindProps('showGridLines',  self._lbCanvas)
-        sceneOpts.bindProps('highlightSlice', self._lbCanvas)
-        sceneOpts.bindProps('twoStageRender', self._lbCanvas)
+        self._lbCanvas.bindProps('zax',             sceneOpts)
+        self._lbCanvas.bindProps('nrows',           sceneOpts)
+        self._lbCanvas.bindProps('ncols',           sceneOpts)
+        self._lbCanvas.bindProps('topRow',          sceneOpts)
+        self._lbCanvas.bindProps('sliceSpacing',    sceneOpts)
+        self._lbCanvas.bindProps('zrange',          sceneOpts)
+        self._lbCanvas.bindProps('showCursor',      sceneOpts)
+        self._lbCanvas.bindProps('showGridLines',   sceneOpts)
+        self._lbCanvas.bindProps('highlightSlice',  sceneOpts)
+        self._lbCanvas.bindProps('renderMode',      sceneOpts)
+        self._lbCanvas.bindProps('softwareMode',    sceneOpts)
+        self._lbCanvas.bindProps('resolutionLimit', sceneOpts)
 
         self._canvasSizer = wx.BoxSizer(wx.HORIZONTAL)
         self.getCanvasPanel().SetSizer(self._canvasSizer)
diff --git a/fsl/fslview/views/orthopanel.py b/fsl/fslview/views/orthopanel.py
index bcb145611ed61f0090e530906b1cb20862d05fdf..acd51969ef640dcd2c7dca9f24b1e8f1e94e2dd6 100644
--- a/fsl/fslview/views/orthopanel.py
+++ b/fsl/fslview/views/orthopanel.py
@@ -110,9 +110,17 @@ class OrthoPanel(canvaspanel.CanvasPanel):
         self._ycanvas.bindProps('zoom', sceneOpts, 'yzoom')
         self._zcanvas.bindProps('zoom', sceneOpts, 'zzoom')
 
-        self._xcanvas.bindProps('twoStageRender', sceneOpts)
-        self._ycanvas.bindProps('twoStageRender', sceneOpts)
-        self._zcanvas.bindProps('twoStageRender', sceneOpts)
+        self._xcanvas.bindProps('renderMode',      sceneOpts)
+        self._ycanvas.bindProps('renderMode',      sceneOpts)
+        self._zcanvas.bindProps('renderMode',      sceneOpts)
+
+        self._xcanvas.bindProps('softwareMode',    sceneOpts)
+        self._ycanvas.bindProps('softwareMode',    sceneOpts)
+        self._zcanvas.bindProps('softwareMode',    sceneOpts)
+
+        self._xcanvas.bindProps('resolutionLimit', sceneOpts)
+        self._ycanvas.bindProps('resolutionLimit', sceneOpts)
+        self._zcanvas.bindProps('resolutionLimit', sceneOpts) 
 
         # And a global zoom which controls all canvases at once
         def onZoom(*a):
@@ -169,17 +177,6 @@ class OrthoPanel(canvaspanel.CanvasPanel):
         self._locationChanged()
         self.initProfile()
 
-        # Set up a default layout (this is probably temporary)
-        import fsl.fslview.controls.imagelistpanel      as ilp
-        import fsl.fslview.controls.locationpanel       as lop
-        import fsl.fslview.controls.imagedisplaytoolbar as idt
-        import fsl.fslview.controls.orthotoolbar        as ot
-        
-        self.togglePanel(ilp.ImageListPanel)
-        self.togglePanel(lop.LocationPanel)
-        self.togglePanel(idt.ImageDisplayToolBar, False, self)
-        self.togglePanel(ot .OrthoToolBar,        False, self)
- 
 
     def destroy(self):
         """Called when this panel is closed. 
diff --git a/fsl/tools/bet.py b/fsl/tools/bet.py
index adeeb30d53b6553d7f023238090385bbddef5e06..69f519311cbc18507cb944f7b7f792f597970305 100644
--- a/fsl/tools/bet.py
+++ b/fsl/tools/bet.py
@@ -13,6 +13,7 @@ import fsl.data.image               as fslimage
 import fsl.data.imageio             as iio
 import fsl.utils.transform          as transform
 import fsl.fslview.displaycontext   as displaycontext
+import fsl.fslview.gl               as fslgl
 
 runChoices = OrderedDict((
 
@@ -187,6 +188,10 @@ def selectHeadCentre(opts, button):
     import                                 wx
     import fsl.fslview.views.orthopanel as orthopanel
 
+    # make sure that GL is initialised
+    fslgl.getWXGLContext(button.GetTopLevelParent())
+    fslgl.bootstrap()
+
     image      = fslimage.Image(opts.inputImage)
     imageList  = fslimage.ImageList([image])
     displayCtx = displaycontext.DisplayContext(imageList)
@@ -197,7 +202,6 @@ def selectHeadCentre(opts, button):
                                         displayCtx,
                                         opts.inputImage,
                                         style=wx.RESIZE_BORDER)
-    panel      = frame.panel
     v2dMat     = display.getTransform('voxel',   'display')
     d2vMat     = display.getTransform('display', 'voxel')
 
@@ -222,12 +226,11 @@ def selectHeadCentre(opts, button):
     displayCtx.addListener('location', 'BETHeadCentre', updateOpts)
 
     # Set the initial location on the orthopanel.
-    # TODO this ain't working, as it needs to be
-    # done after the frame has been displayed, i.e
-    # via wx.CallAfter or similar. 
-    voxCoords   = [opts.xCoordinate, opts.yCoordinate, opts.zCoordinate]
-    worldCoords = transform.transform([voxCoords], v2dMat)[0]
-    panel.pos   = worldCoords
+    voxCoords           = [opts.xCoordinate,
+                           opts.yCoordinate,
+                           opts.zCoordinate]
+    worldCoords         = transform.transform([voxCoords], v2dMat)[0]
+    displayCtx.location = worldCoords
 
     # Position the dialog by the button that was clicked
     pos = button.GetScreenPosition()
@@ -272,15 +275,14 @@ def interface(parent, args, opts):
     
     import wx
     
-    frame    = wx.Frame(parent)
-    betPanel = props.buildGUI(
-        frame, opts, betView, optLabels, optTooltips)
+    frame = wx.Frame(parent)
+    
+    props.buildGUI(frame, opts, betView, optLabels, optTooltips)
 
     frame.Layout()
     frame.Fit()
 
     return frame
-    
 
 
 def runBet(parent, opts):
@@ -292,22 +294,25 @@ def runBet(parent, opts):
 
         if exitCode != 0: return
 
+        # make sure that GL is initialised
+        fslgl.getWXGLContext(window.GetTopLevelParent())
+        fslgl.bootstrap()        
+
         inImage   = fslimage.Image(opts.inputImage)
         outImage  = fslimage.Image(opts.outputImage)
         imageList = fslimage.ImageList([inImage, outImage])
 
         displayCtx = displaycontext.DisplayContext(imageList)
         outDisplay = displayCtx.getDisplayProperties(outImage)
+        outOpts    = outDisplay.getDisplayOpts()
 
-        outDisplay.cmap             = 'Reds'
-        outDisplay.displayRange.xlo = 1
-        outDisplay.clipLow          = True
-        outDisplay.clipHigh         = True
+        outOpts.cmap              = 'Red'
+        outOpts.clippingRange.xlo = 1
 
-        frame  = orthopanel.OrthoFrame(parent,
-                                       imageList,
-                                       displayCtx,
-                                       title=opts.outputImage)
+        frame = orthopanel.OrthoFrame(parent,
+                                      imageList,
+                                      displayCtx,
+                                      title=opts.outputImage)
         frame.Show()
         
     runwindow.checkAndRun('BET', opts, parent, Options.genBetCmd,
diff --git a/fsl/tools/fslview_parseargs.py b/fsl/tools/fslview_parseargs.py
index 14cd94f855dcf635ddb7d51711a1bcbfb0c71916..ce24cf676fcf6af1b34e9ea26a14b3df967efa4a 100644
--- a/fsl/tools/fslview_parseargs.py
+++ b/fsl/tools/fslview_parseargs.py
@@ -87,7 +87,7 @@ OPTIONS = td.TypeDict({
                        'showColourBar',
                        'colourBarLocation',
                        'colourBarLabelSide',
-                       'twoStageRender'],
+                       'performance'],
 
     # From here on, all of the keys are
     # the names of HasProperties classes,
@@ -129,8 +129,7 @@ OPTIONS = td.TypeDict({
     'MaskOpts'      : ['colour',
                        'invert',
                        'threshold'],
-    'VectorOpts'    : ['displayMode',
-                       'xColour',
+    'VectorOpts'    : ['xColour',
                        'yColour',
                        'zColour',
                        'suppressX',
@@ -172,7 +171,7 @@ ARGUMENTS = td.TypeDict({
     'SceneOpts.colourBarLocation'  : ('cbl', 'colourBarLocation'),
     'SceneOpts.colourBarLabelSide' : ('cbs', 'colourBarLabelSide'),
     'SceneOpts.showCursor'         : ('hc',  'hideCursor'),
-    'SceneOpts.twoStageRender'     : ('tr',  'twoStageRendering'),
+    'SceneOpts.performance'        : ('p',   'performance'),
     
     'OrthoOpts.xzoom'       : ('xz', 'xzoom'),
     'OrthoOpts.yzoom'       : ('yz', 'yzoom'),
@@ -214,7 +213,6 @@ ARGUMENTS = td.TypeDict({
     'MaskOpts.invert'    : ('mi', 'maskInvert'),
     'MaskOpts.threshold' : ('t',  'threshold'),
 
-    'VectorOpts.displayMode' : ('d',  'displayMode'),
     'VectorOpts.xColour'     : ('xc', 'xColour'),
     'VectorOpts.yColour'     : ('yc', 'yColour'),
     'VectorOpts.zColour'     : ('zc', 'zColour'),
@@ -242,7 +240,8 @@ HELP = td.TypeDict({
     'SceneOpts.showColourBar'      : 'Show colour bar',
     'SceneOpts.colourBarLocation'  : 'Colour bar location',
     'SceneOpts.colourBarLabelSide' : 'Colour bar label orientation',
-    'SceneOpts.twoStageRender'     : 'Enable two-stage rendering',
+    'SceneOpts.performance'        : 'Rendering performance '
+                                     '(1=fastest, 5=best looking)',
     
     'OrthoOpts.xzoom'       : 'X canvas zoom',
     'OrthoOpts.yzoom'       : 'Y canvas zoom',
@@ -284,7 +283,6 @@ HELP = td.TypeDict({
     'MaskOpts.invert'    : 'Invert',
     'MaskOpts.threshold' : 'Threshold',
 
-    'VectorOpts.displayMode'  : 'Display mode',
     'VectorOpts.xColour'      : 'X colour',
     'VectorOpts.yColour'      : 'Y colour',
     'VectorOpts.zColour'      : 'Z colour',
@@ -880,8 +878,7 @@ def applyImageArgs(args, imageList, displayCtx, **kwargs):
             try:
                 modImage = fslimage.Image(args.images[i].modulate)
                 
-                if modImage.shape  != image.shape[ :3] or \
-                   modImage.pixdim != image.pixdim[:3]:
+                if modImage.shape != image.shape[ :3]:
                     raise RuntimeError(
                         'Image {} cannot be used to modulate {} - '
                         'dimensions don\'t match'.format(modImage, image))
diff --git a/fsl/utils/colourbarbitmap.py b/fsl/utils/colourbarbitmap.py
index 1cef1b234be328513f073a178dcb043910ed96bd..f46d36ad8c8bf5a6bc31b575d17104dd5610441d 100644
--- a/fsl/utils/colourbarbitmap.py
+++ b/fsl/utils/colourbarbitmap.py
@@ -34,6 +34,9 @@ def colourBarBitmap(cmap,
                     textColour='#ffffff'):
     """Plots a colour bar using matplotlib, and returns a RGBA bitmap
     of the specified width/height.
+
+    The bitmap is returned as a W*H*4 numpy array, with the top-left
+    pixel located at index ``[0, 0, :]``.
     """
 
     if orientation not in ['vertical', 'horizontal']:
@@ -112,12 +115,15 @@ def colourBarBitmap(cmap,
     ncols, nrows = canvas.get_width_height()
 
     bitmap = np.fromstring(buf, dtype=np.uint8)
-    bitmap = bitmap.reshape(nrows, ncols, 4)
+    bitmap = bitmap.reshape(nrows, ncols, 4).transpose([1, 0, 2])
 
+    # the bitmap is in argb order,
+    # but we want it in rgba
     rgb = bitmap[:, :, 1:]
     a   = bitmap[:, :, 0]
     bitmap = np.dstack((rgb, a))
 
     if orientation == 'vertical':
         bitmap = np.flipud(bitmap.transpose([1, 0, 2]))
+
     return bitmap