Skip to content

Commit 63da1f4

Browse files
Improve Partial Visuallization (isaac-sim#5347)
# Description <!-- Thank you for your interest in sending a pull request. Please make sure to check the contribution guidelines. Link: https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html 💡 Please try to keep PRs small and focused. Large PRs are harder to review and merge. --> Improve Partial Visualization feature where a subset of envs are shown in the launched visualizers to speed up performance, making use of new updates to Newton. Newton visualizers align on using the set_visible_worlds API to select envs to show. Kit visualizer just sets non selected envs invisible. Updated docs and added tests. <!-- As a practice, it is recommended to open an issue to have discussions on the proposed pull request. This makes it easier for the community to keep track of what is being developed or added, and if a given feature is demanded by more than one party. --> ## Type of change <!-- As you go through the list, delete the ones that are not applicable. --> - New feature (non-breaking change which adds functionality) - Documentation update ## Screenshots Please attach before and after screenshots of the change if applicable. <!-- Example: | Before | After | | ------ | ----- | | _gif/png before_ | _gif/png after_ | To upload images to a PR -- simply drag and drop an image while in edit mode and it should upload the image directly. You can then paste that source into the above before/after sections. --> ## Checklist - [ ] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [ ] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [ ] I have added my name to the `CONTRIBUTORS.md` or my name already exists there <!-- As you go through the checklist above, you can mark something as done by putting an x character in it For example, - [x] I have done this task - [ ] I have not done this task --> --------- Signed-off-by: matthewtrepte <mtrepte@nvidia.com>
1 parent b655176 commit 63da1f4

26 files changed

Lines changed: 693 additions & 748 deletions

docs/source/features/visualization.rst

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,21 @@ The effective visualizer mode is resolved from both CLI and ``SimulationCfg.visu
139139
For the migration-focused summary and deprecation context, see
140140
:doc:`/source/migration/migrating_to_isaaclab_3-0`.
141141

142+
Partial Visualization
143+
~~~~~~~~~~~~~~~~~~~~~
144+
145+
Visualizers can be configured to visualize just a subset of environments.
146+
This is called partial visualization.
147+
148+
There are 3 fields exposed in the ``VisualizerCfg`` for selecting environments for partial visualization:
149+
150+
- ``max_visible_envs`` caps how many envs are shown.
151+
- ``visible_env_indices`` explicitly selects the envs to visualize.
152+
- ``randomly_sample_visible_envs`` (default ``True``): when ``visible_env_indices`` is unset and ``max_visible_envs`` is set,
153+
enables randomly sampling the selected envs. If disabled, the first ``max_visible_envs`` envs are selected.
154+
155+
Also, there is a CLI arg ``--max_visible_envs`` that overrides ``VisualizerCfg.max_visible_envs`` for the run.
156+
142157
.. _visualization-common-modes:
143158

144159
.. list-table:: Common modes
@@ -345,7 +360,7 @@ server, allowing you to view and interact with the scene from any browser.
345360
open_browser=True,
346361
label="Isaac Lab Simulation",
347362
share=False,
348-
max_worlds=64,
363+
max_visible_envs=16,
349364
)
350365
351366
**Configuration options:**
@@ -356,7 +371,6 @@ server, allowing you to view and interact with the scene from any browser.
356371
- ``share`` (bool, default ``False``): Request a public share URL from Viser for remote viewing.
357372
- ``record_to_viser`` (str or None, default ``None``): Path to save a ``.viser`` recording file.
358373
- ``verbose`` (bool, default ``True``): Print viewer server startup information.
359-
- ``max_worlds`` (int or None, default ``None``): Maximum number of environments rendered.
360374

361375
.. note::
362376

@@ -366,7 +380,7 @@ server, allowing you to view and interact with the scene from any browser.
366380
Performance Note
367381
----------------
368382

369-
To reduce overhead when visualizing large-scale environments, consider:
383+
When visualizing large-scale environments, consider:
370384

371385
- Using Newton instead of Omniverse or Rerun
372386
- Reducing window sizes

docs/source/overview/core-concepts/scene_data_providers.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ The system has three layers:
2727

2828
1. **BaseSceneDataProvider** — abstract interface defining the contract:
2929

30-
- ``update(env_ids)`` — refresh cached scene data
30+
- ``update()`` — refresh cached scene data (full Newton model/state sync when applicable)
3131
- ``get_newton_model()`` — return Newton model handle (if available)
32-
- ``get_newton_state(env_ids)`` — return Newton state handle (if available)
32+
- ``get_newton_state()`` — return Newton state handle (if available)
3333
- ``get_usd_stage()`` — return USD stage handle (if available)
3434
- ``get_transforms()`` — return body transforms
3535
- ``get_velocities()`` — return body velocities

source/isaaclab/isaaclab/app/app_launcher.py

Lines changed: 110 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -49,57 +49,6 @@ def __call__(self, parser, namespace, values, option_string=None):
4949
setattr(namespace, f"{self.dest}_explicit", True)
5050

5151

52-
def _parse_visualizer_csv(value: str) -> list[str]:
53-
"""Parse visualizer list from a single comma-delimited CLI token."""
54-
valid = {"kit", "newton", "rerun", "viser", "none"}
55-
token = (value or "").strip()
56-
if not token:
57-
raise argparse.ArgumentTypeError(
58-
"Invalid --visualizer value: empty string. Use a comma-separated list, e.g. --viz kit,newton."
59-
)
60-
if " " in token:
61-
raise argparse.ArgumentTypeError(
62-
"Invalid --visualizer value: spaces are not allowed. "
63-
"Use a comma-separated list without spaces, e.g. --viz kit,newton,rerun,viser."
64-
)
65-
66-
names = [item.strip().lower() for item in token.split(",")]
67-
if any(not name for name in names):
68-
raise argparse.ArgumentTypeError(
69-
"Invalid --visualizer value: empty visualizer entry detected. "
70-
"Use a comma-separated list without empty items."
71-
)
72-
invalid = [name for name in names if name not in valid]
73-
if invalid:
74-
raise argparse.ArgumentTypeError(
75-
f"Invalid --visualizer value(s): {', '.join(invalid)}. Valid options: {', '.join(sorted(valid))}."
76-
)
77-
# De-duplicate while preserving order.
78-
return list(dict.fromkeys(names))
79-
80-
81-
def _normalize_visualizer_intent(intent: Any) -> tuple[bool, bool]:
82-
"""Normalize and validate upstream config visualizer intent payload.
83-
84-
The expected schema is:
85-
``{"has_any_visualizers": bool, "has_kit_visualizer": bool}``.
86-
"""
87-
if intent is None:
88-
return False, False
89-
if not isinstance(intent, dict):
90-
raise ValueError("Invalid value for `visualizer_intent`: expected dict or None.")
91-
92-
has_any = intent.get("has_any_visualizers", False)
93-
has_kit = intent.get("has_kit_visualizer", False)
94-
if not isinstance(has_any, bool) or not isinstance(has_kit, bool):
95-
raise ValueError(
96-
"Invalid `visualizer_intent` values: expected booleans for `has_any_visualizers` and `has_kit_visualizer`."
97-
)
98-
if has_kit and not has_any:
99-
raise ValueError("Invalid `visualizer_intent`: `has_kit_visualizer=True` requires `has_any_visualizers=True`.")
100-
return has_any, has_kit
101-
102-
10352
class ExplicitTrueAction(argparse.Action):
10453
"""Custom action to track explicit use of boolean flags."""
10554

@@ -133,6 +82,96 @@ class AppLauncher:
13382
13483
"""
13584

85+
@staticmethod
86+
def sync_visualizer_cli_settings_to_carb(launcher_args: dict) -> None:
87+
"""Write visualizer CLI selection and ``--max_visible_envs`` to carb settings.
88+
89+
Callers may set ``visualizer_explicit`` / ``visualizer_disable_all`` when those values
90+
were resolved elsewhere (e.g. :class:`AppLauncher` strips flags from *launcher_args*).
91+
Otherwise ``disable_all`` is inferred from ``"none"`` in ``visualizer``.
92+
93+
Also used when Kit is skipped (see :mod:`isaaclab_tasks.utils.sim_launcher`).
94+
"""
95+
visualizers = launcher_args.get("visualizer")
96+
97+
if "max_visible_envs" in launcher_args:
98+
v = launcher_args["max_visible_envs"]
99+
if v is not None and int(v) < 0:
100+
raise ValueError(f"Invalid value for --max_visible_envs: {v}. Expected non-negative int.")
101+
102+
cli_explicit = bool(launcher_args.get("visualizer_explicit", False))
103+
if "visualizer_disable_all" in launcher_args:
104+
cli_disable_all = bool(launcher_args["visualizer_disable_all"])
105+
else:
106+
cli_disable_all = bool(cli_explicit) and visualizers is not None and "none" in visualizers
107+
108+
with contextlib.suppress(Exception):
109+
visualizer_str = " ".join(visualizers) if visualizers else ""
110+
settings = get_settings_manager()
111+
settings.set_string("/isaaclab/visualizer/types", visualizer_str)
112+
settings.set_bool("/isaaclab/visualizer/explicit", cli_explicit)
113+
settings.set_bool("/isaaclab/visualizer/disable_all", cli_disable_all)
114+
115+
# Sentinel: ``-1`` means ``--max_visible_envs`` was not passed (see ``SimulationContext``).
116+
if "max_visible_envs" in launcher_args:
117+
settings.set_int("/isaaclab/visualizer/max_visible_envs", int(launcher_args["max_visible_envs"]))
118+
else:
119+
settings.set_int("/isaaclab/visualizer/max_visible_envs", -1)
120+
121+
@staticmethod
122+
def _parse_visualizer_csv(value: str) -> list[str]:
123+
"""Parse visualizer list from a single comma-delimited CLI token."""
124+
valid = {"kit", "newton", "rerun", "viser", "none"}
125+
token = (value or "").strip()
126+
if not token:
127+
raise argparse.ArgumentTypeError(
128+
"Invalid --visualizer value: empty string. Use a comma-separated list, e.g. --viz kit,newton."
129+
)
130+
if " " in token:
131+
raise argparse.ArgumentTypeError(
132+
"Invalid --visualizer value: spaces are not allowed. "
133+
"Use a comma-separated list without spaces, e.g. --viz kit,newton,rerun,viser."
134+
)
135+
136+
names = [item.strip().lower() for item in token.split(",")]
137+
if any(not name for name in names):
138+
raise argparse.ArgumentTypeError(
139+
"Invalid --visualizer value: empty visualizer entry detected. "
140+
"Use a comma-separated list without empty items."
141+
)
142+
invalid = [name for name in names if name not in valid]
143+
if invalid:
144+
raise argparse.ArgumentTypeError(
145+
f"Invalid --visualizer value(s): {', '.join(invalid)}. Valid options: {', '.join(sorted(valid))}."
146+
)
147+
# De-duplicate while preserving order.
148+
return list(dict.fromkeys(names))
149+
150+
@staticmethod
151+
def _normalize_visualizer_intent(intent: Any) -> tuple[bool, bool]:
152+
"""Normalize and validate upstream config visualizer intent payload.
153+
154+
The expected schema is:
155+
``{"has_any_visualizers": bool, "has_kit_visualizer": bool}``.
156+
"""
157+
if intent is None:
158+
return False, False
159+
if not isinstance(intent, dict):
160+
raise ValueError("Invalid value for `visualizer_intent`: expected dict or None.")
161+
162+
has_any = intent.get("has_any_visualizers", False)
163+
has_kit = intent.get("has_kit_visualizer", False)
164+
if not isinstance(has_any, bool) or not isinstance(has_kit, bool):
165+
raise ValueError(
166+
"Invalid `visualizer_intent` values: expected booleans for `has_any_visualizers` and "
167+
"`has_kit_visualizer`."
168+
)
169+
if has_kit and not has_any:
170+
raise ValueError(
171+
"Invalid `visualizer_intent`: `has_kit_visualizer=True` requires `has_any_visualizers=True`."
172+
)
173+
return has_any, has_kit
174+
136175
def __init__(self, launcher_args: argparse.Namespace | dict | None = None, **kwargs):
137176
"""Create a `SimulationApp`_ instance based on the input settings.
138177
@@ -188,7 +227,6 @@ def __init__(self, launcher_args: argparse.Namespace | dict | None = None, **kwa
188227
self._livestream: Literal[0, 1, 2] # 0: Disabled, 1: WebRTC public, 2: WebRTC private
189228
self._offscreen_render: bool # 0: Disabled, 1: Enabled
190229
self._sim_experience_file: str # Experience file to load
191-
self._visualizer_max_worlds: int | None # Optional max worlds override for Newton-based visualizers
192230
self._video_enabled: bool # Whether --video recording is enabled
193231

194232
# Exposed to train scripts
@@ -330,10 +368,9 @@ def add_app_launcher_args(parser: argparse.ArgumentParser) -> None:
330368
- Multiple visualizers can be specified as a comma-delimited list:
331369
``--viz rerun,newton,viser``.
332370
333-
* ``visualizer_max_worlds`` (int | None): Optional global override for the maximum number of worlds
334-
rendered in Newton-based visualizers (newton, rerun, viser). If omitted, each visualizer uses its
335-
config default.
336-
371+
* ``max_visible_envs`` (int | None): Optional global override for partial visualization by capping
372+
how many environments are shown in the visualizers.
373+
More partial visualization configuration fields are available in the ``VisualizerCfg`` class.
337374
338375
.. _`WebRTC`: https://docs.isaacsim.omniverse.nvidia.com/latest/installation/manual_livestream_clients.html#isaac-sim-short-webrtc-streaming-client
339376
@@ -414,7 +451,7 @@ def add_app_launcher_args(parser: argparse.ArgumentParser) -> None:
414451
arg_group.add_argument(
415452
"--visualizer",
416453
"--viz",
417-
type=_parse_visualizer_csv,
454+
type=AppLauncher._parse_visualizer_csv,
418455
action=ExplicitAction,
419456
default=None,
420457
help="Visualizer backends to enable as CSV (e.g., kit,newton,rerun,viser).",
@@ -485,13 +522,10 @@ def add_app_launcher_args(parser: argparse.ArgumentParser) -> None:
485522
),
486523
)
487524
arg_group.add_argument(
488-
"--visualizer_max_worlds",
525+
"--max_visible_envs",
489526
type=int,
490-
default=AppLauncher._APPLAUNCHER_CFG_INFO["visualizer_max_worlds"][1],
491-
help=(
492-
"Optional global max worlds override for Newton-based visualizers (newton/rerun/viser). "
493-
"If omitted, visualizer config defaults are used."
494-
),
527+
default=argparse.SUPPRESS,
528+
help=("When set, caps the nums of envs shown in the launched visualizers."),
495529
)
496530
# special flag for backwards compatibility
497531

@@ -513,7 +547,7 @@ def add_app_launcher_args(parser: argparse.ArgumentParser) -> None:
513547
"device": ([str], "cuda:0"),
514548
"experience": ([str], ""),
515549
"rendering_mode": ([str], "balanced"),
516-
"visualizer_max_worlds": ([int, type(None)], None),
550+
"max_visible_envs": ([int, type(None)], None),
517551
}
518552
"""A dictionary of arguments added manually by the :meth:`AppLauncher.add_app_launcher_args` method.
519553
@@ -783,7 +817,9 @@ def _resolve_headless_settings(self, launcher_args: dict, livestream_arg: int, l
783817
def _resolve_visualizer_settings(self, launcher_args: dict) -> None:
784818
"""Resolve visualizer CLI semantics and normalize selection."""
785819
raw_visualizers = launcher_args.get("visualizer")
786-
cfg_has_any, cfg_has_kit = _normalize_visualizer_intent(launcher_args.pop("visualizer_intent", None))
820+
cfg_has_any, cfg_has_kit = AppLauncher._normalize_visualizer_intent(
821+
launcher_args.pop("visualizer_intent", None)
822+
)
787823
self._cfg_has_any_visualizers = cfg_has_any
788824
self._cfg_has_kit_visualizer = cfg_has_kit
789825
visualizer_explicit = bool(launcher_args.pop("visualizer_explicit", False))
@@ -793,7 +829,7 @@ def _resolve_visualizer_settings(self, launcher_args: dict) -> None:
793829
visualizer_types: list[str] = []
794830
if raw_visualizers is not None:
795831
if isinstance(raw_visualizers, str):
796-
visualizer_types = _parse_visualizer_csv(raw_visualizers)
832+
visualizer_types = AppLauncher._parse_visualizer_csv(raw_visualizers)
797833
else:
798834
visualizer_types = [str(v).strip().lower() for v in raw_visualizers if str(v).strip()]
799835

@@ -1152,28 +1188,14 @@ def _set_animation_recording_settings(self, launcher_args: dict) -> None:
11521188
settings.set_float("/isaaclab/anim_recording/stop_time", stop_time)
11531189

11541190
def _set_visualizer_settings(self, launcher_args: dict) -> None:
1155-
"""Store visualizer selection and max-worlds override in settings."""
1156-
visualizers = launcher_args.get("visualizer")
1157-
visualizer_max_worlds = launcher_args.get("visualizer_max_worlds")
1158-
1159-
if visualizer_max_worlds is not None and visualizer_max_worlds < 0:
1160-
raise ValueError(
1161-
f"Invalid value for --visualizer_max_worlds: {visualizer_max_worlds}. Expected non-negative int."
1162-
)
1163-
1164-
with contextlib.suppress(Exception):
1165-
visualizer_str = " ".join(visualizers) if visualizers else ""
1166-
settings = get_settings_manager()
1167-
cli_visualizer_explicit = getattr(self, "_cli_visualizer_explicit", False)
1168-
cli_visualizer_disable_all = getattr(self, "_cli_visualizer_disable_all", False)
1169-
settings.set_string("/isaaclab/visualizer/types", visualizer_str)
1170-
settings.set_bool("/isaaclab/visualizer/explicit", cli_visualizer_explicit)
1171-
settings.set_bool("/isaaclab/visualizer/disable_all", cli_visualizer_disable_all)
1172-
# Store as int setting where -1 means "use per-visualizer defaults".
1173-
if visualizer_max_worlds is None:
1174-
settings.set_int("/isaaclab/visualizer/max_worlds", -1)
1175-
else:
1176-
settings.set_int("/isaaclab/visualizer/max_worlds", int(visualizer_max_worlds))
1191+
"""Persist visualizer CLI flags and ``max_visible_envs`` override for :class:`SimulationContext`."""
1192+
AppLauncher.sync_visualizer_cli_settings_to_carb(
1193+
{
1194+
**launcher_args,
1195+
"visualizer_explicit": getattr(self, "_cli_visualizer_explicit", False),
1196+
"visualizer_disable_all": getattr(self, "_cli_visualizer_disable_all", False),
1197+
}
1198+
)
11771199

11781200
def _interrupt_signal_handle_callback(self, signal, frame):
11791201
"""Handle the interrupt signal from the keyboard."""

source/isaaclab/isaaclab/envs/common.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,8 @@ class ViewerCfg:
3535
"""Configuration of the scene viewport camera.
3636
3737
Note:
38-
Overriding non-default fields is deprecated. In a future release, Isaac Sim viewport camera
39-
configuration will be expressed only through ``KitVisualizerCfg`` under
40-
``SimulationCfg.visualizer_cfgs``; use ``NewtonVisualizerCfg`` for the Newton viewer.
41-
Those visualizer configs replace the viewport camera pose, resolution, prim path, and
42-
frame-origin behavior that this class used to configure.
38+
ViewerCfg is deprecated. In a future release, this config will be streamlined with
39+
the KitVisualizerCfg.
4340
"""
4441

4542
eye: tuple[float, float, float] = (7.5, 7.5, 7.5)
@@ -107,12 +104,8 @@ def __post_init__(self) -> None:
107104
differing.append(f.name)
108105
if differing:
109106
warnings.warn(
110-
"ViewerCfg is deprecated when overriding default viewport camera fields "
111-
f"({', '.join(sorted(differing))}). In a future release, Isaac Sim viewport camera "
112-
"settings will be configured only through ``SimulationCfg.visualizer_cfgs`` using "
113-
"``KitVisualizerCfg`` (viewport camera pose, resolution, prim path, and "
114-
"frame-origin options). For the Newton viewer, use ``NewtonVisualizerCfg``. "
115-
"Migrate overrides out of ``ViewerCfg`` accordingly.",
107+
"ViewerCfg is deprecated. In a future release, this config will be streamlined with "
108+
"the KitVisualizerCfg.",
116109
DeprecationWarning,
117110
stacklevel=2,
118111
)

source/isaaclab/isaaclab/physics/base_scene_data_provider.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ class BaseSceneDataProvider(ABC):
1515
"""Backend-agnostic scene data provider interface."""
1616

1717
@abstractmethod
18-
def update(self, env_ids: list[int] | None = None) -> None:
19-
"""Refresh any cached scene data."""
18+
def update(self) -> None:
19+
"""Refresh any cached scene data (full model/state)."""
2020
raise NotImplementedError
2121

2222
@abstractmethod
@@ -25,8 +25,8 @@ def get_newton_model(self) -> Any | None:
2525
raise NotImplementedError
2626

2727
@abstractmethod
28-
def get_newton_state(self, env_ids: list[int] | None = None) -> Any | None:
29-
"""Return Newton state handle when available."""
28+
def get_newton_state(self) -> Any | None:
29+
"""Return Newton state handle when available (full state)."""
3030
raise NotImplementedError
3131

3232
@abstractmethod

0 commit comments

Comments
 (0)