> 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.

# Wrapping environments

Environment wrappers are a way to modify the behavior of an environment without modifying the environment itself.
This can be used to apply functions to modify observations or rewards, record videos, enforce time limits, etc.
A detailed description of the API is available in the `gymnasium.Wrapper` class.

At present, all RL environments inheriting from the `~envs.ManagerBasedRLEnv` or `~envs.DirectRLEnv` classes
are compatible with `gymnasium.Wrapper`, since the base class implements the `gymnasium.Env` interface.
In order to wrap an environment, you need to first initialize the base environment. After that, you can
wrap it with as many wrappers as you want by calling `env = wrapper(env, *args, **kwargs)` repeatedly.

For example, here is how you would wrap an environment to enforce that reset is called before step or render:

```python
import gymnasium as gym

from isaaclab.app import launch_simulation

import isaaclab_tasks  # noqa: F401
from isaaclab_tasks.utils import load_cfg_from_registry

cfg = load_cfg_from_registry("Isaac-Reach-Franka", "env_cfg_entry_point")
# start the simulation runtime this configuration needs, and stop it on exit
with launch_simulation(cfg):
    # create base environment
    env = gym.make("Isaac-Reach-Franka", cfg=cfg)
    # wrap environment to enforce that reset is called before step
    env = gym.wrappers.OrderEnforcing(env)
```

## Wrapper for recording videos

The `gymnasium.wrappers.RecordVideo` wrapper can be used to record videos of the environment.
The wrapper takes a `video_dir` argument, which specifies where to save the videos. The videos are saved in
[mp4](https://en.wikipedia.org/wiki/MP4_file_format) format at specified intervals for specified
number of environment steps or episodes.

To use the wrapper, you need to first install `ffmpeg`. On Ubuntu, you can install it by running:

```bash
sudo apt-get install ffmpeg
```

> **Warning**
>
> By default, when running an environment in headless mode, the Omniverse viewport is disabled. This is done to
> improve performance by avoiding unnecessary rendering.
>
> We notice the following performance in different rendering modes with the  `Isaac-Reach-Franka` environment
> using an RTX 3090 GPU:
>
> * No GUI execution without off-screen rendering enabled: \~65,000 FPS
> * No GUI execution with off-screen enabled: \~57,000 FPS
> * GUI execution with full rendering: \~13,000 FPS

The viewport camera used for rendering is the default camera in the scene called `"/OmniverseKit_Persp"`.
The camera's pose and image resolution can be configured through the
`~envs.ViewerCfg` class.

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

<Warning>

`ViewerCfg` is deprecated and will be removed in a future release.
Configure the viewport camera via `~isaaclab_visualizers.kit.KitVisualizerCfg`
and add it to `isaaclab.sim.SimulationCfg.visualizer_cfgs` instead::

from isaaclab.sim import SimulationCfg
from isaaclab_visualizers.kit import KitVisualizerCfg

sim_cfg = SimulationCfg(visualizer_cfgs=[KitVisualizerCfg(eye=(7.5, 7.5, 7.5), lookat=(0.0, 0.0, 0.0))])

</Warning>

    """

    eye: tuple[float, float, float] = (7.5, 7.5, 7.5)
    """Initial camera position (in m). Default is (7.5, 7.5, 7.5)."""

    lookat: tuple[float, float, float] = (0.0, 0.0, 0.0)
    """Initial camera target position (in m). Default is (0.0, 0.0, 0.0)."""

    cam_prim_path: str = "/OmniverseKit_Persp"
    """The camera prim path to record images from. Default is "/OmniverseKit_Persp"."""

    resolution: tuple[int, int] = (1280, 720)
    """The resolution (width, height) of the camera. Default is (1280, 720)."""

    origin_type: Literal["world", "env", "asset_root", "asset_body"] = "world"
    """The frame in which the camera position (eye) and target (lookat) are defined. Default is "world"."""

    env_index: int = 0
    """The environment index for frame origin. Default is 0."""

    asset_name: str | None = None
    """The asset name in the interactive scene for the frame origin. Default is None."""

    body_name: str | None = None
    """The name of the body in `asset_name` for the frame origin. Default is None."""

    def __post_init__(self) -> None:
        # Warn only when the user configured a non-default field so that bare `ViewerCfg()`
        # usage (e.g. in task configs that haven't been migrated yet) stays silent.
        #
        # @configclass stores mutable defaults (tuples, lists) via default_factory rather than
        # default, so we must check both to obtain the canonical default value.
        differing = []
        for f in fields(self):
            if not f.init:
                continue
            if f.default is not MISSING:
                default = f.default
            elif f.default_factory is not MISSING:  # type: ignore[misc]
                default = f.default_factory()  # type: ignore[misc]
            else:
                continue
            if not _viewer_cfg_field_matches_default(getattr(self, f.name), default):
                differing.append(f.name)
        if differing:
            warnings.warn(
                "ViewerCfg is deprecated and will be removed in a future release. "
                "Use KitVisualizerCfg added to SimulationCfg.visualizer_cfgs instead.",
                DeprecationWarning,
                stacklevel=2,
            )
```

To record videos, add a `~envs.utils.video_recorder_cfg.VideoRecorderCfg` to the environment
configuration. It records from a visualizer (or a camera sensor) into `mp4` clips while the environment steps,
so no wrapper or `render_mode` is needed, and the runtime the visualizer needs starts automatically.

As an example, the following code records 200-step clips of the `Isaac-Reach-Franka` environment from a Kit
visualizer every 1500 steps into the `videos/train` folder. See [Recording video](/source/how-to/record_video) for the other
sources and clip options.

```python
import gymnasium as gym

from isaaclab_visualizers.kit import KitVisualizerCfg

from isaaclab.app import launch_simulation
from isaaclab.envs.utils.video_recorder_cfg import VideoRecorderCfg

# record from a Kit visualizer with this camera pose
env_cfg.sim.visualizer_cfgs = KitVisualizerCfg(eye=(1.0, 1.0, 1.0), lookat=(0.0, 0.0, 0.0))
env_cfg.video_recorders = [
    VideoRecorderCfg(source="visualizer:kit", output_dir="videos/train", video_length=200, video_interval=1500)
]
with launch_simulation(env_cfg):
    env = gym.make(task_name, cfg=env_cfg)
```

## Wrapper for learning frameworks

Every learning framework has its own API for interacting with environments. For example, the
[Stable-Baselines3](https://stable-baselines3.readthedocs.io/en/master/) library uses the [gym.Env](https://gymnasium.farama.org/api/env/)
interface to interact with environments. However, libraries like [RL-Games](https://github.com/Denys88/rl_games), [RSL-RL](https://github.com/leggedrobotics/rsl_rl) or [SKRL](https://skrl.readthedocs.io)
use their own API for interfacing with a learning environments. Since there is no one-size-fits-all
solution, we do not base the `~envs.ManagerBasedRLEnv` and `~envs.DirectRLEnv` classes on any particular learning framework's
environment definition. Instead, we implement wrappers to make it compatible with the learning
framework's environment definition.

As an example of how to use the RL task environment with Stable-Baselines3:

```python
from isaaclab_rl.sb3 import Sb3VecEnvWrapper

# create isaac-env instance
env = gym.make(task_name, cfg=env_cfg)
# wrap around environment for stable baselines
env = Sb3VecEnvWrapper(env)
```

Stable-Baselines3 requires finite continuous action bounds. When the environment has an unbounded action
space, `~isaaclab_rl.sb3.Sb3VecEnvWrapper` exposes normalized `[-1, 1]` bounds to Stable-Baselines3
without changing the underlying environment. Set `action_bounds` when the policy uses a different
finite action domain.

> **Warning**
>
> Wrapping the environment with the respective learning framework's wrapper should happen in the end,
> i.e. after all other wrappers have been applied. This is because the learning framework's wrapper
> modifies the interpretation of environment's APIs which may no longer be compatible with `gymnasium.Env`.

## Adding new wrappers

All new wrappers should be added to the `isaaclab_rl` module.
They should check that the underlying environment is an instance of `isaaclab.envs.ManagerBasedRLEnv`
or `~envs.DirectRLEnv`
before applying the wrapper. This can be done by using the `unwrapped` property.

We include a set of wrappers in this module that can be used as a reference to implement your own wrappers.
If you implement a new wrapper, please consider contributing it to the framework by opening a pull request.