1818# under the License.
1919
2020import argparse
21- import bpy
2221import mujoco
2322import os
2423import pathlib
3231import math
3332
3433import numpy as np
34+ import trimesh # Added trimesh
3535
3636from urdf_parser_py .urdf import URDF
3737
@@ -52,11 +52,11 @@ def add_mujoco_info(raw_xml, output_filepath, publish_topic):
5252
5353 # Use relative path for fixed directory otherwise use absolute path
5454 if not publish_topic :
55- meshdir = "assets"
55+ asset_dir = "assets"
5656 else :
57- meshdir = os .path .join (output_filepath , "assets" )
57+ asset_dir = os .path .join (output_filepath , "assets" )
5858
59- compiler_element .setAttribute ("meshdir " , meshdir )
59+ compiler_element .setAttribute ("assetdir " , asset_dir )
6060 compiler_element .setAttribute ("balanceinertia" , "true" )
6161 compiler_element .setAttribute ("discardvisual" , "false" )
6262 compiler_element .setAttribute ("strippath" , "false" )
@@ -191,9 +191,79 @@ def replace_package_names(xml_data):
191191 return xml_data
192192
193193
194+ # get required images from dae so that we can copy them to the temporary filepath
195+ def get_images_from_dae (dae_path ):
196+
197+ dae_dir = os .path .dirname (dae_path )
198+
199+ doc = minidom .parse (dae_path )
200+
201+ image_paths = []
202+ seen = set ()
203+
204+ # access data from dae files with this structure to access image_filepath
205+ # <library_images>
206+ # <image id="id" name="name">
207+ # <init_from>image_filepath</init_from>
208+ # </image>
209+ # </library_images>
210+ for image in doc .getElementsByTagName ("image" ):
211+ init_from_elems = image .getElementsByTagName ("init_from" )
212+ if not init_from_elems :
213+ continue
214+
215+ path = init_from_elems [0 ].firstChild
216+ if path is None :
217+ continue
218+
219+ image_path = path .nodeValue .strip ()
220+
221+ # Resolve relative paths
222+ if not os .path .isabs (image_path ):
223+ image_path = os .path .normpath (os .path .join (dae_dir , image_path ))
224+
225+ # make sure it is an image
226+ if image_path .lower ().endswith ((".png" , ".jpg" , ".jpeg" )):
227+ # ignore duplucates
228+ if image_path not in seen :
229+ seen .add (image_path )
230+ image_paths .append (image_path )
231+
232+ return image_paths
233+
234+
235+ # Change all files that match "material_{some_int}.{png, jpg, jpeg}"
236+ # in a specified directory to be "material_{modifier}_{some_int}.{png, jpg, jpeg}"
237+ # This is important because trimesh puts out materials that look like
238+ # material_{some_int}.{png, jpg, jpeg}, and they need to be indexed per item
239+ def rename_material_textures (dir_path , modifier ):
240+ dir_path = pathlib .Path (dir_path )
241+
242+ # regex to match files we want to modify
243+ pattern = re .compile (r"^material_(\d+)\.(png|jpg|jpeg)$" , re .IGNORECASE )
244+
245+ for path in dir_path .iterdir ():
246+ if not path .is_file ():
247+ continue
248+
249+ m = pattern .match (path .name )
250+ if not m :
251+ continue
252+
253+ # extract important components and reorder
254+ index , ext = m .groups ()
255+ new_name = f"material_{ modifier } _{ index } .{ ext } "
256+ new_path = path .with_name (new_name )
257+
258+ print (f"{ path .name } -> { new_name } " )
259+ path .rename (new_path )
260+
261+
194262def convert_to_objs (mesh_info_dict , directory , xml_data , convert_stl_to_obj , decompose_dict ):
195263 # keep track of files we have already processed so we don't do it again
196264 converted_filenames = []
265+ # keep track of what material number we are on to get unique materials
266+ mtl_num = 0
197267
198268 # clean assets directory and remake required paths
199269 if os .path .exists (f"{ directory } assets/" ):
@@ -212,10 +282,6 @@ def convert_to_objs(mesh_info_dict, directory, xml_data, convert_stl_to_obj, dec
212282
213283 print (f"processing { full_filepath } " )
214284
215- # Clear any existing objects in the scene
216- bpy .ops .object .select_all (action = "SELECT" )
217- bpy .ops .object .delete (use_global = False )
218-
219285 # if we want to decompose the mesh, put it in decomposed filepath, otherwise put it in full
220286 if filename_no_ext in decompose_dict :
221287 assets_relative_filepath = f"{ DECOMPOSED_PATH_NAME } /{ filename_no_ext } /"
@@ -224,22 +290,31 @@ def convert_to_objs(mesh_info_dict, directory, xml_data, convert_stl_to_obj, dec
224290 assets_relative_filepath = f"{ COMPOSED_PATH_NAME } /"
225291 assets_relative_filepath += filename_no_ext
226292
293+ output_path = f"{ directory } assets/{ assets_relative_filepath } .obj"
294+
227295 # Import the .stl or .dae file
228296 if filename_ext .lower () == ".stl" :
229297 if filename_no_ext in decompose_dict and not convert_stl_to_obj :
230298 raise ValueError ("The --convert_stl_to_obj argument must be specified to decompose .stl mesh" )
231299 if convert_stl_to_obj :
232- bpy .ops .wm .stl_import (filepath = full_filepath )
300+ # Load mesh using trimesh
301+ mesh = trimesh .load (full_filepath )
233302
234303 # bring in file color from urdf
235- new_mat = bpy .data .materials .new (name = f"material_{ filename_no_ext } " )
236- new_mat .diffuse_color = mesh_item ["color" ]
237- o = bpy .context .selected_objects [0 ]
238- o .active_material = new_mat
239-
240- bpy .ops .wm .obj_export (
241- filepath = f"{ directory } assets/{ assets_relative_filepath } .obj" , forward_axis = "Y" , up_axis = "Z"
242- )
304+ if "color" in mesh_item :
305+ # trimesh expects 0-255 uint8 for colors
306+ rgba = mesh_item ["color" ]
307+ # make a material for the rgba values and export it
308+ mtl_modifier = f"m{ mtl_num } "
309+ mtl_name = "mtl_" + mtl_modifier
310+ material = trimesh .visual .material .SimpleMaterial (name = mtl_name , diffuse = rgba ) # RGBA
311+ mesh .visual = trimesh .visual .TextureVisuals (material = material )
312+
313+ # increment material number
314+ mtl_num = mtl_num + 1
315+
316+ # Export to OBJ
317+ mesh .export (output_path , include_color = True , mtl_name = mtl_name )
243318 xml_data = xml_data .replace (full_filepath , f"{ assets_relative_filepath } .obj" )
244319
245320 else :
@@ -258,24 +333,66 @@ def convert_to_objs(mesh_info_dict, directory, xml_data, convert_stl_to_obj, dec
258333 pass
259334 # objs are ok as is
260335 elif filename_ext .lower () == ".dae" :
336+ # keep track of the image files that we need to copy from the dae
337+ image_files = get_images_from_dae (full_filepath )
338+ # keep track of the files that were copied to tmp directory to delete
339+ copied_image_files = []
340+
261341 # set z axis to up in the dae file because that is how mujoco expects it
262342 z_up_dae_txt = set_up_axis_to_z_up (full_filepath )
263343
264344 # make a temporary file rather than overwriting the old one
265- temp_file = tempfile .NamedTemporaryFile (suffix = ".dae" , delete = False )
345+ temp_file = tempfile .NamedTemporaryFile (suffix = ".dae" , mode = "w+" , delete = False )
266346 temp_filepath = temp_file .name
347+ temp_folder = os .path .dirname (temp_filepath )
267348 try :
268- with open (temp_filepath , "w" ) as f :
269- f .write (z_up_dae_txt )
270- # import into blender
271- bpy .ops .wm .collada_import (filepath = temp_filepath )
272- finally :
349+ temp_file .write (z_up_dae_txt )
273350 temp_file .close ()
274- os .remove (temp_filepath )
275351
276- bpy .ops .wm .obj_export (
277- filepath = f"{ directory } assets/{ assets_relative_filepath } .obj" , forward_axis = "Y" , up_axis = "Z"
278- )
352+ # copy relevant images into the temporary directory
353+ for image_file in image_files :
354+ shutil .copy2 (image_file , temp_folder )
355+ copied_image_files .append (f"{ temp_folder } /{ os .path .basename (image_file )} " )
356+
357+ # Load scene using trimesh
358+ scene = trimesh .load (temp_filepath , force = trimesh .scene )
359+
360+ # give the material a unique name so that it can be properly referenced
361+ mtl_modifier = f"m{ mtl_num } "
362+ mtl_name = "mtl_" + mtl_modifier
363+ mtl_filepath = os .path .dirname (output_path ) + f"/{ mtl_name } "
364+
365+ scene .export (output_path , include_color = True , mtl_name = mtl_name )
366+ # rename textures
367+ rename_material_textures (dir_path = os .path .dirname (output_path ), modifier = mtl_modifier )
368+
369+ # we need to modify the material names to not all be material_X so they don't conflict
370+ # all of the objs will have a line that looks like
371+ # usemtl material_0
372+ # and all of the mtl files will have a line that looks like
373+ # newmtl material_0
374+ # We will modify both of them to be numberd by mtl_num like so
375+ # usemtl material_{mtl_num}_0
376+ # newmtl material_{mtl_num}_0
377+
378+ if os .path .exists (mtl_filepath ):
379+ for filepath in [mtl_filepath , output_path ]:
380+ with open (filepath ) as f :
381+ data = f .read ()
382+ data = data .replace ("material_" , f"material_{ mtl_modifier } _" )
383+ with open (filepath , "w" ) as f :
384+ f .write (data )
385+ # increment
386+ mtl_num = mtl_num + 1
387+ finally :
388+ if os .path .exists (temp_filepath ):
389+ os .remove (temp_filepath )
390+
391+ # get rid of copied image files in the temporary file directory
392+ for copied_image_file in copied_image_files :
393+ if os .path .exists (copied_image_file ):
394+ os .remove (copied_image_file )
395+
279396 xml_data = xml_data .replace (full_filepath , f"{ assets_relative_filepath } .obj" )
280397 else :
281398 print (f"Can't convert { full_filepath } \n \t Only stl and dae file extensions are supported at the moment" )
@@ -446,6 +563,17 @@ def update_obj_assets(dom, output_filepath, mesh_info_dict):
446563 for sub_material in sub_materials :
447564 asset_element .appendChild (sub_material )
448565
566+ # bring in the textures, and modify filepath to properly reference filepaths
567+ sub_textures = sub_asset_element .getElementsByTagName ("texture" )
568+ for sub_texture in sub_textures :
569+ if sub_texture .hasAttribute ("file" ):
570+ sub_texture_file = sub_texture .getAttribute ("file" )
571+ if composed_type == DECOMPOSED_PATH_NAME :
572+ sub_texture .setAttribute ("file" , f"{ composed_type } /{ mesh_name } /{ mesh_name } /{ sub_texture_file } " )
573+ else :
574+ sub_texture .setAttribute ("file" , f"{ composed_type } /{ mesh_name } /{ sub_texture_file } " )
575+ asset_element .appendChild (sub_texture )
576+
449577 sub_body = sub_dom .getElementsByTagName ("body" )
450578 sub_body = sub_body [0 ]
451579
0 commit comments