Source code for omni.isaac.lab.envs.ui.base_env_window

# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause

from __future__ import annotations

import asyncio
import os
import weakref
from datetime import datetime
from typing import TYPE_CHECKING

import omni.kit.app
import omni.kit.commands
import omni.usd
from pxr import PhysxSchema, Sdf, Usd, UsdGeom, UsdPhysics

from omni.isaac.lab.ui.widgets import ManagerLiveVisualizer

if TYPE_CHECKING:
    import omni.ui

    from ..manager_based_env import ManagerBasedEnv


[docs]class BaseEnvWindow: """Window manager for the basic environment. This class creates a window that is used to control the environment. The window contains controls for rendering, debug visualization, and other environment-specific UI elements. Users can add their own UI elements to the window by using the `with` context manager. This can be done either be inheriting the class or by using the `env.window` object directly from the standalone execution script. Example for adding a UI element from the standalone execution script: >>> with env.window.ui_window_elements["main_vstack"]: >>> ui.Label("My UI element") """
[docs] def __init__(self, env: ManagerBasedEnv, window_name: str = "IsaacLab"): """Initialize the window. Args: env: The environment object. window_name: The name of the window. Defaults to "IsaacLab". """ # store inputs self.env = env # prepare the list of assets that can be followed by the viewport camera # note that the first two options are "World" and "Env" which are special cases self._viewer_assets_options = [ "World", "Env", *self.env.scene.rigid_objects.keys(), *self.env.scene.articulations.keys(), ] # Listeners for environment selection changes self._ui_listeners: list[ManagerLiveVisualizer] = [] print("Creating window for environment.") # create window for UI self.ui_window = omni.ui.Window( window_name, width=400, height=500, visible=True, dock_preference=omni.ui.DockPreference.RIGHT_TOP ) # dock next to properties window asyncio.ensure_future(self._dock_window(window_title=self.ui_window.title)) # keep a dictionary of stacks so that child environments can add their own UI elements # this can be done by using the `with` context manager self.ui_window_elements = dict() # create main frame self.ui_window_elements["main_frame"] = self.ui_window.frame with self.ui_window_elements["main_frame"]: # create main stack self.ui_window_elements["main_vstack"] = omni.ui.VStack(spacing=5, height=0) with self.ui_window_elements["main_vstack"]: # create collapsable frame for simulation self._build_sim_frame() # create collapsable frame for viewer self._build_viewer_frame() # create collapsable frame for debug visualization self._build_debug_vis_frame() with self.ui_window_elements["debug_frame"]: with self.ui_window_elements["debug_vstack"]: self._visualize_manager(title="Actions", class_name="action_manager") self._visualize_manager(title="Observations", class_name="observation_manager")
def __del__(self): """Destructor for the window.""" # destroy the window if self.ui_window is not None: self.ui_window.visible = False self.ui_window.destroy() self.ui_window = None """ Build sub-sections of the UI. """ def _build_sim_frame(self): """Builds the sim-related controls frame for the UI.""" # create collapsable frame for controls self.ui_window_elements["sim_frame"] = omni.ui.CollapsableFrame( title="Simulation Settings", width=omni.ui.Fraction(1), height=0, collapsed=False, style=omni.isaac.ui.ui_utils.get_style(), horizontal_scrollbar_policy=omni.ui.ScrollBarPolicy.SCROLLBAR_AS_NEEDED, vertical_scrollbar_policy=omni.ui.ScrollBarPolicy.SCROLLBAR_ALWAYS_ON, ) with self.ui_window_elements["sim_frame"]: # create stack for controls self.ui_window_elements["sim_vstack"] = omni.ui.VStack(spacing=5, height=0) with self.ui_window_elements["sim_vstack"]: # create rendering mode dropdown render_mode_cfg = { "label": "Rendering Mode", "type": "dropdown", "default_val": self.env.sim.render_mode.value, "items": [member.name for member in self.env.sim.RenderMode if member.value >= 0], "tooltip": "Select a rendering mode\n" + self.env.sim.RenderMode.__doc__, "on_clicked_fn": lambda value: self.env.sim.set_render_mode(self.env.sim.RenderMode[value]), } self.ui_window_elements["render_dropdown"] = omni.isaac.ui.ui_utils.dropdown_builder(**render_mode_cfg) # create animation recording box record_animate_cfg = { "label": "Record Animation", "type": "state_button", "a_text": "START", "b_text": "STOP", "tooltip": "Record the animation of the scene. Only effective if fabric is disabled.", "on_clicked_fn": lambda value: self._toggle_recording_animation_fn(value), } self.ui_window_elements["record_animation"] = omni.isaac.ui.ui_utils.state_btn_builder( **record_animate_cfg ) # disable the button if fabric is not enabled self.ui_window_elements["record_animation"].enabled = not self.env.sim.is_fabric_enabled() def _build_viewer_frame(self): """Build the viewer-related control frame for the UI.""" # create collapsable frame for viewer self.ui_window_elements["viewer_frame"] = omni.ui.CollapsableFrame( title="Viewer Settings", width=omni.ui.Fraction(1), height=0, collapsed=False, style=omni.isaac.ui.ui_utils.get_style(), horizontal_scrollbar_policy=omni.ui.ScrollBarPolicy.SCROLLBAR_AS_NEEDED, vertical_scrollbar_policy=omni.ui.ScrollBarPolicy.SCROLLBAR_ALWAYS_ON, ) with self.ui_window_elements["viewer_frame"]: # create stack for controls self.ui_window_elements["viewer_vstack"] = omni.ui.VStack(spacing=5, height=0) with self.ui_window_elements["viewer_vstack"]: # create a number slider to move to environment origin # NOTE: slider is 1-indexed, whereas the env index is 0-indexed viewport_origin_cfg = { "label": "Environment Index", "type": "button", "default_val": self.env.cfg.viewer.env_index + 1, "min": 1, "max": self.env.num_envs, "tooltip": "The environment index to follow. Only effective if follow mode is not 'World'.", } self.ui_window_elements["viewer_env_index"] = omni.isaac.ui.ui_utils.int_builder(**viewport_origin_cfg) # create a number slider to move to environment origin self.ui_window_elements["viewer_env_index"].add_value_changed_fn(self._set_viewer_env_index_fn) # create a tracker for the camera location viewer_follow_cfg = { "label": "Follow Mode", "type": "dropdown", "default_val": 0, "items": [name.replace("_", " ").title() for name in self._viewer_assets_options], "tooltip": "Select the viewport camera following mode.", "on_clicked_fn": self._set_viewer_origin_type_fn, } self.ui_window_elements["viewer_follow"] = omni.isaac.ui.ui_utils.dropdown_builder(**viewer_follow_cfg) # add viewer default eye and lookat locations self.ui_window_elements["viewer_eye"] = omni.isaac.ui.ui_utils.xyz_builder( label="Camera Eye", tooltip="Modify the XYZ location of the viewer eye.", default_val=self.env.cfg.viewer.eye, step=0.1, on_value_changed_fn=[self._set_viewer_location_fn] * 3, ) self.ui_window_elements["viewer_lookat"] = omni.isaac.ui.ui_utils.xyz_builder( label="Camera Target", tooltip="Modify the XYZ location of the viewer target.", default_val=self.env.cfg.viewer.lookat, step=0.1, on_value_changed_fn=[self._set_viewer_location_fn] * 3, ) def _build_debug_vis_frame(self): """Builds the debug visualization frame for various scene elements. This function inquires the scene for all elements that have a debug visualization implemented and creates a checkbox to toggle the debug visualization for each element that has it implemented. If the element does not have a debug visualization implemented, a label is created instead. """ # create collapsable frame for debug visualization self.ui_window_elements["debug_frame"] = omni.ui.CollapsableFrame( title="Scene Debug Visualization", width=omni.ui.Fraction(1), height=0, collapsed=False, style=omni.isaac.ui.ui_utils.get_style(), horizontal_scrollbar_policy=omni.ui.ScrollBarPolicy.SCROLLBAR_AS_NEEDED, vertical_scrollbar_policy=omni.ui.ScrollBarPolicy.SCROLLBAR_ALWAYS_ON, ) with self.ui_window_elements["debug_frame"]: # create stack for debug visualization self.ui_window_elements["debug_vstack"] = omni.ui.VStack(spacing=5, height=0) with self.ui_window_elements["debug_vstack"]: elements = [ self.env.scene.terrain, *self.env.scene.rigid_objects.values(), *self.env.scene.articulations.values(), *self.env.scene.sensors.values(), ] names = [ "terrain", *self.env.scene.rigid_objects.keys(), *self.env.scene.articulations.keys(), *self.env.scene.sensors.keys(), ] # create one for the terrain for elem, name in zip(elements, names): if elem is not None: self._create_debug_vis_ui_element(name, elem) def _visualize_manager(self, title: str, class_name: str) -> None: """Checks if the attribute with the name 'class_name' can be visualized. If yes, create vis interface. Args: title: The title of the manager visualization frame. class_name: The name of the manager to visualize. """ if hasattr(self.env, class_name) and class_name in self.env.manager_visualizers: manager = self.env.manager_visualizers[class_name] if hasattr(manager, "has_debug_vis_implementation"): self._create_debug_vis_ui_element(title, manager) else: print( f"ManagerLiveVisualizer cannot be created for manager: {class_name}, has_debug_vis_implementation" " does not exist" ) else: print(f"ManagerLiveVisualizer cannot be created for manager: {class_name}, Manager does not exist") """ Custom callbacks for UI elements. """ def _toggle_recording_animation_fn(self, value: bool): """Toggles the animation recording.""" if value: # log directory to save the recording if not hasattr(self, "animation_log_dir"): # create a new log directory log_dir = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") self.animation_log_dir = os.path.join(os.getcwd(), "recordings", log_dir) # start the recording _ = omni.kit.commands.execute( "StartRecording", target_paths=[("/World", True)], live_mode=True, use_frame_range=False, start_frame=0, end_frame=0, use_preroll=False, preroll_frame=0, record_to="FILE", fps=0, apply_root_anim=False, increment_name=True, record_folder=self.animation_log_dir, take_name="TimeSample", ) else: # stop the recording _ = omni.kit.commands.execute("StopRecording") # save the current stage stage = omni.usd.get_context().get_stage() source_layer = stage.GetRootLayer() # output the stage to a file stage_usd_path = os.path.join(self.animation_log_dir, "Stage.usd") source_prim_path = "/" # creates empty anon layer temp_layer = Sdf.Find(stage_usd_path) if temp_layer is None: temp_layer = Sdf.Layer.CreateNew(stage_usd_path) temp_stage = Usd.Stage.Open(temp_layer) # update stage data UsdGeom.SetStageUpAxis(temp_stage, UsdGeom.GetStageUpAxis(stage)) UsdGeom.SetStageMetersPerUnit(temp_stage, UsdGeom.GetStageMetersPerUnit(stage)) # copy the prim Sdf.CreatePrimInLayer(temp_layer, source_prim_path) Sdf.CopySpec(source_layer, source_prim_path, temp_layer, source_prim_path) # set the default prim temp_layer.defaultPrim = Sdf.Path(source_prim_path).name # remove all physics from the stage for prim in temp_stage.TraverseAll(): # skip if the prim is an instance if prim.IsInstanceable(): continue # if prim has articulation then disable it if prim.HasAPI(UsdPhysics.ArticulationRootAPI): prim.RemoveAPI(UsdPhysics.ArticulationRootAPI) prim.RemoveAPI(PhysxSchema.PhysxArticulationAPI) # if prim has rigid body then disable it if prim.HasAPI(UsdPhysics.RigidBodyAPI): prim.RemoveAPI(UsdPhysics.RigidBodyAPI) prim.RemoveAPI(PhysxSchema.PhysxRigidBodyAPI) # if prim is a joint type then disable it if prim.IsA(UsdPhysics.Joint): prim.GetAttribute("physics:jointEnabled").Set(False) # resolve all paths relative to layer path omni.usd.resolve_paths(source_layer.identifier, temp_layer.identifier) # save the stage temp_layer.Save() # print the path to the saved stage print("Recording completed.") print(f"\tSaved recorded stage to : {stage_usd_path}") print(f"\tSaved recorded animation to: {os.path.join(self.animation_log_dir, 'TimeSample_tk001.usd')}") print("\nTo play the animation, check the instructions in the following link:") print( "\thttps://docs.omniverse.nvidia.com/extensions/latest/ext_animation_stage-recorder.html#using-the-captured-timesamples" ) print("\n") # reset the log directory self.animation_log_dir = None def _set_viewer_origin_type_fn(self, value: str): """Sets the origin of the viewport's camera. This is based on the drop-down menu in the UI.""" # Extract the viewport camera controller from environment vcc = self.env.viewport_camera_controller if vcc is None: raise ValueError("Viewport camera controller is not initialized! Please check the rendering mode.") # Based on origin type, update the camera view if value == "World": vcc.update_view_to_world() elif value == "Env": vcc.update_view_to_env() else: # find which index the asset is fancy_names = [name.replace("_", " ").title() for name in self._viewer_assets_options] # store the desired env index viewer_asset_name = self._viewer_assets_options[fancy_names.index(value)] # update the camera view vcc.update_view_to_asset_root(viewer_asset_name) def _set_viewer_location_fn(self, model: omni.ui.SimpleFloatModel): """Sets the viewport camera location based on the UI.""" # access the viewport camera controller (for brevity) vcc = self.env.viewport_camera_controller if vcc is None: raise ValueError("Viewport camera controller is not initialized! Please check the rendering mode.") # obtain the camera locations and set them in the viewpoint camera controller eye = [self.ui_window_elements["viewer_eye"][i].get_value_as_float() for i in range(3)] lookat = [self.ui_window_elements["viewer_lookat"][i].get_value_as_float() for i in range(3)] # update the camera view vcc.update_view_location(eye, lookat) def _set_viewer_env_index_fn(self, model: omni.ui.SimpleIntModel): """Sets the environment index and updates the camera if in 'env' origin mode.""" # access the viewport camera controller (for brevity) vcc = self.env.viewport_camera_controller if vcc is None: raise ValueError("Viewport camera controller is not initialized! Please check the rendering mode.") # store the desired env index, UI is 1-indexed vcc.set_view_env_index(model.as_int - 1) # notify additional listeners for listener in self._ui_listeners: listener.set_env_selection(model.as_int - 1) """ Helper functions - UI building. """ def _create_debug_vis_ui_element(self, name: str, elem: object): """Create a checkbox for toggling debug visualization for the given element.""" from omni.kit.window.extensions import SimpleCheckBox with omni.ui.HStack(): # create the UI element text = ( "Toggle debug visualization." if elem.has_debug_vis_implementation else "Debug visualization not implemented." ) omni.ui.Label( name.replace("_", " ").title(), width=omni.isaac.ui.ui_utils.LABEL_WIDTH - 12, alignment=omni.ui.Alignment.LEFT_CENTER, tooltip=text, ) has_cfg = hasattr(elem, "cfg") and elem.cfg is not None is_checked = False if has_cfg: is_checked = (hasattr(elem.cfg, "debug_vis") and elem.cfg.debug_vis) or ( hasattr(elem, "debug_vis") and elem.debug_vis ) self.ui_window_elements[f"{name}_cb"] = SimpleCheckBox( model=omni.ui.SimpleBoolModel(), enabled=elem.has_debug_vis_implementation, checked=is_checked, on_checked_fn=lambda value, e=weakref.proxy(elem): e.set_debug_vis(value), ) omni.isaac.ui.ui_utils.add_line_rect_flourish() # Create a panel for the debug visualization if isinstance(elem, ManagerLiveVisualizer): self.ui_window_elements[f"{name}_panel"] = omni.ui.Frame(width=omni.ui.Fraction(1)) if not elem.set_vis_frame(self.ui_window_elements[f"{name}_panel"]): print(f"Frame failed to set for ManagerLiveVisualizer: {name}") # Add listener for environment selection changes if isinstance(elem, ManagerLiveVisualizer): self._ui_listeners.append(elem) async def _dock_window(self, window_title: str): """Docks the custom UI window to the property window.""" # wait for the window to be created for _ in range(5): if omni.ui.Workspace.get_window(window_title): break await self.env.sim.app.next_update_async() # dock next to properties window custom_window = omni.ui.Workspace.get_window(window_title) property_window = omni.ui.Workspace.get_window("Property") if custom_window and property_window: custom_window.dock_in(property_window, omni.ui.DockPosition.SAME, 1.0) custom_window.focus()