@@ -971,6 +971,8 @@ def __init__(self, cfg: NewtonVisualizerCfg):
971971 self ._scene_cameras : dict = {}
972972 self ._scene_camera_names : list [str ] = []
973973 self ._active_camera_idx : int = 0
974+ self ._interactive_scene = None
975+ self ._viewer_origin : torch .Tensor | None = None
974976
975977 # ------------------------------------------------------------------
976978 # Shared lifecycle
@@ -1040,6 +1042,7 @@ def initialize(self, scene_data_provider: SceneDataProvider) -> None:
10401042 self ._viewer .set_visible_worlds (self ._resolved_visible_env_ids )
10411043 self ._viewer .set_world_offsets (self .cfg .world_spacing )
10421044 self ._apply_camera_focal_length ()
1045+ self ._setup_initial_camera_origin (apply = False )
10431046 initial_pose = self ._resolve_initial_camera_pose ()
10441047 self ._apply_camera_pose (initial_pose )
10451048 self ._viewer ._paused = False
@@ -1113,6 +1116,9 @@ def step(self, dt: float) -> None:
11131116 self ._sim_time += dt
11141117 self ._step_counter += 1
11151118
1119+ if self .cfg .origin_type == "asset" :
1120+ self ._update_asset_tracking_camera ()
1121+
11161122 # Headless mode renders on demand via render_rgb_array(). Keep the latest
11171123 # physics state available without paying the per-step render cost.
11181124 if self ._runtime_headless :
@@ -1268,6 +1274,15 @@ def set_camera_view(
12681274 self .cfg .lookat = target_t
12691275 self ._apply_camera_pose ((eye_t , target_t ))
12701276
1277+ def reapply_origin (self ) -> None :
1278+ """Recompute the camera position from the current origin config."""
1279+ self ._setup_initial_camera_origin ()
1280+
1281+ @property
1282+ def viewer_origin (self ) -> torch .Tensor | None :
1283+ """World-space origin offset applied to the configured eye and look-at target."""
1284+ return self ._viewer_origin
1285+
12711286 # ------------------------------------------------------------------
12721287 # Hook methods — override in subclasses
12731288 # ------------------------------------------------------------------
@@ -1323,7 +1338,86 @@ def _uses_streaming_view(self) -> bool:
13231338
13241339 def _resolve_initial_camera_pose (self ) -> tuple [tuple [float , float , float ], tuple [float , float , float ]]:
13251340 """Resolve initial camera pose from config or USD camera path."""
1326- return self ._resolve_cfg_camera_pose (type (self ).__name__ )
1341+ return self ._camera_pose_with_origin (self ._resolve_cfg_camera_pose (type (self ).__name__ ))
1342+
1343+ def _setup_initial_camera_origin (self , * , apply : bool = True ) -> None :
1344+ """Position the viewer camera according to :attr:`NewtonVisualizerCfg.origin_type`."""
1345+ from isaaclab .sim import SimulationContext # noqa: PLC0415
1346+
1347+ self ._interactive_scene = getattr (SimulationContext .instance (), "_interactive_scene" , None )
1348+ self ._viewer_origin = self ._resolve_configured_origin ()
1349+
1350+ if apply :
1351+ self ._apply_viewer_origin_to_camera ()
1352+
1353+ def _resolve_configured_origin (self ) -> torch .Tensor :
1354+ """Resolve the configured camera origin."""
1355+ if self .cfg .origin_type == "world" :
1356+ return torch .zeros (3 )
1357+ if self .cfg .origin_type == "env" :
1358+ return self ._resolve_env_origin ()
1359+ if self .cfg .origin_type == "asset" :
1360+ return self ._resolve_asset_origin ()
1361+ logger .warning ("[NewtonVisualizer] Unknown origin_type '%s'; defaulting to world." , self .cfg .origin_type )
1362+ return torch .zeros (3 )
1363+
1364+ def _resolve_env_origin (self ) -> torch .Tensor :
1365+ """Resolve the configured environment origin."""
1366+ scene = self ._interactive_scene
1367+ if scene is None :
1368+ logger .warning ("[NewtonVisualizer] origin_type='env' requested but no scene is registered yet." )
1369+ return torch .zeros (3 )
1370+
1371+ num_envs = scene .num_envs
1372+ if not (0 <= self .cfg .origin_env_index < num_envs ):
1373+ raise ValueError (
1374+ f"[NewtonVisualizer] origin_env_index { self .cfg .origin_env_index } is out of range "
1375+ f"[0, { num_envs - 1 } ] for origin_type='env'."
1376+ )
1377+ return scene .env_origins [self .cfg .origin_env_index ]
1378+
1379+ def _resolve_asset_origin (self ) -> torch .Tensor :
1380+ """Resolve the initial asset origin."""
1381+ if self .cfg .origin_track_path is None :
1382+ raise ValueError ("[NewtonVisualizer] origin_type='asset' requires origin_track_path to be set." )
1383+ return torch .zeros (3 )
1384+
1385+ def _update_asset_tracking_camera (self ) -> None :
1386+ """Update the viewer camera to track an asset root or body."""
1387+ scene = self ._interactive_scene
1388+ if scene is None or self .cfg .origin_track_path is None :
1389+ return
1390+ asset_name , _ , body_name = self .cfg .origin_track_path .partition ("/" )
1391+ try :
1392+ asset = scene [asset_name ]
1393+ except KeyError :
1394+ return
1395+ if body_name :
1396+ body_ids , _ = asset .find_bodies (body_name )
1397+ origin = asset .data .body_pos_w .torch [self .cfg .origin_env_index , body_ids [0 ]]
1398+ else :
1399+ origin = asset .data .root_pos_w .torch [self .cfg .origin_env_index ]
1400+ self ._viewer_origin = origin
1401+ self ._apply_viewer_origin_to_camera ()
1402+
1403+ def _apply_viewer_origin_to_camera (self ) -> None :
1404+ """Compute absolute eye/target from :attr:`_viewer_origin` and push to the viewer."""
1405+ self ._apply_camera_pose (self ._resolve_initial_camera_pose ())
1406+
1407+ def _camera_pose_with_origin (
1408+ self ,
1409+ pose : tuple [tuple [float , float , float ], tuple [float , float , float ]],
1410+ ) -> tuple [tuple [float , float , float ], tuple [float , float , float ]]:
1411+ if self ._viewer_origin is None :
1412+ return pose
1413+ origin = self ._viewer_origin .detach ().cpu ().numpy ()
1414+ eye = np .array (pose [0 ], dtype = float ) + origin
1415+ target = np .array (pose [1 ], dtype = float ) + origin
1416+ return (float (eye [0 ]), float (eye [1 ]), float (eye [2 ])), (
1417+ float (target [0 ]),
1418+ float (target [1 ]),
1419+ float (target [2 ]),
1420+ )
13271421
13281422 def _resolve_streaming_renderer_cfg (self ):
13291423 """Return the renderer cfg for the auto-created streaming camera.
@@ -1919,6 +2013,8 @@ def render_rgb_array(self) -> np.ndarray:
19192013 """
19202014 if self ._viewer is None :
19212015 raise RuntimeError ("NewtonGLVisualizer must be initialized before capturing an RGB frame." )
2016+ if self .cfg .origin_type == "asset" :
2017+ self ._update_asset_tracking_camera ()
19222018 if self ._runtime_headless and self ._state is not None and not self ._viewer .is_paused ():
19232019 self ._pre_step ()
19242020 self ._viewer .begin_frame (self ._sim_time )
@@ -2178,6 +2274,8 @@ def render_rgb_array(self) -> np.ndarray | None:
21782274 """
21792275 if self ._viewer is None :
21802276 return None
2277+ if self .cfg .origin_type == "asset" :
2278+ self ._update_asset_tracking_camera ()
21812279 if self ._runtime_headless and self ._state is not None and not self ._viewer .is_paused ():
21822280 self ._pre_step ()
21832281 self ._viewer .begin_frame (self ._sim_time )
0 commit comments