Scene pointer events.ΒΆ
This example shows how to use scene pointer events to specify rays, and how they can be used to interact with the scene (e.g., ray-mesh intersections).
To get the demo data, see ./assets/download_dragon_mesh.sh
.
1from __future__ import annotations
2
3import time
4from pathlib import Path
5from typing import cast
6
7import numpy as np
8import trimesh
9import trimesh.creation
10import trimesh.ray
11
12import viser
13import viser.transforms as tf
14from viser.theme import TitlebarConfig
15
16server = viser.ViserServer()
17server.gui.configure_theme(
18 brand_color=(130, 0, 150),
19 titlebar_content=TitlebarConfig(buttons=(), image=None),
20)
21server.scene.set_up_direction("+y")
22
23mesh = cast(
24 trimesh.Trimesh, trimesh.load_mesh(str(Path(__file__).parent / "assets/dragon.obj"))
25)
26mesh.apply_scale(0.05)
27
28mesh_handle = server.scene.add_mesh_trimesh(
29 name="/mesh",
30 mesh=mesh,
31 position=(0.0, 0.0, 0.0),
32)
33
34hit_pos_handles: list[viser.GlbHandle] = []
35
36
37# Buttons + callbacks will operate on a per-client basis, but will modify the global scene! :)
38@server.on_client_connect
39def _(client: viser.ClientHandle) -> None:
40 # Set up the camera -- this gives a nice view of the full mesh.
41 client.camera.position = np.array([0.0, 0.0, -10.0])
42 client.camera.wxyz = np.array([0.0, 0.0, 0.0, 1.0])
43
44 # Tests "click" scenepointerevent.
45 click_button_handle = client.gui.add_button("Add sphere", icon=viser.Icon.POINTER)
46
47 @click_button_handle.on_click
48 def _(_):
49 click_button_handle.disabled = True
50
51 @client.scene.on_pointer_event(event_type="click")
52 def _(event: viser.ScenePointerEvent) -> None:
53 # Check for intersection with the mesh, using trimesh's ray-mesh intersection.
54 # Note that mesh is in the mesh frame, so we need to transform the ray.
55 R_world_mesh = tf.SO3(mesh_handle.wxyz)
56 R_mesh_world = R_world_mesh.inverse()
57 origin = (R_mesh_world @ np.array(event.ray_origin)).reshape(1, 3)
58 direction = (R_mesh_world @ np.array(event.ray_direction)).reshape(1, 3)
59 intersector = trimesh.ray.ray_triangle.RayMeshIntersector(mesh)
60 hit_pos, _, _ = intersector.intersects_location(origin, direction)
61
62 if len(hit_pos) == 0:
63 return
64 client.scene.remove_pointer_callback()
65
66 # Get the first hit position (based on distance from the ray origin).
67 hit_pos = hit_pos[np.argmin(np.sum((hit_pos - origin) ** 2, axis=-1))]
68
69 # Create a sphere at the hit location.
70 hit_pos_mesh = trimesh.creation.icosphere(radius=0.1)
71 hit_pos_mesh.vertices += R_world_mesh @ hit_pos
72 hit_pos_mesh.visual.vertex_colors = (0.5, 0.0, 0.7, 1.0) # type: ignore
73 hit_pos_handle = server.scene.add_mesh_trimesh(
74 name=f"/hit_pos_{len(hit_pos_handles)}", mesh=hit_pos_mesh
75 )
76 hit_pos_handles.append(hit_pos_handle)
77
78 @client.scene.on_pointer_callback_removed
79 def _():
80 click_button_handle.disabled = False
81
82 # Tests "rect-select" scenepointerevent.
83 paint_button_handle = client.gui.add_button("Paint mesh", icon=viser.Icon.PAINT)
84
85 @paint_button_handle.on_click
86 def _(_):
87 paint_button_handle.disabled = True
88
89 @client.scene.on_pointer_event(event_type="rect-select")
90 def _(message: viser.ScenePointerEvent) -> None:
91 client.scene.remove_pointer_callback()
92
93 global mesh_handle
94 camera = message.client.camera
95
96 # Put the mesh in the camera frame.
97 R_world_mesh = tf.SO3(mesh_handle.wxyz)
98 R_mesh_world = R_world_mesh.inverse()
99 R_camera_world = tf.SE3.from_rotation_and_translation(
100 tf.SO3(camera.wxyz), camera.position
101 ).inverse()
102 vertices = cast(np.ndarray, mesh.vertices)
103 vertices = (R_mesh_world.as_matrix() @ vertices.T).T
104 vertices = (
105 R_camera_world.as_matrix()
106 @ np.hstack([vertices, np.ones((vertices.shape[0], 1))]).T
107 ).T[:, :3]
108
109 # Get the camera intrinsics, and project the vertices onto the image plane.
110 fov, aspect = camera.fov, camera.aspect
111 vertices_proj = vertices[:, :2] / vertices[:, 2].reshape(-1, 1)
112 vertices_proj /= np.tan(fov / 2)
113 vertices_proj[:, 0] /= aspect
114
115 # Move the origin to the upper-left corner, and scale to [0, 1].
116 # ... make sure to match the OpenCV's image coordinates!
117 vertices_proj = (1 + vertices_proj) / 2
118
119 # Select the vertices that lie inside the 2D selected box, once projected.
120 mask = (
121 (vertices_proj > np.array(message.screen_pos[0]))
122 & (vertices_proj < np.array(message.screen_pos[1]))
123 ).all(axis=1)[..., None]
124
125 # Update the mesh color based on whether the vertices are inside the box
126 mesh.visual.vertex_colors = np.where( # type: ignore
127 mask, (0.5, 0.0, 0.7, 1.0), (0.9, 0.9, 0.9, 1.0)
128 )
129 mesh_handle = server.scene.add_mesh_trimesh(
130 name="/mesh",
131 mesh=mesh,
132 position=(0.0, 0.0, 0.0),
133 )
134
135 @client.scene.on_pointer_callback_removed
136 def _():
137 paint_button_handle.disabled = False
138
139 # Button to clear spheres.
140 clear_button_handle = client.gui.add_button("Clear scene", icon=viser.Icon.X)
141
142 @clear_button_handle.on_click
143 def _(_):
144 """Reset the mesh color and remove all click-generated spheres."""
145 global mesh_handle
146 for handle in hit_pos_handles:
147 handle.remove()
148 hit_pos_handles.clear()
149 mesh.visual.vertex_colors = (0.9, 0.9, 0.9, 1.0) # type: ignore
150 mesh_handle = server.scene.add_mesh_trimesh(
151 name="/mesh",
152 mesh=mesh,
153 position=(0.0, 0.0, 0.0),
154 )
155
156
157while True:
158 time.sleep(10.0)