diff --git a/changelog.txt b/changelog.txt
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,3 +1,6 @@
+0.7.3.0:
+- add object models
+
 0.7.2.0:
 - add option to set initial camera position
 
diff --git a/not-gloss.cabal b/not-gloss.cabal
--- a/not-gloss.cabal
+++ b/not-gloss.cabal
@@ -1,5 +1,5 @@
 name:                not-gloss
-version:             0.7.2.1
+version:             0.7.3.0
 stability:           Experimental
 synopsis:            Painless 3D graphics, no affiliation with gloss
 description:{
@@ -40,9 +40,12 @@
                        GLUT >= 2.3.0,
                        time >= 1.4.0,
                        OpenGLRaw >= 2.3.0,
+                       OpenGL >= 2.3.0,
                        spatial-math >= 0.2.1.2,
+                       vector,
                        cereal,
                        binary,
+                       vector-binary-instances,
                        bytestring,
                        bmp
 
diff --git a/src/Vis.hs b/src/Vis.hs
--- a/src/Vis.hs
+++ b/src/Vis.hs
@@ -15,6 +15,8 @@
            , SpecialKey(..)
            , BitmapFont(..)
            , Flavour(..)
+           , LoadedObjModel
+           , loadObjModel
            , module Vis.GlossColor
            ) where
 
@@ -23,7 +25,7 @@
 import Vis.Vis ( Options(..), visMovie )
 import Vis.Camera ( Camera0(..) )
 import Vis.Interface ( display, animate, simulate, play, animateIO, simulateIO, playIO )
-import Vis.VisObject ( VisObject(..) )
+import Vis.VisObject ( VisObject(..), LoadedObjModel, loadObjModel )
 import Vis.GlossColor
 
 -- | Some reasonable default options.
diff --git a/src/Vis/VisObject.hs b/src/Vis/VisObject.hs
--- a/src/Vis/VisObject.hs
+++ b/src/Vis/VisObject.hs
@@ -6,19 +6,26 @@
 
 module Vis.VisObject ( VisObject(..)
                      , drawObjects
+                     , LoadedObjModel
+                     , loadObjModel
                      , setPerspectiveMode
                      ) where
 
 import GHC.Generics ( Generic )
 
 import Control.Monad ( when )
+import qualified Data.Foldable as F
 import Data.Maybe ( fromJust, isJust )
 import Data.Word ( Word8 )
+import qualified Data.Vector.Storable as VS
 import qualified Data.Serialize as S
 import qualified Data.Binary as B
+import Data.Vector.Cereal ()
+import Data.Vector.Binary ()
 import Graphics.Rendering.OpenGL.Raw
+import qualified Graphics.Rendering.OpenGL as GL
 import qualified Graphics.UI.GLUT as GLUT
-import Foreign.C.Types ( CFloat(..) )
+import Foreign.C.Types ( CFloat(..), CInt(..) )
 import Graphics.UI.GLUT ( BitmapFont(..), Capability(..), Color4(..), Face(..)
                         , Flavour(..), MatrixMode(..), PrimitiveMode(..), Size(..)
                         , Vertex3(..), Vector3(..)
@@ -60,8 +67,14 @@
                  | Text3d String (V3 a) BitmapFont GlossColor.Color
                  | Text2d String (a,a) BitmapFont GlossColor.Color
                  | Points [V3 a] (Maybe GLfloat) GlossColor.Color
+                 | ObjModel LoadedObjModel GlossColor.Color
                  deriving (Generic, Functor)
 
+data LoadedObjModel = LoadedObjModel (VS.Vector Double) (VS.Vector Double) Int deriving (Generic)
+
+instance S.Serialize LoadedObjModel
+instance B.Binary LoadedObjModel
+
 toFlavour :: Bool -> Flavour
 toFlavour False = Solid
 toFlavour True = Wireframe
@@ -193,7 +206,7 @@
     glVertex3d x1 y1 z1
     glVertex3d x2 y2 z2
     glEnd
-   
+
 -- quad
 drawObject (Quad (V3 x0 y0 z0) (V3 x1 y1 z1) (V3 x2 y2 z2) (V3 x3 y3 z3) col) =
   GLUT.preservingMatrix $ do
@@ -212,7 +225,7 @@
   GLUT.preservingMatrix $ do
     setMaterialDiffuse col
     setColor col
-    
+
     -- GLUT.translate (Vector3 0 0 (-height/2) :: Vector3 GLdouble)
 
     let nslices = 10 :: Int
@@ -223,7 +236,7 @@
           where
             angle = 2*pi/(fromIntegral nslices)
             angles = reverse $ map ((angle*) . fromIntegral) [0..(nslices+1)]
-            
+
     -- Cover the base and top
     glBegin gl_TRIANGLE_FAN
     glNormal3d 0 0 (-1)
@@ -283,7 +296,7 @@
 drawObject (Line' pathcols) =
   GLUT.preservingMatrix $ do
     GLUT.lighting $= Disabled
-    
+
     glBegin gl_LINE_STRIP
     let f (xyz, col) = do
           let V3 x y z = fmap realToFrac xyz
@@ -327,7 +340,7 @@
                                      ] | x0 <- [-r,-r+r/n..r], y0 <- [-r,-r+r/n..r]]
     drawWithEps eps
     drawWithEps (-eps)
-    
+
     glEnable gl_BLEND
 
 
@@ -343,9 +356,9 @@
 
         rotAngle = acos(z/(sqrt(x*x + y*y + z*z) + 1e-15))*180/pi :: GLdouble
         rotAxis = Vector3 (-y) x 0
-    
+
     GLUT.rotate rotAngle rotAxis
-    
+
     -- cylinder
     drawObject $ Cylinder (cylinderHeight, cylinderRadius) col
     -- cone
@@ -396,3 +409,36 @@
     GLUT.pointSize $= s'
     GLUT.lighting $= Enabled
 
+drawObject (Vis.VisObject.ObjModel (LoadedObjModel vvec nvec numVerts') col) =
+  GLUT.preservingMatrix $ do
+    setMaterialDiffuse col
+    setColor col
+
+    -- enable vertex/normal arrays
+    -- todo: Should this be done every time?
+    --       Either enable at the start, or push/pop to preserve user attributes
+    GL.clientState GL.VertexArray $= GL.Enabled
+    GL.clientState GL.NormalArray $= GL.Enabled
+
+    -- set the vertex and normal arrays
+    let numVerts :: CInt
+        numVerts = fromIntegral numVerts'
+        va = GL.VertexArrayDescriptor 3 GL.Double 0
+        na = GL.VertexArrayDescriptor 3 GL.Double 0
+    VS.unsafeWith vvec $ \vptr -> GL.arrayPointer GL.VertexArray $= va vptr
+    VS.unsafeWith nvec $ \nptr -> GL.arrayPointer GL.NormalArray $= na nptr
+    -- draw the triangles
+    GL.drawArrays GL.Triangles 0 numVerts
+
+    -- disable vertex/normal arrays
+    GL.clientState GL.VertexArray $= GL.Disabled
+    GL.clientState GL.NormalArray $= GL.Disabled
+
+-- | turn a list of vertex/normal tuples into vertex/normal arrays
+loadObjModel :: F.Foldable f => f (V3 Double, V3 Double) -> LoadedObjModel
+loadObjModel vns = LoadedObjModel (VS.fromList vs) (VS.fromList ns) n
+  where
+    vs = F.concatMap (\(V3 x y z) -> [x,y,z]) vs'
+    ns = F.concatMap (\(V3 x y z) -> [x,y,z]) ns'
+    (vs',ns') = unzip $ F.toList vns
+    n = length vs'
