diff --git a/fsl/fsleyes/gl/globject.py b/fsl/fsleyes/gl/globject.py
index 210f7a8fbc721688d408a3c92e6c973ab107d7be..e3e4a201670a263caac3172bf7856b9c3e6d6d1d 100644
--- a/fsl/fsleyes/gl/globject.py
+++ b/fsl/fsleyes/gl/globject.py
@@ -290,6 +290,7 @@ class GLSimpleObject(GLObject):
         """Create a ``GLSimpleObject``. """
         GLObject.__init__(self)
 
+        
     def destroy( self):
         """Overrides :meth:`GLObject.destroy`. Does nothing. """
         pass
diff --git a/fsl/fsleyes/gl/textures/rendertexture.py b/fsl/fsleyes/gl/textures/rendertexture.py
index 11771f57fbb3936bbe2d129e0b14089fb55c06d1..1279e77838f89c8d02c2eb3a7eeb848b546e1893 100644
--- a/fsl/fsleyes/gl/textures/rendertexture.py
+++ b/fsl/fsleyes/gl/textures/rendertexture.py
@@ -1,9 +1,18 @@
 #!/usr/bin/env python
 #
-# rendertexture.py -
+# rendertexture.py - The RenderTexture and GLObjectRenderTexture classes.
 #
 # Author: Paul McCarthy <pauldmccarthy@gmail.com>
 #
+"""This module provides the :class:`RenderTexture` and
+:class:`GLObjectRenderTexture` classes, which are :class:`.Texture2D`
+sub-classes intended to be used as targets for off-screen rendering.
+
+These classes are used by the :class:`.SliceCanvas` and
+:class:`.LightBoxCanvas` classes for off-screen rendering. See also the
+:class:`.RenderTextureStack`, which uses :class:`RenderTexture` instances.
+"""
+
 
 import logging
 
@@ -19,16 +28,48 @@ 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.
+    """The ``RenderTexture`` class encapsulates a 2D texture, a frame buffer,
+    and a render buffer, intended to be used as a target for off-screen
+    rendering. Using a ``RenderTexture`` (``tex`` in the example below)
+    as the rendering target is easy::
+
+        # Set the texture size in pixels
+        tex.setSize(1024, 768)
+    
+        # Bind the texture/frame buffer, and configure
+        # the viewport for orthoghraphic display. 
+        lo = (0.0, 0.0, 0.0)
+        hi = (1.0, 1.0, 1.0)
+        tex.bindAsRenderTarget()
+        tex.setRenderViewport(0, 1, lo, hi)
+
+        # ...
+        # draw the scene
+        # ...
+
+        # Unbind the texture/frame buffer,
+        # and restore the previous viewport.
+        tex.unbindAsRenderTarget()
+        tex.restoreViewport()
+
+    
+    The contents of the ``RenderTexture`` can later be drawn to the screen
+    via the :meth:`.Texture2D.draw` or :meth:`.Texture2D.drawOnBounds`
+    methods.
     """
     
     def __init__(self, name, interp=gl.GL_NEAREST):
-        """
+        """Create a ``RenderTexture``.
+
+        :arg name:   A unique name for this ``RenderTexture``.
 
-        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``).
+        :arg interp: Texture interpolation - either ``GL_NEAREST`` (the
+                     default) or ``GL_LINEAR``.
+
+        .. note:: A rendering target must have been set for the GL context
+                  before a frame buffer can be created ... in other words,
+                  call ``context.SetCurrent`` before creating a
+                  ``RenderTexture``.
         """
 
         texture.Texture2D.__init__(self, name, interp)
@@ -47,7 +88,12 @@ class RenderTexture(texture.Texture2D):
 
         
     def destroy(self):
-        texture.Texture.destroy(self)
+        """Must be called when this ``RenderTexture`` is no longer needed.
+        Destroys the frame buffer and render buffer, and calls
+        :meth:`.Texture2D.destroy`.
+        """
+        
+        texture.Texture2D.destroy(self)
 
         log.debug('Deleting RB{}/FBO{}'.format(
             self.__renderBuffer,
@@ -57,11 +103,33 @@ class RenderTexture(texture.Texture2D):
 
 
     def setData(self, data):
+        """Raises a :exc:`NotImplementedError`. The ``RenderTexture`` derives
+        from the :class:`.Texture2D` class, but is not intended to have its
+        texture data manually set - see the :class:`.Texture2D` documentation.
+        """
         raise NotImplementedError('Texture data cannot be set for {} '
                                   'instances'.format(type(self).__name__))
 
 
     def setRenderViewport(self, xax, yax, lo, hi):
+        """Configures the GL viewport for a 2D orthographic display. See the
+        :func:`.routines.show2D` function.
+
+        The existing viewport settings are cached, and can be restored via
+        the :meth:`restoreViewport` method.
+
+        :arg xax: The display coordinate system axis which corresponds to the
+                  horizontal screen axis.
+        
+        :arg yax: The display coordinate system axis which corresponds to the
+                  vertical screen axis.
+        
+        :arg lo:  A tuple containing the minimum ``(x, y, z)`` display
+                  coordinates.
+        
+        :arg hi:  A tuple containing the maximum ``(x, y, z)`` display
+                  coordinates.
+        """
 
         if self.__oldSize    is not None or \
            self.__oldProjMat is not None or \
@@ -85,6 +153,9 @@ class RenderTexture(texture.Texture2D):
             
 
     def restoreViewport(self):
+        """Restores the GL viewport settings which were saved via a prior call
+        to :meth:`setRenderViewport`.
+        """
 
         if self.__oldSize    is None or \
            self.__oldProjMat is None or \
@@ -110,6 +181,12 @@ class RenderTexture(texture.Texture2D):
 
         
     def bindAsRenderTarget(self):
+        """Configures the frame buffer and render buffer of this
+        ``RenderTexture`` as the targets for rendering.
+
+        The existing farme buffer and render buffer are cached, and can be
+        restored via the :meth:`unbindAsRenderTarget` method.
+        """
 
         if self.__oldFrameBuffer  is not None or \
            self.__oldRenderBuffer is not None:
@@ -133,6 +210,9 @@ class RenderTexture(texture.Texture2D):
 
 
     def unbindAsRenderTarget(self):
+        """Restores the frame buffer and render buffer which were saved via a
+        prior call to :meth:`bindAsRenderTarget`.
+        """
         
         if self.__oldFrameBuffer  is None or \
            self.__oldRenderBuffer is None:
@@ -158,6 +238,10 @@ class RenderTexture(texture.Texture2D):
 
         
     def refresh(self):
+        """Overrides :meth:`.Texture2D.refresh`. Calls the base-class
+        implementation, and ensures that the frame buffer and render buffer
+        of this ``RenderTexture`` are configured correctly.
+        """
         texture.Texture2D.refresh(self)
 
         width, height = self.getSize()
@@ -184,22 +268,50 @@ class RenderTexture(texture.Texture2D):
             gl.GL_DEPTH_STENCIL_ATTACHMENT,
             glfbo.GL_RENDERBUFFER_EXT,
             self.__renderBuffer)
-            
+
+        self.unbindAsRenderTarget()
+        self.unbindTexture()            
+
+        # Complain if something is not right
         if glfbo.glCheckFramebufferStatusEXT(glfbo.GL_FRAMEBUFFER_EXT) != \
            glfbo.GL_FRAMEBUFFER_COMPLETE_EXT:
-            self.unbindAsRenderTarget()
-            self.unbindTexture()            
             raise RuntimeError('An error has occurred while '
                                'configuring the frame buffer')
 
-        self.unbindAsRenderTarget()
-        self.unbindTexture()
-
         
 class GLObjectRenderTexture(RenderTexture):
+    """The ``GLObjectRenderTexture`` is a :class:`RenderTexture` intended to
+    be used for rendering :class:`.GLObject` instances off-screen. 
+
+    
+    The advantage of using a ``GLObjectRenderTexture`` over a
+    :class:`.RenderTexture` is that a ``GLObjectRenderTexture`` will
+    automatically adjust its size to suit the resolution of the
+    :class:`.GLObject` - see the :meth:`.GLObject.getDataResolution` method.
+
+    
+    In order to accomplish this, the :meth:`setAxes` method must be called
+    whenever the display orientation changes, so that the render texture
+    size can be re-calculated.
+    """
     
     def __init__(self, name, globj, xax, yax, maxResolution=1024):
-        """
+        """Create a ``GLObjectRenderTexture``.
+
+        :arg name:          A unique name for this ``GLObjectRenderTexture``.
+        
+        :arg globj:         The :class:`.GLObject` instance which is to be
+                            rendered.
+        
+        :arg xax:           Index of the display coordinate system axis to be
+                            used as the horizontal render texture axis.
+        
+        :arg yax:           Index of the display coordinate system axis to be
+                            used as the vertical render texture axis.
+        
+        :arg maxResolution: Maximum resolution in pixels, along either the
+                            horizontal or vertical axis, for this
+                            ``GLObjectRenderTexture``.
         """
         
         self.__globj         = globj
@@ -214,27 +326,44 @@ class GLObjectRenderTexture(RenderTexture):
 
         self.__updateSize()        
 
-    
-    def setAxes(self, xax, yax):
-        self.__xax = xax
-        self.__yax = yax
-        self.__updateSize()
-
         
     def destroy(self):
+        """Must be called when this ``GLObjectRenderTexture`` is no longer
+        needed. Removes the update listener from the :class:`.GLObject`, and
+        calls :meth:`.RenderTexture.destroy`.
+        """
 
         name = '{}_{}'.format(self.getTextureName(), id(self))
         self.__globj.removeUpdateListener(name) 
         RenderTexture.destroy(self)
 
+        
+    def setAxes(self, xax, yax):
+        """This method must be called when the display orientation of the
+        :class:`GLObject` changes. It updates the size of this
+        ``GLObjectRenderTexture`` so that the resolution and aspect ratio
+        of the ``GLOBject`` are maintained.
+        """
+        self.__xax = xax
+        self.__yax = yax
+        self.__updateSize()
+
     
     def setSize(self, width, height):
+        """Raises a :exc:`NotImplementedError`. The size of a
+        ``GLObjectRenderTexture`` is set automatically.
+        """
         raise NotImplementedError(
             'Texture size cannot be set for {} instances'.format(
                 type(self).__name__))
         
         
     def __updateSize(self, *a):
+        """Updates the size of this ``GLObjectRenderTexture``, basing it
+        on the resolution returned by the :meth:`.GLObject.getDataResolution`
+        method. If that method returns ``None``, a default resolution is used.
+        
+        """
         globj  = self.__globj
         maxRes = self.__maxResolution
 
diff --git a/fsl/fsleyes/gl/textures/rendertexturestack.py b/fsl/fsleyes/gl/textures/rendertexturestack.py
index 9b3f98ad78003b7219eab39f28f75f5b9664a771..96cf5f1ee0ab699ce5e26e9577df3fe0834854df 100644
--- a/fsl/fsleyes/gl/textures/rendertexturestack.py
+++ b/fsl/fsleyes/gl/textures/rendertexturestack.py
@@ -1,17 +1,20 @@
 #!/usr/bin/env python
 #
-# rendertexturelist.py -
+# rendertexturestack.py - The RenderTextureStack class.
 #
 # Author: Paul McCarthy <pauldmccarthy@gmail.com>
 #
+"""This module provides the :class:`RenderTextureStack` class, which is used
+by the :class:`.SliceCanvas` class to store a collection of off-screen
+:class:`.RenderTexture` instances containing rendered slices of
+:class:`.GLObject` instances.
+"""
 
 import logging
 
-import numpy     as np
 import OpenGL.GL as gl
 
 import fsl.fsleyes.gl.routines as glroutines
-import fsl.utils.transform     as transform
 import                            rendertexture
 
 
@@ -19,8 +22,30 @@ log = logging.getLogger(__name__)
 
 
 class RenderTextureStack(object):
+    """The ``RenderTextureStack`` class creates and maintains a collection of
+    :class:`.RenderTexture` instances, each of which is used to display a
+    single slice of a :class:`.GLObject` along a specific display axis.
 
+    The purpose of the ``RenderTextureStack`` is to pre-generate 2D slices of
+    a :class:`.GLObject` so that they do not have to be rendered on-demand.
+    Rendering a ``GLObject`` slices from a pre-generated off-screen texture
+    provides better performance than rendering the ``GLObject`` slice
+    in real time.
+
+    The :class:`.RenderTexture` textures are updated in an idle loop, which is
+    triggered by the ``wx.EVT_IDLE`` event.
+    """
+
+    
     def __init__(self, globj):
+        """Create a ``RenderTextureStack``. A listener is registered on the
+        ``wx.EVT_IDLE`` event, so that the :meth:`__textureUpdateLoop` method
+        is called periodically.  An update listener is registered on the
+        ``GLObject``, so that the textures can be refreshed whenever it
+        changes.
+
+        :arg globj: The :class:`.GLObject` instance.
+        """
 
 
         self.name = '{}_{}_{}'.format(
@@ -48,80 +73,54 @@ class RenderTextureStack(object):
         import wx
         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
+        
+    def destroy(self):
+        """Must be called when this ``RenderTextureStack`` is no longer needed.
+        Calls the :meth:`__destroyTextures` method.
+        """
+        self.__destroyTextures()
 
-        if index > limit and index <= limit + 1:
-            index = limit
 
-        return int(index)
+    def getGLObject(self):
+        """Returns the :class:`.GLObject` associated with this
+        ``RenderTextureStack``.
+        """
+        return self.__globj
 
     
-    def __indexToZpos(self, index):
-        zmin  = self.__zmin
-        zmax  = self.__zmax
-        ntexs = len(self.__textures)
-        return index * (zmax - zmin) / ntexs + zmin
+    def draw(self, zpos, xform=None):
+        """Draws the pre-generated :class:`.RenderTexture` which corresponds
+        to the  specified Z position.
 
+        :arg zpos:  Position of slice to render.
 
-    def __textureUpdateLoop(self, ev):
-        ev.Skip()
+        :arg xform: Transformation matrix to apply to rendered slice vertices.
+        """
 
-        if len(self.__updateQueue) == 0 or len(self.__textures) == 0:
-            return
+        xax = self.__xax
+        yax = self.__yax
 
-        idx = self.__updateQueue.pop(0)
+        texIdx                  = self.__zposToIndex(zpos)
+        self.__lastDrawnTexture = texIdx
 
-        if not self.__textureDirty[idx]:
+        if texIdx < 0 or texIdx >= len(self.__textures):
             return
 
-        tex = self.__textures[idx]
-        
-        log.debug('Refreshing texture slice {} (zax {})'.format(
-            idx, self.__zax))
-        
-        self.__refreshTexture(tex, idx)
+        lo, hi  = self.__globj.getDisplayBounds()
+        texture = self.__textures[texIdx]
 
-        if len(self.__updateQueue) > 0:
-            ev.RequestMore()
+        if self.__textureDirty[texIdx]:
+            self.__refreshTexture(texture, texIdx)
 
-            
-    def getGLObject(self):
-        return self.__globj
+        texture.drawOnBounds(
+            zpos, lo[xax], hi[xax], lo[yax], hi[yax], xax, yax, xform)
 
     
     def setAxes(self, xax, yax):
+        """This method must be called when the display orientation of the
+        :class:`.GLObject` changes. It destroys and re-creates all
+        :class:`.RenderTexture` instances.
+        """
 
         zax        = 3 - xax - yax
         self.__xax = xax
@@ -151,19 +150,77 @@ class RenderTextureStack(object):
 
         
     def __destroyTextures(self):
+        """Destroys all :class:`.RenderTexture` instances. This is performed
+        asynchronously, via the ``.wx.CallLater`` function.
+        """
 
         import wx
         texes = self.__textures
         self.__textures = []
         for tex in texes:
             wx.CallLater(50, tex.destroy)
+
+            
+    def __refreshAllTextures(self, *a):
+        """Marks all :class:`.RenderTexture`  instances as *dirty*, so that
+        they will be refreshed by the :meth:`.__textureUpdateLoop`.
+        """
+
+        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 __textureUpdateLoop(self, ev):
+        """This method is called periodically through the ``wx.EVT_IDLE``
+        event. It loops through all :class:`.RenderTexture` instances, and
+        refreshes any that have been marked as *dirty*.
+        """
+        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]
         
-    
-    def destroy(self):
-        self.__destroyTextures()
+        log.debug('Refreshing texture slice {} (zax {})'.format(
+            idx, self.__zax))
+        
+        self.__refreshTexture(tex, idx)
 
+        if len(self.__updateQueue) > 0:
+            ev.RequestMore()
 
+        
     def __refreshTexture(self, tex, idx):
+        """Refreshes the given :class:`.RenderTexture`.
+
+        :arg tex: The ``RenderTexture`` to refresh.
+        :arg idx: Index of the ``RenderTexture``.
+        """
 
         zpos = self.__indexToZpos(idx)
         xax  = self.__xax
@@ -209,35 +266,28 @@ class RenderTextureStack(object):
 
         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)
+    def __zposToIndex(self, zpos):
+        """Converts a Z location in the display coordinate system into a
+        ``RenderTexture`` index.
+        """        
+        zmin  = self.__zmin
+        zmax  = self.__zmax
+        ntexs = len(self.__textures)
+        limit = len(self.__textures) - 1
+        index = ntexs * (zpos - zmin) / (zmax - zmin)
 
-        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 index > limit and index <= limit + 1:
+            index = limit
 
-        if xform is not None:
-            vertices = transform.transform(vertices, xform=xform)
+        return int(index)
 
-        texture.draw(vertices)
+    
+    def __indexToZpos(self, index):
+        """Converts a ``RenderTexture`` index into a Z location in the display
+        coordinate system.
+        """
+        zmin  = self.__zmin
+        zmax  = self.__zmax
+        ntexs = len(self.__textures)
+        return index * (zmax - zmin) / ntexs + zmin