Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Added
^^^^^

* Added the mesh-collision schema-fragment API: the
:class:`~isaaclab.sim.schemas.MeshCollisionFragment` marker and
:class:`~isaaclab.sim.schemas.UsdPhysicsMeshCollisionCfg` (carrying the standard
``physics:approximation`` token via ``UsdPhysics.MeshCollisionAPI``).
* Added :func:`~isaaclab.sim.schemas.apply_mesh_collision_properties`, which applies
``UsdPhysics.MeshCollisionAPI`` as the implicit anchor, resolves the
``physics:approximation`` token from whichever cooking fragment is present (validated against
:const:`~isaaclab.sim.schemas.MESH_APPROXIMATION_TOKENS`), and dispatches each fragment via its
``func``.

Changed
^^^^^^^

* Changed the mesh-converter ``mesh_collision_props`` slot
(:attr:`~isaaclab.sim.converters.MeshConverterCfg.mesh_collision_props`) to also accept a list of
:class:`~isaaclab.sim.schemas.MeshCollisionFragment` fragments. Legacy single cfgs continue to
work through a transition bridge in the converter.
8 changes: 8 additions & 0 deletions source/isaaclab/isaaclab/sim/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,21 @@ __all__ = [
"NewtonMeshCollisionPropertiesCfg",
"NewtonRigidBodyPropertiesCfg",
"NewtonSDFCollisionPropertiesCfg",
"MeshCollisionFragment",
"PhysxJointDrivePropertiesCfg",
"PhysxRigidBodyPropertiesCfg",
"RigidBodyBaseCfg",
"RigidBodyFragment",
"SchemaFragment",
"SpatialTendonFragment",
"UsdPhysicsCollisionCfg",
"UsdPhysicsMeshCollisionCfg",
"UsdPhysicsRigidBodyCfg",
"apply_collision_properties",
"apply_fixed_tendon_properties",
"apply_mass_properties",
"apply_mesh_collision",
"apply_mesh_collision_properties",
"apply_namespaced",
"apply_rigid_body_properties",
"apply_spatial_tendon_properties",
Expand Down Expand Up @@ -236,6 +240,7 @@ from .schemas import (
MassCfg,
MassFragment,
MassPropertiesCfg,
MeshCollisionFragment,
MeshCollisionPropertiesCfg,
PhysxJointDrivePropertiesCfg,
PhysxRigidBodyPropertiesCfg,
Expand All @@ -248,11 +253,14 @@ from .schemas import (
TriangleMeshPropertiesCfg,
TriangleMeshSimplificationPropertiesCfg,
UsdPhysicsCollisionCfg,
UsdPhysicsMeshCollisionCfg,
UsdPhysicsRigidBodyCfg,
activate_contact_sensors,
apply_collision_properties,
apply_fixed_tendon_properties,
apply_mass_properties,
apply_mesh_collision,
apply_mesh_collision_properties,
apply_namespaced,
apply_rigid_body_properties,
apply_spatial_tendon_properties,
Expand Down
28 changes: 21 additions & 7 deletions source/isaaclab/isaaclab/sim/converters/mesh_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
from isaacsim.core.experimental.utils.app import enable_extension
from pxr import Gf, Tf, Usd, UsdGeom, UsdPhysics, UsdUtils

from isaaclab.sim import schemas
from isaaclab.sim.converters.asset_converter_base import AssetConverterBase
from isaaclab.sim.converters.mesh_converter_cfg import MeshConverterCfg
from isaaclab.sim.schemas import schemas
from isaaclab.sim.schemas.schemas_cfg import SchemaFragment
from isaaclab.sim.utils import delete_prim, export_prim_to_file

# import logger
Expand Down Expand Up @@ -130,17 +131,30 @@ def _convert_asset(self, cfg: MeshConverterCfg):
coll_frags = (
cfg.collision_props if isinstance(cfg.collision_props, (list, tuple)) else [cfg.collision_props]
)
if coll_frags and all(isinstance(f, schemas.SchemaFragment) for f in coll_frags):
if coll_frags and all(isinstance(f, SchemaFragment) for f in coll_frags):
schemas.apply_collision_properties(str(child_mesh_prim.GetPath()), coll_frags, stage=stage)
else:
schemas.define_collision_properties(
prim_path=child_mesh_prim.GetPath(), cfg=cfg.collision_props, stage=stage
)
# Add collision mesh
if cfg.mesh_collision_props is not None:
schemas.define_mesh_collision_properties(
prim_path=child_mesh_prim.GetPath(), cfg=cfg.mesh_collision_props, stage=stage
# Transition bridge: route a fragment (or list of fragments) through the new
# ``apply_mesh_collision_properties`` family writer; otherwise fall back to the
# legacy single-cfg ``define_mesh_collision_properties`` path.
mesh_collision_frags = (
cfg.mesh_collision_props
if isinstance(cfg.mesh_collision_props, (list, tuple))
else [cfg.mesh_collision_props]
)
if all(isinstance(f, SchemaFragment) for f in mesh_collision_frags):
schemas.apply_mesh_collision_properties(
prim_path=child_mesh_prim.GetPath(), fragments=mesh_collision_frags, stage=stage
)
else:
schemas.define_mesh_collision_properties(
prim_path=child_mesh_prim.GetPath(), cfg=cfg.mesh_collision_props, stage=stage
)
# Delete the old Xform and make the new Xform the default prim
stage.SetDefaultPrim(xform_prim)
# Apply default Xform rotation to mesh -> enable to set rotation and scale
Expand Down Expand Up @@ -192,15 +206,15 @@ def _convert_asset(self, cfg: MeshConverterCfg):
# apply mass properties (transition shim, remove later: fragment list -> apply_*; legacy cfg -> define_*)
if cfg.mass_props is not None:
# normalize a single fragment to a list so the convenience form routes like a list
mass_frags = [cfg.mass_props] if isinstance(cfg.mass_props, schemas.SchemaFragment) else cfg.mass_props
if isinstance(mass_frags, (list, tuple)) and all(isinstance(f, schemas.SchemaFragment) for f in mass_frags):
mass_frags = [cfg.mass_props] if isinstance(cfg.mass_props, SchemaFragment) else cfg.mass_props
if isinstance(mass_frags, (list, tuple)) and all(isinstance(f, SchemaFragment) for f in mass_frags):
schemas.apply_mass_properties(str(xform_prim.GetPath()), mass_frags, stage=stage)
else:
schemas.define_mass_properties(prim_path=xform_prim.GetPath(), cfg=cfg.mass_props, stage=stage)
# apply rigid body properties (transition shim, remove later: fragment list -> apply_*; legacy cfg -> define_*)
if cfg.rigid_props is not None:
rigid_frags = cfg.rigid_props if isinstance(cfg.rigid_props, (list, tuple)) else [cfg.rigid_props]
if rigid_frags and all(isinstance(f, schemas.SchemaFragment) for f in rigid_frags):
if rigid_frags and all(isinstance(f, SchemaFragment) for f in rigid_frags):
schemas.apply_rigid_body_properties(str(xform_prim.GetPath()), rigid_frags, stage=stage)
else:
schemas.define_rigid_body_properties(prim_path=xform_prim.GetPath(), cfg=cfg.rigid_props, stage=stage)
Expand Down
16 changes: 15 additions & 1 deletion source/isaaclab/isaaclab/sim/converters/mesh_converter_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,22 @@ class MeshConverterCfg(AssetConverterBaseCfg):
Note:
If None, then no collision properties will be added.
"""
mesh_collision_props: schemas_cfg.MeshCollisionBaseCfg = None
mesh_collision_props: (
schemas_cfg.MeshCollisionBaseCfg
| schemas_cfg.MeshCollisionFragment
| list[schemas_cfg.MeshCollisionFragment]
| None
) = None
"""Mesh approximation properties to apply to all collision meshes in the USD.

Accepts either a single legacy cfg (e.g. :class:`~isaaclab.sim.schemas.MeshCollisionBaseCfg` or
a ``Physx*PropertiesCfg`` cooking cfg) or a list of
:class:`~isaaclab.sim.schemas.MeshCollisionFragment` fragments (e.g.
``[UsdPhysicsMeshCollisionCfg(...), PhysxConvexHullCfg(...)]``). When a fragment list is given,
``UsdPhysics.MeshCollisionAPI`` is applied as the implicit anchor, the ``physics:approximation``
token is resolved from whichever cooking fragment is present, and each fragment writes its own
namespace.

Note:
If None, then no mesh approximation properties will be added.
"""
Expand Down
8 changes: 8 additions & 0 deletions source/isaaclab/isaaclab/sim/schemas/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ __all__ = [
"apply_collision_properties",
"apply_fixed_tendon_properties",
"apply_mass_properties",
"apply_mesh_collision",
"apply_mesh_collision_properties",
"apply_namespaced",
"apply_rigid_body_properties",
"apply_spatial_tendon_properties",
Expand Down Expand Up @@ -43,10 +45,12 @@ __all__ = [
"MassFragment",
"MassPropertiesCfg",
"MeshCollisionBaseCfg",
"MeshCollisionFragment",
"RigidBodyFragment",
"SchemaFragment",
"SpatialTendonFragment",
"UsdPhysicsCollisionCfg",
"UsdPhysicsMeshCollisionCfg",
"UsdPhysicsRigidBodyCfg",
"MujocoJointDrivePropertiesCfg",
"MujocoRigidBodyPropertiesCfg",
Expand All @@ -68,6 +72,8 @@ from .schemas import (
apply_collision_properties,
apply_fixed_tendon_properties,
apply_mass_properties,
apply_mesh_collision,
apply_mesh_collision_properties,
apply_namespaced,
apply_rigid_body_properties,
apply_spatial_tendon_properties,
Expand Down Expand Up @@ -104,11 +110,13 @@ from .schemas_cfg import (
MassFragment,
MassPropertiesCfg,
MeshCollisionBaseCfg,
MeshCollisionFragment,
RigidBodyBaseCfg,
RigidBodyFragment,
SchemaFragment,
SpatialTendonFragment,
UsdPhysicsCollisionCfg,
UsdPhysicsMeshCollisionCfg,
UsdPhysicsRigidBodyCfg,
)

Expand Down
110 changes: 110 additions & 0 deletions source/isaaclab/isaaclab/sim/schemas/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,13 @@ def apply_namespaced(cfg: schemas_cfg.SchemaFragment, prim_path: str, stage: Usd
# ``func`` is the only non-USD field; non-scalar values raise in the setter
if f.name == "func":
continue
# ``mesh_approximation_name`` is not a namespaced attribute: it is the standard
# ``physics:approximation`` token, written by ``apply_mesh_collision_properties`` (the
# family writer) which validates it against ``MESH_APPROXIMATION_TOKENS``. Skip it here
# so a mesh-collision cooking fragment dispatched through this generic applier does not
# author a spurious ``<namespace>:meshApproximationName`` attribute.
if f.name == "mesh_approximation_name":
continue
value = getattr(cfg, f.name)
if value is None:
continue
Expand Down Expand Up @@ -468,6 +475,109 @@ def apply_rigid_body_properties(
return success


def apply_mesh_collision(
cfg: schemas_cfg.MeshCollisionFragment, prim_path: str, stage: Usd.Stage | None = None
) -> bool:
"""Apply a single mesh-collision fragment: its namespaced cooking attrs plus the shared token.

This is the default :attr:`~isaaclab.sim.schemas.SchemaFragment.func` for every
:class:`~isaaclab.sim.schemas.MeshCollisionFragment`. Unlike the generic :func:`apply_namespaced`,
a mesh-collision fragment additionally authors the shared ``physics:approximation`` token (via the
standard ``UsdPhysics.MeshCollisionAPI``) on top of its own backend cooking namespace.

The token is *not* a plain namespaced attribute -- it is shared state on the family anchor implied
by the present cooking fragment. Each fragment carries a :attr:`mesh_approximation_name` whose
default encodes the token its schema implies (e.g. ``"convexHull"`` for :class:`PhysxConvexHullCfg`,
``"sdf"`` for :class:`PhysxSDFMeshCfg`). A name of ``"none"`` leaves the token unchanged, so when
several fragments are dispatched in order by :func:`apply_mesh_collision_properties` the last one
with a non-``"none"`` name wins -- this is how a core fragment composes with a backend cooking
fragment. The name is validated against :const:`MESH_APPROXIMATION_TOKENS`; an unknown name raises
``ValueError``. :attr:`mesh_approximation_name` is skipped by :func:`apply_namespaced`, so it is
never authored as a spurious ``<namespace>:meshApproximationName`` attribute.

Args:
cfg: The mesh-collision fragment to apply.
prim_path: The prim path to author on. This prim should be a Mesh.
stage: The stage where to find the prim. Defaults to None, in which case the current
stage is used.

Returns:
True if the fragment was applied successfully.

Raises:
ValueError: If the prim at ``prim_path`` is not valid, or when the fragment's mesh
approximation name is not in :const:`MESH_APPROXIMATION_TOKENS`.
"""
if stage is None:
stage = get_current_stage()
prim = stage.GetPrimAtPath(prim_path)
if not prim.IsValid():
raise ValueError(f"Prim path '{prim_path}' is not valid.")
# ensure the standard MeshCollisionAPI anchor (carrier of ``physics:approximation``) exists
if not UsdPhysics.MeshCollisionAPI(prim):
UsdPhysics.MeshCollisionAPI.Apply(prim)
# write the fragment's backend cooking namespace + applied schema; ``mesh_approximation_name`` is
# skipped by the generic applier as it is the shared token handled below
success = apply_namespaced(cfg, prim_path, stage)
# author the shared ``physics:approximation`` token this fragment implies; ``"none"`` leaves the
# token untouched so a later non-"none" fragment in a list dispatch wins
name = getattr(cfg, "mesh_approximation_name", None)
if name is not None and name != "none":
if name not in MESH_APPROXIMATION_TOKENS:
raise ValueError(
f"Invalid mesh approximation name: '{name}'. "
f"Valid options are: {list(MESH_APPROXIMATION_TOKENS.keys())}"
)
safe_set_attribute_on_usd_schema(
UsdPhysics.MeshCollisionAPI(prim), "Approximation", MESH_APPROXIMATION_TOKENS[name], camel_case=False
)
return success


def apply_mesh_collision_properties(
prim_path: str, fragments: Iterable[schemas_cfg.MeshCollisionFragment], stage: Usd.Stage | None = None
) -> bool:
"""Apply a list of mesh-collision fragments to a prim.

Applies ``UsdPhysics.MeshCollisionAPI`` as the implicit anchor (the carrier of the
``physics:approximation`` token), then dispatches each fragment via its
:attr:`~isaaclab.sim.schemas.SchemaFragment.func`. The default mesh-collision func
(:func:`apply_mesh_collision`) authors both the fragment's backend cooking namespace and the
shared approximation token it implies, so composing a core fragment with a backend cooking
fragment lets the last fragment with a non-``"none"`` :attr:`mesh_approximation_name` set the
token. Backend cooking fragments carry their own funcs, so core never imports a backend.

Args:
prim_path: The prim path to apply the mesh-collision schemas on. This prim should be a Mesh.
fragments: An iterable of :class:`~isaaclab.sim.schemas.MeshCollisionFragment` instances.
stage: The stage where to find the prim. Defaults to None, in which case the current
stage is used.

Returns:
True if all fragments applied successfully, False if any fragment reported failure.

Raises:
ValueError: If the prim at ``prim_path`` is not valid, or when a fragment's mesh
approximation name is not in :const:`MESH_APPROXIMATION_TOKENS`.
"""
if stage is None:
stage = get_current_stage()
prim = stage.GetPrimAtPath(prim_path)
# fail loudly on an invalid path (matches the sibling apply_* writers)
if not prim.IsValid():
raise ValueError(f"Prim path '{prim_path}' is not valid.")
# apply the standard MeshCollisionAPI anchor (carrier of ``physics:approximation``)
if not UsdPhysics.MeshCollisionAPI(prim):
UsdPhysics.MeshCollisionAPI.Apply(prim)
# dispatch each fragment via its ``func`` (cooking-schema namespace + implied approximation
# token), aggregating per-fragment results so a reported failure is not masked by the anchor
success = True
for cfg in fragments:
func = cfg.func if callable(cfg.func) else string_to_callable(cfg.func)
success = bool(func(cfg, prim_path, stage)) and success
return success


def define_rigid_body_properties(prim_path: str, cfg: schemas_cfg.RigidBodyBaseCfg, stage: Usd.Stage | None = None):
"""Apply the rigid body schema on the input prim and set its properties.

Expand Down
54 changes: 50 additions & 4 deletions source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,24 @@ class CollisionFragment(SchemaFragment):
pass


@configclass
class MeshCollisionFragment(SchemaFragment):
"""Marker base for mesh-collision fragments; types the ``mesh_collision_props`` slot.

A mesh-collision concept is split across one *core* fragment carrying the standard
``physics:approximation`` token (:class:`UsdPhysicsMeshCollisionCfg`) and one cooking
fragment per backend cooking schema (PhysX convex hull / decomposition / triangle mesh /
SDF, Newton mesh / SDF). Whichever cooking fragment is present implies the approximation
token written to ``physics:approximation`` -- see
:func:`~isaaclab.sim.schemas.apply_mesh_collision_properties`.
"""

# Mesh-collision fragments author the shared ``physics:approximation`` token in addition to their
# own namespaced cooking attrs, so they dispatch through :func:`~isaaclab.sim.schemas.apply_mesh_collision`
# (not the generic :func:`~isaaclab.sim.schemas.apply_namespaced`). See that func for the token coupling.
func: Callable | str = "isaaclab.sim.schemas:apply_mesh_collision"


@configclass
class FixedTendonFragment(SchemaFragment):
"""Marker base for fixed-tendon fragments; types the ``fixed_tendons_props`` slot.
Expand Down Expand Up @@ -233,6 +251,37 @@ class UsdPhysicsCollisionCfg(CollisionFragment):
"""


@configclass
class UsdPhysicsMeshCollisionCfg(MeshCollisionFragment):
"""``physics:approximation`` mesh-collision token from `UsdPhysics.MeshCollisionAPI`_.

Carries the standard mesh-collision approximation token (:attr:`mesh_approximation_name`
written to ``physics:approximation``). The ``UsdPhysics.MeshCollisionAPI`` schema is applied
as the implicit anchor by the mesh-collision family writer
(:func:`~isaaclab.sim.schemas.apply_mesh_collision_properties`), so this fragment owns no
applied schema of its own.

.. note::
The ``physics:approximation`` attribute is a ``TfToken`` validated against
:const:`~isaaclab.sim.schemas.MESH_APPROXIMATION_TOKENS`; the family writer (not the generic
:func:`~isaaclab.sim.schemas.apply_namespaced` applier) handles the token write, so this
fragment overrides nothing but the namespace metadata. When a PhysX/Newton cooking fragment
is present alongside this one, its default :attr:`mesh_approximation_name` sets the token.

.. _UsdPhysics.MeshCollisionAPI: https://openusd.org/release/api/class_usd_physics_mesh_collision_a_p_i.html
"""

_usd_namespace: ClassVar[str | None] = "physics"
_usd_applied_schema: ClassVar[str | None] = None # MeshCollisionAPI applied by the family anchor

mesh_approximation_name: str = "none"
"""Name of mesh collision approximation method. Default: "none".

Writes the ``physics:approximation`` token via :class:`UsdPhysics.MeshCollisionAPI`.
Refer to :const:`~isaaclab.sim.schemas.MESH_APPROXIMATION_TOKENS` for available options.
"""


@configclass
class ArticulationRootBaseCfg:
"""Solver-common properties to apply to the root of an articulation.
Expand Down Expand Up @@ -637,10 +686,7 @@ class MeshCollisionBaseCfg:
"""

# -- Class metadata (not dataclass fields) --
# The standard ``UsdPhysics.MeshCollisionAPI`` is always applied by the writer when a
# mesh-collision cfg is supplied; ``_usd_applied_schema`` here records the standard
# API name so subclasses that author no PhysX namespace can rely on the writer's
# standard-vs-PhysX gating logic. PhysX-cooking subclasses override this.
# Records the standard API name for the writer's standard-vs-PhysX gating; cooking subclasses override.
_usd_applied_schema: ClassVar[str | None] = "MeshCollisionAPI"
# Base class authors no PhysX-namespaced fields, so no namespace is defined.
_usd_namespace: ClassVar[str | None] = None
Expand Down
Loading
Loading