### Run mjviser with pip install Source: https://github.com/mujocolab/mjviser/blob/main/README.md Install mjviser using pip and run it from the command line. Specify the model path. ```bash pip install mjviser mujviser path/to/model.xml ``` -------------------------------- ### Load robot_descriptions models with uvx Source: https://github.com/mujocolab/mjviser/blob/main/README.md Load MuJoCo models from the robot_descriptions library using uvx. This example loads the 'go1' model. ```bash uvx --with robot_descriptions mjviser go1 ``` -------------------------------- ### Custom Motion Playback with ViserMujocoScene and Timeline Scrubber Source: https://context7.com/mujocolab/mjviser/llms.txt Build a custom motion player by recording qpos/qvel trajectories and scrubbing through them using a Viser slider. This allows for detailed inspection of motion, with contact replays supported at any frame. The example records 3 seconds of motion. ```python import time import mujoco import numpy as np import viser from mjviser import ViserMujocoScene model = mujoco.MjModel.from_xml_path("robot.xml") data = mujoco.MjData(model) mujoco.mj_forward(model, data) # Record 3 seconds. dt = model.opt.timestep n_steps = int(3.0 / dt) qpos_rec = np.zeros((n_steps, model.nq)) qvel_rec = np.zeros((n_steps, model.nv)) for i in range(n_steps): qpos_rec[i] = data.qpos qvel_rec[i] = data.qvel mujoco.mj_step(model, data) server = viser.ViserServer() scene = ViserMujocoScene(server, model, num_envs=1) tabs = scene.create_visualization_gui() replay = mujoco.MjData(model) frame = [0] with tabs.add_tab("Playback"): tl = server.gui.add_slider("Frame", min=0, max=n_steps - 1, step=1, initial_value=0) @tl.on_update def _(_): frame[0] = int(tl.value) replay.qpos[:] = qpos_rec[frame[0]] replay.qvel[:] = qvel_rec[frame[0]] mujoco.mj_forward(model, replay) scene.update_from_mjdata(replay) # Render initial frame, then serve. replay.qpos[:] = qpos_rec[0] mujoco.mj_forward(model, replay) scene.update_from_mjdata(replay) try: while True: time.sleep(1.0 / 60.0) except KeyboardInterrupt: server.stop() ``` -------------------------------- ### Run mjviser with uvx Source: https://github.com/mujocolab/mjviser/blob/main/README.md Execute mjviser directly using uvx without local installation. Specify the model path. ```bash uvx mjviser path/to/model.xml ``` -------------------------------- ### Custom ViserMujocoScene with GUI Source: https://github.com/mujocolab/mjviser/blob/main/README.md Directly use ViserMujocoScene for advanced control. This example creates a visualization GUI, adds a slider for 'Force', and includes a basic simulation loop. ```python server = viser.ViserServer() scene = ViserMujocoScene(server, model, num_envs=1) scene.create_visualization_gui() with server.gui.add_folder("My Controls"): slider = server.gui.add_slider("Force", min=0, max=100, initial_value=0) while True: mujoco.mj_step(model, data) scene.update_from_mjdata(data) ``` -------------------------------- ### Fuzzy path matching with mjviser Source: https://github.com/mujocolab/mjviser/blob/main/README.md Use mjviser with fuzzy path matching to find models in the current directory. Examples include 'humanoid' and 'shadow_hand'. ```bash mjviser humanoid # finds **/humanoid*.xml ``` ```bash mjviser shadow_hand # finds **/shadow_hand*.xml ``` -------------------------------- ### Update Batched Multi-Env Scene from Arrays Source: https://context7.com/mujocolab/mjviser/llms.txt Update the Viser scene with pre-computed body transforms from NumPy arrays of shape `(num_envs, nbody, ...)`. Optionally provide `qpos`, `qvel`, and `ctrl` for decor overlays. This example simulates `NUM_ENVS` independently and updates the scene. ```python import numpy as np import mujoco import viser from mjviser import ViserMujocoScene NUM_ENVS = 8 model = mujoco.MjModel.from_xml_path("robot.xml") server = viser.ViserServer() scene = ViserMujocoScene(server, model, num_envs=NUM_ENVS) scene.create_visualization_gui() # Simulate NUM_ENVS independently (plain MuJoCo here for illustration). datas = [mujoco.MjData(model) for _ in range(NUM_ENVS)] while True: for d in datas: mujoco.mj_step(model, d) xpos = np.stack([d.xpos for d in datas]) # (N, nbody, 3) xmat = np.stack([d.xmat.reshape(-1, 3, 3) for d in datas]) # (N, nbody, 3, 3) qpos = np.stack([d.qpos for d in datas]) qvel = np.stack([d.qvel for d in datas]) scene.update_from_arrays(xpos, xmat, qpos=qpos, qvel=qvel) ``` -------------------------------- ### Viewer with Custom Reset Function Source: https://context7.com/mujocolab/mjviser/llms.txt Allows a custom reset function to be injected, enabling specific initial states for the simulation, like setting an elevated starting position. ```python import mujoco import viser from mjviser import Viewer # --- Custom reset function --- def my_reset(m: mujoco.MjModel, d: mujoco.MjData) -> None: mujoco.mj_resetData(m, d) d.qpos[2] = 1.5 # start elevated mujoco.mj_forward(m, d) Viewer(model, data, reset_fn=my_reset).run() ``` -------------------------------- ### Convert MuJoCo Geometry to Trimesh with merge_geoms and merge_sites Source: https://context7.com/mujocolab/mjviser/llms.txt Low-level utilities to merge MuJoCo geoms or sites into a single `trimesh.Trimesh` object in local body space. These functions handle mesh assets, primitive types, textures, and heightfields. Examples show collecting non-transparent geoms for a specific body and merging sites. ```python import mujoco from mjviser.conversions import merge_geoms, merge_sites, is_fixed_body, get_body_name model = mujoco.MjModel.from_xml_path("robot.xml") # Collect all non-transparent geoms for body 3. body_id = 3 geom_ids = [i for i in range(model.ngeom) if model.geom_bodyid[i] == body_id and model.geom_rgba[i, 3] > 0] mesh = merge_geoms(model, geom_ids) print(mesh.vertices.shape, mesh.faces.shape) # e.g. (1234, 3) (2048, 3) # Merge sites for a body. site_ids = [i for i in range(model.nsite) if model.site_bodyid[i] == body_id] if site_ids: site_mesh = merge_sites(model, site_ids) print(site_mesh.vertices.shape) # Check if a body is welded to world (fixed / static). print(is_fixed_body(model, 0)) # True for world body print(get_body_name(model, 3)) # "forearm" or "body_3" ``` -------------------------------- ### Basic Python API usage Source: https://github.com/mujocolab/mjviser/blob/main/README.md Initialize and run the mjviser Viewer with a MuJoCo model and data. The URL to access the viewer will be printed. ```python import mujoco from mjviser import Viewer model = mujoco.MjModel.from_xml_path("robot.xml") data = mujoco.MjData(model) Viewer(model, data).run() ``` -------------------------------- ### Minimal Viewer Usage Source: https://context7.com/mujocolab/mjviser/llms.txt Basic initialization and running of the Viewer class. Opens the viewer at http://localhost:8080 and blocks until closed. ```python import mujoco import viser from mjviser import Viewer # --- Minimal usage --- model = mujoco.MjModel.from_xml_path("robot.xml") data = mujoco.MjData(model) Viewer(model, data).run() # blocks; open http://localhost:8080 ``` -------------------------------- ### Format and Lint Code with Ruff Source: https://github.com/mujocolab/mjviser/blob/main/CLAUDE.md Run `make format` to automatically format code according to project style guidelines and lint it using Ruff. This command should be used frequently during development. ```sh make format # Format and lint (ruff) ``` -------------------------------- ### Run Tests Source: https://github.com/mujocolab/mjviser/blob/main/CLAUDE.md Execute `make test` to run the project's test suite. Ensure all tests pass to maintain code quality and stability. ```sh make test # Run tests ``` -------------------------------- ### Viewer with multi-environment support Source: https://context7.com/mujocolab/mjviser/llms.txt Visualize parallel environments simulated with mujoco-warp or any other vectorised physics backend by passing num_envs, a batched step_fn, and a batched render_fn. ```APIDOC ## `Viewer` with multi-environment support (mujoco-warp) Pass `num_envs`, a batched `step_fn`, and a batched `render_fn` to visualize parallel environments simulated with `mujoco-warp` or any other vectorised physics backend. ```python import math import mujoco import mujoco_warp as mjwarp import numpy as np from mjviser import Viewer NUM_ENVS = 4 SPACING = 2.0 model = mujoco.MjModel.from_xml_path("humanoid.xml") data = mujoco.MjData(model) mujoco.mj_forward(model, data) m = mjwarp.put_model(model) d = mjwarp.put_data(model, data, nworld=NUM_ENVS) # Arrange envs on a grid. cols = math.ceil(math.sqrt(NUM_ENVS)) origins = np.zeros((NUM_ENVS, 3)) for i in range(NUM_ENVS): r, c = divmod(i, cols) origins[i, :2] = [c * SPACING, r * SPACING] qpos = d.qpos.numpy() for i in range(NUM_ENVS): qpos[i, :2] = origins[i, :2] d.qpos.assign(qpos) mjwarp.forward(m, d) def step(_m, _d): mjwarp.step(m, d) def render(scene): xpos = d.xpos.numpy() xmat = d.xmat.numpy().reshape(NUM_ENVS, -1, 3, 3) scene.update_from_arrays( xpos, xmat, qpos=d.qpos.numpy(), qvel=d.qvel.numpy(), ctrl=d.ctrl.numpy() if model.nu > 0 else None, ) def reset(_m, _d): nd = mjwarp.put_data(model, data, nworld=NUM_ENVS) d.qpos.assign(nd.qpos.numpy()) d.qvel.assign(nd.qvel.numpy()) mjwarp.forward(m, d) Viewer(model, data, step_fn=step, render_fn=render, reset_fn=reset, num_envs=NUM_ENVS).run() ``` ``` -------------------------------- ### Launch Viser with mjviser CLI Source: https://context7.com/mujocolab/mjviser/llms.txt Launch the MuJoCo viewer directly from the shell without writing Python code. Supports direct file paths, fuzzy name matching against the working directory, and robot_descriptions model names. You can also specify a custom port. ```bash # Direct file path mjviser path/to/model.xml # Fuzzy search (finds **/shadow_hand*.xml under CWD) mjviser shadow_hand # robot_descriptions integration (57 MuJoCo models) uvx --with robot_descriptions mjviser go1 uvx --with robot_descriptions mjviser panda # Custom port mjviser model.xml --port 7070 # Python -m form python -m mjviser model.xml --port 8090 ``` -------------------------------- ### Low-level Scene Management with ViserMujocoScene Source: https://context7.com/mujocolab/mjviser/llms.txt Use `ViserMujocoScene` for direct control over Viser geometry handles and visualization state, suitable for custom simulation loops or GUIs. It requires a `ViserServer` instance and a `mujoco.MjModel`. ```python import time import mujoco import viser from mjviser import ViserMujocoScene model = mujoco.MjModel.from_xml_path("robot.xml") data = mujoco.MjData(model) server = viser.ViserServer() scene = ViserMujocoScene(server, model, num_envs=1) # Add the standard Scene / Visualization / Groups tab group. tabs = scene.create_visualization_gui() # Append a custom tab to the same tab group. with tabs.add_tab("My Controls"): slider = server.gui.add_slider("Gain", min=0.0, max=10.0, step=0.1, initial_value=1.0) step_dt = model.opt.timestep try: while True: t0 = time.perf_counter() mujoco.mj_step(model, data) scene.update_from_mjdata(data) # push state to browser elapsed = time.perf_counter() - t0 time.sleep(max(0.0, step_dt - elapsed)) except KeyboardInterrupt: server.stop() ``` -------------------------------- ### Run All Code Checks Source: https://github.com/mujocolab/mjviser/blob/main/CLAUDE.md Use `make check` to run both code formatting/linting (`make format`) and type checking (`make type`). It is mandatory to run `make check` before committing code. ```sh make check # make format && make type ``` -------------------------------- ### Visualize Parallel Environments with Viewer and mujoco-warp Source: https://context7.com/mujocolab/mjviser/llms.txt Use this snippet to visualize multiple environments simulated with `mujoco-warp`. It requires passing a batched `step_fn`, `render_fn`, and `num_envs` to the `Viewer`. ```python import math import mujoco import mujoco_warp as mjwarp import numpy as np from mjviser import Viewer NUM_ENVS = 4 SPACING = 2.0 model = mujoco.MjModel.from_xml_path("humanoid.xml") data = mujoco.MjData(model) mujoco.mj_forward(model, data) m = mjwarp.put_model(model) d = mjwarp.put_data(model, data, nworld=NUM_ENVS) # Arrange envs on a grid. cols = math.ceil(math.sqrt(NUM_ENVS)) origins = np.zeros((NUM_ENVS, 3)) for i in range(NUM_ENVS): r, c = divmod(i, cols) origins[i, :2] = [c * SPACING, r * SPACING] qpos = d.qpos.numpy() for i in range(NUM_ENVS): qpos[i, :2] = origins[i, :2] d.qpos.assign(qpos) mjwarp.forward(m, d) def step(_m, _d): mjwarp.step(m, d) def render(scene): xpos = d.xpos.numpy() xmat = d.xmat.numpy().reshape(NUM_ENVS, -1, 3, 3) scene.update_from_arrays( xpos, xmat, qpos=d.qpos.numpy(), qvel=d.qvel.numpy(), ctrl=d.ctrl.numpy() if model.nu > 0 else None, ) def reset(_m, _d): nd = mjwarp.put_data(model, data, nworld=NUM_ENVS) d.qpos.assign(nd.qpos.numpy()) d.qvel.assign(nd.qvel.numpy()) mjwarp.forward(m, d) Viewer(model, data, step_fn=step, render_fn=render, reset_fn=reset, num_envs=NUM_ENVS).run() ``` -------------------------------- ### ViserMujocoScene.create_visualization_gui Source: https://context7.com/mujocolab/mjviser/llms.txt Full tabbed GUI that adds Scene / Visualization / Groups tabs into the current Viser GUI context and returns the `GuiTabGroupHandle` so callers can append custom tabs. ```APIDOC ## `ViserMujocoScene.create_visualization_gui` — Full tabbed GUI Adds Scene / Visualization / Groups tabs into the current Viser GUI context and returns the `GuiTabGroupHandle` so callers can append custom tabs. ```python import viser import mujoco from mjviser import ViserMujocoScene model = mujoco.MjModel.from_xml_path("robot.xml") server = viser.ViserServer() scene = ViserMujocoScene(server, model, num_envs=1) tabs = scene.create_visualization_gui( camera_distance=3.0, camera_azimuth=135.0, camera_elevation=30.0, ) ``` ``` -------------------------------- ### Specify port with mjviser Source: https://github.com/mujocolab/mjviser/blob/main/README.md Run mjviser and bind it to a specific port using the --port flag. The default port is 8080. ```bash mjviser model.xml --port 7070 ``` -------------------------------- ### Viewer with Shared ViserServer Source: https://context7.com/mujocolab/mjviser/llms.txt Attaches the Viewer to a pre-existing ViserServer, allowing for additional GUI panels and controls to be added independently. ```python import mujoco import viser from mjviser import Viewer # --- Shared ViserServer (attach extra GUI panels) --- server = viser.ViserServer(port=7070) with server.gui.add_folder("My Panel"): btn = server.gui.add_button("Hello") Viewer(model, data, server=server).run() ``` -------------------------------- ### Create Full Tabbed GUI with ViserMujocoScene Source: https://context7.com/mujocolab/mjviser/llms.txt This function adds standard Scene, Visualization, and Groups tabs to the Viser GUI. It returns a `GuiTabGroupHandle` allowing you to append custom tabs. You can customize initial camera parameters. ```python import viser import mujoco from mjviser import ViserMujocoScene model = mujoco.MjModel.from_xml_path("robot.xml") server = viser.ViserServer() scene = ViserMujocoScene(server, model, num_envs=1) tabs = scene.create_visualization_gui( camera_distance=3.0, camera_azimuth=135.0, camera_elevation=30.0, ) ``` -------------------------------- ### Viewer with Custom Step Function Source: https://context7.com/mujocolab/mjviser/llms.txt Integrates a custom step function, such as applying random torques to the simulation. The provided `step_fn` replaces the default physics step. ```python import mujoco import viser from mjviser import Viewer import numpy as np # --- Custom step function (e.g. random torques) --- def random_torques(m: mujoco.MjModel, d: mujoco.MjData) -> None: d.ctrl[:] = np.random.uniform(-0.5, 0.5, size=m.nu) mujoco.mj_step(m, d) Viewer(model, data, step_fn=random_torques).run() ``` -------------------------------- ### ViserMujocoScene Source: https://context7.com/mujocolab/mjviser/llms.txt Low-level scene manager that handles all Viser geometry handles and visualization state. Use it directly when you want to own the simulation loop or build fully custom GUIs. ```APIDOC ## `ViserMujocoScene` — Low-level scene manager `ViserMujocoScene` handles all Viser geometry handles and visualization state. Use it directly when you want to own the simulation loop or build fully custom GUIs. ```python import time import mujoco import viser from mjviser import ViserMujocoScene model = mujoco.MjModel.from_xml_path("robot.xml") data = mujoco.MjData(model) server = viser.ViserServer() scene = ViserMujocoScene(server, model, num_envs=1) # Add the standard Scene / Visualization / Groups tab group. tabs = scene.create_visualization_gui() # Append a custom tab to the same tab group. with tabs.add_tab("My Controls"): slider = server.gui.add_slider("Gain", min=0.0, max=10.0, step=0.1, initial_value=1.0) step_dt = model.opt.timestep try: while True: t0 = time.perf_counter() mujoco.mj_step(model, data) scene.update_from_mjdata(data) # push state to browser elapsed = time.perf_counter() - t0 time.sleep(max(0.0, step_dt - elapsed)) except KeyboardInterrupt: server.stop() ``` ``` -------------------------------- ### Update Single-Env Scene from MjData Source: https://context7.com/mujocolab/mjviser/llms.txt This method updates the Viser scene with transforms from a `mujoco.MjData` object. It automatically handles mocap bodies and camera tracking. Enable contact point and force visualization by setting `show_contact_points` and `show_contact_forces` to `True`. ```python import mujoco import viser from mjviser import ViserMujocoScene model = mujoco.MjModel.from_xml_path("robot.xml") data = mujoco.MjData(model) mujoco.mj_forward(model, data) server = viser.ViserServer() scene = ViserMujocoScene(server, model, num_envs=1) # Enable contact-point visualization before the loop. scene.show_contact_points = True scene.show_contact_forces = True for _ in range(1000): mujoco.mj_step(model, data) scene.update_from_mjdata(data) # called every step or every N steps ``` -------------------------------- ### ViserMujocoScene.update_from_arrays Source: https://context7.com/mujocolab/mjviser/llms.txt Batched multi-env scene update that accepts pre-computed body transforms as NumPy arrays with shape `(num_envs, nbody, …)`. Optionally accepts `qpos`/`qvel`/`ctrl` for decor overlays (contacts, tendons) in the selected environment. ```APIDOC ## `ViserMujocoScene.update_from_arrays` — Batched multi-env scene update Accepts pre-computed body transforms as NumPy arrays with shape `(num_envs, nbody, …)`. Optionally accepts `qpos`/`qvel`/`ctrl` for decor overlays (contacts, tendons) in the selected environment. ```python import numpy as np import mujoco import viser from mjviser import ViserMujocoScene NUM_ENVS = 8 model = mujoco.MjModel.from_xml_path("robot.xml") server = viser.ViserServer() scene = ViserMujocoScene(server, model, num_envs=NUM_ENVS) scene.create_visualization_gui() # Simulate NUM_ENVS independently (plain MuJoCo here for illustration). datas = [mujoco.MjData(model) for _ in range(NUM_ENVS)] while True: for d in datas: mujoco.mj_step(model, d) xpos = np.stack([d.xpos for d in datas]) # (N, nbody, 3) xmat = np.stack([d.xmat.reshape(-1, 3, 3) for d in datas]) # (N, nbody, 3, 3) qpos = np.stack([d.qpos for d in datas]) qvel = np.stack([d.qvel for d in datas]) scene.update_from_arrays(xpos, xmat, qpos=qpos, qvel=qvel) ``` ``` -------------------------------- ### ViserMujocoScene.update_from_mjdata Source: https://context7.com/mujocolab/mjviser/llms.txt Single-env scene update that reads transforms directly from a `mujoco.MjData` object and pushes them to the Viser scene. Handles mocap bodies and camera tracking automatically. ```APIDOC ## `ViserMujocoScene.update_from_mjdata` — Single-env scene update Reads transforms directly from a `mujoco.MjData` object and pushes them to the Viser scene. Handles mocap bodies and camera tracking automatically. ```python import mujoco import viser from mjviser import ViserMujocoScene model = mujoco.MjModel.from_xml_path("robot.xml") data = mujoco.MjData(model) mujoco.mj_forward(model, data) server = viser.ViserServer() scene = ViserMujocoScene(server, model, num_envs=1) # Enable contact-point visualization before the loop. scene.show_contact_points = True scene.show_contact_forces = True for _ in range(1000): mujoco.mj_step(model, data) scene.update_from_mjdata(data) # called every step or every N steps ``` ``` -------------------------------- ### Viewer with Custom Render Function Source: https://context7.com/mujocolab/mjviser/llms.txt Utilizes a custom `render_fn` for per-frame rendering logic, such as overlaying ghosted models or updating multi-environment data. This function replaces the default scene update. ```python from collections import deque import mujoco import numpy as np import viser.transforms as vtf from mjviser import Viewer from mjviser.conversions import get_body_name, is_fixed_body, merge_geoms from mjviser.scene import ViserMujocoScene model = mujoco.MjModel.from_xml_path("robot.xml") data = mujoco.MjData(model) DELAY = 60 # frames history: deque = deque(maxlen=DELAY) ghost_handles: dict = {} def step(m, d): history.append((d.xpos.copy(), d.xmat.copy())) mujoco.mj_step(m, d) def render(scene: ViserMujocoScene): scene.update_from_mjdata(data) # normal render # Lazily build semi-transparent ghost handles. if not ghost_handles: for i in range(model.nbody): if is_fixed_body(model, i): continue geoms = [g for g in range(model.ngeom) if model.geom_bodyid[g] == i and model.geom_rgba[g, 3] > 0 and model.geom_group[g] <= 2] if not geoms: continue mesh = merge_geoms(model, geoms) name = get_body_name(model, i) ghost_handles[i] = scene.server.scene.add_batched_meshes_simple( f"/ghost/{name}", mesh.vertices, mesh.faces, batched_wxyzs=np.array([[1,0,0,0]], dtype=np.float32), batched_positions=np.zeros((1, 3), dtype=np.float32), batched_colors=np.array([100, 130, 230], dtype=np.uint8), opacity=0.5, cast_shadow=False, receive_shadow=False, ) if len(history) == DELAY: xpos, xmat = history[0] offset = scene._scene_offset xquat = vtf.SO3.from_matrix(xmat.reshape(-1, 3, 3)).wxyz for body_id, handle in ghost_handles.items(): handle.batched_positions = (xpos[body_id] + offset)[None].astype(np.float32) handle.batched_wxyzs = xquat[body_id][None].astype(np.float32) Viewer(model, data, step_fn=step, render_fn=render).run() ``` -------------------------------- ### Type Check Code with Pyright Source: https://github.com/mujocolab/mjviser/blob/main/CLAUDE.md Execute `make type` to perform static type checking on the codebase using Pyright. This helps catch type-related errors before runtime. ```sh make type # Type check (pyright) ``` -------------------------------- ### Control Overlay Visibility in ViserMujocoScene Source: https://context7.com/mujocolab/mjviser/llms.txt Programmatically enable or disable various visualization overlays like contact points, forces, tendons, inertia, actuators, and convex hulls. These properties directly control MuJoCo mjvOption flags and update the scene immediately. The frame mode can also be set to 'None', 'Body', 'Geom', or 'Site'. ```python import mujoco import viser from mjviser import ViserMujocoScene model = mujoco.MjModel.from_xml_path("robot.xml") data = mujoco.MjData(model) server = viser.ViserServer() scene = ViserMujocoScene(server, model, num_envs=1) # Enable overlays programmatically (also controllable via GUI). scene.show_contact_points = True scene.show_contact_forces = True scene.show_tendons = True # auto-enabled when model.ntendon > 0 scene.show_inertia = False scene.show_actuators = False scene.show_convex_hull = True # semi-transparent hull over mesh geoms # frame_mode: "None" | "Body" | "Geom" | "Site" scene.frame_mode = "Body" mujoco.mj_step(model, data) scene.update_from_mjdata(data) ``` -------------------------------- ### Reload Geometry with ViserMujocoScene.rebuild_visual_handles Source: https://context7.com/mujocolab/mjviser/llms.txt Call this method after modifying MuJoCo model geometry at runtime, such as swapping mesh assets or changing color properties. It removes and recreates all Viser geometry handles, re-uploading the geometry to the browser. ```python import mujoco import viser from mjviser import ViserMujocoScene model = mujoco.MjModel.from_xml_path("robot.xml") server = viser.ViserServer() scene = ViserMujocoScene(server, model, num_envs=1) # ... modify model geometry ... # e.g. model.geom_rgba[0] = [1, 0, 0, 1] scene.rebuild_visual_handles() # re-uploads geometry to the browser ``` === COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.