Refactor viewer initialization and protocol.

Introduced a ViewerConfig dataclass to encapsulate common viewer window parameters. The NativeViewer now accepts a ViewerConfig and no longer requires a model at initialization. The Viewer protocol has been updated to include a close method (renamed from stop) and an upload_image method. Added checks for None models/data in studio event handling.

PiperOrigin-RevId: 939631378
Change-Id: Ibc07d0e775efa0d79ce5552ee52102e943a9b290
This commit is contained in:
Matija Kecman
2026-06-28 23:06:14 -07:00
committed by Copybara-Service
parent 79f6deac99
commit f594778813
4 changed files with 61 additions and 26 deletions
@@ -23,25 +23,21 @@ to use these classes.
import mujoco import mujoco
from mujoco.experimental.studio import native_viewer_cc as _viewer from mujoco.experimental.studio import native_viewer_cc as _viewer
from mujoco.experimental.studio import ux from mujoco.experimental.studio import ux
from mujoco.experimental.studio import viewer_protocol from mujoco.experimental.studio import viewer_protocol as vp
from mujoco.experimental.dear_imgui import dear_imgui as imgui from mujoco.experimental.dear_imgui import dear_imgui as imgui
class NativeViewer(viewer_protocol.Viewer): class NativeViewer(vp.Viewer):
"""Simulation-agnostic native viewer for MuJoCo models.""" """Simulation-agnostic native viewer for MuJoCo models."""
def __init__( def __init__(
self, self,
model: mujoco.MjModel, config: vp.ViewerConfig,
*, *,
camera: mujoco.MjvCamera | None = None, camera: mujoco.MjvCamera | None = None,
vis_options: mujoco.MjvOption | None = None, vis_options: mujoco.MjvOption | None = None,
perturb: mujoco.MjvPerturb | None = None, perturb: mujoco.MjvPerturb | None = None,
render_flags: ux.RenderFlags | None = None, render_flags: ux.RenderFlags | None = None,
title: str = '',
width: int = 1200,
height: int = 800,
gfx: str | None = None,
) -> None: ) -> None:
"""Initializes the NativeViewer. """Initializes the NativeViewer.
@@ -49,25 +45,23 @@ class NativeViewer(viewer_protocol.Viewer):
visualization option objects unless they are provided. visualization option objects unless they are provided.
Args: Args:
model: The MuJoCo model, used to initialize the renderer. config: Viewer window configuration.
camera: Camera parameters. Internal object is created if None. camera: Camera parameters. Internal object is created if None.
vis_options: Visualization options. Internal object is created if None. vis_options: Visualization options. Internal object is created if None.
perturb: Perturbation parameters. Internal object is created if None. perturb: Perturbation parameters. Internal object is created if None.
render_flags: Render flags. Internal object is created if None. render_flags: Render flags. Internal object is created if None.
title: Title of the viewer window.
width: Initial width of the viewer window.
height: Initial height of the viewer window.
gfx: Graphics mode. If None, uses the platform default.
""" """
self.config = config
self.camera = camera or mujoco.MjvCamera() self.camera = camera or mujoco.MjvCamera()
self.perturb = perturb or mujoco.MjvPerturb() self.perturb = perturb or mujoco.MjvPerturb()
self.vis_options = vis_options or mujoco.MjvOption() self.vis_options = vis_options or mujoco.MjvOption()
self._viewer = _viewer.Viewer(title, width, height, gfx or '') self._viewer = _viewer.Viewer(
self._viewer.InitRenderer(model) config.title, config.width, config.height, config.gfx or ''
)
# This class does not own the model but we need to know if the model being # This class does not own the model but we need to know if the model being
# rendered has changed, so we store the unique python object id here so we # rendered has changed, so we store the unique python object id here so we
# can use it to detect model changes. # can use it to detect model changes.
self._renderer_model_id = id(model) self._renderer_model_id = id(None)
self._is_running = True self._is_running = True
if render_flags is not None: if render_flags is not None:
self.render_flags = render_flags self.render_flags = render_flags
@@ -114,9 +108,14 @@ class NativeViewer(viewer_protocol.Viewer):
self.render_flags.flags, self.render_flags.flags,
) )
def close(self) -> None:
"""Close the viewer."""
self._is_running = False
# TODO(matijak): Remove stop() and rename callers to close().
def stop(self) -> None: def stop(self) -> None:
"""Stop the viewer.""" """Stop the viewer."""
self._is_running = False self.close()
def get_drop_file(self) -> str: def get_drop_file(self) -> str:
"""Returns the path of the file dropped into the window, or empty string.""" """Returns the path of the file dropped into the window, or empty string."""
+2 -2
View File
@@ -30,12 +30,12 @@ def main(argv: list[str]) -> None:
app = studio_app.StudioApp.from_argv(argv) app = studio_app.StudioApp.from_argv(argv)
# Initialize the viewer. # Initialize the viewer.
viewer = native_viewer.NativeViewer( config = viewer_protocol.ViewerConfig(
app.model,
width=_WIDTH.value, width=_WIDTH.value,
height=_HEIGHT.value, height=_HEIGHT.value,
gfx=_GFX.value, gfx=_GFX.value,
) )
viewer = native_viewer.NativeViewer(config)
# Main viewer loop. # Main viewer loop.
while viewer.is_running(): while viewer.is_running():
@@ -175,8 +175,9 @@ def handle_step_control_keyboard_events(
step_control.set_pause_state(sim.PauseState.UNPAUSED) step_control.set_pause_state(sim.PauseState.UNPAUSED)
return True return True
elif pressed(imgui.Key.Backspace): elif pressed(imgui.Key.Backspace):
mujoco.mj_resetData(model, data) if model is not None and data is not None:
mujoco.mj_forward(model, data) mujoco.mj_resetData(model, data)
mujoco.mj_forward(model, data)
return True return True
elif pressed(imgui.Key.Minus): elif pressed(imgui.Key.Minus):
ux.set_speed_index(step_control, ux_state, ux_state.speed_index + 1) ux.set_speed_index(step_control, ux_state, ux_state.speed_index + 1)
@@ -13,12 +13,12 @@
# limitations under the License. # limitations under the License.
"""Structural protocol defining the common viewer interface. """Structural protocol defining the common viewer interface.
StudioApp uses the protocol for convenience methods that accept any viewer. ViewerApp uses the protocol for convenience methods that accept any viewer.
""" """
import dataclasses import dataclasses
from typing import Any import enum
from typing import Protocol from typing import Any, Protocol
import mujoco import mujoco
from mujoco.experimental.studio import ux from mujoco.experimental.studio import ux
import numpy as np import numpy as np
@@ -35,10 +35,36 @@ GFX_MODES = (
) )
class ViewerMode(enum.StrEnum):
"""Determines where the viewer is rendered."""
NATIVE = 'native'
WEB = 'web'
# -----------------------------------------------------------------------------
# Viewer configuration.
# -----------------------------------------------------------------------------
@dataclasses.dataclass
class ViewerConfig:
"""Common configuration for creating a viewer window."""
title: str = ''
width: int = 1200
height: int = 800
gfx: str = ''
viewer_mode: ViewerMode = ViewerMode.NATIVE
# Legacy message types kept for backward compatibility.
# Will be removed when callers are migrated.
@dataclasses.dataclass @dataclasses.dataclass
class SimToView: class SimToView:
"""A message sent from the simulation to the viewer.""" """A message sent from the simulation to the viewer."""
model: mujoco.MjModel | None = None model: mujoco.MjModel | None = None
state: np.ndarray | None = None state: np.ndarray | None = None
state_sig: int = 0 state_sig: int = 0
@@ -48,7 +74,6 @@ class SimToView:
@dataclasses.dataclass @dataclasses.dataclass
class ViewToSim: class ViewToSim:
"""A message sent from the viewer to the simulation.""" """A message sent from the viewer to the simulation."""
state: np.ndarray | None = None state: np.ndarray | None = None
state_sig: int = 0 state_sig: int = 0
reset: bool = False reset: bool = False
@@ -56,6 +81,11 @@ class ViewToSim:
user_data: dict[str, Any] = dataclasses.field(default_factory=dict) user_data: dict[str, Any] = dataclasses.field(default_factory=dict)
# -----------------------------------------------------------------------------
# Structural interface for any viewer.
# -----------------------------------------------------------------------------
class Viewer(Protocol): class Viewer(Protocol):
"""Structural interface for any viewer.""" """Structural interface for any viewer."""
@@ -70,8 +100,13 @@ class Viewer(Protocol):
def sync(self, model: mujoco.MjModel, data: mujoco.MjData) -> None: def sync(self, model: mujoco.MjModel, data: mujoco.MjData) -> None:
... ...
def stop(self) -> None: def close(self) -> None:
... ...
def get_drop_file(self) -> str: def get_drop_file(self) -> str:
... ...
def upload_image(
self, tex_id: int, img: str | bytes, width: int, height: int, bpp: int
) -> int:
...