Skip to content

Commit 0e43abd

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

9 files changed

Lines changed: 393 additions & 39 deletions

File tree

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

Lines changed: 157 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,49 @@
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 _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+
103214
def 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

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

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)