Adding Rigid Body Material USD data classes and writers#6287
Adding Rigid Body Material USD data classes and writers#6287vidurv-nvidia wants to merge 4 commits into
Conversation
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.
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 SummaryThis 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
Confidence Score: 5/5Safe 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
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
%%{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
Reviews (3): Last reviewed commit: "Validate physics-material fragment lists..." | Re-trigger Greptile |
| # legacy single-cfg path (rigid or deformable material cfg with its own spawner ``func``) | ||
| return material.func(prim_path, material) |
There was a problem hiding this comment.
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.
| # 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) |
| def spawn_physics_material( | ||
| prim_path: str, | ||
| material, | ||
| stage: Usd.Stage | None = None, | ||
| ) -> Usd.Prim: |
There was a problem hiding this comment.
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.
| 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!
| 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 |
There was a problem hiding this comment.
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.
| 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) |
There was a problem hiding this comment.
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.
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.
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.Materialprim 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
isaaclab.sim.spawners.materials):RigidBodyMaterialFragment— marker base typing thephysics_materialslot.UsdPhysicsRigidBodyMaterialCfg— solver-commonphysics:*friction/restitution (anchorUsdPhysics.MaterialAPI).spawn_rigid_body_material_from_fragments(prim_path, fragments, stage)— spawns theUsdShade.Materialprim, applies theMaterialAPIanchor, dispatches each fragment'sfunc(defaultapply_namespaced).spawn_physics_material(prim_path, material, stage)— slot dispatcher: fragment list → fragment writer, else legacy cfg via its ownfunc.isaaclab_physx.sim.spawners.materials):PhysxMaterialCfg— single-namespacephysxMaterial:*(PhysxMaterialAPI): compliant-contact spring + combine-mode tokens.Changed
physics_materialslots (shapes,meshes,from_files) now acceptRigidBodyMaterialFragment | list[...]in addition to the legacy material cfg; consume sites route throughspawn_physics_material. Legacy single-cfg path unchanged.Scope
Tests
test_material_fragments.py(6): fragment metadata; spawn-from-fragments composesphysics:*+physxMaterial:*with theMaterialAPIanchor +PhysxMaterialAPI; single-fragment; partial-update (None left unauthored); slot dispatcher handles both fragment and legacy forms.test_spawn_materials.py(6) andtest_spawn_shapes.py(12) pass — legacy path intact.Checklist
./isaaclab.sh --format