diff --git a/docs/source/overview/sim/sim_manager.md b/docs/source/overview/sim/sim_manager.md index 2eca587c..b7d86691 100644 --- a/docs/source/overview/sim/sim_manager.md +++ b/docs/source/overview/sim/sim_manager.md @@ -85,6 +85,44 @@ The manager provides methods to add, retrieve and remove various simulation asse For more details on simulation assets, please refer to their respective documentation pages. +### USD Import and Export + +#### Importing USD Files + +EmbodiChain supports importing USD files (`.usd`, `.usda`, `.usdc`) for both rigid objects and articulations. When importing USD files, you can choose whether to use the physical properties defined in the USD file or override them with configuration values: + +```python +# Import rigid object with USD properties +rigid_cfg = RigidObjectCfg( + shape=MeshCfg(fpath=get_data_path("path/to/object.usd")), + use_usd_properties=True # Use properties from USD file +) +obj = sim.add_rigid_object(cfg=rigid_cfg) + +# Import articulation with USD properties +robot_cfg = ArticulationCfg( + fpath=get_data_path("path/to/robot.usd"), + use_usd_properties=True # Use joint drive properties from USD +) +robot = sim.add_articulation(cfg=robot_cfg) +``` + +#### Exporting to USD + +You can export the current simulation scene to a USD file using the `export_usd()` method: + +```python +# Export the entire scene to USD +sim.export_usd("my_scene.usda") +``` + +This exports all objects, articulations, robots, and their current states to a USD file, which can be: +- Reimported into EmbodiChain with preserved properties +- Opened in USD-compatible tools (e.g., USD Viewer, Omniverse) +- Used as assets for other simulations + +See `scripts/tutorials/sim/export_usd.py` for a complete example. + ## Simulation Loop ### Manual Update mode diff --git a/embodichain/lab/sim/objects/articulation.py b/embodichain/lab/sim/objects/articulation.py index bee50f71..d7a94d94 100644 --- a/embodichain/lab/sim/objects/articulation.py +++ b/embodichain/lab/sim/objects/articulation.py @@ -629,6 +629,24 @@ def __init__( self.default_joint_max_effort = self._data.qf_limits.clone() self.default_joint_max_velocity = self._data.qvel_limits.clone() + # Write the USD properties back to cfg + usd_drive_pros = self.cfg.drive_pros + usd_drive_pros.stiffness = ( + self.default_joint_stiffness[0].cpu().numpy().tolist() + ) + usd_drive_pros.damping = ( + self.default_joint_damping[0].cpu().numpy().tolist() + ) + usd_drive_pros.friction = ( + self.default_joint_friction[0].cpu().numpy().tolist() + ) + usd_drive_pros.max_effort = ( + self.default_joint_max_effort[0].cpu().numpy().tolist() + ) + usd_drive_pros.max_velocity = ( + self.default_joint_max_velocity[0].cpu().numpy().tolist() + ) + self.pk_chain = None if self.cfg.build_pk_chain: self.pk_chain = create_pk_chain( diff --git a/embodichain/lab/sim/objects/rigid_object.py b/embodichain/lab/sim/objects/rigid_object.py index a1a45baa..43acf238 100644 --- a/embodichain/lab/sim/objects/rigid_object.py +++ b/embodichain/lab/sim/objects/rigid_object.py @@ -216,6 +216,15 @@ def __init__( for entity in entities: entity.set_body_scale(*cfg.body_scale) entity.set_physical_attr(cfg.attrs.attr()) + else: + # Read current properties from USD-loaded entities and write back to cfg + # Use first entity as reference + first_entity: MeshObject = entities[0] + + cfg.body_scale = tuple(first_entity.get_body_scale()) + cfg.attrs = RigidBodyAttributesCfg().from_dict( + first_entity.get_physical_attr().as_dict() + ) if device.type == "cuda": self._world.update(0.001) @@ -872,8 +881,7 @@ def reset(self, env_ids: Sequence[int] | None = None) -> None: local_env_ids = self._all_indices if env_ids is None else env_ids num_instances = len(local_env_ids) - if not self.cfg.use_usd_properties: - self.set_attrs(self.cfg.attrs, env_ids=local_env_ids) + self.set_attrs(self.cfg.attrs, env_ids=local_env_ids) pos = torch.as_tensor( self.cfg.init_pos, dtype=torch.float32, device=self.device diff --git a/embodichain/lab/sim/sim_manager.py b/embodichain/lab/sim/sim_manager.py index e592bd06..ea7c656b 100644 --- a/embodichain/lab/sim/sim_manager.py +++ b/embodichain/lab/sim/sim_manager.py @@ -1705,6 +1705,23 @@ def reset_objects_state( if uid not in excluded_uids: sensor.reset(env_ids) + def export_usd(self, fpath: str) -> bool: + """Export the current simulation scene to a USD file. + + Args: + fpath (str): The file path to save the USD file. + + Returns: + bool: True if export is successful, False otherwise. + """ + try: + self._env.export_to_usd_file(fpath) + logger.log_info(f"Simulation scene exported to USD file: {fpath}") + return True + except Exception as e: + logger.log_error(f"Failed to export simulation scene to USD: {e}") + return False + def destroy(self) -> None: """Destroy all simulated assets and release resources.""" # Clean up all gizmos before destroying the simulation diff --git a/pyproject.toml b/pyproject.toml index ccc73cbb..2702b6b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ dynamic = ["version"] # Core install dependencies (kept from requirements.txt). Some VCS links are # specified using PEP 508 direct references where present. dependencies = [ - "dexsim_engine==0.3.10", + "dexsim_engine==0.3.11", "setuptools>=78.1.1", "gymnasium>=0.29.1", "langchain", diff --git a/scripts/tutorials/sim/export_usd.py b/scripts/tutorials/sim/export_usd.py new file mode 100644 index 00000000..90e81691 --- /dev/null +++ b/scripts/tutorials/sim/export_usd.py @@ -0,0 +1,284 @@ +# ---------------------------------------------------------------------------- +# Copyright (c) 2021-2026 DexForce Technology Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ---------------------------------------------------------------------------- + +""" +This script demonstrates how to export a simulation scene to a usd file using the SimulationManager. +""" + +import argparse +import numpy as np +from embodichain.lab.sim import SimulationManager, SimulationManagerCfg +from embodichain.lab.sim.objects import Robot, RigidObject +from embodichain.lab.sim.cfg import ( + LightCfg, + JointDrivePropertiesCfg, + RigidObjectCfg, + RigidBodyAttributesCfg, + ArticulationCfg, +) +from embodichain.lab.sim.shapes import MeshCfg +from embodichain.data import get_data_path +from embodichain.utils import logger + +from embodichain.lab.sim.robots.dexforce_w1.cfg import DexforceW1Cfg + + +def parse_arguments(): + """ + Parse command-line arguments to configure the simulation. + + Returns: + argparse.Namespace: Parsed arguments including number of environments and rendering options. + """ + parser = argparse.ArgumentParser( + description="Create and simulate a robot in SimulationManager" + ) + + parser.add_argument( + "--enable_rt", action="store_true", help="Enable ray tracing rendering" + ) + parser.add_argument("--headless", action="store_true", help="Enable headless mode") + parser.add_argument( + "--device", + type=str, + default="cpu", + help="device to run the environment on, e.g., 'cpu' or 'cuda'", + ) + return parser.parse_args() + + +def initialize_simulation(args) -> SimulationManager: + """ + Initialize the simulation environment based on the provided arguments. + + Args: + args (argparse.Namespace): Parsed command-line arguments. + + Returns: + SimulationManager: Configured simulation manager instance. + """ + config = SimulationManagerCfg( + headless=True, + sim_device=args.device, + enable_rt=args.enable_rt, + physics_dt=1.0 / 100.0, + num_envs=1, + arena_space=2.5, + ) + sim = SimulationManager(config) + + if args.enable_rt: + light = sim.add_light( + cfg=LightCfg( + uid="main_light", + color=(0.6, 0.6, 0.6), + intensity=30.0, + init_pos=(1.0, 0, 3.0), + ) + ) + + return sim + + +def create_robot(sim: SimulationManager) -> Robot: + """ + Create and configure a robot with an arm and a dexterous hand in the simulation. + + Args: + sim (SimulationManager): The simulation manager instance. + + Returns: + Robot: The configured robot instance added to the simulation. + """ + cfg = DexforceW1Cfg.from_dict( + { + "uid": "dexforce_w1", + "init_pos": [0.4, -0.5, 0.0], + } + ) + cfg.solver_cfg["left_arm"].tcp = np.array( + [ + [1.0, 0.0, 0.0, 0.012], + [0.0, 1.0, 0.0, 0.04], + [0.0, 0.0, 1.0, 0.11], + [0.0, 0.0, 0.0, 1.0], + ] + ) + cfg.solver_cfg["right_arm"].tcp = np.array( + [ + [1.0, 0.0, 0.0, 0.012], + [0.0, 1.0, 0.0, -0.04], + [0.0, 0.0, 1.0, 0.11], + [0.0, 0.0, 0.0, 1.0], + ] + ) + + cfg.init_qpos = [ + 1.0000e00, + -2.0000e00, + 1.0000e00, + 0.0000e00, + -2.6921e-05, + -2.6514e-03, + -1.5708e00, + 1.4575e00, + -7.8540e-01, + 1.2834e-01, + 1.5708e00, + -2.2310e00, + -7.8540e-01, + 1.4461e00, + -1.5708e00, + 1.6716e00, + 7.8540e-01, + 7.6745e-01, + 0.0000e00, + 3.8108e-01, + 0.0000e00, + 0.0000e00, + 0.0000e00, + 0.0000e00, + 1.5000e00, + 0.0000e00, + 0.0000e00, + 0.0000e00, + 0.0000e00, + 1.5000e00, + 6.9974e-02, + 7.3950e-02, + 6.6574e-02, + 6.0923e-02, + 0.0000e00, + 6.7342e-02, + 7.0862e-02, + 6.3684e-02, + 5.7822e-02, + 0.0000e00, + ] + return sim.add_robot(cfg=cfg) + + +def create_table(sim: SimulationManager) -> RigidObject: + """ + Create a table rigid object in the simulation. + + Args: + sim (SimulationManager): The simulation manager instance. + + Returns: + RigidObject: The table object added to the simulation. + """ + scoop_cfg = RigidObjectCfg( + uid="table", + shape=MeshCfg( + fpath=get_data_path("MultiW1Data/table_a.obj"), + ), + attrs=RigidBodyAttributesCfg( + mass=0.5, + ), + max_convex_hull_num=8, + body_type="kinematic", + init_pos=[1.1, -0.5, 0.08], + init_rot=[0.0, 0.0, 0.0], + ) + scoop = sim.add_rigid_object(cfg=scoop_cfg) + return scoop + + +def create_caffe(sim: SimulationManager) -> Robot: + """ + Create a caffe (container) articulated object in the simulation. + + Args: + sim (SimulationManager): The simulation manager instance. + + Returns: + Robot: The caffe object added to the simulation. + """ + container_cfg = ArticulationCfg( + uid="caffe", + fpath=get_data_path("MultiW1Data/cafe/cafe.urdf"), + init_pos=[1.05, -0.5, 0.79], + init_rot=[0, 0, -30], + attrs=RigidBodyAttributesCfg( + mass=1.0, + ), + drive_pros=JointDrivePropertiesCfg( + stiffness=1.0, damping=0.1, max_effort=100.0, drive_type="force" + ), + ) + print(f"Loading URDF file from: {container_cfg.fpath}") + container = sim.add_articulation(cfg=container_cfg) + return container + + +def create_cup(sim: SimulationManager) -> RigidObject: + """ + Create a cup rigid object in the simulation. + + Args: + sim (SimulationManager): The simulation manager instance. + + Returns: + RigidObject: The cup object added to the simulation. + """ + scoop_cfg = RigidObjectCfg( + uid="cup", + shape=MeshCfg( + fpath=get_data_path("MultiW1Data/paper_cup_2.obj"), + ), + attrs=RigidBodyAttributesCfg( + mass=0.3, + ), + max_convex_hull_num=1, + body_type="dynamic", + init_pos=[0.86, -0.76, 0.841], + init_rot=[0.0, 0.0, 0.0], + ) + scoop = sim.add_rigid_object(cfg=scoop_cfg) + return scoop + + +def main(): + """ + Main function to create simulation scene. + + Initializes the simulation and creates the robot and objects in the scene. + """ + args = parse_arguments() + sim = initialize_simulation(args) + + robot = create_robot(sim) + table = create_table(sim) + caffe = create_caffe(sim) + cup = create_cup(sim) + + sim.export_usd("w1_coffee_scene.usda") + + logger.log_info("Scene exported successfully.") + + if not args.headless: + sim.open_window() + logger.log_info("Press Ctrl+C to exit.") + try: + while True: + sim.update(step=1) + except KeyboardInterrupt: + logger.log_info("\nExit") + + +if __name__ == "__main__": + main() diff --git a/tests/sim/objects/test_usd.py b/tests/sim/objects/test_usd.py index 06258c20..350c9daf 100644 --- a/tests/sim/objects/test_usd.py +++ b/tests/sim/objects/test_usd.py @@ -55,7 +55,7 @@ def test_import_rigid(self): shape=MeshCfg(fpath=sugar_box_path), body_type="dynamic", use_usd_properties=False, - init_pos=[0.0, 0.0, 0.1], + init_pos=[0.0, 1.0, 0.1], attrs=default_attr, ) ) @@ -109,7 +109,7 @@ def test_usd_properties(self): fpath=h1_path, build_pk_chain=False, use_usd_properties=True, - init_pos=[0.0, 0.0, 1.2], + init_pos=[1.0, 0.0, 1.2], ) ) @@ -148,7 +148,7 @@ def test_usd_properties(self): shape=MeshCfg(fpath=sugar_box_path), body_type="dynamic", use_usd_properties=True, - init_pos=[0.0, 0.0, 0.1], + init_pos=[1.0, 1.0, 0.1], ) ) body0 = sugar_box._entities[0].get_physical_body() @@ -160,6 +160,9 @@ def test_usd_properties(self): # assert(body0.get_solver_iteration_counts()==(4, 1)) # assert(body0.get_max_angular_velocity()==100) + def export_usd(self): + self.sim.export_usd("test_export.usda") + def teardown_method(self): """Clean up resources after each test method.""" self.sim.destroy() @@ -181,6 +184,7 @@ def setup_method(self): test.setup_method() test.test_import_rigid() test.test_import_articulation() + test.export_usd() test.test_usd_properties() # from IPython import embed