Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
f2c6ac1
move branch
matthewtrepte Mar 24, 2026
39cedf8
speedup tests
matthewtrepte Mar 24, 2026
7f23505
fix kit visualizer test
matthewtrepte Mar 24, 2026
457f3d7
readd physx flag
matthewtrepte Mar 24, 2026
554e944
clean tests
matthewtrepte Mar 24, 2026
a6782f8
physics backends
matthewtrepte Mar 24, 2026
39d9a99
wip
matthewtrepte Mar 25, 2026
260ffcb
update camera_position/camera_target_position -> eye/lookat
matthewtrepte Apr 1, 2026
fdd4530
resolve overlapping beahvior in viewrcfg and visualizercfg
matthewtrepte Apr 2, 2026
ce1bff9
init bug fix for stale renderer images after reset w/o kit viz
matthewtrepte Apr 2, 2026
a886aa9
clean
matthewtrepte Apr 3, 2026
9305232
wip
matthewtrepte Apr 13, 2026
14efc99
wip
matthewtrepte Apr 13, 2026
19e1022
prepping
matthewtrepte Apr 14, 2026
5eca07a
rm file
matthewtrepte Apr 14, 2026
0d08fea
fix
matthewtrepte Apr 14, 2026
6f01ce4
preppin
matthewtrepte Apr 14, 2026
8d20245
Merge branch 'develop' into mtrepte/update_viz_2
matthewtrepte Apr 14, 2026
0a98936
move tests
matthewtrepte Apr 15, 2026
89ca77d
lint
matthewtrepte Apr 15, 2026
9e57174
docs
matthewtrepte Apr 16, 2026
0e7d0e2
pass tests
matthewtrepte Apr 16, 2026
38d93e0
PR feedback
matthewtrepte Apr 16, 2026
1e9496b
Merge branch 'develop' into mtrepte/update_viz_2
matthewtrepte Apr 16, 2026
fa49a5f
linting
matthewtrepte Apr 16, 2026
c8f6451
tune frozen viz tests
matthewtrepte Apr 16, 2026
85b700b
lint
matthewtrepte Apr 16, 2026
7eb4a5b
Merge branch 'develop' into mtrepte/update_viz_2
matthewtrepte Apr 16, 2026
c835728
rm docs section from a diff PR
matthewtrepte Apr 16, 2026
d2d556e
clean up docs
matthewtrepte Apr 16, 2026
f5ee0cb
clean
matthewtrepte Apr 16, 2026
bbb82ff
edit comment
matthewtrepte Apr 16, 2026
0ab9bca
add deprecation warning to ViewerCfg
matthewtrepte Apr 16, 2026
c41a009
add tests from 5056
matthewtrepte Apr 17, 2026
65e300d
Merge branch 'develop' into mtrepte/update_viz_2
matthewtrepte Apr 17, 2026
e2ed226
finalize!
matthewtrepte Apr 17, 2026
48f5cf4
fb
matthewtrepte Apr 17, 2026
d645d15
move file
matthewtrepte Apr 17, 2026
3312472
rm
matthewtrepte Apr 17, 2026
b2ef7cf
remove
matthewtrepte Apr 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 11 additions & 17 deletions docs/source/features/visualization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,12 @@ You can also configure custom visualizers in the code by defining ``VisualizerCf
dock_position="SAME",
window_width=1280,
window_height=720,
camera_position=(0.0, 0.0, 20.0), # high top down view
camera_target=(0.0, 0.0, 0.0),
eye=(0.0, 0.0, 20.0), # high top down view
lookat=(0.0, 0.0, 0.0),
),
NewtonVisualizerCfg(
camera_position=(5.0, 5.0, 5.0), # closer quarter view
camera_target=(0.0, 0.0, 0.0),
eye=(5.0, 5.0, 5.0), # closer quarter view
lookat=(0.0, 0.0, 0.0),
show_joints=True,
),
RerunVisualizerCfg(
Expand Down Expand Up @@ -201,8 +201,8 @@ Omniverse Visualizer
window_height=720, # Viewport height in pixels

# Camera settings
camera_position=(8.0, 8.0, 3.0), # Initial camera position (x, y, z)
camera_target=(0.0, 0.0, 0.0), # Camera look-at target
eye=(8.0, 8.0, 3.0), # Initial camera position (x, y, z)
lookat=(0.0, 0.0, 0.0), # Camera look-at target

# Feature toggles
enable_markers=True, # Enable visualization markers
Expand All @@ -217,7 +217,7 @@ Newton Visualizer

- Lightweight OpenGL rendering with low overhead
- Visualization markers (joints, contacts, springs, COM)
- Training and rendering pause controls
- Simulation and rendering pause controls
- Adjustable update frequency for performance tuning
- Some customizable rendering options (shadows, sky, wireframe)

Expand Down Expand Up @@ -255,8 +255,8 @@ Newton Visualizer
window_height=1080, # Window height in pixels

# Camera settings
camera_position=(8.0, 8.0, 3.0), # Initial camera position (x, y, z)
camera_target=(0.0, 0.0, 0.0), # Camera look-at target
eye=(8.0, 8.0, 3.0), # Initial camera position (x, y, z)
lookat=(0.0, 0.0, 0.0), # Camera look-at target

# Performance tuning
update_frequency=1, # Update every N frames (1=every frame)
Expand Down Expand Up @@ -303,8 +303,8 @@ Rerun Visualizer
bind_address="0.0.0.0", # Endpoint host formatting/reuse checks

# Camera settings
camera_position=(8.0, 8.0, 3.0), # Initial camera position (x, y, z)
camera_target=(0.0, 0.0, 0.0), # Camera look-at target
eye=(8.0, 8.0, 3.0), # Initial camera position (x, y, z)
lookat=(0.0, 0.0, 0.0), # Camera look-at target

# History settings
keep_historical_data=False, # Keep transforms for time scrubbing
Expand Down Expand Up @@ -393,12 +393,6 @@ the num of environments can be overwritten and decreased using ``--num_envs``:
python scripts/reinforcement_learning/rsl_rl/train.py --task Isaac-Cartpole-v0 --viz rerun --num_envs 512


.. note::

A future feature will support visualizing only a subset of environments, which will improve visualization performance
and reduce resource usage while maintaining full-scale training in the background.


**Rerun Visualizer FPS Control**

The FPS control in the Rerun visualizer UI may not affect the visualization frame rate in all configurations.
Expand Down
6 changes: 5 additions & 1 deletion source/isaaclab/isaaclab/app/app_launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ def __init__(self, launcher_args: argparse.Namespace | dict | None = None, **kwa
self._offscreen_render: bool # 0: Disabled, 1: Enabled
self._sim_experience_file: str # Experience file to load
self._visualizer_max_worlds: int | None # Optional max worlds override for Newton-based visualizers
self._video_enabled: bool # Whether --video recording is enabled

# Exposed to train scripts
self.device_id: int # device ID for GPU simulation (defaults to 0)
Expand Down Expand Up @@ -858,12 +859,13 @@ def _resolve_xr_settings(self, launcher_args: dict):

def _resolve_viewport_settings(self, launcher_args: dict):
"""Resolve viewport related settings."""
self._video_enabled = bool(launcher_args.get("video", False))
# Check if we can disable the viewport to improve performance
# This should only happen if we are running headless and do not require livestreaming or video recording
# This is different from offscreen_render because this only affects the default viewport and
# not other render-products in the scene
self._render_viewport = True
if self._headless and not self._livestream and not launcher_args.get("video", False):
if self._headless and not self._livestream and not self._video_enabled:
self._render_viewport = False

# hide_ui flag
Expand Down Expand Up @@ -1085,6 +1087,8 @@ def _load_extensions(self):
# (no Kit GUI) the AR profile must be enabled programmatically so that
# the OpenXR session starts without user interaction
settings.set_bool("/isaaclab/xr/auto_start", self._headless and self._xr)
# set setting to indicate video recording mode
settings.set_bool("/isaaclab/video/enabled", self._video_enabled)

# set setting to indicate no RTX sensors are used (set to True when RTX sensor is created)
settings.set_bool("/isaaclab/render/rtx_sensors", False)
Expand Down
23 changes: 22 additions & 1 deletion source/isaaclab/isaaclab/cli/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,9 @@ def _install_extra_frameworks(framework_name: str = "all") -> None:
"newton_actuators",
"warp",
"mujoco_warp",
"websockets",
"viser",
"imgui_bundle",
]
"""Package directory names in Isaac Sim prebundle directories to repoint.

Expand Down Expand Up @@ -352,7 +355,25 @@ def _repoint_prebundle_packages() -> None:
print_warning(f"site-packages directory not found: {site_packages} — skipping prebundle repoint.")
return

prebundle_dirs = list(isaacsim_path.rglob("pip_prebundle"))
# Discover pip_prebundle directories from both the Isaac Sim tree and
# Omniverse cache roots. Some Isaac Sim directories are symlinked into
# ~/.local/share/ov and may be missed by a plain rglob() on _isaac_sim.
candidate_roots: set[Path] = set()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why was this change needed? is there something else in the installation that's broken?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep the viser installation was broken due to a few shared and conflicting libraries with Isaac Sim

for root in (
isaacsim_path,
isaacsim_path.resolve(),
isaacsim_path / "extscache",
Path.home() / ".local" / "share" / "ov" / "data" / "exts",
Path.home() / ".local" / "share" / "ov" / "data" / "exts" / "v2",
):
if root.exists():
candidate_roots.add(root)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Warning: rglob("pip_prebundle") on broad roots like ~/.local/share/ov/data/exts could be slow on machines with large Omniverse caches (thousands of extensions). This runs during isaaclab install so user-facing latency matters.

Consider adding a maxdepth-style guard (e.g. limit recursion depth) or at minimum logging the discovery time so users can diagnose slow installs:

import time
t0 = time.monotonic()
for root in candidate_roots:
    prebundle_dirs.update(root.rglob("pip_prebundle"))
print_debug(f"Discovered {len(prebundle_dirs)} pip_prebundle dirs in {time.monotonic() - t0:.1f}s")

candidate_roots.add(root.resolve())

prebundle_dirs: set[Path] = set()
for root in candidate_roots:
prebundle_dirs.update(root.rglob("pip_prebundle"))

if not prebundle_dirs:
print_debug("No pip_prebundle directories found under Isaac Sim.")
return
Expand Down
52 changes: 51 additions & 1 deletion source/isaaclab/isaaclab/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

from __future__ import annotations

import warnings
from dataclasses import MISSING, fields
from typing import Dict, Literal, TypeVar # noqa: UP035

import gymnasium as gym
Expand All @@ -17,9 +19,28 @@
##


def _viewer_cfg_value_matches_default(current: object, default: object) -> bool:
"""Return True if ``current`` matches the dataclass field default (including list/tuple equivalence)."""
if current == default:
return True
if isinstance(current, (list, tuple)) and isinstance(default, (list, tuple)):
if len(current) != len(default):
return False
return all(a == b for a, b in zip(current, default, strict=True))
return False


@configclass
class ViewerCfg:
"""Configuration of the scene viewport camera."""
"""Configuration of the scene viewport camera.

Note:
Overriding non-default fields is deprecated. In a future release, Isaac Sim viewport camera
configuration will be expressed only through ``KitVisualizerCfg`` under
``SimulationCfg.visualizer_cfgs``; use ``NewtonVisualizerCfg`` for the Newton viewer.
Those visualizer configs replace the viewport camera pose, resolution, prim path, and
frame-origin behavior that this class used to configure.
"""

eye: tuple[float, float, float] = (7.5, 7.5, 7.5)
"""Initial camera position (in m). Default is (7.5, 7.5, 7.5)."""
Expand Down Expand Up @@ -67,6 +88,35 @@ class ViewerCfg:
This quantity is only effective if :attr:`origin` is set to "asset_body".
"""

def __post_init__(self) -> None:
# Dataclasses do not record which arguments were passed explicitly vs defaulted, and
# warning only on ``**kwargs`` would miss positional arguments. Comparing each field to
# its declared default catches any non-default effective configuration (including
# ``replace()`` and ``from_dict``), while keeping ``ViewerCfg()`` silent.
differing: list[str] = []
for f in fields(self):
if not f.init:
continue
if f.default is not MISSING:
default_val = f.default
elif f.default_factory is not MISSING:
default_val = f.default_factory()
else:
continue
if not _viewer_cfg_value_matches_default(getattr(self, f.name), default_val):
differing.append(f.name)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Warning: The __post_init__ deprecation check fires on every ViewerCfg construction, including internal framework code that may use non-default values intentionally (e.g. task configs, test fixtures). The stacklevel=2 may not always point to user code — for configclass-decorated classes, the actual call stack through from_dict() or replace() can be deeper.

Consider:

  1. Using stacklevel=3 or computing it dynamically to ensure the warning points at the user's config file, not framework internals.
  2. Adding a _suppress_deprecation escape hatch for internal usage (e.g. ViewerCfg(_suppress_deprecation=True)).

if differing:
warnings.warn(
"ViewerCfg is deprecated when overriding default viewport camera fields "
f"({', '.join(sorted(differing))}). In a future release, Isaac Sim viewport camera "
"settings will be configured only through ``SimulationCfg.visualizer_cfgs`` using "
"``KitVisualizerCfg`` (viewport camera pose, resolution, prim path, and "
"frame-origin options). For the Newton viewer, use ``NewtonVisualizerCfg``. "
"Migrate overrides out of ``ViewerCfg`` accordingly.",
DeprecationWarning,
stacklevel=2,
)


##
# Types.
Expand Down
8 changes: 4 additions & 4 deletions source/isaaclab/isaaclab/envs/direct_marl_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,10 +151,10 @@ def _init_sim(self, render_mode: str | None = None, **kwargs):
# viewport is not available in other rendering modes so the function will throw a warning
# FIXME: This needs to be fixed in the future when we unify the UI functionalities even for
# non-rendering modes.
# Initialize when GUI is available OR when visualizers are active (headless rendering)
# Visualizers support camera updates via sim.set_camera_view() which forwards to all active visualizers
has_visualizers = bool(self.sim.get_setting("/isaaclab/visualizer"))
if self.sim.has_gui or has_visualizers:
# Initialize when a Kit viewport exists. ViewportCameraController uses omni.kit (renderer camera);
# skip in kitless Newton-only runs (e.g. --viz rerun) where no Kit app is running.
has_visualizers = self.sim.has_active_visualizers()
if (self.sim.has_gui or has_visualizers) and has_kit():
self.viewport_camera_controller = ViewportCameraController(self, self.cfg.viewer)
else:
self.viewport_camera_controller = None
Expand Down
8 changes: 4 additions & 4 deletions source/isaaclab/isaaclab/envs/direct_rl_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,10 @@ def _init_sim(self, render_mode: str | None = None, **kwargs):
# viewport is not available in other rendering modes so the function will throw a warning
# FIXME: This needs to be fixed in the future when we unify the UI functionalities even for
# non-rendering modes.
# Initialize when GUI is available OR when visualizers are active (headless rendering)
# Visualizers support camera updates via sim.set_camera_view() which forwards to all active visualizers
has_visualizers = bool(self.sim.get_setting("/isaaclab/visualizer"))
if self.sim.has_gui or has_visualizers:
# Initialize when a Kit viewport exists. ViewportCameraController uses omni.kit (renderer camera);
# skip in kitless Newton-only runs (e.g. --viz rerun) where no Kit app is running.
has_visualizers = self.sim.has_active_visualizers()
if (self.sim.has_gui or has_visualizers) and has_kit():
self.viewport_camera_controller = ViewportCameraController(self, self.cfg.viewer)
else:
self.viewport_camera_controller = None
Expand Down
9 changes: 5 additions & 4 deletions source/isaaclab/isaaclab/envs/manager_based_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from isaaclab.utils.configclass import resolve_cfg_presets
from isaaclab.utils.seed import configure_seed
from isaaclab.utils.timer import Timer
from isaaclab.utils.version import has_kit

from .common import VecEnvObs
from .manager_based_env_cfg import ManagerBasedEnvCfg
Expand Down Expand Up @@ -166,10 +167,10 @@ def _init_sim(self):
# viewport is not available in other rendering modes so the function will throw a warning
# FIXME: This needs to be fixed in the future when we unify the UI functionalities even for
# non-rendering modes.
# Initialize when GUI is available OR when visualizers are active (headless rendering)
# Visualizers support camera updates via sim.set_camera_view() which forwards to all active visualizers
has_visualizers = bool(self.sim.get_setting("/isaaclab/visualizer"))
if self.sim.has_gui or has_visualizers:
# Initialize when a Kit viewport exists. ViewportCameraController uses omni.kit (renderer camera);
# skip in kitless Newton-only runs (e.g. --viz rerun) where no Kit app is running.
has_visualizers = self.sim.has_active_visualizers()
if (self.sim.has_gui or has_visualizers) and has_kit():
self.viewport_camera_controller = ViewportCameraController(self, self.cfg.viewer)
else:
self.viewport_camera_controller = None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,11 @@ def update_view_location(self, eye: Sequence[float] | None = None, lookat: Seque
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)
# set the renderer viewport camera view (does not broadcast to visualizers)
Comment thread
matthewtrepte marked this conversation as resolved.
# Kit-specific helper lives in isaaclab_physx
from isaaclab_physx.renderers.kit_viewport_utils import set_kit_renderer_camera_view
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Warning: This introduces a hard import from isaaclab (core) → isaaclab_physx (backend-specific package). ViewportCameraController lives in the core package but now depends on isaaclab_physx.renderers.kit_viewport_utils at call time.

This is mitigated by the has_kit() guard at the env level (the controller is only created when Kit is present), but the import will raise ModuleNotFoundError in a pure-Newton deployment that somehow reaches this code path. The previous self._env.sim.set_camera_view() was backend-agnostic.

Consider wrapping with a fallback:

try:
    from isaaclab_physx.renderers.kit_viewport_utils import set_kit_renderer_camera_view
    set_kit_renderer_camera_view(eye=cam_eye, target=cam_target, camera_prim_path=self.cfg.cam_prim_path)
except (ImportError, ModuleNotFoundError):
    pass  # Kit-only; Newton-only deployments skip viewport camera


set_kit_renderer_camera_view(eye=cam_eye, target=cam_target, camera_prim_path=self.cfg.cam_prim_path)

"""
Private Functions
Expand Down
50 changes: 50 additions & 0 deletions source/isaaclab/isaaclab/envs/utils/recording_hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# 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

"""Hooks that run after visualizers during :meth:`~isaaclab.sim.SimulationContext.render`.

Lives alongside :mod:`video_recorder` / :mod:`video_recorder_cfg` because both tie into
``--video`` / ``rgb_array`` recording. Keeps :class:`~isaaclab.sim.SimulationContext` free
of imports from ``isaaclab_physx``, ``isaaclab_newton``, and other recording backends.
Each integration is loaded lazily so optional extensions are not required at import time.
"""

from __future__ import annotations

from typing import Any


def run_recording_hooks_after_visualizers(sim: Any) -> None:
"""Run recording-related work after :meth:`~isaaclab.sim.SimulationContext.render` steps visualizers.

Isaac Sim / RTX follow-up is loaded lazily so minimal installs still work.
Newton GL video is handled by :class:`~isaaclab.envs.utils.video_recorder.VideoRecorder`
(e.g. :class:`~isaaclab_newton.video_recording.newton_gl_perspective_video.NewtonGlPerspectiveVideo`),
not here.

Args:
sim: Active :class:`~isaaclab.sim.SimulationContext` instance.
"""
_recording_followup_isaac_sim(sim)


def _recording_followup_isaac_sim(sim: Any) -> None:
"""Isaac Sim: keep RTX / Replicator outputs fresh when recording video without a Kit visualizer.

When ``--video`` uses ``rgb_array`` / :class:`~gymnasium.wrappers.RecordVideo`, Replicator
render products must see Kit's event loop pumped. :class:`~isaaclab_visualizers.kit.KitVisualizer`
already calls ``omni.kit.app.get_app().update()`` in its ``step()``; if no such visualizer
is active, we pump here (guarded by ``/isaaclab/video/enabled`` and ``is_rendering``).

Implemented by ``pump_kit_app_for_headless_video_render_if_needed`` in
:mod:`isaaclab_physx.renderers.isaac_rtx_renderer_utils`.
"""
try:
from isaaclab_physx.renderers.isaac_rtx_renderer_utils import (
pump_kit_app_for_headless_video_render_if_needed,
)
except ImportError:
return
pump_kit_app_for_headless_video_render_if_needed(sim)
10 changes: 10 additions & 0 deletions source/isaaclab/isaaclab/physics/physics_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,16 @@ def pre_render(cls) -> None:
"""
pass

@classmethod
def after_visualizers_render(cls) -> None:
"""Hook after visualizers have stepped during :meth:`~isaaclab.sim.SimulationContext.render`.

Use for physics-backend sync (e.g. fabric) if needed. Recording pipelines (Kit/RTX,
Newton GL video, etc.) run from :mod:`isaaclab.envs.utils.recording_hooks` so they are not
tied to a specific physics manager. Default is a no-op.
"""
pass

@classmethod
def close(cls) -> None:
"""Clean up physics resources.
Expand Down
Loading
Loading