1616
1717import os
1818
19+ from sdformat_mjcf .sdformat_to_mjcf .mesh_io import convert_mesh_to_obj_multimesh
20+ from sdformat_mjcf .sdformat_to_mjcf .converters .material import add_material
1921import sdformat_mjcf .utils .sdf_utils as su
2022
2123COLLISION_GEOM_GROUP = 3
2224VISUAL_GEOM_GROUP = 0
2325
26+ def _add_geoms_after_conversion (body , name , pose , conversion_output ,
27+ is_visual ):
28+ geom_list = []
29+ for path , info in conversion_output .obj_files .items ():
30+ geom = body .add (
31+ "geom" ,
32+ name = su .find_unique_name (body , "geom" , name ),
33+ pos = su .vec3d_to_list (pose .pos ()),
34+ euler = su .quat_to_euler_list (pose .rot ()),
35+ )
36+ geom .type = "mesh"
37+ file_without_extension = os .path .splitext (
38+ os .path .basename (path ))[0 ]
39+ asset_loaded = geom .root .asset .find ('mesh' , file_without_extension )
40+ if asset_loaded is None :
41+ geom .mesh = geom .root .asset .add ('mesh' , file = path )
42+ if is_visual :
43+ # Some visual meshes are too thin and can cause inertia
44+ # calculation in Mujoco to fail. Mark them as "shell" to prevent
45+ # Mujoco from failing in compilation. The visual geometry will
46+ # not be used for inertia computation anyway since we explicitly
47+ # convert inertia from sdformat to mjcf.
48+ # https://github.com/google-deepmind/mujoco/issues/2455
49+ geom .mesh .inertia = "shell"
50+ mat_asset = geom .root .asset .add ("material" ,
51+ name = "material_" + file_without_extension ,
52+ specular = info .mat .specular ,
53+ shininess = info .mat .shininess ,
54+ rgba = info .mat .rgba )
55+ geom .material = mat_asset
56+ else :
57+ geom .mesh = asset_loaded
58+ return geom_list
2459
25- def add_geometry (body , name , pose , sdf_geom ):
60+ def _validate_uri (uri ):
61+ if 'http://' in uri or 'https://' in uri :
62+ raise RuntimeError ("Fuel meshes are not yet supported" )
63+ if 'model://' in uri :
64+ prefix = 'model://'
65+ # TODO: Support sdf::ParserConfig::AddURIPath to resolve URIs.
66+ return uri .replace (prefix , '' , 1 )
67+ return uri
68+
69+ def _is_unsupported_mesh_geo (root , sdf_geom ):
70+ if not sdf_geom .mesh_shape ():
71+ return False
72+ mesh_shape = sdf_geom .mesh_shape ()
73+ uri = _validate_uri (mesh_shape .uri ())
74+ _ , basename = os .path .split (uri )
75+ filename , extension = os .path .splitext (basename )
76+ # Mujoco supports .obj, .stl and .msh
77+ # Out of these, .obj is only supported if it has a single mesh in it.
78+ # So treat it as unsupported for this check so these files are sanitized
79+ # for Mujoco.
80+ return extension not in [".stl" , ".msh" ]
81+
82+ def add_geometry (body , name , pose , sdf_geom , is_visual = False ):
2683 """
2784 Converts an SDFormat geometry to an MJCF geom and add it to the given body.
2885
@@ -74,22 +131,40 @@ def add_geometry(body, name, pose, sdf_geom):
74131 geom .size = [sphere_shape .radius ()]
75132 elif sdf_geom .mesh_shape ():
76133 mesh_shape = sdf_geom .mesh_shape ()
77- uri = mesh_shape .uri ()
134+ uri = _validate_uri (mesh_shape .uri ())
135+ if _is_unsupported_mesh_geo (sdf_geom ):
136+ raise RuntimeError (
137+ f"Call `convert_and_add_mesh` for unsupported mesh geo { uri } " )
78138 extension_tokens = os .path .basename (mesh_shape .uri ()).split ("." )
79139 if (len (extension_tokens ) == 1 ):
80140 raise RuntimeError ("Unable to find the mesh extension {}"
81141 .format (uri ))
82- file_without_extension = os .path .splitext (
83- os .path .basename (mesh_shape .uri ()))[0 ]
84- if 'http://' in uri or 'https://' in uri :
85- raise RuntimeError ("Fuel meshes are not yet supported" )
142+ file_without_extension = os .path .splitext (os .path .basename (uri ))[0 ]
86143 geom .type = "mesh"
87144 asset_loaded = geom .root .asset .find ('mesh' , file_without_extension )
88- dirname = os .path .dirname (mesh_shape .file_path ())
89- mesh_file_path = os .path .join (dirname , uri )
90145 if asset_loaded is None :
91- geom .mesh = geom .root .asset .add ('mesh' ,
92- file = mesh_file_path )
146+ dirname = os .path .dirname (mesh_shape .file_path ())
147+ mesh_file_path = os .path .join (dirname , uri )
148+ try :
149+ geom .mesh = geom .root .asset .add ('mesh' ,
150+ file = mesh_file_path )
151+ except ValueError as e :
152+ # Try converting mesh to .obj. This could result in multiple
153+ # geos, one per mesh in the input file.
154+ converted_path = os .path .join (dirname ,
155+ file_without_extension + ".obj" )
156+ print (f"Converting { mesh_file_path } to { converted_path } " )
157+ result = convert_mesh_to_obj_multimesh (mesh_file_path ,
158+ converted_path )
159+ if isinstance (result , str ):
160+ geom .mesh = geom .root .asset .add ('mesh' ,
161+ file = result )
162+ else :
163+ geom .remove ()
164+ return _add_geoms_after_conversion (body , name , pose ,
165+ result , is_visual )
166+ if is_visual :
167+ geom .mesh .inertia = "shell"
93168 else :
94169 geom .mesh = asset_loaded
95170 geom .mesh .scale = su .vec3d_to_list (mesh_shape .scale ())
@@ -99,6 +174,101 @@ def add_geometry(body, name, pose, sdf_geom):
99174
100175 return geom
101176
177+ def _add_mesh_geom_with_assets (body , name , pose , mjcf_mesh_asset ,
178+ is_visual = False , mjcf_material_asset = None ):
179+ geom = body .add (
180+ "geom" ,
181+ name = su .find_unique_name (body , "geom" , name ),
182+ pos = su .vec3d_to_list (pose .pos ()),
183+ euler = su .quat_to_euler_list (pose .rot ()),
184+ )
185+ geom .type = "mesh"
186+ if is_visual :
187+ # Some visual meshes are too thin and can cause inertia
188+ # calculation in Mujoco to fail. Mark them as "shell" to prevent
189+ # Mujoco from failing in compilation. The visual geometry will
190+ # not be used for inertia computation anyway since we explicitly
191+ # convert inertia from sdformat to mjcf.
192+ # https://github.com/google-deepmind/mujoco/issues/2455
193+ geom .mesh .inertia = "shell"
194+ geom .mesh = mjcf_mesh_asset
195+ if mjcf_material_asset :
196+ geom .material = mjcf_material_asset
197+ return geom
198+
199+ def convert_and_add_mesh (body , name , pose , sdf_mesh , is_visual = False ):
200+ # Check if asset was loaded already with the uri key. If so, just add the
201+ # asset:
202+ uri = _validate_uri (sdf_mesh .uri ())
203+ file_without_extension = os .path .splitext (os .path .basename (uri ))[0 ]
204+ mesh_asset_name = file_without_extension
205+ mesh_loaded = body .root .asset .find ('mesh' , mesh_asset_name )
206+ material_asset_name = "material_" + file_without_extension
207+ material_loaded = body .root .asset .find ('mesh' , material_asset_name )
208+ if mesh_loaded :
209+ return _add_mesh_geom_with_assets (body , name , pose , mesh_loaded ,
210+ is_visual ,
211+ mjcf_material_asset = material_loaded )
212+
213+ # Try converting mesh to sanitized .obj. This could result in multiple
214+ # geos, one per mesh in the input file.
215+ dirname = os .path .dirname (sdf_mesh .file_path ())
216+ mesh_file_path = os .path .join (dirname , uri )
217+ # Pass a nominal path to `convert_mesh_to_obj_multimesh`. If multiple
218+ # sub-meshes are present, only the file name without extension from this
219+ # nominal path will be used as a prefix for the output files.
220+ converted_path = os .path .join (dirname , file_without_extension + ".obj" )
221+ print (f"Converting { mesh_file_path } to { converted_path } " )
222+ result = convert_mesh_to_obj_multimesh (mesh_file_path , converted_path )
223+ geom_list = []
224+ for path , info in result .obj_files .items ():
225+ output_file_without_extension = os .path .splitext (
226+ os .path .basename (path ))[0 ]
227+ sub_mesh_loaded = geom .root .asset .find ('mesh' ,
228+ output_file_without_extension )
229+ sub_mesh_material_asset_name = (
230+ "material_" + output_file_without_extension )
231+ material_loaded = body .root .asset .find ('mesh' ,
232+ sub_mesh_material_asset_name )
233+ if sub_mesh_loaded :
234+ geom_list .append (
235+ _add_mesh_geom_with_assets (body , name , pose , mesh_loaded ,
236+ is_visual ,
237+ mjcf_material_asset = material_loaded ))
238+
239+
240+ geom = body .add (
241+ "geom" ,
242+ name = su .find_unique_name (body , "geom" , name ),
243+ pos = su .vec3d_to_list (pose .pos ()),
244+ euler = su .quat_to_euler_list (pose .rot ()),
245+ )
246+ geom .type = "mesh"
247+ file_without_extension = os .path .splitext (
248+ os .path .basename (path ))[0 ]
249+ asset_loaded = geom .root .asset .find ('mesh' , file_without_extension )
250+ if asset_loaded is None :
251+ geom .mesh = geom .root .asset .add ('mesh' , file = path )
252+ if is_visual :
253+ # Some visual meshes are too thin and can cause inertia
254+ # calculation in Mujoco to fail. Mark them as "shell" to prevent
255+ # Mujoco from failing in compilation. The visual geometry will
256+ # not be used for inertia computation anyway since we explicitly
257+ # convert inertia from sdformat to mjcf.
258+ # https://github.com/google-deepmind/mujoco/issues/2455
259+ geom .mesh .inertia = "shell"
260+ mat_asset = geom .root .asset .add ("material" ,
261+ name = "material_" + file_without_extension ,
262+ specular = info .mat .specular ,
263+ shininess = info .mat .shininess ,
264+ rgba = info .mat .rgba )
265+ geom .material = mat_asset
266+ else :
267+ geom .mesh = asset_loaded
268+ return geom_list
269+ return _add_geoms_after_conversion (body , name , pose ,
270+ result , is_visual )
271+
102272
103273def apply_surface_to_geometry (geom , sdf_surface ):
104274 """
@@ -142,14 +312,21 @@ def add_visual(body, vis):
142312 :param mjcf.Element body: The MJCF body to which the geom is added.
143313 :param sdformat.Visual vis: Visual object to be converted.
144314 :return: The newly created MJCF geom.
145- :rtype: mjcf.Element
315+ :rtype: mjcf.Element or list of mjcf
146316 """
147317 sem_pose = vis .semantic_pose ()
148318 pose = su .graph_resolver .resolve_pose (sem_pose )
149- geom = add_geometry (body , vis .name (), pose , vis .geometry ())
150- geom .group = VISUAL_GEOM_GROUP
151- # Visual geoms do not collide with any other geom, so we set their contype
152- # and conaffinity to 0.
153- geom .contype = 0
154- geom .conaffinity = 0
155- return geom
319+ geoms = add_geometry (body , vis .name (), pose , vis .geometry (), is_visual = True )
320+ if not isinstance (geoms , list ):
321+ geoms = [geoms ]
322+ for geom in geoms :
323+ geom .group = VISUAL_GEOM_GROUP
324+ # Visual geoms do not collide with any other geom, so we set their
325+ # contype and conaffinity to 0.
326+ geom .contype = 0
327+ geom .conaffinity = 0
328+ if vis .material () is not None :
329+ mjcf_mat = add_material (geom , vis .material ())
330+ for geom in geoms :
331+ geom .material = mjcf_mat
332+ return geoms
0 commit comments