diff --git a/fsl/fsleyes/displaycontext/tensoropts.py b/fsl/fsleyes/displaycontext/tensoropts.py
index e3d55e6b9b7e714fdccfa7a3726e848b9629cb65..f8455697db7028cc88d3c1f3ca4181d19e78a3f4 100644
--- a/fsl/fsleyes/displaycontext/tensoropts.py
+++ b/fsl/fsleyes/displaycontext/tensoropts.py
@@ -25,7 +25,10 @@ class TensorOpts(vectoropts.VectorOpts):
 
     # Enable/disable lighting effects
     lighting = props.Boolean(default=False)
-    
+
+
+    # Tensor ellipsoid resolution
+    tensorResolution = props.Int(default=10)
     
     def __init__(self, *args, **kwargs):
         
diff --git a/fsl/fsleyes/gl/__init__.py b/fsl/fsleyes/gl/__init__.py
index fc892789d59f2216a91476427df7d624135cb3e0..253938ee5e44f88eb713d03d11acd66e56ac6b65 100644
--- a/fsl/fsleyes/gl/__init__.py
+++ b/fsl/fsleyes/gl/__init__.py
@@ -123,6 +123,7 @@ as:
    ~fsl.fsleyes.gl.gllinevector.GLLineVector
    ~fsl.fsleyes.gl.glrgbvector.GLRGBVector
    ~fsl.fsleyes.gl.glmodel.GLModel
+   ~fsl.fsleyes.gl.gltensor.GLTensor
 
 These objects are created and destroyed automatically by :class:`.SliceCanvas`
 instances, so application code does not need to worry about them too much.
diff --git a/fsl/fsleyes/gl/gl21/gllinevector_funcs.py b/fsl/fsleyes/gl/gl21/gllinevector_funcs.py
index b788a27dffc8991ad0593fa41e04624c2c14c16c..8617f60fe715dec76d99fa222eea16c113481e34 100644
--- a/fsl/fsleyes/gl/gl21/gllinevector_funcs.py
+++ b/fsl/fsleyes/gl/gl21/gllinevector_funcs.py
@@ -43,11 +43,10 @@ def init(self):
     :class:`.Image`  overlay.
     """
     
-    self.shaders            = None
-    self.vertexBuffer       = gl.glGenBuffers(1)
-    self.texCoordBuffer     = gl.glGenBuffers(1)
-    self.vertexIDBuffer     = gl.glGenBuffers(1)
-    self.lineVertices       = None
+    self.shaders        = None
+    self.vertexBuffer   = gl.glGenBuffers(1)
+    self.texCoordBuffer = gl.glGenBuffers(1)
+    self.vertexIDBuffer = gl.glGenBuffers(1)
 
     self._vertexResourceName = '{}_{}_vertices'.format(
         type(self).__name__, id(self.image))
diff --git a/fsl/fsleyes/gl/gl21/gltensor_funcs.py b/fsl/fsleyes/gl/gl21/gltensor_funcs.py
index 0e925ea1b3e9600f788dcd127c63e6afd1de3de2..300a95b8f0a1d4b45e220b64a126466f55d444c5 100644
--- a/fsl/fsleyes/gl/gl21/gltensor_funcs.py
+++ b/fsl/fsleyes/gl/gl21/gltensor_funcs.py
@@ -7,10 +7,15 @@
 
 import logging
 
-import OpenGL.GL                as gl
 
-import fsl.fsleyes.gl           as fslgl
+import numpy                          as np
+import OpenGL.GL                      as gl
+import OpenGL.raw.GL._types           as gltypes
+import OpenGL.GL.ARB.instanced_arrays as arbia
+
+import fsl.utils.transform      as transform
 import fsl.fsleyes.gl.resources as glresources
+import fsl.fsleyes.gl.routines  as glroutines
 import fsl.fsleyes.gl.textures  as textures
 import fsl.fsleyes.gl.shaders   as shaders
 
@@ -20,7 +25,6 @@ log = logging.getLogger(__name__)
 
 def init(self):
 
-
     image = self.image
 
     v1 = image.V1()
@@ -38,7 +42,7 @@ def init(self):
     imgs  = [ v1,   v2,   v3,   l1,   l2,   l3]
 
     for  name, img in zip(names, imgs):
-        texName = '{}_{}'.format(type(self).__name__, id(img))
+        texName = '{}_{}_{}'.format(type(self).__name__, name, id(img))
 
         if name[0] == 'v':
             prefilter = vPrefilter
@@ -58,17 +62,32 @@ def init(self):
 
         setattr(self, '{}Texture'.format(name), tex)
 
+    self.vertexBuffer = gl.glGenBuffers(1)
+    self.voxelBuffer  = gl.glGenBuffers(1)
 
     self.shaders = None
 
     compileShaders(self)
+    updateShaderState(self)
 
 
 def destroy(self):
-    pass
+    gl.glDeleteBuffers(1, gltypes.GLuint(self.vertexBuffer))
+    gl.glDeleteBuffers(1, gltypes.GLuint(self.voxelBuffer))
+    gl.glDeleteProgram(self.shaders)
+    
+    names = ['v1', 'v2', 'v3', 'l1', 'l2', 'l3']
+
+    for name in names:
+        attrName = '{}Texture'.format(name)
+        tex      = getattr(self, attrName)
+        
+        glresources.delete(tex.getTextureName())
+        setattr(self, attrName, None)
 
 
 def compileShaders(self):
+    
     if self.shaders is not None:
         gl.glDeleteProgram(self.shaders)
 
@@ -77,18 +96,175 @@ def compileShaders(self):
     
     self.shaders = shaders.compileShaders(vertShaderSrc, fragShaderSrc)
 
+    vertUniforms = ['v1Texture',       'v2Texture',  'v3Texture',
+                    'l1Texture',       'l2Texture',  'l3Texture',
+                    'v1ValXform',      'v2ValXform', 'v3ValXform',
+                    'l1ValXform',      'l2ValXform', 'l3ValXform',
+                    'voxToDisplayMat', 'imageShape', 'resolution']
+
+    vertAtts     = ['voxel', 'vertex']
+
+    fragUniforms = ['imageTexture',   'modTexture',     'modThreshold',
+                    'xColourTexture', 'yColourTexture', 'zColourTexture',
+                    'voxValXform',    'cmapXform',      'imageShape',
+                    'useSpline']
+
+    for vu in vertUniforms:
+        loc = gl.glGetUniformLocation(self.shaders, vu)
+        setattr(self, '{}Pos'.format(vu), loc)
+
+    for fu in fragUniforms:
+        if hasattr(self, '{}Pos'.format(fu)):
+            continue
+        loc = gl.glGetUniformLocation(self.shaders, fu)
+        setattr(self, '{}Pos'.format(fu), loc)
+
+    for va in vertAtts:
+        loc = gl.glGetAttribLocation(self.shaders, va)
+        setattr(self, '{}Pos'.format(va), loc)
+
 
 def updateShaderState(self):
-    pass
+
+    gl.glUseProgram(self.shaders)
+
+    opts = self.displayOpts
+
+    # Textures used by the fragment shader
+    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)
+
+    # Textures used by the vertex shader
+    gl.glUniform1i(self.v1TexturePos, 5)
+    gl.glUniform1i(self.v2TexturePos, 6)
+    gl.glUniform1i(self.v3TexturePos, 7)
+    gl.glUniform1i(self.l1TexturePos, 8)
+    gl.glUniform1i(self.l2TexturePos, 9)
+    gl.glUniform1i(self.l3TexturePos, 10)
+
+    # Voxel value transforms use by the fragment shader
+    cmapXform   = self.xColourTexture.getCoordinateTransform()
+    voxValXform = self.imageTexture.voxValXform
+    v1ValXform  = self.v1Texture.voxValXform
+    v2ValXform  = self.v2Texture.voxValXform
+    v3ValXform  = self.v3Texture.voxValXform
+    l1ValXform  = self.l1Texture.voxValXform
+    l2ValXform  = self.l2Texture.voxValXform
+    l3ValXform  = self.l3Texture.voxValXform
+    
+    voxValXform = np.array(voxValXform, dtype=np.float32).ravel('C')
+    cmapXform   = np.array(cmapXform,   dtype=np.float32).ravel('C')
+    v1ValXform  = np.array(v1ValXform,  dtype=np.float32).ravel('C')
+    v2ValXform  = np.array(v2ValXform,  dtype=np.float32).ravel('C')
+    v3ValXform  = np.array(v3ValXform,  dtype=np.float32).ravel('C')
+    l1ValXform  = np.array(l1ValXform,  dtype=np.float32).ravel('C')
+    l2ValXform  = np.array(l2ValXform,  dtype=np.float32).ravel('C')
+    l3ValXform  = np.array(l3ValXform,  dtype=np.float32).ravel('C')
+    
+    gl.glUniformMatrix4fv(self.voxValXformPos, 1, False, voxValXform)
+    gl.glUniformMatrix4fv(self.cmapXformPos,   1, False, cmapXform)
+    gl.glUniformMatrix4fv(self.v1ValXformPos,  1, False, v1ValXform)
+    gl.glUniformMatrix4fv(self.v2ValXformPos,  1, False, v2ValXform)
+    gl.glUniformMatrix4fv(self.v3ValXformPos,  1, False, v3ValXform)
+    gl.glUniformMatrix4fv(self.l1ValXformPos,  1, False, l1ValXform)
+    gl.glUniformMatrix4fv(self.l2ValXformPos,  1, False, l2ValXform)
+    gl.glUniformMatrix4fv(self.l3ValXformPos,  1, False, l3ValXform)
+
+    # Other miscellaneous uniforms
+    imageShape   = np.array(self.image.shape[:3], dtype=np.float32)
+    resolution   = opts.tensorResolution
+    modThreshold = opts.modThreshold / 100.0
+    useSpline    = 0
+
+    gl.glUniform3fv(self.imageShapePos, 1, imageShape)
+    gl.glUniform1f( self.resolutionPos,    resolution)
+    gl.glUniform1f( self.modThresholdPos,  modThreshold)
+    gl.glUniform1f( self.useSplinePos,     useSpline)
+    
+    # Vertices and indices
+    vertices = glroutines.unitSphere(resolution)
+
+    self.nVertices = len(vertices)
+
+    vertices = vertices.ravel('C')
+
+    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.glUseProgram(0)
 
 
 def preDraw(self):
-    pass
+    gl.glUseProgram(self.shaders) 
+    self.v1Texture.bindTexture(gl.GL_TEXTURE5)
+    self.v2Texture.bindTexture(gl.GL_TEXTURE6)
+    self.v3Texture.bindTexture(gl.GL_TEXTURE7)
+    self.l1Texture.bindTexture(gl.GL_TEXTURE8)
+    self.l2Texture.bindTexture(gl.GL_TEXTURE9)
+    self.l3Texture.bindTexture(gl.GL_TEXTURE10)
+    gl.glEnableVertexAttribArray(self.voxelPos)
+    gl.glEnableVertexAttribArray(self.vertexPos)
+ 
 
 
 def draw(self, zpos, xform=None):
-    pass
+
+    image  = self.image
+    opts   = self.displayOpts
+    v2dMat = opts.getTransform('voxel',   'display')
+    d2vMat = opts.getTransform('display', 'voxel')
+
+    resolution = np.array([opts.resolution] * 3)
+
+    if opts.transform == 'id':
+        resolution = resolution / min(image.pixdim[:3])
+    elif opts.transform == 'pixdim':
+        resolution = map(lambda r, p: max(r, p), resolution, image.pixdim[:3])
+
+    voxels = glroutines.calculateSamplePoints(
+        image.shape,
+        resolution,
+        v2dMat,
+        self.xax,
+        self.yax)[0]
+
+    voxels[:, self.zax] = zpos
+
+    voxels  = transform.transform(voxels, d2vMat)
+    nVoxels = len(voxels)
+
+    voxels = np.array(voxels, dtype=np.float32).ravel('C')
+
+    gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.voxelBuffer)
+    gl.glBufferData(
+        gl.GL_ARRAY_BUFFER, voxels.nbytes, voxels, gl.GL_STATIC_DRAW)
+    gl.glVertexAttribPointer(
+        self.voxelPos, 3, gl.GL_FLOAT, gl.GL_FALSE, 0, None)
+    arbia.glVertexAttribDivisorARB(self.voxelPos, 1)
+
+    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)
+
+    gl.glDrawArraysInstanced(gl.GL_QUADS, 0, self.nVertices, nVoxels)
 
 
 def postDraw(self):
-    pass
+    gl.glUseProgram(0)
+    gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0)
+    self.v1Texture.unbindTexture()
+    self.v2Texture.unbindTexture()
+    self.v3Texture.unbindTexture()
+    self.l1Texture.unbindTexture()
+    self.l2Texture.unbindTexture()
+    self.l3Texture.unbindTexture()
+    gl.glDisableVertexAttribArray(self.voxelPos)
+    gl.glDisableVertexAttribArray(self.vertexPos) 
diff --git a/fsl/fsleyes/gl/gl21/gltensor_vert.glsl b/fsl/fsleyes/gl/gl21/gltensor_vert.glsl
index 6874d1b7eae141c85a05d4ccfec3bfd83a86696d..438564dfed665d042594b99aa580e3a1d6ffcaa1 100644
--- a/fsl/fsleyes/gl/gl21/gltensor_vert.glsl
+++ b/fsl/fsleyes/gl/gl21/gltensor_vert.glsl
@@ -5,27 +5,6 @@
  */
 #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;
@@ -46,53 +25,15 @@ uniform vec3 imageShape;
 
 uniform float resolution;
 
-attribute float index;
 attribute vec3  voxel;
+attribute vec3  vertex;
 
 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;
 
@@ -112,25 +53,18 @@ void main(void) {
   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);
+  // Transform the sphere by the tensor
+  // eigenvalues and eigenvectors, and
+  // shift it by the voxel coordinates
+  // to transform it in to the image
+  // voxel coordinate system.
+  vec3 pos = vertex;
+  pos.x *= l1 * 150;
+  pos.y *= l2 * 150;
+  pos.z *= l3 * 150;
   
-  pos = pos * eigvecs + voxel;
+  pos  = mat3(v1, v2, v3) * pos;
+  pos += voxel;
 
   // Transform from voxels into display
   gl_Position = gl_ModelViewProjectionMatrix *
diff --git a/fsl/fsleyes/gl/routines.py b/fsl/fsleyes/gl/routines.py
index aac8bc38063b50607fd9c274fa0d2b1529ae37bf..61bc7738c08c3aa430fd9cfa76756b76864a92dd 100644
--- a/fsl/fsleyes/gl/routines.py
+++ b/fsl/fsleyes/gl/routines.py
@@ -83,6 +83,9 @@ def show2D(xax, yax, width, height, lo, hi):
     elif zax == 1:
         gl.glRotatef(270, 1, 0, 0)
 
+    gl.glDepthMask(gl.GL_FALSE)
+    gl.glDisable(gl.GL_DEPTH_TEST)
+
 
 def calculateSamplePoints(shape, resolution, xform, xax, yax, origin='centre'):
     """Calculates a uniform grid of points, in the display coordinate system
@@ -635,3 +638,52 @@ def planeEquation(xyz1, xyz2, xyz3):
               (x3 * ((y1 * z2) - (y2 * z1))))
 
     return eq
+
+
+def unitSphere(res):
+    """Generates a unit sphere, as described in the *Sphere Generation*
+    article, on Paul Bourke's excellent website:
+
+        http://paulbourke.net/geometry/circlesphere/
+
+    :arg res: Resolution - the number of angles to sample.
+
+    :returns: A ``numpy.float32`` array of size ``(4 * res**2, 3)`` containing
+              the ``(x, y, z)`` vertices which can be used to draw a unit
+              sphere (using the ``GL_QUAD`` primitive type).
+    """
+
+    # TODO Eliminate duplicate vertices,
+    #      and return an index array
+
+    ustep = np.pi       / res
+    vstep = np.pi * 2.0 / res
+
+    u = np.linspace(-np.pi / 2, np.pi / 2 + ustep, res + 1, dtype=np.float32)
+    v = np.linspace(-np.pi,     np.pi     + vstep, res + 1, dtype=np.float32)
+
+    cosu = np.cos(u)
+    cosv = np.cos(v)
+    sinu = np.sin(u)
+    sinv = np.sin(v) 
+
+    vertices = np.zeros((res * res * 4, 3), dtype=np.float32)
+
+    cucv   = np.outer(cosu[:-1], cosv[:-1]).flatten()
+    cusv   = np.outer(cosu[:-1], sinv[:-1]).flatten()
+    cu1cv  = np.outer(cosu[1:],  cosv[:-1]).flatten()
+    cu1sv  = np.outer(cosu[1:],  sinv[:-1]).flatten()
+    cu1cv1 = np.outer(cosu[1:],  cosv[1:]) .flatten()
+    cu1sv1 = np.outer(cosu[1:],  sinv[1:]) .flatten()
+    cucv1  = np.outer(cosu[:-1], cosv[1:]) .flatten()
+    cusv1  = np.outer(cosu[:-1], sinv[1:]) .flatten()
+    
+    su     = np.repeat(sinu[:-1], res)
+    s1u    = np.repeat(sinu[1:],  res)
+
+    vertices.T[:,  ::4] = [cucv,   cusv,   su]
+    vertices.T[:, 1::4] = [cu1cv,  cu1sv,  s1u]
+    vertices.T[:, 2::4] = [cu1cv1, cu1sv1, s1u]
+    vertices.T[:, 3::4] = [cucv1,  cusv1,  su]
+
+    return vertices
diff --git a/fsl/fsleyes/gl/textures/texture.py b/fsl/fsleyes/gl/textures/texture.py
index 4ba6e0a9f07de22ba80d81a7480815fa58f7cb4d..38bf5af7f48553921e70a0b376f3dd3502ccad33 100644
--- a/fsl/fsleyes/gl/textures/texture.py
+++ b/fsl/fsleyes/gl/textures/texture.py
@@ -143,7 +143,6 @@ class Texture(object):
 
         if textureUnit is not None:
             gl.glActiveTexture(textureUnit)
-            gl.glEnable(self.__ttype)
 
         gl.glBindTexture(self.__ttype, self.__texture)
 
@@ -155,7 +154,6 @@ class Texture(object):
 
         if self.__textureUnit is not None:
             gl.glActiveTexture(self.__textureUnit)
-            gl.glDisable(self.__ttype)
             
         gl.glBindTexture(self.__ttype, 0)