Repository: vispy/vispy

Issue:
meshio for i/o
Main author of [meshio](https://github.com/nschloe/meshio) here.

meshio can read a large number of formats which are applicable for vispy, I think it would be a good match for meshio.


Discussion:
Great! Glad to have you here. Do you have specific interfaces in mind? How do you see VisPy users using meshio?

Right now we have a couple utility functions that use other libraries like:

https://github.com/vispy/vispy/blob/master/vispy/io/mesh.py

Which uses some functions created for reading STL files which was copied from the trimesh project:

https://github.com/vispy/vispy/blob/master/vispy/io/stl.py

There are also some wavefront readers and writers:

https://github.com/vispy/vispy/blob/master/vispy/io/wavefront.py
> How do you see VisPy users using meshio?

Well, how do users use `load_stl` right now? _Edit:_ Via `read_mesh` it seems:
```python
import vispy.io

mesh = vispy.io.read_mesh("out.stl")
print(mesh)
```
So, I imagine the same thing with the difference that you can put just about anything for `out.stl`. One difficulty I do see is the fact that your reader always returns outward normals and "texture coordinates". Is this a strict requirement? Also, do you _need_ triangles or can you handle other shapes are well?
@larsoner or @kmuehlbauer might know more, otherwise there may be existing pull requests or issues that have users who are more familiar with this. I don't really use meshes and am not familiar with the various ways (common or not) that they are described. I believe we read things this way because it is directly passed to our MeshVisual object. I wouldn't be surprised if this is limited to triangles. The texture coordinates might be flexible, but it really depends on what the [MeshVisual](https://github.com/vispy/vispy/blob/master/vispy/visuals/mesh.py#L150) and its lower-level object [MeshData](https://github.com/vispy/vispy/blob/master/vispy/geometry/meshdata.py#L24) can handle.

Note: There are some very important changes being worked on in open PRs (https://github.com/vispy/vispy/pulls?utf8=%E2%9C%93&q=is%3Apr+is%3Aopen+mesh). They are missing a few changes, fixes, and tests before they can be merged but they may make some of this more possible.
@nschloe There were quite some attempts to improve on mesh loading, afair. I'm too not involved too much into meshes in general. But it would be nice, if meshio could be used to serve MishVisual/MeshData the needed values.

As @djhoese already mentioned, there are open PR which change Mesh Handling within vispy, ping @asnt. 
@nschloe Please have a look at #1665 which mentions some ides/wishes for improvement on loading meshes.

I think that a loading mechanism that returns all the mesh info with a predictable and systematic data structure would be welcome. It seems meshio can provide some of that with `mesh.points`, `mesh.cells`... but I don't see support for other attribute arrays and data (texture images, texture coordinates...) in the [OBJ loader](https://github.com/nschloe/meshio/blob/3c12f1598e6a01ef26f9b12c94ebfa358d4c547f/meshio/obj/_obj.py#L22), for example. Did I miss it or is it planned? Is it implemented for other mesh formats maybe?

> One difficulty I do see is the fact that your reader always returns outward normals and "texture coordinates". Is this a strict requirement?

The current loaders return a tuple `(vertices, faces, normals, texcoords)`. This is restrictive to specific cases. A more flexible structure would be welcome.

> Also, do you need triangles or can you handle other shapes are well?

I am not sure but I think most of vispy assumes triangles as primitives. Being able to load quads would open the door to variations, though. (At least, we would be able to display quad meshes by converting them as triangle meshes internally. That might be OK for some applications.)
>  but I don't see support for other attribute arrays and data (texture images, texture coordinates...) 

There is `mesh.cell_data`, `mesh.point_data` etc. which host such things. It is possible that some readers haven't implemented support for this yet (e.g., obj files), but this would definitely be something we want are are going to have in meshio.

> I am not sure but I think most of vispy assumes triangles as primitives.

That's fair enough. meshio supports many more cell types (3D too, like tetrahedra, pyramids etc.), but it's easy to just single out the triangles in a file.
I think VisPy's preference for triangles is that it tries to keep OpenGL 2.x and OpenGL ES support. I *think* older GPUs had a limitation on triangles only and quads were just made up of two triangles. As @asnt points out we could convert internally if needed.
Is it a problem that meshio supports Python 3.5+ only? (Not Python 2.7.)
The next release of vispy will be likely be 3.6+ (definitely not 2.7).

Resolving patch:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -222,6 +222,7 @@ def run(self):
         'sdl2': ['PySDL2'],
         'wx': ['wxPython'],
         'doc': ['sphinx_bootstrap_theme', 'numpydoc'],
+        'io': ['meshio'],
     },
     packages=find_packages(exclude=['make']),
     ext_modules=cythonize(extensions),
diff --git a/vispy/io/mesh.py b/vispy/io/mesh.py
--- a/vispy/io/mesh.py
+++ b/vispy/io/mesh.py
@@ -5,6 +5,7 @@
 """ Reading and writing of data like images and meshes.
 """
 
+import os
 from os import path as op
 
 from .wavefront import WavefrontReader, WavefrontWriter
@@ -46,14 +47,26 @@ def read_mesh(fname):
         normals = mesh['face_normals']
         texcoords = None
         return vertices, faces, normals, texcoords
-    elif not format:
-        raise ValueError('read_mesh needs could not determine format.')
     else:
-        raise ValueError('read_mesh does not understand format %s.' % fmt)
+        try:
+            import meshio
+        except ImportError:
+            raise ValueError('read_mesh does not understand format %s.' % fmt)
+
+        try:
+            mesh = meshio.read(fname)
+        except meshio.ReadError:
+            raise ValueError('read_mesh does not understand format %s.' % fmt)
+
+        triangles = mesh.get_cells_type("triangle")
+        if len(triangles) == 0:
+            raise ValueError('mesh file does not contain triangles.')
+
+        return mesh.points, triangles, None, None
 
 
 def write_mesh(fname, vertices, faces, normals, texcoords, name='',
-               format='obj', overwrite=False, reshape_faces=True):
+               format=None, overwrite=False, reshape_faces=True):
     """ Write mesh data to file.
 
     Parameters
@@ -82,8 +95,29 @@ def write_mesh(fname, vertices, faces, normals, texcoords, name='',
     if op.isfile(fname) and not overwrite:
         raise IOError('file "%s" exists, use overwrite=True' % fname)
 
+    if format is None:
+        format = os.path.splitext(fname)[1][1:]
+
     # Check format
-    if format not in ('obj'):
-        raise ValueError('Only "obj" format writing currently supported')
-    WavefrontWriter.write(fname, vertices, faces,
-                          normals, texcoords, name, reshape_faces)
+    if format == 'obj':
+        WavefrontWriter.write(fname, vertices, faces,
+                              normals, texcoords, name, reshape_faces)
+        return
+
+    try:
+        import meshio
+    except ImportError:
+        raise ValueError('write_mesh does not understand format %s.' % format)
+
+    cell_data = {}
+    if normals is not None:
+        cell_data["normals"] = [normals]
+    if texcoords is not None:
+        cell_data["texcoords"] = [texcoords]
+
+    mesh = meshio.Mesh(vertices, [("triangle", faces)], cell_data=cell_data)
+
+    try:
+        mesh.write(fname, file_format=format)
+    except meshio.WriteError:
+        raise ValueError('write_mesh does not understand format %s.' % format)


Test patch:
diff --git a/vispy/io/tests/test_io.py b/vispy/io/tests/test_io.py
--- a/vispy/io/tests/test_io.py
+++ b/vispy/io/tests/test_io.py
@@ -64,6 +64,26 @@ def test_wavefront_non_triangular():
     assert lines[-2].startswith('f 2 1 8 7 6 4')
 
 
+def test_meshio():
+    '''Test meshio i/o'''
+    vertices = np.array([[0.0, 0.0, 0.0],
+                         [1.0, 0.0, 0.],
+                         [-.0, 1.0, 0.],
+                         [1.0, 1.0, 0.]])
+
+    faces = np.array([[0, 1, 3],
+                      [1, 2, 3]])
+    fname_out = op.join(temp_dir, 'temp.vtk')
+    write_mesh(fname_out, vertices=vertices,
+               faces=faces, normals=None,
+               texcoords=None, overwrite=True,
+               reshape_faces=False)
+    out_vertices, out_faces, _, _ = read_mesh(fname_out)
+
+    assert np.all(np.abs(out_vertices - vertices) < 1.0e-14)
+    assert np.all(out_faces == faces)
+
+
 def _slow_calculate_normals(rr, tris):
     """Efficiently compute vertex normals for triangulated surface"""
     # first, compute triangle normals