Repository: mikedh/trimesh

Issue:
export textures to obj file
The support for vt in obj export seems to be commented out
https://github.com/mikedh/trimesh/blob/0b08484e64f0d65c00a04e2b0cf96424515aed03/trimesh/exchange/obj.py#L741
Is this code deprecated? Is there a better method to mesh with uv map to obj file?

Discussion:

Patch:
diff --git a/.travis.yml b/.travis.yml
index 86a27318..90c805f1 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -57,20 +57,22 @@ before_install:
   -P /usr/bin
 - sudo chmod +x /usr/bin/testVHACD
 
+
 install:
 - if [[ "$PYTHON_VERSION" == "2.7" ]]; then wget --quiet https://repo.continuum.io/miniconda/Miniconda2-latest-Linux-x86_64.sh
-  -O miniconda.sh; else wget --quiet https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh
+  -O miniconda.sh; else wget --quiet https://repo.anaconda.com/miniconda/Miniconda3-4.6.14-Linux-x86_64.sh
   -O miniconda.sh; fi
 - bash miniconda.sh -b -p $HOME/miniconda
 - export PATH="$HOME/miniconda/bin:$PATH"
 - conda config --set always_yes yes --set changeps1 no
 - conda config --add channels conda-forge
-- conda update -q conda
 - conda info -a
 - conda create -q -n test-environment python=$PYTHON_VERSION
 - source activate test-environment
 # make sure we parked our car in the right spot
 - python -c "import os,sys;assert '{}.{}'.format(*sys.version_info[:2])==os.environ['PYTHON_VERSION']"
+# lock pip version for python 3.4
+- if [[ "$PYTHON_VERSION" == "3.4" ]]; then conda install pip===10.0.1; fi
 # only install the basic version of the library to make sure
 # trimesh imports and functions with only the minimal install
 - pip install .
diff --git a/trimesh/exchange/export.py b/trimesh/exchange/export.py
index f395855f..5f412287 100644
--- a/trimesh/exchange/export.py
+++ b/trimesh/exchange/export.py
@@ -7,7 +7,7 @@ from .. import util
 
 from .urdf import export_urdf  # NOQA
 from .gltf import export_glb, export_gltf
-from .obj import _obj_exporters
+from .obj import export_obj
 from .off import _off_exporters
 from .stl import export_stl, export_stl_ascii
 from .ply import _ply_exporters
@@ -194,13 +194,13 @@ _mesh_exporters = {
     'dict': export_dict,
     'json': export_json,
     'glb': export_glb,
+    'obj': export_obj,
     'gltf': export_gltf,
     'dict64': export_dict64,
     'msgpack': export_msgpack,
     'stl_ascii': export_stl_ascii
 }
 _mesh_exporters.update(_ply_exporters)
-_mesh_exporters.update(_obj_exporters)
 _mesh_exporters.update(_off_exporters)
 _mesh_exporters.update(_collada_exporters)
 _mesh_exporters.update(_xyz_exporters)
diff --git a/trimesh/exchange/obj.py b/trimesh/exchange/obj.py
index ea6491fd..4f3f9c9b 100644
--- a/trimesh/exchange/obj.py
+++ b/trimesh/exchange/obj.py
@@ -1,5 +1,5 @@
-import numpy as np
 import collections
+import numpy as np
 
 try:
     import PIL.Image as Image
@@ -688,7 +688,8 @@ def _preprocess_faces(text, split_object=False):
 
 def export_obj(mesh,
                include_normals=True,
-               include_color=True):
+               include_color=True,
+               include_texture=False):
     """
     Export a mesh as a Wavefront OBJ file
 
@@ -696,11 +697,24 @@ def export_obj(mesh,
     -----------
     mesh : trimesh.Trimesh
       Mesh to be exported
+    include_normals : bool
+      Inlcude vertex normals in export
+    include_color : bool
+      Include vertex color in export
+    include_texture bool
+      Include texture in export?
+      False by default as it will change the return
+      values to include files that must be saved in the
+      same location as the exported mesh.
 
     Returns
     -----------
     export : str
       OBJ format output
+    texture : dict
+      [OPTIONAL]
+      Contains files that need to be saved in the same
+      directory as the exported mesh: {file name : bytes}
     """
     # store the multiple options for formatting
     # vertex indexes for faces
@@ -722,45 +736,59 @@ def export_obj(mesh,
         v_blob = mesh.vertices
 
     # add the first vertex key and convert the array
-    export = 'v ' + util.array_to_string(v_blob,
-                                         col_delim=' ',
-                                         row_delim='\nv ',
-                                         digits=8) + '\n'
+    export = collections.deque(
+        ['v ' + util.array_to_string(
+            v_blob,
+            col_delim=' ',
+            row_delim='\nv ',
+            digits=8) + '\n'])
 
     # only include vertex normals if they're already stored
     if include_normals and 'vertex_normals' in mesh._cache:
         # if vertex normals are stored in cache export them
         face_type.append('vn')
-        export += 'vn '
-        export += util.array_to_string(mesh.vertex_normals,
-                                       col_delim=' ',
-                                       row_delim='\nvn ',
-                                       digits=8) + '\n'
-
-    """
-    TODO: update this to use TextureVisuals
-    if include_texture:
-        # if vertex texture exists and is the right shape export here
+        export.append('vn ' + util.array_to_string(
+            mesh.vertex_normals,
+            col_delim=' ',
+            row_delim='\nvn ',
+            digits=8) + '\n')
+
+    tex_data = None
+    if include_texture and hasattr(mesh.visual, 'uv'):
+        # if vertex texture exists and is the right shape
         face_type.append('vt')
-        export += 'vt '
-
-        export += util.array_to_string(mesh.metadata['vertex_texture'],
-                                       col_delim=' ',
-                                       row_delim='\nvt ',
-                                       digits=8) + '\n'
-    """
+        # add the uv coordinates
+        export.append('vt ' + util.array_to_string(
+            mesh.visual.uv,
+            col_delim=' ',
+            row_delim='\nvt ',
+            digits=8) + '\n')
+        tex_data, tex_name, mtl_name = mesh.visual.material.to_obj()
+        # add the reference to the MTL file
+        export.appendleft('mtllib {}'.format(mtl_name))
+        # add the directive to use the exported material
+        export.appendleft('usemtl {}'.format(tex_name))
 
     # the format for a single vertex reference of a face
     face_format = face_formats[tuple(face_type)]
-    faces = 'f ' + util.array_to_string(mesh.faces + 1,
-                                        col_delim=' ',
-                                        row_delim='\nf ',
-                                        value_format=face_format)
     # add the exported faces to the export
-    export += faces
+    export.append('f ' + util.array_to_string(
+        mesh.faces + 1,
+        col_delim=' ',
+        row_delim='\nf ',
+        value_format=face_format))
+
+    # add a created-with header to the top of the file
+    export.appendleft('# https://github.com/mikedh/trimesh')
+
+    # combine elements into a single string
+    export = '\n'.join(export)
+
+    # if we exported texture it changes returned values
+    if include_texture and tex_data is not None:
+        return export, tex_data
 
     return export
 
 
 _obj_loaders = {'obj': load_obj}
-_obj_exporters = {'obj': export_obj}
diff --git a/trimesh/util.py b/trimesh/util.py
index d310026e..772462a8 100644
--- a/trimesh/util.py
+++ b/trimesh/util.py
@@ -2145,7 +2145,14 @@ class FunctionRegistry(Mapping):
 class TemporaryDirectory(object):
     """
     Same basic usage as tempfile.TemporaryDirectory
-    but functional in Python 2.7+
+    but functional in Python 2.7+.
+
+    Example
+    ---------
+    ```
+    with trimesh.util.TemporaryDirectory() as path:
+       writable = os.path.join(path, 'hi.txt')
+    ```
     """
 
     def __enter__(self):
diff --git a/trimesh/version.py b/trimesh/version.py
index afee0c7b..759f5368 100644
--- a/trimesh/version.py
+++ b/trimesh/version.py
@@ -1,1 +1,1 @@
-__version__ = '3.5.18'
+__version__ = '3.5.19'
diff --git a/trimesh/visual/material.py b/trimesh/visual/material.py
index f4e90e21..dc3d670a 100644
--- a/trimesh/visual/material.py
+++ b/trimesh/visual/material.py
@@ -7,6 +7,7 @@ Store visual materials as objects.
 import numpy as np
 
 from . import color
+from .. import util
 
 
 class Material(object):
@@ -51,6 +52,59 @@ class SimpleMaterial(Material):
     def to_color(self, uv):
         return color.uv_to_color(uv, self.image)
 
+    def to_obj(self, tex_name=None, mtl_name=None):
+        """
+        Convert the current material to an OBJ format material.
+
+        Parameters
+        -----------
+        name : str or None
+          Name to apply to the material
+
+        Returns
+        -----------
+        tex_name : str
+          Name of material
+        mtl_name : str
+          Name of mtl file in files
+        files : dict
+          Data as {file name : bytes}
+        """
+        # material parameters as 0.0-1.0 RGB
+        Ka = color.to_float(self.ambient)[:3]
+        Kd = color.to_float(self.diffuse)[:3]
+        Ks = color.to_float(self.specular)[:3]
+
+        if tex_name is None:
+            tex_name = 'material0'
+        if mtl_name is None:
+            mtl_name = '{}.mtl'.format(tex_name)
+
+        # what is the name of the export image to save
+        image_name = '{}.{}'.format(
+            tex_name, self.image.format).lower()
+
+        # create an MTL file
+        mtl = '\n'.join(
+            ['# https://github.com/mikedh/trimesh',
+             'newmtl {}'.format(tex_name),
+             'Ka {} {} {}'.format(*Ka),
+             'Kd {} {} {}'.format(*Kd),
+             'Ks {} {} {}'.format(*Ks),
+             'Ns {}'.format(self.glossiness),
+             'map_Kd {}'.format(image_name)])
+
+        # save the image texture as bytes in the original format
+        f_obj = util.BytesIO()
+        self.image.save(fp=f_obj, format=self.image.format)
+        f_obj.seek(0)
+
+        # collect the OBJ data into files
+        data = {mtl_name: mtl.encode('utf-8'),
+                image_name: f_obj.read()}
+
+        return data, tex_name, mtl_name
+
     def __hash__(self):
         """
         Provide a hash of the material so we can detect
diff --git a/trimesh/visual/resolvers.py b/trimesh/visual/resolvers.py
index 21c2db55..085575c8 100644
--- a/trimesh/visual/resolvers.py
+++ b/trimesh/visual/resolvers.py
@@ -9,10 +9,10 @@ import os
 
 # URL parsing for remote resources via WebResolver
 try:
-    # python 3
+    # Python 3
     from urllib.parse import urlparse, urljoin
 except ImportError:
-    # python 2
+    # Python 2
     from urlparse import urlparse, urljoin
 
 
@@ -120,8 +120,11 @@ class ZipResolver(Resolver):
                     break
         # get the stored data
         obj = self.archive[name]
+        # if the dict is storing data as bytes just reture
         if isinstance(obj, (bytes, str)):
             return obj
+
+        # otherwise get it as a file object
         # read file object from beginning
         obj.seek(0)
         # data is stored as a file object
diff --git a/trimesh/visual/texture.py b/trimesh/visual/texture.py
index 5b9b170d..15c83ea0 100644
--- a/trimesh/visual/texture.py
+++ b/trimesh/visual/texture.py
@@ -116,7 +116,8 @@ class TextureVisuals(object):
         if values is None:
             self._data.clear()
         else:
-            self._data['uv'] = np.asanyarray(values, dtype=np.float64)
+            self._data['uv'] = np.asanyarray(
+                values, dtype=np.float64)
 
     def copy(self):
         """


Test patch:
diff --git a/tests/test_obj.py b/tests/test_obj.py
index cbd6aa66..a0545c1b 100644
--- a/tests/test_obj.py
+++ b/tests/test_obj.py
@@ -114,10 +114,12 @@ class OBJTest(g.unittest.TestCase):
                                      for _ in range(len(mesh.vertices))]
         # export and then reload the file as OBJ
         rec = g.trimesh.load(
-            g.trimesh.util.wrap_as_stream(mesh.export(file_type='obj')),
+            g.trimesh.util.wrap_as_stream(
+                mesh.export(file_type='obj')),
             file_type='obj')
         # assert colors have survived the export cycle
-        assert (mesh.visual.vertex_colors == rec.visual.vertex_colors).all()
+        assert (mesh.visual.vertex_colors ==
+                rec.visual.vertex_colors).all()
 
     def test_single_vn(self):
         """
@@ -127,6 +129,29 @@ class OBJTest(g.unittest.TestCase):
         assert len(m.vertices) > 0
         assert len(m.faces) > 0
 
+    def test_mtl(self):
+        # get a mesh with texture
+        m = g.get_mesh('fuze.obj')
+        # export the mesh including data
+        obj, data = g.trimesh.exchange.export.export_obj(
+            m, include_texture=True)
+        with g.trimesh.util.TemporaryDirectory() as path:
+            # where is the OBJ file going to be saved
+            obj_path = g.os.path.join(path, 'test.obj')
+            with open(obj_path, 'w') as f:
+                f.write(obj)
+            # save the MTL and images
+            for k, v in data.items():
+                with open(g.os.path.join(path, k), 'wb') as f:
+                    f.write(v)
+            # reload the mesh from the export
+            rec = g.trimesh.load(obj_path)
+        # make sure loaded image is the same size as the original
+        assert (rec.visual.material.image.size ==
+                m.visual.material.image.size)
+        # make sure the faces are the same size
+        assert rec.faces.shape == m.faces.shape
+
 
 if __name__ == '__main__':
     g.trimesh.util.attach_to_log()