Skip to content

Commit 47c034c

Browse files
Add fallback conversion to obj for unsupported mesh files
Signed-off-by: Shameek Ganguly <shameek@intrinsic.ai>
1 parent 1e4dfa2 commit 47c034c

File tree

7 files changed

+455
-26
lines changed

7 files changed

+455
-26
lines changed

sdformat_mjcf/src/sdformat_mjcf/sdformat_to_mjcf/converters/geometry.py

Lines changed: 195 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,70 @@
1616

1717
import 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
1921
import sdformat_mjcf.utils.sdf_utils as su
2022

2123
COLLISION_GEOM_GROUP = 3
2224
VISUAL_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

103273
def 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

sdformat_mjcf/src/sdformat_mjcf/sdformat_to_mjcf/converters/link.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
add_visual,
1818
)
1919
from sdformat_mjcf.sdformat_to_mjcf.converters.light import add_light
20-
from sdformat_mjcf.sdformat_to_mjcf.converters.material import add_material
2120
from sdformat_mjcf.sdformat_to_mjcf.converters.sensor import add_sensor
2221
import sdformat_mjcf.utils.sdf_utils as su
2322

@@ -91,9 +90,7 @@ def add_link(body, link, parent_name="world", link_pose=None):
9190
for vi in range(link.visual_count()):
9291
vis = link.visual_by_index(vi)
9392
if vis.geometry() is not None:
94-
visual_geom = add_visual(body, vis)
95-
if vis.material() is not None:
96-
add_material(visual_geom, vis.material())
93+
add_visual(body, vis)
9794

9895
for li in range(link.light_count()):
9996
light = link.light_by_index(li)

sdformat_mjcf/src/sdformat_mjcf/sdformat_to_mjcf/converters/material.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@
2424
MATERIAL_NUMBER = 0
2525

2626

27-
def add_material(geom, material):
27+
def add_material(root, material):
2828
"""
2929
Converts an SDFormat material to an MJCF material.
3030
31-
:param mjcf.Element geom: The MJCF geom to add the material
31+
:param mjcf.RootElement root: The MJCF root in which the material will be
32+
created.
3233
:param sdf.Material material: The SDF material to convert
3334
:return: The newly created MJCF material.
3435
:rtype: mjcf.Element
@@ -38,7 +39,7 @@ def add_material(geom, material):
3839
em_color = material.emissive()
3940
specular = (sp_color.r() + sp_color.g() + sp_color.b()) / 3.0
4041
emissive = (em_color.r() + em_color.g() + em_color.b()) / 3.0
41-
asset = geom.root.asset
42+
asset = root.asset
4243
r_mat = None
4344
if pbr is not None:
4445
workflow = pbr.workflow(sdf.PbrWorkflowType.METAL)
@@ -81,5 +82,4 @@ def add_material(geom, material):
8182
clamp(diff.g() * 0.8 + amb.g() * 0.4, 0, 1),
8283
clamp(diff.b() * 0.8 + amb.b() * 0.4, 0, 1),
8384
clamp(diff.a() * 0.8 + amb.a() * 0.4, 0, 1)])
84-
geom.material = r_mat
8585
return r_mat

0 commit comments

Comments
 (0)