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:
add_mesh()/update_mesh()— manage aMeshadd_skinned_mesh()/update_skinned_mesh()— upload mesh data once and update only bone transformsadd_joints()/update_joints()— manage skeleton joint framesadd_collision_geometry()/update_collision_geometry()— manage collision proxy capsules, ellipsoids, and boxesadd_character()/update_character()/remove_character()— manage a complete character
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):
add_character_param_sliders()— one slider per model parameteradd_log_panel()— scrollable HTML log strip (returns aLogPanel)
- 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:
objectOpaque handle bag returned by
add_character().Pass this to
update_character()orremove_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:
objectScrollable HTML log strip created by
add_log_panel().Call
log()to append a line. The panel keeps the lastmax_linesentries 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
CharacterHandlesfor use withupdate_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_updateevent. The callback receives a single positional argument (the viser event object), which it can ignore.initial_values – Optional length-
n_paramsarray 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), eachlog()call also prints tostdoutfor terminal visibility.
- Returns:
A
LogPanelwhose.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
Meshto 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 needsupdate_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:
Trueif 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 inrl_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’swxyzquaternion 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’swxyzquaternion order.