Parse MJCF XML, convert meshes, resolve conflicts, write updated XML.
(xml_path: Path)
| 291 | |
| 292 | |
| 293 | def process_xml(xml_path: Path): |
| 294 | """Parse MJCF XML, convert meshes, resolve conflicts, write updated XML.""" |
| 295 | |
| 296 | if not xml_path.exists(): |
| 297 | print(f"Error: XML file not found: {xml_path}") |
| 298 | return |
| 299 | |
| 300 | xml_dir = xml_path.parent |
| 301 | |
| 302 | print(f"XML: {xml_path}") |
| 303 | print(f"Dir: {xml_dir}") |
| 304 | print("=" * 60) |
| 305 | |
| 306 | # Parse XML |
| 307 | tree = ET.parse(str(xml_path)) |
| 308 | root = tree.getroot() |
| 309 | |
| 310 | # Phase -1: Flatten <include> fragments (gym-aloha and friends split robots |
| 311 | # across <mujocoinclude> files, some referenced from inside <worldbody>). |
| 312 | # After this the tree is a single self-contained <mujoco> with no includes, |
| 313 | # so mesh conversion below sees every mesh and Unreal imports one flat model. |
| 314 | include_count = sum(1 for _ in root.iter("include")) |
| 315 | if include_count: |
| 316 | print(f"Flattening {include_count} <include> fragment(s)...") |
| 317 | root = flatten_includes(root, xml_dir.resolve()) |
| 318 | tree = ET.ElementTree(root) |
| 319 | remaining = sum(1 for _ in root.iter("include")) |
| 320 | print(f" -> {remaining} include(s) remain after flatten") |
| 321 | |
| 322 | # Find meshdir from compiler |
| 323 | meshdir = "" |
| 324 | for compiler in root.iter("compiler"): |
| 325 | md = compiler.get("meshdir", "") |
| 326 | if md: |
| 327 | meshdir = md |
| 328 | |
| 329 | mesh_base = xml_dir / meshdir if meshdir else xml_dir |
| 330 | print(f"Mesh directory: {mesh_base}") |
| 331 | |
| 332 | # Phase 0: Materialize inline ``<mesh vertex="..." face="...">`` entries. |
| 333 | # These get rewritten to file= entries before Phase 1 runs, so the rest |
| 334 | # of the pipeline treats them like any other on-disk mesh. |
| 335 | inline_count = materialize_inline_meshes(root, mesh_base) |
| 336 | if inline_count: |
| 337 | print(f"Materialized {inline_count} inline mesh(es) to {mesh_base}") |
| 338 | |
| 339 | # Collect all mesh elements |
| 340 | mesh_elements = list(root.iter("mesh")) |
| 341 | |
| 342 | # Also convert meshes referenced by <flexcomp file="..."> |
| 343 | for flexcomp in root.iter("flexcomp"): |
| 344 | file_attr = flexcomp.get("file") |
| 345 | if file_attr: |
| 346 | source_path = mesh_base / file_attr |
| 347 | if source_path.exists(): |
| 348 | output_glb = source_path.with_suffix(".glb") |
| 349 | if not output_glb.exists() or output_glb.stat().st_mtime < source_path.stat().st_mtime: |
| 350 | print(f"\n[flexcomp] Converting mesh: {source_path.name} -> {output_glb.name}") |
no test coverage detected