Last active
September 4, 2025 15:20
-
-
Save BuildingAtom/9218db8902fd7ddabbd9b08e7bd9ff4d to your computer and use it in GitHub Desktop.
Mini toolscript to convert xacro URDF's to flat URDF's with all mesh/obj data as STL's in the same folder without ROS.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| Copyright 2025 Adam Li (@BuildingAtom) | |
| Permission is hereby granted, free of charge, to any person obtaining a copy of | |
| this software and associated documentation files (the “Software”), to deal in | |
| the Software without restriction, including without limitation the rights to | |
| use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies | |
| of the Software, and to permit persons to whom the Software is furnished to do | |
| so, subject to the following conditions: | |
| The above copyright notice and this permission notice shall be included in all | |
| copies or substantial portions of the Software. | |
| THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| SOFTWARE. | |
| """ | |
| import os, shutil | |
| import trimesh | |
| # Need trimesh, xacrodoc | |
| def rename_path(path: str, new_delim: str = "_"): | |
| """ Takes the system path delimiter and replaces it with a new delimiter. | |
| This is useful for creating a new path that is compatible with MuJoCo's requirements. | |
| Args: | |
| path: The original path to be modified. | |
| new_delim: The new delimiter to replace the system path delimiter with. | |
| Defaults to "_". | |
| Returns: | |
| A new path string with the system path delimiter replaced by `new_delim`. | |
| """ | |
| new_path = path.replace(os.sep, new_delim) | |
| if os.altsep: | |
| new_path = path.replace(os.altsep, new_delim) | |
| return new_path | |
| def rename_and_copy_assets(els, new_base, dirname, reprocess=False): | |
| """Rename and copy assets described by the dom xml elements in `els` to `new_base`. | |
| This function modifies the `filename` attribute of each element in `els` in place, | |
| replacing the common base path with a new name derived from the original filename and | |
| removing the common base directory (for MuJoCo). | |
| Args: | |
| els: A list of DOM elements that have a 'filename' attribute. | |
| new_base: The base directory where the renamed files will be copied. | |
| """ | |
| base = os.path.commonpath([e.getAttribute("filename") for e in els]) | |
| for e in els: | |
| filename = e.getAttribute("filename") | |
| filename = os.path.relpath(filename, base) | |
| new_filename = rename_path(filename) | |
| if reprocess: | |
| new_filename_base, _ = os.path.splitext(new_filename) | |
| new_filename = new_filename_base + ".stl" | |
| mesh = trimesh.load(os.path.join(base, filename)) | |
| mesh.export(os.path.join(new_base, new_filename)) | |
| else: | |
| shutil.copy(os.path.join(base, filename), os.path.join(new_base, dirname, new_filename)) | |
| e.setAttribute("filename", os.path.join(dirname, new_filename)) | |
| def export_urdf_assets(doc, path, mesh_dirname="meshes", material_dirname="mats", reprocess_mesh=True): | |
| """Export URDF assets from the given document to the specified path. | |
| This function looks for mesh and material elements in the URDF document, | |
| renames their filenames to be compatible with MuJoCo's requirements, and copies | |
| the corresponding files to the specified directories. | |
| Args: | |
| doc: The URDF document from which to extract assets. | |
| path: The directory where the assets will be exported. | |
| mesh_dirname: The name of the directory where mesh files will be stored. | |
| material_dirname: The name of the directory where material files will be stored. | |
| Returns: | |
| mesh_path, mat_path: Paths to the directories where meshes and materials were exported. | |
| If no meshes or materials were found, returns None for the respective paths. | |
| """ | |
| # Isolate the relevant elements from the document | |
| mesh_el = [e for e in doc.getElementsByTagName("mesh") if e.hasAttribute("filename")] | |
| mat_el = [e for e in doc.getElementsByTagName("mesh") if e.hasAttribute("material")] | |
| # Prepare output directories for meshes and materials | |
| mesh_path = None | |
| mat_path = None | |
| if len(mesh_el) > 0: | |
| mesh_path = os.path.join(path, mesh_dirname) | |
| os.makedirs(os.path.join(mesh_path), exist_ok=True) | |
| if len(mat_el) > 0: | |
| mat_path = os.path.join(path, material_dirname) | |
| os.makedirs(os.path.join(mat_path), exist_ok=True) | |
| # first remove the file:// prefix from all filenames | |
| prefix = "file://" | |
| prefix_len = len(prefix) | |
| for e in (mesh_el + mat_el): | |
| filename = e.getAttribute("filename") | |
| if filename.startswith(prefix): | |
| filename = filename[prefix_len:] | |
| e.setAttribute("filename", filename) | |
| # remove the common base from all filenames and copy assets | |
| if mesh_path is not None: | |
| rename_and_copy_assets(mesh_el, path, mesh_dirname, reprocess=reprocess_mesh) | |
| if mat_path is not None: | |
| rename_and_copy_assets(mat_el, path, material_dirname, reprocess=False) | |
| return mesh_path, mat_path | |
| def recompile_xacro_to_flaturdf(xacro_path: str, target_path: str, target_filename: str = None, overwrite: bool = False): | |
| """Recompile a Xacro file to a flat URDF file with all assets exported reprocessed and | |
| saved in the same directory as the URDF file. | |
| Args: | |
| target_path: The directory where the flat URDF file will be saved. | |
| xacro_path: The path to the Xacro file to be compiled. | |
| target_filename: Optional; the name of the output URDF file. If not provided, | |
| it will be derived from the Xacro file name. | |
| overwrite: Optional; if True, will overwrite the output file if it already exists. | |
| """ | |
| # output check | |
| if target_filename is None: | |
| target_filename = os.path.splitext(os.path.basename(xacro_path))[0] + ".urdf" | |
| output_path = os.path.join(target_path, target_filename) | |
| if os.path.exists(output_path) and not overwrite: | |
| raise FileExistsError(f"Output file {output_path} already exists. Please choose a different target path or filename.") | |
| from xacrodoc import XacroDoc | |
| doc = XacroDoc.from_file(xacro_path) | |
| export_urdf_assets(doc.doc, target_path, mesh_dirname="./", material_dirname="./", reprocess_mesh=True) | |
| doc.to_urdf_file(os.path.join(target_path, target_filename)) | |
| if __name__ == "__main__": | |
| import argparse | |
| parser = argparse.ArgumentParser(description="Recompile a Xacro file to a flat URDF file.") | |
| parser.add_argument("xacro_path", type=str, help="Path to the Xacro file to be compiled.") | |
| parser.add_argument("target_path", type=str, help="Directory where the flat URDF file will be saved.") | |
| parser.add_argument("--target_filename", type=str, default=None, help="Name of the output URDF file.") | |
| parser.add_argument("--overwrite", action="store_true", help="If set, will overwrite the output file if it already exists.") | |
| args = parser.parse_args() | |
| recompile_xacro_to_flaturdf(args.xacro_path, args.target_path, args.target_filename, args.overwrite) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment