diff --git a/docs/source/api/lab/omni.isaac.lab.assets.rst b/docs/source/api/lab/omni.isaac.lab.assets.rst index b77cdb61ef..d7dcfbac19 100644 --- a/docs/source/api/lab/omni.isaac.lab.assets.rst +++ b/docs/source/api/lab/omni.isaac.lab.assets.rst @@ -15,6 +15,9 @@ Articulation ArticulationData ArticulationCfg + DeformableObject + DeformableObjectData + DeformableObjectCfg .. currentmodule:: omni.isaac.lab.assets @@ -67,3 +70,23 @@ Articulation :inherited-members: :show-inheritance: :exclude-members: __init__, class_type + +Deformable Object +----------------- + +.. autoclass:: DeformableObject + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: DeformableObjectData + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__ + +.. autoclass:: DeformableObjectCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type diff --git a/docs/source/how-to/save_camera_output.rst b/docs/source/how-to/save_camera_output.rst index 94a834492a..21a3927126 100644 --- a/docs/source/how-to/save_camera_output.rst +++ b/docs/source/how-to/save_camera_output.rst @@ -99,4 +99,4 @@ The simulation should start, and you can observe different objects falling down. in the ``IsaacLab/source/standalone/tutorials/04_sensors`` directory, where the images will be saved. Additionally, you should see the point cloud in the 3D space drawn on the viewport. -To stop the simulation, close the window, press the ``STOP`` button in the UI, or use ``Ctrl+C`` in the terminal. +To stop the simulation, close the window, or use ``Ctrl+C`` in the terminal. diff --git a/docs/source/tutorials/01_assets/index.rst b/docs/source/tutorials/01_assets/index.rst index 4fd5ec69d0..4f02bb351e 100644 --- a/docs/source/tutorials/01_assets/index.rst +++ b/docs/source/tutorials/01_assets/index.rst @@ -3,8 +3,8 @@ Interacting with Assets Having spawned objects in the scene, these tutorials show you how to create physics handles for these objects and interact with them. These revolve around the :class:`~omni.isaac.lab.assets.AssetBase` -class and its derivatives such as :class:`~omni.isaac.lab.assets.RigidObject` and -:class:`~omni.isaac.lab.assets.Articulation`. +class and its derivatives such as :class:`~omni.isaac.lab.assets.RigidObject`, +:class:`~omni.isaac.lab.assets.Articulation` and :class:`~omni.isaac.lab.assets.DeformableObject`. .. toctree:: :maxdepth: 1 @@ -12,3 +12,4 @@ class and its derivatives such as :class:`~omni.isaac.lab.assets.RigidObject` an run_rigid_object run_articulation + run_deformable_object diff --git a/docs/source/tutorials/01_assets/run_articulation.rst b/docs/source/tutorials/01_assets/run_articulation.rst index 1d1423d270..4fc27e0257 100644 --- a/docs/source/tutorials/01_assets/run_articulation.rst +++ b/docs/source/tutorials/01_assets/run_articulation.rst @@ -122,8 +122,7 @@ To run the code and see the results, let's run the script from the terminal: This command should open a stage with a ground plane, lights, and two cart-poles that are moving around randomly. -To stop the simulation, you can either close the window, press the ``STOP`` button in the UI, or press ``Ctrl+C`` -in the terminal. +To stop the simulation, you can either close the window, or press ``Ctrl+C`` in the terminal. In this tutorial, we learned how to create and interact with a simple articulation. We saw how to set the state of an articulation (its root and joint state) and how to apply commands to it. We also saw how to update its diff --git a/docs/source/tutorials/01_assets/run_deformable_object.rst b/docs/source/tutorials/01_assets/run_deformable_object.rst new file mode 100644 index 0000000000..1348a52d23 --- /dev/null +++ b/docs/source/tutorials/01_assets/run_deformable_object.rst @@ -0,0 +1,175 @@ +.. _tutorial-interact-deformable-object: + + +Interacting with a deformable object +==================================== + +.. currentmodule:: omni.isaac.lab + +While deformable objects sometimes refer to a broader class of objects, such as cloths, fluids and soft bodies, +in PhysX, deformable objects syntactically correspond to soft bodies. Unlike rigid objects, soft bodies can deform +under external forces and collisions. + +Soft bodies are simulated using Finite Element Method (FEM) in PhysX. The soft body comprises of two tetrahedral +meshes -- a simulation mesh and a collision mesh. The simulation mesh is used to simulate the deformations of +the soft body, while the collision mesh is used to detect collisions with other objects in the scene. +For more details, please check the `PhysX documentation`_. + +This tutorial shows how to interact with a deformable object in the simulation. We will spawn a +set of soft cubes and see how to set their nodal positions and velocities, along with apply kinematic +commands to the mesh nodes to move the soft body. + + +The Code +~~~~~~~~ + +The tutorial corresponds to the ``run_deformable_object.py`` script in the ``source/standalone/tutorials/01_assets`` directory. + +.. dropdown:: Code for run_deformable_object.py + :icon: code + + .. literalinclude:: ../../../../source/standalone/tutorials/01_assets/run_deformable_object.py + :language: python + :emphasize-lines: 61-73, 75-77, 102-110, 112-115, 117-118, 123-130, 132-133, 139-140 + :linenos: + + +The Code Explained +~~~~~~~~~~~~~~~~~~ + +Designing the scene +------------------- + +Similar to the :ref:`tutorial-interact-rigid-object` tutorial, we populate the scene with a ground plane +and a light source. In addition, we add a deformable object to the scene using the :class:`assets.DeformableObject` +class. This class is responsible for spawning the prims at the input path and initializes their corresponding +deformable body physics handles. + +In this tutorial, we create a cubical soft object using the spawn configuration similar to the deformable cube +in the :ref:`Spawn Objects ` tutorial. The only difference is that now we wrap +the spawning configuration into the :class:`assets.DeformableObjectCfg` class. This class contains information about +the asset's spawning strategy and default initial state. When this class is passed to +the :class:`assets.DeformableObject` class, it spawns the object and initializes the corresponding physics handles +when the simulation is played. + +.. note:: + The deformable object is only supported in GPU simulation and requires a mesh object to be spawned with the + deformable body physics properties on it. + + +As seen in the rigid body tutorial, we can spawn the deformable object into the scene in a similar fashion by creating +an instance of the :class:`assets.DeformableObject` class by passing the configuration object to its constructor. + +.. literalinclude:: ../../../../source/standalone/tutorials/01_assets/run_deformable_object.py + :language: python + :start-at: # Create separate groups called "Origin1", "Origin2", "Origin3" + :end-at: cube_object = DeformableObject(cfg=cfg) + +Running the simulation loop +--------------------------- + +Continuing from the rigid body tutorial, we reset the simulation at regular intervals, apply kinematic commands +to the deformable body, step the simulation, and update the deformable object's internal buffers. + +Resetting the simulation state +"""""""""""""""""""""""""""""" + +Unlike rigid bodies and articulations, deformable objects have a different state representation. The state of a +deformable object is defined by the nodal positions and velocities of the mesh. The nodal positions and velocities +are defined in the **simulation world frame** and are stored in the :attr:`assets.DeformableObject.data` attribute. + +We use the :attr:`assets.DeformableObject.data.default_nodal_state_w` attribute to get the default nodal state of the +spawned object prims. This default state can be configured from the :attr:`assets.DeformableObjectCfg.init_state` +attribute, which we left as identity in this tutorial. + +.. attention:: + The initial state in the configuration :attr:`assets.DeformableObjectCfg` specifies the pose + of the deformable object at the time of spawning. Based on this initial state, the default nodal state is + obtained when the simulation is played for the first time. + +We apply transformations to the nodal positions to randomize the initial state of the deformable object. + +.. literalinclude:: ../../../../source/standalone/tutorials/01_assets/run_deformable_object.py + :language: python + :start-at: # reset the nodal state of the object + :end-at: nodal_state[..., :3] = cube_object.transform_nodal_pos(nodal_state[..., :3], pos_w, quat_w) + +To reset the deformable object, we first set the nodal state by calling the :meth:`assets.DeformableObject.write_nodal_state_to_sim` +method. This method writes the nodal state of the deformable object prim into the simulation buffer. +Additionally, we free all the kinematic targets set for the nodes in the previous simulation step by calling +the :meth:`assets.DeformableObject.write_nodal_kinematic_target_to_sim` method. We explain the +kinematic targets in the next section. + +Finally, we call the :meth:`assets.DeformableObject.reset` method to reset any internal buffers and caches. + +.. literalinclude:: ../../../../source/standalone/tutorials/01_assets/run_deformable_object.py + :language: python + :start-at: # write nodal state to simulation + :end-at: cube_object.reset() + +Stepping the simulation +""""""""""""""""""""""" + +Deformable bodies support user-driven kinematic control where a user can specify position targets for some of +the mesh nodes while the rest of the nodes are simulated using the FEM solver. This `partial kinematic`_ control +is useful for applications where the user wants to interact with the deformable object in a controlled manner. + +In this tutorial, we apply kinematic commands to two out of the four cubes in the scene. We set the position +targets for the node at index 0 (bottom-left corner) to move the cube along the z-axis. + +At every step, we increment the kinematic position target for the node by a small value. Additionally, +we set the flag to indicate that the target is a kinematic target for that node in the simulation buffer. +These are set into the simulation buffer by calling the :meth:`assets.DeformableObject.write_nodal_kinematic_target_to_sim` +method. + +.. literalinclude:: ../../../../source/standalone/tutorials/01_assets/run_deformable_object.py + :language: python + :start-at: # update the kinematic target for cubes at index 0 and 3 + :end-at: cube_object.write_nodal_kinematic_target_to_sim(nodal_kinematic_target) + +Similar to the rigid object and articulation, we perform the :meth:`assets.DeformableObject.write_data_to_sim` method +before stepping the simulation. For deformable objects, this method does not apply any external forces to the object. +However, we keep this method for completeness and future extensions. + +.. literalinclude:: ../../../../source/standalone/tutorials/01_assets/run_deformable_object.py + :language: python + :start-at: # write internal data to simulation + :end-at: cube_object.write_data_to_sim() + +Updating the state +"""""""""""""""""" + +After stepping the simulation, we update the internal buffers of the deformable object prims to reflect their new state +inside the :class:`assets.DeformableObject.data` attribute. This is done using the :meth:`assets.DeformableObject.update` method. + +At a fixed interval, we print the root position of the deformable object to the terminal. As mentioned +earlier, there is no concept of a root state for deformable objects. However, we compute the root position as +the average position of all the nodes in the mesh. + +.. literalinclude:: ../../../../source/standalone/tutorials/01_assets/run_deformable_object.py + :language: python + :start-at: # update buffers + :end-at: print(f"Root position (in world): {cube_object.data.root_pos_w[:, :3]}") + + +The Code Execution +~~~~~~~~~~~~~~~~~~ + +Now that we have gone through the code, let's run the script and see the result: + +.. code-block:: bash + + ./isaaclab.sh -p source/standalone/tutorials/01_assets/run_deformable_object.py + + +This should open a stage with a ground plane, lights, and several green cubes. Two of the four cubes must be dropping +from a height and settling on to the ground. Meanwhile the other two cubes must be moving along the z-axis. You +should see a marker showing the kinematic target position for the nodes at the bottom-left corner of the cubes. +To stop the simulation, you can either close the window, or press ``Ctrl+C`` in the terminal + +This tutorial showed how to spawn rigid objects and wrap them in a :class:`DeformableObject` class to initialize their +physics handles which allows setting and obtaining their state. In the next tutorial, we will see how to interact +with an articulated object which is a collection of rigid objects connected by joints. + +.. _PhysX documentation: https://nvidia-omniverse.github.io/PhysX/physx/5.4.1/docs/SoftBodies.html +.. _partial kinematic: https://nvidia-omniverse.github.io/PhysX/physx/5.4.1/docs/SoftBodies.html#kinematic-soft-bodies diff --git a/docs/source/tutorials/05_controllers/run_diff_ik.rst b/docs/source/tutorials/05_controllers/run_diff_ik.rst index 7aee394e2e..0a745d8b02 100644 --- a/docs/source/tutorials/05_controllers/run_diff_ik.rst +++ b/docs/source/tutorials/05_controllers/run_diff_ik.rst @@ -150,5 +150,4 @@ The script will start a simulation with 128 robots. The robots will be controlle The current and desired end-effector poses should be displayed using frame markers. When the robot reaches the desired pose, the command should cycle through to the next pose specified in the script. -To stop the simulation, you can either close the window, or press the ``STOP`` button in the UI, or -press ``Ctrl+C`` in the terminal. +To stop the simulation, you can either close the window, or press ``Ctrl+C`` in the terminal. diff --git a/source/extensions/omni.isaac.lab/config/extension.toml b/source/extensions/omni.isaac.lab/config/extension.toml index b738a1431e..1e2e28c633 100644 --- a/source/extensions/omni.isaac.lab/config/extension.toml +++ b/source/extensions/omni.isaac.lab/config/extension.toml @@ -1,7 +1,7 @@ [package] # Note: Semantic Versioning is used: https://semver.org/ -version = "0.22.0" +version = "0.22.1" # Description title = "Isaac Lab framework for Robot Learning" diff --git a/source/extensions/omni.isaac.lab/docs/CHANGELOG.rst b/source/extensions/omni.isaac.lab/docs/CHANGELOG.rst index c8f0418ed1..cb1acca8c3 100644 --- a/source/extensions/omni.isaac.lab/docs/CHANGELOG.rst +++ b/source/extensions/omni.isaac.lab/docs/CHANGELOG.rst @@ -1,6 +1,17 @@ Changelog --------- +0.22.1 (2024-08-17) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added APIs to interact with the physics simulation of deformable objects. This includes setting the + material properties, setting kinematic targets, and getting the state of the deformable object. + For more information, please refer to the :mod:`omni.isaac.lab.assets.DeformableObject` class. + + 0.22.0 (2024-08-14) ~~~~~~~~~~~~~~~~~~~ diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/__init__.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/__init__.py index 0b983a2f71..f0d29abcae 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/__init__.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/__init__.py @@ -41,4 +41,5 @@ from .articulation import Articulation, ArticulationCfg, ArticulationData from .asset_base import AssetBase from .asset_base_cfg import AssetBaseCfg +from .deformable_object import DeformableObject, DeformableObjectCfg, DeformableObjectData from .rigid_object import RigidObject, RigidObjectCfg, RigidObjectData diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/articulation/articulation.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/articulation/articulation.py index 6405bb3c35..350134222f 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/articulation/articulation.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/articulation/articulation.py @@ -909,6 +909,10 @@ def _initialize_impl(self): # -- articulation self._root_physx_view = self._physics_sim_view.create_articulation_view(root_prim_path_expr.replace(".*", "*")) + # check if the articulation was created + if self._root_physx_view._backend is None: + raise RuntimeError(f"Failed to create articulation at: {self.cfg.prim_path}. Please check PhysX logs.") + # log information about the articulation carb.log_info(f"Articulation initialized at: {self.cfg.prim_path} with root '{root_prim_path_expr}'.") carb.log_info(f"Is fixed root: {self.is_fixed_base}") diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/deformable_object/__init__.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/deformable_object/__init__.py new file mode 100644 index 0000000000..33d1c733b5 --- /dev/null +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/deformable_object/__init__.py @@ -0,0 +1,10 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Sub-module for deformable object assets.""" + +from .deformable_object import DeformableObject +from .deformable_object_cfg import DeformableObjectCfg +from .deformable_object_data import DeformableObjectData diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/deformable_object/deformable_object.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/deformable_object/deformable_object.py new file mode 100644 index 0000000000..4a57c316c9 --- /dev/null +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/deformable_object/deformable_object.py @@ -0,0 +1,413 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import torch +from collections.abc import Sequence +from typing import TYPE_CHECKING + +import carb +import omni.physics.tensors.impl.api as physx +from pxr import PhysxSchema, UsdShade + +import omni.isaac.lab.sim as sim_utils +import omni.isaac.lab.utils.math as math_utils +from omni.isaac.lab.markers import VisualizationMarkers + +from ..asset_base import AssetBase +from .deformable_object_data import DeformableObjectData + +if TYPE_CHECKING: + from .deformable_object_cfg import DeformableObjectCfg + + +class DeformableObject(AssetBase): + """A deformable object asset class. + + Deformable objects are assets that can be deformed in the simulation. They are typically used for + soft bodies, such as stuffed animals and food items. + + Unlike rigid object assets, deformable objects have a more complex structure and require additional + handling for simulation. The simulation of deformable objects follows a finite element approach, where + the object is discretized into a mesh of nodes and elements. The nodes are connected by elements, which + define the material properties of the object. The nodes can be moved and deformed, and the elements + respond to these changes. + + The state of a deformable object comprises of its nodal positions and velocities, and not the object's root + position and orientation. The nodal positions and velocities are in the simulation frame. + + Soft bodies can be `partially kinematic`_, where some nodes are driven by kinematic targets, and the rest are + simulated. The kinematic targets are the desired positions of the nodes, and the simulation drives the nodes + towards these targets. This is useful for partial control of the object, such as moving a stuffed animal's + head while the rest of the body is simulated. + + .. attention:: + This class is experimental and subject to change due to changes on the underlying PhysX API on which + it depends. We will try to maintain backward compatibility as much as possible but some changes may be + necessary. + + .. _partially kinematic: https://nvidia-omniverse.github.io/PhysX/physx/5.4.1/docs/SoftBodies.html#kinematic-soft-bodies + """ + + cfg: DeformableObjectCfg + """Configuration instance for the deformable object.""" + + def __init__(self, cfg: DeformableObjectCfg): + """Initialize the deformable object. + + Args: + cfg: A configuration instance. + """ + super().__init__(cfg) + + """ + Properties + """ + + @property + def data(self) -> DeformableObjectData: + return self._data + + @property + def num_instances(self) -> int: + return self.root_physx_view.count + + @property + def num_bodies(self) -> int: + """Number of bodies in the asset. + + This is always 1 since each object is a single deformable body. + """ + return 1 + + @property + def root_physx_view(self) -> physx.SoftBodyView: + """Deformable body view for the asset (PhysX). + + Note: + Use this view with caution. It requires handling of tensors in a specific way. + """ + return self._root_physx_view + + @property + def material_physx_view(self) -> physx.SoftBodyMaterialView | None: + """Deformable material view for the asset (PhysX). + + This view is optional and may not be available if the material is not bound to the deformable body. + If the material is not available, then the material properties will be set to default values. + + Note: + Use this view with caution. It requires handling of tensors in a specific way. + """ + return self._material_physx_view + + @property + def max_sim_elements_per_body(self) -> int: + """The maximum number of simulation mesh elements per deformable body.""" + return self.root_physx_view.max_sim_elements_per_body + + @property + def max_collision_elements_per_body(self) -> int: + """The maximum number of collision mesh elements per deformable body.""" + return self.root_physx_view.max_elements_per_body + + @property + def max_sim_vertices_per_body(self) -> int: + """The maximum number of simulation mesh vertices per deformable body.""" + return self.root_physx_view.max_sim_vertices_per_body + + @property + def max_collision_vertices_per_body(self) -> int: + """The maximum number of collision mesh vertices per deformable body.""" + return self.root_physx_view.max_vertices_per_body + + """ + Operations. + """ + + def reset(self, env_ids: Sequence[int] | None = None): + # Think: Should we reset the kinematic targets when resetting the object? + # This is not done in the current implementation. We assume users will reset the kinematic targets. + pass + + def write_data_to_sim(self): + pass + + def update(self, dt: float): + self._data.update(dt) + + """ + Operations - Write to simulation. + """ + + def write_nodal_state_to_sim(self, nodal_state: torch.Tensor, env_ids: Sequence[int] | None = None): + """Set the nodal state over selected environment indices into the simulation. + + The nodal state comprises of the nodal positions and velocities. Since these are nodes, the velocity only has + a translational component. All the quantities are in the simulation frame. + + Args: + nodal_state: Nodal state in simulation frame. + Shape is (len(env_ids), max_sim_vertices_per_body, 6). + env_ids: Environment indices. If None, then all indices are used. + """ + # set into simulation + self.write_nodal_pos_to_sim(nodal_state[..., :3], env_ids=env_ids) + self.write_nodal_velocity_to_sim(nodal_state[..., 3:], env_ids=env_ids) + + def write_nodal_pos_to_sim(self, nodal_pos: torch.Tensor, env_ids: Sequence[int] | None = None): + """Set the nodal positions over selected environment indices into the simulation. + + The nodal position comprises of individual nodal positions of the simulation mesh for the deformable body. + The positions are in the simulation frame. + + Args: + nodal_pos: Nodal positions in simulation frame. + Shape is (len(env_ids), max_sim_vertices_per_body, 3). + env_ids: Environment indices. If None, then all indices are used. + """ + # resolve all indices + physx_env_ids = env_ids + if env_ids is None: + env_ids = slice(None) + physx_env_ids = self._ALL_INDICES + # note: we need to do this here since tensors are not set into simulation until step. + # set into internal buffers + self._data.nodal_pos_w[env_ids] = nodal_pos.clone() + # set into simulation + self.root_physx_view.set_sim_nodal_positions(self._data.nodal_pos_w, indices=physx_env_ids) + + def write_nodal_velocity_to_sim(self, nodal_vel: torch.Tensor, env_ids: Sequence[int] | None = None): + """Set the nodal velocity over selected environment indices into the simulation. + + The nodal velocity comprises of individual nodal velocities of the simulation mesh for the deformable + body. Since these are nodes, the velocity only has a translational component. The velocities are in the + simulation frame. + + Args: + nodal_vel: Nodal velocities in simulation frame. + Shape is (len(env_ids), max_sim_vertices_per_body, 3). + env_ids: Environment indices. If None, then all indices are used. + """ + # resolve all indices + physx_env_ids = env_ids + if env_ids is None: + env_ids = slice(None) + physx_env_ids = self._ALL_INDICES + # note: we need to do this here since tensors are not set into simulation until step. + # set into internal buffers + self._data.nodal_vel_w[env_ids] = nodal_vel.clone() + # set into simulation + self.root_physx_view.set_sim_nodal_velocities(self._data.nodal_vel_w, indices=physx_env_ids) + + def write_nodal_kinematic_target_to_sim(self, targets: torch.Tensor, env_ids: Sequence[int] | None = None): + """Set the kinematic targets of the simulation mesh for the deformable bodies indicated by the indices. + + The kinematic targets comprise of individual nodal positions of the simulation mesh for the deformable body + and a flag indicating whether the node is kinematically driven or not. The positions are in the simulation frame. + + Note: + The flag is set to 0.0 for kinematically driven nodes and 1.0 for free nodes. + + Args: + targets: The kinematic targets comprising of nodal positions and flags. + Shape is (len(env_ids), max_sim_vertices_per_body, 4). + env_ids: Environment indices. If None, then all indices are used. + """ + # resolve all indices + physx_env_ids = env_ids + if env_ids is None: + env_ids = slice(None) + physx_env_ids = self._ALL_INDICES + # store into internal buffers + self._data.nodal_kinematic_target[env_ids] = targets.clone() + # set into simulation + self.root_physx_view.set_sim_kinematic_targets(self._data.nodal_kinematic_target, indices=physx_env_ids) + + """ + Operations - Helper. + """ + + def transform_nodal_pos( + self, nodal_pos: torch.tensor, pos: torch.Tensor | None = None, quat: torch.Tensor | None = None + ) -> torch.Tensor: + """Transform the nodal positions based on the pose transformation. + + This function computes the transformation of the nodal positions based on the pose transformation. + It multiplies the nodal positions with the rotation matrix of the pose and adds the translation. + Internally, it calls the :meth:`omni.isaac.lab.utils.math.transform_points` function. + + Args: + nodal_pos: The nodal positions in the simulation frame. Shape is (N, max_sim_vertices_per_body, 3). + pos: The position transformation. Shape is (N, 3). + Defaults to None, in which case the position is assumed to be zero. + quat: The orientation transformation as quaternion (w, x, y, z). Shape is (N, 4). + Defaults to None, in which case the orientation is assumed to be identity. + + Returns: + The transformed nodal positions. Shape is (N, max_sim_vertices_per_body, 3). + """ + # offset the nodal positions to center them around the origin + mean_nodal_pos = nodal_pos.mean(dim=1, keepdim=True) + nodal_pos = nodal_pos - mean_nodal_pos + # transform the nodal positions based on the pose around the origin + return math_utils.transform_points(nodal_pos, pos, quat) + mean_nodal_pos + + """ + Internal helper. + """ + + def _initialize_impl(self): + # create simulation view + self._physics_sim_view = physx.create_simulation_view(self._backend) + self._physics_sim_view.set_subspace_roots("/") + # obtain the first prim in the regex expression (all others are assumed to be a copy of this) + template_prim = sim_utils.find_first_matching_prim(self.cfg.prim_path) + if template_prim is None: + raise RuntimeError(f"Failed to find prim for expression: '{self.cfg.prim_path}'.") + template_prim_path = template_prim.GetPath().pathString + + # find deformable root prims + root_prims = sim_utils.get_all_matching_child_prims( + template_prim_path, predicate=lambda prim: prim.HasAPI(PhysxSchema.PhysxDeformableBodyAPI) + ) + if len(root_prims) == 0: + raise RuntimeError( + f"Failed to find a deformable body when resolving '{self.cfg.prim_path}'." + " Please ensure that the prim has 'PhysxSchema.PhysxDeformableBodyAPI' applied." + ) + if len(root_prims) > 1: + raise RuntimeError( + f"Failed to find a single deformable body when resolving '{self.cfg.prim_path}'." + f" Found multiple '{root_prims}' under '{template_prim_path}'." + " Please ensure that there is only one deformable body in the prim path tree." + ) + # we only need the first one from the list + root_prim = root_prims[0] + + # find deformable material prims + material_prim = None + # obtain material prim from the root prim + # note: here we assume that all the root prims have their material prims at similar paths + # and we only need to find the first one. This may not be the case for all scenarios. + # However, the checks in that case get cumbersome and are not included here. + if root_prim.HasAPI(UsdShade.MaterialBindingAPI): + # check the materials that are bound with the purpose 'physics' + material_paths = UsdShade.MaterialBindingAPI(root_prim).GetDirectBindingRel("physics").GetTargets() + # iterate through targets and find the deformable body material + if len(material_paths) > 0: + for mat_path in material_paths: + mat_prim = root_prim.GetStage().GetPrimAtPath(mat_path) + if mat_prim.HasAPI(PhysxSchema.PhysxDeformableBodyMaterialAPI): + material_prim = mat_prim + break + if material_prim is None: + carb.log_info( + f"Failed to find a deformable material binding for '{root_prim.GetPath().pathString}'." + " The material properties will be set to default values and are not modifiable at runtime." + " If you want to modify the material properties, please ensure that the material is bound" + " to the deformable body." + ) + + # resolve root path back into regex expression + # -- root prim expression + root_prim_path = root_prim.GetPath().pathString + root_prim_path_expr = self.cfg.prim_path + root_prim_path[len(template_prim_path) :] + # -- object view + self._root_physx_view = self._physics_sim_view.create_soft_body_view(root_prim_path_expr.replace(".*", "*")) + + # Return if the asset is not found + if self._root_physx_view._backend is None: + raise RuntimeError(f"Failed to create deformable body at: {self.cfg.prim_path}. Please check PhysX logs.") + + # resolve material path back into regex expression + if material_prim is not None: + # -- material prim expression + material_prim_path = material_prim.GetPath().pathString + # check if the material prim is under the template prim + # if not then we are assuming that the single material prim is used for all the deformable bodies + if template_prim_path in material_prim_path: + material_prim_path_expr = self.cfg.prim_path + material_prim_path[len(template_prim_path) :] + else: + material_prim_path_expr = material_prim_path + # -- material view + self._material_physx_view = self._physics_sim_view.create_soft_body_material_view( + material_prim_path_expr.replace(".*", "*") + ) + else: + self._material_physx_view = None + + # log information about the deformable body + carb.log_info(f"Deformable body initialized at: {root_prim_path_expr}") + carb.log_info(f"Number of instances: {self.num_instances}") + carb.log_info(f"Number of bodies: {self.num_bodies}") + if self._material_physx_view is not None: + carb.log_info(f"Deformable material initialized at: {material_prim_path_expr}") + carb.log_info(f"Number of instances: {self._material_physx_view.count}") + else: + carb.log_info("No deformable material found. Material properties will be set to default values.") + + # container for data access + self._data = DeformableObjectData(self.root_physx_view, self.device) + + # create buffers + self._create_buffers() + # update the deformable body data + self.update(0.0) + + def _create_buffers(self): + """Create buffers for storing data.""" + # constants + self._ALL_INDICES = torch.arange(self.num_instances, dtype=torch.long, device=self.device) + + # default state + # we use the initial nodal positions at spawn time as the default state + # note: these are all in the simulation frame + nodal_positions = self.root_physx_view.get_sim_nodal_positions() + nodal_velocities = torch.zeros_like(nodal_positions) + self._data.default_nodal_state_w = torch.cat((nodal_positions, nodal_velocities), dim=-1) + + # kinematic targets + self._data.nodal_kinematic_target = self.root_physx_view.get_sim_kinematic_targets() + # set all nodes as non-kinematic targets by default + self._data.nodal_kinematic_target[..., -1] = 1.0 + + """ + Internal simulation callbacks. + """ + + def _set_debug_vis_impl(self, debug_vis: bool): + # set visibility of markers + # note: parent only deals with callbacks. not their visibility + if debug_vis: + if not hasattr(self, "target_visualizer"): + self.target_visualizer = VisualizationMarkers(self.cfg.visualizer_cfg) + # set their visibility to true + self.target_visualizer.set_visibility(True) + else: + if hasattr(self, "target_visualizer"): + self.target_visualizer.set_visibility(False) + + def _debug_vis_callback(self, event): + # check where to visualize + targets_enabled = self.data.nodal_kinematic_target[:, :, 3] == 0.0 + num_enabled = int(torch.sum(targets_enabled).item()) + # get positions if any targets are enabled + if num_enabled == 0: + # create a marker below the ground + positions = torch.tensor([[0.0, 0.0, -10.0]], device=self.device) + else: + positions = self.data.nodal_kinematic_target[targets_enabled][..., :3] + # show target visualizer + self.target_visualizer.visualize(positions) + + def _invalidate_initialize_callback(self, event): + """Invalidates the scene elements.""" + # call parent + super()._invalidate_initialize_callback(event) + # set all existing views to None to invalidate them + self._physics_sim_view = None + self._root_physx_view = None diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/deformable_object/deformable_object_cfg.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/deformable_object/deformable_object_cfg.py new file mode 100644 index 0000000000..eb2d0e2b06 --- /dev/null +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/deformable_object/deformable_object_cfg.py @@ -0,0 +1,29 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +from omni.isaac.lab.markers import VisualizationMarkersCfg +from omni.isaac.lab.markers.config import DEFORMABLE_TARGET_MARKER_CFG +from omni.isaac.lab.utils import configclass + +from ..asset_base_cfg import AssetBaseCfg +from .deformable_object import DeformableObject + + +@configclass +class DeformableObjectCfg(AssetBaseCfg): + """Configuration parameters for a deformable object.""" + + class_type: type = DeformableObject + + visualizer_cfg: VisualizationMarkersCfg = DEFORMABLE_TARGET_MARKER_CFG.replace( + prim_path="/Visuals/DeformableTarget" + ) + """The configuration object for the visualization markers. Defaults to DEFORMABLE_TARGET_MARKER_CFG. + + Note: + This attribute is only used when debug visualization is enabled. + """ diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/deformable_object/deformable_object_data.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/deformable_object/deformable_object_data.py new file mode 100644 index 0000000000..b339807c27 --- /dev/null +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/deformable_object/deformable_object_data.py @@ -0,0 +1,238 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import torch +import weakref + +import omni.physics.tensors.impl.api as physx + +import omni.isaac.lab.utils.math as math_utils +from omni.isaac.lab.utils.buffers import TimestampedBuffer + + +class DeformableObjectData: + """Data container for a deformable object. + + This class contains the data for a deformable object in the simulation. The data includes the nodal states of + the root deformable body in the object. The data is stored in the simulation world frame unless otherwise specified. + + A deformable object in PhysX uses two tetrahedral meshes to represent the object: + + 1. **Simulation mesh**: This mesh is used for the simulation and is the one that is deformed by the solver. + 2. **Collision mesh**: This mesh only needs to match the surface of the simulation mesh and is used for + collision detection. + + The APIs exposed provides the data for both the simulation and collision meshes. These are specified + by the `sim` and `collision` prefixes in the property names. + + The data is lazily updated, meaning that the data is only updated when it is accessed. This is useful + when the data is expensive to compute or retrieve. The data is updated when the timestamp of the buffer + is older than the current simulation timestamp. The timestamp is updated whenever the data is updated. + """ + + def __init__(self, root_physx_view: physx.SoftBodyView, device: str): + """Initializes the deformable object data. + + Args: + root_physx_view: The root deformable body view of the object. + device: The device used for processing. + """ + # Set the parameters + self.device = device + # Set the root deformable body view + # note: this is stored as a weak reference to avoid circular references between the asset class + # and the data container. This is important to avoid memory leaks. + self._root_physx_view: physx.SoftBodyView = weakref.proxy(root_physx_view) + + # Set initial time stamp + self._sim_timestamp = 0.0 + + # Initialize the lazy buffers. + # -- node state in simulation world frame + self._nodal_pos_w = TimestampedBuffer() + self._nodal_vel_w = TimestampedBuffer() + self._nodal_state_w = TimestampedBuffer() + # -- mesh element-wise rotations + self._sim_element_quat_w = TimestampedBuffer() + self._collision_element_quat_w = TimestampedBuffer() + # -- mesh element-wise deformation gradients + self._sim_element_deform_gradient_w = TimestampedBuffer() + self._collision_element_deform_gradient_w = TimestampedBuffer() + # -- mesh element-wise stresses + self._sim_element_stress_w = TimestampedBuffer() + self._collision_element_stress_w = TimestampedBuffer() + + def update(self, dt: float): + """Updates the data for the deformable object. + + Args: + dt: The time step for the update. This must be a positive value. + """ + # update the simulation timestamp + self._sim_timestamp += dt + + ## + # Defaults. + ## + + default_nodal_state_w: torch.Tensor = None + """Default nodal state ``[nodal_pos, nodal_vel]`` in simulation world frame. + Shape is (num_instances, max_sim_vertices_per_body, 6). + """ + + ## + # Kinematic commands + ## + + nodal_kinematic_target: torch.Tensor = None + """Simulation mesh kinematic targets for the deformable bodies. + Shape is (num_instances, max_sim_vertices_per_body, 4). + + The kinematic targets are used to drive the simulation mesh vertices to the target positions. + The targets are stored as (x, y, z, is_not_kinematic) where "is_not_kinematic" is a binary + flag indicating whether the vertex is kinematic or not. The flag is set to 0 for kinematic vertices + and 1 for non-kinematic vertices. + """ + + ## + # Properties. + ## + + @property + def nodal_pos_w(self): + """Nodal positions in simulation world frame. Shape is (num_instances, max_sim_vertices_per_body, 3).""" + if self._nodal_pos_w.timestamp < self._sim_timestamp: + self._nodal_pos_w.data = self._root_physx_view.get_sim_nodal_positions() + self._nodal_pos_w.timestamp = self._sim_timestamp + return self._nodal_pos_w.data + + @property + def nodal_vel_w(self): + """Nodal velocities in simulation world frame. Shape is (num_instances, max_sim_vertices_per_body, 3).""" + if self._nodal_vel_w.timestamp < self._sim_timestamp: + self._nodal_vel_w.data = self._root_physx_view.get_sim_nodal_velocities() + self._nodal_vel_w.timestamp = self._sim_timestamp + return self._nodal_vel_w.data + + @property + def nodal_state_w(self): + """Nodal state ``[nodal_pos, nodal_vel]`` in simulation world frame. + Shape is (num_instances, max_sim_vertices_per_body, 6). + """ + if self._nodal_state_w.timestamp < self._sim_timestamp: + nodal_positions = self.nodal_pos_w + nodal_velocities = self.nodal_vel_w + # set the buffer data and timestamp + self._nodal_state_w.data = torch.cat((nodal_positions, nodal_velocities), dim=-1) + self._nodal_state_w.timestamp = self._sim_timestamp + return self._nodal_state_w.data + + @property + def sim_element_quat_w(self): + """Simulation mesh element-wise rotations as quaternions for the deformable bodies in simulation world frame. + Shape is (num_instances, max_sim_elements_per_body, 4). + + The rotations are stored as quaternions in the order (w, x, y, z). + """ + if self._sim_element_quat_w.timestamp < self._sim_timestamp: + # convert from xyzw to wxyz + quats = self._root_physx_view.get_sim_element_rotations().view(self._root_physx_view.count, -1, 4) + quats = math_utils.convert_quat(quats, to="wxyz") + # set the buffer data and timestamp + self._sim_element_quat_w.data = quats + self._sim_element_quat_w.timestamp = self._sim_timestamp + return self._sim_element_quat_w.data + + @property + def collision_element_quat_w(self): + """Collision mesh element-wise rotations as quaternions for the deformable bodies in simulation world frame. + Shape is (num_instances, max_collision_elements_per_body, 4). + + The rotations are stored as quaternions in the order (w, x, y, z). + """ + if self._collision_element_quat_w.timestamp < self._sim_timestamp: + # convert from xyzw to wxyz + quats = self._root_physx_view.get_element_rotations().view(self._root_physx_view.count, -1, 4) + quats = math_utils.convert_quat(quats, to="wxyz") + # set the buffer data and timestamp + self._collision_element_quat_w.data = quats + self._collision_element_quat_w.timestamp = self._sim_timestamp + return self._collision_element_quat_w.data + + @property + def sim_element_deform_gradient_w(self): + """Simulation mesh element-wise second-order deformation gradient tensors for the deformable bodies + in simulation world frame. Shape is (num_instances, max_sim_elements_per_body, 3, 3). + """ + if self._sim_element_deform_gradient_w.timestamp < self._sim_timestamp: + # set the buffer data and timestamp + self._sim_element_deform_gradient_w.data = ( + self._root_physx_view.get_sim_element_deformation_gradients().view( + self._root_physx_view.count, -1, 3, 3 + ) + ) + self._sim_element_deform_gradient_w.timestamp = self._sim_timestamp + return self._sim_element_deform_gradient_w.data + + @property + def collision_element_deform_gradient_w(self): + """Collision mesh element-wise second-order deformation gradient tensors for the deformable bodies + in simulation world frame. Shape is (num_instances, max_collision_elements_per_body, 3, 3). + """ + if self._collision_element_deform_gradient_w.timestamp < self._sim_timestamp: + # set the buffer data and timestamp + self._collision_element_deform_gradient_w.data = ( + self._root_physx_view.get_element_deformation_gradients().view(self._root_physx_view.count, -1, 3, 3) + ) + self._collision_element_deform_gradient_w.timestamp = self._sim_timestamp + return self._collision_element_deform_gradient_w.data + + @property + def sim_element_stress_w(self): + """Simulation mesh element-wise second-order Cauchy stress tensors for the deformable bodies + in simulation world frame. Shape is (num_instances, max_sim_elements_per_body, 3, 3). + """ + if self._sim_element_stress_w.timestamp < self._sim_timestamp: + # set the buffer data and timestamp + self._sim_element_stress_w.data = self._root_physx_view.get_sim_element_stresses().view( + self._root_physx_view.count, -1, 3, 3 + ) + self._sim_element_stress_w.timestamp = self._sim_timestamp + return self._sim_element_stress_w.data + + @property + def collision_element_stress_w(self): + """Collision mesh element-wise second-order Cauchy stress tensors for the deformable bodies + in simulation world frame. Shape is (num_instances, max_collision_elements_per_body, 3, 3). + """ + if self._collision_element_stress_w.timestamp < self._sim_timestamp: + # set the buffer data and timestamp + self._collision_element_stress_w.data = self._root_physx_view.get_element_stresses().view( + self._root_physx_view.count, -1, 3, 3 + ) + self._collision_element_stress_w.timestamp = self._sim_timestamp + return self._collision_element_stress_w.data + + ## + # Derived properties. + ## + + @property + def root_pos_w(self) -> torch.Tensor: + """Root position from nodal positions of the simulation mesh for the deformable bodies in simulation world frame. + Shape is (num_instances, 3). + + This quantity is computed as the mean of the nodal positions. + """ + return self.nodal_pos_w.mean(dim=1) + + @property + def root_vel_w(self) -> torch.Tensor: + """Root velocity from vertex velocities for the deformable bodies in simulation world frame. + Shape is (num_instances, 3). + + This quantity is computed as the mean of the nodal velocities. + """ + return self.nodal_vel_w.mean(dim=1) diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/rigid_object/rigid_object.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/rigid_object/rigid_object.py index 9136d3681b..f0b37703e5 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/rigid_object/rigid_object.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/rigid_object/rigid_object.py @@ -70,7 +70,10 @@ def num_instances(self) -> int: @property def num_bodies(self) -> int: - """Number of bodies in the asset.""" + """Number of bodies in the asset. + + This is always 1 since each object is a single rigid body. + """ return 1 @property @@ -125,7 +128,7 @@ def update(self, dt: float): """ def find_bodies(self, name_keys: str | Sequence[str], preserve_order: bool = False) -> tuple[list[int], list[str]]: - """Find bodies in the articulation based on the name keys. + """Find bodies in the rigid body based on the name keys. Please check the :meth:`omni.isaac.lab.utils.string_utils.resolve_matching_names` function for more information on the name matching. @@ -291,7 +294,11 @@ def _initialize_impl(self): # -- object view self._root_physx_view = self._physics_sim_view.create_rigid_body_view(root_prim_path_expr.replace(".*", "*")) - # log information about the articulation + # check if the rigid body was created + if self._root_physx_view._backend is None: + raise RuntimeError(f"Failed to create rigid body at: {self.cfg.prim_path}. Please check PhysX logs.") + + # log information about the rigid body carb.log_info(f"Rigid body initialized at: {self.cfg.prim_path} with root '{root_prim_path_expr}'.") carb.log_info(f"Number of instances: {self.num_instances}") carb.log_info(f"Number of bodies: {self.num_bodies}") diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/rigid_object/rigid_object_data.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/rigid_object/rigid_object_data.py index de358e5cfe..c2c20382a0 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/rigid_object/rigid_object_data.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/assets/rigid_object/rigid_object_data.py @@ -94,7 +94,7 @@ def update(self, dt: float): """ default_mass: torch.Tensor = None - """Default mass read from the simulation. Shape is (num_instances, num_bodies).""" + """Default mass read from the simulation. Shape is (num_instances, 1).""" ## # Properties. @@ -218,7 +218,7 @@ def root_ang_vel_b(self) -> torch.Tensor: @property def body_pos_w(self) -> torch.Tensor: - """Positions of all bodies in simulation world frame. Shape is (num_instances, num_bodies, 3). + """Positions of all bodies in simulation world frame. Shape is (num_instances, 1, 3). This quantity is the position of the rigid bodies' actor frame. """ @@ -226,7 +226,7 @@ def body_pos_w(self) -> torch.Tensor: @property def body_quat_w(self) -> torch.Tensor: - """Orientation (w, x, y, z) of all bodies in simulation world frame. Shape is (num_instances, num_bodies, 4). + """Orientation (w, x, y, z) of all bodies in simulation world frame. Shape is (num_instances, 1, 4). This quantity is the orientation of the rigid bodies' actor frame. """ @@ -234,7 +234,7 @@ def body_quat_w(self) -> torch.Tensor: @property def body_vel_w(self) -> torch.Tensor: - """Velocity of all bodies in simulation world frame. Shape is (num_instances, num_bodies, 6). + """Velocity of all bodies in simulation world frame. Shape is (num_instances, 1, 6). This quantity contains the linear and angular velocities of the rigid bodies' center of mass frame. """ @@ -242,7 +242,7 @@ def body_vel_w(self) -> torch.Tensor: @property def body_lin_vel_w(self) -> torch.Tensor: - """Linear velocity of all bodies in simulation world frame. Shape is (num_instances, num_bodies, 3). + """Linear velocity of all bodies in simulation world frame. Shape is (num_instances, 1, 3). This quantity is the linear velocity of the rigid bodies' center of mass frame. """ @@ -250,7 +250,7 @@ def body_lin_vel_w(self) -> torch.Tensor: @property def body_ang_vel_w(self) -> torch.Tensor: - """Angular velocity of all bodies in simulation world frame. Shape is (num_instances, num_bodies, 3). + """Angular velocity of all bodies in simulation world frame. Shape is (num_instances, 1, 3). This quantity is the angular velocity of the rigid bodies' center of mass frame. """ @@ -258,7 +258,7 @@ def body_ang_vel_w(self) -> torch.Tensor: @property def body_lin_acc_w(self) -> torch.Tensor: - """Linear acceleration of all bodies in simulation world frame. Shape is (num_instances, num_bodies, 3). + """Linear acceleration of all bodies in simulation world frame. Shape is (num_instances, 1, 3). This quantity is the linear acceleration of the rigid bodies' center of mass frame. """ @@ -266,7 +266,7 @@ def body_lin_acc_w(self) -> torch.Tensor: @property def body_ang_acc_w(self) -> torch.Tensor: - """Angular acceleration of all bodies in simulation world frame. Shape is (num_instances, num_bodies, 3). + """Angular acceleration of all bodies in simulation world frame. Shape is (num_instances, 1, 3). This quantity is the angular acceleration of the rigid bodies' center of mass frame. """ diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/markers/config/__init__.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/markers/config/__init__.py index 2a2a8b9d67..ebdc81cbf7 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/markers/config/__init__.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/markers/config/__init__.py @@ -37,6 +37,16 @@ ) """Configuration for the contact sensor marker.""" +DEFORMABLE_TARGET_MARKER_CFG = VisualizationMarkersCfg( + markers={ + "target": sim_utils.SphereCfg( + radius=0.02, + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.75, 0.8)), + ), + }, +) +"""Configuration for the deformable object's kinematic target marker.""" + ## # Frames. diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/scene/interactive_scene.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/scene/interactive_scene.py index 86e9287b4c..c803f0e305 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/scene/interactive_scene.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/scene/interactive_scene.py @@ -14,7 +14,15 @@ from pxr import PhysxSchema import omni.isaac.lab.sim as sim_utils -from omni.isaac.lab.assets import Articulation, ArticulationCfg, AssetBaseCfg, RigidObject, RigidObjectCfg +from omni.isaac.lab.assets import ( + Articulation, + ArticulationCfg, + AssetBaseCfg, + DeformableObject, + DeformableObjectCfg, + RigidObject, + RigidObjectCfg, +) from omni.isaac.lab.sensors import ContactSensorCfg, FrameTransformerCfg, SensorBase, SensorBaseCfg from omni.isaac.lab.terrains import TerrainImporter, TerrainImporterCfg @@ -101,6 +109,7 @@ def __init__(self, cfg: InteractiveSceneCfg): # initialize scene elements self._terrain = None self._articulations = dict() + self._deformable_objects = dict() self._rigid_objects = dict() self._sensors = dict() self._extras = dict() @@ -277,6 +286,11 @@ def articulations(self) -> dict[str, Articulation]: """A dictionary of articulations in the scene.""" return self._articulations + @property + def deformable_objects(self) -> dict[str, DeformableObject]: + """A dictionary of deformable objects in the scene.""" + return self._deformable_objects + @property def rigid_objects(self) -> dict[str, RigidObject]: """A dictionary of rigid objects in the scene.""" @@ -320,6 +334,8 @@ def reset(self, env_ids: Sequence[int] | None = None): # -- assets for articulation in self._articulations.values(): articulation.reset(env_ids) + for deformable_object in self._deformable_objects.values(): + deformable_object.reset(env_ids) for rigid_object in self._rigid_objects.values(): rigid_object.reset(env_ids) # -- sensors @@ -331,6 +347,8 @@ def write_data_to_sim(self): # -- assets for articulation in self._articulations.values(): articulation.write_data_to_sim() + for deformable_object in self._deformable_objects.values(): + deformable_object.write_data_to_sim() for rigid_object in self._rigid_objects.values(): rigid_object.write_data_to_sim() @@ -343,6 +361,8 @@ def update(self, dt: float) -> None: # -- assets for articulation in self._articulations.values(): articulation.update(dt) + for deformable_object in self._deformable_objects.values(): + deformable_object.update(dt) for rigid_object in self._rigid_objects.values(): rigid_object.update(dt) # -- sensors @@ -360,7 +380,13 @@ def keys(self) -> list[str]: The keys of the scene entities. """ all_keys = ["terrain"] - for asset_family in [self._articulations, self._rigid_objects, self._sensors, self._extras]: + for asset_family in [ + self._articulations, + self._deformable_objects, + self._rigid_objects, + self._sensors, + self._extras, + ]: all_keys += list(asset_family.keys()) return all_keys @@ -379,7 +405,13 @@ def __getitem__(self, key: str) -> Any: all_keys = ["terrain"] # check if it is in other dictionaries - for asset_family in [self._articulations, self._rigid_objects, self._sensors, self._extras]: + for asset_family in [ + self._articulations, + self._deformable_objects, + self._rigid_objects, + self._sensors, + self._extras, + ]: out = asset_family.get(key) # if found, return if out is not None: @@ -418,6 +450,8 @@ def _add_entities_from_cfg(self): self._terrain = asset_cfg.class_type(asset_cfg) elif isinstance(asset_cfg, ArticulationCfg): self._articulations[asset_name] = asset_cfg.class_type(asset_cfg) + elif isinstance(asset_cfg, DeformableObjectCfg): + self._deformable_objects[asset_name] = asset_cfg.class_type(asset_cfg) elif isinstance(asset_cfg, RigidObjectCfg): self._rigid_objects[asset_name] = asset_cfg.class_type(asset_cfg) elif isinstance(asset_cfg, SensorBaseCfg): diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/schemas/schemas.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/schemas/schemas.py index be61cb6bd1..1ca5c301d5 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/schemas/schemas.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/schemas/schemas.py @@ -809,7 +809,7 @@ def modify_deformable_body_properties( # set into PhysX API for attr_name, value in cfg.items(): - if attr_name in ["rest_offset", "collision_offset"]: + if attr_name in ["rest_offset", "contact_offset"]: safe_set_attribute_on_usd_schema(physx_collision_api, attr_name, value, camel_case=True) else: safe_set_attribute_on_usd_schema(physx_deformable_api, attr_name, value, camel_case=True) diff --git a/source/extensions/omni.isaac.lab/test/assets/test_deformable_object.py b/source/extensions/omni.isaac.lab/test/assets/test_deformable_object.py new file mode 100644 index 0000000000..5ada861115 --- /dev/null +++ b/source/extensions/omni.isaac.lab/test/assets/test_deformable_object.py @@ -0,0 +1,432 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +# ignore private usage of variables warning +# pyright: reportPrivateUsage=none + + +"""Launch Isaac Sim Simulator first.""" + +from omni.isaac.lab.app import AppLauncher, run_tests + +# Can set this to False to see the GUI for debugging +# This will also add lights to the scene +HEADLESS = True + +# launch omniverse app +app_launcher = AppLauncher(headless=HEADLESS) +simulation_app = app_launcher.app + +"""Rest everything follows.""" + +import ctypes +import torch +import unittest + +import omni.isaac.core.utils.prims as prim_utils + +import omni.isaac.lab.sim as sim_utils +import omni.isaac.lab.utils.math as math_utils +from omni.isaac.lab.assets import DeformableObject, DeformableObjectCfg +from omni.isaac.lab.sim import build_simulation_context + + +def generate_cubes_scene( + num_cubes: int = 1, + height: float = 1.0, + initial_rot: tuple[float, ...] = (1.0, 0.0, 0.0, 0.0), + has_api: bool = True, + material_path: str | None = "material", + kinematic_enabled: bool = False, + device: str = "cuda:0", +) -> DeformableObject: + """Generate a scene with the provided number of cubes. + + Args: + num_cubes: Number of cubes to generate. + height: Height of the cubes. Default is 1.0. + initial_rot: Initial rotation of the cubes. Default is (1.0, 0.0, 0.0, 0.0). + has_api: Whether the cubes have a deformable body API on them. + material_path: Path to the material file. If None, no material is added. Default is "material", + which is path relative to the spawned object prim path. + kinematic_enabled: Whether the cubes are kinematic. + device: Device to use for the simulation. + + Returns: + The deformable object representing the cubes. + + """ + origins = torch.tensor([(i * 1.0, 0, height) for i in range(num_cubes)]).to(device) + # Create Top-level Xforms, one for each cube + for i, origin in enumerate(origins): + prim_utils.create_prim(f"/World/Table_{i}", "Xform", translation=origin) + + # Resolve spawn configuration + if has_api: + spawn_cfg = sim_utils.MeshCuboidCfg( + size=(0.2, 0.2, 0.2), + deformable_props=sim_utils.DeformableBodyPropertiesCfg(kinematic_enabled=kinematic_enabled), + ) + # Add physics material if provided + if material_path is not None: + spawn_cfg.physics_material = sim_utils.DeformableBodyMaterialCfg() + spawn_cfg.physics_material_path = material_path + else: + spawn_cfg.physics_material = None + else: + # since no deformable body properties defined, this is just a static collider + spawn_cfg = sim_utils.MeshCuboidCfg( + size=(0.2, 0.2, 0.2), + collision_props=sim_utils.CollisionPropertiesCfg(), + ) + # Create deformable object + cube_object_cfg = DeformableObjectCfg( + prim_path="/World/Table_.*/Object", + spawn=spawn_cfg, + init_state=DeformableObjectCfg.InitialStateCfg(pos=(0.0, 0.0, height), rot=initial_rot), + ) + cube_object = DeformableObject(cfg=cube_object_cfg) + + return cube_object + + +class TestDeformableObject(unittest.TestCase): + """Test for deformable object class.""" + + """ + Tests + """ + + def test_initialization(self): + """Test initialization for prim with deformable body API at the provided prim path. + + This test checks that the deformable object is correctly initialized with deformable material at + different paths. + """ + for material_path in [None, "/World/SoftMaterial", "material"]: + for num_cubes in (1, 2): + with self.subTest(num_cubes=num_cubes, material_path=material_path): + with build_simulation_context(auto_add_lighting=True) as sim: + # Generate cubes scene + cube_object = generate_cubes_scene(num_cubes=num_cubes, material_path=material_path) + + # Check that boundedness of deformable object is correct + self.assertEqual(ctypes.c_long.from_address(id(cube_object)).value, 1) + + # Play sim + sim.reset() + + # Check that boundedness of deformable object is correct + self.assertEqual(ctypes.c_long.from_address(id(cube_object)).value, 1) + + # Check if object is initialized + self.assertTrue(cube_object.is_initialized) + + # Check correct number of cubes + self.assertEqual(cube_object.num_instances, num_cubes) + self.assertEqual(cube_object.root_physx_view.count, num_cubes) + + # Check correct number of materials in the view + if material_path: + if material_path.startswith("/"): + self.assertEqual(cube_object.material_physx_view.count, 1) + else: + self.assertEqual(cube_object.material_physx_view.count, num_cubes) + else: + self.assertIsNone(cube_object.material_physx_view) + + # Check buffers that exists and have correct shapes + self.assertEqual( + cube_object.data.nodal_state_w.shape, + (num_cubes, cube_object.max_sim_vertices_per_body, 6), + ) + self.assertEqual( + cube_object.data.nodal_kinematic_target.shape, + (num_cubes, cube_object.max_sim_vertices_per_body, 4), + ) + self.assertEqual(cube_object.data.root_pos_w.shape, (num_cubes, 3)) + self.assertEqual(cube_object.data.root_vel_w.shape, (num_cubes, 3)) + + # Simulate physics + for _ in range(2): + # perform rendering + sim.step() + # update object + cube_object.update(sim.cfg.dt) + + # check we can get all the sim data from the object + self.assertEqual( + cube_object.data.sim_element_quat_w.shape, + (num_cubes, cube_object.max_sim_elements_per_body, 4), + ) + self.assertEqual( + cube_object.data.sim_element_deform_gradient_w.shape, + (num_cubes, cube_object.max_sim_elements_per_body, 3, 3), + ) + self.assertEqual( + cube_object.data.sim_element_stress_w.shape, + (num_cubes, cube_object.max_sim_elements_per_body, 3, 3), + ) + self.assertEqual( + cube_object.data.collision_element_quat_w.shape, + (num_cubes, cube_object.max_collision_elements_per_body, 4), + ) + self.assertEqual( + cube_object.data.collision_element_deform_gradient_w.shape, + (num_cubes, cube_object.max_collision_elements_per_body, 3, 3), + ) + self.assertEqual( + cube_object.data.collision_element_stress_w.shape, + (num_cubes, cube_object.max_collision_elements_per_body, 3, 3), + ) + + def test_initialization_on_device_cpu(self): + """Test that initialization fails with deformable body API on the CPU.""" + with build_simulation_context(device="cpu", auto_add_lighting=True) as sim: + # Generate cubes scene + cube_object = generate_cubes_scene(num_cubes=5, device="cpu") + + # Check that boundedness of deformable object is correct + self.assertEqual(ctypes.c_long.from_address(id(cube_object)).value, 1) + + # Play sim + sim.reset() + + # Check if object is initialized + self.assertFalse(cube_object.is_initialized) + + def test_initialization_with_kinematic_enabled(self): + """Test that initialization for prim with kinematic flag enabled.""" + for num_cubes in (1, 2): + with self.subTest(num_cubes=num_cubes): + with build_simulation_context(auto_add_lighting=True) as sim: + # Generate cubes scene + cube_object = generate_cubes_scene(num_cubes=num_cubes, kinematic_enabled=True) + + # Check that boundedness of deformable object is correct + self.assertEqual(ctypes.c_long.from_address(id(cube_object)).value, 1) + + # Play sim + sim.reset() + + # Check if object is initialized + self.assertTrue(cube_object.is_initialized) + + # Check buffers that exists and have correct shapes + self.assertEqual(cube_object.data.root_pos_w.shape, (num_cubes, 3)) + self.assertEqual(cube_object.data.root_vel_w.shape, (num_cubes, 3)) + + # Simulate physics + for _ in range(2): + # perform rendering + sim.step() + # update object + cube_object.update(sim.cfg.dt) + # check that the object is kinematic + default_nodal_state_w = cube_object.data.default_nodal_state_w.clone() + torch.testing.assert_close(cube_object.data.nodal_state_w, default_nodal_state_w) + + def test_initialization_with_no_deformable_body(self): + """Test that initialization fails when no deformable body is found at the provided prim path.""" + for num_cubes in (1, 2): + with self.subTest(num_cubes=num_cubes): + with build_simulation_context(auto_add_lighting=True) as sim: + # Generate cubes scene + cube_object = generate_cubes_scene(num_cubes=num_cubes, has_api=False) + + # Check that boundedness of deformable object is correct + self.assertEqual(ctypes.c_long.from_address(id(cube_object)).value, 1) + + # Play sim + sim.reset() + + # Check if object is initialized + self.assertFalse(cube_object.is_initialized) + + def test_set_nodal_state(self): + """Test setting the state of the deformable object. + + In this test, we set the state of the deformable object to a random state and check + that the object is in that state after simulation. We set gravity to zero as + we don't want any external forces acting on the object to ensure state remains static. + """ + for num_cubes in (1, 2): + with self.subTest(num_cubes=num_cubes): + # Turn off gravity for this test as we don't want any external forces acting on the object + # to ensure state remains static + with build_simulation_context(gravity_enabled=False, auto_add_lighting=True) as sim: + # Generate cubes scene + cube_object = generate_cubes_scene(num_cubes=num_cubes) + + # Play the simulator + sim.reset() + + # Set each state type individually as they are dependent on each other + for state_type_to_randomize in ["nodal_pos_w", "nodal_vel_w"]: + state_dict = { + "nodal_pos_w": torch.zeros_like(cube_object.data.nodal_pos_w), + "nodal_vel_w": torch.zeros_like(cube_object.data.nodal_vel_w), + } + + # Now we are ready! + for _ in range(5): + # reset object + cube_object.reset() + + # Set random state + state_dict[state_type_to_randomize] = torch.randn( + num_cubes, cube_object.max_sim_vertices_per_body, 3, device=sim.device + ) + + # perform simulation + for _ in range(5): + nodal_state = torch.cat( + [ + state_dict["nodal_pos_w"], + state_dict["nodal_vel_w"], + ], + dim=-1, + ) + # reset nodal state + cube_object.write_nodal_state_to_sim(nodal_state) + + # assert that set node quantities are equal to the ones set in the state_dict + torch.testing.assert_close( + cube_object.data.nodal_state_w, nodal_state, rtol=1e-5, atol=1e-5 + ) + + # perform step + sim.step() + # update object + cube_object.update(sim.cfg.dt) + + def test_set_nodal_state_with_applied_transform(self): + """Test setting the state of the deformable object with applied transform. + + In this test, we apply a random pose to the object and check that the mean of the nodal positions + is equal to the applied pose after simulation. We set gravity to zero as we don't want any external + forces acting on the object to ensure state remains static. + """ + for num_cubes in (1, 2): + with self.subTest(num_cubes=num_cubes): + # Turn off gravity for this test as we don't want any external forces acting on the object + # to ensure state remains static + with build_simulation_context(gravity_enabled=False, auto_add_lighting=True) as sim: + # Generate cubes scene + cube_object = generate_cubes_scene(num_cubes=num_cubes) + + # Play the simulator + sim.reset() + + for randomize_pos in [True, False]: + for randomize_rot in [True, False]: + # Now we are ready! + for _ in range(5): + # reset the nodal state of the object + nodal_state = cube_object.data.default_nodal_state_w.clone() + mean_nodal_pos_default = nodal_state[..., :3].mean(dim=1) + # sample randomize position and rotation + if randomize_pos: + pos_w = torch.rand(cube_object.num_instances, 3, device=sim.device) + pos_w[:, 2] += 0.5 + else: + pos_w = None + if randomize_rot: + quat_w = math_utils.random_orientation(cube_object.num_instances, device=sim.device) + else: + quat_w = None + # apply random pose to the object + nodal_state[..., :3] = cube_object.transform_nodal_pos( + nodal_state[..., :3], pos_w, quat_w + ) + # compute mean of initial nodal positions + mean_nodal_pos_init = nodal_state[..., :3].mean(dim=1) + + # check computation is correct + if pos_w is None: + torch.testing.assert_close( + mean_nodal_pos_init, mean_nodal_pos_default, rtol=1e-5, atol=1e-5 + ) + else: + torch.testing.assert_close( + mean_nodal_pos_init, mean_nodal_pos_default + pos_w, rtol=1e-5, atol=1e-5 + ) + + # write nodal state to simulation + cube_object.write_nodal_state_to_sim(nodal_state) + # reset object + cube_object.reset() + + # perform simulation + for _ in range(50): + # perform step + sim.step() + # update object + cube_object.update(sim.cfg.dt) + + # check that the mean of the nodal positions is equal to the applied pose + torch.testing.assert_close( + cube_object.data.root_pos_w, mean_nodal_pos_init, rtol=1e-5, atol=1e-5 + ) + + def test_set_kinematic_targets(self): + """Test setting kinematic targets for the deformable object. + + In this test, we set one of the cubes with only kinematic targets for its nodal positions and check + that the object is in that state after simulation. + """ + for num_cubes in (2, 4): + with self.subTest(num_cubes=num_cubes): + # Turn off gravity for this test as we don't want any external forces acting on the object + # to ensure state remains static + with build_simulation_context(auto_add_lighting=True) as sim: + # Generate cubes scene + cube_object = generate_cubes_scene(num_cubes=num_cubes, height=1.0) + + # Play the simulator + sim.reset() + + # Get sim kinematic targets + nodal_kinematic_targets = cube_object.root_physx_view.get_sim_kinematic_targets().clone() + + # Now we are ready! + for _ in range(5): + # reset nodal state + cube_object.write_nodal_state_to_sim(cube_object.data.default_nodal_state_w) + + default_root_pos = cube_object.data.default_nodal_state_w.mean(dim=1) + + # reset object + cube_object.reset() + + # write kinematic targets + # -- enable kinematic targets for the first cube + nodal_kinematic_targets[1:, :, 3] = 1.0 + nodal_kinematic_targets[0, :, 3] = 0.0 + # -- set kinematic targets for the first cube + nodal_kinematic_targets[0, :, :3] = cube_object.data.default_nodal_state_w[0, :, :3] + # -- write kinematic targets to simulation + cube_object.write_nodal_kinematic_target_to_sim( + nodal_kinematic_targets[0], env_ids=torch.tensor([0], device=sim.device) + ) + + # perform simulation + for _ in range(20): + # perform step + sim.step() + # update object + cube_object.update(sim.cfg.dt) + + # assert that set node quantities are equal to the ones set in the state_dict + torch.testing.assert_close( + cube_object.data.nodal_pos_w[0], nodal_kinematic_targets[0, :, :3], rtol=1e-5, atol=1e-5 + ) + # see other cubes are dropping + root_pos_w = cube_object.data.root_pos_w + self.assertTrue(torch.all(root_pos_w[1:, 2] < default_root_pos[1:, 2])) + + +if __name__ == "__main__": + run_tests() diff --git a/source/standalone/demos/deformables.py b/source/standalone/demos/deformables.py index edd8b860bb..74cfd8d242 100644 --- a/source/standalone/demos/deformables.py +++ b/source/standalone/demos/deformables.py @@ -36,9 +36,8 @@ import torch import tqdm -import omni.isaac.core.utils.prims as prim_utils - import omni.isaac.lab.sim as sim_utils +from omni.isaac.lab.assets import DeformableObject, DeformableObjectCfg def define_origins(num_origins: int, spacing: float) -> list[list[float]]: @@ -56,8 +55,8 @@ def define_origins(num_origins: int, spacing: float) -> list[list[float]]: return env_origins.tolist() -def design_scene(): - """Designs the scene by spawning ground plane, light, and deformable meshes.""" +def design_scene() -> tuple[dict, list[list[float]]]: + """Designs the scene.""" # Ground-plane cfg_ground = sim_utils.GroundPlaneCfg() cfg_ground.func("/World/defaultGroundPlane", cfg_ground) @@ -69,11 +68,6 @@ def design_scene(): ) cfg_light.func("/World/light", cfg_light) - # create new xform prims for all objects to be spawned under - origins = define_origins(num_origins=4, spacing=5.5) - for idx, origin in enumerate(origins): - prim_utils.create_prim(f"/World/Origin{idx:02d}", "Xform", translation=origin) - # spawn a red cone cfg_sphere = sim_utils.MeshSphereCfg( radius=0.25, @@ -118,7 +112,7 @@ def design_scene(): } # Create separate groups of deformable objects - origins = define_origins(num_origins=25, spacing=0.5) + origins = define_origins(num_origins=64, spacing=0.6) print("[INFO]: Spawning objects...") # Iterate over all the origins and randomly spawn objects for idx, origin in tqdm.tqdm(enumerate(origins), total=len(origins)): @@ -132,7 +126,52 @@ def design_scene(): # randomize the color obj_cfg.visual_material.diffuse_color = (random.random(), random.random(), random.random()) # spawn the object - obj_cfg.func(f"/World/Origin.*/Object{idx:02d}", obj_cfg, translation=origin) + obj_cfg.func(f"/World/Origin/Object{idx:02d}", obj_cfg, translation=origin) + + # create a view for all the deformables + # note: since we manually spawned random deformable meshes above, we don't need to + # specify the spawn configuration for the deformable object + cfg = DeformableObjectCfg( + prim_path="/World/Origin/Object.*", + spawn=None, + init_state=DeformableObjectCfg.InitialStateCfg(), + ) + deformable_object = DeformableObject(cfg=cfg) + + # return the scene information + scene_entities = {"deformable_object": deformable_object} + return scene_entities, origins + + +def run_simulator(sim: sim_utils.SimulationContext, entities: dict[str, DeformableObject], origins: torch.Tensor): + """Runs the simulation loop.""" + # Define simulation stepping + sim_dt = sim.get_physics_dt() + sim_time = 0.0 + count = 0 + # Simulate physics + while simulation_app.is_running(): + # reset + if count % 400 == 0: + # reset counters + sim_time = 0.0 + count = 0 + # reset deformable object state + for _, deform_body in enumerate(entities.values()): + # root state + nodal_state = deform_body.data.default_nodal_state_w.clone() + deform_body.write_nodal_state_to_sim(nodal_state) + # reset the internal state + deform_body.reset() + print("[INFO]: Resetting deformable object state...") + # perform step + sim.step() + # update sim-time + sim_time += sim_dt + count += 1 + # update buffers + for deform_body in entities.values(): + deform_body.update(sim_dt) def main(): @@ -141,20 +180,18 @@ def main(): sim_cfg = sim_utils.SimulationCfg(dt=0.01) sim = sim_utils.SimulationContext(sim_cfg) # Set main camera - sim.set_camera_view([8.0, 8.0, 6.0], [0.0, 0.0, 0.0]) + sim.set_camera_view([4.0, 4.0, 3.0], [0.5, 0.5, 0.0]) # Design scene by adding assets to it - design_scene() - + scene_entities, scene_origins = design_scene() + scene_origins = torch.tensor(scene_origins, device=sim.device) # Play the simulator sim.reset() # Now we are ready! print("[INFO]: Setup complete...") - # Simulate physics - while simulation_app.is_running(): - # perform step - sim.step() + # Run the simulator + run_simulator(sim, scene_entities, scene_origins) if __name__ == "__main__": diff --git a/source/standalone/tutorials/01_assets/run_deformable_object.py b/source/standalone/tutorials/01_assets/run_deformable_object.py new file mode 100644 index 0000000000..9d20b37cf5 --- /dev/null +++ b/source/standalone/tutorials/01_assets/run_deformable_object.py @@ -0,0 +1,168 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +""" +This script demonstrates how to work with the deformable object and interact with it. + +.. code-block:: bash + + # Usage + ./isaaclab.sh -p source/standalone/tutorials/01_assets/run_deformable_object.py + +""" + +"""Launch Isaac Sim Simulator first.""" + + +import argparse + +from omni.isaac.lab.app import AppLauncher + +# add argparse arguments +parser = argparse.ArgumentParser(description="Tutorial on interacting with a deformable object.") +# 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 torch + +import omni.isaac.core.utils.prims as prim_utils + +import omni.isaac.lab.sim as sim_utils +import omni.isaac.lab.utils.math as math_utils +from omni.isaac.lab.assets import DeformableObject, DeformableObjectCfg +from omni.isaac.lab.sim import SimulationContext + + +def design_scene(): + """Designs the scene.""" + # Ground-plane + cfg = sim_utils.GroundPlaneCfg() + cfg.func("/World/defaultGroundPlane", cfg) + # Lights + cfg = sim_utils.DomeLightCfg(intensity=2000.0, color=(0.8, 0.8, 0.8)) + cfg.func("/World/Light", cfg) + + # Create separate groups called "Origin1", "Origin2", "Origin3" + # Each group will have a robot in it + origins = [[0.25, 0.25, 0.0], [-0.25, 0.25, 0.0], [0.25, -0.25, 0.0], [-0.25, -0.25, 0.0]] + for i, origin in enumerate(origins): + prim_utils.create_prim(f"/World/Origin{i}", "Xform", translation=origin) + + # Deformable Object + cfg = DeformableObjectCfg( + prim_path="/World/Origin.*/Cube", + spawn=sim_utils.MeshCuboidCfg( + size=(0.2, 0.2, 0.2), + deformable_props=sim_utils.DeformableBodyPropertiesCfg(rest_offset=0.0, contact_offset=0.001), + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.5, 0.1, 0.0)), + physics_material=sim_utils.DeformableBodyMaterialCfg(poissons_ratio=0.4, youngs_modulus=1e5), + ), + init_state=DeformableObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 1.0)), + debug_vis=True, + ) + cube_object = DeformableObject(cfg=cfg) + + # return the scene information + scene_entities = {"cube_object": cube_object} + return scene_entities, origins + + +def run_simulator(sim: sim_utils.SimulationContext, entities: dict[str, DeformableObject], origins: torch.Tensor): + """Runs the simulation loop.""" + # Extract scene entities + # note: we only do this here for readability. In general, it is better to access the entities directly from + # the dictionary. This dictionary is replaced by the InteractiveScene class in the next tutorial. + cube_object = entities["cube_object"] + # Define simulation stepping + sim_dt = sim.get_physics_dt() + sim_time = 0.0 + count = 0 + + # Nodal kinematic targets of the deformable bodies + nodal_kinematic_target = cube_object.data.nodal_kinematic_target.clone() + + # Simulate physics + while simulation_app.is_running(): + # reset + if count % 250 == 0: + # reset counters + sim_time = 0.0 + count = 0 + + # reset the nodal state of the object + nodal_state = cube_object.data.default_nodal_state_w.clone() + # apply random pose to the object + pos_w = torch.rand(cube_object.num_instances, 3, device=sim.device) * 0.1 + origins + quat_w = math_utils.random_orientation(cube_object.num_instances, device=sim.device) + nodal_state[..., :3] = cube_object.transform_nodal_pos(nodal_state[..., :3], pos_w, quat_w) + + # write nodal state to simulation + cube_object.write_nodal_state_to_sim(nodal_state) + + # write kinematic target to nodal state and free all vertices + nodal_kinematic_target[..., :3] = nodal_state[..., :3] + nodal_kinematic_target[..., 3] = 1.0 + cube_object.write_nodal_kinematic_target_to_sim(nodal_kinematic_target) + + # reset buffers + cube_object.reset() + + print("----------------------------------------") + print("[INFO]: Resetting object state...") + + # update the kinematic target for cubes at index 0 and 3 + # we slightly move the cube in the z-direction by picking the vertex at index 0 + nodal_kinematic_target[[0, 3], 0, 2] += 0.001 + # set vertex at index 0 to be kinematically constrained + # 0: constrained, 1: free + nodal_kinematic_target[[0, 3], 0, 3] = 0.0 + # write kinematic target to simulation + cube_object.write_nodal_kinematic_target_to_sim(nodal_kinematic_target) + + # write internal data to simulation + cube_object.write_data_to_sim() + # perform step + sim.step() + # update sim-time + sim_time += sim_dt + count += 1 + # update buffers + cube_object.update(sim_dt) + # print the root position + if count % 50 == 0: + print(f"Root position (in world): {cube_object.data.root_pos_w[:, :3]}") + + +def main(): + """Main function.""" + # Load kit helper + sim_cfg = sim_utils.SimulationCfg() + sim = SimulationContext(sim_cfg) + # Set main camera + sim.set_camera_view(eye=[3.0, 0.0, 1.0], target=[0.0, 0.0, 0.5]) + # Design scene + scene_entities, scene_origins = design_scene() + scene_origins = torch.tensor(scene_origins, device=sim.device) + # Play the simulator + sim.reset() + # Now we are ready! + print("[INFO]: Setup complete...") + # Run the simulator + run_simulator(sim, scene_entities, scene_origins) + + +if __name__ == "__main__": + # run the main function + main() + # close sim app + simulation_app.close()