Skip to content

Instantly share code, notes, and snippets.

@BuildingAtom
Last active September 4, 2025 15:20
Show Gist options
  • Select an option

  • Save BuildingAtom/9218db8902fd7ddabbd9b08e7bd9ff4d to your computer and use it in GitHub Desktop.

Select an option

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.
"""
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