Skip to content
Draft
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.
6 changes: 6 additions & 0 deletions source/isaaclab/isaaclab/sim/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,15 @@ __all__ = [
"NewtonMeshCollisionPropertiesCfg",
"NewtonRigidBodyPropertiesCfg",
"NewtonSDFCollisionPropertiesCfg",
"MeshCollisionFragment",
"PhysxJointDrivePropertiesCfg",
"PhysxRigidBodyPropertiesCfg",
"RigidBodyBaseCfg",
"RigidBodyFragment",
"SchemaFragment",
"UsdPhysicsMeshCollisionCfg",
"UsdPhysicsRigidBodyCfg",
"apply_mesh_collision_properties",
"apply_namespaced",
"apply_rigid_body_properties",
"SDFMeshPropertiesCfg",
Expand Down Expand Up @@ -221,6 +224,7 @@ from .schemas import (
FixedTendonPropertiesCfg,
JointDriveBaseCfg,
MassPropertiesCfg,
MeshCollisionFragment,
MeshCollisionPropertiesCfg,
PhysxJointDrivePropertiesCfg,
PhysxRigidBodyPropertiesCfg,
Expand All @@ -231,8 +235,10 @@ from .schemas import (
SpatialTendonPropertiesCfg,
TriangleMeshPropertiesCfg,
TriangleMeshSimplificationPropertiesCfg,
UsdPhysicsMeshCollisionCfg,
UsdPhysicsRigidBodyCfg,
activate_contact_sensors,
apply_mesh_collision_properties,
apply_namespaced,
apply_rigid_body_properties,
define_articulation_root_properties,
Expand Down
20 changes: 17 additions & 3 deletions source/isaaclab/isaaclab/sim/converters/mesh_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
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 @@ -131,9 +132,22 @@ def _convert_asset(self, cfg: MeshConverterCfg):
)
# 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 @@ -185,7 +199,7 @@ def _convert_asset(self, cfg: MeshConverterCfg):
# apply mass properties
if cfg.mass_props is not None:
schemas.define_mass_properties(prim_path=xform_prim.GetPath(), cfg=cfg.mass_props, stage=stage)
# apply rigid body properties (transition routing: fragment list -> apply_*; legacy cfg -> define_*)
# 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):
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 @@ -32,8 +32,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
6 changes: 6 additions & 0 deletions source/isaaclab/isaaclab/sim/schemas/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ __all__ = [
"PHYSX_MESH_COLLISION_CFGS",
"USD_MESH_COLLISION_CFGS",
"activate_contact_sensors",
"apply_mesh_collision_properties",
"apply_namespaced",
"apply_rigid_body_properties",
"define_actuator_properties",
Expand Down Expand Up @@ -35,8 +36,10 @@ __all__ = [
"JointDriveBaseCfg",
"MassPropertiesCfg",
"MeshCollisionBaseCfg",
"MeshCollisionFragment",
"RigidBodyFragment",
"SchemaFragment",
"UsdPhysicsMeshCollisionCfg",
"UsdPhysicsRigidBodyCfg",
"MujocoJointDrivePropertiesCfg",
"MujocoRigidBodyPropertiesCfg",
Expand All @@ -55,6 +58,7 @@ from .schemas import (
PHYSX_MESH_COLLISION_CFGS,
USD_MESH_COLLISION_CFGS,
activate_contact_sensors,
apply_mesh_collision_properties,
apply_namespaced,
apply_rigid_body_properties,
define_articulation_root_properties,
Expand Down Expand Up @@ -86,9 +90,11 @@ from .schemas_cfg import (
JointDriveBaseCfg,
MassPropertiesCfg,
MeshCollisionBaseCfg,
MeshCollisionFragment,
RigidBodyBaseCfg,
RigidBodyFragment,
SchemaFragment,
UsdPhysicsMeshCollisionCfg,
UsdPhysicsRigidBodyCfg,
)

Expand Down
71 changes: 71 additions & 0 deletions source/isaaclab/isaaclab/sim/schemas/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,13 @@ def apply_namespaced(cfg, prim_path: str, stage: Usd.Stage | None = None) -> boo
for f in dataclasses.fields(cfg):
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 @@ -448,6 +455,70 @@ def apply_rigid_body_properties(prim_path: str, fragments, stage: Usd.Stage | No
return True


def apply_mesh_collision_properties(prim_path: str, fragments, 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), resolves and writes that token, then dispatches each
fragment via its :attr:`~isaaclab.sim.schemas.SchemaFragment.func`. Backend cooking fragments
carry backend-specific funcs (the generic :func:`apply_namespaced` applier), so core never
imports a backend.

.. attention::
**Approximation-token coupling.** The ``physics:approximation`` token is *not* a plain
namespaced attribute: it is shared state set by whichever cooking fragment is present.
Each fragment carries a :attr:`mesh_approximation_name` whose default encodes the token its
cooking schema implies (e.g. ``"convexHull"`` for :class:`PhysxConvexHullCfg`, ``"sdf"``
for :class:`PhysxSDFMeshCfg`). This writer scans the fragment list and uses the last
fragment whose :attr:`mesh_approximation_name` is set to a non-``"none"`` value (mirroring
the legacy single-cfg behavior), falling back to ``"none"`` when none is set. The token is
validated against :const:`MESH_APPROXIMATION_TOKENS`; an unknown name raises ``ValueError``.
The :attr:`mesh_approximation_name` field is therefore handled here and explicitly skipped
by :func:`apply_namespaced` so it is never authored as a namespaced attribute.

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 the properties were successfully set.

Raises:
ValueError: When the resolved mesh approximation name is not in
:const:`MESH_APPROXIMATION_TOKENS`.
"""
if stage is None:
stage = get_current_stage()
prim = stage.GetPrimAtPath(prim_path)
# apply the standard MeshCollisionAPI anchor (carrier of ``physics:approximation``)
if not UsdPhysics.MeshCollisionAPI(prim):
UsdPhysics.MeshCollisionAPI.Apply(prim)

# resolve the shared approximation token: last fragment with a non-"none" name wins
approximation_name = "none"
for cfg in fragments:
name = getattr(cfg, "mesh_approximation_name", None)
if name is not None and name != "none":
approximation_name = name
if approximation_name not in MESH_APPROXIMATION_TOKENS:
raise ValueError(
f"Invalid mesh approximation name: '{approximation_name}'. "
f"Valid options are: {list(MESH_APPROXIMATION_TOKENS.keys())}"
)
approximation_token = MESH_APPROXIMATION_TOKENS[approximation_name]
safe_set_attribute_on_usd_schema(
UsdPhysics.MeshCollisionAPI(prim), "Approximation", approximation_token, camel_case=False
)

# dispatch each fragment via its ``func`` (cooking-schema application + namespaced tuning attrs)
for cfg in fragments:
func = cfg.func if callable(cfg.func) else string_to_callable(cfg.func)
func(cfg, prim_path, stage)
return True


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
51 changes: 47 additions & 4 deletions source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,52 @@ class UsdPhysicsRigidBodyCfg(RigidBodyFragment):
"""


@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`.
"""

pass


@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 @@ -529,10 +575,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
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ def _spawn_from_usd_file(

# modify rigid body properties
if cfg.rigid_props is not None:
# transition routing: new fragment list -> apply_*; legacy single cfg -> modify_*
# transition shim, remove later: new fragment list -> apply_*; legacy single cfg -> modify_*
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):
schemas.apply_rigid_body_properties(prim_path, rigid_frags)
Expand Down
2 changes: 1 addition & 1 deletion source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ def _spawn_mesh_geom_from_mesh(
# apply mass properties
if cfg.mass_props is not None:
schemas.define_mass_properties(prim_path, cfg.mass_props, stage=stage)
# apply rigid properties (transition routing: fragment list -> apply_*; legacy cfg -> define_*)
# apply rigid properties (transition shim, remove later: fragment list -> apply_*; legacy cfg -> define_*)
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):
schemas.apply_rigid_body_properties(prim_path, rigid_frags, stage=stage)
Expand Down
2 changes: 1 addition & 1 deletion source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ def _spawn_geom_from_prim_type(
schemas.define_mass_properties(prim_path, cfg.mass_props, stage=stage)
# apply rigid body properties
if cfg.rigid_props is not None:
# transition routing: new fragment list -> apply_*; legacy single cfg -> define_*
# transition shim, remove later: new fragment list -> apply_*; legacy single cfg -> define_*
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):
schemas.apply_rigid_body_properties(prim_path, rigid_frags, stage=stage)
Expand Down
Loading
Loading