Rewrite `` `` (MuJoCo's inline-data form) to `` `` by emitting an OBJ next to the rest of the mesh assets. Returns the number of inline meshes materialized. MuJoCo accepts meshes declared inline via ``vertex`` (flat list of 3-tuple floats
(root, mesh_base: Path)
| 218 | |
| 219 | |
| 220 | def materialize_inline_meshes(root, mesh_base: Path) -> int: |
| 221 | """Rewrite ``<mesh vertex="..." face="...">`` (MuJoCo's inline-data form) |
| 222 | to ``<mesh file="...">`` by emitting an OBJ next to the rest of the |
| 223 | mesh assets. Returns the number of inline meshes materialized. |
| 224 | |
| 225 | MuJoCo accepts meshes declared inline via ``vertex`` (flat list of |
| 226 | 3-tuple floats) and ``face`` (flat list of 3-tuple ints) attributes |
| 227 | instead of an external ``file``. Unreal's importer can't follow the |
| 228 | inline form, so we synthesize a real OBJ on disk and rewrite the |
| 229 | ``<mesh>`` element to point at it. The downstream conversion phase |
| 230 | then produces a GLB companion just like any other ``file=`` mesh. |
| 231 | """ |
| 232 | materialized = 0 |
| 233 | mesh_base.mkdir(parents=True, exist_ok=True) |
| 234 | for mesh_el in list(root.iter("mesh")): |
| 235 | if mesh_el.get("file"): |
| 236 | continue |
| 237 | vertex_attr = mesh_el.get("vertex") |
| 238 | face_attr = mesh_el.get("face") |
| 239 | if not vertex_attr or not face_attr: |
| 240 | continue |
| 241 | |
| 242 | name = mesh_el.get("name") |
| 243 | if not name: |
| 244 | print(f" x Inline mesh has no name; skipping (cannot synthesize filename)") |
| 245 | continue |
| 246 | |
| 247 | try: |
| 248 | verts = _parse_floats(vertex_attr) |
| 249 | faces = [int(t) for t in face_attr.replace(",", " ").split() if t] |
| 250 | except ValueError as e: |
| 251 | print(f" x Failed to parse vertex/face for '{name}': {e}") |
| 252 | continue |
| 253 | |
| 254 | if len(verts) % 3 != 0 or len(faces) % 3 != 0: |
| 255 | print(f" x Inline mesh '{name}' has non-triangular layout " |
| 256 | f"(verts={len(verts)}, faces={len(faces)}) — skipping") |
| 257 | continue |
| 258 | |
| 259 | n_verts = len(verts) // 3 |
| 260 | n_faces = len(faces) // 3 |
| 261 | |
| 262 | # Filename-safe stem. MuJoCo names can contain '/' (the scene XML |
| 263 | # uses slash-paths heavily) which would create stray directories. |
| 264 | safe_stem = name.replace("/", "_").replace("\\", "_").replace(":", "_") |
| 265 | obj_path = mesh_base / f"{safe_stem}.obj" |
| 266 | |
| 267 | print(f"\n[inline] Materializing '{name}' -> {obj_path.name} " |
| 268 | f"({n_verts} verts, {n_faces} faces)") |
| 269 | |
| 270 | # Standard OBJ — vertices are 1-indexed in OBJ but 0-indexed in MuJoCo. |
| 271 | with open(obj_path, "w", encoding="utf-8") as f: |
| 272 | f.write(f"# Generated by clean_meshes.py from inline MJCF mesh '{name}'\n") |
| 273 | for i in range(n_verts): |
| 274 | f.write(f"v {verts[i*3]:.6f} {verts[i*3+1]:.6f} {verts[i*3+2]:.6f}\n") |
| 275 | for i in range(n_faces): |
| 276 | a, b, c = faces[i*3] + 1, faces[i*3+1] + 1, faces[i*3+2] + 1 |
| 277 | f.write(f"f {a} {b} {c}\n") |
no test coverage detected