Skip to content

Adding Rigid Body Material USD data classes and writers#6287

Open
vidurv-nvidia wants to merge 4 commits into
isaac-sim:developfrom
vidurv-nvidia:vidurv/schema-frag-materials
Open

Adding Rigid Body Material USD data classes and writers#6287
vidurv-nvidia wants to merge 4 commits into
isaac-sim:developfrom
vidurv-nvidia:vidurv/schema-frag-materials

Conversation

@vidurv-nvidia

Copy link
Copy Markdown

Description

Converts rigid-body physics materials to the single-namespace "fragment" model already used by the rigid-body / collision / mass / mesh / tendon / joint-drive / articulation families. Additive and backward-compatible — the legacy inheritance cfgs remain as deprecated shims.

The one material-specific twist vs. the schema families: a physics material is spawned as its own UsdShade.Material prim and bound (not applied onto the body prim). So the family writer both spawns the prim + applies the anchor and dispatches the fragment list.

Added

  • Core (isaaclab.sim.spawners.materials):
    • RigidBodyMaterialFragment — marker base typing the physics_material slot.
    • UsdPhysicsRigidBodyMaterialCfg — solver-common physics:* friction/restitution (anchor UsdPhysics.MaterialAPI).
    • spawn_rigid_body_material_from_fragments(prim_path, fragments, stage) — spawns the UsdShade.Material prim, applies the MaterialAPI anchor, dispatches each fragment's func (default apply_namespaced).
    • spawn_physics_material(prim_path, material, stage) — slot dispatcher: fragment list → fragment writer, else legacy cfg via its own func.
  • PhysX (isaaclab_physx.sim.spawners.materials):
    • PhysxMaterialCfg — single-namespace physxMaterial:* (PhysxMaterialAPI): compliant-contact spring + combine-mode tokens.

Changed

  • Spawner physics_material slots (shapes, meshes, from_files) now accept RigidBodyMaterialFragment | list[...] in addition to the legacy material cfg; consume sites route through spawn_physics_material. Legacy single-cfg path unchanged.

Scope

  • Rigid-body materials only. Deformable-body materials are deferred — they pair with the deferred deformable-body family (multi-inherited OmniPhysics + PhysX material APIs).

Tests

  • New test_material_fragments.py (6): fragment metadata; spawn-from-fragments composes physics:* + physxMaterial:* with the MaterialAPI anchor + PhysxMaterialAPI; single-fragment; partial-update (None left unauthored); slot dispatcher handles both fragment and legacy forms.
  • Regression: existing test_spawn_materials.py (6) and test_spawn_shapes.py (12) pass — legacy path intact.

Checklist

  • Ran ./isaaclab.sh --format
  • Added changelog fragments (core + physx, minor)

Convert rigid-body physics materials to the single-namespace "fragment"
model used by the other schema families. Add RigidBodyMaterialFragment and
the solver-common UsdPhysicsRigidBodyMaterialCfg (physics:* friction /
restitution) in core, and PhysxMaterialCfg (physxMaterial:* compliant
contact + combine modes) in the PhysX extension.

A material is spawned as a separate UsdShade.Material prim and bound, so the
family writer spawn_rigid_body_material_from_fragments spawns the prim,
applies the UsdPhysics.MaterialAPI anchor, and dispatches each fragment.
Spawner physics_material slots now accept a list of fragments alongside the
legacy material cfg, dispatched through spawn_physics_material; the legacy
single-cfg path is unchanged.
@github-actions github-actions Bot added the isaac-lab Related to Isaac Lab team label Jun 28, 2026
@vidurv-nvidia vidurv-nvidia marked this pull request as ready for review June 28, 2026 06:56
Rename test prim paths MatA/MatB to MaterialA/MaterialB so codespell no
longer flags the truncated tokens, and drop a stray blank line per ruff.
@greptile-apps

greptile-apps Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces rigid-body physics-material "fragments" — a single-namespace config pattern already used across other schema families — letting spawners accept either a list of RigidBodyMaterialFragment instances or the existing legacy PhysicsMaterialCfg, dispatched through a new spawn_physics_material slot function.

  • Core additions: RigidBodyMaterialFragment marker base, UsdPhysicsRigidBodyMaterialCfg (solver-common physics:* attrs), spawn_rigid_body_material_from_fragments (creates the UsdShade.Material prim, applies the UsdPhysics.MaterialAPI anchor, then dispatches each fragment via apply_namespaced), and spawn_physics_material dispatcher with explicit validation for empty/mixed lists.
  • PhysX extension: PhysxMaterialCfg adds the physxMaterial:* single-namespace fragment (PhysxMaterialAPI) alongside the existing PhysxRigidBodyMaterialCfg legacy class.
  • Call-site migration: three physics_material spawner sites (shapes, meshes, from_files) switch from cfg.physics_material.func(...) to spawn_physics_material(...), and all three cfg types widen their physics_material field union to include fragment forms; legacy callers are fully backward-compatible.

Confidence Score: 5/5

Safe to merge. The change is purely additive: legacy callers are unchanged, the new dispatch path is gated behind isinstance checks, and invalid inputs (empty list, mixed-type list) surface clear errors rather than opaque AttributeErrors.

The dispatch logic in spawn_physics_material is straightforward and handles all three input forms correctly. apply_namespaced already skips the func field (confirmed in schemas.py), so no spurious physics:func attribute is written. The UsdPhysics.MaterialAPI anchor is applied before fragment dispatch, satisfying the schema-presence requirement for physics:* attribute writes. Tests cover multi-namespace composition, single-fragment form, the slot dispatcher for both forms, and partial-update (None-field) semantics. No correctness issues were found beyond concerns already surfaced in prior review threads.

No files require special attention.

Important Files Changed

Filename Overview
source/isaaclab/isaaclab/sim/spawners/materials/physics_materials.py Adds spawn_rigid_body_material_from_fragments and spawn_physics_material dispatcher; legacy path acknowledged with a comment; apply_namespaced correctly skips the func field.
source/isaaclab/isaaclab/sim/spawners/materials/physics_materials_cfg.py Adds RigidBodyMaterialFragment marker base and UsdPhysicsRigidBodyMaterialCfg fragment with None-default fields; inherits func from SchemaFragment correctly.
source/isaaclab_physx/isaaclab_physx/sim/spawners/materials/physics_materials_cfg.py Adds PhysxMaterialCfg as a single-namespace RigidBodyMaterialFragment for physxMaterial:* attributes; mirrors PhysxRigidBodyMaterialCfg fields with None defaults.
source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py Both call sites migrated to spawn_physics_material; _spawn_from_usd_file forwards stage, spawn_ground_plane omits it (intentional, single-stage workflow, documented in previous review).
source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py Replaces direct material.func call with spawn_physics_material dispatcher; stage is correctly forwarded.
source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py Same spawn_physics_material switch as shapes.py; stage forwarded correctly.
source/isaaclab/test/sim/test_material_fragments.py Six tests covering fragment metadata, multi-namespace composition, single-fragment form, slot dispatcher (both fragment and legacy), empty/mixed-list rejection, and None-field partial-update semantics.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Caller as Spawner (shapes/meshes/from_files)
    participant SPM as spawn_physics_material
    participant SRBMFF as spawn_rigid_body_material_from_fragments
    participant AN as apply_namespaced
    participant Stage as Usd.Stage

    Caller->>SPM: (prim_path, material, stage)
    alt material is list/tuple
        SPM->>SPM: validate: non-empty, all RigidBodyMaterialFragment
        SPM->>SRBMFF: (prim_path, list(material), stage)
    else material is RigidBodyMaterialFragment
        SPM->>SRBMFF: (prim_path, [material], stage)
    else legacy PhysicsMaterialCfg
        SPM->>Caller: material.func(prim_path, material)
    end

    SRBMFF->>Stage: GetPrimAtPath / Material.Define
    SRBMFF->>Stage: UsdPhysics.MaterialAPI.Apply (anchor)
    loop for each fragment
        SRBMFF->>AN: func(cfg, prim_path, stage)
        AN->>Stage: AddAppliedSchema (if _usd_applied_schema set)
        AN->>Stage: "set physics:* / physxMaterial:* attrs"
    end
    SRBMFF-->>Caller: Usd.Prim
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Caller as Spawner (shapes/meshes/from_files)
    participant SPM as spawn_physics_material
    participant SRBMFF as spawn_rigid_body_material_from_fragments
    participant AN as apply_namespaced
    participant Stage as Usd.Stage

    Caller->>SPM: (prim_path, material, stage)
    alt material is list/tuple
        SPM->>SPM: validate: non-empty, all RigidBodyMaterialFragment
        SPM->>SRBMFF: (prim_path, list(material), stage)
    else material is RigidBodyMaterialFragment
        SPM->>SRBMFF: (prim_path, [material], stage)
    else legacy PhysicsMaterialCfg
        SPM->>Caller: material.func(prim_path, material)
    end

    SRBMFF->>Stage: GetPrimAtPath / Material.Define
    SRBMFF->>Stage: UsdPhysics.MaterialAPI.Apply (anchor)
    loop for each fragment
        SRBMFF->>AN: func(cfg, prim_path, stage)
        AN->>Stage: AddAppliedSchema (if _usd_applied_schema set)
        AN->>Stage: "set physics:* / physxMaterial:* attrs"
    end
    SRBMFF-->>Caller: Usd.Prim
Loading

Reviews (3): Last reviewed commit: "Validate physics-material fragment lists..." | Re-trigger Greptile

Comment on lines +92 to +93
# legacy single-cfg path (rigid or deformable material cfg with its own spawner ``func``)
return material.func(prim_path, material)

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 stage is silently dropped on the legacy path. Callers in meshes.py and from_files.py pass an explicit stage, expecting it to be used for both the fragment and legacy code paths. On the legacy branch, material.func(prim_path, material) ignores stage entirely and whatever the legacy func does internally (typically get_current_stage()) takes over instead.

Suggested change
# legacy single-cfg path (rigid or deformable material cfg with its own spawner ``func``)
return material.func(prim_path, material)
# legacy single-cfg path (rigid or deformable material cfg with its own spawner ``func``)
# NOTE: legacy funcs do not accept a ``stage`` kwarg; they call get_current_stage() internally.
return material.func(prim_path, material)

Comment on lines +68 to +72
def spawn_physics_material(
prim_path: str,
material,
stage: Usd.Stage | None = None,
) -> Usd.Prim:

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!

Comment on lines +20 to +65
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.

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

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.

Annotate the spawn_physics_material material parameter with its accepted
union, document that the legacy dispatch path intentionally does not forward
stage (legacy funcs resolve the stage internally), and note that the fragment
writer expects a concrete prim path rather than a regex pattern.
Comment on lines +97 to +104
fragments = material if isinstance(material, (list, tuple)) else [material]
if fragments and all(isinstance(f, physics_materials_cfg.RigidBodyMaterialFragment) for f in fragments):
return spawn_rigid_body_material_from_fragments(prim_path, list(fragments), 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)

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.

P1 AttributeError on empty or mixed-type list input

fragments = material if isinstance(material, (list, tuple)) else [material] keeps material as the original list, then the if fragments and all(...) guard short-circuits on an empty list (if [] is falsy), which falls through to material.func(prim_path, material) where material is an empty list. That raises AttributeError: 'list' object has no attribute 'func' instead of a clear validation error. The same failure occurs if a user mistakenly passes a list containing a non-fragment element (the all() check fails, legacy branch runs with a list as material).

An early guard that validates and rejects a list on the legacy branch would make the error actionable.

@isaac-sim isaac-sim deleted a comment from greptile-apps Bot Jun 28, 2026
Reject an empty list (ValueError) and a list containing non-fragment
entries (TypeError) in spawn_physics_material, so a malformed slot value
surfaces a clear error instead of an opaque AttributeError on the legacy
path. Route a lone fragment through the fragment writer explicitly. Add a
regression test for the empty and mixed-type cases.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

isaac-lab Related to Isaac Lab team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant