diff --git a/fsl/fslview/gl/__init__.py b/fsl/fslview/gl/__init__.py
index fa59939788ec866f8f561e5564d3da247a6c1093..ddb4471d77a54d242d3089dd4a7037262576ebe9 100644
--- a/fsl/fslview/gl/__init__.py
+++ b/fsl/fslview/gl/__init__.py
@@ -37,6 +37,16 @@ Two super classes are provided for each of these cases:
 
  - The :class:`OSMesaCanvasTarget` class for off-screen rendering using
    OSMesa.
+
+After the :func:`boostrap` function has been called, the following
+package-level attributes will be available:
+
+ - ``GL_VERSION``:     A string containing the target OpenGL version, in the
+                       format ``major.minor``, e.g. ``2.1``.
+
+ - ``glvolume_funcs``: The version-specific module containing functions for
+                       rendering :class:`~fsl.fslview.gl.glvolume.GLVolume`
+                       instances.
 """
 
 import            logging 
@@ -81,7 +91,8 @@ def bootstrap(glVersion=None):
     """
 
     import sys
-    import OpenGL.GL as gl
+    import OpenGL.GL         as gl
+    import OpenGL.extensions as glexts
     import gl14
     import gl21
 
@@ -111,75 +122,39 @@ def bootstrap(glVersion=None):
     # fall back to the gl14 implementation
     if glpkg == gl21:
 
-        import OpenGL.extensions as glexts
 
-        exts = ['GL_ARB_texture_rg',
-                'GL_EXT_gpu_shader4']
+        # List any GL21 extensions here
+        exts = []
         
-        exts = map(glexts.hasExtension, exts)
-        
-        if not all(exts):
+        if not all(map(glexts.hasExtension, exts)):
+            log.debug('One of these OpenGL extensions is '
+                      'not available: [{}]. Falling back '
+                      'to an older OpenGL implementation.'
+                      .format(', '.join(exts))) 
             verstr = '1.4'
             glpkg = gl14
 
+    # If using GL14, and the ARB_vertex_program
+    # and ARB_fragment_program extensions are
+    # not present, we're screwed.
+    if glpkg == gl14:
+        
+        exts = ['GL_ARB_vertex_program',
+                'GL_ARB_fragment_program']
+        
+        if not all(map(glexts.hasExtension, exts)):
+            raise RuntimeError('One of these OpenGL extensions is '
+                               'not available: [{}]. This software '
+                               'cannot run on the available graphics '
+                               'hardware.'.format(', '.join(exts)))
+
     log.debug('Using OpenGL {} implementation'.format(verstr))
 
+    thismod.GL_VERSION     = verstr
     thismod.glvolume_funcs = glpkg.glvolume_funcs
     thismod._bootstrapped  = True
 
 
-def compilePrograms(vertexProgramSrc, fragmentProgramSrc):
-    
-    import OpenGL.GL                      as gl
-    import OpenGL.GL.ARB.fragment_program as arbfp
-    import OpenGL.GL.ARB.vertex_program   as arbvp
-    
-    gl.glEnable(arbvp.GL_VERTEX_PROGRAM_ARB) 
-    gl.glEnable(arbfp.GL_FRAGMENT_PROGRAM_ARB)
-    
-    fragmentProgram = arbfp.glGenProgramsARB(1)
-    vertexProgram   = arbvp.glGenProgramsARB(1) 
-
-    # vertex program
-    arbvp.glBindProgramARB(arbvp.GL_VERTEX_PROGRAM_ARB,
-                           vertexProgram)
-
-    arbvp.glProgramStringARB(arbvp.GL_VERTEX_PROGRAM_ARB,
-                             arbvp.GL_PROGRAM_FORMAT_ASCII_ARB,
-                             len(vertexProgramSrc),
-                             vertexProgramSrc)
-
-    if (gl.glGetError() == gl.GL_INVALID_OPERATION):
-
-        position = gl.glGetIntegerv(arbvp.GL_PROGRAM_ERROR_POSITION_ARB)
-        message  = gl.glGetString(  arbvp.GL_PROGRAM_ERROR_STRING_ARB)
-
-        raise RuntimeError('Error compiling vertex program '
-                           '({}): {}'.format(position, message)) 
-
-    # fragment program
-    arbfp.glBindProgramARB(arbfp.GL_FRAGMENT_PROGRAM_ARB,
-                           fragmentProgram)
-
-    arbfp.glProgramStringARB(arbfp.GL_FRAGMENT_PROGRAM_ARB,
-                             arbfp.GL_PROGRAM_FORMAT_ASCII_ARB,
-                             len(fragmentProgramSrc),
-                             fragmentProgramSrc)
-
-    if (gl.glGetError() == gl.GL_INVALID_OPERATION):
-
-        position = gl.glGetIntegerv(arbfp.GL_PROGRAM_ERROR_POSITION_ARB)
-        message  = gl.glGetString(  arbfp.GL_PROGRAM_ERROR_STRING_ARB)
-
-        raise RuntimeError('Error compiling fragment program '
-                           '({}): {}'.format(position, message))
-
-    gl.glDisable(arbvp.GL_VERTEX_PROGRAM_ARB)
-    gl.glDisable(arbfp.GL_FRAGMENT_PROGRAM_ARB)
-    
-    return vertexProgram, fragmentProgram
-
-
 def getWXGLContext():
     """Create and return a GL context object for rendering to a
     :class:`wx.glcanvas.GLCanvas` canvas.
diff --git a/fsl/fslview/gl/gl14/glvolume_frag.prog b/fsl/fslview/gl/gl14/glvolume_frag.prog
new file mode 100644
index 0000000000000000000000000000000000000000..adf2ce3fbbcc10912d1f16b3ae17b28f54171072
--- /dev/null
+++ b/fsl/fslview/gl/gl14/glvolume_frag.prog
@@ -0,0 +1,89 @@
+!!ARBfp1.0
+#
+# Fragment program used for rendering GLVolume instances.
+#
+# This fragment program does the following:
+# 
+#  1. Retrieves the texture coordinates corresponding to the fragment
+# 
+#  2. Transforms those coordinates into voxel coordinates
+# 
+#  3. Uses those voxel coordinates to look up the corresponding voxel
+#     value in the 3D image texture.
+# 
+#  4. Uses that voxel value to look up the corresponding colour in the
+#     1D colour map texture.
+# 
+#  5. Sets the fragment colour.
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+
+TEMP  dispTexCoord;
+TEMP  voxTexCoord;
+TEMP  normVoxTexCoord;
+TEMP  voxValue;
+TEMP  voxColour;
+PARAM imageShape    = program.local[0];
+PARAM imageShapeInv = program.local[1];
+
+# This matrix scales the voxel value to
+# lie in a range which is appropriate to
+# the current display range 
+PARAM voxValXform[4] = { state.matrix.texture[1] };
+
+# This matrix transforms coordinates
+# from the display coordinate system
+# to image voxel coordinates
+PARAM dispToVoxMat[4] = { state.matrix.texture[0] };
+
+# retrieve the 3D texture coordinates
+# (which are in terms of the display
+# coordinate system)
+MOV dispTexCoord, fragment.texcoord[0];
+
+# Transform said coordinates
+# into voxel coordinates
+DP4 voxTexCoord.x, dispToVoxMat[0], dispTexCoord;
+DP4 voxTexCoord.y, dispToVoxMat[1], dispTexCoord;
+DP4 voxTexCoord.z, dispToVoxMat[2], dispTexCoord;
+
+# Offset voxel coordinates by 0.5 
+# so they are centred within a voxel
+ADD voxTexCoord, voxTexCoord, { 0.5, 0.5, 0.5, 0.0 };
+
+# Normalise voxel coordinates to 
+# lie in the range (0, 1), so they 
+# can be used for texture lookup
+MUL normVoxTexCoord, voxTexCoord, imageShapeInv;
+
+# look up image voxel value
+# from 3D image texture
+TEX voxValue, normVoxTexCoord, texture[0], 3D;
+
+# Scale voxel value according
+# to the current display range
+MUL voxValue, voxValue, voxValXform[0].x;
+ADD voxValue, voxValue, voxValXform[0].w;
+
+# look up the appropriate colour
+# in the 1D colour map texture
+TEX voxColour, voxValue.x, texture[1], 1D;
+
+# If any of the voxel coordinates are
+# less than 0, clear the voxel colour
+CMP voxColour.w, voxTexCoord.x, 0.0, voxColour.w;
+CMP voxColour.w, voxTexCoord.y, 0.0, voxColour.w;
+CMP voxColour.w, voxTexCoord.z, 0.0, voxColour.w;
+
+# If any voxel coordinates are greater than
+# the image shape, clear the voxel colour
+SUB voxTexCoord, voxTexCoord, imageShape;
+CMP voxColour.w, voxTexCoord.x, voxColour.w, 0.0;
+CMP voxColour.w, voxTexCoord.y, voxColour.w, 0.0;
+CMP voxColour.w, voxTexCoord.z, voxColour.w, 0.0;
+
+# Colour the pixel!
+MOV result.color, voxColour;
+
+END
diff --git a/fsl/fslview/gl/gl14/glvolume_funcs.py b/fsl/fslview/gl/gl14/glvolume_funcs.py
index 45aaf431a756d5a940ffcb4f8ee8ae144efb141a..875b1d7b9fa7cbb74d5a297b7e9b4a7d30c9400a 100644
--- a/fsl/fslview/gl/gl14/glvolume_funcs.py
+++ b/fsl/fslview/gl/gl14/glvolume_funcs.py
@@ -36,129 +36,18 @@ import OpenGL.GL                      as gl
 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      as fslgl
-
-
-_glvolume_vertex_program = """!!ARBvp1.0
-
-# Transform the vertex coordinates from the display
-# coordinate system to the screen coordinate system
-TEMP vertexPos;
-
-DP4 vertexPos.x, state.matrix.mvp.row[0], vertex.position;
-DP4 vertexPos.y, state.matrix.mvp.row[1], vertex.position;
-DP4 vertexPos.z, state.matrix.mvp.row[2], vertex.position;
-DP4 vertexPos.w, state.matrix.mvp.row[3], vertex.position;
-
-MOV result.position, vertexPos;
-
-# Set the vertex texture coordinate
-# to the vertex position
-MOV result.texcoord[0], vertex.position;
-
-END
-"""
-"""The vertex program does two things:
-
-  - Transforms vertex coordinates from display space into screen space
-
-  - Sets the vertex texture coordinate from its display coordinate
-"""
-
-
-_glvolume_fragment_program = """!!ARBfp1.0
-TEMP  dispTexCoord;
-TEMP  voxTexCoord;
-TEMP  normVoxTexCoord;
-TEMP  voxValue;
-TEMP  voxColour;
-PARAM imageShape    = program.local[0];
-PARAM imageShapeInv = program.local[1];
-
-# This matrix scales the voxel value to
-# lie in a range which is appropriate to
-# the current display range 
-PARAM voxValXform[4] = { state.matrix.texture[1] };
-
-# This matrix transforms coordinates
-# from the display coordinate system
-# to image voxel coordinates
-PARAM dispToVoxMat[4] = { state.matrix.texture[0] };
-
-# retrieve the 3D texture coordinates
-# (which are in terms of the display
-# coordinate system)
-MOV dispTexCoord, fragment.texcoord[0];
-
-# Transform said coordinates
-# into voxel coordinates
-DP4 voxTexCoord.x, dispToVoxMat[0], dispTexCoord;
-DP4 voxTexCoord.y, dispToVoxMat[1], dispTexCoord;
-DP4 voxTexCoord.z, dispToVoxMat[2], dispTexCoord;
-
-# Offset voxel coordinates by 0.5 
-# so they are centred within a voxel
-ADD voxTexCoord, voxTexCoord, { 0.5, 0.5, 0.5, 0.0 };
-
-# Normalise voxel coordinates to 
-# lie in the range (0, 1), so they 
-# can be used for texture lookup
-MUL normVoxTexCoord, voxTexCoord, imageShapeInv;
-
-# look up image voxel value
-# from 3D image texture
-TEX voxValue, normVoxTexCoord, texture[0], 3D;
-
-# Scale voxel value according
-# to the current display range
-MUL voxValue, voxValue, voxValXform[0].x;
-ADD voxValue, voxValue, voxValXform[0].w;
-
-# look up the appropriate colour
-# in the 1D colour map texture
-TEX voxColour, voxValue.x, texture[1], 1D;
-
-# If any of the voxel coordinates are
-# less than 0, clear the voxel colour
-CMP voxColour.w, voxTexCoord.x, 0.0, voxColour.w;
-CMP voxColour.w, voxTexCoord.y, 0.0, voxColour.w;
-CMP voxColour.w, voxTexCoord.z, 0.0, voxColour.w;
-
-# If any voxel coordinates are greater than
-# the image shape, clear the voxel colour
-SUB voxTexCoord, voxTexCoord, imageShape;
-CMP voxColour.w, voxTexCoord.x, voxColour.w, 0.0;
-CMP voxColour.w, voxTexCoord.y, voxColour.w, 0.0;
-CMP voxColour.w, voxTexCoord.z, voxColour.w, 0.0;
-
-# Colour the pixel!
-MOV result.color, voxColour;
-
-END
-"""
-"""
-The fragment shader does the following:
-
- 1. Retrieves the texture coordinates corresponding to the fragment
-
- 2. Transforms those coordinates into voxel coordinates
-
- 3. Uses those voxel coordinates to look up the corresponding voxel
-    value in the 3D image texture.
-
- 4. Uses that voxel value to look up the corresponding colour in the
-    1D colour map texture.
-
- 5. Sets the fragment colour.
-"""
+import fsl.utils.transform    as transform
+import fsl.fslview.gl.shaders as shaders
 
 
 def init(glvol, xax, yax):
     """Compiles the vertex and fragment programs used for rendering."""
 
-    vertexProgram, fragmentProgram = fslgl.compilePrograms(
-        _glvolume_vertex_program, _glvolume_fragment_program)
+    vertShaderSrc = shaders.getVertexShader(  glvol)
+    fragShaderSrc = shaders.getFragmentShader(glvol) 
+
+    vertexProgram, fragmentProgram = shaders.compilePrograms(
+        vertShaderSrc, fragShaderSrc)
 
     glvol.vertexProgram   = vertexProgram
     glvol.fragmentProgram = fragmentProgram
diff --git a/fsl/fslview/gl/gl14/glvolume_vert.prog b/fsl/fslview/gl/gl14/glvolume_vert.prog
new file mode 100644
index 0000000000000000000000000000000000000000..5833a45f9c9303d3c1a45f199db02bbada55996f
--- /dev/null
+++ b/fsl/fslview/gl/gl14/glvolume_vert.prog
@@ -0,0 +1,29 @@
+!!ARBvp1.0
+#
+# Vertex program used for rendering GLVolume instances.
+#
+# This vertex program does two things:
+#
+#  - Transforms vertex coordinates from display space into screen space
+#
+#  - Sets the vertex texture coordinate from its display coordinate
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+
+# Transform the vertex coordinates from the display
+# coordinate system to the screen coordinate system
+TEMP vertexPos;
+
+DP4 vertexPos.x, state.matrix.mvp.row[0], vertex.position;
+DP4 vertexPos.y, state.matrix.mvp.row[1], vertex.position;
+DP4 vertexPos.z, state.matrix.mvp.row[2], vertex.position;
+DP4 vertexPos.w, state.matrix.mvp.row[3], vertex.position;
+
+MOV result.position, vertexPos;
+
+# Set the vertex texture coordinate
+# to the vertex position
+MOV result.texcoord[0], vertex.position;
+
+END
diff --git a/fsl/fslview/gl/gl21/glvolume_funcs.py b/fsl/fslview/gl/gl21/glvolume_funcs.py
index f594a473266ccaf4f16a792a1ca4674a7f4c9a4e..60e95a28a5e5ac5265ee61750ef7e4c5fc7a2237 100644
--- a/fsl/fslview/gl/gl21/glvolume_funcs.py
+++ b/fsl/fslview/gl/gl21/glvolume_funcs.py
@@ -30,61 +30,21 @@ import numpy             as np
 import OpenGL.GL         as gl
 import OpenGL.arrays.vbo as vbo
 
-import shaders
-import fsl.utils.transform as transform
+import fsl.fslview.gl.shaders as shaders
+import fsl.utils.transform    as transform
 
 
 def _compileShaders(glvol):
     """Compiles and links the OpenGL GLSL vertex and fragment shader
-    programs, and attaches a reference to the resulting program to
-    the given GLVolume object. Raises an error if compilation/linking
-    fails.
-
-    I'm explicitly not using the PyOpenGL
-    :func:`OpenGL.GL.shaders.compileProgram` function, because it attempts
-    to validate the program after compilation, which fails due to texture
-    data not being bound at the time of validation.
+    programs, and attaches a reference to the resulting program, and
+    all GLSL variables, to the given GLVolume object. 
     """
 
     vertShaderSrc = shaders.getVertexShader(  glvol)
     fragShaderSrc = shaders.getFragmentShader(glvol)
+    glvol.shaders = shaders.compileShaders(vertShaderSrc, fragShaderSrc)
 
-    # vertex shader
-    vertShader = gl.glCreateShader(gl.GL_VERTEX_SHADER)
-    gl.glShaderSource(vertShader, vertShaderSrc)
-    gl.glCompileShader(vertShader)
-    vertResult = gl.glGetShaderiv(vertShader, gl.GL_COMPILE_STATUS)
-
-    if vertResult != gl.GL_TRUE:
-        raise RuntimeError('{}'.format(gl.glGetShaderInfoLog(vertShader)))
-
-    # fragment shader
-    fragShader = gl.glCreateShader(gl.GL_FRAGMENT_SHADER)
-    gl.glShaderSource(fragShader, fragShaderSrc)
-    gl.glCompileShader(fragShader)
-    fragResult = gl.glGetShaderiv(fragShader, gl.GL_COMPILE_STATUS)
-
-    if fragResult != gl.GL_TRUE:
-        raise RuntimeError('{}'.format(gl.glGetShaderInfoLog(fragShader)))
-
-    # link all of the shaders!
-    program = gl.glCreateProgram()
-    gl.glAttachShader(program, vertShader)
-    gl.glAttachShader(program, fragShader)
-
-    gl.glLinkProgram(program)
-
-    gl.glDeleteShader(vertShader)
-    gl.glDeleteShader(fragShader)
-
-    linkResult = gl.glGetProgramiv(program, gl.GL_LINK_STATUS)
-
-    if linkResult != gl.GL_TRUE:
-        raise RuntimeError('{}'.format(gl.glGetProgramInfoLog(program)))
-
-    glvol.shaders = program
-
-    # Indices of all vertex/fragment shader parameters
+    # indices of all vertex/fragment shader parameters
     glvol.worldToWorldMatPos = gl.glGetUniformLocation(glvol.shaders,
                                                        'worldToWorldMat')
     glvol.xaxPos             = gl.glGetUniformLocation(glvol.shaders,
diff --git a/fsl/fslview/gl/gl21/glvolume_vert.glsl b/fsl/fslview/gl/gl21/glvolume_vert.glsl
index f48f08f3b5d1fecb64a07ba9f782295d3f61f416..f368f3eb3e49f225d3aa300a34fd4447557af7ef 100644
--- a/fsl/fslview/gl/gl21/glvolume_vert.glsl
+++ b/fsl/fslview/gl/gl21/glvolume_vert.glsl
@@ -1,7 +1,7 @@
 /*
- * OpenGL vertex shader used by fsl/fslview/gl/gl21/slicecanvas_draw.py.
+ * OpenGL vertex shader used by fsl/fslview/gl/gl21/glvolume_funcs.py.
  *
- * All this shader does is transfer  texture coordinates through
+ * All this shader does is transfer texture coordinates through
  * to the fragment shader.
  *
  * Author: Paul McCarthy <pauldmccarthy@gmail.com>
diff --git a/fsl/fslview/gl/gl21/shaders.py b/fsl/fslview/gl/gl21/shaders.py
deleted file mode 100644
index 11c6ae7fcd653537290b7f258f6ebf7c260c594e..0000000000000000000000000000000000000000
--- a/fsl/fslview/gl/gl21/shaders.py
+++ /dev/null
@@ -1,95 +0,0 @@
-#!/usr/bin/env python
-#
-# shaders.py - Convenience functions for managing vertex/fragment shaders.
-#
-# Author: Paul McCarthy <pauldmccarthy@gmail.com>
-#
-"""Convenience for managing vertex and fragment shader source code.
-
-The :mod:`shaders` module provides convenience functions for accessing the
-vertex and fragment shader source files used to render different types of GL
-objects.
-
-All shader programs and associated files are assumed to be located in the same
-directory as this module (i.e. the :mod:`fsl.fslview.gl.gl21` package).
-
-When a shader file is loaded, a simple preprocessor is applied to the source -
-any lines of the form '#pragma include filename', will be replaced with the
-contents of the specified file.
-"""
-
-import logging
-log = logging.getLogger(__name__)
-
-import os.path as op
-
-import fsl.fslview.gl.glvolume  as glvolume
-
-    
-def getVertexShader(globj):
-    """Returns the vertex shader source for the given GL object."""
-    return _getShader(globj, 'vert')
-
-    
-def getFragmentShader(globj):
-    """Returns the fragment shader source for the given GL object.""" 
-    return _getShader(globj, 'frag')
-
-
-def _getShader(globj, shaderType):
-    """Returns the shader source for the given GL object and the given
-    shader type ('vert' or 'frag').
-    """
-    fname = _getFileName(globj, shaderType)
-    with open(fname, 'rt') as f: src = f.read()
-    return _preprocess(src)    
-
-
-def _getFileName(globj, shaderType):
-    """Returns the file name of the shader program for the given GL object
-    and shader type.
-    """
-
-    if shaderType not in ('vert', 'frag'):
-        raise RuntimeError('Invalid shader type: {}'.format(shaderType))
-
-    if   isinstance(globj, glvolume .GLVolume):  prefix = 'glvolume'
-    else:
-        raise RuntimeError('Unknown GL object type: '
-                           '{}'.format(type(globj)))
-
-    return op.join(op.dirname(__file__), '{}_{}.glsl'.format(
-        prefix, shaderType))
- 
-
-def _preprocess(src):
-    """'Preprocess' the given shader source.
-
-    This amounts to searching for lines containing '#pragma include filename',
-    and replacing those lines with the contents of the specified files.
-    """
-
-    lines    = src.split('\n')
-    lines    = [l.strip() for l in lines]
-
-    pragmas = []
-    for linei, line in enumerate(lines):
-        if line.startswith('#pragma'):
-            pragmas.append(linei)
-
-    includes = []
-    for linei in pragmas:
-
-        line = lines[linei].split()
-        
-        if len(line) != 3:       continue
-        if line[1] != 'include': continue
-
-        includes.append((linei, line[2]))
-
-    for linei, fname in includes:
-        fname = op.join(op.dirname(__file__), fname)
-        with open(fname, 'rt') as f:
-            lines[linei] = f.read()
-
-    return '\n'.join(lines)
diff --git a/fsl/fslview/gl/shaders.py b/fsl/fslview/gl/shaders.py
new file mode 100644
index 0000000000000000000000000000000000000000..3c5490c6756782057cd81f2bb4eeb77914442cd0
--- /dev/null
+++ b/fsl/fslview/gl/shaders.py
@@ -0,0 +1,220 @@
+#!/usr/bin/env python
+#
+# shaders.py - Convenience functions for managing vertex/fragment shaders.
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+"""Convenience for managing vertex and fragment shader source code.
+
+The :mod:`shaders` module provides convenience functions for accessing the
+vertex and fragment shader source files used to render different types of GL
+objects.
+
+All shader programs and associated files are assumed to be located in one of
+the OpenGL version specific packages, i.e. :mod:`fsl.fslview.gl.gl14`
+(ARB_vertex_program/ARB_fragment_program shaders) or
+:mod:`fsl.fslview.gl.gl21` (GLSL shaders).
+
+When a shader file is loaded, a simple preprocessor is applied to the source -
+any lines of the form '#pragma include filename', will be replaced with the
+contents of the specified file.
+"""
+
+import logging
+
+import os.path as op
+
+import fsl.fslview.gl          as fslgl
+import fsl.fslview.gl.glvolume as glvolume
+import fsl.fslview.gl.gltensor as gltensor
+
+
+log = logging.getLogger(__name__)
+
+
+def compilePrograms(vertexProgramSrc, fragmentProgramSrc):
+    """Compiles the given vertex and fragment programs (written according
+    to the ARB_vertex_program and ARB_fragment_program extensions), and
+    returns references to the compiled programs.
+    """
+    
+    import OpenGL.GL                      as gl
+    import OpenGL.GL.ARB.fragment_program as arbfp
+    import OpenGL.GL.ARB.vertex_program   as arbvp
+    
+    gl.glEnable(arbvp.GL_VERTEX_PROGRAM_ARB) 
+    gl.glEnable(arbfp.GL_FRAGMENT_PROGRAM_ARB)
+    
+    fragmentProgram = arbfp.glGenProgramsARB(1)
+    vertexProgram   = arbvp.glGenProgramsARB(1) 
+
+    # vertex program
+    arbvp.glBindProgramARB(arbvp.GL_VERTEX_PROGRAM_ARB,
+                           vertexProgram)
+
+    arbvp.glProgramStringARB(arbvp.GL_VERTEX_PROGRAM_ARB,
+                             arbvp.GL_PROGRAM_FORMAT_ASCII_ARB,
+                             len(vertexProgramSrc),
+                             vertexProgramSrc)
+
+    if (gl.glGetError() == gl.GL_INVALID_OPERATION):
+
+        position = gl.glGetIntegerv(arbvp.GL_PROGRAM_ERROR_POSITION_ARB)
+        message  = gl.glGetString(  arbvp.GL_PROGRAM_ERROR_STRING_ARB)
+
+        raise RuntimeError('Error compiling vertex program '
+                           '({}): {}'.format(position, message)) 
+
+    # fragment program
+    arbfp.glBindProgramARB(arbfp.GL_FRAGMENT_PROGRAM_ARB,
+                           fragmentProgram)
+
+    arbfp.glProgramStringARB(arbfp.GL_FRAGMENT_PROGRAM_ARB,
+                             arbfp.GL_PROGRAM_FORMAT_ASCII_ARB,
+                             len(fragmentProgramSrc),
+                             fragmentProgramSrc)
+
+    if (gl.glGetError() == gl.GL_INVALID_OPERATION):
+
+        position = gl.glGetIntegerv(arbfp.GL_PROGRAM_ERROR_POSITION_ARB)
+        message  = gl.glGetString(  arbfp.GL_PROGRAM_ERROR_STRING_ARB)
+
+        raise RuntimeError('Error compiling fragment program '
+                           '({}): {}'.format(position, message))
+
+    gl.glDisable(arbvp.GL_VERTEX_PROGRAM_ARB)
+    gl.glDisable(arbfp.GL_FRAGMENT_PROGRAM_ARB)
+    
+    return vertexProgram, fragmentProgram
+
+
+def compileShaders(vertShaderSrc, fragShaderSrc):
+    """Compiles and links the OpenGL GLSL vertex and fragment shader
+    programs, and returns a reference to the resulting program. Raises
+    an error if compilation/linking fails.
+
+    I'm explicitly not using the PyOpenGL
+    :func:`OpenGL.GL.shaders.compileProgram` function, because it attempts
+    to validate the program after compilation, which fails due to texture
+    data not being bound at the time of validation.
+    """
+    import OpenGL.GL as gl
+    
+    # vertex shader
+    vertShader = gl.glCreateShader(gl.GL_VERTEX_SHADER)
+    gl.glShaderSource(vertShader, vertShaderSrc)
+    gl.glCompileShader(vertShader)
+    vertResult = gl.glGetShaderiv(vertShader, gl.GL_COMPILE_STATUS)
+
+    if vertResult != gl.GL_TRUE:
+        raise RuntimeError('{}'.format(gl.glGetShaderInfoLog(vertShader)))
+
+    # fragment shader
+    fragShader = gl.glCreateShader(gl.GL_FRAGMENT_SHADER)
+    gl.glShaderSource(fragShader, fragShaderSrc)
+    gl.glCompileShader(fragShader)
+    fragResult = gl.glGetShaderiv(fragShader, gl.GL_COMPILE_STATUS)
+
+    if fragResult != gl.GL_TRUE:
+        raise RuntimeError('{}'.format(gl.glGetShaderInfoLog(fragShader)))
+
+    # link all of the shaders!
+    program = gl.glCreateProgram()
+    gl.glAttachShader(program, vertShader)
+    gl.glAttachShader(program, fragShader)
+
+    gl.glLinkProgram(program)
+
+    gl.glDeleteShader(vertShader)
+    gl.glDeleteShader(fragShader)
+
+    linkResult = gl.glGetProgramiv(program, gl.GL_LINK_STATUS)
+
+    if linkResult != gl.GL_TRUE:
+        raise RuntimeError('{}'.format(gl.glGetProgramInfoLog(program)))
+
+    return program
+
+
+def getVertexShader(globj):
+    """Returns the vertex shader source for the given GL object."""
+    return _getShader(globj, 'vert')
+
+
+def getFragmentShader(globj):
+    """Returns the fragment shader source for the given GL object.""" 
+    return _getShader(globj, 'frag')
+
+
+def _getShader(globj, shaderType):
+    """Returns the shader source for the given GL object and the given
+    shader type ('vert' or 'frag').
+    """
+    fname = _getFileName(globj, shaderType)
+    with open(fname, 'rt') as f: src = f.read()
+    return _preprocess(src)    
+
+
+def _getFileName(globj, shaderType):
+    """Returns the file name of the shader program for the given GL object
+    and shader type.
+    """
+
+    if   fslgl.GL_VERSION == '2.1':
+        subdir = 'gl21'
+        suffix = 'glsl'
+    elif fslgl.GL_VERSION == '1.4':
+        subdir = 'gl14'
+        suffix = 'prog'
+
+    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
+    if   isinstance(globj, str):               prefix =  globj
+    elif isinstance(globj, glvolume.GLVolume): prefix = 'glvolume'
+    elif isinstance(globj, gltensor.GLTensor): prefix = 'gltensor'
+    else:
+        raise RuntimeError('Unknown GL object type: '
+                           '{}'.format(type(globj)))
+
+    return op.join(op.dirname(__file__), subdir, '{}_{}.{}'.format(
+        prefix, shaderType, suffix))
+ 
+
+def _preprocess(src):
+    """'Preprocess' the given shader source.
+
+    This amounts to searching for lines containing '#pragma include filename',
+    and replacing those lines with the contents of the specified files.
+    """
+
+    if   fslgl.GL_VERSION == '2.1': subdir = 'gl21'
+    elif fslgl.GL_VERSION == '1.4': subdir = 'gl14'
+
+    lines    = src.split('\n')
+    lines    = [l.strip() for l in lines]
+
+    pragmas = []
+    for linei, line in enumerate(lines):
+        if line.startswith('#pragma'):
+            pragmas.append(linei)
+
+    includes = []
+    for linei in pragmas:
+
+        line = lines[linei].split()
+        
+        if len(line) != 3:       continue
+        if line[1] != 'include': continue
+
+        includes.append((linei, line[2]))
+
+    for linei, fname in includes:
+        fname = op.join(op.dirname(__file__), subdir, fname)
+        with open(fname, 'rt') as f:
+            lines[linei] = f.read()
+
+    return '\n'.join(lines)