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;