From 46a6ebacaff5591b1fc2ac9c45d955479e93c5cd Mon Sep 17 00:00:00 2001 From: Vidur Vij Date: Thu, 4 Jun 2026 18:23:20 -0700 Subject: [PATCH 01/11] feat(schemas): add mesh-collision schema-fragment API Add the additive mesh-collision schema-fragment family, mirroring the rigid-body pilot. Core gains the MeshCollisionFragment marker, the UsdPhysicsMeshCollisionCfg fragment carrying the physics:approximation token, and the apply_mesh_collision_properties family writer (applies the MeshCollisionAPI anchor, resolves and validates the approximation token from whichever cooking fragment is present, then dispatches each fragment via its func). PhysX cooking fragments (PhysxConvexHullCfg, PhysxConvexDecompositionCfg, PhysxTriangleMeshCfg, PhysxTriangleMeshSimplificationCfg, PhysxSDFMeshCfg) and Newton cooking fragments (NewtonMeshCollisionCfg, NewtonSDFCollisionCfg) each own a single namespace and applied schema, dispatched through the generic apply_namespaced applier. Widen the MeshConverterCfg.mesh_collision_props slot to accept a fragment list and add the transition bridge in mesh_converter.py without breaking the legacy single-cfg path. Legacy cfgs and modify_mesh_collision_properties stay intact. --- ...vidurv-schema-frag-meshcollision.minor.rst | 20 ++ source/isaaclab/isaaclab/sim/__init__.pyi | 6 + .../isaaclab/sim/converters/mesh_converter.py | 18 +- .../sim/converters/mesh_converter_cfg.py | 16 +- .../isaaclab/sim/schemas/__init__.pyi | 6 + .../isaaclab/isaaclab/sim/schemas/schemas.py | 72 +++++ .../isaaclab/sim/schemas/schemas_cfg.py | 46 ++++ .../test/sim/test_mesh_collision_fragments.py | 247 ++++++++++++++++++ ...vidurv-schema-frag-meshcollision.minor.rst | 9 + .../isaaclab_newton/sim/schemas/__init__.pyi | 4 + .../sim/schemas/schemas_cfg.py | 105 ++++++++ ...vidurv-schema-frag-meshcollision.minor.rst | 11 + .../isaaclab_physx/sim/schemas/__init__.pyi | 10 + .../isaaclab_physx/sim/schemas/schemas_cfg.py | 168 ++++++++++++ 14 files changed, 735 insertions(+), 3 deletions(-) create mode 100644 source/isaaclab/changelog.d/vidurv-schema-frag-meshcollision.minor.rst create mode 100644 source/isaaclab/test/sim/test_mesh_collision_fragments.py create mode 100644 source/isaaclab_newton/changelog.d/vidurv-schema-frag-meshcollision.minor.rst create mode 100644 source/isaaclab_physx/changelog.d/vidurv-schema-frag-meshcollision.minor.rst diff --git a/source/isaaclab/changelog.d/vidurv-schema-frag-meshcollision.minor.rst b/source/isaaclab/changelog.d/vidurv-schema-frag-meshcollision.minor.rst new file mode 100644 index 000000000000..3532b8f68ffe --- /dev/null +++ b/source/isaaclab/changelog.d/vidurv-schema-frag-meshcollision.minor.rst @@ -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. diff --git a/source/isaaclab/isaaclab/sim/__init__.pyi b/source/isaaclab/isaaclab/sim/__init__.pyi index e0365d3e759c..6988381491d1 100644 --- a/source/isaaclab/isaaclab/sim/__init__.pyi +++ b/source/isaaclab/isaaclab/sim/__init__.pyi @@ -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", @@ -222,6 +225,7 @@ from .schemas import ( FixedTendonPropertiesCfg, JointDriveBaseCfg, MassPropertiesCfg, + MeshCollisionFragment, MeshCollisionPropertiesCfg, PhysxJointDrivePropertiesCfg, PhysxRigidBodyPropertiesCfg, @@ -232,8 +236,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, diff --git a/source/isaaclab/isaaclab/sim/converters/mesh_converter.py b/source/isaaclab/isaaclab/sim/converters/mesh_converter.py index f814e4fdb877..aa64e6671ae3 100644 --- a/source/isaaclab/isaaclab/sim/converters/mesh_converter.py +++ b/source/isaaclab/isaaclab/sim/converters/mesh_converter.py @@ -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 @@ -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 diff --git a/source/isaaclab/isaaclab/sim/converters/mesh_converter_cfg.py b/source/isaaclab/isaaclab/sim/converters/mesh_converter_cfg.py index 73ec37e777b6..057c9ecdc1eb 100644 --- a/source/isaaclab/isaaclab/sim/converters/mesh_converter_cfg.py +++ b/source/isaaclab/isaaclab/sim/converters/mesh_converter_cfg.py @@ -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. """ diff --git a/source/isaaclab/isaaclab/sim/schemas/__init__.pyi b/source/isaaclab/isaaclab/sim/schemas/__init__.pyi index af153a60fc63..ff630fc881e0 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_mesh_collision_properties", "apply_namespaced", "apply_rigid_body_properties", "define_actuator_properties", @@ -35,8 +36,10 @@ __all__ = [ "JointDriveBaseCfg", "MassPropertiesCfg", "MeshCollisionBaseCfg", + "MeshCollisionFragment", "RigidBodyFragment", "SchemaFragment", + "UsdPhysicsMeshCollisionCfg", "UsdPhysicsRigidBodyCfg", "MujocoJointDrivePropertiesCfg", "MujocoRigidBodyPropertiesCfg", @@ -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, @@ -86,9 +90,11 @@ from .schemas_cfg import ( JointDriveBaseCfg, MassPropertiesCfg, MeshCollisionBaseCfg, + MeshCollisionFragment, RigidBodyBaseCfg, RigidBodyFragment, SchemaFragment, + UsdPhysicsMeshCollisionCfg, UsdPhysicsRigidBodyCfg, ) diff --git a/source/isaaclab/isaaclab/sim/schemas/schemas.py b/source/isaaclab/isaaclab/sim/schemas/schemas.py index 6617543cad61..f09bf18efc17 100644 --- a/source/isaaclab/isaaclab/sim/schemas/schemas.py +++ b/source/isaaclab/isaaclab/sim/schemas/schemas.py @@ -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 ``:meshApproximationName`` attribute. + if f.name == "mesh_approximation_name": + continue value = getattr(cfg, f.name) if value is None: continue @@ -468,6 +475,71 @@ def apply_rigid_body_properties( return success +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 approximation token shared across the fragment list: the last fragment whose + # ``mesh_approximation_name`` is set to a non-"none" value wins; otherwise "none". + 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. diff --git a/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py b/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py index ac5d8b22c020..2831cadbc3e8 100644 --- a/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py +++ b/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py @@ -176,6 +176,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. diff --git a/source/isaaclab/test/sim/test_mesh_collision_fragments.py b/source/isaaclab/test/sim/test_mesh_collision_fragments.py new file mode 100644 index 000000000000..47b25d119f30 --- /dev/null +++ b/source/isaaclab/test/sim/test_mesh_collision_fragments.py @@ -0,0 +1,247 @@ +# 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/Mesh"): + UsdGeom.Xform.Define(stage, path) + return stage.GetPrimAtPath(path) + + +# ------------------------------------------------------------------------------------- +# Fragment metadata + marker hierarchy +# ------------------------------------------------------------------------------------- + + +def test_mesh_collision_fragment_metadata_defaults(): + from isaaclab.sim.schemas import MeshCollisionFragment, SchemaFragment, UsdPhysicsMeshCollisionCfg + + cfg = UsdPhysicsMeshCollisionCfg(mesh_approximation_name="convexHull") + assert isinstance(cfg, MeshCollisionFragment) and isinstance(cfg, SchemaFragment) + assert type(cfg)._usd_namespace == "physics" + assert type(cfg)._usd_applied_schema is None # anchor applies MeshCollisionAPI, not the fragment + assert cfg.func == "isaaclab.sim.schemas:apply_namespaced" + assert cfg.mesh_approximation_name == "convexHull" + + +# ------------------------------------------------------------------------------------- +# Core USD fragment: physics:approximation token via apply_mesh_collision_properties +# ------------------------------------------------------------------------------------- + + +def test_usd_mesh_collision_fragment_writes_approximation_token(): + from isaaclab.sim.schemas import UsdPhysicsMeshCollisionCfg, apply_mesh_collision_properties + + sim_utils.create_new_stage() + SimulationContext(SimulationCfg(dt=0.01)) + stage = sim_utils.get_current_stage() + _make_xform(stage, "/World/M0") + apply_mesh_collision_properties( + "/World/M0", [UsdPhysicsMeshCollisionCfg(mesh_approximation_name="boundingCube")], stage + ) + prim = stage.GetPrimAtPath("/World/M0") + assert bool(UsdPhysics.MeshCollisionAPI(prim)) + assert prim.GetAttribute("physics:approximation").Get() == "boundingCube" + + +# ------------------------------------------------------------------------------------- +# PhysX cooking fragments (isaaclab_physx): each writes its own physx*Collision namespace +# ------------------------------------------------------------------------------------- + + +def test_physx_convex_hull_fragment_writes_namespace(): + from isaaclab_physx.sim.schemas import PhysxConvexHullCfg + + 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/M1") + UsdPhysics.MeshCollisionAPI.Apply(prim) + apply_namespaced(PhysxConvexHullCfg(hull_vertex_limit=32, min_thickness=0.002), "/World/M1", stage) + assert prim.GetAttribute("physxConvexHullCollision:hullVertexLimit").Get() == 32 + assert abs(prim.GetAttribute("physxConvexHullCollision:minThickness").Get() - 0.002) < 1e-6 + # ``mesh_approximation_name`` must NOT be authored as a namespaced attr by the generic applier. + assert not prim.HasAttribute("physxConvexHullCollision:meshApproximationName") + + +def test_physx_convex_decomposition_fragment_writes_namespace(): + from isaaclab_physx.sim.schemas import PhysxConvexDecompositionCfg + + 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/M2") + UsdPhysics.MeshCollisionAPI.Apply(prim) + apply_namespaced(PhysxConvexDecompositionCfg(max_convex_hulls=8, shrink_wrap=True), "/World/M2", stage) + assert prim.GetAttribute("physxConvexDecompositionCollision:maxConvexHulls").Get() == 8 + assert prim.GetAttribute("physxConvexDecompositionCollision:shrinkWrap").Get() is True + + +def test_physx_triangle_mesh_fragment_writes_namespace(): + from isaaclab_physx.sim.schemas import PhysxTriangleMeshCfg + + 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/M3") + UsdPhysics.MeshCollisionAPI.Apply(prim) + apply_namespaced(PhysxTriangleMeshCfg(weld_tolerance=0.01), "/World/M3", stage) + assert abs(prim.GetAttribute("physxTriangleMeshCollision:weldTolerance").Get() - 0.01) < 1e-6 + + +def test_physx_triangle_mesh_simplification_fragment_writes_namespace(): + from isaaclab_physx.sim.schemas import PhysxTriangleMeshSimplificationCfg + + 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/M4") + UsdPhysics.MeshCollisionAPI.Apply(prim) + apply_namespaced(PhysxTriangleMeshSimplificationCfg(simplification_metric=0.7), "/World/M4", stage) + ns = "physxTriangleMeshSimplificationCollision" + assert abs(prim.GetAttribute(f"{ns}:simplificationMetric").Get() - 0.7) < 1e-6 + + +def test_physx_sdf_mesh_fragment_writes_namespace(): + from isaaclab_physx.sim.schemas import PhysxSDFMeshCfg + + 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/M5") + UsdPhysics.MeshCollisionAPI.Apply(prim) + apply_namespaced(PhysxSDFMeshCfg(sdf_resolution=128, sdf_margin=0.02), "/World/M5", stage) + assert prim.GetAttribute("physxSDFMeshCollision:sdfResolution").Get() == 128 + assert abs(prim.GetAttribute("physxSDFMeshCollision:sdfMargin").Get() - 0.02) < 1e-6 + + +# ------------------------------------------------------------------------------------- +# Newton cooking fragments (isaaclab_newton): newton namespace + applied schema +# ------------------------------------------------------------------------------------- + + +def test_newton_mesh_collision_fragment_writes_namespace(): + from isaaclab_newton.sim.schemas import NewtonMeshCollisionCfg + + 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/M6") + UsdPhysics.MeshCollisionAPI.Apply(prim) + apply_namespaced(NewtonMeshCollisionCfg(max_hull_vertices=24), "/World/M6", stage) + assert prim.GetAttribute("newton:maxHullVertices").Get() == 24 + assert "NewtonMeshCollisionAPI" in prim.GetAppliedSchemas() + + +def test_newton_sdf_collision_fragment_writes_namespace(): + from isaaclab_newton.sim.schemas import NewtonSDFCollisionCfg + + 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/M7") + UsdPhysics.MeshCollisionAPI.Apply(prim) + apply_namespaced(NewtonSDFCollisionCfg(sdf_max_resolution=64, hydroelastic_enabled=True), "/World/M7", stage) + assert prim.GetAttribute("newton:sdfMaxResolution").Get() == 64 + assert prim.GetAttribute("newton:hydroelasticEnabled").Get() is True + assert "NewtonSDFCollisionAPI" in prim.GetAppliedSchemas() + + +# ------------------------------------------------------------------------------------- +# Composition through apply_mesh_collision_properties: token coupling + multi-namespace +# ------------------------------------------------------------------------------------- + + +def test_apply_mesh_collision_properties_composes_namespaces(): + from isaaclab_newton.sim.schemas import NewtonMeshCollisionCfg + from isaaclab_physx.sim.schemas import PhysxConvexHullCfg + + from isaaclab.sim.schemas import UsdPhysicsMeshCollisionCfg, apply_mesh_collision_properties + + sim_utils.create_new_stage() + SimulationContext(SimulationCfg(dt=0.01)) + stage = sim_utils.get_current_stage() + _make_xform(stage, "/World/M8") + apply_mesh_collision_properties( + "/World/M8", + [ + UsdPhysicsMeshCollisionCfg(), + PhysxConvexHullCfg(hull_vertex_limit=48), + NewtonMeshCollisionCfg(max_hull_vertices=48), + ], + stage, + ) + prim = stage.GetPrimAtPath("/World/M8") + assert bool(UsdPhysics.MeshCollisionAPI(prim)) # implicit anchor applied + # token coupling: the convex-hull cooking fragment sets ``physics:approximation`` + assert prim.GetAttribute("physics:approximation").Get() == "convexHull" + assert prim.GetAttribute("physxConvexHullCollision:hullVertexLimit").Get() == 48 + assert prim.GetAttribute("newton:maxHullVertices").Get() == 48 + + +def test_apply_mesh_collision_properties_rejects_invalid_token(): + import pytest + + from isaaclab.sim.schemas import UsdPhysicsMeshCollisionCfg, apply_mesh_collision_properties + + sim_utils.create_new_stage() + SimulationContext(SimulationCfg(dt=0.01)) + stage = sim_utils.get_current_stage() + _make_xform(stage, "/World/M9") + with pytest.raises(ValueError): + apply_mesh_collision_properties( + "/World/M9", [UsdPhysicsMeshCollisionCfg(mesh_approximation_name="notAToken")], stage + ) + + +# ------------------------------------------------------------------------------------- +# Public imports +# ------------------------------------------------------------------------------------- + + +def test_public_imports(): + from isaaclab_newton.sim.schemas import NewtonMeshCollisionCfg, NewtonSDFCollisionCfg # noqa: F401 + from isaaclab_physx.sim.schemas import ( # noqa: F401 + PhysxConvexDecompositionCfg, + PhysxConvexHullCfg, + PhysxSDFMeshCfg, + PhysxTriangleMeshCfg, + PhysxTriangleMeshSimplificationCfg, + ) + + from isaaclab.sim.schemas import ( # noqa: F401 + MeshCollisionFragment, + SchemaFragment, + UsdPhysicsMeshCollisionCfg, + apply_mesh_collision_properties, + apply_namespaced, + ) diff --git a/source/isaaclab_newton/changelog.d/vidurv-schema-frag-meshcollision.minor.rst b/source/isaaclab_newton/changelog.d/vidurv-schema-frag-meshcollision.minor.rst new file mode 100644 index 000000000000..4246cfd9a1f5 --- /dev/null +++ b/source/isaaclab_newton/changelog.d/vidurv-schema-frag-meshcollision.minor.rst @@ -0,0 +1,9 @@ +Added +^^^^^ + +* Added the Newton mesh-collision cooking fragments: + :class:`~isaaclab_newton.sim.schemas.NewtonMeshCollisionCfg` (``newton:maxHullVertices`` via + ``NewtonMeshCollisionAPI``) and :class:`~isaaclab_newton.sim.schemas.NewtonSDFCollisionCfg` + (Newton SDF generation and hydroelastic-contact attributes via ``NewtonSDFCollisionAPI``). Each is + a single-namespace :class:`~isaaclab.sim.schemas.MeshCollisionFragment` dispatched via + :func:`~isaaclab.sim.schemas.apply_mesh_collision_properties`. diff --git a/source/isaaclab_newton/isaaclab_newton/sim/schemas/__init__.pyi b/source/isaaclab_newton/isaaclab_newton/sim/schemas/__init__.pyi index 89ca2d068afb..c82fcf052648 100644 --- a/source/isaaclab_newton/isaaclab_newton/sim/schemas/__init__.pyi +++ b/source/isaaclab_newton/isaaclab_newton/sim/schemas/__init__.pyi @@ -12,8 +12,10 @@ __all__ = [ "NewtonDeformableBodyPropertiesCfg", "NewtonJointDrivePropertiesCfg", "NewtonMaterialPropertiesCfg", + "NewtonMeshCollisionCfg", "NewtonMeshCollisionPropertiesCfg", "NewtonRigidBodyPropertiesCfg", + "NewtonSDFCollisionCfg", "NewtonSDFCollisionPropertiesCfg", ] @@ -26,7 +28,9 @@ from .schemas_cfg import ( NewtonDeformableBodyPropertiesCfg, NewtonJointDrivePropertiesCfg, NewtonMaterialPropertiesCfg, + NewtonMeshCollisionCfg, NewtonMeshCollisionPropertiesCfg, NewtonRigidBodyPropertiesCfg, + NewtonSDFCollisionCfg, NewtonSDFCollisionPropertiesCfg, ) 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..82f675558802 100644 --- a/source/isaaclab_newton/isaaclab_newton/sim/schemas/schemas_cfg.py +++ b/source/isaaclab_newton/isaaclab_newton/sim/schemas/schemas_cfg.py @@ -13,6 +13,7 @@ DeformableBodyPropertiesBaseCfg, JointDriveBaseCfg, MeshCollisionBaseCfg, + MeshCollisionFragment, RigidBodyBaseCfg, RigidBodyFragment, ) @@ -289,6 +290,110 @@ class NewtonSDFCollisionPropertiesCfg(NewtonCollisionPropertiesCfg): """ +# ------------------------------------------------------------------------------------- +# Mesh-collision cooking fragments (single-namespace; Newton cooking add-on schemas). +# +# Each fragment owns the ``newton`` namespace + its applied schema and is dispatched through the +# generic ``apply_namespaced`` applier by the family writer +# ``isaaclab.sim.schemas.apply_mesh_collision_properties``. They author no ``mesh_approximation_name`` +# of their own (the ``physics:approximation`` token is set by the PhysX/USD fragment present in the +# same list), so they only tune Newton-native cooking attributes. +# ------------------------------------------------------------------------------------- + + +@configclass +class NewtonMeshCollisionCfg(MeshCollisionFragment): + """``newton:maxHullVertices`` mesh-cooking attribute from ``NewtonMeshCollisionAPI``. + + A single-namespace fragment (see :class:`~isaaclab.sim.schemas.SchemaFragment`) carrying + Newton's convex-hull vertex limit. Dispatched alongside the USD/PhysX mesh-collision fragments + via :func:`~isaaclab.sim.schemas.apply_mesh_collision_properties`. + + .. note:: + If the values are None, they are not modified. + """ + + _usd_namespace: ClassVar[str | None] = "newton" + _usd_applied_schema: ClassVar[str | None] = "NewtonMeshCollisionAPI" + + max_hull_vertices: int | None = None + """Maximum vertices in the convex hull approximation [dimensionless]. + + Only relevant when ``physics:approximation = "convexHull"``. + Written to ``newton:maxHullVertices`` via ``NewtonMeshCollisionAPI``. + Set to ``-1`` to use as many vertices as needed for a perfect hull. + """ + + +@configclass +class NewtonSDFCollisionCfg(MeshCollisionFragment): + """``newton:*`` SDF and hydroelastic mesh-cooking attributes from ``NewtonSDFCollisionAPI``. + + A single-namespace fragment carrying Newton SDF generation and hydroelastic-contact attributes + consumed by Newton's USD importer. Mirrors the legacy + :class:`NewtonSDFCollisionPropertiesCfg`. Dispatched alongside the USD/PhysX mesh-collision + fragments via :func:`~isaaclab.sim.schemas.apply_mesh_collision_properties`. + + .. note:: + If the values are None, they are not modified. + """ + + _usd_namespace: ClassVar[str | None] = "newton" + _usd_applied_schema: ClassVar[str | None] = "NewtonSDFCollisionAPI" + + sdf_max_resolution: int | None = None + """Maximum SDF grid dimension [dimensionless]. + + Newton requires this value to be divisible by 8. If :attr:`sdf_target_voxel_size` is also + authored, Newton uses the target voxel size and ignores this resolution. + Written to ``newton:sdfMaxResolution`` via ``NewtonSDFCollisionAPI``. + """ + + sdf_narrow_band_inner: float | None = None + """Inner narrow-band distance for SDF generation [m]. + + Written to ``newton:sdfNarrowBandInner`` via ``NewtonSDFCollisionAPI``. + """ + + sdf_narrow_band_outer: float | None = None + """Outer narrow-band distance for SDF generation [m]. + + Written to ``newton:sdfNarrowBandOuter`` via ``NewtonSDFCollisionAPI``. + """ + + sdf_target_voxel_size: float | None = None + """Target SDF voxel size [m]. + + Takes precedence over :attr:`sdf_max_resolution` in Newton's USD importer. + Written to ``newton:sdfTargetVoxelSize`` via ``NewtonSDFCollisionAPI``. + """ + + sdf_texture_format: Literal["uint8", "uint16", "float32"] | None = None + """Subgrid texture storage format for generated SDFs. + + Written to ``newton:sdfTextureFormat`` via ``NewtonSDFCollisionAPI``. + """ + + sdf_padding: float | None = None + """SDF AABB padding [m]. + + Written to ``newton:sdfPadding`` via ``NewtonSDFCollisionAPI``. + """ + + hydroelastic_enabled: bool | None = None + """Whether Newton should use SDF-based hydroelastic contacts for this shape. + + Both participating collision shapes must enable hydroelastic contacts for Newton to use this + path. Written to ``newton:hydroelasticEnabled`` via ``NewtonSDFCollisionAPI``. + """ + + hydroelastic_stiffness: float | None = None + """Hydroelastic contact stiffness. + + Written to ``newton:hydroelasticStiffness`` via ``NewtonSDFCollisionAPI``. + """ + + @configclass class NewtonMaterialPropertiesCfg(RigidBodyMaterialBaseCfg): """Newton-specific rigid body material properties. diff --git a/source/isaaclab_physx/changelog.d/vidurv-schema-frag-meshcollision.minor.rst b/source/isaaclab_physx/changelog.d/vidurv-schema-frag-meshcollision.minor.rst new file mode 100644 index 000000000000..8ced9b2e5d2a --- /dev/null +++ b/source/isaaclab_physx/changelog.d/vidurv-schema-frag-meshcollision.minor.rst @@ -0,0 +1,11 @@ +Added +^^^^^ + +* Added the PhysX mesh-collision cooking fragments: + :class:`~isaaclab_physx.sim.schemas.PhysxConvexHullCfg`, + :class:`~isaaclab_physx.sim.schemas.PhysxConvexDecompositionCfg`, + :class:`~isaaclab_physx.sim.schemas.PhysxTriangleMeshCfg`, + :class:`~isaaclab_physx.sim.schemas.PhysxTriangleMeshSimplificationCfg`, and + :class:`~isaaclab_physx.sim.schemas.PhysxSDFMeshCfg`. Each is a single-namespace + :class:`~isaaclab.sim.schemas.MeshCollisionFragment` owning one ``physx*Collision:*`` namespace and + applied schema, dispatched via :func:`~isaaclab.sim.schemas.apply_mesh_collision_properties`. diff --git a/source/isaaclab_physx/isaaclab_physx/sim/schemas/__init__.pyi b/source/isaaclab_physx/isaaclab_physx/sim/schemas/__init__.pyi index 10d2502ddf2b..e2d139e919b2 100644 --- a/source/isaaclab_physx/isaaclab_physx/sim/schemas/__init__.pyi +++ b/source/isaaclab_physx/isaaclab_physx/sim/schemas/__init__.pyi @@ -17,7 +17,9 @@ __all__ = [ "OmniPhysicsDeformableBodyPropertiesCfg", "PhysxArticulationRootPropertiesCfg", "PhysxCollisionPropertiesCfg", + "PhysxConvexDecompositionCfg", "PhysxConvexDecompositionPropertiesCfg", + "PhysxConvexHullCfg", "PhysxConvexHullPropertiesCfg", "PhysxDeformableBodyPropertiesCfg", "PhysxDeformableCollisionPropertiesCfg", @@ -25,9 +27,12 @@ __all__ = [ "PhysxJointDrivePropertiesCfg", "PhysxRigidBodyCfg", "PhysxRigidBodyPropertiesCfg", + "PhysxSDFMeshCfg", "PhysxSDFMeshPropertiesCfg", "PhysxSpatialTendonPropertiesCfg", + "PhysxTriangleMeshCfg", "PhysxTriangleMeshPropertiesCfg", + "PhysxTriangleMeshSimplificationCfg", "PhysxTriangleMeshSimplificationPropertiesCfg", "RigidBodyPropertiesCfg", "SDFMeshPropertiesCfg", @@ -52,7 +57,9 @@ from .schemas_cfg import ( OmniPhysicsDeformableBodyPropertiesCfg, PhysxArticulationRootPropertiesCfg, PhysxCollisionPropertiesCfg, + PhysxConvexDecompositionCfg, PhysxConvexDecompositionPropertiesCfg, + PhysxConvexHullCfg, PhysxConvexHullPropertiesCfg, PhysxDeformableBodyPropertiesCfg, PhysxDeformableCollisionPropertiesCfg, @@ -60,9 +67,12 @@ from .schemas_cfg import ( PhysxJointDrivePropertiesCfg, PhysxRigidBodyCfg, PhysxRigidBodyPropertiesCfg, + PhysxSDFMeshCfg, PhysxSDFMeshPropertiesCfg, PhysxSpatialTendonPropertiesCfg, + PhysxTriangleMeshCfg, PhysxTriangleMeshPropertiesCfg, + PhysxTriangleMeshSimplificationCfg, PhysxTriangleMeshSimplificationPropertiesCfg, RigidBodyPropertiesCfg, SDFMeshPropertiesCfg, 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..b07b4a67a0dd 100644 --- a/source/isaaclab_physx/isaaclab_physx/sim/schemas/schemas_cfg.py +++ b/source/isaaclab_physx/isaaclab_physx/sim/schemas/schemas_cfg.py @@ -14,6 +14,7 @@ DeformableBodyPropertiesBaseCfg, JointDriveBaseCfg, MeshCollisionBaseCfg, + MeshCollisionFragment, RigidBodyBaseCfg, RigidBodyFragment, ) @@ -535,6 +536,173 @@ def __post_init__(self): super().__post_init__() +# ------------------------------------------------------------------------------------- +# Mesh-collision cooking fragments (single-namespace; PhysX cooking add-on schemas). +# +# Each fragment owns one ``physx*Collision:*`` namespace + applied schema, and carries a +# ``mesh_approximation_name`` whose default encodes the ``physics:approximation`` token its +# cooking schema implies. The token is written by the family writer +# ``isaaclab.sim.schemas.apply_mesh_collision_properties`` (which scans the fragment list and +# validates the name against ``MESH_APPROXIMATION_TOKENS``); the tuning attributes are written by +# the generic ``apply_namespaced`` applier. +# ------------------------------------------------------------------------------------- + + +@configclass +class PhysxConvexHullCfg(MeshCollisionFragment): + """``physxConvexHullCollision:*`` mesh-cooking attributes from `PhysxConvexHullCollisionAPI`_. + + A single-namespace fragment (see :class:`~isaaclab.sim.schemas.SchemaFragment`) for the PhysX + convex-hull cooking schema. The ``convexHull`` token is written to ``physics:approximation`` by + :func:`~isaaclab.sim.schemas.apply_mesh_collision_properties`. + + .. _PhysxConvexHullCollisionAPI: https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/latest/class_physx_schema_physx_convex_hull_collision_a_p_i.html + """ + + _usd_namespace: ClassVar[str | None] = "physxConvexHullCollision" + _usd_applied_schema: ClassVar[str | None] = "PhysxConvexHullCollisionAPI" + + mesh_approximation_name: str = "convexHull" + """Name of mesh collision approximation method. Default: "convexHull".""" + + hull_vertex_limit: int | None = None + """Convex hull vertex limit used for convex hull cooking [dimensionless]. Defaults to 64.""" + + min_thickness: float | None = None + """Convex hull min thickness [m]. Range: [0, inf). Default value is 0.001.""" + + +@configclass +class PhysxConvexDecompositionCfg(MeshCollisionFragment): + """``physxConvexDecompositionCollision:*`` mesh-cooking attributes from `PhysxConvexDecompositionCollisionAPI`_. + + A single-namespace fragment for the PhysX convex-decomposition cooking schema. The + ``convexDecomposition`` token is written to ``physics:approximation`` by + :func:`~isaaclab.sim.schemas.apply_mesh_collision_properties`. + + .. _PhysxConvexDecompositionCollisionAPI: https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/latest/class_physx_schema_physx_convex_decomposition_collision_a_p_i.html + """ + + _usd_namespace: ClassVar[str | None] = "physxConvexDecompositionCollision" + _usd_applied_schema: ClassVar[str | None] = "PhysxConvexDecompositionCollisionAPI" + + mesh_approximation_name: str = "convexDecomposition" + """Name of mesh collision approximation method. Default: "convexDecomposition".""" + + hull_vertex_limit: int | None = None + """Convex hull vertex limit used for convex hull cooking [dimensionless]. Defaults to 64.""" + + max_convex_hulls: int | None = None + """Maximum of convex hulls created during convex decomposition [dimensionless]. Default value is 32.""" + + min_thickness: float | None = None + """Convex hull min thickness [m]. Range: [0, inf). Default value is 0.001.""" + + voxel_resolution: int | None = None + """Voxel resolution used for convex decomposition [dimensionless]. Defaults to 500,000 voxels.""" + + error_percentage: float | None = None + """Convex decomposition error percentage parameter [%]. Defaults to 10 percent.""" + + shrink_wrap: bool | None = None + """Attempts to adjust the convex hull points so that they are projected onto the surface of the + original graphics mesh. Defaults to False. + """ + + +@configclass +class PhysxTriangleMeshCfg(MeshCollisionFragment): + """``physxTriangleMeshCollision:*`` mesh-cooking attributes from `PhysxTriangleMeshCollisionAPI`_. + + A single-namespace fragment for the PhysX triangle-mesh cooking schema (PhysX-only colliders). + + .. _PhysxTriangleMeshCollisionAPI: https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/latest/class_physx_schema_physx_triangle_mesh_collision_a_p_i.html + """ + + _usd_namespace: ClassVar[str | None] = "physxTriangleMeshCollision" + _usd_applied_schema: ClassVar[str | None] = "PhysxTriangleMeshCollisionAPI" + + mesh_approximation_name: str = "none" + """Name of mesh collision approximation method. Default: "none" (uses triangle mesh).""" + + weld_tolerance: float | None = None + """Mesh weld tolerance controlling the distance at which vertices are welded [m]. + + Default ``-inf`` autocomputes the welding tolerance from the mesh size; ``0`` disables welding. + Range: [0, inf). + """ + + +@configclass +class PhysxTriangleMeshSimplificationCfg(MeshCollisionFragment): + """``physxTriangleMeshSimplificationCollision:*`` attributes from `PhysxTriangleMeshSimplificationCollisionAPI`_. + + A single-namespace fragment for the PhysX triangle-mesh-simplification cooking schema. The + ``meshSimplification`` token is written to ``physics:approximation`` by + :func:`~isaaclab.sim.schemas.apply_mesh_collision_properties`. + + .. _PhysxTriangleMeshSimplificationCollisionAPI: https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/latest/class_physx_schema_physx_triangle_mesh_simplification_collision_a_p_i.html + """ + + _usd_namespace: ClassVar[str | None] = "physxTriangleMeshSimplificationCollision" + _usd_applied_schema: ClassVar[str | None] = "PhysxTriangleMeshSimplificationCollisionAPI" + + mesh_approximation_name: str = "meshSimplification" + """Name of mesh collision approximation method. Default: "meshSimplification".""" + + simplification_metric: float | None = None + """Mesh simplification accuracy [dimensionless]. Defaults to 0.55.""" + + weld_tolerance: float | None = None + """Mesh weld tolerance controlling the distance at which vertices are welded [m]. + + Default ``-inf`` autocomputes the welding tolerance from the mesh size; ``0`` disables welding. + Range: [0, inf). + """ + + +@configclass +class PhysxSDFMeshCfg(MeshCollisionFragment): + """``physxSDFMeshCollision:*`` mesh-cooking attributes from `PhysxSDFMeshCollisionAPI`_. + + A single-namespace fragment for the PhysX signed-distance-field cooking schema (PhysX-only + colliders). The ``sdf`` token is written to ``physics:approximation`` by + :func:`~isaaclab.sim.schemas.apply_mesh_collision_properties`. + + .. _PhysxSDFMeshCollisionAPI: https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/latest/class_physx_schema_physx_s_d_f_mesh_collision_a_p_i.html + """ + + _usd_namespace: ClassVar[str | None] = "physxSDFMeshCollision" + _usd_applied_schema: ClassVar[str | None] = "PhysxSDFMeshCollisionAPI" + + mesh_approximation_name: str = "sdf" + """Name of mesh collision approximation method. Default: "sdf".""" + + sdf_margin: float | None = None + """Margin to increase the size of the SDF relative to the mesh bounding-box diagonal [dimensionless]. + + Scale-independent (fraction of the bounding-box diagonal). Default value is 0.01. Range: [0, inf). + """ + + sdf_narrow_band_thickness: float | None = None + """Size of the narrow band around the mesh surface with high-resolution SDF samples [dimensionless]. + + Scale-independent (fraction of the bounding-box diagonal). Default value is 0.01. Range: [0, 1]. + """ + + sdf_resolution: int | None = None + """Uniform SDF sampling resolution (largest AABB extent divided by this value) [dimensionless]. + + Default value is 256. Range: (1, inf). + """ + + sdf_subgrid_resolution: int | None = None + """Subgrid resolution enabling SDF sparsity; ``0`` selects a dense SDF [dimensionless]. + + Default value is 6. Range: [0, inf). + """ + + @configclass class PhysxConvexHullPropertiesCfg(MeshCollisionBaseCfg): """PhysX convex-hull cooking properties for a mesh collider. From ffdde563dded88d888a8ce9fc22dc0c099827891 Mon Sep 17 00:00:00 2001 From: Vidur Vij Date: Thu, 4 Jun 2026 20:14:08 -0700 Subject: [PATCH 02/11] fix(schemas): NewtonSDFCollisionCfg applies no schema (NewtonSDFCollisionAPI unregistered) --- source/isaaclab/test/sim/test_mesh_collision_fragments.py | 3 ++- .../isaaclab_newton/sim/schemas/schemas_cfg.py | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/source/isaaclab/test/sim/test_mesh_collision_fragments.py b/source/isaaclab/test/sim/test_mesh_collision_fragments.py index 47b25d119f30..3586dfd1ee3b 100644 --- a/source/isaaclab/test/sim/test_mesh_collision_fragments.py +++ b/source/isaaclab/test/sim/test_mesh_collision_fragments.py @@ -173,7 +173,8 @@ def test_newton_sdf_collision_fragment_writes_namespace(): apply_namespaced(NewtonSDFCollisionCfg(sdf_max_resolution=64, hydroelastic_enabled=True), "/World/M7", stage) assert prim.GetAttribute("newton:sdfMaxResolution").Get() == 64 assert prim.GetAttribute("newton:hydroelasticEnabled").Get() is True - assert "NewtonSDFCollisionAPI" in prim.GetAppliedSchemas() + # ``NewtonSDFCollisionAPI`` is not a registered applied API schema in the current Newton + # build, so the fragment writes the ``newton:*`` attributes without applying a schema. # ------------------------------------------------------------------------------------- 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 82f675558802..dad7fd6652cd 100644 --- a/source/isaaclab_newton/isaaclab_newton/sim/schemas/schemas_cfg.py +++ b/source/isaaclab_newton/isaaclab_newton/sim/schemas/schemas_cfg.py @@ -339,7 +339,11 @@ class NewtonSDFCollisionCfg(MeshCollisionFragment): """ _usd_namespace: ClassVar[str | None] = "newton" - _usd_applied_schema: ClassVar[str | None] = "NewtonSDFCollisionAPI" + # ``NewtonSDFCollisionAPI`` is not a registered applied API schema in the current Newton + # build (verified via ``Usd.SchemaRegistry``), so no schema is applied; the ``newton:*`` + # attributes are authored directly and consumed by Newton's USD importer. Switch this to + # ``"NewtonSDFCollisionAPI"`` once that schema ships. + _usd_applied_schema: ClassVar[str | None] = None sdf_max_resolution: int | None = None """Maximum SDF grid dimension [dimensionless]. From 76ecbd558ede9ddc616327d9749f347b033ed6c1 Mon Sep 17 00:00:00 2001 From: Vidur Vij Date: Thu, 4 Jun 2026 20:32:54 -0700 Subject: [PATCH 03/11] fix(schemas): keep NewtonSDFCollisionAPI authored; assert via apiSchemas listOp --- .../test/sim/test_mesh_collision_fragments.py | 23 +++++++++++++++++-- .../sim/schemas/schemas_cfg.py | 10 ++++---- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/source/isaaclab/test/sim/test_mesh_collision_fragments.py b/source/isaaclab/test/sim/test_mesh_collision_fragments.py index 3586dfd1ee3b..2359c8d28143 100644 --- a/source/isaaclab/test/sim/test_mesh_collision_fragments.py +++ b/source/isaaclab/test/sim/test_mesh_collision_fragments.py @@ -23,6 +23,23 @@ def _make_xform(stage, path="/World/Mesh"): return stage.GetPrimAtPath(path) +def _has_authored_api_schema(prim, schema_name: str) -> bool: + """Return whether a schema name is applied or authored in ``apiSchemas`` metadata. + + A schema that is authored via ``AddAppliedSchema`` but not registered in the current build + appears in the ``apiSchemas`` listOp yet not in the composed ``GetAppliedSchemas()``. + """ + if schema_name in prim.GetAppliedSchemas(): + return True + api_schemas = prim.GetMetadata("apiSchemas") + if api_schemas is None: + return False + return any( + schema_name in getattr(api_schemas, item_list) + for item_list in ("explicitItems", "prependedItems", "appendedItems", "addedItems") + ) + + # ------------------------------------------------------------------------------------- # Fragment metadata + marker hierarchy # ------------------------------------------------------------------------------------- @@ -173,8 +190,10 @@ def test_newton_sdf_collision_fragment_writes_namespace(): apply_namespaced(NewtonSDFCollisionCfg(sdf_max_resolution=64, hydroelastic_enabled=True), "/World/M7", stage) assert prim.GetAttribute("newton:sdfMaxResolution").Get() == 64 assert prim.GetAttribute("newton:hydroelasticEnabled").Get() is True - # ``NewtonSDFCollisionAPI`` is not a registered applied API schema in the current Newton - # build, so the fragment writes the ``newton:*`` attributes without applying a schema. + # ``NewtonSDFCollisionAPI`` is authored into the ``apiSchemas`` listOp (like the legacy cfg) but + # is not a registered schema in this Newton build, so it is absent from the composed + # ``GetAppliedSchemas()``. Assert the authored token, matching the legacy Newton test. + assert _has_authored_api_schema(prim, "NewtonSDFCollisionAPI") # ------------------------------------------------------------------------------------- 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 dad7fd6652cd..19339b58997e 100644 --- a/source/isaaclab_newton/isaaclab_newton/sim/schemas/schemas_cfg.py +++ b/source/isaaclab_newton/isaaclab_newton/sim/schemas/schemas_cfg.py @@ -339,11 +339,11 @@ class NewtonSDFCollisionCfg(MeshCollisionFragment): """ _usd_namespace: ClassVar[str | None] = "newton" - # ``NewtonSDFCollisionAPI`` is not a registered applied API schema in the current Newton - # build (verified via ``Usd.SchemaRegistry``), so no schema is applied; the ``newton:*`` - # attributes are authored directly and consumed by Newton's USD importer. Switch this to - # ``"NewtonSDFCollisionAPI"`` once that schema ships. - _usd_applied_schema: ClassVar[str | None] = None + # ``NewtonSDFCollisionAPI`` is authored into the prim's ``apiSchemas`` listOp (matching the + # legacy ``NewtonSDFCollisionPropertiesCfg``). It is not a *registered* applied API schema in + # the current Newton build, so it does not appear in the composed ``GetAppliedSchemas()`` until + # the schema ships -- but it is authored, and Newton's importer reads the ``newton:*`` attrs. + _usd_applied_schema: ClassVar[str | None] = "NewtonSDFCollisionAPI" sdf_max_resolution: int | None = None """Maximum SDF grid dimension [dimensionless]. From 1d637476a3e6d671926805266b98ccedb55b3478 Mon Sep 17 00:00:00 2001 From: Vidur Vij Date: Mon, 8 Jun 2026 20:08:38 -0700 Subject: [PATCH 04/11] style(schemas): trim verbose comments in mesh-collision fragment code No behavior change; collapse over-explained inline comments to terse intent. --- source/isaaclab/isaaclab/sim/schemas/schemas.py | 3 +-- source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py | 5 +---- .../isaaclab_newton/sim/schemas/schemas_cfg.py | 8 +++----- .../isaaclab_physx/sim/schemas/schemas_cfg.py | 10 ++++------ 4 files changed, 9 insertions(+), 17 deletions(-) diff --git a/source/isaaclab/isaaclab/sim/schemas/schemas.py b/source/isaaclab/isaaclab/sim/schemas/schemas.py index f09bf18efc17..027ec51483aa 100644 --- a/source/isaaclab/isaaclab/sim/schemas/schemas.py +++ b/source/isaaclab/isaaclab/sim/schemas/schemas.py @@ -516,8 +516,7 @@ def apply_mesh_collision_properties(prim_path: str, fragments, stage: Usd.Stage if not UsdPhysics.MeshCollisionAPI(prim): UsdPhysics.MeshCollisionAPI.Apply(prim) - # resolve the approximation token shared across the fragment list: the last fragment whose - # ``mesh_approximation_name`` is set to a non-"none" value wins; otherwise "none". + # 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) diff --git a/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py b/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py index 2831cadbc3e8..167135484318 100644 --- a/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py +++ b/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py @@ -584,10 +584,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 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 19339b58997e..0ac6f9d582a3 100644 --- a/source/isaaclab_newton/isaaclab_newton/sim/schemas/schemas_cfg.py +++ b/source/isaaclab_newton/isaaclab_newton/sim/schemas/schemas_cfg.py @@ -293,11 +293,9 @@ class NewtonSDFCollisionPropertiesCfg(NewtonCollisionPropertiesCfg): # ------------------------------------------------------------------------------------- # Mesh-collision cooking fragments (single-namespace; Newton cooking add-on schemas). # -# Each fragment owns the ``newton`` namespace + its applied schema and is dispatched through the -# generic ``apply_namespaced`` applier by the family writer -# ``isaaclab.sim.schemas.apply_mesh_collision_properties``. They author no ``mesh_approximation_name`` -# of their own (the ``physics:approximation`` token is set by the PhysX/USD fragment present in the -# same list), so they only tune Newton-native cooking attributes. +# Each fragment owns the ``newton`` namespace + its applied schema, dispatched via ``apply_namespaced``. +# They author no ``mesh_approximation_name`` (the token is set by the PhysX/USD fragment in the same +# list), so they only tune Newton-native cooking attributes. # ------------------------------------------------------------------------------------- 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 b07b4a67a0dd..a4303c0c715f 100644 --- a/source/isaaclab_physx/isaaclab_physx/sim/schemas/schemas_cfg.py +++ b/source/isaaclab_physx/isaaclab_physx/sim/schemas/schemas_cfg.py @@ -539,12 +539,10 @@ def __post_init__(self): # ------------------------------------------------------------------------------------- # Mesh-collision cooking fragments (single-namespace; PhysX cooking add-on schemas). # -# Each fragment owns one ``physx*Collision:*`` namespace + applied schema, and carries a -# ``mesh_approximation_name`` whose default encodes the ``physics:approximation`` token its -# cooking schema implies. The token is written by the family writer -# ``isaaclab.sim.schemas.apply_mesh_collision_properties`` (which scans the fragment list and -# validates the name against ``MESH_APPROXIMATION_TOKENS``); the tuning attributes are written by -# the generic ``apply_namespaced`` applier. +# Each fragment owns one ``physx*Collision:*`` namespace + applied schema; its +# ``mesh_approximation_name`` default encodes the ``physics:approximation`` token its cooking +# schema implies. The token is written by the family writer +# ``isaaclab.sim.schemas.apply_mesh_collision_properties``; tuning attrs go via ``apply_namespaced``. # ------------------------------------------------------------------------------------- From afd0729d919a1831372fc598b7d5764a9aed8e64 Mon Sep 17 00:00:00 2001 From: Vidur Vij Date: Thu, 25 Jun 2026 00:45:03 -0700 Subject: [PATCH 05/11] Use directly-imported SchemaFragment in mesh converter rigid shim The rigid transition shim referenced schemas.SchemaFragment, but the schemas module does not define SchemaFragment (it lives in schemas_cfg). SchemaFragment is already imported directly in this file; use it, matching the mesh-collision shim and fixing the AttributeError at convert time. --- source/isaaclab/isaaclab/sim/converters/mesh_converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/isaaclab/isaaclab/sim/converters/mesh_converter.py b/source/isaaclab/isaaclab/sim/converters/mesh_converter.py index aa64e6671ae3..51d6851d38bc 100644 --- a/source/isaaclab/isaaclab/sim/converters/mesh_converter.py +++ b/source/isaaclab/isaaclab/sim/converters/mesh_converter.py @@ -202,7 +202,7 @@ def _convert_asset(self, cfg: MeshConverterCfg): # 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) From 24c9e0e50fdc59e6435aa08a6984f39395b465a2 Mon Sep 17 00:00:00 2001 From: Vidur Vij Date: Thu, 25 Jun 2026 09:25:23 -0700 Subject: [PATCH 06/11] Use bare SchemaFragment in merged mass shim in mesh converter The develop merge brought in the mass shim using schemas.SchemaFragment (package form), but this branch imports the schemas module (which lacks SchemaFragment) and references the directly-imported SchemaFragment. Make the mass-shim checks use the bare SchemaFragment too, matching the mesh-collision and rigid shims and fixing the AttributeError. --- source/isaaclab/isaaclab/sim/converters/mesh_converter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/isaaclab/isaaclab/sim/converters/mesh_converter.py b/source/isaaclab/isaaclab/sim/converters/mesh_converter.py index 6013471a6f2d..71534795f3ba 100644 --- a/source/isaaclab/isaaclab/sim/converters/mesh_converter.py +++ b/source/isaaclab/isaaclab/sim/converters/mesh_converter.py @@ -199,8 +199,8 @@ 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) From 0cfd28bc6b2503bcc46845aaef201aa485f208cd Mon Sep 17 00:00:00 2001 From: Vidur Vij Date: Thu, 25 Jun 2026 13:47:58 -0700 Subject: [PATCH 07/11] Harden mesh-collision writer prim guard and result aggregation Bring apply_mesh_collision_properties in line with the sibling apply_* writers (already done for rigid body and mass): add the prim-validity ValueError guard, aggregate per-fragment results instead of unconditionally returning True, and annotate the fragments param as Iterable[MeshCollisionFragment]. Add invalid-prim and aggregation tests. --- .../isaaclab/isaaclab/sim/schemas/schemas.py | 21 ++++++++----- .../test/sim/test_mesh_collision_fragments.py | 31 +++++++++++++++++++ 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/source/isaaclab/isaaclab/sim/schemas/schemas.py b/source/isaaclab/isaaclab/sim/schemas/schemas.py index a90a8ead400d..f57d25848b6c 100644 --- a/source/isaaclab/isaaclab/sim/schemas/schemas.py +++ b/source/isaaclab/isaaclab/sim/schemas/schemas.py @@ -475,7 +475,9 @@ def apply_rigid_body_properties( return success -def apply_mesh_collision_properties(prim_path: str, fragments, stage: Usd.Stage | None = None) -> bool: +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 @@ -503,15 +505,18 @@ def apply_mesh_collision_properties(prim_path: str, fragments, stage: Usd.Stage stage is used. Returns: - True if the properties were successfully set. + True if all fragments applied successfully, False if any fragment reported failure. Raises: - ValueError: When the resolved mesh approximation name is not in - :const:`MESH_APPROXIMATION_TOKENS`. + ValueError: If the prim at ``prim_path`` is not valid, or 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) + # 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) @@ -532,11 +537,13 @@ def apply_mesh_collision_properties(prim_path: str, fragments, stage: Usd.Stage UsdPhysics.MeshCollisionAPI(prim), "Approximation", approximation_token, camel_case=False ) - # dispatch each fragment via its ``func`` (cooking-schema application + namespaced tuning attrs) + # dispatch each fragment via its ``func`` (cooking-schema application + namespaced tuning attrs), + # aggregating per-fragment results so a reported failure is not masked by the always-applied anchor + success = True for cfg in fragments: func = cfg.func if callable(cfg.func) else string_to_callable(cfg.func) - func(cfg, prim_path, stage) - return True + 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): diff --git a/source/isaaclab/test/sim/test_mesh_collision_fragments.py b/source/isaaclab/test/sim/test_mesh_collision_fragments.py index 2359c8d28143..ad6671ac2086 100644 --- a/source/isaaclab/test/sim/test_mesh_collision_fragments.py +++ b/source/isaaclab/test/sim/test_mesh_collision_fragments.py @@ -243,6 +243,37 @@ def test_apply_mesh_collision_properties_rejects_invalid_token(): ) +def test_apply_mesh_collision_properties_raises_on_invalid_prim(): + import pytest + + from isaaclab.sim.schemas import UsdPhysicsMeshCollisionCfg, apply_mesh_collision_properties + + sim_utils.create_new_stage() + SimulationContext(SimulationCfg(dt=0.01)) + stage = sim_utils.get_current_stage() + with pytest.raises(ValueError): + apply_mesh_collision_properties("/World/DoesNotExist", [UsdPhysicsMeshCollisionCfg()], stage) + + +def test_apply_mesh_collision_properties_aggregates_fragment_results(): + from isaaclab.sim.schemas import UsdPhysicsMeshCollisionCfg, apply_mesh_collision_properties + + sim_utils.create_new_stage() + SimulationContext(SimulationCfg(dt=0.01)) + stage = sim_utils.get_current_stage() + _make_xform(stage, "/World/Magg") + + # a fragment whose applier reports failure must make the aggregate return False + failing = UsdPhysicsMeshCollisionCfg() + failing.func = lambda cfg, prim_path, stage=None: False + assert apply_mesh_collision_properties("/World/Magg", [failing], stage) is False + + # all-succeeding fragments return True + ok = UsdPhysicsMeshCollisionCfg() + ok.func = lambda cfg, prim_path, stage=None: True + assert apply_mesh_collision_properties("/World/Magg", [ok], stage) is True + + # ------------------------------------------------------------------------------------- # Public imports # ------------------------------------------------------------------------------------- From 277fdab6f1366ac48c29389b738e668dea232764 Mon Sep 17 00:00:00 2001 From: Vidur Vij Date: Thu, 25 Jun 2026 14:17:09 -0700 Subject: [PATCH 08/11] Note Newton 1.3.0 requirement on NewtonSDFCollisionCfg The newton:sdf*/hydroelastic* attributes (and detection of the unregistered NewtonSDFCollisionAPI token via the raw apiSchemas list-op) are consumed by Newton's USD importer starting in Newton 1.3.0; on older builds they are authored but inert. Document this so the attributes' effect is not a surprise. --- .../isaaclab_newton/sim/schemas/schemas_cfg.py | 6 ++++++ 1 file changed, 6 insertions(+) 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 0ac6f9d582a3..1d4225075126 100644 --- a/source/isaaclab_newton/isaaclab_newton/sim/schemas/schemas_cfg.py +++ b/source/isaaclab_newton/isaaclab_newton/sim/schemas/schemas_cfg.py @@ -332,6 +332,12 @@ class NewtonSDFCollisionCfg(MeshCollisionFragment): :class:`NewtonSDFCollisionPropertiesCfg`. Dispatched alongside the USD/PhysX mesh-collision fragments via :func:`~isaaclab.sim.schemas.apply_mesh_collision_properties`. + .. note:: + These ``newton:sdf*`` / ``newton:hydroelastic*`` attributes are read by Newton's USD + importer starting in Newton 1.3.0 (which also detects the unregistered + ``NewtonSDFCollisionAPI`` token via the raw ``apiSchemas`` list-op). On older Newton builds + they are authored but inert. + .. note:: If the values are None, they are not modified. """ From 9037c0b912d0e8e52068a1a420645c2fc5327cc9 Mon Sep 17 00:00:00 2001 From: Vidur Vij Date: Thu, 25 Jun 2026 14:42:15 -0700 Subject: [PATCH 09/11] Materialize fragments in mesh-collision writer for one-shot iterables apply_mesh_collision_properties iterates fragments twice (approximation-token resolution, then dispatch), so a generator/map/filter passed as the Iterable was exhausted by the first pass -- the dispatch loop wrote nothing yet still returned True (silent no-op). Materialize to a list once at the top. Add a generator regression test. --- .../isaaclab/isaaclab/sim/schemas/schemas.py | 3 +++ .../test/sim/test_mesh_collision_fragments.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/source/isaaclab/isaaclab/sim/schemas/schemas.py b/source/isaaclab/isaaclab/sim/schemas/schemas.py index f57d25848b6c..cfe79a92a2fb 100644 --- a/source/isaaclab/isaaclab/sim/schemas/schemas.py +++ b/source/isaaclab/isaaclab/sim/schemas/schemas.py @@ -513,6 +513,9 @@ def apply_mesh_collision_properties( """ if stage is None: stage = get_current_stage() + # materialize: this writer iterates ``fragments`` twice (approximation-token resolution, then + # dispatch), so a one-shot iterable (generator) would be exhausted by the first pass + fragments = list(fragments) prim = stage.GetPrimAtPath(prim_path) # fail loudly on an invalid path (matches the sibling apply_* writers) if not prim.IsValid(): diff --git a/source/isaaclab/test/sim/test_mesh_collision_fragments.py b/source/isaaclab/test/sim/test_mesh_collision_fragments.py index ad6671ac2086..d6b59851efc3 100644 --- a/source/isaaclab/test/sim/test_mesh_collision_fragments.py +++ b/source/isaaclab/test/sim/test_mesh_collision_fragments.py @@ -274,6 +274,25 @@ def test_apply_mesh_collision_properties_aggregates_fragment_results(): assert apply_mesh_collision_properties("/World/Magg", [ok], stage) is True +def test_apply_mesh_collision_properties_accepts_generator(): + # the writer iterates fragments twice (token resolution + dispatch); a one-shot generator must + # be materialized, not silently exhausted by the first pass + from isaaclab_physx.sim.schemas import PhysxConvexHullCfg + + from isaaclab.sim.schemas import UsdPhysicsMeshCollisionCfg, apply_mesh_collision_properties + + sim_utils.create_new_stage() + SimulationContext(SimulationCfg(dt=0.01)) + stage = sim_utils.get_current_stage() + _make_xform(stage, "/World/Mgen") + frags = (f for f in [UsdPhysicsMeshCollisionCfg(), PhysxConvexHullCfg(hull_vertex_limit=48)]) + apply_mesh_collision_properties("/World/Mgen", frags, stage) + prim = stage.GetPrimAtPath("/World/Mgen") + # both passes ran: approximation token resolved AND the per-fragment namespaced attr written + assert prim.GetAttribute("physics:approximation").Get() == "convexHull" + assert prim.GetAttribute("physxConvexHullCollision:hullVertexLimit").Get() == 48 + + # ------------------------------------------------------------------------------------- # Public imports # ------------------------------------------------------------------------------------- From 536f9c1cc76abcdc75492c404073f9be9c36b8d3 Mon Sep 17 00:00:00 2001 From: Vidur Vij Date: Thu, 25 Jun 2026 23:46:36 -0700 Subject: [PATCH 10/11] Route mesh-collision fragments through a per-fragment func Move the physics:approximation token coupling out of the apply_mesh_collision_properties writer and into apply_mesh_collision, the default func every MeshCollisionFragment now carries. The writer collapses to the same generic shape as the sibling family writers (apply the MeshCollisionAPI anchor, then dispatch each fragment's func), and the shared-token last-non-none-wins behavior now emerges from dispatch order rather than a special reduction pass in the writer body. This makes mesh collision consistent with the tendon fragments, where the backend application logic lives in a func referenced by the fragment rather than being special-cased by the caller. --- source/isaaclab/isaaclab/sim/__init__.pyi | 2 + .../isaaclab/sim/schemas/__init__.pyi | 2 + .../isaaclab/isaaclab/sim/schemas/schemas.py | 107 +++++++++++------- .../isaaclab/sim/schemas/schemas_cfg.py | 5 +- .../test/sim/test_mesh_collision_fragments.py | 42 ++++++- 5 files changed, 115 insertions(+), 43 deletions(-) diff --git a/source/isaaclab/isaaclab/sim/__init__.pyi b/source/isaaclab/isaaclab/sim/__init__.pyi index d5484505b1f1..97c5fe1f586f 100644 --- a/source/isaaclab/isaaclab/sim/__init__.pyi +++ b/source/isaaclab/isaaclab/sim/__init__.pyi @@ -70,6 +70,7 @@ __all__ = [ "UsdPhysicsRigidBodyCfg", "apply_collision_properties", "apply_mass_properties", + "apply_mesh_collision", "apply_mesh_collision_properties", "apply_namespaced", "apply_rigid_body_properties", @@ -251,6 +252,7 @@ from .schemas import ( activate_contact_sensors, apply_collision_properties, apply_mass_properties, + apply_mesh_collision, apply_mesh_collision_properties, apply_namespaced, apply_rigid_body_properties, diff --git a/source/isaaclab/isaaclab/sim/schemas/__init__.pyi b/source/isaaclab/isaaclab/sim/schemas/__init__.pyi index 9bf6db0a0b97..9b63e0652956 100644 --- a/source/isaaclab/isaaclab/sim/schemas/__init__.pyi +++ b/source/isaaclab/isaaclab/sim/schemas/__init__.pyi @@ -10,6 +10,7 @@ __all__ = [ "activate_contact_sensors", "apply_collision_properties", "apply_mass_properties", + "apply_mesh_collision", "apply_mesh_collision_properties", "apply_namespaced", "apply_rigid_body_properties", @@ -66,6 +67,7 @@ from .schemas import ( activate_contact_sensors, apply_collision_properties, apply_mass_properties, + apply_mesh_collision, apply_mesh_collision_properties, apply_namespaced, apply_rigid_body_properties, diff --git a/source/isaaclab/isaaclab/sim/schemas/schemas.py b/source/isaaclab/isaaclab/sim/schemas/schemas.py index 1813592e88d7..55377be3ef23 100644 --- a/source/isaaclab/isaaclab/sim/schemas/schemas.py +++ b/source/isaaclab/isaaclab/sim/schemas/schemas.py @@ -475,28 +475,77 @@ 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 ``: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), 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. + ``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. @@ -508,14 +557,11 @@ def apply_mesh_collision_properties( 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 the resolved mesh + 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() - # materialize: this writer iterates ``fragments`` twice (approximation-token resolution, then - # dispatch), so a one-shot iterable (generator) would be exhausted by the first pass - fragments = list(fragments) prim = stage.GetPrimAtPath(prim_path) # fail loudly on an invalid path (matches the sibling apply_* writers) if not prim.IsValid(): @@ -523,25 +569,8 @@ def apply_mesh_collision_properties( # 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), - # aggregating per-fragment results so a reported failure is not masked by the always-applied anchor + # 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) diff --git a/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py b/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py index 32893b9bcde0..4047990ef241 100644 --- a/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py +++ b/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py @@ -195,7 +195,10 @@ class MeshCollisionFragment(SchemaFragment): :func:`~isaaclab.sim.schemas.apply_mesh_collision_properties`. """ - pass + # 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 diff --git a/source/isaaclab/test/sim/test_mesh_collision_fragments.py b/source/isaaclab/test/sim/test_mesh_collision_fragments.py index d6b59851efc3..c592c6de75e3 100644 --- a/source/isaaclab/test/sim/test_mesh_collision_fragments.py +++ b/source/isaaclab/test/sim/test_mesh_collision_fragments.py @@ -52,7 +52,7 @@ def test_mesh_collision_fragment_metadata_defaults(): assert isinstance(cfg, MeshCollisionFragment) and isinstance(cfg, SchemaFragment) assert type(cfg)._usd_namespace == "physics" assert type(cfg)._usd_applied_schema is None # anchor applies MeshCollisionAPI, not the fragment - assert cfg.func == "isaaclab.sim.schemas:apply_namespaced" + assert cfg.func == "isaaclab.sim.schemas:apply_mesh_collision" assert cfg.mesh_approximation_name == "convexHull" @@ -275,8 +275,8 @@ def test_apply_mesh_collision_properties_aggregates_fragment_results(): def test_apply_mesh_collision_properties_accepts_generator(): - # the writer iterates fragments twice (token resolution + dispatch); a one-shot generator must - # be materialized, not silently exhausted by the first pass + # the writer dispatches fragments from any iterable; a one-shot generator is consumed once and + # each fragment authors both its namespace and its implied approximation token from isaaclab_physx.sim.schemas import PhysxConvexHullCfg from isaaclab.sim.schemas import UsdPhysicsMeshCollisionCfg, apply_mesh_collision_properties @@ -293,6 +293,41 @@ def test_apply_mesh_collision_properties_accepts_generator(): assert prim.GetAttribute("physxConvexHullCollision:hullVertexLimit").Get() == 48 +# ------------------------------------------------------------------------------------- +# apply_mesh_collision: the per-fragment func carrying the approximation-token coupling +# ------------------------------------------------------------------------------------- + + +def test_apply_mesh_collision_writes_namespace_and_implied_token(): + # the per-fragment func (the default ``func`` of every MeshCollisionFragment) writes the + # fragment's namespaced cooking attrs AND the ``physics:approximation`` token it implies + from isaaclab_physx.sim.schemas import PhysxConvexHullCfg + + from isaaclab.sim.schemas import apply_mesh_collision + + sim_utils.create_new_stage() + SimulationContext(SimulationCfg(dt=0.01)) + stage = sim_utils.get_current_stage() + prim = _make_xform(stage, "/World/Mfunc") + UsdPhysics.MeshCollisionAPI.Apply(prim) + apply_mesh_collision(PhysxConvexHullCfg(hull_vertex_limit=16), "/World/Mfunc", stage) + assert prim.GetAttribute("physxConvexHullCollision:hullVertexLimit").Get() == 16 + assert prim.GetAttribute("physics:approximation").Get() == "convexHull" + + +def test_apply_mesh_collision_rejects_invalid_token(): + import pytest + + from isaaclab.sim.schemas import UsdPhysicsMeshCollisionCfg, apply_mesh_collision + + sim_utils.create_new_stage() + SimulationContext(SimulationCfg(dt=0.01)) + stage = sim_utils.get_current_stage() + _make_xform(stage, "/World/Mfunc2") + with pytest.raises(ValueError): + apply_mesh_collision(UsdPhysicsMeshCollisionCfg(mesh_approximation_name="notAToken"), "/World/Mfunc2", stage) + + # ------------------------------------------------------------------------------------- # Public imports # ------------------------------------------------------------------------------------- @@ -312,6 +347,7 @@ def test_public_imports(): MeshCollisionFragment, SchemaFragment, UsdPhysicsMeshCollisionCfg, + apply_mesh_collision, apply_mesh_collision_properties, apply_namespaced, ) From 2caefdec479b8297f729f1be4534240a7fdddbcf Mon Sep 17 00:00:00 2001 From: Vidur Vij Date: Fri, 26 Jun 2026 09:41:55 -0700 Subject: [PATCH 11/11] Fix mesh_converter SchemaFragment reference on the module import mesh_converter.py imports the schemas module (from isaaclab.sim.schemas import schemas), which does not export SchemaFragment (it lives in schemas_cfg). The collider transition shim used schemas.SchemaFragment, raising AttributeError on any collision_props. Use the bare SchemaFragment already imported in this file, matching the other shims here. --- source/isaaclab/isaaclab/sim/converters/mesh_converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/isaaclab/isaaclab/sim/converters/mesh_converter.py b/source/isaaclab/isaaclab/sim/converters/mesh_converter.py index ed652ec94437..186811143b7f 100644 --- a/source/isaaclab/isaaclab/sim/converters/mesh_converter.py +++ b/source/isaaclab/isaaclab/sim/converters/mesh_converter.py @@ -131,7 +131,7 @@ 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(