From b3ef7a8c2fc29131c3e356448a458edb36766b31 Mon Sep 17 00:00:00 2001 From: Matija Kecman Date: Tue, 4 Aug 2026 09:13:09 -0700 Subject: [PATCH] Simplify MuJoCo Studio viewer selection and unify model flags. This change removes the ViewerMode enum and instead infers whether to launch the Web Viewer or Native Viewer based on the graphics mode (gfx) setting. Specifically, setting gfx to "web" or "webgl" will now launch the Web Viewer (in future "webgpu" would also launch the Web Viewer). Additionally, this unifies command-line flags across studio scripts and samples by replacing the --mjcf flag with --model (with positional argument fallback) and standardizing absl flags usage. --model is a better name since formats like .mjz and .mjb are also supported by this argument. PiperOrigin-RevId: 959058940 Change-Id: If4e0ace664ddc6e45cb681d168679148e8c4901d --- .../experimental/studio/launch_passive.py | 17 ++++----- .../experimental/studio/sample/ghost.py | 24 ++++++++----- .../experimental/studio/sample/implot.py | 26 ++++++++------ .../experimental/studio/sample/render.py | 35 ++++++++++--------- python/mujoco/experimental/studio/viewer.py | 23 +++++------- .../experimental/studio/viewer_protocol.py | 15 ++------ .../platform/hal/graphics_mode.cc | 2 +- 7 files changed, 68 insertions(+), 74 deletions(-) diff --git a/python/mujoco/experimental/studio/launch_passive.py b/python/mujoco/experimental/studio/launch_passive.py index 708e75f1..1c6d05c4 100644 --- a/python/mujoco/experimental/studio/launch_passive.py +++ b/python/mujoco/experimental/studio/launch_passive.py @@ -86,26 +86,21 @@ def run_viewer_target( """Creates the appropriate viewer and runs the viewer loop. Args: - config: Configuration specifying the viewer mode and window settings. + config: Configuration specifying the viewer window settings. viewer_endpoint: Endpoint for communicating with the simulation side. handlers: Optional list of viewer-side handler instances, which are classes with methods decorated with ``@handler``. - - Raises: - ValueError: If the viewer mode requested in config is unknown. """ - if config.viewer_mode == viewer_protocol.ViewerMode.NATIVE: + if config.gfx in ('web', 'webgl'): # In future we may add 'webgpu' here too. + from mujoco.experimental.studio import web_viewer # pylint: disable=g-import-not-at-top + + viewer = web_viewer.WebViewer(config, viewer_endpoint, handlers=handlers) + else: from mujoco.experimental.studio import native_viewer # pylint: disable=g-import-not-at-top viewer = native_viewer.NativeViewer( config, viewer_endpoint, handlers=handlers ) - elif config.viewer_mode == viewer_protocol.ViewerMode.WEB: - from mujoco.experimental.studio import web_viewer # pylint: disable=g-import-not-at-top - - viewer = web_viewer.WebViewer(config, viewer_endpoint, handlers=handlers) - else: - raise ValueError(f'Unknown viewer mode: {config.viewer_mode!r}') viewer_protocol.run_viewer_loop(viewer) diff --git a/python/mujoco/experimental/studio/sample/ghost.py b/python/mujoco/experimental/studio/sample/ghost.py index cd95989e..213b6450 100644 --- a/python/mujoco/experimental/studio/sample/ghost.py +++ b/python/mujoco/experimental/studio/sample/ghost.py @@ -38,12 +38,15 @@ from mujoco.experimental.dear_imgui import dear_imgui as imgui vp = viewer_protocol -_GFX = _flags.DEFINE_enum('gfx', None, vp.GFX_MODES, 'Graphics mode.') +_MODEL = _flags.DEFINE_string('model', None, 'Path to model file.') +_GFX = _flags.DEFINE_enum( + 'gfx', None, vp.GFX_MODES, 'Graphics mode ("web" launches Web Viewer).' +) +_PORT = _flags.DEFINE_integer( + 'port', 0, 'Web Viewer port (0 picks first free port >= 8080).' +) _WIDTH = _flags.DEFINE_integer('width', 1200, 'Width of the output image.') _HEIGHT = _flags.DEFINE_integer('height', 800, 'Height of the output image') -_VIEWER = _flags.DEFINE_enum_class( - 'viewer', vp.ViewerMode.NATIVE, vp.ViewerMode, 'Viewer mode.' -) class GhostRenderer: @@ -155,15 +158,18 @@ class GhostRenderer: def main(argv: list[str]) -> None: - if len(argv) != 2: - raise _app.UsageError('Please provide exactly one MJCF path argument.') + model_path = _MODEL.value or ( + argv[1] if len(argv) > 1 and not argv[1].startswith('--') else None + ) + if not model_path: + raise _app.UsageError('Please provide a model path argument or --model flag.') data = None try: - if (data := parser.parse(argv[1])) is None: + if (data := parser.parse(model_path)) is None: raise ValueError('parser returned None') except Exception as ex: # pylint: disable=broad-except - print(f'Failed to load model from {argv[1]!r}: {ex}') + print(f'Failed to load model from {model_path!r}: {ex}') sys.exit(1) model = data.model @@ -172,7 +178,7 @@ def main(argv: list[str]) -> None: width=_WIDTH.value, height=_HEIGHT.value, gfx=_GFX.value or '', - viewer_mode=_VIEWER.value, + http_port=_PORT.value, ) ghost_renderer = GhostRenderer() diff --git a/python/mujoco/experimental/studio/sample/implot.py b/python/mujoco/experimental/studio/sample/implot.py index a50324cc..7ff57b43 100644 --- a/python/mujoco/experimental/studio/sample/implot.py +++ b/python/mujoco/experimental/studio/sample/implot.py @@ -17,7 +17,7 @@ This script runs a Studio viewer and adds an 'Inspect Body' window using ImGui and ImPlot bindings to visualize selected body data. The example demonstrates how responsive UI layout rules are easily implemented. -Provide an MJCF model file via the first command-line argument to launch. +Provide a model file via the --model flag or positional argument to launch. """ import math @@ -40,12 +40,15 @@ from mujoco.experimental.implot import implot vp = viewer_protocol -_GFX = _flags.DEFINE_enum('gfx', None, vp.GFX_MODES, 'Graphics mode.') +_MODEL = _flags.DEFINE_string('model', None, 'Path to model file.') +_GFX = _flags.DEFINE_enum( + 'gfx', None, vp.GFX_MODES, 'Graphics mode ("web" launches Web Viewer).' +) +_PORT = _flags.DEFINE_integer( + 'port', 0, 'Web Viewer port (0 picks first free port >= 8080).' +) _WIDTH = _flags.DEFINE_integer('width', 1200, 'Width of the output image.') _HEIGHT = _flags.DEFINE_integer('height', 800, 'Height of the output image') -_VIEWER = _flags.DEFINE_enum_class( - 'viewer', vp.ViewerMode.NATIVE, vp.ViewerMode, 'Viewer mode.' -) _N_HISTORY = 100 @@ -211,12 +214,15 @@ class BodyInspector: def main(argv: list[str]) -> None: - if len(argv) < 2: - print('Usage: implot ') + model_path = _MODEL.value or ( + argv[1] if len(argv) > 1 and not argv[1].startswith('--') else None + ) + if not model_path: + print('Usage: implot --model=') sys.exit(1) - if (data := parser.parse(argv[1])) is None: - print(f'Error loading model from {argv[1]!r}') + if (data := parser.parse(model_path)) is None: + print(f'Error loading model from {model_path!r}') sys.exit(1) model = data.model @@ -227,7 +233,7 @@ def main(argv: list[str]) -> None: width=_WIDTH.value, height=_HEIGHT.value, gfx=_GFX.value or '', - viewer_mode=_VIEWER.value, + http_port=_PORT.value, ) with launch_passive.launch_passive( diff --git a/python/mujoco/experimental/studio/sample/render.py b/python/mujoco/experimental/studio/sample/render.py index a3431de2..07594b6c 100644 --- a/python/mujoco/experimental/studio/sample/render.py +++ b/python/mujoco/experimental/studio/sample/render.py @@ -16,37 +16,38 @@ import os import sys -from absl import app -from absl import flags +from absl import app as _app +from absl import flags as _flags import mujoco from mujoco.experimental.studio import parser from mujoco.experimental.studio import renderer from mujoco.experimental.studio import viewer_protocol from PIL import Image -_MODEL = flags.DEFINE_string('model', '', 'Model file to load.') -_OUTPUT = flags.DEFINE_string('output', '', 'Output file to save.') -_GFX = flags.DEFINE_enum( +_MODEL = _flags.DEFINE_string('model', None, 'Path to model file.') +_OUTPUT = _flags.DEFINE_string('output', '', 'Output file to save.') +_GFX = _flags.DEFINE_enum( 'gfx', None, viewer_protocol.GFX_MODES, 'Rendering graphics mode.' ) -_WIDTH = flags.DEFINE_integer('width', 320, 'Width of the output image.') -_HEIGHT = flags.DEFINE_integer('height', 240, 'Height of the output image.') -_STEPS = flags.DEFINE_integer('steps', 1, 'Number of steps before render.') +_WIDTH = _flags.DEFINE_integer('width', 320, 'Width of the output image.') +_HEIGHT = _flags.DEFINE_integer('height', 240, 'Height of the output image.') +_STEPS = _flags.DEFINE_integer('steps', 1, 'Number of steps before render.') -def main(argv): - if len(argv) > 1: - raise app.UsageError('Too many command-line arguments.') - if not _MODEL.value: - raise ValueError('`model` flag is required.') +def main(argv: list[str]) -> None: + model_path = _MODEL.value or ( + argv[1] if len(argv) > 1 and not argv[1].startswith('--') else None + ) + if not model_path: + raise _app.UsageError('Please provide a model path argument or --model flag.') if not _OUTPUT.value: - raise ValueError('`output flag is required.') + raise _app.UsageError('`output` flag is required.') try: - data = parser.parse(_MODEL.value) + data = parser.parse(model_path) model = data.model except Exception as ex: # pylint: disable=broad-except - print(f'Error loading model from `{_MODEL.value}`: {ex}') + print(f'Error loading model from `{model_path}`: {ex}') sys.exit(-1) for _ in range(_STEPS.value): @@ -73,4 +74,4 @@ def main(argv): if __name__ == '__main__': - app.run(main) + _app.run(main) diff --git a/python/mujoco/experimental/studio/viewer.py b/python/mujoco/experimental/studio/viewer.py index 35b4a525..4cda28f6 100644 --- a/python/mujoco/experimental/studio/viewer.py +++ b/python/mujoco/experimental/studio/viewer.py @@ -14,7 +14,7 @@ """Interactive Studio GUI viewer for MuJoCo.""" from absl import app as _app -from absl import flags +from absl import flags as _flags from mujoco.experimental.studio import launch_passive from mujoco.experimental.studio import messages from mujoco.experimental.studio import parser @@ -24,19 +24,15 @@ from mujoco.experimental.studio import viewer_protocol vp = viewer_protocol -_GFX = flags.DEFINE_enum('gfx', None, vp.GFX_MODES, 'Graphics mode.') -_WIDTH = flags.DEFINE_integer('width', 1200, 'Width of output window.') -_HEIGHT = flags.DEFINE_integer('height', 800, 'Height of output window.') -_MJCF_PATH = flags.DEFINE_string('mjcf', None, 'Path to MJCF file.') -_VIEWER = flags.DEFINE_enum_class( - 'viewer', vp.ViewerMode.NATIVE, vp.ViewerMode, 'Viewer mode.' +_MODEL = _flags.DEFINE_string('model', None, 'Path to model file.') +_GFX = _flags.DEFINE_enum( + 'gfx', None, vp.GFX_MODES, 'Graphics mode ("web" launches Web Viewer).' ) -_PORT = flags.DEFINE_integer( - 'port', - 0, - 'Web viewer port. 0 picks the first free port starting at 8080, so ' - 'several viewers can run side by side.', +_PORT = _flags.DEFINE_integer( + 'port', 0, 'Web Viewer port (0 picks first free port >= 8080).' ) +_WIDTH = _flags.DEFINE_integer('width', 1200, 'Width of the output image.') +_HEIGHT = _flags.DEFINE_integer('height', 800, 'Height of the output image') def main(argv: list[str]) -> None: @@ -44,12 +40,11 @@ def main(argv: list[str]) -> None: width=_WIDTH.value, height=_HEIGHT.value, gfx=_GFX.value or '', - viewer_mode=_VIEWER.value, http_port=_PORT.value, ) # Resolve model path, if provided. - model_path = _MJCF_PATH.value or ( + model_path = _MODEL.value or ( argv[1] if len(argv) > 1 and not argv[1].startswith('--') else None ) diff --git a/python/mujoco/experimental/studio/viewer_protocol.py b/python/mujoco/experimental/studio/viewer_protocol.py index e6193d90..9c7c4864 100644 --- a/python/mujoco/experimental/studio/viewer_protocol.py +++ b/python/mujoco/experimental/studio/viewer_protocol.py @@ -33,17 +33,11 @@ GFX_MODES = ( 'opengl_software', 'vulkan', 'vulkan_software', + 'web', 'webgl', ) -class ViewerMode(enum.StrEnum): - """Determines where the viewer is rendered.""" - - NATIVE = 'native' - WEB = 'web' - - # ----------------------------------------------------------------------------- # Viewer configuration. # ----------------------------------------------------------------------------- @@ -56,11 +50,8 @@ class ViewerConfig: title: str = '' width: int = 1200 height: int = 800 - gfx: str = '' - viewer_mode: ViewerMode = ViewerMode.NATIVE - # Web viewer only: public port. 0 picks the first free port starting at 8080, - # so several viewers can run side by side. - http_port: int = 0 + gfx: str = '' # Graphics mode ('web' launches Web Viewer). + http_port: int = 0 # Web Viewer port (0 picks first free port >= 8080). # Legacy message types kept for backward compatibility. diff --git a/src/experimental/platform/hal/graphics_mode.cc b/src/experimental/platform/hal/graphics_mode.cc index 87547e5a..c87e0cd3 100644 --- a/src/experimental/platform/hal/graphics_mode.cc +++ b/src/experimental/platform/hal/graphics_mode.cc @@ -74,7 +74,7 @@ GraphicsMode GraphicsModeFromString(std::string_view str, return GraphicsMode::FilamentVulkan; } else if (str == "vulkan_software") { return GraphicsMode::FilamentVulkanSoftware; - } else if (str == "webgl") { + } else if (str == "web" || str == "webgl") { return GraphicsMode::FilamentWebGl; } else if (str == "opengl_headless") { return GraphicsMode::FilamentOpenGlHeadless;