From 0fcd06441912eb361a9b7655df35919b2f8f6745 Mon Sep 17 00:00:00 2001 From: Vidur Vij Date: Thu, 4 Jun 2026 18:23:20 -0700 Subject: [PATCH 1/5] 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 9578caa87b49..3fd46a2e2003 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", @@ -221,6 +224,7 @@ from .schemas import ( FixedTendonPropertiesCfg, JointDriveBaseCfg, MassPropertiesCfg, + MeshCollisionFragment, MeshCollisionPropertiesCfg, PhysxJointDrivePropertiesCfg, PhysxRigidBodyPropertiesCfg, @@ -231,8 +235,10 @@ from .schemas import ( SpatialTendonPropertiesCfg, TriangleMeshPropertiesCfg, TriangleMeshSimplificationPropertiesCfg, + UsdPhysicsMeshCollisionCfg, UsdPhysicsRigidBodyCfg, activate_contact_sensors, + apply_mesh_collision_properties, apply_namespaced, apply_rigid_body_properties, define_articulation_root_properties, diff --git a/source/isaaclab/isaaclab/sim/converters/mesh_converter.py b/source/isaaclab/isaaclab/sim/converters/mesh_converter.py index 9c25697e88f1..03f54c288dcd 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 11eca17349bc..9fe94e0ca045 100644 --- a/source/isaaclab/isaaclab/sim/schemas/schemas.py +++ b/source/isaaclab/isaaclab/sim/schemas/schemas.py @@ -242,6 +242,13 @@ def apply_namespaced(cfg, prim_path: str, stage: Usd.Stage | None = None) -> boo for f in dataclasses.fields(cfg): if f.name == "func": continue + # ``mesh_approximation_name`` is not a namespaced attribute: it is the standard + # ``physics:approximation`` token, written by ``apply_mesh_collision_properties`` (the + # family writer) which validates it against ``MESH_APPROXIMATION_TOKENS``. Skip it here + # so a mesh-collision cooking fragment dispatched through this generic applier does not + # author a spurious ``:meshApproximationName`` attribute. + if f.name == "mesh_approximation_name": + continue value = getattr(cfg, f.name) if value is None: continue @@ -448,6 +455,71 @@ def apply_rigid_body_properties(prim_path: str, fragments, stage: Usd.Stage | No return True +def apply_mesh_collision_properties(prim_path: str, fragments, stage: Usd.Stage | None = None) -> bool: + """Apply a list of mesh-collision fragments to a prim. + + Applies ``UsdPhysics.MeshCollisionAPI`` as the implicit anchor (the carrier of the + ``physics:approximation`` token), resolves and writes that token, then dispatches each + fragment via its :attr:`~isaaclab.sim.schemas.SchemaFragment.func`. Backend cooking fragments + carry backend-specific funcs (the generic :func:`apply_namespaced` applier), so core never + imports a backend. + + .. attention:: + **Approximation-token coupling.** The ``physics:approximation`` token is *not* a plain + namespaced attribute: it is shared state set by whichever cooking fragment is present. + Each fragment carries a :attr:`mesh_approximation_name` whose default encodes the token its + cooking schema implies (e.g. ``"convexHull"`` for :class:`PhysxConvexHullCfg`, ``"sdf"`` + for :class:`PhysxSDFMeshCfg`). This writer scans the fragment list and uses the last + fragment whose :attr:`mesh_approximation_name` is set to a non-``"none"`` value (mirroring + the legacy single-cfg behavior), falling back to ``"none"`` when none is set. The token is + validated against :const:`MESH_APPROXIMATION_TOKENS`; an unknown name raises ``ValueError``. + The :attr:`mesh_approximation_name` field is therefore handled here and explicitly skipped + by :func:`apply_namespaced` so it is never authored as a namespaced attribute. + + Args: + prim_path: The prim path to apply the mesh-collision schemas on. This prim should be a Mesh. + fragments: An iterable of :class:`~isaaclab.sim.schemas.MeshCollisionFragment` instances. + stage: The stage where to find the prim. Defaults to None, in which case the current + stage is used. + + Returns: + True if the properties were successfully set. + + Raises: + ValueError: When the resolved mesh approximation name is not in + :const:`MESH_APPROXIMATION_TOKENS`. + """ + if stage is None: + stage = get_current_stage() + prim = stage.GetPrimAtPath(prim_path) + # apply the standard MeshCollisionAPI anchor (carrier of ``physics:approximation``) + if not UsdPhysics.MeshCollisionAPI(prim): + UsdPhysics.MeshCollisionAPI.Apply(prim) + + # resolve the 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 bbd29df6b652..247987914aa6 100644 --- a/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py +++ b/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py @@ -167,6 +167,52 @@ class UsdPhysicsRigidBodyCfg(RigidBodyFragment): """ +@configclass +class MeshCollisionFragment(SchemaFragment): + """Marker base for mesh-collision fragments; types the ``mesh_collision_props`` slot. + + A mesh-collision concept is split across one *core* fragment carrying the standard + ``physics:approximation`` token (:class:`UsdPhysicsMeshCollisionCfg`) and one cooking + fragment per backend cooking schema (PhysX convex hull / decomposition / triangle mesh / + SDF, Newton mesh / SDF). Whichever cooking fragment is present implies the approximation + token written to ``physics:approximation`` -- see + :func:`~isaaclab.sim.schemas.apply_mesh_collision_properties`. + """ + + pass + + +@configclass +class UsdPhysicsMeshCollisionCfg(MeshCollisionFragment): + """``physics:approximation`` mesh-collision token from `UsdPhysics.MeshCollisionAPI`_. + + Carries the standard mesh-collision approximation token (:attr:`mesh_approximation_name` + written to ``physics:approximation``). The ``UsdPhysics.MeshCollisionAPI`` schema is applied + as the implicit anchor by the mesh-collision family writer + (:func:`~isaaclab.sim.schemas.apply_mesh_collision_properties`), so this fragment owns no + applied schema of its own. + + .. note:: + The ``physics:approximation`` attribute is a ``TfToken`` validated against + :const:`~isaaclab.sim.schemas.MESH_APPROXIMATION_TOKENS`; the family writer (not the generic + :func:`~isaaclab.sim.schemas.apply_namespaced` applier) handles the token write, so this + fragment overrides nothing but the namespace metadata. When a PhysX/Newton cooking fragment + is present alongside this one, its default :attr:`mesh_approximation_name` sets the token. + + .. _UsdPhysics.MeshCollisionAPI: https://openusd.org/release/api/class_usd_physics_mesh_collision_a_p_i.html + """ + + _usd_namespace: ClassVar[str | None] = "physics" + _usd_applied_schema: ClassVar[str | None] = None # MeshCollisionAPI applied by the family anchor + + mesh_approximation_name: str = "none" + """Name of mesh collision approximation method. Default: "none". + + Writes the ``physics:approximation`` token via :class:`UsdPhysics.MeshCollisionAPI`. + Refer to :const:`~isaaclab.sim.schemas.MESH_APPROXIMATION_TOKENS` for available options. + """ + + @configclass class ArticulationRootBaseCfg: """Solver-common properties to apply to the root of an articulation. 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 d17403be74bc2519b2cab592405322ff8f758f20 Mon Sep 17 00:00:00 2001 From: Vidur Vij Date: Thu, 4 Jun 2026 20:14:08 -0700 Subject: [PATCH 2/5] 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 f3fb9cfd64059fa0e347e65b2c2ced305779d0c3 Mon Sep 17 00:00:00 2001 From: Vidur Vij Date: Thu, 4 Jun 2026 20:32:54 -0700 Subject: [PATCH 3/5] 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 09a2848d00108d7d9b25db73ce7b0ac0ab91043f Mon Sep 17 00:00:00 2001 From: Vidur Vij Date: Fri, 5 Jun 2026 12:37:05 -0700 Subject: [PATCH 4/5] docs(schemas): mark transition shim if/else for removal post-migration --- source/isaaclab/isaaclab/sim/converters/mesh_converter.py | 2 +- source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py | 2 +- source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py | 2 +- source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/source/isaaclab/isaaclab/sim/converters/mesh_converter.py b/source/isaaclab/isaaclab/sim/converters/mesh_converter.py index 03f54c288dcd..aa64e6671ae3 100644 --- a/source/isaaclab/isaaclab/sim/converters/mesh_converter.py +++ b/source/isaaclab/isaaclab/sim/converters/mesh_converter.py @@ -199,7 +199,7 @@ def _convert_asset(self, cfg: MeshConverterCfg): # apply mass properties if cfg.mass_props is not None: schemas.define_mass_properties(prim_path=xform_prim.GetPath(), cfg=cfg.mass_props, stage=stage) - # apply rigid body properties (transition routing: fragment list -> apply_*; legacy cfg -> define_*) + # apply rigid body properties (transition shim, remove later: fragment list -> apply_*; legacy cfg -> define_*) if cfg.rigid_props is not None: rigid_frags = cfg.rigid_props if isinstance(cfg.rigid_props, (list, tuple)) else [cfg.rigid_props] if rigid_frags and all(isinstance(f, schemas.SchemaFragment) for f in rigid_frags): 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..4545eb23e115 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) diff --git a/source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py b/source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py index d8eaffa5593a..27f815343c0a 100644 --- a/source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py +++ b/source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py @@ -444,7 +444,7 @@ def _spawn_mesh_geom_from_mesh( # apply mass properties if cfg.mass_props is not None: schemas.define_mass_properties(prim_path, cfg.mass_props, stage=stage) - # apply rigid properties (transition routing: fragment list -> apply_*; legacy cfg -> define_*) + # apply rigid properties (transition shim, remove later: fragment list -> apply_*; legacy cfg -> define_*) rigid_frags = cfg.rigid_props if isinstance(cfg.rigid_props, (list, tuple)) else [cfg.rigid_props] if rigid_frags and all(isinstance(f, schemas.SchemaFragment) for f in rigid_frags): schemas.apply_rigid_body_properties(prim_path, rigid_frags, stage=stage) diff --git a/source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py b/source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py index d4af64a14677..0601cbed5411 100644 --- a/source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py +++ b/source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py @@ -322,7 +322,7 @@ def _spawn_geom_from_prim_type( schemas.define_mass_properties(prim_path, cfg.mass_props, stage=stage) # apply rigid body properties if cfg.rigid_props is not None: - # transition routing: new fragment list -> apply_*; legacy single cfg -> define_* + # transition shim, remove later: new fragment list -> apply_*; legacy single cfg -> define_* rigid_frags = cfg.rigid_props if isinstance(cfg.rigid_props, (list, tuple)) else [cfg.rigid_props] if rigid_frags and all(isinstance(f, schemas.SchemaFragment) for f in rigid_frags): schemas.apply_rigid_body_properties(prim_path, rigid_frags, stage=stage) From 00e1a7bb52e1969efa1fb6ff16c5f0bac8e47eae Mon Sep 17 00:00:00 2001 From: Vidur Vij Date: Mon, 8 Jun 2026 20:08:38 -0700 Subject: [PATCH 5/5] 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 9fe94e0ca045..31742098627c 100644 --- a/source/isaaclab/isaaclab/sim/schemas/schemas.py +++ b/source/isaaclab/isaaclab/sim/schemas/schemas.py @@ -496,8 +496,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 247987914aa6..f6e22245f607 100644 --- a/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py +++ b/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py @@ -575,10 +575,7 @@ class MeshCollisionBaseCfg: """ # -- Class metadata (not dataclass fields) -- - # The standard ``UsdPhysics.MeshCollisionAPI`` is always applied by the writer when a - # mesh-collision cfg is supplied; ``_usd_applied_schema`` here records the standard - # API name so subclasses that author no PhysX namespace can rely on the writer's - # standard-vs-PhysX gating logic. PhysX-cooking subclasses override this. + # Records the standard API name for the writer's standard-vs-PhysX gating; cooking subclasses override. _usd_applied_schema: ClassVar[str | None] = "MeshCollisionAPI" # Base class authors no PhysX-namespaced fields, so no namespace is defined. _usd_namespace: ClassVar[str | None] = None 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``. # -------------------------------------------------------------------------------------