From 9dcf5c4ab07954eec93a6ad2da33c85b26561212 Mon Sep 17 00:00:00 2001 From: Vidur Vij Date: Thu, 4 Jun 2026 18:20:22 -0700 Subject: [PATCH 1/2] feat(schemas): add collision schema-fragment API Add the additive single-namespace collision fragment family mirroring the rigid-body pilot: the CollisionFragment marker and UsdPhysicsCollisionCfg in core, PhysxCollisionCfg in isaaclab_physx, and NewtonCollisionCfg in isaaclab_newton. Add the apply_collision_properties family writer that applies UsdPhysics.CollisionAPI as the implicit anchor and dispatches each fragment via its func. Widen the collision_props spawner and mesh-converter slots to accept a fragment list and add a transition bridge at every spawn site so legacy CollisionPropertiesCfg cfgs keep working. Export the new public names from the core, sim-level, physx, and newton stubs, add a fragment test file, and add changelog fragments. --- .../vidurv-schema-frag-collision.minor.rst | 20 +++ source/isaaclab/isaaclab/sim/__init__.pyi | 6 + .../isaaclab/sim/converters/mesh_converter.py | 11 +- .../sim/converters/mesh_converter_cfg.py | 9 +- .../isaaclab/sim/schemas/__init__.pyi | 6 + .../isaaclab/isaaclab/sim/schemas/schemas.py | 28 +++ .../isaaclab/sim/schemas/schemas_cfg.py | 28 +++ .../sim/spawners/from_files/from_files.py | 7 +- .../isaaclab/sim/spawners/meshes/meshes.py | 7 +- .../isaaclab/sim/spawners/shapes/shapes.py | 7 +- .../isaaclab/sim/spawners/spawner_cfg.py | 13 +- .../test/sim/test_collision_fragments.py | 170 ++++++++++++++++++ .../vidurv-schema-frag-collision.minor.rst | 8 + .../isaaclab_newton/sim/schemas/__init__.pyi | 2 + .../sim/schemas/schemas_cfg.py | 36 ++++ .../vidurv-schema-frag-collision.minor.rst | 7 + .../isaaclab_physx/sim/schemas/__init__.pyi | 2 + .../isaaclab_physx/sim/schemas/schemas_cfg.py | 53 ++++++ 18 files changed, 412 insertions(+), 8 deletions(-) create mode 100644 source/isaaclab/changelog.d/vidurv-schema-frag-collision.minor.rst create mode 100644 source/isaaclab/test/sim/test_collision_fragments.py create mode 100644 source/isaaclab_newton/changelog.d/vidurv-schema-frag-collision.minor.rst create mode 100644 source/isaaclab_physx/changelog.d/vidurv-schema-frag-collision.minor.rst diff --git a/source/isaaclab/changelog.d/vidurv-schema-frag-collision.minor.rst b/source/isaaclab/changelog.d/vidurv-schema-frag-collision.minor.rst new file mode 100644 index 000000000000..d31eb5bcee92 --- /dev/null +++ b/source/isaaclab/changelog.d/vidurv-schema-frag-collision.minor.rst @@ -0,0 +1,20 @@ +Added +^^^^^ + +* Added the collision schema-fragment API: the + :class:`~isaaclab.sim.schemas.CollisionFragment` marker and + :class:`~isaaclab.sim.schemas.UsdPhysicsCollisionCfg` (the ``physics:collisionEnabled`` + single-namespace fragment). Each fragment carries ``_usd_namespace`` / ``_usd_applied_schema`` + metadata and a ``func`` applier so a prim can carry collision properties from multiple USD + namespaces at once. +* Added :func:`~isaaclab.sim.schemas.apply_collision_properties`, which applies a list of + collision fragments with ``UsdPhysics.CollisionAPI`` as the implicit anchor. + +Changed +^^^^^^^ + +* Changed the spawner ``collision_props`` slot + (:attr:`~isaaclab.sim.spawners.RigidObjectSpawnerCfg.collision_props`) and the mesh-converter + ``collision_props`` slot to also accept a list of + :class:`~isaaclab.sim.schemas.CollisionFragment` fragments. Legacy single cfgs continue to work + through a transition bridge in the spawn writers. diff --git a/source/isaaclab/isaaclab/sim/__init__.pyi b/source/isaaclab/isaaclab/sim/__init__.pyi index 9578caa87b49..f62caabf0be1 100644 --- a/source/isaaclab/isaaclab/sim/__init__.pyi +++ b/source/isaaclab/isaaclab/sim/__init__.pyi @@ -39,6 +39,7 @@ __all__ = [ "BoundingCubePropertiesCfg", "BoundingSpherePropertiesCfg", "CollisionBaseCfg", + "CollisionFragment", "ConvexDecompositionPropertiesCfg", "ConvexHullPropertiesCfg", "DeformableBodyPropertiesBaseCfg", @@ -61,7 +62,9 @@ __all__ = [ "RigidBodyBaseCfg", "RigidBodyFragment", "SchemaFragment", + "UsdPhysicsCollisionCfg", "UsdPhysicsRigidBodyCfg", + "apply_collision_properties", "apply_namespaced", "apply_rigid_body_properties", "SDFMeshPropertiesCfg", @@ -214,6 +217,7 @@ from .schemas import ( BoundingCubePropertiesCfg, BoundingSpherePropertiesCfg, CollisionBaseCfg, + CollisionFragment, ConvexDecompositionPropertiesCfg, ConvexHullPropertiesCfg, DeformableBodyPropertiesBaseCfg, @@ -231,8 +235,10 @@ from .schemas import ( SpatialTendonPropertiesCfg, TriangleMeshPropertiesCfg, TriangleMeshSimplificationPropertiesCfg, + UsdPhysicsCollisionCfg, UsdPhysicsRigidBodyCfg, activate_contact_sensors, + apply_collision_properties, apply_namespaced, apply_rigid_body_properties, define_articulation_root_properties, diff --git a/source/isaaclab/isaaclab/sim/converters/mesh_converter.py b/source/isaaclab/isaaclab/sim/converters/mesh_converter.py index 9c25697e88f1..d44460e09161 100644 --- a/source/isaaclab/isaaclab/sim/converters/mesh_converter.py +++ b/source/isaaclab/isaaclab/sim/converters/mesh_converter.py @@ -126,9 +126,16 @@ def _convert_asset(self, cfg: MeshConverterCfg): # Apply collider properties to mesh if cfg.collision_props is not None: # -- Collider properties such as offset, scale, etc. - schemas.define_collision_properties( - prim_path=child_mesh_prim.GetPath(), cfg=cfg.collision_props, stage=stage + # transition routing: new fragment list -> apply_*; legacy single cfg -> define_* + 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): + 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( diff --git a/source/isaaclab/isaaclab/sim/converters/mesh_converter_cfg.py b/source/isaaclab/isaaclab/sim/converters/mesh_converter_cfg.py index 73ec37e777b6..09c887a48996 100644 --- a/source/isaaclab/isaaclab/sim/converters/mesh_converter_cfg.py +++ b/source/isaaclab/isaaclab/sim/converters/mesh_converter_cfg.py @@ -26,9 +26,16 @@ class MeshConverterCfg(AssetConverterBaseCfg): If None, then no rigid body properties will be added. """ - collision_props: schemas_cfg.CollisionPropertiesCfg = None + collision_props: ( + schemas_cfg.CollisionPropertiesCfg | schemas_cfg.CollisionFragment | list[schemas_cfg.CollisionFragment] + ) = None """Collision properties to apply to the USD. Defaults to None. + Accepts either a single legacy cfg (e.g. :class:`~isaaclab.sim.schemas.CollisionBaseCfg`) or a + list of :class:`~isaaclab.sim.schemas.CollisionFragment` fragments. When a fragment list is + given, ``UsdPhysics.CollisionAPI`` is applied as the implicit anchor and each fragment writes + its own namespace. + Note: If None, then no collision properties will be added. """ diff --git a/source/isaaclab/isaaclab/sim/schemas/__init__.pyi b/source/isaaclab/isaaclab/sim/schemas/__init__.pyi index af153a60fc63..4e46e287e8a2 100644 --- a/source/isaaclab/isaaclab/sim/schemas/__init__.pyi +++ b/source/isaaclab/isaaclab/sim/schemas/__init__.pyi @@ -8,6 +8,7 @@ __all__ = [ "PHYSX_MESH_COLLISION_CFGS", "USD_MESH_COLLISION_CFGS", "activate_contact_sensors", + "apply_collision_properties", "apply_namespaced", "apply_rigid_body_properties", "define_actuator_properties", @@ -30,6 +31,7 @@ __all__ = [ "BoundingCubePropertiesCfg", "BoundingSpherePropertiesCfg", "CollisionBaseCfg", + "CollisionFragment", "DeformableBodyPropertiesBaseCfg", "DeformableBodyPropertiesCfg", "JointDriveBaseCfg", @@ -37,6 +39,7 @@ __all__ = [ "MeshCollisionBaseCfg", "RigidBodyFragment", "SchemaFragment", + "UsdPhysicsCollisionCfg", "UsdPhysicsRigidBodyCfg", "MujocoJointDrivePropertiesCfg", "MujocoRigidBodyPropertiesCfg", @@ -55,6 +58,7 @@ from .schemas import ( PHYSX_MESH_COLLISION_CFGS, USD_MESH_COLLISION_CFGS, activate_contact_sensors, + apply_collision_properties, apply_namespaced, apply_rigid_body_properties, define_articulation_root_properties, @@ -81,6 +85,7 @@ from .schemas_cfg import ( BoundingCubePropertiesCfg, BoundingSpherePropertiesCfg, CollisionBaseCfg, + CollisionFragment, DeformableBodyPropertiesBaseCfg, DeformableBodyPropertiesCfg, JointDriveBaseCfg, @@ -89,6 +94,7 @@ from .schemas_cfg import ( RigidBodyBaseCfg, RigidBodyFragment, SchemaFragment, + UsdPhysicsCollisionCfg, UsdPhysicsRigidBodyCfg, ) diff --git a/source/isaaclab/isaaclab/sim/schemas/schemas.py b/source/isaaclab/isaaclab/sim/schemas/schemas.py index 11eca17349bc..1472f33ecec7 100644 --- a/source/isaaclab/isaaclab/sim/schemas/schemas.py +++ b/source/isaaclab/isaaclab/sim/schemas/schemas.py @@ -537,6 +537,34 @@ def modify_rigid_body_properties( """ +def apply_collision_properties(prim_path: str, fragments, stage: Usd.Stage | None = None) -> bool: + """Apply a list of collision fragments to a prim. + + Applies ``UsdPhysics.CollisionAPI`` as the implicit anchor (the defining schema for a + collider), then dispatches each fragment via its + :attr:`~isaaclab.sim.schemas.SchemaFragment.func`. Backend fragments carry backend-specific + funcs, so core never imports a backend. + + Args: + prim_path: The prim path to apply the collision schemas on. + fragments: An iterable of :class:`~isaaclab.sim.schemas.CollisionFragment` 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. + """ + if stage is None: + stage = get_current_stage() + prim = stage.GetPrimAtPath(prim_path) + if not UsdPhysics.CollisionAPI(prim): + UsdPhysics.CollisionAPI.Apply(prim) + 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_collision_properties( prim_path: str, cfg: schemas_cfg.CollisionPropertiesCfg, stage: Usd.Stage | None = None ): diff --git a/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py b/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py index bbd29df6b652..a39180b0c78f 100644 --- a/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py +++ b/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py @@ -167,6 +167,34 @@ class UsdPhysicsRigidBodyCfg(RigidBodyFragment): """ +@configclass +class CollisionFragment(SchemaFragment): + """Marker base for collision fragments; types the ``collision_props`` slot.""" + + pass + + +@configclass +class UsdPhysicsCollisionCfg(CollisionFragment): + """``physics:*`` collision attributes from `UsdPhysics.CollisionAPI`_. + + The ``UsdPhysics.CollisionAPI`` schema is applied as the implicit anchor by the collision + family writer (:func:`~isaaclab.sim.schemas.apply_collision_properties`), so this fragment + owns no applied schema of its own. + + .. _UsdPhysics.CollisionAPI: https://openusd.org/dev/api/class_usd_physics_collision_a_p_i.html + """ + + _usd_namespace: ClassVar[str | None] = "physics" + _usd_applied_schema: ClassVar[str | None] = None # CollisionAPI applied by the family anchor + + collision_enabled: bool | None = None + """Whether to enable or disable collisions. + + Writes ``physics:collisionEnabled`` via :class:`UsdPhysics.CollisionAPI`. + """ + + @configclass class ArticulationRootBaseCfg: """Solver-common properties to apply to the root of an articulation. diff --git a/source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py b/source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py index a2cf6b684c17..79fb79232674 100644 --- a/source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py +++ b/source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py @@ -352,7 +352,12 @@ def _spawn_from_usd_file( schemas.modify_rigid_body_properties(prim_path, cfg.rigid_props) # modify collision properties if cfg.collision_props is not None: - schemas.modify_collision_properties(prim_path, cfg.collision_props) + # transition routing: new fragment list -> apply_*; legacy single cfg -> modify_* + 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): + schemas.apply_collision_properties(prim_path, coll_frags) + else: + schemas.modify_collision_properties(prim_path, cfg.collision_props) # modify mass properties if cfg.mass_props is not None: schemas.modify_mass_properties(prim_path, cfg.mass_props) diff --git a/source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py b/source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py index d8eaffa5593a..925c0bbdae71 100644 --- a/source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py +++ b/source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py @@ -415,7 +415,12 @@ def _spawn_mesh_geom_from_mesh( mesh_collision_api = UsdPhysics.MeshCollisionAPI.Apply(mesh_prim) mesh_collision_api.GetApproximationAttr().Set(collision_approximation) # apply collision properties - schemas.define_collision_properties(mesh_prim_path, cfg.collision_props, stage=stage) + # transition routing: new fragment list -> apply_*; legacy single cfg -> define_* + 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): + schemas.apply_collision_properties(mesh_prim_path, coll_frags, stage=stage) + else: + schemas.define_collision_properties(mesh_prim_path, cfg.collision_props, stage=stage) # apply visual material if cfg.visual_material is not None: diff --git a/source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py b/source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py index d4af64a14677..9ef2aab6bb39 100644 --- a/source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py +++ b/source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py @@ -294,7 +294,12 @@ def _spawn_geom_from_prim_type( create_prim(mesh_prim_path, prim_type, scale=scale, attributes=attributes, stage=stage) # apply collision properties if cfg.collision_props is not None: - schemas.define_collision_properties(mesh_prim_path, cfg.collision_props, stage=stage) + # transition routing: new fragment list -> apply_*; legacy single cfg -> define_* + 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): + schemas.apply_collision_properties(mesh_prim_path, coll_frags, stage=stage) + else: + schemas.define_collision_properties(mesh_prim_path, cfg.collision_props, stage=stage) # apply visual material if cfg.visual_material is not None: if not cfg.visual_material_path.startswith("/"): diff --git a/source/isaaclab/isaaclab/sim/spawners/spawner_cfg.py b/source/isaaclab/isaaclab/sim/spawners/spawner_cfg.py index 1d52d451ba1b..97b5ed3883e2 100644 --- a/source/isaaclab/isaaclab/sim/spawners/spawner_cfg.py +++ b/source/isaaclab/isaaclab/sim/spawners/spawner_cfg.py @@ -99,8 +99,17 @@ class RigidObjectSpawnerCfg(SpawnerCfg): make the object static and will not be affected by gravity or other forces. """ - collision_props: schemas.CollisionPropertiesCfg | None = None - """Properties to apply to all collision meshes.""" + collision_props: ( + schemas.CollisionPropertiesCfg | schemas.CollisionFragment | list[schemas.CollisionFragment] | None + ) = None + """Properties to apply to all collision meshes. + + Accepts either a single legacy cfg (e.g. :class:`~isaaclab.sim.schemas.CollisionBaseCfg`) or a + list of :class:`~isaaclab.sim.schemas.CollisionFragment` fragments + (e.g. ``[UsdPhysicsCollisionCfg(...), PhysxCollisionCfg(...)]``). When a fragment list is given, + ``UsdPhysics.CollisionAPI`` is applied as the implicit anchor and each fragment writes its own + namespace. + """ activate_contact_sensors: bool = False """Activate contact reporting on all rigid bodies. Defaults to False. diff --git a/source/isaaclab/test/sim/test_collision_fragments.py b/source/isaaclab/test/sim/test_collision_fragments.py new file mode 100644 index 000000000000..698006595928 --- /dev/null +++ b/source/isaaclab/test/sim/test_collision_fragments.py @@ -0,0 +1,170 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Launch Isaac Sim Simulator first.""" + +from isaaclab.app import AppLauncher + +# launch omniverse app +simulation_app = AppLauncher(headless=True).app + +"""Rest everything follows.""" + +from pxr import UsdGeom, UsdPhysics + +import isaaclab.sim as sim_utils +from isaaclab.sim import SimulationCfg, SimulationContext + + +def _make_xform(stage, path="/World/Body"): + UsdGeom.Xform.Define(stage, path) + return stage.GetPrimAtPath(path) + + +# ------------------------------------------------------------------------------------- +# CollisionFragment marker + UsdPhysicsCollisionCfg +# ------------------------------------------------------------------------------------- + + +def test_collision_fragment_metadata_defaults(): + from isaaclab.sim.schemas import CollisionFragment, SchemaFragment, UsdPhysicsCollisionCfg + + cfg = UsdPhysicsCollisionCfg(collision_enabled=True) + assert isinstance(cfg, CollisionFragment) and isinstance(cfg, SchemaFragment) + assert type(cfg)._usd_namespace == "physics" + assert type(cfg)._usd_applied_schema is None # anchor applies CollisionAPI, not the fragment + assert cfg.func == "isaaclab.sim.schemas:apply_namespaced" + assert cfg.collision_enabled is True + + +# ------------------------------------------------------------------------------------- +# UsdPhysicsCollisionCfg writes its physics namespace via apply_namespaced +# ------------------------------------------------------------------------------------- + + +def test_usd_physics_collision_fragment_writes_physics_namespace(): + from isaaclab.sim.schemas import UsdPhysicsCollisionCfg, apply_namespaced + + sim_utils.create_new_stage() + SimulationContext(SimulationCfg(dt=0.01)) + stage = sim_utils.get_current_stage() + prim = _make_xform(stage) + UsdPhysics.CollisionAPI.Apply(prim) + apply_namespaced(UsdPhysicsCollisionCfg(collision_enabled=True), "/World/Body", stage) + assert prim.GetAttribute("physics:collisionEnabled").Get() is True + + +# ------------------------------------------------------------------------------------- +# PhysxCollisionCfg (isaaclab_physx) +# ------------------------------------------------------------------------------------- + + +def test_physx_collision_fragment_writes_physx_namespace(): + from isaaclab_physx.sim.schemas import PhysxCollisionCfg + + from isaaclab.sim.schemas import apply_namespaced + + sim_utils.create_new_stage() + SimulationContext(SimulationCfg(dt=0.01)) + stage = sim_utils.get_current_stage() + prim = _make_xform(stage, "/World/C2") + UsdPhysics.CollisionAPI.Apply(prim) + apply_namespaced( + PhysxCollisionCfg(contact_offset=0.02, rest_offset=0.0, torsional_patch_radius=0.1), "/World/C2", stage + ) + assert abs(prim.GetAttribute("physxCollision:contactOffset").Get() - 0.02) < 1e-6 + assert abs(prim.GetAttribute("physxCollision:restOffset").Get() - 0.0) < 1e-6 + assert abs(prim.GetAttribute("physxCollision:torsionalPatchRadius").Get() - 0.1) < 1e-6 + + +# ------------------------------------------------------------------------------------- +# NewtonCollisionCfg (isaaclab_newton) +# ------------------------------------------------------------------------------------- + + +def test_newton_collision_fragment_writes_newton_namespace(): + from isaaclab_newton.sim.schemas import NewtonCollisionCfg + + from isaaclab.sim.schemas import apply_namespaced + + sim_utils.create_new_stage() + SimulationContext(SimulationCfg(dt=0.01)) + stage = sim_utils.get_current_stage() + prim = _make_xform(stage, "/World/C3") + UsdPhysics.CollisionAPI.Apply(prim) + apply_namespaced(NewtonCollisionCfg(contact_margin=0.01, contact_gap=0.005), "/World/C3", stage) + assert abs(prim.GetAttribute("newton:contactMargin").Get() - 0.01) < 1e-6 + assert abs(prim.GetAttribute("newton:contactGap").Get() - 0.005) < 1e-6 + + +# ------------------------------------------------------------------------------------- +# apply_collision_properties dispatch (implicit anchor + multi-namespace) +# ------------------------------------------------------------------------------------- + + +def test_apply_collision_properties_composes_namespaces(): + from isaaclab_newton.sim.schemas import NewtonCollisionCfg + from isaaclab_physx.sim.schemas import PhysxCollisionCfg + + from isaaclab.sim.schemas import UsdPhysicsCollisionCfg, apply_collision_properties + + sim_utils.create_new_stage() + SimulationContext(SimulationCfg(dt=0.01)) + stage = sim_utils.get_current_stage() + _make_xform(stage, "/World/C4") + apply_collision_properties( + "/World/C4", + [ + UsdPhysicsCollisionCfg(collision_enabled=True), + PhysxCollisionCfg(contact_offset=0.02), + NewtonCollisionCfg(contact_margin=0.01), + ], + stage, + ) + prim = stage.GetPrimAtPath("/World/C4") + assert bool(UsdPhysics.CollisionAPI(prim)) # implicit anchor applied + assert prim.GetAttribute("physics:collisionEnabled").Get() is True + assert abs(prim.GetAttribute("physxCollision:contactOffset").Get() - 0.02) < 1e-6 + assert abs(prim.GetAttribute("newton:contactMargin").Get() - 0.01) < 1e-6 + + +# ------------------------------------------------------------------------------------- +# spawner slot accepts a fragment list + transition routing +# ------------------------------------------------------------------------------------- + + +def test_spawn_shape_with_collision_fragment_list(): + from isaaclab_physx.sim.schemas import PhysxCollisionCfg + + from isaaclab.sim.schemas import UsdPhysicsCollisionCfg + + sim_utils.create_new_stage() + SimulationContext(SimulationCfg(dt=0.01)) + cfg = sim_utils.CuboidCfg( + size=(1, 1, 1), + collision_props=[UsdPhysicsCollisionCfg(collision_enabled=True), PhysxCollisionCfg(contact_offset=0.03)], + ) + cfg.func("/World/Cube", cfg) + prim = sim_utils.get_current_stage().GetPrimAtPath("/World/Cube/geometry/mesh") + assert bool(UsdPhysics.CollisionAPI(prim)) + assert abs(prim.GetAttribute("physxCollision:contactOffset").Get() - 0.03) < 1e-6 + + +# ------------------------------------------------------------------------------------- +# public imports +# ------------------------------------------------------------------------------------- + + +def test_public_imports(): + from isaaclab_newton.sim.schemas import NewtonCollisionCfg # noqa: F401 + from isaaclab_physx.sim.schemas import PhysxCollisionCfg # noqa: F401 + + from isaaclab.sim.schemas import ( # noqa: F401 + CollisionFragment, + SchemaFragment, + UsdPhysicsCollisionCfg, + apply_collision_properties, + apply_namespaced, + ) diff --git a/source/isaaclab_newton/changelog.d/vidurv-schema-frag-collision.minor.rst b/source/isaaclab_newton/changelog.d/vidurv-schema-frag-collision.minor.rst new file mode 100644 index 000000000000..dee587ff7e7c --- /dev/null +++ b/source/isaaclab_newton/changelog.d/vidurv-schema-frag-collision.minor.rst @@ -0,0 +1,8 @@ +Added +^^^^^ + +* Added :class:`~isaaclab_newton.sim.schemas.NewtonCollisionCfg`, the ``newton:*`` + single-namespace collision fragment (``newton:contactMargin``, ``newton:contactGap`` via + ``NewtonCollisionAPI``). It composes with + :class:`~isaaclab.sim.schemas.UsdPhysicsCollisionCfg` and + :class:`~isaaclab_physx.sim.schemas.PhysxCollisionCfg` in a ``collision_props`` fragment list. diff --git a/source/isaaclab_newton/isaaclab_newton/sim/schemas/__init__.pyi b/source/isaaclab_newton/isaaclab_newton/sim/schemas/__init__.pyi index 89ca2d068afb..f7413adbb883 100644 --- a/source/isaaclab_newton/isaaclab_newton/sim/schemas/__init__.pyi +++ b/source/isaaclab_newton/isaaclab_newton/sim/schemas/__init__.pyi @@ -8,6 +8,7 @@ __all__ = [ "MujocoRigidBodyCfg", "MujocoRigidBodyPropertiesCfg", "NewtonArticulationRootPropertiesCfg", + "NewtonCollisionCfg", "NewtonCollisionPropertiesCfg", "NewtonDeformableBodyPropertiesCfg", "NewtonJointDrivePropertiesCfg", @@ -22,6 +23,7 @@ from .schemas_cfg import ( MujocoRigidBodyCfg, MujocoRigidBodyPropertiesCfg, NewtonArticulationRootPropertiesCfg, + NewtonCollisionCfg, NewtonCollisionPropertiesCfg, NewtonDeformableBodyPropertiesCfg, NewtonJointDrivePropertiesCfg, diff --git a/source/isaaclab_newton/isaaclab_newton/sim/schemas/schemas_cfg.py b/source/isaaclab_newton/isaaclab_newton/sim/schemas/schemas_cfg.py index 4c99379e1ad8..edf007790b5e 100644 --- a/source/isaaclab_newton/isaaclab_newton/sim/schemas/schemas_cfg.py +++ b/source/isaaclab_newton/isaaclab_newton/sim/schemas/schemas_cfg.py @@ -10,6 +10,7 @@ from isaaclab.sim.schemas.schemas_cfg import ( ArticulationRootBaseCfg, CollisionBaseCfg, + CollisionFragment, DeformableBodyPropertiesBaseCfg, JointDriveBaseCfg, MeshCollisionBaseCfg, @@ -157,6 +158,41 @@ class MujocoJointDrivePropertiesCfg(NewtonJointDrivePropertiesCfg): """ +@configclass +class NewtonCollisionCfg(CollisionFragment): + """``newton:*`` collision attributes for Newton's contact pipeline. + + A single-namespace fragment (see :class:`~isaaclab.sim.schemas.SchemaFragment`) carrying + Newton-native contact-geometry attributes (``NewtonCollisionAPI``). Applied alongside + :class:`~isaaclab.sim.schemas.UsdPhysicsCollisionCfg` via + :func:`~isaaclab.sim.schemas.apply_collision_properties`. + + .. note:: + The contact / rest offsets live on :class:`~isaaclab_physx.sim.schemas.PhysxCollisionCfg` + as ``physxCollision:*`` fields; Newton reads them via its PhysX-bridge resolver, so they + are not duplicated here. + """ + + _usd_namespace: ClassVar[str | None] = "newton" + _usd_applied_schema: ClassVar[str | None] = "NewtonCollisionAPI" + + contact_margin: float | None = None + """Outward inflation of the collision surface [m]. + + Extends the effective collision surface outward. Sum of both bodies' margins is used for + collision detection. Essential for thin shells and cloth. Written to ``newton:contactMargin`` + via ``NewtonCollisionAPI``. Range: [0, inf). + """ + + contact_gap: float | None = None + """Additional contact detection gap [m]. + + AABBs are expanded by this value; contacts are detected earlier to avoid tunneling. Written to + ``newton:contactGap`` via ``NewtonCollisionAPI``. Set to ``-inf`` to use Newton's builder + default. Range: [0, inf). + """ + + @configclass class NewtonCollisionPropertiesCfg(CollisionBaseCfg): """Newton-specific collision properties. diff --git a/source/isaaclab_physx/changelog.d/vidurv-schema-frag-collision.minor.rst b/source/isaaclab_physx/changelog.d/vidurv-schema-frag-collision.minor.rst new file mode 100644 index 000000000000..f15e2c27b248 --- /dev/null +++ b/source/isaaclab_physx/changelog.d/vidurv-schema-frag-collision.minor.rst @@ -0,0 +1,7 @@ +Added +^^^^^ + +* Added :class:`~isaaclab_physx.sim.schemas.PhysxCollisionCfg`, the ``physxCollision:*`` + single-namespace collision fragment (PhysX ``PhysxCollisionAPI``). It carries + ``contact_offset`` / ``rest_offset`` plus the torsional patch-friction fields, and composes with + :class:`~isaaclab.sim.schemas.UsdPhysicsCollisionCfg` in a ``collision_props`` fragment list. diff --git a/source/isaaclab_physx/isaaclab_physx/sim/schemas/__init__.pyi b/source/isaaclab_physx/isaaclab_physx/sim/schemas/__init__.pyi index 10d2502ddf2b..767ba36f06d7 100644 --- a/source/isaaclab_physx/isaaclab_physx/sim/schemas/__init__.pyi +++ b/source/isaaclab_physx/isaaclab_physx/sim/schemas/__init__.pyi @@ -16,6 +16,7 @@ __all__ = [ "MeshCollisionPropertiesCfg", "OmniPhysicsDeformableBodyPropertiesCfg", "PhysxArticulationRootPropertiesCfg", + "PhysxCollisionCfg", "PhysxCollisionPropertiesCfg", "PhysxConvexDecompositionPropertiesCfg", "PhysxConvexHullPropertiesCfg", @@ -51,6 +52,7 @@ from .schemas_cfg import ( MeshCollisionPropertiesCfg, OmniPhysicsDeformableBodyPropertiesCfg, PhysxArticulationRootPropertiesCfg, + PhysxCollisionCfg, PhysxCollisionPropertiesCfg, PhysxConvexDecompositionPropertiesCfg, PhysxConvexHullPropertiesCfg, diff --git a/source/isaaclab_physx/isaaclab_physx/sim/schemas/schemas_cfg.py b/source/isaaclab_physx/isaaclab_physx/sim/schemas/schemas_cfg.py index 18a5de023f90..c81ea84f7101 100644 --- a/source/isaaclab_physx/isaaclab_physx/sim/schemas/schemas_cfg.py +++ b/source/isaaclab_physx/isaaclab_physx/sim/schemas/schemas_cfg.py @@ -11,6 +11,7 @@ from isaaclab.sim.schemas.schemas_cfg import ( ArticulationRootBaseCfg, CollisionBaseCfg, + CollisionFragment, DeformableBodyPropertiesBaseCfg, JointDriveBaseCfg, MeshCollisionBaseCfg, @@ -391,6 +392,58 @@ def __post_init__(self): super().__post_init__() +@configclass +class PhysxCollisionCfg(CollisionFragment): + """``physxCollision:*`` collision attributes from `PhysxCollisionAPI`_. + + A single-namespace fragment (see :class:`~isaaclab.sim.schemas.SchemaFragment`) for the + PhysX collision add-on schema. Applied alongside + :class:`~isaaclab.sim.schemas.UsdPhysicsCollisionCfg` via + :func:`~isaaclab.sim.schemas.apply_collision_properties`. + + The :attr:`contact_offset` / :attr:`rest_offset` knobs live here as plain + ``physxCollision:*`` fields. Newton's USD importer consumes the same attributes via its + PhysX-bridge resolver, so they are not duplicated on the Newton collision fragment. + + .. _PhysxCollisionAPI: https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/104.2/class_physx_schema_physx_collision_a_p_i.html + """ + + _usd_namespace: ClassVar[str | None] = "physxCollision" + _usd_applied_schema: ClassVar[str | None] = "PhysxCollisionAPI" + + contact_offset: float | None = None + """Contact offset for the collision shape [m]. + + The collision detector generates contact points as soon as two shapes get closer than the sum + of their contact offsets. This quantity should be non-negative, which means contact generation + can potentially start before the shapes actually penetrate. + + Writes ``physxCollision:contactOffset``. Newton's USD importer consumes the same attribute via + its PhysX-bridge resolver. + """ + + rest_offset: float | None = None + """Rest offset for the collision shape [m]. + + The rest offset quantifies how close a shape gets to others at rest. At rest, the distance + between two vertically stacked objects is the sum of their rest offsets. If a pair of shapes + have a positive rest offset, the shapes will be separated at rest by an air gap. + + Writes ``physxCollision:restOffset``. Newton's USD importer consumes the same attribute via its + PhysX-bridge resolver. + """ + + torsional_patch_radius: float | None = None + """Radius of the contact patch for applying torsional friction [m]. + + It is used to approximate rotational friction introduced by the compression of contacting + surfaces. If the radius is zero, no torsional friction is applied. + """ + + min_torsional_patch_radius: float | None = None + """Minimum radius of the contact patch for applying torsional friction [m].""" + + @configclass class PhysxCollisionPropertiesCfg(CollisionBaseCfg): """PhysX-specific rigid-body collision properties. From ff532dac7a64babef1dfd98ce3ee29e146fe9aa7 Mon Sep 17 00:00:00 2001 From: Vidur Vij Date: Fri, 5 Jun 2026 12:37:05 -0700 Subject: [PATCH 2/2] docs(schemas): mark transition shim if/else for removal post-migration --- source/isaaclab/isaaclab/sim/converters/mesh_converter.py | 4 ++-- .../isaaclab/isaaclab/sim/spawners/from_files/from_files.py | 4 ++-- source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py | 4 ++-- source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/source/isaaclab/isaaclab/sim/converters/mesh_converter.py b/source/isaaclab/isaaclab/sim/converters/mesh_converter.py index d44460e09161..28a8ca568ee8 100644 --- a/source/isaaclab/isaaclab/sim/converters/mesh_converter.py +++ b/source/isaaclab/isaaclab/sim/converters/mesh_converter.py @@ -126,7 +126,7 @@ def _convert_asset(self, cfg: MeshConverterCfg): # Apply collider properties to mesh if cfg.collision_props is not None: # -- Collider properties such as offset, scale, etc. - # transition routing: new fragment list -> apply_*; legacy single cfg -> define_* + # transition shim, remove later: new fragment list -> apply_*; legacy single cfg -> define_* coll_frags = ( cfg.collision_props if isinstance(cfg.collision_props, (list, tuple)) else [cfg.collision_props] ) @@ -192,7 +192,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): diff --git a/source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py b/source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py index 79fb79232674..6b998b5f61df 100644 --- a/source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py +++ b/source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py @@ -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) @@ -352,7 +352,7 @@ def _spawn_from_usd_file( schemas.modify_rigid_body_properties(prim_path, cfg.rigid_props) # modify collision properties if cfg.collision_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_* 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): schemas.apply_collision_properties(prim_path, coll_frags) diff --git a/source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py b/source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py index 925c0bbdae71..74e940201ce4 100644 --- a/source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py +++ b/source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py @@ -415,7 +415,7 @@ def _spawn_mesh_geom_from_mesh( mesh_collision_api = UsdPhysics.MeshCollisionAPI.Apply(mesh_prim) mesh_collision_api.GetApproximationAttr().Set(collision_approximation) # apply collision properties - # transition routing: new fragment list -> apply_*; legacy single cfg -> define_* + # transition shim, remove later: new fragment list -> apply_*; legacy single cfg -> define_* 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): schemas.apply_collision_properties(mesh_prim_path, coll_frags, stage=stage) @@ -449,7 +449,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) diff --git a/source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py b/source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py index 9ef2aab6bb39..6257c82b132f 100644 --- a/source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py +++ b/source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py @@ -294,7 +294,7 @@ def _spawn_geom_from_prim_type( create_prim(mesh_prim_path, prim_type, scale=scale, attributes=attributes, stage=stage) # apply collision properties if cfg.collision_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_* 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): schemas.apply_collision_properties(mesh_prim_path, coll_frags, stage=stage) @@ -327,7 +327,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)