Parse MJCF XML, convert meshes, resolve conflicts, write updated XML.
(xml_path: Path)
| 291 | |
| 292 | print(f"\n[inline] Materializing '{name}' -> {obj_path.name} " |
| 293 | f"({n_verts} verts, {n_faces} faces)") |
| 294 | |
| 295 | # Standard OBJ — vertices are 1-indexed in OBJ but 0-indexed in MuJoCo. |
| 296 | with open(obj_path, "w", encoding="utf-8") as f: |
| 297 | f.write(f"# Generated by clean_meshes.py from inline MJCF mesh '{name}'\n") |
| 298 | for i in range(n_verts): |
| 299 | f.write(f"v {verts[i*3]:.6f} {verts[i*3+1]:.6f} {verts[i*3+2]:.6f}\n") |
| 300 | for i in range(n_faces): |
| 301 | a, b, c = faces[i*3] + 1, faces[i*3+1] + 1, faces[i*3+2] + 1 |
| 302 | f.write(f"f {a} {b} {c}\n") |
| 303 | |
| 304 | # Rewrite the element: drop inline data, point at the new OBJ. |
| 305 | # Path is relative to meshdir (matching how file= entries already |
| 306 | # work). We preserve scale and content_type if present. |
| 307 | rel_path = obj_path.name |
| 308 | del mesh_el.attrib["vertex"] |
| 309 | del mesh_el.attrib["face"] |
| 310 | mesh_el.set("file", rel_path) |
| 311 | if "content_type" not in mesh_el.attrib: |
| 312 | mesh_el.set("content_type", "model/obj") |
| 313 | materialized += 1 |
| 314 | |
| 315 | return materialized |
| 316 | |
| 317 | |
| 318 | def process_xml(xml_path: Path): |
| 319 | """Parse MJCF XML, convert meshes, resolve conflicts, write updated XML.""" |
| 320 | |
| 321 | if not xml_path.exists(): |
| 322 | print(f"Error: XML file not found: {xml_path}") |
| 323 | return |
| 324 | |
| 325 | xml_dir = xml_path.parent |
| 326 | |
| 327 | print(f"XML: {xml_path}") |
| 328 | print(f"Dir: {xml_dir}") |
| 329 | print("=" * 60) |
| 330 | |
| 331 | # Parse XML |
| 332 | tree = ET.parse(str(xml_path)) |
| 333 | root = tree.getroot() |
| 334 | |
| 335 | # Phase -1: Flatten <include> fragments (gym-aloha and friends split robots |
| 336 | # across <mujocoinclude> files, some referenced from inside <worldbody>). |
| 337 | # After this the tree is a single self-contained <mujoco> with no includes, |
| 338 | # so mesh conversion below sees every mesh and Unreal imports one flat model. |
| 339 | include_count = sum(1 for _ in root.iter("include")) |
| 340 | if include_count: |
| 341 | print(f"Flattening {include_count} <include> fragment(s)...") |
| 342 | root = flatten_includes(root, xml_dir.resolve()) |
| 343 | tree = ET.ElementTree(root) |
| 344 | remaining = sum(1 for _ in root.iter("include")) |
| 345 | print(f" -> {remaining} include(s) remain after flatten") |
| 346 | |
| 347 | # Find meshdir from compiler |
| 348 | meshdir = "" |
| 349 | for compiler in root.iter("compiler"): |
| 350 | md = compiler.get("meshdir", "") |
no test coverage detected