pymomentum.viser_vis

Viser Visualization Helpers

Helper functions for visualizing momentum characters in a viser web viewer, providing a live interactive 3D scene with per-frame updates.

Unlike the Rerun helpers (pymomentum.rerun_vis), viser is a live server — you add geometry once and then update transforms each frame. The typical usage pattern is:

import viser
import pymomentum.geometry as geo
from pymomentum.viser_vis import add_character, update_character

server = viser.ViserServer(host="::", port=8080)
character = geo.Character.load_urdf("robot.urdf")
skel_state = geo.joint_parameters_to_skeleton_state(character, joint_params)

handles = add_character(server, "character", character, skel_state)

# In your simulation loop:
for frame in frames:
    new_skel_state = geo.joint_parameters_to_skeleton_state(character, params)
    update_character(server, handles, character, new_skel_state)

Individual functions are also available:

Mesh wireframe and collision proxy visibility are controlled through viser’s built-in scene-tree panel (each is its own scene node), so no bespoke GUI toggle is provided — a single control avoids the desync between a custom checkbox and the scene tree’s client-side visibility override.

GUI widget helpers (optional, for apps building interactive viewers):

pymomentum.viser_vis.CM_TO_M: float = 0.01

Default scale factor to convert momentum units (cm) to meters for viser.

class pymomentum.viser_vis.CharacterHandles(entity_path: str = '', joint_frames: dict[str, ~typing.Any] = <factory>, bones_handle: ~typing.Any | None = None, mesh_handle: ~typing.Any | None = None, wireframe_handle: ~typing.Any | None = None, collision_root_handle: ~typing.Any | None = None, collision_handles: list[~pymomentum.viser_vis._CollisionCapsuleHandles | ~pymomentum.viser_vis._CollisionEllipsoidHandles | ~pymomentum.viser_vis._CollisionBoxHandles] = <factory>)

Bases: object

Opaque handle bag returned by add_character().

Pass this to update_character() or remove_character().

__init__(entity_path: str = '', joint_frames: dict[str, ~typing.Any] = <factory>, bones_handle: ~typing.Any | None = None, mesh_handle: ~typing.Any | None = None, wireframe_handle: ~typing.Any | None = None, collision_root_handle: ~typing.Any | None = None, collision_handles: list[~pymomentum.viser_vis._CollisionCapsuleHandles | ~pymomentum.viser_vis._CollisionEllipsoidHandles | ~pymomentum.viser_vis._CollisionBoxHandles] = <factory>) → None
bones_handle: Any | None = None
collision_handles: list[_CollisionCapsuleHandles | _CollisionEllipsoidHandles | _CollisionBoxHandles]
collision_root_handle: Any | None = None
entity_path: str = ''
joint_frames: dict[str, Any]
mesh_handle: Any | None = None
wireframe_handle: Any | None = None
class pymomentum.viser_vis.LogPanel(html_handle: Any, *, height_px: int = 80, max_lines: int = 50, echo_to_stdout: bool = True)

Bases: object

Scrollable HTML log strip created by add_log_panel().

Call log() to append a line. The panel keeps the last max_lines entries and auto-scrolls to the latest.

__init__(html_handle: Any, *, height_px: int = 80, max_lines: int = 50, echo_to_stdout: bool = True) → None
log(msg: str, level: str = 'info') → None

Append a log line. level is one of "info", "warning", "error".

pymomentum.viser_vis.add_character(server: viser.ViserServer, entity_path: str, character: pymomentum.geometry.Character, skel_state: np.ndarray, *, posed_mesh: pymomentum.geometry.Mesh | None = None, use_skinned_mesh: bool = False, color: tuple[int, int, int] = (200, 200, 200), scale: float = 0.01, axes_length: float = 0.05, axes_radius: float = 0.002, collision_geometry: bool = False, collision_geometry_visible: bool = True) → CharacterHandles

Add a complete character visualization to the viser scene.

Creates joint frames and optionally a mesh, returning handles for subsequent updates via update_character().

  • {entity_path}/joints/{name} — per-joint coordinate frames

  • {entity_path}/mesh — posed or skinned mesh

  • {entity_path}/collision_proxies — collision capsules, ellipsoids, and boxes, if requested

Parameters:
  • server – The viser server instance.

  • entity_path – Root scene-graph path for the character.

  • character – The character to visualize.

  • skel_state – Skeleton state array of shape (n_joints, 8).

  • posed_mesh – An optional pre-posed mesh for the legacy mesh path. If None and use_skinned_mesh is false, no mesh is added.

  • use_skinned_mesh – If true and the character has a skinned mesh, add a client-side skinned mesh instead of uploading posed vertices.

  • color – RGB color tuple for the mesh.

  • axes_length – Length of joint axis indicators.

  • axes_radius – Radius of joint axis indicators.

  • collision_geometry – If true, add collision proxy capsules, ellipsoids, and boxes.

  • collision_geometry_visible – Initial visibility for collision proxies.

Returns:

A CharacterHandles for use with update_character().

pymomentum.viser_vis.add_character_param_sliders(server: viser.ViserServer, character: pymomentum.geometry.Character, *, on_change: Callable[[Any], None] | None = None, initial_values: np.ndarray | None = None) → list[Any]

Build one slider per model parameter of character.

Unbounded parameter limits are clamped via _normalize_param_limit() so the sliders stay usable. The caller reads slider values from the returned list (e.g. np.array([s.value for s in sliders])) and converts them back into model parameters.

Parameters:
  • server – The viser server instance.

  • character – The character whose model parameters drive the sliders.

  • on_change – Optional callback wired to every slider’s on_update event. The callback receives a single positional argument (the viser event object), which it can ignore.

  • initial_values – Optional length-n_params array of initial slider values. Each value is clamped to its slider’s range. If None, all sliders start at 0 (or the closest in-range value).

Returns:

The list of slider handles, in parameter-index order.

pymomentum.viser_vis.add_collision_geometry(server: viser.ViserServer, entity_path: str, character: pymomentum.geometry.Character, skel_state: np.ndarray, *, scale: float = 0.01, color: tuple[int, int, int] = (128, 64, 64), opacity: float = 0.45, visible: bool = True) → tuple[Any, list[_CollisionPrimitiveHandles]]

Add collision proxy primitives to the viser scene.

Momentum stores tapered capsules, ellipsoids, and boxes. Capsules are rendered as a cylinder plus two end icospheres using the larger endpoint radius (viser does not expose tapered capsules); ellipsoids are rendered as a scaled icosphere and boxes as a box mesh.

Parameters:
  • server – The viser server instance.

  • entity_path – Scene-graph path for the collision proxy group.

  • character – The character whose collision geometry is visualized.

  • skel_state – Skeleton state array of shape (n_joints, 8).

  • scale – Unit conversion factor. Defaults to CM_TO_M.

  • color – RGB color tuple for the collision proxies.

  • opacity – Material opacity for the proxies.

  • visible – Initial visibility for the collision proxy group.

Returns:

A tuple of (root_handle, collision primitive handle groups), one handle group per primitive in collision-geometry order.

pymomentum.viser_vis.add_floor(server: viser.ViserServer, entity_path: str = '/ground', *, height: float = 0.0, up: str = 'z', width: float = 20.0, cell_size: float = 0.5, shadow_opacity: float = 0.3, plane_opacity: float = 0.5) → Any

Add a ground plane grid with shadow receiving to the viser scene.

Parameters:
  • server – The viser server instance.

  • entity_path – Scene-graph path for the grid node.

  • height – Position along the up axis for the ground plane.

  • up – The up axis — "y" for Y-up (glTF/FBX convention) or "z" for Z-up (URDF/robotics convention).

  • width – Width and depth of the grid (in scene units).

  • cell_size – Size of each grid cell.

  • shadow_opacity – Shadow darkness (0 = none, 1 = black).

  • plane_opacity – Ground plane opacity (0 = transparent, 1 = opaque).

Returns:

A viser grid handle.

pymomentum.viser_vis.add_joints(server: viser.ViserServer, entity_path: str, character: pymomentum.geometry.Character, skel_state: np.ndarray, *, scale: float = 0.01, axes_length: float = 0.05, axes_radius: float = 0.002, bone_line_width: float = 4.0, bone_color: tuple[int, int, int] = (180, 180, 180)) → tuple[dict[str, Any], Any | None]

Add per-joint coordinate frames and bone lines to the viser scene.

Each joint is rendered as a small set of RGB axes at its world-space position and orientation. Parent-child joints are connected by grey line segments.

Parameters:
  • server – The viser server instance.

  • entity_path – Base scene-graph path; joints are added as children.

  • character – The character whose skeleton is visualized.

  • skel_state – Skeleton state array of shape (n_joints, 8) with per-joint [tx, ty, tz, rx, ry, rz, rw, scale].

  • axes_length – Length of the axis indicators (in scene units).

  • axes_radius – Radius of the axis indicators.

  • bone_line_width – Width of the bone line segments.

  • bone_color – RGB color tuple for the bone lines.

Returns:

A tuple of (joint_handle_dict, bones_handle_or_None).

pymomentum.viser_vis.add_log_panel(server: viser.ViserServer, *, height_px: int = 80, max_lines: int = 50, echo_to_stdout: bool = True) → LogPanel

Add a scrollable HTML log strip to the GUI and return its controller.

Parameters:
  • server – The viser server instance.

  • height_px – Fixed height of the log strip.

  • max_lines – Maximum number of lines retained on screen.

  • echo_to_stdout – If True (default), each log() call also prints to stdout for terminal visibility.

Returns:

A LogPanel whose .log(msg, level) method appends lines.

pymomentum.viser_vis.add_mesh(server: viser.ViserServer, entity_path: str, mesh: pymomentum.geometry.Mesh, *, color: tuple[int, int, int] = (200, 200, 200), scale: float = 0.01) → tuple[Any, Any]

Add a Mesh to the viser scene.

Creates both a solid mesh and a hidden wireframe mesh for toggling.

Parameters:
  • server – The viser server instance.

  • entity_path – Scene-graph path for the mesh node.

  • mesh – The mesh to add (typically from pose_mesh()).

  • color – RGB color tuple (r, g, b) in 0–255.

  • scale – Unit conversion factor applied to vertex positions. Defaults to CM_TO_M (0.01) to convert momentum’s cm to meters.

Returns:

A tuple of (solid_handle, wireframe_handle). The solid handle is a list of uniformly colored submesh handles when per-vertex colors are present.

pymomentum.viser_vis.add_skinned_mesh(server: viser.ViserServer, entity_path: str, character: pymomentum.geometry.Character, skel_state: np.ndarray, *, color: tuple[int, int, int] = (200, 200, 200), scale: float = 0.01) → Any

Add a client-side skinned mesh to the viser scene.

Unlike add_mesh(), this sends rest vertices, faces, skin weights, and bind-pose bones once. Subsequent animation only needs update_skinned_mesh(), which updates per-joint bone transforms rather than re-uploading all posed vertex positions.

Parameters:
  • server – The viser server instance.

  • entity_path – Scene-graph path for the mesh node.

  • character – Character with a mesh, skin weights, and bind pose.

  • skel_state – Initial skeleton state array of shape (n_joints, 8).

  • color – RGB color tuple (r, g, b) in 0–255.

  • scale – Unit conversion factor applied to vertices and bone translations. Defaults to CM_TO_M.

Returns:

The viser skinned mesh handle. When per-vertex colors are present, returns a list of uniformly colored skinned submesh handles.

pymomentum.viser_vis.can_use_skinned_mesh(character: pymomentum.geometry.Character) → bool

Return whether Viser client-side skinning can represent character.

Viser uses at most four skinning influences per vertex. Characters with more influences should use the posed-vertex mesh path to preserve the original deformation.

Parameters:

character – Character to inspect.

Returns:

True if the character has a mesh and no vertex has more than four nonzero skin weights.

pymomentum.viser_vis.max_skin_influences(character: pymomentum.geometry.Character) → int

Return the maximum nonzero skinning influences per vertex for character.

Parameters:

character – Character to inspect.

Returns:

The largest number of nonzero skin weights on any mesh vertex, or zero if the character has no skinned mesh.

pymomentum.viser_vis.remove_character(handles: CharacterHandles, server: viser.ViserServer | None = None) → None

Remove all character scene nodes from the viser scene.

Parameters:
  • handles – The handles returned by add_character().

  • server – If provided, removes the entire scene subtree by name (cleans up parent nodes from viser’s scene tree).

pymomentum.viser_vis.update_character(server: viser.ViserServer, handles: CharacterHandles, character: pymomentum.geometry.Character, skel_state: np.ndarray, *, posed_mesh: pymomentum.geometry.Mesh | None = None, scale: float = 0.01) → None

Update all character transforms in a single atomic batch.

Wraps updates in server.atomic() to avoid partial-frame rendering artifacts, following the pattern used in rl_robot’s viser renderer.

Parameters:
  • server – The viser server instance.

  • handles – The handles returned by add_character().

  • character – The character being updated.

  • skel_state – New skeleton state array of shape (n_joints, 8).

  • posed_mesh – If provided, updates mesh vertex positions.

  • scale – Unit conversion factor. Defaults to CM_TO_M.

pymomentum.viser_vis.update_collision_geometry(collision_handles: list[_CollisionPrimitiveHandles], character: pymomentum.geometry.Character, skel_state: np.ndarray, *, scale: float = 0.01) → None

Update collision proxy positions and sizes.

Always repositions every primitive from the current skel_state, regardless of visibility, so the proxies stay in sync with the model. Visibility is a separate concern controlled through viser’s scene-tree panel; gating updates on visibility would leave proxies stranded at a stale pose when later revealed through a path that does not re-run this.

Parameters:
  • collision_handles – Collision primitive handles returned by add_collision_geometry().

  • character – The character whose collision geometry is visualized.

  • skel_state – Skeleton state array of shape (n_joints, 8).

  • scale – Unit conversion factor. Defaults to CM_TO_M.

Returns:

None.

pymomentum.viser_vis.update_joints(handles: dict[str, Any], bones_handle: Any | None, character: pymomentum.geometry.Character, skel_state: np.ndarray, *, scale: float = 0.01) → None

Update joint frame positions, orientations, and bone lines.

Parameters:
  • handles – The joint handle dict returned by add_joints().

  • bones_handle – The bones handle returned by add_joints().

  • character – The character whose skeleton is being updated.

  • skel_state – New skeleton state array of shape (n_joints, 8).

  • scale – Unit conversion factor. Defaults to CM_TO_M.

pymomentum.viser_vis.update_joints_transforms(handles: dict[str, Any], bones_handle: Any | None, character: pymomentum.geometry.Character, positions: np.ndarray, wxyzs: np.ndarray, bone_segments: np.ndarray | None = None) → None

Update joint frames from precomputed scene-space transforms.

Parameters:
  • handles – The joint handle dict returned by add_joints().

  • bones_handle – The bones handle returned by add_joints().

  • character – The character whose skeleton is being updated.

  • positions – Joint positions with shape (n_joints, 3) in scene units.

  • wxyzs – Joint orientations with shape (n_joints, 4) in Viser’s wxyz quaternion order.

  • bone_segments – Optional precomputed bone line segments with shape (n_bones, 2, 3) in scene units.

pymomentum.viser_vis.update_mesh(mesh_handle: Any, wireframe_handle: Any | None, mesh: pymomentum.geometry.Mesh, *, scale: float = 0.01) → None

Update vertex positions of existing mesh handles.

Parameters:
  • mesh_handle – The solid mesh handle from add_mesh().

  • wireframe_handle – The wireframe handle from add_mesh().

  • mesh – The new posed mesh.

  • scale – Unit conversion factor. Defaults to CM_TO_M.

pymomentum.viser_vis.update_mesh_vertices(mesh_handle: Any, wireframe_handle: Any | None, vertices: ndarray, *, scale: float = 0.01) → None

Update vertex positions of existing mesh handles from a vertex array.

Parameters:
  • mesh_handle – The solid mesh handle from add_mesh().

  • wireframe_handle – The wireframe handle from add_mesh().

  • vertices – The new vertex positions.

  • scale – Unit conversion factor. Defaults to CM_TO_M.

pymomentum.viser_vis.update_skinned_mesh(skinned_mesh_handle: Any, skel_state: ndarray, *, scale: float = 0.01) → None

Update a skinned mesh by sending only bone transforms.

Parameters:
  • skinned_mesh_handle – The handle returned by add_skinned_mesh(). This may be a list of uniformly colored skinned submesh handles.

  • skel_state – Skeleton state array of shape (n_joints, 8).

  • scale – Unit conversion factor for bone translations.

pymomentum.viser_vis.update_skinned_mesh_transforms(skinned_mesh_handle: Any, positions: ndarray, wxyzs: ndarray) → None

Update a skinned mesh from precomputed bone transforms.

Parameters:
  • skinned_mesh_handle – The handle returned by add_skinned_mesh(). This may be a list of uniformly colored skinned submesh handles.

  • positions – Bone positions with shape (n_joints, 3) in scene units.

  • wxyzs – Bone orientations with shape (n_joints, 4) in Viser’s wxyz quaternion order.