Source code for omni.isaac.lab.envs.ui.viewport_camera_controller
# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
from __future__ import annotations
import copy
import numpy as np
import torch
import weakref
from collections.abc import Sequence
from typing import TYPE_CHECKING
import omni.kit.app
import omni.timeline
if TYPE_CHECKING:
from omni.isaac.lab.envs import DirectRLEnv, ManagerBasedEnv, ViewerCfg
[docs]class ViewportCameraController:
"""This class handles controlling the camera associated with a viewport in the simulator.
It can be used to set the viewpoint camera to track different origin types:
- **world**: the center of the world (static)
- **env**: the center of an environment (static)
- **asset_root**: the root of an asset in the scene (e.g. tracking a robot moving in the scene)
On creation, the camera is set to track the origin type specified in the configuration.
For the :attr:`asset_root` origin type, the camera is updated at each rendering step to track the asset's
root position. For this, it registers a callback to the post update event stream from the simulation app.
"""
[docs] def __init__(self, env: ManagerBasedEnv | DirectRLEnv, cfg: ViewerCfg):
"""Initialize the ViewportCameraController.
Args:
env: The environment.
cfg: The configuration for the viewport camera controller.
Raises:
ValueError: If origin type is configured to be "env" but :attr:`cfg.env_index` is out of bounds.
ValueError: If origin type is configured to be "asset_root" but :attr:`cfg.asset_name` is unset.
"""
# store inputs
self._env = env
self._cfg = copy.deepcopy(cfg)
# cast viewer eye and look-at to numpy arrays
self.default_cam_eye = np.array(self._cfg.eye)
self.default_cam_lookat = np.array(self._cfg.lookat)
# set the camera origins
if self.cfg.origin_type == "env":
# check that the env_index is within bounds
self.set_view_env_index(self.cfg.env_index)
# set the camera origin to the center of the environment
self.update_view_to_env()
elif self.cfg.origin_type == "asset_root":
# note: we do not yet update camera for tracking an asset origin, as the asset may not yet be
# in the scene when this is called. Instead, we subscribe to the post update event to update the camera
# at each rendering step.
if self.cfg.asset_name is None:
raise ValueError(f"No asset name provided for viewer with origin type: '{self.cfg.origin_type}'.")
else:
# set the camera origin to the center of the world
self.update_view_to_world()
# subscribe to post update event so that camera view can be updated at each rendering step
app_interface = omni.kit.app.get_app_interface()
app_event_stream = app_interface.get_post_update_event_stream()
self._viewport_camera_update_handle = app_event_stream.create_subscription_to_pop(
lambda event, obj=weakref.proxy(self): obj._update_tracking_callback(event)
)
def __del__(self):
"""Unsubscribe from the callback."""
# use hasattr to handle case where __init__ has not completed before __del__ is called
if hasattr(self, "_viewport_camera_update_handle") and self._viewport_camera_update_handle is not None:
self._viewport_camera_update_handle.unsubscribe()
self._viewport_camera_update_handle = None
"""
Properties
"""
@property
def cfg(self) -> ViewerCfg:
"""The configuration for the viewer."""
return self._cfg
"""
Public Functions
"""
[docs] def set_view_env_index(self, env_index: int):
"""Sets the environment index for the camera view.
Args:
env_index: The index of the environment to set the camera view to.
Raises:
ValueError: If the environment index is out of bounds. It should be between 0 and num_envs - 1.
"""
# check that the env_index is within bounds
if env_index < 0 or env_index >= self._env.num_envs:
raise ValueError(
f"Out of range value for attribute 'env_index': {env_index}."
f" Expected a value between 0 and {self._env.num_envs - 1} for the current environment."
)
# update the environment index
self.cfg.env_index = env_index
# update the camera view if the origin is set to env type (since, the camera view is static)
# note: for assets, the camera view is updated at each rendering step
if self.cfg.origin_type == "env":
self.update_view_to_env()
[docs] def update_view_to_world(self):
"""Updates the viewer's origin to the origin of the world which is (0, 0, 0)."""
# set origin type to world
self.cfg.origin_type = "world"
# update the camera origins
self.viewer_origin = torch.zeros(3)
# update the camera view
self.update_view_location()
[docs] def update_view_to_env(self):
"""Updates the viewer's origin to the origin of the selected environment."""
# set origin type to world
self.cfg.origin_type = "env"
# update the camera origins
self.viewer_origin = self._env.scene.env_origins[self.cfg.env_index]
# update the camera view
self.update_view_location()
[docs] def update_view_to_asset_root(self, asset_name: str):
"""Updates the viewer's origin based upon the root of an asset in the scene.
Args:
asset_name: The name of the asset in the scene. The name should match the name of the
asset in the scene.
Raises:
ValueError: If the asset is not in the scene.
"""
# check if the asset is in the scene
if self.cfg.asset_name != asset_name:
asset_entities = [*self._env.scene.rigid_objects.keys(), *self._env.scene.articulations.keys()]
if asset_name not in asset_entities:
raise ValueError(f"Asset '{asset_name}' is not in the scene. Available entities: {asset_entities}.")
# update the asset name
self.cfg.asset_name = asset_name
# set origin type to asset_root
self.cfg.origin_type = "asset_root"
# update the camera origins
self.viewer_origin = self._env.scene[self.cfg.asset_name].data.root_pos_w[self.cfg.env_index]
# update the camera view
self.update_view_location()
[docs] def update_view_location(self, eye: Sequence[float] | None = None, lookat: Sequence[float] | None = None):
"""Updates the camera view pose based on the current viewer origin and the eye and lookat positions.
Args:
eye: The eye position of the camera. If None, the current eye position is used.
lookat: The lookat position of the camera. If None, the current lookat position is used.
"""
# store the camera view pose for later use
if eye is not None:
self.default_cam_eye = np.asarray(eye)
if lookat is not None:
self.default_cam_lookat = np.asarray(lookat)
# set the camera locations
viewer_origin = self.viewer_origin.detach().cpu().numpy()
cam_eye = viewer_origin + self.default_cam_eye
cam_target = viewer_origin + self.default_cam_lookat
# set the camera view
self._env.sim.set_camera_view(eye=cam_eye, target=cam_target)
"""
Private Functions
"""
def _update_tracking_callback(self, event):
"""Updates the camera view at each rendering step."""
# update the camera view if the origin is set to asset_root
# in other cases, the camera view is static and does not need to be updated continuously
if self.cfg.origin_type == "asset_root" and self.cfg.asset_name is not None:
self.update_view_to_asset_root(self.cfg.asset_name)