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
This commit is contained in:
Matija Kecman
2026-08-04 09:13:09 -07:00
committed by Copybara-Service
parent 0a10860257
commit b3ef7a8c2f
7 changed files with 68 additions and 74 deletions
@@ -86,26 +86,21 @@ def run_viewer_target(
"""Creates the appropriate viewer and runs the viewer loop. """Creates the appropriate viewer and runs the viewer loop.
Args: 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. viewer_endpoint: Endpoint for communicating with the simulation side.
handlers: Optional list of viewer-side handler instances, which are classes handlers: Optional list of viewer-side handler instances, which are classes
with methods decorated with ``@handler``. 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 from mujoco.experimental.studio import native_viewer # pylint: disable=g-import-not-at-top
viewer = native_viewer.NativeViewer( viewer = native_viewer.NativeViewer(
config, viewer_endpoint, handlers=handlers 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) viewer_protocol.run_viewer_loop(viewer)
@@ -38,12 +38,15 @@ from mujoco.experimental.dear_imgui import dear_imgui as imgui
vp = viewer_protocol 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.') _WIDTH = _flags.DEFINE_integer('width', 1200, 'Width of the output image.')
_HEIGHT = _flags.DEFINE_integer('height', 800, 'Height 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: class GhostRenderer:
@@ -155,15 +158,18 @@ class GhostRenderer:
def main(argv: list[str]) -> None: def main(argv: list[str]) -> None:
if len(argv) != 2: model_path = _MODEL.value or (
raise _app.UsageError('Please provide exactly one MJCF path argument.') 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 data = None
try: try:
if (data := parser.parse(argv[1])) is None: if (data := parser.parse(model_path)) is None:
raise ValueError('parser returned None') raise ValueError('parser returned None')
except Exception as ex: # pylint: disable=broad-except 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) sys.exit(1)
model = data.model model = data.model
@@ -172,7 +178,7 @@ def main(argv: list[str]) -> None:
width=_WIDTH.value, width=_WIDTH.value,
height=_HEIGHT.value, height=_HEIGHT.value,
gfx=_GFX.value or '', gfx=_GFX.value or '',
viewer_mode=_VIEWER.value, http_port=_PORT.value,
) )
ghost_renderer = GhostRenderer() ghost_renderer = GhostRenderer()
@@ -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 and ImPlot bindings to visualize selected body data. The example demonstrates
how responsive UI layout rules are easily implemented. 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 import math
@@ -40,12 +40,15 @@ from mujoco.experimental.implot import implot
vp = viewer_protocol 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.') _WIDTH = _flags.DEFINE_integer('width', 1200, 'Width of the output image.')
_HEIGHT = _flags.DEFINE_integer('height', 800, 'Height 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 _N_HISTORY = 100
@@ -211,12 +214,15 @@ class BodyInspector:
def main(argv: list[str]) -> None: def main(argv: list[str]) -> None:
if len(argv) < 2: model_path = _MODEL.value or (
print('Usage: implot <model_path.xml>') argv[1] if len(argv) > 1 and not argv[1].startswith('--') else None
)
if not model_path:
print('Usage: implot --model=<model_path.xml>')
sys.exit(1) sys.exit(1)
if (data := parser.parse(argv[1])) is None: if (data := parser.parse(model_path)) is None:
print(f'Error loading model from {argv[1]!r}') print(f'Error loading model from {model_path!r}')
sys.exit(1) sys.exit(1)
model = data.model model = data.model
@@ -227,7 +233,7 @@ def main(argv: list[str]) -> None:
width=_WIDTH.value, width=_WIDTH.value,
height=_HEIGHT.value, height=_HEIGHT.value,
gfx=_GFX.value or '', gfx=_GFX.value or '',
viewer_mode=_VIEWER.value, http_port=_PORT.value,
) )
with launch_passive.launch_passive( with launch_passive.launch_passive(
@@ -16,37 +16,38 @@
import os import os
import sys import sys
from absl import app from absl import app as _app
from absl import flags from absl import flags as _flags
import mujoco import mujoco
from mujoco.experimental.studio import parser from mujoco.experimental.studio import parser
from mujoco.experimental.studio import renderer from mujoco.experimental.studio import renderer
from mujoco.experimental.studio import viewer_protocol from mujoco.experimental.studio import viewer_protocol
from PIL import Image from PIL import Image
_MODEL = flags.DEFINE_string('model', '', 'Model file to load.') _MODEL = _flags.DEFINE_string('model', None, 'Path to model file.')
_OUTPUT = flags.DEFINE_string('output', '', 'Output file to save.') _OUTPUT = _flags.DEFINE_string('output', '', 'Output file to save.')
_GFX = flags.DEFINE_enum( _GFX = _flags.DEFINE_enum(
'gfx', None, viewer_protocol.GFX_MODES, 'Rendering graphics mode.' 'gfx', None, viewer_protocol.GFX_MODES, 'Rendering graphics mode.'
) )
_WIDTH = flags.DEFINE_integer('width', 320, 'Width of the output image.') _WIDTH = _flags.DEFINE_integer('width', 320, 'Width of the output image.')
_HEIGHT = flags.DEFINE_integer('height', 240, 'Height 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.') _STEPS = _flags.DEFINE_integer('steps', 1, 'Number of steps before render.')
def main(argv): def main(argv: list[str]) -> None:
if len(argv) > 1: model_path = _MODEL.value or (
raise app.UsageError('Too many command-line arguments.') argv[1] if len(argv) > 1 and not argv[1].startswith('--') else None
if not _MODEL.value: )
raise ValueError('`model` flag is required.') if not model_path:
raise _app.UsageError('Please provide a model path argument or --model flag.')
if not _OUTPUT.value: if not _OUTPUT.value:
raise ValueError('`output flag is required.') raise _app.UsageError('`output` flag is required.')
try: try:
data = parser.parse(_MODEL.value) data = parser.parse(model_path)
model = data.model model = data.model
except Exception as ex: # pylint: disable=broad-except 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) sys.exit(-1)
for _ in range(_STEPS.value): for _ in range(_STEPS.value):
@@ -73,4 +74,4 @@ def main(argv):
if __name__ == '__main__': if __name__ == '__main__':
app.run(main) _app.run(main)
+9 -14
View File
@@ -14,7 +14,7 @@
"""Interactive Studio GUI viewer for MuJoCo.""" """Interactive Studio GUI viewer for MuJoCo."""
from absl import app as _app 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 launch_passive
from mujoco.experimental.studio import messages from mujoco.experimental.studio import messages
from mujoco.experimental.studio import parser from mujoco.experimental.studio import parser
@@ -24,19 +24,15 @@ from mujoco.experimental.studio import viewer_protocol
vp = viewer_protocol vp = viewer_protocol
_GFX = flags.DEFINE_enum('gfx', None, vp.GFX_MODES, 'Graphics mode.') _MODEL = _flags.DEFINE_string('model', None, 'Path to model file.')
_WIDTH = flags.DEFINE_integer('width', 1200, 'Width of output window.') _GFX = _flags.DEFINE_enum(
_HEIGHT = flags.DEFINE_integer('height', 800, 'Height of output window.') 'gfx', None, vp.GFX_MODES, 'Graphics mode ("web" launches Web Viewer).'
_MJCF_PATH = flags.DEFINE_string('mjcf', None, 'Path to MJCF file.')
_VIEWER = flags.DEFINE_enum_class(
'viewer', vp.ViewerMode.NATIVE, vp.ViewerMode, 'Viewer mode.'
) )
_PORT = flags.DEFINE_integer( _PORT = _flags.DEFINE_integer(
'port', 'port', 0, 'Web Viewer port (0 picks first free port >= 8080).'
0,
'Web viewer port. 0 picks the first free port starting at 8080, so '
'several viewers can run side by side.',
) )
_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: def main(argv: list[str]) -> None:
@@ -44,12 +40,11 @@ def main(argv: list[str]) -> None:
width=_WIDTH.value, width=_WIDTH.value,
height=_HEIGHT.value, height=_HEIGHT.value,
gfx=_GFX.value or '', gfx=_GFX.value or '',
viewer_mode=_VIEWER.value,
http_port=_PORT.value, http_port=_PORT.value,
) )
# Resolve model path, if provided. # 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 argv[1] if len(argv) > 1 and not argv[1].startswith('--') else None
) )
@@ -33,17 +33,11 @@ GFX_MODES = (
'opengl_software', 'opengl_software',
'vulkan', 'vulkan',
'vulkan_software', 'vulkan_software',
'web',
'webgl', 'webgl',
) )
class ViewerMode(enum.StrEnum):
"""Determines where the viewer is rendered."""
NATIVE = 'native'
WEB = 'web'
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Viewer configuration. # Viewer configuration.
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -56,11 +50,8 @@ class ViewerConfig:
title: str = '' title: str = ''
width: int = 1200 width: int = 1200
height: int = 800 height: int = 800
gfx: str = '' gfx: str = '' # Graphics mode ('web' launches Web Viewer).
viewer_mode: ViewerMode = ViewerMode.NATIVE http_port: int = 0 # Web Viewer port (0 picks first free port >= 8080).
# 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
# Legacy message types kept for backward compatibility. # Legacy message types kept for backward compatibility.
@@ -74,7 +74,7 @@ GraphicsMode GraphicsModeFromString(std::string_view str,
return GraphicsMode::FilamentVulkan; return GraphicsMode::FilamentVulkan;
} else if (str == "vulkan_software") { } else if (str == "vulkan_software") {
return GraphicsMode::FilamentVulkanSoftware; return GraphicsMode::FilamentVulkanSoftware;
} else if (str == "webgl") { } else if (str == "web" || str == "webgl") {
return GraphicsMode::FilamentWebGl; return GraphicsMode::FilamentWebGl;
} else if (str == "opengl_headless") { } else if (str == "opengl_headless") {
return GraphicsMode::FilamentOpenGlHeadless; return GraphicsMode::FilamentOpenGlHeadless;