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 _set_mesh_inertia (mjcf_mesh , is_visual ):
27+ if is_visual :
28+ # Some visual meshes are too thin and can cause inertia
29+ # calculation in Mujoco to fail. Mark them as "shell" to prevent
30+ # Mujoco from failing in compilation. The visual geometry will
31+ # not be used for inertia computation anyway since we explicitly
32+ # convert inertia from sdformat to mjcf.
33+ # https://github.com/google-deepmind/mujoco/issues/2455
34+ mjcf_mesh .inertia = "shell"
2435
25- def add_geometry (body , name , pose , sdf_geom ):
36+
37+ def _validate_uri (uri ):
38+ if 'http://' in uri or 'https://' in uri :
39+ raise RuntimeError ("Fuel meshes are not yet supported" )
40+ if 'model://' in uri :
41+ prefix = 'model://'
42+ # TODO: Support sdf::ParserConfig::AddURIPath to resolve URIs.
43+ return uri .replace (prefix , '' , 1 )
44+ return uri
45+
46+
47+ def _is_unsupported_mesh_geo (sdf_geom ):
48+ if not sdf_geom .mesh_shape ():
49+ return False
50+ mesh_shape = sdf_geom .mesh_shape ()
51+ uri = _validate_uri (mesh_shape .uri ())
52+ _ , basename = os .path .split (uri )
53+ filename , extension = os .path .splitext (basename )
54+ # Mujoco supports .obj, .stl and .msh
55+ # Out of these, .obj is only supported if it has a single mesh in it.
56+ # So treat it as unsupported for this check so these files are sanitized
57+ # for Mujoco.
58+ return extension not in [".stl" , ".msh" ]
59+
60+
61+ def add_geometry (body , name , pose , sdf_geom , is_visual = False ):
2662 """
2763 Converts an SDFormat geometry to an MJCF geom and add it to the given body.
2864
@@ -74,22 +110,23 @@ def add_geometry(body, name, pose, sdf_geom):
74110 geom .size = [sphere_shape .radius ()]
75111 elif sdf_geom .mesh_shape ():
76112 mesh_shape = sdf_geom .mesh_shape ()
77- uri = mesh_shape .uri ()
78- extension_tokens = os .path .basename (mesh_shape .uri ()).split ("." )
113+ uri = _validate_uri (mesh_shape .uri ())
114+ if _is_unsupported_mesh_geo (sdf_geom ):
115+ raise RuntimeError (
116+ f"Call `convert_and_add_mesh` for unsupported mesh geo { uri } " )
117+ extension_tokens = os .path .basename (uri ).split ("." )
79118 if (len (extension_tokens ) == 1 ):
80119 raise RuntimeError ("Unable to find the mesh extension {}"
81120 .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" )
121+ file_without_extension = os .path .splitext (os .path .basename (uri ))[0 ]
86122 geom .type = "mesh"
87123 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 )
90124 if asset_loaded is None :
125+ dirname = os .path .dirname (mesh_shape .file_path ())
126+ mesh_file_path = os .path .join (dirname , uri )
91127 geom .mesh = geom .root .asset .add ('mesh' ,
92128 file = mesh_file_path )
129+ _set_mesh_inertia (geom .mesh , is_visual )
93130 else :
94131 geom .mesh = asset_loaded
95132 geom .mesh .scale = su .vec3d_to_list (mesh_shape .scale ())
@@ -100,6 +137,80 @@ def add_geometry(body, name, pose, sdf_geom):
100137 return geom
101138
102139
140+ def _add_mesh_geom_with_assets (body , name , pose , mjcf_mesh_asset ,
141+ mjcf_material_asset = None ):
142+ geom = body .add (
143+ "geom" ,
144+ name = su .find_unique_name (body , "geom" , name ),
145+ pos = su .vec3d_to_list (pose .pos ()),
146+ euler = su .quat_to_euler_list (pose .rot ()),
147+ )
148+ geom .type = "mesh"
149+ geom .mesh = mjcf_mesh_asset
150+ if mjcf_material_asset :
151+ geom .material = mjcf_material_asset
152+ return geom
153+
154+
155+ def convert_and_add_mesh (body , name , pose , sdf_mesh , is_visual = False ):
156+ # Check if asset was loaded already with the uri key. If so, just add the
157+ # asset. This can happen if the converted mesh has a single sub-mesh,
158+ # which was loaded already.
159+ uri = _validate_uri (sdf_mesh .uri ())
160+ file_without_extension = os .path .splitext (os .path .basename (uri ))[0 ]
161+ mesh_asset_name = file_without_extension
162+ mesh_loaded = body .root .asset .find ('mesh' , mesh_asset_name )
163+ material_asset_name = "material_" + file_without_extension
164+ material_loaded = body .root .asset .find ('material' , material_asset_name )
165+ if mesh_loaded :
166+ geom = _add_mesh_geom_with_assets (body , name , pose , mesh_loaded ,
167+ mjcf_material_asset = material_loaded )
168+ return [geom ]
169+
170+ # Try converting mesh to sanitized .obj. This could result in multiple
171+ # geos, one per mesh in the input file.
172+ dirname = os .path .dirname (sdf_mesh .file_path ())
173+ mesh_file_path = os .path .join (dirname , uri )
174+ # Pass a nominal path to `convert_mesh_to_obj_multimesh`. If multiple
175+ # sub-meshes are present, only the file name without extension from this
176+ # nominal path will be used as a prefix for the output files.
177+ converted_path = os .path .join (dirname , file_without_extension + ".obj" )
178+ print (f"Converting { mesh_file_path } to { converted_path } " )
179+ result = convert_mesh_to_obj_multimesh (mesh_file_path , converted_path )
180+ geom_list = []
181+ for path , info in result .obj_files .items ():
182+ output_file_without_extension = os .path .splitext (
183+ os .path .basename (path ))[0 ]
184+ sub_mesh_loaded = body .root .asset .find ('mesh' ,
185+ output_file_without_extension )
186+ sub_mesh_material_asset_name = (
187+ "material_" + output_file_without_extension )
188+ material_loaded = body .root .asset .find ('material' ,
189+ sub_mesh_material_asset_name )
190+ if sub_mesh_loaded :
191+ geom_list .append (
192+ _add_mesh_geom_with_assets (body , name , pose , sub_mesh_loaded ,
193+ mjcf_material_asset = material_loaded ))
194+ continue
195+
196+ # Add mesh and material assets
197+ mesh = body .root .asset .add ('mesh' , file = path )
198+ _set_mesh_inertia (mesh , is_visual )
199+ mesh .scale = su .vec3d_to_list (sdf_mesh .scale ())
200+ material_asset = None
201+ if is_visual :
202+ material_asset = body .root .asset .add ("material" ,
203+ name = sub_mesh_material_asset_name ,
204+ specular = info .mat .specular ,
205+ shininess = info .mat .shininess ,
206+ rgba = info .mat .rgba )
207+ geom_list .append (
208+ _add_mesh_geom_with_assets (body , name , pose , mesh ,
209+ mjcf_material_asset = material_asset ))
210+
211+ return geom_list
212+
213+
103214def apply_surface_to_geometry (geom , sdf_surface ):
104215 """
105216 Applies surface parameters from an SDFormat surface to an MJCF geom.
@@ -127,10 +238,20 @@ def add_collision(body, col):
127238 """
128239 sem_pose = col .semantic_pose ()
129240 pose = su .graph_resolver .resolve_pose (sem_pose )
130- geom = add_geometry (body , col .name (), pose , col .geometry ())
131- geom .group = COLLISION_GEOM_GROUP
132- apply_surface_to_geometry (geom , col .surface ())
133- return geom
241+ if _is_unsupported_mesh_geo (col .geometry ()):
242+ sdf_mesh = col .geometry ().mesh_shape ()
243+ geoms = convert_and_add_mesh (body , col .name (), pose , sdf_mesh ,
244+ is_visual = False )
245+ else :
246+ geom = add_geometry (body , col .name (), pose , col .geometry (),
247+ is_visual = False )
248+ geoms = [geom ]
249+ for geom in geoms :
250+ geom .group = COLLISION_GEOM_GROUP
251+ apply_surface_to_geometry (geom , col .surface ())
252+ if len (geoms ) == 1 :
253+ return geoms [0 ]
254+ return geoms
134255
135256
136257def add_visual (body , vis ):
@@ -142,14 +263,29 @@ def add_visual(body, vis):
142263 :param mjcf.Element body: The MJCF body to which the geom is added.
143264 :param sdformat.Visual vis: Visual object to be converted.
144265 :return: The newly created MJCF geom.
145- :rtype: mjcf.Element
266+ :rtype: mjcf.Element or list of mjcf
146267 """
147268 sem_pose = vis .semantic_pose ()
148269 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
270+ if _is_unsupported_mesh_geo (vis .geometry ()):
271+ sdf_mesh = vis .geometry ().mesh_shape ()
272+ geoms = convert_and_add_mesh (body , vis .name (), pose , sdf_mesh ,
273+ is_visual = True )
274+ else :
275+ geom = add_geometry (body , vis .name (), pose , vis .geometry (),
276+ is_visual = True )
277+ geoms = [geom ]
278+
279+ for geom in geoms :
280+ geom .group = VISUAL_GEOM_GROUP
281+ # Visual geoms do not collide with any other geom, so we set their
282+ # contype and conaffinity to 0.
283+ geom .contype = 0
284+ geom .conaffinity = 0
285+ if vis .material () is not None :
286+ mjcf_mat = add_material (geom .root , vis .material ())
287+ for geom in geoms :
288+ geom .material = mjcf_mat
289+ if len (geoms ) == 1 :
290+ return geoms [0 ]
291+ return geoms
0 commit comments