Skip to content

Commit 01e34c4

Browse files
Use trimesh instead of bpy (#35)
--------- Co-authored-by: Nathan Dunkelberger <nathan.b.dunkelberger@nasa.gov>
1 parent 3c19c9b commit 01e34c4

File tree

2 files changed

+158
-27
lines changed

2 files changed

+158
-27
lines changed

package.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
<test_depend>urdfdom_py</test_depend>
2727
<test_depend>xacro</test_depend>
2828

29+
<exec_depend>python3-collada-pip</exec_depend>
30+
<exec_depend>python3-scipy</exec_depend>
31+
2932
<export>
3033
<build_type>ament_cmake</build_type>
3134
</export>

scripts/make_mjcf_from_robot_description.py

Lines changed: 155 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
# under the License.
1919

2020
import argparse
21-
import bpy
2221
import mujoco
2322
import os
2423
import pathlib
@@ -32,6 +31,7 @@
3231
import math
3332

3433
import numpy as np
34+
import trimesh # Added trimesh
3535

3636
from 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+
194262
def 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\tOnly 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

Comments
 (0)