diff --git a/source/isaaclab/changelog.d/zhengyuz-physx-scene-data-shadowhand.skip b/source/isaaclab/changelog.d/zhengyuz-physx-scene-data-shadowhand.skip new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/source/isaaclab/test/sim/test_newton_manager_visualization_state.py b/source/isaaclab/test/sim/test_newton_manager_visualization_state.py index 1c2b5b44f60b..919d4f986a4c 100644 --- a/source/isaaclab/test/sim/test_newton_manager_visualization_state.py +++ b/source/isaaclab/test/sim/test_newton_manager_visualization_state.py @@ -161,3 +161,24 @@ def test_update_visualization_state_noop_when_backend_is_newton(monkeypatch): NewtonManager.update_visualization_state() assert NewtonManager._model == "live-model" assert NewtonManager._state_0 == "live-state" + + +def test_resolve_scene_data_body_paths_uses_joint_body_targets(): + """PhysX visualization sync maps Newton joint labels to the actual body prim path.""" + import pytest + + pytest.importorskip("pxr") + from isaaclab_newton.physics import NewtonManager + + from pxr import Usd, UsdGeom, UsdPhysics + + stage = Usd.Stage.CreateInMemory() + body_prim = UsdGeom.Xform.Define(stage, "/World/envs/env_0/Robot/robot0_forearm").GetPrim() + UsdPhysics.RigidBodyAPI.Apply(body_prim) + joint = UsdPhysics.FixedJoint.Define(stage, "/World/envs/env_0/Robot/joints/robot0_forearm") + joint.GetBody1Rel().SetTargets([body_prim.GetPath()]) + + body_paths = ["/World/envs/env_0/Robot/joints/robot0_forearm"] + resolved_paths = NewtonManager._resolve_scene_data_body_paths(body_paths, stage) + + assert resolved_paths == ["/World/envs/env_0/Robot/robot0_forearm"] diff --git a/source/isaaclab_newton/changelog.d/zhengyuz-physx-scene-data-shadowhand.rst b/source/isaaclab_newton/changelog.d/zhengyuz-physx-scene-data-shadowhand.rst new file mode 100644 index 000000000000..48a4850fe4ee --- /dev/null +++ b/source/isaaclab_newton/changelog.d/zhengyuz-physx-scene-data-shadowhand.rst @@ -0,0 +1,5 @@ +Fixed +^^^^^ + +* Fixed Newton visualizers on PhysX simulations when a Newton body label points + at a USD joint prim by resolving the label through the joint's rigid-body target. diff --git a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py index 4a7712f2ee94..238253b11f29 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py @@ -1835,12 +1835,38 @@ def update_visualization_state(cls, scene_data_provider: SceneDataProvider | Non if cls._scene_data is None: cls._scene_data = SceneDataFormat.Transform() if cls._scene_data_mapping is None: - body_paths = list(getattr(cls._model, "body_label", None) or []) + body_paths = cls._resolve_scene_data_body_paths(list(cls._model.body_label), scene_data_provider.usd_stage) cls._scene_data_mapping = scene_data_provider.create_mapping(body_paths) cls._scene_data.transforms = cls._state_0.body_q scene_data_provider.get_transforms(cls._scene_data, mapping=cls._scene_data_mapping) + @staticmethod + def _resolve_scene_data_body_paths(body_paths: list[str | None], stage) -> list[str | None]: + """Map Newton joint labels to their target rigid-body prim paths.""" + if stage is None: + return body_paths + + from pxr import UsdPhysics + + def _joint_body_path(prim): + joint = UsdPhysics.Joint(prim) + for rel in (joint.GetBody1Rel(), joint.GetBody0Rel()): + for target_path in rel.GetTargets(): + target_prim = stage.GetPrimAtPath(target_path) + if target_prim.IsValid() and target_prim.HasAPI(UsdPhysics.RigidBodyAPI): + return target_path.pathString + return None + + resolved_paths = body_paths.copy() + for index, body_path in enumerate(body_paths): + if body_path is None: + continue + prim = stage.GetPrimAtPath(body_path) + if prim.IsValid() and prim.IsA(UsdPhysics.Joint): + resolved_paths[index] = _joint_body_path(prim) or body_path + return resolved_paths + @classmethod def get_state_1(cls) -> State: """Get the next state.""" diff --git a/source/isaaclab_physx/changelog.d/zhengyuz-physx-scene-data-shadowhand.rst b/source/isaaclab_physx/changelog.d/zhengyuz-physx-scene-data-shadowhand.rst new file mode 100644 index 000000000000..19f040e4289b --- /dev/null +++ b/source/isaaclab_physx/changelog.d/zhengyuz-physx-scene-data-shadowhand.rst @@ -0,0 +1,5 @@ +Fixed +^^^^^ + +* Fixed PhysX scene-data rigid-body view discovery to ignore USD joint prims + even when an asset authors ``RigidBodyAPI`` on them. diff --git a/source/isaaclab_physx/isaaclab_physx/physics/physx_manager.py b/source/isaaclab_physx/isaaclab_physx/physics/physx_manager.py index 36701a248f5f..d399d95546be 100644 --- a/source/isaaclab_physx/isaaclab_physx/physics/physx_manager.py +++ b/source/isaaclab_physx/isaaclab_physx/physics/physx_manager.py @@ -191,6 +191,8 @@ def get_rigid_body_view(self) -> omni.physics.tensors.RigidBodyView | None: rigid_body_paths: list[str] = [] non_rigid_body_names: set[str] = set() for prim in stage.Traverse(): + if prim.IsA(UsdPhysics.Joint): + continue prim_path = prim.GetPath().pathString if prim.HasAPI(UsdPhysics.RigidBodyAPI): rigid_body_paths.append(prim_path) diff --git a/source/isaaclab_physx/test/sim/test_physx_scene_data_backend.py b/source/isaaclab_physx/test/sim/test_physx_scene_data_backend.py new file mode 100644 index 000000000000..c73648993933 --- /dev/null +++ b/source/isaaclab_physx/test/sim/test_physx_scene_data_backend.py @@ -0,0 +1,44 @@ +# 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 + +from types import SimpleNamespace + +import pytest + +pytest.importorskip("pxr") +pytest.importorskip("omni.physics.tensors") + + +def test_scene_data_rigid_body_view_skips_joint_prims_with_rigid_body_api(monkeypatch): + """Joint prims must not be passed to PhysX tensor rigid-body views.""" + from isaaclab_physx.physics import physx_manager + from isaaclab_physx.physics.physx_manager import PhysxSceneDataBackend + + from pxr import Usd, UsdGeom, UsdPhysics + + stage = Usd.Stage.CreateInMemory() + body_prim = UsdGeom.Xform.Define(stage, "/World/envs/env_0/Robot/robot0_forearm").GetPrim() + UsdPhysics.RigidBodyAPI.Apply(body_prim) + joint_prim = UsdPhysics.FixedJoint.Define(stage, "/World/envs/env_0/Robot/joints/robot0_forearm").GetPrim() + UsdPhysics.RigidBodyAPI.Apply(joint_prim) + + captured_paths = [] + + class _SimulationView: + def create_rigid_body_view(self, body_paths): + captured_paths.extend(body_paths) + return SimpleNamespace(prim_paths=body_paths) + + monkeypatch.setattr( + physx_manager.omni.usd, + "get_context", + lambda: SimpleNamespace(get_stage=lambda: stage), + ) + + backend = PhysxSceneDataBackend() + backend.simulation_view = _SimulationView() + backend.get_rigid_body_view() + + assert captured_paths == ["/World/envs/env_*/Robot/robot0_forearm"]