Skip to content

Commit dd67a72

Browse files
Bug fixes
Signed-off-by: Shameek Ganguly <shameek@intrinsic.ai>
1 parent 47c034c commit dd67a72

File tree

4 files changed

+97
-172
lines changed

4 files changed

+97
-172
lines changed

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

Lines changed: 68 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -23,39 +23,16 @@
2323
COLLISION_GEOM_GROUP = 3
2424
VISUAL_GEOM_GROUP = 0
2525

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
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"
35+
5936

6037
def _validate_uri(uri):
6138
if 'http://' in uri or 'https://' in uri:
@@ -66,7 +43,8 @@ def _validate_uri(uri):
6643
return uri.replace(prefix, '', 1)
6744
return uri
6845

69-
def _is_unsupported_mesh_geo(root, sdf_geom):
46+
47+
def _is_unsupported_mesh_geo(sdf_geom):
7048
if not sdf_geom.mesh_shape():
7149
return False
7250
mesh_shape = sdf_geom.mesh_shape()
@@ -79,6 +57,7 @@ def _is_unsupported_mesh_geo(root, sdf_geom):
7957
# for Mujoco.
8058
return extension not in [".stl", ".msh"]
8159

60+
8261
def add_geometry(body, name, pose, sdf_geom, is_visual=False):
8362
"""
8463
Converts an SDFormat geometry to an MJCF geom and add it to the given body.
@@ -135,7 +114,7 @@ def add_geometry(body, name, pose, sdf_geom, is_visual=False):
135114
if _is_unsupported_mesh_geo(sdf_geom):
136115
raise RuntimeError(
137116
f"Call `convert_and_add_mesh` for unsupported mesh geo {uri}")
138-
extension_tokens = os.path.basename(mesh_shape.uri()).split(".")
117+
extension_tokens = os.path.basename(uri).split(".")
139118
if (len(extension_tokens) == 1):
140119
raise RuntimeError("Unable to find the mesh extension {}"
141120
.format(uri))
@@ -145,26 +124,9 @@ def add_geometry(body, name, pose, sdf_geom, is_visual=False):
145124
if asset_loaded is None:
146125
dirname = os.path.dirname(mesh_shape.file_path())
147126
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"
127+
geom.mesh = geom.root.asset.add('mesh',
128+
file=mesh_file_path)
129+
_set_mesh_inertia(geom.mesh, is_visual)
168130
else:
169131
geom.mesh = asset_loaded
170132
geom.mesh.scale = su.vec3d_to_list(mesh_shape.scale())
@@ -174,41 +136,36 @@ def add_geometry(body, name, pose, sdf_geom, is_visual=False):
174136

175137
return geom
176138

139+
177140
def _add_mesh_geom_with_assets(body, name, pose, mjcf_mesh_asset,
178-
is_visual=False, mjcf_material_asset=None):
141+
mjcf_material_asset=None):
179142
geom = body.add(
180143
"geom",
181144
name=su.find_unique_name(body, "geom", name),
182145
pos=su.vec3d_to_list(pose.pos()),
183146
euler=su.quat_to_euler_list(pose.rot()),
184147
)
185148
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"
194149
geom.mesh = mjcf_mesh_asset
195150
if mjcf_material_asset:
196151
geom.material = mjcf_material_asset
197152
return geom
198153

154+
199155
def convert_and_add_mesh(body, name, pose, sdf_mesh, is_visual=False):
200156
# Check if asset was loaded already with the uri key. If so, just add the
201-
# asset:
157+
# asset. This can happen if the converted mesh has a single sub-mesh,
158+
# which was loaded already.
202159
uri = _validate_uri(sdf_mesh.uri())
203160
file_without_extension = os.path.splitext(os.path.basename(uri))[0]
204161
mesh_asset_name = file_without_extension
205162
mesh_loaded = body.root.asset.find('mesh', mesh_asset_name)
206163
material_asset_name = "material_" + file_without_extension
207-
material_loaded = body.root.asset.find('mesh', material_asset_name)
164+
material_loaded = body.root.asset.find('material', material_asset_name)
208165
if mesh_loaded:
209-
return _add_mesh_geom_with_assets(body, name, pose, mesh_loaded,
210-
is_visual,
166+
geom = _add_mesh_geom_with_assets(body, name, pose, mesh_loaded,
211167
mjcf_material_asset=material_loaded)
168+
return [geom]
212169

213170
# Try converting mesh to sanitized .obj. This could result in multiple
214171
# geos, one per mesh in the input file.
@@ -224,50 +181,34 @@ def convert_and_add_mesh(body, name, pose, sdf_mesh, is_visual=False):
224181
for path, info in result.obj_files.items():
225182
output_file_without_extension = os.path.splitext(
226183
os.path.basename(path))[0]
227-
sub_mesh_loaded = geom.root.asset.find('mesh',
184+
sub_mesh_loaded = body.root.asset.find('mesh',
228185
output_file_without_extension)
229186
sub_mesh_material_asset_name = (
230187
"material_" + output_file_without_extension)
231-
material_loaded = body.root.asset.find('mesh',
188+
material_loaded = body.root.asset.find('material',
232189
sub_mesh_material_asset_name)
233190
if sub_mesh_loaded:
234191
geom_list.append(
235192
_add_mesh_geom_with_assets(body, name, pose, mesh_loaded,
236-
is_visual,
237193
mjcf_material_asset=material_loaded))
194+
continue
238195

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_loaded,
209+
mjcf_material_asset=material_asset))
239210

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
268211
return geom_list
269-
return _add_geoms_after_conversion(body, name, pose,
270-
result, is_visual)
271212

272213

273214
def apply_surface_to_geometry(geom, sdf_surface):
@@ -297,10 +238,20 @@ def add_collision(body, col):
297238
"""
298239
sem_pose = col.semantic_pose()
299240
pose = su.graph_resolver.resolve_pose(sem_pose)
300-
geom = add_geometry(body, col.name(), pose, col.geometry())
301-
geom.group = COLLISION_GEOM_GROUP
302-
apply_surface_to_geometry(geom, col.surface())
303-
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
304255

305256

306257
def add_visual(body, vis):
@@ -316,17 +267,25 @@ def add_visual(body, vis):
316267
"""
317268
sem_pose = vis.semantic_pose()
318269
pose = su.graph_resolver.resolve_pose(sem_pose)
319-
geoms = add_geometry(body, vis.name(), pose, vis.geometry(), is_visual=True)
320-
if not isinstance(geoms, list):
321-
geoms = [geoms]
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+
322279
for geom in geoms:
323280
geom.group = VISUAL_GEOM_GROUP
324281
# Visual geoms do not collide with any other geom, so we set their
325282
# contype and conaffinity to 0.
326283
geom.contype = 0
327284
geom.conaffinity = 0
328285
if vis.material() is not None:
329-
mjcf_mat = add_material(geom, vis.material())
286+
mjcf_mat = add_material(geom.root, vis.material())
330287
for geom in geoms:
331288
geom.material = mjcf_mat
289+
if len(geoms) == 1:
290+
return geoms[0]
332291
return geoms

sdformat_mjcf/src/sdformat_mjcf/sdformat_to_mjcf/mesh_io.py

Lines changed: 4 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -4,63 +4,18 @@
44
import sys
55
import trimesh
66

7-
def convert_mesh_to_obj(input_filepath, output_filepath):
8-
"""
9-
Converts an input mesh file (e.g. glb) to an STL file.
10-
Supported file formats: .dae, .glb, .glTF and others that Assimp supports.
11-
12-
Args:
13-
input_filepath (str): Path to the input mesh file.
14-
output_filepath (str): Path where the output .stl file will be saved.
15-
"""
16-
if not os.path.exists(input_filepath):
17-
raise RuntimeError("Unable to find the input mesh file {}"
18-
.format(input_filepath))
19-
try:
20-
# trimesh.load handles scenes (multiple meshes) or single meshes
21-
model = trimesh.load(input_filepath)
22-
except Exception as e:
23-
print(f"\nERROR: Failed to load GLB file. Check if the file is valid.")
24-
print(f"Detail: {e}")
25-
sys.exit(1)
26-
27-
base_path, _ = os.path.splitext(output_filepath)
28-
mtl_filepath = base_path + ".mtl"
29-
mtl_filename = os.path.basename(mtl_filepath)
30-
31-
try:
32-
if isinstance(model, trimesh.Trimesh):
33-
# If it's a single mesh, wrap it in a scene for consistent export
34-
scene = trimesh.Scene([model])
35-
elif isinstance(model, trimesh.Scene):
36-
scene = model
37-
else:
38-
print(f"\nERROR: Loaded model object type is unexpected: "
39-
f"{type(model)}. Conversion failed.")
40-
sys.exit(1)
41-
42-
# Export the scene to OBJ
43-
scene.export(
44-
output_filepath,
45-
file_type='obj',
46-
mtl_name=mtl_filename
47-
)
48-
except Exception as e:
49-
print(f"\nERROR: Failed to export model to OBJ.")
50-
print(f"Detail: {e}")
51-
sys.exit(1)
52-
53-
547
@dataclass
558
class Material:
569
specular: float
5710
shininess: float
5811
rgba: list[float] # each channel has a value from 0 to 1
5912

13+
6014
@dataclass
6115
class MeshInfo:
6216
mat: Material
6317

18+
6419
@dataclass
6520
class ConversionOutput:
6621
# Dictionary with key = obj filename for mesh, value = assigned material
@@ -78,6 +33,7 @@ def _extract_material_info(mesh_material) -> Material:
7833
default_rgba = [0.5, 0.5, 0.5, 1.0] # Default grey
7934

8035
# Simple defaults for common material properties
36+
# TODO: Make this configurable
8137
specular = 0.5
8238
shininess = 30.0
8339
color = default_rgba
@@ -86,15 +42,12 @@ def _extract_material_info(mesh_material) -> Material:
8642
if isinstance(mesh_material, trimesh.visual.material.PBRMaterial):
8743
# PBRMaterial uses baseColorFactor
8844
color = mesh_material.baseColorFactor
89-
print("PBRMaterial", color)
9045
elif isinstance(mesh_material, trimesh.visual.material.SimpleMaterial):
9146
# SimpleMaterial usually has a diffuse color
9247
color = mesh_material.diffuse
93-
print("SimpleMaterial", color)
9448
elif hasattr(mesh_material, 'main_color'):
9549
# Fallback for older/custom materials
9650
color = mesh_material.main_color
97-
print("main_color", color)
9851

9952
# --- Color Standardization ---
10053
if isinstance(color, np.ndarray):
@@ -196,13 +149,11 @@ def convert_mesh_to_obj_multimesh(input_filepath: str, output_filepath: str) ->
196149

197150
if extracted_material is None:
198151
extracted_material = Material(specular=0.5, shininess=30.0, rgba=[0.5, 0.5, 0.5, 1.0])
199-
else:
200-
print(extracted_material.rgba)
201152

202153
mesh_info = MeshInfo(mat=extracted_material)
203154

204155
# Store result
205-
conversion_results[os.path.basename(obj_filename)] = mesh_info
156+
conversion_results[obj_filename] = mesh_info
206157

207158
except Exception as e:
208159
print(f"\nERROR: Failed to export mesh '{G_name}' to OBJ.")

0 commit comments

Comments
 (0)