Skip to content
Open
53 changes: 31 additions & 22 deletions source/isaaclab/isaaclab/scene/interactive_scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
RigidObjectCollection,
RigidObjectCollectionCfg,
)
from isaaclab.physics.scene_data_requirements import resolve_scene_data_requirements
from isaaclab.physics.scene_data_requirements import aggregate_requirements, resolve_scene_data_requirements
from isaaclab.sensors import ContactSensorCfg, FrameTransformerCfg, SensorBase, SensorBaseCfg
from isaaclab.sim import SimulationContext
from isaaclab.sim.utils.stage import get_current_stage, get_current_stage_id
Expand Down Expand Up @@ -140,7 +140,6 @@ def __init__(self, cfg: InteractiveSceneCfg):
self.stage_id = get_current_stage_id()
self.sim.clear_scene_data_visualizer_prebuilt_artifact()
self.physics_backend = self.sim.physics_manager.__name__.lower()
visualizer_clone_fn = None
requested_viz_types = set(self.sim.resolve_visualizer_types())
if self.physics_backend.startswith("ovphysx"):
from isaaclab_ovphysx.cloner import ovphysx_replicate
Expand Down Expand Up @@ -190,26 +189,7 @@ def __init__(self, cfg: InteractiveSceneCfg):
if has_scene_cfg_entities:
self._add_entities_from_cfg()

requirements = resolve_scene_data_requirements(
visualizer_types=requested_viz_types,
renderer_types=self._sensor_renderer_types(),
)
self.sim.update_scene_data_requirements(requirements)
visualizer_clone_fn = cloner.resolve_visualizer_clone_fn(
physics_backend=self.physics_backend,
requirements=requirements,
stage=self.stage,
set_visualizer_artifact=self.sim.set_scene_data_visualizer_prebuilt_artifact,
)
if visualizer_clone_fn is not None:
logger.debug(
"Enabling visualizer artifact prebuild for clone path "
"(backend=%s, requires_newton_model=%s, requires_usd_stage=%s).",
self.physics_backend,
requirements.requires_newton_model,
requirements.requires_usd_stage,
)
self.cloner_cfg.visualizer_clone_fn = visualizer_clone_fn
self._refresh_visualizer_clone_fn_from_requirements(requested_viz_types)

if has_scene_cfg_entities:
self.clone_environments(copy_from_source=(not self.cfg.replicate_physics))
Expand All @@ -226,6 +206,8 @@ def clone_environments(self, copy_from_source: bool = False):
If True, clones are independent copies of the source prim and won't reflect its changes (start-up time
may increase). Defaults to False.
"""
self._refresh_visualizer_clone_fn_from_requirements()

# PhysX-only: set env id bit count for replicated physics. Newton handles env separation in its own API.
# Intentionally matches both physx and ovphysx (both are PhysX-based)
if self.cfg.replicate_physics and "physx" in self.physics_backend:
Expand All @@ -252,6 +234,33 @@ def clone_environments(self, copy_from_source: bool = False):
if self.cloner_cfg.clone_usd:
cloner.usd_replicate(self.stage, *replicate_args)

def _refresh_visualizer_clone_fn_from_requirements(self, visualizer_types=()) -> None:
"""Refresh clone-time visualizer prebuild hook from current scene-data requirements."""
discovered_req = resolve_scene_data_requirements(
visualizer_types=visualizer_types,
renderer_types=self._sensor_renderer_types(),
)
current_req = self.sim.get_scene_data_requirements()
requirements = aggregate_requirements((current_req, discovered_req))
if requirements != current_req:
self.sim.update_scene_data_requirements(requirements)

visualizer_clone_fn = cloner.resolve_visualizer_clone_fn(
physics_backend=self.physics_backend,
requirements=requirements,
stage=self.stage,
set_visualizer_artifact=self.sim.set_scene_data_visualizer_prebuilt_artifact,
)
if visualizer_clone_fn is not None:
logger.debug(
"Enabling visualizer artifact prebuild for clone path "
"(backend=%s, requires_newton_model=%s, requires_usd_stage=%s).",
self.physics_backend,
requirements.requires_newton_model,
requirements.requires_usd_stage,
)
self.cloner_cfg.visualizer_clone_fn = visualizer_clone_fn

def _sensor_renderer_types(self) -> list[str]:
"""Return renderer type names used by scene sensors."""
renderer_types: list[str] = []
Expand Down
25 changes: 25 additions & 0 deletions source/isaaclab/isaaclab/sensors/camera/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ def __init__(self, cfg: CameraCfg):
self._check_supported_data_types(cfg)
# initialize base class
super().__init__(cfg)
self._register_renderer_scene_data_requirements()

# toggle rendering of rtx sensors as True
# this flag is read by SimulationContext to determine if rtx sensors should be rendered
Expand Down Expand Up @@ -154,6 +155,30 @@ def __init__(self, cfg: CameraCfg):
for prim in self.stage.Traverse():
prim.SetInstanceable(False)

def _register_renderer_scene_data_requirements(self) -> None:
"""Register renderer requirements early enough for clone-time prebuilds."""
renderer_type = getattr(getattr(self.cfg, "renderer_cfg", None), "renderer_type", None)
if renderer_type is None:
return

from isaaclab.physics.scene_data_requirements import aggregate_requirements, requirement_for_renderer_type
from isaaclab.sim import SimulationContext

sim = SimulationContext.instance()
if sim is None:
logger.debug("SimulationContext not available; deferring renderer requirements registration.")
return

try:
renderer_req = requirement_for_renderer_type(renderer_type)
except ValueError:
return

current_req = sim.get_scene_data_requirements()
merged_req = aggregate_requirements((current_req, renderer_req))
if merged_req != current_req:
sim.update_scene_data_requirements(merged_req)

def __del__(self):
"""Unsubscribes from callbacks and cleans up renderer resources."""
# unsubscribe callbacks
Expand Down
2 changes: 1 addition & 1 deletion source/isaaclab/isaaclab/visualizers/visualizer_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class VisualizerCfg:
lookat: tuple[float, float, float] = (0.0, 0.0, 0.0)
"""Initial camera look-at point (x, y, z) in world coordinates."""

cam_source: Literal["cfg", "prim_path"] = "cfg"
cam_source: Literal["cfg", "prim_path"] = "prim_path"
"""Camera source mode: 'cfg' uses eye/lookat, 'prim_path' follows a camera prim."""

cam_prim_path: str = "/World/envs/env_0/Camera"
Expand Down
40 changes: 40 additions & 0 deletions source/isaaclab/test/scene/test_interactive_scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import isaaclab.sim as sim_utils
from isaaclab.actuators import ImplicitActuatorCfg
from isaaclab.assets import ArticulationCfg, RigidObjectCfg
from isaaclab.physics.scene_data_requirements import SceneDataRequirement
from isaaclab.scene import InteractiveScene, InteractiveSceneCfg
from isaaclab.sim import build_simulation_context
from isaaclab.utils import configclass
Expand Down Expand Up @@ -134,6 +135,13 @@ def test_clone_environments_non_cfg_invokes_visualizer_clone_fn(monkeypatch: pyt
scene = object.__new__(InteractiveScene)
scene.cfg = SimpleNamespace(replicate_physics=False, num_envs=3)
scene.stage = object()
scene.physics_backend = "physx"
scene._sensors = {}
scene.sim = SimpleNamespace(
get_scene_data_requirements=lambda: SceneDataRequirement(),
update_scene_data_requirements=lambda requirements: None,
set_scene_data_visualizer_prebuilt_artifact=lambda artifact: None,
)
scene.env_fmt = "/World/envs/env_{}"
scene._ALL_INDICES = torch.arange(3, dtype=torch.long)
scene._default_env_origins = torch.zeros((3, 3), dtype=torch.float32)
Expand Down Expand Up @@ -180,6 +188,38 @@ def _usd_replicate(stage, *args, **kwargs):
assert len(usd_calls) == 1


def test_refresh_visualizer_clone_fn_uses_registered_requirements(monkeypatch: pytest.MonkeyPatch):
"""Clone-time prebuild hook should be installed from requirements registered after scene init."""
scene = object.__new__(InteractiveScene)
scene.physics_backend = "physx"
scene.stage = object()
scene._sensors = {}
scene.cloner_cfg = SimpleNamespace(visualizer_clone_fn=None)

requirements = SceneDataRequirement(requires_newton_model=True)
scene.sim = SimpleNamespace(
get_scene_data_requirements=lambda: requirements,
update_scene_data_requirements=lambda requirements: None,
set_scene_data_visualizer_prebuilt_artifact=lambda artifact: None,
)

captured = {}

def _resolve_visualizer_clone_fn(**kwargs):
captured.update(kwargs)
return "visualizer-clone-fn"

monkeypatch.setattr(
"isaaclab.scene.interactive_scene.cloner.resolve_visualizer_clone_fn",
_resolve_visualizer_clone_fn,
)

scene._refresh_visualizer_clone_fn_from_requirements()

assert captured["requirements"].requires_newton_model
assert scene.cloner_cfg.visualizer_clone_fn == "visualizer-clone-fn"


def assert_state_equal(s1: dict, s2: dict, path=""):
"""
Recursively assert that s1 and s2 have the same nested keys
Expand Down
20 changes: 20 additions & 0 deletions source/isaaclab/test/sensors/test_camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import copy
import random
from types import SimpleNamespace

import numpy as np
import pytest
Expand All @@ -27,7 +28,9 @@
from pxr import Gf, Usd, UsdGeom

import isaaclab.sim as sim_utils
from isaaclab.physics.scene_data_requirements import SceneDataRequirement
from isaaclab.sensors.camera import Camera, CameraCfg
from isaaclab.sim import SimulationContext

# sample camera poses
POSITION = (2.5, 2.5, 2.5)
Expand All @@ -43,6 +46,23 @@
WIDTH = 320


def test_camera_registers_renderer_scene_data_requirements(monkeypatch: pytest.MonkeyPatch):
"""Camera creation path should register renderer-driven scene-data requirements."""
camera = object.__new__(Camera)
camera.cfg = SimpleNamespace(renderer_cfg=SimpleNamespace(renderer_type="newton_warp"))
updates = []
sim = SimpleNamespace(
get_scene_data_requirements=lambda: SceneDataRequirement(),
update_scene_data_requirements=updates.append,
)

monkeypatch.setattr(SimulationContext, "instance", staticmethod(lambda: sim))

camera._register_renderer_scene_data_requirements()

assert updates == [SceneDataRequirement(requires_newton_model=True)]


def setup() -> tuple[sim_utils.SimulationContext, CameraCfg, float]:
camera_cfg = CameraCfg(
height=HEIGHT,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,68 +67,11 @@ def test_load_prebuilt_artifact_populates_provider_state():
assert provider._xform_mask_buf is None


def test_load_prebuilt_artifact_missing_falls_back_to_usd_build():
"""When no artifact is registered, the USD-traversal fallback is invoked."""
def test_load_prebuilt_artifact_missing_sets_error_state():
"""When no artifact is registered, model/state stay unset."""
provider = _make_provider()
fallback_artifact = VisualizerPrebuiltArtifacts(
model="usd-built-model",
state="usd-built-state",
rigid_body_paths=["/World/envs/env_0/A"],
articulation_paths=[],
num_envs=2,
)
stored: list[VisualizerPrebuiltArtifacts] = []
provider._simulation_context = SimpleNamespace(
get_scene_data_visualizer_prebuilt_artifact=lambda: None,
set_scene_data_visualizer_prebuilt_artifact=stored.append,
)
provider._stage = None
provider._xform_views = {}
provider._view_body_index_map = {}
provider._view_order_tensors = {}
provider._pose_buf_num_bodies = 0
provider._positions_buf = None
provider._orientations_buf = None
provider._covered_buf = None
provider._xform_mask_buf = None

with (
patch.object(
PhysxSceneDataProvider,
"_build_newton_artifact_from_usd_fallback",
autospec=True,
return_value=fallback_artifact,
),
patch(
"isaaclab_physx.scene_data_providers.physx_scene_data_provider.replace_newton_shape_colors",
lambda m, s: None,
),
):
provider._load_newton_model_from_prebuilt_artifact()

assert provider._last_newton_model_build_source == "usd_fallback"
assert provider._newton_model == "usd-built-model"
assert provider._newton_state == "usd-built-state"
assert provider._rigid_body_paths == ["/World/envs/env_0/A"]
assert provider._num_envs_at_last_newton_build == 2
# The fallback artifact is cached on the simulation context so subsequent providers see it.
assert stored == [fallback_artifact]


def test_load_prebuilt_artifact_missing_and_fallback_failed_sets_missing_state():
"""When both the prebuilt artifact and the USD-traversal fallback fail, model/state stay unset."""
provider = _make_provider()
provider._simulation_context = SimpleNamespace(
get_scene_data_visualizer_prebuilt_artifact=lambda: None,
set_scene_data_visualizer_prebuilt_artifact=lambda artifact: None,
)
with patch.object(
PhysxSceneDataProvider,
"_build_newton_artifact_from_usd_fallback",
autospec=True,
return_value=None,
):
provider._load_newton_model_from_prebuilt_artifact()
provider._simulation_context = SimpleNamespace(get_scene_data_visualizer_prebuilt_artifact=lambda: None)
provider._load_newton_model_from_prebuilt_artifact()
assert provider._last_newton_model_build_source == "missing"
assert provider._newton_model is None
assert provider._newton_state is None
Loading
Loading