> This page is for version Release 3.0 (default).
> For other versions, use one of these documentation indexes:
> - Release 3.0 (default): https://docs.nvidia.com/isaaclab/3.0/llms.txt
> - Release 2.3: https://docs.nvidia.com/isaaclab/2.3/llms.txt
> - Release 2.2: https://docs.nvidia.com/isaaclab/2.2/llms.txt
> - Release 2.1: https://docs.nvidia.com/isaaclab/2.1/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.nvidia.com/isaaclab/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nvidia.com/isaaclab/_mcp/server.

# Streaming a camera view in a visualizer

This guide shows how to show ground-truth camera frames from many environments in one live
panel of a visualizer. For how the view works and which visualizers support it, see
[Streaming Camera View](https://isaac-sim.github.io/IsaacLab/release/3.0.0/source/concepts/visualization.html#streaming-camera-view).

## Quick Start

Run the `run_tiled_camera_visualizer.py` script in `IsaacLab/scripts/tutorials/07_visualizers`:

#### Linux

```bash
uv run python scripts/tutorials/07_visualizers/run_tiled_camera_visualizer.py \
    --task Isaac-Velocity-Rough-AnymalD --num_envs 256 --viz kit
```

#### Windows

```batch
uv run python scripts/tutorials/07_visualizers/run_tiled_camera_visualizer.py ^
    --task Isaac-Velocity-Rough-AnymalD --num_envs 256 --viz kit
```

```python
# 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

"""
This script demonstrates the visualizer tiled camera panel.

~~~bash
   # Kit visualizer tiled camera panel
   uv run python scripts/tutorials/07_visualizers/run_tiled_camera_visualizer.py \
--task Isaac-Velocity-Rough-AnymalD --num_envs 256 --viz kit

   # Newton visualizer tiled camera panel
   uv run python scripts/tutorials/07_visualizers/run_tiled_camera_visualizer.py \
       --task IsaacContrib-Stack-Cube-Galbot-Left-Arm-Gripper-Visuomotor --num_envs 25 --viz newton_gl
~~~

"""

from __future__ import annotations

import argparse
import contextlib
import sys

import gymnasium as gym
import torch

import isaaclab_tasks  # noqa: F401

with contextlib.suppress(ImportError):
    import isaaclab_tasks_experimental  # noqa: F401
from isaaclab.app import add_launcher_args, launch_simulation

from isaaclab_tasks.utils import resolve_task_config, setup_preset_cli

KIT_DEFAULT_TASK = "Isaac-Velocity-Rough-AnymalD"
NEWTON_DEFAULT_TASK = "IsaacContrib-Stack-Cube-Galbot-Left-Arm-Gripper-Visuomotor"
SUPPORTED_TILED_VISUALIZERS = {"kit", "newton", "newton_gl", "newton_rtx"}
UNSUPPORTED_TILED_VISUALIZERS = {"rerun", "viser"}

def _resolve_env_regex_path(prim_path: str) -> str:
    """Resolve scene config env namespace macros to the cloned-env regex."""
    return prim_path.format(ENV_REGEX_NS="/World/envs/env_.*")

def _requested_visualizers(args_cli: argparse.Namespace) -> list[str]:
    """Return requested visualizers, defaulting to Kit for this tutorial."""
    visualizers = args_cli.visualizer or ["kit"]
    visualizers = [str(visualizer).lower() for visualizer in visualizers]

    if "none" in visualizers:
        raise ValueError("This demo requires a tiled-camera visualizer. Use '--viz kit' or '--viz newton_gl'.")
    unsupported = sorted(set(visualizers) & UNSUPPORTED_TILED_VISUALIZERS)
    if unsupported:
        raise ValueError(
            "The visualizer tiled camera panel is only implemented for Kit and Newton. "
            f"Unsupported selection: {unsupported}."
        )
    unknown = sorted(set(visualizers) - SUPPORTED_TILED_VISUALIZERS)
    if unknown:
        raise ValueError(f"Unknown visualizer selection for this demo: {unknown}.")
    return visualizers

def _make_kit_visualizer_cfg(env_cfg):
    """Create the Kit streaming-camera visualizer for the selected task."""
    from isaaclab_visualizers.kit import KitVisualizerCfg

    visualizer_cfg = KitVisualizerCfg()
    visualizer_cfg.streaming_view = True
    visualizer_cfg.streaming_envs = 36

    ego_cam_cfg = getattr(env_cfg.scene, "ego_cam", None)
    if ego_cam_cfg is not None:
        visualizer_cfg.streaming_sensor_prim_path = _resolve_env_regex_path(ego_cam_cfg.prim_path)
        return visualizer_cfg

    visualizer_cfg.streaming_sensor_prim_path = None
    visualizer_cfg.streaming_cam_eye = (3.0, 3.0, 3.0)
    visualizer_cfg.streaming_cam_target_prim_path = "/World/envs/*/Robot/base"
    # Here is an alternative eye position for a top down view
    # visualizer_cfg.streaming_cam_eye = (0.0, 0.0, 5.0)
    return visualizer_cfg

def _make_newton_visualizer_cfg(env_cfg):
    """Create the Newton streaming-camera visualizer for the selected task."""
    from isaaclab_visualizers.newton import NewtonGLVisualizerCfg

    visualizer_cfg = NewtonGLVisualizerCfg()
    visualizer_cfg.streaming_view = True
    visualizer_cfg.streaming_envs = 12

    ego_cam_cfg = getattr(env_cfg.scene, "ego_cam", None)
    if ego_cam_cfg is not None:
        visualizer_cfg.streaming_sensor_prim_path = _resolve_env_regex_path(ego_cam_cfg.prim_path)
        return visualizer_cfg

    # Here are other robot mounted camera options for this environment
    # visualizer_cfg.streaming_sensor_prim_path = "/World/envs/env_.*/Robot/left_arm_camera_sim_view_frame/left_camera"
    # visualizer_cfg.streaming_sensor_prim_path = (
    #     "/World/envs/env_.*/Robot/right_arm_camera_sim_view_frame/right_camera"
    # )
    visualizer_cfg.streaming_sensor_prim_path = None
    visualizer_cfg.streaming_cam_eye = (3.0, 3.0, 3.0)
    visualizer_cfg.streaming_cam_target_prim_path = "/World/envs/*/Robot/base"
    return visualizer_cfg

def _configure_visualizers(env_cfg, args_cli: argparse.Namespace) -> None:
    """Attach tiled camera visualizer configs to the environment simulation config."""
    visualizers = _requested_visualizers(args_cli)
    args_cli.visualizer = visualizers
    env_cfg.sim.visualizer_cfgs = [
        _make_kit_visualizer_cfg(env_cfg) if visualizer == "kit" else _make_newton_visualizer_cfg(env_cfg)
        for visualizer in visualizers
    ]

def _resolve_task(args_cli: argparse.Namespace) -> str:
    """Resolve the task for the selected visualizer."""
    if args_cli.task is not None:
        return args_cli.task
    if "newton" in _requested_visualizers(args_cli):
        return NEWTON_DEFAULT_TASK
    return KIT_DEFAULT_TASK

# add argparse arguments
parser = argparse.ArgumentParser(description="Showcase the Kit/Newton visualizer tiled camera panel.")
parser.add_argument("--num_envs", type=int, default=None, help="Number of environments to simulate.")
parser.add_argument("--task", type=str, default=None, help="Name of the task.")
# append simulation launcher cli args
add_launcher_args(parser)
args_cli, hydra_args = setup_preset_cli(parser)
args_cli.task = _resolve_task(args_cli)
sys.argv = [sys.argv[0]] + hydra_args

def main():
    """Run a random-action environment with a tiled camera visualizer."""
    # parse configuration via Hydra (supports preset selection, e.g. presets=newton_mjwarp)
    env_cfg, _ = resolve_task_config(args_cli.task, "")
    _configure_visualizers(env_cfg, args_cli)

    with launch_simulation(env_cfg, args_cli):
        # override with CLI arguments
        env_cfg.scene.num_envs = args_cli.num_envs if args_cli.num_envs is not None else env_cfg.scene.num_envs
        env_cfg.sim.device = args_cli.device if args_cli.device is not None else env_cfg.sim.device

        # create environment
        env = gym.make(args_cli.task, cfg=env_cfg)

        # print info (this is vectorized environment)
        print(f"[INFO]: Gym observation space: {env.observation_space}")
        print(f"[INFO]: Gym action space: {env.action_space}")
        env.reset()

        # keep stepping until all visualizer windows have been closed
        sim = env.unwrapped.sim
        if not sim.visualizers:
            print("[WARN]: No visualizers found. Exiting.")
            env.close()
            return

        while True:
            if sim.visualizers and not any(v.is_running() and not v.is_closed for v in sim.visualizers):
                break
            with torch.inference_mode():
                actions = 2 * torch.rand(env.action_space.shape, device=env.unwrapped.device) - 1
                env.step(actions)

        env.close()

if __name__ == "__main__":
    main()
```

See `Examples`\_ below for the two ways to run the script. For the `VisualizerCfg` fields that
customize streaming, see `Configuration`\_ and `Troubleshooting`\_.

## Examples

Running `run_tiled_camera_visualizer.py` demonstrates two ways to use the streaming camera
view:

* auto-created cameras pointed at and following moving AnymalD robots, shown in the Kit visualizer
* streaming from existing wrist-mounted robot cameras, shown in the Newton visualizer

### Example 1: Following AnymalD Robots

#### Linux

```bash
uv run python scripts/tutorials/07_visualizers/run_tiled_camera_visualizer.py \
    --task Isaac-Velocity-Rough-AnymalD --num_envs 256 --viz kit
```

#### Windows

```batch
uv run python scripts/tutorials/07_visualizers/run_tiled_camera_visualizer.py ^
    --task Isaac-Velocity-Rough-AnymalD --num_envs 256 --viz kit
```

The script's `KitVisualizerCfg` creates cameras that point at and follow each robot's base
prim, offset by `streaming_cam_eye` (here `(3.0, 3.0, 3.0)`; try `(0, 0, 5)` for a
top-down view). Of the 256 environments, 36 are randomly sampled for the camera view.

[Watch the embedded video](https://download.isaacsim.omniverse.nvidia.com/isaaclab/images/streaming_kit_anymal_interactive.mp4)

[Watch the embedded video](https://download.isaacsim.omniverse.nvidia.com/isaaclab/images/streaming_kit_anymal_tiled.mp4)

### Example 2: Streaming from Robot-Mounted Cameras

#### Linux

```bash
uv run --extra teleop python scripts/tutorials/07_visualizers/run_tiled_camera_visualizer.py \
    --task IsaacContrib-Stack-Cube-Galbot-Left-Arm-Gripper-Visuomotor --num_envs 25 --viz newton_gl
```

#### Windows

```batch
uv run --extra teleop python scripts/tutorials/07_visualizers/run_tiled_camera_visualizer.py ^
    --task IsaacContrib-Stack-Cube-Galbot-Left-Arm-Gripper-Visuomotor --num_envs 25 --viz newton_gl
```

The Galbot cube-stacking environment ships with wrist-mounted cameras giving an egocentric
view of the gripper, table, and cubes. The script's `NewtonGLVisualizerCfg` streams from the
existing sensor at `/World/envs/env_.*/Robot/head_camera_sim_view_frame/head_camera`; edit
`streaming_sensor_prim_path` to show a different camera. Of the 25 environments, 12 camera
feeds are shown by default.

[Watch the embedded video](https://download.isaacsim.omniverse.nvidia.com/isaaclab/images/streaming_newton_galbot_interactive.mp4)

[Watch the embedded video](https://download.isaacsim.omniverse.nvidia.com/isaaclab/images/streaming_newton_galbot_tiled.mp4)

## Configuration

To customize streaming camera behavior, edit the highlighted `VisualizerCfg` fields in
`run_tiled_camera_visualizer.py`:

* For auto-created cameras, `streaming_cam_target_prim_path` chooses the followed prim and
  `streaming_cam_eye` sets the camera offset from that prim.  Defaults to `None`, which
  causes the visualizer to adopt the first scene camera it discovers at init; no explicit path
  is needed when a `TiledCamera` sensor is already in the scene.
* For existing scene cameras, `streaming_sensor_prim_path` must match an Isaac Lab
  `~isaaclab.sensors.Camera` sensor prim path in the selected task.
* `streaming_envs` controls how many environment tiles are shown. Pass an `int` to randomly
  sample that many environments, or a `list[int]` to pin specific environment indices.
* `streaming_gt_types` selects which ground-truth types are shown, e.g.
  `["rgb", "depth", "segmentation", "normals"]`.
* `streaming_depth_min` / `streaming_depth_max` set the depth colormap range in metres.

## Troubleshooting

* If a generated view fails with a missing prim error, verify that
  `streaming_cam_target_prim_path` resolves in each selected environment; common template
  forms are `/World/envs/*/...` and `/World/envs/env_.*/...`.  In most cases you can leave
  it as `None` and let the visualizer adopt an existing scene camera automatically.
* If an existing-camera view reports that no Isaac Lab camera owns the prim, check that
  `streaming_sensor_prim_path` matches a `~isaaclab.sensors.Camera` sensor in the task.
* If the depth panel shows a flat color, adjust `streaming_depth_min` and
  `streaming_depth_max` to bracket the expected depth range in your scene.
* If the view is too expensive, reduce `streaming_envs`, `--num_envs`, or the camera
  resolution.

> **Warning**
>
> **Newton MJWarp with** `replicate_physics=True` **and auto-created cameras**
>
> With `replicate_physics=True`, only `env_0` has a USD prim after physics
> initialization. Cameras for the remaining environments (`env_1` through `env_{N-1}`)
> are dropped, causing initialization to fail:
>
> ```
> RuntimeError: Number of camera prims in the view (1) does not match
> the number of environments (N).
> ```
>
> **Workaround**: set `streaming_sensor_prim_path` to a scene camera that was declared in
> the scene config before physics init (for example, a `TiledCamera` on a vision-based
> task).

## See also

* [Streaming Camera View](https://isaac-sim.github.io/IsaacLab/release/3.0.0/source/concepts/visualization.html#streaming-camera-view): how the streaming view works and which visualizers support it
* [https://isaac-sim.github.io/IsaacLab/release/3.0.0/source/concepts/visualization.html](https://isaac-sim.github.io/IsaacLab/release/3.0.0/source/concepts/visualization.html): visualizer configuration and UI controls
* [/source/how-to/record_video](/source/how-to/record_video): recording the streaming view to video
* [/source/how-to/configure_rendering](/source/how-to/configure_rendering): customizing RTX rendering settings