Source code for isaaclab.envs.ui.base_env_window

# 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 __future__ import annotations

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

from pxr import Sdf, Usd, UsdGeom, UsdPhysics

from isaaclab.sim.utils.stage import resolve_paths
from isaaclab.ui.widgets import ManagerLiveVisualizer
from isaaclab.ui.widgets.ui_visualizer_base import UiVisualizerBase
from isaaclab.utils.version import has_kit

if has_kit():
    import isaacsim
    import omni.kit.commands


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(), ] # get stage handle self.stage = env.sim.stage # 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"]: # Live Plots — manager data plots, shown first self._build_debug_vis_frame() with self.ui_window_elements["debug_frame"]: with self.ui_window_elements["debug_vstack"]: self._visualize_manager(title="Training Metrics", class_name="episode") self._visualize_manager(title="Actions", class_name="action_manager") self._visualize_manager(title="Observations", class_name="observation_manager") # Visualization Markers — scene element debug overlays self._build_vis_markers_frame() # create collapsable frame for simulation self._build_sim_frame() # create collapsable frame for viewer self._build_viewer_frame()
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=isaacsim.gui.components.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 if visualizer supports it self._build_render_mode_dropdown() # 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"] = isaacsim.gui.components.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.get_setting( "/isaaclab/fabric_enabled" ) # create reset episode button reset_cfg = { "label": "Reset Episode", "type": "button", "text": "RESET", "tooltip": "Force-reset all environments immediately.", "on_clicked_fn": lambda: self.env.sim.request_reset(), } self.ui_window_elements["reset_episode"] = isaacsim.gui.components.ui_utils.btn_builder(**reset_cfg) def _build_render_mode_dropdown(self): """Build rendering mode dropdown if a visualizer supports it.""" # Find first visualizer with render_mode support viz = None RenderMode = None for v in self.env.sim.visualizers: if hasattr(v, "render_mode") and hasattr(v, "set_render_mode"): viz = v # Get RenderMode enum from the visualizer's module RenderMode = type(v.render_mode) break if viz is None or RenderMode is None: return def on_render_mode_changed(value: str): if viz is not None and hasattr(viz, "set_render_mode"): viz.set_render_mode(RenderMode[value]) render_mode_cfg = { "label": "Rendering Mode", "type": "dropdown", "default_val": viz.render_mode.value, "items": [member.name for member in RenderMode if member.value >= 0], "tooltip": "Select a rendering mode\n" + (RenderMode.__doc__ or ""), "on_clicked_fn": on_render_mode_changed, } self.ui_window_elements["render_dropdown"] = isaacsim.gui.components.ui_utils.dropdown_builder( **render_mode_cfg ) 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=isaacsim.gui.components.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 _viz = self._get_kit_visualizer() viewport_origin_cfg = { "label": "Environment Index", "type": "button", "default_val": (_viz.cfg.origin_env_index if _viz is not None else 0) + 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"] = isaacsim.gui.components.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"] = isaacsim.gui.components.ui_utils.dropdown_builder( **viewer_follow_cfg ) # add viewer default eye and lookat locations self.ui_window_elements["viewer_eye"] = isaacsim.gui.components.ui_utils.xyz_builder( label="Camera Eye", tooltip="Modify the XYZ location of the viewer eye.", default_val=_viz.cfg.eye if _viz is not None else (4.0, -4.0, 3.0), step=0.1, on_value_changed_fn=[self._set_viewer_location_fn] * 3, ) self.ui_window_elements["viewer_lookat"] = isaacsim.gui.components.ui_utils.xyz_builder( label="Camera Target", tooltip="Modify the XYZ location of the viewer target.", default_val=_viz.cfg.lookat if _viz is not None else (0.0, 0.0, 0.0), step=0.1, on_value_changed_fn=[self._set_viewer_location_fn] * 3, ) def _build_debug_vis_frame(self): """Builds the Live Plots frame for manager data visualizers.""" self.ui_window_elements["debug_frame"] = omni.ui.CollapsableFrame( title="Live Plots", width=omni.ui.Fraction(1), height=0, collapsed=False, style=isaacsim.gui.components.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"]: self.ui_window_elements["debug_vstack"] = omni.ui.VStack(spacing=5, height=0) def _build_vis_markers_frame(self): """Builds the Visualization Markers frame for scene element debug overlays. Creates a checkbox per scene element (terrain, rigid objects, articulations, sensors) that has a debug visualization implemented. """ self.ui_window_elements["vis_markers_frame"] = omni.ui.CollapsableFrame( title="Visualization Markers", width=omni.ui.Fraction(1), height=0, collapsed=False, style=isaacsim.gui.components.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["vis_markers_frame"]: vstack = omni.ui.VStack(spacing=5, height=0) with 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(), ] 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, "manager_visualizers") and class_name in self.env.manager_visualizers: manager = self.env.manager_visualizers[class_name] if hasattr(manager, "has_content") and not manager.has_content: return 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 source_layer = self.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(self.stage)) UsdGeom.SetStageMetersPerUnit(temp_stage, UsdGeom.GetStageMetersPerUnit(self.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.RemoveAppliedSchema("PhysxArticulationAPI") # if prim has rigid body then disable it if prim.HasAPI(UsdPhysics.RigidBodyAPI): prim.RemoveAPI(UsdPhysics.RigidBodyAPI) prim.RemoveAppliedSchema("PhysxRigidBodyAPI") # if prim is a joint type then disable it if prim.IsA(UsdPhysics.Joint): prim.GetAttribute("physics:jointEnabled").Set(False) # resolve paths so asset references remain valid from the new location 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 _get_kit_visualizer(self): """Return the first KitVisualizer active on the simulation, or None.""" try: from isaaclab_visualizers.kit import KitVisualizer except ImportError: return None for viz in self.env.sim._visualizers: if isinstance(viz, KitVisualizer): return viz return 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.""" viz = self._get_kit_visualizer() if viz is None: return if value == "World": viz.cfg.origin_type = "world" elif value == "Env": viz.cfg.origin_type = "env" else: fancy_names = [name.replace("_", " ").title() for name in self._viewer_assets_options] viewer_asset_name = self._viewer_assets_options[fancy_names.index(value)] viz.cfg.origin_type = "asset" viz.cfg.origin_track_path = viewer_asset_name # Reposition the viewport camera immediately so the new origin is reflected # without waiting for the next env.step() call. viz.reapply_origin() def _set_viewer_location_fn(self, model: omni.ui.SimpleFloatModel): """Sets the viewport camera location based on the UI.""" viz = self._get_kit_visualizer() if viz is None: return 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)] viz.set_camera_view(eye, lookat) # Persist the slider values back to cfg so that asset-tracking steps do not # snap the camera back to the previously configured offsets. origin = viz.viewer_origin if origin is not None: origin_np = origin.detach().cpu().numpy() viz.cfg.eye = tuple(float(e - o) for e, o in zip(eye, origin_np)) viz.cfg.lookat = tuple(float(lk - o) for lk, o in zip(lookat, origin_np)) else: viz.cfg.eye = tuple(float(v) for v in eye) viz.cfg.lookat = tuple(float(v) for v in 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.""" viz = self._get_kit_visualizer() env_index = model.as_int - 1 if viz is not None: viz.cfg.origin_env_index = env_index # For "env" origin the camera must be repositioned immediately; for asset-tracking # origins reapply_origin() defers the update to the next step() call (asset state # is needed to compute the new position). if viz.cfg.origin_type == "env": viz.reapply_origin() # notify additional listeners for listener in self._ui_listeners: listener.set_env_selection(env_index) """ 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=isaacsim.gui.components.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), ) isaacsim.gui.components.ui_utils.add_line_rect_flourish() # Create a panel and listener for UiVisualizerBase subclasses (managers and scalar groups). if isinstance(elem, UiVisualizerBase): if elem.has_vis_frame_implementation: 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 visualizer: {name}") if elem.has_env_selection_implementation: 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()