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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions source/isaaclab/changelog.d/vidurv-schema-frag-materials.minor.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Added
^^^^^

* Added the rigid-body physics-material "fragment" classes
:class:`~isaaclab.sim.spawners.materials.RigidBodyMaterialFragment` (marker base) and
:class:`~isaaclab.sim.spawners.materials.UsdPhysicsRigidBodyMaterialCfg` (solver-common
``physics:*`` friction/restitution), plus the family writer
:func:`~isaaclab.sim.spawners.materials.spawn_rigid_body_material_from_fragments` and the slot
dispatcher :func:`~isaaclab.sim.spawners.materials.spawn_physics_material`. Spawner
``physics_material`` slots now accept a list of single-namespace fragments in addition to the
legacy material cfg.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from isaaclab.sim import converters, schemas
from isaaclab.sim.spawners.materials import SurfaceDeformableBodyMaterialBaseCfg
from isaaclab.sim.spawners.materials.physics_materials import spawn_physics_material
from isaaclab.sim.utils import (
add_labels,
bind_physics_material,
Expand Down Expand Up @@ -211,7 +212,7 @@ def spawn_ground_plane(

# Create physics material
if cfg.physics_material is not None:
cfg.physics_material.func(f"{prim_path}/physicsMaterial", cfg.physics_material)
spawn_physics_material(f"{prim_path}/physicsMaterial", cfg.physics_material)
# Apply physics material to ground plane
collision_prim = get_first_matching_child_prim(
prim_path,
Expand Down Expand Up @@ -478,8 +479,8 @@ def _spawn_from_usd_file(
material_path = f"{prim_path}/{cfg.physics_material_path}"
else:
material_path = cfg.physics_material_path
# create material
cfg.physics_material.func(material_path, cfg.physics_material)
# create material (accepts a legacy material cfg or rigid-body fragment(s))
spawn_physics_material(material_path, cfg.physics_material, stage=stage)
# apply material
bind_physics_material(prim_path, material_path, stage=stage)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,17 @@ class FileCfg(RigidObjectSpawnerCfg, DeformableObjectSpawnerCfg):
This parameter is ignored if `physics_material` is not None.
"""

physics_material: materials.PhysicsMaterialCfg | None = None
physics_material: (
materials.PhysicsMaterialCfg
| materials.RigidBodyMaterialFragment
| list[materials.RigidBodyMaterialFragment]
| None
) = None
"""Physics material properties.

Accepts either a legacy material cfg or a list of single-namespace
:class:`~isaaclab.sim.spawners.materials.RigidBodyMaterialFragment` instances.

Note:
If None, then no custom physics material will be added.
"""
Expand Down
13 changes: 12 additions & 1 deletion source/isaaclab/isaaclab/sim/spawners/materials/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@

__all__ = [
"spawn_rigid_body_material",
"spawn_rigid_body_material_from_fragments",
"spawn_physics_material",
"spawn_deformable_body_material",
"PhysicsMaterialCfg",
"RigidBodyMaterialBaseCfg",
"RigidBodyMaterialFragment",
"UsdPhysicsRigidBodyMaterialCfg",
"DeformableBodyMaterialBaseCfg",
"DeformableBodyMaterialCfg",
"SurfaceDeformableBodyMaterialBaseCfg",
Expand All @@ -20,10 +24,17 @@ __all__ = [
"VisualMaterialCfg",
]

from .physics_materials import spawn_rigid_body_material, spawn_deformable_body_material
from .physics_materials import (
spawn_deformable_body_material,
spawn_physics_material,
spawn_rigid_body_material,
spawn_rigid_body_material_from_fragments,
)
from .physics_materials_cfg import (
PhysicsMaterialCfg,
RigidBodyMaterialBaseCfg,
RigidBodyMaterialFragment,
UsdPhysicsRigidBodyMaterialCfg,
DeformableBodyMaterialBaseCfg,
DeformableBodyMaterialCfg,
SurfaceDeformableBodyMaterialBaseCfg,
Expand Down
104 changes: 104 additions & 0 deletions source/isaaclab/isaaclab/sim/spawners/materials/physics_materials.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,114 @@
from isaaclab.sim.schemas.schemas import _apply_namespaced_schemas
from isaaclab.sim.utils import clone
from isaaclab.sim.utils.stage import get_current_stage
from isaaclab.utils.string import string_to_callable

from . import physics_materials_cfg


def spawn_rigid_body_material_from_fragments(
prim_path: str,
fragments: physics_materials_cfg.RigidBodyMaterialFragment | list[physics_materials_cfg.RigidBodyMaterialFragment],
stage: Usd.Stage | None = None,
) -> Usd.Prim:
"""Spawn a rigid-body physics material from a list of single-namespace fragments.

Creates (or reuses) the ``UsdShade.Material`` prim at ``prim_path``, applies the standard
``UsdPhysics.MaterialAPI`` anchor, then dispatches each fragment via its
:attr:`~isaaclab.sim.schemas.SchemaFragment.func` to author its namespace onto the material prim.
Backend fragments carry backend-specific namespaces (e.g. PhysX ``physxMaterial:*``) without core
importing a backend.

.. note::
Unlike the ``@clone``-decorated :func:`spawn_rigid_body_material`, this writer expects a
concrete ``prim_path`` and does not resolve regex prim-path patterns. Physics materials are
spawned at a single derived path by :func:`spawn_physics_material` and the spawner internals,
so regex resolution does not apply here.

Args:
prim_path: The prim path to spawn the material at.
fragments: A single :class:`~isaaclab.sim.spawners.materials.RigidBodyMaterialFragment` or a list
of them.
stage: The stage to spawn on. Defaults to None, in which case the current stage is used.

Returns:
The spawned rigid body material prim.

Raises:
ValueError: When a prim already exists at the path and is not a material.
"""
if stage is None:
stage = get_current_stage()
if not isinstance(fragments, (list, tuple)):
fragments = [fragments]

# create the material prim if none exists yet
if not stage.GetPrimAtPath(prim_path).IsValid():
UsdShade.Material.Define(stage, prim_path)
prim = stage.GetPrimAtPath(prim_path)
if not prim.IsA(UsdShade.Material):
raise ValueError(f"A prim already exists at path: '{prim_path}' but is not a material.")

# apply the standard UsdPhysics MaterialAPI anchor (the defining schema for a physics material)
if not UsdPhysics.MaterialAPI(prim):
UsdPhysics.MaterialAPI.Apply(prim)

# dispatch each fragment's applier (writes its single namespace onto the material prim)
for cfg in fragments:
func = cfg.func if callable(cfg.func) else string_to_callable(cfg.func)
func(cfg, prim_path, stage)
return prim
Comment on lines +20 to +71

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing @clone decorator

spawn_rigid_body_material and spawn_deformable_body_material are both decorated with @clone, which resolves regex prim-path patterns (e.g. /World/Robot_.*/body) to concrete paths before spawning. spawn_rigid_body_material_from_fragments skips the decorator, so callers who pass a regex path directly will silently get a single prim at the literal pattern string rather than one prim per match. The docstring does not note this limitation, which diverges from the documented behaviour of the rest of the spawner family.



def spawn_physics_material(
prim_path: str,
material: physics_materials_cfg.PhysicsMaterialCfg
| physics_materials_cfg.RigidBodyMaterialFragment
| list[physics_materials_cfg.RigidBodyMaterialFragment],
stage: Usd.Stage | None = None,
) -> Usd.Prim:
Comment on lines +74 to +80

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The material parameter has no type annotation. Every other public function in this file is fully annotated, and this function sits at the dispatch boundary between the fragment and legacy interfaces. Adding the union type makes the contract explicit for type checkers and readers.

Suggested change
def spawn_physics_material(
prim_path: str,
material,
stage: Usd.Stage | None = None,
) -> Usd.Prim:
def spawn_physics_material(
prim_path: str,
material: physics_materials_cfg.PhysicsMaterialCfg
| physics_materials_cfg.RigidBodyMaterialFragment
| list[physics_materials_cfg.RigidBodyMaterialFragment],
stage: Usd.Stage | None = None,
) -> Usd.Prim:

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

"""Spawn a physics material from a spawner ``physics_material`` slot value.

Dispatches the two accepted slot forms: a list of (or single) rigid-body fragments is spawned via
:func:`spawn_rigid_body_material_from_fragments`; otherwise the value is a legacy material cfg and is
spawned via its own :attr:`func`. Lets spawners accept both the fragment and the legacy interface
from one call site.

Args:
prim_path: The prim path to spawn the material at.
material: A :class:`~isaaclab.sim.spawners.materials.RigidBodyMaterialFragment` (or list of
them), or a legacy material cfg carrying a :attr:`func`.
stage: The stage to spawn on. Defaults to None, in which case the current stage is used.

Returns:
The spawned material prim.

Raises:
ValueError: When ``material`` is an empty list.
TypeError: When ``material`` is a list containing anything other than
:class:`~isaaclab.sim.spawners.materials.RigidBodyMaterialFragment` instances.
"""
# a list/tuple slot value is the fragment-list form; validate it up front so a malformed list
# surfaces a clear error here rather than an opaque ``AttributeError`` on the legacy path below
if isinstance(material, (list, tuple)):
if not material:
raise ValueError(f"Cannot spawn a physics material at '{prim_path}' from an empty fragment list.")
if not all(isinstance(f, physics_materials_cfg.RigidBodyMaterialFragment) for f in material):
raise TypeError(
"A physics-material fragment list must contain only RigidBodyMaterialFragment instances; got"
f" {[type(f).__name__ for f in material]}."
)
return spawn_rigid_body_material_from_fragments(prim_path, list(material), stage)
# a lone fragment routes through the fragment writer too
if isinstance(material, physics_materials_cfg.RigidBodyMaterialFragment):
return spawn_rigid_body_material_from_fragments(prim_path, [material], stage)
# legacy single-cfg path (rigid or deformable material cfg with its own spawner ``func``).
# NOTE: legacy material funcs take only ``(prim_path, cfg)`` and resolve the stage internally via
# ``get_current_stage()``; they have no ``stage`` parameter, so ``stage`` is intentionally not
# forwarded here. This is invisible in single-stage workflows (the only ones materials are used in).
return material.func(prim_path, material)


@clone
def spawn_rigid_body_material(prim_path: str, cfg: physics_materials_cfg.RigidBodyMaterialBaseCfg) -> Usd.Prim:
"""Create material with rigid-body physics properties.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from dataclasses import MISSING
from typing import ClassVar

from isaaclab.sim.schemas.schemas_cfg import SchemaFragment
from isaaclab.utils.configclass import configclass

# Names that moved out of this submodule into ``isaaclab_physx.sim.spawners.materials.physics_materials_cfg``.
Expand Down Expand Up @@ -89,6 +90,45 @@ class RigidBodyMaterialBaseCfg(PhysicsMaterialCfg):
"""The restitution coefficient. Defaults to 0.0."""


@configclass
class RigidBodyMaterialFragment(SchemaFragment):
"""Marker base for rigid-body physics-material fragments; types the ``physics_material`` slot.

A rigid-body physics material is a single ``UsdShade.Material`` prim that carries one or more
physics-material schemas. The fragments author single namespaces onto that prim: the solver-common
``physics:*`` friction/restitution (:class:`UsdPhysicsRigidBodyMaterialCfg`) and any backend-specific
namespace (e.g. PhysX ``physxMaterial:*`` via :class:`~isaaclab_physx.sim.spawners.materials.PhysxMaterialCfg`).
The defining ``UsdPhysics.MaterialAPI`` anchor is applied by the family writer
(:func:`~isaaclab.sim.spawners.materials.spawn_rigid_body_material_from_fragments`).
"""

pass


@configclass
class UsdPhysicsRigidBodyMaterialCfg(RigidBodyMaterialFragment):
"""``physics:*`` rigid-body material attributes from `UsdPhysics.MaterialAPI`_.

The ``UsdPhysics.MaterialAPI`` schema is applied as the implicit anchor by the rigid-body material
family writer, so this fragment owns no applied schema of its own. ``None`` fields are left
unchanged on the material prim.

.. _UsdPhysics.MaterialAPI: https://openusd.org/dev/api/class_usd_physics_material_a_p_i.html
"""

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

static_friction: float | None = None
"""The static friction coefficient. Writes ``physics:staticFriction``."""

dynamic_friction: float | None = None
"""The dynamic friction coefficient. Writes ``physics:dynamicFriction``."""

restitution: float | None = None
"""The restitution coefficient. Writes ``physics:restitution``."""


@configclass
class DeformableBodyMaterialBaseCfg(PhysicsMaterialCfg):
"""Base physics material parameters for volume deformable bodies.
Expand Down
5 changes: 3 additions & 2 deletions source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from isaaclab.sim.utils import bind_physics_material, bind_visual_material, clone, create_prim, get_current_stage

from ..materials import DeformableBodyMaterialBaseCfg, RigidBodyMaterialCfg, SurfaceDeformableBodyMaterialBaseCfg
from ..materials.physics_materials import spawn_physics_material

if TYPE_CHECKING:
from . import meshes_cfg
Expand Down Expand Up @@ -439,8 +440,8 @@ def _spawn_mesh_geom_from_mesh(
material_path = f"{geom_prim_path}/{cfg.physics_material_path}"
else:
material_path = cfg.physics_material_path
# create material
cfg.physics_material.func(material_path, cfg.physics_material)
# create material (accepts a legacy material cfg or rigid-body fragment(s))
spawn_physics_material(material_path, cfg.physics_material, stage=stage)
# apply material
bind_physics_material(prim_path, material_path, stage=stage)

Expand Down
10 changes: 9 additions & 1 deletion source/isaaclab/isaaclab/sim/spawners/meshes/meshes_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,17 @@ class MeshCfg(RigidObjectSpawnerCfg, DeformableObjectSpawnerCfg):
This parameter is ignored if `physics_material` is not None.
"""

physics_material: materials.PhysicsMaterialCfg | None = None
physics_material: (
materials.PhysicsMaterialCfg
| materials.RigidBodyMaterialFragment
| list[materials.RigidBodyMaterialFragment]
| None
) = None
"""Physics material properties.

Accepts either a legacy material cfg or a list of single-namespace
:class:`~isaaclab.sim.spawners.materials.RigidBodyMaterialFragment` instances.

Note:
If None, then no physics material will be added.
"""
Expand Down
5 changes: 3 additions & 2 deletions source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from pxr import Usd

from isaaclab.sim import schemas
from isaaclab.sim.spawners.materials.physics_materials import spawn_physics_material
from isaaclab.sim.utils import bind_physics_material, bind_visual_material, clone, create_prim, get_current_stage

if TYPE_CHECKING:
Expand Down Expand Up @@ -316,8 +317,8 @@ def _spawn_geom_from_prim_type(
material_path = f"{geom_prim_path}/{cfg.physics_material_path}"
else:
material_path = cfg.physics_material_path
# create material
cfg.physics_material.func(material_path, cfg.physics_material)
# create material (accepts a legacy material cfg or rigid-body fragment(s))
spawn_physics_material(material_path, cfg.physics_material, stage=stage)
# apply material
bind_physics_material(mesh_prim_path, material_path, stage=stage)

Expand Down
11 changes: 10 additions & 1 deletion source/isaaclab/isaaclab/sim/spawners/shapes/shapes_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,18 @@ class ShapeCfg(RigidObjectSpawnerCfg):
If the path is relative, then it will be relative to the prim's path.
This parameter is ignored if `physics_material` is not None.
"""
physics_material: materials.PhysicsMaterialCfg | None = None
physics_material: (
materials.PhysicsMaterialCfg
| materials.RigidBodyMaterialFragment
| list[materials.RigidBodyMaterialFragment]
| None
) = None
"""Physics material properties.

Accepts either a legacy material cfg (e.g.
:class:`~isaaclab_physx.sim.spawners.materials.PhysxRigidBodyMaterialCfg`) or a list of
single-namespace :class:`~isaaclab.sim.spawners.materials.RigidBodyMaterialFragment` instances.

Note:
If None, then no physics material will be added.
"""
Expand Down
Loading
Loading