Skip to content

Commit 8852500

Browse files
committed
Add mjviser web viewer backend (--viewer=viser).
1 parent b5cbd11 commit 8852500

1 file changed

Lines changed: 95 additions & 40 deletions

File tree

mujoco_warp/viewer.py

Lines changed: 95 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2025 The Newton Developers
1+
# Copyright 2026 The Newton Developers
22
#
33
# Licensed under the Apache License, Version 2.0 (the "License");
44
# you may not use this file except in compliance with the License.
@@ -62,6 +62,7 @@ class EngineOptions(enum.IntEnum):
6262
_KEYFRAME = flags.DEFINE_integer("keyframe", 0, "keyframe to initialize simulation.")
6363
_DEVICE = flags.DEFINE_string("device", None, "override the default Warp device")
6464
_REPLAY = flags.DEFINE_string("replay", None, "keyframe sequence to replay, keyframe name must prefix match")
65+
_VIEWER = flags.DEFINE_enum("viewer", "mujoco", ["mujoco", "viser"], "Viewer backend (mujoco native or mjviser web)")
6566

6667
_VIEWER_GLOBAL_STATE = {"running": True, "step_once": False}
6768

@@ -105,6 +106,47 @@ def _compile_step(m, d):
105106
return capture.graph
106107

107108

109+
def _make_warp_step_fn(m, d, graph, ctrls=None):
110+
ctrlid = [0]
111+
112+
def step_fn(mjm, mjd):
113+
if ctrls is not None and ctrlid[0] < len(ctrls):
114+
mjd.ctrl[:] = ctrls[ctrlid[0]]
115+
ctrlid[0] += 1
116+
117+
wp.copy(d.ctrl, wp.array([mjd.ctrl.astype(np.float32)]))
118+
wp.copy(d.act, wp.array([mjd.act.astype(np.float32)]))
119+
wp.copy(d.xfrc_applied, wp.array([mjd.xfrc_applied.astype(np.float32)]))
120+
wp.copy(d.qpos, wp.array([mjd.qpos.astype(np.float32)]))
121+
wp.copy(d.qvel, wp.array([mjd.qvel.astype(np.float32)]))
122+
wp.copy(d.time, wp.array([mjd.time], dtype=wp.float32))
123+
124+
if graph is None:
125+
mjw.step(m, d)
126+
else:
127+
wp.capture_launch(graph)
128+
wp.synchronize()
129+
130+
mjw.get_data_into(mjd, mjm, d)
131+
132+
return step_fn
133+
134+
135+
def _make_c_step_fn(ctrls=None):
136+
if ctrls is None:
137+
return None
138+
139+
ctrlid = [0]
140+
141+
def step_fn(mjm, mjd):
142+
if ctrlid[0] < len(ctrls):
143+
mjd.ctrl[:] = ctrls[ctrlid[0]]
144+
ctrlid[0] += 1
145+
mujoco.mj_step(mjm, mjd)
146+
147+
return step_fn
148+
149+
108150
def _main(argv: Sequence[str]) -> None:
109151
"""Runs viewer app."""
110152
if len(argv) < 2:
@@ -169,45 +211,58 @@ def _main(argv: Sequence[str]) -> None:
169211
print(f"Data\n nworld: {d.nworld} nconmax: {int(d.naconmax / d.nworld)} njmax: {d.njmax}\n")
170212
print(f"MuJoCo Warp simulating with dt = {m.opt.timestep.numpy()[0]:.3f}...")
171213

172-
with mujoco.viewer.launch_passive(mjm, mjd, key_callback=key_callback) as viewer:
173-
opt = copy.copy(mjm.opt)
174-
175-
while True:
176-
start = time.time()
177-
178-
if ctrls is not None and ctrlid < len(ctrls):
179-
mjd.ctrl[:] = ctrls[ctrlid]
180-
ctrlid += 1
181-
182-
if _ENGINE.value == EngineOptions.C:
183-
mujoco.mj_step(mjm, mjd)
184-
else: # mjwarp
185-
wp.copy(d.ctrl, wp.array([mjd.ctrl.astype(np.float32)]))
186-
wp.copy(d.act, wp.array([mjd.act.astype(np.float32)]))
187-
wp.copy(d.xfrc_applied, wp.array([mjd.xfrc_applied.astype(np.float32)]))
188-
wp.copy(d.qpos, wp.array([mjd.qpos.astype(np.float32)]))
189-
wp.copy(d.qvel, wp.array([mjd.qvel.astype(np.float32)]))
190-
wp.copy(d.time, wp.array([mjd.time], dtype=wp.float32))
191-
# if the user changed an option in the MuJoCo Simulate UI, go ahead and recompile the step
192-
# TODO: update memory tied to option max iterations
193-
if mjm.opt != opt:
194-
opt = copy.copy(mjm.opt)
195-
m = mjw.put_model(mjm)
196-
graph = _compile_step(m, d) if wp.get_device().is_cuda else None
197-
if _VIEWER_GLOBAL_STATE["running"] or _VIEWER_GLOBAL_STATE["step_once"]:
198-
_VIEWER_GLOBAL_STATE["step_once"] = False
199-
if graph is None:
200-
mjw.step(m, d)
201-
else:
202-
wp.capture_launch(graph)
203-
wp.synchronize()
204-
mjw.get_data_into(mjd, mjm, d)
205-
206-
viewer.sync()
207-
208-
elapsed = time.time() - start
209-
if elapsed < mjm.opt.timestep:
210-
time.sleep(mjm.opt.timestep - elapsed)
214+
if _VIEWER.value == "viser":
215+
try:
216+
from mjviser import Viewer as MjViserViewer
217+
except ImportError:
218+
raise SystemExit("mjviser required for --viewer=viser: pip install mjviser")
219+
220+
if _ENGINE.value == EngineOptions.WARP:
221+
step_fn = _make_warp_step_fn(m, d, graph, ctrls)
222+
else:
223+
step_fn = _make_c_step_fn(ctrls)
224+
225+
MjViserViewer(mjm, mjd, step_fn=step_fn).run()
226+
else:
227+
with mujoco.viewer.launch_passive(mjm, mjd, key_callback=key_callback) as viewer:
228+
opt = copy.copy(mjm.opt)
229+
230+
while True:
231+
start = time.time()
232+
233+
if ctrls is not None and ctrlid < len(ctrls):
234+
mjd.ctrl[:] = ctrls[ctrlid]
235+
ctrlid += 1
236+
237+
if _ENGINE.value == EngineOptions.C:
238+
mujoco.mj_step(mjm, mjd)
239+
else: # mjwarp
240+
wp.copy(d.ctrl, wp.array([mjd.ctrl.astype(np.float32)]))
241+
wp.copy(d.act, wp.array([mjd.act.astype(np.float32)]))
242+
wp.copy(d.xfrc_applied, wp.array([mjd.xfrc_applied.astype(np.float32)]))
243+
wp.copy(d.qpos, wp.array([mjd.qpos.astype(np.float32)]))
244+
wp.copy(d.qvel, wp.array([mjd.qvel.astype(np.float32)]))
245+
wp.copy(d.time, wp.array([mjd.time], dtype=wp.float32))
246+
# if the user changed an option in the MuJoCo Simulate UI, go ahead and recompile the step
247+
# TODO: update memory tied to option max iterations
248+
if mjm.opt != opt:
249+
opt = copy.copy(mjm.opt)
250+
m = mjw.put_model(mjm)
251+
graph = _compile_step(m, d) if wp.get_device().is_cuda else None
252+
if _VIEWER_GLOBAL_STATE["running"] or _VIEWER_GLOBAL_STATE["step_once"]:
253+
_VIEWER_GLOBAL_STATE["step_once"] = False
254+
if graph is None:
255+
mjw.step(m, d)
256+
else:
257+
wp.capture_launch(graph)
258+
wp.synchronize()
259+
mjw.get_data_into(mjd, mjm, d)
260+
261+
viewer.sync()
262+
263+
elapsed = time.time() - start
264+
if elapsed < mjm.opt.timestep:
265+
time.sleep(mjm.opt.timestep - elapsed)
211266

212267

213268
def main():

0 commit comments

Comments
 (0)