diff --git a/modules/gfx/pymod/__init__.py b/modules/gfx/pymod/__init__.py
index 0cd783f9fa44d36dd8ab1696257b943e1d2732c6..4d96c534e4e7978849ae4406dc2b828524d860c2 100644
--- a/modules/gfx/pymod/__init__.py
+++ b/modules/gfx/pymod/__init__.py
@@ -243,7 +243,7 @@ def _primlist_add_text(self,text,pos,color=None,point_size=None):
   if not point_size:
     point_size=1.0
   self._add_text(text,pos,color,point_size)
-  
+
 PrimList.AddPoint=_primlist_add_point
 PrimList.AddLine=_primlist_add_line
 PrimList.AddSphere=_primlist_add_sphere
diff --git a/modules/gfx/pymod/export_primlist.cc b/modules/gfx/pymod/export_primlist.cc
index b8326509d8ed7065f120ab4760a541b5766e207b..27b46737645328b01d212d6601f2b45807b6ca76 100644
--- a/modules/gfx/pymod/export_primlist.cc
+++ b/modules/gfx/pymod/export_primlist.cc
@@ -25,8 +25,88 @@ using namespace boost::python;
 using namespace ost;
 using namespace ost::gfx;
 
+#if OST_NUMPY_SUPPORT_ENABLED
+#include <numpy/arrayobject.h>
+#endif
+
+namespace {
+  void add_mesh(PrimList& p, object ova, object ona, object oca, object oia)
+  {
+#if OST_NUMPY_SUPPORT_ENABLED
+    if(!PyArray_Check(ova.ptr())) {
+      throw std::runtime_error("ova is not a numpy array");
+    }
+    PyArrayObject* va=reinterpret_cast<PyArrayObject*>(ova.ptr());
+    if(!PyArray_ISCONTIGUOUS(va)) {
+      throw std::runtime_error("expected vertex array to be contiguous");
+    }
+    if(!PyArray_TYPE(va)==NPY_FLOAT) {
+      throw std::runtime_error("expected vertex array to be of dtype=float32");
+    }
+    size_t v_size=PyArray_SIZE(va);
+    if(v_size%3!=0) {
+      throw std::runtime_error("expected vertex array size to be divisible by 3");
+    }
+    size_t v_count=v_size/3;
+    float* vp=reinterpret_cast<float*>(PyArray_DATA(va));
+    float* np=0;
+    float* cp=0;
+    if(ona!=object()) {
+      if(!PyArray_Check(ona.ptr())) {
+        throw std::runtime_error("ona is not a numpy array");
+      }
+      PyArrayObject* na=reinterpret_cast<PyArrayObject*>(ona.ptr());
+      if(!PyArray_ISCONTIGUOUS(na)) {
+        throw std::runtime_error("expected normal array to be contiguous");
+      }
+      if(!PyArray_TYPE(na)==NPY_FLOAT) {
+        throw std::runtime_error("expected normal array to be of dtype=float32");
+      }
+      if(PyArray_SIZE(na)!=v_size) {
+        throw std::runtime_error("expected normal array size to match vertex array size");
+      }
+      np=reinterpret_cast<float*>(PyArray_DATA(na));
+    }
+    if(oca!=object()) {
+      if(!PyArray_Check(oca.ptr())) {
+        throw std::runtime_error("oca is not a numpy array");
+      }
+      PyArrayObject* ca=reinterpret_cast<PyArrayObject*>(oca.ptr());
+      if(!PyArray_ISCONTIGUOUS(ca)) {
+        throw std::runtime_error("expected color array to be contiguous");
+      }
+      if(!PyArray_TYPE(ca)==NPY_FLOAT) {
+        throw std::runtime_error("expected color array to be of dtype=float32");
+      }
+      if(PyArray_SIZE(ca)!=v_count*4) {
+        throw std::runtime_error("expected color array size to equal vertex-count x 4");
+      }
+      cp=reinterpret_cast<float*>(PyArray_DATA(ca));
+    }
+    if(!PyArray_Check(oia.ptr())) {
+      throw std::runtime_error("oia is not a numpy array");
+    }
+    PyArrayObject* ia=reinterpret_cast<PyArrayObject*>(oia.ptr());
+    if(!PyArray_ISCONTIGUOUS(ia)) {
+      throw std::runtime_error("expected vertex array to be contiguous");
+    }
+    if(!PyArray_TYPE(ia)==NPY_UINT) {
+      throw std::runtime_error("expected vertex array to be of dtype=uint32");
+    }
+    size_t i_size=PyArray_SIZE(ia);
+    unsigned int* ip=reinterpret_cast<unsigned int*>(PyArray_DATA(ia));
+
+    p.AddMesh(vp,np,cp,v_count,ip,i_size/3);
+#else
+    throw std::runtime_error("AddMesh requires compiled-in numpy support");
+#endif
+  }
+}
+
 void export_primlist()
 {
+  import_array(); // magic handshake for numpy module
+
   class_<PrimList, bases<GfxObj>, boost::shared_ptr<PrimList>, boost::noncopyable>("PrimList", init<const String& >())
     .def("Clear",&PrimList::Clear)
     .def("_add_line",&PrimList::AddLine)
@@ -34,6 +114,7 @@ void export_primlist()
     .def("_add_sphere",&PrimList::AddSphere)
     .def("_add_cyl",&PrimList::AddCyl)
     .def("_add_text",&PrimList::AddText)
+    .def("AddMesh",add_mesh)
     .def("SetColor",&PrimList::SetColor)
     .def("SetDiameter",&PrimList::SetDiameter)
     .def("SetRadius",&PrimList::SetRadius)
diff --git a/modules/gfx/src/prim_list.cc b/modules/gfx/src/prim_list.cc
index 6113aecfb45e02867496f1c6129a92d9da8db2a1..a9debc0edd67dfa55d9a4057fb9ad0b77bdf4d2f 100644
--- a/modules/gfx/src/prim_list.cc
+++ b/modules/gfx/src/prim_list.cc
@@ -38,7 +38,8 @@ PrimList::PrimList(const String& name):
   texts_(),
   sphere_detail_(4),
   arc_detail_(4),
-  simple_va_()
+  simple_va_(),
+  vas_()
 {}
 
 void PrimList::Clear()
@@ -48,13 +49,14 @@ void PrimList::Clear()
   spheres_.clear();
   cyls_.clear();
   texts_.clear();
+  vas_.clear();
   Scene::Instance().RequestRedraw();
   this->FlagRebuild();
 }
 
 geom::AlignedCuboid PrimList::GetBoundingBox() const
 {
-  if(points_.empty() && lines_.empty() && spheres_.empty() && cyls_.empty()) {
+  if(points_.empty() && lines_.empty() && spheres_.empty() && cyls_.empty() && texts_.empty() && vas_.empty()) {
     return geom::AlignedCuboid(geom::Vec3(-1,-1,-1),geom::Vec3(1,1,1));
   }
   geom::Vec3 minc(std::numeric_limits<float>::max(),
@@ -101,6 +103,11 @@ void PrimList::ProcessLimits(geom::Vec3& minc, geom::Vec3& maxc,
     minc=geom::Min(minc,tpos);
     maxc=geom::Max(maxc,tpos);
   }
+  for(std::vector<IndexedVertexArray>::const_iterator it=vas_.begin();it!=vas_.end();++it) {
+    geom::AlignedCuboid bb=it->GetBoundingBox();
+    minc=geom::Min(minc,bb.GetMin());
+    maxc=geom::Max(maxc,bb.GetMax());
+  }
   minc-=1.0;
   maxc+=1.0;
 }
@@ -152,12 +159,15 @@ void PrimList::CustomRenderGL(RenderPass pass)
     va_.RenderGL();
     simple_va_.RenderGL();
     render_text();
+    for(std::vector<IndexedVertexArray>::iterator it=vas_.begin();it!=vas_.end();++it) {
+      it->RenderGL();
+    }
   }
 }
 
 void PrimList::CustomRenderPov(PovState& pov)
 {
-  if(points_.empty() && lines_.empty()) return;
+  if(points_.empty() && lines_.empty() && spheres_.empty() && cyls_.empty()) return;
   pov.write_merge_or_union(GetName());
 
   for(SpherePrimList::const_iterator it=points_.begin();it!=points_.end();++it) {
@@ -262,6 +272,42 @@ void PrimList::SetColor(const Color& c)
   FlagRebuild();
 }
 
+void PrimList::AddMesh(float* v, float* n, float* c, size_t nv, unsigned int* i, size_t ni)
+{
+  static float dummy_normal[]={0.0,0.0,1.0};
+  static float dummy_color[]={1.0,1.0,1.0,1.0};
+  vas_.push_back(IndexedVertexArray());
+  IndexedVertexArray& va=vas_.back();
+  va.SetLighting(true);
+  va.SetTwoSided(true);
+  va.SetColorMaterial(true);
+  va.SetCullFace(false);
+  float* vv=v;
+  float* nn=n;
+  if(!n) {
+    nn=dummy_normal;
+    va.SetLighting(false);
+  }
+  float* cc=c;
+  if(!c) {
+    cc=dummy_color;
+  }
+  for(size_t k=0;k<nv;++k) {
+    va.Add(geom::Vec3(vv[0],vv[1],vv[2]),
+           geom::Vec3(nn[0],nn[1],nn[2]),
+           Color(cc[0],cc[1],cc[2],cc[3]));
+    vv+=3;
+    if(n) nn+=3;
+    if(c) cc+=4;
+  }
+  unsigned int* ii=i;
+  for(size_t k=0;k<ni;++k) {
+    va.AddTri(ii[0],ii[1],ii[2]);
+    ii+=3;
+  }
+  Scene::Instance().RequestRedraw();
+  FlagRebuild();
+}
 
 ////////////////////////////////
 // private methods
diff --git a/modules/gfx/src/prim_list.hh b/modules/gfx/src/prim_list.hh
index 4e8a4c297ea044eda07d2cc199a4988b3d7e4e23..395e53e2ad22ce359b37281caead22cad1a423e9 100644
--- a/modules/gfx/src/prim_list.hh
+++ b/modules/gfx/src/prim_list.hh
@@ -139,6 +139,24 @@ class DLLEXPORT_OST_GFX PrimList: public GfxObj
 
   // TODO: add point and line pixel width
 
+  /*!
+    \brief add triangle mesh
+
+    v : pointer to nv*3 floats for the positions (mandatory)
+    n : pointer to nv*3 floats for the normals (may be NULL)
+    c : pointer to nv*4 floats for the colors (may be NULL)
+    nv: number of vertices, normals, and colors
+    i : pointer to ni*3 vertex indices
+    ni: number of index triplets
+
+    Python interface, using numpy arrays:
+
+      AddMesh(vertex_array, normal_array, color_array, index_array)
+
+    where normal_array and color_array may be None
+  */
+  void AddMesh(float* v, float* n, float* c, size_t nv, unsigned int* i, size_t ni);
+
  protected:
   virtual void CustomPreRenderGL(bool flag);
 
@@ -152,6 +170,8 @@ class DLLEXPORT_OST_GFX PrimList: public GfxObj
   unsigned int arc_detail_;
 
   IndexedVertexArray simple_va_;
+
+  std::vector<IndexedVertexArray> vas_;
   
   void prep_simple_va();
   void prep_va();
diff --git a/modules/gfx/tests/test_gfx.py b/modules/gfx/tests/test_gfx.py
index b6911eee12fc2a5401a11d1284dc14d1eb7e3f17..ff6e462e7f73f9478073618398b3700149c156a2 100644
--- a/modules/gfx/tests/test_gfx.py
+++ b/modules/gfx/tests/test_gfx.py
@@ -9,6 +9,11 @@ import ost.mol as mol
 import ost.gfx as gfx
 import ost.geom as geom
 
+has_numpy=True
+import numpy
+if not has_numpy:
+  has_numpy=False
+ 
 def col_delta(c1,c2):
   return geom.Distance(geom.Vec3(c1[0],c1[1],c1[2]),geom.Vec3(c2[0],c2[1],c2[2]))
 
@@ -71,6 +76,16 @@ class TestGfx(unittest.TestCase):
     pl.AddCyl(geom.Vec3(0,0,0),geom.Vec3(1,2,3),radius1=0.5,radius2=0.1,color1=gfx.BLUE,color2=gfx.GREEN)
     pl.AddText("foo",[0,2,3])
     pl.AddText("bar",[-2,0,0],color=gfx.WHITE,point_size=8)
+    if has_numpy:
+      pl.AddMesh(numpy.zeros((5,3),dtype=numpy.float32),
+                 numpy.zeros((5,3),dtype=numpy.float32),
+                 numpy.zeros((5,4),dtype=numpy.float32),
+                 numpy.zeros((2,3),dtype=numpy.uint32))
+      pl.AddMesh(numpy.zeros((7,3),dtype=numpy.float32),
+                 None,
+                 None,
+                 numpy.zeros((4,3),dtype=numpy.uint32))
+                 
 
 if __name__== '__main__':
   unittest.main()