Unverified Commit d6dd9035 authored by Masoud Moghani's avatar Masoud Moghani Committed by GitHub

Add APIs for deformable asset (#630)

# Description

This MR adds deformable object API to assets in the core framework. The
class creates two physics views: one for the deformable object and the
other one for the deformable material. Based on these, the users can set
and get different nodal information from the solver, as well as
randomize deformable material properties.

The MR also adds some basic tests and a script in the tutorial that
showcases the deformable object.

## Type of change

- New feature (non-breaking change which adds functionality)
- This change requires a documentation update

## Screenshots

Output from:

```bash
./isaaclab.sh -p source/standalone/tutorials/01_assets/run_deformable_object.py
```


https://github.com/user-attachments/assets/9265f4d4-bebf-41b4-9d73-35c558f47a15

## Checklist

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

---------
Signed-off-by: 's avatarMayank Mittal <12863862+Mayankm96@users.noreply.github.com>
Co-authored-by: 's avatarMayank Mittal <mittalma@leggedrobotics.com>
parent 0b4f7bd3
......@@ -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
......@@ -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.
......@@ -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
......@@ -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
......
.. _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-spawn-prims>` 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
......@@ -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.
[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"
......
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)
~~~~~~~~~~~~~~~~~~~
......
......@@ -41,4 +41,5 @@ the corresponding actuator torques.
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
......@@ -909,6 +909,10 @@ class Articulation(AssetBase):
# -- 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}")
......
# 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
# 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
# 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.
"""
# 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)
......@@ -70,7 +70,10 @@ class RigidObject(AssetBase):
@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 @@ class RigidObject(AssetBase):
"""
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 @@ class RigidObject(AssetBase):
# -- 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}")
......
......@@ -94,7 +94,7 @@ class RigidObjectData:
"""
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 @@ class RigidObjectData:
@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 @@ class RigidObjectData:
@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 @@ class RigidObjectData:
@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 @@ class RigidObjectData:
@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 @@ class RigidObjectData:
@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 @@ class RigidObjectData:
@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 @@ class RigidObjectData:
@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.
"""
......
......@@ -37,6 +37,16 @@ CONTACT_SENSOR_MARKER_CFG = VisualizationMarkersCfg(
)
"""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.
......
......@@ -14,7 +14,15 @@ from omni.isaac.core.prims import XFormPrimView
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 @@ class InteractiveScene:
# 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 @@ class InteractiveScene:
"""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 @@ class InteractiveScene:
# -- 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 @@ class InteractiveScene:
# -- 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 @@ class InteractiveScene:
# -- 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 @@ class InteractiveScene:
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 @@ class InteractiveScene:
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 @@ class InteractiveScene:
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):
......
......@@ -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)
......
# 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()
......@@ -36,9 +36,8 @@ import random
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__":
......
# 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()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment