diff --git a/fsl/fsleyes/displaycontext/tensoropts.py b/fsl/fsleyes/displaycontext/tensoropts.py
index 2cbb07b476e3bbf9e6f2484350531193d7c377a0..e3d55e6b9b7e714fdccfa7a3726e848b9629cb65 100644
--- a/fsl/fsleyes/displaycontext/tensoropts.py
+++ b/fsl/fsleyes/displaycontext/tensoropts.py
@@ -7,15 +7,26 @@
 
 import logging
 
-import volumeopts
+import props
+
+import vectoropts
 
 
 log = logging.getLogger(__name__)
 
 
-class TensorOpts(volumeopts.ImageOpts):
+class TensorOpts(vectoropts.VectorOpts):
+
+
+    # Only show 2D ellipses around 
+    # the three primary tensor axes
+    outline  = props.Boolean(default=False)
 
+
+    # Enable/disable lighting effects
+    lighting = props.Boolean(default=False)
+    
     
     def __init__(self, *args, **kwargs):
         
-        volumeopts.ImageOpts.__init__(self, *args, **kwargs)
+        vectoropts.VectorOpts.__init__(self, *args, **kwargs)
diff --git a/fsl/fsleyes/displaycontext/vectoropts.py b/fsl/fsleyes/displaycontext/vectoropts.py
index 079182e5dada1d6ff3cd8a8427892ff32027ed7a..ee3e6bd05213e02317f02eff82c7180304df9c9d 100644
--- a/fsl/fsleyes/displaycontext/vectoropts.py
+++ b/fsl/fsleyes/displaycontext/vectoropts.py
@@ -19,9 +19,9 @@ import                   volumeopts
 
 
 class VectorOpts(volumeopts.ImageOpts):
-    """The ``VectorOpts`` class is the base class for :class:`LineVectorOpts` and
-    :class:`RGBVectorOpts`. It contains display settings which are common to
-    both.
+    """The ``VectorOpts`` class is the base class for :class:`LineVectorOpts`,
+    :class:`RGBVectorOpts`, and :class:`.TensorOpts`. It contains display
+    settings which are common to each of them.
     """
 
 
diff --git a/fsl/fsleyes/gl/__init__.py b/fsl/fsleyes/gl/__init__.py
index bfd07005d960fbba43e6b82c98f7ca52392e4614..fc892789d59f2216a91476427df7d624135cb3e0 100644
--- a/fsl/fsleyes/gl/__init__.py
+++ b/fsl/fsleyes/gl/__init__.py
@@ -263,7 +263,10 @@ def bootstrap(glVersion=None):
                            rendering :class:`.GLModel` instances.
     
     ``gllabel_funcs``      The version-specific module containing functions for
-                           rendering :class:`.GLLabel` instances. 
+                           rendering :class:`.GLLabel` instances.
+    
+    ``gltensor_funcs``     The version-specific module containing functions for
+                           rendering :class:`.GLTensor` instances. 
     ====================== ====================================================
     
 
@@ -375,6 +378,7 @@ def bootstrap(glVersion=None):
     thismod.gllinevector_funcs = glpkg.gllinevector_funcs
     thismod.glmodel_funcs      = glpkg.glmodel_funcs
     thismod.gllabel_funcs      = glpkg.gllabel_funcs
+    thismod.gltensor_funcs     = glpkg.gltensor_funcs
     thismod._bootstrapped      = True
 
 
diff --git a/fsl/fsleyes/gl/gl21/__init__.py b/fsl/fsleyes/gl/gl21/__init__.py
index f5873e38593c571d15cbcc7f92cc8ae678074f1b..1b02bcdbdd238d1e3939f96f0fd96996347e7a99 100644
--- a/fsl/fsleyes/gl/gl21/__init__.py
+++ b/fsl/fsleyes/gl/gl21/__init__.py
@@ -15,6 +15,7 @@ exist:
    ~fsl.fsleyes.gl.gl21.gllinevector_funcs
    ~fsl.fsleyes.gl.gl21.glmodel_funcs
    ~fsl.fsleyes.gl.gl21.gllabel_funcs
+   ~fsl.fsleyes.gl.gl21.gltensor_funcs
 """
 
 import glvolume_funcs
@@ -22,3 +23,4 @@ import glrgbvector_funcs
 import gllinevector_funcs
 import glmodel_funcs
 import gllabel_funcs
+import gltensor_funcs
diff --git a/fsl/fsleyes/gl/gl21/gltensor_funcs.py b/fsl/fsleyes/gl/gl21/gltensor_funcs.py
new file mode 100644
index 0000000000000000000000000000000000000000..0e925ea1b3e9600f788dcd127c63e6afd1de3de2
--- /dev/null
+++ b/fsl/fsleyes/gl/gl21/gltensor_funcs.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python
+#
+# gltensor_funcs.py -
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+
+import logging
+
+import OpenGL.GL                as gl
+
+import fsl.fsleyes.gl           as fslgl
+import fsl.fsleyes.gl.resources as glresources
+import fsl.fsleyes.gl.textures  as textures
+import fsl.fsleyes.gl.shaders   as shaders
+
+
+log = logging.getLogger(__name__)
+
+
+def init(self):
+
+
+    image = self.image
+
+    v1 = image.V1()
+    v2 = image.V2()
+    v3 = image.V3()
+    l1 = image.L1()
+    l2 = image.L2()
+    l3 = image.L3()
+
+
+    def vPrefilter(d):
+        return d.transpose((3, 0, 1, 2))
+
+    names = ['v1', 'v2', 'v3', 'l1', 'l2', 'l3']
+    imgs  = [ v1,   v2,   v3,   l1,   l2,   l3]
+
+    for  name, img in zip(names, imgs):
+        texName = '{}_{}'.format(type(self).__name__, id(img))
+
+        if name[0] == 'v':
+            prefilter = vPrefilter
+            nvals     = 3
+        else:
+            prefilter = None
+            nvals     = 1
+        
+        tex = glresources.get(
+            texName,
+            textures.ImageTexture,
+            texName,
+            img,
+            nvals=nvals,
+            normalise=True,
+            prefilter=prefilter)
+
+        setattr(self, '{}Texture'.format(name), tex)
+
+
+    self.shaders = None
+
+    compileShaders(self)
+
+
+def destroy(self):
+    pass
+
+
+def compileShaders(self):
+    if self.shaders is not None:
+        gl.glDeleteProgram(self.shaders)
+
+    vertShaderSrc = shaders.getVertexShader(  self)
+    fragShaderSrc = shaders.getFragmentShader(self)
+    
+    self.shaders = shaders.compileShaders(vertShaderSrc, fragShaderSrc)
+
+
+def updateShaderState(self):
+    pass
+
+
+def preDraw(self):
+    pass
+
+
+def draw(self, zpos, xform=None):
+    pass
+
+
+def postDraw(self):
+    pass
diff --git a/fsl/fsleyes/gl/gl21/gltensor_vert.glsl b/fsl/fsleyes/gl/gl21/gltensor_vert.glsl
new file mode 100644
index 0000000000000000000000000000000000000000..6874d1b7eae141c85a05d4ccfec3bfd83a86696d
--- /dev/null
+++ b/fsl/fsleyes/gl/gl21/gltensor_vert.glsl
@@ -0,0 +1,142 @@
+/*
+ * OpenGL vertex shader used for rendering GLTensor instances.
+ *
+ * Author: Paul McCarthy <pauldmccarthy@gmail.com>
+ */
+#version 120
+
+#define PI      3.141592653589793
+#define PI_ON_2 1.5707963267948966
+
+
+/*
+ Required inputs:
+
+  - Textures containing V1, V2, V3 and L1, L2, L3
+
+  - Voxel coordinates (these are the vertices)
+  - Vertex index 
+  - Ellipsoid resolution
+
+  **We use (index % resolution) to calculate:**
+
+  - U and V angles 
+
+  - 
+
+ */
+
+uniform sampler3D v1Texture;
+uniform sampler3D v2Texture;
+uniform sampler3D v3Texture;
+uniform sampler3D l1Texture;
+uniform sampler3D l2Texture;
+uniform sampler3D l3Texture;
+
+uniform mat4 v1ValXform;
+uniform mat4 v2ValXform;
+uniform mat4 v3ValXform;
+uniform mat4 l1ValXform;
+uniform mat4 l2ValXform;
+uniform mat4 l3ValXform;
+
+uniform mat4 voxToDisplayMat;
+
+uniform vec3 imageShape;
+
+uniform float resolution;
+
+attribute float index;
+attribute vec3  voxel;
+
+varying vec3 fragVoxCoord;
+varying vec3 fragTexCoord;
+
+
+vec3 ellipsoidVertex(float l1, float l2, float l3, float u, float v) {
+
+  float cosu = cos(u);
+  float cosv = cos(v);
+  float sinu = sin(u);
+  float sinv = sin(v);
+  
+  float spcu = sign(cosu) * pow(abs(cosu), 2);
+  float spcv = sign(cosv) * pow(abs(cosv), 2);
+  float spsu = sign(sinu) * pow(abs(sinu), 2);
+  float spsv = sign(sinv) * pow(abs(sinv), 2);
+
+  vec3 x;
+
+  x.x = spcu * spcv;
+  x.y = spcu * spsv;
+  x.z = spsu;
+
+  return x;
+}
+
+
+
+void main(void) {
+
+  float umin = -PI_ON_2;
+  float umax =  PI_ON_2;
+  float vmin = -PI;
+  float vmax =  PI;
+  float ustep = (umax - umin) / resolution;
+  float vstep = (vmax - vmin) / resolution;
+
+  // Index of this vertex 
+  // within the ellipsoid
+  float ellipsoidIndex = mod(index, resolution);
+
+  // Ellipsoid angles for this vertex
+  float u = umin + ustep * ellipsoidIndex;
+  float v = vmin + vstep * ellipsoidIndex;
+
+  // Lookup the tensor parameters from the textures
+  vec3 texCoord = (voxel + 0.5) / imageShape;
+
+  vec3  v1 = texture3D(v1Texture, texCoord).xyz;
+  vec3  v2 = texture3D(v2Texture, texCoord).xyz;
+  vec3  v3 = texture3D(v3Texture, texCoord).xyz;
+  float l1 = texture3D(l1Texture, texCoord).x;
+  float l2 = texture3D(l2Texture, texCoord).x;
+  float l3 = texture3D(l3Texture, texCoord).x;
+
+  // Transform from normalised 
+  // texture values to real values
+  v1 = v1 * v1ValXform[0].x + v1ValXform[3].x;
+  v2 = v2 * v2ValXform[0].x + v2ValXform[3].x;
+  v3 = v3 * v3ValXform[0].x + v3ValXform[3].x;
+  l1 = l1 * l1ValXform[0].x + l1ValXform[3].x;
+  l2 = l2 * l2ValXform[0].x + l2ValXform[3].x;
+  l3 = l3 * l3ValXform[0].x + l3ValXform[3].x;
+
+  // Calculate the position of
+  // this vertex on the ellipsoid.
+  // Vertices are grouped into quads -
+  // figure out what corner we're on
+  vec3 pos;
+
+  float corner = mod(ellipsoidIndex, 4);
+  
+  if      (corner == 0) pos = ellipsoidVertex(l1, l2, l3, u,         v);
+  else if (corner == 1) pos = ellipsoidVertex(l1, l2, l3, u + ustep, v);
+  else if (corner == 2) pos = ellipsoidVertex(l1, l2, l3, u + ustep, v + vstep);
+  else if (corner == 3) pos = ellipsoidVertex(l1, l2, l3, u,         v + vstep);
+
+  // Transform the vertex from
+  // the fibre coordinate system
+  // to the voxel coordinate system
+  mat3 eigvecs = mat3(v1, v2, v3);
+  
+  pos = pos * eigvecs + voxel;
+
+  // Transform from voxels into display
+  gl_Position = gl_ModelViewProjectionMatrix *
+                voxToDisplayMat              *
+                vec4(pos, 1);
+
+  fragVoxCoord = voxel;
+  fragTexCoord = texCoord;
+}
diff --git a/fsl/fsleyes/gl/gltensor.py b/fsl/fsleyes/gl/gltensor.py
new file mode 100644
index 0000000000000000000000000000000000000000..1be569b2c5daba93bd4e8874cb3869c5211492d9
--- /dev/null
+++ b/fsl/fsleyes/gl/gltensor.py
@@ -0,0 +1,62 @@
+#!/usr/bin/env python
+#
+# gltensor.py -
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+
+import logging
+
+import glvector
+
+import fsl.fsleyes.gl as fslgl
+
+log = logging.getLogger(__name__)
+
+
+class GLTensor(glvector.GLVector):
+
+    
+    def __init__(self, image, display):
+        glvector.GLVector.__init__(self, image, display)
+
+        fslgl.gltensor_funcs.init(self)
+
+
+    def destroy(self):
+        glvector.GLVector.destroy(self)
+        fslgl.gltensor_funcs.destroy(self)
+
+        
+    def getDataResolution(self, xax, yax):
+        """Overrides :meth:`.GLVector.getDataResolution`. Returns a pixel
+        resolution suitable for rendering this ``GLTensor``.
+        """
+
+        res       = list(glvector.GLVector.getDataResolution(self, xax, yax))
+        res[xax] *= 20
+        res[yax] *= 20
+        
+        return res
+
+
+    def compileShaders(self):
+        fslgl.gltensor_funcs.compileShaders(self)
+
+
+    def updateShaderState(self):
+        fslgl.gltensor_funcs.updateShaderState(self)
+
+        
+    def preDraw(self):
+        glvector.GLVector.preDraw(self)
+        fslgl.gltensor_funcs.preDraw(self)
+
+
+    def draw(self, zpos, xform=None):
+        fslgl.gltensor_funcs.draw(self, zpos, xform)
+
+
+    def postDraw(self):
+        glvector.GLVector.postDraw(self)
+        fslgl.gltensor_funcs.postDraw(self)
diff --git a/fsl/fsleyes/gl/shaders.py b/fsl/fsleyes/gl/shaders.py
index 770c1b937bd225ee761ae3670583cb38dd8c51b0..f1321e6c8074188d64d932d53d08c9fc8b1191e4 100644
--- a/fsl/fsleyes/gl/shaders.py
+++ b/fsl/fsleyes/gl/shaders.py
@@ -253,6 +253,9 @@ _shaderTypePrefixMap = td.TypeDict({
 
     ('GLModel',      'vert') : 'glmodel',
     ('GLModel',      'frag') : 'glmodel',
+
+    ('GLTensor',     'vert') : 'gltensor',
+    ('GLTensor',     'frag') : 'glvector', 
 })
 """This dictionary provides a mapping between :class:`.GLObject` types,
 and file name prefixes, identifying the shader programs to use.