Unverified Commit dd22e7a6 authored by qqqwan's avatar qqqwan Committed by GitHub

Adds support for MJCF converter (#957)

# Description

Added command-line MJCF conversion script and corresponding support and
test files.
- `convert_mjcf.py` in `standalone/tools/`
- `mjcf_converter.py` and `mjcf_converter_cfg.py` in
`extensions/omni.isaac.lab/omni/isaac/lab/sim/converters/`
- tests and corresponding tutorial update


## Checklist

- [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with
`./isaaclab.sh --format`
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] I have updated the changelog and the corresponding version in the
extension's `config/extension.toml` file
- [x] I have added my name to the `CONTRIBUTORS.md` or my name already
exists there

---------
Signed-off-by: 's avatarKelly Guo <kellyg@nvidia.com>
Signed-off-by: 's avatarKelly Guo <kellyguo123@hotmail.com>
Co-authored-by: 's avatarKelly Guo <kellyg@nvidia.com>
Co-authored-by: 's avatarKelly Guo <kellyguo123@hotmail.com>
parent 710f64d4
...@@ -60,6 +60,7 @@ Guidelines for modifications: ...@@ -60,6 +60,7 @@ Guidelines for modifications:
* Xavier Nal * Xavier Nal
* Zhengyu Zhang * Zhengyu Zhang
* Ziqi Fan * Ziqi Fan
* Qian Wan
## Acknowledgements ## Acknowledgements
......
This diff is collapsed.
...@@ -62,6 +62,7 @@ keywords = ["experience", "app", "usd"] ...@@ -62,6 +62,7 @@ keywords = ["experience", "app", "usd"]
"omni.kit.viewport.menubar.render" = {} "omni.kit.viewport.menubar.render" = {}
"omni.kit.viewport.menubar.camera" = {} "omni.kit.viewport.menubar.camera" = {}
"omni.kit.viewport.menubar.display" = {} "omni.kit.viewport.menubar.display" = {}
"omni.kit.viewport.menubar.lighting" = {}
"omni.kit.viewport.rtx" = {} "omni.kit.viewport.rtx" = {}
"omni.kit.widget.cache_indicator" = {} "omni.kit.widget.cache_indicator" = {}
......
[package] [package]
# Note: Semantic Versioning is used: https://semver.org/ # Note: Semantic Versioning is used: https://semver.org/
version = "0.22.10" version = "0.22.11"
# Description # Description
title = "Isaac Lab framework for Robot Learning" title = "Isaac Lab framework for Robot Learning"
......
Changelog Changelog
--------- ---------
0.22.11 (2024-09-10)
~~~~~~~~~~~~~~~~~~~~
Added
^^^^^
* Added config class, support, and tests for MJCF conversion via standalone python scripts.
0.22.10 (2024-09-09) 0.22.10 (2024-09-09)
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~
......
...@@ -20,5 +20,7 @@ from .asset_converter_base import AssetConverterBase ...@@ -20,5 +20,7 @@ from .asset_converter_base import AssetConverterBase
from .asset_converter_base_cfg import AssetConverterBaseCfg from .asset_converter_base_cfg import AssetConverterBaseCfg
from .mesh_converter import MeshConverter from .mesh_converter import MeshConverter
from .mesh_converter_cfg import MeshConverterCfg from .mesh_converter_cfg import MeshConverterCfg
from .mjcf_converter import MjcfConverter
from .mjcf_converter_cfg import MjcfConverterCfg
from .urdf_converter import UrdfConverter from .urdf_converter import UrdfConverter
from .urdf_converter_cfg import UrdfConverterCfg from .urdf_converter_cfg import UrdfConverterCfg
# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
from __future__ import annotations
import os
import omni.kit.commands
import omni.usd
from omni.isaac.core.utils.extensions import enable_extension
from pxr import Usd
from .asset_converter_base import AssetConverterBase
from .mjcf_converter_cfg import MjcfConverterCfg
class MjcfConverter(AssetConverterBase):
"""Converter for a MJCF description file to a USD file.
This class wraps around the `omni.isaac.mjcf_importer`_ extension to provide a lazy implementation
for MJCF to USD conversion. It stores the output USD file in an instanceable format since that is
what is typically used in all learning related applications.
.. caution::
The current lazy conversion implementation does not automatically trigger USD generation if
only the mesh files used by the MJCF are modified. To force generation, either set
:obj:`AssetConverterBaseCfg.force_usd_conversion` to True or delete the output directory.
.. note::
From Isaac Sim 2023.1 onwards, the extension name changed from ``omni.isaac.mjcf`` to
``omni.importer.mjcf``. This converter class automatically detects the version of Isaac Sim
and uses the appropriate extension.
.. _omni.isaac.mjcf_importer: https://docs.omniverse.nvidia.com/isaacsim/latest/ext_omni_isaac_mjcf.html
"""
cfg: MjcfConverterCfg
"""The configuration instance for MJCF to USD conversion."""
def __init__(self, cfg: MjcfConverterCfg):
"""Initializes the class.
Args:
cfg: The configuration instance for URDF to USD conversion.
"""
super().__init__(cfg=cfg)
"""
Implementation specific methods.
"""
def _convert_asset(self, cfg: MjcfConverterCfg):
"""Calls underlying Omniverse command to convert MJCF to USD.
Args:
cfg: The configuration instance for MJCF to USD conversion.
"""
import_config = self._get_mjcf_import_config(cfg)
omni.kit.commands.execute(
"MJCFCreateAsset",
mjcf_path=cfg.asset_path,
import_config=import_config,
dest_path=self.usd_path,
)
# fix the issue that material paths are not relative
if self.cfg.make_instanceable:
instanced_usd_path = os.path.join(self.usd_dir, self.usd_instanceable_meshes_path)
stage = Usd.Stage.Open(instanced_usd_path)
# resolve all paths relative to layer path
source_layer = stage.GetRootLayer()
omni.usd.resolve_paths(source_layer.identifier, source_layer.identifier)
stage.Save()
# fix the issue that material paths are not relative
# note: This issue seems to have popped up in Isaac Sim 2023.1.1
stage = Usd.Stage.Open(self.usd_path)
# resolve all paths relative to layer path
source_layer = stage.GetRootLayer()
omni.usd.resolve_paths(source_layer.identifier, source_layer.identifier)
stage.Save()
def _get_mjcf_import_config(self, cfg: MjcfConverterCfg) -> omni.importer.mjcf.ImportConfig:
"""Returns the import configuration for MJCF to USD conversion.
Args:
cfg: The configuration instance for MJCF to USD conversion.
Returns:
The constructed ``ImportConfig`` object containing the desired settings.
"""
# Enable MJCF Extensions
enable_extension("omni.importer.mjcf")
from omni.importer.mjcf import _mjcf as omni_mjcf
import_config = omni_mjcf.ImportConfig()
# set the unit scaling factor, 1.0 means meters, 100.0 means cm
import_config.set_distance_scale(1.0)
# set imported robot as default prim
import_config.set_make_default_prim(True)
# add a physics scene to the stage on import if none exists
import_config.set_create_physics_scene(False)
# set flag to parse <site> tag
import_config.set_import_sites(True)
# -- instancing settings
# meshes will be placed in a separate usd file
import_config.set_make_instanceable(cfg.make_instanceable)
import_config.set_instanceable_usd_path(self.usd_instanceable_meshes_path)
# -- asset settings
# default density used for links, use 0 to auto-compute
import_config.set_density(cfg.link_density)
# import inertia tensor from urdf, if it is not specified in urdf it will import as identity
import_config.set_import_inertia_tensor(cfg.import_inertia_tensor)
# -- physics settings
# create fix joint for base link
import_config.set_fix_base(cfg.fix_base)
# self collisions between links in the articulation
import_config.set_self_collision(cfg.self_collision)
return import_config
# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
from dataclasses import MISSING
from omni.isaac.lab.sim.converters.asset_converter_base_cfg import AssetConverterBaseCfg
from omni.isaac.lab.utils import configclass
@configclass
class MjcfConverterCfg(AssetConverterBaseCfg):
"""The configuration class for MjcfConverter."""
link_density = 0.0
"""Default density used for links. Defaults to 0.
This setting is only effective if ``"inertial"`` properties are missing in the MJCF.
"""
import_inertia_tensor: bool = True
"""Import the inertia tensor from mjcf. Defaults to True.
If the ``"inertial"`` tag is missing, then it is imported as an identity.
"""
fix_base: bool = MISSING
"""Create a fix joint to the root/base link. Defaults to True."""
import_sites: bool = True
"""Import the sites from the MJCF. Defaults to True."""
self_collision: bool = False
"""Activate self-collisions between links of the articulation. Defaults to False."""
# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Launch Isaac Sim Simulator first."""
from omni.isaac.lab.app import AppLauncher, run_tests
# launch omniverse app
config = {"headless": True}
simulation_app = AppLauncher(config).app
"""Rest everything follows."""
import os
import unittest
import omni.isaac.core.utils.prims as prim_utils
import omni.isaac.core.utils.stage as stage_utils
from omni.isaac.core.simulation_context import SimulationContext
from omni.isaac.core.utils.extensions import enable_extension, get_extension_path_from_name
from omni.isaac.lab.sim.converters import MjcfConverter, MjcfConverterCfg
class TestMjcfConverter(unittest.TestCase):
"""Test fixture for the MjcfConverter class."""
def setUp(self):
"""Create a blank new stage for each test."""
# Create a new stage
stage_utils.create_new_stage()
# retrieve path to mjcf importer extension
enable_extension("omni.importer.mjcf")
extension_path = get_extension_path_from_name("omni.importer.mjcf")
# default configuration
self.config = MjcfConverterCfg(
asset_path=f"{extension_path}/data/mjcf/nv_ant.xml",
import_sites=True,
fix_base=False,
make_instanceable=True,
)
# Simulation time-step
self.dt = 0.01
# Load kit helper
self.sim = SimulationContext(physics_dt=self.dt, rendering_dt=self.dt, backend="numpy")
def tearDown(self) -> None:
"""Stops simulator after each test."""
# stop simulation
self.sim.stop()
# cleanup stage and context
self.sim.clear()
self.sim.clear_all_callbacks()
self.sim.clear_instance()
def test_no_change(self):
"""Call conversion twice. This should not generate a new USD file."""
mjcf_converter = MjcfConverter(self.config)
time_usd_file_created = os.stat(mjcf_converter.usd_path).st_mtime_ns
# no change to config only define the usd directory
new_config = self.config
new_config.usd_dir = mjcf_converter.usd_dir
# convert to usd but this time in the same directory as previous step
new_mjcf_converter = MjcfConverter(new_config)
new_time_usd_file_created = os.stat(new_mjcf_converter.usd_path).st_mtime_ns
self.assertEqual(time_usd_file_created, new_time_usd_file_created)
def test_config_change(self):
"""Call conversion twice but change the config in the second call. This should generate a new USD file."""
mjcf_converter = MjcfConverter(self.config)
time_usd_file_created = os.stat(mjcf_converter.usd_path).st_mtime_ns
# change the config
new_config = self.config
new_config.fix_base = not self.config.fix_base
# define the usd directory
new_config.usd_dir = mjcf_converter.usd_dir
# convert to usd but this time in the same directory as previous step
new_mjcf_converter = MjcfConverter(new_config)
new_time_usd_file_created = os.stat(new_mjcf_converter.usd_path).st_mtime_ns
self.assertNotEqual(time_usd_file_created, new_time_usd_file_created)
def test_create_prim_from_usd(self):
"""Call conversion and create a prim from it."""
urdf_converter = MjcfConverter(self.config)
prim_path = "/World/Robot"
prim_utils.create_prim(prim_path, usd_path=urdf_converter.usd_path)
self.assertTrue(prim_utils.is_prim_path_valid(prim_path))
if __name__ == "__main__":
run_tests()
# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""
Utility to convert a MJCF into USD format.
MuJoCo XML Format (MJCF) is an XML file format used in MuJoCo to describe all elements of a robot. For more information, see: http://www.mujoco.org/book/XMLreference.html
This script uses the MJCF importer extension from Isaac Sim (``omni.isaac.mjcf_importer``) to convert a MJCF asset into USD format. It is designed as a convenience script for command-line use. For more information on the MJCF importer, see the documentation for the extension:
https://docs.omniverse.nvidia.com/app_isaacsim/app_isaacsim/ext_omni_isaac_mjcf.html
positional arguments:
input The path to the input URDF file.
output The path to store the USD file.
optional arguments:
-h, --help Show this help message and exit
--fix-base Fix the base to where it is imported. (default: False)
--import-sites Import sites by parse <site> tag. (default: True)
--make-instanceable Make the asset instanceable for efficient cloning. (default: False)
"""
"""Launch Isaac Sim Simulator first."""
import argparse
from omni.isaac.lab.app import AppLauncher
# add argparse arguments
parser = argparse.ArgumentParser(description="Utility to convert a MJCF into USD format.")
parser.add_argument("input", type=str, help="The path to the input MJCF file.")
parser.add_argument("output", type=str, help="The path to store the USD file.")
parser.add_argument("--fix-base", action="store_true", default=False, help="Fix the base to where it is imported.")
parser.add_argument(
"--import-sites", action="store_true", default=False, help="Import sites by parsing the <site> tag."
)
parser.add_argument(
"--make-instanceable",
action="store_true",
default=False,
help="Make the asset instanceable for efficient cloning.",
)
# append AppLauncher cli args
AppLauncher.add_app_launcher_args(parser)
# parse the arguments
args_cli = parser.parse_args()
# launch omniverse app
app_launcher = AppLauncher(args_cli)
simulation_app = app_launcher.app
"""Rest everything follows."""
import contextlib
import os
import carb
import omni.isaac.core.utils.stage as stage_utils
import omni.kit.app
from omni.isaac.lab.sim.converters import MjcfConverter, MjcfConverterCfg
from omni.isaac.lab.utils.assets import check_file_path
from omni.isaac.lab.utils.dict import print_dict
def main():
# check valid file path
mjcf_path = args_cli.input
if not os.path.isabs(mjcf_path):
mjcf_path = os.path.abspath(mjcf_path)
if not check_file_path(mjcf_path):
raise ValueError(f"Invalid file path: {mjcf_path}")
# create destination path
dest_path = args_cli.output
if not os.path.isabs(dest_path):
dest_path = os.path.abspath(dest_path)
# create the converter configuration
mjcf_converter_cfg = MjcfConverterCfg(
asset_path=mjcf_path,
usd_dir=os.path.dirname(dest_path),
usd_file_name=os.path.basename(dest_path),
fix_base=args_cli.fix_base,
import_sites=args_cli.import_sites,
force_usd_conversion=True,
make_instanceable=args_cli.make_instanceable,
)
# Print info
print("-" * 80)
print("-" * 80)
print(f"Input MJCF file: {mjcf_path}")
print("MJCF importer config:")
print_dict(mjcf_converter_cfg.to_dict(), nesting=0)
print("-" * 80)
print("-" * 80)
# Create mjcf converter and import the file
mjcf_converter = MjcfConverter(mjcf_converter_cfg)
# print output
print("MJCF importer output:")
print(f"Generated USD file: {mjcf_converter.usd_path}")
print("-" * 80)
print("-" * 80)
# Determine if there is a GUI to update:
# acquire settings interface
carb_settings_iface = carb.settings.get_settings()
# read flag for whether a local GUI is enabled
local_gui = carb_settings_iface.get("/app/window/enabled")
# read flag for whether livestreaming GUI is enabled
livestream_gui = carb_settings_iface.get("/app/livestream/enabled")
# Simulate scene (if not headless)
if local_gui or livestream_gui:
# Open the stage with USD
stage_utils.open_stage(mjcf_converter.usd_path)
# Reinitialize the simulation
app = omni.kit.app.get_app_interface()
# Run simulation
with contextlib.suppress(KeyboardInterrupt):
while app.is_running():
# perform step
app.update()
if __name__ == "__main__":
# run the main function
main()
# close sim app
simulation_app.close()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment